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
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// Pack pages and the live phone preview. Loaded after manage.js and shares its state and helpers (state, api, esc, code, render, reload, noticeβ¦).
|
|
2
|
+
// The preview draws the same widgets, layout, and tile packing as the phone app, so what you see here is roughly what the phone shows.
|
|
3
|
+
|
|
4
|
+
/** Bundled glyphs for `logo.kind === 'icon'`, as the phone draws them. */
|
|
5
|
+
const PHONE_ICONS = { agents: 'π€', mail: 'βοΈ', calendar: 'π
', chat: 'π¬', code: 'β¨οΈ', github: 'π', files: 'π', chart: 'π', bell: 'π', bolt: 'β‘οΈ', globe: 'π', home: 'π ', music: 'π΅', note: 'π', star: 'βοΈ', tools: 'π οΈ' };
|
|
6
|
+
const PREVIEW_REFRESH_MS = 5000;
|
|
7
|
+
let preview = null;
|
|
8
|
+
let previewError = null;
|
|
9
|
+
let previewDevice = '';
|
|
10
|
+
let previewOpen = null;
|
|
11
|
+
let previewTimer = null;
|
|
12
|
+
let packPageData = null;
|
|
13
|
+
let packPageError = null;
|
|
14
|
+
|
|
15
|
+
/** The pack a `#mcp/<id>` address opens, if any. */
|
|
16
|
+
function packPageId() {
|
|
17
|
+
const [head, ...rest] = location.hash.slice(1).split('/');
|
|
18
|
+
return head === 'mcp' && rest.length && rest[0] !== 'library' ? decodeURIComponent(rest.join('/')) : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The library pack a `#mcp/library/<slug>` address opens, if any. */
|
|
22
|
+
function libraryPageSlug() {
|
|
23
|
+
const [head, kind, ...rest] = location.hash.slice(1).split('/');
|
|
24
|
+
return head === 'mcp' && kind === 'library' && rest.length ? decodeURIComponent(rest.join('/')) : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let libraryPage = null;
|
|
28
|
+
let libraryPageError = null;
|
|
29
|
+
let libraryConfirmed = false;
|
|
30
|
+
|
|
31
|
+
/** What a tool may do, as the pack pages show it. */
|
|
32
|
+
const accessPill = (access) => `<span class="pill ${access === 'write' ? 'warn' : ''}">${access === 'write' ? 'changes things' : 'reads'}</span>`;
|
|
33
|
+
|
|
34
|
+
/** Who stands behind a pack, and what installing any pack means. Community packs get a warning. */
|
|
35
|
+
function trustNoticeHtml(publisher) {
|
|
36
|
+
const community = publisher?.source === 'community';
|
|
37
|
+
return `<div class="card trust-notice ${community ? 'community' : ''}" role="${community ? 'alert' : 'note'}"><strong>${community ? `Community widget by ${esc(publisher.name)}` : 'Made by Sidebud'}</strong><p>${community ? 'Sidebud did not make or review this widget, and may not support it. It may be unsafe, stop working, or behave differently than described. Install it only if you trust its author, and read what it can do first.' : 'The Sidebud team builds and maintains this widget and checks it before it is listed.'}</p><p class="hint">Every widget runs on this computer with the access listed here. It installs switched off, and your agent sets it up; you choose when to turn it on, and you can turn off any tool.</p></div>`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The library pack's page: what it can do, its trust, and its widget on a phone, before anything is installed. */
|
|
41
|
+
function libraryPackView() {
|
|
42
|
+
const slug = libraryPageSlug();
|
|
43
|
+
const page = libraryPage?.slug === slug ? libraryPage : null;
|
|
44
|
+
if (!page && !libraryPageError) void loadLibraryPack(slug);
|
|
45
|
+
if (libraryPageError) return `<section class="section"><a href="#mcp">β Integrations</a><div class="card muted">${esc(libraryPageError)}</div></section>`;
|
|
46
|
+
if (!page) return '<section class="section"><a href="#mcp">β Integrations</a><div class="card muted">Loadingβ¦</div></section>';
|
|
47
|
+
const a = page.access;
|
|
48
|
+
const community = page.publisher.source === 'community';
|
|
49
|
+
const installed = page.installedVersion !== null;
|
|
50
|
+
const tools = `<table class="tool-table"><thead><tr><th>Tool</th><th>Access</th><th>Used by</th></tr></thead><tbody>${a.tools.map((tool) => `<tr><td>${code(tool.name)}${tool.description ? `<div class="hint">${esc(tool.description)}</div>` : ''}</td><td>${accessPill(tool.access)}</td><td class="hint">${tool.usedBy.map(esc).join(', ') || 'Your agent, when you ask'}</td></tr>`).join('')}</tbody></table><p class="hint">Your computer only ever calls these tools. After installing, you can turn off any of them on the pack's page; a tool that is off is never called by the widget, a control, or your agent.</p>`;
|
|
51
|
+
const runs = a.launch.transport === 'stdio' ? `<p>${code(a.launch.command)}</p><p class="hint">${a.launch.ownServer ? 'A small server that comes with this pack, run with Node.js while the pack is on. Its code is in the pack folder.' : 'A program already on this computer, started while the pack is on.'}${a.launch.env.length ? ` It receives ${a.launch.env.map(code).join(', ')}.` : ''}</p>` : `<p>${code(a.launch.url)}</p><p class="hint">A service on the internet this computer connects to. Keys go only to it.</p>`;
|
|
52
|
+
const keys = `${a.accountSignIn ? '<p>You sign in to an account in this computer\'s browser.</p>' : ''}${a.secrets.map((secret) => `<p>${code(secret.name)} <span class="hint">${esc(secret.description)}</span></p>`).join('')}${!a.accountSignIn && !a.secrets.length ? '<p class="hint">No keys or sign-in.</p>' : ''}<p class="hint">Keys and tokens are saved in this computer's keychain and never shown again.</p>`;
|
|
53
|
+
const action = installed
|
|
54
|
+
? `<a class="button" href="#mcp/${encodeURIComponent(page.id)}">Open its page Β· installed ${esc(page.installedVersion)}</a>`
|
|
55
|
+
: `${community ? `<label class="confirm-line"><input type="checkbox" data-library-confirm ${libraryConfirmed ? 'checked' : ''}> I trust ${esc(page.publisher.name)} and have read what this widget can do</label>` : ''}<button class="primary" data-action="install-library-pack" data-id="${esc(page.slug)}" ${community && !libraryConfirmed ? 'disabled' : ''}>${page.needsReinstall ? 'Reinstall' : 'Install'} ${esc(page.name)}</button>`;
|
|
56
|
+
return `<section class="section pack-page"><a href="#mcp">β Integrations</a><div class="pack-page-head"><div><h2>${esc(page.name)}</h2><p class="hint">${esc(page.author)} Β· ${esc(page.license)} Β· version ${esc(page.version)}${page.homepage ? ` Β· <a href="${esc(page.homepage)}" target="_blank" rel="noreferrer">Homepage</a>` : ''}</p><p>${esc(page.description)}</p></div><span class="badge ${community ? 'warn' : ''}">${community ? 'Community' : 'Sidebud'}</span></div>${trustNoticeHtml(page.publisher)}<div class="actions library-install">${action}</div><div class="pack-page-grid"><div class="stack"><div class="card stack"><h3>What it can do on this computer</h3>${tools}</div><div class="card stack"><h3>What it runs</h3>${runs}</div><div class="card stack"><h3>Keys and accounts</h3>${keys}</div><div class="card stack"><h3>Network</h3><p class="hint">${esc(a.network.join(', ') || 'None declared beyond what it runs.')}</p></div>${a.setup.steps.length ? `<div class="card stack"><h3>Setting it up</h3><ol class="plain-steps">${a.setup.steps.map((step) => `<li>${esc(step)}</li>`).join('')}</ol>${a.setup.prerequisites.length ? `<p class="hint">Needs: ${a.setup.prerequisites.map(esc).join(', ')}</p>` : ''}</div>` : ''}</div><aside class="pack-preview"><h3>On your phone</h3><p class="hint">${page.hasPreview ? 'Made-up sample data from the pack. Tap the tile to open it.' : 'This pack has no sample data, so its widget is drawn empty.'}</p><div id="library-preview">${libraryPreviewBody(page)}</div></aside></div></section>`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function libraryPreviewBody(page) {
|
|
60
|
+
const tile = { ...page.tile, place: { ...page.tile.place, row: 0, col: 0 }, draft: true };
|
|
61
|
+
return phoneDevice([tile], tile.place.height, previewOpen === tile.id ? tile : null, { label: `${page.name} preview` });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function loadLibraryPack(slug) {
|
|
65
|
+
if (loadLibraryPack.loading === slug) return;
|
|
66
|
+
loadLibraryPack.loading = slug;
|
|
67
|
+
try {
|
|
68
|
+
libraryPage = await api(`library-pack?id=${encodeURIComponent(slug)}`);
|
|
69
|
+
libraryPageError = null;
|
|
70
|
+
libraryConfirmed = false;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
libraryPageError = error.message;
|
|
73
|
+
} finally {
|
|
74
|
+
loadLibraryPack.loading = null;
|
|
75
|
+
}
|
|
76
|
+
if (libraryPageSlug() === slug) render();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function logoHtml(logo, className = 'phone-logo') {
|
|
80
|
+
if (logo?.kind === 'image') return `<img class="${className}" src="${esc(logo.src)}" alt="">`;
|
|
81
|
+
const glyph = logo?.kind === 'emoji' ? logo.value : logo?.kind === 'icon' ? PHONE_ICONS[logo.name] || 'β«οΈ' : 'β«οΈ';
|
|
82
|
+
return `<span class="${className}" aria-hidden="true">${esc(glyph)}</span>`;
|
|
83
|
+
}
|
|
84
|
+
const tone = (value) => `tone-${esc(value || 'neutral')}`;
|
|
85
|
+
|
|
86
|
+
function glanceHtml(tile) {
|
|
87
|
+
const glance = tile.glance;
|
|
88
|
+
if (glance?.stats) {
|
|
89
|
+
const stats = glance.stats.slice(0, tile.size === 'small' ? 1 : 2);
|
|
90
|
+
return `<span class="phone-glance">${stats.map((stat, index) => `${index ? ' ' : ''}${esc(stat.label)} Β· <span class="${stat.tone === 'neutral' ? '' : tone(stat.tone)}">${esc(stat.value)}</span>`).join('')}</span>`;
|
|
91
|
+
}
|
|
92
|
+
if (glance?.line) return `<span class="phone-glance ${glance.tone === 'neutral' ? '' : tone(glance.tone)}">${esc(glance.line)}</span>`;
|
|
93
|
+
return tile.subtitle ? `<span class="phone-glance">${esc(tile.subtitle)}</span>` : '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A row's time relative to now, as the phone shows it (protocol `relativeTime`). */
|
|
97
|
+
function relativeTime(iso) {
|
|
98
|
+
const at = Date.parse(iso);
|
|
99
|
+
if (Number.isNaN(at)) return '';
|
|
100
|
+
const seconds = Math.round((at - Date.now()) / 1000);
|
|
101
|
+
const size = Math.abs(seconds);
|
|
102
|
+
if (size < 45) return seconds > 0 ? 'in a moment' : 'just now';
|
|
103
|
+
const [amount, unit] = size < 3600 ? [Math.round(size / 60), 'm'] : size < 86400 ? [Math.round(size / 3600), 'h'] : size < 30 * 86400 ? [Math.round(size / 86400), 'd'] : [0, ''];
|
|
104
|
+
if (!unit) return new Date(at).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
105
|
+
return seconds > 0 ? `in ${amount}${unit}` : `${amount}${unit} ago`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A row's right side: its status in its tone, or else its time. */
|
|
109
|
+
function rowEndHtml(item) {
|
|
110
|
+
if (item.status) return `<span class="${tone(item.status.tone)}">${esc(item.status.text)}</span>`;
|
|
111
|
+
return item.time ? `<span class="muted">${esc(relativeTime(item.time))}</span>` : '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A large tile's previews, as widget-tile.tsx draws them: one list's top four rows, two previews' headings and top row each, and a board's column counts. */
|
|
115
|
+
function tileListsHtml(tile) {
|
|
116
|
+
const lists = tile.blocks.filter((block) => block.type === 'list' || block.type === 'board').slice(0, 2);
|
|
117
|
+
const rows = lists.length > 1 ? 1 : 4;
|
|
118
|
+
const heading = (list) => (list.title && lists.length > 1 ? `<span class="muted phone-list-title">${esc(list.title)}</span>` : '');
|
|
119
|
+
return lists.map((list) => list.type === 'board'
|
|
120
|
+
? `<span class="phone-tile-list">${heading(list)}<span class="phone-board-counts">${list.columns.map((column) => `<span class="muted">${esc(column.title)} <span class="${column.count ? tone(column.tone) : 'muted'}">${column.count}</span></span>`).join('')}</span></span>`
|
|
121
|
+
: `<span class="phone-tile-list">${heading(list)}${list.items.slice(0, rows).map((item) => `<span><span>${esc(item.title)}</span>${rowEndHtml(item)}</span>`).join('') || `<span class="muted">${esc(list.emptyText || '')}</span>`}</span>`).join('');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** One home tile, as widget-tile.tsx draws it: logo, title, glance, badge, and list previews when large. */
|
|
125
|
+
function tileHtml(tile, place = tile.place) {
|
|
126
|
+
const badge = tile.badge && tile.badge.text && tile.badge.text !== '0' ? tile.badge : null;
|
|
127
|
+
const lists = tile.size === 'large' ? tileListsHtml(tile) : '';
|
|
128
|
+
const text = tile.size === 'large' && !lists ? tile.blocks.find((block) => block.type === 'text') : null;
|
|
129
|
+
return `<button type="button" class="phone-tile size-${esc(tile.size)} ${previewOpen === tile.id ? 'open' : ''} ${tile.draft ? 'draft' : ''}" data-preview-tile="${esc(tile.id)}" data-row="${place.row}" data-col="${place.col}" data-width="${place.width}" data-height="${place.height}" aria-label="${esc(tile.title)}">
|
|
130
|
+
<span class="phone-tile-row"><span class="phone-logo-wrap">${logoHtml(tile.logo)}${badge && tile.size === 'small' ? `<span class="phone-badge-dot bg-${esc(badge.tone)}"></span>` : ''}</span><span class="phone-tile-text"><strong>${esc(tile.title)}</strong>${glanceHtml(tile)}</span>${badge && tile.size !== 'small' ? `<span class="phone-badge ${tone(badge.tone)}">${esc(badge.text)}</span>` : ''}<span class="phone-chevron" aria-hidden="true">βΊ</span></span>
|
|
131
|
+
${lists}${text ? `<span class="phone-tile-list muted">${esc(text.text)}</span>` : ''}
|
|
132
|
+
</button>`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** What tapping a tile opens: its blocks and chosen controls (controls are shown, not run, in the preview). `back` adds the app's "βΉ Home". */
|
|
136
|
+
function detailHtml(tile, { back = false } = {}) {
|
|
137
|
+
if (!tile) return '';
|
|
138
|
+
const blocks = tile.blocks.map((block) => {
|
|
139
|
+
if (block.type === 'stats') return `<div class="phone-stats">${block.items.map((item) => `<div><strong class="${item.tone === 'neutral' ? '' : tone(item.tone)}">${esc(item.value)}</strong><span class="muted">${esc(item.label)}</span></div>`).join('')}</div>`;
|
|
140
|
+
if (block.type === 'list') return `<div class="phone-list">${block.title ? `<span class="muted phone-list-title">${esc(block.title)}</span>` : ''}${block.items.slice(0, 6).map((item) => `<div><div><strong>${esc(item.title)}</strong>${item.subtitle ? `<span class="muted">${esc(item.subtitle)}</span>` : ''}</div><div class="phone-row-end">${item.status ? `<span class="${tone(item.status.tone)}">${esc(item.status.text)}</span>` : ''}${item.time ? `<span class="muted">${esc(relativeTime(item.time))}</span>` : ''}</div></div>`).join('') || `<p class="muted">${esc(block.emptyText || 'Nothing here')}</p>`}${block.items.length > 6 ? `<span class="phone-more">Show ${block.items.length - 6} more</span>` : ''}</div>`;
|
|
141
|
+
if (block.type === 'board') return `<div class="phone-board">${block.title ? `<span class="muted phone-list-title">${esc(block.title)}</span>` : ''}<div class="phone-columns">${block.columns.map((column) => `<div class="phone-column"><div class="phone-column-head"><span class="phone-dot bg-${esc(column.tone)}"></span><strong>${esc(column.title)}</strong><span class="muted">${column.count}</span></div>${column.items.map((item) => `<div class="phone-card"><span>${esc(item.title)}</span>${item.subtitle || item.time ? `<span class="muted">${esc(item.subtitle || '')}${item.subtitle && item.time ? ' Β· ' : ''}${item.time ? esc(relativeTime(item.time)) : ''}</span>` : ''}</div>`).join('') || '<span class="muted">Nothing here</span>'}${column.count > column.items.length ? `<span class="muted">+${column.count - column.items.length} more</span>` : ''}</div>`).join('')}</div></div>`;
|
|
142
|
+
if (block.type === 'text') return `<p class="phone-text ${block.tone === 'neutral' ? '' : tone(block.tone)}">${esc(block.text)}</p>`;
|
|
143
|
+
if (block.type === 'progress') return `<div class="phone-progress"><span>${esc(block.label)}</span><span class="phone-bar"><span data-progress="${Number(block.value) || 0}"></span></span></div>`;
|
|
144
|
+
return '';
|
|
145
|
+
});
|
|
146
|
+
return `<div class="phone-detail" aria-label="${esc(tile.title)} detail">${back ? '<div class="device-nav"><button type="button" class="device-back" data-preview-home>βΉ Home</button><span>Controls</span></div>' : ''}<div class="phone-detail-head">${logoHtml(tile.logo, 'phone-logo phone-logo-large')}<div><strong>${esc(tile.title)}</strong>${tile.subtitle ? `<span class="muted">${esc(tile.subtitle)}</span>` : ''}</div></div>${blocks.join('')}${tile.controls.length ? `<div class="phone-controls">${tile.controls.map((control) => `<span class="phone-control ${control.style === 'primary' ? 'primary' : ''}" title="${esc(control.confirm ? `Asks first: ${control.confirm}` : 'Runs on the phone')}">${esc(control.label)}</span>`).join('')}</div>` : ''}<p class="hint">Updated ${esc(new Date(tile.updatedAt).toLocaleTimeString())}</p></div>`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The app's home screen: its header, the voice card, and tiles packed on its two-column grid. */
|
|
150
|
+
function homeScreenHtml(tiles, rows, { computer = state.name } = {}) {
|
|
151
|
+
return `<div class="device-header"><img class="device-app-logo" src="/sidebud-icon.png" alt=""><div><strong>Sidebud</strong><span class="muted">Think. Speak. Get it done.</span></div><span class="device-computer">β ${esc(computer)} β</span></div><div class="device-voice"><span class="device-wave" aria-hidden="true">${'<i></i>'.repeat(24)}</span><span class="muted">Tap Start chat to talk</span></div><div class="phone-grid" data-rows="${Math.max(rows, 1)}">${tiles.map((tile) => tileHtml(tile)).join('') || '<p class="muted phone-empty">No widgets yet.</p>'}</div>`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** An iPhone-sized device around one app screen: status bar with the island, the screen, the app's bottom buttons, and the home indicator. */
|
|
155
|
+
function deviceHtml(screen, { label = 'Phone preview' } = {}) {
|
|
156
|
+
const time = new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }).replace(/\s?[AP]M$/i, '');
|
|
157
|
+
return `<div class="device" role="group" aria-label="${esc(label)}"><div class="device-screen phone"><div class="device-status"><span>${esc(time)}</span><span class="device-island" aria-hidden="true"></span><span class="device-icons" aria-hidden="true">βββ β β</span></div><div class="device-body">${screen}</div><div class="device-dock"><span class="device-dock-primary">π Start chat</span><span class="device-dock-secondary">π¬ Messages</span></div><div class="device-home" aria-hidden="true"></div></div></div>`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** A device showing the home screen, or the opened tile when `open` is set (tapping navigates, as on the phone). */
|
|
161
|
+
function phoneDevice(tiles, rows, open, options = {}) {
|
|
162
|
+
return deviceHtml(open ? detailHtml(open, { back: true }) : homeScreenHtml(tiles, rows, options), options);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Places tiles and progress bars from their data attributes (the page's policy allows no inline styles). */
|
|
166
|
+
function applyPreviewLayout(root = document) {
|
|
167
|
+
root.querySelectorAll('.phone-grid').forEach((grid) => {
|
|
168
|
+
grid.style.gridTemplateRows = `repeat(${grid.dataset.rows}, 76px)`;
|
|
169
|
+
});
|
|
170
|
+
root.querySelectorAll('[data-preview-tile]').forEach((tile) => {
|
|
171
|
+
tile.style.gridRow = `${Number(tile.dataset.row) + 1} / span ${tile.dataset.height}`;
|
|
172
|
+
tile.style.gridColumn = `${Number(tile.dataset.col) + 1} / span ${tile.dataset.width}`;
|
|
173
|
+
});
|
|
174
|
+
root.querySelectorAll('[data-progress]').forEach((bar) => {
|
|
175
|
+
bar.style.width = `${Math.round(Math.min(1, Math.max(0, Number(bar.dataset.progress))) * 100)}%`;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function previewTiles() {
|
|
180
|
+
return preview?.tiles || [];
|
|
181
|
+
}
|
|
182
|
+
function packTile(packId) {
|
|
183
|
+
return previewTiles().find((tile) => tile.adapter === `pack:${packId}`) || null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The Phone preview section: every tile on the chosen phone's home screen, refreshed live. */
|
|
187
|
+
function phoneView() {
|
|
188
|
+
const devices = preview?.devices || [];
|
|
189
|
+
const open = previewTiles().find((tile) => tile.id === previewOpen) || null;
|
|
190
|
+
return `<section class="section"><div><h2>Phone preview</h2><p class="lede">Your widgets as the Sidebud app shows them, with live data from this computer. Tap a tile to see its detail view. The layout follows ${devices.length ? 'the phone you choose' : 'the phone app once one is paired'}; arrange tiles on the phone or under Phone layouts. The <a href="#guide/see-how-it-looks">Guide</a> explains what you see.</p></div>${devices.length ? `<label class="preview-device">Phone <select data-preview-device><option value="">Most recently arranged</option>${devices.map((device) => `<option value="${esc(device.id)}" ${previewDevice === device.id ? 'selected' : ''}>${esc(device.name)}</option>`).join('')}</select></label>` : ''}<div id="phone-preview">${phonePreviewBody(open)}</div></section>`;
|
|
191
|
+
}
|
|
192
|
+
function phonePreviewBody(open) {
|
|
193
|
+
if (previewError) return `<div class="card muted">${esc(previewError)}</div>`;
|
|
194
|
+
if (!preview) return '<div class="card muted">Loading the previewβ¦</div>';
|
|
195
|
+
const draftId = previewDraftId();
|
|
196
|
+
const draft = draftId ? (preview.drafts || []).find((tile) => tile.id === draftId) : null;
|
|
197
|
+
if (draftId) return draftPreviewBody(draft);
|
|
198
|
+
const drafts = preview.drafts || [];
|
|
199
|
+
return `<div class="preview-layout">${phoneDevice(preview.tiles, preview.rows, open)}<div class="preview-side"><div class="card stack"><h3>How to use it</h3><p class="hint">Tap a tile to open it; βΉ Home goes back. Controls are shown, not run.</p></div>${preview.hidden.length ? `<div class="card"><h3>Hidden on this phone</h3><p class="hint">${preview.hidden.map((tile) => esc(tile.title)).join(', ')}</p></div>` : ''}${drafts.length ? `<div class="card stack"><h3>Drafts your agent previewed</h3><ul class="plain-list">${drafts.map((tile) => `<li><a href="#phone/draft/${encodeURIComponent(tile.id)}">${esc(tile.title)}</a> <span class="hint">${esc(new Date(tile.updatedAt).toLocaleTimeString())}</span></li>`).join('')}</ul></div>` : ''}</div></div>`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** A draft pack the agent previewed (packs__preview): on the home screen first, above the phone's own tiles, and opened. */
|
|
203
|
+
function draftPreviewBody(draft) {
|
|
204
|
+
if (!draft) return '<div class="card muted">That draft preview is gone (the companion restarted, or ten newer drafts replaced it). Ask your agent to preview it again.</div>';
|
|
205
|
+
const shifted = previewTiles().map((tile) => ({ ...tile, place: { ...tile.place, row: tile.place.row + draft.place.height } }));
|
|
206
|
+
const home = homeScreenHtml([{ ...draft, draft: true }, ...shifted], (preview.rows || 0) + draft.place.height);
|
|
207
|
+
return `<div class="card stack draft-banner"><strong>Draft preview: ${esc(draft.title)}</strong><p class="hint">A draft pack, not installed. Shown first on the home screen (left) and opened (right), with data from its server at ${esc(new Date(draft.updatedAt).toLocaleTimeString())}. <a href="#phone">Back to your phone</a></p></div><div class="preview-pair">${deviceHtml(home, { label: 'Home screen with the draft' })}${deviceHtml(detailHtml(draft, { back: true }), { label: 'The draft opened' })}</div>`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The draft a `#phone/draft/<id>` address shows, if any. */
|
|
211
|
+
function previewDraftId() {
|
|
212
|
+
const [head, kind, ...rest] = location.hash.slice(1).split('/');
|
|
213
|
+
return head === 'phone' && kind === 'draft' && rest.length ? decodeURIComponent(rest.join('/')) : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Re-reads the preview; only the preview's own container is redrawn, so forms elsewhere keep their input. */
|
|
217
|
+
async function loadPreview() {
|
|
218
|
+
clearTimeout(previewTimer);
|
|
219
|
+
const onPreview = section() === 'phone' || Boolean(packPageId());
|
|
220
|
+
if (!onPreview) return;
|
|
221
|
+
try {
|
|
222
|
+
preview = await api(`preview${previewDevice ? `?device=${encodeURIComponent(previewDevice)}` : ''}`);
|
|
223
|
+
previewError = null;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
previewError = error.message;
|
|
226
|
+
}
|
|
227
|
+
redrawPreview();
|
|
228
|
+
// A pack page's status (errors, needs setup) changes as its widget reads; re-read it unless the owner is typing.
|
|
229
|
+
if (packPageId() && !document.activeElement?.matches?.('input, select, textarea')) void reload(false);
|
|
230
|
+
previewTimer = setTimeout(() => void loadPreview(), PREVIEW_REFRESH_MS);
|
|
231
|
+
}
|
|
232
|
+
function redrawPreview() {
|
|
233
|
+
const library = $('#library-preview');
|
|
234
|
+
if (library && libraryPage) {
|
|
235
|
+
library.innerHTML = libraryPreviewBody(libraryPage);
|
|
236
|
+
applyPreviewLayout(library);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const target = $('#phone-preview') || $('#pack-preview');
|
|
240
|
+
if (!target) return;
|
|
241
|
+
if (target.id === 'phone-preview') target.innerHTML = phonePreviewBody(previewTiles().find((tile) => tile.id === previewOpen) || null);
|
|
242
|
+
else target.innerHTML = packPreviewBody(packPageId());
|
|
243
|
+
applyPreviewLayout(target);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function packPreviewBody(packId) {
|
|
247
|
+
const pack = state.packs.packs.find((candidate) => candidate.id === packId);
|
|
248
|
+
if (!pack?.enabled) return '<div class="card muted">Turn the integration on to see its widget here.</div>';
|
|
249
|
+
const tile = packTile(packId);
|
|
250
|
+
if (!tile) return `<div class="card muted">${preview ? 'The widget has not published yet. It appears after its first read.' : 'Loading the previewβ¦'}</div>`;
|
|
251
|
+
const only = { ...tile, place: { ...tile.place, row: 0, col: 0 } };
|
|
252
|
+
return phoneDevice([only], tile.place.height, previewOpen === tile.id ? tile : null);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const statusLabel = { ok: 'Enabled', needs_setup: 'Needs setup', error: 'Error', disabled: 'Disabled' };
|
|
256
|
+
const statusClass = { ok: 'good', needs_setup: 'warn', error: 'bad' };
|
|
257
|
+
|
|
258
|
+
/** One pack's own page: status, settings and keys, per-tool permissions, widget and voice options, setup, and its skill. */
|
|
259
|
+
function packPageView() {
|
|
260
|
+
const packId = packPageId();
|
|
261
|
+
const p = state.packs.packs.find((candidate) => candidate.id === packId);
|
|
262
|
+
if (!p) return `<section class="section"><a href="#mcp">β Integrations</a><div class="card muted">${esc(packId)} is not installed.</div></section>`;
|
|
263
|
+
const page = packPageData?.id === packId ? packPageData.data : null;
|
|
264
|
+
const settings = 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('');
|
|
265
|
+
const keys = p.secrets.map((s) => `<label>${esc(s.name)} ${s.isSet ? '<span class="pill">set</span>' : '<span class="pill warn">not set</span>'}<input data-pack-secret="${esc(s.name)}" type="password" autocomplete="new-password" placeholder="${s.isSet ? 'Leave blank to keep' : 'Paste the key; it stays in this computer\'s keychain'}"></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('');
|
|
266
|
+
const account = state.packAuth[p.id] ? `<div class="stack"><strong>Account</strong><p class="hint">${esc(accountLabel(state.packAuth[p.id]))}. Consent opens in this computer's browser; tokens stay in its keychain.</p><div class="actions"><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></div>` : '';
|
|
267
|
+
const tools = page
|
|
268
|
+
? `<table class="tool-table"><thead><tr><th>Allowed</th><th>Tool</th><th>Access</th><th>Used by</th></tr></thead><tbody>${page.tools.map((tool) => `<tr><td><input type="checkbox" data-pack-tool="${esc(tool.name)}" ${tool.allowed ? 'checked' : ''} aria-label="Allow ${esc(tool.name)}"></td><td>${code(tool.name)}${tool.description ? `<div class="hint">${esc(tool.description)}</div>` : ''}${tool.pinned.length ? `<div class="hint">Pinned to its settings: ${tool.pinned.map(esc).join(', ')}</div>` : ''}</td><td><span class="pill ${tool.access === 'write' ? 'warn' : ''}">${tool.access === 'write' ? 'changes things' : 'reads'}</span></td><td class="hint">${tool.usedBy.map(esc).join(', ') || 'The execution agent'}</td></tr>`).join('')}</tbody></table><p class="hint">A tool you turn off is never called: not by the widget, a control, or the execution agent. Turning off a tool the widget reads shows an error on the widget instead of its data.</p><div class="actions"><button class="primary" data-action="save-pack-tools" data-id="${esc(p.id)}">Save permissions</button></div><p class="hint">Network: ${esc(p.permissions.network.join(', ') || 'none declared')}</p>`
|
|
269
|
+
: '<p class="muted">Loadingβ¦</p>';
|
|
270
|
+
const refresh = p.refresh ? `<label>Refresh the widget every <select data-refresh-pack="${esc(p.id)}">${[...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>` : '';
|
|
271
|
+
// The pack's default is on for agent updates and off for others; the owner's choice replaces it.
|
|
272
|
+
const announceDefault = p.announce ? (p.announce.custom ? !p.announce.on : p.announce.on) : false;
|
|
273
|
+
const announce = p.announce ? `<label>Spoken updates during calls<select data-announce-pack="${esc(p.id)}">${[['on', 'On', true], ['off', 'Off', false]].map(([value, label, on]) => `<option value="${value}" ${p.announce.on === on ? 'selected' : ''}>${label}${announceDefault === on ? ' (default)' : ''}</option>`).join('')}</select></label><p class="hint">In a call or work mode, the voice agent tells you at the next pause when this widget reports something${page?.announces?.length ? `: ${page.announces.map(esc).join(', ')}` : ''}.</p>` : '<p class="hint">This integration has no spoken updates.</p>';
|
|
274
|
+
const controls = page?.controls.length ? `<ul class="plain-list">${page.controls.map((control) => `<li><strong>${esc(control.label)}</strong> ${control.default ? '<span class="pill">shown by default</span>' : ''} ${control.consequential ? '<span class="pill warn">asks first</span>' : ''}<div class="hint">${control.prompt ? `Says: β${esc(control.prompt)}β` : `Runs ${code(control.tool)}`}</div></li>`).join('')}</ul><p class="hint">Each phone chooses which controls appear on the widget.</p>` : '<p class="hint">No controls.</p>';
|
|
275
|
+
const setup = page ? `${page.setup.steps.length ? `<ol class="plain-steps">${page.setup.steps.map((step) => `<li>${esc(step)}</li>`).join('')}</ol>` : '<p class="hint">No setup steps.</p>'}${page.setup.prerequisites.length ? `<p class="hint">Needs: ${page.setup.prerequisites.map((item) => `${esc(item.name)}${item.found === null ? '' : item.found ? ' β' : ' (not found on this computer)'}`).join(', ')}</p>` : ''}` : '';
|
|
276
|
+
const runs = page ? `<p>${page.launch.transport === 'stdio' ? `Starts ${code(page.launch.command)}${page.ownServer ? ', the small server this pack ships' : ''}${page.launch.env.length ? ` with ${page.launch.env.map(code).join(', ')} set` : ''}.` : `Connects to ${code(page.launch.url)}.`}</p><p class="hint">Pack folder: ${code(page.folder)}. Keys and tokens are masked here and never leave this computer.</p>` : '';
|
|
277
|
+
return `<section class="section pack-page"><a href="#mcp">β Integrations</a><div class="pack-page-head"><div><h2>${esc(p.name)}</h2><p class="hint">${esc(p.author)} Β· ${esc(p.license)} Β· version ${esc(p.version)}${page?.homepage ? ` Β· <a href="${esc(page.homepage)}" target="_blank" rel="noreferrer">Homepage</a>` : ''}</p><p>${esc(p.description)}</p></div><div class="stack"><span class="badge ${statusClass[p.status.state] || ''}">${esc(statusLabel[p.status.state] || p.status.state)}</span>${p.status.detail ? `<p class="hint">${esc(packStatusDetail(p))}</p>` : ''}</div></div><div class="actions"><button data-action="toggle-pack" data-id="${esc(p.id)}" data-enabled="${p.enabled}">${p.enabled ? 'Turn off' : 'Turn on'}</button><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="check-pack" data-id="${esc(p.id)}" ${p.enabled ? '' : 'disabled'}>Check connection</button><button data-action="export-pack" data-id="${esc(p.id)}">Export</button><button data-action="customize-pack" data-id="${esc(p.id)}">Customize a copy</button><button class="danger" data-action="remove-pack" data-id="${esc(p.id)}">Remove</button></div><div id="pack-editor"></div>${packPageError ? `<div class="card muted">${esc(packPageError)}</div>` : ''}<div class="pack-page-grid"><div class="stack"><div class="card stack"><h3>Settings and keys</h3>${settings || '<p class="hint">No settings.</p>'}${keys}${settings || keys ? `<div class="actions"><button class="primary" data-action="save-pack" data-id="${esc(p.id)}">Save settings</button></div>` : ''}${account}</div><div class="card stack"><h3>Permissions</h3>${tools}</div><div class="card stack"><h3>Widget and voice</h3>${refresh}${announce}<h4>Controls</h4>${controls}</div><div class="card stack"><h3>Setup</h3>${setup}</div><div class="card stack"><h3>What it runs</h3>${runs}</div>${page?.skill ? `<details class="card"><summary><strong>Skill for the execution agent</strong></summary><p class="hint">Your agent reads this whenever the integration is on. Change it by editing ${code('SKILL.md')} in a copy of the pack, or ask your agent to.</p><pre class="skill">${esc(page.skill)}</pre></details>` : ''}</div><aside class="pack-preview"><h3>On your phone</h3><div id="pack-preview">${packPreviewBody(packId)}</div></aside></div></section>`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function loadPackPage() {
|
|
281
|
+
const packId = packPageId();
|
|
282
|
+
if (!packId) return;
|
|
283
|
+
try {
|
|
284
|
+
const loadedFor = state;
|
|
285
|
+
packPageData = { id: packId, data: await api(`pack-page?id=${encodeURIComponent(packId)}`), state: loadedFor };
|
|
286
|
+
packPageError = null;
|
|
287
|
+
} catch (error) {
|
|
288
|
+
packPageError = error.message;
|
|
289
|
+
}
|
|
290
|
+
// Redraw only if the page is still showing this pack and nothing is being typed.
|
|
291
|
+
if (packPageId() === packId && !document.activeElement?.matches?.('input, select, textarea')) {
|
|
292
|
+
render();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Called after every render: starts or stops the live preview and fetches the pack page when needed. */
|
|
297
|
+
function afterPacksRender() {
|
|
298
|
+
applyPreviewLayout();
|
|
299
|
+
const packId = packPageId();
|
|
300
|
+
// Re-read the pack page when it is new or the companion state changed (a save, a status change).
|
|
301
|
+
if (packId && (packPageData?.id !== packId || packPageData.state !== state) && !loadPackPage.running) {
|
|
302
|
+
loadPackPage.running = true;
|
|
303
|
+
void loadPackPage().finally(() => { loadPackPage.running = false; });
|
|
304
|
+
}
|
|
305
|
+
if (section() === 'phone' || packId) {
|
|
306
|
+
if (!previewTimer) void loadPreview();
|
|
307
|
+
} else {
|
|
308
|
+
clearTimeout(previewTimer);
|
|
309
|
+
previewTimer = null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
document.addEventListener('click', (event) => {
|
|
314
|
+
if (!state) return;
|
|
315
|
+
if (event.target.closest?.('[data-preview-home]')) {
|
|
316
|
+
previewOpen = null;
|
|
317
|
+
redrawPreview();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const tile = event.target.closest?.('[data-preview-tile]');
|
|
321
|
+
// A draft's page already shows it opened beside the home screen.
|
|
322
|
+
if (!tile || previewDraftId()) return;
|
|
323
|
+
previewOpen = tile.dataset.previewTile;
|
|
324
|
+
redrawPreview();
|
|
325
|
+
});
|
|
326
|
+
document.addEventListener('change', (event) => {
|
|
327
|
+
if (event.target.matches?.('[data-preview-device]')) {
|
|
328
|
+
previewDevice = event.target.value;
|
|
329
|
+
preview = null;
|
|
330
|
+
redrawPreview();
|
|
331
|
+
void loadPreview();
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
document.addEventListener('change', (event) => {
|
|
336
|
+
if (!event.target.matches?.('[data-library-confirm]')) return;
|
|
337
|
+
libraryConfirmed = event.target.checked;
|
|
338
|
+
const install = document.querySelector('.library-install [data-action="install-library-pack"]');
|
|
339
|
+
if (install) install.disabled = !libraryConfirmed;
|
|
340
|
+
});
|
package/src/manage/setup.js
CHANGED
|
@@ -7,10 +7,6 @@ const SETUP_STEPS = [
|
|
|
7
7
|
{ id: 'phone', title: 'Pair your phone', short: 'Pair phone' },
|
|
8
8
|
{ id: 'done', title: 'All set', short: 'Finish' },
|
|
9
9
|
];
|
|
10
|
-
/** Recommended on a first setup; the owner can untick any of them. */
|
|
11
|
-
const DEFAULT_INTEGRATIONS = new Set(['t3-agents', 'obsidian']);
|
|
12
|
-
/** Catalog packs offered in setup; T3 Agents is only offered once the app is on this computer. */
|
|
13
|
-
const SETUP_INTEGRATIONS = ['t3-agents', 'obsidian', 'calendar', 'gmail'];
|
|
14
10
|
let setupSelections = null;
|
|
15
11
|
let setupBusy = false;
|
|
16
12
|
|
|
@@ -74,7 +70,7 @@ function setupNav(current, { next = 'Continue', nextAction = 'setup-next', skip
|
|
|
74
70
|
return `<div class="setup-nav">${index > 0 ? `<button data-action="setup-step" data-step="${SETUP_STEPS[index - 1].id}">Back</button>` : '<span></span>'}<div class="actions">${skip ? '<button data-action="setup-next">Skip for now</button>' : ''}<button class="primary" data-action="${nextAction}">${esc(next)}</button></div></div>`;
|
|
75
71
|
}
|
|
76
72
|
function appStep() {
|
|
77
|
-
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>
|
|
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></ol></div></div>${setupNav('app', { next: 'I have the app', nextAction: 'setup-app-done', skip: false })}`;
|
|
78
74
|
}
|
|
79
75
|
function executionStep() {
|
|
80
76
|
if (!state.providers || state.connector.kind === 'memory')
|
|
@@ -92,22 +88,25 @@ function voiceStep() {
|
|
|
92
88
|
// Same controls and ids as the Voice page, so its provider/model handlers and the voice sample work here too.
|
|
93
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 })}`;
|
|
94
90
|
}
|
|
91
|
+
/** Library packs whose app is on this computer are suggested; the owner can untick any of them. */
|
|
95
92
|
function integrationSelections() {
|
|
96
93
|
if (!setupSelections) {
|
|
97
|
-
setupSelections = new Set(
|
|
94
|
+
setupSelections = new Set(state.integrationCatalog.filter((entry) => entry.detected && !entry.installed).map((entry) => entry.id));
|
|
98
95
|
}
|
|
99
96
|
return setupSelections;
|
|
100
97
|
}
|
|
101
98
|
function integrationsStep() {
|
|
102
99
|
const selected = integrationSelections();
|
|
103
100
|
const option = (entry) => {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
const note = entry.installed ? 'Added' : t3?.note || entry.account;
|
|
107
|
-
return `<label class="integration-option ${unavailable ? 'unavailable' : ''}"><input type="checkbox" data-integration="${esc(entry.id)}" ${entry.installed ? 'checked disabled' : unavailable ? 'disabled' : selected.has(entry.id) ? 'checked' : ''}><span><strong>${esc(entry.name)}</strong>${DEFAULT_INTEGRATIONS.has(entry.id) ? ' <span class="pill">Recommended</span>' : ''}<br><span class="hint">${esc(entry.description)}</span><br><span class="hint">${esc(note)}</span></span></label>`;
|
|
101
|
+
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>`;
|
|
108
103
|
};
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
// Apps found on this computer first, then the rest of the library.
|
|
105
|
+
const packs = [...state.integrationCatalog].sort((a, b) => Number(b.detected) - Number(a.detected));
|
|
106
|
+
const empty = state.libraryError
|
|
107
|
+
? `<p class="hint">The widget library could not be reached (${esc(state.libraryError)}). You can add integrations later under <a href="#mcp">MCP & widgets</a>, or ask your agent to build one.</p>`
|
|
108
|
+
: '<p class="hint">The widget library has no packs yet.</p>';
|
|
109
|
+
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' })}`;
|
|
111
110
|
}
|
|
112
111
|
function phoneStep() {
|
|
113
112
|
const active = state.devices.filter((device) => !device.revokedAt);
|
|
@@ -178,7 +177,7 @@ async function handleSetupAction(action, button) {
|
|
|
178
177
|
// Each pack installs disabled and starts the execution agent on its setup, the same as from MCP & widgets.
|
|
179
178
|
for (const id of chosen) {
|
|
180
179
|
try {
|
|
181
|
-
const result = await api('pack/install-
|
|
180
|
+
const result = await api('pack/install-library', { id });
|
|
182
181
|
if (!result.setup.started) failures.push(`${result.name}: ${result.setup.note}`);
|
|
183
182
|
} catch (error) {
|
|
184
183
|
failures.push(`${state.integrationCatalog.find((entry) => entry.id === id)?.name || id}: ${error.message}`);
|
package/packs/BRAND_ASSETS.md
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# Integration logos
|
|
2
|
-
|
|
3
|
-
The Gmail and Google Calendar `logo.png` files identify their respective integrations. They are rendered at 192 px from Google's official product assets, retrieved 2026-09-23:
|
|
4
|
-
|
|
5
|
-
- https://www.gstatic.com/images/branding/productlogos/gmail_2026/v2/web/192px.svg
|
|
6
|
-
- https://www.gstatic.com/images/branding/productlogos/calendar_2026/v2/web/192px.svg
|
|
7
|
-
|
|
8
|
-
Google retains its trademarks and rights in these logos; they are not covered by this repository's MIT license. Their use does not imply Google endorsement. The companion serves the small raster files by content hash over the paired connection; the mobile widget JSON contains only that hash.
|
package/packs/calendar/logo.png
DELETED
|
Binary file
|
package/packs/calendar/pack.json
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schemaVersion": 1,
|
|
3
|
-
"id": "dev.remotevoiceagent.calendar",
|
|
4
|
-
"name": "Calendar",
|
|
5
|
-
"description": "Upcoming events and calendar availability. Read-only Google API access stays on your computer.",
|
|
6
|
-
"version": "1.0.2",
|
|
7
|
-
"author": {
|
|
8
|
-
"name": "Sidebud"
|
|
9
|
-
},
|
|
10
|
-
"license": "MIT",
|
|
11
|
-
"logo": {
|
|
12
|
-
"kind": "file",
|
|
13
|
-
"path": "logo.png"
|
|
14
|
-
},
|
|
15
|
-
"tile": {
|
|
16
|
-
"size": "medium"
|
|
17
|
-
},
|
|
18
|
-
"mcp": {
|
|
19
|
-
"transport": "builtin",
|
|
20
|
-
"service": "calendar"
|
|
21
|
-
},
|
|
22
|
-
"auth": {
|
|
23
|
-
"type": "oauth2",
|
|
24
|
-
"authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
|
|
25
|
-
"tokenUrl": "https://oauth2.googleapis.com/token",
|
|
26
|
-
"scopes": [
|
|
27
|
-
"https://www.googleapis.com/auth/calendar.calendarlist.readonly",
|
|
28
|
-
"https://www.googleapis.com/auth/calendar.events.readonly",
|
|
29
|
-
"https://www.googleapis.com/auth/calendar.events.freebusy"
|
|
30
|
-
],
|
|
31
|
-
"clientIdSecret": "GOOGLE_OAUTH_CLIENT_ID",
|
|
32
|
-
"clientSecretSecret": "GOOGLE_OAUTH_CLIENT_SECRET",
|
|
33
|
-
"authorizationParams": {
|
|
34
|
-
"access_type": "offline",
|
|
35
|
-
"prompt": "consent"
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"secrets": [
|
|
39
|
-
{
|
|
40
|
-
"name": "GOOGLE_OAUTH_CLIENT_ID",
|
|
41
|
-
"description": "Desktop OAuth client ID with Calendar API enabled"
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"name": "GOOGLE_OAUTH_CLIENT_SECRET",
|
|
45
|
-
"description": "Desktop OAuth client secret"
|
|
46
|
-
}
|
|
47
|
-
],
|
|
48
|
-
"permissions": {
|
|
49
|
-
"tools": [
|
|
50
|
-
{
|
|
51
|
-
"name": "list_calendars",
|
|
52
|
-
"access": "read",
|
|
53
|
-
"description": "List calendars"
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"name": "list_events",
|
|
57
|
-
"access": "read",
|
|
58
|
-
"description": "Read upcoming events"
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"name": "get_event",
|
|
62
|
-
"access": "read",
|
|
63
|
-
"description": "Read one event"
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"name": "get_free_busy",
|
|
67
|
-
"access": "read",
|
|
68
|
-
"description": "Read availability"
|
|
69
|
-
}
|
|
70
|
-
],
|
|
71
|
-
"network": [
|
|
72
|
-
"www.googleapis.com",
|
|
73
|
-
"accounts.google.com",
|
|
74
|
-
"oauth2.googleapis.com"
|
|
75
|
-
]
|
|
76
|
-
},
|
|
77
|
-
"settings": [
|
|
78
|
-
{
|
|
79
|
-
"id": "calendarId",
|
|
80
|
-
"label": "Calendar ID",
|
|
81
|
-
"type": "text",
|
|
82
|
-
"default": "primary",
|
|
83
|
-
"description": "Use primary for your main calendar."
|
|
84
|
-
}
|
|
85
|
-
],
|
|
86
|
-
"data": [
|
|
87
|
-
{
|
|
88
|
-
"id": "upcoming",
|
|
89
|
-
"tool": "list_events",
|
|
90
|
-
"arguments": {
|
|
91
|
-
"calendarId": "{{setting.calendarId}}",
|
|
92
|
-
"pageSize": 20
|
|
93
|
-
},
|
|
94
|
-
"refreshSeconds": 60
|
|
95
|
-
}
|
|
96
|
-
],
|
|
97
|
-
"glance": {
|
|
98
|
-
"stats": [
|
|
99
|
-
{
|
|
100
|
-
"label": "Next 7 days",
|
|
101
|
-
"value": "{{upcoming/events|count}}",
|
|
102
|
-
"tone": "info"
|
|
103
|
-
}
|
|
104
|
-
]
|
|
105
|
-
},
|
|
106
|
-
"blocks": [
|
|
107
|
-
{
|
|
108
|
-
"type": "list",
|
|
109
|
-
"items": "{{upcoming/events}}",
|
|
110
|
-
"item": {
|
|
111
|
-
"title": "{{item/summary}}",
|
|
112
|
-
"subtitle": "{{item/startsAt}}"
|
|
113
|
-
},
|
|
114
|
-
"limit": 10,
|
|
115
|
-
"emptyText": "No upcoming events."
|
|
116
|
-
}
|
|
117
|
-
],
|
|
118
|
-
"controls": [
|
|
119
|
-
{
|
|
120
|
-
"id": "agenda",
|
|
121
|
-
"kind": "prompt",
|
|
122
|
-
"label": "Read my agenda",
|
|
123
|
-
"prompt": "Summarize my upcoming calendar events.",
|
|
124
|
-
"default": true
|
|
125
|
-
}
|
|
126
|
-
]
|
|
127
|
-
}
|