@pellux/goodvibes-tui 1.16.1 → 1.18.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 (257) hide show
  1. package/CHANGELOG.md +138 -9
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +5490 -1806
  4. package/package.json +7 -7
  5. package/src/cli/completions/generate.ts +5 -4
  6. package/src/cli/config-overrides.ts +51 -11
  7. package/src/cli/doctor.ts +4 -5
  8. package/src/cli/entrypoint.ts +13 -8
  9. package/src/cli/help.ts +7 -6
  10. package/src/cli/launch-auto-update.ts +214 -0
  11. package/src/cli/management-commands.ts +82 -15
  12. package/src/cli/management.ts +0 -2
  13. package/src/cli/parser.ts +5 -0
  14. package/src/cli/status.ts +13 -0
  15. package/src/cli/surface-command.ts +5 -5
  16. package/src/cli/tui-startup.ts +60 -97
  17. package/src/cli/types.ts +2 -0
  18. package/src/config/index.ts +1 -1
  19. package/src/config/tui-extension-settings.ts +33 -0
  20. package/src/core/composer-state.ts +1 -1
  21. package/src/core/consolidation-receipt.ts +41 -0
  22. package/src/core/conversation-line-cache.ts +3 -3
  23. package/src/core/conversation-rendering.ts +18 -9
  24. package/src/core/conversation.ts +2 -2
  25. package/src/core/long-task-notifier.ts +7 -7
  26. package/src/core/memory-provenance.ts +67 -0
  27. package/src/core/pairing-devices.ts +49 -0
  28. package/src/core/pairing-handoff.ts +101 -0
  29. package/src/core/pairing-offers.ts +83 -0
  30. package/src/core/pairing-origin.ts +78 -0
  31. package/src/core/pairing-tailscale-gateway.ts +56 -0
  32. package/src/core/power-chip-source.ts +91 -0
  33. package/src/core/power-status.ts +95 -0
  34. package/src/core/stable-host.ts +171 -0
  35. package/src/core/stream-event-wiring.ts +29 -6
  36. package/src/core/system-message-noise.ts +1 -1
  37. package/src/core/system-message-router.ts +6 -6
  38. package/src/core/turn-budget-outcome.ts +60 -0
  39. package/src/core/turn-cancellation.ts +40 -1
  40. package/src/core/turn-event-wiring.ts +14 -8
  41. package/src/daemon/cli.ts +64 -58
  42. package/src/daemon/lifecycle.ts +41 -0
  43. package/src/daemon/service-commands.ts +5 -5
  44. package/src/export/cost-utils.ts +128 -6
  45. package/src/input/autocomplete.ts +2 -2
  46. package/src/input/command-registry.ts +55 -9
  47. package/src/input/commands/acp-runtime.ts +72 -0
  48. package/src/input/commands/ci-runtime.ts +5 -1
  49. package/src/input/commands/codebase-runtime.ts +6 -6
  50. package/src/input/commands/cost-runtime.ts +1 -1
  51. package/src/input/commands/devices-runtime.ts +109 -0
  52. package/src/input/commands/git-runtime.ts +28 -4
  53. package/src/input/commands/graph-runtime.ts +45 -0
  54. package/src/input/commands/health-runtime.ts +2 -2
  55. package/src/input/commands/intelligence-runtime.ts +1 -1
  56. package/src/input/commands/local-auth-runtime.ts +1 -1
  57. package/src/input/commands/local-provider-runtime.ts +49 -10
  58. package/src/input/commands/local-runtime.ts +53 -5
  59. package/src/input/commands/memory.ts +2 -2
  60. package/src/input/commands/operator-panel-runtime.ts +5 -5
  61. package/src/input/commands/operator-rpc.ts +32 -11
  62. package/src/input/commands/permissions-provenance.ts +1 -1
  63. package/src/input/commands/permissions-runtime.ts +99 -8
  64. package/src/input/commands/planning-runtime.ts +3 -3
  65. package/src/input/commands/platform-sandbox-runtime.ts +1 -1
  66. package/src/input/commands/power-runtime.ts +56 -0
  67. package/src/input/commands/provider-accounts-runtime.ts +71 -6
  68. package/src/input/commands/provider.ts +7 -5
  69. package/src/input/commands/qrcode-runtime.ts +14 -19
  70. package/src/input/commands/queue-runtime.ts +70 -0
  71. package/src/input/commands/recall-files-git.ts +13 -51
  72. package/src/input/commands/recall-files.ts +8 -11
  73. package/src/input/commands/recall-review.ts +2 -2
  74. package/src/input/commands/relay-runtime.ts +1 -1
  75. package/src/input/commands/remote-runtime.ts +1 -1
  76. package/src/input/commands/runtime-services.ts +4 -4
  77. package/src/input/commands/schedule-runtime.ts +14 -7
  78. package/src/input/commands/services-runtime.ts +1 -1
  79. package/src/input/commands/session-workflow.ts +2 -2
  80. package/src/input/commands/settings-sync-runtime.ts +2 -2
  81. package/src/input/commands/share-runtime.ts +1 -1
  82. package/src/input/commands/shell-core.ts +46 -68
  83. package/src/input/commands/subscription-runtime.ts +1 -1
  84. package/src/input/commands/update-runtime.ts +153 -96
  85. package/src/input/commands/work-plan-runtime.ts +1 -1
  86. package/src/input/commands/workstream-attempts.ts +6 -1
  87. package/src/input/commands/workstream-runtime.ts +46 -27
  88. package/src/input/commands/worktree-runtime.ts +35 -8
  89. package/src/input/commands.ts +10 -2
  90. package/src/input/config-modal-types.ts +14 -3
  91. package/src/input/config-modal.ts +75 -13
  92. package/src/input/feature-unit-layout.ts +93 -82
  93. package/src/input/feed-context-factory.ts +2 -2
  94. package/src/input/handler-command-route.ts +2 -2
  95. package/src/input/handler-content-actions.ts +1 -1
  96. package/src/input/handler-feed-routes.ts +7 -7
  97. package/src/input/handler-feed.ts +4 -4
  98. package/src/input/handler-interactions.ts +3 -2
  99. package/src/input/handler-modal-routes.ts +12 -4
  100. package/src/input/handler-modal-token-routes.ts +1 -1
  101. package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
  102. package/src/input/handler-onboarding.ts +3 -2
  103. package/src/input/handler-picker-routes.ts +26 -13
  104. package/src/input/handler-shortcuts.ts +29 -6
  105. package/src/input/handler.ts +2 -2
  106. package/src/input/keybindings.ts +21 -3
  107. package/src/input/onboarding/onboarding-feature-units.ts +34 -27
  108. package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
  109. package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
  110. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
  111. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
  112. package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
  113. package/src/input/panel-integration-actions.ts +2 -2
  114. package/src/input/panel-mouse-geometry.ts +1 -1
  115. package/src/input/panel-paste-flood-guard.ts +1 -1
  116. package/src/input/provider-key-intake.ts +167 -0
  117. package/src/input/sandbox-exec-config.ts +2 -2
  118. package/src/input/selection-modal.ts +1 -1
  119. package/src/input/settings-modal-activation.ts +5 -0
  120. package/src/input/settings-modal-adjustment.ts +4 -31
  121. package/src/input/settings-modal-data.ts +52 -57
  122. package/src/input/settings-modal-mutations.ts +12 -68
  123. package/src/input/settings-modal-types.ts +27 -17
  124. package/src/input/settings-modal.ts +74 -55
  125. package/src/main.ts +90 -90
  126. package/src/panels/agent-inspector-shared.ts +4 -4
  127. package/src/panels/base-panel.ts +2 -2
  128. package/src/panels/builtin/agent.ts +4 -4
  129. package/src/panels/builtin/development.ts +4 -4
  130. package/src/panels/builtin/knowledge.ts +1 -1
  131. package/src/panels/builtin/operations.ts +83 -11
  132. package/src/panels/builtin/session.ts +5 -5
  133. package/src/panels/builtin/shared.ts +16 -4
  134. package/src/panels/builtin-modals.ts +59 -42
  135. package/src/panels/builtin-panels.ts +1 -1
  136. package/src/panels/cost-tracker-panel.ts +124 -7
  137. package/src/panels/diff-panel.ts +39 -6
  138. package/src/panels/eval-registry.ts +2 -2
  139. package/src/panels/fleet-acts.ts +446 -0
  140. package/src/panels/fleet-deep-link.ts +1 -1
  141. package/src/panels/fleet-gateway.ts +117 -0
  142. package/src/panels/fleet-observed-render.ts +128 -0
  143. package/src/panels/fleet-panel-format.ts +163 -11
  144. package/src/panels/fleet-panel-worktree-detail.ts +14 -0
  145. package/src/panels/fleet-panel.ts +73 -57
  146. package/src/panels/fleet-read-model.ts +84 -14
  147. package/src/panels/fleet-spawn.ts +266 -0
  148. package/src/panels/fleet-steer.ts +2 -2
  149. package/src/panels/fleet-stop.ts +41 -5
  150. package/src/panels/fleet-tabs.ts +15 -16
  151. package/src/panels/fleet-transcript.ts +5 -5
  152. package/src/panels/git-panel.ts +2 -2
  153. package/src/panels/index.ts +4 -1
  154. package/src/panels/local-auth-panel.ts +1 -1
  155. package/src/panels/memory-consolidation-gateway.ts +94 -0
  156. package/src/panels/modals/devices-modal.ts +161 -0
  157. package/src/panels/modals/hooks-modal.ts +1 -1
  158. package/src/panels/modals/keybindings-modal.ts +2 -2
  159. package/src/panels/modals/knowledge-modal.ts +1 -1
  160. package/src/panels/modals/marketplace-modal.ts +4 -4
  161. package/src/panels/modals/memory-modal.ts +153 -9
  162. package/src/panels/modals/modal-surface-helpers.ts +1 -1
  163. package/src/panels/modals/modal-theme.ts +1 -1
  164. package/src/panels/modals/pairing-modal.ts +198 -68
  165. package/src/panels/modals/planning-modal.ts +2 -2
  166. package/src/panels/modals/plugins-modal.ts +1 -1
  167. package/src/panels/modals/policy-modal.ts +1 -1
  168. package/src/panels/modals/security-modal.ts +7 -1
  169. package/src/panels/modals/skills-modal.ts +1 -1
  170. package/src/panels/modals/work-plan-modal.ts +3 -3
  171. package/src/panels/notifications-feed.ts +127 -0
  172. package/src/panels/notifications-panel.ts +160 -0
  173. package/src/panels/panel-manager.ts +9 -9
  174. package/src/panels/plugins-panel.ts +4 -4
  175. package/src/panels/project-planning-answer-actions.ts +1 -1
  176. package/src/panels/provider-health-routes.ts +2 -2
  177. package/src/panels/skills-panel.ts +4 -5
  178. package/src/panels/token-budget-panel.ts +11 -3
  179. package/src/panels/types.ts +5 -5
  180. package/src/panels/workstream-graph-render.ts +123 -0
  181. package/src/permissions/broker-approval-card.ts +201 -0
  182. package/src/permissions/hunk-selection.ts +28 -4
  183. package/src/permissions/prompt.ts +244 -29
  184. package/src/renderer/autocomplete-overlay.ts +107 -54
  185. package/src/renderer/code-block.ts +2 -2
  186. package/src/renderer/compaction-history-modal.ts +1 -1
  187. package/src/renderer/compaction-preview.ts +1 -1
  188. package/src/renderer/compaction-quality.ts +1 -1
  189. package/src/renderer/composer-fragments.ts +69 -0
  190. package/src/renderer/compositor.ts +2 -2
  191. package/src/renderer/config-modal.ts +1 -1
  192. package/src/renderer/conversation-overlays.ts +1 -1
  193. package/src/renderer/footer-tips.ts +3 -3
  194. package/src/renderer/hint-grammar.ts +1 -1
  195. package/src/renderer/modal-factory.ts +1 -1
  196. package/src/renderer/model-picker-overlay.ts +1 -1
  197. package/src/renderer/model-workspace.ts +7 -2
  198. package/src/renderer/panel-composite.ts +2 -2
  199. package/src/renderer/panel-workspace-bar.ts +1 -1
  200. package/src/renderer/process-indicator.ts +1 -1
  201. package/src/renderer/selection-modal-overlay.ts +1 -1
  202. package/src/renderer/session-picker-modal.ts +2 -2
  203. package/src/renderer/settings-modal-helpers.ts +23 -5
  204. package/src/renderer/settings-modal.ts +129 -39
  205. package/src/renderer/shell-surface.ts +6 -0
  206. package/src/renderer/terminal-bg-probe.ts +1 -1
  207. package/src/renderer/theme.ts +3 -3
  208. package/src/renderer/tool-call.ts +13 -7
  209. package/src/renderer/tool-result-summary.ts +1 -1
  210. package/src/renderer/turn-injection.ts +7 -6
  211. package/src/renderer/ui-factory.ts +38 -1
  212. package/src/runtime/bootstrap-command-context.ts +7 -2
  213. package/src/runtime/bootstrap-command-parts.ts +9 -5
  214. package/src/runtime/bootstrap-core.ts +41 -41
  215. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  216. package/src/runtime/bootstrap-shell.ts +13 -5
  217. package/src/runtime/bootstrap.ts +34 -32
  218. package/src/runtime/channel-composition.ts +46 -0
  219. package/src/runtime/code-index-services.ts +3 -4
  220. package/src/runtime/daemon-attach-notices.ts +120 -0
  221. package/src/runtime/durability-services.ts +90 -0
  222. package/src/runtime/embedded-service-factories.ts +40 -0
  223. package/src/runtime/feature-settings.ts +147 -0
  224. package/src/runtime/fleet-services.ts +41 -0
  225. package/src/runtime/idle-power-services.ts +85 -0
  226. package/src/runtime/index.ts +6 -21
  227. package/src/runtime/interaction-seams.ts +109 -0
  228. package/src/runtime/legacy-daemon-migration.ts +29 -19
  229. package/src/runtime/memory-fold.ts +1 -1
  230. package/src/runtime/notification-dispatch.ts +110 -0
  231. package/src/runtime/onboarding/apply.ts +3 -29
  232. package/src/runtime/onboarding/fast-path.ts +39 -6
  233. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  234. package/src/runtime/onboarding/snapshot.ts +0 -1
  235. package/src/runtime/onboarding/types.ts +9 -3
  236. package/src/runtime/power-keepawake-remote.ts +75 -0
  237. package/src/runtime/process-lifecycle.ts +3 -1
  238. package/src/runtime/provider-fallback.ts +55 -0
  239. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  240. package/src/runtime/release-artifacts.ts +19 -103
  241. package/src/runtime/resume-notice.ts +1 -1
  242. package/src/runtime/services.ts +113 -113
  243. package/src/runtime/session-continuity-hints.ts +38 -0
  244. package/src/runtime/surface-feature-flags.ts +40 -9
  245. package/src/runtime/terminal-output-guard.ts +2 -2
  246. package/src/runtime/trust/workspace-registration.ts +10 -2
  247. package/src/runtime/trust/workspace-trust.ts +58 -31
  248. package/src/runtime/ui-services.ts +6 -1
  249. package/src/runtime/update-check.ts +15 -69
  250. package/src/runtime/workstream-services.ts +2 -2
  251. package/src/shell/blocking-input.ts +108 -132
  252. package/src/shell/recovery-input-helpers.ts +53 -166
  253. package/src/shell/ui-openers.ts +38 -6
  254. package/src/utils/format-duration.ts +1 -1
  255. package/src/verification/verification-ledger.ts +21 -4
  256. package/src/version.ts +1 -1
  257. package/src/input/commands/flags-runtime.ts +0 -328
