@timurproko/a1 0.1.8-dev.182 → 0.1.8-dev.218
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +2 -2
- package/bin/sync-pi-tui-proxy.js +1 -1
- package/bin/ui.js +1 -1
- package/dist/cli/dispatch.js +3 -3
- package/dist/cli/packages.js +1 -1
- package/dist/cli/version-stats.js +3 -3
- package/dist/composition/owned-ui.js +1 -1
- package/dist/composition/settings-route-host.js +6 -4
- package/dist/features/launch/development-launch.js +1 -1
- package/dist/features/owned-ui/customization.d.ts +2 -0
- package/dist/features/owned-ui/customization.js +2 -0
- package/dist/features/owned-ui/diagnostics.d.ts +1 -0
- package/dist/features/owned-ui/diagnostics.js +1 -0
- package/dist/features/owned-ui/project-trust-prompt.d.ts +11 -6
- package/dist/features/owned-ui/project-trust-prompt.js +134 -17
- package/dist/features/owned-ui/settings-app.js +47 -50
- package/dist/features/workspace/reducer.d.ts +1 -0
- package/dist/features/workspace/reducer.js +1 -0
- package/dist/features/workspace/router.d.ts +1 -0
- package/dist/features/workspace/router.js +1 -0
- package/dist/features/workspace/store.d.ts +1 -0
- package/dist/features/workspace/store.js +1 -0
- package/dist/foundation/launch-guardian/main.js +1 -1
- package/dist/foundation/native-host-protocol/codec.d.ts +2 -0
- package/dist/foundation/native-host-protocol/codec.js +2 -0
- package/dist/foundation/process-containment/linux-process-inspector.d.ts +1 -0
- package/dist/foundation/process-containment/linux-process-inspector.js +1 -0
- package/dist/foundation/process-containment/native-guardian-containment.d.ts +1 -0
- package/dist/foundation/process-containment/native-guardian-containment.js +1 -0
- package/dist/foundation/process-containment/windows-process-inspector.d.ts +1 -0
- package/dist/foundation/process-containment/windows-process-inspector.js +1 -0
- package/dist/foundation/protocol/client.d.ts +1 -0
- package/dist/foundation/protocol/client.js +1 -0
- package/dist/foundation/protocol/messages.d.ts +1 -0
- package/dist/foundation/protocol/messages.js +1 -0
- package/dist/foundation/release/bootstrap.js +6 -6
- package/dist/foundation/release/cohort-state.d.ts +1 -0
- package/dist/foundation/release/cohort-state.js +1 -0
- package/dist/foundation/release/release-gc.js +1 -1
- package/dist/foundation/release/release.js +1 -1
- package/dist/foundation/release/update-transaction.d.ts +1 -0
- package/dist/foundation/release/update-transaction.js +1 -0
- package/dist/foundation/release/update.js +12 -12
- package/dist/foundation/storage/control-store.d.ts +1 -0
- package/dist/foundation/storage/control-store.js +1 -0
- package/dist/foundation/structured-agent-runtime/backpressure.d.ts +1 -0
- package/dist/foundation/structured-agent-runtime/backpressure.js +1 -0
- package/dist/foundation/structured-agent-runtime/commands.d.ts +1 -0
- package/dist/foundation/structured-agent-runtime/commands.js +1 -0
- package/dist/foundation/structured-agent-runtime/reconnection.d.ts +1 -0
- package/dist/foundation/structured-agent-runtime/reconnection.js +1 -0
- package/dist/foundation/structured-agent-runtime/state.d.ts +1 -0
- package/dist/foundation/structured-agent-runtime/state.js +1 -0
- package/dist/foundation/supervision/main.js +1 -1
- package/dist/foundation/supervision/server.d.ts +1 -0
- package/dist/foundation/supervision/server.js +4 -6
- package/dist/integrations/pi/components/conformance.d.ts +1 -0
- package/dist/integrations/pi/components/conformance.js +1 -0
- package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
- package/dist/integrations/pi/components/shell-footer-status.js +8 -7
- package/dist/integrations/pi/components/shell-shared-facade.js +1 -1
- package/dist/integrations/pi/engine/adapter.d.ts +7 -0
- package/dist/integrations/pi/engine/adapter.js +37 -35
- package/dist/integrations/pi/engine/conformance.d.ts +2 -0
- package/dist/integrations/pi/engine/conformance.js +2 -0
- package/dist/integrations/pi/engine/http-dispatcher.js +1 -1
- package/dist/integrations/pi/engine/model-auth-integration.d.ts +1 -0
- package/dist/integrations/pi/engine/model-auth-integration.js +1 -0
- package/dist/integrations/pi/engine/package-integration.js +1 -1
- package/dist/integrations/pi/engine/project-trust-preflight.js +1 -1
- package/dist/integrations/pi/engine/resource-extension-integration.d.ts +1 -0
- package/dist/integrations/pi/engine/resource-extension-integration.js +1 -0
- package/dist/integrations/pi/engine/session-integration.d.ts +1 -0
- package/dist/integrations/pi/engine/session-integration.js +2 -1
- package/dist/integrations/pi/engine/settings-effects.d.ts +13 -2
- package/dist/integrations/pi/engine/settings-effects.js +62 -42
- package/dist/integrations/pi/engine/settings-integration.js +4 -4
- package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -0
- package/dist/integrations/pi/engine/workflow-controllers.js +1 -0
- package/dist/integrations/pi/session-ui/index.d.ts +1 -0
- package/dist/integrations/pi/session-ui/index.js +1 -0
- package/dist/integrations/pi/session-ui/prompt-chips.js +1 -1
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +30 -2
- package/dist/integrations/pi/session-ui/session-shell-root.js +133 -72
- package/dist/integrations/pi/session-ui/session-shell.d.ts +9 -2
- package/dist/integrations/pi/session-ui/session-shell.js +112 -37
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +44 -11
- package/dist/integrations/pi/session-ui/stream-presentation-coalescer.d.ts +29 -0
- package/dist/integrations/pi/session-ui/stream-presentation-coalescer.js +79 -0
- package/dist/integrations/pi/session-ui/system-clipboard.js +6 -6
- package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
- package/dist/integrations/pi/tui-runtime/adapter.js +138 -8
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +25 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +63 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +240 -0
- package/dist/integrations/pi/tui-runtime/index.d.ts +2 -0
- package/dist/integrations/pi/tui-runtime/index.js +2 -0
- package/dist/integrations/pi/tui-runtime/input-presentation-coordinator.d.ts +39 -0
- package/dist/integrations/pi/tui-runtime/input-presentation-coordinator.js +139 -0
- package/dist/native/darwin-arm64/manifest.json +3 -3
- package/dist/native/darwin-arm64/process-guardian +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/apps/host.js +5 -9
- package/dist/ui/apps/registry.d.ts +1 -0
- package/dist/ui/apps/registry.js +1 -0
- package/dist/ui/components/dialog-panel.js +5 -3
- package/dist/ui/components/frame.d.ts +1 -0
- package/dist/ui/components/frame.js +1 -0
- package/dist/ui/components/index.d.ts +1 -0
- package/dist/ui/components/index.js +1 -0
- package/dist/ui/components/line-input.d.ts +1 -6
- package/dist/ui/components/line-input.js +4 -9
- package/dist/ui/components/list-view.d.ts +0 -6
- package/dist/ui/components/list-view.js +14 -7
- package/dist/ui/components/progress-status.d.ts +2 -0
- package/dist/ui/components/progress-status.js +4 -0
- package/dist/ui/components/scrollbar.js +1 -1
- package/dist/ui/components/shortcuts.d.ts +1 -0
- package/dist/ui/components/shortcuts.js +1 -0
- package/dist/ui/components/spans.d.ts +0 -5
- package/dist/ui/components/spans.js +3 -8
- package/dist/ui/components/surface.d.ts +0 -5
- package/dist/ui/components/surface.js +1 -5
- package/dist/ui/components/text-selection.d.ts +10 -1
- package/dist/ui/components/text-selection.js +96 -29
- package/dist/ui/components/text.js +3 -3
- package/dist/ui/components/transcript-viewport.d.ts +42 -0
- package/dist/ui/components/transcript-viewport.js +287 -39
- package/dist/ui/settings/sections.d.ts +0 -2
- package/dist/ui/settings/sections.js +10 -16
- package/dist/ui/settings/session.js +5 -4
- package/dist/ui/settings/store.d.ts +1 -0
- package/dist/ui/settings/store.js +2 -1
- package/docs/architecture/code-documentation.md +77 -0
- package/docs/architecture/project-structure.md +1 -1
- package/docs/architecture/ui-reference-provenance.md +23 -3
- package/docs/ci-release-runbook.md +48 -4
- package/docs/manual-owned-ui-checkpoint.md +28 -1
- package/docs/manual-terminal-colour-check.md +4 -3
- package/package.json +7 -2
|
@@ -2,11 +2,13 @@ import { PRODUCT_TEXT } from "../../../product-identity.js";
|
|
|
2
2
|
import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../../ui/components/index.js";
|
|
3
3
|
import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../engine/index.js";
|
|
4
4
|
import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
|
|
5
|
-
import { PiTuiRuntimeAdapter, } from "../tui-runtime/index.js";
|
|
5
|
+
import { DamageAwareTerminalAdapter, PiTuiRuntimeAdapter, classifyPiTuiInput, } from "../tui-runtime/index.js";
|
|
6
6
|
import { canonicalizeClipboardImage } from "./clipboard-image.js";
|
|
7
|
+
import { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer, } from "./stream-presentation-coalescer.js";
|
|
7
8
|
import { preloadSystemClipboard, readSystemClipboardContent, writeSystemClipboardText, } from "./system-clipboard.js";
|
|
8
9
|
import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
|
|
9
10
|
export { OwnedUiSessionShellRoot } from "./session-shell-root.js";
|
|
11
|
+
/** Coordinates backend, owned presentation, and Pi TUI lifecycles for one interactive session. */
|
|
10
12
|
export class OwnedUiSessionShell {
|
|
11
13
|
backend;
|
|
12
14
|
root;
|
|
@@ -26,6 +28,8 @@ export class OwnedUiSessionShell {
|
|
|
26
28
|
#disposed = false;
|
|
27
29
|
#pointerReporting = false;
|
|
28
30
|
#customViewport;
|
|
31
|
+
#damageTerminal;
|
|
32
|
+
#streamPresentation;
|
|
29
33
|
#removeViewportPreInput;
|
|
30
34
|
#unsubscribeSettings;
|
|
31
35
|
#unbindPiSettings;
|
|
@@ -52,11 +56,19 @@ export class OwnedUiSessionShell {
|
|
|
52
56
|
this.#resolveStopped = resolve;
|
|
53
57
|
});
|
|
54
58
|
let runtime;
|
|
59
|
+
let damageTerminal;
|
|
60
|
+
let streamPresentation;
|
|
55
61
|
let pendingClipboardWrite = Promise.resolve();
|
|
56
62
|
this.root = new OwnedUiSessionShellRoot(this.backend.view(), options.cwd, {
|
|
57
63
|
getColumns: () => runtime?.viewport().columns ?? options.terminal?.columns ?? 80,
|
|
58
64
|
getRows: () => runtime?.viewport().rows ?? options.terminal?.rows ?? 24,
|
|
59
65
|
requestRender: force => runtime?.requestRender(force),
|
|
66
|
+
onViewportFrame: frame => damageTerminal?.arm(frame.descriptor, {
|
|
67
|
+
overlayActive: runtime?.hasOverlay() ?? false,
|
|
68
|
+
selectionActive: this.root.hasActiveSelection(),
|
|
69
|
+
replacementSurfaceActive: !this.root.usesDefaultInputSurface(),
|
|
70
|
+
}),
|
|
71
|
+
enableDockInputReuse: options.inputPresentation?.viewportReuse !== false,
|
|
60
72
|
onSubmit: text => { void this.submit(text); },
|
|
61
73
|
onInterrupt: () => { void this.interrupt(); },
|
|
62
74
|
onClear: () => { void this.clearOrExit(); },
|
|
@@ -91,7 +103,7 @@ export class OwnedUiSessionShell {
|
|
|
91
103
|
}
|
|
92
104
|
}
|
|
93
105
|
catch {
|
|
94
|
-
//
|
|
106
|
+
// Compatibility: treat an unavailable or malformed image as text-capable clipboard input.
|
|
95
107
|
}
|
|
96
108
|
const text = await options.clipboard.readText();
|
|
97
109
|
return text === null ? null : { kind: "text", text };
|
|
@@ -105,7 +117,7 @@ export class OwnedUiSessionShell {
|
|
|
105
117
|
}, options.sessionLayout, {
|
|
106
118
|
resolve: assetId => this.backend.resolveTranscriptImage(assetId),
|
|
107
119
|
});
|
|
108
|
-
//
|
|
120
|
+
// Invariant: bare A1 owns a bounded viewport and therefore always runs on the alternate
|
|
109
121
|
// fullscreen surface. The pinned comparison profiles still honor Pi's mode.
|
|
110
122
|
const tuiMode = this.#customViewport
|
|
111
123
|
? "fullscreen"
|
|
@@ -113,12 +125,41 @@ export class OwnedUiSessionShell {
|
|
|
113
125
|
const runtimeOptions = {
|
|
114
126
|
root: this.root,
|
|
115
127
|
mode: tuiMode,
|
|
116
|
-
...(this.#customViewport ? {
|
|
128
|
+
...(this.#customViewport ? {
|
|
129
|
+
decorateTerminal: (terminal) => {
|
|
130
|
+
damageTerminal = new DamageAwareTerminalAdapter(terminal, {
|
|
131
|
+
regionalScroll: process.env.TERM !== "dumb",
|
|
132
|
+
onResize: () => streamPresentation?.noteImmediatePresentation(),
|
|
133
|
+
});
|
|
134
|
+
return damageTerminal;
|
|
135
|
+
},
|
|
136
|
+
...(options.inputPresentation?.coordination === false ? {} : {
|
|
137
|
+
inputCoordination: {
|
|
138
|
+
classify: (data, focusedOverlay) => classifyPiTuiInput(data, focusedOverlay ?? this.root.inputCoordinationSurface()),
|
|
139
|
+
onReceipt: () => streamPresentation?.noteImmediatePresentation(),
|
|
140
|
+
...(options.inputPresentation?.scheduler === undefined
|
|
141
|
+
? {}
|
|
142
|
+
: { scheduler: options.inputPresentation.scheduler }),
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
} : { layoutRoot: this.root.layoutRoot() }),
|
|
146
|
+
...(options.inputPresentation?.onEvent === undefined ? {} : {
|
|
147
|
+
inputDiagnostics: {
|
|
148
|
+
onEvent: options.inputPresentation.onEvent,
|
|
149
|
+
...(options.inputPresentation.now === undefined ? {} : { now: options.inputPresentation.now }),
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
117
152
|
...(options.terminal === undefined ? {} : { terminal: options.terminal }),
|
|
118
153
|
hardwareCursor: this.backend.view().terminal.hardwareCursor,
|
|
119
154
|
};
|
|
120
155
|
runtime = new PiTuiRuntimeAdapter(runtimeOptions);
|
|
121
156
|
this.runtime = runtime;
|
|
157
|
+
this.#damageTerminal = damageTerminal ?? null;
|
|
158
|
+
const presentationInterval = options.streamPresentation?.intervalMs ?? STREAM_PRESENTATION_INTERVAL_MS;
|
|
159
|
+
streamPresentation = options.streamPresentation?.scheduler === undefined
|
|
160
|
+
? new StreamPresentationCoalescer(() => this.runtime.requestRender(), presentationInterval)
|
|
161
|
+
: new StreamPresentationCoalescer(() => this.runtime.requestRender(), presentationInterval, options.streamPresentation.scheduler);
|
|
162
|
+
this.#streamPresentation = streamPresentation;
|
|
122
163
|
const initialPiSettings = this.backend.pinnedSettingsSnapshot();
|
|
123
164
|
this.runtime.setHardwareCursor(initialPiSettings.showHardwareCursor);
|
|
124
165
|
this.runtime.setClearOnShrink(initialPiSettings.clearOnShrink);
|
|
@@ -147,7 +188,9 @@ export class OwnedUiSessionShell {
|
|
|
147
188
|
});
|
|
148
189
|
this.#removeViewportPreInput = this.#customViewport
|
|
149
190
|
? this.runtime.addPreInputListener(data => {
|
|
150
|
-
|
|
191
|
+
if (options.inputPresentation?.coordination === false)
|
|
192
|
+
this.#streamPresentation.noteImmediatePresentation();
|
|
193
|
+
// Invariant: an overlay or editor-replacement screen owns its entire pointer
|
|
151
194
|
// surface. Letting the transcript pre-router inspect those reports
|
|
152
195
|
// steals settings value menus and numeric +/- controls before the
|
|
153
196
|
// settings app can receive them.
|
|
@@ -228,7 +271,7 @@ export class OwnedUiSessionShell {
|
|
|
228
271
|
requestRender: () => this.runtime.requestRender(),
|
|
229
272
|
},
|
|
230
273
|
agentDir: this.backend.agentDir,
|
|
231
|
-
setInputSurface: component => this.root.setInputSurface(component),
|
|
274
|
+
setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
|
|
232
275
|
showOverlay: (component, overlayOptions) => this.runtime.showOverlay(component, overlayOptions),
|
|
233
276
|
listenInput: handler => this.runtime.addInputListener(handler),
|
|
234
277
|
replaceWidget: (key, component, placement) => this.root.setExtensionWidget(key, component, placement),
|
|
@@ -242,7 +285,7 @@ export class OwnedUiSessionShell {
|
|
|
242
285
|
setEditorText: text => this.root.editor.setText(text),
|
|
243
286
|
pasteToEditor: text => this.root.editor.insertText(text),
|
|
244
287
|
addAutocompleteProvider: factory => this.root.editor.addAutocompleteProvider(factory),
|
|
245
|
-
setCustomEditor: component => this.root.setInputSurface(component),
|
|
288
|
+
setCustomEditor: component => this.root.setInputSurface(component, true, "opaque"),
|
|
246
289
|
getFooterData: () => this.root.extensionFooterData(),
|
|
247
290
|
getToolsExpanded: () => this.root.toolsExpanded,
|
|
248
291
|
setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
|
|
@@ -255,7 +298,7 @@ export class OwnedUiSessionShell {
|
|
|
255
298
|
});
|
|
256
299
|
this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
|
|
257
300
|
this.#unsubscribe = this.backend.onEvent(event => {
|
|
258
|
-
//
|
|
301
|
+
// Performance: a streamed chunk names one block, and touching only that block is what keeps the
|
|
259
302
|
// cost of a chunk the same in a long session as in a new one. Everything else
|
|
260
303
|
// resynchronizes the view, which is cheap next to re-reading the transcript.
|
|
261
304
|
if (event.type === "agent-run-started")
|
|
@@ -278,6 +321,9 @@ export class OwnedUiSessionShell {
|
|
|
278
321
|
view() {
|
|
279
322
|
return this.backend.view();
|
|
280
323
|
}
|
|
324
|
+
damagePresentationDecision() {
|
|
325
|
+
return this.#damageTerminal?.lastDecision ?? null;
|
|
326
|
+
}
|
|
281
327
|
start() {
|
|
282
328
|
if (this.#started)
|
|
283
329
|
return;
|
|
@@ -367,7 +413,7 @@ export class OwnedUiSessionShell {
|
|
|
367
413
|
this.root.editor.setText("");
|
|
368
414
|
return { outcome: "completed", diagnostic: null };
|
|
369
415
|
}
|
|
370
|
-
//
|
|
416
|
+
// Compatibility: match Pi: with an empty editor, two escapes inside 500 ms open the
|
|
371
417
|
// configured session navigator. The first escape intentionally does not
|
|
372
418
|
// interrupt or mutate the prompt.
|
|
373
419
|
const action = this.backend.pinnedSettingsSnapshot().doubleEscapeAction;
|
|
@@ -486,7 +532,12 @@ export class OwnedUiSessionShell {
|
|
|
486
532
|
onCancel?.();
|
|
487
533
|
},
|
|
488
534
|
});
|
|
489
|
-
const handle = this.runtime.showOverlay(component, {
|
|
535
|
+
const handle = this.runtime.showOverlay(component, {
|
|
536
|
+
width: "70%",
|
|
537
|
+
maxHeight: "80%",
|
|
538
|
+
anchor: "center",
|
|
539
|
+
inputCoordination: "owned",
|
|
540
|
+
});
|
|
490
541
|
this.#dialogHandle = handle;
|
|
491
542
|
return handle;
|
|
492
543
|
}
|
|
@@ -686,7 +737,7 @@ export class OwnedUiSessionShell {
|
|
|
686
737
|
return;
|
|
687
738
|
}
|
|
688
739
|
if (callback === "onTuiModeChange") {
|
|
689
|
-
//
|
|
740
|
+
// Invariant: the custom bare-A1 surface is permanently fullscreen; this callback
|
|
690
741
|
// remains available only to pinned comparison profiles.
|
|
691
742
|
if (this.#customViewport)
|
|
692
743
|
return;
|
|
@@ -907,12 +958,7 @@ export class OwnedUiSessionShell {
|
|
|
907
958
|
this.root.appendDaxnuts();
|
|
908
959
|
}
|
|
909
960
|
}
|
|
910
|
-
|
|
911
|
-
* Turns terminal pointer reporting on for a screen that reads the pointer, and off for
|
|
912
|
-
* every path that ends it. While it is on the terminal hands A1 the wheel and the
|
|
913
|
-
* button instead of scrolling and selecting itself, so leaving it on outlives the
|
|
914
|
-
* screen that wanted it and takes the terminal's own scrolling and selection with it.
|
|
915
|
-
*/
|
|
961
|
+
// Invariant: pointer reporting is disabled on every path that ends the owning screen.
|
|
916
962
|
#setPointerReporting(enabled, forceOff = false) {
|
|
917
963
|
const effective = forceOff ? false : this.#customViewport || enabled;
|
|
918
964
|
if (this.#pointerReporting === effective)
|
|
@@ -929,13 +975,16 @@ export class OwnedUiSessionShell {
|
|
|
929
975
|
this.root.clearViewportPointerState();
|
|
930
976
|
this.#setPointerReporting(false, true);
|
|
931
977
|
this.#removeViewportPreInput();
|
|
978
|
+
this.#streamPresentation.dispose();
|
|
932
979
|
this.#unsubscribeSettings();
|
|
933
980
|
const exitMode = this.backend.disposed
|
|
934
981
|
? this.#fullscreenExitOutput
|
|
935
982
|
: this.backend.pinnedSettingsSnapshot().fullscreenExitOutput;
|
|
936
983
|
const exitTranscript = this.root.exitTranscript(this.runtime.viewport().columns);
|
|
937
|
-
const
|
|
938
|
-
const resumeHint =
|
|
984
|
+
const resume = this.backend.currentSessionResumeMetadata();
|
|
985
|
+
const resumeHint = resume === null
|
|
986
|
+
? ""
|
|
987
|
+
: `${dim("To resume this session:")} ${formatSessionResumeCommand(resume)}`;
|
|
939
988
|
const fullscreenExitText = this.runtime.mode !== "fullscreen"
|
|
940
989
|
? ""
|
|
941
990
|
: exitMode === "resume-hint"
|
|
@@ -952,13 +1001,18 @@ export class OwnedUiSessionShell {
|
|
|
952
1001
|
if (fullscreenExitText.length > 0)
|
|
953
1002
|
this.runtime.writeAfterStop(`${fullscreenExitText}\n`);
|
|
954
1003
|
}
|
|
955
|
-
|
|
956
|
-
* Applies one transcript block without re-reading the session. The listeners still hear
|
|
957
|
-
* the view they would have heard, so nothing downstream can tell the difference.
|
|
958
|
-
*/
|
|
1004
|
+
// Invariant: incremental block updates notify listeners with the same complete view contract.
|
|
959
1005
|
#syncBlock(block) {
|
|
960
1006
|
this.root.applyTranscriptBlock(block);
|
|
961
|
-
this.
|
|
1007
|
+
if (this.#customViewport && block.status === "live" && isCoalescedStreamBlock(block.kind)) {
|
|
1008
|
+
this.#streamPresentation.request();
|
|
1009
|
+
}
|
|
1010
|
+
else if (this.#streamPresentation.pending) {
|
|
1011
|
+
this.#streamPresentation.flush();
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
this.#streamPresentation.presentImmediate();
|
|
1015
|
+
}
|
|
962
1016
|
const view = this.view();
|
|
963
1017
|
for (const listener of this.#listeners)
|
|
964
1018
|
listener(view);
|
|
@@ -970,12 +1024,13 @@ export class OwnedUiSessionShell {
|
|
|
970
1024
|
this.runtime.setTerminalProgress(this.#terminalProgressEnabled && view.lifecycle === "busy");
|
|
971
1025
|
}
|
|
972
1026
|
#syncView() {
|
|
1027
|
+
this.#streamPresentation.noteImmediatePresentation();
|
|
973
1028
|
const view = this.view();
|
|
974
1029
|
if (this.backend.sessionGeneration !== this.#sessionGeneration) {
|
|
975
1030
|
this.#sessionGeneration = this.backend.sessionGeneration;
|
|
976
1031
|
this.#activeLoginDialog = undefined;
|
|
977
1032
|
this.#extensionBridge.reset();
|
|
978
|
-
//
|
|
1033
|
+
// Invariant: a replaced session takes its transient viewport and owned-route state with it.
|
|
979
1034
|
this.root.resetViewport();
|
|
980
1035
|
this.#dialogHandle?.hide();
|
|
981
1036
|
this.#dialogHandle = undefined;
|
|
@@ -1001,10 +1056,10 @@ export class OwnedUiSessionShell {
|
|
|
1001
1056
|
return { outcome: "failed", diagnostic: "runtime is not active" };
|
|
1002
1057
|
this.#dialogHandle?.hide();
|
|
1003
1058
|
this.#dialogSource = "route";
|
|
1004
|
-
//
|
|
1059
|
+
// Protocol: any-event reporting: hover and drag are what the screen is driven by, and
|
|
1005
1060
|
// it also stops the terminal treating a drag as a text selection.
|
|
1006
1061
|
this.#setPointerReporting(true);
|
|
1007
|
-
//
|
|
1062
|
+
// Protocol: the interrupt chord is global, so it is watched on raw input rather than
|
|
1008
1063
|
// through the overlay: the pinned shell handles that key before an overlay
|
|
1009
1064
|
// ever sees it, which is why an owned screen must not rely on being asked.
|
|
1010
1065
|
let armedAt = 0;
|
|
@@ -1020,7 +1075,7 @@ export class OwnedUiSessionShell {
|
|
|
1020
1075
|
}
|
|
1021
1076
|
armedAt = now;
|
|
1022
1077
|
this.runtime.requestRender();
|
|
1023
|
-
//
|
|
1078
|
+
// Invariant: the presented screen owns the chord, so the pinned shell never sees a
|
|
1024
1079
|
// stray interrupt while it is up.
|
|
1025
1080
|
return { consume: true };
|
|
1026
1081
|
});
|
|
@@ -1034,7 +1089,7 @@ export class OwnedUiSessionShell {
|
|
|
1034
1089
|
this.#dialogId = undefined;
|
|
1035
1090
|
this.#dialogSource = undefined;
|
|
1036
1091
|
};
|
|
1037
|
-
//
|
|
1092
|
+
// Compatibility: fullscreen Pi owns a fallback text-selection layer before focused overlay
|
|
1038
1093
|
// components see pointer input. Route every mouse report to the owned screen
|
|
1039
1094
|
// at the pre-input boundary so dropdowns, value hover, and numeric +/- work,
|
|
1040
1095
|
// and consume even unhandled reports so settings content is never selected.
|
|
@@ -1072,7 +1127,12 @@ export class OwnedUiSessionShell {
|
|
|
1072
1127
|
closeSurface();
|
|
1073
1128
|
void this.shutdown();
|
|
1074
1129
|
});
|
|
1075
|
-
this.#dialogHandle = this.runtime.showOverlay(component, {
|
|
1130
|
+
this.#dialogHandle = this.runtime.showOverlay(component, {
|
|
1131
|
+
width: "100%",
|
|
1132
|
+
maxHeight: "100%",
|
|
1133
|
+
anchor: "top-left",
|
|
1134
|
+
inputCoordination: "owned",
|
|
1135
|
+
});
|
|
1076
1136
|
this.#dialogId = surface.id;
|
|
1077
1137
|
return { outcome: "completed", diagnostic: null };
|
|
1078
1138
|
}
|
|
@@ -1080,7 +1140,7 @@ export class OwnedUiSessionShell {
|
|
|
1080
1140
|
if (!this.runtime.active)
|
|
1081
1141
|
return;
|
|
1082
1142
|
if (dialog === null) {
|
|
1083
|
-
//
|
|
1143
|
+
// Invariant: locally owned routes (notably /settings) are independent of backend
|
|
1084
1144
|
// lifecycle/status events and remain open while an agent is working.
|
|
1085
1145
|
if (this.#dialogSource !== "backend")
|
|
1086
1146
|
return;
|
|
@@ -1108,7 +1168,12 @@ export class OwnedUiSessionShell {
|
|
|
1108
1168
|
this.#dialogSource = undefined;
|
|
1109
1169
|
},
|
|
1110
1170
|
});
|
|
1111
|
-
this.#dialogHandle = this.runtime.showOverlay(component, {
|
|
1171
|
+
this.#dialogHandle = this.runtime.showOverlay(component, {
|
|
1172
|
+
width: "70%",
|
|
1173
|
+
maxHeight: "80%",
|
|
1174
|
+
anchor: "center",
|
|
1175
|
+
inputCoordination: "owned",
|
|
1176
|
+
});
|
|
1112
1177
|
this.#dialogId = dialog.id;
|
|
1113
1178
|
}
|
|
1114
1179
|
async #slashCommand(text) {
|
|
@@ -1120,7 +1185,7 @@ export class OwnedUiSessionShell {
|
|
|
1120
1185
|
return this.#openOwnedRoute(name);
|
|
1121
1186
|
if (isWorkflowRoute(name))
|
|
1122
1187
|
return this.runWorkflow({ command: name, argument });
|
|
1123
|
-
//
|
|
1188
|
+
// Compatibility: unknown slash input, prompt templates, skills, and extension commands remain Pi prompt input.
|
|
1124
1189
|
this.root.editor.addToHistory(text);
|
|
1125
1190
|
this.root.resumeViewportFollowing();
|
|
1126
1191
|
return this.#execute({
|
|
@@ -1259,14 +1324,24 @@ export class OwnedUiSessionShell {
|
|
|
1259
1324
|
return `pi-shell-${prefix}-${this.#sequence}`;
|
|
1260
1325
|
}
|
|
1261
1326
|
}
|
|
1262
|
-
|
|
1263
|
-
return
|
|
1327
|
+
function isCoalescedStreamBlock(kind) {
|
|
1328
|
+
return kind === "assistant" || kind === "thinking" || kind === "tool-call" || kind === "tool-result";
|
|
1329
|
+
}
|
|
1330
|
+
export function formatSessionResumeCommand(metadata) {
|
|
1331
|
+
const args = [PRODUCT_TEXT.commandName];
|
|
1332
|
+
if (!metadata.usesDefaultSessionDir)
|
|
1333
|
+
args.push("--session-dir", quoteCommandArgument(metadata.sessionDir));
|
|
1334
|
+
args.push("--session", metadata.sessionId);
|
|
1335
|
+
return args.join(" ");
|
|
1264
1336
|
}
|
|
1265
1337
|
export function quoteCommandArgument(value) {
|
|
1266
|
-
if (
|
|
1267
|
-
return
|
|
1338
|
+
if (value.length > 0 && !/[^a-zA-Z0-9_\-./~:@]/.test(value))
|
|
1339
|
+
return value;
|
|
1268
1340
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
1269
1341
|
}
|
|
1342
|
+
function dim(value) {
|
|
1343
|
+
return `\u001b[2m${value}\u001b[22m`;
|
|
1344
|
+
}
|
|
1270
1345
|
function modelReference(model) {
|
|
1271
1346
|
if (typeof model !== "object" || model === null)
|
|
1272
1347
|
return "";
|
|
@@ -24,11 +24,16 @@ export declare class SessionViewportController {
|
|
|
24
24
|
get config(): OwnedUiViewportSettings;
|
|
25
25
|
get editorPointerSelecting(): boolean;
|
|
26
26
|
get transcriptPointerSelecting(): boolean;
|
|
27
|
+
get hasSelection(): boolean;
|
|
28
|
+
get selectionRevision(): number;
|
|
29
|
+
get presentationRevision(): number;
|
|
30
|
+
get frame(): TranscriptViewportFrame | null;
|
|
27
31
|
setEditorPointerFrame(frame: {
|
|
28
32
|
readonly rowStart: number;
|
|
29
33
|
readonly rowEnd: number;
|
|
30
34
|
} | undefined): void;
|
|
31
35
|
compose(input: TranscriptViewportFrameInput): TranscriptViewportFrame;
|
|
36
|
+
composeDockOnly(dockRows: readonly string[], width: number, height: number): TranscriptViewportFrame | null;
|
|
32
37
|
/** Returns true when document wrapping may have changed. */
|
|
33
38
|
setConfig(config: OwnedUiViewportSettings): boolean;
|
|
34
39
|
resumeFollowing(): void;
|
|
@@ -16,21 +16,26 @@ export class SessionViewportController {
|
|
|
16
16
|
scrollbarSpeed: "normal",
|
|
17
17
|
};
|
|
18
18
|
#dragGrabOffset = null;
|
|
19
|
-
|
|
19
|
+
// Invariant: a left-button sequence begun in dock chrome remains owned by the dock.
|
|
20
20
|
#dockPointerSuppressed = false;
|
|
21
|
-
|
|
21
|
+
// Invariant: editor selection remains true only while its left button is held.
|
|
22
22
|
#editorPointerSelecting = false;
|
|
23
|
-
|
|
23
|
+
// Invariant: pointer routing uses the most recently composed editor rows.
|
|
24
24
|
#editorPointerFrame;
|
|
25
25
|
#selectionAutoScrollTimer;
|
|
26
26
|
#selectionAutoScrollPointer;
|
|
27
27
|
#pointerPosition;
|
|
28
28
|
#hoveredHyperlinkKey;
|
|
29
|
+
#lastRequestedSelectionRevision = -1;
|
|
30
|
+
#presentationRevision = 0;
|
|
29
31
|
#activityTimer;
|
|
30
32
|
constructor(options) {
|
|
31
33
|
this.#enabled = options.enabled;
|
|
32
34
|
this.#editor = options.editor;
|
|
33
|
-
this.#requestRender =
|
|
35
|
+
this.#requestRender = force => {
|
|
36
|
+
this.#presentationRevision += 1;
|
|
37
|
+
options.requestRender(force);
|
|
38
|
+
};
|
|
34
39
|
this.#hasEditorLinks = options.hasEditorLinks ?? (() => false);
|
|
35
40
|
}
|
|
36
41
|
get config() {
|
|
@@ -42,16 +47,36 @@ export class SessionViewportController {
|
|
|
42
47
|
get transcriptPointerSelecting() {
|
|
43
48
|
return this.#viewport.selectionActive;
|
|
44
49
|
}
|
|
50
|
+
get hasSelection() {
|
|
51
|
+
return this.#viewport.hasSelection;
|
|
52
|
+
}
|
|
53
|
+
get selectionRevision() {
|
|
54
|
+
return this.#viewport.selectionRevision;
|
|
55
|
+
}
|
|
56
|
+
get presentationRevision() {
|
|
57
|
+
return this.#presentationRevision;
|
|
58
|
+
}
|
|
59
|
+
get frame() {
|
|
60
|
+
return this.#viewport.frame;
|
|
61
|
+
}
|
|
45
62
|
setEditorPointerFrame(frame) {
|
|
46
63
|
this.#editorPointerFrame = frame;
|
|
47
64
|
}
|
|
48
65
|
compose(input) {
|
|
49
66
|
this.#viewport.setConfig(this.#config);
|
|
67
|
+
const selectionRevision = this.#viewport.selectionRevision;
|
|
50
68
|
const frame = this.#viewport.compose(input);
|
|
69
|
+
// Concurrency: if selection changed during composition, this frame truthfully carries
|
|
70
|
+
// the older revision and exactly one ordinary follow-up render publishes the latest.
|
|
71
|
+
if (this.#viewport.selectionRevision !== selectionRevision
|
|
72
|
+
&& this.#lastRequestedSelectionRevision < this.#viewport.selectionRevision) {
|
|
73
|
+
this.#lastRequestedSelectionRevision = this.#viewport.selectionRevision;
|
|
74
|
+
this.#requestRender();
|
|
75
|
+
}
|
|
51
76
|
if (this.#pointerPosition !== undefined && !this.#viewport.selectionActive && !this.#editorPointerSelecting) {
|
|
52
77
|
const next = this.#hyperlinkKeyAt(frame, this.#pointerPosition.column, this.#pointerPosition.row);
|
|
53
78
|
if (this.#hoveredHyperlinkKey !== undefined && next !== this.#hoveredHyperlinkKey) {
|
|
54
|
-
// Windows Terminal can leave its solid native-hover underline behind
|
|
79
|
+
// Platform: Windows Terminal can leave its solid native-hover underline behind
|
|
55
80
|
// when a following viewport moves the link away from a stationary pointer.
|
|
56
81
|
this.#requestRender(true);
|
|
57
82
|
}
|
|
@@ -59,6 +84,9 @@ export class SessionViewportController {
|
|
|
59
84
|
}
|
|
60
85
|
return frame;
|
|
61
86
|
}
|
|
87
|
+
composeDockOnly(dockRows, width, height) {
|
|
88
|
+
return this.#viewport.composeDockOnly(dockRows, width, height);
|
|
89
|
+
}
|
|
62
90
|
/** Returns true when document wrapping may have changed. */
|
|
63
91
|
setConfig(config) {
|
|
64
92
|
const appearanceChanged = config.scrollbarAppearance !== this.#config.scrollbarAppearance;
|
|
@@ -84,6 +112,7 @@ export class SessionViewportController {
|
|
|
84
112
|
this.#requestRender();
|
|
85
113
|
}
|
|
86
114
|
reset() {
|
|
115
|
+
this.#presentationRevision += 1;
|
|
87
116
|
this.#clearActivityTimer();
|
|
88
117
|
this.#stopSelectionAutoScroll();
|
|
89
118
|
this.#editorPointerSelecting = false;
|
|
@@ -93,6 +122,7 @@ export class SessionViewportController {
|
|
|
93
122
|
this.#viewport.reset();
|
|
94
123
|
}
|
|
95
124
|
clearPointerState() {
|
|
125
|
+
this.#presentationRevision += 1;
|
|
96
126
|
this.#clearActivityTimer();
|
|
97
127
|
this.#dragGrabOffset = null;
|
|
98
128
|
this.#dockPointerSuppressed = false;
|
|
@@ -107,15 +137,15 @@ export class SessionViewportController {
|
|
|
107
137
|
handlePreInput(data, allowWheel = true, now = Date.now()) {
|
|
108
138
|
if (!this.#enabled)
|
|
109
139
|
return { data, consumed: false };
|
|
110
|
-
// Pi components share one keybinding manager. Restore bare A1's aliases
|
|
140
|
+
// Compatibility: Pi components share one keybinding manager. Restore bare A1's aliases
|
|
111
141
|
// before the focused vanilla editor handles this input.
|
|
112
142
|
this.#editor.activateKeybindings();
|
|
113
|
-
//
|
|
143
|
+
// Platform: handle the physical paste chord at the pre-input boundary. Windows
|
|
114
144
|
// terminals vary between forwarding Ctrl+V and terminal-owned bracketed paste.
|
|
115
145
|
if (this.#editor.matchesTerminalKey(data, "ctrl+v") && this.#editor.pasteClipboard()) {
|
|
116
146
|
return { data: "", consumed: true };
|
|
117
147
|
}
|
|
118
|
-
// URL chip deletion must overwrite terminal link cells in the same frame.
|
|
148
|
+
// Platform: URL chip deletion must overwrite terminal link cells in the same frame.
|
|
119
149
|
if (EDITOR_LINK_DELETION_INPUTS.has(data) && this.#hasEditorLinks())
|
|
120
150
|
this.#requestRender(true);
|
|
121
151
|
if (this.#viewport.frame === null)
|
|
@@ -180,7 +210,7 @@ export class SessionViewportController {
|
|
|
180
210
|
if (!this.#viewport.selectionActive && !this.#editorPointerSelecting) {
|
|
181
211
|
const nextHyperlink = this.#hyperlinkKeyAt(frame, event.column, event.row);
|
|
182
212
|
if (this.#hoveredHyperlinkKey !== undefined && nextHyperlink !== this.#hoveredHyperlinkKey) {
|
|
183
|
-
//
|
|
213
|
+
// Platform: a forced redraw overwrites Windows Terminal's cached solid hover
|
|
184
214
|
// underline exactly once when the pointer leaves or changes links.
|
|
185
215
|
forceRepaint = true;
|
|
186
216
|
}
|
|
@@ -308,7 +338,7 @@ export class SessionViewportController {
|
|
|
308
338
|
}
|
|
309
339
|
if (this.#viewport.releaseSelection()) {
|
|
310
340
|
this.#stopSelectionAutoScroll();
|
|
311
|
-
//
|
|
341
|
+
// Platform: restore OSC 8 links only after the held-button selection paint has
|
|
312
342
|
// ended, then overwrite any terminal-cached hover cells immediately.
|
|
313
343
|
forceRepaint = true;
|
|
314
344
|
repaint = true;
|
|
@@ -329,8 +359,10 @@ export class SessionViewportController {
|
|
|
329
359
|
});
|
|
330
360
|
if (activity)
|
|
331
361
|
this.#scheduleActivityExpiry();
|
|
332
|
-
if (repaint)
|
|
362
|
+
if (repaint) {
|
|
363
|
+
this.#lastRequestedSelectionRevision = this.#viewport.selectionRevision;
|
|
333
364
|
this.#requestRender(forceRepaint);
|
|
365
|
+
}
|
|
334
366
|
return routed;
|
|
335
367
|
}
|
|
336
368
|
#hyperlinkKeyAt(frame, column, row) {
|
|
@@ -370,6 +402,7 @@ export class SessionViewportController {
|
|
|
370
402
|
this.#stopSelectionAutoScroll();
|
|
371
403
|
return;
|
|
372
404
|
}
|
|
405
|
+
this.#lastRequestedSelectionRevision = this.#viewport.selectionRevision;
|
|
373
406
|
this.#requestRender();
|
|
374
407
|
this.#scheduleActivityExpiry();
|
|
375
408
|
this.#selectionAutoScrollTimer = setTimeout(() => this.#selectionAutoScrollTick(), SELECTION_AUTO_SCROLL_INTERVAL_MS);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 33 ms (30.3 fps): baseline chunks arrive every 5 ms, while this cadence
|
|
3
|
+
* retains immediate first paint/input and explicit final-state flushing.
|
|
4
|
+
*/
|
|
5
|
+
export declare const STREAM_PRESENTATION_INTERVAL_MS = 33;
|
|
6
|
+
export interface StreamPresentationScheduler {
|
|
7
|
+
now(): number;
|
|
8
|
+
setTimeout(callback: () => void, delayMs: number): ReturnType<typeof setTimeout>;
|
|
9
|
+
clearTimeout(timer: ReturnType<typeof setTimeout>): void;
|
|
10
|
+
}
|
|
11
|
+
/** Bounds terminal presentation requests while semantic root state remains current. */
|
|
12
|
+
export declare class StreamPresentationCoalescer {
|
|
13
|
+
#private;
|
|
14
|
+
readonly present: () => void;
|
|
15
|
+
readonly intervalMs: number;
|
|
16
|
+
readonly scheduler: StreamPresentationScheduler;
|
|
17
|
+
constructor(present: () => void, intervalMs?: number, scheduler?: StreamPresentationScheduler);
|
|
18
|
+
get pending(): boolean;
|
|
19
|
+
request(): void;
|
|
20
|
+
/** Cancels a superseded stream frame; the immediate input/overlay/resize path renders current state. */
|
|
21
|
+
preempt(): void;
|
|
22
|
+
/** Records an immediate input/status/overlay frame and cancels an obsolete stream timer. */
|
|
23
|
+
noteImmediatePresentation(): void;
|
|
24
|
+
/** Presents current semantic state immediately regardless of pending stream work. */
|
|
25
|
+
presentImmediate(): void;
|
|
26
|
+
/** Presents the newest semantic state immediately only when a stream frame is pending. */
|
|
27
|
+
flush(): void;
|
|
28
|
+
dispose(): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 33 ms (30.3 fps): baseline chunks arrive every 5 ms, while this cadence
|
|
3
|
+
* retains immediate first paint/input and explicit final-state flushing.
|
|
4
|
+
*/
|
|
5
|
+
export const STREAM_PRESENTATION_INTERVAL_MS = 33;
|
|
6
|
+
const SYSTEM_SCHEDULER = {
|
|
7
|
+
now: Date.now,
|
|
8
|
+
setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
9
|
+
clearTimeout: timer => clearTimeout(timer),
|
|
10
|
+
};
|
|
11
|
+
/** Bounds terminal presentation requests while semantic root state remains current. */
|
|
12
|
+
export class StreamPresentationCoalescer {
|
|
13
|
+
present;
|
|
14
|
+
intervalMs;
|
|
15
|
+
scheduler;
|
|
16
|
+
#timer;
|
|
17
|
+
#lastPresentationAt = Number.NEGATIVE_INFINITY;
|
|
18
|
+
#disposed = false;
|
|
19
|
+
constructor(present, intervalMs = STREAM_PRESENTATION_INTERVAL_MS, scheduler = SYSTEM_SCHEDULER) {
|
|
20
|
+
this.present = present;
|
|
21
|
+
this.intervalMs = intervalMs;
|
|
22
|
+
this.scheduler = scheduler;
|
|
23
|
+
if (!Number.isSafeInteger(intervalMs) || intervalMs < 1)
|
|
24
|
+
throw new RangeError("stream presentation interval must be positive");
|
|
25
|
+
}
|
|
26
|
+
get pending() { return this.#timer !== undefined; }
|
|
27
|
+
request() {
|
|
28
|
+
if (this.#disposed || this.#timer !== undefined)
|
|
29
|
+
return;
|
|
30
|
+
const elapsed = this.scheduler.now() - this.#lastPresentationAt;
|
|
31
|
+
if (elapsed >= this.intervalMs) {
|
|
32
|
+
this.#presentNow();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this.#timer = this.scheduler.setTimeout(() => {
|
|
36
|
+
this.#timer = undefined;
|
|
37
|
+
this.#presentNow();
|
|
38
|
+
}, Math.max(0, this.intervalMs - elapsed));
|
|
39
|
+
}
|
|
40
|
+
/** Cancels a superseded stream frame; the immediate input/overlay/resize path renders current state. */
|
|
41
|
+
preempt() {
|
|
42
|
+
if (this.#timer === undefined)
|
|
43
|
+
return;
|
|
44
|
+
this.scheduler.clearTimeout(this.#timer);
|
|
45
|
+
this.#timer = undefined;
|
|
46
|
+
}
|
|
47
|
+
/** Records an immediate input/status/overlay frame and cancels an obsolete stream timer. */
|
|
48
|
+
noteImmediatePresentation() {
|
|
49
|
+
if (this.#disposed)
|
|
50
|
+
return;
|
|
51
|
+
this.preempt();
|
|
52
|
+
this.#lastPresentationAt = this.scheduler.now();
|
|
53
|
+
}
|
|
54
|
+
/** Presents current semantic state immediately regardless of pending stream work. */
|
|
55
|
+
presentImmediate() {
|
|
56
|
+
if (this.#disposed)
|
|
57
|
+
return;
|
|
58
|
+
this.noteImmediatePresentation();
|
|
59
|
+
this.present();
|
|
60
|
+
}
|
|
61
|
+
/** Presents the newest semantic state immediately only when a stream frame is pending. */
|
|
62
|
+
flush() {
|
|
63
|
+
if (this.#disposed || this.#timer === undefined)
|
|
64
|
+
return;
|
|
65
|
+
this.scheduler.clearTimeout(this.#timer);
|
|
66
|
+
this.#timer = undefined;
|
|
67
|
+
this.#presentNow();
|
|
68
|
+
}
|
|
69
|
+
dispose() {
|
|
70
|
+
this.preempt();
|
|
71
|
+
this.#disposed = true;
|
|
72
|
+
}
|
|
73
|
+
#presentNow() {
|
|
74
|
+
if (this.#disposed)
|
|
75
|
+
return;
|
|
76
|
+
this.#lastPresentationAt = this.scheduler.now();
|
|
77
|
+
this.present();
|
|
78
|
+
}
|
|
79
|
+
}
|