@timurproko/a1 0.2.7-dev.809 → 0.2.7-dev.820
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/README.md +4 -2
- package/dist/app/session-shell/resident-tab-coordinator.d.ts +6 -9
- package/dist/app/session-shell/resident-tab-coordinator.js +9 -52
- package/dist/app/session-shell/response-copy-transport.d.ts +13 -0
- package/dist/app/session-shell/response-copy-transport.js +16 -7
- package/dist/app/session-shell/session-shell-root.d.ts +1 -2
- package/dist/app/session-shell/session-shell.js +2 -2
- package/dist/app/session-shell/session-viewport-controller.js +6 -4
- package/dist/composition/owned-ui.js +1 -2
- package/dist/features/owned-ui/settings-app.js +1 -1
- package/dist/foundation/release/progress-palette.generated.d.ts +2 -2
- package/dist/foundation/release/progress-palette.generated.js +2 -2
- package/dist/foundation/resident-tabs/bridge-client.d.ts +1 -1
- package/dist/foundation/resident-tabs/bridge-client.js +1 -1
- package/dist/foundation/resident-tabs/settings.d.ts +0 -1
- package/dist/foundation/resident-tabs/settings.js +0 -2
- package/dist/foundation/startup/startup-descriptor.js +2 -2
- package/dist/integrations/pi/components/resources/builtin-themes.d.ts +2 -2
- package/dist/integrations/pi/components/resources/builtin-themes.js +2 -2
- package/dist/integrations/pi/components/shell-extension-ui.js +79 -60
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +1 -1
- package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
- package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/session-footer.js +1 -1
- package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
- package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
- package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/status-indicator.js +1 -1
- package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
- package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/thinking-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +2 -4
- package/dist/integrations/pi/components/upstream/components/tool-execution.js +77 -98
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
- package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
- package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
- package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
- package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
- package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
- package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/theme/theme.js +1 -1
- package/dist/integrations/pi/engine/resources/changelog.json +4 -4
- package/dist/integrations/pi/startup-public.js +164 -150
- package/dist/integrations/pi/startup-public.manifest.json +351 -310
- package/dist/integrations/pi/tui-runtime/adapter.js +4 -0
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +5 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
- package/dist/native/linux-x64/terminal-host.node +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
- package/dist/native/win32-x64/terminal-host.node +0 -0
- package/dist/runtime-payload-inventory.json +5181 -5369
- package/dist/ui/components/dialog-panel.js +1 -1
- package/dist/ui/components/prompt-input.d.ts +2 -2
- package/dist/ui/components/prompt-input.js +2 -2
- package/dist/ui/components/submitted-prompt.d.ts +2 -2
- package/dist/ui/components/submitted-prompt.js +3 -3
- package/dist/ui/components/transcript-viewport.d.ts +1 -1
- package/dist/ui/components/transcript-viewport.js +151 -43
- package/dist/ui/settings/declarations.d.ts +13 -9
- package/dist/ui/settings/declarations.js +10 -7
- package/dist/ui/settings/migrations.js +7 -0
- package/docs/architecture/resident-tabs.md +6 -6
- package/docs/architecture/response-copy-delivery.md +1 -1
- package/docs/architecture/toolchain.md +2 -2
- package/package.json +5 -5
|
@@ -69,7 +69,7 @@ export function renderSteppedDialogPanel(state, width, theme) {
|
|
|
69
69
|
});
|
|
70
70
|
const input = state.input === undefined
|
|
71
71
|
? []
|
|
72
|
-
: [...renderInputRow(state.input, width, { ruled: false, promptGlyph: "
|
|
72
|
+
: [...renderInputRow(state.input, width, { ruled: false, promptGlyph: "", theme }).lines, ""];
|
|
73
73
|
const step = `(step ${state.step}/${state.steps})`;
|
|
74
74
|
const title = `${state.title} ${step}`;
|
|
75
75
|
const paintedTitle = `${theme.bold(theme.fg("accent", state.title))} ${theme.fg("dim", step)}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type UiTheme } from "./theme.js";
|
|
2
|
-
export declare const PROMPT_GLYPH = "
|
|
3
|
-
export declare const PROMPT_PREFIX_WIDTH =
|
|
2
|
+
export declare const PROMPT_GLYPH = "";
|
|
3
|
+
export declare const PROMPT_PREFIX_WIDTH = 0;
|
|
4
4
|
type RuleTheme = Pick<UiTheme, "fg">;
|
|
5
5
|
export declare function promptRuleText(text: string, theme: RuleTheme): string;
|
|
6
6
|
export declare function promptRule(width: number, theme: RuleTheme): string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { displayWidth, truncateToWidth } from "./text.js";
|
|
2
2
|
import { PLAIN_THEME } from "./theme.js";
|
|
3
|
-
export const PROMPT_GLYPH = "
|
|
4
|
-
export const PROMPT_PREFIX_WIDTH =
|
|
3
|
+
export const PROMPT_GLYPH = "";
|
|
4
|
+
export const PROMPT_PREFIX_WIDTH = 0;
|
|
5
5
|
export function promptRuleText(text, theme) {
|
|
6
6
|
return theme.fg("border", text);
|
|
7
7
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export type UiTimeFormat = "20:00" | "8:00 PM";
|
|
2
2
|
/** Deterministic local hour/minute presentation shared by A1-owned time labels. */
|
|
3
3
|
export declare function formatLocalTime(source: Date | number, format?: UiTimeFormat): string | null;
|
|
4
|
-
/** Visible width of the submitted-prompt prefix (
|
|
5
|
-
export declare const SUBMITTED_PROMPT_PREFIX_WIDTH =
|
|
4
|
+
/** Visible width of the submitted-prompt prefix (` ❯ `). */
|
|
5
|
+
export declare const SUBMITTED_PROMPT_PREFIX_WIDTH = 3;
|
|
6
6
|
export interface SubmittedPromptStyle {
|
|
7
7
|
readonly prefix?: (text: string) => string;
|
|
8
8
|
readonly timestamp?: (text: string) => string;
|
|
@@ -10,8 +10,8 @@ export function formatLocalTime(source, format = "8:00 PM") {
|
|
|
10
10
|
return `${String(hour).padStart(2, "0")}:${minute}`;
|
|
11
11
|
return `${hour % 12 || 12}:${minute} ${hour < 12 ? "AM" : "PM"}`;
|
|
12
12
|
}
|
|
13
|
-
/** Visible width of the submitted-prompt prefix (
|
|
14
|
-
export const SUBMITTED_PROMPT_PREFIX_WIDTH =
|
|
13
|
+
/** Visible width of the submitted-prompt prefix (` ❯ `). */
|
|
14
|
+
export const SUBMITTED_PROMPT_PREFIX_WIDTH = 3;
|
|
15
15
|
const TIMESTAMP_MARGIN = 3;
|
|
16
16
|
const MIN_USEFUL_CONTENT_WIDTH = 8;
|
|
17
17
|
/** Formats transcript metadata, never render time, in the active A1 time format. */
|
|
@@ -42,7 +42,7 @@ export function composeSubmittedPromptRows(contentRows, width, source, style = {
|
|
|
42
42
|
if (width <= 0)
|
|
43
43
|
return [];
|
|
44
44
|
const layout = submittedPromptLayout(width, source, format);
|
|
45
|
-
const prefix = (style.prefix ?? identity)("❯ ");
|
|
45
|
+
const prefix = (style.prefix ?? identity)(" ❯ ");
|
|
46
46
|
const rows = (contentRows.length === 0 ? [""] : contentRows).map((row, index) => {
|
|
47
47
|
const content = truncateToWidth(row, layout.contentWidth);
|
|
48
48
|
return `${index === 0 ? prefix : " ".repeat(SUBMITTED_PROMPT_PREFIX_WIDTH)}${content}`;
|
|
@@ -178,7 +178,7 @@ export declare class TranscriptViewport {
|
|
|
178
178
|
releaseSelection(): boolean;
|
|
179
179
|
clearSelection(): boolean;
|
|
180
180
|
selectedText(): string | null;
|
|
181
|
-
/** Captures
|
|
181
|
+
/** Captures selected text, including document rows of a visible selection scrolled off-screen. */
|
|
182
182
|
captureSelectedText(): SelectionCopySnapshot | null;
|
|
183
183
|
scrollBy(lines: number, now?: number): boolean;
|
|
184
184
|
scrollTo(position: number, now?: number): boolean;
|
|
@@ -4,6 +4,10 @@ import { backgroundSgrSpan, overlaySpan } from "./spans.js";
|
|
|
4
4
|
import { faint, truncateToWidth, displayWidth, stripAnsi } from "./text.js";
|
|
5
5
|
import { extendTextSelection, orderedTextSelection, pressTextSelection, releaseTextSelection, textSelectionLineExtendColumn, textSelectionPointAt, textSelectionText, usefulTextLineContent, } from "./text-selection.js";
|
|
6
6
|
const CONTROL_STYLE_RESET = "\u001b]8;;\u001b\\\u001b[0m";
|
|
7
|
+
// Protocol: upstream OSC 133 zones are layout annotations, not fullscreen cursor commands.
|
|
8
|
+
// Styling/padding can move them away from the row prefix Pi strips; 133;A then advances a real
|
|
9
|
+
// terminal parser to a fresh line. The custom viewport owns prompt anchors separately.
|
|
10
|
+
const VIEWPORT_PROMPT_ZONES = /\u001b\]133;[ABC](?:\u0007|\u001b\\)/gu;
|
|
7
11
|
// Invariant: scrollbar chrome closes source links and decoration without clearing
|
|
8
12
|
// the row surface painted beneath its dedicated, non-semantic gutter.
|
|
9
13
|
const GUTTER_DECORATION_RESET = "\u001b]8;;\u001b\\\u001b[22;23;24;25;27;28;29;39;54;55m";
|
|
@@ -11,6 +15,19 @@ const IDENTITY_ROW = (row) => row;
|
|
|
11
15
|
function documentSelectionAnchor(row, source, liveFromBottom) {
|
|
12
16
|
return { kind: "document", row, source, identity: stripAnsi(source).trimEnd(), liveFromBottom };
|
|
13
17
|
}
|
|
18
|
+
function sameSelectionRowAnchor(left, right) {
|
|
19
|
+
if (left.kind !== right.kind)
|
|
20
|
+
return false;
|
|
21
|
+
if (left.kind === "document" && right.kind === "document") {
|
|
22
|
+
return left.row === right.row && left.source === right.source && left.liveFromBottom === right.liveFromBottom;
|
|
23
|
+
}
|
|
24
|
+
if (left.kind === "screen" && right.kind === "screen") {
|
|
25
|
+
return left.row === right.row && left.source === right.source;
|
|
26
|
+
}
|
|
27
|
+
if (left.kind === "transient" && right.kind === "transient")
|
|
28
|
+
return left.fromBottom === right.fromBottom;
|
|
29
|
+
return left.kind === "dock" && right.kind === "dock" && left.fromBottom === right.fromBottom;
|
|
30
|
+
}
|
|
14
31
|
function viewportSelectionAnchor(row, source, selectableDocumentRowCount, documentRowCount) {
|
|
15
32
|
return row < selectableDocumentRowCount
|
|
16
33
|
? documentSelectionAnchor(row, source)
|
|
@@ -136,7 +153,12 @@ export class TranscriptViewport {
|
|
|
136
153
|
this.scrollBy(1, now);
|
|
137
154
|
else if (autoScroll && frameRow <= 1 && this.#scrollTop > 0)
|
|
138
155
|
this.scrollBy(-1, now);
|
|
139
|
-
const
|
|
156
|
+
const pointerLine = clamp(frameRow - 1, 0, this.#selectionRows.length - 1);
|
|
157
|
+
// Invariant: a pinned prompt alias is chrome; an upward drag reaching it keeps the pointer
|
|
158
|
+
// column on the first visible content row.
|
|
159
|
+
const line = pointerLine < this.#firstSelectableRow && pointerLine < selection.anchor.line
|
|
160
|
+
? this.#firstSelectableRow
|
|
161
|
+
: pointerLine;
|
|
140
162
|
const selectionWidth = line < (this.#frame?.hits.viewportHeight ?? 0)
|
|
141
163
|
? this.#contentWidth
|
|
142
164
|
: this.#selectionWidth;
|
|
@@ -147,12 +169,13 @@ export class TranscriptViewport {
|
|
|
147
169
|
? { line, column: targetColumn }
|
|
148
170
|
: textSelectionPointAt(line, targetColumn, this.#selectionRows[line] ?? "");
|
|
149
171
|
const next = extendTextSelection(selection, point);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
172
|
+
const headAnchor = this.#selectionAnchorAt(line);
|
|
173
|
+
if (headAnchor === undefined || this.#selectionAnchors === undefined) {
|
|
174
|
+
this.clearSelection();
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
// Invariant: edge auto-scroll changes the document row under a still pointer; that advances the head.
|
|
178
|
+
if (next !== selection || !sameSelectionRowAnchor(this.#selectionAnchors.head, headAnchor)) {
|
|
156
179
|
this.#selection = next;
|
|
157
180
|
this.#selectionAnchors = { ...this.#selectionAnchors, head: headAnchor };
|
|
158
181
|
this.#selectionRevision += 1;
|
|
@@ -180,25 +203,66 @@ export class TranscriptViewport {
|
|
|
180
203
|
return true;
|
|
181
204
|
}
|
|
182
205
|
selectedText() {
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
206
|
+
const visible = this.#visibleSelection();
|
|
207
|
+
if (visible === undefined)
|
|
185
208
|
return null;
|
|
186
|
-
const
|
|
209
|
+
const offscreen = offscreenSelectionRows(this.#selection, this.#selectionAnchors, this.#scrollTop, this.#firstSelectableRow, this.#viewportHeight, this.#documentRows.length);
|
|
210
|
+
// Rationale: edge auto-scroll grows a drag beyond one screen. Paint stays clipped to the
|
|
211
|
+
// frame, while copy reads the selected document rows past either viewport edge and beneath
|
|
212
|
+
// the floating bottom control.
|
|
213
|
+
const selectionRows = [];
|
|
214
|
+
const rowAnchors = [];
|
|
215
|
+
const controlLine = (this.#frame?.hits.bottom?.row ?? 0) - 1;
|
|
216
|
+
const pushDocumentRow = (row) => {
|
|
217
|
+
const source = this.#documentRows[row] ?? "";
|
|
218
|
+
selectionRows.push(truncateToWidth(source, this.#contentWidth));
|
|
219
|
+
rowAnchors.push(documentSelectionAnchor(row, source));
|
|
220
|
+
};
|
|
221
|
+
const pushBelow = () => {
|
|
222
|
+
for (let row = offscreen.below?.from ?? 0; row <= (offscreen.below?.to ?? -1); row += 1)
|
|
223
|
+
pushDocumentRow(row);
|
|
224
|
+
};
|
|
225
|
+
for (let row = offscreen.above?.from ?? 0; row <= (offscreen.above?.to ?? -1); row += 1)
|
|
226
|
+
pushDocumentRow(row);
|
|
227
|
+
if (offscreen.belowFirst)
|
|
228
|
+
pushBelow();
|
|
229
|
+
for (let line = visible.start.line; line <= visible.end.line; line += 1) {
|
|
187
230
|
const anchor = this.#selectionRowAnchors[line];
|
|
231
|
+
if (line === controlLine && anchor?.kind === "document")
|
|
232
|
+
pushDocumentRow(anchor.row);
|
|
233
|
+
else {
|
|
234
|
+
selectionRows.push(this.#selectionRows[line] ?? "");
|
|
235
|
+
rowAnchors.push(anchor);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (!offscreen.belowFirst)
|
|
239
|
+
pushBelow();
|
|
240
|
+
const offscreenStart = offscreen.above !== undefined || offscreen.belowFirst === true;
|
|
241
|
+
const offscreenEnd = offscreen.below !== undefined && !offscreen.belowFirst;
|
|
242
|
+
const selection = {
|
|
243
|
+
...visible,
|
|
244
|
+
start: { line: 0, column: offscreenStart ? offscreen.startColumn ?? 0 : visible.start.column },
|
|
245
|
+
end: {
|
|
246
|
+
line: selectionRows.length - 1,
|
|
247
|
+
column: offscreenEnd ? offscreen.endColumn ?? Number.MAX_SAFE_INTEGER : visible.end.column,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
const copiedLines = Array.from({ length: Math.max(0, selection.end.line - selection.start.line + 1) }, (_value, index) => selection.start.line + index).filter(line => {
|
|
251
|
+
const anchor = rowAnchors[line];
|
|
188
252
|
return anchor?.kind === "document" && anchor.row < this.#selectableDocumentRowCount
|
|
189
253
|
|| anchor?.kind === "dock" && this.#copyableDockRows.has(anchor.fromBottom);
|
|
190
254
|
});
|
|
191
255
|
if (copiedLines.length === 0)
|
|
192
256
|
return null;
|
|
193
|
-
const transcriptLines = copiedLines.filter(line =>
|
|
257
|
+
const transcriptLines = copiedLines.filter(line => rowAnchors[line]?.kind === "document");
|
|
194
258
|
const prompt = transcriptLines.length === 0 ? undefined : this.#promptAnchors.find(candidate => transcriptLines.every(line => {
|
|
195
|
-
const anchor =
|
|
259
|
+
const anchor = rowAnchors[line];
|
|
196
260
|
return anchor?.kind === "document" && anchor.row >= candidate.firstRow && anchor.row <= candidate.lastRow;
|
|
197
261
|
}));
|
|
198
262
|
const included = new Set(copiedLines);
|
|
199
|
-
const text = textSelectionText(selection,
|
|
200
|
-
const row =
|
|
201
|
-
const anchor =
|
|
263
|
+
const text = textSelectionText(selection, selectionRows, line => {
|
|
264
|
+
const row = selectionRows[line] ?? "";
|
|
265
|
+
const anchor = rowAnchors[line];
|
|
202
266
|
const dock = anchor?.kind === "dock" ? this.#copyableDockRows.get(anchor.fromBottom) : undefined;
|
|
203
267
|
if (prompt !== undefined && anchor?.kind === "document") {
|
|
204
268
|
const first = anchor.row === prompt.firstRow;
|
|
@@ -207,11 +271,12 @@ export class TranscriptViewport {
|
|
|
207
271
|
: row;
|
|
208
272
|
return selectionCopyLineContent(copyRow, first ? "first" : "continuation");
|
|
209
273
|
}
|
|
210
|
-
|
|
274
|
+
// Invariant: the dock editor has no prefix; transcript prompts still carry chrome.
|
|
275
|
+
return dock?.prompt !== undefined ? usefulTextLineContent(row, 1) : usefulTextLineContent(row);
|
|
211
276
|
}, line => included.has(line));
|
|
212
277
|
return text.length === 0 ? null : text;
|
|
213
278
|
}
|
|
214
|
-
/** Captures
|
|
279
|
+
/** Captures selected text, including document rows of a visible selection scrolled off-screen. */
|
|
215
280
|
captureSelectedText() {
|
|
216
281
|
if (!this.#copyableSelection)
|
|
217
282
|
return null;
|
|
@@ -362,7 +427,7 @@ export class TranscriptViewport {
|
|
|
362
427
|
contentLength: documentRows.length,
|
|
363
428
|
viewportHeight,
|
|
364
429
|
scroll: this.#scrollTop,
|
|
365
|
-
//
|
|
430
|
+
// Invariant: the rail keeps its top gap; the shell owns the spacing above the dock.
|
|
366
431
|
trackHeight: Math.max(0, viewportHeight - 1),
|
|
367
432
|
});
|
|
368
433
|
const presentation = scrollbarPresentation({
|
|
@@ -466,7 +531,7 @@ export class TranscriptViewport {
|
|
|
466
531
|
const bottomHovered = pointer !== undefined && pointer.row === bottomHit.row
|
|
467
532
|
&& pointer.column >= bottomHit.columnStart && pointer.column <= bottomHit.columnEnd;
|
|
468
533
|
control = { row, left, text: `${CONTROL_STYLE_RESET}${theme.bottomControl(label, bottomHovered)}` };
|
|
469
|
-
// Invariant: the control floats above selection;
|
|
534
|
+
// Invariant: the control floats above selection; copy restores the text beneath it.
|
|
470
535
|
selectionRows[row] = overlaySpan(padRowPreservingBackground(selectionRows[row] ?? "", width), left, left + labelWidth, " ".repeat(labelWidth));
|
|
471
536
|
}
|
|
472
537
|
}
|
|
@@ -504,7 +569,7 @@ export class TranscriptViewport {
|
|
|
504
569
|
// Invariant: paint the rail after selection over the gutter's continued row
|
|
505
570
|
// background. Source content and selection still end at the preceding cell.
|
|
506
571
|
let railCell = "";
|
|
507
|
-
if (row < viewportHeight && presentation.visible && geometry !== null
|
|
572
|
+
if (row > 0 && row < viewportHeight && presentation.visible && geometry !== null) {
|
|
508
573
|
const trackRow = row - 1;
|
|
509
574
|
const thumb = isThumbRow(geometry, trackRow);
|
|
510
575
|
const glyph = thumb ? presentation.thumbGlyph : presentation.trackGlyph;
|
|
@@ -526,7 +591,7 @@ export class TranscriptViewport {
|
|
|
526
591
|
return overlayWidth === 0 ? withControl : overlaySpan(padRowPreservingBackground(withControl, width), overlayLimit - overlayWidth, overlayLimit, `${GUTTER_DECORATION_RESET}${overlayText}`, { inheritStartStyle: true });
|
|
527
592
|
});
|
|
528
593
|
rowRecomputed ||= !final.reused;
|
|
529
|
-
frameRows[row] = final.value;
|
|
594
|
+
frameRows[row] = final.value.replace(VIEWPORT_PROMPT_ZONES, "");
|
|
530
595
|
(rowRecomputed ? recomputedRows : reusedRows).push(row + 1);
|
|
531
596
|
const state = `${width}\u0000${row}\u0000${painted}\u0000${rangeKey}\u0000${range === null ? "" : selectionPainterId}\u0000${railCell}\u0000${overlayText}\u0000${rowControl?.text ?? ""}`;
|
|
532
597
|
visibleStates.push(state);
|
|
@@ -627,19 +692,10 @@ export class TranscriptViewport {
|
|
|
627
692
|
|| !sameRectangle(previous.descriptor.transcript, expectedTranscript))
|
|
628
693
|
return null;
|
|
629
694
|
const descriptor = {
|
|
695
|
+
...previous.descriptor,
|
|
630
696
|
frameId: ++this.#frameId,
|
|
631
|
-
width: boundedWidth,
|
|
632
|
-
height: boundedHeight,
|
|
633
|
-
transcript: expectedTranscript,
|
|
634
|
-
dock: expectedDock,
|
|
635
697
|
previousDocumentRange: previous.descriptor.nextDocumentRange,
|
|
636
|
-
nextDocumentRange: previous.descriptor.nextDocumentRange,
|
|
637
698
|
previousFollowingEnd: previous.followingEnd,
|
|
638
|
-
followingEnd: previous.followingEnd,
|
|
639
|
-
transientRowCount: previous.descriptor.transientRowCount,
|
|
640
|
-
transientAlignmentGapRows: previous.descriptor.transientAlignmentGapRows,
|
|
641
|
-
bottomAlignedTailRowCount: previous.descriptor.bottomAlignedTailRowCount,
|
|
642
|
-
liveTailRows: previous.descriptor.liveTailRows,
|
|
643
699
|
verticalShiftRows: 0,
|
|
644
700
|
safeVerticalShift: false,
|
|
645
701
|
selectionRevision: this.#selectionRevision,
|
|
@@ -650,12 +706,8 @@ export class TranscriptViewport {
|
|
|
650
706
|
this.#selectionRows = [...this.#selectionRows.slice(0, viewportHeight), ...dock].slice(0, boundedHeight);
|
|
651
707
|
this.#copyableDockRows = copyableDockRowsBySuffix(dockRows, copyableDockRows);
|
|
652
708
|
const frame = {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
scrollTop: previous.scrollTop,
|
|
656
|
-
maxScroll: previous.maxScroll,
|
|
657
|
-
followingEnd: previous.followingEnd,
|
|
658
|
-
hits: previous.hits,
|
|
709
|
+
...previous,
|
|
710
|
+
rows: [...previous.rows.slice(0, viewportHeight), ...dock.map(row => row.replace(VIEWPORT_PROMPT_ZONES, ""))].slice(0, boundedHeight),
|
|
659
711
|
descriptor,
|
|
660
712
|
selectionDamage: {
|
|
661
713
|
revision: this.#selectionRevision,
|
|
@@ -710,7 +762,7 @@ export class TranscriptViewport {
|
|
|
710
762
|
const anchors = this.#selectionAnchors;
|
|
711
763
|
if (selection === undefined || anchors === undefined)
|
|
712
764
|
return;
|
|
713
|
-
const project = (anchor) => {
|
|
765
|
+
const project = (anchor, moving) => {
|
|
714
766
|
if (anchor.kind === "dock") {
|
|
715
767
|
const dockIndex = dockRows.length - anchor.fromBottom - 1;
|
|
716
768
|
return dockIndex < 0 || dockIndex >= dockRows.length
|
|
@@ -752,7 +804,7 @@ export class TranscriptViewport {
|
|
|
752
804
|
// Concurrency: a streaming chunk may replace or reflow the pointer's current live-tail
|
|
753
805
|
// row before the next motion report. During the held gesture only, retain its distance
|
|
754
806
|
// from the persistent tail; release restores identity-only projection.
|
|
755
|
-
if (selection.selecting && anchor.liveFromBottom !== undefined && this.#liveTailStartRow !== undefined) {
|
|
807
|
+
if (selection.selecting && moving && anchor.liveFromBottom !== undefined && this.#liveTailStartRow !== undefined) {
|
|
756
808
|
const documentRow = this.#selectableDocumentRowCount - anchor.liveFromBottom - 1;
|
|
757
809
|
if (documentRow >= this.#liveTailStartRow && documentRow < this.#selectableDocumentRowCount) {
|
|
758
810
|
const visible = this.#selectionRowAnchors.findIndex(candidate => candidate?.kind === "document" && candidate.row === documentRow);
|
|
@@ -764,10 +816,18 @@ export class TranscriptViewport {
|
|
|
764
816
|
}
|
|
765
817
|
}
|
|
766
818
|
}
|
|
819
|
+
// Concurrency: streaming may restyle a held endpoint's row; it keeps its document index.
|
|
820
|
+
// Following the tail would drag the press row toward the newest output.
|
|
821
|
+
if (selection.selecting && anchor.row >= 0 && anchor.row < documentRows.length) {
|
|
822
|
+
return {
|
|
823
|
+
line: anchor.row - this.#scrollTop,
|
|
824
|
+
anchor: documentSelectionAnchor(anchor.row, documentRows[anchor.row] ?? "", anchor.liveFromBottom),
|
|
825
|
+
};
|
|
826
|
+
}
|
|
767
827
|
return undefined;
|
|
768
828
|
};
|
|
769
|
-
const projectedAnchor = project(anchors.anchor);
|
|
770
|
-
const projectedHead = project(anchors.head);
|
|
829
|
+
const projectedAnchor = project(anchors.anchor, false);
|
|
830
|
+
const projectedHead = project(anchors.head, true);
|
|
771
831
|
if (projectedAnchor === undefined || projectedHead === undefined) {
|
|
772
832
|
this.clearSelection();
|
|
773
833
|
return;
|
|
@@ -787,11 +847,59 @@ export class TranscriptViewport {
|
|
|
787
847
|
// Invariant: mixed viewport/dock ranges project across the same complete frame regardless
|
|
788
848
|
// of origin. Document-only ranges remain clipped to the scrollable viewport when off-screen.
|
|
789
849
|
const rows = crossesDock ? this.#selectionRows.length : this.#viewportHeight;
|
|
790
|
-
|
|
850
|
+
const ordered = orderedTextSelection(dockOrderedSelection(this.#selection, anchors, this.#viewportHeight));
|
|
851
|
+
return visibleTextSelection(ordered, rows, this.#firstSelectableRow);
|
|
791
852
|
}
|
|
792
853
|
#updateCopyableSelection() {
|
|
793
|
-
|
|
854
|
+
// Performance: off-screen rows count as copyable by range alone; capture trims whitespace later.
|
|
855
|
+
const offscreen = offscreenSelectionRows(this.#selection, this.#selectionAnchors, this.#scrollTop, this.#firstSelectableRow, this.#viewportHeight, this.#documentRows.length);
|
|
856
|
+
const selectable = [offscreen.above, offscreen.below]
|
|
857
|
+
.some(range => range !== undefined && range.from < this.#selectableDocumentRowCount);
|
|
858
|
+
this.#copyableSelection = selectable && this.#visibleSelection() !== undefined || this.selectedText() !== null;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Invariant: dock rows follow the whole transcript, so a document endpoint scrolled below the
|
|
863
|
+
* viewport orders before every dock row instead of at its numeric frame line among them.
|
|
864
|
+
*/
|
|
865
|
+
function dockOrderedSelection(selection, anchors, viewportHeight) {
|
|
866
|
+
if (selection === undefined || anchors === undefined || anchors.anchor.kind !== "dock" && anchors.head.kind !== "dock") {
|
|
867
|
+
return selection;
|
|
794
868
|
}
|
|
869
|
+
const place = (point, kind) => kind === "document" && point.line >= viewportHeight ? { line: viewportHeight, column: 1, from: 0, to: 0 } : point;
|
|
870
|
+
return { ...selection, anchor: place(selection.anchor, anchors.anchor.kind), head: place(selection.head, anchors.head.kind) };
|
|
871
|
+
}
|
|
872
|
+
/** Document rows of a selection whose endpoints lie beyond the visible viewport edges. */
|
|
873
|
+
function offscreenSelectionRows(selection, anchors, scrollTop, firstSelectableRow, viewportHeight, documentRowCount) {
|
|
874
|
+
const ordered = orderedTextSelection(dockOrderedSelection(selection, anchors, viewportHeight));
|
|
875
|
+
if (selection === undefined || ordered === undefined || anchors === undefined)
|
|
876
|
+
return {};
|
|
877
|
+
const placed = dockOrderedSelection(selection, anchors, viewportHeight);
|
|
878
|
+
const anchorFirst = placed.anchor.line < placed.head.line
|
|
879
|
+
|| placed.anchor.line === placed.head.line && placed.anchor.column <= placed.head.column;
|
|
880
|
+
const top = anchorFirst ? anchors.anchor : anchors.head;
|
|
881
|
+
const bottom = anchorFirst ? anchors.head : anchors.anchor;
|
|
882
|
+
const topPoint = anchorFirst ? selection.anchor : selection.head;
|
|
883
|
+
const bottomPoint = anchorFirst ? selection.head : selection.anchor;
|
|
884
|
+
const firstVisibleRow = scrollTop + firstSelectableRow;
|
|
885
|
+
const pastVisibleRow = scrollTop + viewportHeight;
|
|
886
|
+
const columns = {
|
|
887
|
+
startColumn: selection.fullRow ? 0 : topPoint.from ?? Math.max(0, topPoint.column - 1),
|
|
888
|
+
endColumn: selection.fullRow ? Number.MAX_SAFE_INTEGER : bottomPoint.to ?? bottomPoint.column,
|
|
889
|
+
};
|
|
890
|
+
if (top.kind === "document" && bottom.kind === "dock" && top.row >= pastVisibleRow) {
|
|
891
|
+
return { ...columns, below: { from: top.row, to: documentRowCount - 1 }, belowFirst: true };
|
|
892
|
+
}
|
|
893
|
+
return {
|
|
894
|
+
...columns,
|
|
895
|
+
...(top.kind === "document" && top.row < firstVisibleRow && ordered.start.line < firstSelectableRow
|
|
896
|
+
? { above: { from: top.row, to: firstVisibleRow - 1 } }
|
|
897
|
+
: {}),
|
|
898
|
+
...(top.kind === "document" && bottom.kind === "document" && bottom.row >= pastVisibleRow
|
|
899
|
+
&& ordered.end.line >= viewportHeight
|
|
900
|
+
? { below: { from: pastVisibleRow, to: bottom.row } }
|
|
901
|
+
: {}),
|
|
902
|
+
};
|
|
795
903
|
}
|
|
796
904
|
function visibleTextSelection(selection, rowCount, firstRow) {
|
|
797
905
|
if (selection === undefined || rowCount <= firstRow || selection.end.line < firstRow || selection.start.line >= rowCount)
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
export declare const OWNED_UI_SETTINGS_VERSION =
|
|
1
|
+
export declare const OWNED_UI_SETTINGS_VERSION = 18;
|
|
2
2
|
export type OwnedUiSettingValue = string | number | boolean;
|
|
3
3
|
export type OwnedUiSettingApplication = "live" | "restart";
|
|
4
4
|
export interface OwnedUiSettingDeclaration {
|
|
5
5
|
readonly id: string;
|
|
6
|
-
/** Optional presentation label; the id remains the persistence key. */
|
|
7
6
|
readonly label?: string;
|
|
8
|
-
/** Optional settings-screen group for related owned controls. */
|
|
9
7
|
readonly section?: {
|
|
10
8
|
readonly id: string;
|
|
11
9
|
readonly title: string;
|
|
@@ -15,10 +13,6 @@ export interface OwnedUiSettingDeclaration {
|
|
|
15
13
|
readonly defaultValue: OwnedUiSettingValue;
|
|
16
14
|
readonly allowedValues: readonly OwnedUiSettingValue[];
|
|
17
15
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Every A1 setting, keyed by its id. The table is the one declaration: ids, defaults, and allowed
|
|
20
|
-
* values are read from it, and `OwnedSettingValueOf` derives each getter's type from it.
|
|
21
|
-
*/
|
|
22
16
|
export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
|
|
23
17
|
readonly updateCheck: Readonly<{
|
|
24
18
|
id: "updateCheck";
|
|
@@ -176,6 +170,18 @@ export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
|
|
|
176
170
|
defaultValue: 8;
|
|
177
171
|
allowedValues: readonly [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
|
|
178
172
|
}>;
|
|
173
|
+
readonly tabsPosition: Readonly<{
|
|
174
|
+
id: "tabsPosition";
|
|
175
|
+
label: "Position";
|
|
176
|
+
section: Readonly<{
|
|
177
|
+
id: "tabs";
|
|
178
|
+
title: "Tabs";
|
|
179
|
+
}>;
|
|
180
|
+
description: "Place the resident tab bar at the top or bottom of the terminal.";
|
|
181
|
+
application: "live";
|
|
182
|
+
defaultValue: "top";
|
|
183
|
+
allowedValues: readonly ["top", "bottom"];
|
|
184
|
+
}>;
|
|
179
185
|
readonly tabsAutoName: Readonly<{
|
|
180
186
|
id: "tabsAutoName";
|
|
181
187
|
label: "Auto name";
|
|
@@ -190,9 +196,7 @@ export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
|
|
|
190
196
|
}>;
|
|
191
197
|
}>;
|
|
192
198
|
export type OwnedSettingId = keyof typeof OWNED_SETTING_DECLARATIONS;
|
|
193
|
-
/** The value type a setting's declaration allows: a literal union for a choice, `number` for a stepped range. */
|
|
194
199
|
export type OwnedSettingValueOf<Id extends OwnedSettingId> = (typeof OWNED_SETTING_DECLARATIONS)[Id]["allowedValues"][number];
|
|
195
|
-
/** The declarations in declaration order, for resolution, persistence, and the settings screen. */
|
|
196
200
|
export declare const OWNED_UI_SETTING_DECLARATIONS: readonly OwnedUiSettingDeclaration[];
|
|
197
201
|
export declare function isOwnedSettingId(id: string): id is OwnedSettingId;
|
|
198
202
|
export declare function assertOwnedUiSettingDeclarations(declarations: readonly OwnedUiSettingDeclaration[]): void;
|
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
export const OWNED_UI_SETTINGS_VERSION =
|
|
1
|
+
export const OWNED_UI_SETTINGS_VERSION = 18;
|
|
2
2
|
const UI_ACCENT_COLOR_CHOICES = Object.freeze([
|
|
3
3
|
"purple", "blue", "cyan", "green", "orange", "pink",
|
|
4
4
|
]);
|
|
5
5
|
const MAX_ID_LENGTH = 64;
|
|
6
6
|
const ID_PATTERN = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
|
|
7
|
-
/** Declared first so the settings screen opens on it; sections follow first-declaration order. */
|
|
8
7
|
const GENERIC_SECTION = Object.freeze({ id: "generic", title: "Generic" });
|
|
9
8
|
const APPEARANCE_SECTION = Object.freeze({ id: "appearance", title: "Appearance" });
|
|
10
9
|
const SCROLL_SECTION = Object.freeze({ id: "scroll", title: "Scroll" });
|
|
11
10
|
const AGENT_SECTION = Object.freeze({ id: "agent", title: "Agent" });
|
|
12
11
|
const TABS_SECTION = Object.freeze({ id: "tabs", title: "Tabs" });
|
|
13
|
-
/**
|
|
14
|
-
* Every A1 setting, keyed by its id. The table is the one declaration: ids, defaults, and allowed
|
|
15
|
-
* values are read from it, and `OwnedSettingValueOf` derives each getter's type from it.
|
|
16
|
-
*/
|
|
17
12
|
export const OWNED_SETTING_DECLARATIONS = Object.freeze({
|
|
18
13
|
updateCheck: Object.freeze({
|
|
19
14
|
id: "updateCheck",
|
|
@@ -136,6 +131,15 @@ export const OWNED_SETTING_DECLARATIONS = Object.freeze({
|
|
|
136
131
|
defaultValue: 8,
|
|
137
132
|
allowedValues: Object.freeze([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
|
|
138
133
|
}),
|
|
134
|
+
tabsPosition: Object.freeze({
|
|
135
|
+
id: "tabsPosition",
|
|
136
|
+
label: "Position",
|
|
137
|
+
section: TABS_SECTION,
|
|
138
|
+
description: "Place the resident tab bar at the top or bottom of the terminal.",
|
|
139
|
+
application: "live",
|
|
140
|
+
defaultValue: "top",
|
|
141
|
+
allowedValues: Object.freeze(["top", "bottom"]),
|
|
142
|
+
}),
|
|
139
143
|
tabsAutoName: Object.freeze({
|
|
140
144
|
id: "tabsAutoName",
|
|
141
145
|
label: "Auto name",
|
|
@@ -146,7 +150,6 @@ export const OWNED_SETTING_DECLARATIONS = Object.freeze({
|
|
|
146
150
|
allowedValues: Object.freeze([true, false]),
|
|
147
151
|
}),
|
|
148
152
|
});
|
|
149
|
-
/** The declarations in declaration order, for resolution, persistence, and the settings screen. */
|
|
150
153
|
export const OWNED_UI_SETTING_DECLARATIONS = Object.freeze(Object.values(OWNED_SETTING_DECLARATIONS));
|
|
151
154
|
export function isOwnedSettingId(id) {
|
|
152
155
|
return Object.hasOwn(OWNED_SETTING_DECLARATIONS, id);
|
|
@@ -137,6 +137,13 @@ export const OWNED_UI_SETTINGS_MIGRATIONS = Object.freeze([
|
|
|
137
137
|
return { ...values };
|
|
138
138
|
},
|
|
139
139
|
}),
|
|
140
|
+
Object.freeze({
|
|
141
|
+
to: 18,
|
|
142
|
+
description: "Introduce the top resident tab-bar position default.",
|
|
143
|
+
migrate(values) {
|
|
144
|
+
return { ...values };
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
140
147
|
]);
|
|
141
148
|
/** Shape-selected compatibility for the metadata-free product object. */
|
|
142
149
|
export function normalizeOwnedUiSettingsValues(source) {
|
|
@@ -229,7 +229,7 @@ The channels carry bounded generation-1 frames: a u32 little-endian length follo
|
|
|
229
229
|
|
|
230
230
|
### 6. Rendering, input, and outer-terminal fidelity
|
|
231
231
|
|
|
232
|
-
- **Composition.** The attach client composes row 0 (the strip) and rows `1..N` (the viewed tab's surface). The tab PTY is sized `cols × (rows − 1)`. Ghostty remains the sole child-terminal parser. Its stream parser reports synchronized-output begin/end/reset boundaries to the holder, which keeps mutable parser state separate from an immutable committed surface and never publishes an open transaction. A non-renewable 250 ms deadline publishes one typed recovery commit for a malformed transaction. The outer composer owns the only synchronized envelope and falls back to a full repaint only when identity, geometry generation, dimensions, or the structured base changes.
|
|
232
|
+
- **Composition.** The attach client composes row 0 (the strip) and rows `1..N` (the viewed tab's surface). The tab PTY is sized `cols × (rows − 1)`. Ghostty remains the sole child-terminal parser. Its stream parser reports synchronized-output begin/end/reset boundaries to the holder, which keeps mutable parser state separate from an immutable committed surface and never publishes an open transaction. A non-renewable 250 ms deadline publishes one typed recovery commit for a malformed transaction. On Windows, ConPTY re-renders the child's cells on its own schedule and relays the DEC 2026 markers when it parses them, so a render pass can land between a frame's markers and the rest of its cells can arrive after the end marker; the holder therefore treats a relayed end marker as "frame parsed", keeps the state private until the following render pass has arrived and settled for 2 ms, and publishes anyway after a 40 ms bound. A begin marker that arrives while that pass is still pending does not publish the torn state. Unix PTYs relay bytes verbatim, so their markers still delimit frames exactly. The outer composer owns the only synchronized envelope and falls back to a full repaint only when identity, geometry generation, dimensions, or the structured base changes.
|
|
233
233
|
- **Outer modes.** The client negotiates the outer terminal's modes: raw mode, alternate screen, bracketed paste, focus events, SGR mouse, kitty keyboard protocol where available, and synchronized output.
|
|
234
234
|
- Child-requested modes are tracked in the holder's model. Input is re-encoded per child mode by libghostty-vt, which the proof already does.
|
|
235
235
|
- Mouse rows are offset by the strip. Row 0 belongs to the client.
|
|
@@ -303,7 +303,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
303
303
|
| Tab child exits or crashes | holder sees the child exit | If the exit was not requested: `crashed`. The holder respawns `ui.js --tab --session <file>` with backoff 1 s, 5 s, 30 s. After 3 restarts in 10 min: `failed`, with a banner `✗ <reason> — [r] retry · [f] start fresh · [alt+w] close`. An interrupted prompt (reported via the bridge, or detected as a trailing user entry) is offered back into the editor and never resent. Child stderr, if separately available, is a private content-bearing recovery artifact, never an ordinary diagnostic log. |
|
|
304
304
|
| Holder crash | pipe EOF plus verified process identity/exit | Pseudoterminal loss is not proof that every descendant exited. Verify the old writer/tree is gone and acquire its writer lease before resuming in a new holder; otherwise keep the tab blocked without duplicate writers. |
|
|
305
305
|
| Holder hang | holder control heartbeat every 5 s, 3 missed over a healthy supervision path, then one heartbeat of grace for the holder-local watchdog record | Terminate only the verified holder/tree within bounded deadlines, then follow the lease-gated recovery path. |
|
|
306
|
-
| A1 child hang | missing child event-loop heartbeat over a healthy bridge path |
|
|
306
|
+
| A1 child hang | missing child event-loop heartbeat over a healthy bridge path | Record `unresponsive` at 30 s without changing the chip or painting a notice; restart at `tabsUnresponsiveRestartSeconds` (default 120), preserving journal/screen. A server/bridge transport outage alone is not proof of a child hang. |
|
|
307
307
|
| Server crash | clients and holders see the pipe EOF | Holders keep running. A client or holder starts a replacement, which acquires the released writer lease, increments the epoch, derives expected credentials, and re-admits identity-verified holders. A live but unresponsive recorded owner is terminated only after native identity verification; an unverifiable owner blocks replacement. After 3 starts in 60 s, clients show `✗ tab host stopped — [r] restart · [q] quit`. |
|
|
308
308
|
| Attach client crash or terminal close | the server sees the pipe EOF | Nothing happens to tabs. Pending requests stay `needs-input`. |
|
|
309
309
|
| Reboot or logout | the platform boot identity differs, or no verified holders exist for a new OS session | Tabs are not restored in this version. The server moves the previous tab set to registry history, and bare `a1` starts with one new tab. Every previous session stays resumable through the normal session picker or `a1 --session`. A pending journaled prompt is offered when its session is next opened in a tab, for up to seven days. Restoring tabs after reboot is a follow-up change. |
|
|
@@ -377,7 +377,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
377
377
|
Internal resources remain bounded without imposing a user-visible tab-count quota.
|
|
378
378
|
|
|
379
379
|
- The only user-facing Tabs setting during resident launches is `tabsAutoName` (default on). Legacy `tabsMax` data is preserved but ignored. Tab enablement is not a setting. Direct `a1 solo` and `a1 pi` launches omit the Tabs section entirely.
|
|
380
|
-
- Unresponsive restart
|
|
380
|
+
- Unresponsive restart timing is internal fixed behavior at 120 seconds; there is no progress-stall notice; concurrent starts use internal default 2/hard cap 8 and prewarm uses internal count/hard cap 1.
|
|
381
381
|
- Every open tab keeps its holder and A1 process until explicit close. There is no idle-suspension setting, timer, lifecycle, or glyph.
|
|
382
382
|
- Scrollback is capped at 10 MiB per holder, and the replay after a server restart is the retained model itself.
|
|
383
383
|
- The server exits after 10 minutes with no attached client and no live user tab; the prewarmed standby does not keep it alive and is stopped first.
|
|
@@ -458,7 +458,7 @@ The v2 daemon was unreliable for structural reasons, and a forensic pass over it
|
|
|
458
458
|
| Synchronous `taskkill` loops blocked the daemon for 30–60 s | Blocking kills on the event loop | Kills run asynchronously and concurrently, with deadlines. The loop keeps answering heartbeats. |
|
|
459
459
|
| Blocked ConPTY writes stalled the loop, and keystrokes or pastes were silently dropped ("newest wins") | No flow control | A dedicated writer per holder with a bounded queue. Reserve capacity for a complete paste before delivery; reject an over-capacity paste visibly without delivering a prefix. After admission, preserve byte order and report delivery uncertainty if a child or process fails; never silently truncate or retry accepted bytes. |
|
|
460
460
|
| Terminal queries (DA/DSR/CPR) were answered by the busy shared emulator, so children stalled | One emulator loop for all agents | Each holder answers its own tab's queries from its own model. |
|
|
461
|
-
| Repaints arrived in fragments ("ghost frames"), patched with timing heuristics and two screen sources | Emulation reconciled with bridge surfaces by timers | One source of truth, the holder model. Frames are published at synchronized-output boundaries, with a bounded coalescing window. |
|
|
461
|
+
| Repaints arrived in fragments ("ghost frames"), patched with timing heuristics and two screen sources | Emulation reconciled with bridge surfaces by timers | One source of truth, the holder model. Frames are published at synchronized-output boundaries, with a bounded coalescing window; where ConPTY relays those boundaries ahead of the cells it re-renders, publication waits for the render pass instead of the marker. |
|
|
462
462
|
| A late exit event poisoned the replacement child | Events were not tied to an incarnation | Incarnation IDs on every event. Stale events are discarded. |
|
|
463
463
|
| Resizes were lost in transit | Edge-triggered resize | Size is desired state, reconciled until observed. |
|
|
464
464
|
| Restore spawned duplicate agents | No restore gate | A single start gate for crash restarts, at most one start in flight per tab, and conditional revisions. The duplicate-start invariant is checked in simulation. |
|
|
@@ -479,8 +479,8 @@ The v2 daemon was unreliable for structural reasons, and a forensic pass over it
|
|
|
479
479
|
**Agents that stop making progress.** Liveness and progress are separate signals:
|
|
480
480
|
|
|
481
481
|
- Holder heartbeat: a Rust main-loop tick every 5 s seen by the server; three missed ticks over an otherwise healthy supervision path plus one heartbeat of grace trigger verified holder recovery. The grace keeps external termination behind the holder's 15-second process-local watchdog so its own crash evidence wins despite scheduler jitter.
|
|
482
|
-
- A1 heartbeat: the child's Node event loop, seen through the bridge. After 30 s
|
|
483
|
-
- Agent progress: model or tool events
|
|
482
|
+
- A1 heartbeat: the child's Node event loop, seen through the bridge. After 30 s the server records `unresponsive` in the registry and counters, without changing the presented status, chip glyph, or colour and without a notice row or restart key; after `tabsUnresponsiveRestartSeconds` (default 120) the tab restarts from its session with its journal and last screen preserved. A missing server or bridge transport degrades status, not proof of a child event-loop stall; reacquire liveness evidence before destructive recovery.
|
|
483
|
+
- Agent progress: not supervised. A working tab without model or tool events keeps its working status indefinitely, gains no transcript notice, and is never auto-killed, because a legitimate tool can run for a long time.
|
|
484
484
|
|
|
485
485
|
**Data-loss envelope.** Distinguish process termination, power loss, and loss of both holder and child:
|
|
486
486
|
|
|
@@ -19,7 +19,7 @@ A generated probe on Windows Node 24.16.0 copied the same eight-character range
|
|
|
19
19
|
- Copy: 5-second admission deadline, one executing request and one newest pending request, at most 16 MiB UTF-8 text. Source snapshots retain only selected rows, capped at 65,536 rows and 32 Mi UTF-16 code units; they do not keep an obsolete whole-transcript graph.
|
|
20
20
|
- Paste: eight distinct admitted requests, a 5-second acquisition budget including the captured write wait, and a 15-second deadline through insertion. A canceled executor counts against capacity until its stop fence. Image conversion has one process-wide slot. Copy supersession is deliberately not used for paste edits.
|
|
21
21
|
- Text transfer: 16 MiB UTF-8 cap; IPC fragments are at most 16,384 code units and preserve surrogate pairs. The receiver acknowledges each fragment. Prepared large text is stored behind the existing compact chip rather than inserted as millions of editor characters. Existing source-image, pixel, encoded-output and attachment-count limits remain unchanged.
|
|
22
|
-
- Clipboard executors have a 250 ms stop grace. A helper that has already reported a complete prepared result and lingers past that grace is reaped, not canceled: it is still terminated, but its prepared text is delivered exactly rather than replaced with an empty payload. Only an explicit cancellation, a protocol violation, or an incomplete byte count discards prepared text. POSIX command fallbacks share the paste helper's isolated process group; strict command reads use a forceful kill signal. Unconfirmed executor termination retains its capacity instead of allowing unbounded replacements or late A1-controlled writes. No helper inherits the UI terminal or places clipboard payloads in argv, temporary files, or logs.
|
|
22
|
+
- Clipboard executors have a 250 ms stop grace. A helper that has already reported a complete prepared result and lingers past that grace is reaped, not canceled: it is still terminated, but its prepared text is delivered exactly rather than replaced with an empty payload. Only an explicit cancellation, a protocol violation, or an incomplete byte count discards prepared text. POSIX command fallbacks share the paste helper's isolated process group; strict command reads use a forceful kill signal. Native POSIX copy writers (`wl-copy`, `xclip`, `pbcopy`) start detached in their own process group, as in pinned Pi 1.4, so a writer that daemonizes to keep serving the clipboard is not stopped when Ctrl+Z suspends A1. Unconfirmed executor termination retains its capacity instead of allowing unbounded replacements or late A1-controlled writes. No helper inherits the UI terminal or places clipboard payloads in argv, temporary files, or logs.
|
|
23
23
|
- Local copy prefers native delivery outside the terminal. OSC 52 uses the existing writer only on a non-blocking supported output route and is capped at a complete 64 KiB control, or a lower advertised limit. Submission is not clipboard acknowledgment. Remote/multiplexed sessions never silently read or write a different host's clipboard. Native Ctrl+V acquisition is unavailable when only the remote terminal owns the desired clipboard; normal terminal-provided paste remains supported.
|
|
24
24
|
- URL-chip decoration: at most 65,536 UTF-8 bytes of added OSC 8 metadata per editor render pass across rows and repeated occurrences, including opening/closing pairs and cleanup closes. Targets that do not fit are omitted whole before control construction; a UTF-16 length precheck avoids scanning huge backing values. The budget resets each pass. Full chip data, atomic editing and exact copy/history/submission expansion remain intact; ordinary in-budget links retain their targets. Terminal automatic URL recognition is separate from explicit metadata.
|
|
25
25
|
- Path-list presentation: the isolated classifier budgets 4,096 UTF-16 units per paste for existing individual tags plus 20 suffix-reserve units per occurrence. Over-budget lists use one existing text-paste chip backed by all classified full paths concatenated in order, including repetitions, without extra separators or re-normalization. No individual members are adopted into the UI before compaction. Small lists retain their file/folder/image-file chips; failed/timed-out classification retains the existing original-text fallback. Existing draft/history chips and the synchronous compatibility classifier are unchanged.
|
|
@@ -34,9 +34,9 @@ It contains no executable built from A1 sources, separate session-lock addon, br
|
|
|
34
34
|
|
|
35
35
|
## Native addon layer
|
|
36
36
|
|
|
37
|
-
`npm run doctor` reports this contract against the current machine—Node range, npm major, git, the GitHub CLI, Rust/Cargo, and whether `node_modules` matches the lockfile—and names the
|
|
37
|
+
`npm run doctor` reports this contract against the current machine—Node range, npm major, git, the GitHub CLI, Rust/Cargo, Windows Microsoft C++ readiness where applicable, and whether `node_modules` matches the lockfile—and names the explicit setup action for anything missing. `npm run build` resolves the same native environment and runs the check before cleaning or compiling, so an unmet prerequisite fails by name rather than inside a module resolver, Cargo, or the linker. Unmet tool prerequisites block; a Node or npm version outside the declared contract is reported as an advisory without failing the build.
|
|
38
38
|
|
|
39
|
-
Source builds require Rust/Cargo 1.85 or newer; `rust-toolchain.toml` pins the exact version CI and publication install through `scripts/development/prepare-ci-rust.sh`, and rustup selects that version for Cargo commands inside the repository, installing it on first use. The Zig cache (`mlugg/setup-zig`, one entry per workflow and platform per run, bounded to 512 MiB) stays enabled: measured on 2026-10-16, a cold Zig build of `libghostty-vt` added about 40 to 60 seconds to a terminal-host test build (Linux 2m21s cold against 1m20s warm, Windows 2m05s against 1m26s), which the cache saves on every warm run.
|
|
39
|
+
Source builds require Rust/Cargo 1.85 or newer; `rust-toolchain.toml` pins the exact version CI and publication install through `scripts/development/prepare-ci-rust.sh`, and rustup selects that version for Cargo commands inside the repository, installing it on first use. On Windows, the resolver honors explicit `CARGO` and `RUSTC` executables, then the launching `PATH`, then `%USERPROFILE%\.cargo\bin`; the fallback changes only build-child state. Windows native linking also requires Visual Studio or Visual Studio Build Tools with the Desktop development with C++ workload, MSVC x64/x86 tools, and a Windows SDK. Visual Studio Code is not a compiler prerequisite. An already valid developer environment is retained; otherwise the resolver uses the installed stable Visual Studio locator and `vcvars64.bat`, verifies that `cl.exe` and Microsoft's `link.exe` come from the selected VC tools, and passes the captured environment only to child processes. This prevents Git Bash's GNU `link.exe` from shadowing MSVC without changing persistent `PATH`. A1 never installs these tools, elevates privileges, or accepts licenses automatically. The Zig cache (`mlugg/setup-zig`, one entry per workflow and platform per run, bounded to 512 MiB) stays enabled: measured on 2026-10-16, a cold Zig build of `libghostty-vt` added about 40 to 60 seconds to a terminal-host test build (Linux 2m21s cold against 1m20s warm, Windows 2m05s against 1m26s), which the cache saves on every warm run.
|
|
40
40
|
|
|
41
41
|
`npm run build` compiles `native/process-guardian` as the host platform's Node-API 9 addon, places `process-guardian.node` under `dist/native/<platform>-<architecture>/`, loads it to observe its version identity, and writes the v2 integrity manifest. It also builds the current platform's release-profile `terminal-host.node`, whose v2 manifest binds the addon, `dist/foundation/resident-tabs/host-entry.js`, and notices, so the ordinary `npm run build && ./scripts/dev` handoff exercises resident tabs without deployment. On Windows the normal build reuses a compatible ignored terminal-library cache or downloads the newest compatible library from the current branch or a successful CI run belonging to a pull request already merged into `develop`, then links it under the crate's manifest verification; it never silently invokes the quarantined Zig package fetch. An unavailable or mismatched prerequisite fails the build instead of leaving a nominally successful single-agent-only tree. The npm `prepare` lifecycle and validation preflight use the explicit core build because selected native lanes assemble and certify terminal hosts separately.
|
|
42
42
|
|