@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.
Files changed (118) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +23 -2
  3. package/docs/foundation-artifacts/operator-contract.json +78 -1
  4. package/package.json +3 -2
  5. package/src/audio/spoken-turn-controller.ts +31 -1
  6. package/src/audio/spoken-turn-wiring.ts +26 -4
  7. package/src/cli/bundle-command.ts +1 -1
  8. package/src/cli/completions/generate.ts +662 -0
  9. package/src/cli/config-overrides.ts +68 -0
  10. package/src/cli/help.ts +4 -2
  11. package/src/cli/management-commands.ts +1 -1
  12. package/src/cli/management.ts +1 -8
  13. package/src/cli/parser.ts +14 -18
  14. package/src/cli/service-command.ts +1 -1
  15. package/src/cli/surface-command.ts +1 -1
  16. package/src/cli/tui-startup.ts +72 -10
  17. package/src/cli/types.ts +12 -3
  18. package/src/cli-flags.ts +1 -0
  19. package/src/config/atomic-write.ts +70 -0
  20. package/src/config/read-versioned.ts +115 -0
  21. package/src/core/conversation-rendering.ts +49 -15
  22. package/src/core/conversation.ts +101 -16
  23. package/src/core/format-user-error.ts +192 -0
  24. package/src/core/stream-event-wiring.ts +144 -0
  25. package/src/core/stream-stall-watchdog.ts +103 -0
  26. package/src/core/system-message-router.ts +5 -1
  27. package/src/export/cost-utils.ts +71 -0
  28. package/src/export/gist-uploader.ts +136 -0
  29. package/src/input/command-registry.ts +31 -1
  30. package/src/input/commands/control-room-runtime.ts +5 -5
  31. package/src/input/commands/experience-runtime.ts +5 -4
  32. package/src/input/commands/knowledge.ts +1 -1
  33. package/src/input/commands/local-auth-runtime.ts +27 -5
  34. package/src/input/commands/local-setup.ts +4 -6
  35. package/src/input/commands/memory-product-runtime.ts +8 -6
  36. package/src/input/commands/operator-panel-runtime.ts +1 -1
  37. package/src/input/commands/operator-runtime.ts +3 -10
  38. package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
  39. package/src/input/commands/recall-review.ts +26 -2
  40. package/src/input/commands/services-runtime.ts +2 -2
  41. package/src/input/commands/session-workflow.ts +3 -3
  42. package/src/input/commands/share-runtime.ts +99 -12
  43. package/src/input/commands/tts-runtime.ts +30 -4
  44. package/src/input/commands.ts +2 -2
  45. package/src/input/delete-key-policy.ts +46 -0
  46. package/src/input/feed-context-factory.ts +2 -0
  47. package/src/input/handler-feed.ts +3 -0
  48. package/src/input/handler-interactions.ts +2 -15
  49. package/src/input/handler-modal-routes.ts +91 -12
  50. package/src/input/handler-modal-token-routes.ts +3 -0
  51. package/src/input/handler-onboarding-cloudflare.ts +1 -1
  52. package/src/input/handler-onboarding.ts +55 -69
  53. package/src/input/handler-types.ts +163 -0
  54. package/src/input/handler.ts +5 -2
  55. package/src/input/input-history.ts +76 -6
  56. package/src/input/model-picker-filter.ts +265 -0
  57. package/src/input/model-picker-items.ts +208 -0
  58. package/src/input/model-picker.ts +92 -325
  59. package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
  60. package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
  61. package/src/input/onboarding/onboarding-wizard-apply.ts +4 -4
  62. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +2 -2
  63. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
  64. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
  65. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
  66. package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
  67. package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
  68. package/src/input/onboarding/onboarding-wizard-steps.ts +18 -25
  69. package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
  70. package/src/input/onboarding/onboarding-wizard.ts +3 -3
  71. package/src/input/settings-modal-data.ts +304 -0
  72. package/src/input/settings-modal-mutations.ts +154 -0
  73. package/src/input/settings-modal.ts +182 -220
  74. package/src/main.ts +57 -57
  75. package/src/panels/builtin/agent.ts +4 -1
  76. package/src/panels/builtin/development.ts +4 -1
  77. package/src/panels/confirm-state.ts +27 -12
  78. package/src/panels/cost-tracker-panel.ts +23 -67
  79. package/src/panels/eval-panel.ts +10 -9
  80. package/src/panels/knowledge-panel.ts +3 -5
  81. package/src/panels/local-auth-panel.ts +124 -4
  82. package/src/panels/project-planning-panel.ts +42 -4
  83. package/src/panels/search-focus.ts +11 -5
  84. package/src/panels/subscription-panel.ts +33 -25
  85. package/src/panels/types.ts +28 -1
  86. package/src/panels/wrfc-panel.ts +224 -41
  87. package/src/renderer/agent-detail-modal.ts +11 -10
  88. package/src/renderer/code-block.ts +10 -2
  89. package/src/renderer/compositor.ts +18 -4
  90. package/src/renderer/context-inspector.ts +1 -5
  91. package/src/renderer/diff.ts +94 -21
  92. package/src/renderer/markdown.ts +29 -13
  93. package/src/renderer/settings-modal-helpers.ts +1 -1
  94. package/src/renderer/settings-modal.ts +77 -8
  95. package/src/renderer/syntax-highlighter.ts +10 -3
  96. package/src/renderer/term-caps.ts +318 -0
  97. package/src/renderer/theme.ts +158 -0
  98. package/src/renderer/tool-call.ts +12 -2
  99. package/src/renderer/ui-factory.ts +50 -6
  100. package/src/runtime/bootstrap-command-context.ts +1 -0
  101. package/src/runtime/bootstrap-command-parts.ts +14 -0
  102. package/src/runtime/bootstrap-core.ts +121 -13
  103. package/src/runtime/bootstrap.ts +2 -0
  104. package/src/runtime/onboarding/apply.ts +4 -6
  105. package/src/runtime/onboarding/index.ts +1 -0
  106. package/src/runtime/onboarding/markers.ts +42 -49
  107. package/src/runtime/onboarding/progress.ts +148 -0
  108. package/src/runtime/onboarding/state.ts +133 -55
  109. package/src/runtime/onboarding/types.ts +20 -0
  110. package/src/runtime/services.ts +21 -0
  111. package/src/runtime/wrfc-persistence.ts +237 -0
  112. package/src/shell/blocking-input.ts +20 -5
  113. package/src/tools/wrfc-agent-guard.ts +64 -3
  114. package/src/utils/format-elapsed.ts +30 -0
  115. package/src/utils/terminal-width.ts +45 -0
  116. package/src/version.ts +1 -1
  117. package/src/work-plans/work-plan-store.ts +4 -6
  118. 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
