@howaboua/pi-codex-conversion 1.0.4 → 1.0.6

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 CHANGED
@@ -10,6 +10,9 @@ This package replaces Pi's default Codex/GPT experience with a narrower Codex-li
10
10
 
11
11
  ![Available tools](./available-tools.png)
12
12
 
13
+ > [!NOTE]
14
+ > Native OpenAI Codex Responses web search runs silently. Pi does not expose native web-search usage events to extensions, so the adapter shows a one-time session notice instead of per-search tool-call history.
15
+
13
16
  ## Active tools in adapter mode
14
17
 
15
18
  When the adapter is active, the LLM sees these tools:
@@ -55,6 +58,7 @@ npm run check
55
58
  - `write_stdin({ session_id, chars: "y\\n" })` renders like `Interacted with background terminal`
56
59
  - `view_image({ path: "/absolute/path/to/screenshot.png" })` is available on image-capable models
57
60
  - `web_search` is surfaced only on `openai-codex`, and the adapter rewrites it into the native OpenAI Responses `type: "web_search"` payload instead of executing a local function tool
61
+ - when native web search is available, the adapter shows a one-time session notice; individual searches are not surfaced because Pi does not expose native web-search execution events to extensions
58
62
 
59
63
  Raw command output is still available by expanding the tool result.
60
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/pi-codex-conversion",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Codex-oriented tool and prompt adapter for pi coding agent",
5
5
  "type": "module",
6
6
  "repository": {
package/src/index.ts CHANGED
@@ -8,12 +8,13 @@ import { createExecSessionManager } from "./tools/exec-session-manager.ts";
8
8
  import { buildCodexSystemPrompt, extractPiPromptSkills, type PromptSkill } from "./prompt/build-system-prompt.ts";
9
9
  import { registerViewImageTool, supportsOriginalImageDetail } from "./tools/view-image-tool.ts";
10
10
  import {
11
- WEB_SEARCH_ACTIVITY_MESSAGE_TYPE,
12
- payloadContainsWebSearchTool,
13
- registerWebSearchMessageRenderer,
14
11
  registerWebSearchTool,
12
+ registerWebSearchSessionNoteRenderer,
15
13
  rewriteNativeWebSearchTool,
14
+ shouldShowWebSearchSessionNote,
16
15
  supportsNativeWebSearch,
16
+ WEB_SEARCH_SESSION_NOTE_TEXT,
17
+ WEB_SEARCH_SESSION_NOTE_TYPE,
17
18
  } from "./tools/web-search-tool.ts";
18
19
  import { registerWriteStdinTool } from "./tools/write-stdin-tool.ts";
19
20
 
@@ -21,7 +22,7 @@ interface AdapterState {
21
22
  enabled: boolean;
22
23
  previousToolNames?: string[];
23
24
  promptSkills: PromptSkill[];
24
- pendingWebSearchCount: number;
25
+ webSearchNoticeShown: boolean;
25
26
  }
26
27
 
27
28
  const ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, VIEW_IMAGE_TOOL_NAME, WEB_SEARCH_TOOL_NAME];
@@ -35,20 +36,21 @@ function getCommandArg(args: unknown): string | undefined {
35
36
 
36
37
  export default function codexConversion(pi: ExtensionAPI) {
37
38
  const tracker = createExecCommandTracker();
38
- const state: AdapterState = { enabled: false, promptSkills: [], pendingWebSearchCount: 0 };
39
+ const state: AdapterState = { enabled: false, promptSkills: [], webSearchNoticeShown: false };
39
40
  const sessions = createExecSessionManager();
40
41
 
41
42
  registerApplyPatchTool(pi);
42
43
  registerExecCommandTool(pi, tracker, sessions);
43
44
  registerWriteStdinTool(pi, sessions);
44
45
  registerWebSearchTool(pi);
45
- registerWebSearchMessageRenderer(pi);
46
+ registerWebSearchSessionNoteRenderer(pi);
46
47
 
47
48
  sessions.onSessionExit((_sessionId, command) => {
48
49
  tracker.recordCommandFinished(command);
49
50
  });
50
51
 
51
52
  pi.on("session_start", async (_event, ctx) => {
53
+ state.webSearchNoticeShown = false;
52
54
  syncAdapter(pi, ctx, state);
53
55
  });
54
56
 
@@ -84,39 +86,19 @@ export default function codexConversion(pi: ExtensionAPI) {
84
86
  };
85
87
  });
