@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.14
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 +63 -0
- package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-za420td8.md} +63 -0
- package/dist/cli.js +3658 -3627
- package/dist/types/advisor/delta-split.d.ts +24 -0
- package/dist/types/advisor/runtime.d.ts +2 -2
- package/dist/types/async/job-manager.d.ts +8 -1
- package/dist/types/cli/update-cli.d.ts +53 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-resolver.d.ts +15 -2
- package/dist/types/config/settings-schema.d.ts +14 -0
- package/dist/types/discovery/agents-md.d.ts +10 -1
- package/dist/types/eval/runner-cache.d.ts +12 -0
- package/dist/types/extensibility/extensions/runner.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +15 -4
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
- package/dist/types/lib/xai-http.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +8 -5
- package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +4 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
- package/dist/types/modes/interactive-mode.d.ts +17 -8
- package/dist/types/modes/types.d.ts +7 -10
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +24 -3
- package/dist/types/session/messages.d.ts +20 -0
- package/dist/types/session/retry-fallback-chains.d.ts +13 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +35 -5
- package/dist/types/session/turn-recovery.d.ts +34 -5
- package/dist/types/slash-commands/types.d.ts +5 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/tools/approval.d.ts +7 -0
- package/dist/types/tools/builtin-names.d.ts +1 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/think.d.ts +38 -0
- package/dist/types/tools/todo.d.ts +14 -15
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/local-date.d.ts +2 -0
- package/dist/types/vibe/runtime.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/brave.d.ts +8 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
- package/dist/types/web/search/providers/jina.d.ts +3 -3
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +4 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
- package/package.json +13 -13
- package/src/advisor/delta-split.ts +98 -0
- package/src/advisor/runtime.ts +321 -69
- package/src/async/job-manager.ts +14 -3
- package/src/cli/gallery-fixtures/agentic.ts +16 -0
- package/src/cli/plugin-cli.ts +30 -2
- package/src/cli/update-cli.ts +259 -24
- package/src/config/keybindings.ts +52 -9
- package/src/config/model-resolver.ts +19 -3
- package/src/config/settings-schema.ts +16 -0
- package/src/cursor.ts +10 -5
- package/src/discovery/agents-md.ts +61 -23
- package/src/eval/jl/kernel.ts +2 -20
- package/src/eval/py/kernel.ts +2 -20
- package/src/eval/rb/kernel.ts +2 -20
- package/src/eval/runner-cache.ts +41 -0
- package/src/exec/non-interactive-env.ts +14 -3
- package/src/extensibility/extensions/loader.ts +5 -2
- package/src/extensibility/extensions/runner.ts +184 -66
- package/src/extensibility/extensions/types.ts +26 -2
- package/src/extensibility/extensions/wrapper.ts +13 -7
- package/src/extensibility/plugins/marketplace/manager.ts +6 -2
- package/src/hindsight/client.ts +1 -1
- package/src/lib/xai-http.ts +0 -4
- package/src/lsp/client.ts +2 -0
- package/src/lsp/servers.ts +1 -1
- package/src/mcp/tool-bridge.ts +15 -6
- package/src/modes/components/agent-hub-renderer.ts +9 -3
- package/src/modes/components/agent-hub.ts +2 -1
- package/src/modes/components/status-line/component.ts +58 -6
- package/src/modes/components/status-line/segments.ts +12 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/user-message.ts +20 -5
- package/src/modes/controllers/event-controller.ts +48 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -7
- package/src/modes/controllers/input-controller.ts +25 -6
- package/src/modes/controllers/selector-controller.ts +5 -0
- package/src/modes/interactive-mode.ts +315 -129
- package/src/modes/rpc/rpc-frame.ts +13 -5
- package/src/modes/theme/tui-adapters.ts +4 -5
- package/src/modes/types.ts +13 -7
- package/src/modes/utils/hotkeys-markdown.ts +10 -6
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/registry/persisted-agents.ts +43 -8
- package/src/sdk.ts +146 -9
- package/src/session/agent-session-types.ts +4 -0
- package/src/session/agent-session.ts +91 -10
- package/src/session/messages.ts +98 -28
- package/src/session/retry-fallback-chains.ts +14 -0
- package/src/session/session-advisors.ts +32 -15
- package/src/session/session-history-format.ts +15 -1
- package/src/session/session-maintenance.ts +8 -8
- package/src/session/session-manager.ts +6 -2
- package/src/session/session-tools.ts +355 -183
- package/src/session/turn-recovery.ts +225 -47
- package/src/slash-commands/builtin-modes.ts +41 -12
- package/src/slash-commands/types.ts +5 -1
- package/src/task/executor.ts +92 -45
- package/src/task/structured-subagent.ts +5 -5
- package/src/tools/approval.ts +44 -10
- package/src/tools/builtin-names.ts +1 -2
- package/src/tools/fetch.ts +21 -2
- package/src/tools/image-gen.ts +6 -8
- package/src/tools/index.ts +8 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/think.ts +62 -0
- package/src/tools/todo.ts +70 -26
- package/src/tools/tts.ts +3 -2
- package/src/tools/write.ts +7 -3
- package/src/utils/local-date.ts +13 -0
- package/src/utils/tools-manager.ts +2 -2
- package/src/vibe/runtime.ts +22 -14
- package/src/web/kagi.ts +91 -34
- package/src/web/parallel.ts +11 -2
- package/src/web/scrapers/crates-io.ts +2 -2
- package/src/web/scrapers/discogs.ts +2 -2
- package/src/web/scrapers/docs-rs.ts +2 -2
- package/src/web/scrapers/github.ts +2 -2
- package/src/web/scrapers/musicbrainz.ts +1 -2
- package/src/web/scrapers/pubmed.ts +2 -2
- package/src/web/scrapers/sec-edgar.ts +2 -2
- package/src/web/search/providers/brave.ts +121 -46
- package/src/web/search/providers/codex.ts +88 -12
- package/src/web/search/providers/exa.ts +45 -10
- package/src/web/search/providers/firecrawl.ts +53 -11
- package/src/web/search/providers/gemini.ts +139 -27
- package/src/web/search/providers/jina.ts +48 -25
- package/src/web/search/providers/parallel.ts +23 -9
- package/src/web/search/providers/perplexity.ts +24 -7
- package/src/web/search/providers/searxng.ts +77 -1
- package/src/web/search/providers/tavily.ts +23 -22
- package/src/web/search/providers/tinyfish.ts +44 -10
- package/src/web/search/providers/xai.ts +85 -14
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extension runner - executes extensions and manages their lifecycle.
|
|
3
3
|
*/
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
5
|
import type {
|
|
5
6
|
AgentMessage,
|
|
6
7
|
AgentTool,
|
|
@@ -62,6 +63,7 @@ import type {
|
|
|
62
63
|
SessionStopEventResult,
|
|
63
64
|
ToolCallEvent,
|
|
64
65
|
ToolCallEventResult,
|
|
66
|
+
ToolRegistrationListener,
|
|
65
67
|
ToolResultEvent,
|
|
66
68
|
ToolResultEventResult,
|
|
67
69
|
UserBashEvent,
|
|
@@ -332,8 +334,15 @@ const noOpUIContext: ExtensionUIContext = {
|
|
|
332
334
|
setToolsExpanded: () => {},
|
|
333
335
|
};
|
|
334
336
|
|
|
337
|
+
interface ToolRegistrationScope {
|
|
338
|
+
pending: Set<Promise<void>>;
|
|
339
|
+
signal?: AbortSignal;
|
|
340
|
+
closed: boolean;
|
|
341
|
+
}
|
|
342
|
+
|
|
335
343
|
export class ExtensionRunner {
|
|
336
344
|
#uiContext: ExtensionUIContext;
|
|
345
|
+
#toolApprovalPreviewWaiter?: (toolCallId: string) => Promise<void>;
|
|
337
346
|
#errorListeners: Set<ExtensionErrorListener> = new Set();
|
|
338
347
|
#getModel: () => Model | undefined = () => undefined;
|
|
339
348
|
#isIdleFn: () => boolean = () => true;
|
|
@@ -352,6 +361,8 @@ export class ExtensionRunner {
|
|
|
352
361
|
#shutdownHandler: ShutdownHandler = () => {};
|
|
353
362
|
#getMemoryFn?: () => MemoryRuntimeContext | undefined;
|
|
354
363
|
#commandDiagnostics: Array<{ type: string; message: string; path: string }> = [];
|
|
364
|
+
#toolRegistrationScope = new AsyncLocalStorage<ToolRegistrationScope>();
|
|
365
|
+
#toolRegistrationBarrier: Promise<void> | undefined;
|
|
355
366
|
#initialized = false;
|
|
356
367
|
/**
|
|
357
368
|
* Buffer for `credential_disabled` events received via {@link emitCredentialDisabled}
|
|
@@ -521,7 +532,11 @@ export class ExtensionRunner {
|
|
|
521
532
|
this.runtime.appendEntry = actions.appendEntry;
|
|
522
533
|
this.runtime.getActiveTools = actions.getActiveTools;
|
|
523
534
|
this.runtime.getAllTools = actions.getAllTools;
|
|
524
|
-
this.runtime.setActiveTools =
|
|
535
|
+
this.runtime.setActiveTools = async toolNames => {
|
|
536
|
+
const registrationBarrier = this.#toolRegistrationBarrier;
|
|
537
|
+
if (registrationBarrier) await registrationBarrier;
|
|
538
|
+
await actions.setActiveTools(toolNames);
|
|
539
|
+
};
|
|
525
540
|
this.runtime.getCommands = actions.getCommands;
|
|
526
541
|
this.runtime.setModel = actions.setModel;
|
|
527
542
|
this.runtime.getThinkingLevel = actions.getThinkingLevel;
|
|
@@ -650,6 +665,18 @@ export class ExtensionRunner {
|
|
|
650
665
|
if (event.signal.aborted) return undefined;
|
|
651
666
|
return await this.emit({ type: "session_stop", ...event });
|
|
652
667
|
}
|
|
668
|
+
/** Registers the interactive transcript gate that must settle before a tool approval is presented. */
|
|
669
|
+
setToolApprovalPreviewWaiter(waiter: (toolCallId: string) => Promise<void>): () => void {
|
|
670
|
+
this.#toolApprovalPreviewWaiter = waiter;
|
|
671
|
+
return () => {
|
|
672
|
+
if (this.#toolApprovalPreviewWaiter === waiter) this.#toolApprovalPreviewWaiter = undefined;
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Waits until the interactive transcript can show the tool call being approved. */
|
|
677
|
+
async waitForToolApprovalPreview(toolCallId: string): Promise<void> {
|
|
678
|
+
await this.#toolApprovalPreviewWaiter?.(toolCallId);
|
|
679
|
+
}
|
|
653
680
|
|
|
654
681
|
getUIContext(): ExtensionUIContext {
|
|
655
682
|
return this.#uiContext;
|
|
@@ -674,6 +701,88 @@ export class ExtensionRunner {
|
|
|
674
701
|
return tools;
|
|
675
702
|
}
|
|
676
703
|
|
|
704
|
+
/** Get the effective registered tool for a name using normal last-extension-wins precedence. */
|
|
705
|
+
getRegisteredTool(name: string): RegisteredTool | undefined {
|
|
706
|
+
for (let index = this.extensions.length - 1; index >= 0; index -= 1) {
|
|
707
|
+
const tool = this.extensions[index]?.tools.get(name);
|
|
708
|
+
if (tool) return tool;
|
|
709
|
+
}
|
|
710
|
+
return undefined;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Observe tools registered after extension factories have loaded. Listener
|
|
715
|
+
* promises are drained before the lifecycle handler that registered them
|
|
716
|
+
* completes, keeping the model tool snapshot and system prompt coherent.
|
|
717
|
+
*/
|
|
718
|
+
onToolRegistered(listener: (tool: RegisteredTool, signal?: AbortSignal) => void | Promise<void>): () => void {
|
|
719
|
+
const subscriptions: Array<{ extension: Extension; listener: ToolRegistrationListener }> = [];
|
|
720
|
+
for (const extension of this.extensions) {
|
|
721
|
+
const trackRegistration = (pending: Promise<void>): void => {
|
|
722
|
+
const registrationBarrier = pending.then(
|
|
723
|
+
() => undefined,
|
|
724
|
+
() => undefined,
|
|
725
|
+
);
|
|
726
|
+
this.#toolRegistrationBarrier = registrationBarrier;
|
|
727
|
+
void registrationBarrier.then(() => {
|
|
728
|
+
if (this.#toolRegistrationBarrier === registrationBarrier) this.#toolRegistrationBarrier = undefined;
|
|
729
|
+
});
|
|
730
|
+
const scope = this.#toolRegistrationScope.getStore();
|
|
731
|
+
if (scope && !scope.closed) {
|
|
732
|
+
scope.pending.add(pending);
|
|
733
|
+
void pending.then(
|
|
734
|
+
() => scope.pending.delete(pending),
|
|
735
|
+
() => {},
|
|
736
|
+
);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
void pending.catch(error => {
|
|
740
|
+
this.emitError({
|
|
741
|
+
extensionPath: extension.path,
|
|
742
|
+
event: "tool_registration",
|
|
743
|
+
error: error instanceof Error ? error.message : String(error),
|
|
744
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
};
|
|
748
|
+
const wrapped: ToolRegistrationListener = toolName => {
|
|
749
|
+
const tool = extension.tools.get(toolName);
|
|
750
|
+
if (!tool) return;
|
|
751
|
+
try {
|
|
752
|
+
const scope = this.#toolRegistrationScope.getStore();
|
|
753
|
+
const registrationSignal =
|
|
754
|
+
scope && !scope.closed ? scope.signal : AbortSignal.timeout(extensionHandlerTimeoutMs);
|
|
755
|
+
const pending = listener(tool, registrationSignal);
|
|
756
|
+
if (pending) trackRegistration(pending);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
trackRegistration(Promise.reject(error));
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
extension.toolRegistrationListeners ??= new Set();
|
|
762
|
+
extension.toolRegistrationListeners.add(wrapped);
|
|
763
|
+
subscriptions.push({ extension, listener: wrapped });
|
|
764
|
+
}
|
|
765
|
+
return () => {
|
|
766
|
+
for (const subscription of subscriptions) {
|
|
767
|
+
subscription.extension.toolRegistrationListeners?.delete(subscription.listener);
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async #flushToolRegistrations(pendingRegistrations: Set<Promise<void>>): Promise<void> {
|
|
773
|
+
let firstFailure: PromiseRejectedResult | undefined;
|
|
774
|
+
while (pendingRegistrations.size > 0) {
|
|
775
|
+
const pending = Array.from(pendingRegistrations);
|
|
776
|
+
const settled = await Promise.allSettled(pending);
|
|
777
|
+
for (let index = 0; index < settled.length; index += 1) {
|
|
778
|
+
pendingRegistrations.delete(pending[index]);
|
|
779
|
+
const result = settled[index];
|
|
780
|
+
if (!firstFailure && result?.status === "rejected") firstFailure = result;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (firstFailure) throw firstFailure.reason;
|
|
784
|
+
}
|
|
785
|
+
|
|
677
786
|
/**
|
|
678
787
|
* Aggregate the registered CLI flags across a set of extensions (last write
|
|
679
788
|
* wins on name collision). Static so callers that need the flag set before a
|
|
@@ -928,45 +1037,73 @@ export class ExtensionRunner {
|
|
|
928
1037
|
ctx: ExtensionContext,
|
|
929
1038
|
ext: Extension,
|
|
930
1039
|
timeoutMs: number,
|
|
1040
|
+
onFailure?: (kind: "timeout" | "error", message: string) => TResult,
|
|
931
1041
|
): Promise<TResult | undefined> {
|
|
932
1042
|
const signal =
|
|
933
1043
|
event.type === "session_stop" && "signal" in event && event.signal instanceof AbortSignal
|
|
934
1044
|
? event.signal
|
|
935
1045
|
: undefined;
|
|
936
1046
|
if (signal?.aborted) return undefined;
|
|
1047
|
+
const registrationScope: ToolRegistrationScope = { pending: new Set(), closed: false };
|
|
1048
|
+
let handlerResult: TResult | typeof EXTENSION_HANDLER_TIMEOUT | typeof EXTENSION_HANDLER_ABORTED | undefined;
|
|
1049
|
+
let handlerFailure: { error: unknown } | undefined;
|
|
937
1050
|
try {
|
|
938
|
-
|
|
939
|
-
handlerSignal =>
|
|
1051
|
+
handlerResult = await raceHandlerWithTimeout(
|
|
1052
|
+
async handlerSignal => {
|
|
1053
|
+
registrationScope.signal = handlerSignal;
|
|
1054
|
+
let result: TResult | undefined;
|
|
1055
|
+
try {
|
|
1056
|
+
result = await this.#toolRegistrationScope.run(registrationScope, () =>
|
|
1057
|
+
handler(event, createHandlerContext(ctx, handlerSignal)),
|
|
1058
|
+
);
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
handlerFailure = { error };
|
|
1061
|
+
} finally {
|
|
1062
|
+
registrationScope.closed = true;
|
|
1063
|
+
}
|
|
1064
|
+
try {
|
|
1065
|
+
await this.#flushToolRegistrations(registrationScope.pending);
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
handlerFailure ??= { error };
|
|
1068
|
+
}
|
|
1069
|
+
return result;
|
|
1070
|
+
},
|
|
940
1071
|
timeoutMs,
|
|
941
1072
|
signal,
|
|
942
1073
|
);
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
handlerFailure = { error };
|
|
1076
|
+
} finally {
|
|
1077
|
+
registrationScope.closed = true;
|
|
1078
|
+
}
|
|
1079
|
+
if (handlerResult === EXTENSION_HANDLER_ABORTED) return undefined;
|
|
1080
|
+
if (handlerResult === EXTENSION_HANDLER_TIMEOUT) {
|
|
1081
|
+
const error = `handler timed out after ${timeoutMs}ms`;
|
|
1082
|
+
logger.warn("Extension handler timed out", {
|
|
1083
|
+
extensionPath: ext.path,
|
|
1084
|
+
event: event.type,
|
|
1085
|
+
timeoutMs,
|
|
1086
|
+
});
|
|
1087
|
+
this.emitError({
|
|
1088
|
+
extensionPath: ext.path,
|
|
1089
|
+
event: event.type,
|
|
1090
|
+
error,
|
|
1091
|
+
});
|
|
1092
|
+
return onFailure?.("timeout", error);
|
|
1093
|
+
}
|
|
1094
|
+
if (handlerFailure) {
|
|
1095
|
+
const message =
|
|
1096
|
+
handlerFailure.error instanceof Error ? handlerFailure.error.message : String(handlerFailure.error);
|
|
1097
|
+
const stack = handlerFailure.error instanceof Error ? handlerFailure.error.stack : undefined;
|
|
962
1098
|
this.emitError({
|
|
963
1099
|
extensionPath: ext.path,
|
|
964
1100
|
event: event.type,
|
|
965
1101
|
error: message,
|
|
966
1102
|
stack,
|
|
967
1103
|
});
|
|
968
|
-
return
|
|
1104
|
+
return onFailure?.("error", message);
|
|
969
1105
|
}
|
|
1106
|
+
return handlerResult as TResult | undefined;
|
|
970
1107
|
}
|
|
971
1108
|
|
|
972
1109
|
async emit<TEvent extends RunnerEmitEvent>(event: TEvent): Promise<RunnerEmitResult<TEvent>> {
|
|
@@ -1100,46 +1237,26 @@ export class ExtensionRunner {
|
|
|
1100
1237
|
if (!handlers || handlers.length === 0) continue;
|
|
1101
1238
|
|
|
1102
1239
|
for (const handler of handlers) {
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
extensionPath: ext.path,
|
|
1118
|
-
event: "tool_call",
|
|
1119
|
-
error,
|
|
1120
|
-
});
|
|
1121
|
-
return {
|
|
1122
|
-
block: true,
|
|
1123
|
-
reason: `Extension ${ext.path} timed out after ${timeoutMs}ms`,
|
|
1124
|
-
};
|
|
1125
|
-
}
|
|
1240
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
1241
|
+
handler,
|
|
1242
|
+
event,
|
|
1243
|
+
ctx,
|
|
1244
|
+
ext,
|
|
1245
|
+
timeoutMs,
|
|
1246
|
+
(kind, message) => ({
|
|
1247
|
+
block: true,
|
|
1248
|
+
reason:
|
|
1249
|
+
kind === "timeout"
|
|
1250
|
+
? `Extension ${ext.path} timed out after ${timeoutMs}ms`
|
|
1251
|
+
: `Extension ${ext.path} failed: ${message}`,
|
|
1252
|
+
}),
|
|
1253
|
+
);
|
|
1126
1254
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1255
|
+
if (handlerResult) {
|
|
1256
|
+
result = handlerResult;
|
|
1257
|
+
if (result.block) {
|
|
1258
|
+
return result;
|
|
1132
1259
|
}
|
|
1133
|
-
} catch (err) {
|
|
1134
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1135
|
-
const stack = err instanceof Error ? err.stack : undefined;
|
|
1136
|
-
this.emitError({
|
|
1137
|
-
extensionPath: ext.path,
|
|
1138
|
-
event: "tool_call",
|
|
1139
|
-
error: message,
|
|
1140
|
-
stack,
|
|
1141
|
-
});
|
|
1142
|
-
return { block: true, reason: `Extension ${ext.path} failed: ${message}` };
|
|
1143
1260
|
}
|
|
1144
1261
|
}
|
|
1145
1262
|
}
|
|
@@ -1242,13 +1359,14 @@ export class ExtensionRunner {
|
|
|
1242
1359
|
| InputEventResult
|
|
1243
1360
|
| undefined;
|
|
1244
1361
|
if (result?.handled) return result;
|
|
1245
|
-
if (result?.text !== undefined)
|
|
1246
|
-
|
|
1247
|
-
currentImages = result.images ?? currentImages;
|
|
1248
|
-
}
|
|
1362
|
+
if (result?.text !== undefined) currentText = result.text;
|
|
1363
|
+
if (result?.images !== undefined) currentImages = result.images;
|
|
1249
1364
|
}
|
|
1250
1365
|
}
|
|
1251
|
-
|
|
1366
|
+
const transformed: InputEventResult = {};
|
|
1367
|
+
if (currentText !== text) transformed.text = currentText;
|
|
1368
|
+
if (currentImages !== images) transformed.images = currentImages;
|
|
1369
|
+
return transformed;
|
|
1252
1370
|
}
|
|
1253
1371
|
|
|
1254
1372
|
async emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {
|
|
@@ -38,7 +38,16 @@ import type {
|
|
|
38
38
|
TSchema,
|
|
39
39
|
} from "@oh-my-pi/pi-ai";
|
|
40
40
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/oauth/types";
|
|
41
|
-
import type {
|
|
41
|
+
import type {
|
|
42
|
+
AutocompleteItem,
|
|
43
|
+
AutocompleteProvider,
|
|
44
|
+
Component,
|
|
45
|
+
EditorTheme,
|
|
46
|
+
KeyId,
|
|
47
|
+
OverlayHandle,
|
|
48
|
+
OverlayOptions,
|
|
49
|
+
TUI,
|
|
50
|
+
} from "@oh-my-pi/pi-tui";
|
|
42
51
|
import type { logger as PiLogger } from "@oh-my-pi/pi-utils";
|
|
43
52
|
import type { KeybindingsManager } from "../../config/keybindings";
|
|
44
53
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
@@ -105,6 +114,7 @@ import type {
|
|
|
105
114
|
} from "../shared-events";
|
|
106
115
|
import type { SlashCommandInfo } from "../slash-commands";
|
|
107
116
|
|
|
117
|
+
export type { OverlayHandle, OverlayOptions } from "@oh-my-pi/pi-tui";
|
|
108
118
|
export type { AppKeybinding, KeybindingsManager } from "../../config/keybindings";
|
|
109
119
|
export type { ExecOptions, ExecResult } from "../../exec/exec";
|
|
110
120
|
export type { AgentToolResult, AgentToolUpdateCallback };
|
|
@@ -214,6 +224,16 @@ export type ExtensionUiComponent = Component & { dispose?(): void };
|
|
|
214
224
|
export type ExtensionUiComponentFactory = (tui: TUI, theme: Theme) => ExtensionUiComponent;
|
|
215
225
|
export type ExtensionWidgetContent = string[] | ExtensionUiComponentFactory | undefined;
|
|
216
226
|
|
|
227
|
+
/** Options for `ExtensionUIContext.custom()` (overlay rendering of a custom component). */
|
|
228
|
+
export interface ExtensionCustomOptions {
|
|
229
|
+
/** Render the component as an overlay over the transcript instead of replacing the editor area. */
|
|
230
|
+
overlay?: boolean;
|
|
231
|
+
/** Static or lazily resolved overlay positioning/sizing options forwarded to `showOverlay`. */
|
|
232
|
+
overlayOptions?: OverlayOptions | (() => OverlayOptions);
|
|
233
|
+
/** Invoked with the overlay handle once the overlay is created (overlay mode only). */
|
|
234
|
+
onHandle?: (handle: OverlayHandle) => void;
|
|
235
|
+
}
|
|
236
|
+
|
|
217
237
|
/** Wrap the current autocomplete provider with additional behavior (pi-compatible). */
|
|
218
238
|
export type AutocompleteProviderFactory = (current: AutocompleteProvider) => AutocompleteProvider;
|
|
219
239
|
|
|
@@ -280,7 +300,7 @@ export interface ExtensionUIContext {
|
|
|
280
300
|
keybindings: KeybindingsManager,
|
|
281
301
|
done: (result: T) => void,
|
|
282
302
|
) => ExtensionUiComponent | Promise<ExtensionUiComponent>,
|
|
283
|
-
options?:
|
|
303
|
+
options?: ExtensionCustomOptions,
|
|
284
304
|
): Promise<T>;
|
|
285
305
|
|
|
286
306
|
/** Set the text in the core input editor. */
|
|
@@ -1467,6 +1487,9 @@ export interface RegisteredTool<TParams extends TSchema = TSchema, TDetails = un
|
|
|
1467
1487
|
extensionPath: string;
|
|
1468
1488
|
}
|
|
1469
1489
|
|
|
1490
|
+
/** Internal observer invoked when an already-loaded extension registers or replaces a tool. */
|
|
1491
|
+
export type ToolRegistrationListener = (toolName: string) => void;
|
|
1492
|
+
|
|
1470
1493
|
export interface ExtensionFlag {
|
|
1471
1494
|
name: string;
|
|
1472
1495
|
description?: string;
|
|
@@ -1589,6 +1612,7 @@ export interface Extension {
|
|
|
1589
1612
|
label?: string;
|
|
1590
1613
|
handlers: Map<string, HandlerFn[]>;
|
|
1591
1614
|
tools: Map<string, RegisteredTool<any, any>>;
|
|
1615
|
+
toolRegistrationListeners?: Set<ToolRegistrationListener>;
|
|
1592
1616
|
assistantThinkingRenderers: AssistantThinkingRenderer[];
|
|
1593
1617
|
messageRenderers: Map<string, MessageRenderer>;
|
|
1594
1618
|
commands: Map<string, RegisteredCommand>;
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
ToolLoadMode,
|
|
10
10
|
} from "@oh-my-pi/pi-agent-core";
|
|
11
11
|
import type { ComputerSafetyCheck, ImageContent, Static, TextContent, TSchema } from "@oh-my-pi/pi-ai";
|
|
12
|
-
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
12
|
+
import { sanitizeText, untilAborted } from "@oh-my-pi/pi-utils";
|
|
13
13
|
import type { Settings } from "../../config/settings";
|
|
14
14
|
import type { Theme } from "../../modes/theme/theme";
|
|
15
15
|
import { type ApprovalMode, formatApprovalPrompt, resolveApproval, truncateForPrompt } from "../../tools/approval";
|
|
@@ -190,10 +190,11 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
190
190
|
const configuredMode = (settings?.get("tools.approvalMode") ?? "yolo") as ApprovalMode;
|
|
191
191
|
const approvalMode: ApprovalMode = cliAutoApprove ? "yolo" : configuredMode;
|
|
192
192
|
const userPolicies = (settings?.get("tools.approval") ?? {}) as Record<string, unknown>;
|
|
193
|
-
|
|
193
|
+
const preResolved = resolveApproval(this.tool, approvalArgs(params, context), approvalMode, userPolicies);
|
|
194
|
+
if (preResolved.policy === "deny") {
|
|
194
195
|
throw new Error(
|
|
195
|
-
`Tool "${this.tool.name}" is blocked by user policy.\n` +
|
|
196
|
-
`To allow: remove "tools.approval.${this.tool.name}: deny" from config.`,
|
|
196
|
+
`Tool "${preResolved.policyKey ?? this.tool.name}" is blocked by user policy.\n` +
|
|
197
|
+
`To allow: remove "tools.approval.${preResolved.policyKey ?? this.tool.name}: deny" from config.`,
|
|
197
198
|
);
|
|
198
199
|
}
|
|
199
200
|
|
|
@@ -242,8 +243,8 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
242
243
|
context?.xdevTierResolved?.(resolved.tier);
|
|
243
244
|
if (resolved.policy === "deny") {
|
|
244
245
|
throw new Error(
|
|
245
|
-
`Tool "${this.tool.name}" is blocked by user policy.\n` +
|
|
246
|
-
`To allow: remove "tools.approval.${this.tool.name}: deny" from config.`,
|
|
246
|
+
`Tool "${resolved.policyKey ?? this.tool.name}" is blocked by user policy.\n` +
|
|
247
|
+
`To allow: remove "tools.approval.${resolved.policyKey ?? this.tool.name}: deny" from config.`,
|
|
247
248
|
);
|
|
248
249
|
}
|
|
249
250
|
const pendingSafetyChecks = computerSafetyChecks(context);
|
|
@@ -255,7 +256,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
255
256
|
// and tool-demanded overrides still prompt. Provider safety checks are
|
|
256
257
|
// stronger: yolo, per-tool allow, and xdev approval never acknowledge
|
|
257
258
|
// them on the user's behalf.
|
|
258
|
-
const explicitPrompt = resolved.override || Object.hasOwn(userPolicies, this.tool.name);
|
|
259
|
+
const explicitPrompt = resolved.override || Object.hasOwn(userPolicies, resolved.policyKey ?? this.tool.name);
|
|
259
260
|
const xdevBypass = context?.xdevApproved === true && effectiveParams === params;
|
|
260
261
|
const approvalCheck = {
|
|
261
262
|
required: pendingSafetyChecks.length > 0 || (resolved.policy === "prompt" && (explicitPrompt || !xdevBypass)),
|
|
@@ -263,6 +264,11 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
263
264
|
};
|
|
264
265
|
|
|
265
266
|
if (approvalCheck.required) {
|
|
267
|
+
const scheduledCall = context?.toolCall?.toolCalls[context.toolCall.index];
|
|
268
|
+
if (scheduledCall?.id === toolCallId && scheduledCall.name === this.tool.name) {
|
|
269
|
+
await untilAborted(signal, () => this.runner.waitForToolApprovalPreview(toolCallId));
|
|
270
|
+
}
|
|
271
|
+
|
|
266
272
|
const hasApprovalHandlers =
|
|
267
273
|
this.runner.hasHandlers("tool_approval_requested") || this.runner.hasHandlers("tool_approval_resolved");
|
|
268
274
|
const sessionId = context?.sessionManager?.getSessionId() ?? "";
|
|
@@ -440,14 +440,14 @@ export class MarketplaceManager {
|
|
|
440
440
|
return "0.0.0";
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
|
|
443
|
+
/** Validates and removes a marketplace plugin, or only validates when `dryRun` is set. */
|
|
444
|
+
async uninstallPlugin(pluginId: string, scope?: "user" | "project", options?: { dryRun?: boolean }): Promise<void> {
|
|
444
445
|
const parsed = parsePluginId(pluginId);
|
|
445
446
|
if (!parsed) {
|
|
446
447
|
throw new Error(`Invalid plugin ID format: "${pluginId}". Expected "name@marketplace".`);
|
|
447
448
|
}
|
|
448
449
|
|
|
449
450
|
const { userEntries, projectEntries, userReg, projectReg } = await this.#findInBothRegistries(pluginId);
|
|
450
|
-
|
|
451
451
|
const inUser = userEntries && userEntries.length > 0;
|
|
452
452
|
const inProject = projectEntries && projectEntries.length > 0;
|
|
453
453
|
|
|
@@ -481,6 +481,10 @@ export class MarketplaceManager {
|
|
|
481
481
|
const registryPath = this.#registryPath(targetScope);
|
|
482
482
|
const packageNames = await this.#resolveInstalledPackageNames(targetEntries, parsed.name);
|
|
483
483
|
|
|
484
|
+
if (options?.dryRun) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
484
488
|
const updatedReg = removeInstalledPlugin(targetReg, pluginId);
|
|
485
489
|
await writeInstalledPluginsRegistry(registryPath, updatedReg);
|
|
486
490
|
|
package/src/hindsight/client.ts
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* tests to spy on.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
11
12
|
import { isTimeoutError, withTimeoutSignal } from "../utils/fetch-timeout";
|
|
12
13
|
import type { HindsightConfig } from "./config";
|
|
13
14
|
|
|
14
|
-
const USER_AGENT = "oh-my-pi-coding-agent";
|
|
15
15
|
const DEFAULT_USER_AGENT = USER_AGENT;
|
|
16
16
|
/** Fallback deadlines (ms) applied when the caller supplies no override. */
|
|
17
17
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
package/src/lib/xai-http.ts
CHANGED
|
@@ -12,10 +12,6 @@ interface XAICredentials {
|
|
|
12
12
|
baseURL: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function ohMyPiXAIUserAgent(): string {
|
|
16
|
-
return "oh-my-pi/xai";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
15
|
/** xAI provider ids supported by shared HTTP tool transport resolution. */
|
|
20
16
|
export type XAIHttpProvider = "xai-oauth" | "xai";
|
|
21
17
|
|
package/src/lsp/client.ts
CHANGED
|
@@ -1339,6 +1339,7 @@ export async function sendRequest(
|
|
|
1339
1339
|
timeout = setTimeout(() => {
|
|
1340
1340
|
if (client.pendingRequests.has(id)) {
|
|
1341
1341
|
client.pendingRequests.delete(id);
|
|
1342
|
+
void sendNotification(client, "$/cancelRequest", { id }).catch(() => {});
|
|
1342
1343
|
const err = new Error(`LSP request ${method} timed out after ${effectiveTimeoutMs}ms`);
|
|
1343
1344
|
cleanup();
|
|
1344
1345
|
reject(err);
|
|
@@ -1405,6 +1406,7 @@ export async function sendNotification(
|
|
|
1405
1406
|
* Shutdown all LSP clients.
|
|
1406
1407
|
*/
|
|
1407
1408
|
export async function shutdownAll(): Promise<void> {
|
|
1409
|
+
stopIdleChecker();
|
|
1408
1410
|
const clientsToShutdown = Array.from(clients.values());
|
|
1409
1411
|
clients.clear();
|
|
1410
1412
|
// Mid-initialize clients live only in clientLocks (publication is deferred
|
package/src/lsp/servers.ts
CHANGED
|
@@ -197,9 +197,9 @@ export function getConfig(cwd: string): LspConfig {
|
|
|
197
197
|
let config = configCache.get(cwd);
|
|
198
198
|
if (!config) {
|
|
199
199
|
config = loadConfig(cwd);
|
|
200
|
-
setIdleTimeout(config.idleTimeoutMs);
|
|
201
200
|
configCache.set(cwd, config);
|
|
202
201
|
}
|
|
202
|
+
setIdleTimeout(config.idleTimeoutMs);
|
|
203
203
|
return config;
|
|
204
204
|
}
|
|
205
205
|
|
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -357,6 +357,18 @@ export function createMCPToolName(serverName: string, toolName: string): string
|
|
|
357
357
|
return `mcp__${sanitizedServerName}_${normalizedToolName}`;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
export interface MCPToolOriginSource {
|
|
361
|
+
readonly name: string;
|
|
362
|
+
readonly mcpServerName?: unknown;
|
|
363
|
+
readonly mcpToolName?: unknown;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Stable identity for a tool's original MCP route, before its public name was normalized. */
|
|
367
|
+
export function getMCPToolOriginKey(tool: MCPToolOriginSource): string | undefined {
|
|
368
|
+
if (typeof tool.mcpServerName !== "string" || typeof tool.mcpToolName !== "string") return undefined;
|
|
369
|
+
return `${tool.mcpServerName}\u0000${tool.mcpToolName}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
360
372
|
/**
|
|
361
373
|
* Keeps one MCP tool per minted name and logs collisions between distinct MCP
|
|
362
374
|
* origins. The winner is chosen by a stable origin key (server name + original
|
|
@@ -365,19 +377,16 @@ export function createMCPToolName(serverName: string, toolName: string): string
|
|
|
365
377
|
* silently flip ownership of the minted name. Non-MCP tools pass through
|
|
366
378
|
* unchanged.
|
|
367
379
|
*/
|
|
368
|
-
export function deduplicateMCPToolsByName<T extends
|
|
369
|
-
tools: readonly T[],
|
|
370
|
-
): T[] {
|
|
380
|
+
export function deduplicateMCPToolsByName<T extends MCPToolOriginSource>(tools: readonly T[]): T[] {
|
|
371
381
|
const deduplicated: T[] = [];
|
|
372
382
|
const registered = new Map<string, { tool: T; originKey: string; index: number }>();
|
|
373
383
|
|
|
374
384
|
for (const tool of tools) {
|
|
375
|
-
|
|
385
|
+
const originKey = getMCPToolOriginKey(tool);
|
|
386
|
+
if (originKey === undefined) {
|
|
376
387
|
deduplicated.push(tool);
|
|
377
388
|
continue;
|
|
378
389
|
}
|
|
379
|
-
|
|
380
|
-
const originKey = `${tool.mcpServerName}\u0000${tool.mcpToolName}`;
|
|
381
390
|
const existing = registered.get(tool.name);
|
|
382
391
|
if (!existing) {
|
|
383
392
|
registered.set(tool.name, { tool, originKey, index: deduplicated.length });
|
|
@@ -95,19 +95,25 @@ function formatResolvedModelBadge(resolved: string, preserveProvider = false, fa
|
|
|
95
95
|
/**
|
|
96
96
|
* Resolved model + reasoning level for a hub row. Exact executor progress is
|
|
97
97
|
* authoritative (and survives completion); direct live sessions are the
|
|
98
|
-
* fallback for agents without an observer snapshot
|
|
98
|
+
* fallback for agents without an observer snapshot — the main session has no
|
|
99
|
+
* snapshot at all, so its row is read straight off the live session.
|
|
100
|
+
*
|
|
101
|
+
* Every source reports the model that produced the row's work, never the one
|
|
102
|
+
* the session merely points at: an armed fallback that has not served yet stays
|
|
103
|
+
* attributed to whichever model last actually spoke.
|
|
99
104
|
*/
|
|
100
105
|
export function modelBadge(ref: AgentRef, observed: ObservableSession | undefined): string | undefined {
|
|
101
106
|
const progress = observed?.progress;
|
|
102
107
|
const liveThinkingLevel = ref.session?.thinkingLevel;
|
|
108
|
+
const serving = ref.session?.servingModel;
|
|
103
109
|
const fallbackSelector =
|
|
104
|
-
|
|
110
|
+
(serving?.isFallback ? serving.selector : undefined) ??
|
|
105
111
|
(progress?.resolvedModelIsFallback ? progress.resolvedModel : undefined) ??
|
|
106
112
|
(ref.history?.resolvedModelIsFallback ? ref.history.resolvedModel : undefined);
|
|
107
113
|
if (fallbackSelector) {
|
|
108
114
|
return `${theme.fg("warning", "fallback →")} ${formatResolvedModelBadge(fallbackSelector, true, liveThinkingLevel)}`;
|
|
109
115
|
}
|
|
110
|
-
const resolvedModel = progress?.resolvedModel ?? ref.history?.resolvedModel;
|
|
116
|
+
const resolvedModel = progress?.resolvedModel ?? ref.history?.resolvedModel ?? serving?.selector;
|
|
111
117
|
if (resolvedModel) return formatResolvedModelBadge(resolvedModel, false, liveThinkingLevel);
|
|
112
118
|
const model = ref.session?.model;
|
|
113
119
|
if (!model) return undefined;
|
|
@@ -36,6 +36,7 @@ import { type AgentRef, AgentRegistry, type AgentStatus, MAIN_AGENT_ID } from ".
|
|
|
36
36
|
import { registerPersistedSubagents } from "../../registry/persisted-agents";
|
|
37
37
|
import { USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
38
38
|
import { shortenPath, truncateToWidth } from "../../tools/render-utils";
|
|
39
|
+
import { formatLocalDateTimeWithOffset } from "../../utils/local-date";
|
|
39
40
|
import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
|
|
40
41
|
import { theme } from "../theme/theme";
|
|
41
42
|
import { matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
|
|
@@ -829,7 +830,7 @@ export class AgentHubOverlayComponent extends Container implements SelectListMou
|
|
|
829
830
|
`Spawned by ${sanitizeDisplayText(ref.parentId ?? MAIN_AGENT_ID)}${children.length > 0 ? ` · ${children.length} children` : ""}`,
|
|
830
831
|
);
|
|
831
832
|
if (children.length > 0) add(theme.fg("dim", formatChildIds(children, width)));
|
|
832
|
-
add(theme.fg("dim", `Registered ${new Date(ref.createdAt)
|
|
833
|
+
add(theme.fg("dim", `Registered ${formatLocalDateTimeWithOffset(new Date(ref.createdAt))}`));
|
|
833
834
|
|
|
834
835
|
section("Changes");
|
|
835
836
|
add(
|