@pellux/goodvibes-tui 1.13.1 → 1.14.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 (158) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -6
  3. package/bin/goodvibes +6 -0
  4. package/bin/goodvibes-daemon +6 -0
  5. package/bin/launcher-support.js +34 -1
  6. package/docs/foundation-artifacts/operator-contract.json +8076 -2047
  7. package/package.json +15 -11
  8. package/scripts/postinstall.js +73 -48
  9. package/src/cli/bundle-command.ts +5 -5
  10. package/src/cli/completion.ts +1 -1
  11. package/src/cli/completions/generate.ts +15 -3
  12. package/src/cli/doctor.ts +385 -0
  13. package/src/cli/entrypoint.ts +72 -1
  14. package/src/cli/help.ts +29 -9
  15. package/src/cli/hooks-command.ts +74 -0
  16. package/src/cli/hooks-report.ts +167 -0
  17. package/src/cli/index.ts +3 -0
  18. package/src/cli/management.ts +13 -1
  19. package/src/cli/parser.ts +10 -2
  20. package/src/cli/plugin-bundles-command.ts +228 -0
  21. package/src/cli/plugin-command.ts +179 -0
  22. package/src/cli/service-posture.ts +10 -0
  23. package/src/cli/status.ts +275 -2
  24. package/src/cli/tui-startup.ts +119 -1
  25. package/src/cli/types.ts +3 -1
  26. package/src/config/index.ts +16 -0
  27. package/src/config/tui-extension-settings.ts +169 -0
  28. package/src/core/approval-alert.ts +15 -2
  29. package/src/core/compaction-receipt.ts +127 -0
  30. package/src/core/model-routing-chip.ts +82 -0
  31. package/src/core/permission-mode.ts +89 -0
  32. package/src/core/rewind-receipt.ts +83 -0
  33. package/src/core/rewind-turn-anchors.ts +160 -0
  34. package/src/core/scriptable-statusline.ts +141 -0
  35. package/src/core/session-auto-titler.ts +110 -0
  36. package/src/core/stream-event-wiring.ts +20 -0
  37. package/src/core/system-message-router.ts +1 -1
  38. package/src/core/terminal-notifier.ts +102 -0
  39. package/src/core/turn-event-wiring.ts +80 -1
  40. package/src/input/command-reference.ts +96 -0
  41. package/src/input/command-registry.ts +25 -0
  42. package/src/input/commands/channel-pairing.ts +245 -0
  43. package/src/input/commands/channel-runtime.ts +152 -6
  44. package/src/input/commands/checkin-runtime.ts +89 -0
  45. package/src/input/commands/checkpoint-runtime.ts +7 -179
  46. package/src/input/commands/ci-runtime.ts +207 -0
  47. package/src/input/commands/cost-attribution-format.ts +52 -0
  48. package/src/input/commands/cost-runtime.ts +44 -4
  49. package/src/input/commands/editor-runtime.ts +22 -0
  50. package/src/input/commands/flags-runtime.ts +328 -0
  51. package/src/input/commands/health-metrics-format.ts +67 -0
  52. package/src/input/commands/health-runtime.ts +55 -1
  53. package/src/input/commands/memory.ts +23 -0
  54. package/src/input/commands/operator-rpc.ts +95 -0
  55. package/src/input/commands/palette-runtime.ts +22 -0
  56. package/src/input/commands/permissions-provenance.ts +264 -0
  57. package/src/input/commands/permissions-runtime.ts +21 -0
  58. package/src/input/commands/planning-runtime.ts +28 -1
  59. package/src/input/commands/platform-access-runtime.ts +2 -97
  60. package/src/input/commands/principals-runtime.ts +212 -0
  61. package/src/input/commands/product-runtime.ts +43 -3
  62. package/src/input/commands/recall-files-config.ts +54 -0
  63. package/src/input/commands/recall-files-git.ts +57 -0
  64. package/src/input/commands/recall-files.ts +185 -0
  65. package/src/input/commands/recall-query.ts +8 -3
  66. package/src/input/commands/recall-review.ts +2 -2
  67. package/src/input/commands/recall-shared.ts +13 -0
  68. package/src/input/commands/relay-runtime.ts +86 -0
  69. package/src/input/commands/review-runtime.ts +160 -0
  70. package/src/input/commands/rewind-runtime.ts +362 -0
  71. package/src/input/commands/schedule-nl.ts +147 -0
  72. package/src/input/commands/schedule-runtime.ts +29 -10
  73. package/src/input/commands/secret-runtime.ts +55 -0
  74. package/src/input/commands/session-content.ts +19 -8
  75. package/src/input/commands/session-workflow.ts +10 -0
  76. package/src/input/commands/update-runtime.ts +363 -0
  77. package/src/input/commands/workstream-attempt-dependents.ts +103 -0
  78. package/src/input/commands/workstream-attempts.ts +244 -0
  79. package/src/input/commands/workstream-runtime.ts +164 -26
  80. package/src/input/commands/worktree-runtime.ts +56 -2
  81. package/src/input/commands.ts +135 -80
  82. package/src/input/composer-capture.ts +67 -0
  83. package/src/input/composer-editor.ts +134 -0
  84. package/src/input/concealed-input.ts +94 -0
  85. package/src/input/exec-env-scrub-config.ts +84 -0
  86. package/src/input/feature-unit-layout.ts +142 -0
  87. package/src/input/feed-context-factory.ts +2 -0
  88. package/src/input/handler-feed-routes.ts +17 -0
  89. package/src/input/handler-feed.ts +3 -0
  90. package/src/input/handler-modal-routes.ts +5 -2
  91. package/src/input/handler-shortcuts.ts +37 -0
  92. package/src/input/handler.ts +24 -2
  93. package/src/input/keybindings.ts +11 -2
  94. package/src/input/onboarding/onboarding-feature-units.ts +536 -0
  95. package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
  96. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
  97. package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
  98. package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
  99. package/src/input/relay-settings-descriptions.ts +35 -0
  100. package/src/input/sandbox-exec-config.ts +107 -0
  101. package/src/input/settings-modal-adjustment.ts +4 -7
  102. package/src/input/settings-modal-data.ts +178 -8
  103. package/src/input/settings-modal-mutations.ts +41 -13
  104. package/src/input/settings-modal-types.ts +42 -5
  105. package/src/input/settings-modal.ts +67 -15
  106. package/src/input/submission-router.ts +4 -1
  107. package/src/input/worktree-setup-config.ts +93 -0
  108. package/src/main.ts +43 -43
  109. package/src/panels/builtin/development.ts +14 -0
  110. package/src/panels/diff-review-model.ts +189 -0
  111. package/src/panels/diff-review-panel.ts +395 -0
  112. package/src/panels/fleet-panel-format.ts +114 -2
  113. package/src/panels/fleet-panel.ts +65 -78
  114. package/src/panels/fleet-read-model.ts +91 -11
  115. package/src/panels/fleet-stop.ts +19 -4
  116. package/src/panels/modals/memory-modal.ts +11 -2
  117. package/src/permissions/hunk-selection.ts +86 -5
  118. package/src/permissions/prompt.ts +108 -7
  119. package/src/permissions/sandbox-exec-gate.ts +162 -0
  120. package/src/renderer/settings-modal-helpers.ts +21 -1
  121. package/src/renderer/settings-modal.ts +70 -44
  122. package/src/renderer/shell-surface.ts +23 -1
  123. package/src/renderer/system-message.ts +4 -0
  124. package/src/renderer/tool-result-summary.ts +95 -5
  125. package/src/renderer/ui-factory.ts +22 -0
  126. package/src/runtime/bootstrap-command-context.ts +15 -1
  127. package/src/runtime/bootstrap-command-parts.ts +14 -2
  128. package/src/runtime/bootstrap-core.ts +38 -38
  129. package/src/runtime/bootstrap-notifier-sync.ts +41 -0
  130. package/src/runtime/bootstrap-shell.ts +8 -0
  131. package/src/runtime/bootstrap.ts +21 -23
  132. package/src/runtime/context-accounting-source.ts +103 -0
  133. package/src/runtime/conversation-rewind-port.ts +130 -0
  134. package/src/runtime/fleet-needs-input-push.ts +70 -0
  135. package/src/runtime/install-self-check-startup.ts +36 -0
  136. package/src/runtime/install-self-check.ts +149 -0
  137. package/src/runtime/onboarding/fast-path.ts +93 -0
  138. package/src/runtime/process-lifecycle.ts +32 -24
  139. package/src/runtime/relay-reachability-bridge.ts +53 -0
  140. package/src/runtime/release-artifacts.ts +79 -0
  141. package/src/runtime/services.ts +33 -33
  142. package/src/runtime/trust/workspace-registration.ts +184 -0
  143. package/src/runtime/trust/workspace-trust.ts +202 -0
  144. package/src/runtime/ui-services.ts +2 -1
  145. package/src/runtime/update-check.ts +113 -0
  146. package/src/runtime/workspace-checkpointing.ts +32 -0
  147. package/src/runtime/workstream-attempts-validation.ts +77 -0
  148. package/src/runtime/workstream-draft-edits.ts +95 -0
  149. package/src/runtime/workstream-draft-store.ts +101 -0
  150. package/src/runtime/workstream-draft-types.ts +42 -0
  151. package/src/runtime/workstream-services.ts +70 -39
  152. package/src/shell/blocking-input.ts +29 -0
  153. package/src/shell/ui-openers.ts +65 -0
  154. package/src/verification/verification-ledger.ts +56 -2
  155. package/src/version.ts +1 -1
  156. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
  157. package/src/runtime/gateway-verbs.ts +0 -47
  158. package/src/runtime/render-scheduler.ts +0 -90
