heyquilt 0.3.138 → 0.3.140
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 +6 -0
- package/bin/quilt.js +7 -143
- package/package.json +1 -1
- package/src/account.js +0 -29
- package/src/api/memory-store.js +0 -29
- package/src/api/routes/teams.js +0 -23
- package/src/api/server.js +5 -19
- package/src/api/supabase-store.js +0 -52
- package/src/branch-view.js +6 -131
- package/src/control.js +2 -33
- package/src/duties.js +1 -1
- package/src/github-commit.js +3 -315
- package/src/mcp.js +8 -157
- package/src/relay-ask.js +3 -66
- package/src/relay-cluster.js +3 -5
- package/src/relay-mcp.js +6 -418
- package/src/relay-prune.js +6 -145
- package/src/relay-turn-sync.js +1 -1
- package/src/server.js +33 -1331
- package/src/session.js +52 -3526
- package/src/status.js +2 -58
- package/src/turn-sync.js +15 -2
- package/src/ui/changes.js +5 -14
- package/src/ui/chat.js +0 -1
- package/src/ui/common.js +0 -2
- package/src/ui/git-rule.js +2 -30
- package/src/ui/home.js +3 -76
- package/src/ui/session.js +34 -528
- package/src/ui/tree.js +2 -8
- package/src/ui/workspaces.js +2 -122
- package/src/ui-server.js +1 -38
- package/src/api/routes/github.js +0 -380
- package/src/branches.js +0 -194
- package/src/clash.js +0 -128
- package/src/commit-request.js +0 -59
- package/src/commit.js +0 -213
- package/src/git-hook.js +0 -48
- package/src/gitignore.js +0 -49
- package/src/gitstate.js +0 -1069
- package/src/relay-ci.js +0 -111
- package/src/relay-clash.js +0 -214
- package/src/relay-commit.js +0 -923
- package/src/relay-filekey.js +0 -46
- package/src/relay-git.js +0 -274
- package/src/relay-githttp.js +0 -166
- package/src/relay-github.js +0 -109
- package/src/relay-outbound.js +0 -1295
- package/src/relay-repo-moves.js +0 -43
- package/src/relay-sync.js +0 -795
- package/src/relay-upstream.js +0 -889
- package/src/sync-words.js +0 -42
- package/src/upstream.js +0 -192
package/src/ui/tree.js
CHANGED
|
@@ -5,8 +5,6 @@ import { esc, I, ago, colorFor, loadingNote } from './common.js'
|
|
|
5
5
|
export const CHAT_FILES = 'Chat files'
|
|
6
6
|
|
|
7
7
|
export const RECENT_MS = 2 * 60 * 1000
|
|
8
|
-
/** How long a file brought in from GitHub keeps its GitHub badge (an edit in the session takes it away sooner). */
|
|
9
|
-
export const GITHUB_BADGE_MS = 10 * 60 * 1000
|
|
10
8
|
|
|
11
9
|
const shortAgo = (ts) => {
|
|
12
10
|
const s = Math.max(0, Math.round((Date.now() - ts) / 1000))
|
|
@@ -122,14 +120,10 @@ export function renderTree (el, tree, { me, expanded, selected }) {
|
|
|
122
120
|
const byOther = recent && f.edited.by !== me
|
|
123
121
|
const ownClaim = f.claim && claimFolder(f.claim.pattern) === f.path ? f.claim : null
|
|
124
122
|
// The holder of a claim is the one editing it: their claim badge says so, one badge is enough on a narrow row.
|
|
125
|
-
// GitHub's version, not edited in the session since: GitHub's commit takes the "who edited" place, for ten
|
|
126
|
-
// minutes after it came in (an older relay doesn't say when: then after the commit).
|
|
127
|
-
const gh = f.github && Date.now() - (f.github.came || f.github.at || 0) < GITHUB_BADGE_MS ? f.github : null
|
|
128
|
-
const ghBadge = gh ? `<span class="t-badge gh" title="${esc(`From GitHub: ${gh.sha.slice(0, 7)}${gh.by ? ` by ${gh.by}` : ''}${gh.subject ? `: ${gh.subject}` : ''}. Not edited in the session since.`)}">${I.github}<span class="t-gh">${esc(gh.by || 'GitHub')}${gh.at ? ` · ${shortAgo(gh.at)}` : ''}</span></span>` : ''
|
|
129
123
|
const ghost = ownClaim && ownClaim.box && !boxMine(ownClaim)
|
|
130
124
|
rows.push(`<div class="t-row t-file${selected === f.path ? ' on' : ''}${ghost ? ' ghost' : ''}" role="treeitem" tabindex="-1" data-file="${esc(f.path)}" style="--depth:${depth}" title="${esc(f.path)}">
|
|
131
|
-
<span class="t-ico">${I.file}</span><span class="t-name">${esc(f.name)}</span
|
|
132
|
-
${recent && !
|
|
125
|
+
<span class="t-ico">${I.file}</span><span class="t-name">${esc(f.name)}</span>
|
|
126
|
+
${recent && !(ownClaim && ownClaim.by === f.edited.by) ? editBadge(f, byOther) : ''}
|
|
133
127
|
${claimBadge(ownClaim)}
|
|
134
128
|
<button class="t-more" data-more="${esc(f.path)}" data-kind="file" aria-label="More for ${esc(f.name)}">${I.more}</button></div>`)
|
|
135
129
|
}
|
package/src/ui/workspaces.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// (Sessions, Files, People & agents, Settings). Sessions stay in session.js; the file library
|
|
3
3
|
// (the Files section, All files and uploads) is files.js.
|
|
4
4
|
import { mountSecrets } from './secrets.js'
|
|
5
|
-
import { I, state, $, esc, basename, toast, api, ask, avatar, colorFor, ago, bytes, COLORS, loadingHtml
|
|
6
|
-
import { gitControl, gitSegHtml } from './git-rule.js'
|
|
5
|
+
import { I, state, $, esc, basename, toast, api, ask, avatar, colorFor, ago, bytes, COLORS, loadingHtml } from './common.js'
|
|
7
6
|
import { filesSectionHtml, bindFilesSection } from './files.js'
|
|
8
7
|
import { memberMiniHtml, memberMenuItems, openAgentMenu } from './agent-place.js'
|
|
9
8
|
|
|
@@ -260,7 +259,7 @@ export function workspacePageHtml () {
|
|
|
260
259
|
${w.description ? `<p>${esc(w.description)}</p>` : ''}
|
|
261
260
|
<div class="ws-statline">${running.length ? `<span class="live"><span class="dot"></span><b>${running.length}</b> open now</span>` : ''}${stat(sessionCount, 'session', 'sessions')}${stat(fileCount, 'file', 'files')}${stat(people.length + agents.length, 'person or agent', 'people and agents')}</div>
|
|
262
261
|
</div>
|
|
263
|
-
<div class="acts"><button class="btn sm" data-team title="People and agents in this workspace">${I.users}<span>Team</span><span class="count">${people.length + agents.length}</span></button
|
|
262
|
+
<div class="acts"><button class="btn sm" data-team title="People and agents in this workspace">${I.users}<span>Team</span><span class="count">${people.length + agents.length}</span></button>${admin ? `<button class="btn sm" data-invite-ws>${I.link}<span>Invite</span></button><button class="btn sm ghost icon" data-ws-settings title="Workspace settings" aria-label="Workspace settings">${I.gear}</button>` : ''}</div>
|
|
264
263
|
</header>
|
|
265
264
|
|
|
266
265
|
<div class="ws-dash">
|
|
@@ -375,7 +374,6 @@ export function bindWorkspacePage (root, { go, rerender, newSessionDialog, invit
|
|
|
375
374
|
// The team (people and agents) is a dialog from the Team button, so the page is sessions and files.
|
|
376
375
|
root.querySelector('[data-team]')?.addEventListener('click', () => openTeamDialog({ id, inviteDialog, reload, rerender }))
|
|
377
376
|
root.querySelector('[data-ws-settings]')?.addEventListener('click', () => settingsDialog(reload, go, dialog))
|
|
378
|
-
root.querySelector('[data-ws-github]')?.addEventListener('click', () => githubDialog(id, dialog))
|
|
379
377
|
bindFilesSection(root, { id, reload, go })
|
|
380
378
|
// Secrets: the workspace's, with its team's above (secrets.js); .env files come from its sessions on this computer.
|
|
381
379
|
const sec = root.querySelector('[data-ws-secrets]')
|
|
@@ -390,124 +388,6 @@ function usageLine (u) {
|
|
|
390
388
|
return `<p class="hint" data-ws-usage>${esc(`${used} used · ${n} ${n === 1 ? 'file' : 'files'}`)}</p>`
|
|
391
389
|
}
|
|
392
390
|
|
|
393
|
-
/**
|
|
394
|
-
* GitHub for a workspace (plan §2): who connected Quilt's GitHub App for it (its sessions use that before
|
|
395
|
-
* their people's own, then the org's), which repositories its sessions may use, and, in an org, whether
|
|
396
|
-
* the org requires GitHub. Admins change it; everyone else sees it.
|
|
397
|
-
*/
|
|
398
|
-
let githubOpening = false
|
|
399
|
-
/**
|
|
400
|
-
* Git for everyone in a workspace's GitHub settings (mockup option A): the workspace's rule, and its org's
|
|
401
|
-
* for org admins, each as the one segmented control. A pick saves at once, as in the session's settings.
|
|
402
|
-
*/
|
|
403
|
-
function gitRuleCardsHtml (g, wsName) {
|
|
404
|
-
const org = g.org
|
|
405
|
-
const orgRule = org && org.agentCommits ? { by: 'org', name: org.name, mode: org.agentCommits } : null
|
|
406
|
-
const ws = gitControl({ value: g.agentCommits || '', rule: orgRule, canEdit: !!g.admin })
|
|
407
|
-
const wsPill = ws.rulePill ? `<span class="pill rule">${esc(ws.rulePill)}</span>` : org ? `<span class="pill">Org ${esc(org.name)} sets nothing</span>` : g.agentCommits ? '' : '<span class="pill">Not set</span>'
|
|
408
|
-
const clear = (scope) => `<button type="button" class="text-btn" data-git-clear="${scope}">Clear it</button>`
|
|
409
|
-
const wsHint = g.agentCommits
|
|
410
|
-
? `A ceiling for its sessions: each owner may choose stricter. ${g.admin ? clear('workspace') : 'Clear it'} to let each session decide.`
|
|
411
|
-
: 'Nothing selected: each session\'s owner decides. A choice here is a ceiling for its sessions: each owner may choose stricter.'
|
|
412
|
-
const cards = [`<div class="pm-card git-card" data-git-scope="workspace">
|
|
413
|
-
<div class="git-head"><h4>Git for every session in ${esc(wsName)}</h4>${wsPill}</div>
|
|
414
|
-
${gitSegHtml(ws, { attr: 'data-git-pick', label: `Git for every session in ${wsName}` })}
|
|
415
|
-
${ws.lockedHint ? `<span class="hint">${esc(ws.lockedHint)}</span>` : ''}
|
|
416
|
-
<span class="hint">${wsHint}${g.admin ? '' : ' Its admins change this.'}</span>
|
|
417
|
-
</div>`]
|
|
418
|
-
if (org) {
|
|
419
|
-
const o = gitControl({ value: org.agentCommits || '', canEdit: !!org.admin })
|
|
420
|
-
const oHint = org.agentCommits
|
|
421
|
-
? `A ceiling for every workspace, team and session in ${esc(org.name)}: each may choose stricter. ${org.admin ? clear('org') : 'Clear it'} to let them decide.`
|
|
422
|
-
: 'Nothing selected: its workspaces, teams and sessions decide.'
|
|
423
|
-
cards.push(`<div class="pm-card git-card" data-git-scope="org">
|
|
424
|
-
<div class="git-head"><h4>Git for every session in ${esc(org.name)}</h4><span class="pill">${org.agentCommits ? 'Org rule' : 'Not set'}</span></div>
|
|
425
|
-
${gitSegHtml(o, { attr: 'data-git-pick', label: `Git for every session in ${org.name}` })}
|
|
426
|
-
<span class="hint">${oHint}${org.admin ? '' : ' Org admins change this.'}</span>
|
|
427
|
-
</div>`)
|
|
428
|
-
}
|
|
429
|
-
return cards.join('')
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
async function githubDialog (id, dialog) {
|
|
433
|
-
if (githubOpening || document.querySelector('.modal-back [data-gh-connect]')) return // one at a time, however often the button is clicked
|
|
434
|
-
githubOpening = true
|
|
435
|
-
let g
|
|
436
|
-
try { g = await api('GET', wsUrl(id, '/github')) } catch (err) { toast(err.message); return } finally { githubOpening = false }
|
|
437
|
-
const conn = (list, mine) => list.length
|
|
438
|
-
? `<ul class="ws-gh-list">${list.map((c) => `<li><b>${c.login ? `@${esc(c.login)}` : 'GitHub'}</b> <span class="hint">connected by ${esc(c.name || 'someone')}${c.at ? `, ${esc(ago(c.at))}` : ''}</span>${mine && (g.admin || c.connectedBy === g.me) ? ` <button type="button" class="btn sm ghost" data-gh-remove="${esc(c.connectedBy)}">Remove</button>` : ''}</li>`).join('')}</ul>`
|
|
439
|
-
: '<p class="hint">Nobody has connected GitHub here yet.</p>'
|
|
440
|
-
const wsName = (state.workspace && state.workspace.workspace.id === id && state.workspace.workspace.name) || 'this workspace'
|
|
441
|
-
const { form, close } = dialog(`
|
|
442
|
-
<h3>GitHub for this workspace</h3>
|
|
443
|
-
<p class="hint">Sessions here use this workspace's GitHub connection first, then ${g.org ? `${esc(g.org.name)}'s` : 'their org\'s'}, then their own people's.</p>
|
|
444
|
-
<div class="field"><span class="label">This workspace</span>${conn(g.connections, true)}
|
|
445
|
-
<button type="button" class="btn sm" data-gh-connect>${I.branch}<span>Connect GitHub for this workspace</span></button></div>
|
|
446
|
-
${g.org ? `<div class="field"><span class="label">${esc(g.org.name)}</span>${conn(g.org.connections, false)}${g.org.admin ? `<button type="button" class="btn sm" data-gh-connect-org>${I.branch}<span>Connect GitHub for ${esc(g.org.name)}</span></button>` : ''}</div>` : ''}
|
|
447
|
-
<div class="field"><span class="label">Repositories</span>
|
|
448
|
-
<label class="radio"><input type="radio" name="access" value="open" ${g.access !== 'repos' ? 'checked' : ''} ${g.admin ? '' : 'disabled'}> Any repository its people can reach</label>
|
|
449
|
-
<label class="radio"><input type="radio" name="access" value="repos" ${g.access === 'repos' ? 'checked' : ''} ${g.admin ? '' : 'disabled'}> Only these, one per line (owner/name)</label>
|
|
450
|
-
<textarea class="input" name="repos" rows="3" placeholder="acme/widgets" ${g.admin ? '' : 'disabled'}>${esc((g.repos || []).join('\n'))}</textarea></div>
|
|
451
|
-
<div class="field"><span class="label">Git for everyone</span><div class="git-cards" data-git-cards>${gitRuleCardsHtml(g, wsName)}</div></div>
|
|
452
|
-
${g.org ? `<label class="toggle"><input type="checkbox" name="requireGithub" ${g.org.requireGithub ? 'checked' : ''} ${g.org.admin ? '' : 'disabled'}><span class="track"><span class="knob"></span></span><span class="tg-text"><b>${esc(g.org.name)} requires GitHub</b><span class="hint">An agent then gets the GitHub permissions of the person it works for, no more. ${g.org.admin ? 'An org setting: it applies to every workspace of the org.' : 'Org admins change this.'}</span></span></label>` : ''}
|
|
453
|
-
<p class="error" data-error></p>
|
|
454
|
-
<div class="actions"><span class="spacer"></span><button type="button" class="btn ghost" data-cancel>${g.admin || (g.org && g.org.admin) ? 'Cancel' : 'Close'}</button>${g.admin || (g.org && g.org.admin) ? '<button class="btn primary" type="submit">Save</button>' : ''}</div>`)
|
|
455
|
-
form.querySelector('[data-gh-connect]').onclick = async () => {
|
|
456
|
-
try {
|
|
457
|
-
const { url } = await api('POST', '/api/github/connect', { scope: 'workspace', id })
|
|
458
|
-
window.open(url, '_blank', 'noopener')
|
|
459
|
-
toast('Finish on GitHub, then open this again')
|
|
460
|
-
close()
|
|
461
|
-
} catch (err) { form.querySelector('[data-error]').textContent = err.message }
|
|
462
|
-
}
|
|
463
|
-
form.querySelector('[data-gh-connect-org]')?.addEventListener('click', async () => {
|
|
464
|
-
try {
|
|
465
|
-
const { url } = await api('POST', '/api/github/connect', { scope: 'org', id: g.org.slug })
|
|
466
|
-
window.open(url, '_blank', 'noopener')
|
|
467
|
-
toast('Finish on GitHub, then open this again')
|
|
468
|
-
close()
|
|
469
|
-
} catch (err) { form.querySelector('[data-error]').textContent = err.message }
|
|
470
|
-
})
|
|
471
|
-
form.querySelector('[data-git-cards]').addEventListener('click', async (e) => {
|
|
472
|
-
const pick = e.target.closest('[data-git-pick]')
|
|
473
|
-
const clear = e.target.closest('[data-git-clear]')
|
|
474
|
-
if (!pick && !clear) return
|
|
475
|
-
if (pick && (pick.disabled || pick.classList.contains('on'))) return
|
|
476
|
-
const scope = clear ? clear.dataset.gitClear : pick.closest('[data-git-scope]').dataset.gitScope
|
|
477
|
-
const agentCommits = clear ? '' : pick.dataset.gitPick
|
|
478
|
-
try {
|
|
479
|
-
if (scope === 'org') await api('POST', `/api/orgs/${encodeURIComponent(g.org.slug)}/github`, { agentCommits })
|
|
480
|
-
else await api('POST', wsUrl(id, '/github'), { agentCommits })
|
|
481
|
-
g = await api('GET', wsUrl(id, '/github'))
|
|
482
|
-
form.querySelector('[data-git-cards]').innerHTML = gitRuleCardsHtml(g, wsName)
|
|
483
|
-
toast(agentCommits ? AGENT_COMMIT_WORDS[agentCommits] : scope === 'org' ? 'Cleared: its workspaces and sessions decide.' : 'Cleared: each session decides.')
|
|
484
|
-
} catch (err) { form.querySelector('[data-error]').textContent = err.message }
|
|
485
|
-
})
|
|
486
|
-
form.querySelectorAll('[data-gh-remove]').forEach((b) => {
|
|
487
|
-
b.onclick = async () => {
|
|
488
|
-
try { await api('POST', wsUrl(id, '/github/disconnect'), { connectedBy: b.dataset.ghRemove }); close(); githubDialog(id, dialog) } catch (err) { form.querySelector('[data-error]').textContent = err.message }
|
|
489
|
-
}
|
|
490
|
-
})
|
|
491
|
-
form.onsubmit = async (e) => {
|
|
492
|
-
e.preventDefault()
|
|
493
|
-
const f = new FormData(form)
|
|
494
|
-
const save = form.querySelector('button[type=submit]')
|
|
495
|
-
save.disabled = true
|
|
496
|
-
try {
|
|
497
|
-
if (g.admin) {
|
|
498
|
-
const repos = String(f.get('repos') || '').split(/[\s,]+/).map((x) => x.trim()).filter(Boolean)
|
|
499
|
-
await api('POST', wsUrl(id, '/github'), { access: f.get('access') === 'repos' ? 'repos' : 'open', repos })
|
|
500
|
-
}
|
|
501
|
-
const require = f.get('requireGithub') === 'on'
|
|
502
|
-
if (g.org && g.org.admin && require !== !!g.org.requireGithub) await api('POST', `/api/orgs/${encodeURIComponent(g.org.slug)}/github`, { requireGithub: require })
|
|
503
|
-
close(); toast('Saved')
|
|
504
|
-
} catch (err) {
|
|
505
|
-
form.querySelector('[data-error]').textContent = err.message
|
|
506
|
-
save.disabled = false
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
391
|
/** `dialog(html)` is home.js's: a modal form that closes on Cancel, Escape or a click outside. */
|
|
512
392
|
function settingsDialog (reload, go, dialog) {
|
|
513
393
|
const w = state.workspace.workspace
|
package/src/ui-server.js
CHANGED
|
@@ -14,14 +14,13 @@ import { runSession, decodeInvite, encodeInvite, newConn, readConfig, recentSess
|
|
|
14
14
|
import { MAX_SHARED_FILE_BYTES } from './protocol.js'
|
|
15
15
|
import { getSettings, saveSettings, unsupportedRelay, relayUrl } from './settings.js'
|
|
16
16
|
import { PICKUP_MODES, pickupMode } from './tasks.js'
|
|
17
|
-
import { AGENT_COMMITS } from './github-commit.js'
|
|
18
17
|
import { execFile } from 'node:child_process'
|
|
19
18
|
import { installedEditors, openIn, copyToClipboard } from './editors.js'
|
|
20
19
|
import { autowork, autoConfig, publicConfig, AUTO_MODES } from './autowork.js'
|
|
21
20
|
import { migrateDir } from './legacy.js'
|
|
22
21
|
import { writePrivateJson } from './private-file.js'
|
|
23
22
|
import { autoToolFor } from './autowork-tools.js'
|
|
24
|
-
import { readAccount, saveAccount,
|
|
23
|
+
import { readAccount, saveAccount, 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, listSandboxKeys, createSandboxKey, rotateSandboxKey, revokeSandboxKey, removeSandboxKey, aiVmFollow, aiVmUnfollow, aiVmStatus, aiVmConnect, aiVmUpdate, aiVmSignIn, aiVmWake, aiVmDisconnect, voiceIce, transcribeVoice, apiUrl } from './account.js'
|
|
25
24
|
import { aiName } from './persona.js'
|
|
26
25
|
import { effectiveAccess, builtinType } from './session-access.js'
|
|
27
26
|
import { cleanSessionName, BAD_SESSION_NAME, SESSION_NAME_MAX } from './session-name.js'
|
|
@@ -68,13 +67,9 @@ function profile () {
|
|
|
68
67
|
shareAgent: s.shareAgent !== false,
|
|
69
68
|
summarize: !!s.summarize,
|
|
70
69
|
preferLocal: !!s.preferLocal,
|
|
71
|
-
// Quilt pushes your local commits for you on branches that sync with GitHub: off unless you turn it on.
|
|
72
|
-
pushLocalCommits: s.pushLocalCommits === true,
|
|
73
70
|
theme: THEMES.includes(s.theme) ? s.theme : 'dark', // dark unless you pick another
|
|
74
71
|
report: s.report !== false,
|
|
75
72
|
aiTasks: pickupMode(s.aiTasks),
|
|
76
|
-
// What agents may commit in sessions you start ('off', 'branches', 'any'); each session's owner can change it there.
|
|
77
|
-
agentCommits: AGENT_COMMITS.includes(s.agentCommits) ? s.agentCommits : 'branches',
|
|
78
73
|
// When work comes to your AI (autowork.js): 'chat', 'open' or 'auto', with its model, files and daily cap.
|
|
79
74
|
auto: publicConfig(autoConfig(s)),
|
|
80
75
|
// Sounds (src/ui/sounds.js): each on unless switched off.
|
|
@@ -99,7 +94,6 @@ function updateProfile (b) {
|
|
|
99
94
|
if ('shareAgent' in b) patch.shareAgent = b.shareAgent ? undefined : false
|
|
100
95
|
if ('summarize' in b) patch.summarize = b.summarize ? true : undefined
|
|
101
96
|
if ('preferLocal' in b) patch.preferLocal = b.preferLocal ? true : undefined
|
|
102
|
-
if ('pushLocalCommits' in b) patch.pushLocalCommits = b.pushLocalCommits ? true : undefined
|
|
103
97
|
if ('theme' in b) {
|
|
104
98
|
if (!THEMES.includes(b.theme)) throw httpError(400, 'Pick Light, Dark or System.')
|
|
105
99
|
patch.theme = b.theme === 'dark' ? undefined : b.theme
|
|
@@ -109,10 +103,6 @@ function updateProfile (b) {
|
|
|
109
103
|
if (!PICKUP_MODES.includes(b.aiTasks)) throw httpError(400, 'Pick Off, Assigned to it, or Assigned and unassigned.')
|
|
110
104
|
patch.aiTasks = b.aiTasks === 'off' ? undefined : b.aiTasks
|
|
111
105
|
}
|
|
112
|
-
if ('agentCommits' in b) {
|
|
113
|
-
if (!AGENT_COMMITS.includes(b.agentCommits)) throw httpError(400, 'Pick Not at all, To branches of their own, or To any branch.')
|
|
114
|
-
patch.agentCommits = b.agentCommits === 'branches' ? undefined : b.agentCommits
|
|
115
|
-
}
|
|
116
106
|
if ('autoWork' in b) {
|
|
117
107
|
if (!AUTO_MODES.includes(b.autoWork)) throw httpError(400, 'Pick Leave it for my chat, Open it in my tool, or Work on its own.')
|
|
118
108
|
patch.autoWork = b.autoWork === 'chat' ? undefined : b.autoWork
|
|
@@ -480,8 +470,6 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
480
470
|
apiAuth: personApiAuth,
|
|
481
471
|
// A new session is named after its folder (the owner can rename it later).
|
|
482
472
|
startName: mode === 'create' ? cleanSessionName([...path.basename(dir)].slice(0, SESSION_NAME_MAX).join('')) || '' : '',
|
|
483
|
-
// Your default for what agents may commit, set on the new room once the relay makes you its owner.
|
|
484
|
-
startAgentCommits: mode === 'create' ? me.agentCommits : '',
|
|
485
473
|
// A new session is a quilt from the start when the API says so (quilt-default.js).
|
|
486
474
|
newSession: mode === 'create',
|
|
487
475
|
workspace: workspace || '',
|
|
@@ -935,15 +923,7 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
935
923
|
return { ok: true }
|
|
936
924
|
},
|
|
937
925
|
'GET /api/access-types': () => asAccount(async (token) => ({ types: await listAccessTypes({ token }) })),
|
|
938
|
-
// Quilt's GitHub App: whether this account connected GitHub, and the link that connects it (opened in the browser).
|
|
939
926
|
...cloudRoutes,
|
|
940
|
-
'GET /api/github': () => asAccount(async (token) => githubStatus({ token })),
|
|
941
|
-
'POST /api/github/connect': (b) => asAccount(async (token) => ({ url: await githubConnectUrl({ token, repo: b && b.repo, ...(b && ['workspace', 'org'].includes(b.scope) ? { scope: b.scope, id: String(b.id || '') } : {}) }) })),
|
|
942
|
-
// GitHub for a workspace (plan §2): its connections, access mode, and its org's Require GitHub.
|
|
943
|
-
'GET /api/workspaces/:id/github': (b, id) => asAccount(async (token) => workspaceGithub({ token, id: needWorkspaceId(id) })),
|
|
944
|
-
'POST /api/workspaces/:id/github': (b, id) => asAccount(async (token) => setWorkspaceGithub({ token, id: needWorkspaceId(id), ...('access' in b ? { access: String(b.access || ''), repos: Array.isArray(b.repos) ? b.repos.map(String) : [] } : {}), ...('agentCommits' in b ? { agentCommits: String(b.agentCommits || '') } : {}) })),
|
|
945
|
-
'POST /api/workspaces/:id/github/disconnect': (b, id) => asAccount(async (token) => removeWorkspaceGithub({ token, id: needWorkspaceId(id), connectedBy: b && b.connectedBy ? String(b.connectedBy) : '' })),
|
|
946
|
-
'POST /api/orgs/:slug/github': (b, slug) => asAccount(async (token) => setOrgGithub({ token, slug: String(slug || ''), ...(b && 'requireGithub' in b ? { requireGithub: b.requireGithub === true } : {}), ...(b && 'agentCommits' in b ? { agentCommits: String(b.agentCommits || '') } : {}) })),
|
|
947
927
|
'GET /api/collaborators': () => asAccount(async (token) => ({ collaborators: await listCollaborators({ token }) })),
|
|
948
928
|
'GET /api/sessions/:id/grants': (b, id) => { const s = owned(id); return asAccount(async (token) => ({ grants: await listGrants({ token, room: s.room }) })) },
|
|
949
929
|
'POST /api/sessions/:id/members/access': (b, id) => setAccess(id, b),
|
|
@@ -1040,7 +1020,6 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
1040
1020
|
}),
|
|
1041
1021
|
'POST /api/sessions': (b) => start(b),
|
|
1042
1022
|
'POST /api/sessions/:id/open-in': async (b, id) => { await openIn(String(b.app || ''), get(id).root); return { ok: true } },
|
|
1043
|
-
'GET /api/sessions/:id/merges': (b, id) => ({ merges: get(id).mergeList() }),
|
|
1044
1023
|
// Gathering the project's AI rules into AGENTS.md (project-rules.js): look first, then apply what was shown.
|
|
1045
1024
|
'GET /api/sessions/:id/rules/gather': (b, id) => get(id).rulesGatherPlan(),
|
|
1046
1025
|
'POST /api/sessions/:id/rules/gather': (b, id) => get(id).applyRulesGather(String(b.key || '')),
|
|
@@ -1115,10 +1094,8 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
1115
1094
|
'GET /api/sessions/:id/blame': (b, id, url) => get(id).quiltBlame(String(url.searchParams.get('path') || '')),
|
|
1116
1095
|
'GET /api/sessions/:id/patch': (b, id, url) => get(id).quiltShow(String(url.searchParams.get('commit') || ''), String(url.searchParams.get('path') || '')),
|
|
1117
1096
|
'GET /api/sessions/:id/search': (b, id, url) => get(id).quiltSearch(String(url.searchParams.get('q') || '')),
|
|
1118
|
-
// Commits pushed or merged elsewhere: fetch and bring them in now (they also come in by themselves).
|
|
1119
1097
|
// In a session that moved into Seam: the square panel's Commit and Merge onto main, as this member (control.js seamTool).
|
|
1120
1098
|
'POST /api/sessions/:id/seam': (b, id) => squareTool(get(id), b),
|
|
1121
|
-
'POST /api/sessions/:id/branches/sync': async (b, id) => { const r = await get(id).syncBranchNow(); pushStatus(id); return r },
|
|
1122
1099
|
'GET /api/sessions/:id/file': (b, id, url) => {
|
|
1123
1100
|
const f = get(id).readShared(url.searchParams.get('path'))
|
|
1124
1101
|
if (!f) throw httpError(404, 'That file is not in this session.')
|
|
@@ -1131,8 +1108,6 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
1131
1108
|
},
|
|
1132
1109
|
'POST /api/sessions/:id/members/deny': async (b, id) => (await admitter(id).deny(b.key), { ok: true }),
|
|
1133
1110
|
'POST /api/sessions/:id/admit-by': async (b, id) => (await owned(id).setAdmitBy(b.admitBy), { ok: true, admitBy: b.admitBy }),
|
|
1134
|
-
// Owner only: the read-only GitHub token the relay brings commits in with while everyone's offline ('' clears it).
|
|
1135
|
-
'POST /api/sessions/:id/github-token': (b, id) => owned(id).setGithubToken(b.token),
|
|
1136
1111
|
'POST /api/sessions/:id/members/set': async (b, id) => (await get(id).setMember(b.key, { role: b.role, scopes: b.scopes }), { ok: true }),
|
|
1137
1112
|
'POST /api/sessions/:id/members/remove': (b, id) => removeMember(id, b.key),
|
|
1138
1113
|
// Owner only: a link a chat-only AI (ChatGPT, claude.ai, Grok…) works through (chat-links.js).
|
|
@@ -1201,20 +1176,8 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
1201
1176
|
'GET /api/github/status': () => { throw httpError(410, GITHUB_GONE) },
|
|
1202
1177
|
'GET /api/github/repos': () => { throw httpError(410, GITHUB_GONE) },
|
|
1203
1178
|
'GET /api/github/branches': () => { throw httpError(410, GITHUB_GONE) },
|
|
1204
|
-
'POST /api/sessions/:id/commit-request': (b, id) => { const r = get(id).requestCommit(b.message, { files: Array.isArray(b.files) ? b.files : null, task: b.task || null, by: b.by ? String(b.by).slice(0, 80) : null }); pushStatus(id); return r },
|
|
1205
|
-
// Commits a request in this folder (commit.js), and what is not committed here yet, by who made it.
|
|
1206
|
-
'POST /api/sessions/:id/commit-session': async (b, id) => { try { return await get(id).commitSessionWork(String(b.message || ''), { mine: b.mine === true }) } finally { pushStatus(id) } },
|
|
1207
|
-
'POST /api/sessions/:id/commit-request/commit': async (b, id) => { try { return await get(id).commitRequest(String(b.id || '')) } finally { pushStatus(id) } },
|
|
1208
|
-
'POST /api/sessions/:id/agent-commits': async (b, id) => { try { return await get(id).setAgentCommits(b.mode) } finally { pushStatus(id) } },
|
|
1209
1179
|
// "Make this a quilt" (Seam's plans 16, flag 9): the owner's button. The relay makes the session a quilt and moves it into its squares, as the owner.
|
|
1210
1180
|
'POST /api/sessions/:id/quilt-make': async (b, id) => { try { return await get(id).quiltMake(process.env.SEAM_TOKEN || (readAccount() || {}).token) } finally { pushStatus(id) } },
|
|
1211
|
-
'POST /api/sessions/:id/github-sync': async (b, id) => { try { return await get(id).setGithubSync(b.on !== false) } finally { pushStatus(id) } },
|
|
1212
|
-
'POST /api/sessions/:id/large-to-relay': async (b, id) => { try { return await get(id).setLargeToRelay(b.on === true) } finally { pushStatus(id) } },
|
|
1213
|
-
'POST /api/sessions/:id/github/recheck': async (b, id) => { try { return { github: await get(id).githubRecheck() } } finally { pushStatus(id) } },
|
|
1214
|
-
'GET /api/sessions/:id/uncommitted': (b, id) => get(id).uncommittedWork(),
|
|
1215
|
-
// The git panel: your own work not committed here (yours and your AIs'), each boxed or not.
|
|
1216
|
-
'GET /api/sessions/:id/mine-uncommitted': (b, id) => get(id).myUncommitted(),
|
|
1217
|
-
'POST /api/sessions/:id/commit-request/done': (b, id) => { const done = get(id).resolveCommitRequests({ ids: b.id ? [String(b.id)] : null }); pushStatus(id); return { done } },
|
|
1218
1181
|
'GET /api/fs': (b, id, url) => listDir(url.searchParams.get('path') || os.homedir()),
|
|
1219
1182
|
// Reply first, then shut down, so the page hears back before we exit.
|
|
1220
1183
|
'POST /api/shutdown': () => {
|