@kolisachint/hoocode-agent 0.5.29 → 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.
@@ -197,6 +197,10 @@ class ToolsSubmenu extends Container {
197
197
  ? "Core tool. Disabling leaves the agent unable to perform this action in every session."
198
198
  : "Disable to remove this tool from the agent this session and every future session.",
199
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,
200
204
  values: ["on", "off"],
201
205
  }));
202
206
  const items = [...groupItems, ...toolItems];
@@ -243,39 +247,45 @@ function byteLabels(current) {
243
247
  return all.sort((a, b) => a[1] - b[1]).map(([label]) => label);
244
248
  }
245
249
  /**
246
- * Submenu for per-tool runtime settings. These feed the tool runtime the next
247
- * time it is built (next session / rebuild), so changes apply to future tool
248
- * 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.
249
256
  */
250
257
  class ToolSettingsSubmenu extends Container {
251
258
  settingsList;
252
259
  constructor(config, callbacks, onCancel) {
253
260
  super();
254
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
+ },
255
269
  {
256
270
  id: "output-max-bytes",
257
- label: "Output max bytes",
271
+ label: "Max bytes",
258
272
  description: "Byte cap on a single read/bash result before truncation. Applies to future tool calls.",
259
273
  currentValue: bytesToLabel(config.toolOutputMaxBytes),
260
274
  values: byteLabels(config.toolOutputMaxBytes),
261
275
  },
262
276
  {
263
277
  id: "output-max-lines",
264
- label: "Output max lines",
278
+ label: "Max lines",
265
279
  description: "Line cap on a single read/bash result before truncation. Applies to future tool calls.",
266
280
  currentValue: String(config.toolOutputMaxLines),
267
281
  values: presetValues([200, 400, 800, 1600, 3200], config.toolOutputMaxLines),
268
282
  },
269
- {
270
- id: "context-gc",
271
- label: "Context GC",
272
- description: "Stub superseded read results (files later edited/re-read) out of the outgoing context.",
273
- currentValue: config.contextGc ? "true" : "false",
274
- values: ["true", "false"],
275
- },
276
283
  ];
