@timurproko/a1 0.1.8-dev.271 → 0.1.8-dev.277

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 (34) hide show
  1. package/dist/composition/owned-ui.js +6 -0
  2. package/dist/contracts/owned-ui/index.d.ts +1 -0
  3. package/dist/contracts/owned-ui/index.js +1 -0
  4. package/dist/contracts/owned-ui/model.d.ts +52 -0
  5. package/dist/contracts/owned-ui/prompt-suggestions.d.ts +3 -0
  6. package/dist/contracts/owned-ui/prompt-suggestions.js +41 -0
  7. package/dist/contracts/owned-ui/validation.d.ts +5 -1
  8. package/dist/contracts/owned-ui/validation.js +67 -3
  9. package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -0
  10. package/dist/integrations/pi/components/owned-editor-ux.js +5 -3
  11. package/dist/integrations/pi/components/shell-editor-autocomplete.js +15 -0
  12. package/dist/integrations/pi/components/shell-shared-facade.d.ts +8 -0
  13. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +15 -2
  14. package/dist/integrations/pi/components/upstream/components/owned-editor.js +102 -3
  15. package/dist/integrations/pi/engine/adapter.d.ts +3 -2
  16. package/dist/integrations/pi/engine/adapter.js +106 -2
  17. package/dist/integrations/pi/engine/conformance.d.ts +1 -1
  18. package/dist/integrations/pi/engine/conformance.js +2 -2
  19. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  20. package/dist/integrations/pi/session-ui/index.d.ts +1 -0
  21. package/dist/integrations/pi/session-ui/index.js +1 -0
  22. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +27 -0
  23. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +132 -0
  24. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +12 -1
  25. package/dist/integrations/pi/session-ui/session-shell-root.js +27 -1
  26. package/dist/integrations/pi/session-ui/session-shell.js +86 -2
  27. package/dist/native/darwin-arm64/manifest.json +1 -1
  28. package/dist/native/linux-x64/manifest.json +1 -1
  29. package/dist/native/win32-x64/manifest.json +2 -2
  30. package/dist/native/win32-x64/process-guardian.exe +0 -0
  31. package/dist/ui/settings/declarations.d.ts +1 -1
  32. package/dist/ui/settings/declarations.js +9 -1
  33. package/dist/ui/settings/migrations.js +7 -0
  34. package/package.json +1 -1
@@ -37,6 +37,11 @@ export async function composeOwnedUi(options = {}) {
37
37
  snapshot: () => viewportSettingsSnapshot(settings),
38
38
  onChange: listener => settings.onChange(() => listener(viewportSettingsSnapshot(settings))),
39
39
  };
