@oh-my-pi/pi-coding-agent 15.11.1 → 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 +27 -1
- package/dist/cli.js +629 -614
- package/dist/types/config/settings-schema.d.ts +36 -0
- 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/modes/components/plan-review-overlay.d.ts +2 -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 +1 -1
- package/dist/types/session/agent-session.d.ts +17 -3
- package/dist/types/slash-commands/available-commands.d.ts +34 -0
- 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/package.json +11 -11
- package/src/config/settings-schema.ts +40 -0
- package/src/exec/bash-executor.ts +21 -6
- 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 +4 -4
- package/src/irc/bus.ts +14 -3
- package/src/lsp/defaults.json +6 -0
- package/src/lsp/render.ts +2 -28
- package/src/memories/index.ts +2 -0
- package/src/modes/acp/acp-agent.ts +4 -67
- package/src/modes/components/plan-review-overlay.ts +32 -3
- package/src/modes/controllers/streaming-reveal.ts +16 -8
- package/src/modes/interactive-mode.ts +32 -0
- 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 +7 -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/session/agent-session.ts +95 -6
- package/src/slash-commands/available-commands.ts +105 -0
- 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/irc/bus.ts
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
* AgentLifecycleManager, idle agents are woken with a real turn, and busy
|
|
8
8
|
* agents receive the message as a non-interrupting aside at the next step
|
|
9
9
|
* boundary (see AgentSession.deliverIrcMessage). Replies are real turns by
|
|
10
|
-
* the recipient, observed via `wait
|
|
10
|
+
* the recipient, observed via `wait` — with one exception: when the sender
|
|
11
|
+
* awaits a reply and the recipient is mid-turn with async execution
|
|
12
|
+
* disabled, the recipient session generates an ephemeral side-channel
|
|
13
|
+
* auto-reply (it may be blocked in a synchronous task spawn whose batch
|
|
14
|
+
* includes the sender, so a real turn could never happen in time).
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
import { logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
@@ -80,8 +84,15 @@ export class IrcBus {
|
|
|
80
84
|
* context, so buffering it too would double-deliver via a later
|
|
81
85
|
* `wait`/`inbox` and inflate unread counts. Only a failed live hand-off
|
|
82
86
|
* is buffered for the recipient to drain later.
|
|
87
|
+
*
|
|
88
|
+
* `opts.expectsReply` marks sends whose caller is blocked on an answer
|
|
89
|
+
* (`send await:true`). It is forwarded to the recipient session so a
|
|
90
|
+
* mid-turn recipient that cannot reach a step boundary (async execution
|
|
91
|
+
* disabled — e.g. blocked in a synchronous task spawn awaiting the
|
|
92
|
+
* sender's own batch) can generate an ephemeral side-channel auto-reply
|
|
93
|
+
* instead of stranding the sender until timeout.
|
|
83
94
|
*/
|
|
84
|
-
async send(msg: Omit<IrcMessage, "id" | "ts"
|
|
95
|
+
async send(msg: Omit<IrcMessage, "id" | "ts">, opts?: { expectsReply?: boolean }): Promise<IrcDeliveryReceipt> {
|
|
85
96
|
const message: IrcMessage = { ...msg, id: Snowflake.next(), ts: Date.now() };
|
|
86
97
|
const ref = this.#registry.get(message.to);
|
|
87
98
|
if (!ref || ref.status === "aborted") {
|
|
@@ -118,7 +129,7 @@ export class IrcBus {
|
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
try {
|
|
121
|
-
const delivery = await session.deliverIrcMessage(message);
|
|
132
|
+
const delivery = await session.deliverIrcMessage(message, opts);
|
|
122
133
|
this.#relayToMainUi(message);
|
|
123
134
|
return { to: message.to, outcome: revived ? "revived" : delivery };
|
|
124
135
|
} catch (error) {
|
package/src/lsp/defaults.json
CHANGED
|
@@ -248,6 +248,12 @@
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
},
|
|
251
|
+
"expert": {
|
|
252
|
+
"command": "expert",
|
|
253
|
+
"args": ["--stdio"],
|
|
254
|
+
"fileTypes": [".ex", ".exs", ".heex", ".eex"],
|
|
255
|
+
"rootMarkers": ["mix.exs", "mix.lock"]
|
|
256
|
+
},
|
|
251
257
|
"erlangls": {
|
|
252
258
|
"command": "erlang_ls",
|
|
253
259
|
"args": [],
|
package/src/lsp/render.ts
CHANGED
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
* - Collapsible/expandable views
|
|
9
9
|
*/
|
|
10
10
|
import type { RenderResultOptions } from "@oh-my-pi/pi-agent-core";
|
|
11
|
-
import { type HighlightColors, highlightCode as nativeHighlightCode, supportsLanguage } from "@oh-my-pi/pi-natives";
|
|
12
11
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
13
|
-
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
12
|
+
import { getLanguageFromPath, highlightCode as highlightThemeCode, type Theme } from "../modes/theme/theme";
|
|
14
13
|
import {
|
|
15
14
|
formatExpandHint,
|
|
16
15
|
formatMoreItems,
|
|
@@ -219,7 +218,7 @@ function renderHover(
|
|
|
219
218
|
const beforeCode = fullText.slice(0, codeStart).trimEnd();
|
|
220
219
|
const afterCode = fullText.slice(fullText.indexOf("```", 3) + 3).trim();
|
|
221
220
|
|
|
222
|
-
const codeLines =
|
|
221
|
+
const codeLines = highlightThemeCode(code, lang, theme);
|
|
223
222
|
const icon = theme.styledSymbol("status.info", "accent");
|
|
224
223
|
const langLabel = lang ? theme.fg("mdCodeBlockBorder", ` ${lang}`) : "";
|
|
225
224
|
|
|
@@ -274,31 +273,6 @@ function renderHover(
|
|
|
274
273
|
return output.split("\n");
|
|
275
274
|
}
|
|
276
275
|
|
|
277
|
-
/**
|
|
278
|
-
* Syntax highlight code using native highlighter.
|
|
279
|
-
*/
|
|
280
|
-
function highlightCode(codeText: string, language: string, theme: Theme): string[] {
|
|
281
|
-
const validLang = language && supportsLanguage(language) ? language : undefined;
|
|
282
|
-
try {
|
|
283
|
-
const colors: HighlightColors = {
|
|
284
|
-
comment: theme.getFgAnsi("syntaxComment"),
|
|
285
|
-
keyword: theme.getFgAnsi("syntaxKeyword"),
|
|
286
|
-
function: theme.getFgAnsi("syntaxFunction"),
|
|
287
|
-
variable: theme.getFgAnsi("syntaxVariable"),
|
|
288
|
-
string: theme.getFgAnsi("syntaxString"),
|
|
289
|
-
number: theme.getFgAnsi("syntaxNumber"),
|
|
290
|
-
type: theme.getFgAnsi("syntaxType"),
|
|
291
|
-
operator: theme.getFgAnsi("syntaxOperator"),
|
|
292
|
-
punctuation: theme.getFgAnsi("syntaxPunctuation"),
|
|
293
|
-
inserted: theme.getFgAnsi("toolDiffAdded"),
|
|
294
|
-
deleted: theme.getFgAnsi("toolDiffRemoved"),
|
|
295
|
-
};
|
|
296
|
-
return nativeHighlightCode(codeText, validLang, colors).split("\n");
|
|
297
|
-
} catch {
|
|
298
|
-
return codeText.split("\n");
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
276
|
// =============================================================================
|
|
303
277
|
// Diagnostics Rendering
|
|
304
278
|
// =============================================================================
|
package/src/memories/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { ModelRegistry } from "../config/model-registry";
|
|
|
11
11
|
import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model-resolver";
|
|
12
12
|
import type { Settings } from "../config/settings";
|
|
13
13
|
import consolidationTemplate from "../prompts/memories/consolidation.md" with { type: "text" };
|
|
14
|
+
import consolidationSystemTemplate from "../prompts/memories/consolidation_system.md" with { type: "text" };
|
|
14
15
|
import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
|
|
15
16
|
import stageOneInputTemplate from "../prompts/memories/stage_one_input.md" with { type: "text" };
|
|
16
17
|
import stageOneSystemTemplate from "../prompts/memories/stage_one_system.md" with { type: "text" };
|
|
@@ -752,6 +753,7 @@ async function runConsolidationModel(options: {
|
|
|
752
753
|
const response = await completeSimple(
|
|
753
754
|
model,
|
|
754
755
|
{
|
|
756
|
+
systemPrompt: [consolidationSystemTemplate],
|
|
755
757
|
messages: [{ role: "user", content: [{ type: "text", text: input }], timestamp: Date.now() }],
|
|
756
758
|
},
|
|
757
759
|
{
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
} from "../../extensibility/extensions";
|
|
57
57
|
import { runExtensionCompact } from "../../extensibility/extensions/compact-handler";
|
|
58
58
|
import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
|
|
59
|
-
import { buildSkillPromptMessage
|
|
59
|
+
import { buildSkillPromptMessage } from "../../extensibility/skills";
|
|
60
60
|
import { loadSlashCommands } from "../../extensibility/slash-commands";
|
|
61
61
|
import { resolveLocalUrlToPath } from "../../internal-urls";
|
|
62
62
|
import { MCPManager } from "../../mcp/manager";
|
|
@@ -71,12 +71,8 @@ import {
|
|
|
71
71
|
type SessionInfo as StoredSessionInfo,
|
|
72
72
|
type UsageStatistics,
|
|
73
73
|
} from "../../session/session-manager";
|
|
74
|
-
import {
|
|
75
|
-
|
|
76
|
-
ACP_BUILTIN_SLASH_COMMANDS,
|
|
77
|
-
executeAcpBuiltinSlashCommand,
|
|
78
|
-
isAcpBuiltinShadowedName,
|
|
79
|
-
} from "../../slash-commands/acp-builtins";
|
|
74
|
+
import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
|
|
75
|
+
import { buildAvailableSlashCommands, toAcpAvailableCommands } from "../../slash-commands/available-commands";
|
|
80
76
|
import { AUTO_THINKING, parseConfiguredThinkingLevel } from "../../thinking";
|
|
81
77
|
import { normalizeLocalScheme } from "../../tools/path-utils";
|
|
82
78
|
import { runResolveInvocation } from "../../tools/resolve";
|
|
@@ -1662,66 +1658,7 @@ export class AcpAgent implements Agent {
|
|
|
1662
1658
|
}
|
|
1663
1659
|
|
|
1664
1660
|
async #buildAvailableCommands(session: AgentSession): Promise<AvailableCommand[]> {
|
|
1665
|
-
|
|
1666
|
-
const seenNames = new Set<string>();
|
|
1667
|
-
const appendCommand = (command: AvailableCommand): void => {
|
|
1668
|
-
if (seenNames.has(command.name)) {
|
|
1669
|
-
return;
|
|
1670
|
-
}
|
|
1671
|
-
seenNames.add(command.name);
|
|
1672
|
-
commands.push(command);
|
|
1673
|
-
};
|
|
1674
|
-
|
|
1675
|
-
// Advertise in the order dispatch resolves them (mirrors AgentSession
|
|
1676
|
-
// dispatch: builtins → skills → extensions → custom TS → file-based).
|
|
1677
|
-
// `appendCommand` dedupes by name so earlier entries win; extension
|
|
1678
|
-
// commands therefore correctly shadow custom TS commands of the same
|
|
1679
|
-
// name, matching the runtime behaviour of #tryExecuteExtensionCommand
|
|
1680
|
-
// running before #tryExecuteCustomCommand.
|
|
1681
|
-
for (const command of ACP_BUILTIN_SLASH_COMMANDS) {
|
|
1682
|
-
appendCommand(command);
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
if (session.skillsSettings?.enableSkillCommands) {
|
|
1686
|
-
for (const skill of session.skills) {
|
|
1687
|
-
appendCommand({
|
|
1688
|
-
name: getSkillSlashCommandName(skill),
|
|
1689
|
-
description: skill.description || `Run ${skill.name} skill`,
|
|
1690
|
-
input: { hint: "arguments" },
|
|
1691
|
-
});
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
for (const command of session.extensionRunner?.getRegisteredCommands(ACP_BUILTIN_RESERVED_NAMES) ?? []) {
|
|
1696
|
-
// Reserved-set filtering in getRegisteredCommands only covers exact
|
|
1697
|
-
// names; colon-namespaced names whose prefix is a builtin (e.g.
|
|
1698
|
-
// `model:foo`) would still dispatch to the builtin in ACP.
|
|
1699
|
-
if (isAcpBuiltinShadowedName(command.name)) {
|
|
1700
|
-
continue;
|
|
1701
|
-
}
|
|
1702
|
-
appendCommand({
|
|
1703
|
-
name: command.name,
|
|
1704
|
-
description: command.description ?? "(extension command)",
|
|
1705
|
-
input: { hint: "arguments" },
|
|
1706
|
-
});
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
for (const command of session.customCommands) {
|
|
1710
|
-
appendCommand({
|
|
1711
|
-
name: command.command.name,
|
|
1712
|
-
description: command.command.description,
|
|
1713
|
-
input: { hint: "arguments" },
|
|
1714
|
-
});
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
for (const command of await loadSlashCommands({ cwd: session.sessionManager.getCwd() })) {
|
|
1718
|
-
appendCommand({
|
|
1719
|
-
name: command.name,
|
|
1720
|
-
description: command.description,
|
|
1721
|
-
});
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
return commands;
|
|
1661
|
+
return toAcpAvailableCommands(await buildAvailableSlashCommands(session));
|
|
1725
1662
|
}
|
|
1726
1663
|
|
|
1727
1664
|
#toSessionInfo(session: StoredSessionInfo): SessionInfo {
|
|
@@ -83,6 +83,8 @@ export interface PlanReviewOverlayCallbacks {
|
|
|
83
83
|
onCancel: () => void;
|
|
84
84
|
/** Invoked when the external-editor key is pressed (overlay stays open). */
|
|
85
85
|
onExternalEditor?: () => void;
|
|
86
|
+
/** Invoked when the external-editor key edits the active annotation draft. */
|
|
87
|
+
onAnnotationExternalEditor?: (draft: string, commit: (text: string | null) => void) => void;
|
|
86
88
|
/** Invoked with the new full plan text after an in-overlay delete/undo. */
|
|
87
89
|
onPlanEdited?: (content: string) => void;
|
|
88
90
|
/** Invoked with the Refine feedback markdown whenever annotations change. */
|
|
@@ -282,6 +284,12 @@ export class PlanReviewOverlay implements Component {
|
|
|
282
284
|
handleInput(keyData: string): void {
|
|
283
285
|
if (keyData.startsWith("\x1b[<") && this.#handleMouse(keyData)) return;
|
|
284
286
|
if (this.#annotating) {
|
|
287
|
+
if (this.callbacks.onAnnotationExternalEditor && matchesAppExternalEditor(keyData)) {
|
|
288
|
+
this.callbacks.onAnnotationExternalEditor(this.#input.getValue(), text => {
|
|
289
|
+
if (text !== null) this.#submitAnnotation(text);
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
285
293
|
this.#input.handleInput(keyData);
|
|
286
294
|
return;
|
|
287
295
|
}
|
|
@@ -603,11 +611,23 @@ export class PlanReviewOverlay implements Component {
|
|
|
603
611
|
}
|
|
604
612
|
for (const section of annotated) {
|
|
605
613
|
feedback += `\n## ${section.title}\n`;
|
|
606
|
-
for (const note of section.annotations) feedback +=
|
|
614
|
+
for (const note of section.annotations) feedback += this.#formatAnnotationFeedback(note);
|
|
607
615
|
}
|
|
608
616
|
this.callbacks.onFeedbackChange?.(feedback);
|
|
609
617
|
}
|
|
610
618
|
|
|
619
|
+
#formatAnnotationFeedback(note: string): string {
|
|
620
|
+
if (!note.includes("\n")) return `- ${note}\n`;
|
|
621
|
+
const fence = this.#markdownFenceFor(note);
|
|
622
|
+
return `${fence}md\n${note}\n${fence}\n`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#markdownFenceFor(text: string): string {
|
|
626
|
+
let fence = "```";
|
|
627
|
+
while (text.includes(fence)) fence += "`";
|
|
628
|
+
return fence;
|
|
629
|
+
}
|
|
630
|
+
|
|
611
631
|
#renderSliderLines(): string[] {
|
|
612
632
|
const slider = this.#slider;
|
|
613
633
|
if (!slider) return [];
|
|
@@ -676,7 +696,14 @@ export class PlanReviewOverlay implements Component {
|
|
|
676
696
|
if (section.level >= 1 && section.annotations.length > 0 && rendered.length > 0) {
|
|
677
697
|
lines.push(rendered[0]!);
|
|
678
698
|
for (const note of section.annotations) {
|
|
679
|
-
|
|
699
|
+
const noteLines = note.split(/\r?\n/);
|
|
700
|
+
for (let j = 0; j < noteLines.length; j++) {
|
|
701
|
+
const prefix =
|
|
702
|
+
j === 0
|
|
703
|
+
? `${theme.fg("warning", "▎ ")}${theme.fg("dim", "note: ")}`
|
|
704
|
+
: `${theme.fg("warning", "▎ ")}${theme.fg("dim", " ")}`;
|
|
705
|
+
lines.push(`${prefix}${theme.fg("accent", noteLines[j] ?? "")}`);
|
|
706
|
+
}
|
|
680
707
|
}
|
|
681
708
|
for (let k = 1; k < rendered.length; k++) lines.push(rendered[k]!);
|
|
682
709
|
} else {
|
|
@@ -749,7 +776,9 @@ export class PlanReviewOverlay implements Component {
|
|
|
749
776
|
const section = this.#sections[this.#toc[this.#tocCursor]!];
|
|
750
777
|
const title = section?.title ?? "";
|
|
751
778
|
const caption = `${theme.fg("dim", "Annotate")} ${theme.fg("accent", `‹${title}›`)}`;
|
|
752
|
-
|
|
779
|
+
const hintParts = ["enter save", "esc cancel"];
|
|
780
|
+
if (this.#externalEditorLabel) hintParts.push(`${this.#externalEditorLabel} editor`);
|
|
781
|
+
return [caption, this.#input.render(innerWidth)[0] ?? "", theme.fg("dim", hintParts.join(" · "))];
|
|
753
782
|
}
|
|
754
783
|
return [theme.fg("dim", this.#buildHelp())];
|
|
755
784
|
}
|
|
@@ -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) {
|
|
@@ -1785,6 +1785,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1785
1785
|
onPick: choice => finish(choice),
|
|
1786
1786
|
onCancel: () => finish(undefined),
|
|
1787
1787
|
onExternalEditor: dialogOptions?.onExternalEditor,
|
|
1788
|
+
onAnnotationExternalEditor: (draft, commit) => void this.#openPlanAnnotationInExternalEditor(draft, commit),
|
|
1788
1789
|
onPlanEdited: dialogOptions?.onPlanEdited,
|
|
1789
1790
|
onFeedbackChange: dialogOptions?.onFeedbackChange,
|
|
1790
1791
|
},
|
|
@@ -1899,6 +1900,37 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1899
1900
|
}
|
|
1900
1901
|
}
|
|
1901
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
|
+
|
|
1902
1934
|
async #applyPlanExecutionModel(entry: ResolvedRoleModel | undefined): Promise<void> {
|
|
1903
1935
|
if (!entry) return;
|
|
1904
1936
|
try {
|
|
@@ -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 });
|