@north-light/crouter 0.3.158 → 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/storage-tiers.md +2 -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 -8
- package/dist/clients/attach/chrome/bash-jobs.js +20 -32
- package/dist/clients/attach/chrome/canvas-panels.js +10 -3
- package/dist/clients/attach/chrome/roster.d.ts +10 -4
- package/dist/clients/attach/chrome/roster.js +96 -36
- 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 +568 -566
- package/dist/commands/cron.js +2 -2
- package/dist/commands/node.js +141 -3
- package/dist/commands/search.js +1 -1
- package/dist/commands/surface-inspect.js +4 -3
- 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__/canvas-inbox-watcher.test.js +5 -3
- package/dist/core/__tests__/tmux-surface.test.js +5 -2
- package/dist/core/bash-jobs.d.ts +11 -0
- package/dist/core/bash-jobs.js +41 -1
- 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/feed/inbox.d.ts +3 -3
- package/dist/core/feed/inbox.js +10 -6
- package/dist/core/inspector/core.d.ts +17 -2
- package/dist/core/inspector/core.js +172 -23
- package/dist/core/inspector/model.d.ts +30 -1
- package/dist/core/inspector/model.js +39 -0
- package/dist/core/inspector/text.js +14 -1
- package/dist/core/inspector/tui.js +71 -3
- package/dist/core/keybindings/__tests__/resolve.test.js +2 -2
- package/dist/core/keybindings/catalog.d.ts +3 -3
- package/dist/core/keybindings/catalog.js +5 -2
- 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/core/tui/host.js +21 -6
- package/dist/daemon/api/map.js +2 -0
- package/dist/pi-extensions/canvas-bash-valve.js +5 -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
|
@@ -27,7 +27,8 @@ import { profilesStateBlock } from '../core/profiles/state-block.js';
|
|
|
27
27
|
import { stateBlock } from '../core/help.js';
|
|
28
28
|
import { fuzzyMatch } from '../core/canvas/browse/model.js';
|
|
29
29
|
import { loadPins, savePins } from '../core/canvas/browse/pins.js';
|
|
30
|
-
import {
|
|
30
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
31
|
+
import { activeBackgroundBashJobs, backgroundRunningBashJobs, bashJobPaths, formatBashElapsed, jobStartedAtMs, readJobPgid, tailJobLog } from '../core/bash-jobs.js';
|
|
31
32
|
// The API seam — every canvas mutation/read routes through the daemon (B-1).
|
|
32
33
|
import { cliClient, cliCanvasSource, ApiCanvasSource, rethrowAsCliError, getNodeOrNull } from './api-client.js';
|
|
33
34
|
// Past this much context, an ORCHESTRATOR that spawns a managed child is better
|
|
@@ -842,16 +843,153 @@ const nodeBashBackground = defineLeaf({
|
|
|
842
843
|
return `Handed ${result['backgrounded']} bash command(s) to the background job system for ${result['node_id']} · ran ${formatBashElapsed(oldestElapsedMs)} in the foreground.`;
|
|
843
844
|
},
|
|
844
845
|
});
|
|
846
|
+
const nodeBashList = defineLeaf({
|
|
847
|
+
name: 'list',
|
|
848
|
+
description: 'list a node’s live background bash jobs',
|
|
849
|
+
whenToUse: 'the Inspector’s jobs section reads this to show what a node handed off to the background',
|
|
850
|
+
tier: 'hidden',
|
|
851
|
+
help: {
|
|
852
|
+
name: 'node bash list',
|
|
853
|
+
summary: 'read every still-running background bash job owned by one node',
|
|
854
|
+
params: [
|
|
855
|
+
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node whose background jobs to list. Defaults to the node in --pane, else the caller (CRTR_NODE_ID).' },
|
|
856
|
+
{ kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE.' },
|
|
857
|
+
{ kind: 'flag', name: 'tail', type: 'int', required: false, default: 0, constraint: 'Include this many trailing log lines per job (0 = none).' },
|
|
858
|
+
],
|
|
859
|
+
output: [
|
|
860
|
+
{ name: 'node_id', type: 'string', required: true, constraint: 'The target node.' },
|
|
861
|
+
{ name: 'jobs', type: 'object[]', required: true, constraint: 'Oldest-first {job_id, command, started_at, elapsed_ms, log_path, pgid, log_tail} for every live background job.' },
|
|
862
|
+
],
|
|
863
|
+
outputKind: 'object',
|
|
864
|
+
effects: ['Reads the node context jobs directory. Changes nothing.'],
|
|
865
|
+
},
|
|
866
|
+
run: async (input) => {
|
|
867
|
+
const pane = input['pane'] ?? process.env['TMUX_PANE'];
|
|
868
|
+
let id = input['node'];
|
|
869
|
+
if (id === undefined || id === '')
|
|
870
|
+
id = nodeInPane(pane);
|
|
871
|
+
if (id === undefined || id === '')
|
|
872
|
+
id = process.env['CRTR_NODE_ID'];
|
|
873
|
+
if (id === undefined || id === '') {
|
|
874
|
+
throw new InputError({ error: 'no_node', message: 'no node found for the bash job list', next: 'Run from inside a node, pass --node <id>, or --pane <pane>.' });
|
|
875
|
+
}
|
|
876
|
+
const node = await getNodeOrNull(id);
|
|
877
|
+
if (node === null) {
|
|
878
|
+
throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
|
|
879
|
+
}
|
|
880
|
+
const now = Date.now();
|
|
881
|
+
const tail = Math.max(0, Math.trunc(Number(input['tail'] ?? 0)));
|
|
882
|
+
const dir = contextDir(node.node_id);
|
|
883
|
+
const jobs = activeBackgroundBashJobs(dir).map((job) => ({
|
|
884
|
+
job_id: job.jobId,
|
|
885
|
+
command: job.command,
|
|
886
|
+
started_at: new Date(job.startedAtMs).toISOString(),
|
|
887
|
+
elapsed_ms: now - job.startedAtMs,
|
|
888
|
+
log_path: job.logPath,
|
|
889
|
+
pgid: readJobPgid(dir, job.jobId) ?? null,
|
|
890
|
+
log_tail: tailJobLog(job.logPath, tail),
|
|
891
|
+
}));
|
|
892
|
+
return { node_id: node.node_id, jobs };
|
|
893
|
+
},
|
|
894
|
+
render: (result) => {
|
|
895
|
+
const jobs = result['jobs'];
|
|
896
|
+
if (jobs.length === 0)
|
|
897
|
+
return `No background bash jobs for ${result['node_id']}.`;
|
|
898
|
+
return jobs.map((job) => `${job.job_id} · ${formatBashElapsed(job.elapsed_ms)} · ${job.command.split('\n')[0]}\n log: ${job.log_path}`).join('\n');
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
/** SIGTERM the job's process group, give it a moment, then SIGKILL. Returns
|
|
902
|
+
* false only when the group was already gone at the first signal. */
|
|
903
|
+
function stopJobGroup(pgid) {
|
|
904
|
+
try {
|
|
905
|
+
process.kill(-pgid, 'SIGTERM');
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
const nodeBashKill = defineLeaf({
|
|
913
|
+
name: 'kill',
|
|
914
|
+
description: 'stop one of a node’s background bash jobs',
|
|
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
|
+
tier: 'hidden',
|
|
917
|
+
help: {
|
|
918
|
+
name: 'node bash kill',
|
|
919
|
+
summary: 'terminate a background bash job’s process group and tell the owning node it was canceled',
|
|
920
|
+
params: [
|
|
921
|
+
{ kind: 'positional', name: 'job', required: true, constraint: 'Job id, as reported by node bash list.' },
|
|
922
|
+
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node owning the job. Defaults to the node in --pane, else the caller (CRTR_NODE_ID).' },
|
|
923
|
+
{ kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE.' },
|
|
924
|
+
],
|
|
925
|
+
output: [
|
|
926
|
+
{ name: 'node_id', type: 'string', required: true, constraint: 'The owning node.' },
|
|
927
|
+
{ name: 'job_id', type: 'string', required: true, constraint: 'The job that was stopped.' },
|
|
928
|
+
{ name: 'signaled', type: 'boolean', required: true, constraint: 'True when the process group was still alive and received SIGTERM.' },
|
|
929
|
+
],
|
|
930
|
+
outputKind: 'object',
|
|
931
|
+
effects: [
|
|
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 stopped — which steers that node mid-turn.',
|
|
934
|
+
],
|
|
935
|
+
},
|
|
936
|
+
run: async (input) => {
|
|
937
|
+
const pane = input['pane'] ?? process.env['TMUX_PANE'];
|
|
938
|
+
let id = input['node'];
|
|
939
|
+
if (id === undefined || id === '')
|
|
940
|
+
id = nodeInPane(pane);
|
|
941
|
+
if (id === undefined || id === '')
|
|
942
|
+
id = process.env['CRTR_NODE_ID'];
|
|
943
|
+
if (id === undefined || id === '') {
|
|
944
|
+
throw new InputError({ error: 'no_node', message: 'no node found for the bash job', next: 'Run from inside a node, pass --node <id>, or --pane <pane>.' });
|
|
945
|
+
}
|
|
946
|
+
const node = await getNodeOrNull(id);
|
|
947
|
+
if (node === null) {
|
|
948
|
+
throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
|
|
949
|
+
}
|
|
950
|
+
const jobId = input['job'];
|
|
951
|
+
const dir = contextDir(node.node_id);
|
|
952
|
+
const paths = bashJobPaths(dir, jobId);
|
|
953
|
+
if (!existsSync(paths.dir)) {
|
|
954
|
+
throw new InputError({ error: 'not_found', message: `no background job ${jobId} for ${node.node_id}`, next: 'List live jobs with `crtr node bash list`.' });
|
|
955
|
+
}
|
|
956
|
+
const pgid = readJobPgid(dir, jobId);
|
|
957
|
+
if (pgid === undefined) {
|
|
958
|
+
throw new InputError({
|
|
959
|
+
error: 'not_stoppable',
|
|
960
|
+
message: `job ${jobId} recorded no process group`,
|
|
961
|
+
next: `It started before crtr persisted one. Stop it by hand, then it will disappear from the list. Log: ${paths.jobLog}`,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
const signaled = stopJobGroup(pgid);
|
|
965
|
+
// The supervisor shares the group it leads, so it usually dies with the
|
|
966
|
+
// command and never writes job.exit. Record the cancellation ourselves —
|
|
967
|
+
// that sentinel is what retires the job from every live-jobs read.
|
|
968
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
969
|
+
try {
|
|
970
|
+
process.kill(-pgid, 'SIGKILL');
|
|
971
|
+
}
|
|
972
|
+
catch { /* already gone */ }
|
|
973
|
+
if (!existsSync(paths.jobExit))
|
|
974
|
+
writeFileSync(paths.jobExit, '143\n');
|
|
975
|
+
await cliClient().sendMessage(node.node_id, {
|
|
976
|
+
body: `Background bash job ${jobId} was stopped by the human before it finished. Log: ${paths.jobLog} Command: ${paths.cmdSh}`,
|
|
977
|
+
tier: 'urgent',
|
|
978
|
+
});
|
|
979
|
+
return { node_id: node.node_id, job_id: jobId, signaled };
|
|
980
|
+
},
|
|
981
|
+
render: (result) => `Canceled background bash job ${result['job_id']} for ${result['node_id']}${result['signaled'] === true ? '' : ' (its process group was already gone)'}.`,
|
|
982
|
+
});
|
|
845
983
|
const nodeBash = defineBranch({
|
|
846
984
|
name: 'bash',
|
|
847
985
|
description: 'control file-backed bash jobs owned by a node',
|
|
848
|
-
whenToUse: '
|
|
986
|
+
whenToUse: 'a running bash command must be handed off, listed, or stopped — the tmux prefix menu backgrounds, the Inspector’s jobs section lists and cancels',
|
|
849
987
|
tier: 'hidden',
|
|
850
988
|
help: {
|
|
851
989
|
name: 'node bash',
|
|
852
990
|
summary: 'control file-backed bash jobs owned by a node',
|
|
853
991
|
},
|
|
854
|
-
children: [nodeBashBackground],
|
|
992
|
+
children: [nodeBashBackground, nodeBashList, nodeBashKill],
|
|
855
993
|
});
|
|
856
994
|
export const surfaceNodeIdLeaf = defineLeaf({
|
|
857
995
|
name: 'id',
|
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',
|
|
@@ -8,17 +8,18 @@ import { runCoreView } from '../core/tui/host.js';
|
|
|
8
8
|
export const surfaceInspectLeaf = defineLeaf({
|
|
9
9
|
name: 'inspect',
|
|
10
10
|
description: 'host the native Inspector for one node',
|
|
11
|
-
whenToUse: 'you want a live native screen for a node’s overview, raw record,
|
|
11
|
+
whenToUse: 'you want a live native screen for a node’s overview, raw record, owner-scoped crons, and live background bash jobs. Use node inspect show for the underlying one-shot data read instead.',
|
|
12
12
|
help: {
|
|
13
13
|
name: 'surface inspect',
|
|
14
14
|
summary: 'host the native Inspector in this pane (the attach viewer opens it as a tmux popup)',
|
|
15
|
+
// sections: overview · schedule (armed crons) · jobs (live background bash) · raw
|
|
15
16
|
params: [
|
|
16
17
|
{ kind: 'positional', name: 'node', required: true, constraint: 'Node id or resolvable reference to inspect.' },
|
|
17
|
-
{ kind: 'flag', name: 'section', type: 'enum', choices: ['overview', 'schedule', 'raw'], required: false, default: 'overview', constraint: 'Section focused when the Inspector opens.' },
|
|
18
|
+
{ kind: 'flag', name: 'section', type: 'enum', choices: ['overview', 'schedule', 'jobs', 'raw'], required: false, default: 'overview', constraint: 'Section focused when the Inspector opens.' },
|
|
18
19
|
],
|
|
19
20
|
output: [],
|
|
20
21
|
outputKind: 'object',
|
|
21
|
-
effects: ['Reads node and
|
|
22
|
+
effects: ['Reads node, cron, and background bash job data through crtr subprocesses and crtrd. Takes over the hosting pane (or popup) until it quits.'],
|
|
22
23
|
},
|
|
23
24
|
run: async (input) => {
|
|
24
25
|
const node = input['node'];
|
|
@@ -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
|
];
|
|
@@ -75,7 +75,7 @@ async function waitFor(predicate, timeoutMs = 3000, stepMs = 10) {
|
|
|
75
75
|
await wait(stepMs);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
test('coalesce inlines short
|
|
78
|
+
test('coalesce inlines short report bodies with a trailing artifact pointer and only hints for remaining canonical refs', () => {
|
|
79
79
|
freshNode('child');
|
|
80
80
|
const child = createNode({
|
|
81
81
|
node_id: 'child',
|
|
@@ -93,8 +93,10 @@ test('coalesce inlines short reports with their absolute path and only hints for
|
|
|
93
93
|
const digest = coalesce([
|
|
94
94
|
finalizeInboxEntry({ from: 'child', tier: 'normal', kind: 'update', label: 'short report', ref: 'child:reports/short-update.md' }),
|
|
95
95
|
]);
|
|
96
|
-
|
|
97
|
-
assert.
|
|
96
|
+
// The label is NOT reprinted above the body it duplicates; the path trails it.
|
|
97
|
+
assert.doesNotMatch(digest, /\[update\] short report/);
|
|
98
|
+
assert.match(digest, /\[update\]\n {4}short report body\n/);
|
|
99
|
+
assert.match(digest, new RegExp(`\\(report: ${reportPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)`));
|
|
98
100
|
assert.doesNotMatch(digest, /Dereference a ref with/);
|
|
99
101
|
const guidance = 'Dereference a ref with `crtr canvas history read <ref>`.';
|
|
100
102
|
const canonical = coalesce([
|