@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 +27 -0
- package/index.ts +31 -0
- package/package.json +1 -1
- package/src/config.ts +144 -1
- package/src/live/controller.ts +8 -1
- package/src/live/focus.ts +92 -0
- package/src/live/index.ts +192 -40
- package/src/live/queue.ts +388 -0
- package/src/live/visualizer.ts +13 -3
- package/src/websearch.ts +446 -0
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
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 =
|
|
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,
|
package/src/live/controller.ts
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|