@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
package/src/cli/parser.ts CHANGED
@@ -87,6 +87,7 @@ function createDefaultFlags(): GoodVibesCliFlags {
87
87
  fork: undefined,
88
88
  yes: false,
89
89
  nonInteractive: false,
90
+ strict: false,
90
91
  };
91
92
  }
92
93
 
@@ -260,6 +261,10 @@ export function parseGoodVibesCli(
260
261
  flags = withFlag(withFlag(flags, 'nonInteractive', true), 'yes', true);
261
262
  continue;
262
263
  }
264
+ if (name === '--strict') {
265
+ flags = withFlag(flags, 'strict', true);
266
+ continue;
267
+ }
263
268
 
264
269
  if (name === '--provider') {
265
270
  const consumed = getValue(argv, index, inlineValue, name, errors);
package/src/cli/status.ts CHANGED
@@ -502,6 +502,19 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
502
502
  return findings;
503
503
  }
504
504
 
505
+ /**
506
+ * The doctor command's exit code. Advisory findings (`severity: 'warning'`)
507
+ * are notes on an otherwise-usable install and must never make a healthy
508
+ * install report failure — only a must-fix (`severity: 'risk'`) finding
509
+ * exits non-zero. `strict` (for CI) flips warnings to failures too, so a
510
+ * pipeline can require a fully clean report instead of just "usable".
511
+ */
512
+ export function resolveDoctorExitCode(findings: readonly CliDoctorFinding[], strict = false): number {
513
+ if (findings.some((finding) => finding.severity === 'risk')) return 1;
514
+ if (strict && findings.length > 0) return 1;
515
+ return 0;
516
+ }
517
+
505
518
  export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnapshot {
506
519
  const config = options.configManager;
507
520
  const controlPlaneBinding = resolveRuntimeEndpointBinding(config, 'controlPlane');
@@ -5,7 +5,7 @@ import {
5
5
  GOODVIBES_NTFY_REMOTE_TOPIC,
6
6
  resolveGoodVibesNtfyTopics,
7
7
  } from '@pellux/goodvibes-sdk/platform/integrations';
8
- import { enableFeatureFlags, getMissingSurfaceFeatureFlags, getServerSurfaceFeatureFlags } from '../runtime/surface-feature-flags.ts';
8
+ import { enableFeatureFlags, getMissingSurfaceFeatureFlags, getServerSurfaceFeatureFlags, surfaceFeatureGateSettingsKeys } from '../runtime/surface-feature-flags.ts';
9
9
  import { resolveRuntimeEndpointBinding } from './endpoints.ts';
10
10
  import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
11
11
  import type { CliCommandRuntime } from './types.ts';
@@ -147,7 +147,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
147
147
  readinessIssues.push(`${surface.label} is enabled but missing ${surface.missing.join(', ')}.`);
148
148
  }
149
149
  if (surface.missingFeatureFlags.length > 0) {
150
- readinessIssues.push(`${surface.label} is enabled but feature gates are disabled: ${surface.missingFeatureFlags.join(', ')}.`);
150
+ readinessIssues.push(`${surface.label} is enabled but these settings are off: ${surfaceFeatureGateSettingsKeys(surface.missingFeatureFlags).join(', ')}.`);
151
151
  }
152
152
  }
