@howaboua/pi-codex-conversion 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { Type } from "@sinclair/typebox";
3
- import { Text } from "@mariozechner/pi-tui";
4
- import { renderExecCommandCall } from "./codex-rendering.ts";
3
+ import { Container, Text } from "@mariozechner/pi-tui";
4
+ import { renderExecCommandCall, renderGroupedExecCommandCall } from "./codex-rendering.ts";
5
5
  import type { ExecCommandTracker } from "./exec-command-state.ts";
6
6
  import type { ExecSessionManager, UnifiedExecResult } from "./exec-session-manager.ts";
7
7
  import { formatUnifiedExecResult } from "./unified-exec-format.ts";
@@ -56,6 +56,62 @@ function isUnifiedExecResult(details: unknown): details is UnifiedExecResult {
56
56
  return typeof details === "object" && details !== null;
57
57
  }
58
58
 
59
+ function createEmptyResultComponent(): Container {
60
+ return new Container();
61
+ }
62
+
63
+ interface ExecCommandRenderContextLike {
64
+ toolCallId?: string;
65
+ invalidate?: () => void;
66
+ }
67
+
68
+ const renderExecCommandCallWithOptionalContext: any = (
69
+ args: { cmd?: unknown },
70
+ theme: { fg(role: string, text: string): string; bold(text: string): string },
71
+ context: ExecCommandRenderContextLike | undefined,
72
+ tracker: ExecCommandTracker,
73
+ ) => {
74
+ const command = typeof args.cmd === "string" ? args.cmd : "";
75
+ tracker.registerRenderContext(context?.toolCallId, context?.invalidate ?? (() => {}));
76
+ const renderInfo = tracker.getRenderInfo(context?.toolCallId, command);
77
+ if (renderInfo.hidden) {
78
+ return new Text("", 0, 0);
79
+ }
80
+ const text = renderInfo.actionGroups
81
+ ? renderGroupedExecCommandCall(renderInfo.actionGroups, renderInfo.status, theme)
82
+ : renderExecCommandCall(command, renderInfo.status, theme);
83
+ return new Text(text, 0, 0);
84
+ };
85
+
86
+ const renderExecCommandResultWithOptionalContext: any = (
87
+ result: { content: Array<{ type: string; text?: string }>; details?: unknown },
88
+ options: { expanded: boolean; isPartial: boolean },
89
+ theme: { fg(role: string, text: string): string },
90
+ context: ExecCommandRenderContextLike | undefined,
91
+ tracker: ExecCommandTracker,
92
+ ) => {
93
+ if (options.isPartial || !options.expanded) {
94
+ return createEmptyResultComponent();
95
+ }
96
+
97
+ const command = context && "args" in context && context.args && typeof (context as any).args.cmd === "string" ? (context as any).args.cmd : undefined;
98
+ if (tracker.getRenderInfo(context?.toolCallId, command ?? "").hidden) {
99
+ return createEmptyResultComponent();
100
+ }
101
+
102
+ const details = isUnifiedExecResult(result.details) ? result.details : undefined;
103
+ const content = result.content.find((item) => item.type === "text");
104
+ const output = details?.output ?? (content?.type === "text" ? content.text : "");
105
+ let text = theme.fg("dim", output || "(no output)");
106
+ if (details?.session_id !== undefined) {
107
+ text += `\n${theme.fg("accent", `Session ${details.session_id} still running`)}`;
108
+ }
109
+ if (details?.exit_code !== undefined) {
110
+ text += `\n${theme.fg("muted", `Exit code: ${details.exit_code}`)}`;
111
+ }
112
+ return new Text(text, 0, 0);
113
+ };
114
+
59
115
  export function registerExecCommandTool(pi: ExtensionAPI, tracker: ExecCommandTracker, sessions: ExecSessionManager): void {
60
116
  pi.registerTool({
61
117
  name: "exec_command",
@@ -65,43 +121,31 @@ export function registerExecCommandTool(pi: ExtensionAPI, tracker: ExecCommandTr
65
121
  promptGuidelines: [
66
122
  "Use exec_command for search, listing files, and local text-file reads.",
67
123
  "Prefer rg or rg --files when possible.",
124
+ "For short or non-interactive commands, omit `yield_time_ms` so the default wait can avoid unnecessary follow-up calls.",
68
125
  "Keep tty disabled unless the command truly needs interactive terminal behavior.",
69
126
  ],
70
127
  parameters: EXEC_COMMAND_PARAMETERS,
71
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
128
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
72
129
  if (signal?.aborted) {
73
130
  throw new Error("exec_command aborted");
74
131
  }
75
132
  const typedParams = parseExecCommandParams(params);
76
133
  const result = await sessions.exec(typedParams, ctx.cwd, signal);
77
134
  if (result.session_id !== undefined) {
78
- tracker.recordPersistentSession(typedParams.cmd);
135
+ tracker.recordPersistentSession(toolCallId, result.session_id);
79
136
  }
80
137
  return {
81
138
  content: [{ type: "text", text: formatUnifiedExecResult(result, typedParams.cmd) }],
82
139
  details: result,
83
140
  };
84
141
  },
85
- renderCall(args, theme) {
86
- const command = typeof args.cmd === "string" ? args.cmd : "";
87
- return new Text(renderExecCommandCall(command, tracker.getState(command), theme), 0, 0);
88
- },
89
- renderResult(result, { expanded, isPartial }, theme) {
90
- if (isPartial || !expanded) {
91
- return undefined;
92
- }
93
-
94
- const details = isUnifiedExecResult(result.details) ? result.details : undefined;
95
- const content = result.content.find((item) => item.type === "text");
96
- const output = details?.output ?? (content?.type === "text" ? content.text : "");
97
- let text = theme.fg("dim", output || "(no output)");
98
- if (details?.session_id !== undefined) {
99
- text += `\n${theme.fg("accent", `Session ${details.session_id} still running`)}`;
100
- }
101
- if (details?.exit_code !== undefined) {
102
- text += `\n${theme.fg("muted", `Exit code: ${details.exit_code}`)}`;
103
- }
104
- return new Text(text, 0, 0);
105
- },
142
+ renderCall: ((args: { cmd?: unknown }, theme: { fg(role: string, text: string): string; bold(text: string): string }, context?: ExecCommandRenderContextLike) =>
143
+ renderExecCommandCallWithOptionalContext(args, theme, context, tracker)) as any,
144
+ renderResult: ((
145
+ result: { content: Array<{ type: string; text?: string }>; details?: unknown },
146
+ options: { expanded: boolean; isPartial: boolean },
147
+ theme: { fg(role: string, text: string): string },
148
+ context?: ExecCommandRenderContextLike,
149
+ ) => renderExecCommandResultWithOptionalContext(result, options, theme, context, tracker)) as any,
106
150
  });
107
151
  }
