@oh-my-pi/pi-coding-agent 12.6.0 → 12.7.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.
@@ -9,6 +9,7 @@ import type {
9
9
  ExtensionError,
10
10
  ExtensionUIContext,
11
11
  ExtensionUIDialogOptions,
12
+ TerminalInputHandler,
12
13
  } from "../../extensibility/extensions";
13
14
  import { HookEditorComponent } from "../../modes/components/hook-editor";
14
15
  import { HookInputComponent } from "../../modes/components/hook-input";
@@ -20,6 +21,7 @@ import { setTerminalTitle } from "../../utils/title-generator";
20
21
  export class ExtensionUiController {
21
22
  #hookSelectorOverlay: OverlayHandle | undefined;
22
23
  #hookInputOverlay: OverlayHandle | undefined;
24
+ #extensionTerminalInputUnsubscribers = new Set<() => void>();
23
25
 
24
26
  readonly #dialogOverlayOptions = {
25
27
  anchor: "bottom-center",
@@ -41,6 +43,7 @@ export class ExtensionUiController {
41
43
  confirm: (title, message, _dialogOptions) => this.showHookConfirm(title, message),
42
44
  input: (title, placeholder, _dialogOptions) => this.showHookInput(title, placeholder),
43
45
  notify: (message, type) => this.showHookNotify(message, type),
46
+ onTerminalInput: handler => this.addExtensionTerminalInputListener(handler),
44
47
  setStatus: (key, text) => this.setHookStatus(key, text),
45
48
  setWorkingMessage: message => this.ctx.setWorkingMessage(message),
46
49
  setWidget: (key, content) => this.setHookWidget(key, content),
@@ -157,6 +160,7 @@ export class ExtensionUiController {
157
160
  this.ctx.statusContainer.clear();
158
161
 
159
162
  // Create new session
163
+ this.clearExtensionTerminalInputListeners();
160
164
  const success = await this.ctx.session.newSession({ parentSession: options?.parentSession });
161
165
  if (!success) {
162
166
  return { cancelled: true };
@@ -351,6 +355,7 @@ export class ExtensionUiController {
351
355
  this.ctx.statusContainer.clear();
352
356
 
353
357
  // Create new session
358
+ this.clearExtensionTerminalInputListeners();
354
359
  const success = await this.ctx.session.newSession({ parentSession: options?.parentSession });
355
360
  if (!success) {
356
361
  return { cancelled: true };
@@ -450,6 +455,7 @@ export class ExtensionUiController {
450
455
  confirm: async (_title: string, _message: string, _dialogOptions) => false,
451
456
  input: async (_title: string, _placeholder?: string, _dialogOptions?: unknown) => undefined,
452
457
  notify: () => {},
458
+ onTerminalInput: () => () => {},
453
459
  setStatus: () => {},
454
460
  setWorkingMessage: () => {},
455
461
  setWidget: () => {},
@@ -726,6 +732,22 @@ export class ExtensionUiController {
726
732
  /**
727
733
  * Show an extension error in the UI.
728
734
  */
735
+ addExtensionTerminalInputListener(handler: TerminalInputHandler): () => void {
736
+ const unsubscribe = this.ctx.ui.addInputListener(handler);
737
+ this.#extensionTerminalInputUnsubscribers.add(unsubscribe);
738
+ return () => {
739
+ unsubscribe();
740
+ this.#extensionTerminalInputUnsubscribers.delete(unsubscribe);
741
+ };
742
+ }
743
+
744
+ clearExtensionTerminalInputListeners(): void {
745
+ for (const unsubscribe of this.#extensionTerminalInputUnsubscribers) {
746
+ unsubscribe();
747
+ }
748
+ this.#extensionTerminalInputUnsubscribers.clear();
749
+ }
750
+
729
751
  showExtensionError(extensionPath: string, error: string): void {
730
752
  const errorText = new Text(theme.fg("error", `Extension "${extensionPath}" error: ${error}`), 1, 0);
731
753
  this.ctx.chatContainer.addChild(errorText);
@@ -279,7 +279,9 @@ export class SelectorController {
279
279
 
280
280
  // Provider settings - update runtime preferences
281
281
  case "webSearchProvider":
282
- setPreferredSearchProvider(value as "auto" | "exa" | "perplexity" | "anthropic");
282
+ setPreferredSearchProvider(
283
+ value as "auto" | "exa" | "jina" | "zai" | "perplexity" | "anthropic" | "gemini" | "codex",
284
+ );
283
285
  break;
284
286
  case "imageProvider":
285
287
  setPreferredImageProvider(value as "auto" | "gemini" | "openrouter");
@@ -720,6 +720,7 @@ export class InteractiveMode implements InteractiveModeContext {
720
720
  this.#sttController.dispose();
721
721
  this.#sttController = undefined;
722
722
  }
723
+ this.#extensionUiController.clearExtensionTerminalInputListeners();
723
724
  this.statusLine.dispose();
724
725
  if (this.unsubscribe) {
725
726
  this.unsubscribe();
@@ -919,6 +920,7 @@ export class InteractiveMode implements InteractiveModeContext {
919
920
  }
920
921
 
921
922
  handleClearCommand(): Promise<void> {
923
+ this.#extensionUiController.clearExtensionTerminalInputListeners();
922
924
  return this.#commandController.handleClearCommand();
923
925
  }
924
926
 
@@ -164,6 +164,11 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
164
164
  );
165
165
  }
166
166
 
167
+ onTerminalInput(): () => void {
168
+ // Raw terminal input not supported in RPC mode
169
+ return () => {};
170
+ }
171
+
167
172
  notify(message: string, type?: "info" | "warning" | "error"): void {
168
173
  // Fire and forget - no response needed
169
174
  this.output({
@@ -51,10 +51,16 @@ import type {
51
51
  ExtensionCommandContext,
52
52
  ExtensionRunner,
53
53
  ExtensionUIContext,
54
+ MessageEndEvent,
55
+ MessageStartEvent,
56
+ MessageUpdateEvent,
54
57
  SessionBeforeBranchResult,
55
58
  SessionBeforeCompactResult,
56
59
  SessionBeforeSwitchResult,
57
60
  SessionBeforeTreeResult,
61
+ ToolExecutionEndEvent,
62
+ ToolExecutionStartEvent,
63
+ ToolExecutionUpdateEvent,
58
64
  TreePreparation,
59
65
  TurnEndEvent,
60
66
  TurnStartEvent,
@@ -102,6 +108,7 @@ import {
102
108
  pythonExecutionToText,
103
109
  } from "./messages";
104
110
  import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager";
111
+ import { getLatestCompactionEntry } from "./session-manager";
105
112
 
106
113
  /** Session-specific events that extend the core AgentEvent */
107
114
  export type AgentSessionEvent =
@@ -226,6 +233,7 @@ const noOpUIContext: ExtensionUIContext = {
226
233
  confirm: async (_title, _message, _dialogOptions) => false,
227
234
  input: async (_title, _placeholder, _dialogOptions) => undefined,
228
235
  notify: () => {},
236
+ onTerminalInput: () => () => {},
229
237
  setStatus: () => {},
230
238
  setWorkingMessage: () => {},
231
239
  setWidget: () => {},
@@ -874,6 +882,51 @@ export class AgentSession {
874
882
  };
875
883
  await this.#extensionRunner.emit(hookEvent);
876
884
  this.#turnIndex++;
885
+ } else if (event.type === "message_start") {
886
+ const extensionEvent: MessageStartEvent = {
887
+ type: "message_start",
888
+ message: event.message,
889
+ };
890
+ await this.#extensionRunner.emit(extensionEvent);
891
+ } else if (event.type === "message_update") {
892
+ const extensionEvent: MessageUpdateEvent = {
893
+ type: "message_update",
894
+ message: event.message,
895
+ assistantMessageEvent: event.assistantMessageEvent,
896
+ };
897
+ await this.#extensionRunner.emit(extensionEvent);
898
+ } else if (event.type === "message_end") {
899
+ const extensionEvent: MessageEndEvent = {
900
+ type: "message_end",
901
+ message: event.message,
902
+ };
903
+ await this.#extensionRunner.emit(extensionEvent);
904
+ } else if (event.type === "tool_execution_start") {
905
+ const extensionEvent: ToolExecutionStartEvent = {
906
+ type: "tool_execution_start",
907
+ toolCallId: event.toolCallId,
908
+ toolName: event.toolName,
909
+ args: event.args,
910
+ };
911
+ await this.#extensionRunner.emit(extensionEvent);
912
+ } else if (event.type === "tool_execution_update") {
913
+ const extensionEvent: ToolExecutionUpdateEvent = {
914
+ type: "tool_execution_update",
915
+ toolCallId: event.toolCallId,
916
+ toolName: event.toolName,
917
+ args: event.args,
918
+ partialResult: event.partialResult,
919
+ };
920
+ await this.#extensionRunner.emit(extensionEvent);
921
+ } else if (event.type === "tool_execution_end") {
922
+ const extensionEvent: ToolExecutionEndEvent = {
923
+ type: "tool_execution_end",
924
+ toolCallId: event.toolCallId,
925
+ toolName: event.toolName,
926
+ result: event.result,
927
+ isError: event.isError ?? false,
928
+ };
929
+ await this.#extensionRunner.emit(extensionEvent);
877
930
  } else if (event.type === "auto_compaction_start") {
878
931
  await this.#extensionRunner.emit({ type: "auto_compaction_start", reason: event.reason });
879
932
  } else if (event.type === "auto_compaction_end") {
@@ -2664,9 +2717,9 @@ Be thorough - include exact file paths, function names, error messages, and tech
2664
2717
  // The error shouldn't trigger another compaction since we already compacted.
2665
2718
  // Example: opus fails → switch to codex → compact → switch back to opus → opus error
2666
2719
  // is still in context but shouldn't trigger compaction again.
2667
- const compactionEntry = this.sessionManager.getBranch().find(e => e.type === "compaction");
2720
+ const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
2668
2721
  const errorIsFromBeforeCompaction =
2669
- compactionEntry && assistantMessage.timestamp < new Date(compactionEntry.timestamp).getTime();
2722
+ compactionEntry !== null && assistantMessage.timestamp < new Date(compactionEntry.timestamp).getTime();
2670
2723
 
2671
2724
  // Case 1: Overflow - LLM returned context overflow error
2672
2725
  if (sameModel && !errorIsFromBeforeCompaction && isContextOverflow(assistantMessage, contextWindow)) {
@@ -3959,6 +4012,35 @@ Be thorough - include exact file paths, function names, error messages, and tech
3959
4012
  const contextWindow = model.contextWindow ?? 0;
3960
4013
  if (contextWindow <= 0) return undefined;
3961
4014
 
4015
+ // After compaction, the last assistant usage reflects pre-compaction context size.
4016
+ // We can only trust usage from an assistant that responded after the latest compaction.
4017
+ // If no such assistant exists, context token count is unknown until the next LLM response.
4018
+ const branchEntries = this.sessionManager.getBranch();
4019
+ const latestCompaction = getLatestCompactionEntry(branchEntries);
4020
+
4021
+ if (latestCompaction) {
4022
+ // Check if there's a valid assistant usage after the compaction boundary
4023
+ const compactionIndex = branchEntries.lastIndexOf(latestCompaction);
4024
+ let hasPostCompactionUsage = false;
4025
+ for (let i = branchEntries.length - 1; i > compactionIndex; i--) {
4026
+ const entry = branchEntries[i];
4027
+ if (entry.type === "message" && entry.message.role === "assistant") {
4028
+ const assistant = entry.message;
4029
+ if (assistant.stopReason !== "aborted" && assistant.stopReason !== "error") {
4030
+ const contextTokens = calculateContextTokens(assistant.usage);
4031
+ if (contextTokens > 0) {
4032
+ hasPostCompactionUsage = true;
4033
+ }
4034
+ break;
4035
+ }
4036
+ }
4037
+ }
4038
+
4039
+ if (!hasPostCompactionUsage) {
4040
+ return { tokens: null, contextWindow, percent: null };
4041
+ }
4042
+ }
4043
+
3962
4044
  const estimate = this.#estimateContextTokens();
3963
4045
  const percent = (estimate.tokens / contextWindow) * 100;
3964
4046
 
@@ -3966,9 +4048,6 @@ Be thorough - include exact file paths, function names, error messages, and tech
3966
4048
  tokens: estimate.tokens,
3967
4049
  contextWindow,
3968
4050
  percent,
3969
- usageTokens: estimate.usageTokens,
3970
- trailingTokens: estimate.trailingTokens,
3971
- lastUsageIndex: estimate.lastUsageIndex,
3972
4051
  };
3973
4052
  }
3974
4053
 
@@ -3985,9 +4064,6 @@ Be thorough - include exact file paths, function names, error messages, and tech
3985
4064
  */
3986
4065
  #estimateContextTokens(): {
3987
4066
  tokens: number;
3988
- usageTokens: number;
3989
- trailingTokens: number;
3990
- lastUsageIndex: number | null;
3991
4067
  } {
3992
4068
  const messages = this.messages;
3993
4069
 
@@ -4014,9 +4090,6 @@ Be thorough - include exact file paths, function names, error messages, and tech
4014
4090
  }
4015
4091
  return {
4016
4092
  tokens: estimated,
4017
- usageTokens: 0,
4018
- trailingTokens: estimated,
4019
- lastUsageIndex: null,
4020
4093
  };
4021
4094
  }
4022
4095
 
@@ -4028,9 +4101,6 @@ Be thorough - include exact file paths, function names, error messages, and tech
4028
4101
 
4029
4102
  return {
4030
4103
  tokens: usageTokens + trailingTokens,
4031
- usageTokens,
4032
- trailingTokens,
4033
- lastUsageIndex,
4034
4104
  };
4035
4105
  }
4036
4106
 
@@ -684,7 +684,7 @@ function renderFindings(
684
684
  const findingContinue = isLastFinding ? " " : `${theme.tree.vertical} `;
685
685
 
686
686
  const { color } = getPriorityInfo(finding.priority);
687
- const titleText = finding.title.replace(/^\[P\d\]\s*/, "");
687
+ const titleText = finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled";
688
688
  const loc = `${path.basename(finding.file_path)}:${finding.line_start}`;
689
689
 
690
690
  lines.push(
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Unified Web Search Tool
3
3
  *
4
- * Single tool supporting Anthropic, Perplexity, Exa, Jina, Gemini, and Codex
4
+ * Single tool supporting Anthropic, Perplexity, Exa, Jina, Gemini, Codex, and Z.AI
5
5
  * providers with provider-specific parameters exposed conditionally.
6
6
  *
7
7
  * When EXA_API_KEY is available, additional specialized tools are exposed:
@@ -33,7 +33,7 @@ import { SearchProviderError } from "./types";
33
33
  export const webSearchSchema = Type.Object({
34
34
  query: Type.String({ description: "Search query" }),
35
35
  provider: Type.Optional(
36
- StringEnum(["auto", "exa", "jina", "anthropic", "perplexity", "gemini", "codex"], {
36
+ StringEnum(["auto", "exa", "jina", "zai", "anthropic", "perplexity", "gemini", "codex"], {
37
37
  description: "Search provider (default: auto)",
38
38
  }),
39
39
  ),
@@ -47,7 +47,7 @@ export const webSearchSchema = Type.Object({
47
47
 
48
48
  export type SearchParams = {
49
49
  query: string;
50
- provider?: "auto" | "exa" | "jina" | "anthropic" | "perplexity" | "gemini" | "codex";
50
+ provider?: "auto" | "exa" | "jina" | "zai" | "anthropic" | "perplexity" | "gemini" | "codex";
51
51
  recency?: "day" | "week" | "month" | "year";
52
52
  limit?: number;
53
53
  /** Maximum output tokens. Defaults to 4096. */
@@ -56,6 +56,8 @@ export type SearchParams = {
56
56
  temperature?: number;
57
57
  /** Number of search results to retrieve. Defaults to 10. */
58
58
  num_search_results?: number;
59
+ /** Disable provider fallback when explicit provider is selected (CLI/debug use). */
60
+ no_fallback?: boolean;
59
61
  };
60
62
 
61
63
  function formatProviderList(providers: SearchProvider[]): string {
@@ -68,6 +70,9 @@ function formatProviderError(error: unknown, provider: SearchProvider): string {
68
70
  return "Anthropic web search returned 404 (model or endpoint not found).";
69
71
  }
70
72
  if (error.status === 401 || error.status === 403) {
73
+ if (error.provider === "zai") {
74
+ return error.message;
75
+ }
71
76
  return `${getSearchProvider(error.provider).label} authorization failed (${error.status}). Check API key or base URL.`;
72
77
  }
73
78
  return error.message;
@@ -165,7 +170,12 @@ async function executeSearch(
165
170
  _toolCallId: string,
166
171
  params: SearchParams,
167
172
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
168
- const providers = await resolveProviderChain(params.provider);
173
+ const providers =
174
+ params.provider && params.provider !== "auto" && params.no_fallback
175
+ ? (await getSearchProvider(params.provider).isAvailable())
176
+ ? [getSearchProvider(params.provider)]
177
+ : []
178
+ : await resolveProviderChain(params.provider);
169
179
 
170
180
  if (providers.length === 0) {
171
181
  const message = "No web search provider configured.";
@@ -226,7 +236,7 @@ export async function runSearchQuery(
226
236
  /**
227
237
  * Web search tool implementation.
228
238
  *
229
- * Supports Anthropic, Perplexity, Exa, Jina, Gemini, and Codex providers with automatic fallback.
239
+ * Supports Anthropic, Perplexity, Exa, Jina, Gemini, Codex, and Z.AI providers with automatic fallback.
230
240
  * Session is accepted for interface consistency but not used.
231
241
  */
232
242
  export class SearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails> {
@@ -5,6 +5,7 @@ import { ExaProvider } from "./providers/exa";
5
5
  import { GeminiProvider } from "./providers/gemini";
6
6
  import { JinaProvider } from "./providers/jina";
7
7
  import { PerplexityProvider } from "./providers/perplexity";
8
+ import { ZaiProvider } from "./providers/zai";
8
9
  import type { SearchProviderId } from "./types";
9
10
 
10
11
  export type { SearchParams } from "./providers/base";
@@ -14,12 +15,13 @@ const SEARCH_PROVIDERS: Record<SearchProviderId, SearchProvider> = {
14
15
  exa: new ExaProvider(),
15
16
  jina: new JinaProvider(),
16
17
  perplexity: new PerplexityProvider(),
18
+ zai: new ZaiProvider(),
17
19
  anthropic: new AnthropicProvider(),
18
20
  gemini: new GeminiProvider(),
19
21
  codex: new CodexProvider(),
20
22
  } as const;
21
23
 
22
- const SEARCH_PROVIDER_ORDER: SearchProviderId[] = ["exa", "jina", "perplexity", "anthropic", "gemini", "codex"];
24
+ const SEARCH_PROVIDER_ORDER: SearchProviderId[] = ["exa", "jina", "perplexity", "anthropic", "gemini", "codex", "zai"];
23
25
 
24
26
  export function getSearchProvider(provider: SearchProviderId): SearchProvider {
25
27
  return SEARCH_PROVIDERS[provider];