@@ -1,14 +1,148 @@
1
- import type { CommandRegistry } from '../command-registry.ts';
1
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
2
2
  import { requireIntegrationHelpers } from './runtime-services.ts';
3
+ import { runChannelPairing } from './channel-pairing.ts';
4
+ import { describeOperatorRpcError, getOperatorRpc } from './operator-rpc.ts';
5
+ import type { OperatorMethodOutput } from '@pellux/goodvibes-sdk';
6
+
7
+ type ChannelProfileBinding = OperatorMethodOutput<'channels.profiles.list'>['bindings'][number];
8
+
9
+ function renderProfileBinding(binding: ChannelProfileBinding): string {
10
+ const scope = binding.channelId ? `${binding.surfaceKind}:${binding.channelId}` : `${binding.surfaceKind} (surface-wide default)`;
11
+ const lines = [` ${scope}`];
12
+ if (binding.model) lines.push(` model: ${binding.model}${binding.provider ? ` (${binding.provider})` : ''}`);
13
+ if (binding.permissionMode) lines.push(` permission mode: ${binding.permissionMode}`);
14
+ return lines.join('\n');
15
+ }
16
+
17
+ const CHANNEL_PROFILES_SUBCOMMANDS = ['list', 'get', 'set', 'delete'] as const;
18
+
19
+ async function handleChannelProfilesSubcommand(args: string[], ctx: CommandContext): Promise<void> {
20
+ const sub = args[0] ?? 'list';
21
+ if (!(CHANNEL_PROFILES_SUBCOMMANDS as readonly string[]).includes(sub)) {
22
+ ctx.print(
23
+ 'Usage: /channel profiles <subcommand>\n'
24
+ + ' list — every per-channel profile binding\n'
25
+ + ' get <surfaceKind> [channelId] — one binding\n'
26
+ + ' set <surfaceKind> [channelId] [--model x] [--provider x] [--permission-mode x] — bind (upsert)\n'
27
+ + ' delete <surfaceKind> [channelId] — remove a binding'
28
+ );
29
+ return;
30
+ }
31
+
32
+ if (sub === 'list') {
33
+ const rpc = getOperatorRpc(ctx);
34
+ if (!rpc.available) {
35
+ ctx.print(`[channel profiles] ${rpc.reason}`);
36
+ return;
37
+ }
38
+ try {
39
+ const { bindings } = await rpc.sdk.operator.invoke('channels.profiles.list', {});
40
+ if (bindings.length === 0) {
41
+ ctx.print('[channel profiles] no profile bindings configured.');
42
+ return;
43
+ }
44
+ ctx.print(['Channel profile bindings:', ...bindings.map(renderProfileBinding)].join('\n'));
45
+ } catch (error) {
46
+ ctx.print(`[channel profiles list] ${describeOperatorRpcError(error)}`);
47
+ }
48
+ return;
49
+ }
50
+
51
+ if (sub === 'get') {
52
+ const surfaceKind = args[1];
53
+ const channelId = args[2];
54
+ if (!surfaceKind) {
55
+ ctx.print('Usage: /channel profiles get <surfaceKind> [channelId]');
56
+ return;
57
+ }
58
+ const rpc = getOperatorRpc(ctx);
59
+ if (!rpc.available) {
60
+ ctx.print(`[channel profiles] ${rpc.reason}`);
61
+ return;
62
+ }
63
+ try {
64
+ const { binding } = await rpc.sdk.operator.invoke('channels.profiles.get', { surfaceKind, ...(channelId ? { channelId } : {}) });
65
+ ctx.print(renderProfileBinding(binding));
66
+ } catch (error) {
67
+ ctx.print(`[channel profiles get] ${describeOperatorRpcError(error)}`);
68
+ }
69
+ return;
70
+ }
71
+
72
+ if (sub === 'set') {
73
+ const rest = args.slice(1);
74
+ const flagStart = rest.findIndex((token) => token.startsWith('--'));
75
+ const positional = flagStart === -1 ? rest : rest.slice(0, flagStart);
76
+ const flagTokens = flagStart === -1 ? [] : rest.slice(flagStart);
77
+ const surfaceKind = positional[0];
78
+ const channelId = positional[1];
79
+ if (!surfaceKind) {
80
+ ctx.print('Usage: /channel profiles set <surfaceKind> [channelId] [--model x] [--provider x] [--permission-mode plan|normal|accept-edits|auto]');
81
+ return;
82
+ }
83
+ const flags = new Map<string, string>();
84
+ for (let i = 0; i < flagTokens.length; i++) {
85
+ const token = flagTokens[i]!;
86
+ if (!token.startsWith('--')) continue;
87
+ const name = token.slice(2);
88
+ const value = flagTokens[i + 1];
89
+ if (value !== undefined) {
90
+ flags.set(name, value);
91
+ i++;
92
+ }
93
+ }
94
+ const rpc = getOperatorRpc(ctx);
95
+ if (!rpc.available) {
96
+ ctx.print(`[channel profiles] ${rpc.reason}`);
97
+ return;
98
+ }
99
+ try {
100
+ const { binding } = await rpc.sdk.operator.invoke('channels.profiles.set', {
101
+ surfaceKind,
102
+ ...(channelId ? { channelId } : {}),
103
+ ...(flags.has('model') ? { model: flags.get('model')! } : {}),
104
+ ...(flags.has('provider') ? { provider: flags.get('provider')! } : {}),
105
+ ...(flags.has('permission-mode') ? { permissionMode: flags.get('permission-mode') as 'plan' | 'normal' | 'accept-edits' | 'auto' } : {}),
106
+ });
107
+ ctx.print(`[channel profiles set] bound ${binding.id}\n${renderProfileBinding(binding)}`);
108
+ } catch (error) {
109
+ ctx.print(`[channel profiles set] ${describeOperatorRpcError(error)}`);
110
+ }
111
+ return;
112
+ }
113
+
114
+ if (sub === 'delete') {
115
+ const surfaceKind = args[1];
116
+ const channelId = args[2];
117
+ if (!surfaceKind) {
118
+ ctx.print('Usage: /channel profiles delete <surfaceKind> [channelId]');
119
+ return;
120
+ }
121
+ const rpc = getOperatorRpc(ctx);
122
+ if (!rpc.available) {
123
+ ctx.print(`[channel profiles] ${rpc.reason}`);
124
+ return;
125
+ }
126
+ try {
127
+ const result = await rpc.sdk.operator.invoke('channels.profiles.delete', { surfaceKind, ...(channelId ? { channelId } : {}) });
128
+ ctx.print(result.deleted
129
+ ? `[channel profiles delete] removed binding for ${surfaceKind}${channelId ? `:${channelId}` : ''}.`
130
+ : `[channel profiles delete] no binding found for ${surfaceKind}${channelId ? `:${channelId}` : ''}.`);
131
+ } catch (error) {
132
+ ctx.print(`[channel profiles delete] ${describeOperatorRpcError(error)}`);
133
+ }
134
+ return;
135
+ }
136
+ }
3
137
 