@@ -65,10 +65,19 @@ export interface ExecSessionManager {
65
65
  shutdown(): void;
66
66
  }
67
67
 
68
+ export interface ExecSessionManagerOptions {
69
+ defaultExecYieldTimeMs?: number;
70
+ defaultWriteYieldTimeMs?: number;
71
+ minNonInteractiveExecYieldTimeMs?: number;
72
+ minEmptyWriteYieldTimeMs?: number;
73
+ }
74
+
68
75
  const DEFAULT_EXEC_YIELD_TIME_MS = 10_000;
69
- const DEFAULT_WRITE_YIELD_TIME_MS = 10_000;
76
+ const DEFAULT_WRITE_YIELD_TIME_MS = 250;
70
77
  const DEFAULT_MAX_OUTPUT_TOKENS = 10_000;
71
78
  const MIN_YIELD_TIME_MS = 250;
79
+ const MIN_NON_INTERACTIVE_EXEC_YIELD_TIME_MS = 5_000;
80
+ const MIN_EMPTY_WRITE_YIELD_TIME_MS = 5_000;
72
81
  const MAX_YIELD_TIME_MS = 30_000;
73
82
  const MAX_COMMAND_HISTORY = 256;
74
83
 
@@ -139,6 +148,32 @@ function clampYieldTime(yieldTimeMs: number | undefined, fallback: number): numb
139
148
  return Math.min(MAX_YIELD_TIME_MS, Math.max(MIN_YIELD_TIME_MS, value));
