@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
@@ -79,6 +79,116 @@ export const FEATURE_KNOB_LOCAL_SETTINGS = [
79
79
  'runtime.toolBudget.maxCostUsd',
80
80
  ] as const;
81
81
 
82
+ /**
83
+ * Config keys added by the paired-device and trigger-family domains in SDK
84
+ * 1.14.0. Like FEATURE_KNOB_LOCAL_SETTINGS above, these arrived as new
85
+ * CONFIG_SCHEMA entries that raised the settings inventory `total` with no
86
+ * matching behavior coverage, dropping `localBehaviorPercent` below its floor.
87
+ *
88
+ * THIS CONSTANT IS NOT A DIAL. Each key below is counted because a test that
89
+ * exercises it was actually written:
90
+ * `src/test/verification/device-and-trigger-settings-persistence.test.ts` runs,
91
+ * for every key in this list, the same four-part persistence contract the
92
+ * feature-knob keys are counted for — schema default exposure, `set()` through
93
+ * the key's own validator to disk, reload into a fresh ConfigManager with
94
+ * read-back equality, and reset-to-default that also survives reload. That test
95
+ * additionally asserts this list is exactly the non-enablement key set of both
96
+ * domains and overlaps neither of the other counted sets, so nothing here is
97
+ * double-counted and nothing drifts in uncounted.
98
+ *
99
+ * PER-KEY EVIDENCE — what is verified, and how live the key is TODAY.
100
+ * "persistence" below means the four-part contract above; the second column
101
+ * records whether anything in this tree (TUI or the pinned SDK) reads the key,
102
+ * audited by tracing each key to its consumers.
103
+ *
104
+ * watchers.triggers.backoffLadderMs persistence · live (trigger supervisor retry ladder)
105
+ * watchers.triggers.breakerStrikes persistence · live (strike breaker)
106
+ * watchers.triggers.defaultCheckIntervalMs persistence · live (condition-check scheduler)
107
+ * watchers.triggers.probeTimeoutMs persistence · live (probe execution)
108
+ * watchers.triggers.maxConcurrentChecks persistence · live (check concurrency cap)
109
+ * watchers.triggers.observationRingSize persistence · live (observation ring)
110
+ * watchers.triggers.runHistoryLimit persistence · live (run history retention)
111
+ * watchers.triggers.runHistoryTtlHours persistence · live (run history sweep)
112
+ * watchers.triggers.eventLogLimit persistence · live (event log retention)
113
+ * watchers.triggers.eventLogTtlHours persistence · live (event log sweep)
114
+ * watchers.triggers.sweepIntervalMs persistence · live (housekeeping cadence)
115
+ * watchers.triggers.supervisionTickMs persistence · live (supervision loop tick)
116
+ * watchers.triggers.streamQueueLimit persistence · live (stream watcher queue)
117
+ * watchers.triggers.streamBatchLines persistence · live (stream batching)
118
+ * watchers.triggers.streamBatchIntervalMs persistence · live (stream batching)
119
+ * watchers.triggers.onExitMaxDurationMs persistence · live (on-exit trigger duration cap)
120
+ * watchers.triggers.onExitStdin persistence · live (on-exit stdin posture)
121
+ * watchers.triggers.outputTailBytes persistence · live (captured output tail)
122
+ * device.capabilities.allowAlwaysOffer persistence · settings surface only in this build
123
+ * device.capabilities.requestTimeoutSeconds persistence · settings surface only in this build
124
+ * device.location.precision persistence · settings surface only in this build
125
+ * device.clipboard.readMode persistence · settings surface only in this build
126
+ * device.capture.retentionHours persistence · settings surface only in this build
127
+ * device.capture.maxArtifacts persistence · settings surface only in this build
128
+ * device.capture.sweepIntervalMinutes persistence · settings surface only in this build
129
+ * device.grants.expiryDays persistence · settings surface only in this build
130
+ * device.grants.maxPerNode persistence · settings surface only in this build
131
+ * device.grants.auditRetentionDays persistence · settings surface only in this build
132
+ * device.nodes.maxPaired persistence · live (enforced at the pairing path)
133
+ *
134
+ * On `device.nodes.maxPaired`: the SDK enforces it where a device pairs —
135
+ * PairingTokenManager.mint refuses a NEW node at the cap with
136
+ * PairingLimitReachedError (code DEVICE_NODES_MAX_PAIRED, carrying the setting
137
+ * name, the cap and the live count), mapped to HTTP 409 by the pairing and
138
+ * pairing-handoff routes. An already-paired node supersedes its own record
139
+ * instead of being refused, migration mints off the legacy shared token are
140
+ * deliberately exempt, lowering the cap unpairs nobody, and a non-positive or
141
+ * non-finite value reads as no cap so a broken setting cannot lock the owner
142
+ * out. The cap is read per mint, so a change applies without a restart. What is
143
+ * counted HERE is still only the persistence contract this repo's test
144
+ * exercises — the enforcement itself is SDK-side and SDK-tested, and is recorded
145
+ * in this column so the "how live is it" audit stays accurate, not to claim
146
+ * coverage this repo did not write.
147
+ *
148
+ * On the other ten `device.*` rows: DeviceCapabilityService carries its own
149
+ * policy struct whose defaults match those keys one for one, and names them in
150
+ * its field comments and refusal messages, but no code path in either tree maps
151
+ * configuration into that struct. Persistence is therefore the whole of what is
152
+ * verified for them, and the settings-workspace description for the `device`
153
+ * category says exactly that to the user rather than implying live knobs.
154
+ *
155
+ * DELIBERATELY NOT COUNTED: the 24 non-enablement `voice.wake.*` keys from the
156
+ * same release. `wake-word-detection` is declared `notOperable` — no surface
157
+ * captures audio — so its rows stay in `total` and out of the numerator. That is
158
+ * the honest reading, and it is why this list raises coverage by 29 and not 53.
159
+ */
160
+ export const DEVICE_AND_TRIGGER_LOCAL_SETTINGS = [
161
+ 'watchers.triggers.backoffLadderMs',
162
+ 'watchers.triggers.breakerStrikes',
163
+ 'watchers.triggers.defaultCheckIntervalMs',
164
+ 'watchers.triggers.probeTimeoutMs',
165
+ 'watchers.triggers.maxConcurrentChecks',
166
+ 'watchers.triggers.observationRingSize',
167
+ 'watchers.triggers.runHistoryLimit',
168
+ 'watchers.triggers.runHistoryTtlHours',
169
+ 'watchers.triggers.eventLogLimit',
170
+ 'watchers.triggers.eventLogTtlHours',
171
+ 'watchers.triggers.sweepIntervalMs',
172
+ 'watchers.triggers.supervisionTickMs',
173
+ 'watchers.triggers.streamQueueLimit',
174
+ 'watchers.triggers.streamBatchLines',
175
+ 'watchers.triggers.streamBatchIntervalMs',
176
+ 'watchers.triggers.onExitMaxDurationMs',
177
+ 'watchers.triggers.onExitStdin',
178
+ 'watchers.triggers.outputTailBytes',
179
+ 'device.capabilities.allowAlwaysOffer',
180
+ 'device.capabilities.requestTimeoutSeconds',
181
+ 'device.location.precision',
182
+ 'device.clipboard.readMode',
183
+ 'device.capture.retentionHours',
184
+ 'device.capture.maxArtifacts',
185
+ 'device.capture.sweepIntervalMinutes',
186
+ 'device.grants.expiryDays',
187
+ 'device.grants.maxPerNode',
188
+ 'device.grants.auditRetentionDays',
189
+ 'device.nodes.maxPaired',
190
+ ] as const;
191
+
82
192
  /**
83
193
  * Every feature's enablement settings key whose on/off writes round-trip
84
194
  * through a REAL on-disk ConfigManager in
@@ -95,8 +205,11 @@ const ENABLEMENT_KEYS_BEHAVIOR_VERIFIED = new Set(
95
205
  .filter((key) => !(FEATURE_KNOB_LOCAL_SETTINGS as readonly string[]).includes(key)),
96
206
  ).size;
97
207
 
98
- /** Settings with real local behavior verification: the authored baseline plus the persistence-tested feature-knob and enablement keys. */
99
- const SETTINGS_BEHAVIOR_VERIFIED = SETTINGS_BEHAVIOR_BASELINE + FEATURE_KNOB_LOCAL_SETTINGS.length + ENABLEMENT_KEYS_BEHAVIOR_VERIFIED;
208
+ /** Settings with real local behavior verification: the authored baseline plus the persistence-tested feature-knob, device/trigger, and enablement keys. */
209
+ const SETTINGS_BEHAVIOR_VERIFIED = SETTINGS_BEHAVIOR_BASELINE
210
+ + FEATURE_KNOB_LOCAL_SETTINGS.length
211
+ + DEVICE_AND_TRIGGER_LOCAL_SETTINGS.length
212
+ + ENABLEMENT_KEYS_BEHAVIOR_VERIFIED;
100
213
 
