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.
Files changed (52) hide show
  1. package/RELEASES.md +6 -0
  2. package/bin/quilt.js +7 -143
  3. package/package.json +1 -1
  4. package/src/account.js +0 -29
  5. package/src/api/memory-store.js +0 -29
  6. package/src/api/routes/teams.js +0 -23
  7. package/src/api/server.js +5 -19
  8. package/src/api/supabase-store.js +0 -52
  9. package/src/branch-view.js +6 -131
  10. package/src/control.js +2 -33
  11. package/src/duties.js +1 -1
  12. package/src/github-commit.js +3 -315
  13. package/src/mcp.js +8 -157
  14. package/src/relay-ask.js +3 -66
  15. package/src/relay-cluster.js +3 -5
  16. package/src/relay-mcp.js +6 -418
  17. package/src/relay-prune.js +6 -145
  18. package/src/relay-turn-sync.js +1 -1
  19. package/src/server.js +33 -1331
  20. package/src/session.js +52 -3526
  21. package/src/status.js +2 -58
  22. package/src/turn-sync.js +15 -2
  23. package/src/ui/changes.js +5 -14
  24. package/src/ui/chat.js +0 -1
  25. package/src/ui/common.js +0 -2
  26. package/src/ui/git-rule.js +2 -30
  27. package/src/ui/home.js +3 -76
  28. package/src/ui/session.js +34 -528
  29. package/src/ui/tree.js +2 -8
  30. package/src/ui/workspaces.js +2 -122
  31. package/src/ui-server.js +1 -38
  32. package/src/api/routes/github.js +0 -380
  33. package/src/branches.js +0 -194
  34. package/src/clash.js +0 -128
  35. package/src/commit-request.js +0 -59
  36. package/src/commit.js +0 -213
  37. package/src/git-hook.js +0 -48
  38. package/src/gitignore.js +0 -49
  39. package/src/gitstate.js +0 -1069
  40. package/src/relay-ci.js +0 -111
  41. package/src/relay-clash.js +0 -214
  42. package/src/relay-commit.js +0 -923
  43. package/src/relay-filekey.js +0 -46
  44. package/src/relay-git.js +0 -274
  45. package/src/relay-githttp.js +0 -166
  46. package/src/relay-github.js +0 -109
  47. package/src/relay-outbound.js +0 -1295
  48. package/src/relay-repo-moves.js +0 -43
  49. package/src/relay-sync.js +0 -795
  50. package/src/relay-upstream.js +0 -889
  51. package/src/sync-words.js +0 -42
  52. 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>${ghBadge}
132
- ${recent && !gh && !(ownClaim && ownClaim.by === f.edited.by) ? editBadge(f, byOther) : ''}
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
  }
@@ -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, AGENT_COMMIT_WORDS } from './common.js'
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><button class="btn sm" data-ws-github title="GitHub for this workspace's sessions">${I.branch}<span>GitHub</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>
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, 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'
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': () => {