@pellux/goodvibes-agent 1.5.5 → 1.5.6

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 (165) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -1
  3. package/dist/package/main.js +94130 -79335
  4. package/docs/tools-and-commands.md +2 -1
  5. package/package.json +6 -3
  6. package/src/agent/autonomy-schedule-format.ts +1 -1
  7. package/src/agent/autonomy-schedule.ts +3 -3
  8. package/src/agent/calendar/calendar-oauth-service.ts +324 -0
  9. package/src/agent/calendar-subscription-registry.ts +497 -0
  10. package/src/agent/email/email-service.ts +57 -0
  11. package/src/agent/email/smtp-client.ts +30 -10
  12. package/src/agent/memory-prompt.ts +143 -12
  13. package/src/agent/operator-actions.ts +26 -22
  14. package/src/agent/prompt-context-receipts.ts +45 -18
  15. package/src/agent/reminder-schedule-format.ts +1 -1
  16. package/src/agent/reminder-schedule.ts +3 -3
  17. package/src/agent/routine-schedule-format.ts +2 -2
  18. package/src/agent/routine-schedule-promotion.ts +5 -5
  19. package/src/agent/routine-schedule-receipts.ts +2 -2
  20. package/src/agent/schedule-edit-format.ts +1 -1
  21. package/src/agent/schedule-edit.ts +5 -5
  22. package/src/agent/session-registration.ts +281 -0
  23. package/src/agent/vibe-file.ts +50 -0
  24. package/src/cli/local-library-command-shared.ts +146 -0
  25. package/src/cli/management.ts +2 -1
  26. package/src/cli/personas-command.ts +252 -0
  27. package/src/cli/resume-relaunch-notice.ts +151 -0
  28. package/src/cli/routines-command.ts +25 -2
  29. package/src/cli/service-posture.ts +3 -2
  30. package/src/cli/skill-bundle-command.ts +175 -0
  31. package/src/cli/skills-command.ts +309 -0
  32. package/src/cli/status.ts +43 -7
  33. package/src/cli/tui-startup.ts +24 -1
  34. package/src/config/agent-settings-policy.ts +0 -1
  35. package/src/config/secret-config.ts +4 -0
  36. package/src/core/conversation-rendering.ts +20 -11
  37. package/src/core/focus-tracker.ts +41 -0
  38. package/src/core/system-message-noise.ts +108 -0
  39. package/src/core/system-message-router.ts +54 -1
  40. package/src/core/thinking-overlay.ts +83 -0
  41. package/src/input/agent-workspace-access-command-editor-submission.ts +75 -108
  42. package/src/input/agent-workspace-access-command-editors.ts +108 -128
  43. package/src/input/agent-workspace-activation.ts +15 -0
  44. package/src/input/agent-workspace-basic-command-editor-submission.ts +292 -513
  45. package/src/input/agent-workspace-basic-command-editors.ts +433 -564
  46. package/src/input/agent-workspace-calendar-connect-editor.ts +116 -0
  47. package/src/input/agent-workspace-calendar-oauth-editor.ts +152 -0
  48. package/src/input/agent-workspace-calendar-subscribe-editor.ts +128 -0
  49. package/src/input/agent-workspace-categories.ts +7 -2
  50. package/src/input/agent-workspace-channel-command-editor-submission.ts +38 -52
  51. package/src/input/agent-workspace-channel-command-editors.ts +42 -46
  52. package/src/input/agent-workspace-command-editor-engine.ts +133 -0
  53. package/src/input/agent-workspace-command-editor.ts +4 -0
  54. package/src/input/agent-workspace-direct-editor-submission.ts +59 -0
  55. package/src/input/agent-workspace-email-connect-editor.ts +162 -0
  56. package/src/input/agent-workspace-knowledge-command-editor-submission.ts +52 -81
  57. package/src/input/agent-workspace-knowledge-command-editors.ts +71 -71
  58. package/src/input/agent-workspace-library-command-editor-submission.ts +10 -28
  59. package/src/input/agent-workspace-library-command-editors.ts +16 -2
  60. package/src/input/agent-workspace-live-counters.ts +55 -0
  61. package/src/input/agent-workspace-mcp-command-editor-submission.ts +60 -82
  62. package/src/input/agent-workspace-mcp-command-editors.ts +52 -0
  63. package/src/input/agent-workspace-media-command-editor-submission.ts +12 -39
  64. package/src/input/agent-workspace-media-command-editors.ts +10 -8
  65. package/src/input/agent-workspace-memory-command-editor-submission.ts +76 -151
  66. package/src/input/agent-workspace-memory-command-editors.ts +116 -141
  67. package/src/input/agent-workspace-operations-command-editor-submission.ts +131 -184
  68. package/src/input/agent-workspace-operations-command-editors.ts +150 -162
  69. package/src/input/agent-workspace-provider-command-editor-submission.ts +60 -106
  70. package/src/input/agent-workspace-provider-command-editors.ts +58 -68
  71. package/src/input/agent-workspace-search.ts +8 -1
  72. package/src/input/agent-workspace-session-command-editor-submission.ts +104 -132
  73. package/src/input/agent-workspace-session-command-editors.ts +160 -195
  74. package/src/input/agent-workspace-settings.ts +40 -1
  75. package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +55 -71
  76. package/src/input/agent-workspace-skill-bundle-command-editors.ts +65 -69
  77. package/src/input/agent-workspace-snapshot-builders.ts +431 -0
  78. package/src/input/agent-workspace-snapshot-config.ts +43 -0
  79. package/src/input/agent-workspace-snapshot.ts +198 -432
  80. package/src/input/agent-workspace-task-command-editor-submission.ts +34 -44
  81. package/src/input/agent-workspace-task-command-editors.ts +27 -28
  82. package/src/input/agent-workspace-types.ts +35 -1
  83. package/src/input/agent-workspace.ts +5 -14
  84. package/src/input/command-registry.ts +23 -1
  85. package/src/input/commands/calendar-connect-runtime.ts +226 -0
  86. package/src/input/commands/calendar-runtime.ts +110 -7
  87. package/src/input/commands/calendar-subscription-runtime.ts +225 -0
  88. package/src/input/commands/email-runtime.ts +100 -40
  89. package/src/input/commands/knowledge.ts +1 -1
  90. package/src/input/commands/network-scan-runtime.ts +75 -0
  91. package/src/input/commands/operator-actions-runtime.ts +9 -6
  92. package/src/input/commands/personas-runtime.ts +1 -1
  93. package/src/input/commands/schedule-runtime.ts +4 -4
  94. package/src/input/commands/session-content.ts +13 -1
  95. package/src/input/commands/session-workflow.ts +15 -20
  96. package/src/input/commands/session.ts +3 -1
  97. package/src/input/commands/shell-core.ts +20 -6
  98. package/src/input/commands.ts +2 -2
  99. package/src/input/delete-key-policy.ts +46 -0
  100. package/src/input/feed-context-factory.ts +10 -0
  101. package/src/input/handler-feed.ts +87 -0
  102. package/src/input/handler-modal-routes.ts +6 -1
  103. package/src/input/handler.ts +5 -0
  104. package/src/input/panel-paste-flood-guard.ts +94 -0
  105. package/src/input/settings-modal-types.ts +5 -3
  106. package/src/input/settings-modal.ts +21 -0
  107. package/src/main.ts +36 -36
  108. package/src/permissions/approval-posture.ts +141 -0
  109. package/src/renderer/agent-workspace-context-lines.ts +10 -2
  110. package/src/renderer/compositor.ts +27 -4
  111. package/src/renderer/diff.ts +61 -18
  112. package/src/renderer/fullscreen-primitives.ts +37 -18
  113. package/src/renderer/markdown.ts +20 -10
  114. package/src/renderer/modal-factory.ts +25 -15
  115. package/src/renderer/overlay-box.ts +23 -12
  116. package/src/renderer/process-indicator.ts +8 -3
  117. package/src/renderer/prompt-content-width.ts +16 -0
  118. package/src/renderer/settings-modal-helpers.ts +2 -0
  119. package/src/renderer/settings-modal.ts +2 -0
  120. package/src/renderer/startup-theme-probe.ts +35 -0
  121. package/src/renderer/status-glyphs.ts +11 -15
  122. package/src/renderer/system-message.ts +17 -2
  123. package/src/renderer/term-caps.ts +318 -0
  124. package/src/renderer/terminal-bg-probe.ts +373 -0
  125. package/src/renderer/terminal-escapes.ts +24 -0
  126. package/src/renderer/theme-mode-config.ts +87 -0
  127. package/src/renderer/theme.ts +241 -0
  128. package/src/renderer/thinking.ts +12 -3
  129. package/src/renderer/tool-call.ts +7 -3
  130. package/src/renderer/ui-factory.ts +92 -36
  131. package/src/renderer/ui-primitives.ts +33 -93
  132. package/src/runtime/bootstrap-core.ts +15 -0
  133. package/src/runtime/bootstrap-hook-bridge.ts +6 -0
  134. package/src/runtime/bootstrap-shell.ts +2 -0
  135. package/src/runtime/bootstrap.ts +68 -5
  136. package/src/runtime/calendar-boot-refresh.ts +105 -0
  137. package/src/runtime/lan-scan-consent.ts +253 -0
  138. package/src/runtime/services.ts +127 -2
  139. package/src/runtime/session-spine-rest-transport.ts +160 -0
  140. package/src/runtime/terminal-output-guard.ts +6 -1
  141. package/src/runtime/ui-services.ts +3 -0
  142. package/src/shell/agent-workspace-fullscreen.ts +5 -0
  143. package/src/shell/terminal-focus-mode.ts +120 -0
  144. package/src/shell/ui-openers.ts +13 -4
  145. package/src/tools/agent-harness-autonomy-intake.ts +5 -5
  146. package/src/tools/agent-harness-autonomy-queue.ts +1 -1
  147. package/src/tools/agent-harness-background-processes.ts +2 -1
  148. package/src/tools/agent-harness-metadata.ts +16 -8
  149. package/src/tools/agent-harness-operator-methods.ts +44 -8
  150. package/src/tools/agent-harness-personal-ops-discovery.ts +57 -10
  151. package/src/tools/agent-harness-personal-ops-lanes.ts +17 -5
  152. package/src/tools/agent-harness-personal-ops-operations.ts +7 -7
  153. package/src/tools/agent-harness-personal-ops-types.ts +6 -0
  154. package/src/tools/agent-harness-prompt-context.ts +26 -12
  155. package/src/tools/agent-harness-workspace-actions.ts +4 -0
  156. package/src/tools/agent-local-registry-args.ts +117 -0
  157. package/src/tools/agent-local-registry-memory.ts +227 -0
  158. package/src/tools/agent-local-registry-tool.ts +19 -237
  159. package/src/tools/agent-operator-briefing-tool.ts +2 -2
  160. package/src/tools/agent-operator-method-tool.ts +13 -0
  161. package/src/tools/agent-policy-explanation.ts +39 -4
  162. package/src/tools/agent-schedule-tool.ts +5 -5
  163. package/src/utils/terminal-width.ts +98 -1
  164. package/src/version.ts +1 -1
  165. package/src/cli/local-library-command.ts +0 -825
