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.
- package/RELEASES.md +10 -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-follow.js +18 -5
- package/src/session.js +58 -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/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
|
|
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
|
-
|
|
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 &&
|
|
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
|
|
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 {
|
|
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))}` : '
|
|
173
|
-
${own ? rows(p.files) : ''}
|
|
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
|
|
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']
|
package/src/ui/git-rule.js
CHANGED
|
@@ -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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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
|
|
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>
|
|
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,
|
|
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 {
|
|
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/<agent>/<task>, 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'),
|
|
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) => {
|