@timurproko/a1 0.2.7-dev.788 → 0.2.7-dev.794

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 (135) hide show
  1. package/dist/app/session-shell/session-shell-root.d.ts +2 -1
  2. package/dist/app/session-shell/session-shell-root.js +41 -15
  3. package/dist/app/session-shell/session-shell.d.ts +1 -0
  4. package/dist/app/session-shell/session-shell.js +15 -4
  5. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -2
  6. package/dist/app/session-shell/session-viewport-controller.js +24 -12
  7. package/dist/cli/tabs.js +1 -1
  8. package/dist/composition/owned-ui.d.ts +2 -0
  9. package/dist/composition/owned-ui.js +42 -4
  10. package/dist/contracts/owned-ui/index.d.ts +1 -1
  11. package/dist/contracts/owned-ui/model.d.ts +3 -0
  12. package/dist/features/owned-ui/settings-app.js +9 -6
  13. package/dist/features/resident-tab/index.d.ts +3 -1
  14. package/dist/features/resident-tab/index.js +5 -1
  15. package/dist/features/resident-tab/shortcut-bindings-file.d.ts +8 -0
  16. package/dist/features/resident-tab/shortcut-bindings-file.js +40 -0
  17. package/dist/features/resident-tab/shortcut-declarations.d.ts +13 -0
  18. package/dist/features/resident-tab/{shortcuts.js → shortcut-declarations.js} +6 -17
  19. package/dist/features/resident-tab/shortcut-projection.d.ts +29 -0
  20. package/dist/features/resident-tab/shortcut-projection.js +64 -0
  21. package/dist/foundation/launch-guardian/exit-watcher.d.ts +14 -0
  22. package/dist/foundation/launch-guardian/exit-watcher.js +94 -0
  23. package/dist/foundation/launch-guardian/main.d.ts +2 -1
  24. package/dist/foundation/launch-guardian/main.js +58 -6
  25. package/dist/foundation/process-containment/{native-guardian-containment.d.ts → addon-containment.d.ts} +7 -5
  26. package/dist/foundation/process-containment/addon-containment.js +139 -0
  27. package/dist/foundation/process-containment/addon-process-inspector.d.ts +11 -0
  28. package/dist/foundation/process-containment/addon-process-inspector.js +36 -0
  29. package/dist/foundation/process-containment/addon.d.ts +42 -0
  30. package/dist/foundation/process-containment/addon.js +57 -0
  31. package/dist/foundation/process-containment/artifact.d.ts +1 -1
  32. package/dist/foundation/process-containment/artifact.js +6 -5
  33. package/dist/foundation/process-containment/contracts.d.ts +2 -0
  34. package/dist/foundation/process-containment/index.d.ts +4 -4
  35. package/dist/foundation/process-containment/index.js +3 -3
  36. package/dist/foundation/process-containment/native-process-inspector.d.ts +2 -1
  37. package/dist/foundation/process-containment/native-process-inspector.js +10 -10
  38. package/dist/foundation/release/bootstrap.js +54 -16
  39. package/dist/foundation/release/index.d.ts +1 -1
  40. package/dist/foundation/release/index.js +1 -1
  41. package/dist/foundation/release/release-store.d.ts +12 -0
  42. package/dist/foundation/release/release-store.js +124 -2
  43. package/dist/foundation/resident-tabs/bounded-host-command.d.ts +3 -1
  44. package/dist/foundation/resident-tabs/bounded-host-command.js +6 -2
  45. package/dist/foundation/resident-tabs/bridge-client.d.ts +7 -0
  46. package/dist/foundation/resident-tabs/bridge-client.js +30 -1
  47. package/dist/foundation/resident-tabs/doctor.d.ts +0 -1
  48. package/dist/foundation/resident-tabs/doctor.js +4 -4
  49. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +7 -2
  50. package/dist/foundation/resident-tabs/ensure-terminal-host.js +15 -11
  51. package/dist/foundation/resident-tabs/host-entry.d.ts +1 -0
  52. package/dist/foundation/resident-tabs/host-entry.js +17 -0
  53. package/dist/foundation/resident-tabs/index.d.ts +2 -2
  54. package/dist/foundation/resident-tabs/index.js +1 -1
  55. package/dist/foundation/resident-tabs/maintenance.d.ts +5 -4
  56. package/dist/foundation/resident-tabs/maintenance.js +32 -17
  57. package/dist/foundation/resident-tabs/prompt-journal.d.ts +0 -2
  58. package/dist/foundation/resident-tabs/prompt-journal.js +0 -1
  59. package/dist/foundation/resident-tabs/release-holds.d.ts +3 -2
  60. package/dist/foundation/resident-tabs/release-holds.js +16 -7
  61. package/dist/foundation/resident-tabs/settings.d.ts +0 -1
  62. package/dist/foundation/resident-tabs/settings.js +1 -7
  63. package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +11 -15
  64. package/dist/foundation/resident-tabs/terminal-host-artifact.js +57 -32
  65. package/dist/foundation/session-writer-lock/index.d.ts +1 -3
  66. package/dist/foundation/session-writer-lock/index.js +8 -6
  67. package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -0
  68. package/dist/integrations/pi/components/owned-editor-ux.js +13 -0
  69. package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -1
  70. package/dist/integrations/pi/components/shell-presenters-info.d.ts +1 -0
  71. package/dist/integrations/pi/components/shell-presenters-info.js +10 -30
  72. package/dist/integrations/pi/components/shell-shared-facade.d.ts +4 -0
  73. package/dist/native/darwin-arm64/manifest.json +10 -7
  74. package/dist/native/darwin-arm64/process-guardian.node +0 -0
  75. package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
  76. package/dist/native/darwin-arm64/terminal-host.manifest.json +16 -14
  77. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  78. package/dist/native/darwin-x64/manifest.json +10 -7
  79. package/dist/native/darwin-x64/process-guardian.node +0 -0
  80. package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
  81. package/dist/native/darwin-x64/terminal-host.manifest.json +16 -14
  82. package/dist/native/darwin-x64/terminal-host.node +0 -0
  83. package/dist/native/linux-x64/manifest.json +10 -7
  84. package/dist/native/linux-x64/process-guardian.node +0 -0
  85. package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
  86. package/dist/native/linux-x64/terminal-host.manifest.json +16 -14
  87. package/dist/native/linux-x64/terminal-host.node +0 -0
  88. package/dist/native/win32-x64/manifest.json +10 -7
  89. package/dist/native/win32-x64/process-guardian.node +0 -0
  90. package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
  91. package/dist/native/win32-x64/terminal-host.manifest.json +16 -14
  92. package/dist/native/win32-x64/terminal-host.node +0 -0
  93. package/dist/product-identity.d.ts +1 -1
  94. package/dist/product-identity.js +1 -1
  95. package/dist/product-identity.json +3 -2
  96. package/dist/runtime-payload-inventory.json +1 -0
  97. package/dist/ui/components/index.d.ts +2 -2
  98. package/dist/ui/components/index.js +1 -1
  99. package/dist/ui/components/selection-copy.d.ts +2 -0
  100. package/dist/ui/components/selection-copy.js +13 -0
  101. package/dist/ui/components/submitted-prompt.d.ts +7 -4
  102. package/dist/ui/components/submitted-prompt.js +18 -10
  103. package/dist/ui/components/transcript-viewport.d.ts +10 -6
  104. package/dist/ui/components/transcript-viewport.js +28 -13
  105. package/dist/ui/settings/declarations.d.ts +13 -13
  106. package/dist/ui/settings/declarations.js +12 -11
  107. package/dist/ui/settings/migrations.js +7 -0
  108. package/docs/architecture/boundaries.md +10 -9
  109. package/docs/architecture/evidence/resident-tabs/macos-x64.md +1 -1
  110. package/docs/architecture/process-guardian-provenance.md +17 -11
  111. package/docs/architecture/project-structure.md +8 -0
  112. package/docs/architecture/resident-tabs.md +54 -36
  113. package/docs/architecture/terminal-host-provenance.md +6 -4
  114. package/docs/architecture/toolchain.md +6 -6
  115. package/docs/openspec-archive-automation.md +3 -3
  116. package/docs/validation.md +7 -3
  117. package/package.json +1 -1
  118. package/dist/features/resident-tab/shortcuts.d.ts +0 -10
  119. package/dist/foundation/process-containment/darwin-process-inspector.d.ts +0 -5
  120. package/dist/foundation/process-containment/darwin-process-inspector.js +0 -7
  121. package/dist/foundation/process-containment/native-guardian-containment.js +0 -152
  122. package/dist/foundation/process-containment/windows-process-inspector.d.ts +0 -19
  123. package/dist/foundation/process-containment/windows-process-inspector.js +0 -56
  124. package/dist/native/darwin-arm64/a1_session_lock.node +0 -0
  125. package/dist/native/darwin-arm64/process-guardian +0 -0
  126. package/dist/native/darwin-arm64/terminal-host +0 -0
  127. package/dist/native/darwin-x64/a1_session_lock.node +0 -0
  128. package/dist/native/darwin-x64/process-guardian +0 -0
  129. package/dist/native/darwin-x64/terminal-host +0 -0
  130. package/dist/native/linux-x64/a1_session_lock.node +0 -0
  131. package/dist/native/linux-x64/process-guardian +0 -0
  132. package/dist/native/linux-x64/terminal-host +0 -0
  133. package/dist/native/win32-x64/a1_session_lock.node +0 -0
  134. package/dist/native/win32-x64/process-guardian.exe +0 -0
  135. package/dist/native/win32-x64/terminal-host.exe +0 -0