4
138
  export function registerChannelRuntimeCommands(registry: CommandRegistry): void {
5
139
  registry.register({
6
140
  name: 'channel',
7
- aliases: [],
8
- description: 'Inspect channel routes, delivery strategies, and ingress policies',
9
- usage: '[status|routes|delivery|policy] [--json]',
10
- argsHint: 'status | routes | delivery | policy',
11
- handler(args, ctx) {
141
+ aliases: ['channels'],
142
+ description: 'Pair channels and inspect routes, delivery strategies, ingress policies, and per-channel profile bindings',
143
+ usage: '[pair [surface]|status|routes|delivery|policy|profiles [list|get|set|delete]] [--json]',
144
+ argsHint: 'pair | status | routes | delivery | policy | profiles',
145
+ async handler(args, ctx) {
12
146
  const sub = args[0];
13
147
  const asJson = args.includes('--json');
14
148
 
@@ -17,6 +151,16 @@ export function registerChannelRuntimeCommands(registry: CommandRegistry): void
17
151
  return;
18
152
  }
19
153
 
154
+ if (sub === 'pair') {
155
+ await runChannelPairing(args.slice(1), ctx);
156
+ return;
157
+ }
158
+
159
+ if (sub === 'profiles') {
160
+ await handleChannelProfilesSubcommand(args.slice(1), ctx);
161
+ return;
162
+ }
163
+
20
164
  const helpers = requireIntegrationHelpers(ctx);
21
165
 
22
166
  if (sub === 'status') {
@@ -126,10 +270,12 @@ export function registerChannelRuntimeCommands(registry: CommandRegistry): void
126
270
  ctx.print(
127
271
  'Usage: /channel <subcommand>\n'
128
272
  + ' (no args) — open the Routes panel\n'
273
+ + ' pair [surface] — guided channel pairing: list adapters, enter declared credentials, verify\n'
129
274
  + ' status — channel overview: routes, sessions, tasks, pending approvals\n'
130
275
  + ' routes — active route binding snapshot\n'
131
276
  + ' delivery — outbound delivery snapshot\n'
132
277
  + ' policy — configured channel surfaces and ingress policy location\n'
278
+ + ' profiles [list|get|set|delete] — per-channel model/permission profile bindings\n'
133
279
  + '\n'
134
280
  + 'Options:\n'
135
281
  + ' --json Output raw JSON for scripting'
@@ -0,0 +1,89 @@
1
+ /**
2
+ * checkin-runtime.ts
3
+ *
4
+ * `/checkin` — the proactive check-in surface: config (enabled/cadence/
5
+ * delivery channel/quiet hours) and its receipt trail, over the operator
6
+ * wire (see operator-rpc.ts for why this goes over HTTP rather than the
7
+ * in-process OperatorClient facade). Off by default; every run — scheduled
8
+ * or manual — leaves a receipt (`checkin.receipts.list`) recording whether it
9
+ * stayed quiet, delivered a message, or was skipped, so the config surface
10
+ * in /config's Automation group is never the only place this feature's
11
+ * behavior is visible.
12
+ */
13
+ import type { CommandRegistry } from '../command-registry.ts';
14
+ import { describeOperatorRpcError, getOperatorRpc } from './operator-rpc.ts';
15
+ import type { OperatorMethodOutput } from '@pellux/goodvibes-sdk';
16
+
17
+ type CheckinConfig = OperatorMethodOutput<'checkin.config.get'>['config'];
18
+ type CheckinReceipt = OperatorMethodOutput<'checkin.receipts.list'>['receipts'][number];
19
+
20
+ export function renderConfig(config: CheckinConfig): string {
21
+ return [
22
+ 'Check-in config:',
23
+ ` enabled: ${config.enabled ? 'yes' : 'no'}`,
24
+ ` cadence (cron): ${config.cadence || '(not set)'}`,
25
+ ` delivery channel: ${config.deliveryChannel || '(not set)'}`,
26
+ ` quiet hours: ${config.quietHours || '(none)'}`,
27
+ ].join('\n');
28
+ }
29
+
30
+ export function renderReceipt(receipt: CheckinReceipt): string {
31
+ const parts = [` ${new Date(receipt.ranAt).toISOString()} [${receipt.trigger}] ${receipt.outcome}`];
32
+ parts.push(` ${receipt.briefingSummary}`);
33
+ if (receipt.decisionReason) parts.push(` reason: ${receipt.decisionReason}`);
34
+ if (receipt.deliveredMessage) parts.push(` delivered: ${receipt.deliveredMessage}`);
35
+ if (receipt.deliveryChannel) parts.push(` channel: ${receipt.deliveryChannel}`);
36
+ if (receipt.error) parts.push(` error: ${receipt.error}`);
37
+ return parts.join('\n');
38
+ }
39
+
40
+ export function registerCheckinRuntimeCommands(registry: CommandRegistry): void {
41
+ registry.register({
42
+ name: 'checkin',
43
+ description: 'Proactive check-in: view config + recent receipts, or trigger one now',
44
+ usage: '[run]',
45
+ argsHint: '[run]',
46
+ async handler(args, ctx) {
47
+ const rpc = getOperatorRpc(ctx);
48
+ if (!rpc.available) {
49
+ ctx.print(`[checkin] ${rpc.reason}`);
50
+ return;
51
+ }
52
+
53
+ if (args[0] === 'run') {
54
+ try {
55
+ const result = await rpc.sdk.operator.invoke('checkin.run', {});
56
+ const lines = [`[checkin run] outcome: ${result.outcome}`, ` ${result.summary}`];
57
+ if (result.deliveryId) lines.push(` delivery id: ${result.deliveryId}`);
58
+ ctx.print(lines.join('\n'));
59
+ } catch (error) {
60
+ ctx.print(`[checkin run] ${describeOperatorRpcError(error)}`);
61
+ }
62
+ return;
63
+ }
64
+
65
+ if (args.length > 0) {
66
+ ctx.print('Usage: /checkin [run]');
67
+ return;
68
+ }
69
+
70
+ try {
71
+ const [{ config }, { receipts }] = await Promise.all([
72
+ rpc.sdk.operator.invoke('checkin.config.get', {}),
73
+ rpc.sdk.operator.invoke('checkin.receipts.list', { limit: 10 }),
74
+ ]);
75
+ const lines = [renderConfig(config), ''];
76
+ if (receipts.length === 0) {
77
+ lines.push('No check-in receipts yet.');
78
+ } else {
79
+ lines.push('Recent receipts:');
80
+ lines.push(...receipts.map(renderReceipt));
81
+ }
82
+ lines.push('', ' /checkin run — trigger one now');
83
+ ctx.print(lines.join('\n'));
84
+ } catch (error) {
85
+ ctx.print(`[checkin] ${describeOperatorRpcError(error)}`);
86
+ }
87
+ },
88
+ });
89
+ }
@@ -1,26 +1,19 @@
1
1
  // ---------------------------------------------------------------------------
2
- // checkpoint-runtime.ts — /checkpoints, /checkpoint, /rewind
2
+ // checkpoint-runtime.ts — /checkpoints, /checkpoint
3
3
  //
4
4
  // UX over WorkspaceCheckpointManager (@pellux/goodvibes-sdk/platform/workspace),
5
- // the whole-workspace, git-backed rewind engine wired onto RuntimeServices as
5
+ // the whole-workspace, git-backed snapshot engine wired onto RuntimeServices as
6
6
  // `workspaceCheckpointManager` (src/runtime/services.ts) and threaded through
7
7
  // CommandContext.workspace exactly like FileUndoManager already is.
8
8
  //
9
- // Architecture note (why the confirm step lives in DiffPanel, not here):
10
- // SlashCommand.handler(args, ctx) is a single-shot call — there is no "await
11
- // next keypress" primitive in the command layer. So `/rewind <id>` can only
12
- // resolve the target, load the preview, open+focus the diff panel, and arm
13
- // its confirm overlay (DiffPanel.confirmOverlay, a PanelConfirmOverlay —
14
- // see panel-confirm-overlay.ts); the actual y/n/Enter/Esc handling happens
15
- // in DiffPanel.handleInput() via the project's canonical ConfirmState<T>
16
- // contract (confirm-state.ts), which every other destructive-action confirm
17
- // in this codebase already uses (see git-panel.ts).
9
+ // Restoring a checkpoint is now done through the unified, message-anchored
10
+ // /rewind (rewind-runtime.ts), which rewinds files AND/OR conversation to a
11
+ // completed turn — reusing this same checkpoint store for the files half.
18
12
  // ---------------------------------------------------------------------------
19
13
 
20
14
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
21
- import type { WorkspaceCheckpoint, WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
15
+ import type { WorkspaceCheckpoint } from '@pellux/goodvibes-sdk/platform/workspace';
22
16
  import type { CommandContext, CommandRegistry } from '../command-registry.ts';
23
- import { requirePanelManager } from './runtime-services.ts';
24
17
 
25
18
  // ---------------------------------------------------------------------------
26
19
  // Formatting helpers
@@ -50,62 +43,6 @@ function formatBytes(bytes: number): string {
50
43
  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
51
44
  }
52
45
 
53
- function plural(n: number, word: string): string {
54
- return `${n} ${word}${n === 1 ? '' : 's'}`;
55
- }
56
-
57
- // ---------------------------------------------------------------------------
58
- // Checkpoint resolution — shared by /rewind
59
- // ---------------------------------------------------------------------------
60
-
61
- type ResolveResult =
62
- | { checkpoint: WorkspaceCheckpoint }
63
- | { error: string };
64
-
65
- /**
66
- * Resolve a user-supplied checkpoint reference ('last' or an id / id prefix,
67
- * as shown by /checkpoints) against the live checkpoint list. Resolution
68
- * happens entirely against list() results — never against
69
- * WorkspaceCheckpointManager's own requireCheckpoint() — so an unknown ref is
70
- * caught here, before anything touches the diff panel or arms a confirm.
71
- *
72
- * mgr.list() is guarded here (not left to the caller) so every caller gets a
73
- * plain ResolveResult and can never be handed a rejected promise: if the
74
- * manager's list() call fails (including a cached init() failure — see
75
- * services.ts — which makes every WorkspaceCheckpointManager method reject
76
- * forever), that surfaces as an honest `{ error }` instead of an unhandled
77
- * rejection.
78
- */
79
- async function resolveCheckpointTarget(
80
- mgr: WorkspaceCheckpointManager,
81
- ref: string,
82
- ): Promise<ResolveResult> {
83
- let all: WorkspaceCheckpoint[];
84
- try {
85
- all = await mgr.list();
86
- } catch (err) {
87
- return { error: `Failed to list checkpoints: ${summarizeError(err)}` };
88
- }
89
- if (ref === 'last') {
90
- const checkpoint = all[0];
91
- if (!checkpoint) {
92
- return { error: 'No checkpoints to rewind to. Use /checkpoint <label> to create one.' };
93
- }
94
- return { checkpoint };
95
- }
96
- const exact = all.find((c) => c.id === ref);
97
- if (exact) return { checkpoint: exact };
98
-
99
- const prefixMatches = all.filter((c) => c.id.startsWith(ref) || shortId(c.id).startsWith(ref));
100
- if (prefixMatches.length === 1) return { checkpoint: prefixMatches[0]! };
101
- if (prefixMatches.length > 1) {
102
- return {
103
- error: `Checkpoint id "${ref}" is ambiguous — matches ${prefixMatches.length} checkpoints. Use a longer prefix or run /checkpoints for full ids.`,
104
- };
105
- }
106
- return { error: `Unknown checkpoint id: "${ref}". Run /checkpoints to see available checkpoints.` };
107
- }
108
-
109
46
  // ---------------------------------------------------------------------------
110
47
  // Commands
111
48
  // ---------------------------------------------------------------------------
@@ -140,7 +77,7 @@ export function registerCheckpointRuntimeCommands(registry: CommandRegistry): vo
140
77
  // them would cost one diff spawn per checkpoint (O(checkpoints)), which
141
78
  // stops being cheap once there are more than a handful. /rewind <id>
142
79
  // loads exactly one diff, so the exact file list is available there.
143
- ctx.print('Use /rewind <id|last> to preview the exact file changes and restore a checkpoint.');
80
+ ctx.print('Use /rewind to preview and restore a completed turn (files and/or conversation) — it reuses these checkpoints for the files half.');
144
81
  },
145
82
  });
146
83
 
@@ -168,113 +105,4 @@ export function registerCheckpointRuntimeCommands(registry: CommandRegistry): vo
168
105
  }
169
106
  },
170
107
  });
