@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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** Q23 / D8 / D11: disposable request evidence, derived only from the selected log ancestry. */
|
|
2
|
+
import type { LogEvent } from "./events.ts";
|
|
3
|
+
import { deriveAcceptance, type AcceptanceState } from "./acceptance.ts";
|
|
4
|
+
import { changedPaths } from "./checkpoints.ts";
|
|
5
|
+
import { evidenceStale, isCommandResult, resolveEvidence } from "./evidence.ts";
|
|
6
|
+
import { executionOutcome, modelOutcome, type ExecutionOutcome } from "./outcome.ts";
|
|
7
|
+
import { deriveWorkItems, type WorkItem } from "./work-state.ts";
|
|
8
|
+
import { deriveRequestProgress, type RequestProgress, type RequestRecord, type RequestSource } from "./request-progress.ts";
|
|
9
|
+
|
|
10
|
+
export type RequestPart = { node: number; events: readonly LogEvent[] };
|
|
11
|
+
export type RequestCommand = {
|
|
12
|
+
source: RequestSource; command: string; exit: number; status: "pass" | "fail" | "stale" | "superseded";
|
|
13
|
+
gate?: string; workspace_sha256?: string; workspace_scope?: "source-v1"; criteria: string[];
|
|
14
|
+
};
|
|
15
|
+
export type RequestArtifact = { source: RequestSource; path: string; sha256: string | null;
|
|
16
|
+
kind: "read" | "edit" | "checkpoint" | "command_output" };
|
|
17
|
+
export type RequestState = {
|
|
18
|
+
request: RequestSource; inputs: RequestSource[];
|
|
19
|
+
status: "verified" | "awaiting_human" | "needs_work" | "blocked" | "unverified" | "not_applicable";
|
|
20
|
+
execution?: ExecutionOutcome; acceptance?: AcceptanceState;
|
|
21
|
+
commands: RequestCommand[]; artifacts: RequestArtifact[]; work: WorkItem[];
|
|
22
|
+
reported_blockers: { source: RequestSource; reason: string }[];
|
|
23
|
+
progress: RequestProgress;
|
|
24
|
+
changed: boolean; used_tools: boolean; reviewed: boolean;
|
|
25
|
+
final?: { source: RequestSource; text: string; has_tools: boolean };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** New steering events refer to their original user event. Legacy users remain separate requests. */
|
|
29
|
+
export function currentRequestRecords(parts: readonly RequestPart[]): RequestRecord[] {
|
|
30
|
+
const records = parts.flatMap(part => part.events.map(event => ({ node: part.node, event })));
|
|
31
|
+
const last = records.findLastIndex(({ event }) => event.t === "user");
|
|
32
|
+
if (last < 0) return [];
|
|
33
|
+
const user = records[last]!.event;
|
|
34
|
+
if (user.t !== "user") return [];
|
|
35
|
+
const start = user.request === undefined ? last : records.findIndex(({ event }) => event.t === "user" && event.id === user.request);
|
|
36
|
+
// Never reach through an unrelated user request because of an invalid reference.
|
|
37
|
+
if (start < 0 || records.slice(start + 1, last + 1).some(({ event }) => event.t === "user" && event.request !== user.request)) return records.slice(last);
|
|
38
|
+
return records.slice(start);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function deriveRequestState(parts: readonly RequestPart[]): RequestState | undefined {
|
|
42
|
+
const records = currentRequestRecords(parts);
|
|
43
|
+
const first = records[0];
|
|
44
|
+
if (!first) return undefined;
|
|
45
|
+
const events = records.map(record => record.event);
|
|
46
|
+
const source = ({ node, event }: RequestRecord): RequestSource => ({ node, event: event.id });
|
|
47
|
+
const request = source(first);
|
|
48
|
+
const acceptance = deriveAcceptance(undefined, events.filter(event => event.t !== "acceptance_contract" || event.request === request.event));
|
|
49
|
+
const commands: RequestCommand[] = [], artifacts: RequestArtifact[] = [];
|
|
50
|
+
const lastCommands = new Map<string, string>();
|
|
51
|
+
for (const event of events) if (isCommandResult(event)) lastCommands.set(event.effect.cmd, event.id);
|
|
52
|
+
let work: WorkItem[] = [];
|
|
53
|
+
const ids = new Set(events.map(event => event.id)), completed = new Set<string>();
|
|
54
|
+
// Keep the existing work-item same-segment evidence contract (D11/O.2).
|
|
55
|
+
for (const part of parts) {
|
|
56
|
+
work = deriveWorkItems(work, part.events);
|
|
57
|
+
for (let index = 0; index < part.events.length; index++) {
|
|
58
|
+
const event = part.events[index]!;
|
|
59
|
+
// Legacy completed claims supersede a same-named blocker only when the
|
|
60
|
+
// original same-segment evidence resolves, just as in deriveHandoff.
|
|
61
|
+
if (event.t === "claim" && event.work_id === undefined && ids.has(event.id) &&
|
|
62
|
+
resolveEvidence(part.events.slice(0, index), event.evidence).ok) completed.add(event.what);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
work = work.filter(item => ids.has(item.opened_at) || ids.has(item.updated_at));
|
|
66
|
+
const reported_blockers: RequestState["reported_blockers"] = [];
|
|
67
|
+
let execution: ExecutionOutcome | undefined, final: RequestState["final"], changed = false;
|
|
68
|
+
for (const record of records) {
|
|
69
|
+
const event = record.event, ref = source(record);
|
|
70
|
+
if (event.t === "user") { execution = undefined; final = undefined; }
|
|
71
|
+
else if (event.t === "assistant") {
|
|
72
|
+
final = { source: ref, text: event.blocks.flatMap(block => block.type === "text" ? [block.text] : []).join("\n").trim(),
|
|
73
|
+
has_tools: event.blocks.some(block => block.type === "tool_use") };
|
|
74
|
+
execution = final.has_tools ? undefined : modelOutcome(event.stop_reason ?? null);
|
|
75
|
+
} else if (event.t === "outcome") execution = { ...event.outcome };
|
|
76
|
+
else if (event.t === "interrupt") execution = executionOutcome("cancelled");
|
|
77
|
+
else if (event.t === "guidance" && (event.reason === "completion_review" || event.reason === "output_recovery")) execution = undefined;
|
|
78
|
+
if (event.t === "blocked" && (event.work_id ? work.some(item => item.id === event.work_id && item.status === "blocked") : !completed.has(event.what))) {
|
|
79
|
+
reported_blockers.push({ source: ref, reason: event.reason });
|
|
80
|
+
}
|
|
81
|
+
if (isCommandResult(event)) {
|
|
82
|
+
const effect = event.effect;
|
|
83
|
+
const evidence = { command: effect.cmd, exit: effect.exit, event_id: event.id, at: event.at,
|
|
84
|
+
...(effect.workspace_sha256 === undefined ? {} : { workspace_sha256: effect.workspace_sha256 }),
|
|
85
|
+
...(effect.workspace_scope === undefined ? {} : { workspace_scope: effect.workspace_scope }) };
|
|
86
|
+
commands.push({ source: ref, command: effect.cmd, exit: effect.exit,
|
|
87
|
+
status: !event.ok || effect.exit !== 0 ? "fail" : effect.workspace_observed === false || evidenceStale(events, evidence) ? "stale"
|
|
88
|
+
: lastCommands.get(effect.cmd) !== event.id ? "superseded" : "pass",
|
|
89
|
+
...(effect.gate === undefined ? {} : { gate: effect.gate }),
|
|
90
|
+
...(effect.workspace_sha256 === undefined ? {} : { workspace_sha256: effect.workspace_sha256 }),
|
|
91
|
+
...(effect.workspace_scope === undefined ? {} : { workspace_scope: effect.workspace_scope }),
|
|
92
|
+
criteria: acceptance?.criteria.filter(item => item.kind === "command" && item.command === effect.cmd).map(item => item.id) ?? [] });
|
|
93
|
+
for (const output of effect.artifacts?.outputs ?? []) if (output.after.state === "observed") artifacts.push({
|
|
94
|
+
source: ref, path: output.path, sha256: output.after.sha256, kind: "command_output" });
|
|
95
|
+
} else if (event.t === "tool_result" && (event.effect?.kind === "read" || event.effect?.kind === "edit")) {
|
|
96
|
+
const effect = event.effect;
|
|
97
|
+
artifacts.push({ source: ref, path: effect.path, sha256: effect.kind === "read" ? effect.sha256 : effect.sha256_after, kind: effect.kind });
|
|
98
|
+
if (effect.kind === "edit" && effect.sha256_before !== effect.sha256_after) changed = true;
|
|
99
|
+
} else if (event.t === "checkpoint" && event.phase === "applied") {
|
|
100
|
+
for (const path of changedPaths(event.changes)) artifacts.push({ source: ref, path, sha256: event.changes[path]!.after?.sha256 ?? null, kind: "checkpoint" });
|
|
101
|
+
changed ||= changedPaths(event.changes).length > 0;
|
|
102
|
+
} else if (event.t === "workspace_observation") changed ||= event.changed;
|
|
103
|
+
else if (event.t === "snapshot") changed ||= Object.keys(event.added).length + Object.keys(event.changed).length + event.removed.length > 0;
|
|
104
|
+
}
|
|
105
|
+
return { request, inputs: records.filter(record => record.event.t === "user").map(source),
|
|
106
|
+
// Work notes can describe a later phase the user explicitly asked to defer.
|
|
107
|
+
// Only the declared request contract defines what must finish in this request.
|
|
108
|
+
status: reported_blockers.length ? "blocked" : acceptance?.status ?? "unverified",
|
|
109
|
+
...(execution ? { execution } : {}), ...(acceptance ? { acceptance } : {}),
|
|
110
|
+
commands, artifacts, work, reported_blockers, progress: deriveRequestProgress(records), changed,
|
|
111
|
+
used_tools: events.some(event => event.t === "tool_result"),
|
|
112
|
+
reviewed: events.some(event => event.t === "guidance" && event.reason === "completion_review"),
|
|
113
|
+
...(final ? { final } : {}) };
|
|
114
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** R8: declarations select a target; only an owned runtime observation supplies its identity. */
|
|
2
|
+
import { parseCommandArtifactPlan, validArtifactPath, type CommandArtifactPlan } from "./command-artifacts.ts";
|
|
3
|
+
import type { ArtifactRef } from "./events.ts";
|
|
4
|
+
import { isObservationArtifact } from "./observation-view.ts";
|
|
5
|
+
import type { ProcessSource } from "./process-receipt.ts";
|
|
6
|
+
|
|
7
|
+
export type RuntimeTarget = { id: string; build: string; artifacts: CommandArtifactPlan; entry: string;
|
|
8
|
+
interpreter?: string; args?: string[]; workdir?: string; port?: number };
|
|
9
|
+
export type TargetBuild = { node: number; event: string; manifest: ArtifactRef };
|
|
10
|
+
export type RuntimeInstance = { id: string; session: number; pid: number; source: ProcessSource;
|
|
11
|
+
entry: { path: string; sha256: string }; program: { path: string; sha256: string; device: string; inode: string } };
|
|
12
|
+
export type RuntimeTargetReceipt = {
|
|
13
|
+
kind: "runtime_target"; version: 1; target: string; request: string; contract_event: string;
|
|
14
|
+
state: "ready" | "not_ready" | "stale" | "unknown"; reason: string; observed_at: string;
|
|
15
|
+
source: ProcessSource; build?: TargetBuild; instance?: RuntimeInstance; port?: number;
|
|
16
|
+
checks: { build: boolean; inputs: boolean; artifacts: boolean; process: boolean; image: boolean; port: boolean | null };
|
|
17
|
+
manifest?: ArtifactRef;
|
|
18
|
+
};
|
|
19
|
+
export type TargetAcceptance = { id: string; build: string; status: "not_run" | RuntimeTargetReceipt["state"];
|
|
20
|
+
observation?: string; binding?: string; session?: number; pid?: number; entry?: string; port?: number; build_event?: string; reason?: string };
|
|
21
|
+
|
|
22
|
+
const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
const id = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/.test(value);
|
|
24
|
+
const text = (value: unknown): value is string => typeof value === "string" && value.length > 0;
|
|
25
|
+
const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
26
|
+
const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
27
|
+
const source = (value: unknown): value is ProcessSource => record(value) && ["host", "workspace", "cwd", "runtime"].every(key => text(value[key]));
|
|
28
|
+
|
|
29
|
+
export function parseRuntimeTarget(value: unknown): RuntimeTarget {
|
|
30
|
+
if (!record(value) || Object.keys(value).some(key => !["id", "build", "artifacts", "entry", "interpreter", "args", "workdir", "port"].includes(key)) ||
|
|
31
|
+
!id(value["id"]) || !id(value["build"]) || !validArtifactPath(value["entry"])) throw Error("invalid runtime target declaration");
|
|
32
|
+
const artifacts = parseCommandArtifactPlan(value["artifacts"]);
|
|
33
|
+
if (!artifacts.paths.includes(value["entry"])) throw Error("target entry must be an observed build artifact");
|
|
34
|
+
const interpreter = value["interpreter"], args = value["args"], workdir = value["workdir"], port = value["port"];
|
|
35
|
+
if (interpreter !== undefined && (typeof interpreter !== "string" || !interpreter.startsWith("/") || /[\x00-\x1f\x7f]/.test(interpreter) || interpreter.length > 4096)) throw Error("target interpreter needs an explicit absolute path");
|
|
36
|
+
// Assumed argv budget prevents declarations from becoming an unbounded prompt or exec payload.
|
|
37
|
+
if (args !== undefined && (!Array.isArray(args) || args.length > 64 || !args.every(arg => typeof arg === "string" && !arg.includes("\0")) || JSON.stringify(args).length > 8192)) throw Error("target args must be a bounded literal string array");
|
|
38
|
+
if (workdir !== undefined && workdir !== "." && !validArtifactPath(workdir)) throw Error("target workdir must stay inside the workspace");
|
|
39
|
+
if (port !== undefined && (!natural(port) || port < 1 || port > 65535)) throw Error("target port must be 1..65535 on IPv4 loopback");
|
|
40
|
+
return { id: value["id"], build: value["build"], entry: value["entry"], artifacts,
|
|
41
|
+
...(interpreter !== undefined ? { interpreter: interpreter as string } : {}), ...(args !== undefined ? { args: [...args as string[]] } : {}),
|
|
42
|
+
...(workdir !== undefined ? { workdir: workdir as string } : {}), ...(port !== undefined ? { port: port as number } : {}) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function targetIsReady(receipt: Pick<RuntimeTargetReceipt, "build" | "instance" | "checks">): boolean {
|
|
46
|
+
return receipt.build !== undefined && receipt.instance !== undefined && receipt.checks.build && receipt.checks.inputs && receipt.checks.artifacts &&
|
|
47
|
+
receipt.checks.process && receipt.checks.image && receipt.checks.port !== false;
|
|
48
|
+
}
|
|
49
|
+
export function isRuntimeTargetReceipt(value: unknown): value is RuntimeTargetReceipt {
|
|
50
|
+
if (!record(value) || value["kind"] !== "runtime_target" || value["version"] !== 1 || !id(value["target"]) || !text(value["request"]) ||
|
|
51
|
+
!text(value["contract_event"]) || !text(value["observed_at"]) || !text(value["reason"]) || !source(value["source"]) ||
|
|
52
|
+
!["ready", "not_ready", "stale", "unknown"].includes(String(value["state"]))) return false;
|
|
53
|
+
const build = value["build"], instance = value["instance"], checks = value["checks"], port = value["port"];
|
|
54
|
+
if (build !== undefined && (!record(build) || !natural(build["node"]) || !text(build["event"]) || !isObservationArtifact(build["manifest"]))) return false;
|
|
55
|
+
if (instance !== undefined && (!record(instance) || !text(instance["id"]) || !natural(instance["session"]) || instance["session"] < 1 ||
|
|
56
|
+
!natural(instance["pid"]) || instance["pid"] < 1 || !source(instance["source"]) ||
|
|
57
|
+
!["host", "workspace", "cwd", "runtime"].every(key => (instance["source"] as Record<string, string>)[key] === (value["source"] as Record<string, string>)[key]) ||
|
|
58
|
+
!record(instance["entry"]) || !text(instance["entry"]["path"]) || !digest(instance["entry"]["sha256"]) ||
|
|
59
|
+
!record(instance["program"]) || !text(instance["program"]["path"]) || !digest(instance["program"]["sha256"]) ||
|
|
60
|
+
!text(instance["program"]["device"]) || !text(instance["program"]["inode"]))) return false;
|
|
61
|
+
if (!record(checks) || !["build", "inputs", "artifacts", "process", "image"].every(key => typeof checks[key] === "boolean") ||
|
|
62
|
+
!(checks["port"] === null || typeof checks["port"] === "boolean") ||
|
|
63
|
+
(port === undefined ? checks["port"] !== null : !natural(port) || port < 1 || port > 65535 || checks["port"] === null)) return false;
|
|
64
|
+
if (value["manifest"] !== undefined && !isObservationArtifact(value["manifest"])) return false;
|
|
65
|
+
return value["state"] !== "ready" || targetIsReady(value as RuntimeTargetReceipt) && isObservationArtifact(value["manifest"]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** lsof -F0 is a field protocol; matching a process name or display substring proves nothing. */
|
|
69
|
+
export function lsofFiles(output: string, pid: number): Record<string, string>[] {
|
|
70
|
+
let process = -1, file: Record<string, string> | undefined;
|
|
71
|
+
const files: Record<string, string>[] = [];
|
|
72
|
+
for (const chunk of output.split("\0")) {
|
|
73
|
+
const field = chunk.replace(/^\n/, "");
|
|
74
|
+
if (!field) continue;
|
|
75
|
+
const key = field[0]!, value = field.slice(1);
|
|
76
|
+
if (key === "p") { process = Number(value); file = undefined; }
|
|
77
|
+
else if (key === "f") { file = { f: value }; if (process === pid) files.push(file); }
|
|
78
|
+
else if (file && process === pid) {
|
|
79
|
+
if (key === "T") { const split = value.indexOf("="); if (split > 0) file[`T${value.slice(0, split)}`] = value.slice(split + 1); }
|
|
80
|
+
else file[key] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return files;
|
|
84
|
+
}
|
|
85
|
+
export function lsofOwnsImage(output: string, pid: number, device: string, inode: string): boolean {
|
|
86
|
+
return lsofFiles(output, pid).some(file => {
|
|
87
|
+
if (file["f"] !== "txt" || file["i"] !== inode || !/^0x[0-9a-f]+$/i.test(file["D"] ?? "")) return false;
|
|
88
|
+
try { return BigInt(file["D"]!) === BigInt(device); } catch { return false; }
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function lsofOwnsPort(output: string, pid: number, port: number): boolean {
|
|
92
|
+
return lsofFiles(output, pid).some(file => file["t"] === "IPv4" && file["P"] === "TCP" && file["TST"] === "LISTEN" &&
|
|
93
|
+
["*", "127.0.0.1"].some(host => file["n"] === `${host}:${port}`));
|
|
94
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** R2: scan completeness and preview completeness answer different questions. */
|
|
2
|
+
import { headTailSelection, isObservationArtifact, isBytePreview, type BytePreview } from "./observation-view.ts";
|
|
3
|
+
import type { ArtifactRef } from "./events.ts";
|
|
4
|
+
import { artifactRecovery } from "./prompt-policy.ts";
|
|
5
|
+
|
|
6
|
+
export interface SearchRow { prefix: string; text: string; }
|
|
7
|
+
export interface SearchScan {
|
|
8
|
+
status: "complete" | "partial" | "unknown";
|
|
9
|
+
reasons: { code: string; path?: string }[];
|
|
10
|
+
examined_files: number | null;
|
|
11
|
+
source_versions: "recorded" | "unknown";
|
|
12
|
+
}
|
|
13
|
+
export interface SearchObservation {
|
|
14
|
+
engine: "walk" | "ripgrep" | "fd" | "directory";
|
|
15
|
+
rows: SearchRow[];
|
|
16
|
+
scan: SearchScan;
|
|
17
|
+
sources: { path: string; sha256: string }[];
|
|
18
|
+
raw?: Uint8Array;
|
|
19
|
+
rawErrors?: Uint8Array;
|
|
20
|
+
limit: number;
|
|
21
|
+
empty: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SearchReceipt {
|
|
25
|
+
kind: "search";
|
|
26
|
+
version: 1;
|
|
27
|
+
source: { host: string; workspace: string; resource: string };
|
|
28
|
+
engine: SearchObservation["engine"];
|
|
29
|
+
scan: { status: SearchScan["status"]; reasons: string[]; examined_files: number | null; source_versions: SearchScan["source_versions"] };
|
|
30
|
+
matches: number;
|
|
31
|
+
artifact: ArtifactRef;
|
|
32
|
+
manifest: ArtifactRef;
|
|
33
|
+
raw?: ArtifactRef;
|
|
34
|
+
raw_errors?: ArtifactRef;
|
|
35
|
+
preview: BytePreview;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
39
|
+
const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
40
|
+
export function isSearchReceipt(value: unknown): value is SearchReceipt {
|
|
41
|
+
if (!object(value) || value["kind"] !== "search" || value["version"] !== 1 || !isObservationArtifact(value["artifact"]) || !isObservationArtifact(value["manifest"]) || !natural(value["matches"])) return false;
|
|
42
|
+
if (!["walk", "ripgrep", "fd", "directory"].includes(String(value["engine"])) || value["raw"] !== undefined && !isObservationArtifact(value["raw"])) return false;
|
|
43
|
+
if (value["raw_errors"] !== undefined && !isObservationArtifact(value["raw_errors"])) return false;
|
|
44
|
+
const source = value["source"], scan = value["scan"];
|
|
45
|
+
if (!object(source) || !["host", "workspace", "resource"].every(key => typeof source[key] === "string" && source[key] !== "")) return false;
|
|
46
|
+
if (!object(scan) || !["complete", "partial", "unknown"].includes(String(scan["status"])) || !["recorded", "unknown"].includes(String(scan["source_versions"])) ||
|
|
47
|
+
!Array.isArray(scan["reasons"]) || !scan["reasons"].every(reason => typeof reason === "string") || scan["examined_files"] !== null && !natural(scan["examined_files"])) return false;
|
|
48
|
+
if (scan["status"] === "complete" && scan["reasons"].length !== 0 || scan["status"] !== "complete" && scan["reasons"].length === 0) return false;
|
|
49
|
+
return isBytePreview(value["preview"], value["artifact"].bytes);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const encoder = new TextEncoder();
|
|
53
|
+
const bytes = (text: string) => encoder.encode(text).length;
|
|
54
|
+
export const searchObservationText = (rows: readonly SearchRow[]): string => rows.map(row => row.prefix + row.text).join("\n");
|
|
55
|
+
|
|
56
|
+
/** Map count, per-line, and UTF-8 byte cuts back to the same immutable full observation. */
|
|
57
|
+
export function previewSearchObservation(observation: SearchObservation, artifact: string, maxBytes: number, maxLineChars = 500, notice = "", tools?: readonly string[]) {
|
|
58
|
+
let text = "", source = 0, view = 0;
|
|
59
|
+
const spans: { view: number; source: number; bytes: number }[] = [];
|
|
60
|
+
const mapped = (value: string) => { const size = bytes(value); if (size) spans.push({ view, source, bytes: size }); text += value; view += size; };
|
|
61
|
+
const synthetic = (value: string) => { text += value; view += bytes(value); };
|
|
62
|
+
if (notice) synthetic(notice + "\n");
|
|
63
|
+
const shown = Math.max(0, Math.min(observation.rows.length, observation.limit));
|
|
64
|
+
for (let index = 0; index < observation.rows.length; index++) {
|
|
65
|
+
const row = observation.rows[index]!;
|
|
66
|
+
if (index < shown) {
|
|
67
|
+
let end = Math.min(maxLineChars, row.text.length);
|
|
68
|
+
if (end > 0 && end < row.text.length && /[\uD800-\uDBFF]/.test(row.text[end - 1]!)) end--;
|
|
69
|
+
const kept = row.prefix + row.text.slice(0, end);
|
|
70
|
+
mapped(kept);
|
|
71
|
+
if (end < row.text.length) synthetic(`… [line truncated; ${artifactRecovery(artifact, source + bytes(kept), tools)}]`);
|
|
72
|
+
}
|
|
73
|
+
source += bytes(row.prefix + row.text);
|
|
74
|
+
if (index < observation.rows.length - 1) { if (index < shown) mapped("\n"); source++; }
|
|
75
|
+
}
|
|
76
|
+
if (!observation.rows.length) synthetic(observation.scan.status === "complete" ? observation.empty : "[No matches observed in the scanned portion]");
|
|
77
|
+
if (shown < observation.rows.length) synthetic(`[${observation.rows.length - shown} more matches omitted; ${artifactRecovery(artifact, 0, tools)}]`);
|
|
78
|
+
if (observation.scan.status !== "complete") synthetic(`\n[scan ${observation.scan.status}: ${[...new Set(observation.scan.reasons.map(reason => reason.code === "match_timeout" ? "search cut off: match_timeout" : reason.code))].join(", ")}]`);
|
|
79
|
+
const previewBytes = encoder.encode(text);
|
|
80
|
+
const selected = headTailSelection(previewBytes, previewBytes, previewBytes.length, maxBytes,
|
|
81
|
+
omitted => `\n[truncated ${omitted} preview bytes; ${artifactRecovery(artifact, 0, tools)}]\n`);
|
|
82
|
+
const visible: [number, number][] = [];
|
|
83
|
+
for (const [lo, hi] of selected.visible) for (const span of spans) {
|
|
84
|
+
const begin = Math.max(lo, span.view), end = Math.min(hi, span.view + span.bytes);
|
|
85
|
+
if (end <= begin) continue;
|
|
86
|
+
const a = span.source + begin - span.view, b = span.source + end - span.view;
|
|
87
|
+
const last = visible.at(-1);
|
|
88
|
+
if (last?.[1] === a) last[1] = b;
|
|
89
|
+
else visible.push([a, b]);
|
|
90
|
+
}
|
|
91
|
+
let next = 0;
|
|
92
|
+
for (const [lo, hi] of visible) { if (lo !== next) break; next = hi; }
|
|
93
|
+
const full = encoder.encode(searchObservationText(observation.rows));
|
|
94
|
+
const semantic = visible.map(([lo, hi]) => new TextDecoder().decode(full.subarray(lo, hi))).join("");
|
|
95
|
+
return { text: selected.text, semantic, preview: { unit: "utf8_bytes" as const, complete: next === source,
|
|
96
|
+
visible, next_offset: next === source ? null : next } };
|
|
97
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session tree as data. Pure: restore points + parent pointers in,
|
|
3
|
+
* a forest out; plus a flattening that the UI and the line-mode printer
|
|
4
|
+
* share. Moved from src/tui/tree.ts when the view was rewritten (D20) so the
|
|
5
|
+
* shape of history does not depend on any renderer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Same mark /back uses for a branch the session rewound away from. */
|
|
9
|
+
export const LEFT_BRANCH_MARK = "↩";
|
|
10
|
+
export const EMPTY_TREE_MESSAGE = "nothing to go back to -- no exchange has completed yet";
|
|
11
|
+
|
|
12
|
+
export type TreeKind = "user" | "other";
|
|
13
|
+
export type TreeFilter = "user" | "default" | "no-tools" | "user-only" | "labeled-only" | "all";
|
|
14
|
+
|
|
15
|
+
export interface TreeNode {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly prompt: string;
|
|
18
|
+
readonly onPath: boolean;
|
|
19
|
+
readonly current: boolean;
|
|
20
|
+
readonly kind: TreeKind;
|
|
21
|
+
readonly children: readonly TreeNode[];
|
|
22
|
+
readonly category?: "tool" | "message" | "metadata";
|
|
23
|
+
readonly label?: string;
|
|
24
|
+
readonly labelAt?: string;
|
|
25
|
+
readonly at?: string;
|
|
26
|
+
readonly content?: string;
|
|
27
|
+
/** Exact event position; user messages navigate before themselves. */
|
|
28
|
+
readonly target?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A restore point as the log already knows it. parent pointers are a
|
|
33
|
+
* separate map so this file never imports store.
|
|
34
|
+
*/
|
|
35
|
+
export interface TreePoint {
|
|
36
|
+
readonly node: number;
|
|
37
|
+
readonly atEvent: string;
|
|
38
|
+
readonly prompt: string;
|
|
39
|
+
readonly onPath: boolean;
|
|
40
|
+
readonly kind?: TreeKind;
|
|
41
|
+
readonly category?: "tool" | "message" | "metadata";
|
|
42
|
+
readonly label?: string;
|
|
43
|
+
readonly labelAt?: string;
|
|
44
|
+
readonly at?: string;
|
|
45
|
+
readonly content?: string;
|
|
46
|
+
readonly target?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type NodeParent = { readonly node: number; readonly at_event?: string } | null;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Restore points plus parent pointers → the tree the view paints.
|
|
53
|
+
*
|
|
54
|
+
* Same-node exchanges stay a chain. A child node whose parent.at_event
|
|
55
|
+
* matches a point hangs off that point as a sibling of the continuation,
|
|
56
|
+
* which is how a rewind becomes a visible fork instead of a missing row.
|
|
57
|
+
*/
|
|
58
|
+
export function treeFromPoints(
|
|
59
|
+
points: readonly TreePoint[],
|
|
60
|
+
parents: ReadonlyMap<number, NodeParent>,
|
|
61
|
+
currentNode: number,
|
|
62
|
+
): TreeNode[] {
|
|
63
|
+
const byNode = new Map<number, TreePoint[]>();
|
|
64
|
+
for (const p of points) {
|
|
65
|
+
const list = byNode.get(p.node) ?? [];
|
|
66
|
+
list.push(p);
|
|
67
|
+
byNode.set(p.node, list);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const lastHere = [...points].reverse().find((p) => p.onPath && p.node === currentNode);
|
|
71
|
+
const lastOnPath = lastHere ?? [...points].reverse().find((p) => p.onPath);
|
|
72
|
+
|
|
73
|
+
function toNode(p: TreePoint, children: readonly TreeNode[]): TreeNode {
|
|
74
|
+
return {
|
|
75
|
+
...p,
|
|
76
|
+
id: `${p.node}:${p.atEvent}`,
|
|
77
|
+
prompt: p.prompt,
|
|
78
|
+
onPath: p.onPath,
|
|
79
|
+
current: lastOnPath !== undefined && lastOnPath.node === p.node && lastOnPath.atEvent === p.atEvent,
|
|
80
|
+
kind: p.kind ?? "user",
|
|
81
|
+
children,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Wire event chains and branch edges separately. Recursing once per event
|
|
86
|
+
// overflows the JS stack on otherwise valid long sessions.
|
|
87
|
+
type MutableNode = TreeNode & { children: TreeNode[] };
|
|
88
|
+
const chains = new Map<number, MutableNode[]>();
|
|
89
|
+
const events = new Map<string, MutableNode>();
|
|
90
|
+
for (const [id, pts] of byNode) {
|
|
91
|
+
const nodes = pts.map(p => toNode(p, []) as MutableNode);
|
|
92
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
93
|
+
events.set(nodes[i]!.id, nodes[i]!);
|
|
94
|
+
if (nodes[i + 1]) nodes[i]!.children.push(nodes[i + 1]!);
|
|
95
|
+
}
|
|
96
|
+
chains.set(id, nodes);
|
|
97
|
+
}
|
|
98
|
+
const forest: TreeNode[] = [];
|
|
99
|
+
for (const [id, nodes] of [...chains].sort(([a], [b]) => a - b)) {
|
|
100
|
+
const first = nodes[0];
|
|
101
|
+
if (!first) continue;
|
|
102
|
+
let parent = parents.get(id);
|
|
103
|
+
const visited = new Set<number>([id]);
|
|
104
|
+
while (parent && !chains.get(parent.node)?.length && !visited.has(parent.node)) {
|
|
105
|
+
visited.add(parent.node); parent = parents.get(parent.node);
|
|
106
|
+
}
|
|
107
|
+
const anchor = parent && !visited.has(parent.node) ? events.get(`${parent.node}:${parent.at_event}`) ?? chains.get(parent.node)?.at(-1) : undefined;
|
|
108
|
+
if (anchor) anchor.children.push(first); else forest.push(first);
|
|
109
|
+
}
|
|
110
|
+
return forest;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
export interface TreeRow { readonly node: TreeNode; readonly depth: number; readonly branch: string }
|
|
115
|
+
|
|
116
|
+
/** Depth-first rows with connector prefixes; iterative so a 10k-event session cannot overflow the stack. */
|
|
117
|
+
export function flattenTree(nodes: readonly TreeNode[], filter: TreeFilter = "all"): TreeRow[] {
|
|
118
|
+
const rows: TreeRow[] = [];
|
|
119
|
+
const stack = visibleTree(nodes, filter).map((node, i, all) => ({ node, prefix: "", root: true, last: i === all.length - 1 })).reverse();
|
|
120
|
+
while (stack.length) {
|
|
121
|
+
const { node, prefix, root, last } = stack.pop()!;
|
|
122
|
+
rows.push({ node, depth: prefix.length / 3, branch: root ? "" : prefix + (last ? "└─ " : "├─ ") });
|
|
123
|
+
const next = root ? "" : prefix + (last ? " " : "│ ");
|
|
124
|
+
const children = node.children;
|
|
125
|
+
for (let i = children.length - 1; i >= 0; i--) stack.push({ node: children[i]!, prefix: next.length > 30 ? next.slice(-30) : next, root: false, last: i === children.length - 1 });
|
|
126
|
+
}
|
|
127
|
+
return rows;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Filtering hides a node but keeps its descendants: they re-attach to the
|
|
132
|
+
* nearest visible ancestor, so a "user only" view is still a tree.
|
|
133
|
+
*/
|
|
134
|
+
export function visibleTree(nodes: readonly TreeNode[], filter: TreeFilter): TreeNode[] {
|
|
135
|
+
if (filter === "all") return [...nodes];
|
|
136
|
+
const out: TreeNode[] = [];
|
|
137
|
+
const stack = [...nodes].reverse().map(node => ({ node, into: out }));
|
|
138
|
+
while (stack.length) {
|
|
139
|
+
const { node: n, into } = stack.pop()!;
|
|
140
|
+
const include = filter === "user" || filter === "user-only" ? n.kind === "user" :
|
|
141
|
+
filter === "labeled-only" ? Boolean(n.label) : filter === "no-tools" ? n.category !== "tool" && n.category !== "metadata" : n.category !== "metadata";
|
|
142
|
+
const children: TreeNode[] = [];
|
|
143
|
+
if (include) into.push({ ...n, children });
|
|
144
|
+
for (let i = n.children.length - 1; i >= 0; i--) stack.push({ node: n.children[i]!, into: include ? children : into });
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
package/src/core/settings.ts
CHANGED
|
@@ -4,8 +4,12 @@ import { packageDeclaration, type PackageDeclaration } from "./packages.ts";
|
|
|
4
4
|
/** Validated configuration. D19: display policy never mutates the log. */
|
|
5
5
|
export interface Settings {
|
|
6
6
|
readonly theme?: string;
|
|
7
|
+
/** Presentation only: no animation is allowed to advance a log or block input. */
|
|
8
|
+
readonly animations?: boolean;
|
|
7
9
|
readonly thinking?: string;
|
|
8
10
|
readonly statusBar?: boolean;
|
|
11
|
+
/** Look for a newer published xeno once a day and say so (docs/tasks/feat-update-check.md). Default on. */
|
|
12
|
+
readonly updateCheck?: boolean;
|
|
9
13
|
readonly hideThinkingBlock?: boolean;
|
|
10
14
|
readonly showCacheMissNotices?: boolean;
|
|
11
15
|
readonly defaultProvider?: string;
|
|
@@ -13,9 +17,11 @@ export interface Settings {
|
|
|
13
17
|
readonly defaultThinking?: string;
|
|
14
18
|
readonly modelThinkingLevels?: Readonly<Record<string, ThinkingLevel>>;
|
|
15
19
|
readonly scopedModels?: readonly string[];
|
|
20
|
+
/** provider/model keys, most recent first; the /model picker's first section. */
|
|
21
|
+
readonly recentModels?: readonly string[];
|
|
16
22
|
readonly steeringMode?: "all" | "one-at-a-time";
|
|
17
23
|
readonly followUpMode?: "all" | "one-at-a-time";
|
|
18
|
-
readonly tuiMode?: "regular" | "fullscreen";
|
|
24
|
+
readonly tuiMode?: "auto" | "regular" | "fullscreen";
|
|
19
25
|
readonly fullscreenExitOutput?: "transcript" | "resume-hint";
|
|
20
26
|
readonly fullscreenScrollbar?: "auto" | "always" | "hidden";
|
|
21
27
|
readonly fullscreenCopyOnSelect?: boolean;
|
|
@@ -35,12 +41,14 @@ export interface Settings {
|
|
|
35
41
|
readonly packages?: readonly PackageDeclaration[];
|
|
36
42
|
readonly enableSkillCommands?: boolean;
|
|
37
43
|
readonly defaultProjectTrust?: "ask" | "always" | "never";
|
|
38
|
-
readonly terminal?: { readonly clearOnShrink?: boolean; readonly showTerminalProgress?: boolean; readonly showImages?: boolean; readonly imageWidthCells?: number
|
|
44
|
+
readonly terminal?: { readonly clearOnShrink?: boolean; readonly showTerminalProgress?: boolean; readonly showImages?: boolean; readonly imageWidthCells?: number;
|
|
45
|
+
/** How pictures are drawn; `auto` names them instead when the terminal can only draw block characters. */
|
|
46
|
+
readonly imageProtocol?: "auto" | "kitty" | "sixel" | "blocks" };
|
|
39
47
|
readonly images?: { readonly autoResize?: boolean; readonly blockImages?: boolean };
|
|
40
48
|
readonly markdown?: { readonly mermaid?: "off" | "final" | "streaming" };
|
|
41
49
|
}
|
|
42
|
-
export const SETTINGS_KEYS = ["theme", "thinking", "statusBar", "hideThinkingBlock", "showCacheMissNotices", "defaultProvider", "defaultModel", "defaultThinking",
|
|
43
|
-
"modelThinkingLevels", "scopedModels", "steeringMode", "followUpMode", "tuiMode", "fullscreenExitOutput", "fullscreenScrollbar",
|
|
50
|
+
export const SETTINGS_KEYS = ["theme", "animations", "thinking", "statusBar", "updateCheck", "hideThinkingBlock", "showCacheMissNotices", "defaultProvider", "defaultModel", "defaultThinking",
|
|
51
|
+
"modelThinkingLevels", "scopedModels", "recentModels", "steeringMode", "followUpMode", "tuiMode", "fullscreenExitOutput", "fullscreenScrollbar",
|
|
44
52
|
"fullscreenCopyOnSelect", "fullscreenWheelScrollLines", "editorPaddingX", "outputPad", "autocompleteMaxVisible",
|
|
45
53
|
"showHardwareCursor", "quietStartup", "doubleEscapeAction", "treeFilterMode", "externalEditor", "terminal", "images", "markdown",
|
|
46
54
|
"skills", "prompts", "themes", "extensions", "packages", "enableSkillCommands", "defaultProjectTrust"] as const;
|
|
@@ -52,11 +60,11 @@ type Rule = { readonly kind: "boolean" | "string" | "strings" } | { readonly kin
|
|
|
52
60
|
const levels: Rule = { kind: "enum", values: THINKING_LEVELS };
|
|
53
61
|
const delivery: Rule = { kind: "enum", values: ["all", "one-at-a-time"] };
|
|
54
62
|
const rules: Readonly<Record<string, Rule>> = {
|
|
55
|
-
theme: { kind: "string" }, thinking: levels, statusBar: { kind: "boolean" },
|
|
63
|
+
theme: { kind: "string" }, animations: { kind: "boolean" }, thinking: levels, statusBar: { kind: "boolean" }, updateCheck: { kind: "boolean" },
|
|
56
64
|
hideThinkingBlock: { kind: "boolean" }, showCacheMissNotices: { kind: "boolean" },
|
|
57
65
|
defaultProvider: { kind: "string" }, defaultModel: { kind: "string" }, defaultThinking: levels,
|
|
58
|
-
scopedModels: { kind: "strings" }, steeringMode: delivery, followUpMode: delivery,
|
|
59
|
-
tuiMode: { kind: "enum", values: ["regular", "fullscreen"] },
|
|
66
|
+
scopedModels: { kind: "strings" }, recentModels: { kind: "strings" }, steeringMode: delivery, followUpMode: delivery,
|
|
67
|
+
tuiMode: { kind: "enum", values: ["auto", "regular", "fullscreen"] },
|
|
60
68
|
fullscreenExitOutput: { kind: "enum", values: ["transcript", "resume-hint"] },
|
|
61
69
|
fullscreenScrollbar: { kind: "enum", values: ["auto", "always", "hidden"] },
|
|
62
70
|
fullscreenCopyOnSelect: { kind: "boolean" },
|
|
@@ -70,6 +78,7 @@ const rules: Readonly<Record<string, Rule>> = {
|
|
|
70
78
|
externalEditor: { kind: "string" }, "terminal.clearOnShrink": { kind: "boolean" },
|
|
71
79
|
"terminal.showTerminalProgress": { kind: "boolean" },
|
|
72
80
|
"terminal.showImages": { kind: "boolean" }, "terminal.imageWidthCells": { kind: "integer", min: 1, max: 240 },
|
|
81
|
+
"terminal.imageProtocol": { kind: "enum", values: ["auto", "kitty", "sixel", "blocks"] },
|
|
73
82
|
"images.autoResize": { kind: "boolean" }, "images.blockImages": { kind: "boolean" },
|
|
74
83
|
"markdown.mermaid": { kind: "enum", values: ["off", "final", "streaming"] },
|
|
75
84
|
skills: { kind: "strings" }, prompts: { kind: "strings" }, themes: { kind: "strings" }, extensions: { kind: "strings" },
|
|
@@ -77,19 +86,22 @@ const rules: Readonly<Record<string, Rule>> = {
|
|
|
77
86
|
};
|
|
78
87
|
/** One catalog supplies menu values and command validation. */
|
|
79
88
|
export const DISPLAY_SETTINGS = [
|
|
89
|
+
{ id: "animations", label: "Animations", fallback: "on" },
|
|
80
90
|
{ id: "hideThinkingBlock", label: "Hide thinking blocks", fallback: "on" },
|
|
81
91
|
{ id: "showCacheMissNotices", label: "Cache miss notices", fallback: "off" },
|
|
82
92
|
{ id: "enableSkillCommands", label: "Skill commands", fallback: "on" },
|
|
83
93
|
{ id: "defaultProjectTrust", label: "Default project trust", fallback: "ask" },
|
|
94
|
+
{ id: "updateCheck", label: "Check for updates", fallback: "on" },
|
|
84
95
|
{ id: "terminal.showImages", label: "Show images", fallback: "on" },
|
|
96
|
+
{ id: "terminal.imageProtocol", label: "Image protocol", fallback: "auto" },
|
|
85
97
|
{ id: "terminal.imageWidthCells", label: "Image width", fallback: "60" },
|
|
86
98
|
{ id: "images.autoResize", label: "Resize images", fallback: "on" },
|
|
87
99
|
{ id: "images.blockImages", label: "Block image transmission", fallback: "off" },
|
|
88
100
|
{ id: "markdown.mermaid", label: "Mermaid diagrams", fallback: "streaming" },
|
|
89
|
-
{ id: "tuiMode", label: "TUI mode", fallback: "
|
|
101
|
+
{ id: "tuiMode", label: "TUI mode", fallback: "fullscreen" },
|
|
90
102
|
{ id: "fullscreenExitOutput", label: "Fullscreen exit output", fallback: "transcript" },
|
|
91
|
-
{ id: "fullscreenScrollbar", label: "Fullscreen scrollbar", fallback: "
|
|
92
|
-
{ id: "fullscreenCopyOnSelect", label: "
|
|
103
|
+
{ id: "fullscreenScrollbar", label: "Fullscreen scrollbar", fallback: "hidden" },
|
|
104
|
+
{ id: "fullscreenCopyOnSelect", label: "Copy on select", fallback: "on" },
|
|
93
105
|
{ id: "fullscreenWheelScrollLines", label: "Fullscreen wheel scroll lines", fallback: "auto" },
|
|
94
106
|
{ id: "editorPaddingX", label: "Editor padding", fallback: "0" },
|
|
95
107
|
{ id: "outputPad", label: "Output padding", fallback: "1" },
|
|
@@ -101,6 +113,13 @@ export const DISPLAY_SETTINGS = [
|
|
|
101
113
|
{ id: "terminal.clearOnShrink", label: "Clear on shrink", fallback: "off" },
|
|
102
114
|
{ id: "terminal.showTerminalProgress", label: "Terminal progress", fallback: "on" },
|
|
103
115
|
] as const;
|
|
116
|
+
/** Only fields with a real picker may be suggested without a value. Raw config keys remain writable explicitly. */
|
|
117
|
+
export function interactiveSettingKeys(): string[] {
|
|
118
|
+
const pickers = ["theme", "thinking", "defaultModel", "modelThinkingLevels"];
|
|
119
|
+
return [...new Set([...pickers, ...SETTINGS_KEYS, ...DISPLAY_SETTINGS.map(item => item.id)])]
|
|
120
|
+
.filter(key => pickers.includes(key) || settingValues(key).length > 0);
|
|
121
|
+
}
|
|
122
|
+
|
|
104
123
|
export function defaultSettings(): Settings { return { theme: "dark", thinking: "high", statusBar: true }; }
|
|
105
124
|
export function mergeSettings(layers: SettingsLayers): Settings {
|
|
106
125
|
const { defaultProjectTrust: _projectCannotGrantTrust, ...project } = layers.project ?? {};
|
|
@@ -166,6 +185,10 @@ function validate(key: string, raw: unknown, out: Record<string, unknown>, diagn
|
|
|
166
185
|
else diagnostics.push("settings." + key + " must be " + (rule.kind === "enum" ? rule.values.join("|") : rule.kind === "integer" ? rule.min + ".." + rule.max + (rule.auto ? " or auto" : "") : rule.kind));
|
|
167
186
|
}
|
|
168
187
|
export function settingValues(key: string): string[] {
|
|
188
|
+
// `auto` is the unset default and stays valid in settings.json; cycling it in
|
|
189
|
+
// the menu would be a press that visibly does nothing on a terminal where
|
|
190
|
+
// auto already means fullscreen, so the menu toggles the two real screens.
|
|
191
|
+
if (key === "tuiMode") return ["regular", "fullscreen"];
|
|
169
192
|
const rule = rules[key];
|
|
170
193
|
if (rule?.kind === "enum") return [...rule.values];
|
|
171
194
|
if (rule?.kind === "boolean") return ["on", "off"];
|