@narumitw/pi-codex-compact 0.51.3 → 0.53.0

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,235 +1,247 @@
1
+ import type { Api } from "@earendil-works/pi-ai";
1
2
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
3
  import type { MenuDefinition } from "@narumitw/pi-tui-kit";
3
- import { usesCodexResponsesApi } from "./model-api.js";
4
- import type {
5
- CodexCompactSettings,
6
- CodexCompactSettingsRuntime,
7
- CodexCompactSettingsState,
8
- } from "./settings.js";
4
+ import { resolveCompactionRouteForApi } from "./model-api.js";
5
+ import type { CodexCompactSettings, CodexCompactSettingsRuntime, CodexCompactSettingsState } from "./settings.js";
6
+ import { terminalText as safeText } from "./terminal.js";
9
7
 
10
8
  type Screen = "main" | "settings" | "invalid";
11
9
  type Action =
12
- | "compact-now"
13
- | "set-enabled"
14
- | "set-timeout"
15
- | "set-retries"
16
- | "set-retention"
17
- | "set-notify";
10
+ | "compact-now"
11
+ | "set-enabled"
12
+ | "set-protocol"
13
+ | "set-timeout"
14
+ | "set-retries"
15
+ | "set-retention"
16
+ | "set-notify";
18
17
 
19
18
  export interface SettingsMenuOwner {
20
- signal: AbortSignal;
21
- isCurrent(): boolean;
19
+ signal: AbortSignal;
20
+ isCurrent(): boolean;
22
21
  }
23
22
 
24
23
  interface CompactMenuStatus {
25
- model: string;
26
- remoteEligible: boolean;
27
- }
28
-
29
- function safeText(value: string): string {
30
- return Array.from(value, (character) => {
31
- const codePoint = character.codePointAt(0) ?? 0;
32
- return codePoint < 32 || (codePoint >= 127 && codePoint <= 159) ? " " : character;
33
- }).join("");
24
+ model: string;
25
+ api?: Api;
34
26
  }
35
27
 
36
28
  function timeoutLabel(milliseconds: number): string {
37
- return `${milliseconds / 60_000} min`;
29
+ return `${milliseconds / 60_000} min`;
38
30
  }
39
31
 
40
32
  function retentionLabel(tokens: number): string {
41
- return `${tokens / 1000}K tokens`;
33
+ return `${tokens / 1000}K tokens`;
34
+ }
35
+
36
+ function protocolLabel(protocol: CodexCompactSettings["protocol"]): string {
37
+ switch (protocol) {
38
+ case "auto":
39
+ return "Auto";
40
+ case "remote-v2":
41
+ return "Remote V2";
42
+ case "responses-compact":
43
+ return "Responses Compact";
44
+ }
42
45
  }
43
46
 
44
47
  async function update(
45
- runtime: CodexCompactSettingsRuntime,
46
- ctx: ExtensionCommandContext,
47
- patch: Partial<CodexCompactSettings>,
48
- signal: AbortSignal,
48
+ runtime: CodexCompactSettingsRuntime,
49
+ ctx: ExtensionCommandContext,
50
+ patch: Partial<CodexCompactSettings>,
51
+ signal: AbortSignal,
49
52
  ) {
50
- try {
51
- await runtime.update(patch, signal);
52
- ctx.ui.notify("Codex compaction settings saved.", "info");
53
- return { kind: "stay" as const };
54
- } catch (error) {
55
- if (signal.aborted) return { kind: "rejected" as const };
56
- ctx.ui.notify(
57
- `Could not save pi-codex-compact.json: ${safeText(error instanceof Error ? error.message : String(error))}`,
58
- "error",
59
- );
60
- return { kind: "rejected" as const };
61
- }
53
+ try {
54
+ await runtime.update(patch, signal);
55
+ if (signal.aborted) return { kind: "rejected" as const };
56
+ ctx.ui.notify("Responses compaction settings saved.", "info");
57
+ return { kind: "stay" as const };
58
+ } catch (error) {
59
+ if (signal.aborted) return { kind: "rejected" as const };
60
+ ctx.ui.notify(
61
+ `Could not save pi-codex-compact.json: ${safeText(error instanceof Error ? error.message : String(error))}`,
62
+ "error",
63
+ );
64
+ return { kind: "rejected" as const };
65
+ }
62
66
  }
63
67
 
