@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
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { discoverSkills, type SkillRecord } from '../agent/skill-discovery.ts';
|
|
2
|
+
import { formatAgentRecordOrigin, formatAgentRecordReviewState } from '../agent/record-labels.ts';
|
|
3
|
+
import {
|
|
4
|
+
buildAgentSkillRequirements,
|
|
5
|
+
evaluateAgentSkillReadiness,
|
|
6
|
+
formatAgentSkillRequirement,
|
|
7
|
+
type AgentSkillRecord,
|
|
8
|
+
} from '../agent/skill-registry.ts';
|
|
9
|
+
import type { CliCommandOutput } from './types.ts';
|
|
10
|
+
import type { CliCommandRuntime } from './management.ts';
|
|
11
|
+
import { handleSkillBundleCommand, renderBundleList } from './skill-bundle-command.ts';
|
|
12
|
+
import {
|
|
13
|
+
csvOption,
|
|
14
|
+
errorOutput,
|
|
15
|
+
failure,
|
|
16
|
+
hasFlag,
|
|
17
|
+
optionValue,
|
|
18
|
+
parseOptions,
|
|
19
|
+
requiredOption,
|
|
20
|
+
shellPaths,
|
|
21
|
+
skillRegistry,
|
|
22
|
+
success,
|
|
23
|
+
type ParsedOptions,
|
|
24
|
+
} from './local-library-command-shared.ts';
|
|
25
|
+
|
|
26
|
+
function summarizeSkill(skill: AgentSkillRecord): string {
|
|
27
|
+
const enabled = skill.enabled ? 'enabled' : 'disabled';
|
|
28
|
+
const tags = skill.tags.length > 0 ? ` tags ${skill.tags.join(', ')}` : '';
|
|
29
|
+
const readiness = evaluateAgentSkillReadiness(skill);
|
|
30
|
+
const ready = readiness.ready ? 'ready' : `needs ${readiness.missing.map(formatAgentSkillRequirement).join(',')}`;
|
|
31
|
+
return ` ${skill.id} ${enabled} ${formatAgentRecordReviewState(skill.reviewState)} ${ready} ${skill.name} - ${skill.description}${tags}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderSkillList(title: string, path: string, skills: readonly AgentSkillRecord[], emptyMessage?: string): string {
|
|
35
|
+
if (skills.length === 0) {
|
|
36
|
+
return [
|
|
37
|
+
title,
|
|
38
|
+
emptyMessage ? '' : ' No local Agent skills yet.',
|
|
39
|
+
` ${emptyMessage ?? 'No Agent-local skills yet.'}`,
|
|
40
|
+
emptyMessage ? '' : ' Create one with: goodvibes-agent skills create --name <name> --description <summary> --procedure <steps>',
|
|
41
|
+
].filter(Boolean).join('\n');
|
|
42
|
+
}
|
|
43
|
+
return [
|
|
44
|
+
`${title} (${skills.length})`,
|
|
45
|
+
` store ${path}`,
|
|
46
|
+
...skills.map(summarizeSkill),
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function summarizeDiscoveredSkill(skill: SkillRecord): string {
|
|
51
|
+
const description = skill.description ? ` - ${skill.description}` : '';
|
|
52
|
+
const dependencies = skill.dependencies.length > 0 ? ` dependencies ${skill.dependencies.join(', ')}` : '';
|
|
53
|
+
const includes = skill.includes.length > 0 ? ` includes ${skill.includes.join(', ')}` : '';
|
|
54
|
+
return [
|
|
55
|
+
` ${skill.name} ${skill.origin}${description}${dependencies}${includes}`,
|
|
56
|
+
` path ${skill.path}`,
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renderDiscoveredSkillList(skills: readonly SkillRecord[]): string {
|
|
61
|
+
if (skills.length === 0) {
|
|
62
|
+
return [
|
|
63
|
+
'Discovered Agent skill files',
|
|
64
|
+
' No SKILL.md or .md skill files found in Agent skill folders.',
|
|
65
|
+
' Search roots: .goodvibes/skills, .goodvibes/agent/skills, ~/.goodvibes/skills, ~/.goodvibes/agent/skills',
|
|
66
|
+
].join('\n');
|
|
67
|
+
}
|
|
68
|
+
return [
|
|
69
|
+
`Discovered Agent skill files (${skills.length})`,
|
|
70
|
+
...skills.map(summarizeDiscoveredSkill),
|
|
71
|
+
'',
|
|
72
|
+
'Import one with: goodvibes-agent skills import-discovered <name> --yes',
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function discoveredSkillLookupValues(skill: SkillRecord): readonly string[] {
|
|
77
|
+
const slug = skill.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
78
|
+
const basename = skill.path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? '';
|
|
79
|
+
return [skill.name, slug, skill.path, basename]
|
|
80
|
+
.map((value) => value.trim().toLowerCase())
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function findDiscoveredSkill(skills: readonly SkillRecord[], idOrName: string): SkillRecord | null {
|
|
85
|
+
const lookup = idOrName.trim().toLowerCase();
|
|
86
|
+
if (!lookup) return null;
|
|
87
|
+
return skills.find((skill) => discoveredSkillLookupValues(skill).includes(lookup)) ?? null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function discoveredFrontmatterList(skill: SkillRecord, key: string): readonly string[] {
|
|
91
|
+
const value = skill.frontmatter[key];
|
|
92
|
+
if (!value) return [];
|
|
93
|
+
return value.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function discoveredFrontmatterAnyList(skill: SkillRecord, keys: readonly string[]): readonly string[] {
|
|
97
|
+
for (const key of keys) {
|
|
98
|
+
const values = discoveredFrontmatterList(skill, key);
|
|
99
|
+
if (values.length > 0) return values;
|
|
100
|
+
}
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function renderSkill(skill: AgentSkillRecord): string {
|
|
105
|
+
const readiness = evaluateAgentSkillReadiness(skill);
|
|
106
|
+
return [
|
|
107
|
+
`Skill ${skill.name}`,
|
|
108
|
+
` id ${skill.id}`,
|
|
109
|
+
` enabled: ${skill.enabled ? 'yes' : 'no'}`,
|
|
110
|
+
` readiness: ${readiness.ready ? 'ready' : 'needs setup'}`,
|
|
111
|
+
` requirements: ${skill.requirements.map(formatAgentSkillRequirement).join(', ') || '(none)'}`,
|
|
112
|
+
readiness.missing.length > 0 ? ` missing: ${readiness.missing.map(formatAgentSkillRequirement).join(', ')}` : '',
|
|
113
|
+
` review: ${formatAgentRecordReviewState(skill.reviewState)}`,
|
|
114
|
+
` origin: ${formatAgentRecordOrigin(skill.source, skill.provenance)}`,
|
|
115
|
+
` tags: ${skill.tags.join(', ') || '(none)'}`,
|
|
116
|
+
` triggers: ${skill.triggers.join(', ') || '(manual)'}`,
|
|
117
|
+
` created: ${skill.createdAt}`,
|
|
118
|
+
` updated: ${skill.updatedAt}`,
|
|
119
|
+
skill.staleReason ? ` stale reason: ${skill.staleReason}` : '',
|
|
120
|
+
'',
|
|
121
|
+
skill.description,
|
|
122
|
+
'',
|
|
123
|
+
skill.procedure,
|
|
124
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function usageSkills(): string {
|
|
128
|
+
return 'Usage: goodvibes-agent skills [list|enabled|active|attention|discover|import-discovered <name> --yes|search <query>|show <id>|create [--requires-env A,B] [--requires-command gh,jq]|update <id>|enable <id>|disable <id>|review <id>|stale <id> <reason>|delete <id> --yes|bundle ...]';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function skillPayloadFromOptions(options: ParsedOptions): {
|
|
132
|
+
readonly name: string;
|
|
133
|
+
readonly description: string;
|
|
134
|
+
readonly procedure: string;
|
|
135
|
+
readonly tags: readonly string[] | undefined;
|
|
136
|
+
readonly triggers: readonly string[] | undefined;
|
|
137
|
+
readonly requirements: ReturnType<typeof buildAgentSkillRequirements>;
|
|
138
|
+
readonly enabled: boolean | undefined;
|
|
139
|
+
readonly provenance: string;
|
|
140
|
+
} {
|
|
141
|
+
const usage = 'Usage: goodvibes-agent skills create --name <name> --description <summary> --procedure <steps>';
|
|
142
|
+
return {
|
|
143
|
+
name: requiredOption(options, 'name', usage),
|
|
144
|
+
description: requiredOption(options, 'description', usage),
|
|
145
|
+
procedure: requiredOption(options, 'procedure', usage),
|
|
146
|
+
tags: csvOption(options, 'tags'),
|
|
147
|
+
triggers: csvOption(options, 'triggers'),
|
|
148
|
+
requirements: buildAgentSkillRequirements({
|
|
149
|
+
env: csvOption(options, 'requires-env'),
|
|
150
|
+
commands: csvOption(options, 'requires-command') ?? csvOption(options, 'requires-commands'),
|
|
151
|
+
}),
|
|
152
|
+
enabled: hasFlag(options, 'enabled') ? true : undefined,
|
|
153
|
+
provenance: optionValue(options, 'provenance') ?? 'Command',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function handleSkillsCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
158
|
+
try {
|
|
159
|
+
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
160
|
+
const normalized = sub.toLowerCase();
|
|
161
|
+
if (normalized === 'bundle' || normalized === 'bundles') return handleSkillBundleCommand(runtime, rest);
|
|
162
|
+
const registry = skillRegistry(runtime);
|
|
163
|
+
const snapshot = registry.snapshot();
|
|
164
|
+
if (normalized === 'list' || normalized === 'ls') {
|
|
165
|
+
return success(runtime, 'agent.skills.list', { path: snapshot.path, skills: snapshot.skills, enabledCount: snapshot.enabledSkills.length }, renderSkillList('Agent skills', snapshot.path, snapshot.skills));
|
|
166
|
+
}
|
|
167
|
+
if (normalized === 'enabled') {
|
|
168
|
+
return success(runtime, 'agent.skills.enabled', { path: snapshot.path, skills: snapshot.enabledSkills }, renderSkillList('Enabled Agent skills', snapshot.path, snapshot.enabledSkills));
|
|
169
|
+
}
|
|
170
|
+
if (normalized === 'active') {
|
|
171
|
+
return success(runtime, 'agent.skills.active', { path: snapshot.path, skills: snapshot.activeSkills, bundles: snapshot.enabledBundles }, [
|
|
172
|
+
renderSkillList('Active Agent skills', snapshot.path, snapshot.activeSkills),
|
|
173
|
+
snapshot.enabledBundles.length > 0 ? renderBundleList('Enabled Agent skill bundles', snapshot.path, snapshot.enabledBundles, snapshot.skills) : '',
|
|
174
|
+
].filter(Boolean).join('\n\n'));
|
|
175
|
+
}
|
|
176
|
+
if (normalized === 'attention' || normalized === 'needs-setup') {
|
|
177
|
+
const skills = snapshot.skills.filter((skill) => !evaluateAgentSkillReadiness(skill).ready);
|
|
178
|
+
return success(runtime, 'agent.skills.attention', { path: snapshot.path, skills }, renderSkillList('Agent skills needing setup', snapshot.path, skills, 'No Agent-local skills need setup.'));
|
|
179
|
+
}
|
|
180
|
+
if (normalized === 'discover') {
|
|
181
|
+
const discovered = await discoverSkills(shellPaths(runtime));
|
|
182
|
+
return success(runtime, 'agent.skills.discover', { skills: discovered }, renderDiscoveredSkillList(discovered));
|
|
183
|
+
}
|
|
184
|
+
if (normalized === 'import-discovered' || normalized === 'import-skill') {
|
|
185
|
+
const options = parseOptions(rest);
|
|
186
|
+
const name = options.positionals.join(' ').trim();
|
|
187
|
+
if (!name) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills import-discovered <name> [--enabled] --yes', 2);
|
|
188
|
+
const discovered = findDiscoveredSkill(await discoverSkills(shellPaths(runtime)), name);
|
|
189
|
+
if (!discovered) {
|
|
190
|
+
return failure(runtime, 'skill_discovery_not_found', `Unknown discovered Agent skill ${name}\nRun goodvibes-agent skills discover to inspect available skill files.`, 1);
|
|
191
|
+
}
|
|
192
|
+
if (!hasFlag(options, 'yes')) {
|
|
193
|
+
return success(runtime, 'agent.skills.import_discovered.preview', { skill: discovered }, [
|
|
194
|
+
'Agent skill import preview',
|
|
195
|
+
` name ${discovered.name}`,
|
|
196
|
+
` origin ${discovered.origin}`,
|
|
197
|
+
` path ${discovered.path}`,
|
|
198
|
+
` description ${discovered.description || '(none)'}`,
|
|
199
|
+
` procedure characters ${discovered.body.length}`,
|
|
200
|
+
' next rerun with --yes to import into the Agent-local skill registry',
|
|
201
|
+
].join('\n'));
|
|
202
|
+
}
|
|
203
|
+
const skill = registry.create({
|
|
204
|
+
name: discovered.name,
|
|
205
|
+
description: discovered.description || `Imported skill from ${discovered.origin} skill file.`,
|
|
206
|
+
procedure: discovered.body,
|
|
207
|
+
tags: discoveredFrontmatterList(discovered, 'tags'),
|
|
208
|
+
triggers: discoveredFrontmatterList(discovered, 'triggers'),
|
|
209
|
+
requirements: buildAgentSkillRequirements({
|
|
210
|
+
env: discoveredFrontmatterAnyList(discovered, ['requiresEnv', 'requires-env', 'requires_env']),
|
|
211
|
+
commands: discoveredFrontmatterAnyList(discovered, ['requiresCommands', 'requires-commands', 'requires_commands', 'commands']),
|
|
212
|
+
}),
|
|
213
|
+
enabled: hasFlag(options, 'enabled'),
|
|
214
|
+
source: 'imported',
|
|
215
|
+
provenance: `discovered:${discovered.origin}:${discovered.path}`,
|
|
216
|
+
});
|
|
217
|
+
return success(runtime, 'agent.skills.import_discovered', skill, [
|
|
218
|
+
`Imported Agent skill ${skill.id}: ${skill.name}${skill.enabled ? ' (enabled)' : ''}`,
|
|
219
|
+
` name ${skill.name}`,
|
|
220
|
+
` enabled ${skill.enabled ? 'yes' : 'no'}`,
|
|
221
|
+
].join('\n'));
|
|
222
|
+
}
|
|
223
|
+
if (normalized === 'search' || normalized === 'find') {
|
|
224
|
+
const query = rest.join(' ').trim();
|
|
225
|
+
const results = registry.search(query);
|
|
226
|
+
return success(runtime, 'agent.skills.search', { query, results }, renderSkillList(`Agent skills matching "${query}"`, snapshot.path, results));
|
|
227
|
+
}
|
|
228
|
+
if (normalized === 'show' || normalized === 'get') {
|
|
229
|
+
const id = rest[0];
|
|
230
|
+
if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills show <id>', 2);
|
|
231
|
+
const skill = registry.get(id);
|
|
232
|
+
if (!skill) return failure(runtime, 'skill_not_found', `Unknown Agent skill ${id}`, 1);
|
|
233
|
+
return success(runtime, 'agent.skills.show', skill, renderSkill(skill));
|
|
234
|
+
}
|
|
235
|
+
if (normalized === 'create') {
|
|
236
|
+
const skill = registry.create(skillPayloadFromOptions(parseOptions(rest)));
|
|
237
|
+
return success(runtime, 'agent.skills.create', skill, [
|
|
238
|
+
'Agent skill created',
|
|
239
|
+
` id ${skill.id}`,
|
|
240
|
+
skill.enabled ? ' (enabled)' : '',
|
|
241
|
+
` enabled ${skill.enabled ? 'yes' : 'no'}`,
|
|
242
|
+
].filter(Boolean).join('\n'));
|
|
243
|
+
}
|
|
244
|
+
if (normalized === 'update') {
|
|
245
|
+
const id = rest[0];
|
|
246
|
+
if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills update <id> [--name ...] [--description ...] [--procedure ...]', 2);
|
|
247
|
+
const options = parseOptions(rest.slice(1));
|
|
248
|
+
const skill = registry.update(id, {
|
|
249
|
+
name: optionValue(options, 'name'),
|
|
250
|
+
description: optionValue(options, 'description'),
|
|
251
|
+
procedure: optionValue(options, 'procedure'),
|
|
252
|
+
tags: csvOption(options, 'tags'),
|
|
253
|
+
triggers: csvOption(options, 'triggers'),
|
|
254
|
+
requirements: options.values.has('requires-env') || options.values.has('requires-command') || options.values.has('requires-commands')
|
|
255
|
+
? buildAgentSkillRequirements({
|
|
256
|
+
env: csvOption(options, 'requires-env'),
|
|
257
|
+
commands: csvOption(options, 'requires-command') ?? csvOption(options, 'requires-commands'),
|
|
258
|
+
})
|
|
259
|
+
: undefined,
|
|
260
|
+
provenance: optionValue(options, 'provenance'),
|
|
261
|
+
});
|
|
262
|
+
return success(runtime, 'agent.skills.update', skill, [
|
|
263
|
+
'Agent skill updated',
|
|
264
|
+
` id ${skill.id}`,
|
|
265
|
+
].join('\n'));
|
|
266
|
+
}
|
|
267
|
+
if (normalized === 'enable' || normalized === 'disable') {
|
|
268
|
+
const id = rest[0];
|
|
269
|
+
if (!id) return failure(runtime, 'invalid_skill_command', `Usage: goodvibes-agent skills ${normalized} <id>`, 2);
|
|
270
|
+
const skill = registry.setEnabled(id, normalized === 'enable');
|
|
271
|
+
return success(runtime, `agent.skills.${normalized}`, skill, [
|
|
272
|
+
`Agent skill ${normalized === 'enable' ? 'enabled' : 'disabled'}`,
|
|
273
|
+
` id ${skill.id}`,
|
|
274
|
+
].join('\n'));
|
|
275
|
+
}
|
|
276
|
+
if (normalized === 'review') {
|
|
277
|
+
const id = rest[0];
|
|
278
|
+
if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills review <id>', 2);
|
|
279
|
+
const skill = registry.markReviewed(id);
|
|
280
|
+
return success(runtime, 'agent.skills.review', skill, [
|
|
281
|
+
'Agent skill reviewed',
|
|
282
|
+
` id ${skill.id}`,
|
|
283
|
+
].join('\n'));
|
|
284
|
+
}
|
|
285
|
+
if (normalized === 'stale') {
|
|
286
|
+
const id = rest[0];
|
|
287
|
+
if (!id || rest.length < 2) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills stale <id> <reason>', 2);
|
|
288
|
+
const skill = registry.markStale(id, rest.slice(1).join(' '));
|
|
289
|
+
return success(runtime, 'agent.skills.stale', skill, [
|
|
290
|
+
'Agent skill marked stale',
|
|
291
|
+
` id ${skill.id}`,
|
|
292
|
+
].join('\n'));
|
|
293
|
+
}
|
|
294
|
+
if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') {
|
|
295
|
+
const options = parseOptions(rest);
|
|
296
|
+
const id = options.positionals[0];
|
|
297
|
+
if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills delete <id> --yes', 2);
|
|
298
|
+
if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete Agent skill ${id} without --yes.`, 2);
|
|
299
|
+
const skill = registry.deleteSkill(id);
|
|
300
|
+
return success(runtime, 'agent.skills.delete', skill, [
|
|
301
|
+
`Agent skill deleted: ${id}`,
|
|
302
|
+
` id ${skill.id}`,
|
|
303
|
+
].join('\n'));
|
|
304
|
+
}
|
|
305
|
+
return failure(runtime, 'invalid_skill_command', usageSkills(), 2);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
return errorOutput(runtime, error, 'agent.skills.error');
|
|
308
|
+
}
|
|
309
|
+
}
|
package/src/cli/status.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { CliExternalRuntimeSnapshot } from './external-runtime.ts';
|
|
|
7
7
|
import { getProviderIdFromModel } from '../config/provider-model.ts';
|
|
8
8
|
import { formatAgentRecordSource } from '../agent/record-labels.ts';
|
|
9
9
|
import { formatAgentKnowledgeFailureKind } from './agent-knowledge-format.ts';
|
|
10
|
+
import { computeApprovalPosture, type ApprovalPosture } from '../permissions/approval-posture.ts';
|
|
10
11
|
|
|
11
12
|
export interface CliStatusOptions {
|
|
12
13
|
readonly configManager: Pick<ConfigManager, 'get'>;
|
|
@@ -51,6 +52,8 @@ export interface CliStatusSnapshot {
|
|
|
51
52
|
readonly auth: {
|
|
52
53
|
readonly permissionMode: unknown;
|
|
53
54
|
readonly permissionLabel: string;
|
|
55
|
+
readonly autoApprove: boolean;
|
|
56
|
+
readonly bypassesPrompts: boolean;
|
|
54
57
|
readonly secretPolicy: unknown;
|
|
55
58
|
readonly secretPolicyLabel: string;
|
|
56
59
|
readonly runtimeAuthSignal: CliAuthStatus | null;
|
|
@@ -79,11 +82,27 @@ function yesNo(value: unknown): string {
|
|
|
79
82
|
return value === true ? 'yes' : 'no';
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
/** permissions.tools.* keys — mirrors PermissionsToolConfig in the SDK config schema. */
|
|
86
|
+
const PERMISSION_TOOL_KEYS = [
|
|
87
|
+
'read', 'write', 'edit', 'exec', 'find', 'fetch', 'analyze',
|
|
88
|
+
'inspect', 'agent', 'state', 'workflow', 'registry', 'delegate', 'mcp',
|
|
89
|
+
] as const;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Reads the effective approval posture the SAME way the permission gate
|
|
93
|
+
* decides it (behavior.autoApprove first, then permissions.mode), so the
|
|
94
|
+
* displayed label can never disagree with what actually happens when a tool
|
|
95
|
+
* runs. Do not compute a posture label from permissions.mode alone here —
|
|
96
|
+
* that was the original bug (this surface ignored behavior.autoApprove).
|
|
97
|
+
*/
|
|
98
|
+
function readApprovalPosture(config: Pick<ConfigManager, 'get'>): ApprovalPosture {
|
|
99
|
+
const customTools: Record<string, unknown> = {};
|
|
100
|
+
for (const key of PERMISSION_TOOL_KEYS) customTools[key] = config.get(`permissions.tools.${key}`);
|
|
101
|
+
return computeApprovalPosture({
|
|
102
|
+
autoApprove: config.get('behavior.autoApprove') === true,
|
|
103
|
+
mode: config.get('permissions.mode'),
|
|
104
|
+
customTools,
|
|
105
|
+
});
|
|
87
106
|
}
|
|
88
107
|
|
|
89
108
|
function secretPolicyLabel(policy: unknown): string {
|
|
@@ -97,6 +116,7 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
|
|
|
97
116
|
const config = options.configManager;
|
|
98
117
|
const permissionMode = config.get('permissions.mode');
|
|
99
118
|
const secretPolicy = config.get('storage.secretPolicy');
|
|
119
|
+
const posture = readApprovalPosture(config);
|
|
100
120
|
const marker = options.onboardingMarkers?.effective;
|
|
101
121
|
|
|
102
122
|
const findings: CliDoctorFinding[] = [];
|
|
@@ -176,6 +196,18 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
|
|
|
176
196
|
});
|
|
177
197
|
}
|
|
178
198
|
|
|
199
|
+
if (posture.autoApprove) {
|
|
200
|
+
findings.push({
|
|
201
|
+
id: 'auto-approve-enabled',
|
|
202
|
+
area: 'security',
|
|
203
|
+
severity: 'risk',
|
|
204
|
+
summary: 'Auto-approve is on: tool calls never prompt.',
|
|
205
|
+
cause: 'behavior.autoApprove is true.',
|
|
206
|
+
impact: 'Every tool call — including powerful write, edit, network, and execution actions — runs without a Human-in-the-Loop (HITL) approval prompt, regardless of permissions.mode or any custom per-tool rule.',
|
|
207
|
+
action: 'Disable behavior.autoApprove unless this is an intentionally trusted environment.',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
179
211
|
if (permissionMode === 'allow-all') {
|
|
180
212
|
findings.push({
|
|
181
213
|
id: 'allow-all-permissions',
|
|
@@ -210,6 +242,7 @@ export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnap
|
|
|
210
242
|
const webBinding = resolveRuntimeEndpointBinding(config, 'web');
|
|
211
243
|
const marker = options.onboardingMarkers?.effective;
|
|
212
244
|
const findings = buildCliDoctorFindings(options);
|
|
245
|
+
const posture = readApprovalPosture(config);
|
|
213
246
|
return {
|
|
214
247
|
title: options.doctor ? 'GoodVibes Agent doctor' : 'GoodVibes Agent status',
|
|
215
248
|
workingDirectory: options.workingDirectory,
|
|
@@ -221,7 +254,9 @@ export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnap
|
|
|
221
254
|
},
|
|
222
255
|
auth: {
|
|
223
256
|
permissionMode: config.get('permissions.mode'),
|
|
224
|
-
permissionLabel:
|
|
257
|
+
permissionLabel: posture.label,
|
|
258
|
+
autoApprove: posture.autoApprove,
|
|
259
|
+
bypassesPrompts: posture.bypassesPrompts,
|
|
225
260
|
secretPolicy: config.get('storage.secretPolicy'),
|
|
226
261
|
secretPolicyLabel: secretPolicyLabel(config.get('storage.secretPolicy')),
|
|
227
262
|
runtimeAuthSignal: options.auth ?? null,
|
|
@@ -267,7 +302,8 @@ export function renderCliStatus(options: CliStatusOptions): string {
|
|
|
267
302
|
` reasoning ${String(config.get('provider.reasoningEffort'))}`,
|
|
268
303
|
'',
|
|
269
304
|
'Auth',
|
|
270
|
-
` permissions: ${
|
|
305
|
+
` permissions: ${snapshot.auth.permissionLabel} (${String(config.get('permissions.mode'))})`,
|
|
306
|
+
` autoApprove: ${yesNo(snapshot.auth.autoApprove)} (behavior.autoApprove)`,
|
|
271
307
|
` secretPolicy: ${secretPolicyLabel(config.get('storage.secretPolicy'))} (${String(config.get('storage.secretPolicy'))})`,
|
|
272
308
|
options.auth
|
|
273
309
|
? ` local auth store ${options.auth.userStorePresent ? 'present' : 'missing'} (${options.auth.userStorePath})`
|
package/src/cli/tui-startup.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { CommandContext, CommandRegistry } from '../input/command-registry.
|
|
|
2
2
|
import type { InputHandler } from '../input/handler.ts';
|
|
3
3
|
import { readOnboardingCompletionMarker } from '../runtime/onboarding/index.ts';
|
|
4
4
|
import type { GoodVibesCliParseResult } from './types.ts';
|
|
5
|
+
import { checkRecoveryFile, readLastSessionPointer } from '@/runtime/index.ts';
|
|
6
|
+
import { resolveResumableSession, surfaceResumeRelaunchNotice } from './resume-relaunch-notice.ts';
|
|
5
7
|
|
|
6
8
|
export type InteractiveTerminalCheckInput = {
|
|
7
9
|
readonly binary: string;
|
|
@@ -95,7 +97,7 @@ export function applyInitialTuiCliState(options: {
|
|
|
95
97
|
readonly input: InputHandler;
|
|
96
98
|
readonly commandRegistry: CommandRegistry;
|
|
97
99
|
readonly commandContext: CommandContext;
|
|
98
|
-
readonly shellPaths: Parameters<typeof readOnboardingCompletionMarker>[0];
|
|
100
|
+
readonly shellPaths: Parameters<typeof readOnboardingCompletionMarker>[0] & { readonly homeDirectory: string };
|
|
99
101
|
readonly render: () => void;
|
|
100
102
|
}): void {
|
|
101
103
|
const { cli, input, commandRegistry, commandContext, shellPaths, render } = options;
|
|
@@ -110,6 +112,27 @@ export function applyInitialTuiCliState(options: {
|
|
|
110
112
|
}
|
|
111
113
|
} else if (!onboardingCompletionMarker.payload) {
|
|
112
114
|
input.openAgentWorkspace(commandContext, 'setup', 'ONBOARDING');
|
|
115
|
+
} else {
|
|
116
|
+
// Normal relaunch: onboarding is done and the user didn't ask for
|
|
117
|
+
// onboarding or an explicit `sessions resume`. Surface an honest,
|
|
118
|
+
// non-blocking resume affordance instead of silently starting fresh
|
|
119
|
+
// (the W4-A4 dogfood finding) — never auto-resume, declining is
|
|
120
|
+
// frictionless (just start typing).
|
|
121
|
+
surfaceResumeRelaunchNotice({
|
|
122
|
+
getLastSessionPointer: () => readLastSessionPointer({
|
|
123
|
+
workingDirectory: shellPaths.workingDirectory,
|
|
124
|
+
homeDirectory: shellPaths.homeDirectory,
|
|
125
|
+
}),
|
|
126
|
+
isRecoveryPending: () => Boolean(checkRecoveryFile({
|
|
127
|
+
workingDirectory: shellPaths.workingDirectory,
|
|
128
|
+
homeDirectory: shellPaths.homeDirectory,
|
|
129
|
+
})),
|
|
130
|
+
findSession: (sessionId) => {
|
|
131
|
+
const sessionManager = commandContext.session?.sessionManager;
|
|
132
|
+
return sessionManager ? resolveResumableSession(sessionManager, sessionId) : null;
|
|
133
|
+
},
|
|
134
|
+
print: (text) => commandContext.print(text),
|
|
135
|
+
});
|
|
113
136
|
}
|
|
114
137
|
|
|
115
138
|
if (seededPrompt) {
|
|
@@ -5,6 +5,10 @@ import type { SecretScope, SecretStorageMedium } from './secrets.ts';
|
|
|
5
5
|
export const SECRET_CONFIG_KEYS = new Set<ConfigKey>([
|
|
6
6
|
// email section (app-layer extension, key is string-cast for ConfigKey compatibility)
|
|
7
7
|
'email.passwordRef' as unknown as ConfigKey,
|
|
8
|
+
// calendar OAuth advanced overrides — only the client SECRET is secret-backed; the
|
|
9
|
+
// client id is not a secret (RFC 8252) and stays a plain config value.
|
|
10
|
+
'calendar.google.clientSecretRef' as unknown as ConfigKey,
|
|
11
|
+
'calendar.microsoft.clientSecretRef' as unknown as ConfigKey,
|
|
8
12
|
'surfaces.slack.signingSecret',
|
|
9
13
|
'surfaces.slack.botToken',
|
|
10
14
|
'surfaces.slack.appToken',
|
|
@@ -11,7 +11,13 @@ import { LAYOUT } from '../renderer/layout.ts';
|
|
|
11
11
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
12
12
|
import { renderConversationCollapsedFragment, renderConversationEventLine } from '../renderer/conversation-surface.ts';
|
|
13
13
|
import { GLYPHS } from '../renderer/ui-primitives.ts';
|
|
14
|
+
import { activeTheme } from '../renderer/theme.ts';
|
|
14
15
|
import type { BlockMeta, ConversationMessageSnapshot } from './conversation';
|
|
16
|
+
|
|
17
|
+
// Transcript tokens are read live per render (const T = activeTheme() at the top
|
|
18
|
+
// of each render function that styles content) so a dark→light repaint
|
|
19
|
+
// re-resolves with no module reload. Dark values are byte-identical to the
|
|
20
|
+
// agent's prior static reads.
|
|
15
21
|
import { parseDiffForApply } from '@pellux/goodvibes-sdk/platform/core';
|
|
16
22
|
import { extractUserDisplayText } from '@pellux/goodvibes-sdk/platform/core';
|
|
17
23
|
|
|
@@ -39,9 +45,10 @@ export function renderConversationUserMessage(
|
|
|
39
45
|
message: Extract<Message, { role: 'user' }>,
|
|
40
46
|
width: number,
|
|
41
47
|
): void {
|
|
48
|
+
const T = activeTheme();
|
|
42
49
|
const displayText = extractUserDisplayText(message.content);
|
|
43
50
|
if (message.cancelled) {
|
|
44
|
-
context.history.addLines(UIFactory.createMessageBar(width, displayText,
|
|
51
|
+
context.history.addLines(UIFactory.createMessageBar(width, displayText, T.errorBarBg, '196', ' x ', true));
|
|
45
52
|
return;
|
|
46
53
|
}
|
|
47
54
|
context.history.addLines(UIFactory.createMessageBar(width, displayText));
|
|
@@ -55,22 +62,23 @@ export function renderConversationAssistantMessage(
|
|
|
55
62
|
collapseThreshold: number,
|
|
56
63
|
msgIdx: number,
|
|
57
64
|
): void {
|
|
65
|
+
const T = activeTheme();
|
|
58
66
|
const assistantHeaderDetails = [];
|
|
59
67
|
if (message.model) {
|
|
60
|
-
assistantHeaderDetails.push({ text: ` ${message.model}${message.provider ? ` (${message.provider})` : ''} `, fg:
|
|
68
|
+
assistantHeaderDetails.push({ text: ` ${message.model}${message.provider ? ` (${message.provider})` : ''} `, fg: T.modelNameDim, dim: true });
|
|
61
69
|
}
|
|
62
70
|
if (message.toolCalls && message.toolCalls.length > 0) {
|
|
63
|
-
assistantHeaderDetails.push({ text: ` ${GLYPHS.status.pending} tools:${message.toolCalls.length} `, fg:
|
|
71
|
+
assistantHeaderDetails.push({ text: ` ${GLYPHS.status.pending} tools:${message.toolCalls.length} `, fg: T.toolAccent });
|
|
64
72
|
}
|
|
65
73
|
if (message.reasoningContent || message.reasoningSummary) {
|
|
66
|
-
assistantHeaderDetails.push({ text: ` ${GLYPHS.status.active} reasoning `, fg:
|
|
74
|
+
assistantHeaderDetails.push({ text: ` ${GLYPHS.status.active} reasoning `, fg: T.reasoningAccent, dim: true });
|
|
67
75
|
}
|
|
68
76
|
if (assistantHeaderDetails.length > 0) {
|
|
69
77
|
context.history.addLine(renderConversationEventLine(width, {
|
|
70
78
|
marker: GLYPHS.status.active,
|
|
71
|
-
markerFg:
|
|
79
|
+
markerFg: T.assistantHeader,
|
|
72
80
|
label: 'assistant',
|
|
73
|
-
labelFg:
|
|
81
|
+
labelFg: T.assistantHeader,
|
|
74
82
|
detailFg: '244',
|
|
75
83
|
}, assistantHeaderDetails));
|
|
76
84
|
}
|
|
@@ -198,6 +206,7 @@ export function renderConversationToolMessage(
|
|
|
198
206
|
width: number,
|
|
199
207
|
msgIdx: number,
|
|
200
208
|
): void {
|
|
209
|
+
const T = activeTheme();
|
|
201
210
|
const collapseKey = `msg_${msgIdx}`;
|
|
202
211
|
const blockIdx = context.blockRegistry.length;
|
|
203
212
|
const startLine = context.history.getLineCount();
|
|
@@ -221,13 +230,13 @@ export function renderConversationToolMessage(
|
|
|
221
230
|
|
|
222
231
|
context.history.addLine(renderConversationEventLine(width, {
|
|
223
232
|
marker: blockType === 'diff' ? GLYPHS.status.dualPane : GLYPHS.status.active,
|
|
224
|
-
markerFg: blockType === 'diff' ?
|
|
233
|
+
markerFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
|
|
225
234
|
label: blockType === 'diff' ? 'diff' : 'tool result',
|
|
226
|
-
labelFg: blockType === 'diff' ?
|
|
235
|
+
labelFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
|
|
227
236
|
detailFg: '244',
|
|
228
237
|
}, [
|
|
229
238
|
...(message.toolName
|
|
230
|
-
? [{ text: ` ${friendlyToolLabel(message.toolName)} `, fg:
|
|
239
|
+
? [{ text: ` ${friendlyToolLabel(message.toolName)} `, fg: T.toolNameFg }]
|
|
231
240
|
: [{ text: ` ${summarizeCallId(message.callId || 'standalone')} `, fg: '244' as const, dim: true }]),
|
|
232
241
|
{ text: ` ${isCollapsed ? GLYPHS.navigation.collapsed : GLYPHS.navigation.expanded} ${lineCount} line${lineCount === 1 ? '' : 's'} `, fg: '244', dim: true },
|
|
233
242
|
]));
|
|
@@ -242,9 +251,9 @@ export function renderConversationToolMessage(
|
|
|
242
251
|
: preview;
|
|
243
252
|
const rendered = renderConversationCollapsedFragment(collapsedText, width, {
|
|
244
253
|
prefix: blockType === 'diff' ? ` ${GLYPHS.status.dualPane} ` : ` ${GLYPHS.navigation.collapsed} `,
|
|
245
|
-
prefixFg: blockType === 'diff' ?
|
|
254
|
+
prefixFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
|
|
246
255
|
text: '244',
|
|
247
|
-
bodyBg:
|
|
256
|
+
bodyBg: T.collapsedBodyBg,
|
|
248
257
|
dim: true,
|
|
249
258
|
});
|
|
250
259
|
context.history.addLines(rendered);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* focus-tracker — tracks whether the terminal window currently has OS-level
|
|
3
|
+
* input focus, using the terminal focus-reporting protocol (DECSET ?1004h,
|
|
4
|
+
* enabled at terminal setup in main.ts). The SDK's InputTokenizer already
|
|
5
|
+
* parses the resulting `\x1b[I` (focus-in) / `\x1b[O` (focus-out) sequences
|
|
6
|
+
* into `{ type: 'focus', action }` tokens (platform/core/tokenizer.ts); this
|
|
7
|
+
* module is the single place those tokens land (consumed in
|
|
8
|
+
* src/input/handler-feed.ts's feedInputTokens()).
|
|
9
|
+
*
|
|
10
|
+
* Honest degradation: a terminal that does not implement focus reporting
|
|
11
|
+
* (or a multiplexer that swallows the sequences) never sends focus tokens.
|
|
12
|
+
* In that case `isFocused()` stays `null` forever — "unknown", never a
|
|
13
|
+
* guessed `true`/`false`. Callers that gate a user-facing behavior on focus
|
|
14
|
+
* (e.g. the unfocused-alert notifiers in this directory) must treat `null`
|
|
15
|
+
* the same as "not focused" per the fallback rule: alerts fire when the
|
|
16
|
+
* terminal is definitely unfocused OR when focus state was never observed.
|
|
17
|
+
*/
|
|
18
|
+
export class FocusTracker {
|
|
19
|
+
private focused: boolean | null = null;
|
|
20
|
+
|
|
21
|
+
/** True/false once at least one focus token has arrived; null if none ever has. */
|
|
22
|
+
isFocused(): boolean | null {
|
|
23
|
+
return this.focused;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Called from the input pipeline on every focus token. */
|
|
27
|
+
setFocused(value: boolean): void {
|
|
28
|
+
this.focused = value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The alert-gating rule shared by every unfocused-alert notifier: fire
|
|
33
|
+
* when the terminal is known to be unfocused, or when focus was never
|
|
34
|
+
* observed (honest fallback — never silently suppress on an unknown
|
|
35
|
+
* terminal). Only `isFocused() === true` (a real, observed focus-in with
|
|
36
|
+
* no observed focus-out since) suppresses an alert.
|
|
37
|
+
*/
|
|
38
|
+
shouldAlertWhenUnfocused(): boolean {
|
|
39
|
+
return this.focused !== true;
|
|
40
|
+
}
|
|
41
|
+
}
|