@visual-z/xeno 0.1.3 → 0.2.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 (148) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +143 -33
  3. package/docs/install.md +18 -6
  4. package/docs/providers.md +147 -0
  5. package/package.json +8 -2
  6. package/src/apps/cli.ts +42 -14
  7. package/src/apps/commands.ts +32 -4
  8. package/src/apps/completions.ts +9 -6
  9. package/src/apps/export-cli.ts +24 -0
  10. package/src/apps/extension-ui.ts +330 -0
  11. package/src/apps/input.ts +16 -1
  12. package/src/apps/model-catalog.ts +42 -0
  13. package/src/apps/project-trust.ts +49 -0
  14. package/src/apps/render.ts +1 -0
  15. package/src/apps/resource-browser.ts +75 -0
  16. package/src/apps/resources.ts +25 -0
  17. package/src/apps/session-browser.ts +113 -0
  18. package/src/apps/session-picker.ts +55 -0
  19. package/src/apps/shell.ts +1177 -378
  20. package/src/apps/startup.ts +400 -268
  21. package/src/core/action-defaults.ts +306 -0
  22. package/src/core/attachment-input.ts +17 -0
  23. package/src/core/catalog-merge.ts +19 -0
  24. package/src/core/event-validation.ts +19 -2
  25. package/src/core/events.ts +45 -2
  26. package/src/core/export.ts +6 -2
  27. package/src/core/extension-flags.ts +14 -0
  28. package/src/core/images.ts +46 -0
  29. package/src/core/json-event.ts +2 -0
  30. package/src/core/keymap.ts +43 -119
  31. package/src/core/model-replay.ts +22 -0
  32. package/src/core/model-scope.ts +53 -0
  33. package/src/core/packages.ts +47 -0
  34. package/src/core/progress.ts +9 -0
  35. package/src/core/project.ts +21 -3
  36. package/src/core/prompts.ts +33 -5
  37. package/src/core/queue.ts +18 -0
  38. package/src/core/resource-selection.ts +36 -0
  39. package/src/{runtime → core}/resume.ts +15 -22
  40. package/src/core/session-archive.ts +4 -0
  41. package/src/core/session-browser.ts +82 -0
  42. package/src/core/session-entry.ts +56 -0
  43. package/src/core/session-position.ts +26 -0
  44. package/src/core/settings.ts +169 -154
  45. package/src/core/theme.ts +15 -6
  46. package/src/core/thinking.ts +26 -0
  47. package/src/core/tokens.ts +2 -0
  48. package/src/core/transcript.ts +15 -5
  49. package/src/runtime/auth.ts +116 -11
  50. package/src/runtime/catalog.ts +54 -151
  51. package/src/runtime/clipboard.ts +114 -0
  52. package/src/runtime/extension-loader.ts +37 -0
  53. package/src/runtime/extension-ui.ts +55 -0
  54. package/src/runtime/extensions.ts +273 -0
  55. package/src/runtime/images.ts +76 -0
  56. package/src/runtime/loop.ts +213 -83
  57. package/src/runtime/model-config.ts +232 -0
  58. package/src/runtime/model-data.LICENSE +27 -0
  59. package/src/runtime/model-data.ts +21 -0
  60. package/src/runtime/model-replay.ts +10 -0
  61. package/src/runtime/model-scope.ts +20 -0
  62. package/src/runtime/model-sources.ts +151 -0
  63. package/src/runtime/model.ts +74 -11
  64. package/src/runtime/models-fetch.ts +6 -104
  65. package/src/runtime/oauth.ts +4 -3
  66. package/src/runtime/openai-responses.ts +12 -4
  67. package/src/runtime/openai.ts +13 -4
  68. package/src/runtime/package-manager.ts +125 -0
  69. package/src/runtime/pi-provider.ts +235 -0
  70. package/src/runtime/progress.ts +26 -0
  71. package/src/runtime/provider-access.ts +21 -0
  72. package/src/runtime/provider-protocols.ts +29 -0
  73. package/src/runtime/provider-request.ts +73 -0
  74. package/src/runtime/provider.ts +26 -6
  75. package/src/runtime/rich-text.ts +36 -0
  76. package/src/runtime/tools.ts +41 -5
  77. package/src/store/command.ts +7 -1
  78. package/src/store/context-files.ts +1 -1
  79. package/src/store/log.ts +65 -0
  80. package/src/store/models.ts +28 -9
  81. package/src/store/observations.ts +4 -2
  82. package/src/store/packages.ts +98 -0
  83. package/src/store/recovery.ts +8 -0
  84. package/src/store/resource-index.ts +170 -0
  85. package/src/store/resources.ts +75 -142
  86. package/src/store/session-archive.ts +162 -0
  87. package/src/store/session-browser.ts +68 -0
  88. package/src/store/session-position.ts +27 -0
  89. package/src/store/sessions.ts +87 -66
  90. package/src/store/trust.ts +13 -0
  91. package/src/store/workspace.ts +6 -0
  92. package/src/tui/chat-viewport.ts +47 -0
  93. package/src/tui/editor.ts +62 -15
  94. package/src/tui/host.ts +50 -9
  95. package/src/tui/input.ts +8 -2
  96. package/src/tui/select.ts +24 -6
  97. package/src/tui/settings.ts +4 -0
  98. package/src/tui/system-theme.ts +700 -0
  99. package/src/tui/transcript.ts +63 -11
  100. package/src/tui/tree.ts +161 -95
  101. package/src/vendor/pi-rich/LICENSE.upstream +21 -0
  102. package/src/vendor/pi-rich/UPSTREAM.json +17 -0
  103. package/src/vendor/pi-rich/clipboard-command.ts +44 -0
  104. package/src/vendor/pi-rich/exif-orientation.ts +182 -0
  105. package/src/vendor/pi-rich/html.ts +51 -0
  106. package/src/vendor/pi-rich/image-convert.ts +49 -0
  107. package/src/vendor/pi-rich/image-resize-core.ts +164 -0
  108. package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
  109. package/src/vendor/pi-rich/image-resize.ts +123 -0
  110. package/src/vendor/pi-rich/mime.ts +116 -0
  111. package/src/vendor/pi-rich/photon.ts +139 -0
  112. package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
  113. package/src/vendor/pi-tui/UPSTREAM.json +50 -0
  114. package/src/vendor/pi-tui/VENDOR.md +7 -5
  115. package/src/vendor/pi-tui/autocomplete.ts +35 -10
  116. package/src/vendor/pi-tui/colors.ts +367 -0
  117. package/src/vendor/pi-tui/components/box.ts +8 -9
  118. package/src/vendor/pi-tui/components/editor.ts +4 -2
  119. package/src/vendor/pi-tui/components/markdown.ts +8 -2
  120. package/src/vendor/pi-tui/index.ts +31 -1
  121. package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
  122. package/src/vendor/pi-tui/native/clipboard.h +208 -0
  123. package/src/vendor/pi-tui/native/darwin/README.md +22 -0
  124. package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
  125. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  126. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  127. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
  128. package/src/vendor/pi-tui/native/linux/README.md +23 -0
  129. package/src/vendor/pi-tui/native/linux/build.sh +65 -0
  130. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  131. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  132. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
  133. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
  134. package/src/vendor/pi-tui/native/napi.h +91 -0
  135. package/src/vendor/pi-tui/native/win32/README.md +31 -0
  136. package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
  137. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  138. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  139. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
  140. package/src/vendor/pi-tui/native-platform.ts +2 -0
  141. package/src/vendor/pi-tui/oklab.ts +233 -0
  142. package/src/vendor/pi-tui/terminal-colors.ts +26 -8
  143. package/src/vendor/pi-tui/terminal-image.ts +43 -8
  144. package/src/vendor/pi-tui/terminal.ts +16 -9
  145. package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
  146. package/src/vendor/pi-tui/tui.ts +112 -75
  147. package/src/vendor/pi-tui/utils.ts +89 -49
  148. package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
