@visual-z/xeno 0.1.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.
Files changed (180) hide show
  1. package/README.md +498 -0
  2. package/docs/install.md +130 -0
  3. package/package.json +43 -0
  4. package/src/apps/approve.ts +49 -0
  5. package/src/apps/cli.ts +37 -0
  6. package/src/apps/commands.ts +226 -0
  7. package/src/apps/completions.ts +28 -0
  8. package/src/apps/input.ts +297 -0
  9. package/src/apps/render.ts +207 -0
  10. package/src/apps/shell.ts +2353 -0
  11. package/src/apps/startup.ts +1302 -0
  12. package/src/core/benchmark.ts +195 -0
  13. package/src/core/boundary.ts +174 -0
  14. package/src/core/churn.ts +59 -0
  15. package/src/core/completion-review.ts +66 -0
  16. package/src/core/context-policy.ts +60 -0
  17. package/src/core/context-window.ts +54 -0
  18. package/src/core/continuation.ts +114 -0
  19. package/src/core/cycle.ts +17 -0
  20. package/src/core/data-query.ts +201 -0
  21. package/src/core/deadline.ts +39 -0
  22. package/src/core/derive.ts +489 -0
  23. package/src/core/edit-plan.ts +200 -0
  24. package/src/core/environment.ts +29 -0
  25. package/src/core/event-validation.ts +99 -0
  26. package/src/core/events.ts +486 -0
  27. package/src/core/evidence-links.ts +71 -0
  28. package/src/core/evidence.ts +89 -0
  29. package/src/core/execution.ts +12 -0
  30. package/src/core/exhaustive.ts +18 -0
  31. package/src/core/experiment.ts +40 -0
  32. package/src/core/export.ts +169 -0
  33. package/src/core/file-references.ts +35 -0
  34. package/src/core/frontmatter.ts +213 -0
  35. package/src/core/handoff.ts +397 -0
  36. package/src/core/harness-comparison.ts +90 -0
  37. package/src/core/history-query.ts +103 -0
  38. package/src/core/history.ts +100 -0
  39. package/src/core/json-event.ts +135 -0
  40. package/src/core/json-pointer.ts +46 -0
  41. package/src/core/keymap.ts +241 -0
  42. package/src/core/observation-view.ts +87 -0
  43. package/src/core/outcome.ts +45 -0
  44. package/src/core/overflow.ts +59 -0
  45. package/src/core/permissions.ts +182 -0
  46. package/src/core/policy-evaluation.ts +55 -0
  47. package/src/core/progress.ts +101 -0
  48. package/src/core/project.ts +372 -0
  49. package/src/core/prompts.ts +83 -0
  50. package/src/core/queue.ts +60 -0
  51. package/src/core/read-bounds.ts +15 -0
  52. package/src/core/recall-analysis.ts +90 -0
  53. package/src/core/recall-trial.ts +153 -0
  54. package/src/core/response-recovery.ts +46 -0
  55. package/src/core/scheduling.ts +8 -0
  56. package/src/core/settings.ts +174 -0
  57. package/src/core/skills.ts +121 -0
  58. package/src/core/stall.ts +48 -0
  59. package/src/core/text-normalize.ts +81 -0
  60. package/src/core/theme.ts +179 -0
  61. package/src/core/tokens.ts +149 -0
  62. package/src/core/tool-contract.ts +102 -0
  63. package/src/core/tool-input.ts +38 -0
  64. package/src/core/transcript.ts +60 -0
  65. package/src/core/window-trial.ts +65 -0
  66. package/src/core/work-state.ts +103 -0
  67. package/src/core/yaml.ts +282 -0
  68. package/src/index.ts +10 -0
  69. package/src/runtime/auth.ts +255 -0
  70. package/src/runtime/benchmark.ts +187 -0
  71. package/src/runtime/catalog.ts +365 -0
  72. package/src/runtime/evidence-notes.ts +33 -0
  73. package/src/runtime/exec-sessions.ts +214 -0
  74. package/src/runtime/execution-control.ts +39 -0
  75. package/src/runtime/harness-comparison.ts +356 -0
  76. package/src/runtime/history-tools.ts +108 -0
  77. package/src/runtime/loop.ts +1983 -0
  78. package/src/runtime/model.ts +614 -0
  79. package/src/runtime/models-fetch.ts +155 -0
  80. package/src/runtime/oauth-device.ts +86 -0
  81. package/src/runtime/oauth.ts +759 -0
  82. package/src/runtime/openai-responses.ts +286 -0
  83. package/src/runtime/openai.ts +675 -0
  84. package/src/runtime/provider.ts +498 -0
  85. package/src/runtime/recall-trial.ts +226 -0
  86. package/src/runtime/resume.ts +66 -0
  87. package/src/runtime/retry.ts +297 -0
  88. package/src/runtime/tools.ts +1193 -0
  89. package/src/runtime/usage.ts +225 -0
  90. package/src/runtime/window-trial.ts +140 -0
  91. package/src/runtime/working-notes.ts +30 -0
  92. package/src/store/atomic.ts +31 -0
  93. package/src/store/auth-store.ts +50 -0
  94. package/src/store/blobs.ts +142 -0
  95. package/src/store/command.ts +75 -0
  96. package/src/store/config.ts +69 -0
  97. package/src/store/context-files.ts +356 -0
  98. package/src/store/file-index.ts +81 -0
  99. package/src/store/git-branch.ts +35 -0
  100. package/src/store/history-search.ts +19 -0
  101. package/src/store/log.ts +642 -0
  102. package/src/store/models.ts +41 -0
  103. package/src/store/observations.ts +181 -0
  104. package/src/store/query-source.ts +44 -0
  105. package/src/store/recovery.ts +39 -0
  106. package/src/store/replay.ts +63 -0
  107. package/src/store/resources.ts +361 -0
  108. package/src/store/restore.ts +302 -0
  109. package/src/store/search.ts +644 -0
  110. package/src/store/serial.ts +13 -0
  111. package/src/store/sessions.ts +463 -0
  112. package/src/store/trust.ts +69 -0
  113. package/src/store/workspace.ts +256 -0
  114. package/src/store/worktree.ts +233 -0
  115. package/src/tui/ansi.ts +67 -0
  116. package/src/tui/autocomplete.ts +29 -0
  117. package/src/tui/complete.ts +87 -0
  118. package/src/tui/diff.ts +160 -0
  119. package/src/tui/editor.ts +893 -0
  120. package/src/tui/exhaustive.ts +14 -0
  121. package/src/tui/fuzzy.ts +75 -0
  122. package/src/tui/host.ts +223 -0
  123. package/src/tui/input.ts +69 -0
  124. package/src/tui/keys.ts +378 -0
  125. package/src/tui/markdown.ts +17 -0
  126. package/src/tui/render.ts +153 -0
  127. package/src/tui/select.ts +240 -0
  128. package/src/tui/settings.ts +40 -0
  129. package/src/tui/statusline.ts +162 -0
  130. package/src/tui/style.ts +205 -0
  131. package/src/tui/terminal.ts +30 -0
  132. package/src/tui/text.ts +52 -0
  133. package/src/tui/toolview.ts +386 -0
  134. package/src/tui/transcript.ts +71 -0
  135. package/src/tui/tree.ts +405 -0
  136. package/src/tui/width.ts +567 -0
  137. package/src/vendor/pi-tui/LICENSE.upstream +21 -0
  138. package/src/vendor/pi-tui/VENDOR.md +23 -0
  139. package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
  140. package/src/vendor/pi-tui/autocomplete.ts +835 -0
  141. package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
  142. package/src/vendor/pi-tui/components/box.ts +167 -0
  143. package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
  144. package/src/vendor/pi-tui/components/editor.ts +2470 -0
  145. package/src/vendor/pi-tui/components/h-stack.ts +44 -0
  146. package/src/vendor/pi-tui/components/image.ts +127 -0
  147. package/src/vendor/pi-tui/components/input.ts +494 -0
  148. package/src/vendor/pi-tui/components/loader.ts +101 -0
  149. package/src/vendor/pi-tui/components/markdown.ts +1015 -0
  150. package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
  151. package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
  152. package/src/vendor/pi-tui/components/select-list.ts +273 -0
  153. package/src/vendor/pi-tui/components/settings-list.ts +328 -0
  154. package/src/vendor/pi-tui/components/spacer.ts +28 -0
  155. package/src/vendor/pi-tui/components/stack.ts +154 -0
  156. package/src/vendor/pi-tui/components/text.ts +107 -0
  157. package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
  158. package/src/vendor/pi-tui/components/v-stack.ts +33 -0
  159. package/src/vendor/pi-tui/editor-component.ts +74 -0
  160. package/src/vendor/pi-tui/fuzzy.ts +138 -0
  161. package/src/vendor/pi-tui/index.ts +156 -0
  162. package/src/vendor/pi-tui/keybindings.ts +320 -0
  163. package/src/vendor/pi-tui/keys.ts +1401 -0
  164. package/src/vendor/pi-tui/kill-ring.ts +46 -0
  165. package/src/vendor/pi-tui/latex.ts +1506 -0
  166. package/src/vendor/pi-tui/layout-node.ts +51 -0
  167. package/src/vendor/pi-tui/layout.ts +449 -0
  168. package/src/vendor/pi-tui/native-modifiers.ts +13 -0
  169. package/src/vendor/pi-tui/native-module-path.ts +31 -0
  170. package/src/vendor/pi-tui/native-platform.ts +63 -0
  171. package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
  172. package/src/vendor/pi-tui/terminal-colors.ts +73 -0
  173. package/src/vendor/pi-tui/terminal-image.ts +696 -0
  174. package/src/vendor/pi-tui/terminal.ts +547 -0
  175. package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
  176. package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
  177. package/src/vendor/pi-tui/tui.ts +1456 -0
  178. package/src/vendor/pi-tui/undo-stack.ts +28 -0
  179. package/src/vendor/pi-tui/utils.ts +1345 -0
  180. package/src/vendor/pi-tui/word-navigation.ts +117 -0
