@timurproko/a1 0.1.8-dev.332 → 0.1.8-dev.368

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 (110) hide show
  1. package/README.md +40 -11
  2. package/dist/composition/owned-ui.d.ts +2 -0
  3. package/dist/composition/owned-ui.js +20 -3
  4. package/dist/contracts/owned-ui/index.d.ts +2 -0
  5. package/dist/contracts/owned-ui/index.js +2 -0
  6. package/dist/contracts/owned-ui/model.d.ts +38 -3
  7. package/dist/contracts/owned-ui/prompt-history.d.ts +18 -0
  8. package/dist/contracts/owned-ui/prompt-suggestions.d.ts +1 -1
  9. package/dist/contracts/owned-ui/prompt-suggestions.js +4 -1
  10. package/dist/contracts/owned-ui/suggestion-diagnostics.d.ts +22 -0
  11. package/dist/contracts/owned-ui/suggestion-diagnostics.js +9 -0
  12. package/dist/contracts/owned-ui/transcript-lifecycle.d.ts +5 -0
  13. package/dist/contracts/owned-ui/transcript-lifecycle.js +33 -0
  14. package/dist/contracts/owned-ui/validation.js +45 -2
  15. package/dist/contracts/presentation/index.d.ts +8 -0
  16. package/dist/features/owned-ui/settings-app.js +1 -1
  17. package/dist/features/prompt-history/image-sidecar.d.ts +32 -0
  18. package/dist/features/prompt-history/image-sidecar.js +130 -0
  19. package/dist/features/prompt-history/index.d.ts +2 -0
  20. package/dist/features/prompt-history/index.js +1 -0
  21. package/dist/features/prompt-history/paths.d.ts +1 -0
  22. package/dist/features/prompt-history/paths.js +2 -1
  23. package/dist/features/prompt-history/store.d.ts +1 -1
  24. package/dist/features/prompt-history/store.js +46 -2
  25. package/dist/features/prompt-history/worker.js +1 -1
  26. package/dist/features/prompt-suggestions/diagnostics.d.ts +18 -0
  27. package/dist/features/prompt-suggestions/diagnostics.js +82 -0
  28. package/dist/features/prompt-suggestions/index.d.ts +1 -0
  29. package/dist/features/prompt-suggestions/index.js +1 -0
  30. package/dist/foundation/release/bootstrap.d.ts +2 -0
  31. package/dist/foundation/release/bootstrap.js +54 -18
  32. package/dist/foundation/release/index.d.ts +1 -0
  33. package/dist/foundation/release/index.js +1 -0
  34. package/dist/foundation/release/update-launch.d.ts +6 -0
  35. package/dist/foundation/release/update-launch.js +17 -0
  36. package/dist/foundation/release/update.d.ts +2 -0
  37. package/dist/foundation/release/update.js +20 -21
  38. package/dist/foundation/supervision/main.js +5 -2
  39. package/dist/foundation/supervision/server.js +40 -13
  40. package/dist/integrations/pi/components/owned-editor-ux.d.ts +9 -1
  41. package/dist/integrations/pi/components/owned-editor-ux.js +11 -8
  42. package/dist/integrations/pi/components/prompt-input-port.d.ts +18 -0
  43. package/dist/integrations/pi/components/prompt-input-port.js +1 -0
  44. package/dist/integrations/pi/components/shell-editor-autocomplete.js +66 -11
  45. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  46. package/dist/integrations/pi/components/shell-footer-status.js +9 -5
  47. package/dist/integrations/pi/components/shell-presenters-info.js +7 -2
  48. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +6 -2
  49. package/dist/integrations/pi/components/shell-presenters-transcript.js +81 -49
  50. package/dist/integrations/pi/components/shell-shared-facade.d.ts +17 -4
  51. package/dist/integrations/pi/components/shell-shared-facade.js +2 -1
  52. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +3 -1
  53. package/dist/integrations/pi/components/submitted-prompt-adapter.js +20 -4
  54. package/dist/integrations/pi/components/tool-result-adapter.d.ts +6 -0
  55. package/dist/integrations/pi/components/tool-result-adapter.js +27 -0
  56. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -0
  57. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +5 -0
  58. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -2
  59. package/dist/integrations/pi/components/upstream/components/owned-editor.js +41 -33
  60. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  61. package/dist/integrations/pi/components/upstream/components/session-footer.js +12 -7
  62. package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
  63. package/dist/integrations/pi/engine/adapter.d.ts +2 -1
  64. package/dist/integrations/pi/engine/adapter.js +181 -84
  65. package/dist/integrations/pi/engine/tool-rendering.d.ts +13 -0
  66. package/dist/integrations/pi/engine/tool-rendering.js +122 -0
  67. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +30 -0
  68. package/dist/integrations/pi/session-ui/prompt-chips.js +128 -1
  69. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +8 -0
  70. package/dist/integrations/pi/session-ui/prompt-history-controller.js +30 -2
  71. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +11 -3
  72. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +126 -51
  73. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +24 -2
  74. package/dist/integrations/pi/session-ui/session-shell-root.js +108 -38
  75. package/dist/integrations/pi/session-ui/session-shell.js +51 -31
  76. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -1
  77. package/dist/integrations/pi/session-ui/session-viewport-controller.js +106 -16
  78. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  79. package/dist/integrations/pi/tui-runtime/adapter.js +35 -11
  80. package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -1
  81. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -0
  82. package/dist/integrations/pi/tui-runtime/index.js +1 -0
  83. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +13 -0
  84. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +59 -0
  85. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +32 -0
  86. package/dist/integrations/pi/tui-runtime/overlay-geometry.js +159 -0
  87. package/dist/native/darwin-arm64/manifest.json +1 -1
  88. package/dist/native/linux-x64/manifest.json +1 -1
  89. package/dist/native/win32-x64/manifest.json +2 -2
  90. package/dist/native/win32-x64/process-guardian.exe +0 -0
  91. package/dist/product-identity.d.ts +1 -1
  92. package/dist/product-identity.js +1 -1
  93. package/dist/product-identity.json +1 -0
  94. package/dist/ui/components/index.d.ts +1 -0
  95. package/dist/ui/components/index.js +1 -0
  96. package/dist/ui/components/line-input.d.ts +2 -3
  97. package/dist/ui/components/line-input.js +4 -20
  98. package/dist/ui/components/mouse.d.ts +1 -1
  99. package/dist/ui/components/mouse.js +4 -3
  100. package/dist/ui/components/prompt-input.d.ts +32 -0
  101. package/dist/ui/components/prompt-input.js +54 -0
  102. package/docs/architecture/boundaries.md +1 -0
  103. package/docs/architecture/history-editor-provenance.md +5 -1
  104. package/docs/architecture/project-structure.md +1 -0
  105. package/docs/architecture/prompt-suggestions.md +68 -0
  106. package/docs/architecture/toolchain.md +1 -1
  107. package/docs/architecture/ui-reference-provenance.md +23 -0
  108. package/docs/ci-release-runbook.md +45 -7
  109. package/docs/features/modal-content-interaction.md +37 -0
  110. package/package.json +1 -1
