my-durable 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/LICENSE +21 -0
- package/README.md +328 -2
- package/ai_docs/durable_pi/announcement.md +860 -0
- package/ai_docs/durable_pi/changelog.md +51 -0
- package/ai_docs/durable_pi/chord-delta-findings.md +362 -0
- package/ai_docs/durable_pi/coding-agent-integration.md +70 -0
- package/ai_docs/durable_pi/pico-v5-chord-usage.md +391 -0
- package/ai_docs/durable_pi/pico-v5-handoff.md +921 -0
- package/ai_docs/durable_pi/readme.md +617 -0
- package/ai_docs/durable_pi/sources.md +22 -0
- package/ai_docs/durable_pi/spec.md +4696 -0
- package/ai_docs/durable_pi/vacation-planner.md +57 -0
- package/ai_docs/pi/chord.md +268 -0
- package/ai_docs/pi/cli-integration.md +110 -0
- package/ai_docs/pi/cli.md +325 -0
- package/ai_docs/pi/codemode.md +211 -0
- package/ai_docs/pi/compaction.md +467 -0
- package/ai_docs/pi/configuration.md +51 -0
- package/ai_docs/pi/containerization.md +187 -0
- package/ai_docs/pi/custom-provider.md +177 -0
- package/ai_docs/pi/environment-variables.md +102 -0
- package/ai_docs/pi/extensions.md +278 -0
- package/ai_docs/pi/how-pi-works.md +53 -0
- package/ai_docs/pi/index.md +43 -0
- package/ai_docs/pi/json.md +230 -0
- package/ai_docs/pi/keybindings.md +198 -0
- package/ai_docs/pi/llama-cpp.md +118 -0
- package/ai_docs/pi/mcp.md +274 -0
- package/ai_docs/pi/message-types.md +265 -0
- package/ai_docs/pi/models.md +209 -0
- package/ai_docs/pi/overview.md +166 -0
- package/ai_docs/pi/packages.md +133 -0
- package/ai_docs/pi/pi-agent-core.md +568 -0
- package/ai_docs/pi/pi-ai.md +1942 -0
- package/ai_docs/pi/prompt-templates.md +63 -0
- package/ai_docs/pi/providers.md +204 -0
- package/ai_docs/pi/quickstart.md +140 -0
- package/ai_docs/pi/readme.md +99 -0
- package/ai_docs/pi/rpc-commands.md +864 -0
- package/ai_docs/pi/rpc-extension-ui.md +204 -0
- package/ai_docs/pi/rpc.md +197 -0
- package/ai_docs/pi/sdk.md +151 -0
- package/ai_docs/pi/security.md +103 -0
- package/ai_docs/pi/session-format.md +297 -0
- package/ai_docs/pi/sessions.md +70 -0
- package/ai_docs/pi/settings.md +173 -0
- package/ai_docs/pi/shell-aliases.md +97 -0
- package/ai_docs/pi/skills.md +97 -0
- package/ai_docs/pi/slash-commands.md +64 -0
- package/ai_docs/pi/sources.md +57 -0
- package/ai_docs/pi/terminal-setup.md +223 -0
- package/ai_docs/pi/termux.md +121 -0
- package/ai_docs/pi/themes.md +140 -0
- package/ai_docs/pi/tmux.md +59 -0
- package/ai_docs/pi/toolkit.md +168 -0
- package/ai_docs/pi/tui.md +129 -0
- package/ai_docs/pi/usage.md +98 -0
- package/ai_docs/pi/virtual-models.md +118 -0
- package/ai_docs/pi/windows.md +71 -0
- package/apps/dpi-backend/.nvmrc +1 -0
- package/apps/dpi-backend/API.md +99 -0
- package/apps/dpi-backend/README.md +119 -0
- package/apps/dpi-backend/deploy/backend.env.example +9 -0
- package/apps/dpi-backend/deploy/dpi-backend.plist +26 -0
- package/apps/dpi-backend/deploy/dpi-backend.service +22 -0
- package/apps/dpi-backend/package.json +33 -0
- package/apps/dpi-backend/prompts/one_sentence.md +3 -0
- package/apps/dpi-backend/prompts/smart_ass_opus_fix.md +169 -0
- package/apps/dpi-backend/scripts/check-contract.mjs +151 -0
- package/apps/dpi-backend/scripts/migrate-playground.ts +66 -0
- package/apps/dpi-backend/scripts/record-nested-fixture.mts +154 -0
- package/apps/dpi-backend/scripts/token.ts +57 -0
- package/apps/dpi-backend/src/addons.ts +139 -0
- package/apps/dpi-backend/src/auth.ts +20 -0
- package/apps/dpi-backend/src/config.ts +54 -0
- package/apps/dpi-backend/src/contract-check.ts +118 -0
- package/apps/dpi-backend/src/errors.ts +50 -0
- package/apps/dpi-backend/src/extensions.ts +510 -0
- package/apps/dpi-backend/src/faux.ts +80 -0
- package/apps/dpi-backend/src/history.ts +165 -0
- package/apps/dpi-backend/src/idempotency.ts +20 -0
- package/apps/dpi-backend/src/jev.ts +43 -0
- package/apps/dpi-backend/src/jobs.ts +790 -0
- package/apps/dpi-backend/src/main.ts +76 -0
- package/apps/dpi-backend/src/model-catalog.ts +180 -0
- package/apps/dpi-backend/src/pairing-files.ts +142 -0
- package/apps/dpi-backend/src/pairing.ts +459 -0
- package/apps/dpi-backend/src/routes.ts +217 -0
- package/apps/dpi-backend/src/shared-state.ts +85 -0
- package/apps/dpi-backend/src/store.ts +1240 -0
- package/apps/dpi-backend/src/streams.ts +256 -0
- package/apps/dpi-backend/src/tunnel.ts +955 -0
- package/apps/dpi-backend/tsconfig.json +13 -0
- package/apps/dpi-playground/README.md +83 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-CFGc7lHT.js +77 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-DFdKpwm_.css +1 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/anthropic.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/deepseek.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/durable-pi.svg +14 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/gemini.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/grok.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/kimi.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/lab.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/meta.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/minimax.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/openai.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/qwen.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/xiaomi.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/zai.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/index.html +16 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/index.html +15 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/package.json +5 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/anthropic.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/deepseek.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/durable-pi.svg +14 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/gemini.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/grok.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/kimi.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/lab.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/meta.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/minimax.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/openai.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/qwen.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/xiaomi.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/zai.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/App.vue +180 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/commands.ts +119 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/composables.ts +129 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/hub.ts +40 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/icon-path.ts +7 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/ids.ts +2 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/jobs.ts +80 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/link-policy.ts +16 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown-policy.ts +115 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown.ts +60 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-close.ts +23 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-handoff.ts +56 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair.ts +166 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/qrcode.ts +323 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/signature.ts +146 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/store.ts +373 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/stream.ts +183 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/AgentView.vue +342 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Composer.vue +198 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ContextBar.vue +254 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceCredentialsDialog.vue +40 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceIcon.vue +56 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DirBrowser.vue +95 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ExtensionsPanel.vue +96 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/HelpDialog.vue +69 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Inspector.vue +425 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/JobCard.vue +221 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/MobileNav.vue +39 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ModelPicker.vue +71 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NetworkSidebar.vue +236 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NewSessionDialog.vue +114 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/OfflinePanel.vue +128 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PairDeviceWizard.vue +433 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PromptDialog.vue +89 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/StateEditor.vue +179 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TasksPanel.vue +280 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Toast.vue +11 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TokenManager.vue +175 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TopBar.vue +114 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TranscriptItem.vue +152 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TreeRow.vue +195 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TunnelStatusBar.vue +107 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/extensions.css +105 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/jobs.css +655 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/main.ts +22 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/network.css +675 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/style.css +3203 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/tsconfig.json +16 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/vite.config.ts +14 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/package.json +5 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/audit.ts +406 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/client-ip.ts +72 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/credentials.ts +265 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/guard.ts +43 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/http-errors.ts +156 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/identity.ts +60 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/limiter.ts +165 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/main.ts +460 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/monitor.ts +320 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/mux.ts +277 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/pairing.ts +816 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/proxy.ts +128 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/rotation.ts +583 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/seed.ts +11 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/transport.ts +108 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-token.ts +11 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-transport.ts +560 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel.ts +873 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/tsconfig.json +13 -0
- package/apps/dpi-playground/package.json +37 -0
- package/apps/dpi-sdk/README.md +74 -0
- package/apps/dpi-sdk/package.json +20 -0
- package/apps/dpi-sdk/scripts/net.ts +73 -0
- package/apps/dpi-sdk/src/api.ts +631 -0
- package/apps/dpi-sdk/src/app-state.ts +111 -0
- package/apps/dpi-sdk/src/client.ts +330 -0
- package/apps/dpi-sdk/src/errors.ts +16 -0
- package/apps/dpi-sdk/src/jobs.ts +99 -0
- package/apps/dpi-sdk/src/models.ts +472 -0
- package/apps/dpi-sdk/src/node/chunk.ts +488 -0
- package/apps/dpi-sdk/src/node/config.ts +175 -0
- package/apps/dpi-sdk/src/node/drivers.ts +402 -0
- package/apps/dpi-sdk/src/node/ops.ts +114 -0
- package/apps/dpi-sdk/src/node/private-dir.ts +48 -0
- package/apps/dpi-sdk/src/node/registry-migration.ts +549 -0
- package/apps/dpi-sdk/src/node/registry.ts +390 -0
- package/apps/dpi-sdk/src/node/sse-writer.ts +151 -0
- package/apps/dpi-sdk/src/protocol/backoff.ts +31 -0
- package/apps/dpi-sdk/src/protocol/limits.ts +199 -0
- package/apps/dpi-sdk/src/protocol/pairing.ts +311 -0
- package/apps/dpi-sdk/src/protocol/rest-routes.ts +105 -0
- package/apps/dpi-sdk/src/protocol/tunnel.ts +331 -0
- package/apps/dpi-sdk/src/sse.ts +117 -0
- package/apps/dpi-sdk/src/transcript.ts +291 -0
- package/apps/dpi-sdk/tsconfig.json +13 -0
- package/apps/dpi-sdk/w +20 -0
- package/apps/dpi-tui/README.md +54 -0
- package/apps/dpi-tui/bin/dpi-tui.mjs +5 -0
- package/apps/dpi-tui/package.json +20 -0
- package/apps/dpi-tui/src/app.ts +433 -0
- package/apps/dpi-tui/src/commands.ts +151 -0
- package/apps/dpi-tui/src/main.ts +122 -0
- package/apps/dpi-tui/src/pairing.ts +287 -0
- package/apps/dpi-tui/src/pickers.ts +238 -0
- package/apps/dpi-tui/src/render.ts +164 -0
- package/apps/dpi-tui/src/theme.ts +57 -0
- package/apps/dpi-tui/tsconfig.json +14 -0
- package/bin/device.mjs +5 -0
- package/bin/hub.mjs +5 -0
- package/bin/tui.mjs +5 -0
- package/deploy/Caddyfile +120 -0
- package/docs/verification.md +102 -0
- package/dpi-network.example.yaml +62 -0
- package/images/01_durable_agent_network.svg +185 -0
- package/images/02_pi_made_durable.svg +204 -0
- package/images/03_restart_mid_tool.svg +168 -0
- package/images/07_one_api_every_client.svg +123 -0
- package/images/08_device_session_agent.svg +123 -0
- package/images/09_compact_keep_history.svg +143 -0
- package/images/10_shared_app_state.svg +177 -0
- package/images/11_durable_jobs_wait.svg +141 -0
- package/images/12_tools_change_mid_run.svg +231 -0
- package/images/13_hook_blocks_tool.svg +122 -0
- package/images/14_fork_at_any_entry.svg +119 -0
- package/images/15_web_client.png +0 -0
- package/images/16_terminal_client.svg +43 -0
- package/images/17_devices_are_data.svg +182 -0
- package/images/18_should_every_agent_be_durable.svg +118 -0
- package/images/19_sqlite_on_every_device.svg +155 -0
- package/images/20_admission_and_effects.svg +134 -0
- package/justfile +33 -0
- package/package.json +41 -4
- package/scripts/verify.mjs +210 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// The TUI: one device, one agent at a time. Layout, keys and streams; rendering lives in render.ts.
|
|
2
|
+
import {
|
|
3
|
+
CombinedAutocompleteProvider,
|
|
4
|
+
Editor,
|
|
5
|
+
matchesKey,
|
|
6
|
+
ProcessTerminal,
|
|
7
|
+
ScrollView,
|
|
8
|
+
Text,
|
|
9
|
+
TuiAltScreen,
|
|
10
|
+
VStack,
|
|
11
|
+
type Component,
|
|
12
|
+
type OverlayHandle,
|
|
13
|
+
type TuiInputListenerResult,
|
|
14
|
+
} from "@earendil-works/pi-tui";
|
|
15
|
+
import { DpiClient, follow, type AgentHandle, type ConnectionState } from "../../dpi-sdk/src/client.ts";
|
|
16
|
+
import { isDpiError } from "../../dpi-sdk/src/errors.ts";
|
|
17
|
+
import { AgentModel, SessionModel } from "../../dpi-sdk/src/models.ts";
|
|
18
|
+
import { sumUsage } from "../../dpi-sdk/src/transcript.ts";
|
|
19
|
+
import {
|
|
20
|
+
THINKING_LEVELS,
|
|
21
|
+
type DeviceInfo,
|
|
22
|
+
type SessionSummary,
|
|
23
|
+
type ThinkingLevel,
|
|
24
|
+
type WhenBusy,
|
|
25
|
+
type WireAssistantMessage,
|
|
26
|
+
} from "../../dpi-sdk/src/api.ts";
|
|
27
|
+
import type { NetworkRegistry } from "../../dpi-sdk/src/node/registry.ts";
|
|
28
|
+
import { COMMANDS, runCommand } from "./commands.ts";
|
|
29
|
+
import { pickModel, pickSession, Prompt } from "./pickers.ts";
|
|
30
|
+
import { Line, spinner, TranscriptView } from "./render.ts";
|
|
31
|
+
import { c, editorTheme } from "./theme.ts";
|
|
32
|
+
|
|
33
|
+
export interface DeviceRef {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
url: string;
|
|
37
|
+
token: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const newId = () => crypto.randomUUID();
|
|
41
|
+
/** Quote a shell word only when it needs it. */
|
|
42
|
+
const shellWord = (s: string) => (/^[\w.:@/~-]+$/.test(s) ? s : `'${s}'`);
|
|
43
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
44
|
+
const compact = (n: number) => Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(n);
|
|
45
|
+
|
|
46
|
+
export class App {
|
|
47
|
+
readonly tui = new TuiAltScreen(new ProcessTerminal(), false, undefined, {
|
|
48
|
+
scrollToEndIndicator: () => c.muted(" ↓ new output "),
|
|
49
|
+
});
|
|
50
|
+
readonly model = new AgentModel();
|
|
51
|
+
readonly session = new SessionModel();
|
|
52
|
+
sessionId?: string;
|
|
53
|
+
agentId?: number;
|
|
54
|
+
handle?: AgentHandle;
|
|
55
|
+
private header = new Line("");
|
|
56
|
+
private view = new TranscriptView();
|
|
57
|
+
private status = new Text("", 1, 0);
|
|
58
|
+
private editor = new Editor(this.tui, editorTheme, { paddingX: 1 });
|
|
59
|
+
private footer = new Line("");
|
|
60
|
+
private connection: ConnectionState | "idle" = "idle";
|
|
61
|
+
private streams?: AbortController;
|
|
62
|
+
private overlays: Array<{ handle: OverlayHandle; onClose?: () => void }> = [];
|
|
63
|
+
private notice?: { text: string; error: boolean; at: number };
|
|
64
|
+
private ticker?: ReturnType<typeof setInterval>;
|
|
65
|
+
private sessions: SessionSummary[] = [];
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
public device: DeviceRef,
|
|
69
|
+
public client: DpiClient,
|
|
70
|
+
public info: DeviceInfo,
|
|
71
|
+
public registry?: NetworkRegistry,
|
|
72
|
+
) {
|
|
73
|
+
this.tui.setLayoutRoot(
|
|
74
|
+
new VStack([
|
|
75
|
+
{ component: this.header, basis: "auto" },
|
|
76
|
+
{ component: new ScrollView(this.view, { follow: "end", primary: true }), basis: 0, grow: 1, minSize: 3 },
|
|
77
|
+
{ component: this.status, basis: "auto", visible: () => this.statusLines().length > 0 },
|
|
78
|
+
{ component: this.editor, basis: "auto", shrink: 1, minSize: 1 },
|
|
79
|
+
{ component: this.footer, basis: "auto" },
|
|
80
|
+
]),
|
|
81
|
+
);
|
|
82
|
+
this.editor.setAutocompleteProvider(new CombinedAutocompleteProvider(COMMANDS, process.cwd()));
|
|
83
|
+
this.editor.onSubmit = (text) => void this.submit(text, this.busy() ? "steer" : "followUp");
|
|
84
|
+
this.tui.addInputListener((data) => this.key(data));
|
|
85
|
+
this.model.subscribe(() => this.redraw());
|
|
86
|
+
this.session.subscribe(() => this.redraw(false));
|
|
87
|
+
this.tui.setFocus(this.editor);
|
|
88
|
+
this.tui.start();
|
|
89
|
+
this.redraw();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ------------------------------------------------------------------ streams
|
|
93
|
+
|
|
94
|
+
async open(sessionRef?: string, agentRef?: string) {
|
|
95
|
+
if (!sessionRef) return pickSession(this);
|
|
96
|
+
await this.run(async () => {
|
|
97
|
+
const s = await this.client.session(sessionRef).get();
|
|
98
|
+
const agentId = agentRef
|
|
99
|
+
? (await this.client.agent(s.id, agentRef).get()).id
|
|
100
|
+
: (s.agents.find((a) => a.kind === "root") ?? s.agents[0])?.id ?? 1;
|
|
101
|
+
await this.attach(s.id, agentId);
|
|
102
|
+
});
|
|
103
|
+
if (!this.sessionId) await pickSession(this);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async attach(sessionId: string, agentId: number) {
|
|
107
|
+
this.streams?.abort();
|
|
108
|
+
const streams = (this.streams = new AbortController());
|
|
109
|
+
const sameSession = sessionId === this.sessionId;
|
|
110
|
+
this.sessionId = sessionId;
|
|
111
|
+
this.agentId = agentId;
|
|
112
|
+
this.handle = this.client.agent(sessionId, agentId);
|
|
113
|
+
this.model.clear();
|
|
114
|
+
this.view.reset();
|
|
115
|
+
if (!sameSession) this.session.clear();
|
|
116
|
+
void this.refreshSessions();
|
|
117
|
+
const handle = this.handle;
|
|
118
|
+
void follow(
|
|
119
|
+
(signal) => handle.events(signal),
|
|
120
|
+
(frame) => this.model.apply(frame),
|
|
121
|
+
(state, error) => {
|
|
122
|
+
this.connection = state;
|
|
123
|
+
if (error && state !== "offline") this.flash(error.message, true);
|
|
124
|
+
this.redraw(false);
|
|
125
|
+
},
|
|
126
|
+
streams.signal,
|
|
127
|
+
);
|
|
128
|
+
void follow(
|
|
129
|
+
(signal) => this.client.session(sessionId).events(signal),
|
|
130
|
+
(frame) => this.session.apply(frame),
|
|
131
|
+
() => {},
|
|
132
|
+
streams.signal,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async switchDevice(id: string) {
|
|
137
|
+
const row = this.registry?.get(id);
|
|
138
|
+
if (!row?.token) return this.flash(`No token for ${id}. Run: just net register ${id}`, true);
|
|
139
|
+
if (!row.url) return this.flash(`${id} is tunnel-only (no LAN URL) — use the hub web UI.`, true);
|
|
140
|
+
const client = new DpiClient(row.url, row.token);
|
|
141
|
+
await this.run(async () => {
|
|
142
|
+
this.info = await client.device({ timeoutMs: 8000 });
|
|
143
|
+
this.streams?.abort();
|
|
144
|
+
this.device = { id: row.id, name: row.name, url: row.url!, token: row.token! };
|
|
145
|
+
this.client = client;
|
|
146
|
+
this.sessionId = undefined;
|
|
147
|
+
this.agentId = undefined;
|
|
148
|
+
this.handle = undefined;
|
|
149
|
+
this.model.clear();
|
|
150
|
+
this.session.clear();
|
|
151
|
+
this.view.reset();
|
|
152
|
+
await pickSession(this);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async refreshSessions() {
|
|
157
|
+
try {
|
|
158
|
+
this.sessions = (await this.client.tree()).sessions;
|
|
159
|
+
this.redraw(false);
|
|
160
|
+
} catch {}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
sessionSummary(): SessionSummary | undefined {
|
|
164
|
+
const s = this.sessions.find((x) => x.id === this.sessionId);
|
|
165
|
+
return s && this.session.state.ready ? { ...s, agents: this.session.state.agents } : s;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ------------------------------------------------------------------ input
|
|
169
|
+
|
|
170
|
+
busy() {
|
|
171
|
+
return this.model.state.running || (this.agentId !== undefined && this.session.foregroundBusy(this.agentId));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private key(data: string): TuiInputListenerResult {
|
|
175
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
176
|
+
if (this.overlays.length) return this.closeOverlay(), { consume: true };
|
|
177
|
+
if (this.editor.getText()) return this.editor.setText(""), { consume: true };
|
|
178
|
+
this.exit();
|
|
179
|
+
return { consume: true };
|
|
180
|
+
}
|
|
181
|
+
if (this.overlays.length) return undefined;
|
|
182
|
+
if (matchesKey(data, "ctrl+d") && !this.editor.getText()) return this.exit(), { consume: true };
|
|
183
|
+
if (matchesKey(data, "escape") && !this.editor.isShowingAutocomplete()) {
|
|
184
|
+
if (this.busy()) void this.interrupt();
|
|
185
|
+
return { consume: true };
|
|
186
|
+
}
|
|
187
|
+
if (matchesKey(data, "alt+enter")) {
|
|
188
|
+
const text = this.editor.getText();
|
|
189
|
+
this.editor.setText("");
|
|
190
|
+
void this.submit(text, "followUp");
|
|
191
|
+
return { consume: true };
|
|
192
|
+
}
|
|
193
|
+
if (matchesKey(data, "shift+tab")) return void this.cycleThinking(), { consume: true };
|
|
194
|
+
if (matchesKey(data, "ctrl+l")) return void pickModel(this), { consume: true };
|
|
195
|
+
if (matchesKey(data, "ctrl+o")) {
|
|
196
|
+
this.view.expand = !this.view.expand;
|
|
197
|
+
this.redraw();
|
|
198
|
+
return { consume: true };
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async submit(text: string, whenBusy: WhenBusy) {
|
|
204
|
+
const content = text.trim();
|
|
205
|
+
if (!content) return;
|
|
206
|
+
if (content.startsWith("/") && (await runCommand(this, content))) return;
|
|
207
|
+
if (!this.handle) return this.flash("Pick a session first (/sessions).", true);
|
|
208
|
+
// The same requestId on every retry: the device admits the message exactly once.
|
|
209
|
+
const requestId = newId();
|
|
210
|
+
for (let attempt = 1; ; attempt++) {
|
|
211
|
+
try {
|
|
212
|
+
await this.handle.send(content, whenBusy, requestId);
|
|
213
|
+
this.editor.addToHistory(content);
|
|
214
|
+
if (whenBusy === "steer") this.flash("Steering: joins after the current tool round");
|
|
215
|
+
return;
|
|
216
|
+
} catch (e) {
|
|
217
|
+
if (isDpiError(e) && e.code === "offline" && attempt < 3) {
|
|
218
|
+
await sleep(1000 * attempt);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (!this.editor.getText()) this.editor.setText(text);
|
|
222
|
+
this.flash(isDpiError(e) && e.code === "busy" ? "Agent is busy (reject mode)" : (e as Error).message, true);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private async interrupt() {
|
|
229
|
+
await this.run(async () => {
|
|
230
|
+
await this.handle?.abort(false);
|
|
231
|
+
this.flash("Interrupted. Background jobs continue.");
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async cycleThinking() {
|
|
236
|
+
if (!this.handle) return;
|
|
237
|
+
const current = (this.model.state.agent.thinkingLevel ?? "low") as ThinkingLevel;
|
|
238
|
+
const next = THINKING_LEVELS[(THINKING_LEVELS.indexOf(current) + 1) % THINKING_LEVELS.length];
|
|
239
|
+
await this.run(() => this.handle!.configure({ thinkingLevel: next }));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ------------------------------------------------------------------ overlays and helpers
|
|
243
|
+
|
|
244
|
+
overlay(component: Component, onClose?: () => void) {
|
|
245
|
+
this.overlays.push({
|
|
246
|
+
handle: this.tui.showOverlay(component, { width: "70%", minWidth: 40, maxHeight: "80%" }),
|
|
247
|
+
onClose,
|
|
248
|
+
});
|
|
249
|
+
this.tui.requestRender();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
closeOverlay(exitIfNothing = false) {
|
|
253
|
+
const overlay = this.overlays.pop();
|
|
254
|
+
overlay?.handle.hide();
|
|
255
|
+
overlay?.onClose?.();
|
|
256
|
+
if (!this.overlays.length) this.tui.setFocus(this.editor);
|
|
257
|
+
if (exitIfNothing) return this.exit();
|
|
258
|
+
this.tui.requestRender();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
prompt(label: string, initial: string) {
|
|
262
|
+
return new Promise<string | undefined>((done) => {
|
|
263
|
+
this.overlay(
|
|
264
|
+
new Prompt(
|
|
265
|
+
label,
|
|
266
|
+
initial,
|
|
267
|
+
(value) => {
|
|
268
|
+
this.closeOverlay();
|
|
269
|
+
done(value.trim());
|
|
270
|
+
},
|
|
271
|
+
() => {
|
|
272
|
+
this.closeOverlay();
|
|
273
|
+
done(undefined);
|
|
274
|
+
},
|
|
275
|
+
),
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
flash(text: string, error = false) {
|
|
281
|
+
this.notice = { text, error, at: Date.now() };
|
|
282
|
+
this.redraw(false);
|
|
283
|
+
setTimeout(() => this.redraw(false), 6100);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async run(fn: () => Promise<unknown>) {
|
|
287
|
+
try {
|
|
288
|
+
await fn();
|
|
289
|
+
} catch (e) {
|
|
290
|
+
this.flash((e as Error).message, true);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
exit() {
|
|
295
|
+
this.streams?.abort();
|
|
296
|
+
clearInterval(this.ticker);
|
|
297
|
+
this.tui.stop();
|
|
298
|
+
const ref = this.reattachRef();
|
|
299
|
+
if (ref) {
|
|
300
|
+
const adhoc = this.device.id === "adhoc";
|
|
301
|
+
const target = adhoc
|
|
302
|
+
? `--url ${this.device.url} --token <token> -s ${shellWord(ref.session)} -a ${shellWord(ref.agent)}`
|
|
303
|
+
: shellWord(`${this.device.id}:${ref.session}:${ref.agent}`);
|
|
304
|
+
process.stdout.write(
|
|
305
|
+
`\n${c.muted("The agent keeps running on")} ${adhoc ? this.device.name : this.device.id}${c.muted(". Reattach:")}\n just tui attach ${target}\n\n`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
process.exit(0);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Names when they resolve to exactly this session and agent, otherwise ids. */
|
|
312
|
+
private reattachRef() {
|
|
313
|
+
const session = this.sessionSummary();
|
|
314
|
+
const agent = session?.agents.find((a) => a.id === this.agentId);
|
|
315
|
+
if (!session || this.agentId === undefined) return undefined;
|
|
316
|
+
const usable = (name: string) => !name.includes(":") && !name.includes("'");
|
|
317
|
+
const same = (a: string, b: string) => a.toLowerCase() === b.toLowerCase();
|
|
318
|
+
const sessionRef =
|
|
319
|
+
usable(session.name) && this.sessions.filter((s) => same(s.name, session.name)).length === 1 ? session.name : session.id;
|
|
320
|
+
const agentRef =
|
|
321
|
+
agent?.named && usable(agent.name) && session.agents.filter((a) => same(a.name, agent.name)).length === 1
|
|
322
|
+
? agent.name
|
|
323
|
+
: this.agentId === 1
|
|
324
|
+
? "main"
|
|
325
|
+
: String(this.agentId);
|
|
326
|
+
return { session: sessionRef, agent: agentRef };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ------------------------------------------------------------------ chrome
|
|
330
|
+
|
|
331
|
+
/** Context window left, in percent: the last reply's prompt + output against the model's window. */
|
|
332
|
+
private contextLeft(): number | undefined {
|
|
333
|
+
const s = this.model.state;
|
|
334
|
+
const m = s.agent.model;
|
|
335
|
+
const window = this.info.models.find((x) => x.id === m?.modelId && x.provider === m?.provider)?.contextWindow;
|
|
336
|
+
if (!window) return undefined;
|
|
337
|
+
let used = 0;
|
|
338
|
+
for (let i = s.entries.length - 1; i >= 0 && !used; i--) {
|
|
339
|
+
const entry = s.entries[i];
|
|
340
|
+
if (entry.kind !== "pi.assistant") continue;
|
|
341
|
+
const reply = entry.model?.find((x) => x.role === "assistant") as WireAssistantMessage | undefined;
|
|
342
|
+
const u = reply?.usage;
|
|
343
|
+
if (u) used = u.totalTokens || u.input + u.output + u.cacheRead + u.cacheWrite;
|
|
344
|
+
}
|
|
345
|
+
return Math.max(0, Math.min(100, Math.floor(100 * (1 - used / window))));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private statusLines() {
|
|
349
|
+
const s = this.model.state;
|
|
350
|
+
const lines: string[] = [];
|
|
351
|
+
if (this.busy())
|
|
352
|
+
lines.push(
|
|
353
|
+
`${c.warn("●")} ${c.secondary(s.retry ? `retrying: ${s.retry.error}` : s.compactions.length ? "compacting…" : "working…")} ${c.muted("esc to interrupt")}`,
|
|
354
|
+
);
|
|
355
|
+
for (const item of s.inbox)
|
|
356
|
+
lines.push(c.muted(`${item.mode === "steer" ? "↪ steer" : "⏳ queued"}: ${(item.content ?? "context update").replace(/\s+/g, " ")}`));
|
|
357
|
+
for (const n of s.notices.slice(-2)) lines.push(c.err(n));
|
|
358
|
+
if (this.notice && Date.now() - this.notice.at < 6000)
|
|
359
|
+
lines.push(this.notice.error ? c.err(this.notice.text) : c.ok(this.notice.text));
|
|
360
|
+
return lines;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private redraw(items = true) {
|
|
364
|
+
const s = this.model.state;
|
|
365
|
+
if (items) this.view.update(s.items);
|
|
366
|
+
const running = s.items.some(
|
|
367
|
+
(i) => (i.kind === "tool" && i.call.status === "running") || (i.kind === "assistant" && i.blocks.some((b) => b.type === "tool" && b.call.status === "running")),
|
|
368
|
+
);
|
|
369
|
+
if (running && !this.ticker)
|
|
370
|
+
this.ticker = setInterval(() => {
|
|
371
|
+
spinner.frame++;
|
|
372
|
+
this.tui.requestRender();
|
|
373
|
+
}, 120);
|
|
374
|
+
else if (!running && this.ticker) {
|
|
375
|
+
clearInterval(this.ticker);
|
|
376
|
+
this.ticker = undefined;
|
|
377
|
+
}
|
|
378
|
+
const conn =
|
|
379
|
+
this.connection === "live"
|
|
380
|
+
? c.ok("● live")
|
|
381
|
+
: this.connection === "idle"
|
|
382
|
+
? c.muted("○ no agent")
|
|
383
|
+
: this.connection === "connecting"
|
|
384
|
+
? c.warn("◌ connecting")
|
|
385
|
+
: c.err(`○ ${this.connection}`);
|
|
386
|
+
const lineage = this.agentId === undefined ? [] : this.session.lineage(this.agentId);
|
|
387
|
+
const label = (kind: string, value: string) => `${c.muted(kind)} ${value}`;
|
|
388
|
+
const agentLabel = lineage.length
|
|
389
|
+
? lineage
|
|
390
|
+
.map((a) =>
|
|
391
|
+
a.kind === "fork"
|
|
392
|
+
? label("fork", `⑂ ${a.name}`)
|
|
393
|
+
: a.kind === "subagent"
|
|
394
|
+
? label("subagent", `↳ ${a.name}`)
|
|
395
|
+
: label("agent", a.name),
|
|
396
|
+
)
|
|
397
|
+
.join(c.muted(" › "))
|
|
398
|
+
: this.agentId !== undefined
|
|
399
|
+
? label("agent", `#${this.agentId}`)
|
|
400
|
+
: "";
|
|
401
|
+
const session = this.sessions.find((x) => x.id === this.sessionId);
|
|
402
|
+
this.header.text = " " + (
|
|
403
|
+
[
|
|
404
|
+
c.bold(c.accent("dpi")),
|
|
405
|
+
label("device", c.secondary(this.device.id)),
|
|
406
|
+
session ? label("workspace", c.bold(session.name)) : c.muted("no workspace"),
|
|
407
|
+
agentLabel,
|
|
408
|
+
conn,
|
|
409
|
+
]
|
|
410
|
+
.filter(Boolean)
|
|
411
|
+
.join(c.muted(" │ "))
|
|
412
|
+
);
|
|
413
|
+
this.status.setText(this.statusLines().join("\n"));
|
|
414
|
+
const u = sumUsage(s.usage);
|
|
415
|
+
const model = (s.agent.model?.modelId ?? "").split("/").pop();
|
|
416
|
+
const ctx = this.contextLeft();
|
|
417
|
+
const bg = this.agentId === undefined ? 0 : this.session.backgroundTasks(this.agentId).length;
|
|
418
|
+
this.footer.text = " " + (
|
|
419
|
+
c.muted(
|
|
420
|
+
[
|
|
421
|
+
model ? `${model}: ${s.agent.thinkingLevel ?? "low"}` : "",
|
|
422
|
+
ctx === undefined ? "" : `${ctx}% CTX`,
|
|
423
|
+
`${compact(u.tokens)} tok $${u.cost.toFixed(3)}`,
|
|
424
|
+
bg ? `${bg} bg` : "",
|
|
425
|
+
"/help",
|
|
426
|
+
]
|
|
427
|
+
.filter(Boolean)
|
|
428
|
+
.join(" │ "),
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
this.tui.requestRender();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Slash commands. Anything that is not a known command goes to the agent as text, as in pi.
|
|
2
|
+
import type { SlashCommand } from "@earendil-works/pi-tui";
|
|
3
|
+
import { THINKING_LEVELS, type ThinkingLevel } from "../../dpi-sdk/src/api.ts";
|
|
4
|
+
import type { App } from "./app.ts";
|
|
5
|
+
import { Panel, pickAgent, pickDevice, pickModel, pickSession } from "./pickers.ts";
|
|
6
|
+
import { parsePairArgs, runPairing } from "./pairing.ts";
|
|
7
|
+
import { c } from "./theme.ts";
|
|
8
|
+
|
|
9
|
+
export const COMMANDS: SlashCommand[] = [
|
|
10
|
+
{ name: "sessions", description: "switch or create a session on this device" },
|
|
11
|
+
{ name: "agents", description: "switch agent: roots, forks, subagents" },
|
|
12
|
+
{ name: "model", description: "choose this agent's model" },
|
|
13
|
+
{ name: "devices", description: "switch device" },
|
|
14
|
+
{ name: "new", description: "new agent in this session", argumentHint: "[name]" },
|
|
15
|
+
{ name: "fork", description: "fork at an entry (default: the last reply)", argumentHint: "[entryId] [name]" },
|
|
16
|
+
{
|
|
17
|
+
name: "thinking",
|
|
18
|
+
description: "set the thinking level",
|
|
19
|
+
argumentHint: "<level>",
|
|
20
|
+
getArgumentCompletions: (prefix) =>
|
|
21
|
+
THINKING_LEVELS.filter((l) => l.startsWith(prefix)).map((l) => ({ value: l, label: l })),
|
|
22
|
+
},
|
|
23
|
+
{ name: "compact", description: "summarize older context", argumentHint: "[instructions]" },
|
|
24
|
+
{ name: "reset", description: "start a fresh context", argumentHint: "[handoff note]" },
|
|
25
|
+
{ name: "name", description: "rename this agent", argumentHint: "<name>" },
|
|
26
|
+
{ name: "tasks", description: "live tasks in this session" },
|
|
27
|
+
{ name: "pair", description: "pair THIS device with a hub", argumentHint: "<CODE|PAIR_URL> [--hub ORIGIN] [--force]" },
|
|
28
|
+
{ name: "help", description: "keys and commands" },
|
|
29
|
+
{ name: "quit", description: "exit (the agent keeps running)" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const KEYS: [string, string][] = [
|
|
33
|
+
["enter", "send; steers the agent while it works"],
|
|
34
|
+
["alt+enter", "queue as a follow-up"],
|
|
35
|
+
["esc", "interrupt foreground work"],
|
|
36
|
+
["shift+tab", "cycle thinking level"],
|
|
37
|
+
["ctrl+l", "model picker"],
|
|
38
|
+
["ctrl+o", "expand tool output and thinking"],
|
|
39
|
+
["ctrl+c", "clear input, or exit when empty"],
|
|
40
|
+
["ctrl+d", "exit"],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** Returns false when `text` is not a command, so the caller sends it to the agent. */
|
|
44
|
+
export async function runCommand(app: App, text: string): Promise<boolean> {
|
|
45
|
+
const match = text.match(/^\/(\S+)\s*([\s\S]*)$/);
|
|
46
|
+
const command = COMMANDS.find((c) => c.name === match?.[1]);
|
|
47
|
+
if (!match || !command) return false;
|
|
48
|
+
const arg = match[2].trim();
|
|
49
|
+
const handle = app.handle;
|
|
50
|
+
switch (command.name) {
|
|
51
|
+
case "sessions":
|
|
52
|
+
await pickSession(app);
|
|
53
|
+
break;
|
|
54
|
+
case "agents":
|
|
55
|
+
pickAgent(app);
|
|
56
|
+
break;
|
|
57
|
+
case "model":
|
|
58
|
+
await pickModel(app);
|
|
59
|
+
break;
|
|
60
|
+
case "devices":
|
|
61
|
+
pickDevice(app);
|
|
62
|
+
break;
|
|
63
|
+
case "new":
|
|
64
|
+
await app.run(async () => {
|
|
65
|
+
const created = await app.client.session(app.sessionId!).createAgent(arg ? { name: arg } : {});
|
|
66
|
+
await app.attach(app.sessionId!, created.id);
|
|
67
|
+
app.flash(`New agent ${created.name}`);
|
|
68
|
+
});
|
|
69
|
+
break;
|
|
70
|
+
case "fork": {
|
|
71
|
+
const [first, ...rest] = arg.split(/\s+/).filter(Boolean);
|
|
72
|
+
const explicit = first && /^\d+$/.test(first) ? Number(first) : undefined;
|
|
73
|
+
const name = (explicit === undefined ? [first, ...rest] : rest).filter(Boolean).join(" ") || undefined;
|
|
74
|
+
const entryId =
|
|
75
|
+
explicit ?? app.model.state.entries.filter((e) => e.kind === "pi.assistant").at(-1)?.id;
|
|
76
|
+
if (entryId === undefined) return app.flash("Nothing to fork yet.", true), true;
|
|
77
|
+
await app.run(async () => {
|
|
78
|
+
const created = await handle!.fork({ entryId, name });
|
|
79
|
+
await app.attach(app.sessionId!, created.id);
|
|
80
|
+
app.flash(`Forked at #${entryId} → ${created.name}`);
|
|
81
|
+
});
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "thinking": {
|
|
85
|
+
if (!THINKING_LEVELS.includes(arg as ThinkingLevel))
|
|
86
|
+
return app.flash(`Levels: ${THINKING_LEVELS.join(", ")}`, true), true;
|
|
87
|
+
await app.run(async () => {
|
|
88
|
+
await handle!.configure({ thinkingLevel: arg as ThinkingLevel });
|
|
89
|
+
});
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "compact":
|
|
93
|
+
await app.run(async () => {
|
|
94
|
+
await handle!.compact(arg || undefined);
|
|
95
|
+
app.flash("Compaction requested");
|
|
96
|
+
});
|
|
97
|
+
break;
|
|
98
|
+
case "reset":
|
|
99
|
+
await app.run(async () => {
|
|
100
|
+
await handle!.reset(arg || undefined);
|
|
101
|
+
app.flash("New context started; history stays stored");
|
|
102
|
+
});
|
|
103
|
+
break;
|
|
104
|
+
case "name":
|
|
105
|
+
if (!arg) return app.flash("Usage: /name <name>", true), true;
|
|
106
|
+
await app.run(async () => {
|
|
107
|
+
await handle!.rename(arg);
|
|
108
|
+
app.flash(`Agent renamed to ${arg}`);
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
case "pair":
|
|
112
|
+
await runPairing(app, parsePairArgs(arg));
|
|
113
|
+
break;
|
|
114
|
+
case "tasks":
|
|
115
|
+
app.overlay(
|
|
116
|
+
new Panel(() => {
|
|
117
|
+
const tasks = app.session.state.tasks;
|
|
118
|
+
const lines = [c.bold(c.accent(`Live tasks in ${app.sessionSummary()?.name ?? "session"}`)), ""];
|
|
119
|
+
if (!tasks.length) lines.push(c.muted("No live tasks."));
|
|
120
|
+
for (const t of tasks)
|
|
121
|
+
lines.push(
|
|
122
|
+
`${t.background ? c.secondary("◌ bg") : c.warn("● fg")} #${t.id} ${t.kind} ${c.muted(`agent #${t.conversationId} │ ${t.state.status}${t.state.phase ? `/${t.state.phase}` : ""}`)}`,
|
|
123
|
+
);
|
|
124
|
+
lines.push("", c.muted("esc close"));
|
|
125
|
+
return lines;
|
|
126
|
+
}, () => app.closeOverlay()),
|
|
127
|
+
);
|
|
128
|
+
break;
|
|
129
|
+
case "help":
|
|
130
|
+
app.overlay(
|
|
131
|
+
new Panel(
|
|
132
|
+
() => [
|
|
133
|
+
c.bold(c.accent("dpi-tui")),
|
|
134
|
+
c.muted(`${app.device.id} │ ${app.device.url}`),
|
|
135
|
+
"",
|
|
136
|
+
...KEYS.map(([k, d]) => `${c.secondary(k.padEnd(11))} ${d}`),
|
|
137
|
+
"",
|
|
138
|
+
...COMMANDS.map((x) => `${c.secondary(`/${x.name}`.padEnd(11))} ${x.description ?? ""}`),
|
|
139
|
+
"",
|
|
140
|
+
c.muted("Closing dpi-tui never stops the agent. esc to close."),
|
|
141
|
+
],
|
|
142
|
+
() => app.closeOverlay(),
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
break;
|
|
146
|
+
case "quit":
|
|
147
|
+
app.exit();
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|