@pellux/goodvibes-tui 1.13.1 → 1.15.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.
- package/CHANGELOG.md +104 -0
- package/README.md +6 -6
- package/bin/goodvibes +6 -0
- package/bin/goodvibes-daemon +6 -0
- package/bin/launcher-support.js +34 -1
- package/docs/foundation-artifacts/operator-contract.json +8076 -2047
- package/package.json +15 -11
- package/scripts/postinstall.js +73 -48
- package/src/cli/bundle-command.ts +5 -5
- package/src/cli/completion.ts +1 -1
- package/src/cli/completions/generate.ts +15 -3
- package/src/cli/doctor.ts +385 -0
- package/src/cli/entrypoint.ts +72 -1
- package/src/cli/help.ts +29 -9
- package/src/cli/hooks-command.ts +74 -0
- package/src/cli/hooks-report.ts +167 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/management.ts +13 -1
- package/src/cli/parser.ts +10 -2
- package/src/cli/plugin-bundles-command.ts +228 -0
- package/src/cli/plugin-command.ts +179 -0
- package/src/cli/service-posture.ts +10 -0
- package/src/cli/status.ts +275 -2
- package/src/cli/tui-startup.ts +119 -1
- package/src/cli/types.ts +3 -1
- package/src/config/index.ts +16 -0
- package/src/config/tui-extension-settings.ts +169 -0
- package/src/core/approval-alert.ts +15 -2
- package/src/core/compaction-receipt.ts +127 -0
- package/src/core/model-routing-chip.ts +82 -0
- package/src/core/permission-mode.ts +89 -0
- package/src/core/rewind-receipt.ts +83 -0
- package/src/core/rewind-turn-anchors.ts +160 -0
- package/src/core/scriptable-statusline.ts +141 -0
- package/src/core/session-auto-titler.ts +110 -0
- package/src/core/stream-event-wiring.ts +20 -0
- package/src/core/system-message-router.ts +1 -1
- package/src/core/terminal-notifier.ts +102 -0
- package/src/core/turn-event-wiring.ts +80 -1
- package/src/input/command-reference.ts +96 -0
- package/src/input/command-registry.ts +25 -0
- package/src/input/commands/channel-pairing.ts +245 -0
- package/src/input/commands/channel-runtime.ts +152 -6
- package/src/input/commands/checkin-runtime.ts +89 -0
- package/src/input/commands/checkpoint-runtime.ts +7 -179
- package/src/input/commands/ci-runtime.ts +207 -0
- package/src/input/commands/cost-attribution-format.ts +52 -0
- package/src/input/commands/cost-runtime.ts +44 -4
- package/src/input/commands/editor-runtime.ts +22 -0
- package/src/input/commands/flags-runtime.ts +328 -0
- package/src/input/commands/health-metrics-format.ts +67 -0
- package/src/input/commands/health-runtime.ts +55 -1
- package/src/input/commands/memory.ts +23 -0
- package/src/input/commands/operator-rpc.ts +95 -0
- package/src/input/commands/palette-runtime.ts +22 -0
- package/src/input/commands/permissions-provenance.ts +264 -0
- package/src/input/commands/permissions-runtime.ts +21 -0
- package/src/input/commands/planning-runtime.ts +28 -1
- package/src/input/commands/platform-access-runtime.ts +2 -97
- package/src/input/commands/principals-runtime.ts +212 -0
- package/src/input/commands/product-runtime.ts +43 -3
- package/src/input/commands/recall-files-config.ts +54 -0
- package/src/input/commands/recall-files-git.ts +57 -0
- package/src/input/commands/recall-files.ts +185 -0
- package/src/input/commands/recall-query.ts +8 -3
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/recall-shared.ts +13 -0
- package/src/input/commands/relay-runtime.ts +86 -0
- package/src/input/commands/review-runtime.ts +160 -0
- package/src/input/commands/rewind-runtime.ts +362 -0
- package/src/input/commands/schedule-nl.ts +147 -0
- package/src/input/commands/schedule-runtime.ts +29 -10
- package/src/input/commands/secret-runtime.ts +55 -0
- package/src/input/commands/session-content.ts +19 -8
- package/src/input/commands/session-workflow.ts +10 -0
- package/src/input/commands/update-runtime.ts +412 -0
- package/src/input/commands/workstream-attempt-dependents.ts +103 -0
- package/src/input/commands/workstream-attempts.ts +244 -0
- package/src/input/commands/workstream-runtime.ts +164 -26
- package/src/input/commands/worktree-runtime.ts +56 -2
- package/src/input/commands.ts +135 -80
- package/src/input/composer-capture.ts +67 -0
- package/src/input/composer-editor.ts +134 -0
- package/src/input/concealed-input.ts +94 -0
- package/src/input/exec-env-scrub-config.ts +84 -0
- package/src/input/feature-unit-layout.ts +142 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed-routes.ts +17 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-modal-routes.ts +5 -2
- package/src/input/handler-shortcuts.ts +37 -0
- package/src/input/handler.ts +24 -2
- package/src/input/keybindings.ts +11 -2
- package/src/input/onboarding/onboarding-feature-units.ts +536 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
- package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
- package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
- package/src/input/relay-settings-descriptions.ts +35 -0
- package/src/input/sandbox-exec-config.ts +107 -0
- package/src/input/settings-modal-adjustment.ts +4 -7
- package/src/input/settings-modal-data.ts +178 -8
- package/src/input/settings-modal-mutations.ts +41 -13
- package/src/input/settings-modal-types.ts +42 -5
- package/src/input/settings-modal.ts +67 -15
- package/src/input/submission-router.ts +4 -1
- package/src/input/worktree-setup-config.ts +93 -0
- package/src/main.ts +43 -43
- package/src/panels/builtin/development.ts +14 -0
- package/src/panels/diff-review-model.ts +189 -0
- package/src/panels/diff-review-panel.ts +395 -0
- package/src/panels/fleet-panel-format.ts +114 -2
- package/src/panels/fleet-panel.ts +65 -78
- package/src/panels/fleet-read-model.ts +91 -11
- package/src/panels/fleet-stop.ts +19 -4
- package/src/panels/modals/memory-modal.ts +11 -2
- package/src/permissions/hunk-selection.ts +86 -5
- package/src/permissions/prompt.ts +108 -7
- package/src/permissions/sandbox-exec-gate.ts +162 -0
- package/src/renderer/settings-modal-helpers.ts +21 -1
- package/src/renderer/settings-modal.ts +70 -44
- package/src/renderer/shell-surface.ts +23 -1
- package/src/renderer/system-message.ts +4 -0
- package/src/renderer/tool-result-summary.ts +95 -5
- package/src/renderer/ui-factory.ts +22 -0
- package/src/runtime/bootstrap-command-context.ts +15 -1
- package/src/runtime/bootstrap-command-parts.ts +14 -2
- package/src/runtime/bootstrap-core.ts +38 -38
- package/src/runtime/bootstrap-notifier-sync.ts +41 -0
- package/src/runtime/bootstrap-shell.ts +8 -0
- package/src/runtime/bootstrap.ts +21 -23
- package/src/runtime/context-accounting-source.ts +103 -0
- package/src/runtime/conversation-rewind-port.ts +130 -0
- package/src/runtime/fleet-needs-input-push.ts +70 -0
- package/src/runtime/install-self-check-startup.ts +36 -0
- package/src/runtime/install-self-check.ts +149 -0
- package/src/runtime/onboarding/fast-path.ts +93 -0
- package/src/runtime/process-lifecycle.ts +32 -24
- package/src/runtime/relay-reachability-bridge.ts +53 -0
- package/src/runtime/release-artifacts.ts +109 -0
- package/src/runtime/services.ts +33 -33
- package/src/runtime/trust/workspace-registration.ts +184 -0
- package/src/runtime/trust/workspace-trust.ts +202 -0
- package/src/runtime/ui-services.ts +2 -1
- package/src/runtime/update-check.ts +113 -0
- package/src/runtime/workspace-checkpointing.ts +32 -0
- package/src/runtime/workstream-attempts-validation.ts +77 -0
- package/src/runtime/workstream-draft-edits.ts +95 -0
- package/src/runtime/workstream-draft-store.ts +101 -0
- package/src/runtime/workstream-draft-types.ts +42 -0
- package/src/runtime/workstream-services.ts +70 -39
- package/src/shell/blocking-input.ts +29 -0
- package/src/shell/ui-openers.ts +65 -0
- package/src/verification/verification-ledger.ts +56 -2
- package/src/version.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
- package/src/runtime/gateway-verbs.ts +0 -47
- 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: '
|
|
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
|
|
2
|
+
// checkpoint-runtime.ts — /checkpoints, /checkpoint
|
|
3
3
|
//
|
|
4
4
|
// UX over WorkspaceCheckpointManager (@pellux/goodvibes-sdk/platform/workspace),
|
|
5
|
-
// the whole-workspace, git-backed
|
|
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
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
|
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
|
|
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
|
}
|