86
88
 
87
- pi.on("context", async (event) => {
88
- return {
89
- messages: event.messages.filter(
90
- (message) => !(message.role === "custom" && message.customType === WEB_SEARCH_ACTIVITY_MESSAGE_TYPE),
91
- ),
92
- };
93
- });
94
-
95
- pi.on("turn_start", async () => {
96
- state.pendingWebSearchCount = 0;
97
- });
98
-
99
89
  pi.on("before_provider_request", async (event, ctx) => {
100
90
  if (!isOpenAICodexContext(ctx)) {
101
91
  return undefined;
102
92
  }
103
- if (payloadContainsWebSearchTool(event.payload)) {
104
- state.pendingWebSearchCount += 1;
105
- }
106
93
  return rewriteNativeWebSearchTool(event.payload, ctx.model);
107
94
  });
108
95
 
109
- pi.on("agent_end", async () => {
110
- if (state.pendingWebSearchCount <= 0) {
111
- return;
112
- }
113
- pi.sendMessage({
114
- customType: WEB_SEARCH_ACTIVITY_MESSAGE_TYPE,
115
- content: "",
116
- display: true,
117
- details: { count: state.pendingWebSearchCount },
118
- });
119
- state.pendingWebSearchCount = 0;
96
+ pi.on("context", async (event) => {
97
+ return {
98
+ messages: event.messages.filter(
99
+ (message) => !(message.role === "custom" && message.customType === WEB_SEARCH_SESSION_NOTE_TYPE),
100
+ ),
101
+ };
120
102
  });
121
103
  }
122
104
 
@@ -124,6 +106,7 @@ function syncAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterStat
124
106
  state.promptSkills = extractPiPromptSkills(ctx.getSystemPrompt());
125
107
 
126
108
  registerViewImageTool(pi, { allowOriginalDetail: supportsOriginalImageDetail(ctx.model) });
109
+ maybeShowWebSearchSessionNote(pi, ctx, state);
127
110
 
128
111
  if (isCodexLikeContext(ctx)) {
129
112
  enableAdapter(pi, ctx, state);
@@ -190,3 +173,15 @@ export function restoreTools(previousTools: string[], activeTools: string[]): st
190
173
  function hasAdapterTools(activeTools: string[]): boolean {
191
174
  return activeTools.some((toolName) => ADAPTER_TOOL_NAMES.includes(toolName));
192
175
  }
176
+
177
+ function maybeShowWebSearchSessionNote(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
178
+ if (!shouldShowWebSearchSessionNote(ctx.model, ctx.hasUI, state.webSearchNoticeShown)) {
179
+ return;
180
+ }
181
+ pi.sendMessage({
182
+ customType: WEB_SEARCH_SESSION_NOTE_TYPE,
183
+ content: WEB_SEARCH_SESSION_NOTE_TEXT,
184
+ display: true,
185
+ });
186
+ state.webSearchNoticeShown = true;
187
+ }
@@ -4,24 +4,12 @@ export interface PromptSkill {
4
4
  filePath: string;
5
5
  }
6
6
 
7
- const PI_INTRO =
8
- "You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
9
- const CODEX_INTRO =
10
- "You are Codex running inside pi, a coding agent harness. Work directly in the user's workspace and finish the task end-to-end when feasible.";
11
-
12
7
  const CODEX_GUIDELINES = [
13
8
  "Use `parallel` only when tool calls are independent and can safely run at the same time.",
14
9
  "Use `write_stdin` when an exec session returns `session_id`, and continue until `exit_code` is present.",
15
10
  "Do not request `tty` unless interactive terminal behavior is required.",
16
11
  ];
17
12
 
18
- function rewriteIntro(prompt: string): string {
19
- if (!prompt.startsWith(PI_INTRO)) {
20
- return prompt;
21
- }
22
- return `${CODEX_INTRO}${prompt.slice(PI_INTRO.length)}`;
23
- }
24
-
25
13
  function insertBeforeTrailingContext(prompt: string, section: string): string {
26
14
  const currentDateIndex = prompt.lastIndexOf("\nCurrent date:");
27
15
  if (currentDateIndex !== -1) {
@@ -114,5 +102,5 @@ function injectGuidelines(prompt: string): string {
114
102
  }
115
103
 
116
104
  export function buildCodexSystemPrompt(basePrompt: string, options: { skills?: PromptSkill[]; shell?: string } = {}): string {
117
- return injectShell(injectSkills(injectGuidelines(rewriteIntro(basePrompt)), options.skills ?? []), options.shell);
105
+ return injectShell(injectSkills(injectGuidelines(basePrompt), options.skills ?? []), options.shell);
118
106
  }
@@ -32,14 +32,6 @@ export function renderWriteStdinCall(
32
32
  return text;
33
33
  }
34
34
 
35
- export function renderWebSearchActivity(count: number, theme: RenderTheme, expanded = false): string {
36
- let text = `${theme.fg("dim", "•")} ${theme.bold("Searched the web")}`;
37
- if (expanded && count > 1) {
38
- text += `\n${theme.fg("dim", " └ ")}${theme.fg("muted", `${count} web searches in this turn`)}`;
39
- }
40
- return text;
41
- }
42
-
43
35
  function renderExplorationText(actions: ShellAction[], state: ExecCommandStatus, theme: RenderTheme): string {
44
36
  const header = state === "running" ? "Exploring" : "Explored";
45
37
  let text = `${theme.fg("dim", "•")} ${theme.bold(header)}`;
@@ -2,14 +2,19 @@ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@mariozechn
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import { Box, Text } from "@mariozechner/pi-tui";
4
4
  import { isOpenAICodexModel } from "../adapter/codex-model.ts";
5
- import { renderWebSearchActivity } from "./codex-rendering.ts";
6
5
 
7
6
  export const WEB_SEARCH_UNSUPPORTED_MESSAGE = "web_search is only available with the openai-codex provider";
8
7
  const WEB_SEARCH_LOCAL_EXECUTION_MESSAGE =
9
8
  "web_search is a native openai-codex provider tool and should not execute locally";
10
- export const WEB_SEARCH_ACTIVITY_MESSAGE_TYPE = "codex-web-search";
9
+ export const WEB_SEARCH_SESSION_NOTE_TYPE = "codex-web-search-session-note";
10
+ export const WEB_SEARCH_SESSION_NOTE_TEXT =
11
+ "Native OpenAI Codex web search is enabled for this session. Search runs silently and is not surfaced as a separate tool call.";
12
+ const WEB_SEARCH_MULTIMODAL_CONTENT_TYPES = ["text", "image"] as const;
11
13
 
12
- const WEB_SEARCH_PARAMETERS = Type.Object({}, { additionalProperties: false });
14
+ const WEB_SEARCH_PARAMETERS = Type.Unsafe<Record<string, never>>({
15
+ type: "object",
16
+ additionalProperties: false,
17
+ });
13
18
 
14
19
  interface FunctionToolPayload {
15
20
  type?: unknown;
@@ -21,14 +26,32 @@ interface ResponsesPayload {
21
26
  [key: string]: unknown;
22
27
  }
23
28
 
24
- export interface WebSearchActivityDetails {
25
- count: number;
29
+ interface ResponsesWebSearchTool {
30
+ type: "web_search";
31
+ external_web_access: true;
32
+ search_content_types?: string[];
26
33
  }
27
34
 
28
35
  export function supportsNativeWebSearch(model: ExtensionContext["model"]): boolean {
29
36
  return isOpenAICodexModel(model);
30
37
  }
31
38
 
39
+ export function shouldShowWebSearchSessionNote(
40
+ model: ExtensionContext["model"],
41
+ hasUI: boolean,
42
+ alreadyShown: boolean,
43
+ ): boolean {
44
+ return hasUI && !alreadyShown && supportsNativeWebSearch(model);
45
+ }
46
+
47
+ export function supportsMultimodalNativeWebSearch(model: ExtensionContext["model"]): boolean {
48
+ if (!supportsNativeWebSearch(model)) {
49
+ return false;
50
+ }
51
+ const id = (model?.id ?? "").toLowerCase();
52
+ return !id.includes("spark");
53
+ }
54
+
32
55
  function isWebSearchFunctionTool(tool: unknown): tool is FunctionToolPayload {
33
56
  return !!tool && typeof tool === "object" && (tool as FunctionToolPayload).type === "function" && (tool as FunctionToolPayload).name === "web_search";
34
57
  }
@@ -50,10 +73,14 @@ export function rewriteNativeWebSearchTool(payload: unknown, model: ExtensionCon
50
73
  }
51
74
  rewritten = true;
52
75
  // Match Codex's native tool shape rather than exposing a synthetic function tool.
53
- return {
76
+ const nativeTool: ResponsesWebSearchTool = {
54
77
  type: "web_search",
55
78
  external_web_access: true,
56
79
  };
80
+ if (supportsMultimodalNativeWebSearch(model)) {
81
+ nativeTool.search_content_types = [...WEB_SEARCH_MULTIMODAL_CONTENT_TYPES];
82
+ }
83
+ return nativeTool;
57
84
  });
58
85
 
59
86
  if (!rewritten) {
@@ -66,28 +93,14 @@ export function rewriteNativeWebSearchTool(payload: unknown, model: ExtensionCon
66
93
  };
67
94
  }
68
95
 
69
- export function payloadContainsWebSearchTool(payload: unknown): boolean {
70
- if (!payload || typeof payload !== "object") {
71
- return false;
72
- }
73
- const tools = (payload as ResponsesPayload).tools;
74
- if (!Array.isArray(tools)) {
75
- return false;
76
- }
77
- return tools.some(
78
- (tool) =>
79
- !!tool &&
80
- typeof tool === "object" &&
81
- (("type" in tool && (tool as { type?: unknown }).type === "web_search") || isWebSearchFunctionTool(tool)),
82
- );
83
- }
84
-
85
96
  export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETERS> {
86
97
  return {
87
98
  name: "web_search",
88
99
  label: "web_search",
89
- description: "Search the internet for sources related to the prompt.",
90
- promptSnippet: "Search the internet for sources related to the prompt.",
100
+ description:
101
+ "Search the web for sources relevant to the current task. Use it when you need up-to-date information, external references, or broader context beyond the workspace.",
102
+ promptSnippet:
103
+ "Search the web for sources relevant to the current task. Use it when you need up-to-date information, external references, or broader context beyond the workspace.",
91
104
  parameters: WEB_SEARCH_PARAMETERS,
92
105
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
93
106
  if (!supportsNativeWebSearch(ctx.model)) {
@@ -113,11 +126,11 @@ export function registerWebSearchTool(pi: ExtensionAPI): void {
113
126
  pi.registerTool(createWebSearchTool());
114
127
  }
115
128
 
116
- export function registerWebSearchMessageRenderer(pi: ExtensionAPI): void {
117
- pi.registerMessageRenderer<WebSearchActivityDetails>(WEB_SEARCH_ACTIVITY_MESSAGE_TYPE, (message, { expanded }, theme) => {
118
- const count = typeof message.details?.count === "number" ? message.details.count : 1;
129
+ export function registerWebSearchSessionNoteRenderer(pi: ExtensionAPI): void {
130
+ pi.registerMessageRenderer(WEB_SEARCH_SESSION_NOTE_TYPE, (_message, _options, theme) => {
119
131
  const box = new Box(1, 1, (text) => theme.bg("toolSuccessBg", text));
120
- box.addChild(new Text(renderWebSearchActivity(count, theme, expanded), 0, 0));
132
+ box.addChild(new Text(theme.bold("Web search enabled"), 0, 0));
133
+ box.addChild(new Text(`\n${theme.fg("dim", WEB_SEARCH_SESSION_NOTE_TEXT)}`, 0, 0));
121
134
  return box;
122
135
  });
123
136
  }