@monotykamary/pi-better-openai 0.1.24 → 0.1.26

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
@@ -34,11 +34,13 @@ Usage display, image generation, and live voice require pi's `openai-codex` OAut
34
34
  - Interactive settings picker via `/openai-settings`.
35
35
  - Footer customization for model, thinking, fast mode, usage, and token/cost context.
36
36
  - OpenAI image generation/editing through the `openai_image` tool and `/openai-image` command.
37
+ - Live web search through the `openai_websearch` tool and `/openai-websearch` command, backed by the ChatGPT Codex search backend.
37
38
  - Codex-backed realtime voice through `/live`, with an animated microphone waveform and coding-task delegation into the active pi session.
38
39
  - Animated Codex custom pets rendered in the Better OpenAI footer.
39
40
  - Commands:
40
41
  - `/fast` toggles fast mode.
41
42
  - `/openai-image <prompt>` generates an image directly.
43
+ - `/openai-websearch <query>` searches the web and inserts the cited answer into the session.
42
44
  - `/live` starts or stops realtime voice mode. `Ctrl+Shift+L` is the keyboard toggle.
43
45
  - `/pets [help|list|wake [slug]|tuck|select <slug>]` renders or manages custom pets from `${CODEX_HOME:-~/.codex}/pets`.
44
46
  - `/openai-usage` shows current OpenAI subscription usage.
@@ -151,6 +153,31 @@ Save modes:
151
153
 
152
154
  The repository ignores `.pi/`, so generated images and local config should not be committed.
153
155
 
156
+ ## Web search
157
+
158
+ Use the command for a quick search:
159
+
160
+ ```text
161
+ /openai-websearch latest tanstack query release
162
+ ```
163
+
164
+ Agents can call the `openai_websearch` tool directly. Supported parameters:
165
+
166
+ - `query` (required): the web search query.
167
+ - `responseLength`: `short`, `medium`, or `long`. Defaults to the configured value.
168
+
169
+ The tool returns a synthesized answer plus cited source URLs. It calls the
170
+ undocumented `chatgpt.com/backend-api/codex/alpha/search` endpoint with your ChatGPT
171
+ OAuth credentials (`openai-codex` login), so it can change or break without notice;
172
+ OAuth/API-key-only setups without ChatGPT login are not supported.
173
+
174
+ Settings under `websearch` in the config file or the `/openai-settings` picker:
175
+
176
+ - `enabled` (default `true`), `model` (default `gpt-5.6-luna`),
177
+ `reasoningEffort` (default `max`), `responseLength` (default `short`),
178
+ `maxOutputTokens` (default `4096`, clamped to 256-100000), and
179
+ `timeoutMs` (default `25000`, clamped to 5000-120000).
180
+
154
181
  ## Codex pets
155
182
 
156
183
  Codex pets are an OpenAI Codex app feature, so the floating overlay and pet picker are still controlled by Codex (`Settings → Appearance → Pets` or `/pet`). This extension can also render compatible custom pet spritesheets directly in pi's Better OpenAI footer.
