mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-09-04 02:01:48 +03:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61b2f169a8 | ||
|
|
54e5c63b7e | ||
|
|
41477c2510 | ||
|
|
0573cefa3b |
+10
@@ -49,6 +49,16 @@ def _isolate_sessions_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> I
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _isolate_pairing_store(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Keep channel pairing tests out of the user's active pairing store."""
|
||||||
|
pairing_path = tmp_path / "pairing.json"
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.pairing.store._store_path",
|
||||||
|
lambda: pairing_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
def _use_windows_system_ca_for_default_http_clients() -> Iterator[None]:
|
def _use_windows_system_ca_for_default_http_clients() -> Iterator[None]:
|
||||||
"""Avoid reparsing certifi's CA bundle for every offline HTTP client.
|
"""Avoid reparsing certifi's CA bundle for every offline HTTP client.
|
||||||
|
|||||||
@@ -115,19 +115,24 @@ def generate_code(
|
|||||||
sender_id: str,
|
sender_id: str,
|
||||||
ttl: int = _TTL_DEFAULT_S,
|
ttl: int = _TTL_DEFAULT_S,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create a new pairing code for *sender_id* on *channel*.
|
"""Return an active pairing code for *sender_id* on *channel*.
|
||||||
|
|
||||||
Returns the code (e.g. ``"ABCD-EFGH"``).
|
Returns the code (e.g. ``"ABCD-EFGH"``).
|
||||||
"""
|
"""
|
||||||
with _LOCK:
|
with _LOCK:
|
||||||
data = _load()
|
data = _load()
|
||||||
_gc_pending(data)
|
_gc_pending(data)
|
||||||
|
sender = str(sender_id)
|
||||||
|
for code, info in data.get("pending", {}).items():
|
||||||
|
if info["channel"] == channel and str(info["sender_id"]) == sender:
|
||||||
|
return code
|
||||||
|
|
||||||
raw = "".join(secrets.choice(_ALPHABET) for _ in range(_CODE_LENGTH))
|
raw = "".join(secrets.choice(_ALPHABET) for _ in range(_CODE_LENGTH))
|
||||||
code = f"{raw[:4]}-{raw[4:]}"
|
code = f"{raw[:4]}-{raw[4:]}"
|
||||||
|
|
||||||
data.setdefault("pending", {})[code] = {
|
data.setdefault("pending", {})[code] = {
|
||||||
"channel": channel,
|
"channel": channel,
|
||||||
"sender_id": str(sender_id),
|
"sender_id": sender,
|
||||||
"created_at": time.time(),
|
"created_at": time.time(),
|
||||||
"expires_at": time.time() + ttl,
|
"expires_at": time.time() + ttl,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ class TestGenerateCode:
|
|||||||
codes = {store.generate_code("telegram", str(i)) for i in range(20)}
|
codes = {store.generate_code("telegram", str(i)) for i in range(20)}
|
||||||
assert len(codes) == 20
|
assert len(codes) == 20
|
||||||
|
|
||||||
|
def test_reuses_active_code_for_same_sender(self) -> None:
|
||||||
|
first = store.generate_code("telegram", "123")
|
||||||
|
|
||||||
|
assert store.generate_code("telegram", "123") == first
|
||||||
|
assert len(store.list_pending()) == 1
|
||||||
|
|
||||||
|
def test_scopes_reused_codes_to_channel(self) -> None:
|
||||||
|
telegram = store.generate_code("telegram", "123")
|
||||||
|
discord = store.generate_code("discord", "123")
|
||||||
|
|
||||||
|
assert telegram != discord
|
||||||
|
assert len(store.list_pending()) == 2
|
||||||
|
|
||||||
def test_ttl_expiration(self, monkeypatch) -> None:
|
def test_ttl_expiration(self, monkeypatch) -> None:
|
||||||
clock = {"now": 1_000.0}
|
clock = {"now": 1_000.0}
|
||||||
monkeypatch.setattr(store.time, "time", lambda: clock["now"])
|
monkeypatch.setattr(store.time, "time", lambda: clock["now"])
|
||||||
|
|||||||
+10
-10
@@ -5,7 +5,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "@nanobot/tui",
|
"name": "@nanobot/tui",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentui/core": "0.5.3",
|
"@opentui/core": "0.5.10",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.13",
|
"@types/bun": "^1.3.13",
|
||||||
@@ -14,23 +14,23 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@opentui/core": ["@opentui/core@0.5.3", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.3", "@opentui/core-darwin-x64": "0.5.3", "@opentui/core-linux-arm64": "0.5.3", "@opentui/core-linux-arm64-musl": "0.5.3", "@opentui/core-linux-x64": "0.5.3", "@opentui/core-linux-x64-musl": "0.5.3", "@opentui/core-win32-arm64": "0.5.3", "@opentui/core-win32-x64": "0.5.3" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-K8EQu44cx0rhnn3v3baCQW18Bpci3GltZayOwVpGGsbiAGL1WUYqwQjuaWsmS0c4dCa9rQ5xCEoHB1C4936nDg=="],
|
"@opentui/core": ["@opentui/core@0.5.10", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.10", "@opentui/core-darwin-x64": "0.5.10", "@opentui/core-linux-arm64": "0.5.10", "@opentui/core-linux-arm64-musl": "0.5.10", "@opentui/core-linux-x64": "0.5.10", "@opentui/core-linux-x64-musl": "0.5.10", "@opentui/core-win32-arm64": "0.5.10", "@opentui/core-win32-x64": "0.5.10" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-C3a2UbmefeAjIxAgm4BqjuSxKT4oqutfvYFwVvUgMxmGRHkNbBc/s7sukV0JgwcxFcV3uMFrXxo+E+BQtvuOiw=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-R39YeUqaMb/rH1h6G4MkB4MLVKIrRaUaXLfVqorZM4xgU5BxnfPetRk1vWR9vuLCvDwskg+kQ589kULw0o6AWA=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Vyb+nTbhab8ZcRy5gg1loEEGwRcIbjAeVRIBfHBcbFDqmITBOg7x2gqJ+x/TnoOy4uwMhCmICUN2wiyREw3r1Q=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-1pmUas/chTVFGeiN19kaOx+5Xbte/DLhcgKyACwWO0M3+xE3z1v/6QGSyX6CoP5HBpmDroiX+JHv1ic/JlGd/g=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tTFLcM7Oj1gTyhm/bUdAt3C6grZdCxPk6+/g2azcZBUlI3/62LwbeRS6HbQKFFmm+1fUmX8cq6kWrtul885mVg=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nMo9Q9VIaQVdw2SNKlwIEWMmf3z+cI4jRdCkh36e2RU1FO7LrIBAEmV1ZuRp1CIFVGPkqCXIizCeckZHTr4yQ=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-ncJXcgudhBf2GdJyF3xVQN/Ec+1F7GOL+pRrURmgBYSj2v1w6EyoDQFAACtPTK2c3R38W6fvZwL4JSLlm4EFXQ=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-QOYAxbbWrhYo27Cd6m0ATzpEx9YCAKAq82LgfUn0xu+VKXLNu+Q3hMNSVbG0SepUQQZil5rz228R9o9Cs8995w=="],
|
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-dGMphDKexSdeYqwl0wgoFBP88Ta/cdi1Zc1mk29/ENkSCGz+74zlCHgqTHRNGLmI8W5TfuUtCyktQH11/Z+TBQ=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.3", "", { "os": "linux", "cpu": "x64" }, "sha512-hdAYLriLpTj3lvpMyL25GPBzvM2w/n2KCSbIwTmgS2F/dPZYCKJxHEETj2lCvtStSp7KuY8tkg3Xl5RAq1v7gA=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-5qtYaOgwVycZD1GaGshTRsi0rXPAmVExO03N1JQaHu+NYxK/vXSOc7Bu4QW0sPXx3Sp0SpzpP+FHjXABfoK66g=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.3", "", { "os": "linux", "cpu": "x64" }, "sha512-BkVIiPQ1TOf5/FfmIpf7DQU5rT/FO6ASW5R/o/wonI5Pdul7XiDCu86gzGyk1x5k9Sbh6GLeq1fe8/tPmI7IaA=="],
|
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-Oj4H9hApuvuTKPWxh4SoZAgGJorR7vbvnrZA/cAkSMAk2VGSoHRRcqeXQbcH8IcdjVZ0KFpv8Zkl/D5Ye+2mew=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-AjObTyZPU0xsK3Yk8GmhkboK6OcMoHBbydqYAybeHD4+v6axScSuZ3OEI9J05JJ9T7H2nZNky75tsdnjsvZJmg=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-A9VhgvTxQoUdZ+8LmUumEng1sQNbj9QQQT3NYG9mSxI54qTANi7vOWNSphMiY6RMVsr22pgm6nUvSSvJXv7Jog=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.3", "", { "os": "win32", "cpu": "x64" }, "sha512-e3nRlF2nSkLKCUPBF32OL9EDgtQDIh2pBo7tjhumpTyJ3qoNOa3us7DsM290Vw4xnakM6jpk9r9NzRf72CuMVg=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.10", "", { "os": "win32", "cpu": "x64" }, "sha512-u3KHa7kEeWrmKVDRJYpxSGO+g5E9cMGlrmTsPN3GVPHUmQMiREUawLXUvsU8+IHaQnqG3Q5nuE1yf4fPBzS+Qw=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentui/core": "0.5.3"
|
"@opentui/core": "0.5.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.13",
|
"@types/bun": "^1.3.13",
|
||||||
|
|||||||
+88
-1
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "@opentui/core"
|
} from "@opentui/core"
|
||||||
import {
|
import {
|
||||||
MockTreeSitterClient,
|
MockTreeSitterClient,
|
||||||
|
TestRecorder,
|
||||||
createTestRenderer,
|
createTestRenderer,
|
||||||
type TestRendererSetup,
|
type TestRendererSetup,
|
||||||
} from "@opentui/core/testing"
|
} from "@opentui/core/testing"
|
||||||
@@ -491,6 +492,17 @@ describe("NanobotTui layout", () => {
|
|||||||
expect(ui.composer.plainText).toContain("replacement")
|
expect(ui.composer.plainText).toContain("replacement")
|
||||||
expect(ui.composer.plainText).not.toContain("Image #1")
|
expect(ui.composer.plainText).not.toContain("Image #1")
|
||||||
|
|
||||||
|
ui.composer.setText("")
|
||||||
|
setup.mockInput.pressKey("v", { ctrl: true })
|
||||||
|
await waitUntil(() => ui.composer.plainText === "[Image #1] ")
|
||||||
|
ui.composer.cursorOffset = 10
|
||||||
|
setup.mockInput.pressArrow("left", { shift: true })
|
||||||
|
await waitUntil(() => ui.composer.cursorOffset === 0)
|
||||||
|
await setup.mockInput.typeText("replacement")
|
||||||
|
await waitUntil(() => ui.draft.imageCount === 0)
|
||||||
|
expect(ui.composer.plainText).toContain("replacement")
|
||||||
|
expect(ui.composer.plainText).not.toContain("Image #1")
|
||||||
|
|
||||||
ui.composer.setText("")
|
ui.composer.setText("")
|
||||||
setup.mockInput.pressKey("v", { ctrl: true })
|
setup.mockInput.pressKey("v", { ctrl: true })
|
||||||
await waitUntil(() => ui.composer.plainText === "[Image #1] ")
|
await waitUntil(() => ui.composer.plainText === "[Image #1] ")
|
||||||
@@ -2052,6 +2064,80 @@ describe("NanobotTui layout", () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("keeps streamed fenced code visible while completing the response", async () => {
|
||||||
|
setup = await createRenderer({ width: 100, height: 30, screenMode: "alternate-screen" })
|
||||||
|
const app = mount(setup)
|
||||||
|
const response = [
|
||||||
|
"Commit types:",
|
||||||
|
"",
|
||||||
|
"```text",
|
||||||
|
"feat:",
|
||||||
|
"fix:",
|
||||||
|
"perf:",
|
||||||
|
"docs:",
|
||||||
|
"test:",
|
||||||
|
"refactor:",
|
||||||
|
"chore:",
|
||||||
|
"```",
|
||||||
|
"",
|
||||||
|
"Include the reason in the body.",
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
|
app.accept({ event: "attached", chat_id: "chat" })
|
||||||
|
app.accept({ event: "delta", chat_id: "chat", text: response })
|
||||||
|
await setup.flush()
|
||||||
|
expect(setup.captureCharFrame()).toContain("feat:")
|
||||||
|
|
||||||
|
const recorder = new TestRecorder(setup.renderer)
|
||||||
|
recorder.rec()
|
||||||
|
app.accept({ event: "stream_end", chat_id: "chat" })
|
||||||
|
app.accept({ event: "turn_end", chat_id: "chat" })
|
||||||
|
await setup.flush()
|
||||||
|
recorder.stop()
|
||||||
|
|
||||||
|
expect(recorder.recordedFrames.length).toBeGreaterThan(0)
|
||||||
|
expect(recorder.recordedFrames.every(({ frame }) => frame.includes("feat:"))).toBeTrue()
|
||||||
|
expect(recorder.recordedFrames.every(({ frame }) => (
|
||||||
|
frame.includes("Include the reason in the body.")
|
||||||
|
))).toBeTrue()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("renders fenced plain text from light-theme history", async () => {
|
||||||
|
setup = await createRenderer({ width: 100, height: 30, screenMode: "alternate-screen" })
|
||||||
|
const app = NanobotTui.mount(
|
||||||
|
setup.renderer,
|
||||||
|
{ ...options, theme: "light" },
|
||||||
|
client(),
|
||||||
|
new MockTreeSitterClient({ autoResolveTimeout: 0 }),
|
||||||
|
)
|
||||||
|
const response = [
|
||||||
|
"Commit types:",
|
||||||
|
"",
|
||||||
|
"```text",
|
||||||
|
"feat:",
|
||||||
|
"fix:",
|
||||||
|
"perf:",
|
||||||
|
"docs:",
|
||||||
|
"test:",
|
||||||
|
"refactor:",
|
||||||
|
"chore:",
|
||||||
|
"```",
|
||||||
|
"",
|
||||||
|
"Include the reason in the body.",
|
||||||
|
].join("\n")
|
||||||
|
const transcript = (app as unknown as { transcript: Transcript }).transcript
|
||||||
|
|
||||||
|
transcript.history([{ role: "assistant", content: response }])
|
||||||
|
await setup.flush()
|
||||||
|
|
||||||
|
const code = setup.captureSpans().lines
|
||||||
|
.flatMap((line) => line.spans)
|
||||||
|
.find((span) => span.text.includes("feat:"))
|
||||||
|
|
||||||
|
expect(setup.captureCharFrame()).toContain("feat:")
|
||||||
|
expect(code?.fg.toInts().slice(0, 3)).toEqual([24, 24, 27])
|
||||||
|
})
|
||||||
|
|
||||||
test("renders assistant LaTeX as Unicode text without changing code", async () => {
|
test("renders assistant LaTeX as Unicode text without changing code", async () => {
|
||||||
setup = await createRenderer({ width: 96, height: 24, screenMode: "alternate-screen" })
|
setup = await createRenderer({ width: 96, height: 24, screenMode: "alternate-screen" })
|
||||||
const app = mount(setup)
|
const app = mount(setup)
|
||||||
@@ -2102,7 +2188,7 @@ describe("NanobotTui layout", () => {
|
|||||||
syntaxStyle: { getStyle(name: string): { fg?: { toInts(): number[] } } | undefined } | null
|
syntaxStyle: { getStyle(name: string): { fg?: { toInts(): number[] } } | undefined } | null
|
||||||
}
|
}
|
||||||
transcript: {
|
transcript: {
|
||||||
markdown: Set<{ syntaxStyle: object }>
|
markdown: Set<{ fg?: { toInts(): number[] }; syntaxStyle: object }>
|
||||||
frames: Set<{ borderColor: { toInts(): number[] } }>
|
frames: Set<{ borderColor: { toInts(): number[] } }>
|
||||||
userRows: Set<{ backgroundColor: { intent: string; toInts(): number[] } }>
|
userRows: Set<{ backgroundColor: { intent: string; toInts(): number[] } }>
|
||||||
userMessages: Set<{ renderable: TextRenderable }>
|
userMessages: Set<{ renderable: TextRenderable }>
|
||||||
@@ -2136,6 +2222,7 @@ describe("NanobotTui layout", () => {
|
|||||||
expect(internals.composer.textColor.toInts().slice(0, 3)).toEqual([24, 24, 27])
|
expect(internals.composer.textColor.toInts().slice(0, 3)).toEqual([24, 24, 27])
|
||||||
expect(sessionFrame?.borderColor.toInts().slice(0, 3)).toEqual([212, 212, 216])
|
expect(sessionFrame?.borderColor.toInts().slice(0, 3)).toEqual([212, 212, 216])
|
||||||
expect(userRow?.backgroundColor.toInts().slice(0, 3)).toEqual([240, 240, 240])
|
expect(userRow?.backgroundColor.toInts().slice(0, 3)).toEqual([240, 240, 240])
|
||||||
|
expect(markdown?.fg?.toInts().slice(0, 3)).toEqual([24, 24, 27])
|
||||||
expect(markdown?.syntaxStyle).not.toBe(darkSyntax)
|
expect(markdown?.syntaxStyle).not.toBe(darkSyntax)
|
||||||
expect(internals.composer.syntaxStyle).not.toBe(darkComposerSyntax)
|
expect(internals.composer.syntaxStyle).not.toBe(darkComposerSyntax)
|
||||||
expect(internals.composer.syntaxStyle?.getStyle("image.placeholder")?.fg?.toInts().slice(0, 3))
|
expect(internals.composer.syntaxStyle?.getStyle("image.placeholder")?.fg?.toInts().slice(0, 3))
|
||||||
|
|||||||
+17
-3
@@ -1738,16 +1738,30 @@ export class NanobotTui {
|
|||||||
key.preventDefault()
|
key.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!key.ctrl && !key.meta && !key.shift && (key.name === "left" || key.name === "right")) {
|
if (!key.ctrl && !key.meta && (key.name === "left" || key.name === "right")) {
|
||||||
const direction = key.name === "left" ? -1 : 1
|
const direction = key.name === "left" ? -1 : 1
|
||||||
|
const cursor = this.composerStringCursor()
|
||||||
const target = this.draft.moveImageCursor(
|
const target = this.draft.moveImageCursor(
|
||||||
this.composer.plainText,
|
this.composer.plainText,
|
||||||
this.composerStringCursor(),
|
cursor,
|
||||||
direction,
|
direction,
|
||||||
)
|
)
|
||||||
if (target !== null) {
|
if (target !== null) {
|
||||||
this.composerCursor = target
|
this.composerCursor = target
|
||||||
this.setComposerStringCursor(this.composer.plainText, target)
|
if (key.shift) {
|
||||||
|
const cursorOffset = this.composerOffsetForStringIndex(this.composer.plainText, cursor)
|
||||||
|
const targetOffset = this.composerOffsetForStringIndex(this.composer.plainText, target)
|
||||||
|
this.composer.setSelection(
|
||||||
|
Math.min(cursorOffset, targetOffset),
|
||||||
|
Math.max(cursorOffset, targetOffset),
|
||||||
|
)
|
||||||
|
// OpenTUI 0.5.10 clears the selection through the public cursor
|
||||||
|
// setter. Move the native edit cursor directly so the placeholder
|
||||||
|
// remains one selected, replaceable unit.
|
||||||
|
this.composer.editBuffer.setCursorByOffset(targetOffset)
|
||||||
|
} else {
|
||||||
|
this.setComposerStringCursor(this.composer.plainText, target)
|
||||||
|
}
|
||||||
key.preventDefault()
|
key.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,10 @@ export class Transcript {
|
|||||||
message.displayContent,
|
message.displayContent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
for (const renderable of this.markdown) renderable.syntaxStyle = theme.syntax
|
for (const renderable of this.markdown) {
|
||||||
|
renderable.fg = theme.text
|
||||||
|
renderable.syntaxStyle = theme.syntax
|
||||||
|
}
|
||||||
for (const frame of this.frames) frame.borderColor = theme.border
|
for (const frame of this.frames) frame.borderColor = theme.border
|
||||||
for (const row of this.userRows) {
|
for (const row of this.userRows) {
|
||||||
row.backgroundColor = theme.userBackground
|
row.backgroundColor = theme.userBackground
|
||||||
@@ -664,6 +667,7 @@ export class Transcript {
|
|||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
|
fg: this.theme.text,
|
||||||
syntaxStyle: this.theme.syntax,
|
syntaxStyle: this.theme.syntax,
|
||||||
streaming,
|
streaming,
|
||||||
internalBlockMode: "top-level",
|
internalBlockMode: "top-level",
|
||||||
|
|||||||
@@ -993,7 +993,7 @@ export const ChatList = memo(function ChatList({
|
|||||||
) : null}
|
) : null}
|
||||||
<span className="min-w-0 flex-1 overflow-hidden">
|
<span className="min-w-0 flex-1 overflow-hidden">
|
||||||
{projectMode ? (
|
{projectMode ? (
|
||||||
<span className="relative flex w-full min-w-0 items-baseline gap-2">
|
<span className="relative flex w-full min-w-0 items-center gap-2">
|
||||||
<SidebarSessionHandle handle={s.handle} />
|
<SidebarSessionHandle handle={s.handle} />
|
||||||
<span className="min-w-0 flex-1 truncate font-medium leading-5">
|
<span className="min-w-0 flex-1 truncate font-medium leading-5">
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -983,6 +983,7 @@ describe("ChatList", () => {
|
|||||||
session({
|
session({
|
||||||
chatId: "alpha",
|
chatId: "alpha",
|
||||||
title: "Alpha task",
|
title: "Alpha task",
|
||||||
|
handle: { id: "handle_alpha", name: "mira" },
|
||||||
updatedAt: "2026-05-20T11:00:00Z",
|
updatedAt: "2026-05-20T11:00:00Z",
|
||||||
workspaceScope: {
|
workspaceScope: {
|
||||||
project_path: "/Users/me/nanobot",
|
project_path: "/Users/me/nanobot",
|
||||||
@@ -1030,6 +1031,12 @@ describe("ChatList", () => {
|
|||||||
"border-sidebar-foreground/10",
|
"border-sidebar-foreground/10",
|
||||||
);
|
);
|
||||||
expect(within(nanobotSection).getByText("Alpha task")).toBeInTheDocument();
|
expect(within(nanobotSection).getByText("Alpha task")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
within(nanobotSection)
|
||||||
|
.getByText("@mira")
|
||||||
|
.closest("[data-sidebar-session-handle]")
|
||||||
|
?.parentElement,
|
||||||
|
).toHaveClass("items-center");
|
||||||
expect(within(nanobotSection).getByText("Zeta task")).toBeInTheDocument();
|
expect(within(nanobotSection).getByText("Zeta task")).toBeInTheDocument();
|
||||||
expect(nanobotText.indexOf("Alpha task")).toBeLessThan(nanobotText.indexOf("Zeta task"));
|
expect(nanobotText.indexOf("Alpha task")).toBeLessThan(nanobotText.indexOf("Zeta task"));
|
||||||
expect(within(nanobotSection).getByLabelText("Agent running")).toBeInTheDocument();
|
expect(within(nanobotSection).getByLabelText("Agent running")).toBeInTheDocument();
|
||||||
|
|||||||
Reference in New Issue
Block a user