heyquilt 0.3.138 → 0.3.141

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 (53) hide show
  1. package/RELEASES.md +10 -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-follow.js +18 -5
  21. package/src/session.js +58 -3526
  22. package/src/status.js +2 -58
  23. package/src/turn-sync.js +15 -2
  24. package/src/ui/changes.js +5 -14
  25. package/src/ui/chat.js +0 -1
  26. package/src/ui/common.js +0 -2
  27. package/src/ui/git-rule.js +2 -30
  28. package/src/ui/home.js +3 -76
  29. package/src/ui/session.js +34 -528
  30. package/src/ui/tree.js +2 -8
  31. package/src/ui/workspaces.js +2 -122
  32. package/src/ui-server.js +1 -38
  33. package/src/api/routes/github.js +0 -380
  34. package/src/branches.js +0 -194
  35. package/src/clash.js +0 -128
  36. package/src/commit-request.js +0 -59
  37. package/src/commit.js +0 -213
  38. package/src/git-hook.js +0 -48
  39. package/src/gitignore.js +0 -49
  40. package/src/gitstate.js +0 -1069
  41. package/src/relay-ci.js +0 -111
  42. package/src/relay-clash.js +0 -214
  43. package/src/relay-commit.js +0 -923
  44. package/src/relay-filekey.js +0 -46
  45. package/src/relay-git.js +0 -274
  46. package/src/relay-githttp.js +0 -166
  47. package/src/relay-github.js +0 -109
  48. package/src/relay-outbound.js +0 -1295
  49. package/src/relay-repo-moves.js +0 -43
  50. package/src/relay-sync.js +0 -795
  51. package/src/relay-upstream.js +0 -889
  52. package/src/sync-words.js +0 -42
  53. package/src/upstream.js +0 -192
package/src/status.js CHANGED
@@ -2,36 +2,11 @@
2
2
  // `quilt status`, and the MCP `quilt_status` tool, so every tool sees the same view.
3
3
 
4
4
  import { authorOf } from './ui/chat.js'
5
- import { branchesMarkdown } from './branches.js'
6
5
  import { taskMarkdown } from './tasks.js'
7
6
  import { mergeAction } from './merges.js'
8
7
  import { catchUpMarkdown } from './catchup.js'
9
- import { DEFAULT_KEY } from './branchdocs.js'
10
8
  import { voiceStatusLines } from './voice.js'
11
9
 
