@timurproko/a1 0.1.8-dev.528 → 0.1.8-dev.544

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 (108) hide show
  1. package/bin/update-recovery.js +25 -3
  2. package/dist/app/session-shell/session-shell-root.d.ts +1 -0
  3. package/dist/app/session-shell/session-shell-root.js +3 -1
  4. package/dist/app/session-shell/session-shell.d.ts +1 -1
  5. package/dist/app/session-shell/session-shell.js +10 -4
  6. package/dist/composition/owned-ui.js +4 -3
  7. package/dist/composition/settings-route-host.d.ts +5 -9
  8. package/dist/composition/settings-route-host.js +2 -2
  9. package/dist/contracts/owned-ui/model.d.ts +4 -0
  10. package/dist/contracts/owned-ui/validation.js +25 -0
  11. package/dist/features/owned-ui/index.d.ts +1 -1
  12. package/dist/features/owned-ui/reference-screen-app.d.ts +9 -3
  13. package/dist/features/owned-ui/reference-screen-app.js +68 -20
  14. package/dist/features/owned-ui/settings-app.js +69 -24
  15. package/dist/foundation/release/index.d.ts +2 -2
  16. package/dist/foundation/release/index.js +1 -1
  17. package/dist/foundation/release/update-recovery.d.ts +7 -1
  18. package/dist/foundation/release/update-recovery.js +39 -9
  19. package/dist/foundation/release/update.d.ts +7 -0
  20. package/dist/foundation/release/update.js +115 -24
  21. package/dist/foundation/startup/startup-descriptor.js +2 -2
  22. package/dist/integrations/pi/components/index.d.ts +3 -1
  23. package/dist/integrations/pi/components/index.js +2 -1
  24. package/dist/integrations/pi/components/shell-editor-autocomplete.d.ts +9 -1
  25. package/dist/integrations/pi/components/shell-editor-autocomplete.js +10 -9
  26. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  27. package/dist/integrations/pi/components/shell-footer-status.js +2 -2
  28. package/dist/integrations/pi/components/shell-hotkey-sections.d.ts +7 -0
  29. package/dist/integrations/pi/components/shell-hotkey-sections.js +29 -0
  30. package/dist/integrations/pi/components/shell-presenters-info.d.ts +1 -0
  31. package/dist/integrations/pi/components/shell-presenters-info.js +1 -1
  32. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +0 -1
  33. package/dist/integrations/pi/components/shell-selectors-dialogs.js +1 -7
  34. package/dist/integrations/pi/components/thinking-selector-dialog.d.ts +8 -0
  35. package/dist/integrations/pi/components/thinking-selector-dialog.js +22 -0
  36. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  37. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +1 -1
  38. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  39. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  40. package/dist/integrations/pi/components/upstream/components/daxnuts.d.ts +1 -1
  41. package/dist/integrations/pi/components/upstream/components/daxnuts.js +1 -1
  42. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  43. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -1
  44. package/dist/integrations/pi/components/upstream/components/extension-editor.js +7 -2
  45. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  46. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
  47. package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
  48. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  49. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  50. package/dist/integrations/pi/components/upstream/components/session-footer.js +22 -11
  51. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  52. package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
  53. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
  54. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
  55. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +1 -1
  56. package/dist/integrations/pi/components/upstream/components/status-indicator.js +1 -1
  57. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +33 -0
  58. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +140 -0
  59. package/dist/integrations/pi/components/upstream/components/tool-execution.js +1 -1
  60. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  61. package/dist/integrations/pi/components/upstream/components/tree-selector.js +8 -1
  62. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  63. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  64. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +1 -1
  65. package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
  66. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  67. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  68. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  69. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  70. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  71. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  72. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  73. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  74. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
  75. package/dist/integrations/pi/components/upstream/theme/theme.js +1 -1
  76. package/dist/integrations/pi/engine/adapter.js +1 -0
  77. package/dist/integrations/pi/engine/repository-pr.d.ts +8 -0
  78. package/dist/integrations/pi/engine/repository-pr.js +62 -0
  79. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  80. package/dist/integrations/pi/engine/session-runtime.d.ts +5 -0
  81. package/dist/integrations/pi/engine/session-runtime.js +60 -1
  82. package/dist/integrations/pi/engine/workflow-support.js +3 -1
  83. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  84. package/dist/integrations/pi/engine/workflows.js +4 -2
  85. package/dist/integrations/pi/startup-public.js +63 -56
  86. package/dist/integrations/pi/startup-public.manifest.json +200 -169
  87. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
  88. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
  89. package/dist/native/darwin-arm64/manifest.json +1 -1
  90. package/dist/native/linux-x64/manifest.json +1 -1
  91. package/dist/native/win32-x64/manifest.json +2 -2
  92. package/dist/native/win32-x64/process-guardian.exe +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 +1 -0
  96. package/dist/runtime-payload-inventory.json +2948 -2646
  97. package/dist/ui/components/index.d.ts +1 -1
  98. package/dist/ui/components/list-block.d.ts +10 -6
  99. package/dist/ui/components/list-block.js +28 -15
  100. package/dist/ui/components/list-view.d.ts +1 -1
  101. package/dist/ui/components/list-view.js +4 -2
  102. package/dist/ui/components/mouse.js +4 -2
  103. package/dist/ui/components/theme.d.ts +1 -5
  104. package/dist/ui/components/theme.js +0 -4
  105. package/dist/ui/components/value-menu.d.ts +3 -1
  106. package/dist/ui/components/value-menu.js +13 -5
  107. package/docs/local-worktree-cleanup.md +1 -1
  108. package/package.json +3 -3