- await ctx.executeCommand('recall', ['queue', ...(args[1] ? [args[1]] : [])]);
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
- await ctx.executeCommand('recall', ['queue', ...(args[1] ? [args[1]] : [])]);
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', 'p'],
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 estimateTokens = (text: string): number => Math.ceil(text.length / 4);
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 text = typeof m.content === 'string'
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 registerIntegrationRuntimeCommands(registry: CommandRegistry): void {
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
- const limit = Math.max(1, parseInt(args[0] ?? '10', 10) || 10);
11
- const queue = memory.reviewQueue(limit);
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
- mkdirSync(dirname(targetPath), { recursive: true });
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: ['sess'],
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\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 = remainingArgs.includes('--redact');
43
- const pathArgs = remainingArgs.filter(a => a !== '--redact');
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: ctx.session.runtime.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(`[share] mkdir failed for ${dirname(outputPath)}:`, mkdirErr instanceof Error ? { message: mkdirErr.message } : undefined);
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
- ctx.print(`Exported ${format.toUpperCase()} session to ${outputPath}${redact ? ' (sensitive data redacted)' : ''}`);
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 normal prompt and play the assistant response through live TTS',
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.print('Usage: /tts <prompt> or /tts stop');
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
  }
@@ -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 { registerIntegrationRuntimeCommands } from './commands/integration-runtime.ts';
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
- registerIntegrationRuntimeCommands(registry);
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, readOnboardingCheckMarker, writeOnboardingCheckMarker } from '../runtime/onboarding/index.ts';
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);