40
+ const promptSuggestions = settings === null || !ownedSurfaces ? null : {
41
+ generator: adapter,
42
+ enabled: () => settings.value("promptSuggestions") !== false,
43
+ onChange: (listener) => settings.onChange(() => listener(settings.value("promptSuggestions") !== false)),
44
+ };
40
45
  const shell = new OwnedUiSessionShell({
41
46
  backend: adapter,
42
47
  cwd: adapter.cwd,
@@ -44,6 +49,7 @@ export async function composeOwnedUi(options = {}) {
44
49
  ...(routeHost === null ? {} : { routeHost }),
45
50
  ...(ownedSurfaces ? { sessionLayout: "custom-viewport" } : {}),
46
51
  ...(viewportSettings === null ? {} : { viewportSettings }),
52
+ ...(promptSuggestions === null ? {} : { promptSuggestions }),
47
53
  });
48
54
  const application = {
49
55
  get disposed() { return adapter.disposed; },
@@ -1,3 +1,4 @@
1
1
  export * from "./extension-ui.js";
2
2
  export * from "./model.js";
3
+ export * from "./prompt-suggestions.js";
3
4
  export * from "./validation.js";
@@ -1,3 +1,4 @@
1
1
  export * from "./extension-ui.js";
2
2
  export * from "./model.js";
3
+ export * from "./prompt-suggestions.js";
3
4
  export * from "./validation.js";
@@ -11,6 +11,39 @@ export interface OwnedUiModelInfo {
11
11
  readonly modelId: string;
12
12
  readonly displayName: string;
13
13
  }
14
+ export interface OwnedUiPromptSuggestionIdentity {
15
+ readonly sessionId: OwnedUiSessionId;
16
+ readonly sessionGeneration: number;
17
+ readonly runSequence: number;
18
+ readonly responseSequence: number;
19
+ readonly model: OwnedUiModelInfo;
20
+ }
21
+ export interface OwnedUiPromptSuggestionRequest {
22
+ readonly identity: OwnedUiPromptSuggestionIdentity;
23
+ readonly signal: AbortSignal;
24
+ }
25
+ export interface OwnedUiPromptSuggestionResult {
26
+ readonly identity: OwnedUiPromptSuggestionIdentity;
27
+ readonly text: string | null;
28
+ }
29
+ export type OwnedUiPromptSuggestionState = {
30
+ readonly status: "idle";
31
+ } | {
32
+ readonly status: "generating";
33
+ readonly identity: OwnedUiPromptSuggestionIdentity;
34
+ readonly settled: boolean;
35
+ } | {
36
+ readonly status: "prepared";
37
+ readonly identity: OwnedUiPromptSuggestionIdentity;
38
+ readonly text: string;
39
+ } | {
40
+ readonly status: "available";
41
+ readonly identity: OwnedUiPromptSuggestionIdentity;
42
+ readonly text: string;
43
+ };
44
+ export interface OwnedUiPromptSuggestionGeneratorPort {
45
+ generate(request: OwnedUiPromptSuggestionRequest): Promise<OwnedUiPromptSuggestionResult>;
46
+ }
14
47
  export type OwnedUiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
15
48
  export interface OwnedUiViewportSettings {
16
49
  readonly scrollbarAppearance: "auto" | "always" | "hidden";
@@ -220,11 +253,30 @@ export type OwnedUiEvent = {
220
253
  readonly type: "assistant-message-completed";
221
254
  readonly sessionId: OwnedUiSessionId;
222
255
  readonly sequence: number;
256
+ readonly sessionGeneration: number;
257
+ readonly runSequence: number;
258
+ readonly responseSequence: number;
259
+ readonly model: OwnedUiModelInfo | null;
260
+ readonly assistantMessageCount: number;
261
+ readonly successful: boolean;
262
+ readonly stopReason: string | null;
263
+ readonly toolContinuation: boolean;
223
264
  } | {
224
265
  /** A fresh agent run has started, used by follow-mode surfaces. */
225
266
  readonly type: "agent-run-started";
226
267
  readonly sessionId: OwnedUiSessionId;
227
268
  readonly sequence: number;
269
+ } | {
270
+ /** The final settlement of one run, after authoritative transcript reconciliation. */
271
+ readonly type: "agent-run-settled";
272
+ readonly sessionId: OwnedUiSessionId;
273
+ readonly sequence: number;
274
+ readonly sessionGeneration: number;
275
+ readonly runSequence: number;
276
+ readonly responseSequence: number;
277
+ readonly model: OwnedUiModelInfo | null;
278
+ readonly assistantMessageCount: number;
279
+ readonly successful: boolean;
228
280
  } | {
229
281
  readonly type: "editor-state";
230
282
  readonly sessionId: OwnedUiSessionId;
@@ -0,0 +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.";
2
+ /** Converts untrusted model output into one inert, bounded user-voice candidate. */
3
+ export declare function normalizePromptSuggestionCandidate(candidate: string | null | undefined): string | null;
@@ -0,0 +1,41 @@
1
+ export const CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION = `[NEXT USER INPUT]
2
+ Predict the one short response the user is most likely to type next.
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.
5
+ Do not answer as the assistant. Do not add a label, explanation, quotation marks, Markdown, or multiple sentences.
6
+ Return only 2-12 words, except a natural one-word command or answer is allowed.`;
7
+ const ALLOWED_SINGLE_WORDS = new Set([
8
+ "yes", "yeah", "yep", "sure", "ok", "okay", "no",
9
+ "continue", "apply", "commit", "push", "deploy", "test", "check", "stop", "exit", "quit",
10
+ ]);
11
+ /** Converts untrusted model output into one inert, bounded user-voice candidate. */
12
+ export function normalizePromptSuggestionCandidate(candidate) {
13
+ if (typeof candidate !== "string")
14
+ return null;
15
+ const suggestion = candidate.trim();
16
+ if (suggestion.length === 0 || [...suggestion].length >= 100)
17
+ return null;
18
+ if (/[\p{C}\r\n\t]/u.test(suggestion))
19
+ return null;
20
+ if (/[\*`#]|__|~~/.test(suggestion))
21
+ return null;
22
+ if (/[.!?]\s+\S/u.test(suggestion))
23
+ return null;
24
+ const lower = suggestion.toLowerCase();
25
+ if (/^(api error:|prompt is too long|request timed out|invalid api key|image was too large)/.test(lower))
26
+ return null;
27
+ if (lower === "done" || /^(nothing to suggest|no suggestion|stay silent|silence\b)/.test(lower))
28
+ return null;
29
+ if (/^\w+:\s/u.test(suggestion))
30
+ return null;
31
+ if (/^(let me|i(?:'|’)ll|i(?:'|’)ve|i(?:'|’)m|i can|i would|i think|here(?:'|’)s|here is|here are|you can|you should|you could|sure,|of course|certainly)\b/i.test(suggestion))
32
+ return null;
33
+ if (/\b(thanks|thank you|looks good|sounds good|that works|that worked|nice|great|perfect|awesome|excellent)\b/i.test(suggestion))
34
+ return null;
35
+ const words = suggestion.split(/\s+/u);
36
+ if (words.length > 12)
37
+ return null;
38
+ if (words.length === 1 && !suggestion.startsWith("/") && !ALLOWED_SINGLE_WORDS.has(lower))
39
+ return null;
40
+ return suggestion;
41
+ }
@@ -1,6 +1,10 @@
1
- import { type OwnedUiCommand, type OwnedUiCustomization, type OwnedUiDiagnostics, type OwnedUiEditorState, type OwnedUiEvent, type OwnedUiSessionViewModel, type OwnedUiSnapshot, type OwnedUiStatusView, type OwnedUiTerminalSurface, type OwnedUiTranscriptBlock } from "./model.js";
1
+ import { type OwnedUiCommand, type OwnedUiCustomization, type OwnedUiDiagnostics, type OwnedUiEditorState, type OwnedUiEvent, type OwnedUiPromptSuggestionIdentity, type OwnedUiPromptSuggestionRequest, type OwnedUiPromptSuggestionResult, type OwnedUiPromptSuggestionState, type OwnedUiSessionViewModel, type OwnedUiSnapshot, type OwnedUiStatusView, type OwnedUiTerminalSurface, type OwnedUiTranscriptBlock } from "./model.js";
2
2
  export declare function assertOwnedUiCommand(command: OwnedUiCommand): void;
3
3
  export declare function assertOwnedUiEvent(event: OwnedUiEvent): void;
4
+ export declare function assertOwnedUiPromptSuggestionIdentity(identity: OwnedUiPromptSuggestionIdentity): void;
5
+ export declare function assertOwnedUiPromptSuggestionRequest(request: OwnedUiPromptSuggestionRequest): void;
6
+ export declare function assertOwnedUiPromptSuggestionResult(result: OwnedUiPromptSuggestionResult): void;
7
+ export declare function assertOwnedUiPromptSuggestionState(state: OwnedUiPromptSuggestionState): void;
4
8
  export declare function assertOwnedUiSessionViewModel(view: OwnedUiSessionViewModel): void;
5
9
  export declare function assertOwnedUiSnapshot(snapshot: OwnedUiSnapshot): void;
6
10
  export declare function assertOwnedUiCustomization(customization: OwnedUiCustomization): void;
@@ -116,8 +116,30 @@ export function assertOwnedUiEvent(event) {
116
116
  assertOwnedUiTranscriptBlock(event.block);
117
117
  return;
118
118
  case "assistant-message-completed":
119
+ assertNonNegativeInteger(event.sessionGeneration, "owned-UI assistant response session generation");
120
+ assertNonNegativeInteger(event.runSequence, "owned-UI assistant response run sequence");
121
+ assertNonNegativeInteger(event.responseSequence, "owned-UI assistant response sequence");
122
+ assertNonNegativeInteger(event.assistantMessageCount, "owned-UI assistant response count");
123
+ if (event.model !== null)
124
+ assertOwnedUiModelInfo(event.model);
125
+ if (typeof event.successful !== "boolean")
126
+ throw new TypeError("owned-UI assistant response success state is invalid");
127
+ assertOptionalText(event.stopReason, "owned-UI assistant response stop reason", MAX_LABEL_LENGTH);
128
+ if (typeof event.toolContinuation !== "boolean")
129
+ throw new TypeError("owned-UI assistant response tool-continuation state is invalid");
130
+ return;
119
131
  case "agent-run-started":
120
132
  return;
133
+ case "agent-run-settled":
134
+ assertNonNegativeInteger(event.sessionGeneration, "owned-UI settlement session generation");
135
+ assertNonNegativeInteger(event.runSequence, "owned-UI settlement run sequence");
136
+ assertNonNegativeInteger(event.responseSequence, "owned-UI settlement response sequence");
137
+ assertNonNegativeInteger(event.assistantMessageCount, "owned-UI settlement assistant message count");
138
+ if (event.model !== null)
139
+ assertOwnedUiModelInfo(event.model);
140
+ if (typeof event.successful !== "boolean")
141
+ throw new TypeError("owned-UI settlement success state is invalid");
142
+ return;
121
143
  case "editor-state":
122
144
  assertOwnedUiEditorState(event.editor);
123
145
  return;
@@ -153,6 +175,38 @@ export function assertOwnedUiEvent(event) {
153
175
  throw new TypeError("owned-UI event type is unknown");
154
176
  }
155
177
  }
178
+ export function assertOwnedUiPromptSuggestionIdentity(identity) {
179
+ assertId(identity.sessionId, "prompt-suggestion session id");
180
+ assertNonNegativeInteger(identity.sessionGeneration, "prompt-suggestion session generation");
181
+ assertNonNegativeInteger(identity.runSequence, "prompt-suggestion run sequence");
182
+ assertNonNegativeInteger(identity.responseSequence, "prompt-suggestion response sequence");
183
+ assertOwnedUiModelInfo(identity.model);
184
+ }
185
+ export function assertOwnedUiPromptSuggestionRequest(request) {
186
+ assertOwnedUiPromptSuggestionIdentity(request.identity);
187
+ if (typeof request.signal !== "object" || request.signal === null
188
+ || typeof request.signal.aborted !== "boolean"
189
+ || typeof request.signal.addEventListener !== "function") {
190
+ throw new TypeError("prompt-suggestion abort signal is invalid");
191
+ }
192
+ }
193
+ export function assertOwnedUiPromptSuggestionResult(result) {
194
+ assertOwnedUiPromptSuggestionIdentity(result.identity);
195
+ assertPromptSuggestionText(result.text, true);
196
+ }
197
+ export function assertOwnedUiPromptSuggestionState(state) {
198
+ if (state.status === "idle")
199
+ return;
200
+ if (state.status !== "generating" && state.status !== "prepared" && state.status !== "available") {
201
+ throw new TypeError("prompt-suggestion state is invalid");
202
+ }
203
+ assertOwnedUiPromptSuggestionIdentity(state.identity);
204
+ if (state.status === "generating" && typeof state.settled !== "boolean") {
205
+ throw new TypeError("prompt-suggestion settlement state is invalid");
206
+ }
207
+ if (state.status === "prepared" || state.status === "available")
208
+ assertPromptSuggestionText(state.text, false);
209
+ }
156
210
  export function assertOwnedUiSessionViewModel(view) {
157
211
  if (view.contractVersion !== OWNED_UI_CONTRACT_VERSION) {
158
212
  throw new TypeError("unsupported owned-UI contract version");
@@ -164,9 +218,7 @@ export function assertOwnedUiSessionViewModel(view) {
164
218
  assertOwnedUiStatusView(view.status);
165
219
  assertOwnedUiTerminalSurface(view.terminal);
166
220
  if (view.activeModel !== null) {
167
- assertId(view.activeModel.providerId, "owned-UI provider id");
168
- assertId(view.activeModel.modelId, "owned-UI model id");
169
- assertBoundedText(view.activeModel.displayName, "owned-UI model display name", MAX_LABEL_LENGTH);
221
+ assertOwnedUiModelInfo(view.activeModel);
170
222
  }
171
223
  assertEnum(view.thinkingLevel, THINKING_LEVELS, "owned-UI thinking level");
172
224
  assertCollection(view.activeCommandIds, "owned-UI active commands", MAX_ACTIVE_COMMANDS);
@@ -331,6 +383,11 @@ export function assertOwnedUiDiagnostics(diagnostic) {
331
383
  if (typeof diagnostic.recoverable !== "boolean")
332
384
  throw new TypeError("owned-UI diagnostic recoverability is invalid");
333
385
  }
386
+ function assertOwnedUiModelInfo(model) {
387
+ assertId(model.providerId, "owned-UI provider id");
388
+ assertId(model.modelId, "owned-UI model id");
389
+ assertBoundedText(model.displayName, "owned-UI model display name", MAX_LABEL_LENGTH);
390
+ }
334
391
  function assertOwnedUiDialog(dialog) {
335
392
  assertId(dialog.id, "owned-UI dialog id");
336
393
  assertBoundedText(dialog.title, "owned-UI dialog title", MAX_LABEL_LENGTH);
@@ -381,6 +438,13 @@ function assertOptionalText(value, name, maximumBytes) {
381
438
  return;
382
439
  assertBoundedText(value, name, maximumBytes);
383
440
  }
441
+ function assertPromptSuggestionText(value, allowEmpty) {
442
+ if (value === null)
443
+ return;
444
+ if (typeof value !== "string" || value.includes("\0") || (!allowEmpty && value.length === 0) || [...value].length >= 100) {
445
+ throw new TypeError("prompt-suggestion text is invalid");
446
+ }
447
+ }
384
448
  function assertCollection(value, name, maximum) {
385
449
  if (!Array.isArray(value) || value.length > maximum)
386
450
  throw new RangeError(`${name} exceeds its maximum length`);
@@ -51,5 +51,7 @@ export interface PromptSelectionUxOptions {
51
51
  readonly decorateRow: (row: string, width: number) => string;
52
52
  readonly requestRender: () => void;
53
53
  readonly getRows: () => number;
54
+ /** Presentation-only columns reserved before semantic editor text. */
55
+ readonly promptPrefixWidth?: number;
54
56
  }
55
57
  export declare function createPromptSelectionInterceptor(editor: Editor, keybindings: KeybindingsManager, options: PromptSelectionUxOptions): OwnedEditorUxInterceptor;
@@ -200,7 +200,9 @@ class PromptSelectionInterceptor {
200
200
  const rows = next().map(row => row.replaceAll(ATOMIC_SPACE_SENTINEL, " "));
201
201
  const maxPadding = Math.max(0, Math.floor((width - 1) / 2));
202
202
  const padding = Math.min(this.editor.getPaddingX(), maxPadding);
203
- const contentWidth = Math.max(1, width - padding * 2);
203
+ const prefixWidth = this.options.promptPrefixWidth ?? 0;
204
+ const innerWidth = Math.max(1, width - prefixWidth);
205
+ const contentWidth = Math.max(1, innerWidth - padding * 2);
204
206
  const layoutWidth = Math.max(1, contentWidth - (padding ? 0 : 1));
205
207
  const visualLines = editorVisualLineMap(this.editor, layoutWidth)
206
208
  ?? buildVisualLineMap(editorState(this.editor).lines, layoutWidth);
@@ -226,7 +228,7 @@ class PromptSelectionInterceptor {
226
228
  if (to <= from)
227
229
  continue;
228
230
  const line = editorState(this.editor).lines[visual.logicalLine] ?? "";
229
- const fromColumn = padding + visibleWidth(line.slice(segmentStart, from));
231
+ const fromColumn = prefixWidth + padding + visibleWidth(line.slice(segmentStart, from));
230
232
  const toColumn = fromColumn + visibleWidth(line.slice(from, to));
231
233
  const rendered = rows[row + 1];
232
234
  if (rendered !== undefined && toColumn > fromColumn) {
@@ -338,7 +340,7 @@ class PromptSelectionInterceptor {
338
340
  return undefined;
339
341
  const line = editorState(this.editor).lines[visual.logicalLine] ?? "";
340
342
  const segment = line.slice(visual.startCol, visual.startCol + visual.length);
341
- const displayColumn = Math.max(0, column - 1 - geometry.padding);
343
+ const displayColumn = Math.max(0, column - 1 - geometry.padding - (this.options.promptPrefixWidth ?? 0));
342
344
  return {
343
345
  line: visual.logicalLine,
344
346
  col: visual.startCol + indexAtDisplayWidth(segment, displayColumn),
@@ -42,6 +42,12 @@ export function createPiShellEditor(options) {
42
42
  }, keybindings, {
43
43
  paddingX: PINNED_PI_LAYOUT.editorPaddingX,
44
44
  autocompleteMaxVisible: PINNED_PI_LAYOUT.autocompleteMaxVisible,
45
+ ...(options.keybindingProfile === "a1" && options.promptPresentation !== undefined ? {
46
+ promptPrefix: options.promptPresentation.prefix,
47
+ styleSuggestion: options.promptPresentation.styleSuggestion,
48
+ styleSuggestionCaret: options.promptPresentation.styleSuggestionCaret,
49
+ terminalRows: options.getRows,
50
+ } : {}),
45
51
  });
46
52
  const editorUx = options.keybindingProfile === "a1"
47
53
  ? new OwnedEditorUxInterception([
@@ -55,6 +61,7 @@ export function createPiShellEditor(options) {
55
61
  decorateRow: options.decorateEditorRow ?? (row => row),
56
62
  requestRender: options.requestRender,
57
63
  getRows: options.getRows,
64
+ ...(options.promptPresentation === undefined ? {} : { promptPrefixWidth: 2 }),
58
65
  }),
59
66
  ], {
60
67
  render: width => editor.render(width),
@@ -128,6 +135,9 @@ export function createPiShellEditor(options) {
128
135
  editor.onAction("app.message.followUp", options.onFollowUp);
129
136
  if (options.onDequeue !== undefined)
130
137
  editor.onAction("app.message.dequeue", options.onDequeue);
138
+ if (options.onPromptSuggestionAccepted !== undefined) {
139
+ editor.onPromptSuggestionAccepted = options.onPromptSuggestionAccepted;
140
+ }
131
141
  return {
132
142
  render: width => editorUx?.render(width) ?? editor.render(width),
133
143
  activateKeybindings: () => setKeybindings(keybindings),
@@ -183,6 +193,11 @@ export function createPiShellEditor(options) {
183
193
  thinkingLevel = level;
184
194
  updateBorderColor();
185
195
  },
196
+ setPromptSuggestion(text) {
197
+ editor.setPromptSuggestion(text);
198
+ editor.invalidate();
199
+ },
200
+ canPresentPromptSuggestion: () => editor.canPresentPromptSuggestion(),
186
201
  hasSelection: () => editorUx?.hasSelection() ?? false,
187
202
  ownsPointer: () => editorUx?.ownsPointer() ?? false,
188
203
  handlePointer: event => editorUx?.handlePointer(event) ?? false,
@@ -41,6 +41,8 @@ export interface PiShellEditorPort extends PiShellComponentPort {
41
41
  setAutocompleteMaxVisible(maxVisible: number): void;
42
42
  addAutocompleteProvider(factory: unknown): void;
43
43
  setThinkingLevel(level: OwnedUiThinkingLevel): void;
44
+ setPromptSuggestion(text: string | null): void;
45
+ canPresentPromptSuggestion(): boolean;
44
46
  hasSelection(): boolean;
45
47
  ownsPointer(): boolean;
46
48
  handlePointer(event: PiShellEditorPointerEvent): boolean;
@@ -131,6 +133,7 @@ export interface PiShellEditorOptions {
131
133
  readonly onMessageCopy?: (() => void) | undefined;
132
134
  readonly onFollowUp?: (() => void) | undefined;
133
135
  readonly onDequeue?: (() => void) | undefined;
136
+ readonly onPromptSuggestionAccepted?: (text: string) => void;
134
137
  readonly onCopyText?: (text: string) => void;
135
138
  readonly readClipboardContent?: () => Promise<PiShellClipboardContent | null>;
136
139
  readonly transformPastedContent?: (content: PiShellClipboardContent) => string;
@@ -141,6 +144,11 @@ export interface PiShellEditorOptions {
141
144
  readonly cwd?: string;
142
145
  readonly agentDir?: string;
143
146
  readonly autocompleteCommands?: readonly PiShellAutocompleteCommand[];
147
+ readonly promptPresentation?: {
148
+ readonly prefix: string;
149
+ readonly styleSuggestion: (text: string) => string;
150
+ readonly styleSuggestionCaret: (text: string) => string;
151
+ };
144
152
  }
145
153
  export interface PiShellSelectorOption {
146
154
  readonly id: string;
@@ -1,18 +1,31 @@
1
1
  /**
2
2
  * Adapted from @earendil-works/pi-coding-agent 0.84.2
3
3
  * packages/coding-agent/src/modes/interactive/components/custom-editor.ts (MIT).
4
- * Modifications: A1-owned class name and A1-owned synchronized keybinding contract.
4
+ * Modifications: A1-owned class name, synchronized keybinding contract, and a semantic
5
+ * bare-A1 prompt-prefix/contextual-suggestion presentation branch.
5
6
  */
6
7
  import { Editor, type EditorOptions, type EditorTheme, type TUI } from "#pi-tui";
7
8
  import type { AppKeybinding, KeybindingsManager } from "../adjacent/core/keybindings.js";
9
+ export interface OwnedEditorOptions extends EditorOptions {
10
+ readonly promptPrefix?: string;
11
+ readonly styleSuggestion?: (text: string) => string;
12
+ readonly styleSuggestionCaret?: (text: string) => string;
13
+ readonly terminalRows?: () => number;
14
+ }
8
15
  export declare class OwnedEditor extends Editor {
16
+ #private;
9
17
  private readonly keybindings;
10
18
  readonly actionHandlers: Map<keyof import("../adjacent/core/keybindings.js").AppKeybindings, () => void>;
11
19
  onEscape?: () => void;
12
20
  onCtrlD?: () => void;
13
21
  onPasteImage?: () => void;
14
22
  onExtensionShortcut?: (data: string) => boolean;
15
- constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: EditorOptions);
23
+ onPromptSuggestionAccepted?: (text: string) => void;
24
+ constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: OwnedEditorOptions);
25
+ setPromptSuggestion(text: string | null): void;
26
+ canPresentPromptSuggestion(): boolean;
27
+ setText(text: string): void;
28
+ render(width: number): string[];
16
29
  onAction(action: AppKeybinding, handler: () => void): void;
17
30
  handleInput(data: string): void;
18
31
  }
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Adapted from @earendil-works/pi-coding-agent 0.84.2
3
3
  * packages/coding-agent/src/modes/interactive/components/custom-editor.ts (MIT).
4
- * Modifications: A1-owned class name and A1-owned synchronized keybinding contract.
4
+ * Modifications: A1-owned class name, synchronized keybinding contract, and a semantic
5
+ * bare-A1 prompt-prefix/contextual-suggestion presentation branch.
5
6
  */
6
- import { Editor } from "#pi-tui";
7
+ import { CURSOR_MARKER, Editor, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "#pi-tui";
8
+ const PROMPT_GRAPHEMES = new Intl.Segmenter(undefined, { granularity: "grapheme" });
7
9
  export class OwnedEditor extends Editor {
8
10
  keybindings;
9
11
  actionHandlers = new Map();
@@ -11,14 +13,63 @@ export class OwnedEditor extends Editor {
11
13
  onCtrlD;
12
14
  onPasteImage;
13
15
  onExtensionShortcut;
14
- constructor(tui, theme, keybindings, options) {
16
+ onPromptSuggestionAccepted;
17
+ #promptSuggestion = null;
18
+ #promptPrefix;
19
+ #styleSuggestion;
20
+ #styleSuggestionCaret;
21
+ #terminalRows;
22
+ constructor(tui, theme, keybindings, options = {}) {
15
23
  super(tui, theme, options);
16
24
  this.keybindings = keybindings;
25
+ this.#promptPrefix = options.promptPrefix ?? "";
26
+ this.#styleSuggestion = options.styleSuggestion ?? (text => text);
27
+ this.#styleSuggestionCaret = options.styleSuggestionCaret ?? (text => `\u001b[7m${text}\u001b[27m`);
28
+ this.#terminalRows = options.terminalRows ?? (() => 24);
29
+ }
30
+ setPromptSuggestion(text) {
31
+ this.#promptSuggestion = text;
32
+ }
33
+ canPresentPromptSuggestion() {
34
+ return this.#promptPrefix.length > 0
35
+ && this.focused
36
+ && !this.disableSubmit
37
+ && this.getText().length === 0
38
+ && !this.isShowingAutocomplete();
39
+ }
40
+ setText(text) {
41
+ if (text.length > 0)
42
+ this.#promptSuggestion = null;
43
+ super.setText(text);
44
+ }
45
+ render(width) {
46
+ if (this.#promptPrefix.length === 0)
47
+ return super.render(width);
48
+ if (this.#promptSuggestion !== null && this.canPresentPromptSuggestion()) {
49
+ return this.#renderSuggestion(width);
50
+ }
51
+ return this.#renderPrefixedEditor(width);
17
52
  }
18
53
  onAction(action, handler) { this.actionHandlers.set(action, handler); }
19
54
  handleInput(data) {
20
55
  if (this.onExtensionShortcut?.(data))
21
56
  return;
57
+ if (this.#promptSuggestion !== null
58
+ && !this.isShowingAutocomplete()
59
+ && this.canPresentPromptSuggestion()
60
+ && this.keybindings.matches(data, "tui.input.submit")) {
61
+ return;
62
+ }
63
+ if (this.#promptSuggestion !== null
64
+ && !this.isShowingAutocomplete()
65
+ && this.canPresentPromptSuggestion()
66
+ && this.keybindings.matches(data, "tui.input.tab")) {
67
+ const accepted = this.#promptSuggestion;
68
+ this.#promptSuggestion = null;
69
+ super.setText(accepted);
70
+ this.onPromptSuggestionAccepted?.(accepted);
71
+ return;
72
+ }
22
73
  if (this.keybindings.matches(data, "app.clipboard.pasteImage")) {
23
74
  this.onPasteImage?.();
24
75
  return;
@@ -52,4 +103,52 @@ export class OwnedEditor extends Editor {
52
103
  }
53
104
  super.handleInput(data);
54
105
  }
106
+ #renderSuggestion(width) {
107
+ const prefixWidth = visibleWidth(this.#promptPrefix);
108
+ const innerWidth = Math.max(1, width - prefixWidth);
109
+ const maxPadding = Math.max(0, Math.floor((innerWidth - 1) / 2));
110
+ const paddingX = Math.min(this.getPaddingX(), maxPadding);
111
+ const contentWidth = Math.max(1, innerWidth - paddingX * 2);
112
+ const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1));
113
+ const chunks = wrapTextWithAnsi(this.#promptSuggestion ?? "", layoutWidth).map(text => ({ text }));
114
+ const maxVisible = Math.max(5, Math.floor(this.#terminalRows() * 0.3));
115
+ const visible = chunks.slice(0, maxVisible);
116
+ const horizontal = this.borderColor("─".repeat(Math.max(0, width)));
117
+ const leftPadding = " ".repeat(paddingX);
118
+ const rightPadding = leftPadding;
119
+ const rows = visible.map((chunk, index) => {
120
+ let content;
121
+ if (index === 0) {
122
+ const first = [...PROMPT_GRAPHEMES.segment(chunk.text)][0]?.segment ?? " ";
123
+ const remaining = chunk.text.slice(first === " " && chunk.text.length === 0 ? 0 : first.length);
124
+ const marker = this.focused ? CURSOR_MARKER : "";
125
+ content = `${marker}${this.#styleSuggestionCaret(first)}${this.#styleSuggestion(remaining)}`;
126
+ }
127
+ else {
128
+ content = this.#styleSuggestion(chunk.text);
129
+ }
130
+ const plainWidth = visibleWidth(chunk.text);
131
+ const padding = " ".repeat(Math.max(0, contentWidth - plainWidth));
132
+ const prefix = index === 0 ? this.#promptPrefix : " ".repeat(prefixWidth);
133
+ return truncateToWidth(`${prefix}${leftPadding}${content}${padding}${rightPadding}`, width);
134
+ });
135
+ return [horizontal, ...rows, horizontal];
136
+ }
137
+ #renderPrefixedEditor(width) {
138
+ const prefixWidth = visibleWidth(this.#promptPrefix);
139
+ const innerWidth = Math.max(1, width - prefixWidth);
140
+ const rows = super.render(innerWidth);
141
+ const maxPadding = Math.max(0, Math.floor((innerWidth - 1) / 2));
142
+ const paddingX = Math.min(this.getPaddingX(), maxPadding);
143
+ const contentWidth = Math.max(1, innerWidth - paddingX * 2);
144
+ const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1));
145
+ const layoutCount = this.getLines().flatMap(line => wrapTextWithAnsi(line, layoutWidth)).length || 1;
146
+ const visibleCount = Math.min(layoutCount, Math.max(5, Math.floor(this.#terminalRows() * 0.3)));
147
+ const bottomBorder = visibleCount + 1;
148
+ return rows.map((row, index) => {
149
+ if (index === 0 || index === bottomBorder)
150
+ return `${row}${this.borderColor("─".repeat(prefixWidth))}`;
151
+ return `${index === 1 ? this.#promptPrefix : " ".repeat(prefixWidth)}${row}`;
152
+ });
153
+ }
55
154
  }
@@ -1,6 +1,6 @@
1
1
  import type { PiSessionForkPrompt, PiSessionSelection } from "./session-selection.js";
2
2
  import { type AgentSessionRuntime, type AgentSessionServices, type SessionInfo } from "@earendil-works/pi-coding-agent";
3
- import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, type OwnedUiCommand, type OwnedUiCommandOutcome, type OwnedUiImageAttachment, type OwnedUiEvent, type OwnedUiSessionViewModel, type OwnedUiSnapshot } from "../../../contracts/owned-ui/index.js";
3
+ import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, type OwnedUiCommand, type OwnedUiCommandOutcome, type OwnedUiImageAttachment, type OwnedUiPromptSuggestionGeneratorPort, type OwnedUiPromptSuggestionRequest, type OwnedUiPromptSuggestionResult, type OwnedUiEvent, type OwnedUiSessionViewModel, type OwnedUiSnapshot } from "../../../contracts/owned-ui/index.js";
4
4
  import { type PiAuthenticationProviderOption, type PiBashWorkflowResult, type PiPinnedSettingsCallback, type PiPinnedSettingsSnapshot, type PiWorkflowAutocompleteCommand, type PiWorkflowHost, type PiWorkflowInteractionHost, type PiWorkflowOption, type PiWorkflowRequest, type PiWorkflowResult } from "./workflows.js";
5
5
  import { PiSettingsIntegration } from "./settings-integration.js";
6
6
  import type { PiSettingOwnerHandlers } from "./settings-effects.js";
@@ -127,7 +127,7 @@ export interface AdapterCommandResult {
127
127
  readonly diagnostic: string | null;
128
128
  }
129
129
  /** Owns the pinned Pi session lifecycle and translates its events into the neutral agent-engine contract. */
130
- export declare class PiEngineAdapter {
130
+ export declare class PiEngineAdapter implements OwnedUiPromptSuggestionGeneratorPort {
131
131
  #private;
132
132
  constructor(options?: PiEngineAdapterOptions);
133
133
  setWorkflowInteractionHost(interaction: PiWorkflowInteractionHost): void;
@@ -139,6 +139,7 @@ export declare class PiEngineAdapter {
139
139
  currentSessionFile(): string | null;
140
140
  currentSessionResumeMetadata(): PiSessionResumeMetadata | null;
141
141
  get disposed(): boolean;
142
+ generate(request: OwnedUiPromptSuggestionRequest): Promise<OwnedUiPromptSuggestionResult>;
142
143
  start(): Promise<OwnedUiSessionViewModel>;
143
144
  onEvent(listener: (event: OwnedUiEvent) => void): () => void;
144
145
  flushEvents(): Promise<void>;