@timurproko/a1 0.2.7-dev.810 → 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.
Files changed (36) hide show
  1. package/dist/app/session-shell/resident-tab-coordinator.d.ts +6 -9
  2. package/dist/app/session-shell/resident-tab-coordinator.js +9 -52
  3. package/dist/app/session-shell/session-shell-root.d.ts +1 -2
  4. package/dist/app/session-shell/session-shell.js +2 -2
  5. package/dist/app/session-shell/session-viewport-controller.js +6 -4
  6. package/dist/composition/owned-ui.js +1 -2
  7. package/dist/features/owned-ui/settings-app.js +1 -1
  8. package/dist/foundation/resident-tabs/bridge-client.d.ts +1 -1
  9. package/dist/foundation/resident-tabs/bridge-client.js +1 -1
  10. package/dist/foundation/resident-tabs/settings.d.ts +0 -1
  11. package/dist/foundation/resident-tabs/settings.js +0 -2
  12. package/dist/native/darwin-arm64/manifest.json +1 -1
  13. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  14. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  15. package/dist/native/darwin-x64/manifest.json +1 -1
  16. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  17. package/dist/native/darwin-x64/terminal-host.node +0 -0
  18. package/dist/native/linux-x64/manifest.json +1 -1
  19. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  20. package/dist/native/linux-x64/terminal-host.node +0 -0
  21. package/dist/native/win32-x64/manifest.json +2 -2
  22. package/dist/native/win32-x64/process-guardian.node +0 -0
  23. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  24. package/dist/native/win32-x64/terminal-host.node +0 -0
  25. package/dist/ui/components/dialog-panel.js +1 -1
  26. package/dist/ui/components/prompt-input.d.ts +2 -2
  27. package/dist/ui/components/prompt-input.js +2 -2
  28. package/dist/ui/components/submitted-prompt.d.ts +2 -2
  29. package/dist/ui/components/submitted-prompt.js +3 -3
  30. package/dist/ui/components/transcript-viewport.d.ts +1 -1
  31. package/dist/ui/components/transcript-viewport.js +141 -24
  32. package/dist/ui/settings/declarations.d.ts +13 -9
  33. package/dist/ui/settings/declarations.js +10 -7
  34. package/dist/ui/settings/migrations.js +7 -0
  35. package/docs/architecture/resident-tabs.md +4 -4
  36. package/package.json +1 -1
@@ -1,18 +1,15 @@
1
1
  import type { OwnedUiEvent, PromptHistoryImageSidecarPort } from "../../contracts/owned-ui/index.js";
2
2
  import type { SessionConcern } from "./session-concern.js";
3
3
  import type { OwnedUiShellEngineOptions, OwnedUiSessionShellRoot } from "./session-shell-root.js";
4
- import type { OwnedUiPresenterTerminal } from "./shell-host.js";
5
4
  export type ResidentTabOptions = NonNullable<OwnedUiShellEngineOptions["residentTab"]>;
6
5
  export interface ResidentTabCoordinatorPorts {
7
6
  readonly root: Pick<OwnedUiSessionShellRoot, "appendWorkflowStatus" | "rehydrateHistoryText" | "editor">;
8
- readonly terminal: Pick<OwnedUiPresenterTerminal, "requestRender">;
9
- readonly disposed: () => boolean;
10
7
  }
11
8
  /**
12
- * The session presenter's side of a resident tab: recovered input, the progress and stall status
13
- * it reports to the strip, prompt admission and correlation through the recovery journal, draft
14
- * checkpoints, detach on quit, and the graceful stop when the presenter is released. Outside a
15
- * resident tab every operation is a no-op.
9
+ * The session presenter's side of a resident tab: recovered input, the idle status it reports to
10
+ * the strip, prompt admission and correlation through the recovery journal, draft checkpoints,
11
+ * detach on quit, and the graceful stop when the presenter is released. Outside a resident tab
12
+ * every operation is a no-op.
16
13
  */
