@oh-my-pi/pi-coding-agent 11.2.3 → 11.3.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 +100 -0
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/hooks/status-line.ts +1 -1
- package/examples/sdk/11-sessions.ts +1 -1
- package/package.json +8 -8
- package/src/cli/args.ts +9 -6
- package/src/cli/update-cli.ts +2 -2
- package/src/commands/index/index.ts +2 -5
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/changelog/index.ts +2 -2
- package/src/config/keybindings.ts +16 -1
- package/src/config/model-registry.ts +25 -20
- package/src/config/model-resolver.ts +8 -8
- package/src/config/resolve-config-value.ts +92 -0
- package/src/config/settings-schema.ts +9 -0
- package/src/config.ts +14 -1
- package/src/export/html/template.css +7 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +33 -16
- package/src/extensibility/custom-commands/bundled/review/index.ts +1 -1
- package/src/extensibility/extensions/index.ts +18 -0
- package/src/extensibility/extensions/loader.ts +15 -0
- package/src/extensibility/extensions/runner.ts +78 -1
- package/src/extensibility/extensions/types.ts +131 -5
- package/src/extensibility/extensions/wrapper.ts +1 -1
- package/src/extensibility/plugins/git-url.ts +270 -0
- package/src/extensibility/plugins/index.ts +2 -0
- package/src/extensibility/slash-commands.ts +45 -0
- package/src/index.ts +7 -0
- package/src/lsp/render.ts +50 -43
- package/src/lsp/utils.ts +2 -2
- package/src/main.ts +11 -10
- package/src/mcp/transports/stdio.ts +3 -5
- package/src/modes/components/custom-message.ts +0 -8
- package/src/modes/components/diff.ts +1 -7
- package/src/modes/components/footer.ts +4 -4
- package/src/modes/components/model-selector.ts +4 -0
- package/src/modes/components/todo-display.ts +13 -3
- package/src/modes/components/tool-execution.ts +30 -16
- package/src/modes/components/tree-selector.ts +50 -19
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +34 -2
- package/src/modes/controllers/input-controller.ts +47 -33
- package/src/modes/controllers/selector-controller.ts +10 -15
- package/src/modes/interactive-mode.ts +50 -38
- package/src/modes/print-mode.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +17 -2
- package/src/modes/rpc/rpc-types.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/patch/applicator.ts +2 -3
- package/src/patch/fuzzy.ts +1 -1
- package/src/patch/shared.ts +74 -61
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/task.md +6 -0
- package/src/sdk.ts +15 -11
- package/src/session/agent-session.ts +72 -23
- package/src/session/auth-storage.ts +2 -1
- package/src/session/blob-store.ts +105 -0
- package/src/session/session-manager.ts +107 -44
- package/src/task/executor.ts +19 -9
- package/src/task/render.ts +80 -58
- package/src/tools/ask.ts +28 -5
- package/src/tools/bash.ts +47 -39
- package/src/tools/browser.ts +248 -26
- package/src/tools/calculator.ts +42 -23
- package/src/tools/fetch.ts +33 -16
- package/src/tools/find.ts +57 -22
- package/src/tools/grep.ts +54 -25
- package/src/tools/index.ts +5 -5
- package/src/tools/notebook.ts +19 -6
- package/src/tools/path-utils.ts +26 -1
- package/src/tools/python.ts +20 -14
- package/src/tools/read.ts +21 -8
- package/src/tools/render-utils.ts +5 -45
- package/src/tools/ssh.ts +59 -53
- package/src/tools/submit-result.ts +2 -2
- package/src/tools/todo-write.ts +32 -14
- package/src/tools/truncate.ts +1 -1
- package/src/tools/write.ts +39 -24
- package/src/tui/output-block.ts +61 -3
- package/src/tui/tree-list.ts +4 -4
- package/src/tui/utils.ts +71 -1
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/utils/tools-manager.ts +18 -2
- package/src/web/scrapers/osv.ts +4 -1
- package/src/web/scrapers/youtube.ts +1 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/render.ts +96 -90
|
@@ -32,6 +32,8 @@ import type {
|
|
|
32
32
|
MessageRenderer,
|
|
33
33
|
RegisteredCommand,
|
|
34
34
|
RegisteredTool,
|
|
35
|
+
ResourcesDiscoverEvent,
|
|
36
|
+
ResourcesDiscoverResult,
|
|
35
37
|
SessionBeforeCompactResult,
|
|
36
38
|
SessionBeforeTreeResult,
|
|
37
39
|
SessionCompactingResult,
|
|
@@ -64,6 +66,8 @@ export type NavigateTreeHandler = (
|
|
|
64
66
|
options?: { summarize?: boolean },
|
|
65
67
|
) => Promise<{ cancelled: boolean }>;
|
|
66
68
|
|
|
69
|
+
export type SwitchSessionHandler = (sessionPath: string) => Promise<{ cancelled: boolean }>;
|
|
70
|
+
|
|
67
71
|
export type ShutdownHandler = () => void;
|
|
68
72
|
|
|
69
73
|
/**
|
|
@@ -102,6 +106,8 @@ const noOpUIContext: ExtensionUIContext = {
|
|
|
102
106
|
getAllThemes: () => Promise.resolve([]),
|
|
103
107
|
getTheme: () => Promise.resolve(undefined),
|
|
104
108
|
setTheme: (_theme: string | Theme) => Promise.resolve({ success: false, error: "UI not available" }),
|
|
109
|
+
getToolsExpanded: () => false,
|
|
110
|
+
setToolsExpanded: () => {},
|
|
105
111
|
};
|
|
106
112
|
|
|
107
113
|
export class ExtensionRunner {
|
|
@@ -114,10 +120,13 @@ export class ExtensionRunner {
|
|
|
114
120
|
private hasPendingMessagesFn: () => boolean = () => false;
|
|
115
121
|
private getContextUsageFn: () => ContextUsage | undefined = () => undefined;
|
|
116
122
|
private compactFn: (instructionsOrOptions?: string | CompactOptions) => Promise<void> = async () => {};
|
|
123
|
+
private getSystemPromptFn: () => string = () => "";
|
|
117
124
|
private newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
|
|
118
125
|
private branchHandler: BranchHandler = async () => ({ cancelled: false });
|
|
119
126
|
private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
|
|
127
|
+
private switchSessionHandler: SwitchSessionHandler = async () => ({ cancelled: false });
|
|
120
128
|
private shutdownHandler: ShutdownHandler = () => {};
|
|
129
|
+
private commandDiagnostics: Array<{ type: string; message: string; path: string }> = [];
|
|
121
130
|
|
|
122
131
|
constructor(
|
|
123
132
|
private readonly extensions: Extension[],
|
|
@@ -142,6 +151,7 @@ export class ExtensionRunner {
|
|
|
142
151
|
this.runtime.getActiveTools = actions.getActiveTools;
|
|
143
152
|
this.runtime.getAllTools = actions.getAllTools;
|
|
144
153
|
this.runtime.setActiveTools = actions.setActiveTools;
|
|
154
|
+
this.runtime.getCommands = actions.getCommands;
|
|
145
155
|
this.runtime.setModel = actions.setModel;
|
|
146
156
|
this.runtime.getThinkingLevel = actions.getThinkingLevel;
|
|
147
157
|
this.runtime.setThinkingLevel = actions.setThinkingLevel;
|
|
@@ -152,6 +162,7 @@ export class ExtensionRunner {
|
|
|
152
162
|
this.abortFn = contextActions.abort;
|
|
153
163
|
this.hasPendingMessagesFn = contextActions.hasPendingMessages;
|
|
154
164
|
this.shutdownHandler = contextActions.shutdown;
|
|
165
|
+
this.getSystemPromptFn = contextActions.getSystemPrompt;
|
|
155
166
|
|
|
156
167
|
// Command context actions (optional, only for interactive mode)
|
|
157
168
|
if (commandContextActions) {
|
|
@@ -159,6 +170,7 @@ export class ExtensionRunner {
|
|
|
159
170
|
this.newSessionHandler = commandContextActions.newSession;
|
|
160
171
|
this.branchHandler = commandContextActions.branch;
|
|
161
172
|
this.navigateTreeHandler = commandContextActions.navigateTree;
|
|
173
|
+
this.switchSessionHandler = commandContextActions.switchSession;
|
|
162
174
|
this.getContextUsageFn = commandContextActions.getContextUsage;
|
|
163
175
|
this.compactFn = commandContextActions.compact;
|
|
164
176
|
}
|
|
@@ -279,16 +291,31 @@ export class ExtensionRunner {
|
|
|
279
291
|
return undefined;
|
|
280
292
|
}
|
|
281
293
|
|
|
282
|
-
getRegisteredCommands(): RegisteredCommand[] {
|
|
294
|
+
getRegisteredCommands(reserved?: Set<string>): RegisteredCommand[] {
|
|
295
|
+
this.commandDiagnostics = [];
|
|
296
|
+
|
|
283
297
|
const commands: RegisteredCommand[] = [];
|
|
284
298
|
for (const ext of this.extensions) {
|
|
285
299
|
for (const command of ext.commands.values()) {
|
|
300
|
+
if (reserved?.has(command.name)) {
|
|
301
|
+
const message = `Extension command '${command.name}' from ${ext.path} conflicts with built-in commands. Skipping.`;
|
|
302
|
+
this.commandDiagnostics.push({ type: "warning", message, path: ext.path });
|
|
303
|
+
if (!this.hasUI()) {
|
|
304
|
+
logger.warn(message);
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
286
309
|
commands.push(command);
|
|
287
310
|
}
|
|
288
311
|
}
|
|
289
312
|
return commands;
|
|
290
313
|
}
|
|
291
314
|
|
|
315
|
+
getCommandDiagnostics(): Array<{ type: string; message: string; path: string }> {
|
|
316
|
+
return this.commandDiagnostics;
|
|
317
|
+
}
|
|
318
|
+
|
|
292
319
|
getCommand(name: string): RegisteredCommand | undefined {
|
|
293
320
|
for (const ext of this.extensions) {
|
|
294
321
|
const command = ext.commands.get(name);
|
|
@@ -316,6 +343,7 @@ export class ExtensionRunner {
|
|
|
316
343
|
abort: () => this.abortFn(),
|
|
317
344
|
hasPendingMessages: () => this.hasPendingMessagesFn(),
|
|
318
345
|
shutdown: () => this.shutdownHandler(),
|
|
346
|
+
getSystemPrompt: () => this.getSystemPromptFn(),
|
|
319
347
|
hasQueuedMessages: () => this.hasPendingMessagesFn(), // deprecated alias
|
|
320
348
|
};
|
|
321
349
|
}
|
|
@@ -335,6 +363,7 @@ export class ExtensionRunner {
|
|
|
335
363
|
newSession: options => this.newSessionHandler(options),
|
|
336
364
|
branch: entryId => this.branchHandler(entryId),
|
|
337
365
|
navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),
|
|
366
|
+
switchSession: sessionPath => this.switchSessionHandler(sessionPath),
|
|
338
367
|
compact: instructionsOrOptions => this.compactFn(instructionsOrOptions),
|
|
339
368
|
};
|
|
340
369
|
}
|
|
@@ -493,6 +522,54 @@ export class ExtensionRunner {
|
|
|
493
522
|
return undefined;
|
|
494
523
|
}
|
|
495
524
|
|
|
525
|
+
async emitResourcesDiscover(
|
|
526
|
+
cwd: string,
|
|
527
|
+
reason: ResourcesDiscoverEvent["reason"],
|
|
528
|
+
): Promise<{
|
|
529
|
+
skillPaths: Array<{ path: string; extensionPath: string }>;
|
|
530
|
+
promptPaths: Array<{ path: string; extensionPath: string }>;
|
|
531
|
+
themePaths: Array<{ path: string; extensionPath: string }>;
|
|
532
|
+
}> {
|
|
533
|
+
const ctx = this.createContext();
|
|
534
|
+
const skillPaths: Array<{ path: string; extensionPath: string }> = [];
|
|
535
|
+
const promptPaths: Array<{ path: string; extensionPath: string }> = [];
|
|
536
|
+
const themePaths: Array<{ path: string; extensionPath: string }> = [];
|
|
537
|
+
|
|
538
|
+
for (const ext of this.extensions) {
|
|
539
|
+
const handlers = ext.handlers.get("resources_discover");
|
|
540
|
+
if (!handlers || handlers.length === 0) continue;
|
|
541
|
+
|
|
542
|
+
for (const handler of handlers) {
|
|
543
|
+
try {
|
|
544
|
+
const event: ResourcesDiscoverEvent = { type: "resources_discover", cwd, reason };
|
|
545
|
+
const handlerResult = await handler(event, ctx);
|
|
546
|
+
const result = handlerResult as ResourcesDiscoverResult | undefined;
|
|
547
|
+
|
|
548
|
+
if (result?.skillPaths?.length) {
|
|
549
|
+
skillPaths.push(...result.skillPaths.map(path => ({ path, extensionPath: ext.path })));
|
|
550
|
+
}
|
|
551
|
+
if (result?.promptPaths?.length) {
|
|
552
|
+
promptPaths.push(...result.promptPaths.map(path => ({ path, extensionPath: ext.path })));
|
|
553
|
+
}
|
|
554
|
+
if (result?.themePaths?.length) {
|
|
555
|
+
themePaths.push(...result.themePaths.map(path => ({ path, extensionPath: ext.path })));
|
|
556
|
+
}
|
|
557
|
+
} catch (err) {
|
|
558
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
559
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
560
|
+
this.emitError({
|
|
561
|
+
extensionPath: ext.path,
|
|
562
|
+
event: "resources_discover",
|
|
563
|
+
error: message,
|
|
564
|
+
stack,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return { skillPaths, promptPaths, themePaths };
|
|
571
|
+
}
|
|
572
|
+
|
|
496
573
|
/** Emit input event. Transforms chain, "handled" short-circuits. */
|
|
497
574
|
async emitInput(
|
|
498
575
|
text: string,
|
|
@@ -28,8 +28,19 @@ import type {
|
|
|
28
28
|
SessionEntry,
|
|
29
29
|
SessionManager,
|
|
30
30
|
} from "../../session/session-manager";
|
|
31
|
-
import type {
|
|
31
|
+
import type {
|
|
32
|
+
BashToolDetails,
|
|
33
|
+
BashToolInput,
|
|
34
|
+
FindToolDetails,
|
|
35
|
+
FindToolInput,
|
|
36
|
+
GrepToolDetails,
|
|
37
|
+
GrepToolInput,
|
|
38
|
+
ReadToolDetails,
|
|
39
|
+
ReadToolInput,
|
|
40
|
+
WriteToolInput,
|
|
41
|
+
} from "../../tools";
|
|
32
42
|
import type { EventBus } from "../../utils/event-bus";
|
|
43
|
+
import type { SlashCommandInfo } from "../slash-commands";
|
|
33
44
|
|
|
34
45
|
export type { AppAction, KeybindingsManager } from "../../config/keybindings";
|
|
35
46
|
export type { ExecOptions, ExecResult } from "../../exec/exec";
|
|
@@ -123,6 +134,12 @@ export interface ExtensionUIContext {
|
|
|
123
134
|
|
|
124
135
|
/** Set the current theme by name or Theme object. */
|
|
125
136
|
setTheme(theme: string | Theme): Promise<{ success: boolean; error?: string }>;
|
|
137
|
+
|
|
138
|
+
/** Get current tool output expansion state. */
|
|
139
|
+
getToolsExpanded(): boolean;
|
|
140
|
+
|
|
141
|
+
/** Set tool output expansion state. */
|
|
142
|
+
setToolsExpanded(expanded: boolean): void;
|
|
126
143
|
}
|
|
127
144
|
|
|
128
145
|
// ============================================================================
|
|
@@ -171,6 +188,8 @@ export interface ExtensionContext {
|
|
|
171
188
|
hasPendingMessages(): boolean;
|
|
172
189
|
/** Gracefully shutdown and exit. */
|
|
173
190
|
shutdown(): void;
|
|
191
|
+
/** Get the current effective system prompt. */
|
|
192
|
+
getSystemPrompt(): string;
|
|
174
193
|
/** @deprecated Use hasPendingMessages() instead */
|
|
175
194
|
hasQueuedMessages(): boolean;
|
|
176
195
|
}
|
|
@@ -198,6 +217,9 @@ export interface ExtensionCommandContext extends ExtensionContext {
|
|
|
198
217
|
/** Navigate to a different point in the session tree. */
|
|
199
218
|
navigateTree(targetId: string, options?: { summarize?: boolean }): Promise<{ cancelled: boolean }>;
|
|
200
219
|
|
|
220
|
+
/** Switch to a different session file. */
|
|
221
|
+
switchSession(sessionPath: string): Promise<{ cancelled: boolean }>;
|
|
222
|
+
|
|
201
223
|
/** Compact the session context (interactive mode shows UI). */
|
|
202
224
|
compact(instructionsOrOptions?: string | CompactOptions): Promise<void>;
|
|
203
225
|
}
|
|
@@ -243,9 +265,9 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
|
|
|
243
265
|
execute(
|
|
244
266
|
toolCallId: string,
|
|
245
267
|
params: Static<TParams>,
|
|
268
|
+
signal: AbortSignal | undefined,
|
|
246
269
|
onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
|
|
247
270
|
ctx: ExtensionContext,
|
|
248
|
-
signal?: AbortSignal,
|
|
249
271
|
): Promise<AgentToolResult<TDetails>>;
|
|
250
272
|
|
|
251
273
|
/** Called on session lifecycle events - use to reconstruct state or cleanup resources */
|
|
@@ -263,6 +285,24 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
|
|
|
263
285
|
) => Component;
|
|
264
286
|
}
|
|
265
287
|
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// Resource Events
|
|
290
|
+
// ============================================================================
|
|
291
|
+
|
|
292
|
+
/** Fired after session_start to allow extensions to provide additional resource paths. */
|
|
293
|
+
export interface ResourcesDiscoverEvent {
|
|
294
|
+
type: "resources_discover";
|
|
295
|
+
cwd: string;
|
|
296
|
+
reason: "startup" | "reload";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Result from resources_discover event handler */
|
|
300
|
+
export interface ResourcesDiscoverResult {
|
|
301
|
+
skillPaths?: string[];
|
|
302
|
+
promptPaths?: string[];
|
|
303
|
+
themePaths?: string[];
|
|
304
|
+
}
|
|
305
|
+
|
|
266
306
|
// ============================================================================
|
|
267
307
|
// Session Events
|
|
268
308
|
// ============================================================================
|
|
@@ -454,14 +494,56 @@ export interface InputEvent {
|
|
|
454
494
|
// Tool Events
|
|
455
495
|
// ============================================================================
|
|
456
496
|
|
|
457
|
-
|
|
458
|
-
export interface ToolCallEvent {
|
|
497
|
+
interface ToolCallEventBase {
|
|
459
498
|
type: "tool_call";
|
|
460
|
-
toolName: string;
|
|
461
499
|
toolCallId: string;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export interface BashToolCallEvent extends ToolCallEventBase {
|
|
503
|
+
toolName: "bash";
|
|
504
|
+
input: BashToolInput;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export interface ReadToolCallEvent extends ToolCallEventBase {
|
|
508
|
+
toolName: "read";
|
|
509
|
+
input: ReadToolInput;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export interface EditToolCallEvent extends ToolCallEventBase {
|
|
513
|
+
toolName: "edit";
|
|
462
514
|
input: Record<string, unknown>;
|
|
463
515
|
}
|
|
464
516
|
|
|
517
|
+
export interface WriteToolCallEvent extends ToolCallEventBase {
|
|
518
|
+
toolName: "write";
|
|
519
|
+
input: WriteToolInput;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export interface GrepToolCallEvent extends ToolCallEventBase {
|
|
523
|
+
toolName: "grep";
|
|
524
|
+
input: GrepToolInput;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export interface FindToolCallEvent extends ToolCallEventBase {
|
|
528
|
+
toolName: "find";
|
|
529
|
+
input: FindToolInput;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export interface CustomToolCallEvent extends ToolCallEventBase {
|
|
533
|
+
toolName: string;
|
|
534
|
+
input: Record<string, unknown>;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Fired before a tool executes. Can block. */
|
|
538
|
+
export type ToolCallEvent =
|
|
539
|
+
| BashToolCallEvent
|
|
540
|
+
| ReadToolCallEvent
|
|
541
|
+
| EditToolCallEvent
|
|
542
|
+
| WriteToolCallEvent
|
|
543
|
+
| GrepToolCallEvent
|
|
544
|
+
| FindToolCallEvent
|
|
545
|
+
| CustomToolCallEvent;
|
|
546
|
+
|
|
465
547
|
interface ToolResultEventBase {
|
|
466
548
|
type: "tool_result";
|
|
467
549
|
toolCallId: string;
|
|
@@ -515,8 +597,43 @@ export type ToolResultEvent =
|
|
|
515
597
|
| FindToolResultEvent
|
|
516
598
|
| CustomToolResultEvent;
|
|
517
599
|
|
|
600
|
+
/**
|
|
601
|
+
* Type guard for narrowing ToolCallEvent by tool name.
|
|
602
|
+
*
|
|
603
|
+
* Built-in tools narrow automatically (no type params needed):
|
|
604
|
+
* ```ts
|
|
605
|
+
* if (isToolCallEventType("bash", event)) {
|
|
606
|
+
* event.input.command; // string
|
|
607
|
+
* }
|
|
608
|
+
* ```
|
|
609
|
+
*
|
|
610
|
+
* Custom tools require explicit type parameters:
|
|
611
|
+
* ```ts
|
|
612
|
+
* if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) {
|
|
613
|
+
* event.input.action; // typed
|
|
614
|
+
* }
|
|
615
|
+
* ```
|
|
616
|
+
*
|
|
617
|
+
* Note: Direct narrowing via `event.toolName === "bash"` doesn't work because
|
|
618
|
+
* CustomToolCallEvent.toolName is `string` which overlaps with all literals.
|
|
619
|
+
*/
|
|
620
|
+
export function isToolCallEventType(toolName: "bash", event: ToolCallEvent): event is BashToolCallEvent;
|
|
621
|
+
export function isToolCallEventType(toolName: "read", event: ToolCallEvent): event is ReadToolCallEvent;
|
|
622
|
+
export function isToolCallEventType(toolName: "edit", event: ToolCallEvent): event is EditToolCallEvent;
|
|
623
|
+
export function isToolCallEventType(toolName: "write", event: ToolCallEvent): event is WriteToolCallEvent;
|
|
624
|
+
export function isToolCallEventType(toolName: "grep", event: ToolCallEvent): event is GrepToolCallEvent;
|
|
625
|
+
export function isToolCallEventType(toolName: "find", event: ToolCallEvent): event is FindToolCallEvent;
|
|
626
|
+
export function isToolCallEventType<TName extends string, TInput extends Record<string, unknown>>(
|
|
627
|
+
toolName: TName,
|
|
628
|
+
event: ToolCallEvent,
|
|
629
|
+
): event is ToolCallEvent & { toolName: TName; input: TInput };
|
|
630
|
+
export function isToolCallEventType(toolName: string, event: ToolCallEvent): boolean {
|
|
631
|
+
return event.toolName === toolName;
|
|
632
|
+
}
|
|
633
|
+
|
|
518
634
|
/** Union of all event types */
|
|
519
635
|
export type ExtensionEvent =
|
|
636
|
+
| ResourcesDiscoverEvent
|
|
520
637
|
| SessionEvent
|
|
521
638
|
| ContextEvent
|
|
522
639
|
| BeforeAgentStartEvent
|
|
@@ -659,6 +776,7 @@ export interface ExtensionAPI {
|
|
|
659
776
|
// Event Subscription
|
|
660
777
|
// =========================================================================
|
|
661
778
|
|
|
779
|
+
on(event: "resources_discover", handler: ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult>): void;
|
|
662
780
|
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
|
663
781
|
on(
|
|
664
782
|
event: "session_before_switch",
|
|
@@ -775,6 +893,9 @@ export interface ExtensionAPI {
|
|
|
775
893
|
/** Set the active tools by name. */
|
|
776
894
|
setActiveTools(toolNames: string[]): Promise<void>;
|
|
777
895
|
|
|
896
|
+
/** Get available slash commands in the current session. */
|
|
897
|
+
getCommands(): SlashCommandInfo[];
|
|
898
|
+
|
|
778
899
|
/** Set the current model. Returns false if no API key available. */
|
|
779
900
|
setModel(model: Model): Promise<boolean>;
|
|
780
901
|
|
|
@@ -833,6 +954,8 @@ export type GetActiveToolsHandler = () => string[];
|
|
|
833
954
|
|
|
834
955
|
export type GetAllToolsHandler = () => string[];
|
|
835
956
|
|
|
957
|
+
export type GetCommandsHandler = () => SlashCommandInfo[];
|
|
958
|
+
|
|
836
959
|
export type SetActiveToolsHandler = (toolNames: string[]) => Promise<void>;
|
|
837
960
|
|
|
838
961
|
export type SetModelHandler = (model: Model) => Promise<boolean>;
|
|
@@ -855,6 +978,7 @@ export interface ExtensionActions {
|
|
|
855
978
|
getActiveTools: GetActiveToolsHandler;
|
|
856
979
|
getAllTools: GetAllToolsHandler;
|
|
857
980
|
setActiveTools: SetActiveToolsHandler;
|
|
981
|
+
getCommands: GetCommandsHandler;
|
|
858
982
|
setModel: SetModelHandler;
|
|
859
983
|
getThinkingLevel: GetThinkingLevelHandler;
|
|
860
984
|
setThinkingLevel: SetThinkingLevelHandler;
|
|
@@ -869,6 +993,7 @@ export interface ExtensionContextActions {
|
|
|
869
993
|
shutdown: () => void;
|
|
870
994
|
getContextUsage: () => ContextUsage | undefined;
|
|
871
995
|
compact: (instructionsOrOptions?: string | CompactOptions) => Promise<void>;
|
|
996
|
+
getSystemPrompt: () => string;
|
|
872
997
|
}
|
|
873
998
|
|
|
874
999
|
/** Actions for ExtensionCommandContext (ctx.* in command handlers). */
|
|
@@ -882,6 +1007,7 @@ export interface ExtensionCommandContextActions {
|
|
|
882
1007
|
branch: (entryId: string) => Promise<{ cancelled: boolean }>;
|
|
883
1008
|
navigateTree: (targetId: string, options?: { summarize?: boolean }) => Promise<{ cancelled: boolean }>;
|
|
884
1009
|
compact: (instructionsOrOptions?: string | CompactOptions) => Promise<void>;
|
|
1010
|
+
switchSession: (sessionPath: string) => Promise<{ cancelled: boolean }>;
|
|
885
1011
|
}
|
|
886
1012
|
|
|
887
1013
|
/** Full runtime = state + actions. */
|
|
@@ -32,7 +32,7 @@ export class RegisteredToolAdapter implements AgentTool<any, any, any> {
|
|
|
32
32
|
onUpdate?: AgentToolUpdateCallback<any>,
|
|
33
33
|
_context?: AgentToolContext,
|
|
34
34
|
) {
|
|
35
|
-
return this.registeredTool.definition.execute(toolCallId, params, onUpdate, this.runner.createContext()
|
|
35
|
+
return this.registeredTool.definition.execute(toolCallId, params, signal, onUpdate, this.runner.createContext());
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
renderCall?(args: any, theme: any) {
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsed git URL information.
|
|
3
|
+
*/
|
|
4
|
+
export type GitSource = {
|
|
5
|
+
/** Always "git" for git sources */
|
|
6
|
+
type: "git";
|
|
7
|
+
/** Clone URL (always valid for git clone, without ref suffix) */
|
|
8
|
+
repo: string;
|
|
9
|
+
/** Git host domain (e.g., "github.com") */
|
|
10
|
+
host: string;
|
|
11
|
+
/** Repository path (e.g., "user/repo") */
|
|
12
|
+
path: string;
|
|
13
|
+
/** Git ref (branch, tag, commit) if specified */
|
|
14
|
+
ref?: string;
|
|
15
|
+
/** True if ref was specified (package won't be auto-updated) */
|
|
16
|
+
pinned: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Known git hosts and their URL extraction logic. */
|
|
20
|
+
const KNOWN_HOSTS: Record<string, (pathname: string, hash: string) => { user: string; project: string } | null> = {
|
|
21
|
+
"github.com": extractStandard,
|
|
22
|
+
"gitlab.com": extractGitLab,
|
|
23
|
+
"bitbucket.org": extractStandard,
|
|
24
|
+
"git.sr.ht": extractStandard,
|
|
25
|
+
"codeberg.org": extractStandard,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function stripUrlCredentials(url: string): string {
|
|
29
|
+
if (!url.includes("://")) return url;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = new URL(url);
|
|
32
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return url;
|
|
33
|
+
if (!parsed.username && !parsed.password) return url;
|
|
34
|
+
parsed.username = "";
|
|
35
|
+
parsed.password = "";
|
|
36
|
+
return parsed.toString().replace(/\/$/, "");
|
|
37
|
+
} catch {
|
|
38
|
+
return url;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extractStandard(pathname: string, _hash: string): { user: string; project: string } | null {
|
|
43
|
+
const [, user, project] = pathname.split("/", 3);
|
|
44
|
+
if (!user || !project) return null;
|
|
45
|
+
return { user, project: project.replace(/\.git$/, "") };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractGitLab(pathname: string, _hash: string): { user: string; project: string } | null {
|
|
49
|
+
const path = pathname.startsWith("/") ? pathname.slice(1) : pathname;
|
|
50
|
+
if (path.includes("/-/") || path.includes("/archive.tar.gz")) return null;
|
|
51
|
+
const segments = path.split("/");
|
|
52
|
+
let project = segments.pop();
|
|
53
|
+
if (!project) return null;
|
|
54
|
+
project = project.replace(/\.git$/, "");
|
|
55
|
+
const user = segments.join("/");
|
|
56
|
+
if (!user || !project) return null;
|
|
57
|
+
return { user, project };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Try to parse a URL against known git hosts.
|
|
62
|
+
* Returns `{ domain, user, project, committish }` or null.
|
|
63
|
+
*/
|
|
64
|
+
function tryKnownHost(candidate: string): { domain: string; user: string; project: string; committish: string } | null {
|
|
65
|
+
let parsed: URL;
|
|
66
|
+
try {
|
|
67
|
+
parsed = new URL(candidate);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const hostname = parsed.hostname.startsWith("www.") ? parsed.hostname.slice(4) : parsed.hostname;
|
|
73
|
+
const extractor = KNOWN_HOSTS[hostname];
|
|
74
|
+
if (!extractor) return null;
|
|
75
|
+
|
|
76
|
+
const segments = extractor(parsed.pathname, parsed.hash);
|
|
77
|
+
if (!segments) return null;
|
|
78
|
+
|
|
79
|
+
let committish = "";
|
|
80
|
+
if (parsed.hash) {
|
|
81
|
+
try {
|
|
82
|
+
committish = decodeURIComponent(parsed.hash.slice(1));
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
domain: hostname,
|
|
90
|
+
user: segments.user,
|
|
91
|
+
project: segments.project,
|
|
92
|
+
committish,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function splitRef(url: string): { repo: string; ref?: string } {
|
|
97
|
+
const scpLikeMatch = url.match(/^git@([^:]+):(.+)$/);
|
|
98
|
+
if (scpLikeMatch) {
|
|
99
|
+
const pathWithMaybeRef = scpLikeMatch[2] ?? "";
|
|
100
|
+
const refSeparator = pathWithMaybeRef.indexOf("@");
|
|
101
|
+
if (refSeparator < 0) return { repo: url };
|
|
102
|
+
const repoPath = pathWithMaybeRef.slice(0, refSeparator);
|
|
103
|
+
const ref = pathWithMaybeRef.slice(refSeparator + 1);
|
|
104
|
+
if (!repoPath || !ref) return { repo: url };
|
|
105
|
+
return {
|
|
106
|
+
repo: `git@${scpLikeMatch[1] ?? ""}:${repoPath}`,
|
|
107
|
+
ref,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (url.includes("://")) {
|
|
112
|
+
try {
|
|
113
|
+
const parsed = new URL(url);
|
|
114
|
+
const pathWithMaybeRef = parsed.pathname.replace(/^\/+/, "");
|
|
115
|
+
const refSeparator = pathWithMaybeRef.indexOf("@");
|
|
116
|
+
if (refSeparator < 0) return { repo: url };
|
|
117
|
+
const repoPath = pathWithMaybeRef.slice(0, refSeparator);
|
|
118
|
+
const ref = pathWithMaybeRef.slice(refSeparator + 1);
|
|
119
|
+
if (!repoPath || !ref) return { repo: url };
|
|
120
|
+
parsed.pathname = `/${repoPath}`;
|
|
121
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
|
122
|
+
parsed.username = "";
|
|
123
|
+
parsed.password = "";
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
repo: parsed.toString().replace(/\/$/, ""),
|
|
127
|
+
ref,
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return { repo: url };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const slashIndex = url.indexOf("/");
|
|
135
|
+
if (slashIndex < 0) return { repo: url };
|
|
136
|
+
const host = url.slice(0, slashIndex);
|
|
137
|
+
const pathWithMaybeRef = url.slice(slashIndex + 1);
|
|
138
|
+
const refSeparator = pathWithMaybeRef.indexOf("@");
|
|
139
|
+
if (refSeparator < 0) return { repo: url };
|
|
140
|
+
const repoPath = pathWithMaybeRef.slice(0, refSeparator);
|
|
141
|
+
const ref = pathWithMaybeRef.slice(refSeparator + 1);
|
|
142
|
+
if (!repoPath || !ref) return { repo: url };
|
|
143
|
+
return { repo: `${host}/${repoPath}`, ref };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Try known-host parsing and build a GitSource from the result. */
|
|
147
|
+
function tryKnownHostSource(
|
|
148
|
+
split: { repo: string; ref?: string },
|
|
149
|
+
candidate: string,
|
|
150
|
+
repoUrl: string,
|
|
151
|
+
): GitSource | null {
|
|
152
|
+
const info = tryKnownHost(candidate);
|
|
153
|
+
if (!info) return null;
|
|
154
|
+
if (split.ref && info.project.includes("@")) return null;
|
|
155
|
+
return {
|
|
156
|
+
type: "git",
|
|
157
|
+
repo: stripUrlCredentials(repoUrl),
|
|
158
|
+
host: info.domain,
|
|
159
|
+
path: `${info.user}/${info.project}`.replace(/\.git$/, ""),
|
|
160
|
+
ref: info.committish || split.ref || undefined,
|
|
161
|
+
pinned: Boolean(info.committish || split.ref),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseGenericGitUrl(url: string): GitSource | null {
|
|
166
|
+
const { repo: repoWithoutRef, ref } = splitRef(url);
|
|
167
|
+
let repo = repoWithoutRef;
|
|
168
|
+
let host = "";
|
|
169
|
+
let repoPath = "";
|
|
170
|
+
|
|
171
|
+
const scpLikeMatch = repoWithoutRef.match(/^git@([^:]+):(.+)$/);
|
|
172
|
+
if (scpLikeMatch) {
|
|
173
|
+
host = scpLikeMatch[1] ?? "";
|
|
174
|
+
repoPath = scpLikeMatch[2] ?? "";
|
|
175
|
+
} else if (/^https?:\/\/|^ssh:\/\//.test(repoWithoutRef)) {
|
|
176
|
+
try {
|
|
177
|
+
const parsed = new URL(repoWithoutRef);
|
|
178
|
+
if (parsed.hash) {
|
|
179
|
+
try {
|
|
180
|
+
decodeURIComponent(parsed.hash.slice(1));
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
host = parsed.hostname;
|
|
186
|
+
repoPath = parsed.pathname.replace(/^\/+/, "");
|
|
187
|
+
repo = stripUrlCredentials(repoWithoutRef);
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
const slashIndex = repoWithoutRef.indexOf("/");
|
|
193
|
+
if (slashIndex < 0) return null;
|
|
194
|
+
repo = `https://${repoWithoutRef}`;
|
|
195
|
+
try {
|
|
196
|
+
const parsed = new URL(repo);
|
|
197
|
+
host = parsed.hostname;
|
|
198
|
+
repoPath = parsed.pathname.replace(/^\/+/, "");
|
|
199
|
+
repo = stripUrlCredentials(repo);
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
if (!host.includes(".") && host !== "localhost") return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const normalizedPath = repoPath.replace(/\.git$/, "").replace(/^\/+/, "");
|
|
207
|
+
if (!host || !normalizedPath || normalizedPath.split("/").length < 2) return null;
|
|
208
|
+
|
|
209
|
+
return { type: "git", repo, host, path: normalizedPath, ref, pinned: Boolean(ref) };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Parse any git URL (SSH or HTTPS) into a GitSource.
|
|
214
|
+
*
|
|
215
|
+
* Handles:
|
|
216
|
+
* - `git:` prefixed URLs (`git:github.com/user/repo`)
|
|
217
|
+
* - SSH SCP-like URLs (`git@github.com:user/repo`)
|
|
218
|
+
* - HTTPS/HTTP/SSH protocol URLs
|
|
219
|
+
* - Bare `host/user/repo` shorthand
|
|
220
|
+
* - Ref pinning via `@ref` suffix
|
|
221
|
+
*
|
|
222
|
+
* Recognizes GitHub, GitLab, Bitbucket, Sourcehut, and Codeberg natively.
|
|
223
|
+
* Falls back to generic URL parsing for other hosts.
|
|
224
|
+
*/
|
|
225
|
+
export function parseGitUrl(source: string): GitSource | null {
|
|
226
|
+
const url = source.startsWith("git:") ? source.slice(4).trim() : source;
|
|
227
|
+
const hashIndex = url.indexOf("#");
|
|
228
|
+
if (hashIndex >= 0) {
|
|
229
|
+
const hash = url.slice(hashIndex + 1);
|
|
230
|
+
if (hash) {
|
|
231
|
+
try {
|
|
232
|
+
decodeURIComponent(hash);
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const split = splitRef(url);
|
|
239
|
+
|
|
240
|
+
// SCP-like SSH URLs (git@host:user/repo) — convert to https for host matching
|
|
241
|
+
const scpMatch = split.repo.match(/^git@([^:]+):(.+)$/);
|
|
242
|
+
|
|
243
|
+
// Try known hosts with the repo URL directly
|
|
244
|
+
const directCandidates: string[] = [];
|
|
245
|
+
if (scpMatch) {
|
|
246
|
+
directCandidates.push(`https://${scpMatch[1]}/${scpMatch[2]}`);
|
|
247
|
+
} else if (/^https?:\/\/|^ssh:\/\//.test(split.repo)) {
|
|
248
|
+
directCandidates.push(split.repo);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const candidate of directCandidates) {
|
|
252
|
+
const withRef = split.ref ? `${candidate.replace(/#.*$/, "")}#${split.ref}` : candidate;
|
|
253
|
+
const needsHttps =
|
|
254
|
+
!split.repo.startsWith("http://") &&
|
|
255
|
+
!split.repo.startsWith("https://") &&
|
|
256
|
+
!split.repo.startsWith("ssh://") &&
|
|
257
|
+
!split.repo.startsWith("git@");
|
|
258
|
+
const result = tryKnownHostSource(split, withRef, needsHttps ? `https://${split.repo}` : split.repo);
|
|
259
|
+
if (result) return result;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Try with https:// prefix for bare host/user/repo shorthand
|
|
263
|
+
if (!split.repo.includes("://") && !split.repo.startsWith("git@")) {
|
|
264
|
+
const httpsCandidate = split.ref ? `https://${split.repo}#${split.ref}` : `https://${url}`;
|
|
265
|
+
const result = tryKnownHostSource(split, httpsCandidate, `https://${split.repo}`);
|
|
266
|
+
if (result) return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return parseGenericGitUrl(url);
|
|
270
|
+
}
|