@timurproko/a1 0.2.6-dev.727 → 0.2.6-dev.734

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 (104) hide show
  1. package/dist/app/session-shell/clipboard-services.d.ts +19 -0
  2. package/dist/app/session-shell/clipboard-services.js +7 -0
  3. package/dist/app/session-shell/exit-notice.d.ts +9 -5
  4. package/dist/app/session-shell/exit-notice.js +5 -5
  5. package/dist/app/session-shell/index.d.ts +3 -1
  6. package/dist/app/session-shell/index.js +1 -0
  7. package/dist/app/session-shell/paste-preparation-client.d.ts +3 -0
  8. package/dist/app/session-shell/paste-preparation-client.js +5 -2
  9. package/dist/app/session-shell/path-chip-presentation.d.ts +10 -3
  10. package/dist/app/session-shell/path-chip-presentation.js +48 -6
  11. package/dist/app/session-shell/prompt-chips.js +30 -4
  12. package/dist/app/session-shell/quit-outro.d.ts +3 -1
  13. package/dist/app/session-shell/quit-outro.js +23 -8
  14. package/dist/app/session-shell/response-copy-transport.d.ts +6 -1
  15. package/dist/app/session-shell/response-copy-transport.js +8 -2
  16. package/dist/app/session-shell/session-shell-root.d.ts +20 -10
  17. package/dist/app/session-shell/session-shell-root.js +6 -6
  18. package/dist/app/session-shell/session-shell.d.ts +19 -21
  19. package/dist/app/session-shell/session-shell.js +656 -860
  20. package/dist/composition/owned-ui.d.ts +20 -2
  21. package/dist/composition/owned-ui.js +66 -35
  22. package/dist/composition/session-info-reference.d.ts +2 -2
  23. package/dist/composition/session-info-reference.js +2 -2
  24. package/dist/contracts/agent-engine/capability-ports.d.ts +1 -85
  25. package/dist/contracts/agent-engine/capability-ports.js +1 -35
  26. package/dist/contracts/agent-engine/index.d.ts +1 -7
  27. package/dist/contracts/agent-engine/index.js +0 -4
  28. package/dist/contracts/owned-ui/index.d.ts +5 -1
  29. package/dist/contracts/owned-ui/index.js +1 -0
  30. package/dist/contracts/owned-ui/model.d.ts +6 -0
  31. package/dist/contracts/owned-ui/session-backend.d.ts +119 -0
  32. package/dist/contracts/owned-ui/session-presenters.d.ts +55 -0
  33. package/dist/contracts/owned-ui/session-presenters.js +1 -0
  34. package/dist/contracts/owned-ui/session-workflows.d.ts +342 -0
  35. package/dist/contracts/owned-ui/session-workflows.js +30 -0
  36. package/dist/features/owned-ui/settings-app.js +35 -15
  37. package/dist/foundation/release/bootstrap.js +1 -2
  38. package/dist/foundation/release/release-gc.js +1 -1
  39. package/dist/foundation/release/update.js +0 -4
  40. package/dist/foundation/storage/control-store.js +0 -12
  41. package/dist/integrations/pi/components/components.js +0 -16
  42. package/dist/integrations/pi/components/index.d.ts +3 -3
  43. package/dist/integrations/pi/components/index.js +2 -2
  44. package/dist/integrations/pi/components/models-dialog.js +1 -30
  45. package/dist/integrations/pi/components/shell-editor-autocomplete.js +11 -9
  46. package/dist/integrations/pi/components/shell-extension-ui.d.ts +3 -3
  47. package/dist/integrations/pi/components/shell-extension-ui.js +6 -2
  48. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -4
  49. package/dist/integrations/pi/components/shell-shared-facade.d.ts +21 -1
  50. package/dist/integrations/pi/components/shell-shared-facade.js +18 -2
  51. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +5 -1
  52. package/dist/integrations/pi/components/upstream/theme/theme.js +20 -2
  53. package/dist/integrations/pi/engine/adapter.d.ts +44 -24
  54. package/dist/integrations/pi/engine/adapter.js +130 -22
  55. package/dist/integrations/pi/engine/command-dispatch.d.ts +2 -5
  56. package/dist/integrations/pi/engine/extension-ui-binding.d.ts +2 -11
  57. package/dist/integrations/pi/engine/host-ports.d.ts +20 -0
  58. package/dist/integrations/pi/engine/host-ports.js +13 -0
  59. package/dist/integrations/pi/engine/host.d.ts +70 -0
  60. package/dist/integrations/pi/engine/host.js +147 -0
  61. package/dist/integrations/pi/engine/index.d.ts +8 -4
  62. package/dist/integrations/pi/engine/index.js +3 -1
  63. package/dist/integrations/pi/engine/resource-catalog.d.ts +4 -23
  64. package/dist/integrations/pi/engine/runtime-integration.d.ts +8 -2
  65. package/dist/integrations/pi/engine/runtime-integration.js +12 -7
  66. package/dist/integrations/pi/engine/session-integration.d.ts +3 -8
  67. package/dist/integrations/pi/engine/session-integration.js +0 -49
  68. package/dist/integrations/pi/engine/session-runtime.d.ts +18 -3
  69. package/dist/integrations/pi/engine/session-runtime.js +18 -7
  70. package/dist/integrations/pi/engine/settings-bridge.d.ts +2 -2
  71. package/dist/integrations/pi/engine/settings-bridge.js +0 -2
  72. package/dist/integrations/pi/engine/settings-port.d.ts +2 -0
  73. package/dist/integrations/pi/engine/settings-port.js +3 -5
  74. package/dist/integrations/pi/engine/workflows.d.ts +33 -260
  75. package/dist/integrations/pi/engine/workflows.js +6 -28
  76. package/dist/integrations/pi/session-presenters/index.d.ts +2 -0
  77. package/dist/integrations/pi/session-presenters/index.js +1 -0
  78. package/dist/integrations/pi/session-presenters/presenters.d.ts +16 -0
  79. package/dist/integrations/pi/session-presenters/presenters.js +341 -0
  80. package/dist/integrations/pi/tui-runtime/adapter.js +0 -2
  81. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
  82. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +66 -1
  83. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -1
  84. package/dist/integrations/pi/tui-runtime/index.js +1 -1
  85. package/dist/native/darwin-arm64/manifest.json +1 -1
  86. package/dist/native/linux-x64/manifest.json +1 -1
  87. package/dist/native/win32-x64/manifest.json +2 -2
  88. package/dist/native/win32-x64/process-guardian.exe +0 -0
  89. package/dist/ui/settings/declarations.d.ts +13 -1
  90. package/dist/ui/settings/declarations.js +12 -1
  91. package/dist/ui/settings/migrations.js +7 -0
  92. package/docs/architecture/boundaries.md +9 -1
  93. package/docs/architecture/project-structure.md +3 -2
  94. package/package.json +1 -1
  95. package/dist/contracts/agent-engine/model.d.ts +0 -78
  96. package/dist/contracts/agent-engine/model.js +0 -1
  97. package/dist/contracts/agent-engine/ports.d.ts +0 -18
  98. package/dist/contracts/agent-engine/serialization.d.ts +0 -7
  99. package/dist/contracts/agent-engine/serialization.js +0 -58
  100. package/dist/contracts/agent-engine/validation.d.ts +0 -5
  101. package/dist/contracts/agent-engine/validation.js +0 -128
  102. package/dist/integrations/pi/components/upstream/theme/system-theme.d.ts +0 -61
  103. package/dist/integrations/pi/components/upstream/theme/system-theme.js +0 -709
  104. /package/dist/contracts/{agent-engine/ports.js → owned-ui/session-backend.js} +0 -0
