@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.
- package/dist/composition/owned-ui.js +6 -0
- package/dist/contracts/owned-ui/index.d.ts +1 -0
- package/dist/contracts/owned-ui/index.js +1 -0
- package/dist/contracts/owned-ui/model.d.ts +52 -0
- package/dist/contracts/owned-ui/prompt-suggestions.d.ts +3 -0
- package/dist/contracts/owned-ui/prompt-suggestions.js +41 -0
- package/dist/contracts/owned-ui/validation.d.ts +5 -1
- package/dist/contracts/owned-ui/validation.js +67 -3
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -0
- package/dist/integrations/pi/components/owned-editor-ux.js +66 -11
- package/dist/integrations/pi/components/path-word-ranges.d.ts +2 -0
- package/dist/integrations/pi/components/path-word-ranges.js +37 -0
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +15 -0
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +8 -0
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +15 -2
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +102 -3
- package/dist/integrations/pi/engine/adapter.d.ts +3 -2
- package/dist/integrations/pi/engine/adapter.js +106 -2
- package/dist/integrations/pi/engine/conformance.d.ts +1 -1
- package/dist/integrations/pi/engine/conformance.js +2 -2
- package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
- package/dist/integrations/pi/session-ui/index.d.ts +1 -0
- package/dist/integrations/pi/session-ui/index.js +1 -0
- package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +27 -0
- package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +132 -0
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +12 -1
- package/dist/integrations/pi/session-ui/session-shell-root.js +27 -1
- package/dist/integrations/pi/session-ui/session-shell.js +86 -2
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/settings/declarations.d.ts +1 -1
- package/dist/ui/settings/declarations.js +9 -1
- package/dist/ui/settings/migrations.js +7 -0
- 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; },
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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,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
|
|
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
|
-
|
|
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
|
}
|