@pellux/goodvibes-tui 0.22.0 → 0.24.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 +47 -0
- package/README.md +17 -8
- package/package.json +1 -1
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management-utils.ts +352 -0
- package/src/cli/management.ts +116 -344
- package/src/cli/surface-command.ts +1 -1
- package/src/core/context-auto-compact.ts +43 -10
- package/src/core/conversation-rendering.ts +5 -2
- package/src/core/conversation-types.ts +24 -0
- package/src/core/conversation.ts +7 -12
- package/src/core/long-task-notifier.ts +145 -0
- package/src/core/session-recovery.ts +147 -0
- package/src/core/stream-event-wiring.ts +199 -7
- package/src/core/transcript-journal.ts +339 -0
- package/src/core/turn-event-wiring.ts +67 -4
- package/src/input/commands/channel-runtime.ts +139 -0
- package/src/input/commands/control-room-runtime.ts +0 -2
- package/src/input/commands/diff-runtime.ts +1 -1
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +23 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-runtime.ts +1 -2
- package/src/input/commands/memory-product-runtime.ts +2 -2
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/onboarding-runtime.ts +0 -1
- package/src/input/commands/policy.ts +1 -1
- package/src/input/commands/profile-sync-runtime.ts +4 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +0 -1
- package/src/input/commands/runtime-services.ts +30 -1
- package/src/input/commands/session-content.ts +2 -2
- package/src/input/commands/session-workflow.ts +32 -2
- package/src/input/commands/session.ts +1 -1
- package/src/input/commands/settings-sync-runtime.ts +9 -9
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +56 -6
- package/src/input/commands/work-plan-runtime.ts +8 -8
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +6 -0
- package/src/input/handler-feed-routes.ts +19 -1
- package/src/input/handler-feed.ts +11 -0
- package/src/input/handler-prompt-buffer.ts +28 -0
- package/src/input/handler-shortcuts.ts +88 -2
- package/src/input/handler-ui-state.ts +2 -2
- package/src/input/handler.ts +39 -3
- package/src/input/keybindings.ts +33 -3
- package/src/input/kill-ring.ts +134 -0
- package/src/input/model-picker.ts +18 -1
- package/src/input/search.ts +18 -6
- package/src/input/settings-modal-activation.ts +134 -0
- package/src/input/settings-modal-adjustment.ts +124 -0
- package/src/input/settings-modal-data.ts +53 -0
- package/src/input/settings-modal.ts +48 -145
- package/src/main.ts +50 -50
- package/src/panels/base-panel.ts +2 -1
- package/src/panels/provider-health-domains.ts +3 -3
- package/src/panels/provider-health-panel.ts +13 -9
- package/src/panels/provider-health-tracker.ts +7 -4
- package/src/panels/settings-sync-panel.ts +3 -3
- package/src/panels/work-plan-panel.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +55 -0
- package/src/renderer/compaction-preview.ts +146 -0
- package/src/renderer/diff-view.ts +2 -2
- package/src/renderer/help-overlay.ts +1 -0
- package/src/renderer/model-picker-overlay.ts +23 -11
- package/src/renderer/progress.ts +3 -3
- package/src/renderer/search-overlay.ts +8 -5
- package/src/renderer/settings-modal-helpers.ts +2 -2
- package/src/renderer/settings-modal.ts +1 -1
- package/src/renderer/ui-factory.ts +11 -0
- package/src/runtime/bootstrap-core.ts +92 -0
- package/src/runtime/bootstrap-hook-bridge.ts +18 -0
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/shell/blocking-input.ts +32 -0
- package/src/shell/recovery-input-helpers.ts +71 -0
- package/src/utils/browser.ts +29 -0
- package/src/utils/terminal-width.ts +10 -3
- package/src/version.ts +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ServiceRegistry } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
2
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
3
|
+
import { probeTermCaps } from '../../renderer/term-caps.ts';
|
|
3
4
|
import { evaluateSessionMaintenance, formatSessionMaintenanceLines } from '@/runtime/index.ts';
|
|
4
5
|
import { estimateConversationTokens } from '@pellux/goodvibes-sdk/platform/core';
|
|
5
6
|
import type { CommandRegistry } from '../command-registry.ts';
|
|
@@ -39,7 +40,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
|
|
|
39
40
|
name: 'health',
|
|
40
41
|
aliases: ['doctor'],
|
|
41
42
|
description: 'Health workspace for startup posture, service readiness, sandbox posture, and provider health',
|
|
42
|
-
usage: '[open|review|setup|services|sandbox|provider|accounts|auth|settings|intelligence|remote|mcp|continuity|worktrees|maintenance|repair [domain]]',
|
|
43
|
+
usage: '[open|review|setup|services|sandbox|provider|accounts|auth|settings|intelligence|remote|mcp|continuity|worktrees|maintenance|term|repair [domain]]',
|
|
43
44
|
async handler(args, ctx) {
|
|
44
45
|
const sub = (args[0] ?? 'review').toLowerCase();
|
|
45
46
|
const readModels = requireReadModels(ctx);
|
|
@@ -127,8 +128,8 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
|
|
|
127
128
|
` recent failures: ${settings.recentFailureCount}`,
|
|
128
129
|
` staged bundle: ${settings.hasStagedManagedBundle ? 'present' : 'none'}`,
|
|
129
130
|
...(issues.length > 0 ? issues.map((issue) => ` issue: ${issue}`) : [' no active settings-control issues detected']),
|
|
130
|
-
' next: /
|
|
131
|
-
' next: /
|
|
131
|
+
' next: /settings-sync panel',
|
|
132
|
+
' next: /settings-sync show <key>',
|
|
132
133
|
' next: /managed staged',
|
|
133
134
|
].join('\n'));
|
|
134
135
|
return;
|
|
@@ -282,6 +283,23 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
|
|
|
282
283
|
return;
|
|
283
284
|
}
|
|
284
285
|
|
|
286
|
+
if (sub === 'term') {
|
|
287
|
+
const caps = probeTermCaps(process.stdout as NodeJS.WriteStream);
|
|
288
|
+
const issues: string[] = [];
|
|
289
|
+
if (caps.capability === 'none') issues.push('terminal reports no color support — UI rendering will be degraded (no ANSI colors)');
|
|
290
|
+
if (caps.capability === 'basic16') issues.push('terminal limited to 16 ANSI colors — gradient and true-color UI elements will be approximated');
|
|
291
|
+
if (!caps.syncedOutput) issues.push('DEC Synchronized Output (mode 2026) is disabled — screen-tearing may be visible on slow connections');
|
|
292
|
+
ctx.print([
|
|
293
|
+
'Health Review: Terminal Capabilities',
|
|
294
|
+
` color capability: ${caps.capability}`,
|
|
295
|
+
` synced output (mode 2026): ${caps.syncedOutput ? 'enabled' : 'disabled'}`,
|
|
296
|
+
` NO_COLOR env: ${process.env['NO_COLOR'] !== undefined && process.env['NO_COLOR'] !== '' ? 'set (forces none)' : 'unset'}`,
|
|
297
|
+
` TERM env: ${process.env['TERM'] ?? '(unset)'}`,
|
|
298
|
+
...(issues.length > 0 ? issues.map((issue) => ` issue: ${issue}`) : [' no terminal capability issues detected']),
|
|
299
|
+
].join('\n'));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
285
303
|
if (sub === 'repair') {
|
|
286
304
|
const domain = (args[1] ?? 'review').toLowerCase();
|
|
287
305
|
const lines = ['Health Repair'];
|
|
@@ -290,7 +308,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
|
|
|
290
308
|
lines.push(' domain: settings');
|
|
291
309
|
lines.push(...(
|
|
292
310
|
settings.conflicts.length > 0
|
|
293
|
-
? [' /
|
|
311
|
+
? [' /settings-sync panel', ' /settings-sync show <key>', ' /managed staged']
|
|
294
312
|
: [' no active settings repair actions suggested']
|
|
295
313
|
));
|
|
296
314
|
lines.push(' verify: /health settings');
|
|
@@ -426,6 +444,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
|
|
|
426
444
|
' /health remote',
|
|
427
445
|
' /health maintenance',
|
|
428
446
|
' /health worktrees',
|
|
447
|
+
' /health term',
|
|
429
448
|
' /health repair <domain>',
|
|
430
449
|
' /setup onboarding',
|
|
431
450
|
].join('\n'));
|
|
@@ -132,7 +132,7 @@ function renderKnowledgeAskResult(result: KnowledgeAskResult): string {
|
|
|
132
132
|
export const knowledgeCommand: SlashCommand = {
|
|
133
133
|
name: 'knowledge',
|
|
134
134
|
aliases: ['know'],
|
|
135
|
-
description: 'Structured knowledge graph: ingest URLs/bookmarks, inspect issues, and build compact prompt packets
|
|
135
|
+
description: 'Structured knowledge graph: ingest URLs/bookmarks, inspect issues, and build compact prompt packets',
|
|
136
136
|
usage: '<subcommand> [args]',
|
|
137
137
|
argsHint: 'status|ask|ingest-url|import-bookmarks|import-urls|list|search|get|queue|review-issue|candidates|reports|schedules|lint|packet|explain|reindex|consolidate',
|
|
138
138
|
handler: async (args: string[], context: CommandContext): Promise<void> => {
|
|
@@ -53,7 +53,6 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
|
|
|
53
53
|
name: 'incident-review',
|
|
54
54
|
aliases: [],
|
|
55
55
|
description: 'Alias for /incident open',
|
|
56
|
-
usage: '',
|
|
57
56
|
handler(_args, ctx) {
|
|
58
57
|
if (ctx.openIncidentPanel) {
|
|
59
58
|
ctx.openIncidentPanel();
|
|
@@ -249,7 +248,7 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
|
|
|
249
248
|
aliases: ['img'],
|
|
250
249
|
description: 'Attach an image file to the next message',
|
|
251
250
|
usage: '<path> [prompt text]',
|
|
252
|
-
argsHint: '<path> [prompt]',
|
|
251
|
+
argsHint: '<path> [prompt text]',
|
|
253
252
|
async handler(args, ctx) {
|
|
254
253
|
if (args.length === 0) {
|
|
255
254
|
ctx.print('Usage: /image <path> [prompt text]\nSupported formats: PNG, JPEG, WebP, GIF');
|
|
@@ -55,7 +55,7 @@ export function registerMemoryProductRuntimeCommands(registry: CommandRegistry):
|
|
|
55
55
|
|
|
56
56
|
registry.register({
|
|
57
57
|
name: 'session-memory',
|
|
58
|
-
description: 'Dedicated front-door for session-scoped memory capture and review. All subcommands are filtered to scope=session
|
|
58
|
+
description: 'Dedicated front-door for session-scoped memory capture and review. All subcommands are filtered to scope=session',
|
|
59
59
|
usage: '[queue [limit] | export <path> | add <class> <summary...>]',
|
|
60
60
|
async handler(args, ctx) {
|
|
61
61
|
const sub = (args[0] ?? 'queue').toLowerCase();
|
|
@@ -82,7 +82,7 @@ export function registerMemoryProductRuntimeCommands(registry: CommandRegistry):
|
|
|
82
82
|
|
|
83
83
|
registry.register({
|
|
84
84
|
name: 'team-memory',
|
|
85
|
-
description: 'Dedicated front-door for team/shared memory review and exchange. The queue and export subcommands are filtered to scope=team
|
|
85
|
+
description: 'Dedicated front-door for team/shared memory review and exchange. The queue and export subcommands are filtered to scope=team',
|
|
86
86
|
usage: '[queue [limit] | export <path> | import <path> | capture policy]',
|
|
87
87
|
async handler(args, ctx) {
|
|
88
88
|
const sub = (args[0] ?? 'queue').toLowerCase();
|
|
@@ -25,7 +25,7 @@ import { VALID_CLASSES, VALID_REVIEW_STATES, VALID_SCOPES } from './recall-share
|
|
|
25
25
|
export const recallCommand: SlashCommand = {
|
|
26
26
|
name: 'recall',
|
|
27
27
|
aliases: ['rc'],
|
|
28
|
-
description: 'Project memory: add decisions, constraints, incidents, and patterns with provenance
|
|
28
|
+
description: 'Project memory: add decisions, constraints, incidents, and patterns with provenance',
|
|
29
29
|
usage: '<subcommand> [args]',
|
|
30
30
|
argsHint: 'add|search|link|get|list|remove',
|
|
31
31
|
handler: async (args: string[], context: CommandContext): Promise<void> => {
|
|
@@ -5,7 +5,6 @@ export function registerOnboardingRuntimeCommands(registry: CommandRegistry): vo
|
|
|
5
5
|
registry.register({
|
|
6
6
|
name: 'onboarding',
|
|
7
7
|
description: 'Open the onboarding wizard with current settings preloaded for review and editing',
|
|
8
|
-
usage: '',
|
|
9
8
|
handler(_args, ctx) {
|
|
10
9
|
openOnboardingWizard(ctx, { mode: 'edit', reset: true });
|
|
11
10
|
ctx.print('Opening onboarding wizard.');
|
|
@@ -4,7 +4,7 @@ import { dispatchPolicyCommand } from './policy-dispatch.ts';
|
|
|
4
4
|
export const policyCommand: SlashCommand = {
|
|
5
5
|
name: 'policy',
|
|
6
6
|
aliases: ['pol'],
|
|
7
|
-
description: 'Open the policy panel or manage versioned policy bundles (load, simulate, diff, promote, rollback)
|
|
7
|
+
description: 'Open the policy panel or manage versioned policy bundles (load, simulate, diff, promote, rollback)',
|
|
8
8
|
usage: '<subcommand> [args]',
|
|
9
9
|
argsHint: 'load|simulate|diff|lint|preflight|promote|rollback|status',
|
|
10
10
|
handler: async (args: string[], context: CommandContext): Promise<void> => {
|
|
@@ -16,7 +16,8 @@ function inspectProfileSyncBundle(bundle: ProfileSyncBundle): string {
|
|
|
16
16
|
|
|
17
17
|
export function registerProfileSyncRuntimeCommands(registry: CommandRegistry): void {
|
|
18
18
|
registry.register({
|
|
19
|
-
name: '
|
|
19
|
+
name: 'profile-sync',
|
|
20
|
+
aliases: ['profilesync'],
|
|
20
21
|
description: 'Export, import, and inspect profile sync bundles',
|
|
21
22
|
usage: '[list|export <path>|inspect <path>|import <path> [prefix]]',
|
|
22
23
|
handler(args, ctx) {
|
|
@@ -36,7 +37,7 @@ export function registerProfileSyncRuntimeCommands(registry: CommandRegistry): v
|
|
|
36
37
|
|
|
37
38
|
const pathArg = args[1];
|
|
38
39
|
if (!pathArg) {
|
|
39
|
-
ctx.print(`Usage: /
|
|
40
|
+
ctx.print(`Usage: /profile-sync ${sub} <path>${sub === 'import' ? ' [prefix]' : ''}`);
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
43
|
const targetPath = shellPaths.resolveWorkspacePath(pathArg);
|
|
@@ -93,7 +94,7 @@ export function registerProfileSyncRuntimeCommands(registry: CommandRegistry): v
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
recordSettingsSyncFailure('profiles', `unsupported subcommand: ${sub}`, controlPlaneConfigDir);
|
|
96
|
-
ctx.print('Usage: /
|
|
97
|
+
ctx.print('Usage: /profile-sync [list|export <path>|inspect <path>|import <path> [prefix]]');
|
|
97
98
|
},
|
|
98
99
|
});
|
|
99
100
|
}
|
|
@@ -365,7 +365,7 @@ function handleFallbackTest(
|
|
|
365
365
|
export const providerCommand: SlashCommand = {
|
|
366
366
|
name: 'provider-opt',
|
|
367
367
|
aliases: ['prov-opt'],
|
|
368
|
-
description: 'Manage provider routing optimizer (route, pin, explain, fallback)
|
|
368
|
+
description: 'Manage provider routing optimizer (route, pin, explain, fallback)',
|
|
369
369
|
usage: '<subcommand> [args]',
|
|
370
370
|
argsHint: 'optimizer|route|explain-route|pin|fallback',
|
|
371
371
|
handler: async (args: string[], context: CommandContext): Promise<void> => {
|
|
@@ -12,7 +12,6 @@ export function registerQrcodeRuntimeCommands(registry: CommandRegistry): void {
|
|
|
12
12
|
name: 'qrcode',
|
|
13
13
|
aliases: ['qr', 'pair'],
|
|
14
14
|
description: 'Open the QR code panel for companion app pairing',
|
|
15
|
-
usage: '',
|
|
16
15
|
handler(_args, ctx) {
|
|
17
16
|
openCommandPanel(ctx, 'qr-code');
|
|
18
17
|
},
|
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
CommandSessionServices,
|
|
8
8
|
CommandWorkspaceServices,
|
|
9
9
|
} from '../command-registry.ts';
|
|
10
|
+
import { getLastCompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
|
|
11
|
+
import type { CompactionContext, CompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
|
|
10
12
|
import type { UiReadModels } from '../../runtime/ui-read-models.ts';
|
|
11
13
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
12
14
|
import type { EcosystemCatalogPathOptions } from '@/runtime/index.ts';
|
|
@@ -236,13 +238,40 @@ export function requireProviderApi(context: CommandContext): ProviderApi {
|
|
|
236
238
|
return requireContextValue(context.clients?.providerApi, 'clients.providerApi');
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Compact the conversation and return the CompactionEvent recorded by the SDK,
|
|
243
|
+
* or null if no event was recorded (e.g. compaction was skipped or produced no
|
|
244
|
+
* change).
|
|
245
|
+
*/
|
|
246
|
+
export async function compactConversation(context: CommandContext): Promise<CompactionEvent | null> {
|
|
247
|
+
const eventBefore = getLastCompactionEvent();
|
|
248
|
+
const sessionMemories = context.session.sessionMemoryStore?.list() ?? [];
|
|
249
|
+
const compactionCtx: CompactionContext = {
|
|
250
|
+
messages: context.session.conversationManager.getMessagesForLLM(),
|
|
251
|
+
sessionMemories,
|
|
252
|
+
agents: [],
|
|
253
|
+
wrfcChains: [],
|
|
254
|
+
activePlan: null,
|
|
255
|
+
lineageEntries: [],
|
|
256
|
+
compactionCount: 0,
|
|
257
|
+
contextWindow: 0,
|
|
258
|
+
trigger: 'manual',
|
|
259
|
+
extractionModelId: context.session.runtime.model,
|
|
260
|
+
extractionProvider: context.session.runtime.provider,
|
|
261
|
+
};
|
|
240
262
|
await context.session.conversationManager.compact(
|
|
241
263
|
context.provider.providerRegistry,
|
|
242
264
|
context.session.runtime.model,
|
|
243
265
|
'manual',
|
|
244
266
|
context.session.runtime.provider,
|
|
267
|
+
compactionCtx,
|
|
245
268
|
);
|
|
269
|
+
const eventAfter = getLastCompactionEvent();
|
|
270
|
+
// Return the new event only if it differs from the one recorded before the call.
|
|
271
|
+
if (eventAfter !== null && eventAfter !== eventBefore) {
|
|
272
|
+
return eventAfter;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
246
275
|
}
|
|
247
276
|
|
|
248
277
|
export function requireKnowledgeApi(context: CommandContext): KnowledgeApi {
|
|
@@ -162,7 +162,7 @@ export function registerSessionContentCommands(registry: CommandRegistry): void
|
|
|
162
162
|
registry.register({
|
|
163
163
|
name: 'undo',
|
|
164
164
|
aliases: [],
|
|
165
|
-
description: 'Undo last action. /undo file — revert last file write/edit. /undo — remove last conversation turn
|
|
165
|
+
description: 'Undo last action. /undo file — revert last file write/edit. /undo — remove last conversation turn',
|
|
166
166
|
usage: '[file]',
|
|
167
167
|
argsHint: '[file]',
|
|
168
168
|
handler(args, ctx) {
|
|
@@ -191,7 +191,7 @@ export function registerSessionContentCommands(registry: CommandRegistry): void
|
|
|
191
191
|
|
|
192
192
|
registry.register({
|
|
193
193
|
name: 'redo',
|
|
194
|
-
description: 'Redo last undone action. /redo file — re-apply last reverted file. /redo — restore conversation turn
|
|
194
|
+
description: 'Redo last undone action. /redo file — re-apply last reverted file. /redo — restore conversation turn',
|
|
195
195
|
usage: '[file]',
|
|
196
196
|
argsHint: '[file]',
|
|
197
197
|
handler(args, ctx) {
|
|
@@ -6,7 +6,8 @@ import type { TranscriptEventKind } from '@pellux/goodvibes-sdk/platform/core';
|
|
|
6
6
|
import type { ConversationTitleSource } from '../../core/conversation';
|
|
7
7
|
import type { SessionReturnContextSummary } from '@/runtime/index.ts';
|
|
8
8
|
import { formatReturnContextForDisplay, getReturnContextMode, maybeAssistReturnContextSummary } from '@/runtime/index.ts';
|
|
9
|
-
import { requirePanelManager, requireProviderApi, requireSessionManager } from './runtime-services.ts';
|
|
9
|
+
import { requirePanelManager, requireProviderApi, requireSessionManager, requireShellPaths } from './runtime-services.ts';
|
|
10
|
+
import { replayJournalForSession } from '../../core/session-recovery.ts';
|
|
10
11
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
11
12
|
|
|
12
13
|
function parseTranscriptKind(raw: string | undefined): TranscriptEventKind | 'all' {
|
|
@@ -240,6 +241,26 @@ export async function handleSessionWorkflowCommand(args: string[], ctx: CommandC
|
|
|
240
241
|
ctx.session.conversationManager.fromJSON({ messages: messages as never[], title: meta.title, titleSource: meta.titleSource });
|
|
241
242
|
ctx.session.conversationManager.rebuildHistory();
|
|
242
243
|
ctx.session.runtime.sessionId = found.name;
|
|
244
|
+
|
|
245
|
+
// Journal replay: recover turns that post-date the loaded snapshot.
|
|
246
|
+
const shellPaths = requireShellPaths(ctx);
|
|
247
|
+
const journalReplay = replayJournalForSession({
|
|
248
|
+
homeDirectory: shellPaths.homeDirectory,
|
|
249
|
+
snapshotTimestamp: meta.timestamp,
|
|
250
|
+
conversation: ctx.session.conversationManager,
|
|
251
|
+
sessionId: found.name,
|
|
252
|
+
persistSnapshot: (replayedMessages) => {
|
|
253
|
+
sm.save(found.name, replayedMessages as never[], {
|
|
254
|
+
title: ctx.session.conversationManager.title || meta.title,
|
|
255
|
+
model: meta.model,
|
|
256
|
+
provider: meta.provider,
|
|
257
|
+
timestamp: Date.now(),
|
|
258
|
+
titleSource: meta.titleSource,
|
|
259
|
+
returnContext: meta.returnContext,
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
243
264
|
if (meta.model) {
|
|
244
265
|
try {
|
|
245
266
|
const selected = await providerApi.selectModel(meta.model);
|
|
@@ -252,7 +273,16 @@ export async function handleSessionWorkflowCommand(args: string[], ctx: CommandC
|
|
|
252
273
|
}
|
|
253
274
|
if (meta.provider) ctx.session.runtime.provider = meta.provider;
|
|
254
275
|
ctx.renderRequest();
|
|
255
|
-
|
|
276
|
+
const resumedMsgCount = ctx.session.conversationManager.getMessageCount();
|
|
277
|
+
ctx.print(`Resumed session: ${found.name}\n Name: ${meta.title || '(untitled)'}\n Messages: ${resumedMsgCount}\n Model: ${meta.model || ctx.session.runtime.model}`);
|
|
278
|
+
if (journalReplay.replayed > 0) {
|
|
279
|
+
ctx.print(` [Recovery] Replayed ${journalReplay.replayed} journal record(s) — restored turns since last snapshot.`);
|
|
280
|
+
}
|
|
281
|
+
if (journalReplay.hadCorruptTail && journalReplay.replayed === 0) {
|
|
282
|
+
ctx.print(' [Recovery] Journal tail was corrupt or unrecognised (quarantined). Proceeding with snapshot only.');
|
|
283
|
+
} else if (journalReplay.hadCorruptTail) {
|
|
284
|
+
ctx.print(' [Recovery] Journal tail was partially corrupt (quarantined). Replay stopped at last good record.');
|
|
285
|
+
}
|
|
256
286
|
const reopenedPanels = reopenPanelsFromReturnContext(ctx, meta.returnContext);
|
|
257
287
|
const returnContextMode = getReturnContextMode(ctx.platform.configManager);
|
|
258
288
|
if (returnContextMode !== 'off' && meta.returnContext) {
|
|
@@ -339,7 +339,7 @@ function handleCancel(args: string[], context: CommandContext): void {
|
|
|
339
339
|
export const sessionCommand: SlashCommand = {
|
|
340
340
|
name: 'session',
|
|
341
341
|
aliases: ['sess'],
|
|
342
|
-
description: 'Session lifecycle and orchestration: list, resume, fork, save, export, link-task, handoff, graph, cancel
|
|
342
|
+
description: 'Session lifecycle and orchestration: list, resume, fork, save, export, link-task, handoff, graph, cancel',
|
|
343
343
|
usage: '<subcommand> [args]',
|
|
344
344
|
argsHint: 'list|rename|resume|fork|save|info|export|search|delete|events|groups|hotspots|link-task|handoff|graph|cancel',
|
|
345
345
|
handler: async (args: string[], context: CommandContext): Promise<void> => {
|
|
@@ -24,8 +24,8 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
24
24
|
|
|
25
25
|
export function registerSettingsSyncRuntimeCommands(registry: CommandRegistry): void {
|
|
26
26
|
registry.register({
|
|
27
|
-
name: '
|
|
28
|
-
aliases: ['
|
|
27
|
+
name: 'settings-sync',
|
|
28
|
+
aliases: ['settingssync'],
|
|
29
29
|
description: 'Review sync posture, export/import settings-sync bundles, and open the settings sync workspace',
|
|
30
30
|
usage: '[review|panel|show <key>|staged|conflicts|resolve <key> <local|synced>|failures|rollback-history|export <path>|inspect <path>|pull <path>|push <path>|lock <key> <source> <reason...>|unlock <key>]',
|
|
31
31
|
handler(args, ctx) {
|
|
@@ -39,7 +39,7 @@ export function registerSettingsSyncRuntimeCommands(registry: CommandRegistry):
|
|
|
39
39
|
if (sub === 'show') {
|
|
40
40
|
const key = args[1] as ConfigKey | undefined;
|
|
41
41
|
if (!key || !CONFIG_KEYS.has(key)) {
|
|
42
|
-
ctx.print('Usage: /
|
|
42
|
+
ctx.print('Usage: /settings-sync show <config-key>');
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
ctx.print(formatResolvedSettingReview(ctx.platform.configManager, key));
|
|
@@ -63,7 +63,7 @@ export function registerSettingsSyncRuntimeCommands(registry: CommandRegistry):
|
|
|
63
63
|
const key = args[1] as ConfigKey | undefined;
|
|
64
64
|
const resolution = (args[2] ?? '').toLowerCase();
|
|
65
65
|
if (!key || !CONFIG_KEYS.has(key) || (resolution !== 'local' && resolution !== 'synced')) {
|
|
66
|
-
ctx.print('Usage: /
|
|
66
|
+
ctx.print('Usage: /settings-sync resolve <config-key> <local|synced>');
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
const changed = resolveSettingsSyncConflict(ctx.platform.configManager, key, resolution);
|
|
@@ -102,7 +102,7 @@ export function registerSettingsSyncRuntimeCommands(registry: CommandRegistry):
|
|
|
102
102
|
if (sub === 'export' || sub === 'push') {
|
|
103
103
|
const pathArg = args[1];
|
|
104
104
|
if (!pathArg) {
|
|
105
|
-
ctx.print(`Usage: /
|
|
105
|
+
ctx.print(`Usage: /settings-sync ${sub} <path>`);
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
const targetPath = shellPaths.resolveWorkspacePath(pathArg);
|
|
@@ -122,7 +122,7 @@ export function registerSettingsSyncRuntimeCommands(registry: CommandRegistry):
|
|
|
122
122
|
if (sub === 'inspect') {
|
|
123
123
|
const pathArg = args[1];
|
|
124
124
|
if (!pathArg) {
|
|
125
|
-
ctx.print('Usage: /
|
|
125
|
+
ctx.print('Usage: /settings-sync inspect <path>');
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
const sourcePath = shellPaths.resolveWorkspacePath(pathArg);
|
|
@@ -133,7 +133,7 @@ export function registerSettingsSyncRuntimeCommands(registry: CommandRegistry):
|
|
|
133
133
|
if (sub === 'pull') {
|
|
134
134
|
const pathArg = args[1];
|
|
135
135
|
if (!pathArg) {
|
|
136
|
-
ctx.print('Usage: /
|
|
136
|
+
ctx.print('Usage: /settings-sync pull <path>');
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
139
|
const sourcePath = shellPaths.resolveWorkspacePath(pathArg);
|
|
@@ -152,7 +152,7 @@ export function registerSettingsSyncRuntimeCommands(registry: CommandRegistry):
|
|
|
152
152
|
const source = args[2];
|
|
153
153
|
const reason = args.slice(3).join(' ').trim();
|
|
154
154
|
if (!key || !source || !reason || !CONFIG_KEYS.has(key)) {
|
|
155
|
-
ctx.print('Usage: /
|
|
155
|
+
ctx.print('Usage: /settings-sync lock <config-key> <source> <reason...>');
|
|
156
156
|
return;
|
|
157
157
|
}
|
|
158
158
|
setManagedSettingLock(key, source, reason, controlPlaneConfigDir);
|
|
@@ -162,7 +162,7 @@ export function registerSettingsSyncRuntimeCommands(registry: CommandRegistry):
|
|
|
162
162
|
if (sub === 'unlock') {
|
|
163
163
|
const key = args[1] as ConfigKey | undefined;
|
|
164
164
|
if (!key || !CONFIG_KEYS.has(key)) {
|
|
165
|
-
ctx.print('Usage: /
|
|
165
|
+
ctx.print('Usage: /settings-sync unlock <config-key>');
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
168
168
|
ctx.print(clearManagedSettingLock(key, controlPlaneConfigDir) ? `Managed lock cleared for ${key}.` : `No managed lock found for ${key}.`);
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
resolveGithubToken,
|
|
18
18
|
} from '../../export/gist-uploader.ts';
|
|
19
19
|
import { copyToClipboard } from '../../utils/clipboard.ts';
|
|
20
|
-
import { openBrowser } from '../../
|
|
20
|
+
import { openBrowser } from '../../utils/browser.ts';
|
|
21
21
|
|
|
22
22
|
export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
23
23
|
registry.register({
|
|
@@ -3,7 +3,9 @@ import type { SelectionItem } from '../selection-modal.ts';
|
|
|
3
3
|
import { EFFORT_DESCRIPTIONS } from '@pellux/goodvibes-sdk/platform/providers';
|
|
4
4
|
import { REASONING_BUDGET_MAP } from '@pellux/goodvibes-sdk/platform/providers';
|
|
5
5
|
import { executeWriteQuit } from './quit-shared.ts';
|
|
6
|
-
import { compactConversation, requireKeybindingsManager, requireProviderApi } from './runtime-services.ts';
|
|
6
|
+
import { compactConversation, requireKeybindingsManager, requireProviderApi, requireSessionMemoryStore } from './runtime-services.ts';
|
|
7
|
+
import { buildCompactionPreview, buildCompactionAfterNotice, buildPinUsageText, buildPinSuccessText } from '../../renderer/compaction-preview.ts';
|
|
8
|
+
import { buildCompactionHistoryText } from '../../renderer/compaction-history-modal.ts';
|
|
7
9
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
8
10
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
9
11
|
|
|
@@ -13,7 +15,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
13
15
|
aliases: ['m'],
|
|
14
16
|
description: 'Select or display the current LLM model',
|
|
15
17
|
usage: '[model-id]',
|
|
16
|
-
argsHint: '[
|
|
18
|
+
argsHint: '[model-id]',
|
|
17
19
|
async handler(args, ctx) {
|
|
18
20
|
const providerApi = requireProviderApi(ctx);
|
|
19
21
|
if (args.length === 0) {
|
|
@@ -204,9 +206,57 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
204
206
|
aliases: [],
|
|
205
207
|
description: 'Summarize conversation to free context window',
|
|
206
208
|
async handler(_args, ctx) {
|
|
207
|
-
ctx.
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
const messages = ctx.session.conversationManager.getMessagesForLLM();
|
|
210
|
+
// contextWindow is not on CommandContext; preview shows message/token counts
|
|
211
|
+
// without the capacity-% clause (still honest; no fabricated value).
|
|
212
|
+
const contextWindow = 0;
|
|
213
|
+
const memStore = ctx.session.sessionMemoryStore;
|
|
214
|
+
const pinnedMemoryCount = memStore ? memStore.list().length : 0;
|
|
215
|
+
// Pre-compact preview: honest estimate, clearly labelled.
|
|
216
|
+
const preview = buildCompactionPreview({ messages, contextWindow, pinnedMemoryCount, trigger: 'manual' });
|
|
217
|
+
ctx.print(preview);
|
|
218
|
+
const event = await compactConversation(ctx);
|
|
219
|
+
if (event) {
|
|
220
|
+
// Post-compact notice: uses real CompactionEvent figures.
|
|
221
|
+
ctx.print(buildCompactionAfterNotice({ event, pinnedMemoryCount }));
|
|
222
|
+
} else {
|
|
223
|
+
ctx.print('[Context] Compact complete.');
|
|
224
|
+
}
|
|
225
|
+
ctx.renderRequest();
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
registry.register({
|
|
230
|
+
name: 'compact-history',
|
|
231
|
+
aliases: ['compaction-history'],
|
|
232
|
+
description: 'Show compaction history for this session',
|
|
233
|
+
handler(_args, ctx) {
|
|
234
|
+
ctx.print(buildCompactionHistoryText());
|
|
235
|
+
ctx.renderRequest();
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
registry.register({
|
|
240
|
+
name: 'keep',
|
|
241
|
+
aliases: [],
|
|
242
|
+
description: 'Pin text to session memory (survives compaction)',
|
|
243
|
+
usage: '<text>',
|
|
244
|
+
argsHint: '<text to preserve>',
|
|
245
|
+
handler(args, ctx) {
|
|
246
|
+
const text = args.join(' ').trim();
|
|
247
|
+
if (!text) {
|
|
248
|
+
ctx.print(buildPinUsageText());
|
|
249
|
+
ctx.renderRequest();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const memStore = requireSessionMemoryStore(ctx);
|
|
253
|
+
const id = memStore.add(text);
|
|
254
|
+
if (!id) {
|
|
255
|
+
ctx.print('[Pin] Nothing pinned — text was blank.');
|
|
256
|
+
} else {
|
|
257
|
+
const count = memStore.list().length;
|
|
258
|
+
ctx.print(buildPinSuccessText(id, text, count));
|
|
259
|
+
}
|
|
210
260
|
ctx.renderRequest();
|
|
211
261
|
},
|
|
212
262
|
});
|
|
@@ -244,7 +294,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
244
294
|
aliases: ['e'],
|
|
245
295
|
description: 'Show or set reasoning effort level',
|
|
246
296
|
usage: '[level]',
|
|
247
|
-
argsHint: '
|
|
297
|
+
argsHint: '[instant|low|medium|high]',
|
|
248
298
|
async handler(args, ctx) {
|
|
249
299
|
const currentModel = await requireProviderApi(ctx).getCurrentModel();
|
|
250
300
|
const validLevels = currentModel.reasoningEffort ?? [];
|
|
@@ -37,7 +37,7 @@ function openPanel(ctx: import('../command-registry.ts').CommandContext): void {
|
|
|
37
37
|
|
|
38
38
|
function formatList(store: WorkPlanStore): string {
|
|
39
39
|
const items = store.listItems();
|
|
40
|
-
if (items.length === 0) return 'Work plan is empty. Add one with /
|
|
40
|
+
if (items.length === 0) return 'Work plan is empty. Add one with /work-plan add <title>.';
|
|
41
41
|
return [
|
|
42
42
|
`Work Plan (${items.length})`,
|
|
43
43
|
...items.map((item) => {
|
|
@@ -78,8 +78,8 @@ function parseAddArgs(args: string[]): { title: string; owner?: string; source?:
|
|
|
78
78
|
|
|
79
79
|
export function registerWorkPlanRuntimeCommands(registry: CommandRegistry): void {
|
|
80
80
|
registry.register({
|
|
81
|
-
name: '
|
|
82
|
-
aliases: ['wp', 'todo'],
|
|
81
|
+
name: 'work-plan',
|
|
82
|
+
aliases: ['wp', 'todo', 'workplan'],
|
|
83
83
|
description: 'Track a persistent workspace-scoped work plan',
|
|
84
84
|
usage: '[panel|list|show|add <title> [--owner name] [--source label] [--notes text]|done <id>|start <id>|block <id>|fail <id>|cancel <id>|pending <id>|remove <id>|clear-done]',
|
|
85
85
|
argsHint: '[panel|add|list|done]',
|
|
@@ -107,7 +107,7 @@ export function registerWorkPlanRuntimeCommands(registry: CommandRegistry): void
|
|
|
107
107
|
if (subcommand === 'add') {
|
|
108
108
|
const parsed = parseAddArgs(args.slice(1));
|
|
109
109
|
if (!parsed.title) {
|
|
110
|
-
ctx.print('Usage: /
|
|
110
|
+
ctx.print('Usage: /work-plan add <title> [--owner name] [--source label] [--notes text]');
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
const addOptions = {
|
|
@@ -123,7 +123,7 @@ export function registerWorkPlanRuntimeCommands(registry: CommandRegistry): void
|
|
|
123
123
|
if (subcommand === 'remove' || subcommand === 'delete' || subcommand === 'rm') {
|
|
124
124
|
const id = args[1];
|
|
125
125
|
if (!id) {
|
|
126
|
-
ctx.print(`Usage: /
|
|
126
|
+
ctx.print(`Usage: /work-plan ${subcommand} <id>`);
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
129
|
const item = store.removeItem(id);
|
|
@@ -138,7 +138,7 @@ export function registerWorkPlanRuntimeCommands(registry: CommandRegistry): void
|
|
|
138
138
|
if (subcommand === 'cycle' || subcommand === 'toggle') {
|
|
139
139
|
const id = args[1];
|
|
140
140
|
if (!id) {
|
|
141
|
-
ctx.print(`Usage: /
|
|
141
|
+
ctx.print(`Usage: /work-plan ${subcommand} <id>`);
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
144
144
|
const item = store.cycleItemStatus(id);
|
|
@@ -149,7 +149,7 @@ export function registerWorkPlanRuntimeCommands(registry: CommandRegistry): void
|
|
|
149
149
|
if (status) {
|
|
150
150
|
const id = args[1];
|
|
151
151
|
if (!id) {
|
|
152
|
-
ctx.print(`Usage: /
|
|
152
|
+
ctx.print(`Usage: /work-plan ${subcommand} <id>`);
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
155
|
const item = store.setItemStatus(id, status);
|
|
@@ -157,7 +157,7 @@ export function registerWorkPlanRuntimeCommands(registry: CommandRegistry): void
|
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
159
159
|
if (WORK_PLAN_STATUSES.includes(subcommand as WorkPlanItemStatus)) {
|
|
160
|
-
ctx.print(`Usage: /
|
|
160
|
+
ctx.print(`Usage: /work-plan ${subcommand} <id>`);
|
|
161
161
|
return;
|
|
162
162
|
}
|
|
163
163
|
ctx.print(`Unknown workplan subcommand: ${subcommand}`);
|
package/src/input/commands.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { registerGitRuntimeCommands } from './commands/git-runtime.ts';
|
|
|
18
18
|
import { registerNotifyRuntimeCommands } from './commands/notify-runtime.ts';
|
|
19
19
|
import { registerReplayRuntimeCommands } from './commands/replay-runtime.ts';
|
|
20
20
|
import { registerShareRuntimeCommands } from './commands/share-runtime.ts';
|
|
21
|
+
import { registerChannelRuntimeCommands } from './commands/channel-runtime.ts';
|
|
21
22
|
import { registerLocalSetupCommands } from './commands/local-setup.ts';
|
|
22
23
|
import { registerProductRuntimeCommands } from './commands/product-runtime.ts';
|
|
23
24
|
import { registerPlatformRuntimeCommands } from './commands/platform-runtime.ts';
|
|
@@ -70,6 +71,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
|
|
|
70
71
|
registerNotifyRuntimeCommands(registry);
|
|
71
72
|
registerReplayRuntimeCommands(registry);
|
|
72
73
|
registerShareRuntimeCommands(registry);
|
|
74
|
+
registerChannelRuntimeCommands(registry);
|
|
73
75
|
registerLocalSetupCommands(registry);
|
|
74
76
|
registerProductRuntimeCommands(registry);
|
|
75
77
|
registerPlatformRuntimeCommands(registry);
|
|
@@ -38,6 +38,7 @@ import type { Panel } from '../panels/types.ts';
|
|
|
38
38
|
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
39
39
|
import type { KeybindingsManager } from './keybindings.ts';
|
|
40
40
|
import type { ModelPickerTarget } from './model-picker.ts';
|
|
41
|
+
import type { KillRing } from './kill-ring.ts';
|
|
41
42
|
import type { PanelMouseLayout } from './handler-feed-routes.ts';
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -124,6 +125,7 @@ export interface FeedContextStableRefs {
|
|
|
124
125
|
conversationManager: ConversationManager | null;
|
|
125
126
|
panelManager: PanelManager;
|
|
126
127
|
keybindingsManager: KeybindingsManager;
|
|
128
|
+
killRing: KillRing;
|
|
127
129
|
getHistory: () => InfiniteBuffer;
|
|
128
130
|
getViewportHeight: () => number;
|
|
129
131
|
getScrollTop: () => number;
|
|
@@ -145,6 +147,10 @@ export interface FeedContextClosures {
|
|
|
145
147
|
handleRedo: () => void;
|
|
146
148
|
handlePaste: () => void;
|
|
147
149
|
saveUndoState: () => void;
|
|
150
|
+
/** Save undo state with text-insertion coalescing (burst typing merges into one group). */
|
|
151
|
+
saveUndoStateForText: () => void;
|
|
152
|
+
/** Break the current coalescing group (call on cursor moves). */
|
|
153
|
+
breakUndoCoalesce: () => void;
|
|
148
154
|
ensureInputCursorVisible: (contentWidth?: number) => void;
|
|
149
155
|
registerPaste: (content: string) => string;
|
|
150
156
|
executeBlockAction: (id: string) => void;
|