@pellux/goodvibes-tui 1.16.1 → 1.17.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 (219) hide show
  1. package/CHANGELOG.md +89 -9
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1091 -271
  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/parser.ts +5 -0
  12. package/src/cli/status.ts +13 -0
  13. package/src/cli/surface-command.ts +5 -5
  14. package/src/cli/tui-startup.ts +60 -97
  15. package/src/cli/types.ts +2 -0
  16. package/src/config/index.ts +1 -1
  17. package/src/config/tui-extension-settings.ts +33 -0
  18. package/src/core/composer-state.ts +1 -1
  19. package/src/core/conversation-line-cache.ts +3 -3
  20. package/src/core/conversation-rendering.ts +4 -4
  21. package/src/core/conversation.ts +2 -2
  22. package/src/core/long-task-notifier.ts +7 -7
  23. package/src/core/stream-event-wiring.ts +8 -6
  24. package/src/core/system-message-noise.ts +1 -1
  25. package/src/core/system-message-router.ts +6 -6
  26. package/src/core/turn-cancellation.ts +1 -1
  27. package/src/core/turn-event-wiring.ts +8 -8
  28. package/src/daemon/cli.ts +20 -5
  29. package/src/daemon/lifecycle.ts +41 -0
  30. package/src/daemon/service-commands.ts +5 -5
  31. package/src/export/cost-utils.ts +128 -6
  32. package/src/input/autocomplete.ts +2 -2
  33. package/src/input/command-registry.ts +34 -9
  34. package/src/input/commands/ci-runtime.ts +5 -1
  35. package/src/input/commands/codebase-runtime.ts +6 -6
  36. package/src/input/commands/cost-runtime.ts +1 -1
  37. package/src/input/commands/health-runtime.ts +2 -2
  38. package/src/input/commands/intelligence-runtime.ts +1 -1
  39. package/src/input/commands/local-auth-runtime.ts +1 -1
  40. package/src/input/commands/local-provider-runtime.ts +49 -10
  41. package/src/input/commands/local-runtime.ts +53 -5
  42. package/src/input/commands/memory.ts +2 -2
  43. package/src/input/commands/operator-panel-runtime.ts +5 -5
  44. package/src/input/commands/permissions-provenance.ts +1 -1
  45. package/src/input/commands/permissions-runtime.ts +99 -8
  46. package/src/input/commands/planning-runtime.ts +3 -3
  47. package/src/input/commands/platform-sandbox-runtime.ts +1 -1
  48. package/src/input/commands/provider-accounts-runtime.ts +71 -6
  49. package/src/input/commands/provider.ts +7 -5
  50. package/src/input/commands/recall-files-git.ts +13 -51
  51. package/src/input/commands/recall-files.ts +8 -11
  52. package/src/input/commands/recall-review.ts +2 -2
  53. package/src/input/commands/relay-runtime.ts +1 -1
  54. package/src/input/commands/remote-runtime.ts +1 -1
  55. package/src/input/commands/runtime-services.ts +4 -4
  56. package/src/input/commands/schedule-runtime.ts +14 -7
  57. package/src/input/commands/services-runtime.ts +1 -1
  58. package/src/input/commands/session-workflow.ts +2 -2
  59. package/src/input/commands/settings-sync-runtime.ts +2 -2
  60. package/src/input/commands/share-runtime.ts +1 -1
  61. package/src/input/commands/shell-core.ts +46 -68
  62. package/src/input/commands/subscription-runtime.ts +1 -1
  63. package/src/input/commands/update-runtime.ts +153 -96
  64. package/src/input/commands/work-plan-runtime.ts +1 -1
  65. package/src/input/commands/workstream-runtime.ts +2 -2
  66. package/src/input/commands.ts +0 -2
  67. package/src/input/config-modal-types.ts +3 -3
  68. package/src/input/config-modal.ts +10 -11
  69. package/src/input/feature-unit-layout.ts +93 -82
  70. package/src/input/feed-context-factory.ts +2 -2
  71. package/src/input/handler-command-route.ts +2 -2
  72. package/src/input/handler-content-actions.ts +1 -1
  73. package/src/input/handler-feed-routes.ts +7 -7
  74. package/src/input/handler-feed.ts +4 -4
  75. package/src/input/handler-interactions.ts +3 -2
  76. package/src/input/handler-modal-routes.ts +12 -4
  77. package/src/input/handler-modal-token-routes.ts +1 -1
  78. package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
  79. package/src/input/handler-onboarding.ts +3 -2
  80. package/src/input/handler-picker-routes.ts +26 -13
  81. package/src/input/handler-shortcuts.ts +6 -6
  82. package/src/input/handler.ts +2 -2
  83. package/src/input/keybindings.ts +2 -2
  84. package/src/input/onboarding/onboarding-feature-units.ts +34 -27
  85. package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
  86. package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
  87. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
  88. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
  89. package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
  90. package/src/input/panel-integration-actions.ts +2 -2
  91. package/src/input/panel-mouse-geometry.ts +1 -1
  92. package/src/input/panel-paste-flood-guard.ts +1 -1
  93. package/src/input/provider-key-intake.ts +167 -0
  94. package/src/input/sandbox-exec-config.ts +2 -2
  95. package/src/input/selection-modal.ts +1 -1
  96. package/src/input/settings-modal-activation.ts +5 -0
  97. package/src/input/settings-modal-adjustment.ts +4 -31
  98. package/src/input/settings-modal-data.ts +22 -21
  99. package/src/input/settings-modal-mutations.ts +12 -68
  100. package/src/input/settings-modal-types.ts +23 -16
  101. package/src/input/settings-modal.ts +74 -55
  102. package/src/main.ts +48 -48
  103. package/src/panels/agent-inspector-shared.ts +4 -4
  104. package/src/panels/base-panel.ts +2 -2
  105. package/src/panels/builtin/agent.ts +4 -4
  106. package/src/panels/builtin/development.ts +4 -4
  107. package/src/panels/builtin/knowledge.ts +1 -1
  108. package/src/panels/builtin/operations.ts +29 -10
  109. package/src/panels/builtin/session.ts +5 -5
  110. package/src/panels/builtin/shared.ts +4 -4
  111. package/src/panels/builtin-modals.ts +10 -10
  112. package/src/panels/builtin-panels.ts +1 -1
  113. package/src/panels/cost-tracker-panel.ts +124 -7
  114. package/src/panels/diff-panel.ts +5 -5
  115. package/src/panels/eval-registry.ts +2 -2
  116. package/src/panels/fleet-deep-link.ts +1 -1
  117. package/src/panels/fleet-panel-format.ts +44 -8
  118. package/src/panels/fleet-panel.ts +16 -16
  119. package/src/panels/fleet-read-model.ts +52 -13
  120. package/src/panels/fleet-steer.ts +2 -2
  121. package/src/panels/fleet-stop.ts +3 -3
  122. package/src/panels/fleet-tabs.ts +13 -14
  123. package/src/panels/fleet-transcript.ts +5 -5
  124. package/src/panels/git-panel.ts +2 -2
  125. package/src/panels/index.ts +4 -1
  126. package/src/panels/local-auth-panel.ts +1 -1
  127. package/src/panels/modals/hooks-modal.ts +1 -1
  128. package/src/panels/modals/keybindings-modal.ts +2 -2
  129. package/src/panels/modals/knowledge-modal.ts +1 -1
  130. package/src/panels/modals/marketplace-modal.ts +4 -4
  131. package/src/panels/modals/memory-modal.ts +4 -4
  132. package/src/panels/modals/modal-surface-helpers.ts +1 -1
  133. package/src/panels/modals/modal-theme.ts +1 -1
  134. package/src/panels/modals/pairing-modal.ts +2 -2
  135. package/src/panels/modals/planning-modal.ts +2 -2
  136. package/src/panels/modals/plugins-modal.ts +1 -1
  137. package/src/panels/modals/policy-modal.ts +1 -1
  138. package/src/panels/modals/security-modal.ts +1 -1
  139. package/src/panels/modals/skills-modal.ts +1 -1
  140. package/src/panels/modals/work-plan-modal.ts +3 -3
  141. package/src/panels/notifications-feed.ts +127 -0
  142. package/src/panels/notifications-panel.ts +160 -0
  143. package/src/panels/panel-manager.ts +9 -9
  144. package/src/panels/plugins-panel.ts +4 -4
  145. package/src/panels/project-planning-answer-actions.ts +1 -1
  146. package/src/panels/provider-health-routes.ts +2 -2
  147. package/src/panels/skills-panel.ts +4 -5
  148. package/src/panels/token-budget-panel.ts +11 -3
  149. package/src/panels/types.ts +5 -5
  150. package/src/permissions/broker-approval-card.ts +201 -0
  151. package/src/permissions/hunk-selection.ts +28 -4
  152. package/src/permissions/prompt.ts +244 -29
  153. package/src/renderer/autocomplete-overlay.ts +107 -54
  154. package/src/renderer/code-block.ts +2 -2
  155. package/src/renderer/compaction-history-modal.ts +1 -1
  156. package/src/renderer/compaction-preview.ts +1 -1
  157. package/src/renderer/compaction-quality.ts +1 -1
  158. package/src/renderer/compositor.ts +2 -2
  159. package/src/renderer/config-modal.ts +1 -1
  160. package/src/renderer/conversation-overlays.ts +1 -1
  161. package/src/renderer/footer-tips.ts +3 -3
  162. package/src/renderer/hint-grammar.ts +1 -1
  163. package/src/renderer/modal-factory.ts +1 -1
  164. package/src/renderer/model-picker-overlay.ts +1 -1
  165. package/src/renderer/model-workspace.ts +7 -2
  166. package/src/renderer/panel-composite.ts +2 -2
  167. package/src/renderer/panel-workspace-bar.ts +1 -1
  168. package/src/renderer/process-indicator.ts +1 -1
  169. package/src/renderer/selection-modal-overlay.ts +1 -1
  170. package/src/renderer/session-picker-modal.ts +2 -2
  171. package/src/renderer/settings-modal-helpers.ts +14 -5
  172. package/src/renderer/settings-modal.ts +127 -39
  173. package/src/renderer/shell-surface.ts +3 -0
  174. package/src/renderer/terminal-bg-probe.ts +1 -1
  175. package/src/renderer/theme.ts +3 -3
  176. package/src/renderer/tool-call.ts +1 -1
  177. package/src/renderer/tool-result-summary.ts +1 -1
  178. package/src/renderer/turn-injection.ts +7 -6
  179. package/src/renderer/ui-factory.ts +6 -1
  180. package/src/runtime/bootstrap-command-context.ts +4 -1
  181. package/src/runtime/bootstrap-command-parts.ts +6 -4
  182. package/src/runtime/bootstrap-core.ts +35 -35
  183. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  184. package/src/runtime/bootstrap-shell.ts +6 -5
  185. package/src/runtime/bootstrap.ts +34 -32
  186. package/src/runtime/channel-composition.ts +46 -0
  187. package/src/runtime/code-index-services.ts +3 -4
  188. package/src/runtime/daemon-attach-notices.ts +120 -0
  189. package/src/runtime/durability-services.ts +55 -0
  190. package/src/runtime/embedded-service-factories.ts +40 -0
  191. package/src/runtime/feature-settings.ts +147 -0
  192. package/src/runtime/index.ts +4 -21
  193. package/src/runtime/legacy-daemon-migration.ts +29 -19
  194. package/src/runtime/memory-fold.ts +1 -1
  195. package/src/runtime/notification-dispatch.ts +110 -0
  196. package/src/runtime/onboarding/apply.ts +3 -29
  197. package/src/runtime/onboarding/fast-path.ts +39 -6
  198. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  199. package/src/runtime/onboarding/snapshot.ts +0 -1
  200. package/src/runtime/onboarding/types.ts +9 -3
  201. package/src/runtime/provider-fallback.ts +55 -0
  202. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  203. package/src/runtime/release-artifacts.ts +19 -103
  204. package/src/runtime/resume-notice.ts +1 -1
  205. package/src/runtime/services.ts +81 -82
  206. package/src/runtime/session-continuity-hints.ts +38 -0
  207. package/src/runtime/surface-feature-flags.ts +40 -9
  208. package/src/runtime/terminal-output-guard.ts +2 -2
  209. package/src/runtime/trust/workspace-registration.ts +10 -2
  210. package/src/runtime/trust/workspace-trust.ts +58 -31
  211. package/src/runtime/ui-services.ts +3 -1
  212. package/src/runtime/update-check.ts +15 -69
  213. package/src/runtime/workstream-services.ts +2 -2
  214. package/src/shell/blocking-input.ts +102 -6
  215. package/src/shell/ui-openers.ts +38 -6
  216. package/src/utils/format-duration.ts +1 -1
  217. package/src/verification/verification-ledger.ts +21 -4
  218. package/src/version.ts +1 -1
  219. package/src/input/commands/flags-runtime.ts +0 -328
