@visual-z/xeno 0.2.1 → 0.4.0
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/CHANGELOG.md +18 -0
- package/README.md +126 -34
- package/docs/app-server.md +84 -0
- package/docs/install.md +51 -13
- package/docs/providers.md +24 -3
- package/package.json +7 -5
- package/src/apps/app-server.ts +28 -0
- package/src/apps/approve.ts +3 -3
- package/src/apps/cli.ts +25 -10
- package/src/apps/commands.ts +45 -8
- package/src/apps/completions.ts +78 -22
- package/src/apps/control-transport.ts +89 -0
- package/src/apps/image-draft.ts +46 -0
- package/src/apps/model-catalog.ts +2 -1
- package/src/apps/project-trust.ts +2 -42
- package/src/apps/provider-connect.ts +346 -0
- package/src/apps/render.ts +22 -47
- package/src/apps/resources.ts +5 -2
- package/src/apps/shell.ts +333 -1542
- package/src/apps/startup-input.ts +45 -9
- package/src/apps/startup.ts +149 -94
- package/src/apps/ui/app.ts +1553 -0
- package/src/apps/ui/control-model.ts +26 -0
- package/src/apps/ui/extension-host.ts +194 -0
- package/src/apps/ui/image-cache.ts +78 -0
- package/src/apps/ui/palette.ts +88 -0
- package/src/apps/ui/project.ts +127 -0
- package/src/apps/ui/standalone.ts +146 -0
- package/src/apps/ui/status-model.ts +68 -0
- package/src/apps/ui/surface.ts +86 -0
- package/src/apps/ui/theme.ts +86 -0
- package/src/core/acceptance.ts +107 -0
- package/src/core/action-defaults.ts +47 -295
- package/src/core/benchmark.ts +2 -2
- package/src/core/branch-receipt.ts +24 -0
- package/src/core/checkpoints.ts +64 -0
- package/src/core/churn.ts +2 -0
- package/src/core/command-artifacts.ts +94 -0
- package/src/core/command-contract.ts +35 -0
- package/src/core/completion-action.ts +20 -0
- package/src/core/completion-review.ts +37 -21
- package/src/core/context-detail.ts +37 -0
- package/src/core/context-policy.ts +74 -1
- package/src/core/continuation.ts +17 -2
- package/src/core/control-protocol.ts +62 -0
- package/src/core/control-view.ts +42 -0
- package/src/core/derive.ts +29 -3
- package/src/core/draft-images.ts +56 -0
- package/src/core/environment.ts +145 -0
- package/src/core/evaluation-coverage.ts +74 -0
- package/src/core/event-validation.ts +81 -6
- package/src/core/events.ts +99 -3
- package/src/core/evidence-links.ts +7 -0
- package/src/core/evidence.ts +13 -3
- package/src/core/execution.ts +8 -2
- package/src/core/experiment.ts +133 -0
- package/src/core/export.ts +9 -0
- package/src/core/failure-recovery.ts +173 -0
- package/src/core/file-precondition.ts +72 -0
- package/src/core/handoff.ts +65 -4
- package/src/core/harness-diagnostics.ts +206 -0
- package/src/core/history-query.ts +38 -2
- package/src/core/history.ts +38 -1
- package/src/core/integration-health.ts +34 -0
- package/src/core/keymap.ts +8 -20
- package/src/core/model-picker.ts +64 -0
- package/src/core/native-eval.ts +25 -0
- package/src/core/note-guidance.ts +34 -0
- package/src/core/oauth-fields.ts +44 -0
- package/src/core/observation-view.ts +49 -8
- package/src/core/permissions.ts +24 -2
- package/src/core/process-receipt.ts +105 -0
- package/src/core/process-state.ts +37 -0
- package/src/core/program-status.ts +31 -0
- package/src/core/project.ts +70 -19
- package/src/core/prompt-policy.ts +37 -0
- package/src/core/query-eval-contract.ts +14 -0
- package/src/core/read-observation.ts +82 -0
- package/src/core/recovery-corpus.ts +17 -0
- package/src/core/recovery-evaluation.ts +60 -0
- package/src/core/recovery-sources.ts +74 -0
- package/src/core/request-progress.ts +87 -0
- package/src/core/request-state.ts +114 -0
- package/src/core/runtime-target.ts +94 -0
- package/src/core/search-observation.ts +97 -0
- package/src/core/session-tree.ts +147 -0
- package/src/core/settings.ts +33 -10
- package/src/core/skills.ts +44 -0
- package/src/core/sql-query.ts +211 -0
- package/src/core/theme.ts +36 -58
- package/src/core/tool-contract.ts +51 -2
- package/src/core/tool-discovery.ts +87 -0
- package/src/core/tool-failure.ts +60 -0
- package/src/core/tool-input.ts +6 -0
- package/src/core/tool-mode-protocol.ts +25 -0
- package/src/core/transcript.ts +19 -4
- package/src/core/trial-lifecycle.ts +14 -0
- package/src/core/tui-mode.ts +29 -0
- package/src/core/update-check.ts +97 -0
- package/src/core/version.ts +55 -0
- package/src/core/work-state.ts +49 -3
- package/src/core/working-set.ts +91 -0
- package/src/runtime/auth.ts +11 -4
- package/src/runtime/branch-jobs.ts +36 -0
- package/src/runtime/catalog.ts +13 -3
- package/src/runtime/clipboard.ts +15 -4
- package/src/runtime/command-artifacts.ts +64 -0
- package/src/runtime/control-connection.ts +104 -0
- package/src/runtime/environment-tools.ts +60 -0
- package/src/runtime/evidence-notes.ts +4 -2
- package/src/runtime/exec-sessions.ts +206 -61
- package/src/runtime/execution-control.ts +1 -0
- package/src/runtime/extension-ui.ts +22 -30
- package/src/runtime/extensions.ts +95 -16
- package/src/runtime/harness-comparison.ts +27 -7
- package/src/runtime/harness-eval.ts +462 -0
- package/src/runtime/history-cache.ts +57 -0
- package/src/runtime/history-tools.ts +62 -12
- package/src/runtime/integrations.ts +210 -0
- package/src/runtime/loop.ts +1088 -282
- package/src/runtime/model-discovery.ts +208 -0
- package/src/runtime/model.ts +9 -0
- package/src/runtime/native-eval-fixtures.ts +33 -0
- package/src/runtime/native-eval.ts +146 -0
- package/src/runtime/oauth.ts +80 -3
- package/src/runtime/pi-provider.ts +169 -12
- package/src/runtime/program-status.ts +38 -0
- package/src/runtime/provider-connect.ts +109 -0
- package/src/runtime/provider-presets.ts +87 -0
- package/src/runtime/provider-request.ts +13 -2
- package/src/runtime/provider.ts +23 -9
- package/src/runtime/query-eval-verifier.ts +28 -0
- package/src/runtime/recovery-coverage.ts +71 -0
- package/src/runtime/recovery-evidence.ts +76 -0
- package/src/runtime/recovery-fixtures.ts +116 -0
- package/src/runtime/runtime-target.ts +149 -0
- package/src/runtime/session-control.ts +198 -0
- package/src/runtime/tool-broker.ts +137 -0
- package/src/runtime/tool-discovery.ts +57 -0
- package/src/runtime/tool-mode-eval.ts +146 -0
- package/src/runtime/tool-queue.ts +77 -0
- package/src/runtime/tools.ts +628 -183
- package/src/runtime/trial-control.ts +45 -0
- package/src/runtime/update-check.ts +47 -0
- package/src/runtime/window-trial.ts +2 -1
- package/src/runtime/working-notes.ts +33 -3
- package/src/runtime/workspace-lifecycle.ts +97 -0
- package/src/store/acceptance.ts +15 -0
- package/src/store/checkpoint-history.ts +43 -0
- package/src/store/command-artifacts.ts +96 -0
- package/src/store/command.ts +65 -37
- package/src/store/file-checkpoints.ts +104 -0
- package/src/store/file-index.ts +13 -1
- package/src/store/git-checkpoints.ts +67 -0
- package/src/store/observations.ts +61 -20
- package/src/store/process-receipts.ts +34 -0
- package/src/store/query-source.ts +13 -8
- package/src/store/recovery.ts +2 -3
- package/src/store/replay.ts +16 -4
- package/src/store/runtime-target.ts +64 -0
- package/src/store/search.ts +171 -132
- package/src/store/session-archive.ts +4 -0
- package/src/store/source-cancellation.ts +6 -0
- package/src/store/update-cache.ts +36 -0
- package/src/store/workspace-observation.ts +113 -0
- package/src/tui/ansi-spans.ts +126 -0
- package/src/tui/style.ts +19 -16
- package/src/tui/ui/atomic-draft.ts +121 -0
- package/src/tui/ui/canvas.ts +96 -0
- package/src/tui/ui/colors.ts +70 -0
- package/src/tui/ui/composer.ts +200 -0
- package/src/tui/ui/control-panel.ts +23 -0
- package/src/tui/ui/dialog.ts +379 -0
- package/src/tui/ui/draft.ts +23 -0
- package/src/tui/ui/edge.ts +186 -0
- package/src/tui/ui/image-size.ts +40 -0
- package/src/tui/ui/ink.ts +210 -0
- package/src/tui/ui/items.ts +40 -0
- package/src/tui/ui/keys.ts +40 -0
- package/src/tui/ui/layout.ts +74 -0
- package/src/tui/ui/list-model.ts +86 -0
- package/src/tui/ui/list-view.ts +172 -0
- package/src/tui/ui/mascot.ts +67 -0
- package/src/tui/ui/messages.ts +518 -0
- package/src/tui/ui/motion.ts +28 -0
- package/src/tui/ui/paths.ts +10 -0
- package/src/tui/ui/pictures.ts +78 -0
- package/src/tui/ui/screen.ts +421 -0
- package/src/tui/ui/status.ts +279 -0
- package/src/tui/ui/styled.ts +85 -0
- package/src/tui/ui/testing.ts +5 -0
- package/src/tui/ui/tool-card.ts +150 -0
- package/src/apps/extension-ui.ts +0 -330
- package/src/apps/resource-browser.ts +0 -75
- package/src/apps/session-browser.ts +0 -113
- package/src/apps/session-picker.ts +0 -55
- package/src/tui/ansi.ts +0 -67
- package/src/tui/autocomplete.ts +0 -29
- package/src/tui/chat-viewport.ts +0 -47
- package/src/tui/complete.ts +0 -87
- package/src/tui/editor.ts +0 -940
- package/src/tui/host.ts +0 -264
- package/src/tui/input.ts +0 -75
- package/src/tui/keys.ts +0 -378
- package/src/tui/markdown.ts +0 -17
- package/src/tui/render.ts +0 -153
- package/src/tui/select.ts +0 -258
- package/src/tui/settings.ts +0 -44
- package/src/tui/statusline.ts +0 -162
- package/src/tui/system-theme.ts +0 -700
- package/src/tui/terminal.ts +0 -30
- package/src/tui/text.ts +0 -52
- package/src/tui/toolview.ts +0 -386
- package/src/tui/transcript.ts +0 -123
- package/src/tui/tree.ts +0 -471
- package/src/vendor/pi-tui/LICENSE.upstream +0 -21
- package/src/vendor/pi-tui/UPSTREAM.json +0 -50
- package/src/vendor/pi-tui/VENDOR.md +0 -25
- package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
- package/src/vendor/pi-tui/autocomplete.ts +0 -860
- package/src/vendor/pi-tui/colors.ts +0 -367
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
- package/src/vendor/pi-tui/components/box.ts +0 -166
- package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
- package/src/vendor/pi-tui/components/editor.ts +0 -2472
- package/src/vendor/pi-tui/components/h-stack.ts +0 -44
- package/src/vendor/pi-tui/components/image.ts +0 -127
- package/src/vendor/pi-tui/components/input.ts +0 -494
- package/src/vendor/pi-tui/components/loader.ts +0 -101
- package/src/vendor/pi-tui/components/markdown.ts +0 -1021
- package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
- package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
- package/src/vendor/pi-tui/components/select-list.ts +0 -273
- package/src/vendor/pi-tui/components/settings-list.ts +0 -328
- package/src/vendor/pi-tui/components/spacer.ts +0 -28
- package/src/vendor/pi-tui/components/stack.ts +0 -154
- package/src/vendor/pi-tui/components/text.ts +0 -107
- package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
- package/src/vendor/pi-tui/components/v-stack.ts +0 -33
- package/src/vendor/pi-tui/editor-component.ts +0 -74
- package/src/vendor/pi-tui/fuzzy.ts +0 -138
- package/src/vendor/pi-tui/index.ts +0 -186
- package/src/vendor/pi-tui/keybindings.ts +0 -320
- package/src/vendor/pi-tui/keys.ts +0 -1401
- package/src/vendor/pi-tui/kill-ring.ts +0 -46
- package/src/vendor/pi-tui/latex.ts +0 -1506
- package/src/vendor/pi-tui/layout-node.ts +0 -51
- package/src/vendor/pi-tui/layout.ts +0 -449
- package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
- package/src/vendor/pi-tui/native/clipboard.h +0 -208
- package/src/vendor/pi-tui/native/darwin/README.md +0 -22
- package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
- package/src/vendor/pi-tui/native/linux/README.md +0 -23
- package/src/vendor/pi-tui/native/linux/build.sh +0 -65
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
- package/src/vendor/pi-tui/native/napi.h +0 -91
- package/src/vendor/pi-tui/native/win32/README.md +0 -31
- package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
- package/src/vendor/pi-tui/native-modifiers.ts +0 -13
- package/src/vendor/pi-tui/native-module-path.ts +0 -31
- package/src/vendor/pi-tui/native-platform.ts +0 -65
- package/src/vendor/pi-tui/oklab.ts +0 -233
- package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
- package/src/vendor/pi-tui/terminal-colors.ts +0 -91
- package/src/vendor/pi-tui/terminal-image.ts +0 -731
- package/src/vendor/pi-tui/terminal.ts +0 -554
- package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
- package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
- package/src/vendor/pi-tui/tui.ts +0 -1493
- package/src/vendor/pi-tui/undo-stack.ts +0 -28
- package/src/vendor/pi-tui/utils.ts +0 -1385
- package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
- package/src/vendor/pi-tui/word-navigation.ts +0 -117
package/src/apps/approve.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { PermissionRequest } from "../core/permissions.ts";
|
|
11
11
|
import type { ApprovalChoice } from "../runtime/tools.ts";
|
|
12
|
-
import type {
|
|
12
|
+
import type { ListItem } from "../tui/ui/list-model.ts";
|
|
13
13
|
|
|
14
14
|
export function approvalHeading(request: PermissionRequest): string {
|
|
15
15
|
if (request.command !== undefined) {
|
|
@@ -24,8 +24,8 @@ export function approvalHeading(request: PermissionRequest): string {
|
|
|
24
24
|
return `${request.tool} wants to run. Approval is the last look, not a boundary.`;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function approvalItems(request: PermissionRequest):
|
|
28
|
-
const items:
|
|
27
|
+
export function approvalItems(request: PermissionRequest): ListItem[] {
|
|
28
|
+
const items: ListItem[] = [{ value: "once", label: "this once" }];
|
|
29
29
|
if (request.command !== undefined) {
|
|
30
30
|
items.push({ value: "command", label: "this command this session" });
|
|
31
31
|
}
|
package/src/apps/cli.ts
CHANGED
|
@@ -19,8 +19,16 @@ import { createStartupInput } from "./startup-input.ts";
|
|
|
19
19
|
import { runPrompt, runRepl } from "./shell.ts";
|
|
20
20
|
import { runPackageCommand } from "../runtime/package-manager.ts";
|
|
21
21
|
import { exportHtmlFile } from "./export-cli.ts";
|
|
22
|
+
import { CONTROL_SCHEMA } from "../core/control-protocol.ts";
|
|
23
|
+
import { sessionControl } from "../runtime/session-control.ts";
|
|
24
|
+
import { listenControl } from "./control-transport.ts";
|
|
25
|
+
import { runAppServer } from "./app-server.ts";
|
|
22
26
|
|
|
23
|
-
const
|
|
27
|
+
const serverMode = process.argv[2] === "app-server";
|
|
28
|
+
if (serverMode && process.argv[3] === "schema") { console.log(JSON.stringify(CONTROL_SCHEMA, null, 2)); process.exit(0); }
|
|
29
|
+
const args = parseArgs(process.argv.slice(serverMode ? 3 : 2), { deferExtensions: true });
|
|
30
|
+
if (serverMode) { args.print = true; args.mode = "json"; if (args.prompt) throw Error("app-server receives prompts through turn/start"); }
|
|
31
|
+
let closeControl: (() => Promise<void>) | undefined;
|
|
24
32
|
const input = createStartupInput({ interactive: isInteractiveSession(args) && !args.packageCommand && !args.version && !args.exportFile && !args.listModels && !args.modelsCommand });
|
|
25
33
|
process.once("exit", input.restore);
|
|
26
34
|
try {
|
|
@@ -43,22 +51,29 @@ try {
|
|
|
43
51
|
finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
|
|
44
52
|
} else {
|
|
45
53
|
const app = await startup(args, input);
|
|
46
|
-
if (args.
|
|
47
|
-
|
|
48
|
-
args.prompt = mergePrintPrompt(args.prompt,
|
|
49
|
-
await readPipedStdin(process.stdin, args.prompt.length > 0 ? { firstByteMs: 3_000 } : {}));
|
|
50
|
-
}
|
|
51
|
-
if (args.prompt) {
|
|
54
|
+
if (args.controlSocket) closeControl = await listenControl(args.controlSocket, () => sessionControl(app.session));
|
|
55
|
+
if (serverMode) {
|
|
52
56
|
input.restore();
|
|
53
|
-
await
|
|
57
|
+
await runAppServer(app);
|
|
54
58
|
} else {
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
if (args.print) {
|
|
60
|
+
// With a prompt already given, stdin is optional context: wait briefly for it.
|
|
61
|
+
args.prompt = mergePrintPrompt(args.prompt,
|
|
62
|
+
await readPipedStdin(process.stdin, args.prompt.length > 0 ? { firstByteMs: 3_000 } : {}));
|
|
63
|
+
}
|
|
64
|
+
if (args.prompt) {
|
|
65
|
+
input.restore();
|
|
66
|
+
await runPrompt(app, args.prompt);
|
|
67
|
+
} else {
|
|
68
|
+
input.phase("Loading conversation");
|
|
69
|
+
await runRepl(app, input.handoff);
|
|
70
|
+
}
|
|
57
71
|
}
|
|
58
72
|
}
|
|
59
73
|
} catch (error) {
|
|
60
74
|
console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1;
|
|
61
75
|
} finally {
|
|
76
|
+
await closeControl?.();
|
|
62
77
|
process.off("exit", input.restore);
|
|
63
78
|
input.restore();
|
|
64
79
|
}
|
package/src/apps/commands.ts
CHANGED
|
@@ -16,10 +16,17 @@ import { skillInvocationMessage, type Skill } from "../core/skills.ts";
|
|
|
16
16
|
|
|
17
17
|
export type Command =
|
|
18
18
|
| { kind: "extension"; name: string; args: string }
|
|
19
|
-
| { kind: "prompt"; text: string }
|
|
19
|
+
| { kind: "prompt"; text: string; skill?: Skill }
|
|
20
20
|
| { kind: "quit" }
|
|
21
21
|
| { kind: "usage" }
|
|
22
22
|
| { kind: "state" }
|
|
23
|
+
| { kind: "context" }
|
|
24
|
+
| { kind: "control" }
|
|
25
|
+
| { kind: "acceptance" }
|
|
26
|
+
| { kind: "tasks" }
|
|
27
|
+
| { kind: "branches"; action?: "wait" | "cancel" | "status"; node?: number }
|
|
28
|
+
| { kind: "processes" }
|
|
29
|
+
| { kind: "accept"; criterion: string }
|
|
23
30
|
| { kind: "model"; pattern?: string }
|
|
24
31
|
| { kind: "scoped-models" }
|
|
25
32
|
| { kind: "login"; provider?: string }
|
|
@@ -54,7 +61,7 @@ export const COMMANDS = [
|
|
|
54
61
|
{ name: "scoped-models", description: "Enable and order models for cycling", busy: "idle" },
|
|
55
62
|
{ name: "thinking", description: "Set reasoning effort", argumentHint: "[level]", busy: "idle" },
|
|
56
63
|
{ name: "settings", description: "Configure appearance, input and model behavior", argumentHint: "[key value]", busy: "idle" },
|
|
57
|
-
{ name: "
|
|
64
|
+
{ name: "provider", description: "Connect and manage providers: account, API key, gateway or local server", argumentHint: "[provider]", busy: "idle" },
|
|
58
65
|
{ name: "logout", description: "Remove a stored credential", argumentHint: "[provider]", busy: "idle" },
|
|
59
66
|
{ name: "new", description: "Start a new session", busy: "idle" },
|
|
60
67
|
{ name: "resume", description: "Search saved sessions", argumentHint: "[id]", busy: "idle" },
|
|
@@ -66,13 +73,20 @@ export const COMMANDS = [
|
|
|
66
73
|
{ name: "session", description: "Show session details", busy: "run" },
|
|
67
74
|
{ name: "usage", description: "Show token usage and costs", busy: "run" },
|
|
68
75
|
{ name: "state", description: "Show log and handoff state", busy: "run" },
|
|
76
|
+
{ name: "context", description: "Explain context sources, omissions and token estimates", busy: "run" },
|
|
77
|
+
{ name: "control", description: "Inspect the goal, evidence and context; control the active turn", busy: "run" },
|
|
78
|
+
{ name: "acceptance", description: "Show declared criteria and current evidence", busy: "idle" },
|
|
79
|
+
{ name: "tasks", description: "Show work dependencies, readiness and ownership", busy: "run" },
|
|
80
|
+
{ name: "branches", description: "Inspect or join owned background work", argumentHint: "[wait|cancel|status node]", busy: "idle" },
|
|
81
|
+
{ name: "processes", description: "Show process observations and available handles", busy: "run" },
|
|
82
|
+
{ name: "accept", description: "Record your acceptance of a human criterion", argumentHint: "<criterion ID>", busy: "idle" },
|
|
69
83
|
{ name: "export", description: "Export HTML or JSONL", argumentHint: "[file]", busy: "idle" },
|
|
70
84
|
{ name: "import", description: "Import a portable JSONL session", argumentHint: "<file>", busy: "idle" },
|
|
71
85
|
{ name: "debug", description: "Write local terminal diagnostics", argumentHint: "[file]", busy: "run" },
|
|
72
86
|
{ name: "changelog", description: "Show local release notes", busy: "run" },
|
|
73
87
|
{ name: "copy", description: "Copy the last assistant reply", busy: "run" },
|
|
74
88
|
{ name: "attachments", description: "Remove images from the current draft", busy: "run" },
|
|
75
|
-
{ name: "models", description: "Refresh the model catalog", argumentHint: "refresh", busy: "idle" },
|
|
89
|
+
{ name: "models", description: "Refresh the model catalog", argumentHint: "<refresh>", busy: "idle" },
|
|
76
90
|
{ name: "trust", description: "Set workspace configuration trust", argumentHint: "[here|parent|never]", busy: "idle" },
|
|
77
91
|
{ name: "reload", description: "Reload settings, skills, prompts, themes and models", busy: "idle" },
|
|
78
92
|
{ name: "config", description: "Configure resource sources and enablement", busy: "idle" },
|
|
@@ -82,10 +96,18 @@ export const COMMANDS = [
|
|
|
82
96
|
{ name: "exit", description: "Exit xeno", busy: "run" },
|
|
83
97
|
] as const;
|
|
84
98
|
|
|
85
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Hidden spellings of a listed command. Not in COMMANDS, so the `/` menu,
|
|
101
|
+
* the startup hint and completion show one entry per job.
|
|
102
|
+
*/
|
|
103
|
+
const ALIASES: Readonly<Record<string, string>> = { login: "provider", connect: "provider" };
|
|
104
|
+
|
|
105
|
+
// Hidden spellings still own their names, so resources cannot promise an unreachable route.
|
|
106
|
+
export const BUILTIN_COMMAND_NAMES: readonly string[] = [...COMMANDS.map(command => command.name), ...Object.keys(ALIASES)];
|
|
86
107
|
|
|
87
108
|
export function commandDefinition(input: string): (typeof COMMANDS)[number] | undefined {
|
|
88
|
-
const
|
|
109
|
+
const typed = input.trim().split(/\s/, 1)[0]?.slice(1).toLowerCase() ?? "";
|
|
110
|
+
const name = ALIASES[typed] ?? typed;
|
|
89
111
|
return COMMANDS.find((command) => command.name === name);
|
|
90
112
|
}
|
|
91
113
|
|
|
@@ -95,6 +117,7 @@ export interface SlashCatalog {
|
|
|
95
117
|
readonly prompts: ReadonlyMap<string, PromptTemplate>;
|
|
96
118
|
readonly skills: ReadonlyMap<string, Skill>;
|
|
97
119
|
readonly skillCommands?: boolean;
|
|
120
|
+
readonly skillExecution?: boolean;
|
|
98
121
|
}
|
|
99
122
|
|
|
100
123
|
export function resolveSlashCatalog(
|
|
@@ -140,7 +163,9 @@ export function parseCommand(input: string, catalog?: SlashCatalog): Command {
|
|
|
140
163
|
if (!text.startsWith("/")) return { kind: "prompt", text };
|
|
141
164
|
|
|
142
165
|
const [head, ...rest] = text.split(/\s+/);
|
|
143
|
-
const
|
|
166
|
+
const typed = (head ?? "").toLowerCase();
|
|
167
|
+
const token = `/${ALIASES[typed.slice(1)] ?? typed.slice(1)}`;
|
|
168
|
+
if (token === "/control") return { kind: "control" };
|
|
144
169
|
const arg = rest.join(" ");
|
|
145
170
|
|
|
146
171
|
if (!commandDefinition(text)) return resolveRegistered(token, arg, text, rest.length, catalog);
|
|
@@ -157,9 +182,20 @@ export function parseCommand(input: string, catalog?: SlashCatalog): Command {
|
|
|
157
182
|
return { kind: "usage" };
|
|
158
183
|
case "/state":
|
|
159
184
|
return { kind: "state" };
|
|
185
|
+
case "/context": return { kind: "context" };
|
|
186
|
+
case "/acceptance": return { kind: "acceptance" };
|
|
187
|
+
case "/tasks": return { kind: "tasks" };
|
|
188
|
+
case "/processes": return { kind: "processes" };
|
|
189
|
+
case "/branches": {
|
|
190
|
+
if (!arg) return { kind: "branches" };
|
|
191
|
+
const action = rest[0], node = Number(rest[1]);
|
|
192
|
+
return (action === "wait" || action === "cancel" || action === "status") && Number.isSafeInteger(node) && node >= 0 && rest.length === 2
|
|
193
|
+
? { kind: "branches", action, node } : { kind: "unknown", input: text };
|
|
194
|
+
}
|
|
195
|
+
case "/accept": return { kind: "accept", criterion: arg };
|
|
160
196
|
case "/model":
|
|
161
197
|
return arg.length > 0 ? { kind: "model", pattern: arg } : { kind: "model" };
|
|
162
|
-
case "/
|
|
198
|
+
case "/provider":
|
|
163
199
|
return rest[0] === undefined ? { kind: "login" } : { kind: "login", provider: rest[0] };
|
|
164
200
|
case "/logout":
|
|
165
201
|
return rest[0] === undefined ? { kind: "logout" } : { kind: "logout", provider: rest[0] };
|
|
@@ -240,7 +276,8 @@ function resolveRegistered(
|
|
|
240
276
|
const name = token.slice("/skill:".length);
|
|
241
277
|
const skill = catalog.skills.get(name);
|
|
242
278
|
if (skill !== undefined) {
|
|
243
|
-
return { kind: "
|
|
279
|
+
if (skill.userInvocable === false) return { kind: "unknown", input: text };
|
|
280
|
+
return { kind: "prompt", text: skillInvocationMessage(skill, arg), ...(catalog.skillExecution ? { skill } : {}) };
|
|
244
281
|
}
|
|
245
282
|
}
|
|
246
283
|
const prompt = catalog.prompts.get(token.slice(1));
|
package/src/apps/completions.ts
CHANGED
|
@@ -1,31 +1,87 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* What the composer offers for the text at the cursor. Pure given its
|
|
3
|
+
* sources, so the three cases are tested without a terminal:
|
|
4
|
+
* "/mo|" command names (the palette's own entries)
|
|
5
|
+
* "/thinking h|" the command's argument values
|
|
6
|
+
* "see @src/ap|" workspace paths (ranked fuzzy, quoted when they contain spaces)
|
|
7
|
+
* `@` inside a word (an e-mail) or after a backslash is not a reference.
|
|
8
|
+
*/
|
|
2
9
|
import type { App } from "./startup.ts";
|
|
3
|
-
import {
|
|
4
|
-
import { SUBSCRIPTION_OPTIONS } from "../runtime/oauth.ts";
|
|
5
|
-
import { modelKey } from "../core/session-position.ts";
|
|
10
|
+
import { SUBSCRIPTION_OPTIONS, subscriptionById } from "../runtime/oauth.ts";
|
|
6
11
|
import { supportedThinkingLevels } from "../core/thinking.ts";
|
|
7
|
-
import {
|
|
12
|
+
import { interactiveSettingKeys } from "../core/settings.ts";
|
|
13
|
+
import { fuzzyFilter } from "../tui/fuzzy.ts";
|
|
14
|
+
import { COMMANDS, commandDefinition } from "./commands.ts";
|
|
15
|
+
import { commandCompletionRank } from "../core/completion-action.ts";
|
|
16
|
+
import type { Completion } from "../tui/ui/composer.ts";
|
|
17
|
+
import type { ListItem } from "../tui/ui/list-model.ts";
|
|
8
18
|
|
|
9
|
-
export
|
|
10
|
-
|
|
19
|
+
export interface CompletionSources {
|
|
20
|
+
readonly commands: readonly { name: string; description: string }[];
|
|
21
|
+
readonly argumentValues: (command: string) => readonly string[];
|
|
22
|
+
readonly paths: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Twelve rows: enough to see siblings of a path without hiding the composer. Assumed. */
|
|
26
|
+
const MAX_ITEMS = 12;
|
|
27
|
+
|
|
28
|
+
export function completionAt(text: string, cursor: number, sources: CompletionSources): Completion | undefined {
|
|
29
|
+
const before = text.slice(0, cursor);
|
|
30
|
+
const line = before.slice(before.lastIndexOf("\n") + 1);
|
|
31
|
+
const lineStart = cursor - line.length;
|
|
32
|
+
if (line.startsWith("/") && lineStart === 0) {
|
|
33
|
+
const space = line.indexOf(" ");
|
|
34
|
+
if (space < 0) {
|
|
35
|
+
const query = line.slice(1).toLowerCase();
|
|
36
|
+
const items = sources.commands
|
|
37
|
+
.map(command => ({ command, rank: commandCompletionRank(command.name, query) }))
|
|
38
|
+
.filter(item => Number.isFinite(item.rank)).sort((a, b) => a.rank - b.rank)
|
|
39
|
+
.map(({ command }) => ({ value: `/${command.name} `, label: `/${command.name}`, description: command.description }));
|
|
40
|
+
return items.length ? { kind: "command", start: 0, end: cursor, items: items.slice(0, MAX_ITEMS) } : undefined;
|
|
41
|
+
}
|
|
42
|
+
const command = line.slice(1, space).toLowerCase();
|
|
43
|
+
const arg = line.slice(space + 1);
|
|
44
|
+
if (arg.includes(" ")) return undefined;
|
|
45
|
+
const values = sources.argumentValues(command);
|
|
46
|
+
if (!values.length) return undefined;
|
|
47
|
+
const items = fuzzyFilter(values, arg).slice(0, MAX_ITEMS).map(value => ({ value, label: value }));
|
|
48
|
+
return items.length ? { kind: "argument", start: space + 1, end: cursor, items } : undefined;
|
|
49
|
+
}
|
|
50
|
+
const at = line.lastIndexOf("@");
|
|
51
|
+
if (at < 0) return undefined;
|
|
52
|
+
const prev = line[at - 1];
|
|
53
|
+
if (prev !== undefined && !/\s/.test(prev)) return undefined;
|
|
54
|
+
const query = line.slice(at + 1);
|
|
55
|
+
if (/\s/.test(query) && !query.startsWith("\"")) return undefined;
|
|
56
|
+
const bare = query.replace(/^"/, "");
|
|
57
|
+
const items: ListItem[] = fuzzyFilter(sources.paths, bare).slice(0, MAX_ITEMS)
|
|
58
|
+
.map(path => ({ value: `@${/\s/.test(path) ? JSON.stringify(path) : path} `, label: path }));
|
|
59
|
+
return items.length ? { kind: "path", start: lineStart + at, end: cursor, items } : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function completionSources(app: App, paths: readonly string[]): CompletionSources {
|
|
63
|
+
const providers = [...new Set([...app.catalog.map(m => m.provider).filter(provider => !subscriptionById(provider)), ...SUBSCRIPTION_OPTIONS.map(p => p.id)])];
|
|
11
64
|
const values: Record<string, () => string[]> = {
|
|
12
|
-
|
|
65
|
+
// Model identities belong to the searchable picker. The generic completer
|
|
66
|
+
// still handles arbitrary argument sources; this app supplies only one
|
|
67
|
+
// model selection surface, rather than a second list with different keys.
|
|
13
68
|
thinking: () => [...supportedThinkingLevels(app.providerConfig.spec)],
|
|
14
|
-
|
|
15
|
-
settings:
|
|
69
|
+
provider: () => providers, logout: () => Object.keys(app.auth).filter(provider => app.auth[provider] !== undefined),
|
|
70
|
+
settings: interactiveSettingKeys,
|
|
16
71
|
models: () => ["refresh"], trust: () => ["once", "here", "parent", "never"],
|
|
17
72
|
};
|
|
18
|
-
|
|
19
|
-
...
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
...
|
|
73
|
+
return {
|
|
74
|
+
commands: [...COMMANDS.filter(command => command.name !== "exit"), ...commandEntries(app)],
|
|
75
|
+
argumentValues: command => values[commandDefinition(`/${command}`)?.name ?? command.toLowerCase()]?.() ?? [],
|
|
76
|
+
paths,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Built-ins, then extension commands, prompt templates and skills: the same names the palette lists. */
|
|
81
|
+
export function commandEntries(app: App): { name: string; description: string; section: string }[] {
|
|
82
|
+
return [
|
|
83
|
+
...[...app.extensions?.commands ?? []].map(([name, command]) => ({ name, description: command.description ?? "extension command", section: "Extensions" })),
|
|
84
|
+
...[...(app.slash?.prompts.values() ?? [])].map(p => ({ name: p.name, description: p.description ?? "prompt template", section: "Prompts" })),
|
|
85
|
+
...(app.slash?.skillCommands === false ? [] : [...(app.slash?.skills.values() ?? [])].map(s => ({ name: `skill:${s.name}`, description: s.description ?? "skill", section: "Skills" }))),
|
|
29
86
|
];
|
|
30
|
-
return new WorkspaceAutocomplete([...commands, ...registered], app.workspace, paths);
|
|
31
87
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Q24: bounded local JSONL transport. No TCP listener or additional runtime dependency. */
|
|
2
|
+
import { createServer, type Socket } from "node:net";
|
|
3
|
+
import { chmodSync } from "node:fs";
|
|
4
|
+
import { lstat, mkdir, rm } from "node:fs/promises";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { CONTROL_SCHEMA } from "../core/control-protocol.ts";
|
|
7
|
+
import { ControlConnection, type RpcOutput } from "../runtime/control-connection.ts";
|
|
8
|
+
import type { SessionControl } from "../runtime/session-control.ts";
|
|
9
|
+
|
|
10
|
+
export class ControlJsonl {
|
|
11
|
+
private buffer = Buffer.alloc(0);
|
|
12
|
+
private pending = 0;
|
|
13
|
+
private tail = Promise.resolve();
|
|
14
|
+
private closed = false;
|
|
15
|
+
readonly connection: ControlConnection;
|
|
16
|
+
private readonly stop: () => void;
|
|
17
|
+
private readonly output: (text: string) => void;
|
|
18
|
+
constructor(control: () => SessionControl, output: (text: string) => void, stop: () => void, denyOnClose = false) {
|
|
19
|
+
this.stop = stop;
|
|
20
|
+
this.output = text => { try { output(text); } catch { this.close(); this.stop(); } };
|
|
21
|
+
this.connection = new ControlConnection(control, message => this.write(message), { denyOnClose });
|
|
22
|
+
}
|
|
23
|
+
private write(message: RpcOutput): void {
|
|
24
|
+
if (this.closed) return;
|
|
25
|
+
const text = JSON.stringify(message) + "\n";
|
|
26
|
+
if (Buffer.byteLength(text) > CONTROL_SCHEMA.limits.frameBytes) {
|
|
27
|
+
if ("id" in message) this.output(JSON.stringify({ id: message.id, error: { code: -32002, message: "response exceeds frame limit; use paginated inspectors" } }) + "\n");
|
|
28
|
+
else this.fail("event exceeds frame limit; reconnect and resynchronize");
|
|
29
|
+
} else this.output(text);
|
|
30
|
+
}
|
|
31
|
+
private fail(message: string): void {
|
|
32
|
+
if (this.closed) return;
|
|
33
|
+
this.output(JSON.stringify({ id: null, error: { code: -32001, message } }) + "\n");
|
|
34
|
+
this.close(); this.stop();
|
|
35
|
+
}
|
|
36
|
+
feed(chunk: Uint8Array): void {
|
|
37
|
+
if (this.closed) return;
|
|
38
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
39
|
+
for (;;) {
|
|
40
|
+
const newline = this.buffer.indexOf(10);
|
|
41
|
+
if (newline < 0) { if (this.buffer.length > CONTROL_SCHEMA.limits.frameBytes) this.fail("input frame exceeds limit"); return; }
|
|
42
|
+
if (newline > CONTROL_SCHEMA.limits.frameBytes) { this.fail("input frame exceeds limit"); return; }
|
|
43
|
+
const line = this.buffer.subarray(0, newline).toString("utf8"); this.buffer = this.buffer.subarray(newline + 1);
|
|
44
|
+
if (!line.trim()) continue;
|
|
45
|
+
if (++this.pending > CONTROL_SCHEMA.limits.queuedRequests) { this.fail("request queue is full"); return; }
|
|
46
|
+
this.tail = this.tail.then(async () => {
|
|
47
|
+
if (this.closed) return;
|
|
48
|
+
let value: unknown;
|
|
49
|
+
try { value = JSON.parse(line); }
|
|
50
|
+
catch { this.write({ id: null, error: { code: -32700, message: "invalid JSON" } }); return; }
|
|
51
|
+
await this.connection.receive(value);
|
|
52
|
+
}).finally(() => { this.pending--; });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async drained(): Promise<void> { await this.tail; }
|
|
56
|
+
close(): void { this.closed = true; this.buffer = Buffer.alloc(0); this.connection.close(); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function listenControl(path: string, control: () => SessionControl): Promise<() => Promise<void>> {
|
|
60
|
+
const absolute = resolve(path), parent = dirname(absolute);
|
|
61
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
62
|
+
const directory = await lstat(parent);
|
|
63
|
+
if (!directory.isDirectory() || directory.mode & 0o077 || process.getuid && directory.uid !== process.getuid()) throw Error("control socket requires an owned private directory (mode 0700)");
|
|
64
|
+
try { await lstat(absolute); throw Error("control socket path already exists; refusing to replace it"); }
|
|
65
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
|
|
66
|
+
const clients = new Map<Socket, ControlJsonl>();
|
|
67
|
+
const server = createServer(socket => {
|
|
68
|
+
// Assumed local adapter capacity; together with per-peer bounds this caps retained transport memory.
|
|
69
|
+
if (clients.size >= 32) { socket.destroy(); return; }
|
|
70
|
+
const peer = new ControlJsonl(control, text => {
|
|
71
|
+
if (socket.writableLength + Buffer.byteLength(text) > CONTROL_SCHEMA.limits.outputBytes) socket.destroy(); else socket.write(text);
|
|
72
|
+
}, () => socket.destroy());
|
|
73
|
+
clients.set(socket, peer);
|
|
74
|
+
socket.on("data", chunk => peer.feed(typeof chunk === "string" ? Buffer.from(chunk) : chunk));
|
|
75
|
+
socket.on("error", () => socket.destroy());
|
|
76
|
+
socket.on("close", () => { peer.close(); clients.delete(socket); });
|
|
77
|
+
});
|
|
78
|
+
await new Promise<void>((resolveReady, reject) => {
|
|
79
|
+
server.once("error", reject);
|
|
80
|
+
server.listen(absolute, () => { chmodSync(absolute, 0o600); resolveReady(); });
|
|
81
|
+
});
|
|
82
|
+
const owned = await lstat(absolute);
|
|
83
|
+
return async () => {
|
|
84
|
+
for (const [socket, peer] of clients) { peer.close(); socket.destroy(); }
|
|
85
|
+
await new Promise<void>((resolveClosed, reject) => server.close(error => error ? reject(error) : resolveClosed()));
|
|
86
|
+
try { const current = await lstat(absolute); if (current.ino === owned.ino && current.dev === owned.dev) await rm(absolute); }
|
|
87
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { draftImageInput, draftImageSpans, focusedDraftImage, imageMarker } from "../core/draft-images.ts";
|
|
2
|
+
import type { ImageRef } from "../core/images.ts";
|
|
3
|
+
|
|
4
|
+
/** D19: the renderer owns text and undo; the app only binds tokens to D8 blob identities. */
|
|
5
|
+
export interface ImageDraftEditor {
|
|
6
|
+
readonly text: string;
|
|
7
|
+
readonly cursor: number;
|
|
8
|
+
setAtoms(spans: (text: string) => readonly { start: number; end: number }[]): void;
|
|
9
|
+
insert(text: string): void;
|
|
10
|
+
removeAtom(label: string): void;
|
|
11
|
+
resetDraft(project: (text: string) => string): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ImageDraft {
|
|
15
|
+
private readonly editor: ImageDraftEditor;
|
|
16
|
+
private readonly registry = new Map<number, ImageRef>();
|
|
17
|
+
// Never reuse an ID: undo, kill/yank and local history can restore an earlier tag.
|
|
18
|
+
private nextId = 1;
|
|
19
|
+
|
|
20
|
+
constructor(editor: ImageDraftEditor) {
|
|
21
|
+
this.editor = editor;
|
|
22
|
+
editor.setAtoms(text => draftImageSpans(text, this.registry));
|
|
23
|
+
}
|
|
24
|
+
get images(): ImageRef[] { return this.input(this.editor.text).images; }
|
|
25
|
+
get focusedImage() { return focusedDraftImage(this.editor.text, this.editor.cursor, this.registry); }
|
|
26
|
+
input(text: string) { return draftImageInput(text, this.registry); }
|
|
27
|
+
|
|
28
|
+
insertImages(images: readonly ImageRef[]): void {
|
|
29
|
+
let labels = "";
|
|
30
|
+
for (const image of images) {
|
|
31
|
+
while (this.editor.text.includes(imageMarker(this.nextId))) this.nextId++;
|
|
32
|
+
const id = this.nextId++;
|
|
33
|
+
this.registry.set(id, image); labels += imageMarker(id);
|
|
34
|
+
}
|
|
35
|
+
if (labels) this.editor.insert(labels);
|
|
36
|
+
}
|
|
37
|
+
removeImage(index: number): void {
|
|
38
|
+
const ids = [...new Set(draftImageSpans(this.editor.text, this.registry).map(span => span.id))];
|
|
39
|
+
const id = ids[index];
|
|
40
|
+
if (id !== undefined) this.editor.removeAtom(imageMarker(id));
|
|
41
|
+
}
|
|
42
|
+
reset(): void {
|
|
43
|
+
this.editor.resetDraft(text => this.input(text).text);
|
|
44
|
+
this.registry.clear();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { modelKey } from "../core/session-position.ts";
|
|
2
2
|
import { clampThinkingLevel } from "../core/thinking.ts";
|
|
3
|
-
import { createProvider } from "../runtime/catalog.ts";
|
|
3
|
+
import { createProvider, providerSessionId } from "../runtime/catalog.ts";
|
|
4
4
|
import { storedIsOAuth } from "../runtime/auth.ts";
|
|
5
5
|
import { outputTokenBudget, type ModelSpec } from "../runtime/model.ts";
|
|
6
6
|
import { providerAccess, setProviderAccess } from "../runtime/provider-access.ts";
|
|
@@ -29,6 +29,7 @@ export async function publishModelCatalog(app: App, models: readonly ModelSpec[]
|
|
|
29
29
|
const config = { ...app.providerConfig, spec, maxTokens: outputTokenBudget(spec), effort: clampThinkingLevel(spec, app.providerConfig.effort) };
|
|
30
30
|
const credential = app.credentialOf(spec);
|
|
31
31
|
const next = createProvider(config, credential.ok ? credential.key : "", retryHandler(app.io), {
|
|
32
|
+
sessionId: providerSessionId(app.session.config.logDir),
|
|
32
33
|
effects: app.effects, oauth: !app.args.apiKey && storedIsOAuth(app.auth, spec.provider),
|
|
33
34
|
});
|
|
34
35
|
const needsCut = providerReplayKey(previous) !== providerReplayKey(spec);
|
|
@@ -1,49 +1,9 @@
|
|
|
1
|
+
/** Project trust decisions; the picker itself is apps/ui/standalone.ts (startup) and apps/ui/app.ts (/trust). */
|
|
1
2
|
import { dirname } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import { createInteractiveTui } from "../tui/host.ts";
|
|
4
|
-
import { IDENTITY } from "../tui/style.ts";
|
|
5
|
-
import { installBindings } from "../tui/input.ts";
|
|
6
|
-
import { loadUiResources } from "../store/resources.ts";
|
|
7
|
-
import type { Style } from "../tui/style.ts";
|
|
8
|
-
import { projectTrust, saveTrust } from "../store/trust.ts";
|
|
9
|
-
import { truncateToWidth } from "../vendor/pi-tui/utils.ts";
|
|
3
|
+
import { saveTrust } from "../store/trust.ts";
|
|
10
4
|
|
|
11
5
|
export type TrustChoice = "once" | "here" | "parent" | "never";
|
|
12
6
|
export async function applyTrustChoice(workspace: string, choice: TrustChoice): Promise<boolean> {
|
|
13
7
|
if (choice !== "once") await saveTrust(choice === "parent" ? dirname(workspace) : workspace, choice !== "never");
|
|
14
8
|
return choice !== "never";
|
|
15
9
|
}
|
|
16
|
-
export class TrustSelector extends SelectList {
|
|
17
|
-
private readonly workspace: string;
|
|
18
|
-
private readonly decision: Awaited<ReturnType<typeof projectTrust>>;
|
|
19
|
-
constructor(workspace: string, decision: Awaited<ReturnType<typeof projectTrust>>, style: Style) {
|
|
20
|
-
super([
|
|
21
|
-
{ value: "once", label: "Trust for this session", description: "Load project resources without saving a decision" },
|
|
22
|
-
{ value: "here", label: "Trust this directory", description: workspace },
|
|
23
|
-
{ value: "parent", label: "Trust parent directory", description: dirname(workspace) },
|
|
24
|
-
{ value: "never", label: "Do not trust this directory", description: "Skip project configuration and executable extensions" },
|
|
25
|
-
], { title: "Project trust", search: false, style,
|
|
26
|
-
...(decision.saved ? { selectedValue: decision.saved.trusted ? decision.saved.path === workspace ? "here" : "parent" : "never" } : {}) });
|
|
27
|
-
this.workspace = workspace; this.decision = decision;
|
|
28
|
-
}
|
|
29
|
-
override render(width: number): string[] {
|
|
30
|
-
const saved = this.decision.saved;
|
|
31
|
-
return ["Project trust", this.workspace, `Saved: ${saved ? `${saved.trusted ? "trusted" : "untrusted"} (${saved.path})` : "none"}`,
|
|
32
|
-
`Current session: ${this.decision.trusted ? "trusted" : "untrusted"} · Tool permissions are separate`, ...super.render(width).slice(1)]
|
|
33
|
-
.map(line => truncateToWidth(line, width, ""));
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
export async function pickProjectTrust(workspace: string, handoff?: () => void): Promise<boolean> {
|
|
37
|
-
const decision = await projectTrust(workspace);
|
|
38
|
-
installBindings((await loadUiResources({ workspace, trusted: false })).keymap.byAction);
|
|
39
|
-
const screen = createInteractiveTui();
|
|
40
|
-
const select = new TrustSelector(workspace, decision, IDENTITY);
|
|
41
|
-
screen.addChild(select); screen.setFocus(select);
|
|
42
|
-
try {
|
|
43
|
-
const choice = await new Promise<string | null>(resolve => {
|
|
44
|
-
select.onInput = data => { const state = select.acceptInput(data); if (state.cancelled) resolve(null); else if (state.confirmed) resolve(state.confirmed.value); screen.requestRender(); };
|
|
45
|
-
handoff?.(); screen.start();
|
|
46
|
-
});
|
|
47
|
-
return choice === null ? false : applyTrustChoice(workspace, choice as TrustChoice);
|
|
48
|
-
} finally { await screen.terminal.drainInput(); screen.stop(); }
|
|
49
|
-
}
|