@pellux/goodvibes-agent 1.4.4 → 1.5.1
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 +22 -0
- package/README.md +52 -7
- package/dist/package/main.js +7491 -12253
- package/docs/README.md +2 -2
- package/docs/channels-remote-and-api.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/release-and-publishing.md +1 -1
- package/docs/tools-and-commands.md +5 -5
- package/package.json +4 -2
- package/release/live-verification/live-verification.json +13 -13
- package/release/live-verification/live-verification.md +15 -15
- package/release/performance-snapshot.json +2 -2
- package/release/release-notes.md +5 -7
- package/release/release-readiness.json +6 -6
- package/src/agent/behavior-discovery-summary.ts +4 -18
- package/src/agent/calendar-registry.ts +322 -0
- package/src/agent/competitive-feature-inventory.ts +268 -130
- package/src/agent/document-registry.ts +5 -1
- package/src/agent/email/email-service.ts +350 -0
- package/src/agent/email/imap-client.ts +596 -0
- package/src/agent/email/smtp-client.ts +453 -0
- package/src/agent/ics-calendar.ts +662 -0
- package/src/agent/ics-timezone.ts +172 -0
- package/src/agent/markdown-frontmatter.ts +31 -0
- package/src/agent/memory-safety.ts +1 -1
- package/src/agent/note-registry.ts +3 -9
- package/src/agent/persona-discovery.ts +3 -15
- package/src/agent/persona-registry.ts +1 -10
- package/src/agent/research-source-registry.ts +5 -1
- package/src/agent/routine-discovery.ts +3 -15
- package/src/agent/runtime-profile.ts +3 -0
- package/src/agent/skill-discovery.ts +3 -15
- package/src/agent/skill-draft-proposer.ts +203 -0
- package/src/agent/skill-draft-runner.ts +201 -0
- package/src/agent/skill-registry.ts +36 -1
- package/src/agent/skill-standard.ts +99 -0
- package/src/agent/vibe-file.ts +7 -22
- package/src/cli/completion.ts +1 -1
- package/src/cli/config-overrides.ts +10 -1
- package/src/cli/redaction.ts +17 -5
- package/src/config/provider-model.ts +2 -1
- package/src/config/secret-config.ts +13 -6
- package/src/core/activity-feed.ts +97 -0
- package/src/core/away-digest.ts +161 -0
- package/src/core/conversation-rendering.ts +7 -3
- package/src/core/conversation.ts +22 -15
- package/src/core/hardware-profile.ts +362 -0
- package/src/core/last-seen-store.ts +78 -0
- package/src/core/plain-language.ts +52 -0
- package/src/core/setup-incomplete-hint.ts +90 -0
- package/src/core/system-message-router.ts +38 -87
- package/src/input/agent-workspace-basic-command-editor-submission.ts +60 -220
- package/src/input/agent-workspace-basic-command-editors.ts +39 -141
- package/src/input/agent-workspace-categories.ts +40 -125
- package/src/input/agent-workspace-command-editor.ts +4 -0
- package/src/input/agent-workspace-host-category.ts +0 -5
- package/src/input/agent-workspace-local-editor-submission.ts +3 -1
- package/src/input/agent-workspace-navigation.ts +10 -3
- package/src/input/agent-workspace-onboarding-actions.ts +132 -0
- package/src/input/agent-workspace-onboarding-categories.ts +29 -26
- package/src/input/agent-workspace-onboarding-finish.ts +11 -4
- package/src/input/agent-workspace-onboarding-state.ts +111 -0
- package/src/input/agent-workspace-profile-editor-submission.ts +260 -0
- package/src/input/agent-workspace-profile-editors.ts +155 -0
- package/src/input/agent-workspace-subscription-editor.ts +7 -0
- package/src/input/agent-workspace-types.ts +5 -1
- package/src/input/agent-workspace.ts +65 -39
- package/src/input/command-registry.ts +18 -5
- package/src/input/commands/agent-runtime-profile-runtime.ts +2 -1
- package/src/input/commands/agent-skills-runtime.ts +60 -3
- package/src/input/commands/calendar-runtime.ts +209 -0
- package/src/input/commands/channels-runtime.ts +1 -0
- package/src/input/commands/command-error.ts +9 -0
- package/src/input/commands/compat-runtime.ts +1 -0
- package/src/input/commands/config.ts +1 -0
- package/src/input/commands/conversation-runtime.ts +1 -1
- package/src/input/commands/delegation-runtime.ts +5 -6
- package/src/input/commands/email-runtime.ts +387 -0
- package/src/input/commands/experience-runtime.ts +17 -4
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +6 -1
- package/src/input/commands/knowledge-format.ts +73 -0
- package/src/input/commands/knowledge.ts +9 -74
- package/src/input/commands/local-provider-runtime.ts +2 -1
- package/src/input/commands/local-runtime.ts +10 -4
- package/src/input/commands/mcp-runtime.ts +7 -0
- package/src/input/commands/notify-runtime.ts +17 -1
- package/src/input/commands/onboarding-runtime.ts +1 -0
- package/src/input/commands/operator-actions-runtime.ts +1 -0
- package/src/input/commands/operator-runtime.ts +3 -0
- package/src/input/commands/personas-runtime.ts +1 -0
- package/src/input/commands/platform-access-runtime.ts +40 -17
- package/src/input/commands/product-runtime.ts +6 -1
- package/src/input/commands/provider-accounts-runtime.ts +3 -2
- package/src/input/commands/qrcode-runtime.ts +1 -0
- package/src/input/commands/routines-runtime.ts +1 -0
- package/src/input/commands/runtime-services.ts +0 -13
- package/src/input/commands/security-runtime.ts +1 -0
- package/src/input/commands/session-content.ts +1 -0
- package/src/input/commands/shell-core.ts +5 -2
- package/src/input/commands/subscription-runtime.ts +33 -9
- package/src/input/commands/support-bundle-runtime.ts +8 -1
- package/src/input/commands/tasks-runtime.ts +1 -0
- package/src/input/commands/tts-runtime.ts +1 -0
- package/src/input/commands/vibe-runtime.ts +1 -0
- package/src/input/commands.ts +4 -0
- package/src/input/feed-context-factory.ts +3 -12
- package/src/input/handler-command-route.ts +7 -60
- package/src/input/handler-feed-routes.ts +0 -194
- package/src/input/handler-feed.ts +2 -54
- package/src/input/handler-interactions.ts +0 -2
- package/src/input/handler-modal-stack.ts +0 -9
- package/src/input/handler-picker-routes.ts +24 -1
- package/src/input/handler-shortcuts.ts +11 -40
- package/src/input/handler-ui-state.ts +13 -0
- package/src/input/handler.ts +8 -35
- package/src/input/keybindings.ts +40 -17
- package/src/input/model-picker-local-fit.ts +130 -0
- package/src/input/shell-passthrough.ts +58 -0
- package/src/input/submission-router.ts +0 -5
- package/src/main.ts +129 -90
- package/src/renderer/activity-sidebar.ts +186 -0
- package/src/renderer/agent-workspace-context-lines.ts +5 -48
- package/src/renderer/agent-workspace-style.ts +1 -1
- package/src/renderer/agent-workspace.ts +14 -2
- package/src/renderer/compositor.ts +37 -125
- package/src/renderer/conversation-overlays.ts +3 -3
- package/src/renderer/help-overlay.ts +7 -5
- package/src/renderer/model-workspace.ts +101 -86
- package/src/{panels → renderer}/polish.ts +3 -3
- package/src/renderer/shell-surface.ts +4 -5
- package/src/renderer/status-token.ts +31 -11
- package/src/renderer/tab-strip.ts +1 -1
- package/src/renderer/tool-call.ts +7 -8
- package/src/renderer/tool-labels.ts +92 -0
- package/src/renderer/ui-factory.ts +48 -96
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +6 -15
- package/src/runtime/bootstrap-core.ts +34 -13
- package/src/runtime/bootstrap-hook-bridge.ts +3 -1
- package/src/runtime/bootstrap-shell.ts +3 -50
- package/src/runtime/bootstrap.ts +3 -4
- package/src/runtime/context.ts +1 -1
- package/src/runtime/diagnostics/panels/index.ts +0 -1
- package/src/runtime/diagnostics/panels/policy.ts +1 -1
- package/src/runtime/execution-ledger.ts +16 -1
- package/src/runtime/index.ts +6 -1
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/onboarding-state.ts +200 -0
- package/src/{panels → runtime}/provider-account-snapshot.ts +5 -2
- package/src/runtime/services.ts +16 -4
- package/src/runtime/terminal-output-guard.ts +7 -0
- package/src/runtime/tool-permission-safety.ts +1 -1
- package/src/runtime/ui/model-picker/data-provider.ts +1 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +2 -2
- package/src/runtime/ui/model-picker/types.ts +2 -2
- package/src/runtime/ui/provider-health/data-provider.ts +3 -3
- package/src/runtime/ui/provider-health/types.ts +2 -2
- package/src/runtime/ui-services.ts +0 -2
- package/src/shell/agent-workspace-fullscreen.ts +0 -1
- package/src/shell/autonomy-surfacing.ts +272 -0
- package/src/shell/session-continuity-hints.ts +0 -6
- package/src/shell/startup-wiring.ts +221 -0
- package/src/shell/ui-openers.ts +18 -29
- package/src/tools/agent-context-policy.ts +1 -1
- package/src/tools/agent-harness-background-processes.ts +4 -4
- package/src/tools/agent-harness-browser-cockpit-route.ts +3 -3
- package/src/tools/agent-harness-command-runner.ts +6 -1
- package/src/tools/agent-harness-document-ops.ts +26 -53
- package/src/tools/agent-harness-execution-history.ts +1 -13
- package/src/tools/agent-harness-execution-posture.ts +0 -15
- package/src/tools/agent-harness-keybinding-metadata.ts +28 -29
- package/src/tools/agent-harness-local-model-cookbook.ts +24 -1
- package/src/tools/agent-harness-metadata.ts +16 -0
- package/src/tools/agent-harness-mode-catalog.ts +2 -9
- package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
- package/src/tools/agent-harness-setup-posture-utils.ts +1 -1
- package/src/tools/agent-harness-tool-schema.ts +13 -14
- package/src/tools/agent-harness-tool.ts +27 -23
- package/src/tools/agent-harness-ui-surface-metadata.ts +10 -20
- package/src/tools/agent-harness-workspace-action-runner.ts +3 -4
- package/src/tools/agent-workspace-tool.ts +2 -33
- package/src/utils/terminal-width.ts +9 -3
- package/src/version.ts +1 -1
- package/src/input/agent-workspace-panel-route.ts +0 -44
- package/src/input/panel-integration-actions.ts +0 -26
- package/src/panels/approval-panel.ts +0 -149
- package/src/panels/automation-control-panel.ts +0 -212
- package/src/panels/base-panel.ts +0 -254
- package/src/panels/builtin/agent.ts +0 -58
- package/src/panels/builtin/knowledge.ts +0 -26
- package/src/panels/builtin/operations.ts +0 -121
- package/src/panels/builtin/session.ts +0 -138
- package/src/panels/builtin/shared.ts +0 -275
- package/src/panels/builtin/usage.ts +0 -21
- package/src/panels/builtin-panels.ts +0 -23
- package/src/panels/confirm-state.ts +0 -61
- package/src/panels/context-visualizer-panel.ts +0 -204
- package/src/panels/cost-tracker-panel.ts +0 -444
- package/src/panels/docs-panel.ts +0 -285
- package/src/panels/index.ts +0 -25
- package/src/panels/knowledge-panel.ts +0 -417
- package/src/panels/memory-panel.ts +0 -226
- package/src/panels/panel-list-panel.ts +0 -464
- package/src/panels/panel-manager.ts +0 -570
- package/src/panels/provider-accounts-panel.ts +0 -233
- package/src/panels/provider-health-domains.ts +0 -208
- package/src/panels/provider-health-panel.ts +0 -720
- package/src/panels/provider-health-tracker.ts +0 -115
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/qr-panel.ts +0 -207
- package/src/panels/schedule-panel.ts +0 -321
- package/src/panels/scrollable-list-panel.ts +0 -491
- package/src/panels/search-focus.ts +0 -32
- package/src/panels/security-panel.ts +0 -295
- package/src/panels/session-browser-panel.ts +0 -395
- package/src/panels/session-maintenance.ts +0 -125
- package/src/panels/subscription-panel.ts +0 -263
- package/src/panels/system-messages-panel.ts +0 -230
- package/src/panels/tasks-panel.ts +0 -344
- package/src/panels/thinking-panel.ts +0 -304
- package/src/panels/token-budget-panel.ts +0 -475
- package/src/panels/tool-inspector-panel.ts +0 -436
- package/src/panels/types.ts +0 -54
- package/src/renderer/panel-composite.ts +0 -158
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/renderer/panel-workspace-bar.ts +0 -42
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/tools/agent-harness-panel-metadata.ts +0 -211
- /package/src/runtime/perf/{panel-health-monitor.ts → component-health.ts} +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient autonomy surfacing module.
|
|
3
|
+
*
|
|
4
|
+
* HISTORY: this factory was extracted from main.ts to keep that file under the
|
|
5
|
+
* 800-line cap. After extraction it gained two capabilities:
|
|
6
|
+
* 1. Calendar merging — listCalendarEvents callback merges upcoming events
|
|
7
|
+
* into the Coming-up sidebar alongside scheduled jobs (buildCalendarEventsLister).
|
|
8
|
+
* 2. Skill-draft accrual — onAwayDigest callback runs skill-draft proposal
|
|
9
|
+
* once per away-digest pass and appends a feed line when drafts are created
|
|
10
|
+
* (buildSkillDraftProposer).
|
|
11
|
+
* These additions are NOT "no behavior change" refactors; they add real surface.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { buildAwayDigest, formatDigestTime, formatRelativeTime } from '../core/away-digest.ts';
|
|
15
|
+
import { LastSeenStore } from '../core/last-seen-store.ts';
|
|
16
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
17
|
+
import { AgentCalendarRegistry } from '../agent/calendar-registry.ts';
|
|
18
|
+
import { AgentSkillRegistry } from '../agent/skill-registry.ts';
|
|
19
|
+
import { runSkillDraftProposer } from '../agent/skill-draft-runner.ts';
|
|
20
|
+
import type { CommandContext } from '../input/command-registry.ts';
|
|
21
|
+
|
|
22
|
+
interface AutomationJobLike {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly enabled: boolean;
|
|
25
|
+
readonly nextRunAt?: number;
|
|
26
|
+
readonly lastRunAt?: number;
|
|
27
|
+
readonly runCount?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ApprovalLike {
|
|
31
|
+
readonly status: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AutonomyMessageRouter {
|
|
35
|
+
high(message: string): void;
|
|
36
|
+
getFeed(): { push(text: string, priority?: 'high' | 'low', kind?: string): void } | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AutonomySurfacingOptions {
|
|
40
|
+
readonly shellPaths: Parameters<typeof LastSeenStore.fromShellPaths>[0];
|
|
41
|
+
readonly listAutomationJobs: () => readonly AutomationJobLike[];
|
|
42
|
+
readonly listApprovals: () => readonly ApprovalLike[];
|
|
43
|
+
readonly getTasksSnapshot: () => readonly unknown[];
|
|
44
|
+
readonly router: AutonomyMessageRouter;
|
|
45
|
+
readonly render: () => void;
|
|
46
|
+
/**
|
|
47
|
+
* Optional callback to list upcoming local calendar events.
|
|
48
|
+
* Each item carries a formatted label and a numeric sort key (ms since epoch)
|
|
49
|
+
* so the Coming up section merges calendar and job entries chronologically.
|
|
50
|
+
*/
|
|
51
|
+
readonly listCalendarEvents?: () => readonly { label: string; start: number }[];
|
|
52
|
+
/**
|
|
53
|
+
* Optional callback invoked once per announceAwayDigest pass.
|
|
54
|
+
* Should run skill draft proposal and return the count of drafts created.
|
|
55
|
+
* When > 0, a line is appended to the digest feed.
|
|
56
|
+
*/
|
|
57
|
+
readonly onAwayDigest?: () => number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const COMING_UP_TTL_MS = 60_000;
|
|
61
|
+
const FETCH_TIMEOUT_MS = 2500;
|
|
62
|
+
const LAST_SEEN_REFRESH_MS = 5 * 60_000;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Ambient autonomy surfacing for the shell: the launch "While you were away"
|
|
66
|
+
* digest and the sidebar's Coming up entries. Everything here is best-effort
|
|
67
|
+
* and offline-tolerant — failures are silent and renders are never blocked.
|
|
68
|
+
*/
|
|
69
|
+
export function createAutonomySurfacing(options: AutonomySurfacingOptions) {
|
|
70
|
+
const lastSeenStore = LastSeenStore.fromShellPaths(options.shellPaths);
|
|
71
|
+
|
|
72
|
+
// Refresh lastSeenAt every 5 minutes so rapid restarts still detect activity.
|
|
73
|
+
const lastSeenRefreshInterval = setInterval(() => {
|
|
74
|
+
lastSeenStore.save();
|
|
75
|
+
}, LAST_SEEN_REFRESH_MS);
|
|
76
|
+
lastSeenRefreshInterval.unref();
|
|
77
|
+
|
|
78
|
+
const comingUpCache: { items: string[]; fetchedAt: number | null; fetching: boolean } = {
|
|
79
|
+
items: [],
|
|
80
|
+
fetchedAt: null,
|
|
81
|
+
fetching: false,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function refreshComingUp(): void {
|
|
85
|
+
if (comingUpCache.fetching) return;
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
if (comingUpCache.fetchedAt !== null && now - comingUpCache.fetchedAt < COMING_UP_TTL_MS) return;
|
|
88
|
+
comingUpCache.fetching = true;
|
|
89
|
+
Promise.resolve().then(() => {
|
|
90
|
+
try {
|
|
91
|
+
const jobs = options.listAutomationJobs();
|
|
92
|
+
const jobItems: Array<{ label: string; sortKey: number }> = jobs
|
|
93
|
+
.filter((job) => job.enabled && job.nextRunAt !== undefined && job.nextRunAt > now)
|
|
94
|
+
.sort((a, b) => (a.nextRunAt ?? 0) - (b.nextRunAt ?? 0))
|
|
95
|
+
.map((job) => {
|
|
96
|
+
const when = job.nextRunAt ? formatRelativeTime(job.nextRunAt, now) : '';
|
|
97
|
+
const name = job.name.length > 22 ? `${job.name.slice(0, 20)}…` : job.name;
|
|
98
|
+
return { label: when ? `${name} — ${when}` : name, sortKey: job.nextRunAt ?? 0 };
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const calItems: Array<{ label: string; sortKey: number }> = [];
|
|
102
|
+
if (options.listCalendarEvents) {
|
|
103
|
+
try {
|
|
104
|
+
for (const ev of options.listCalendarEvents()) {
|
|
105
|
+
calItems.push({ label: ev.label, sortKey: ev.start });
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// Calendar unavailable — skip silently.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
comingUpCache.items = [...jobItems, ...calItems]
|
|
113
|
+
.sort((a, b) => a.sortKey - b.sortKey)
|
|
114
|
+
.slice(0, 3)
|
|
115
|
+
.map((item) => item.label);
|
|
116
|
+
comingUpCache.fetchedAt = Date.now();
|
|
117
|
+
} catch {
|
|
118
|
+
// Offline or manager unavailable — leave cache as-is.
|
|
119
|
+
} finally {
|
|
120
|
+
comingUpCache.fetching = false;
|
|
121
|
+
}
|
|
122
|
+
}).catch(() => {
|
|
123
|
+
comingUpCache.fetching = false;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function announceAwayDigest(): void {
|
|
128
|
+
void (async () => {
|
|
129
|
+
try {
|
|
130
|
+
const lastSeenAt = lastSeenStore.read();
|
|
131
|
+
const withTimeout = <T>(get: () => T) => Promise.race([
|
|
132
|
+
Promise.resolve(get()),
|
|
133
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), FETCH_TIMEOUT_MS)),
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
const [jobsResult, tasksResult] = await Promise.allSettled([
|
|
137
|
+
withTimeout(options.listAutomationJobs),
|
|
138
|
+
withTimeout(options.getTasksSnapshot),
|
|
139
|
+
]);
|
|
140
|
+
const jobs = jobsResult.status === 'fulfilled' ? jobsResult.value : [];
|
|
141
|
+
const allTasks = tasksResult.status === 'fulfilled' ? tasksResult.value : [];
|
|
142
|
+
|
|
143
|
+
const firedSchedules = lastSeenAt !== null
|
|
144
|
+
? jobs
|
|
145
|
+
.filter((job) => job.lastRunAt !== undefined && job.lastRunAt > lastSeenAt)
|
|
146
|
+
.map((job) => ({ name: job.name, lastRunAt: job.lastRunAt, runCount: job.runCount ?? 0 }))
|
|
147
|
+
: [];
|
|
148
|
+
|
|
149
|
+
const changedTasks = lastSeenAt !== null
|
|
150
|
+
? allTasks
|
|
151
|
+
.filter((task) => {
|
|
152
|
+
const completedAt = (task as { completedAt?: number }).completedAt;
|
|
153
|
+
return completedAt !== undefined ? completedAt > lastSeenAt : false;
|
|
154
|
+
})
|
|
155
|
+
.map((task) => ({
|
|
156
|
+
title: (task as { title?: string; name?: string }).title
|
|
157
|
+
?? (task as { name?: string }).name
|
|
158
|
+
?? 'Task',
|
|
159
|
+
status: (task as { status?: string }).status ?? 'done',
|
|
160
|
+
completedAt: (task as { completedAt?: number }).completedAt,
|
|
161
|
+
}))
|
|
162
|
+
: [];
|
|
163
|
+
|
|
164
|
+
const pendingApprovals = options.listApprovals()
|
|
165
|
+
.filter((approval) => approval.status === 'pending').length;
|
|
166
|
+
|
|
167
|
+
const digest = buildAwayDigest({
|
|
168
|
+
lastSeenAt,
|
|
169
|
+
schedules: firedSchedules,
|
|
170
|
+
tasks: changedTasks,
|
|
171
|
+
pendingApprovals,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (digest !== null) {
|
|
175
|
+
const full = [digest.headline, ...digest.lines].join('\n ');
|
|
176
|
+
options.router.high(`[Status] ${full}`);
|
|
177
|
+
for (const line of digest.lines) {
|
|
178
|
+
options.router.getFeed()?.push(`[Status] ${line}`, 'low', 'schedule');
|
|
179
|
+
}
|
|
180
|
+
options.render();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Invoke optional skill-draft hook once per pass.
|
|
184
|
+
if (options.onAwayDigest) {
|
|
185
|
+
try {
|
|
186
|
+
const drafted = options.onAwayDigest();
|
|
187
|
+
if (drafted > 0) {
|
|
188
|
+
options.router.getFeed()?.push(
|
|
189
|
+
`[Status] I drafted ${drafted} skill${drafted !== 1 ? 's' : ''} from recent work — review them under Memory`,
|
|
190
|
+
'low',
|
|
191
|
+
'schedule',
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// Skill draft hook failed — skip silently.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch (err) {
|
|
199
|
+
logger.debug('away-digest failed (non-fatal)', { error: err instanceof Error ? err.message : String(err) });
|
|
200
|
+
}
|
|
201
|
+
})();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function stop(): void {
|
|
205
|
+
clearInterval(lastSeenRefreshInterval);
|
|
206
|
+
lastSeenStore.save();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
refreshComingUp,
|
|
211
|
+
announceAwayDigest,
|
|
212
|
+
stop,
|
|
213
|
+
// Return a defensive copy so callers cannot mutate internal cache state.
|
|
214
|
+
comingUpItems: (): readonly string[] => [...comingUpCache.items],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Build the onAwayDigest callback for the given shellPaths and command context.
|
|
220
|
+
* Extracted so main.ts can pass a single symbol instead of an inline lambda.
|
|
221
|
+
* (See module-header note for history.)
|
|
222
|
+
*/
|
|
223
|
+
export function buildSkillDraftProposer(
|
|
224
|
+
shellPaths: Parameters<typeof AgentSkillRegistry.fromShellPaths>[0],
|
|
225
|
+
context: CommandContext,
|
|
226
|
+
): () => number {
|
|
227
|
+
return () =>
|
|
228
|
+
runSkillDraftProposer(
|
|
229
|
+
context,
|
|
230
|
+
AgentSkillRegistry.fromShellPaths(shellPaths),
|
|
231
|
+
).proposed;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Build the listCalendarEvents callback for the given shellPaths.
|
|
236
|
+
* Extracts the closure that was previously inlined in main.ts so that
|
|
237
|
+
* file stays under the 800-line cap.
|
|
238
|
+
*
|
|
239
|
+
* The optional `upcomingEvents` parameter is a testability seam: when provided,
|
|
240
|
+
* it replaces the `registry.upcoming(7)` call so unit tests can inject a
|
|
241
|
+
* deterministic event set without mocking the module. Production callers omit
|
|
242
|
+
* this parameter; the default is the real registry lookup.
|
|
243
|
+
*/
|
|
244
|
+
export function buildCalendarEventsLister(
|
|
245
|
+
shellPaths: Parameters<typeof LastSeenStore.fromShellPaths>[0],
|
|
246
|
+
upcomingEvents?: () => readonly import('../agent/calendar-registry.ts').AgentCalendarEvent[],
|
|
247
|
+
): () => readonly { label: string; start: number }[] {
|
|
248
|
+
return () => {
|
|
249
|
+
const now = Date.now();
|
|
250
|
+
const registry = AgentCalendarRegistry.fromShellPaths(shellPaths);
|
|
251
|
+
return (upcomingEvents ?? (() => registry.upcoming(7)))()
|
|
252
|
+
.map((e) => {
|
|
253
|
+
const allDay = e.allDay;
|
|
254
|
+
// Sort key: use local midnight for all-day events so they sort at the
|
|
255
|
+
// correct local calendar date rather than UTC midnight (which would land
|
|
256
|
+
// the previous day for timezones east of UTC).
|
|
257
|
+
// Timed events: parse the ISO string directly; Date.parse handles
|
|
258
|
+
// offset-bearing strings correctly.
|
|
259
|
+
const startMs = allDay
|
|
260
|
+
? new Date(`${e.start}T00:00:00`).getTime() // no tz suffix ⇒ local time
|
|
261
|
+
: Date.parse(e.start);
|
|
262
|
+
const sortMs = isNaN(startMs) ? now : startMs;
|
|
263
|
+
// Display: format via local Date methods so the user sees their own
|
|
264
|
+
// timezone, never a raw stored offset or UTC-shifted string.
|
|
265
|
+
const label = e.title.length > 22 ? `${e.title.slice(0, 20)}…` : e.title;
|
|
266
|
+
const when = allDay
|
|
267
|
+
? e.start // date-only string, no time component to misformat
|
|
268
|
+
: formatDigestTime(startMs, now);
|
|
269
|
+
return { label: `${label} — ${when}`, start: sortMs };
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
}
|
|
@@ -18,15 +18,10 @@ interface ShellRemoteSnapshot {
|
|
|
18
18
|
readonly contracts: readonly ShellRemoteContract[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
interface ShellPanelRecord {
|
|
22
|
-
readonly id: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
21
|
export function buildShellSessionContinuityHints(
|
|
26
22
|
sessionSnapshot: ShellSessionSnapshot,
|
|
27
23
|
tasksSnapshot: ShellTaskSnapshot,
|
|
28
24
|
remoteSnapshot: ShellRemoteSnapshot,
|
|
29
|
-
openPanels: readonly ShellPanelRecord[],
|
|
30
25
|
) {
|
|
31
26
|
return {
|
|
32
27
|
pendingApprovals: sessionSnapshot.pendingApproval ? 1 : 0,
|
|
@@ -34,6 +29,5 @@ export function buildShellSessionContinuityHints(
|
|
|
34
29
|
blockedTasks: tasksSnapshot.tasks.filter((task) => task.status === 'blocked').length,
|
|
35
30
|
remoteContracts: remoteSnapshot.contracts.length,
|
|
36
31
|
remoteRunners: remoteSnapshot.contracts.slice(0, 4).map((contract) => contract.runnerId),
|
|
37
|
-
openPanels: openPanels.map((panel) => panel.id),
|
|
38
32
|
};
|
|
39
33
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
2
|
+
import type { HookDispatcher, HookEventPath, HookPhase, HookCategory } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
3
|
+
import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
4
|
+
import {
|
|
5
|
+
checkRecoveryFile,
|
|
6
|
+
formatReturnContextForDisplay,
|
|
7
|
+
persistConversation,
|
|
8
|
+
writeRecoveryFile,
|
|
9
|
+
} from '@/runtime/index.ts';
|
|
10
|
+
import type { SessionSnapshot } from '@/runtime/index.ts';
|
|
11
|
+
import {
|
|
12
|
+
readOnboardingCheckMarker,
|
|
13
|
+
readOnboardingCompletionMarker,
|
|
14
|
+
} from '../runtime/onboarding/index.ts';
|
|
15
|
+
import { deriveOnboardingState } from '../runtime/onboarding/onboarding-state.ts';
|
|
16
|
+
import { buildSetupIncompleteHint } from '../core/setup-incomplete-hint.ts';
|
|
17
|
+
|
|
18
|
+
export interface SessionPersistenceAndRecoveryDeps {
|
|
19
|
+
readonly buildCurrentSessionSnapshot: () => SessionSnapshot;
|
|
20
|
+
readonly runtime: { readonly sessionId: string; readonly model: string; readonly provider: string };
|
|
21
|
+
readonly conversation: { readonly title?: string | null };
|
|
22
|
+
readonly workingDir: string;
|
|
23
|
+
readonly homeDirectory: string;
|
|
24
|
+
readonly systemMessageRouter: { high(message: string): void; low(message: string): void };
|
|
25
|
+
readonly render: () => void;
|
|
26
|
+
readonly unsubs: Array<() => void>;
|
|
27
|
+
readonly uiServicesTurns: {
|
|
28
|
+
on(event: 'TURN_COMPLETED' | 'STREAM_START' | 'STREAM_DELTA', handler: () => void): () => void;
|
|
29
|
+
};
|
|
30
|
+
readonly hookDispatcher: HookDispatcher;
|
|
31
|
+
readonly sessionManager: SessionManager;
|
|
32
|
+
/**
|
|
33
|
+
* Called whenever the computed stream-token speed changes so that main.ts
|
|
34
|
+
* (which owns the render closure) can keep the value up to date.
|
|
35
|
+
*/
|
|
36
|
+
readonly onStreamSpeedUpdate: (tokensPerSecond: number) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SessionPersistenceAndRecoveryResult {
|
|
40
|
+
/** Interval handle for the periodic recovery-file writer; clear it on exit. */
|
|
41
|
+
recoveryInterval: ReturnType<typeof setInterval>;
|
|
42
|
+
/** True if an unsaved recovery session was found and the user prompt was shown. */
|
|
43
|
+
recoveryPending: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wires streaming-speed event subscriptions, auto-save on turn completion,
|
|
48
|
+
* and the post-first-render recovery-file check + periodic writer.
|
|
49
|
+
*
|
|
50
|
+
* Call AFTER the first render() so that:
|
|
51
|
+
* - announceAwayDigest has already run
|
|
52
|
+
* - startHardwareProbe has already fired
|
|
53
|
+
* - The recovery prompt lands as ambient context, not a startup blocker
|
|
54
|
+
*/
|
|
55
|
+
export function wireSessionPersistenceAndRecovery(
|
|
56
|
+
deps: SessionPersistenceAndRecoveryDeps,
|
|
57
|
+
): SessionPersistenceAndRecoveryResult {
|
|
58
|
+
const {
|
|
59
|
+
buildCurrentSessionSnapshot,
|
|
60
|
+
runtime,
|
|
61
|
+
conversation,
|
|
62
|
+
workingDir,
|
|
63
|
+
homeDirectory,
|
|
64
|
+
systemMessageRouter,
|
|
65
|
+
render,
|
|
66
|
+
unsubs,
|
|
67
|
+
uiServicesTurns,
|
|
68
|
+
hookDispatcher,
|
|
69
|
+
sessionManager,
|
|
70
|
+
onStreamSpeedUpdate,
|
|
71
|
+
} = deps;
|
|
72
|
+
|
|
73
|
+
// --- Streaming speed + tool preview wiring ---
|
|
74
|
+
let streamStartTime = 0;
|
|
75
|
+
let streamDeltaCount = 0;
|
|
76
|
+
|
|
77
|
+
unsubs.push(uiServicesTurns.on('TURN_COMPLETED', () => {
|
|
78
|
+
// Auto-save after every LLM turn so kills don't lose the session
|
|
79
|
+
try {
|
|
80
|
+
const snapshot = buildCurrentSessionSnapshot();
|
|
81
|
+
persistConversation(
|
|
82
|
+
runtime.sessionId,
|
|
83
|
+
snapshot,
|
|
84
|
+
runtime.model,
|
|
85
|
+
runtime.provider,
|
|
86
|
+
conversation.title || '',
|
|
87
|
+
{ workingDirectory: workingDir, homeDirectory, sessionManager },
|
|
88
|
+
);
|
|
89
|
+
hookDispatcher.fire({ path: 'Lifecycle:session:save' as HookEventPath, phase: 'Lifecycle' as HookPhase, category: 'session' as HookCategory, specific: 'save', sessionId: runtime.sessionId, timestamp: Date.now(), payload: { sessionId: runtime.sessionId } }).catch((err: unknown) => logger.debug('hook fire error', { error: summarizeError(err) }));
|
|
90
|
+
} catch (e) { logger.debug('auto-save on turn:complete failed', { error: summarizeError(e) }); }
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
unsubs.push(uiServicesTurns.on('STREAM_START', () => {
|
|
94
|
+
streamStartTime = Date.now();
|
|
95
|
+
streamDeltaCount = 0;
|
|
96
|
+
onStreamSpeedUpdate(0);
|
|
97
|
+
}));
|
|
98
|
+
unsubs.push(uiServicesTurns.on('STREAM_DELTA', () => {
|
|
99
|
+
streamDeltaCount++;
|
|
100
|
+
const elapsed = (Date.now() - streamStartTime) / 1000;
|
|
101
|
+
// Note: counts stream deltas, not actual tokens. ~1 delta per token for most providers.
|
|
102
|
+
onStreamSpeedUpdate(elapsed > 0 ? streamDeltaCount / elapsed : 0);
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
// Recovery file check: display prompt if an unsaved session exists.
|
|
106
|
+
// Runs after the first render so the message lands as ambient context.
|
|
107
|
+
let recoveryPending = false;
|
|
108
|
+
const recoveryInfo = checkRecoveryFile({ workingDirectory: workingDir, homeDirectory });
|
|
109
|
+
if (recoveryInfo) {
|
|
110
|
+
systemMessageRouter.high(`[Recovery] Found unsaved session from ${new Date(recoveryInfo.timestamp).toLocaleString()}. Title: "${recoveryInfo.title}". Press Ctrl+R to restore, Esc to discard, or start typing to ignore it.`);
|
|
111
|
+
for (const line of formatReturnContextForDisplay(recoveryInfo.returnContext)) {
|
|
112
|
+
systemMessageRouter.low(`[Recovery] ${line}`);
|
|
113
|
+
}
|
|
114
|
+
render();
|
|
115
|
+
recoveryPending = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const recoveryInterval = setInterval(() => {
|
|
119
|
+
const snapshot = buildCurrentSessionSnapshot();
|
|
120
|
+
writeRecoveryFile(
|
|
121
|
+
snapshot,
|
|
122
|
+
runtime.sessionId,
|
|
123
|
+
conversation.title ?? '',
|
|
124
|
+
{ workingDirectory: workingDir, homeDirectory },
|
|
125
|
+
);
|
|
126
|
+
}, 60_000);
|
|
127
|
+
|
|
128
|
+
return { recoveryInterval, recoveryPending };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Deps for the setup-incomplete hint, kept narrow so the function is easily
|
|
133
|
+
* testable and doesn't pull in the full bootstrap context.
|
|
134
|
+
*/
|
|
135
|
+
export interface SetupIncompleteHintDeps {
|
|
136
|
+
/**
|
|
137
|
+
* Shell path service — used to locate onboarding marker files.
|
|
138
|
+
* Must satisfy the minimal interface required by readOnboardingCheckMarker.
|
|
139
|
+
*/
|
|
140
|
+
readonly shellPaths: Parameters<typeof readOnboardingCheckMarker>[0];
|
|
141
|
+
/**
|
|
142
|
+
* Whether a model route is currently configured and usable for chat.
|
|
143
|
+
* Pass true when the provider registry has a ready model, false otherwise.
|
|
144
|
+
*/
|
|
145
|
+
readonly providerReady: boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Whether a local model route has been detected and is ready for chat.
|
|
148
|
+
* Pass true when the local-model-cookbook status === 'detected-local-route'.
|
|
149
|
+
* Omit or pass false when unavailable. Best-effort: errors should produce false.
|
|
150
|
+
* This mirrors the 'local-model-readiness' plan item in the real buildSetupPlan.
|
|
151
|
+
*/
|
|
152
|
+
readonly localReady?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Whether the assistant service (connected host) is known to be running.
|
|
155
|
+
* Pass true/false for a confirmed live signal; omit or pass null/undefined
|
|
156
|
+
* when the signal is unreliable or unavailable at startup time.
|
|
157
|
+
*/
|
|
158
|
+
readonly hostReady?: boolean | null;
|
|
159
|
+
/** Low-priority message channel — same interface as SystemMessageRouter.low(). */
|
|
160
|
+
readonly systemMessageRouter: { low(message: string): void };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Derives the current onboarding state from disk markers and, if setup is
|
|
165
|
+
* in-progress, pushes a plain-language hint to the conversation feed.
|
|
166
|
+
*
|
|
167
|
+
* Best-effort: any error is caught silently so startup is never blocked.
|
|
168
|
+
* Call after the first render, alongside announceAwayDigest.
|
|
169
|
+
*/
|
|
170
|
+
export function wireSetupIncompleteHint(deps: SetupIncompleteHintDeps): void {
|
|
171
|
+
try {
|
|
172
|
+
const { shellPaths, providerReady, localReady, hostReady, systemMessageRouter } = deps;
|
|
173
|
+
|
|
174
|
+
const checkMarker = readOnboardingCheckMarker(shellPaths, 'user');
|
|
175
|
+
const completionMarker = readOnboardingCompletionMarker(shellPaths, 'user');
|
|
176
|
+
|
|
177
|
+
// Build a minimal plan with BOTH provider-access AND local-model-readiness so
|
|
178
|
+
// that deriveReadyToChat mirrors the workspace's canonical definition:
|
|
179
|
+
// readyToChat = providerItem.ready OR localModelItem.ready
|
|
180
|
+
//
|
|
181
|
+
// local-model-readiness uses blocksAutonomy:false so it never becomes a false
|
|
182
|
+
// blocker when only the local route is available (matching real plan behaviour).
|
|
183
|
+
// We intentionally skip the full buildSetupPlan (requires CommandContext +
|
|
184
|
+
// async collectSnapshot) because this is a best-effort ambient hint.
|
|
185
|
+
const minimalPlan = [
|
|
186
|
+
{
|
|
187
|
+
id: 'provider-access',
|
|
188
|
+
label: 'Model access',
|
|
189
|
+
status: providerReady ? ('ready' as const) : ('blocked' as const),
|
|
190
|
+
priority: 20,
|
|
191
|
+
blocksAutonomy: true,
|
|
192
|
+
reason: '',
|
|
193
|
+
nextAction: 'Choose a model to start chatting.',
|
|
194
|
+
userRoute: 'Agent Workspace -> Start -> Choose main model',
|
|
195
|
+
modelRoute: '',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'local-model-readiness',
|
|
199
|
+
label: 'Local model',
|
|
200
|
+
status: localReady ? ('ready' as const) : ('recommended' as const),
|
|
201
|
+
priority: 25,
|
|
202
|
+
blocksAutonomy: false,
|
|
203
|
+
reason: '',
|
|
204
|
+
nextAction: 'Run /agent to set up a local model route.',
|
|
205
|
+
userRoute: 'Agent Workspace -> Models -> Local models',
|
|
206
|
+
modelRoute: '',
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
const state = deriveOnboardingState({ plan: minimalPlan, checkMarker, completionMarker });
|
|
211
|
+
|
|
212
|
+
const hint = buildSetupIncompleteHint(state, hostReady);
|
|
213
|
+
if (hint === null) return;
|
|
214
|
+
|
|
215
|
+
for (const line of hint.lines) {
|
|
216
|
+
systemMessageRouter.low(`[Setup] ${line}`);
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// Never block startup on hint errors.
|
|
220
|
+
}
|
|
221
|
+
}
|
package/src/shell/ui-openers.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { getProviderIdFromModel } from '../config/provider-model.ts';
|
|
|
3
3
|
import type { ConversationManager } from '../core/conversation';
|
|
4
4
|
import type { CommandContext } from '../input/command-registry.ts';
|
|
5
5
|
import type { InputHandler } from '../input/handler.ts';
|
|
6
|
-
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
7
6
|
import { EFFORT_DESCRIPTIONS } from '@pellux/goodvibes-sdk/platform/providers';
|
|
8
7
|
import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
9
8
|
import type { MutableRuntimeState } from '@/runtime/index.ts';
|
|
@@ -13,13 +12,12 @@ import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config'
|
|
|
13
12
|
import type { SecretsManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
14
13
|
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
15
14
|
import type { ModelPickerTargetInfo } from '../input/model-picker.ts';
|
|
15
|
+
import { buildLocalFitRecommendations, buildSignInRow, LOCAL_REC_PROVIDER } from '../input/model-picker-local-fit.ts';
|
|
16
16
|
import { syncServiceSettingToPlatform } from './service-settings-sync.ts';
|
|
17
|
-
import { agentWorkspaceCategoryForPanel } from '../input/agent-workspace-panel-route.ts';
|
|
18
17
|
|
|
19
18
|
type WireShellUiOpenersOptions = {
|
|
20
19
|
commandContext: CommandContext;
|
|
21
20
|
input: InputHandler;
|
|
22
|
-
panelManager: PanelManager;
|
|
23
21
|
conversation: ConversationManager;
|
|
24
22
|
configManager: ConfigManager;
|
|
25
23
|
providerRegistry: ProviderRegistry;
|
|
@@ -84,7 +82,6 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
84
82
|
const {
|
|
85
83
|
commandContext,
|
|
86
84
|
input,
|
|
87
|
-
panelManager,
|
|
88
85
|
conversation,
|
|
89
86
|
configManager,
|
|
90
87
|
providerRegistry,
|
|
@@ -190,10 +187,19 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
190
187
|
|
|
191
188
|
commandContext.openModelPicker = () => {
|
|
192
189
|
void (async () => {
|
|
193
|
-
const
|
|
190
|
+
const catalogModels = providerRegistry.getSelectableModels();
|
|
194
191
|
const configuredIds = new Set(getConfiguredProviderIds());
|
|
195
192
|
input.modelPicker.configuredProviders = configuredIds;
|
|
196
|
-
|
|
193
|
+
|
|
194
|
+
// CRITICAL GATE: inject synthetic local recommendations ONLY when zero
|
|
195
|
+
// providers are configured. When any real credential exists these must
|
|
196
|
+
// not be present in the list. The sign-in row is appended last so it
|
|
197
|
+
// is always reachable but does not displace hardware-fit entries.
|
|
198
|
+
const models = configuredIds.size === 0
|
|
199
|
+
? [...buildLocalFitRecommendations(), buildSignInRow(), ...catalogModels]
|
|
200
|
+
: catalogModels;
|
|
201
|
+
|
|
202
|
+
const providerIds = [...new Set(models.map((m) => m.provider).filter((p) => p !== LOCAL_REC_PROVIDER))];
|
|
197
203
|
const secretProviderIds = await resolveSecretProviderIds();
|
|
198
204
|
input.modelPicker.configuredViaMap = buildConfiguredViaMap(providerIds, configuredIds, subscriptionManager, secretProviderIds);
|
|
199
205
|
void getPinned().then((pinned) => {
|
|
@@ -202,7 +208,11 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
202
208
|
void input.modelPicker.loadRecentModels().catch(() => {}); // best-effort: prefetch for UI, failure is non-visible
|
|
203
209
|
input.modalOpened('modelPicker');
|
|
204
210
|
input.modelPicker.setTargetInfos(buildModelPickerTargets());
|
|
205
|
-
|
|
211
|
+
// Pre-select the best-fit local recommendation when no providers are configured.
|
|
212
|
+
const preSelectId = configuredIds.size === 0 && models.length > 0
|
|
213
|
+
? (models[0]?.registryKey ?? models[0]?.id ?? getCurrentModelForPickerTarget())
|
|
214
|
+
: getCurrentModelForPickerTarget();
|
|
215
|
+
input.modelPicker.openAllModels(models, preSelectId);
|
|
206
216
|
render();
|
|
207
217
|
})().catch((error: unknown) => {
|
|
208
218
|
commandContext.print?.(`Model picker failed to open: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -439,36 +449,15 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
439
449
|
render();
|
|
440
450
|
};
|
|
441
451
|
|
|
442
|
-
commandContext.
|
|
443
|
-
panelManager.hide();
|
|
444
|
-
input.panelFocused = false;
|
|
452
|
+
commandContext.openWorkspacePicker = () => {
|
|
445
453
|
conversation.setSplashSuppressed(false);
|
|
446
|
-
conversation.log('Panel picker is handled through Agent Workspace. Use /agent for current operator controls.', { fg: '214' });
|
|
447
454
|
input.openAgentWorkspace(commandContext, 'home');
|
|
448
455
|
conversation.rebuildHistory();
|
|
449
456
|
render();
|
|
450
457
|
};
|
|
451
458
|
|
|
452
|
-
commandContext.focusPanels = () => {
|
|
453
|
-
input.panelFocused = false;
|
|
454
|
-
input.openAgentWorkspace(commandContext, 'home');
|
|
455
|
-
render();
|
|
456
|
-
};
|
|
457
|
-
|
|
458
459
|
commandContext.focusPrompt = () => {
|
|
459
|
-
input.panelFocused = false;
|
|
460
460
|
input.indicatorFocused = false;
|
|
461
461
|
render();
|
|
462
462
|
};
|
|
463
|
-
|
|
464
|
-
commandContext.showPanel = (panelId, pane) => {
|
|
465
|
-
void pane;
|
|
466
|
-
panelManager.hide();
|
|
467
|
-
input.panelFocused = false;
|
|
468
|
-
conversation.setSplashSuppressed(false);
|
|
469
|
-
conversation.log(`Panel route "${panelId}" is handled through Agent Workspace. Opening the matching operator area.`, { fg: '214' });
|
|
470
|
-
input.openAgentWorkspace(commandContext, agentWorkspaceCategoryForPanel(panelId));
|
|
471
|
-
conversation.rebuildHistory();
|
|
472
|
-
render();
|
|
473
|
-
};
|
|
474
463
|
}
|
|
@@ -160,7 +160,7 @@ function buildAgentCapabilitiesContract(registry?: ToolRegistry): Record<string,
|
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
162
|
area: 'Harness operation',
|
|
163
|
-
can: 'Discover and use harness modes, slash commands, workspace actions, settings,
|
|
163
|
+
can: 'Discover and use harness modes, slash commands, workspace actions, settings, UI surfaces, keybindings, and model tools.',
|
|
164
164
|
tools: [optionalTool('agent_harness'), optionalTool('host'), optionalTool('settings'), 'goodvibes_context'],
|
|
165
165
|
inspect: 'agent_harness mode:"modes" query:"capability"',
|
|
166
166
|
},
|
|
@@ -658,7 +658,7 @@ export async function runBackgroundProcessAction(context: CommandContext, args:
|
|
|
658
658
|
const action = readProcessAction(args);
|
|
659
659
|
if (action === 'write') {
|
|
660
660
|
const confirmationError = requireConfirmed(args, 'Background process stdin write');
|
|
661
|
-
if (confirmationError)
|
|
661
|
+
if (confirmationError) return { status: 'needs_confirmation', reason: confirmationError, policy: 'Writing stdin to a background process requires confirm:true and explicitUserRequest.' };
|
|
662
662
|
const writeInput = firstManagerFunction(manager, STDIN_WRITE_METHOD_NAMES);
|
|
663
663
|
if (!writeInput) {
|
|
664
664
|
return {
|
|
@@ -710,7 +710,7 @@ export async function runBackgroundProcessAction(context: CommandContext, args:
|
|
|
710
710
|
}
|
|
711
711
|
if (action === 'start' || action === 'spawn' || action === 'run') {
|
|
712
712
|
const confirmationError = requireConfirmed(args, 'Background process start');
|
|
713
|
-
if (confirmationError)
|
|
713
|
+
if (confirmationError) return { status: 'needs_confirmation', reason: confirmationError, policy: 'Starting a background process requires confirm:true and explicitUserRequest.' };
|
|
714
714
|
const command = readCommand(args);
|
|
715
715
|
if (!command) throw new Error('Background process start requires command.');
|
|
716
716
|
if (looksLikeSudo(command)) {
|
|
@@ -749,7 +749,7 @@ export async function runBackgroundProcessAction(context: CommandContext, args:
|
|
|
749
749
|
|
|
750
750
|
if (action === 'stop' || action === 'kill' || action === 'cancel') {
|
|
751
751
|
const confirmationError = requireConfirmed(args, 'Background process stop');
|
|
752
|
-
if (confirmationError)
|
|
752
|
+
if (confirmationError) return { status: 'needs_confirmation', reason: confirmationError, policy: 'Stopping a background process requires confirm:true and explicitUserRequest.' };
|
|
753
753
|
const processId = readProcessId(args);
|
|
754
754
|
if (!processId) throw new Error('Background process stop requires processId.');
|
|
755
755
|
const stopped = manager.stop(processId);
|
|
@@ -767,7 +767,7 @@ export async function runBackgroundProcessAction(context: CommandContext, args:
|
|
|
767
767
|
|
|
768
768
|
if (action === 'wait') {
|
|
769
769
|
const confirmationError = requireConfirmed(args, 'Background process wait');
|
|
770
|
-
if (confirmationError)
|
|
770
|
+
if (confirmationError) return { status: 'needs_confirmation', reason: confirmationError, policy: 'Waiting on a background process requires confirm:true and explicitUserRequest.' };
|
|
771
771
|
const processId = readProcessId(args);
|
|
772
772
|
if (!processId) throw new Error('Background process wait requires processId.');
|
|
773
773
|
const timeoutMs = clampTimeout(args.timeoutMs ?? readField(args, 'timeoutMs'), DEFAULT_WAIT_TIMEOUT_MS);
|