package/index.ts CHANGED
@@ -24,10 +24,12 @@ import {
24
24
  DEFAULT_LIVE_CONFIG,
25
25
  DEFAULT_PET_CONFIG,
26
26
  DEFAULT_SUPPORTED_MODELS,
27
+ DEFAULT_WEBSEARCH_CONFIG,
27
28
  PET_STATES,
28
29
  FOOTER_SETTING_DESCRIPTORS,
29
30
  USAGE_SETTING_DESCRIPTORS,
30
31
  IMAGE_SETTING_DESCRIPTORS,
32
+ WEBSEARCH_SETTING_DESCRIPTORS,
31
33
  LIVE_SETTING_DESCRIPTORS,
32
34
  PET_SETTING_DESCRIPTORS,
33
35
  FAST_SETTING_DESCRIPTORS,
@@ -51,6 +53,7 @@ import {
51
53
  readCodexAuth,
52
54
  } from "./src/usage.ts";
53
55
  import { registerOpenAIImage, _imageTest } from "./src/image.ts";
56
+ import { registerOpenAIWebSearch, _websearchTest } from "./src/websearch.ts";
54
57
  import { registerOpenAILive } from "./src/live/index.ts";
55
58
  import {
56
59
  type CodexPetPackage,
@@ -296,6 +299,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
296
299
  "",
297
300
  `Image enabled: ${cfg.image.enabled}`,
298
301
  `Image default save: ${cfg.image.defaultSave}`,
302
+ `Websearch enabled: ${cfg.websearch.enabled}`,
303
+ `Websearch model: ${cfg.websearch.model} (${cfg.websearch.reasoningEffort}/${cfg.websearch.responseLength}, ${cfg.websearch.maxOutputTokens} tokens, ${cfg.websearch.timeoutMs}ms)`,
299
304
  `Live enabled: ${cfg.live.enabled}`,
300
305
  `Live voice: ${cfg.live.voice}`,
301
306
  `Pet enabled: ${cfg.pets.enabled}`,
@@ -538,6 +543,12 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
538
543
  : "disabled";
539
544
  }
540
545
 
546
+ function websearchSettingsSummary(cfg: ResolvedConfig): string {
547
+ return cfg.websearch.enabled
548
+ ? `enabled · ${cfg.websearch.model} · ${cfg.websearch.reasoningEffort}/${cfg.websearch.responseLength}`
549
+ : "disabled";
550
+ }
551
+
541
552
  function liveSettingsSummary(cfg: ResolvedConfig): string {
542
553
  return cfg.live.enabled ? `enabled · ${cfg.live.voice}` : "disabled";
543
554
  }
@@ -596,6 +607,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
596
607
  return settingsItemsFromDescriptors(IMAGE_SETTING_DESCRIPTORS, cfg);
597
608
  }
598
609
 
610
+ function buildWebsearchSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
611
+ return settingsItemsFromDescriptors(WEBSEARCH_SETTING_DESCRIPTORS, cfg);
612
+ }
613
+
599
614
  function buildLiveSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
600
615
  return settingsItemsFromDescriptors(LIVE_SETTING_DESCRIPTORS, cfg);
601
616
  }
@@ -690,6 +705,19 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
690
705
  () => done(imageSettingsSummary(config(ctx))),
691
706
  ),
692
707
  },
708
+ {
709
+ id: "section.websearch",
710
+ label: "Web search tool",
711
+ currentValue: websearchSettingsSummary(cfg),
712
+ description: "Configure ChatGPT Codex web search defaults.",
713
+ submenu: (_value, done) =>
714
+ settingsSubmenu(
715
+ "Web search tool settings",
716
+ () => buildWebsearchSettingsItems(config(ctx)),
717
+ ctx,
718
+ () => done(websearchSettingsSummary(config(ctx))),
719
+ ),
720
+ },
693
721
  {
694
722
  id: "section.live",
695
723
  label: "Live voice",
@@ -854,6 +882,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
854
882
  });
855
883
 
856
884
  registerOpenAIImage(pi, config);
885
+ registerOpenAIWebSearch(pi, config);
857
886
  registerOpenAILive(pi, config);
