@pellux/goodvibes-tui 0.19.33 → 0.19.35
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 +23 -0
- package/README.md +6 -3
- package/docs/foundation-artifacts/operator-contract.json +284 -112
- package/package.json +2 -2
- package/src/cli/management.ts +2 -2
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +370 -0
- package/src/input/commands/local-auth-runtime.ts +4 -4
- package/src/input/commands/tts-runtime.ts +93 -10
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +1 -0
- package/src/input/handler-feed.ts +6 -0
- package/src/input/handler-modal-routes.ts +23 -10
- package/src/input/handler-modal-token-routes.ts +9 -0
- package/src/input/handler-onboarding-cloudflare.ts +391 -0
- package/src/input/handler-onboarding.ts +33 -0
- package/src/input/handler-picker-routes.ts +1 -1
- package/src/input/handler.ts +4 -1
- package/src/input/model-picker-types.ts +125 -0
- package/src/input/model-picker.ts +144 -135
- package/src/input/onboarding/onboarding-wizard-apply.ts +85 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +494 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +204 -0
- package/src/input/onboarding/onboarding-wizard-constants.ts +12 -1
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +117 -0
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +3 -41
- package/src/input/onboarding/onboarding-wizard-steps.ts +6 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +8 -0
- package/src/input/settings-modal-types.ts +2 -1
- package/src/input/settings-modal.ts +30 -8
- package/src/renderer/buffer.ts +40 -2
- package/src/renderer/compositor.ts +25 -17
- package/src/renderer/model-picker-overlay.ts +70 -0
- package/src/renderer/settings-modal-helpers.ts +9 -0
- package/src/runtime/cloudflare-control-plane.ts +349 -0
- package/src/runtime/onboarding/apply.ts +9 -8
- package/src/runtime/onboarding/derivation.ts +26 -1
- package/src/runtime/onboarding/snapshot.ts +2 -0
- package/src/runtime/onboarding/types.ts +5 -1
- package/src/shell/ui-openers.ts +10 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLOUDFLARE_COMPONENT_IDS,
|
|
3
|
+
CLOUDFLARE_COMPONENT_LABELS,
|
|
4
|
+
DEFAULT_CLOUDFLARE_COMPONENT_SELECTION,
|
|
5
|
+
type CloudflareBatchMode,
|
|
6
|
+
type CloudflareComponent,
|
|
7
|
+
type CloudflareComponentSelection,
|
|
8
|
+
type CloudflareProvisionRequest,
|
|
9
|
+
} from '../../runtime/cloudflare-control-plane.ts';
|
|
10
|
+
import { buildGoodVibesSecretRef, normalizeText } from './onboarding-wizard-helpers.ts';
|
|
11
|
+
import type { OnboardingWizardController } from './onboarding-wizard.ts';
|
|
12
|
+
import type { OnboardingWizardRadioOption } from './onboarding-wizard-types.ts';
|
|
13
|
+
|
|
14
|
+
export const CLOUDFLARE_SETUP_SOURCE_OPTIONS: readonly OnboardingWizardRadioOption[] = [
|
|
15
|
+
{
|
|
16
|
+
id: 'save-only',
|
|
17
|
+
label: 'Save settings only',
|
|
18
|
+
hint: 'Persist Cloudflare fields without passing a token to the daemon. Provision later from the Cloudflare command or settings.',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'bootstrap-token',
|
|
22
|
+
label: 'Paste temporary bootstrap token',
|
|
23
|
+
hint: 'Use a short-lived Cloudflare token once. The SDK creates and stores a narrower GoodVibes operational token.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'bootstrap-env',
|
|
27
|
+
label: 'Read bootstrap token from environment',
|
|
28
|
+
hint: 'Read a temporary token from an environment variable and pass it once to the SDK. The value is not stored.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'operational-token',
|
|
32
|
+
label: 'Paste final operational token',
|
|
33
|
+
hint: 'Use a token you already created. The SDK can store it as a GoodVibes secret during provisioning.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'operational-env',
|
|
37
|
+
label: 'Use final token from environment',
|
|
38
|
+
hint: 'Use an environment-backed token reference such as CLOUDFLARE_API_TOKEN.',
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export const CLOUDFLARE_BATCH_MODE_OPTIONS: readonly OnboardingWizardRadioOption[] = [
|
|
43
|
+
{
|
|
44
|
+
id: 'off',
|
|
45
|
+
label: 'Off',
|
|
46
|
+
hint: 'Keep daemon requests on the immediate local path. Cloudflare resource settings can still be saved.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'explicit',
|
|
50
|
+
label: 'Explicit batch only',
|
|
51
|
+
hint: 'Only requests explicitly marked for batch execution use the configured batch path.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'eligible-by-default',
|
|
55
|
+
label: 'Eligible requests batch by default',
|
|
56
|
+
hint: 'Batch-capable daemon work can use the configured batch path unless the caller opts out.',
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export const CLOUDFLARE_YES_NO_OPTIONS: readonly OnboardingWizardRadioOption[] = [
|
|
61
|
+
{ id: 'yes', label: 'Yes', hint: 'Enable this behavior.' },
|
|
62
|
+
{ id: 'no', label: 'No', hint: 'Leave this behavior off.' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export const CLOUDFLARE_PROVISION_OPTIONS: readonly OnboardingWizardRadioOption[] = [
|
|
66
|
+
{
|
|
67
|
+
id: 'no',
|
|
68
|
+
label: 'No, save configuration only',
|
|
69
|
+
hint: 'Final Apply saves the settings. Use the Cloudflare command or this wizard later to provision resources.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'yes',
|
|
73
|
+
label: 'Yes, create or update Cloudflare resources',
|
|
74
|
+
hint: 'Final Apply asks the daemon SDK route to create/update selected Cloudflare resources and verify the Worker when possible.',
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
export type CloudflareSetupSource =
|
|
79
|
+
| 'save-only'
|
|
80
|
+
| 'bootstrap-token'
|
|
81
|
+
| 'bootstrap-env'
|
|
82
|
+
| 'operational-token'
|
|
83
|
+
| 'operational-env';
|
|
84
|
+
|
|
85
|
+
export function cloudflareComponentFieldId(component: CloudflareComponent): string {
|
|
86
|
+
return `cloudflare.component.${component}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function cloudflareComponentLabel(component: CloudflareComponent): string {
|
|
90
|
+
return CLOUDFLARE_COMPONENT_LABELS[component];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isCloudflareConfigured(controller: OnboardingWizardController): boolean {
|
|
94
|
+
const config = controller.runtimeSnapshot?.config.cloudflare;
|
|
95
|
+
if (!config) return false;
|
|
96
|
+
return config.enabled
|
|
97
|
+
|| normalizeText(config.accountId).length > 0
|
|
98
|
+
|| normalizeText(config.apiTokenRef).length > 0
|
|
99
|
+
|| normalizeText(config.workerBaseUrl).length > 0
|
|
100
|
+
|| normalizeText(config.workerName).length > 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function shouldShowCloudflareStep(controller: OnboardingWizardController): boolean {
|
|
104
|
+
return controller.isCapabilitySelected('cloudflare-batch') || isCloudflareConfigured(controller);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getCloudflareSetupSource(controller: OnboardingWizardController): CloudflareSetupSource {
|
|
108
|
+
const configuredTokenRef = controller.runtimeSnapshot?.config.cloudflare.apiTokenRef ?? '';
|
|
109
|
+
const defaultValue = configuredTokenRef.startsWith('goodvibes://secrets/env/') ? 'operational-env' : 'save-only';
|
|
110
|
+
const value = controller.getStringFieldValue('cloudflare.setup-source', defaultValue);
|
|
111
|
+
if (
|
|
112
|
+
value === 'bootstrap-token'
|
|
113
|
+
|| value === 'bootstrap-env'
|
|
114
|
+
|| value === 'operational-token'
|
|
115
|
+
|| value === 'operational-env'
|
|
116
|
+
|| value === 'save-only'
|
|
117
|
+
) {
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
return 'save-only';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getCloudflareComponentSelection(controller: OnboardingWizardController): Record<CloudflareComponent, boolean> {
|
|
124
|
+
const selected: Record<CloudflareComponent, boolean> = { ...DEFAULT_CLOUDFLARE_COMPONENT_SELECTION };
|
|
125
|
+
const configured = controller.runtimeSnapshot?.config.cloudflare;
|
|
126
|
+
for (const component of CLOUDFLARE_COMPONENT_IDS) {
|
|
127
|
+
const fallback = configured?.enabled === true
|
|
128
|
+
? component === 'workers' || component === 'queues'
|
|
129
|
+
: DEFAULT_CLOUDFLARE_COMPONENT_SELECTION[component];
|
|
130
|
+
selected[component] = controller.getBooleanFieldValue(cloudflareComponentFieldId(component), fallback);
|
|
131
|
+
}
|
|
132
|
+
return selected;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getSelectedCloudflareComponents(controller: OnboardingWizardController): CloudflareComponentSelection {
|
|
136
|
+
return getCloudflareComponentSelection(controller);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getCloudflareBatchMode(controller: OnboardingWizardController): CloudflareBatchMode {
|
|
140
|
+
const value = controller.getStringFieldValue('cloudflare.batch-mode', controller.runtimeSnapshot?.config.batch.mode ?? 'off');
|
|
141
|
+
return value === 'explicit' || value === 'eligible-by-default' ? value : 'off';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function buildCloudflareApiTokenRef(envName: string): string {
|
|
145
|
+
const normalized = normalizeText(envName) || 'CLOUDFLARE_API_TOKEN';
|
|
146
|
+
return `goodvibes://secrets/env/${encodeURIComponent(normalized)}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildCloudflareProvisionRequest(controller: OnboardingWizardController, options: {
|
|
150
|
+
readonly includeTransientSecrets?: boolean;
|
|
151
|
+
} = {}): CloudflareProvisionRequest {
|
|
152
|
+
const components = getCloudflareComponentSelection(controller);
|
|
153
|
+
const setupSource = getCloudflareSetupSource(controller);
|
|
154
|
+
const accountId = controller.getStringFieldValue('cloudflare.account-id', controller.runtimeSnapshot?.config.cloudflare.accountId ?? '');
|
|
155
|
+
const zoneId = controller.getStringFieldValue('cloudflare.zone-id', controller.runtimeSnapshot?.config.cloudflare.zoneId ?? '');
|
|
156
|
+
const zoneName = controller.getStringFieldValue('cloudflare.zone-name', controller.runtimeSnapshot?.config.cloudflare.zoneName ?? '');
|
|
157
|
+
const apiToken = setupSource === 'operational-token' && options.includeTransientSecrets
|
|
158
|
+
? controller.getStringFieldValue('cloudflare.operational-token', '')
|
|
159
|
+
: '';
|
|
160
|
+
const apiTokenRef = setupSource === 'operational-env'
|
|
161
|
+
? buildCloudflareApiTokenRef(controller.getStringFieldValue('cloudflare.operational-env-name', 'CLOUDFLARE_API_TOKEN'))
|
|
162
|
+
: controller.runtimeSnapshot?.config.cloudflare.apiTokenRef ?? '';
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
components,
|
|
166
|
+
...(accountId ? { accountId } : {}),
|
|
167
|
+
...(zoneId ? { zoneId } : {}),
|
|
168
|
+
...(zoneName ? { zoneName } : {}),
|
|
169
|
+
...(apiToken ? { apiToken, storeApiToken: true } : {}),
|
|
170
|
+
...(!apiToken && apiTokenRef ? { apiTokenRef } : {}),
|
|
171
|
+
workerName: controller.getStringFieldValue('cloudflare.worker-name', controller.runtimeSnapshot?.config.cloudflare.workerName ?? 'goodvibes-batch-worker'),
|
|
172
|
+
workerSubdomain: controller.getStringFieldValue('cloudflare.worker-subdomain', controller.runtimeSnapshot?.config.cloudflare.workerSubdomain ?? ''),
|
|
173
|
+
workerHostname: controller.getStringFieldValue('cloudflare.worker-hostname', controller.runtimeSnapshot?.config.cloudflare.workerHostname ?? ''),
|
|
174
|
+
workerBaseUrl: controller.getStringFieldValue('cloudflare.worker-base-url', controller.runtimeSnapshot?.config.cloudflare.workerBaseUrl ?? ''),
|
|
175
|
+
daemonBaseUrl: controller.getStringFieldValue('cloudflare.daemon-base-url', controller.runtimeSnapshot?.config.cloudflare.daemonBaseUrl ?? ''),
|
|
176
|
+
daemonHostname: controller.getStringFieldValue('cloudflare.daemon-hostname', controller.runtimeSnapshot?.config.cloudflare.daemonHostname ?? ''),
|
|
177
|
+
queueName: controller.getStringFieldValue('cloudflare.queue-name', controller.runtimeSnapshot?.config.cloudflare.queueName ?? 'goodvibes-batch'),
|
|
178
|
+
deadLetterQueueName: controller.getStringFieldValue('cloudflare.dead-letter-queue-name', controller.runtimeSnapshot?.config.cloudflare.deadLetterQueueName ?? 'goodvibes-batch-dlq'),
|
|
179
|
+
tunnelName: controller.getStringFieldValue('cloudflare.tunnel-name', controller.runtimeSnapshot?.config.cloudflare.tunnelName ?? 'goodvibes-daemon'),
|
|
180
|
+
tunnelId: controller.getStringFieldValue('cloudflare.tunnel-id', controller.runtimeSnapshot?.config.cloudflare.tunnelId ?? ''),
|
|
181
|
+
tunnelServiceUrl: controller.getStringFieldValue('cloudflare.tunnel-service-url', ''),
|
|
182
|
+
tunnelTokenRef: controller.getStringFieldValue('cloudflare.tunnel-token-ref', controller.runtimeSnapshot?.config.cloudflare.tunnelTokenRef ?? ''),
|
|
183
|
+
accessAppId: controller.getStringFieldValue('cloudflare.access-app-id', controller.runtimeSnapshot?.config.cloudflare.accessAppId ?? ''),
|
|
184
|
+
accessServiceTokenId: controller.getStringFieldValue('cloudflare.access-service-token-id', controller.runtimeSnapshot?.config.cloudflare.accessServiceTokenId ?? ''),
|
|
185
|
+
accessServiceTokenRef: controller.getStringFieldValue('cloudflare.access-service-token-ref', controller.runtimeSnapshot?.config.cloudflare.accessServiceTokenRef ?? ''),
|
|
186
|
+
kvNamespaceName: controller.getStringFieldValue('cloudflare.kv-namespace-name', controller.runtimeSnapshot?.config.cloudflare.kvNamespaceName ?? 'goodvibes-runtime'),
|
|
187
|
+
kvNamespaceId: controller.getStringFieldValue('cloudflare.kv-namespace-id', controller.runtimeSnapshot?.config.cloudflare.kvNamespaceId ?? ''),
|
|
188
|
+
durableObjectNamespaceName: controller.getStringFieldValue('cloudflare.do-namespace-name', controller.runtimeSnapshot?.config.cloudflare.durableObjectNamespaceName ?? 'GoodVibesCoordinator'),
|
|
189
|
+
durableObjectNamespaceId: controller.getStringFieldValue('cloudflare.do-namespace-id', controller.runtimeSnapshot?.config.cloudflare.durableObjectNamespaceId ?? ''),
|
|
190
|
+
r2BucketName: controller.getStringFieldValue('cloudflare.r2-bucket-name', controller.runtimeSnapshot?.config.cloudflare.r2BucketName ?? 'goodvibes-artifacts'),
|
|
191
|
+
secretsStoreName: controller.getStringFieldValue('cloudflare.secrets-store-name', controller.runtimeSnapshot?.config.cloudflare.secretsStoreName ?? 'goodvibes'),
|
|
192
|
+
secretsStoreId: controller.getStringFieldValue('cloudflare.secrets-store-id', controller.runtimeSnapshot?.config.cloudflare.secretsStoreId ?? ''),
|
|
193
|
+
workerCron: controller.getStringFieldValue('cloudflare.worker-cron', controller.runtimeSnapshot?.config.cloudflare.workerCron ?? '*/5 * * * *'),
|
|
194
|
+
enableWorkersDev: true,
|
|
195
|
+
queueJobPayloads: false,
|
|
196
|
+
persistConfig: true,
|
|
197
|
+
verify: true,
|
|
198
|
+
batchMode: getCloudflareBatchMode(controller),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function buildCloudflareOperationalTokenRef(): string {
|
|
203
|
+
return buildGoodVibesSecretRef('CLOUDFLARE_API_TOKEN');
|
|
204
|
+
}
|
|
@@ -6,6 +6,7 @@ export const STEP_ORDER: readonly OnboardingWizardStepId[] = [
|
|
|
6
6
|
'network',
|
|
7
7
|
'access',
|
|
8
8
|
'external-services',
|
|
9
|
+
'cloudflare',
|
|
9
10
|
'provider-access',
|
|
10
11
|
'default-model',
|
|
11
12
|
'experience',
|
|
@@ -41,7 +42,13 @@ export const DEFAULT_CAPABILITIES: readonly OnboardingStep1CapabilityItem[] = [
|
|
|
41
42
|
id: 'external-integrations',
|
|
42
43
|
label: 'Connect GoodVibes to external apps and services',
|
|
43
44
|
selected: false,
|
|
44
|
-
detail: 'Enable setup screens for Slack, Discord, Telegram, Teams, Matrix, and other app surfaces you choose.',
|
|
45
|
+
detail: 'Enable setup screens for Slack, Discord, Telegram, Home Assistant, Teams, Matrix, and other app surfaces you choose.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'cloudflare-batch',
|
|
49
|
+
label: 'Use Cloudflare for batch or remote daemon work',
|
|
50
|
+
selected: false,
|
|
51
|
+
detail: 'Optionally configure Cloudflare Workers and Queues for explicit or eligible background batch jobs. Immediate local daemon behavior stays the default unless enabled.',
|
|
45
52
|
},
|
|
46
53
|
];
|
|
47
54
|
|
|
@@ -95,6 +102,7 @@ export const INBOUND_EXTERNAL_SURFACE_IDS = new Set<string>([
|
|
|
95
102
|
'bluebubbles',
|
|
96
103
|
'discord',
|
|
97
104
|
'googleChat',
|
|
105
|
+
'homeassistant',
|
|
98
106
|
'imessage',
|
|
99
107
|
'mattermost',
|
|
100
108
|
'matrix',
|
|
@@ -116,6 +124,9 @@ export const REQUIRED_EXTERNAL_SETUP_FIELD_IDS = new Set<string>([
|
|
|
116
124
|
'external-services.telegram.bot-token',
|
|
117
125
|
'external-services.telegram.webhook-secret',
|
|
118
126
|
'external-services.webhook.default-target',
|
|
127
|
+
'external-services.homeassistant.instance-url',
|
|
128
|
+
'external-services.homeassistant.access-token',
|
|
129
|
+
'external-services.homeassistant.webhook-secret',
|
|
119
130
|
'external-services.google-chat.webhook-url',
|
|
120
131
|
'external-services.google-chat.verification-token',
|
|
121
132
|
'external-services.signal.bridge-url',
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { ExternalSurfaceSpec } from './onboarding-wizard-external-surfaces.ts';
|
|
2
|
+
|
|
3
|
+
export const WEBHOOK_SURFACE_SPEC: ExternalSurfaceSpec = {
|
|
4
|
+
id: 'webhook',
|
|
5
|
+
enabledFieldId: 'external-services.webhook',
|
|
6
|
+
enabledConfigKey: 'surfaces.webhook.enabled',
|
|
7
|
+
label: 'Outbound webhook surface',
|
|
8
|
+
hint: 'Enable outbound webhook delivery targets.',
|
|
9
|
+
defaultEnabled: (snapshot) => snapshot?.config.surfaces.webhook.enabled ?? false,
|
|
10
|
+
fields: [
|
|
11
|
+
{
|
|
12
|
+
id: 'external-services.webhook.default-target',
|
|
13
|
+
configKey: 'surfaces.webhook.defaultTarget',
|
|
14
|
+
kind: 'text',
|
|
15
|
+
label: 'Default webhook target',
|
|
16
|
+
hint: 'Fallback URL used for outbound webhook deliveries.',
|
|
17
|
+
placeholder: 'https://example.com/goodvibes',
|
|
18
|
+
defaultValue: (snapshot) => snapshot?.config.surfaces.webhook.defaultTarget ?? '',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'external-services.webhook.secret',
|
|
22
|
+
configKey: 'surfaces.webhook.secret',
|
|
23
|
+
kind: 'masked',
|
|
24
|
+
label: 'Webhook signing secret',
|
|
25
|
+
hint: 'Secret used to sign outbound webhook payloads.',
|
|
26
|
+
placeholder: 'secret',
|
|
27
|
+
defaultValue: (snapshot) => snapshot?.config.surfaces.webhook.secret ?? '',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'external-services.webhook.timeout-ms',
|
|
31
|
+
configKey: 'surfaces.webhook.timeoutMs',
|
|
32
|
+
kind: 'text',
|
|
33
|
+
valueType: 'number',
|
|
34
|
+
label: 'Webhook timeout ms',
|
|
35
|
+
hint: 'Request timeout for outbound webhook deliveries.',
|
|
36
|
+
placeholder: '10000',
|
|
37
|
+
defaultNumber: 10000,
|
|
38
|
+
min: 1000,
|
|
39
|
+
max: 60000,
|
|
40
|
+
defaultValue: (snapshot) => String(snapshot?.config.surfaces.webhook.timeoutMs ?? 10000),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const HOME_ASSISTANT_SURFACE_SPEC: ExternalSurfaceSpec = {
|
|
46
|
+
id: 'homeassistant',
|
|
47
|
+
enabledFieldId: 'external-services.homeassistant',
|
|
48
|
+
enabledConfigKey: 'surfaces.homeassistant.enabled',
|
|
49
|
+
label: 'Home Assistant surface',
|
|
50
|
+
hint: 'Enable the Home Assistant companion surface, daemon callbacks, and event delivery.',
|
|
51
|
+
defaultEnabled: (snapshot) => snapshot?.config.surfaces.homeassistant.enabled ?? false,
|
|
52
|
+
fields: [
|
|
53
|
+
{
|
|
54
|
+
id: 'external-services.homeassistant.instance-url',
|
|
55
|
+
configKey: 'surfaces.homeassistant.instanceUrl',
|
|
56
|
+
kind: 'text',
|
|
57
|
+
label: 'Home Assistant URL',
|
|
58
|
+
hint: 'Base URL of the Home Assistant instance.',
|
|
59
|
+
placeholder: 'http://homeassistant.local:8123',
|
|
60
|
+
defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.instanceUrl ?? '',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'external-services.homeassistant.access-token',
|
|
64
|
+
configKey: 'surfaces.homeassistant.accessToken',
|
|
65
|
+
kind: 'masked',
|
|
66
|
+
label: 'Home Assistant access token',
|
|
67
|
+
hint: 'Long-lived Home Assistant access token or goodvibes:// secret reference.',
|
|
68
|
+
placeholder: 'long-lived access token',
|
|
69
|
+
defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.accessToken ?? '',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'external-services.homeassistant.webhook-secret',
|
|
73
|
+
configKey: 'surfaces.homeassistant.webhookSecret',
|
|
74
|
+
kind: 'masked',
|
|
75
|
+
label: 'Home Assistant webhook secret',
|
|
76
|
+
hint: 'Shared secret used to verify inbound Home Assistant callbacks.',
|
|
77
|
+
placeholder: 'webhook secret',
|
|
78
|
+
defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.webhookSecret ?? '',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'external-services.homeassistant.default-conversation-id',
|
|
82
|
+
configKey: 'surfaces.homeassistant.defaultConversationId',
|
|
83
|
+
kind: 'text',
|
|
84
|
+
label: 'Default conversation ID',
|
|
85
|
+
hint: 'Default Home Assistant conversation id used for route binding.',
|
|
86
|
+
placeholder: 'goodvibes',
|
|
87
|
+
defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.defaultConversationId ?? 'goodvibes',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'external-services.homeassistant.device-id',
|
|
91
|
+
configKey: 'surfaces.homeassistant.deviceId',
|
|
92
|
+
kind: 'text',
|
|
93
|
+
label: 'Home Assistant device ID',
|
|
94
|
+
hint: 'Stable device identifier exposed by the GoodVibes daemon.',
|
|
95
|
+
placeholder: 'goodvibes-daemon',
|
|
96
|
+
defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.deviceId ?? 'goodvibes-daemon',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'external-services.homeassistant.device-name',
|
|
100
|
+
configKey: 'surfaces.homeassistant.deviceName',
|
|
101
|
+
kind: 'text',
|
|
102
|
+
label: 'Home Assistant device name',
|
|
103
|
+
hint: 'Display name for the GoodVibes daemon device in Home Assistant.',
|
|
104
|
+
placeholder: 'GoodVibes Daemon',
|
|
105
|
+
defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.deviceName ?? 'GoodVibes Daemon',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'external-services.homeassistant.event-type',
|
|
109
|
+
configKey: 'surfaces.homeassistant.eventType',
|
|
110
|
+
kind: 'text',
|
|
111
|
+
label: 'Home Assistant event type',
|
|
112
|
+
hint: 'Event type used for daemon-to-Home Assistant deliveries.',
|
|
113
|
+
placeholder: 'goodvibes_message',
|
|
114
|
+
defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.eventType ?? 'goodvibes_message',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { DEFAULT_CONFIG, type ConfigKey } from '../../config/index.ts';
|
|
8
8
|
import type { OnboardingSnapshotState } from '../../runtime/onboarding/index.ts';
|
|
9
9
|
import { TELEGRAM_MODE_OPTIONS, WHATSAPP_PROVIDER_OPTIONS } from './onboarding-wizard-constants.ts';
|
|
10
|
+
import { HOME_ASSISTANT_SURFACE_SPEC, WEBHOOK_SURFACE_SPEC } from './onboarding-wizard-external-surface-extra-specs.ts';
|
|
10
11
|
import type { OnboardingWizardRadioOption } from './onboarding-wizard-types.ts';
|
|
11
12
|
|
|
12
13
|
export interface ExternalSurfaceSetupFieldSpec {
|
|
@@ -333,47 +334,8 @@ export const EXTERNAL_SURFACE_SPECS: readonly ExternalSurfaceSpec[] = [
|
|
|
333
334
|
},
|
|
334
335
|
],
|
|
335
336
|
},
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
enabledFieldId: 'external-services.webhook',
|
|
339
|
-
enabledConfigKey: 'surfaces.webhook.enabled',
|
|
340
|
-
label: 'Outbound webhook surface',
|
|
341
|
-
hint: 'Enable outbound webhook delivery targets.',
|
|
342
|
-
defaultEnabled: (snapshot) => snapshot?.config.surfaces.webhook.enabled ?? false,
|
|
343
|
-
fields: [
|
|
344
|
-
{
|
|
345
|
-
id: 'external-services.webhook.default-target',
|
|
346
|
-
configKey: 'surfaces.webhook.defaultTarget',
|
|
347
|
-
kind: 'text',
|
|
348
|
-
label: 'Default webhook target',
|
|
349
|
-
hint: 'Fallback URL used for outbound webhook deliveries.',
|
|
350
|
-
placeholder: 'https://example.com/goodvibes',
|
|
351
|
-
defaultValue: (snapshot) => snapshot?.config.surfaces.webhook.defaultTarget ?? '',
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
id: 'external-services.webhook.secret',
|
|
355
|
-
configKey: 'surfaces.webhook.secret',
|
|
356
|
-
kind: 'masked',
|
|
357
|
-
label: 'Webhook signing secret',
|
|
358
|
-
hint: 'Secret used to sign outbound webhook payloads.',
|
|
359
|
-
placeholder: 'secret',
|
|
360
|
-
defaultValue: (snapshot) => snapshot?.config.surfaces.webhook.secret ?? '',
|
|
361
|
-
},
|
|
362
|
-
{
|
|
363
|
-
id: 'external-services.webhook.timeout-ms',
|
|
364
|
-
configKey: 'surfaces.webhook.timeoutMs',
|
|
365
|
-
kind: 'text',
|
|
366
|
-
valueType: 'number',
|
|
367
|
-
label: 'Webhook timeout ms',
|
|
368
|
-
hint: 'Request timeout for outbound webhook deliveries.',
|
|
369
|
-
placeholder: '10000',
|
|
370
|
-
defaultNumber: 10000,
|
|
371
|
-
min: 1000,
|
|
372
|
-
max: 60000,
|
|
373
|
-
defaultValue: (snapshot) => String(snapshot?.config.surfaces.webhook.timeoutMs ?? 10000),
|
|
374
|
-
},
|
|
375
|
-
],
|
|
376
|
-
},
|
|
337
|
+
WEBHOOK_SURFACE_SPEC,
|
|
338
|
+
HOME_ASSISTANT_SURFACE_SPEC,
|
|
377
339
|
{
|
|
378
340
|
id: 'googleChat',
|
|
379
341
|
enabledFieldId: 'external-services.google-chat',
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { NETWORK_MODE_OPTIONS, REASONING_OPTIONS, HITL_MODE_OPTIONS, GUIDANCE_MODE_OPTIONS, PERMISSION_MODE_OPTIONS, SECRET_POLICY_OPTIONS } from './onboarding-wizard-constants.ts';
|
|
2
|
+
import { shouldShowCloudflareStep } from './onboarding-wizard-cloudflare.ts';
|
|
3
|
+
import { buildCloudflareStep } from './onboarding-wizard-cloudflare-step.ts';
|
|
2
4
|
import {
|
|
3
5
|
EXTERNAL_SURFACE_SPECS,
|
|
4
6
|
getExternalSurfaceAutoStartDefaultValue,
|
|
@@ -19,27 +21,25 @@ export function buildOnboardingWizardSteps(controller: OnboardingWizardControlle
|
|
|
19
21
|
const steps: OnboardingWizardStepDefinition[] = [
|
|
20
22
|
buildCapabilitiesStep(controller),
|
|
21
23
|
];
|
|
22
|
-
|
|
23
24
|
if (hasServers) {
|
|
24
25
|
steps.push(buildNetworkStep(controller));
|
|
25
26
|
}
|
|
26
|
-
|
|
27
27
|
if (hasServers || controller.hasExistingAccessState()) {
|
|
28
28
|
steps.push(buildAccessStep(controller));
|
|
29
29
|
}
|
|
30
|
-
|
|
31
30
|
if (wantsExternalServices) {
|
|
32
31
|
steps.push(buildExternalServicesStep(controller));
|
|
33
32
|
for (const surface of getSelectedExternalSurfaceSpecs(controller)) {
|
|
34
33
|
steps.push(buildExternalSurfaceStep(controller, surface));
|
|
35
34
|
}
|
|
36
35
|
}
|
|
37
|
-
|
|
36
|
+
if (shouldShowCloudflareStep(controller)) {
|
|
37
|
+
steps.push(buildCloudflareStep(controller));
|
|
38
|
+
}
|
|
38
39
|
steps.push(buildProviderAccessStep(controller));
|
|
39
40
|
steps.push(buildDefaultModelStep(controller));
|
|
40
41
|
steps.push(buildExperienceStep(controller));
|
|
41
42
|
steps.push(buildReviewStep(controller));
|
|
42
|
-
|
|
43
43
|
return steps.map(addApplyAndContinueAction);
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -49,7 +49,7 @@ function buildApplyAndContinueAction(step: OnboardingWizardStepDefinition): Onbo
|
|
|
49
49
|
id: `${step.id}.apply-and-continue`,
|
|
50
50
|
action: 'apply-and-continue',
|
|
51
51
|
label: 'Apply & Continue To Next Section',
|
|
52
|
-
hint: '
|
|
52
|
+
hint: 'Save the current wizard selections in this onboarding session and move to the next section. Settings are persisted on the final Review apply.',
|
|
53
53
|
defaultValue: 'Apply & next',
|
|
54
54
|
spacerBeforeRows: 2,
|
|
55
55
|
};
|
|
@@ -25,6 +25,7 @@ export type OnboardingWizardStepId =
|
|
|
25
25
|
| 'access'
|
|
26
26
|
| 'external-services'
|
|
27
27
|
| OnboardingWizardExternalSurfaceStepId
|
|
28
|
+
| 'cloudflare'
|
|
28
29
|
| 'provider-access'
|
|
29
30
|
| 'default-model'
|
|
30
31
|
| 'experience'
|
|
@@ -47,6 +48,13 @@ export type OnboardingWizardAction =
|
|
|
47
48
|
| 'clear-capabilities'
|
|
48
49
|
| 'select-all-external-surfaces'
|
|
49
50
|
| 'clear-external-surfaces'
|
|
51
|
+
| 'cloudflare-token-requirements'
|
|
52
|
+
| 'cloudflare-create-operational-token'
|
|
53
|
+
| 'cloudflare-discover'
|
|
54
|
+
| 'cloudflare-validate'
|
|
55
|
+
| 'cloudflare-provision'
|
|
56
|
+
| 'cloudflare-verify'
|
|
57
|
+
| 'cloudflare-disable'
|
|
50
58
|
| 'start-openai-subscription'
|
|
51
59
|
| 'finish-openai-subscription';
|
|
52
60
|
|
|
@@ -2,7 +2,7 @@ import type { ConfigSetting } from '@pellux/goodvibes-sdk/platform/config/schema
|
|
|
2
2
|
import type { ProviderAuthFreshness, ProviderAuthRoute } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
|
|
3
3
|
import type { FeatureFlag, FlagState } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/types';
|
|
4
4
|
|
|
5
|
-
export type SettingsCategory = 'display' | 'ui' | 'provider' | 'subscriptions' | 'behavior' | 'storage' | 'permissions' | 'mcp' | 'sandbox' | 'surfaces' | 'danger' | 'tools' | 'flags' | 'network';
|
|
5
|
+
export type SettingsCategory = 'display' | 'ui' | 'provider' | 'subscriptions' | 'behavior' | 'storage' | 'permissions' | 'mcp' | 'sandbox' | 'surfaces' | 'cloudflare' | 'danger' | 'tools' | 'flags' | 'network';
|
|
6
6
|
|
|
7
7
|
export const SETTINGS_CATEGORIES: SettingsCategory[] = [
|
|
8
8
|
'display',
|
|
@@ -15,6 +15,7 @@ export const SETTINGS_CATEGORIES: SettingsCategory[] = [
|
|
|
15
15
|
'mcp',
|
|
16
16
|
'sandbox',
|
|
17
17
|
'surfaces',
|
|
18
|
+
'cloudflare',
|
|
18
19
|
'danger',
|
|
19
20
|
'tools',
|
|
20
21
|
'flags',
|
|
@@ -41,13 +41,21 @@ export {
|
|
|
41
41
|
type SubscriptionEntry,
|
|
42
42
|
} from './settings-modal-types.ts';
|
|
43
43
|
|
|
44
|
+
type ModelPickerLaunch =
|
|
45
|
+
| { readonly flow: 'providerModel'; readonly target: ModelPickerTarget }
|
|
46
|
+
| { readonly flow: 'model'; readonly target: ModelPickerTarget };
|
|
47
|
+
|
|
44
48
|
/**
|
|
45
|
-
* Map
|
|
46
|
-
*
|
|
49
|
+
* Map config keys to the shared provider/model picker flows. Provider rows open
|
|
50
|
+
* provider first; model rows open directly to models for the same target.
|
|
47
51
|
*/
|
|
48
|
-
function
|
|
49
|
-
if (key === 'helper.globalProvider'
|
|
50
|
-
if (key === '
|
|
52
|
+
function _modelPickerLaunchForKey(key: string): ModelPickerLaunch | null {
|
|
53
|
+
if (key === 'helper.globalProvider') return { flow: 'providerModel', target: 'helper' };
|
|
54
|
+
if (key === 'helper.globalModel') return { flow: 'model', target: 'helper' };
|
|
55
|
+
if (key === 'tools.llmProvider') return { flow: 'providerModel', target: 'tool' };
|
|
56
|
+
if (key === 'tools.llmModel') return { flow: 'model', target: 'tool' };
|
|
57
|
+
if (key === 'tts.llmProvider') return { flow: 'providerModel', target: 'tts' };
|
|
58
|
+
if (key === 'tts.llmModel') return { flow: 'model', target: 'tts' };
|
|
51
59
|
return null;
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -94,6 +102,8 @@ export class SettingsModal {
|
|
|
94
102
|
* Consumed and cleared by the route handler after each Enter/Space action.
|
|
95
103
|
*/
|
|
96
104
|
public pendingModelPickerTarget: ModelPickerTarget | null = null;
|
|
105
|
+
/** Set when the highlighted setting should open provider selection before model selection. */
|
|
106
|
+
public pendingProviderModelPickerTarget: ModelPickerTarget | null = null;
|
|
97
107
|
/** Provider awaiting explicit logout confirmation, if any. */
|
|
98
108
|
public subscriptionLogoutConfirmationTarget: string | null = null;
|
|
99
109
|
|
|
@@ -146,6 +156,8 @@ export class SettingsModal {
|
|
|
146
156
|
this.selectedIndex = 0;
|
|
147
157
|
this.editingMode = false;
|
|
148
158
|
this.editBuffer = '';
|
|
159
|
+
this.pendingModelPickerTarget = null;
|
|
160
|
+
this.pendingProviderModelPickerTarget = null;
|
|
149
161
|
this.mcpAllowAllConfirmationTarget = null;
|
|
150
162
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
151
163
|
this.lastSaveTriggeredRestart = null;
|
|
@@ -156,6 +168,8 @@ export class SettingsModal {
|
|
|
156
168
|
this.active = false;
|
|
157
169
|
this.editingMode = false;
|
|
158
170
|
this.editBuffer = '';
|
|
171
|
+
this.pendingModelPickerTarget = null;
|
|
172
|
+
this.pendingProviderModelPickerTarget = null;
|
|
159
173
|
this.mcpAllowAllConfirmationTarget = null;
|
|
160
174
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
161
175
|
this.lastSaveTriggeredRestart = null;
|
|
@@ -290,9 +304,13 @@ export class SettingsModal {
|
|
|
290
304
|
const { setting } = entry;
|
|
291
305
|
|
|
292
306
|
// Delegate provider/model picker settings to the model picker UI
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
295
|
-
|
|
307
|
+
const pickerLaunch = _modelPickerLaunchForKey(setting.key);
|
|
308
|
+
if (pickerLaunch !== null) {
|
|
309
|
+
if (pickerLaunch.flow === 'providerModel') {
|
|
310
|
+
this.pendingProviderModelPickerTarget = pickerLaunch.target;
|
|
311
|
+
} else {
|
|
312
|
+
this.pendingModelPickerTarget = pickerLaunch.target;
|
|
313
|
+
}
|
|
296
314
|
return;
|
|
297
315
|
}
|
|
298
316
|
|
|
@@ -521,6 +539,8 @@ export class SettingsModal {
|
|
|
521
539
|
cat = 'network';
|
|
522
540
|
} else if (rawCat === 'surfaces') {
|
|
523
541
|
cat = 'surfaces';
|
|
542
|
+
} else if (rawCat === 'cloudflare' || rawCat === 'batch') {
|
|
543
|
+
cat = 'cloudflare';
|
|
524
544
|
} else {
|
|
525
545
|
cat = rawCat as SettingsCategory;
|
|
526
546
|
}
|
|
@@ -750,6 +770,8 @@ export class SettingsModal {
|
|
|
750
770
|
}
|
|
751
771
|
} else if (rawCat === 'surfaces') {
|
|
752
772
|
cat = 'surfaces';
|
|
773
|
+
} else if (rawCat === 'cloudflare' || rawCat === 'batch') {
|
|
774
|
+
cat = 'cloudflare';
|
|
753
775
|
} else {
|
|
754
776
|
cat = rawCat as SettingsCategory;
|
|
755
777
|
}
|
package/src/renderer/buffer.ts
CHANGED
|
@@ -39,7 +39,13 @@ export class TerminalBuffer {
|
|
|
39
39
|
|
|
40
40
|
public blitLine(row: number, line: Line): void {
|
|
41
41
|
if (row >= 0 && row < this.height) {
|
|
42
|
-
this.cells[row]
|
|
42
|
+
const current = this.cells[row]!;
|
|
43
|
+
const next = createEmptyLine(this.width);
|
|
44
|
+
for (let x = 0; x < Math.min(line.length, this.width); x++) {
|
|
45
|
+
next[x] = { ...line[x]! };
|
|
46
|
+
}
|
|
47
|
+
if (linesEqual(current, next, this.width)) return;
|
|
48
|
+
this.cells[row] = next;
|
|
43
49
|
this.dirtyRows[row] = true;
|
|
44
50
|
}
|
|
45
51
|
}
|
|
@@ -56,12 +62,17 @@ export class TerminalBuffer {
|
|
|
56
62
|
* If dimensions changed, reallocates cells array.
|
|
57
63
|
* Always clears the dirty bitmap.
|
|
58
64
|
*/
|
|
59
|
-
public reset(width: number, height: number): void {
|
|
65
|
+
public reset(width: number, height: number, source?: TerminalBuffer | null): void {
|
|
60
66
|
if (width !== this.width || height !== this.height) {
|
|
61
67
|
this.width = width;
|
|
62
68
|
this.height = height;
|
|
63
69
|
this.cells = Array.from({ length: height }, () => createEmptyLine(width));
|
|
64
70
|
this.dirtyRows = new Array(height).fill(false);
|
|
71
|
+
} else if (source && source.width === width && source.height === height) {
|
|
72
|
+
for (let y = 0; y < this.height; y++) {
|
|
73
|
+
this.cells[y] = source.cells[y]!.map(cell => ({ ...cell }));
|
|
74
|
+
this.dirtyRows[y] = false;
|
|
75
|
+
}
|
|
65
76
|
} else {
|
|
66
77
|
for (let y = 0; y < this.height; y++) {
|
|
67
78
|
const row = this.cells[y]!;
|
|
@@ -72,4 +83,31 @@ export class TerminalBuffer {
|
|
|
72
83
|
}
|
|
73
84
|
}
|
|
74
85
|
}
|
|
86
|
+
|
|
87
|
+
public clearDirty(): void {
|
|
88
|
+
this.dirtyRows.fill(false);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function linesEqual(left: Line, right: Line, width: number): boolean {
|
|
93
|
+
for (let x = 0; x < width; x++) {
|
|
94
|
+
const a = left[x];
|
|
95
|
+
const b = right[x];
|
|
96
|
+
if (!a && !b) continue;
|
|
97
|
+
if (!a || !b) return false;
|
|
98
|
+
if (
|
|
99
|
+
a.char !== b.char
|
|
100
|
+
|| a.fg !== b.fg
|
|
101
|
+
|| a.bg !== b.bg
|
|
102
|
+
|| a.bold !== b.bold
|
|
103
|
+
|| a.dim !== b.dim
|
|
104
|
+
|| a.underline !== b.underline
|
|
105
|
+
|| a.italic !== b.italic
|
|
106
|
+
|| a.strikethrough !== b.strikethrough
|
|
107
|
+
|| (a.link ?? '') !== (b.link ?? '')
|
|
108
|
+
) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
75
113
|
}
|