@pellux/goodvibes-agent 1.1.3 → 1.1.5
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 +13 -0
- package/dist/package/main.js +1780 -939
- package/package.json +1 -1
- package/src/input/agent-workspace-activation.ts +21 -1
- package/src/input/agent-workspace-basic-command-editors.ts +8 -9
- package/src/input/agent-workspace-categories.ts +263 -115
- package/src/input/agent-workspace-category-actions.ts +25 -0
- package/src/input/agent-workspace-host-category.ts +57 -0
- package/src/input/agent-workspace-local-editor-submission.ts +193 -0
- package/src/input/agent-workspace-search.ts +6 -0
- package/src/input/agent-workspace-settings.ts +314 -0
- package/src/input/agent-workspace-setup.ts +20 -0
- package/src/input/agent-workspace-snapshot.ts +89 -0
- package/src/input/agent-workspace-subscription-editor.ts +214 -0
- package/src/input/agent-workspace-types.ts +56 -1
- package/src/input/agent-workspace.ts +139 -164
- package/src/input/handler-modal-token-routes.ts +12 -12
- package/src/input/handler.ts +2 -1
- package/src/main.ts +11 -7
- package/src/renderer/agent-workspace.ts +80 -14
- package/src/shell/agent-workspace-fullscreen.ts +11 -3
- package/src/tools/agent-harness-ui-surface-metadata.ts +1 -1
- package/src/tools/agent-harness-workspace-actions.ts +2 -2
- package/src/version.ts +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AgentWorkspaceCategory } from './agent-workspace-types.ts';
|
|
2
|
+
|
|
3
|
+
export const AGENT_WORKSPACE_HOST_CATEGORY: AgentWorkspaceCategory = {
|
|
4
|
+
id: 'host',
|
|
5
|
+
group: 'OPERATIONS',
|
|
6
|
+
label: 'Connected Host',
|
|
7
|
+
summary: 'Connected-host health, tasks, sessions, channels, and automation.',
|
|
8
|
+
detail: 'Use this workspace to inspect the GoodVibes host surfaces that Agent can see: system health, remote routes, host tasks, sessions, channels, schedules, knowledge, media, MCP, provider auth, support bundles, and telemetry/config posture.',
|
|
9
|
+
actions: [
|
|
10
|
+
{ id: 'host-overview', label: 'Host overview', detail: 'Review connected-host, provider, settings, continuity, and Agent health without starting or mutating host work.', command: '/health review', kind: 'command', safety: 'read-only' },
|
|
11
|
+
{ id: 'host-services', label: 'Host services', detail: 'Inspect connected-host service readiness, credentials, and integration issues without changing them.', command: '/health host', kind: 'command', safety: 'read-only' },
|
|
12
|
+
{ id: 'host-setup', label: 'Setup review', detail: 'Inspect setup issues that affect connected-host and Agent runtime readiness.', command: '/health setup', kind: 'command', safety: 'read-only' },
|
|
13
|
+
{ id: 'host-remote', label: 'Remote routes', detail: 'Inspect remote build-host route health and recovery signals without starting build work.', command: '/health remote', kind: 'command', safety: 'read-only' },
|
|
14
|
+
{ id: 'host-maintenance', label: 'Session maintenance', detail: 'Review continuity, compaction, and current-session maintenance posture.', command: '/health maintenance', kind: 'command', safety: 'read-only' },
|
|
15
|
+
{ id: 'host-continuity', label: 'Continuity', detail: 'Inspect last-session pointer, recovery-file posture, and return-context state.', command: '/health continuity', kind: 'command', safety: 'read-only' },
|
|
16
|
+
{ id: 'host-tasks', label: 'Host tasks', detail: 'Inspect connected-host task state without creating, retrying, or mutating tasks.', command: '/tasks list', kind: 'command', safety: 'read-only' },
|
|
17
|
+
{ id: 'host-task-filter', label: 'Filter host tasks', detail: 'Open a status/type form for read-only connected-host task filtering.', editorKind: 'task-list-filter', kind: 'editor', safety: 'read-only' },
|
|
18
|
+
{ id: 'host-task-show', label: 'Inspect host task', detail: 'Open a task-id form for read-only connected-host task metadata.', editorKind: 'task-show', kind: 'editor', safety: 'read-only' },
|
|
19
|
+
{ id: 'host-task-output', label: 'Show task output', detail: 'Open a task-id form for read-only connected-host task output.', editorKind: 'task-output', kind: 'editor', safety: 'read-only' },
|
|
20
|
+
{ id: 'host-sessions', label: 'Host session routes', detail: 'Browse saved Agent sessions and return-context posture exposed through the runtime session surface.', command: '/session list', kind: 'command', safety: 'read-only' },
|
|
21
|
+
{ id: 'host-session-graph', label: 'Session graph', detail: 'Open a read-only form for cross-session graph inspection; graph mutation remains blocked in Agent.', editorKind: 'session-graph', kind: 'editor', safety: 'read-only' },
|
|
22
|
+
{ id: 'host-channels-status', label: 'Channel status', detail: 'Inspect connected channel runtime status from the host without mutating delivery state.', command: '/channels status', kind: 'command', safety: 'read-only' },
|
|
23
|
+
{ id: 'host-channel-accounts', label: 'Channel accounts', detail: 'Inspect connected channel accounts without printing secret values or sending messages.', command: '/channels accounts', kind: 'command', safety: 'read-only' },
|
|
24
|
+
{ id: 'host-channel-policies', label: 'Channel policies', detail: 'Inspect channel delivery policy posture without changing routing.', command: '/channels policies', kind: 'command', safety: 'read-only' },
|
|
25
|
+
{ id: 'host-channels', label: 'Open Channels', detail: 'Jump to the channel setup and delivery workspace.', targetCategoryId: 'channels', kind: 'workspace', safety: 'safe' },
|
|
26
|
+
{ id: 'host-automation', label: 'Open Automation', detail: 'Jump to connected schedules, reminders, receipts, and explicit run controls.', targetCategoryId: 'automation', kind: 'workspace', safety: 'safe' },
|
|
27
|
+
{ id: 'host-schedules', label: 'Schedule status', detail: 'Inspect schedules and run history without running or mutating them.', command: '/schedule list', kind: 'command', safety: 'read-only' },
|
|
28
|
+
{ id: 'host-schedule-reconcile', label: 'Reconcile schedules', detail: 'Compare local promotion receipts with live connected schedules.', command: '/schedule reconcile', kind: 'command', safety: 'read-only' },
|
|
29
|
+
{ id: 'host-knowledge', label: 'Knowledge route', detail: 'Inspect isolated Agent Knowledge route readiness and source counts.', command: '/knowledge status', kind: 'command', safety: 'read-only' },
|
|
30
|
+
{ id: 'host-knowledge-open', label: 'Open Knowledge', detail: 'Jump to Agent Knowledge source, graph, review, and ingest controls.', targetCategoryId: 'knowledge', kind: 'workspace', safety: 'safe' },
|
|
31
|
+
{ id: 'host-media', label: 'Media providers', detail: 'Inspect configured media providers before generating media with confirmation.', command: '/media providers', kind: 'command', safety: 'read-only' },
|
|
32
|
+
{ id: 'host-media-open', label: 'Open Voice and Media', detail: 'Jump to voice, TTS, image input, media provider, and browser-tool setup actions.', targetCategoryId: 'voice-media', kind: 'workspace', safety: 'safe' },
|
|
33
|
+
{ id: 'host-mcp-health', label: 'MCP health', detail: 'Inspect MCP lifecycle issues without changing trust posture or approving tool-definition review.', command: '/health mcp', kind: 'command', safety: 'read-only' },
|
|
34
|
+
{ id: 'host-mcp-review', label: 'MCP review', detail: 'Inspect MCP server connection, trust, role, and review posture.', command: '/mcp review', kind: 'command', safety: 'read-only' },
|
|
35
|
+
{ id: 'host-tools-open', label: 'Open Tools and MCP', detail: 'Jump to MCP server setup, tool inventory, secrets, and security review.', targetCategoryId: 'tools', kind: 'workspace', safety: 'safe' },
|
|
36
|
+
{ id: 'host-provider-accounts', label: 'Provider accounts', detail: 'Review provider account routes, subscription windows, and billing-path safety.', command: '/accounts review', kind: 'command', safety: 'read-only' },
|
|
37
|
+
{ id: 'host-provider-detail', label: 'Provider detail', detail: 'Open a provider-id form for account and provider configuration review.', editorKind: 'provider-inspect', kind: 'editor', safety: 'read-only' },
|
|
38
|
+
{ id: 'host-provider-routes', label: 'Provider routes', detail: 'Open a provider-id form for account, subscription, and route inspection.', editorKind: 'provider-routes', kind: 'editor', safety: 'read-only' },
|
|
39
|
+
{ id: 'host-provider-repair', label: 'Provider repair guidance', detail: 'Open a provider-id form for read-only provider repair guidance.', editorKind: 'provider-account-repair', kind: 'editor', safety: 'read-only' },
|
|
40
|
+
{ id: 'host-auth-owner', label: 'Connected-host auth owner', detail: 'Show that connected-host auth administration belongs to the owning GoodVibes host, not Agent.', command: '/auth local', kind: 'command', safety: 'read-only' },
|
|
41
|
+
{ id: 'host-auth-review', label: 'Provider auth review', detail: 'Review provider auth posture without printing token values.', command: '/auth review', kind: 'command', safety: 'read-only' },
|
|
42
|
+
{ id: 'host-auth-detail', label: 'Provider auth detail', detail: 'Open a provider-id form for read-only provider auth inspection.', editorKind: 'auth-show', kind: 'editor', safety: 'read-only' },
|
|
43
|
+
{ id: 'host-auth-repair', label: 'Provider auth repair', detail: 'Open a provider-id form for provider auth repair guidance.', editorKind: 'auth-repair', kind: 'editor', safety: 'read-only' },
|
|
44
|
+
{ id: 'host-auth-bundle-export', label: 'Export auth bundle', detail: 'Open a confirmed form that exports a redacted provider auth review bundle.', editorKind: 'auth-bundle-export', kind: 'editor', safety: 'safe' },
|
|
45
|
+
{ id: 'host-auth-bundle-inspect', label: 'Inspect auth bundle', detail: 'Open a form that inspects a provider auth review bundle before sharing or import.', editorKind: 'auth-bundle-inspect', kind: 'editor', safety: 'read-only' },
|
|
46
|
+
{ id: 'host-subscription-bundle-export', label: 'Export subscription bundle', detail: 'Open a confirmed form that exports redacted provider subscription state for review.', editorKind: 'subscription-bundle-export', kind: 'editor', safety: 'safe' },
|
|
47
|
+
{ id: 'host-subscription-bundle-inspect', label: 'Inspect subscription bundle', detail: 'Open a form that inspects provider subscription state before sharing.', editorKind: 'subscription-bundle-inspect', kind: 'editor', safety: 'read-only' },
|
|
48
|
+
{ id: 'host-security', label: 'Security review', detail: 'Inspect token posture, MCP attack paths, policy lint, plugin risk, and incident pressure.', command: '/security review', kind: 'command', safety: 'read-only' },
|
|
49
|
+
{ id: 'host-trust', label: 'Trust review', detail: 'Review permission, secret, plugin, and MCP trust posture without exporting bundles or changing trust.', command: '/trust review', kind: 'command', safety: 'read-only' },
|
|
50
|
+
{ id: 'host-support-bundle-export', label: 'Export support bundle', detail: 'Export a redacted Agent support bundle from the Host page.', editorKind: 'support-bundle-export', kind: 'editor', safety: 'safe' },
|
|
51
|
+
{ id: 'host-support-bundle-inspect', label: 'Inspect support bundle', detail: 'Inspect a support bundle before import or sharing.', editorKind: 'support-bundle-inspect', kind: 'editor', safety: 'read-only' },
|
|
52
|
+
{ id: 'host-support-bundle-import', label: 'Import support bundle', detail: 'Import reviewed, non-redacted config values from a support bundle.', editorKind: 'support-bundle-import', kind: 'editor', safety: 'safe' },
|
|
53
|
+
{ id: 'host-telemetry', label: 'Telemetry settings', detail: 'Open telemetry payload policy settings for connected-host and Agent observability review.', command: '/config telemetry', kind: 'command', safety: 'safe' },
|
|
54
|
+
{ id: 'host-config', label: 'Control-plane settings', detail: 'Open configuration controls for service, channels, tools, automation, provider, and storage posture.', command: '/config', kind: 'command', safety: 'safe' },
|
|
55
|
+
{ id: 'host-safety', label: 'Host mutation policy', detail: 'Agent may inspect connected-host surfaces and run explicit confirmed actions, but does not silently create tasks, send channel messages, mutate auth, or start automation.', kind: 'guidance', safety: 'blocked' },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
2
|
+
import { AgentNoteRegistry } from '../agent/note-registry.ts';
|
|
3
|
+
import { AgentPersonaRegistry } from '../agent/persona-registry.ts';
|
|
4
|
+
import { AgentRoutineRegistry } from '../agent/routine-registry.ts';
|
|
5
|
+
import { createAgentRuntimeProfile, type AgentRuntimeProfileInfo } from '../agent/runtime-profile.ts';
|
|
6
|
+
import { AgentSkillRegistry } from '../agent/skill-registry.ts';
|
|
7
|
+
import { createAgentWorkspaceLearnedBehavior } from './agent-workspace-learned-behavior.ts';
|
|
8
|
+
import { buildAgentWorkspaceRequirements } from './agent-workspace-requirements.ts';
|
|
9
|
+
import { isAffirmative, splitList } from './agent-workspace-editors.ts';
|
|
10
|
+
import type { AgentWorkspaceLocalEditor, AgentWorkspaceLocalEditorKind } from './agent-workspace-types.ts';
|
|
11
|
+
|
|
12
|
+
type LearnedBehaviorTarget = Exclude<AgentWorkspaceLocalEditorKind, 'memory' | 'note' | 'profile'>;
|
|
13
|
+
|
|
14
|
+
export interface AgentWorkspaceLocalEditorSubmissionCallbacks {
|
|
15
|
+
readonly readField: (id: string) => string;
|
|
16
|
+
readonly learnedBehaviorTarget: () => LearnedBehaviorTarget;
|
|
17
|
+
readonly submitDeleteEditor: () => void;
|
|
18
|
+
readonly finishLocalEditor: (kind: AgentWorkspaceLocalEditorKind, id: string, name: string, verb: 'Created' | 'Updated') => void;
|
|
19
|
+
readonly finishProfileEditor: (profile: AgentRuntimeProfileInfo) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function submitAgentWorkspaceLocalRegistryEditor(
|
|
23
|
+
shellPaths: ShellPathService,
|
|
24
|
+
editor: AgentWorkspaceLocalEditor,
|
|
25
|
+
callbacks: AgentWorkspaceLocalEditorSubmissionCallbacks,
|
|
26
|
+
): void {
|
|
27
|
+
const field = callbacks.readField;
|
|
28
|
+
if (editor.mode === 'delete') {
|
|
29
|
+
callbacks.submitDeleteEditor();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (editor.kind === 'learned-behavior') {
|
|
33
|
+
const created = createAgentWorkspaceLearnedBehavior(shellPaths, {
|
|
34
|
+
target: callbacks.learnedBehaviorTarget(),
|
|
35
|
+
name: field('name'),
|
|
36
|
+
description: field('description'),
|
|
37
|
+
notes: field('notes'),
|
|
38
|
+
tags: splitList(field('tags')),
|
|
39
|
+
triggers: splitList(field('triggers')),
|
|
40
|
+
enable: isAffirmative(field('enable')),
|
|
41
|
+
});
|
|
42
|
+
callbacks.finishLocalEditor(created.kind, created.id, created.name, 'Created');
|
|
43
|
+
} else if (editor.kind === 'profile') {
|
|
44
|
+
const template = field('template');
|
|
45
|
+
const templateId = template && template.toLowerCase() !== 'none' ? template : undefined;
|
|
46
|
+
const profile = createAgentRuntimeProfile(shellPaths.homeDirectory, field('name'), {
|
|
47
|
+
...(templateId ? { templateId } : {}),
|
|
48
|
+
});
|
|
49
|
+
callbacks.finishProfileEditor(profile);
|
|
50
|
+
} else if (editor.kind === 'note') {
|
|
51
|
+
submitNoteEditor(shellPaths, editor, field, callbacks.finishLocalEditor);
|
|
52
|
+
} else if (editor.kind === 'persona') {
|
|
53
|
+
submitPersonaEditor(shellPaths, editor, field, callbacks.finishLocalEditor);
|
|
54
|
+
} else if (editor.kind === 'skill') {
|
|
55
|
+
submitSkillEditor(shellPaths, editor, field, callbacks.finishLocalEditor);
|
|
56
|
+
} else {
|
|
57
|
+
submitRoutineEditor(shellPaths, editor, field, callbacks.finishLocalEditor);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function submitNoteEditor(
|
|
62
|
+
shellPaths: ShellPathService,
|
|
63
|
+
editor: AgentWorkspaceLocalEditor,
|
|
64
|
+
field: (id: string) => string,
|
|
65
|
+
finish: AgentWorkspaceLocalEditorSubmissionCallbacks['finishLocalEditor'],
|
|
66
|
+
): void {
|
|
67
|
+
const registry = AgentNoteRegistry.fromShellPaths(shellPaths);
|
|
68
|
+
if (editor.mode === 'update' && editor.recordId) {
|
|
69
|
+
const updated = registry.update(editor.recordId, {
|
|
70
|
+
title: field('title'),
|
|
71
|
+
body: field('body'),
|
|
72
|
+
sourceUrl: field('sourceUrl'),
|
|
73
|
+
tags: splitList(field('tags')),
|
|
74
|
+
provenance: 'Workspace',
|
|
75
|
+
});
|
|
76
|
+
finish('note', updated.id, updated.title, 'Updated');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const created = registry.create({
|
|
80
|
+
title: field('title'),
|
|
81
|
+
body: field('body'),
|
|
82
|
+
sourceUrl: field('sourceUrl'),
|
|
83
|
+
tags: splitList(field('tags')),
|
|
84
|
+
source: 'user',
|
|
85
|
+
provenance: 'Workspace',
|
|
86
|
+
});
|
|
87
|
+
finish('note', created.id, created.title, 'Created');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function submitPersonaEditor(
|
|
91
|
+
shellPaths: ShellPathService,
|
|
92
|
+
editor: AgentWorkspaceLocalEditor,
|
|
93
|
+
field: (id: string) => string,
|
|
94
|
+
finish: AgentWorkspaceLocalEditorSubmissionCallbacks['finishLocalEditor'],
|
|
95
|
+
): void {
|
|
96
|
+
const registry = AgentPersonaRegistry.fromShellPaths(shellPaths);
|
|
97
|
+
if (editor.mode === 'update' && editor.recordId) {
|
|
98
|
+
const wasActive = registry.snapshot().activePersonaId === editor.recordId;
|
|
99
|
+
const updated = registry.update(editor.recordId, {
|
|
100
|
+
name: field('name'),
|
|
101
|
+
description: field('description'),
|
|
102
|
+
body: field('body'),
|
|
103
|
+
tags: splitList(field('tags')),
|
|
104
|
+
triggers: splitList(field('triggers')),
|
|
105
|
+
provenance: 'Workspace',
|
|
106
|
+
});
|
|
107
|
+
if (isAffirmative(field('activate'))) registry.setActive(updated.id);
|
|
108
|
+
else if (wasActive) registry.clearActive();
|
|
109
|
+
finish('persona', updated.id, updated.name, 'Updated');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const created = registry.create({
|
|
113
|
+
name: field('name'),
|
|
114
|
+
description: field('description'),
|
|
115
|
+
body: field('body'),
|
|
116
|
+
tags: splitList(field('tags')),
|
|
117
|
+
triggers: splitList(field('triggers')),
|
|
118
|
+
source: 'user',
|
|
119
|
+
provenance: 'Workspace',
|
|
120
|
+
});
|
|
121
|
+
if (isAffirmative(field('activate'))) registry.setActive(created.id);
|
|
122
|
+
finish('persona', created.id, created.name, 'Created');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function submitSkillEditor(
|
|
126
|
+
shellPaths: ShellPathService,
|
|
127
|
+
editor: AgentWorkspaceLocalEditor,
|
|
128
|
+
field: (id: string) => string,
|
|
129
|
+
finish: AgentWorkspaceLocalEditorSubmissionCallbacks['finishLocalEditor'],
|
|
130
|
+
): void {
|
|
131
|
+
const registry = AgentSkillRegistry.fromShellPaths(shellPaths);
|
|
132
|
+
if (editor.mode === 'update' && editor.recordId) {
|
|
133
|
+
const updated = registry.update(editor.recordId, {
|
|
134
|
+
name: field('name'),
|
|
135
|
+
description: field('description'),
|
|
136
|
+
procedure: field('procedure'),
|
|
137
|
+
triggers: splitList(field('triggers')),
|
|
138
|
+
tags: splitList(field('tags')),
|
|
139
|
+
requirements: buildAgentWorkspaceRequirements(field),
|
|
140
|
+
provenance: 'Workspace',
|
|
141
|
+
});
|
|
142
|
+
registry.setEnabled(updated.id, isAffirmative(field('enabled')));
|
|
143
|
+
finish('skill', updated.id, updated.name, 'Updated');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const created = registry.create({
|
|
147
|
+
name: field('name'),
|
|
148
|
+
description: field('description'),
|
|
149
|
+
procedure: field('procedure'),
|
|
150
|
+
triggers: splitList(field('triggers')),
|
|
151
|
+
tags: splitList(field('tags')),
|
|
152
|
+
requirements: buildAgentWorkspaceRequirements(field),
|
|
153
|
+
enabled: isAffirmative(field('enabled')),
|
|
154
|
+
source: 'user',
|
|
155
|
+
provenance: 'Workspace',
|
|
156
|
+
});
|
|
157
|
+
finish('skill', created.id, created.name, 'Created');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function submitRoutineEditor(
|
|
161
|
+
shellPaths: ShellPathService,
|
|
162
|
+
editor: AgentWorkspaceLocalEditor,
|
|
163
|
+
field: (id: string) => string,
|
|
164
|
+
finish: AgentWorkspaceLocalEditorSubmissionCallbacks['finishLocalEditor'],
|
|
165
|
+
): void {
|
|
166
|
+
const registry = AgentRoutineRegistry.fromShellPaths(shellPaths);
|
|
167
|
+
if (editor.mode === 'update' && editor.recordId) {
|
|
168
|
+
const updated = registry.update(editor.recordId, {
|
|
169
|
+
name: field('name'),
|
|
170
|
+
description: field('description'),
|
|
171
|
+
steps: field('steps'),
|
|
172
|
+
triggers: splitList(field('triggers')),
|
|
173
|
+
tags: splitList(field('tags')),
|
|
174
|
+
requirements: buildAgentWorkspaceRequirements(field),
|
|
175
|
+
provenance: 'Workspace',
|
|
176
|
+
});
|
|
177
|
+
registry.setEnabled(updated.id, isAffirmative(field('enabled')));
|
|
178
|
+
finish('routine', updated.id, updated.name, 'Updated');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const created = registry.create({
|
|
182
|
+
name: field('name'),
|
|
183
|
+
description: field('description'),
|
|
184
|
+
steps: field('steps'),
|
|
185
|
+
triggers: splitList(field('triggers')),
|
|
186
|
+
tags: splitList(field('tags')),
|
|
187
|
+
requirements: buildAgentWorkspaceRequirements(field),
|
|
188
|
+
enabled: isAffirmative(field('enabled')),
|
|
189
|
+
source: 'user',
|
|
190
|
+
provenance: 'Workspace',
|
|
191
|
+
});
|
|
192
|
+
finish('routine', created.id, created.name, 'Created');
|
|
193
|
+
}
|
|
@@ -40,6 +40,9 @@ export function searchAgentWorkspaceActions(
|
|
|
40
40
|
action.command ?? '',
|
|
41
41
|
action.editorKind ?? '',
|
|
42
42
|
action.targetCategoryId ?? '',
|
|
43
|
+
action.modelPickerFlow ?? '',
|
|
44
|
+
action.modelPickerTarget ?? '',
|
|
45
|
+
action.settingsTarget ?? '',
|
|
43
46
|
action.localOperation ?? '',
|
|
44
47
|
action.safety,
|
|
45
48
|
].join(' ').toLowerCase();
|
|
@@ -80,6 +83,9 @@ function scoreActionSearchResult(
|
|
|
80
83
|
score += scoreField(action.editorKind, terms, exactQuery, 85, 26);
|
|
81
84
|
score += scoreField(action.command, terms, exactQuery, 75, 22);
|
|
82
85
|
score += scoreField(action.label, terms, exactQuery, 65, 18);
|
|
86
|
+
score += scoreField(action.modelPickerTarget, terms, exactQuery, 58, 18);
|
|
87
|
+
score += scoreField(action.settingsTarget, terms, exactQuery, 58, 18);
|
|
88
|
+
score += scoreField(action.modelPickerFlow, terms, exactQuery, 54, 16);
|
|
83
89
|
score += scoreField(action.localOperation, terms, exactQuery, 50, 16);
|
|
84
90
|
score += scoreField(action.targetCategoryId, terms, exactQuery, 40, 12);
|
|
85
91
|
score += scoreField(action.detail, terms, exactQuery, 24, 6);
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import type { ConfigKey, ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
|
|
3
|
+
import { setHarnessSetting } from '../agent/harness-control.ts';
|
|
4
|
+
import { isExternalHostOwnedSettingKey } from '../config/agent-settings-policy.ts';
|
|
5
|
+
import { buildAgentWorkspaceRuntimeSnapshot } from './agent-workspace-snapshot.ts';
|
|
6
|
+
import type { CommandContext } from './command-registry.ts';
|
|
7
|
+
import type {
|
|
8
|
+
AgentWorkspaceAction,
|
|
9
|
+
AgentWorkspaceActionResult,
|
|
10
|
+
AgentWorkspaceLocalEditor,
|
|
11
|
+
AgentWorkspaceRuntimeSnapshot,
|
|
12
|
+
} from './agent-workspace-types.ts';
|
|
13
|
+
|
|
14
|
+
const GOODVIBES_TUI_SURFACE_ROOT = 'tui';
|
|
15
|
+
|
|
16
|
+
const TUI_IMPORTABLE_SETTING_PREFIXES = [
|
|
17
|
+
'display.',
|
|
18
|
+
'provider.',
|
|
19
|
+
'behavior.',
|
|
20
|
+
'storage.',
|
|
21
|
+
'permissions.',
|
|
22
|
+
'ui.',
|
|
23
|
+
'tts.',
|
|
24
|
+
'surfaces.',
|
|
25
|
+
'helper.',
|
|
26
|
+
'tools.',
|
|
27
|
+
'release.',
|
|
28
|
+
'automation.',
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
type SettingActionEffect =
|
|
32
|
+
| {
|
|
33
|
+
readonly kind: 'result';
|
|
34
|
+
readonly status: string;
|
|
35
|
+
readonly result: AgentWorkspaceActionResult;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
readonly kind: 'apply';
|
|
39
|
+
readonly setting: ConfigSetting;
|
|
40
|
+
readonly value: unknown;
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
readonly kind: 'editor';
|
|
44
|
+
readonly editor: AgentWorkspaceLocalEditor;
|
|
45
|
+
readonly status: string;
|
|
46
|
+
readonly result: AgentWorkspaceActionResult;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export interface SettingMutationOutcome {
|
|
50
|
+
readonly status: string;
|
|
51
|
+
readonly result: AgentWorkspaceActionResult;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface TuiSettingsImportOutcome extends SettingMutationOutcome {
|
|
55
|
+
readonly runtimeSnapshot: AgentWorkspaceRuntimeSnapshot | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
59
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readJsonRecord(path: string): Record<string, unknown> | null {
|
|
63
|
+
if (!existsSync(path)) return null;
|
|
64
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8')) as unknown;
|
|
65
|
+
return isRecord(parsed) ? parsed : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readNestedSettingValue(record: Record<string, unknown>, key: string): unknown {
|
|
69
|
+
let cursor: unknown = record;
|
|
70
|
+
for (const part of key.split('.')) {
|
|
71
|
+
if (!isRecord(cursor) || !(part in cursor)) return undefined;
|
|
72
|
+
cursor = cursor[part];
|
|
73
|
+
}
|
|
74
|
+
return cursor;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function canImportTuiSetting(key: string): boolean {
|
|
78
|
+
return !isExternalHostOwnedSettingKey(key)
|
|
79
|
+
&& TUI_IMPORTABLE_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function valuesMatch(left: unknown, right: unknown): boolean {
|
|
83
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function agentWorkspaceSettingSchema(context: CommandContext | null, key: string): ConfigSetting | null {
|
|
87
|
+
return context?.platform?.configManager
|
|
88
|
+
?.getSchema()
|
|
89
|
+
.find((setting) => setting.key === key) ?? null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isAgentWorkspaceActionVisible(context: CommandContext | null, action: AgentWorkspaceAction): boolean {
|
|
93
|
+
const key = action.visibleWhenSettingKey?.trim();
|
|
94
|
+
if (!key) return true;
|
|
95
|
+
const configManager = context?.platform?.configManager;
|
|
96
|
+
if (!configManager) return false;
|
|
97
|
+
return configManager.get(key as ConfigKey) === action.visibleWhenSettingValue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function buildAgentWorkspaceSettingActionEffect(
|
|
101
|
+
context: CommandContext | null,
|
|
102
|
+
action: AgentWorkspaceAction,
|
|
103
|
+
): SettingActionEffect {
|
|
104
|
+
const settingKey = action.settingKey?.trim();
|
|
105
|
+
const configManager = context?.platform?.configManager;
|
|
106
|
+
if (!settingKey || !configManager) {
|
|
107
|
+
return {
|
|
108
|
+
kind: 'result',
|
|
109
|
+
status: 'Setting is unavailable in this runtime.',
|
|
110
|
+
result: {
|
|
111
|
+
kind: 'error',
|
|
112
|
+
title: 'Setting unavailable',
|
|
113
|
+
detail: action.detail,
|
|
114
|
+
safety: action.safety,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const setting = agentWorkspaceSettingSchema(context, settingKey);
|
|
119
|
+
if (!setting) {
|
|
120
|
+
return {
|
|
121
|
+
kind: 'result',
|
|
122
|
+
status: `Unknown setting: ${settingKey}`,
|
|
123
|
+
result: {
|
|
124
|
+
kind: 'error',
|
|
125
|
+
title: 'Unknown setting',
|
|
126
|
+
detail: `No Agent setting exists for ${settingKey}.`,
|
|
127
|
+
safety: action.safety,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (action.settingValueHint !== undefined) {
|
|
133
|
+
return { kind: 'apply', setting, value: action.settingValueHint };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const currentValue = configManager.get(setting.key as ConfigKey);
|
|
137
|
+
if (setting.type === 'boolean') {
|
|
138
|
+
return { kind: 'apply', setting, value: !Boolean(currentValue) };
|
|
139
|
+
}
|
|
140
|
+
if (setting.type === 'enum' && setting.enumValues && setting.enumValues.length > 0) {
|
|
141
|
+
const currentIndex = Math.max(0, setting.enumValues.indexOf(String(currentValue)));
|
|
142
|
+
return { kind: 'apply', setting, value: setting.enumValues[(currentIndex + 1) % setting.enumValues.length]! };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
kind: 'editor',
|
|
147
|
+
editor: createSettingEditor(setting, String(currentValue ?? ''), action),
|
|
148
|
+
status: `Editing ${setting.key}.`,
|
|
149
|
+
result: {
|
|
150
|
+
kind: 'guidance',
|
|
151
|
+
title: `Edit ${setting.key}`,
|
|
152
|
+
detail: setting.description,
|
|
153
|
+
safety: action.safety,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function applyAgentWorkspaceSettingValue(
|
|
159
|
+
context: CommandContext | null,
|
|
160
|
+
setting: ConfigSetting,
|
|
161
|
+
value: unknown,
|
|
162
|
+
): Promise<SettingMutationOutcome> {
|
|
163
|
+
const configManager = context?.platform?.configManager;
|
|
164
|
+
if (!configManager) {
|
|
165
|
+
return {
|
|
166
|
+
status: 'Setting is unavailable in this runtime.',
|
|
167
|
+
result: {
|
|
168
|
+
kind: 'error',
|
|
169
|
+
title: `${setting.key} update failed`,
|
|
170
|
+
detail: 'The Agent workspace has no config manager for this runtime.',
|
|
171
|
+
safety: 'safe',
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const result = await setHarnessSetting(configManager, context?.platform?.secretsManager, setting.key, value);
|
|
177
|
+
return {
|
|
178
|
+
status: `${result.key} set.`,
|
|
179
|
+
result: {
|
|
180
|
+
kind: 'refreshed',
|
|
181
|
+
title: `${result.key} updated`,
|
|
182
|
+
detail: `Current value: ${String(result.current)}`,
|
|
183
|
+
safety: 'safe',
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
188
|
+
return {
|
|
189
|
+
status: detail,
|
|
190
|
+
result: {
|
|
191
|
+
kind: 'error',
|
|
192
|
+
title: `${setting.key} update failed`,
|
|
193
|
+
detail,
|
|
194
|
+
safety: 'safe',
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function importAgentWorkspaceTuiSettings(context: CommandContext | null): Promise<TuiSettingsImportOutcome> {
|
|
201
|
+
const shellPaths = context?.workspace?.shellPaths;
|
|
202
|
+
const configManager = context?.platform?.configManager;
|
|
203
|
+
if (!shellPaths || !configManager) {
|
|
204
|
+
return {
|
|
205
|
+
status: 'GoodVibes TUI settings import is unavailable in this runtime.',
|
|
206
|
+
runtimeSnapshot: null,
|
|
207
|
+
result: {
|
|
208
|
+
kind: 'error',
|
|
209
|
+
title: 'Import unavailable',
|
|
210
|
+
detail: 'The workspace cannot locate shell paths or the Agent config manager.',
|
|
211
|
+
safety: 'safe',
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const sources = [
|
|
217
|
+
{ label: 'user', path: shellPaths.resolveUserPath(GOODVIBES_TUI_SURFACE_ROOT, 'settings.json') },
|
|
218
|
+
{ label: 'project', path: shellPaths.resolveProjectPath(GOODVIBES_TUI_SURFACE_ROOT, 'settings.json') },
|
|
219
|
+
];
|
|
220
|
+
const values = new Map<string, { readonly value: unknown; readonly source: string }>();
|
|
221
|
+
const parseErrors: string[] = [];
|
|
222
|
+
for (const source of sources) {
|
|
223
|
+
try {
|
|
224
|
+
const record = readJsonRecord(source.path);
|
|
225
|
+
if (!record) continue;
|
|
226
|
+
for (const setting of configManager.getSchema()) {
|
|
227
|
+
if (!canImportTuiSetting(setting.key)) continue;
|
|
228
|
+
const value = readNestedSettingValue(record, setting.key);
|
|
229
|
+
if (value !== undefined) values.set(setting.key, { value, source: source.label });
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
parseErrors.push(`${source.label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (values.size === 0) {
|
|
237
|
+
const detail = parseErrors.length > 0
|
|
238
|
+
? `No importable settings found. ${parseErrors.join('; ')}`
|
|
239
|
+
: 'No GoodVibes TUI settings file with importable Agent-owned settings was found.';
|
|
240
|
+
return {
|
|
241
|
+
status: 'No GoodVibes TUI settings imported.',
|
|
242
|
+
runtimeSnapshot: null,
|
|
243
|
+
result: {
|
|
244
|
+
kind: parseErrors.length > 0 ? 'error' : 'guidance',
|
|
245
|
+
title: 'Nothing imported',
|
|
246
|
+
detail,
|
|
247
|
+
safety: 'safe',
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const imported: string[] = [];
|
|
253
|
+
const unchanged: string[] = [];
|
|
254
|
+
const skipped: string[] = [];
|
|
255
|
+
for (const [key, entry] of values) {
|
|
256
|
+
const setting = agentWorkspaceSettingSchema(context, key);
|
|
257
|
+
if (!setting) continue;
|
|
258
|
+
if (valuesMatch(configManager.get(setting.key as ConfigKey), entry.value)) {
|
|
259
|
+
unchanged.push(setting.key);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
await setHarnessSetting(configManager, context?.platform?.secretsManager, setting.key, entry.value);
|
|
264
|
+
imported.push(`${setting.key} (${entry.source})`);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
skipped.push(`${setting.key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
status: imported.length > 0 ? `Imported ${imported.length} GoodVibes TUI setting(s).` : 'No GoodVibes TUI settings changed.',
|
|
272
|
+
runtimeSnapshot: context ? buildAgentWorkspaceRuntimeSnapshot(context) : null,
|
|
273
|
+
result: {
|
|
274
|
+
kind: skipped.length > 0 && imported.length === 0 ? 'error' : imported.length > 0 ? 'refreshed' : 'guidance',
|
|
275
|
+
title: imported.length > 0 ? 'GoodVibes TUI settings imported' : 'No settings changed',
|
|
276
|
+
detail: [
|
|
277
|
+
imported.length > 0 ? `Imported: ${imported.slice(0, 10).join(', ')}${imported.length > 10 ? `, +${imported.length - 10} more` : ''}.` : '',
|
|
278
|
+
unchanged.length > 0 ? `${unchanged.length} setting(s) already matched.` : '',
|
|
279
|
+
skipped.length > 0 ? `Skipped: ${skipped.slice(0, 5).join('; ')}${skipped.length > 5 ? `; +${skipped.length - 5} more` : ''}.` : '',
|
|
280
|
+
parseErrors.length > 0 ? `Parse issues: ${parseErrors.join('; ')}.` : '',
|
|
281
|
+
].filter((line) => line.length > 0).join(' '),
|
|
282
|
+
safety: 'safe',
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function createSettingEditor(setting: ConfigSetting, currentValue: string, action: AgentWorkspaceAction): AgentWorkspaceLocalEditor {
|
|
288
|
+
const valueHint = setting.type === 'number'
|
|
289
|
+
? 'Enter a number.'
|
|
290
|
+
: setting.type === 'string'
|
|
291
|
+
? 'Enter a value. Leave empty to clear it.'
|
|
292
|
+
: setting.enumValues
|
|
293
|
+
? `Allowed values: ${setting.enumValues.join(', ')}.`
|
|
294
|
+
: action.detail;
|
|
295
|
+
return {
|
|
296
|
+
kind: 'setting-set',
|
|
297
|
+
mode: 'update',
|
|
298
|
+
recordId: setting.key,
|
|
299
|
+
title: `Set ${setting.key}`,
|
|
300
|
+
selectedFieldIndex: 0,
|
|
301
|
+
message: action.detail,
|
|
302
|
+
fields: [
|
|
303
|
+
{
|
|
304
|
+
id: 'value',
|
|
305
|
+
label: setting.key,
|
|
306
|
+
value: currentValue,
|
|
307
|
+
required: false,
|
|
308
|
+
multiline: false,
|
|
309
|
+
hint: action.settingValueHint ?? valueHint,
|
|
310
|
+
redact: /(?:secret|token|password|api[-_.]?key|signing)/i.test(setting.key),
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
@@ -14,6 +14,9 @@ export interface AgentWorkspaceSetupChecklistInput {
|
|
|
14
14
|
readonly provider: string;
|
|
15
15
|
readonly model: string;
|
|
16
16
|
readonly runtimeBaseUrl: string;
|
|
17
|
+
readonly activeSubscriptionCount: number;
|
|
18
|
+
readonly pendingSubscriptionCount: number;
|
|
19
|
+
readonly availableSubscriptionProviderCount: number;
|
|
17
20
|
readonly sessionMemoryCount: number;
|
|
18
21
|
readonly localMemoryCount: number;
|
|
19
22
|
readonly localMemoryReviewQueueCount: number;
|
|
@@ -69,6 +72,23 @@ export function buildAgentWorkspaceSetupChecklist(input: AgentWorkspaceSetupChec
|
|
|
69
72
|
: 'Choose a provider and model before relying on assistant turns.',
|
|
70
73
|
command: 'Setup -> Provider and model',
|
|
71
74
|
},
|
|
75
|
+
{
|
|
76
|
+
id: 'subscriptions',
|
|
77
|
+
label: 'Provider subscriptions',
|
|
78
|
+
status: input.activeSubscriptionCount > 0
|
|
79
|
+
? 'ready'
|
|
80
|
+
: input.pendingSubscriptionCount > 0
|
|
81
|
+
? 'recommended'
|
|
82
|
+
: input.availableSubscriptionProviderCount > 0 ? 'recommended' : 'optional',
|
|
83
|
+
detail: input.activeSubscriptionCount > 0
|
|
84
|
+
? `${input.activeSubscriptionCount} provider subscription session(s) are active.`
|
|
85
|
+
: input.pendingSubscriptionCount > 0
|
|
86
|
+
? `${input.pendingSubscriptionCount} provider subscription login(s) are pending completion.`
|
|
87
|
+
: input.availableSubscriptionProviderCount > 0
|
|
88
|
+
? `${input.availableSubscriptionProviderCount} subscription-capable provider(s) are available. Start login if you want subscription routing.`
|
|
89
|
+
: 'No subscription-capable providers are available yet. Use API keys or add an OAuth provider service.',
|
|
90
|
+
command: 'Start -> Start subscription login',
|
|
91
|
+
},
|
|
72
92
|
{
|
|
73
93
|
id: 'agent-knowledge',
|
|
74
94
|
label: 'Agent Knowledge',
|