858
887
  registerOpenAIPets(pi, {
859
888
  wake: async (ctx, slug) => {
@@ -1302,6 +1331,7 @@ export const _test = {
1302
1331
  DEFAULT_IMAGE_CONFIG,
1303
1332
  DEFAULT_LIVE_CONFIG,
1304
1333
  DEFAULT_PET_CONFIG,
1334
+ DEFAULT_WEBSEARCH_CONFIG,
1305
1335
  SERVICE_TIER,
1306
1336
  configPaths,
1307
1337
  abbreviateHomePath,
@@ -1324,5 +1354,6 @@ export const _test = {
1324
1354
  readCodexAuth,
1325
1355
  textPanel,
1326
1356
  imageTest: _imageTest,
1357
+ websearchTest: _websearchTest,
1327
1358
  petsTest: _petsTest,
1328
1359
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-better-openai",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Improve OpenAI in pi with fast mode, usage stats, realtime voice, image generation, and footer polish.",
5
5
  "keywords": [
6
6
  "fast",
package/src/config.ts CHANGED
@@ -13,6 +13,8 @@ import { piAgentDir } from "./paths.ts";
13
13
  export const FOOTER_MODES = ["replace", "status", "off"] as const;
14
14
  export const IMAGE_SAVE_MODES = ["none", "project", "global", "custom"] as const;
15
15
  export const IMAGE_OUTPUT_FORMATS = ["png", "jpeg", "webp"] as const;
16
+ export const WEBSEARCH_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
17
+ export const WEBSEARCH_RESPONSE_LENGTHS = ["short", "medium", "long"] as const;
16
18
  export const PET_PLACEMENTS = [
17
19
  "stacked",
18
20
  "inline-left",
@@ -45,6 +47,8 @@ export const DEFAULT_SUPPORTED_MODELS = [
45
47
  export type FooterMode = (typeof FOOTER_MODES)[number];
46
48
  export type ImageSaveMode = (typeof IMAGE_SAVE_MODES)[number];
47
49
  export type ImageOutputFormat = (typeof IMAGE_OUTPUT_FORMATS)[number];
50
+ export type WebsearchReasoningEffort = (typeof WEBSEARCH_REASONING_EFFORTS)[number];
51
+ export type WebsearchResponseLength = (typeof WEBSEARCH_RESPONSE_LENGTHS)[number];
48
52
  export type PetPlacement = (typeof PET_PLACEMENTS)[number];
49
53
  export type PetState = (typeof PET_STATES)[number];
50
54
 
@@ -67,6 +71,15 @@ export type ImageConfig = {
67
71
  timeoutMs?: number;
68
72
  };
69
73
 
74
+ export type WebsearchConfig = {
75
+ enabled?: boolean;
76
+ model?: string;
77
+ reasoningEffort?: WebsearchReasoningEffort;
78
+ responseLength?: WebsearchResponseLength;
79
+ maxOutputTokens?: number;
80
+ timeoutMs?: number;
81
+ };
82
+
70
83
  export type LiveConfig = {
71
84
  enabled?: boolean;
72
85
  voice?: LiveVoice;
@@ -93,6 +106,7 @@ export interface ConfigFile {
93
106
  usage?: UsageConfig;
94
107
  footer?: FooterConfig;
95
108
  image?: ImageConfig;
109
+ websearch?: WebsearchConfig;
96
110
  live?: LiveConfig;
97
111
  pets?: PetConfig;
98
112
  }
@@ -115,6 +129,7 @@ export interface ResolvedConfig {
115
129
  usage: Required<UsageConfig>;
116
130
  footer: Required<FooterConfig>;
117
131
  image: Required<ImageConfig>;
132
+ websearch: Required<WebsearchConfig>;
118
133
  live: Required<LiveConfig>;
119
134
  pets: Required<PetConfig>;
120
135
  }
@@ -140,6 +155,17 @@ export const DEFAULT_IMAGE_CONFIG: Required<ImageConfig> = {
140
155
  timeoutMs: 180_000,
141
156
  };
142
157
 
158
+ export const DEFAULT_WEBSEARCH_MODEL = "gpt-5.6-luna";
159
+
160
+ export const DEFAULT_WEBSEARCH_CONFIG: Required<WebsearchConfig> = {
161
+ enabled: true,
162
+ model: DEFAULT_WEBSEARCH_MODEL,
163
+ reasoningEffort: "max",
164
+ responseLength: "short",
165
+ maxOutputTokens: 4096,
166
+ timeoutMs: 25_000,
167
+ };
168
+
143
169
  export const DEFAULT_LIVE_CONFIG: Required<LiveConfig> = {
144
170
  enabled: true,
145
171
  voice: DEFAULT_LIVE_VOICE,
@@ -166,11 +192,19 @@ export const DEFAULT_CONFIG: ConfigFile = {
166
192
  usage: DEFAULT_USAGE_CONFIG,
167
193
  footer: DEFAULT_FOOTER_CONFIG,
168
194
  image: DEFAULT_IMAGE_CONFIG,
195
+ websearch: DEFAULT_WEBSEARCH_CONFIG,
169
196
  live: DEFAULT_LIVE_CONFIG,
170
197
  pets: DEFAULT_PET_CONFIG,
171
198
  };
172
199
 
173
- export type SettingsOptionSection = "root" | "usage" | "footer" | "image" | "live" | "pets";
200
+ export type SettingsOptionSection =
201
+ | "root"
202
+ | "usage"
203
+ | "footer"
204
+ | "image"
205
+ | "websearch"
206
+ | "live"
207
+ | "pets";
174
208
 
175
209
  export type SettingsValueContext = {
176
210
  petEmptyValue?: string;
@@ -314,6 +348,69 @@ export const IMAGE_SETTING_DESCRIPTORS: readonly SettingsOptionDescriptor[] = [
314
348
  },
315
349
  ];
316
350
 
351
+ export const WEBSEARCH_SETTING_DESCRIPTORS: readonly SettingsOptionDescriptor[] = [
352
+ {
353
+ id: "websearch.enabled",
354
+ section: "websearch",
355
+ key: "enabled",
356
+ label: "Web search tool",
357
+ currentValue: (cfg) => String(cfg.websearch.enabled),
358
+ values: ["true", "false"],
359
+ description: "Allow the openai_websearch tool to make search requests.",
360
+ parse: booleanSetting,
361
+ },
362
+ {
363
+ id: "websearch.model",
364
+ section: "websearch",
365
+ key: "model",
366
+ label: "Search model",
367
+ currentValue: (cfg) => cfg.websearch.model,
368
+ values: ["gpt-5.6-luna"],
369
+ description: "Model used by the ChatGPT Codex search backend.",
370
+ parse: stringSetting,
371
+ },
372
+ {
373
+ id: "websearch.reasoningEffort",
374
+ section: "websearch",
375
+ key: "reasoningEffort",
376
+ label: "Search reasoning",
377
+ currentValue: (cfg) => cfg.websearch.reasoningEffort,
378
+ values: WEBSEARCH_REASONING_EFFORTS,
379
+ description: "Reasoning effort requested from the search backend.",
380
+ parse: stringSetting,
381
+ },
382
+ {
383
+ id: "websearch.responseLength",
384
+ section: "websearch",
385
+ key: "responseLength",
386
+ label: "Answer length",
387
+ currentValue: (cfg) => cfg.websearch.responseLength,
388
+ values: WEBSEARCH_RESPONSE_LENGTHS,
389
+ description: "Default answer length produced by the search backend.",
390
+ parse: stringSetting,
391
+ },
392
+ {
393
+ id: "websearch.maxOutputTokens",
394
+ section: "websearch",
395
+ key: "maxOutputTokens",
396
+ label: "Max output tokens",
397
+ currentValue: (cfg) => String(cfg.websearch.maxOutputTokens),
398
+ values: ["1024", "2048", "4096", "8192"],
399
+ description: "Maximum output tokens for a search request.",
400
+ parse: numberSetting,
401
+ },
402
+ {
403
+ id: "websearch.timeoutMs",
404
+ section: "websearch",
405
+ key: "timeoutMs",
406
+ label: "Search timeout",
407
+ currentValue: (cfg) => String(cfg.websearch.timeoutMs),
408
+ values: ["10000", "25000", "60000", "120000"],
409
+ description: "Web search request timeout in milliseconds.",
410
+ parse: numberSetting,
411
+ },
412
+ ];
413
+
317
414
  export const LIVE_SETTING_DESCRIPTORS: readonly SettingsOptionDescriptor[] = [
318
415
  {
319
416
  id: "live.enabled",
@@ -446,6 +543,7 @@ export const SETTINGS_OPTION_DESCRIPTORS: readonly SettingsOptionDescriptor[] =
446
543
  ...FOOTER_SETTING_DESCRIPTORS,
447
544
  ...USAGE_SETTING_DESCRIPTORS,
448
545
  ...IMAGE_SETTING_DESCRIPTORS,
546
+ ...WEBSEARCH_SETTING_DESCRIPTORS,
449
547
  ...LIVE_SETTING_DESCRIPTORS,
450
548
  ...PET_SETTING_DESCRIPTORS,
451
549
  ];
@@ -553,6 +651,28 @@ export function readConfig(path: string): ConfigFile | undefined {
553
651
  config.image.outputFormat = parsed.image.outputFormat as ImageOutputFormat;
554
652
  if (typeof parsed.image.timeoutMs === "number") config.image.timeoutMs = parsed.image.timeoutMs;
555
653
  }
654
+ if (isRecord(parsed.websearch)) {
655
+ config.websearch = {};
656
+ if (typeof parsed.websearch.enabled === "boolean")
657
+ config.websearch.enabled = parsed.websearch.enabled;
658
+ if (typeof parsed.websearch.model === "string" && parsed.websearch.model.trim())
659
+ config.websearch.model = parsed.websearch.model.trim();
660
+ if (
661
+ typeof parsed.websearch.reasoningEffort === "string" &&
662
+ (WEBSEARCH_REASONING_EFFORTS as readonly string[]).includes(parsed.websearch.reasoningEffort)
663
+ )
664
+ config.websearch.reasoningEffort = parsed.websearch
665
+ .reasoningEffort as WebsearchReasoningEffort;
666
+ if (
667
+ typeof parsed.websearch.responseLength === "string" &&
668
+ (WEBSEARCH_RESPONSE_LENGTHS as readonly string[]).includes(parsed.websearch.responseLength)
669
+ )
670
+ config.websearch.responseLength = parsed.websearch.responseLength as WebsearchResponseLength;
671
+ if (typeof parsed.websearch.maxOutputTokens === "number")
672
+ config.websearch.maxOutputTokens = parsed.websearch.maxOutputTokens;
673
+ if (typeof parsed.websearch.timeoutMs === "number")
674
+ config.websearch.timeoutMs = parsed.websearch.timeoutMs;
675
+ }
556
676
  if (isRecord(parsed.live)) {
557
677
  config.live = {};
558
678
  if (typeof parsed.live.enabled === "boolean") config.live.enabled = parsed.live.enabled;
@@ -701,6 +821,29 @@ export function resolveConfig(cwd: string): ResolvedConfig {
701
821
  ),
702
822
  ),
703
823
  },
824
+ websearch: {
825
+ ...DEFAULT_WEBSEARCH_CONFIG,
826
+ ...globalConfig.websearch,
827
+ ...projectConfig.websearch,
828
+ maxOutputTokens: Math.max(
829
+ 256,
830
+ Math.min(
831
+ 100_000,
832
+ projectConfig.websearch?.maxOutputTokens ??
833
+ globalConfig.websearch?.maxOutputTokens ??
834
+ DEFAULT_WEBSEARCH_CONFIG.maxOutputTokens,
835
+ ),
836
+ ),
837
+ timeoutMs: Math.max(
838
+ 5_000,
839
+ Math.min(
840
+ 120_000,
841
+ projectConfig.websearch?.timeoutMs ??
842
+ globalConfig.websearch?.timeoutMs ??
843
+ DEFAULT_WEBSEARCH_CONFIG.timeoutMs,
844
+ ),
845
+ ),
846
+ },
704
847
  live: {
705
848
  ...DEFAULT_LIVE_CONFIG,
706
849
  ...globalConfig.live,
@@ -25,7 +25,14 @@ Treat delegation context as your own internal progress and result. Never mention
25
25
 
26
26
  For greetings, clarification, or ordinary conversation that needs no tools, answer directly without delegating. Preserve one-assistant continuity: converse here, delegate execution, then communicate the returned result as your own.`;
27
27
 
28
- export type LivePhase = "connecting" | "listening" | "working" | "speaking" | "muted" | "error";
28
+ export type LivePhase =
29
+ | "standby"
30
+ | "connecting"
31
+ | "listening"
32
+ | "working"
33
+ | "speaking"
34
+ | "muted"
35
+ | "error";
29
36
 
30
37
  export interface LiveTranscript {
31
38
  role: "user" | "assistant";
@@ -0,0 +1,92 @@
1
+ // Terminal focus tracking via private mode 1004 (xterm focus-event reporting).
2
+ // Enabling `CSI ? 1004 h` makes the terminal emit `CSI I` on focus-in and
3
+ // `CSI O` on focus-out as input sequences; pi-tui's stdin buffer frames them
4
+ // as generic CSI and forwards them to input listeners unmodified, so an
5
+ // extension can observe them without any upstream keymap changes. Support is
6
+ // detected with a DECRQM probe (`CSI ? 1004 $ p` -> `CSI ? 1004 ; 1 $ y` set /
7
+ // `; 2 $ y` reset); terminals that ignore both sequences simply fall back to
8
+ // the FIFO floor policy. Supported by iTerm2, kitty, WezTerm, Alacritty, foot,
9
+ // Windows Terminal, xterm.js, and modern tmux.
10
+
11
+ export const FOCUS_REPORTING_ENABLE = "\x1b[?1004h";
12
+ export const FOCUS_REPORTING_DISABLE = "\x1b[?1004l";
13
+ export const FOCUS_IN_SEQUENCE = "\x1b[I";
14
+ export const FOCUS_OUT_SEQUENCE = "\x1b[O";
15
+ export const DECRQM_FOCUS_QUERY = "\x1b[?1004$p";
16
+
17
+ export type FocusInputListener = (data: string) => { consume?: boolean; data?: string } | undefined;
18
+
19
+ export interface FocusTerminalHandle {
20
+ write(data: string): void;
21
+ addInputListener(listener: FocusInputListener): () => void;
22
+ }
23
+
24
+ export function parseFocusSequence(data: string): boolean | undefined {
25
+ if (data === FOCUS_IN_SEQUENCE) return true;
26
+ if (data === FOCUS_OUT_SEQUENCE) return false;
27
+ return undefined;
28
+ }
29
+
30
+ export function parseDecrqmFocusResponse(data: string): boolean | undefined {
31
+ if (data === "\x1b[?1004;1$y" || data === "\x1b[?1004;3$y") return true;
32
+ if (data === "\x1b[?1004;2$y" || data === "\x1b[?1004;4$y") return false;
33
+ return undefined;
34
+ }
35
+
36
+ export function probeFocusReporting(
37
+ handle: FocusTerminalHandle,
38
+ timeoutMs = 400,
39
+ ): Promise<boolean> {
40
+ return new Promise((resolve) => {
41
+ let settled = false;
42
+ let removeListener: (() => void) | undefined;
43
+ const done = (supported: boolean) => {
44
+ if (settled) return;
45
+ settled = true;
46
+ clearTimeout(timer);
47
+ removeListener?.();
48
+ resolve(supported);
49
+ };
50
+ const timer = setTimeout(() => done(false), timeoutMs);
51
+ timer.unref?.();
52
+ try {
53
+ removeListener = handle.addInputListener((data) => {
54
+ const state = parseDecrqmFocusResponse(data);
55
+ if (state === undefined) return undefined;
56
+ done(true);
57
+ return { consume: true };
58
+ });
59
+ handle.write(DECRQM_FOCUS_QUERY);
60
+ } catch {
61
+ done(false);
62
+ }
63
+ });
64
+ }
65
+
66
+ export function attachFocusReporting(
67
+ handle: FocusTerminalHandle,
68
+ onFocus: (focused: boolean) => void,
69
+ ): () => void {
70
+ const removeListener = handle.addInputListener((data) => {
71
+ const focused = parseFocusSequence(data);
72
+ if (focused === undefined) return undefined;
73
+ onFocus(focused);
74
+ return { consume: true };
75
+ });
76
+ try {
77
+ handle.write(FOCUS_REPORTING_ENABLE);
78
+ } catch {
79
+ // The terminal write path is best-effort; focus events simply never arrive.
80
+ }
81
+ let disposed = false;
82
+ return () => {
83
+ if (disposed) return;
84
+ disposed = true;
85
+ removeListener();
86
+ try {
87
+ handle.write(FOCUS_REPORTING_DISABLE);
88
+ } catch {
89
+ // Nothing to undo on a torn-down tty.
90
+ }
91
+ };
92
+ }