@pellux/goodvibes-agent 1.1.4 → 1.1.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.
@@ -0,0 +1,214 @@
1
+ import { beginOpenAICodexLogin, exchangeOpenAICodexCode, getSubscriptionProviderConfig } from '@pellux/goodvibes-sdk/platform/config';
2
+ import type { OAuthProviderConfig, ProviderSubscription } from '@pellux/goodvibes-sdk/platform/config';
3
+ import { openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils';
4
+ import type { CommandContext } from './command-registry.ts';
5
+ import { isAffirmative } from './agent-workspace-editors.ts';
6
+ import { buildAgentWorkspaceRuntimeSnapshot } from './agent-workspace-snapshot.ts';
7
+ import type { AgentWorkspaceActionResult, AgentWorkspaceLocalEditor, AgentWorkspaceRuntimeSnapshot } from './agent-workspace-types.ts';
8
+
9
+ export interface AgentWorkspaceSubscriptionEditorHost {
10
+ localEditor: AgentWorkspaceLocalEditor | null;
11
+ runtimeSnapshot: AgentWorkspaceRuntimeSnapshot | null;
12
+ status: string;
13
+ lastActionResult: AgentWorkspaceActionResult | null;
14
+ clampSelection(): void;
15
+ }
16
+
17
+ export type AgentWorkspaceSubscriptionFieldReader = (id: string) => string;
18
+
19
+ export async function submitAgentWorkspaceSubscriptionLoginStartEditor(
20
+ host: AgentWorkspaceSubscriptionEditorHost,
21
+ editor: AgentWorkspaceLocalEditor,
22
+ context: CommandContext | null,
23
+ readField: AgentWorkspaceSubscriptionFieldReader,
24
+ ): Promise<void> {
25
+ if (!isAffirmative(readField('confirm'))) {
26
+ host.localEditor = { ...editor, message: 'Subscription login start not confirmed. Type yes, then press Enter.' };
27
+ host.status = 'Subscription login start not confirmed.';
28
+ return;
29
+ }
30
+ try {
31
+ const provider = readField('provider') || 'openai';
32
+ const openBrowser = isAffirmative(readField('openBrowser'));
33
+ const { manager, services } = subscriptionServices(context);
34
+ const resolved = getSubscriptionProviderConfig(provider, services.get(provider));
35
+ if (!resolved) {
36
+ throw new Error(`OAuth is not configured for ${provider}. Add an OAuth provider service or choose a built-in subscription provider.`);
37
+ }
38
+
39
+ const started = provider === 'openai' && resolved.source === 'builtin'
40
+ ? await beginOpenAICodexLogin()
41
+ : null;
42
+ const authorizationUrl = started
43
+ ? started.authorizationUrl
44
+ : (await manager.beginOAuthLogin(provider, resolveManualLoginConfig(resolved.oauth))).authorizationUrl;
45
+
46
+ if (started) {
47
+ manager.savePending({
48
+ provider,
49
+ state: started.state,
50
+ verifier: started.verifier,
51
+ redirectUri: started.redirectUri,
52
+ createdAt: Date.now(),
53
+ });
54
+ }
55
+
56
+ const browserOpened = openBrowser ? await openExternalUrl(authorizationUrl) : false;
57
+ host.localEditor = null;
58
+ host.runtimeSnapshot = context ? buildAgentWorkspaceRuntimeSnapshot(context) : host.runtimeSnapshot;
59
+ host.status = `Subscription login started for ${provider}.`;
60
+ host.lastActionResult = {
61
+ kind: 'refreshed',
62
+ title: 'Subscription login started',
63
+ detail: [
64
+ `Provider: ${provider}.`,
65
+ `Browser: ${openBrowser ? (browserOpened ? 'opened' : 'open failed') : 'skipped'}.`,
66
+ 'Use Finish subscription login with the callback code or redirect URL.',
67
+ `Authorization URL: ${authorizationUrl}`,
68
+ ].join(' '),
69
+ safety: 'safe',
70
+ };
71
+ host.clampSelection();
72
+ } catch (error) {
73
+ const detail = error instanceof Error ? error.message : String(error);
74
+ host.localEditor = { ...editor, message: detail };
75
+ host.status = detail;
76
+ host.lastActionResult = { kind: 'error', title: 'Subscription login start failed', detail, safety: 'safe' };
77
+ }
78
+ }
79
+
80
+ export async function submitAgentWorkspaceSubscriptionLoginFinishEditor(
81
+ host: AgentWorkspaceSubscriptionEditorHost,
82
+ editor: AgentWorkspaceLocalEditor,
83
+ context: CommandContext | null,
84
+ readField: AgentWorkspaceSubscriptionFieldReader,
85
+ ): Promise<void> {
86
+ if (!isAffirmative(readField('confirm'))) {
87
+ host.localEditor = { ...editor, message: 'Subscription login finish not confirmed. Type yes, then press Enter.' };
88
+ host.status = 'Subscription login finish not confirmed.';
89
+ return;
90
+ }
91
+ try {
92
+ const provider = readField('provider') || 'openai';
93
+ const codeInput = readField('code');
94
+ const code = extractAuthorizationCode(codeInput) ?? codeInput;
95
+ const { manager, services } = subscriptionServices(context);
96
+ const resolved = getSubscriptionProviderConfig(provider, services.get(provider));
97
+ if (!resolved) {
98
+ throw new Error(`OAuth is not configured for ${provider}. Start with a configured or built-in subscription provider.`);
99
+ }
100
+
101
+ const record = provider === 'openai' && resolved.source === 'builtin'
102
+ ? (() => {
103
+ const pending = manager.getPending(provider);
104
+ if (!pending) throw new Error(`No pending OAuth login for ${provider}. Start subscription login first.`);
105
+ return exchangeOpenAICodexCode(code, pending.verifier).then((token) => {
106
+ const now = Date.now();
107
+ return manager.saveSubscription({
108
+ provider,
109
+ accessToken: token.accessToken,
110
+ tokenType: token.tokenType,
111
+ ...(typeof token.refreshToken === 'string' && token.refreshToken.length > 0
112
+ ? { refreshToken: token.refreshToken }
113
+ : {}),
114
+ ...(typeof token.expiresAt === 'number' && Number.isFinite(token.expiresAt)
115
+ ? { expiresAt: token.expiresAt }
116
+ : {}),
117
+ ...(token.scopes ? { scopes: token.scopes } : {}),
118
+ authMode: 'oauth',
119
+ overrideAmbientApiKeys: false,
120
+ createdAt: manager.get(provider)?.createdAt ?? now,
121
+ updatedAt: now,
122
+ });
123
+ });
124
+ })()
125
+ : manager.completeOAuthLogin(provider, resolveManualLoginConfig(resolved.oauth), code);
126
+
127
+ const saved = await record;
128
+ host.localEditor = null;
129
+ host.runtimeSnapshot = context ? buildAgentWorkspaceRuntimeSnapshot(context) : host.runtimeSnapshot;
130
+ host.status = `Subscription session saved for ${provider}.`;
131
+ host.lastActionResult = {
132
+ kind: 'refreshed',
133
+ title: 'Subscription session saved',
134
+ detail: [
135
+ `Provider: ${provider}.`,
136
+ `Token type: ${saved.tokenType}.`,
137
+ `Expires: ${saved.expiresAt ? new Date(saved.expiresAt).toISOString() : 'n/a'}.`,
138
+ describeSubscriptionPrecedence(saved),
139
+ ].join(' '),
140
+ safety: 'safe',
141
+ };
142
+ host.clampSelection();
143
+ } catch (error) {
144
+ const detail = error instanceof Error ? error.message : String(error);
145
+ host.localEditor = { ...editor, message: detail };
146
+ host.status = detail;
147
+ host.lastActionResult = { kind: 'error', title: 'Subscription login finish failed', detail, safety: 'safe' };
148
+ }
149
+ }
150
+
151
+ export function submitAgentWorkspaceSubscriptionLogoutEditor(
152
+ host: AgentWorkspaceSubscriptionEditorHost,
153
+ editor: AgentWorkspaceLocalEditor,
154
+ context: CommandContext | null,
155
+ readField: AgentWorkspaceSubscriptionFieldReader,
156
+ ): void {
157
+ if (!isAffirmative(readField('confirm'))) {
158
+ host.localEditor = { ...editor, message: 'Subscription logout not confirmed. Type yes, then press Enter.' };
159
+ host.status = 'Subscription logout not confirmed.';
160
+ return;
161
+ }
162
+ try {
163
+ const provider = readField('provider') || 'openai';
164
+ const { manager } = subscriptionServices(context);
165
+ const removed = manager.logout(provider);
166
+ host.localEditor = null;
167
+ host.runtimeSnapshot = context ? buildAgentWorkspaceRuntimeSnapshot(context) : host.runtimeSnapshot;
168
+ host.status = removed ? `Logged out of ${provider}.` : `No subscription session existed for ${provider}.`;
169
+ host.lastActionResult = {
170
+ kind: removed ? 'refreshed' : 'guidance',
171
+ title: removed ? 'Subscription session removed' : 'No subscription session found',
172
+ detail: removed
173
+ ? `Removed active or pending subscription state for ${provider}. Ambient API key resolution applies if configured.`
174
+ : `No active or pending subscription state existed for ${provider}.`,
175
+ safety: 'safe',
176
+ };
177
+ host.clampSelection();
178
+ } catch (error) {
179
+ const detail = error instanceof Error ? error.message : String(error);
180
+ host.localEditor = { ...editor, message: detail };
181
+ host.status = detail;
182
+ host.lastActionResult = { kind: 'error', title: 'Subscription logout failed', detail, safety: 'safe' };
183
+ }
184
+ }
185
+
186
+ function subscriptionServices(context: CommandContext | null) {
187
+ const manager = context?.platform.subscriptionManager;
188
+ const services = context?.platform.serviceRegistry;
189
+ if (!manager || !services) throw new Error('Subscription services are unavailable in this runtime.');
190
+ return { manager, services };
191
+ }
192
+
193
+ function extractAuthorizationCode(input: string): string | null {
194
+ const trimmed = input.trim();
195
+ if (!trimmed) return null;
196
+ try {
197
+ const url = new URL(trimmed);
198
+ return url.searchParams.get('code');
199
+ } catch {
200
+ return null;
201
+ }
202
+ }
203
+
204
+ function resolveManualLoginConfig(config: OAuthProviderConfig): OAuthProviderConfig {
205
+ return config.manualRedirectUri
206
+ ? { ...config, redirectUri: config.manualRedirectUri }
207
+ : config;
208
+ }
209
+
210
+ function describeSubscriptionPrecedence(record: Pick<ProviderSubscription, 'overrideAmbientApiKeys'>): string {
211
+ return record.overrideAmbientApiKeys
212
+ ? 'Subscription routing now overrides ambient API keys for this provider.'
213
+ : 'Subscription session is stored for subscription-backed flows; ambient API keys are unchanged.';
214
+ }
@@ -1,3 +1,4 @@
1
+ import type { ModelPickerTarget } from './model-picker.ts';
1
2
  import type { AgentWorkspaceChannelStatus } from './agent-workspace-channels.ts';
2
3
  import type { AgentWorkspaceSetupChecklistItem } from './agent-workspace-setup.ts';
3
4
  import type { AgentWorkspaceVoiceMediaReadiness } from './agent-workspace-voice-media.ts';
@@ -17,7 +18,7 @@ export const AGENT_WORKSPACE_CATEGORY_IDS = [
17
18
  'onboarding-channels',
18
19
  'onboarding-voice-media',
19
20
  'onboarding-context',
20
- 'onboarding-verify',
21
+ 'onboarding-automation',
21
22
  'research',
22
23
  'artifacts',
23
24
  'conversation',
@@ -40,7 +41,7 @@ export const AGENT_WORKSPACE_CATEGORY_IDS = [
40
41
 
41
42
  export type AgentWorkspaceCategoryId = (typeof AGENT_WORKSPACE_CATEGORY_IDS)[number];
42
43
 
43
- export type AgentWorkspaceActionKind = 'command' | 'guidance' | 'workspace' | 'editor' | 'setting' | 'settings-import' | 'local-selection' | 'local-operation' | 'onboarding-complete';
44
+ export type AgentWorkspaceActionKind = 'command' | 'guidance' | 'workspace' | 'editor' | 'setting' | 'settings-import' | 'model-picker' | 'settings-modal' | 'local-selection' | 'local-operation' | 'onboarding-complete';
44
45
 
45
46
  export type AgentWorkspaceLocalEditorKind = 'memory' | 'note' | 'persona' | 'skill' | 'routine' | 'profile';
46
47
 
@@ -258,6 +259,9 @@ export interface AgentWorkspaceAction {
258
259
  readonly editorKind?: AgentWorkspaceEditorKind;
259
260
  readonly settingKey?: string;
260
261
  readonly settingValueHint?: string;
262
+ readonly modelPickerTarget?: ModelPickerTarget;
263
+ readonly modelPickerFlow?: 'providerModel' | 'model';
264
+ readonly settingsTarget?: string;
261
265
  readonly visibleWhenSettingKey?: string;
262
266
  readonly visibleWhenSettingValue?: string | boolean | number;
263
267
  readonly localKind?: AgentWorkspaceLocalEditorKind;
@@ -362,11 +366,49 @@ export interface AgentWorkspaceRuntimeSnapshot {
362
366
  readonly provider: string;
363
367
  readonly model: string;
364
368
  readonly modelDisplayName: string;
369
+ readonly embeddingProvider: string;
370
+ readonly reasoningEffort: string;
371
+ readonly helperEnabled: boolean;
372
+ readonly toolLlmEnabled: boolean;
373
+ readonly providerFailureHints: boolean;
374
+ readonly cacheEnabled: boolean;
375
+ readonly cacheStableTtl: string;
376
+ readonly cacheMonitorHitRate: boolean;
377
+ readonly cacheHitRateWarningThreshold: number;
378
+ readonly hitlMode: string;
379
+ readonly guidanceMode: string;
380
+ readonly saveHistory: boolean;
381
+ readonly autoApprove: boolean;
382
+ readonly autoCompactThreshold: number;
383
+ readonly staleContextWarnings: boolean;
384
+ readonly showThinking: boolean;
385
+ readonly showReasoningSummary: boolean;
386
+ readonly theme: string;
387
+ readonly stream: boolean;
388
+ readonly lineNumbers: string;
389
+ readonly operationalMessages: string;
390
+ readonly systemMessages: string;
391
+ readonly releaseChannel: string;
392
+ readonly permissionMode: string;
393
+ readonly toolAutoHeal: boolean;
394
+ readonly toolsDefaultTokenBudget: number;
395
+ readonly artifactMaxBytes: number;
396
+ readonly rawPromptTelemetry: boolean;
397
+ readonly automationEnabled: boolean;
398
+ readonly automationMaxConcurrentRuns: number;
399
+ readonly automationRunHistoryLimit: number;
400
+ readonly automationDefaultTimeoutMs: number;
401
+ readonly automationCatchUpWindowMinutes: number;
402
+ readonly automationFailureCooldownMs: number;
403
+ readonly automationDeleteAfterRun: boolean;
365
404
  readonly sessionId: string;
366
405
  readonly workingDirectory: string;
367
406
  readonly homeDirectory: string;
368
407
  readonly runtimeBaseUrl: string;
369
408
  readonly runtimeOwnership: 'external';
409
+ readonly activeSubscriptionCount: number;
410
+ readonly pendingSubscriptionCount: number;
411
+ readonly availableSubscriptionProviderCount: number;
370
412
  readonly sessionMemoryCount: number;
371
413
  readonly localMemoryCount: number;
372
414
  readonly localMemoryReviewQueueCount: number;
@@ -6,21 +6,21 @@ import type { CommandContext } from './command-registry.ts';
6
6
  import { AgentNoteRegistry } from '../agent/note-registry.ts';
7
7
  import { AgentPersonaRegistry } from '../agent/persona-registry.ts';
8
8
  import { AgentRoutineRegistry } from '../agent/routine-registry.ts';
9
- import { createAgentRuntimeProfile, type AgentRuntimeProfileInfo } from '../agent/runtime-profile.ts';
9
+ import type { AgentRuntimeProfileInfo } from '../agent/runtime-profile.ts';
10
10
  import { AgentSkillRegistry } from '../agent/skill-registry.ts';
11
11
  import { activateAgentWorkspaceSelection } from './agent-workspace-activation.ts';
12
12
  import { AGENT_WORKSPACE_CATEGORIES } from './agent-workspace-categories.ts';
13
13
  import { buildAgentWorkspaceCommandEditorSubmission, isAgentWorkspaceCommandEditorKind } from './agent-workspace-command-editor.ts';
14
- import { createDeleteEditor, editorCategoryId, isAffirmative, splitList } from './agent-workspace-editors.ts';
15
- import { createAgentWorkspaceLearnedBehavior } from './agent-workspace-learned-behavior.ts';
14
+ import { createDeleteEditor, editorCategoryId } from './agent-workspace-editors.ts';
15
+ import { submitAgentWorkspaceLocalRegistryEditor } from './agent-workspace-local-editor-submission.ts';
16
16
  import { clampAgentWorkspaceLocalLibrarySelection, moveAgentWorkspaceLocalLibraryItemSelection, selectedAgentWorkspaceLocalLibraryItem, type AgentWorkspaceLocalSelectionIndexes } from './agent-workspace-local-selection.ts';
17
17
  import { applyAgentWorkspaceLocalLibraryOperation } from './agent-workspace-local-operations.ts';
18
18
  import { deleteAgentWorkspaceMemoryEditor, submitAgentWorkspaceMemoryEditor } from './agent-workspace-memory-editor.ts';
19
19
  import { jumpAgentWorkspaceSelection, moveAgentWorkspaceSelection, selectAgentWorkspaceCategory } from './agent-workspace-navigation.ts';
20
- import { buildAgentWorkspaceRequirements } from './agent-workspace-requirements.ts';
21
20
  import { appendAgentWorkspaceActionSearchText, backspaceAgentWorkspaceActionSearch, beginAgentWorkspaceActionSearch, clearAgentWorkspaceActionSearch, commitAgentWorkspaceActionSearchSelection, searchAgentWorkspaceActions } from './agent-workspace-search.ts';
22
21
  import { agentWorkspaceSettingSchema, applyAgentWorkspaceSettingValue, buildAgentWorkspaceSettingActionEffect, importAgentWorkspaceTuiSettings, isAgentWorkspaceActionVisible } from './agent-workspace-settings.ts';
23
22
  import { buildAgentWorkspaceRuntimeSnapshot } from './agent-workspace-snapshot.ts';
23
+ import { submitAgentWorkspaceSubscriptionLoginFinishEditor, submitAgentWorkspaceSubscriptionLoginStartEditor, submitAgentWorkspaceSubscriptionLogoutEditor } from './agent-workspace-subscription-editor.ts';
24
24
  import type { AgentWorkspaceAction, AgentWorkspaceActionResult, AgentWorkspaceActionSearchResult, AgentWorkspaceCategory, AgentWorkspaceCommandDispatcher, AgentWorkspaceEditorField, AgentWorkspaceFocusPane, AgentWorkspaceLocalEditor, AgentWorkspaceLocalEditorKind, AgentWorkspaceLocalLibraryItem, AgentWorkspaceLocalOperation, AgentWorkspacePromptDispatcher, AgentWorkspaceRuntimeSnapshot } from './agent-workspace-types.ts';
25
25
  import { writeOnboardingCheckMarker, writeOnboardingCompletionMarker } from '../runtime/onboarding/index.ts';
26
26
 
@@ -333,6 +333,55 @@ export class AgentWorkspace {
333
333
  }
334
334
  }
335
335
 
336
+ openModelPickerAction(action: AgentWorkspaceAction, requestRender?: () => void): void {
337
+ const target = action.modelPickerTarget ?? 'main';
338
+ const opened = action.modelPickerFlow === 'model'
339
+ ? this.context?.openModelPickerWithTarget?.(target)
340
+ : this.context?.openProviderModelPickerWithTarget?.(target);
341
+ if (!opened) {
342
+ this.status = 'Model picker is unavailable.';
343
+ this.lastActionResult = {
344
+ kind: 'error',
345
+ title: 'Model picker unavailable',
346
+ detail: 'This runtime cannot open the model picker from Agent workspace.',
347
+ safety: action.safety,
348
+ };
349
+ requestRender?.();
350
+ return;
351
+ }
352
+ this.status = `Opening ${action.label}.`;
353
+ this.lastActionResult = {
354
+ kind: 'dispatched',
355
+ title: `Opening ${action.label}`,
356
+ detail: 'Opened the shared provider/model picker for this setup target.',
357
+ safety: action.safety,
358
+ };
359
+ requestRender?.();
360
+ }
361
+
362
+ openSettingsModalAction(action: AgentWorkspaceAction, requestRender?: () => void): void {
363
+ if (!this.context?.openSettingsModal) {
364
+ this.status = 'Settings are unavailable.';
365
+ this.lastActionResult = {
366
+ kind: 'error',
367
+ title: 'Settings unavailable',
368
+ detail: 'This runtime cannot open settings from Agent workspace.',
369
+ safety: action.safety,
370
+ };
371
+ requestRender?.();
372
+ return;
373
+ }
374
+ this.context.openSettingsModal(action.settingsTarget);
375
+ this.status = `Opening ${action.label}.`;
376
+ this.lastActionResult = {
377
+ kind: 'dispatched',
378
+ title: `Opening ${action.label}`,
379
+ detail: 'Opened the shared settings surface for this setup area.',
380
+ safety: action.safety,
381
+ };
382
+ requestRender?.();
383
+ }
384
+
336
385
  applySettingAction(action: AgentWorkspaceAction, requestRender?: () => void): void {
337
386
  const effect = buildAgentWorkspaceSettingActionEffect(this.context, action);
338
387
  if (effect.kind === 'result') {
@@ -446,6 +495,19 @@ export class AgentWorkspace {
446
495
  this.status = `${missing.label} is required.`;
447
496
  return;
448
497
  }
498
+ if (editor.kind === 'subscription-login-start') {
499
+ void submitAgentWorkspaceSubscriptionLoginStartEditor(this, editor, this.context, (id) => this.editorField(id)).finally(() => requestRender?.());
500
+ return;
501
+ }
502
+ if (editor.kind === 'subscription-login-finish') {
503
+ void submitAgentWorkspaceSubscriptionLoginFinishEditor(this, editor, this.context, (id) => this.editorField(id)).finally(() => requestRender?.());
504
+ return;
505
+ }
506
+ if (editor.kind === 'subscription-logout') {
507
+ submitAgentWorkspaceSubscriptionLogoutEditor(this, editor, this.context, (id) => this.editorField(id));
508
+ requestRender?.();
509
+ return;
510
+ }
449
511
  if (isAgentWorkspaceCommandEditorKind(editor.kind)) {
450
512
  this.submitCommandEditor(editor);
451
513
  requestRender?.();
@@ -496,135 +558,13 @@ export class AgentWorkspace {
496
558
  return;
497
559
  }
498
560
  try {
499
- if (editor.mode === 'delete') {
500
- this.submitLocalDeleteEditor(shellPaths, editor);
501
- return;
502
- }
503
- if (editor.kind === 'learned-behavior') {
504
- const created = createAgentWorkspaceLearnedBehavior(shellPaths, {
505
- target: this.learnedBehaviorTarget(),
506
- name: this.editorField('name'),
507
- description: this.editorField('description'),
508
- notes: this.editorField('notes'),
509
- tags: splitList(this.editorField('tags')),
510
- triggers: splitList(this.editorField('triggers')),
511
- enable: isAffirmative(this.editorField('enable')),
512
- });
513
- this.finishLocalEditor(created.kind, created.id, created.name, 'Created');
514
- } else if (editor.kind === 'profile') {
515
- const template = this.editorField('template');
516
- const templateId = template && template.toLowerCase() !== 'none' ? template : undefined;
517
- const profile = createAgentRuntimeProfile(shellPaths.homeDirectory, this.editorField('name'), {
518
- ...(templateId ? { templateId } : {}),
519
- });
520
- this.finishProfileEditor(profile);
521
- } else if (editor.kind === 'note') {
522
- const registry = AgentNoteRegistry.fromShellPaths(shellPaths);
523
- if (editor.mode === 'update' && editor.recordId) {
524
- const updated = registry.update(editor.recordId, {
525
- title: this.editorField('title'),
526
- body: this.editorField('body'),
527
- sourceUrl: this.editorField('sourceUrl'),
528
- tags: splitList(this.editorField('tags')),
529
- provenance: 'Workspace',
530
- });
531
- this.finishLocalEditor(editor.kind, updated.id, updated.title, 'Updated');
532
- return;
533
- }
534
- const created = registry.create({
535
- title: this.editorField('title'),
536
- body: this.editorField('body'),
537
- sourceUrl: this.editorField('sourceUrl'),
538
- tags: splitList(this.editorField('tags')),
539
- source: 'user',
540
- provenance: 'Workspace',
541
- });
542
- this.finishLocalEditor(editor.kind, created.id, created.title, 'Created');
543
- } else if (editor.kind === 'persona') {
544
- const registry = AgentPersonaRegistry.fromShellPaths(shellPaths);
545
- if (editor.mode === 'update' && editor.recordId) {
546
- const wasActive = registry.snapshot().activePersonaId === editor.recordId;
547
- const updated = registry.update(editor.recordId, {
548
- name: this.editorField('name'),
549
- description: this.editorField('description'),
550
- body: this.editorField('body'),
551
- tags: splitList(this.editorField('tags')),
552
- triggers: splitList(this.editorField('triggers')),
553
- provenance: 'Workspace',
554
- });
555
- if (isAffirmative(this.editorField('activate'))) registry.setActive(updated.id);
556
- else if (wasActive) registry.clearActive();
557
- this.finishLocalEditor(editor.kind, updated.id, updated.name, 'Updated');
558
- return;
559
- }
560
- const created = registry.create({
561
- name: this.editorField('name'),
562
- description: this.editorField('description'),
563
- body: this.editorField('body'),
564
- tags: splitList(this.editorField('tags')),
565
- triggers: splitList(this.editorField('triggers')),
566
- source: 'user',
567
- provenance: 'Workspace',
568
- });
569
- if (isAffirmative(this.editorField('activate'))) registry.setActive(created.id);
570
- this.finishLocalEditor(editor.kind, created.id, created.name, 'Created');
571
- } else if (editor.kind === 'skill') {
572
- const registry = AgentSkillRegistry.fromShellPaths(shellPaths);
573
- if (editor.mode === 'update' && editor.recordId) {
574
- const updated = registry.update(editor.recordId, {
575
- name: this.editorField('name'),
576
- description: this.editorField('description'),
577
- procedure: this.editorField('procedure'),
578
- triggers: splitList(this.editorField('triggers')),
579
- tags: splitList(this.editorField('tags')),
580
- requirements: buildAgentWorkspaceRequirements((id) => this.editorField(id)),
581
- provenance: 'Workspace',
582
- });
583
- registry.setEnabled(updated.id, isAffirmative(this.editorField('enabled')));
584
- this.finishLocalEditor(editor.kind, updated.id, updated.name, 'Updated');
585
- return;
586
- }
587
- const created = registry.create({
588
- name: this.editorField('name'),
589
- description: this.editorField('description'),
590
- procedure: this.editorField('procedure'),
591
- triggers: splitList(this.editorField('triggers')),
592
- tags: splitList(this.editorField('tags')),
593
- requirements: buildAgentWorkspaceRequirements((id) => this.editorField(id)),
594
- enabled: isAffirmative(this.editorField('enabled')),
595
- source: 'user',
596
- provenance: 'Workspace',
597
- });
598
- this.finishLocalEditor(editor.kind, created.id, created.name, 'Created');
599
- } else {
600
- const registry = AgentRoutineRegistry.fromShellPaths(shellPaths);
601
- if (editor.mode === 'update' && editor.recordId) {
602
- const updated = registry.update(editor.recordId, {
603
- name: this.editorField('name'),
604
- description: this.editorField('description'),
605
- steps: this.editorField('steps'),
606
- triggers: splitList(this.editorField('triggers')),
607
- tags: splitList(this.editorField('tags')),
608
- requirements: buildAgentWorkspaceRequirements((id) => this.editorField(id)),
609
- provenance: 'Workspace',
610
- });
611
- registry.setEnabled(updated.id, isAffirmative(this.editorField('enabled')));
612
- this.finishLocalEditor(editor.kind, updated.id, updated.name, 'Updated');
613
- return;
614
- }
615
- const created = registry.create({
616
- name: this.editorField('name'),
617
- description: this.editorField('description'),
618
- steps: this.editorField('steps'),
619
- triggers: splitList(this.editorField('triggers')),
620
- tags: splitList(this.editorField('tags')),
621
- requirements: buildAgentWorkspaceRequirements((id) => this.editorField(id)),
622
- enabled: isAffirmative(this.editorField('enabled')),
623
- source: 'user',
624
- provenance: 'Workspace',
625
- });
626
- this.finishLocalEditor(editor.kind, created.id, created.name, 'Created');
627
- }
561
+ submitAgentWorkspaceLocalRegistryEditor(shellPaths, editor, {
562
+ readField: (id) => this.editorField(id),
563
+ learnedBehaviorTarget: () => this.learnedBehaviorTarget(),
564
+ submitDeleteEditor: () => this.submitLocalDeleteEditor(shellPaths, editor),
565
+ finishLocalEditor: (kind, id, name, verb) => this.finishLocalEditor(kind, id, name, verb),
566
+ finishProfileEditor: (profile) => this.finishProfileEditor(profile),
567
+ });
628
568
  } catch (error) {
629
569
  const detail = error instanceof Error ? error.message : String(error);
630
570
  this.localEditor = { ...editor, message: detail };
@@ -131,6 +131,18 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
131
131
  return withState(state, true);
132
132
  }
133
133
 
134
+ if (handleModelPickerToken({
135
+ modelPicker: state.modelPicker,
136
+ modalStack: state.modalStack,
137
+ commandContext: state.commandContext,
138
+ getViewportHeight: state.getViewportHeight,
139
+ requestRender: state.requestRender,
140
+ handleEscape: state.handleEscape,
141
+ onModelPickerCommit: state.onModelPickerCommit,
142
+ }, token)) {
143
+ return withState(state, true);
144
+ }
145
+
134
146
  if (handleSettingsModalToken({
135
147
  settingsModal: state.settingsModal,
136
148
  commandContext: state.commandContext,
@@ -200,18 +212,6 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
200
212
  return withState(state, true, historyState);
201
213
  }
202
214
 
203
- if (handleModelPickerToken({
204
- modelPicker: state.modelPicker,
205
- modalStack: state.modalStack,
206
- commandContext: state.commandContext,
207
- getViewportHeight: state.getViewportHeight,
208
- requestRender: state.requestRender,
209
- handleEscape: state.handleEscape,
210
- onModelPickerCommit: state.onModelPickerCommit,
211
- }, token)) {
212
- return withState(state, true);
213
- }
214
-
215
215
  if (handleLiveTailToken({
216
216
  liveTailModal: state.liveTailModal,
217
217
  processModal: state.processModal,
@@ -336,7 +336,8 @@ export class InputHandler {
336
336
  public handleBlockToggle(): void { handleBlockToggleForHandler(this); }
337
337
  public handleCtrlC(): void { handleCtrlCForHandler(this); }
338
338
  public modalOpened(name: string): void {
339
- if (name !== 'agentWorkspace' && this.agentWorkspace.active) {
339
+ const keepAgentWorkspaceUnderlay = name === 'modelPicker' || name === 'settings';
340
+ if (name !== 'agentWorkspace' && !keepAgentWorkspaceUnderlay && this.agentWorkspace.active) {
340
341
  this.closeAgentWorkspaceModal();
341
342
  }
342
343
  modalOpenedForHandler(this, name);
package/src/main.ts CHANGED
@@ -15,6 +15,8 @@ import { PermissionPromptUI } from './permissions/prompt.ts';
15
15
  import { CommandRegistry } from './input/command-registry.ts';
16
16
  import type { CommandContext } from './input/command-registry.ts';
17
17
  import { renderProcessIndicator } from './renderer/process-indicator.ts';
18
+ import { renderModelWorkspace } from './renderer/model-workspace.ts';
19
+ import { renderSettingsModal } from './renderer/settings-modal.ts';
18
20
  import { registerBuiltinCommands } from './input/commands.ts';
19
21
  import { ScheduleManager } from '@pellux/goodvibes-sdk/platform/tools';
20
22
  import { InputHistory } from './input/input-history.ts';
@@ -40,7 +42,7 @@ import {
40
42
  } from '@/runtime/index.ts';
41
43
  import type { SessionSnapshot } from '@/runtime/index.ts';
42
44
  import { handleBlockingShellInput, type PendingPermissionState } from './shell/blocking-input.ts';
43
- import { createAgentWorkspaceFullscreenComposite } from './shell/agent-workspace-fullscreen.ts';
45
+ import { createAgentWorkspaceFullscreenComposite, createFullscreenCompositeFromLines } from './shell/agent-workspace-fullscreen.ts';
44
46
  import { getTerminalSize } from './shell/terminal-size.ts';
45
47
  import { wireShellUiOpeners } from './shell/ui-openers.ts';
46
48
  import { deriveComposerState } from './core/composer-state.ts';
@@ -477,6 +479,14 @@ async function main() {
477
479
  input.setPanelMouseLayout(null);
478
480
  activeConversationWidth = width;
479
481
  conversation.setSplashSuppressed(true);
482
+ if (input.modelPicker.active) {
483
+ compositor.composite(createFullscreenCompositeFromLines(renderModelWorkspace(input.modelPicker, width, height), width, height));
484
+ return;
485
+ }
486
+ if (input.settingsModal.active) {
487
+ compositor.composite(createFullscreenCompositeFromLines(renderSettingsModal(input.settingsModal, width, height), width, height));
488
+ return;
489
+ }
480
490
  compositor.composite(createAgentWorkspaceFullscreenComposite(input.agentWorkspace, width, height));
481
491
  return;
482
492
  }
@@ -709,7 +719,6 @@ async function main() {
709
719
  streamTokenSpeed = elapsed > 0 ? streamDeltaCount / elapsed : 0;
710
720
  }));
711
721
 
712
- // --- Terminal setup ---
713
722
  stdin.setRawMode(true);
714
723
  stdin.resume();
715
724
  stdin.setEncoding('utf8');
@@ -748,11 +757,9 @@ async function main() {
748
757
  process.on('unhandledRejection', unhandledRejectionHandler);
749
758
  stdout.on('resize', resizeHandler);
750
759
 
751
- // Initial render
752
760
  conversation.rebuildHistory();
753
761
  render();
754
762
 
755
- // --- Crash recovery check ---
756
763
  const recoveryInfo = checkRecoveryFile({ workingDirectory: workingDir, homeDirectory });
757
764
  if (recoveryInfo) {
758
765
  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.`);
@@ -763,7 +770,6 @@ async function main() {
763
770
  recoveryPending = true;
764
771
  }
765
772
 
766
- // --- Auto-save to recovery file every 60s ---
767
773
  recoveryInterval = setInterval(() => {
768
774
  const snapshot = buildCurrentSessionSnapshot();
769
775
  writeRecoveryFile(
@@ -773,9 +779,7 @@ async function main() {
773
779
  { workingDirectory: workingDir, homeDirectory },
774
780
  );
775
781
  }, 60_000);
776
-
777
782
  }
778
-
779
783
  main().catch((err: unknown) => {
780
784
  const detail = formatFatalStartupErrorForLog(err);
781
785
  try {