@timurproko/a1 0.1.8-dev.269 → 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 (36) 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 +66 -11
  11. package/dist/integrations/pi/components/path-word-ranges.d.ts +2 -0
  12. package/dist/integrations/pi/components/path-word-ranges.js +37 -0
  13. package/dist/integrations/pi/components/shell-editor-autocomplete.js +15 -0
  14. package/dist/integrations/pi/components/shell-shared-facade.d.ts +8 -0
  15. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +15 -2
  16. package/dist/integrations/pi/components/upstream/components/owned-editor.js +102 -3
  17. package/dist/integrations/pi/engine/adapter.d.ts +3 -2
  18. package/dist/integrations/pi/engine/adapter.js +106 -2
  19. package/dist/integrations/pi/engine/conformance.d.ts +1 -1
  20. package/dist/integrations/pi/engine/conformance.js +2 -2
  21. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  22. package/dist/integrations/pi/session-ui/index.d.ts +1 -0
  23. package/dist/integrations/pi/session-ui/index.js +1 -0
  24. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +27 -0
  25. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +132 -0
  26. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +12 -1
  27. package/dist/integrations/pi/session-ui/session-shell-root.js +27 -1
  28. package/dist/integrations/pi/session-ui/session-shell.js +86 -2
  29. package/dist/native/darwin-arm64/manifest.json +1 -1
  30. package/dist/native/linux-x64/manifest.json +1 -1
  31. package/dist/native/win32-x64/manifest.json +2 -2
  32. package/dist/native/win32-x64/process-guardian.exe +0 -0
  33. package/dist/ui/settings/declarations.d.ts +1 -1
  34. package/dist/ui/settings/declarations.js +9 -1
  35. package/dist/ui/settings/migrations.js +7 -0
  36. 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;
@@ -1,4 +1,5 @@
1
1
  import { CURSOR_MARKER, decodeKittyPrintable, visibleWidth, } from "#pi-tui";
