heyquilt 0.3.116 → 0.3.119
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/RELEASES.md +21 -0
- package/package.json +1 -1
- package/src/account.js +3 -3
- package/src/ai-vm.js +26 -2
- package/src/api/memory-store.js +2 -2
- package/src/api/routes/ai-vm.js +70 -4
- package/src/api/server.js +4 -4
- package/src/api/supabase-store.js +3 -3
- package/src/autowork.js +54 -6
- package/src/hooks.js +9 -4
- package/src/integrations.js +6 -4
- package/src/quilts/AGENTS.md +1 -0
- package/src/quilts/CLAUDE.md +1 -0
- package/src/quilts/tools.js +0 -2
- package/src/relay-ai-vm.js +5 -3
- package/src/runner.js +3 -2
- package/src/session.js +20 -4
- package/src/setup.js +9 -4
- package/src/ui/app.css +162 -31
- package/src/ui/cloud-ai.js +371 -153
- package/src/ui/home.js +29 -16
- package/src/ui/session.js +12 -1
- package/src/ui-server.js +21 -4
package/src/ui/home.js
CHANGED
|
@@ -14,7 +14,7 @@ import { bindSandboxKeys } from './sandbox-keys.js'
|
|
|
14
14
|
import { agentsTableHtml, agentDetailsHtml } from './agents-table.js'
|
|
15
15
|
import { SOUND_EVENTS, playSound } from './sounds.js'
|
|
16
16
|
import { gitControl, gitSegHtml, thisSessionGitHtml } from './git-rule.js'
|
|
17
|
-
import { cloudAiCardHtml, bindCloudAi } from './cloud-ai.js'
|
|
17
|
+
import { cloudAiCardHtml, bindCloudAi, cloudSetupOn, cloudSetupHtml, cloudSetupStep } from './cloud-ai.js'
|
|
18
18
|
import { feedbackDockHtml, bindFeedback } from './feedback.js'
|
|
19
19
|
|
|
20
20
|
export const tildify = (p) => state.defaults.home && String(p).startsWith(state.defaults.home) ? `~${String(p).slice(state.defaults.home.length)}` : p
|
|
@@ -809,7 +809,13 @@ function settingsPageHtml () {
|
|
|
809
809
|
<p class="settings-scope" data-settings-scope="account">Your defaults for new sessions. A session already running keeps its own, in its Session settings.</p>
|
|
810
810
|
</header>
|
|
811
811
|
<nav class="settings-tabs page-tabs" role="tablist" aria-label="Account settings">${settingsTabs().map(([id, label, icon]) => `<button type="button" role="tab" class="tab ${tab === id ? 'on' : ''}" data-settings-tab="${id}" aria-selected="${tab === id}">${I[icon] || ''}<span>${label}</span></button>`).join('')}</nav>
|
|
812
|
-
<div role="tabpanel">${settingsHtml()}</div>`
|
|
812
|
+
<div role="tabpanel">${tab === 'agents' && cloudSetupOn() ? cloudSetupPageHtml() : settingsHtml()}</div>`
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** Setting up your cloud AI on the Settings page: the whole tab panel, header to footer. */
|
|
816
|
+
function cloudSetupPageHtml () {
|
|
817
|
+
const f = cloudSetupHtml()
|
|
818
|
+
return `<div class="ai-page-flow"><div class="ss-head">${f.head}</div>${f.body}${f.foot}</div>`
|
|
813
819
|
}
|
|
814
820
|
|
|
815
821
|
/**
|
|
@@ -833,13 +839,8 @@ function settingsHtml (session = null) {
|
|
|
833
839
|
</section>` : ''}
|
|
834
840
|
|
|
835
841
|
${show('your-ai') ? `<form class="card settings-sec" id="your-ai-sec" autocomplete="off">
|
|
836
|
-
<div class="sec-intro"><h2>
|
|
842
|
+
<div class="sec-intro"><h2>The AI on this computer</h2><p>The AI tool you code with. In a session it works as you, on what you ask it: what people write to you is yours.</p></div>
|
|
837
843
|
<div class="sec-body">
|
|
838
|
-
<div class="field">
|
|
839
|
-
<label for="s-aitasks">Let my AI pick up tasks by itself</label>
|
|
840
|
-
<select class="input" id="s-aitasks" name="aiTasks">${[['off', 'Off'], ['mine', 'Tasks assigned to it'], ['any', 'Assigned to it, then unassigned ones']].map(([v, l]) => `<option value="${v}" ${p.aiTasks === v ? 'selected' : ''}>${l}</option>`).join('')}</select>
|
|
841
|
-
<span class="hint">When your AI finishes a piece of work, Quilt hands it the next To do task from the board, in whichever AI tool you use. Applies at once.</span>
|
|
842
|
-
</div>
|
|
843
844
|
${toggle('shareAgent', p.shareAgent, 'Share my AI chat', 'Partners see your prompts, the replies and which files it touches. You can pause it inside any session.')}
|
|
844
845
|
${toggle('summarize', p.summarize, 'Summarize my chats', 'Your prompts and your AI’s replies are shortened to a sentence or two on this computer before they’re shared. Uses your claude CLI (a few Haiku tokens each); if it isn’t available, the text is just shortened.')}
|
|
845
846
|
<div class="sec-actions"><span></span><button class="btn primary" type="submit">Save</button></div>
|
|
@@ -1162,10 +1163,10 @@ function bindSettings (root, refresh) {
|
|
|
1162
1163
|
if (sess) saveForm(sess, (f) => ({ joinDir: f.get('joinDir'), preferLocal: !!f.get('preferLocal'), pushLocalCommits: !!f.get('pushLocalCommits'), report: !!f.get('report') }))
|
|
1163
1164
|
|
|
1164
1165
|
const ai = $('#your-ai-sec', root)
|
|
1165
|
-
if (ai) saveForm(ai, (f) => ({ shareAgent: !!f.get('shareAgent'), summarize: !!f.get('summarize')
|
|
1166
|
+
if (ai) saveForm(ai, (f) => ({ shareAgent: !!f.get('shareAgent'), summarize: !!f.get('summarize') }))
|
|
1166
1167
|
|
|
1167
|
-
|
|
1168
|
-
if (cloud) bindCloudAi(
|
|
1168
|
+
// Your cloud AI: its card, or its setup in place of the tab (in the pop-up, its header and footer too).
|
|
1169
|
+
if ($('#cloud-ai-sec', root) || $('.ai-flow-body', root)) bindCloudAi(root.closest('.settings-modal') || root, refresh)
|
|
1169
1170
|
|
|
1170
1171
|
const agit = $('#agents-git-sec', root)
|
|
1171
1172
|
if (agit) bindAgentsGit(agit)
|
|
@@ -1247,14 +1248,26 @@ export function openSettings (opts = {}) {
|
|
|
1247
1248
|
document.body.appendChild(back)
|
|
1248
1249
|
const body = $('#settings-body', back)
|
|
1249
1250
|
const nav = $('.settings-tabs-slot', back)
|
|
1251
|
+
const main = $('.ss-main', back)
|
|
1252
|
+
const head = $('.ss-head', back)
|
|
1253
|
+
const plainHead = head.innerHTML
|
|
1254
|
+
const closeBtn = head.querySelector('[data-settings-close]').outerHTML
|
|
1250
1255
|
let shown = ''
|
|
1251
1256
|
const draw = () => {
|
|
1252
1257
|
const tab = settingsTab()
|
|
1253
|
-
|
|
1254
|
-
|
|
1258
|
+
// Setting up your cloud AI takes the whole right side: its own header, steps and footer.
|
|
1259
|
+
const flow = tab === 'agents' && cloudSetupOn() ? cloudSetupHtml() : null
|
|
1260
|
+
// The same tab (or step of the setup) keeps where it was scrolled to; another starts at the top.
|
|
1261
|
+
const key = flow ? `setup:${cloudSetupStep()}` : tab
|
|
1262
|
+
const scroll = key === shown ? body.scrollTop : 0
|
|
1263
|
+
shown = key
|
|
1255
1264
|
nav.innerHTML = settingsTabsHtml(tab)
|
|
1256
|
-
|
|
1257
|
-
|
|
1265
|
+
back.querySelector('.ai-flow-foot')?.remove()
|
|
1266
|
+
main.classList.toggle('ai-flow', !!flow)
|
|
1267
|
+
head.innerHTML = flow ? flow.head + closeBtn : plainHead
|
|
1268
|
+
if (!flow) $('.ss-name', back).textContent = settingsTabLabel(tab)
|
|
1269
|
+
body.innerHTML = flow ? flow.body : settingsHtml(session)
|
|
1270
|
+
if (flow) main.insertAdjacentHTML('beforeend', flow.foot)
|
|
1258
1271
|
bindSettings(body, draw)
|
|
1259
1272
|
body.scrollTop = scroll
|
|
1260
1273
|
// The sidebar's profile card, if it is showing, follows the saved profile.
|
|
@@ -1283,7 +1296,7 @@ export function openSettings (opts = {}) {
|
|
|
1283
1296
|
const close = () => back.remove()
|
|
1284
1297
|
back.addEventListener('mousedown', (e) => { if (e.target === back) close() })
|
|
1285
1298
|
back.addEventListener('keydown', (e) => { if (e.key === 'Escape') close() })
|
|
1286
|
-
back.
|
|
1299
|
+
back.addEventListener('click', (e) => { if (e.target.closest('[data-settings-close]')) close() })
|
|
1287
1300
|
body.addEventListener('click', (e) => {
|
|
1288
1301
|
if (!session || !e.target.closest('[data-open-session-vc]')) return
|
|
1289
1302
|
close()
|
package/src/ui/session.js
CHANGED
|
@@ -3242,6 +3242,15 @@ function renderTyping () {
|
|
|
3242
3242
|
if (nearBottom) list.scrollTop = list.scrollHeight
|
|
3243
3243
|
}
|
|
3244
3244
|
|
|
3245
|
+
// Cloud AIs seen in this app by their own names: name -> their person (cloud-ai.js setup).
|
|
3246
|
+
const cloudAiOf = new Map()
|
|
3247
|
+
/** The "<person>'s AI" tag after a cloud AI's own name ("Juniper"); none for "<person>'s AI" itself. */
|
|
3248
|
+
function aiTag (by, me) {
|
|
3249
|
+
const of = cloudAiOf.get(by)
|
|
3250
|
+
if (!of || by === `${of}'s AI`) return ''
|
|
3251
|
+
return `<span class="ai-tag">${esc(of === me ? 'Your AI' : `${of}'s AI`)}</span>`
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3245
3254
|
function renderMessages (incoming = false, force = false) {
|
|
3246
3255
|
const el = $('#messages')
|
|
3247
3256
|
const s = sum()
|
|
@@ -3254,6 +3263,8 @@ function renderMessages (incoming = false, force = false) {
|
|
|
3254
3263
|
const colors = new Map(s.status.peers.map((p) => [p.name, p.color]))
|
|
3255
3264
|
colors.set(name, s.status.me.color)
|
|
3256
3265
|
const names = mentionNames(s)
|
|
3266
|
+
// A person's cloud AI with a name of its own ("Juniper") is shown as theirs, also while it sleeps.
|
|
3267
|
+
for (const p of s.status.peers) if (p && p.cloud && typeof p.of === 'string') cloudAiOf.set(p.name, p.of)
|
|
3257
3268
|
// Finished calls sit in the chat at when they began (voice.js).
|
|
3258
3269
|
const calls = callCards(current)
|
|
3259
3270
|
el.innerHTML = list.length || calls.length
|
|
@@ -3269,7 +3280,7 @@ function renderMessages (incoming = false, force = false) {
|
|
|
3269
3280
|
// The sender's face sits in the name line above the bubble, so the bubble has the pane's width.
|
|
3270
3281
|
const face = mine ? '' : relay ? `<span class="relay-q avatar">${I.globe}</span>` : avatar(by, colors.get(by) || colors.get(m.by))
|
|
3271
3282
|
return `<div class="msg${mine ? ' mine' : ''}">
|
|
3272
|
-
<div style="min-width:0"><div class="head">${face}<b${by !== m.by ? ` title="${esc(m.by)}"` : ''}>${mine ? 'You' : esc(by === `${name}'s AI` ? 'Your AI' : by)}</b>${dm}<span>${esc(clock(m.ts))}</span></div>
|
|
3283
|
+
<div style="min-width:0"><div class="head">${face}<b${by !== m.by ? ` title="${esc(m.by)}"` : ''}>${mine ? 'You' : esc(by === `${name}'s AI` ? 'Your AI' : by)}</b>${aiTag(by, name)}${dm}<span>${esc(clock(m.ts))}</span></div>
|
|
3273
3284
|
<div class="bubble">${m.text ? `<div class="text">${textHtml(m.text, names, name, { meAgent: s.status.me.kind === 'agent' })}</div>` : ''}${file}</div></div></div>`
|
|
3274
3285
|
})() })), ...calls].sort((a, b) => a.ts - b.ts).map((x) => x.html).join('')
|
|
3275
3286
|
: '<div class="day-empty"><div><b>Say hi.</b></div><div class="hint">Messages, direct messages and files you share appear here. Drop a file on this panel to send it.</div></div>'
|
package/src/ui-server.js
CHANGED
|
@@ -21,6 +21,7 @@ import { installedEditors, openIn, copyToClipboard } from './editors.js'
|
|
|
21
21
|
import { autowork, autoConfig, publicConfig, AUTO_MODES } from './autowork.js'
|
|
22
22
|
import { migrateDir } from './legacy.js'
|
|
23
23
|
import { writePrivateJson } from './private-file.js'
|
|
24
|
+
import { autoToolFor } from './autowork-tools.js'
|
|
24
25
|
import { readAccount, saveAccount, workspaceGithub, setWorkspaceGithub, removeWorkspaceGithub, setOrgGithub, clearAccount, clearAccountIf, resumeAccount, startLink, waitForLink, fetchMe, signOut, revokeToken, accountFromProfile, renameSession, createAgentInvite, listAgents, agentDetails, listAccessTypes, listCollaborators, listGrants, putGrant, deleteGrant, inviteToSession, listSessionInvites, cancelSessionInvite, listWorkspaces, listOrgs, createWorkspace, getWorkspace, updateWorkspace, deleteWorkspace, putWorkspaceMember, removeWorkspaceMember, getAgentPlacement, putAgentPlacement, listOrgAgents, putWorkspaceAgent, deleteWorkspaceAgent, createWorkspaceAgentInvite, excludeSessionAgent, listExcludedSessionAgents, listSessionAgents, includeSessionAgent, setSessionWorkspace, moveSessionToWorkspace, listWorkspaceInvites, inviteToWorkspace, cancelWorkspaceInvite, listMyInvites, answerMyInvite, announceSessionStarted, announceWhenReported, listWorkspaceFiles, createWorkspaceFile, confirmWorkspaceFile, workspaceFileDownload, updateWorkspaceFile, deleteWorkspaceFile, createWorkspaceFolder, listWorkspaceFileVersions, githubStatus, githubConnectUrl, listSandboxKeys, createSandboxKey, rotateSandboxKey, revokeSandboxKey, removeSandboxKey, aiVmFollow, aiVmUnfollow, aiVmStatus, aiVmConnect, aiVmUpdate, aiVmSignIn, aiVmWake, aiVmDisconnect, voiceIce, transcribeVoice, apiUrl } from './account.js'
|
|
25
26
|
import { aiName } from './persona.js'
|
|
26
27
|
import { effectiveAccess, builtinType } from './session-access.js'
|
|
@@ -601,12 +602,19 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
601
602
|
// A session nobody named is shown as the app shows it: by this computer's folder for it.
|
|
602
603
|
const local = new Map([...runs.values()].filter((e) => e.run && e.run.session).map((e) => [e.run.session.room, e.run.session.sessionName || path.basename(e.run.dir || '')]))
|
|
603
604
|
const sessions = (vm.sessions || []).map((x) => (x.name ? x : { ...x, name: local.get(x.room) || '' }))
|
|
604
|
-
return { vm: { ...vm, sessions, name: aiName(person) } }
|
|
605
|
+
return { vm: { ...vm, sessions, person, name: vm.aiName || aiName(person) } }
|
|
605
606
|
}
|
|
606
607
|
const cloudNone = (err) => { if (err.status === 404) return cloudView(null); throw err }
|
|
607
|
-
/**
|
|
608
|
+
/**
|
|
609
|
+
* What Settings may change: the tool, model, instructions and daily cap (blank is no cap), and
|
|
610
|
+
* what setting it up chose: its name and color, which tasks it takes, whether it changes files,
|
|
611
|
+
* whether every message wakes it, and which sessions it joins. The API checks each.
|
|
612
|
+
*/
|
|
608
613
|
const cloudPatch = (b) => {
|
|
609
614
|
const out = {}
|
|
615
|
+
for (const k of ['name', 'color', 'tasks', 'sessionsMode']) if (b[k] !== undefined) out[k] = String(b[k])
|
|
616
|
+
for (const k of ['files', 'wake']) if (b[k] !== undefined) out[k] = b[k] === true
|
|
617
|
+
if (b.rooms !== undefined) out.rooms = Array.isArray(b.rooms) ? b.rooms.map(String) : []
|
|
610
618
|
if (b.tool !== undefined) out.tool = String(b.tool)
|
|
611
619
|
if (b.model !== undefined) out.model = String(b.model).trim()
|
|
612
620
|
if (b.prompt !== undefined) out.prompt = String(b.prompt)
|
|
@@ -618,12 +626,21 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
618
626
|
return out
|
|
619
627
|
}
|
|
620
628
|
const cloudRoutes = {
|
|
629
|
+
// The tools a cloud AI can run, with their models, for setting one up before it exists.
|
|
630
|
+
'GET /api/cloud-ai/tools': () => ({ tools: CLOUD_TOOLS.map((tool) => ({ tool, models: autoToolFor(tool)?.models || [] })) }),
|
|
621
631
|
'GET /api/cloud-ai': (b, id, url) => asAccount(async (token) => cloudView(await aiVmStatus({ token, wait: url.searchParams.get('wait') }))),
|
|
622
632
|
'POST /api/cloud-ai/connect': (b) => {
|
|
623
633
|
if (!CLOUD_TOOLS.includes(b && b.tool)) throw httpError(400, 'Pick Claude Code, Codex or Cursor.')
|
|
624
|
-
|
|
634
|
+
const { tool: _t, ...setup } = cloudPatch(b)
|
|
635
|
+
handed.clear()
|
|
636
|
+
return asAccount(async (token) => cloudView(await aiVmConnect({ token, tool: b.tool, setup })))
|
|
637
|
+
},
|
|
638
|
+
'POST /api/cloud-ai/settings': (b) => {
|
|
639
|
+
const patch = cloudPatch(b || {})
|
|
640
|
+
// Sessions picked anew are handed over again (the API joins only the ones chosen).
|
|
641
|
+
if (patch.sessionsMode !== undefined || patch.rooms !== undefined) handed.clear()
|
|
642
|
+
return asAccount(async (token) => cloudView(await aiVmUpdate({ token, patch })))
|
|
625
643
|
},
|
|
626
|
-
'POST /api/cloud-ai/settings': (b) => { const patch = cloudPatch(b || {}); return asAccount(async (token) => cloudView(await aiVmUpdate({ token, patch }))) },
|
|
627
644
|
'POST /api/cloud-ai/sign-in': (b) => asAccount(async (token) => aiVmSignIn({ token, ...(b && b.code !== undefined ? { code: String(b.code).trim() } : {}) })),
|
|
628
645
|
'POST /api/cloud-ai/wake': () => asAccount(async (token) => cloudView(await aiVmWake({ token }))),
|
|
629
646
|
'POST /api/cloud-ai/disconnect': () => asAccount(async (token) => { await aiVmDisconnect({ token }).catch(cloudNone); hasCloudAi = false; return { ok: true } })
|