@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.
- package/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/dist/package/main.js +94130 -79335
- package/docs/tools-and-commands.md +2 -1
- package/package.json +6 -3
- package/src/agent/autonomy-schedule-format.ts +1 -1
- package/src/agent/autonomy-schedule.ts +3 -3
- package/src/agent/calendar/calendar-oauth-service.ts +324 -0
- package/src/agent/calendar-subscription-registry.ts +497 -0
- package/src/agent/email/email-service.ts +57 -0
- package/src/agent/email/smtp-client.ts +30 -10
- package/src/agent/memory-prompt.ts +143 -12
- package/src/agent/operator-actions.ts +26 -22
- package/src/agent/prompt-context-receipts.ts +45 -18
- package/src/agent/reminder-schedule-format.ts +1 -1
- package/src/agent/reminder-schedule.ts +3 -3
- package/src/agent/routine-schedule-format.ts +2 -2
- package/src/agent/routine-schedule-promotion.ts +5 -5
- package/src/agent/routine-schedule-receipts.ts +2 -2
- package/src/agent/schedule-edit-format.ts +1 -1
- package/src/agent/schedule-edit.ts +5 -5
- package/src/agent/session-registration.ts +281 -0
- package/src/agent/vibe-file.ts +50 -0
- package/src/cli/local-library-command-shared.ts +146 -0
- package/src/cli/management.ts +2 -1
- package/src/cli/personas-command.ts +252 -0
- package/src/cli/resume-relaunch-notice.ts +151 -0
- package/src/cli/routines-command.ts +25 -2
- package/src/cli/service-posture.ts +3 -2
- package/src/cli/skill-bundle-command.ts +175 -0
- package/src/cli/skills-command.ts +309 -0
- package/src/cli/status.ts +43 -7
- package/src/cli/tui-startup.ts +24 -1
- package/src/config/agent-settings-policy.ts +0 -1
- package/src/config/secret-config.ts +4 -0
- package/src/core/conversation-rendering.ts +20 -11
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/system-message-noise.ts +108 -0
- package/src/core/system-message-router.ts +54 -1
- package/src/core/thinking-overlay.ts +83 -0
- package/src/input/agent-workspace-access-command-editor-submission.ts +75 -108
- package/src/input/agent-workspace-access-command-editors.ts +108 -128
- package/src/input/agent-workspace-activation.ts +15 -0
- package/src/input/agent-workspace-basic-command-editor-submission.ts +292 -513
- package/src/input/agent-workspace-basic-command-editors.ts +433 -564
- package/src/input/agent-workspace-calendar-connect-editor.ts +116 -0
- package/src/input/agent-workspace-calendar-oauth-editor.ts +152 -0
- package/src/input/agent-workspace-calendar-subscribe-editor.ts +128 -0
- package/src/input/agent-workspace-categories.ts +7 -2
- package/src/input/agent-workspace-channel-command-editor-submission.ts +38 -52
- package/src/input/agent-workspace-channel-command-editors.ts +42 -46
- package/src/input/agent-workspace-command-editor-engine.ts +133 -0
- package/src/input/agent-workspace-command-editor.ts +4 -0
- package/src/input/agent-workspace-direct-editor-submission.ts +59 -0
- package/src/input/agent-workspace-email-connect-editor.ts +162 -0
- package/src/input/agent-workspace-knowledge-command-editor-submission.ts +52 -81
- package/src/input/agent-workspace-knowledge-command-editors.ts +71 -71
- package/src/input/agent-workspace-library-command-editor-submission.ts +10 -28
- package/src/input/agent-workspace-library-command-editors.ts +16 -2
- package/src/input/agent-workspace-live-counters.ts +55 -0
- package/src/input/agent-workspace-mcp-command-editor-submission.ts +60 -82
- package/src/input/agent-workspace-mcp-command-editors.ts +52 -0
- package/src/input/agent-workspace-media-command-editor-submission.ts +12 -39
- package/src/input/agent-workspace-media-command-editors.ts +10 -8
- package/src/input/agent-workspace-memory-command-editor-submission.ts +76 -151
- package/src/input/agent-workspace-memory-command-editors.ts +116 -141
- package/src/input/agent-workspace-operations-command-editor-submission.ts +131 -184
- package/src/input/agent-workspace-operations-command-editors.ts +150 -162
- package/src/input/agent-workspace-provider-command-editor-submission.ts +60 -106
- package/src/input/agent-workspace-provider-command-editors.ts +58 -68
- package/src/input/agent-workspace-search.ts +8 -1
- package/src/input/agent-workspace-session-command-editor-submission.ts +104 -132
- package/src/input/agent-workspace-session-command-editors.ts +160 -195
- package/src/input/agent-workspace-settings.ts +40 -1
- package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +55 -71
- package/src/input/agent-workspace-skill-bundle-command-editors.ts +65 -69
- package/src/input/agent-workspace-snapshot-builders.ts +431 -0
- package/src/input/agent-workspace-snapshot-config.ts +43 -0
- package/src/input/agent-workspace-snapshot.ts +198 -432
- package/src/input/agent-workspace-task-command-editor-submission.ts +34 -44
- package/src/input/agent-workspace-task-command-editors.ts +27 -28
- package/src/input/agent-workspace-types.ts +35 -1
- package/src/input/agent-workspace.ts +5 -14
- package/src/input/command-registry.ts +23 -1
- package/src/input/commands/calendar-connect-runtime.ts +226 -0
- package/src/input/commands/calendar-runtime.ts +110 -7
- package/src/input/commands/calendar-subscription-runtime.ts +225 -0
- package/src/input/commands/email-runtime.ts +100 -40
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/network-scan-runtime.ts +75 -0
- package/src/input/commands/operator-actions-runtime.ts +9 -6
- package/src/input/commands/personas-runtime.ts +1 -1
- package/src/input/commands/schedule-runtime.ts +4 -4
- package/src/input/commands/session-content.ts +13 -1
- package/src/input/commands/session-workflow.ts +15 -20
- package/src/input/commands/session.ts +3 -1
- package/src/input/commands/shell-core.ts +20 -6
- package/src/input/commands.ts +2 -2
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +10 -0
- package/src/input/handler-feed.ts +87 -0
- package/src/input/handler-modal-routes.ts +6 -1
- package/src/input/handler.ts +5 -0
- package/src/input/panel-paste-flood-guard.ts +94 -0
- package/src/input/settings-modal-types.ts +5 -3
- package/src/input/settings-modal.ts +21 -0
- package/src/main.ts +36 -36
- package/src/permissions/approval-posture.ts +141 -0
- package/src/renderer/agent-workspace-context-lines.ts +10 -2
- package/src/renderer/compositor.ts +27 -4
- package/src/renderer/diff.ts +61 -18
- package/src/renderer/fullscreen-primitives.ts +37 -18
- package/src/renderer/markdown.ts +20 -10
- package/src/renderer/modal-factory.ts +25 -15
- package/src/renderer/overlay-box.ts +23 -12
- package/src/renderer/process-indicator.ts +8 -3
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +2 -0
- package/src/renderer/startup-theme-probe.ts +35 -0
- package/src/renderer/status-glyphs.ts +11 -15
- package/src/renderer/system-message.ts +17 -2
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/terminal-bg-probe.ts +373 -0
- package/src/renderer/terminal-escapes.ts +24 -0
- package/src/renderer/theme-mode-config.ts +87 -0
- package/src/renderer/theme.ts +241 -0
- package/src/renderer/thinking.ts +12 -3
- package/src/renderer/tool-call.ts +7 -3
- package/src/renderer/ui-factory.ts +92 -36
- package/src/renderer/ui-primitives.ts +33 -93
- package/src/runtime/bootstrap-core.ts +15 -0
- package/src/runtime/bootstrap-hook-bridge.ts +6 -0
- package/src/runtime/bootstrap-shell.ts +2 -0
- package/src/runtime/bootstrap.ts +68 -5
- package/src/runtime/calendar-boot-refresh.ts +105 -0
- package/src/runtime/lan-scan-consent.ts +253 -0
- package/src/runtime/services.ts +127 -2
- package/src/runtime/session-spine-rest-transport.ts +160 -0
- package/src/runtime/terminal-output-guard.ts +6 -1
- package/src/runtime/ui-services.ts +3 -0
- package/src/shell/agent-workspace-fullscreen.ts +5 -0
- package/src/shell/terminal-focus-mode.ts +120 -0
- package/src/shell/ui-openers.ts +13 -4
- package/src/tools/agent-harness-autonomy-intake.ts +5 -5
- package/src/tools/agent-harness-autonomy-queue.ts +1 -1
- package/src/tools/agent-harness-background-processes.ts +2 -1
- package/src/tools/agent-harness-metadata.ts +16 -8
- package/src/tools/agent-harness-operator-methods.ts +44 -8
- package/src/tools/agent-harness-personal-ops-discovery.ts +57 -10
- package/src/tools/agent-harness-personal-ops-lanes.ts +17 -5
- package/src/tools/agent-harness-personal-ops-operations.ts +7 -7
- package/src/tools/agent-harness-personal-ops-types.ts +6 -0
- package/src/tools/agent-harness-prompt-context.ts +26 -12
- package/src/tools/agent-harness-workspace-actions.ts +4 -0
- package/src/tools/agent-local-registry-args.ts +117 -0
- package/src/tools/agent-local-registry-memory.ts +227 -0
- package/src/tools/agent-local-registry-tool.ts +19 -237
- package/src/tools/agent-operator-briefing-tool.ts +2 -2
- package/src/tools/agent-operator-method-tool.ts +13 -0
- package/src/tools/agent-policy-explanation.ts +39 -4
- package/src/tools/agent-schedule-tool.ts +5 -5
- package/src/utils/terminal-width.ts +98 -1
- package/src/version.ts +1 -1
- 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
|
-
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
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(
|
|
127
|
-
|
|
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));
|
package/src/input/commands.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/input/handler.ts
CHANGED
|
@@ -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
|
|