@pellux/goodvibes-tui 1.21.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +714 -177
  4. package/docs/foundation-artifacts/peer-contract.json +8 -4
  5. package/package.json +9 -7
  6. package/src/cli/entrypoint.ts +6 -0
  7. package/src/cli/service-posture.ts +6 -2
  8. package/src/config/run-daemon-config-migration.ts +46 -0
  9. package/src/core/conversation-compaction-render.ts +88 -0
  10. package/src/core/conversation-line-cache.ts +120 -92
  11. package/src/core/conversation-rendering.ts +290 -233
  12. package/src/core/conversation-turn-structure.ts +464 -0
  13. package/src/core/conversation-types.ts +8 -9
  14. package/src/daemon/cli.ts +71 -5
  15. package/src/input/commands/local-runtime.ts +14 -14
  16. package/src/input/commands/operator-rpc.ts +15 -4
  17. package/src/input/daemon-owned-settings-descriptions.ts +53 -0
  18. package/src/input/handler-content-actions.ts +4 -2
  19. package/src/input/handler-onboarding-daemon-adopt.ts +10 -0
  20. package/src/input/onboarding/onboarding-feature-units.ts +38 -0
  21. package/src/input/onboarding/onboarding-wizard-types.ts +1 -0
  22. package/src/input/search.ts +1 -1
  23. package/src/input/settings-modal-data.ts +15 -85
  24. package/src/input/settings-modal-search.ts +99 -0
  25. package/src/input/settings-modal-types.ts +3 -1
  26. package/src/renderer/conversation-surface.ts +37 -11
  27. package/src/renderer/conversation-tree.ts +179 -0
  28. package/src/renderer/settings-modal-helpers.ts +56 -2
  29. package/src/renderer/settings-modal.ts +9 -50
  30. package/src/renderer/tool-call.ts +38 -5
  31. package/src/runtime/cloudflare-control-plane.ts +4 -1
  32. package/src/runtime/feature-settings.ts +7 -0
  33. package/src/runtime/index.ts +22 -1
  34. package/src/runtime/legacy-daemon-migration.ts +2 -0
  35. package/src/verification/verification-ledger.ts +115 -2
  36. package/src/version.ts +1 -1
  37. package/src/core/conversation-tool-groups.ts +0 -152
