@kolisachint/hoocode-agent 0.5.28 → 0.5.30

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +4 -2
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/core/light.d.ts +9 -0
  6. package/dist/core/light.d.ts.map +1 -1
  7. package/dist/core/light.js +12 -4
  8. package/dist/core/light.js.map +1 -1
  9. package/dist/core/settings-manager.d.ts +8 -0
  10. package/dist/core/settings-manager.d.ts.map +1 -1
  11. package/dist/core/settings-manager.js +12 -0
  12. package/dist/core/settings-manager.js.map +1 -1
  13. package/dist/core/settings-types.d.ts.map +1 -1
  14. package/dist/core/settings-types.js.map +1 -1
  15. package/dist/main.d.ts.map +1 -1
  16. package/dist/main.js +6 -5
  17. package/dist/main.js.map +1 -1
  18. package/dist/modes/interactive/components/settings-selector.d.ts +31 -0
  19. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  20. package/dist/modes/interactive/components/settings-selector.js +245 -51
  21. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  22. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  23. package/dist/modes/interactive/interactive-mode.js +43 -1
  24. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  25. package/docs/plugins.md +3 -2
  26. package/docs/settings.md +64 -0
  27. package/docs/usage.md +3 -3
  28. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  29. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  30. package/examples/extensions/sandbox/package.json +1 -1
  31. package/examples/extensions/with-deps/package.json +1 -1
  32. package/package.json +4 -4
@@ -57,9 +57,9 @@ const LEARN_KEYS = new Set(LEARN_SETTINGS.map((setting) => setting.key));
57
57
  * Preset list for a numeric row, guaranteed to contain the value in force.
58
58
  *
59
59
  * Without this a value set by hand in settings.json — say 45 days — is absent
60
- * from the cycle, so the first keypress silently snaps it to the first preset.
61
- * These particular settings gate whether `/learn` finds anything at all, so a
62
- * stray keystroke narrowing the window is exactly the surprise to avoid.
60
+ * from the cycle, so the first keypress silently snaps it to the first preset,
61
+ * discarding a deliberate choice the pane never showed as unusual. Every numeric
62
+ * cycle row goes through this for that reason.
63
63
  */
64
64
  function presetValues(presets, current) {
65
65
  const all = presets.includes(current) ? presets : [...presets, current].sort((a, b) => a - b);
@@ -97,6 +97,73 @@ class WarningSettingsSubmenu extends Container {
97
97
  this.settingsList.handleInput(data);
98
98
  }
99
99
  }
