@oh-my-pi/pi-coding-agent 13.15.2 → 13.16.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/CHANGELOG.md +26 -16
- package/package.json +7 -7
- package/src/config/keybindings.ts +6 -0
- package/src/config/model-registry.ts +215 -57
- package/src/config/settings-schema.ts +27 -0
- package/src/extensibility/extensions/types.ts +6 -1
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/custom-editor.ts +6 -4
- package/src/modes/components/hook-editor.ts +57 -8
- package/src/modes/components/model-selector.ts +48 -29
- package/src/modes/components/settings-defs.ts +10 -1
- package/src/modes/components/settings-selector.ts +92 -5
- package/src/modes/controllers/extension-ui-controller.ts +32 -4
- package/src/modes/controllers/input-controller.ts +22 -9
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +7 -2
- package/src/modes/rpc/rpc-mode.ts +78 -30
- package/src/modes/rpc/rpc-types.ts +9 -1
- package/src/modes/theme/theme.ts +70 -0
- package/src/modes/types.ts +6 -1
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/tools/ask.md +1 -0
- package/src/prompts/tools/hashline.md +20 -5
- package/src/sdk.ts +9 -1
- package/src/session/agent-session.ts +338 -80
- package/src/session/messages.ts +23 -0
- package/src/session/session-manager.ts +65 -0
- package/src/system-prompt.ts +63 -2
- package/src/tools/ask.ts +109 -61
- package/src/tools/ast-edit.ts +2 -16
- package/src/tools/ast-grep.ts +2 -17
- package/src/tools/browser.ts +35 -17
- package/src/tools/grep.ts +4 -17
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/render-utils.ts +27 -0
- package/src/tui/tree-list.ts +51 -22
- package/src/utils/image-input.ts +11 -1
- package/src/web/search/providers/codex.ts +10 -3
|
@@ -2,6 +2,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import type { Effort } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import {
|
|
4
4
|
Container,
|
|
5
|
+
Input,
|
|
5
6
|
matchesKey,
|
|
6
7
|
type SelectItem,
|
|
7
8
|
SelectList,
|
|
@@ -31,6 +32,52 @@ import { getPreset } from "./status-line/presets";
|
|
|
31
32
|
/**
|
|
32
33
|
* A submenu component for selecting from a list of options.
|
|
33
34
|
*/
|
|
35
|
+
/**
|
|
36
|
+
* Submenu component for free-text string settings.
|
|
37
|
+
* Mirrors the ConfigInputSubmenu pattern from plugin-settings.ts.
|
|
38
|
+
*/
|
|
39
|
+
class TextInputSubmenu extends Container {
|
|
40
|
+
#input: Input;
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
label: string,
|
|
44
|
+
description: string,
|
|
45
|
+
currentValue: string,
|
|
46
|
+
private readonly onSubmit: (value: string) => void,
|
|
47
|
+
private readonly onCancel: () => void,
|
|
48
|
+
) {
|
|
49
|
+
super();
|
|
50
|
+
|
|
51
|
+
this.addChild(new Text(theme.bold(theme.fg("accent", label)), 0, 0));
|
|
52
|
+
if (description) {
|
|
53
|
+
this.addChild(new Spacer(1));
|
|
54
|
+
this.addChild(new Text(theme.fg("muted", description), 0, 0));
|
|
55
|
+
}
|
|
56
|
+
this.addChild(new Spacer(1));
|
|
57
|
+
|
|
58
|
+
this.#input = new Input();
|
|
59
|
+
if (currentValue) {
|
|
60
|
+
this.#input.setValue(currentValue);
|
|
61
|
+
// Move cursor to end of pre-filled value (ctrl+e = cursorLineEnd).
|
|
62
|
+
this.#input.handleInput("\x05");
|
|
63
|
+
}
|
|
64
|
+
this.#input.onSubmit = value => {
|
|
65
|
+
this.onSubmit(value); // empty string clears the setting
|
|
66
|
+
};
|
|
67
|
+
this.addChild(this.#input);
|
|
68
|
+
this.addChild(new Spacer(1));
|
|
69
|
+
this.addChild(new Text(theme.fg("dim", " Enter to save · Esc to cancel · Clear field to unset"), 0, 0));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
handleInput(data: string): void {
|
|
73
|
+
if (data === "\x1b" || data === "\x1b\x1b") {
|
|
74
|
+
this.onCancel();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.#input.handleInput(data);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
34
81
|
class SelectSubmenu extends Container {
|
|
35
82
|
#selectList: SelectList;
|
|
36
83
|
#previewText: Text | null = null;
|
|
@@ -180,6 +227,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
180
227
|
#statusPreviewContainer: Container | null = null;
|
|
181
228
|
#statusPreviewText: Text | null = null;
|
|
182
229
|
#currentTabId: SettingTab | "plugins" = "appearance";
|
|
230
|
+
#textInputActive = false;
|
|
183
231
|
|
|
184
232
|
constructor(
|
|
185
233
|
private readonly context: SettingsRuntimeContext,
|
|
@@ -277,6 +325,15 @@ export class SettingsSelectorComponent extends Container {
|
|
|
277
325
|
currentValue: this.#getSubmenuCurrentValue(def.path, currentValue),
|
|
278
326
|
submenu: (cv, done) => this.#createSubmenu(def, cv, done),
|
|
279
327
|
};
|
|
328
|
+
|
|
329
|
+
case "text":
|
|
330
|
+
return {
|
|
331
|
+
id: def.path,
|
|
332
|
+
label: def.label,
|
|
333
|
+
description: def.description,
|
|
334
|
+
currentValue: (currentValue as string) ?? "",
|
|
335
|
+
submenu: (cv, done) => this.#createTextInput(def, cv, done),
|
|
336
|
+
};
|
|
280
337
|
}
|
|
281
338
|
}
|
|
282
339
|
|
|
@@ -389,6 +446,34 @@ export class SettingsSelectorComponent extends Container {
|
|
|
389
446
|
);
|
|
390
447
|
}
|
|
391
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Create a text input submenu for a plain string setting.
|
|
451
|
+
*/
|
|
452
|
+
#createTextInput(
|
|
453
|
+
def: SettingDef & { type: "text" },
|
|
454
|
+
currentValue: string,
|
|
455
|
+
done: (value?: string) => void,
|
|
456
|
+
): Container {
|
|
457
|
+
this.#textInputActive = true;
|
|
458
|
+
const wrappedDone = (value?: string) => {
|
|
459
|
+
this.#textInputActive = false;
|
|
460
|
+
done(value);
|
|
461
|
+
};
|
|
462
|
+
return new TextInputSubmenu(
|
|
463
|
+
def.label,
|
|
464
|
+
def.description,
|
|
465
|
+
currentValue,
|
|
466
|
+
value => {
|
|
467
|
+
// Empty string clears the setting; undefined-typed string settings
|
|
468
|
+
// store "" which the browser.ts expandPath ignores (no-op fallback).
|
|
469
|
+
this.#setSettingValue(def.path, value);
|
|
470
|
+
this.callbacks.onChange(def.path, value);
|
|
471
|
+
wrappedDone(value);
|
|
472
|
+
},
|
|
473
|
+
() => wrappedDone(),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
392
477
|
/**
|
|
393
478
|
* Set a setting value, handling type conversion.
|
|
394
479
|
*/
|
|
@@ -510,12 +595,14 @@ export class SettingsSelectorComponent extends Container {
|
|
|
510
595
|
}
|
|
511
596
|
|
|
512
597
|
handleInput(data: string): void {
|
|
513
|
-
// Handle tab switching
|
|
598
|
+
// Handle tab switching — but NOT when a text input is active, since
|
|
599
|
+
// arrow keys must reach the cursor and Tab must not switch tabs.
|
|
514
600
|
if (
|
|
515
|
-
|
|
516
|
-
matchesKey(data, "
|
|
517
|
-
|
|
518
|
-
|
|
601
|
+
!this.#textInputActive &&
|
|
602
|
+
(matchesKey(data, "tab") ||
|
|
603
|
+
matchesKey(data, "shift+tab") ||
|
|
604
|
+
matchesKey(data, "left") ||
|
|
605
|
+
matchesKey(data, "right"))
|
|
519
606
|
) {
|
|
520
607
|
this.#tabBar.handleInput(data);
|
|
521
608
|
return;
|
|
@@ -50,7 +50,8 @@ export class ExtensionUiController {
|
|
|
50
50
|
this.ctx.editor.handleInput(`\x1b[200~${text}\x1b[201~`);
|
|
51
51
|
},
|
|
52
52
|
getEditorText: () => this.ctx.editor.getText(),
|
|
53
|
-
editor: (title, prefill
|
|
53
|
+
editor: (title, prefill, dialogOptions, editorOptions) =>
|
|
54
|
+
this.showHookEditor(title, prefill, dialogOptions, editorOptions),
|
|
54
55
|
get theme() {
|
|
55
56
|
return theme;
|
|
56
57
|
},
|
|
@@ -783,26 +784,53 @@ export class ExtensionUiController {
|
|
|
783
784
|
/**
|
|
784
785
|
* Show a multi-line editor for hooks (with Ctrl+G support).
|
|
785
786
|
*/
|
|
786
|
-
showHookEditor(
|
|
787
|
+
showHookEditor(
|
|
788
|
+
title: string,
|
|
789
|
+
prefill?: string,
|
|
790
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
791
|
+
editorOptions?: { promptStyle?: boolean },
|
|
792
|
+
): Promise<string | undefined> {
|
|
787
793
|
const { promise, resolve } = Promise.withResolvers<string | undefined>();
|
|
794
|
+
let settled = false;
|
|
795
|
+
const onAbort = () => {
|
|
796
|
+
this.hideHookEditor();
|
|
797
|
+
if (!settled) {
|
|
798
|
+
settled = true;
|
|
799
|
+
resolve(undefined);
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
const finish = (value: string | undefined) => {
|
|
803
|
+
if (settled) return;
|
|
804
|
+
settled = true;
|
|
805
|
+
dialogOptions?.signal?.removeEventListener("abort", onAbort);
|
|
806
|
+
resolve(value);
|
|
807
|
+
};
|
|
788
808
|
this.ctx.hookEditor = new HookEditorComponent(
|
|
789
809
|
this.ctx.ui,
|
|
790
810
|
title,
|
|
791
811
|
prefill,
|
|
792
812
|
value => {
|
|
793
813
|
this.hideHookEditor();
|
|
794
|
-
|
|
814
|
+
finish(value);
|
|
795
815
|
},
|
|
796
816
|
() => {
|
|
797
817
|
this.hideHookEditor();
|
|
798
|
-
|
|
818
|
+
finish(undefined);
|
|
799
819
|
},
|
|
820
|
+
editorOptions,
|
|
800
821
|
);
|
|
801
822
|
|
|
802
823
|
this.ctx.editorContainer.clear();
|
|
803
824
|
this.ctx.editorContainer.addChild(this.ctx.hookEditor);
|
|
804
825
|
this.ctx.ui.setFocus(this.ctx.hookEditor);
|
|
805
826
|
this.ctx.ui.requestRender();
|
|
827
|
+
if (dialogOptions?.signal) {
|
|
828
|
+
if (dialogOptions.signal.aborted) {
|
|
829
|
+
onAbort();
|
|
830
|
+
} else {
|
|
831
|
+
dialogOptions.signal.addEventListener("abort", onAbort, { once: true });
|
|
832
|
+
}
|
|
833
|
+
}
|
|
806
834
|
return promise;
|
|
807
835
|
}
|
|
808
836
|
|
|
@@ -11,6 +11,7 @@ import type { AgentSessionEvent } from "../../session/agent-session";
|
|
|
11
11
|
import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
|
|
12
12
|
import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
|
|
13
13
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
14
|
+
import { ensureSupportedImageInput } from "../../utils/image-input";
|
|
14
15
|
import { resizeImage } from "../../utils/image-resize";
|
|
15
16
|
import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
|
|
16
17
|
|
|
@@ -95,7 +96,11 @@ export class InputController {
|
|
|
95
96
|
this.ctx.editor.onCycleModelForward = () => this.cycleRoleModel();
|
|
96
97
|
this.ctx.editor.setActionKeys("app.model.cycleBackward", this.ctx.keybindings.getKeys("app.model.cycleBackward"));
|
|
97
98
|
this.ctx.editor.onCycleModelBackward = () => this.cycleRoleModel({ temporary: true });
|
|
98
|
-
this.ctx.editor.
|
|
99
|
+
this.ctx.editor.setActionKeys(
|
|
100
|
+
"app.model.selectTemporary",
|
|
101
|
+
this.ctx.keybindings.getKeys("app.model.selectTemporary"),
|
|
102
|
+
);
|
|
103
|
+
this.ctx.editor.onSelectModelTemporary = () => this.ctx.showModelSelector({ temporaryOnly: true });
|
|
99
104
|
|
|
100
105
|
// Global debug handler on TUI (works regardless of focus)
|
|
101
106
|
this.ctx.ui.onDebug = () => this.ctx.showDebugSelector();
|
|
@@ -498,17 +503,25 @@ export class InputController {
|
|
|
498
503
|
const image = await readImageFromClipboard();
|
|
499
504
|
if (image) {
|
|
500
505
|
const base64Data = image.data.toBase64();
|
|
501
|
-
let imageData = {
|
|
506
|
+
let imageData = await ensureSupportedImageInput({
|
|
507
|
+
type: "image",
|
|
508
|
+
data: base64Data,
|
|
509
|
+
mimeType: image.mimeType,
|
|
510
|
+
});
|
|
511
|
+
if (!imageData) {
|
|
512
|
+
this.ctx.showStatus(`Unsupported clipboard image format: ${image.mimeType}`);
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
502
515
|
if (settings.get("images.autoResize")) {
|
|
503
516
|
try {
|
|
504
517
|
const resized = await resizeImage({
|
|
505
518
|
type: "image",
|
|
506
|
-
data:
|
|
507
|
-
mimeType:
|
|
519
|
+
data: imageData.data,
|
|
520
|
+
mimeType: imageData.mimeType,
|
|
508
521
|
});
|
|
509
|
-
imageData = { data: resized.data, mimeType: resized.mimeType };
|
|
522
|
+
imageData = { type: "image", data: resized.data, mimeType: resized.mimeType };
|
|
510
523
|
} catch {
|
|
511
|
-
|
|
524
|
+
// Keep the normalized image when resize fails.
|
|
512
525
|
}
|
|
513
526
|
}
|
|
514
527
|
|
|
@@ -595,8 +608,8 @@ export class InputController {
|
|
|
595
608
|
|
|
596
609
|
async cycleRoleModel(options?: { temporary?: boolean }): Promise<void> {
|
|
597
610
|
try {
|
|
598
|
-
const
|
|
599
|
-
const result = await this.ctx.session.cycleRoleModels(
|
|
611
|
+
const cycleOrder = settings.get("cycleOrder");
|
|
612
|
+
const result = await this.ctx.session.cycleRoleModels(cycleOrder, options);
|
|
600
613
|
if (!result) {
|
|
601
614
|
this.ctx.showStatus("Only one role model available");
|
|
602
615
|
return;
|
|
@@ -612,7 +625,7 @@ export class InputController {
|
|
|
612
625
|
: "";
|
|
613
626
|
const tempLabel = options?.temporary ? " (temporary)" : "";
|
|
614
627
|
const cycleSeparator = theme.fg("dim", " > ");
|
|
615
|
-
const cycleLabel =
|
|
628
|
+
const cycleLabel = cycleOrder
|
|
616
629
|
.map(role => {
|
|
617
630
|
if (role === result.role) {
|
|
618
631
|
return theme.bold(theme.fg("accent", role));
|
|
@@ -3,7 +3,7 @@ import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai";
|
|
|
3
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
6
|
-
import {
|
|
6
|
+
import { getRoleInfo } from "../../config/model-registry";
|
|
7
7
|
import { settings } from "../../config/settings";
|
|
8
8
|
import { DebugSelectorComponent } from "../../debug";
|
|
9
9
|
import { disableProvider, enableProvider } from "../../discovery";
|
|
@@ -406,7 +406,7 @@ export class SelectorController {
|
|
|
406
406
|
// Don't call done() - selector stays open for role assignment
|
|
407
407
|
} else {
|
|
408
408
|
// Other roles (smol, slow): just update settings, not current model
|
|
409
|
-
const roleInfo =
|
|
409
|
+
const roleInfo = getRoleInfo(role, settings);
|
|
410
410
|
const roleLabel = roleInfo?.name ?? role;
|
|
411
411
|
this.ctx.showStatus(`${roleLabel} model: ${model.id}`);
|
|
412
412
|
// Don't call done() - selector stays open
|
|
@@ -1399,8 +1399,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1399
1399
|
this.#extensionUiController.hideHookInput();
|
|
1400
1400
|
}
|
|
1401
1401
|
|
|
1402
|
-
showHookEditor(
|
|
1403
|
-
|
|
1402
|
+
showHookEditor(
|
|
1403
|
+
title: string,
|
|
1404
|
+
prefill?: string,
|
|
1405
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
1406
|
+
editorOptions?: { promptStyle?: boolean },
|
|
1407
|
+
): Promise<string | undefined> {
|
|
1408
|
+
return this.#extensionUiController.showHookEditor(title, prefill, dialogOptions, editorOptions);
|
|
1404
1409
|
}
|
|
1405
1410
|
|
|
1406
1411
|
hideHookEditor(): void {
|
|
@@ -29,6 +29,77 @@ import type {
|
|
|
29
29
|
// Re-export types for consumers
|
|
30
30
|
export type * from "./rpc-types";
|
|
31
31
|
|
|
32
|
+
export type PendingExtensionRequest = {
|
|
33
|
+
resolve: (response: RpcExtensionUIResponse) => void;
|
|
34
|
+
reject: (error: Error) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type RpcOutput = (obj: RpcResponse | RpcExtensionUIRequest | object) => void;
|
|
38
|
+
|
|
39
|
+
export function requestRpcEditor(
|
|
40
|
+
pendingRequests: Map<string, PendingExtensionRequest>,
|
|
41
|
+
output: RpcOutput,
|
|
42
|
+
title: string,
|
|
43
|
+
prefill?: string,
|
|
44
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
45
|
+
editorOptions?: { promptStyle?: boolean },
|
|
46
|
+
): Promise<string | undefined> {
|
|
47
|
+
if (dialogOptions?.signal?.aborted) return Promise.resolve(undefined);
|
|
48
|
+
|
|
49
|
+
const id = Snowflake.next() as string;
|
|
50
|
+
const { promise, resolve, reject } = Promise.withResolvers<string | undefined>();
|
|
51
|
+
let settled = false;
|
|
52
|
+
|
|
53
|
+
const cleanup = () => {
|
|
54
|
+
dialogOptions?.signal?.removeEventListener("abort", onAbort);
|
|
55
|
+
pendingRequests.delete(id);
|
|
56
|
+
};
|
|
57
|
+
const finish = (value: string | undefined) => {
|
|
58
|
+
if (settled) return;
|
|
59
|
+
settled = true;
|
|
60
|
+
cleanup();
|
|
61
|
+
resolve(value);
|
|
62
|
+
};
|
|
63
|
+
const fail = (error: Error) => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
settled = true;
|
|
66
|
+
cleanup();
|
|
67
|
+
reject(error);
|
|
68
|
+
};
|
|
69
|
+
const onAbort = () => {
|
|
70
|
+
output({
|
|
71
|
+
type: "extension_ui_request",
|
|
72
|
+
id: Snowflake.next() as string,
|
|
73
|
+
method: "cancel",
|
|
74
|
+
targetId: id,
|
|
75
|
+
} as RpcExtensionUIRequest);
|
|
76
|
+
finish(undefined);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
dialogOptions?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
80
|
+
pendingRequests.set(id, {
|
|
81
|
+
resolve: response => {
|
|
82
|
+
if ("cancelled" in response && response.cancelled) {
|
|
83
|
+
finish(undefined);
|
|
84
|
+
} else if ("value" in response) {
|
|
85
|
+
finish(response.value);
|
|
86
|
+
} else {
|
|
87
|
+
finish(undefined);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
reject: fail,
|
|
91
|
+
});
|
|
92
|
+
output({
|
|
93
|
+
type: "extension_ui_request",
|
|
94
|
+
id,
|
|
95
|
+
method: "editor",
|
|
96
|
+
title,
|
|
97
|
+
prefill,
|
|
98
|
+
promptStyle: editorOptions?.promptStyle,
|
|
99
|
+
} as RpcExtensionUIRequest);
|
|
100
|
+
return promise;
|
|
101
|
+
}
|
|
102
|
+
|
|
32
103
|
/**
|
|
33
104
|
* Run in RPC mode.
|
|
34
105
|
* Listens for JSON commands on stdin, outputs events and responses on stdout.
|
|
@@ -55,12 +126,6 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
55
126
|
return { id, type: "response", command, success: false, error: message };
|
|
56
127
|
};
|
|
57
128
|
|
|
58
|
-
// Pending extension UI requests waiting for response
|
|
59
|
-
type PendingExtensionRequest = {
|
|
60
|
-
resolve: (response: RpcExtensionUIResponse) => void;
|
|
61
|
-
reject: (error: Error) => void;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
129
|
const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
|
|
65
130
|
|
|
66
131
|
// Shutdown request flag (wrapped in object to allow mutation with const)
|
|
@@ -261,30 +326,13 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
261
326
|
return "";
|
|
262
327
|
}
|
|
263
328
|
|
|
264
|
-
async editor(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
resolve(undefined);
|
|
272
|
-
} else if ("value" in response) {
|
|
273
|
-
resolve(response.value);
|
|
274
|
-
} else {
|
|
275
|
-
resolve(undefined);
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
reject,
|
|
279
|
-
});
|
|
280
|
-
this.output({
|
|
281
|
-
type: "extension_ui_request",
|
|
282
|
-
id,
|
|
283
|
-
method: "editor",
|
|
284
|
-
title,
|
|
285
|
-
prefill,
|
|
286
|
-
} as RpcExtensionUIRequest);
|
|
287
|
-
return promise;
|
|
329
|
+
async editor(
|
|
330
|
+
title: string,
|
|
331
|
+
prefill?: string,
|
|
332
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
333
|
+
editorOptions?: { promptStyle?: boolean },
|
|
334
|
+
): Promise<string | undefined> {
|
|
335
|
+
return requestRpcEditor(this.pendingRequests, this.output, title, prefill, dialogOptions, editorOptions);
|
|
288
336
|
}
|
|
289
337
|
|
|
290
338
|
get theme(): Theme {
|
|
@@ -194,7 +194,15 @@ export type RpcExtensionUIRequest =
|
|
|
194
194
|
placeholder?: string;
|
|
195
195
|
timeout?: number;
|
|
196
196
|
}
|
|
197
|
-
| {
|
|
197
|
+
| {
|
|
198
|
+
type: "extension_ui_request";
|
|
199
|
+
id: string;
|
|
200
|
+
method: "editor";
|
|
201
|
+
title: string;
|
|
202
|
+
prefill?: string;
|
|
203
|
+
promptStyle?: boolean;
|
|
204
|
+
}
|
|
205
|
+
| { type: "extension_ui_request"; id: string; method: "cancel"; targetId: string }
|
|
198
206
|
| {
|
|
199
207
|
type: "extension_ui_request";
|
|
200
208
|
id: string;
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -955,6 +955,76 @@ export type ThemeColor =
|
|
|
955
955
|
| "statusLineCost"
|
|
956
956
|
| "statusLineSubagents";
|
|
957
957
|
|
|
958
|
+
/** Set of all valid ThemeColor string values for runtime validation */
|
|
959
|
+
const THEME_COLOR_RECORD = {
|
|
960
|
+
accent: true,
|
|
961
|
+
border: true,
|
|
962
|
+
borderAccent: true,
|
|
963
|
+
borderMuted: true,
|
|
964
|
+
success: true,
|
|
965
|
+
error: true,
|
|
966
|
+
warning: true,
|
|
967
|
+
muted: true,
|
|
968
|
+
dim: true,
|
|
969
|
+
text: true,
|
|
970
|
+
thinkingText: true,
|
|
971
|
+
userMessageText: true,
|
|
972
|
+
customMessageText: true,
|
|
973
|
+
customMessageLabel: true,
|
|
974
|
+
toolTitle: true,
|
|
975
|
+
toolOutput: true,
|
|
976
|
+
mdHeading: true,
|
|
977
|
+
mdLink: true,
|
|
978
|
+
mdLinkUrl: true,
|
|
979
|
+
mdCode: true,
|
|
980
|
+
mdCodeBlock: true,
|
|
981
|
+
mdCodeBlockBorder: true,
|
|
982
|
+
mdQuote: true,
|
|
983
|
+
mdQuoteBorder: true,
|
|
984
|
+
mdHr: true,
|
|
985
|
+
mdListBullet: true,
|
|
986
|
+
toolDiffAdded: true,
|
|
987
|
+
toolDiffRemoved: true,
|
|
988
|
+
toolDiffContext: true,
|
|
989
|
+
syntaxComment: true,
|
|
990
|
+
syntaxKeyword: true,
|
|
991
|
+
syntaxFunction: true,
|
|
992
|
+
syntaxVariable: true,
|
|
993
|
+
syntaxString: true,
|
|
994
|
+
syntaxNumber: true,
|
|
995
|
+
syntaxType: true,
|
|
996
|
+
syntaxOperator: true,
|
|
997
|
+
syntaxPunctuation: true,
|
|
998
|
+
thinkingOff: true,
|
|
999
|
+
thinkingMinimal: true,
|
|
1000
|
+
thinkingLow: true,
|
|
1001
|
+
thinkingMedium: true,
|
|
1002
|
+
thinkingHigh: true,
|
|
1003
|
+
thinkingXhigh: true,
|
|
1004
|
+
bashMode: true,
|
|
1005
|
+
pythonMode: true,
|
|
1006
|
+
statusLineSep: true,
|
|
1007
|
+
statusLineModel: true,
|
|
1008
|
+
statusLinePath: true,
|
|
1009
|
+
statusLineGitClean: true,
|
|
1010
|
+
statusLineGitDirty: true,
|
|
1011
|
+
statusLineContext: true,
|
|
1012
|
+
statusLineSpend: true,
|
|
1013
|
+
statusLineStaged: true,
|
|
1014
|
+
statusLineDirty: true,
|
|
1015
|
+
statusLineUntracked: true,
|
|
1016
|
+
statusLineOutput: true,
|
|
1017
|
+
statusLineCost: true,
|
|
1018
|
+
statusLineSubagents: true,
|
|
1019
|
+
} satisfies Record<ThemeColor, true>;
|
|
1020
|
+
|
|
1021
|
+
const VALID_THEME_COLORS: ReadonlySet<string> = new Set(Object.keys(THEME_COLOR_RECORD));
|
|
1022
|
+
|
|
1023
|
+
/** Check if a string is a valid ThemeColor value */
|
|
1024
|
+
export function isValidThemeColor(color: string): color is ThemeColor {
|
|
1025
|
+
return VALID_THEME_COLORS.has(color);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
958
1028
|
export type ThemeBg =
|
|
959
1029
|
| "selectedBg"
|
|
960
1030
|
| "userMessageBg"
|
package/src/modes/types.ts
CHANGED
|
@@ -243,7 +243,12 @@ export interface InteractiveModeContext {
|
|
|
243
243
|
hideHookSelector(): void;
|
|
244
244
|
showHookInput(title: string, placeholder?: string): Promise<string | undefined>;
|
|
245
245
|
hideHookInput(): void;
|
|
246
|
-
showHookEditor(
|
|
246
|
+
showHookEditor(
|
|
247
|
+
title: string,
|
|
248
|
+
prefill?: string,
|
|
249
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
250
|
+
editorOptions?: { promptStyle?: boolean },
|
|
251
|
+
): Promise<string | undefined>;
|
|
247
252
|
hideHookEditor(): void;
|
|
248
253
|
showHookNotify(message: string, type?: "info" | "warning" | "error"): void;
|
|
249
254
|
showHookCustom<T>(
|
|
@@ -40,7 +40,7 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
|
|
|
40
40
|
`| \`${appKey(bindings, "app.thinking.cycle")}\` | Cycle thinking level |`,
|
|
41
41
|
`| \`${appKey(bindings, "app.model.cycleForward")}\` | Cycle role models (slow/default/smol) |`,
|
|
42
42
|
`| \`${appKey(bindings, "app.model.cycleBackward")}\` | Cycle role models (temporary) |`,
|
|
43
|
-
"
|
|
43
|
+
`| \`${appKey(bindings, "app.model.selectTemporary")}\` | Select model (temporary) |`,
|
|
44
44
|
`| \`${appKey(bindings, "app.model.select")}\` | Select model (set roles) |`,
|
|
45
45
|
`| \`${appKey(bindings, "app.plan.toggle")}\` | Toggle plan mode |`,
|
|
46
46
|
`| \`${appKey(bindings, "app.history.search")}\` | Search prompt history |`,
|
|
@@ -40,6 +40,11 @@ If a skill covers your output, you **MUST** read `skill://<name>` before proceed
|
|
|
40
40
|
{{/list}}
|
|
41
41
|
</skills>
|
|
42
42
|
{{/if}}
|
|
43
|
+
{{#if alwaysApplyRules.length}}
|
|
44
|
+
{{#each alwaysApplyRules}}
|
|
45
|
+
{{content}}
|
|
46
|
+
{{/each}}
|
|
47
|
+
{{/if}}
|
|
43
48
|
{{#if rules.length}}
|
|
44
49
|
Rules are local constraints.
|
|
45
50
|
You **MUST** read `rule://<name>` when working in that domain.
|
|
@@ -124,6 +124,12 @@ You **MUST** use the following skills, to save you time, when working in their d
|
|
|
124
124
|
{{/each}}
|
|
125
125
|
{{/if}}
|
|
126
126
|
|
|
127
|
+
{{#if alwaysApplyRules.length}}
|
|
128
|
+
{{#each alwaysApplyRules}}
|
|
129
|
+
{{content}}
|
|
130
|
+
{{/each}}
|
|
131
|
+
{{/if}}
|
|
132
|
+
|
|
127
133
|
{{#if rules.length}}
|
|
128
134
|
# Rules
|
|
129
135
|
Domain-specific rules from past experience. **MUST** read `rule://<name>` when working in their territory.
|
package/src/prompts/tools/ask.md
CHANGED
|
@@ -8,6 +8,7 @@ Asks user when you need clarification or input during task execution.
|
|
|
8
8
|
- Use `recommended: <index>` to mark default (0-indexed); " (Recommended)" added automatically
|
|
9
9
|
- Use `questions` for multiple related questions instead of asking one at a time
|
|
10
10
|
- Set `multi: true` on question to allow multiple selections
|
|
11
|
+
- `ask.timeout` only applies while choosing options; once the user selects "Other (type your own)", there is no timeout
|
|
11
12
|
</instruction>
|
|
12
13
|
|
|
13
14
|
<caution>
|
|
@@ -2,8 +2,6 @@ Applies precise file edits using `LINE#ID` anchors from `read` output.
|
|
|
2
2
|
|
|
3
3
|
Read the file first. Copy anchors exactly from the latest `read` output. In one `edit` call, batch all edits for one file. After any successful edit, re-read before editing that file again.
|
|
4
4
|
|
|
5
|
-
This matters: your output is checked against the real file state. Invalid anchors, duplicated boundary lines, or semantically equivalent rewrites will fail.
|
|
6
|
-
|
|
7
5
|
<operations>
|
|
8
6
|
**Top level**
|
|
9
7
|
- `path` — file path
|
|
@@ -61,6 +59,24 @@ Replace only the catch body. Do not target the shared boundary line `} catch (er
|
|
|
61
59
|
```
|
|
62
60
|
</example>
|
|
63
61
|
|
|
62
|
+
<example name="replace whole block including closing brace">
|
|
63
|
+
Replace the entire body of `alpha`, including its closing `}`. `end` **MUST** be {{hlineref 7 "}"}} because `content` includes `}`.
|
|
64
|
+
```
|
|
65
|
+
{
|
|
66
|
+
path: "util.ts",
|
|
67
|
+
edits: [{
|
|
68
|
+
loc: { block: { pos: {{hlineref 6 "\tlog();"}}, end: {{hlineref 7 "}"}} } },
|
|
69
|
+
content: [
|
|
70
|
+
"\tvalidate();",
|
|
71
|
+
"\tlog();",
|
|
72
|
+
"}"
|
|
73
|
+
]
|
|
74
|
+
}]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
**Wrong**: using `end: {{hlineref 6 "\tlog();"}}` with the same content — line 7 (`}`) survives the replacement AND content emits `}`, producing two closing braces.
|
|
78
|
+
</example>
|
|
79
|
+
|
|
64
80
|
<example name="replace one line">
|
|
65
81
|
```
|
|
66
82
|
{
|
|
@@ -108,9 +124,8 @@ When adding a sibling declaration, prefer `prepend` on the next declaration.
|
|
|
108
124
|
- Make the minimum exact edit. Do not rewrite nearby code unless the consumed range requires it.
|
|
109
125
|
- Use anchors exactly as `N#ID` from the latest `read` output.
|
|
110
126
|
- `block` requires both `pos` and `end`. Other anchored ops require one anchor.
|
|
111
|
-
-
|
|
112
|
-
- **
|
|
113
|
-
- Do not target shared boundary lines such as `} else {`, `} catch (…) {`, `}),`, or `},{`.
|
|
127
|
+
- When your replacement `content` ends with a closing delimiter (`}`, `*/`, `)`, `]`), verify `end` includes the original line carrying that delimiter. If `end` stops one line too early, the original delimiter survives and your content adds a second copy.
|
|
128
|
+
- **Self-check**: compare the last line of `content` with the line immediately after `end` in the file. If they match (e.g., both are `}`), extend `end` to include that line.
|
|
114
129
|
- For a block, either replace only the body or replace the whole block. Do not split block boundaries.
|
|
115
130
|
- `content` must be literal file content with matching indentation. If the file uses tabs, use real tabs.
|
|
116
131
|
- Do not use this tool to reformat or clean up unrelated code.
|
package/src/sdk.ts
CHANGED
|
@@ -796,6 +796,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
796
796
|
}),
|
|
797
797
|
);
|
|
798
798
|
|
|
799
|
+
// collect alwaysApply rules — full content injected into system prompt
|
|
800
|
+
const alwaysApplyRules = rulesResult.items.filter((rule: Rule) => {
|
|
801
|
+
if (registeredTtsrRuleNames.has(rule.name)) return false;
|
|
802
|
+
return rule.alwaysApply === true;
|
|
803
|
+
});
|
|
804
|
+
|
|
799
805
|
const contextFiles = await logger.timeAsync(
|
|
800
806
|
"discoverContextFiles",
|
|
801
807
|
async () => options.contextFiles ?? (await discoverContextFiles(cwd, agentDir)),
|
|
@@ -930,7 +936,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
930
936
|
);
|
|
931
937
|
internalRouter.register(
|
|
932
938
|
new RuleProtocolHandler({
|
|
933
|
-
getRules: () => rulebookRules,
|
|
939
|
+
getRules: () => [...rulebookRules, ...alwaysApplyRules],
|
|
934
940
|
}),
|
|
935
941
|
);
|
|
936
942
|
internalRouter.register(new PiProtocolHandler());
|
|
@@ -1252,6 +1258,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1252
1258
|
tools: promptTools,
|
|
1253
1259
|
toolNames,
|
|
1254
1260
|
rules: rulebookRules,
|
|
1261
|
+
alwaysApplyRules,
|
|
1255
1262
|
skillsSettings: settings.getGroup("skills"),
|
|
1256
1263
|
appendSystemPrompt: appendPrompt,
|
|
1257
1264
|
repeatToolDescriptions,
|
|
@@ -1272,6 +1279,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1272
1279
|
tools: promptTools,
|
|
1273
1280
|
toolNames,
|
|
1274
1281
|
rules: rulebookRules,
|
|
1282
|
+
alwaysApplyRules,
|
|
1275
1283
|
skillsSettings: settings.getGroup("skills"),
|
|
1276
1284
|
customPrompt: options.systemPrompt,
|
|
1277
1285
|
appendSystemPrompt: appendPrompt,
|