@@ -0,0 +1,179 @@
1
+ /**
2
+ * conversation-tree.ts — column geometry for the transcript's branch tree.
3
+ *
4
+ * The transcript renders an assistant turn as a small tree: the turn header at
5
+ * depth 0, each tool call it issues as a depth-1 branch, and each call's result
6
+ * as a depth-2 branch under that call. This module owns the *only* arithmetic
7
+ * that decides where those branch glyphs and their content land, so every row
8
+ * type (event lines, tool-call rows, collapsed fragments) agrees on a single
9
+ * column grid instead of each re-deriving it.
10
+ *
11
+ * Geometry, relative to the existing flush layout (LAYOUT.LEFT_MARGIN = 4):
12
+ *
13
+ * depth 0 ` ● assistant …` marker col 3, content col 5
14
+ * depth 1 `✓ ├ config.get …` connector col 5, content col 7
15
+ * depth 2 `✓ │ └ ▸ 627 lines …` connector col 7, content col 9
16
+ *
17
+ * The connector sits where the parent's content began, and content starts two
18
+ * columns after it — the same marker→content relationship a depth-0 row has.
19
+ * Status lives in a fixed gutter at column 0, OUTSIDE the indent, so every ✓
20
+ * and ✗ in the transcript lines up in one column regardless of nesting depth.
21
+ *
22
+ * Every level steps by exactly TREE_STEP_COLS, and indent, gutter and content
23
+ * columns are functions of depth and width ALONE — never of sibling status.
24
+ * That is what makes a `└`→`├` connector flip cost exactly one cell: the
25
+ * column it occupies exists either way, so no text moves.
26
+ *
27
+ * Narrow terminals: indenting unconditionally would eat the content budget and
28
+ * silently truncate the informative end of a row — the failure class the
29
+ * markdown-table column bug came from. treeIndentCols() therefore clamps the
30
+ * indent to whatever still leaves TREE_MIN_CONTENT_COLS of content, and drops
31
+ * to 0 (flush, no branch glyph) when even one level does not fit. Losing the
32
+ * visual nesting on a 30-column terminal is acceptable; losing characters is
33
+ * not.
34
+ */
35
+
36
+ import { LAYOUT } from './layout.ts';
37
+ import type { Line } from '../types/grid.ts';
38
+ import { createStyledCell } from '../types/grid.ts';
39
+
40
+ /** Columns added per nesting level. Fixed — never content-dependent. */
41
+ const TREE_STEP_COLS = 2;
42
+
43
+ /**
44
+ * Width of the reserved status column that every row carries, at every depth.
45
+ *
46
+ * The column exists whether or not the row currently has a marker: a completed
47
+ * row shows ✓, a failed row its failure glyph, an in-flight or plain row a
48
+ * blank of the same width. Because the space is always reserved, a marker
49
+ * appearing or resolving changes only that cell's contents and can never shift
50
+ * the label beside it. Combined with fixed per-depth indentation this gives the
51
+ * whole tree one rule: columns are fixed, only their contents change.
52
+ */
53
+ const TREE_STATUS_COLS = 2;
54
+
55
+ /**
56
+ * Content columns a branch row is guaranteed to keep. Below this the indent is
57
+ * reduced (and finally dropped) rather than squeezing the row's text.
58
+ */
59
+ const TREE_MIN_CONTENT_COLS = 16;
60
+
61
+
62
+ /**
63
+ * Effective indent, in columns, for a branch at `depth` rendered at `width`.
64
+ *
65
+ * Returns 0 for depth <= 0 (a flush, non-branch row) and for widths too narrow
66
+ * to carry even one level while keeping TREE_MIN_CONTENT_COLS of content. The
67
+ * result is always a whole multiple of TREE_STEP_COLS, so branch glyphs across
68
+ * different depths stay on the same column grid.
69
+ */
70
+ export function treeIndentCols(depth: number, width: number): number {
71
+ if (depth <= 0) return 0;
72
+ // The status gutter is charged first and never given back — see TREE_GUTTER_COL.
73
+ const budget = width - LAYOUT.LEFT_MARGIN - LAYOUT.RIGHT_MARGIN - TREE_STATUS_COLS - TREE_MIN_CONTENT_COLS;
74
+ if (budget < TREE_STEP_COLS) return 0;
75
+ const affordableLevels = Math.floor(budget / TREE_STEP_COLS);
76
+ return Math.min(depth, affordableLevels) * TREE_STEP_COLS;
77
+ }
78
+
79
+ /**
80
+ * Write the status gutter for a row: `glyph` when it has one, an untouched
81
+ * blank of the same width when it does not. Success and failure differ by
82
+ * GLYPH, not only by colour, so the distinction survives a monochrome terminal
83
+ * and colour-blind viewers.
84
+ */
85
+ export function writeTreeStatusGutter(
86
+ line: Line,
87
+ glyph: string | null,
88
+ fg: string,
89
+ width: number,
90
+ ): void {
91
+ if (!glyph || TREE_GUTTER_COL >= width) return;
92
+ line[TREE_GUTTER_COL] = createStyledCell(glyph, { fg, bold: true });
93
+ }
94
+
95
+ /**
96
+ * Column the connector (`├`/`└`) occupies for a given effective indent, and
97
+ * the column an ancestor's `│` gutter occupies at that same indent.
98
+ */
99
+ export function treeBranchCol(indentCols: number): number {
100
+ return LAYOUT.LEFT_MARGIN - 1 + indentCols;
101
+ }
102
+
103
+ /**
104
+ * The status gutter: a fixed column at the far LEFT of every row, before any
105
+ * indentation, carrying ✓ / ✕ / blank.
106
+ *
107
+ * Deliberately outside the indent so it does not scale with depth. With the
108
+ * marker placed after the connector instead, status would sit at a different
109
+ * horizontal position on every row depending on nesting, so scanning for what
110
+ * completed means tracking a ragged edge — and that degrades precisely as
111
+ * trees get deeper. Here every marker in the transcript lines up in one
112
+ * column no matter how deep its row sits, for the cost of one fixed column.
113
+ *
114
+ * It is never clamped: narrow terminals clamp the INDENT (see treeIndentCols)
115
+ * and leave the gutter intact, because losing nesting depth is recoverable by
116
+ * widening the terminal while losing which rows succeeded is not.
117
+ */
118
+ const TREE_GUTTER_COL = 0;
119
+
120
+ /** Column where a branch row's text starts, immediately after its connector. */
121
+ export function treeContentCol(indentCols: number): number {
122
+ return treeBranchCol(indentCols) + 2;
123
+ }
124
+
125
+ /**
126
+ * Columns at which ancestor `│` gutters are drawn for a row at `depth`, given
127
+ * the set of ancestor depths whose subtrees are still open.
128
+ *
129
+ * Clamping can push two levels onto the same column at narrow widths; a gutter
130
+ * is emitted only when it lands strictly left of this row's own connector, so
131
+ * a clamped tree degrades by losing gutters rather than by overwriting the
132
+ * connector.
133
+ */
134
+ function treeGutterCols(
135
+ openAncestorDepths: readonly number[],
136
+ depth: number,
137
+ width: number,
138
+ ): number[] {
139
+ const ownCol = treeBranchCol(treeIndentCols(depth, width));
140
+ const cols: number[] = [];
141
+ for (const ancestorDepth of openAncestorDepths) {
142
+ const col = treeBranchCol(treeIndentCols(ancestorDepth, width));
143
+ if (col < ownCol) cols.push(col);
144
+ }
145
+ return cols;
146
+ }
147
+
148
+ /**
149
+ * Draw a branch row's connector and the `│` gutters of its still-open
150
+ * ancestors onto an already-rendered line, in place.
151
+ *
152
+ * Connectors are recomputed from live structure on every rebuild (see
153
+ * conversation-turn-structure.ts), never cached, so a row that stops being
154
+ * last flips `└`→`├` here with nothing stale to invalidate. Only this cell
155
+ * changes: indent, status gutter and content column are functions of depth and
156
+ * width alone, so a sibling arriving repaints one connector cell and moves no
157
+ * text.
158
+ */
159
+ export function drawBranchConnector(
160
+ line: Line,
161
+ depth: number,
162
+ connector: string | undefined,
163
+ openAncestorDepths: readonly number[],
164
+ width: number,
165
+ fg: string,
166
+ ): void {
167
+ if (depth <= 0 || !connector) return;
168
+ const indent = treeIndentCols(depth, width);
169
+ if (indent <= 0) return;
170
+ for (const gutterCol of treeGutterCols(openAncestorDepths, depth, width)) {
171
+ if (gutterCol >= 0 && gutterCol < width) {
172
+ line[gutterCol] = createStyledCell('\u2502', { fg, dim: true });
173
+ }
174
+ }
175
+ const col = treeBranchCol(indent);
176
+ if (col >= 0 && col < width) {
177
+ line[col] = createStyledCell(connector, { fg, dim: true });
178
+ }
179
+ }
@@ -4,7 +4,7 @@
4
4
  * architecture cap. No layout logic lives here.
