@pellux/goodvibes-tui 0.20.3 → 0.22.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 (142) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +23 -2
  3. package/docs/foundation-artifacts/operator-contract.json +78 -1
  4. package/package.json +4 -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 +658 -0
  9. package/src/cli/config-overrides.ts +68 -0
  10. package/src/cli/entrypoint.ts +6 -0
  11. package/src/cli/help.ts +4 -2
  12. package/src/cli/management-commands.ts +1 -1
  13. package/src/cli/management.ts +1 -8
  14. package/src/cli/parser.ts +31 -18
  15. package/src/cli/service-command.ts +1 -1
  16. package/src/cli/surface-command.ts +1 -1
  17. package/src/cli/tui-startup.ts +72 -10
  18. package/src/cli/types.ts +14 -3
  19. package/src/cli-flags.ts +1 -0
  20. package/src/config/atomic-write.ts +70 -0
  21. package/src/config/goodvibes-home-audit.ts +2 -0
  22. package/src/config/read-versioned.ts +115 -0
  23. package/src/core/context-auto-compact.ts +77 -0
  24. package/src/core/conversation-rendering.ts +49 -15
  25. package/src/core/conversation.ts +101 -16
  26. package/src/core/format-user-error.ts +192 -0
  27. package/src/core/stream-event-wiring.ts +144 -0
  28. package/src/core/stream-stall-watchdog.ts +103 -0
  29. package/src/core/system-message-router.ts +5 -1
  30. package/src/core/turn-event-wiring.ts +124 -0
  31. package/src/daemon/cli.ts +5 -0
  32. package/src/export/cost-utils.ts +71 -0
  33. package/src/export/gist-uploader.ts +136 -0
  34. package/src/input/command-registry.ts +32 -1
  35. package/src/input/commands/control-room-runtime.ts +10 -10
  36. package/src/input/commands/experience-runtime.ts +5 -4
  37. package/src/input/commands/knowledge.ts +1 -1
  38. package/src/input/commands/local-auth-runtime.ts +27 -5
  39. package/src/input/commands/local-setup.ts +4 -6
  40. package/src/input/commands/memory-product-runtime.ts +8 -6
  41. package/src/input/commands/operator-panel-runtime.ts +1 -1
  42. package/src/input/commands/operator-runtime.ts +3 -10
  43. package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
  44. package/src/input/commands/provider.ts +57 -3
  45. package/src/input/commands/recall-review.ts +26 -2
  46. package/src/input/commands/services-runtime.ts +2 -2
  47. package/src/input/commands/session-workflow.ts +8 -16
  48. package/src/input/commands/session.ts +70 -20
  49. package/src/input/commands/share-runtime.ts +99 -12
  50. package/src/input/commands/tts-runtime.ts +30 -4
  51. package/src/input/commands.ts +2 -4
  52. package/src/input/delete-key-policy.ts +46 -0
  53. package/src/input/feed-context-factory.ts +2 -0
  54. package/src/input/handler-feed.ts +3 -0
  55. package/src/input/handler-interactions.ts +2 -15
  56. package/src/input/handler-modal-routes.ts +128 -12
  57. package/src/input/handler-modal-token-routes.ts +22 -5
  58. package/src/input/handler-onboarding-cloudflare.ts +1 -1
  59. package/src/input/handler-onboarding.ts +73 -69
  60. package/src/input/handler-types.ts +163 -0
  61. package/src/input/handler.ts +6 -2
  62. package/src/input/input-history.ts +76 -6
  63. package/src/input/model-picker-filter.ts +265 -0
  64. package/src/input/model-picker-items.ts +208 -0
  65. package/src/input/model-picker.ts +92 -325
  66. package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
  67. package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
  68. package/src/input/onboarding/onboarding-wizard-apply.ts +14 -4
  69. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +16 -2
  70. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
  71. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
  72. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
  73. package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
  74. package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
  75. package/src/input/onboarding/onboarding-wizard-steps.ts +24 -25
  76. package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
  77. package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
  78. package/src/input/onboarding/onboarding-wizard.ts +3 -3
  79. package/src/input/settings-modal-behavior.ts +5 -0
  80. package/src/input/settings-modal-data.ts +378 -0
  81. package/src/input/settings-modal-mutations.ts +157 -0
  82. package/src/input/settings-modal-reset.ts +154 -0
  83. package/src/input/settings-modal.ts +236 -232
  84. package/src/main.ts +93 -85
  85. package/src/panels/agent-inspector-panel.ts +120 -18
  86. package/src/panels/agent-inspector-shared.ts +29 -0
  87. package/src/panels/builtin/agent.ts +4 -1
  88. package/src/panels/builtin/development.ts +5 -1
  89. package/src/panels/builtin/knowledge.ts +14 -13
  90. package/src/panels/builtin/operations.ts +22 -1
  91. package/src/panels/builtin/shared.ts +7 -0
  92. package/src/panels/cockpit-panel.ts +123 -3
  93. package/src/panels/cockpit-read-model.ts +232 -0
  94. package/src/panels/confirm-state.ts +27 -12
  95. package/src/panels/cost-tracker-panel.ts +23 -67
  96. package/src/panels/eval-panel.ts +10 -9
  97. package/src/panels/index.ts +1 -1
  98. package/src/panels/knowledge-graph-panel.ts +84 -0
  99. package/src/panels/local-auth-panel.ts +124 -4
  100. package/src/panels/memory-panel.ts +370 -40
  101. package/src/panels/project-planning-panel.ts +42 -4
  102. package/src/panels/search-focus.ts +11 -5
  103. package/src/panels/session-maintenance.ts +66 -15
  104. package/src/panels/subscription-panel.ts +33 -25
  105. package/src/panels/types.ts +28 -1
  106. package/src/panels/wrfc-panel.ts +224 -41
  107. package/src/renderer/agent-detail-modal.ts +118 -13
  108. package/src/renderer/code-block.ts +10 -2
  109. package/src/renderer/compositor.ts +18 -4
  110. package/src/renderer/context-inspector.ts +1 -5
  111. package/src/renderer/context-status-hint.ts +54 -0
  112. package/src/renderer/diff.ts +94 -21
  113. package/src/renderer/markdown.ts +29 -13
  114. package/src/renderer/settings-modal-helpers.ts +1 -1
  115. package/src/renderer/settings-modal.ts +90 -10
  116. package/src/renderer/shell-surface.ts +10 -0
  117. package/src/renderer/syntax-highlighter.ts +10 -3
  118. package/src/renderer/term-caps.ts +318 -0
  119. package/src/renderer/theme.ts +158 -0
  120. package/src/renderer/tool-call.ts +12 -2
  121. package/src/renderer/ui-factory.ts +50 -6
  122. package/src/runtime/bootstrap-command-context.ts +1 -0
  123. package/src/runtime/bootstrap-command-parts.ts +18 -0
  124. package/src/runtime/bootstrap-core.ts +145 -13
  125. package/src/runtime/bootstrap-shell.ts +11 -0
  126. package/src/runtime/bootstrap.ts +9 -0
  127. package/src/runtime/onboarding/apply.ts +4 -6
  128. package/src/runtime/onboarding/index.ts +1 -0
  129. package/src/runtime/onboarding/markers.ts +42 -49
  130. package/src/runtime/onboarding/progress.ts +148 -0
  131. package/src/runtime/onboarding/state.ts +133 -55
  132. package/src/runtime/onboarding/types.ts +20 -0
  133. package/src/runtime/services.ts +27 -1
  134. package/src/runtime/wrfc-persistence.ts +237 -0
  135. package/src/shell/blocking-input.ts +20 -5
  136. package/src/tools/wrfc-agent-guard.ts +64 -3
  137. package/src/utils/format-elapsed.ts +30 -0
  138. package/src/utils/terminal-width.ts +45 -0
  139. package/src/version.ts +1 -1
  140. package/src/work-plans/work-plan-store.ts +4 -6
  141. package/src/panels/knowledge-panel.ts +0 -345
  142. package/src/planning/project-planning-coordinator.ts +0 -543
