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

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 (116) 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 +4 -2
  6. package/dist/app/session-shell/index.js +2 -1
  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 +29 -11
  17. package/dist/app/session-shell/session-shell-root.js +62 -53
  18. package/dist/app/session-shell/session-shell.d.ts +55 -29
  19. package/dist/app/session-shell/session-shell.js +748 -1132
  20. package/dist/app/session-shell/shell-host.d.ts +105 -0
  21. package/dist/app/session-shell/shell-host.js +523 -0
  22. package/dist/cli/session-context.js +6 -3
  23. package/dist/composition/owned-ui.d.ts +20 -2
  24. package/dist/composition/owned-ui.js +107 -56
  25. package/dist/composition/session-info-reference.d.ts +2 -2
  26. package/dist/composition/session-info-reference.js +2 -2
  27. package/dist/contracts/agent-engine/capability-ports.d.ts +1 -85
  28. package/dist/contracts/agent-engine/capability-ports.js +1 -35
  29. package/dist/contracts/agent-engine/index.d.ts +1 -7
  30. package/dist/contracts/agent-engine/index.js +0 -4
  31. package/dist/contracts/owned-ui/index.d.ts +5 -1
  32. package/dist/contracts/owned-ui/index.js +1 -0
  33. package/dist/contracts/owned-ui/model.d.ts +6 -0
  34. package/dist/contracts/owned-ui/session-backend.d.ts +119 -0
  35. package/dist/contracts/owned-ui/session-presenters.d.ts +82 -0
  36. package/dist/contracts/owned-ui/session-presenters.js +1 -0
  37. package/dist/contracts/owned-ui/session-workflows.d.ts +342 -0
  38. package/dist/contracts/owned-ui/session-workflows.js +30 -0
  39. package/dist/features/launch/index.d.ts +1 -1
  40. package/dist/features/launch/index.js +1 -1
  41. package/dist/features/launch/session-context.d.ts +1 -0
  42. package/dist/features/launch/session-context.js +1 -0
  43. package/dist/features/owned-ui/settings-app.js +35 -15
  44. package/dist/foundation/lifecycle/index.d.ts +1 -1
  45. package/dist/foundation/lifecycle/index.js +1 -1
  46. package/dist/foundation/lifecycle/session-repository-context.d.ts +8 -2
  47. package/dist/foundation/lifecycle/session-repository-context.js +200 -65
  48. package/dist/foundation/release/bootstrap.js +1 -2
  49. package/dist/foundation/release/release-gc.js +1 -1
  50. package/dist/foundation/release/update.js +0 -4
  51. package/dist/foundation/storage/control-store.js +0 -12
  52. package/dist/integrations/pi/components/components.js +0 -16
  53. package/dist/integrations/pi/components/index.d.ts +3 -3
  54. package/dist/integrations/pi/components/index.js +2 -2
  55. package/dist/integrations/pi/components/models-dialog.js +1 -30
  56. package/dist/integrations/pi/components/shell-editor-autocomplete.js +11 -9
  57. package/dist/integrations/pi/components/shell-extension-ui.d.ts +3 -3
  58. package/dist/integrations/pi/components/shell-extension-ui.js +6 -2
  59. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -4
  60. package/dist/integrations/pi/components/shell-shared-facade.d.ts +21 -1
  61. package/dist/integrations/pi/components/shell-shared-facade.js +18 -2
  62. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +5 -1
  63. package/dist/integrations/pi/components/upstream/theme/theme.js +20 -2
  64. package/dist/integrations/pi/engine/adapter.d.ts +44 -24
  65. package/dist/integrations/pi/engine/adapter.js +130 -22
  66. package/dist/integrations/pi/engine/command-dispatch.d.ts +2 -5
  67. package/dist/integrations/pi/engine/extension-ui-binding.d.ts +2 -11
  68. package/dist/integrations/pi/engine/host-ports.d.ts +20 -0
  69. package/dist/integrations/pi/engine/host-ports.js +13 -0
  70. package/dist/integrations/pi/engine/host.d.ts +70 -0
  71. package/dist/integrations/pi/engine/host.js +147 -0
  72. package/dist/integrations/pi/engine/index.d.ts +8 -4
  73. package/dist/integrations/pi/engine/index.js +3 -1
  74. package/dist/integrations/pi/engine/resource-catalog.d.ts +4 -23
  75. package/dist/integrations/pi/engine/runtime-integration.d.ts +8 -2
  76. package/dist/integrations/pi/engine/runtime-integration.js +12 -7
  77. package/dist/integrations/pi/engine/session-integration.d.ts +3 -8
  78. package/dist/integrations/pi/engine/session-integration.js +0 -49
  79. package/dist/integrations/pi/engine/session-runtime.d.ts +18 -3
  80. package/dist/integrations/pi/engine/session-runtime.js +18 -7
  81. package/dist/integrations/pi/engine/settings-bridge.d.ts +2 -2
  82. package/dist/integrations/pi/engine/settings-bridge.js +0 -2
  83. package/dist/integrations/pi/engine/settings-port.d.ts +2 -0
  84. package/dist/integrations/pi/engine/settings-port.js +3 -5
  85. package/dist/integrations/pi/engine/workflows.d.ts +33 -260
  86. package/dist/integrations/pi/engine/workflows.js +6 -28
  87. package/dist/integrations/pi/session-presenters/index.d.ts +2 -0
  88. package/dist/integrations/pi/session-presenters/index.js +1 -0
  89. package/dist/integrations/pi/session-presenters/presenters.d.ts +16 -0
  90. package/dist/integrations/pi/session-presenters/presenters.js +341 -0
  91. package/dist/integrations/pi/tui-runtime/adapter.js +0 -2
  92. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +18 -0
  93. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +84 -3
  94. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -1
  95. package/dist/integrations/pi/tui-runtime/index.js +1 -1
  96. package/dist/native/darwin-arm64/manifest.json +1 -1
  97. package/dist/native/linux-x64/manifest.json +1 -1
  98. package/dist/native/win32-x64/manifest.json +2 -2
  99. package/dist/native/win32-x64/process-guardian.exe +0 -0
  100. package/dist/ui/settings/declarations.d.ts +13 -1
  101. package/dist/ui/settings/declarations.js +12 -1
  102. package/dist/ui/settings/migrations.js +7 -0
  103. package/docs/architecture/boundaries.md +9 -1
  104. package/docs/architecture/project-structure.md +3 -2
  105. package/docs/architecture/ui-reference-provenance.md +1 -1
  106. package/package.json +1 -1
  107. package/dist/contracts/agent-engine/model.d.ts +0 -78
  108. package/dist/contracts/agent-engine/model.js +0 -1
  109. package/dist/contracts/agent-engine/ports.d.ts +0 -18
  110. package/dist/contracts/agent-engine/serialization.d.ts +0 -7
  111. package/dist/contracts/agent-engine/serialization.js +0 -58
  112. package/dist/contracts/agent-engine/validation.d.ts +0 -5
  113. package/dist/contracts/agent-engine/validation.js +0 -128
  114. package/dist/integrations/pi/components/upstream/theme/system-theme.d.ts +0 -61
  115. package/dist/integrations/pi/components/upstream/theme/system-theme.js +0 -709
  116. /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;