@@ -6,8 +6,9 @@
6
6
  */
7
7
 
8
8
  import type { Line } from '../types/grid.ts';
9
- import type { SettingsModal, SettingEntry, FlagEntry, McpEntry, SubscriptionEntry, SettingsCategory } from '../input/settings-modal.ts';
9
+ import type { SettingsModal, SettingEntry, McpEntry, SubscriptionEntry, SettingsCategory } from '../input/settings-modal.ts';
10
10
  import { SETTINGS_CATEGORIES, SETTINGS_CATEGORY_GROUPS } from '../input/settings-modal.ts';
11
+ import { FEATURE_SETTINGS_BY_ID, isFeatureValueEnabled } from '../runtime/feature-settings.ts';
11
12
  import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
12
13
  import { CATEGORY_LABELS, describeUiRouting, formatValue, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
13
14
  import { isSecretConfigKey } from '../config/secret-config.ts';
@@ -27,6 +28,8 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
27
28
  display: 'Presentation settings for the terminal transcript: streaming, line numbers, thinking visibility, reasoning summaries, token speed, and tool previews.',
28
29
  ui: 'Controls where operational messages render and whether voice surfaces are enabled. These settings change visibility, not provider behavior.',
29
30
  provider: 'Default model routing for normal chat turns, embeddings, reasoning effort, and persistent system prompt file.',
31
+ pricing: 'Manual model prices (USD per 1M tokens, keyed provider:model). Your price wins over provider-served and catalog prices in every cost display, immediately.',
32
+ update: 'Self-update posture: whether the daemon checks hourly and updates at an idle moment, the release source it checks, and the check cadence. Clients update at launch; every swap keeps the previous binary for one-command rollback.',
30
33
  subscriptions: 'Provider subscription login state and routing posture. Active sessions can be reviewed or signed out here; API keys remain managed through secrets.',
31
34
  behavior: 'Day-to-day shell behavior: approval posture, compaction, history, guidance, notifications, stale-context warnings, return context, and Human-in-the-Loop mode.',
32
35
  storage: 'Local storage posture, including secret storage policy and maximum artifact size for knowledge/home graph/document ingestion.',
@@ -57,7 +60,6 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
57
60
  release: 'Release-channel preference.',
58
61
  danger: 'High-impact switches for daemon and HTTP listener behavior. These are operational overrides, not normal preferences.',
59
62
  tools: 'Tool LLM and helper model routing. Empty provider/model values inherit the active chat route unless a specific helper/tool route is set.',
60
- flags: 'Advanced feature toggles with no tuning knobs — internal and startup-gated runtime gates. Features that DO have config sit as units (toggle + settings) in their topical category, not here.',
61
63
  network: 'Combined network view for daemon control-plane, HTTP listener, browser web surface, and general outbound network settings.',
62
64
  fetch: 'Fetch response sanitization and host trust tiers for the fetch tool: sanitize mode plus default trusted/blocked host lists. Gated by the Fetch Response Sanitization feature.',
63
65
  agents: 'Sub-agent context-window awareness and per-turn passive knowledge/code injection: token budget, relevance floor, code-chunk limit, and the compaction threshold. Gated by the agent context/injection features.',
@@ -65,7 +67,7 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
65
67
  integrations: 'Integration delivery reliability (Slack/Discord/webhook): retry counts, backoff bounds, dead-letter queue size, and SLO enforcement. Gated by the Integration Delivery SLO feature.',
66
68
  policy: 'Policy-as-code bundle loading: where the startup policy bundle is loaded from and its file path. Gated by the Policy-as-Code feature.',
67
69
  notifications: 'Notification router burst-suppression tuning: burst window, threshold, and cooldown. Gated by the Adaptive Notification Suppression feature.',
68
- relay: 'Outbound zero-knowledge relay reachability, for reaching this daemon from outside the LAN without opening an inbound port. Off by default; also gated by the relay-connect feature flag. The relay operator sees only ciphertext and connection metadata — self-host your own relay for full control.',
70
+ relay: 'Outbound zero-knowledge relay reachability, for reaching this daemon from outside the LAN without opening an inbound port. Off by default — relay.enabled is the switch. The relay operator sees only ciphertext and connection metadata — self-host your own relay for full control.',
69
71
  learning: 'Idle-time memory consolidation: merging duplicate standing memory records and decaying/archiving stale ones. Off by default — nothing runs until enabled.',
70
72
  };