100
+ /**
101
+ * The artifact platform targets, in the order the pane shows them. Each row is an
102
+ * independent on/off toggle because `--platform` (and the `platform` setting it
103
+ * mirrors) takes a list: emitting for two platforms at once is a supported shape.
104
+ * The tokens here are the canonical ones — the CLI aliases (`copilot`, `gh`,
105
+ * `native`) fold into these before anything reads them.
106
+ */
107
+ const PLATFORM_ROWS = [
108
+ {
109
+ platform: "claude",
110
+ label: "claude",
111
+ description: "Claude Code layout: .claude/ scaffolds, and authored plugins drop into ~/.claude/skills/<id>/. The default when nothing is set.",
112
+ },
113
+ {
114
+ platform: "github",
115
+ label: "github (copilot, gh)",
116
+ description: "Copilot layout: .github/ scaffolds, and authored plugins are produced under ~/.agents/publish/github/<id>/.",
117
+ },
118
+ {
119
+ platform: "agents",
120
+ label: "agents (native)",
121
+ description: "Cross-vendor .agents/ layout. Scaffolds only - a plugin belongs to no marketplace in this layout, so plugin authoring ignores it.",
122
+ },
123
+ ];
124
+ /** Row value for the platform setting: the selection, or the fallback when it is empty. */
125
+ function platformSummary(platforms) {
126
+ return platforms.length > 0 ? platforms.join(", ") : "default (claude)";
127
+ }
128
+ /**
129
+ * Submenu for the session's artifact platform targets (the `platform` setting,
130
+ * same knob as `--platform`).
131
+ *
132
+ * A list rather than a cycle: the setting is a list, and the three tokens are not
133
+ * mutually exclusive. Turning everything off is legal and means "unset" — the
134
+ * per-consumer defaults come back (claude for plugins, .hoocode/ for scaffolds).
135
+ */
136
+ class PlatformSubmenu extends Container {
137
+ settingsList;
138
+ selected;
139
+ constructor(platforms, onChange, onDone) {
140
+ super();
141
+ this.selected = new Set(platforms);
142
+ const items = PLATFORM_ROWS.map(({ platform, label, description }) => ({
143
+ id: platform,
144
+ label,
145
+ description,
146
+ currentValue: this.selected.has(platform) ? "on" : "off",
147
+ values: ["on", "off"],
148
+ }));
149
+ this.settingsList = new SettingsList(items, Math.min(items.length, 10), getSettingsListTheme(), (id, newValue) => {
150
+ const platform = id;
151
+ if (newValue === "on")
152
+ this.selected.add(platform);
153
+ else
154
+ this.selected.delete(platform);
155
+ onChange(this.ordered());
156
+ }, () => onDone(platformSummary(this.ordered())));
157
+ this.addChild(this.settingsList);
158
+ }
159
+ /** Selection in the pane's order, so the persisted list does not depend on click order. */
160
+ ordered() {
161
+ return PLATFORM_ROWS.map((row) => row.platform).filter((platform) => this.selected.has(platform));
162
+ }
163
+ handleInput(data) {
164
+ this.settingsList.handleInput(data);
165
+ }
166
+ }
100
167
  /**
101
168
  * Submenu for tool availability. The first rows are group switches (web,
102
169
  * semantic search) that decide whether a group's tools
@@ -130,6 +197,10 @@ class ToolsSubmenu extends Container {
130
197
  ? "Core tool. Disabling leaves the agent unable to perform this action in every session."
131
198
  : "Disable to remove this tool from the agent this session and every future session.",
132
199
  currentValue: tool.enabled ? "on" : "off",
200
+ // What the schema costs on every request, whether the tool is on or off:
201
+ // off, it is the price of turning it back on. This is the number that
202
+ // makes a tool worth disabling, so it belongs beside the switch.
203
+ valueSuffix: tool.tokens !== undefined ? `${tokenCount(tool.tokens)} tok/turn` : undefined,
133
204
  values: ["on", "off"],
134
205
  }));
135
206
  const items = [...groupItems, ...toolItems];
@@ -168,52 +239,65 @@ function bytesToLabel(bytes) {
168
239
  const match = TOOL_OUTPUT_BYTE_PRESETS.find(([, b]) => b === bytes);
169
240
  return match ? match[0] : `${Math.round(bytes / 1024)} KB`;
170
241
  }
242
+ /** Byte-cap labels to cycle through, including a hand-set cap that matches no preset. */
243
+ function byteLabels(current) {
244
+ const all = TOOL_OUTPUT_BYTE_PRESETS.some(([, bytes]) => bytes === current)
245
+ ? [...TOOL_OUTPUT_BYTE_PRESETS]
246
+ : [...TOOL_OUTPUT_BYTE_PRESETS, [bytesToLabel(current), current]];
247
+ return all.sort((a, b) => a[1] - b[1]).map(([label]) => label);
248
+ }
171
249
  /**
172
- * Submenu for per-tool runtime settings. These feed the tool runtime the next
173
- * time it is built (next session / rebuild), so changes apply to future tool
174
- * calls rather than retroactively.
250
+ * Submenu for what a tool result looks like: how much of it is rendered, and
251
+ * where it is truncated. The display level applies to the transcript at once;
252
+ * the caps feed the tool runtime, so they bind on future tool calls.
253
+ *
254
+ * Context GC is deliberately not here. It is not about a tool's output but about
255
+ * what stays in the outgoing context, which is the Context category's subject.
175
256
  */