101
214
  const EXTERNAL_SLASH_COMMANDS = new Set([
102
215
  'auth',
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '1.21.0';
9
+ let _version = '1.22.0';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  // Only trust a version read from OUR OWN package.json. A Bun single-file
@@ -1,152 +0,0 @@
1
- /**
2
- * conversation-tool-groups.ts — detects runs of consecutive tool-result
3
- * messages that belong to one assistant turn, so the transcript can fold them
4
- * under a single collapsible group header instead of rendering one
5
- * "tool result" header per call. Before this module, an assistant turn with
6
- * N tool calls produced N independent header+body blocks in a row — visual
7
- * spam once N is more than one or two.
8
- *
9
- * A run only counts as a group when it has >= 2 matched tool-result messages:
10
- * a single result renders exactly as it always has (see
11
- * renderConversationToolMessage in conversation-rendering.ts, which only
12
- * consults this module's output when a membership entry exists).
13
- *
14
- * Pure function of the message slice — no rendering, no width, no side
15
- * effects. Called once per render pass (appendConversationMessages /
16
- * MessageLineCache.renderInto) over the currently-visible message slice, with
17
- * the absolute index of the slice's first message so group keys and member
18
- * indexes line up with the same collapseKey / blockRegistry indices a cold
19
- * render would produce (messages may be a post-clearDisplay slice, not the
20
- * full snapshot — see appendConversationMessages's msgIndexOffset doc).
21
- */
22
-
23
- import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
24
- import { renderExpandedToolResultLines } from '../renderer/tool-result-expanded-lines.ts';
25
-
26
- type Message = ConversationMessageSnapshot;
27
-
28
- /** One consolidated run of tool-result messages sharing one assistant turn. */
29
- export interface ToolGroup {
30
- /** Absolute index of the assistant message that owns this group. */
31
- readonly assistantIdx: number;
32
- /** Absolute indexes of the matched tool-result messages, in order. */
33
- readonly toolMessageIndexes: readonly number[];
34
- }
35
-
36
- /**
37
- * Per-tool-message membership, keyed by the ABSOLUTE index of the tool-result
38
- * message (the owning assistant message is never a key — only its tool-result
39
- * children are). `isFirst` marks the member whose render site owns the
40
- * group's header line and BlockMeta; every other member renders nothing while
41
- * the group is collapsed (see renderConversationToolMessage).
42
- */
43
- export interface ToolGroupMembership {
44
- readonly groupKey: string;
45
- readonly isFirst: boolean;
46
- /** Matched tool-result messages in the group (the header's "N tools"). */
47
- readonly toolCount: number;
48
- /** Sum of each member's EXPANDED render line count (the header's "N lines") —
49
- * the same post-render count each member's own "N lines" badge shows, so
50
- * the group total matches what expanding every member actually reveals,
51
- * not a raw-content estimate (a folded JSON result can pretty-print to far
52
- * more lines than its raw content has). */
53
- readonly totalLines: number;
54
- /** Each member's tool name, in order (falls back to 'tool' when a result
55
- * carries no name) — the header dedupes these into e.g. "read×3 exec". */
56
- readonly toolNames: readonly string[];
57
- /**
58
- * Absolute indexes of every tool-result message in the group, in order —
59
- * the same list for every member. Carried onto the group's BlockMeta so a
60
- * single expand pass can also open each member's own collapse key
61
- * (`msg_<idx>`), since a folded member pushes no BlockMeta of its own to
62
- * toggle individually (see local-runtime.ts's toggleBlocks).
63
- */
64
- readonly memberIndexes: readonly number[];
65
- }
66
-
67
- /**
68
- * Scan `messages` for assistant-message runs of >=2 consecutive matching
69
- * tool-result messages. A run breaks on the first message that isn't a
70
- * matching tool result: any non-tool message (interleaved user/system/
71
- * assistant content), or a tool message whose callId isn't one of the
72
- * assistant's own toolCalls ids (a result belonging to a different turn never
73
- * gets folded into a group it doesn't belong to).
74
- */
75
- export function detectToolGroups(
76
- messages: readonly Message[],
77
- indexOffset = 0,
78
- ): ToolGroup[] {
79
- const groups: ToolGroup[] = [];
80
-
81
- for (let i = 0; i < messages.length; i++) {
82
- const message = messages[i];
83
- if (message.role !== 'assistant' || !message.toolCalls || message.toolCalls.length === 0) continue;
84
- const callIds = new Set(
85
- message.toolCalls
86
- .map((tc) => tc.id)
87
- .filter((id): id is string => id !== undefined),
88
- );
89
- if (callIds.size === 0) continue;
90
-
91
- const toolMessageIndexes: number[] = [];
92
- let j = i + 1;
93
- while (j < messages.length) {
94
- const next = messages[j];
95
- if (next.role !== 'tool' || !next.callId || !callIds.has(next.callId)) break;
96
- toolMessageIndexes.push(indexOffset + j);
97
- j++;
98
- }
99
-
100
- if (toolMessageIndexes.length >= 2) {
101
- groups.push({ assistantIdx: indexOffset + i, toolMessageIndexes });
102
- }
103
- }
104
-
105
- return groups;
106
- }
107
-
108
- /** Build the per-tool-message membership lookup used by the renderer/cache.
109
- * `width` must match the width the transcript is actually rendered at — it
110
- * feeds the same expanded-render line count each member's own badge uses
111
- * (see renderExpandedToolResultLines), so the group total is never computed
112
- * against a different width than what the member badges show. */
113
- export function buildToolGroupMembership(
114
- groups: readonly ToolGroup[],
115
- messages: readonly Message[],
116
- indexOffset: number,
117
- width: number,
118
- ): ReadonlyMap<number, ToolGroupMembership> {
119
- const membership = new Map<number, ToolGroupMembership>();
120
- for (const group of groups) {
121
- const groupKey = `group_${group.assistantIdx}`;
122
- let totalLines = 0;
123
- const toolNames: string[] = [];
124
- for (const absIdx of group.toolMessageIndexes) {
125
- const message = messages[absIdx - indexOffset];
126
- if (message && message.role === 'tool') {
127
- totalLines += renderExpandedToolResultLines(message.content, width).length;
128
- toolNames.push(message.toolName ?? 'tool');
129
- }
130
- }
131
- group.toolMessageIndexes.forEach((absIdx, idx) => {
132
- membership.set(absIdx, {
133
- groupKey,
134
- isFirst: idx === 0,
135
- toolCount: group.toolMessageIndexes.length,
136
- totalLines,
137
- toolNames,
138
- memberIndexes: group.toolMessageIndexes,
139
- });
140
- });
141
- }
142
- return membership;
143
- }
144
-
145
- /** Convenience wrapper: detect groups, then build the membership map in one call. */
146
- export function computeToolGroupMembership(
147
- messages: readonly Message[],
148
- indexOffset = 0,
149
- width = 80,
150
- ): ReadonlyMap<number, ToolGroupMembership> {
151
- return buildToolGroupMembership(detectToolGroups(messages, indexOffset), messages, indexOffset, width);
152
- }