64
68
  export function createCodexCompactMenu(
65
- runtime: CodexCompactSettingsRuntime,
66
- options: { onCompactRequested?: () => void; status?: CompactMenuStatus } = {},
69
+ runtime: CodexCompactSettingsRuntime,
70
+ options: { onCompactRequested?: () => void; status?: CompactMenuStatus } = {},
67
71
  ): MenuDefinition<CodexCompactSettingsState, Screen, Action, ExtensionCommandContext> {
68
- return {
69
- start: "main",
70
- screens: {
71
- main: ({ state }) => ({
72
- kind: "actions",
73
- title: "Codex Remote Compaction",
74
- lines: [
75
- `Remote V2: ${state.settings.enabled ? "On" : "Off"}`,
76
- `Active model: ${safeText(options.status?.model ?? "none")}`,
77
- `Compact route: ${safeText(compactRoute(state, options.status))}`,
78
- ],
79
- items: [
80
- {
81
- id: "compact-now",
82
- label: "Compact now",
83
- description: "Close this menu and compact the active session immediately.",
84
- action: "compact-now",
85
- },
86
- state.kind === "invalid"
87
- ? {
88
- id: "settings",
89
- label: "Settings",
90
- description: "Read-only until the invalid settings file is repaired.",
91
- to: "invalid" as const,
92
- }
93
- : { id: "settings", label: "Settings", to: "settings" as const },
94
- { id: "close", label: "Close", close: true },
95
- ],
96
- hint: "close",
97
- }),
98
- settings: ({ state }) => ({
99
- kind: "settings",
100
- title: "Codex Remote Compaction Settings",
101
- lines: [`User settings · ${safeText(state.path)}`],
102
- items: [
103
- {
104
- id: "enabled",
105
- label: "Remote compaction",
106
- description: "Use Codex Remote V2 when the model uses openai-codex-responses.",
107
- currentValue: state.settings.enabled ? "On" : "Off",
108
- values: ["On", "Off"],
109
- action: "set-enabled",
110
- },
111
- {
112
- id: "requestTimeoutMs",
113
- label: "Request timeout",
114
- description: "Maximum time for the extension-owned remote compaction request.",
115
- currentValue: timeoutLabel(state.settings.requestTimeoutMs),
116
- values: ["2 min", "5 min", "10 min"],
117
- action: "set-timeout",
118
- },
119
- {
120
- id: "maxRetries",
121
- label: "Transport retries",
122
- description: "Retry transient provider failures before falling back to Pi.",
123
- currentValue: String(state.settings.maxRetries),
124
- values: ["0", "1", "2"],
125
- action: "set-retries",
126
- },
127
- {
128
- id: "replacementTokenBudget",
129
- label: "Retained user history",
130
- description: "Approximate user-message budget kept beside the opaque checkpoint.",
131
- currentValue: retentionLabel(state.settings.replacementTokenBudget),
132
- values: ["32K tokens", "64K tokens", "96K tokens", "128K tokens"],
133
- action: "set-retention",
134
- },
135
- {
136
- id: "notifyOnFallback",
137
- label: "Fallback notifications",
138
- description: "Warn when Remote V2 fails and native Pi compaction takes over.",
139
- currentValue: state.settings.notifyOnFallback ? "On" : "Off",
140
- values: ["On", "Off"],
141
- action: "set-notify",
142
- },
143
- ],
144
- }),
145
- invalid: ({ state }) => ({
146
- kind: "detail",
147
- title: "Codex Compact Settings · Read only",
148
- lines: [
149
- `Invalid settings file: ${safeText(state.path)}`,
150
- `Issue: ${safeText(state.issue ?? "unknown validation error")}`,
151
- "Built-in defaults are active. Repair the file and run /reload; it will not be overwritten.",
152
- ],
153
- hint: "back",
154
- }),
155
- },
156
- actions: {
157
- "compact-now": async () => {
158
- options.onCompactRequested?.();
159
- return { kind: "close" };
160
- },
161
- "set-enabled": ({ ctx, value, signal }) =>
162
- update(runtime, ctx, { enabled: value === "On" }, signal),
163
- "set-timeout": ({ ctx, value, signal }) =>
164
- update(
165
- runtime,
166
- ctx,
167
- { requestTimeoutMs: Number.parseInt(value ?? "5", 10) * 60_000 },
168
- signal,
169
- ),
170
- "set-retries": ({ ctx, value, signal }) =>
171
- update(runtime, ctx, { maxRetries: Number.parseInt(value ?? "2", 10) }, signal),
172
- "set-retention": ({ ctx, value, signal }) =>
173
- update(
174
- runtime,
175
- ctx,
176
- { replacementTokenBudget: Number.parseInt(value ?? "64", 10) * 1000 },
177
- signal,
178
- ),
179
- "set-notify": ({ ctx, value, signal }) =>
180
- update(runtime, ctx, { notifyOnFallback: value === "On" }, signal),
181
- },
182
- };
72
+ return {
73
+ start: "main",
74
+ screens: {
75
+ main: ({ state }) => ({
76
+ kind: "actions",
77
+ title: "Responses Compaction",
78
+ lines: [
79
+ `Remote compaction: ${state.settings.enabled ? "On" : "Off"}`,
80
+ `Protocol setting: ${protocolLabel(state.settings.protocol)}`,
81
+ `Active model: ${safeText(options.status?.model ?? "none")}`,
82
+ `Compact route: ${safeText(compactRoute(state, options.status))}`,
83
+ ],
84
+ items: [
85
+ {
86
+ id: "compact-now",
87
+ label: "Compact now",
88
+ description: "Close this menu and compact the active session immediately.",
89
+ action: "compact-now",
90
+ },
91
+ state.kind === "invalid"
92
+ ? {
93
+ id: "settings",
94
+ label: "Settings",
95
+ description: "Read-only until the invalid settings file is repaired.",
96
+ to: "invalid" as const,
97
+ }
98
+ : { id: "settings", label: "Settings", to: "settings" as const },
99
+ { id: "close", label: "Close", close: true },
100
+ ],
101
+ hint: "close",
102
+ }),
103
+ settings: ({ state }) => ({
104
+ kind: "settings",
105
+ title: "Responses Compaction Settings",
106
+ lines: [`User settings · ${safeText(state.path)}`],
107
+ items: [
108
+ {
109
+ id: "enabled",
110
+ label: "Remote compaction",
111
+ description: "Use a supported Responses compaction protocol.",
112
+ currentValue: state.settings.enabled ? "On" : "Off",
113
+ values: ["On", "Off"],
114
+ action: "set-enabled",
115
+ },
116
+ {
117
+ id: "protocol",
118
+ label: "Protocol",
119
+ description: "Choose automatically or force one supported remote protocol.",
120
+ currentValue: protocolLabel(state.settings.protocol),
121
+ values: ["Auto", "Remote V2", "Responses Compact"],
122
+ action: "set-protocol",
123
+ },
124
+ {
125
+ id: "requestTimeoutMs",
126
+ label: "Request timeout",
127
+ description: "Maximum time for the extension-owned remote compaction request.",
128
+ currentValue: timeoutLabel(state.settings.requestTimeoutMs),
129
+ values: ["2 min", "5 min", "10 min"],
130
+ action: "set-timeout",
131
+ },
132
+ {
133
+ id: "maxRetries",
134
+ label: "Transport retries",
135
+ description: "Retry transient provider failures before falling back to Pi.",
136
+ currentValue: String(state.settings.maxRetries),
137
+ values: ["0", "1", "2"],
138
+ action: "set-retries",
139
+ },
140
+ {
141
+ id: "replacementTokenBudget",
142
+ label: "Retained user history",
143
+ description: "Approximate user-message budget kept beside the opaque checkpoint.",
144
+ currentValue: retentionLabel(state.settings.replacementTokenBudget),
145
+ values: ["32K tokens", "64K tokens", "96K tokens", "128K tokens"],
146
+ action: "set-retention",
147
+ },
148
+ {
149
+ id: "notifyOnFallback",
150
+ label: "Fallback notifications",
151
+ description: "Warn when remote compaction fails and Pi native takes over.",
152
+ currentValue: state.settings.notifyOnFallback ? "On" : "Off",
153
+ values: ["On", "Off"],
154
+ action: "set-notify",
155
+ },
156
+ ],
157
+ }),
158
+ invalid: ({ state }) => ({
159
+ kind: "detail",
160
+ title: "Codex Compact Settings · Read only",
161
+ lines: [
162
+ `Invalid settings file: ${safeText(state.path)}`,
163
+ `Issue: ${safeText(state.issue ?? "unknown validation error")}`,
164
+ "Built-in defaults are active. Repair the file and run /reload; it will not be overwritten.",
165
+ ],
166
+ hint: "back",
167
+ }),
168
+ },
169
+ actions: {
170
+ "compact-now": async () => {
171
+ options.onCompactRequested?.();
172
+ return { kind: "close" };
173
+ },
174
+ "set-enabled": ({ ctx, value, signal }) => update(runtime, ctx, { enabled: value === "On" }, signal),
175
+ "set-protocol": ({ ctx, value, signal }) =>
176
+ update(
177
+ runtime,
178
+ ctx,
179
+ {
180
+ protocol:
181
+ value === "Remote V2" ? "remote-v2" : value === "Responses Compact" ? "responses-compact" : "auto",
182
+ },
183
+ signal,
184
+ ),
185
+ "set-timeout": ({ ctx, value, signal }) =>
186
+ update(runtime, ctx, { requestTimeoutMs: Number.parseInt(value ?? "5", 10) * 60_000 }, signal),
187
+ "set-retries": ({ ctx, value, signal }) =>
188
+ update(runtime, ctx, { maxRetries: Number.parseInt(value ?? "2", 10) }, signal),
189
+ "set-retention": ({ ctx, value, signal }) =>
190
+ update(runtime, ctx, { replacementTokenBudget: Number.parseInt(value ?? "64", 10) * 1000 }, signal),
191
+ "set-notify": ({ ctx, value, signal }) => update(runtime, ctx, { notifyOnFallback: value === "On" }, signal),
192
+ },
193
+ };
183
194
  }