@@ -0,0 +1,306 @@
1
+ /** Adapted from Pi keybindings at 02eed88 (MIT). Platform-neutral defaults; overrides share scoped ownership. */
2
+ export const ACTION_DEFAULTS = {
3
+ "editor.cursorUp": [
4
+ "up"
5
+ ],
6
+ "editor.cursorDown": [
7
+ "down"
8
+ ],
9
+ "tui.editor.historyPrevious": [],
10
+ "tui.editor.historyNext": [],
11
+ "editor.cursorLeft": [
12
+ "left",
13
+ "ctrl+b"
14
+ ],
15
+ "editor.cursorRight": [
16
+ "right",
17
+ "ctrl+f"
18
+ ],
19
+ "editor.cursorWordLeft": [
20
+ "alt+left",
21
+ "ctrl+left",
22
+ "alt+b"
23
+ ],
24
+ "editor.cursorWordRight": [
25
+ "alt+right",
26
+ "ctrl+right",
27
+ "alt+f"
28
+ ],
29
+ "editor.cursorLineStart": [
30
+ "home",
31
+ "ctrl+home",
32
+ "ctrl+a"
33
+ ],
34
+ "editor.cursorLineEnd": [
35
+ "end",
36
+ "ctrl+end",
37
+ "ctrl+e"
38
+ ],
39
+ "tui.editor.jumpForward": [
40
+ "ctrl+]"
41
+ ],
42
+ "tui.editor.jumpBackward": [
43
+ "ctrl+alt+]"
44
+ ],
45
+ "tui.editor.pageUp": [
46
+ "pageUp",
47
+ "ctrl+pageUp"
48
+ ],
49
+ "tui.editor.pageDown": [
50
+ "pageDown",
51
+ "ctrl+pageDown"
52
+ ],
53
+ "editor.deleteCharBackward": [
54
+ "backspace"
55
+ ],
56
+ "editor.deleteCharForward": [
57
+ "delete",
58
+ "ctrl+d"
59
+ ],
60
+ "editor.deleteWordBackward": [
61
+ "ctrl+w",
62
+ "alt+backspace"
63
+ ],
64
+ "editor.deleteWordForward": [
65
+ "alt+d",
66
+ "alt+delete"
67
+ ],
68
+ "editor.deleteToLineStart": [
69
+ "ctrl+u"
70
+ ],
71
+ "editor.deleteToLineEnd": [
72
+ "ctrl+k"
73
+ ],
74
+ "editor.yank": [
75
+ "ctrl+y"
76
+ ],
77
+ "tui.editor.yankPop": [
78
+ "alt+y"
79
+ ],
80
+ "editor.undo": [
81
+ "ctrl+-",
82
+ "ctrl+_"
83
+ ],
84
+ "editor.newLine": [
85
+ "shift+enter",
86
+ "ctrl+j"
87
+ ],
88
+ "editor.submit": [
89
+ "enter"
90
+ ],
91
+ "editor.complete": [
92
+ "tab"
93
+ ],
94
+ "tui.input.copy": [
95
+ "ctrl+c"
96
+ ],
97
+ "tui.select.up": [
98
+ "up"
99
+ ],
100
+ "tui.select.down": [
101
+ "down"
102
+ ],
103
+ "tui.select.pageUp": [
104
+ "pageUp"
105
+ ],
106
+ "tui.select.pageDown": [
107
+ "pageDown"
108
+ ],
109
+ "tui.select.confirm": [
110
+ "enter"
111
+ ],
112
+ "tui.select.cancel": [
113
+ "escape",
114
+ "ctrl+c"
115
+ ],
116
+ "tui.altScreen.pageUp": [
117
+ "pageUp"
118
+ ],
119
+ "tui.altScreen.pageDown": [
120
+ "pageDown"
121
+ ],
122
+ "tui.altScreen.halfPageUp": [],
123
+ "tui.altScreen.halfPageDown": [],
124
+ "tui.altScreen.lineUp": [],
125
+ "tui.altScreen.lineDown": [],
126
+ "tui.altScreen.previousPrompt": [
127
+ "ctrl+shift+up",
128
+ "ctrl+up"
129
+ ],
130
+ "tui.altScreen.nextPrompt": [
131
+ "ctrl+shift+down",
132
+ "ctrl+down"
133
+ ],
134
+ "tui.altScreen.search": [
135
+ "ctrl+shift+f"
136
+ ],
137
+ "tui.altScreen.searchNext": [
138
+ "enter",
139
+ "ctrl+g"
140
+ ],
141
+ "tui.altScreen.searchPrevious": [
142
+ "shift+enter",
143
+ "ctrl+shift+g"
144
+ ],
145
+ "tui.altScreen.searchClose": [
146
+ "escape"
147
+ ],
148
+ "tui.altScreen.top": [
149
+ "home"
150
+ ],
151
+ "tui.altScreen.bottom": [
152
+ "end"
153
+ ],
154
+ "app.interrupt": [
155
+ "escape"
156
+ ],
157
+ "app.clear": [
158
+ "ctrl+c"
159
+ ],
160
+ "app.exit": [
161
+ "ctrl+d"
162
+ ],
163
+ "app.suspend": [
164
+ "ctrl+z"
165
+ ],
166
+ "app.cycleThinking": [
167
+ "shift+tab"
168
+ ],
169
+ "app.saveDefault": [
170
+ "ctrl+s"
171
+ ],
172
+ "app.cycleModel": [
173
+ "ctrl+p"
174
+ ],
175
+ "app.cycleModelReverse": [
176
+ "shift+ctrl+p"
177
+ ],
178
+ "app.model": [
179
+ "ctrl+l"
180
+ ],
181
+ "app.toggleTools": [
182
+ "ctrl+o"
183
+ ],
184
+ "app.toggleThinking": [
185
+ "ctrl+t"
186
+ ],
187
+ "app.session.toggleNamedFilter": [
188
+ "ctrl+n"
189
+ ],
190
+ "app.externalEditor": [
191
+ "ctrl+g"
192
+ ],
193
+ "app.message.copy": [
194
+ "ctrl+x"
195
+ ],
196
+ "app.message.followUp": [
197
+ "alt+enter"
198
+ ],
199
+ "app.message.dequeue": [
200
+ "alt+up"
201
+ ],
202
+ "app.clipboard.pasteImage": [
203
+ "ctrl+v"
204
+ ],
205
+ "app.session.new": [],
206
+ "app.session.tree": [],
207
+ "app.session.fork": [],
208
+ "app.session.resume": [],
209
+ "app.tree.foldOrUp": [
210
+ "alt+left",
211
+ "ctrl+left"
212
+ ],
213
+ "app.tree.unfoldOrDown": [
214
+ "alt+right",
215
+ "ctrl+right"
216
+ ],
217
+ "app.tree.editLabel": [
218
+ "shift+l"
219
+ ],
220
+ "app.tree.toggleLabelTimestamp": [
221
+ "shift+t"
222
+ ],
223
+ "app.session.togglePath": [
224
+ "ctrl+p"
225
+ ],
226
+ "app.session.toggleSort": [
227
+ "ctrl+s"
228
+ ],
229
+ "app.session.rename": [
230
+ "ctrl+r"
231
+ ],
232
+ "app.session.delete": [
233
+ "ctrl+d"
234
+ ],
235
+ "app.session.deleteNoninvasive": [
236
+ "ctrl+backspace"
237
+ ],
238
+ "app.models.save": [
239
+ "ctrl+s"
240
+ ],
241
+ "app.models.enableAll": [
242
+ "ctrl+a"
243
+ ],
244
+ "app.models.clearAll": [
245
+ "ctrl+x"
246
+ ],
247
+ "app.models.toggleProvider": [
248
+ "ctrl+p"
249
+ ],
250
+ "app.models.reorderUp": [
251
+ "alt+up"
252
+ ],
253
+ "app.models.reorderDown": [
254
+ "alt+down"
255
+ ],
256
+ "app.tree.filter.default": [
257
+ "ctrl+d"
258
+ ],
259
+ "app.tree.filter.noTools": [
260
+ "ctrl+t"
261
+ ],
262
+ "app.tree.filter.userOnly": [
263
+ "ctrl+u"
264
+ ],
265
+ "app.tree.filter.labeledOnly": [
266
+ "ctrl+l"
267
+ ],
268
+ "app.tree.filter.all": [
269
+ "ctrl+a"
270
+ ],
271
+ "app.tree.filter.cycleForward": [
272
+ "ctrl+o"
273
+ ],
274
+ "app.tree.filter.cycleBackward": [
275
+ "shift+ctrl+o"
276
+ ]
277
+ } as const;
278
+ export const ACTION_ALIASES: Readonly<Record<string,string>> = {
279
+ "tui.input.submit": "editor.submit",
280
+ "tui.input.newLine": "editor.newLine",
281
+ "tui.input.tab": "editor.complete",
282
+ "app.model.select": "app.model",
283
+ "app.model.cycleForward": "app.cycleModel",
284
+ "app.model.cycleBackward": "app.cycleModelReverse",
285
+ "app.thinking.cycle": "app.cycleThinking",
286
+ "app.thinking.toggle": "app.toggleThinking",
287
+ "app.tools.expand": "app.toggleTools",
288
+ "app.editor.external": "app.externalEditor",
289
+ "app.thinking.save": "app.saveDefault",
290
+ "tui.editor.cursorUp": "editor.cursorUp",
291
+ "tui.editor.cursorDown": "editor.cursorDown",
292
+ "tui.editor.cursorLeft": "editor.cursorLeft",
293
+ "tui.editor.cursorRight": "editor.cursorRight",
294
+ "tui.editor.cursorWordLeft": "editor.cursorWordLeft",
295
+ "tui.editor.cursorWordRight": "editor.cursorWordRight",
296
+ "tui.editor.cursorLineStart": "editor.cursorLineStart",
297
+ "tui.editor.cursorLineEnd": "editor.cursorLineEnd",
298
+ "tui.editor.deleteCharBackward": "editor.deleteCharBackward",
299
+ "tui.editor.deleteCharForward": "editor.deleteCharForward",
300
+ "tui.editor.deleteWordBackward": "editor.deleteWordBackward",
301
+ "tui.editor.deleteWordForward": "editor.deleteWordForward",
302
+ "tui.editor.deleteToLineStart": "editor.deleteToLineStart",
303
+ "tui.editor.deleteToLineEnd": "editor.deleteToLineEnd",
304
+ "tui.editor.yank": "editor.yank",
305
+ "tui.editor.undo": "editor.undo"
306
+ };
@@ -0,0 +1,17 @@
1
+ /** Only an entire list of image paths is a drop; ordinary prose and shell syntax remain literal. */
2
+ export function droppedImagePaths(text: string): string[] | null {
3
+ const paths: string[] = [];
4
+ let token = "", quote = "", escaped = false;
5
+ for (const char of text.trim()) {
6
+ if (escaped) { token += char; escaped = false; }
7
+ else if (char === "\\" && quote !== "'") escaped = true;
8
+ else if (quote) { if (char === quote) quote = ""; else token += char; }
9
+ else if (char === "'" || char === '"') quote = char;
10
+ else if (/\s/.test(char)) { if (token) { paths.push(token); token = ""; } }
11
+ else token += char;
12
+ }
13
+ if (escaped || quote) return null;
14
+ if (token) paths.push(token);
15
+ return paths.length > 0 && paths.every(path => !/[\x00-\x1f]/.test(path) && !path.startsWith("@") &&
16
+ /\.(png|jpe?g|gif|webp|bmp)$/i.test(path) && !/[;$|<>`]/.test(path)) ? paths : null;
17
+ }
@@ -0,0 +1,19 @@
1
+ import { modelKey } from "./session-position.ts";
2
+
3
+ /** D13: identity and selection are independent of provider SDKs and source order. */
4
+ export function mergeCatalogs<T extends { provider: string; id: string }>(...layers: readonly (readonly T[])[]): T[] {
5
+ const models = new Map<string, T>();
6
+ for (const layer of layers) for (const model of layer) models.set(modelKey(model), model);
7
+ return [...models.values()];
8
+ }
9
+
10
+ /** Provider-local allow/deny patterns use literal characters plus '*' and '?'. */
11
+ export function matchesModelPattern(id: string, pattern: string): boolean {
12
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
13
+ return new RegExp(`^${escaped}$`, "u").test(id);
14
+ }
15
+
16
+ export function modelEnabled(id: string, allow?: readonly string[], deny?: readonly string[]): boolean {
17
+ return (allow === undefined || allow.some(pattern => matchesModelPattern(id, pattern)))
18
+ && !(deny?.some(pattern => matchesModelPattern(id, pattern)) ?? false);
19
+ }
@@ -2,6 +2,7 @@
2
2
  import type { LogEvent, NodeRef } from "./events.ts";
3
3
  import { isExecutionOutcome } from "./outcome.ts";
4
4
  import { isJsonValue, isToolDiagnostic } from "./tool-contract.ts";
5
+ import { validImageRef } from "./images.ts";
5
6
 
6
7
  export function isRecord(value: unknown): value is Record<string, unknown> {
7
8
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -23,11 +24,27 @@ export function isLogEvent(value: unknown): value is LogEvent {
23
24
  const carried = value["carried_from"];
24
25
  if (carried !== undefined && !(isRecord(carried) && natural(carried["node"]) && typeof carried["event"] === "string")) return false;
25
26
  const has = (...keys: string[]): boolean => keys.every((key) => typeof value[key] === "string");
27
+ const images = value["images"];
28
+ if (images !== undefined && (!Array.isArray(images) || !images.every(validImageRef))) return false;
26
29
  switch (value["t"]) {
30
+ case "extension": return has("customType", "content") && typeof value["display"] === "boolean" && typeof value["inContext"] === "boolean" && (value["data"] === undefined || isJsonValue(value["data"]));
27
31
  case "user": return has("text");
32
+ case "outcome": return has("reason") && isExecutionOutcome(value["outcome"]);
33
+ case "user_bash": {
34
+ const ref = value["content_ref"];
35
+ const outcome = value["outcome"];
36
+ if (!isExecutionOutcome(outcome) || !["tool_completed", "tool_failed", "cancelled", "timeout"].includes(outcome.reason)) return false;
37
+ if (outcome.reason === "tool_completed" && value["exit"] !== 0) return false;
38
+ if (outcome.reason === "tool_failed" && value["exit"] === 0) return false;
39
+ return has("command", "content") && Number.isInteger(value["exit"]) && typeof value["excludeFromContext"] === "boolean" &&
40
+ isRecord(ref) && typeof ref["sha256"] === "string" &&
41
+ typeof ref["path"] === "string" && natural(ref["bytes"]);
42
+ }
28
43
  case "guidance": return has("text") && ["continuation", "repetition", "budget", "boundary", "stall", "output_recovery", "completion_review", "churn", "deadline"].includes(String(value["reason"])) && strings(value["sources"]);
29
44
  case "interrupt": return has("user_id");
30
- case "assistant": return (value["stop_reason"] === null || optionalString(value["stop_reason"])) && Array.isArray(value["blocks"]) && value["blocks"].every((b: unknown) => {
45
+ case "assistant": return (value["stop_reason"] === null || optionalString(value["stop_reason"])) &&
46
+ (value["replay_key"] === undefined || typeof value["replay_key"] === "string" && /^[a-f0-9]{64}$/.test(value["replay_key"])) &&
47
+ Array.isArray(value["blocks"]) && value["blocks"].every((b: unknown) => {
31
48
  if (!isRecord(b)) return false;
32
49
  if (b["type"] === "text") return typeof b["text"] === "string";
33
50
  if (b["type"] === "thinking") return typeof b["text"] === "string" && "raw" in b;
@@ -54,7 +71,7 @@ export function isLogEvent(value: unknown): value is LogEvent {
54
71
  (effect["sha256_before"] === null || typeof effect["sha256_before"] === "string");
55
72
  }
56
73
  case "cut": return has("handoff_ref") && ["trigger", "hard_limit", "model_checkpoint"].includes(String(value["reason"])) &&
57
- optionalString(value["retained_from"]) &&
74
+ optionalString(value["retained_from"]) && optionalString(value["next_context_ref"]) &&
58
75
  (value["next_node"] === undefined || natural(value["next_node"])) && optionalString(value["next_model"]) &&
59
76
  (value["handoff_tokens"] === undefined || (typeof value["handoff_tokens"] === "number" && Number.isFinite(value["handoff_tokens"]) && value["handoff_tokens"] > 0)) &&
60
77
  (value["predecessor"] === null || validNodeRef(value["predecessor"]));
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import type { ExecutionOutcome } from "./outcome.ts";
22
+ import type { ImageRef } from "./images.ts";
22
23
  import type { JsonValue, ToolDiagnostic } from "./tool-contract.ts";
23
24
 
24
25
  export type EventId = string;
@@ -78,6 +79,29 @@ export interface UserEvent {
78
79
  id: EventId;
79
80
  at: IsoTime;
80
81
  text: string;
82
+ images?: readonly ImageRef[];
83
+ }
84
+
85
+ /** User-authorized shell execution is distinct from a model tool invocation (Pi !/!!). */
86
+ export interface UserBashEvent {
87
+ t: "user_bash";
88
+ id: EventId;
89
+ at: IsoTime;
90
+ command: string;
91
+ content: string;
92
+ exit: number;
93
+ excludeFromContext: boolean;
94
+ outcome: ExecutionOutcome;
95
+ content_ref: ArtifactRef;
96
+ }
97
+
98
+ /** A terminal execution fact; human history must not turn a truncated answer into success. */
99
+ export interface OutcomeEvent {
100
+ t: "outcome";
101
+ id: EventId;
102
+ at: IsoTime;
103
+ reason: string;
104
+ outcome: ExecutionOutcome;
81
105
  }
82
106
 
83
107
  /** A factual harness notice, appended once rather than rewriting the prompt (N.1). */
@@ -119,7 +143,7 @@ export interface InterruptEvent {
119
143
  * what a UI renders.
120
144
  */
121
145
  export type AssistantBlock =
122
- | { type: "text"; text: string }
146
+ | { type: "text"; text: string; wire?: unknown }
123
147
  | { type: "thinking"; text: string; raw: unknown }
124
148
  /**
125
149
  * `wire` is the provider's own per-call data beyond id, name and arguments
@@ -134,6 +158,8 @@ export interface AssistantEvent {
134
158
  blocks: readonly AssistantBlock[];
135
159
  /** N.12: provider-observed termination, never a claim of task completion. Legacy logs omit it. */
136
160
  stop_reason?: string | null;
161
+ /** D8: opaque digest of the wire profile that produced signed blocks; no credentials. */
162
+ replay_key?: string;
137
163
  }
138
164
 
139
165
  export interface ToolResultEvent {
@@ -144,6 +170,7 @@ export interface ToolResultEvent {
144
170
  ok: boolean;
145
171
  /** What the model sees. Large payloads belong on disk, referenced by effect. */
146
172
  content: string;
173
+ images?: readonly ImageRef[];
147
174
  /** Digest of the complete observation, even when it fits inline in the log. */
148
175
  content_sha256?: Sha256;
149
176
  /** O.3: semantic observation digest excludes presentation metadata. */
@@ -170,6 +197,12 @@ export interface CutEvent {
170
197
  /** Durable transition intent; recovery opens this exact successor. */
171
198
  next_node?: number;
172
199
  next_model?: string;
200
+ /** Paired with next_model so crash recovery preserves the selected provider. */
201
+ next_provider?: string;
202
+ /** The successor request effort is durable even if the adapter has not been installed. */
203
+ next_thinking?: string;
204
+ /** D6: resource reload activates this frozen context only in the successor. */
205
+ next_context_ref?: Sha256;
173
206
  /** V2 carried-state budget, fixed in the log for identical replay. */
174
207
  handoff_tokens?: number;
175
208
  /** N.8: first retained event on the effective ancestry; no copied transcript. */
@@ -439,8 +472,18 @@ export interface EvidenceLinkEvent {
439
472
  carried_from?: CarriedFrom;
440
473
  }
441
474
 
475
+ /** Extension state is append-only data; only explicit messages enter either projection (D1). */
476
+ export interface ExtensionEvent {
477
+ t: "extension"; id: string; at: string; customType: string;
478
+ content: string; display: boolean; inContext: boolean;
479
+ data?: import("./tool-contract.ts").JsonValue;
480
+ }
481
+
442
482
  export type LogEvent =
483
+ | ExtensionEvent
443
484
  | UserEvent
485
+ | UserBashEvent
486
+ | OutcomeEvent
444
487
  | GuidanceEvent
445
488
  | InterruptEvent
446
489
  | AssistantEvent
@@ -462,7 +505,7 @@ export type LogEvent =
462
505
  | WorkStatusEvent;
463
506
 
464
507
  export const HARNESS_EVENTS = [
465
- "user", "guidance", "interrupt", "assistant", "tool_result", "cut", "snapshot", "rewind",
508
+ "user", "user_bash", "outcome", "guidance", "interrupt", "assistant", "tool_result", "cut", "snapshot", "rewind", "extension",
466
509
  ] as const;
467
510
  export const MODEL_EVENTS = [
468
511
  "claim", "decision", "pitfall", "blocked", "question", "next_action", "work_status", "evidence_link",
@@ -13,6 +13,7 @@
13
13
 
14
14
  import type { AssistantBlock, LogEvent } from "./events.ts";
15
15
  import { assertNever } from "./exhaustive.ts";
16
+ import { imageDescription } from "./images.ts";
16
17
 
17
18
  export interface HtmlMeta {
18
19
  title: string;
@@ -90,8 +91,11 @@ function escapeHtml(s: string): string {
90
91
 
91
92
  function eventHtml(e: LogEvent): string {
92
93
  switch (e.t) {
94
+ case "extension": return e.display ? wrap("event", e.customType, e.at, e.content || JSON.stringify(e.data)) : "";
93
95
  case "user":
94
- return wrap("user", "user", e.at, e.text);
96
+ return wrap("user", "user", e.at, [e.text, ...(e.images ?? []).map(imageDescription)].join("\n"));
97
+ case "user_bash": return wrap("tool", e.excludeFromContext ? "user bash (excluded)" : "user bash", e.at, `$ ${e.command}\n${e.content}`);
98
+ case "outcome": return wrap("event", e.outcome.status, e.at, `${e.outcome.reason}: ${e.reason}`);
95
99
  case "guidance":
96
100
  return wrap("event", `harness ${e.reason}`, e.at, e.text);
97
101
  case "assistant":
@@ -99,7 +103,7 @@ function eventHtml(e: LogEvent): string {
99
103
  case "interrupt":
100
104
  return wrap("event", "interrupt", e.at, `cancelled ${e.user_id}`);
101
105
  case "tool_result":
102
- return wrap("tool", `tool_result ${e.ok ? "ok" : "fail"}`, e.at, e.content);
106
+ return wrap("tool", `tool_result ${e.ok ? "ok" : "fail"}`, e.at, [e.content, ...(e.images ?? []).map(imageDescription)].join("\n"));
103
107
  case "cut":
104
108
  return wrap("event", "cut", e.at, `${e.reason} → ${e.handoff_ref}`);
105
109
  case "snapshot":
@@ -0,0 +1,14 @@
1
+ /** Typed extension flags are data; factories and the final CLI pass share their interpretation. */
2
+ export interface ExtensionFlag { type: "string" | "boolean"; description?: string; default?: string | boolean }
3
+ export interface FlagInput { value?: string; inline: boolean }
4
+ export function extensionFlagValue(name: string, definition: ExtensionFlag, input?: FlagInput): string | boolean | undefined {
5
+ if (!input) return definition.default;
6
+ if (definition.type === "string") {
7
+ if (input.value === undefined) throw new Error(`--${name} requires a string value`);
8
+ return input.value;
9
+ }
10
+ if (input.value === "true") return true;
11
+ if (input.value === "false") return false;
12
+ if (input.inline) throw new Error(`--${name} requires true or false`);
13
+ return true;
14
+ }
@@ -0,0 +1,46 @@
1
+ /** Immutable attachment metadata; D8 stores bytes once and projects references without I/O. */
2
+ export interface ImageRef {
3
+ readonly sha256: string;
4
+ readonly mimeType: "image/png" | "image/jpeg" | "image/gif" | "image/webp";
5
+ readonly width: number;
6
+ readonly height: number;
7
+ readonly bytes: number;
8
+ readonly name: string;
9
+ /** Submission-time policy; enabling images later cannot rewrite an earlier request prefix. */
10
+ readonly blocked?: true;
11
+ readonly original?: { readonly sha256: string; readonly width: number; readonly height: number };
12
+ }
13
+ export interface ImageContent { readonly type: "image"; readonly image: ImageRef; readonly data?: string }
14
+ export type UserInput = string | { readonly text: string; readonly images?: readonly ImageRef[] };
15
+ export function validImageRef(value: unknown): value is ImageRef {
16
+ if (!value || typeof value !== "object") return false;
17
+ const v = value as Record<string, unknown>;
18
+ const digest = (s: unknown) => typeof s === "string" && /^[a-f0-9]{64}$/.test(s);
19
+ const positive = (n: unknown) => typeof n === "number" && Number.isSafeInteger(n) && n > 0;
20
+ if (v["blocked"] !== undefined && v["blocked"] !== true) return false;
21
+ if (!digest(v["sha256"]) || !positive(v["width"]) || !positive(v["height"]) || !positive(v["bytes"]) ||
22
+ typeof v["name"] !== "string" || !["image/png", "image/jpeg", "image/gif", "image/webp"].includes(String(v["mimeType"]))) return false;
23
+ if (v["original"] === undefined) return true;
24
+ if (!v["original"] || typeof v["original"] !== "object") return false;
25
+ const o = v["original"] as Record<string, unknown>;
26
+ return digest(o["sha256"]) && positive(o["width"]) && positive(o["height"]);
27
+ }
28
+ export function submissionImages(images: readonly ImageRef[], blocked: boolean): ImageRef[] {
29
+ return images.map(image => {
30
+ if (!validImageRef(image)) throw new Error("Invalid image reference");
31
+ const { sha256, mimeType, width, height, bytes, name, original } = image;
32
+ return { sha256, mimeType, width, height, bytes, name, ...(original ? { original } : {}), ...(blocked ? { blocked: true as const } : {}) };
33
+ });
34
+ }
35
+ export function userInputData(input: UserInput, blocked?: boolean): { text: string; images?: readonly ImageRef[] } {
36
+ return typeof input === "string" ? { text: input } : { text: input.text,
37
+ ...(input.images?.length ? { images: blocked === undefined ? input.images : submissionImages(input.images, blocked) } : {}) };
38
+ }
39
+ export function imageDescription(image: ImageRef): string {
40
+ const original = image.original;
41
+ return `[Image: ${image.name}, ${image.width}x${image.height}${original ? `; original ${original.width}x${original.height}, coordinate scale ${(original.width / image.width).toFixed(2)}` : ""}]`;
42
+ }
43
+ /** Assumption, not provider billing: 32px patches plus framing; anchors replace this after the response. */
44
+ export function estimateImageTokens(image: Pick<ImageRef, "width" | "height">): number {
45
+ return 256 + Math.ceil(image.width / 32) * Math.ceil(image.height / 32) * 2;
46
+ }
@@ -22,6 +22,7 @@ export type JsonLoopKind =
22
22
  | "text_delta"
23
23
  | "thinking_delta"
24
24
  | "discard"
25
+ | "progress"
25
26
  | "tool_call"
26
27
  | "tool_result"
27
28
  | "usage"
@@ -67,6 +68,7 @@ export function toJsonEvent(e: JsonLoopEvent, at: string): Record<string, unknow
67
68
  case "text_delta":
68
69
  case "thinking_delta":
69
70
  case "discard":
71
+ case "progress":
70
72
  return null;
71
73
  case "assistant_text":
72
74
  return { type: "assistant_text", text: e.text ?? "", at };