@oh-my-pi/pi-coding-agent 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/package.json +5 -5
- package/src/core/hooks/loader.ts +6 -3
- package/src/core/hooks/tool-wrapper.ts +1 -0
- package/src/core/session-manager.ts +4 -1
- package/src/core/settings-manager.ts +6 -0
- package/src/core/tools/index.ts +1 -1
- package/src/core/tools/schema-validation.test.ts +30 -0
- package/src/modes/interactive/components/hook-editor.ts +1 -1
- package/src/modes/interactive/components/hook-message.ts +5 -0
- package/src/modes/interactive/controllers/command-controller.ts +11 -0
- package/src/modes/interactive/controllers/event-controller.ts +10 -8
- package/src/modes/interactive/controllers/input-controller.ts +47 -15
- package/src/modes/interactive/controllers/selector-controller.ts +36 -8
- package/src/modes/interactive/interactive-mode.ts +22 -1
- package/src/modes/interactive/types.ts +3 -0
- package/src/modes/rpc/rpc-mode.ts +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [4.6.0] - 2026-01-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Add `/skill:name` slash commands for quick skill access (toggle via `skills.enableSkillCommands` setting)
|
|
9
|
+
- Add `cwd` to SessionInfo for session list display
|
|
10
|
+
- Add custom summarization instructions option in tree selector
|
|
11
|
+
- Add Alt+Up (dequeue) to restore all queued messages at once
|
|
12
|
+
- Add `shutdownRequested` and `checkShutdownRequested()` for extension-initiated shutdown
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Component `invalidate()` now properly rebuilds content on theme changes
|
|
16
|
+
- Force full re-render after returning from external editor
|
|
17
|
+
|
|
5
18
|
## [4.5.0] - 2026-01-12
|
|
6
19
|
|
|
7
20
|
## [4.4.9] - 2026-01-12
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "4.
|
|
43
|
-
"@oh-my-pi/pi-agent-core": "4.
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "4.
|
|
45
|
-
"@oh-my-pi/pi-tui": "4.
|
|
42
|
+
"@oh-my-pi/pi-ai": "4.6.0",
|
|
43
|
+
"@oh-my-pi/pi-agent-core": "4.6.0",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "4.6.0",
|
|
45
|
+
"@oh-my-pi/pi-tui": "4.6.0",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
47
|
"@sinclair/typebox": "^0.34.46",
|
|
48
48
|
"ajv": "^8.17.1",
|
package/src/core/hooks/loader.ts
CHANGED
|
@@ -25,7 +25,7 @@ type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
|
|
25
25
|
*/
|
|
26
26
|
export type SendMessageHandler = <T = unknown>(
|
|
27
27
|
message: Pick<HookMessage<T>, "customType" | "content" | "display" | "details">,
|
|
28
|
-
triggerTurn?: boolean,
|
|
28
|
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
|
|
29
29
|
) => void;
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -129,11 +129,14 @@ function createHookAPI(
|
|
|
129
129
|
}
|
|
130
130
|
handlers.get(event)!.push(handler);
|
|
131
131
|
},
|
|
132
|
-
sendMessage<T = unknown>(
|
|
132
|
+
sendMessage<T = unknown>(
|
|
133
|
+
message: HookMessage<T>,
|
|
134
|
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
|
|
135
|
+
): void {
|
|
133
136
|
if (!sendMessageHandler) {
|
|
134
137
|
throw new Error("sendMessage handler not initialized");
|
|
135
138
|
}
|
|
136
|
-
sendMessageHandler(message,
|
|
139
|
+
sendMessageHandler(message, options);
|
|
137
140
|
},
|
|
138
141
|
appendEntry<T = unknown>(customType: string, data?: T): void {
|
|
139
142
|
if (!appendEntryHandler) {
|
|
@@ -164,6 +164,8 @@ export interface SessionContext {
|
|
|
164
164
|
export interface SessionInfo {
|
|
165
165
|
path: string;
|
|
166
166
|
id: string;
|
|
167
|
+
/** Working directory where the session was started. Empty string for old sessions. */
|
|
168
|
+
cwd: string;
|
|
167
169
|
title?: string;
|
|
168
170
|
created: Date;
|
|
169
171
|
modified: Date;
|
|
@@ -1707,7 +1709,7 @@ export class SessionManager {
|
|
|
1707
1709
|
if (lines.length === 0) continue;
|
|
1708
1710
|
|
|
1709
1711
|
// Check first line for valid session header
|
|
1710
|
-
let header: { type: string; id: string; title?: string; timestamp: string } | null = null;
|
|
1712
|
+
let header: { type: string; id: string; cwd?: string; title?: string; timestamp: string } | null = null;
|
|
1711
1713
|
try {
|
|
1712
1714
|
const first = JSON.parse(lines[0]);
|
|
1713
1715
|
if (first.type === "session" && first.id) {
|
|
@@ -1753,6 +1755,7 @@ export class SessionManager {
|
|
|
1753
1755
|
sessions.push({
|
|
1754
1756
|
path: file,
|
|
1755
1757
|
id: header.id,
|
|
1758
|
+
cwd: typeof header.cwd === "string" ? header.cwd : "",
|
|
1756
1759
|
title: header.title,
|
|
1757
1760
|
created: new Date(header.timestamp),
|
|
1758
1761
|
modified: stats.mtime,
|
|
@@ -26,6 +26,7 @@ export interface RetrySettings {
|
|
|
26
26
|
|
|
27
27
|
export interface SkillsSettings {
|
|
28
28
|
enabled?: boolean; // default: true
|
|
29
|
+
enableSkillCommands?: boolean; // default: true - register skills as /skill:name commands
|
|
29
30
|
enableCodexUser?: boolean; // default: true
|
|
30
31
|
enableClaudeUser?: boolean; // default: true
|
|
31
32
|
enableClaudeProject?: boolean; // default: true
|
|
@@ -821,6 +822,7 @@ export class SettingsManager {
|
|
|
821
822
|
getSkillsSettings(): Required<SkillsSettings> {
|
|
822
823
|
return {
|
|
823
824
|
enabled: this.settings.skills?.enabled ?? true,
|
|
825
|
+
enableSkillCommands: this.settings.skills?.enableSkillCommands ?? true,
|
|
824
826
|
enableCodexUser: this.settings.skills?.enableCodexUser ?? true,
|
|
825
827
|
enableClaudeUser: this.settings.skills?.enableClaudeUser ?? true,
|
|
826
828
|
enableClaudeProject: this.settings.skills?.enableClaudeProject ?? true,
|
|
@@ -832,6 +834,10 @@ export class SettingsManager {
|
|
|
832
834
|
};
|
|
833
835
|
}
|
|
834
836
|
|
|
837
|
+
getEnableSkillCommands(): boolean {
|
|
838
|
+
return this.settings.skills?.enableSkillCommands ?? true;
|
|
839
|
+
}
|
|
840
|
+
|
|
835
841
|
getCommandsSettings(): Required<CommandsSettings> {
|
|
836
842
|
return {
|
|
837
843
|
enableClaudeUser: this.settings.commands?.enableClaudeUser ?? true,
|
package/src/core/tools/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { type AskToolDetails, askTool, createAskTool } from "./ask";
|
|
2
|
-
export { type BashOperations, type BashToolDetails, createBashTool } from "./bash";
|
|
2
|
+
export { type BashOperations, type BashToolDetails, type BashToolOptions, createBashTool } from "./bash";
|
|
3
3
|
export { type CalculatorToolDetails, createCalculatorTool } from "./calculator";
|
|
4
4
|
export { createCompleteTool } from "./complete";
|
|
5
5
|
export { createEditTool, type EditToolDetails } from "./edit";
|
|
@@ -198,6 +198,36 @@ describe("sanitizeSchemaForGoogle", () => {
|
|
|
198
198
|
expect(sanitizeSchemaForGoogle(true)).toBe(true);
|
|
199
199
|
expect(sanitizeSchemaForGoogle(null)).toBe(null);
|
|
200
200
|
});
|
|
201
|
+
|
|
202
|
+
it("preserves property names that match schema keywords (e.g., 'pattern')", () => {
|
|
203
|
+
const schema = {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
pattern: { type: "string", description: "The search pattern" },
|
|
207
|
+
format: { type: "string", description: "Output format" },
|
|
208
|
+
},
|
|
209
|
+
required: ["pattern"],
|
|
210
|
+
};
|
|
211
|
+
const sanitized = sanitizeSchemaForGoogle(schema) as Record<string, unknown>;
|
|
212
|
+
const props = sanitized.properties as Record<string, unknown>;
|
|
213
|
+
expect(props.pattern).toEqual({ type: "string", description: "The search pattern" });
|
|
214
|
+
expect(props.format).toEqual({ type: "string", description: "Output format" });
|
|
215
|
+
expect(sanitized.required).toEqual(["pattern"]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("still strips schema keywords from non-properties contexts", () => {
|
|
219
|
+
const schema = {
|
|
220
|
+
type: "string",
|
|
221
|
+
pattern: "^[a-z]+$",
|
|
222
|
+
format: "email",
|
|
223
|
+
minLength: 1,
|
|
224
|
+
};
|
|
225
|
+
const sanitized = sanitizeSchemaForGoogle(schema) as Record<string, unknown>;
|
|
226
|
+
expect(sanitized.pattern).toBeUndefined();
|
|
227
|
+
expect(sanitized.format).toBeUndefined();
|
|
228
|
+
expect(sanitized.minLength).toBeUndefined();
|
|
229
|
+
expect(sanitized.type).toBe("string");
|
|
230
|
+
});
|
|
201
231
|
});
|
|
202
232
|
|
|
203
233
|
describe("tool schema validation (post-sanitization)", () => {
|
|
@@ -476,6 +476,17 @@ export class CommandController {
|
|
|
476
476
|
await this.executeCompaction(customInstructions, false);
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
+
async handleSkillCommand(skillPath: string, args: string): Promise<void> {
|
|
480
|
+
try {
|
|
481
|
+
const content = fs.readFileSync(skillPath, "utf-8");
|
|
482
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
|
|
483
|
+
const message = args ? `${body}\n\n---\n\nUser: ${args}` : body;
|
|
484
|
+
await this.ctx.session.prompt(message);
|
|
485
|
+
} catch (err) {
|
|
486
|
+
this.ctx.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
479
490
|
async executeCompaction(customInstructions?: string, isAuto = false): Promise<void> {
|
|
480
491
|
if (this.ctx.loadingAnimation) {
|
|
481
492
|
this.ctx.loadingAnimation.stop();
|
|
@@ -150,6 +150,15 @@ export class EventController {
|
|
|
150
150
|
if (event.message.role === "user") break;
|
|
151
151
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
152
152
|
this.ctx.streamingMessage = event.message;
|
|
153
|
+
let errorMessage: string | undefined;
|
|
154
|
+
if (this.ctx.streamingMessage.stopReason === "aborted" && !this.ctx.session.isTtsrAbortPending) {
|
|
155
|
+
const retryAttempt = this.ctx.session.retryAttempt;
|
|
156
|
+
errorMessage =
|
|
157
|
+
retryAttempt > 0
|
|
158
|
+
? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
|
|
159
|
+
: "Operation aborted";
|
|
160
|
+
this.ctx.streamingMessage.errorMessage = errorMessage;
|
|
161
|
+
}
|
|
153
162
|
if (this.ctx.session.isTtsrAbortPending && this.ctx.streamingMessage.stopReason === "aborted") {
|
|
154
163
|
const msgWithoutAbort = { ...this.ctx.streamingMessage, stopReason: "stop" as const };
|
|
155
164
|
this.ctx.streamingComponent.updateContent(msgWithoutAbort);
|
|
@@ -162,14 +171,7 @@ export class EventController {
|
|
|
162
171
|
this.ctx.streamingMessage.stopReason === "error"
|
|
163
172
|
) {
|
|
164
173
|
if (!this.ctx.session.isTtsrAbortPending) {
|
|
165
|
-
|
|
166
|
-
if (this.ctx.streamingMessage.stopReason === "aborted") {
|
|
167
|
-
const retryAttempt = this.ctx.session.retryAttempt;
|
|
168
|
-
errorMessage =
|
|
169
|
-
retryAttempt > 0
|
|
170
|
-
? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
|
|
171
|
-
: "Operation aborted";
|
|
172
|
-
} else {
|
|
174
|
+
if (!errorMessage) {
|
|
173
175
|
errorMessage = this.ctx.streamingMessage.errorMessage || "Error";
|
|
174
176
|
}
|
|
175
177
|
for (const [toolCallId, component] of this.ctx.pendingTools.entries()) {
|
|
@@ -24,14 +24,7 @@ export class InputController {
|
|
|
24
24
|
setupKeyHandlers(): void {
|
|
25
25
|
this.ctx.editor.onEscape = () => {
|
|
26
26
|
if (this.ctx.loadingAnimation) {
|
|
27
|
-
|
|
28
|
-
const queuedMessages = this.ctx.session.clearQueue();
|
|
29
|
-
const queuedText = [...queuedMessages.steering, ...queuedMessages.followUp].join("\n\n");
|
|
30
|
-
const currentText = this.ctx.editor.getText();
|
|
31
|
-
const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
|
|
32
|
-
this.ctx.editor.setText(combinedText);
|
|
33
|
-
this.ctx.updatePendingMessagesDisplay();
|
|
34
|
-
this.ctx.agent.abort();
|
|
27
|
+
this.restoreQueuedMessagesToEditor({ abort: true });
|
|
35
28
|
} else if (this.ctx.session.isBashRunning) {
|
|
36
29
|
this.ctx.session.abortBash();
|
|
37
30
|
} else if (this.ctx.isBashMode) {
|
|
@@ -241,6 +234,27 @@ export class InputController {
|
|
|
241
234
|
return;
|
|
242
235
|
}
|
|
243
236
|
|
|
237
|
+
// Handle skill commands (/skill:name [args])
|
|
238
|
+
if (text.startsWith("/skill:")) {
|
|
239
|
+
const spaceIndex = text.indexOf(" ");
|
|
240
|
+
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
241
|
+
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
|
242
|
+
const skillPath = this.ctx.skillCommands?.get(commandName);
|
|
243
|
+
if (skillPath) {
|
|
244
|
+
this.ctx.editor.addToHistory(text);
|
|
245
|
+
this.ctx.editor.setText("");
|
|
246
|
+
try {
|
|
247
|
+
const content = fs.readFileSync(skillPath, "utf-8");
|
|
248
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
|
|
249
|
+
const message = args ? `${body}\n\n---\n\nUser: ${args}` : body;
|
|
250
|
+
await this.ctx.session.prompt(message);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
this.ctx.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
244
258
|
// Handle bash command (! for normal, !! for excluded from context)
|
|
245
259
|
if (text.startsWith("!")) {
|
|
246
260
|
const isExcluded = text.startsWith("!!");
|
|
@@ -341,15 +355,33 @@ export class InputController {
|
|
|
341
355
|
}
|
|
342
356
|
|
|
343
357
|
handleDequeue(): void {
|
|
344
|
-
const
|
|
345
|
-
if (
|
|
358
|
+
const restored = this.restoreQueuedMessagesToEditor();
|
|
359
|
+
if (restored === 0) {
|
|
360
|
+
this.ctx.showStatus("No queued messages to restore");
|
|
361
|
+
} else {
|
|
362
|
+
this.ctx.showStatus(`Restored ${restored} queued message${restored > 1 ? "s" : ""} to editor`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
346
365
|
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
|
|
366
|
+
restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
|
|
367
|
+
const { steering, followUp } = this.ctx.session.clearQueue();
|
|
368
|
+
const allQueued = [...steering, ...followUp];
|
|
369
|
+
if (allQueued.length === 0) {
|
|
370
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
371
|
+
if (options?.abort) {
|
|
372
|
+
this.ctx.agent.abort();
|
|
373
|
+
}
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
const queuedText = allQueued.join("\n\n");
|
|
377
|
+
const currentText = options?.currentText ?? this.ctx.editor.getText();
|
|
378
|
+
const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
|
|
379
|
+
this.ctx.editor.setText(combinedText);
|
|
351
380
|
this.ctx.updatePendingMessagesDisplay();
|
|
352
|
-
|
|
381
|
+
if (options?.abort) {
|
|
382
|
+
this.ctx.agent.abort();
|
|
383
|
+
}
|
|
384
|
+
return allQueued.length;
|
|
353
385
|
}
|
|
354
386
|
|
|
355
387
|
handleBackgroundCommand(): void {
|
|
@@ -352,16 +352,41 @@ export class SelectorController {
|
|
|
352
352
|
return;
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
// Ask about summarization
|
|
355
|
+
// Ask about summarization
|
|
356
356
|
done(); // Close selector first
|
|
357
357
|
|
|
358
|
+
// Loop until user makes a complete choice or cancels to tree
|
|
359
|
+
let wantsSummary = false;
|
|
360
|
+
let customInstructions: string | undefined;
|
|
361
|
+
|
|
358
362
|
const branchSummariesEnabled = this.ctx.settingsManager.getBranchSummaryEnabled();
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
363
|
+
|
|
364
|
+
while (branchSummariesEnabled) {
|
|
365
|
+
const summaryChoice = await this.ctx.showHookSelector("Summarize branch?", [
|
|
366
|
+
"No summary",
|
|
367
|
+
"Summarize",
|
|
368
|
+
"Summarize with custom prompt",
|
|
369
|
+
]);
|
|
370
|
+
|
|
371
|
+
if (summaryChoice === undefined) {
|
|
372
|
+
// User pressed escape - re-show tree selector
|
|
373
|
+
this.showTreeSelector();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
wantsSummary = summaryChoice !== "No summary";
|
|
378
|
+
|
|
379
|
+
if (summaryChoice === "Summarize with custom prompt") {
|
|
380
|
+
customInstructions = await this.ctx.showHookEditor("Custom summarization instructions");
|
|
381
|
+
if (customInstructions === undefined) {
|
|
382
|
+
// User cancelled - loop back to summary selector
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// User made a complete choice
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
365
390
|
|
|
366
391
|
// Set up escape handler and loader if summarizing
|
|
367
392
|
let summaryLoader: Loader | undefined;
|
|
@@ -384,7 +409,10 @@ export class SelectorController {
|
|
|
384
409
|
}
|
|
385
410
|
|
|
386
411
|
try {
|
|
387
|
-
const result = await this.ctx.session.navigateTree(entryId, {
|
|
412
|
+
const result = await this.ctx.session.navigateTree(entryId, {
|
|
413
|
+
summarize: wantsSummary,
|
|
414
|
+
customInstructions,
|
|
415
|
+
});
|
|
388
416
|
|
|
389
417
|
if (result.aborted) {
|
|
390
418
|
// Summarization aborted - re-show tree selector
|
|
@@ -109,6 +109,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
109
109
|
public lastEscapeTime = 0;
|
|
110
110
|
public lastVoiceInterruptAt = 0;
|
|
111
111
|
public voiceAutoModeEnabled = false;
|
|
112
|
+
public shutdownRequested = false;
|
|
113
|
+
private isShuttingDown = false;
|
|
112
114
|
public voiceProgressTimer: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
113
115
|
public voiceProgressSpoken = false;
|
|
114
116
|
public voiceProgressLastLength = 0;
|
|
@@ -118,6 +120,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
118
120
|
public lastStatusSpacer: Spacer | undefined = undefined;
|
|
119
121
|
public lastStatusText: Text | undefined = undefined;
|
|
120
122
|
public fileSlashCommands: Set<string> = new Set();
|
|
123
|
+
public skillCommands: Map<string, string> = new Map();
|
|
121
124
|
|
|
122
125
|
private pendingSlashCommands: SlashCommand[] = [];
|
|
123
126
|
private cleanupUnsubscribe?: () => void;
|
|
@@ -231,8 +234,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
231
234
|
description: `${loaded.command.description} (${loaded.source})`,
|
|
232
235
|
}));
|
|
233
236
|
|
|
237
|
+
// Build skill commands from session.skills (if enabled)
|
|
238
|
+
const skillCommandList: SlashCommand[] = [];
|
|
239
|
+
if (this.settingsManager.getEnableSkillCommands?.()) {
|
|
240
|
+
for (const skill of this.session.skills) {
|
|
241
|
+
const commandName = `skill:${skill.name}`;
|
|
242
|
+
this.skillCommands.set(commandName, skill.filePath);
|
|
243
|
+
skillCommandList.push({ name: commandName, description: skill.description });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
234
247
|
// Store pending commands for init() where file commands are loaded async
|
|
235
|
-
this.pendingSlashCommands = [...slashCommands, ...hookCommands, ...customCommands];
|
|
248
|
+
this.pendingSlashCommands = [...slashCommands, ...hookCommands, ...customCommands, ...skillCommandList];
|
|
236
249
|
|
|
237
250
|
this.uiHelpers = new UiHelpers(this);
|
|
238
251
|
this.voiceManager = new VoiceManager(this);
|
|
@@ -486,6 +499,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
486
499
|
}
|
|
487
500
|
|
|
488
501
|
async shutdown(): Promise<void> {
|
|
502
|
+
if (this.isShuttingDown) return;
|
|
503
|
+
this.isShuttingDown = true;
|
|
504
|
+
|
|
489
505
|
this.voiceAutoModeEnabled = false;
|
|
490
506
|
await this.voiceSupervisor.stop();
|
|
491
507
|
|
|
@@ -499,6 +515,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
499
515
|
process.exit(0);
|
|
500
516
|
}
|
|
501
517
|
|
|
518
|
+
async checkShutdownRequested(): Promise<void> {
|
|
519
|
+
if (!this.shutdownRequested) return;
|
|
520
|
+
await this.shutdown();
|
|
521
|
+
}
|
|
522
|
+
|
|
502
523
|
// Extension UI integration
|
|
503
524
|
setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void {
|
|
504
525
|
this.toolUiContextSetter(uiContext, hasUI);
|
|
@@ -74,6 +74,7 @@ export interface InteractiveModeContext {
|
|
|
74
74
|
lastEscapeTime: number;
|
|
75
75
|
lastVoiceInterruptAt: number;
|
|
76
76
|
voiceAutoModeEnabled: boolean;
|
|
77
|
+
shutdownRequested: boolean;
|
|
77
78
|
voiceProgressTimer: ReturnType<typeof setTimeout> | undefined;
|
|
78
79
|
voiceProgressSpoken: boolean;
|
|
79
80
|
voiceProgressLastLength: number;
|
|
@@ -83,11 +84,13 @@ export interface InteractiveModeContext {
|
|
|
83
84
|
lastStatusSpacer: Spacer | undefined;
|
|
84
85
|
lastStatusText: Text | undefined;
|
|
85
86
|
fileSlashCommands: Set<string>;
|
|
87
|
+
skillCommands: Map<string, string>;
|
|
86
88
|
todoItems: TodoItem[];
|
|
87
89
|
|
|
88
90
|
// Lifecycle
|
|
89
91
|
init(): Promise<void>;
|
|
90
92
|
shutdown(): Promise<void>;
|
|
93
|
+
checkShutdownRequested(): Promise<void>;
|
|
91
94
|
|
|
92
95
|
// Extension UI integration
|
|
93
96
|
setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
|
|
@@ -189,6 +189,14 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
189
189
|
// Component factories are not supported in RPC mode - would need TUI access
|
|
190
190
|
},
|
|
191
191
|
|
|
192
|
+
setFooter(_factory: unknown): void {
|
|
193
|
+
// Custom footer not supported in RPC mode - requires TUI access
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
setHeader(_factory: unknown): void {
|
|
197
|
+
// Custom header not supported in RPC mode - requires TUI access
|
|
198
|
+
},
|
|
199
|
+
|
|
192
200
|
setTitle(title: string): void {
|
|
193
201
|
// Fire and forget - host can implement terminal title control
|
|
194
202
|
output({
|
|
@@ -257,9 +265,9 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
257
265
|
return { success: false, error: "Theme switching not supported in RPC mode" };
|
|
258
266
|
},
|
|
259
267
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
268
|
+
setEditorComponent(): void {
|
|
269
|
+
// Custom editor components not supported in RPC mode
|
|
270
|
+
},
|
|
263
271
|
});
|
|
264
272
|
|
|
265
273
|
// Set up extensions with RPC-based UI context
|