sidebud 0.2.0 → 0.5.0
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 +118 -0
- package/README.md +37 -0
- package/dist/main.js +11284 -3582
- package/package.json +3 -1
- package/skills/widget-packs/SKILL.md +9 -8
- package/src/manage/guide.md +5 -0
- package/src/manage/index.html +4 -0
- package/src/manage/manage.css +129 -0
- package/src/manage/manage.js +244 -29
- package/src/manage/packs.js +37 -6
- package/src/manage/settings.js +232 -6
- package/src/manage/setup.js +128 -23
package/src/manage/setup.js
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
1
|
// First-run setup. Loaded after manage.js and shares its state and helpers (state, api, esc, render, reload, notice…).
|
|
2
|
-
const
|
|
2
|
+
const ALL_SETUP_STEPS = [
|
|
3
3
|
{ id: 'app', title: 'Get the app', short: 'Phone app' },
|
|
4
|
+
// Only while the Sidebud site's paywall is on (the companion then reports this step): the plan comes with the account.
|
|
5
|
+
{ id: 'account', title: 'Sign in to Sidebud', short: 'Sidebud account' },
|
|
4
6
|
{ id: 'execution', title: 'Choose who does the work', short: 'Execution agent' },
|
|
5
7
|
{ id: 'voice', title: 'Choose a voice', short: 'Voice agent' },
|
|
6
8
|
{ id: 'integrations', title: 'Add integrations', short: 'Integrations' },
|
|
9
|
+
{ id: 'agents', title: 'Choose your default agents', short: 'Default agents' },
|
|
10
|
+
{ id: 'decisions', title: 'Fast decisions', short: 'Fast decisions', beta: true, optional: true },
|
|
11
|
+
{ id: 'network', title: 'Who can reach this computer', short: 'Network' },
|
|
7
12
|
{ id: 'phone', title: 'Pair your phone', short: 'Pair phone' },
|
|
13
|
+
// macOS, an installed companion: the companion reports this step only there.
|
|
14
|
+
{ id: 'startup', title: 'Start Sidebud when you log in', short: 'Start at login', optional: true },
|
|
8
15
|
{ id: 'done', title: 'All set', short: 'Finish' },
|
|
9
16
|
];
|
|
17
|
+
/** The steps the companion reports, in order, plus the finish page. */
|
|
18
|
+
function setupSteps() {
|
|
19
|
+
return ALL_SETUP_STEPS.filter((step) => step.id === 'done' || (state && state.setup.steps[step.id]));
|
|
20
|
+
}
|
|
10
21
|
let setupSelections = null;
|
|
11
22
|
let setupBusy = false;
|
|
12
23
|
|
|
13
24
|
function currentSetupStep() {
|
|
14
25
|
const wanted = location.hash.slice(1).split('/')[1];
|
|
15
|
-
|
|
26
|
+
const steps = setupSteps();
|
|
27
|
+
// A required sign-in cannot be skipped: later steps wait for it.
|
|
28
|
+
const account = state?.setup.steps.account;
|
|
29
|
+
if (account && !account.done && steps.findIndex((step) => step.id === wanted) > steps.findIndex((step) => step.id === 'account')) return 'account';
|
|
30
|
+
if (steps.some((step) => step.id === wanted)) return wanted;
|
|
16
31
|
if (!state) return 'app';
|
|
17
32
|
// Once finished, Setup opens on the completion page; each step stays reachable from the stepper.
|
|
18
33
|
if (state.setup.completedAt) return 'done';
|
|
19
|
-
return
|
|
34
|
+
return steps.find((step) => step.id !== 'done' && !state.setup.steps[step.id].done)?.id || 'done';
|
|
20
35
|
}
|
|
21
36
|
function goToSetupStep(step) {
|
|
22
37
|
location.hash = `#setup/${step}`;
|
|
@@ -60,17 +75,26 @@ function checkCard(kind, intro) {
|
|
|
60
75
|
return `<div class="card stack check-card"><div class="row"><div><h3>${kind === 'execution' ? 'Test the execution agent' : 'Test voice to agent'}</h3><p class="hint">${intro}</p></div><span class="badge ${tone}">${esc(status)}</span></div>${check ? `<p class="${check.status === 'failed' ? 'error-text' : 'hint'}" role="status" aria-live="polite">${esc(check.detail)}${!check.current && check.status !== 'running' ? ' Your selection changed since this ran.' : ''}</p>` : ''}<div class="actions"><button class="primary" data-action="setup-check" data-kind="${kind}" ${check?.status === 'running' || !ready ? 'disabled' : ''}>${blocked ? 'Apply changes and run the test' : check ? 'Run the test again' : 'Run the test'}</button></div></div>`;
|
|
61
76
|
}
|
|
62
77
|
function setupStepper(current) {
|
|
63
|
-
return `<ol class="setup-steps">${
|
|
78
|
+
return `<ol class="setup-steps">${setupSteps().map((step, index) => {
|
|
64
79
|
const done = step.id === 'done' ? Boolean(state.setup.completedAt) : state.setup.steps[step.id].done;
|
|
65
|
-
return `<li><button data-action="setup-step" data-step="${step.id}" class="${step.id === current ? 'current' : ''} ${done ? 'done' : ''}" ${step.id === current ? 'aria-current="step"' : ''}><span class="step-mark" aria-hidden="true">${done ? '✓' : index + 1}</span><span>${esc(step.short)}</span></button></li>`;
|
|
80
|
+
return `<li><button data-action="setup-step" data-step="${step.id}" class="${step.id === current ? 'current' : ''} ${done ? 'done' : ''}" ${step.id === current ? 'aria-current="step"' : ''}><span class="step-mark" aria-hidden="true">${done ? '✓' : index + 1}</span><span>${esc(step.short)}${step.beta ? '<span class="tag-beta">Beta</span>' : ''}</span></button></li>`;
|
|
66
81
|
}).join('')}</ol>`;
|
|
67
82
|
}
|
|
68
|
-
function setupNav(current, { next = 'Continue', nextAction = 'setup-next', skip = true } = {}) {
|
|
69
|
-
const
|
|
70
|
-
|
|
83
|
+
function setupNav(current, { next = 'Continue', nextAction = 'setup-next', skip = true, skipAction = 'setup-next', disabled = false } = {}) {
|
|
84
|
+
const steps = setupSteps();
|
|
85
|
+
const index = steps.findIndex((step) => step.id === current);
|
|
86
|
+
return `<div class="setup-nav">${index > 0 ? `<button data-action="setup-step" data-step="${steps[index - 1].id}">Back</button>` : '<span></span>'}<div class="actions">${skip ? `<button data-action="${skipAction}">Skip for now</button>` : ''}<button class="primary" data-action="${nextAction}" ${disabled ? 'disabled' : ''}>${esc(next)}</button></div></div>`;
|
|
71
87
|
}
|
|
72
88
|
function appStep() {
|
|
73
|
-
return `<div class="setup-grid"><div class="card stack"><h3>Install Sidebud on your phone</h3><p>Scan this code with your phone’s camera. It opens the right store for your phone.</p><img class="download-qr" src="${esc(state.download.qr)}" alt="QR code to download the Sidebud app"><p class="hint">Or open <a href="${esc(state.download.url)}" target="_blank" rel="noopener">${esc(state.download.url.replace(/^https:\/\//, ''))}</a> on your phone. iPhone builds are available through TestFlight during the beta.</p></div><div class="card stack"><h3>What happens next</h3><ol class="plain-steps"><li><strong>Pick an execution agent.</strong> It does the work on this computer, and we test it with a real prompt.</li><li><strong>Pick a voice.</strong> We test that the voice can hand requests to your agent.</li><li><strong>Add integrations</strong> from the widget library, or have your agent build your own.</li><li><strong>Pair your phone</strong> by scanning a code in the app. You can <a href="#setup/phone">pair now</a> if the app is already installed.</li
|
|
89
|
+
return `<div class="setup-grid"><div class="card stack"><h3>Install Sidebud on your phone</h3><p>Scan this code with your phone’s camera. It opens the right store for your phone.</p><img class="download-qr" src="${esc(state.download.qr)}" alt="QR code to download the Sidebud app"><p class="hint">Or open <a href="${esc(state.download.url)}" target="_blank" rel="noopener">${esc(state.download.url.replace(/^https:\/\//, ''))}</a> on your phone. iPhone builds are available through TestFlight during the beta.</p></div><div class="card stack"><h3>What happens next</h3><ol class="plain-steps"><li><strong>Pick an execution agent.</strong> It does the work on this computer, and we test it with a real prompt.</li><li><strong>Pick a voice.</strong> We test that the voice can hand requests to your agent.</li><li><strong>Add integrations</strong> from the widget library, or have your agent build your own.</li><li><strong>Choose your default agents</strong>: the agents app with your main work agents, such as T3 Agents.</li><li><strong>Turn on fast decisions</strong> (optional, beta) so simple requests are answered in about a second.</li><li><strong>Choose who can reach this computer</strong>: your Wi-Fi network, or only your tailnet.</li><li><strong>Pair your phone</strong> by scanning a code in the app. You can <a href="#setup/phone">pair now</a> if the app is already installed.</li>${state.setup.steps.startup ? '<li><strong>Start Sidebud when you log in</strong> (optional), so it keeps running without a terminal window.</li>' : ''}</ol></div></div>${setupNav('app', { next: 'I have the app', nextAction: 'setup-app-done', skip: false })}`;
|
|
90
|
+
}
|
|
91
|
+
/** The same sign-in card as Settings; Continue stays off until this computer is signed in. */
|
|
92
|
+
function accountStep() {
|
|
93
|
+
const signedIn = state.setup.steps.account.done;
|
|
94
|
+
const plan = signedIn && state.plan.status === 'inactive'
|
|
95
|
+
? `<div class="card stack"><h3>Plan</h3><p class="hint">Your Sidebud plan isn't active yet. Calls, requests, and agent tests start working once it is.${state.plan.url ? '' : ' Check your plan on the Sidebud site.'}</p>${state.plan.url ? `<div class="actions"><a class="button primary" href="${esc(state.plan.url)}" target="_blank" rel="noopener">Manage plan</a></div>` : ''}</div>`
|
|
96
|
+
: '';
|
|
97
|
+
return `<p class="hint">Sidebud needs your account on this computer. You approve the sign-in in your browser; no password is typed here.</p>${accountCard()}${plan}${setupNav('account', { skip: false, disabled: !signedIn, next: signedIn ? 'Continue' : 'Sign in to continue' })}`;
|
|
74
98
|
}
|
|
75
99
|
function executionStep() {
|
|
76
100
|
if (!state.providers || state.connector.kind === 'memory')
|
|
@@ -86,7 +110,7 @@ function voiceStep() {
|
|
|
86
110
|
const model = provider.models.some((option) => option.id === saved.model) ? saved.model : provider.defaultModel;
|
|
87
111
|
const agent = state.setup.companionAgent;
|
|
88
112
|
// Same controls and ids as the Voice page, so its provider/model handlers and the voice sample work here too.
|
|
89
|
-
return `<div class="card stack"><h3>Voice agent</h3><p class="hint">The voice agent talks with you in calls and typed chat, and hands work to your execution agent. Its API key stays on this computer.</p><label>Voice provider<select id="voice-provider">${Object.values(catalog).map((option) => `<option value="${esc(option.id)}" ${option.id === selectedId ? 'selected' : ''}>${esc(option.label)}${option.id === 'gemini' ? ' (recommended)' : ''}</option>`).join('')}</select></label><p id="voice-untested" class="hint" ${provider.untested ? '' : 'hidden'}>Untested: this provider has not been run against the real service yet, so expect rough edges.</p><label>Voice model<select id="voice-model">${voiceModelOptions(provider, model)}</select></label><div id="voice-endpoint" class="stack">${voiceEndpointFields(provider, saved)}</div><label>Voice<select id="voice-name">${voiceNameOptions(voiceOptionsFor(provider, model), saved.voiceName || provider.defaultVoice)}</select></label><div class="voice-preview"><button type="button" data-action="preview-voice">Play voice sample</button><button type="button" data-action="stop-voice-preview" hidden>Stop sample</button><audio id="voice-preview-audio" controls preload="none" hidden></audio></div><p id="voice-preview-status" class="hint" role="status" aria-live="polite">Save a key below, then play a sample.</p></div><div id="voice-key-panel" class="stack">${voiceKeysView(selectedId)}</div><div class="actions"><button class="primary" data-action="setup-save-voice">Save voice and test it</button></div>${checkCard('voice', agent ? `Sends the voice agent a request as if you typed it, and checks that ${esc(agent.label)} receives and answers it.` : 'Finish the execution agent step first.')}${setupNav('voice', { next: state.setup.steps.voice.done ? 'Continue' : 'Continue without testing', skip: false })}`;
|
|
113
|
+
return `<div class="card stack"><h3>Voice agent</h3><p class="hint">The voice agent talks with you in calls and typed chat, and hands work to your execution agent. Its API key stays on this computer.</p><label>Voice provider<select id="voice-provider">${Object.values(catalog).map((option) => `<option value="${esc(option.id)}" ${option.id === selectedId ? 'selected' : ''}>${esc(option.label)}${option.id === 'gemini' ? ' (recommended)' : ''}</option>`).join('')}</select></label><p id="voice-untested" class="hint" ${provider.untested ? '' : 'hidden'}>Untested: this provider has not been run against the real service yet, so expect rough edges.</p><label>Voice model<select id="voice-model">${voiceModelOptions(provider, model)}</select></label><div id="voice-endpoint" class="stack">${voiceEndpointFields(provider, saved)}</div><label>Voice<select id="voice-name">${voiceNameOptions(voiceOptionsFor(provider, model), saved.voiceName || provider.defaultVoice)}</select></label><div class="voice-preview"><button type="button" data-action="preview-voice">Play voice sample</button><button type="button" data-action="stop-voice-preview" hidden>Stop sample</button><audio id="voice-preview-audio" controls preload="none" hidden></audio></div><p id="voice-preview-status" class="hint" role="status" aria-live="polite">Save a key below, then play a sample.</p></div><div id="voice-key-panel" class="stack">${voiceKeysView(selectedId)}</div><div class="card stack"><h3>Spoken mute and unmute</h3>${commandPhraseFields(voice.commands)}<p class="hint">The defaults work as they are. Change them any time on the <a href="#voice">Voice</a> page, along with auto-mute.</p><div class="actions"><button data-action="setup-save-commands">Save phrases</button></div></div><div class="actions"><button class="primary" data-action="setup-save-voice">Save voice and test it</button></div>${checkCard('voice', agent ? `Sends the voice agent a request as if you typed it, and checks that ${esc(agent.label)} receives and answers it.` : 'Finish the execution agent step first.')}${setupNav('voice', { next: state.setup.steps.voice.done ? 'Continue' : 'Continue without testing', skip: false })}`;
|
|
90
114
|
}
|
|
91
115
|
/** Library packs whose app is on this computer are suggested; the owner can untick any of them. */
|
|
92
116
|
function integrationSelections() {
|
|
@@ -99,7 +123,7 @@ function integrationsStep() {
|
|
|
99
123
|
const selected = integrationSelections();
|
|
100
124
|
const option = (entry) => {
|
|
101
125
|
const note = entry.installed ? 'Added' : entry.account;
|
|
102
|
-
return `<label class="integration-option"><input type="checkbox" data-integration="${esc(entry.id)}" ${entry.installed ? 'checked disabled' : selected.has(entry.id) ? 'checked' : ''}><span><strong>${esc(entry.name)}</strong>${entry.detected ? ' <span class="pill">Found on this computer</span>' : ''}<br><span class="hint">${esc(entry.description)}</span><br><span class="hint">${esc(note)}</span></span></label>`;
|
|
126
|
+
return `<label class="integration-option"><input type="checkbox" data-integration="${esc(entry.id)}" ${entry.installed ? 'checked disabled' : selected.has(entry.id) ? 'checked' : ''}><span><strong>${esc(entry.name)}</strong>${entry.detected ? ' <span class="pill">Found on this computer</span>' : ''}<br><span class="hint">${esc(entry.description)}</span><br><span class="hint">${esc(note)}</span><span class="access-line">${accessBadge(entry.access)}${controlsComputer(entry.access) ? `<span class="hint">${esc(entry.access.points[0].text)}</span>` : ''}</span></span></label>`;
|
|
103
127
|
};
|
|
104
128
|
// Apps found on this computer first, then the rest of the library.
|
|
105
129
|
const packs = [...state.integrationCatalog].sort((a, b) => Number(b.detected) - Number(a.detected));
|
|
@@ -108,11 +132,37 @@ function integrationsStep() {
|
|
|
108
132
|
: '<p class="hint">The widget library has no packs yet.</p>';
|
|
109
133
|
return `<div class="card stack"><h3>What should Sidebud reach?</h3><p class="hint">These are widget packs from the Sidebud widget library. Each is added disabled. Your execution agent then sets it up, uses settings and keys it finds on this computer, and reports on your phone what it needs from you. Your agent can also build packs for anything else you use.</p>${packs.length ? `<div class="integration-options">${packs.map(option).join('')}</div>` : empty}<p class="hint">More integrations, and ways to make your own, are under <a href="#mcp">MCP & widgets</a>.</p></div>${setupNav('integrations', { next: 'Add selected and continue', nextAction: 'setup-add-integrations' })}`;
|
|
110
134
|
}
|
|
135
|
+
/** The user's default agents: their main work agents app, which is not the execution agent. */
|
|
136
|
+
function agentsStep() {
|
|
137
|
+
const apps = (state.packs?.packs || []).filter((pack) => pack.agentsApp);
|
|
138
|
+
const chosen = state.packs?.defaultAgents || '';
|
|
139
|
+
const status = (pack) => (pack.status.state === 'ok' ? '' : pack.status.state === 'disabled' ? ' · not turned on yet' : ' · being set up');
|
|
140
|
+
const body = apps.length
|
|
141
|
+
? `<div class="integration-options">${apps.map((pack) => `<label class="integration-option"><input type="radio" name="default-agents" value="${esc(pack.id)}" ${pack.id === chosen ? 'checked' : ''}><span><strong>${esc(pack.name)}</strong>${pack.id === chosen ? ' <span class="pill">Default</span>' : ''}<br><span class="hint">${esc(pack.description)}${esc(status(pack))}</span></span></label>`).join('')}</div>`
|
|
142
|
+
: '<p class="muted">No agents app is installed yet. Add T3 Agents, Hermes, OpenClaw, or another agents app under <a href="#mcp">MCP & widgets</a>, then choose it under Execution → Default agents.</p>';
|
|
143
|
+
return `<div class="card stack"><h3>Where do your main work agents live?</h3><p class="hint">Your default agents are the agents app you work with most. When you ask “what agents are running?”, Sidebud checks this app. They are not your execution agent: the execution agent takes requests from voice and does the work on this computer.</p>${body}</div>${setupNav('agents', apps.length ? { next: 'Save and continue', nextAction: 'setup-save-agents' } : { next: 'Continue', nextAction: 'setup-ack-agents', skip: false })}`;
|
|
144
|
+
}
|
|
145
|
+
/** Optional while in beta: a fast decision model (the user's own TypeSafe key, or a local server). */
|
|
146
|
+
function decisionsStep() {
|
|
147
|
+
const jev = state.jev;
|
|
148
|
+
const on = jev.status.state === 'ready';
|
|
149
|
+
return `<div class="card stack"><div class="row"><div><h3>Answer simple requests in about a second <span class="tag-beta">Beta</span></h3><p class="hint">A fast decision model (TypeSafe’s Jev) decides whether this computer can handle a request itself: open or quit apps, volume, music, reminders, what your default agents are doing, one integration’s data, and home tiles. Anything else, or anything it is unsure about, goes to your execution agent as usual. You can skip this and turn it on later under Fast decisions.</p></div><span class="badge ${on ? 'good' : 'warn'}">${on ? `On · ${jev.status.kind === 'local' ? 'local server' : 'TypeSafe'}` : 'Off'}</span></div>
|
|
150
|
+
<label>TypeSafe API key<input id="setup-jev-key" type="password" autocomplete="new-password" placeholder="${jev.key.isSet ? 'A key is saved. Paste a new one to replace it.' : 'Paste your key from typesafe.ai'}"></label>
|
|
151
|
+
<label>Or a local Jev-compatible server (optional)<input id="setup-jev-url" type="url" placeholder="Empty: TypeSafe’s hosted API" value="${esc(jev.config.url ?? '')}"></label>
|
|
152
|
+
<p class="hint">Your key stays in this computer’s keychain and is only sent to TypeSafe. A local server gets no key.</p></div>${setupNav('decisions', { next: on ? 'Test and continue' : 'Save and test', nextAction: 'setup-save-decisions', skipAction: 'setup-skip-decisions' })}`;
|
|
153
|
+
}
|
|
154
|
+
function startupStep() {
|
|
155
|
+
const installed = state.install?.loginItem.installed;
|
|
156
|
+
return `${loginItemCard({ setup: true })}${setupNav('startup', { next: installed ? 'Continue' : 'Not now', nextAction: installed ? 'setup-next' : 'setup-ack-startup', skip: false })}`;
|
|
157
|
+
}
|
|
158
|
+
function networkStep() {
|
|
159
|
+
return `<div class="card stack"><h3>Where will your phone connect from?</h3><p class="hint">Your phone talks to this computer directly. Choose who can reach it; you can change this later in Settings.</p>${networkChoice({ setup: true })}</div>${setupNav('network', { next: 'Save and continue', nextAction: 'setup-save-network', skip: false })}`;
|
|
160
|
+
}
|
|
111
161
|
function phoneStep() {
|
|
112
162
|
const active = state.devices.filter((device) => !device.revokedAt);
|
|
113
163
|
if (active.length)
|
|
114
164
|
return `<div class="card stack"><h3>Your phone is paired</h3><p>${active.map((device) => `<strong>${esc(device.name)}</strong> (${esc(device.platform)})`).join(', ')} can use ${esc(state.name)}. Open Sidebud on your phone and check that it shows this computer as connected.</p><p class="hint">Pair more phones, or remove one, under <a href="#connection">Connection</a>.</p></div>${setupNav('phone', { skip: false })}`;
|
|
115
|
-
return `<div class="setup-grid"><div class="card stack"><h3>Scan with the Sidebud app</h3>${pairing ? `<img class="pairing-qr" src="${esc(pairing.qr)}" alt="Pairing QR code"><p class="hint">${Date.parse(pairing.expiresAt) <= Date.now() ? 'This code has expired. Generate a new one.' : `Works once. Expires ${esc(new Date(pairing.expiresAt).toLocaleTimeString())}.`}</p>` : `<p class="muted">${pairingLoading ? 'Preparing pairing code…' : 'Generate a code to pair your phone.'}</p>`}${pairingError ? `<p role="alert" class="hint error-text">Could not create a pairing code: ${esc(pairingError)}</p>` : ''}<div class="actions"><button data-action="pair-phone" ${pairingLoading ? 'disabled' : ''}>${pairing ? 'Generate a new code' : 'Generate pairing code'}</button></div></div><div class="card stack"><h3>On your phone</h3><ol class="plain-steps"><li>Open Sidebud. No app yet? <a href="#setup/app">Get it here</a>.</li><li>Choose <strong>Add computer</strong>, then <strong>Scan pairing code</strong>.</li><li>Point the camera at this code. This page updates when the phone is paired.</li></ol><p class="hint">Your phone must reach this computer on the same network or over Tailscale.</p></div></div>${setupNav('phone', { next: 'Continue', skip: true })}`;
|
|
165
|
+
return `<div class="setup-grid"><div class="card stack"><h3>Scan with the Sidebud app</h3>${pairing ? `<img class="pairing-qr" src="${esc(pairing.qr)}" alt="Pairing QR code"><p class="hint">${Date.parse(pairing.expiresAt) <= Date.now() ? 'This code has expired. Generate a new one.' : `Works once. Expires ${esc(new Date(pairing.expiresAt).toLocaleTimeString())}.`}</p>` : `<p class="muted">${pairingLoading ? 'Preparing pairing code…' : 'Generate a code to pair your phone.'}</p>`}${pairingError ? `<p role="alert" class="hint error-text">Could not create a pairing code: ${esc(pairingError)}</p>` : ''}<div class="actions"><button data-action="pair-phone" ${pairingLoading ? 'disabled' : ''}>${pairing ? 'Generate a new code' : 'Generate pairing code'}</button></div></div><div class="card stack"><h3>On your phone</h3><ol class="plain-steps"><li>Open Sidebud. No app yet? <a href="#setup/app">Get it here</a>.</li><li>Choose <strong>Add computer</strong>, then <strong>Scan pairing code</strong>.</li><li>Point the camera at this code. This page updates when the phone is paired.</li></ol><p class="hint">Your phone must reach this computer ${state.network.exposure === 'private' ? 'over Tailscale' : 'on the same network or over Tailscale'} (<a href="#setup/network">change</a>).</p></div></div>${setupNav('phone', { next: 'Continue', skip: true })}`;
|
|
116
166
|
}
|
|
117
167
|
/** Shown once setup is finished: carry on in the phone app; this page stays for managing the computer. */
|
|
118
168
|
function completedView() {
|
|
@@ -120,31 +170,37 @@ function completedView() {
|
|
|
120
170
|
{ hash: 'execution', title: 'Execution agents', text: 'Add agents, change the default, set thinking levels and prompts.' },
|
|
121
171
|
{ hash: 'mcp', title: 'MCP & widgets', text: 'Add integrations, set them up, and choose what your phone shows and hears.' },
|
|
122
172
|
{ hash: 'voice', title: 'Voice', text: 'Change the voice agent, model, and voice.' },
|
|
173
|
+
{ hash: 'decisions', title: 'Fast decisions (Beta)', text: 'Add or change your Jev key, or a local server, and test it.' },
|
|
123
174
|
{ hash: 'connection', title: 'Connection', text: 'Pair another phone or remove one.' },
|
|
124
175
|
];
|
|
125
|
-
return `<div class="card stack setup-complete"><div class="row"><span class="done-badge" aria-hidden="true">✓</span><div><h3>Well done. Sidebud is set up on ${esc(state.name)}.</h3><p>Continue in the Sidebud app on your phone from here: start a voice chat, send a message, and check on your widgets and running work.</p></div></div></div><div class="card stack"><div><h3>Manage everything from this dashboard, any time</h3><p class="hint">Keep the companion running on this computer. Open this page again with <span class="code">sidebud manage</span> or <span class="code">sidebud setup</span>.</p></div><div class="manage-links">${manage.map((item) => `<a class="manage-link" href="#${item.hash}"><strong>${esc(item.title)}</strong><span class="hint">${esc(item.text)}</span></a>`).join('')}</div></div
|
|
176
|
+
return `<div class="card stack setup-complete"><div class="row"><span class="done-badge" aria-hidden="true">✓</span><div><h3>Well done. Sidebud is set up on ${esc(state.name)}.</h3><p>Continue in the Sidebud app on your phone from here: start a voice chat, send a message, and check on your widgets and running work.</p></div></div></div><div class="card stack"><div><h3>Manage everything from this dashboard, any time</h3><p class="hint">Keep the companion running on this computer. Open this page again with <span class="code">sidebud manage</span> or <span class="code">sidebud setup</span>.</p></div><div class="manage-links">${manage.map((item) => `<a class="manage-link" href="#${item.hash}"><strong>${esc(item.title)}</strong><span class="hint">${esc(item.text)}</span></a>`).join('')}</div></div>${setupAccountCard()}<div class="actions"><button data-action="setup-reopen">Run setup again</button></div>`;
|
|
177
|
+
}
|
|
178
|
+
/** Optional on the last step: sign in, then choose whether settings follow the account. Setup finishes without it. */
|
|
179
|
+
function setupAccountCard() {
|
|
180
|
+
return state.capabilities.flags.companionSignIn ? accountCard({ setup: true }) : '';
|
|
126
181
|
}
|
|
127
182
|
function doneStep() {
|
|
128
183
|
if (state.setup.completedAt) return completedView();
|
|
129
|
-
const rows =
|
|
184
|
+
const rows = setupSteps().filter((step) => step.id !== 'done').map((step) => {
|
|
130
185
|
const done = state.setup.steps[step.id].done;
|
|
131
|
-
return `<li class="${done ? 'done' : ''}"><span class="step-mark" aria-hidden="true">${done ? '✓' : '•'}</span><span>${esc(step.title)}</span>${done ? '' : `<button data-action="setup-step" data-step="${step.id}">Finish this</button>`}</li>`;
|
|
186
|
+
return `<li class="${done ? 'done' : ''}"><span class="step-mark" aria-hidden="true">${done ? '✓' : '•'}</span><span>${esc(step.title)}${step.optional ? ' <span class="hint">(optional)</span>' : ''}</span>${done ? '' : `<button data-action="setup-step" data-step="${step.id}">Finish this</button>`}</li>`;
|
|
132
187
|
}).join('');
|
|
133
|
-
const ready =
|
|
134
|
-
return `<div class="card stack"><h3>${state.setup.completedAt ? 'Setup is finished' : ready ? 'Everything is ready' : 'Almost there'}</h3><ul class="setup-summary">${rows}</ul><p class="hint">Change any of this later from the sections on the left. Your phone shows your widgets, running tasks, and a button to start a voice chat.</p><div class="actions">${state.setup.completedAt ? '<button data-action="setup-reopen">Run setup again</button>' : '<button class="primary" data-action="setup-complete">Finish setup</button>'}</div></div
|
|
188
|
+
const ready = setupSteps().every((step) => step.id === 'done' || step.optional || state.setup.steps[step.id].done);
|
|
189
|
+
return `<div class="card stack"><h3>${state.setup.completedAt ? 'Setup is finished' : ready ? 'Everything is ready' : 'Almost there'}</h3><ul class="setup-summary">${rows}</ul><p class="hint">Change any of this later from the sections on the left. Your phone shows your widgets, running tasks, and a button to start a voice chat.</p><div class="actions">${state.setup.completedAt ? '<button data-action="setup-reopen">Run setup again</button>' : '<button class="primary" data-action="setup-complete">Finish setup</button>'}</div></div>${setupAccountCard()}${crashReportsCard({ setup: true })}`;
|
|
135
190
|
}
|
|
136
191
|
function setupView() {
|
|
137
192
|
const current = currentSetupStep();
|
|
138
|
-
const step =
|
|
139
|
-
const body = { app: appStep, execution: executionStep, voice: voiceStep, integrations: integrationsStep, phone: phoneStep, done: doneStep }[current]();
|
|
193
|
+
const step = setupSteps().find((candidate) => candidate.id === current);
|
|
194
|
+
const body = { app: appStep, account: accountStep, execution: executionStep, voice: voiceStep, integrations: integrationsStep, agents: agentsStep, decisions: decisionsStep, network: networkStep, phone: phoneStep, startup: startupStep, done: doneStep }[current]();
|
|
140
195
|
const title = current === 'done' && state.setup.completedAt ? 'Well done' : step.title;
|
|
141
|
-
return `<section class="section setup"><div><h2>${esc(title)}</h2><p class="lede">Set up Sidebud on ${esc(state.name)}. Keys, accounts, and agents stay on this computer.</p></div>${setupStepper(current)}${body}</section>`;
|
|
196
|
+
return `<section class="section setup"><div><h2>${esc(title)}${step.beta ? ' <span class="tag-beta">Beta</span>' : ''}</h2><p class="lede">Set up Sidebud on ${esc(state.name)}. Keys, accounts, and agents stay on this computer.</p></div>${setupStepper(current)}${body}</section>`;
|
|
142
197
|
}
|
|
143
198
|
/** Handles setup buttons; returns false for actions it does not own. */
|
|
144
199
|
async function handleSetupAction(action, button) {
|
|
145
200
|
if (!action.startsWith('setup-')) return false;
|
|
146
201
|
const current = currentSetupStep();
|
|
147
|
-
const
|
|
202
|
+
const steps = setupSteps();
|
|
203
|
+
const next = () => goToSetupStep(steps[steps.findIndex((step) => step.id === current) + 1]?.id || 'done');
|
|
148
204
|
if (action === 'setup-step') goToSetupStep(button.dataset.step);
|
|
149
205
|
else if (action === 'setup-next') next();
|
|
150
206
|
else if (action === 'setup-app-done') {
|
|
@@ -160,18 +216,30 @@ async function handleSetupAction(action, button) {
|
|
|
160
216
|
const live = state.voice.catalog[provider];
|
|
161
217
|
const endpoint = voiceEndpoint();
|
|
162
218
|
if (live?.needsWorkspaceId && !endpoint.workspaceId) throw new Error(`Add your ${live.label} workspace ID.`);
|
|
219
|
+
const phrases = commandPhrases();
|
|
220
|
+
if (phrases.error) throw new Error(phrases.error);
|
|
163
221
|
const typedKey = document.querySelector(`[data-voice-key="${provider}"]`)?.value;
|
|
164
222
|
if (typedKey) await api('voice-key', { provider, value: typedKey });
|
|
165
223
|
else if (!state.voice.keyStates[provider]?.isSet) throw new Error(`Add a ${live.label} API key first.`);
|
|
166
|
-
// Keep the owner's other voice settings (
|
|
167
|
-
await api('voice', { voice: { ...state.voice.voice, provider, model: $('#voice-model').value || null, voiceName: $('#voice-name').value || null, ...endpoint } });
|
|
224
|
+
// Keep the owner's other voice settings (call limits, auto-mute); setup changes the agent and the spoken commands.
|
|
225
|
+
await api('voice', { voice: { ...state.voice.voice, provider, model: $('#voice-model').value || null, voiceName: $('#voice-name').value || null, ...endpoint, commands: phrases.commands } });
|
|
168
226
|
await reload();
|
|
169
227
|
if (await applyChanges()) {
|
|
170
228
|
await api('setup/check', { kind: 'voice' });
|
|
171
229
|
await reload();
|
|
172
230
|
}
|
|
231
|
+
} else if (action === 'setup-save-commands') {
|
|
232
|
+
// Saved on their own too, so they need no voice key and survive skipping the voice test.
|
|
233
|
+
const phrases = commandPhrases();
|
|
234
|
+
if (phrases.error) throw new Error(phrases.error);
|
|
235
|
+
await api('voice', { voice: { ...state.voice.voice, commands: phrases.commands } });
|
|
236
|
+
notice('Spoken commands saved.');
|
|
237
|
+
await reload();
|
|
173
238
|
} else if (action === 'setup-add-integrations') {
|
|
174
239
|
const chosen = [...document.querySelectorAll('[data-integration]')].filter((box) => box.checked && !box.disabled).map((box) => box.dataset.integration);
|
|
240
|
+
// Packs that can control the whole computer are named, and need an explicit OK, before anything is added.
|
|
241
|
+
const full = state.integrationCatalog.filter((entry) => chosen.includes(entry.id) && controlsComputer(entry.access));
|
|
242
|
+
if (full.length && !confirm(`${full.map((entry) => entry.name).join(', ')} can control this whole computer: ${full.length === 1 ? 'it starts' : 'they start'} agents or run commands that can do anything you can here, including changing and deleting your files and using your signed-in accounts.\n\nAdd ${full.length === 1 ? 'it' : 'them'} anyway? Each is added switched off.`)) return true;
|
|
175
243
|
const failures = [];
|
|
176
244
|
button.disabled = true;
|
|
177
245
|
// Each pack installs disabled and starts the execution agent on its setup, the same as from MCP & widgets.
|
|
@@ -189,6 +257,43 @@ async function handleSetupAction(action, button) {
|
|
|
189
257
|
if (failures.length) notice(`Added, but some need attention: ${failures.join(' ')}`, true);
|
|
190
258
|
else if (chosen.length) notice(`Added. Your execution agent is setting up ${chosen.length === 1 ? 'the integration' : 'the integrations'} and reports on your phone when it needs you.`);
|
|
191
259
|
next();
|
|
260
|
+
} else if (action === 'setup-save-agents') {
|
|
261
|
+
const picked = document.querySelector('input[name="default-agents"]:checked')?.value;
|
|
262
|
+
if (!picked) throw new Error('Choose the agents app with your main work agents, or skip for now.');
|
|
263
|
+
await api('default-agents', { packId: picked });
|
|
264
|
+
await api('setup/ack', { step: 'agents' });
|
|
265
|
+
await reload();
|
|
266
|
+
next();
|
|
267
|
+
} else if (action === 'setup-ack-agents') {
|
|
268
|
+
await api('setup/ack', { step: 'agents' });
|
|
269
|
+
await reload();
|
|
270
|
+
next();
|
|
271
|
+
} else if (action === 'setup-ack-startup') {
|
|
272
|
+
await api('setup/ack', { step: 'startup' });
|
|
273
|
+
await reload();
|
|
274
|
+
next();
|
|
275
|
+
} else if (action === 'setup-skip-decisions') {
|
|
276
|
+
// Optional while in beta: skipping counts as done, so it never holds up finishing setup.
|
|
277
|
+
await api('setup/ack', { step: 'decisions' });
|
|
278
|
+
await reload();
|
|
279
|
+
next();
|
|
280
|
+
} else if (action === 'setup-save-decisions') {
|
|
281
|
+
const key = $('#setup-jev-key').value.trim();
|
|
282
|
+
const url = $('#setup-jev-url').value.trim();
|
|
283
|
+
if (!key && !url && !state.jev.key.isSet) throw new Error('Paste your TypeSafe key, or add a local server, or skip for now.');
|
|
284
|
+
if (key) await api('jev-key', { value: key });
|
|
285
|
+
if (url !== (state.jev.config.url ?? '') || state.jev.config.mode !== 'auto') await api('jev', { jev: { ...state.jev.config, mode: 'auto', url: url || null } });
|
|
286
|
+
await reload();
|
|
287
|
+
// Keys and servers apply on restart; the test then runs against the new setting.
|
|
288
|
+
if (state.restart.pending && !(await applyChanges())) return true;
|
|
289
|
+
const result = await api('jev/test', {});
|
|
290
|
+
await api('setup/ack', { step: 'decisions' });
|
|
291
|
+
await reload();
|
|
292
|
+
notice(`Fast decisions work: ${result.model} answered in ${result.ms} ms.`);
|
|
293
|
+
next();
|
|
294
|
+
} else if (action === 'setup-save-network') {
|
|
295
|
+
// The listener moves on restart, so the pairing code on the next step advertises the right addresses.
|
|
296
|
+
if (await saveNetworkChoice()) next();
|
|
192
297
|
} else if (action === 'setup-complete') {
|
|
193
298
|
await api('setup/complete', {});
|
|
194
299
|
history.pushState(null, '', '#setup/done');
|