2
+ import { promptPathWordRanges } from "./path-word-ranges.js";
2
3
  export class OwnedEditorUxInterception {
3
4
  interceptors;
4
5
  fallback;
@@ -59,12 +60,13 @@ class PromptSelectionInterceptor {
59
60
  #lastClick;
60
61
  #redoStack = [];
61
62
  #selectionRevision = 0;
63
+ #wordDirection;
62
64
  #geometry;
63
65
  constructor(editor, keybindings, options) {
64
66
  this.editor = editor;
65
67
  this.keybindings = keybindings;
66
68
  this.options = options;
67
- installAtomicSegmentation(editor, options.atomicRanges);
69
+ installAtomicSegmentation(editor, options.atomicRanges, () => this.#wordDirection);
68
70
  }
69
71
  handleInput(data, next) {
70
72
  if (this.keybindings.matches(data, "owned.editor.selectAll")) {
@@ -167,24 +169,30 @@ class PromptSelectionInterceptor {
167
169
  if (this.keybindings.matches(data, "tui.editor.cursorWordLeft")) {
168
170
  const before = this.#cursor();
169
171
  const startedWithAtomicFocus = this.#atomicFocus() !== undefined;
170
- next();
172
+ this.#delegateWord(-1, next);
171
173
  let after = this.#cursor();
172
174
  const landedAtomic = this.#atomicRangeAt(after);
173
175
  if (!startedWithAtomicFocus && landedAtomic?.start === after.col && after.col > 0) {
174
176
  const line = editorState(this.editor).lines[after.line] ?? "";
175
177
  const previous = [...GRAPHEMES.segment(line.slice(0, after.col))].at(-1);
176
178
  if (previous !== undefined && !/^\s+$/u.test(previous.segment)) {
177
- next();
179
+ this.#delegateWord(-1, next);
178
180
  after = this.#cursor();
179
181
  }
180
182
  }
181
- if (!samePosition(before, after))
183
+ const line = editorState(this.editor).lines[after.line] ?? "";
184
+ const landedPath = promptPathWordRanges(line).some(range => range.start === after.col);
185
+ if (!samePosition(before, after) && !landedPath)
182
186
  this.#moveOntoPreviousSeparator();
183
187
  this.#requestRender();
184
188
  return;
185
189
  }
186
190
  const beforeText = this.editor.getText();
187
- next();
191
+ const wordDirection = this.#wordDirectionFor(data);
192
+ if (wordDirection === undefined)
193
+ next();
194
+ else
195
+ this.#delegateWord(wordDirection, next);
188
196
  if (this.editor.getText() !== beforeText)
189
197
  this.#redoStack = [];
190
198
  }
@@ -192,7 +200,9 @@ class PromptSelectionInterceptor {
192
200
  const rows = next().map(row => row.replaceAll(ATOMIC_SPACE_SENTINEL, " "));
193
201
  const maxPadding = Math.max(0, Math.floor((width - 1) / 2));
194
202
  const padding = Math.min(this.editor.getPaddingX(), maxPadding);
195
- 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);
196
206
  const layoutWidth = Math.max(1, contentWidth - (padding ? 0 : 1));
197
207
  const visualLines = editorVisualLineMap(this.editor, layoutWidth)
198
208
  ?? buildVisualLineMap(editorState(this.editor).lines, layoutWidth);
@@ -218,7 +228,7 @@ class PromptSelectionInterceptor {
218
228
  if (to <= from)
219
229
  continue;
220
230
  const line = editorState(this.editor).lines[visual.logicalLine] ?? "";
221
- const fromColumn = padding + visibleWidth(line.slice(segmentStart, from));
231
+ const fromColumn = prefixWidth + padding + visibleWidth(line.slice(segmentStart, from));
222
232
  const toColumn = fromColumn + visibleWidth(line.slice(from, to));
223
233
  const rendered = rows[row + 1];
224
234
  if (rendered !== undefined && toColumn > fromColumn) {
@@ -330,7 +340,7 @@ class PromptSelectionInterceptor {
330
340
  return undefined;
331
341
  const line = editorState(this.editor).lines[visual.logicalLine] ?? "";
332
342
  const segment = line.slice(visual.startCol, visual.startCol + visual.length);
333
- const displayColumn = Math.max(0, column - 1 - geometry.padding);
343
+ const displayColumn = Math.max(0, column - 1 - geometry.padding - (this.options.promptPrefixWidth ?? 0));
334
344
  return {
335
345
  line: visual.logicalLine,
336
346
  col: visual.startCol + indexAtDisplayWidth(segment, displayColumn),
@@ -584,11 +594,28 @@ class PromptSelectionInterceptor {
584
594
  #snapshot() {
585
595
  return { text: this.editor.getText(), cursor: this.#cursor() };
586
596
  }
597
+ #wordDirectionFor(data) {
598
+ if (this.keybindings.matches(data, "tui.editor.deleteWordBackward"))
599
+ return -1;
600
+ if (this.keybindings.matches(data, "tui.editor.deleteWordForward")
601
+ || this.keybindings.matches(data, "tui.editor.cursorWordRight"))
602
+ return 1;
603
+ return undefined;
604
+ }
605
+ #delegateWord(direction, next) {
606
+ this.#wordDirection = direction;
607
+ try {
608
+ next();
609
+ }
610
+ finally {
611
+ this.#wordDirection = undefined;
612
+ }
613
+ }
587
614
  #requestRender() {
588
615
  this.options.requestRender();
589
616
  }
590
617
  }
591
- function installAtomicSegmentation(editor, rangesForText) {
618
+ function installAtomicSegmentation(editor, rangesForText, wordDirection) {
592
619
  if (Reflect.get(editor, ATOMIC_SEGMENTATION) === true)
593
620
  return;
594
621
  const originalValue = Reflect.get(editor, "segment");
@@ -597,7 +624,14 @@ function installAtomicSegmentation(editor, rangesForText) {
597
624
  const original = originalValue.bind(editor);
598
625
  Reflect.set(editor, "segment", (text, mode) => {
599
626
  const segments = [...original(text, mode)].filter(isEditorSegment);
600
- const ranges = rangesForText(text);
627
+ const ranges = rangesForText(text).map(range => ({ ...range, wordLike: false }));
628
+ if (mode === "word") {
629
+ for (const range of contextualPathRanges(editor, text, wordDirection())) {
630
+ if (!ranges.some(existing => rangesOverlap(existing, range)))
631
+ ranges.push({ ...range, wordLike: true });
632
+ }
633
+ }
634
+ ranges.sort((left, right) => left.start - right.start);
601
635
  if (ranges.length === 0)
602
636
  return segments;
603
637
  const merged = [];
@@ -608,10 +642,12 @@ function installAtomicSegmentation(editor, rangesForText) {
608
642
  const range = ranges[rangeIndex];
609
643
  if (range !== undefined && segment.index >= range.start && segment.index < range.end) {
610
644
  if (segment.index === range.start) {
645
+ const source = text.slice(range.start, range.end);
611
646
  merged.push({
612
- segment: text.slice(range.start, range.end).replaceAll(" ", ATOMIC_SPACE_SENTINEL),
647
+ segment: range.wordLike ? "w".repeat(source.length) : source.replaceAll(" ", ATOMIC_SPACE_SENTINEL),
613
648
  index: range.start,
614
649
  input: text,
650
+ ...(range.wordLike ? { isWordLike: true } : {}),
615
651
  });
616
652
  }
617
653
  continue;
@@ -622,6 +658,25 @@ function installAtomicSegmentation(editor, rangesForText) {
622
658
  });
623
659
  Reflect.set(editor, ATOMIC_SEGMENTATION, true);
624
660
  }
661
+ function contextualPathRanges(editor, text, direction) {
662
+ if (direction === undefined)
663
+ return promptPathWordRanges(text);
664
+ const state = editorState(editor);
665
+ const line = state.lines[state.cursorLine] ?? "";
666
+ const offset = direction < 0 ? 0 : state.cursorCol;
667
+ if (text !== (direction < 0 ? line.slice(0, state.cursorCol) : line.slice(state.cursorCol))) {
668
+ return promptPathWordRanges(text);
669
+ }
670
+ const end = offset + text.length;
671
+ return promptPathWordRanges(line).flatMap(range => {
672
+ const start = Math.max(range.start, offset);
673
+ const finish = Math.min(range.end, end);
674
+ return finish <= start ? [] : [{ start: start - offset, end: finish - offset }];
675
+ });
676
+ }
677
+ function rangesOverlap(left, right) {
678
+ return left.start < right.end && right.start < left.end;
679
+ }
625
680
  function isEditorSegment(value) {
626
681
  if (typeof value !== "object" || value === null)
627
682
  return false;
@@ -0,0 +1,2 @@
1
+ import type { PiShellEditorTextRange } from "./shell-shared-facade.js";
2
+ export declare function promptPathWordRanges(line: string): readonly PiShellEditorTextRange[];
@@ -0,0 +1,37 @@
1
+ const WHITESPACE = /\s/u;
2
+ const DRIVE_ROOT = /^[A-Za-z]:[\\/]/u;
3
+ const UNC_ROOT = /^(?:\\\\|\/\/)[^\\/\s]+[\\/][^\\/\s]+/u;
4
+ const POSIX_ROOT = /^\//u;
5
+ const EXPLICIT_RELATIVE_ROOT = /^(?:\.{1,2}|~)[\\/]/u;
6
+ export function promptPathWordRanges(line) {
7
+ const ranges = [];
8
+ let index = 0;
9
+ while (index < line.length) {
10
+ while (index < line.length && WHITESPACE.test(line[index] ?? ""))
11
+ index += 1;
12
+ if (index >= line.length)
13
+ break;
14
+ const start = index;
15
+ const quote = line[index] === '"' || line[index] === "'" ? line[index] : undefined;
16
+ if (quote !== undefined) {
17
+ const closing = line.indexOf(quote, index + 1);
18
+ if (closing >= 0 && (closing + 1 === line.length || WHITESPACE.test(line[closing + 1] ?? ""))) {
19
+ if (isExplicitPath(line.slice(index + 1, closing)))
20
+ ranges.push({ start, end: closing + 1 });
21
+ index = closing + 1;
22
+ continue;
23
+ }
24
+ }
25
+ while (index < line.length && !WHITESPACE.test(line[index] ?? ""))
26
+ index += 1;
27
+ if (isExplicitPath(line.slice(start, index)))
28
+ ranges.push({ start, end: index });
29
+ }
30
+ return ranges;
31
+ }
32
+ function isExplicitPath(value) {
33
+ return DRIVE_ROOT.test(value)
34
+ || UNC_ROOT.test(value)
35
+ || POSIX_ROOT.test(value)
36
+ || EXPLICIT_RELATIVE_ROOT.test(value);
37
+ }
@@ -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
  }