sidebud 0.1.0 → 0.4.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 +65 -0
- package/dist/main.js +9361 -4209
- package/package.json +3 -2
- package/skills/widget-packs/SKILL.md +199 -0
- package/src/manage/guide/app-arrange.png +0 -0
- package/src/manage/guide/app-disk.png +0 -0
- package/src/manage/guide/app-open.png +0 -0
- package/src/manage/guide/app-thread.png +0 -0
- package/src/manage/guide/app-tiles.png +0 -0
- package/src/manage/guide/draft.png +0 -0
- package/src/manage/guide/layouts.png +0 -0
- package/src/manage/guide/library.png +0 -0
- package/src/manage/guide/pack-page.png +0 -0
- package/src/manage/guide/permissions.png +0 -0
- package/src/manage/guide/phone-home.png +0 -0
- package/src/manage/guide/phone-open.png +0 -0
- package/src/manage/guide/widget-voice.png +0 -0
- package/src/manage/guide.js +155 -0
- package/src/manage/guide.md +139 -0
- package/src/manage/index.html +14 -0
- package/src/manage/manage.css +782 -0
- package/src/manage/manage.js +155 -39
- package/src/manage/packs.js +347 -0
- package/src/manage/settings.js +1 -1
- package/src/manage/setup.js +83 -23
- package/packs/BRAND_ASSETS.md +0 -8
- package/packs/calendar/logo.png +0 -0
- package/packs/calendar/pack.json +0 -127
- package/packs/discord/pack.json +0 -107
- package/packs/filesystem/pack.json +0 -84
- package/packs/github/pack.json +0 -127
- package/packs/gmail/logo.png +0 -0
- package/packs/gmail/pack.json +0 -274
- package/packs/obsidian/pack.json +0 -88
- package/packs/t3-agents/logo.png +0 -0
- package/packs/t3-agents/pack.json +0 -492
- package/packs/telegram/pack.json +0 -137
- package/packs/trello/pack.json +0 -102
- package/packs/whatsapp/pack.json +0 -118
package/src/manage/manage.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
const SECTIONS = ['setup', 'connection', 'voice', 'execution', 'mcp', 'layouts', 'settings'];
|
|
1
|
+
const SECTIONS = ['setup', 'connection', 'voice', 'execution', 'decisions', 'mcp', 'phone', 'layouts', 'guide', 'settings'];
|
|
2
2
|
// Links carry the token in the fragment (never sent to the server): `#t=<token>&s=<section>`.
|
|
3
3
|
const linked = new URLSearchParams(location.hash.startsWith('#t=') ? location.hash.slice(1) : '');
|
|
4
4
|
const token = linked.get('t') || sessionStorage.getItem('rvaManageToken');
|
|
5
5
|
if (token) {
|
|
6
6
|
sessionStorage.setItem('rvaManageToken', token);
|
|
7
|
+
// A draft link (from the agent's packs__preview) opens that draft on the Phone preview.
|
|
7
8
|
if (linked.get('t'))
|
|
8
|
-
history.replaceState(null, '', `#${SECTIONS.includes(linked.get('s')) ? linked.get('s') : 'connection'}`);
|
|
9
|
+
history.replaceState(null, '', linked.get('s') === 'phone' && linked.get('d') ? `#phone/draft/${encodeURIComponent(linked.get('d'))}` : `#${SECTIONS.includes(linked.get('s')) ? linked.get('s') : 'connection'}`);
|
|
9
10
|
}
|
|
10
11
|
// Dark is the default; a light choice is remembered per browser.
|
|
11
12
|
function applyTheme(theme) {
|
|
@@ -132,15 +133,21 @@ function render() {
|
|
|
132
133
|
document
|
|
133
134
|
.querySelectorAll('nav a')
|
|
134
135
|
.forEach((a) => a.classList.toggle('active', a.hash === `#${current}`));
|
|
136
|
+
const scrollTo = location.hash.startsWith('#guide/');
|
|
135
137
|
$('#app').innerHTML = restartBanner() + setupBanner() + {
|
|
136
138
|
setup: setupView,
|
|
137
139
|
connection: connectionView,
|
|
138
140
|
voice: voiceView,
|
|
139
141
|
execution: executionView,
|
|
140
|
-
|
|
142
|
+
decisions: decisionsView,
|
|
143
|
+
mcp: () => (packPageId() ? packPageView() : libraryPageSlug() ? libraryPackView() : mcpView()),
|
|
144
|
+
phone: phoneView,
|
|
141
145
|
layouts: layoutsView,
|
|
146
|
+
guide: guideView,
|
|
142
147
|
settings: settingsView,
|
|
143
148
|
}[current]();
|
|
149
|
+
afterPacksRender();
|
|
150
|
+
if (scrollTo) scrollToGuideAnchor();
|
|
144
151
|
if (showsExecution()) void discoverModels();
|
|
145
152
|
if (current === 'setup') afterSetupRender();
|
|
146
153
|
if (
|
|
@@ -188,11 +195,25 @@ function connectionView() {
|
|
|
188
195
|
const connected = new Set(state.connectedDeviceIds || []);
|
|
189
196
|
return `<section class="section"><div><h2>Connect a phone</h2><p class="lede">Pair your phone with ${esc(state.name)}. This page manages the companion on this computer; choose among your computers in the phone app. Rename this computer under <a href="#settings">Settings</a>.</p></div><div class="card stack"><div><h3>Pairing code</h3><p class="hint">On your phone, choose Add computer → Scan pairing code. Each code works once and expires shortly.</p></div>${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.' : `Expires ${esc(new Date(pairing.expiresAt).toLocaleString())}.`}</p>` : `<p class="muted">${pairingLoading ? 'Preparing pairing code…' : 'Generate a code to connect this phone.'}</p>`}${pairingError ? `<p role="alert" class="hint error-text">Could not create a pairing code: ${esc(pairingError)} Check this computer’s reachable address in config.json, then try again.</p>` : ''}<div class="actions"><button class="primary" data-action="pair-phone" ${pairingLoading ? 'disabled' : ''}>${pairingLoading ? 'Preparing…' : pairing ? 'Generate a new code' : pairingError ? 'Try again' : 'Generate pairing code'}</button></div></div><div><h3>Paired phones</h3><p class="lede">${active.length ? 'Phones authorized to use this computer.' : 'No phone is paired yet. Scan the code above to connect your first phone.'}</p></div><div class="grid">${active.map((device) => `<div class="card"><div class="row"><div><h3>${esc(device.name)}</h3><p class="hint">${esc(device.platform)} · Paired ${esc(new Date(device.createdAt).toLocaleDateString())}</p></div><span class="badge ${connected.has(device.id) ? 'good' : ''}">${connected.has(device.id) ? 'Connected now' : 'Offline'}</span></div><p class="hint">${device.lastSeenAt ? `Last seen ${esc(new Date(device.lastSeenAt).toLocaleString())}` : 'Has not connected yet'} · Credential expires ${esc(new Date(device.expiresAt).toLocaleDateString())}</p><div class="actions"><button class="danger" data-action="revoke-phone" data-id="${esc(device.id)}">Remove phone</button></div></div>`).join('') || '<div class="card muted">Your paired phones will appear here.</div>'}</div><div class="actions"><button data-action="refresh">Refresh status</button></div></section>`;
|
|
190
197
|
}
|
|
198
|
+
/** Marks a value pinned on this computer (ADR 0014); account and default values carry no badge. */
|
|
199
|
+
function sourceBadge(section, path) {
|
|
200
|
+
return state.settingSources?.[section]?.[path] === 'override' ? '<span class="badge source" title="Kept on this computer when your account’s settings change">This computer</span>' : '';
|
|
201
|
+
}
|
|
202
|
+
/** A field label with its source badge on the same line. */
|
|
203
|
+
function sourceLabel(text, section, ...paths) {
|
|
204
|
+
const badges = paths.map((path) => sourceBadge(section, path)).join('');
|
|
205
|
+
return badges ? `<span class="label-text">${text}${badges}</span>` : text;
|
|
206
|
+
}
|
|
207
|
+
/** A synced voice choice can name a provider whose key is not on this computer; keys never come from the phone. */
|
|
208
|
+
function voiceNeedsKey(providerId) {
|
|
209
|
+
const key = state.voice.keyStates[providerId];
|
|
210
|
+
return Boolean(key && !key.isSet);
|
|
211
|
+
}
|
|
191
212
|
function voiceKeyView(providerId) {
|
|
192
213
|
const provider = state.voice.catalog[providerId];
|
|
193
214
|
if (!provider) return '';
|
|
194
215
|
const key = state.voice.keyStates[providerId];
|
|
195
|
-
return `<div class="card"><div class="row"><div><h3>${esc(provider.label)} API key</h3><p class="hint">${key.fromEnv ? 'Provided by this computer’s environment. Environment keys take priority.' : key.isSet ? 'Stored in this computer’s keychain. Enter a new key to replace it.' : 'Add
|
|
216
|
+
return `<div class="card"><div class="row"><div><h3>${esc(provider.label)} API key</h3><p class="hint">${key.fromEnv ? 'Provided by this computer’s environment. Environment keys take priority.' : key.isSet ? 'Stored in this computer’s keychain. Enter a new key to replace it.' : 'Add this provider’s key here to use it. It stays in this computer’s keychain and is never synced or asked for on the phone.'}</p></div><span class="badge ${key.isSet ? 'good' : 'warn'}">${key.isSet ? 'Configured' : 'Needs a key on this computer'}</span></div><div class="actions"><input data-voice-key="${esc(providerId)}" type="password" autocomplete="new-password" aria-label="${esc(provider.label)} API key" placeholder="Paste a key to set or replace"><button data-action="save-voice-key" data-provider="${esc(providerId)}">Save key</button>${key.isSet && !key.fromEnv ? `<button class="danger" data-action="clear-voice-key" data-provider="${esc(providerId)}">Clear key</button>` : ''}</div></div>`;
|
|
196
217
|
}
|
|
197
218
|
function voiceKeysView(voiceProvider) {
|
|
198
219
|
return state.voice.catalog[voiceProvider]
|
|
@@ -219,6 +240,10 @@ function voiceEndpointFields(provider, voice) {
|
|
|
219
240
|
}
|
|
220
241
|
/** Model Studio shows an API host (`ws-….ap-southeast-1.maas.aliyuncs.com`) or base URL, not a bare ID; both carry the ID and region. */
|
|
221
242
|
const MODEL_STUDIO_HOST = /^(?:[a-z]+:\/\/)?([a-z0-9-]+)\.(ap-southeast-1|cn-beijing)\.maas\.aliyuncs\.com/i;
|
|
243
|
+
/** The spoken command phrase fields, shared by the Voice page and setup; read back by commandPhrases(). */
|
|
244
|
+
function commandPhraseFields(commands) {
|
|
245
|
+
return `<p class="hint">Your phone listens for these during a call, on the phone itself. Muting the mic stops what the assistant hears (it still speaks reports); muting the voice stops it speaking on the phone (replies still arrive as text). Use at least two words each; separate alternatives with commas. Unmute phrases work while muted. Changes apply from the next call after you apply them.</p><label>Mute mic and voice<input id="voice-mute-phrases" maxlength="220" autocomplete="off" spellcheck="false" value="${esc(commands.mute.join(', '))}"></label><label>Mute mic only<input id="voice-mute-mic-phrases" maxlength="220" autocomplete="off" spellcheck="false" value="${esc(commands.muteMic.join(', '))}"></label><label>Mute voice only<input id="voice-mute-voice-phrases" maxlength="220" autocomplete="off" spellcheck="false" value="${esc(commands.muteVoice.join(', '))}"></label><label>Unmute everything<input id="voice-unmute-phrases" maxlength="220" autocomplete="off" spellcheck="false" value="${esc(commands.unmute.join(', '))}"></label><label>End the call<input id="voice-end-call-phrases" maxlength="220" autocomplete="off" spellcheck="false" value="${esc(commands.endCall.join(', '))}"></label>`;
|
|
246
|
+
}
|
|
222
247
|
/** Same rules as the companion's voiceCommandPhraseSchema, checked here to say what is wrong. */
|
|
223
248
|
function commandPhrases() {
|
|
224
249
|
const read = (id) => $(id).value.split(',').map((phrase) => phrase.toLowerCase().replace(/[^a-z'\s]/g, ' ').replace(/\s+/g, ' ').trim()).filter(Boolean);
|
|
@@ -243,12 +268,36 @@ function voiceUsageView(usage) {
|
|
|
243
268
|
const usd = (value) => `$${value < 0.1 ? value.toFixed(3) : value.toFixed(2)}`;
|
|
244
269
|
return `<p><strong>This month ≈ ${esc(usd(usage.estimatedUsd))}</strong> · ${esc(minutes(usage.minutes))} connected, ${esc(minutes(usage.speakingMinutes))} speaking, across ${usage.calls} ${usage.calls === 1 ? 'call' : 'calls'}</p><ul class="hint">${usage.models.map((row) => `<li>${esc(row.providerLabel)} · ${esc(row.model)}: ${esc(minutes(row.minutes))} connected, ${esc(minutes(row.speakingMinutes))} speaking${row.estimatedUsd === null ? ' (no list price)' : ` ≈ ${esc(usd(row.estimatedUsd))} (billed ${row.billing === 'connected' ? 'while connected' : 'for speech'})`}</li>`).join('')}</ul><p class="hint">An estimate from list prices, not a bill. Speaking time counts your voiced audio and the assistant's replies. Check your provider's billing page for actual charges.</p>`;
|
|
245
270
|
}
|
|
271
|
+
/** Its own page while it is in beta: the fast-decision model and its key. */
|
|
272
|
+
function decisionsView() {
|
|
273
|
+
return `<section class="section"><div><h2>Fast decisions <span class="tag-beta">Beta</span></h2><p class="lede">Let simple requests skip the execution agent: a fast decision model answers or acts on this computer in about a second. Anything it is unsure about goes to the execution agent as usual.</p></div>${jevView()}</section>`;
|
|
274
|
+
}
|
|
275
|
+
/** Fast decisions (Jev): the user's own TypeSafe key, or a local server with the same API. */
|
|
276
|
+
function jevView() {
|
|
277
|
+
const jev = state.jev;
|
|
278
|
+
if (!jev) return '';
|
|
279
|
+
const { status, config, key } = jev;
|
|
280
|
+
const running = status.state === 'ready' ? `On · ${status.kind === 'local' ? 'local server' : 'TypeSafe'}` : status.state === 'needs_key' ? 'Off · needs a key' : 'Off';
|
|
281
|
+
const keyHint = key.fromEnv ? 'Provided by this computer’s environment (TYPESAFE_API_KEY).' : key.isSet ? 'Stored in this computer’s keychain.' : 'Use your own key from typesafe.ai. It stays in this computer’s keychain and is only sent to TypeSafe.';
|
|
282
|
+
return `<div class="card stack"><div class="row"><div><h3>Decision model (Jev)</h3><p class="hint">A System One model decides in about a tenth of a second whether the computer can do a request itself: open or quit apps, volume, music, dark mode, lock the screen, reminders, what agents are doing, one integration’s data, and home tile layout. Anything else, or anything it is unsure about, goes to the worker as usual. Without a key or local server, every request goes to the worker.</p></div><span class="badge ${status.state === 'ready' ? 'good' : 'warn'}">${esc(running)}</span></div>
|
|
283
|
+
<div class="check"><input id="jev-on" type="checkbox" ${config.mode === 'auto' ? 'checked' : ''}><label for="jev-on">Use fast decisions when a key or local server is set</label></div>
|
|
284
|
+
<label>Local Jev-compatible server (optional)<input id="jev-url" type="url" placeholder="Empty: TypeSafe’s hosted API" value="${esc(config.url ?? '')}"></label>
|
|
285
|
+
<p class="hint">For a model running on this computer or your network that serves the same <span class="code">POST /v1/systemone</span> API. No key is sent to it.</p>
|
|
286
|
+
<label>Model<input id="jev-model" value="${esc(config.model)}"></label>
|
|
287
|
+
<label>Act only at this confidence or higher (0.5–0.99)<input id="jev-confidence" type="number" min="0.5" max="0.99" step="0.01" value="${config.minConfidence}"></label>
|
|
288
|
+
<label>Give up after (ms)<input id="jev-timeout" type="number" min="200" max="10000" step="100" value="${config.timeoutMs}"></label>
|
|
289
|
+
${jev.computerUse ? `<div class="stack"><h4>Computer use</h4><p class="hint">${jev.computerUse.available ? 'On: for click, scroll, and type requests that no integration covers, Jev moves the cursor, clicks, scrolls, and types on this computer. The app running the companion needs Accessibility access in System Settings.' : 'Off: turn on fast decisions to let Jev drive the screen.'}</p></div>` : ''}
|
|
290
|
+
<div class="actions"><button class="primary" data-action="save-jev">Save fast decisions</button><button data-action="test-jev" ${status.state === 'ready' ? '' : 'disabled'}>Test</button></div>
|
|
291
|
+
<h4>TypeSafe API key</h4><p class="hint">${keyHint}</p>
|
|
292
|
+
<div class="actions"><input id="jev-key" type="password" autocomplete="new-password" aria-label="TypeSafe API key" placeholder="Paste a key to set or replace"><button data-action="save-jev-key">Save key</button>${key.isSet && !key.fromEnv ? '<button class="danger" data-action="clear-jev-key">Clear key</button>' : ''}</div>
|
|
293
|
+
<p class="hint">Changes apply after a restart; you will be prompted.</p></div>`;
|
|
294
|
+
}
|
|
246
295
|
function voiceView() {
|
|
247
296
|
const { voice, catalog } = state.voice;
|
|
248
297
|
const selected = catalog[voice.provider];
|
|
249
298
|
const voices = voiceOptionsFor(selected, voice.model);
|
|
250
299
|
return `<section class="section"><div><h2>Voice agent</h2><p class="lede">The voice agent talks with you in calls and answers typed messages in the chat, as one assistant. It hands all computer work to the execution agent below and cannot run local tools itself.</p></div>
|
|
251
|
-
<div class="card stack"><h3>Voice agent model</h3><p class="hint">Used for calls and typed chat. Qwen and OpenAI Realtime reply to typed messages in text; the others speak, and their transcript is the reply (billed as audio). GPT-Live cannot read typed messages, so they go straight to the execution agent. With voice off, typed messages get simple local replies.</p
|
|
300
|
+
<div class="card stack"><h3>Voice agent model</h3><p class="hint">Used for calls and typed chat. Qwen and OpenAI Realtime reply to typed messages in text; the others speak, and their transcript is the reply (billed as audio). GPT-Live cannot read typed messages, so they go straight to the execution agent. With voice off, typed messages get simple local replies.</p>${voiceNeedsKey(voice.provider) ? `<p class="hint error-text" role="status">${esc(selected.label)} needs a key on this computer. Add it under the API key below.</p>` : ''}<label>${sourceLabel('Voice provider', 'voice', 'provider')}<select id="voice-provider"><option value="none" ${voice.provider === 'none' ? 'selected' : ''}>Off</option>${Object.values(
|
|
252
301
|
catalog
|
|
253
302
|
)
|
|
254
303
|
.map(
|
|
@@ -257,7 +306,7 @@ function voiceView() {
|
|
|
257
306
|
)
|
|
258
307
|
.join(
|
|
259
308
|
''
|
|
260
|
-
)}<option value="echo" ${voice.provider === 'echo' ? 'selected' : ''}>Echo (audio test)</option></select></label><label
|
|
309
|
+
)}<option value="echo" ${voice.provider === 'echo' ? 'selected' : ''}>Echo (audio test)</option></select></label><label>${sourceLabel('Voice model', 'voice', 'model')}<select id="voice-model" ${selected ? '' : 'disabled'}>${voiceModelOptions(selected, voice.model)}</select></label><p id="voice-untested" class="hint" ${selected?.untested ? '' : 'hidden'}>Untested: this provider has not been run against the real service yet, so expect rough edges.</p><p class="hint">Prices are estimates from list prices. Most models charge only for speech, so a quiet or muted call costs little; models priced "while connected" charge for every minute the call is open, muted or not.</p><div id="voice-endpoint" class="stack">${voiceEndpointFields(selected, voice)}</div><label>${sourceLabel('Voice name', 'voice', 'voiceName')}<select id="voice-name">${voiceNameOptions(voices, voice.voiceName)}</select></label><div class="voice-preview"><button type="button" data-action="preview-voice" ${selected ? '' : 'disabled'}>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">${selected ? `Sample phrase: “Hi, I'm your assistant. What would you like to work on today?”` : 'Choose a live voice provider to hear a sample.'}</p><label>${sourceLabel('Maximum call length (minutes)', 'voice', 'maxSessionMinutes')}<input id="voice-minutes" type="number" min="1" max="60" value="${voice.maxSessionMinutes}"></label><div class="stack"><h4>${sourceLabel('Spoken mute and unmute', 'voice', 'commands')}</h4>${commandPhraseFields(voice.commands)}<label>${sourceLabel('Auto-mute the mic after this many quiet seconds', 'voice', 'autoMute', 'autoMute.seconds')}<input id="voice-auto-mute" type="number" min="5" max="300" value="${voice.autoMute.seconds}"></label><div class="check"><input id="voice-auto-mute-on" type="checkbox" ${voice.autoMute.enabled ? 'checked' : ''}><label for="voice-auto-mute-on">Start calls with auto-mute on</label></div><p class="hint">Quiet means neither you nor the assistant is speaking. The call screen has an auto-mute toggle to change it during a call. Ending the call stops voice only; messages and task updates keep arriving in the app. Auto-mute works when the phone can listen for your unmute phrase.</p></div><div id="voice-search-row" class="check" ${voiceSearchSupported(selected) ? '' : 'hidden'}><input id="voice-search" type="checkbox" ${voice.webSearch ? 'checked' : ''}><label for="voice-search">${sourceLabel('Allow web search in voice calls', 'voice', 'webSearch')}</label></div><p id="voice-search-note" class="hint" ${selected && !voiceSearchSupported(selected) ? '' : 'hidden'}>This provider's built-in web search is not available in this companion.</p><div class="actions"><button class="primary" data-action="save-voice">Save voice settings</button></div><p class="hint">Provider and model changes apply after a restart; you will be prompted.</p></div><div class="card stack"><h3>Voice usage</h3>${voiceUsageView(state.voice.usage)}</div><div id="voice-key-panel" class="stack">${voiceKeysView(voice.provider)}</div></section>`;
|
|
261
310
|
}
|
|
262
311
|
function previewStatus(message, error = false) {
|
|
263
312
|
const status = $('#voice-preview-status');
|
|
@@ -474,14 +523,16 @@ function updateModelChoices(selectedModel = '') {
|
|
|
474
523
|
}
|
|
475
524
|
const interval = (seconds) => (seconds < 60 ? `${seconds} sec` : seconds >= 3600 && seconds % 3600 === 0 ? `${seconds / 3600} hr` : `${Math.round(seconds / 60)} min`);
|
|
476
525
|
|
|
477
|
-
/** Which widget updates the voice agent speaks during calls; each widget can override it. */
|
|
478
|
-
function announceView() {
|
|
479
|
-
const mode = state.packs.announceMode || 'agents';
|
|
480
|
-
const option = (value, label) => `<option value="${value}" ${mode === value ? 'selected' : ''}>${label}</option>`;
|
|
481
|
-
return `<div class="card stack"><div><h3>Spoken updates during calls</h3><p class="hint">In a call or work mode, the voice agent tells you at the next pause when a widget reports something: an agent thread finished, failed, or needs you; new mail; and so on. Each widget below can override this.</p></div><label>Speak updates from<select id="announce-mode">${option('agents', 'Agents only (recommended)')}${option('all', 'Every widget')}${option('none', 'No widgets')}</select></label></div>`;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
526
|
/** `embedded` drops the page heading when setup shows these controls under its own. */
|
|
527
|
+
/** The user's default agents: the agents app with their main work agents, separate from the execution agent. */
|
|
528
|
+
function defaultAgentsView() {
|
|
529
|
+
const apps = (state.packs?.packs || []).filter(pack => pack.agentsApp && pack.enabled);
|
|
530
|
+
const chosen = state.packs?.defaultAgents || '';
|
|
531
|
+
const hint = apps.length
|
|
532
|
+
? '“What agents are running?” checks this app’s threads. It is not the execution agent above, which takes requests from voice and does the work.'
|
|
533
|
+
: 'Install and turn on an agents app (for example T3 Agents, Hermes, or OpenClaw) under MCP & widgets to choose one.';
|
|
534
|
+
return `<div class="card stack"><h3>Default agents</h3><p class="hint">${hint}</p>${apps.length ? `<label>Your main work agents<select id="default-agents"><option value="" ${chosen ? '' : 'selected'}>${apps.length === 1 ? 'Automatic (the only agents app)' : 'Not chosen: the execution agent checks'}</option>${apps.map(app => `<option value="${esc(app.id)}" ${app.id === chosen ? 'selected' : ''}>${esc(app.name)}</option>`).join('')}</select></label><div class="actions"><button data-action="save-default-agents">Save default agents</button></div>` : ''}</div>`;
|
|
535
|
+
}
|
|
485
536
|
function executionView({ embedded = false } = {}) {
|
|
486
537
|
const providers = state.providers;
|
|
487
538
|
const executors = state.executors || [];
|
|
@@ -505,9 +556,9 @@ function executionView({ embedded = false } = {}) {
|
|
|
505
556
|
const readiness = instance => !instance.enabled ? 'Disabled' : !instance.status ? 'Not checked' : !instance.status.installed ? 'Not installed' : instance.status.auth === 'unauthenticated' ? 'Sign-in needed' : instance.status.auth === 'authenticated' ? 'Ready' : 'Installed · sign-in not verified';
|
|
506
557
|
const profiles = providers?.profiles.filter(profile => instances.some(instance => [instance.instanceId, instance.displayName, instance.driver].includes(profile.provider))) || [];
|
|
507
558
|
const promptFor = executor => executor.connector === 'direct' ? state.profilePrompts?.[executor.id.slice('direct:'.length)] : undefined;
|
|
508
|
-
const agentRows = group.agents.map(executor => `<div class="agent-choice ${executor.isDefault ? 'chosen' : ''}"><div><strong>${esc(executor.label)}</strong><p class="hint">${esc(executor.detail || 'Uses the provider’s default model')}${promptFor(executor) ? ' · Custom system prompt' : ''}</p></div><div class="agent-actions">${executor.connector === 'direct' ? `<button data-action="edit-profile" data-id="${esc(executor.id.slice('direct:'.length))}" aria-label="Edit ${esc(executor.label)} thinking level and system prompt">Edit</button>` : ''}${executor.isDefault ? '<span class="badge good">Default</span>' : `<button data-action="set-default-executor" data-id="${esc(executor.id)}" aria-label="Use ${esc(executor.label)} by default">Use by default</button>`}</div></div>`).join('');
|
|
559
|
+
const agentRows = group.agents.map(executor => `<div class="agent-choice ${executor.isDefault ? 'chosen' : ''}"><div><strong>${esc(executor.label)}</strong><p class="hint">${esc(executor.detail || 'Uses the provider’s default model')}${promptFor(executor) ? ' · Custom system prompt' : ''}</p></div><div class="agent-actions">${executor.connector === 'direct' ? `<button data-action="edit-profile" data-id="${esc(executor.id.slice('direct:'.length))}" aria-label="Edit ${esc(executor.label)} model, thinking level, permissions, and system prompt">Edit</button>` : ''}${executor.isDefault ? '<span class="badge good">Default</span>' : `<button data-action="set-default-executor" data-id="${esc(executor.id)}" aria-label="Use ${esc(executor.label)} by default">Use by default</button>`}</div></div>`).join('');
|
|
509
560
|
return `<section class="section execution">${embedded ? '<p class="lede">Choose the agent on this computer that does the work you ask for. Voice hands requests to it.</p>' : '<div><h2>Who does the work?</h2><p class="lede">Choose the agent that handles tasks you send from your phone.</p></div>'}
|
|
510
|
-
<div class="default-agent"><div><small>YOUR DEFAULT AGENT</small><h3>${esc(selected?.label || 'Choose an agent below')}</h3><p>${esc(selected?.detail || 'Add an agent, then choose it as your default.')}</p></div><span class="badge">${selected ? 'Used unless you name another agent' : 'Not set up yet'}</span></div>
|
|
561
|
+
<div class="default-agent"><div><small>YOUR DEFAULT AGENT</small><h3>${sourceLabel(esc(selected?.label || 'Choose an agent below'), 'agents', 'defaultProfileId')}</h3><p>${esc(selected?.detail || 'Add an agent, then choose it as your default.')}</p></div><span class="badge">${selected ? 'Used unless you name another agent' : 'Not set up yet'}</span></div>
|
|
511
562
|
<div class="row"><h3>Choose a provider</h3><button data-action="refresh-executors">Check availability</button></div>
|
|
512
563
|
<div class="provider-tabs" role="tablist" aria-label="Execution providers">${groups.map(candidate => `<button role="tab" aria-selected="${candidate.id === group.id}" class="provider-tab ${candidate.id === group.id ? 'selected' : ''}" data-action="select-execution-provider" data-id="${esc(candidate.id)}"><strong>${esc(candidate.label)}</strong><span>${candidate.agents.length ? `${candidate.agents.length} agent${candidate.agents.length === 1 ? '' : 's'}` : 'Set up'}</span></button>`).join('')}</div>
|
|
513
564
|
<div class="card stack" role="tabpanel" aria-label="${esc(group.label)} agents"><div class="row"><div><h3>${esc(group.label)}</h3><p class="hint">Agents and models running on this computer.</p></div></div>
|
|
@@ -520,21 +571,34 @@ function executionView({ embedded = false } = {}) {
|
|
|
520
571
|
<label>Model<select id="profile-model">${modelChoices(selectedInstance)}</select></label><div class="actions"><button type="button" data-action="refresh-models" data-driver="${esc(group.id)}">Refresh models</button></div><p id="profile-model-status" class="hint" role="status" aria-live="polite">${modelMessage(selectedInstance)}</p>
|
|
521
572
|
<label>Thinking level<select id="profile-effort" ${effortLevelsFor(selectedInstance, '').length ? '' : 'disabled'}>${effortChoices(effortLevelsFor(selectedInstance, ''), effortLevelsFor(selectedInstance, '').includes(DEFAULT_EFFORT) ? DEFAULT_EFFORT : '')}</select></label><p id="profile-effort-status" class="hint">${effortHint(effortLevelsFor(selectedInstance, ''))}</p>
|
|
522
573
|
<label><span>System prompt <span class="optional">· Optional</span></span><textarea id="profile-prompt" maxlength="32000" placeholder="For example: Keep changes small, explain your plan before editing, and run the tests you touch."></textarea></label><p class="hint">Added to ${esc(group.label)}’s own instructions for every task this agent runs.</p>
|
|
523
|
-
<details><summary>Permissions</summary><label>When the agent changes files or runs commands<select id="profile-mode"
|
|
574
|
+
<details><summary>Permissions</summary><label>When the agent changes files or runs commands<select id="profile-mode">${runtimeModeChoices('approval-required')}</select></label></details>
|
|
524
575
|
<button class="primary" data-action="quick-add-agent" data-driver="${esc(group.id)}">Add agent and use by default</button><p class="hint">Uses this provider’s local installation and sign-in. Adding an agent does not install the provider or grant access to additional folders.</p>
|
|
525
576
|
</div></details>
|
|
526
577
|
${instances.length ? `<details><summary>Manage ${esc(group.label)} installation and agents</summary><div class="stack">${instances.map(instance => `<div class="row"><strong>${esc(instance.displayName)}</strong><div class="actions"><button data-action="edit-provider" data-id="${esc(instance.instanceId)}">Settings</button><button data-action="toggle-provider" data-id="${esc(instance.instanceId)}" data-driver="${esc(instance.driver)}" data-enabled="${instance.enabled}">${instance.enabled ? 'Disable' : 'Enable'}</button><button class="danger" data-action="remove-provider" data-id="${esc(instance.instanceId)}">Remove installation</button></div></div>`).join('')}${profiles.map(profile => `<div class="row"><span>${esc(profile.name)}${profile.available ? '' : ' · unavailable'}</span><button class="danger" data-action="remove-profile" data-id="${esc(profile.id)}">Remove agent</button></div>`).join('')}</div></details>` : ''}`}
|
|
527
578
|
</div><div id="profile-editor"></div><div id="provider-editor"></div>
|
|
528
579
|
${providers ? `<div class="card stack"><h3>Where local agents work</h3><p class="hint">${providers.folderRoots?.length ? `Any folder inside ${providers.folderRoots.map(root => `<span class="code">${esc(root)}</span>`).join(', ')}. Ask for a project by name or path; the companion lists the git projects it finds there.` : 'Only the named folders below.'} Set <span class="code">providers.folderRoots</span> in config.json to change this.</p>${providers.workspaces.length ? `<details><summary>Named folders · ${providers.workspaces.length}</summary>${providers.workspaces.map(workspace => `<p><strong>${esc(workspace.name)}</strong><br><span class="code">${esc(workspace.path)}</span></p>`).join('')}</details>` : ''}</div>` : ''}
|
|
580
|
+
${defaultAgentsView()}
|
|
529
581
|
</section>`;
|
|
530
582
|
}
|
|
583
|
+
const RUNTIME_MODES = [['approval-required', 'Ask me first'], ['read-only', 'Read only'], ['auto-accept-edits', 'Accept edits automatically'], ['auto', 'Automatic'], ['full-access', 'Full access']];
|
|
584
|
+
function runtimeModeChoices(selected) {
|
|
585
|
+
return RUNTIME_MODES.map(([value, label]) => `<option value="${value}" ${value === selected ? 'selected' : ''}>${label}</option>`).join('');
|
|
586
|
+
}
|
|
587
|
+
/** The installation a profile runs on, by the id or display name it names. */
|
|
588
|
+
const profileInstance = profile => state.providers.instances.find(candidate => [candidate.instanceId, candidate.displayName].includes(profile.provider));
|
|
589
|
+
/** Thinking levels for the edit form's model; a saved level stays visible even before the model list loads. */
|
|
590
|
+
function editEffortLevels(instance, model, saved) {
|
|
591
|
+
const levels = effortLevelsFor(instance, model);
|
|
592
|
+
return saved && !levels.includes(saved) && !instance?.models?.length ? [...levels, saved] : levels;
|
|
593
|
+
}
|
|
531
594
|
function profileEditor(profile) {
|
|
532
595
|
const current = state.profilePrompts?.[profile.id] || '';
|
|
533
|
-
const instance =
|
|
534
|
-
const
|
|
535
|
-
//
|
|
536
|
-
const
|
|
537
|
-
|
|
596
|
+
const instance = profileInstance(profile);
|
|
597
|
+
const shown = editEffortLevels(instance, profile.model, profile.reasoningEffort);
|
|
598
|
+
// A saved model the installation no longer lists (or has not listed yet) stays selectable.
|
|
599
|
+
const listed = (instance?.models || []).some(model => model.id === profile.model);
|
|
600
|
+
const models = `${modelChoices(instance, profile.model || '')}${profile.model && !listed ? `<option value="${esc(profile.model)}" selected>${esc(profile.model)}</option>` : ''}`;
|
|
601
|
+
return `<div class="card stack"><div class="row"><div><h3>${esc(profile.name)}</h3><p class="hint">${esc(profile.provider)}${profile.model ? ` · ${esc(profile.model)}` : ''}. Changes apply to tasks started after you save.</p></div><button data-action="close-profile">Close</button></div><label>Model<select id="profile-model-edit" data-id="${esc(profile.id)}">${models}</select></label><p class="hint">${modelMessage(instance)}</p><label>Thinking level<select id="profile-effort-edit" ${shown.length ? '' : 'disabled'}>${effortChoices(shown, profile.reasoningEffort || '')}</select></label><p id="profile-effort-edit-status" class="hint">${effortHint(shown)}</p><label>When the agent changes files or runs commands<select id="profile-mode-edit">${runtimeModeChoices(profile.runtimeMode)}</select></label><p class="hint">For threads you start with this agent. As the execution agent (requests from your phone and its jobs) it always has full access, in the companion's own folder.</p><label>System prompt<textarea id="profile-prompt-edit" class="prompt-text" maxlength="32000" placeholder="Describe how this agent should work. Added to ${esc(profile.provider)}’s own instructions at the start of each task.">${esc(current)}</textarea></label><div class="actions"><button class="primary" data-action="save-profile" data-id="${esc(profile.id)}">Save agent</button>${current ? `<button class="danger" data-action="clear-profile-prompt" data-id="${esc(profile.id)}">Remove prompt</button>` : ''}</div></div>`;
|
|
538
602
|
}
|
|
539
603
|
function providerEditor(p) {
|
|
540
604
|
return `<div class="card stack"><div class="row"><h3>${esc(p.displayName)} settings</h3><button data-action="close-provider">Close</button></div><label>Display name<input id="provider-name" value="${esc(p.displayName)}"></label>${p.settingsForm.map((field) => `<label>${esc(field.label)}${field.control === 'switch' ? `<input data-setting="${esc(field.id)}" type="checkbox" ${p.settings[field.id] ? 'checked' : ''}>` : field.control === 'select' ? `<select data-setting="${esc(field.id)}">${(field.options || []).map((o) => `<option value="${esc(o.id)}" ${p.settings[field.id] === o.id ? 'selected' : ''}>${esc(o.label)}</option>`).join('')}</select>` : `<input data-setting="${esc(field.id)}" type="${field.control === 'password' ? 'password' : 'text'}" value="${field.control === 'password' ? '' : esc(p.settings[field.id] || '')}" placeholder="${field.control === 'password' && p.secretsSet.includes(field.id) ? 'Already set — leave blank to keep' : esc(field.placeholder || '')}">`}</label>`).join('')}<details><summary>Environment variables</summary><p class="hint">One per line: NAME=value. Prefix a name with ! for a secret. Existing secret values are kept when left blank.</p><textarea id="provider-env">${esc(p.environment.map((v) => `${v.sensitive ? '!' : ''}${v.name}=${v.value || ''}`).join('\n'))}</textarea></details><div class="actions"><button class="primary" data-action="save-provider" data-id="${esc(p.instanceId)}">Save provider</button></div></div>`;
|
|
@@ -555,17 +619,15 @@ function packStatusDetail(pack) {
|
|
|
555
619
|
: detail;
|
|
556
620
|
}
|
|
557
621
|
function catalogView() {
|
|
558
|
-
|
|
622
|
+
const intro = `<div><h3>Widget library</h3><p class="hint">Widget packs from the Sidebud widget library. Install one and this computer's agent sets it up: it finds settings and keys already on this computer and reports what needs you. Every new install starts disabled until setup enables it. Your agent can also build new packs, or change any installed one, when you ask.</p>${state.libraryError ? `<p class="hint">The library could not be reached: ${esc(state.libraryError)}</p>` : ''}</div>`;
|
|
623
|
+
return `${intro}<div class="grid">${state.integrationCatalog.map(entry => `<article class="card stack"><div class="row"><h3><a href="#mcp/library/${encodeURIComponent(entry.id)}">${esc(entry.name)}</a></h3><span class="badge ${entry.publisher.source === 'community' && !entry.publisher.official ? 'warn' : ''}">${entry.installed ? 'Installed' : entry.detected ? 'App found' : entry.publisher.official ? `Official · ${esc(entry.publisher.official.domain)}` : entry.publisher.source === 'community' ? 'Community' : esc(entry.publisher.name)}</span></div><p>${esc(entry.description)}</p><p class="hint">${esc(entry.account)} · ${esc(entry.connectsTo)}</p>${entry.tools.length ? `<details><summary>Access</summary><p class="hint">${entry.tools.map(tool => `${code(tool.name)} (${esc(tool.access)})`).join(', ')}</p></details>` : ''}<a class="hint" href="#mcp/library/${encodeURIComponent(entry.id)}">What it can do and how it looks →</a>${entry.publisher.source === 'community' && !entry.installed ? `<a class="button" href="#mcp/library/${encodeURIComponent(entry.id)}">Review and install</a>` : entry.updateAvailable ? `<button data-action="install-library-pack" data-id="${esc(entry.id)}" data-replace="true" aria-label="Update ${esc(entry.name)} from version ${esc(entry.installedVersion)} to ${esc(entry.version)}">Update to ${esc(entry.version)}</button>` : `<button data-action="install-library-pack" data-id="${esc(entry.id)}" ${entry.installed ? 'disabled' : ''} ${entry.needsReinstall ? `title="The installed copy no longer loads. Reinstalling keeps its settings and keys."` : ''}>${entry.installed ? `Installed · ${esc(entry.installedVersion)}` : entry.needsReinstall ? `Reinstall ${esc(entry.name)}` : `Install ${esc(entry.name)}`}</button>`}</article>`).join('')}</div>`;
|
|
559
624
|
}
|
|
560
625
|
function mcpView() {
|
|
561
|
-
return `<section class="section"><div><h2>Integrations & widgets</h2><p class="lede">Connect tools to Sidebud and choose which ones appear on your phone. Credentials and connections stay on this computer
|
|
626
|
+
return `<section class="section"><div><h2>Integrations & widgets</h2><p class="lede">Connect tools to Sidebud and choose which ones appear on your phone. Credentials and connections stay on this computer. New here? Start with the <a href="#guide">Guide</a>.</p></div><h3>Installed integrations</h3><div class="grid">${state.packs.packs.map(p => `<div class="card"><div class="row"><h3><a href="#mcp/${encodeURIComponent(p.id)}">${esc(p.name)}</a></h3><span class="badge ${{ ok: 'good', needs_setup: 'warn', error: 'bad' }[p.status.state] || ''}">${esc({ ok: 'Enabled', needs_setup: 'Needs setup', error: 'Error', disabled: 'Disabled' }[p.status.state] || p.status.state)}</span></div><p class="hint">${esc(p.description)}</p><p class="hint">${esc(packStatusDetail(p))}</p>${state.packAuth[p.id] ? `<p class="hint">Account: ${esc(accountLabel(state.packAuth[p.id]))}</p>` : ''}${state.connectionErrors[p.id] ? `<p class="hint">Connection failed: ${esc(state.connectionErrors[p.id])}</p>` : ''}${p.refresh ? `<label class="hint">Refresh every <select data-refresh-pack="${esc(p.id)}" aria-label="Refresh ${esc(p.name)} every">${[...new Set([...p.refresh.options, p.refresh.defaultSeconds, p.refresh.seconds])].sort((a, b) => a - b).map((seconds) => `<option value="${seconds}" ${seconds === p.refresh.seconds ? 'selected' : ''}>${esc(interval(seconds))}${seconds === p.refresh.defaultSeconds ? ' (default)' : ''}</option>`).join('')}</select></label>` : ''}${p.announce ? `<p class="hint">Spoken updates: ${p.announce.on ? 'on' : 'off'}${p.announce.custom ? '' : ' (default)'} · <button class="link" data-action="toggle-announce" data-id="${esc(p.id)}" data-on="${p.announce.on}">${p.announce.on ? 'Turn off' : 'Turn on'}</button>${p.announce.custom ? ` · <button class="link" data-action="default-announce" data-id="${esc(p.id)}">Use default</button>` : ''}</p>` : ''}<div class="actions pack-actions"><button class="primary" data-action="setup-pack" data-id="${esc(p.id)}">${p.status.state === 'ok' ? 'Check setup with my agent' : 'Set up with my agent'}</button><button data-action="share-pack" disabled title="Publishing to your Sidebud account arrives with accounts">Share</button><details class="menu"><summary aria-label="More actions for ${esc(p.name)}">…</summary><div class="menu-list" role="menu"><button role="menuitem" data-action="toggle-pack" data-id="${esc(p.id)}" data-enabled="${p.enabled}">${p.enabled ? 'Disable' : 'Enable'}</button><a role="menuitem" class="button" href="#mcp/${encodeURIComponent(p.id)}">Settings & permissions</a><button role="menuitem" data-action="check-pack" data-id="${esc(p.id)}" ${p.enabled ? '' : 'disabled'}>Check connection</button><button role="menuitem" data-action="customize-pack" data-id="${esc(p.id)}">Customize a copy</button><button role="menuitem" data-action="export-pack" data-id="${esc(p.id)}">Export</button><button role="menuitem" data-action="remove-pack" data-id="${esc(p.id)}">Remove</button></div></details></div></div>`).join('') || '<div class="card muted">No integrations installed. Choose one from the widget library below, or ask your agent to build one.</div>'}</div><div id="pack-editor"></div>${catalogView()}<div class="card"><h3>Import a widget pack</h3><p class="hint">Choose a pack file exported from Sidebud or downloaded from the widget library. It installs disabled, then this computer's agent sets it up. Pack files hold the manifest, logo, skill, and any server files, never settings, keys, or account tokens.</p><div class="actions"><input id="pack-import" type="file" accept=".json,application/json" aria-label="Widget pack export file"><button data-action="import-pack">Import</button></div></div><div class="card"><h3>Install your own widget pack</h3><p class="hint">Provide the absolute folder containing pack.json. Existing installations are never overwritten.</p><div class="actions"><input id="pack-folder" aria-label="Widget pack folder" placeholder="/path/to/widget-pack"><button data-action="install-pack">Validate & install</button></div></div><div class="card"><h3>Build or change a pack</h3><p class="hint">Ask your agent: "make a widget that shows …" or "change my Gmail widget to …". It follows the widget-packs skill, builds or edits the pack, and asks before turning it on. See how it will look under <a href="#phone">Phone preview</a>, and how it all works in the <a href="#guide">Guide</a>.</p><p class="hint">Community submissions and reviewed release distribution are planned. Local packs can be installed now.</p></div></section>`;
|
|
562
627
|
}
|
|
563
628
|
function customizePackEditor(p) {
|
|
564
629
|
return `<div class="card stack"><h3>Customize ${esc(p.name)}</h3><p class="hint">Creates a disabled copy with its own settings and credentials. The original is preserved.</p><label>New integration ID<input id="copy-pack-id" placeholder="dev.yourname.notes"></label><label>New display name<input id="copy-pack-name" placeholder="My notes"></label><div class="actions"><button data-action="save-pack-copy" data-id="${esc(p.id)}">Create copy</button><button data-action="close-pack">Cancel</button></div></div>`;
|
|
565
630
|
}
|
|
566
|
-
function packEditor(p) {
|
|
567
|
-
return `<div class="card stack"><div class="row"><div><h3>${esc(p.name)}</h3><p class="hint">${esc(p.author)} · ${esc(p.license)} · ${esc(p.version)}</p></div><button data-action="close-pack">Close</button></div><div><strong>Declared tools</strong><p class="hint">${p.permissions.tools.map((t) => `${code(t.name)} <span class="pill">${esc(t.access)}</span>`).join(' ') || 'None'}</p><p class="hint">Network: ${esc(p.permissions.network.join(', ') || 'none declared')}</p></div>${p.settings.map((setting) => `<label>${esc(setting.label)}${setting.type === 'boolean' ? `<input data-pack-setting="${esc(setting.id)}" type="checkbox" ${setting.value ? 'checked' : ''}>` : setting.type === 'select' ? `<select data-pack-setting="${esc(setting.id)}">${setting.options.map((o) => `<option value="${esc(o.id)}" ${setting.value === o.id ? 'selected' : ''}>${esc(o.label)}</option>`).join('')}</select>` : `<input data-pack-setting="${esc(setting.id)}" value="${esc(setting.value || '')}">`}</label>${setting.description ? `<p class="hint">${esc(setting.description)}</p>` : ''}`).join('')}${p.secrets.map((s) => `<label>${esc(s.name)} ${s.isSet ? '(set)' : ''}<input data-pack-secret="${esc(s.name)}" type="password" autocomplete="new-password" placeholder="${s.isSet ? 'Leave blank to keep' : 'Set on this computer'}"></label><p class="hint">${esc(s.description)}</p>${s.isSet ? `<button class="danger" data-action="clear-pack-secret" data-id="${esc(p.id)}" data-secret="${esc(s.name)}">Clear ${esc(s.name)}</button>` : ''}`).join('')}<div class="actions"><button class="primary" data-action="save-pack" data-id="${esc(p.id)}">Save settings</button>${state.packAuth[p.id] ? `<button data-action="connect-pack" data-id="${esc(p.id)}" ${state.connecting.includes(p.id) ? 'disabled' : ''}>${state.connecting.includes(p.id) ? 'Connecting…' : 'Connect account'}</button><button class="danger" data-action="disconnect-pack" data-id="${esc(p.id)}">Disconnect (local)</button>` : ''}</div><p class="hint">Account: ${esc(accountLabel(state.packAuth[p.id]))}. ${state.packAuth[p.id] ? "OAuth consent opens in this computer's browser. Disconnect removes local credentials and disables this pack; it does not revoke the provider grant." : 'Configure any required credentials above. Disable blocks new calls; clear saved credentials here and revoke them at the provider when removing access.'}</p></div>`;
|
|
568
|
-
}
|
|
569
631
|
function layoutsView() {
|
|
570
632
|
const active = state.devices.filter((d) => !d.revokedAt);
|
|
571
633
|
return `<section class="section"><div><h2>Phone layouts</h2><p class="lede">Review each paired phone's tile order, visibility, and size. Changes sync to that phone through the companion.</p></div>${
|
|
@@ -599,8 +661,8 @@ document.addEventListener('change', (event) => {
|
|
|
599
661
|
void api('pack', { packId: pack.id, refreshSeconds: seconds === pack.refresh.defaultSeconds ? null : seconds }).then(() => { notice('Refresh interval saved.'); return reload(); }, (error) => notice(error.message || 'Could not save.'));
|
|
600
662
|
return;
|
|
601
663
|
}
|
|
602
|
-
if (event.target.
|
|
603
|
-
void api('
|
|
664
|
+
if (event.target.dataset.announcePack) {
|
|
665
|
+
void api('pack', { packId: event.target.dataset.announcePack, announce: event.target.value === 'on' }).then(() => { notice('Spoken updates saved.'); return reload(); }, (error) => notice(error.message || 'Could not save.'));
|
|
604
666
|
return;
|
|
605
667
|
}
|
|
606
668
|
if (event.target.id === 'profile-effort') {
|
|
@@ -611,6 +673,16 @@ document.addEventListener('change', (event) => {
|
|
|
611
673
|
updateEffortChoices();
|
|
612
674
|
return;
|
|
613
675
|
}
|
|
676
|
+
if (event.target.id === 'profile-model-edit') {
|
|
677
|
+
// Keep the chosen thinking level when the new model offers it.
|
|
678
|
+
const profile = state.providers.profiles.find(candidate => candidate.id === event.target.dataset.id);
|
|
679
|
+
const effort = $('#profile-effort-edit');
|
|
680
|
+
const levels = editEffortLevels(profileInstance(profile), event.target.value, null);
|
|
681
|
+
effort.innerHTML = effortChoices(levels, levels.includes(effort.value) ? effort.value : '');
|
|
682
|
+
effort.disabled = !levels.length;
|
|
683
|
+
$('#profile-effort-edit-status').textContent = effortHint(levels);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
614
686
|
if (event.target.id === 'profile-instance') {
|
|
615
687
|
updateModelChoices();
|
|
616
688
|
showModelLoading(false);
|
|
@@ -758,6 +830,32 @@ document.addEventListener('click', async (event) => {
|
|
|
758
830
|
await reload();
|
|
759
831
|
return;
|
|
760
832
|
}
|
|
833
|
+
if (action === 'save-jev') {
|
|
834
|
+
const url = $('#jev-url').value.trim();
|
|
835
|
+
await api('jev', {
|
|
836
|
+
jev: {
|
|
837
|
+
mode: $('#jev-on').checked ? 'auto' : 'off',
|
|
838
|
+
url: url || null,
|
|
839
|
+
model: $('#jev-model').value.trim() || 'jev-latest',
|
|
840
|
+
minConfidence: Number($('#jev-confidence').value),
|
|
841
|
+
timeoutMs: Number($('#jev-timeout').value),
|
|
842
|
+
},
|
|
843
|
+
});
|
|
844
|
+
notice('Fast decision settings saved. Restart to apply them.');
|
|
845
|
+
await reload();
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (action === 'save-jev-key' || action === 'clear-jev-key') {
|
|
849
|
+
await api('jev-key', { value: action === 'clear-jev-key' ? '' : $('#jev-key').value });
|
|
850
|
+
notice('Key updated in this computer’s keychain. Restart to apply it.');
|
|
851
|
+
await reload();
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (action === 'test-jev') {
|
|
855
|
+
const result = await api('jev/test', {});
|
|
856
|
+
notice(`Fast decisions work: ${result.model} answered “${result.choice}” in ${result.ms} ms.`);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
761
859
|
if (action === 'save-voice-key' || action === 'clear-voice-key') {
|
|
762
860
|
await api('voice-key', {
|
|
763
861
|
provider: button.dataset.provider,
|
|
@@ -772,6 +870,12 @@ document.addEventListener('click', async (event) => {
|
|
|
772
870
|
await reload();
|
|
773
871
|
return;
|
|
774
872
|
}
|
|
873
|
+
if (action === 'save-default-agents') {
|
|
874
|
+
await api('default-agents', { packId: $('#default-agents').value || null });
|
|
875
|
+
notice('Default agents saved.');
|
|
876
|
+
await reload();
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
775
879
|
if (action === 'set-default-executor') {
|
|
776
880
|
await api('executor', { executorId: id });
|
|
777
881
|
notice('Default executor updated.');
|
|
@@ -819,7 +923,7 @@ document.addEventListener('click', async (event) => {
|
|
|
819
923
|
return;
|
|
820
924
|
}
|
|
821
925
|
if (action === 'save-profile') {
|
|
822
|
-
await api('profile/update', { profileId: id, systemPrompt: $('#profile-prompt-edit').value, reasoningEffort: $('#profile-effort-edit').value || null });
|
|
926
|
+
await api('profile/update', { profileId: id, modelLabel: $('#profile-model-edit').value || null, runtimeMode: $('#profile-mode-edit').value, systemPrompt: $('#profile-prompt-edit').value, reasoningEffort: $('#profile-effort-edit').value || null });
|
|
823
927
|
notice('Agent saved. Changes apply to its next task.');
|
|
824
928
|
await reload();
|
|
825
929
|
return;
|
|
@@ -905,7 +1009,7 @@ document.addEventListener('click', async (event) => {
|
|
|
905
1009
|
link.download = `${id}.sidebud-widget.json`;
|
|
906
1010
|
link.click();
|
|
907
1011
|
URL.revokeObjectURL(link.href);
|
|
908
|
-
notice('Exported. The file holds the pack and
|
|
1012
|
+
notice('Exported. The file holds the pack (manifest, logo, skill, and any server files); settings and keys stay on this computer.');
|
|
909
1013
|
return;
|
|
910
1014
|
}
|
|
911
1015
|
if (action === 'import-pack') {
|
|
@@ -936,11 +1040,11 @@ document.addEventListener('click', async (event) => {
|
|
|
936
1040
|
notice(`Connected. All ${result.tools} declared tools are available. This check did not execute them.`);
|
|
937
1041
|
return;
|
|
938
1042
|
}
|
|
939
|
-
if (action === '
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
);
|
|
943
|
-
|
|
1043
|
+
if (action === 'save-pack-tools') {
|
|
1044
|
+
const blockedTools = [...document.querySelectorAll('[data-pack-tool]')].filter((box) => !box.checked).map((box) => box.dataset.packTool);
|
|
1045
|
+
await api('pack', { packId: id, blockedTools });
|
|
1046
|
+
notice(blockedTools.length ? `Saved. ${blockedTools.length === 1 ? 'One tool is' : `${blockedTools.length} tools are`} turned off for this integration.` : 'Saved. Every declared tool is allowed.');
|
|
1047
|
+
await reload();
|
|
944
1048
|
return;
|
|
945
1049
|
}
|
|
946
1050
|
if (action === 'close-pack') {
|
|
@@ -981,12 +1085,24 @@ document.addEventListener('click', async (event) => {
|
|
|
981
1085
|
await reload();
|
|
982
1086
|
return;
|
|
983
1087
|
}
|
|
984
|
-
if (action === '
|
|
985
|
-
|
|
986
|
-
|
|
1088
|
+
if (action === 'remove-pack') {
|
|
1089
|
+
if (!confirm(`Remove ${id}? Its settings, keys, and account sign-in are deleted from this computer. You can install it again from the library or an exported file.`)) return;
|
|
1090
|
+
await api('pack/remove', { packId: id });
|
|
1091
|
+
if (packPageId() === id) location.hash = '#mcp';
|
|
1092
|
+
notice('Integration removed.');
|
|
1093
|
+
await reload();
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
if (action === 'install-library-pack') {
|
|
1097
|
+
libraryPage = null;
|
|
1098
|
+
const result = await api('pack/install-library', {
|
|
1099
|
+
id: button.dataset.id,
|
|
1100
|
+
...(button.dataset.replace === 'true' ? { replace: true } : {}),
|
|
987
1101
|
});
|
|
988
1102
|
notice(
|
|
989
|
-
result.setup.started
|
|
1103
|
+
result.updated && !result.setup.started
|
|
1104
|
+
? `${result.name} updated to ${result.version}; its settings and keys were kept.`
|
|
1105
|
+
: result.setup.started
|
|
990
1106
|
? `${result.name} installed. This computer's agent is setting it up and reports back when it finishes.`
|
|
991
1107
|
: `${result.name} installed, but setup did not start: ${result.setup.note} Set it up in Settings & permissions.`
|
|
992
1108
|
);
|