sidebud 0.1.0 → 0.2.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 +17 -0
- package/dist/main.js +4725 -3310
- package/package.json +2 -2
- package/skills/widget-packs/SKILL.md +198 -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 +10 -0
- package/src/manage/manage.css +744 -0
- package/src/manage/manage.js +66 -35
- package/src/manage/packs.js +340 -0
- package/src/manage/setup.js +12 -13
- 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', '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,20 @@ 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
|
-
mcp: mcpView,
|
|
142
|
+
mcp: () => (packPageId() ? packPageView() : libraryPageSlug() ? libraryPackView() : mcpView()),
|
|
143
|
+
phone: phoneView,
|
|
141
144
|
layouts: layoutsView,
|
|
145
|
+
guide: guideView,
|
|
142
146
|
settings: settingsView,
|
|
143
147
|
}[current]();
|
|
148
|
+
afterPacksRender();
|
|
149
|
+
if (scrollTo) scrollToGuideAnchor();
|
|
144
150
|
if (showsExecution()) void discoverModels();
|
|
145
151
|
if (current === 'setup') afterSetupRender();
|
|
146
152
|
if (
|
|
@@ -474,13 +480,6 @@ function updateModelChoices(selectedModel = '') {
|
|
|
474
480
|
}
|
|
475
481
|
const interval = (seconds) => (seconds < 60 ? `${seconds} sec` : seconds >= 3600 && seconds % 3600 === 0 ? `${seconds / 3600} hr` : `${Math.round(seconds / 60)} min`);
|
|
476
482
|
|
|
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
483
|
/** `embedded` drops the page heading when setup shows these controls under its own. */
|
|
485
484
|
function executionView({ embedded = false } = {}) {
|
|
486
485
|
const providers = state.providers;
|
|
@@ -505,7 +504,7 @@ function executionView({ embedded = false } = {}) {
|
|
|
505
504
|
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
505
|
const profiles = providers?.profiles.filter(profile => instances.some(instance => [instance.instanceId, instance.displayName, instance.driver].includes(profile.provider))) || [];
|
|
507
506
|
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('');
|
|
507
|
+
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
508
|
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
509
|
<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>
|
|
511
510
|
<div class="row"><h3>Choose a provider</h3><button data-action="refresh-executors">Check availability</button></div>
|
|
@@ -520,7 +519,7 @@ function executionView({ embedded = false } = {}) {
|
|
|
520
519
|
<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
520
|
<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
521
|
<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"
|
|
522
|
+
<details><summary>Permissions</summary><label>When the agent changes files or runs commands<select id="profile-mode">${runtimeModeChoices('approval-required')}</select></label></details>
|
|
524
523
|
<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
524
|
</div></details>
|
|
526
525
|
${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>` : ''}`}
|
|
@@ -528,13 +527,25 @@ function executionView({ embedded = false } = {}) {
|
|
|
528
527
|
${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>` : ''}
|
|
529
528
|
</section>`;
|
|
530
529
|
}
|
|
530
|
+
const RUNTIME_MODES = [['approval-required', 'Ask me first'], ['read-only', 'Read only'], ['auto-accept-edits', 'Accept edits automatically'], ['auto', 'Automatic'], ['full-access', 'Full access']];
|
|
531
|
+
function runtimeModeChoices(selected) {
|
|
532
|
+
return RUNTIME_MODES.map(([value, label]) => `<option value="${value}" ${value === selected ? 'selected' : ''}>${label}</option>`).join('');
|
|
533
|
+
}
|
|
534
|
+
/** The installation a profile runs on, by the id or display name it names. */
|
|
535
|
+
const profileInstance = profile => state.providers.instances.find(candidate => [candidate.instanceId, candidate.displayName].includes(profile.provider));
|
|
536
|
+
/** Thinking levels for the edit form's model; a saved level stays visible even before the model list loads. */
|
|
537
|
+
function editEffortLevels(instance, model, saved) {
|
|
538
|
+
const levels = effortLevelsFor(instance, model);
|
|
539
|
+
return saved && !levels.includes(saved) && !instance?.models?.length ? [...levels, saved] : levels;
|
|
540
|
+
}
|
|
531
541
|
function profileEditor(profile) {
|
|
532
542
|
const current = state.profilePrompts?.[profile.id] || '';
|
|
533
|
-
const instance =
|
|
534
|
-
const
|
|
535
|
-
//
|
|
536
|
-
const
|
|
537
|
-
|
|
543
|
+
const instance = profileInstance(profile);
|
|
544
|
+
const shown = editEffortLevels(instance, profile.model, profile.reasoningEffort);
|
|
545
|
+
// A saved model the installation no longer lists (or has not listed yet) stays selectable.
|
|
546
|
+
const listed = (instance?.models || []).some(model => model.id === profile.model);
|
|
547
|
+
const models = `${modelChoices(instance, profile.model || '')}${profile.model && !listed ? `<option value="${esc(profile.model)}" selected>${esc(profile.model)}</option>` : ''}`;
|
|
548
|
+
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
549
|
}
|
|
539
550
|
function providerEditor(p) {
|
|
540
551
|
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 +566,15 @@ function packStatusDetail(pack) {
|
|
|
555
566
|
: detail;
|
|
556
567
|
}
|
|
557
568
|
function catalogView() {
|
|
558
|
-
|
|
569
|
+
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>`;
|
|
570
|
+
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' ? 'warn' : ''}">${entry.installed ? 'Installed' : entry.detected ? 'App found' : 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
571
|
}
|
|
560
572
|
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
|
|
573
|
+
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
574
|
}
|
|
563
575
|
function customizePackEditor(p) {
|
|
564
576
|
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
577
|
}
|
|
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
578
|
function layoutsView() {
|
|
570
579
|
const active = state.devices.filter((d) => !d.revokedAt);
|
|
571
580
|
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 +608,8 @@ document.addEventListener('change', (event) => {
|
|
|
599
608
|
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
609
|
return;
|
|
601
610
|
}
|
|
602
|
-
if (event.target.
|
|
603
|
-
void api('
|
|
611
|
+
if (event.target.dataset.announcePack) {
|
|
612
|
+
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
613
|
return;
|
|
605
614
|
}
|
|
606
615
|
if (event.target.id === 'profile-effort') {
|
|
@@ -611,6 +620,16 @@ document.addEventListener('change', (event) => {
|
|
|
611
620
|
updateEffortChoices();
|
|
612
621
|
return;
|
|
613
622
|
}
|
|
623
|
+
if (event.target.id === 'profile-model-edit') {
|
|
624
|
+
// Keep the chosen thinking level when the new model offers it.
|
|
625
|
+
const profile = state.providers.profiles.find(candidate => candidate.id === event.target.dataset.id);
|
|
626
|
+
const effort = $('#profile-effort-edit');
|
|
627
|
+
const levels = editEffortLevels(profileInstance(profile), event.target.value, null);
|
|
628
|
+
effort.innerHTML = effortChoices(levels, levels.includes(effort.value) ? effort.value : '');
|
|
629
|
+
effort.disabled = !levels.length;
|
|
630
|
+
$('#profile-effort-edit-status').textContent = effortHint(levels);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
614
633
|
if (event.target.id === 'profile-instance') {
|
|
615
634
|
updateModelChoices();
|
|
616
635
|
showModelLoading(false);
|
|
@@ -819,7 +838,7 @@ document.addEventListener('click', async (event) => {
|
|
|
819
838
|
return;
|
|
820
839
|
}
|
|
821
840
|
if (action === 'save-profile') {
|
|
822
|
-
await api('profile/update', { profileId: id, systemPrompt: $('#profile-prompt-edit').value, reasoningEffort: $('#profile-effort-edit').value || null });
|
|
841
|
+
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
842
|
notice('Agent saved. Changes apply to its next task.');
|
|
824
843
|
await reload();
|
|
825
844
|
return;
|
|
@@ -905,7 +924,7 @@ document.addEventListener('click', async (event) => {
|
|
|
905
924
|
link.download = `${id}.sidebud-widget.json`;
|
|
906
925
|
link.click();
|
|
907
926
|
URL.revokeObjectURL(link.href);
|
|
908
|
-
notice('Exported. The file holds the pack and
|
|
927
|
+
notice('Exported. The file holds the pack (manifest, logo, skill, and any server files); settings and keys stay on this computer.');
|
|
909
928
|
return;
|
|
910
929
|
}
|
|
911
930
|
if (action === 'import-pack') {
|
|
@@ -936,11 +955,11 @@ document.addEventListener('click', async (event) => {
|
|
|
936
955
|
notice(`Connected. All ${result.tools} declared tools are available. This check did not execute them.`);
|
|
937
956
|
return;
|
|
938
957
|
}
|
|
939
|
-
if (action === '
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
);
|
|
943
|
-
|
|
958
|
+
if (action === 'save-pack-tools') {
|
|
959
|
+
const blockedTools = [...document.querySelectorAll('[data-pack-tool]')].filter((box) => !box.checked).map((box) => box.dataset.packTool);
|
|
960
|
+
await api('pack', { packId: id, blockedTools });
|
|
961
|
+
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.');
|
|
962
|
+
await reload();
|
|
944
963
|
return;
|
|
945
964
|
}
|
|
946
965
|
if (action === 'close-pack') {
|
|
@@ -981,12 +1000,24 @@ document.addEventListener('click', async (event) => {
|
|
|
981
1000
|
await reload();
|
|
982
1001
|
return;
|
|
983
1002
|
}
|
|
984
|
-
if (action === '
|
|
985
|
-
|
|
986
|
-
|
|
1003
|
+
if (action === 'remove-pack') {
|
|
1004
|
+
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;
|
|
1005
|
+
await api('pack/remove', { packId: id });
|
|
1006
|
+
if (packPageId() === id) location.hash = '#mcp';
|
|
1007
|
+
notice('Integration removed.');
|
|
1008
|
+
await reload();
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (action === 'install-library-pack') {
|
|
1012
|
+
libraryPage = null;
|
|
1013
|
+
const result = await api('pack/install-library', {
|
|
1014
|
+
id: button.dataset.id,
|
|
1015
|
+
...(button.dataset.replace === 'true' ? { replace: true } : {}),
|
|
987
1016
|
});
|
|
988
1017
|
notice(
|
|
989
|
-
result.setup.started
|
|
1018
|
+
result.updated && !result.setup.started
|
|
1019
|
+
? `${result.name} updated to ${result.version}; its settings and keys were kept.`
|
|
1020
|
+
: result.setup.started
|
|
990
1021
|
? `${result.name} installed. This computer's agent is setting it up and reports back when it finishes.`
|
|
991
1022
|
: `${result.name} installed, but setup did not start: ${result.setup.note} Set it up in Settings & permissions.`
|
|
992
1023
|
);
|