12
- /** A shell word for a path (quoted only when it needs to be). */
13
- const shellWord = (p) => /^[\w./@+-]+$/.test(p) ? p : `'${p.replace(/'/g, "'\\''")}'`
14
-
15
- /** The one line that makes way for a pull over files the session put here, and pulls. */
16
- export function makeWayCommand (paths) {
17
- return `rm ${paths.map(shellWord).join(' ')} && git pull --autostash`
18
- }
19
-
20
- /**
21
- * What to tell a person's AI when the upstream adds files the session already
22
- * put in this folder (git refuses to pull over untracked files). `adds`: [{ path, same, waiting }].
23
- */
24
- export function pullAdvice ({ upstream, behind, adds }) {
25
- const inWay = adds.filter((a) => !a.waiting)
26
- const differ = adds.filter((a) => !a.same)
27
- const list = (xs) => xs.map((a) => a.path).join(', ')
28
- const lines = [`${upstream} has ${behind} commit${behind === 1 ? '' : 's'} adding ${list(adds)}, which the session already put in this folder${differ.length ? '' : ' with the same content'}.`]
29
- if (inWay.length) lines.push(`git won't pull over untracked files: make way and pull with \`${makeWayCommand(inWay.map((a) => a.path))}\`. Quilt keeps them for everyone and doesn't share the removal (a plain delete at any other time deletes for everyone).`)
30
- else lines.push('They are out of the way: pull now (`git pull --autostash`). Quilt keeps them for everyone meanwhile.')
31
- for (const a of differ) lines.push(`${a.path} differs from the session's: after the pull Quilt merges the two (a merge record if they clash).`)
32
- return lines.join(' ')
33
- }
34
-
35
10
  const ago = (ts) => {
36
11
  const s = Math.max(0, Math.round((Date.now() - ts) / 1000))
37
12
  if (s < 60) return `${s}s ago`
@@ -89,39 +64,11 @@ export function renderStatus (st, { asAi = false, mentionYours = false } = {}) {
89
64
  out.push(taskMarkdown(st.tasks, st.me.name, { tool: st.me.tool, asAi, mentionYours }))
90
65
  out.push('')
91
66
 
92
- if (st.branches && st.branches.length) {
93
- out.push('## Branches')
94
- out.push(branchesMarkdown(st.branches, { limit: 6 }))
95
- out.push('')
96
- }
97
-
98
- const openCommits = (st.commits || []).filter((r) => r.state === 'open')
99
- if (openCommits.length) {
100
- out.push('## Commit requests')
101
- for (const r of openCommits) {
102
- const who = authorOf(r) === st.me.name ? 'you' : authorOf(r)
103
- const branch = r.branch && r.branch !== DEFAULT_KEY ? ` on \`${r.branch}\`` : ''
104
- out.push(`- [${r.id}] ${ago(r.ts)}: ${who} asked for a commit${branch}: ${r.message}${r.error ? `. Last try: ${r.error}` : ''}`)
105
- if (Array.isArray(r.files) && r.files.length) out.push(` Files: ${r.files.slice(0, 20).join(', ')}${r.files.length > 20 ? `, and ${r.files.length - 20} more` : ''}`)
106
- if (r.description) out.push(` ${String(r.description).replace(/\s+/g, ' ')}`)
107
- }
108
- if (st.canCommit) out.push('This folder has git: commit one in the app (Commit and push), or by hand and close it with quilt_commit_request_done.')
109
- out.push('')
110
- }
111
-
112
- const pull = st.git && st.git.pull
113
- if (pull && pull.adds && pull.adds.length) {
114
- out.push('## Pulling')
115
- out.push(pullAdvice(pull))
116
- for (const a of pull.adds) out.push(`- \`${a.path}\`: ${a.waiting ? 'out of the way, waiting for your pull' : a.same ? 'same content as the session\'s' : 'differs from the session\'s'}`)
117
- out.push('')
118
- }
119
-
120
67
  // Boxes (docs/superpowers/specs/2026-10-16-git-boxes-design.md): unfinished work only in its owner's folder.
121
68
  const boxed = st.claims.filter((c) => c.box)
122
69
  if (boxed.length) {
123
70
  out.push('## Boxed work')
124
- for (const c of boxed) out.push(c.by === st.me.name ? `- \`${c.pattern}\`: your unfinished work: in your folder only, until you commit it (or quilt_unpack)` : `- \`${c.pattern}\`: ${c.by}'s unfinished work: not in your folder until they commit or unpack it`)
71
+ for (const c of boxed) out.push(c.by === st.me.name ? `- \`${c.pattern}\`: your unfinished work: in your folder only, until you unpack it (quilt_unpack)` : `- \`${c.pattern}\`: ${c.by}'s unfinished work: not in your folder until they unpack it`)
125
72
  out.push('')
126
73
  }
127
74
  const plain = st.claims.filter((c) => !c.box)
@@ -150,10 +97,7 @@ export function renderStatus (st, { asAi = false, mentionYours = false } = {}) {
150
97
  if (!act.length) out.push('_Nothing yet._')
151
98
  for (const a of act) {
152
99
  const who = a.by === st.me.name ? 'you' : a.by
153
- if (a.kind === 'pulled') out.push(`- ${ago(a.ts)}: ${who} pulled ${a.detail || 'commits'}`)
154
- else if (a.kind === 'brought') out.push(`- ${ago(a.ts)}: ${who} brought in ${a.detail || 'commits'}${a.branch ? ` on \`${a.branch}\`` : ''}`)
155
- else if (a.kind === 'switched') out.push(`- ${ago(a.ts)}: ${who} switched to \`${a.branch}\``)
156
- else out.push(`- ${ago(a.ts)}: ${who} ${a.kind} \`${a.path}\`${a.detail ? ` (${a.detail})` : ''}`)
100
+ out.push(`- ${ago(a.ts)}: ${who} ${a.kind} \`${a.path}\`${a.detail ? ` (${a.detail})` : ''}`)
157
101
  }
158
102
  out.push('')
159
103
 
package/src/turn-sync.js CHANGED
@@ -7,7 +7,6 @@ import fs from 'node:fs'
7
7
  import path from 'node:path'
8
8
  import crypto from 'node:crypto'
9
9
  import { loadIgnore, walk, isIgnored, isSafeRelPath, resolveInside, looksBinary, LARGE_FILE_BYTES } from './fsutil.js'
10
- import { headRef } from './gitstate.js'
11
10
  import { hostedClient } from './hosted-client.js'
12
11
 
13
12
  /** Texts larger than this, and binaries this large or larger, are not carried by quilt sync. */
@@ -119,7 +118,7 @@ export async function turnSync ({ dir = process.cwd(), key, fetch: fetchImpl = g
119
118
 
120
119
  for (let round = 0; round < ROUNDS; round++) {
121
120
  const files = scanFolder(root, ig)
122
- const plan = await step({ step: 'plan', files, ...(round === 0 && headRef(root) ? { branch: headRef(root) } : {}) })
121
+ const plan = await step({ step: 'plan', files, ...(round === 0 && headBranch(root) ? { branch: headBranch(root) } : {}) })
123
122
  sum.branch = plan.branch || sum.branch
124
123
  add(sum.skipped, plan.skipped || [])
125
124
  // Versions of ours that waited for a holder went in as the plan was made.
@@ -210,3 +209,17 @@ export function describeSync (s, { name = '' } = {}) {
210
209
  if (s.skipped.length) lines.push(` Not carried by quilt sync (too large, or kept in Quilt's storage): ${list(s.skipped)}.`)
211
210
  return lines.join('\n')
212
211
  }
212
+
213
+ /** The branch a git checkout in `root` is on, read from .git/HEAD (no git run); null when none or detached. */
214
+ function headBranch (root) {
215
+ try {
216
+ let dir = path.join(root, '.git')
217
+ if (fs.statSync(dir).isFile()) {
218
+ const m = /^gitdir:\s*(.+)$/m.exec(fs.readFileSync(dir, 'utf8'))
219
+ if (!m) return null
220
+ dir = path.resolve(root, m[1].trim())
221
+ }
222
+ const m = /^ref:\s*refs\/heads\/(.+)$/m.exec(fs.readFileSync(path.join(dir, 'HEAD'), 'utf8'))
223
+ return m ? m[1].trim() : null
224
+ } catch { return null }
225
+ }
package/src/ui/changes.js CHANGED
@@ -1,8 +1,8 @@
1
- // The Changes part of the git panel in the session top bar (session.js: the branch chip opens
1
+ // The Changes part of the source control panel in the session top bar (session.js: the branch chip opens
2
2
  // it): what has changed in this session and who changed it, grouped by person or by file. Everyone sees
3
3
  // it, since the counts live in the shared document. Clicking a file opens it
4
4
  // on its Changes tab: every change to it, with who made it and the diff.
5
- import { I, state, $, esc, api, ago, avatar, colorFor, loadingNote, remember, recall } from './common.js'
5
+ import { state, $, esc, api, ago, avatar, colorFor, loadingNote, remember, recall } from './common.js'
6
6
  import { unseenCount, newestChange } from './changes-seen.js'
7
7
 
8
8
  let sid = null // session shown
@@ -12,7 +12,6 @@ let refreshTimer = null
12
12
  let openFile = () => {}
13
13
  let closePanel = () => {}
14
14
  let counted = () => {}
15
- const openPulls = new Set() // people whose "Pulled from git" group is open, kept across repaints
16
15
 
17
16
  export const changesMarkup = () => `
18
17
  <section class="git-sec chg-panel" id="chg-panel" aria-label="Changes in this session">
@@ -40,10 +39,6 @@ export function bindChanges (id, signal, { onOpen, close, onCount } = {}) {
40
39
  view = b.dataset.view
41
40
  paint()
42
41
  })
43
- $('#chg-list').addEventListener('toggle', (e) => {
44
- const d = e.target.closest('details.chg-pulled')
45
- if (d) d.open ? openPulls.add(d.dataset.name) : openPulls.delete(d.dataset.name)
46
- }, true)
47
42
  $('#chg-list').addEventListener('click', (e) => {
48
43
  const b = e.target.closest('button[data-path]')
49
44
  if (!b) return
@@ -162,19 +157,15 @@ function paint () {
162
157
  if (view === 'people') {
163
158
  list.innerHTML = data.people.map((p) => {
164
159
  const own = p.fileCount || 0
165
- // What a git pull brought is other people's commits: shown apart, folded, never added to their own count.
166
- const pulled = p.pulled && p.pulled.fileCount
167
- ? `<details class="chg-pulled"${openPulls.has(p.name) ? ' open' : ''} data-name="${esc(p.name)}"><summary>${I.down}<span>Pulled from git</span><span class="hint">${plural(p.pulled.fileCount, 'file')} · ${esc(ago(p.pulled.ts))}</span><span class="spacer"></span>${delta(p.pulled)}</summary>${rows(p.pulled.files)}</details>`
168
- : ''
169
160
  return `
170
161
  <section class="chg-person">
171
162
  <div class="chg-row">${avatar(p.name, colorFor(p.name, colors.get(p.name)))}<b>${esc(who(p.name))}</b>
172
- <span class="hint">${own ? `${plural(own, 'file')} · ${esc(ago(p.files[0].ts))}` : 'only pulled from git'}</span><span class="spacer"></span>${own ? delta(p) : ''}</div>
173
- ${own ? rows(p.files) : ''}${pulled}
163
+ <span class="hint">${own ? `${plural(own, 'file')} · ${esc(ago(p.files[0].ts))}` : ''}</span><span class="spacer"></span>${own ? delta(p) : ''}</div>
164
+ ${own ? rows(p.files) : ''}
174
165
  </section>`
175
166
  }).join('')
176
167
  } else {
177
168
  list.innerHTML = `<ul class="chg-files">${data.files.slice(0, SHOWN).map((f) => fileRow(f, `<span class="hint">${esc(ago(f.ts))}</span>${delta(f)}
178
- <div class="chg-by">${f.by.map((b) => `<span class="chg-chip${b.pulled ? ' pulled' : ''}" style="--c:${esc(colorFor(b.name, colors.get(b.name)))}"${b.pulled ? ` title="${esc(who(b.name))} brought this in with a git pull"` : ''}>${esc(who(b.name))}${b.pulled ? ' <i>pulled</i>' : ''} <span class="add">+${b.added}</span> <span class="del">−${b.removed}</span></span>`).join('')}</div>`)).join('')}${more(data.files.length)}</ul>`
169
+ <div class="chg-by">${f.by.map((b) => `<span class="chg-chip" style="--c:${esc(colorFor(b.name, colors.get(b.name)))}">${esc(who(b.name))} <span class="add">+${b.added}</span> <span class="del">−${b.removed}</span></span>`).join('')}</div>`)).join('')}${more(data.files.length)}</ul>`
179
170
  }
180
171
  }
package/src/ui/chat.js CHANGED
@@ -124,7 +124,6 @@ export const isRelay = (name) => typeof name === 'string' && RELAY_NAMES.has(nam
124
124
 
125
125
  /** What the relay answers, offered in the composer after @Quilt. `owner`: only the session owner may ask it. */
126
126
  export const RELAY_QUESTIONS = [
127
- { id: 'git', text: 'What is the status of the relay against git?' },
128
127
  { id: 'members', text: 'How many members are connected?' },
129
128
  { id: 'branches', text: 'How many branches are tracked?' },
130
129
  { id: 'trim', text: 'Trim unused branches', owner: true }
package/src/ui/common.js CHANGED
@@ -119,8 +119,6 @@ export const ago = (ts) => {
119
119
  return new Date(ts).toLocaleDateString()
120
120
  }
121
121
  export const clock = (ts) => new Date(ts).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
122
- /** Git for this session (git for everyone): the toast that confirms a pick in its control (git-rule.js). */
123
- export const AGENT_COMMIT_WORDS = { off: 'Git is off for everyone here', branches: 'Git is on for everyone here: agents push to branches of their own, with pull requests', any: 'Git is on for everyone here: agents may push to any branch' }
124
122
 
125
123
  // People's colours. None sits on the vermilion accent, so nobody's lane looks live or alarming; src/session.js keeps the same list.
126
124
  export const PALETTE = ['#4f4fa0', '#3b6a9a', '#4a7a45', '#855a9c', '#a8701c', '#2e7a80', '#9c4f6b']
@@ -44,32 +44,12 @@ export function gitControl ({ value = '', rule = null, canEdit = true } = {}) {
44
44
  return { segments, rulePill: `${who}: ${UP_TO[ceiling]}`, lockedHint }
45
45
  }
46
46
 
47
- /** The status line ("Git is on for everyone here, through …"), its first clause bold as in the mockup. */
48
- export function gitStatusParts (words) {
49
- const s = String(words || '')
50
- const m = s.match(/^(.*?)(, | \(|: )/)
51
- return m ? { strong: m[1], rest: s.slice(m[1].length) } : { strong: s, rest: '' }
52
- }
53
-
54
47
  const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]))
55
48
 
56
- /** The control as the app draws it: `attr` is the data attribute each button carries its value in. */
57
- export function gitSegHtml (model, { attr, label }) {
58
- return `<div class="seg" role="group" aria-label="${esc(label)}">${model.segments.map((s) =>
59
- `<button type="button"${s.on ? ' class="on"' : ''} ${attr}="${s.value}" aria-pressed="${s.on}"${s.disabled ? ' disabled' : ''}><b>${esc(s.head)}</b>${esc(s.sub)}</button>`).join('')}</div>`
60
- }
61
-
62
- /** A choice in words, as the control shows it: "On, agents: own branches"; '' when nothing holds. */
63
- export function gitChoiceWords (value) {
64
- const s = GIT_SEGMENTS.find((x) => x.value === value)
65
- return s ? `${s.head}, ${s.sub}` : ''
66
- }
67
-
68
49
  /**
69
50
  * The app's Settings (for every session) opened from inside a session: a card at the top of its Version
70
51
  * control section that says what git is in this session, as its Session settings say it (the same values:
71
52
  * the session's own choice, capped by a rule above it, and its status line), with the button that opens
72
- * them. The default under it is only for sessions started later, so the two never read as two answers.
73
53
  * `session`: { name, value, rule, words }.
74
54
  */
75
55
  export function thisSessionGitHtml (session) {
@@ -77,21 +57,13 @@ export function thisSessionGitHtml (session) {
77
57
  // A session in Seam (`seam`: its quilt): people and AIs commit in their squares; git's choice is put away until it moves back.
78
58
  if (session.seam) {
79
59
  return `<section class="card settings-sec" id="this-session-git-sec">
80
- <div class="sec-intro"><h2>This session${session.name ? `: ${esc(session.name)}` : ''}</h2><p>This session's code is committed and merged in Seam, as the quilt ${esc(session.seam)}; the session itself is Quilt. Everyone's folder follows its square, people and their AIs commit there and merge onto main, and GitHub gets main from Seam by itself. The account default below is only for sessions on git.</p></div>
60
+ <div class="sec-intro"><h2>This session${session.name ? `: ${esc(session.name)}` : ''}</h2><p>This session's code is committed and merged in Seam, as the quilt ${esc(session.seam)}; the session itself is Quilt. Everyone's folder follows its square, people and their AIs commit there and merge onto main, and GitHub gets main from Seam by itself.</p></div>
81
61
  <div class="sec-body">
82
62
  <div class="sec-actions"><span class="hint">Where this session's history is, in one place.</span><button class="btn" type="button" data-open-session-vc>Open Session settings</button></div>
83
63
  </div>
84
64
  </section>`
85
65
  }
86
- const m = gitControl({ value: session.value || 'branches', rule: session.rule && session.rule.mode ? session.rule : null })
87
- const holds = (m.segments.find((s) => s.on) || {}).value || ''
88
- const line = session.words ? gitStatusParts(session.words) : null
89
66
  return `<section class="card settings-sec" id="this-session-git-sec">
90
- <div class="sec-intro"><h2>This session${session.name ? `: ${esc(session.name)}` : ''}</h2><p>What git is right now in the session you have open. Its owner changes it in Session settings, under Version control. The account default below is only for sessions you start from now on.</p></div>
91
- <div class="sec-body">
92
- <div class="kv"><span>This session's source control</span><b data-this-session-git="${esc(holds)}">${esc(gitChoiceWords(holds) || 'Not set')}</b>${m.rulePill ? `<span class="pill rule">${esc(m.rulePill)}</span>` : ''}</div>
93
- ${line ? `<p class="hint"><b>${esc(line.strong)}</b>${esc(line.rest)}</p>` : ''}
94
- <div class="sec-actions"><span class="hint">Everything about this session's git and GitHub is in one place.</span><button class="btn" type="button" data-open-session-vc>Open Session settings</button></div>
95
- </div>
67
+ <div class="sec-intro"><h2>This session${session.name ? `: ${esc(session.name)}` : ''}</h2><p>This session's code is not in Seam yet: its owner makes it a quilt from Session settings.</p></div>
96
68
  </section>`
97
69
  }
package/src/ui/home.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Home and Settings: a sidebar with your profile and open sessions, next to
2
2
  // either the start/join page or your settings. Sessions themselves live in session.js.
3
- import { I, state, $, esc, basename, ago, toast, api, ask, decodeInvite, avatar, PALETTE, AGENT_COMMIT_WORDS, loadingNote } from './common.js'
3
+ import { I, state, $, esc, basename, ago, toast, api, ask, decodeInvite, avatar, PALETTE, loadingNote } from './common.js'
4
4
  import { go, pickFolder, signedOutNow, agentInviteHtml } from './app.js'
5
5
  import { agentPaste } from './invite.js'
6
6
  import { openKindMenu, kindLineHtml } from './agent-kinds.js'
@@ -13,7 +13,7 @@ import { agentCardHtml, bindPlacement, placeableWorkspaces } from './agent-place
13
13
  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
- import { gitControl, gitSegHtml, thisSessionGitHtml } from './git-rule.js'
16
+ import { thisSessionGitHtml } from './git-rule.js'
17
17
  import { cloudAiCardHtml, bindCloudAi, cloudSetupOn, cloudSetupHtml, cloudSetupStep } from './cloud-ai.js'
18
18
  import { feedbackDockHtml, bindFeedback } from './feedback.js'
19
19
 
@@ -714,19 +714,6 @@ function settingsHtml (session = null) {
714
714
  </section>` : ''}
715
715
 
716
716
  ${show('agents-git') ? thisSessionGitHtml(session) : ''}
717
- ${show('agents-git') ? `<section class="card settings-sec" id="agents-git-sec">
718
- <div class="sec-intro"><h2>Account default for new sessions</h2><p>Your default git for everyone in the sessions you start from now on. It doesn't change a session that is already running. People and agents use git from their own folders, as themselves, with short-lived credentials from Quilt's GitHub app bound to the session's repository: nobody else connects or sets anything up.</p></div>
719
- <div class="sec-body">
720
- <div class="field">
721
- <span class="label" id="s-agentcommits-l">Account default source control</span>
722
- <div id="s-agentcommits">${gitSegHtml(gitControl({ value: p.agentCommits || 'branches' }), { attr: 'data-git-default', label: 'Account default source control' })}</div>
723
- <span class="hint">Git is for everyone in the session, people and agents, through one GitHub connection. Off for everyone: nobody pushes through Quilt, and agents ask a person to commit. Branches of their own: a task puts the agent on quilt/&lt;agent&gt;/&lt;task&gt;, its push opens a pull request, and a person merges by moving the task to Done. Any branch: agents may push to the session's branch and merge too. Saved at once, for sessions you start from now on; a running session's owner changes it in that session's Session settings, under Version control.</span>
724
- </div>
725
- <div class="kv"><span>Your GitHub account</span><span id="agents-gh"><span class="hint">${loadingNote('Checking…')}</span></span></div>
726
- <div class="sec-actions"><span class="hint">Connect once, with a GitHub account that can write to your repositories. Git is then on for everyone in your sessions, people and agents; pushes come in within seconds.</span><button class="btn" type="button" id="agents-gh-connect" hidden>${I.link}<span>Connect GitHub</span></button></div>
727
- </div>
728
- </section>` : ''}
729
-
730
717
  ${show('profile') ? `<form class="card settings-sec" id="profile-sec" autocomplete="off">
731
718
  <div class="sec-intro"><h2>Profile</h2><p>How you show up to the people you code with.</p></div>
732
719
  <div class="sec-body">
@@ -781,7 +768,6 @@ function settingsHtml (session = null) {
781
768
  <span class="hint">Each session gets its own folder in here.</span>
782
769
  </div>
783
770
  ${toggle('preferLocal', p.preferLocal, 'Keep my files when joining a folder that has some', 'When off, their versions of the same files win.')}
784
- ${toggle('pushLocalCommits', p.pushLocalCommits, 'Push my local commits for me', 'When you commit in a folder whose branch syncs with GitHub, Quilt pushes the commit for you (through the relay if your git can\'t). When off, you push with git yourself.')}
785
771
  ${toggle('report', p.report, 'Send problem reports to Quilt', 'The app tells Quilt which actions you take (not what you read), whether they worked and how long they took, with the error message when something fails, your app version and OS. Never your files, your chats or your links.')}
786
772
  <div class="sec-actions"><span></span><button class="btn primary" type="submit">Save</button></div>
787
773
  </div>
@@ -914,63 +900,6 @@ function bindAgents (root) {
914
900
  }
915
901
  }
916
902
 
917
- /**
918
- * Agents and git: your default for what agents may commit in sessions you start (saved at once),
919
- * and this account's GitHub connection. Connect opens GitHub's install page; the card then watches
920
- * for the connection for up to ten minutes.
921
- */
922
- function bindAgentsGit (sec) {
923
- const box = $('#s-agentcommits', sec)
924
- const drawSeg = (v) => { box.innerHTML = gitSegHtml(gitControl({ value: v }), { attr: 'data-git-default', label: 'Account default source control' }) }
925
- box.addEventListener('click', async (e) => {
926
- const b = e.target.closest('[data-git-default]')
927
- if (!b || b.classList.contains('on')) return
928
- const v = b.dataset.gitDefault
929
- drawSeg(v)
930
- try {
931
- state.profile = await api('POST', '/api/settings', { agentCommits: v })
932
- toast(AGENT_COMMIT_WORDS[v] || 'Saved')
933
- } catch (err) {
934
- drawSeg(state.profile.agentCommits || 'branches')
935
- toast(err.message)
936
- }
937
- })
938
- const out = $('#agents-gh', sec)
939
- const btn = $('#agents-gh-connect', sec)
940
- const draw = (gh) => {
941
- if (!gh.available) { out.innerHTML = '<span class="hint">GitHub is not set up on this server.</span>'; btn.hidden = true; return }
942
- out.innerHTML = gh.connected ? `<b>Connected as @${esc(gh.login)}</b>` : '<span class="hint warn">Not connected</span> <span class="hint">(yours; a session can still have git on through another member\'s connection)</span>'
943
- btn.hidden = false
944
- btn.innerHTML = `${I.link}<span>${gh.connected ? 'Add repositories' : 'Connect GitHub'}</span>`
945
- }
946
- const load = () => api('GET', '/api/github').then(draw, (err) => { out.innerHTML = `<span class="hint warn">${esc(err.message)}</span>` })
947
- load()
948
- btn.onclick = async () => {
949
- let was = null
950
- try { was = (await api('GET', '/api/github')).login || null } catch {}
951
- btn.disabled = true
952
- try {
953
- const { url } = await api('POST', '/api/github/connect', {})
954
- window.open(url, '_blank', 'noopener')
955
- toast('Finish on GitHub, then come back')
956
- } catch (err) { btn.disabled = false; toast(err.message); return }
957
- out.innerHTML = `<span class="hint">${loadingNote('Finish on GitHub, then come back…')}</span>`
958
- const until = Date.now() + 10 * 60 * 1000
959
- while (sec.isConnected && Date.now() < until) {
960
- await new Promise((r) => setTimeout(r, 3000))
961
- let gh = null
962
- try { gh = await api('GET', '/api/github') } catch { continue }
963
- if (!gh.login || gh.login === was) continue
964
- btn.disabled = false
965
- draw(gh)
966
- toast(`Connected as @${gh.login}: agents can commit in your sessions now`)
967
- return
968
- }
969
- btn.disabled = false
970
- if (sec.isConnected) load()
971
- }
972
- }
973
-
974
903
  /** Wires the settings cards inside `root`; `refresh()` redraws them after a save. */
975
904
  function bindSettings (root, refresh) {
976
905
  // Tabs: the pick is kept on this computer, and the cards are drawn again.
@@ -1014,7 +943,7 @@ function bindSettings (root, refresh) {
1014
943
 
1015
944
  const sess = $('#sessions-sec', root)
1016
945
  if (sess) sess.querySelector('[data-browse-settings]').onclick = () => pickFolder($('#s-joindir', root))
1017
- if (sess) saveForm(sess, (f) => ({ joinDir: f.get('joinDir'), preferLocal: !!f.get('preferLocal'), pushLocalCommits: !!f.get('pushLocalCommits'), report: !!f.get('report') }))
946
+ if (sess) saveForm(sess, (f) => ({ joinDir: f.get('joinDir'), preferLocal: !!f.get('preferLocal'), report: !!f.get('report') }))
1018
947
 
1019
948
  const ai = $('#your-ai-sec', root)
1020
949
  if (ai) saveForm(ai, (f) => ({ shareAgent: !!f.get('shareAgent'), summarize: !!f.get('summarize') }))
@@ -1022,8 +951,6 @@ function bindSettings (root, refresh) {
1022
951
  // Your cloud AI: its card, or its setup in place of the tab (in the pop-up, its header and footer too).
1023
952
  if ($('#cloud-ai-sec', root) || $('.ai-flow-body', root)) bindCloudAi(root.closest('.settings-modal') || root, refresh)
1024
953
 
1025
- const agit = $('#agents-git-sec', root)
1026
- if (agit) bindAgentsGit(agit)
1027
954
 
1028
955
  // Appearance applies at once; the server puts it on <html> at the next launch.
1029
956
  $('#appearance-sec', root)?.addEventListener('click', async (e) => {