@north-light/crouter 0.3.159 → 0.3.160
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/dist/api/dto/nodes.d.ts +3 -0
- package/dist/builtin-memory/internal/INDEX.md +1 -0
- package/dist/builtin-memory/internal/workflow-codification.md +82 -0
- package/dist/clients/attach/__tests__/bash-call-width.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/bash-call-width.test.js +38 -0
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +20 -0
- package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +3 -1
- package/dist/clients/attach/chrome/bash-jobs.d.ts +5 -3
- package/dist/clients/attach/chrome/bash-jobs.js +15 -8
- package/dist/clients/attach/chrome/canvas-panels.js +10 -3
- package/dist/clients/attach/chrome/roster.d.ts +7 -2
- package/dist/clients/attach/chrome/roster.js +87 -3
- package/dist/clients/attach/input/controller.d.ts +3 -0
- package/dist/clients/attach/input/controller.js +1 -0
- package/dist/clients/attach/render/assistant-message.d.ts +13 -0
- package/dist/clients/attach/render/assistant-message.js +55 -0
- package/dist/clients/attach/render/chat-view.d.ts +29 -0
- package/dist/clients/attach/render/chat-view.js +233 -12
- package/dist/clients/attach/render/context-message.js +1 -1
- package/dist/clients/attach/render/edit-diff.js +20 -1
- package/dist/clients/attach/render/tool-calls.d.ts +32 -0
- package/dist/clients/attach/render/tool-calls.js +251 -0
- package/dist/clients/attach/session/input-wiring.d.ts +3 -0
- package/dist/clients/attach/session/input-wiring.js +3 -0
- package/dist/clients/attach/slash/dispatch.d.ts +4 -0
- package/dist/clients/attach/slash/dispatch.js +8 -0
- package/dist/clients/attach/viewer.js +512 -508
- package/dist/commands/cron.js +2 -2
- package/dist/commands/node.js +3 -3
- package/dist/commands/search.js +1 -1
- package/dist/commands/sys/config.js +10 -2
- package/dist/commands/sys/doctor.js +22 -1
- package/dist/commands/sys/setup-core.d.ts +5 -1
- package/dist/commands/sys/setup-core.js +16 -2
- package/dist/commands/sys/setup-wizard.d.ts +11 -0
- package/dist/commands/sys/setup-wizard.js +111 -6
- package/dist/core/__tests__/tmux-surface.test.js +5 -2
- package/dist/core/canvas/canvas.js +1 -1
- package/dist/core/canvas/labels.d.ts +1 -0
- package/dist/core/canvas/labels.js +3 -1
- package/dist/core/canvas/nav-render.d.ts +6 -0
- package/dist/core/canvas/nav-render.js +8 -0
- package/dist/core/canvas/types.d.ts +6 -0
- package/dist/core/config.js +3 -2
- package/dist/core/keybindings/__tests__/resolve.test.js +1 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +2 -0
- package/dist/core/preview-registry.d.ts +5 -0
- package/dist/core/preview-registry.js +10 -0
- package/dist/core/runtime/canvas-extensions.d.ts +4 -0
- package/dist/core/runtime/canvas-extensions.js +4 -0
- package/dist/core/runtime/naming-persist.d.ts +4 -3
- package/dist/core/runtime/naming-persist.js +10 -9
- package/dist/core/runtime/naming.d.ts +28 -8
- package/dist/core/runtime/naming.js +168 -32
- package/dist/core/runtime/nerd-font.d.ts +28 -0
- package/dist/core/runtime/nerd-font.js +127 -0
- package/dist/core/runtime/tmux.js +16 -0
- package/dist/daemon/api/map.js +2 -0
- package/dist/pi-extensions/naming-tool.d.ts +24 -0
- package/dist/pi-extensions/naming-tool.js +67 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.js +1 -0
- package/dist/web-client/assets/index-B76ZKfT_.js +79 -0
- package/dist/web-client/assets/{index-CpEl9LTS.css → index-CqLKj8Xu.css} +1 -1
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/web-client/assets/index-CsuwzlcQ.js +0 -79
package/dist/commands/cron.js
CHANGED
|
@@ -138,7 +138,7 @@ const addLeaf = defineLeaf({
|
|
|
138
138
|
{ kind: 'flag', name: 'tz', type: 'string', required: false, constraint: 'IANA zone (e.g. "America/New_York") for a bare-ISO --at or a calendar --every. Defaults to the host zone.' },
|
|
139
139
|
{ kind: 'flag', name: 'on-output', type: 'enum', choices: ['silent', 'on-failure', 'always', 'on-change'], required: false, default: 'on-failure', constraint: 'What each run does with its output. silent: record in the run log, tell nobody — never escalate. on-failure (default): no success delivery; a failing run pauses the cron and spawns a node to deal with it. always: deliver stdout every run to --sink at --tier. on-change: deliver stdout only when it differs from the previous run\'s — the shape polling wants.' },
|
|
140
140
|
{ kind: 'flag', name: 'sink', type: 'string', required: false, constraint: 'Where always/on-change stdout deliveries go: "node:<id>" (an existing node\'s inbox — must exist now; gone/finalized at fire time is a failure), "spawn:<kind>" (each delivered output creates a self-finishing node with stdout as kickoff — a managed child while its creator exists, otherwise a terminal parentless root), or "human" (the humanloop inbox). Required by always/on-change, rejected otherwise.' },
|
|
141
|
-
{ kind: 'flag', name: 'tier', type: 'enum', choices: ['normal', 'urgent', 'critical'], required: false, default: '
|
|
141
|
+
{ kind: 'flag', name: 'tier', type: 'enum', choices: ['normal', 'urgent', 'critical'], required: false, default: 'urgent', constraint: 'Inbox priority of a node-sink delivery. urgent (default): steers the target mid-turn, or wakes it when dormant — a fire is news the node asked for on a schedule, so it should land now. normal: waits for the running turn to settle. critical: aborts the running turn outright.' },
|
|
142
142
|
{ kind: 'flag', name: 'expires', type: 'string', required: false, constraint: 'Clock bound (same grammar as --at): the row is deleted once this instant passes. The self-limiting half of a poll-until pattern.' },
|
|
143
143
|
{ kind: 'flag', name: 'anchor', type: 'string', required: false, constraint: 'Couple the cron\'s lifetime to a node: the row is deleted when that node is deleted. Without an anchor it outlives every node; it ends through its one-shot run, --expires, explicit cancellation, or self-cancellation from its bash.' },
|
|
144
144
|
{ kind: 'flag', name: 'anchor-self', type: 'bool', required: false, constraint: 'Anchor to the calling node ($CRTR_NODE_ID). Errors outside a node.' },
|
|
@@ -259,7 +259,7 @@ const addLeaf = defineLeaf({
|
|
|
259
259
|
scope,
|
|
260
260
|
on_output: input['onOutput'] ?? 'on-failure',
|
|
261
261
|
sink: typeof input['sink'] === 'string' && input['sink'] !== '' ? input['sink'] : null,
|
|
262
|
-
tier: input['tier'] ?? '
|
|
262
|
+
tier: input['tier'] ?? 'urgent',
|
|
263
263
|
expires_at: expiresAt,
|
|
264
264
|
anchor_node: anchorNode,
|
|
265
265
|
cancel_on_wake: cancelOnWake,
|
package/dist/commands/node.js
CHANGED
|
@@ -912,7 +912,7 @@ function stopJobGroup(pgid) {
|
|
|
912
912
|
const nodeBashKill = defineLeaf({
|
|
913
913
|
name: 'kill',
|
|
914
914
|
description: 'stop one of a node’s background bash jobs',
|
|
915
|
-
whenToUse: 'a human watching the Inspector wants a long-running background job to stop now, or an agent is abandoning work it backgrounded',
|
|
915
|
+
whenToUse: 'a human watching the Inspector or the attach cockpit’s bash row wants a long-running background job to stop now, or an agent is abandoning work it backgrounded',
|
|
916
916
|
tier: 'hidden',
|
|
917
917
|
help: {
|
|
918
918
|
name: 'node bash kill',
|
|
@@ -930,7 +930,7 @@ const nodeBashKill = defineLeaf({
|
|
|
930
930
|
outputKind: 'object',
|
|
931
931
|
effects: [
|
|
932
932
|
'Sends SIGTERM (then SIGKILL) to the job’s whole process group, stopping the command and anything it forked.',
|
|
933
|
-
'Records the job’s exit so it stops showing as live, and sends the owning node an urgent inbox message saying the job was
|
|
933
|
+
'Records the job’s exit so it stops showing as live, and sends the owning node an urgent inbox message saying the job was stopped — which steers that node mid-turn.',
|
|
934
934
|
],
|
|
935
935
|
},
|
|
936
936
|
run: async (input) => {
|
|
@@ -973,7 +973,7 @@ const nodeBashKill = defineLeaf({
|
|
|
973
973
|
if (!existsSync(paths.jobExit))
|
|
974
974
|
writeFileSync(paths.jobExit, '143\n');
|
|
975
975
|
await cliClient().sendMessage(node.node_id, {
|
|
976
|
-
body: `Background bash job ${jobId} was
|
|
976
|
+
body: `Background bash job ${jobId} was stopped by the human before it finished. Log: ${paths.jobLog} Command: ${paths.cmdSh}`,
|
|
977
977
|
tier: 'urgent',
|
|
978
978
|
});
|
|
979
979
|
return { node_id: node.node_id, job_id: jobId, signaled };
|
package/dist/commands/search.js
CHANGED
|
@@ -12,7 +12,7 @@ export function registerSearch() {
|
|
|
12
12
|
rootEntry: {
|
|
13
13
|
concept: 'web search for agents — find pages, get grounded answers, extract page content (Exa)',
|
|
14
14
|
desc: 'search the web, answer a question with citations, or extract content from known URLs',
|
|
15
|
-
useWhen: 'you need information from the live web — current events, documentation, sources, facts beyond your training. Use web for relevant pages with excerpts, answer for one synthesized cited answer, and contents for clean text from URLs you already hold. Needs an Exa API key (EXA_API_KEY or ~/.crouter/exa.key).',
|
|
15
|
+
useWhen: 'you need information from the live web — current events, documentation, sources, facts beyond your training. Use web for relevant pages with excerpts, answer for one synthesized cited answer, and contents for clean text from URLs you already hold. This reaches PUBLIC pages read-only: for anything behind the user’s login, or any action inside a web app, drive their own browser with `crtr device`, and to call a service’s API server-side use `crtr connector`. Needs an Exa API key (EXA_API_KEY or ~/.crouter/exa.key).',
|
|
16
16
|
},
|
|
17
17
|
help: {
|
|
18
18
|
name: 'search',
|
|
@@ -16,6 +16,7 @@ const TOP_LEVEL_KEYS = new Set([
|
|
|
16
16
|
'completion_bell',
|
|
17
17
|
'live_cycles',
|
|
18
18
|
'condensed_history',
|
|
19
|
+
'fold_finished_tools',
|
|
19
20
|
'modelLadders',
|
|
20
21
|
]);
|
|
21
22
|
const MODEL_PROVIDERS = new Set(['anthropic', 'openai']);
|
|
@@ -153,6 +154,13 @@ function setNestedValue(cfg, key, rawValue, value) {
|
|
|
153
154
|
cfg.condensed_history = rawValue;
|
|
154
155
|
return;
|
|
155
156
|
}
|
|
157
|
+
if (key === 'fold_finished_tools') {
|
|
158
|
+
if (typeof value !== 'boolean') {
|
|
159
|
+
throw usage(`fold_finished_tools must be true or false`);
|
|
160
|
+
}
|
|
161
|
+
cfg.fold_finished_tools = value;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
156
164
|
if (parts.length === 1) {
|
|
157
165
|
cfg[topKey] = value;
|
|
158
166
|
return;
|
|
@@ -175,7 +183,7 @@ const configGet = defineLeaf({
|
|
|
175
183
|
name: 'sys config get',
|
|
176
184
|
summary: 'read a config value by dotted key',
|
|
177
185
|
params: [
|
|
178
|
-
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: 'Dotted key path. Top-level keys: auto_update, marketplaces, plugins, max_panes_per_window, brokerThresholds (user only; warning and automaticReviveCap), completion_bell, live_cycles, condensed_history, modelLadders, or the whole keybindings object. Keybindings are edited only through sys setup.' },
|
|
186
|
+
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: 'Dotted key path. Top-level keys: auto_update, marketplaces, plugins, max_panes_per_window, brokerThresholds (user only; warning and automaticReviveCap), completion_bell, live_cycles, condensed_history, fold_finished_tools, modelLadders, or the whole keybindings object. Keybindings are edited only through sys setup.' },
|
|
179
187
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Scope to read from. Default: user.' },
|
|
180
188
|
],
|
|
181
189
|
output: [
|
|
@@ -209,7 +217,7 @@ const configSet = defineLeaf({
|
|
|
209
217
|
name: 'sys config set',
|
|
210
218
|
summary: 'write a config value by dotted key',
|
|
211
219
|
params: [
|
|
212
|
-
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: 'Dotted key path. Supported: auto_update.crtr, auto_update.content, auto_update.interval_hours, max_panes_per_window, brokerThresholds.warning, brokerThresholds.automaticReviveCap (user scope; positive integers), completion_bell, live_cycles (integer >= 1 — trailing cycles the attach viewer renders at full fidelity; older cycles condense to prose), condensed_history (none|user|agent|both — which messages of a condensed cycle still render; none keeps only the cycle dividers), modelLadders.defaultProvider, modelLadders.<anthropic|openai>.<ultra|strong|medium|light>. Keybindings are edited only through sys setup.' },
|
|
220
|
+
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: 'Dotted key path. Supported: auto_update.crtr, auto_update.content, auto_update.interval_hours, max_panes_per_window, brokerThresholds.warning, brokerThresholds.automaticReviveCap (user scope; positive integers), completion_bell, live_cycles (integer >= 1 — trailing cycles the attach viewer renders at full fidelity; older cycles condense to prose), condensed_history (none|user|agent|both — which messages of a condensed cycle still render; none keeps only the cycle dividers), fold_finished_tools (true|false — fold every settled tool call in the attach viewer to its single call line), modelLadders.defaultProvider, modelLadders.<anthropic|openai>.<ultra|strong|medium|light>. Keybindings are edited only through sys setup.' },
|
|
213
221
|
{ kind: 'flag', name: 'value', type: 'string', required: true, constraint: 'value VALUE — string, required. Stored as-is if quoted; coerced to number or boolean when unambiguous.' },
|
|
214
222
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Scope to write to. Default: user.' },
|
|
215
223
|
],
|
|
@@ -10,6 +10,8 @@ import { lsRemote } from '../../core/git.js';
|
|
|
10
10
|
import { isPidAlive } from '../../core/canvas/pid.js';
|
|
11
11
|
import { cliClient } from '../api-client.js';
|
|
12
12
|
import { readFault } from '../../core/runtime/fault.js';
|
|
13
|
+
import { detectNerdFont, nerdFontInstallCommand, NERD_FONT_SELECT_HINT } from '../../core/runtime/nerd-font.js';
|
|
14
|
+
import { detectPackageManager } from './setup-core.js';
|
|
13
15
|
import { validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
|
|
14
16
|
import { buildExternalCommandSnapshot } from '../../core/configured-clis/discovery.js';
|
|
15
17
|
import { effectiveConfiguredClis } from '../../core/configured-clis/registration.js';
|
|
@@ -277,6 +279,23 @@ async function verifySealed(root) {
|
|
|
277
279
|
}
|
|
278
280
|
await walk(root);
|
|
279
281
|
}
|
|
282
|
+
/** The viewer draws every tool and command icon with Nerd Font codepoints, so a
|
|
283
|
+
* machine with no Nerd Font installed renders tofu boxes. Nothing else in
|
|
284
|
+
* crtr notices — this is the one place a fresh install learns why. Never
|
|
285
|
+
* auto-fixed: installing a font, and pointing the terminal profile at it, is a
|
|
286
|
+
* human action. */
|
|
287
|
+
function runNerdFontCheck() {
|
|
288
|
+
const detection = detectNerdFont();
|
|
289
|
+
if (detection.installed) {
|
|
290
|
+
return pass('user', 'terminal:nerd-font', `installed (${detection.evidence}) — if icons still render as boxes, ${NERD_FONT_SELECT_HINT}`);
|
|
291
|
+
}
|
|
292
|
+
const plan = nerdFontInstallCommand(detectPackageManager());
|
|
293
|
+
return failCheck('user', 'terminal:nerd-font', 'no Nerd Font installed — viewer tool/command icons will render as tofu boxes', {
|
|
294
|
+
kind: 'human_action',
|
|
295
|
+
description: `Install a Nerd Font (${plan.hint}), ${NERD_FONT_SELECT_HINT}`,
|
|
296
|
+
...(plan.command ? { command: plan.command } : {}),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
280
299
|
async function generationChecks(label, lookup, allowAbsent) {
|
|
281
300
|
if (lookup.generation === null) {
|
|
282
301
|
const message = lookup.error ?? 'no runtime generation';
|
|
@@ -486,7 +505,7 @@ export const sysDoctorLeaf = defineLeaf({
|
|
|
486
505
|
{ kind: 'flag', name: 'remote', type: 'bool', required: false, constraint: 'Check git remotes with ls-remote (slow — makes network calls).' },
|
|
487
506
|
],
|
|
488
507
|
output: [
|
|
489
|
-
{ name: 'checks', type: 'object[]', required: true, constraint: 'Each: {scope, name, status, message, fixed?, remediation?}. status: pass | fail. scope may be user | project | canvas. Runtime checks validate selected and process generation manifests, sealing/containment, and full SHA-256 inventories. Command-plugin manifest checks appear as plugin:<name>:commands (validated statically, never executed); their remediation is always a human_action (disable/update/remove the plugin). remediation (when present) is {kind, description, ...payload} where kind is remove_config_key | rm_path | human_action; fault human_action remediations include the canonical logs command. Sorted by scope then name.' },
|
|
508
|
+
{ name: 'checks', type: 'object[]', required: true, constraint: 'Each: {scope, name, status, message, fixed?, remediation?}. status: pass | fail. scope may be user | project | canvas. Runtime checks validate selected and process generation manifests, sealing/containment, and full SHA-256 inventories. terminal:nerd-font (user scope) reports whether a Nerd Font is installed for the viewer\'s icon glyphs; its remediation is always a human_action carrying the platform install command, never auto-fixed. Command-plugin manifest checks appear as plugin:<name>:commands (validated statically, never executed); their remediation is always a human_action (disable/update/remove the plugin). remediation (when present) is {kind, description, ...payload} where kind is remove_config_key | rm_path | human_action; fault human_action remediations include the canonical logs command. Sorted by scope then name.' },
|
|
490
509
|
{ name: 'ok', type: 'boolean', required: true, constraint: 'True when no unresolved fail checks remain.' },
|
|
491
510
|
],
|
|
492
511
|
outputKind: 'object',
|
|
@@ -506,6 +525,8 @@ export const sysDoctorLeaf = defineLeaf({
|
|
|
506
525
|
const results = runChecksForScope(scope, { fix, remote });
|
|
507
526
|
allResults.push(...results);
|
|
508
527
|
}
|
|
528
|
+
if (scopes.includes('user'))
|
|
529
|
+
allResults.push(runNerdFontCheck());
|
|
509
530
|
allResults.push(...runCommandPluginChecks(scopes));
|
|
510
531
|
allResults.push(...runConfiguredCliChecks(scopes));
|
|
511
532
|
allResults.push(...await runFaultChecks());
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type BindingId, type BindingResolution, type KeybindingDiagnostic, type SparseKeybindingOverrides } from '../../core/keybindings/index.js';
|
|
2
2
|
export type SetupPackageId = 'crtr-extensions' | 'mode-switch' | 'oauth-adapter' | 'capture-plugin';
|
|
3
|
-
|
|
3
|
+
/** `nerd-font` is not a command on PATH like the other two — it is an installed
|
|
4
|
+
* font face, detected by fontconfig/font-directory scan. The viewer renders
|
|
5
|
+
* every tool and command icon with Nerd Font codepoints, so without it a fresh
|
|
6
|
+
* machine shows tofu boxes. */
|
|
7
|
+
export type SetupTool = 'tmux' | 'nvim' | 'nerd-font';
|
|
4
8
|
/** `marketplace-plugin` installs a crtr plugin from a registered marketplace
|
|
5
9
|
* (`installSource` is a `<marketplace>/<plugin>` ref) instead of running the
|
|
6
10
|
* bundled pi CLI. Offered in setup and NOT installed by first-run bootstrap:
|
|
@@ -9,6 +9,7 @@ import { builtinPiPackageDir } from '../../core/scope.js';
|
|
|
9
9
|
import { readJsonIfExists } from '../../core/fs-utils.js';
|
|
10
10
|
import { resolveBundledPiCliPath } from '../../core/runtime/pi-cli.js';
|
|
11
11
|
import { installTmuxBindings } from '../../core/runtime/tmux-chrome.js';
|
|
12
|
+
import { detectNerdFont, nerdFontInstallCommand, NERD_FONT_SELECT_HINT } from '../../core/runtime/nerd-font.js';
|
|
12
13
|
import { globalHumanloopHealth, globalHumanloopInstallCommand } from '../../core/runtime/package-health.js';
|
|
13
14
|
import { registerCrouterRoot } from '../human/shared.js';
|
|
14
15
|
import { installFromMarketplace } from '../pkg/plugin-manage.js';
|
|
@@ -301,6 +302,19 @@ export function buildSystemInstallPlan(tool, packageManager, installed) {
|
|
|
301
302
|
autoInstall: false,
|
|
302
303
|
};
|
|
303
304
|
}
|
|
305
|
+
if (tool === 'nerd-font') {
|
|
306
|
+
const plan = nerdFontInstallCommand(packageManager);
|
|
307
|
+
return {
|
|
308
|
+
tool,
|
|
309
|
+
label,
|
|
310
|
+
installed: false,
|
|
311
|
+
checked: plan.autoInstall,
|
|
312
|
+
packageManager,
|
|
313
|
+
command: plan.command,
|
|
314
|
+
hint: `${plan.hint} — ${NERD_FONT_SELECT_HINT}`,
|
|
315
|
+
autoInstall: plan.autoInstall,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
304
318
|
const packageName = tool === 'nvim' ? 'neovim' : 'tmux';
|
|
305
319
|
switch (packageManager) {
|
|
306
320
|
case 'brew':
|
|
@@ -361,7 +375,7 @@ export function buildSystemInstallPlan(tool, packageManager, installed) {
|
|
|
361
375
|
}
|
|
362
376
|
}
|
|
363
377
|
export function buildSystemToolState(tool, packageManager, commandExists = detectCommandOnPath) {
|
|
364
|
-
const installed = commandExists(tool);
|
|
378
|
+
const installed = tool === 'nerd-font' ? detectNerdFont().installed : commandExists(tool);
|
|
365
379
|
return {
|
|
366
380
|
tool,
|
|
367
381
|
installed,
|
|
@@ -397,7 +411,7 @@ export function buildSetupInitialState(opts = {}) {
|
|
|
397
411
|
installedPackageIds.add(id);
|
|
398
412
|
const commandExists = opts.commandExists ?? detectCommandOnPath;
|
|
399
413
|
const packageManager = opts.packageManager ?? detectPackageManager(process.platform, commandExists);
|
|
400
|
-
const systemTools = ['tmux', 'nvim'].map((tool) => buildSystemToolState(tool, packageManager, commandExists));
|
|
414
|
+
const systemTools = ['tmux', 'nvim', 'nerd-font'].map((tool) => buildSystemToolState(tool, packageManager, commandExists));
|
|
401
415
|
return { manifest, installedPackageIds, systemTools, exaKeyPath: resolveExaKeyPath(homeDir) };
|
|
402
416
|
}
|
|
403
417
|
export function buildSetupStaticInstructions(opts) {
|
|
@@ -62,6 +62,8 @@ export declare class SetupWizard implements Component, Focusable {
|
|
|
62
62
|
private keyNotice;
|
|
63
63
|
private brokerLimitIndex;
|
|
64
64
|
private brokerLimitNotice;
|
|
65
|
+
private displayIndex;
|
|
66
|
+
private displayNotice;
|
|
65
67
|
private cachedWidth;
|
|
66
68
|
private cachedHeight;
|
|
67
69
|
private cachedLines;
|
|
@@ -74,6 +76,14 @@ export declare class SetupWizard implements Component, Focusable {
|
|
|
74
76
|
invalidate(): void;
|
|
75
77
|
private setTab;
|
|
76
78
|
private moveTab;
|
|
79
|
+
private moveDisplayRow;
|
|
80
|
+
/** Advance the selected display setting to its next value and persist it to user
|
|
81
|
+
* config immediately — these are one-keystroke view preferences, so there is no
|
|
82
|
+
* separate save gesture to forget. A viewer picks the new value up on its next
|
|
83
|
+
* attach (`live_cycles` / `condensed_history` / `fold_finished_tools` are all read
|
|
84
|
+
* per ChatView), and `completion_bell` on the next turn end. */
|
|
85
|
+
private cycleDisplaySetting;
|
|
86
|
+
private handleDisplayInput;
|
|
77
87
|
private cancel;
|
|
78
88
|
dispose(): void;
|
|
79
89
|
private matches;
|
|
@@ -124,6 +134,7 @@ export declare class SetupWizard implements Component, Focusable {
|
|
|
124
134
|
private renderKeybindings;
|
|
125
135
|
private renderModels;
|
|
126
136
|
private renderBrokerLimits;
|
|
137
|
+
private renderDisplay;
|
|
127
138
|
render(width: number): string[];
|
|
128
139
|
}
|
|
129
140
|
export declare function runSetup(): Promise<void>;
|
|
@@ -13,6 +13,7 @@ import { removeProviderRow } from '../../core/provider-management.js';
|
|
|
13
13
|
import { readConfig, readRawConfigAtRoot, readRawScopeConfig, updateRawConfigAtomically, persistDefaultKindModel, persistModelLadders, readMergedLaunchConfig } from '../../core/config.js';
|
|
14
14
|
import { findProjectScopeRoots, userScopeRoot } from '../../core/scope.js';
|
|
15
15
|
import { loadProfileManifest, profileRoot } from '../../core/profiles/manifest.js';
|
|
16
|
+
import { CONDENSED_HISTORY_MODES } from '../../types.js';
|
|
16
17
|
import { BINDING_CATALOG, formatBinding, formatGesture, matchesPiTuiInput, persistUserKeybindings, resolveKeybindings, KeybindingConcurrentEditError, KeybindingValidationError, } from '../../core/keybindings/index.js';
|
|
17
18
|
import { configuredLadderProviders, inferPortableModelSelection } from '../../core/runtime/model-selection.js';
|
|
18
19
|
import { cliClient } from '../api-client.js';
|
|
@@ -79,7 +80,11 @@ function currentWidth() {
|
|
|
79
80
|
return Math.max(1, process.stdout.columns ?? 80);
|
|
80
81
|
}
|
|
81
82
|
function toolLabel(tool) {
|
|
82
|
-
|
|
83
|
+
if (tool === 'nvim')
|
|
84
|
+
return 'nvim';
|
|
85
|
+
if (tool === 'nerd-font')
|
|
86
|
+
return 'Nerd Font';
|
|
87
|
+
return 'tmux';
|
|
83
88
|
}
|
|
84
89
|
function positiveInteger(value) {
|
|
85
90
|
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1)
|
|
@@ -652,6 +657,45 @@ class ProviderPanel {
|
|
|
652
657
|
return useFrame ? framed(body, width) : body.map((line) => truncateToWidth(line, width));
|
|
653
658
|
}
|
|
654
659
|
}
|
|
660
|
+
const TAB_LABELS = ['Providers', 'Plugins', 'System', 'Exa', 'Keybindings', 'Models', 'Broker limits', 'Display'];
|
|
661
|
+
const DISPLAY_TAB = 7;
|
|
662
|
+
/** The viewer display settings the Display tab edits, in row order. Each row owns
|
|
663
|
+
* its own value cycling and its own formatting, so adding a setting here is the
|
|
664
|
+
* whole change — the tab's input and render loops stay generic. Every one is a
|
|
665
|
+
* user-scope `ScopeConfig` key persisted the moment it changes. */
|
|
666
|
+
const DISPLAY_ROWS = [
|
|
667
|
+
{
|
|
668
|
+
key: 'fold_finished_tools',
|
|
669
|
+
label: 'Fold finished tools',
|
|
670
|
+
description: 'Fold every settled tool call to its call line (path included). Toggle live with /fold-tools.',
|
|
671
|
+
format: (cfg) => (cfg.fold_finished_tools ? 'on' : 'off'),
|
|
672
|
+
next: (cfg) => ({ fold_finished_tools: !cfg.fold_finished_tools }),
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
key: 'completion_bell',
|
|
676
|
+
label: 'Completion bell',
|
|
677
|
+
description: 'Ring a short sound in the viewer when the node you are watching finishes a turn.',
|
|
678
|
+
format: (cfg) => (cfg.completion_bell ? 'on' : 'off'),
|
|
679
|
+
next: (cfg) => ({ completion_bell: !cfg.completion_bell }),
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
key: 'live_cycles',
|
|
683
|
+
label: 'Live cycles',
|
|
684
|
+
description: 'Trailing refresh cycles rendered at full fidelity; older cycles condense to text. Default 2.',
|
|
685
|
+
format: (cfg) => String(cfg.live_cycles),
|
|
686
|
+
next: (cfg, delta) => ({ live_cycles: ((cfg.live_cycles - 1 + delta + 6) % 6) + 1 }),
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
key: 'condensed_history',
|
|
690
|
+
label: 'Condensed history',
|
|
691
|
+
description: 'Which messages of a condensed cycle still render. Tool calls are never kept.',
|
|
692
|
+
format: (cfg) => cfg.condensed_history,
|
|
693
|
+
next: (cfg, delta) => ({
|
|
694
|
+
condensed_history: CONDENSED_HISTORY_MODES[(CONDENSED_HISTORY_MODES.indexOf(cfg.condensed_history) + delta + CONDENSED_HISTORY_MODES.length)
|
|
695
|
+
% CONDENSED_HISTORY_MODES.length],
|
|
696
|
+
}),
|
|
697
|
+
},
|
|
698
|
+
];
|
|
655
699
|
const STRENGTHS = ['ultra', 'strong', 'medium', 'light'];
|
|
656
700
|
const LADDER_PROVIDER_KEYS = ['anthropic', 'openai'];
|
|
657
701
|
const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
@@ -1265,6 +1309,8 @@ export class SetupWizard {
|
|
|
1265
1309
|
keyNotice;
|
|
1266
1310
|
brokerLimitIndex = 0;
|
|
1267
1311
|
brokerLimitNotice;
|
|
1312
|
+
displayIndex = 0;
|
|
1313
|
+
displayNotice;
|
|
1268
1314
|
cachedWidth;
|
|
1269
1315
|
cachedHeight;
|
|
1270
1316
|
cachedLines;
|
|
@@ -1336,9 +1382,39 @@ export class SetupWizard {
|
|
|
1336
1382
|
this.invalidate();
|
|
1337
1383
|
}
|
|
1338
1384
|
moveTab(delta) {
|
|
1339
|
-
const next = ((this.activeTab + delta +
|
|
1385
|
+
const next = ((this.activeTab + delta + TAB_LABELS.length) % TAB_LABELS.length);
|
|
1340
1386
|
this.setTab(next);
|
|
1341
1387
|
}
|
|
1388
|
+
moveDisplayRow(delta) {
|
|
1389
|
+
this.displayIndex = (this.displayIndex + delta + DISPLAY_ROWS.length) % DISPLAY_ROWS.length;
|
|
1390
|
+
this.displayNotice = undefined;
|
|
1391
|
+
this.invalidate();
|
|
1392
|
+
}
|
|
1393
|
+
/** Advance the selected display setting to its next value and persist it to user
|
|
1394
|
+
* config immediately — these are one-keystroke view preferences, so there is no
|
|
1395
|
+
* separate save gesture to forget. A viewer picks the new value up on its next
|
|
1396
|
+
* attach (`live_cycles` / `condensed_history` / `fold_finished_tools` are all read
|
|
1397
|
+
* per ChatView), and `completion_bell` on the next turn end. */
|
|
1398
|
+
cycleDisplaySetting(delta) {
|
|
1399
|
+
const row = DISPLAY_ROWS[this.displayIndex];
|
|
1400
|
+
const current = readConfig('user');
|
|
1401
|
+
const next = row.next(current, delta);
|
|
1402
|
+
updateRawConfigAtomically('user', (latest) => ({ ...latest, ...next }));
|
|
1403
|
+
this.displayNotice = `Saved ${row.key} = ${row.format({ ...current, ...next })}. Applies on the next attach.`;
|
|
1404
|
+
this.invalidate();
|
|
1405
|
+
}
|
|
1406
|
+
handleDisplayInput(data) {
|
|
1407
|
+
if (this.matches('crtr.setup.list-down', data))
|
|
1408
|
+
this.moveDisplayRow(1);
|
|
1409
|
+
else if (this.matches('crtr.setup.list-up', data))
|
|
1410
|
+
this.moveDisplayRow(-1);
|
|
1411
|
+
else if (this.matches('crtr.setup.toggle', data) || this.matches('crtr.setup.cell-next', data))
|
|
1412
|
+
this.cycleDisplaySetting(1);
|
|
1413
|
+
else if (this.matches('crtr.setup.cell-previous', data))
|
|
1414
|
+
this.cycleDisplaySetting(-1);
|
|
1415
|
+
else if (this.matches('crtr.setup.advance', data))
|
|
1416
|
+
this.moveTab(1);
|
|
1417
|
+
}
|
|
1342
1418
|
cancel() {
|
|
1343
1419
|
if (this.confirming) {
|
|
1344
1420
|
this.confirming = false;
|
|
@@ -1899,11 +1975,15 @@ export class SetupWizard {
|
|
|
1899
1975
|
this.invalidate();
|
|
1900
1976
|
return;
|
|
1901
1977
|
}
|
|
1978
|
+
if (this.activeTab === DISPLAY_TAB) {
|
|
1979
|
+
this.handleDisplayInput(data);
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1902
1982
|
this.handleBrokerLimitsInput(data);
|
|
1903
1983
|
}
|
|
1904
1984
|
renderHeader(width) {
|
|
1905
|
-
const tabs =
|
|
1906
|
-
const fullTabLine = tabs
|
|
1985
|
+
const tabs = TAB_LABELS;
|
|
1986
|
+
const fullTabLine = tabs.slice()
|
|
1907
1987
|
.map((label, index) => tabPill(label, index === this.activeTab))
|
|
1908
1988
|
.join(theme.fg('dim', ' · '));
|
|
1909
1989
|
const tabLine = visibleWidth(fullTabLine) <= width
|
|
@@ -1923,6 +2003,9 @@ export class SetupWizard {
|
|
|
1923
2003
|
: `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} row · ${this.label('crtr.setup.cell-previous')}/${this.label('crtr.setup.cell-next')} column · ${this.label('crtr.setup.models.scope-next')} scope · ${this.label('crtr.setup.models.open')} edit · ${this.label('crtr.setup.tab-next')} next · ${this.label('crtr.setup.cancel')} cancel`;
|
|
1924
2004
|
return [truncateToWidth(theme.fg('dim', hint), width)];
|
|
1925
2005
|
}
|
|
2006
|
+
if (this.activeTab === DISPLAY_TAB) {
|
|
2007
|
+
return [truncateToWidth(theme.fg('dim', `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} row · ${this.label('crtr.setup.toggle')} or ${this.label('crtr.setup.cell-next')}/${this.label('crtr.setup.cell-previous')} change value · ${tabs} · ${this.label('crtr.setup.cancel')} cancel`), width)];
|
|
2008
|
+
}
|
|
1926
2009
|
if (this.activeTab === 6) {
|
|
1927
2010
|
return [truncateToWidth(theme.fg('dim', `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} field · type value · Enter save · ${this.fieldSafeLabel('crtr.setup.tab-next')}/${this.fieldSafeLabel('crtr.setup.tab-previous')} switch tabs · ${this.label('crtr.setup.cancel')} cancel`), width)];
|
|
1928
2011
|
}
|
|
@@ -1973,7 +2056,7 @@ export class SetupWizard {
|
|
|
1973
2056
|
}
|
|
1974
2057
|
renderSystems(width) {
|
|
1975
2058
|
const out = [];
|
|
1976
|
-
out.push(truncateToWidth(`${theme.fg('accent', theme.bold('System deps'))}${theme.fg('dim', ' tmux + nvim check')}`, width));
|
|
2059
|
+
out.push(truncateToWidth(`${theme.fg('accent', theme.bold('System deps'))}${theme.fg('dim', ' tmux + nvim + Nerd Font check')}`, width));
|
|
1977
2060
|
for (const [index, tool] of this.systemTools.entries()) {
|
|
1978
2061
|
const selected = index === this.systemIndex;
|
|
1979
2062
|
const marker = selected ? theme.fg('accent', '▶') : theme.fg('dim', ' ');
|
|
@@ -2172,6 +2255,26 @@ export class SetupWizard {
|
|
|
2172
2255
|
}
|
|
2173
2256
|
return out;
|
|
2174
2257
|
}
|
|
2258
|
+
renderDisplay(width) {
|
|
2259
|
+
const out = [];
|
|
2260
|
+
const cfg = readConfig('user');
|
|
2261
|
+
out.push(truncateToWidth(`${theme.fg('accent', theme.bold('Display'))}${theme.fg('dim', ' user scope · attach viewer')}`, width));
|
|
2262
|
+
out.push(truncateToWidth(theme.fg('dim', 'Each change saves immediately and applies to viewers attached from now on.'), width));
|
|
2263
|
+
out.push('');
|
|
2264
|
+
for (const [index, row] of DISPLAY_ROWS.entries()) {
|
|
2265
|
+
const selected = index === this.displayIndex;
|
|
2266
|
+
const marker = selected ? theme.fg('accent', '▶') : ' ';
|
|
2267
|
+
const name = selected ? theme.fg('accent', row.label) : row.label;
|
|
2268
|
+
out.push(truncateToWidth(`${marker} ${padAnsi(name, 24)} ${theme.fg('success', row.format(cfg))}`, width));
|
|
2269
|
+
out.push(truncateToWidth(theme.fg('dim', ` ${row.description}`), width));
|
|
2270
|
+
}
|
|
2271
|
+
if (this.displayNotice) {
|
|
2272
|
+
out.push('');
|
|
2273
|
+
for (const line of wrapText(this.displayNotice, width))
|
|
2274
|
+
out.push(theme.fg('success', line));
|
|
2275
|
+
}
|
|
2276
|
+
return out;
|
|
2277
|
+
}
|
|
2175
2278
|
render(width) {
|
|
2176
2279
|
const bypass = this.activeTab === 5 && this.models.isPickerOpen();
|
|
2177
2280
|
const height = this.terminalRows();
|
|
@@ -2194,7 +2297,9 @@ export class SetupWizard {
|
|
|
2194
2297
|
? this.renderKeybindings(width)
|
|
2195
2298
|
: this.activeTab === 5
|
|
2196
2299
|
? this.renderModels(width)
|
|
2197
|
-
: this.
|
|
2300
|
+
: this.activeTab === 6
|
|
2301
|
+
? this.renderBrokerLimits(width)
|
|
2302
|
+
: this.renderDisplay(width)),
|
|
2198
2303
|
'',
|
|
2199
2304
|
...this.renderFooter(width),
|
|
2200
2305
|
];
|
|
@@ -205,7 +205,7 @@ test('tmux installer remaps owned pairs and preserves occupied user bindings', (
|
|
|
205
205
|
const currentNode = defaultMenu.indexOf('-#[bold]Current node');
|
|
206
206
|
const lifecycle = defaultMenu.indexOf('-#[bold]Lifecycle');
|
|
207
207
|
const system = defaultMenu.indexOf('-#[bold]System');
|
|
208
|
-
for (const item of ['resume node', 'graph view', 'focus manager', 'show ›']) {
|
|
208
|
+
for (const item of ['resume node', 'graph view', 'focus manager', 'show ›', 'view settings ›']) {
|
|
209
209
|
assert.ok(defaultMenu.indexOf(item) > explore && defaultMenu.indexOf(item) < currentNode, `${item} belongs in Explore`);
|
|
210
210
|
}
|
|
211
211
|
assert.match(defaultMenu, /"show ›" s .*display-menu.*metadata.*context \+ reports/);
|
|
@@ -213,7 +213,10 @@ test('tmux installer remaps owned pairs and preserves occupied user bindings', (
|
|
|
213
213
|
assert.ok(defaultMenu.indexOf(item) > currentNode && defaultMenu.indexOf(item) < lifecycle, `${item} belongs to the current node`);
|
|
214
214
|
}
|
|
215
215
|
assert.ok(defaultMenu.indexOf('fork into idle root') > lifecycle && defaultMenu.indexOf('fork into idle root') < system, 'fork belongs in Lifecycle');
|
|
216
|
-
assert.
|
|
216
|
+
assert.match(defaultMenu, /"view settings ›" w .*display-menu.*fold finished tool calls/);
|
|
217
|
+
// Matched with its selector, so the Explore group's "view settings ›" item
|
|
218
|
+
// (selector `w`) cannot satisfy this assertion by substring.
|
|
219
|
+
assert.ok(defaultMenu.indexOf('settings p ') > system, 'settings belongs in System');
|
|
217
220
|
assert.ok(defaultMenu.indexOf('create ›') > system, 'create belongs in System');
|
|
218
221
|
assert.match(tmuxBinding(socket, 'root', 'M-v') ?? '', /switch-client/);
|
|
219
222
|
// The inbox toggle installs at its default key (alt+i → M-i) in the same sweep.
|
|
@@ -21,7 +21,7 @@ import { ensureHome, ensureNodeDirs, nodeMetaPath, nodeDir, nodesRoot, jobDir, }
|
|
|
21
21
|
/** The identity keys meta.json persists. Listed explicitly so no runtime field
|
|
22
22
|
* can ever leak onto disk even when a fully-hydrated NodeMeta is handed in. */
|
|
23
23
|
const IDENTITY_KEYS = [
|
|
24
|
-
'node_id', 'name', 'description', 'cycles', 'created', 'cwd', 'host_kind', 'profile_id', 'kind', 'mode',
|
|
24
|
+
'node_id', 'name', 'description', 'icon', 'cycles', 'created', 'cwd', 'host_kind', 'profile_id', 'kind', 'mode',
|
|
25
25
|
'lifecycle', 'persona_ack', 'parent', 'spawned_by', 'fork_from', 'human_work_target', 'private_fork_phase',
|
|
26
26
|
'private_fork_recipe', 'passive_default', 'pi_session_id', 'pi_session_file', 'cycle_pending', 'model_override',
|
|
27
27
|
'launch', 'managed_worktree',
|
|
@@ -9,6 +9,7 @@ export interface NamedNode {
|
|
|
9
9
|
name: string;
|
|
10
10
|
kind: string;
|
|
11
11
|
description?: string;
|
|
12
|
+
icon?: string;
|
|
12
13
|
}
|
|
13
14
|
/** A node's human label: its explicit handle plus the pi-generated description
|
|
14
15
|
* of its first task, e.g. `fix-auth refactor-login-flow`. The handle is
|
|
@@ -34,5 +34,7 @@ export function personaLabel(meta) {
|
|
|
34
34
|
* badge. Recomputed from meta on every revive and pushed live when naming
|
|
35
35
|
* generates the first task description. */
|
|
36
36
|
export function editorLabel(meta) {
|
|
37
|
-
|
|
37
|
+
const icon = (meta.icon ?? '').trim();
|
|
38
|
+
const name = fullName(meta);
|
|
39
|
+
return icon !== '' ? `${icon} ${name}` : name;
|
|
38
40
|
}
|
|
@@ -23,6 +23,12 @@ export declare const GRAY = "\u001B[90m";
|
|
|
23
23
|
* `isPidAlive` / `isBusy` reads entirely (a remote node id may collide with a
|
|
24
24
|
* local one) and renders the bare status glyph instead. */
|
|
25
25
|
export declare function nodeGlyph(node: NodeMeta | null, remote?: boolean): string;
|
|
26
|
+
/** Drop every SGR sequence, keeping the visible text. The selection bar uses
|
|
27
|
+
* this: reverse-video INVERTS a coloured cell into a coloured block, so a row
|
|
28
|
+
* built from tinted spans highlights as a patchwork of background swatches
|
|
29
|
+
* instead of one bar. Stripping colour first leaves a single clean bar — the
|
|
30
|
+
* glyphs (●/○/✓/✗/⚙/◷) still carry the signal, exactly as under NO_COLOR. */
|
|
31
|
+
export declare function stripSgr(s: string): string;
|
|
26
32
|
/** Visible width, ignoring ANSI escapes. */
|
|
27
33
|
export declare function visibleWidth(s: string): number;
|
|
28
34
|
/** Truncate to `max` VISIBLE columns: escape sequences are copied through
|
|
@@ -71,6 +71,14 @@ export function nodeGlyph(node, remote = false) {
|
|
|
71
71
|
return `${ESC}${v.color}m${v.glyph}${RESET}`;
|
|
72
72
|
}
|
|
73
73
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
74
|
+
/** Drop every SGR sequence, keeping the visible text. The selection bar uses
|
|
75
|
+
* this: reverse-video INVERTS a coloured cell into a coloured block, so a row
|
|
76
|
+
* built from tinted spans highlights as a patchwork of background swatches
|
|
77
|
+
* instead of one bar. Stripping colour first leaves a single clean bar — the
|
|
78
|
+
* glyphs (●/○/✓/✗/⚙/◷) still carry the signal, exactly as under NO_COLOR. */
|
|
79
|
+
export function stripSgr(s) {
|
|
80
|
+
return s.replace(ANSI_RE, '');
|
|
81
|
+
}
|
|
74
82
|
/** Visible width, ignoring ANSI escapes. */
|
|
75
83
|
export function visibleWidth(s) {
|
|
76
84
|
return s.replace(ANSI_RE, '').length;
|
|
@@ -87,6 +87,12 @@ export interface NodeIdentity {
|
|
|
87
87
|
/** A 2-4 word kebab-case handle derived from the node's first prompt (named
|
|
88
88
|
* headlessly by pi; see runtime/naming.ts). Shown in the editor label. */
|
|
89
89
|
description?: string;
|
|
90
|
+
/** A single Nerd Font glyph depicting the node's work, chosen by the same
|
|
91
|
+
* headless namer that produced `description` (see runtime/naming.ts). Kept as
|
|
92
|
+
* its own field rather than baked into the description so each surface decides
|
|
93
|
+
* whether to render it; absent on a node named before icons existed, or when
|
|
94
|
+
* the model returned nothing usable. */
|
|
95
|
+
icon?: string;
|
|
90
96
|
/** How many times this node has been (re)launched — born at 0, bumped on
|
|
91
97
|
* every revive. Exposed to the pi footer as CRTR_CYCLES so a refresh/crash
|
|
92
98
|
* cycle reads at a glance there. */
|
package/dist/core/config.js
CHANGED
|
@@ -359,12 +359,13 @@ function mergeConfig(partial) {
|
|
|
359
359
|
const condensed_history = CONDENSED_HISTORY_MODES.includes(partial.condensed_history)
|
|
360
360
|
? partial.condensed_history
|
|
361
361
|
: defaults.condensed_history;
|
|
362
|
+
const fold_finished_tools = typeof partial.fold_finished_tools === 'boolean' ? partial.fold_finished_tools : defaults.fold_finished_tools;
|
|
362
363
|
const keybindings = normalizeKeybindings(partial.keybindings);
|
|
363
364
|
const modelLadders = mergeModelLadders(partial.modelLadders);
|
|
364
365
|
const kinds = mergeKinds(partial.kinds);
|
|
365
366
|
const remoteCanvas = mergeRemoteCanvas(partial.remoteCanvas);
|
|
366
367
|
const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
|
|
367
|
-
return { schema_version, marketplaces, plugins, configuredClis, auto_update, max_panes_per_window, brokerThresholds, completion_bell, live_cycles, condensed_history, keybindings, modelLadders, kinds, remoteCanvas, spawnEnv };
|
|
368
|
+
return { schema_version, marketplaces, plugins, configuredClis, auto_update, max_panes_per_window, brokerThresholds, completion_bell, live_cycles, condensed_history, fold_finished_tools, keybindings, modelLadders, kinds, remoteCanvas, spawnEnv };
|
|
368
369
|
}
|
|
369
370
|
/** Raw (un-defaulted) partial config for one scope, or null if the scope has
|
|
370
371
|
* no root or no config.json. Used by `readMergedLaunchConfig` to layer
|
|
@@ -497,7 +498,7 @@ export function subKindsAvailableTo(kind) {
|
|
|
497
498
|
* an untouched key is never rewritten. */
|
|
498
499
|
const CONFIG_TOP_LEVEL_KEYS = [
|
|
499
500
|
'schema_version', 'marketplaces', 'plugins', 'configuredClis', 'auto_update',
|
|
500
|
-
'max_panes_per_window', 'brokerThresholds', 'completion_bell', 'live_cycles', 'condensed_history', 'keybindings', 'modelLadders', 'kinds',
|
|
501
|
+
'max_panes_per_window', 'brokerThresholds', 'completion_bell', 'live_cycles', 'condensed_history', 'fold_finished_tools', 'keybindings', 'modelLadders', 'kinds',
|
|
501
502
|
'remoteCanvas', 'spawnEnv',
|
|
502
503
|
];
|
|
503
504
|
/** The SPARSE `modelLadders` to persist after a config mutation: the raw
|
|
@@ -14,7 +14,7 @@ function ids(prefix, actions) {
|
|
|
14
14
|
return actions.map((action) => `${prefix}.${action}`);
|
|
15
15
|
}
|
|
16
16
|
const REQUIRED_IDS = [
|
|
17
|
-
...ids('crtr.tmux.menu', ['open', 'promote', 'resume', 'fork', 'demote', 'detach', 'close-subtree', 'background-bash', 'file-review', 'pin', 'show', 'metadata', 'context', 'providers', 'graph', 'focus-manager', 'copy-id', 'reload-viewer', 'issues']),
|
|
17
|
+
...ids('crtr.tmux.menu', ['open', 'promote', 'resume', 'fork', 'demote', 'detach', 'close-subtree', 'background-bash', 'file-review', 'pin', 'show', 'view-settings', 'fold-tools', 'metadata', 'context', 'providers', 'graph', 'focus-manager', 'copy-id', 'reload-viewer', 'issues']),
|
|
18
18
|
...ids('crtr.tmux.menu.issue', ['idea', 'bug']),
|
|
19
19
|
...ids('crtr.tmux.node', ['next', 'previous']),
|
|
20
20
|
'crtr.tmux.inbox.toggle',
|