@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.
- package/README.md +7 -1
- package/package.json +3 -3
- package/src/index.ts +26 -4
- package/src/patch/core.ts +69 -24
- package/src/patch/types.ts +22 -0
- package/src/prompt/build-system-prompt.ts +3 -0
- package/src/shell/parse-command.ts +0 -2
- package/src/shell/parse.ts +0 -6
- package/src/shell/tokenize.ts +12 -1
- package/src/tools/apply-patch-rendering.ts +352 -0
- package/src/tools/apply-patch-tool.ts +204 -28
- package/src/tools/codex-rendering.ts +57 -10
- package/src/tools/exec-command-state.ts +188 -16
- package/src/tools/exec-command-tool.ts +69 -25
- package/src/tools/exec-session-manager.ts +60 -4
- package/src/tools/web-search-tool.ts +6 -2
- package/src/tools/write-stdin-tool.ts +10 -2
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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)");
|