@oh-my-pi/pi-coding-agent 15.11.0 → 15.11.2
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 +57 -2
- package/dist/cli.js +678 -657
- package/dist/types/capability/mcp.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +49 -4
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
- package/dist/types/extensibility/custom-tools/loader.d.ts +2 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +8 -4
- package/dist/types/extensibility/extensions/types.d.ts +2 -2
- package/dist/types/extensibility/hooks/types.d.ts +8 -4
- package/dist/types/irc/bus.d.ts +15 -2
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +6 -1
- package/dist/types/mcp/transports/stdio.d.ts +1 -0
- package/dist/types/mcp/types.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +30 -0
- package/dist/types/modes/theme/theme.d.ts +3 -2
- package/dist/types/session/agent-session.d.ts +17 -3
- package/dist/types/slash-commands/available-commands.d.ts +34 -0
- package/dist/types/task/index.d.ts +3 -3
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/attach.d.ts +4 -4
- package/dist/types/tools/browser/registry.d.ts +1 -0
- package/dist/types/tools/irc.d.ts +3 -2
- package/dist/types/tools/path-utils.d.ts +0 -4
- package/dist/types/tools/render-utils.d.ts +22 -0
- package/package.json +11 -11
- package/src/capability/mcp.ts +1 -0
- package/src/cli/gallery-cli.ts +5 -4
- package/src/config/mcp-schema.json +4 -0
- package/src/config/settings-schema.ts +55 -4
- package/src/edit/renderer.ts +96 -46
- package/src/exec/bash-executor.ts +21 -6
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +6 -1
- package/src/extensibility/custom-commands/loader.ts +3 -1
- package/src/extensibility/custom-commands/types.ts +6 -3
- package/src/extensibility/custom-tools/loader.ts +4 -7
- package/src/extensibility/custom-tools/types.ts +8 -4
- package/src/extensibility/extensions/loader.ts +2 -1
- package/src/extensibility/extensions/types.ts +2 -2
- package/src/extensibility/hooks/loader.ts +3 -1
- package/src/extensibility/hooks/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/irc/bus.ts +14 -3
- package/src/lsp/defaults.json +6 -0
- package/src/lsp/render.ts +2 -28
- package/src/mcp/manager.ts +3 -0
- package/src/mcp/oauth-discovery.ts +27 -2
- package/src/mcp/oauth-flow.ts +47 -1
- package/src/mcp/transports/stdio.ts +3 -0
- package/src/mcp/types.ts +2 -0
- package/src/memories/index.ts +2 -0
- package/src/modes/acp/acp-agent.ts +4 -67
- package/src/modes/components/assistant-message.ts +15 -0
- package/src/modes/components/btw-panel.ts +5 -1
- package/src/modes/components/mcp-add-wizard.ts +13 -0
- package/src/modes/components/plan-review-overlay.ts +32 -3
- package/src/modes/components/settings-selector.ts +2 -0
- package/src/modes/components/status-line/component.ts +22 -12
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/transcript-container.ts +99 -18
- package/src/modes/components/tree-selector.ts +6 -1
- package/src/modes/controllers/event-controller.ts +28 -4
- package/src/modes/controllers/mcp-command-controller.ts +34 -2
- package/src/modes/controllers/selector-controller.ts +4 -0
- package/src/modes/controllers/streaming-reveal.ts +16 -8
- package/src/modes/controllers/tool-args-reveal.ts +174 -0
- package/src/modes/interactive-mode.ts +41 -2
- package/src/modes/rpc/rpc-client.ts +32 -0
- package/src/modes/rpc/rpc-mode.ts +82 -7
- package/src/modes/rpc/rpc-types.ts +23 -0
- package/src/modes/theme/theme.ts +13 -7
- package/src/modes/utils/ui-helpers.ts +13 -4
- package/src/prompts/memories/consolidation_system.md +4 -0
- package/src/prompts/system/irc-autoreply.md +6 -0
- package/src/prompts/system/irc-incoming.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/task.md +7 -2
- package/src/session/agent-session.ts +120 -10
- package/src/slash-commands/available-commands.ts +105 -0
- package/src/task/index.ts +15 -10
- package/src/task/render.ts +10 -4
- package/src/tools/bash.ts +5 -1
- package/src/tools/browser/attach.ts +26 -7
- package/src/tools/browser/registry.ts +11 -1
- package/src/tools/irc.ts +16 -4
- package/src/tools/job.ts +7 -3
- package/src/tools/path-utils.ts +22 -15
- package/src/tools/render-utils.ts +56 -0
- package/src/tools/write.ts +65 -47
- package/src/web/search/providers/anthropic.ts +29 -4
|
@@ -163,7 +163,7 @@ export class StreamingRevealController {
|
|
|
163
163
|
this.#hideThinkingBlock = this.#getHideThinkingBlock();
|
|
164
164
|
this.#smoothStreaming = this.#getSmoothStreaming();
|
|
165
165
|
if (!this.#smoothStreaming) {
|
|
166
|
-
component.updateContent(message);
|
|
166
|
+
component.updateContent(message, { transient: true });
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
169
|
const total = this.#visibleUnits(message);
|
|
@@ -171,10 +171,12 @@ export class StreamingRevealController {
|
|
|
171
171
|
// A tool call is a transcript-order boundary: finish any leading
|
|
172
172
|
// assistant text before EventController renders the separate tool card.
|
|
173
173
|
this.#revealed = total;
|
|
174
|
-
component.updateContent(buildDisplayMessage(message, this.#revealed, this.#hideThinkingBlock, this.#countOf)
|
|
174
|
+
component.updateContent(buildDisplayMessage(message, this.#revealed, this.#hideThinkingBlock, this.#countOf), {
|
|
175
|
+
transient: true,
|
|
176
|
+
});
|
|
175
177
|
return;
|
|
176
178
|
}
|
|
177
|
-
this.#renderCurrent(
|
|
179
|
+
this.#renderCurrent();
|
|
178
180
|
this.#syncTimer(total);
|
|
179
181
|
}
|
|
180
182
|
|
|
@@ -182,7 +184,7 @@ export class StreamingRevealController {
|
|
|
182
184
|
this.#target = message;
|
|
183
185
|
if (!this.#component) return;
|
|
184
186
|
if (!this.#smoothStreaming) {
|
|
185
|
-
this.#component.updateContent(message);
|
|
187
|
+
this.#component.updateContent(message, { transient: true });
|
|
186
188
|
return;
|
|
187
189
|
}
|
|
188
190
|
const total = this.#visibleUnits(message);
|
|
@@ -193,13 +195,16 @@ export class StreamingRevealController {
|
|
|
193
195
|
this.#stopTimer();
|
|
194
196
|
this.#component.updateContent(
|
|
195
197
|
buildDisplayMessage(message, this.#revealed, this.#hideThinkingBlock, this.#countOf),
|
|
198
|
+
{
|
|
199
|
+
transient: true,
|
|
200
|
+
},
|
|
196
201
|
);
|
|
197
202
|
return;
|
|
198
203
|
}
|
|
199
204
|
if (this.#revealed > total) {
|
|
200
205
|
this.#revealed = total;
|
|
201
206
|
}
|
|
202
|
-
this.#renderCurrent(
|
|
207
|
+
this.#renderCurrent();
|
|
203
208
|
this.#syncTimer(total);
|
|
204
209
|
}
|
|
205
210
|
|
|
@@ -225,11 +230,14 @@ export class StreamingRevealController {
|
|
|
225
230
|
return total;
|
|
226
231
|
}
|
|
227
232
|
|
|
228
|
-
#renderCurrent(
|
|
233
|
+
#renderCurrent(): void {
|
|
229
234
|
if (!this.#target || !this.#component) return;
|
|
235
|
+
// Every controller render is an in-flight streaming snapshot, even when
|
|
236
|
+
// smooth reveal has temporarily caught up to the current target. The
|
|
237
|
+
// message_end handler performs the only stable non-transient render.
|
|
230
238
|
this.#component.updateContent(
|
|
231
239
|
buildDisplayMessage(this.#target, this.#revealed, this.#hideThinkingBlock, this.#countOf),
|
|
232
|
-
{ transient:
|
|
240
|
+
{ transient: true },
|
|
233
241
|
);
|
|
234
242
|
}
|
|
235
243
|
|
|
@@ -269,7 +277,7 @@ export class StreamingRevealController {
|
|
|
269
277
|
}
|
|
270
278
|
this.#revealed = Math.min(total, this.#revealed + nextStep(total - this.#revealed));
|
|
271
279
|
component.updateContent(buildDisplayMessage(target, this.#revealed, this.#hideThinkingBlock, this.#countOf), {
|
|
272
|
-
transient:
|
|
280
|
+
transient: true,
|
|
273
281
|
});
|
|
274
282
|
this.#requestRender();
|
|
275
283
|
if (this.#revealed >= total) {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { parseStreamingJson } from "@oh-my-pi/pi-ai/utils/json-parse";
|
|
2
|
+
import { nextStep, STREAMING_REVEAL_FRAME_MS } from "./streaming-reveal";
|
|
3
|
+
|
|
4
|
+
/** Minimal component surface the reveal pushes frames into. */
|
|
5
|
+
type ToolArgsRevealComponent = {
|
|
6
|
+
updateArgs(args: unknown, toolCallId?: string): void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type ToolArgsRevealControllerOptions = {
|
|
10
|
+
getSmoothStreaming(): boolean;
|
|
11
|
+
requestRender(): void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type RevealEntry = {
|
|
15
|
+
component: ToolArgsRevealComponent | undefined;
|
|
16
|
+
/** Latest raw streamed argument text (JSON for function tools, raw text for custom tools). */
|
|
17
|
+
target: string;
|
|
18
|
+
/** Revealed UTF-16 code units of `target`. */
|
|
19
|
+
revealed: number;
|
|
20
|
+
/** Custom-tool raw input: display args are `{ input: prefix }`, never parsed as JSON. */
|
|
21
|
+
rawInput: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Clamp a slice end into `text`, never splitting a surrogate pair: a prefix
|
|
25
|
+
* ending on a high surrogate would feed a lone surrogate into the parsed
|
|
26
|
+
* preview args (providers decode UTF-8 incrementally, so the raw stream
|
|
27
|
+
* itself never contains one). */
|
|
28
|
+
function clampSliceEnd(text: string, end: number): number {
|
|
29
|
+
if (end <= 0) return 0;
|
|
30
|
+
if (end >= text.length) return text.length;
|
|
31
|
+
const code = text.charCodeAt(end - 1);
|
|
32
|
+
return code >= 0xd800 && code <= 0xdbff ? end + 1 : end;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Display args for a revealed raw-stream prefix. Function-tool prefixes are
|
|
36
|
+
* re-parsed with the same streaming-tolerant parser providers use, so every
|
|
37
|
+
* frame is a state the provider itself could have produced; custom tools
|
|
38
|
+
* mirror the provider's `{ input }` shape. `__partialJson` carries the
|
|
39
|
+
* matching raw prefix for renderers that read it directly (bash env preview,
|
|
40
|
+
* edit strategies). */
|
|
41
|
+
function buildDisplayArgs(prefix: string, rawInput: boolean): Record<string, unknown> {
|
|
42
|
+
const base: Record<string, unknown> = rawInput ? { input: prefix } : parseStreamingJson(prefix);
|
|
43
|
+
return { ...base, __partialJson: prefix };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Paces streamed tool-call arguments the same way StreamingRevealController
|
|
48
|
+
* paces assistant text: providers that deliver `partialJson` in large batches
|
|
49
|
+
* (or throttle their partial parses) would otherwise make write/edit/bash
|
|
50
|
+
* streaming previews jump in chunks. Each pending tool call reveals its raw
|
|
51
|
+
* argument stream at the shared 30fps cadence with the same adaptive
|
|
52
|
+
* catch-up step, re-parsing the revealed prefix per frame.
|
|
53
|
+
*
|
|
54
|
+
* Reveal units are UTF-16 code units of the raw stream, not graphemes —
|
|
55
|
+
* the prefix goes through a JSON parser rather than straight to the screen,
|
|
56
|
+
* so only surrogate-pair integrity matters (see {@link clampSliceEnd}).
|
|
57
|
+
*/
|
|
58
|
+
export class ToolArgsRevealController {
|
|
59
|
+
readonly #getSmoothStreaming: () => boolean;
|
|
60
|
+
readonly #requestRender: () => void;
|
|
61
|
+
readonly #entries = new Map<string, RevealEntry>();
|
|
62
|
+
#timer: NodeJS.Timeout | undefined;
|
|
63
|
+
|
|
64
|
+
constructor(options: ToolArgsRevealControllerOptions) {
|
|
65
|
+
this.#getSmoothStreaming = options.getSmoothStreaming;
|
|
66
|
+
this.#requestRender = options.requestRender;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Record the latest streamed argument text for a tool call and return the
|
|
71
|
+
* args to render right now. With smoothing disabled the full target passes
|
|
72
|
+
* through in the caller's legacy shape (`{ ...args, __partialJson }`).
|
|
73
|
+
*/
|
|
74
|
+
setTarget(
|
|
75
|
+
id: string,
|
|
76
|
+
partialJson: string,
|
|
77
|
+
rawInput: boolean,
|
|
78
|
+
fullArgs: Record<string, unknown>,
|
|
79
|
+
): Record<string, unknown> {
|
|
80
|
+
if (!this.#getSmoothStreaming()) {
|
|
81
|
+
// Toggle may flip mid-call: drop any live entry so ticks stop.
|
|
82
|
+
this.#entries.delete(id);
|
|
83
|
+
return { ...fullArgs, __partialJson: partialJson };
|
|
84
|
+
}
|
|
85
|
+
let entry = this.#entries.get(id);
|
|
86
|
+
if (!entry) {
|
|
87
|
+
entry = { component: undefined, target: partialJson, revealed: 0, rawInput };
|
|
88
|
+
this.#entries.set(id, entry);
|
|
89
|
+
} else {
|
|
90
|
+
// Streams only append; a non-prefix target means a rewind — snap into range.
|
|
91
|
+
if (!partialJson.startsWith(entry.target)) {
|
|
92
|
+
entry.revealed = Math.min(entry.revealed, partialJson.length);
|
|
93
|
+
}
|
|
94
|
+
entry.target = partialJson;
|
|
95
|
+
}
|
|
96
|
+
entry.revealed = clampSliceEnd(entry.target, entry.revealed);
|
|
97
|
+
this.#syncTimer();
|
|
98
|
+
return buildDisplayArgs(entry.target.slice(0, entry.revealed), entry.rawInput);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Attach the component future ticks push frames into. */
|
|
102
|
+
bind(id: string, component: ToolArgsRevealComponent): void {
|
|
103
|
+
const entry = this.#entries.get(id);
|
|
104
|
+
if (entry) entry.component = component;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Final arguments arrived (the JSON closed): drop the reveal so the
|
|
108
|
+
* caller's final-args render wins immediately, mirroring how assistant
|
|
109
|
+
* text snaps to the full message at message_end. */
|
|
110
|
+
finish(id: string): void {
|
|
111
|
+
this.#entries.delete(id);
|
|
112
|
+
if (this.#entries.size === 0) this.#stopTimer();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Snap every live entry to its full received stream and clear. Used at
|
|
116
|
+
* message_end (abort/error mid-stream) so sealed components freeze showing
|
|
117
|
+
* everything that arrived rather than a mid-reveal prefix. */
|
|
118
|
+
flushAll(): void {
|
|
119
|
+
for (const [id, entry] of this.#entries) {
|
|
120
|
+
if (entry.component && entry.revealed < entry.target.length) {
|
|
121
|
+
entry.component.updateArgs(buildDisplayArgs(entry.target, entry.rawInput), id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
this.#entries.clear();
|
|
125
|
+
this.#stopTimer();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Clear without pushing (teardown). */
|
|
129
|
+
stop(): void {
|
|
130
|
+
this.#entries.clear();
|
|
131
|
+
this.#stopTimer();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#syncTimer(): void {
|
|
135
|
+
for (const entry of this.#entries.values()) {
|
|
136
|
+
if (entry.revealed < entry.target.length) {
|
|
137
|
+
this.#startTimer();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
this.#stopTimer();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#startTimer(): void {
|
|
145
|
+
if (this.#timer) return;
|
|
146
|
+
this.#timer = setInterval(() => {
|
|
147
|
+
this.#tick();
|
|
148
|
+
}, STREAMING_REVEAL_FRAME_MS);
|
|
149
|
+
this.#timer.unref?.();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#stopTimer(): void {
|
|
153
|
+
if (!this.#timer) return;
|
|
154
|
+
clearInterval(this.#timer);
|
|
155
|
+
this.#timer = undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#tick(): void {
|
|
159
|
+
let advanced = false;
|
|
160
|
+
for (const [id, entry] of this.#entries) {
|
|
161
|
+
const backlog = entry.target.length - entry.revealed;
|
|
162
|
+
if (backlog <= 0 || !entry.component) continue;
|
|
163
|
+
entry.revealed = clampSliceEnd(entry.target, entry.revealed + nextStep(backlog));
|
|
164
|
+
entry.component.updateArgs(buildDisplayArgs(entry.target.slice(0, entry.revealed), entry.rawInput), id);
|
|
165
|
+
advanced = true;
|
|
166
|
+
}
|
|
167
|
+
if (advanced) {
|
|
168
|
+
this.#requestRender();
|
|
169
|
+
} else {
|
|
170
|
+
// Every entry caught up (or unbound); setTarget restarts on growth.
|
|
171
|
+
this.#stopTimer();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -496,8 +496,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
496
496
|
}
|
|
497
497
|
|
|
498
498
|
playWelcomeIntro(): void {
|
|
499
|
-
this.#welcomeComponent
|
|
499
|
+
const welcome = this.#welcomeComponent;
|
|
500
|
+
// Component-scoped: the intro only mutates the welcome box's own rows,
|
|
501
|
+
// so a resumed long transcript is not re-walked per animation frame.
|
|
502
|
+
welcome?.playIntro(() => this.ui.requestComponentRender(welcome));
|
|
500
503
|
}
|
|
504
|
+
|
|
501
505
|
async init(options: InteractiveModeInitOptions = {}): Promise<void> {
|
|
502
506
|
if (this.isInitialized) return;
|
|
503
507
|
|
|
@@ -1050,6 +1054,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1050
1054
|
separator: settings.get("statusLine.separator"),
|
|
1051
1055
|
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
1052
1056
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
1057
|
+
transparent: settings.get("statusLine.transparent"),
|
|
1053
1058
|
segmentOptions: settings.get("statusLine.segmentOptions"),
|
|
1054
1059
|
});
|
|
1055
1060
|
}
|
|
@@ -1780,6 +1785,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1780
1785
|
onPick: choice => finish(choice),
|
|
1781
1786
|
onCancel: () => finish(undefined),
|
|
1782
1787
|
onExternalEditor: dialogOptions?.onExternalEditor,
|
|
1788
|
+
onAnnotationExternalEditor: (draft, commit) => void this.#openPlanAnnotationInExternalEditor(draft, commit),
|
|
1783
1789
|
onPlanEdited: dialogOptions?.onPlanEdited,
|
|
1784
1790
|
onFeedbackChange: dialogOptions?.onFeedbackChange,
|
|
1785
1791
|
},
|
|
@@ -1894,6 +1900,37 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1894
1900
|
}
|
|
1895
1901
|
}
|
|
1896
1902
|
|
|
1903
|
+
async #openPlanAnnotationInExternalEditor(draft: string, commit: (text: string | null) => void): Promise<void> {
|
|
1904
|
+
const editorCmd = getEditorCommand();
|
|
1905
|
+
if (!editorCmd) {
|
|
1906
|
+
this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
let ttyHandle: fs.FileHandle | null = null;
|
|
1911
|
+
try {
|
|
1912
|
+
ttyHandle = await this.#openEditorTerminalHandle();
|
|
1913
|
+
this.ui.stop();
|
|
1914
|
+
|
|
1915
|
+
const stdio: [number | "inherit", number | "inherit", number | "inherit"] = ttyHandle
|
|
1916
|
+
? [ttyHandle.fd, ttyHandle.fd, ttyHandle.fd]
|
|
1917
|
+
: ["inherit", "inherit", "inherit"];
|
|
1918
|
+
|
|
1919
|
+
const result = await openInEditor(editorCmd, draft, { extension: ".md", stdio });
|
|
1920
|
+
if (result !== null) {
|
|
1921
|
+
commit(result);
|
|
1922
|
+
}
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
this.showWarning(`Failed to open external editor: ${error instanceof Error ? error.message : String(error)}`);
|
|
1925
|
+
} finally {
|
|
1926
|
+
if (ttyHandle) {
|
|
1927
|
+
await ttyHandle.close();
|
|
1928
|
+
}
|
|
1929
|
+
this.ui.start();
|
|
1930
|
+
this.ui.requestRender(true);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1897
1934
|
async #applyPlanExecutionModel(entry: ResolvedRoleModel | undefined): Promise<void> {
|
|
1898
1935
|
if (!entry) return;
|
|
1899
1936
|
try {
|
|
@@ -3036,7 +3073,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3036
3073
|
this.#voiceAnimationInterval = setInterval(() => {
|
|
3037
3074
|
this.#voiceHue = (this.#voiceHue + 8) % 360;
|
|
3038
3075
|
this.#updateMicIcon();
|
|
3039
|
-
|
|
3076
|
+
// Component-scoped: the hue sweep only recolors the editor's cursor
|
|
3077
|
+
// glyph, so the transcript subtree is reused per animation frame.
|
|
3078
|
+
this.ui.requestComponentRender(this.editor);
|
|
3040
3079
|
}, 60);
|
|
3041
3080
|
}
|
|
3042
3081
|
|
|
@@ -13,6 +13,8 @@ import type { FileSink } from "bun";
|
|
|
13
13
|
import type { BashResult } from "../../exec/bash-executor";
|
|
14
14
|
import type { AgentSessionEvent, SessionStats } from "../../session/agent-session";
|
|
15
15
|
import type {
|
|
16
|
+
RpcAvailableCommandsUpdateFrame,
|
|
17
|
+
RpcAvailableSlashCommand,
|
|
16
18
|
RpcCommand,
|
|
17
19
|
RpcExtensionUIRequest,
|
|
18
20
|
RpcHandoffResult,
|
|
@@ -63,6 +65,7 @@ export type RpcSessionEventListener = (event: AgentSessionEvent) => void;
|
|
|
63
65
|
export type RpcSubagentLifecycleListener = (payload: RpcSubagentLifecycleFrame["payload"]) => void;
|
|
64
66
|
export type RpcSubagentProgressListener = (payload: RpcSubagentProgressFrame["payload"]) => void;
|
|
65
67
|
export type RpcSubagentEventListener = (payload: RpcSubagentEventFrame["payload"]) => void;
|
|
68
|
+
export type RpcAvailableCommandsUpdateListener = (commands: RpcAvailableSlashCommand[]) => void;
|
|
66
69
|
|
|
67
70
|
export interface RpcClientToolContext<TDetails = unknown> {
|
|
68
71
|
toolCallId: string;
|
|
@@ -161,6 +164,11 @@ function isRpcSubagentEventFrame(value: unknown): value is RpcSubagentEventFrame
|
|
|
161
164
|
return value.type === "subagent_event" && isRecord(value.payload);
|
|
162
165
|
}
|
|
163
166
|
|
|
167
|
+
function isRpcAvailableCommandsUpdateFrame(value: unknown): value is RpcAvailableCommandsUpdateFrame {
|
|
168
|
+
if (!isRecord(value)) return false;
|
|
169
|
+
return value.type === "available_commands_update" && Array.isArray(value.commands);
|
|
170
|
+
}
|
|
171
|
+
|
|
164
172
|
function isRpcHostToolCallRequest(value: unknown): value is RpcHostToolCallRequest {
|
|
165
173
|
if (!isRecord(value)) return false;
|
|
166
174
|
return (
|
|
@@ -202,6 +210,7 @@ export class RpcClient {
|
|
|
202
210
|
#subagentLifecycleListeners = new Set<RpcSubagentLifecycleListener>();
|
|
203
211
|
#subagentProgressListeners = new Set<RpcSubagentProgressListener>();
|
|
204
212
|
#subagentEventListeners = new Set<RpcSubagentEventListener>();
|
|
213
|
+
#availableCommandsUpdateListeners = new Set<RpcAvailableCommandsUpdateListener>();
|
|
205
214
|
#pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =
|
|
206
215
|
new Map();
|
|
207
216
|
#customTools: RpcClientCustomTool[] = [];
|
|
@@ -377,6 +386,14 @@ export class RpcClient {
|
|
|
377
386
|
return () => this.#subagentEventListeners.delete(listener);
|
|
378
387
|
}
|
|
379
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Subscribe to slash-command availability updates emitted by the RPC server.
|
|
391
|
+
*/
|
|
392
|
+
onAvailableCommandsUpdate(listener: RpcAvailableCommandsUpdateListener): () => void {
|
|
393
|
+
this.#availableCommandsUpdateListeners.add(listener);
|
|
394
|
+
return () => this.#availableCommandsUpdateListeners.delete(listener);
|
|
395
|
+
}
|
|
396
|
+
|
|
380
397
|
/**
|
|
381
398
|
* Get collected stderr output (useful for debugging).
|
|
382
399
|
*/
|
|
@@ -511,6 +528,14 @@ export class RpcClient {
|
|
|
511
528
|
return this.#getData<{ models: ModelInfo[] }>(response).models;
|
|
512
529
|
}
|
|
513
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Get list of available slash commands.
|
|
533
|
+
*/
|
|
534
|
+
async getAvailableCommands(): Promise<RpcAvailableSlashCommand[]> {
|
|
535
|
+
const response = await this.#send({ type: "get_available_commands" });
|
|
536
|
+
return this.#getData<{ commands: RpcAvailableSlashCommand[] }>(response).commands;
|
|
537
|
+
}
|
|
538
|
+
|
|
514
539
|
/**
|
|
515
540
|
* Set thinking level.
|
|
516
541
|
*/
|
|
@@ -825,6 +850,13 @@ export class RpcClient {
|
|
|
825
850
|
return;
|
|
826
851
|
}
|
|
827
852
|
|
|
853
|
+
if (isRpcAvailableCommandsUpdateFrame(data)) {
|
|
854
|
+
for (const listener of this.#availableCommandsUpdateListeners) {
|
|
855
|
+
listener(data.commands);
|
|
856
|
+
}
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
828
860
|
if (!isAgentSessionEvent(data)) return;
|
|
829
861
|
|
|
830
862
|
for (const listener of this.#sessionEventListeners) {
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
14
14
|
import { $env, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
|
|
15
|
+
import { reset as resetCapabilities } from "../../capability";
|
|
16
|
+
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
15
17
|
import {
|
|
16
18
|
type ExtensionUIContext,
|
|
17
19
|
type ExtensionUIDialogOptions,
|
|
@@ -19,8 +21,13 @@ import {
|
|
|
19
21
|
type ExtensionWidgetOptions,
|
|
20
22
|
getExtensionUISelectOptionLabel,
|
|
21
23
|
} from "../../extensibility/extensions";
|
|
24
|
+
import { buildSkillPromptMessage } from "../../extensibility/skills";
|
|
25
|
+
import { loadSlashCommands } from "../../extensibility/slash-commands";
|
|
22
26
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
23
27
|
import type { AgentSession } from "../../session/agent-session";
|
|
28
|
+
import { SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
|
|
29
|
+
import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
|
|
30
|
+
import { buildAvailableSlashCommands } from "../../slash-commands/available-commands";
|
|
24
31
|
import type { EventBus } from "../../utils/event-bus";
|
|
25
32
|
import { initializeExtensions } from "../runtime-init";
|
|
26
33
|
import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
|
|
@@ -70,6 +77,28 @@ export type RpcSessionChangeResult =
|
|
|
70
77
|
| { type: "branch"; data: { text: string; cancelled: boolean } };
|
|
71
78
|
|
|
72
79
|
export type RpcSessionChangeSession = Pick<AgentSession, "newSession" | "switchSession" | "branch">;
|
|
80
|
+
|
|
81
|
+
export type RpcSkillCommandSession = Pick<AgentSession, "promptCustomMessage" | "skills" | "skillsSettings">;
|
|
82
|
+
|
|
83
|
+
export async function tryRunRpcSkillCommand(session: RpcSkillCommandSession, text: string): Promise<boolean> {
|
|
84
|
+
if (!text.startsWith("/skill:")) return false;
|
|
85
|
+
if (!session.skillsSettings?.enableSkillCommands) return false;
|
|
86
|
+
const spaceIndex = text.indexOf(" ");
|
|
87
|
+
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
88
|
+
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
|
89
|
+
const skillName = commandName.slice("skill:".length);
|
|
90
|
+
const skill = session.skills.find(candidate => candidate.name === skillName);
|
|
91
|
+
if (!skill) return false;
|
|
92
|
+
const built = await buildSkillPromptMessage(skill, args);
|
|
93
|
+
await session.promptCustomMessage({
|
|
94
|
+
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
95
|
+
content: built.message,
|
|
96
|
+
display: true,
|
|
97
|
+
details: built.details,
|
|
98
|
+
attribution: "user",
|
|
99
|
+
});
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
73
102
|
export type RpcSubagentResetRegistry = Pick<RpcSubagentRegistry, "clear">;
|
|
74
103
|
|
|
75
104
|
export async function handleRpcSessionChange(
|
|
@@ -511,6 +540,24 @@ export async function runRpcMode(
|
|
|
511
540
|
output(event);
|
|
512
541
|
});
|
|
513
542
|
|
|
543
|
+
const getAvailableCommands = async () => buildAvailableSlashCommands(session);
|
|
544
|
+
const reloadPluginState = async () => {
|
|
545
|
+
const cwd = session.sessionManager.getCwd();
|
|
546
|
+
const projectPath = await resolveActiveProjectRegistryPath(cwd);
|
|
547
|
+
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
548
|
+
resetCapabilities();
|
|
549
|
+
session.setSlashCommands(await loadSlashCommands({ cwd }));
|
|
550
|
+
await session.refreshSshTool({ activateIfAvailable: true });
|
|
551
|
+
await emitAvailableCommandsUpdate();
|
|
552
|
+
};
|
|
553
|
+
const emitAvailableCommandsUpdate = async () => {
|
|
554
|
+
output({ type: "available_commands_update", commands: await getAvailableCommands() });
|
|
555
|
+
};
|
|
556
|
+
session.subscribeCommandMetadataChanged(() => {
|
|
557
|
+
void emitAvailableCommandsUpdate();
|
|
558
|
+
});
|
|
559
|
+
await emitAvailableCommandsUpdate();
|
|
560
|
+
|
|
514
561
|
// Handle a single command
|
|
515
562
|
const handleCommand = async (command: RpcCommand): Promise<RpcResponse> => {
|
|
516
563
|
const id = command.id;
|
|
@@ -521,6 +568,33 @@ export async function runRpcMode(
|
|
|
521
568
|
// =================================================================
|
|
522
569
|
|
|
523
570
|
case "prompt": {
|
|
571
|
+
if (await tryRunRpcSkillCommand(session, command.message)) {
|
|
572
|
+
return success(id, "prompt");
|
|
573
|
+
}
|
|
574
|
+
const builtinResult = await executeAcpBuiltinSlashCommand(command.message, {
|
|
575
|
+
session,
|
|
576
|
+
sessionManager: session.sessionManager,
|
|
577
|
+
settings: session.settings,
|
|
578
|
+
cwd: session.sessionManager.getCwd(),
|
|
579
|
+
output: text => output({ type: "command_output", text }),
|
|
580
|
+
refreshCommands: emitAvailableCommandsUpdate,
|
|
581
|
+
reloadPlugins: reloadPluginState,
|
|
582
|
+
notifyTitleChanged: async () => {
|
|
583
|
+
output({ type: "session_info_update", title: session.sessionName, sessionId: session.sessionId });
|
|
584
|
+
},
|
|
585
|
+
notifyConfigChanged: async () => {
|
|
586
|
+
output({ type: "config_update", model: session.model, thinkingLevel: session.thinkingLevel });
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
if (builtinResult !== false) {
|
|
590
|
+
if ("prompt" in builtinResult) {
|
|
591
|
+
session
|
|
592
|
+
.prompt(builtinResult.prompt, { images: command.images })
|
|
593
|
+
.catch(e => output(error(id, "prompt", e.message)));
|
|
594
|
+
}
|
|
595
|
+
return success(id, "prompt");
|
|
596
|
+
}
|
|
597
|
+
|
|
524
598
|
// Don't await - events will stream
|
|
525
599
|
// Extension commands are executed immediately, file prompt templates are expanded
|
|
526
600
|
// If streaming and streamingBehavior specified, queues via steer/followUp
|
|
@@ -556,8 +630,11 @@ export async function runRpcMode(
|
|
|
556
630
|
return success(id, "abort_and_prompt");
|
|
557
631
|
}
|
|
558
632
|
|
|
559
|
-
case "new_session":
|
|
633
|
+
case "new_session":
|
|
634
|
+
case "switch_session":
|
|
635
|
+
case "branch": {
|
|
560
636
|
const result = await handleRpcSessionChange(session, command, subagentRegistry);
|
|
637
|
+
if (!result.data.cancelled) await emitAvailableCommandsUpdate();
|
|
561
638
|
return success(id, result.type, result.data);
|
|
562
639
|
}
|
|
563
640
|
|
|
@@ -592,6 +669,10 @@ export async function runRpcMode(
|
|
|
592
669
|
return success(id, "get_state", state);
|
|
593
670
|
}
|
|
594
671
|
|
|
672
|
+
case "get_available_commands": {
|
|
673
|
+
return success(id, "get_available_commands", { commands: await getAvailableCommands() });
|
|
674
|
+
}
|
|
675
|
+
|
|
595
676
|
case "set_todos": {
|
|
596
677
|
session.setTodoPhases(command.phases);
|
|
597
678
|
return success(id, "set_todos", { todoPhases: session.getTodoPhases() });
|
|
@@ -770,12 +851,6 @@ export async function runRpcMode(
|
|
|
770
851
|
return success(id, "export_html", { path });
|
|
771
852
|
}
|
|
772
853
|
|
|
773
|
-
case "switch_session":
|
|
774
|
-
case "branch": {
|
|
775
|
-
const result = await handleRpcSessionChange(session, command, subagentRegistry);
|
|
776
|
-
return success(id, result.type, result.data);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
854
|
case "get_branch_messages": {
|
|
780
855
|
const messages = session.getUserMessagesForBranching();
|
|
781
856
|
return success(id, "get_branch_messages", { messages });
|
|
@@ -11,6 +11,7 @@ import type { BashResult } from "../../exec/bash-executor";
|
|
|
11
11
|
import type { ContextUsage } from "../../extensibility/extensions/types";
|
|
12
12
|
import type { AgentSessionEvent, SessionStats } from "../../session/agent-session";
|
|
13
13
|
import type { FileEntry } from "../../session/session-manager";
|
|
14
|
+
import type { AvailableSlashCommandSource } from "../../slash-commands/available-commands";
|
|
14
15
|
import type {
|
|
15
16
|
AgentProgress,
|
|
16
17
|
SubagentEventPayload,
|
|
@@ -34,6 +35,7 @@ export type RpcCommand =
|
|
|
34
35
|
|
|
35
36
|
// State
|
|
36
37
|
| { id?: string; type: "get_state" }
|
|
38
|
+
| { id?: string; type: "get_available_commands" }
|
|
37
39
|
| { id?: string; type: "set_todos"; phases: TodoPhase[] }
|
|
38
40
|
| { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
|
|
39
41
|
| { id?: string; type: "set_host_uri_schemes"; schemes: RpcHostUriSchemeDefinition[] }
|
|
@@ -110,6 +112,20 @@ export interface RpcSessionState {
|
|
|
110
112
|
contextUsage?: ContextUsage;
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
export interface RpcAvailableSlashCommand {
|
|
116
|
+
name: string;
|
|
117
|
+
aliases?: string[];
|
|
118
|
+
description?: string;
|
|
119
|
+
input?: { hint?: string };
|
|
120
|
+
subcommands?: Array<{ name: string; description?: string; usage?: string }>;
|
|
121
|
+
source: AvailableSlashCommandSource;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface RpcAvailableCommandsUpdateFrame {
|
|
125
|
+
type: "available_commands_update";
|
|
126
|
+
commands: RpcAvailableSlashCommand[];
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
export interface RpcHandoffResult {
|
|
114
130
|
savedPath?: string;
|
|
115
131
|
}
|
|
@@ -156,6 +172,13 @@ export type RpcResponse =
|
|
|
156
172
|
|
|
157
173
|
// State
|
|
158
174
|
| { id?: string; type: "response"; command: "get_state"; success: true; data: RpcSessionState }
|
|
175
|
+
| {
|
|
176
|
+
id?: string;
|
|
177
|
+
type: "response";
|
|
178
|
+
command: "get_available_commands";
|
|
179
|
+
success: true;
|
|
180
|
+
data: { commands: RpcAvailableSlashCommand[] };
|
|
181
|
+
}
|
|
159
182
|
| { id?: string; type: "response"; command: "set_todos"; success: true; data: { todoPhases: TodoPhase[] } }
|
|
160
183
|
| { id?: string; type: "response"; command: "set_host_tools"; success: true; data: { toolNames: string[] } }
|
|
161
184
|
| { id?: string; type: "response"; command: "set_host_uri_schemes"; success: true; data: { schemes: string[] } }
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -108,6 +108,7 @@ export type SymbolKey =
|
|
|
108
108
|
| "icon.time"
|
|
109
109
|
| "icon.pi"
|
|
110
110
|
| "icon.agents"
|
|
111
|
+
| "icon.job"
|
|
111
112
|
| "icon.cache"
|
|
112
113
|
| "icon.input"
|
|
113
114
|
| "icon.output"
|
|
@@ -304,6 +305,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
304
305
|
"icon.time": "⏱",
|
|
305
306
|
"icon.pi": "π",
|
|
306
307
|
"icon.agents": "👥",
|
|
308
|
+
"icon.job": "⚙",
|
|
307
309
|
"icon.cache": "💾",
|
|
308
310
|
"icon.input": "⤵",
|
|
309
311
|
"icon.output": "⤴",
|
|
@@ -567,6 +569,8 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
567
569
|
"icon.pi": "\ue22c",
|
|
568
570
|
// pick: | alt:
|
|
569
571
|
"icon.agents": "\uf0c0",
|
|
572
|
+
// pick: (nf-fa-gear) | alt: ⚙
|
|
573
|
+
"icon.job": "\uf013",
|
|
570
574
|
// pick: | alt:
|
|
571
575
|
"icon.cache": "\uf1c0",
|
|
572
576
|
// pick: | alt: →
|
|
@@ -796,6 +800,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
796
800
|
"icon.time": "t:",
|
|
797
801
|
"icon.pi": "pi",
|
|
798
802
|
"icon.agents": "AG",
|
|
803
|
+
"icon.job": "bg",
|
|
799
804
|
"icon.cache": "cache",
|
|
800
805
|
"icon.input": "in:",
|
|
801
806
|
"icon.output": "out:",
|
|
@@ -1678,6 +1683,7 @@ export class Theme {
|
|
|
1678
1683
|
time: this.#symbols["icon.time"],
|
|
1679
1684
|
pi: this.#symbols["icon.pi"],
|
|
1680
1685
|
agents: this.#symbols["icon.agents"],
|
|
1686
|
+
job: this.#symbols["icon.job"],
|
|
1681
1687
|
cache: this.#symbols["icon.cache"],
|
|
1682
1688
|
input: this.#symbols["icon.input"],
|
|
1683
1689
|
output: this.#symbols["icon.output"],
|
|
@@ -2559,10 +2565,10 @@ const HIGHLIGHT_CACHE_MAX = 256;
|
|
|
2559
2565
|
const highlightCache = new LRUCache<string, string>({ max: HIGHLIGHT_CACHE_MAX });
|
|
2560
2566
|
let highlightCacheTheme: Theme | undefined;
|
|
2561
2567
|
|
|
2562
|
-
function highlightCached(code: string, validLang: string | undefined): string | null {
|
|
2563
|
-
if (highlightCacheTheme !==
|
|
2568
|
+
function highlightCached(code: string, validLang: string | undefined, highlightTheme: Theme): string | null {
|
|
2569
|
+
if (highlightCacheTheme !== highlightTheme) {
|
|
2564
2570
|
highlightCache.clear();
|
|
2565
|
-
highlightCacheTheme =
|
|
2571
|
+
highlightCacheTheme = highlightTheme;
|
|
2566
2572
|
}
|
|
2567
2573
|
const key = `${validLang ?? ""}\x00${code}`;
|
|
2568
2574
|
const hit = highlightCache.get(key);
|
|
@@ -2571,7 +2577,7 @@ function highlightCached(code: string, validLang: string | undefined): string |
|
|
|
2571
2577
|
}
|
|
2572
2578
|
let highlighted: string;
|
|
2573
2579
|
try {
|
|
2574
|
-
highlighted = nativeHighlightCode(code, validLang, getHighlightColors(
|
|
2580
|
+
highlighted = nativeHighlightCode(code, validLang, getHighlightColors(highlightTheme));
|
|
2575
2581
|
} catch {
|
|
2576
2582
|
return null;
|
|
2577
2583
|
}
|
|
@@ -2583,9 +2589,9 @@ function highlightCached(code: string, validLang: string | undefined): string |
|
|
|
2583
2589
|
* Highlight code with syntax coloring based on file extension or language.
|
|
2584
2590
|
* Returns array of highlighted lines.
|
|
2585
2591
|
*/
|
|
2586
|
-
export function highlightCode(code: string, lang?: string): string[] {
|
|
2592
|
+
export function highlightCode(code: string, lang?: string, highlightTheme: Theme = theme): string[] {
|
|
2587
2593
|
const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
|
|
2588
|
-
const highlighted = highlightCached(code, validLang);
|
|
2594
|
+
const highlighted = highlightCached(code, validLang, highlightTheme);
|
|
2589
2595
|
// Always return a fresh array: callers (e.g. renderCodeCell) push extra lines
|
|
2590
2596
|
// onto the result, which would corrupt the cached string otherwise.
|
|
2591
2597
|
return (highlighted ?? code).split("\n");
|
|
@@ -2633,7 +2639,7 @@ export function getMarkdownTheme(): MarkdownTheme {
|
|
|
2633
2639
|
resolveMermaidAscii,
|
|
2634
2640
|
highlightCode: (code: string, lang?: string): string[] => {
|
|
2635
2641
|
const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
|
|
2636
|
-
const highlighted = highlightCached(code, validLang);
|
|
2642
|
+
const highlighted = highlightCached(code, validLang, theme);
|
|
2637
2643
|
if (highlighted !== null) return highlighted.split("\n");
|
|
2638
2644
|
return code.split("\n").map(line => theme.fg("mdCodeBlock", line));
|
|
2639
2645
|
},
|