@@ -0,0 +1,171 @@
1
+ /**
2
+ * stable-host.ts — a stable name for printed/QR pairing links.
3
+ *
4
+ * The legacy resolution used the first non-internal IPv4 in enumeration order
5
+ * (see getLocalNetworkIp in management-utils.ts). That address is bound to the
6
+ * current DHCP lease: when the lease changes, every printed link and every QR
7
+ * that encoded it stops resolving. This module resolves a name that survives a
8
+ * lease change where one exists, and only falls back to a routable address when
9
+ * no stable name is available.
10
+ *
11
+ * Resolution ladder (highest preference first):
12
+ * 1. Tailscale MagicDNS name — preferred when tailscale is up. It resolves on
13
+ * and off the LAN and is never bound to a DHCP lease.
14
+ * 2. mDNS `<hostname>.local` — a LAN name a DHCP change does not break.
15
+ * 3. Gateway-routed interface address — the IPv4 on the default-route
16
+ * interface (not merely the first non-internal one, which can land on a
17
+ * bridge/VPN). The last resort; DHCP-bound.
18
+ * 4. Loopback — nothing routable enumerated.
19
+ *
20
+ * The resolver (resolveStableHost) is a pure function of injected inputs so the
21
+ * ladder is testable under mocked interface / tailscale states; the probes that
22
+ * gather those inputs are impure and bounded, and degrade to undefined rather
23
+ * than throw when a tool is missing or slow.
24
+ */
25
+ import { execFileSync } from 'node:child_process';
26
+ import { hostname as osHostname, networkInterfaces } from 'node:os';
27
+
28
+ export type StableHostKind = 'tailscale-magicdns' | 'mdns-local' | 'gateway-interface' | 'loopback';
29
+
30
+ export interface TailscaleState {
31
+ /** BackendState === 'Running'. */
32
+ readonly up: boolean;
33
+ /** Self.DNSName from `tailscale status --json`, trailing dot stripped. */
34
+ readonly magicDnsName?: string | undefined;
35
+ }
36
+
37
+ export interface StableHostInputs {
38
+ /** os.hostname(), possibly an FQDN — the `.local` name derives from its first label. */
39
+ readonly hostname: string;
40
+ /** The routable IPv4 on the default-route interface, when known. */
41
+ readonly gatewayInterfaceIp?: string | undefined;
42
+ /** First non-internal IPv4 in enumeration order — the legacy fallback address. */
43
+ readonly firstNonInternalIp?: string | undefined;
44
+ /** Tailscale state, when the CLI is present and answered in time. */
45
+ readonly tailscale?: TailscaleState | undefined;
46
+ }
47
+
48
+ export interface ResolvedStableHost {
49
+ readonly host: string;
50
+ readonly kind: StableHostKind;
51
+ /** True when `host` is a name that survives a DHCP lease change. */
52
+ readonly stable: boolean;
53
+ }
54
+
55
+ function stripTrailingDot(name: string): string {
56
+ return name.replace(/\.+$/, '');
57
+ }
58
+
59
+ /** `<hostname>.local` from the first label of a hostname, or null when there is no usable label. */
60
+ export function mdnsLocalName(host: string): string | null {
61
+ const label = (host ?? '').trim().split('.')[0]?.trim();
62
+ if (!label || label.toLowerCase() === 'localhost') return null;
63
+ // Already a `.local` name (hostname was set to the mDNS form): keep it as-is.
64
+ if (host.trim().toLowerCase().endsWith('.local')) return stripTrailingDot(host.trim());
65
+ return `${label}.local`;
66
+ }
67
+
68
+ /**
69
+ * Resolve the stable host from injected inputs. Pure — no I/O — so the ladder is
70
+ * exercised directly under mocked interface / tailscale states.
71
+ */
72
+ export function resolveStableHost(inputs: StableHostInputs): ResolvedStableHost {
73
+ const magic = inputs.tailscale?.up ? inputs.tailscale.magicDnsName?.trim() : undefined;
74
+ if (magic) return { host: stripTrailingDot(magic), kind: 'tailscale-magicdns', stable: true };
75
+
76
+ const local = mdnsLocalName(inputs.hostname);
77
+ if (local) return { host: local, kind: 'mdns-local', stable: true };
78
+
79
+ const routed = inputs.gatewayInterfaceIp?.trim() || inputs.firstNonInternalIp?.trim();
80
+ if (routed) return { host: routed, kind: 'gateway-interface', stable: false };
81
+
82
+ return { host: '127.0.0.1', kind: 'loopback', stable: false };
83
+ }
84
+
85
+ /** First non-internal IPv4 in enumeration order (the legacy fallback address). */
86
+ export function firstNonInternalIpv4(nets: ReturnType<typeof networkInterfaces>): string | undefined {
87
+ for (const name of Object.keys(nets)) {
88
+ for (const netInfo of nets[name] ?? []) {
89
+ if (netInfo.family === 'IPv4' && !netInfo.internal) return netInfo.address;
90
+ }
91
+ }
92
+ return undefined;
93
+ }
94
+
95
+ /** Parse the `src <ip>` field out of a Linux `ip route get` line. */
96
+ export function parseIpRouteSrc(output: string): string | undefined {
97
+ const match = /\bsrc\s+(\d+\.\d+\.\d+\.\d+)/.exec(output);
98
+ return match?.[1];
99
+ }
100
+
101
+ /** Parse `Self.DNSName` + running state out of `tailscale status --json` output. */
102
+ export function parseTailscaleStatus(json: string): TailscaleState | undefined {
103
+ try {
104
+ const parsed = JSON.parse(json) as { BackendState?: unknown; Self?: { DNSName?: unknown } };
105
+ const up = parsed.BackendState === 'Running';
106
+ const dnsName = typeof parsed.Self?.DNSName === 'string' ? stripTrailingDot(parsed.Self.DNSName) : undefined;
107
+ return { up, ...(dnsName ? { magicDnsName: dnsName } : {}) };
108
+ } catch {
109
+ return undefined;
110
+ }
111
+ }
112
+
113
+ function probeGatewayInterfaceIp(): string | undefined {
114
+ try {
115
+ const out = execFileSync('ip', ['route', 'get', '1.1.1.1'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 1500 });
116
+ return parseIpRouteSrc(out);
117
+ } catch {
118
+ return undefined;
119
+ }
120
+ }
121
+
122
+ function probeTailscaleState(): TailscaleState | undefined {
123
+ try {
124
+ const out = execFileSync('tailscale', ['status', '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 1500 });
125
+ return parseTailscaleStatus(out);
126
+ } catch {
127
+ return undefined;
128
+ }
129
+ }
130
+
131
+ /** Gather the impure inputs the resolver needs, each bounded and failure-tolerant. */
132
+ export function probeStableHostInputs(): StableHostInputs {
133
+ let nets: ReturnType<typeof networkInterfaces> = {};
134
+ try {
135
+ nets = networkInterfaces();
136
+ } catch {
137
+ nets = {};
138
+ }
139
+ const first = firstNonInternalIpv4(nets);
140
+ const gateway = probeGatewayInterfaceIp();
141
+ const tailscale = probeTailscaleState();
142
+ return {
143
+ hostname: safeHostname(),
144
+ ...(gateway ? { gatewayInterfaceIp: gateway } : {}),
145
+ ...(first ? { firstNonInternalIp: first } : {}),
146
+ ...(tailscale ? { tailscale } : {}),
147
+ };
148
+ }
149
+
150
+ function safeHostname(): string {
151
+ try {
152
+ return osHostname();
153
+ } catch {
154
+ return '';
155
+ }
156
+ }
157
+
158
+ /**
159
+ * The stable host for a printed/QR link given the endpoint's bind host. A
160
+ * wildcard bind (0.0.0.0 / ::) resolves through the ladder; an explicit bind
161
+ * host is honored as-is (loopback stays loopback). `probe` is injectable for
162
+ * tests.
163
+ */
164
+ export function stableUrlHostForBindHost(
165
+ host: string,
166
+ probe: () => StableHostInputs = probeStableHostInputs,
167
+ ): ResolvedStableHost {
168
+ if (host === '0.0.0.0' || host === '::') return resolveStableHost(probe());
169
+ const bound = host || '127.0.0.1';
170
+ return { host: bound, kind: bound === '127.0.0.1' ? 'loopback' : 'gateway-interface', stable: false };
171
+ }
@@ -24,6 +24,13 @@ export interface StreamMetrics {
24
24
  activeToolStartedAtMs: number | undefined;
25
25
  /** Name of the currently executing tool; cleared when execution completes. */
26
26
  activeToolName: string | undefined;
27
+ /**
28
+ * callId of the currently executing tool call; cleared when execution
29
+ * completes. This is the real orchestrator callId (from TOOL_EXECUTING),
30
+ * so a per-tool cancel affordance targets exactly the running call — never
31
+ * the synthetic 'live' render id.
32
+ */
33
+ activeToolCallId: string | undefined;
27
34
  /**
28
35
  * Epoch ms of the most recent STREAM_START or STREAM_DELTA; undefined when
29
36
  * idle (no turn in flight). Read every render frame — not just on the
@@ -49,6 +56,16 @@ export interface StreamMetrics {
49
56
  reconnectMaxAttempts: number | undefined;
50
57
  }
51
58
 
59
+ /** The idle initial StreamMetrics — mutated in place by wireStreamEventMetrics handlers. */
60
+ export function createStreamMetrics(): StreamMetrics {
61
+ return {
62
+ startTime: 0, deltaCount: 0, tokenSpeed: 0, ttftMs: undefined, ttftRecorded: false,
63
+ activeToolStartedAtMs: undefined, activeToolName: undefined, activeToolCallId: undefined,
64
+ lastDeltaAtMs: undefined, stallEpisode: 0,
65
+ reconnectAttempt: undefined, reconnectMaxAttempts: undefined,
66
+ };
67
+ }
68
+
52
69
  /** Minimal orchestrator surface required for stream token-speed calculation. */
53
70
  interface StreamOrchestrator {
54
71
  readonly streamingOutputTokens: number;
@@ -115,11 +132,11 @@ function isStreamRetryLikePayload(payload: unknown): payload is StreamRetryLikeP
115
132
 
116
133
  /**
117
134
  * Minimal cost lookup surface for attaching cost-delta information to failover notices.
118
- * Returns USD-per-1M-token pricing for the given model ID.
119
- * The implementation may consult a catalog; if the model is unknown both fields are 0.
135
+ * Returns USD-per-1M-token pricing for the given model ID, or null when the
136
+ * catalog honestly does not know the model (never a fabricated zero).
120
137
  */
121
138
  export interface FailoverCostLookup {
122
- getCostFromCatalog(modelId: string): { readonly input: number; readonly output: number };
139
+ getCostFromCatalog(modelId: string): { readonly input: number; readonly output: number } | null;
123
140
  }
124
141
 
125
142
  export interface WireStreamEventMetricsOptions {
@@ -219,9 +236,11 @@ function buildCostDeltaSuffix(
219
236
  // Registry key format: `provider:modelId` — modelId may itself contain `:`.
220
237
  const fromModelId = fromRegistryKey ? fromRegistryKey.split(':').slice(1).join(':') : '';
221
238
  const toModelId = toRegistryKey.split(':').slice(1).join(':');
222
- const fromCost = fromModelId ? lookup.getCostFromCatalog(fromModelId) : { input: 0, output: 0 };
223
- const toCost = lookup.getCostFromCatalog(toModelId);
224
- // Report unavailable when either side has zero pricing (unknown model).
239
+ // A null catalog answer means honestly unknown — treated exactly like the
240
+ // legacy zero-pricing sentinel below.
241
+ const fromCost = (fromModelId ? lookup.getCostFromCatalog(fromModelId) : null) ?? { input: 0, output: 0 };
242
+ const toCost = lookup.getCostFromCatalog(toModelId) ?? { input: 0, output: 0 };
243
+ // Report unavailable when either side has no pricing (unknown model).
225
244
  if (fromCost.input === 0 && fromCost.output === 0 && !fromModelId) {
226
245
  return ' [cost data unavailable]';
227
246
  }
@@ -441,6 +460,7 @@ export function wireStreamEventMetrics(
441
460
  unsubs.push(events.tools.on('TOOL_EXECUTING', (ev) => {
442
461
  metrics.activeToolStartedAtMs = ev.startedAt;
443
462
  metrics.activeToolName = ev.tool;
463
+ metrics.activeToolCallId = ev.callId;
444
464
  render();
445
465
  }));
446
466
  // On every tool-completion path, reset lastDeltaAtMs to "now" so the
@@ -454,16 +474,19 @@ export function wireStreamEventMetrics(
454
474
  unsubs.push(events.tools.on('TOOL_SUCCEEDED', () => {
455
475
  metrics.activeToolStartedAtMs = undefined;
456
476
  metrics.activeToolName = undefined;
477
+ metrics.activeToolCallId = undefined;
457
478
  metrics.lastDeltaAtMs = Date.now();
458
479
  }));
459
480
  unsubs.push(events.tools.on('TOOL_FAILED', () => {
460
481
  metrics.activeToolStartedAtMs = undefined;
461
482
  metrics.activeToolName = undefined;
483
+ metrics.activeToolCallId = undefined;
462
484
  metrics.lastDeltaAtMs = Date.now();
463
485
  }));
464
486
  unsubs.push(events.tools.on('TOOL_CANCELLED', () => {
465
487
  metrics.activeToolStartedAtMs = undefined;
466
488
  metrics.activeToolName = undefined;
489
+ metrics.activeToolCallId = undefined;
467
490
  metrics.lastDeltaAtMs = Date.now();
468
491
  }));
469
492
 
@@ -4,7 +4,7 @@
4
4
  * source these rules catch is emitted (from the SDK) through
5
5
  * SystemMessageRouter, so this one funnel is where first-run plumbing is kept
6
6
  * out of the user's stream while the information stays reachable elsewhere
7
- * (fleet panel, /health, /model, activity log). (UX-B item 1.)
7
+ * (fleet panel, /health, /model, activity log). (item 1.)
8
8
  */
9
9
 
10
10
  export interface NoiseGateDeps {
@@ -9,7 +9,7 @@
9
9
  * that target — plus whether a panel is attached — into a { toPanel,
10
10
  * toConversation } decision.
11
11
  *
12
- * W6.1 (the purge): the SystemMessagesPanel this router used to optionally
12
+ * (the purge): the SystemMessagesPanel this router used to optionally
13
13
  * push into was DELETE-disposition (no surviving human surface — a picker
14
14
  * over the old panel registry, not something worth a dedicated console) and
15
15
  * has been removed entirely, so this router now always resolves with
@@ -61,7 +61,7 @@ export type {
61
61
  SystemMessageTarget,
62
62
  } from '@/runtime/index.ts';
63
63
 
64
- /** Panel emphasis level. Panel delivery was removed in W6.1 (see file doc); kept as the priority vocabulary for callers and for the SDK's delivery-resolution signature. */
64
+ /** Panel emphasis level. Panel delivery was removed in (see file doc); kept as the priority vocabulary for callers and for the SDK's delivery-resolution signature. */
65
65
  export type SystemMessagePriority = 'high' | 'low';
66
66
 
67
67
  /**
@@ -88,7 +88,7 @@ function mustReachConversation(message: string): boolean {
88
88
 
89
89
  /**
90
90
  * Routes system messages to the conversation based on priority level and
91
- * configured target. See file doc for the W6.1 panel removal.
91
+ * configured target. See file doc for the panel removal.
92
92
  */
93
93
  export class SystemMessageRouter {
94
94
  /** Buffered provider "from last session" replay lines, folded on a microtask. */
@@ -123,7 +123,7 @@ export class SystemMessageRouter {
123
123
  kind: SystemMessageKind,
124
124
  ): void {
125
125
  // Noise gate — keep first-run plumbing out of the transcript while the
126
- // information stays reachable via other live surfaces. (UX-B item 1.)
126
+ // information stays reachable via other live surfaces. (item 1.)
127
127
  const verdict = classifyNoise(message, this.noiseDeps);
128
128
  if (verdict.action === 'drop') return;
129
129
  if (verdict.action === 'foldProviderReplay') {
@@ -137,7 +137,7 @@ export class SystemMessageRouter {
137
137
  /** Post-noise-gate delivery: resolve target and append to the conversation. */
138
138
  private deliver(message: string, kind: SystemMessageKind): void {
139
139
  const target = this.getTargetForKind(kind);
140
- // hasPanel is always false post-W6.1 — resolveSystemMessageDelivery's own
140
+ // hasPanel is always false now that panel delivery was removed — resolveSystemMessageDelivery's own
141
141
  // contract means every target ('panel' | 'conversation' | 'both')
142
142
  // resolves toConversation: true in that case (see file doc).
143
143
  const delivery = resolveSystemMessageDelivery(target, false);
@@ -153,7 +153,7 @@ export class SystemMessageRouter {
153
153
  * Buffer a provider "from last session" replay line and schedule a microtask
154
154
  * flush. The SDK emits the whole persisted-provider burst synchronously, so a
155
155
  * single microtask captures the full burst and folds it to one quiet line.
156
- * (UX-B item 1b.)
156
+ * (item 1b.)
157
157
  */
158
158
  private bufferProviderReplay(message: string): void {
159
159
  this.providerReplayBuffer.push(message);
@@ -0,0 +1,60 @@
1
+ /**
2
+ * turn-budget-outcome.ts — render a turn-budget exhaustion as an honest budget
3
+ * line, distinct from an infrastructure failure.
4
+ *
5
+ * When a member agent spends its whole turn budget the SDK stamps a typed
6
+ * outcome — failureKind 'max_turns' with the ceiling that applied and which
7
+ * input set it — instead of leaving consumers to regex a prose message. This
8
+ * renders that typed outcome in plain language: a budget was reached (not a
9
+ * crash), with the number and where the number came from.
10
+ */
11
+ export type TurnBudgetSource = 'default' | 'spawn-override' | 'policy-bound';
12
+
13
+ /** Plain-language provenance for a turn ceiling. */
14
+ export function describeTurnBudgetSource(source: TurnBudgetSource | undefined): string {
15
+ switch (source) {
16
+ case 'spawn-override':
17
+ return 'a per-spawn override';
18
+ case 'policy-bound':
19
+ return 'the policy cap (agents.maxTurnsCap)';
20
+ case 'default':
21
+ return 'the default limit (agents.maxTurns)';
22
+ default:
23
+ return 'the configured turn limit';
24
+ }
25
+ }
26
+
27
+ export interface TurnBudgetOutcome {
28
+ readonly limit?: number | undefined;
29
+ readonly source?: TurnBudgetSource | undefined;
30
+ }
31
+
32
+ /**
33
+ * A short budget line: "reached its turn budget — 50 turns (the default limit
34
+ * (agents.maxTurns))". Honest about the fact that the run stopped because it hit
35
+ * a ceiling, not because something broke.
36
+ */
37
+ export function formatTurnBudgetOutcome(outcome: TurnBudgetOutcome): string {
38
+ const source = describeTurnBudgetSource(outcome.source);
39
+ return outcome.limit !== undefined
40
+ ? `reached its turn budget — ${outcome.limit} turn${outcome.limit === 1 ? '' : 's'} (${source})`
41
+ : `reached its turn budget (${source})`;
42
+ }
43
+
44
+ /** The machine-readable failure reason the SDK stamps on a turn-budget exhaustion. */
45
+ export const TURN_BUDGET_FAILURE_REASON = 'max_turns';
46
+
47
+ export function isTurnBudgetReason(reason: string | null | undefined): boolean {
48
+ return reason === TURN_BUDGET_FAILURE_REASON;
49
+ }
50
+
51
+ /**
52
+ * Render a failure-reason string for a surface that only has the reason (not the
53
+ * typed limit/source event fields), mapping the machine-readable 'max_turns' to
54
+ * a plain budget phrase distinct from an infrastructure failure. Any other
55
+ * reason passes through unchanged.
56
+ */
57
+ export function describeFailureReason(reason: string | null | undefined): string {
58
+ if (!reason) return '';
59
+ return isTurnBudgetReason(reason) ? 'reached its turn budget' : reason;
60
+ }
@@ -5,7 +5,7 @@ import type { SpokenTurnRuntime } from '../audio/spoken-turn-wiring.ts';
5
5
  * Builds the shared "cancel the active turn" action: stops any in-flight
6
6
  * spoken output, then aborts the orchestrator's turn if one is actively
7
7
  * streaming. Extracted from main.ts so the file can stay under its 800-line
8
- * cap while still wiring `commandContext.isGenerating` (see W1.6 FIX 1).
8
+ * cap while still wiring `commandContext.isGenerating` (see FIX 1).
9
9
  *
10
10
  * Returns whether SPEECH was actively stopped — a Ctrl+C that silenced live
11
11
  * TTS is consumed by that job (an earlier replay fix), same as a press that
@@ -23,3 +23,42 @@ export function createCancelGeneration(
23
23
  return stoppedSpeech;
24
24
  };
25
25
  }
26
+
27
+ /** The narrow orchestrator surface the per-tool cancel needs. */
28
+ export interface ToolCancelOrchestrator {
29
+ /** Cancel ONE in-flight tool call; false when no such call is running. */
30
+ cancelToolCall(callId: string): boolean;
31
+ /** The callIds of tool calls currently in flight. */
32
+ listRunningToolCalls(): readonly string[];
33
+ }
34
+
35
+ /**
36
+ * Builds the "cancel the running tool call" action: stops JUST the currently-
37
+ * running tool call, leaving the turn to continue. The in-process orchestrator
38
+ * holds the per-call AbortController — this is the local-session equivalent of
39
+ * the sessions.toolCalls.cancel wire verb. The cancelled call settles as a
40
+ * structured "cancelled by user" result (partial output preserved) the model
41
+ * adapts to in the same turn.
42
+ *
43
+ * Target selection is honest: the tracked active callId (the live transcript
44
+ * row) when known, else the sole in-flight call when exactly one is running,
45
+ * else nothing (returns false — never guesses which of several to stop).
46
+ * `onCancelled` fires only on a real cancellation, with the callId stopped.
47
+ */
48
+ export function createCancelToolCall(
49
+ orchestrator: ToolCancelOrchestrator,
50
+ getActiveToolCallId: () => string | undefined,
51
+ onCancelled: (callId: string) => void,
52
+ ): () => boolean {
53
+ return () => {
54
+ let target = getActiveToolCallId();
55
+ if (target === undefined) {
56
+ const running = orchestrator.listRunningToolCalls();
57
+ if (running.length === 1) target = running[0];
58
+ }
59
+ if (target === undefined) return false;
60
+ const cancelled = orchestrator.cancelToolCall(target);
61
+ if (cancelled) onCancelled(target);
62
+ return cancelled;
63
+ };
64
+ }
@@ -1,6 +1,7 @@
1
1
  import type { UiRuntimeEvents, RuntimeEventBus } from '@/runtime/index.ts';
2
2
  import { buildPersistedSessionContext, persistConversation } from '@/runtime/index.ts';
3
3
  import { buildCompactionReceiptBlock } from './compaction-receipt.ts';
4
+ import { formatTurnBudgetOutcome } from './turn-budget-outcome.ts';
4
5
  import { persistTurnAnchors, recordTurnAnchor, summarizeTurnLabel } from './rewind-turn-anchors.ts';
5
6
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
6
7
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
@@ -78,12 +79,12 @@ export interface WireTurnEventHandlersOptions {
78
79
  */
79
80
  readonly webhookNotifier?: WebhookNotifier | null;
80
81
  /**
81
- * Terminal focus tracker (W2.3). Gates the long-task, budget-breach, and
82
+ * Terminal focus tracker. Gates the long-task, budget-breach, and
82
83
  * agent/chain-failure desktop alerts wired in this module — see
83
- * alert-gating.ts. Optional; when absent, none of the new W2.3 alert
84
- * behavior is gated by focus (long-task keeps its pre-W2.3 always-fire
85
- * behavior, and budget-breach/failure alerts are skipped entirely, since
86
- * they are new-in-W2.3 and have no pre-existing unconditional-fire path
84
+ * alert-gating.ts. Optional; when absent, none of the new alert
85
+ * behavior is gated by focus (long-task keeps its always-fire
86
+ * behavior from before focus gating existed, and budget-breach/failure alerts are
87
+ * skipped entirely, since they have no pre-existing unconditional-fire path
87
88
  * to fall back to).
88
89
  */
89
90
  readonly focusTracker?: Pick<FocusTracker, 'shouldAlertWhenUnfocused'> | null;
@@ -152,7 +153,7 @@ export function wireTurnEventHandlers(
152
153
  // Track turn start time for long-task notification threshold.
153
154
  let turnStartTime: number | null = null;
154
155
 
155
- // Budget-breach edge-trigger checker (W2.3) — one instance per session,
156
+ // Budget-breach edge-trigger checker — one instance per session,
156
157
  // piggybacking on the same TURN_COMPLETED handler as the long-task
157
158
  // notification below rather than adding a second TURN_COMPLETED subscription.
158
159
  const budgetBreachNotifier: BudgetBreachNotifier | null = focusTracker
@@ -193,7 +194,7 @@ export function wireTurnEventHandlers(
193
194
  focusTracker,
194
195
  configGet,
195
196
  });
196
- // Budget-breach alert (W2.3): edge-triggered, piggybacks on this same
197
+ // Budget-breach alert: edge-triggered, piggybacks on this same
197
198
  // TURN_COMPLETED handler rather than a second subscription.
198
199
  if (budgetBreachNotifier) {
199
200
  const sessionModel = providerRegistry.getCurrentModel().id ?? 'unknown';
@@ -264,7 +265,7 @@ export function wireTurnEventHandlers(
264
265
  refreshGit();
265
266
  }));
266
267
 
267
- // Agent/chain-failure desktop alerts (W2.3). The SDK's WebhookNotifier and
268
+ // Agent/chain-failure desktop alerts. The SDK's WebhookNotifier and
268
269
  // Notifier already fire webhook/Slack/Discord notifications for these two
269
270
  // events unconditionally (attachToRuntimeBus in the SDK's
270
271
  // platform/integrations — pre-existing, not focus-gated: an out-of-band
@@ -290,6 +291,11 @@ export function wireTurnEventHandlers(
290
291
  // chain/owner/cohort surfaces.
291
292
  if (payload.failureKind === 'cancelled') {
292
293
  notifyCompletion('GoodVibes — WRFC chain cancelled', `chain ${payload.chainId.slice(0, 12)} cancelled: ${payload.reason}`, FORCE_NOTIFY_DURATION_MS);
294
+ } else if (payload.failureKind === 'max_turns') {
295
+ // A turn-budget exhaustion is a spent ceiling, not an infrastructure
296
+ // failure — narrate the limit and where it came from, from the typed
297
+ // event fields, never a regex of the prose reason.
298
+ notifyCompletion('GoodVibes — WRFC chain hit its turn budget', `chain ${payload.chainId.slice(0, 12)} ${formatTurnBudgetOutcome({ limit: payload.turnLimit, source: payload.turnLimitSource })}`, FORCE_NOTIFY_DURATION_MS);
293
299
  } else {
294
300
  const kindLabel = payload.failureKind === 'transport' ? 'transient transport error' : payload.reason;
295
301
  notifyCompletion('GoodVibes — WRFC chain failed', `chain ${payload.chainId.slice(0, 12)} failed: ${kindLabel}`, FORCE_NOTIFY_DURATION_MS);