opencode-clm 0.3.0 → 0.4.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.
- package/README.md +17 -7
- package/index.ts +29 -10
- package/package.json +1 -1
- package/src/budget.ts +160 -16
- package/src/clm.ts +119 -47
- package/src/compaction.ts +14 -6
- package/src/overflow.ts +6 -3
- package/src/panel/keys.ts +11 -1
- package/src/panel/model.ts +3 -1
- package/src/panel/view.ts +29 -9
- package/src/presentation.ts +8 -2
- package/src/settings-table.ts +83 -17
- package/src/settings.ts +69 -2
- package/src/tui/data.ts +8 -2
- package/src/tui/panel.ts +3 -2
- package/src/tui/plugin.ts +3 -2
package/src/settings-table.ts
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { basename, resolve } from "node:path";
|
|
12
12
|
|
|
13
|
-
import { resolveBudgetPolicy } from "./budget.ts";
|
|
13
|
+
import { FALLBACK_BUDGET, formatPercent, resolveBudget, resolveBudgetPolicy, type BudgetPolicyConfig } from "./budget.ts";
|
|
14
14
|
import { MIN_OBSERVATION_CAP, resolveObservationCap } from "./observation.ts";
|
|
15
15
|
import { toEditGate, type EditGate } from "./policy.ts";
|
|
16
|
-
import { HOUSE_STEERING, parseCompaction, parseFlag, parseFractions, parseTokens, type ClmSettings, type CompactionMode } from "./settings.ts";
|
|
16
|
+
import { HOUSE_STEERING, parseBudget, parseCompaction, parseCooldown, parseFlag, parseFractions, parseMode, parseTokens, type ClmMode, type ClmSettings, type CompactionMode } from "./settings.ts";
|
|
17
17
|
|
|
18
18
|
export type SettingKey =
|
|
19
|
-
| "editing" | "budget" | "reserve" | "reminders" | "gate" | "guard" | "compaction" | "cap" | "steering" | "oneTool" | "trailer"
|
|
19
|
+
| "editing" | "budget" | "reserve" | "reminders" | "cooldown" | "gate" | "guard" | "compaction" | "cap" | "steering" | "mode" | "oneTool" | "trailer"
|
|
20
20
|
| "compactPrompt" | "reasoning";
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -26,10 +26,13 @@ export type SettingKey =
|
|
|
26
26
|
*/
|
|
27
27
|
export interface ClmOverrides {
|
|
28
28
|
editing?: boolean;
|
|
29
|
-
|
|
29
|
+
/** Tokens, a share of the model window as text (`"50%"`), or null for the whole window. */
|
|
30
|
+
budget?: number | BudgetPercent | null;
|
|
30
31
|
reserve?: number;
|
|
31
32
|
/** Reminder fractions; empty disables every reminder, the budget − reserve one included. */
|
|
32
33
|
reminders?: number[];
|
|
34
|
+
/** Reminder cooldown as a share of the budget; 0 is off. */
|
|
35
|
+
cooldown?: number;
|
|
33
36
|
gate?: EditGate;
|
|
34
37
|
guard?: "withhold" | "off";
|
|
35
38
|
compaction?: CompactionMode;
|
|
@@ -42,8 +45,12 @@ export interface ClmOverrides {
|
|
|
42
45
|
reasoning?: boolean;
|
|
43
46
|
oneTool?: boolean;
|
|
44
47
|
trailer?: boolean;
|
|
48
|
+
mode?: ClmMode;
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
/** A budget percentage as stored in overrides.json: `"50%"`, `"12.5%"`. */
|
|
52
|
+
export type BudgetPercent = `${number}%`;
|
|
53
|
+
|
|
47
54
|
/** Settings in force for one session: the resolved settings plus whether edits apply. */
|
|
48
55
|
export interface SettingsValues {
|
|
49
56
|
editing: boolean;
|
|
@@ -53,6 +60,8 @@ export interface SettingsValues {
|
|
|
53
60
|
export interface FormatContext {
|
|
54
61
|
/** Model context window, shown next to a `window` budget. */
|
|
55
62
|
modelWindow?: number;
|
|
63
|
+
/** Model output limit; a percentage budget is a share of the window minus this. */
|
|
64
|
+
modelOutput?: number;
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
export interface ParseContext {
|
|
@@ -82,6 +91,24 @@ export function compactTokens(value: number): string {
|
|
|
82
91
|
return value.toLocaleString("en-US");
|
|
83
92
|
}
|
|
84
93
|
|
|
94
|
+
const percentText = (fraction: number) => formatPercent(fraction) as BudgetPercent;
|
|
95
|
+
|
|
96
|
+
/** `"50%"` → 0.5; undefined for anything but a percentage from 1% to 100%. */
|
|
97
|
+
export function budgetPercentFraction(value: unknown): number | undefined {
|
|
98
|
+
if (typeof value !== "string" || !/^\d+(?:\.\d+)?%$/.test(value)) return undefined;
|
|
99
|
+
const percent = Number(value.slice(0, -1));
|
|
100
|
+
return percent >= 1 && percent <= 100 ? percent / 100 : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The budget fields of a policy for a `budget` override. */
|
|
104
|
+
function budgetFields(budget: number | BudgetPercent | null): Pick<BudgetPolicyConfig, "contextBudget" | "contextFraction"> {
|
|
105
|
+
if (budget === null) return { contextBudget: undefined, contextFraction: undefined };
|
|
106
|
+
if (typeof budget === "number") return { contextBudget: budget, contextFraction: undefined };
|
|
107
|
+
const fraction = budgetPercentFraction(budget);
|
|
108
|
+
if (fraction === undefined) throw new Error(`budget must be a percentage from 1% to 100%, got ${JSON.stringify(budget)}`);
|
|
109
|
+
return { contextBudget: undefined, contextFraction: fraction };
|
|
110
|
+
}
|
|
111
|
+
|
|
85
112
|
const onOff = (value: boolean) => (value ? "on" : "off");
|
|
86
113
|
|
|
87
114
|
function formatReminders(fractions: readonly number[]): string {
|
|
@@ -138,19 +165,25 @@ export const SETTINGS_TABLE: readonly SettingDescriptor[] = [
|
|
|
138
165
|
key: "budget",
|
|
139
166
|
name: "budget",
|
|
140
167
|
label: "Budget",
|
|
141
|
-
description:
|
|
142
|
-
placeholder: "32k, 1.5m, or window",
|
|
168
|
+
description: `Token budget the reminders, the edit gate and the overflow guard measure against, OpenCode's system prompt and tool schemas included. A percentage of the model window minus its output limit (50%; ${compactTokens(FALLBACK_BUDGET)} while the window is unknown), a number (32k), or "window" for all of it. Env: CLM_BUDGET.`,
|
|
169
|
+
placeholder: "50%, 32k, 1.5m, or window",
|
|
143
170
|
format: (values, context) => {
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
171
|
+
const policy = values.settings.budget;
|
|
172
|
+
if (policy.contextBudget !== undefined) return compactTokens(policy.contextBudget);
|
|
173
|
+
if (policy.contextFraction !== undefined) {
|
|
174
|
+
const resolved = context?.modelWindow ? resolveBudget(policy, context.modelWindow, context.modelOutput) : undefined;
|
|
175
|
+
return `${formatPercent(policy.contextFraction)}${resolved ? ` (${compactTokens(resolved.budget)})` : ""}`;
|
|
176
|
+
}
|
|
146
177
|
return `window${context?.modelWindow ? ` (${compactTokens(context.modelWindow)})` : ""}`;
|
|
147
178
|
},
|
|
179
|
+
// The settings page pre-fills its prompt with the formatted value: `50% (131,072)`.
|
|
148
180
|
parse: (text) => {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
181
|
+
const parsed = parseBudget(text.replace(/\s*\([^)]*\)\s*$/, ""), "budget");
|
|
182
|
+
switch (parsed.kind) {
|
|
183
|
+
case "window": return { budget: null };
|
|
184
|
+
case "fraction": return { budget: percentText(parsed.fraction) };
|
|
185
|
+
case "tokens": return { budget: parsed.tokens };
|
|
186
|
+
}
|
|
154
187
|
},
|
|
155
188
|
},
|
|
156
189
|
{
|
|
@@ -171,6 +204,17 @@ export const SETTINGS_TABLE: readonly SettingDescriptor[] = [
|
|
|
171
204
|
format: (values) => formatReminders(values.settings.budget.remindAtFractions),
|
|
172
205
|
parse: (text) => ({ reminders: parseReminders(text) }),
|
|
173
206
|
},
|
|
207
|
+
{
|
|
208
|
+
key: "cooldown",
|
|
209
|
+
// Named after the reminders it governs; `cooldown` is an alias.
|
|
210
|
+
name: "reminder-cooldown",
|
|
211
|
+
label: "Reminder cooldown",
|
|
212
|
+
description: "After an accepted edit, percentage reminders stay silent until the context grows by this share of the budget; tiers crossed meanwhile are skipped. The budget − reserve reminder always fires. A share (10%, 0.1) or off. Env: CLM_REMINDER_COOLDOWN.",
|
|
213
|
+
choices: ["10%", "25%", "5%", "off"],
|
|
214
|
+
placeholder: "10%, 0.1, or off",
|
|
215
|
+
format: (values) => (values.settings.budget.reminderCooldown > 0 ? formatPercent(values.settings.budget.reminderCooldown) : "off"),
|
|
216
|
+
parse: (text) => ({ cooldown: parseCooldown(text, "reminder-cooldown") }),
|
|
217
|
+
},
|
|
174
218
|
{
|
|
175
219
|
key: "gate",
|
|
176
220
|
name: "gate",
|
|
@@ -223,6 +267,15 @@ export const SETTINGS_TABLE: readonly SettingDescriptor[] = [
|
|
|
223
267
|
return { steering: parsePathSetting(text, context.directory, ["none", "off", ""]) };
|
|
224
268
|
},
|
|
225
269
|
},
|
|
270
|
+
{
|
|
271
|
+
key: "mode",
|
|
272
|
+
name: "mode",
|
|
273
|
+
label: "Mode",
|
|
274
|
+
description: "edit: the model edits its context through the mirror. notices-only: no mirror, no protocol prompt and no edits; budget notices and reminders, the overflow guard, the observation cap and the size trailer still apply, to compare telling the model its budget with letting it edit. An accepted revision is kept but not applied while notices-only is in force. Env: CLM_MODE.",
|
|
275
|
+
choices: ["edit", "notices-only"],
|
|
276
|
+
format: (values) => values.settings.mode,
|
|
277
|
+
parse: (text) => ({ mode: parseMode(text) }),
|
|
278
|
+
},
|
|
226
279
|
{
|
|
227
280
|
key: "oneTool",
|
|
228
281
|
name: "one-tool",
|
|
@@ -270,6 +323,9 @@ const ALIASES: Record<string, SettingKey> = {
|
|
|
270
323
|
remind: "reminders",
|
|
271
324
|
"remind-at": "reminders",
|
|
272
325
|
remindat: "reminders",
|
|
326
|
+
cooldown: "cooldown",
|
|
327
|
+
"reminder-cooldown": "cooldown",
|
|
328
|
+
remindercooldown: "cooldown",
|
|
273
329
|
gate: "gate",
|
|
274
330
|
"edit-gate": "gate",
|
|
275
331
|
guard: "guard",
|
|
@@ -290,6 +346,7 @@ const ALIASES: Record<string, SettingKey> = {
|
|
|
290
346
|
"compact-prompt": "compactPrompt",
|
|
291
347
|
compactprompt: "compactPrompt",
|
|
292
348
|
reasoning: "reasoning",
|
|
349
|
+
mode: "mode",
|
|
293
350
|
};
|
|
294
351
|
|
|
295
352
|
/** Names `/clm config` lists. */
|
|
@@ -313,10 +370,13 @@ export function unknownSettingText(name: string): string {
|
|
|
313
370
|
export function applyOverrides(base: ClmSettings, overrides: ClmOverrides): ClmSettings {
|
|
314
371
|
const budget = resolveBudgetPolicy({
|
|
315
372
|
...base.budget,
|
|
316
|
-
|
|
373
|
+
...(overrides.budget !== undefined
|
|
374
|
+
? budgetFields(overrides.budget)
|
|
375
|
+
: { contextBudget: base.budget.contextBudget, contextFraction: base.budget.contextFraction }),
|
|
317
376
|
reserve: overrides.reserve ?? base.budget.reserve,
|
|
318
377
|
remindAtFractions: overrides.reminders ?? base.budget.remindAtFractions,
|
|
319
378
|
remindAtReserve: overrides.reminders !== undefined ? overrides.reminders.length > 0 : base.budget.remindAtReserve,
|
|
379
|
+
reminderCooldown: overrides.cooldown ?? base.budget.reminderCooldown,
|
|
320
380
|
});
|
|
321
381
|
const observationCap = resolveObservationCap({
|
|
322
382
|
maxCharacters: overrides.cap === null ? undefined : overrides.cap ?? base.observationCap.maxCharacters,
|
|
@@ -330,6 +390,7 @@ export function applyOverrides(base: ClmSettings, overrides: ClmOverrides): ClmS
|
|
|
330
390
|
compaction: overrides.compaction ?? base.compaction,
|
|
331
391
|
oneTool: overrides.oneTool ?? base.oneTool,
|
|
332
392
|
trailer: overrides.trailer ?? base.trailer,
|
|
393
|
+
mode: overrides.mode ?? base.mode,
|
|
333
394
|
observationCap,
|
|
334
395
|
steeringPath: overrides.steering === null ? undefined : overrides.steering ?? base.steeringPath,
|
|
335
396
|
compactPromptPath: overrides.compactPrompt === null ? undefined : overrides.compactPrompt ?? base.compactPromptPath,
|
|
@@ -342,9 +403,11 @@ function baseValue(key: keyof ClmOverrides, values: SettingsValues): unknown {
|
|
|
342
403
|
const settings = values.settings;
|
|
343
404
|
switch (key) {
|
|
344
405
|
case "editing": return values.editing;
|
|
345
|
-
case "budget": return settings.budget.contextBudget ??
|
|
406
|
+
case "budget": return settings.budget.contextBudget ??
|
|
407
|
+
(settings.budget.contextFraction !== undefined ? percentText(settings.budget.contextFraction) : null);
|
|
346
408
|
case "reserve": return settings.budget.reserve;
|
|
347
409
|
case "reminders": return settings.budget.remindAtReserve || settings.budget.remindAtFractions.length > 0 ? [...settings.budget.remindAtFractions] : [];
|
|
410
|
+
case "cooldown": return settings.budget.reminderCooldown;
|
|
348
411
|
case "gate": return settings.gate;
|
|
349
412
|
case "guard": return settings.guard;
|
|
350
413
|
case "compaction": return settings.compaction;
|
|
@@ -355,6 +418,7 @@ function baseValue(key: keyof ClmOverrides, values: SettingsValues): unknown {
|
|
|
355
418
|
case "steering": return settings.steeringPath ?? null;
|
|
356
419
|
case "compactPrompt": return settings.compactPromptPath ?? null;
|
|
357
420
|
case "reasoning": return settings.reasoning;
|
|
421
|
+
case "mode": return settings.mode;
|
|
358
422
|
}
|
|
359
423
|
}
|
|
360
424
|
|
|
@@ -366,7 +430,7 @@ function baseValue(key: keyof ClmOverrides, values: SettingsValues): unknown {
|
|
|
366
430
|
*/
|
|
367
431
|
export function settingsAsOverrides(settings: ClmSettings): ClmOverrides {
|
|
368
432
|
const values: SettingsValues = { editing: true, settings };
|
|
369
|
-
const keys: Array<keyof ClmOverrides> = ["budget", "reserve", "reminders", "gate", "guard", "compaction", "cap", "capHead", "steering", "oneTool", "trailer", "compactPrompt", "reasoning"];
|
|
433
|
+
const keys: Array<keyof ClmOverrides> = ["budget", "reserve", "reminders", "cooldown", "gate", "guard", "compaction", "cap", "capHead", "steering", "mode", "oneTool", "trailer", "compactPrompt", "reasoning"];
|
|
370
434
|
return Object.fromEntries(keys.map((key) => [key, baseValue(key, values)])) as ClmOverrides;
|
|
371
435
|
}
|
|
372
436
|
|
|
@@ -385,9 +449,10 @@ const isPath = (value: unknown) => value === null || (typeof value === "string"
|
|
|
385
449
|
const VALID: Record<keyof ClmOverrides, (value: unknown) => boolean> = {
|
|
386
450
|
editing: (value) => typeof value === "boolean",
|
|
387
451
|
// Integers: state.json's budget check stores them as counts.
|
|
388
|
-
budget: (value) => value === null || (Number.isInteger(value) && (value as number) > 0),
|
|
452
|
+
budget: (value) => value === null || (Number.isInteger(value) && (value as number) > 0) || budgetPercentFraction(value) !== undefined,
|
|
389
453
|
reserve: (value) => Number.isInteger(value) && (value as number) >= 0,
|
|
390
454
|
reminders: (value) => Array.isArray(value) && value.every((fraction) => isNumber(fraction) && fraction > 0 && fraction < 1),
|
|
455
|
+
cooldown: (value) => isNumber(value) && value >= 0 && value < 1,
|
|
391
456
|
gate: (value) => value === "fit" || value === "shrink" || value === "none",
|
|
392
457
|
guard: (value) => value === "withhold" || value === "off",
|
|
393
458
|
compaction: (value) => value === "auto" || value === "off" || value === "on",
|
|
@@ -398,6 +463,7 @@ const VALID: Record<keyof ClmOverrides, (value: unknown) => boolean> = {
|
|
|
398
463
|
steering: isPath,
|
|
399
464
|
compactPrompt: isPath,
|
|
400
465
|
reasoning: (value) => typeof value === "boolean",
|
|
466
|
+
mode: (value) => value === "edit" || value === "notices-only",
|
|
401
467
|
};
|
|
402
468
|
|
|
403
469
|
/**
|
package/src/settings.ts
CHANGED
|
@@ -47,6 +47,22 @@ export interface ClmSettings {
|
|
|
47
47
|
oneTool: boolean;
|
|
48
48
|
/** Append `[context: ~N of B tokens after this result]` to tool results (paper-harness parity). */
|
|
49
49
|
trailer: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* `edit`: the model edits its context through the mirror. `notices-only`: no mirror, no
|
|
52
|
+
* protocol prompt and no edits; budget notices, reminders, the guard, the cap and the
|
|
53
|
+
* trailer still apply (to compare telling the model its budget with letting it edit).
|
|
54
|
+
*/
|
|
55
|
+
mode: ClmMode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type ClmMode = "edit" | "notices-only";
|
|
59
|
+
|
|
60
|
+
/** `edit`, `notices-only`, or `notices` (an alias of `notices-only`). */
|
|
61
|
+
export function parseMode(value: unknown, name = "mode"): ClmMode {
|
|
62
|
+
const text = String(value).trim().toLowerCase();
|
|
63
|
+
if (text === "edit") return "edit";
|
|
64
|
+
if (text === "notices-only" || text === "notices") return "notices-only";
|
|
65
|
+
throw new Error(`${name} must be edit or notices-only, got ${JSON.stringify(value)}`);
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
export type CompactionMode = "auto" | "off" | "on";
|
|
@@ -64,6 +80,7 @@ export const SKILLS_DIR = join(PACKAGE_DIR, "skills");
|
|
|
64
80
|
type Env = Record<string, string | undefined>;
|
|
65
81
|
|
|
66
82
|
const OFF_WORDS = ["none", "off"];
|
|
83
|
+
const BUDGET_FORMS = `a number of tokens (32000, 32k), a percentage of the model window (1% to 100%), or "window"`;
|
|
67
84
|
|
|
68
85
|
function pick(options: Record<string, unknown>, key: string, env: Env, envKey: string): unknown {
|
|
69
86
|
if (options[key] !== undefined) return options[key];
|
|
@@ -85,6 +102,34 @@ export function parseTokens(value: unknown, name: string): number {
|
|
|
85
102
|
return Math.round(Number(match[1]) * scale);
|
|
86
103
|
}
|
|
87
104
|
|
|
105
|
+
export type BudgetValue =
|
|
106
|
+
| { kind: "tokens"; tokens: number }
|
|
107
|
+
| { kind: "fraction"; fraction: number }
|
|
108
|
+
| { kind: "window" };
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A budget setting: `32000` / `32k` (tokens), `50%` (of the model window minus its output
|
|
112
|
+
* limit; 1% to 100%), or `window` / `model` (the whole of it).
|
|
113
|
+
*/
|
|
114
|
+
export function parseBudget(value: unknown, name = "budget"): BudgetValue {
|
|
115
|
+
const text = String(value).trim().toLowerCase();
|
|
116
|
+
if (text === "window" || text === "model") return { kind: "window" };
|
|
117
|
+
if (text.endsWith("%")) {
|
|
118
|
+
const number = text.slice(0, -1).trim();
|
|
119
|
+
const percent = /^\d+(?:\.\d+)?$/.test(number) ? Number(number) : Number.NaN;
|
|
120
|
+
if (!(percent >= 1 && percent <= 100)) throw new Error(`${name} must be ${BUDGET_FORMS}, got ${JSON.stringify(value)}`);
|
|
121
|
+
return { kind: "fraction", fraction: percent / 100 };
|
|
122
|
+
}
|
|
123
|
+
let tokens: number;
|
|
124
|
+
try {
|
|
125
|
+
tokens = parseTokens(value, name);
|
|
126
|
+
} catch {
|
|
127
|
+
throw new Error(`${name} must be ${BUDGET_FORMS}, got ${JSON.stringify(value)}`);
|
|
128
|
+
}
|
|
129
|
+
if (tokens <= 0) throw new Error(`${name} must be a positive number of tokens or "window", got ${JSON.stringify(value)}`);
|
|
130
|
+
return { kind: "tokens", tokens };
|
|
131
|
+
}
|
|
132
|
+
|
|
88
133
|
export function parseFlag(value: unknown, name: string): boolean {
|
|
89
134
|
if (typeof value === "boolean") return value;
|
|
90
135
|
const key = String(value).trim().toLowerCase();
|
|
@@ -111,6 +156,23 @@ export function parseFractions(value: unknown, name: string): number[] | "off" {
|
|
|
111
156
|
return [...new Set(fractions)];
|
|
112
157
|
}
|
|
113
158
|
|
|
159
|
+
const COOLDOWN_FORMS = `a share of the budget between 0 and 1, as a percentage (10%) or a fraction (0.1), or off`;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The reminder cooldown: `10%` or `0.1` (a share of the budget, strictly between 0 and 1), or
|
|
163
|
+
* `off` / `none` / `0` for no cooldown (0).
|
|
164
|
+
*/
|
|
165
|
+
export function parseCooldown(value: unknown, name = "reminderCooldown"): number {
|
|
166
|
+
if (value === false) return 0;
|
|
167
|
+
const text = String(value).trim().toLowerCase();
|
|
168
|
+
if (text === "off" || text === "none" || text === "0" || text === "0%") return 0;
|
|
169
|
+
const percent = text.endsWith("%");
|
|
170
|
+
const number = percent ? text.slice(0, -1).trim() : text;
|
|
171
|
+
const parsed = /^\d*\.?\d+$/.test(number) ? Number(number) / (percent ? 100 : 1) : Number.NaN;
|
|
172
|
+
if (!(parsed > 0 && parsed < 1)) throw new Error(`${name} must be ${COOLDOWN_FORMS}, got ${JSON.stringify(value)}`);
|
|
173
|
+
return parsed;
|
|
174
|
+
}
|
|
175
|
+
|
|
114
176
|
function withName<T>(name: string, parse: () => T): T {
|
|
115
177
|
try {
|
|
116
178
|
return parse();
|
|
@@ -132,6 +194,7 @@ export function resolveSettings(options: Record<string, unknown> = {}, env: Env
|
|
|
132
194
|
const budgetRaw = pick(options, "budget", env, "CLM_BUDGET");
|
|
133
195
|
const reserveRaw = pick(options, "reserve", env, "CLM_RESERVE");
|
|
134
196
|
const remindRaw = pick(options, "remindAt", env, "CLM_REMIND_AT");
|
|
197
|
+
const cooldownRaw = pick(options, "reminderCooldown", env, "CLM_REMINDER_COOLDOWN");
|
|
135
198
|
const gateRaw = pick(options, "gate", env, "CLM_EDIT_GATE");
|
|
136
199
|
const guardRaw = pick(options, "guard", env, "CLM_OVERFLOW");
|
|
137
200
|
const capRaw = pick(options, "observationCap", env, "CLM_OBSERVATION_CAP");
|
|
@@ -144,11 +207,13 @@ export function resolveSettings(options: Record<string, unknown> = {}, env: Env
|
|
|
144
207
|
const compactionRaw = pick(options, "compaction", env, "CLM_NATIVE_COMPACTION");
|
|
145
208
|
const oneToolRaw = pick(options, "oneTool", env, "CLM_ONE_TOOL_PER_TURN");
|
|
146
209
|
const trailerRaw = pick(options, "trailer", env, "CLM_SIZE_TRAILER");
|
|
210
|
+
const modeRaw = pick(options, "mode", env, "CLM_MODE");
|
|
147
211
|
|
|
148
212
|
const overrides: Partial<BudgetPolicyConfig> = {};
|
|
149
213
|
if (budgetRaw !== undefined) {
|
|
150
|
-
|
|
151
|
-
|
|
214
|
+
const parsed = parseBudget(budgetRaw, "budget");
|
|
215
|
+
overrides.contextBudget = parsed.kind === "tokens" ? parsed.tokens : undefined;
|
|
216
|
+
overrides.contextFraction = parsed.kind === "fraction" ? parsed.fraction : undefined;
|
|
152
217
|
}
|
|
153
218
|
if (reserveRaw !== undefined) overrides.reserve = parseTokens(reserveRaw, "reserve");
|
|
154
219
|
if (remindRaw !== undefined) {
|
|
@@ -164,6 +229,7 @@ export function resolveSettings(options: Record<string, unknown> = {}, env: Env
|
|
|
164
229
|
overrides.remindAtFractions = fractions;
|
|
165
230
|
}
|
|
166
231
|
}
|
|
232
|
+
if (cooldownRaw !== undefined) overrides.reminderCooldown = parseCooldown(cooldownRaw, "reminderCooldown");
|
|
167
233
|
const budget = withName("budget", () => resolveBudgetPolicy({ ...DEFAULT_BUDGET_POLICY, ...overrides }));
|
|
168
234
|
|
|
169
235
|
let guard: ClmSettings["guard"] = "withhold";
|
|
@@ -213,5 +279,6 @@ export function resolveSettings(options: Record<string, unknown> = {}, env: Env
|
|
|
213
279
|
compaction: compactionRaw === undefined ? "auto" : parseCompaction(compactionRaw),
|
|
214
280
|
oneTool: oneToolRaw === undefined ? false : parseFlag(oneToolRaw, "oneTool"),
|
|
215
281
|
trailer: trailerRaw === undefined ? false : parseFlag(trailerRaw, "trailer"),
|
|
282
|
+
mode: modeRaw === undefined ? "edit" : parseMode(modeRaw),
|
|
216
283
|
};
|
|
217
284
|
}
|
package/src/tui/data.ts
CHANGED
|
@@ -155,14 +155,15 @@ export function sizeLine(latest: Pick<TimelinePoint, "tokens" | "measured" | "es
|
|
|
155
155
|
* `clm 12k / 32k · r2` (newest size against the budget in force), `clm off · r2`, or
|
|
156
156
|
* undefined before the session has CLM data.
|
|
157
157
|
*/
|
|
158
|
-
export function footerText(model: Pick<PanelModel, "found" | "enabled" | "revision" | "timeline" | "budget">): string | undefined {
|
|
158
|
+
export function footerText(model: Pick<PanelModel, "found" | "enabled" | "revision" | "timeline" | "budget"> & Partial<Pick<PanelModel, "noticesOnly">>): string | undefined {
|
|
159
159
|
if (!model.found) return undefined;
|
|
160
160
|
if (!model.enabled) return `clm off · r${model.revision}`;
|
|
161
|
+
const mode = model.noticesOnly ? " notices-only" : "";
|
|
161
162
|
const latest = model.timeline.points.at(-1);
|
|
162
163
|
const size = latest === undefined
|
|
163
164
|
? ""
|
|
164
165
|
: ` ${latest.measured ? "" : "~"}${formatTokenCount(latest.tokens)}${model.budget !== undefined ? ` / ${formatTokenCount(model.budget)}` : ""}`;
|
|
165
|
-
return `clm${size} · r${model.revision}`;
|
|
166
|
+
return `clm${mode}${size} · r${model.revision}`;
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
/** Key of the settings page's "Reset to defaults" row, and the choice that resets. */
|
|
@@ -199,6 +200,11 @@ export function detailLines(
|
|
|
199
200
|
? "Estimate not calibrated yet (characters ÷ 4 until the provider reports a size)"
|
|
200
201
|
: `Estimate ×${calibration.factor.toFixed(2)}, calibrated from ${calibration.samples} provider count${calibration.samples === 1 ? "" : "s"}`);
|
|
201
202
|
}
|
|
203
|
+
if (effective.settings.mode === "notices-only") {
|
|
204
|
+
lines.push(effective.editing
|
|
205
|
+
? "Mode notices-only: budget notices and the guard run; no mirror, the model cannot edit its context"
|
|
206
|
+
: "Mode notices-only (CLM editing is off, so nothing runs)");
|
|
207
|
+
}
|
|
202
208
|
const limit = model.budgetInfo?.limit;
|
|
203
209
|
lines.push(effective.settings.guard === "off"
|
|
204
210
|
? "Guard off"
|
package/src/tui/panel.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { TextChunk } from "@opentui/core";
|
|
|
9
9
|
import type { TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui";
|
|
10
10
|
|
|
11
11
|
import type { Page } from "../panel/command.ts";
|
|
12
|
-
import { reduce, type Effect, type Key } from "../panel/keys.ts";
|
|
12
|
+
import { reduce, withMessage, type Effect, type Key } from "../panel/keys.ts";
|
|
13
13
|
import type { Line, Tone } from "../panel/lines.ts";
|
|
14
14
|
import type { PanelModel } from "../panel/model.ts";
|
|
15
15
|
import { initialPanelState, renderPanel, type PanelSize, type PanelState } from "../panel/view.ts";
|
|
@@ -165,7 +165,8 @@ export function createPanel(api: PanelApi, options: PanelOptions): PanelControll
|
|
|
165
165
|
await controller.reload();
|
|
166
166
|
const row = model?.settings.rows.find((candidate) => candidate.key === setting);
|
|
167
167
|
const shown = `${row?.label ?? setting}: ${row?.value ?? value}`;
|
|
168
|
-
|
|
168
|
+
const message = error ? { text: error, warning: true } : { text: shown, warning: false };
|
|
169
|
+
state = model ? withMessage(state, message, model, size()) : { ...state, message };
|
|
169
170
|
render();
|
|
170
171
|
};
|
|
171
172
|
|
package/src/tui/plugin.ts
CHANGED
|
@@ -139,8 +139,9 @@ export const tui: TuiPlugin = async (api, tuiOptions) => {
|
|
|
139
139
|
const preferSettings = overridesNewer(current.overridesAt, files.snapshot);
|
|
140
140
|
const model = buildPanelModel(files, { ...(latest ? { latest } : {}), ...(budget ? { budget } : {}), ...(preferSettings ? { preferSettings } : {}) });
|
|
141
141
|
model.enabled = current.values.effective.editing;
|
|
142
|
+
if (current.values.effective.settings.mode === "notices-only") model.noticesOnly = true;
|
|
142
143
|
model.settings = settingsView(current.values, model, {
|
|
143
|
-
format: { ...(limits.context ? { modelWindow: limits.context } : {}) },
|
|
144
|
+
format: { ...(limits.context ? { modelWindow: limits.context } : {}), ...(limits.output ? { modelOutput: limits.output } : {}) },
|
|
144
145
|
...(current.warning ? { warning: current.warning } : {}),
|
|
145
146
|
});
|
|
146
147
|
model.settings.rows.push(resetRow(model.settings.changed.length));
|
|
@@ -456,7 +457,7 @@ export function statusSummary(model: PanelModel): string {
|
|
|
456
457
|
if (!model.found) return `No CLM data for this session (${model.directory}).`;
|
|
457
458
|
const latest = model.timeline.points.at(-1);
|
|
458
459
|
const parts = [
|
|
459
|
-
`CLM ${model.enabled ? "on" : "off"} · revision ${model.revision}`,
|
|
460
|
+
`CLM ${model.enabled ? "on" : "off"}${model.enabled && model.noticesOnly ? " · mode notices-only" : ""} · revision ${model.revision}`,
|
|
460
461
|
latest ? `last request ${latest.measured ? "" : "~"}${formatTokenCount(latest.tokens)}` : "no requests yet",
|
|
461
462
|
model.budget !== undefined ? `budget ${formatTokenCount(model.budget)}` : undefined,
|
|
462
463
|
model.budgetInfo?.overhead !== undefined ? `fixed overhead ~${formatTokenCount(model.budgetInfo.overhead)}` : undefined,
|