@pellux/goodvibes-agent 1.3.0 → 1.4.1

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.
@@ -5,6 +5,8 @@ import type {
5
5
  AgentWorkspaceLocalEditor,
6
6
  AgentWorkspaceRuntimeSnapshot,
7
7
  } from '../input/agent-workspace.ts';
8
+ import type { AgentWorkspaceEditorKind } from '../input/agent-workspace-types.ts';
9
+ import type { AgentWorkspaceSetupChecklistItem } from '../input/agent-workspace-setup.ts';
8
10
  import type { Line } from '../types/grid.ts';
9
11
  import { wrapText } from '../utils/terminal-width.ts';
10
12
  import { GLYPHS } from './ui-primitives.ts';
@@ -18,11 +20,36 @@ import {
18
20
  } from './fullscreen-workspace.ts';
19
21
  import { actionResultColor, type AgentWorkspaceContextLine as ContextLine } from './agent-workspace-style.ts';
20
22
  import { compactText, reviewerReadinessContextLines, snapshotLines } from './agent-workspace-context-lines.ts';
23
+ import { ONBOARDING_COMPLETE_SYNTHETIC_ACTION, ONBOARDING_CRITICAL_STEP_IDS } from '../input/agent-workspace-onboarding-finish.ts';
24
+
25
+ function onboardingCategoryReadiness(
26
+ category: AgentWorkspaceCategory,
27
+ checklist: readonly AgentWorkspaceSetupChecklistItem[],
28
+ ): 'ready' | 'attention' | 'optional' {
29
+ const CATEGORY_CHECKLIST_MAP: Record<string, readonly string[]> = {
30
+ 'setup': ['provider-model', 'connected-host-auth', 'runtime'],
31
+ 'account-model': ['provider-model', 'subscriptions'],
32
+ 'assistant-behavior': [],
33
+ 'tools-permissions': [],
34
+ 'onboarding-display': [],
35
+ 'onboarding-channels': ['channels'],
36
+ 'onboarding-voice-media': ['voice-media'],
37
+ 'onboarding-context': ['agent-knowledge', 'persona', 'skills', 'routines', 'memory', 'notes', 'profile'],
38
+ 'onboarding-automation': [],
39
+ };
40
+ const itemIds = CATEGORY_CHECKLIST_MAP[category.id];
41
+ if (!itemIds || itemIds.length === 0) return 'optional';
42
+ const mapped = itemIds.map((id) => checklist.find((item) => item.id === id));
43
+ if (mapped.every((item) => item?.status === 'ready')) return 'ready';
44
+ if (mapped.some((item) => item?.status === 'blocked' || item?.status === 'recommended')) return 'attention';
45
+ return 'optional';
46
+ }
21
47
 