@@ -0,0 +1,100 @@
1
+ /** D20: bounded carried state, with deterministic pages rebuilt from the log. */
2
+ import { emitHandoff, type Handoff } from "./handoff.ts";
3
+ import { estimateTokens } from "./tokens.ts";
4
+
5
+ // A carried handoff must leave room for another segment's work. This budget is
6
+ // the existing 5k work floor, not a new segment threshold or an accuracy claim.
7
+ export const DEFAULT_HANDOFF_TOKENS = 5_000;
8
+ export const HISTORY_SECTIONS = [
9
+ "user_turns", "state.completed", "state.unverified_claims", "state.in_flight",
10
+ "state.blocked", "decisions", "pitfalls", "file_anchors", "verification",
11
+ "branches", "open_questions", "progress_evidence",
12
+ "state.work_items",
13
+ "evidence_links",
14
+ ] as const;
15
+ export type HistorySection = (typeof HISTORY_SECTIONS)[number];
16
+
17
+ export function historyItems(h: Handoff, section: HistorySection): readonly unknown[] {
18
+ switch (section) {
19
+ case "state.completed": return h.state.completed;
20
+ case "state.unverified_claims": return h.state.unverified_claims;
21
+ case "state.in_flight": return h.state.in_flight;
22
+ case "state.blocked": return h.state.blocked;
23
+ case "state.work_items": return h.state.work_items ?? [];
24
+ case "evidence_links": return h.evidence_links ?? [];
25
+ case "progress_evidence": return h.progress_evidence ?? [];
26
+ default: return h[section];
27
+ }
28
+ }
29
+
30
+ function suffixes(h: Handoff, offsets: Record<HistorySection, number>): Handoff {
31
+ return {
32
+ ...h,
33
+ user_turns: h.user_turns.slice(offsets.user_turns),
34
+ state: {
35
+ ...h.state,
36
+ completed: h.state.completed.slice(offsets["state.completed"]),
37
+ unverified_claims: h.state.unverified_claims.slice(offsets["state.unverified_claims"]),
38
+ in_flight: h.state.in_flight.slice(offsets["state.in_flight"]),
39
+ blocked: h.state.blocked.slice(offsets["state.blocked"]),
40
+ ...(h.state.work_items !== undefined ? { work_items: h.state.work_items.slice(offsets["state.work_items"] ?? 0) } : {}),
41
+ },
42
+ decisions: h.decisions.slice(offsets.decisions),
43
+ pitfalls: h.pitfalls.slice(offsets.pitfalls),
44
+ file_anchors: h.file_anchors.slice(offsets.file_anchors),
45
+ verification: h.verification.slice(offsets.verification),
46
+ branches: h.branches.slice(offsets.branches),
47
+ open_questions: h.open_questions.slice(offsets.open_questions),
48
+ progress_evidence: (h.progress_evidence ?? []).slice(offsets.progress_evidence),
49
+ ...(h.evidence_links !== undefined ? { evidence_links: h.evidence_links.slice(offsets.evidence_links ?? 0) } : {}),
50
+ };
51
+ }
52
+
53
+ export function boundHandoff(full: Handoff, budget = DEFAULT_HANDOFF_TOKENS): Handoff {
54
+ if (!Number.isFinite(budget) || budget <= 0) throw new RangeError("handoff budget must be positive");
55
+ const size = (h: Handoff): number => estimateTokens.count([], emitHandoff(h));
56
+ if (size(full) <= budget) return full;
57
+ if (full.predecessor === null) throw new Error("history requires an immutable source");
58
+ // An absent new section must not change legacy projections or frozen replay.
59
+ const sections = HISTORY_SECTIONS.filter(section => (section !== "state.work_items" || full.state.work_items !== undefined) &&
60
+ (section !== "evidence_links" || full.evidence_links !== undefined));
61
+ const offsets = Object.fromEntries(sections.map((s) => [s, historyItems(full, s).length])) as Record<HistorySection, number>;
62
+ const build = (): Handoff => ({
63
+ ...suffixes(full, offsets),
64
+ handoff_version: 2,
65
+ history: {
66
+ source: full.predecessor!,
67
+ sections: Object.fromEntries(sections.map((s) => [s, {
68
+ total: historyItems(full, s).length, offset: offsets[s],
69
+ }])),
70
+ },
71
+ });
72
+ let carried = build();
73
+ // The objective stays verbatim (D2). An oversized objective is an explicit
74
+ // input problem; repeatedly cutting it would create an infinite cut loop.
75
+ if (size(carried) > budget) throw new RangeError("handoff scalar fields exceed budget; shorten the objective or frozen input");
76
+ const active = new Set<HistorySection>(sections);
77
+ // Round-robin suffix growth prevents one long section starving every other
78
+ // category. Each retained range is contiguous and independently pageable.
79
+ while (active.size > 0) {
80
+ for (const section of active) {
81
+ if (offsets[section] === 0) { active.delete(section); continue; }
82
+ offsets[section]--;
83
+ const candidate = build();
84
+ if (size(candidate) <= budget) carried = candidate;
85
+ else { offsets[section]++; active.delete(section); }
86
+ }
87
+ }
88
+ return carried;
89
+ }
90
+
91
+ export function historyPage(full: Handoff, section: HistorySection, offset: number, limit: number): {
92
+ total: number; offset: number; next: number | null; items: readonly unknown[];
93
+ } {
94
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1) {
95
+ throw new RangeError("history offset and limit must be nonnegative/positive integers");
96
+ }
97
+ const items = historyItems(full, section);
98
+ const end = Math.min(items.length, offset + limit);
99
+ return { total: items.length, offset, next: end < items.length ? end : null, items: items.slice(offset, end) };
100
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * LoopEvent → one JSON object for `--mode json`.
3
+ *
4
+ * Declared here, not imported from runtime, so core stays below the loop
5
+ * (D13: core does not import a layer above it). The kind union is kept in
6
+ * lockstep with LoopEvent; a new kind makes the call site in apps fail to
7
+ * type-check until this switch grows a case.
8
+ *
9
+ * Deltas return null. A pipe cannot retract a half-turn, and a retry would
10
+ * then emit the same prefix twice — the same reason `-p` waits for the
11
+ * complete block. The caller still sees `assistant_text` / `thinking` once
12
+ * the turn has a final block.
13
+ */
14
+
15
+ import { assertNever } from "./exhaustive.ts";
16
+ import type { ExecutionOutcome } from "./outcome.ts";
17
+
18
+ export type JsonLoopKind =
19
+ | "turn_start"
20
+ | "assistant_text"
21
+ | "thinking"
22
+ | "text_delta"
23
+ | "thinking_delta"
24
+ | "discard"
25
+ | "tool_call"
26
+ | "tool_result"
27
+ | "usage"
28
+ | "cut"
29
+ | "stop_and_ask"
30
+ | "branch_start"
31
+ | "branch_done"
32
+ | "snapshot_failed"
33
+ | "undeclared_change"
34
+ | "done";
35
+
36
+ /**
37
+ * Structural match for runtime LoopEvent. Copied rather than imported so
38
+ * this module cannot grow an upward dependency.
39
+ */
40
+ export interface JsonLoopEvent {
41
+ kind: JsonLoopKind;
42
+ text?: string;
43
+ tool?: { name: string; input: unknown };
44
+ ok?: boolean;
45
+ usage?: {
46
+ input_tokens: number;
47
+ output_tokens: number;
48
+ cache_creation_input_tokens: number;
49
+ cache_read_input_tokens: number;
50
+ node: number;
51
+ ms: number;
52
+ };
53
+ prefix?: string;
54
+ node?: number;
55
+ reason?: string;
56
+ at?: number;
57
+ outcome?: ExecutionOutcome;
58
+ }
59
+
60
+ /**
61
+ * `null` means an internal event that must not go on the wire.
62
+ * `at` is supplied by the caller (ISO from the session clock) so this
63
+ * function stays free of Date.
64
+ */
65
+ export function toJsonEvent(e: JsonLoopEvent, at: string): Record<string, unknown> | null {
66
+ switch (e.kind) {
67
+ case "text_delta":
68
+ case "thinking_delta":
69
+ case "discard":
70
+ return null;
71
+ case "assistant_text":
72
+ return { type: "assistant_text", text: e.text ?? "", at };
73
+ case "thinking":
74
+ return { type: "thinking", text: e.text ?? "", at };
75
+ case "tool_call":
76
+ return { type: "tool_call", tool: e.tool ?? { name: "", input: {} }, at };
77
+ case "tool_result":
78
+ return { type: "tool_result", text: e.text ?? "", ok: e.ok !== false,
79
+ ...(e.outcome ? { outcome: e.outcome } : {}), at };
80
+ case "usage":
81
+ return {
82
+ type: "usage",
83
+ ...(e.usage !== undefined
84
+ ? {
85
+ usage: {
86
+ input_tokens: e.usage.input_tokens,
87
+ output_tokens: e.usage.output_tokens,
88
+ cache_creation_input_tokens: e.usage.cache_creation_input_tokens,
89
+ cache_read_input_tokens: e.usage.cache_read_input_tokens,
90
+ node: e.usage.node,
91
+ ms: e.usage.ms,
92
+ },
93
+ }
94
+ : {}),
95
+ ...(e.prefix !== undefined ? { prefix: e.prefix } : {}),
96
+ ok: e.ok !== false,
97
+ at,
98
+ };
99
+ case "turn_start":
100
+ return { type: "turn_start", ...(e.node !== undefined ? { node: e.node } : {}), at };
101
+ case "cut":
102
+ return {
103
+ type: "cut",
104
+ ...(e.node !== undefined ? { node: e.node } : {}),
105
+ ...(e.reason !== undefined ? { reason: e.reason } : {}),
106
+ at,
107
+ };
108
+ case "stop_and_ask":
109
+ return { type: "stop_and_ask", ...(e.reason !== undefined ? { reason: e.reason } : {}), at };
110
+ case "done":
111
+ return { type: "done", ...(e.reason !== undefined ? { reason: e.reason } : {}),
112
+ ...(e.outcome ? { outcome: e.outcome } : {}), at };
113
+ case "branch_start":
114
+ return {
115
+ type: "branch_start",
116
+ ...(e.node !== undefined ? { node: e.node } : {}),
117
+ ...(e.reason !== undefined ? { reason: e.reason } : {}),
118
+ at,
119
+ };
120
+ case "branch_done":
121
+ return {
122
+ type: "branch_done",
123
+ ...(e.outcome ? { outcome: e.outcome } : {}),
124
+ ...(e.node !== undefined ? { node: e.node } : {}),
125
+ ...(e.text !== undefined ? { text: e.text } : {}),
126
+ at,
127
+ };
128
+ case "snapshot_failed":
129
+ return { type: "snapshot_failed", ...(e.reason !== undefined ? { reason: e.reason } : {}), at };
130
+ case "undeclared_change":
131
+ return { type: "undeclared_change", ...(e.reason !== undefined ? { reason: e.reason } : {}), at };
132
+ default:
133
+ return assertNever(e.kind, "toJsonEvent");
134
+ }
135
+ }
@@ -0,0 +1,46 @@
1
+ /** Parsed-JSON traversal and recoverable diagnostics (research N.5, RFC 6901). */
2
+ // Assumed diagnostic budget, not a data limit: show only complete short keys.
3
+ // Twelve forty-unit keys keep even JSON-escaped control characters below 4 KiB.
4
+ const HINT_KEYS = 12;
5
+ const HINT_KEY_UNITS = 40;
6
+
7
+ export function validateJsonPointer(pointer: string): void {
8
+ if ((pointer !== "" && !pointer.startsWith("/")) || /~(?:[^01]|$)/.test(pointer)) {
9
+ throw new Error('invalid JSON pointer: use "" for the root, / between keys, ~0 for ~ and ~1 for /');
10
+ }
11
+ }
12
+
13
+ function arrayIndex(key: string, length: number): boolean {
14
+ const index = Number(key);
15
+ return /^(0|[1-9][0-9]*)$/.test(key) && Number.isSafeInteger(index) && index < length;
16
+ }
17
+
18
+ function missingPointer(parent: unknown, segment: number): Error {
19
+ const array = Array.isArray(parent);
20
+ const kind = parent === null ? "null" : array ? "array" : typeof parent;
21
+ const keys = parent !== null && typeof parent === "object"
22
+ ? Object.keys(parent).filter((key) => !array || arrayIndex(key, parent.length)) : [];
23
+ const shown = keys.filter((key) => key.length <= HINT_KEY_UNITS).sort().slice(0, HINT_KEYS);
24
+ // Segment ordinal identifies the valid prefix without echoing unbounded input
25
+ // paths or source values. Omitted names remain readable through that prefix.
26
+ return new Error(`JSON pointer does not exist at segment ${segment}; parent type: ${kind}. ` +
27
+ `Available keys (${shown.length} of ${keys.length}): ${JSON.stringify(shown)}; ` +
28
+ `${keys.length - shown.length} omitted (long names or diagnostic budget). ` +
29
+ `Inspect the pointer prefix before segment ${segment}; use pointer="" for the document root. ` +
30
+ "The pointer / selects an empty key, not the root.");
31
+ }
32
+
33
+ export function resolveJsonPointer(document: unknown, pointer: string): unknown {
34
+ validateJsonPointer(pointer);
35
+ if (pointer === "") return document;
36
+ let value = document;
37
+ for (const [index, raw] of pointer.slice(1).split("/").entries()) {
38
+ const key = raw.replaceAll("~1", "/").replaceAll("~0", "~");
39
+ if (value === null || typeof value !== "object" ||
40
+ (Array.isArray(value) && !arrayIndex(key, value.length)) || !Object.hasOwn(value, key)) {
41
+ throw missingPointer(value, index + 1);
42
+ }
43
+ value = (value as Record<string, unknown>)[key];
44
+ }
45
+ return value;
46
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Action id ↔ key chord. Pure: the file is read in store; this module
3
+ * parses, reports conflicts by name, and lets the last definition win.
4
+ *
5
+ * Silent drop of one side of a conflict is how a shortcut "just stops
6
+ * working" with no trail (feat-theming design). Namespaced ids follow
7
+ * pi's scheme; the action list is ours.
8
+ */
9
+
10
+ export const ACTION_IDS = [
11
+ "editor.cursorLeft",
12
+ "editor.cursorRight",
13
+ "editor.cursorWordLeft",
14
+ "editor.cursorWordRight",
15
+ "editor.cursorLineStart",
16
+ "editor.cursorLineEnd",
17
+ "editor.cursorUp",
18
+ "editor.cursorDown",
19
+ "editor.deleteCharBackward",
20
+ "editor.deleteCharForward",
21
+ "editor.deleteWordBackward",
22
+ "editor.deleteToLineStart",
23
+ "editor.deleteToLineEnd",
24
+ "editor.submit",
25
+ "editor.newLine",
26
+ "editor.undo",
27
+ "editor.yank",
28
+ "editor.deleteWordForward",
29
+ "editor.complete",
30
+ "app.toggleTools",
31
+ "app.toggleThinking",
32
+ "app.interrupt",
33
+ "app.exit",
34
+ "app.message.followUp",
35
+ "app.message.dequeue",
36
+ "app.model",
37
+ "app.cycleModel",
38
+ "app.cycleModelReverse",
39
+ "app.cycleThinking",
40
+ "app.saveDefault",
41
+ "app.externalEditor",
42
+ ] as const;
43
+
44
+ export type ActionId = (typeof ACTION_IDS)[number];
45
+
46
+ export interface Chord {
47
+ readonly ctrl: boolean;
48
+ readonly alt: boolean;
49
+ readonly shift: boolean;
50
+ readonly super: boolean;
51
+ readonly key: string;
52
+ }
53
+
54
+ export interface Keymap {
55
+ readonly byKey: Readonly<Record<string, ActionId>>;
56
+ readonly byAction: Readonly<Record<string, readonly string[]>>;
57
+ readonly diagnostics: readonly string[];
58
+ }
59
+
60
+ const ACTION_SET = new Set<string>(ACTION_IDS);
61
+
62
+ const NAMED_KEYS = new Set([
63
+ "escape",
64
+ "esc",
65
+ "enter",
66
+ "return",
67
+ "tab",
68
+ "space",
69
+ "backspace",
70
+ "delete",
71
+ "insert",
72
+ "home",
73
+ "end",
74
+ "pageup",
75
+ "pagedown",
76
+ "up",
77
+ "down",
78
+ "left",
79
+ "right",
80
+ ]);
81
+
82
+ export function parseKey(spec: string): Chord | { error: string } {
83
+ const raw = spec.trim();
84
+ if (raw.length === 0) return { error: `unparseable key: ${JSON.stringify(spec)}` };
85
+ const parts = raw.split("+").map((p) => p.trim()).filter((p) => p.length > 0);
86
+ if (parts.length === 0) return { error: `unparseable key: ${JSON.stringify(spec)}` };
87
+ let ctrl = false;
88
+ let alt = false;
89
+ let shift = false;
90
+ let superKey = false;
91
+ const last = parts[parts.length - 1]!;
92
+ for (const part of parts.slice(0, -1)) {
93
+ const m = part.toLowerCase();
94
+ if (m === "ctrl" || m === "control") ctrl = true;
95
+ else if (m === "alt" || m === "option") alt = true;
96
+ else if (m === "shift") shift = true;
97
+ else if (m === "super" || m === "cmd" || m === "meta") superKey = true;
98
+ else return { error: `unparseable key: ${JSON.stringify(spec)}` };
99
+ }
100
+ const key = normalizeKeyName(last);
101
+ if (key === null) return { error: `unparseable key: ${JSON.stringify(spec)}` };
102
+ return { ctrl, alt, shift, super: superKey, key };
103
+ }
104
+
105
+ export function formatKey(chord: Chord): string {
106
+ const parts: string[] = [];
107
+ if (chord.ctrl) parts.push("ctrl");
108
+ if (chord.alt) parts.push("alt");
109
+ if (chord.shift) parts.push("shift");
110
+ if (chord.super) parts.push("super");
111
+ parts.push(chord.key);
112
+ return parts.join("+");
113
+ }
114
+
115
+ export function resolveAction(map: Keymap, spec: string): ActionId | undefined {
116
+ const parsed = parseKey(spec);
117
+ if ("error" in parsed) return undefined;
118
+ return map.byKey[formatKey(parsed)];
119
+ }
120
+
121
+ export function defaultKeymap(): Keymap {
122
+ return parseKeymap(
123
+ {
124
+ "editor.cursorLeft": ["left", "ctrl+b"],
125
+ "editor.cursorRight": ["right", "ctrl+f"],
126
+ "editor.cursorWordLeft": ["alt+left", "ctrl+left", "alt+b"],
127
+ "editor.cursorWordRight": ["alt+right", "ctrl+right", "alt+f"],
128
+ "editor.cursorLineStart": ["home", "ctrl+a"],
129
+ "editor.cursorLineEnd": ["end", "ctrl+e"],
130
+ "editor.cursorUp": ["up"],
131
+ "editor.cursorDown": ["down"],
132
+ "editor.deleteCharBackward": ["backspace"],
133
+ "editor.deleteCharForward": ["delete", "ctrl+d"],
134
+ "editor.deleteWordBackward": ["ctrl+w", "alt+backspace"],
135
+ "editor.deleteWordForward": ["alt+d"],
136
+ "editor.deleteToLineStart": ["ctrl+u"],
137
+ "editor.deleteToLineEnd": ["ctrl+k"],
138
+ "editor.submit": ["enter"],
139
+ "editor.newLine": ["shift+enter", "ctrl+j"],
140
+ "editor.undo": ["ctrl+-", "ctrl+z", "ctrl+_"],
141
+ "editor.yank": ["ctrl+y"],
142
+ "editor.complete": ["tab"],
143
+ "app.toggleTools": ["ctrl+o"],
144
+ "app.toggleThinking": ["ctrl+t"],
145
+ "app.interrupt": ["escape"],
146
+ "app.exit": ["ctrl+c"],
147
+ "app.message.followUp": ["alt+enter"],
148
+ "app.message.dequeue": ["alt+up"],
149
+ "app.model": ["ctrl+l"],
150
+ "app.cycleModel": ["ctrl+p"],
151
+ "app.cycleModelReverse": ["ctrl+shift+p"],
152
+ "app.cycleThinking": ["shift+tab"],
153
+ "app.saveDefault": ["ctrl+s"],
154
+ "app.externalEditor": ["ctrl+g"],
155
+ },
156
+ emptyKeymap(),
157
+ );
158
+ }
159
+
160
+ export function parseKeymap(json: unknown, base?: Keymap): Keymap {
161
+ const diagnostics: string[] = [];
162
+ const byKey: { [k: string]: ActionId } = { ...(base?.byKey ?? {}) };
163
+
164
+ if (json === undefined || json === null) {
165
+ return finish(byKey, base?.diagnostics ?? []);
166
+ }
167
+ if (typeof json !== "object" || Array.isArray(json)) {
168
+ return finish(byKey, [...(base?.diagnostics ?? []), "keybindings is not an object"]);
169
+ }
170
+
171
+ const rec = json as { readonly [k: string]: unknown };
172
+ for (const [action, raw] of Object.entries(rec)) {
173
+ if (!ACTION_SET.has(action)) {
174
+ diagnostics.push(`unknown action: ${action}`);
175
+ continue;
176
+ }
177
+ const specs = keySpecs(raw);
178
+ if (specs === null) {
179
+ diagnostics.push(`${action}: bindings must be a string or array of strings`);
180
+ continue;
181
+ }
182
+ // User (or this file) replaces the previous chords for this action.
183
+ for (const [k, a] of Object.entries(byKey)) {
184
+ if (a === action) delete byKey[k];
185
+ }
186
+ for (const spec of specs) {
187
+ const parsed = parseKey(spec);
188
+ if ("error" in parsed) {
189
+ diagnostics.push(`${parsed.error} (action ${action})`);
190
+ continue;
191
+ }
192
+ const id = formatKey(parsed);
193
+ const prev = byKey[id];
194
+ if (prev !== undefined && prev !== action) {
195
+ diagnostics.push(`key ${id} is bound to both ${prev} and ${action}; ${action} wins`);
196
+ }
197
+ byKey[id] = action as ActionId;
198
+ }
199
+ }
200
+
201
+ return finish(byKey, [...(base?.diagnostics ?? []), ...diagnostics]);
202
+ }
203
+
204
+ export function listBindings(map: Keymap): { action: string; keys: readonly string[] }[] {
205
+ return ACTION_IDS.filter((a) => (map.byAction[a] ?? []).length > 0).map((action) => ({
206
+ action,
207
+ keys: map.byAction[action] ?? [],
208
+ }));
209
+ }
210
+
211
+ function emptyKeymap(): Keymap {
212
+ return { byKey: {}, byAction: {}, diagnostics: [] };
213
+ }
214
+
215
+ function finish(byKey: { readonly [k: string]: ActionId }, diagnostics: readonly string[]): Keymap {
216
+ const byAction: { [k: string]: string[] } = {};
217
+ for (const [key, action] of Object.entries(byKey)) {
218
+ const list = byAction[action] ?? [];
219
+ list.push(key);
220
+ byAction[action] = list;
221
+ }
222
+ return { byKey, byAction, diagnostics };
223
+ }
224
+
225
+ function keySpecs(raw: unknown): string[] | null {
226
+ if (typeof raw === "string") return [raw];
227
+ if (Array.isArray(raw) && raw.every((x) => typeof x === "string")) return raw as string[];
228
+ return null;
229
+ }
230
+
231
+ function normalizeKeyName(raw: string): string | null {
232
+ const lower = raw.toLowerCase();
233
+ if (lower === "esc") return "escape";
234
+ if (lower === "return") return "enter";
235
+ if (lower === "pageup") return "pageup";
236
+ if (lower === "pagedown") return "pagedown";
237
+ if (NAMED_KEYS.has(lower)) return lower === "esc" ? "escape" : lower === "return" ? "enter" : lower;
238
+ if (/^f([1-9]|1[0-2])$/.test(lower)) return lower;
239
+ if (raw.length === 1) return lower;
240
+ return null;
241
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * How a bounded observation is presented to the model.
3
+ *
4
+ * The complete bytes always live in a blob (D8: reference, never inline), so
5
+ * this is only a view -- but it is the view the model reasons from. Keeping
6
+ * only the head, as this module's predecessor did, cuts exactly the part a
7
+ * failing build or test run prints last: the error and the summary. Codex
8
+ * keeps half of each end (`utils/string/src/truncate.rs`); so does this.
9
+ */
10
+
11
+ /** A UTF-8 continuation byte: cutting before one would split a character. */
12
+ const continuation = (byte: number | undefined) => byte !== undefined && (byte & 0xc0) === 0x80;
13
+
14
+ /**
15
+ * Head and tail of an observation within `limit` bytes.
16
+ *
17
+ * `head` is the first bytes and `tail` the last bytes of an observation of
18
+ * `total` bytes; each need only be as long as `limit`. When the whole
19
+ * observation fits, it is returned unchanged. Otherwise the marker, which is
20
+ * given the omitted byte count and the byte offset where the gap starts (so
21
+ * the model can page the missing middle back), sits between the two ends.
22
+ * Cuts land on character boundaries, which is why a view can come out a few
23
+ * bytes under budget but never over it for valid UTF-8 input.
24
+ */
25
+ export function headTailView(
26
+ head: Uint8Array,
27
+ tail: Uint8Array,
28
+ total: number,
29
+ limit: number,
30
+ marker: (omitted: number, offset: number) => string,
31
+ ): string {
32
+ const decoder = new TextDecoder();
33
+ if (total <= limit && head.length >= total) return decoder.decode(head.subarray(0, total));
34
+ // Digits of the real numbers never exceed digits of `total`, so this bounds
35
+ // the marker before the cut points that decide its numbers are known.
36
+ const reserve = new TextEncoder().encode(marker(total, total)).length;
37
+ const budget = Math.max(0, limit - reserve);
38
+ let headEnd = Math.min(Math.floor(budget / 2), head.length);
39
+ while (headEnd > 0 && continuation(head[headEnd])) headEnd--;
40
+ let tailStart = tail.length - Math.min(budget - headEnd, tail.length);
41
+ while (tailStart < tail.length && continuation(tail[tailStart])) tailStart++;
42
+ const kept = tail.length - tailStart;
43
+ return decoder.decode(head.subarray(0, headEnd)) +
44
+ marker(total - headEnd - kept, headEnd) +
45
+ decoder.decode(tail.subarray(tailStart));
46
+ }
47
+
48
+ /** Signals a shell reports as 128+n; the common ones a model can act on. */
49
+ const SIGNALS: Readonly<Record<number, string>> = {
50
+ 1: "SIGHUP", 2: "SIGINT", 6: "SIGABRT", 9: "SIGKILL", 11: "SIGSEGV", 13: "SIGPIPE", 15: "SIGTERM",
51
+ };
52
+
53
+ const seconds = (ms: number) => `${Number((ms / 1000).toFixed(1))}s`;
54
+
55
+ /**
56
+ * First line of a command result.
57
+ *
58
+ * `exit N` stays first because both the TUI and the log readers key on it.
59
+ * What follows states facts the exit code alone hides: a timeout reads as
60
+ * `exit 143`, which a model otherwise takes for the program's own failure and
61
+ * reruns unchanged (codex puts wall time and exit in the same header,
62
+ * `core/src/tools/context.rs`).
63
+ */
64
+ export function commandHeader(input: {
65
+ exit: number;
66
+ wallMs: number;
67
+ stopped?: "timeout" | "cancelled";
68
+ /** The timeout actually applied. */
69
+ timeoutMs: number;
70
+ /** What the model asked for, when that differs from what was applied. */
71
+ requestedMs?: number;
72
+ }): string {
73
+ const parts = [`exit ${input.exit}`, `${(input.wallMs / 1000).toFixed(1)}s`];
74
+ if (input.stopped === "timeout") {
75
+ const requested = input.requestedMs !== undefined && input.requestedMs !== input.timeoutMs
76
+ ? ` (requested ${seconds(input.requestedMs)}, capped)` : "";
77
+ // Say what to do differently: the same command with the same timeout
78
+ // will time out the same way.
79
+ parts.push(`timed out after ${seconds(input.timeoutMs)}${requested}; process group terminated; ` +
80
+ "rerun with a larger timeout, or start long-lived work in the background with output redirected to a file and poll it");
81
+ } else if (input.stopped === "cancelled") {
82
+ parts.push("cancelled; process group terminated");
83
+ } else if (input.exit > 128 && SIGNALS[input.exit - 128] !== undefined) {
84
+ parts.push(`128+${input.exit - 128}: likely terminated by ${SIGNALS[input.exit - 128]}`);
85
+ }
86
+ return parts.join(" · ");
87
+ }
@@ -0,0 +1,45 @@
1
+ /** O.1 / D11: execution termination is a fact, distinct from verified work. */
2
+ const STATUS = {
3
+ model_end: "completed",
4
+ tool_completed: "completed",
5
+ cancelled: "cancelled",
6
+ not_started: "cancelled",
7
+ max_turns: "stopped",
8
+ context_budget: "stopped",
9
+ stalled: "stopped",
10
+ no_progress: "stopped",
11
+ output_limit: "stopped",
12
+ timeout: "stopped",
13
+ model_refusal: "failed",
14
+ provider_error: "failed",
15
+ execution_error: "failed",
16
+ projection_invalid: "failed",
17
+ snapshot_failed: "failed",
18
+ tool_failed: "failed",
19
+ unknown: "unknown",
20
+ } as const;
21
+
22
+ export type ExecutionReason = keyof typeof STATUS;
23
+ export type ExecutionOutcome = {
24
+ status: (typeof STATUS)[ExecutionReason];
25
+ reason: ExecutionReason;
26
+ };
27
+
28
+ export function executionOutcome(reason: ExecutionReason): ExecutionOutcome {
29
+ return { status: STATUS[reason], reason };
30
+ }
31
+
32
+ /** Unknown/legacy provider reasons never silently turn into success. */
33
+ export function modelOutcome(reason: string | null): ExecutionOutcome {
34
+ if (reason === "end_turn" || reason === "stop" || reason === "stop_sequence") return executionOutcome("model_end");
35
+ if (reason === "max_tokens" || reason === "length") return executionOutcome("output_limit");
36
+ if (reason === "refusal" || reason === "content_filter") return executionOutcome("model_refusal");
37
+ if (reason === "error") return executionOutcome("provider_error");
38
+ return executionOutcome("unknown");
39
+ }
40
+
41
+ export function isExecutionOutcome(value: unknown): value is ExecutionOutcome {
42
+ if (value === null || typeof value !== "object" || !("reason" in value) || !("status" in value)) return false;
43
+ return typeof value.reason === "string" && Object.hasOwn(STATUS, value.reason) &&
44
+ value.status === STATUS[value.reason as ExecutionReason];
45
+ }