package/README.md CHANGED
@@ -45,6 +45,18 @@ a1 pi # vanilla Pi oracle: ~/.pi/agent
45
45
  Unsupported commands exit quietly without launching anything. The removed
46
46
  `update:<preview>` forms are not aliases; use `update --develop`.
47
47
 
48
+ ## Agent input
49
+
50
+ The agent prompt and Settings search share one input component: neutral white
51
+ bars and an undimmed arrow matching submitted prompts. Thinking-level color is
52
+ shown on the level name in the status bar, not on the input bars.
53
+
54
+ In bare A1, **Ctrl+L cycles the thinking level**. **Shift+Tab is unassigned** and
55
+ performs no agent-input action, including while a suggestion is visible. Model
56
+ selection has **no default shortcut**; use `/model`. Explicit keybinding overrides
57
+ remain configurable, and `/hotkeys` shows the resolved bindings. The `a1 pi`
58
+ comparison profile keeps its original presentation and shortcuts.
59
+
48
60
  ## Prompt suggestions
49
61
 
50
62
  Bare A1's `/settings` → Agent → Prompt suggestions controls faint next-prompt
@@ -53,6 +65,12 @@ with the selected model for eligible turns. Disabling it takes effect immediatel
53
65
  Tab accepts a suggestion as editable text; Enter submits it afterward. The setting
54
66
  remains stored in A1's profile, and `a1 pi` is unchanged.
55
67
 
68
+ Suggestions use the selected model's lowest supported reasoning effort without
69
+ changing your main thinking level. Clear next actions such as `archive it` are
70
+ preferred when consistent with your intent; genuinely unclear cases can remain
71
+ empty. [Private diagnostic capture](docs/architecture/prompt-suggestions.md)
72
+ explains how to distinguish skipped, empty, rejected, and timed-out suggestions.
73
+
56
74
  ## Prompt history
57
75
 
58
76
  Bare A1 recalls recent unique prompts across sessions with Up/Down. History is
@@ -160,18 +178,29 @@ a1 update --develop 0.1.8-dev.107 # install that exact full preview versio
160
178
  ### Stable
161
179
 
162
180
  ```sh
163
- npm run release -- patch # 0.1.1 -> 0.1.2
164
- npm run release -- minor # 0.1.1 -> 0.2.0
165
- npm run release -- major # 0.1.1 -> 1.0.0
166
- npm run release -- 0.4.0 # an exact version
181
+ npm run release -- patch # 0.1.8-dev -> 0.1.8; already-stable 0.1.8 -> 0.1.9
182
+ npm run release -- minor # 0.1.8-dev -> 0.2.0
183
+ npm run release -- major # 0.1.8-dev -> 1.0.0
184
+ npm run release -- 0.4.0 # an exact stable version
167
185
  ```
168
186
 
169
- The command lands the version on `develop` through a self-merging pull request,
170
- dispatches publication for that exact commit, and waits for success. CI validates
171
- the packed release on Windows, Linux, and macOS, publishes to npm `latest` with
172
- provenance, then writes the `v<version>` tag and the GitHub Release. `master`
173
- fast-forwards to the released commit, so it always points at what npm `latest`
174
- serves. A failed release leaves nothing behind: no tag, no GitHub Release, no
175
- moved branch.
187
+ Run from the repository root on a clean `develop` matching `origin/develop`.
188
+ A target is required; bare `npm run release` displays usage and releases nothing.
189
+ `patch` promotes the current prerelease rather than skipping its stable version.
190
+
191
+ The command prepares a version-only PR in an isolated detached worktree, prints
192
+ its URL, and waits for you to validate and **merge it manually**. Neither this PR
193
+ nor the next-development PR is auto-merged, and green CI alone does not advance
194
+ the release. After the stable PR merges, the command dispatches publication for
195
+ that exact authoritative commit and waits for success. CI validates the packed
196
+ release on Windows, Linux, and macOS, publishes to npm `latest` with provenance,
197
+ then writes the `v<version>` tag and GitHub Release and fast-forwards `master`.
198
+
199
+ Only after confirmed publication of `0.1.8` does the command prepare the separate
200
+ `0.1.9-dev` PR. It reports development reopened only after you manually merge that
201
+ PR too. Work added to your checkout during either wait is preserved, not reset.
202
+ If publication fails or is uncertain, no reopening PR is prepared. If publication
203
+ succeeded but reopening failed, inspect the reported phase and PR; do not republish
204
+ the immutable stable version.
176
205
 