140
149
  }
141
150
 
151
+ function clampExecYieldTime(
152
+ yieldTimeMs: number | undefined,
153
+ fallback: number,
154
+ isInteractive: boolean,
155
+ minNonInteractiveExecYieldTimeMs: number,
156
+ ): number {
157
+ const value = clampYieldTime(yieldTimeMs, fallback);
158
+ if (isInteractive) {
159
+ return value;
160
+ }
161
+ return Math.min(MAX_YIELD_TIME_MS, Math.max(minNonInteractiveExecYieldTimeMs, value));
162
+ }
163
+
164
+ function clampWriteYieldTime(
165
+ yieldTimeMs: number | undefined,
166
+ fallback: number,
167
+ isEmptyPoll: boolean,
168
+ minEmptyWriteYieldTimeMs: number,
169
+ ): number {
170
+ const value = clampYieldTime(yieldTimeMs, fallback);
171
+ if (!isEmptyPoll) {
172
+ return value;
173
+ }
174
+ return Math.min(MAX_YIELD_TIME_MS, Math.max(minEmptyWriteYieldTimeMs, value));
175
+ }
176
+
142
177
  function maxCharsForTokens(maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS): number {
143
178
  return Math.max(256, maxOutputTokens * 4);
144
179
  }
@@ -309,11 +344,21 @@ function registerAbortHandler(signal: AbortSignal | undefined, onAbort: () => vo
309
344
  return () => signal.removeEventListener("abort", abortListener);
310
345
  }
311
346
 
