@giladbarnea/pi-user-agents 0.0.0
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/INTERNALS.md +65 -0
- package/PARSER_SPEC.md +264 -0
- package/README.md +127 -0
- package/agent-viewer.ts +653 -0
- package/autocomplete.ts +576 -0
- package/command-line.ts +746 -0
- package/context-meter.ts +41 -0
- package/editor-coloring.ts +160 -0
- package/index.ts +74 -0
- package/package.json +44 -0
- package/runner.ts +591 -0
- package/shared.ts +214 -0
- package/tool-render/FORK.md +108 -0
- package/tool-render/ansi.ts +77 -0
- package/tool-render/constants.ts +48 -0
- package/tool-render/diff.ts +957 -0
- package/tool-render/index.ts +137 -0
- package/tool-render/text.ts +212 -0
- package/tool-render/theme.ts +71 -0
- package/tool-render/tools.ts +502 -0
- package/transcript.ts +303 -0
- package/widget.ts +523 -0
package/autocomplete.ts
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CustomEditor,
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
type ExtensionContext,
|
|
5
|
+
type ModelRuntime,
|
|
6
|
+
resolveModelScopeWithDiagnostics,
|
|
7
|
+
SettingsManager,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import {
|
|
10
|
+
type AutocompleteItem,
|
|
11
|
+
type AutocompleteProvider,
|
|
12
|
+
type AutocompleteSuggestions,
|
|
13
|
+
decodeKittyPrintable,
|
|
14
|
+
type EditorComponent,
|
|
15
|
+
fuzzyFilter,
|
|
16
|
+
} from "@earendil-works/pi-tui";
|
|
17
|
+
import {
|
|
18
|
+
AGENT_OPTIONS,
|
|
19
|
+
analyzeAgentEditorInput,
|
|
20
|
+
type OptionCursorExpectation,
|
|
21
|
+
type ValueCursorExpectation,
|
|
22
|
+
} from "./command-line.js";
|
|
23
|
+
import { decorateColoringEditor } from "./editor-coloring.js";
|
|
24
|
+
import { createAgentValueValidator } from "./runner.js";
|
|
25
|
+
|
|
26
|
+
type EditorFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>>;
|
|
27
|
+
type CompletionModel = { provider: string; id: string; name?: string };
|
|
28
|
+
type CompletionTool = { name: string; description: string };
|
|
29
|
+
type CompletionResourceCommand = ReturnType<ExtensionAPI["getCommands"]>[number];
|
|
30
|
+
type ModelScopeSource = Pick<ExtensionContext["modelRegistry"], "getAvailable">;
|
|
31
|
+
type AutocompleteRuntimeEditor = EditorComponent & {
|
|
32
|
+
forceFileAutocomplete?: (explicitTab?: boolean) => void;
|
|
33
|
+
getCursor?: () => { line: number; col: number };
|
|
34
|
+
isShowingAutocomplete?: () => boolean;
|
|
35
|
+
tryTriggerAutocomplete?: (explicitTab?: boolean) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const DECORATED_EDITOR = Symbol.for("pi-user-agents:autocomplete-editor");
|
|
39
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
40
|
+
// Pi's resolver only calls getAvailable(), but its SDK type requires the internal ModelRuntime unavailable to extensions.
|
|
41
|
+
const resolveExtensionModelScope = resolveModelScopeWithDiagnostics as unknown as (
|
|
42
|
+
patterns: string[],
|
|
43
|
+
modelSource: ModelScopeSource,
|
|
44
|
+
) => ReturnType<typeof resolveModelScopeWithDiagnostics>;
|
|
45
|
+
|
|
46
|
+
type DecoratedEditor = AutocompleteRuntimeEditor & { [DECORATED_EDITOR]?: true };
|
|
47
|
+
|
|
48
|
+
/** @example getOptionItems(analyzeAgentEditorInput("/agent --thi", 12) as OptionCursorExpectation)[0]?.label // "--thinking <level>" */
|
|
49
|
+
export function getOptionItems(expectation: OptionCursorExpectation): AutocompleteItem[] {
|
|
50
|
+
return AGENT_OPTIONS.flatMap((option) => {
|
|
51
|
+
if (!option.autocomplete || expectation.usedSemanticIds.has(option.semanticId)) return [];
|
|
52
|
+
return option.names
|
|
53
|
+
.filter((name) => name.startsWith(expectation.fragment))
|
|
54
|
+
.map((name) => ({
|
|
55
|
+
value: name,
|
|
56
|
+
label: option.arity === "value" ? `${name} <${option.placeholder}>` : name,
|
|
57
|
+
description: option.description,
|
|
58
|
+
}));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createAgentAutocompleteProvider(
|
|
63
|
+
current: AutocompleteProvider,
|
|
64
|
+
getModels: () => readonly CompletionModel[] = () => [],
|
|
65
|
+
getTools: () => readonly CompletionTool[] = () => [],
|
|
66
|
+
getCommands: () => readonly CompletionResourceCommand[] = () => [],
|
|
67
|
+
getScopedModels: () => readonly CompletionModel[] = () => [],
|
|
68
|
+
): AutocompleteProvider {
|
|
69
|
+
return {
|
|
70
|
+
async getSuggestions(
|
|
71
|
+
lines,
|
|
72
|
+
cursorLine,
|
|
73
|
+
cursorCol,
|
|
74
|
+
options,
|
|
75
|
+
): Promise<AutocompleteSuggestions | null> {
|
|
76
|
+
if (cursorLine !== 0) return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
77
|
+
const expectation = analyzeAgentEditorInput(lines[0] ?? "", cursorCol);
|
|
78
|
+
if (expectation?.kind === "option") {
|
|
79
|
+
const items = getOptionItems(expectation);
|
|
80
|
+
return items.length === 0 ? null : { items, prefix: expectation.fragment };
|
|
81
|
+
}
|
|
82
|
+
if (expectation?.kind === "value" && expectation.option.completionDomain === "thinking") {
|
|
83
|
+
const items = THINKING_LEVELS.filter((level) => level.startsWith(expectation.fragment)).map(
|
|
84
|
+
(level) => ({ value: level, label: level }),
|
|
85
|
+
);
|
|
86
|
+
return items.length === 0 ? null : { items, prefix: expectation.fragment };
|
|
87
|
+
}
|
|
88
|
+
if (expectation?.kind === "value" && expectation.option.completionDomain === "model") {
|
|
89
|
+
const items = getModelItems(
|
|
90
|
+
expectation.fragment,
|
|
91
|
+
getModels(),
|
|
92
|
+
getScopedModels(),
|
|
93
|
+
expectation.parsedProvider,
|
|
94
|
+
);
|
|
95
|
+
return items.length === 0 ? null : { items, prefix: expectation.fragment };
|
|
96
|
+
}
|
|
97
|
+
if (expectation?.kind === "value" && expectation.option.completionDomain === "provider") {
|
|
98
|
+
const items = getProviderItems(expectation.fragment, getModels());
|
|
99
|
+
return items.length === 0 ? null : { items, prefix: expectation.fragment };
|
|
100
|
+
}
|
|
101
|
+
if (expectation?.kind === "value" && expectation.option.completionDomain === "tool") {
|
|
102
|
+
const segment = getToolSegment(expectation, lines[0] ?? "", cursorCol);
|
|
103
|
+
const items = getToolItems(segment.fragment, getTools(), segment.selectedNames);
|
|
104
|
+
return items.length === 0 ? null : { items, prefix: segment.fragment };
|
|
105
|
+
}
|
|
106
|
+
if (expectation?.kind === "value" && expectation.option.completionDomain === "path") {
|
|
107
|
+
const shortcuts = getResourceShortcutItems(expectation, getCommands());
|
|
108
|
+
if (shortcuts.length === 0)
|
|
109
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
110
|
+
const filesystem = await current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
111
|
+
return {
|
|
112
|
+
items: [...shortcuts, ...(filesystem?.items ?? [])],
|
|
113
|
+
prefix: filesystem?.prefix ?? expectation.fragment,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
120
|
+
if (cursorLine !== 0)
|
|
121
|
+
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
122
|
+
const expectation = analyzeAgentEditorInput(lines[0] ?? "", cursorCol);
|
|
123
|
+
if (
|
|
124
|
+
expectation?.kind === "option" &&
|
|
125
|
+
getOptionItems(expectation).some((candidate) => candidate.value === item.value)
|
|
126
|
+
) {
|
|
127
|
+
const option = AGENT_OPTIONS.find((candidate) => candidate.names.includes(item.value));
|
|
128
|
+
if (
|
|
129
|
+
option?.arity === "value" &&
|
|
130
|
+
(option.completionDomain === "path" || option.completionDomain === undefined)
|
|
131
|
+
) {
|
|
132
|
+
return applyGuardedOptionEdit(
|
|
133
|
+
lines,
|
|
134
|
+
expectation.replacementStart,
|
|
135
|
+
expectation.replacementEnd,
|
|
136
|
+
item.value,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return applyCompletionEdit(
|
|
140
|
+
lines,
|
|
141
|
+
expectation.replacementStart,
|
|
142
|
+
expectation.replacementEnd,
|
|
143
|
+
item.value,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const thinkingValue =
|
|
147
|
+
expectation?.kind === "value" &&
|
|
148
|
+
expectation.option.completionDomain === "thinking" &&
|
|
149
|
+
THINKING_LEVELS.some((level) => level === item.value);
|
|
150
|
+
if (thinkingValue && expectation.kind === "value") {
|
|
151
|
+
return applyCompletionEdit(
|
|
152
|
+
lines,
|
|
153
|
+
expectation.replacementStart,
|
|
154
|
+
expectation.replacementEnd,
|
|
155
|
+
item.value,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const modelValue =
|
|
159
|
+
expectation?.kind === "value" &&
|
|
160
|
+
expectation.option.completionDomain === "model" &&
|
|
161
|
+
getModelItems(
|
|
162
|
+
expectation.fragment,
|
|
163
|
+
getModels(),
|
|
164
|
+
getScopedModels(),
|
|
165
|
+
expectation.parsedProvider,
|
|
166
|
+
).some((model) => model.value === item.value);
|
|
167
|
+
if (modelValue && expectation.kind === "value") {
|
|
168
|
+
return applyCompletionEdit(
|
|
169
|
+
lines,
|
|
170
|
+
expectation.replacementStart,
|
|
171
|
+
expectation.replacementEnd,
|
|
172
|
+
item.value,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const providerValue =
|
|
176
|
+
expectation?.kind === "value" &&
|
|
177
|
+
expectation.option.completionDomain === "provider" &&
|
|
178
|
+
getProviderItems(expectation.fragment, getModels()).some(
|
|
179
|
+
(provider) => provider.value === item.value,
|
|
180
|
+
);
|
|
181
|
+
if (providerValue && expectation.kind === "value") {
|
|
182
|
+
return applyCompletionEdit(
|
|
183
|
+
lines,
|
|
184
|
+
expectation.replacementStart,
|
|
185
|
+
expectation.replacementEnd,
|
|
186
|
+
item.value,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (expectation?.kind === "value" && expectation.option.completionDomain === "tool") {
|
|
190
|
+
const segment = getToolSegment(expectation, lines[0] ?? "", cursorCol);
|
|
191
|
+
const toolValue = getToolItems(segment.fragment, getTools(), segment.selectedNames).some(
|
|
192
|
+
(tool) => tool.value === item.value,
|
|
193
|
+
);
|
|
194
|
+
if (toolValue) return applyToolCompletionEdit(lines, segment, item.value);
|
|
195
|
+
}
|
|
196
|
+
if (expectation?.kind === "value" && expectation.option.completionDomain === "path") {
|
|
197
|
+
const resourceShortcut = getResourceShortcutItems(expectation, getCommands()).some(
|
|
198
|
+
(candidate) => candidate.value === item.value && candidate.label === item.label,
|
|
199
|
+
);
|
|
200
|
+
if (resourceShortcut) return applyResourceShortcutEdit(lines, expectation, item.value);
|
|
201
|
+
const result = current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
202
|
+
if (item.label.endsWith("/")) return result;
|
|
203
|
+
const completedLine = result.lines[result.cursorLine] ?? "";
|
|
204
|
+
const cursorAfterSeparator = /\s/.test(completedLine[result.cursorCol] ?? "")
|
|
205
|
+
? result.cursorCol + 1
|
|
206
|
+
: result.cursorCol;
|
|
207
|
+
return { ...result, cursorCol: cursorAfterSeparator };
|
|
208
|
+
}
|
|
209
|
+
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
213
|
+
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function registerAgentAutocomplete(pi: ExtensionAPI): void {
|
|
219
|
+
let autocompleteEditorFactory: EditorFactory | undefined;
|
|
220
|
+
|
|
221
|
+
function installEditorBridge(ctx: ExtensionContext): void {
|
|
222
|
+
if (ctx.mode !== "tui") return;
|
|
223
|
+
const previousEditorFactory = ctx.ui.getEditorComponent();
|
|
224
|
+
if (autocompleteEditorFactory && previousEditorFactory === autocompleteEditorFactory) return;
|
|
225
|
+
|
|
226
|
+
const modelRuntime = (ctx.modelRegistry as unknown as { runtime: ModelRuntime }).runtime;
|
|
227
|
+
const isValueValid = createAgentValueValidator(modelRuntime, () =>
|
|
228
|
+
pi.getAllTools().map((tool) => tool.name),
|
|
229
|
+
);
|
|
230
|
+
autocompleteEditorFactory = (tui, theme, keybindings) => {
|
|
231
|
+
const editor =
|
|
232
|
+
previousEditorFactory?.(tui, theme, keybindings) ??
|
|
233
|
+
new CustomEditor(tui, theme, keybindings);
|
|
234
|
+
const autocompleteEditor = decorateAutocompleteEditor(
|
|
235
|
+
editor,
|
|
236
|
+
(data) =>
|
|
237
|
+
keybindings.matches(data, "tui.input.tab") ||
|
|
238
|
+
keybindings.matches(data, "tui.select.confirm"),
|
|
239
|
+
);
|
|
240
|
+
return decorateColoringEditor(autocompleteEditor, () => ctx.ui.theme, isValueValid);
|
|
241
|
+
};
|
|
242
|
+
ctx.ui.setEditorComponent(autocompleteEditorFactory);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
246
|
+
if (ctx.mode !== "tui") return;
|
|
247
|
+
const settingsManager = SettingsManager.create(ctx.cwd);
|
|
248
|
+
const { scopedModels } = await resolveExtensionModelScope(
|
|
249
|
+
settingsManager.getEnabledModels() ?? [],
|
|
250
|
+
ctx.modelRegistry,
|
|
251
|
+
);
|
|
252
|
+
ctx.ui.addAutocompleteProvider((current) => {
|
|
253
|
+
installEditorBridge(ctx);
|
|
254
|
+
return createAgentAutocompleteProvider(
|
|
255
|
+
current,
|
|
256
|
+
() => ctx.modelRegistry.getAll(),
|
|
257
|
+
() => pi.getAllTools(),
|
|
258
|
+
() => pi.getCommands(),
|
|
259
|
+
() => scopedModels.map(({ model }) => model),
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
installEditorBridge(ctx);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function getModelItems(
|
|
267
|
+
fragment: string,
|
|
268
|
+
models: readonly CompletionModel[],
|
|
269
|
+
scopedModels: readonly CompletionModel[],
|
|
270
|
+
provider?: string,
|
|
271
|
+
): AutocompleteItem[] {
|
|
272
|
+
const providerModels = models.filter(
|
|
273
|
+
(model) => provider === undefined || model.provider === provider,
|
|
274
|
+
);
|
|
275
|
+
return fuzzyFilter(
|
|
276
|
+
scopedModelsFirst(providerModels, scopedModels),
|
|
277
|
+
fragment,
|
|
278
|
+
(model) => `${model.provider}/${model.id} ${model.name ?? ""}`,
|
|
279
|
+
).map((model) => {
|
|
280
|
+
const reference = `${model.provider}/${model.id}`;
|
|
281
|
+
return {
|
|
282
|
+
value: reference,
|
|
283
|
+
label: reference,
|
|
284
|
+
description: model.name && model.name !== model.id ? model.name : undefined,
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** @example scopedModelsFirst([{ provider: "all", id: "first" }, { provider: "scope", id: "second" }], [{ provider: "scope", id: "second" }])[0]?.provider // "scope" */
|
|
290
|
+
function scopedModelsFirst(
|
|
291
|
+
models: readonly CompletionModel[],
|
|
292
|
+
scopedModels: readonly CompletionModel[],
|
|
293
|
+
): CompletionModel[] {
|
|
294
|
+
const scopedReferences = new Set(scopedModels.map((model) => `${model.provider}/${model.id}`));
|
|
295
|
+
return [
|
|
296
|
+
...models.filter((model) => scopedReferences.has(`${model.provider}/${model.id}`)),
|
|
297
|
+
...models.filter((model) => !scopedReferences.has(`${model.provider}/${model.id}`)),
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getProviderItems(
|
|
302
|
+
fragment: string,
|
|
303
|
+
models: readonly CompletionModel[],
|
|
304
|
+
): AutocompleteItem[] {
|
|
305
|
+
const query = fragment.toLowerCase();
|
|
306
|
+
return [...new Set(models.map((model) => model.provider))]
|
|
307
|
+
.filter((provider) => provider.toLowerCase().includes(query))
|
|
308
|
+
.map((provider) => ({ value: provider, label: provider }));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
type ToolSegment = {
|
|
312
|
+
fragment: string;
|
|
313
|
+
replacementStart: number;
|
|
314
|
+
replacementEnd: number;
|
|
315
|
+
selectedNames: ReadonlySet<string>;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
function getToolSegment(
|
|
319
|
+
expectation: ValueCursorExpectation,
|
|
320
|
+
line: string,
|
|
321
|
+
cursorCol: number,
|
|
322
|
+
): ToolSegment {
|
|
323
|
+
const value = line.slice(expectation.replacementStart, expectation.replacementEnd);
|
|
324
|
+
const cursorOffset = cursorCol - expectation.replacementStart;
|
|
325
|
+
const segmentStart = value.lastIndexOf(",", cursorOffset - 1) + 1;
|
|
326
|
+
const nextComma = value.indexOf(",", cursorOffset);
|
|
327
|
+
const segmentEnd = nextComma === -1 ? value.length : nextComma;
|
|
328
|
+
const selectedNames = new Set(
|
|
329
|
+
value
|
|
330
|
+
.split(",")
|
|
331
|
+
.filter((_name, index) => index !== value.slice(0, segmentStart).split(",").length - 1),
|
|
332
|
+
);
|
|
333
|
+
return {
|
|
334
|
+
fragment: value.slice(segmentStart, cursorOffset),
|
|
335
|
+
replacementStart: expectation.replacementStart + segmentStart,
|
|
336
|
+
replacementEnd: expectation.replacementStart + segmentEnd,
|
|
337
|
+
selectedNames,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function getToolItems(
|
|
342
|
+
fragment: string,
|
|
343
|
+
tools: readonly CompletionTool[],
|
|
344
|
+
selectedNames: ReadonlySet<string>,
|
|
345
|
+
): AutocompleteItem[] {
|
|
346
|
+
const query = fragment.toLowerCase();
|
|
347
|
+
return tools
|
|
348
|
+
.filter((tool) => !selectedNames.has(tool.name))
|
|
349
|
+
.filter((tool) => tool.name.toLowerCase().includes(query))
|
|
350
|
+
.map((tool) => ({ value: tool.name, label: tool.name, description: tool.description }));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function getResourceShortcutItems(
|
|
354
|
+
expectation: ValueCursorExpectation,
|
|
355
|
+
commands: readonly CompletionResourceCommand[],
|
|
356
|
+
): AutocompleteItem[] {
|
|
357
|
+
const source =
|
|
358
|
+
expectation.option.semanticId === "skill"
|
|
359
|
+
? "skill"
|
|
360
|
+
: expectation.option.semanticId === "prompt-template"
|
|
361
|
+
? "prompt"
|
|
362
|
+
: undefined;
|
|
363
|
+
if (source === undefined) return [];
|
|
364
|
+
const query = expectation.fragment.toLowerCase();
|
|
365
|
+
return commands
|
|
366
|
+
.filter((command) => command.source === source)
|
|
367
|
+
.filter((command) => command.name.toLowerCase().includes(query))
|
|
368
|
+
.map((command) => ({
|
|
369
|
+
value: command.sourceInfo.path,
|
|
370
|
+
label: command.name,
|
|
371
|
+
description: command.description,
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function applyCompletionEdit(
|
|
376
|
+
lines: string[],
|
|
377
|
+
replacementStart: number,
|
|
378
|
+
replacementEnd: number,
|
|
379
|
+
value: string,
|
|
380
|
+
): { lines: string[]; cursorLine: number; cursorCol: number } {
|
|
381
|
+
const line = lines[0] ?? "";
|
|
382
|
+
const before = line.slice(0, replacementStart);
|
|
383
|
+
const after = line.slice(replacementEnd).replace(/^\s+/, "");
|
|
384
|
+
const insertion = `${value} `;
|
|
385
|
+
const nextLines = [...lines];
|
|
386
|
+
nextLines[0] = `${before}${insertion}${after}`;
|
|
387
|
+
return { lines: nextLines, cursorLine: 0, cursorCol: before.length + insertion.length };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function applyResourceShortcutEdit(
|
|
391
|
+
lines: string[],
|
|
392
|
+
expectation: ValueCursorExpectation,
|
|
393
|
+
path: string,
|
|
394
|
+
): { lines: string[]; cursorLine: number; cursorCol: number } {
|
|
395
|
+
if (!expectation.quoted) {
|
|
396
|
+
const value = /\s/.test(path) ? `"${path}"` : path;
|
|
397
|
+
return applyCompletionEdit(
|
|
398
|
+
lines,
|
|
399
|
+
expectation.replacementStart,
|
|
400
|
+
expectation.replacementEnd,
|
|
401
|
+
value,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const line = lines[0] ?? "";
|
|
405
|
+
const before = line.slice(0, expectation.replacementStart);
|
|
406
|
+
const afterValue = line.slice(expectation.replacementEnd);
|
|
407
|
+
const insertion = `${path}${afterValue[0] ?? ""} `;
|
|
408
|
+
const nextLines = [...lines];
|
|
409
|
+
nextLines[0] = `${before}${insertion}${afterValue.slice(1).replace(/^\s+/, "")}`;
|
|
410
|
+
return { lines: nextLines, cursorLine: 0, cursorCol: before.length + insertion.length };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function applyToolCompletionEdit(
|
|
414
|
+
lines: string[],
|
|
415
|
+
segment: ToolSegment,
|
|
416
|
+
value: string,
|
|
417
|
+
): { lines: string[]; cursorLine: number; cursorCol: number } {
|
|
418
|
+
const line = lines[0] ?? "";
|
|
419
|
+
const nextLines = [...lines];
|
|
420
|
+
nextLines[0] = `${line.slice(0, segment.replacementStart)}${value}${line.slice(segment.replacementEnd)}`;
|
|
421
|
+
return { lines: nextLines, cursorLine: 0, cursorCol: segment.replacementStart + value.length };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function applyGuardedOptionEdit(
|
|
425
|
+
lines: string[],
|
|
426
|
+
replacementStart: number,
|
|
427
|
+
replacementEnd: number,
|
|
428
|
+
optionName: string,
|
|
429
|
+
): { lines: string[]; cursorLine: number; cursorCol: number } {
|
|
430
|
+
const line = lines[0] ?? "";
|
|
431
|
+
const before = line.slice(0, replacementStart);
|
|
432
|
+
const after = line.slice(replacementEnd).replace(/^\s+/, "");
|
|
433
|
+
const insertion = `${optionName} "" `;
|
|
434
|
+
const nextLines = [...lines];
|
|
435
|
+
nextLines[0] = `${before}${insertion}${after}`;
|
|
436
|
+
return { lines: nextLines, cursorLine: 0, cursorCol: before.length + optionName.length + 2 };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function decorateAutocompleteEditor<T extends EditorComponent>(
|
|
440
|
+
editor: T,
|
|
441
|
+
isCompletionAcceptance: (data: string) => boolean,
|
|
442
|
+
): T {
|
|
443
|
+
const runtimeEditor = editor as DecoratedEditor;
|
|
444
|
+
if (runtimeEditor[DECORATED_EDITOR]) return editor;
|
|
445
|
+
const handleInput = editor.handleInput.bind(editor);
|
|
446
|
+
editor.handleInput = (data) => {
|
|
447
|
+
const getCursor = runtimeEditor.getCursor;
|
|
448
|
+
if (typeof getCursor !== "function")
|
|
449
|
+
throw new Error("pi-user-agents autocomplete requires editor.getCursor()");
|
|
450
|
+
const beforeCursor = getCursor.call(editor);
|
|
451
|
+
const beforeExpectation =
|
|
452
|
+
beforeCursor.line === 0
|
|
453
|
+
? analyzeAgentEditorInput(editor.getText(), beforeCursor.col)
|
|
454
|
+
: undefined;
|
|
455
|
+
const wasShowingAutocomplete = runtimeEditor.isShowingAutocomplete?.call(editor) ?? false;
|
|
456
|
+
const printableCharacter =
|
|
457
|
+
decodeKittyPrintable(data) ??
|
|
458
|
+
(data.length === 1 && data.charCodeAt(0) >= 32 ? data : undefined);
|
|
459
|
+
handleInput(data);
|
|
460
|
+
|
|
461
|
+
const cursor = getCursor.call(editor);
|
|
462
|
+
if (cursor.line !== 0) return;
|
|
463
|
+
const expectation = analyzeAgentEditorInput(editor.getText(), cursor.col);
|
|
464
|
+
const typedOptionDash = printableCharacter === "-" && expectation?.kind === "option";
|
|
465
|
+
const completionAcceptance = isCompletionAcceptance(data);
|
|
466
|
+
const typedEagerValueBoundary =
|
|
467
|
+
printableCharacter === " " &&
|
|
468
|
+
beforeExpectation?.kind === "option" &&
|
|
469
|
+
expectation?.kind === "value" &&
|
|
470
|
+
(expectation.option.completionDomain === "tool" ||
|
|
471
|
+
expectation.option.completionDomain === "path");
|
|
472
|
+
const typedToolComma =
|
|
473
|
+
printableCharacter === "," &&
|
|
474
|
+
beforeExpectation?.kind === "value" &&
|
|
475
|
+
beforeExpectation.option.completionDomain === "tool" &&
|
|
476
|
+
expectation?.kind === "value" &&
|
|
477
|
+
expectation.option.completionDomain === "tool";
|
|
478
|
+
const acceptedChainedOption =
|
|
479
|
+
wasShowingAutocomplete &&
|
|
480
|
+
completionAcceptance &&
|
|
481
|
+
beforeExpectation?.kind === "option" &&
|
|
482
|
+
expectation?.kind === "value" &&
|
|
483
|
+
expectation.option.completionDomain !== undefined;
|
|
484
|
+
const acceptedPathDirectory =
|
|
485
|
+
wasShowingAutocomplete &&
|
|
486
|
+
completionAcceptance &&
|
|
487
|
+
beforeExpectation?.kind === "value" &&
|
|
488
|
+
beforeExpectation.option.completionDomain === "path" &&
|
|
489
|
+
expectation?.kind === "value" &&
|
|
490
|
+
expectation.option.completionDomain === "path";
|
|
491
|
+
const optionItems =
|
|
492
|
+
beforeExpectation?.kind === "option" ? getOptionItems(beforeExpectation) : [];
|
|
493
|
+
const onlyOption =
|
|
494
|
+
optionItems.length === 1
|
|
495
|
+
? AGENT_OPTIONS.find((option) => option.names.includes(optionItems[0]?.value ?? ""))
|
|
496
|
+
: undefined;
|
|
497
|
+
const pendingChainedOption =
|
|
498
|
+
!wasShowingAutocomplete &&
|
|
499
|
+
completionAcceptance &&
|
|
500
|
+
expectation?.kind === "option" &&
|
|
501
|
+
onlyOption?.completionDomain !== undefined;
|
|
502
|
+
if (pendingChainedOption) {
|
|
503
|
+
observeChainedOptionEdit(editor, runtimeEditor, onlyOption.semanticId);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (
|
|
507
|
+
!typedOptionDash &&
|
|
508
|
+
!typedEagerValueBoundary &&
|
|
509
|
+
!typedToolComma &&
|
|
510
|
+
!acceptedChainedOption &&
|
|
511
|
+
!acceptedPathDirectory
|
|
512
|
+
)
|
|
513
|
+
return;
|
|
514
|
+
|
|
515
|
+
if (typedEagerValueBoundary || typedToolComma) {
|
|
516
|
+
triggerValueAutocomplete(editor, runtimeEditor, expectation);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (acceptedPathDirectory) {
|
|
520
|
+
triggerValueAutocomplete(editor, runtimeEditor, expectation);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (acceptedChainedOption) {
|
|
524
|
+
triggerValueAutocomplete(editor, runtimeEditor, expectation);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const triggerAutocomplete = runtimeEditor.tryTriggerAutocomplete;
|
|
528
|
+
if (typeof triggerAutocomplete !== "function")
|
|
529
|
+
throw new Error("pi-user-agents autocomplete requires editor.tryTriggerAutocomplete()");
|
|
530
|
+
triggerAutocomplete.call(editor, false);
|
|
531
|
+
};
|
|
532
|
+
runtimeEditor[DECORATED_EDITOR] = true;
|
|
533
|
+
return editor;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function observeChainedOptionEdit(
|
|
537
|
+
editor: EditorComponent,
|
|
538
|
+
runtimeEditor: AutocompleteRuntimeEditor,
|
|
539
|
+
semanticId: string,
|
|
540
|
+
): void {
|
|
541
|
+
const deadline = Date.now() + 500;
|
|
542
|
+
const observe = (): void => {
|
|
543
|
+
const cursor = runtimeEditor.getCursor?.call(editor);
|
|
544
|
+
const expectation =
|
|
545
|
+
cursor?.line === 0 ? analyzeAgentEditorInput(editor.getText(), cursor.col) : undefined;
|
|
546
|
+
if (
|
|
547
|
+
expectation?.kind === "value" &&
|
|
548
|
+
expectation.option.semanticId === semanticId &&
|
|
549
|
+
expectation.option.completionDomain !== undefined
|
|
550
|
+
) {
|
|
551
|
+
triggerValueAutocomplete(editor, runtimeEditor, expectation);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (runtimeEditor.isShowingAutocomplete?.call(editor) || Date.now() >= deadline) return;
|
|
555
|
+
setTimeout(observe, 5);
|
|
556
|
+
};
|
|
557
|
+
setTimeout(observe, 0);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function triggerValueAutocomplete(
|
|
561
|
+
editor: EditorComponent,
|
|
562
|
+
runtimeEditor: AutocompleteRuntimeEditor,
|
|
563
|
+
expectation: ValueCursorExpectation,
|
|
564
|
+
): void {
|
|
565
|
+
if (expectation.option.completionDomain === "path") {
|
|
566
|
+
const forceFileAutocomplete = runtimeEditor.forceFileAutocomplete;
|
|
567
|
+
if (typeof forceFileAutocomplete !== "function")
|
|
568
|
+
throw new Error("pi-user-agents autocomplete requires editor.forceFileAutocomplete()");
|
|
569
|
+
forceFileAutocomplete.call(editor, false);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const triggerAutocomplete = runtimeEditor.tryTriggerAutocomplete;
|
|
573
|
+
if (typeof triggerAutocomplete !== "function")
|
|
574
|
+
throw new Error("pi-user-agents autocomplete requires editor.tryTriggerAutocomplete()");
|
|
575
|
+
triggerAutocomplete.call(editor, false);
|
|
576
|
+
}
|