171
-
172
- registry.register({
173
- name: 'rewind',
174
- description: 'Preview and restore a workspace checkpoint (files only — conversation history is unchanged)',
175
- usage: '<id|last>',
176
- argsHint: '<id|last>',
177
- handler: async (args, ctx: CommandContext) => {
178
- const mgr = ctx.workspace.workspaceCheckpointManager;
179
- if (!mgr) {
180
- ctx.print('Checkpoints are not available in this session.');
181
- return;
182
- }
183
- const ref = args[0];
184
- if (!ref) {
185
- ctx.print('Usage: /rewind <id|last>. Run /checkpoints to see available checkpoints.');
186
- return;
187
- }
188
-
189
- const resolved = await resolveCheckpointTarget(mgr, ref);
190
- if ('error' in resolved) {
191
- ctx.print(resolved.error);
192
- return;
193
- }
194
- const { checkpoint } = resolved;
195
-
196
- let diff: Awaited<ReturnType<WorkspaceCheckpointManager['diff']>>;
197
- try {
198
- diff = await mgr.diff(checkpoint.id);
199
- } catch (err) {
200
- ctx.print(`Could not load checkpoint preview: ${summarizeError(err)}`);
201
- return;
202
- }
203
-
204
- const { DiffPanel } = await import('../../panels/diff-panel.ts');
205
- const pm = requirePanelManager(ctx);
206
- let panel = pm.getAllOpen().find((p) => p.id === 'diff');
207
- if (!panel) {
208
- try {
209
- panel = pm.open('diff');
210
- } catch {
211
- ctx.print('Could not open diff panel.');
212
- return;
213
- }
214
- }
215
- pm.activateById('diff');
216
- if (!pm.isVisible()) pm.show();
217
- // Must focus the panel — otherwise the user is left typing at the
218
- // prompt while the panel silently shows a pending confirm they never
219
- // see (the single most likely UX bug in this feature).
220
- ctx.focusPanels?.();
221
-
222
- const diffPanel = panel as InstanceType<typeof DiffPanel>;
223
- if (diff.unifiedDiff.trim()) {
224
- diffPanel.loadRawDiff(diff.unifiedDiff);
225
- } else {
226
- diffPanel.showDiff('(no changes)', '@@ -0,0 +0,0 @@\n Working tree already matches this checkpoint.');
227
- }
228
-
229
- diffPanel.confirmOverlay.arm({
230
- id: checkpoint.id,
231
- // renderConfirmLines (panels/confirm-state.ts) builds the on-screen
232
- // prompt as `${verb} "${label}"?` itself — it supplies both the verb
233
- // and the surrounding quotes. A label that already contained "Restore
234
- // "..."" duplicated both, rendering as `Restore "Restore
235
- // "baseline-pin"…`. label here must be just the descriptive subject,
236
- // with no verb and no quotes of its own (same convention GitPanel
237
- // already follows for its own confirms — see git-panel.ts:352,370).
238
- label: `${checkpoint.label} (${plural(diff.files.length, 'file')} differ) — files only, conversation history is unchanged`,
239
- verb: 'Restore',
240
- onConfirm: async () => {
241
- try {
242
- const result = await mgr.restore(checkpoint.id, { safetyCheckpoint: true });
243
- ctx.print(
244
- `Rewind complete: restored ${plural(result.restoredFiles.length, 'file')}, removed ${plural(result.removedFiles.length, 'file')}. ` +
245
- `Safety checkpoint: ${result.safetyCheckpointId ? shortId(result.safetyCheckpointId) : '(none — working tree already matched)'}.`,
246
- );
247
- // Conversation is NOT rewound: addSystemMessage (not ctx.print)
248
- // persists this note into real turn/session history so it is
249
- // indexed by getTranscriptEventIndex() and session save/load,
250
- // while staying a distinct message kind from user/assistant turns.
251
- ctx.session.conversationManager.addSystemMessage(
252
- `[Rewind] Restored checkpoint "${checkpoint.label}" (${plural(result.restoredFiles.length, 'file')} restored, ${plural(result.removedFiles.length, 'file')} removed). ` +
253
- 'Files were rewound — conversation history is unchanged. Use /undo to remove turns separately.',
254
- );
255
- // Auto-close the preview: once the restore has actually happened,
256
- // the diff panel showing the (now-stale) pre-restore preview has
257
- // nothing left to confirm and no obvious way to dismiss it
258
- // otherwise (same pm.close(id) + focusPrompt() pattern as
259
- // /panel close — see operator-panel-runtime.ts).
260
- pm.close('diff');
261
- ctx.focusPrompt?.();
262
- ctx.renderRequest();
263
- } catch (err) {
264
- ctx.print(`Rewind failed: ${summarizeError(err)}`);
265
- }
266
- },
267
- onCancel: () => {
268
- ctx.print('Rewind cancelled — no files changed.');
269
- // Same auto-close as the confirm path above — a denied restore also
270
- // leaves nothing actionable in the preview panel.
271
- pm.close('diff');
272
- ctx.focusPrompt?.();
273
- ctx.renderRequest();
274
- },
275
- });
276
- ctx.print(`Previewing checkpoint ${shortId(checkpoint.id)} "${checkpoint.label}". Confirm in the diff panel: Enter/y to restore, n/Esc to cancel.`);
277
- ctx.renderRequest();
278
- },
279
- });
280
108
  }