312
- export function createExecSessionManager(): ExecSessionManager {
347
+ export function createExecSessionManager(options: ExecSessionManagerOptions = {}): ExecSessionManager {
313
348
  let nextSessionId = 1;
314
349
  const sessions = new Map<number, ExecSession>();
315
350
  const commandHistory = new Map<number, string>();
316
351
  const exitListeners = new Set<(sessionId: number, command: string) => void>();
352
+ const defaultExecYieldTimeMs = options.defaultExecYieldTimeMs ?? DEFAULT_EXEC_YIELD_TIME_MS;
353
+ const defaultWriteYieldTimeMs = options.defaultWriteYieldTimeMs ?? DEFAULT_WRITE_YIELD_TIME_MS;
354
+ const minNonInteractiveExecYieldTimeMs = Math.min(
355
+ MAX_YIELD_TIME_MS,
356
+ Math.max(MIN_YIELD_TIME_MS, options.minNonInteractiveExecYieldTimeMs ?? MIN_NON_INTERACTIVE_EXEC_YIELD_TIME_MS),
357
+ );
358
+ const minEmptyWriteYieldTimeMs = Math.min(
359
+ MAX_YIELD_TIME_MS,
360
+ Math.max(MIN_YIELD_TIME_MS, options.minEmptyWriteYieldTimeMs ?? MIN_EMPTY_WRITE_YIELD_TIME_MS),
361
+ );
317
362
 
318
363
  function rememberCommand(sessionId: number, command: string): void {
319
364
  commandHistory.set(sessionId, command);
@@ -494,7 +539,10 @@ export function createExecSessionManager(): ExecSessionManager {
494
539
  sessions.set(session.id, session);
495
540
  rememberCommand(session.id, session.command);
496
541
 
497
- const waitedMs = await waitForExitOrTimeout(session, clampYieldTime(input.yield_time_ms, DEFAULT_EXEC_YIELD_TIME_MS));
542
+ const waitedMs = await waitForExitOrTimeout(
543
+ session,
544
+ clampExecYieldTime(input.yield_time_ms, defaultExecYieldTimeMs, session.interactive, minNonInteractiveExecYieldTimeMs),
545
+ );
498
546
  return makeResult(session, waitedMs, input.max_output_tokens);
499
547
  },
500
548
  write: async (input) => {
@@ -512,7 +560,15 @@ export function createExecSessionManager(): ExecSessionManager {
512
560
  }
513
561
  const waitedMs =
514
562
  session.exitCode === undefined
515
- ? await waitForExitOrTimeout(session, clampYieldTime(input.yield_time_ms, DEFAULT_WRITE_YIELD_TIME_MS))
563
+ ? await waitForExitOrTimeout(
564
+ session,
565
+ clampWriteYieldTime(
566
+ input.yield_time_ms,
567
+ defaultWriteYieldTimeMs,
568
+ !input.chars || input.chars.length === 0,
569
+ minEmptyWriteYieldTimeMs,
570
+ ),
571
+ )
516
572
  : 0;
517
573
  return makeResult(session, waitedMs, input.max_output_tokens);
518
574
  },
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
2
  import { Type } from "@sinclair/typebox";
3
- import { Box, Text } from "@mariozechner/pi-tui";
3
+ import { Box, Container, Text } from "@mariozechner/pi-tui";
4
4
  import { isOpenAICodexModel } from "../adapter/codex-model.ts";
5
5
 
6
6
  export const WEB_SEARCH_UNSUPPORTED_MESSAGE = "web_search is only available with the openai-codex provider";
@@ -56,6 +56,10 @@ function isWebSearchFunctionTool(tool: unknown): tool is FunctionToolPayload {
56
56
  return !!tool && typeof tool === "object" && (tool as FunctionToolPayload).type === "function" && (tool as FunctionToolPayload).name === "web_search";
57
57
  }
58
58
 
59
+ function createEmptyResultComponent(): Container {
60
+ return new Container();
61
+ }
62
+
59
63
  export function rewriteNativeWebSearchTool(payload: unknown, model: ExtensionContext["model"]): unknown {
60
64
  if (!supportsNativeWebSearch(model) || !payload || typeof payload !== "object") {
61
65
  return payload;
@@ -113,7 +117,7 @@ export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETE
113
117
  },
114
118
  renderResult(result, { expanded }, theme) {
115
119
  if (!expanded) {
116
- return undefined;
120
+ return createEmptyResultComponent();
117
121
  }
118
122
  const textBlock = result.content.find((item) => item.type === "text");
119
123
  const text = textBlock?.type === "text" ? textBlock.text : "(no output)";
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { Type } from "@sinclair/typebox";
3
- import { Text } from "@mariozechner/pi-tui";
3
+ import { Container, Text } from "@mariozechner/pi-tui";
4
4
  import { renderWriteStdinCall } from "./codex-rendering.ts";
5
5
  import type { ExecSessionManager, UnifiedExecResult } from "./exec-session-manager.ts";
6
6
  import { formatUnifiedExecResult } from "./unified-exec-format.ts";
@@ -100,12 +100,20 @@ function isUnifiedExecResult(details: unknown): details is UnifiedExecResult {
100
100
  return typeof details === "object" && details !== null;
101
101
  }
102
102
 
103
+ function createEmptyResultComponent(): Container {
104
+ return new Container();
105
+ }
106
+
103
107
  export function registerWriteStdinTool(pi: ExtensionAPI, sessions: ExecSessionManager): void {
104
108
  pi.registerTool({
105
109
  name: "write_stdin",
106
110
  label: "write_stdin",
107
111
  description: "Writes characters to an existing unified exec session and returns recent output.",
108
112
  promptSnippet: "Write to an exec session.",
113
+ promptGuidelines: [
114
+ "Use empty `chars` only to poll a running exec session.",
115
+ "When polling with empty `chars`, wait meaningfully between polls and do not repeatedly poll by reflex.",
116
+ ],
109
117
  parameters: WRITE_STDIN_PARAMETERS,
110
118
  async execute(_toolCallId, params) {
111
119
  const typed = parseWriteStdinParams(params);
@@ -129,7 +137,7 @@ export function registerWriteStdinTool(pi: ExtensionAPI, sessions: ExecSessionMa
129
137
  return new Text(renderWriteStdinCall(sessionId, input, command, theme), 0, 0);
130
138
  },
131
139
  renderResult(result, { expanded, isPartial }, theme) {
132
- if (isPartial || !expanded) return undefined;
140
+ if (isPartial || !expanded) return createEmptyResultComponent();
133
141
  const state = getResultState(result);
134
142
  const output = renderTerminalText(state.output);
135
143
  let text = theme.fg("dim", output || "(no output)");