153
153
  const value = {
@@ -185,7 +185,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
185
185
  ` http-listener: ${yesNo(value.httpListener.enabled)} (${value.httpListener.hostMode} ${value.httpListener.host}:${value.httpListener.port})${includeProbe ? ` reachable=${yesNo(value.httpListener.reachable)}` : ''}`,
186
186
  '',
187
187
  'External surfaces:',
188
- ...value.surfaces.map((surface) => ` ${surface.label.padEnd(16)} enabled=${yesNo(surface.enabled)} ready=${yesNo(surface.ready)}${surface.enabled && surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.enabled && surface.missingFeatureFlags.length > 0 ? ` featureGates=${surface.missingFeatureFlags.join(',')}` : ''}`),
188
+ ...value.surfaces.map((surface) => ` ${surface.label.padEnd(16)} enabled=${yesNo(surface.enabled)} ready=${yesNo(surface.ready)}${surface.enabled && surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.enabled && surface.missingFeatureFlags.length > 0 ? ` settingsOff=${surfaceFeatureGateSettingsKeys(surface.missingFeatureFlags).join(',')}` : ''}`),
189
189
  ...(filteredSurfaces.some((surface) => surface.id === 'ntfy') ? [
190
190
  '',
191
191
  'ntfy inbound topics:',
@@ -260,7 +260,7 @@ export async function buildListenerTestResult(runtime: CliCommandRuntime): Promi
260
260
  if (isNetworkFacing(enabled, binding) && auth.bootstrapCredentialPresent) issues.push('Network-facing listener still has a bootstrap credential file.');
261
261
  for (const surface of surfaces) {
262
262
  if (surface.missing.length > 0) issues.push(`${surface.label} is enabled but missing ${surface.missing.join(', ')}.`);
263
- if (surface.missingFeatureFlags.length > 0) issues.push(`${surface.label} is enabled but feature gates are disabled: ${surface.missingFeatureFlags.join(', ')}.`);
263
+ if (surface.missingFeatureFlags.length > 0) issues.push(`${surface.label} is enabled but these settings are off: ${surfaceFeatureGateSettingsKeys(surface.missingFeatureFlags).join(', ')}.`);
264
264
  }
265
265
  return { enabled, ...binding, posture, reachable, service, auth, surfaces, issues };
266
266
  }
@@ -276,7 +276,7 @@ export function formatListenerTestResult(runtime: CliCommandRuntime, value: List
276
276
  ` local auth users: ${value.auth.userStorePresent ? 'present' : 'missing'}`,
277
277
  ` bootstrap credential: ${value.auth.bootstrapCredentialPresent ? 'present' : 'missing'}`,
278
278
  value.surfaces.length === 0 ? ' enabled webhook surfaces: none' : ' enabled webhook surfaces:',
279
- ...value.surfaces.map((surface) => ` ${surface.label}: ready=${yesNo(surface.ready)}${surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.missingFeatureFlags.length > 0 ? ` featureGates=${surface.missingFeatureFlags.join(',')}` : ''}`),
279
+ ...value.surfaces.map((surface) => ` ${surface.label}: ready=${yesNo(surface.ready)}${surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.missingFeatureFlags.length > 0 ? ` settingsOff=${surfaceFeatureGateSettingsKeys(surface.missingFeatureFlags).join(',')}` : ''}`),
280
280
  value.issues.length === 0 ? ' readiness: ready' : ' readiness: needs attention',
281
281
  ...value.issues.map((issue) => ` - ${issue}`),
282
282
  ].join('\n'));
@@ -1,6 +1,7 @@
1
1
  import type { CommandContext, CommandRegistry } from '../input/command-registry.ts';
2
2
  import type { InputHandler } from '../input/handler.ts';
3
3
  import type { SelectionItem } from '../input/selection-modal.ts';
4
+ import type { WorkspaceRegistrationManager } from '../runtime/trust/workspace-registration.ts';
4
5
  import { hasResumableWizardProgress, readOnboardingCheckMarker, readWizardProgress } from '../runtime/onboarding/index.ts';
5
6
  import { startOnboardingFastPath } from '../runtime/onboarding/fast-path.ts';
6
7
  import { readLastSessionPointer } from '@/runtime/index.ts';
@@ -13,111 +14,62 @@ export type TuiStartupShellPaths = Parameters<typeof readOnboardingCheckMarker>[
13
14
 
14
15
  export type FirstOpenTrustLevel = 'trusted' | 'restricted';
15
16
 
16
- /** The decisions a first-open prompt choice resolves to. Absent halves were not asked. */
17
- export interface FirstOpenDecision {
18
- readonly trust?: FirstOpenTrustLevel;
19
- readonly register?: 'yes' | 'no';
20
- }
21
-
22
17
  /**
23
- * decodeFirstOpenChoice — pure mapping from a chosen selection-item id (or null
24
- * on Escape/enter-through) to the trust + registration decisions, given which
25
- * halves the prompt actually offered. Kept pure so the combined-prompt semantics
26
- * are unit-testable without a live modal.
27
- *
28
- * Escape/enter-through (id === null) is the safe default: an offered trust half
29
- * settles to 'restricted' (matching the standalone trust prompt), and an offered
30
- * register half defaults to NO — recorded as a subtree-scoped decline so the
31
- * directory is not re-asked every startup.
18
+ * decodeFirstOpenChoice — pure mapping from a chosen selection-item id (or
19
+ * null on Escape/enter-through) to the trust decision. Kept pure so the
20
+ * consequence-time trust prompt's semantics (raised by trustGatedAsk, wired
21
+ * in main.ts's trustPromptRef.requestTrustDecision) are unit-testable
22
+ * without a live modal. Escape/enter-through (id === null) is the safe
23
+ * default: 'restricted'.
32
24
  */
33
- export function decodeFirstOpenChoice(
34
- id: string | null,
35
- offered: { readonly trustNeeded: boolean; readonly registerNeeded: boolean },
36
- ): FirstOpenDecision {
37
- const decision: { trust?: FirstOpenTrustLevel; register?: 'yes' | 'no' } = {};
38
- if (offered.trustNeeded && offered.registerNeeded) {
39
- // Combined 2x2 choice.
40
- const trust: FirstOpenTrustLevel = id === 'trust-register' || id === 'trust-only' ? 'trusted' : 'restricted';
41
- const register: 'yes' | 'no' = id === 'trust-register' || id === 'restrict-register' ? 'yes' : 'no';
42
- decision.trust = trust;
43
- decision.register = register;
44
- } else if (offered.trustNeeded) {
45
- decision.trust = id === 'trusted' ? 'trusted' : 'restricted';
46
- } else if (offered.registerNeeded) {
47
- decision.register = id === 'register' ? 'yes' : 'no';
48
- }
49
- return decision;
25
+ export function decodeFirstOpenChoice(id: string | null): FirstOpenTrustLevel {
26
+ return id === 'trusted' ? 'trusted' : 'restricted';
50
27
  }
51
28
 
52
- /** Build the selection rows for a first-open prompt given which halves apply. */
53
- export function buildFirstOpenItems(offered: {
54
- readonly trustNeeded: boolean;
55
- readonly registerNeeded: boolean;
56
- }): { readonly title: string; readonly items: SelectionItem[] } {
57
- if (offered.trustNeeded && offered.registerNeeded) {
58
- return {
59
- title: 'New workspace — trust level and registration',
60
- items: [
61
- { id: 'trust-register', label: 'Trust & register this workspace', detail: 'Full capability, and track it in your workspace registry', primaryAction: 'select' },
62
- { id: 'trust-only', label: 'Trust, don\'t register', detail: 'Full capability; leave it out of the registry', primaryAction: 'select' },
63
- { id: 'restrict-register', label: 'Keep restricted, register', detail: 'Read-only for now; still track it in the registry', primaryAction: 'select' },
64
- { id: 'restrict-only', label: 'Keep restricted, don\'t register', detail: 'Read-only exploration; nothing recorded (default)', primaryAction: 'select' },
65
- ],
66
- };
67
- }
68
- if (offered.trustNeeded) {
69
- return {
70
- title: 'New workspace — choose a trust level',
71
- items: [
72
- { id: 'trusted', label: 'Trust this workspace', detail: 'Full capability — all tools may run', primaryAction: 'select' },
73
- { id: 'restricted', label: 'Keep restricted (read-only)', detail: 'Explore safely; writes and commands are denied until trusted', primaryAction: 'select' },
74
- ],
75
- };
76
- }
29
+ /**
30
+ * Build the selection rows for the trust prompt. This used to also offer a
31
+ * "register this directory?" half (a combined 2x2 when both were needed) —
32
+ * that half is gone: registration self-records (see
33
+ * selfRecordWorkspaceRegistration below) rather than ever asking a question
34
+ * about a registry the user hasn't met yet.
35
+ */
36
+ export function buildFirstOpenItems(): { readonly title: string; readonly items: SelectionItem[] } {
77
37
  return {
78
- title: 'Register this directory as a workspace?',
38
+ title: 'New workspace — choose a trust level',
79
39
  items: [
80
- { id: 'register', label: 'Register this workspace', detail: 'Track this project root in your workspace registry', primaryAction: 'select' },
81
- { id: 'skip', label: 'Don\'t register', detail: 'Leave it out; not asked again for this directory (default)', primaryAction: 'select' },
40
+ { id: 'trusted', label: 'Trust this workspace', detail: 'Full capability — all tools may run', primaryAction: 'select' },
41
+ { id: 'restricted', label: 'Keep restricted (read-only)', detail: 'Explore safely; writes and commands are denied until trusted', primaryAction: 'select' },
82
42
  ],
83
43
  };
84
44
  }
85
45
 
86
46
  /**
87
- * First-open workspace prompt — one surface folding the TUI-local trust gate and
88
- * the platform-wide registration half. When GoodVibes opens a workspace it has
89
- * no trust decision for, and/or one whose registration resolves UNKNOWN, ask
90
- * once (never two stacked modals): trust level AND "register this directory?".
47
+ * Registration self-records: a workspace the user actually works in becomes
48
+ * its own registry entry with no question ever asked (the former "Register
49
+ * this directory as a workspace?" prompt — sometimes a four-option 2x2 when
50
+ * trust was undecided too — is gone entirely).
51
+ *
52
+ * "Actually works in" is anchored to TRUST, not mere directory-open: this is
53
+ * only ever called for a workspace that is (or has just become) trusted,
54
+ * never for one that's merely been glanced at read-only. Labeled 'via TUI'
55
+ * in the shared registry so its provenance is honest.
91
56
  *
92
- * The two records are independent: a grandfathered-trusted workspace skips the
93
- * trust half but may still see the register half once; a covered/declined/broad
94
- * directory skips the register half. Escape/enter-through takes the safe default
95
- * (restricted, and a recorded decline for the register half).
57
+ * OWNER-BOUNDARY RIDER (binding): self-recording must not widen the
58
+ * checkpoint-owning consumer's boundary. That boundary now reads the SAME
59
+ * shared registration store this writes to, gated by each record's
60
+ * `checkpointEligible` flag (absent means false). So the separation is no
61
+ * longer "a separate list the agent owns" — it is one store, and this self
62
+ * record stays out of scope by stamping its `origin` ('tui') for honest
63
+ * provenance while NEVER setting `checkpointEligible`. Only the consumer that
64
+ * owns checkpointing re-stamps its own roots eligible on boot.
96
65
  */
97
- async function promptWorkspaceFirstOpen(commandContext: CommandContext, render: () => void): Promise<void> {
98
- const trustManager = commandContext.workspace?.workspaceTrustManager;
99
- const registrationManager = commandContext.workspace?.workspaceRegistrationManager;
100
- const trustNeeded = Boolean(trustManager && !trustManager.isDecided());
101
- const evaluation = registrationManager ? await registrationManager.evaluate() : null;
102
- const registerNeeded = Boolean(evaluation?.offerRegister);
103
- if (!trustNeeded && !registerNeeded) return;
104
-
105
- const offered = { trustNeeded, registerNeeded };
106
- const { title, items } = buildFirstOpenItems(offered);
107
- commandContext.openSelection?.(
108
- title,
109
- items,
110
- { allowSearch: false, primaryVerbLabel: 'Choose' },
111
- (result) => {
112
- const decision = decodeFirstOpenChoice(result?.item.id ?? null, offered);
113
- const apply = async () => {
114
- if (decision.trust && trustManager) await trustManager.setLevel(decision.trust);
115
- if (decision.register === 'yes' && registrationManager) await registrationManager.register();
116
- else if (decision.register === 'no' && registrationManager) await registrationManager.decline();
117
- };
118
- void apply().finally(render);
119
- },
120
- );
66
+ export async function selfRecordWorkspaceRegistration(
67
+ registrationManager: Pick<WorkspaceRegistrationManager, 'evaluate' | 'register'> | undefined,
68
+ ): Promise<void> {
69
+ if (!registrationManager) return;
70
+ const evaluation = await registrationManager.evaluate();
71
+ if (!evaluation.offerRegister) return;
72
+ await registrationManager.register('via TUI', 'tui');
121
73
  }
122
74
 
123
75
  export function applyInitialTuiCliState(options: {
@@ -131,6 +83,16 @@ export function applyInitialTuiCliState(options: {
131
83
  const { cli, input, commandRegistry, commandContext, shellPaths, render } = options;
132
84
  const globalOnboardingMarker = readOnboardingCheckMarker(shellPaths, 'user');
133
85
 
86
+ // Registration self-records on every launch of an already-trusted
87
+ // workspace (fire-and-forget — never blocks or gates startup, never a
88
+ // modal). A workspace that is undecided or restricted is never
89
+ // self-registered here; for one that becomes trusted just now (via the
90
+ // consequence-time trust prompt), main.ts's trustPromptRef wiring calls
91
+ // this same helper right after the decision.
92
+ if (commandContext.workspace?.workspaceTrustManager?.isTrusted()) {
93
+ void selfRecordWorkspaceRegistration(commandContext.workspace.workspaceRegistrationManager);
94
+ }
95
+
134
96
  // Seeded prompt is always applied synchronously, regardless of session branch.
135
97
  const seededPrompt = cli.flags.prompt ?? (cli.rawCommand === undefined && cli.positionals.length > 0 ? cli.positionals.join(' ') : undefined);
136
98
  if (seededPrompt) {
@@ -183,6 +145,9 @@ export function applyInitialTuiCliState(options: {
183
145
  } else if (!globalOnboardingMarker.exists) {
184
146
  // Fast path: get a brand-new user to a working session in the fewest steps.
185
147
  // Falls back to the full wizard when the surface can't detect providers.
148
+ // Trust stays undecided here on purpose — the first non-read tool
149
+ // request is what raises the trust question now (see main.ts's
150
+ // trustPromptRef), not this startup branch.
186
151
  startOnboardingFastPath({ input, commandContext, shellPaths, render });
187
152
  } else {
188
153
  // User has completed onboarding before but left a wizard session in progress.
@@ -202,11 +167,9 @@ export function applyInitialTuiCliState(options: {
202
167
  },
203
168
  });
204
169
  }
205
- } else {
206
- // Returning user, no wizard to resume: this is the moment to ask, in one
207
- // surface, about a genuinely new workspace's trust level and whether to
208
- // register it — before they run anything here.
209
- void promptWorkspaceFirstOpen(commandContext, render);
210
170
  }
171
+ // Returning user, no wizard to resume, nothing else to do at startup:
172
+ // trust (if still undecided) is asked at the first non-read tool
173
+ // request, not here — see main.ts's trustPromptRef wiring.
211
174
  }
212
175
  }
package/src/cli/types.ts CHANGED
@@ -60,6 +60,8 @@ export interface GoodVibesCliFlags {
60
60
  readonly fork: string | true | undefined;
61
61
  readonly yes: boolean;
62
62
  readonly nonInteractive: boolean;
63
+ /** `doctor --strict`: advisory findings also fail (exit 1), for CI. Ignored by every other command. */
64
+ readonly strict: boolean;
63
65
  }
64
66
 
65
67
  export interface GoodVibesCliParseResult {
@@ -8,7 +8,7 @@
8
8
 
9
9
  export { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
10
10
  export type { DeepReadonly } from '@pellux/goodvibes-sdk/platform/config';
11
- export type { GoodVibesConfig, ConfigKey, ConfigValue, ConfigSetting, PermissionMode, PermissionAction, PermissionsToolConfig, NotificationsConfig, PersistedFlagState } from '@pellux/goodvibes-sdk/platform/config';
11
+ export type { GoodVibesConfig, ConfigKey, ConfigValue, ConfigSetting, PermissionMode, PermissionAction, PermissionsToolConfig, NotificationsConfig } from '@pellux/goodvibes-sdk/platform/config';
12
12
  export { DEFAULT_CONFIG, CONFIG_SCHEMA } from '@pellux/goodvibes-sdk/platform/config';
13
13
  export { ConfigError } from '@pellux/goodvibes-sdk/platform/types';
14
14
 
@@ -99,6 +99,39 @@ export function readStatuslineSettings(configManager: Pick<ConfigManager, 'getRa
99
99
  return out;
100
100
  }
101
101
 
102
+ // ─── update.* — launch-time self-update behavior ─────────────────────────────
103
+
104
+ /**
105
+ * User-settable launch-time self-update behavior. The feature itself (check at
106
+ * launch, install via the checksum-verified download/verify/swap path, restart
107
+ * onto the new binary) defaults ON for binary installs; the defaults live in
108
+ * the consumer (src/cli/launch-auto-update.ts), matching this file's rule that
109
+ * readers return only what the user actually set.
110
+ */
111
+ export interface UpdateSettings {
112
+ /** Check for a newer release at TUI launch and install it before starting. Default: true. */
113
+ readonly autoUpdateAtLaunch?: boolean;
114
+ /** How long the launch-time version check may take before it is skipped. Defaults to 2500; clamped to [250, 30000]. */
115
+ readonly launchCheckTimeoutMs?: number;
116
+ }
117
+
118
+ const LAUNCH_CHECK_MIN_TIMEOUT_MS = 250;
119
+ const LAUNCH_CHECK_MAX_TIMEOUT_MS = 30_000;
120
+
121
+ /** Read `update.*` from settings.json, validating and clamping the timeout. */
122
+ export function readUpdateSettings(configManager: Pick<ConfigManager, 'getRaw'>): UpdateSettings {
123
+ const src = readNamespace(configManager, 'update');
124
+ if (!src) return {};
125
+ const out: { autoUpdateAtLaunch?: boolean; launchCheckTimeoutMs?: number } = {};
126
+ const autoUpdateAtLaunch = readBoolean(src, 'autoUpdateAtLaunch');
127
+ if (autoUpdateAtLaunch !== undefined) out.autoUpdateAtLaunch = autoUpdateAtLaunch;
128
+ const launchCheckTimeoutMs = readPositiveInt(src, 'launchCheckTimeoutMs');
129
+ if (launchCheckTimeoutMs !== undefined) {
130
+ out.launchCheckTimeoutMs = Math.min(LAUNCH_CHECK_MAX_TIMEOUT_MS, Math.max(LAUNCH_CHECK_MIN_TIMEOUT_MS, launchCheckTimeoutMs));
131
+ }
132
+ return out;
133
+ }
134
+
102
135
  // ─── checkpoints.* — workspace checkpoint root-guard options ─────────────────
103
136
 
104
137
  /**
@@ -24,7 +24,7 @@ export function deriveComposerState(input: ComposerStateInput): ComposerState {
24
24
  // status tag: `risk:approval-wait`. It is therefore NOT also duplicated as a
25
25
  // `approval` flag, and (below) it suppresses the competing `state:` turn tag —
26
26
  // the turn is blocked on the user, not really streaming. Precedence:
27
- // approval-wait > live turn state. (UX-B item 4.)
27
+ // approval-wait > live turn state. (item 4.)
28
28
  const approvalWait = input.pendingApproval === true;
29
29
  if (approvalWait) {
30
30
  pendingRisk = 'approval-wait';
@@ -0,0 +1,41 @@
1
+ // ---------------------------------------------------------------------------
2
+ // consolidation-receipt.ts — the one-line notice for an idle-time memory
3
+ // consolidation run.
4
+ //
5
+ // The SDK's MemoryConsolidationScheduler runs at idle and produces a
6
+ // MemoryConsolidationRunReceipt (merged / archived / decayed / proposed). This
7
+ // formats it into the same one-line shape every other attach-time receipt
8
+ // uses, so a run that changed something surfaces on the next attach like a
9
+ // crash/update/migration receipt. A pure no-op run yields null — no noise.
10
+ //
11
+ // A run that PROPOSED something (a contradiction or cross-scope-duplicate
12
+ // awaiting human judgment — never auto-applied) names where to actually look:
13
+ // the Memory modal's Proposals tab (`/memory`, then the Proposals tab —
14
+ // memory-modal.ts's proposalsTab()), which lists each proposal's kind,
15
+ // reason, and affected record ids and jumps to them in the Review Queue.
16
+ // Without this pointer the receipt line was the only trace a proposal ever
17
+ // existed — a count with nowhere to go.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ import type { MemoryConsolidationRunReceipt } from '@pellux/goodvibes-sdk/platform/state';
21
+
22
+ /**
23
+ * A one-line receipt for a consolidation run that changed something, or null
24
+ * when the run merged/archived/decayed/proposed nothing (a quiet idle pass —
25
+ * no notice, matching the check-in "stayed quiet" discipline).
26
+ */
27
+ export function formatConsolidationReceipt(receipt: MemoryConsolidationRunReceipt): string | null {
28
+ const merged = receipt.merged.length;
29
+ const archived = receipt.archived.length;
30
+ const decayed = receipt.decayed.length;
31
+ const proposed = receipt.proposed.length;
32
+ if (merged + archived + decayed + proposed === 0) return null;
33
+ const parts: string[] = [];
34
+ if (merged) parts.push(`${merged} merged`);
35
+ if (archived) parts.push(`${archived} archived`);
36
+ if (decayed) parts.push(`${decayed} decayed`);
37
+ if (proposed) parts.push(`${proposed} proposed`);
38
+ const base = `Memory consolidation: ${parts.join(', ')} (scanned ${receipt.scanned}).`;
39
+ if (proposed === 0) return base;
40
+ return `${base} Review the ${proposed} proposed change${proposed === 1 ? '' : 's'} with /memory (Proposals tab).`;
41
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * conversation-line-cache.ts — per-message Line[] production cache (WO-209).
2
+ * conversation-line-cache.ts — per-message Line[] production cache.
3
3
  *
4
4
  * The measured defect (perf baseline 2026-07-03, transcript.build_1k): appending
5
5
  * ONE message to an N-message conversation re-rendered all N messages
@@ -98,7 +98,7 @@ interface KeyMeta {
98
98
  /**
99
99
  * True when this (assistant) message has a tool call with no result yet — it
100
100
  * renders as pending rather than done. Keyed so the entry invalidates and
101
- * re-renders ✓ once the tool result arrives. (UX-B item 2c.)
101
+ * re-renders ✓ once the tool result arrives. (item 2c.)
102
102
  */
103
103
  readonly hasPendingTool: boolean;
104
104
  }
@@ -286,7 +286,7 @@ export class MessageLineCache {
286
286
  };
287
287
 
288
288
  // Tool calls with no matching tool-result message are still pending; the
289
- // render context and the cache key both depend on this. (UX-B item 2c.)
289
+ // render context and the cache key both depend on this. (item 2c.)
290
290
  const completedToolCallIds = collectCompletedToolCallIds(messages);
291
291
  const renderContext: ConversationRenderContext = { ...context, completedToolCallIds };
292
292
 
@@ -1,7 +1,7 @@
1
1
  import { UIFactory } from '../renderer/ui-factory.ts';
2
2
  import { renderMarkdownTracked } from '../renderer/markdown.ts';
3
3
  import { renderDiffView } from '../renderer/diff-view.ts';
4
- import { activeTheme } from '../renderer/theme.ts';
4
+ import { activeTheme, activeUiTones } from '../renderer/theme.ts';
5
5
  import { renderToolCallBlock } from '../renderer/tool-call.ts';
6
6
  import { summarizeToolResult } from '../renderer/tool-result-summary.ts';
7
7
  import type { ToolCall } from '@pellux/goodvibes-sdk/platform/types';
@@ -46,7 +46,7 @@ type Message = ConversationMessageSnapshot;
46
46
  * Collect the ids of tool calls that have a matching tool-result message in the
47
47
  * given slice — i.e. the tools that actually ran. Used to decide whether an
48
48
  * assistant tool call still shows as pending (awaiting approval) or done.
49
- * (UX-B item 2c.)
49
+ * (item 2c.)
50
50
  */
51
51
  export function collectCompletedToolCallIds(messages: readonly Message[]): Set<string> {
52
52
  const ids = new Set<string>();
@@ -79,7 +79,7 @@ export interface ConversationRenderContext {
79
79
  * awaiting a decision (e.g. an approval prompt) and renders with a pending
80
80
  * glyph instead of the completed ✓. When undefined (single-message callers
81
81
  * without sibling context), every tool call renders as done — the prior
82
- * behaviour. (UX-B item 2c.)
82
+ * behaviour. (item 2c.)
83
83
  */
84
84
  readonly completedToolCallIds?: ReadonlySet<string>;
85
85
  }
@@ -273,7 +273,7 @@ export function renderConversationToolMessage(
273
273
  // Human one-line summary for tool results (write/read/exec/edit): shown as the
274
274
  // collapsed line so the transcript reads "wrote foo.txt (532 B)" instead of a
275
275
  // raw JSON blob; the full payload stays behind the expand toggle. Only for
276
- // 'tool' blocks (diffs render their own view). (UX-B item 3.)
276
+ // 'tool' blocks (diffs render their own view). (item 3.)
277
277
  const resultSummary = blockType === 'tool'
278
278
  ? summarizeToolResult(message.toolName, message.content)
279
279
  : null;
@@ -291,11 +291,20 @@ export function renderConversationToolMessage(
291
291
  context.collapseState.set(collapseKey, isShort ? false : true);
292
292
  }
293
293
 
294
+ // A per-call user cancellation settles as a tool result whose content leads
295
+ // with "Error: cancelled by user" (the SDK's structured cancelled shape; any
296
+ // partial output the tool produced before it stopped follows on later lines).
297
+ // Render it structurally as a distinct "cancelled" block in the warn tone —
298
+ // not a generic tool result, and not a hard error — so the transcript reads
299
+ // the user's decision honestly while the partial output stays visible below.
300
+ const isCancelled = blockType === 'tool' && /^Error: cancelled by user\b/.test(contentLines[0] ?? '');
301
+ const warnTone = activeUiTones().chrome.warn;
302
+
294
303
  context.history.addLine(renderConversationEventLine(width, {
295
- marker: blockType === 'diff' ? GLYPHS.status.dualPane : GLYPHS.status.active,
296
- markerFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
297
- label: blockType === 'diff' ? 'diff' : 'tool result',
298
- labelFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
304
+ marker: isCancelled ? GLYPHS.status.blocked : (blockType === 'diff' ? GLYPHS.status.dualPane : GLYPHS.status.active),
305
+ markerFg: isCancelled ? warnTone : (blockType === 'diff' ? T.diffAccent : T.toolAccent),
306
+ label: isCancelled ? 'cancelled' : (blockType === 'diff' ? 'diff' : 'tool result'),
307
+ labelFg: isCancelled ? warnTone : (blockType === 'diff' ? T.diffAccent : T.toolAccent),
299
308
  detailFg: '244',
300
309
  }, [
301
310
  ...(message.toolName
@@ -379,7 +388,7 @@ export function appendConversationMessages(
379
388
  const lineNumberMode = context.configManager?.get('display.lineNumbers') ?? 'off';
380
389
  const collapseThreshold = context.configManager?.get('display.collapseThreshold') ?? 30;
381
390
  // Derive pending vs done for tool calls from sibling tool-result messages,
382
- // unless the caller already supplied the set. (UX-B item 2c.)
391
+ // unless the caller already supplied the set. (item 2c.)
383
392
  const renderContext: ConversationRenderContext = context.completedToolCallIds !== undefined
384
393
  ? context
385
394
  : { ...context, completedToolCallIds: collectCompletedToolCallIds(messages) };
@@ -67,7 +67,7 @@ export interface OrchestratorUsageTotals {
67
67
  * TUI-side counterpart to SDK Orchestrator.usage: after a session resume
68
68
  * replays historical messages into a freshly-constructed Orchestrator (whose
69
69
  * `usage` starts at all zeros — bootstrap.ts always constructs a fresh
70
- * instance, see W0.9), this lets the caller hydrate the footer's token
70
+ * instance, see), this lets the caller hydrate the footer's token
71
71
  * counters from messages that already carry real usage data instead of
72
72
  * waiting for the first new turn to populate them.
73
73
  */
@@ -98,7 +98,7 @@ export class ConversationManager extends SdkConversationManager {
98
98
  /** When true the buffer needs to be rebuilt before the next display. */
99
99
  private dirty = true;
100
100
  /**
101
- * Per-message Line[] cache (WO-209). rebuildHistory() reuses cached lines for
101
+ * Per-message Line[] cache. rebuildHistory() reuses cached lines for
102
102
  * messages whose complete render inputs are unchanged, so appending one message
103
103
  * to an N-message conversation no longer re-renders all N. A cache-served
104
104
  * rebuild is byte-identical to a cold one (pure memoisation).
@@ -18,12 +18,12 @@
18
18
  * When neither target is available the function is an honest no-op (debug log
19
19
  * only; no user-facing error spam).
20
20
  *
21
- * Focus tracking (W2.3): when `focusTracker` is supplied, notifications are
21
+ * Focus tracking: when `focusTracker` is supplied, notifications are
22
22
  * gated the same way as the other unfocused-user alert classes (see
23
23
  * alert-gating.ts) — they fire when the terminal is unfocused or focus state
24
24
  * was never observed, and are suppressed when it's known to be focused,
25
25
  * unless `behavior.notifyOnlyWhenUnfocused` is turned off. `focusTracker` is
26
- * optional and defaults to "always fire" (the pre-W2.3 behavior) when
26
+ * optional and defaults to "always fire" (the behavior before focus gating existed) when
27
27
  * omitted, so existing callers that don't have a FocusTracker in scope are
28
28
  * unaffected.
29
29
  */
@@ -82,11 +82,11 @@ export interface MaybeNotifyLongTaskOptions {
82
82
  readonly webhookNotifier?: WebhookNotifier | null;
83
83
 
84
84
  /**
85
- * Terminal focus tracker (W2.3). When supplied together with `configGet`,
85
+ * Terminal focus tracker. When supplied together with `configGet`,
86
86
  * the notification additionally respects behavior.notifyOnlyWhenUnfocused
87
87
  * (default on): suppressed when the terminal is known to be focused, fires
88
88
  * when unfocused or when focus was never observed. Omit both to preserve
89
- * pre-W2.3 behavior (always fire once the threshold is met).
89
+ * the behavior before focus gating existed (always fire once the threshold is met).
90
90
  */
91
91
  readonly focusTracker?: Pick<FocusTracker, 'shouldAlertWhenUnfocused'> | null;
92
92
 
@@ -121,9 +121,9 @@ export function maybeNotifyLongTask(opts: MaybeNotifyLongTaskOptions): boolean {
121
121
  return false;
122
122
  }
123
123
 
124
- // Focus gate (W2.3): only applied when both a tracker and a config reader
125
- // are supplied. Absent either one, behavior is unchanged from pre-W2.3
126
- // (always fire once the threshold is met).
124
+ // Focus gate: only applied when both a tracker and a config reader
125
+ // are supplied. Absent either one, behavior is unchanged from before focus
126
+ // gating existed (always fire once the threshold is met).
127
127
  if (focusTracker && configGet && readNotifyOnlyWhenUnfocused(configGet) && !focusTracker.shouldAlertWhenUnfocused()) {
128
128
  logger.debug('long-task-notifier: suppressed — terminal focused');
129
129
  return false;
@@ -0,0 +1,67 @@
1
+ // ---------------------------------------------------------------------------
2
+ // memory-provenance.ts — the optional "used N memories" turn chip (owner-ruled
3
+ // 2026-07-13, default OFF).
4
+ //
5
+ // The SDK tags each turn payload with the memory-sourced injection ids it drew
6
+ // on: `metadata.memory.recordIds: string[]` (absent when none) — the SAME
7
+ // convention the webui's chip reads, so both surfaces name the same records.
8
+ // When the memory-provenance setting is ON, a turn that used memories shows a
9
+ // small chip naming how many, with a drill-in listing them. When OFF (default),
10
+ // NOTHING is rendered and the metadata is never even read.
11
+ // ---------------------------------------------------------------------------
12
+
13
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
14
+
15
+ /** TUI-local setting key: show the per-turn memory-provenance chip. Default OFF. */
16
+ export const MEMORY_SHOW_PROVENANCE_CONFIG_KEY = 'memory.showProvenance';
17
+ export const MEMORY_SHOW_PROVENANCE_DEFAULT = false;
18
+ export const MEMORY_SHOW_PROVENANCE_DESCRIPTION =
19
+ 'Show a "used N memories" chip on turns that drew on your standing memories, with a drill-in ' +
20
+ 'listing them (Alt+M to expand). Off by default; when off, nothing is rendered and no metadata is read.';
21
+
22
+ /**
23
+ * Read the memory-provenance toggle defensively. `memory.*` is a TUI-local
24
+ * section with no SDK DEFAULT_CONFIG entry, so ConfigManager.get() throws for
25
+ * it until something has been written — an unset key means the default (OFF),
26
+ * not an error (same pattern as memory.projection.dir).
27
+ */
28
+ export function readMemoryShowProvenance(configManager: Pick<ConfigManager, 'get'>): boolean {
29
+ try {
30
+ return configManager.get(MEMORY_SHOW_PROVENANCE_CONFIG_KEY as Parameters<ConfigManager['get']>[0]) === true;
31
+ } catch {
32
+ return MEMORY_SHOW_PROVENANCE_DEFAULT;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Extract the memory-sourced record ids a turn drew on, from the SDK's
38
+ * `metadata.memory.recordIds` convention. Structural (not a named import) so it
39
+ * reads the field whether or not the pinned SDK's TurnEvent type surfaces it
40
+ * yet, and yields an empty list for any turn without the field — never a guess,
41
+ * never a throw.
42
+ */
43
+ export function memoryRecordIdsFromTurn(turn: unknown): readonly string[] {
44
+ const metadata = (turn as { metadata?: unknown } | null | undefined)?.metadata;
45
+ const memory = (metadata as { memory?: unknown } | null | undefined)?.memory;
46
+ const recordIds = (memory as { recordIds?: unknown } | null | undefined)?.recordIds;
47
+ if (!Array.isArray(recordIds)) return [];
48
+ return recordIds.filter((id): id is string => typeof id === 'string');
49
+ }
50
+
51
+ /**
52
+ * Build the settings-modal synthetic entry for memory.showProvenance (TUI-local,
53
+ * default OFF). Read defensively — memory.* has no SDK DEFAULT_CONFIG entry, so
54
+ * an unset key means the OFF default, not an error.
55
+ */
56
+ export function buildMemoryProvenanceSyntheticEntry(configManager: Pick<ConfigManager, 'get'>): {
57
+ setting: { key: string; type: 'boolean'; default: boolean; description: string };
58
+ currentValue: boolean;
59
+ isDefault: boolean;
60
+ } {
61
+ const currentValue = readMemoryShowProvenance(configManager);
62
+ return {
63
+ setting: { key: MEMORY_SHOW_PROVENANCE_CONFIG_KEY, type: 'boolean', default: MEMORY_SHOW_PROVENANCE_DEFAULT, description: MEMORY_SHOW_PROVENANCE_DESCRIPTION },
64
+ currentValue,
65
+ isDefault: currentValue === MEMORY_SHOW_PROVENANCE_DEFAULT,
66
+ };
67
+ }