heyquilt 0.0.1 → 0.3.90
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/LICENSE +85 -0
- package/README.md +630 -2
- package/RELEASES.md +733 -0
- package/assets/logo.svg +1 -0
- package/bin/quilt.js +816 -0
- package/desktop/main.js +315 -0
- package/package.json +163 -7
- package/src/account.js +563 -0
- package/src/admit-policy.js +32 -0
- package/src/agent-join.js +370 -0
- package/src/agent-task-workflow.js +176 -0
- package/src/agents/actions.js +85 -0
- package/src/agents/claude-code.js +180 -0
- package/src/agents/common.js +21 -0
- package/src/agents/cursor-transcripts.js +170 -0
- package/src/agents/cursor.js +404 -0
- package/src/agents/index.js +69 -0
- package/src/ai-vm-signin.js +98 -0
- package/src/ai-vm.js +380 -0
- package/src/api/access.js +75 -0
- package/src/api/activity.js +181 -0
- package/src/api/agent-auth.js +130 -0
- package/src/api/agent-placement.js +157 -0
- package/src/api/audit.js +83 -0
- package/src/api/auth.js +17 -0
- package/src/api/domains.js +30 -0
- package/src/api/file-paths.js +40 -0
- package/src/api/file-store.js +99 -0
- package/src/api/fly-machines.js +76 -0
- package/src/api/github-app.js +172 -0
- package/src/api/http.js +34 -0
- package/src/api/invite-email.js +53 -0
- package/src/api/issues.js +110 -0
- package/src/api/join-text.js +78 -0
- package/src/api/mailer.js +47 -0
- package/src/api/memory-store.js +1083 -0
- package/src/api/org-access.js +54 -0
- package/src/api/permissions.js +71 -0
- package/src/api/relay-cluster.js +251 -0
- package/src/api/relay-machines.js +60 -0
- package/src/api/routes/access-types.js +54 -0
- package/src/api/routes/agent-invites.js +115 -0
- package/src/api/routes/agents.js +157 -0
- package/src/api/routes/ai-vm.js +369 -0
- package/src/api/routes/github.js +380 -0
- package/src/api/routes/grants.js +70 -0
- package/src/api/routes/invites.js +188 -0
- package/src/api/routes/issues.js +47 -0
- package/src/api/routes/join.js +167 -0
- package/src/api/routes/members.js +110 -0
- package/src/api/routes/orgs.js +177 -0
- package/src/api/routes/relay-cluster.js +90 -0
- package/src/api/routes/relay.js +76 -0
- package/src/api/routes/session-invites.js +119 -0
- package/src/api/routes/sessions.js +92 -0
- package/src/api/routes/teams.js +132 -0
- package/src/api/routes/workspace-agents.js +332 -0
- package/src/api/routes/workspace-files.js +277 -0
- package/src/api/routes/workspace-invites.js +196 -0
- package/src/api/routes/workspaces.js +225 -0
- package/src/api/server.js +623 -0
- package/src/api/slugs.js +20 -0
- package/src/api/supabase-store.js +765 -0
- package/src/api/team-access.js +30 -0
- package/src/api/tokens.js +31 -0
- package/src/api/workspace-access.js +62 -0
- package/src/api/workspace-reach.js +60 -0
- package/src/autowork-tools.js +166 -0
- package/src/autowork.js +387 -0
- package/src/blobstore.js +99 -0
- package/src/boxes.js +37 -0
- package/src/branchdocs.js +235 -0
- package/src/branches.js +194 -0
- package/src/catchup.js +111 -0
- package/src/chat-archive.js +85 -0
- package/src/chat-links.js +923 -0
- package/src/clash.js +140 -0
- package/src/cloud-ai.js +50 -0
- package/src/commit-request.js +59 -0
- package/src/commit.js +213 -0
- package/src/connection.js +569 -0
- package/src/control.js +245 -0
- package/src/conversation.js +125 -0
- package/src/doctor.js +156 -0
- package/src/duties.js +303 -0
- package/src/editors.js +228 -0
- package/src/fsutil.js +87 -0
- package/src/git-hook.js +48 -0
- package/src/git.js +147 -0
- package/src/github-commit.js +320 -0
- package/src/gitignore.js +49 -0
- package/src/gitstate.js +900 -0
- package/src/history.js +307 -0
- package/src/hooks.js +371 -0
- package/src/hosted-agents.js +24 -0
- package/src/hosted-client.js +112 -0
- package/src/identity.js +99 -0
- package/src/inbox.js +237 -0
- package/src/integrations.js +221 -0
- package/src/key-cli.js +115 -0
- package/src/largefiles.js +91 -0
- package/src/legacy.js +25 -0
- package/src/mcp-proxy.js +40 -0
- package/src/mcp-room.js +32 -0
- package/src/mcp.js +1261 -0
- package/src/merge3.js +158 -0
- package/src/merges.js +148 -0
- package/src/pass-source.js +176 -0
- package/src/passes.js +64 -0
- package/src/pathrules.js +127 -0
- package/src/persona.js +101 -0
- package/src/presence.js +360 -0
- package/src/private-file.js +41 -0
- package/src/procs.js +70 -0
- package/src/project-rules.js +178 -0
- package/src/protocol.js +92 -0
- package/src/quiet-warnings.js +9 -0
- package/src/relay-ai-vm.js +82 -0
- package/src/relay-ask.js +141 -0
- package/src/relay-ci.js +111 -0
- package/src/relay-clash.js +597 -0
- package/src/relay-cluster.js +454 -0
- package/src/relay-commit.js +887 -0
- package/src/relay-filekey.js +46 -0
- package/src/relay-git.js +274 -0
- package/src/relay-githttp.js +137 -0
- package/src/relay-github.js +109 -0
- package/src/relay-lease.js +137 -0
- package/src/relay-lfs.js +37 -0
- package/src/relay-mcp.js +1679 -0
- package/src/relay-metrics.js +75 -0
- package/src/relay-outbound.js +1144 -0
- package/src/relay-sync.js +719 -0
- package/src/relay-turn-sync.js +307 -0
- package/src/relay-upstream.js +934 -0
- package/src/relay-webhooks.js +111 -0
- package/src/releases.js +131 -0
- package/src/report.js +129 -0
- package/src/runner.js +306 -0
- package/src/same-edit.js +18 -0
- package/src/server.js +4187 -0
- package/src/session-access.js +170 -0
- package/src/session-name.js +15 -0
- package/src/session.js +7361 -0
- package/src/settings.js +46 -0
- package/src/setup.js +184 -0
- package/src/shell-path.js +46 -0
- package/src/status.js +199 -0
- package/src/summarize.js +109 -0
- package/src/sync-words.js +42 -0
- package/src/task-comments.js +92 -0
- package/src/tasks.js +670 -0
- package/src/textdiff.js +20 -0
- package/src/turn-sync-plan.js +142 -0
- package/src/turn-sync.js +191 -0
- package/src/ui/access-form.js +37 -0
- package/src/ui/agent-env-key.js +76 -0
- package/src/ui/agent-guide.js +127 -0
- package/src/ui/agent-kinds.js +175 -0
- package/src/ui/agent-place.js +265 -0
- package/src/ui/app.css +2309 -0
- package/src/ui/app.js +542 -0
- package/src/ui/board.js +465 -0
- package/src/ui/boxes.js +76 -0
- package/src/ui/branches.js +111 -0
- package/src/ui/catchup.js +77 -0
- package/src/ui/changes.js +159 -0
- package/src/ui/chat.js +266 -0
- package/src/ui/cloud-ai-model.js +107 -0
- package/src/ui/cloud-ai.js +221 -0
- package/src/ui/common.js +431 -0
- package/src/ui/csv.js +21 -0
- package/src/ui/diffview.js +74 -0
- package/src/ui/feed-convs.js +39 -0
- package/src/ui/feed.js +174 -0
- package/src/ui/files.js +579 -0
- package/src/ui/fileview.js +123 -0
- package/src/ui/git-rule.js +60 -0
- package/src/ui/home.js +1323 -0
- package/src/ui/index.html +16 -0
- package/src/ui/invite.js +97 -0
- package/src/ui/invites.js +79 -0
- package/src/ui/loom-model.js +516 -0
- package/src/ui/loom.css +349 -0
- package/src/ui/loom.js +753 -0
- package/src/ui/mark.js +104 -0
- package/src/ui/merges.js +158 -0
- package/src/ui/rail-model.js +78 -0
- package/src/ui/rail.js +173 -0
- package/src/ui/releases.js +182 -0
- package/src/ui/schedule.js +156 -0
- package/src/ui/session.js +3394 -0
- package/src/ui/signin.js +75 -0
- package/src/ui/sounds.js +110 -0
- package/src/ui/tool-logo.js +86 -0
- package/src/ui/tree.js +276 -0
- package/src/ui/workspaces.js +542 -0
- package/src/ui-server.js +1272 -0
- package/src/update-check.js +67 -0
- package/src/upstream.js +155 -0
- package/src/webhooks.js +235 -0
- package/src/workspace-cli.js +62 -0
- package/src/workspace-tools.js +340 -0
- package/index.js +0 -2
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Settings → Your AI, a cloud AI (docs/superpowers/specs/2026-10-17-ai-vm.md): what the card
|
|
2
|
+
// shows for the person's cloud AI as the API reports it. Pure, so it can be tested without a browser.
|
|
3
|
+
|
|
4
|
+
/** The tools a cloud AI runs, and how each signs in. Only Claude asks for a code pasted back. */
|
|
5
|
+
export const CLOUD_TOOLS = [
|
|
6
|
+
{ tool: 'Claude Code', short: 'Claude', account: 'Claude', pastes: true },
|
|
7
|
+
{ tool: 'Codex', short: 'Codex', account: 'ChatGPT', pastes: false },
|
|
8
|
+
{ tool: 'Cursor', short: 'Cursor', account: 'Cursor', pastes: false }
|
|
9
|
+
]
|
|
10
|
+
export const cloudTool = (tool) => CLOUD_TOOLS.find((t) => t.tool === tool) || { tool, short: tool || 'Your tool', account: tool || 'your tool', pastes: false }
|
|
11
|
+
|
|
12
|
+
/** Which face the card shows: Connect ('none'), the sign-in steps ('signin'), or the cloud AI ('ready'). */
|
|
13
|
+
export function cloudPhase (vm) {
|
|
14
|
+
if (!vm) return 'none'
|
|
15
|
+
return vm.signedIn ? 'ready' : 'signin'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const STARTING = ['creating', 'starting']
|
|
19
|
+
|
|
20
|
+
/** The status pill: a label and its tone (ok, sleep, warn, bad or none). */
|
|
21
|
+
export function cloudStatus (vm) {
|
|
22
|
+
if (vm.state === 'failed') return { label: 'Failed', tone: 'bad' }
|
|
23
|
+
if (!vm.signedIn) {
|
|
24
|
+
if (vm.signIn && vm.signIn.step === 'failed') return { label: 'Sign-in failed', tone: 'bad' }
|
|
25
|
+
if (STARTING.includes(vm.state) && !vm.signIn) return { label: 'Starting', tone: 'warn' }
|
|
26
|
+
return { label: 'Signing in', tone: 'warn' }
|
|
27
|
+
}
|
|
28
|
+
if (vm.state === 'running') return { label: 'Running', tone: 'ok' }
|
|
29
|
+
if (vm.state === 'suspended') return { label: 'Asleep', tone: 'sleep' }
|
|
30
|
+
if (STARTING.includes(vm.state)) return { label: 'Starting', tone: 'warn' }
|
|
31
|
+
return { label: 'Stopped', tone: '' }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The numbered sign-in steps: the machine starting, the tool's own sign-in page (with its device
|
|
36
|
+
* code, when it has one), then the code pasted back (Claude) or the tool finishing by itself.
|
|
37
|
+
* Each is { key, title, hint, done, now, ... }.
|
|
38
|
+
*/
|
|
39
|
+
export function signInSteps (vm) {
|
|
40
|
+
const t = cloudTool(vm.tool)
|
|
41
|
+
const s = vm.signIn || null
|
|
42
|
+
const step = s ? s.step : ''
|
|
43
|
+
const up = !STARTING.includes(vm.state) || !!s
|
|
44
|
+
const machine = {
|
|
45
|
+
key: 'machine',
|
|
46
|
+
title: vm.state === 'failed' ? "Your machine didn't start" : up ? 'Machine started' : 'Starting your machine',
|
|
47
|
+
hint: vm.state === 'failed' ? '' : up ? (vm.region ? `In ${vm.region}` : '') : 'A minute or so, the first time.',
|
|
48
|
+
error: vm.state === 'failed' ? (vm.error || 'It stopped.') : '',
|
|
49
|
+
done: up && vm.state !== 'failed',
|
|
50
|
+
now: !up || vm.state === 'failed'
|
|
51
|
+
}
|
|
52
|
+
const shown = step === 'link' || step === 'code'
|
|
53
|
+
const page = {
|
|
54
|
+
key: 'page',
|
|
55
|
+
title: shown
|
|
56
|
+
? (s.code ? `Open ${t.short}'s sign-in page and enter this code` : `Open ${t.short}'s sign-in page and approve`)
|
|
57
|
+
: step === 'failed' ? `${t.short}'s sign-in stopped` : (step === 'waiting' || step === 'done') ? `Approved on ${t.short}'s page` : `${t.short}'s sign-in`,
|
|
58
|
+
hint: up && vm.state !== 'failed' && (!step || step === 'starting') ? `Starting ${t.short}'s own sign-in on the machine…` : '',
|
|
59
|
+
url: shown ? s.url || '' : '',
|
|
60
|
+
code: shown ? s.code || '' : '',
|
|
61
|
+
error: step === 'failed' ? (s.error || 'It stopped; start it again.') : '',
|
|
62
|
+
done: step === 'waiting' || step === 'done',
|
|
63
|
+
now: up && vm.state !== 'failed' && !(step === 'waiting' || step === 'done')
|
|
64
|
+
}
|
|
65
|
+
if (t.pastes) {
|
|
66
|
+
const canPaste = shown && s.needsCode === true
|
|
67
|
+
return [machine, page, {
|
|
68
|
+
key: 'paste',
|
|
69
|
+
title: step === 'waiting' ? `Checking the code with ${t.short}…` : `Paste the code ${t.short} shows you`,
|
|
70
|
+
hint: `Used once by ${t.short} on your machine, never kept.`,
|
|
71
|
+
canPaste,
|
|
72
|
+
done: step === 'done',
|
|
73
|
+
now: canPaste || step === 'waiting'
|
|
74
|
+
}]
|
|
75
|
+
}
|
|
76
|
+
return [machine, page, {
|
|
77
|
+
key: 'finish',
|
|
78
|
+
title: step === 'waiting' ? `${t.short} is finishing the sign-in…` : `${t.short} finishes by itself once you approve`,
|
|
79
|
+
hint: '',
|
|
80
|
+
done: step === 'done',
|
|
81
|
+
now: step === 'waiting'
|
|
82
|
+
}]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** "182k", "9.5k", "2.5M": tokens, short. */
|
|
86
|
+
export function shortTokens (n) {
|
|
87
|
+
const x = Math.max(0, Math.floor(Number(n) || 0))
|
|
88
|
+
const trim = (v) => String(Number(v.toFixed(1)))
|
|
89
|
+
if (x < 1000) return String(x)
|
|
90
|
+
if (x < 10000) return `${trim(x / 1000)}k`
|
|
91
|
+
if (x < 1e6) return `${Math.round(x / 1000)}k`
|
|
92
|
+
return `${trim(x / 1e6)}M`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Today's use: tokens and runs, and how much of the daily cap that is (pct null with no cap). */
|
|
96
|
+
export function cloudUsage (vm) {
|
|
97
|
+
const u = vm.usageToday || {}
|
|
98
|
+
const tokens = Number(u.tokens) || 0
|
|
99
|
+
const cap = Number(vm.cap) || 0
|
|
100
|
+
return {
|
|
101
|
+
tokens: shortTokens(tokens),
|
|
102
|
+
runs: Number(u.runs) || 0,
|
|
103
|
+
pct: cap > 0 ? Math.min(100, Math.round((tokens / cap) * 100)) : null,
|
|
104
|
+
cap: cap > 0 ? shortTokens(cap) : '',
|
|
105
|
+
over: cap > 0 && tokens >= cap
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Settings → Your AI: your AI on its own cloud machine, as one card that grows
|
|
2
|
+
// (docs/mockups/2026-10-17-ai-vm.html, option A). Three Connect tiles; then the tool's own
|
|
3
|
+
// sign-in as numbered steps, in place; then the cloud AI itself: its status, sessions, today's
|
|
4
|
+
// use and its settings. Disconnect asks again inside the card. The card follows the API's long
|
|
5
|
+
// poll (GET /api/cloud-ai?wait=<version>) while it is on screen.
|
|
6
|
+
import { I, state, $, esc, toast, api, avatar, loadingNote } from './common.js'
|
|
7
|
+
import { toolLogo } from './tool-logo.js'
|
|
8
|
+
import { CLOUD_TOOLS, cloudTool, cloudPhase, cloudStatus, signInSteps, cloudUsage } from './cloud-ai-model.js'
|
|
9
|
+
|
|
10
|
+
const TITLE = 'Your AI on a cloud machine'
|
|
11
|
+
|
|
12
|
+
/** The card's frame; bindCloudAi fills it. */
|
|
13
|
+
export function cloudAiCardHtml () {
|
|
14
|
+
return `<section class="card settings-sec cloud-ai" id="cloud-ai-sec" aria-live="polite">
|
|
15
|
+
<div class="sec-intro"><h2>${TITLE}</h2><p>Your own AI tool, signed in with your own account, on a machine of yours that is always there.</p></div>
|
|
16
|
+
<div class="sec-body"><p class="hint">${loadingNote()}</p></div>
|
|
17
|
+
</section>`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const pill = (s) => `<span class="pill cai-pill ${s.tone}"><span class="dot"></span>${esc(s.label)}</span>`
|
|
21
|
+
|
|
22
|
+
function connectHtml (ui) {
|
|
23
|
+
return `<div class="sec-intro"><h2>${TITLE}</h2><p>Your own AI tool, signed in with your own account, on a machine of yours that is always there. It answers for your AI when none of your AI sessions is open.</p></div>
|
|
24
|
+
<div class="sec-body cai-body">
|
|
25
|
+
<div class="cai-tools">${CLOUD_TOOLS.map((t, i) => `<div class="cai-tool">
|
|
26
|
+
${toolLogo(t.tool, { className: 'cai-logo' })}
|
|
27
|
+
<b>${esc(t.tool)}</b><span class="hint">Sign in with ${esc(t.account)}</span>
|
|
28
|
+
<button class="btn sm${i === 0 ? ' primary' : ''}" type="button" data-cloud-connect="${esc(t.tool)}" ${ui.busy ? 'disabled' : ''}>${ui.busy === t.tool ? loadingNote('Starting…') : `Connect ${esc(t.short)}`}</button>
|
|
29
|
+
</div>`).join('')}</div>
|
|
30
|
+
${ui.error ? `<p class="error">${esc(ui.error)}</p>` : ''}
|
|
31
|
+
<p class="hint">Included in every plan for now. Quilt never sees your Claude, ChatGPT or Cursor login, and your tool's own plan pays for what it uses.</p>
|
|
32
|
+
</div>`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stepHtml (s) {
|
|
36
|
+
const body = []
|
|
37
|
+
// While the tool checks a pasted code there is nothing more to type.
|
|
38
|
+
const pasting = s.key === 'paste' && (s.canPaste || !s.now)
|
|
39
|
+
if (s.hint && (s.key !== 'paste' || pasting)) body.push(`<span class="hint">${esc(s.hint)}</span>`)
|
|
40
|
+
if (s.error) body.push(`<span class="error">${esc(s.error)}</span>`)
|
|
41
|
+
if (s.code) body.push(`<div class="cai-code" aria-label="Code">${esc(s.code)}</div>`)
|
|
42
|
+
if (s.url) {
|
|
43
|
+
body.push(`<div class="cai-link">${esc(s.url)}</div>
|
|
44
|
+
<div class="cai-two"><button class="btn primary" type="button" data-cloud-open>${I.external}<span>Open sign-in page</span></button><button class="btn" type="button" data-cloud-copy>${I.copy}<span>Copy link</span></button></div>`)
|
|
45
|
+
}
|
|
46
|
+
if (pasting) {
|
|
47
|
+
body.push(`<form class="row cai-paste" id="cloud-code-form" autocomplete="off">
|
|
48
|
+
<input class="input grow mono" name="code" placeholder="Paste the code here" aria-label="The code from the sign-in page" ${s.canPaste ? '' : 'disabled'}>
|
|
49
|
+
<button class="btn" type="submit" ${s.canPaste ? '' : 'disabled'}>Send</button>
|
|
50
|
+
</form>`)
|
|
51
|
+
}
|
|
52
|
+
return `<li class="cai-step${s.done ? ' done' : ''}${s.now ? ' now' : ''}${s.error ? ' bad' : ''}"><div><b>${esc(s.title)}</b>${body.join('')}</div></li>`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function confirmHtml (vm) {
|
|
56
|
+
return `<div class="cai-confirm" role="group" aria-label="Disconnect your cloud AI?">
|
|
57
|
+
<p><b>Disconnect your cloud AI?</b> Its machine and disk are deleted, with any work it hasn't pushed. Your ${esc(cloudTool(vm.tool).account)} account itself is not touched.</p>
|
|
58
|
+
<div class="cai-two"><button class="btn" type="button" data-cloud-keep>Keep it</button><button class="btn danger" type="button" data-cloud-delete>${I.trash}<span>Delete machine and disk</span></button></div>
|
|
59
|
+
</div>`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function signInHtml (vm, ui) {
|
|
63
|
+
const t = cloudTool(vm.tool)
|
|
64
|
+
// A machine that didn't start is started again; otherwise the tool's sign-in is.
|
|
65
|
+
const again = vm.state === 'failed'
|
|
66
|
+
? `<button class="btn" type="button" data-cloud-wake ${ui.busy === 'wake' ? 'disabled' : ''}>${I.refresh}<span>Start the machine again</span></button>`
|
|
67
|
+
: `<button class="btn" type="button" data-cloud-restart ${ui.busy === 'restart' ? 'disabled' : ''}>${I.refresh}<span>Start the sign-in again</span></button>`
|
|
68
|
+
return `<div class="sec-intro"><h2>Sign ${esc(vm.tool)} in on your cloud machine</h2><p>${esc(t.short)}'s own sign-in, running on the machine. Quilt shows you its steps.</p>${pill(cloudStatus(vm))}</div>
|
|
69
|
+
<div class="sec-body cai-body">
|
|
70
|
+
<ol class="cai-steps">${signInSteps(vm).map(stepHtml).join('')}</ol>
|
|
71
|
+
${ui.error ? `<p class="error">${esc(ui.error)}</p>` : ''}
|
|
72
|
+
${ui.confirming ? confirmHtml(vm) : `<div class="cai-two">${again}<button class="btn danger" type="button" data-cloud-disconnect>Disconnect…</button></div>`}
|
|
73
|
+
</div>`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readyHtml (vm, ui) {
|
|
77
|
+
const st = cloudStatus(vm)
|
|
78
|
+
const u = cloudUsage(vm)
|
|
79
|
+
const me = state.profile && state.profile.name ? state.profile.name.split(/\s+/)[0] : 'your'
|
|
80
|
+
const online = vm.state === 'running' ? true : vm.state === 'suspended' ? 'away' : false
|
|
81
|
+
const sleeping = ['suspended', 'stopped', 'failed'].includes(vm.state)
|
|
82
|
+
const sessions = (vm.sessions || []).length
|
|
83
|
+
? `<div class="cai-sessions">${vm.sessions.map((s) => `<div class="cai-sess"><span class="grow"><b>${esc(s.name || s.room)}</b></span><span class="hint">joined</span></div>`).join('')}</div>`
|
|
84
|
+
: '<p class="hint">In no session yet. It joins the sessions you join, and stays when you leave for the day.</p>'
|
|
85
|
+
return `<div class="sec-intro"><h2>${TITLE}</h2><p>It answers for your AI when none of your AI sessions is open: tasks, @mentions and messages, while your computer is closed.</p></div>
|
|
86
|
+
<div class="sec-body cai-body">
|
|
87
|
+
<div class="cai-who">${avatar(vm.name || 'AI', state.profile && state.profile.color, online)}
|
|
88
|
+
<div class="grow"><b>${esc(vm.name || 'Cloud AI')}</b><span class="hint">${esc(vm.tool)} on your cloud machine · one of ${esc(me === 'your' ? 'your' : `${me}'s`)} AI</span></div>
|
|
89
|
+
${pill(st)}
|
|
90
|
+
${sleeping ? `<button class="btn sm" type="button" data-cloud-wake ${ui.busy === 'wake' ? 'disabled' : ''}>${vm.state === 'suspended' ? 'Wake now' : 'Start it again'}</button>` : ''}
|
|
91
|
+
</div>
|
|
92
|
+
${vm.state === 'failed' && vm.error ? `<p class="error">${esc(vm.error)}</p>` : ''}
|
|
93
|
+
${vm.state === 'suspended' ? '<p class="hint">Asleep while there is nothing to do. It wakes by itself on a task, an @mention or a message.</p>' : ''}
|
|
94
|
+
${sessions}
|
|
95
|
+
<div class="cai-kv">
|
|
96
|
+
<div><b>${esc(u.tokens)}</b><span>tokens today</span></div>
|
|
97
|
+
<div><b>${u.runs}</b><span>run${u.runs === 1 ? '' : 's'} today</span></div>
|
|
98
|
+
<div><b>${u.pct === null ? 'No cap' : `${u.pct}%`}</b><span>${u.pct === null ? 'set a daily cap below' : `of ${esc(u.cap)} daily cap`}</span></div>
|
|
99
|
+
</div>
|
|
100
|
+
${u.pct === null ? '' : `<div class="cai-meter${u.over ? ' over' : ''}" role="meter" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${u.pct}" aria-label="Today's use of the daily cap"><i style="width:${u.pct}%"></i></div>`}
|
|
101
|
+
${u.over ? '<p class="hint warn">The daily cap is reached: work waits for your chat until tomorrow.</p>' : ''}
|
|
102
|
+
<hr class="cai-sep">
|
|
103
|
+
<form class="cai-form" id="cloud-settings-form" autocomplete="off">
|
|
104
|
+
<div class="cai-two">
|
|
105
|
+
<div class="field"><label for="cai-model">Model</label>
|
|
106
|
+
<input class="input" id="cai-model" name="model" list="cai-models" value="${esc(vm.model || '')}" placeholder="${esc((vm.models || [])[0] || 'The tool’s default')}">
|
|
107
|
+
<datalist id="cai-models">${(vm.models || []).map((m) => `<option value="${esc(m)}">`).join('')}</datalist></div>
|
|
108
|
+
<div class="field"><label for="cai-cap">Daily cap (tokens)</label>
|
|
109
|
+
<input class="input" id="cai-cap" name="cap" type="number" min="0" step="1000" value="${vm.cap || ''}" placeholder="No limit"></div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="field"><label for="cai-prompt">Instructions for every run</label>
|
|
112
|
+
<textarea class="input" id="cai-prompt" name="prompt" rows="3" placeholder="Anything it should always do, or never do.">${esc(vm.prompt || '')}</textarea></div>
|
|
113
|
+
${ui.error ? `<p class="error">${esc(ui.error)}</p>` : ''}
|
|
114
|
+
${ui.confirming ? confirmHtml(vm) : `<div class="cai-two"><button class="btn primary" type="submit" ${ui.busy === 'save' ? 'disabled' : ''}>Save</button><button class="btn danger" type="button" data-cloud-disconnect>Disconnect…</button></div>`}
|
|
115
|
+
</form>
|
|
116
|
+
</div>`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The card's inside for `vm` (null: not connected) and what the person is doing in it. */
|
|
120
|
+
export function cloudAiInnerHtml (vm, ui = {}) {
|
|
121
|
+
const phase = cloudPhase(vm)
|
|
122
|
+
if (phase === 'none') return connectHtml(ui)
|
|
123
|
+
return phase === 'signin' ? signInHtml(vm, ui) : readyHtml(vm, ui)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Fills the card, and keeps it current with the API while it is on screen. */
|
|
127
|
+
export function bindCloudAi (sec) {
|
|
128
|
+
let vm = null
|
|
129
|
+
let polling = false
|
|
130
|
+
const ui = { busy: '', error: '', confirming: false }
|
|
131
|
+
const edited = new Map() // field name -> what the person typed, kept across redraws
|
|
132
|
+
|
|
133
|
+
const draw = () => {
|
|
134
|
+
const focus = document.activeElement && sec.contains(document.activeElement) ? document.activeElement.name || document.activeElement.id : ''
|
|
135
|
+
sec.innerHTML = cloudAiInnerHtml(vm, ui)
|
|
136
|
+
for (const [name, value] of edited) { const el = sec.querySelector(`[name="${name}"]`); if (el && !el.disabled) el.value = value }
|
|
137
|
+
if (focus) { const el = sec.querySelector(`[name="${focus}"]`) || document.getElementById(focus); if (el && sec.contains(el) && !el.disabled) el.focus() }
|
|
138
|
+
}
|
|
139
|
+
const take = (r) => {
|
|
140
|
+
const before = cloudPhase(vm)
|
|
141
|
+
vm = r.vm
|
|
142
|
+
if (cloudPhase(vm) !== before) { edited.clear(); ui.confirming = false }
|
|
143
|
+
draw()
|
|
144
|
+
if (vm) poll()
|
|
145
|
+
}
|
|
146
|
+
// The API holds each request until something changes (or 25 s pass), so this is not a busy loop.
|
|
147
|
+
const poll = async () => {
|
|
148
|
+
if (polling) return
|
|
149
|
+
polling = true
|
|
150
|
+
for (;;) {
|
|
151
|
+
if (!sec.isConnected || !vm) break
|
|
152
|
+
try {
|
|
153
|
+
const r = await api('GET', `/api/cloud-ai?wait=${encodeURIComponent(vm.version || 0)}`)
|
|
154
|
+
if (!sec.isConnected) break
|
|
155
|
+
if (!r.vm || !vm || r.vm.version !== vm.version || cloudPhase(r.vm) !== cloudPhase(vm)) take(r)
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err.signedOut || !sec.isConnected) break
|
|
158
|
+
await new Promise((resolve) => setTimeout(resolve, 5000))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
polling = false
|
|
162
|
+
}
|
|
163
|
+
const act = async (busy, fn) => {
|
|
164
|
+
ui.busy = busy
|
|
165
|
+
ui.error = ''
|
|
166
|
+
draw()
|
|
167
|
+
try { await fn() } catch (err) { ui.error = err.message }
|
|
168
|
+
ui.busy = ''
|
|
169
|
+
draw()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
sec.addEventListener('input', (e) => { if (e.target.name) edited.set(e.target.name, e.target.value) })
|
|
173
|
+
sec.addEventListener('click', async (e) => {
|
|
174
|
+
const b = e.target.closest('button')
|
|
175
|
+
if (!b || b.disabled) return
|
|
176
|
+
if (b.dataset.cloudConnect) return act(b.dataset.cloudConnect, async () => take(await api('POST', '/api/cloud-ai/connect', { tool: b.dataset.cloudConnect })))
|
|
177
|
+
if (b.hasAttribute('data-cloud-open')) { window.open(signInUrl(), '_blank', 'noopener'); return }
|
|
178
|
+
if (b.hasAttribute('data-cloud-copy')) {
|
|
179
|
+
try { await navigator.clipboard.writeText(signInUrl()); toast('Copied the link') } catch { toast('Select the link and press ⌘/Ctrl+C to copy') }
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
if (b.hasAttribute('data-cloud-restart')) return act('restart', async () => { await api('POST', '/api/cloud-ai/sign-in', {}); toast(`Starting ${cloudTool(vm.tool).short}'s sign-in again`) })
|
|
183
|
+
if (b.hasAttribute('data-cloud-wake')) return act('wake', async () => take(await api('POST', '/api/cloud-ai/wake')))
|
|
184
|
+
if (b.hasAttribute('data-cloud-disconnect')) { ui.confirming = true; ui.error = ''; draw(); $('[data-cloud-keep]', sec)?.focus(); return }
|
|
185
|
+
if (b.hasAttribute('data-cloud-keep')) { ui.confirming = false; draw(); return }
|
|
186
|
+
if (b.hasAttribute('data-cloud-delete')) {
|
|
187
|
+
return act('delete', async () => {
|
|
188
|
+
await api('POST', '/api/cloud-ai/disconnect')
|
|
189
|
+
ui.confirming = false
|
|
190
|
+
take({ vm: null })
|
|
191
|
+
toast('Disconnected: its machine and disk are deleted')
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
sec.addEventListener('submit', (e) => {
|
|
196
|
+
e.preventDefault()
|
|
197
|
+
const f = new FormData(e.target)
|
|
198
|
+
if (e.target.id === 'cloud-code-form') {
|
|
199
|
+
const code = String(f.get('code') || '').trim()
|
|
200
|
+
if (!code) return
|
|
201
|
+
return act('code', async () => {
|
|
202
|
+
await api('POST', '/api/cloud-ai/sign-in', { code })
|
|
203
|
+
edited.delete('code')
|
|
204
|
+
toast(`Sent to ${cloudTool(vm.tool).short} on your machine`)
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
if (e.target.id === 'cloud-settings-form') {
|
|
208
|
+
return act('save', async () => {
|
|
209
|
+
const r = await api('POST', '/api/cloud-ai/settings', { model: f.get('model'), prompt: f.get('prompt'), cap: f.get('cap') })
|
|
210
|
+
edited.clear()
|
|
211
|
+
take(r)
|
|
212
|
+
toast('Saved')
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
const signInUrl = () => (vm && vm.signIn && vm.signIn.url) || ''
|
|
217
|
+
|
|
218
|
+
api('GET', '/api/cloud-ai').then(take, (err) => {
|
|
219
|
+
$('.sec-body', sec).innerHTML = `<p class="hint warn">${esc(err.message)}</p>`
|
|
220
|
+
})
|
|
221
|
+
}
|