@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/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: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: ".venv/bin" },
217
- { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: "venv/bin" },
218
- { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: ".env/bin" },
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 (!text.startsWith("/skill:")) {
834
+ if (!record.session.skillsSettings?.enableSkillCommands) {
835
835
  return false;
836
836
  }
837
- if (!record.session.skillsSettings?.enableSkillCommands) {
837
+ const parsed = parseSkillInvocation(text);
838
+ if (!parsed) {
838
839
  return false;
839
840
  }
840
- const spaceIndex = text.indexOf(" ");
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(content.id, partialJson, {
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(content.id);
683
+ this.#toolArgsReveal.finish(pendingKey);
662
684
  renderArgs = content.arguments;
663
685
  }
664
- if (!this.ctx.pendingTools.has(content.id)) {
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(content.id, component);
684
- this.#toolArgsReveal.bind(content.id, component);
705
+ this.ctx.pendingTools.set(pendingKey, component);
706
+ this.#toolArgsReveal.bind(pendingKey, component);
685
707
  } else {
686
- const component = this.ctx.pendingTools.get(content.id);
708
+ const component = this.ctx.pendingTools.get(pendingKey);
687
709
  if (component) {
688
710
  component.updateArgs(renderArgs, content.id);
689
- this.#toolArgsReveal.bind(content.id, component);
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.todoReminderContainer.clear();
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
- this.showSelector(done => {
966
- const selector = new SessionSelectorComponent(
967
- sessions,
968
- async (session: SessionInfo) => {
969
- done();
970
- await this.handleResumeSession(session.path);
971
- },
972
- () => {
973
- done();
974
- this.ctx.ui.requestRender();
975
- },
976
- () => {
977
- void this.ctx.shutdown();
978
- },
979
- {
980
- onDelete: async (session: SessionInfo) => {
981
- if (!(await this.#detachActiveSessionBeforeDeletion(session.path))) {
982
- return false;
983
- }
984
- const storage = new FileSessionStorage();
985
- try {
986
- await storage.deleteSessionWithArtifacts(session.path);
987
- return true;
988
- } catch (err) {
989
- throw new Error(`Failed to delete session: ${err instanceof Error ? err.message : String(err)}`, {
990
- cause: err,
991
- });
992
- }
993
- },
994
- historyMatcher,
995
- loadAllSessions: () => SessionManager.listAll(),
996
- getTerminalRows: () => this.ctx.ui.terminal.rows,
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
- selector.setOnRequestRender(() => this.ctx.ui.requestRender());
1000
- return { component: selector, focus: selector };
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 spaceIndex = text.indexOf(" ");
92
- const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
93
- const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
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
- function parseSkillCommand(text: string): ParsedSkillCommand | undefined {
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 = parseSkillCommand(text);
38
+ const parsed = parseSkillInvocation(text);
51
39
  if (!parsed) return false;
52
- return ctx.skillCommands.has(parsed.commandName);
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 = parseSkillCommand(text);
50
+ const parsed = parseSkillInvocation(text);
63
51
  if (!parsed) return undefined;
64
- const skillPath = ctx.skillCommands.get(parsed.commandName);
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: skillName || parsed.commandName,
66
+ name: parsed.name,
79
67
  path: skillPath,
80
68
  args: parsed.args || undefined,
81
69
  lineCount: body ? body.split("\n").length : 0,
@@ -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;