@@ -5,7 +5,7 @@
5
5
  import { spawn } from "node:child_process";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { chmod, lstat, mkdir, open, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
8
- import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
8
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
 
11
11
  const SCHEMA = "a1-update-recovery-v1";
@@ -200,9 +200,15 @@ async function readCapsule(path) {
200
200
  const packageRoot = await realpath(value.packageRoot).catch(() => resolve(value.packageRoot));
201
201
  const expectedPackage = resolve(globalRoot, ...value.packageName.split("/"));
202
202
  if (!samePath(packageRoot, expectedPackage) || !containedBy(globalRoot, packageRoot)) throw new Error("A1 update recovery package root escapes npm global root");
203
- const expectedNpmArguments = ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${value.packageName}@${value.targetVersion}`];
204
- if (JSON.stringify(value.npmArguments) !== JSON.stringify(expectedNpmArguments)) throw new Error("A1 update recovery npm arguments are invalid");
203
+ const prefix = npmPrefixForGlobalRoot(globalRoot);
204
+ const expectedNpmArguments = ["install", "--global", "--prefix", prefix, "--loglevel=error", "--no-fund", "--no-audit", `${value.packageName}@${value.targetVersion}`];
205
+ const legacyNpmArguments = ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${value.packageName}@${value.targetVersion}`];
206
+ if (![expectedNpmArguments, legacyNpmArguments].some(arguments_ => sameArguments(value.npmArguments, arguments_))) {
207
+ throw new Error("A1 update recovery npm arguments are invalid");
208
+ }
205
209
  const launcherRoot = resolve(value.launcherRoot);
210
+ const expectedLauncherRoot = process.platform === "win32" ? prefix : resolve(prefix, "bin");
211
+ if (!samePath(launcherRoot, expectedLauncherRoot)) throw new Error("A1 update recovery launcher root is invalid");
206
212
  const expectedLaunchers = process.platform === "win32"
207
213
  ? [resolve(launcherRoot, "a1"), resolve(launcherRoot, "a1.cmd"), resolve(launcherRoot, "a1.ps1")]
208
214
  : [resolve(launcherRoot, "a1")];
@@ -306,6 +312,22 @@ async function writeJson(path, value) {
306
312
  await rename(temporary, path);
307
313
  }
308
314
 
315
+ /** @param {string} globalRoot */
316
+ function npmPrefixForGlobalRoot(globalRoot) {
317
+ const root = resolve(globalRoot);
318
+ const nodeModules = process.platform === "win32" ? basename(root).toLowerCase() === "node_modules" : basename(root) === "node_modules";
319
+ if (!nodeModules) throw new Error("A1 update recovery npm global root layout is invalid");
320
+ if (process.platform === "win32") return dirname(root);
321
+ const libraryRoot = dirname(root);
322
+ if (basename(libraryRoot) !== "lib") throw new Error("A1 update recovery npm global root layout is invalid");
323
+ return dirname(libraryRoot);
324
+ }
325
+
326
+ /** @param {string[]} left @param {string[]} right */
327
+ function sameArguments(left, right) {
328
+ return left.length === right.length && left.every((value, index) => value === right[index]);
329
+ }
330
+
309
331
  /**
310
332
  * @param {string} left
311
333
  * @param {string} right
@@ -265,6 +265,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
265
265
  readonly dockOnly: number;
266
266
  };
267
267
  transcriptRenderCount(): number;
268
+ setFooterLevel(visible: boolean): void;
268
269
  setInputSurface(component: PiShellComponentPort | null, disposePrevious?: boolean, coordination?: Exclude<PiTuiInputSurfaceKind, "editor">): void;
269
270
  handleInput(data: string): void;
270
271
  invalidate(): void;
@@ -51,6 +51,7 @@ export class OwnedUiSessionShellRoot {
51
51
  #componentRuntime;
52
52
  #toolsExpanded = false;
53
53
  #thinkingVisible = true;
54
+ #footerLevel = true;
54
55
  #mermaidRenderingMode = "off";
55
56
  #showImages = true;
56
57
  #imageWidthCells = 80;
@@ -107,7 +108,7 @@ export class OwnedUiSessionShellRoot {
107
108
  this.resources = createPiShellLoadedResources(startup.resources ?? [], startup.expanded ?? false);
108
109
  this.#status = createPiShellStatus(view, progressStatusText, handlers);
109
110
  this.#status.setProgressPresentation(this.#customViewport ? "custom-viewport" : "pinned");
110
- this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd, this.#customViewport ? "a1" : "pi");
111
+ this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd, this.#customViewport ? "a1" : "pi", () => this.#footerLevel);
111
112
  this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions, this.#customViewport ? "custom-viewport" : "pinned");
112
113
  this.editor = createPiShellEditor({
113
114
  ...handlers,
@@ -970,6 +971,7 @@ export class OwnedUiSessionShellRoot {
970
971
  transcriptRenderCount() {
971
972
  return this.#transcriptBlockRenders;
972
973
  }
974
+ setFooterLevel(visible) { this.#footerLevel = visible; }
973
975
  setInputSurface(component, disposePrevious = true, coordination = "owned") {
974
976
  const next = component ?? this.editor;
975
977
  const nextCoordination = next === this.editor ? "editor" : coordination;
@@ -36,7 +36,7 @@ export declare class OwnedUiSessionShell {
36
36
  queueFollowUp(): Promise<AdapterCommandResult>;
37
37
  restoreQueuedInput(): void;
38
38
  showSelector(title: string, options: readonly PiShellSelectorOption[], onSelect: (id: string) => void, onCancel?: () => void): PiTuiOverlayHandle;
39
- showThinkingSelector(): void;
39
+ showThinkingSelector(): Promise<void>;
40
40
  /** The Skills dialog: a replacement input like the model selector, applying the chosen skill through the prompt path. */
41
41
  showSkillsSelector(skills?: readonly PiShellSkillSummary[]): void;
42
42
  showModelSelector(initialSearchInput?: string): void;
@@ -14,7 +14,7 @@ import { createPiExtensionUiBridge } from "../../integrations/pi/components/shel
14
14
  import { createPiShellEditor } from "../../integrations/pi/components/shell-editor-autocomplete.js";
15
15
  import { SKILLS_COMMAND_NAME, findSkillByArgument, rewriteSkillsTunnelSubmission, skillPrompt, skillsFromCommands, } from "../../integrations/pi/components/skills-command.js";
16
16
  import { createPiQueuedInputStatus, createPiShellFooter, createPiShellHeader, createPiShellLoadedResources, createPiShellStatus, } from "../../integrations/pi/components/shell-footer-status.js";
17
- import { createPiShellArmin, createPiShellAuthProviderSelector, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellModelSelector, createPiShellModelsDialog, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellSkillsSelector, createPiShellThinkingSelector, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
17
+ import { createPiShellArmin, createPiShellAuthProviderSelector, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellModelSelector, createPiShellModelsDialog, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellSkillsSelector, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
18
18
  import { createPiShellChangelog, createPiShellHotkeys, createPiShellSessionInfo, renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
19
19
  import { CHANGELOG_ROUTE } from "../../features/owned-ui/reference-routes.js";
20
20
  import { createPiShellTranscriptComponent, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellTranscriptBlock, } from "../../integrations/pi/components/shell-presenters-transcript.js";
@@ -839,9 +839,10 @@ export class OwnedUiSessionShell {
839
839
  this.#dialogHandle = handle;
840
840
  return handle;
841
841
  }
842
- showThinkingSelector() {
842
+ async showThinkingSelector() {
843
843
  const snapshot = this.backend.pinnedSettingsSnapshot();
844
844
  const close = () => {
845
+ this.root.setFooterLevel(true);
845
846
  this.root.setInputSurface(null);
846
847
  this.runtime.requestRender();
847
848
  };
@@ -849,7 +850,12 @@ export class OwnedUiSessionShell {
849
850
  close();
850
851
  void this.runWorkflow({ command: "thinking", argument: "", selection: level, ...(persist ? { persist: true } : {}) });
851
852
  };
852
- const component = createPiShellThinkingSelector(snapshot.thinkingLevel, snapshot.availableThinkingLevels, level => select(level, false), close, level => select(level, true), snapshot.defaultThinkingLevel);
853
+ const { createPiShellThinkingSelector } = await import("../../integrations/pi/components/thinking-selector-dialog.js");
854
+ const component = createPiShellThinkingSelector(snapshot.thinkingLevel, snapshot.availableThinkingLevels, level => select(level, false), close, level => select(level, true), snapshot.defaultThinkingLevel, this.#customViewport ? {
855
+ profile: "bare",
856
+ cycleBinding: this.root.editor.keybindingConfig()["app.thinking.cycle"] ?? [],
857
+ } : undefined);
858
+ this.root.setFooterLevel(false);
853
859
  this.root.setInputSurface(component);
854
860
  this.runtime.requestRender();
855
861
  }
@@ -1188,7 +1194,7 @@ export class OwnedUiSessionShell {
1188
1194
  return { outcome: "completed", diagnostic: null };
1189
1195
  }
1190
1196
  if (request.command === "thinking" && request.selection === undefined && request.argument.trim().length === 0) {
1191
- this.showThinkingSelector();
1197
+ await this.showThinkingSelector();
1192
1198
  return { outcome: "completed", diagnostic: null };
1193
1199
  }
1194
1200
  if (request.command === "trust" && request.selection === undefined && request.confirmed === undefined) {
@@ -12,7 +12,7 @@ import { OwnedUiSessionShell } from "../app/session-shell/session-shell.js";
12
12
  import { OwnedSettingsManager } from "../ui/settings/manager.js";
13
13
  import { createPiTerminalBridge } from "../integrations/pi/tui-runtime/presentation-adapter.js";
14
14
  import { createOwnedRouteHost } from "./settings-route-host.js";
15
- import { renderPiShellChangelogLines, renderPiShellHotkeysLines } from "../integrations/pi/components/shell-presenters-info.js";
15
+ import { renderPiShellChangelogLines } from "../integrations/pi/components/shell-presenters-info.js";
16
16
  import { readPinnedCommandChangelog } from "../integrations/pi/engine/changelog.js";
17
17
  export async function composeOwnedUiApplication(options = {}) {
18
18
  return (await composeOwnedUi(options)).application;
@@ -47,11 +47,12 @@ export async function composeOwnedUi(options = {}) {
47
47
  const references = {
48
48
  changelog: async (input) => {
49
49
  const markdown = input?.document ?? await readPinnedCommandChangelog();
50
- return width => renderPiShellChangelogLines(markdown, width);
50
+ return { rows: width => renderPiShellChangelogLines(markdown, width) };
51
51
  },
52
52
  hotkeys: async () => {
53
53
  const presentation = shell.hotkeysPresentation();
54
- return width => renderPiShellHotkeysLines(presentation, width);
54
+ const { renderPiShellHotkeySections } = await import("../integrations/pi/components/shell-hotkey-sections.js");
55
+ return { sections: width => renderPiShellHotkeySections(presentation, width) };
55
56
  },
56
57
  };
57
58
  const routeHost = settings === null || !ownedSurfaces ? null : createOwnedRouteHost(settings, references);
@@ -1,16 +1,12 @@
1
+ import type { ReferenceDocumentProvider } from "../features/owned-ui/reference-screen-app.js";
1
2
  import type { OwnedSettingsManager } from "../ui/settings/manager.js";
2
3
  import type { UiRouteHost, UiRouteInput } from "../ui/apps/contracts.js";
3
4
  /** Styled document rows for one content width. */
4
- export type OwnedReferenceRows = (width: number) => readonly string[];
5
- /**
6
- * The documents the reference routes present, supplied by the composition so the
7
- * route host never reads the changelog or the shell's keybindings itself.
8
- */
5
+ export type OwnedReferenceRows = NonNullable<ReferenceDocumentProvider["rows"]>;
6
+ /** Documents supplied by composition so the route host never reads shell data itself. */
9
7
  export interface OwnedReferenceProviders {
10
- /** The complete pinned changelog, or the supplied document when the route input carries one. */
11
- changelog(input?: UiRouteInput): Promise<OwnedReferenceRows>;
12
- /** The shell's keyboard shortcuts as they stand when the screen opens. */
13
- hotkeys(): Promise<OwnedReferenceRows>;
8
+ changelog(input?: UiRouteInput): Promise<ReferenceDocumentProvider>;
9
+ hotkeys(): Promise<ReferenceDocumentProvider>;
14
10
  }
15
11
  /**
16
12
  * Declares the A1-owned settings route, and the changelog and hotkeys reference
@@ -72,11 +72,11 @@ async function loadSettingsSurface(settings) {
72
72
  async function loadReferenceSurface(settings, id, route, title, document) {
73
73
  // Rationale: the app module and the document load together, so a document that fails
74
74
  // to render is reported by the placeholder rather than by a screen that never fills.
75
- const [{ ReferenceScreenApp }, rows] = await Promise.all([import("../features/owned-ui/reference-screen-app.js"), document]);
75
+ const [{ ReferenceScreenApp }, content] = await Promise.all([import("../features/owned-ui/reference-screen-app.js"), document]);
76
76
  return hostApp(id, route, () => new ReferenceScreenApp({
77
77
  id,
78
78
  title,
79
- document: { rows },
79
+ document: content,
80
80
  scrollSettings: () => ({
81
81
  scrollbarAppearance: settings.value("scrollbarAppearance"),
82
82
  scrollbarStyle: settings.value("scrollbarStyle"),
@@ -115,6 +115,10 @@ export interface OwnedUiUsageView {
115
115
  }
116
116
  export interface OwnedUiFooterView {
117
117
  readonly branch: string | null;
118
+ readonly pullRequest?: {
119
+ readonly number: number;
120
+ readonly url: string;
121
+ } | null;
118
122
  readonly sessionName: string | null;
119
123
  readonly availableProviderCount: number;
120
124
  readonly extensionStatuses: readonly (readonly [string, string])[];
@@ -396,6 +396,13 @@ export function assertOwnedUiStatusView(status) {
396
396
  }
397
397
  if (status.footer !== undefined) {
398
398
  assertOptionalText(status.footer.branch, "owned-UI footer branch", MAX_LABEL_LENGTH);
399
+ if (status.footer.pullRequest !== undefined && status.footer.pullRequest !== null) {
400
+ const pullRequest = status.footer.pullRequest;
401
+ assertIntegerInRange(pullRequest.number, 1, Number.MAX_SAFE_INTEGER, "owned-UI footer pull request number");
402
+ if (!isCanonicalGitHubPullRequestUrl(pullRequest.url, pullRequest.number)) {
403
+ throw new TypeError("owned-UI footer pull request URL is invalid");
404
+ }
405
+ }
399
406
  assertOptionalText(status.footer.sessionName, "owned-UI footer session name", MAX_LABEL_LENGTH);
400
407
  assertIntegerInRange(status.footer.availableProviderCount, 0, 1_000, "owned-UI footer provider count");
401
408
  assertCollection(status.footer.extensionStatuses, "owned-UI footer extension statuses", MAX_BADGES);
@@ -422,6 +429,24 @@ export function assertOwnedUiDiagnostics(diagnostic) {
422
429
  if (typeof diagnostic.recoverable !== "boolean")
423
430
  throw new TypeError("owned-UI diagnostic recoverability is invalid");
424
431
  }
432
+ function isCanonicalGitHubPullRequestUrl(value, number) {
433
+ if (typeof value !== "string" || value.length > MAX_LABEL_LENGTH)
434
+ return false;
435
+ try {
436
+ const url = new URL(value);
437
+ return url.protocol === "https:"
438
+ && url.hostname.toLowerCase() === "github.com"
439
+ && url.username === ""
440
+ && url.password === ""
441
+ && url.port === ""
442
+ && url.search === ""
443
+ && url.hash === ""
444
+ && new RegExp(`^/[^/]+/[^/]+/pull/${number}/?$`, "u").test(url.pathname);
445
+ }
446
+ catch {
447
+ return false;
448
+ }
449
+ }
425
450
  function assertOwnedUiModelInfo(model) {
426
451
  assertId(model.providerId, "owned-UI provider id");
427
452
  assertId(model.modelId, "owned-UI model id");
@@ -5,5 +5,5 @@ export type { ConsoleProjectTrustPromptOptions, OwnedProjectTrustPrompt, OwnedPr
5
5
  export { createConsoleSessionForkPrompt } from "./session-fork-prompt.js";
6
6
  export { SETTINGS_APP_ID, SETTINGS_ROUTE, SETTINGS_SHORTCUTS, SettingsApp } from "./settings-app.js";
7
7
  export { REFERENCE_SCREEN_SHORTCUTS, ReferenceScreenApp } from "./reference-screen-app.js";
8
- export type { ReferenceDocumentProvider, ReferenceScreenOptions } from "./reference-screen-app.js";
8
+ export type { ReferenceDocumentProvider, ReferenceDocumentSection, ReferenceScreenOptions } from "./reference-screen-app.js";
9
9
  export { CHANGELOG_APP_ID, CHANGELOG_ROUTE, CHANGELOG_TITLE, HOTKEYS_APP_ID, HOTKEYS_ROUTE, HOTKEYS_TITLE, } from "./reference-routes.js";
@@ -4,10 +4,16 @@ import { ShortcutRegistry, type PaneInputResult, type PaneMouseEvent, type PaneR
4
4
  type Action = "up" | "down" | "page-up" | "page-down" | "first" | "last" | "close";
5
5
  export declare const REFERENCE_SCREEN_SHORTCUTS: ShortcutRegistry<Action>;
6
6
  /** Rows of a document already styled and wrapped for one content width. */
7
+ export interface ReferenceDocumentSection {
8
+ readonly title: string;
9
+ readonly rows: readonly string[];
10
+ }
7
11
  export interface ReferenceDocumentProvider {
8
- /** The rows for this width, or null while the document is not available yet. */
9
- rows(width: number): readonly string[] | null;
10
- /** Settles once `rows` answers; absent when the document is available at once. */
12
+ /** Flat rows for this width, or null while the document is not available yet. */
13
+ readonly rows?: (width: number) => readonly string[] | null;
14
+ /** Structured groups opt into shared Settings-style headers and pinning. */
15
+ readonly sections?: (width: number) => readonly ReferenceDocumentSection[] | null;
16
+ /** Settles once the selected content provider answers; absent when available at once. */
11
17
  readonly ready?: Promise<void>;
12
18
  }
13
19
  export interface ReferenceScreenOptions {
@@ -1,4 +1,4 @@
1
- import { GLOBAL_SCOPE, PLAIN_THEME, RAIL_COLUMNS, ShortcutRegistry, ScrollbarRails, assertNoShortcutConflicts, displayWidth, padToWidth, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, scrollbarWheelRows, truncateToWidth, withScrollbarRail, } from "../../ui/components/index.js";
1
+ import { GLOBAL_SCOPE, PLAIN_THEME, RAIL_COLUMNS, ShortcutRegistry, ScrollbarRails, assertNoShortcutConflicts, displayWidth, layoutList, padToWidth, renderGroupHeader, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, scrollbarWheelRows, truncateToWidth, withScrollbarRail, } from "../../ui/components/index.js";
2
2
  const SCOPE = "reference-screen";
3
3
  /** Rows above the document: the top rule. */
4
4
  const HEADER_ROWS = 1;
@@ -91,16 +91,38 @@ export class ReferenceScreenApp {
91
91
  const contentWidth = reservesRail ? Math.max(0, rect.width - RAIL_COLUMNS) : rect.width;
92
92
  const bodyHeight = Math.max(0, rect.height - HEADER_ROWS - FOOTER_ROWS);
93
93
  this.#bodyHeight = bodyHeight;
94
- const rows = this.#rows(contentWidth, theme);
95
- const maxScroll = Math.max(0, rows.length - bodyHeight);
96
- this.#scroll = Math.min(Math.max(0, this.#scroll), maxScroll);
94
+ const content = this.#content(contentWidth, theme);
95
+ const body = [];
96
+ let contentLength;
97
+ let viewportLength = bodyHeight;
98
+ if (content.kind === "sections") {
99
+ const layout = layoutList(content.rows, bodyHeight, this.#scroll, { topPadding: false });
100
+ this.#scroll = layout.scroll;
101
+ contentLength = content.rows.length;
102
+ viewportLength = layout.visible;
103
+ if (layout.stickyHeader !== undefined)
104
+ body.push(renderGroupHeader(layout.stickyHeader, contentWidth, theme));
105
+ for (const index of layout.rowIndexes)
106
+ body.push(this.#renderSectionRow(content.rows[index], contentWidth, theme));
107
+ }
108
+ else {
109
+ contentLength = content.rows.length;
110
+ const maxScroll = Math.max(0, content.rows.length - bodyHeight);
111
+ this.#scroll = Math.min(Math.max(0, this.#scroll), maxScroll);
112
+ for (let index = this.#scroll; index < this.#scroll + bodyHeight; index++) {
113
+ const row = content.rows[index] ?? "";
114
+ body.push(displayWidth(row) > contentWidth ? truncateToWidth(row, contentWidth) : row);
115
+ }
116
+ }
117
+ while (body.length < bodyHeight)
118
+ body.push("");
97
119
  const now = Date.now();
98
120
  // Rationale: every way of scrolling ends in this frame, so a moved document is noticed here
99
121
  // once rather than at each wheel, drag, and key branch.
100
122
  if (this.#renderedScroll !== undefined && this.#renderedScroll !== this.#scroll)
101
123
  this.#noteScrollActivity(host, now);
102
124
  this.#renderedScroll = this.#scroll;
103
- const geometry = scrollbarGeometry({ contentLength: rows.length, viewportHeight: bodyHeight, scroll: this.#scroll, trackHeight: bodyHeight });
125
+ const geometry = scrollbarGeometry({ contentLength, viewportHeight: viewportLength, scroll: this.#scroll, trackHeight: bodyHeight });
104
126
  const presentation = scrollbarPresentation({
105
127
  geometry,
106
128
  appearance: settings.scrollbarAppearance,
@@ -111,14 +133,9 @@ export class ReferenceScreenApp {
111
133
  now,
112
134
  });
113
135
  this.#railFrame = reservesRail && geometry !== null
114
- ? { rail: { key: this.id, column: rect.width, rowStart: HEADER_ROWS, trackHeight: geometry.trackHeight }, geometry, page: bodyHeight }
136
+ ? { rail: { key: this.id, column: rect.width, rowStart: HEADER_ROWS, trackHeight: geometry.trackHeight }, geometry, page: viewportLength }
115
137
  : null;
116
- const body = [];
117
- for (let index = this.#scroll; index < this.#scroll + bodyHeight; index++) {
118
- const row = rows[index] ?? "";
119
- body.push(displayWidth(row) > contentWidth ? truncateToWidth(row, contentWidth) : row);
120
- }
121
- const withRail = withScrollbarRail(body, geometry, contentWidth, theme, { presentation });
138
+ const withRail = withScrollbarRail(body.slice(0, bodyHeight), geometry, contentWidth, theme, { presentation });
122
139
  const hint = this.#interruptArmed ? "press ctrl+c again to exit a1" : REFERENCE_SCREEN_SHORTCUTS.hint(SCOPE, HINT_SEPARATOR);
123
140
  // Compatibility: the v2 reference screen frames its document between two border-coloured rules.
124
141
  const rule = theme.fg("border", "─".repeat(rect.width));
@@ -197,19 +214,50 @@ export class ReferenceScreenApp {
197
214
  return { owned: true, changed: true };
198
215
  }
199
216
  // Invariant: the title is the first document row, as in v2, so it scrolls with the document.
200
- #rows(width, theme) {
217
+ #content(width, theme) {
201
218
  // Compatibility: one leading space, so the title lines up with the padded Markdown rows as in v2.
202
219
  const title = ` ${theme.bold(theme.fg("accent", this.#title))}`;
203
220
  if (this.#failure !== null)
204
- return [title, "", this.#failure];
221
+ return { kind: "flat", rows: [title, "", this.#failure] };
205
222
  const cached = this.#cached;
206
223
  if (cached !== null && cached.width === width)
207
- return [title, ...cached.rows];
208
- const rows = this.#document.rows(width);
209
- if (rows === null)
210
- return [title, "", LOADING_NOTICE];
211
- this.#cached = { width, rows };
212
- return [title, ...rows];
224
+ return this.#withTitle(cached.content, title);
225
+ const sections = this.#document.sections?.(width);
226
+ if (sections === null)
227
+ return { kind: "flat", rows: [title, "", LOADING_NOTICE] };
228
+ if (sections !== undefined) {
229
+ const rows = [];
230
+ sections.forEach((section, sectionIndex) => {
231
+ if (sectionIndex > 0)
232
+ rows.push({ kind: "spacer" });
233
+ const group = `section-${sectionIndex}`;
234
+ rows.push({ kind: "group", group, title: section.title });
235
+ for (const row of section.rows)
236
+ rows.push({ kind: "note", group, text: row });
237
+ });
238
+ const content = { kind: "sections", rows };
239
+ this.#cached = { width, content };
240
+ return this.#withTitle(content, title);
241
+ }
242
+ const flat = this.#document.rows?.(width);
243
+ if (flat === null)
244
+ return { kind: "flat", rows: [title, "", LOADING_NOTICE] };
245
+ const content = { kind: "flat", rows: flat ?? [] };
246
+ this.#cached = { width, content };
247
+ return this.#withTitle(content, title);
248
+ }
249
+ #withTitle(content, title) {
250
+ if (content.kind === "flat")
251
+ return { kind: "flat", rows: [title, ...content.rows] };
252
+ return { kind: "sections", rows: [{ kind: "note", group: "", text: title }, { kind: "spacer" }, ...content.rows] };
253
+ }
254
+ #renderSectionRow(row, width, theme) {
255
+ if (row === undefined || row.kind === "spacer")
256
+ return "";
257
+ if (row.kind === "group")
258
+ return renderGroupHeader(row.title, width, theme);
259
+ const text = row.kind === "note" ? row.text : row.value;
260
+ return displayWidth(text) > width ? truncateToWidth(text, width) : text;
213
261
  }
214
262
  #scrollBy(distance) {
215
263
  // Invariant: the upper clamp happens at render, where the body height and row count are known.
@@ -1,10 +1,16 @@
1
- import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, dialogValueColumn, numericValues, RAIL_COLUMNS, renderDialogPanel, renderEmptyState, renderGroupHeader, renderInputRow, renderListRow, renderNote, renderStatusLine, dialogRowAt, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, isThumbRow, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
1
+ import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, dialogValueColumn, numericValues, RAIL_COLUMNS, renderDialogPanel, renderEmptyState, renderGroupHeader, renderInputRow, renderListRow, renderNote, statusText, dialogRowAt, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, truncateToWidth, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, isThumbRow, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
2
2
  import { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
3
3
  export { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
4
4
  const SCOPE = SETTINGS_APP_ID;
5
+ const SETTINGS_TOP_RULE_ROWS = 1;
6
+ const SETTINGS_TITLE_ROWS = 1;
7
+ const SETTINGS_FOOTER_DIVIDER_ROWS = 1;
8
+ const SETTINGS_SEARCH_INPUT_ROWS = 3;
9
+ const SETTINGS_STATUS_ROWS = 1;
10
+ const SETTINGS_CONTENT_INSET = 1;
5
11
  /** The panel a setting with parts opens: its own keys, its own hint. */
6
12
  const DIALOG_SCOPE = `${SETTINGS_APP_ID}-parts`;
7
- const SCROLLBAR_TOP_INSET = 1;
13
+ const SCROLLBAR_TOP_INSET = 0;
8
14
  /** Identity of the settings list rail in the shared rail state. */
9
15
  const RAIL_KEY = "settings";
10
16
  // Compatibility: the transcript rail stays lit this long after a scroll, and repaints just after.
@@ -72,13 +78,15 @@ export class SettingsApp {
72
78
  #reveal;
73
79
  #notice = null;
74
80
  #filter = null;
81
+ #scrollBeforeFilter = null;
75
82
  #menu = null;
76
83
  #structured = null;
77
84
  #interruptArmed = false;
78
85
  // Invariant: pending values remain visible until the source reflects them.
79
86
  #pending = new Map();
80
- #footerHeight = 1;
81
87
  #dialogValueColumn = 0;
88
+ #bodyTopForFrame = SETTINGS_TOP_RULE_ROWS + SETTINGS_TITLE_ROWS;
89
+ #bodyHeightForFrame = 0;
82
90
  #panelTop = 0;
83
91
  #panelTopForFrame = 0;
84
92
  #hoverKey = null;
@@ -109,16 +117,30 @@ export class SettingsApp {
109
117
  const rows = this.#rows();
110
118
  const selected = indexOfKey(rows, this.#selectedKey);
111
119
  const footer = this.#footerLines(rect.width, theme);
112
- this.#footerHeight = footer.length;
113
- this.#panelTopForFrame = Math.max(0, rect.height - footer.length);
114
- this.#panelTop = this.#panelTopForFrame;
115
- const bodyHeight = Math.max(0, rect.height - footer.length);
116
- if (this.#selectionNeedsReveal) {
120
+ const dividerRows = this.#filter === null ? SETTINGS_FOOTER_DIVIDER_ROWS : 0;
121
+ const topRows = Math.min(SETTINGS_TOP_RULE_ROWS, rect.height);
122
+ const contentHeight = Math.max(0, rect.height - topRows - dividerRows - footer.length);
123
+ let titleRows = this.#scroll <= 0 ? Math.min(SETTINGS_TITLE_ROWS, contentHeight) : 0;
124
+ let bodyHeight = contentHeight - titleRows;
125
+ if (this.#selectionNeedsReveal && bodyHeight > 0) {
117
126
  this.#scroll = scrollForSelection(rows, bodyHeight, this.#scroll, selected, this.#reveal);
118
127
  this.#selectionNeedsReveal = false;
128
+ this.#reveal = undefined;
129
+ }
130
+ if (this.#scroll > 0 && titleRows > 0) {
131
+ titleRows = 0;
132
+ bodyHeight = contentHeight;
119
133
  }
120
- this.#reveal = undefined;
121
- const layout = layoutList(rows, bodyHeight, this.#scroll);
134
+ let layout = layoutList(rows, bodyHeight, this.#scroll);
135
+ if (layout.scroll === 0 && titleRows === 0) {
136
+ titleRows = Math.min(SETTINGS_TITLE_ROWS, contentHeight);
137
+ bodyHeight = contentHeight - titleRows;
138
+ layout = layoutList(rows, bodyHeight, 0);
139
+ }
140
+ this.#bodyTopForFrame = topRows + titleRows;
141
+ this.#bodyHeightForFrame = bodyHeight;
142
+ this.#panelTopForFrame = topRows + contentHeight + dividerRows;
143
+ this.#panelTop = this.#panelTopForFrame;
122
144
  this.#scroll = layout.scroll;
123
145
  const now = Date.now();
124
146
  // Rationale: every way of scrolling ends in this frame, so a moved list is noticed here
@@ -136,7 +158,7 @@ export class SettingsApp {
136
158
  contentLength: rows.length,
137
159
  viewportHeight: layout.visible,
138
160
  scroll: layout.scroll,
139
- trackHeight: Math.max(0, bodyHeight - SCROLLBAR_TOP_INSET),
161
+ trackHeight: Math.max(0, titleRows + bodyHeight - SCROLLBAR_TOP_INSET),
140
162
  });
141
163
  const presentation = scrollbarPresentation({
142
164
  geometry,
@@ -149,7 +171,7 @@ export class SettingsApp {
149
171
  });
150
172
  this.#railFrame = reservesRail && geometry !== null
151
173
  ? {
152
- rail: { key: RAIL_KEY, column: rect.width, rowStart: SCROLLBAR_TOP_INSET, trackHeight: geometry.trackHeight },
174
+ rail: { key: RAIL_KEY, column: rect.width, rowStart: topRows + SCROLLBAR_TOP_INSET, trackHeight: geometry.trackHeight },
153
175
  geometry,
154
176
  page: layout.visible,
155
177
  }
@@ -170,8 +192,8 @@ export class SettingsApp {
170
192
  const view = this.#viewRow(row.value);
171
193
  this.#frameRows.push({
172
194
  key: view.key,
173
- screenRow: body.length,
174
- valueColumn,
195
+ screenRow: this.#bodyTopForFrame + body.length,
196
+ valueColumn: valueColumn + Math.min(SETTINGS_CONTENT_INSET, contentWidth),
175
197
  valueWidth: displayWidth(view.value),
176
198
  stepper: view.stepper !== undefined,
177
199
  });
@@ -181,11 +203,15 @@ export class SettingsApp {
181
203
  while (body.length < bodyHeight)
182
204
  body.push("");
183
205
  }
184
- const withRail = withScrollbarRail(body.slice(0, bodyHeight), geometry, contentWidth, theme, {
206
+ const title = truncateToWidth(` ${theme.bold(theme.fg("accent", "Settings"))}`, contentWidth);
207
+ const scrollingFrame = [...(titleRows === 0 ? [] : [title]), ...body.slice(0, bodyHeight)];
208
+ const withRail = withScrollbarRail(scrollingFrame, geometry, contentWidth, theme, {
185
209
  topInset: SCROLLBAR_TOP_INSET,
186
210
  presentation,
187
211
  });
188
- return this.#withMenu([...withRail, ...footer], selected, layout, valueColumn, theme, rect, reservesRail ? RAIL_COLUMNS : 0);
212
+ const rule = theme.fg("border", "─".repeat(Math.max(0, rect.width)));
213
+ const frame = this.#withMenu([rule, ...withRail, ...(dividerRows === 0 ? [] : [rule]), ...footer], selected, layout, valueColumn, theme, rect, reservesRail ? RAIL_COLUMNS : 0);
214
+ return frame.slice(0, rect.height).concat(Array(Math.max(0, rect.height - frame.length)).fill(""));
189
215
  }
190
216
  onInput(data, host) {
191
217
  if (this.#structured !== null)
@@ -205,6 +231,7 @@ export class SettingsApp {
205
231
  host.close();
206
232
  return { consumed: true };
207
233
  case "open-filter":
234
+ this.#scrollBeforeFilter = this.#scroll;
208
235
  this.#filter = new LineInput("");
209
236
  this.#notice = null;
210
237
  return { consumed: true };
@@ -316,9 +343,15 @@ export class SettingsApp {
316
343
  if (event.kind === "wheel-up" || event.kind === "wheel-down") {
317
344
  // Invariant: the whole list pane owns wheel scrolling, including blank space beside
318
345
  // short labels. It must not depend on finding an item under the pointer.
319
- if (event.row < 1 || event.row > this.#panelTopForFrame)
346
+ const screenRow = event.row - 1;
347
+ const wheelBottom = this.#filter === null
348
+ ? this.#bodyTopForFrame + this.#bodyHeightForFrame
349
+ : this.#panelTopForFrame + SETTINGS_SEARCH_INPUT_ROWS + SETTINGS_STATUS_ROWS;
350
+ if (screenRow < 0 || screenRow >= wheelBottom)
320
351
  return { consumed: false };
321
352
  const distance = scrollbarWheelRows(this.#scrollbarSpeed());
353
+ if (this.#filter !== null)
354
+ this.#scrollBeforeFilter = null;
322
355
  this.#scroll = Math.max(0, this.#scroll + (event.kind === "wheel-down" ? distance : -distance));
323
356
  return { consumed: true };
324
357
  }
@@ -520,6 +553,7 @@ export class SettingsApp {
520
553
  // Rationale: the boundary chords jump through the results; plain Home and End stay with
521
554
  // the search cursor, which the shared line input moves below.
522
555
  if (key === "ctrl+home" || key === "ctrl+end") {
556
+ this.#scrollBeforeFilter = null;
523
557
  const rows = this.#rows();
524
558
  const selectable = selectableIndexes(rows);
525
559
  const target = key === "ctrl+end" ? selectable.at(-1) : selectable[0];
@@ -531,6 +565,7 @@ export class SettingsApp {
531
565
  return { consumed: true };
532
566
  }
533
567
  if (key === "up" || key === "down" || key === "shift+up" || key === "shift+down") {
568
+ this.#scrollBeforeFilter = null;
534
569
  const rows = this.#rows();
535
570
  // Rationale: nothing found means nothing to move through; the key is still swallowed
536
571
  // rather than typed into the search.
@@ -548,9 +583,16 @@ export class SettingsApp {
548
583
  return { consumed: true };
549
584
  }
550
585
  const outcome = handleLineInputKey(input, data);
551
- if (outcome.kind === "cancelled")
586
+ if (outcome.kind === "cancelled") {
552
587
  this.#filter = null;
553
- this.#scroll = 0;
588
+ if (this.#scrollBeforeFilter !== null)
589
+ this.#scroll = this.#scrollBeforeFilter;
590
+ this.#scrollBeforeFilter = null;
591
+ }
592
+ else {
593
+ this.#scrollBeforeFilter = null;
594
+ this.#scroll = 0;
595
+ }
554
596
  return { consumed: true };
555
597
  }
556
598
  #cycle(rows, selected, delta) {
@@ -684,16 +726,17 @@ export class SettingsApp {
684
726
  return renderGroupHeader(humanizeTitle(title), width, theme);
685
727
  }
686
728
  #renderRow(row, selected, width, valueColumn, theme) {
687
- if (row === undefined || row.kind === "spacer")
729
+ if (row === undefined || row.kind === "spacer" || width < SETTINGS_CONTENT_INSET)
688
730
  return "";
689
731
  if (row.kind === "group")
690
732
  return this.#header(row.title, theme, width);
733
+ const innerWidth = width - SETTINGS_CONTENT_INSET;
691
734
  if (row.kind === "note")
692
- return renderNote(row.text, width, theme);
735
+ return ` ${renderNote(row.text, innerWidth, theme)}`;
693
736
  const entry = row.value;
694
737
  const key = `${entry.backend}:${entry.id}`;
695
738
  const hovered = this.#hoverKey === key;
696
- return renderListRow(this.#viewRow(entry), { selected, hovered, region: this.#hoverRegion }, valueColumn, width, theme);
739
+ return ` ${renderListRow(this.#viewRow(entry), { selected, hovered, region: this.#hoverRegion }, valueColumn, innerWidth, theme)}`;
697
740
  }
698
741
  #viewRow(entry) {
699
742
  const shown = this.#shownValue(entry);
@@ -723,7 +766,8 @@ export class SettingsApp {
723
766
  index: menu.index,
724
767
  };
725
768
  const frame = valueMenuFrame(state, { screenRow: anchor.screenRow, valueColumn }, {
726
- bodyHeight: lines.length - this.#footerHeight,
769
+ bodyTop: this.#bodyTopForFrame,
770
+ bodyHeight: this.#bodyHeightForFrame,
727
771
  surfaceWidth: rect.width,
728
772
  reservedRight,
729
773
  });
@@ -749,7 +793,8 @@ export class SettingsApp {
749
793
  if (open !== null)
750
794
  return this.#dialogLines(open, width, theme);
751
795
  const hint = this.#interruptArmed ? "press ctrl+c again to exit a1" : SETTINGS_SHORTCUTS.hint(SCOPE);
752
- const status = renderStatusLine({ hint, report: this.#notice }, width, theme);
796
+ const report = statusText({ hint, report: this.#notice });
797
+ const status = truncateToWidth(`${width > 0 ? " " : ""}${theme.fg("dim", report)}`, width);
753
798
  const input = this.#filter;
754
799
  if (input === null)
755
800
  return [status];