@@ -46,6 +46,7 @@ export interface OwnedUiShellEngineOptions {
46
46
  /** Resident tab quit routes detach the outer client without stopping this engine session. */
47
47
  readonly residentTab?: {
48
48
  requestDetach(): Promise<boolean>;
49
+ closeOwnedRoute?(route: string): void;
49
50
  reportStatus?(status: "working" | "idle" | "stalled"): void;
50
51
  readonly stallNoticeSeconds?: number;
51
52
  admit?(input: {
@@ -53,7 +54,6 @@ export interface OwnedUiShellEngineOptions {
53
54
  readonly sessionFile: string | null;
54
55
  readonly kind: "prompt" | "steer" | "follow-up";
55
56
  readonly text: string;
56
- readonly images?: readonly unknown[];
57
57
  }): Promise<{
58
58
  readonly id: string;
59
59
  }>;
@@ -192,6 +192,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
192
192
  readonly requestHyperlinkCleanup?: (rows?: readonly number[]) => void;
193
193
  readonly enableDockInputReuse?: boolean;
194
194
  readonly persistentHistory?: boolean;
195
+ readonly hideSettingsCommand?: boolean;
195
196
  readonly historyEditor?: import("../../integrations/pi/components/index.js").HistoryEditorConstructor;
196
197
  readonly onSubmit: (text: string) => void;
197
198
  readonly onPasteRejected?: (error: unknown) => void;
@@ -96,9 +96,8 @@ export class OwnedUiSessionShellRoot {
96
96
  ...(handlers.promptImageLimit === undefined ? {} : { imageLimit: handlers.promptImageLimit }) });
97
97
  this.#submittedPromptComposer = this.#customViewport
98
98
  ? {
99
- layout: submittedPromptLayout,
100
- compose: (rows, width, source, style) => composeSubmittedPromptRows(rows, width, source, { ...style, prefix: text => promptArrow(text, piTheme()) })
101
- .map(row => nativeHyperlinkStyle(row, nativeTranscriptLinkColor)),
99
+ layout: (width, source) => submittedPromptLayout(width, source, this.#viewportController.config.timeFormat ?? "8:00 PM"),
100
+ compose: (rows, width, source, style) => composeSubmittedPromptRows(rows, width, source, { ...style, prefix: text => promptArrow(text, piTheme()) }, this.#viewportController.config.timeFormat ?? "8:00 PM").map(row => nativeHyperlinkStyle(row, nativeTranscriptLinkColor)),
102
101
  }
103
102
  : undefined;
104
103
  this.#cwd = cwd;
@@ -430,6 +429,23 @@ export class OwnedUiSessionShellRoot {
430
429
  const selectableDocumentRowCount = document.rows.length;
431
430
  const dockStartRow = height - dockRows.length + 1;
432
431
  const editorOffset = dock.editorOffset;
432
+ // Rationale: rendered chip tags are lossy presentation (truncated labels), so a frame drag over
433
+ // a draft holding chips stays visual-only; editor-owned word/line selection still copies the
434
+ // expanded chip text through its semantic route.
435
+ const draft = this.usesDefaultInputSurface() ? this.editor.getText() : "";
436
+ const promptCopyRows = draft.length > 0
437
+ && !draft.split("\n").some(line => this.#promptChips.atomicRanges(line).length > 0)
438
+ ? Math.max(0, dock.inputRows - 2)
439
+ : 0;
440
+ const copyableDockRows = [
441
+ ...Array.from({ length: promptCopyRows }, (_value, index) => ({
442
+ row: editorOffset + index + 1,
443
+ prompt: index === 0 ? "first" : "continuation",
444
+ })),
445
+ ...Array.from({ length: dock.footerRows }, (_value, index) => ({
446
+ row: dockRows.length - dock.footerRows + index,
447
+ })),
448
+ ];
433
449
  this.#viewportController.setInputSurfaceFrame(this.usesDefaultInputSurface() ? undefined : {
434
450
  component: this.#inputSurface,
435
451
  columnStart: 1,
@@ -458,7 +474,7 @@ export class OwnedUiSessionShellRoot {
458
474
  && snapshot.inputSurface === inputSurface
459
475
  && snapshot.viewportRevision === viewportRevision
460
476
  && snapshot.selectionRevision === selectionRevision
461
- ? this.#viewportController.composeDockOnly(dockRows, width, height, dock.footerRows)
477
+ ? this.#viewportController.composeDockOnly(dockRows, width, height, copyableDockRows)
462
478
  : null;
463
479
  if (frame === null) {
464
480
  frame = this.#viewportController.compose({
@@ -474,7 +490,7 @@ export class OwnedUiSessionShellRoot {
474
490
  ...(document.liveTailStartRow === undefined ? {} : { liveTailStartRow: document.liveTailStartRow }),
475
491
  bottomAlignedTailRowCount: steeringRows.length + statusRows.length,
476
492
  dockRows,
477
- explicitCopyDockRows: dock.footerRows,
493
+ copyableDockRows,
478
494
  promptAnchors: document.promptAnchors,
479
495
  width,
480
496
  height,
@@ -541,8 +557,11 @@ export class OwnedUiSessionShellRoot {
541
557
  return this.#viewportController.hasSelection || this.editor.hasSelection();
542
558
  }
543
559
  setViewportConfig(config) {
544
- if (this.#viewportController.setConfig(config))
560
+ if (this.#viewportController.setConfig(config)) {
561
+ this.#renderedRows.clear();
545
562
  this.#documentLayouts.clear();
563
+ this.#visibleViewportSnapshot = undefined;
564
+ }
546
565
  }
547
566
  setFullscreenCopyOnSelect(enabled) {
548
567
  this.#viewportController.setCopyOnSelect(enabled);
@@ -751,11 +770,13 @@ export class OwnedUiSessionShellRoot {
751
770
  }
752
771
  rows.push(...blockRows);
753
772
  if (promptLike && blockRows[0] !== undefined) {
773
+ const source = pinnedPromptSource(block, blockRows[0], blockWidth, this.#viewportController.config.timeFormat);
754
774
  promptAnchors.push({
755
775
  id: block.id,
756
776
  firstRow,
757
777
  lastRow: Math.max(firstRow, rows.length - 1),
758
- sourceRow: pinnedPromptSourceRow(block, blockRows[0], blockWidth),
778
+ sourceRow: source.row,
779
+ copySuffixWidth: source.copySuffixWidth,
759
780
  prominentSourceRow: blockRows[0],
760
781
  });
761
782
  }
@@ -1322,20 +1343,25 @@ function withoutTerminalBackground(text) {
1322
1343
  return text.replace(TERMINAL_BACKGROUND, "");
1323
1344
  }
1324
1345
  /** Preserve the stable metadata-grey foreground for every pinned timestamp state. */
1325
- function pinnedPromptSourceRow(block, sourceRow, width) {
1346
+ function pinnedPromptSource(block, sourceRow, width, timeFormat) {
1347
+ const unchanged = { row: sourceRow, copySuffixWidth: 0 };
1326
1348
  if (typeof block.payload !== "object" || block.payload === null)
1327
- return sourceRow;
1349
+ return unchanged;
1328
1350
  const value = block.payload.timestamp;
1329
1351
  if (typeof value !== "number" || !Number.isFinite(value))
1330
- return sourceRow;
1331
- const timestamp = formatSubmittedPromptTime(value);
1332
- if (timestamp === null || submittedPromptLayout(width, value).timestamp === null)
1333
- return sourceRow;
1352
+ return unchanged;
1353
+ const format = timeFormat ?? "8:00 PM";
1354
+ const timestamp = formatSubmittedPromptTime(value, format);
1355
+ if (timestamp === null || submittedPromptLayout(width, value, format).timestamp === null)
1356
+ return unchanged;
1334
1357
  const rowWidth = displayWidth(sourceRow);
1335
1358
  const timestampWidth = displayWidth(timestamp);
1336
1359
  if (rowWidth < timestampWidth)
1337
- return sourceRow;
1338
- return overlaySpan(sourceRow, rowWidth - timestampWidth, rowWidth, paintPiSubmittedPromptTimestamp(timestamp));
1360
+ return unchanged;
1361
+ return {
1362
+ row: overlaySpan(sourceRow, rowWidth - timestampWidth, rowWidth, paintPiSubmittedPromptTimestamp(timestamp)),
1363
+ copySuffixWidth: timestampWidth,
1364
+ };
1339
1365
  }
1340
1366
  /**
1341
1367
  * The pinned-profile screen: a followed transcript scroll above the dock. The runtime mounts one such layout,
@@ -72,6 +72,7 @@ export declare class OwnedUiSessionPresenter implements OwnedUiHostedPresenter {
72
72
  readonly mode: "regular" | "fullscreen";
73
73
  readonly columns: number;
74
74
  }): OwnedUiPresenterRelease;
75
+ openOwnedRoute(route: string): void;
75
76
  hotkeysPresentation(): PiShellHotkeysPresentation;
76
77
  }
77
78
  /**
@@ -189,6 +189,7 @@ export class OwnedUiSessionPresenter {
189
189
  }),
190
190
  enableDockInputReuse: inputPresentation?.viewportReuse !== false,
191
191
  footerSessionName: this.#residentTabOptions === undefined,
192
+ hideSettingsCommand: this.#residentTabOptions !== undefined,
192
193
  persistentHistory: this.#customViewport && promptHistory !== undefined,
193
194
  ...(promptHistory === undefined ? {} : { historyEditor: promptHistory.editor }),
194
195
  onSubmit: text => { void this.submit(text).catch(() => this.#reportSubmissionError()); },
@@ -270,7 +271,7 @@ export class OwnedUiSessionPresenter {
270
271
  // Invariant: the presenter owns its root (theme subscription and helper clients) and disposes it once the
271
272
  // terminal no longer renders it.
272
273
  constructed.push(() => this.root.dispose());
273
- this.#restoreResidentInput();
274
+ this.#restoreResidentInput(options.history?.imageSidecar);
274
275
  const initialPiSettings = this.backend.settings.snapshot();
275
276
  this.#programStatus = new ProgramStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
276
277
  this.#reloadPresentation = reloadPresentation;
@@ -802,7 +803,6 @@ export class OwnedUiSessionPresenter {
802
803
  sessionFile: this.#currentSessionFile(),
803
804
  kind: type,
804
805
  text: input,
805
- ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
806
806
  });
807
807
  }
808
808
  catch {
@@ -1213,11 +1213,13 @@ export class OwnedUiSessionPresenter {
1213
1213
  this.#resolveStoppedLifecycle();
1214
1214
  }
1215
1215
  }
1216
- #restoreResidentInput() {
1216
+ #restoreResidentInput(imageSidecar) {
1217
1217
  const recovered = this.#residentTabOptions?.recovered;
1218
1218
  if (recovered === undefined)
1219
1219
  return;
1220
- const prompts = recovered.prompts.map(prompt => prompt.text).filter(Boolean);
1220
+ const prompts = recovered.prompts
1221
+ .map(prompt => this.root.rehydrateHistoryText(prompt.text, id => imageSidecar?.readAttachment(id) ?? null))
1222
+ .filter(Boolean);
1221
1223
  if (prompts.length > 0) {
1222
1224
  this.root.editor.setText(prompts[0]);
1223
1225
  if (recovered.draft)
@@ -1342,6 +1344,8 @@ export class OwnedUiSessionPresenter {
1342
1344
  return { outcome: "completed", diagnostic: null };
1343
1345
  }
1344
1346
  if (request.command === "settings" && request.selection === undefined && request.confirmed === undefined) {
1347
+ if (this.#residentTabOptions !== undefined)
1348
+ return rejected("Settings is available from the global Settings tab.");
1345
1349
  this.showSettingsSelector();
1346
1350
  return { outcome: "completed", diagnostic: null };
1347
1351
  }
@@ -1716,6 +1720,7 @@ export class OwnedUiSessionPresenter {
1716
1720
  void onClosed?.()?.catch(() => undefined);
1717
1721
  }
1718
1722
  catch { /* Rationale: a failed acknowledgement stays pending. */ }
1723
+ this.#residentTabOptions?.closeOwnedRoute?.(route);
1719
1724
  }
1720
1725
  },
1721
1726
  });
@@ -1763,6 +1768,9 @@ export class OwnedUiSessionPresenter {
1763
1768
  });
1764
1769
  this.#dialogId = dialog.id;
1765
1770
  }
1771
+ openOwnedRoute(route) {
1772
+ this.#openOwnedRoute(route);
1773
+ }
1766
1774
  hotkeysPresentation() {
1767
1775
  const presentation = this.root.hotkeysPresentation();
1768
1776
  return this.#residentTabOptions === undefined ? presentation : { ...presentation, residentTabs: true };
@@ -1772,6 +1780,9 @@ export class OwnedUiSessionPresenter {
1772
1780
  const separator = body.search(/\s/);
1773
1781
  const name = separator < 0 ? body : body.slice(0, separator);
1774
1782
  const argument = separator < 0 ? "" : body.slice(separator + 1).trimStart();
1783
+ if (name === "settings" && this.#residentTabOptions !== undefined) {
1784
+ return rejected("Settings is available from the global Settings tab.");
1785
+ }
1775
1786
  if (this.#routeHost?.claims(name))
1776
1787
  return this.#openOwnedRoute(name);
1777
1788
  if (this.#isWorkflowRoute(name))
@@ -1,7 +1,7 @@
1
1
  import type { PiTuiPointerSurface } from "../../integrations/pi/tui-runtime/contracts.js";
2
2
  import type { OwnedUiViewportSettings } from "../../contracts/owned-ui/index.js";
3
3
  import { type TranscriptViewportFrame, type TranscriptViewportFrameInput } from "../../ui/components/transcript-viewport.js";
4
- import type { SelectionCopySnapshot } from "../../ui/components/selection-copy.js";
4
+ import { type SelectionCopySnapshot } from "../../ui/components/selection-copy.js";
5
5
  import type { PiShellEditorPort } from "../../integrations/pi/components/shell-shared-facade.js";
6
6
  import type { PasteEvent } from "./paste-protocol.js";
7
7
  export interface SessionViewportControllerOptions {
@@ -64,7 +64,7 @@ export declare class SessionViewportController {
64
64
  setOverlaySurfaces(surfaces: readonly PiTuiPointerSurface[] | null): void;
65
65
  get inputGeometryReady(): boolean;
66
66
  compose(input: Omit<TranscriptViewportFrameInput, "pointerPosition">): TranscriptViewportFrame;
67
- composeDockOnly(dockRows: readonly string[], width: number, height: number, explicitCopyDockRows?: number): TranscriptViewportFrame | null;
67
+ composeDockOnly(dockRows: readonly string[], width: number, height: number, copyableDockRows?: TranscriptViewportFrameInput["copyableDockRows"]): TranscriptViewportFrame | null;
68
68
  /** Returns true when document wrapping may have changed. */
69
69
  setConfig(config: OwnedUiViewportSettings): boolean;
70
70
  resumeFollowing(): void;
@@ -3,6 +3,7 @@ import { TranscriptViewport } from "../../ui/components/transcript-viewport.js";
3
3
  import { readVisibleHyperlinks } from "../../ui/components/visible-hyperlinks.js";
4
4
  import { routeMouseInput } from "../../ui/components/mouse.js";
5
5
  import { scrollForTrackPage, scrollForThumbRow, scrollbarSelectionRows, scrollbarWheelRows } from "../../ui/components/scrollbar.js";
6
+ import { literalSelectionCopySnapshot } from "../../ui/components/selection-copy.js";
6
7
  function withinTailControl(region, column, row) {
7
8
  return row >= region.rowStart && row <= region.rowEnd && column >= region.columnStart && column <= region.columnEnd;
8
9
  }
@@ -22,6 +23,7 @@ export class SessionViewportController {
22
23
  #pasteDiagnosticRequest = 0;
23
24
  #viewport = new TranscriptViewport();
24
25
  #config = {
26
+ timeFormat: "8:00 PM",
25
27
  scrollbarAppearance: "auto",
26
28
  scrollbarStyle: "thin",
27
29
  scrollbarSpeed: "normal",
@@ -165,16 +167,18 @@ export class SessionViewportController {
165
167
  }
166
168
  return frame;
167
169
  }
168
- composeDockOnly(dockRows, width, height, explicitCopyDockRows = 0) {
169
- return this.#viewport.composeDockOnly(dockRows, width, height, explicitCopyDockRows);
170
+ composeDockOnly(dockRows, width, height, copyableDockRows = []) {
171
+ return this.#viewport.composeDockOnly(dockRows, width, height, copyableDockRows);
170
172
  }
171
173
  /** Returns true when document wrapping may have changed. */
172
174
  setConfig(config) {
173
- const appearanceChanged = config.scrollbarAppearance !== this.#config.scrollbarAppearance;
174
- this.#config = config;
175
- this.#viewport.setConfig(config);
175
+ const normalized = { ...config, timeFormat: config.timeFormat ?? "8:00 PM" };
176
+ const layoutChanged = normalized.scrollbarAppearance !== this.#config.scrollbarAppearance
177
+ || normalized.timeFormat !== this.#config.timeFormat;
178
+ this.#config = normalized;
179
+ this.#viewport.setConfig(normalized);
176
180
  this.#requestRender();
177
- return appearanceChanged;
181
+ return layoutChanged;
178
182
  }
179
183
  resumeFollowing() {
180
184
  if (!this.#enabled)
@@ -302,12 +306,10 @@ export class SessionViewportController {
302
306
  });
303
307
  }
304
308
  if (data === "\u0003") {
305
- if (editorActive && this.#editor.hasSelection()) {
306
- if (this.#viewport.clearSelection())
307
- this.#requestRender();
308
- }
309
- else if (this.#viewport.hasSelection) {
310
- const copySelection = this.#viewport.captureSelectedText("explicit");
309
+ // Invariant: the newest complete-frame gesture owns explicit copy even if an
310
+ // older editor word/line selection remains painted beneath it.
311
+ if (this.#viewport.hasSelection) {
312
+ const copySelection = this.#viewport.captureSelectedText();
311
313
  this.#viewport.clearSelection();
312
314
  this.#stopSelectionAutoScroll();
313
315
  this.#requestRender();
@@ -315,6 +317,10 @@ export class SessionViewportController {
315
317
  ? { data: "", consumed: true }
316
318
  : { data: "", consumed: true, copySelection };
317
319
  }
320
+ if (editorActive && this.#editor.hasSelection()) {
321
+ if (this.#viewport.clearSelection())
322
+ this.#requestRender();
323
+ }
318
324
  }
319
325
  const previousPrompt = SHIFT_UP_INPUTS.has(data) || this.#editor.matchesTerminalKey(data, "alt+home");
320
326
  if (editorActive && allowWheel && (previousPrompt || SHIFT_DOWN_INPUTS.has(data))) {
@@ -541,6 +547,12 @@ export class SessionViewportController {
541
547
  const editorRow = pendingEditorClick.row - this.#editorPointerFrame.rowStart + 1;
542
548
  this.#editor.handlePointer({ kind: "press", button: 0, column: pendingEditorClick.column, row: editorRow });
543
549
  this.#editor.handlePointer({ kind: "release", button: 0, column: pendingEditorClick.column, row: editorRow });
550
+ if (this.#copyOnSelect) {
551
+ const selected = this.#editor.selectedText?.();
552
+ if (selected !== undefined) {
553
+ completedCopy = literalSelectionCopySnapshot(selected, this.#viewport.selectionRevision) ?? undefined;
554
+ }
555
+ }
544
556
  }
545
557
  forceRepaint = true;
546
558
  repaint = true;
package/dist/cli/tabs.js CHANGED
@@ -30,7 +30,7 @@ export async function runTabsCommand(request, options = {}) {
30
30
  stateDir: report.stateDir,
31
31
  hostStatus: report,
32
32
  releaseRoots,
33
- settings: { tabsMax: settings.tabsMax, tabsAutoName: settings.tabsAutoName },
33
+ settings: { tabsAutoName: settings.tabsAutoName },
34
34
  });
35
35
  if (request.json)
36
36
  stdout(`${JSON.stringify({ schema: "a1-tabs-doctor-v1", path: bundle.path, bytes: bundle.bytes, dropped: bundle.dropped })}\n`);
@@ -87,4 +87,6 @@ export declare function composeOwnedUiApplication(options?: OwnedUiCompositionOp
87
87
  export declare function composeOwnedUi(options?: OwnedUiCompositionOptions): Promise<OwnedUiComposition>;
88
88
  export declare function bindResidentTabBridge(tabBridge: TabBridgeClient, backend: Pick<ComposedSessionBackend, "identity" | "session" | "workflows" | "generateResidentTabName">, recovery?: Pick<ResidentTabRecovery, "settled"> | null, options?: {
89
89
  readonly autoName?: boolean;
90
+ readonly openRoute?: (route: "settings") => void;
90
91
  }): Promise<() => void>;
92
+ export declare function correlateResidentTabSubmission(recovery: Pick<ResidentTabRecovery, "correlated" | "settled">, backend: Pick<ComposedSessionBackend, "identity" | "session">, submissionId: string, entryId: string): Promise<void>;
@@ -12,6 +12,8 @@ import { createPiSessionPresenters } from "../integrations/pi/session-presenters
12
12
  import { ClipboardDiagnosticCapture } from "../app/session-shell/clipboard-diagnostics.js";
13
13
  import { createOwnedUiClipboardServices } from "../app/session-shell/clipboard-services.js";
14
14
  import { createPiKeybindingsHost } from "../integrations/pi/components/shell-shared-facade.js";
15
+ import { projectResidentTabShortcuts } from "../features/resident-tab/shortcut-projection.js";
16
+ import { readShortcutBindingsFile } from "../features/resident-tab/shortcut-bindings-file.js";
15
17
  import { OwnedUiSessionPresenter, OwnedUiTerminalHost, sessionTerminalHostOptions, } from "../app/session-shell/session-shell.js";
16
18
  import { OwnedSettingsManager } from "../ui/settings/manager.js";
17
19
  import { createPiTerminalBridge } from "../integrations/pi/tui-runtime/presentation-adapter.js";
@@ -100,7 +102,7 @@ export async function composeOwnedUi(options = {}) {
100
102
  namespace: PRODUCT_IDENTITY.commandName,
101
103
  legacyConfigDir: productPaths.configDir,
102
104
  agentProvider: () => backend.settingsPort(),
103
- ...(options.hideTabSettings ? { hiddenOwnedSettingIds: ["tabsMax", "tabsAutoName"] } : {}),
105
+ ...(options.hideTabSettings ? { hiddenOwnedSettingIds: ["tabsAutoName"] } : {}),
104
106
  hiddenAgentSettingIds: ["fullscreenWheelScrollLines"],
105
107
  agentSettingLabelOverrides: { fullscreenCopyOnSelect: "Copy on select" },
106
108
  });
@@ -165,7 +167,7 @@ export async function composeOwnedUi(options = {}) {
165
167
  if (tabBridge !== null && settings !== null && ownedSurfaces)
166
168
  tabBridge.accent(settings.value("accentColor"));
167
169
  let removeControllerBoundaryInput = () => { };
168
- const unsubscribeTabBridge = tabBridge === null ? () => { } : await bindResidentTabBridge(tabBridge, backend, tabRecovery, { autoName: settings?.value("tabsAutoName") ?? true });
170
+ let unsubscribeTabBridge = () => { };
169
171
  let releaseNotes = null;
170
172
  let releaseNotesFailure;
171
173
  if (ownedSurfaces) {
@@ -254,7 +256,18 @@ export async function composeOwnedUi(options = {}) {
254
256
  ? new ClipboardDiagnosticCapture(clipboardDestination) : null;
255
257
  // Invariant: process-wide services are created once here and borrowed by every session shell. The
256
258
  // keybinding registry is applied before any presenter is built, so none of them reads Pi's defaults.
257
- const keybindings = createPiKeybindingsHost({ profile: ownedSurfaces ? "a1" : "pi", agentDir: backend.identity.agentDir });
259
+ const keybindingsHost = createPiKeybindingsHost({ profile: ownedSurfaces ? "a1" : "pi", agentDir: backend.identity.agentDir });
260
+ // Invariant: the resident host dispatches and labels the tab shortcuts this child projects
261
+ // from its effective declarations, so a customization reload republishes the projection.
262
+ const publishTabShortcuts = () => tabBridge?.shortcuts(projectResidentTabShortcuts({
263
+ configured: readShortcutBindingsFile(backend.identity.agentDir),
264
+ reserved: keybindingsHost.manager.getEffectiveConfig(),
265
+ }).assignments);
266
+ const keybindings = tabBridge === null ? keybindingsHost : {
267
+ ...keybindingsHost,
268
+ reload: () => { keybindingsHost.reload(); publishTabShortcuts(); },
269
+ };
270
+ publishTabShortcuts();
258
271
  const clipboardServices = ownedSurfaces ? createOwnedUiClipboardServices() : null;
259
272
  let createdTerminalHost;
260
273
  let terminalHost;
@@ -280,11 +293,15 @@ export async function composeOwnedUi(options = {}) {
280
293
  ...(ownedSurfaces ? { sessionLayout: "custom-viewport" } : {}),
281
294
  ...(tabBridge === null || tabRecovery === null ? {} : { residentTab: {
282
295
  requestDetach: () => tabBridge.requestDetach(),
296
+ closeOwnedRoute: route => {
297
+ if (tabBridge.tabId.startsWith("settings-") && route === "settings")
298
+ void tabBridge.requestClose();
299
+ },
283
300
  reportStatus: status => tabBridge.status(status),
284
301
  stallNoticeSeconds: 300,
285
302
  admit: input => tabRecovery.admit({ ...input, incarnation: options.tab.incarnation }),
286
303
  dispatched: () => tabRecovery.dispatched(),
287
- correlated: (submissionId, entryId) => tabRecovery.correlated(submissionId, entryId),
304
+ correlated: (submissionId, entryId) => correlateResidentTabSubmission(tabRecovery, backend, submissionId, entryId),
288
305
  checkpoint: text => tabRecovery.checkpoint(text),
289
306
  gracefulStop: sessionFile => tabRecovery.gracefulStop(sessionFile),
290
307
  ...(recoveredTabInput === undefined ? {} : { recovered: recoveredTabInput }),
@@ -331,6 +348,12 @@ export async function composeOwnedUi(options = {}) {
331
348
  if (recoveryDegraded)
332
349
  presentRecoveryDegraded(true);
333
350
  terminalHost.attach(presenter);
351
+ if (tabBridge !== null) {
352
+ unsubscribeTabBridge = await bindResidentTabBridge(tabBridge, backend, tabRecovery, {
353
+ autoName: settings?.value("tabsAutoName") ?? true,
354
+ openRoute: route => presenter.openOwnedRoute(route),
355
+ });
356
+ }
334
357
  }
335
358
  catch (error) {
336
359
  await createdTerminalHost?.dispose().catch(() => undefined);
@@ -463,6 +486,12 @@ export async function bindResidentTabBridge(tabBridge, backend, recovery = null,
463
486
  if (command.type === "rename") {
464
487
  void backend.workflows.executeWorkflow({ command: "name", argument: command.name }).catch(() => { });
465
488
  }
489
+ else if (command.type === "route") {
490
+ if (options.openRoute !== undefined)
491
+ options.openRoute(command.route);
492
+ else
493
+ void backend.workflows.executeWorkflow({ command: command.route, argument: "" }).catch(() => { });
494
+ }
466
495
  else if (command.type === "stop") {
467
496
  void backend.session.dispose().catch(() => { });
468
497
  }
@@ -502,6 +531,14 @@ export async function bindResidentTabBridge(tabBridge, backend, recovery = null,
502
531
  unsubscribeEvents();
503
532
  };
504
533
  }
534
+ export async function correlateResidentTabSubmission(recovery, backend, submissionId, entryId) {
535
+ await recovery.correlated(submissionId, entryId);
536
+ // Concurrency: a normal prompt command can resolve after the ready transition already fired.
537
+ // Re-read lifecycle only after correlation is active; a concurrent ready observer and this path
538
+ // safely race because recovery consumes the active entry once and retirement is idempotent.
539
+ if (backend.session.view().lifecycle !== "busy")
540
+ await recovery.settled(currentBackendSessionFile(backend));
541
+ }
505
542
  function currentBackendSessionFile(backend) {
506
543
  const direct = backend.identity.currentSessionFile?.();
507
544
  if (direct !== undefined && direct !== null)
@@ -523,6 +560,7 @@ function quitOutroSettingsSnapshot(settings) {
523
560
  }
524
561
  function viewportSettingsSnapshot(settings) {
525
562
  return {
563
+ timeFormat: settings.value("timeFormat"),
526
564
  scrollbarAppearance: settings.value("scrollbarAppearance"),
527
565
  scrollbarStyle: settings.value("scrollbarStyle"),
528
566
  scrollbarSpeed: settings.value("scrollbarSpeed"),
@@ -3,7 +3,7 @@ export type UiAccentColor = "purple" | "blue" | "cyan" | "green" | "orange" | "p
3
3
  export { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, assertOwnedUiExtensionUiPort, } from "./extension-ui.js";
4
4
  export type { OwnedUiExtensionAutocompleteItem, OwnedUiExtensionAutocompleteProvider, OwnedUiExtensionAutocompleteProviderFactory, OwnedUiExtensionAutocompleteSuggestions, OwnedUiExtensionComponent, OwnedUiExtensionComponentFactory, OwnedUiExtensionComponentFactoryContext, OwnedUiExtensionCustomFactory, OwnedUiExtensionCustomOptions, OwnedUiExtensionDialogOptions, OwnedUiExtensionEditorComponent, OwnedUiExtensionEditorFactory, OwnedUiExtensionEditorThemePort, OwnedUiExtensionEntryRenderInput, OwnedUiExtensionFooterDataPort, OwnedUiExtensionFooterFactory, OwnedUiExtensionInputListener, OwnedUiExtensionKeybindingsPort, OwnedUiExtensionMessageRenderInput, OwnedUiExtensionNotificationSeverity, OwnedUiExtensionOverlayAnchor, OwnedUiExtensionOverlayHandle, OwnedUiExtensionOverlayMargin, OwnedUiExtensionOverlayOptions, OwnedUiExtensionRenderCallback, OwnedUiExtensionRendererPort, OwnedUiExtensionRuntimePort, OwnedUiExtensionSelectListThemePort, OwnedUiExtensionSizeValue, OwnedUiExtensionTerminalPort, OwnedUiExtensionThemeBackground, OwnedUiExtensionThemeColor, OwnedUiExtensionThemeInfo, OwnedUiExtensionThemePort, OwnedUiExtensionToolRenderContext, OwnedUiExtensionUiCallback, OwnedUiExtensionUiPort, OwnedUiExtensionUiProperty, OwnedUiExtensionWidgetOptions, OwnedUiExtensionWidgetPlacement, OwnedUiExtensionWorkingIndicatorOptions, } from "./extension-ui.js";
5
5
  export { OWNED_UI_CONTRACT_VERSION } from "./model.js";
6
- export type { OwnedUiBackgroundSettingsPort, OwnedUiBackgroundStyle, OwnedUiBlockStatus, OwnedUiCommand, OwnedUiCommandOutcome, OwnedUiCorrelationId, OwnedUiCustomization, OwnedUiDiagnostics, OwnedUiDialog, OwnedUiEditorState, OwnedUiEntityId, OwnedUiEvent, OwnedUiFooterView, OwnedUiImageAttachment, OwnedUiLifecycle, OwnedUiModelInfo, OwnedUiOverlay, OwnedUiPromptSuggestionGeneratorPort, OwnedUiPromptSuggestionIdentity, OwnedUiPromptSuggestionReasoning, OwnedUiPromptSuggestionRequest, OwnedUiPromptSuggestionResult, OwnedUiPromptSuggestionState, OwnedUiQuitOutroSettings, OwnedUiQuitOutroSettingsPort, OwnedUiSessionId, OwnedUiSessionViewModel, OwnedUiSeverity, OwnedUiSlotId, OwnedUiSnapshot, OwnedUiStatusView, OwnedUiTerminalSurface, OwnedUiThinkingLevel, OwnedUiToolRendering, OwnedUiToolResultPart, OwnedUiToolState, OwnedUiTranscriptBlock, OwnedUiTranscriptBlockKind, OwnedUiTranscriptImageReference, OwnedUiUsageView, OwnedUiViewportSettings, OwnedUiViewportSettingsPort, } from "./model.js";
6
+ export type { OwnedUiBackgroundSettingsPort, OwnedUiBackgroundStyle, OwnedUiBlockStatus, OwnedUiCommand, OwnedUiCommandOutcome, OwnedUiCorrelationId, OwnedUiCustomization, OwnedUiDiagnostics, OwnedUiDialog, OwnedUiEditorState, OwnedUiEntityId, OwnedUiEvent, OwnedUiFooterView, OwnedUiImageAttachment, OwnedUiLifecycle, OwnedUiModelInfo, OwnedUiOverlay, OwnedUiPromptSuggestionGeneratorPort, OwnedUiPromptSuggestionIdentity, OwnedUiPromptSuggestionReasoning, OwnedUiPromptSuggestionRequest, OwnedUiPromptSuggestionResult, OwnedUiPromptSuggestionState, OwnedUiQuitOutroSettings, OwnedUiQuitOutroSettingsPort, OwnedUiSessionId, OwnedUiSessionViewModel, OwnedUiSeverity, OwnedUiSlotId, OwnedUiSnapshot, OwnedUiStatusView, OwnedUiTerminalSurface, OwnedUiThinkingLevel, OwnedUiTimeFormat, OwnedUiToolRendering, OwnedUiToolResultPart, OwnedUiToolState, OwnedUiTranscriptBlock, OwnedUiTranscriptBlockKind, OwnedUiTranscriptImageReference, OwnedUiUsageView, OwnedUiViewportSettings, OwnedUiViewportSettingsPort, } from "./model.js";
7
7
  export type { OwnedUiExtensionPort, OwnedUiSessionBackend, OwnedUiSessionCatalogPort, OwnedUiSessionFactory, OwnedUiSessionIdentityPort, OwnedUiSessionPort, OwnedUiSessionRequest, OwnedUiSessionSettingsPort, OwnedUiWorkflowPort, } from "./session-backend.js";
8
8
  export type { OwnedUiDialogHost, OwnedUiInputSurface, OwnedUiPresenterHost, OwnedUiSessionPresenter, OwnedUiSessionPresenters, OwnedUiShortcutDescription, OwnedUiTerminalHost, OwnedUiTranscriptRendererPort, } from "./session-presenters.js";
9
9
  export { OWNED_UI_HIDDEN_COMMAND_NAMES, OWNED_UI_MODELS_COMMAND_NAME, OWNED_UI_WORKFLOW_COMMAND_NAMES, } from "./session-workflows.js";
@@ -53,7 +53,10 @@ export interface OwnedUiPromptSuggestionGeneratorPort {
53
53
  generate(request: OwnedUiPromptSuggestionRequest): Promise<OwnedUiPromptSuggestionResult>;
54
54
  }
55
55
  export type OwnedUiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
56
+ export type OwnedUiTimeFormat = "20:00" | "8:00 PM";
56
57
  export interface OwnedUiViewportSettings {
58
+ /** Omitted by older SDK callers; bare A1 composition always supplies the owned default. */
59
+ readonly timeFormat?: OwnedUiTimeFormat;
57
60
  readonly scrollbarAppearance: "auto" | "always" | "hidden";
58
61
  readonly scrollbarStyle: "thin" | "thick";
59
62
  /** normal is baseline; fast is 2x normal; high is normal plus fast. */
@@ -1,5 +1,5 @@
1
1
  import { DIALOG_BACK_SHORTCUT_HINT, DIALOG_CLOSE_SHORTCUT_HINT } from "../../contracts/presentation/index.js";
2
- import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, numericValues, RAIL_COLUMNS, renderDialogPanel, renderSteppedDialogPanel, renderEmptyState, renderGroupHeader, renderInputRow, renderListRow, renderNote, renderShortcutHintsWithClose, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, truncateToWidth, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
2
+ import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, numericValues, RAIL_COLUMNS, renderDialogPanel, renderSteppedDialogPanel, renderEmptyState, renderGroupHeader, renderInputRow, renderListRow, renderNote, renderShortcutHintsWithClose, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, truncateToWidth, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, formatLocalTime, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
3
3
  import { SETTINGS_APP_ID } from "./settings-route.js";
4
4
  export { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
5
5
  const SCOPE = SETTINGS_APP_ID;
@@ -921,7 +921,7 @@ export class SettingsApp {
921
921
  ? CONFIGURE
922
922
  : shown === null
923
923
  ? describeRaw(entry.rawValue)
924
- : displayValue(shown);
924
+ : displaySettingValue(entry, shown);
925
925
  const range = rangeOf(entry);
926
926
  return {
927
927
  key,
@@ -938,11 +938,11 @@ export class SettingsApp {
938
938
  return lines;
939
939
  }
940
940
  const state = {
941
- choices: menu.choices.map(choice => displayValue(choice)),
941
+ choices: menu.choices.map(choice => displaySettingValue(menu.entry, choice)),
942
942
  ...(menu.entry.id === "accentColor" ? {
943
943
  previews: menu.choices.map(choice => theme.accentPreview?.(String(choice), "■") ?? ""),
944
944
  } : {}),
945
- current: this.#shownValue(menu.entry) === null ? null : displayValue(this.#shownValue(menu.entry)),
945
+ current: this.#shownValue(menu.entry) === null ? null : displaySettingValue(menu.entry, this.#shownValue(menu.entry)),
946
946
  index: menu.index,
947
947
  };
948
948
  const frame = valueMenuFrame(state, {
@@ -1084,8 +1084,11 @@ function labelOf(entry) {
1084
1084
  function rangeOf(entry) {
1085
1085
  return { minimum: entry.minimum, maximum: entry.maximum, values: numericValues(entry.choices) };
1086
1086
  }
1087
- /** Booleans read as yes and no; everything else prints as itself. */
1088
- function displayValue(value) {
1087
+ /** Booleans read as yes and no; time-format choices preview the current local time. */
1088
+ function displaySettingValue(entry, value) {
1089
+ if (entry.backend === "a1" && entry.id === "timeFormat" && (value === "20:00" || value === "8:00 PM")) {
1090
+ return formatLocalTime(new Date(), value) ?? value;
1091
+ }
1089
1092
  if (typeof value === "boolean")
1090
1093
  return value ? "yes" : "no";
1091
1094
  return String(value);
@@ -1,3 +1,5 @@
1
1
  export { AUTO_NAME_MAX_LENGTH, AUTO_NAME_TIMEOUT_MS, AUTO_NAME_TOKEN_BUDGETS, fallbackAutoName, generateAutoName, normalizeAutoName, type AutoNameCompletion, } from "./auto-name.js";
2
- export { RESIDENT_TAB_SHORTCUTS, residentTabShortcutRegistry, type ResidentTabAction, type ResidentTabShortcut, } from "./shortcuts.js";
2
+ export { RESIDENT_TAB_SHORTCUTS, type ResidentTabAction, type ResidentTabShortcut, } from "./shortcut-declarations.js";
3
+ export { projectResidentTabShortcuts, RESIDENT_TAB_GLOBAL_SHORTCUTS, type ResidentTabGlobalAction, type ResidentTabShortcutConflict, type ResidentTabShortcutProjection, type ShortcutBindings, } from "./shortcut-projection.js";
4
+ export { readShortcutBindingsFile } from "./shortcut-bindings-file.js";
3
5
  export { deriveResidentTabStatus, type ResidentTabDerivedStatus, type ResidentTabFacts, } from "./status-deriver.js";
@@ -1,3 +1,7 @@
1
1
  export { AUTO_NAME_MAX_LENGTH, AUTO_NAME_TIMEOUT_MS, AUTO_NAME_TOKEN_BUDGETS, fallbackAutoName, generateAutoName, normalizeAutoName, } from "./auto-name.js";
2
- export { RESIDENT_TAB_SHORTCUTS, residentTabShortcutRegistry, } from "./shortcuts.js";
2
+ // Performance: the registry builder lives in `shortcuts.ts`, outside this barrel, so startup
3
+ // presentation can read the declarations without loading the component registry.
4
+ export { RESIDENT_TAB_SHORTCUTS, } from "./shortcut-declarations.js";
5
+ export { projectResidentTabShortcuts, RESIDENT_TAB_GLOBAL_SHORTCUTS, } from "./shortcut-projection.js";
6
+ export { readShortcutBindingsFile } from "./shortcut-bindings-file.js";
3
7
  export { deriveResidentTabStatus, } from "./status-deriver.js";
@@ -0,0 +1,8 @@
1
+ import type { ShortcutBindings } from "./shortcut-projection.js";
2
+ /** Largest `keybindings.json` the projection reads; a larger file counts as absent. */
3
+ export declare const MAX_SHORTCUT_BINDINGS_BYTES: number;
4
+ /**
5
+ * Reads the user's `keybindings.json` as shortcut bindings, keeping `null` entries so an
6
+ * explicit unbind survives, and treating any unreadable or malformed file as empty.
7
+ */
8
+ export declare function readShortcutBindingsFile(agentDir: string): ShortcutBindings;
@@ -0,0 +1,40 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /** Largest `keybindings.json` the projection reads; a larger file counts as absent. */
4
+ export const MAX_SHORTCUT_BINDINGS_BYTES = 256 * 1024;
5
+ /**
6
+ * Reads the user's `keybindings.json` as shortcut bindings, keeping `null` entries so an
7
+ * explicit unbind survives, and treating any unreadable or malformed file as empty.
8
+ */
9
+ export function readShortcutBindingsFile(agentDir) {
10
+ let text;
11
+ try {
12
+ text = readFileSync(join(agentDir, "keybindings.json"), "utf8");
13
+ }
14
+ catch {
15
+ return {};
16
+ }
17
+ if (text.length > MAX_SHORTCUT_BINDINGS_BYTES)
18
+ return {};
19
+ let parsed;
20
+ try {
21
+ parsed = JSON.parse(text.replace(/^/u, ""));
22
+ }
23
+ catch {
24
+ return {};
25
+ }
26
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
27
+ return {};
28
+ const bindings = {};
29
+ for (const [action, value] of Object.entries(parsed)) {
30
+ if (action === "$schema")
31
+ continue;
32
+ if (value === null || typeof value === "string") {
33
+ bindings[action] = value;
34
+ continue;
35
+ }
36
+ if (Array.isArray(value))
37
+ bindings[action] = value.filter((key) => typeof key === "string");
38
+ }
39
+ return bindings;
40
+ }
@@ -0,0 +1,13 @@
1
+ export type ResidentTabAction = "tabs.new" | "tabs.close" | "tabs.rename" | "tabs.settings" | "tabs.retry" | "tabs.fresh" | "tabs.next" | "tabs.previous" | "tabs.moveRight" | "tabs.moveLeft" | `tabs.jump${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10}`;
2
+ export interface ResidentTabShortcut {
3
+ readonly id: ResidentTabAction;
4
+ readonly defaultKey: string | null;
5
+ readonly scope: "global" | "failed-banner";
6
+ readonly description: string;
7
+ }
8
+ /**
9
+ * The tab shortcut declarations, in the order that decides which action keeps a duplicated key.
10
+ * This module stays dependency-free so startup presentation and the resident projection can
11
+ * read it without loading the component registry.
12
+ */
13
+ export declare const RESIDENT_TAB_SHORTCUTS: readonly ResidentTabShortcut[];