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/session.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// AI conversations at once), one person's AI chat, a shared file or the task board;
|
|
3
3
|
// the team chat on the right.
|
|
4
4
|
import { mountSecrets } from './secrets.js'
|
|
5
|
-
import { TOKEN, I, state, $, esc, basename, bytes, clock, ago, avatar, colorFor, toast, api, ask, remember, recall,
|
|
5
|
+
import { TOKEN, I, state, $, esc, basename, bytes, clock, ago, avatar, colorFor, toast, api, ask, remember, recall, toolsOf, NO_POSTING, ACCOUNT_KEY, loadAccessTypes, typeOptions, accessLine, loadingHtml, loadingNote } from './common.js'
|
|
6
6
|
import { openInvite, renderTabs, markRead, go } from './app.js'
|
|
7
7
|
import { renderFeed } from './feed.js'
|
|
8
8
|
import { renderLoom, resetLoom, focusFile, showsLoom } from './loom.js'
|
|
@@ -16,10 +16,9 @@ import { renderFileView, renderFileTabs } from './fileview.js'
|
|
|
16
16
|
import { rolledOff, diffLines } from './diffview.js'
|
|
17
17
|
import { searchMarkup, searchTabMarkup, liveByPerson, fileReadsNeeded, SEARCH_ICON } from './quiltview.js'
|
|
18
18
|
import { changesMarkup, bindChanges, unbindChanges, changesChanged, fileChanges, loadChanges, changedCount, unseenChanges, markChangesSeen } from './changes.js'
|
|
19
|
-
import { chipCounts } from './changes-seen.js'
|
|
20
19
|
import { quiltMark } from './mark.js'
|
|
21
20
|
import { openSettings } from './home.js'
|
|
22
|
-
import { fileCardHref, renderable, textHtml, mentionAt, mentionCandidates, completeMention, ALL_AGENTS, RELAY_NAME, isRelay, relayPrompts, typingNames, typingHtml, TYPING_MS, foldPersonas, aiOwners, aiName, shownName, ownAiChatter
|
|
21
|
+
import { fileCardHref, renderable, textHtml, mentionAt, mentionCandidates, completeMention, ALL_AGENTS, RELAY_NAME, isRelay, relayPrompts, typingNames, typingHtml, TYPING_MS, foldPersonas, aiOwners, aiName, shownName, ownAiChatter } from './chat.js'
|
|
23
22
|
import { toolLogo } from './tool-logo.js'
|
|
24
23
|
import { renderBoard, taskNotesModalHtml, openTaskMenu, foldPlan, matchesSearch } from './board.js'
|
|
25
24
|
import { accessFormValues, accessSaveBody, grantsLoading, grantsLoaded, grantsFailed } from './access-form.js'
|
|
@@ -27,14 +26,11 @@ import { workspaceFilePicker } from './files.js'
|
|
|
27
26
|
import { sessionAgentsHtml } from './agent-place.js'
|
|
28
27
|
import { renderCatchUp, bindCatchUp } from './catchup.js'
|
|
29
28
|
import { boxSummary, boxesOf, unplaced, boxesSig, boxListHtml, boxFoldHtml } from './boxes.js'
|
|
30
|
-
import { gitControl, gitStatusParts, gitSegHtml } from './git-rule.js'
|
|
31
29
|
import { inSeam, makeQuiltWords, seamChip, squareCard, squarePanelHtml, seamSettingsHtml, openSquareButton, squaresSideHtml, SEAM_SIDE_HINT } from './seam-model.js'
|
|
32
30
|
import { callButtonMarkup, callPinMarkup, screenStageMarkup, mountCall, unmountCall, renderCall, callCards } from './voice.js'
|
|
33
31
|
|
|
34
32
|
let current = null // session id being shown
|
|
35
33
|
let timers = []
|
|
36
|
-
let holdNoteTimer = null // shows the "git is busy" note once a hold has lasted HOLD_NOTE_MS
|
|
37
|
-
const HOLD_NOTE_MS = 1000 // a `git add` holds for a moment: no note flashing by for it
|
|
38
34
|
let grantLoad = grantsLoading() // this session's grants (the owner's view, from the API), for the Access sections
|
|
39
35
|
let wsAgents = { id: null, agents: [] } // the agents this session's workspace invites (workspaces on), for its People
|
|
40
36
|
let mounted = null // AbortController for document-level listeners of this mount
|
|
@@ -135,7 +131,6 @@ export function mountSession (id) {
|
|
|
135
131
|
<button class="pop-item" role="menuitem" id="move-ws-btn" hidden>Move to a workspace…</button>
|
|
136
132
|
<button class="pop-item" role="menuitem" data-session-settings>Session settings…</button>
|
|
137
133
|
<button class="pop-item" role="menuitem" data-gather-rules>One set of AI rules…</button>
|
|
138
|
-
<button class="pop-item" role="menuitem" id="ask-commit">Ask for a commit…</button>
|
|
139
134
|
<div class="pop-sep"></div>
|
|
140
135
|
<button class="pop-item" role="menuitem" id="leave-btn">Leave this session</button>
|
|
141
136
|
</div>
|
|
@@ -188,7 +183,6 @@ export function mountSession (id) {
|
|
|
188
183
|
<main class="ws-main">
|
|
189
184
|
<div class="requests catchup" id="catchup" role="region" aria-label="While you were away" hidden></div>
|
|
190
185
|
<div class="requests square" id="square-card" role="status" aria-label="Seam" hidden></div>
|
|
191
|
-
<div class="requests github" id="github-card" role="region" aria-label="GitHub" hidden></div>
|
|
192
186
|
<div class="requests" id="requests" hidden></div>
|
|
193
187
|
<div class="ws-mainbar" id="mainbar" hidden>
|
|
194
188
|
<div class="ws-tabs" id="main-tabs" role="tablist"></div>
|
|
@@ -229,11 +223,9 @@ export function mountSession (id) {
|
|
|
229
223
|
}, () => renderMessages())
|
|
230
224
|
bindAccess()
|
|
231
225
|
bindChanges(id, mounted.signal, { onOpen: (path) => openFile(path, { tab: 'changes' }), close: () => setGitOpen(false), onCount: renderGitChip })
|
|
232
|
-
loadMine(true)
|
|
233
226
|
bindCatchUp($('#catchup'), { sessionId: () => current, onOpen: openFile })
|
|
234
227
|
// Report a bug / Suggest a feature at the bottom of the Boxes pane (feedback.js): onto this session's board.
|
|
235
228
|
bindFeedback($('.ws-boxes'), { session: () => current, where: () => state.sessions.get(current)?.status?.sessionName || 'a session' })
|
|
236
|
-
bindGithubCard()
|
|
237
229
|
bindSquareCard()
|
|
238
230
|
bindMain()
|
|
239
231
|
bindTreeEvents()
|
|
@@ -272,7 +264,6 @@ export function sessionUnmount () {
|
|
|
272
264
|
unmountCall()
|
|
273
265
|
for (const t of timers) clearInterval(t)
|
|
274
266
|
timers = []
|
|
275
|
-
clearTimeout(holdNoteTimer)
|
|
276
267
|
if (mounted) mounted.abort()
|
|
277
268
|
mounted = null
|
|
278
269
|
closeTreeMenu()
|
|
@@ -298,7 +289,6 @@ export function sessionUpdated (id) {
|
|
|
298
289
|
if (ws(id).mode === 'tasks') paintBoard()
|
|
299
290
|
scheduleTree()
|
|
300
291
|
changesChanged()
|
|
301
|
-
loadMine()
|
|
302
292
|
}
|
|
303
293
|
|
|
304
294
|
export function sessionMessage (id) {
|
|
@@ -335,7 +325,6 @@ export function sessionFileChanged (id, { path }) {
|
|
|
335
325
|
if (id !== current) return
|
|
336
326
|
scheduleTree()
|
|
337
327
|
changesChanged()
|
|
338
|
-
loadMine()
|
|
339
328
|
const w = ws(id)
|
|
340
329
|
if (w.fileTabs.includes(path)) {
|
|
341
330
|
if (w.mode === 'files' && w.fileSel === path) { refreshFile(path, true); scheduleHistory(path) } else { w.stale[path] = true; histories.delete(fileKey(path)); renderMainBar() }
|
|
@@ -418,7 +407,6 @@ function bindTop () {
|
|
|
418
407
|
}
|
|
419
408
|
$('#invite-btn').onclick = () => openInvite(current)
|
|
420
409
|
bindOpenIn()
|
|
421
|
-
$('#ask-commit').onclick = askForCommit
|
|
422
410
|
$('#rename-btn').onclick = renameSession
|
|
423
411
|
$('#move-ws-btn').onclick = moveToWorkspace
|
|
424
412
|
bindCommitChip()
|
|
@@ -495,8 +483,7 @@ function bindTop () {
|
|
|
495
483
|
|
|
496
484
|
/** The Session settings window's controls: your AI chat's sharing, and (for the owner) who can get in and what they may do. */
|
|
497
485
|
function bindSessionSettings (root) {
|
|
498
|
-
root.addEventListener('
|
|
499
|
-
root.addEventListener('click', (e) => { if (e.target.closest('[data-gh-connect]')) connectGithub(); else if (e.target.closest('[data-make-quilt]')) makeQuilt(e.target.closest('[data-make-quilt]')); else if (e.target.closest('[data-agent-commits]')) gitSettingChange(e) })
|
|
486
|
+
root.addEventListener('click', (e) => { const b = e.target.closest('[data-make-quilt]'); if (b) makeQuilt(b) })
|
|
500
487
|
root.addEventListener('change', async (e) => {
|
|
501
488
|
if (e.target.matches('[data-share]')) {
|
|
502
489
|
const on = e.target.checked
|
|
@@ -571,11 +558,6 @@ function bindSessionSettings (root) {
|
|
|
571
558
|
loadSessionAgents()
|
|
572
559
|
})
|
|
573
560
|
root.addEventListener('click', async (e) => {
|
|
574
|
-
if (e.target.closest('[data-gh-clear]')) {
|
|
575
|
-
try { await api('POST', `/api/sessions/${current}/github-token`, { token: '' }); toast('The relay no longer has a GitHub token') } catch (err) { toast(err.message) }
|
|
576
|
-
refreshPeople({ force: true })
|
|
577
|
-
return
|
|
578
|
-
}
|
|
579
561
|
if (e.target.closest('[data-end-session]')) {
|
|
580
562
|
if (!await ask({ title: 'End this session for everyone?', message: 'Everyone is disconnected, and the session and its stored files are deleted from the relay. Your own folder is not touched.', ok: 'End session', danger: true })) return
|
|
581
563
|
try { await api('POST', `/api/sessions/${current}/end`); toast('Session ended') } catch (err) { toast(err.message) }
|
|
@@ -600,21 +582,7 @@ function bindSessionSettings (root) {
|
|
|
600
582
|
refreshPeople()
|
|
601
583
|
} catch (err) { toast(err.message) } finally { save.disabled = false }
|
|
602
584
|
})
|
|
603
|
-
|
|
604
|
-
const f = e.target.closest('.pm-gh')
|
|
605
|
-
if (!f) return
|
|
606
|
-
e.preventDefault()
|
|
607
|
-
const token = f.token.value.trim()
|
|
608
|
-
if (!token) return toast('Paste a token first')
|
|
609
|
-
const save = f.querySelector('[type=submit]')
|
|
610
|
-
save.disabled = true
|
|
611
|
-
try {
|
|
612
|
-
await api('POST', `/api/sessions/${current}/github-token`, { token })
|
|
613
|
-
f.token.value = ''
|
|
614
|
-
toast('Token saved: the relay brings in commits while everyone\'s offline')
|
|
615
|
-
refreshPeople({ force: true })
|
|
616
|
-
} catch (err) { toast(err.message) } finally { save.disabled = false }
|
|
617
|
-
})
|
|
585
|
+
|
|
618
586
|
}
|
|
619
587
|
|
|
620
588
|
function toggleDrawer (which) {
|
|
@@ -653,44 +621,6 @@ function agentLine (p) {
|
|
|
653
621
|
}
|
|
654
622
|
|
|
655
623
|
// ------------------------------------------------------------- branches --
|
|
656
|
-
let branchSyncing = false
|
|
657
|
-
|
|
658
|
-
/** One line on how a branch stands against its upstream ('' without one). */
|
|
659
|
-
export function upstreamText (u) {
|
|
660
|
-
if (!u || !u.name) return ''
|
|
661
|
-
const n = (k, w) => `${k} ${w}${k === 1 ? '' : 's'}`
|
|
662
|
-
// A failed fetch: what git last knew is stale, so never "up to date".
|
|
663
|
-
if (u.fetchOk === false) return `can't fetch ${u.name}: ${u.fetchError || 'git fetch failed'}`
|
|
664
|
-
const by = u.mergedBy ? `; being merged by ${u.clashMine ? 'your AI' : u.mergedBy}` : ''
|
|
665
|
-
if (u.diverged) return `diverged from ${u.name}: ${n(u.ahead, 'commit')} here, ${n(u.behind, 'commit')} there${by}`
|
|
666
|
-
const clash = Array.isArray(u.conflicts) ? u.conflicts.length : u.conflicts
|
|
667
|
-
if (clash) return `${n(u.behind, 'commit')} behind ${u.name}; ${n(clash, 'file')} clash with the session's work${by}`
|
|
668
|
-
if (u.waiting) return `${n(u.behind, 'commit')} behind ${u.name}; waiting: ${u.waiting}`
|
|
669
|
-
if (u.behind) return `${n(u.behind, 'commit')} behind ${u.name}`
|
|
670
|
-
if (u.ahead) return `${n(u.ahead, 'commit')} ahead of ${u.name}`
|
|
671
|
-
return `up to date with ${u.name}`
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
/** Whether the label should draw the eye: this folder is behind and Quilt can't bring it in by itself. */
|
|
675
|
-
export function needsHand (u) {
|
|
676
|
-
return !!(u && (u.diverged || u.fetchOk === false || (Array.isArray(u.conflicts) ? u.conflicts.length : u.conflicts)))
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/**
|
|
680
|
-
* The git panel's head in a session still on git: this folder's branch, how it stands against its
|
|
681
|
-
* upstream, and Check. The list of every branch (the branch menu, src/ui/branches.js) went in 16's R2:
|
|
682
|
-
* which branch a folder is on is git's, and new sessions are quilts.
|
|
683
|
-
*/
|
|
684
|
-
export function folderBranchHtml ({ git, syncing = false }) {
|
|
685
|
-
if (!git) return ''
|
|
686
|
-
const u = git.upstream
|
|
687
|
-
return `<div class="br-here br-compact">
|
|
688
|
-
<div class="br-line">${I.branch}<b class="br-name">${esc(git.key)}</b>${u ? `<span class="br-sub${needsHand(u) ? ' warn' : ''}" title="${esc(upstreamText(u))}">${esc(upstreamText(u))}</span>` : ''}${u ? `<button type="button" class="btn sm ghost br-sync" data-branch-sync ${syncing ? 'disabled' : ''} title="Look for new commits on ${esc(u.name || 'the upstream')}">${I.refresh}<span>${syncing ? 'Checking' : 'Check'}</span></button>` : ''}</div>
|
|
689
|
-
${u && Array.isArray(u.conflicts) && u.conflicts.length ? `<ul class="br-clash">${u.conflicts.slice(0, 6).map((c) => `<li><code>${esc(c.path)}</code> ${esc(c.why)}</li>`).join('')}</ul><div class="br-hint">Ask your AI to pull and resolve these: Quilt brings nothing in until they're settled.</div>` : ''}
|
|
690
|
-
${u && u.diverged ? '<div class="br-hint">Quilt never merges git history: ask your AI to pull or rebase, and the session takes in the result.</div>' : ''}
|
|
691
|
-
</div>`
|
|
692
|
-
}
|
|
693
|
-
|
|
694
624
|
let squareBusy = '' // 'commit' or 'merge' while the square panel's button runs (layout 3)
|
|
695
625
|
|
|
696
626
|
function renderBranchMenu () {
|
|
@@ -712,8 +642,8 @@ function renderBranchMenu () {
|
|
|
712
642
|
return
|
|
713
643
|
}
|
|
714
644
|
delete menu.dataset.html
|
|
715
|
-
menu.hidden =
|
|
716
|
-
menu.innerHTML =
|
|
645
|
+
menu.hidden = true
|
|
646
|
+
menu.innerHTML = ''
|
|
717
647
|
}
|
|
718
648
|
|
|
719
649
|
/** The square panel's Commit and Merge onto main (layout 3): Seam's own tools, as this member (control.js seamTool). */
|
|
@@ -747,15 +677,7 @@ function bindBranchMenu () {
|
|
|
747
677
|
menu.addEventListener('click', async (e) => {
|
|
748
678
|
// Merge commits what isn't committed yet first: with the words typed in the box, if any.
|
|
749
679
|
if (e.target.closest('[data-square-open]')) { if (!squareBusy) squareAct('open_square'); return }
|
|
750
|
-
if (e.target.closest('[data-square-merge]')) { if (!squareBusy) squareAct('seam_merge', (menu.querySelector('[data-square-commit] input')?.value || '').trim())
|
|
751
|
-
if (!e.target.closest('[data-branch-sync]') || branchSyncing) return
|
|
752
|
-
branchSyncing = true
|
|
753
|
-
renderBranchMenu()
|
|
754
|
-
const id = current
|
|
755
|
-
try { await api('POST', `/api/sessions/${id}/branches/sync`) } catch (err) { toast(err.message) } finally {
|
|
756
|
-
branchSyncing = false
|
|
757
|
-
if (id === current && !$('#git-panel').hidden) renderBranchMenu()
|
|
758
|
-
}
|
|
680
|
+
if (e.target.closest('[data-square-merge]')) { if (!squareBusy) squareAct('seam_merge', (menu.querySelector('[data-square-commit] input')?.value || '').trim()) }
|
|
759
681
|
})
|
|
760
682
|
}
|
|
761
683
|
|
|
@@ -766,11 +688,8 @@ function renderTop () {
|
|
|
766
688
|
// Code search, only in a session linked to a quilt (the relay reads Seam for it).
|
|
767
689
|
const search = $('#qt-topsearch')
|
|
768
690
|
if (search) search.hidden = !st.quilt
|
|
769
|
-
const g = st.git
|
|
770
691
|
const label = $('#git-branch')
|
|
771
692
|
const seam = inSeam(st) ? seamChip(st) : null
|
|
772
|
-
// In Seam nobody asks a person with git to commit: whoever is on a square commits it.
|
|
773
|
-
$('#ask-commit').hidden = !!seam
|
|
774
693
|
if (label && seam) {
|
|
775
694
|
// In Seam: the square this folder follows, by the name Seam gives it.
|
|
776
695
|
label.hidden = !seam.label
|
|
@@ -778,21 +697,8 @@ function renderTop () {
|
|
|
778
697
|
label.dataset.title = seam.title
|
|
779
698
|
if (!$('#git-panel').hidden) renderBranchMenu()
|
|
780
699
|
} else if (label) {
|
|
781
|
-
label.hidden =
|
|
782
|
-
|
|
783
|
-
// Moving to another branch: said at once. Git busy: only once it has lasted a moment.
|
|
784
|
-
const held = g.hold ? Date.now() - g.hold.since : 0
|
|
785
|
-
const note = g.hold && (g.hold.kind === 'switching' || held >= HOLD_NOTE_MS)
|
|
786
|
-
clearTimeout(holdNoteTimer)
|
|
787
|
-
if (g.hold && !note) holdNoteTimer = setTimeout(renderTop, HOLD_NOTE_MS - held + 20)
|
|
788
|
-
const tag = note ? (g.hold.kind === 'switching' ? (g.hold.waiting === 'refused' ? `not syncing · ${esc(g.hold.to || '?')} isn't in the session` : g.hold.waiting === 'retrying' ? `waiting for ${esc(g.hold.to || '?')}…` : `moving to ${esc(g.hold.to || '?')}…`) : g.hold.conflict ? 'paused: resolve the git conflict' : 'syncing paused: git is busy') : ''
|
|
789
|
-
const up = g.upstream
|
|
790
|
-
const behind = !tag && up && (up.behind || up.diverged) ? `<span class="tag${needsHand(up) ? ' warn' : ''}">${needsHand(up) ? 'needs a pull' : `${up.behind} behind`}</span>` : ''
|
|
791
|
-
const full = g.full ? '<span class="tag warn" title="This branch is over the session\'s size limit: new changes aren\'t saved. Start a fresh copy with git checkout -b.">full</span>' : ''
|
|
792
|
-
label.innerHTML = `${I.branch}<span class="branch-name">${esc(g.key)}</span>${tag ? `<span class="tag" title="${tag}">${tag}</span>` : ''}${behind}${full}`
|
|
793
|
-
label.dataset.title = g.hold ? (g.hold.kind === 'switching' ? (g.hold.waiting === 'refused' ? `The session can't take ${g.hold.to || 'this branch'}, so this folder isn't syncing. Check out ${g.key} again in git to keep syncing there.` : `You checked out ${g.hold.to || 'another branch'} in git: this folder is moving to that branch's work in the session. The branch it left keeps its own.`) : g.hold.conflict ? `git left a conflict in ${g.hold.conflict.join(', ')} on this computer. Resolve it and git add it; Quilt then shares your resolution.` : 'Quilt waits for git to finish, then catches up.') : `This folder is on ${g.key}${up ? `, ${upstreamText(up)}` : ''}${g.full ? '. Over the size limit: new changes aren\'t saved.' : ''}.`
|
|
794
|
-
}
|
|
795
|
-
if (!$('#git-panel').hidden) renderBranchMenu()
|
|
700
|
+
label.hidden = true
|
|
701
|
+
label.innerHTML = ''
|
|
796
702
|
}
|
|
797
703
|
const people = [st.me, ...peopleHere(st)]
|
|
798
704
|
const shown = people.slice(0, 4)
|
|
@@ -811,7 +717,6 @@ function renderTop () {
|
|
|
811
717
|
renderAccess()
|
|
812
718
|
renderCatchUp($('#catchup'), st.catchUp, { colors: new Map([st.me, ...st.peers].map((p) => [p.name, p.color])) })
|
|
813
719
|
renderSquareCard()
|
|
814
|
-
renderGithubCard()
|
|
815
720
|
renderGitChip()
|
|
816
721
|
$('#sess-name').textContent = st.sessionName || basename(sum().dir)
|
|
817
722
|
const me = state.account || {}
|
|
@@ -824,161 +729,35 @@ function renderTop () {
|
|
|
824
729
|
$('#chat-sub').textContent = st.peers.length ? `with ${peopleHere(st).map((p) => p.sessions && p.mine ? 'your AI' : p.name).join(', ')}` : 'just you so far'
|
|
825
730
|
}
|
|
826
731
|
|
|
827
|
-
// ---------------------------------------------------------------
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
// here. When they can't, they ask (commit.js): in a folder with git each
|
|
831
|
-
// request has a Commit button (exactly its files, then a push). "Not committed yet" shows the
|
|
832
|
-
// uncommitted work here by who made it, so nothing finished sits unshipped unnoticed.
|
|
833
|
-
let commitGithub = null // the owner's GitHub connection (GET /api/github), while the panel is open
|
|
834
|
-
// Your own work not committed here (GET mine-uncommitted: yours and your AIs', each boxed or not),
|
|
835
|
-
// for the git chip's count and the panel. Nobody else's: they commit their own.
|
|
836
|
-
let mineView = { id: null, data: null, at: 0, loading: false, again: false, open: new Set(['boxed']) }
|
|
837
|
-
const MINE_FRESH_MS = 3000 // fetched again at most this often as the session changes
|
|
732
|
+
// --------------------------------------------------------------- source control --
|
|
733
|
+
// The chip in the top bar: in Seam, the square this folder follows and its work not committed yet
|
|
734
|
+
// (the panel commits and merges it); otherwise how many files changed in the session.
|
|
838
735
|
|
|
839
|
-
|
|
840
|
-
const id = current
|
|
841
|
-
if (!id) return
|
|
842
|
-
if (mineView.id !== id) mineView = { id, data: null, at: 0, loading: false, again: false, open: new Set(['boxed']) }
|
|
843
|
-
if (mineView.loading) { mineView.again = true; return }
|
|
844
|
-
const wait = MINE_FRESH_MS - (Date.now() - mineView.at)
|
|
845
|
-
if (!force && wait > 0) { clearTimeout(loadMine.timer); loadMine.timer = setTimeout(() => loadMine(true), wait); return }
|
|
846
|
-
mineView.loading = true
|
|
847
|
-
api('GET', `/api/sessions/${id}/mine-uncommitted`).then((r) => {
|
|
848
|
-
if (mineView.id !== id) return
|
|
849
|
-
mineView.data = r && Array.isArray(r.files) ? r : null
|
|
850
|
-
mineView.at = Date.now()
|
|
851
|
-
renderGitChip()
|
|
852
|
-
}, () => {}).finally(() => {
|
|
853
|
-
if (mineView.id !== id) return
|
|
854
|
-
mineView.loading = false
|
|
855
|
-
if (mineView.again) { mineView.again = false; loadMine() }
|
|
856
|
-
})
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
/** How many of your own files aren't committed here (null before the first look, or without git). */
|
|
860
|
-
const mineCount = () => (mineView.id === current && mineView.data ? mineView.data.files.length : null)
|
|
861
|
-
/** Session settings: GitHub sync, large files through the relay, and git for this session. */
|
|
862
|
-
async function gitSettingChange (e) {
|
|
863
|
-
if (e.target.matches('[data-large-to-relay]')) {
|
|
864
|
-
try {
|
|
865
|
-
const r = await api('POST', `/api/sessions/${current}/large-to-relay`, { on: e.target.checked })
|
|
866
|
-
toast(r.largeToRelay ? 'The relay can commit large files on branches that sync with GitHub now.' : 'Large files stay end-to-end encrypted: the relay commits none.')
|
|
867
|
-
} catch (err) { toast(err.message); renderSessionSettings({ force: true }) }
|
|
868
|
-
return
|
|
869
|
-
}
|
|
870
|
-
if (e.target.matches('[data-github-sync]')) {
|
|
871
|
-
try {
|
|
872
|
-
const r = await api('POST', `/api/sessions/${current}/github-sync`, { on: e.target.checked })
|
|
873
|
-
toast(r.githubSync === false ? 'Sync with GitHub is off for this session.' : 'This session syncs with GitHub.')
|
|
874
|
-
} catch (err) { toast(err.message); renderSessionSettings({ force: true }) }
|
|
875
|
-
return
|
|
876
|
-
}
|
|
877
|
-
const seg = e.target.closest('[data-agent-commits]')
|
|
878
|
-
if (!seg || seg.disabled || seg.classList.contains('on')) return
|
|
879
|
-
seg.closest('.seg').querySelectorAll('button').forEach((b) => { b.classList.toggle('on', b === seg); b.setAttribute('aria-pressed', String(b === seg)) })
|
|
880
|
-
try {
|
|
881
|
-
const r = await api('POST', `/api/sessions/${current}/agent-commits`, { mode: seg.dataset.agentCommits })
|
|
882
|
-
toast(AGENT_COMMIT_WORDS[r.agentCommits])
|
|
883
|
-
} catch (err) { toast(err.message); renderSessionSettings({ force: true }) }
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
/** Opens or closes the git panel: this folder's branch, the session against GitHub, and what changed, in one place. */
|
|
736
|
+
/** Opens or closes the source control panel: the square, and what changed, in one place. */
|
|
887
737
|
function setGitOpen (open) {
|
|
888
738
|
const panel = $('#git-panel')
|
|
889
739
|
if (!panel) return
|
|
890
740
|
panel.hidden = !open
|
|
891
741
|
$('#git-chip').setAttribute('aria-expanded', String(open))
|
|
892
|
-
if (open) { markChangesSeen(); renderBranchMenu(); renderCommitPanel();
|
|
742
|
+
if (open) { markChangesSeen(); renderBranchMenu(); renderCommitPanel(); loadChanges().then(() => { if (!panel.hidden) markChangesSeen() }) }
|
|
893
743
|
}
|
|
894
744
|
|
|
895
745
|
function bindCommitChip () {
|
|
896
746
|
const wrap = $('#git-wrap')
|
|
897
747
|
const chip = $('#git-chip')
|
|
898
|
-
const panel = $('#commit-panel')
|
|
899
748
|
chip.onclick = () => setGitOpen($('#git-panel').hidden)
|
|
900
749
|
wrap.addEventListener('keydown', (e) => { if (e.key === 'Escape') { setGitOpen(false); chip.focus() } })
|
|
901
750
|
document.addEventListener('mousedown', (e) => { if (!wrap.contains(e.target) && !e.target.closest('.modal, .dialog')) setGitOpen(false) }, { signal: mounted.signal })
|
|
902
|
-
panel.addEventListener('submit', async (e) => {
|
|
903
|
-
const form = e.target.closest('[data-commit-session]')
|
|
904
|
-
if (!form) return
|
|
905
|
-
e.preventDefault()
|
|
906
|
-
const btn = form.querySelector('button')
|
|
907
|
-
const input = form.querySelector('input')
|
|
908
|
-
btn.disabled = true
|
|
909
|
-
btn.textContent = 'Committing…'
|
|
910
|
-
try {
|
|
911
|
-
const r = await api('POST', `/api/sessions/${current}/commit-session`, { message: input.value, mine: true })
|
|
912
|
-
toast(r.hash ? `Committed ${r.hash.slice(0, 7)}${r.pushed ? ' and pushed' : `; not pushed: ${r.pushError || 'no upstream'}`}` : 'Already in git')
|
|
913
|
-
input.value = ''
|
|
914
|
-
loadMine(true)
|
|
915
|
-
} catch (err) { toast(err.message) } finally {
|
|
916
|
-
btn.disabled = false
|
|
917
|
-
btn.textContent = 'Commit and push'
|
|
918
|
-
}
|
|
919
|
-
})
|
|
920
|
-
// Which of your two lines (boxed, shared) is open stays so across redraws.
|
|
921
|
-
panel.addEventListener('toggle', (e) => {
|
|
922
|
-
const d = e.target.closest && e.target.closest('[data-mine-group]')
|
|
923
|
-
if (d) d.open ? mineView.open.add(d.dataset.mineGroup) : mineView.open.delete(d.dataset.mineGroup)
|
|
924
|
-
}, true)
|
|
925
|
-
panel.addEventListener('click', async (e) => {
|
|
926
|
-
if (e.target.closest('[data-gh-connect]')) { connectGithub(); return }
|
|
927
|
-
const live = e.target.closest('[data-open-live]')
|
|
928
|
-
if (live) { live.disabled = true; openLiveSession(live.dataset.openLive).catch((err) => { toast(err.message); live.disabled = false }); return }
|
|
929
|
-
const b = e.target.closest('[data-done], [data-commit]')
|
|
930
|
-
if (!b) return
|
|
931
|
-
b.disabled = true
|
|
932
|
-
try {
|
|
933
|
-
if (b.dataset.done) {
|
|
934
|
-
const id = b.dataset.done === 'all' ? null : b.dataset.done
|
|
935
|
-
const r = await api('POST', `/api/sessions/${current}/commit-request/done`, id ? { id } : {})
|
|
936
|
-
toast(r.done === 1 ? 'Closed' : `Closed ${r.done}`)
|
|
937
|
-
} else if (b.dataset.commit) {
|
|
938
|
-
b.textContent = 'Committing…'
|
|
939
|
-
const r = await api('POST', `/api/sessions/${current}/commit-request/commit`, { id: b.dataset.commit })
|
|
940
|
-
toast(r.hash ? `Committed ${r.hash.slice(0, 7)}${r.pushed ? ' and pushed' : `; not pushed: ${r.pushError || 'no upstream'}`}` : 'Already in git: closed')
|
|
941
|
-
}
|
|
942
|
-
loadCommitWork()
|
|
943
|
-
} catch (err) { toast(err.message); b.disabled = false; loadCommitWork() }
|
|
944
|
-
})
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
async function loadCommitWork () {
|
|
948
|
-
if ($('#git-panel').hidden) return
|
|
949
|
-
const st = sum().status
|
|
950
|
-
if (st.access?.owner) { try { commitGithub = await api('GET', '/api/github') } catch { commitGithub = null } }
|
|
951
|
-
renderCommitPanel()
|
|
952
751
|
}
|
|
953
752
|
|
|
954
|
-
/**
|
|
955
|
-
* The git chip: this folder's branch, how many files changed in the session, and a dot (with a
|
|
956
|
-
* word when something needs someone) for the session against GitHub. One control for all three,
|
|
957
|
-
* so the top bar has one place for source control.
|
|
958
|
-
*/
|
|
753
|
+
/** The source control chip: the square this folder follows (in Seam) and how many files changed. */
|
|
959
754
|
function renderGitChip () {
|
|
960
755
|
const chip = $('#git-chip')
|
|
961
756
|
if (!chip || !sum()) return
|
|
962
757
|
const st = sum().status
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
// A branch that syncs live already has what it needs (a connection, or the owner's token).
|
|
967
|
-
const liveOn = !!(myBranchOf(st)?.relay?.live)
|
|
968
|
-
const off = !!(myBranchOf(st)?.relay?.sync?.state === 'paused' && /owner turned (live )?sync( with GitHub)? off/.test(myBranchOf(st).relay.sync.why || ''))
|
|
969
|
-
const needs = gh && gh.state && gh.state !== 'ok' && gh.state !== 'not-github' && !liveOn && !off
|
|
970
|
-
const live = syncWords(st)
|
|
971
|
-
const warn = !!(live && live.warn && !open.length)
|
|
972
|
-
chip.className = `git-chip${open.length && !busy.length ? ' ready' : ''}${(needs && !open.length) || warn ? ' needs' : ''}${!open.length && !needs && !warn ? ' quiet' : ''}`
|
|
973
|
-
// Words only when someone has to act; otherwise the dot says where the session stands.
|
|
974
|
-
const say = open.length ? `${open.length === 1 ? 'Commit requested' : 'Commits requested'}${busy.length ? ` · ${busy.length === 1 ? 'someone' : 'people'} working` : ''}`
|
|
975
|
-
: needs ? 'Connect GitHub' : warn ? live.chip : ''
|
|
976
|
-
const sayEl = $('#git-say')
|
|
977
|
-
sayEl.hidden = !say
|
|
978
|
-
sayEl.textContent = say
|
|
979
|
-
const dot = $('#git-dot')
|
|
980
|
-
dot.hidden = !!say || !(live && live.dot)
|
|
981
|
-
dot.className = `sd${live && live.dot ? ` ${live.dot}` : ''}`
|
|
758
|
+
chip.className = 'git-chip quiet'
|
|
759
|
+
$('#git-say').hidden = true
|
|
760
|
+
$('#git-dot').hidden = true
|
|
982
761
|
const branch = $('#git-branch')
|
|
983
762
|
const seam = inSeam(st) ? seamChip(st) : null
|
|
984
763
|
// In Seam the session's own record of changes stopped at the switch (its branch document is kept read-only):
|
|
@@ -992,171 +771,37 @@ function renderGitChip () {
|
|
|
992
771
|
chip.title = [seam.title, seam.countLine, 'Click for your square.'].filter(Boolean).join('\n')
|
|
993
772
|
chip.setAttribute('aria-label', seam.aria)
|
|
994
773
|
$('#git-panel').setAttribute('aria-label', 'Square')
|
|
995
|
-
if (!$('#git-panel').hidden) renderCommitPanel()
|
|
996
774
|
return
|
|
997
775
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
// number, with the session's total.
|
|
1001
|
-
const own = mineCount()
|
|
1002
|
-
const { n, lines: counts } = chipCounts({ own, total: changedCount(), fresh: unseenChanges() })
|
|
776
|
+
const n = changedCount()
|
|
777
|
+
const fresh = unseenChanges()
|
|
1003
778
|
$('#chg-count').textContent = n
|
|
1004
779
|
$('#git-chg').hidden = !n
|
|
1005
780
|
$('#git-sep').hidden = branch.hidden || !n
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
chip.title = [...lines, 'Click for branches, GitHub and every change.'].join('\n')
|
|
1013
|
-
chip.setAttribute('aria-label', `Source control${branch.hidden ? '' : `: ${st.git.key}`}${n ? own === null ? `, ${n} changed` : `, ${n} of yours not committed` : ''}${say ? `, ${say}` : live ? `, ${live.chip}` : ''}`)
|
|
1014
|
-
if (!$('#git-panel').hidden) renderCommitPanel()
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
/** Open requests (Commit, Close), the uncommitted work here by who made it, and committing by itself. */
|
|
781
|
+
$('#git-panel').setAttribute('aria-label', 'Changes')
|
|
782
|
+
chip.title = [n ? `${n} changed in the session` : '', fresh ? `${fresh} new since you last looked` : '', 'Click for every change.'].filter(Boolean).join('\n')
|
|
783
|
+
chip.setAttribute('aria-label', `Source control${n ? `, ${n} changed` : ''}`)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** Nothing of git's to show under the square or the changes. */
|
|
1018
787
|
function renderCommitPanel () {
|
|
1019
788
|
const panel = $('#commit-panel')
|
|
1020
789
|
if (!panel) return
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const req = (r) => {
|
|
1025
|
-
const why = r.error ? `Last try: ${r.error}` : ''
|
|
1026
|
-
return `<li><b>${esc(shownName(authorOf(r), owners(sum())))}</b>${r.task ? ` <span class="hint">· task</span>` : ''}<div>${esc(r.message)}</div>
|
|
1027
|
-
${Array.isArray(r.files) && r.files.length ? fileList(r.files) : ''}
|
|
1028
|
-
${why ? `<p class="commit-why">${esc(why)}</p>` : ''}
|
|
1029
|
-
<div class="commit-acts">${st.canCommit ? `<button type="button" class="btn sm" data-commit="${esc(r.id)}">Commit and push</button>` : ''}<button type="button" class="btn sm ghost" data-done="${esc(r.id)}" title="Close it without a commit from Quilt (committed by hand, or not needed)">Close</button></div></li>`
|
|
1030
|
-
}
|
|
1031
|
-
// Only what needs a person: an agent that may not commit itself asked for one, the session's
|
|
1032
|
-
// work not on GitHub yet (and the commit that sends it), or GitHub to connect. Settings
|
|
1033
|
-
// (sync, large files, what agents may commit) live in Session settings.
|
|
1034
|
-
const html = `
|
|
1035
|
-
${open.length ? `<h4>Asked to be committed</h4><ul class="commit-reqs">${open.map(req).join('')}</ul>` : ''}
|
|
1036
|
-
${mineHtml(st)}
|
|
1037
|
-
${gitLineHtml(st)}
|
|
1038
|
-
${connectHtml(st)}
|
|
1039
|
-
${open.length > 1 ? '<div class="commit-foot"><button type="button" class="btn sm ghost" data-done="all">Close all</button></div>' : ''}`.trim()
|
|
1040
|
-
panel.hidden = !html
|
|
1041
|
-
// Status comes in often: redrawn only when something in it changed, so an open menu or a
|
|
1042
|
-
// file list someone expanded stays as it is.
|
|
1043
|
-
if (panel.dataset.html === html) return
|
|
1044
|
-
panel.dataset.html = html
|
|
1045
|
-
// A commit message being typed survives the redraw.
|
|
1046
|
-
const typing = panel.querySelector('[data-commit-session] input')
|
|
1047
|
-
const kept = typing ? { value: typing.value, focused: document.activeElement === typing, at: typing.selectionStart } : null
|
|
1048
|
-
panel.innerHTML = html
|
|
1049
|
-
const box = kept && panel.querySelector('[data-commit-session] input')
|
|
1050
|
-
if (box) {
|
|
1051
|
-
box.value = kept.value
|
|
1052
|
-
if (kept.focused) { box.focus(); box.setSelectionRange(kept.at, kept.at) }
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
/** This folder's branch in the relay's branch list (or the default branch), or null. */
|
|
1057
|
-
function myBranchOf (st) {
|
|
1058
|
-
const list = st.branches || []
|
|
1059
|
-
return (st.git && list.find((x) => x.name === st.git.key)) || list.find((x) => x.default) || null
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
/** Where this branch stands with GitHub (GitHub as a node: relay-sync.js syncStatus), in a few words, with how to read it. */
|
|
1063
|
-
function syncWords (st) {
|
|
1064
|
-
if (inSeam(st)) return null
|
|
1065
|
-
const b = myBranchOf(st)
|
|
1066
|
-
const r = b && b.relay
|
|
1067
|
-
const s = r && r.sync
|
|
1068
|
-
if (!s) return null
|
|
1069
|
-
const name = b.name
|
|
1070
|
-
const at = s.revision ? ` at ${s.revision.slice(0, 7)}` : ''
|
|
1071
|
-
const t = (ms) => new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
1072
|
-
if (s.state === 'synced') return { chip: 'Committed', dot: 'ok', line: `${name} is in step with GitHub${at}: everything in the session is committed, and pushes and merges there come in within seconds.`, warn: false }
|
|
1073
|
-
if (s.state === 'github-pending') return { chip: 'Uncommitted changes', dot: 'edit', line: `${name} has GitHub's latest${at}. The session has changes that aren't committed: everyone here has them, and GitHub gets them when someone commits.`, warn: false, pending: true }
|
|
1074
|
-
if (s.state === 'conflict-held') return { chip: 'Merge held', line: `${s.why} on ${name}: the session and GitHub changed the same lines. The merge is on the board; everything else keeps syncing.`, warn: true }
|
|
1075
|
-
if (s.state === 'pr-pending') return { chip: s.pr ? `Through pull request #${s.pr.number}` : 'Through a pull request', line: r.leased ? `${name} syncs with GitHub in another session, so this session's commits go through` : `${name} is protected on GitHub, so the session's commits go through`, pr: s.pr || { number: 0, url: '' }, warn: false, leased: !!r.leased, liveIn: r.liveIn || null }
|
|
1076
|
-
if (s.state === 'offline') return { chip: 'GitHub unreachable', line: `GitHub unreachable since ${s.since ? t(s.since) : 'a moment ago'}: the session keeps working, and GitHub's changes and the commits asked for go when it answers.`, warn: true }
|
|
1077
|
-
if (s.state === 'permission-held') return { chip: 'Sync paused', line: s.why, warn: true }
|
|
1078
|
-
if (/owner turned (live )?sync( with GitHub)? off/.test(s.why || '')) return { chip: 'Not syncing', line: `Sync with GitHub is off for this session: files stay in the session, and people commit with git.`, warn: false, off: true }
|
|
1079
|
-
return { chip: 'Not syncing', line: `${(s.why || `sync of ${name} with GitHub is off`).replace(/^./, (c) => c.toUpperCase())}.`, warn: false, off: true }
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
/**
|
|
1083
|
-
* Your own work not committed here, in two lines (mockup option B): what is boxed (in your folder
|
|
1084
|
-
* only so far) and what everyone already has; each opens to its files. Then the commit, which
|
|
1085
|
-
* takes only these. Nobody else's work is counted or listed.
|
|
1086
|
-
*/
|
|
1087
|
-
function mineHtml (st) {
|
|
1088
|
-
const files = mineView.id === current && mineView.data ? mineView.data.files : null
|
|
1089
|
-
if (!files) return ''
|
|
1090
|
-
if (!files.length) return '<div class="mw-head"><h4>Not committed</h4></div><p class="mw-none">Nothing of yours: your work here is all in git.</p>'
|
|
1091
|
-
const boxed = files.filter((f) => f.boxed)
|
|
1092
|
-
const shared = files.filter((f) => !f.boxed)
|
|
1093
|
-
const group = (key, list, ico, label, hint) => list.length
|
|
1094
|
-
? `<details class="mw" data-mine-group="${key}"${mineView.open.has(key) ? ' open' : ''}><summary><span class="mw-ico">${ico}</span><span>${list.length} ${label}</span><span class="hint">${hint}</span></summary><ul>${list.map((f) => `<li><code title="${esc(f.path)}">${esc(f.path)}</code></li>`).join('')}</ul></details>`
|
|
1095
|
-
: ''
|
|
1096
|
-
return `<div class="mw-head"><h4>Not committed</h4></div>
|
|
1097
|
-
${group('boxed', boxed, I.box, 'boxed', 'only in your folder')}
|
|
1098
|
-
${group('shared', shared, '<span class="mw-dot"></span>', 'shared', 'everyone has them')}
|
|
1099
|
-
${st.canCommit ? `<form class="cp-commit" data-commit-session><input class="input" name="message" placeholder="Commit message" aria-label="Commit message" maxlength="500" required autocomplete="off"><button type="submit" class="btn sm primary">Commit and push</button></form>` : ''}`
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
/** The session against GitHub in a line (the long words on hover), and the commit that sends what isn't there yet. */
|
|
1103
|
-
function gitLineHtml (st) {
|
|
1104
|
-
const w = syncWords(st)
|
|
1105
|
-
if (!w || w.off) return ''
|
|
1106
|
-
const b = myBranchOf(st)
|
|
1107
|
-
// Work not on GitHub yet: what is yours shows above (mineHtml); everyone else's is theirs to commit.
|
|
1108
|
-
if (w.pending) return ''
|
|
1109
|
-
const pr = w.pr ? (w.pr.url ? ` · <a href="${esc(w.pr.url)}" target="_blank" rel="noreferrer">pull request #${w.pr.number}</a>` : '') : ''
|
|
1110
|
-
const live = w.liveIn ? ` <button type="button" class="btn sm" data-open-live="${esc(w.liveIn)}">Open the live session</button>` : ''
|
|
1111
|
-
return `<p class="cp-line${w.warn ? ' warn' : ''}" title="${esc(w.line)}">${w.dot ? `<span class="sd ${w.dot}"></span>` : ''}<span>${esc(w.chip)}</span>${pr}${live}</p>`
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
/** Connect GitHub, while nobody here has (one click, by anyone who can write to the repository). */
|
|
1115
|
-
function connectHtml (st) {
|
|
1116
|
-
const gh = githubStateOf(st)
|
|
1117
|
-
if (!gh || !gh.state || gh.state === 'ok' || gh.state === 'not-github' || myBranchOf(st)?.relay?.live) return ''
|
|
1118
|
-
return `<p class="cp-line"><button type="button" class="btn sm" data-gh-connect>${gh.state === 'not-installed' ? 'Add it on GitHub' : 'Connect GitHub'}</button><span class="hint">so git is on for everyone here, agents included</span></p>`
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
/** What else the git card says under the control: the read-only token, or why git isn't working yet. */
|
|
1122
|
-
function agentCommitNote (st) {
|
|
1123
|
-
const gh = githubStateOf(st)
|
|
1124
|
-
if (st.access.githubToken) return 'Agents without a folder commit with the session\'s GitHub token.'
|
|
1125
|
-
if (gh && gh.state && gh.state !== 'ok') return `${esc(githubWords(gh))} Connect GitHub from the branch button in the top bar.`
|
|
1126
|
-
if (!gh) return 'This session doesn\'t follow a GitHub repository yet.'
|
|
1127
|
-
return ''
|
|
790
|
+
panel.hidden = true
|
|
791
|
+
panel.innerHTML = ''
|
|
792
|
+
delete panel.dataset.html
|
|
1128
793
|
}
|
|
1129
794
|
|
|
1130
|
-
/**
|
|
1131
|
-
* Git for this session (mockup option A): one segmented control that fills its card, the rule over the
|
|
1132
|
-
* session as a pill (who set it), the choices it doesn't allow greyed with why, and who git works through.
|
|
1133
|
-
*/
|
|
1134
|
-
function gitCardHtml (st) {
|
|
1135
|
-
const by = st.agentCommitsBy
|
|
1136
|
-
const m = gitControl({ value: st.agentCommits || 'branches', rule: by && by.mode ? by : null })
|
|
1137
|
-
const g = st.gitForEveryone
|
|
1138
|
-
const line = g && g.words ? gitStatusParts(g.words) : null
|
|
1139
|
-
const note = agentCommitNote(st)
|
|
1140
|
-
return `<div class="pm-card git-card">
|
|
1141
|
-
<div class="git-head"><h4>This session's source control</h4>${m.rulePill ? `<span class="pill rule">${esc(m.rulePill)}</span>` : '<span class="pill">No rule above it</span>'}</div>
|
|
1142
|
-
${gitSegHtml(m, { attr: 'data-agent-commits', label: 'This session\'s source control' })}
|
|
1143
|
-
${line ? `<p class="git-line ${g.policy === 'off' ? 'off' : g.on ? 'ok' : ''}"><b>${esc(line.strong)}</b>${esc(line.rest)}</p>` : ''}
|
|
1144
|
-
${m.lockedHint ? `<span class="hint">${esc(m.lockedHint)}</span>` : ''}
|
|
1145
|
-
${note ? `<span class="hint">${note}</span>` : ''}
|
|
1146
|
-
</div>`
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
/** Session settings, for the owner: syncing with GitHub, large files, and git for this session. */
|
|
1150
795
|
/**
|
|
1151
796
|
* "Make this a quilt" (Seam's plans/quilt-source-control/16, flag 9), for the session's owner while its
|
|
1152
|
-
* code is
|
|
797
|
+
* code is not in Seam yet: one button. With no git remote the relay makes the quilt and moves the files
|
|
1153
798
|
* into its squares at once; with one, it becomes a quilt with its history (makeQuiltWords).
|
|
1154
799
|
*/
|
|
1155
800
|
function makeQuiltHtml (st) {
|
|
1156
801
|
if (inSeam(st)) return ''
|
|
1157
802
|
// Everyone hears that live sync stopped at the forced date; only the owner gets the button.
|
|
1158
803
|
if (!st.access?.owner) return st.force && st.force.stopped ? `<div class="pm-section"><div class="pm-title">Seam</div><div class="pm-card git-card"><p class="git-line warn">${esc(st.force.stopped.why || '')}</p></div></div>` : ''
|
|
1159
|
-
const words = makeQuiltWords({ quilt: st.quilt || null, remote: !!
|
|
804
|
+
const words = makeQuiltWords({ quilt: st.quilt || null, remote: !!(st.quilt && st.quilt.hasRemote) || !!(st.force && (st.force.remote || st.force.stopped)), moving: !!st.moving, force: st.force || null, importing: st.importing || null, shadow: st.shadow || null })
|
|
1160
805
|
return `<div class="pm-section"><div class="pm-title">Seam</div><div class="pm-card git-card">
|
|
1161
806
|
<div class="git-head"><h4>Make this a quilt</h4><span class="pill">Seam</span></div>
|
|
1162
807
|
${words.note ? `<p class="git-line warn">${esc(words.note)}</p>` : ''}
|
|
@@ -1186,54 +831,6 @@ async function makeQuilt (btn) {
|
|
|
1186
831
|
} finally { renderSessionSettings({ force: true }) }
|
|
1187
832
|
}
|
|
1188
833
|
|
|
1189
|
-
function githubSettingsHtml (st) {
|
|
1190
|
-
if (!st.access?.owner) return ''
|
|
1191
|
-
const sync = st.access.githubSync
|
|
1192
|
-
const rows = [
|
|
1193
|
-
sync !== undefined ? `<label class="pm-switch"><span><b>Sync with GitHub</b><small>Pushes and merges on GitHub come into the session within seconds; nothing goes to GitHub until someone commits.</small></span><input type="checkbox" role="switch" data-github-sync ${sync !== false ? 'checked' : ''}></label>` : '',
|
|
1194
|
-
sync !== undefined && sync !== false ? `<label class="pm-switch"><span><b>Let the relay commit large files</b><small>Large files are end-to-end encrypted. On branches that sync with GitHub this lets the relay read them, so a commit can include them.</small></span><input type="checkbox" role="switch" data-large-to-relay ${st.access.largeToRelay ? 'checked' : ''}></label>` : ''
|
|
1195
|
-
].filter(Boolean).join('')
|
|
1196
|
-
return `<div class="pm-section"><div class="pm-title">GitHub</div>${gitCardHtml(st)}${rows ? `<div class="pm-card pm-settings">${rows}</div>` : ''}</div>`
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
/** Opens session `room`: the one already open in this app, else a recent one on this computer. */
|
|
1200
|
-
async function openLiveSession (room) {
|
|
1201
|
-
for (const [id, s] of state.sessions) if (s && s.status && s.status.room === room) return go(id)
|
|
1202
|
-
const r = (state.recent || []).find((x) => x.room === room)
|
|
1203
|
-
if (!r) { toast('That session isn\'t on this computer: ask its owner for an invite.'); return }
|
|
1204
|
-
const opened = await api('POST', '/api/sessions', { mode: 'rejoin', dir: r.dir })
|
|
1205
|
-
state.sessions.set(opened.id, opened)
|
|
1206
|
-
await go(opened.id)
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
/** The session's repository on GitHub and whether a member's connection lets agents commit there, from the relay's branch list. */
|
|
1210
|
-
function githubStateOf (st) {
|
|
1211
|
-
if (inSeam(st)) return null // GitHub only receives Seam's export of main: nothing to connect or sync here
|
|
1212
|
-
const list = st.branches || []
|
|
1213
|
-
const b = (st.git && list.find((x) => x.name === st.git.key)) || list.find((x) => x.default) || null
|
|
1214
|
-
const r = b && b.relay
|
|
1215
|
-
if (!r || !r.repo || !/^github\.com\//.test(r.repo)) return null
|
|
1216
|
-
return { repo: r.repo.replace(/^github\.com\//, ''), state: r.github ? r.github.state : '', login: r.github ? r.github.login : '' }
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
/** The connection, in a sentence. */
|
|
1220
|
-
function githubWords (gh) {
|
|
1221
|
-
if (!gh) return ''
|
|
1222
|
-
if (gh.state === 'ok') return `Git is on for everyone here, through @${gh.login}'s connection: people and agents push to ${gh.repo} with git, nothing to set up (a short-lived token each), and pushes come in within seconds. To keep a branch safe for good, protect it on GitHub.`
|
|
1223
|
-
if (gh.state === 'not-installed') return `Quilt isn't installed on ${gh.repo} on GitHub yet.`
|
|
1224
|
-
if (gh.state === 'no-access') return `No connected GitHub account (@${gh.login}) may write to ${gh.repo}.`
|
|
1225
|
-
if (gh.state === 'not-connected') return `Nobody here has connected GitHub for ${gh.repo} yet.`
|
|
1226
|
-
return `Checking ${gh.repo}…`
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
// --------------------------------------------------------- GitHub card --
|
|
1230
|
-
// A session on a GitHub repository that no member has connected yet: one card at the top, for
|
|
1231
|
-
// people, with the one click that lets agents commit and merge on their own. "Not now" keeps it
|
|
1232
|
-
// away for a week; the commit chip still says "Connect GitHub".
|
|
1233
|
-
const GH_DISMISS_MS = 7 * 24 * 60 * 60 * 1000
|
|
1234
|
-
const ghDismissKey = (repo) => `quilt.github.notnow.${repo}`
|
|
1235
|
-
let ghConnecting = null // the repo a Connect click is waiting on (polling the account)
|
|
1236
|
-
|
|
1237
834
|
/**
|
|
1238
835
|
* In a session that is a quilt at layout 3: one plain line on where this folder's work goes (the
|
|
1239
836
|
* square it follows, and how to commit it; session-follow.js), or why an edit reached nobody.
|
|
@@ -1250,86 +847,12 @@ function renderSquareCard () {
|
|
|
1250
847
|
el.innerHTML = html
|
|
1251
848
|
}
|
|
1252
849
|
|
|
1253
|
-
function renderGithubCard () {
|
|
1254
|
-
const el = $('#github-card')
|
|
1255
|
-
if (!el) return
|
|
1256
|
-
const st = sum().status
|
|
1257
|
-
const gh = githubStateOf(st)
|
|
1258
|
-
const dismissed = gh && recall(ghDismissKey(gh.repo))
|
|
1259
|
-
const show = gh && ['not-connected', 'not-installed', 'no-access'].includes(gh.state) && !(dismissed && Date.now() - Number(dismissed) < GH_DISMISS_MS)
|
|
1260
|
-
el.hidden = !show
|
|
1261
|
-
if (!show) { el._html = ''; return }
|
|
1262
|
-
const action = gh.state === 'not-installed' ? 'Add it on GitHub' : 'Connect GitHub'
|
|
1263
|
-
const html = `<div class="gh-card">
|
|
1264
|
-
<div class="gh-text"><b>${esc(gh.state === 'not-installed' ? `Quilt isn't on ${gh.repo} yet` : gh.state === 'no-access' ? `@${gh.login} can't write to ${gh.repo}` : `This session follows ${gh.repo}`)}</b>
|
|
1265
|
-
<span>${esc(gh.state === 'not-installed' ? 'Add the repository to Quilt on GitHub, and agents here commit and merge on their own; pushes come in within seconds.' : gh.state === 'no-access' ? 'Someone whose GitHub account can write to it connects once, and that covers everyone here, agents included.' : 'Connect GitHub once (one click, by anyone whose GitHub account can write to it): agents here then commit and merge on their own, and pushes come in within seconds. Nobody else has to connect.')}</span></div>
|
|
1266
|
-
<div class="gh-acts">${ghConnecting === gh.repo ? `<span class="hint">${loadingNote('Finish on GitHub, then come back…')}</span>` : `<button type="button" class="btn sm primary" data-gh-connect>${action}</button>`}<button type="button" class="btn sm ghost" data-gh-later>Not now</button></div>
|
|
1267
|
-
</div>`
|
|
1268
|
-
if (el._html === html) return
|
|
1269
|
-
el._html = html
|
|
1270
|
-
el.innerHTML = html
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
850
|
/** The square card's button: a folder whose branch has no square opens one (layout 3). */
|
|
1274
851
|
function bindSquareCard () {
|
|
1275
852
|
const el = $('#square-card')
|
|
1276
853
|
if (el) el.addEventListener('click', (e) => { if (e.target.closest('[data-square-open]') && !squareBusy) squareAct('open_square') })
|
|
1277
854
|
}
|
|
1278
855
|
|
|
1279
|
-
function bindGithubCard () {
|
|
1280
|
-
const el = $('#github-card')
|
|
1281
|
-
if (!el) return
|
|
1282
|
-
el.addEventListener('click', (e) => {
|
|
1283
|
-
if (e.target.closest('[data-gh-connect]')) return connectGithub()
|
|
1284
|
-
const later = e.target.closest('[data-gh-later]')
|
|
1285
|
-
if (!later) return
|
|
1286
|
-
const gh = githubStateOf(sum().status)
|
|
1287
|
-
if (gh) remember(ghDismissKey(gh.repo), String(Date.now()))
|
|
1288
|
-
renderGithubCard()
|
|
1289
|
-
})
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
/** Opens GitHub's install page for this session's repository, then watches for the connection and tells the relay to look again. */
|
|
1293
|
-
async function connectGithub () {
|
|
1294
|
-
const gh = githubStateOf(sum().status)
|
|
1295
|
-
try {
|
|
1296
|
-
const { url } = await api('POST', '/api/github/connect', gh ? { repo: gh.repo } : {})
|
|
1297
|
-
window.open(url, '_blank', 'noopener')
|
|
1298
|
-
toast('Finish on GitHub, then come back')
|
|
1299
|
-
} catch (err) { toast(err.message); return }
|
|
1300
|
-
if (!gh) return
|
|
1301
|
-
ghConnecting = gh.repo
|
|
1302
|
-
renderGithubCard()
|
|
1303
|
-
const id = current
|
|
1304
|
-
const until = Date.now() + 10 * 60 * 1000
|
|
1305
|
-
let was = null
|
|
1306
|
-
try { was = (await api('GET', '/api/github')).login || null } catch {}
|
|
1307
|
-
while (Date.now() < until && current === id && ghConnecting === gh.repo) {
|
|
1308
|
-
await new Promise((r) => setTimeout(r, 3000))
|
|
1309
|
-
let now = null
|
|
1310
|
-
try { now = (await api('GET', '/api/github')).login || null } catch {}
|
|
1311
|
-
if (!now || now === was) continue
|
|
1312
|
-
// Connected (or re-connected): the relay checks the repository again, and the card and chip follow.
|
|
1313
|
-
try {
|
|
1314
|
-
const r = await api('POST', `/api/sessions/${id}/github/recheck`)
|
|
1315
|
-
const s = r && r.github && r.github.state
|
|
1316
|
-
toast(s === 'ok' ? `Connected as @${now}: agents can commit here now` : s === 'not-installed' ? `Connected as @${now}, but ${gh.repo} wasn't picked: add it on GitHub` : s === 'no-access' ? `@${now} can't write to ${gh.repo}` : `Connected as @${now}`)
|
|
1317
|
-
} catch (err) { toast(err.message) }
|
|
1318
|
-
break
|
|
1319
|
-
}
|
|
1320
|
-
if (ghConnecting === gh.repo) ghConnecting = null
|
|
1321
|
-
renderGithubCard()
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
async function askForCommit () {
|
|
1325
|
-
const message = await ask({ title: 'Ask for a commit', message: 'Of everything you changed that is not in git yet. A person with git on this branch commits it, and you are told the commit.', ok: 'Ask', input: { label: 'What is the commit for?', placeholder: 'Pricing page and download button' } })
|
|
1326
|
-
if (!message) return
|
|
1327
|
-
try {
|
|
1328
|
-
const r = await api('POST', `/api/sessions/${current}/commit-request`, { message: message.trim() })
|
|
1329
|
-
toast(`Asked for a commit of ${r.files.length} file${r.files.length === 1 ? '' : 's'}`)
|
|
1330
|
-
} catch (err) { toast(err.message) }
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
856
|
// ----------------------------------------------------------------- access --
|
|
1334
857
|
const roleLabel = (r) => r === 'owner' ? 'Owner' : r === 'viewer' ? 'View only' : 'Can edit'
|
|
1335
858
|
const scopesText = (scopes) => (scopes || []).join(', ')
|
|
@@ -1485,23 +1008,6 @@ function membersHtml (st, kind = 'people') {
|
|
|
1485
1008
|
<div class="pm-foot"><button type="button" class="btn sm ghost danger" data-end-session>End session for everyone</button></div>`
|
|
1486
1009
|
}
|
|
1487
1010
|
|
|
1488
|
-
/**
|
|
1489
|
-
* The owner's GitHub token: the relay brings commits in from a private repository with it while
|
|
1490
|
-
* nobody's folder is online, and commits the work of agents without a folder with it (quilt_commit) when it can write.
|
|
1491
|
-
* Never shown back: only whether one is set.
|
|
1492
|
-
*/
|
|
1493
|
-
export function githubTokenHtml (acc) {
|
|
1494
|
-
if (!acc || !acc.owner) return ''
|
|
1495
|
-
const set = !!acc.githubToken
|
|
1496
|
-
return `<div class="pm-section"><div class="pm-title">GitHub token</div>
|
|
1497
|
-
<form class="pm-gh pm-admit" autocomplete="off">
|
|
1498
|
-
<label for="pm-gh-token"><span>GitHub token for this session's repository</span></label>
|
|
1499
|
-
<input class="input" id="pm-gh-token" type="password" name="token" autocomplete="off" spellcheck="false" placeholder="${set ? 'A token is set: paste another to replace it' : 'github_pat_… for this repository'}" aria-label="GitHub token">
|
|
1500
|
-
<div class="pm-gh-row"><button type="submit" class="btn sm">Save token</button>${set ? '<button type="button" class="btn sm ghost" data-gh-clear>Remove token</button>' : ''}</div>
|
|
1501
|
-
<div class="hint">With "Contents: Read" Quilt brings in commits while everyone's offline (private repositories need it). With "Contents: Read and write" (and "Pull requests: Read and write") agents can commit their own work, within what you let them above, without anyone online. It stays on the relay and is never shown to anyone, you included.</div>
|
|
1502
|
-
</form></div>`
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
1011
|
/** With workspaces on, the agents this session's workspace invites (and the ones not invited), for its People. */
|
|
1506
1012
|
async function loadSessionAgents () {
|
|
1507
1013
|
const id = current
|
|
@@ -1908,10 +1414,10 @@ function renderSessionSettings ({ force = false } = {}) {
|
|
|
1908
1414
|
const active = box.contains(document.activeElement) && document.activeElement.closest('.pm-member') ? document.activeElement : null
|
|
1909
1415
|
if (!force && active && box.querySelector('.pm-member.edit')) return
|
|
1910
1416
|
const refocus = active && active.name ? [active.closest('.pm-member').dataset.key, active.name] : null
|
|
1911
|
-
//
|
|
1912
|
-
const vc = inSeam(st) ? seamSettingsHtml(st) : makeQuiltHtml(st)
|
|
1417
|
+
// Source control is Seam's: where the history is (or, for the owner of a session not in Seam yet, the button that moves it there).
|
|
1418
|
+
const vc = inSeam(st) ? seamSettingsHtml(st) : makeQuiltHtml(st)
|
|
1913
1419
|
box.innerHTML = ssTab === 'chat' ? sharingHtml(st)
|
|
1914
|
-
: ssTab === 'vc' ? (vc || '<div class="pm-empty">Only the session owner
|
|
1420
|
+
: ssTab === 'vc' ? (vc || '<div class="pm-empty">Only the session owner sees where this session\'s history is.</div>')
|
|
1915
1421
|
: membersHtml(st, ssTab)
|
|
1916
1422
|
if (refocus) [...box.querySelectorAll('.pm-member')].find((f) => f.dataset.key === refocus[0])?.elements?.[refocus[1]]?.focus()
|
|
1917
1423
|
}
|