22
48
  function buildLeftRows(workspace: AgentWorkspace, height: number): WorkspaceRow[] {
23
49
  const rows: WorkspaceRow[] = [];
24
50
  let selectedRenderedIndex = 0;
25
51
  let lastGroup = '';
52
+ const checklist = workspace.runtimeSnapshot?.setupChecklist ?? [];
26
53
 
27
54
  workspace.categories.forEach((category, index) => {
28
55
  if (category.group !== lastGroup) {
@@ -32,11 +59,24 @@ function buildLeftRows(workspace: AgentWorkspace, height: number): WorkspaceRow[
32
59
  const selected = index === workspace.selectedCategoryIndex;
33
60
  if (selected) selectedRenderedIndex = rows.length;
34
61
  const marker = selected ? GLYPHS.navigation.selected : ' ';
62
+ let readinessGlyph = '';
63
+ let readinessFg: string | undefined;
64
+ if (isOnboardingCategory(category)) {
65
+ const readiness = onboardingCategoryReadiness(category, checklist);
66
+ if (readiness === 'ready') {
67
+ readinessGlyph = GLYPHS.status.success;
68
+ readinessFg = PALETTE.good;
69
+ } else if (readiness === 'attention') {
70
+ readinessGlyph = '!';
71
+ readinessFg = PALETTE.warn;
72
+ }
73
+ }
74
+ const readinessMark = readinessGlyph ? `${readinessGlyph} ` : ' ';
35
75
  rows.push({
36
- text: ` ${marker} ${category.label}`,
76
+ text: ` ${marker} ${readinessMark}${category.label}`,
37
77
  selected: selected && workspace.focusPane === 'categories',
38
78
  kind: 'item',
39
- fg: selected ? PALETTE.text : PALETTE.muted,
79
+ fg: selected ? PALETTE.text : readinessFg ?? PALETTE.muted,
40
80
  bold: selected,
41
81
  });
42
82
  });
@@ -72,41 +112,317 @@ function isOnboardingCategory(category: AgentWorkspaceCategory): boolean {
72
112
  return category.group === 'ONBOARDING';
73
113
  }
74
114
 
75
- function onboardingActionChange(workspace: AgentWorkspace, action: AgentWorkspaceAction): string {
76
- if (action.kind === 'setting') {
77
- return workspace.settingActionPreview(action) ?? 'Update setting';
115
+ interface OnboardingActionColumns {
116
+ readonly setting: string;
117
+ readonly defaultValue: string;
118
+ readonly currentValue: string;
119
+ }
120
+
121
+ function editorPurposeLabel(editorKind: AgentWorkspaceEditorKind): string {
122
+ switch (editorKind) {
123
+ case 'mcp-server': return 'Edit MCP server';
124
+ case 'mcp-tools-server': return 'Edit MCP tools server';
125
+ case 'mcp-repair': return 'Repair MCP connection';
126
+ case 'secret-link': return 'Link secret reference';
127
+ case 'secret-set': return 'Store secret';
128
+ case 'secret-test': return 'Test secret reference';
129
+ case 'secret-delete': return 'Delete secret';
130
+ case 'subscription-login-start': return 'Sign in to provider';
131
+ case 'subscription-login-finish': return 'Finish provider sign-in';
132
+ case 'subscription-logout': return 'Sign out of provider';
133
+ case 'subscription-inspect': return 'Inspect subscription';
134
+ case 'provider-add': return 'Add custom provider';
135
+ case 'provider-remove': return 'Remove custom provider';
136
+ case 'provider-use': return 'Choose provider';
137
+ case 'provider-inspect': return 'Inspect provider';
138
+ case 'provider-routes': return 'Edit provider routes';
139
+ case 'provider-account-repair': return 'Repair provider account';
140
+ case 'memory': return 'Edit memory';
141
+ case 'memory-search': return 'Search memory';
142
+ case 'memory-get': return 'Get memory record';
143
+ case 'memory-explain': return 'Explain memory';
144
+ case 'memory-promote': return 'Promote memory';
145
+ case 'memory-link': return 'Link memory';
146
+ case 'memory-export': return 'Export memory';
147
+ case 'memory-import': return 'Import memory';
148
+ case 'memory-handoff-export': return 'Export memory handoff';
149
+ case 'memory-handoff-inspect': return 'Inspect memory handoff';
150
+ case 'memory-handoff-import': return 'Import memory handoff';
151
+ case 'memory-vector-rebuild': return 'Rebuild memory vectors';
152
+ case 'note': return 'Edit note';
153
+ case 'persona': return 'Edit persona';
154
+ case 'persona-search': return 'Search personas';
155
+ case 'persona-show': return 'Show persona';
156
+ case 'persona-discovery-import': return 'Import persona files';
157
+ case 'skill': return 'Edit skill';
158
+ case 'skill-search': return 'Search skills';
159
+ case 'skill-show': return 'Show skill';
160
+ case 'skill-discovery-import': return 'Import skill files';
161
+ case 'routine': return 'Edit routine';
162
+ case 'routine-search': return 'Search routines';
163
+ case 'routine-show': return 'Show routine';
164
+ case 'routine-discovery-import': return 'Import routine files';
165
+ case 'profile': return 'Edit profile';
166
+ case 'profile-from-discovered': return 'Create profile from files';
167
+ case 'profile-show': return 'Show profile';
168
+ case 'profile-default': return 'Set default profile';
169
+ case 'profile-default-clear': return 'Clear default profile';
170
+ case 'profile-delete': return 'Delete profile';
171
+ case 'profile-template-export': return 'Export profile template';
172
+ case 'profile-template-import': return 'Import profile template';
173
+ case 'profile-template-show': return 'Show profile template';
174
+ case 'profile-template-from-discovered': return 'Create template from files';
175
+ case 'knowledge-url': return 'Ingest URL';
176
+ case 'knowledge-urls': return 'Ingest URLs';
177
+ case 'knowledge-file': return 'Ingest file';
178
+ case 'knowledge-bookmarks': return 'Import bookmarks';
179
+ case 'knowledge-browser-history': return 'Import browser history';
180
+ case 'knowledge-connector-ingest': return 'Run connector ingest';
181
+ case 'knowledge-connector-show': return 'Show connector';
182
+ case 'knowledge-connector-doctor': return 'Check connector health';
183
+ case 'knowledge-reindex': return 'Reindex knowledge';
184
+ case 'knowledge-search': return 'Search knowledge';
185
+ case 'knowledge-ask': return 'Ask knowledge';
186
+ case 'knowledge-get': return 'Get knowledge record';
187
+ case 'knowledge-map': return 'Map knowledge';
188
+ case 'knowledge-review-issue': return 'Review knowledge issue';
189
+ case 'knowledge-consolidate': return 'Consolidate knowledge';
190
+ case 'knowledge-packet': return 'Build knowledge packet';
191
+ case 'knowledge-explain': return 'Explain knowledge';
192
+ case 'delegate-task': return 'Delegate build task';
193
+ case 'document-browse': return 'Browse documents';
194
+ case 'document-show': return 'Show document';
195
+ case 'document-create': return 'Create document';
196
+ case 'document-update': return 'Revise document';
197
+ case 'document-review': return 'Review document';
198
+ case 'document-comment': return 'Add comment';
199
+ case 'document-resolve-comment': return 'Resolve comment';
200
+ case 'document-suggest': return 'Propose suggestion';
201
+ case 'document-accept-suggestion': return 'Accept suggestion';
202
+ case 'document-reject-suggestion': return 'Reject suggestion';
203
+ case 'document-insert-artifact': return 'Insert artifact';
204
+ case 'document-attach-artifact': return 'Attach artifact';
205
+ case 'document-export': return 'Export document';
206
+ case 'document-reviewer-readiness': return 'Review readiness preflight';
207
+ case 'document-review-packet-wizard': return 'Run packet wizard';
208
+ case 'document-review-packet-preset': return 'Save packet preset';
209
+ case 'document-review-packet-preset-refresh': return 'Refresh packet preset';
210
+ case 'document-review-packet-share': return 'Share review packet';
211
+ case 'model-compare': return 'Compare models';
212
+ case 'model-compare-review': return 'Review comparison';
213
+ case 'model-compare-handoff-diff': return 'Diff reviewer handoffs';
214
+ case 'model-compare-judge': return 'Judge comparison';
215
+ case 'model-compare-apply': return 'Apply comparison winner';
216
+ case 'model-compare-route-decision': return 'Record route decision';
217
+ case 'model-compare-export': return 'Export comparison';
218
+ case 'model-compare-analytics': return 'View comparison analytics';
219
+ case 'local-model-benchmark': return 'Run local benchmark';
220
+ case 'auth-show': return 'Show auth status';
221
+ case 'auth-repair': return 'Repair auth';
222
+ case 'auth-bundle-export': return 'Export auth bundle';
223
+ case 'auth-bundle-inspect': return 'Inspect auth bundle';
224
+ case 'trust-bundle-export': return 'Export trust bundle';
225
+ case 'trust-bundle-inspect': return 'Inspect trust bundle';
226
+ case 'support-bundle-export': return 'Export support bundle';
227
+ case 'support-bundle-inspect': return 'Inspect support bundle';
228
+ case 'support-bundle-import': return 'Import support bundle';
229
+ case 'subscription-bundle-export': return 'Export subscription bundle';
230
+ case 'subscription-bundle-inspect': return 'Inspect subscription bundle';
231
+ case 'voice-enable': return 'Enable voice';
232
+ case 'voice-disable': return 'Disable voice';
233
+ case 'voice-bundle-export': return 'Export voice bundle';
234
+ case 'voice-bundle-inspect': return 'Inspect voice bundle';
235
+ case 'tts-prompt': return 'Test TTS prompt';
236
+ case 'image-input': return 'Attach image';
237
+ case 'artifact-browser': return 'Browse artifacts';
238
+ case 'artifact-show': return 'Show artifact';
239
+ case 'artifact-export-file': return 'Export artifact to file';
240
+ case 'artifact-export-package': return 'Export artifact package';
241
+ case 'artifact-promote-knowledge': return 'Promote artifact to knowledge';
242
+ case 'media-generate': return 'Generate media';
243
+ case 'conversation-export': return 'Export conversation';
244
+ case 'conversation-events': return 'Show conversation events';
245
+ case 'conversation-groups': return 'Show conversation groups';
246
+ case 'conversation-find': return 'Find conversation';
247
+ case 'effort-level': return 'Set effort level';
248
+ case 'channel-show': return 'Show channel';
249
+ case 'channel-doctor': return 'Check channel health';
250
+ case 'channel-setup': return 'Set up channel';
251
+ case 'channel-send': return 'Send to channel';
252
+ case 'session-save': return 'Save session';
253
+ case 'session-load': return 'Load session';
254
+ case 'session-rename': return 'Rename session';
255
+ case 'session-resume': return 'Resume session';
256
+ case 'session-info': return 'Inspect session';
257
+ case 'session-export-saved': return 'Export saved session';
258
+ case 'session-search': return 'Search sessions';
259
+ case 'session-delete': return 'Delete session';
260
+ case 'session-fork': return 'Fork session';
261
+ case 'session-graph': return 'Inspect session graph';
262
+ case 'task-list-filter': return 'Filter task list';
263
+ case 'task-show': return 'Show task';
264
+ case 'task-output': return 'View task output';
265
+ case 'plan-seed': return 'Seed plan';
266
+ case 'plan-show': return 'Show plan';
267
+ case 'plan-approve': return 'Approve plan';
268
+ case 'plan-override': return 'Override plan';
269
+ case 'plan-clear': return 'Clear plan';
270
+ case 'health-repair': return 'Repair health issue';
271
+ case 'approval-review': return 'Review approval';
272
+ case 'approval-approve': return 'Approve request';
273
+ case 'approval-deny': return 'Deny request';
274
+ case 'approval-cancel': return 'Cancel approval';
275
+ case 'automation-job-run': return 'Run automation job';
276
+ case 'automation-job-pause': return 'Pause automation job';
277
+ case 'automation-job-resume': return 'Resume automation job';
278
+ case 'automation-run-cancel': return 'Cancel automation run';
279
+ case 'automation-run-retry': return 'Retry automation run';
280
+ case 'schedule-run': return 'Run schedule';
281
+ case 'schedule-edit': return 'Edit schedule';
282
+ case 'routine-receipt': return 'View routine receipt';
283
+ case 'schedule-receipt': return 'View schedule receipt';
284
+ case 'mode-preset': return 'Set mode preset';
285
+ case 'mode-domain': return 'Set mode domain';
286
+ case 'setting-set': return 'Set setting';
287
+ case 'model-pin': return 'Pin model';
288
+ case 'model-unpin': return 'Unpin model';
289
+ case 'workplan-add': return 'Add work item';
290
+ case 'workplan-show': return 'Show work plan';
291
+ case 'workplan-status': return 'Check work plan status';
292
+ case 'workplan-delete': return 'Delete work item';
293
+ case 'workplan-clear-completed': return 'Clear completed items';
294
+ case 'routine-schedule': return 'Schedule routine';
295
+ case 'reminder-schedule': return 'Schedule reminder';
296
+ case 'skill-bundle': return 'Edit skill bundle';
297
+ case 'skill-bundle-search': return 'Search skill bundles';
298
+ case 'skill-bundle-show': return 'Show skill bundle';
299
+ case 'skill-bundle-update': return 'Update skill bundle';
300
+ case 'skill-bundle-enable': return 'Enable skill bundle';
301
+ case 'skill-bundle-disable': return 'Disable skill bundle';
302
+ case 'skill-bundle-review': return 'Review skill bundle';
303
+ case 'skill-bundle-stale': return 'Mark bundle stale';
304
+ case 'skill-bundle-delete': return 'Delete skill bundle';
305
+ case 'learned-behavior': return 'Review learned behavior';
306
+ case 'web-research': return 'Run web research';
307
+ case 'web-fetch': return 'Fetch from web';
308
+ case 'research-run': return 'Run research';
309
+ case 'research-source': return 'Manage research source';
310
+ case 'research-report': return 'Show research report';
311
+ case 'notify-webhook': return 'Add notification webhook';
312
+ case 'notify-webhook-remove': return 'Remove notification webhook';
313
+ case 'notify-webhook-clear': return 'Clear notification webhooks';
314
+ case 'notify-webhook-test': return 'Test notification webhook';
315
+ case 'notify-send': return 'Send notification';
316
+ default: return `Edit ${editorKind}`;
317
+ }
318
+ }
319
+
320
+ function localOperationLabel(localOperation: string): string {
321
+ switch (localOperation) {
322
+ case 'routine-start': return 'Start routine';
323
+ case 'persona-activate':
324
+ case 'persona-use': return 'Activate persona';
325
+ case 'persona-clear': return 'Deactivate persona';
326
+ case 'persona-edit': return 'Edit persona';
327
+ case 'persona-review': return 'Review persona';
328
+ case 'persona-delete': return 'Delete persona';
329
+ case 'memory-edit': return 'Edit memory';
330
+ case 'memory-review': return 'Review memory';
331
+ case 'memory-stale': return 'Mark memory stale';
332
+ case 'memory-delete': return 'Delete memory';
333
+ case 'note-edit': return 'Edit note';
334
+ case 'note-review': return 'Review note';
335
+ case 'note-stale': return 'Mark note stale';
336
+ case 'note-delete': return 'Delete note';
337
+ case 'note-promote-memory': return 'Promote note to memory';
338
+ case 'note-promote-persona': return 'Promote note to persona';
339
+ case 'note-promote-skill': return 'Promote note to skill';
340
+ case 'note-promote-routine': return 'Promote note to routine';
341
+ case 'note-promote-knowledge-url': return 'Promote note to knowledge';
342
+ case 'skill-edit': return 'Edit skill';
343
+ case 'skill-enable': return 'Enable skill';
344
+ case 'skill-disable': return 'Disable skill';
345
+ case 'skill-review': return 'Review skill';
346
+ case 'skill-delete': return 'Delete skill';
347
+ case 'routine-edit': return 'Edit routine';
348
+ case 'routine-enable': return 'Enable routine';
349
+ case 'routine-disable': return 'Disable routine';
350
+ case 'routine-review': return 'Review routine';
351
+ case 'routine-delete': return 'Delete routine';
352
+ default: return 'Apply local action';
78
353
  }
79
- if (action.kind === 'settings-import') return 'Import compatible GoodVibes preferences';
354
+ }
355
+
356
+ function onboardingActionLabel(workspace: AgentWorkspace, action: AgentWorkspaceAction): string {
357
+ if (action.kind === 'settings-import') return 'Import GoodVibes preferences';
80
358
  if (action.kind === 'setup-checkpoint') {
81
- if (action.setupCheckpointOperation === 'show') return 'Show saved setup progress';
82
- if (action.setupCheckpointOperation === 'mark-current') return 'Save current setup progress';
83
- if (action.setupCheckpointOperation === 'clear') return 'Clear saved setup progress';
359
+ if (action.setupCheckpointOperation === 'show') return 'Review saved progress';
360
+ if (action.setupCheckpointOperation === 'mark-current') return 'Save progress';
361
+ if (action.setupCheckpointOperation === 'clear') return 'Clear saved progress';
84
362
  return 'Review setup progress';
85
363
  }
86
- if (action.kind === 'model-picker') return action.modelPickerFlow === 'model' ? 'Choose a model' : 'Choose provider and model';
364
+ if (action.kind === 'model-picker') return action.modelPickerFlow === 'model' ? 'Choose model' : 'Choose provider and model';
87
365
  if (action.kind === 'settings-modal') return 'Open settings';
88
- if (action.kind === 'workspace') return 'Open setup area';
89
- if (action.kind === 'editor') return 'Open guided form';
366
+ if (action.kind === 'workspace') {
367
+ if (action.targetCategoryId) {
368
+ const target = workspace.categories.find((c) => c.id === action.targetCategoryId);
369
+ const label = target ? target.label : action.targetCategoryId;
370
+ return `Switch to ${label}`;
371
+ }
372
+ return 'Switch to setup area';
373
+ }
374
+ if (action.kind === 'editor') return action.editorKind ? editorPurposeLabel(action.editorKind) : 'Open form';
90
375
  if (action.kind === 'local-selection') return 'Move selection';
91
- if (action.kind === 'local-operation') return 'Apply selected library action';
92
- if (action.kind === 'onboarding-complete') return 'Save setup completion';
93
- if (action.safety === 'read-only') return 'Review readiness';
376
+ if (action.kind === 'local-operation') return action.localOperation ? localOperationLabel(action.localOperation) : 'Apply local action';
377
+ if (action.kind === 'onboarding-complete') return 'Finish setup';
378
+ if (action.kind === 'command') {
379
+ const cmd = action.command ?? '';
380
+ return action.commandBehavior === 'inline' ? `Run: ${cmd}` : `Open: ${cmd}`;
381
+ }
382
+ if (action.safety === 'read-only') return 'Review';
94
383
  if (action.safety === 'blocked') return 'Unavailable in this setup step';
95
- return 'Open option';
384
+ return 'Open';
385
+ }
386
+
387
+ function onboardingActionColumns(workspace: AgentWorkspace, action: AgentWorkspaceAction): OnboardingActionColumns {
388
+ if (action.kind === 'setting') {
389
+ return workspace.settingActionDisplay(action) ?? {
390
+ setting: action.label,
391
+ defaultValue: '(unknown)',
392
+ currentValue: '(unknown)',
393
+ };
394
+ }
395
+ return {
396
+ setting: onboardingActionLabel(workspace, action),
397
+ defaultValue: '',
398
+ currentValue: '',
399
+ };
96
400
  }
97
401
 
98
402
  function actionChange(workspace: AgentWorkspace, category: AgentWorkspaceCategory, action: AgentWorkspaceAction): string {
99
- return isOnboardingCategory(category) ? onboardingActionChange(workspace, action) : actionCommand(action);
403
+ return isOnboardingCategory(category) ? onboardingActionColumns(workspace, action).setting : actionCommand(action);
100
404
  }
101
405
 
102
406
  function actionMetaLine(workspace: AgentWorkspace, category: AgentWorkspaceCategory, action: AgentWorkspaceAction): ContextLine {
103
407
  const onboarding = isOnboardingCategory(category);
408
+ if (onboarding) {
409
+ return {
410
+ text: `About: ${compactText(action.detail, 100)}`,
411
+ fg: action.safety === 'blocked' ? PALETTE.warn : PALETTE.muted,
412
+ };
413
+ }
104
414
  return {
105
- text: `${onboarding ? 'Change' : 'Does'}: ${actionChange(workspace, category, action)}`,
106
- fg: action.safety === 'blocked' ? PALETTE.warn : !onboarding && action.kind === 'command' ? PALETTE.info : PALETTE.muted,
415
+ text: `Does: ${actionChange(workspace, category, action)}`,
416
+ fg: action.safety === 'blocked' ? PALETTE.warn : action.kind === 'command' ? PALETTE.info : PALETTE.muted,
107
417
  };
108
418
  }
109
419
 
420
+ function shouldRenderOnboardingSettingsTable(actions: readonly AgentWorkspaceAction[]): boolean {
421
+ // Exclude the synthetic finish footer row — it is not a setting and must not block table mode.
422
+ const nonFinish = actions.filter((a) => a.kind !== 'onboarding-complete');
423
+ return nonFinish.length > 0 && nonFinish.every((action) => action.kind === 'setting');
424
+ }
425
+
110
426
  function editorContextLines(editor: AgentWorkspaceLocalEditor, snapshot: AgentWorkspaceRuntimeSnapshot | null): ContextLine[] {
111
427
  const selected = editor.fields[editor.selectedFieldIndex];
112
428
  const lines: ContextLine[] = [
@@ -244,12 +560,25 @@ function buildEditorFieldRows(editor: AgentWorkspaceLocalEditor, index: number,
244
560
  return rows;
245
561
  }
246
562
 
563
+ function onboardingFinishPrerequisitesMet(workspace: AgentWorkspace): boolean {
564
+ const checklist = workspace.runtimeSnapshot?.setupChecklist ?? [];
565
+ return ONBOARDING_CRITICAL_STEP_IDS.every((id) => checklist.find((item) => item.id === id)?.status === 'ready');
566
+ }
567
+
247
568
  function buildActionRows(workspace: AgentWorkspace, width: number, height: number): WorkspaceRow[] {
248
569
  if (workspace.localEditor) return buildEditorRows(workspace.localEditor, width, height);
249
570
  const category = workspace.selectedActionCategory;
250
571
  const onboarding = isOnboardingCategory(category);
572
+ const settingTable = onboarding
573
+ && !workspace.actionSearchActive
574
+ && shouldRenderOnboardingSettingsTable(workspace.actions);
251
575
  const rows: WorkspaceRow[] = [];
252
- const labelWidth = Math.min(34, Math.max(18, Math.floor(width * 0.38)));
576
+ const valueWidth = settingTable
577
+ ? Math.min(22, Math.max(10, Math.floor(width * 0.18)))
578
+ : 0;
579
+ const labelWidth = settingTable
580
+ ? Math.max(18, width - (valueWidth * 2) - 6)
581
+ : Math.min(34, Math.max(18, Math.floor(width * 0.38)));
253
582
  const commandWidth = Math.max(10, width - labelWidth - 6);
254
583
  if (workspace.actionSearchActive) {
255
584
  rows.push({
@@ -259,32 +588,80 @@ function buildActionRows(workspace: AgentWorkspace, width: number, height: numbe
259
588
  });
260
589
  }
261
590
  rows.push({
262
- text: ` ${padDisplay(workspace.actionSearchActive ? 'Result' : onboarding ? 'Option' : 'Action', labelWidth)} ${padDisplay(onboarding ? 'Change' : 'Does', commandWidth)}`,
591
+ text: settingTable
592
+ ? ` ${padDisplay(workspace.actionSearchActive ? 'Result' : 'Setting', labelWidth)} ${padDisplay('Default', valueWidth)} ${padDisplay('Current', valueWidth)}`
593
+ : ` ${padDisplay(workspace.actionSearchActive ? 'Result' : onboarding ? 'Option' : 'Action', labelWidth)} ${padDisplay('Does', commandWidth)}`,
263
594
  fg: PALETTE.muted,
264
595
  bold: true,
265
596
  });
266
597
 
267
- const actions = workspace.actions;
598
+ // The finish footer row is appended to workspace.actions by the input layer
599
+ // (shouldShowOnboardingFinishFooter). Detect it here by action id for special styling.
600
+ const allActions = workspace.actions;
601
+ const finishActionIndex = allActions.findIndex((a) => a.id === ONBOARDING_COMPLETE_SYNTHETIC_ACTION.id);
602
+
268
603
  const visible = Math.max(1, height - (workspace.actionSearchActive ? 3 : 2));
269
- const window = stableWindow(actions.length, workspace.selectedActionIndex, visible);
604
+ const window = stableWindow(allActions.length, workspace.selectedActionIndex, visible);
270
605
  if (window.start > 0) rows.push({ text: `${GLYPHS.navigation.moreAbove} ${window.start} more action(s) above`, kind: 'more', fg: PALETTE.dim, dim: true });
271
606
 
272
607
  for (let index = window.start; index < window.end; index += 1) {
273
- const action = actions[index]!;
608
+ const action = allActions[index]!;
609
+ const isFinishFooterRow = finishActionIndex >= 0 && index === finishActionIndex;
274
610
  const selected = index === workspace.selectedActionIndex;
275
611
  const searchResult = workspace.actionSearchActive ? workspace.actionSearchResults[index] : null;
276
612
  const actionCategory = searchResult?.category ?? category;
277
613
  const label = searchResult ? `${searchResult.category.label} / ${action.label}` : action.label;
278
614
  const marker = selected ? GLYPHS.navigation.selected : ' ';
279
- rows.push({
280
- text: `${marker} ${padDisplay(label, labelWidth)} ${padDisplay(actionChange(workspace, actionCategory, action), commandWidth)}`,
281
- selected: selected && workspace.focusPane === 'actions',
282
- fg: action.safety === 'blocked' ? PALETTE.warn : selected ? PALETTE.text : PALETTE.info,
283
- bold: selected,
284
- });
615
+
616
+ if (isFinishFooterRow) {
617
+ // Sticky Finish setup footer row
618
+ const prereqsMet = onboardingFinishPrerequisitesMet(workspace);
619
+ const sep = `${GLYPHS.frame.horizontal.repeat(3)} Finish setup ${GLYPHS.frame.horizontal.repeat(3)}`;
620
+ if (!selected) {
621
+ rows.push({
622
+ text: ` ${sep}`,
623
+ kind: 'item',
624
+ fg: PALETTE.dim,
625
+ dim: true,
626
+ });
627
+ }
628
+ const finishDetail = prereqsMet
629
+ ? 'Finish setup'
630
+ : (() => {
631
+ const checklist = workspace.runtimeSnapshot?.setupChecklist ?? [];
632
+ const unmet = [...ONBOARDING_CRITICAL_STEP_IDS]
633
+ .filter((id) => checklist.find((item) => item.id === id)?.status !== 'ready')
634
+ .map((id) => checklist.find((item) => item.id === id)?.label ?? id)
635
+ .join(', ');
636
+ return `Finish needs: ${unmet}`;
637
+ })();
638
+ const text = settingTable
639
+ ? `${marker} ${padDisplay('Finish setup', labelWidth)} ${padDisplay('', valueWidth)} ${padDisplay('', valueWidth)}`
640
+ : `${marker} ${padDisplay('Finish setup', labelWidth)} ${padDisplay(finishDetail, commandWidth)}`;
641
+ rows.push({
642
+ text,
643
+ selected: selected && workspace.focusPane === 'actions',
644
+ fg: prereqsMet ? PALETTE.good : PALETTE.warn,
645
+ bold: selected,
646
+ kind: 'item',
647
+ });
648
+ } else {
649
+ const text = settingTable
650
+ ? (() => {
651
+ const columns = onboardingActionColumns(workspace, action);
652
+ return `${marker} ${padDisplay(columns.setting, labelWidth)} ${padDisplay(columns.defaultValue, valueWidth)} ${padDisplay(columns.currentValue, valueWidth)}`;
653
+ })()
654
+ : `${marker} ${padDisplay(label, labelWidth)} ${padDisplay(actionChange(workspace, actionCategory, action), commandWidth)}`;
655
+ rows.push({
656
+ text,
657
+ selected: selected && workspace.focusPane === 'actions',
658
+ fg: action.safety === 'blocked' ? PALETTE.warn : selected ? PALETTE.text : PALETTE.info,
659
+ bold: selected,
660
+ });
661
+ }
285
662
  }
286
663
 
287
- if (window.end < actions.length) rows.push({ text: `${GLYPHS.navigation.moreBelow} ${actions.length - window.end} more action(s) below`, kind: 'more', fg: PALETTE.dim, dim: true });
664
+ if (window.end < allActions.length) rows.push({ text: `${GLYPHS.navigation.moreBelow} ${allActions.length - window.end} more action(s) below`, kind: 'more', fg: PALETTE.dim, dim: true });
288
665
  rows.push({ text: '' });
289
666
  rows.push({ text: `Status: ${workspace.status}`, fg: PALETTE.muted });
290
667
  if (workspace.lastActionResult) {
@@ -30,7 +30,7 @@ export async function setupPostureCatalogStatus(context: CommandContext): Promis
30
30
  autonomyBlockers: plan.filter((item) => item.blocksAutonomy && item.status !== 'ready').length,
31
31
  nextSetupHandoffs: nextSetupHandoffSummaries(plan, 5),
32
32
  setupWizard,
33
- setupCloseout: setupWizard.closeout,
33
+ setupCloseout: setupWizard._diagnostic.closeout,
34
34
  collectionIssues: snapshot.collectionIssues.length,
35
35
  setupMarkerExists: setupCompletionMarkerExists(context),
36
36
  setupSmokeEvidence,
@@ -81,7 +81,7 @@ export async function setupRepairSummary(context: CommandContext, args: AgentHar
81
81
  status: setupWizard.status,
82
82
  currentStepId: setupWizard.currentStepId,
83
83
  currentStepLabel: setupWizard.currentStepLabel,
84
- closeout: setupWizard.closeout,
84
+ closeout: setupWizard._diagnostic.closeout,
85
85
  },
86
86
  routes: {
87
87
  inspectSetup: DEFAULT_AGENT_SETUP_WIZARD_REVIEW_ROUTE,
@@ -149,14 +149,14 @@ export async function setupPostureSummary(context: CommandContext, args: AgentHa
149
149
  progressLabel: setupWizard.progressLabel,
150
150
  currentStepId: setupWizard.currentStepId,
151
151
  currentStepLabel: setupWizard.currentStepLabel,
152
- repeatedBlocker: setupWizard.repeatedBlocker,
152
+ repeatedBlocker: setupWizard._diagnostic.repeatedBlocker,
153
153
  },
154
- setupCloseout: setupWizard.closeout,
154
+ setupCloseout: setupWizard._diagnostic.closeout,
155
155
  },
156
156
  setupSmokeEvidence,
157
157
  setupSmokeHistory,
158
158
  setupWizard,
159
- setupCloseout: setupWizard.closeout,
159
+ setupCloseout: setupWizard._diagnostic.closeout,
160
160
  currentRoute: snapshot.providerRouting,
161
161
  issues: snapshot.collectionIssues,
162
162
  readinessPlan: filteredPlan.map((item) => describePlanItem(item, includeParameters)),
@@ -175,7 +175,7 @@ export async function setupCheckpointSummary(context: CommandContext): Promise<R
175
175
  const setupWizard = buildSetupWizard(plan, context);
176
176
  return {
177
177
  mode: 'setup_checkpoint',
178
- checkpoint: setupWizard.checkpoint,
178
+ checkpoint: setupWizard._diagnostic.checkpoint,
179
179
  currentStep: setupWizard.currentStepId
180
180
  ? setupWizard.steps.find((step) => step.id === setupWizard.currentStepId) ?? null
181
181
  : null,
@@ -254,7 +254,7 @@ export async function markSetupCheckpoint(context: CommandContext, args: AgentHa
254
254
  progressLabel: updatedWizard.progressLabel,
255
255
  currentStepId: updatedWizard.currentStepId,
256
256
  currentStepLabel: updatedWizard.currentStepLabel,
257
- checkpoint: updatedWizard.checkpoint,
257
+ checkpoint: updatedWizard._diagnostic.checkpoint,
258
258
  },
259
259
  routes: {
260
260
  inspectCheckpoint: DEFAULT_AGENT_SETUP_WIZARD_INSPECT_CHECKPOINT_ROUTE,
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '1.3.0';
9
+ let _version = '1.4.1';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {
12
12
  readonly version?: unknown;