@@ -93,14 +93,17 @@ function automationActionFromParts(scope: string, verb: string): {
93
93
  readonly targetField: 'jobId' | 'runId' | 'scheduleId';
94
94
  } | null {
95
95
  if ((scope === 'job' || scope === 'jobs') && verb === 'run') return { action: 'automation.jobs.run', targetField: 'jobId' };
96
- if ((scope === 'job' || scope === 'jobs') && verb === 'pause') return { action: 'automation.jobs.pause', targetField: 'jobId' };
97
- if ((scope === 'job' || scope === 'jobs') && verb === 'resume') return { action: 'automation.jobs.resume', targetField: 'jobId' };
96
+ // W6-C3: automation.jobs.pause/resume retired (redundant with
97
+ // disable/enable) the user-facing "pause"/"resume" verb is unchanged,
98
+ // it now maps onto the canonical disable/enable wire actions.
99
+ if ((scope === 'job' || scope === 'jobs') && verb === 'pause') return { action: 'automation.jobs.disable', targetField: 'jobId' };
100
+ if ((scope === 'job' || scope === 'jobs') && verb === 'resume') return { action: 'automation.jobs.enable', targetField: 'jobId' };
98
101
  if ((scope === 'run' || scope === 'runs') && verb === 'cancel') return { action: 'automation.runs.cancel', targetField: 'runId' };
99
102
  if ((scope === 'run' || scope === 'runs') && verb === 'retry') return { action: 'automation.runs.retry', targetField: 'runId' };
100
- if ((scope === 'schedule' || scope === 'schedules') && verb === 'run') return { action: 'schedules.run', targetField: 'scheduleId' };
101
- if ((scope === 'schedule' || scope === 'schedules') && verb === 'enable') return { action: 'schedules.enable', targetField: 'scheduleId' };
102
- if ((scope === 'schedule' || scope === 'schedules') && verb === 'disable') return { action: 'schedules.disable', targetField: 'scheduleId' };
103
- if ((scope === 'schedule' || scope === 'schedules') && (verb === 'delete' || verb === 'remove')) return { action: 'schedules.delete', targetField: 'scheduleId' };
103
+ if ((scope === 'schedule' || scope === 'schedules') && verb === 'run') return { action: 'automation.schedules.run', targetField: 'scheduleId' };
104
+ if ((scope === 'schedule' || scope === 'schedules') && verb === 'enable') return { action: 'automation.schedules.enable', targetField: 'scheduleId' };
105
+ if ((scope === 'schedule' || scope === 'schedules') && verb === 'disable') return { action: 'automation.schedules.disable', targetField: 'scheduleId' };
106
+ if ((scope === 'schedule' || scope === 'schedules') && (verb === 'delete' || verb === 'remove')) return { action: 'automation.schedules.delete', targetField: 'scheduleId' };
104
107
  return null;
105
108
  }