@@ -196,18 +196,18 @@ export function registerControlRoomRuntimeCommands(registry: CommandRegistry): v
196
196
  });
197
197
 
198
198
  registry.register({
199
- name: 'knowledge',
200
- aliases: ['know'],
201
- description: 'Inspect durable project knowledge, risks, runbooks, and architecture notes',
199
+ name: 'project-memory',
200
+ aliases: ['pmem'],
201
+ description: 'Inspect durable project memory: risks, runbooks, and architecture notes',
202
202
  usage: '[open | queue [limit] | explain <task...> [--scope <path> ...]]',
203
203
  handler(args, ctx) {
204
204
  const subcommand = (args[0] ?? 'open').toLowerCase();
205
205
  if (subcommand === 'open') {
206
- if (ctx.openKnowledgePanel) {
207
- ctx.openKnowledgePanel();
206
+ if (ctx.openMemoryPanel) {
207
+ ctx.openMemoryPanel();
208
208
  return;
209
209
  }
210
- ctx.print('Knowledge panel is not available in this runtime.');
210
+ ctx.print('Memory panel is not available in this runtime.');
211
211
  return;
212
212
  }
213
213
  const memory = getMemoryApi(ctx);
@@ -237,7 +237,7 @@ export function registerControlRoomRuntimeCommands(registry: CommandRegistry): v
237
237
  });
238
238
  const task = taskTokens.join(' ').trim();
239
239
  if (!task) {
240
- ctx.print('Usage: /knowledge explain <task...> [--scope <path> ...]');
240
+ ctx.print('Usage: /project-memory explain <task...> [--scope <path> ...]');
241
241
  return;
242
242
  }
243
243
  const injections = selectKnowledgeForTask(memory, task, scopeValues);
@@ -245,11 +245,11 @@ export function registerControlRoomRuntimeCommands(registry: CommandRegistry): v
245
245
  ctx.print(prompt ?? 'No reviewed project knowledge matched that task.');
246
246
  return;
247
247
  }
248
- if (ctx.openKnowledgePanel) {
249
- ctx.openKnowledgePanel();
248
+ if (ctx.openMemoryPanel) {
249
+ ctx.openMemoryPanel();
250
250
  return;
251
251
  }
252
- ctx.print(`Unknown knowledge subcommand: ${subcommand}`);
252
+ ctx.print(`Unknown project-memory subcommand: ${subcommand}`);
253
253
  },
254
254
  });