176
257
  class ToolSettingsSubmenu extends Container {
177
258
  settingsList;
178
259
  constructor(config, callbacks, onCancel) {
179
260
  super();
180
261
  const items = [
262
+ {
263
+ id: "tool-output-display",
264
+ label: "Display",
265
+ description: "How tool results render. 'standard': shown (expandable). 'collapsed': hidden. 'peek': hidden with a ▸ reveal caret (press the expand key to reveal).",
266
+ currentValue: config.toolOutputDisplay,
267
+ values: ["standard", "collapsed", "peek"],
268
+ },
181
269
  {
182
270
  id: "output-max-bytes",
183
- label: "Output max bytes",
271
+ label: "Max bytes",
184
272
  description: "Byte cap on a single read/bash result before truncation. Applies to future tool calls.",
185
273
  currentValue: bytesToLabel(config.toolOutputMaxBytes),
186
- values: TOOL_OUTPUT_BYTE_PRESETS.map(([label]) => label),
274
+ values: byteLabels(config.toolOutputMaxBytes),
187
275
  },
188
276
  {
189
277
  id: "output-max-lines",
190
- label: "Output max lines",
278
+ label: "Max lines",
191
279
  description: "Line cap on a single read/bash result before truncation. Applies to future tool calls.",
192
280
  currentValue: String(config.toolOutputMaxLines),
193
- values: ["200", "400", "800", "1600", "3200"],
194
- },
195
- {
196
- id: "context-gc",
197
- label: "Context GC",
198
- description: "Stub superseded read results (files later edited/re-read) out of the outgoing context.",
199
- currentValue: config.contextGc ? "true" : "false",
200
- values: ["true", "false"],
281
+ values: presetValues([200, 400, 800, 1600, 3200], config.toolOutputMaxLines),
201
282
  },
202
283
  ];
203
284
  this.settingsList = new SettingsList(items, Math.min(items.length, 10), getSettingsListTheme(), (id, newValue) => {
204
285
  switch (id) {
286
+ case "tool-output-display":
287
+ callbacks.onToolOutputDisplayChange(newValue);
288
+ break;
205
289
  case "output-max-bytes": {
206
290
  const preset = TOOL_OUTPUT_BYTE_PRESETS.find(([label]) => label === newValue);
207
- if (preset)
208
- callbacks.onToolOutputMaxBytesChange(preset[1]);
291
+ // A hand-set cap has no preset entry; its label is "<n> KB" by
292
+ // construction, so read the number back out of it.
293
+ const bytes = preset ? preset[1] : Math.round(parseFloat(newValue) * 1024);
294
+ if (Number.isFinite(bytes) && bytes > 0)
295
+ callbacks.onToolOutputMaxBytesChange(bytes);
209
296
  break;
210
297
  }
211
298
  case "output-max-lines":
212
299
  callbacks.onToolOutputMaxLinesChange(parseInt(newValue, 10));
213
300
  break;
214
- case "context-gc":
215
- callbacks.onContextGcChange(newValue === "true");
216
- break;
217
301
  }
218
302
  }, onCancel);
219
303
  this.addChild(this.settingsList);
@@ -343,16 +427,52 @@ class SelectSubmenu extends Container {
343
427
  this.selectList.handleInput(data);
344
428
  }
345
429
  }
430
+ /**
431
+ * Token counts, grouped and exact.
432
+ *
433
+ * `formatTokens` (2.7k) exists for the fixed-width chrome, where a count must
434
+ * never grow the box it sits in. This pane is the opposite case: the point of
435
+ * showing a number here is to compare it with another one, and "2.7k" hides the
436
+ * difference between the tool that costs 2,710 and the one that costs 2,749.
437
+ */
438
+ function tokenCount(tokens) {
439
+ return tokens.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
440
+ }
441
+ /**
442
+ * The fixed per-turn cost, as one line under the pane.
443
+ *
444
+ * Every byte of system prompt and active tool schema is re-sent on every
445
+ * request, and the pane is where that number is decided - so the pane is where
446
+ * it should be visible. It reports the *live* session: a tool toggle applies at
447
+ * once and moves it, while a setting that only takes effect next session (a tool
448
+ * group, the light preset) leaves it alone until then.
449
+ */
450
+ function formatSurfaceLine(surface) {
451
+ const tools = `${surface.tools.length} tool${surface.tools.length === 1 ? "" : "s"}`;
452
+ return (` Per-turn surface: ${tokenCount(surface.totalTokens)} tokens ` +
453
+ `${theme.fg("dim", `(${tokenCount(surface.systemPromptTokens)} system prompt + ${tokenCount(surface.toolSchemaTokens)} schemas, ${tools})`)}`);
454
+ }
346
455
  /**
347
456
  * Main settings selector component.
348
457
  */
349
458
  export class SettingsSelectorComponent extends Container {
350
459
  settingsList;
460
+ surfaceLine;
461
+ measureTokenSurface;
351
462
  constructor(config, callbacks) {
352
463
  super();
464
+ this.measureTokenSurface = config.measureTokenSurface;
353
465
  const supportsImages = getCapabilities().images;
354
466
  const followUpKey = keyDisplayText("app.message.followUp");
355
467
  let currentWarnings = { ...config.warnings };
468
+ // A row writes the user settings file; a key the project file also sets is
469
+ // merged over it on the next session, so say so rather than letting the row
470
+ // look like it took.
471
+ const projectPinned = new Set(config.projectPinnedSettings);
472
+ const pinnedNote = (key) => projectPinned.has(key)
473
+ ? ` This repo's .hoocode/settings.json sets ${key}, which overrides this row from the next session on.`
474
+ : "";
475
+ const initialSurface = config.measureTokenSurface?.();
356
476
  const toolsOn = config.tools.filter((t) => t.enabled).length;
357
477
  const toolsOff = config.tools.length - toolsOn;
358
478
  const items = [
@@ -487,7 +607,7 @@ export class SettingsSelectorComponent extends Container {
487
607
  label: "Image width",
488
608
  description: "Preferred inline image width in terminal cells",
489
609
  currentValue: String(config.imageWidthCells),
490
- values: ["60", "80", "120"],
610
+ values: presetValues([60, 80, 120], config.imageWidthCells),
491
611
  });
492
612
  }
493
613
  // Image auto-resize toggle (always available, affects both attached and read images)
@@ -507,8 +627,26 @@ export class SettingsSelectorComponent extends Container {
507
627
  currentValue: config.blockImages ? "true" : "false",
508
628
  values: ["true", "false"],
509
629
  });
510
- // Skill commands toggle (insert after block-images)
511
- const blockImagesIndex = items.findIndex((item) => item.id === "block-images");
630
+ // Context GC (insert after block-images), a leaf the Context category picks up.
631
+ items.splice(items.findIndex((item) => item.id === "block-images") + 1, 0, {
632
+ id: "context-gc",
633
+ label: "Context GC",
634
+ description: "Stub superseded read results (files later edited or re-read) out of the outgoing context.",
635
+ currentValue: config.contextGc ? "true" : "false",
636
+ values: ["true", "false"],
637
+ });
638
+ // The light preset (insert after context GC). Read at startup to pick the
639
+ // tool set and the system prompt, so it lands on the next session.
640
+ const blockImagesIdx = items.findIndex((item) => item.id === "context-gc");
641
+ items.splice(blockImagesIdx + 1, 0, {
642
+ id: "light",
643
+ label: "Light preset",
644
+ description: "Low-token preset for small or local models: read/write/edit/bash only with stripped schemas, a terse system prompt, and no subagents/TodoWrite/skills/context files. Applies on the next session.",
645
+ currentValue: config.light ? "true" : "false",
646
+ values: ["true", "false"],
647
+ });
648
+ // Skill commands toggle (insert after the light preset)
649
+ const blockImagesIndex = items.findIndex((item) => item.id === "light");
512
650
  items.splice(blockImagesIndex + 1, 0, {
513
651
  id: "skill-commands",
514
652
  label: "Skill commands",
@@ -516,19 +654,44 @@ export class SettingsSelectorComponent extends Container {
516
654
  currentValue: config.enableSkillCommands ? "true" : "false",
517
655
  values: ["true", "false"],
518
656
  });
519
- // Plugin install scope (insert after skill-commands). Governs the
520
- // autonomous InstallPlugin only /plugin install asks per install.
657
+ // The autonomous plugin system's master switch (insert after skill-commands).
658
+ // One flag for the lifecycle tools and the reuse nudge, so both flip together.
521
659
  const skillCommandsIdx = items.findIndex((item) => item.id === "skill-commands");
522
660
  items.splice(skillCommandsIdx + 1, 0, {
661
+ id: "plugin-tools",
662
+ label: "Plugin system",
663
+ description: `Autonomous plugin system: the lifecycle tools (SearchPlugins, InstallPlugin, ...), ProposePlugin, and the plugin-reuse nudge. Tools arrive on the next session; the nudge follows at once.${pinnedNote("enablePluginTools")}`,
664
+ currentValue: config.enablePluginTools ? "true" : "false",
665
+ values: ["true", "false"],
666
+ });
667
+ // Plugin install scope (insert after the master switch). Governs the
668
+ // autonomous InstallPlugin only — /plugin install asks per install.
669
+ const pluginToolsIdx = items.findIndex((item) => item.id === "plugin-tools");
670
+ items.splice(pluginToolsIdx + 1, 0, {
523
671
  id: "plugin-install-scope",
524
672
  label: "Plugin install scope",
525
673
  description: "Where autonomous plugin installs go: user (~/.agents) or project (this repo, shared)",
526
674
  currentValue: config.pluginInstallScope,
527
675
  values: ["user", "project"],
528
676
  });
529
- // Hardware cursor toggle (insert after plugin-install-scope)
530
- const skillCommandsIndex = items.findIndex((item) => item.id === "plugin-install-scope");
531
- items.splice(skillCommandsIndex + 1, 0, {
677
+ // Artifact platform targets (insert after plugin-install-scope). Set once and
678
+ // it holds for every later session: it is the `platform` setting, which
679
+ // `--platform` overrides for a single run.
680
+ const pluginScopeIdx = items.findIndex((item) => item.id === "plugin-install-scope");
681
+ let currentPlatforms = [...config.platform];
682
+ items.splice(pluginScopeIdx + 1, 0, {
683
+ id: "platform",
684
+ label: "Platform",
685
+ description: `Vendor layout(s) hoocode writes artifacts in: authored plugins and the /new-skill //new-agent //new-command scaffolds.${pinnedNote("platform")}`,
686
+ currentValue: platformSummary(currentPlatforms),
687
+ submenu: (_currentValue, done) => new PlatformSubmenu(currentPlatforms, (platforms) => {
688
+ currentPlatforms = platforms;
689
+ callbacks.onPlatformChange(platforms);
690
+ }, (summary) => done(summary)),
691
+ });
692
+ // Hardware cursor toggle (insert after the platform row)
693
+ const platformIndex = items.findIndex((item) => item.id === "platform");
694
+ items.splice(platformIndex + 1, 0, {
532
695
  id: "show-hardware-cursor",
533
696
  label: "Show hardware cursor",
534
697
  description: "Show the terminal cursor while still positioning it for IME support",
@@ -560,7 +723,7 @@ export class SettingsSelectorComponent extends Container {
560
723
  label: "Autocomplete max items",
561
724
  description: "Max visible items in autocomplete dropdown (3-20)",
562
725
  currentValue: String(config.autocompleteMaxVisible),
563
- values: ["3", "5", "7", "10", "15", "20"],
726
+ values: presetValues([3, 5, 7, 10, 15, 20], config.autocompleteMaxVisible),
564
727
  });
565
728
  // Clear on shrink toggle (insert after autocomplete-max-visible)
566
729
  const autocompleteIndex = items.findIndex((item) => item.id === "autocomplete-max-visible");
@@ -587,7 +750,7 @@ export class SettingsSelectorComponent extends Container {
587
750
  label: "Voice silence window",
588
751
  description: "Trailing-silence (ms) before voice capture auto-stops (300-10000). Env: VOICETOOLS_SILENCE_MS.",
589
752
  currentValue: String(config.voiceSilenceMs),
590
- values: ["300", "500", "800", "1200", "2000", "3000", "5000", "8000", "10000"],
753
+ values: presetValues([300, 500, 800, 1200, 2000, 3000, 5000, 8000, 10000], config.voiceSilenceMs),
591
754
  });
592
755
  // Webtools request timeout (insert after voice-silence-ms)
593
756
  const voiceSilenceIndex = items.findIndex((item) => item.id === "voice-silence-ms");
@@ -596,7 +759,7 @@ export class SettingsSelectorComponent extends Container {
596
759
  label: "Web tools timeout",
597
760
  description: "Per-request timeout (secs) for webfetch/websearch (1-120). Env: HOOCODE_WEBTOOLS_TIMEOUT.",
598
761
  currentValue: String(config.webtoolsTimeoutSecs),
599
- values: ["5", "10", "15", "30", "60", "120"],
762
+ values: presetValues([5, 10, 15, 30, 60, 120], config.webtoolsTimeoutSecs),
600
763
  });
601
764
  // The /learn thresholds, appended as leaf rows and gathered into their own
602
765
  // category below. They are written to the user settings.json, which /learn
@@ -615,30 +778,28 @@ export class SettingsSelectorComponent extends Container {
615
778
  {
616
779
  id: "tools",
617
780
  label: "Tools",
618
- description: "Enable/disable tools and tool groups (web, semantic search). Changes persist across sessions.",
781
+ description: "Enable/disable tools and tool groups (web, semantic search), each priced by what its schema costs per turn. Changes persist across sessions.",
619
782
  currentValue: toolsOff > 0 ? `${toolsOn} on · ${toolsOff} off` : `${toolsOn} on`,
620
- submenu: (_currentValue, done) => new ToolsSubmenu(config.tools, config.toolGroups, (name, enabled) => callbacks.onToolEnabledChange(name, enabled), (id, enabled) => callbacks.onToolGroupChange(id, enabled), () => done()),
783
+ valueSuffix: initialSurface ? `${tokenCount(initialSurface.toolSchemaTokens)} tok/turn` : undefined,
784
+ submenu: (_currentValue, done) => new ToolsSubmenu(config.tools, config.toolGroups, (name, enabled) => {
785
+ callbacks.onToolEnabledChange(name, enabled);
786
+ // Applied live by the host, so the surface below is already stale.
787
+ this.refreshTokenSurface();
788
+ }, (id, enabled) => callbacks.onToolGroupChange(id, enabled), () => done()),
621
789
  },
622
790
  {
623
- id: "tool-output-display",
624
- label: "Tool output display",
625
- description: "How tool results render. 'standard': shown (expandable). 'collapsed': hidden. 'peek': hidden with a reveal caret (press the expand key to reveal).",
791
+ id: "tool-output",
792
+ label: "Tool output",
793
+ description: "How much of a tool result is rendered, and where it is truncated.",
626
794
  currentValue: config.toolOutputDisplay,
627
- values: ["standard", "collapsed", "peek"],
628
- },
629
- {
630
- id: "tool-settings",
631
- label: "Tool settings",
632
- description: "Per-tool runtime settings: output truncation caps and context garbage collection.",
633
- currentValue: "configure",
634
795
  submenu: (_currentValue, done) => new ToolSettingsSubmenu({
796
+ toolOutputDisplay: config.toolOutputDisplay,
635
797
  toolOutputMaxBytes: config.toolOutputMaxBytes,
636
798
  toolOutputMaxLines: config.toolOutputMaxLines,
637
- contextGc: config.contextGc,
638
799
  }, {
800
+ onToolOutputDisplayChange: callbacks.onToolOutputDisplayChange,
639
801
  onToolOutputMaxBytesChange: callbacks.onToolOutputMaxBytesChange,
640
802
  onToolOutputMaxLinesChange: callbacks.onToolOutputMaxLinesChange,
641
- onContextGcChange: callbacks.onContextGcChange,
642
803
  }, () => done()),
643
804
  },
644
805
  ];
@@ -660,9 +821,6 @@ export class SettingsSelectorComponent extends Container {
660
821
  case "autocompact":
661
822
  callbacks.onAutoCompactChange(newValue === "true");
662
823
  break;
663
- case "tool-output-display":
664
- callbacks.onToolOutputDisplayChange(newValue);
665
- break;
666
824
  case "show-images":
667
825
  callbacks.onShowImagesChange(newValue === "true");
668
826
  break;
@@ -678,9 +836,19 @@ export class SettingsSelectorComponent extends Container {
678
836
  case "skill-commands":
679
837
  callbacks.onEnableSkillCommandsChange(newValue === "true");
680
838
  break;
839
+ case "light":
840
+ callbacks.onLightChange(newValue === "true");
841
+ break;
842
+ case "plugin-tools":
843
+ callbacks.onEnablePluginToolsChange(newValue === "true");
844
+ break;
681
845
  case "plugin-install-scope":
682
846
  callbacks.onPluginInstallScopeChange(newValue);
683
847
  break;
848
+ case "platform":
849
+ // Display only. Each toggle inside the submenu already applied itself;
850
+ // closing it hands back the summary purely to refresh this row's value.
851
+ break;
684
852
  case "steering-mode":
685
853
  callbacks.onSteeringModeChange(newValue);
686
854
  break;
@@ -739,6 +907,10 @@ export class SettingsSelectorComponent extends Container {
739
907
  callbacks.onLearnSettingChange(id, parseInt(newValue, 10));
740
908
  break;
741
909
  }
910
+ // Most rows leave the per-turn surface alone; the ones that do not (a tool
911
+ // toggle, anything that rebuilds the system prompt) move it immediately.
912
+ // Re-measuring after every change is cheaper than knowing which is which.
913
+ this.refreshTokenSurface();
742
914
  };
743
915
  // Partition the flat leaf settings into named category submenus so the
744
916
  // top level stays short. `items` holds autocompact + every leaf setting.
@@ -751,12 +923,21 @@ export class SettingsSelectorComponent extends Container {
751
923
  label,
752
924
  description,
753
925
  currentValue: `${members.length} setting${members.length === 1 ? "" : "s"}`,
926
+ // Categories shortened the top level but also hid every setting from
927
+ // its search: searching "theme" matched no category label. The member
928
+ // labels ride along as search text so the query lands on the category
929
+ // that holds the setting.
930
+ keywords: members.map((member) => member.label).join(" "),
754
931
  submenu: (_currentValue, done) => new CategorySubmenu(members, applyChange, () => done()),
755
932
  };
756
933
  };
757
934
  const topItems = [
758
- ...(byId.has("autocompact") ? [byId.get("autocompact")] : []),
759
935
  ...toolFlagGroup,
936
+ // What the model is sent, and how much of it. Auto-compact used to sit
937
+ // alone at the top level and context GC was filed under tool settings,
938
+ // which left the three settings that decide the token budget in three
939
+ // different places - with the light preset in none of them.
940
+ categoryRow("cat-context", "Context", "What the model is sent and how much of it: compaction, superseded reads, and the low-token preset.", ["autocompact", "context-gc", "light"]),
760
941
  categoryRow("cat-behavior", "Behavior", "Agent and session behavior: steering, follow-up, thinking, escape, tree filter, transport.", ["steering-mode", "follow-up-mode", "thinking", "double-escape-action", "tree-filter-mode", "transport"]),
761
942
  categoryRow("cat-interface", "Interface", "Appearance and editor: theme, thinking visibility, cursor, border, padding, autocomplete, terminal.", [
762
943
  "theme",
@@ -768,6 +949,9 @@ export class SettingsSelectorComponent extends Container {
768
949
  "clear-on-shrink",
769
950
  "terminal-progress",
770
951
  ]),
952
+ // Artifact production. `plugin-install-scope` had no category and was
953
+ // therefore unreachable from the pane despite having a live callback.
954
+ categoryRow("cat-plugins", "Plugins", "The autonomous plugin system's master switch, the vendor layout hoocode writes, and where autonomous installs land.", ["plugin-tools", "platform", "plugin-install-scope"]),
771
955
  categoryRow("cat-images", "Images", "Inline image rendering and resizing.", [
772
956
  "show-images",
773
957
  "image-width-cells",
@@ -792,8 +976,18 @@ export class SettingsSelectorComponent extends Container {
792
976
  enableSearch: true,
793
977
  });
794
978
  this.addChild(this.settingsList);
979
+ if (initialSurface) {
980
+ this.surfaceLine = new Text(formatSurfaceLine(initialSurface), 0, 0);
981
+ this.addChild(this.surfaceLine);
982
+ }
795
983
  this.addChild(new DynamicBorder());
796
984
  }
985
+ /** Re-price the pane after a change that may have altered what each turn sends. */
986
+ refreshTokenSurface() {
987
+ if (!this.surfaceLine || !this.measureTokenSurface)
988
+ return;
989
+ this.surfaceLine.setText(formatSurfaceLine(this.measureTokenSurface()));
990
+ }
797
991
  getSettingsList() {
798
992
  return this.settingsList;
799
993
  }