@kolisachint/hoocode-agent 0.5.29 → 0.5.31
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/CHANGELOG.md +127 -0
- package/dist/core/agent-selection-eval.d.ts +91 -0
- package/dist/core/agent-selection-eval.d.ts.map +1 -0
- package/dist/core/agent-selection-eval.js +186 -0
- package/dist/core/agent-selection-eval.js.map +1 -0
- package/dist/core/builtin-skills.d.ts +61 -0
- package/dist/core/builtin-skills.d.ts.map +1 -0
- package/dist/core/builtin-skills.js +106 -0
- package/dist/core/builtin-skills.js.map +1 -0
- package/dist/core/extensions/plugins/default-marketplace/.agents-plugin/marketplace.json +5 -1
- package/dist/core/extensions/plugins/trigger-judge.d.ts +42 -0
- package/dist/core/extensions/plugins/trigger-judge.d.ts.map +1 -0
- package/dist/core/extensions/plugins/trigger-judge.js +121 -0
- package/dist/core/extensions/plugins/trigger-judge.js.map +1 -0
- package/dist/core/external-tools.d.ts +67 -0
- package/dist/core/external-tools.d.ts.map +1 -0
- package/dist/core/external-tools.js +120 -0
- package/dist/core/external-tools.js.map +1 -0
- package/dist/core/light.d.ts +9 -0
- package/dist/core/light.d.ts.map +1 -1
- package/dist/core/light.js +12 -4
- package/dist/core/light.js.map +1 -1
- package/dist/core/mode-prompts.d.ts +15 -3
- package/dist/core/mode-prompts.d.ts.map +1 -1
- package/dist/core/mode-prompts.js +17 -29
- package/dist/core/mode-prompts.js.map +1 -1
- package/dist/core/tools/propose-plugin.d.ts.map +1 -1
- package/dist/core/tools/propose-plugin.js +7 -7
- package/dist/core/tools/propose-plugin.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +7 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +12 -24
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/extensions/core/modes.d.ts.map +1 -1
- package/dist/extensions/core/modes.js +21 -23
- package/dist/extensions/core/modes.js.map +1 -1
- package/dist/extensions/core/scaffold.d.ts.map +1 -1
- package/dist/extensions/core/scaffold.js +28 -24
- package/dist/extensions/core/scaffold.js.map +1 -1
- package/dist/init-templates.generated.d.ts +3 -0
- package/dist/init-templates.generated.d.ts.map +1 -1
- package/dist/init-templates.generated.js +12 -0
- package/dist/init-templates.generated.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +14 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +27 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +256 -45
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +26 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +30 -0
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +35 -0
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/modes.md +4 -0
- package/docs/plugins.md +10 -0
- package/docs/settings.md +63 -0
- package/docs/skills.md +30 -0
- package/docs/usage.md +3 -3
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +5 -4
- package/templates/prompts/grill-bridge.md +1 -0
- package/templates/prompts/grill-me.md +7 -0
- package/templates/prompts/grill-plan.md +9 -0
- package/templates/prompts/task-background-agents.md +2 -0
- package/templates/prompts/task-background-none.md +1 -0
- package/templates/prompts/task-main.md +19 -0
- package/templates/skills/plugin-authoring/SKILL.md +81 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Container, getCapabilities, Input, SelectList, SettingsList, Spacer, Text, } from "@kolisachint/hoocode-tui";
|
|
2
|
+
import { buildRowGates, statusLabel } from "../../../core/external-tools.js";
|
|
2
3
|
import { getSelectListTheme, getSettingsListTheme, getThemeDescription, theme } from "../theme/theme.js";
|
|
3
4
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
4
5
|
import { keyDisplayText } from "./keybinding-hints.js";
|
|
@@ -164,6 +165,34 @@ class PlatformSubmenu extends Container {
|
|
|
164
165
|
this.settingsList.handleInput(data);
|
|
165
166
|
}
|
|
166
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Trailing sentence for a row whose setting is inert until an external binary
|
|
170
|
+
* shows up. The row is annotated rather than hidden: the binaries were invisible
|
|
171
|
+
* precisely because nothing ever mentioned them, and a row that disappears when
|
|
172
|
+
* its dependency is missing teaches the user nothing at all.
|
|
173
|
+
*/
|
|
174
|
+
function gateNote(gate) {
|
|
175
|
+
if (!gate || gate.installed)
|
|
176
|
+
return "";
|
|
177
|
+
return gate.downloadable
|
|
178
|
+
? ` Needs the ${gate.tool} binary, which hoocode fetches the first time this is used. Until then: ${gate.fallback}`
|
|
179
|
+
: ` Needs the ${gate.tool} binary, and this environment will not fetch it. Until then: ${gate.fallback}`;
|
|
180
|
+
}
|
|
181
|
+
/** "3 of 5 installed" - the one number that says whether this category needs attention. */
|
|
182
|
+
function externalCount(statuses) {
|
|
183
|
+
const installed = statuses.filter((status) => status.installed).length;
|
|
184
|
+
return `${installed} of ${statuses.length} installed`;
|
|
185
|
+
}
|
|
186
|
+
/** The capability words, so the category row says what the binaries are for. */
|
|
187
|
+
function externalSummary(statuses) {
|
|
188
|
+
return statuses.map((status) => status.tool).join(", ");
|
|
189
|
+
}
|
|
190
|
+
/** Value-column marker for a gated row, so the list shows it without selecting. */
|
|
191
|
+
function gateSuffix(gate) {
|
|
192
|
+
if (!gate || gate.installed)
|
|
193
|
+
return undefined;
|
|
194
|
+
return `needs ${gate.tool}`;
|
|
195
|
+
}
|
|
167
196
|
/**
|
|
168
197
|
* Submenu for tool availability. The first rows are group switches (web,
|
|
169
198
|
* semantic search) that decide whether a group's tools
|
|
@@ -180,16 +209,26 @@ class ToolsSubmenu extends Container {
|
|
|
180
209
|
enabled;
|
|
181
210
|
static CORE = new Set(["read", "bash", "edit", "write"]);
|
|
182
211
|
static GROUP_PREFIX = "group:";
|
|
183
|
-
constructor(tools, groups,
|
|
212
|
+
constructor(tools, groups,
|
|
213
|
+
/** Rows gated on an external binary, keyed by row id. See buildRowGates. */
|
|
214
|
+
gates, onChange, onGroupChange, onCancel) {
|
|
184
215
|
super();
|
|
185
216
|
this.enabled = new Map(tools.map((t) => [t.name, t.enabled]));
|
|
186
|
-
const groupItems = groups.map((group) =>
|
|
187
|
-
id
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
217
|
+
const groupItems = groups.map((group) => {
|
|
218
|
+
const id = `${ToolsSubmenu.GROUP_PREFIX}${group.id}`;
|
|
219
|
+
const gate = gates.get(id);
|
|
220
|
+
return {
|
|
221
|
+
id,
|
|
222
|
+
label: `[group] ${group.label}`,
|
|
223
|
+
description: `${group.description} Governs whether these tools exist; applies on the next session.${gateNote(gate)}`,
|
|
224
|
+
currentValue: group.enabled ? "on" : "off",
|
|
225
|
+
// The switch stays settable with the binary missing: the setting is
|
|
226
|
+
// what makes hoocode fetch it, so refusing the toggle would make the
|
|
227
|
+
// group unreachable forever.
|
|
228
|
+
valueSuffix: gateSuffix(gate),
|
|
229
|
+
values: ["on", "off"],
|
|
230
|
+
};
|
|
231
|
+
});
|
|
193
232
|
const toolItems = tools.map((tool) => ({
|
|
194
233
|
id: tool.name,
|
|
195
234
|
label: tool.name,
|
|
@@ -197,6 +236,10 @@ class ToolsSubmenu extends Container {
|
|
|
197
236
|
? "Core tool. Disabling leaves the agent unable to perform this action in every session."
|
|
198
237
|
: "Disable to remove this tool from the agent this session and every future session.",
|
|
199
238
|
currentValue: tool.enabled ? "on" : "off",
|
|
239
|
+
// What the schema costs on every request, whether the tool is on or off:
|
|
240
|
+
// off, it is the price of turning it back on. This is the number that
|
|
241
|
+
// makes a tool worth disabling, so it belongs beside the switch.
|
|
242
|
+
valueSuffix: tool.tokens !== undefined ? `${tokenCount(tool.tokens)} tok/turn` : undefined,
|
|
200
243
|
values: ["on", "off"],
|
|
201
244
|
}));
|
|
202
245
|
const items = [...groupItems, ...toolItems];
|
|
@@ -223,6 +266,88 @@ class ToolsSubmenu extends Container {
|
|
|
223
266
|
this.settingsList.handleInput(data);
|
|
224
267
|
}
|
|
225
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Read-only detail for one external binary: what it is, whether it is here, what
|
|
271
|
+
* it turns on, and what hoocode does without it.
|
|
272
|
+
*
|
|
273
|
+
* A submenu rather than a long `description` because the useful answer is five
|
|
274
|
+
* or six lines and the pane shows descriptions on one. Nothing here is settable
|
|
275
|
+
* - the binary is either on the machine or it is not - so the rows carry no
|
|
276
|
+
* `values` and Enter does nothing but close.
|
|
277
|
+
*/
|
|
278
|
+
class ExternalToolDetailSubmenu extends Container {
|
|
279
|
+
settingsList;
|
|
280
|
+
constructor(status, onCancel) {
|
|
281
|
+
super();
|
|
282
|
+
const acquisition = status.acquisition === "startup"
|
|
283
|
+
? "Downloaded in the background at startup."
|
|
284
|
+
: status.acquisition === "on-demand"
|
|
285
|
+
? "Downloaded the first time the feature is used."
|
|
286
|
+
: "Never downloaded automatically; install it yourself.";
|
|
287
|
+
const location = status.installed
|
|
288
|
+
? status.source === "path"
|
|
289
|
+
? `found on PATH as \`${status.path}\``
|
|
290
|
+
: status.source === "override"
|
|
291
|
+
? `${status.overrideEnv} points at ${status.path}`
|
|
292
|
+
: `hoocode's own copy at ${status.path}`
|
|
293
|
+
: status.downloadable
|
|
294
|
+
? `${acquisition} Not on this machine yet.`
|
|
295
|
+
: "Not installed, and this environment will not download it (offline mode, or no published build for this platform).";
|
|
296
|
+
const rows = [
|
|
297
|
+
["Status", statusLabel(status), location],
|
|
298
|
+
[
|
|
299
|
+
"Enables",
|
|
300
|
+
`${status.enables.length} feature${status.enables.length === 1 ? "" : "s"}`,
|
|
301
|
+
status.enables.join("; "),
|
|
302
|
+
],
|
|
303
|
+
["Without it", "fallback", status.fallback],
|
|
304
|
+
["Source", status.repo || "-", `Release archives come from github.com/${status.repo}. ${acquisition}`],
|
|
305
|
+
["Env", String(status.env.length), status.env.join(" | ")],
|
|
306
|
+
];
|
|
307
|
+
const items = rows.map(([label, value, description], index) => ({
|
|
308
|
+
id: `detail-${index}`,
|
|
309
|
+
label,
|
|
310
|
+
currentValue: value,
|
|
311
|
+
description,
|
|
312
|
+
}));
|
|
313
|
+
this.settingsList = new SettingsList(items, items.length, getSettingsListTheme(), () => { }, onCancel);
|
|
314
|
+
this.addChild(this.settingsList);
|
|
315
|
+
}
|
|
316
|
+
handleInput(data) {
|
|
317
|
+
this.settingsList.handleInput(data);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The external-binary category: one row per managed Rust binary.
|
|
322
|
+
*
|
|
323
|
+
* These binaries were entirely invisible before this pane existed. hoocode works
|
|
324
|
+
* without every one of them, which is the whole reason nobody found them: search
|
|
325
|
+
* silently got slower, semantic ranking silently never happened, and web/voice
|
|
326
|
+
* were simply features nobody knew were there. The row states presence; the
|
|
327
|
+
* submenu states what presence buys.
|
|
328
|
+
*/
|
|
329
|
+
class ExternalToolsSubmenu extends Container {
|
|
330
|
+
settingsList;
|
|
331
|
+
constructor(statuses, onCancel) {
|
|
332
|
+
super();
|
|
333
|
+
const items = statuses.map((status) => ({
|
|
334
|
+
id: `ext-${status.tool}`,
|
|
335
|
+
label: status.label,
|
|
336
|
+
description: `${status.summary} Without it: ${status.fallback}`,
|
|
337
|
+
currentValue: statusLabel(status),
|
|
338
|
+
// The value column says whether it is here; the suffix says what it is
|
|
339
|
+
// for, so the list reads without opening every row.
|
|
340
|
+
valueSuffix: status.installed ? undefined : status.downloadable ? "auto-fetch" : "manual",
|
|
341
|
+
keywords: [status.tool, status.repo, ...status.settingsKeys, ...status.enables].join(" "),
|
|
342
|
+
submenu: (_currentValue, done) => new ExternalToolDetailSubmenu(status, () => done()),
|
|
343
|
+
}));
|
|
344
|
+
this.settingsList = new SettingsList(items, Math.min(items.length, 10), getSettingsListTheme(), () => { }, onCancel, { enableSearch: true });
|
|
345
|
+
this.addChild(this.settingsList);
|
|
346
|
+
}
|
|
347
|
+
handleInput(data) {
|
|
348
|
+
this.settingsList.handleInput(data);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
226
351
|
/** Byte-cap presets shown as human labels; mapped back to raw byte counts. */
|
|
227
352
|
const TOOL_OUTPUT_BYTE_PRESETS = [
|
|
228
353
|
["8 KB", 8 * 1024],
|
|
@@ -243,39 +368,45 @@ function byteLabels(current) {
|
|
|
243
368
|
return all.sort((a, b) => a[1] - b[1]).map(([label]) => label);
|
|
244
369
|
}
|
|
245
370
|
/**
|
|
246
|
-
* Submenu for
|
|
247
|
-
*
|
|
248
|
-
*
|
|
371
|
+
* Submenu for what a tool result looks like: how much of it is rendered, and
|
|
372
|
+
* where it is truncated. The display level applies to the transcript at once;
|
|
373
|
+
* the caps feed the tool runtime, so they bind on future tool calls.
|
|
374
|
+
*
|
|
375
|
+
* Context GC is deliberately not here. It is not about a tool's output but about
|
|
376
|
+
* what stays in the outgoing context, which is the Context category's subject.
|
|
249
377
|
*/
|
|
250
378
|
class ToolSettingsSubmenu extends Container {
|
|
251
379
|
settingsList;
|
|
252
380
|
constructor(config, callbacks, onCancel) {
|
|
253
381
|
super();
|
|
254
382
|
const items = [
|
|
383
|
+
{
|
|
384
|
+
id: "tool-output-display",
|
|
385
|
+
label: "Display",
|
|
386
|
+
description: "How tool results render. 'standard': shown (expandable). 'collapsed': hidden. 'peek': hidden with a ▸ reveal caret (press the expand key to reveal).",
|
|
387
|
+
currentValue: config.toolOutputDisplay,
|
|
388
|
+
values: ["standard", "collapsed", "peek"],
|
|
389
|
+
},
|
|
255
390
|
{
|
|
256
391
|
id: "output-max-bytes",
|
|
257
|
-
label: "
|
|
392
|
+
label: "Max bytes",
|
|
258
393
|
description: "Byte cap on a single read/bash result before truncation. Applies to future tool calls.",
|
|
259
394
|
currentValue: bytesToLabel(config.toolOutputMaxBytes),
|
|
260
395
|
values: byteLabels(config.toolOutputMaxBytes),
|
|
261
396
|
},
|
|
262
397
|
{
|
|
263
398
|
id: "output-max-lines",
|
|
264
|
-
label: "
|
|
399
|
+
label: "Max lines",
|
|
265
400
|
description: "Line cap on a single read/bash result before truncation. Applies to future tool calls.",
|
|
266
401
|
currentValue: String(config.toolOutputMaxLines),
|
|
267
402
|
values: presetValues([200, 400, 800, 1600, 3200], config.toolOutputMaxLines),
|
|
268
403
|
},
|
|
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
404
|
];
|
|
277
405
|
this.settingsList = new SettingsList(items, Math.min(items.length, 10), getSettingsListTheme(), (id, newValue) => {
|
|
278
406
|
switch (id) {
|
|
407
|
+
case "tool-output-display":
|
|
408
|
+
callbacks.onToolOutputDisplayChange(newValue);
|
|
409
|
+
break;
|
|
279
410
|
case "output-max-bytes": {
|
|
280
411
|
const preset = TOOL_OUTPUT_BYTE_PRESETS.find(([label]) => label === newValue);
|
|
281
412
|
// A hand-set cap has no preset entry; its label is "<n> KB" by
|
|
@@ -288,9 +419,6 @@ class ToolSettingsSubmenu extends Container {
|
|
|
288
419
|
case "output-max-lines":
|
|
289
420
|
callbacks.onToolOutputMaxLinesChange(parseInt(newValue, 10));
|
|
290
421
|
break;
|
|
291
|
-
case "context-gc":
|
|
292
|
-
callbacks.onContextGcChange(newValue === "true");
|
|
293
|
-
break;
|
|
294
422
|
}
|
|
295
423
|
}, onCancel);
|
|
296
424
|
this.addChild(this.settingsList);
|
|
@@ -420,13 +548,41 @@ class SelectSubmenu extends Container {
|
|
|
420
548
|
this.selectList.handleInput(data);
|
|
421
549
|
}
|
|
422
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Token counts, grouped and exact.
|
|
553
|
+
*
|
|
554
|
+
* `formatTokens` (2.7k) exists for the fixed-width chrome, where a count must
|
|
555
|
+
* never grow the box it sits in. This pane is the opposite case: the point of
|
|
556
|
+
* showing a number here is to compare it with another one, and "2.7k" hides the
|
|
557
|
+
* difference between the tool that costs 2,710 and the one that costs 2,749.
|
|
558
|
+
*/
|
|
559
|
+
function tokenCount(tokens) {
|
|
560
|
+
return tokens.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* The fixed per-turn cost, as one line under the pane.
|
|
564
|
+
*
|
|
565
|
+
* Every byte of system prompt and active tool schema is re-sent on every
|
|
566
|
+
* request, and the pane is where that number is decided - so the pane is where
|
|
567
|
+
* it should be visible. It reports the *live* session: a tool toggle applies at
|
|
568
|
+
* once and moves it, while a setting that only takes effect next session (a tool
|
|
569
|
+
* group, the light preset) leaves it alone until then.
|
|
570
|
+
*/
|
|
571
|
+
function formatSurfaceLine(surface) {
|
|
572
|
+
const tools = `${surface.tools.length} tool${surface.tools.length === 1 ? "" : "s"}`;
|
|
573
|
+
return (` Per-turn surface: ${tokenCount(surface.totalTokens)} tokens ` +
|
|
574
|
+
`${theme.fg("dim", `(${tokenCount(surface.systemPromptTokens)} system prompt + ${tokenCount(surface.toolSchemaTokens)} schemas, ${tools})`)}`);
|
|
575
|
+
}
|
|
423
576
|
/**
|
|
424
577
|
* Main settings selector component.
|
|
425
578
|
*/
|
|
426
579
|
export class SettingsSelectorComponent extends Container {
|
|
427
580
|
settingsList;
|
|
581
|
+
surfaceLine;
|
|
582
|
+
measureTokenSurface;
|
|
428
583
|
constructor(config, callbacks) {
|
|
429
584
|
super();
|
|
585
|
+
this.measureTokenSurface = config.measureTokenSurface;
|
|
430
586
|
const supportsImages = getCapabilities().images;
|
|
431
587
|
const followUpKey = keyDisplayText("app.message.followUp");
|
|
432
588
|
let currentWarnings = { ...config.warnings };
|
|
@@ -437,6 +593,10 @@ export class SettingsSelectorComponent extends Container {
|
|
|
437
593
|
const pinnedNote = (key) => projectPinned.has(key)
|
|
438
594
|
? ` This repo's .hoocode/settings.json sets ${key}, which overrides this row from the next session on.`
|
|
439
595
|
: "";
|
|
596
|
+
// Which rows are gated on an external binary that is not on this machine.
|
|
597
|
+
// Built once so every row that needs it reads the same answer.
|
|
598
|
+
const rowGates = buildRowGates(config.externalTools);
|
|
599
|
+
const initialSurface = config.measureTokenSurface?.();
|
|
440
600
|
const toolsOn = config.tools.filter((t) => t.enabled).length;
|
|
441
601
|
const toolsOff = config.tools.length - toolsOn;
|
|
442
602
|
const items = [
|
|
@@ -591,8 +751,26 @@ export class SettingsSelectorComponent extends Container {
|
|
|
591
751
|
currentValue: config.blockImages ? "true" : "false",
|
|
592
752
|
values: ["true", "false"],
|
|
593
753
|
});
|
|
594
|
-
//
|
|
595
|
-
|
|
754
|
+
// Context GC (insert after block-images), a leaf the Context category picks up.
|
|
755
|
+
items.splice(items.findIndex((item) => item.id === "block-images") + 1, 0, {
|
|
756
|
+
id: "context-gc",
|
|
757
|
+
label: "Context GC",
|
|
758
|
+
description: "Stub superseded read results (files later edited or re-read) out of the outgoing context.",
|
|
759
|
+
currentValue: config.contextGc ? "true" : "false",
|
|
760
|
+
values: ["true", "false"],
|
|
761
|
+
});
|
|
762
|
+
// The light preset (insert after context GC). Read at startup to pick the
|
|
763
|
+
// tool set and the system prompt, so it lands on the next session.
|
|
764
|
+
const blockImagesIdx = items.findIndex((item) => item.id === "context-gc");
|
|
765
|
+
items.splice(blockImagesIdx + 1, 0, {
|
|
766
|
+
id: "light",
|
|
767
|
+
label: "Light preset",
|
|
768
|
+
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.",
|
|
769
|
+
currentValue: config.light ? "true" : "false",
|
|
770
|
+
values: ["true", "false"],
|
|
771
|
+
});
|
|
772
|
+
// Skill commands toggle (insert after the light preset)
|
|
773
|
+
const blockImagesIndex = items.findIndex((item) => item.id === "light");
|
|
596
774
|
items.splice(blockImagesIndex + 1, 0, {
|
|
597
775
|
id: "skill-commands",
|
|
598
776
|
label: "Skill commands",
|
|
@@ -694,8 +872,9 @@ export class SettingsSelectorComponent extends Container {
|
|
|
694
872
|
items.splice(terminalProgressIndex + 1, 0, {
|
|
695
873
|
id: "voice-silence-ms",
|
|
696
874
|
label: "Voice silence window",
|
|
697
|
-
description:
|
|
875
|
+
description: `Trailing-silence (ms) before voice capture auto-stops (300-10000). Env: VOICETOOLS_SILENCE_MS.${gateNote(rowGates.get("voice-silence-ms"))}`,
|
|
698
876
|
currentValue: String(config.voiceSilenceMs),
|
|
877
|
+
valueSuffix: gateSuffix(rowGates.get("voice-silence-ms")),
|
|
699
878
|
values: presetValues([300, 500, 800, 1200, 2000, 3000, 5000, 8000, 10000], config.voiceSilenceMs),
|
|
700
879
|
});
|
|
701
880
|
// Webtools request timeout (insert after voice-silence-ms)
|
|
@@ -703,8 +882,9 @@ export class SettingsSelectorComponent extends Container {
|
|
|
703
882
|
items.splice(voiceSilenceIndex + 1, 0, {
|
|
704
883
|
id: "webtools-timeout-secs",
|
|
705
884
|
label: "Web tools timeout",
|
|
706
|
-
description:
|
|
885
|
+
description: `Per-request timeout (secs) for webfetch/websearch (1-120). Env: HOOCODE_WEBTOOLS_TIMEOUT.${gateNote(rowGates.get("webtools-timeout-secs"))}`,
|
|
707
886
|
currentValue: String(config.webtoolsTimeoutSecs),
|
|
887
|
+
valueSuffix: gateSuffix(rowGates.get("webtools-timeout-secs")),
|
|
708
888
|
values: presetValues([5, 10, 15, 30, 60, 120], config.webtoolsTimeoutSecs),
|
|
709
889
|
});
|
|
710
890
|
// The /learn thresholds, appended as leaf rows and gathered into their own
|
|
@@ -724,30 +904,28 @@ export class SettingsSelectorComponent extends Container {
|
|
|
724
904
|
{
|
|
725
905
|
id: "tools",
|
|
726
906
|
label: "Tools",
|
|
727
|
-
description: "Enable/disable tools and tool groups (web, semantic search). Changes persist across sessions.",
|
|
907
|
+
description: "Enable/disable tools and tool groups (web, semantic search), each priced by what its schema costs per turn. Changes persist across sessions.",
|
|
728
908
|
currentValue: toolsOff > 0 ? `${toolsOn} on · ${toolsOff} off` : `${toolsOn} on`,
|
|
729
|
-
|
|
909
|
+
valueSuffix: initialSurface ? `${tokenCount(initialSurface.toolSchemaTokens)} tok/turn` : undefined,
|
|
910
|
+
submenu: (_currentValue, done) => new ToolsSubmenu(config.tools, config.toolGroups, rowGates, (name, enabled) => {
|
|
911
|
+
callbacks.onToolEnabledChange(name, enabled);
|
|
912
|
+
// Applied live by the host, so the surface below is already stale.
|
|
913
|
+
this.refreshTokenSurface();
|
|
914
|
+
}, (id, enabled) => callbacks.onToolGroupChange(id, enabled), () => done()),
|
|
730
915
|
},
|
|
731
916
|
{
|
|
732
|
-
id: "tool-output
|
|
733
|
-
label: "Tool output
|
|
734
|
-
description: "How
|
|
917
|
+
id: "tool-output",
|
|
918
|
+
label: "Tool output",
|
|
919
|
+
description: "How much of a tool result is rendered, and where it is truncated.",
|
|
735
920
|
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
921
|
submenu: (_currentValue, done) => new ToolSettingsSubmenu({
|
|
922
|
+
toolOutputDisplay: config.toolOutputDisplay,
|
|
744
923
|
toolOutputMaxBytes: config.toolOutputMaxBytes,
|
|
745
924
|
toolOutputMaxLines: config.toolOutputMaxLines,
|
|
746
|
-
contextGc: config.contextGc,
|
|
747
925
|
}, {
|
|
926
|
+
onToolOutputDisplayChange: callbacks.onToolOutputDisplayChange,
|
|
748
927
|
onToolOutputMaxBytesChange: callbacks.onToolOutputMaxBytesChange,
|
|
749
928
|
onToolOutputMaxLinesChange: callbacks.onToolOutputMaxLinesChange,
|
|
750
|
-
onContextGcChange: callbacks.onContextGcChange,
|
|
751
929
|
}, () => done()),
|
|
752
930
|
},
|
|
753
931
|
];
|
|
@@ -769,9 +947,6 @@ export class SettingsSelectorComponent extends Container {
|
|
|
769
947
|
case "autocompact":
|
|
770
948
|
callbacks.onAutoCompactChange(newValue === "true");
|
|
771
949
|
break;
|
|
772
|
-
case "tool-output-display":
|
|
773
|
-
callbacks.onToolOutputDisplayChange(newValue);
|
|
774
|
-
break;
|
|
775
950
|
case "show-images":
|
|
776
951
|
callbacks.onShowImagesChange(newValue === "true");
|
|
777
952
|
break;
|
|
@@ -787,6 +962,9 @@ export class SettingsSelectorComponent extends Container {
|
|
|
787
962
|
case "skill-commands":
|
|
788
963
|
callbacks.onEnableSkillCommandsChange(newValue === "true");
|
|
789
964
|
break;
|
|
965
|
+
case "light":
|
|
966
|
+
callbacks.onLightChange(newValue === "true");
|
|
967
|
+
break;
|
|
790
968
|
case "plugin-tools":
|
|
791
969
|
callbacks.onEnablePluginToolsChange(newValue === "true");
|
|
792
970
|
break;
|
|
@@ -855,6 +1033,10 @@ export class SettingsSelectorComponent extends Container {
|
|
|
855
1033
|
callbacks.onLearnSettingChange(id, parseInt(newValue, 10));
|
|
856
1034
|
break;
|
|
857
1035
|
}
|
|
1036
|
+
// Most rows leave the per-turn surface alone; the ones that do not (a tool
|
|
1037
|
+
// toggle, anything that rebuilds the system prompt) move it immediately.
|
|
1038
|
+
// Re-measuring after every change is cheaper than knowing which is which.
|
|
1039
|
+
this.refreshTokenSurface();
|
|
858
1040
|
};
|
|
859
1041
|
// Partition the flat leaf settings into named category submenus so the
|
|
860
1042
|
// top level stays short. `items` holds autocompact + every leaf setting.
|
|
@@ -876,8 +1058,12 @@ export class SettingsSelectorComponent extends Container {
|
|
|
876
1058
|
};
|
|
877
1059
|
};
|
|
878
1060
|
const topItems = [
|
|
879
|
-
...(byId.has("autocompact") ? [byId.get("autocompact")] : []),
|
|
880
1061
|
...toolFlagGroup,
|
|
1062
|
+
// What the model is sent, and how much of it. Auto-compact used to sit
|
|
1063
|
+
// alone at the top level and context GC was filed under tool settings,
|
|
1064
|
+
// which left the three settings that decide the token budget in three
|
|
1065
|
+
// different places - with the light preset in none of them.
|
|
1066
|
+
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
1067
|
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
1068
|
categoryRow("cat-interface", "Interface", "Appearance and editor: theme, thinking visibility, cursor, border, padding, autocomplete, terminal.", [
|
|
883
1069
|
"theme",
|
|
@@ -902,6 +1088,21 @@ export class SettingsSelectorComponent extends Container {
|
|
|
902
1088
|
// that decide whether /learn finds anything, and burying them is what
|
|
903
1089
|
// made them undiscoverable in the first place.
|
|
904
1090
|
categoryRow("cat-learn", "Learning", "Thresholds /learn mines sessions with: how far back to look, and how often something must repeat.", LEARN_SETTINGS.map((setting) => setting.key)),
|
|
1091
|
+
// The external binaries. Top level, not folded into Advanced: their whole
|
|
1092
|
+
// problem was that nothing in the product ever said they existed, and a
|
|
1093
|
+
// row two levels down would have been the same silence with extra steps.
|
|
1094
|
+
// This is the only category built from live machine state rather than
|
|
1095
|
+
// settings, so it is assembled here instead of going through categoryRow.
|
|
1096
|
+
{
|
|
1097
|
+
id: "cat-external",
|
|
1098
|
+
label: "External tools",
|
|
1099
|
+
description: `Optional Rust binaries that expand what hoocode can do: ${externalSummary(config.externalTools)}. hoocode runs without every one of them - each row says what it adds and what happens without it.`,
|
|
1100
|
+
currentValue: externalCount(config.externalTools),
|
|
1101
|
+
keywords: config.externalTools
|
|
1102
|
+
.flatMap((status) => [status.tool, status.label, ...status.settingsKeys])
|
|
1103
|
+
.join(" "),
|
|
1104
|
+
submenu: (_currentValue, done) => new ExternalToolsSubmenu(config.externalTools, () => done()),
|
|
1105
|
+
},
|
|
905
1106
|
categoryRow("cat-advanced", "Advanced", "Startup, telemetry, skills, warnings, voice, and web tools.", [
|
|
906
1107
|
"quiet-startup",
|
|
907
1108
|
"collapse-changelog",
|
|
@@ -916,8 +1117,18 @@ export class SettingsSelectorComponent extends Container {
|
|
|
916
1117
|
enableSearch: true,
|
|
917
1118
|
});
|
|
918
1119
|
this.addChild(this.settingsList);
|
|
1120
|
+
if (initialSurface) {
|
|
1121
|
+
this.surfaceLine = new Text(formatSurfaceLine(initialSurface), 0, 0);
|
|
1122
|
+
this.addChild(this.surfaceLine);
|
|
1123
|
+
}
|
|
919
1124
|
this.addChild(new DynamicBorder());
|
|
920
1125
|
}
|
|
1126
|
+
/** Re-price the pane after a change that may have altered what each turn sends. */
|
|
1127
|
+
refreshTokenSurface() {
|
|
1128
|
+
if (!this.surfaceLine || !this.measureTokenSurface)
|
|
1129
|
+
return;
|
|
1130
|
+
this.surfaceLine.setText(formatSurfaceLine(this.measureTokenSurface()));
|
|
1131
|
+
}
|
|
921
1132
|
getSettingsList() {
|
|
922
1133
|
return this.settingsList;
|
|
923
1134
|
}
|