@pellux/goodvibes-agent 0.1.117 → 1.0.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 (253) hide show
  1. package/CHANGELOG.md +13 -4
  2. package/README.md +35 -14
  3. package/bin/goodvibes-agent.ts +16 -2
  4. package/dist/package/main.js +176073 -170980
  5. package/docs/README.md +11 -5
  6. package/docs/channels-remote-and-api.md +50 -0
  7. package/docs/connected-host.md +3 -3
  8. package/docs/getting-started.md +29 -15
  9. package/docs/knowledge-artifacts-and-multimodal.md +91 -0
  10. package/docs/project-planning.md +79 -0
  11. package/docs/providers-and-routing.md +46 -0
  12. package/docs/release-and-publishing.md +44 -9
  13. package/docs/tools-and-commands.md +123 -0
  14. package/docs/voice-and-live-tts.md +51 -0
  15. package/package.json +2 -5
  16. package/src/agent/channel-delivery.ts +201 -0
  17. package/src/agent/media-generation.ts +159 -0
  18. package/src/agent/memory-prompt.ts +0 -1
  19. package/src/agent/note-registry.ts +329 -0
  20. package/src/agent/operator-actions.ts +343 -0
  21. package/src/agent/persona-registry.ts +15 -14
  22. package/src/agent/record-labels.ts +107 -0
  23. package/src/agent/reminder-schedule-format.ts +33 -24
  24. package/src/agent/reminder-schedule.ts +26 -25
  25. package/src/agent/routine-registry.ts +13 -12
  26. package/src/agent/routine-schedule-args.ts +2 -1
  27. package/src/agent/routine-schedule-format.ts +77 -53
  28. package/src/agent/routine-schedule-promotion.ts +34 -32
  29. package/src/agent/routine-schedule-receipts.ts +28 -26
  30. package/src/agent/runtime-profile-starters.ts +2 -2
  31. package/src/agent/runtime-profile.ts +18 -17
  32. package/src/agent/skill-registry.ts +25 -24
  33. package/src/cli/agent-knowledge-args.ts +5 -1
  34. package/src/cli/agent-knowledge-command.ts +39 -33
  35. package/src/cli/agent-knowledge-format.ts +80 -67
  36. package/src/cli/agent-knowledge-methods.ts +1 -1
  37. package/src/cli/agent-knowledge-runtime.ts +32 -26
  38. package/src/cli/bundle-command.ts +13 -8
  39. package/src/cli/config-overrides.ts +37 -36
  40. package/src/cli/external-runtime.ts +10 -4
  41. package/src/cli/help.ts +29 -11
  42. package/src/cli/local-library-command.ts +134 -68
  43. package/src/cli/management-commands.ts +98 -62
  44. package/src/cli/management.ts +52 -26
  45. package/src/cli/memory-command.ts +66 -32
  46. package/src/cli/parser.ts +37 -24
  47. package/src/cli/profiles-command.ts +52 -35
  48. package/src/cli/provider-auth-routes.ts +2 -1
  49. package/src/cli/routines-command.ts +44 -36
  50. package/src/cli/service-posture.ts +6 -6
  51. package/src/cli/status.ts +46 -121
  52. package/src/core/conversation-message-snapshot.ts +131 -0
  53. package/src/input/agent-workspace-activation.ts +33 -7
  54. package/src/input/agent-workspace-basic-command-editor-submission.ts +7 -3
  55. package/src/input/agent-workspace-basic-command-editors.ts +30 -10
  56. package/src/input/agent-workspace-categories.ts +276 -64
  57. package/src/input/agent-workspace-channel-command-editor-submission.ts +34 -0
  58. package/src/input/agent-workspace-channel-command-editors.ts +23 -5
  59. package/src/input/agent-workspace-channels.ts +35 -2
  60. package/src/input/agent-workspace-command-editor.ts +18 -2
  61. package/src/input/agent-workspace-config-reader.ts +16 -0
  62. package/src/input/agent-workspace-delegation-editor-submission.ts +1 -1
  63. package/src/input/agent-workspace-editors.ts +140 -2
  64. package/src/input/agent-workspace-knowledge-query-editor.ts +1 -1
  65. package/src/input/agent-workspace-learned-behavior.ts +2 -2
  66. package/src/input/agent-workspace-library-command-editor-submission.ts +1 -1
  67. package/src/input/agent-workspace-library-command-editors.ts +2 -2
  68. package/src/input/agent-workspace-local-operations.ts +218 -0
  69. package/src/input/agent-workspace-local-selection.ts +75 -0
  70. package/src/input/agent-workspace-media-command-editor-submission.ts +62 -0
  71. package/src/input/agent-workspace-media-command-editors.ts +27 -0
  72. package/src/input/agent-workspace-memory-command-editors.ts +1 -1
  73. package/src/input/agent-workspace-memory-editor.ts +2 -2
  74. package/src/input/agent-workspace-navigation.ts +38 -2
  75. package/src/input/agent-workspace-notify-editor-submission.ts +16 -2
  76. package/src/input/agent-workspace-operations-command-editor-submission.ts +63 -1
  77. package/src/input/agent-workspace-operations-command-editors.ts +80 -3
  78. package/src/input/agent-workspace-panel-route.ts +43 -0
  79. package/src/input/agent-workspace-reminder-schedule-editor.ts +1 -1
  80. package/src/input/agent-workspace-routine-schedule-editor.ts +2 -2
  81. package/src/input/agent-workspace-search.ts +169 -0
  82. package/src/input/agent-workspace-setup.ts +22 -11
  83. package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +6 -6
  84. package/src/input/agent-workspace-skill-bundle-command-editors.ts +10 -10
  85. package/src/input/agent-workspace-snapshot.ts +112 -13
  86. package/src/input/agent-workspace-task-command-editors.ts +4 -4
  87. package/src/input/agent-workspace-token.ts +18 -2
  88. package/src/input/agent-workspace-types.ts +92 -4
  89. package/src/input/agent-workspace-voice-media.ts +3 -6
  90. package/src/input/agent-workspace-web-research-editor.ts +104 -0
  91. package/src/input/agent-workspace.ts +136 -208
  92. package/src/input/command-registry.ts +4 -1
  93. package/src/input/commands/agent-runtime-profile-runtime.ts +45 -41
  94. package/src/input/commands/agent-skills-runtime.ts +83 -70
  95. package/src/input/commands/agent-workspace-runtime.ts +23 -7
  96. package/src/input/commands/brief-runtime.ts +25 -24
  97. package/src/input/commands/channels-runtime.ts +145 -31
  98. package/src/input/commands/delegation-runtime.ts +29 -23
  99. package/src/input/commands/experience-runtime.ts +6 -4
  100. package/src/input/commands/guidance-runtime.ts +4 -4
  101. package/src/input/commands/health-runtime.ts +140 -115
  102. package/src/input/commands/knowledge.ts +57 -56
  103. package/src/input/commands/local-provider-runtime.ts +36 -18
  104. package/src/input/commands/local-runtime.ts +138 -16
  105. package/src/input/commands/local-setup-review.ts +7 -7
  106. package/src/input/commands/mcp-runtime.ts +56 -35
  107. package/src/input/commands/notify-runtime.ts +38 -9
  108. package/src/input/commands/operator-actions-runtime.ts +138 -0
  109. package/src/input/commands/operator-runtime.ts +6 -17
  110. package/src/input/commands/personas-runtime.ts +45 -30
  111. package/src/input/commands/planning-runtime.ts +1 -1
  112. package/src/input/commands/platform-access-runtime.ts +29 -29
  113. package/src/input/commands/provider-accounts-runtime.ts +60 -39
  114. package/src/input/commands/qrcode-runtime.ts +45 -6
  115. package/src/input/commands/recall-bundle.ts +11 -11
  116. package/src/input/commands/recall-capture.ts +13 -11
  117. package/src/input/commands/recall-query.ts +29 -21
  118. package/src/input/commands/recall-review.ts +2 -1
  119. package/src/input/commands/routines-runtime.ts +59 -43
  120. package/src/input/commands/schedule-runtime.ts +33 -20
  121. package/src/input/commands/security-runtime.ts +4 -4
  122. package/src/input/commands/session-content.ts +80 -78
  123. package/src/input/commands/session-workflow.ts +132 -93
  124. package/src/input/commands/session.ts +3 -174
  125. package/src/input/commands/shell-core.ts +32 -17
  126. package/src/input/commands/subscription-runtime.ts +53 -179
  127. package/src/input/commands/tasks-runtime.ts +20 -20
  128. package/src/input/commands/work-plan-runtime.ts +33 -8
  129. package/src/input/commands.ts +2 -2
  130. package/src/input/feed-context-factory.ts +2 -1
  131. package/src/input/file-picker.ts +3 -2
  132. package/src/input/handler-command-route.ts +4 -7
  133. package/src/input/handler-content-actions.ts +89 -1
  134. package/src/input/handler-feed-routes.ts +19 -12
  135. package/src/input/handler-feed.ts +6 -3
  136. package/src/input/handler-interactions.ts +7 -5
  137. package/src/input/handler-modal-stack.ts +3 -3
  138. package/src/input/handler-onboarding.ts +24 -80
  139. package/src/input/handler-shortcuts.ts +15 -4
  140. package/src/input/handler.ts +47 -17
  141. package/src/input/input-history.ts +5 -6
  142. package/src/input/keybindings.ts +22 -11
  143. package/src/input/onboarding/onboarding-wizard-apply.ts +13 -0
  144. package/src/input/onboarding/onboarding-wizard-constants.ts +1 -0
  145. package/src/input/onboarding/onboarding-wizard-operator-steps.ts +147 -57
  146. package/src/input/onboarding/onboarding-wizard-steps.ts +46 -29
  147. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  148. package/src/input/onboarding/onboarding-wizard.ts +68 -0
  149. package/src/input/profile-picker-modal.ts +31 -12
  150. package/src/input/session-picker-modal.ts +21 -4
  151. package/src/input/settings-modal-behavior.ts +0 -3
  152. package/src/input/settings-modal-subscriptions.ts +3 -3
  153. package/src/input/settings-modal-types.ts +2 -13
  154. package/src/input/settings-modal.ts +6 -52
  155. package/src/input/submission-intent.ts +0 -1
  156. package/src/input/submission-router.ts +3 -3
  157. package/src/main.ts +18 -8
  158. package/src/panels/approval-panel.ts +8 -8
  159. package/src/panels/automation-control-panel.ts +2 -2
  160. package/src/panels/base-panel.ts +1 -1
  161. package/src/panels/builtin/agent.ts +1 -1
  162. package/src/panels/builtin/operations.ts +1 -10
  163. package/src/panels/builtin/session.ts +9 -9
  164. package/src/panels/builtin/shared.ts +2 -2
  165. package/src/panels/cost-tracker-panel.ts +1 -1
  166. package/src/panels/docs-panel.ts +5 -3
  167. package/src/panels/index.ts +0 -1
  168. package/src/panels/knowledge-panel.ts +6 -5
  169. package/src/panels/memory-panel.ts +7 -6
  170. package/src/panels/panel-list-panel.ts +36 -80
  171. package/src/panels/project-planning-panel.ts +18 -11
  172. package/src/panels/provider-account-snapshot.ts +51 -25
  173. package/src/panels/provider-accounts-panel.ts +33 -18
  174. package/src/panels/provider-health-domains.ts +45 -4
  175. package/src/panels/provider-health-panel.ts +5 -4
  176. package/src/panels/qr-panel.ts +1 -1
  177. package/src/panels/schedule-panel.ts +9 -17
  178. package/src/panels/security-panel.ts +8 -8
  179. package/src/panels/session-browser-panel.ts +10 -10
  180. package/src/panels/subscription-panel.ts +3 -3
  181. package/src/panels/system-messages-panel.ts +1 -1
  182. package/src/panels/tasks-panel.ts +20 -13
  183. package/src/panels/tool-inspector-panel.ts +19 -12
  184. package/src/panels/work-plan-panel.ts +5 -5
  185. package/src/planning/project-planning-coordinator.ts +1 -1
  186. package/src/provider-auth-route-display.ts +9 -0
  187. package/src/renderer/agent-workspace-style.ts +34 -0
  188. package/src/renderer/agent-workspace.ts +228 -52
  189. package/src/renderer/autocomplete-overlay.ts +25 -6
  190. package/src/renderer/bookmark-modal.ts +19 -4
  191. package/src/renderer/buffer.ts +4 -2
  192. package/src/renderer/context-inspector.ts +50 -13
  193. package/src/renderer/diff.ts +1 -1
  194. package/src/renderer/file-picker-overlay.ts +19 -6
  195. package/src/renderer/help-overlay.ts +106 -33
  196. package/src/renderer/history-search-overlay.ts +19 -4
  197. package/src/renderer/live-tail-modal.ts +27 -5
  198. package/src/renderer/mcp-workspace.ts +164 -50
  199. package/src/renderer/model-picker-overlay.ts +58 -2
  200. package/src/renderer/model-workspace.ts +104 -20
  201. package/src/renderer/process-modal.ts +27 -6
  202. package/src/renderer/profile-picker-modal.ts +20 -4
  203. package/src/renderer/search-overlay.ts +25 -5
  204. package/src/renderer/selection-modal-overlay.ts +46 -14
  205. package/src/renderer/session-picker-modal.ts +18 -1
  206. package/src/renderer/settings-modal-helpers.ts +2 -40
  207. package/src/renderer/settings-modal.ts +83 -50
  208. package/src/renderer/tool-call.ts +20 -11
  209. package/src/runtime/agent-runtime-events.ts +6 -6
  210. package/src/runtime/bootstrap-command-context.ts +7 -1
  211. package/src/runtime/bootstrap-command-parts.ts +9 -7
  212. package/src/runtime/bootstrap-core.ts +19 -1
  213. package/src/runtime/bootstrap-hook-bridge.ts +7 -18
  214. package/src/runtime/bootstrap-shell.ts +4 -4
  215. package/src/runtime/bootstrap.ts +31 -22
  216. package/src/runtime/connected-host-auth.ts +7 -2
  217. package/src/runtime/diagnostics/panels/index.ts +2 -2
  218. package/src/runtime/diagnostics/panels/policy.ts +7 -7
  219. package/src/runtime/index.ts +2 -1
  220. package/src/runtime/onboarding/apply-file-helpers.ts +66 -0
  221. package/src/runtime/onboarding/apply.ts +80 -79
  222. package/src/runtime/onboarding/derivation.ts +5 -8
  223. package/src/runtime/onboarding/snapshot.ts +0 -15
  224. package/src/runtime/onboarding/types.ts +8 -19
  225. package/src/runtime/onboarding/verify.ts +32 -10
  226. package/src/runtime/operator-token-cleanup.ts +1 -1
  227. package/src/runtime/services.ts +91 -22
  228. package/src/runtime/store/selectors/index.ts +3 -3
  229. package/src/runtime/store/state.ts +1 -4
  230. package/src/runtime/surface-feature-flags.ts +41 -6
  231. package/src/runtime/ui-read-models.ts +3 -4
  232. package/src/runtime/ui-services.ts +6 -6
  233. package/src/shell/blocking-input.ts +2 -1
  234. package/src/shell/ui-openers.ts +3 -13
  235. package/src/tools/agent-analysis-registry-policy.ts +0 -1
  236. package/src/tools/agent-channel-send-tool.ts +133 -0
  237. package/src/tools/agent-context-policy.ts +1 -1
  238. package/src/tools/agent-knowledge-ingest-tool.ts +405 -0
  239. package/src/tools/agent-knowledge-tool.ts +170 -0
  240. package/src/tools/agent-local-registry-tool.ts +269 -69
  241. package/src/tools/agent-media-generate-tool.ts +133 -0
  242. package/src/tools/agent-notify-tool.ts +143 -0
  243. package/src/tools/agent-operator-action-tool.ts +137 -0
  244. package/src/tools/agent-operator-briefing-tool.ts +217 -0
  245. package/src/tools/agent-reminder-schedule-tool.ts +237 -0
  246. package/src/tools/agent-tool-policy-guard.ts +8 -8
  247. package/src/tools/agent-work-plan-tool.ts +256 -0
  248. package/src/version.ts +1 -1
  249. package/src/input/commands/policy-dispatch.ts +0 -339
  250. package/src/input/commands/policy.ts +0 -13
  251. package/src/panels/panel-picker.ts +0 -105
  252. package/src/panels/policy-panel.ts +0 -308
  253. package/src/renderer/panel-picker-overlay.ts +0 -202
