@oh-my-pi/pi-coding-agent 16.2.9 → 16.2.11
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 +29 -0
- package/dist/cli.js +3178 -3177
- package/dist/types/extensibility/skills.d.ts +29 -0
- package/dist/types/modes/components/todo-reminder.d.ts +3 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +0 -1
- package/dist/types/modes/skill-command.d.ts +1 -1
- package/dist/types/modes/types.d.ts +0 -1
- package/dist/types/stt/asr-client.d.ts +7 -3
- package/package.json +14 -13
- package/scripts/bundle-dist.ts +23 -4
- package/scripts/generate-docs-index.ts +116 -24
- package/src/async/job-manager.ts +27 -3
- package/src/cli/grep-cli.ts +1 -1
- package/src/config/model-discovery.ts +118 -76
- package/src/extensibility/skills.ts +77 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/lsp/config.ts +17 -3
- package/src/modes/acp/acp-agent.ts +6 -9
- package/src/modes/components/todo-reminder.ts +5 -1
- package/src/modes/controllers/event-controller.ts +40 -24
- package/src/modes/controllers/selector-controller.ts +57 -35
- package/src/modes/controllers/tool-args-reveal.ts +12 -0
- package/src/modes/interactive-mode.ts +0 -6
- package/src/modes/rpc/rpc-mode.ts +5 -8
- package/src/modes/skill-command.ts +8 -20
- package/src/modes/types.ts +0 -1
- package/src/stt/asr-client.ts +87 -27
- package/src/stt/downloader.ts +8 -2
- package/src/task/executor.ts +1 -3
- package/src/task/index.ts +14 -5
- package/src/tools/ast-grep.ts +34 -12
- package/src/tools/grep.ts +11 -8
- package/src/utils/git.ts +22 -1
package/src/lsp/config.ts
CHANGED
|
@@ -209,13 +209,27 @@ export function hasRootMarkers(cwd: string, markers: string[]): boolean {
|
|
|
209
209
|
* Local bin directories to check before $PATH, ordered by priority.
|
|
210
210
|
* Each entry maps a root marker to the bin directory to check.
|
|
211
211
|
*/
|
|
212
|
+
const PYTHON_ROOT_MARKERS = [
|
|
213
|
+
"pyproject.toml",
|
|
214
|
+
"requirements.txt",
|
|
215
|
+
"setup.py",
|
|
216
|
+
"setup.cfg",
|
|
217
|
+
"Pipfile",
|
|
218
|
+
"pyrightconfig.json",
|
|
219
|
+
"ruff.toml",
|
|
220
|
+
".ruff.toml",
|
|
221
|
+
];
|
|
222
|
+
|
|
212
223
|
const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
|
|
213
224
|
// Node.js - check node_modules/.bin/
|
|
214
225
|
{ markers: ["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"], binDir: "node_modules/.bin" },
|
|
215
226
|
// Python - check virtual environment bin directories
|
|
216
|
-
{ markers:
|
|
217
|
-
{ markers:
|
|
218
|
-
{ markers:
|
|
227
|
+
{ markers: PYTHON_ROOT_MARKERS, binDir: ".venv/bin" },
|
|
228
|
+
{ markers: PYTHON_ROOT_MARKERS, binDir: ".venv/Scripts" },
|
|
229
|
+
{ markers: PYTHON_ROOT_MARKERS, binDir: "venv/bin" },
|
|
230
|
+
{ markers: PYTHON_ROOT_MARKERS, binDir: "venv/Scripts" },
|
|
231
|
+
{ markers: PYTHON_ROOT_MARKERS, binDir: ".env/bin" },
|
|
232
|
+
{ markers: PYTHON_ROOT_MARKERS, binDir: ".env/Scripts" },
|
|
219
233
|
// Ruby - check vendor bundle and binstubs
|
|
220
234
|
{ markers: ["Gemfile", "Gemfile.lock"], binDir: "vendor/bundle/bin" },
|
|
221
235
|
{ markers: ["Gemfile", "Gemfile.lock"], binDir: "bin" },
|
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
} from "../../extensibility/extensions";
|
|
54
54
|
import { runExtensionCompact } from "../../extensibility/extensions/compact-handler";
|
|
55
55
|
import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
|
|
56
|
-
import { buildSkillPromptMessage } from "../../extensibility/skills";
|
|
56
|
+
import { buildSkillPromptMessage, parseSkillInvocation } from "../../extensibility/skills";
|
|
57
57
|
import { loadSlashCommands } from "../../extensibility/slash-commands";
|
|
58
58
|
import { resolveLocalUrlToPath } from "../../internal-urls";
|
|
59
59
|
import { MCPManager } from "../../mcp/manager";
|
|
@@ -831,21 +831,18 @@ export class AcpAgent implements Agent {
|
|
|
831
831
|
}
|
|
832
832
|
|
|
833
833
|
async #tryRunSkillCommand(record: ManagedSessionRecord, text: string): Promise<boolean> {
|
|
834
|
-
if (!
|
|
834
|
+
if (!record.session.skillsSettings?.enableSkillCommands) {
|
|
835
835
|
return false;
|
|
836
836
|
}
|
|
837
|
-
|
|
837
|
+
const parsed = parseSkillInvocation(text);
|
|
838
|
+
if (!parsed) {
|
|
838
839
|
return false;
|
|
839
840
|
}
|
|
840
|
-
const
|
|
841
|
-
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
842
|
-
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
|
843
|
-
const skillName = commandName.slice("skill:".length);
|
|
844
|
-
const skill = record.session.skills.find(candidate => candidate.name === skillName);
|
|
841
|
+
const skill = record.session.skills.find(candidate => candidate.name === parsed.name);
|
|
845
842
|
if (!skill) {
|
|
846
843
|
return false;
|
|
847
844
|
}
|
|
848
|
-
const built = await buildSkillPromptMessage(skill, args);
|
|
845
|
+
const built = await buildSkillPromptMessage(skill, parsed.args);
|
|
849
846
|
await record.session.promptCustomMessage({
|
|
850
847
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
851
848
|
content: built.message,
|
|
@@ -3,7 +3,9 @@ import { theme } from "../../modes/theme/theme";
|
|
|
3
3
|
import type { TodoItem } from "../../tools/todo";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Component that renders a todo completion reminder notification
|
|
6
|
+
* Component that renders a todo completion reminder notification, committed into
|
|
7
|
+
* the transcript like a TTSR notification so it stays anchored in history rather
|
|
8
|
+
* than floating above the editor.
|
|
7
9
|
* Shows when the agent stops with incomplete todos.
|
|
8
10
|
*/
|
|
9
11
|
export class TodoReminderComponent extends Container {
|
|
@@ -16,6 +18,8 @@ export class TodoReminderComponent extends Container {
|
|
|
16
18
|
) {
|
|
17
19
|
super();
|
|
18
20
|
|
|
21
|
+
this.addChild(new Spacer(1));
|
|
22
|
+
|
|
19
23
|
this.#box = new Box(1, 1, t => theme.inverse(theme.fg("warning", t)));
|
|
20
24
|
this.#box.setIgnoreTight(true);
|
|
21
25
|
this.addChild(this.#box);
|
|
@@ -72,6 +72,11 @@ export class EventController {
|
|
|
72
72
|
// emits one read per completion — does not break it, so a run of consecutive
|
|
73
73
|
// reads collapses into one group even across completion boundaries.
|
|
74
74
|
#lastVisibleBlockCount = 0;
|
|
75
|
+
// Bumped on each assistant message_start. Scopes the placeholder key for a
|
|
76
|
+
// not-yet-id'd streamed tool block (see the toolCall loop) so a sealed,
|
|
77
|
+
// undeleted pending entry from an aborted message can never be reused by the
|
|
78
|
+
// next message's same content index.
|
|
79
|
+
#streamTurnNonce = 0;
|
|
75
80
|
#renderedCustomMessages = new Set<string>();
|
|
76
81
|
#lastIntent: string | undefined = undefined;
|
|
77
82
|
#backgroundToolCallIds = new Set<string>();
|
|
@@ -401,6 +406,7 @@ export class EventController {
|
|
|
401
406
|
this.ctx.ui.requestRender();
|
|
402
407
|
} else if (event.message.role === "assistant") {
|
|
403
408
|
this.#lastVisibleBlockCount = 0;
|
|
409
|
+
this.#streamTurnNonce++;
|
|
404
410
|
this.ctx.streamingComponent = createAssistantMessageComponent(this.ctx);
|
|
405
411
|
this.ctx.streamingMessage = event.message;
|
|
406
412
|
this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
|
|
@@ -607,18 +613,14 @@ export class EventController {
|
|
|
607
613
|
if (this.ctx.streamingMessage.content.some(content => content.type === "toolCall")) {
|
|
608
614
|
this.ctx.streamingComponent.markTranscriptBlockFinalized();
|
|
609
615
|
}
|
|
610
|
-
for (const content of this.ctx.streamingMessage.content) {
|
|
616
|
+
for (const [contentIndex, content] of this.ctx.streamingMessage.content.entries()) {
|
|
611
617
|
if (content.type !== "toolCall") continue;
|
|
612
|
-
// Anthropic/OpenAI open a streamed tool block with an empty id (and
|
|
613
|
-
// `{}` args) before the id/arguments arrive; Gemini assembles the
|
|
614
|
-
// whole call first, so it never hits this. Keying `pendingTools` by
|
|
615
|
-
// "" would create a placeholder card, and the later real-id frame —
|
|
616
|
-
// `pendingTools.has(realId)` false — would create a SECOND card,
|
|
617
|
-
// orphaning the blank one (no `tool_execution_*` event ever carries
|
|
618
|
-
// "", so it is never matched, updated, or removed). Defer until the
|
|
619
|
-
// provider assigns the real id.
|
|
620
|
-
if (!content.id) continue;
|
|
621
618
|
if (content.name === "read") {
|
|
619
|
+
// Read groups key by the real id (one group spans several reads),
|
|
620
|
+
// and the owned-dialect parser delivers id + args together when
|
|
621
|
+
// the call closes, so a parseable target without an id never
|
|
622
|
+
// occurs here. Defer if it somehow does.
|
|
623
|
+
if (!content.id) continue;
|
|
622
624
|
if (!readArgsHaveTarget(content.arguments)) {
|
|
623
625
|
// Args still streaming — defer until path is parseable so we can route to the
|
|
624
626
|
// read group (regular files) vs ToolExecutionComponent (internal URLs).
|
|
@@ -641,6 +643,26 @@ export class EventController {
|
|
|
641
643
|
// Internal URL read falls through to ToolExecutionComponent below.
|
|
642
644
|
}
|
|
643
645
|
|
|
646
|
+
// The owned-dialect tool parser (text-based tool calls for OAuth
|
|
647
|
+
// Anthropic / OpenAI) appends a tool block when it detects the call
|
|
648
|
+
// opening and only fills `id` + arguments once the call's text
|
|
649
|
+
// closes — so the live preview must stream while `content.id` is "".
|
|
650
|
+
// Key the preview by stable content position until the id lands,
|
|
651
|
+
// then migrate the pending entry + reveal state onto the real id
|
|
652
|
+
// (so `tool_execution_*`, which always carries the real id, matches
|
|
653
|
+
// the same component instead of orphaning a blank card). Native
|
|
654
|
+
// structured tool calls — every Gemini call, and Anthropic/OpenAI
|
|
655
|
+
// function calls — carry the id from the first frame, so
|
|
656
|
+
// `pendingKey === content.id` throughout and the migration no-ops.
|
|
657
|
+
const streamKey = `\u0000stream:${this.#streamTurnNonce}:${contentIndex}`;
|
|
658
|
+
const pendingKey = content.id || streamKey;
|
|
659
|
+
if (content.id && this.ctx.pendingTools.has(streamKey)) {
|
|
660
|
+
const migrated = this.ctx.pendingTools.get(streamKey);
|
|
661
|
+
this.ctx.pendingTools.delete(streamKey);
|
|
662
|
+
if (migrated) this.ctx.pendingTools.set(pendingKey, migrated);
|
|
663
|
+
this.#toolArgsReveal.rekey(streamKey, pendingKey);
|
|
664
|
+
}
|
|
665
|
+
|
|
644
666
|
// Preserve the raw partial JSON only for renderers that need to surface fields before the JSON object closes.
|
|
645
667
|
// Bash uses this to show inline env assignments during streaming instead of popping them in at completion.
|
|
646
668
|
// While the JSON is still open, ToolArgsRevealController paces the
|
|
@@ -652,16 +674,16 @@ export class EventController {
|
|
|
652
674
|
const rawInput = content.customWireName !== undefined;
|
|
653
675
|
const tool = this.ctx.viewSession.getToolByName(content.name);
|
|
654
676
|
if (partialJson) {
|
|
655
|
-
renderArgs = this.#toolArgsReveal.setTarget(
|
|
677
|
+
renderArgs = this.#toolArgsReveal.setTarget(pendingKey, partialJson, {
|
|
656
678
|
rawInput,
|
|
657
679
|
exposeRawPartialJson: exposesRawPartialJson(content.name, rawInput, tool),
|
|
658
680
|
fullArgs: content.arguments,
|
|
659
681
|
});
|
|
660
682
|
} else {
|
|
661
|
-
this.#toolArgsReveal.finish(
|
|
683
|
+
this.#toolArgsReveal.finish(pendingKey);
|
|
662
684
|
renderArgs = content.arguments;
|
|
663
685
|
}
|
|
664
|
-
if (!this.ctx.pendingTools.has(
|
|
686
|
+
if (!this.ctx.pendingTools.has(pendingKey)) {
|
|
665
687
|
this.#resolveDisplaceablePoll(content.name);
|
|
666
688
|
this.#resetReadGroup();
|
|
667
689
|
const component = new ToolExecutionComponent(
|
|
@@ -680,13 +702,13 @@ export class EventController {
|
|
|
680
702
|
);
|
|
681
703
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
682
704
|
this.ctx.chatContainer.addChild(component);
|
|
683
|
-
this.ctx.pendingTools.set(
|
|
684
|
-
this.#toolArgsReveal.bind(
|
|
705
|
+
this.ctx.pendingTools.set(pendingKey, component);
|
|
706
|
+
this.#toolArgsReveal.bind(pendingKey, component);
|
|
685
707
|
} else {
|
|
686
|
-
const component = this.ctx.pendingTools.get(
|
|
708
|
+
const component = this.ctx.pendingTools.get(pendingKey);
|
|
687
709
|
if (component) {
|
|
688
710
|
component.updateArgs(renderArgs, content.id);
|
|
689
|
-
this.#toolArgsReveal.bind(
|
|
711
|
+
this.#toolArgsReveal.bind(pendingKey, component);
|
|
690
712
|
}
|
|
691
713
|
}
|
|
692
714
|
}
|
|
@@ -978,13 +1000,9 @@ export class EventController {
|
|
|
978
1000
|
}
|
|
979
1001
|
// Update todo display when todo tool completes
|
|
980
1002
|
if (event.toolName === "todo" && !event.isError) {
|
|
981
|
-
const hadTodoReminder = (this.ctx.todoReminderContainer?.children.length ?? 0) > 0;
|
|
982
|
-
this.ctx.todoReminderContainer?.clear();
|
|
983
1003
|
const details = event.result.details as { phases?: TodoPhase[] } | undefined;
|
|
984
1004
|
if (details?.phases) {
|
|
985
1005
|
this.ctx.setTodos(details.phases);
|
|
986
|
-
} else if (hadTodoReminder) {
|
|
987
|
-
this.ctx.ui.requestRender();
|
|
988
1006
|
}
|
|
989
1007
|
} else if (event.toolName === "todo" && event.isError) {
|
|
990
1008
|
const textContent = event.result.content.find(
|
|
@@ -1281,9 +1299,7 @@ export class EventController {
|
|
|
1281
1299
|
|
|
1282
1300
|
async #handleTodoReminder(event: Extract<AgentSessionEvent, { type: "todo_reminder" }>): Promise<void> {
|
|
1283
1301
|
const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts);
|
|
1284
|
-
this.ctx.
|
|
1285
|
-
this.ctx.todoReminderContainer.addChild(component);
|
|
1286
|
-
this.ctx.ui.requestRender();
|
|
1302
|
+
this.ctx.present(component);
|
|
1287
1303
|
}
|
|
1288
1304
|
|
|
1289
1305
|
async #handleTodoAutoClear(_event: Extract<AgentSessionEvent, { type: "todo_auto_clear" }>): Promise<void> {
|
|
@@ -962,43 +962,65 @@ export class SelectorController {
|
|
|
962
962
|
// every project's history when the cwd has nothing to resume. See #3099.
|
|
963
963
|
const historyStorage = this.ctx.historyStorage;
|
|
964
964
|
const historyMatcher = historyStorage ? (query: string) => historyStorage.matchingSessionIds(query) : undefined;
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
965
|
+
// Fullscreen session picker on the alternate screen (the /settings idiom):
|
|
966
|
+
// the overlay borrows the alt buffer and enables mouse tracking (wheel
|
|
967
|
+
// scroll + click-to-resume) for its lifetime, leaving the transcript
|
|
968
|
+
// untouched underneath. Anchored top-left at full size so a mouse row maps
|
|
969
|
+
// directly to a rendered line (the overlay paints from screen row 0), and
|
|
970
|
+
// `fillHeight` pads the body so the footer pins to the screen bottom.
|
|
971
|
+
let overlayHandle: OverlayHandle | undefined;
|
|
972
|
+
const done = () => {
|
|
973
|
+
overlayHandle?.hide();
|
|
974
|
+
this.focusActiveEditorArea();
|
|
975
|
+
this.ctx.ui.requestRender();
|
|
976
|
+
};
|
|
977
|
+
const selector = new SessionSelectorComponent(
|
|
978
|
+
sessions,
|
|
979
|
+
async (session: SessionInfo) => {
|
|
980
|
+
done();
|
|
981
|
+
await this.handleResumeSession(session.path);
|
|
982
|
+
},
|
|
983
|
+
() => {
|
|
984
|
+
done();
|
|
985
|
+
},
|
|
986
|
+
() => {
|
|
987
|
+
// Release the alt buffer before teardown: shutdown() awaits flush/save/
|
|
988
|
+
// dispose/drain before stop() leaves the alt screen, so without this the
|
|
989
|
+
// fullscreen picker would freeze on screen for that window on Ctrl+C.
|
|
990
|
+
done();
|
|
991
|
+
void this.ctx.shutdown();
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
onDelete: async (session: SessionInfo) => {
|
|
995
|
+
if (!(await this.#detachActiveSessionBeforeDeletion(session.path))) {
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
const storage = new FileSessionStorage();
|
|
999
|
+
try {
|
|
1000
|
+
await storage.deleteSessionWithArtifacts(session.path);
|
|
1001
|
+
return true;
|
|
1002
|
+
} catch (err) {
|
|
1003
|
+
throw new Error(`Failed to delete session: ${err instanceof Error ? err.message : String(err)}`, {
|
|
1004
|
+
cause: err,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
997
1007
|
},
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1008
|
+
historyMatcher,
|
|
1009
|
+
loadAllSessions: () => SessionManager.listAll(),
|
|
1010
|
+
getTerminalRows: () => this.ctx.ui.terminal.rows,
|
|
1011
|
+
fillHeight: true,
|
|
1012
|
+
},
|
|
1013
|
+
);
|
|
1014
|
+
selector.setOnRequestRender(() => this.ctx.ui.requestRender());
|
|
1015
|
+
overlayHandle = this.ctx.ui.showOverlay(selector, {
|
|
1016
|
+
anchor: "top-left",
|
|
1017
|
+
width: "100%",
|
|
1018
|
+
maxHeight: "100%",
|
|
1019
|
+
margin: 0,
|
|
1020
|
+
fullscreen: true,
|
|
1001
1021
|
});
|
|
1022
|
+
this.ctx.ui.setFocus(selector);
|
|
1023
|
+
this.ctx.ui.requestRender();
|
|
1002
1024
|
}
|
|
1003
1025
|
|
|
1004
1026
|
#refreshSessionTerminalTitle(): void {
|
|
@@ -177,6 +177,18 @@ export class ToolArgsRevealController {
|
|
|
177
177
|
if (entry) entry.component = component;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/** Migrate a live reveal entry from a placeholder key onto the real
|
|
181
|
+
* tool-call id once the owned-dialect parser assigns it. No-op when no
|
|
182
|
+
* entry exists under `from` (smoothing disabled, or the JSON already
|
|
183
|
+
* closed and `finish` cleared it). */
|
|
184
|
+
rekey(from: string, to: string): void {
|
|
185
|
+
if (from === to) return;
|
|
186
|
+
const entry = this.#entries.get(from);
|
|
187
|
+
if (!entry) return;
|
|
188
|
+
this.#entries.delete(from);
|
|
189
|
+
this.#entries.set(to, entry);
|
|
190
|
+
}
|
|
191
|
+
|
|
180
192
|
/** Final arguments arrived (the JSON closed): drop the reveal so the
|
|
181
193
|
* caller's final-args render wins immediately, mirroring how assistant
|
|
182
194
|
* text snaps to the full message at message_end. */
|
|
@@ -379,7 +379,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
379
379
|
chatContainer: TranscriptContainer;
|
|
380
380
|
pendingMessagesContainer: Container;
|
|
381
381
|
statusContainer: Container;
|
|
382
|
-
todoReminderContainer: Container;
|
|
383
382
|
todoContainer: Container;
|
|
384
383
|
subagentContainer: Container;
|
|
385
384
|
btwContainer: Container;
|
|
@@ -548,7 +547,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
548
547
|
this.retryLoader = undefined;
|
|
549
548
|
}
|
|
550
549
|
this.statusContainer.clear();
|
|
551
|
-
this.todoReminderContainer.clear();
|
|
552
550
|
this.pendingMessagesContainer.clear();
|
|
553
551
|
this.#cancelModelCycleClearTimer();
|
|
554
552
|
this.modelCycleContainer.clear();
|
|
@@ -628,7 +626,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
628
626
|
this.chatContainer = new TranscriptContainer();
|
|
629
627
|
this.pendingMessagesContainer = new Container();
|
|
630
628
|
this.statusContainer = new AnchoredLiveContainer();
|
|
631
|
-
this.todoReminderContainer = new AnchoredLiveContainer();
|
|
632
629
|
this.todoContainer = new AnchoredLiveContainer();
|
|
633
630
|
this.subagentContainer = new AnchoredLiveContainer();
|
|
634
631
|
this.btwContainer = new AnchoredLiveContainer();
|
|
@@ -848,7 +845,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
848
845
|
|
|
849
846
|
this.ui.addChild(this.chatContainer);
|
|
850
847
|
this.ui.addChild(this.pendingMessagesContainer);
|
|
851
|
-
this.ui.addChild(this.todoReminderContainer);
|
|
852
848
|
this.ui.addChild(this.todoContainer);
|
|
853
849
|
this.ui.addChild(this.subagentContainer);
|
|
854
850
|
this.ui.addChild(this.btwContainer);
|
|
@@ -4077,14 +4073,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4077
4073
|
},
|
|
4078
4074
|
];
|
|
4079
4075
|
}
|
|
4080
|
-
this.todoReminderContainer.clear();
|
|
4081
4076
|
this.#syncTodoAutoClearTimer();
|
|
4082
4077
|
this.#renderTodoList();
|
|
4083
4078
|
this.ui.requestRender();
|
|
4084
4079
|
}
|
|
4085
4080
|
|
|
4086
4081
|
async reloadTodos(): Promise<void> {
|
|
4087
|
-
this.todoReminderContainer.clear();
|
|
4088
4082
|
await this.#loadTodoList();
|
|
4089
4083
|
this.ui.requestRender();
|
|
4090
4084
|
}
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type ExtensionWidgetOptions,
|
|
23
23
|
getExtensionUISelectOptionLabel,
|
|
24
24
|
} from "../../extensibility/extensions";
|
|
25
|
-
import { buildSkillPromptMessage } from "../../extensibility/skills";
|
|
25
|
+
import { buildSkillPromptMessage, parseSkillInvocation } from "../../extensibility/skills";
|
|
26
26
|
import { loadSlashCommands } from "../../extensibility/slash-commands";
|
|
27
27
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
28
28
|
import type { AgentSession } from "../../session/agent-session";
|
|
@@ -86,15 +86,12 @@ export async function tryRunRpcSkillCommand(
|
|
|
86
86
|
session: RpcSkillCommandSession,
|
|
87
87
|
text: string,
|
|
88
88
|
): Promise<RpcSkillCommandResult | false> {
|
|
89
|
-
if (!text.startsWith("/skill:")) return false;
|
|
90
89
|
if (!session.skillsSettings?.enableSkillCommands) return false;
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
const skillName = commandName.slice("skill:".length);
|
|
95
|
-
const skill = session.skills.find(candidate => candidate.name === skillName);
|
|
90
|
+
const parsed = parseSkillInvocation(text);
|
|
91
|
+
if (!parsed) return false;
|
|
92
|
+
const skill = session.skills.find(candidate => candidate.name === parsed.name);
|
|
96
93
|
if (!skill) return false;
|
|
97
|
-
const built = await buildSkillPromptMessage(skill, args);
|
|
94
|
+
const built = await buildSkillPromptMessage(skill, parsed.args);
|
|
98
95
|
await session.promptCustomMessage({
|
|
99
96
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
100
97
|
content: built.message,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { getSkillSlashCommandName, parseSkillInvocation } from "../extensibility/skills";
|
|
2
3
|
import { type CustomMessage, SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../session/messages";
|
|
3
4
|
import type { InteractiveModeContext } from "./types";
|
|
4
5
|
|
|
@@ -20,11 +21,6 @@ type SkillPromptOptions = {
|
|
|
20
21
|
queueChipText: string;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
interface ParsedSkillCommand {
|
|
24
|
-
commandName: string;
|
|
25
|
-
args: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
24
|
interface InvokeSkillCommandOptions {
|
|
29
25
|
propagateErrors?: boolean;
|
|
30
26
|
queueOnly?: boolean;
|
|
@@ -37,19 +33,11 @@ export interface BuiltSkillCommandPrompt {
|
|
|
37
33
|
options: SkillPromptOptions;
|
|
38
34
|
}
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
if (!text.startsWith("/skill:")) return undefined;
|
|
42
|
-
const spaceIndex = text.indexOf(" ");
|
|
43
|
-
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
44
|
-
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
|
45
|
-
return { commandName, args };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Return true when `text` names a registered `/skill:<name>` command. */
|
|
36
|
+
/** Return true when `text` invokes a registered `/skill:<name>` command. */
|
|
49
37
|
export function isKnownSkillCommand(ctx: SkillCommandHost, text: string): boolean {
|
|
50
|
-
const parsed =
|
|
38
|
+
const parsed = parseSkillInvocation(text);
|
|
51
39
|
if (!parsed) return false;
|
|
52
|
-
return ctx.skillCommands.has(parsed.
|
|
40
|
+
return ctx.skillCommands.has(getSkillSlashCommandName({ name: parsed.name }));
|
|
53
41
|
}
|
|
54
42
|
|
|
55
43
|
/** Build the user-attributed custom message for a registered `/skill:<name>` command. */
|
|
@@ -59,9 +47,10 @@ export async function buildSkillCommandPrompt(
|
|
|
59
47
|
streamingBehavior: "steer" | "followUp",
|
|
60
48
|
images?: ImageContent[],
|
|
61
49
|
): Promise<BuiltSkillCommandPrompt | undefined> {
|
|
62
|
-
const parsed =
|
|
50
|
+
const parsed = parseSkillInvocation(text);
|
|
63
51
|
if (!parsed) return undefined;
|
|
64
|
-
const
|
|
52
|
+
const commandName = getSkillSlashCommandName({ name: parsed.name });
|
|
53
|
+
const skillPath = ctx.skillCommands.get(commandName);
|
|
65
54
|
if (!skillPath) return undefined;
|
|
66
55
|
|
|
67
56
|
const content = await Bun.file(skillPath).text();
|
|
@@ -73,9 +62,8 @@ export async function buildSkillCommandPrompt(
|
|
|
73
62
|
const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
|
|
74
63
|
const textBlock: TextContent = { type: "text", text: message };
|
|
75
64
|
const promptContent = images && images.length > 0 ? [textBlock, ...images] : message;
|
|
76
|
-
const skillName = parsed.commandName.slice("skill:".length);
|
|
77
65
|
const details: SkillPromptDetails = {
|
|
78
|
-
name:
|
|
66
|
+
name: parsed.name,
|
|
79
67
|
path: skillPath,
|
|
80
68
|
args: parsed.args || undefined,
|
|
81
69
|
lineCount: body ? body.split("\n").length : 0,
|
package/src/modes/types.ts
CHANGED
|
@@ -96,7 +96,6 @@ export interface InteractiveModeContext {
|
|
|
96
96
|
chatContainer: TranscriptContainer;
|
|
97
97
|
pendingMessagesContainer: Container;
|
|
98
98
|
statusContainer: Container;
|
|
99
|
-
todoReminderContainer: Container;
|
|
100
99
|
todoContainer: Container;
|
|
101
100
|
subagentContainer: Container;
|
|
102
101
|
btwContainer: Container;
|