mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 16:21:50 +03:00
feat(tui): start fresh chats in launch workspace
This commit is contained in:
@@ -209,7 +209,7 @@ Use `nanobot gateway --background` for the same direct entry point without keepi
|
|||||||
nanobot agent
|
nanobot agent
|
||||||
```
|
```
|
||||||
|
|
||||||
This opens the native terminal client with the same configured model, workspace, tools, streaming protocol, and session engine as the WebUI. Use `/sessions` to switch saved conversations, `/new-chat` to preserve this conversation and start another one, `/branch` to fork from a completed reply, `/context` to inspect the compacted summary and raw message suffix available to the agent, or `/diff` to review the latest turn's file changes. Type `@` to mention an installed app, configured MCP server, or saved session. While nanobot is working, `Enter` steers the current turn, `Tab` queues a visible follow-up for the next turn, and `Option+Up` on macOS (`Alt+Up` on Windows/Linux) returns the latest queued message for editing. Press `Shift+Enter` to add a newline; `Ctrl+J` is the universal fallback for terminals that cannot distinguish modified Enter keys. Use `PageUp` at the top to load earlier transcript pages. The next launch returns to your last session unless `--session` selects another WebSocket session; use `--classic` to resume a session from another channel. The existing nanobot `/new` command keeps its original behavior: it resets the current chat. `nanobot agent` and `nanobot webui` share one on-demand local gateway: either command can start it, each launcher releases only its own client, and the last interactive launcher to exit stops it. Use `nanobot gateway --background` when the gateway must stay alive with no local clients. Type `exit` or press `Ctrl+C` when you are done. Use `nanobot agent --classic` for the legacy Python prompt.
|
This opens the native terminal client with the configured model and tools, using the launch directory as its workspace. Use `/sessions` to switch saved conversations, `/new-chat` to preserve this conversation and start another one, `/branch` to fork from a completed reply, `/context` to inspect the compacted summary and raw message suffix available to the agent, or `/diff` to review the latest turn's file changes. Type `@` to mention an installed app, configured MCP server, or saved session. While nanobot is working, `Enter` steers the current turn, `Tab` queues a visible follow-up for the next turn, and `Option+Up` on macOS (`Alt+Up` on Windows/Linux) returns the latest queued message for editing. Press `Shift+Enter` to add a newline; `Ctrl+J` is the universal fallback for terminals that cannot distinguish modified Enter keys. Use `PageUp` at the top to load earlier transcript pages. Each launch starts a new session; `--session` selects an existing WebSocket session, while `--workspace` overrides the launch directory. Use `--classic` to resume a session from another channel. The existing nanobot `/new` command keeps its original behavior: it resets the current chat. `nanobot agent` and `nanobot webui` share one on-demand local gateway: either command can start it, each launcher releases only its own client, and the last interactive launcher to exit stops it. Use `nanobot gateway --background` when the gateway must stay alive with no local clients. Type `exit` or press `Ctrl+C` when you are done. Use `nanobot agent --classic` for the legacy Python prompt.
|
||||||
|
|
||||||
For one request and an immediate exit, use:
|
For one request and an immediate exit, use:
|
||||||
|
|
||||||
|
|||||||
@@ -100,8 +100,9 @@ Inside the native TUI, `/sessions` switches saved conversations, `/new-chat` sta
|
|||||||
conversation, and `/context` explains the compacted summary and raw session suffix available to
|
conversation, and `/context` explains the compacted summary and raw session suffix available to
|
||||||
the next agent turn. `/branch` forks a saved conversation from a completed reply, and `/diff`
|
the next agent turn. `/branch` forks a saved conversation from a completed reply, and `/diff`
|
||||||
opens the latest turn's file changes as a full-screen unified diff.
|
opens the latest turn's file changes as a full-screen unified diff.
|
||||||
`PageUp` loads older transcript pages when you reach the top. The default
|
`PageUp` loads older transcript pages when you reach the top. By default, each launch starts a
|
||||||
launch returns to the last attached TUI session; `--session` selects a specific session instead.
|
new session using the launch directory as its workspace. `--session` selects a specific existing
|
||||||
|
session, and `--workspace` overrides the launch directory.
|
||||||
|
|
||||||
## Session Storage and Rollback
|
## Session Storage and Rollback
|
||||||
|
|
||||||
|
|||||||
+10
-24
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
@@ -77,8 +76,8 @@ def launch_tui(
|
|||||||
theme: str,
|
theme: str,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Run the native TUI against the shared local gateway."""
|
"""Run the native TUI against the shared local gateway."""
|
||||||
state_path = config_path.parent / "tui" / "state.json"
|
chat_id = _initial_tui_chat_id(session_id)
|
||||||
chat_id = _initial_tui_chat_id(session_id, state_path)
|
tui_workspace = _initial_tui_workspace(workspace_override)
|
||||||
command = _resolve_tui_command()
|
command = _resolve_tui_command()
|
||||||
base_url, bootstrap_secret = _tui_gateway_connection(config)
|
base_url, bootstrap_secret = _tui_gateway_connection(config)
|
||||||
gateway: _GatewayHandle | None = None
|
gateway: _GatewayHandle | None = None
|
||||||
@@ -93,7 +92,7 @@ def launch_tui(
|
|||||||
"NANOBOT_TUI_API_URL": base_url,
|
"NANOBOT_TUI_API_URL": base_url,
|
||||||
"NANOBOT_TUI_MODEL": _model_display(config)[0],
|
"NANOBOT_TUI_MODEL": _model_display(config)[0],
|
||||||
"NANOBOT_TUI_MODEL_PRESET": config.agents.defaults.model_preset or "default",
|
"NANOBOT_TUI_MODEL_PRESET": config.agents.defaults.model_preset or "default",
|
||||||
"NANOBOT_TUI_WORKSPACE": str(config.workspace_path),
|
"NANOBOT_TUI_WORKSPACE": str(tui_workspace),
|
||||||
"NANOBOT_TUI_VERSION": __version__,
|
"NANOBOT_TUI_VERSION": __version__,
|
||||||
"NANOBOT_TUI_ACCESS": (
|
"NANOBOT_TUI_ACCESS": (
|
||||||
"workspace access" if config.tools.restrict_to_workspace else "full access"
|
"workspace access" if config.tools.restrict_to_workspace else "full access"
|
||||||
@@ -105,7 +104,6 @@ def launch_tui(
|
|||||||
env["NANOBOT_TUI_BOOTSTRAP_SECRET"] = bootstrap_secret
|
env["NANOBOT_TUI_BOOTSTRAP_SECRET"] = bootstrap_secret
|
||||||
else:
|
else:
|
||||||
env.pop("NANOBOT_TUI_BOOTSTRAP_SECRET", None)
|
env.pop("NANOBOT_TUI_BOOTSTRAP_SECRET", None)
|
||||||
env["NANOBOT_TUI_STATE_PATH"] = str(state_path)
|
|
||||||
if chat_id:
|
if chat_id:
|
||||||
env["NANOBOT_TUI_CHAT_ID"] = chat_id
|
env["NANOBOT_TUI_CHAT_ID"] = chat_id
|
||||||
else:
|
else:
|
||||||
@@ -478,26 +476,14 @@ def _websocket_chat_id(session_id: str) -> str | None:
|
|||||||
return session_id or None
|
return session_id or None
|
||||||
|
|
||||||
|
|
||||||
def _initial_tui_chat_id(session_id: str | None, state_path: Path) -> str | None:
|
def _initial_tui_chat_id(session_id: str | None) -> str | None:
|
||||||
"""Resume the last TUI chat, while keeping an explicit selector authoritative."""
|
"""Start fresh unless the caller explicitly selects a TUI chat."""
|
||||||
if session_id is not None:
|
if session_id is not None:
|
||||||
return _websocket_chat_id(session_id)
|
return _websocket_chat_id(session_id)
|
||||||
return _read_tui_chat_id(state_path)
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _read_tui_chat_id(path: Path) -> str | None:
|
def _initial_tui_workspace(workspace_override: str | None) -> Path:
|
||||||
"""Read the last attached chat without making launch depend on optional state."""
|
"""Use the launch directory unless the caller explicitly selects a workspace."""
|
||||||
try:
|
workspace = Path(workspace_override) if workspace_override is not None else Path.cwd()
|
||||||
raw_payload: Any = json.loads(path.read_text(encoding="utf-8"))
|
return workspace.expanduser().resolve(strict=False)
|
||||||
except (OSError, json.JSONDecodeError):
|
|
||||||
return None
|
|
||||||
if not isinstance(raw_payload, dict):
|
|
||||||
return None
|
|
||||||
payload = cast(dict[str, Any], raw_payload)
|
|
||||||
value = payload.get("chat_id")
|
|
||||||
if not isinstance(value, str):
|
|
||||||
return None
|
|
||||||
value = value.strip()
|
|
||||||
if not value or len(value) > 256 or any(character in value for character in "\r\n"):
|
|
||||||
return None
|
|
||||||
return value
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from nanobot.cli.tui_launcher import (
|
|||||||
_download_release_tui,
|
_download_release_tui,
|
||||||
_ensure_gateway,
|
_ensure_gateway,
|
||||||
_initial_tui_chat_id,
|
_initial_tui_chat_id,
|
||||||
_read_tui_chat_id,
|
_initial_tui_workspace,
|
||||||
_resolve_source_tui_command,
|
_resolve_source_tui_command,
|
||||||
_resolve_tui_command,
|
_resolve_tui_command,
|
||||||
_websocket_chat_id,
|
_websocket_chat_id,
|
||||||
@@ -67,28 +67,22 @@ def test_native_tui_rejects_a_session_owned_by_another_channel() -> None:
|
|||||||
_websocket_chat_id("telegram:123")
|
_websocket_chat_id("telegram:123")
|
||||||
|
|
||||||
|
|
||||||
def test_tui_chat_state_is_optional_and_validated(tmp_path: Path) -> None:
|
def test_default_tui_starts_fresh_but_explicit_session_wins() -> None:
|
||||||
path = tmp_path / "tui" / "state.json"
|
assert _initial_tui_chat_id(None) is None
|
||||||
assert _read_tui_chat_id(path) is None
|
assert _initial_tui_chat_id("websocket:chosen") == "chosen"
|
||||||
|
|
||||||
path.parent.mkdir()
|
|
||||||
path.write_text('{"schema_version": 1, "chat_id": "saved-chat"}', encoding="utf-8")
|
|
||||||
assert _read_tui_chat_id(path) == "saved-chat"
|
|
||||||
|
|
||||||
path.write_text('{"chat_id": "bad\\nchat"}', encoding="utf-8")
|
|
||||||
assert _read_tui_chat_id(path) is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_default_tui_resumes_but_explicit_session_wins(tmp_path: Path) -> None:
|
def test_default_tui_workspace_is_the_launch_directory(
|
||||||
path = tmp_path / "tui" / "state.json"
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
path.parent.mkdir()
|
tmp_path: Path,
|
||||||
path.write_text('{"chat_id": "saved-chat"}', encoding="utf-8")
|
) -> None:
|
||||||
|
launch_directory = tmp_path / "project"
|
||||||
|
override = tmp_path / "override"
|
||||||
|
launch_directory.mkdir()
|
||||||
|
monkeypatch.chdir(launch_directory)
|
||||||
|
|
||||||
assert _initial_tui_chat_id(None, path) == "saved-chat"
|
assert _initial_tui_workspace(None) == launch_directory.resolve()
|
||||||
assert _initial_tui_chat_id("websocket:chosen", path) == "chosen"
|
assert _initial_tui_workspace(str(override)) == override.resolve()
|
||||||
|
|
||||||
path.unlink()
|
|
||||||
assert _initial_tui_chat_id(None, path) is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_launcher_passes_the_canonical_model_preset_to_the_tui(
|
def test_launcher_passes_the_canonical_model_preset_to_the_tui(
|
||||||
@@ -144,6 +138,7 @@ def test_launcher_passes_the_canonical_model_preset_to_the_tui(
|
|||||||
assert result == 0
|
assert result == 0
|
||||||
assert captured["NANOBOT_TUI_MODEL"] == "openai/gpt-5.6"
|
assert captured["NANOBOT_TUI_MODEL"] == "openai/gpt-5.6"
|
||||||
assert captured["NANOBOT_TUI_MODEL_PRESET"] == "Deep Research"
|
assert captured["NANOBOT_TUI_MODEL_PRESET"] == "Deep Research"
|
||||||
|
assert captured["NANOBOT_TUI_WORKSPACE"] == str(Path.cwd().resolve())
|
||||||
assert captured["NANOBOT_TUI_BOOTSTRAP_URL"] == (
|
assert captured["NANOBOT_TUI_BOOTSTRAP_URL"] == (
|
||||||
"http://127.0.0.1:8765/webui/bootstrap"
|
"http://127.0.0.1:8765/webui/bootstrap"
|
||||||
)
|
)
|
||||||
@@ -151,6 +146,7 @@ def test_launcher_passes_the_canonical_model_preset_to_the_tui(
|
|||||||
assert "NANOBOT_TUI_WS_URL" not in captured
|
assert "NANOBOT_TUI_WS_URL" not in captured
|
||||||
assert "NANOBOT_TUI_API_TOKEN" not in captured
|
assert "NANOBOT_TUI_API_TOKEN" not in captured
|
||||||
assert "NANOBOT_TUI_CHAT_ID" not in captured
|
assert "NANOBOT_TUI_CHAT_ID" not in captured
|
||||||
|
assert "NANOBOT_TUI_STATE_PATH" not in captured
|
||||||
assert events == ["spawned", "waited"]
|
assert events == ["spawned", "waited"]
|
||||||
assert released == [True]
|
assert released == [True]
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -41,9 +41,10 @@ Unsent prompts return to the composer if the turn stops or fails.
|
|||||||
|
|
||||||
Use `/sessions` to search and switch persisted conversations without leaving the terminal.
|
Use `/sessions` to search and switch persisted conversations without leaving the terminal.
|
||||||
`/new-chat` preserves the current conversation and starts another one; nanobot's existing `/new`
|
`/new-chat` preserves the current conversation and starts another one; nanobot's existing `/new`
|
||||||
command keeps its cross-channel behavior and resets the current chat. The next launch returns to
|
command keeps its cross-channel behavior and resets the current chat. Each launch starts a new
|
||||||
the last session unless `--session` selects another one. When earlier transcript pages exist,
|
session using the launch directory as its workspace; `--session` selects an existing session and
|
||||||
press `PageUp` at the top to load them in place.
|
`--workspace` overrides the launch directory. When earlier transcript pages exist, press `PageUp`
|
||||||
|
at the top to load them in place.
|
||||||
|
|
||||||
The native client accepts bare WebSocket chat IDs or `websocket:<id>` selectors. Use
|
The native client accepts bare WebSocket chat IDs or `websocket:<id>` selectors. Use
|
||||||
`nanobot agent --classic --session <channel:id>` to resume a session owned by another channel;
|
`nanobot agent --classic --session <channel:id>` to resume a session owned by another channel;
|
||||||
|
|||||||
+4
-3
@@ -59,7 +59,6 @@ import {
|
|||||||
type TranscriptNavigation,
|
type TranscriptNavigation,
|
||||||
type TranscriptTheme,
|
type TranscriptTheme,
|
||||||
} from "./transcript"
|
} from "./transcript"
|
||||||
import { rememberChat } from "./session-state"
|
|
||||||
import { ComposerDraft } from "./composer-draft"
|
import { ComposerDraft } from "./composer-draft"
|
||||||
import { BranchMenu, branchPoints } from "./branch-menu"
|
import { BranchMenu, branchPoints } from "./branch-menu"
|
||||||
import {
|
import {
|
||||||
@@ -95,7 +94,6 @@ interface AppOptions {
|
|||||||
version: string
|
version: string
|
||||||
access: string
|
access: string
|
||||||
theme: "auto" | ThemeMode
|
theme: "auto" | ThemeMode
|
||||||
statePath?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatClient {
|
interface ChatClient {
|
||||||
@@ -508,6 +506,10 @@ export class NanobotTui {
|
|||||||
}
|
}
|
||||||
: { url: options.wsUrl }),
|
: { url: options.wsUrl }),
|
||||||
chatId: options.chatId,
|
chatId: options.chatId,
|
||||||
|
initialWorkspaceScope: {
|
||||||
|
project_path: options.workspace,
|
||||||
|
access_mode: options.access.toLocaleLowerCase().includes("full") ? "full" : "restricted",
|
||||||
|
},
|
||||||
onEvent: (event) => this.accept(event),
|
onEvent: (event) => this.accept(event),
|
||||||
onStatus: (status, detail) => this.handleStatus(status, detail),
|
onStatus: (status, detail) => this.handleStatus(status, detail),
|
||||||
})
|
})
|
||||||
@@ -914,7 +916,6 @@ export class NanobotTui {
|
|||||||
|
|
||||||
accept(event: InboundEvent): void {
|
accept(event: InboundEvent): void {
|
||||||
if (event.event === "attached") {
|
if (event.event === "attached") {
|
||||||
void rememberChat(this.options.statePath, event.chat_id)
|
|
||||||
this.host.reportSession(event.chat_id)
|
this.host.reportSession(event.chat_id)
|
||||||
if (event.usage) this.lastUsage = event.usage
|
if (event.usage) this.lastUsage = event.usage
|
||||||
if (event.model_preset !== undefined) {
|
if (event.model_preset !== undefined) {
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ const options: AppOptions = {
|
|||||||
version: process.env.NANOBOT_TUI_VERSION?.trim() || "dev",
|
version: process.env.NANOBOT_TUI_VERSION?.trim() || "dev",
|
||||||
access: process.env.NANOBOT_TUI_ACCESS?.trim() || "workspace access",
|
access: process.env.NANOBOT_TUI_ACCESS?.trim() || "workspace access",
|
||||||
theme: themePreference(),
|
theme: themePreference(),
|
||||||
statePath: process.env.NANOBOT_TUI_STATE_PATH?.trim() || undefined,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let app: NanobotTui | undefined
|
let app: NanobotTui | undefined
|
||||||
|
|||||||
@@ -259,6 +259,10 @@ describe("gateway protocol", () => {
|
|||||||
const client = new NanobotClient({
|
const client = new NanobotClient({
|
||||||
url: "ws://nanobot.test/ws",
|
url: "ws://nanobot.test/ws",
|
||||||
chatId: "terminal",
|
chatId: "terminal",
|
||||||
|
initialWorkspaceScope: {
|
||||||
|
project_path: "/tmp/project",
|
||||||
|
access_mode: "restricted",
|
||||||
|
},
|
||||||
onEvent: (event) => events.push(event),
|
onEvent: (event) => events.push(event),
|
||||||
onStatus: () => undefined,
|
onStatus: () => undefined,
|
||||||
})
|
})
|
||||||
@@ -324,6 +328,47 @@ describe("gateway protocol", () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("starts a fresh chat in the launch workspace", () => {
|
||||||
|
const original = globalThis.WebSocket
|
||||||
|
let socket: FakeSocket | undefined
|
||||||
|
Object.defineProperty(globalThis, "WebSocket", {
|
||||||
|
configurable: true,
|
||||||
|
value: class extends FakeSocket {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
socket = this
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new NanobotClient({
|
||||||
|
url: "ws://nanobot.test/ws",
|
||||||
|
initialWorkspaceScope: {
|
||||||
|
project_path: "/tmp/launch-project",
|
||||||
|
access_mode: "restricted",
|
||||||
|
},
|
||||||
|
onEvent: () => undefined,
|
||||||
|
onStatus: () => undefined,
|
||||||
|
})
|
||||||
|
client.connect()
|
||||||
|
if (!socket) throw new Error("socket was not created")
|
||||||
|
socket.emit("message", {
|
||||||
|
data: JSON.stringify({ event: "ready", chat_id: "", client_id: "client" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(socket.sent.map((value) => JSON.parse(value))).toEqual([{
|
||||||
|
type: "new_chat",
|
||||||
|
workspace_scope: {
|
||||||
|
project_path: "/tmp/launch-project",
|
||||||
|
access_mode: "restricted",
|
||||||
|
},
|
||||||
|
}])
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: original })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("loads runtime model and access controls from canonical APIs", async () => {
|
test("loads runtime model and access controls from canonical APIs", async () => {
|
||||||
const original = globalThis.fetch
|
const original = globalThis.fetch
|
||||||
globalThis.fetch = (async (input: string | URL | Request) => {
|
globalThis.fetch = (async (input: string | URL | Request) => {
|
||||||
|
|||||||
+2
-1
@@ -157,6 +157,7 @@ export interface ClientOptions {
|
|||||||
connectionRetryLabel?: string
|
connectionRetryLabel?: string
|
||||||
startupRetryMaxDelayMs?: number
|
startupRetryMaxDelayMs?: number
|
||||||
chatId?: string
|
chatId?: string
|
||||||
|
initialWorkspaceScope?: WorkspaceScopePayload
|
||||||
reconnectDelayMs?: number
|
reconnectDelayMs?: number
|
||||||
onEvent: (event: InboundEvent) => void
|
onEvent: (event: InboundEvent) => void
|
||||||
onStatus: (status: ConnectionStatus, detail?: string) => void
|
onStatus: (status: ConnectionStatus, detail?: string) => void
|
||||||
@@ -995,7 +996,7 @@ export class NanobotClient {
|
|||||||
this.chatId = requestedChatId
|
this.chatId = requestedChatId
|
||||||
this.write({ type: "attach", chat_id: this.chatId })
|
this.write({ type: "attach", chat_id: this.chatId })
|
||||||
} else {
|
} else {
|
||||||
this.write({ type: "new_chat" })
|
this.newChat(this.options.initialWorkspaceScope)
|
||||||
}
|
}
|
||||||
} else if (event.event === "attached") {
|
} else if (event.event === "attached") {
|
||||||
this.chatId = event.chat_id
|
this.chatId = event.chat_id
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { afterEach, describe, expect, test } from "bun:test"
|
|
||||||
import { mkdtemp, readFile, rm } from "node:fs/promises"
|
|
||||||
import { tmpdir } from "node:os"
|
|
||||||
import { join } from "node:path"
|
|
||||||
|
|
||||||
import { rememberChat } from "./session-state"
|
|
||||||
|
|
||||||
describe("TUI session state", () => {
|
|
||||||
let directory = ""
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
if (directory) await rm(directory, { recursive: true, force: true })
|
|
||||||
directory = ""
|
|
||||||
})
|
|
||||||
|
|
||||||
test("atomically remembers the last attached gateway chat", async () => {
|
|
||||||
directory = await mkdtemp(join(tmpdir(), "nanobot-tui-state-"))
|
|
||||||
const path = join(directory, "nested", "state.json")
|
|
||||||
|
|
||||||
await rememberChat(path, "chat-123")
|
|
||||||
|
|
||||||
expect(JSON.parse(await readFile(path, "utf8"))).toEqual({
|
|
||||||
schema_version: 1,
|
|
||||||
chat_id: "chat-123",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { mkdir, rename, rm } from "node:fs/promises"
|
|
||||||
import { dirname } from "node:path"
|
|
||||||
|
|
||||||
export async function rememberChat(path: string | undefined, chatId: string): Promise<void> {
|
|
||||||
if (!path || !chatId) return
|
|
||||||
const temporary = `${path}.tmp-${process.pid}`
|
|
||||||
try {
|
|
||||||
await mkdir(dirname(path), { recursive: true })
|
|
||||||
const content = `${JSON.stringify({ schema_version: 1, chat_id: chatId })}\n`
|
|
||||||
await Bun.write(temporary, content)
|
|
||||||
try {
|
|
||||||
await rename(temporary, path)
|
|
||||||
} catch {
|
|
||||||
// Windows cannot always atomically replace an existing destination.
|
|
||||||
await Bun.write(path, content)
|
|
||||||
await rm(temporary, { force: true })
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Session navigation must keep working when this optional convenience file
|
|
||||||
// is read-only, on a network volume, or removed during shutdown.
|
|
||||||
await rm(temporary, { force: true }).catch(() => {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user