@@ -1,11 +1,11 @@
1
1
  import type {
2
2
  AgentWorkspace,
3
3
  AgentWorkspaceAction,
4
- AgentWorkspaceActionResult,
5
4
  AgentWorkspaceCategory,
6
5
  AgentWorkspaceLocalEditor,
7
6
  AgentWorkspaceRuntimeSnapshot,
8
7
  } from '../input/agent-workspace.ts';
8
+ import { formatAgentRecordReviewState, formatAgentRecordSource } from '../agent/record-labels.ts';
9
9
  import type { Line } from '../types/grid.ts';
10
10
  import { wrapText } from '../utils/terminal-width.ts';
11
11
  import { GLYPHS } from './ui-primitives.ts';
@@ -17,20 +17,7 @@ import {
17
17
  WORKSPACE_PALETTE as PALETTE,
18
18
  type WorkspaceRow,
19
19
  } from './fullscreen-workspace.ts';
20
-
21
- function safetyColor(action: AgentWorkspaceAction): string {
22
- if (action.safety === 'safe') return PALETTE.good;
23
- if (action.safety === 'read-only') return PALETTE.info;
24
- if (action.safety === 'delegates') return PALETTE.warn;
25
- return PALETTE.bad;
26
- }
27
-
28
- function actionResultColor(result: AgentWorkspaceActionResult): string {
29
- if (result.kind === 'blocked' || result.kind === 'error') return PALETTE.bad;
30
- if (result.kind === 'dispatched') return PALETTE.info;
31
- if (result.kind === 'refreshed') return PALETTE.good;
32
- return PALETTE.muted;
33
- }
20
+ import { actionResultColor, safetyColor, setupStatusColor, type AgentWorkspaceContextLine as ContextLine } from './agent-workspace-style.ts';
34
21
 