5
5
  */
6
6
 
7
- import type { SettingEntry, McpEntry, SubscriptionEntry } from '../input/settings-modal-types.ts';
7
+ import type { SettingEntry, McpEntry, SubscriptionEntry, SettingsCategory } from '../input/settings-modal-types.ts';
8
8
  import { SETTINGS_CATEGORIES } from '../input/settings-modal-types.ts';
9
9
  import { isSecretConfigKey, isSecretReferenceValue } from '../config/secret-config.ts';
10
10
  import { UI_TONES } from './ui-primitives.ts';
@@ -61,6 +61,58 @@ export function inferSubscriptionRouteReason(entry: SubscriptionEntry): string |
61
61
  return undefined;
62
62
  }
63
63
 
64
+ export const CATEGORY_INFO: Record<SettingsCategory, string> = {
65
+ display: 'Presentation settings for the terminal transcript: streaming, line numbers, thinking visibility, reasoning summaries, token speed, and tool previews.',
66
+ ui: 'Controls where operational messages render and whether voice surfaces are enabled. These settings change visibility, not provider behavior.',
67
+ provider: 'Default model routing for normal chat turns, embeddings, reasoning effort, and persistent system prompt file.',
68
+ pricing: 'Manual model prices (USD per 1M tokens, keyed provider:model). Your price wins over provider-served and catalog prices in every cost display, immediately.',
69
+ update: 'Self-update posture: whether the daemon checks hourly and updates at an idle moment, the release source it checks, and the check cadence. Clients update at launch; every swap keeps the previous binary for one-command rollback.',
70
+ subscriptions: 'Provider subscription login state and routing posture. Active sessions can be reviewed or signed out here; API keys remain managed through secrets.',
71
+ behavior: 'Day-to-day shell behavior: approval posture, compaction, history, guidance, notifications, stale-context warnings, return context, and Human-in-the-Loop mode.',
72
+ storage: 'Local storage posture, including secret storage policy and maximum artifact size for knowledge/home graph/document ingestion.',
73
+ atRest: 'Data-at-rest policy for the on-disk transcript journal and execution ledger: whether secret/credential patterns are redacted at write time, and the age/size caps that trigger pruning.',
74
+ permissions: 'Permission mode and tool-class policy. These settings decide whether the shell prompts before read/write/exec/network/agent actions.',
75
+ orchestration: 'Agent orchestration limits and recursion controls.',
76
+ planner: 'How /workstream decomposes a goal into work items: agent-driven decomposition (with heuristic fallback) or the forced heuristic path, plus the planning agent\'s turn, token, and wall-clock bounds.',
77
+ wrfc: 'Work-review-fix-cycle thresholds, retry limits, and automatic commit behavior.',
78
+ helper: 'Helper model defaults used by helper subsystems when they do not use the main chat route.',
79
+ tts: 'Text-to-speech provider, voice, and optional spoken-turn LLM overrides.',
80
+ voice: 'Two independent voice capabilities. Local engines (voice.local.*): the STT and TTS engine, binary, and model-path for whisper.cpp/faster-whisper and Piper/Kokoro — all empty by default, configurable-not-configured, nothing auto-downloads, and an unconfigured machine reports honestly (never an error); once set, "Local engines" appears in the TTS provider picker beside elevenlabs, with no billing dimension. Wake-word detection (voice.wake.*, rendered as its own unit below): listening continuously for a spoken wake phrase and handing the utterance that follows to speech-to-text — off by default, because holding a microphone open is meant to be an explicit act. Its published recall figures are measured on synthesised speech only, so no human recording of the phrase is behind them.',
81
+ service: 'Background service posture: enabled state, autostart, restart behavior, service name, platform, and logs.',
82
+ daemon: 'Local session daemon. It hosts the shared session broker and companion chat so a session started here is visible and steerable from other surfaces. On by default, bound to loopback (127.0.0.1) only.',
83
+ controlPlane: 'Daemon control-plane settings for local admin/API access.',
84
+ httpListener: 'HTTP listener settings for webhook and integration ingress.',
85
+ web: 'Browser surface settings for the local or network web UI.',
86
+ batch: 'Batch execution settings, including local vs Cloudflare queue behavior.',
87
+ automation: 'Scheduled and automated run settings, concurrency, timeout, catch-up, cooldown, and retention behavior.',
88
+ checkin: 'Proactive check-in: on a cadence, a briefing is judged and you are contacted only when something warrants it. Off by default; every run — scheduled or manual — leaves a receipt (see /checkin) recording whether it stayed quiet, delivered a message, or was skipped.',
89
+ watchers: 'File/process watcher heartbeat, polling, and recovery-window behavior, plus the trigger family (watchers.triggers.*, rendered as its own unit): stream watchers that regex-filter and batch a long-lived command\'s output, model-free condition checks running a probe/extract/rule pipeline with no LLM in the loop, and one-shot on-exit triggers that fire exactly one payload when a launched command terminates. The trigger family is off by default — a trigger supervises real processes with nobody watching — and its rows tune the backoff ladder, the strike breaker, retention bounds, batching, and process caps.',
90
+ runtime: 'Runtime guardrails such as companion chat limiter and event bus listener caps.',
91
+ telemetry: 'Telemetry payload policy.',
92
+ cache: 'Provider and model cache behavior, TTL, and hit-rate monitoring.',
93
+ diagnostics: 'Post-edit diagnostics: whether the shell runs language diagnostics on a file after the model edits or writes it, surfacing new problems inline.',
94
+ mcp: 'MCP server trust and scope review. Trust changes can expose local files, tools, databases, browsers, or remote automation depending on the server.',
95
+ sandbox: 'Isolation strategy for REPL, MCP, Windows, and QEMU-backed execution. Use these settings to separate risky tools from the host shell.',
96
+ surfaces: 'External app surfaces such as Slack, Discord, ntfy, Home Assistant, Telegram, webhooks, chat bridges, and messaging providers.',
97
+ device: 'Posture for using a PAIRED phone as a tool — its cameras, its screen, its location, its clipboard, and a small set of device effects. Two rows change behaviour today. device.capabilities.mode is the live switch: off stops every capability request reaching any paired device, ask-every-time asks on every single request and never consults a durable grant, and honor-grants (stock) asks the first time and every time after unless you chose "always allow" for that one capability on that one phone. device.nodes.maxPaired is enforced at the moment a device pairs: at the cap a NEW device is refused with a message naming the setting, the cap and how many are already paired, while a phone that is already in the list simply supersedes its own record and keeps working. Lowering the cap below the number already paired unpairs no one — existing devices keep working and only the next new pairing is refused; a device moving off the legacy shared token to its own is exempt so it cannot be stranded; and a nonsensical cap (zero, negative, not a number) is read as no cap at all, because a broken setting must never lock you out of your own daemon. The cap is re-read at every pairing, so a change applies without a restart. The remaining rows state the posture the capability service is built to honour — which capabilities may be granted durably, how long a phone has to answer, how exact a location it reports, whether the clipboard can be read, how long captures and grants live, and how often housekeeping sweeps. They are recorded and read back exactly as you set them, but no surface in THIS build maps them into a running capability session — the capability service currently runs on its own matching defaults. Your values are kept and take effect in the release that wires the session up.',
98
+ conversationGate: 'What a message arriving from one of those channels does. By default it gets a conversational answer, and work is proposed and waits for your agreement rather than starting on its own; you can instead confirm every run, or restore the old behavior where a message starts work immediately. Also how long a pending proposal stays answerable and how many can wait at once. This governs messages from channels only — what you type here always runs, and schedules and triggers were authorized when created.',
99
+ cloudflare: 'Optional Cloudflare control plane, batch queue, Worker, Tunnel, Access, DNS, KV, Durable Objects, Secrets Store, and R2 settings.',
100
+ release: 'Release-channel preference.',
101
+ power: 'Host sleep ownership. Keep-awake holds the host awake (the always-visible "sleep disabled" chip is the safety mechanism — no timer, no AC-only option); automatic inhibition keeps the host awake while real work runs, with an honest hard cap on that work hold.',
102
+ memory: 'Memory provenance surfacing. Whether turns that drew on your standing memories show a small "used N memories" chip with a drill-in listing them. Off by default — when off, nothing is rendered and no context is added.',
103
+ danger: 'High-impact switches for daemon and HTTP listener behavior. These are operational overrides, not normal preferences.',
104
+ tools: 'Tool LLM and helper model routing. Empty provider/model values inherit the active chat route unless a specific helper/tool route is set.',
105
+ network: 'Combined network view for daemon control-plane, HTTP listener, browser web surface, and general outbound network settings.',
106
+ fetch: 'Fetch response sanitization and host trust tiers for the fetch tool: sanitize mode plus default trusted/blocked host lists. Gated by the Fetch Response Sanitization feature.',
107
+ agents: 'Sub-agent context-window awareness and per-turn passive knowledge/code injection: token budget, relevance floor, code-chunk limit, and the compaction threshold. Gated by the agent context/injection features.',
108
+ security: 'API token scope and rotation auditing: rotation cadence, warning lead time, and whether overdue/over-scoped tokens are blocked or only reported. Gated by the Token Scope and Rotation Audit feature.',
109
+ integrations: 'Integration delivery reliability (Slack/Discord/webhook): retry counts, backoff bounds, dead-letter queue size, and SLO enforcement. Gated by the Integration Delivery SLO feature.',
110
+ policy: 'Policy-as-code bundle loading: where the startup policy bundle is loaded from and its file path. Gated by the Policy-as-Code feature.',
111
+ notifications: 'Notification router burst-suppression tuning: burst window, threshold, and cooldown. Gated by the Adaptive Notification Suppression feature.',
112
+ relay: 'Outbound zero-knowledge relay reachability, for reaching this daemon from outside the LAN without opening an inbound port. Off by default — relay.enabled is the switch. The relay operator sees only ciphertext and connection metadata — self-host your own relay for full control.',
113
+ learning: 'Idle-time memory consolidation: merging duplicate standing memory records and decaying/archiving stale ones. Off by default — nothing runs until enabled.',
114
+ };
115
+
64
116
  export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], string> = {
65
117
  display: 'Display',
66
118
  ui: 'UI',
@@ -94,6 +146,8 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
94
146
  mcp: 'MCP',
95
147
  sandbox: 'Sandbox',
96
148
  surfaces: 'Surfaces',
149
+ device: 'Paired Devices',
150
+ conversationGate: 'Channel Message Handling',
97
151
  cloudflare: 'Cloudflare',
98
152
  release: 'Release',
99
153
  power: 'Power',
@@ -148,7 +202,7 @@ const SETTING_LABELS: Partial<Record<string, string>> = {
148
202
  'controlPlane.hostMode': 'CP Host Mode',
149
203
  'controlPlane.host': 'CP Host',
150
204
  'controlPlane.port': 'CP Port',
151
- 'controlPlane.baseUrl': 'CP Base URL',
205
+ 'controlPlane.publicBaseUrl': 'CP Public Base URL',
152
206
  'controlPlane.streamMode': 'CP Stream Mode',
153
207
  'controlPlane.allowRemote': 'CP Allow Remote',
154
208
  'controlPlane.trustProxy': 'CP Trust Proxy',
@@ -10,7 +10,7 @@ import type { SettingsModal, SettingEntry, McpEntry, SubscriptionEntry, Settings
10
10
  import { SETTINGS_CATEGORIES, SETTINGS_CATEGORY_GROUPS } from '../input/settings-modal.ts';
11
11
  import { FEATURE_SETTINGS_BY_ID, isFeatureValueEnabled } from '../runtime/feature-settings.ts';
12
12
  import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
13
- import { CATEGORY_LABELS, describeUiRouting, formatValue, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
13
+ import { CATEGORY_INFO, CATEGORY_LABELS, describeUiRouting, formatValue, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
14
14
  import { isSecretConfigKey } from '../config/secret-config.ts';
15
15
  import { GLYPHS } from './ui-primitives.ts';
16
16
  import { formatHints, joinHints } from './hint-grammar.ts';
@@ -24,55 +24,6 @@ import {
24
24
  type WorkspaceRow,
25
25
  } from './fullscreen-workspace.ts';
26
26
 
27
- const CATEGORY_INFO: Record<SettingsCategory, string> = {
28
- display: 'Presentation settings for the terminal transcript: streaming, line numbers, thinking visibility, reasoning summaries, token speed, and tool previews.',
29
- ui: 'Controls where operational messages render and whether voice surfaces are enabled. These settings change visibility, not provider behavior.',
30
- provider: 'Default model routing for normal chat turns, embeddings, reasoning effort, and persistent system prompt file.',
31
- pricing: 'Manual model prices (USD per 1M tokens, keyed provider:model). Your price wins over provider-served and catalog prices in every cost display, immediately.',
32
- update: 'Self-update posture: whether the daemon checks hourly and updates at an idle moment, the release source it checks, and the check cadence. Clients update at launch; every swap keeps the previous binary for one-command rollback.',
33
- subscriptions: 'Provider subscription login state and routing posture. Active sessions can be reviewed or signed out here; API keys remain managed through secrets.',
34
- behavior: 'Day-to-day shell behavior: approval posture, compaction, history, guidance, notifications, stale-context warnings, return context, and Human-in-the-Loop mode.',
35
- storage: 'Local storage posture, including secret storage policy and maximum artifact size for knowledge/home graph/document ingestion.',
36
- atRest: 'Data-at-rest policy for the on-disk transcript journal and execution ledger: whether secret/credential patterns are redacted at write time, and the age/size caps that trigger pruning.',
37
- permissions: 'Permission mode and tool-class policy. These settings decide whether the shell prompts before read/write/exec/network/agent actions.',
38
- orchestration: 'Agent orchestration limits and recursion controls.',
39
- planner: 'How /workstream decomposes a goal into work items: agent-driven decomposition (with heuristic fallback) or the forced heuristic path, plus the planning agent\'s turn, token, and wall-clock bounds.',
40
- wrfc: 'Work-review-fix-cycle thresholds, retry limits, and automatic commit behavior.',
41
- helper: 'Helper model defaults used by helper subsystems when they do not use the main chat route.',
42
- tts: 'Text-to-speech provider, voice, and optional spoken-turn LLM overrides.',
43
- voice: 'Free, offline local voice engines (voice.local.*): the STT and TTS engine, binary, and model-path for whisper.cpp/faster-whisper and Piper/Kokoro. All empty by default — configurable-not-configured, nothing auto-downloads, and an unconfigured machine reports honestly (never an error). Once set, "Local engines" appears in the TTS provider picker beside elevenlabs, with no billing dimension.',
44
- service: 'Background service posture: enabled state, autostart, restart behavior, service name, platform, and logs.',
45
- daemon: 'Local session daemon. It hosts the shared session broker and companion chat so a session started here is visible and steerable from other surfaces. On by default, bound to loopback (127.0.0.1) only.',
46
- controlPlane: 'Daemon control-plane settings for local admin/API access.',
47
- httpListener: 'HTTP listener settings for webhook and integration ingress.',
48
- web: 'Browser surface settings for the local or network web UI.',
49
- batch: 'Batch execution settings, including local vs Cloudflare queue behavior.',
50
- automation: 'Scheduled and automated run settings, concurrency, timeout, catch-up, cooldown, and retention behavior.',
51
- checkin: 'Proactive check-in: on a cadence, a briefing is judged and you are contacted only when something warrants it. Off by default; every run — scheduled or manual — leaves a receipt (see /checkin) recording whether it stayed quiet, delivered a message, or was skipped.',
52
- watchers: 'File/process watcher heartbeat, polling, and recovery-window behavior.',
53
- runtime: 'Runtime guardrails such as companion chat limiter and event bus listener caps.',
54
- telemetry: 'Telemetry payload policy.',
55
- cache: 'Provider and model cache behavior, TTL, and hit-rate monitoring.',
56
- diagnostics: 'Post-edit diagnostics: whether the shell runs language diagnostics on a file after the model edits or writes it, surfacing new problems inline.',
57
- mcp: 'MCP server trust and scope review. Trust changes can expose local files, tools, databases, browsers, or remote automation depending on the server.',
58
- sandbox: 'Isolation strategy for REPL, MCP, Windows, and QEMU-backed execution. Use these settings to separate risky tools from the host shell.',
59
- surfaces: 'External app surfaces such as Slack, Discord, ntfy, Home Assistant, Telegram, webhooks, chat bridges, and messaging providers.',
60
- cloudflare: 'Optional Cloudflare control plane, batch queue, Worker, Tunnel, Access, DNS, KV, Durable Objects, Secrets Store, and R2 settings.',
61
- release: 'Release-channel preference.',
62
- power: 'Host sleep ownership. Keep-awake holds the host awake (the always-visible "sleep disabled" chip is the safety mechanism — no timer, no AC-only option); automatic inhibition keeps the host awake while real work runs, with an honest hard cap on that work hold.',
63
- memory: 'Memory provenance surfacing. Whether turns that drew on your standing memories show a small "used N memories" chip with a drill-in listing them. Off by default — when off, nothing is rendered and no context is added.',
64
- danger: 'High-impact switches for daemon and HTTP listener behavior. These are operational overrides, not normal preferences.',
65
- tools: 'Tool LLM and helper model routing. Empty provider/model values inherit the active chat route unless a specific helper/tool route is set.',
66
- network: 'Combined network view for daemon control-plane, HTTP listener, browser web surface, and general outbound network settings.',
67
- fetch: 'Fetch response sanitization and host trust tiers for the fetch tool: sanitize mode plus default trusted/blocked host lists. Gated by the Fetch Response Sanitization feature.',
68
- agents: 'Sub-agent context-window awareness and per-turn passive knowledge/code injection: token budget, relevance floor, code-chunk limit, and the compaction threshold. Gated by the agent context/injection features.',
69
- security: 'API token scope and rotation auditing: rotation cadence, warning lead time, and whether overdue/over-scoped tokens are blocked or only reported. Gated by the Token Scope and Rotation Audit feature.',
70
- integrations: 'Integration delivery reliability (Slack/Discord/webhook): retry counts, backoff bounds, dead-letter queue size, and SLO enforcement. Gated by the Integration Delivery SLO feature.',
71
- policy: 'Policy-as-code bundle loading: where the startup policy bundle is loaded from and its file path. Gated by the Policy-as-Code feature.',
72
- notifications: 'Notification router burst-suppression tuning: burst window, threshold, and cooldown. Gated by the Adaptive Notification Suppression feature.',
73
- relay: 'Outbound zero-knowledge relay reachability, for reaching this daemon from outside the LAN without opening an inbound port. Off by default — relay.enabled is the switch. The relay operator sees only ciphertext and connection metadata — self-host your own relay for full control.',
74
- learning: 'Idle-time memory consolidation: merging duplicate standing memory records and decaying/archiving stale ones. Off by default — nothing runs until enabled.',
75
- };
76
27
 
77
28
  const ENUM_VALUE_DESCRIPTIONS: Record<string, Record<string, string>> = {
78
29
  'display.themeMode': {
@@ -285,6 +236,14 @@ function buildFlagContext(entry: SettingEntry | null): string[] {
285
236
  `Feature: ${feature.id} (${feature.domain} domain)`,
286
237
  `Setting: ${feature.enablement.key} = ${formatValue(entry)}`,
287
238
  `State: ${displayState}`,
239
+ // A capability the registry declares not operable reads as disabled no
240
+ // matter what its settings key says. Without this line the row shows a
241
+ // value of true beside a state of disabled and explains nothing — the
242
+ // written reason exists in the registry, so render it where the user meets
243
+ // the contradiction rather than leaving it to be discovered.
244
+ ...(feature.operable === false && feature.inoperableDetail
245
+ ? [`Not available in this build: ${feature.inoperableDetail}`]
246
+ : []),
288
247
  `Default: ${feature.defaultEnabled ? 'enabled' : 'disabled'}`,
289
248
  `Applies: ${feature.restartRequired ? 'on next launch (startup-gated)' : 'immediately'}`,
290
249
  ...(pendingRestart
@@ -6,10 +6,20 @@ import { stripDangerousAnsi } from './ansi-sanitize.ts';
6
6
  import { formatElapsed } from '../utils/format-elapsed.ts';
7
7
  import { GLYPHS } from './ui-primitives.ts';
8
8
  import { activeUiTones } from './theme.ts';
9
+ import { treeBranchCol, treeContentCol, writeTreeStatusGutter } from './conversation-tree.ts';
9
10
 
10
11
  const TOOL_NAME_MIN_WIDTH = 8;
11
12
  const TOOL_NAME_MAX_WIDTH = 20;
12
13
 
14
+ /** Tree placement for a tool-call row rendered as a branch of its turn. */
15
+ export interface ToolCallTreeOptions {
16
+ /** Effective indent in columns, already clamped by treeIndentCols(). */
17
+ readonly indentCols?: number;
18
+ readonly branchFg?: string;
19
+ /** Suppress the tool label because the turn header already carries it. */
20
+ readonly omitToolName?: boolean;
21
+ }
22
+
13
23
  function writeStyledText(
14
24
  line: Line,
15
25
  startCol: number,
@@ -44,9 +54,18 @@ function buildLeftSegments(
44
54
  keyArg: string,
45
55
  suffixText: string,
46
56
  leftBudget: number,
57
+ omitToolName = false,
47
58
  ): Array<{ text: string; fg: string; bold?: boolean; dim?: boolean }> {
48
59
  if (leftBudget <= 0) return [];
49
60
 
61
+ // When every call in the turn shares one label, the label rides on the turn
62
+ // header and each row leads with the argument that actually distinguishes it
63
+ // (`config.get`, `services.restart`). Falls back to showing the name when
64
+ // there is no key argument, so a row can never render empty.
65
+ if (omitToolName && keyArg) {
66
+ return buildLeftSegments(keyArg, '', suffixText, leftBudget, false);
67
+ }
68
+
50
69
  const t = activeUiTones();
51
70
  const segments: Array<{ text: string; fg: string; bold?: boolean; dim?: boolean }> = [];
52
71
  const suffixBudget = suffixText ? Math.min(Math.max(12, Math.floor(leftBudget * 0.3)), 20) : 0;
@@ -164,13 +183,19 @@ export function renderToolCallBlock(
164
183
  errorMsg?: string,
165
184
  frameIndex?: number,
166
185
  startedAtMs?: number,
186
+ tree?: ToolCallTreeOptions,
167
187
  ): Line[] {
168
188
  const line = createEmptyLine(width);
169
189
  const t = activeUiTones();
170
- const margin = LAYOUT.LEFT_MARGIN;
190
+ const indent = Math.max(0, tree?.indentCols ?? 0);
191
+ // In tree mode the status icon moves to the branch's content column, so the
192
+ // ✓ lines up with the content of every other row at the same depth and the
193
+ // branch glyph sits where the parent's content began.
194
+ const margin = indent > 0 ? treeContentCol(indent) : LAYOUT.LEFT_MARGIN;
171
195
  const rightMargin = LAYOUT.RIGHT_MARGIN;
172
196
  const contentEnd = width - rightMargin;
173
197
 
198
+
174
199
  // Status icon. 'pending' means the tool is still awaiting a decision
175
200
  // (e.g. an approval prompt) and has NOT run yet — it uses the hollow idle
176
201
  // glyph so a not-yet-run tool never shows the completed green ✓ (2c).
@@ -207,10 +232,18 @@ export function renderToolCallBlock(
207
232
  : contentEnd;
208
233
  let col: number = leftStart;
209
234
 
210
- if (col < leftEndExclusive) {
211
- line[col] = createStyledCell(icon, { fg: iconColor, bold: status === 'done' || status === 'error' || status === 'cancelled' });
235
+ // In tree mode the status glyph goes in the fixed left gutter (column 0),
236
+ // before the indent, so every marker in the transcript aligns in one column
237
+ // no matter how deep its row sits. The row's own content then starts at the
238
+ // branch's content column with no inline icon slot.
239
+ if (indent > 0) {
240
+ writeTreeStatusGutter(line, icon, iconColor, width);
241
+ } else {
242
+ if (col < leftEndExclusive) {
243
+ line[col] = createStyledCell(icon, { fg: iconColor, bold: status === 'done' || status === 'error' || status === 'cancelled' });
244
+ }
245
+ col += 2; // icon + space
212
246
  }
213
- col += 2; // icon + space
214
247
 
215
248
  // Tool name — extract short name for long MCP tool names, but keep a readable dynamic width.
216
249
  const rawName = toolCall.name.includes('__')
@@ -225,7 +258,7 @@ export function renderToolCallBlock(
225
258
  ? `(${stripDangerousAnsi(resultSummary)})`
226
259
  : '';
227
260
  const leftBudget = Math.max(0, leftEndExclusive - col);
228
- const leftSegments = buildLeftSegments(rawName, keyArg, suffixText, leftBudget);
261
+ const leftSegments = buildLeftSegments(rawName, keyArg, suffixText, leftBudget, tree?.omitToolName ?? false);
229
262
  for (const segment of leftSegments) {
230
263
  col = writeStyledText(line, col, leftEndExclusive, segment.text, {
231
264
  fg: segment.fg,
@@ -282,7 +282,10 @@ function hostForUrl(host: string): string {
282
282
  }
283
283
 
284
284
  export function resolveCloudflareDaemonBaseUrl(configManager: Pick<ConfigManager, 'get'>): string {
285
- const configuredBaseUrl = String(configManager.get('controlPlane.baseUrl' as never) ?? '').trim();
285
+ // An explicitly DECLARED external address (tunnel / proxy) wins; otherwise the
286
+ // URL is built from the bind below. The old `controlPlane.baseUrl` this read
287
+ // was a stored mirror of the bind with no writers, so it drifted from it.
288
+ const configuredBaseUrl = String(configManager.get('controlPlane.publicBaseUrl' as never) ?? '').trim();
286
289
  if (configuredBaseUrl) return configuredBaseUrl.replace(/\/+$/, '');
287
290
  const host = hostForUrl(connectHostForBindHost(String(configManager.get('controlPlane.host' as never) ?? '127.0.0.1')));
288
291
  const portValue = Number(configManager.get('controlPlane.port' as never) ?? 3421);
@@ -73,6 +73,13 @@ export function featuresForEnablementKey(key: string): readonly FeatureSetting[]
73
73
  */
74
74
  export function isFeatureValueEnabled(feature: FeatureSetting, value: unknown): boolean {
75
75
  const { key, kind, enabledValues } = feature.enablement;
76
+ // A capability declared not operable in this build never reads as on, whatever
77
+ // its settings key says. deriveFeatureState already enforces this for the
78
+ // boolean and enum kinds; stating it once here covers the constant kind too,
79
+ // so no surface can show "on" for something that is doing nothing. The user's
80
+ // value is untouched — it is kept for the release that wires the capability up
81
+ // — and `feature.inoperableDetail` is the written reason a surface renders.
82
+ if (feature.operable === false) return false;
76
83
  if (kind === 'constant') {
77
84
  const schema = getConfigSchemaSetting(key);
78
85
  if (schema?.type === 'boolean') return value === true;
@@ -230,6 +230,18 @@ export const isPluginTerminal = operations.isPluginTerminal;
230
230
  export const resolveCapabilityManifest = operations.resolveCapabilityManifest;
231
231
  export const validateManifestV2 = operations.validateManifestV2;
232
232
  export const validatePluginSignature = operations.validatePluginSignature;
233
+ // Instance-type companions for the operations class aliases (see note above the
234
+ // security block): the `export const` form re-exports the value only.
235
+ export type AcpTaskAdapter = InstanceType<typeof operations.AcpTaskAdapter>;
236
+ export type OpsIllegalActionError = InstanceType<typeof operations.OpsIllegalActionError>;
237
+ export type OpsTargetNotFoundError = InstanceType<typeof operations.OpsTargetNotFoundError>;
238
+ export type ToolContractVerifier = InstanceType<typeof operations.ToolContractVerifier>;
239
+ export type McpLifecycleManager = InstanceType<typeof operations.McpLifecycleManager>;
240
+ export type McpPermissionManager = InstanceType<typeof operations.McpPermissionManager>;
241
+ export type McpSchemaFreshnessTracker = InstanceType<typeof operations.McpSchemaFreshnessTracker>;
242
+ export type PluginLifecycleManager = InstanceType<typeof operations.PluginLifecycleManager>;
243
+ export type PluginQuarantineEngine = InstanceType<typeof operations.PluginQuarantineEngine>;
244
+ export type PluginTrustStore = InstanceType<typeof operations.PluginTrustStore>;
233
245
  export const LOW_QUALITY_THRESHOLD = operations.LOW_QUALITY_THRESHOLD;
234
246
  export const computeQualityScore = operations.computeQualityScore;
235
247
  export const createCompactionManager = operations.createCompactionManager;
@@ -487,6 +499,15 @@ export const verifyBundle = security.verifyBundle;
487
499
  export const MAX_INPUT_LENGTH = security.MAX_INPUT_LENGTH;
488
500
  export const MAX_TOKEN_COUNT = security.MAX_TOKEN_COUNT;
489
501
 
502
+ // Instance-type companions for the class aliases above. `export const X = ns.X`
503
+ // re-exports only the value, so `X` is unusable in type position (TS2749/TS2694).
504
+ // These declaration-merge the instance type back onto the same name.
505
+ export type DivergenceDashboard = InstanceType<typeof security.DivergenceDashboard>;
506
+ export type DivergenceGateError = InstanceType<typeof security.DivergenceGateError>;
507
+ export type LayeredPolicyEvaluator = InstanceType<typeof security.LayeredPolicyEvaluator>;
508
+ export type PermissionSimulator = InstanceType<typeof security.PermissionSimulator>;
509
+ export type PolicySignatureError = InstanceType<typeof security.PolicySignatureError>;
510
+
490
511
  export type AuthInspectionSnapshot = Security.AuthInspectionSnapshot;
491
512
  export type ProviderAuthInspection = Security.ProviderAuthInspection;
492
513
  export type DivergenceDashboardSnapshot = Security.DivergenceDashboardSnapshot;
@@ -513,7 +534,7 @@ export type BundleProvenance = Security.BundleProvenance;
513
534
  export type DecisionReason = Security.DecisionReason;
514
535
  export type DivergenceReport = Security.DivergenceReport;
515
536
  export type EnforceGateResult = Security.EnforceGateResult;
516
- export type SignedPolicyBundle = Security.SignedPolicyBundle;
537
+ export type SignedPolicyBundle<T = unknown> = Security.SignedPolicyBundle<T>;
517
538
 
518
539
  export interface InspectableDomain {
519
540
  readonly name: string;
@@ -49,6 +49,7 @@ import { fileURLToPath } from 'node:url';
49
49
  import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
50
50
  import { PlatformServiceManager, type ManagedServiceStatus } from '@pellux/goodvibes-sdk/platform/daemon';
51
51
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
52
+ import { runDaemonConfigMigration } from '../config/run-daemon-config-migration.ts';
52
53
 
53
54
  /** Structurally derived from `PlatformServiceManager`'s own constructor — the
54
55
  * SDK's public `platform/daemon` entry point only re-exports the class and
@@ -113,6 +114,7 @@ export interface BuildManagedDaemonServiceManagerParams {
113
114
  */
114
115
  export function buildManagedDaemonServiceManager(params: BuildManagedDaemonServiceManagerParams): PlatformServiceManager {
115
116
  const workingDirectory = params.workingDirectory ?? params.homeDir;
117
+ if (!params.configManager) runDaemonConfigMigration(params.homeDir);
116
118
  const configManager = params.configManager ?? new ConfigManager({
117
119
  workingDir: workingDirectory,
118
120
  homeDir: params.homeDir,