71
73
 
@@ -181,6 +183,13 @@ function buildSettingContext(modal: SettingsModal, entry: SettingEntry): string[
181
183
  if (entry.locked) lines.push(`Locked: ${entry.lockReason ?? 'This setting is locked by a higher-priority layer.'}`);
182
184
  if (entry.conflict) lines.push(`Conflict: resolve with /settings-sync resolve ${entry.setting.key} local|synced.`);
183
185
 
186
+ // A settings sub-row owned by a feature unit names its feature so "what
187
+ // does this do" is answerable without scrolling back to the header row.
188
+ if (entry.ownerFlagId) {
189
+ const owner = FEATURE_SETTINGS_BY_ID.get(entry.ownerFlagId);
190
+ if (owner) lines.push(`Part of feature: ${owner.name} (the header row above).`);
191
+ }
192
+
184
193
  lines.push('', entry.setting.description);
185
194
 
186
195
  if (
@@ -225,26 +234,86 @@ function buildSettingContext(modal: SettingsModal, entry: SettingEntry): string[
225
234
  return lines;
226
235
  }
227
236
 
228
- function buildFlagContext(entry: FlagEntry | null): string[] {
229
- if (!entry) return ['Feature flags', 'No feature flag is selected.'];
230
- return [
231
- entry.flag.name,
232
- `ID: ${entry.flag.id}`,
233
- `State: ${entry.state}`,
234
- `Default: ${entry.flag.defaultState}`,
235
- `Tier: ${entry.flag.tier}`,
236
- `Runtime toggleable: ${entry.flag.runtimeToggleable ? 'yes' : 'no'}`,
237
- ...(entry.pendingRestart
238
- ? [`Pending restart: saved as ${entry.persistedState}; effective state stays ${entry.state} until the next launch.`]
237
+ /**
238
+ * The option shape of a feature header, rendered from the same schema the
239
+ * write path uses: enum headers list every mode choice (marking the current
240
+ * value and which values keep the feature active), boolean headers state the
241
+ * two positions.
242
+ */
243
+ function buildFeatureOptionLines(entry: SettingEntry): string[] {
244
+ const feature = entry.flag!.feature;
245
+ const setting = entry.setting;
246
+ const lines: string[] = [];
247
+ if (setting.type === 'enum' && setting.enumValues) {
248
+ lines.push('', `Mode choices for ${setting.key}:`);
249
+ const activeValues = feature.enablement.enabledValues ?? [];
250
+ const descriptions = ENUM_VALUE_DESCRIPTIONS[setting.key] ?? {};
251
+ for (const value of setting.enumValues) {
252
+ const marks: string[] = [];
253
+ if (value === String(entry.currentValue)) marks.push('current');
254
+ if (feature.enablement.kind === 'enum') {
255
+ marks.push(activeValues.includes(value) ? 'feature on' : 'feature off');
256
+ }
257
+ const suffix = marks.length > 0 ? ` (${marks.join(', ')})` : '';
258
+ lines.push(`${value}${suffix}: ${descriptions[value] ?? `Use ${value} for this setting.`}`);
259
+ }
260
+ } else if (setting.type === 'boolean') {
261
+ lines.push('');
262
+ lines.push('Possible values:');
263
+ lines.push('true: the feature is enabled.');
264
+ lines.push('false: the feature is disabled.');
265
+ }
266
+ return lines;
267
+ }
268
+
269
+ /**
270
+ * Under-cursor documentation for a feature-unit header, rendered entirely
271
+ * from the SDK's per-feature settings metadata: full behavior description,
272
+ * the real option shape, every settings key that tunes the feature, and the
273
+ * honest live/restart state from the gate manager.
274
+ */
275
+ function buildFlagContext(entry: SettingEntry | null): string[] {
276
+ const flagEntry = entry?.flag ?? null;
277
+ if (!entry || !flagEntry) return ['Features', 'No feature is selected.'];
278
+ const { feature, flag, state, persistedState, pendingRestart } = flagEntry;
279
+ const configOn = isFeatureValueEnabled(feature, entry.currentValue);
280
+ const displayState = state === 'killed' ? 'killed' : configOn ? 'enabled' : 'disabled';
281
+ const lines: string[] = [
282
+ feature.name,
283
+ `Feature: ${feature.id} (${feature.domain} domain)`,
284
+ `Setting: ${feature.enablement.key} = ${formatValue(entry)}`,
285
+ `State: ${displayState}`,
286
+ `Default: ${feature.defaultEnabled ? 'enabled' : 'disabled'}`,
287
+ `Applies: ${feature.restartRequired ? 'on next launch (startup-gated)' : 'immediately'}`,
288
+ ...(pendingRestart
289
+ ? [`Pending restart: saved as ${persistedState}; effective state stays ${state} until the next launch.`]
239
290
  : []),
240
291
  '',
241
- entry.flag.description,
242
- ...(entry.state === 'killed' && entry.flag.killReason ? ['', `Kill reason: ${entry.flag.killReason}`] : []),
243
- '',
244
- entry.flag.runtimeToggleable
245
- ? 'Impact: changes apply immediately and are also persisted as an override when they differ from the default.'
246
- : 'Impact: this flag is persisted as an override and requires restart before startup-only code sees the new state.',
292
+ feature.description,
247
293
  ];
294
+
295
+ lines.push(...buildFeatureOptionLines(entry));
296
+
297
+ lines.push('');
298
+ if (feature.enablement.kind === 'enum') {
299
+ lines.push(`How it turns on: active while ${feature.enablement.key} is ${(feature.enablement.enabledValues ?? []).join(' or ')}.`);
300
+ } else if (feature.enablement.kind === 'constant' && entry.setting.type !== 'boolean') {
301
+ lines.push('How it turns on: always active; the settings listed below tune its behavior directly.');
302
+ } else {
303
+ lines.push(`How it turns on: ${feature.enablement.key} set to true.`);
304
+ }
305
+
306
+ if (feature.settings.length > 1) {
307
+ lines.push('', 'Settings in this feature:');
308
+ for (const key of feature.settings) {
309
+ lines.push(key === feature.enablement.key ? `${key} (this row)` : key);
310
+ }
311
+ }
312
+
313
+ if (state === 'killed' && flag.killReason) {
314
+ lines.push('', `Kill reason: ${flag.killReason}`);
315
+ }
316
+ return lines;
248
317
  }
249
318
 
250
319
  function buildMcpContext(modal: SettingsModal, entry: McpEntry | null): string[] {
@@ -332,9 +401,10 @@ function buildContextLines(modal: SettingsModal, width: number): string[] {
332
401
  lines.push(...buildSubscriptionContext(modal, modal.getSelectedSubscription()));
333
402
  } else {
334
403
  const selected = modal.getSelected();
335
- // A feature-unit toggle header shows flag context (tier, default, impact);
336
- // its config sub-options and plain settings show the setting context.
337
- if (selected?.flag) lines.push(...buildFlagContext(selected.flag));
404
+ // A feature-unit header shows the feature's documentation (full
405
+ // description, option shape, settings list, live/restart state); its
406
+ // settings sub-rows and plain settings show the setting context.
407
+ if (selected?.flag) lines.push(...buildFlagContext(selected));
338
408
  else if (selected) lines.push(...buildSettingContext(modal, selected));
339
409
  else lines.push('No setting is selected in this category.');
340
410
  }
@@ -355,7 +425,6 @@ function buildContextLines(modal: SettingsModal, width: number): string[] {
355
425
  function categoryItemCount(modal: SettingsModal, category: SettingsCategory): number {
356
426
  if (category === 'mcp') return modal.mcpEntries.length;
357
427
  if (category === 'subscriptions') return modal.subscriptionEntries.length;
358
- // 'flags' (Advanced Features) now flows through the normal group path.
359
428
  return modal.groups.get(category)?.length ?? 0;
360
429
  }
361
430
 
@@ -454,15 +523,18 @@ function renderSettingTableRow(
454
523
  defaultWidth: number,
455
524
  ): string {
456
525
  if (entry.flag) {
457
- const { flag, state, pendingRestart, persistedState } = entry.flag;
458
- const stateMark = state === 'enabled' ? '●' : state === 'killed' ? '✕' : '○';
459
- const label = `${stateMark} ${flag.name}`;
460
- const source = `tier ${flag.tier} ${flag.runtimeToggleable ? 'live' : 'restart'}`;
461
- // A startup-gated flag toggled this session keeps its effective state and
462
- // shows the pending value it will take on restart, so the row never implies
463
- // the change already took effect.
464
- const value = pendingRestart ? `${state} → ${persistedState} on restart` : state;
465
- return `${marker} ${padDisplay(label, keyWidth)} ${padDisplay(value, valueWidth)} ${padDisplay('feature', typeWidth)} ${padDisplay(source, sourceWidth)} ${padDisplay(flag.defaultState, defaultWidth)}`;
526
+ const { feature, state, pendingRestart } = entry.flag;
527
+ const configOn = isFeatureValueEnabled(feature, entry.currentValue);
528
+ const stateMark = state === 'killed' ? '✕' : configOn ? '●' : '○';
529
+ const label = `${stateMark} ${feature.name}`;
530
+ const source = feature.restartRequired ? 'restart' : 'live';
531
+ // A startup-gated feature changed this session shows its saved value with
532
+ // a compact restart-pending marker (⟳), so the row never implies the
533
+ // change already took effect; the documentation pane spells out the full
534
+ // "Pending restart: ..." sentence.
535
+ const rawValue = currentSettingValue(modal, entry, selected);
536
+ const value = pendingRestart ? `${rawValue} ⟳` : rawValue;
537
+ return `${marker} ${padDisplay(label, keyWidth)} ${padDisplay(value, valueWidth)} ${padDisplay('feature', typeWidth)} ${padDisplay(source, sourceWidth)} ${padDisplay(feature.defaultEnabled ? 'enabled' : 'disabled', defaultWidth)}`;
466
538
  }
467
539
  const value = currentSettingValue(modal, entry, selected);
468
540
  const source = `${entry.effectiveSource ?? 'default'}${entry.locked ? ' locked' : ''}${entry.conflict ? ' conflict' : ''}`;
@@ -626,9 +698,9 @@ function footerText(modal: SettingsModal, width: number): string {
626
698
  { key: 'Enter', verb: 'Edit trust' },
627
699
  { key: 'Esc', verb: 'Close' },
628
700
  ]));
629
- // 'flags' (Advanced Features) and every topical category that now hosts feature
630
- // units flow through the default settings footer below — Enter/Space toggles a
631
- // feature-unit header exactly as it edits/toggles any setting row.
701
+ // Every topical category that hosts feature units flows through the default
702
+ // settings footer below — Enter/Space toggles a feature-unit header exactly
703
+ // as it edits/toggles any setting row.
632
704
  // Default settings pane: tier the reset affordances by available width.
633
705
  // W<80: minimal — only the most critical action survives.
634
706
  // W<160: compact but still shows both reset affordances.
@@ -639,6 +711,7 @@ function footerText(modal: SettingsModal, width: number): string {
639
711
  return formatHints([
640
712
  { key: 'Up/Down', verb: 'Move' },
641
713
  { key: 'Enter/Space', verb: 'Edit' },
714
+ { key: 'PgUp/PgDn', verb: 'docs' },
642
715
  { key: '⇧R', verb: 'reset cat' },
643
716
  { key: '^⇧R', verb: 'reset all' },
644
717
  { key: 'Esc', verb: 'Close' },
@@ -647,6 +720,7 @@ function footerText(modal: SettingsModal, width: number): string {
647
720
  { key: 'Up/Down', verb: 'Setting' },
648
721
  { key: 'Left', verb: 'Categories' },
649
722
  { key: 'Enter/Space', verb: 'Edit/toggle' },
723
+ { key: 'PgUp/PgDn', verb: 'Scroll docs' },
650
724
  { key: '⇧R', verb: 'reset cat' },
651
725
  { key: '^⇧R', verb: 'reset all' },
652
726
  { key: 'Esc', verb: 'Close' },
@@ -664,12 +738,26 @@ export function renderSettingsModal(
664
738
  ];
665
739
  const metrics = getFullscreenWorkspaceMetrics({ width, height: viewportHeight });
666
740
  const categoryRows = renderCategories(modal, metrics.leftWidth - 2, metrics.bodyRows);
667
- const contextRows = buildContextLines(modal, metrics.contextWidth).map((text, row): WorkspaceRow => {
741
+ // Documentation pane: long content SCROLLS (PgUp/PgDn) with honest
742
+ // more-above/below markers — it is never silently clipped.
743
+ const allContextLines = buildContextLines(modal, metrics.contextWidth);
744
+ const visibleContext = metrics.contextRows;
745
+ const maxContextScroll = Math.max(0, allContextLines.length - visibleContext);
746
+ const contextOffset = Math.min(Math.max(0, modal.contextScroll ?? 0), maxContextScroll);
747
+ const windowedContext = allContextLines.slice(contextOffset, contextOffset + visibleContext);
748
+ if (contextOffset > 0) {
749
+ windowedContext[0] = `${GLYPHS.navigation.moreAbove} ${contextOffset} more line(s) above — PgUp`;
750
+ }
751
+ if (contextOffset + visibleContext < allContextLines.length) {
752
+ const below = allContextLines.length - contextOffset - visibleContext;
753
+ windowedContext[windowedContext.length - 1] = `${GLYPHS.navigation.moreBelow} ${below} more line(s) below — PgDn`;
754
+ }
755
+ const contextRows = windowedContext.map((text, row): WorkspaceRow => {
668
756
  const selectedSetting = modal.getSelected();
669
- const isTitle = row === 0 || (selectedSetting !== null && text === getSettingLabel(selectedSetting));
757
+ const isTitle = (row === 0 && contextOffset === 0) || (selectedSetting !== null && text === getSettingLabel(selectedSetting));
670
758
  return {
671
759
  text,
672
- fg: row === 0 ? PALETTE.title : text.endsWith(':') ? PALETTE.subtitle : PALETTE.text,
760
+ fg: row === 0 && contextOffset === 0 ? PALETTE.title : text.endsWith(':') ? PALETTE.subtitle : PALETTE.text,
673
761
  bold: isTitle,
674
762
  dim: text.length === 0,
675
763
  };
@@ -65,6 +65,8 @@ export interface ShellFooterBuildOptions {
65
65
  * embedded/local mode so no segment renders.
66
66
  */
67
67
  readonly sessionSpineStatus?: 'online' | 'offline';
68
+ /** The web surface's reachable URL, when that surface is enabled; a persistent footer segment. */
69
+ readonly webSurfaceUrl?: string;
68
70
  }
69
71
 
70
72
  export interface ShellFooterBuildResult {
@@ -144,6 +146,7 @@ export function buildShellFooter(
144
146
  options.compact ?? false,
145
147
  options.sessionSpineStatus,
146
148
  options.permissionMode,
149
+ options.webSurfaceUrl,
147
150
  );
148
151
  // Compact posture drops the process indicator and context hint entirely so
149
152
  // the footer fits within ~5 rows on short terminals.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * terminal-bg-probe — OSC 11 terminal-background detection for `auto` theme mode.
3
3
  *
4
- * DEBT-2. On startup, when appearance is `auto` and stdout is a TTY, we ask
4
+ *. On startup, when appearance is `auto` and stdout is a TTY, we ask
5
5
  * the terminal for its background colour with an OSC 11 query and classify the
6
6
  * reply as light or dark. Everything about this is best-effort and conservative:
7
7
  * any timeout, any unparseable reply, any ambiguity resolves to DARK, which is
@@ -21,7 +21,7 @@
21
21
  * DEFAULT_PANEL_PALETTE, DEFAULT_STYLE, FULLSCREEN_PALETTE and
22
22
  * DEFAULT_OVERLAY_PALETTE all read through ONE mode-resolved path instead of
23
23
  * importing the static UI_TONES constant directly. Mode stays 'dark'
24
- * everywhere until the terminal-bg-probe lands (see WO-001) — the light
24
+ * everywhere until the terminal-bg-probe lands (see) — the light
25
25
  * entry is type-complete, not a shipped light theme.
26
26
  */
27
27
 
@@ -173,7 +173,7 @@ export const DARK_THEME: Readonly<ThemeTokens> = DARK;
173
173
  //
174
174
  // UI_TONES (ui-primitives.ts) is the dark entry. The light entry mirrors
175
175
  // dark for every role that has no light-appropriate equivalent yet — the
176
- // WO-001 deliverable is the mode dimension and single read path, not a
176
+ // deliverable is the mode dimension and single read path, not a
177
177
  // shipped light chrome theme (see module doc comment above).
178
178
  // ---------------------------------------------------------------------------
179
179
 
@@ -242,7 +242,7 @@ export function resolveUiTones(mode: ThemeMode): Readonly<UiToneTokens> {
242
242
  }
243
243
 
244
244
  // ===========================================================================
245
- // Active-mode runtime (DEBT-2 terminal-bg-probe landing).
245
+ // Active-mode runtime (terminal-bg-probe landing).
246
246
  //
247
247
  // The mode is decided ONCE at startup — from appearance config (display.themeMode
248
248
  // forced dark/light) or the terminal-background probe (auto) — and is then stable
@@ -173,7 +173,7 @@ export function renderToolCallBlock(
173
173
 
174
174
  // Status icon. 'pending' means the tool is still awaiting a decision
175
175
  // (e.g. an approval prompt) and has NOT run yet — it uses the hollow idle
176
- // glyph so a not-yet-run tool never shows the completed green ✓ (UX-B 2c).
176
+ // glyph so a not-yet-run tool never shows the completed green ✓ (2c).
177
177
  const icon = status === 'done' ? TOOL_STATUS.SUCCESS_ICON
178
178
  : status === 'error' ? TOOL_STATUS.FAIL_ICON
179
179
  : status === 'pending' ? GLYPHS.status.idle
@@ -4,7 +4,7 @@
4
4
  * in conversation-rendering.ts) shows "wrote haiku.txt (532 B)" instead of a
5
5
  * raw `{"files_written":1,"bytes_written":532,...}` JSON blob. The full payload
6
6
  * stays reachable behind the existing collapse/expand toggle — this only
7
- * changes the collapsed preview line, never destroys data. (UX-B item 3.)
7
+ * changes the collapsed preview line, never destroys data. (item 3.)
8
8
  *
9
9
  * Returning `null` means "no honest summary for this shape" — the caller then
10
10
  * falls back to the previous raw-first-line preview, so an unrecognised tool or
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Per-turn knowledge injection record rendering.
3
3
  *
4
- * The SDK's passive per-turn retrieval engine (wo801, W5.1 —
5
- * packages/sdk/src/platform/agents/turn-knowledge-injection.ts) stores one
4
+ * The SDK's passive per-turn retrieval engine
5
+ * (packages/sdk/src/platform/agents/turn-knowledge-injection.ts) stores one
6
6
  * `TurnInjectionRecord` per turn on `AgentRecord.turnInjections` (a bounded
7
7
  * ring, default 20 entries) and appends the same record to the agent's
8
8
  * session transcript as `{type:'knowledge_injection', turn, ...record}`.
@@ -34,10 +34,11 @@ function fmtN(n: number): string {
34
34
  }
35
35
 
36
36
  /**
37
- * Annotate each injected id with its source (Stage B). Code-index hits get a
37
+ * Annotate each injected id with its source. Code-index hits get a
38
38
  * ` [code]` tag; memory records are left bare so a memory-only line renders
39
- * byte-identically to the pre-Stage-B output. `injectedSources` is parallel to
40
- * `injectedIds`; a missing/short entry defaults to memory (no tag).
39
+ * byte-identically to the output from before code-index hits existed.
40
+ * `injectedSources` is parallel to `injectedIds`; a missing/short entry
41
+ * defaults to memory (no tag).
41
42
  */
42
43
  function labelInjectedIds(entry: TurnInjectionEntry): string {
43
44
  const sources = entry.injectedSources ?? [];
@@ -107,7 +108,7 @@ export function buildTurnInjectionsText(agentId: string, entries: readonly TurnI
107
108
 
108
109
  /**
109
110
  * Render the MAIN interactive session's per-turn injection ring
110
- * (`Orchestrator.getTurnInjections()`, wo805), most-recent-turn first, with the
111
+ * (`Orchestrator.getTurnInjections()`), most-recent-turn first, with the
111
112
  * same honest empty state as the per-agent path. The main-session counterpart
112
113
  * to {@link buildTurnInjectionsText}: no agent id, main-session-appropriate
113
114
  * wording, but the identical per-entry rendering (`formatTurnInjectionEntry`).
@@ -202,6 +202,9 @@ export class UIFactory {
202
202
  // Session permission mode (config value). Rendered as a leading pill in the
203
203
  // posture block; undefined suppresses the pill (e.g. bare test callers).
204
204
  permissionMode?: string,
205
+ // The web surface's reachable URL, when that surface is enabled — a
206
+ // persistent low-priority context segment; undefined suppresses it.
207
+ webSurfaceUrl?: string,
205
208
  ): Line[] {
206
209
  const lines: Line[] = [];
207
210
  const promptLines = prompt.split('\n');
@@ -265,7 +268,7 @@ export class UIFactory {
265
268
  }
266
269
  }
267
270
 
268
- // UX-C focus visibility (item 1c): when the panel workspace owns focus
271
+ // focus visibility (item 1c): when the panel workspace owns focus
269
272
  // and the composer is empty, name the state and the way back — the
270
273
  // dimmed prompt alone (TEXT_COLOR/BG_COLOR above) told you nothing was
271
274
  // wrong, but not why keystrokes weren't landing here.
@@ -466,6 +469,8 @@ export class UIFactory {
466
469
  }
467
470
  // Cross-surface spine posture — plain words, no blame. Adopted mode only.
468
471
  if (sessionSpineStatus) ctxParts.push({ text: `spine:${sessionSpineStatus}`, priority: 1 });
472
+ // Web surface reachability — plain, persistent; dropped first under width pressure.
473
+ if (webSurfaceUrl) ctxParts.push({ text: `web:${webSurfaceUrl}`, priority: 2 });
469
474
  if (toolCount) ctxParts.push({ text: `${toolCount} tools`, priority: 2 });
470
475
  // Labeled "notify" (not "hitl") — /mode (aliased /hitl) governs UX
471
476
  // notification verbosity (quiet/balanced/operator), not tool
@@ -67,7 +67,7 @@ export type CreateBootstrapCommandContextOptions = {
67
67
  mcpRegistry: McpRegistry;
68
68
  voiceProviderRegistry?: VoiceProviderRegistry;
69
69
  voiceService?: VoiceService;
70
- /** B31: direct-command consumers (`/search`, `/image`) of already-constructed RuntimeServices. */
70
+ /** Direct-command consumers (`/search`, `/image`) of already-constructed RuntimeServices. */
71
71
  webSearchService?: import('@pellux/goodvibes-sdk/platform/web-search').WebSearchService;
72
72
  mediaProviders?: import('@pellux/goodvibes-sdk/platform/media').MediaProviderRegistry;
73
73
  artifactStore?: import('@pellux/goodvibes-sdk/platform/artifacts').ArtifactStore;
@@ -113,6 +113,7 @@ export type CreateBootstrapCommandContextOptions = {
113
113
  adaptivePlanner?: AdaptivePlanner;
114
114
  sessionOrchestration?: ShellSessionOrchestrationService;
115
115
  operatorClient?: OperatorClient;
116
+ userPermissionRuleStore?: import('@pellux/goodvibes-sdk/platform/permissions').UserPermissionRuleStore;
116
117
  peerClient?: PeerClient;
117
118
  knowledgeApi?: KnowledgeApi;
118
119
  memorySpine?: MemorySpineClient;
@@ -206,6 +207,7 @@ export function createBootstrapCommandContext(
206
207
  adaptivePlanner,
207
208
  sessionOrchestration,
208
209
  operatorClient,
210
+ userPermissionRuleStore,
209
211
  peerClient,
210
212
  knowledgeApi,
211
213
  memorySpine,
@@ -293,6 +295,7 @@ export function createBootstrapCommandContext(
293
295
  }, shellServices);
294
296
  const clients = createBootstrapCommandClientsSection({
295
297
  operatorClient,
298
+ userPermissionRuleStore,
296
299
  peerClient,
297
300
  providerApi,
298
301
  knowledgeApi,
@@ -86,7 +86,7 @@ export interface BootstrapCommandSectionOptions {
86
86
  readonly mcpRegistry: McpRegistry;
87
87
  readonly voiceProviderRegistry?: VoiceProviderRegistry;
88
88
  readonly voiceService?: VoiceService;
89
- /** B31: direct-command consumers (`/search`, `/image`) of already-constructed RuntimeServices. */
89
+ /** Direct-command consumers (`/search`, `/image`) of already-constructed RuntimeServices. */
90
90
  readonly webSearchService?: import('@pellux/goodvibes-sdk/platform/web-search').WebSearchService;
91
91
  readonly mediaProviders?: import('@pellux/goodvibes-sdk/platform/media').MediaProviderRegistry;
92
92
  readonly artifactStore?: import('@pellux/goodvibes-sdk/platform/artifacts').ArtifactStore;
@@ -139,6 +139,7 @@ export interface BootstrapCommandSectionOptions {
139
139
  readonly remoteRuntime?: RemoteCommandService;
140
140
  readonly planRuntime?: PlanRuntimeService;
141
141
  readonly operatorClient?: OperatorClient;
142
+ readonly userPermissionRuleStore?: import('@pellux/goodvibes-sdk/platform/permissions').UserPermissionRuleStore;
142
143
  readonly peerClient?: PeerClient;
143
144
  readonly providerApi?: ProviderApi;
144
145
  readonly knowledgeApi?: KnowledgeApi;
@@ -203,7 +204,7 @@ export function createBootstrapCommandActions(
203
204
  } = options;
204
205
 
205
206
  const showPanel = (panelId: string, pane?: 'top' | 'bottom', target?: PanelDeepLinkTarget) => {
206
- // W6.1 (the purge): a MIGRATE-TO-MODAL id resolves to a modal, not a panel.
207
+ // (the purge): a MIGRATE-TO-MODAL id resolves to a modal, not a panel.
207
208
  // panelManager.open() fires the injected openModal callback and returns a
208
209
  // no-op sentinel — so skip panelManager.show() (which would reveal an empty
209
210
  // panel workspace behind the modal) when this id redirects. Keeps every
@@ -307,7 +308,7 @@ export function createBootstrapCommandActions(
307
308
  openMemoryPanel: () => {
308
309
  showPanel('memory');
309
310
  },
310
- // W6.1: remote/subscription migrated to config-modal surfaces. open() hits
311
+ // remote/subscription migrated to config-modal surfaces. open() hits
311
312
  // the modal redirect and invokes the openModal callback — do NOT go through
312
313
  // showPanel here, which would additionally reveal + focus an (empty) panel
313
314
  // workspace behind the fullscreen modal.
@@ -435,11 +436,12 @@ export function createBootstrapCommandExtensionsSection(
435
436
  export function createBootstrapCommandClientsSection(
436
437
  options: Pick<
437
438
  BootstrapCommandSectionOptions,
438
- 'operatorClient' | 'peerClient' | 'providerApi' | 'knowledgeApi' | 'memorySpine' | 'hookApi' | 'mcpApi' | 'opsApi' | 'directTransport'
439
+ 'operatorClient' | 'userPermissionRuleStore' | 'peerClient' | 'providerApi' | 'knowledgeApi' | 'memorySpine' | 'hookApi' | 'mcpApi' | 'opsApi' | 'directTransport'
439
440
  >,
440
441
  ): BootstrapCommandClientSection {
441
442
  return {
442
443
  operator: options.operatorClient,
444
+ userPermissionRuleStore: options.userPermissionRuleStore,
443
445
  peer: options.peerClient,
444
446
  providerApi: options.providerApi,
445
447
  knowledgeApi: options.knowledgeApi,