@@ -0,0 +1,341 @@
1
+ import { createPiShellModelSelector, createPiShellModelsDialog, createPiShellScopedModelsSelector, createPiShellSessionSelector, createPiShellSettingsSelector, } from "../components/shell-selectors-dialogs.js";
2
+ const MODEL_REFRESH_TIMEOUT_MS = 15_000;
3
+ const MODEL_REFRESH_TIMED_OUT = "Model refresh timed out; showing cached models.";
4
+ /**
5
+ * Builds the selectors and transcript renderers whose inputs are Pi engine objects. This owner is the one place
6
+ * that sees both the engine adapter and the Pi components, so the shell only ever holds the neutral port.
7
+ */
8
+ export function createPiSessionPresenters(adapter, options = {}) {
9
+ const source = () => adapter.presentationSource();
10
+ let lazySelectors = options.lazySelectors;
11
+ let lazySelectorsPromise;
12
+ // Performance: the tree selector module stays out of the startup graph and loads on first use.
13
+ const loadLazySelectors = () => {
14
+ if (lazySelectors !== undefined)
15
+ return Promise.resolve(lazySelectors);
16
+ return lazySelectorsPromise ??= import("../components/lazy-selectors.js").then(module => {
17
+ lazySelectors = module.piShellLazySelectors;
18
+ return module.piShellLazySelectors;
19
+ });
20
+ };
21
+ const renderers = {
22
+ getMessageRenderer: customType => opaqueRenderer(source().pinnedMessageRenderer(customType)),
23
+ getToolRenderers: toolName => opaqueRenderer(source().pinnedToolRenderers(toolName)),
24
+ // Compatibility: the component adapter passes Pi's own keybinding config through the neutral port.
25
+ getShortcuts: bindings => source().pinnedShortcutDescriptions(bindings),
26
+ };
27
+ return {
28
+ transcriptRenderers: () => renderers,
29
+ openModelSelector(host, initialSearchInput) {
30
+ const context = source().pinnedModelSelectorContext();
31
+ const close = closeSurface(host);
32
+ const component = createPiShellModelSelector({
33
+ ...context,
34
+ runtime: {
35
+ getColumns: () => host.viewport().columns,
36
+ getRows: () => host.viewport().rows,
37
+ requestRender: () => host.requestRender(),
38
+ },
39
+ ...(initialSearchInput === undefined ? {} : { initialSearchInput }),
40
+ onSelect: model => {
41
+ close();
42
+ void host.runWorkflow({ command: "model", argument: "", selection: modelReference(model) });
43
+ },
44
+ onSelectAsDefault: model => {
45
+ close();
46
+ void host.runWorkflow({ command: "model", argument: "", selection: modelReference(model), persist: true });
47
+ },
48
+ onCancel: close,
49
+ });
50
+ host.setInputSurface(component);
51
+ host.requestRender();
52
+ },
53
+ async openSessionSelector(host, onExit) {
54
+ const context = source().pinnedSessionSelectorContext();
55
+ const close = closeSurface(host);
56
+ const component = await createPiShellSessionSelector({
57
+ currentSessionsLoader: context.loadCurrentSessions,
58
+ allSessionsLoader: context.loadAllSessions,
59
+ currentSessionFilePath: context.currentSessionFilePath,
60
+ renameSession: context.renameSession,
61
+ requestRender: () => host.requestRender(),
62
+ onSelect: sessionPath => {
63
+ close();
64
+ void host.runWorkflow({ command: "resume", argument: sessionPath });
65
+ },
66
+ onCancel: close,
67
+ onExit: () => {
68
+ close();
69
+ onExit();
70
+ },
71
+ });
72
+ host.setInputSurface(component);
73
+ host.requestRender();
74
+ },
75
+ async openTreeSelector(host, navigate, initialSelectedId) {
76
+ const context = source().pinnedTreeSelectorContext();
77
+ if (context.tree.length === 0) {
78
+ host.appendWorkflowStatus("No entries in session");
79
+ host.requestRender();
80
+ return;
81
+ }
82
+ const close = closeSurface(host);
83
+ const releasePresentation = host.beginPresentationHold?.();
84
+ try {
85
+ const selectors = await loadLazySelectors();
86
+ const component = await selectors.createTree({
87
+ tree: context.tree,
88
+ currentLeafId: context.currentLeafId,
89
+ terminalHeight: host.viewport().rows,
90
+ initialFilterMode: context.filterMode,
91
+ ...(initialSelectedId === undefined ? {} : { initialSelectedId }),
92
+ onLabelChange: context.appendLabelChange,
93
+ onCopy: text => {
94
+ if (!text) {
95
+ host.appendWorkflowResult({ command: "tree", outcome: "failed", message: "Selected entry has no text to copy" });
96
+ host.requestRender();
97
+ return;
98
+ }
99
+ const generation = adapter.identity.sessionBindingGeneration;
100
+ const stale = () => host.disposed || generation !== adapter.identity.sessionBindingGeneration;
101
+ void adapter.workflows.copyWorkflowText(text).then(acknowledged => {
102
+ if (stale())
103
+ return;
104
+ host.appendWorkflowStatus(acknowledged ? "Copied selected message to clipboard" : "Submitted selected message to clipboard");
105
+ host.requestRender();
106
+ }).catch(error => {
107
+ if (stale())
108
+ return;
109
+ host.appendWorkflowResult({ command: "tree", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
110
+ host.requestRender();
111
+ });
112
+ },
113
+ onCancel: close,
114
+ onSelect: entryId => {
115
+ if (entryId === context.currentLeafId) {
116
+ close();
117
+ host.appendWorkflowStatus("Already at this point");
118
+ host.requestRender();
119
+ return;
120
+ }
121
+ if (context.skipSummaryPrompt)
122
+ close();
123
+ navigate(entryId, context.skipSummaryPrompt);
124
+ },
125
+ });
126
+ if (host.disposed)
127
+ return;
128
+ host.setInputSurface(component);
129
+ host.requestRender();
130
+ }
131
+ finally {
132
+ releasePresentation?.();
133
+ }
134
+ },
135
+ openSettingsSelector(host) {
136
+ const snapshot = adapter.settings.snapshot();
137
+ const { currentModel, availableDefaultModels } = source().pinnedSettingsModels();
138
+ const close = closeSurface(host);
139
+ const component = createPiShellSettingsSelector({
140
+ config: {
141
+ ...snapshot,
142
+ availableThinkingLevels: [...snapshot.availableThinkingLevels],
143
+ availableThemes: [...snapshot.availableThemes],
144
+ warnings: { ...snapshot.warnings },
145
+ modelThinkingLevels: { ...snapshot.modelThinkingLevels },
146
+ ...(currentModel === undefined ? {} : { currentModel: currentModel }),
147
+ availableDefaultModels: availableDefaultModels,
148
+ },
149
+ onChange: (callback, value) => {
150
+ if (callback === "onCancel") {
151
+ close();
152
+ return;
153
+ }
154
+ if (callback === "onTuiModeChange") {
155
+ // Invariant: a surface that cannot change TUI mode (bare A1 is permanently fullscreen) ignores this callback.
156
+ if (host.switchTuiMode === undefined)
157
+ return;
158
+ if (value !== "regular" && value !== "fullscreen")
159
+ return;
160
+ if (!host.switchTuiMode(value)) {
161
+ host.appendWorkflowStatus("Close active overlays before changing TUI mode");
162
+ host.requestRender();
163
+ return;
164
+ }
165
+ }
166
+ void source().applyPinnedSettingValue(callback, value).then(result => {
167
+ if (result.outcome === "failed")
168
+ host.appendWorkflowResult(result);
169
+ else if (callback === "onTuiModeChange")
170
+ host.appendWorkflowStatus(`TUI mode: ${value}`);
171
+ host.requestRender();
172
+ });
173
+ },
174
+ onCancel: close,
175
+ });
176
+ host.setInputSurface(component);
177
+ host.requestRender();
178
+ },
179
+ /** The bare-A1 unified Models dialog: switch on Enter, scope on Space, persist on Ctrl+S, all through the engine. */
180
+ openModelsDialog(host, initialQuery) {
181
+ const catalog = adapter.catalog;
182
+ const context = catalog.modelsContext();
183
+ const available = new Set(context.models.map(model => `${model.provider}/${model.id}`));
184
+ const savedScopeIds = context.persistedScopeIds.filter(id => available.has(id));
185
+ // Invariant: an explicit session scope wins; otherwise the dialog starts from what is persisted, never from "all rows scoped".
186
+ const scopeIds = context.sessionScopeIds.length > 0 ? context.sessionScopeIds : savedScopeIds;
187
+ const refresh = new ModelCatalogRefresh();
188
+ const close = () => {
189
+ refresh.stop();
190
+ host.setInputSurface(null);
191
+ host.requestRender();
192
+ };
193
+ const dialog = createPiShellModelsDialog({
194
+ models: context.models,
195
+ activeModelId: context.activeModelId,
196
+ scopeIds,
197
+ savedScopeIds,
198
+ ...(initialQuery === undefined ? {} : { initialQuery }),
199
+ refreshStatus: "Refreshing model catalogs…",
200
+ requestRender: () => host.requestRender(),
201
+ onSelect: modelId => {
202
+ void host.runWorkflow({ command: "models", argument: "", selection: modelId }).then(result => {
203
+ if (!refresh.stopped && result.outcome === "completed")
204
+ close();
205
+ });
206
+ },
207
+ onScopeChange: ids => {
208
+ catalog.setSessionModelScope(ids);
209
+ host.requestRender();
210
+ },
211
+ onSave: ids => {
212
+ try {
213
+ catalog.persistModelScope(ids);
214
+ }
215
+ catch (error) {
216
+ host.appendWorkflowResult({ command: "models", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
217
+ host.requestRender();
218
+ throw error;
219
+ }
220
+ host.appendWorkflowStatus("Model selection saved to settings");
221
+ host.requestRender();
222
+ },
223
+ onCancel: close,
224
+ });
225
+ const component = {
226
+ ...dialog,
227
+ dispose: () => {
228
+ refresh.stop();
229
+ dialog.dispose?.();
230
+ },
231
+ };
232
+ host.setInputSurface(component);
233
+ host.requestRender();
234
+ refresh.run(host, component, signal => catalog.refreshModels(signal), refreshed => component.updateModels(refreshed.models));
235
+ },
236
+ openScopedModelsSelector(host) {
237
+ const catalog = adapter.catalog;
238
+ const initial = catalog.pinnedScopedModelsContext();
239
+ let currentEnabledIds = initial.enabledModelIds === null ? null : [...initial.enabledModelIds];
240
+ let selectionChanged = false;
241
+ const refresh = new ModelCatalogRefresh();
242
+ const close = () => {
243
+ refresh.stop();
244
+ host.setInputSurface(null);
245
+ host.requestRender();
246
+ };
247
+ const selector = createPiShellScopedModelsSelector({
248
+ models: initial.models,
249
+ enabledModelIds: currentEnabledIds,
250
+ refreshStatus: "Refreshing model catalogs…",
251
+ onChange: enabledIds => {
252
+ selectionChanged = true;
253
+ currentEnabledIds = enabledIds === null ? null : [...enabledIds];
254
+ catalog.updateScopedModels(currentEnabledIds);
255
+ host.requestRender();
256
+ },
257
+ onPersist: enabledIds => {
258
+ currentEnabledIds = enabledIds === null ? null : [...enabledIds];
259
+ catalog.persistScopedModels(currentEnabledIds);
260
+ host.appendWorkflowStatus("Model selection saved to settings");
261
+ host.requestRender();
262
+ },
263
+ onCancel: close,
264
+ });
265
+ const component = {
266
+ ...selector,
267
+ dispose: () => {
268
+ refresh.stop();
269
+ selector.dispose?.();
270
+ },
271
+ };
272
+ host.setInputSurface(component);
273
+ host.requestRender();
274
+ refresh.run(host, component, signal => catalog.refreshScopedModels(signal), refreshed => {
275
+ if (!selectionChanged) {
276
+ currentEnabledIds = refreshed.enabledModelIds === null ? null : [...refreshed.enabledModelIds];
277
+ component.updateModels(refreshed.models, currentEnabledIds);
278
+ }
279
+ else {
280
+ component.updateModels(refreshed.models);
281
+ catalog.updateScopedModels(currentEnabledIds);
282
+ }
283
+ });
284
+ },
285
+ };
286
+ }
287
+ /**
288
+ * One model-catalog refresh owned by an open dialog: it aborts after fifteen seconds or when the dialog
289
+ * closes, and a result that arrives after the dialog closed is dropped.
290
+ */
291
+ class ModelCatalogRefresh {
292
+ #controller = new AbortController();
293
+ #timeout;
294
+ #stopped = false;
295
+ #timedOut = false;
296
+ constructor() {
297
+ this.#timeout = setTimeout(() => {
298
+ this.#timedOut = true;
299
+ this.#controller.abort();
300
+ }, MODEL_REFRESH_TIMEOUT_MS);
301
+ }
302
+ get stopped() { return this.#stopped; }
303
+ stop() {
304
+ this.#stopped = true;
305
+ clearTimeout(this.#timeout);
306
+ this.#controller.abort();
307
+ }
308
+ run(host, dialog, refresh, apply) {
309
+ void refresh(this.#controller.signal).then(refreshed => {
310
+ if (this.#stopped)
311
+ return;
312
+ apply(refreshed);
313
+ dialog.setRefreshStatus(this.#timedOut ? MODEL_REFRESH_TIMED_OUT : refreshed.status, this.#timedOut ? "warning" : refreshed.statusKind);
314
+ host.requestRender();
315
+ }).catch(error => {
316
+ if (this.#stopped)
317
+ return;
318
+ dialog.setRefreshStatus(this.#timedOut
319
+ ? MODEL_REFRESH_TIMED_OUT
320
+ : `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`, "warning");
321
+ host.requestRender();
322
+ }).finally(() => clearTimeout(this.#timeout));
323
+ }
324
+ }
325
+ function closeSurface(host) {
326
+ return () => {
327
+ host.setInputSurface(null);
328
+ host.requestRender();
329
+ };
330
+ }
331
+ function opaqueRenderer(value) {
332
+ return (typeof value === "object" && value !== null) || typeof value === "function" ? value : undefined;
333
+ }
334
+ function modelReference(model) {
335
+ if (typeof model !== "object" || model === null)
336
+ return "";
337
+ const value = model;
338
+ const provider = typeof value.provider === "string" ? value.provider : "";
339
+ const id = typeof value.id === "string" ? value.id : typeof value.modelId === "string" ? value.modelId : "";
340
+ return provider && id ? `${provider}/${id}` : "";
341
+ }
@@ -95,7 +95,6 @@ export class PiTuiRuntimeAdapter {
95
95
  #terminal;
96
96
  #tuiTerminal;
97
97
  #tui;
98
- #root;
99
98
  #layoutRoot;
100
99
  #tuiOptions;
101
100
  #logDirectory;
@@ -120,7 +119,6 @@ export class PiTuiRuntimeAdapter {
120
119
  #presentationFrozen = false;
121
120
  #presentationHolds = new Set();
122
121
  constructor(options) {
123
- this.#root = options.root;
124
122
  this.#overlayGeometry = options.onOverlayGeometry === undefined ? undefined : new OverlayGeometryTracker(options.onOverlayGeometry);
125
123
  this.#terminal = options.terminal ?? new ProcessTerminal();
126
124
  this.#consumeUnhandledMouse = options.consumeUnhandledMouse ?? false;
@@ -63,6 +63,9 @@ export declare class DamageAwareTerminalAdapter implements PiTuiTerminalPort {
63
63
  get kittyProtocolActive(): boolean;
64
64
  get lastDecision(): PiTuiDamageDecision;
65
65
  get hyperlinkCleanupPending(): boolean;
66
+ get canvasBackgroundAnsi(): string | null;
67
+ /** Invalidates remembered terminal cells before the shell forces one complete canvas repaint. */
68
+ setCanvasBackground(ansi: string | null): boolean;
66
69
  /**
67
70
  * The rows as last forwarded to the terminal, top to bottom, with styling
68
71
  * intact. A row the adapter has not seen since its last invalidation is empty.
@@ -86,3 +89,7 @@ export declare class DamageAwareTerminalAdapter implements PiTuiTerminalPort {
86
89
  setTitle(title: string): void;
87
90
  setProgress(active: boolean): void;
88
91
  }
92
+ /** Makes SGR background resets return to A1's canvas while preserving explicit backgrounds. */
93
+ export declare function rebaseTerminalDefaultBackground(content: string, backgroundAnsi: string): string;
94
+ /** Paints only synchronized fullscreen rows; clipboard/title/progress controls remain byte-identical. */
95
+ export declare function paintTerminalCanvasFrame(data: string, backgroundAnsi: string): string;
@@ -8,6 +8,7 @@ const ROW_MARKER = /\u001b\[(\d+);1H\u001b\[2K/gu;
8
8
  const ROW_CONTENT_CURSOR = /\u001b\[\d+;1H/u;
9
9
  const CURSOR_SUFFIX = /(?:\u001b\[(\d+);(\d+)H)?\u001b\[\?25[hl]\u001b\[\?2026l$/u;
10
10
  const OUT_OF_BAND = /^(?:\u001b\](?:0|2|9|52);[^\u0007\u001b]*(?:\u0007|\u001b\\)|\u001b\[\?25[hl])+$/u;
11
+ const SGR_SEQUENCE = /\u001b\[([0-9:;]*)m/gu;
11
12
  /**
12
13
  * A fail-closed adapter for one pinned Pi fullscreen write grammar. Semantic
13
14
  * viewport metadata authorizes a shift; bytes are inspected only to validate
@@ -26,6 +27,7 @@ export class DamageAwareTerminalAdapter {
26
27
  #lastConsumedFrameId = 0;
27
28
  #cacheWidth = 0;
28
29
  #cacheHeight = 0;
30
+ #canvasBackgroundAnsi = null;
29
31
  #decision = {
30
32
  frameId: null,
31
33
  transformed: false,
@@ -41,6 +43,15 @@ export class DamageAwareTerminalAdapter {
41
43
  get kittyProtocolActive() { return this.inner.kittyProtocolActive; }
42
44
  get lastDecision() { return this.#decision; }
43
45
  get hyperlinkCleanupPending() { return this.#cleanupRevision > this.#cleanedRevision; }
46
+ get canvasBackgroundAnsi() { return this.#canvasBackgroundAnsi; }
47
+ /** Invalidates remembered terminal cells before the shell forces one complete canvas repaint. */
48
+ setCanvasBackground(ansi) {
49
+ if (this.#canvasBackgroundAnsi === ansi)
50
+ return false;
51
+ this.#canvasBackgroundAnsi = ansi;
52
+ this.#invalidatePresentation();
53
+ return true;
54
+ }
44
55
  /**
45
56
  * The rows as last forwarded to the terminal, top to bottom, with styling
46
57
  * intact. A row the adapter has not seen since its last invalidation is empty.
@@ -140,7 +151,10 @@ export class DamageAwareTerminalAdapter {
140
151
  output = buildDamageWrite(parsed, armed.descriptor, new Set(decision.paintedRows), this.#rows);
141
152
  }
142
153
  }
143
- this.inner.write(output);
154
+ const forwarded = this.#canvasBackgroundAnsi === null
155
+ ? output
156
+ : paintTerminalCanvasFrame(output, this.#canvasBackgroundAnsi);
157
+ this.inner.write(forwarded);
144
158
  if (epoch !== this.#epoch
145
159
  || (armed !== undefined && this.#lastConsumedFrameId !== armed.descriptor.frameId))
146
160
  return;
@@ -363,6 +377,57 @@ export class DamageAwareTerminalAdapter {
363
377
  this.#cacheHeight = 0;
364
378
  }
365
379
  }
380
+ /** Makes SGR background resets return to A1's canvas while preserving explicit backgrounds. */
381
+ export function rebaseTerminalDefaultBackground(content, backgroundAnsi) {
382
+ return content.replace(SGR_SEQUENCE, (sequence, parameters) => (sgrLeavesDefaultBackground(parameters) ? `${sequence}${backgroundAnsi}` : sequence));
383
+ }
384
+ /** Paints only synchronized fullscreen rows; clipboard/title/progress controls remain byte-identical. */
385
+ export function paintTerminalCanvasFrame(data, backgroundAnsi) {
386
+ if (!data.startsWith(BEGIN_SYNCHRONIZED_OUTPUT) || !data.endsWith(END_SYNCHRONIZED_OUTPUT)
387
+ || !ROW_MARKER.test(data)) {
388
+ ROW_MARKER.lastIndex = 0;
389
+ return data;
390
+ }
391
+ ROW_MARKER.lastIndex = 0;
392
+ const rows = data.replace(ROW_MARKER, (_marker, row) => (`\u001b[${row};1H${backgroundAnsi}\u001b[2K${backgroundAnsi}`));
393
+ ROW_MARKER.lastIndex = 0;
394
+ const rebased = rebaseTerminalDefaultBackground(rows, backgroundAnsi);
395
+ return `${rebased.slice(0, -END_SYNCHRONIZED_OUTPUT.length)}\u001b[49m${END_SYNCHRONIZED_OUTPUT}`;
396
+ }
397
+ function sgrLeavesDefaultBackground(parameters) {
398
+ if (parameters.length === 0)
399
+ return true;
400
+ const values = parameters.split(";");
401
+ let defaultBackground;
402
+ for (let index = 0; index < values.length; index += 1) {
403
+ const token = values[index];
404
+ const code = Number.parseInt(token.split(":", 1)[0] || "0", 10);
405
+ if (!Number.isFinite(code))
406
+ continue;
407
+ if (code === 0 || code === 49) {
408
+ defaultBackground = true;
409
+ continue;
410
+ }
411
+ if (code >= 40 && code <= 47 || code >= 100 && code <= 107) {
412
+ defaultBackground = false;
413
+ continue;
414
+ }
415
+ if (code !== 38 && code !== 48 && code !== 58)
416
+ continue;
417
+ if (code === 48)
418
+ defaultBackground = false;
419
+ if (token.includes(":"))
420
+ continue;
421
+ const mode = Number.parseInt(values[index + 1] ?? "", 10);
422
+ if (mode === 5)
423
+ index += 2;
424
+ else if (mode === 2)
425
+ index += 4;
426
+ else
427
+ index += 1;
428
+ }
429
+ return defaultBackground === true;
430
+ }
366
431
  function parsePinnedFullscreenWrite(data) {
367
432
  if (!data.startsWith(BEGIN_SYNCHRONIZED_OUTPUT) || !data.endsWith(END_SYNCHRONIZED_OUTPUT))
368
433
  return null;
@@ -1,6 +1,6 @@
1
1
  export type { PiTuiComponentPort, PiTuiInputDiagnosticsEvent, PiTuiInputDiagnosticsPhase, PiTuiInputListener, PiTuiInputListenerResult, PiTuiLayoutEntry, PiTuiLayoutNode, PiTuiOverlayAnchor, PiTuiOverlayHandle, PiTuiOverlayMargin, PiTuiOverlayOptions, PiTuiOverlayUnfocusOptions, PiTuiPointerSurface, PiTuiPreInputListener, PiTuiRuntimeAdapterOptions, PiTuiRuntimeState, PiTuiScrollState, PiTuiSizeValue, PiTuiStopOptions, PiTuiTerminalPort, PiTuiViewport, } from "./contracts.js";
2
2
  export { samePointerSurfaces } from "./overlay-geometry.js";
3
- export { DamageAwareTerminalAdapter, PINNED_PI_TUI_DAMAGE_GRAMMAR } from "./damage-aware-terminal.js";
3
+ export { DamageAwareTerminalAdapter, PINNED_PI_TUI_DAMAGE_GRAMMAR, paintTerminalCanvasFrame, rebaseTerminalDefaultBackground, } from "./damage-aware-terminal.js";
4
4
  export type { DamageAwareTerminalOptions, PiTuiDamageDecision, PiTuiDamageDecisionReason, PiTuiDamageFrameDescriptor, PiTuiDamageFrameSafety, } from "./damage-aware-terminal.js";
5
5
  export { InputPresentationCoordinator, classifyPiTuiInput } from "./input-presentation-coordinator.js";
6
6
  export type { InputPresentationCoordinatorOptions, PiTuiInputCoordinationDecision, PiTuiInputCoordinationScheduler, PiTuiInputCoordinationTrace, PiTuiInputSurfaceKind, } from "./input-presentation-coordinator.js";
@@ -1,5 +1,5 @@
1
1
  export { samePointerSurfaces } from "./overlay-geometry.js";
2
- export { DamageAwareTerminalAdapter, PINNED_PI_TUI_DAMAGE_GRAMMAR } from "./damage-aware-terminal.js";
2
+ export { DamageAwareTerminalAdapter, PINNED_PI_TUI_DAMAGE_GRAMMAR, paintTerminalCanvasFrame, rebaseTerminalDefaultBackground, } from "./damage-aware-terminal.js";
3
3
  export { InputPresentationCoordinator, classifyPiTuiInput } from "./input-presentation-coordinator.js";
4
4
  export { PiTuiRuntimeAdapter, PiTuiRuntimeError } from "./adapter.js";
5
5
  export { createPiPresentationRuntime, createPiTerminalBridge } from "./presentation-adapter.js";
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-10-09T06:37:13.333Z",
8
+ "builtAt": "2026-10-10T06:00:13.890Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "51b0b2dc1c9ae577f2586c4217066929278726c2c15308d9144eb6f538e118ff",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-10-09T06:37:02.979Z",
8
+ "builtAt": "2026-10-10T06:00:04.832Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "ef62679e83bc1fad3d38f9f21675d9256f08d20e67fd4791571032115ca1b0e9",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-10-09T06:37:43.630Z",
8
+ "builtAt": "2026-10-10T06:00:51.067Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "63c2b95f4d07de25f9b25da06e7386f681474967118bf034ef4b02f0259cc9a3",
11
+ "sha256": "e9c6d7e4596a7cd2634bb74b67925499fe6817cbfb667b574d27ff1b7cb33d93",
12
12
  "size": 288256
13
13
  },
14
14
  "provenance": {
@@ -1,4 +1,4 @@
1
- export declare const OWNED_UI_SETTINGS_VERSION = 12;
1
+ export declare const OWNED_UI_SETTINGS_VERSION = 13;
2
2
  export type OwnedUiSettingValue = string | number | boolean;
3
3
  export type OwnedUiSettingApplication = "live" | "restart";
4
4
  export interface OwnedUiSettingDeclaration {
@@ -44,6 +44,18 @@ export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
44
44
  defaultValue: "purple";
45
45
  allowedValues: readonly ["purple", "blue", "cyan", "green", "orange", "pink"];
46
46
  }>;
47
+ readonly backgroundStyle: Readonly<{
48
+ id: "backgroundStyle";
49
+ label: "Background";
50
+ section: Readonly<{
51
+ id: "appearance";
52
+ title: "Appearance";
53
+ }>;
54
+ description: "Fullscreen canvas background: terminal transparency, a dark accent tint, or fixed neutral dark.";
55
+ application: "live";
56
+ defaultValue: "transparent";
57
+ allowedValues: readonly ["transparent", "accent", "dark"];
58
+ }>;
47
59
  readonly quitAnimation: Readonly<{
48
60
  id: "quitAnimation";
49
61
  label: "Quit animation";
@@ -1,4 +1,4 @@
1
- export const OWNED_UI_SETTINGS_VERSION = 12;
1
+ export const OWNED_UI_SETTINGS_VERSION = 13;
2
2
  const UI_ACCENT_COLOR_CHOICES = Object.freeze([
3
3
  "purple", "blue", "cyan", "green", "orange", "pink",
4
4
  ]);
@@ -32,6 +32,17 @@ export const OWNED_SETTING_DECLARATIONS = Object.freeze({
32
32
  defaultValue: "purple",
33
33
  allowedValues: UI_ACCENT_COLOR_CHOICES,
34
34
  }),
35
+ backgroundStyle: Object.freeze({
36
+ id: "backgroundStyle",
37
+ label: "Background",
38
+ section: APPEARANCE_SECTION,
39
+ description: "Fullscreen canvas background: terminal transparency, a dark accent tint, or fixed neutral dark.",
40
+ application: "live",
41
+ defaultValue: "transparent",
42
+ allowedValues: Object.freeze([
43
+ "transparent", "accent", "dark",
44
+ ]),
45
+ }),
35
46
  quitAnimation: Object.freeze({
36
47
  id: "quitAnimation",
37
48
  label: "Quit animation",
@@ -97,6 +97,13 @@ export const OWNED_UI_SETTINGS_MIGRATIONS = Object.freeze([
97
97
  : { ...values };
98
98
  },
99
99
  }),
100
+ Object.freeze({
101
+ to: 13,
102
+ description: "Introduce the transparent fullscreen background default.",
103
+ migrate(values) {
104
+ return { ...values };
105
+ },
106
+ }),
100
107
  ]);
101
108
  export function assertOwnedUiSettingsMigrations(migrations, currentVersion = OWNED_UI_SETTINGS_VERSION) {
102
109
  const firstProduced = currentVersion - migrations.length + 1;
@@ -30,7 +30,7 @@ The owned Pi-backed surface is not an arbitrary-CLI terminal multiplexer. A feat
30
30
  - `prompt-suggestions`: opt-in bounded diagnostic snapshots of neutral suggestion lifecycle metadata. Composition supplies an explicit local destination; this feature never imports Pi, generates suggestions, stores conversation text, or owns the editor. See [prompt suggestion diagnostics](prompt-suggestions.md).
31
31
  - `storage`: SQLite migrations, prior-boot reconciliation, and plural launch-instance persistence. Legacy foreground rows are historical migration input and never authorize current ownership.
32
32
  - `supervisor`: endpoint identity, plural cohort ownership, per-instance reconciliation, and aggregate release shutdown coordination. It owns no terminal surface.
33
- - `pi-engine-adapter`, `pi-component-adapter`, and `pi-tui-runtime-adapter`: isolate pinned Pi engine and presentation knowledge behind neutral contracts; product features do not import them directly.
33
+ - `pi-engine-adapter`, `pi-component-adapter`, and `pi-tui-runtime-adapter`: isolate pinned Pi engine and presentation knowledge behind neutral contracts; product features do not import them directly. Inside the engine adapter, one process-level engine host owns what is process-wide (the HTTP dispatcher, the theme singletons, the one-time changelog and package-update announcements, the startup trace) and is the only place sessions are created, so every session id is unique per process.
34
34
  - `session-shell` (`src/app/session-shell`): the application layer that composes the feature owners, the UI foundations, and the three Pi adapters into the interactive session; only composition imports it. Its render root assembles semantic document and dock rows, while its focused viewport controller owns follow state, pointer routing, selection, and interaction timers. Owned-app route lifecycle belongs to the neutral `ui-apps` owner and is only hosted by the session shell.
35
35
  - release/update/bootstrap: package-derived immutable release identity, process-guardian integrity, cohort selection, durable update transactions, rollback, and dependency-light command entry. After an unsuccessful bare-A1 child exit, the bootstrap restores owned terminal modes; neither guardian emits terminal controls.
36
36
  - `terminal-cleanup`: bounded, idempotent emergency restoration and fatal-exit diagnostics for an explicitly owned terminal. It does not inspect commands, relay terminal input/output, or emulate terminal state. The owned UI installs its fatal boundary before terminal activation; packaged bootstrap and checkout launch provide the surviving-owner fallback. Fatal records retain at most ten 16 KiB records with trusted classifications and code locations, never prompts, attachments, raw input, credentials, or arbitrary exception messages.
@@ -43,6 +43,14 @@ The JavaScript product path has no PTY or terminal-emulator dependency. Reintrod
43
43
 
44
44
  `scripts/governance/check-architecture.mjs` enforces the structural parts of these boundaries. Cross-cutting rationale belongs here; implementation history belongs in Git and archived OpenSpec changes.
45
45
 
46
+ ## Multi-session limits of the pinned engine
47
+
48
+ The engine host removes A1's own obstacles to several Pi sessions in one process, but three remain in the pinned Pi SDK and must be planned around, not discovered:
49
+
50
+ - The extension cache is keyed by one working directory and cleared on reload (`core/extensions/loader.js`), so sessions in different directories reload each other's extensions.
51
+ - `SettingsManager` writes under a lockfile but never re-reads the file, so a second in-memory copy goes stale after another session writes a setting (`core/settings-manager.js`). The host keeps the one process-wide effect, the HTTP dispatcher, consistent itself.
52
+ - `AgentSessionRuntime.dispose()` releases the session but not the model runtime or the resource loader it was created with (`core/agent-session-runtime.js`), so a session's services outlive it until the process ends.
53
+
46
54
  ## Planned multi-agent boundaries
47
55
 
48
56
  The multi-agent workspace feature, its dependency-free contracts, the structured-agent runtime, and the Node side of the terminal-host protocol were removed from `develop` while the multi-agent plan is on hold; their last state is preserved on the `archive/multi-agent-workspace` branch. A future implementation starts from the current codebase rather than from that copy. The boundary rules below remain the design intent for that work.
@@ -18,13 +18,14 @@ src/
18
18
  prompt-history/ profile-isolated prompt retention and bounded SQLite worker lifecycle
19
19
  prompt-suggestions/ opt-in bounded metadata capture and local diagnostic snapshots
20
20
  contracts/
21
- agent-engine/ dependency-free agent engine, session, package, and capability ports
21
+ agent-engine/ dependency-free agent settings and package contracts
22
22
  owned-ui/ dependency-free owned-session and extension UI contracts
23
23
  presentation/ dependency-free component, terminal, and runtime ports
24
24
  integrations/
25
25
  pi/
26
26
  components/ pinned Pi component and theme adaptation
27
27
  engine/ pinned Pi engine, settings, resource, package, and workflow integration
28
+ session-presenters/ pinned Pi selectors and transcript renderers built from engine objects
28
29
  tui-runtime/ neutral presentation runtime over pinned Pi TUI
29
30
  foundation/
30
31
  launch-guardian/ authenticated launch-instance coordination
@@ -42,7 +43,7 @@ src/
42
43
 
43
44
  Each directory owner exposes `index.ts`, which lists its named exports; `export *` is rejected there so a public contract is readable in one place. Imports within one owner may use private files. Imports crossing owners must use the provider's public entry and follow the dependency DAG declared by `PROJECT_OWNERS`. Two exceptions exist: `src/composition` is the dependency-injection root and may reach past any public entry, and a module on the eager startup path may import a leaf of a provider whose public entry is a prohibited startup entry (`PROHIBITED_STARTUP_ENTRIES` in `startup-graph-policy.mjs`), because loading that barrel would pull the provider's whole graph into startup. `product-identity` is the sole exception to the directory-entry convention because its public entry is `src/product-identity.ts`.
44
45
 
45
- Three layer boundaries hold regardless of the DAG: `src/contracts/*` import nothing outside their own contract; `src/ui/components` imports only contracts; only `src/integrations/pi/*` and the shipped `bin/` entries import the pinned Pi packages. The session shell under `src/app` is the application layer: it may import the contracts, the UI foundations, the three Pi adapters, and the feature owners, and only composition imports it.
46
+ Three layer boundaries hold regardless of the DAG: `src/contracts/*` import nothing outside their own contract; `src/ui/components` imports only contracts; only `src/integrations/pi/*` and the shipped `bin/` entries import the pinned Pi packages. The session shell under `src/app` is the application layer: it may import the contracts, the UI foundations, the three Pi adapters, and the feature owners, and only composition imports it. The shell never holds a Pi engine object: selectors and transcript renderers built from one live in `src/integrations/pi/session-presenters`, the only owner besides composition that may import both the engine and the component adapters, and the shell opens them through the `OwnedUiSessionPresenters` contract.
46
47
 
47
48
  `src/cli` contains command policy but delegates runtime work. `src/composition` is the concrete dependency-injection boundary: it may know both neutral contracts and Pi implementations, while product features receive vendor-neutral ports. Foundation modules never import product features. Pi package knowledge remains inside the Pi adapter owners.
48
49