106
109
 
@@ -41,7 +41,7 @@ function summarizePersona(persona: AgentPersonaRecord, activePersonaId: string |
41
41
  function renderList(title: string, registry: AgentPersonaRegistry, personas: readonly AgentPersonaRecord[]): string {
42
42
  const snapshot = registry.snapshot();
43
43
  if (personas.length === 0) {
44
- return `${title}\n No local Agent personas yet.\n No Agent-local personas yet. Create one with /personas create --name <name> --description <summary> --body <instructions>.`;
44
+ return `${title}\n No local Agent personas yet. Create one with /personas create --name <name> --description <summary> --body <instructions>.`;
45
45
  }
46
46
  return [
47
47
  `${title} (${personas.length})`,
@@ -216,12 +216,12 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
216
216
  return;
217
217
  }
218
218
  const action = sub === 'run'
219
- ? 'schedules.run'
219
+ ? 'automation.schedules.run'
220
220
  : sub === 'enable'
221
- ? 'schedules.enable'
221
+ ? 'automation.schedules.enable'
222
222
  : sub === 'disable'
223
- ? 'schedules.disable'
224
- : 'schedules.delete';
223
+ ? 'automation.schedules.disable'
224
+ : 'automation.schedules.delete';
225
225
  await executeConfirmedOperatorAction(ctx, action, 'scheduleId', scheduleId, args.slice(2), `/schedule ${sub} <schedule-id> --yes`);
226
226
  return;
227
227
  }
@@ -6,6 +6,7 @@ import type { SelectionItem } from '../selection-modal.ts';
6
6
  import type { SessionMeta } from '@pellux/goodvibes-sdk/platform/sessions';
7
7
  import { exportToMarkdown, extractText } from '@pellux/goodvibes-sdk/platform/export';
8
8
  import { requireSessionManager, requireShellPaths } from './runtime-services.ts';
9
+ import { sessionCommand } from './session.ts';
9
10
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
10
11
  import { requireYesFlag, stripYesFlag } from './confirmation.ts';
11
12
  import { readConversationMessageSnapshots } from '../../core/conversation-message-snapshot.ts';
@@ -250,8 +251,19 @@ export function registerSessionContentCommands(registry: CommandRegistry): void
250
251
  registry.register({
251
252
  name: 'sessions',
252
253
  description: 'List saved sessions',
253
- hidden: true,
254
+ usage: '[resume <id|name>]',
255
+ argsHint: '[resume <id|name>]',
254
256
  async handler(_args, ctx) {
257
+ // Matches the TUI's /sessions (W6-C3, worst-class collision #5): this
258
+ // was `hidden: true` here — invisible in help/autocomplete — even
259
+ // though it is a fully working command identical in behavior to the
260
+ // TUI's visible one. Also forward args to /session (the TUI's fix for
261
+ // the `/sessions resume <id>` muscle-memory case), instead of silently
262
+ // listing and dropping the subcommand+id on the floor.
263
+ if (_args.length > 0) {
264
+ await sessionCommand.handler(_args, ctx);
265
+ return;
266
+ }
255
267
  const sessionManager = requireSessionManager(ctx);
256
268
  const sessions = sessionManager.list();
257
269
  if (ctx.openSelection) {
@@ -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 { SessionReturnContextSummary } from '@/runtime/index.ts';
@@ -479,22 +479,17 @@ export async function handleSessionWorkflowCommand(args: string[], ctx: CommandC
479
479
  return false;
480
480
  }
481
481
 
482
- export function registerSessionWorkflowCommands(registry: CommandRegistry): void {
483
- registry.register({
484
- name: 'session',
485
- aliases: ['sess'],
486
- description: 'Manage sessions, resume posture, and transcript structure',
487
- usage: '[list | rename <name> | resume <id|name> | fork | save | info <id> | events [kind] | groups [kind] | hotspots | export <id> [format] | search <query> | delete <id> --yes]',
488
- argsHint: '<list|rename|resume|fork|save|info|events|groups|hotspots|export|search|delete>',
489
- async handler(args, ctx) {
490
- const handled = await handleSessionWorkflowCommand(args, ctx);
491
- if (!handled) {
492
- ctx.print([
493
- 'Unknown session subcommand',
494
- ` subcommand ${args[0] ?? ''}`,
495
- 'Usage: /session [list | rename <name> | resume <id> | fork [name] | save [name] | info [id] | events [kind] | groups [kind] | hotspots | export <id> [format] | search <query> | delete <id> --yes]',
496
- ].join('\n'));
497
- }
498
- },
499
- });
500
- }
482
+ // NOTE (W6-C3, Wave 6 core-verb pass): this file used to also export
483
+ // registerSessionWorkflowCommands(), a second top-level `/session` (alias
484
+ // `sess`) registration with its own usage text. It was NEVER called from
485
+ // startup — commands.ts only registers `sessionCommand` from ./session.ts —
486
+ // so it was dead code (would throw a CommandRegistry collision error if
487
+ // anyone ever did call it, since sessionCommand already owns the name). It
488
+ // has been deleted; there is no functional change, because sessionCommand's
489
+ // own default-branch fallback already calls handleSessionWorkflowCommand
490
+ // above for every subcommand this dead registrar advertised, including
491
+ // events/groups/hotspots — see ./session.ts's usage text, which now documents
492
+ // them too. See docs/decisions/2026-07-06-core-verb-spec.md (agent /session
493
+ // orphan, worst-class collision #4) and
494
+ // src/test/input/session-single-registration.test.ts for the regression
495
+ // guard.
@@ -152,7 +152,7 @@ export const sessionCommand: SlashCommand = {
152
152
  aliases: ['sess'],
153
153
  description: 'Session continuity and read-only cross-session graph inspection.',
154
154
  usage: '<subcommand> [args]',
155
- argsHint: 'list|resume|save|graph',
155
+ argsHint: 'list|resume|save|events|groups|hotspots|graph',
156
156
  handler: async (args: string[], context: CommandContext): Promise<void> => {
157
157
  const [sub, ...rest] = args;
158
158
 
@@ -183,6 +183,8 @@ export const sessionCommand: SlashCommand = {
183
183
  'Usage: /session <subcommand>',
184
184
  ' list | rename <name> | resume <id|name> | fork [name] | save [name] | info [id] | export <id> [format] | search <query> | delete <id> --yes',
185
185
  ' — Session continuity, export, resume, and pruning',
186
+ ' events [kind] | groups [kind] | hotspots',
187
+ ' — Transcript structure: event log, grouped view, hotspot summary',
186
188
  ' graph [--session <sid>] [--format text|json]',
187
189
  ' — Display the cross-session task dependency graph',
188
190
  ' link-task | handoff | cancel',
@@ -99,9 +99,18 @@ function commandDetail(command: SlashCommand): string {
99
99
  return parts.join(' | ');
100
100
  }
101
101
 
102
- function listRegisteredCommandItems(registry: CommandRegistry): SelectionItem[] {
102
+ /**
103
+ * List registered commands as selection items.
104
+ *
105
+ * Hidden commands (`SlashCommand.hidden`) still run when typed and stay
106
+ * discoverable via `/commands`, which lists everything labeled "(hidden)".
107
+ * `/help` (both the interactive picker and the plain-text fallback) filters
108
+ * them out per the doc contract on `SlashCommand.hidden`.
109
+ */
110
+ function listRegisteredCommandItems(registry: CommandRegistry, options: { includeHidden: boolean }): SelectionItem[] {
103
111
  return registry.list()
104
112
  .slice()
113
+ .filter((command) => options.includeHidden || !command.hidden)
105
114
  .sort((a, b) => a.name.localeCompare(b.name))
106
115
  .map((command) => ({
107
116
  id: `/${command.name}`,
@@ -114,7 +123,7 @@ function listRegisteredCommandItems(registry: CommandRegistry): SelectionItem[]
114
123
  }
115
124
 
116
125
  function registeredCommandListText(registry: CommandRegistry): string {
117
- const commands = listRegisteredCommandItems(registry);
126
+ const commands = listRegisteredCommandItems(registry, { includeHidden: false });
118
127
  return [
119
128
  'Open the Agent workspace first, then press / inside it to search every product action.',
120
129
  '',
@@ -123,8 +132,13 @@ function registeredCommandListText(registry: CommandRegistry): string {
123
132
  ].join('\n');
124
133
  }
125
134
 
126
- function openRegisteredCommandSelection(registry: CommandRegistry, ctx: CommandContext): void {
127
- ctx.openSelection?.('Help - Commands', listRegisteredCommandItems(registry), { allowSearch: true }, (result) => {
135
+ function openRegisteredCommandSelection(
136
+ registry: CommandRegistry,
137
+ ctx: CommandContext,
138
+ options: { includeHidden: boolean },
139
+ ): void {
140
+ const items = listRegisteredCommandItems(registry, options);
141
+ ctx.openSelection?.('Help - Commands', items, { allowSearch: true }, (result) => {
128
142
  if (!result) return;
129
143
  const command = result.item.id;
130
144
  if (command.startsWith('/')) {
@@ -184,7 +198,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
184
198
  description: 'Browse all commands in a scrollable list',
185
199
  handler(_args, ctx) {
186
200
  if (ctx.openSelection) {
187
- openRegisteredCommandSelection(registry, ctx);
201
+ openRegisteredCommandSelection(registry, ctx, { includeHidden: true });
188
202
  return;
189
203
  }
190
204
  if (ctx.openHelpOverlay) {
@@ -256,7 +270,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
256
270
  argsHint: '[command]',
257
271
  handler(_args, ctx) {
258
272
  if (ctx.openSelection) {
259
- openRegisteredCommandSelection(registry, ctx);
273
+ openRegisteredCommandSelection(registry, ctx, { includeHidden: false });
260
274
  return;
261
275
  }
262
276
  ctx.print(registeredCommandListText(registry));
@@ -5,7 +5,6 @@ import { recallCommand } from './commands/memory.ts';
5
5
  import { knowledgeCommand } from './commands/knowledge.ts';
6
6
  import { registerShellCoreCommands } from './commands/shell-core.ts';
7
7
  import { registerConfigCommand } from './commands/config.ts';
8
- import { registerSessionWorkflowCommands } from './commands/session-workflow.ts';
9
8
  import { registerScheduleRuntimeCommands } from './commands/schedule-runtime.ts';
10
9
  import { registerOperatorRuntimeCommands } from './commands/operator-runtime.ts';
11
10
  import { registerNotifyRuntimeCommands } from './commands/notify-runtime.ts';
@@ -20,6 +19,7 @@ import { registerLocalRuntimeCommands } from './commands/local-runtime.ts';
20
19
  import { registerExperienceRuntimeCommands } from './commands/experience-runtime.ts';
21
20
  import { registerTasksRuntimeCommands } from './commands/tasks-runtime.ts';
22
21
  import { registerLocalProviderRuntimeCommands } from './commands/local-provider-runtime.ts';
22
+ import { registerNetworkScanRuntimeCommands } from './commands/network-scan-runtime.ts';
23
23
  import { registerHealthRuntimeCommands } from './commands/health-runtime.ts';
24
24
  import { registerProviderAccountsRuntimeCommands } from './commands/provider-accounts-runtime.ts';
25
25
  import { registerConversationRuntimeCommands } from './commands/conversation-runtime.ts';
@@ -86,6 +86,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
86
86
  registerExperienceRuntimeCommands(registry);
87
87
  registerTasksRuntimeCommands(registry);
88
88
  registerLocalProviderRuntimeCommands(registry);
89
+ registerNetworkScanRuntimeCommands(registry);
89
90
  registerHealthRuntimeCommands(registry);
90
91
  registerProviderAccountsRuntimeCommands(registry);
91
92
  registerConversationRuntimeCommands(registry);
@@ -93,7 +94,6 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
93
94
  registerOnboardingRuntimeCommands(registry);
94
95
  registerTtsRuntimeCommands(registry);
95
96
  registerLocalRuntimeCommands(registry);
96
- registerSessionWorkflowCommands(registry);
97
97
  registerScheduleRuntimeCommands(registry);
98
98
  registerSessionContentCommands(registry);
99
99
  registerAgentMemoryCommand(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
+ }
@@ -35,6 +35,8 @@ import type { ProfilePickerModal } from './profile-picker-modal.ts';
35
35
  import type { WrappedPromptInfo } from './handler-prompt-buffer.ts';
36
36
  import type { KeybindingsManager } from './keybindings.ts';
37
37
  import type { ModelPickerTarget } from './model-picker.ts';
38
+ import type { PanelBurstGuardState } from './panel-paste-flood-guard.ts';
39
+ import type { FocusTracker } from '../core/focus-tracker.ts';
38
40
 
39
41
  /**
40
42
  * Initial mutable scalar values for InputFeedContext.
@@ -93,6 +95,10 @@ export interface FeedContextStableRefs {
93
95
  selection: SelectionManager;
94
96
  pasteRegistry: Map<string, string>;
95
97
  imageRegistry: Map<string, { data: string; mediaType: string }>;
98
+ /** W4-R3 — ported from goodvibes-tui's DEBT-5 item 5; mutated in place, never reallocated. */
99
+ burstGuard: PanelBurstGuardState;
100
+ /** W4-R3 — OS-level terminal focus tracker, ported from goodvibes-tui's W2.3. */
101
+ focusTracker: FocusTracker;
96
102
  projectRoot: string;
97
103
  selectionModal: SelectionModal;
98
104
  bookmarkModal: BookmarkModal;
@@ -176,6 +182,10 @@ export function buildInitialFeedContext(
176
182
  ...mutable,
177
183
  // --- requestRender: placeholder, swapped per-feed to buffered version ---
178
184
  requestRender: noop,
185
+ // W4-R3: wiring-layer-only bookkeeping for the paste-flood guard's honest
186
+ // resolution notice (not part of the ported panel-paste-flood-guard.ts
187
+ // module itself; see handler-feed.ts's feedInputTokens).
188
+ burstSuppressedCount: 0,
179
189
  // --- stable refs ---
180
190
  ...stable,
181
191
  // --- closures ---
@@ -37,6 +37,8 @@ import { handleGlobalShortcutToken } from './handler-shortcuts.ts';
37
37
  import { SelectionManager } from './selection.ts';
38
38
  import type { KeybindingsManager } from './keybindings.ts';
39
39
  import type { ModelPickerTarget } from './model-picker.ts';
40
+ import { trackPanelPasteFloodGuard, type PanelBurstGuardState } from './panel-paste-flood-guard.ts';
41
+ import type { FocusTracker } from '../core/focus-tracker.ts';
40
42
 
41
43
  /**
42
44
  * InputFeedContext — The single long-lived context object passed to feedInputTokens
@@ -62,6 +64,16 @@ import type { ModelPickerTarget } from './model-picker.ts';
62
64
  * - `inputHistory`, `conversationManager` — late-wired service handles; synced at
63
65
  * feed() entry only since no in-feed action rewires them
64
66
  * - `pasteRegistry`, `imageRegistry` — owned Maps, never replaced
67
+ * - `burstGuard` (W4-R3, ported from goodvibes-tui's DEBT-5 item 5) — the
68
+ * unbracketed-paste-flood guard's sliding-window state, mutated in place
69
+ * across tokens by trackPanelPasteFloodGuard (see panel-paste-flood-guard.ts).
70
+ * Never reallocated. `burstSuppressedCount` is this wiring layer's own
71
+ * bookkeeping (not part of the ported module) for the honest resolution
72
+ * notice — see feedInputTokens below.
73
+ * - `focusTracker` (W4-R3, ported from goodvibes-tui's W2.3) — tracks OS-level
74
+ * terminal focus from `\x1b[I`/`\x1b[O` tokens (DECSET ?1004h, enabled in
75
+ * main.ts). Shared instance from RuntimeServices, threaded via
76
+ * uiServices.platform.focusTracker (mirrors the TUI's own wiring).
65
77
  * - `selectionModal`, `bookmarkModal`, `settingsModal`, `sessionPickerModal`,
66
78
  * `profilePickerModal` — modal objects constructed once in InputHandler constructor
67
79
  * - `filePicker`, `modelPicker`, `processModal`, `liveTailModal`,
@@ -93,6 +105,12 @@ export interface InputFeedContext {
93
105
  contentWidth: number;
94
106
  readonly pasteRegistry: Map<string, string>;
95
107
  readonly imageRegistry: Map<string, { data: string; mediaType: string }>;
108
+ /** W4-R3 (ported from goodvibes-tui DEBT-5 item 5) — mutated in place, never reallocated. */
109
+ readonly burstGuard: PanelBurstGuardState;
110
+ /** W4-R3 wiring-layer bookkeeping (not part of the ported module) for the honest suppressed-count notice. */
111
+ burstSuppressedCount: number;
112
+ /** W4-R3 (ported from goodvibes-tui W2.3) — OS-level terminal focus, fed from 'focus' tokens below. */
113
+ readonly focusTracker: FocusTracker;
96
114
  readonly projectRoot: string;
97
115
  readonly selection: SelectionManager;
98
116
  readonly selectionModal: SelectionModal;
@@ -158,8 +176,20 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
158
176
  const scrollTop = context.getScrollTop();
159
177
  const lineCount = history.getLineCount();
160
178
  const keybindings = context.keybindingsManager;
179
+ // W4-R3: one `now` per feed() call (not per token) — a genuine unbracketed-paste
180
+ // flood delivers many tokens in one drain, and they should all measure as
181
+ // arriving "at once" (mirrors goodvibes-tui's handler-feed.ts DEBT-5 item 5 doc).
182
+ const now = Date.now();
161
183
 
162
184
  for (const token of tokens) {
185
+ // W4-R3 (ported from goodvibes-tui W2.3): focus-reporting tokens (CSI ?1004h)
186
+ // never reach the composer or any modal route — consumed here, first,
187
+ // unconditionally. No render needed.
188
+ if (token.type === 'focus') {
189
+ context.focusTracker.setFocused(token.action === 'in');
190
+ continue;
191
+ }
192
+
163
193
  if (token.type === 'key' && context.keybindingsManager.matches('clear-cancel', token)) {
164
194
  context.handleCtrlC();
165
195
  continue;
@@ -264,6 +294,63 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
264
294
  }
265
295
  }
266
296
 
297
+ // W4-R3 (ported from goodvibes-tui's panel-paste-flood-guard.ts, DEBT-5 item
298
+ // 5): guards command-mode's key-driven dispatch (handleCommandModeToken,
299
+ // below) from an unbracketed-paste-replay or control-character-injection
300
+ // burst.
301
+ //
302
+ // SCOPE — 'key' tokens, and only while commandMode is active: the TUI's
303
+ // own guard exempts any "capturing" text surface entirely (its own test
304
+ // asserts a capturing panel "receives the full burst untouched by the
305
+ // flood guard" — see panel-focus-route.test.ts) and never touches its own
306
+ // non-panel-focused composer's key handling at all. This agent's plain
307
+ // composer (commandMode false) is exactly that kind of capturing/untouched
308
+ // surface — handlePromptTextToken absorbs pasted/typed text of any length
309
+ // by plain insertion, and handlePromptKeyToken's arrow/backspace/enter
310
+ // handling is the same shape as the TUI's own unguarded composer key
311
+ // route. Guarding those would falsely trip on ordinary fast/bulk delivery
312
+ // (a single feed() call carrying many characters/keys shares one `now`,
313
+ // indistinguishable from a real flood under this millisecond-resolution
314
+ // model — confirmed by a regression in this repo's own
315
+ // command-modal-handoff.test.ts when an earlier version of this guard
316
+ // covered all 'key'/'text' tokens unconditionally) and would add new,
317
+ // product-inconsistent friction (e.g. held-arrow-key auto-repeat) to the
318
+ // agent's default interaction mode that the TUI's own users don't have.
319
+ //
320
+ // commandMode's key dispatch is the genuine analog of a TUI panel's
321
+ // per-character hotkey dispatch — matching the R1 matrix's own adaptation
322
+ // note, "the burst instead becomes command/keybinding dispatch": once
323
+ // commandMode is armed (state.prompt starts with '/'), Enter EXECUTES a
324
+ // slash command (handler-command-route.ts), Tab completes, up/down
325
+ // navigate — real state-changing single-key actions. An unbracketed
326
+ // paste whose content happens to start with '/' and contains a bare '\r'
327
+ // partway through (not '\n' — the tokenizer maps '\n' to shift+enter/
328
+ // newline-insert, code10; only '\r'/code13 is a genuine 'enter' key, see
329
+ // platform/core/tokenizer.ts) would otherwise execute a slash command
330
+ // early with truncated/wrong arguments. A human never sends 9 key-tokens
331
+ // within 120ms.
332
+ //
333
+ // UX-FIRST / HONEST DEGRADED STATE: never silent — a one-shot notice fires
334
+ // the moment the guard trips, and a second notice reports how many
335
+ // keystrokes it suppressed once the burst quiets down.
336
+ if (token.type === 'key' && context.commandMode) {
337
+ const wasSuspended = context.burstGuard.suspended;
338
+ const guard = trackPanelPasteFloodGuard(context.burstGuard, now);
339
+ if (!guard.dispatch) {
340
+ context.burstSuppressedCount++;
341
+ if (guard.showHintNow) {
342
+ context.commandContext?.print('[paste] unbracketed paste flood detected — suppressing extra keystrokes until it settles');
343
+ context.requestRender();
344
+ }
345
+ continue;
346
+ }
347
+ if (wasSuspended) {
348
+ context.commandContext?.print(`[paste] flood cleared — suppressed ${context.burstSuppressedCount} keystroke(s)`);
349
+ context.burstSuppressedCount = 0;
350
+ context.requestRender();
351
+ }
352
+ }
353
+
267
354
  const indicatorRoute = handleIndicatorFocusToken({
268
355
  indicatorFocused: context.indicatorFocused,
269
356
  modalOpened: context.modalOpened,
@@ -2,6 +2,7 @@ import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
2
2
  import type { SelectionResult, SelectionAction } from './selection-modal.ts';
3
3
  import type { CommandContext } from './command-registry.ts';
4
4
  import { openTtsProviderPicker, openTtsVoicePicker } from './tts-settings-actions.ts';
5
+ import { isTextBackspace } from './delete-key-policy.ts';
5
6
 
6
7
  type SelectionRouteState = {
7
8
  selectionModal: {
@@ -136,7 +137,11 @@ export function handleSelectionModalToken(state: SelectionRouteState, token: Inp
136
137
  getAdjustmentStep(selected, token.shift),
137
138
  );
138
139
  }
139
- } else if (token.logicalName === 'backspace') {
140
+ } else if (isTextBackspace(token.logicalName ?? '')) {
141
+ // delete-key-policy (R3 port, ported from goodvibes-tui): this end-anchored
142
+ // search filter has no cursor, so 'delete' (isTextForwardDelete) is correctly
143
+ // a no-op here — it simply falls through this else-if chain untouched, same
144
+ // as the TUI's identical SelectionModal search-filter route.
140
145
  if (state.selectionModal.allowSearch && state.selectionModal.searchFocused && state.selectionModal.query.length > 0) {
141
146
  state.selectionModal.setQuery(state.selectionModal.query.slice(0, -1));
142
147
  }
@@ -67,6 +67,7 @@ import { feedInputTokens } from './handler-feed.ts';
67
67
  import { buildInitialFeedContext, syncFeedContextMutableFields } from './feed-context-factory.ts';
68
68
  import type { UiRuntimeServices } from '../runtime/ui-services.ts';
69
69
  import type { ModelPickerTarget } from './model-picker.ts';
70
+ import type { PanelBurstGuardState } from './panel-paste-flood-guard.ts';
70
71
 
71
72
  type SelectionModalCallback = (result: SelectionResult | null) => void;
72
73
 
@@ -93,6 +94,8 @@ export class InputHandler {
93
94
  public pasteRegistry = new Map<string, string>();
94
95
  public nextPasteId = 1;
95
96
  public lastCtrlCTime = 0;
97
+ /** W4-R3 (ported from goodvibes-tui DEBT-5 item 5) — unbracketed-paste-flood guard state, mutated in place. */
98
+ public burstGuard: PanelBurstGuardState = { timestamps: [], suspended: false, hintShown: false };
96
99
  /** Long-lived feed context — reused across every feed() call to avoid per-keystroke allocation. */
97
100
  public feedContext!: import('./handler-feed.ts').InputFeedContext;
98
101
  public commandRegistry: CommandRegistry | null = null;
@@ -207,6 +210,8 @@ export class InputHandler {
207
210
  selection: this.selection,
208
211
  pasteRegistry: this.pasteRegistry,
209
212
  imageRegistry: this.imageRegistry,
213
+ burstGuard: this.burstGuard,
214
+ focusTracker: this.uiServices.platform.focusTracker,
210
215
  projectRoot: this.uiServices.environment.shellPaths.workingDirectory,
211
216
  selectionModal: this.selectionModal,
212
217
  bookmarkModal: this.bookmarkModal,
@@ -0,0 +1,94 @@
1
+ // ---------------------------------------------------------------------------
2
+ // panel-paste-flood-guard.ts — ported from goodvibes-tui (commit 90eb3a26,
3
+ // DEBT-5 item 5), W4-R3.
4
+ //
5
+ // A terminal WITHOUT bracketed paste delivers a pasted block as a burst of
6
+ // discrete 1-char 'text' tokens (isPasteToken stays false for every one of
7
+ // them, since that flag only fires for a single token whose value.length > 1).
8
+ //
9
+ // AGENT ADAPTATION (this repo has no `src/panels/` — see the W4-R1 parity
10
+ // matrix's R3 row): the TUI wires this guard inside handlePanelFocusToken,
11
+ // where it protects a focused PANEL from having each replayed character
12
+ // dispatched as a real hotkey (K arms kill, etc). This agent has no panel
13
+ // focus concept at all — every token that isn't consumed by a modal route or
14
+ // a global shortcut lands directly in the composer/command dispatch chain
15
+ // (handleIndicatorFocusToken -> handlePromptTextToken -> handleCommandModeToken
16
+ // -> handlePromptKeyToken, see handler-feed.ts). The matrix's own adaptation
17
+ // note ("the burst instead becomes command/keybinding dispatch — the guard
18
+ // wires above the composer/command dispatch") is implemented by gating that
19
+ // same chain in feedInputTokens: see handler-feed.ts's per-token loop, which
20
+ // calls trackPanelPasteFloodGuard for every non-paste 'text' token BEFORE
21
+ // those routes run, using a persistent PanelBurstGuardState carried on
22
+ // InputFeedContext (context.burstGuard — mirrors how pasteRegistry is
23
+ // threaded as a stable, never-reallocated field; see feed-context-factory.ts).
24
+ //
25
+ // This module's own logic is UI-framework-agnostic and is kept byte-identical
26
+ // to the TUI's (same constants, same sliding-window algorithm) — only its
27
+ // CALLERS differ between the two products.
28
+ //
29
+ // This is a RATE guard: more than PANEL_PASTE_FLOOD_THRESHOLD qualifying
30
+ // tokens within the trailing PANEL_PASTE_FLOOD_WINDOW_MS, evaluated with a
31
+ // real sliding window (old timestamps age out of `timestamps` every call). It
32
+ // is deliberately NOT a per-feed char-SUM burst heuristic: that shape summed
33
+ // one feed()'s character count with no timing signal at all, so two ordinary
34
+ // keystrokes landing in a single feed() (a real, common case) could be
35
+ // misread as a burst. This guard:
36
+ // - is keyed on WALL-CLOCK TIMING, not a per-feed token count, so it
37
+ // doesn't care how many tokens land in one feed() call, only how fast
38
+ // they arrive relative to each other;
39
+ // - is sticky once tripped (only a quiet gap — no qualifying token for a
40
+ // full window — clears it) so it doesn't flap dispatch on/off as the
41
+ // count oscillates near the threshold mid-flood.
42
+ //
43
+ // ~8 keys/120ms is far beyond sustained human typing (a fast typist peaks
44
+ // well under that inter-key rate over any real span) but is exactly the
45
+ // shape an unbracketed paste replay takes.
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export const PANEL_PASTE_FLOOD_WINDOW_MS = 120;
49
+ export const PANEL_PASTE_FLOOD_THRESHOLD = 8;
50
+
51
+ /**
52
+ * Guard state — a single persistent instance lives on the caller's
53
+ * long-lived context (this agent's handler-feed.ts InputFeedContext, mirroring
54
+ * how that object already owns `nextPasteId`/`mouseDownRow`/etc.) and is
55
+ * MUTATED IN PLACE by trackPanelPasteFloodGuard below, never replaced — so
56
+ * callers never need to thread a return value back into their own state.
57
+ */
58
+ export interface PanelBurstGuardState {
59
+ timestamps: readonly number[];
60
+ suspended: boolean;
61
+ hintShown: boolean;
62
+ }
63
+
64
+ export interface PanelBurstGuardResult {
65
+ /** False while suspended — the caller must drop this token, not dispatch it. */
66
+ readonly dispatch: boolean;
67
+ /** True exactly once per burst: the call that just tripped suspension. */
68
+ readonly showHintNow: boolean;
69
+ }
70
+
71
+ /** Advance `guard` (mutated in place) by one qualifying token at time `now` (ms). */
72
+ export function trackPanelPasteFloodGuard(guard: PanelBurstGuardState, now: number): PanelBurstGuardResult {
73
+ const lastAt = guard.timestamps.length > 0 ? guard.timestamps[guard.timestamps.length - 1]! : -Infinity;
74
+ const isQuietGap = now - lastAt > PANEL_PASTE_FLOOD_WINDOW_MS;
75
+ if (isQuietGap && guard.suspended) {
76
+ // A silence at least as long as the window means whatever burst was
77
+ // happening has ended — un-suspend so a LATER burst gets its own fresh
78
+ // count and its own one-shot hint.
79
+ guard.suspended = false;
80
+ guard.hintShown = false;
81
+ }
82
+ guard.timestamps = isQuietGap
83
+ ? [now]
84
+ : [...guard.timestamps.filter((t) => t > now - PANEL_PASTE_FLOOD_WINDOW_MS), now];
85
+ if (guard.timestamps.length > PANEL_PASTE_FLOOD_THRESHOLD) {
86
+ guard.suspended = true;
87
+ }
88
+ let showHintNow = false;
89
+ if (guard.suspended && !guard.hintShown) {
90
+ guard.hintShown = true;
91
+ showHintNow = true;
92
+ }
93
+ return { dispatch: !guard.suspended, showHintNow };
94
+ }
@@ -39,6 +39,7 @@ export type SettingsCategory =
39
39
  | 'watchers'
40
40
  | 'network'
41
41
  | 'orchestration'
42
+ | 'planner'
42
43
  | 'runtime'
43
44
  | 'sandbox'
44
45
  | 'batch'
@@ -48,7 +49,8 @@ export type SettingsCategory =
48
49
  | 'cache'
49
50
  | 'mcp'
50
51
  | 'flags'
51
- | 'release';
52
+ | 'release'
53
+ | 'daemon';
52
54
 
53
55
  export type SettingsFocusPane = 'categories' | 'settings';
54
56
 
@@ -60,8 +62,8 @@ export const SETTINGS_CATEGORY_GROUPS: ReadonlyArray<{
60
62
  { label: 'Models and Providers', categories: ['provider', 'subscriptions', 'helper', 'tools', 'tts'] },
61
63
  { label: 'Agent-local state', categories: ['storage', 'cache', 'telemetry'] },
62
64
  { label: 'Channels and Tools', categories: ['surfaces', 'mcp', 'automation'] },
63
- { label: 'Daemon Runtime', categories: ['service', 'controlPlane', 'httpListener', 'web', 'watchers', 'network'] },
64
- { label: 'Advanced Runtime', categories: ['orchestration', 'runtime', 'sandbox', 'batch', 'cloudflare', 'wrfc'] },
65
+ { label: 'Daemon Runtime', categories: ['daemon', 'service', 'controlPlane', 'httpListener', 'web', 'watchers', 'network'] },
66
+ { label: 'Advanced Runtime', categories: ['orchestration', 'planner', 'runtime', 'sandbox', 'batch', 'cloudflare', 'wrfc'] },
65
67
  { label: 'Advanced', categories: ['flags', 'release'] },
66
68
  ];
67
69