17
14
  export declare class ResidentTabCoordinator implements SessionConcern {
18
15
  #private;
@@ -23,8 +20,8 @@ export declare class ResidentTabCoordinator implements SessionConcern {
23
20
  closeOwnedRoute(route: string): void;
24
21
  /** Puts the prompts and draft a previous tab process left unfinished back into the editor. */
25
22
  restoreInput(imageSidecar?: PromptHistoryImageSidecarPort): void;
26
- /** Tracks working, idle, and stalled status from session events and arms the stall notice. */
27
- observeProgress(event: OwnedUiEvent): void;
23
+ /** Reports idle to the strip whenever the session leaves its busy lifecycle. */
24
+ observeLifecycle(event: OwnedUiEvent): void;
28
25
  /** Journals a prompt before it is sent; undefined outside a resident tab. Rejects when the journal cannot sync. */
29
26
  admit(input: Parameters<NonNullable<ResidentTabOptions["admit"]>>[0]): Promise<{
30
27
  readonly id: string;
@@ -1,18 +1,12 @@
1
- const PROGRESS_EVENTS = new Set([
2
- "transcript-block", "session-view", "assistant-message-completed", "agent-run-started",
3
- ]);
4
1
  /**
5
- * The session presenter's side of a resident tab: recovered input, the progress and stall status
6
- * it reports to the strip, prompt admission and correlation through the recovery journal, draft
7
- * checkpoints, detach on quit, and the graceful stop when the presenter is released. Outside a
8
- * resident tab every operation is a no-op.
2
+ * The session presenter's side of a resident tab: recovered input, the idle status it reports to
3
+ * the strip, prompt admission and correlation through the recovery journal, draft checkpoints,
4
+ * detach on quit, and the graceful stop when the presenter is released. Outside a resident tab
5
+ * every operation is a no-op.
9
6
  */
10
7
  export class ResidentTabCoordinator {
11
8
  #options;
12
9
  #ports;
13
- #progressTimer;
14
- #working = false;
15
- #stalled = false;
16
10
  constructor(options, ports) {
17
11
  this.#options = options;
18
12
  this.#ports = ports;
@@ -51,40 +45,10 @@ export class ResidentTabCoordinator {
51
45
  root.appendWorkflowStatus("Recovered the editor draft from the previous tab process.");
52
46
  }
53
47
  }
54
- /** Tracks working, idle, and stalled status from session events and arms the stall notice. */
55
- observeProgress(event) {
56
- const resident = this.#options;
57
- const seconds = resident?.stallNoticeSeconds;
58
- if (resident === undefined || seconds === undefined)
59
- return;
60
- if (event.type === "session-lifecycle") {
61
- this.#working = event.lifecycle === "busy";
62
- if (!this.#working) {
63
- this.#clearTimer();
64
- this.#stalled = false;
65
- resident.reportStatus?.("idle");
66
- return;
67
- }
68
- }
69
- else if (!this.#working || !PROGRESS_EVENTS.has(event.type)) {
70
- return;
71
- }
72
- if (this.#stalled) {
73
- this.#stalled = false;
74
- resident.reportStatus?.("working");
75
- }
76
- this.#clearTimer();
77
- this.#progressTimer = setTimeout(() => {
78
- this.#progressTimer = undefined;
79
- if (this.#ports.disposed() || !this.#working)
80
- return;
81
- this.#stalled = true;
82
- resident.reportStatus?.("stalled");
83
- const minutes = Math.max(1, Math.round(seconds / 60));
84
- this.#ports.root.appendWorkflowStatus(`No progress for ${minutes} min — Esc interrupts · use the tab restart action to restart`);
85
- this.#ports.terminal.requestRender();
86
- }, seconds * 1_000);
87
- this.#progressTimer.unref?.();
48
+ /** Reports idle to the strip whenever the session leaves its busy lifecycle. */
49
+ observeLifecycle(event) {
50
+ if (event.type === "session-lifecycle" && event.lifecycle !== "busy")
51
+ this.#options?.reportStatus?.("idle");
88
52
  }
89
53
  /** Journals a prompt before it is sent; undefined outside a resident tab. Rejects when the journal cannot sync. */
90
54
  admit(input) {
@@ -109,12 +73,5 @@ export class ResidentTabCoordinator {
109
73
  gracefulStop(sessionFile) {
110
74
  return this.#options?.gracefulStop?.(sessionFile) ?? Promise.resolve();
111
75
  }
112
- dispose() {
113
- this.#clearTimer();
114
- }
115
- #clearTimer() {
116
- if (this.#progressTimer !== undefined)
117
- clearTimeout(this.#progressTimer);
118
- this.#progressTimer = undefined;
119
- }
76
+ dispose() { }
120
77
  }
@@ -47,8 +47,7 @@ export interface OwnedUiShellEngineOptions {
47
47
  readonly residentTab?: {
48
48
  requestDetach(): Promise<boolean>;
49
49
  closeOwnedRoute?(route: string): void;
50
- reportStatus?(status: "working" | "idle" | "stalled"): void;
51
- readonly stallNoticeSeconds?: number;
50
+ reportStatus?(status: "working" | "idle"): void;
52
51
  admit?(input: {
53
52
  readonly sessionId: string;
54
53
  readonly sessionFile: string | null;
@@ -251,7 +251,7 @@ export class OwnedUiSessionPresenter {
251
251
  // Invariant: the presenter owns its root (theme subscription and helper clients) and disposes it once the
252
252
  // terminal no longer renders it.
253
253
  constructed.push(() => this.root.dispose());
254
- this.#resident = new ResidentTabCoordinator(options.engine.residentTab, { root: this.root, terminal: this.#terminal, disposed: () => this.#disposed });
254
+ this.#resident = new ResidentTabCoordinator(options.engine.residentTab, { root: this.root });
255
255
  constructed.push(() => this.#resident.dispose());
256
256
  const startupRoute = options.engine.startupRoute;
257
257
  this.#dialogs = new SessionDialogCoordinator({
@@ -366,7 +366,7 @@ export class OwnedUiSessionPresenter {
366
366
  this.#stop.settleAfter(() => this.dispose());
367
367
  return;
368
368
  }
369
- this.#resident.observeProgress(event);
369
+ this.#resident.observeLifecycle(event);
370
370
  // Performance: a streamed chunk names one block, and touching only that block is what keeps the
371
371
  // cost of a chunk the same in a long session as in a new one. Everything else
372
372
  // resynchronizes the view, which is cheap next to re-reading the transcript.
@@ -322,8 +322,8 @@ export class SessionViewportController {
322
322
  this.#requestRender();
323
323
  }
324
324
  }
325
- const previousPrompt = SHIFT_UP_INPUTS.has(data) || this.#editor.matchesTerminalKey(data, "alt+home");
326
- if (editorActive && allowWheel && (previousPrompt || SHIFT_DOWN_INPUTS.has(data))) {
325
+ const previousPrompt = PROMPT_UP_INPUTS.has(data) || this.#editor.matchesTerminalKey(data, "alt+home");
326
+ if (editorActive && allowWheel && (previousPrompt || PROMPT_DOWN_INPUTS.has(data))) {
327
327
  const scrolled = previousPrompt
328
328
  ? this.#viewport.scrollToPreviousPrompt(now)
329
329
  : this.#viewport.scrollToNextPrompt(now);
@@ -670,5 +670,7 @@ export class SessionViewportController {
670
670
  }
671
671
  const SELECTION_AUTO_SCROLL_INTERVAL_MS = 30;
672
672
  const EDITOR_LINK_DELETION_INPUTS = new Set(["\b", "\u007f", "\u001b[3~"]);
673
- const SHIFT_UP_INPUTS = new Set(["\u001b[1;2A"]);
674
- const SHIFT_DOWN_INPUTS = new Set(["\u001b[1;2B"]);
673
+ // Platform: Windows Terminal may consume Shift+Arrow for native selection. Ctrl+Arrow matches
674
+ // solo A1's Windows binding while retaining Shift+Arrow whenever the terminal delivers it.
675
+ const PROMPT_UP_INPUTS = new Set(["\u001b[1;2A", "\u001b[1;5A"]);
676
+ const PROMPT_DOWN_INPUTS = new Set(["\u001b[1;2B", "\u001b[1;5B"]);
@@ -50,7 +50,7 @@ export async function composeOwnedUi(options = {}) {
50
50
  namespace: PRODUCT_IDENTITY.commandName,
51
51
  legacyConfigDir: productPaths.configDir,
52
52
  agentProvider: () => backend.settingsPort(),
53
- ...(options.hideTabSettings ? { hiddenOwnedSettingIds: ["tabsAutoName"] } : {}),
53
+ ...(options.hideTabSettings ? { hiddenOwnedSettingIds: ["tabsAutoName", "tabsPosition"] } : {}),
54
54
  hiddenAgentSettingIds: ["fullscreenWheelScrollLines"],
55
55
  agentSettingLabelOverrides: { fullscreenCopyOnSelect: "Copy on select" },
56
56
  });
@@ -221,7 +221,6 @@ export async function composeOwnedUi(options = {}) {
221
221
  void tabBridge.requestClose();
222
222
  },
223
223
  reportStatus: status => tabBridge.status(status),
224
- stallNoticeSeconds: 300,
225
224
  admit: input => tabRecovery.admit({ ...input, incarnation: options.tab.incarnation }),
226
225
  dispatched: () => tabRecovery.dispatched(),
227
226
  correlated: (submissionId, entryId) => correlateResidentTabSubmission(tabRecovery, backend, submissionId, entryId),
@@ -1026,7 +1026,7 @@ export class SettingsApp {
1026
1026
  const input = this.#filter;
1027
1027
  if (input === null)
1028
1028
  return [status];
1029
- return [...renderInputRow(input, width, { placeholder: SEARCH_PLACEHOLDER, theme }).lines, status];
1029
+ return [...renderInputRow(input, width, { placeholder: SEARCH_PLACEHOLDER, promptGlyph: "", theme }).lines, status];
1030
1030
  }
1031
1031
  }
1032
1032
  function modelLabelParts(label) {
@@ -1,4 +1,4 @@
1
- export type ResidentTabStatus = "starting" | "working" | "needs-input" | "done-unseen" | "done" | "aborted" | "error" | "idle" | "unresponsive" | "stalled" | "crashed" | "restarting" | "failed";
1
+ export type ResidentTabStatus = "starting" | "working" | "needs-input" | "done-unseen" | "done" | "aborted" | "error" | "idle" | "unresponsive" | "crashed" | "restarting" | "failed";
2
2
  export interface TabBridgeCredentials {
3
3
  readonly tabId: string;
4
4
  readonly token: string;
@@ -80,7 +80,7 @@ export class TabBridgeClient {
80
80
  }
81
81
  status(state, degraded = false, pendingRequests = 0, queuedInput = 0) {
82
82
  this.#latestStatus = { state, degraded, accent: this.#accent, pendingRequests, queuedInput };
83
- const recoveryState = state === "unresponsive" || state === "stalled" || state === "crashed" || state === "restarting" || state === "failed";
83
+ const recoveryState = state === "unresponsive" || state === "crashed" || state === "restarting" || state === "failed";
84
84
  if (recoveryState && !this.#supports("recovery-status"))
85
85
  return;
86
86
  this.#send("bridge.status", {
@@ -7,7 +7,6 @@ export interface ResidentTabLaunchSettings {
7
7
  readonly maxConcurrentStarts: number;
8
8
  readonly prewarmCount: number;
9
9
  readonly unresponsiveRestartSeconds: number;
10
- readonly stallNoticeSeconds: number;
11
10
  }
12
11
  /** Read only resident-launch values before any UI or extension module is loaded. */
13
12
  export declare function readResidentTabLaunchSettings(profileDir: string, namespace?: string, legacyConfigDir?: string, profileId?: string): Promise<ResidentTabLaunchSettings>;
@@ -11,7 +11,6 @@ const DEFAULTS = Object.freeze({
11
11
  maxConcurrentStarts: RESIDENT_TABS_MAX_CONCURRENT_STARTS,
12
12
  prewarmCount: RESIDENT_TABS_PREWARM_COUNT,
13
13
  unresponsiveRestartSeconds: 120,
14
- stallNoticeSeconds: 300,
15
14
  });
16
15
  /** Read only resident-launch values before any UI or extension module is loaded. */
17
16
  export async function readResidentTabLaunchSettings(profileDir, namespace = "a1", legacyConfigDir, profileId = namespace) {
@@ -55,6 +54,5 @@ function resolveLaunchValues(document, migrations) {
55
54
  maxConcurrentStarts: RESIDENT_TABS_MAX_CONCURRENT_STARTS,
56
55
  prewarmCount: RESIDENT_TABS_PREWARM_COUNT,
57
56
  unresponsiveRestartSeconds: DEFAULTS.unresponsiveRestartSeconds,
58
- stallNoticeSeconds: DEFAULTS.stallNoticeSeconds,
59
57
  };
60
58
  }
@@ -7,7 +7,7 @@
7
7
  "platform": "darwin",
8
8
  "architecture": "arm64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-23T09:16:51.110Z",
10
+ "builtAt": "2026-10-24T04:45:15.222Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
13
  "sha256": "a87eaaffcd986b624c8b044b756446cc1bdaf4334a1775b97820542fe34f9fb7",
@@ -9,11 +9,11 @@
9
9
  "architecture": "arm64",
10
10
  "target": "aarch64-apple-darwin",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-23T09:19:57.936Z",
12
+ "builtAt": "2026-10-24T04:48:05.486Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "62c8a0656c7496194cd20649e3f2011f460cb381da2a04fde7ce54cbd38d5a50",
16
- "size": 5748592
15
+ "sha256": "f7f580d5ded5e1efa26ae790488efc8284edd15fee620aed091564c0fc460d16",
16
+ "size": 5812800
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,7 +7,7 @@
7
7
  "platform": "darwin",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-23T09:16:58.302Z",
10
+ "builtAt": "2026-10-24T04:45:23.346Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
13
  "sha256": "783a65f0de287a1c02cd5429dbe97bc8633f4cc8a7b92e3e961180b9bdcff814",
@@ -9,11 +9,11 @@
9
9
  "architecture": "x64",
10
10
  "target": "x86_64-apple-darwin",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-23T09:22:31.367Z",
12
+ "builtAt": "2026-10-24T04:50:05.116Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "f74aafbd050eaba8737a9e5bd82b2836f86e8b73168310e7c3316af99442e1f2",
16
- "size": 6272408
15
+ "sha256": "f7f2a1c09ebf4dbd5d28de747497564cb252d253ab7a9009b7aae1fe26bba4ff",
16
+ "size": 6340520
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,7 +7,7 @@
7
7
  "platform": "linux",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-23T09:16:44.112Z",
10
+ "builtAt": "2026-10-24T04:45:06.148Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
13
  "sha256": "118e5954e3864672bd8482ab7edf419cb27dcdf2533720316979b7f5cc5a53a5",
@@ -9,11 +9,11 @@
9
9
  "architecture": "x64",
10
10
  "target": "x86_64-unknown-linux-gnu",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-23T09:18:34.630Z",
12
+ "builtAt": "2026-10-24T04:48:17.317Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "707e4140e1c1b4b2c78c45373d8471bbbcc6232e7ad5d14ad8f5a76a90b6265b",
16
- "size": 6997376
15
+ "sha256": "3c2ed1d62404436ebe39000e93c5428dd9acff3f3158004b94af8a95e778e32a",
16
+ "size": 7069736
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,10 +7,10 @@
7
7
  "platform": "win32",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-23T09:17:41.474Z",
10
+ "builtAt": "2026-10-24T04:45:48.535Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
- "sha256": "f0fb48d96ea06a21d77a4df332ec863d3830caf8ce6adfebcb2a4bb4d86fabdf",
13
+ "sha256": "12f5a962bc427e82345170f1c4db2a875683d5cbb4e818bcc1a314a11c71bf31",
14
14
  "size": 402432
15
15
  },
16
16
  "provenance": {
@@ -9,11 +9,11 @@
9
9
  "architecture": "x64",
10
10
  "target": "x86_64-pc-windows-msvc",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-23T09:20:49.040Z",
12
+ "builtAt": "2026-10-24T04:48:40.147Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "f511333451655f6fdef1da7710c54c9e65058e367f6e8be3b8ac0420fcc70fa1",
16
- "size": 6078464
15
+ "sha256": "9beb6edeb62b28ff7e55d419d724c98f8645a73977d85fbb2c1bd85b7b324916",
16
+ "size": 6138880
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -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: "> ", theme }).lines, ""];
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 = "\u276F ";
3
- export declare const PROMPT_PREFIX_WIDTH = 2;
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 = 2;
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 = 2;
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 = 2;
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 visible selected text, not off-screen history. */
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;
@@ -15,6 +15,19 @@ const IDENTITY_ROW = (row) => row;
15
15
  function documentSelectionAnchor(row, source, liveFromBottom) {
16
16
  return { kind: "document", row, source, identity: stripAnsi(source).trimEnd(), liveFromBottom };
17
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
+ }
18
31
  function viewportSelectionAnchor(row, source, selectableDocumentRowCount, documentRowCount) {
19
32
  return row < selectableDocumentRowCount
20
33
  ? documentSelectionAnchor(row, source)
@@ -140,7 +153,12 @@ export class TranscriptViewport {
140
153
  this.scrollBy(1, now);
141
154
  else if (autoScroll && frameRow <= 1 && this.#scrollTop > 0)
142
155
  this.scrollBy(-1, now);
143
- const line = clamp(frameRow - 1, 0, this.#selectionRows.length - 1);
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;
144
162
  const selectionWidth = line < (this.#frame?.hits.viewportHeight ?? 0)
145
163
  ? this.#contentWidth
146
164
  : this.#selectionWidth;
@@ -151,12 +169,13 @@ export class TranscriptViewport {
151
169
  ? { line, column: targetColumn }
152
170
  : textSelectionPointAt(line, targetColumn, this.#selectionRows[line] ?? "");
153
171
  const next = extendTextSelection(selection, point);
154
- if (next !== selection) {
155
- const headAnchor = this.#selectionAnchorAt(line);
156
- if (headAnchor === undefined || this.#selectionAnchors === undefined) {
157
- this.clearSelection();
158
- return true;
159
- }
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)) {
160
179
  this.#selection = next;
161
180
  this.#selectionAnchors = { ...this.#selectionAnchors, head: headAnchor };
162
181
  this.#selectionRevision += 1;
@@ -184,25 +203,66 @@ export class TranscriptViewport {
184
203
  return true;
185
204
  }
186
205
  selectedText() {
187
- const selection = this.#visibleSelection();
188
- if (selection === undefined)
206
+ const visible = this.#visibleSelection();
207
+ if (visible === undefined)
189
208
  return null;
190
- const copiedLines = Array.from({ length: Math.max(0, selection.end.line - selection.start.line + 1) }, (_value, index) => selection.start.line + index).filter(line => {
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) {
191
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];
192
252
  return anchor?.kind === "document" && anchor.row < this.#selectableDocumentRowCount
193
253
  || anchor?.kind === "dock" && this.#copyableDockRows.has(anchor.fromBottom);
194
254
  });
195
255
  if (copiedLines.length === 0)
196
256
  return null;
197
- const transcriptLines = copiedLines.filter(line => this.#selectionRowAnchors[line]?.kind === "document");
257
+ const transcriptLines = copiedLines.filter(line => rowAnchors[line]?.kind === "document");
198
258
  const prompt = transcriptLines.length === 0 ? undefined : this.#promptAnchors.find(candidate => transcriptLines.every(line => {
199
- const anchor = this.#selectionRowAnchors[line];
259
+ const anchor = rowAnchors[line];
200
260
  return anchor?.kind === "document" && anchor.row >= candidate.firstRow && anchor.row <= candidate.lastRow;
201
261
  }));
202
262
  const included = new Set(copiedLines);
203
- const text = textSelectionText(selection, this.#selectionRows, line => {
204
- const row = this.#selectionRows[line] ?? "";
205
- const anchor = this.#selectionRowAnchors[line];
263
+ const text = textSelectionText(selection, selectionRows, line => {
264
+ const row = selectionRows[line] ?? "";
265
+ const anchor = rowAnchors[line];
206
266
  const dock = anchor?.kind === "dock" ? this.#copyableDockRows.get(anchor.fromBottom) : undefined;
207
267
  if (prompt !== undefined && anchor?.kind === "document") {
208
268
  const first = anchor.row === prompt.firstRow;
@@ -211,11 +271,12 @@ export class TranscriptViewport {
211
271
  : row;
212
272
  return selectionCopyLineContent(copyRow, first ? "first" : "continuation");
213
273
  }
214
- return dock?.prompt !== undefined ? selectionCopyLineContent(row, dock.prompt) : usefulTextLineContent(row);
274
+ // Invariant: the dock editor has no prefix; transcript prompts still carry chrome.
275
+ return dock?.prompt !== undefined ? usefulTextLineContent(row, 1) : usefulTextLineContent(row);
215
276
  }, line => included.has(line));
216
277
  return text.length === 0 ? null : text;
217
278
  }
218
- /** Captures visible selected text, not off-screen history. */
279
+ /** Captures selected text, including document rows of a visible selection scrolled off-screen. */
219
280
  captureSelectedText() {
220
281
  if (!this.#copyableSelection)
221
282
  return null;
@@ -470,7 +531,7 @@ export class TranscriptViewport {
470
531
  const bottomHovered = pointer !== undefined && pointer.row === bottomHit.row
471
532
  && pointer.column >= bottomHit.columnStart && pointer.column <= bottomHit.columnEnd;
472
533
  control = { row, left, text: `${CONTROL_STYLE_RESET}${theme.bottomControl(label, bottomHovered)}` };
473
- // Invariant: the control floats above selection; its cells are never selected or copied.
534
+ // Invariant: the control floats above selection; copy restores the text beneath it.
474
535
  selectionRows[row] = overlaySpan(padRowPreservingBackground(selectionRows[row] ?? "", width), left, left + labelWidth, " ".repeat(labelWidth));
475
536
  }
476
537
  }
@@ -701,7 +762,7 @@ export class TranscriptViewport {
701
762
  const anchors = this.#selectionAnchors;
702
763
  if (selection === undefined || anchors === undefined)
703
764
  return;
704
- const project = (anchor) => {
765
+ const project = (anchor, moving) => {
705
766
  if (anchor.kind === "dock") {
706
767
  const dockIndex = dockRows.length - anchor.fromBottom - 1;
707
768
  return dockIndex < 0 || dockIndex >= dockRows.length
@@ -743,7 +804,7 @@ export class TranscriptViewport {
743
804
  // Concurrency: a streaming chunk may replace or reflow the pointer's current live-tail
744
805
  // row before the next motion report. During the held gesture only, retain its distance
745
806
  // from the persistent tail; release restores identity-only projection.
746
- if (selection.selecting && anchor.liveFromBottom !== undefined && this.#liveTailStartRow !== undefined) {
807
+ if (selection.selecting && moving && anchor.liveFromBottom !== undefined && this.#liveTailStartRow !== undefined) {
747
808
  const documentRow = this.#selectableDocumentRowCount - anchor.liveFromBottom - 1;
748
809
  if (documentRow >= this.#liveTailStartRow && documentRow < this.#selectableDocumentRowCount) {
749
810
  const visible = this.#selectionRowAnchors.findIndex(candidate => candidate?.kind === "document" && candidate.row === documentRow);
@@ -755,10 +816,18 @@ export class TranscriptViewport {
755
816
  }
756
817
  }
757
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
+ }
758
827
  return undefined;
759
828
  };
760
- const projectedAnchor = project(anchors.anchor);
761
- const projectedHead = project(anchors.head);
829
+ const projectedAnchor = project(anchors.anchor, false);
830
+ const projectedHead = project(anchors.head, true);
762
831
  if (projectedAnchor === undefined || projectedHead === undefined) {
763
832
  this.clearSelection();
764
833
  return;
@@ -778,11 +847,59 @@ export class TranscriptViewport {
778
847
  // Invariant: mixed viewport/dock ranges project across the same complete frame regardless
779
848
  // of origin. Document-only ranges remain clipped to the scrollable viewport when off-screen.
780
849
  const rows = crossesDock ? this.#selectionRows.length : this.#viewportHeight;
781
- return visibleTextSelection(orderedTextSelection(this.#selection), rows, this.#firstSelectableRow);
850
+ const ordered = orderedTextSelection(dockOrderedSelection(this.#selection, anchors, this.#viewportHeight));
851
+ return visibleTextSelection(ordered, rows, this.#firstSelectableRow);
782
852
  }
783
853
  #updateCopyableSelection() {
784
- this.#copyableSelection = this.selectedText() !== null;
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;
785
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
+ };
786
903
  }
787
904
  function visibleTextSelection(selection, rowCount, firstRow) {
788
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 = 17;
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 = 17;
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) {
@@ -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 | Warn at 30 s; restart at `tabsUnresponsiveRestartSeconds` (default 120), preserving journal/screen. A server/bridge transport outage alone is not proof of a child hang. |
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 and stall-notice timing are internal fixed behavior at 120 and 300 seconds; concurrent starts use internal default 2/hard cap 8 and prewarm uses internal count/hard cap 1.
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.
@@ -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 it shows `unresponsive`; 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: model or tool events. After `tabsStallNoticeSeconds` (default 300) it shows `stalled` with interrupt and restart actions. It is never auto-killed, because a legitimate tool can run for a long time.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.2.7-dev.810",
3
+ "version": "0.2.7-dev.820",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "privateLaunchContract": "neutral-launch-v1",