277
284
  this.settingsList = new SettingsList(items, Math.min(items.length, 10), getSettingsListTheme(), (id, newValue) => {
278
285
  switch (id) {
286
+ case "tool-output-display":
287
+ callbacks.onToolOutputDisplayChange(newValue);
288
+ break;
279
289
  case "output-max-bytes": {
280
290
  const preset = TOOL_OUTPUT_BYTE_PRESETS.find(([label]) => label === newValue);
281
291
  // A hand-set cap has no preset entry; its label is "<n> KB" by
@@ -288,9 +298,6 @@ class ToolSettingsSubmenu extends Container {
288
298
  case "output-max-lines":
289
299
  callbacks.onToolOutputMaxLinesChange(parseInt(newValue, 10));
290
300
  break;
291
- case "context-gc":
292
- callbacks.onContextGcChange(newValue === "true");
293
- break;
294
301
  }
295
302
  }, onCancel);
296
303
  this.addChild(this.settingsList);
@@ -420,13 +427,41 @@ class SelectSubmenu extends Container {
420
427
  this.selectList.handleInput(data);
421
428
  }
422
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
+ }
423
455
  /**
424
456
  * Main settings selector component.
425
457
  */
426
458
  export class SettingsSelectorComponent extends Container {
427
459
  settingsList;
460
+ surfaceLine;
461
+ measureTokenSurface;
428
462
  constructor(config, callbacks) {
429
463
  super();
464
+ this.measureTokenSurface = config.measureTokenSurface;
430
465
  const supportsImages = getCapabilities().images;
431
466
  const followUpKey = keyDisplayText("app.message.followUp");
432
467
  let currentWarnings = { ...config.warnings };
@@ -437,6 +472,7 @@ export class SettingsSelectorComponent extends Container {
437
472
  const pinnedNote = (key) => projectPinned.has(key)
438
473
  ? ` This repo's .hoocode/settings.json sets ${key}, which overrides this row from the next session on.`
439
474
  : "";
475
+ const initialSurface = config.measureTokenSurface?.();
440
476
  const toolsOn = config.tools.filter((t) => t.enabled).length;
441
477
  const toolsOff = config.tools.length - toolsOn;
442
478
  const items = [
@@ -591,8 +627,26 @@ export class SettingsSelectorComponent extends Container {
591
627
  currentValue: config.blockImages ? "true" : "false",
592
628
  values: ["true", "false"],
593
629
  });
594
- // Skill commands toggle (insert after block-images)
595
- 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");
596
650
  items.splice(blockImagesIndex + 1, 0, {
597
651
  id: "skill-commands",
598
652
  label: "Skill commands",
@@ -724,30 +778,28 @@ export class SettingsSelectorComponent extends Container {
724
778
  {
725
779
  id: "tools",
726
780
  label: "Tools",
727
- 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.",
728
782
  currentValue: toolsOff > 0 ? `${toolsOn} on · ${toolsOff} off` : `${toolsOn} on`,
729
- 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()),
730
789
  },
731
790
  {
732
- id: "tool-output-display",
733
- label: "Tool output display",
734
- 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.",
735
794
  currentValue: config.toolOutputDisplay,
736
- values: ["standard", "collapsed", "peek"],
737
- },
738
- {
739
- id: "tool-settings",
740
- label: "Tool settings",
741
- description: "Per-tool runtime settings: output truncation caps and context garbage collection.",
742
- currentValue: "configure",
743
795
  submenu: (_currentValue, done) => new ToolSettingsSubmenu({
796
+ toolOutputDisplay: config.toolOutputDisplay,
744
797
  toolOutputMaxBytes: config.toolOutputMaxBytes,
745
798
  toolOutputMaxLines: config.toolOutputMaxLines,
746
- contextGc: config.contextGc,
747
799
  }, {
800
+ onToolOutputDisplayChange: callbacks.onToolOutputDisplayChange,
748
801
  onToolOutputMaxBytesChange: callbacks.onToolOutputMaxBytesChange,
749
802
  onToolOutputMaxLinesChange: callbacks.onToolOutputMaxLinesChange,
750
- onContextGcChange: callbacks.onContextGcChange,
751
803
  }, () => done()),
752
804
  },
753
805
  ];
@@ -769,9 +821,6 @@ export class SettingsSelectorComponent extends Container {
769
821
  case "autocompact":
770
822
  callbacks.onAutoCompactChange(newValue === "true");
771
823
  break;
772
- case "tool-output-display":
773
- callbacks.onToolOutputDisplayChange(newValue);
774
- break;
775
824
  case "show-images":
776
825
  callbacks.onShowImagesChange(newValue === "true");
777
826
  break;
@@ -787,6 +836,9 @@ export class SettingsSelectorComponent extends Container {
787
836
  case "skill-commands":
788
837
  callbacks.onEnableSkillCommandsChange(newValue === "true");
789
838
  break;
839
+ case "light":
840
+ callbacks.onLightChange(newValue === "true");
841
+ break;
790
842
  case "plugin-tools":
791
843
  callbacks.onEnablePluginToolsChange(newValue === "true");
792
844
  break;
@@ -855,6 +907,10 @@ export class SettingsSelectorComponent extends Container {
855
907
  callbacks.onLearnSettingChange(id, parseInt(newValue, 10));
856
908
  break;
857
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();
858
914
  };
859
915
  // Partition the flat leaf settings into named category submenus so the
860
916
  // top level stays short. `items` holds autocompact + every leaf setting.
@@ -876,8 +932,12 @@ export class SettingsSelectorComponent extends Container {
876
932
  };
877
933
  };
878
934
  const topItems = [
879
- ...(byId.has("autocompact") ? [byId.get("autocompact")] : []),
880
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"]),
881
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"]),
882
942
  categoryRow("cat-interface", "Interface", "Appearance and editor: theme, thinking visibility, cursor, border, padding, autocomplete, terminal.", [
883
943
  "theme",
@@ -916,8 +976,18 @@ export class SettingsSelectorComponent extends Container {
916
976
  enableSearch: true,
917
977
  });
918
978
  this.addChild(this.settingsList);
979
+ if (initialSurface) {
980
+ this.surfaceLine = new Text(formatSurfaceLine(initialSurface), 0, 0);
981
+ this.addChild(this.surfaceLine);
982
+ }
919
983
  this.addChild(new DynamicBorder());
920
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
+ }
921
991
  getSettingsList() {
922
992
  return this.settingsList;
923
993
  }