@@ -1,6 +1,11 @@
1
1
  import type { PiTuiTerminalPort } from "./contracts.js";
2
2
  export declare const PINNED_PI_TUI_DAMAGE_GRAMMAR = "@earendil-works/pi-tui@1.1.0:tui-alt-screen-one-write-v1";
3
3
  export interface PiTuiDamageFrameDescriptor {
4
+ /**
5
+ * Frame ids are compared only within one presentation epoch; absent means the current epoch. A
6
+ * presenter switch starts a new epoch, because each presenter numbers its own frames.
7
+ */
8
+ readonly epoch?: number;
4
9
  readonly frameId: number;
5
10
  readonly width: number;
6
11
  readonly height: number;
@@ -63,6 +68,15 @@ export declare class DamageAwareTerminalAdapter implements PiTuiTerminalPort {
63
68
  get kittyProtocolActive(): boolean;
64
69
  get lastDecision(): PiTuiDamageDecision;
65
70
  get hyperlinkCleanupPending(): boolean;
71
+ get canvasBackgroundAnsi(): string | null;
72
+ get presentationEpoch(): number;
73
+ /**
74
+ * Starts a new presentation epoch: the remembered cells and the armed frame are discarded and frame ids
75
+ * restart, so the next frame is painted in full rather than reconciled against another presenter's.
76
+ */
77
+ invalidatePresentation(): void;
78
+ /** Invalidates remembered terminal cells before the shell forces one complete canvas repaint. */
79
+ setCanvasBackground(ansi: string | null): boolean;
66
80
  /**
67
81
  * The rows as last forwarded to the terminal, top to bottom, with styling
68
82
  * intact. A row the adapter has not seen since its last invalidation is empty.
@@ -86,3 +100,7 @@ export declare class DamageAwareTerminalAdapter implements PiTuiTerminalPort {
86
100
  setTitle(title: string): void;
87
101
  setProgress(active: boolean): void;
88
102
  }
103
+ /** Makes SGR background resets return to A1's canvas while preserving explicit backgrounds. */
104
+ export declare function rebaseTerminalDefaultBackground(content: string, backgroundAnsi: string): string;
105
+ /** Paints only synchronized fullscreen rows; clipboard/title/progress controls remain byte-identical. */
106
+ 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
@@ -22,10 +23,12 @@ export class DamageAwareTerminalAdapter {
22
23
  #cleanedRevision = 0;
23
24
  #recoveryRevision = 0;
24
25
  #epoch = 0;
26
+ #presentationEpoch = 0;
25
27
  #armed;
26
28
  #lastConsumedFrameId = 0;
27
29
  #cacheWidth = 0;
28
30
  #cacheHeight = 0;
31
+ #canvasBackgroundAnsi = null;
29
32
  #decision = {
30
33
  frameId: null,
31
34
  transformed: false,
@@ -41,6 +44,26 @@ export class DamageAwareTerminalAdapter {
41
44
  get kittyProtocolActive() { return this.inner.kittyProtocolActive; }
42
45
  get lastDecision() { return this.#decision; }
43
46
  get hyperlinkCleanupPending() { return this.#cleanupRevision > this.#cleanedRevision; }
47
+ get canvasBackgroundAnsi() { return this.#canvasBackgroundAnsi; }
48
+ get presentationEpoch() { return this.#presentationEpoch; }
49
+ /**
50
+ * Starts a new presentation epoch: the remembered cells and the armed frame are discarded and frame ids
51
+ * restart, so the next frame is painted in full rather than reconciled against another presenter's.
52
+ */
53
+ invalidatePresentation() {
54
+ this.#presentationEpoch += 1;
55
+ this.#armed = undefined;
56
+ this.#lastConsumedFrameId = 0;
57
+ this.#invalidatePresentation();
58
+ }
59
+ /** Invalidates remembered terminal cells before the shell forces one complete canvas repaint. */
60
+ setCanvasBackground(ansi) {
61
+ if (this.#canvasBackgroundAnsi === ansi)
62
+ return false;
63
+ this.#canvasBackgroundAnsi = ansi;
64
+ this.#invalidatePresentation();
65
+ return true;
66
+ }
44
67
  /**
45
68
  * The rows as last forwarded to the terminal, top to bottom, with styling
46
69
  * intact. A row the adapter has not seen since its last invalidation is empty.
@@ -97,7 +120,7 @@ export class DamageAwareTerminalAdapter {
97
120
  const armed = this.#armed;
98
121
  this.#armed = undefined;
99
122
  const parsed = parsePinnedFullscreenWrite(data);
100
- if (armed !== undefined && armed.descriptor.frameId <= this.#lastConsumedFrameId) {
123
+ if (armed !== undefined && this.#isStale(armed.descriptor)) {
101
124
  // Concurrency: a recognized obsolete frame must never repaint newer cells,
102
125
  // acknowledge cleanup, or become the reference for a subsequent differential.
103
126
  this.#decision = this.#decide(armed, parsed);
@@ -140,7 +163,10 @@ export class DamageAwareTerminalAdapter {
140
163
  output = buildDamageWrite(parsed, armed.descriptor, new Set(decision.paintedRows), this.#rows);
141
164
  }
142
165
  }
143
- this.inner.write(output);
166
+ const forwarded = this.#canvasBackgroundAnsi === null
167
+ ? output
168
+ : paintTerminalCanvasFrame(output, this.#canvasBackgroundAnsi);
169
+ this.inner.write(forwarded);
144
170
  if (epoch !== this.#epoch
145
171
  || (armed !== undefined && this.#lastConsumedFrameId !== armed.descriptor.frameId))
146
172
  return;
@@ -175,7 +201,7 @@ export class DamageAwareTerminalAdapter {
175
201
  shiftRows: descriptor.verticalShiftRows,
176
202
  paintedRows: [],
177
203
  };
178
- if (descriptor.frameId <= this.#lastConsumedFrameId)
204
+ if (this.#isStale(descriptor))
179
205
  return { ...base, reason: "stale-frame" };
180
206
  this.#lastConsumedFrameId = descriptor.frameId;
181
207
  if (this.hyperlinkCleanupPending)
@@ -251,6 +277,10 @@ export class DamageAwareTerminalAdapter {
251
277
  paintedRows: [...painted].sort((left, right) => left - right),
252
278
  };
253
279
  }
280
+ #isStale(descriptor) {
281
+ return (descriptor.epoch ?? this.#presentationEpoch) !== this.#presentationEpoch
282
+ || descriptor.frameId <= this.#lastConsumedFrameId;
283
+ }
254
284
  #rememberFrame(descriptor, parsed, raw) {
255
285
  if (raw.includes("\u001b[2J") || raw.includes("\u001b[?1049") || raw.includes("\u001b_G") || raw.includes("\u001bPq")) {
256
286
  this.#invalidateRows();
@@ -363,6 +393,57 @@ export class DamageAwareTerminalAdapter {
363
393
  this.#cacheHeight = 0;
364
394
  }
365
395
  }
396
+ /** Makes SGR background resets return to A1's canvas while preserving explicit backgrounds. */
397
+ export function rebaseTerminalDefaultBackground(content, backgroundAnsi) {
398
+ return content.replace(SGR_SEQUENCE, (sequence, parameters) => (sgrLeavesDefaultBackground(parameters) ? `${sequence}${backgroundAnsi}` : sequence));
399
+ }
400
+ /** Paints only synchronized fullscreen rows; clipboard/title/progress controls remain byte-identical. */
401
+ export function paintTerminalCanvasFrame(data, backgroundAnsi) {
402
+ if (!data.startsWith(BEGIN_SYNCHRONIZED_OUTPUT) || !data.endsWith(END_SYNCHRONIZED_OUTPUT)
403
+ || !ROW_MARKER.test(data)) {
404
+ ROW_MARKER.lastIndex = 0;
405
+ return data;
406
+ }
407
+ ROW_MARKER.lastIndex = 0;
408
+ const rows = data.replace(ROW_MARKER, (_marker, row) => (`\u001b[${row};1H${backgroundAnsi}\u001b[2K${backgroundAnsi}`));
409
+ ROW_MARKER.lastIndex = 0;
410
+ const rebased = rebaseTerminalDefaultBackground(rows, backgroundAnsi);
411
+ return `${rebased.slice(0, -END_SYNCHRONIZED_OUTPUT.length)}\u001b[49m${END_SYNCHRONIZED_OUTPUT}`;
412
+ }
413
+ function sgrLeavesDefaultBackground(parameters) {
414
+ if (parameters.length === 0)
415
+ return true;
416
+ const values = parameters.split(";");
417
+ let defaultBackground;
418
+ for (let index = 0; index < values.length; index += 1) {
419
+ const token = values[index];
420
+ const code = Number.parseInt(token.split(":", 1)[0] || "0", 10);
421
+ if (!Number.isFinite(code))
422
+ continue;
423
+ if (code === 0 || code === 49) {
424
+ defaultBackground = true;
425
+ continue;
426
+ }
427
+ if (code >= 40 && code <= 47 || code >= 100 && code <= 107) {
428
+ defaultBackground = false;
429
+ continue;
430
+ }
431
+ if (code !== 38 && code !== 48 && code !== 58)
432
+ continue;
433
+ if (code === 48)
434
+ defaultBackground = false;
435
+ if (token.includes(":"))
436
+ continue;
437
+ const mode = Number.parseInt(values[index + 1] ?? "", 10);
438
+ if (mode === 5)
439
+ index += 2;
440
+ else if (mode === 2)
441
+ index += 4;
442
+ else
443
+ index += 1;
444
+ }
445
+ return defaultBackground === true;
446
+ }
366
447
  function parsePinnedFullscreenWrite(data) {
367
448
  if (!data.startsWith(BEGIN_SYNCHRONIZED_OUTPUT) || !data.endsWith(END_SYNCHRONIZED_OUTPUT))
368
449
  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-10T09:15:37.918Z",
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-10T09:15:10.349Z",
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-10T09:16:06.991Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "63c2b95f4d07de25f9b25da06e7386f681474967118bf034ef4b02f0259cc9a3",
11
+ "sha256": "c6e2cb23d012d779478fd1bff750f17687a4c5466bad7621b1b3c200a0557101",
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;