@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
@@ -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>;
@@ -8,7 +8,7 @@ import { promisify } from "node:util";
8
8
  import { PRODUCT_IDENTITY } from "../../../product-identity.js";
9
9
  import { configureOwnedHttpDispatcher } from "./http-dispatcher.js";
10
10
  import { copyToClipboard, CredentialSynchronizationError, DefaultPackageManager, getAgentDir, ProjectTrustStore, SessionManager, VERSION, } from "@earendil-works/pi-coding-agent";
11
- import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, assertOwnedUiCommand, assertOwnedUiExtensionUiPort, assertOwnedUiSnapshot, } from "../../../contracts/owned-ui/index.js";
11
+ import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, assertOwnedUiCommand, assertOwnedUiExtensionUiPort, assertOwnedUiPromptSuggestionRequest, assertOwnedUiPromptSuggestionResult, assertOwnedUiSnapshot, normalizePromptSuggestionCandidate, } from "../../../contracts/owned-ui/index.js";
12
12
  import { PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "./workflows.js";
13
13
  import { createPiRuntimeIntegration } from "./runtime-integration.js";
14
14
  import { PiSessionCommandIntegration } from "./session-integration.js";
@@ -129,6 +129,8 @@ export class PiEngineAdapter {
129
129
  #eventQueueProcessing;
130
130
  #droppedEventCount = 0;
131
131
  #agentRunActive = false;
132
+ #agentRunSequence = 0;
133
+ #assistantResponseSequence = 0;
132
134
  #statusKind = null;
133
135
  #sessionCommands;
134
136
  #gitBranch = null;
@@ -200,6 +202,69 @@ export class PiEngineAdapter {
200
202
  get disposed() {
201
203
  return this.#disposed;
202
204
  }
205
+ async generate(request) {
206
+ assertOwnedUiPromptSuggestionRequest(request);
207
+ const identity = request.identity;
208
+ const session = this.#session;
209
+ const runtime = this.#runtime;
210
+ const activeModel = this.#activeModel;
211
+ if (this.#disposed || session === undefined || runtime === undefined || request.signal.aborted
212
+ || identity.sessionId !== this.#sessionId
213
+ || identity.sessionGeneration !== this.#sessionGeneration
214
+ || identity.runSequence !== this.#agentRunSequence
215
+ || identity.responseSequence !== this.#assistantResponseSequence
216
+ || activeModel === null
217
+ || identity.model.providerId !== activeModel.providerId
218
+ || identity.model.modelId !== activeModel.modelId) {
219
+ return { identity, text: null };
220
+ }
221
+ const model = session.model;
222
+ const agentState = session.agent.state;
223
+ if (model === undefined || typeof runtime.services.modelRuntime.completeSimple !== "function") {
224
+ return { identity, text: null };
225
+ }
226
+ const messages = agentState.messages.filter(message => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
227
+ const reasoning = session.thinkingLevel === "minimal"
228
+ || session.thinkingLevel === "low"
229
+ || session.thinkingLevel === "medium"
230
+ || session.thinkingLevel === "high"
231
+ || session.thinkingLevel === "xhigh"
232
+ || session.thinkingLevel === "max"
233
+ ? session.thinkingLevel
234
+ : undefined;
235
+ const response = await runtime.services.modelRuntime.completeSimple(model, {
236
+ systemPrompt: agentState.systemPrompt,
237
+ messages: [
238
+ ...messages,
239
+ { role: "user", content: CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, timestamp: Date.now() },
240
+ ],
241
+ tools: agentState.tools,
242
+ }, {
243
+ signal: request.signal,
244
+ ...(reasoning === undefined ? {} : { reasoning }),
245
+ });
246
+ if (isRecord(response)
247
+ && (stringValue(response.errorMessage) !== undefined
248
+ || stringValue(response.stopReason) === "error"
249
+ || stringValue(response.stopReason) === "aborted")) {
250
+ const result = { identity, text: null };
251
+ assertOwnedUiPromptSuggestionResult(result);
252
+ return result;
253
+ }
254
+ const content = Array.isArray(response.content) ? response.content : [];
255
+ if (content.some(block => isRecord(block) && block.type === "toolCall")) {
256
+ const result = { identity, text: null };
257
+ assertOwnedUiPromptSuggestionResult(result);
258
+ return result;
259
+ }
260
+ const textBlock = content.find(block => isRecord(block) && block.type === "text" && typeof block.text === "string");
261
+ const result = {
262
+ identity,
263
+ text: normalizePromptSuggestionCandidate(isRecord(textBlock) && typeof textBlock.text === "string" ? textBlock.text : null),
264
+ };
265
+ assertOwnedUiPromptSuggestionResult(result);
266
+ return result;
267
+ }
203
268
  async start() {
204
269
  if (this.#runtime)
205
270
  return this.view();
@@ -1773,6 +1838,8 @@ export class PiEngineAdapter {
1773
1838
  this.#status = { ...this.#status, workingMessage: null, badges: [] };
1774
1839
  this.#statusKind = null;
1775
1840
  this.#agentRunActive = false;
1841
+ this.#agentRunSequence = 0;
1842
+ this.#assistantResponseSequence = 0;
1776
1843
  this.#activeModel = readModel(session.model);
1777
1844
  this.#reconcileActiveModelAvailability();
1778
1845
  this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
@@ -1880,6 +1947,7 @@ export class PiEngineAdapter {
1880
1947
  switch (event.type) {
1881
1948
  case "agent_start":
1882
1949
  this.#agentRunActive = true;
1950
+ this.#agentRunSequence += 1;
1883
1951
  this.#emitEvent({ type: "agent-run-started" });
1884
1952
  this.#enterWorkState("working", "Working");
1885
1953
  return;
@@ -1906,7 +1974,26 @@ export class PiEngineAdapter {
1906
1974
  // finalization is intentionally not a substitute: rebuilds, retries,
1907
1975
  // thinking parts, and tool rows can all finalize independently.
1908
1976
  if (isRecord(event.message) && event.message.role === "assistant") {
1909
- this.#emitEvent({ type: "assistant-message-completed" });
1977
+ this.#assistantResponseSequence += 1;
1978
+ const content = Array.isArray(event.message.content) ? event.message.content : [];
1979
+ const stopReason = stringValue(event.message.stopReason) ?? null;
1980
+ const toolContinuation = stopReason === "toolUse"
1981
+ || content.some(item => isRecord(item) && item.type === "toolCall");
1982
+ const successful = stringValue(event.message.errorMessage) === undefined
1983
+ && stopReason !== "error"
1984
+ && stopReason !== "aborted"
1985
+ && textFromContent(content).trim().length > 0;
1986
+ this.#emitEvent({
1987
+ type: "assistant-message-completed",
1988
+ sessionGeneration: this.#sessionGeneration,
1989
+ runSequence: this.#agentRunSequence,
1990
+ responseSequence: this.#assistantResponseSequence,
1991
+ model: this.#activeModel,
1992
+ assistantMessageCount: this.#transcript.filter(block => block.kind === "assistant").length,
1993
+ successful,
1994
+ stopReason,
1995
+ toolContinuation,
1996
+ });
1910
1997
  }
1911
1998
  return;
1912
1999
  case "turn_end":
@@ -1951,6 +2038,23 @@ export class PiEngineAdapter {
1951
2038
  this.#leaveWorkStates();
1952
2039
  }
1953
2040
  this.#emitView();
2041
+ if (event.type === "agent_settled") {
2042
+ const assistants = finalMessages.filter(message => isRecord(message) && message.role === "assistant");
2043
+ const lastAssistant = assistants.at(-1);
2044
+ const successful = lastAssistant !== undefined
2045
+ && stringValue(lastAssistant.errorMessage) === undefined
2046
+ && stringValue(lastAssistant.stopReason) !== "error"
2047
+ && stringValue(lastAssistant.stopReason) !== "aborted";
2048
+ this.#emitEvent({
2049
+ type: "agent-run-settled",
2050
+ sessionGeneration: this.#sessionGeneration,
2051
+ runSequence: this.#agentRunSequence,
2052
+ responseSequence: this.#assistantResponseSequence,
2053
+ model: this.#activeModel,
2054
+ assistantMessageCount: assistants.length,
2055
+ successful,
2056
+ });
2057
+ }
1954
2058
  return;
1955
2059
  }
1956
2060
  case "queue_update": {
@@ -7,7 +7,7 @@ export declare const REQUIRED_PI_CAPABILITY_OPERATIONS: Readonly<{
7
7
  readonly "public-exports": readonly ["services.create", "session.create", "runtime.create"];
8
8
  readonly "session-lifecycle": readonly ["session.new", "session.resume", "session.rebind", "session.dispose"];
9
9
  readonly "commands-events": readonly ["prompt", "steer", "followUp", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"];
10
- readonly "models-authentication": readonly ["models.list", "models.refresh", "auth.status", "auth.login", "auth.logout", "auth.cancel"];
10
+ readonly "models-authentication": readonly ["models.list", "models.refresh", "models.completeSimple", "auth.status", "auth.login", "auth.logout", "auth.cancel"];
11
11
  readonly settings: readonly ["settings.read", "settings.write", "settings.flush"];
12
12
  readonly "resources-extensions": readonly ["resources.discover", "extensions.inline", "extensions.bind", "extensions.reload", "renderers.invoke"];
13
13
  readonly workflows: readonly ["workflow.route", "workflow.validate", "workflow.diagnostics"];
@@ -9,7 +9,7 @@ export const REQUIRED_PI_CAPABILITY_OPERATIONS = Object.freeze({
9
9
  "public-exports": ["services.create", "session.create", "runtime.create"],
10
10
  "session-lifecycle": ["session.new", "session.resume", "session.rebind", "session.dispose"],
11
11
  "commands-events": ["prompt", "steer", "followUp", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"],
12
- "models-authentication": ["models.list", "models.refresh", "auth.status", "auth.login", "auth.logout", "auth.cancel"],
12
+ "models-authentication": ["models.list", "models.refresh", "models.completeSimple", "auth.status", "auth.login", "auth.logout", "auth.cancel"],
13
13
  settings: ["settings.read", "settings.write", "settings.flush"],
14
14
  "resources-extensions": ["resources.discover", "extensions.inline", "extensions.bind", "extensions.reload", "renderers.invoke"],
15
15
  workflows: ["workflow.route", "workflow.validate", "workflow.diagnostics"],
@@ -86,7 +86,7 @@ export async function runPiUpgradeConformance() {
86
86
  const session = created.session;
87
87
  sessionId = session.sessionId;
88
88
  requireMethods(session, "session commands", ["prompt", "steer", "followUp", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"]);
89
- requireMethods(services.modelRuntime, "models/authentication", ["getModels", "getModel", "checkAuth", "login", "logout", "refresh"]);
89
+ requireMethods(services.modelRuntime, "models/authentication", ["getModels", "getModel", "completeSimple", "checkAuth", "login", "logout", "refresh"]);
90
90
  requireMethods(services.settingsManager, "settings", ["getGlobalSettings", "getProjectSettings", "flush"]);
91
91
  requireMethods(services.resourceLoader, "resources/extensions", ["getExtensions", "getSkills", "getPrompts", "getThemes", "reload"]);
92
92
  const unsubscribe = session.subscribe(() => undefined);
@@ -17,5 +17,5 @@ export declare class PiWorkflowControllerPort implements AgentWorkflowPort {
17
17
  listWorkflows(): Promise<readonly AgentWorkflowDescriptor[]>;
18
18
  executeWorkflow(workflowId: string, input: AgentJsonValue, signal?: AbortSignal): Promise<AgentJsonValue>;
19
19
  }
20
- export declare const PI_BUILTIN_WORKFLOW_ROUTES: readonly ("name" | "share" | "model" | "login" | "logout" | "settings" | "reload" | "compact" | "new" | "resume" | "session" | "scoped-models" | "export" | "import" | "copy" | "changelog" | "hotkeys" | "fork" | "clone" | "tree" | "trust" | "quit" | "debug" | "arminsayshi" | "dementedelves")[];
20
+ export declare const PI_BUILTIN_WORKFLOW_ROUTES: readonly ("name" | "share" | "model" | "login" | "logout" | "settings" | "reload" | "compact" | "new" | "resume" | "quit" | "session" | "scoped-models" | "export" | "import" | "copy" | "changelog" | "hotkeys" | "fork" | "clone" | "tree" | "trust" | "debug" | "arminsayshi" | "dementedelves")[];
21
21
  export declare const PI_WORKFLOW_CAPABILITIES: readonly WorkflowCapability[];
@@ -1,3 +1,4 @@
1
+ export * from "./prompt-suggestion-controller.js";
1
2
  export * from "./session-shell.js";
2
3
  export * from "./stream-presentation-coalescer.js";
3
4
  export * from "./route-host.js";
@@ -1,3 +1,4 @@
1
+ export * from "./prompt-suggestion-controller.js";
1
2
  export * from "./session-shell.js";
2
3
  export * from "./stream-presentation-coalescer.js";
3
4
  export * from "./route-host.js";
@@ -0,0 +1,27 @@
1
+ import { type OwnedUiPromptSuggestionGeneratorPort, type OwnedUiPromptSuggestionIdentity, type OwnedUiPromptSuggestionState } from "../../../contracts/owned-ui/index.js";
2
+ export interface ContextualPromptSuggestionSurface {
3
+ canPresent(identity: OwnedUiPromptSuggestionIdentity): boolean;
4
+ present(text: string): boolean;
5
+ clear(): void;
6
+ requestRender(): void;
7
+ }
8
+ export interface ContextualPromptSuggestionControllerOptions {
9
+ readonly generator: OwnedUiPromptSuggestionGeneratorPort;
10
+ readonly surface: ContextualPromptSuggestionSurface;
11
+ readonly enabled: boolean;
12
+ readonly timeoutMs?: number;
13
+ }
14
+ /** Prefetches one candidate, holds it invisibly, and publishes only after matching settlement. */
15
+ export declare class ContextualPromptSuggestionController {
16
+ #private;
17
+ readonly options: ContextualPromptSuggestionControllerOptions;
18
+ constructor(options: ContextualPromptSuggestionControllerOptions);
19
+ get state(): OwnedUiPromptSuggestionState;
20
+ setEnabled(enabled: boolean): void;
21
+ consider(identity: OwnedUiPromptSuggestionIdentity, eligible: boolean): void;
22
+ settle(identity: OwnedUiPromptSuggestionIdentity): void;
23
+ accept(): void;
24
+ invalidate(): void;
25
+ dispose(): void;
26
+ }
27
+ export declare function samePromptSuggestionIdentity(left: OwnedUiPromptSuggestionIdentity, right: OwnedUiPromptSuggestionIdentity): boolean;
@@ -0,0 +1,132 @@
1
+ import { assertOwnedUiPromptSuggestionResult, normalizePromptSuggestionCandidate, } from "../../../contracts/owned-ui/index.js";
2
+ /** Prefetches one candidate, holds it invisibly, and publishes only after matching settlement. */
3
+ export class ContextualPromptSuggestionController {
4
+ options;
5
+ #state = { status: "idle" };
6
+ #epoch = 0;
7
+ #abort = null;
8
+ #lastConsidered = "";
9
+ #enabled;
10
+ #disposed = false;
11
+ #timeoutMs;
12
+ constructor(options) {
13
+ this.options = options;
14
+ this.#enabled = options.enabled;
15
+ this.#timeoutMs = options.timeoutMs ?? 15_000;
16
+ }
17
+ get state() { return this.#state; }
18
+ setEnabled(enabled) {
19
+ if (this.#enabled === enabled)
20
+ return;
21
+ this.#enabled = enabled;
22
+ this.invalidate();
23
+ }
24
+ consider(identity, eligible) {
25
+ const key = identityKey(identity);
26
+ if (key === this.#lastConsidered)
27
+ return;
28
+ this.#lastConsidered = key;
29
+ this.invalidate();
30
+ if (!this.#enabled || this.#disposed || !eligible)
31
+ return;
32
+ const epoch = this.#epoch;
33
+ const abort = new AbortController();
34
+ this.#abort = abort;
35
+ this.#state = { status: "generating", identity, settled: false };
36
+ const timeout = setTimeout(() => {
37
+ abort.abort();
38
+ if (this.#epoch === epoch && this.#state.status === "generating") {
39
+ this.#abort = null;
40
+ this.#state = { status: "idle" };
41
+ }
42
+ }, this.#timeoutMs);
43
+ timeout.unref?.();
44
+ void this.options.generator.generate({ identity, signal: abort.signal })
45
+ .then(result => this.#receive(epoch, identity, result))
46
+ .catch(() => this.#fail(epoch))
47
+ .finally(() => {
48
+ clearTimeout(timeout);
49
+ if (this.#epoch === epoch)
50
+ this.#abort = null;
51
+ });
52
+ }
53
+ settle(identity) {
54
+ if (this.#disposed || !this.#enabled || this.#state.status === "idle")
55
+ return;
56
+ if (!samePromptSuggestionIdentity(this.#state.identity, identity)) {
57
+ this.invalidate();
58
+ return;
59
+ }
60
+ if (this.#state.status === "generating") {
61
+ this.#state = { ...this.#state, settled: true };
62
+ return;
63
+ }
64
+ if (this.#state.status === "prepared")
65
+ this.#show(identity, this.#state.text);
66
+ }
67
+ accept() {
68
+ this.#invalidate(false);
69
+ }
70
+ invalidate() {
71
+ this.#invalidate(true);
72
+ }
73
+ dispose() {
74
+ this.#disposed = true;
75
+ this.invalidate();
76
+ }
77
+ #invalidate(clearSurface) {
78
+ const hadVisibleSuggestion = this.#state.status === "available";
79
+ this.#epoch += 1;
80
+ this.#abort?.abort();
81
+ this.#abort = null;
82
+ this.#state = { status: "idle" };
83
+ if (clearSurface && hadVisibleSuggestion) {
84
+ this.options.surface.clear();
85
+ this.options.surface.requestRender();
86
+ }
87
+ }
88
+ #fail(epoch) {
89
+ if (this.#epoch !== epoch)
90
+ return;
91
+ this.#abort = null;
92
+ this.#state = { status: "idle" };
93
+ }
94
+ #receive(epoch, identity, result) {
95
+ assertOwnedUiPromptSuggestionResult(result);
96
+ if (this.#disposed || !this.#enabled || this.#epoch !== epoch || this.#state.status !== "generating")
97
+ return;
98
+ if (!samePromptSuggestionIdentity(identity, result.identity)) {
99
+ this.invalidate();
100
+ return;
101
+ }
102
+ const text = normalizePromptSuggestionCandidate(result.text);
103
+ if (text === null) {
104
+ this.#fail(epoch);
105
+ return;
106
+ }
107
+ this.#abort = null;
108
+ if (this.#state.settled)
109
+ this.#show(identity, text);
110
+ else
111
+ this.#state = { status: "prepared", identity, text };
112
+ }
113
+ #show(identity, text) {
114
+ if (!this.options.surface.canPresent(identity) || !this.options.surface.present(text)) {
115
+ this.invalidate();
116
+ return;
117
+ }
118
+ this.#state = { status: "available", identity, text };
119
+ this.options.surface.requestRender();
120
+ }
121
+ }
122
+ export function samePromptSuggestionIdentity(left, right) {
123
+ return left.sessionId === right.sessionId
124
+ && left.sessionGeneration === right.sessionGeneration
125
+ && left.runSequence === right.runSequence
126
+ && left.responseSequence === right.responseSequence
127
+ && left.model.providerId === right.model.providerId
128
+ && left.model.modelId === right.model.modelId;
129
+ }
130
+ function identityKey(identity) {
131
+ return `${identity.sessionId}\0${identity.sessionGeneration}\0${identity.runSequence}\0${identity.responseSequence}\0${identity.model.providerId}\0${identity.model.modelId}`;
132
+ }
@@ -1,4 +1,4 @@
1
- import type { OwnedUiSessionViewModel, OwnedUiViewportSettings, OwnedUiViewportSettingsPort } from "../../../contracts/owned-ui/index.js";
1
+ import type { OwnedUiPromptSuggestionGeneratorPort, OwnedUiSessionViewModel, OwnedUiViewportSettings, OwnedUiViewportSettingsPort } from "../../../contracts/owned-ui/index.js";
2
2
  import type { UiRouteHost } from "../../../ui/apps/index.js";
3
3
  import { type TranscriptViewportFrame, type TranscriptViewportFrameDescriptor } from "../../../ui/components/index.js";
4
4
  import { type PiEngineAdapter, type PiWorkflowMessage, type PiWorkflowResult } from "../engine/index.js";
@@ -39,6 +39,11 @@ export interface OwnedUiSessionShellOptions {
39
39
  readonly scheduler?: StreamPresentationScheduler;
40
40
  };
41
41
  /** Optional deterministic seam for keyboard scheduling and phase evidence. */
42
+ readonly promptSuggestions?: {
43
+ readonly generator: OwnedUiPromptSuggestionGeneratorPort;
44
+ readonly enabled: () => boolean;
45
+ readonly onChange: (listener: (enabled: boolean) => void) => () => void;
46
+ };
42
47
  readonly inputPresentation?: {
43
48
  readonly scheduler?: PiTuiInputCoordinationScheduler;
44
49
  readonly onEvent?: (event: PiTuiInputDiagnosticsEvent) => void;
@@ -73,6 +78,9 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
73
78
  readonly onMessageCopy?: () => void;
74
79
  readonly onFollowUp?: () => void;
75
80
  readonly onDequeue?: () => void;
81
+ readonly onEditorChange?: (text: string) => void;
82
+ readonly onPromptSuggestionAccepted?: (text: string) => void;
83
+ readonly onInputSurfaceChanged?: () => void;
76
84
  readonly onCopyText?: (text: string) => void;
77
85
  readonly readClipboardContent?: () => Promise<PiShellClipboardContent | null>;
78
86
  }, startup?: PiShellHeaderOptions, agentDir?: string, extensionRenderers?: PiShellExtensionRendererResolver, sessionLayout?: "pinned" | "custom-viewport", imageAssets?: PiShellImageAssetResolver);
@@ -83,6 +91,9 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
83
91
  setMermaidRenderingMode(mode: "off" | "final" | "streaming"): void;
84
92
  setImagePresentation(showImages: boolean, imageWidthCells: number): void;
85
93
  preparePromptSubmission(text: string): PreparedPrompt;
94
+ canPreparePromptSuggestion(): boolean;
95
+ canPresentPromptSuggestion(): boolean;
96
+ setPromptSuggestion(text: string | null): void;
86
97
  update(view: OwnedUiSessionViewModel): void;
87
98
  /**
88
99
  * Applies one block: its component is created or updated in place and the order grows
@@ -1,4 +1,4 @@
1
- import { backgroundSgrSpan, composeSubmittedPromptRows, displayWidth, formatSubmittedPromptTime, heldNativeHyperlinkStyle, hyperlinkSgrSpan, nativeHyperlinkStyle, overlaySpan, progressStatusText, submittedPromptLayout, stripAnsi, } from "../../../ui/components/index.js";
1
+ import { PROMPT_GLYPH, backgroundSgrSpan, caretCell, composeSubmittedPromptRows, displayWidth, faint, formatSubmittedPromptTime, heldNativeHyperlinkStyle, hyperlinkSgrSpan, nativeHyperlinkStyle, overlaySpan, progressStatusText, submittedPromptLayout, stripAnsi, } from "../../../ui/components/index.js";
2
2
  import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../engine/index.js";
3
3
  import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piShellVisibleWidth, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
4
4
  import { PiTuiRuntimeAdapter, classifyPiTuiInput, } from "../tui-runtime/index.js";
@@ -29,6 +29,7 @@ export class OwnedUiSessionShellRoot {
29
29
  #viewportController;
30
30
  #viewportTheme;
31
31
  #onViewportFrame;
32
+ #onInputSurfaceChanged;
32
33
  #componentRuntime;
33
34
  #toolsExpanded = false;
34
35
  #thinkingVisible = true;
@@ -73,6 +74,7 @@ export class OwnedUiSessionShellRoot {
73
74
  this.#imageAssets = imageAssets;
74
75
  this.#componentRuntime = handlers;
75
76
  this.#onViewportFrame = handlers.onViewportFrame;
77
+ this.#onInputSurfaceChanged = handlers.onInputSurfaceChanged;
76
78
  this.#dockInputReuseEnabled = handlers.enableDockInputReuse ?? true;
77
79
  this.header = createPiShellHeader(startup);
78
80
  this.resources = createPiShellLoadedResources(startup.resources ?? [], startup.expanded ?? false);
@@ -107,6 +109,15 @@ export class OwnedUiSessionShellRoot {
107
109
  cwd,
108
110
  ...(agentDir === undefined ? {} : { agentDir }),
109
111
  onToolsExpand: () => this.#setToolsExpanded(!this.#toolsExpanded),
112
+ ...(this.#customViewport ? {
113
+ promptPresentation: {
114
+ prefix: PROMPT_GLYPH,
115
+ styleSuggestion: faint,
116
+ styleSuggestionCaret: caretCell,
117
+ },
118
+ } : {}),
119
+ ...(handlers.onEditorChange === undefined ? {} : { onChange: handlers.onEditorChange }),
120
+ ...(handlers.onPromptSuggestionAccepted === undefined ? {} : { onPromptSuggestionAccepted: handlers.onPromptSuggestionAccepted }),
110
121
  });
111
122
  this.#viewportController = new SessionViewportController({
112
123
  enabled: this.#customViewport,
@@ -183,6 +194,20 @@ export class OwnedUiSessionShellRoot {
183
194
  preparePromptSubmission(text) {
184
195
  return this.#promptChips.prepareSubmission(text);
185
196
  }
197
+ canPreparePromptSuggestion() {
198
+ return this.usesDefaultInputSurface()
199
+ && this.#view.dialog === null
200
+ && this.#view.overlay === null
201
+ && this.editor.getText().length === 0;
202
+ }
203
+ canPresentPromptSuggestion() {
204
+ return this.canPreparePromptSuggestion()
205
+ && this.#view.lifecycle === "ready"
206
+ && this.editor.canPresentPromptSuggestion();
207
+ }
208
+ setPromptSuggestion(text) {
209
+ this.editor.setPromptSuggestion(text);
210
+ }
186
211
  update(view) {
187
212
  if (view.diagnostics.length !== this.#view.diagnostics.length
188
213
  || view.diagnostics.some((diagnostic, index) => diagnostic.sequence !== this.#view.diagnostics[index]?.sequence)) {
@@ -769,6 +794,7 @@ export class OwnedUiSessionShellRoot {
769
794
  this.#inputSurface.dispose?.();
770
795
  this.#inputSurface = next;
771
796
  this.#inputSurfaceCoordination = nextCoordination;
797
+ this.#onInputSurfaceChanged?.();
772
798
  this.#inputSurface.setFocused?.(true);
773
799
  this.invalidate();
774
800
  }