@pellux/goodvibes-tui 0.20.3 → 0.21.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 +27 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +3 -2
- package/src/audio/spoken-turn-controller.ts +31 -1
- package/src/audio/spoken-turn-wiring.ts +26 -4
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/completions/generate.ts +662 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management.ts +1 -8
- package/src/cli/parser.ts +14 -18
- package/src/cli/service-command.ts +1 -1
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/tui-startup.ts +72 -10
- package/src/cli/types.ts +12 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/conversation-rendering.ts +49 -15
- package/src/core/conversation.ts +101 -16
- package/src/core/format-user-error.ts +192 -0
- package/src/core/stream-event-wiring.ts +144 -0
- package/src/core/stream-stall-watchdog.ts +103 -0
- package/src/core/system-message-router.ts +5 -1
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +31 -1
- package/src/input/commands/control-room-runtime.ts +5 -5
- package/src/input/commands/experience-runtime.ts +5 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +27 -5
- package/src/input/commands/local-setup.ts +4 -6
- package/src/input/commands/memory-product-runtime.ts +8 -6
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +3 -10
- package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +3 -3
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -2
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +2 -15
- package/src/input/handler-modal-routes.ts +91 -12
- package/src/input/handler-modal-token-routes.ts +3 -0
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +55 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +5 -2
- package/src/input/input-history.ts +76 -6
- package/src/input/model-picker-filter.ts +265 -0
- package/src/input/model-picker-items.ts +208 -0
- package/src/input/model-picker.ts +92 -325
- package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
- package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +4 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
- package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
- package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
- package/src/input/onboarding/onboarding-wizard-steps.ts +18 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-data.ts +304 -0
- package/src/input/settings-modal-mutations.ts +154 -0
- package/src/input/settings-modal.ts +182 -220
- package/src/main.ts +57 -57
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +4 -1
- package/src/panels/confirm-state.ts +27 -12
- package/src/panels/cost-tracker-panel.ts +23 -67
- package/src/panels/eval-panel.ts +10 -9
- package/src/panels/knowledge-panel.ts +3 -5
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/subscription-panel.ts +33 -25
- package/src/panels/types.ts +28 -1
- package/src/panels/wrfc-panel.ts +224 -41
- package/src/renderer/agent-detail-modal.ts +11 -10
- package/src/renderer/code-block.ts +10 -2
- package/src/renderer/compositor.ts +18 -4
- package/src/renderer/context-inspector.ts +1 -5
- package/src/renderer/diff.ts +94 -21
- package/src/renderer/markdown.ts +29 -13
- package/src/renderer/settings-modal-helpers.ts +1 -1
- package/src/renderer/settings-modal.ts +77 -8
- package/src/renderer/syntax-highlighter.ts +10 -3
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/theme.ts +158 -0
- package/src/renderer/tool-call.ts +12 -2
- package/src/renderer/ui-factory.ts +50 -6
- package/src/runtime/bootstrap-command-context.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +14 -0
- package/src/runtime/bootstrap-core.ts +121 -13
- package/src/runtime/bootstrap.ts +2 -0
- package/src/runtime/onboarding/apply.ts +4 -6
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/markers.ts +42 -49
- package/src/runtime/onboarding/progress.ts +148 -0
- package/src/runtime/onboarding/state.ts +133 -55
- package/src/runtime/onboarding/types.ts +20 -0
- package/src/runtime/services.ts +21 -0
- package/src/runtime/wrfc-persistence.ts +237 -0
- package/src/shell/blocking-input.ts +20 -5
- package/src/tools/wrfc-agent-guard.ts +64 -3
- package/src/utils/format-elapsed.ts +30 -0
- package/src/utils/terminal-width.ts +45 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +4 -6
- package/src/planning/project-planning-coordinator.ts +0 -543
|
@@ -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',
|
|
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();
|
|
@@ -64,7 +64,8 @@ export function registerMemoryProductRuntimeCommands(registry: CommandRegistry):
|
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
if (sub === 'queue') {
|
|
67
|
-
|
|
67
|
+
// Pass --scope session so only session-scoped records appear in the queue.
|
|
68
|
+
await ctx.executeCommand('recall', ['queue', '--scope', 'session', ...(args[1] ? [args[1]] : [])]);
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
70
71
|
if (sub === 'export' && args[1]) {
|
|
@@ -75,13 +76,13 @@ export function registerMemoryProductRuntimeCommands(registry: CommandRegistry):
|
|
|
75
76
|
await ctx.executeCommand('recall', ['add', args[1], ...args.slice(2), '--scope', 'session']);
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
|
-
ctx.print('Usage: /session-memory [queue [limit] | export <path> | add <class> <summary...>]');
|
|
79
|
+
ctx.print('Usage: /session-memory [queue [limit] | export <path> | add <class> <summary...>]\nAll subcommands are scoped to session records only.');
|
|
79
80
|
},
|
|
80
81
|
});
|
|
81
82
|
|
|
82
83
|
registry.register({
|
|
83
84
|
name: 'team-memory',
|
|
84
|
-
description: 'Dedicated front-door for team/shared memory review and exchange',
|
|
85
|
+
description: 'Dedicated front-door for team/shared memory review and exchange. The queue and export subcommands are filtered to scope=team.',
|
|
85
86
|
usage: '[queue [limit] | export <path> | import <path> | capture policy]',
|
|
86
87
|
async handler(args, ctx) {
|
|
87
88
|
const sub = (args[0] ?? 'queue').toLowerCase();
|
|
@@ -90,7 +91,8 @@ export function registerMemoryProductRuntimeCommands(registry: CommandRegistry):
|
|
|
90
91
|
return;
|
|
91
92
|
}
|
|
92
93
|
if (sub === 'queue') {
|
|
93
|
-
|
|
94
|
+
// Pass --scope team so only team-scoped records appear in the queue.
|
|
95
|
+
await ctx.executeCommand('recall', ['queue', '--scope', 'team', ...(args[1] ? [args[1]] : [])]);
|
|
94
96
|
return;
|
|
95
97
|
}
|
|
96
98
|
if (sub === 'export' && args[1]) {
|
|
@@ -105,7 +107,7 @@ export function registerMemoryProductRuntimeCommands(registry: CommandRegistry):
|
|
|
105
107
|
await ctx.executeCommand('recall', ['capture', 'policy']);
|
|
106
108
|
return;
|
|
107
109
|
}
|
|
108
|
-
ctx.print('Usage: /team-memory [queue [limit] | export <path> | import <path> | capture policy]');
|
|
110
|
+
ctx.print('Usage: /team-memory [queue [limit] | export <path> | import <path> | capture policy]\nqueue and export are scoped to team records; import applies the bundle\'s own scopes and capture policy is global.');
|
|
109
111
|
},
|
|
110
112
|
});
|
|
111
113
|
}
|
|
@@ -5,7 +5,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
5
5
|
export function registerOperatorPanelCommand(registry: CommandRegistry): void {
|
|
6
6
|
registry.register({
|
|
7
7
|
name: 'panel',
|
|
8
|
-
aliases: ['panels'
|
|
8
|
+
aliases: ['panels'],
|
|
9
9
|
description: 'Open, place, resize, or list panels. Usage: /panel [open <id> [top|bottom]|close <id>|list|toggle|move|focus|split|width|height]',
|
|
10
10
|
usage: '[open <id> [top|bottom]|close <id>|list|toggle|move <top|bottom|other> [id]|focus <top|bottom|toggle>|split [show|hide|toggle]|width <left|right|reset>|height <up|down|reset>]',
|
|
11
11
|
argsHint: '<open|close|list|toggle|move|focus|split|width|height> [id]',
|
|
@@ -5,6 +5,7 @@ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
5
5
|
import { registerOperatorPanelCommand } from './operator-panel-runtime.ts';
|
|
6
6
|
import { requireOpsApi, requireProfileManager, requireReplayEngine } from './runtime-services.ts';
|
|
7
7
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
8
|
+
import { estimateConversationTokens } from '@pellux/goodvibes-sdk/platform/core';
|
|
8
9
|
|
|
9
10
|
export function registerOperatorRuntimeCommands(registry: CommandRegistry): void {
|
|
10
11
|
registerOperatorPanelCommand(registry);
|
|
@@ -32,18 +33,10 @@ export function registerOperatorRuntimeCommands(registry: CommandRegistry): void
|
|
|
32
33
|
ctx.print('[context] No messages in conversation.');
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
|
-
const
|
|
36
|
-
let total = 0;
|
|
36
|
+
const total = estimateConversationTokens(msgs);
|
|
37
37
|
const lines: string[] = ['Context breakdown:'];
|
|
38
38
|
for (const m of msgs) {
|
|
39
|
-
const
|
|
40
|
-
? m.content
|
|
41
|
-
: (m.content as Array<{ type: string; text?: string }>)
|
|
42
|
-
.filter((p) => p.type === 'text')
|
|
43
|
-
.map((p) => p.text ?? '')
|
|
44
|
-
.join('');
|
|
45
|
-
const t = estimateTokens(text);
|
|
46
|
-
total += t;
|
|
39
|
+
const t = estimateConversationTokens([m]);
|
|
47
40
|
lines.push(` ${m.role.padEnd(12)} ~${t.toLocaleString()} tokens`);
|
|
48
41
|
}
|
|
49
42
|
lines.push(` ${'Total'.padEnd(12)} ~${total.toLocaleString()} tokens (${msgs.length} messages)`);
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from '@/runtime/index.ts';
|
|
16
16
|
import { requireEcosystemCatalogPaths, requirePluginPathOptions } from './runtime-services.ts';
|
|
17
17
|
|
|
18
|
-
export function
|
|
18
|
+
export function registerPluginRuntimeCommands(registry: CommandRegistry): void {
|
|
19
19
|
registry.register({
|
|
20
20
|
name: 'plugin',
|
|
21
21
|
aliases: [],
|
|
@@ -7,8 +7,32 @@ export function handleRecallQueue(args: string[], context: CommandContext): void
|
|
|
7
7
|
if (!memory) {
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
|
|
11
|
+
// Extract optional --scope filter before parsing the positional limit argument.
|
|
12
|
+
const scopeIdx = args.indexOf('--scope');
|
|
13
|
+
let scopeFilter: string | undefined;
|
|
14
|
+
let remainingArgs = args;
|
|
15
|
+
if (scopeIdx !== -1 && args[scopeIdx + 1]) {
|
|
16
|
+
const candidateScope = args[scopeIdx + 1];
|
|
17
|
+
if (!isValidScope(candidateScope)) {
|
|
18
|
+
context.print(`[recall] Unknown scope "${candidateScope}". Valid: ${VALID_SCOPES.join(', ')}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
scopeFilter = candidateScope;
|
|
22
|
+
remainingArgs = args.filter((_, i) => i !== scopeIdx && i !== scopeIdx + 1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const limitRaw = remainingArgs.find((a) => !a.startsWith('--'));
|
|
26
|
+
const limit = Math.max(1, parseInt(limitRaw ?? '10', 10) || 10);
|
|
27
|
+
|
|
28
|
+
// When a scope filter is requested, fetch a larger pool then slice to the
|
|
29
|
+
// requested limit so scope-sparse queues still return meaningful results.
|
|
30
|
+
const fetchLimit = scopeFilter ? 1000 : limit;
|
|
31
|
+
const rawQueue = memory.reviewQueue(fetchLimit);
|
|
32
|
+
const queue = scopeFilter
|
|
33
|
+
? rawQueue.filter((record) => record.scope === scopeFilter).slice(0, limit)
|
|
34
|
+
: rawQueue;
|
|
35
|
+
|
|
12
36
|
if (!queue.length) {
|
|
13
37
|
context.print('[recall] Review queue is empty.');
|
|
14
38
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { atomicWriteFileSync } from '../../config/atomic-write.ts';
|
|
3
4
|
import type { CommandRegistry } from '../command-registry.ts';
|
|
4
5
|
import type { SelectionAction, SelectionItem } from '../selection-modal.ts';
|
|
5
6
|
import { openCommandPanel, requireServiceRegistry, requireShellPaths } from './runtime-services.ts';
|
|
@@ -161,8 +162,7 @@ export function registerServicesRuntimeCommands(registry: CommandRegistry): void
|
|
|
161
162
|
try {
|
|
162
163
|
const parsed = JSON.parse(readFileSync(sourcePath, 'utf-8')) as Record<string, unknown>;
|
|
163
164
|
const targetPath = shellPaths.resolveProjectPath('tui', 'services.json');
|
|
164
|
-
|
|
165
|
-
writeFileSync(targetPath, JSON.stringify(parsed, null, 2) + '\n', 'utf-8');
|
|
165
|
+
atomicWriteFileSync(targetPath, JSON.stringify(parsed, null, 2) + '\n', { mkdirp: true });
|
|
166
166
|
ctx.print(`Imported services config from ${sourcePath}`);
|
|
167
167
|
} catch (error) {
|
|
168
168
|
ctx.print(`Failed to import services config: ${summarizeError(error)}`);
|
|
@@ -443,15 +443,15 @@ export async function handleSessionWorkflowCommand(args: string[], ctx: CommandC
|
|
|
443
443
|
|
|
444
444
|
export function registerSessionWorkflowCommands(registry: CommandRegistry): void {
|
|
445
445
|
registry.register({
|
|
446
|
-
name: 'session',
|
|
447
|
-
aliases: ['
|
|
446
|
+
name: 'session-mgmt',
|
|
447
|
+
aliases: ['smgmt'],
|
|
448
448
|
description: 'Manage sessions, resume posture, and transcript structure',
|
|
449
449
|
usage: '[list | rename <name> | resume <id|name> | fork | save | info <id> | events [kind] | groups [kind] | hotspots | export <id> [format] | search <query> | delete <id>]',
|
|
450
450
|
argsHint: '<list|rename|resume|fork|save|info|events|groups|hotspots|export|search|delete>',
|
|
451
451
|
async handler(args, ctx) {
|
|
452
452
|
const handled = await handleSessionWorkflowCommand(args, ctx);
|
|
453
453
|
if (!handled) {
|
|
454
|
-
ctx.print('Unknown subcommand: ' + (args[0] ?? '') + '\nUsage: /session [list | rename <name> | resume <id> | fork [name] | save [name] | info [id] | events [kind] | groups [kind] | hotspots | export <id> [format] | search <query> | delete <id>]');
|
|
454
|
+
ctx.print('Unknown subcommand: ' + (args[0] ?? '') + '\nUsage: /session-mgmt [list | rename <name> | resume <id> | fork [name] | save [name] | info [id] | events [kind] | groups [kind] | hotspots | export <id> [format] | search <query> | delete <id>]');
|
|
455
455
|
}
|
|
456
456
|
},
|
|
457
457
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { writeFile } from 'node:fs/promises';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
2
|
import type { CommandRegistry } from '../command-registry.ts';
|
|
4
3
|
import {
|
|
5
4
|
type ExportMessage,
|
|
@@ -11,13 +10,21 @@ import {
|
|
|
11
10
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
12
11
|
import { requireShellPaths } from './runtime-services.ts';
|
|
13
12
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
13
|
+
import { calcSessionCost } from '../../export/cost-utils.ts';
|
|
14
|
+
import {
|
|
15
|
+
GistUploadTarget,
|
|
16
|
+
NO_TOKEN_GUIDANCE,
|
|
17
|
+
resolveGithubToken,
|
|
18
|
+
} from '../../export/gist-uploader.ts';
|
|
19
|
+
import { copyToClipboard } from '../../utils/clipboard.ts';
|
|
20
|
+
import { openBrowser } from '../../cli/management.ts';
|
|
14
21
|
|
|
15
22
|
export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
16
23
|
registry.register({
|
|
17
24
|
name: 'share',
|
|
18
25
|
aliases: [],
|
|
19
26
|
description: 'Export the current session to a shareable format (html, json, md)',
|
|
20
|
-
usage: '<html|json|md> [path] [--redact]',
|
|
27
|
+
usage: '<html|json|md> [path] [--redact] [--upload] [--copy] [--open]',
|
|
21
28
|
argsHint: '<html|json|md> [path]',
|
|
22
29
|
async handler(args, ctx) {
|
|
23
30
|
const shellPaths = requireShellPaths(ctx);
|
|
@@ -27,20 +34,28 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
|
27
34
|
const format = args[0]?.toLowerCase() as Format | undefined;
|
|
28
35
|
if (!format || !FORMATS.includes(format)) {
|
|
29
36
|
ctx.print(
|
|
30
|
-
'Usage: /share <html|json|md> [path] [--redact]\n'
|
|
37
|
+
'Usage: /share <html|json|md> [path] [--redact] [--upload] [--copy] [--open]\n'
|
|
31
38
|
+ ' html — self-contained HTML with syntax highlighting\n'
|
|
32
39
|
+ ' json — structured JSON (machine-readable)\n'
|
|
33
40
|
+ ' md — Markdown\n\n'
|
|
34
41
|
+ 'Options:\n'
|
|
35
|
-
+ ' --redact Redact API keys and personal paths from output\n
|
|
42
|
+
+ ' --redact Redact API keys and personal paths from output\n'
|
|
43
|
+
+ ' --upload Upload export as a secret GitHub Gist and print the share link\n'
|
|
44
|
+
+ ' --copy Copy the export file path to the clipboard\n'
|
|
45
|
+
+ ' --open Open HTML export in the default browser\n\n'
|
|
36
46
|
+ 'Default path: ~/goodvibes-exports/session-<timestamp>.<ext>',
|
|
37
47
|
);
|
|
38
48
|
return;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
const remainingArgs = args.slice(1);
|
|
42
|
-
const redact
|
|
43
|
-
const
|
|
52
|
+
const redact = remainingArgs.includes('--redact');
|
|
53
|
+
const upload = remainingArgs.includes('--upload');
|
|
54
|
+
const doCopy = remainingArgs.includes('--copy');
|
|
55
|
+
const doOpen = remainingArgs.includes('--open');
|
|
56
|
+
const pathArgs = remainingArgs.filter(
|
|
57
|
+
(a) => a !== '--redact' && a !== '--upload' && a !== '--copy' && a !== '--open',
|
|
58
|
+
);
|
|
44
59
|
const outputPath = pathArgs.length > 0
|
|
45
60
|
? shellPaths.resolveWorkspacePath(pathArgs[0])
|
|
46
61
|
: defaultExportPath(format, shellPaths.homeDirectory);
|
|
@@ -69,7 +84,7 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
|
69
84
|
return;
|
|
70
85
|
}
|
|
71
86
|
|
|
72
|
-
const messages: ExportMessage[] = convData.messages.map(m => ({
|
|
87
|
+
const messages: ExportMessage[] = convData.messages.map((m) => ({
|
|
73
88
|
role: m.role as ExportMessage['role'],
|
|
74
89
|
content: m.content as string,
|
|
75
90
|
toolCalls: m.toolCalls,
|
|
@@ -80,13 +95,32 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
|
80
95
|
usage: m.usage,
|
|
81
96
|
cancelled: m.cancelled,
|
|
82
97
|
}));
|
|
98
|
+
|
|
99
|
+
// Accumulate per-message usage totals for session cost calculation.
|
|
100
|
+
let totalInput = 0;
|
|
101
|
+
let totalOutput = 0;
|
|
102
|
+
let totalCacheRead = 0;
|
|
103
|
+
let totalCacheWrite = 0;
|
|
104
|
+
for (const m of convData.messages) {
|
|
105
|
+
if (m.usage) {
|
|
106
|
+
totalInput += m.usage.inputTokens ?? 0;
|
|
107
|
+
totalOutput += m.usage.outputTokens ?? 0;
|
|
108
|
+
totalCacheRead += m.usage.cacheReadTokens ?? 0;
|
|
109
|
+
totalCacheWrite += m.usage.cacheWriteTokens ?? 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const sessionModel = ctx.session.runtime.model;
|
|
113
|
+
const sessionCostUsd = calcSessionCost(
|
|
114
|
+
totalInput, totalOutput, totalCacheRead, totalCacheWrite, sessionModel,
|
|
115
|
+
);
|
|
116
|
+
|
|
83
117
|
const metadata = {
|
|
84
|
-
model:
|
|
118
|
+
model: sessionModel,
|
|
85
119
|
provider: ctx.session.runtime.provider,
|
|
86
120
|
sessionId: ctx.session.runtime.sessionId,
|
|
87
121
|
title: ctx.session.conversationManager.title || undefined,
|
|
88
122
|
};
|
|
89
|
-
const options = { redact };
|
|
123
|
+
const options = { redact, cost: sessionCostUsd };
|
|
90
124
|
|
|
91
125
|
let outputContent: string;
|
|
92
126
|
try {
|
|
@@ -99,11 +133,14 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
|
99
133
|
}
|
|
100
134
|
|
|
101
135
|
const { mkdirSync } = await import('node:fs');
|
|
102
|
-
const { dirname } = await import('node:path');
|
|
136
|
+
const { dirname, basename } = await import('node:path');
|
|
103
137
|
try {
|
|
104
138
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
105
139
|
} catch (mkdirErr) {
|
|
106
|
-
logger.warn(
|
|
140
|
+
logger.warn(
|
|
141
|
+
`[share] mkdir failed for ${dirname(outputPath)}:`,
|
|
142
|
+
mkdirErr instanceof Error ? { message: mkdirErr.message } : undefined,
|
|
143
|
+
);
|
|
107
144
|
}
|
|
108
145
|
|
|
109
146
|
try {
|
|
@@ -113,7 +150,57 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
|
113
150
|
return;
|
|
114
151
|
}
|
|
115
152
|
|
|
116
|
-
|
|
153
|
+
// Optional Gist upload: resolve auth token then push content as a secret gist.
|
|
154
|
+
let shareLink: string | undefined;
|
|
155
|
+
if (upload) {
|
|
156
|
+
let authHeaders: Record<string, string> | null = null;
|
|
157
|
+
try {
|
|
158
|
+
const svcRegistry = ctx.platform.serviceRegistry;
|
|
159
|
+
if (svcRegistry) {
|
|
160
|
+
authHeaders = await svcRegistry.resolveAuth('github').catch(() => null);
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// serviceRegistry absent or resolveAuth threw — fall through to env var
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const token = resolveGithubToken(authHeaders ?? undefined);
|
|
167
|
+
if (!token) {
|
|
168
|
+
ctx.print(NO_TOKEN_GUIDANCE);
|
|
169
|
+
} else {
|
|
170
|
+
const gistFilename = basename(outputPath);
|
|
171
|
+
const description = metadata.title
|
|
172
|
+
? `GoodVibes session: ${metadata.title}`
|
|
173
|
+
: 'GoodVibes session export';
|
|
174
|
+
const uploader = new GistUploadTarget(token, description);
|
|
175
|
+
const result = await uploader.upload(outputContent, gistFilename);
|
|
176
|
+
if (result.ok) {
|
|
177
|
+
shareLink = result.url;
|
|
178
|
+
} else {
|
|
179
|
+
ctx.print(`Upload failed: ${result.error}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Copy export path to clipboard if requested.
|
|
185
|
+
if (doCopy) {
|
|
186
|
+
copyToClipboard(outputPath);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Open the exported HTML in the default browser if requested.
|
|
190
|
+
if (doOpen && format === 'html') {
|
|
191
|
+
openBrowser(`file://${outputPath}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Emit the summary line, with all post-export hints inline.
|
|
195
|
+
const hints: string[] = [];
|
|
196
|
+
if (redact) hints.push('(sensitive data redacted)');
|
|
197
|
+
if (shareLink) hints.push(`Share link: ${shareLink}`);
|
|
198
|
+
if (doCopy) hints.push('(path copied to clipboard)');
|
|
199
|
+
if (doOpen && format === 'html') hints.push('(opened in browser)');
|
|
200
|
+
if (doOpen && format !== 'html') hints.push('(--open ignored: only applies to html)');
|
|
201
|
+
|
|
202
|
+
const hint = hints.length > 0 ? ' ' + hints.join(' ') : '';
|
|
203
|
+
ctx.print(`Exported ${format.toUpperCase()} session to ${outputPath}${hint}`);
|
|
117
204
|
},
|
|
118
205
|
});
|
|
119
206
|
}
|
|
@@ -3,19 +3,46 @@ import type { CommandRegistry } from '../command-registry.ts';
|
|
|
3
3
|
export function registerTtsRuntimeCommands(registry: CommandRegistry): void {
|
|
4
4
|
registry.register({
|
|
5
5
|
name: 'tts',
|
|
6
|
-
description: 'Submit a
|
|
7
|
-
usage: '<prompt>|stop',
|
|
6
|
+
description: 'Submit a prompt for live TTS playback, or control always-speak mode',
|
|
7
|
+
usage: '<prompt>|stop|on|off',
|
|
8
8
|
handler(args, ctx) {
|
|
9
9
|
const first = (args[0] ?? '').toLowerCase();
|
|
10
|
+
|
|
11
|
+
// /tts stop — cancel active playback
|
|
10
12
|
if (first === 'stop' || first === 'cancel') {
|
|
11
13
|
ctx.stopSpokenOutput?.();
|
|
12
14
|
ctx.print('Live TTS playback stopped.');
|
|
13
15
|
return;
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
// /tts on — enable always-speak mode (every turn spoken automatically)
|
|
19
|
+
if (first === 'on') {
|
|
20
|
+
if (!ctx.platform.voiceService) {
|
|
21
|
+
ctx.print('Live TTS is not available in this runtime.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
ctx.platform.configManager.setDynamic('ui.voiceEnabled', true);
|
|
25
|
+
ctx.platform.configManager.save();
|
|
26
|
+
ctx.print('Always-speak mode enabled. Every submitted turn will be played through live TTS.');
|
|
27
|
+
ctx.renderRequest();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// /tts off — disable always-speak mode
|
|
32
|
+
if (first === 'off') {
|
|
33
|
+
ctx.platform.configManager.setDynamic('ui.voiceEnabled', false);
|
|
34
|
+
ctx.platform.configManager.save();
|
|
35
|
+
ctx.print('Always-speak mode disabled. Use /tts <prompt> to speak individual turns.');
|
|
36
|
+
ctx.renderRequest();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// /tts <prompt> — mark this prompt for spoken output
|
|
16
41
|
const prompt = args.join(' ').trim();
|
|
17
42
|
if (!prompt) {
|
|
18
|
-
ctx.
|
|
43
|
+
const enabled = ctx.platform.configManager.get('ui.voiceEnabled');
|
|
44
|
+
ctx.print(`Usage: /tts <prompt>, /tts stop, /tts on, /tts off
|
|
45
|
+
Always-speak mode is currently ${enabled ? 'on' : 'off'}.`);
|
|
19
46
|
return;
|
|
20
47
|
}
|
|
21
48
|
if (!ctx.submitSpokenInput) {
|
|
@@ -25,5 +52,4 @@ export function registerTtsRuntimeCommands(registry: CommandRegistry): void {
|
|
|
25
52
|
ctx.submitSpokenInput(prompt);
|
|
26
53
|
},
|
|
27
54
|
});
|
|
28
|
-
|
|
29
55
|
}
|
package/src/input/commands.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { registerPlanningRuntimeCommands } from './commands/planning-runtime.ts'
|
|
|
13
13
|
import { registerScheduleRuntimeCommands } from './commands/schedule-runtime.ts';
|
|
14
14
|
import { registerBranchRuntimeCommands } from './commands/branch-runtime.ts';
|
|
15
15
|
import { registerOperatorRuntimeCommands } from './commands/operator-runtime.ts';
|
|
16
|
-
import {
|
|
16
|
+
import { registerPluginRuntimeCommands } from './commands/plugin-runtime.ts';
|
|
17
17
|
import { registerDiffRuntimeCommands } from './commands/diff-runtime.ts';
|
|
18
18
|
import { registerGitRuntimeCommands } from './commands/git-runtime.ts';
|
|
19
19
|
import { registerNotifyRuntimeCommands } from './commands/notify-runtime.ts';
|
|
@@ -65,7 +65,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
|
|
|
65
65
|
registerShellCoreCommands(registry);
|
|
66
66
|
registerConfigCommand(registry);
|
|
67
67
|
registerOperatorRuntimeCommands(registry);
|
|
68
|
-
|
|
68
|
+
registerPluginRuntimeCommands(registry);
|
|
69
69
|
registerDiffRuntimeCommands(registry);
|
|
70
70
|
registerGitRuntimeCommands(registry);
|
|
71
71
|
registerNotifyRuntimeCommands(registry);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Delete-key policy — single source of truth for text-editing key semantics
|
|
3
|
+
//
|
|
4
|
+
// In text-editing contexts:
|
|
5
|
+
// 'backspace' → delete one character backward (the character before cursor /
|
|
6
|
+
// the last character in an end-anchored buffer)
|
|
7
|
+
// 'delete' → forward-delete (the character after cursor) when a moveable
|
|
8
|
+
// cursor exists; a no-op in cursorless/end-anchored contexts,
|
|
9
|
+
// EXCEPT where it opens a confirmation-gated clear action
|
|
10
|
+
// (see planning panel: Delete opens the ConfirmState gate that
|
|
11
|
+
// lets the user clear their entire draft answer).
|
|
12
|
+
//
|
|
13
|
+
// Consequences for each surface:
|
|
14
|
+
// Panel search filters (end-anchored, no cursor)
|
|
15
|
+
// 'backspace' → remove last char ✓
|
|
16
|
+
// 'delete' → no-op ✓ (no cursor; nothing is "forward")
|
|
17
|
+
//
|
|
18
|
+
// Selection modal filters (end-anchored, no cursor)
|
|
19
|
+
// 'backspace' → remove last char ✓
|
|
20
|
+
// 'delete' → no-op ✓
|
|
21
|
+
//
|
|
22
|
+
// Planning panel draft answer (end-anchored, no cursor)
|
|
23
|
+
// 'backspace' → remove last char ✓
|
|
24
|
+
// 'delete' → open ConfirmState gate (y/Enter confirms clear; n/Esc cancels)
|
|
25
|
+
// The draft is NOT wiped until the user confirms.
|
|
26
|
+
//
|
|
27
|
+
// NEVER assign bulk-destructive actions (clear / wipe) to the Delete key
|
|
28
|
+
// without an explicit confirmation gate.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns true when `key` should perform a backward-delete (remove the
|
|
33
|
+
* character before the cursor / at end of an end-anchored buffer).
|
|
34
|
+
*/
|
|
35
|
+
export function isTextBackspace(key: string): boolean {
|
|
36
|
+
return key === 'backspace';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns true when `key` should perform a forward-delete (remove the
|
|
41
|
+
* character after the cursor). Only meaningful when a moveable cursor
|
|
42
|
+
* exists; callers should treat this as a no-op when there is no cursor.
|
|
43
|
+
*/
|
|
44
|
+
export function isTextForwardDelete(key: string): boolean {
|
|
45
|
+
return key === 'delete';
|
|
46
|
+
}
|
|
@@ -161,6 +161,8 @@ export interface FeedContextClosures {
|
|
|
161
161
|
openProviderModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') => boolean;
|
|
162
162
|
onModelPickerCommit: () => boolean;
|
|
163
163
|
onOnboardingAction: (action: import('./onboarding/onboarding-wizard.ts').OnboardingWizardAction) => void;
|
|
164
|
+
/** Called after any wizard step navigation so the handler can persist progress. */
|
|
165
|
+
onStepChange?: () => void;
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
/**
|
|
@@ -162,6 +162,8 @@ export interface InputFeedContext {
|
|
|
162
162
|
readonly openProviderModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') => boolean;
|
|
163
163
|
readonly onModelPickerCommit: () => boolean;
|
|
164
164
|
readonly onOnboardingAction: (action: OnboardingWizardAction) => void;
|
|
165
|
+
/** Called after any wizard step navigation so the handler can persist progress. */
|
|
166
|
+
readonly onStepChange?: () => void;
|
|
165
167
|
readonly exitApp: () => void;
|
|
166
168
|
}
|
|
167
169
|
|
|
@@ -228,6 +230,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
228
230
|
openProviderModelPickerWithTarget: context.openProviderModelPickerWithTarget,
|
|
229
231
|
onModelPickerCommit: context.onModelPickerCommit,
|
|
230
232
|
onOnboardingAction: context.onOnboardingAction,
|
|
233
|
+
onStepChange: context.onStepChange,
|
|
231
234
|
}, token);
|
|
232
235
|
context.selectionCallback = modalRoute.selectionCallback;
|
|
233
236
|
context.helpOverlayActive = modalRoute.helpOverlayActive;
|
|
@@ -1,29 +1,16 @@
|
|
|
1
1
|
import { buildProviderAccountSnapshot } from '@/runtime/index.ts';
|
|
2
2
|
import type { OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
|
|
3
|
-
import { collectOnboardingSnapshot
|
|
3
|
+
import { collectOnboardingSnapshot } from '../runtime/onboarding/index.ts';
|
|
4
4
|
import { cleanupMarkerRegistry, expandPrompt, findMarkerAtPos, handleBlockCopy, handleBlockRerun, handleBlockSave, handleBlockToggle, handleBookmark, handleClipboardPaste, handleCopy, handleCtrlC, handleDiffApply, registerPaste } from './handler-content-actions.ts';
|
|
5
5
|
import { clearModalStack, handleEscape, modalOpened } from './handler-modal-stack.ts';
|
|
6
6
|
import { openOnboardingWizardState, type OpenOnboardingWizardOptions } from './handler-ui-state.ts';
|
|
7
|
-
import type { InputHandler } from './handler.ts';
|
|
7
|
+
import type { InputHandlerLike as InputHandler } from './handler-types.ts';
|
|
8
8
|
|
|
9
9
|
export function openOnboardingWizardForHandler(
|
|
10
10
|
handler: InputHandler,
|
|
11
11
|
modeOrOptions: OnboardingWizardMode | OpenOnboardingWizardOptions = 'new',
|
|
12
12
|
): void {
|
|
13
13
|
const options = typeof modeOrOptions === 'string' ? { mode: modeOrOptions } : modeOrOptions;
|
|
14
|
-
const userMarker = readOnboardingCheckMarker(handler.uiServices.environment.shellPaths, 'user');
|
|
15
|
-
if (!userMarker.payload) {
|
|
16
|
-
try {
|
|
17
|
-
writeOnboardingCheckMarker(handler.uiServices.environment.shellPaths, {
|
|
18
|
-
scope: 'user',
|
|
19
|
-
source: 'wizard',
|
|
20
|
-
mode: options.mode ?? 'new',
|
|
21
|
-
});
|
|
22
|
-
} catch (error) {
|
|
23
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
24
|
-
handler.commandContext?.print?.(`Onboarding check marker could not be written: ${message}`);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
14
|
if (!handler.modalStack.includes('onboarding')) handler.modalOpened('onboarding');
|
|
28
15
|
handler.clearOnboardingModelPickerCancelState();
|
|
29
16
|
openOnboardingWizardState(handler.onboardingWizard, options);
|