184
195
 
185
196
  export async function showCodexCompactMenu(
186
- runtime: CodexCompactSettingsRuntime,
187
- ctx: ExtensionCommandContext,
188
- owner: SettingsMenuOwner,
197
+ runtime: CodexCompactSettingsRuntime,
198
+ ctx: ExtensionCommandContext,
199
+ owner: SettingsMenuOwner,
189
200
  ): Promise<void> {
190
- if (ctx.mode !== "tui") {
191
- ctx.ui.notify(`Edit Codex compaction settings at ${safeText(runtime.get().path)}.`, "info");
192
- return;
193
- }
194
- const { runMenu } = await import("@narumitw/pi-tui-kit");
195
- if (owner.signal.aborted || !owner.isCurrent()) return;
196
- let compactRequested = false;
197
- await runMenu(
198
- ctx,
199
- createCodexCompactMenu(runtime, {
200
- onCompactRequested: () => {
201
- compactRequested = true;
202
- },
203
- status: compactMenuStatus(ctx),
204
- }),
205
- {
206
- getState: () => runtime.get(),
207
- signal: owner.signal,
208
- isCurrent: owner.isCurrent,
209
- },
210
- );
211
- if (!compactRequested || owner.signal.aborted || !owner.isCurrent()) return;
212
- ctx.compact({
213
- onError: (error) => {
214
- if (!owner.signal.aborted && owner.isCurrent()) {
215
- ctx.ui.notify(`Compaction failed: ${safeText(error.message)}`, "error");
216
- }
217
- },
218
- });
201
+ if (ctx.mode === "rpc" && ctx.hasUI) {
202
+ ctx.ui.notify(`Edit Responses compaction settings at ${safeText(runtime.get().path)}.`, "info");
203
+ return;
204
+ }
205
+ if (ctx.mode !== "tui") {
206
+ throw new Error("/codex-compact requires TUI or RPC UI support");
207
+ }
208
+ const { runMenu } = await import("@narumitw/pi-tui-kit");
209
+ if (owner.signal.aborted || !owner.isCurrent()) return;
210
+ let compactRequested = false;
211
+ await runMenu(
212
+ ctx,
213
+ createCodexCompactMenu(runtime, {
214
+ onCompactRequested: () => {
215
+ compactRequested = true;
216
+ },
217
+ status: compactMenuStatus(ctx),
218
+ }),
219
+ {
220
+ getState: () => runtime.get(),
221
+ signal: owner.signal,
222
+ isCurrent: owner.isCurrent,
223
+ },
224
+ );
225
+ if (!compactRequested || owner.signal.aborted || !owner.isCurrent()) return;
226
+ ctx.compact({
227
+ onError: (error) => {
228
+ if (!owner.signal.aborted && owner.isCurrent()) {
229
+ ctx.ui.notify(`Compaction failed: ${safeText(error.message)}`, "error");
230
+ }
231
+ },
232
+ });
219
233
  }
220
234
 
221
235
  export function compactMenuStatus(ctx: ExtensionCommandContext): CompactMenuStatus {
222
- const model = ctx.model;
223
- return {
224
- model: model ? `${model.provider}/${model.id}` : "none",
225
- remoteEligible: usesCodexResponsesApi(model),
226
- };
236
+ const model = ctx.model;
237
+ return {
238
+ model: model ? `${model.provider}/${model.id}` : "none",
239
+ api: model?.api,
240
+ };
227
241
  }
228
242
 
229
- function compactRoute(
230
- state: Readonly<CodexCompactSettingsState>,
231
- status: CompactMenuStatus | undefined,
232
- ): string {
233
- if (!state.settings.enabled) return "Pi native (Remote V2 off)";
234
- return status?.remoteEligible ? "Codex Remote V2" : "Pi native (requires openai-codex-responses)";
243
+ function compactRoute(state: Readonly<CodexCompactSettingsState>, status: CompactMenuStatus | undefined): string {
244
+ const route = resolveCompactionRouteForApi(status?.api, state.settings);
245
+ if (route.kind === "native") return `Pi native (${route.reason})`;
246
+ return route.protocol === "remote-v2" ? "Responses Remote V2" : "Responses Compact API";
235
247
  }