heyquilt 0.3.140 → 0.3.142
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 +8 -0
- package/package.json +1 -1
- package/src/session-follow.js +18 -5
- package/src/session.js +6 -0
- package/src/ui/seam-model.js +22 -12
package/RELEASES.md
CHANGED
|
@@ -11,6 +11,14 @@ Format: `## <version> — <YYYY-MM-DD>`, an optional one-line summary, then bull
|
|
|
11
11
|
Lead each bullet with a short bold phrase. Inline `code` and **bold** are rendered;
|
|
12
12
|
nothing else is.
|
|
13
13
|
|
|
14
|
+
## 0.3.142 — 2026-11-01
|
|
15
|
+
|
|
16
|
+
- **The source control button names your square by its branch.** It said `sq:09a100d0`, which meant nothing to anyone; it now says `main` (or whichever branch your folder is on), and so do its panel and the Squares list. The panel also says plainly that the files waiting to be committed are everyone's edits on that branch, not only yours.
|
|
17
|
+
|
|
18
|
+
## 0.3.141 — 2026-11-01
|
|
19
|
+
|
|
20
|
+
- **A new, empty folder joining a session that is already in Seam takes the square as it is.** Its first files come from the session's old copy of the branch, and before, a folder without git could start following its square while that old copy was still being written to it, so every older file went into the square as its own edits (an agent's new folder put 166 stale files into main's square). Now the folder's own syncing stops before it follows the square, and nothing it got from the session is ever counted as its edits.
|
|
21
|
+
|
|
14
22
|
## 0.3.140 — 2026-11-01
|
|
15
23
|
|
|
16
24
|
0.3.139 was tagged but never published; everything meant for it is here.
|
package/package.json
CHANGED
package/src/session-follow.js
CHANGED
|
@@ -307,8 +307,9 @@ export class SquareFollower {
|
|
|
307
307
|
const s = this.session
|
|
308
308
|
// Held before the first await: nothing of the folder reaches the read-only branch document while the block opens.
|
|
309
309
|
const at = this.inPlaceOn() ? squareFor(quilt, s.squareBranchKey ? s.squareBranchKey() : s.branchKey()) : null
|
|
310
|
-
|
|
311
|
-
|
|
310
|
+
const entered = at && s.enterSquare && !(this.here && this.here.square === at.square) ? at.square : null
|
|
311
|
+
if (entered) s.enterSquare(at, quilt)
|
|
312
|
+
const next = this.turn.then(() => this.settle(quilt, entered)).catch((err) => { if (s.log) s.log(`🧵 ${err.message}`) })
|
|
312
313
|
this.turn = next
|
|
313
314
|
// A merge onto main moves a branch on (quiltNext), and the relay says so: the other squares may be behind now.
|
|
314
315
|
next.then(() => this.catchUpSoon())
|
|
@@ -318,7 +319,8 @@ export class SquareFollower {
|
|
|
318
319
|
/** Seam on for the app, a folder of the session's own, and a session that can hold its sync (a Session). */
|
|
319
320
|
inPlaceOn () { return !!(appSeamUrl() && this.session.root && this.session.enterSquare) }
|
|
320
321
|
|
|
321
|
-
|
|
322
|
+
/** `entered`: the square onQuilt already held the session's sync for. */
|
|
323
|
+
async settle (quilt, entered = null) {
|
|
322
324
|
const s = this.session
|
|
323
325
|
const at = this.inPlaceOn() ? squareFor(quilt, s.squareBranchKey ? s.squareBranchKey() : s.branchKey()) : null
|
|
324
326
|
// The separate folder lets go of a square the branch no longer has (switched back, moved).
|
|
@@ -340,6 +342,11 @@ export class SquareFollower {
|
|
|
340
342
|
} else if (s.log) s.log(`🧵 this folder left square ${squareShort(was)}: its branch is on ${squareShort(at.square)} now.`)
|
|
341
343
|
}
|
|
342
344
|
if (!at || (this.here && this.here.square === at.square)) return
|
|
345
|
+
// The branch may only be known now (a folder without git learns the room's default branch after
|
|
346
|
+
// the relay's quilt message): the session's own sync holds before the block opens, every time, or
|
|
347
|
+
// it writes the read-only branch document's older copy over the square's files and the block takes
|
|
348
|
+
// that for this member's edits (2026-11-01: an agent's new folder put 166 stale files into main's square).
|
|
349
|
+
if (s.enterSquare && entered !== at.square) s.enterSquare(at, quilt)
|
|
343
350
|
await this.openHere(at, quilt)
|
|
344
351
|
}
|
|
345
352
|
|
|
@@ -349,13 +356,18 @@ export class SquareFollower {
|
|
|
349
356
|
const here = { square: at.square, quilt, block: null, problem: '', opening: true }
|
|
350
357
|
this.here = here
|
|
351
358
|
if (s.scheduleStatusWrite) s.scheduleStatusWrite()
|
|
359
|
+
// A folder that joined the quilt empty (Session.joinedEmpty): what it has is the session's sync from
|
|
360
|
+
// the read-only branch document, an older copy of the square's files. Its first look takes nothing
|
|
361
|
+
// of that for its edits (no refused writes, boxes or branch work of its own); only a file changed
|
|
362
|
+
// on disk since the session wrote it is the member's.
|
|
363
|
+
const empty = !fresh && !!s.joinedEmpty
|
|
352
364
|
try {
|
|
353
365
|
// Edits the read-only branch document refused (they reached it after the switch) are the folder's too.
|
|
354
|
-
const unsent = !fresh && s.unsentPaths ? await s.unsentPaths().catch(() => null) : null
|
|
366
|
+
const unsent = !fresh && !empty && s.unsentPaths ? await s.unsentPaths().catch(() => null) : null
|
|
355
367
|
const block = await this.open({
|
|
356
368
|
dir: s.root,
|
|
357
369
|
// A square opened for this folder's branch (Session.openSquareHere: the branch had none) takes the folder as it is: its files are the work.
|
|
358
|
-
inPlace: fresh ? { edited: () => false, fresh: true } : s.folderWins === at.square ? { edited: () => true } : { edited: (p) => s.editedSinceSync(p) || !!(unsent && unsent.has(p)) || !!(s.boxedHere && s.boxedHere(p)), boxed: (p) => !!(s.boxedHere && s.boxedHere(p)), heldBack: (p) => !!(s.heldBackHere && s.heldBackHere(p)), synced: (p) => !!(s.syncedHere && s.syncedHere(p)), base: s.syncedText ? (p) => s.syncedText(p) : null, own: () => (s.branchOwnPaths ? s.branchOwnPaths() : null), atTip: ((quilt && quilt.atTip) || []).includes(s.squareBranchKey ? s.squareBranchKey() : s.branchKey()) },
|
|
370
|
+
inPlace: fresh ? { edited: () => false, fresh: true } : empty ? { edited: (p) => !!(s.editedSinceSync && s.editedSinceSync(p)), fresh: true } : s.folderWins === at.square ? { edited: () => true } : { edited: (p) => s.editedSinceSync(p) || !!(unsent && unsent.has(p)) || !!(s.boxedHere && s.boxedHere(p)), boxed: (p) => !!(s.boxedHere && s.boxedHere(p)), heldBack: (p) => !!(s.heldBackHere && s.heldBackHere(p)), synced: (p) => !!(s.syncedHere && s.syncedHere(p)), base: s.syncedText ? (p) => s.syncedText(p) : null, own: () => (s.branchOwnPaths ? s.branchOwnPaths() : null), atTip: ((quilt && quilt.atTip) || []).includes(s.squareBranchKey ? s.squareBranchKey() : s.branchKey()) },
|
|
359
371
|
skip: (p) => !s.syncable(p),
|
|
360
372
|
skipDir: s.ignoredDir ? (p) => s.ignoredDir(p) : null,
|
|
361
373
|
record: path.join(s.stateDir || path.join(s.root, '.quilt'), 'block.json'),
|
|
@@ -371,6 +383,7 @@ export class SquareFollower {
|
|
|
371
383
|
onEdits: s.squareEdits ? (list, doc, held) => { if (this.here === here) s.squareEdits(list, doc, held) } : null
|
|
372
384
|
})
|
|
373
385
|
if (this.here !== here) { await block.close().catch(() => {}); return }
|
|
386
|
+
if (empty) s.joinedEmpty = false
|
|
374
387
|
here.block = block
|
|
375
388
|
here.opening = false
|
|
376
389
|
if (s.followSquareDoc) s.followSquareDoc(block.doc)
|
package/src/session.js
CHANGED
|
@@ -592,6 +592,11 @@ export class Session extends EventEmitter {
|
|
|
592
592
|
this.loadWebhook()
|
|
593
593
|
this.loadOnMention()
|
|
594
594
|
const hadState = this.loadState()
|
|
595
|
+
// A folder never synced and empty as the session starts: in a session that is a layout-3 quilt already,
|
|
596
|
+
// whatever it gets before its block opens is the read-only branch document's older copy, never its
|
|
597
|
+
// member's edits (session-follow.js openHere; 2026-11-01). Cleared by a first word of the quilt that is
|
|
598
|
+
// not layout 3, and once the block opens.
|
|
599
|
+
this.joinedEmpty = !hadState && !walk(this.root, this.ig).some((rel) => this.syncable(rel))
|
|
595
600
|
// Restarted while the folder follows its square: its .git is set aside (session-follow.js) and stays so; its branch is the note's.
|
|
596
601
|
this.gitAside = hasGitDir(this.root) ? null : gitAsideOf(this.root)
|
|
597
602
|
if (hadState) this.loadClaims()
|
|
@@ -638,6 +643,7 @@ export class Session extends EventEmitter {
|
|
|
638
643
|
// Seam (Seam's plans/quilt-source-control/16): the relay says when the session is a quilt, and which.
|
|
639
644
|
this.conn.on('quilt', (q) => {
|
|
640
645
|
this.quilt = q
|
|
646
|
+
if (!(q && q.layout === 3)) this.joinedEmpty = false
|
|
641
647
|
this.emit('quilt', q)
|
|
642
648
|
// At layout 3 this folder follows its branch's square by itself (session-follow.js).
|
|
643
649
|
if (!this.squareFollower) this.squareFollower = new SquareFollower(this)
|
package/src/ui/seam-model.js
CHANGED
|
@@ -17,12 +17,22 @@ const quiltName = (st) => (st.quilt && (st.quilt.slug || st.quilt.id)) || 'the q
|
|
|
17
17
|
/** The square this folder follows, as Seam names it (`sq:` and the end of its id), or ''. */
|
|
18
18
|
export const squareName = (st) => (st && st.square && st.square.short ? `sq:${st.square.short}` : '')
|
|
19
19
|
|
|
20
|
+
/** The branch whose square this folder follows (`st.squares` is branch -> square name), or ''. */
|
|
21
|
+
export function squareBranch (st) {
|
|
22
|
+
const name = squareName(st)
|
|
23
|
+
const hit = name && Object.entries(st.squares || {}).find(([, sq]) => sq === name)
|
|
24
|
+
return hit ? hit[0] : (name && st.branch) || ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The square as a person reads it: its branch (a square's id means nothing to anyone), or its id when no branch is known. */
|
|
28
|
+
const squareWords = (st) => squareBranch(st) || squareName(st)
|
|
29
|
+
|
|
20
30
|
/**
|
|
21
31
|
* The source control button at layout 3: { label, title, aria } for the square this folder follows, or
|
|
22
32
|
* why it follows none yet. `label` '' leaves the button with its icon and count only.
|
|
23
33
|
*/
|
|
24
34
|
export function seamChip (st) {
|
|
25
|
-
const name =
|
|
35
|
+
const name = squareWords(st)
|
|
26
36
|
const s = st.square
|
|
27
37
|
const label = name
|
|
28
38
|
const title = st.moving ? movingCard(st).text
|
|
@@ -30,13 +40,13 @@ export function seamChip (st) {
|
|
|
30
40
|
: st.readOnly && !(s && !s.opening && !s.problem)
|
|
31
41
|
? `Your edits here are not reaching anyone: ${st.readOnly.why}`
|
|
32
42
|
: !s ? `This session's code is committed and merged in Seam (${quiltName(st)}), and this folder is not following a square yet.`
|
|
33
|
-
: s.problem ? `This folder's work belongs
|
|
34
|
-
: s.opening ? `This folder is starting to follow
|
|
35
|
-
: [`This folder's work is live
|
|
36
|
-
// The number is the square's work not committed yet (everyone's on it): what a commit takes.
|
|
43
|
+
: s.problem ? `This folder's work belongs on ${name}, but it can't follow it yet: ${s.problem}`
|
|
44
|
+
: s.opening ? `This folder is starting to follow ${name}.`
|
|
45
|
+
: [`This folder's work is live on ${name} in ${quiltName(st)}, with everyone else's there. Click to commit it or merge it onto main.`, ...catchUpWords(s)].join(' ')
|
|
46
|
+
// The number is the square's work not committed yet (everyone's on it, not only yours): what a commit takes.
|
|
37
47
|
const count = s && s.work ? s.work.count : 0
|
|
38
|
-
const countLine = count ? `${files(count)} not committed
|
|
39
|
-
return { label, title, aria: `Source control: ${name
|
|
48
|
+
const countLine = count ? `${files(count)} changed on ${name} and not committed yet, by everyone on it.` : ''
|
|
49
|
+
return { label, title, aria: `Source control: ${name || 'Seam'}${count ? `, ${files(count)} not committed` : ''}`, count, countLine }
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
const files = (n) => `${n} file${n === 1 ? '' : 's'}`
|
|
@@ -98,10 +108,10 @@ export function squareCard (st) {
|
|
|
98
108
|
*/
|
|
99
109
|
export function squarePanelHtml (st, { busy = '' } = {}) {
|
|
100
110
|
const s = st.square
|
|
101
|
-
const name =
|
|
111
|
+
const name = squareWords(st)
|
|
102
112
|
const following = s && !s.opening && !s.problem
|
|
103
113
|
const viewer = !!(st.access && st.access.role === 'viewer')
|
|
104
|
-
const head = `<div class="br-here sq-here"><div class="br-title">${name ? `
|
|
114
|
+
const head = `<div class="br-here sq-here"><div class="br-title"${squareName(st) ? ` title="Seam's square ${esc(squareName(st))}"` : ''}>${name ? `Live work on <b>${esc(name)}</b> in ${esc(quiltName(st))}` : `Code committed and merged in Seam: <b>${esc(quiltName(st))}</b>`}</div>`
|
|
105
115
|
// While the session moves, nothing is committed or merged from here: the move is what it waits for.
|
|
106
116
|
if (st.moving) return `${head}<div class="br-sub warn">${esc(movingCard(st).text)}</div></div>`
|
|
107
117
|
if (st.squareless) {
|
|
@@ -112,13 +122,13 @@ export function squarePanelHtml (st, { busy = '' } = {}) {
|
|
|
112
122
|
const why = st.readOnly ? st.readOnly.why : s ? s.line : 'This folder is not following a square yet: Quilt opens it by itself once the session says which.'
|
|
113
123
|
return `${head}<div class="br-sub${st.readOnly || (s && s.problem) ? ' warn' : ''}">${esc(why)}</div></div>`
|
|
114
124
|
}
|
|
115
|
-
const sub =
|
|
125
|
+
const sub = `<div class="br-sub">Everyone on ${esc(name)} edits this together, live: the files below are everyone's edits, not only yours. Commit them to keep a version; merge them to put them on main (GitHub gets main from Seam by itself).</div>`
|
|
116
126
|
const caught = catchUpWords(s)
|
|
117
127
|
const work = (caught.length ? `<div class="br-sub${s.conflicted ? ' warn' : ''}">${esc(caught.join(' '))}</div>` : '') + workHtml(s.work)
|
|
118
128
|
if (viewer) return `${head}${sub}${work}</div>`
|
|
119
129
|
return `${head}${sub}${work}
|
|
120
130
|
<form class="cp-commit" data-square-commit><input class="input" name="message" placeholder="What this commit is" aria-label="Commit message" maxlength="2000" required autocomplete="off"${busy ? ' disabled' : ''}><button type="submit" class="btn sm primary"${busy ? ' disabled' : ''}>${busy === 'commit' ? 'Committing…' : 'Commit'}</button></form>
|
|
121
|
-
<div class="sq-acts"><button type="button" class="btn sm" data-square-merge${busy ? ' disabled' : ''} title="Commits what is not committed yet, then merges
|
|
131
|
+
<div class="sq-acts"><button type="button" class="btn sm" data-square-merge${busy ? ' disabled' : ''} title="Commits what is not committed yet, then merges it onto main once its checks pass">${busy === 'merge' ? 'Merging…' : 'Merge onto main'}</button></div>
|
|
122
132
|
</div>`
|
|
123
133
|
}
|
|
124
134
|
|
|
@@ -201,7 +211,7 @@ export function squaresSideHtml (st) {
|
|
|
201
211
|
add(mine, 'you')
|
|
202
212
|
for (const p of st.peers || []) if (p && p.name && p.branch && squares[p.branch] && !(p.persona && p.of === (st.me && st.me.name))) add(squares[p.branch], p.mine ? 'your AI' : p.name)
|
|
203
213
|
const n = st.square && st.square.work ? st.square.work.count : 0
|
|
204
|
-
const rows = [...groups].map(([sq, who]) => `<li class="sq-side-row${sq === mine ? ' on' : ''}"><div
|
|
214
|
+
const rows = [...groups].map(([sq, who]) => `<li class="sq-side-row${sq === mine ? ' on' : ''}"><div title="Seam's square ${esc(sq)}"><b>${esc(branchOf[sq] || sq)}</b></div><div class="hint">${esc(who.join(', '))}${sq === mine ? ` · ${n ? `${files(n)} not committed` : 'all committed'}` : ''}</div></li>`).join('')
|
|
205
215
|
// The pane's own title says Squares already (session.js, as it says Boxes outside Seam): no second heading.
|
|
206
216
|
return rows ? `<ul class="sq-side">${rows}</ul>` : ''
|
|
207
217
|
}
|