177
206
  `docs/ci-release-runbook.md` has the full picture.
@@ -21,6 +21,8 @@ export interface OwnedUiCompositionOptions {
21
21
  */
22
22
  readonly ownedSurfaces?: "on" | "off";
23
23
  readonly projectTrustPrompt?: PiProjectTrustPreflightPrompt;
24
+ /** Explicit local diagnostic destination; ignored by comparison/settings-free compositions. */
25
+ readonly suggestionDiagnosticsPath?: string;
24
26
  }
25
27
  export interface OwnedUiComposition {
26
28
  readonly application: OwnedUiApplicationPort;
@@ -1,4 +1,6 @@
1
- import { PromptHistoryService } from "../features/prompt-history/index.js";
1
+ import { SuggestionDiagnosticCapture } from "../features/prompt-suggestions/index.js";
2
+ import { PRODUCT_IDENTITY } from "../product-identity.js";
3
+ import { PromptHistoryService, PromptImageSidecar, resolvePromptHistoryPath } from "../features/prompt-history/index.js";
2
4
  import { resolvePromptHistoryDataDir } from "../features/launch/index.js";
3
5
  import { resolveProductPaths } from "../foundation/lifecycle/index.js";
4
6
  import { applyConfiguredPiTheme, getAvailablePiThemes, loadHistoryEditor } from "../integrations/pi/components/index.js";
@@ -39,19 +41,27 @@ export async function composeOwnedUi(options = {}) {
39
41
  snapshot: () => viewportSettingsSnapshot(settings),
40
42
  onChange: listener => settings.onChange(() => listener(viewportSettingsSnapshot(settings))),
41
43
  };
44
+ const diagnosticDestination = options.suggestionDiagnosticsPath ?? process.env[PRODUCT_IDENTITY.environment.suggestionDiagnostics];
45
+ const suggestionDiagnostics = settings !== null && ownedSurfaces && diagnosticDestination?.trim()
46
+ ? new SuggestionDiagnosticCapture({ enabled: true, destination: diagnosticDestination }) : null;
42
47
  const promptSuggestions = settings === null || !ownedSurfaces ? null : {
48
+ ...(suggestionDiagnostics === null ? {} : { diagnostics: suggestionDiagnostics }),
43
49
  generator: adapter,
44
50
  enabled: () => settings.value("promptSuggestions") !== false,
45
51
  onChange: (listener) => settings.onChange(() => listener(settings.value("promptSuggestions") !== false)),
46
52
  };
47
53
  const historyLimit = settings?.value("promptHistoryMaxItems");
48
- const promptHistory = settings === null || !ownedSurfaces || settings.value("promptHistoryEnabled") === false ? null : {
54
+ const historyProfileLocation = settings === null || !ownedSurfaces || settings.value("promptHistoryEnabled") === false
55
+ ? null
56
+ : resolvePromptHistoryPath(resolvePromptHistoryDataDir(), adapter.agentDir);
57
+ const promptHistory = historyProfileLocation === null ? null : {
49
58
  limit: typeof historyLimit === "number" ? historyLimit : 100,
50
59
  store: new PromptHistoryService({
51
60
  dataDir: resolvePromptHistoryDataDir(),
52
61
  profileRoot: adapter.agentDir,
53
62
  limit: typeof historyLimit === "number" ? historyLimit : 100,
54
63
  }),
64
+ imageSidecar: new PromptImageSidecar(historyProfileLocation.imagesDir),
55
65
  };
56
66
  const shell = new OwnedUiSessionShell({
57
67
  backend: adapter,
@@ -68,7 +78,14 @@ export async function composeOwnedUi(options = {}) {
68
78
  start: () => shell.start(),
69
79
  flush: () => adapter.flushEvents(),
70
80
  waitUntilStopped: () => shell.waitUntilStopped(),
71
- dispose: () => shell.dispose(),
81
+ dispose: async () => {
82
+ try {
83
+ await shell.dispose();
84
+ }
85
+ finally {
86
+ suggestionDiagnostics?.dispose();
87
+ }
88
+ },
72
89
  };
73
90
  return { application, settings };
74
91
  }
@@ -1,6 +1,8 @@
1
1
  export * from "./extension-ui.js";
2
2
  export * from "./model.js";
3
+ export * from "./transcript-lifecycle.js";
3
4
  export * from "./image-attachments.js";
4
5
  export * from "./prompt-suggestions.js";
6
+ export * from "./suggestion-diagnostics.js";
5
7
  export * from "./prompt-history.js";
6
8
  export * from "./validation.js";
@@ -1,6 +1,8 @@
1
1
  export * from "./extension-ui.js";
2
2
  export * from "./model.js";
3
+ export * from "./transcript-lifecycle.js";
3
4
  export * from "./image-attachments.js";
4
5
  export * from "./prompt-suggestions.js";
6
+ export * from "./suggestion-diagnostics.js";
5
7
  export * from "./prompt-history.js";
6
8
  export * from "./validation.js";
@@ -22,10 +22,16 @@ export interface OwnedUiPromptSuggestionRequest {
22
22
  readonly identity: OwnedUiPromptSuggestionIdentity;
23
23
  readonly signal: AbortSignal;
24
24
  }
25
- export interface OwnedUiPromptSuggestionResult {
25
+ export type OwnedUiPromptSuggestionReasoning = "ordinary" | "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "unavailable";
26
+ export type OwnedUiPromptSuggestionResult = {
26
27
  readonly identity: OwnedUiPromptSuggestionIdentity;
27
- readonly text: string | null;
28
- }
28
+ } & ({
29
+ readonly outcome: "candidate";
30
+ readonly text: string;
31
+ } | {
32
+ readonly outcome: "empty" | "rejected" | "provider-failure" | "unavailable" | "cancelled";
33
+ readonly text: null;
34
+ });
29
35
  export type OwnedUiPromptSuggestionState = {
30
36
  readonly status: "idle";
31
37
  } | {
@@ -42,6 +48,8 @@ export type OwnedUiPromptSuggestionState = {
42
48
  readonly text: string;
43
49
  };
44
50
  export interface OwnedUiPromptSuggestionGeneratorPort {
51
+ /** Request-local policy; never changes the primary session's thinking setting. */
52
+ suggestionReasoningPolicy?(): OwnedUiPromptSuggestionReasoning;
45
53
  generate(request: OwnedUiPromptSuggestionRequest): Promise<OwnedUiPromptSuggestionResult>;
46
54
  }
47
55
  export type OwnedUiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
@@ -130,6 +138,29 @@ export interface OwnedUiTranscriptImageReference {
130
138
  readonly byteLength: number;
131
139
  readonly source: "user" | "tool-result";
132
140
  }
141
+ /** Argument generation and execution have distinct completion boundaries on one invocation. */
142
+ export interface OwnedUiToolState {
143
+ readonly argsComplete: boolean;
144
+ readonly execution: "pending" | "running" | "succeeded" | "failed" | "aborted";
145
+ }
146
+ /** Vendor-neutral rendering metadata; text and image bytes are not duplicated here. */
147
+ export type OwnedUiToolResultPart = {
148
+ readonly type: "text";
149
+ readonly start: number;
150
+ readonly end: number;
151
+ } | {
152
+ readonly type: "image";
153
+ readonly imageIndex: number;
154
+ };
155
+ export interface OwnedUiToolRendering {
156
+ readonly arguments: unknown;
157
+ readonly result?: {
158
+ readonly content: readonly OwnedUiToolResultPart[];
159
+ readonly details?: unknown;
160
+ };
161
+ /** Visible bounded fallback, never a substitute operation outcome. */
162
+ readonly unavailable?: string;
163
+ }
133
164
  export interface OwnedUiTranscriptBlock {
134
165
  readonly id: OwnedUiEntityId;
135
166
  readonly kind: OwnedUiTranscriptBlockKind;
@@ -138,6 +169,10 @@ export interface OwnedUiTranscriptBlock {
138
169
  readonly title: string | null;
139
170
  readonly text: string;
140
171
  readonly payload: unknown;
172
+ /** Tool-call message completion alone must not settle execution. */
173
+ readonly toolState?: OwnedUiToolState;
174
+ /** Authoritative renderer inputs, separate from diagnostic payloads. */
175
+ readonly toolRendering?: OwnedUiToolRendering;
141
176
  /** Bounded opaque references; image bytes remain in the session-scoped engine asset store. */
142
177
  readonly imageReferences?: readonly OwnedUiTranscriptImageReference[];
143
178
  }
@@ -31,6 +31,24 @@ export interface PromptHistoryPort {
31
31
  onFailure(listener: (code: PromptHistoryFailure) => void): () => void;
32
32
  close(): Promise<void>;
33
33
  }
34
+ export interface PromptHistoryImageSidecarAttachment {
35
+ readonly type: "image";
36
+ readonly data: string;
37
+ readonly mimeType: string;
38
+ }
39
+ export interface PromptHistoryImageSidecarRecord {
40
+ readonly tag: string;
41
+ readonly data: string;
42
+ readonly mimeType: string;
43
+ readonly savedAt: string;
44
+ }
45
+ /** Per-profile durable payload store for image chip attachments referenced by history text.
46
+ * Writes MUST be idempotent and MUST NOT block a history row commit on failure; reads MUST
47
+ * treat every failure as "attachment unavailable" so rehydration silently strips the chip. */
48
+ export interface PromptHistoryImageSidecarPort {
49
+ write(id: string, record: PromptHistoryImageSidecarRecord): void;
50
+ readAttachment(id: string): PromptHistoryImageSidecarAttachment | null;
51
+ }
34
52
  export declare const PROMPT_HISTORY_MAX_ENTRY_BYTES: number;
35
53
  export declare const PROMPT_HISTORY_MAX_TEXT_BYTES: number;
36
54
  export declare const PROMPT_HISTORY_MAX_PENDING = 32;
@@ -1,3 +1,3 @@
1
- export declare const CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION = "[NEXT USER INPUT]\nPredict the one short response the user is most likely to type next.\nUse the user's recent intent and writing style. Prefer a concrete continuation such as approving an offered action, choosing an offered option, running a requested check, committing, or pushing.\nReturn nothing when the next input is unclear, the previous response failed, or the user should assess or correct the result.\nDo not answer as the assistant. Do not add a label, explanation, quotation marks, Markdown, or multiple sentences.\nReturn only 2-12 words, except a natural one-word command or answer is allowed.";
1
+ export declare const CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION = "[NEXT USER INPUT]\nPredict the one short response the user is most likely to type next.\nUse the user's recent intent and writing style. Prefer a concrete continuation such as approving an offered action, choosing an offered option, running a requested check, committing, or pushing.\nPrefer a clearly offered next action consistent with the user's recent intent. An optional alternative alone does not make that continuation unclear.\nFor example, after the user accepted merged work, \"Say archive it\" with an optional \"let me test\" alternative favors: archive it.\nDo not favor archival if the user required testing first and that testing remains outstanding. Do not invent approval or select the first of equally unresolved choices.\nReturn nothing when the next input is genuinely unclear, the previous response failed, or required assessment or correction remains outstanding.\nDo not answer as the assistant. Do not add a label, explanation, quotation marks, Markdown, or multiple sentences.\nReturn only 2-12 words, except a natural one-word command or answer is allowed.";
2
2
  /** Converts untrusted model output into one inert, bounded user-voice candidate. */
3
3
  export declare function normalizePromptSuggestionCandidate(candidate: string | null | undefined): string | null;
@@ -1,7 +1,10 @@
1
1
  export const CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION = `[NEXT USER INPUT]
2
2
  Predict the one short response the user is most likely to type next.
3
3
  Use the user's recent intent and writing style. Prefer a concrete continuation such as approving an offered action, choosing an offered option, running a requested check, committing, or pushing.
4
- Return nothing when the next input is unclear, the previous response failed, or the user should assess or correct the result.
4
+ Prefer a clearly offered next action consistent with the user's recent intent. An optional alternative alone does not make that continuation unclear.
5
+ For example, after the user accepted merged work, "Say archive it" with an optional "let me test" alternative favors: archive it.
6
+ Do not favor archival if the user required testing first and that testing remains outstanding. Do not invent approval or select the first of equally unresolved choices.
7
+ Return nothing when the next input is genuinely unclear, the previous response failed, or required assessment or correction remains outstanding.
5
8
  Do not answer as the assistant. Do not add a label, explanation, quotation marks, Markdown, or multiple sentences.
6
9
  Return only 2-12 words, except a natural one-word command or answer is allowed.`;
7
10
  const ALLOWED_SINGLE_WORDS = new Set([
@@ -0,0 +1,22 @@
1
+ import type { OwnedUiPromptSuggestionReasoning } from "./model.js";
2
+ export declare const SUGGESTION_DECISION_REASONS: readonly ["disabled", "disposed", "early-conversation", "no-model", "failed-response", "incomplete-response", "tool-continuation", "replacement-input", "modal", "draft", "not-ready", "not-focused", "autocomplete", "prompt-mode", "presentation-unavailable", "ineligible", "stale-identity"];
3
+ export type SuggestionDecisionReason = typeof SUGGESTION_DECISION_REASONS[number];
4
+ export type SuggestionDecision = SuggestionDecisionReason | null;
5
+ export declare const SUGGESTION_DIAGNOSTIC_EVENTS: readonly ["skipped", "started", "displayed", "empty", "rejected", "provider-failure", "unavailable", "timeout", "cancelled", "stale-result", "presentation-blocked", "late-result-discarded"];
6
+ export type SuggestionDiagnosticEvent = typeof SUGGESTION_DIAGNOSTIC_EVENTS[number];
7
+ /** Metadata only: never carry a session path, prompt, candidate, or raw error. */
8
+ export interface SuggestionDiagnosticRecord {
9
+ readonly event: SuggestionDiagnosticEvent;
10
+ readonly reason?: SuggestionDecisionReason;
11
+ readonly session: number;
12
+ readonly run: number;
13
+ readonly response: number;
14
+ readonly request: number;
15
+ readonly provider: string;
16
+ readonly model: string;
17
+ readonly reasoning: OwnedUiPromptSuggestionReasoning;
18
+ readonly elapsedMs: number;
19
+ }
20
+ export interface SuggestionDiagnosticObserver {
21
+ record(record: SuggestionDiagnosticRecord): void;
22
+ }
@@ -0,0 +1,9 @@
1
+ export const SUGGESTION_DECISION_REASONS = [
2
+ "disabled", "disposed", "early-conversation", "no-model", "failed-response", "incomplete-response",
3
+ "tool-continuation", "replacement-input", "modal", "draft", "not-ready", "not-focused",
4
+ "autocomplete", "prompt-mode", "presentation-unavailable", "ineligible", "stale-identity",
5
+ ];
6
+ export const SUGGESTION_DIAGNOSTIC_EVENTS = [
7
+ "skipped", "started", "displayed", "empty", "rejected", "provider-failure", "unavailable",
8
+ "timeout", "cancelled", "stale-result", "presentation-blocked", "late-result-discarded",
9
+ ];
@@ -0,0 +1,5 @@
1
+ import type { OwnedUiToolState, OwnedUiTranscriptBlock } from "./model.js";
2
+ /** Compatibility for existing owned components/fixtures without explicit execution state. */
3
+ export declare function transcriptToolState(block: OwnedUiTranscriptBlock): OwnedUiToolState | undefined;
4
+ /** Within a session generation, reject obsolete phases as well as obsolete revisions. */
5
+ export declare function acceptsTranscriptUpdate(current: OwnedUiTranscriptBlock, next: OwnedUiTranscriptBlock): boolean;
@@ -0,0 +1,33 @@
1
+ /** Compatibility for existing owned components/fixtures without explicit execution state. */
2
+ export function transcriptToolState(block) {
3
+ if (block.kind !== "tool-call" && block.kind !== "tool-result")
4
+ return undefined;
5
+ if (block.toolState !== undefined)
6
+ return block.toolState;
7
+ const payload = typeof block.payload === "object" && block.payload !== null ? block.payload : {};
8
+ const isError = "isError" in payload && payload.isError === true;
9
+ const partial = "partialResult" in payload && payload.partialResult === true;
10
+ return {
11
+ argsComplete: block.status === "finalized" || "argsComplete" in payload && payload.argsComplete === true,
12
+ execution: block.kind === "tool-result"
13
+ ? block.status === "live" ? "running" : isError ? "failed" : "succeeded"
14
+ : partial ? "running" : "pending",
15
+ };
16
+ }
17
+ /** Within a session generation, reject obsolete phases as well as obsolete revisions. */
18
+ export function acceptsTranscriptUpdate(current, next) {
19
+ if (current.id !== next.id || current.revision > next.revision)
20
+ return false;
21
+ const before = transcriptToolState(current);
22
+ const after = transcriptToolState(next);
23
+ if (before !== undefined && after !== undefined) {
24
+ const rank = (state) => state.execution === "pending" ? 0 : state.execution === "running" ? 1 : 2;
25
+ if (rank(after) < rank(before))
26
+ return false;
27
+ if (before.argsComplete && !after.argsComplete && rank(after) < 2)
28
+ return false;
29
+ // Compatibility: a legacy finalized call declaration only closed arguments. Actual result finality is a barrier.
30
+ return !(current.status === "finalized" && next.status === "live" && before.execution !== "pending");
31
+ }
32
+ return !(current.status === "finalized" && next.status === "live");
33
+ }
@@ -14,6 +14,7 @@ const MAX_BADGES = 32;
14
14
  const MAX_STATUS_DIAGNOSTICS = 32;
15
15
  const MAX_ACTIVE_COMMANDS = 64;
16
16
  const IMAGE_REFERENCE_SOURCES = new Set(["user", "tool-result"]);
17
+ const TOOL_EXECUTIONS = new Set(["pending", "running", "succeeded", "failed", "aborted"]);
17
18
  const BLOCK_KINDS = new Set([
18
19
  "user",
19
20
  "assistant",
@@ -185,7 +186,14 @@ export function assertOwnedUiPromptSuggestionRequest(request) {
185
186
  }
186
187
  export function assertOwnedUiPromptSuggestionResult(result) {
187
188
  assertOwnedUiPromptSuggestionIdentity(result.identity);
188
- assertPromptSuggestionText(result.text, true);
189
+ if (result.outcome === "candidate") {
190
+ assertPromptSuggestionText(result.text, false);
191
+ if (!result.text.trim())
192
+ throw new TypeError("prompt-suggestion candidate is empty");
193
+ }
194
+ else if (!["empty", "rejected", "provider-failure", "unavailable", "cancelled"].includes(result.outcome) || result.text !== null) {
195
+ throw new TypeError("prompt-suggestion outcome is invalid");
196
+ }
189
197
  }
190
198
  export function assertOwnedUiPromptSuggestionState(state) {
191
199
  if (state.status === "idle")
@@ -270,7 +278,42 @@ export function assertOwnedUiTranscriptBlock(block) {
270
278
  assertNonNegativeInteger(block.revision, "owned-UI transcript block revision");
271
279
  assertOptionalText(block.title, "owned-UI transcript block title", MAX_LABEL_LENGTH);
272
280
  assertPossiblyEmptyText(block.text, "owned-UI transcript block text", MAX_TEXT_BYTES);
273
- assertJsonValue(block.payload, "owned-UI transcript block payload", MAX_PAYLOAD_BYTES);
281
+ assertJsonValue(block.toolRendering === undefined ? block.payload : {
282
+ payload: block.payload, toolRendering: block.toolRendering,
283
+ }, "owned-UI transcript block payload", MAX_PAYLOAD_BYTES);
284
+ if (block.toolRendering !== undefined) {
285
+ if (block.kind !== "tool-call" && block.kind !== "tool-result")
286
+ throw new TypeError("owned-UI tool rendering requires a tool block");
287
+ assertJsonValue(block.toolRendering.arguments, "owned-UI tool arguments", MAX_PAYLOAD_BYTES);
288
+ if (block.toolRendering.unavailable !== undefined) {
289
+ assertBoundedText(block.toolRendering.unavailable, "owned-UI rendering fallback", MAX_MESSAGE_LENGTH);
290
+ }
291
+ if (block.toolRendering.result !== undefined) {
292
+ const result = block.toolRendering.result;
293
+ assertCollection(result.content, "owned-UI tool result parts", MAX_PAYLOAD_BYTES);
294
+ for (const part of result.content) {
295
+ if (part.type === "text") {
296
+ assertIntegerInRange(part.start, 0, block.text.length, "owned-UI tool text start");
297
+ assertIntegerInRange(part.end, part.start, block.text.length, "owned-UI tool text end");
298
+ }
299
+ else if (part.type === "image") {
300
+ assertIntegerInRange(part.imageIndex, 0, (block.imageReferences?.length ?? 0) - 1, "owned-UI tool image index");
301
+ }
302
+ else
303
+ throw new TypeError("owned-UI tool result part is invalid");
304
+ }
305
+ }
306
+ }
307
+ if (block.toolState !== undefined) {
308
+ if (block.kind !== "tool-call" && block.kind !== "tool-result")
309
+ throw new TypeError("owned-UI tool state requires a tool block");
310
+ if (typeof block.toolState.argsComplete !== "boolean")
311
+ throw new TypeError("owned-UI tool argument completion is invalid");
312
+ assertEnum(block.toolState.execution, TOOL_EXECUTIONS, "owned-UI tool execution state");
313
+ const settled = block.toolState.execution !== "pending" && block.toolState.execution !== "running";
314
+ if (settled !== (block.status === "finalized"))
315
+ throw new TypeError("owned-UI tool execution finality is inconsistent");
316
+ }
274
317
  if (block.imageReferences !== undefined) {
275
318
  assertCollection(block.imageReferences, "owned-UI transcript image references", 16);
276
319
  for (const reference of block.imageReferences) {
@@ -6,6 +6,14 @@ export interface PresentationComponentPort {
6
6
  setFocused?(focused: boolean): void;
7
7
  dispose?(): void;
8
8
  }
9
+ /** A painted input surface in terminal coordinates (one-based, inclusive bounds). */
10
+ export interface PresentationPointerSurface {
11
+ readonly component: PresentationComponentPort;
12
+ readonly columnStart: number;
13
+ readonly columnEnd: number;
14
+ readonly rowStart: number;
15
+ readonly rowEnd: number;
16
+ }
9
17
  export interface PresentationEditorPort extends PresentationComponentPort {
10
18
  getText(): string;
11
19
  setText(text: string): void;
@@ -633,7 +633,7 @@ export class SettingsApp {
633
633
  const input = this.#filter;
634
634
  if (input === null)
635
635
  return [status];
636
- return [...renderInputRow(input, width, { placeholder: SEARCH_PLACEHOLDER }).lines, status];
636
+ return [...renderInputRow(input, width, { placeholder: SEARCH_PLACEHOLDER, theme }).lines, status];
637
637
  }
638
638
  }
639
639
  /** The source's own wording when it has one, otherwise the id made readable. */
@@ -0,0 +1,32 @@
1
+ export interface PromptImageSidecarAttachment {
2
+ readonly type: "image";
3
+ readonly data: string;
4
+ readonly mimeType: string;
5
+ }
6
+ export interface PromptImageSidecarRecord {
7
+ readonly tag: string;
8
+ readonly data: string;
9
+ readonly mimeType: string;
10
+ readonly savedAt: string;
11
+ }
12
+ /** Extract every image chip identifier referenced by a recall text; identifiers are the hex bytes
13
+ * embedded in the tag by `PromptChipStore`. Ordering is left-to-right; duplicates are dropped. */
14
+ export declare function collectImageChipIdentifiers(text: string): string[];
15
+ /** Per-profile filesystem sidecar owning image chip payloads that survive a process restart.
16
+ * Writes are idempotent, reads are validated, and unlinks tolerate missing files. Callers must
17
+ * treat every failure as "the sidecar is unavailable"; that maps to the silent-strip contract. */
18
+ export declare class PromptImageSidecar {
19
+ #private;
20
+ constructor(directory: string);
21
+ get directory(): string;
22
+ write(id: string, record: PromptImageSidecarRecord): void;
23
+ read(id: string): PromptImageSidecarRecord | null;
24
+ readAttachment(id: string): PromptImageSidecarAttachment | null;
25
+ /** Enumerate every sidecar identifier that currently has a file on disk. Ignores unrelated
26
+ * files silently; corrupt payloads are still listed so a caller sweep can reclaim them. */
27
+ list(): string[];
28
+ unlink(id: string): void;
29
+ /** Delete sidecars whose identifier is not present in `keep`. Used both by retention pruning
30
+ * of a specific row and by the store-open orphan sweep against every surviving row. */
31
+ sweep(keep: Iterable<string>): void;
32
+ }
@@ -0,0 +1,130 @@
1
+ import { chmodSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, lstatSync } from "node:fs";
2
+ import { posix, win32 } from "node:path";
3
+ const IMAGE_TAG_PATTERN = /\[📷 screenshot-([a-f0-9]+)(?:-resized)?\]/gu;
4
+ const IMAGE_FILENAME_PATTERN = /^([a-f0-9]+)\.json$/u;
5
+ const MAX_SIDECAR_BYTES = 12 * 1024 * 1024;
6
+ /** Extract every image chip identifier referenced by a recall text; identifiers are the hex bytes
7
+ * embedded in the tag by `PromptChipStore`. Ordering is left-to-right; duplicates are dropped. */
8
+ export function collectImageChipIdentifiers(text) {
9
+ const seen = new Set();
10
+ IMAGE_TAG_PATTERN.lastIndex = 0;
11
+ for (;;) {
12
+ const match = IMAGE_TAG_PATTERN.exec(text);
13
+ if (match === null)
14
+ break;
15
+ seen.add(match[1]);
16
+ }
17
+ return [...seen];
18
+ }
19
+ /** Per-profile filesystem sidecar owning image chip payloads that survive a process restart.
20
+ * Writes are idempotent, reads are validated, and unlinks tolerate missing files. Callers must
21
+ * treat every failure as "the sidecar is unavailable"; that maps to the silent-strip contract. */
22
+ export class PromptImageSidecar {
23
+ #directory;
24
+ constructor(directory) {
25
+ this.#directory = directory;
26
+ }
27
+ get directory() { return this.#directory; }
28
+ write(id, record) {
29
+ if (!isValidId(id))
30
+ return;
31
+ this.#ensureDirectory();
32
+ const target = this.#pathFor(id);
33
+ try {
34
+ // Concurrency: another process may have written the same id; a subsequent overwrite is
35
+ // idempotent because sidecar payloads are keyed to a chip tag whose id already collided.
36
+ const temporary = `${target}.tmp`;
37
+ writeFileSync(temporary, JSON.stringify(record), { encoding: "utf8", mode: 0o600, flag: "w" });
38
+ try {
39
+ if (process.platform !== "win32")
40
+ chmodSync(temporary, 0o600);
41
+ }
42
+ catch { /* Compatibility: chmod is best-effort where the platform ignores it. */ }
43
+ renameSync(temporary, target);
44
+ }
45
+ catch {
46
+ // Security: sidecar write failures never propagate into shell diagnostics or history rows.
47
+ }
48
+ }
49
+ read(id) {
50
+ if (!isValidId(id))
51
+ return null;
52
+ try {
53
+ const target = this.#pathFor(id);
54
+ const status = lstatSync(target);
55
+ if (status.isSymbolicLink() || !status.isFile() || status.size > MAX_SIDECAR_BYTES)
56
+ return null;
57
+ const parsed = JSON.parse(readFileSync(target, "utf8"));
58
+ if (typeof parsed !== "object" || parsed === null)
59
+ return null;
60
+ const record = parsed;
61
+ if (typeof record.tag !== "string" || typeof record.data !== "string"
62
+ || typeof record.mimeType !== "string" || typeof record.savedAt !== "string"
63
+ || record.data.length === 0 || record.tag.length === 0 || record.mimeType.length === 0)
64
+ return null;
65
+ return { tag: record.tag, data: record.data, mimeType: record.mimeType, savedAt: record.savedAt };
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ readAttachment(id) {
72
+ const record = this.read(id);
73
+ if (record === null)
74
+ return null;
75
+ return Object.freeze({ type: "image", data: record.data, mimeType: record.mimeType });
76
+ }
77
+ /** Enumerate every sidecar identifier that currently has a file on disk. Ignores unrelated
78
+ * files silently; corrupt payloads are still listed so a caller sweep can reclaim them. */
79
+ list() {
80
+ try {
81
+ const entries = readdirSync(this.#directory, { withFileTypes: true });
82
+ const ids = [];
83
+ for (const entry of entries) {
84
+ if (!entry.isFile())
85
+ continue;
86
+ const match = IMAGE_FILENAME_PATTERN.exec(entry.name);
87
+ if (match !== null)
88
+ ids.push(match[1]);
89
+ }
90
+ return ids;
91
+ }
92
+ catch {
93
+ return [];
94
+ }
95
+ }
96
+ unlink(id) {
97
+ if (!isValidId(id))
98
+ return;
99
+ try {
100
+ unlinkSync(this.#pathFor(id));
101
+ }
102
+ catch { /* Compatibility: ENOENT / EBUSY is indistinguishable from an already-reaped sidecar. */ }
103
+ }
104
+ /** Delete sidecars whose identifier is not present in `keep`. Used both by retention pruning
105
+ * of a specific row and by the store-open orphan sweep against every surviving row. */
106
+ sweep(keep) {
107
+ const survivors = new Set(keep);
108
+ for (const id of this.list()) {
109
+ if (!survivors.has(id))
110
+ this.unlink(id);
111
+ }
112
+ }
113
+ #ensureDirectory() {
114
+ try {
115
+ mkdirSync(this.#directory, { recursive: true, mode: 0o700 });
116
+ if (process.platform !== "win32")
117
+ chmodSync(this.#directory, 0o700);
118
+ }
119
+ catch {
120
+ // Compatibility: the directory may already exist with a different mode we cannot change.
121
+ }
122
+ }
123
+ #pathFor(id) {
124
+ const paths = process.platform === "win32" ? win32 : posix;
125
+ return paths.join(this.#directory, `${id}.json`);
126
+ }
127
+ }
128
+ function isValidId(value) {
129
+ return typeof value === "string" && value.length > 0 && value.length <= 64 && /^[a-f0-9]+$/u.test(value);
130
+ }
@@ -1,2 +1,4 @@
1
1
  export { PromptHistoryService } from "./service.js";
2
2
  export { resolvePromptHistoryPath } from "./paths.js";
3
+ export { PromptImageSidecar, collectImageChipIdentifiers } from "./image-sidecar.js";
4
+ export type { PromptImageSidecarRecord, PromptImageSidecarAttachment } from "./image-sidecar.js";
@@ -1,2 +1,3 @@
1
1
  export { PromptHistoryService } from "./service.js";
2
2
  export { resolvePromptHistoryPath } from "./paths.js";
3
+ export { PromptImageSidecar, collectImageChipIdentifiers } from "./image-sidecar.js";
@@ -1,4 +1,5 @@
1
1
  export declare function resolvePromptHistoryPath(dataDir: string, profileRoot: string, platform?: NodeJS.Platform): {
2
2
  path: string;
3
3
  profileId: string;
4
+ imagesDir: string;
4
5
  };
@@ -12,5 +12,6 @@ export function resolvePromptHistoryPath(dataDir, profileRoot, platform = proces
12
12
  normalized = normalized.replaceAll("\\", "/").toLowerCase();
13
13
  const digest = createHash("sha256").update(JSON.stringify([1, PRODUCT_IDENTITY.commandName, normalized])).digest("hex");
14
14
  const profileId = `${PRODUCT_IDENTITY.filesystem.temporaryPrefix}${digest}`;
15
- return { path: paths.join(dataDir, "history", `${profileId}.sqlite3`), profileId };
15
+ const historyDir = paths.join(dataDir, "history");
16
+ return { path: paths.join(historyDir, `${profileId}.sqlite3`), profileId, imagesDir: paths.join(historyDir, `${profileId}-images`) };
16
17
  }