35
22
  function buildLeftRows(workspace: AgentWorkspace, height: number): WorkspaceRow[] {
36
23
  const rows: WorkspaceRow[] = [];
@@ -75,15 +62,6 @@ function actionCommand(action: AgentWorkspaceAction): string {
75
62
  return action.command ?? '(guidance)';
76
63
  }
77
64
 
78
- type ContextLine = { readonly text: string; readonly fg?: string; readonly bold?: boolean; readonly dim?: boolean };
79
-
80
- function setupStatusColor(status: AgentWorkspaceRuntimeSnapshot['setupChecklist'][number]['status']): string {
81
- if (status === 'ready') return PALETTE.good;
82
- if (status === 'recommended') return PALETTE.warn;
83
- if (status === 'blocked') return PALETTE.bad;
84
- return PALETTE.muted;
85
- }
86
-
87
65
  function setupChecklistLines(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLine[] {
88
66
  const readyCount = snapshot.setupChecklist.filter((item) => item.status === 'ready').length;
89
67
  const recommendedCount = snapshot.setupChecklist.filter((item) => item.status === 'recommended').length;
@@ -104,22 +82,60 @@ function setupChecklistLines(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLi
104
82
  return lines;
105
83
  }
106
84
 
107
- function discoverySummaryLine(label: string, summary: AgentWorkspaceRuntimeSnapshot['discoveredBehavior']['personas'], command: string): ContextLine[] {
85
+ function homeNextActionLines(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLine[] {
86
+ const blocked = snapshot.setupChecklist.filter((item) => item.status === 'blocked');
87
+ const recommended = snapshot.setupChecklist.filter((item) => item.status === 'recommended');
88
+ const optional = snapshot.setupChecklist.filter((item) => item.status === 'optional');
89
+ const candidates = [...blocked, ...recommended, ...optional].slice(0, 5);
90
+ if (candidates.length === 0) {
91
+ return [
92
+ { text: 'Next Actions', fg: PALETTE.title, bold: true },
93
+ { text: 'Core setup is ready. Continue normal assistant work, review Knowledge sources, or tune local skills/routines as needed.', fg: PALETTE.good },
94
+ ];
95
+ }
96
+ const lines: ContextLine[] = [{ text: 'Next Actions', fg: PALETTE.title, bold: true }];
97
+ for (const item of candidates) {
98
+ const command = item.command ? ` -> ${item.command}` : '';
99
+ lines.push({
100
+ text: `${item.status.toUpperCase()} ${item.label}${command}`,
101
+ fg: setupStatusColor(item.status),
102
+ bold: item.status === 'blocked',
103
+ });
104
+ lines.push({ text: ` ${item.detail}`, fg: PALETTE.muted });
105
+ }
106
+ return lines;
107
+ }
108
+
109
+ function companionAccessLine(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLine {
110
+ const access = snapshot.companionAccess;
111
+ const tokenState = access.tokenReadable
112
+ ? `ready sha256:${access.tokenFingerprint ?? 'unknown'}`
113
+ : access.tokenPresent
114
+ ? 'present but unreadable'
115
+ : 'missing';
116
+ const error = access.tokenError ? `; token read error ${access.tokenError}` : '';
117
+ return {
118
+ text: `Companion: ${access.surface}; token ${tokenState}; QR ${access.qrCommand}; manual token text hidden${error}.`,
119
+ fg: access.pairingReady ? PALETTE.good : PALETTE.warn,
120
+ };
121
+ }
122
+
123
+ function discoverySummaryLine(label: string, summary: AgentWorkspaceRuntimeSnapshot['discoveredBehavior']['personas'], actionLabel: string): ContextLine[] {
108
124
  if (summary.count === 0) return [];
109
125
  const names = summary.names.length > 0
110
126
  ? ` ${summary.names.join(', ')}${summary.count > summary.names.length ? `, +${summary.count - summary.names.length} more` : ''}.`
111
127
  : '';
112
128
  return [
113
129
  { text: `${label}: ${summary.count} discovered; project ${summary.projectLocalCount}; global ${summary.globalCount}.`, fg: PALETTE.info, bold: true },
114
- { text: ` ${command} to preview, then use the import form after review.${names}`, fg: PALETTE.muted },
130
+ { text: ` Choose ${actionLabel} to preview, then use the import form after review.${names}`, fg: PALETTE.muted },
115
131
  ];
116
132
  }
117
133
 
118
134
  function behaviorDiscoveryLines(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLine[] {
119
135
  const lines: ContextLine[] = [
120
- ...discoverySummaryLine('Discovered personas', snapshot.discoveredBehavior.personas, '/personas discover'),
121
- ...discoverySummaryLine('Discovered skills', snapshot.discoveredBehavior.skills, '/agent-skills discover'),
122
- ...discoverySummaryLine('Discovered routines', snapshot.discoveredBehavior.routines, '/routines discover'),
136
+ ...discoverySummaryLine('Discovered personas', snapshot.discoveredBehavior.personas, 'Personas -> Discover persona files'),
137
+ ...discoverySummaryLine('Discovered skills', snapshot.discoveredBehavior.skills, 'Skills -> Discover skill files'),
138
+ ...discoverySummaryLine('Discovered routines', snapshot.discoveredBehavior.routines, 'Routines -> Discover routine files'),
123
139
  ];
124
140
  if (lines.length === 0) return [];
125
141
  return [
@@ -153,11 +169,12 @@ function localLibraryLines(
153
169
  item.requirementCount !== undefined && item.requirementCount > 0
154
170
  ? (item.missingRequirementCount && item.missingRequirementCount > 0 ? `needs ${item.missingRequirementCount}/${item.requirementCount}` : `ready ${item.requirementCount}/${item.requirementCount}`)
155
171
  : '',
156
- item.reviewState,
172
+ formatAgentRecordReviewState(item.reviewState),
173
+ item.source ? `origin ${item.source}` : '',
157
174
  item.startCount !== undefined ? `starts ${item.startCount}` : '',
158
175
  ].filter(Boolean).join(' / ');
159
- const tags = item.tags.length > 0 ? ` tags=${item.tags.join(',')}` : '';
160
- const triggers = item.triggers.length > 0 ? ` triggers=${item.triggers.join(',')}` : '';
176
+ const tags = item.tags.length > 0 ? ` tags ${item.tags.join(',')}` : '';
177
+ const triggers = item.triggers.length > 0 ? ` triggers ${item.triggers.join(',')}` : '';
161
178
  const marker = selected ? `${GLYPHS.navigation.selected} ` : '';
162
179
  lines.push({
163
180
  text: `${marker}${item.id}: ${item.name} (${status})`,
@@ -175,6 +192,86 @@ function localLibraryLines(
175
192
  return lines;
176
193
  }
177
194
 
195
+ type LocalRoutineItem = AgentWorkspaceRuntimeSnapshot['localRoutines'][number];
196
+
197
+ function readyRoutineItems(snapshot: AgentWorkspaceRuntimeSnapshot): readonly LocalRoutineItem[] {
198
+ return snapshot.localRoutines.filter((routine) =>
199
+ routine.enabled === true
200
+ && routine.reviewState === 'reviewed'
201
+ && (routine.missingRequirementCount ?? 0) === 0
202
+ );
203
+ }
204
+
205
+ function routinesNeedingSetup(snapshot: AgentWorkspaceRuntimeSnapshot): readonly LocalRoutineItem[] {
206
+ return snapshot.localRoutines.filter((routine) => (routine.missingRequirementCount ?? 0) > 0);
207
+ }
208
+
209
+ function routinesNeedingReview(snapshot: AgentWorkspaceRuntimeSnapshot): readonly LocalRoutineItem[] {
210
+ return snapshot.localRoutines.filter((routine) => routine.reviewState !== 'reviewed');
211
+ }
212
+
213
+ function routineNextActionLine(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLine {
214
+ const discovered = snapshot.discoveredBehavior.routines.count;
215
+ const ready = readyRoutineItems(snapshot);
216
+ const needsSetup = routinesNeedingSetup(snapshot);
217
+ const needsReview = routinesNeedingReview(snapshot);
218
+
219
+ if (snapshot.localRoutineCount === 0 && discovered > 0) {
220
+ return { text: 'Next routine action: Discover routine files, preview one, then import it from this workspace.', fg: PALETTE.warn, bold: true };
221
+ }
222
+ if (snapshot.localRoutineCount === 0) {
223
+ return { text: 'Next routine action: Create routine for a repeatable main-conversation workflow.', fg: PALETTE.warn, bold: true };
224
+ }
225
+ if (needsSetup.length > 0) {
226
+ return { text: `Next routine action: Needs setup for ${needsSetup[0]?.id ?? 'a routine'} before it can be trusted for schedule promotion.`, fg: PALETTE.warn, bold: true };
227
+ }
228
+ if (needsReview.length > 0) {
229
+ return { text: `Next routine action: Review selected or inspect ${needsReview[0]?.id ?? 'a routine'} before schedule promotion.`, fg: PALETTE.warn, bold: true };
230
+ }
231
+ if (ready.length > 0 && snapshot.routineScheduleReceiptCount === 0) {
232
+ return { text: `Next routine action: Promote ${ready[0]?.id ?? 'a reviewed routine'} to a connected schedule when you have a real cadence.`, fg: PALETTE.good, bold: true };
233
+ }
234
+ return { text: 'Next routine action: Start selected in the main conversation, inspect receipts, or reconcile connected schedules.', fg: PALETTE.info, bold: true };
235
+ }
236
+
237
+ function routineScheduleReceiptLines(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLine[] {
238
+ const latest = snapshot.latestRoutineScheduleReceipt;
239
+ const lines: ContextLine[] = [
240
+ {
241
+ text: `Promotion receipts: ${snapshot.routineScheduleReceiptCount}; created: ${snapshot.successfulRoutineScheduleReceiptCount}; failed: ${snapshot.failedRoutineScheduleReceiptCount}`,
242
+ fg: snapshot.failedRoutineScheduleReceiptCount > 0 ? PALETTE.warn : PALETTE.info,
243
+ },
244
+ ];
245
+ if (latest) {
246
+ lines.push({
247
+ text: `Latest receipt: ${latest.id} ${latest.status} routine=${latest.routineId} schedule="${latest.scheduleName}" ${latest.scheduleKind} ${latest.scheduleValue}`,
248
+ fg: latest.status === 'failed' ? PALETTE.warn : PALETTE.good,
249
+ });
250
+ } else {
251
+ lines.push({ text: 'No schedule promotion receipts yet. Confirm a routine promotion from Automation when ready.', fg: PALETTE.muted });
252
+ }
253
+ return lines;
254
+ }
255
+
256
+ function automationNextActionLine(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLine {
257
+ const ready = readyRoutineItems(snapshot);
258
+ const needsSetup = routinesNeedingSetup(snapshot);
259
+ const needsReview = routinesNeedingReview(snapshot);
260
+ if (snapshot.routineScheduleReceiptCount > 0) {
261
+ return { text: 'Next automation action: Reconcile schedules to compare local receipts with the connected host.', fg: PALETTE.info, bold: true };
262
+ }
263
+ if (ready.length > 0) {
264
+ return { text: `Next automation action: Promote ${ready[0]?.id ?? 'a reviewed routine'} or create a one-off reminder.`, fg: PALETTE.good, bold: true };
265
+ }
266
+ if (needsSetup.length > 0) {
267
+ return { text: `Next automation action: Resolve routine setup gaps in ${needsSetup[0]?.id ?? 'Routines'} before promotion, or create a reminder.`, fg: PALETTE.warn, bold: true };
268
+ }
269
+ if (needsReview.length > 0) {
270
+ return { text: `Next automation action: Review ${needsReview[0]?.id ?? 'a routine'} in Routines before schedule promotion, or create a reminder.`, fg: PALETTE.warn, bold: true };
271
+ }
272
+ return { text: 'Next automation action: Create a reminder, or create/import a routine before recurring workflow promotion.', fg: PALETTE.warn, bold: true };
273
+ }
274
+
178
275
  function profileLines(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLine[] {
179
276
  const lines: ContextLine[] = [
180
277
  { text: 'Agent Profiles', fg: PALETTE.title, bold: true },
@@ -185,7 +282,7 @@ function profileLines(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLine[] {
185
282
  }
186
283
  for (const profile of snapshot.runtimeProfiles.slice(0, 6)) {
187
284
  const starter = profile.starterTemplateId ? ` starter=${profile.starterTemplateId}` : ' starter=none';
188
- const created = profile.createdAt ? ` created=${profile.createdAt.slice(0, 10)}` : '';
285
+ const created = profile.createdAt ? ` created ${profile.createdAt.slice(0, 10)}` : '';
189
286
  const states = [
190
287
  profile.id === snapshot.activeRuntimeProfile ? 'active' : '',
191
288
  profile.id === snapshot.selectedRuntimeProfile ? 'default' : '',
@@ -209,8 +306,9 @@ function starterTemplateLines(snapshot: AgentWorkspaceRuntimeSnapshot): ContextL
209
306
  { text: 'Starter Templates', fg: PALETTE.title, bold: true },
210
307
  ];
211
308
  for (const template of snapshot.runtimeStarterTemplates.slice(0, 6)) {
309
+ const origin = template.source === 'local' ? 'Local' : formatAgentRecordSource(template.source);
212
310
  lines.push({
213
- text: `${template.id}: ${template.name} [${template.source}]`,
311
+ text: `${template.id}: ${template.name} [${origin}]`,
214
312
  fg: template.source === 'local' ? PALETTE.good : PALETTE.info,
215
313
  bold: template.id === 'research',
216
314
  });
@@ -232,7 +330,12 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
232
330
  base.push(
233
331
  { text: `Chat route: ${snapshot.provider} / ${snapshot.modelDisplayName}`, fg: PALETTE.info },
234
332
  { text: `Session: ${snapshot.sessionId}`, fg: PALETTE.muted },
235
- { text: `Policy: ${snapshot.executionPolicy}; WRFC ${snapshot.wrfcPolicy}`, fg: PALETTE.good },
333
+ { text: `Policy: ${snapshot.executionPolicy}; delegated review ${snapshot.delegatedReviewPolicy}`, fg: PALETTE.good },
334
+ { text: `Knowledge: ${snapshot.knowledgeRoute}; ${snapshot.knowledgeIsolation}; no fallback`, fg: PALETTE.good },
335
+ { text: `Local: ${snapshot.localMemoryCount} memory, ${snapshot.localNoteCount} notes, ${snapshot.localPersonaCount} personas, ${snapshot.localSkillCount} skills, ${snapshot.localRoutineCount} routines.`, fg: PALETTE.info },
336
+ { text: `Channels: ${snapshot.channels.filter((channel) => channel.ready).length}/${snapshot.channels.length} ready; MCP ${snapshot.mcpConnectedServerCount}/${snapshot.mcpServerCount} connected; voice/media ${snapshot.voiceProviderCount}/${snapshot.mediaProviderCount}.`, fg: PALETTE.info },
337
+ { text: '' },
338
+ ...homeNextActionLines(snapshot),
236
339
  );
237
340
  } else if (category.id === 'setup') {
238
341
  base.push(
@@ -244,6 +347,17 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
244
347
  { text: `Workspace: ${snapshot.workingDirectory}`, fg: PALETTE.muted },
245
348
  { text: `Home: ${snapshot.homeDirectory}`, fg: PALETTE.muted },
246
349
  );
350
+ } else if (category.id === 'artifacts') {
351
+ const mediaReady = snapshot.voiceMediaReadiness.readyMediaProviderCount;
352
+ base.push(
353
+ { text: `Chat route: ${snapshot.provider} / ${snapshot.modelDisplayName}`, fg: PALETTE.info },
354
+ { text: `Agent Knowledge route: ${snapshot.knowledgeRoute}`, fg: PALETTE.info },
355
+ { text: `Media providers: ${mediaReady}/${snapshot.mediaProviderCount} ready; generation-capable ${snapshot.mediaGenerationProviderCount}.`, fg: mediaReady > 0 ? PALETTE.good : PALETTE.warn },
356
+ { text: 'Use this area for concrete files and generated output: attach images, export transcripts, ingest reviewed sources, inspect source records, and generate media.', fg: PALETTE.good },
357
+ { text: 'Agent Knowledge ingest writes only to the isolated Agent segment. Default knowledge and non-Agent routes are not fallback storage.', fg: PALETTE.warn },
358
+ { text: 'Generated media is stored as artifacts and referenced by id; the TUI should not print inline base64.', fg: PALETTE.muted },
359
+ { text: `Workspace path: ${snapshot.workingDirectory}`, fg: PALETTE.muted },
360
+ );
247
361
  } else if (category.id === 'channels') {
248
362
  const enabledCount = snapshot.channels.filter((channel) => channel.enabled).length;
249
363
  const readyCount = snapshot.channels.filter((channel) => channel.ready).length;
@@ -251,16 +365,20 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
251
365
  const readyChannels = snapshot.channels.filter((channel) => channel.ready).map((channel) => channel.label);
252
366
  const needsTarget = snapshot.channels.filter((channel) => channel.setupState === 'needs-target');
253
367
  const needsConfig = snapshot.channels.filter((channel) => channel.setupState === 'needs-config');
368
+ const nextAttentionChannel = needsConfig[0] ?? needsTarget[0] ?? snapshot.channels.find((channel) => !channel.enabled);
254
369
  const disabledChannels = snapshot.channels.filter((channel) => !channel.enabled).map((channel) => channel.label);
255
370
  const disabledPreview = disabledChannels.slice(0, 6).join(', ');
256
371
  const disabledSuffix = disabledChannels.length > 6 ? `, +${disabledChannels.length - 6} more` : '';
257
372
  const orderedChannels = [
258
373
  ...snapshot.channels.filter((channel) => channel.enabled),
259
374
  ...snapshot.channels.filter((channel) => !channel.enabled),
260
- ].slice(0, 6);
375
+ ].slice(0, 3);
261
376
  base.push(
262
377
  { text: `GoodVibes API: ${snapshot.runtimeBaseUrl}`, fg: PALETTE.info },
378
+ companionAccessLine(snapshot),
263
379
  { text: `Readiness: ${readyCount}/${snapshot.channels.length} ready; ${enabledCount} enabled; ${configuredDefaults} default target(s) configured.`, fg: PALETTE.info },
380
+ { text: 'Setup path: pair companion -> inspect readiness -> review accounts/policies/status -> fix one channel -> add explicit notification target if needed.', fg: PALETTE.good },
381
+ { text: `Next channel action: ${nextAttentionChannel ? `${nextAttentionChannel.label} - ${nextAttentionChannel.nextStep}` : 'All enabled channels are ready; keep delivery explicit and review policies before sending.'}`, fg: nextAttentionChannel ? PALETTE.warn : PALETTE.good },
264
382
  { text: `Ready channels: ${readyChannels.join(', ') || 'none'}.`, fg: readyChannels.length > 0 ? PALETTE.good : PALETTE.warn },
265
383
  { text: `Needs default target: ${needsTarget.map((channel) => `${channel.label} -> ${channel.defaultTargetKeys.join('|')}`).join(', ') || 'none'}.`, fg: needsTarget.length > 0 ? PALETTE.warn : PALETTE.muted },
266
384
  { text: `Needs config: ${needsConfig.map((channel) => `${channel.label} -> ${channel.missingRequiredKeys.join('|')}`).join(', ') || 'none'}.`, fg: needsConfig.length > 0 ? PALETTE.warn : PALETTE.muted },
@@ -274,16 +392,25 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
274
392
  fg: channel.ready ? PALETTE.good : channel.enabled ? PALETTE.warn : PALETTE.dim,
275
393
  });
276
394
  }
277
- base.push({ text: 'Only config key names and readiness state are rendered here.', fg: PALETTE.muted });
278
- base.push({ text: 'Pairing: use /pair or /qrcode; inspect notification targets with /notify list and health with /health review.', fg: PALETTE.info });
279
395
  } else if (category.id === 'knowledge') {
280
396
  base.push(
281
397
  { text: `Route family: ${snapshot.knowledgeRoute}/{status,ask,search}`, fg: PALETTE.info },
282
- { text: `Isolation: ${snapshot.knowledgeIsolation}; no default Knowledge/Wiki or non-Agent fallback`, fg: PALETTE.good },
398
+ { text: `Isolation: ${snapshot.knowledgeIsolation}; no default knowledge or non-Agent fallback`, fg: PALETTE.good },
283
399
  { text: 'Ingest: URL, URL-list, and bookmark imports require explicit --yes and write only to Agent Knowledge.', fg: PALETTE.info },
284
400
  { text: 'Review: queue, issues, candidates, reports, reindex, and consolidation stay inside the Agent segment.', fg: PALETTE.muted },
285
401
  { text: 'Agent-owned content appears here only after explicit Agent knowledge ingestion.', fg: PALETTE.muted },
286
402
  );
403
+ } else if (category.id === 'research') {
404
+ base.push(
405
+ { text: `Chat route: ${snapshot.provider} / ${snapshot.modelDisplayName}`, fg: PALETTE.info },
406
+ { text: `Agent Knowledge route: ${snapshot.knowledgeRoute}`, fg: PALETTE.info },
407
+ { text: `Browser tools: ${snapshot.voiceMediaReadiness.browserToolState}; public base URL ${snapshot.browserToolPublicBaseUrl}.`, fg: snapshot.browserToolExposureEnabled ? PALETTE.warn : PALETTE.muted },
408
+ { text: snapshot.voiceMediaReadiness.browserToolNextStep, fg: PALETTE.muted },
409
+ { text: 'Research requests are submitted to the main conversation. Agent may use connected read-only web tools when the user asks.', fg: PALETTE.good },
410
+ { text: 'URL references and web research do not ingest into Agent Knowledge. Ingestion is a separate confirmed Agent Knowledge action.', fg: PALETTE.warn },
411
+ { text: 'Use Agent Knowledge ask/search first for known source-backed context; use web research for current or external sources.', fg: PALETTE.info },
412
+ { text: 'External sends, browser-side effects, package installs, and connected-host mutations are outside this read-only research lane.', fg: PALETTE.warn },
413
+ );
287
414
  } else if (category.id === 'tools') {
288
415
  base.push(
289
416
  { text: `MCP servers: ${snapshot.mcpConnectedServerCount}/${snapshot.mcpServerCount} connected; quarantined ${snapshot.mcpQuarantinedServerCount}; allow-all ${snapshot.mcpAllowAllServerCount}.`, fg: snapshot.mcpQuarantinedServerCount > 0 || snapshot.mcpAllowAllServerCount > 0 ? PALETTE.warn : PALETTE.info },
@@ -357,18 +484,28 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
357
484
  base.push(
358
485
  { text: `Session memories: ${snapshot.sessionMemoryCount}`, fg: PALETTE.info },
359
486
  { text: `Agent memory: ${snapshot.localMemoryCount}; prompt-active: ${snapshot.localMemoryPromptActiveCount}; review queue: ${snapshot.localMemoryReviewQueueCount}`, fg: PALETTE.info },
487
+ { text: `Scratchpad notes: ${snapshot.localNoteCount}; review queue: ${snapshot.localNoteReviewQueueCount}`, fg: PALETTE.info },
360
488
  { text: `Local routines: ${snapshot.localRoutineCount}; enabled: ${snapshot.enabledRoutineCount}`, fg: PALETTE.info },
361
489
  { text: `Local skills: ${snapshot.localSkillCount}; enabled: ${snapshot.enabledSkillCount}; bundles: ${snapshot.localSkillBundleCount}; active skills: ${snapshot.activeSkillCount}`, fg: PALETTE.info },
362
490
  { text: `Local personas: ${snapshot.localPersonaCount}; active: ${snapshot.activePersonaName}`, fg: PALETTE.info },
363
- { text: 'Durable memory, routines, skills, and personas remain Agent-local until shared registry contracts exist.', fg: PALETTE.good },
491
+ { text: 'Durable memory, scratchpad notes, routines, skills, and personas remain Agent-local until shared registry contracts exist.', fg: PALETTE.good },
364
492
  { text: 'Secrets are rejected/redacted; store secret references instead of secret values.', fg: PALETTE.warn },
365
493
  { text: '' },
366
494
  ...localLibraryLines('Agent Memory', snapshot.localMemories, 'No Agent memory yet. Create one here with Create memory.', workspace.selectedLocalLibraryItem('memory')?.id ?? null),
367
495
  );
496
+ } else if (category.id === 'notes') {
497
+ base.push(
498
+ { text: `Scratchpad notes: ${snapshot.localNoteCount}; review queue: ${snapshot.localNoteReviewQueueCount}`, fg: PALETTE.info },
499
+ { text: 'Notes are Agent-local working context for source triage, temporary decisions, and operator handoff.', fg: PALETTE.good },
500
+ { text: 'Notes do not become memory and are not ingested into Agent Knowledge unless the user takes a separate explicit action.', fg: PALETTE.warn },
501
+ { text: 'Use reviewed notes to prefill durable memory, skills, routines, or personas, or to decide that a reviewed source deserves Agent Knowledge ingest.', fg: PALETTE.muted },
502
+ { text: '' },
503
+ ...localLibraryLines('Scratchpad Notes', snapshot.localNotes, 'No local notes yet. Create one here with Create note.', workspace.selectedLocalLibraryItem('note')?.id ?? null),
504
+ );
368
505
  } else if (category.id === 'personas') {
369
506
  base.push(
370
507
  { text: `Personas: ${snapshot.localPersonaCount}; active: ${snapshot.activePersonaName}`, fg: PALETTE.info },
371
- { text: 'Personas are local behavior profiles for the serial main-conversation assistant, not spawned agents.', fg: PALETTE.good },
508
+ { text: 'Personas are local behavior profiles for the serial main-conversation assistant, not separate Agent jobs.', fg: PALETTE.good },
372
509
  { text: 'Use them for tone, role, domain constraints, tool posture, and repeatable operating preferences.', fg: PALETTE.muted },
373
510
  { text: '' },
374
511
  ...localLibraryLines('Persona Library', snapshot.localPersonas, 'No local personas yet. Create one here with Create persona.', workspace.selectedLocalLibraryItem('persona')?.id ?? null),
@@ -384,10 +521,16 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
384
521
  ...localLibraryLines('Skill Bundles', snapshot.localSkillBundles, 'No local skill bundles yet. Use Skill bundles and Create bundle after creating skills.', null),
385
522
  );
386
523
  } else if (category.id === 'routines') {
524
+ const ready = readyRoutineItems(snapshot);
525
+ const needsSetup = routinesNeedingSetup(snapshot);
526
+ const needsReview = routinesNeedingReview(snapshot);
387
527
  base.push(
388
528
  { text: `Routines: ${snapshot.localRoutineCount}; enabled: ${snapshot.enabledRoutineCount}`, fg: PALETTE.info },
529
+ { text: `Schedule-ready routines: ${ready.length}; setup gaps: ${needsSetup.length}; review needed: ${needsReview.length}`, fg: needsSetup.length > 0 || needsReview.length > 0 ? PALETTE.warn : PALETTE.good },
389
530
  { text: 'Routines are repeatable main-conversation workflows. Starting one does not create hidden jobs.', fg: PALETTE.good },
390
531
  { text: 'Scheduling a reviewed routine is explicit and requires a confirmed schedule command.', fg: PALETTE.warn },
532
+ routineNextActionLine(snapshot),
533
+ ...routineScheduleReceiptLines(snapshot),
391
534
  { text: '' },
392
535
  ...localLibraryLines('Routine Library', snapshot.localRoutines, 'No local routines yet. Create one here with Create routine.', workspace.selectedLocalLibraryItem('routine')?.id ?? null),
393
536
  );
@@ -395,18 +538,26 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
395
538
  base.push(
396
539
  { text: 'Work plan and approvals are read or explicitly confirmed through public operator routes.', fg: PALETTE.info },
397
540
  { text: 'This workspace does not approve, deny, cancel, or mutate requests by selection alone.', fg: PALETTE.good },
541
+ { text: 'Approve, deny, and cancel forms require an approval id and typed confirmation.', fg: PALETTE.warn },
398
542
  );
399
543
  } else if (category.id === 'automation') {
544
+ const ready = readyRoutineItems(snapshot);
400
545
  base.push(
401
- { text: 'Automation and schedules default to read-only observability.', fg: PALETTE.info },
546
+ { text: 'Automation and schedules default to read-only observability; side effects require confirmed forms.', fg: PALETTE.info },
402
547
  { text: 'Confirmed reminders and routine promotion use connected schedules only.', fg: PALETTE.info },
403
- { text: 'Local scheduler mutation/run controls remain blocked.', fg: PALETTE.warn },
548
+ { text: 'Local scheduler mutation controls remain blocked; job/run/schedule controls go through the connected host.', fg: PALETTE.warn },
549
+ { text: 'Reminder path: Create reminder -> choose at/every/cron -> optional delivery target -> confirm yes.', fg: PALETTE.good },
550
+ { text: 'Operator path: choose job/run/schedule action -> enter id -> type yes -> dispatch once.', fg: PALETTE.good },
551
+ { text: 'Routine path: Routines -> resolve setup -> review selected -> Promote routine -> Reconcile schedules.', fg: PALETTE.good },
552
+ { text: `Schedule-ready routines: ${ready.length}; local promotion receipts: ${snapshot.routineScheduleReceiptCount}`, fg: ready.length > 0 ? PALETTE.good : PALETTE.warn },
553
+ automationNextActionLine(snapshot),
554
+ ...routineScheduleReceiptLines(snapshot),
404
555
  );
405
556
  } else if (category.id === 'delegate') {
406
557
  base.push(
407
558
  { text: 'Build/fix/review work is handed to GoodVibes TUI/shared-session contracts.', fg: PALETTE.info },
408
- { text: `WRFC policy: ${snapshot.wrfcPolicy}`, fg: PALETTE.warn },
409
- { text: 'Agent does not spawn local Engineer/Reviewer/Tester roots.', fg: PALETTE.good },
559
+ { text: `Delegated review policy: ${snapshot.delegatedReviewPolicy}`, fg: PALETTE.warn },
560
+ { text: 'Agent does not create coding-role Agent jobs.', fg: PALETTE.good },
410
561
  );
411
562
  }
412
563
  if (snapshot.warnings.length > 0) {
@@ -440,6 +591,17 @@ function buildContextRows(workspace: AgentWorkspace, category: AgentWorkspaceCat
440
591
  { text: '' },
441
592
  { text: category.detail, fg: PALETTE.text },
442
593
  { text: '' },
594
+ ...(workspace.actionSearchActive ? [
595
+ { text: 'Action Search', fg: PALETTE.title, bold: true },
596
+ {
597
+ text: workspace.actionSearchQuery.length > 0
598
+ ? `Query: ${workspace.actionSearchQuery} (${workspace.actionSearchResults.length} result${workspace.actionSearchResults.length === 1 ? '' : 's'})`
599
+ : 'Type to search every Agent workspace action.',
600
+ fg: workspace.actionSearchQuery.length > 0 && workspace.actionSearchResults.length === 0 ? PALETTE.warn : PALETTE.info,
601
+ },
602
+ { text: 'Enter opens the selected result. Esc clears search and returns to normal workspace navigation.', fg: PALETTE.muted },
603
+ { text: '' },
604
+ ] satisfies ContextLine[] : []),
443
605
  ...(workspace.localEditor ? editorContextLines(workspace.localEditor) : []),
444
606
  ...(workspace.localEditor ? [{ text: '' }] : []),
445
607
  ...snapshotLines(workspace, category, workspace.runtimeSnapshot),
@@ -532,23 +694,32 @@ function buildActionRows(workspace: AgentWorkspace, width: number, height: numbe
532
694
  const labelWidth = Math.min(28, Math.max(16, Math.floor(width * 0.30)));
533
695
  const safetyWidth = 10;
534
696
  const commandWidth = Math.max(10, width - labelWidth - safetyWidth - 9);
697
+ if (workspace.actionSearchActive) {
698
+ rows.push({
699
+ text: ` Search: ${workspace.actionSearchQuery || '(type to filter actions)'}`,
700
+ fg: workspace.actionSearchQuery.length > 0 && workspace.actions.length === 0 ? PALETTE.warn : PALETTE.info,
701
+ bold: true,
702
+ });
703
+ }
535
704
  rows.push({
536
- text: ` ${padDisplay('Action', labelWidth)} ${padDisplay('Safety', safetyWidth)} ${padDisplay('Command', commandWidth)}`,
705
+ text: ` ${padDisplay(workspace.actionSearchActive ? 'Result' : 'Action', labelWidth)} ${padDisplay('Safety', safetyWidth)} ${padDisplay('Command', commandWidth)}`,
537
706
  fg: PALETTE.muted,
538
707
  bold: true,
539
708
  });
540
709
 
541
710
  const actions = workspace.actions;
542
- const visible = Math.max(1, height - 2);
711
+ const visible = Math.max(1, height - (workspace.actionSearchActive ? 3 : 2));
543
712
  const window = stableWindow(actions.length, workspace.selectedActionIndex, visible);
544
713
  if (window.start > 0) rows.push({ text: `${GLYPHS.navigation.moreAbove} ${window.start} more action(s) above`, kind: 'more', fg: PALETTE.dim, dim: true });
545
714
 
546
715
  for (let index = window.start; index < window.end; index += 1) {
547
716
  const action = actions[index]!;
548
717
  const selected = index === workspace.selectedActionIndex;
718
+ const searchResult = workspace.actionSearchActive ? workspace.actionSearchResults[index] : null;
719
+ const label = searchResult ? `${searchResult.category.label} / ${action.label}` : action.label;
549
720
  const marker = selected ? GLYPHS.navigation.selected : ' ';
550
721
  rows.push({
551
- text: `${marker} ${padDisplay(action.label, labelWidth)} ${padDisplay(action.safety, safetyWidth)} ${padDisplay(actionCommand(action), commandWidth)}`,
722
+ text: `${marker} ${padDisplay(label, labelWidth)} ${padDisplay(action.safety, safetyWidth)} ${padDisplay(actionCommand(action), commandWidth)}`,
552
723
  selected: selected && workspace.focusPane === 'actions',
553
724
  fg: safetyColor(action),
554
725
  bold: selected,
@@ -577,12 +748,15 @@ function footerText(workspace: AgentWorkspace): string {
577
748
  if (workspace.localEditor) {
578
749
  return `Agent workspace · editing ${workspace.localEditor.kind} · Enter next/save · Ctrl-J newline · Esc cancel`;
579
750
  }
751
+ if (workspace.actionSearchActive) {
752
+ return 'Agent workspace · action search · type filter · Up/Down results · Enter open · Esc clear';
753
+ }
580
754
  const focus = workspace.focusPane === 'categories' ? 'categories' : 'actions';
581
- return `Agent workspace · focus ${focus} · Up/Down navigate · Left/Right pane · Enter open/action · R refresh · Esc close`;
755
+ return `Agent workspace · ${focus} · / search · Up/Down · Left/Right · Ctrl+[/] area · Enter open/action · R refresh · Esc close`;
582
756
  }
583
757
 
584
758
  export function renderAgentWorkspace(workspace: AgentWorkspace, width: number, height: number): Line[] {
585
- const category = workspace.selectedCategory;
759
+ const category = workspace.selectedActionCategory;
586
760
  const action = workspace.selectedAction;
587
761
  const setupCategory = category.id === 'setup';
588
762
  const layoutOptions = {
@@ -598,9 +772,11 @@ export function renderAgentWorkspace(workspace: AgentWorkspace, width: number, h
598
772
  width,
599
773
  height,
600
774
  title: 'GoodVibes Agent / Operator Workspace',
601
- stateLabel: workspace.localEditor ? 'Editor' : workspace.focusPane === 'categories' ? 'Categories' : 'Actions',
775
+ stateLabel: workspace.localEditor ? 'Editor' : workspace.actionSearchActive ? 'Search' : workspace.focusPane === 'categories' ? 'Categories' : 'Actions',
602
776
  leftHeader: 'Operator Areas',
603
- mainHeader: `${category.label} · ${category.actions.length} action(s)`,
777
+ mainHeader: workspace.actionSearchActive
778
+ ? `Search actions · ${workspace.actions.length} result(s)`
779
+ : `${category.label} · ${category.actions.length} action(s)`,
604
780
  leftRows: buildLeftRows(workspace, metrics.bodyRows),
605
781
  contextRows: buildContextRows(workspace, category, action, metrics.contextWidth),
606
782
  controlRows: buildActionRows(workspace, metrics.contextWidth, metrics.controlRows),
@@ -15,6 +15,9 @@ const TITLE_FG = DEFAULT_OVERLAY_PALETTE.titleFg;
15
15
  const BODY_FG = DEFAULT_OVERLAY_PALETTE.bodyFg;
16
16
  const MUTED_FG = DEFAULT_OVERLAY_PALETTE.mutedFg;
17
17
  const SELECTED_BG = DEFAULT_OVERLAY_PALETTE.selectedBg;
18
+ const AUTOCOMPLETE_TITLE = ' Commands';
19
+ const AUTOCOMPLETE_EMPTY_QUERY = '/';
20
+ const AUTOCOMPLETE_HINTS = '[Tab] Complete [Up/Down] Navigate [Enter] Execute [Esc] Cancel';
18
21
 
19
22
  interface CellStyle {
20
23
  fg: string;
@@ -27,6 +30,23 @@ function putText(line: Line, startX: number, maxWidth: number, text: string, sty
27
30
  putOverlayText(line, startX, maxWidth, text, style);
28
31
  }
29
32
 
33
+ function autocompleteQueryText(query: string): string {
34
+ return query ? `/${query}` : AUTOCOMPLETE_EMPTY_QUERY;
35
+ }
36
+
37
+ function autocompleteScrollText(selected: string | number, total: string | number): string {
38
+ return `${selected}/${total}`;
39
+ }
40
+
41
+ export function renderAutocompletePackageText(): string {
42
+ return [
43
+ AUTOCOMPLETE_TITLE.trim(),
44
+ autocompleteQueryText('<query>'),
45
+ autocompleteScrollText('<selected>', '<total>'),
46
+ AUTOCOMPLETE_HINTS,
47
+ ].join('\n');
48
+ }
49
+
30
50
  /**
31
51
  * Render the slash command autocomplete dropdown as Line[] for overlay in the viewport.
32
52
  */
@@ -51,8 +71,8 @@ export function renderAutocompleteOverlay(
51
71
  lines.push(createOverlayBorderLine(width, layout, '┌', '─', '┐', BORDER_FG));
52
72
 
53
73
  const titleLine = createOverlayContentLine(width, layout);
54
- const titleText = ' Commands';
55
- const queryText = state.query ? `/${state.query}` : '/';
74
+ const titleText = AUTOCOMPLETE_TITLE;
75
+ const queryText = autocompleteQueryText(state.query);
56
76
  const queryWidth = Math.min(Math.floor(layout.innerWidth / 2), Math.max(8, layout.innerWidth - getDisplayWidth(titleText) - 2));
57
77
  const leftText = fitDisplay(titleText, Math.max(0, layout.innerWidth - queryWidth));
58
78
  const rightText = truncateDisplay(queryText, queryWidth);
@@ -126,8 +146,8 @@ export function renderAutocompleteOverlay(
126
146
  }
127
147
 
128
148
  if (total > maxVisible) {
129
- const scrollLine = createOverlayContentLine(width, layout);
130
- const scrollText = `${state.selectedIndex + 1}/${total}`;
149
+ const scrollLine = createOverlayContentLine(width, layout);
150
+ const scrollText = autocompleteScrollText(state.selectedIndex + 1, total);
131
151
  putText(
132
152
  scrollLine,
133
153
  layout.margin + 2 + Math.max(0, layout.innerWidth - getDisplayWidth(scrollText)),
@@ -139,12 +159,11 @@ export function renderAutocompleteOverlay(
139
159
  }
140
160
 
141
161
  const footerLine = createOverlayContentLine(width, layout);
142
- const hints = '[Tab] Complete [Up/Down] Navigate [Enter] Execute [Esc] Cancel';
143
162
  putText(
144
163
  footerLine,
145
164
  layout.margin + 2,
146
165
  layout.innerWidth,
147
- fitDisplay(truncateDisplay(hints, layout.innerWidth), layout.innerWidth),
166
+ fitDisplay(truncateDisplay(AUTOCOMPLETE_HINTS, layout.innerWidth), layout.innerWidth),
148
167
  { fg: MUTED_FG, dim: true },
149
168
  );
150
169
  lines.push(footerLine);
@@ -13,6 +13,21 @@ import { BookmarkModal } from '../input/bookmark-modal.ts';
13
13
  import type { BookmarkEntry } from '@pellux/goodvibes-sdk/platform/bookmarks';
14
14
  import { getOverlayContentBudget, getStableOverlayContentRows } from './overlay-viewport.ts';
15
15
 
16
+ const BOOKMARK_MODAL_TITLE = 'Bookmarks';
17
+ const BOOKMARK_MODAL_EMPTY_MESSAGE = 'No bookmarks - use Ctrl+B to bookmark a block';
18
+ const BOOKMARK_MODAL_COLUMNS = ' Key Label Time';
19
+ const BOOKMARK_MODAL_HINTS = ['Up/Down Navigate', 'Enter Jump', 'o Open file', 'd Remove', 'Esc Close'];
20
+
21
+ export function renderBookmarkModalPackageText(): string {
22
+ return [
23
+ BOOKMARK_MODAL_TITLE,
24
+ BOOKMARK_MODAL_EMPTY_MESSAGE,
25
+ BOOKMARK_MODAL_COLUMNS.trim(),
26
+ '[<start>-<end> of <count>]',
27
+ ...BOOKMARK_MODAL_HINTS,
28
+ ].join('\n');
29
+ }
30
+
16
31
  // ---------------------------------------------------------------------------
17
32
  // Helpers
18
33
  // ---------------------------------------------------------------------------
@@ -60,7 +75,7 @@ export function renderBookmarkModal(
60
75
  const relSelected = Math.max(0, modal.selectedIndex - modal.scrollOffset);
61
76
 
62
77
  const items = visible.length === 0
63
- ? [{ label: 'No bookmarks - use Ctrl+B to bookmark a block', selected: false }]
78
+ ? [{ label: BOOKMARK_MODAL_EMPTY_MESSAGE, selected: false }]
64
79
  : visible.map((entry, i) => ({
65
80
  label: entryLabel(entry),
66
81
  selected: i === relSelected,
@@ -68,7 +83,7 @@ export function renderBookmarkModal(
68
83
  const sections: import('./modal-factory.ts').ModalSection[] = [
69
84
  {
70
85
  type: 'text',
71
- content: ' Key Label Time',
86
+ content: BOOKMARK_MODAL_COLUMNS,
72
87
  style: { dim: true },
73
88
  },
74
89
  { type: 'separator' },
@@ -90,11 +105,11 @@ export function renderBookmarkModal(
90
105
 
91
106
  return ModalFactory.createModal(
92
107
  {
93
- title: `Bookmarks ${totalStr}`,
108
+ title: `${BOOKMARK_MODAL_TITLE} ${totalStr}`,
94
109
  width: 80,
95
110
  targetContentRows,
96
111
  sections,
97
- hints: ['Up/Down Navigate', 'Enter Jump', 'o Open file', 'd Remove', 'Esc Close'],
112
+ hints: BOOKMARK_MODAL_HINTS,
98
113
  },
99
114
  width,
100
115
  );
@@ -1,5 +1,7 @@
1
1
  import { type Line, type Cell, createEmptyLine, createEmptyCell } from '../types/grid.ts';
2
2
 
3
+ const CELL_KEYS: readonly (keyof Cell)[] = ['char', 'fg', 'bg', 'bold', 'dim', 'underline', 'italic', 'strikethrough', 'link'];
4
+
3
5
  /**
4
6
  * TerminalBuffer - Represents a 2D grid of styled cells.
5
7
  * Tracks a per-row dirty bitmap so the diff engine can skip rows that were
@@ -21,8 +23,8 @@ export class TerminalBuffer {
21
23
  // already matches the current cell value (idempotent write).
22
24
  const current = this.cells[y][x]!;
23
25
  let changed = false;
24
- for (const k in cell) {
25
- if ((cell as unknown as Record<string, unknown>)[k] !== (current as unknown as Record<string, unknown>)[k]) {
26
+ for (const key of CELL_KEYS) {
27
+ if (Object.prototype.hasOwnProperty.call(cell, key) && cell[key] !== current[key]) {
26
28
  changed = true;
27
29
  break;
28
30
  }