255
255
  }
@@ -222,7 +222,7 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
222
222
 
223
223
  registry.register({
224
224
  name: 'voice',
225
- description: 'Review voice posture and package portable voice-surface metadata',
225
+ description: 'Review or toggle always-speak mode (same switch as /tts on|off) and package portable voice metadata',
226
226
  usage: '[review|enable|disable|bundle export <path>|bundle inspect <path>]',
227
227
  handler(args, ctx) {
228
228
  const shellPaths = requireShellPaths(ctx);
@@ -231,8 +231,9 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
231
231
  const enabled = Boolean(ctx.platform.configManager.get('ui.voiceEnabled') ?? false);
232
232
  ctx.print([
233
233
  'Voice Review',
234
- ` enabled: ${enabled ? 'yes' : 'no'}`,
235
- ' posture: optional local companion surface; disabled by default',
234
+ ` always-speak: ${enabled ? 'on' : 'off'}`,
235
+ ' config key: ui.voiceEnabled (same as /tts on|off)',
236
+ ' posture: optional local TTS output; disabled by default',
236
237
  ' note: voice remains an optional operator convenience, not a required SaaS dependency',
237
238
  ].join('\n'));
238
239
  return;
@@ -240,7 +241,7 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
240
241
  if (sub === 'enable' || sub === 'disable') {
241
242
  const next = sub === 'enable';
242
243
  ctx.platform.configManager.setDynamic('ui.voiceEnabled', next);
243
- ctx.print(`Voice surface ${next ? 'enabled' : 'disabled'} for this runtime.`);
244
+ ctx.print(`Always-speak mode ${next ? 'enabled' : 'disabled'}. ${next ? 'Every submitted turn will be played through live TTS.' : 'Use /tts <prompt> to speak individual turns.'}`);
244
245
  return;
245
246
  }
246
247
  if (sub === 'bundle') {
@@ -131,7 +131,7 @@ function renderKnowledgeAskResult(result: KnowledgeAskResult): string {
131
131
 
132
132
  export const knowledgeCommand: SlashCommand = {
133
133
  name: 'knowledge',
134
- aliases: ['know', 'kb'],
134
+ aliases: ['know'],
135
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',
@@ -17,11 +17,22 @@ export function handleLocalAuthCommand(args: string[], ctx: CommandContext): voi
17
17
  if (sub === 'add-user') {
18
18
  const username = args[1];
19
19
  const password = args[2];
20
- const roles = args[3]?.split(',').map((value) => value.trim()).filter(Boolean) ?? ['admin'];
21
- if (!username || !password) {
22
- ctx.print('Usage: /auth local add-user <username> <password> [roles]');
20
+ if (!username) {
21
+ ctx.print('Usage: /auth local add-user <username> <password> [roles]\nTip: invoke without a password to use the masked panel: /auth local add-user <username>');
22
+ return;
23
+ }
24
+ if (!password) {
25
+ // No password supplied — open masked-entry mode on the LocalAuthPanel.
26
+ if (ctx.openLocalAuthMaskedEntry) {
27
+ ctx.openLocalAuthMaskedEntry('add-user', username);
28
+ } else {
29
+ ctx.print('Masked entry unavailable in this context. Use: /auth local add-user <username> <password>');
30
+ }
23
31
  return;
24
32
  }
33
+ // Password supplied as argv: warn that the history entry has been scrubbed.
34
+ ctx.print('Warning: passwords passed as command arguments are scrubbed from history, but may appear in shell scrollback. The masked entry is preferred: /auth local add-user <username>');
35
+ const roles = args[3]?.split(',').map((value) => value.trim()).filter(Boolean) ?? ['admin'];
25
36
  try {
26
37
  const added = auth.addUser(username, password, roles);
27
38
  ctx.print(`Added local auth user ${added.username} (${formatRoles(added.roles)}).`);
@@ -49,10 +60,21 @@ export function handleLocalAuthCommand(args: string[], ctx: CommandContext): voi
49
60
  if (sub === 'rotate-password') {
50
61
  const username = args[1];
51
62
  const password = args[2];
52
- if (!username || !password) {
53
- ctx.print('Usage: /auth local rotate-password <username> <password>');
63
+ if (!username) {
64
+ ctx.print('Usage: /auth local rotate-password <username> <password>\nTip: invoke without a password to use the masked panel: /auth local rotate-password <username>');
65
+ return;
66
+ }
67
+ if (!password) {
68
+ // No password supplied — open masked-entry mode on the LocalAuthPanel.
69
+ if (ctx.openLocalAuthMaskedEntry) {
70
+ ctx.openLocalAuthMaskedEntry('rotate-password', username);
71
+ } else {
72
+ ctx.print('Masked entry unavailable in this context. Use: /auth local rotate-password <username> <password>');
73
+ }
54
74
  return;
55
75
  }
76
+ // Password supplied as argv: warn that the history entry has been scrubbed.
77
+ ctx.print('Warning: passwords passed as command arguments are scrubbed from history, but may appear in shell scrollback. The masked entry is preferred: /auth local rotate-password <username>');
56
78
  try {
57
79
  auth.rotatePassword(username, password);
58
80
  ctx.print(`Rotated password for ${username}. Existing sessions were revoked.`);
@@ -1,5 +1,6 @@
1
1
  import { dirname, join } from 'path';
2
2
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { atomicWriteFileSync } from '../../config/atomic-write.ts';
3
4
  import type { CommandRegistry } from '../command-registry.ts';
4
5
  import type { ConfigKey } from '../../config/index.ts';
5
6
  import { CONFIG_SCHEMA } from '../../config/index.ts';
@@ -204,18 +205,15 @@ export function registerLocalSetupCommands(registry: CommandRegistry): void {
204
205
  }
205
206
  if (bundle.services) {
206
207
  const servicesPath = getShellPaths().resolveProjectPath('tui', 'services.json');
207
- mkdirSync(dirname(servicesPath), { recursive: true });
208
- writeFileSync(servicesPath, JSON.stringify(bundle.services, null, 2) + '\n', 'utf-8');
208
+ atomicWriteFileSync(servicesPath, JSON.stringify(bundle.services, null, 2) + '\n', { mkdirp: true });
209
209
  }
210
210
  if (bundle.ecosystem?.plugins) {
211
211
  const pluginsPath = getShellPaths().resolveProjectPath('tui', 'ecosystem', 'plugins.json');
212
- mkdirSync(dirname(pluginsPath), { recursive: true });
213
- writeFileSync(pluginsPath, JSON.stringify(bundle.ecosystem.plugins, null, 2) + '\n', 'utf-8');
212
+ atomicWriteFileSync(pluginsPath, JSON.stringify(bundle.ecosystem.plugins, null, 2) + '\n', { mkdirp: true });
214
213
  }
215
214
  if (bundle.ecosystem?.skills) {
216
215
  const skillsPath = getShellPaths().resolveProjectPath('tui', 'ecosystem', 'skills.json');
217
- mkdirSync(dirname(skillsPath), { recursive: true });
218
- writeFileSync(skillsPath, JSON.stringify(bundle.ecosystem.skills, null, 2) + '\n', 'utf-8');
216
+ atomicWriteFileSync(skillsPath, JSON.stringify(bundle.ecosystem.skills, null, 2) + '\n', { mkdirp: true });
219
217
  }
220
218
  ctx.print(`Imported setup transfer bundle from ${targetPath}`);
221
219
  } catch (error) {
@@ -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: [],
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Implements the Provider Optimizer panel commands:
5
5
  *
6
+ * /provider optimizer on|off — Enable or disable the provider optimizer
6
7
  * /provider route auto|manual — Set optimizer routing mode
7
8
  * /provider explain-route — Print current route explanation
8
9
  * /provider pin <provider:model> — Pin routing to a specific provider/model
@@ -10,14 +11,19 @@
10
11
  *
11
12
  * When the optimizer is disabled, commands report its status and
12
13
  * explain-route still works (reads current model capabilities).
14
+ * Enabling the optimizer persists the change to config so it survives restart.
13
15
  */
14
16
 
15
17
  import type { SlashCommand, CommandContext } from '../command-registry.ts';
18
+ import type { ConfigKey } from '../../config/index.ts';
16
19
  import type { RouteExplanation } from '@pellux/goodvibes-sdk/platform/providers';
17
20
  import type { FallbackTestResult, FallbackTransition } from '@pellux/goodvibes-sdk/platform/providers';
18
21
  import type { ProviderApiModelRecord } from '@pellux/goodvibes-sdk/platform/providers';
19
22
  import { requireProviderApi } from './runtime-services.ts';
20
23
 
24
+ const PROVIDER_OPTIMIZER_FLAG = 'provider-optimizer';
25
+ const PROVIDER_OPTIMIZER_CONFIG_KEY = `featureFlags.${PROVIDER_OPTIMIZER_FLAG}` as ConfigKey;
26
+
21
27
  // ---------------------------------------------------------------------------
22
28
  // Formatting helpers
23
29
  // ---------------------------------------------------------------------------
@@ -65,6 +71,49 @@ function fmtExplanation(expl: RouteExplanation, context: CommandContext): void {
65
71
  }
66
72
  }
67
73
 
74
+ // ---------------------------------------------------------------------------
75
+ // /provider optimizer on|off
76
+ // ---------------------------------------------------------------------------
77
+
78
+ function handleOptimizerToggle(
79
+ args: string[],
80
+ context: CommandContext,
81
+ ): void {
82
+ const optimizer = requireProviderOptimizer(context);
83
+ if (!optimizer) return;
84
+ const sub = args[0];
85
+
86
+ if (sub !== 'on' && sub !== 'off') {
87
+ context.print('[provider] Usage: /provider optimizer on|off');
88
+ context.print(` Current state: optimizer is ${optimizer.enabled ? 'enabled' : 'disabled'}`);
89
+ context.print(' "on" — activates intelligent failover and auto-routing');
90
+ context.print(' "off" — disables optimizer; provider selection is manual only');
91
+ return;
92
+ }
93
+
94
+ const enable = sub === 'on';
95
+ const wasEnabled = optimizer.enabled;
96
+ optimizer.setEnabled(enable);
97
+
98
+ // Persist to config so the setting survives restart.
99
+ const flagValue = enable ? 'enabled' : 'disabled';
100
+ context.platform.configManager.setDynamic(PROVIDER_OPTIMIZER_CONFIG_KEY, flagValue);
101
+
102
+ if (enable && !wasEnabled) {
103
+ context.print('[provider] Optimizer enabled.');
104
+ context.print(' Intelligent failover is now active: on a request error the optimizer');
105
+ context.print(' will attempt the next viable provider and surface a transcript notice');
106
+ context.print(' naming the from→to transition and reason before retrying.');
107
+ context.print(' Use "/provider route auto" to enable fully automatic routing.');
108
+ } else if (!enable && wasEnabled) {
109
+ context.print('[provider] Optimizer disabled.');
110
+ context.print(' Provider selection returns to manual-only mode. No automatic failover.');
111
+ context.print(' Pinned targets and fallback log are preserved; re-enable to resume.');
112
+ } else {
113
+ context.print(`[provider] Optimizer already ${enable ? 'enabled' : 'disabled'} — no change.`);
114
+ }
115
+ }
116
+
68
117
  // ---------------------------------------------------------------------------
69
118
  // /provider route auto|manual
70
119
  // ---------------------------------------------------------------------------
@@ -85,9 +134,9 @@ function handleRoute(
85
134
 
86
135
  if (!optimizer.enabled) {
87
136
  context.print(
88
- '[provider] Optimizer is currently disabled. Enable it with the provider-optimizer feature flag.',
137
+ '[provider] Optimizer is off routing mode recorded but failover will not fire until optimizer is enabled.',
89
138
  );
90
- context.print(` Routing mode set to: ${sub} (no-op until optimizer is enabled)`);
139
+ context.print(' Enable with: /provider optimizer on');
91
140
  }
92
141
 
93
142
  optimizer.setMode(sub);
@@ -318,11 +367,15 @@ export const providerCommand: SlashCommand = {
318
367
  aliases: ['prov-opt'],
319
368
  description: 'Manage provider routing optimizer (route, pin, explain, fallback).',
320
369
  usage: '<subcommand> [args]',
321
- argsHint: 'route|explain-route|pin|fallback',
370
+ argsHint: 'optimizer|route|explain-route|pin|fallback',
322
371
  handler: async (args: string[], context: CommandContext): Promise<void> => {
323
372
  const [sub, ...rest] = args;
324
373
 
325
374
  switch (sub) {
375
+ case 'optimizer':
376
+ handleOptimizerToggle(rest, context);
377
+ break;
378
+
326
379
  case 'route':
327
380
  handleRoute(rest, context);
328
381
  break;
@@ -345,6 +398,7 @@ export const providerCommand: SlashCommand = {
345
398
  if (!optimizer) return;
346
399
  const lines = [
347
400
  'Usage: /provider <subcommand>',
401
+ ' optimizer on|off — Enable or disable the provider optimizer',
348
402
  ' route auto|manual — Set optimizer routing mode',
349
403
  ' explain-route — Show current route explanation',
350
404
  ' pin <provider:model> — Pin routing to specific provider/model',
@@ -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)}`);
@@ -1,6 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
 
3
- import type { CommandContext, CommandRegistry } from '../command-registry.ts';
3
+ import type { CommandContext } from '../command-registry.ts';
4
4
  import { type SessionMeta } from '@pellux/goodvibes-sdk/platform/sessions';
5
5
  import type { TranscriptEventKind } from '@pellux/goodvibes-sdk/platform/core';
6
6
  import type { ConversationTitleSource } from '../../core/conversation';
@@ -441,21 +441,13 @@ export async function handleSessionWorkflowCommand(args: string[], ctx: CommandC
441
441
  return false;
442
442
  }
443
443
 
444
- export function registerSessionWorkflowCommands(registry: CommandRegistry): void {
445
- registry.register({
446
- name: 'session',
447
- aliases: ['sess'],
448
- description: 'Manage sessions, resume posture, and transcript structure',
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
- argsHint: '<list|rename|resume|fork|save|info|events|groups|hotspots|export|search|delete>',
451
- async handler(args, ctx) {
452
- const handled = await handleSessionWorkflowCommand(args, ctx);
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>]');
455
- }
456
- },
457
- });
458
- }
444
+ // session-mgmt / smgmt was removed in TASK-032.
445
+ // All session lifecycle operations are now first-class subcommands of /session.
446
+ // Use /session list, /session resume, /session save, etc.
447
+ //
448
+ // CommandRegistry.register() throws on duplicate names/aliases, so this
449
+ // registration was intentionally deleted rather than left as dead code.
450
+
459
451
  interface SessionExportData {
460
452
  readonly messages: object[];
461
453
  readonly timestamp?: number;
@@ -319,18 +319,52 @@ function handleCancel(args: string[], context: CommandContext): void {
319
319
  /**
320
320
  * sessionCommand — The `/session` slash command.
321
321
  *
322
- * Routes to multi-session orchestration subcommand handlers based on args[0].
322
+ * The ONE front-door for all session operations. Owns two domains:
323
+ *
324
+ * Lifecycle (continuity, export, resume, pruning):
325
+ * list | rename | resume | fork | save | info | events | groups | hotspots | export | search | delete
326
+ *
327
+ * Orchestration (cross-session task DAG — 40 tests, cycle detection):
328
+ * link-task | handoff | graph | cancel
329
+ *
330
+ * Orchestration-command decision (TASK-032):
331
+ * Both domains live under /session rather than splitting orchestration into
332
+ * a separate /session-orch command. Rationale: they share the same entity
333
+ * (a session) and the same operator mental model ("I am working with sessions").
334
+ * A second front-door would create ambiguity about which command to reach for.
335
+ * Explicit switch routing (not fallthrough) makes both domains first-class;
336
+ * the former /session-mgmt alias (session-mgmt/smgmt) is removed so there
337
+ * is exactly one registration and no silent shadowing.
323
338
  */
324
339
  export const sessionCommand: SlashCommand = {
325
340
  name: 'session',
326
341
  aliases: ['sess'],
327
- description: 'Multi-session orchestration: link tasks, handoff, view graph, and cancel across sessions.',
342
+ description: 'Session lifecycle and orchestration: list, resume, fork, save, export, link-task, handoff, graph, cancel.',
328
343
  usage: '<subcommand> [args]',
329
- argsHint: 'link-task|handoff|graph|cancel',
344
+ argsHint: 'list|rename|resume|fork|save|info|export|search|delete|events|groups|hotspots|link-task|handoff|graph|cancel',
330
345
  handler: async (args: string[], context: CommandContext): Promise<void> => {
331
346
  const [sub, ...rest] = args;
332
347
 
333
348
  switch (sub) {
349
+ // ── Lifecycle subcommands ────────────────────────────────────────────────
350
+ // Each delegates explicitly to handleSessionWorkflowCommand so every
351
+ // subcommand has a deterministic, named path — no silent fallthrough.
352
+ case 'list':
353
+ case 'rename':
354
+ case 'resume':
355
+ case 'fork':
356
+ case 'save':
357
+ case 'info':
358
+ case 'export':
359
+ case 'search':
360
+ case 'delete':
361
+ case 'events':
362
+ case 'groups':
363
+ case 'hotspots':
364
+ await handleSessionWorkflowCommand(args, context);
365
+ break;
366
+
367
+ // ── Orchestration subcommands ─────────────────────────────────────────────
334
368
  case 'link-task':
335
369
  case 'link':
336
370
  handleLinkTask(rest, context);
@@ -350,24 +384,40 @@ export const sessionCommand: SlashCommand = {
350
384
  handleCancel(rest, context);
351
385
  break;
352
386
 
387
+ // ── No-arg: show current session info ────────────────────────────────────
388
+ case undefined:
389
+ await handleSessionWorkflowCommand([], context);
390
+ break;
391
+
353
392
  default: {
354
- const handled = await handleSessionWorkflowCommand(args, context);
355
- if (!handled) {
356
- const usage = [
357
- 'Usage: /session <subcommand>',
358
- ' list | rename <name> | resume <id|name> | fork [name] | save [name] | info [id] | export <id> [format] | search <query> | delete <id>',
359
- ' Session continuity, export, resume, and pruning',
360
- ' link-task <taskId> [--session <sid>] [--depends-on <sid:taskId>] [--label <label>]',
361
- ' Register a task in the cross-session graph',
362
- ' handoff <taskId> --to <sid> [--session <sid>] [--reason <reason>]',
363
- ' Hand a task off to another session',
364
- ' graph [--session <sid>] [--format text|json]',
365
- ' Display the cross-session task dependency graph',
366
- ' cancel <taskId> [--scope task|subtree|session] [--session <sid>] [--reason <reason>]',
367
- ' Cancel tasks with scoped semantics',
368
- ].join('\n');
369
- context.print(usage);
370
- }
393
+ const usage = [
394
+ 'Usage: /session <subcommand>',
395
+ '',
396
+ 'Lifecycle:',
397
+ ' list List saved sessions',
398
+ ' rename <name> Rename the current session',
399
+ ' resume <id|name> Resume a saved session',
400
+ ' fork [name] Fork the current session',
401
+ ' save [name] — Save the current session',
402
+ ' info [id] Show session info',
403
+ ' export <id|.> [markdown|text] — Export session transcript',
404
+ ' search <query> Search session content',
405
+ ' delete <id> Delete a saved session',
406
+ ' events [kind] Show transcript events',
407
+ ' groups [kind] — Show transcript groups',
408
+ ' hotspots — Show transcript hotspots',
409
+ '',
410
+ 'Orchestration:',
411
+ ' link-task <taskId> [--session <sid>] [--depends-on <sid:taskId>] [--label <label>]',
412
+ ' — Register a task in the cross-session graph',
413
+ ' handoff <taskId> --to <sid> [--session <sid>] [--reason <reason>]',
414
+ ' — Hand a task off to another session',
415
+ ' graph [--session <sid>] [--format text|json]',
416
+ ' — Display the cross-session task dependency graph',
417
+ ' cancel <taskId> [--scope task|subtree|session] [--session <sid>] [--reason <reason>]',
418
+ ' — Cancel tasks with scoped semantics',
419
+ ].join('\n');
420
+ context.print(usage);
371
421
  break;
372
422
  }
373
423
  }