heyquilt 0.3.121 → 0.3.138

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/RELEASES.md +292 -17
  2. package/bin/quilt.js +72 -5
  3. package/desktop/main.js +16 -2
  4. package/package.json +3 -1
  5. package/src/agent-task-workflow.js +2 -2
  6. package/src/agents/common.js +13 -0
  7. package/src/ai-vm.js +9 -1
  8. package/src/api/activity.js +5 -0
  9. package/src/api/memory-store.js +21 -10
  10. package/src/api/notify.js +134 -0
  11. package/src/api/routes/browsers.js +29 -0
  12. package/src/api/routes/github.js +0 -13
  13. package/src/api/routes/relay.js +26 -3
  14. package/src/api/routes/seam-connect.js +103 -0
  15. package/src/api/routes/session-migrate.js +39 -0
  16. package/src/api/routes/sessions.js +73 -1
  17. package/src/api/server.js +46 -12
  18. package/src/api/supabase-store.js +53 -4
  19. package/src/autowork.js +182 -16
  20. package/src/connection.js +39 -9
  21. package/src/control.js +139 -13
  22. package/src/force-date.js +27 -0
  23. package/src/fsutil.js +13 -3
  24. package/src/gitstate.js +38 -2
  25. package/src/history.js +25 -1
  26. package/src/mcp.js +93 -7
  27. package/src/merge3.js +11 -3
  28. package/src/now-summary.js +113 -0
  29. package/src/pathrules.js +12 -0
  30. package/src/protocol.js +10 -2
  31. package/src/quilt-default.js +84 -0
  32. package/src/quilt-history.js +106 -0
  33. package/src/quilt-remote.js +78 -0
  34. package/src/relay-commit.js +5 -0
  35. package/src/relay-lease.js +6 -2
  36. package/src/relay-mcp.js +172 -8
  37. package/src/relay-outbound.js +1 -1
  38. package/src/relay-prune.js +4 -1
  39. package/src/release-words.js +2 -2
  40. package/src/runner.js +20 -7
  41. package/src/seam-access.js +97 -0
  42. package/src/seam-claims.js +53 -0
  43. package/src/seam-url.js +15 -0
  44. package/src/server.js +1767 -33
  45. package/src/session-follow.js +515 -0
  46. package/src/session-layout3.js +466 -0
  47. package/src/session-migrate.js +123 -0
  48. package/src/session-quilt.js +145 -0
  49. package/src/session-seam.js +284 -0
  50. package/src/session.js +899 -46
  51. package/src/source-live/bring-in.js +283 -0
  52. package/src/source-live/live-block.js +507 -35
  53. package/src/source-live/live-square.js +17 -6
  54. package/src/status.js +5 -0
  55. package/src/ui/agent-guide.js +1 -1
  56. package/src/ui/app.css +85 -20
  57. package/src/ui/app.js +4 -1
  58. package/src/ui/chat.js +9 -3
  59. package/src/ui/cloud-ai-model.js +13 -0
  60. package/src/ui/cloud-ai.js +10 -2
  61. package/src/ui/common.js +6 -2
  62. package/src/ui/fileview.js +20 -7
  63. package/src/ui/force-words.js +19 -0
  64. package/src/ui/git-rule.js +9 -0
  65. package/src/ui/home.js +0 -147
  66. package/src/ui/loom-model.js +15 -1
  67. package/src/ui/quiltview.js +213 -0
  68. package/src/ui/seam-model.js +251 -0
  69. package/src/ui/session.js +413 -31
  70. package/src/ui/voice-call.js +83 -14
  71. package/src/ui/voice-model.js +39 -0
  72. package/src/ui/voice.js +167 -17
  73. package/src/ui-server.js +80 -28
  74. package/src/update-check.js +25 -1
  75. package/src/voice.js +13 -6
  76. package/src/git.js +0 -147
  77. package/src/quilts/AGENTS.md +0 -1
  78. package/src/quilts/CLAUDE.md +0 -1
  79. package/src/quilts/archiver.js +0 -175
  80. package/src/quilts/autorevert.js +0 -49
  81. package/src/quilts/bisect-run.js +0 -31
  82. package/src/quilts/blame.js +0 -103
  83. package/src/quilts/block.js +0 -347
  84. package/src/quilts/bucket.js +0 -25
  85. package/src/quilts/cbor.js +0 -92
  86. package/src/quilts/claims.js +0 -144
  87. package/src/quilts/cli.js +0 -449
  88. package/src/quilts/client.js +0 -128
  89. package/src/quilts/conflict-format.js +0 -190
  90. package/src/quilts/describe.js +0 -266
  91. package/src/quilts/dogfood.js +0 -82
  92. package/src/quilts/errors.js +0 -8
  93. package/src/quilts/events.js +0 -160
  94. package/src/quilts/export-git.js +0 -130
  95. package/src/quilts/export.js +0 -81
  96. package/src/quilts/fastmerge.js +0 -41
  97. package/src/quilts/hash.js +0 -44
  98. package/src/quilts/history.js +0 -266
  99. package/src/quilts/ids.js +0 -12
  100. package/src/quilts/import-git.js +0 -291
  101. package/src/quilts/import.js +0 -134
  102. package/src/quilts/index.js +0 -72
  103. package/src/quilts/limits.js +0 -67
  104. package/src/quilts/lines.js +0 -49
  105. package/src/quilts/live.js +0 -50
  106. package/src/quilts/merge-smart.js +0 -114
  107. package/src/quilts/merge.js +0 -486
  108. package/src/quilts/objects.js +0 -144
  109. package/src/quilts/pack.js +0 -118
  110. package/src/quilts/records.js +0 -211
  111. package/src/quilts/resolutions.js +0 -69
  112. package/src/quilts/reviews.js +0 -195
  113. package/src/quilts/routes.js +0 -126
  114. package/src/quilts/service.js +0 -923
  115. package/src/quilts/setup.js +0 -34
  116. package/src/quilts/signing.js +0 -149
  117. package/src/quilts/squares.js +0 -239
  118. package/src/quilts/store-memory.js +0 -683
  119. package/src/quilts/store-rpc.js +0 -79
  120. package/src/quilts/store-supabase.js +0 -13
  121. package/src/quilts/tags.js +0 -64
  122. package/src/quilts/textdiff.js +0 -96
  123. package/src/quilts/tokens.js +0 -66
  124. package/src/quilts/tools.js +0 -375
  125. package/src/quilts/tree.js +0 -351
  126. package/src/quilts/webhook.js +0 -116
  127. package/src/quilts/window.js +0 -119
  128. package/src/quilts/xdiff.js +0 -435
  129. package/src/ui/branches.js +0 -111
@@ -0,0 +1,251 @@
1
+ // The app in a session that moved into Seam (layout 3, Seam's plans/quilt-source-control/16): what the
2
+ // source control button, its panel and Session settings show there. Each folder follows its branch's
3
+ // square (session-follow.js), and work lands by committing the square and merging it onto main, in
4
+ // Seam; git is set aside in the folder and GitHub only receives Seam's export of main. So nothing here
5
+ // offers git, branches, pushes or pull requests. Pure: no DOM (test/unit/ui-seam-model.test.js).
6
+
7
+ import { forceAheadWords } from './force-words.js'
8
+
9
+ const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]))
10
+
11
+ /** Whether the session's files are in Seam's squares now (the relay says layout 3): status.quilt from Session.quiltLink. */
12
+ export const inSeam = (st) => !!(st && st.quilt && st.quilt.layout === 3)
13
+
14
+ /** The quilt's name as people read it. */
15
+ const quiltName = (st) => (st.quilt && (st.quilt.slug || st.quilt.id)) || 'the quilt'
16
+
17
+ /** The square this folder follows, as Seam names it (`sq:` and the end of its id), or ''. */
18
+ export const squareName = (st) => (st && st.square && st.square.short ? `sq:${st.square.short}` : '')
19
+
20
+ /**
21
+ * The source control button at layout 3: { label, title, aria } for the square this folder follows, or
22
+ * why it follows none yet. `label` '' leaves the button with its icon and count only.
23
+ */
24
+ export function seamChip (st) {
25
+ const name = squareName(st)
26
+ const s = st.square
27
+ const label = name
28
+ const title = st.moving ? movingCard(st).text
29
+ : st.squareless ? squarelessWords(st).text
30
+ : st.readOnly && !(s && !s.opening && !s.problem)
31
+ ? `Your edits here are not reaching anyone: ${st.readOnly.why}`
32
+ : !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 in square ${name}, but it can't follow it yet: ${s.problem}`
34
+ : s.opening ? `This folder is starting to follow square ${name}.`
35
+ : [`This folder's work is live in square ${name} of ${quiltName(st)}. Click to commit it or merge it onto main.`, ...catchUpWords(s)].join(' ')
36
+ // The number is the square's work not committed yet (everyone's on it): what a commit takes.
37
+ const count = s && s.work ? s.work.count : 0
38
+ const countLine = count ? `${files(count)} not committed in this square.` : ''
39
+ return { label, title, aria: `Source control: ${name ? `square ${name}` : 'Seam'}${count ? `, ${files(count)} not committed` : ''}`, count, countLine }
40
+ }
41
+
42
+ const files = (n) => `${n} file${n === 1 ? '' : 's'}`
43
+ const patches = (n) => `${n} newer patch${n === 1 ? '' : 'es'}`
44
+ const listed = (list) => list.length > 4 ? `${list.slice(0, 4).join(', ')} and ${list.length - 4} more` : list.join(', ')
45
+
46
+ /**
47
+ * Main brought into the square (session-follow.js status: `behind`, `conflicted`, `brought`), in plain
48
+ * words: how far behind main it is, when main's newer patches last came in, and what conflicts with main.
49
+ * The app's square card and panel show these, and quilt_status says them. [] with nothing to say.
50
+ */
51
+ export function catchUpWords (s) {
52
+ if (!s) return []
53
+ const out = []
54
+ const b = s.brought
55
+ const old = Array.isArray(s.outdated) ? s.outdated : []
56
+ if (s.behind && old.length) out.push(`Main has ${patches(s.behind)} this square doesn't have yet. ${s.behind === 1 ? 'It comes' : 'They come'} in once ${listed(old)} ${old.length === 1 ? 'updates' : 'update'} Quilt: the version on ${old.length === 1 ? 'that folder' : 'those folders'} can't follow a square whose base moves.`)
57
+ else if (s.behind) out.push(`Main has ${patches(s.behind)} this square doesn't have yet: Quilt brings ${s.behind === 1 ? 'it' : 'them'} in by itself.`)
58
+ if (b && b.at) {
59
+ const at = new Date(b.at).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
60
+ const combined = b.combined || []
61
+ out.push(`Main's ${b.patches ? patches(b.patches) : 'newer patches'} came in at ${at}${b.by ? ` (brought in by ${b.by})` : ''}${combined.length ? `, combined with this square's work in ${listed(combined)}` : ''}.`)
62
+ const stale = Array.isArray(b.stale) ? b.stale : []
63
+ if (stale.length) out.push(`${files(stale.length)} in this square ${stale.length === 1 ? 'was an older copy' : 'were older copies'} of main's, not anyone's edit: main's ${stale.length === 1 ? 'version stands' : 'versions stand'} (${listed(stale)}).`)
64
+ }
65
+ const n = s.conflicted ?? (b && b.conflicts ? b.conflicts.length : 0)
66
+ if (n) {
67
+ const named = b && b.conflicts && b.conflicts.length ? ` (${listed(b.conflicts)})` : ''
68
+ out.push(`${files(n)} ${n === 1 ? 'conflicts' : 'conflict'} with main and ${n === 1 ? 'is' : 'are'} marked in this folder${named}: settle ${n === 1 ? 'it' : 'them'}, commit, and merge again.`)
69
+ }
70
+ return out
71
+ }
72
+
73
+ /**
74
+ * The square card above the main area: { title, text }, only when something needs the person (a
75
+ * conflict with main, edits reaching nobody, a move under way), else null. The app's words, not
76
+ * quilt_status's: a person commits and merges from the square button, not with a command.
77
+ */
78
+ export function squareCard (st) {
79
+ // The owner is moving the session (into Seam, or back): said first, with what to expect, on git or in Seam alike.
80
+ if (st.moving) return movingCard(st)
81
+ const s = st.square
82
+ const following = s && !s.opening && !s.problem
83
+ // The relay's own words name a tool: a person gets plain ones, and the button in the square panel.
84
+ if (st.squareless) return { ...squarelessWords(st), open: true }
85
+ if (st.readOnly && !following) return { title: 'Your edits here are not reaching anyone', text: st.readOnly.why }
86
+ if (!s) return null
87
+ if (s.problem || s.opening) return { title: s.problem ? 'Not following its square yet' : 'Starting to follow its square', text: s.line }
88
+ // Following its square with nothing to settle: the square button at the top says it, so no card (Daniel, 2026-11-01).
89
+ if (!(s.conflicted ?? (s.brought && s.brought.conflicts ? s.brought.conflicts.length : 0))) return null
90
+ return { title: 'Main conflicts with this square', text: catchUpWords(s).join(' ') }
91
+ }
92
+
93
+ /**
94
+ * The source control panel's top at layout 3, in place of the branch list: the square, its line, and
95
+ * the two things a person does with it, Commit (the square's live work, with a message) and Merge onto
96
+ * main (Seam merges once its checks pass; the folder carries on in a new square). `busy`: 'commit' or
97
+ * 'merge' while that one runs. A viewer, or a folder not following its square yet, gets the line only.
98
+ */
99
+ export function squarePanelHtml (st, { busy = '' } = {}) {
100
+ const s = st.square
101
+ const name = squareName(st)
102
+ const following = s && !s.opening && !s.problem
103
+ const viewer = !!(st.access && st.access.role === 'viewer')
104
+ const head = `<div class="br-here sq-here"><div class="br-title">${name ? `Square <b>${esc(name)}</b> of ${esc(quiltName(st))}` : `Code committed and merged in Seam: <b>${esc(quiltName(st))}</b>`}</div>`
105
+ // While the session moves, nothing is committed or merged from here: the move is what it waits for.
106
+ if (st.moving) return `${head}<div class="br-sub warn">${esc(movingCard(st).text)}</div></div>`
107
+ if (st.squareless) {
108
+ const w = squarelessWords(st)
109
+ return `${head}<div class="br-sub warn">${esc(w.text)}</div>${viewer ? '' : `<div class="sq-acts">${openSquareButton(st, busy)}</div>`}</div>`
110
+ }
111
+ if (!following) {
112
+ 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
+ return `${head}<div class="br-sub${st.readOnly || (s && s.problem) ? ' warn' : ''}">${esc(why)}</div></div>`
114
+ }
115
+ const sub = '<div class="br-sub">Every edit here is live for everyone on this square. Commit it to keep a version; merge it to put it on main (GitHub gets main from Seam by itself).</div>'
116
+ const caught = catchUpWords(s)
117
+ const work = (caught.length ? `<div class="br-sub${s.conflicted ? ' warn' : ''}">${esc(caught.join(' '))}</div>` : '') + workHtml(s.work)
118
+ if (viewer) return `${head}${sub}${work}</div>`
119
+ return `${head}${sub}${work}
120
+ <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 ${esc(name)} onto main once its checks pass">${busy === 'merge' ? 'Merging…' : 'Merge onto main'}</button></div>
122
+ </div>`
123
+ }
124
+
125
+ /** While the owner moves the session (status `moving`, the relay's): { title, text }, what to expect in plain words. */
126
+ export function movingCard (st) {
127
+ const m = st.moving || {}
128
+ return { title: m.kind === 'back' ? 'Moving the code back to git' : 'Moving the code into Seam', text: m.line || (m.kind === 'back' ? 'The session\'s code is moving back from Seam to its git branches. Keep Quilt open: it can take a few minutes, and your edits are kept.' : 'The session\'s code is moving into Seam, where it will be committed and merged; the session stays in Quilt. Keep Quilt open: it can take a few minutes, and your edits are kept.') }
129
+ }
130
+
131
+ /**
132
+ * A folder whose branch has no square in the quilt (made after the move into Seam, or main with nothing
133
+ * to adopt): its edits reach nobody until a square is opened for it. { title, text } in plain words.
134
+ */
135
+ export function squarelessWords (st) {
136
+ const b = (st.squareless && st.squareless.branch) || 'this branch'
137
+ return { title: `${b} has no square yet`, text: `Your folder is on ${b}, which has no square in Seam, so your edits here reach nobody. Open a square for it: your files stay as they are, they become its work, and everyone on ${b} follows it.` }
138
+ }
139
+
140
+ /** The button that opens a square for the folder's branch (squareTool open_square). */
141
+ export function openSquareButton (st, busy = '') {
142
+ const b = (st.squareless && st.squareless.branch) || 'this branch'
143
+ return `<button type="button" class="btn sm primary" data-square-open${busy ? ' disabled' : ''}>${busy === 'open' ? 'Opening…' : `Open a square for ${esc(b)}`}</button>`
144
+ }
145
+
146
+ /** What the square holds not committed yet (everyone's on it), folded: what Commit takes. */
147
+ function workHtml (work) {
148
+ if (!work) return ''
149
+ if (!work.count) return '<div class="br-sub">Nothing here is waiting to be committed.</div>'
150
+ const word = { added: 'new', deleted: 'deleted', modified: '' }
151
+ const more = work.count - work.files.length
152
+ return `<details class="sq-work"><summary>${files(work.count)} not committed yet</summary><ul class="commit-files">${work.files.map((f) => `<li><code title="${esc(f.path)}">${esc(f.path)}</code>${word[f.status] ? ` <span class="hint">${word[f.status]}</span>` : ''}</li>`).join('')}${more > 0 ? `<li class="hint">and ${more} more</li>` : ''}</ul></details>`
153
+ }
154
+
155
+ /**
156
+ * The words on the owner's "Make this a quilt" card (16 flag 9), while the session's code is on git.
157
+ * `quilt`: the status's quilt link ({ slug, shadow }), `remote`: the session has a git remote.
158
+ */
159
+ export function makeQuiltWords ({ quilt = null, remote = false, moving = false, force = null, importing = null, shadow = null } = {}) {
160
+ const w = makeQuiltWordsNow({ quilt, remote, moving, importing, shadow })
161
+ // The forced date (16 flag 9): live sync stopped here, or when it comes (force-date.js).
162
+ if (force && force.stopped) return { ...w, note: force.stopped.why || '' }
163
+ const ahead = force && force.at ? forceAheadWords({ at: Date.parse(force.at), remote: !!force.remote }) : null
164
+ return ahead ? { ...w, note: ahead } : w
165
+ }
166
+
167
+ function makeQuiltWordsNow ({ quilt, remote, moving, importing, shadow }) {
168
+ if (moving) return { text: 'The session\'s code is moving into Seam now. Keep Quilt open: your edits are kept.', button: 'Moving into Seam…', disabled: true }
169
+ // A session with a git remote, step by step (quilt-remote.js): Seam importing it, then linked and ready for the cut-over.
170
+ if (shadow || (quilt && quilt.shadow)) return { text: `Seam follows this session's git remote in the quilt ${(shadow || quilt).slug || ''}. When the team is ready: the repository's GitHub owner lets only Seam push to main (the steps show when you click), then move the session in. Its branches become squares, its uncommitted work a square of its own, and GitHub keeps getting main from Seam.`, button: 'Move into Seam now' }
171
+ if (importing) return { text: `Seam is importing ${importing.repo} into the quilt ${importing.slug} and follows it. Git stays as it is meanwhile: remote, clones and CI. Check again once it has caught up.`, button: 'Check the import' }
172
+ if (quilt && !quilt.shadow) return { text: `This session is the quilt ${quilt.slug} already, but its files are still on its branches. Finish the move to put them in its squares.`, button: 'Finish the move' }
173
+ if (remote) return { text: 'This session has a git remote, so it becomes a quilt with its git history: Seam imports the remote and follows it, then the session moves in when the import is ready. Your git remote, clones and CI keep working.', button: 'Make this a quilt' }
174
+ return { text: 'Move this session\'s code into Seam: main as it is now becomes the quilt\'s first commit, each branch a square, and everyone\'s folder follows its square live. Nothing is lost: the session can go back to its branches.', button: 'Make this a quilt' }
175
+ }
176
+
177
+ /**
178
+ * Session settings' Source control at layout 3, for everyone in it: where the code's history is, in
179
+ * place of the git and GitHub settings, which are put away while the session is in Seam (the relay
180
+ * keeps them for a move back: quilt session migrate --back).
181
+ */
182
+ export function seamSettingsHtml (st) {
183
+ return `<div class="pm-section"><div class="pm-title">Seam</div><div class="pm-card git-card">
184
+ <div class="git-head"><h4>This session's source control</h4><span class="pill">Seam</span></div>
185
+ <p class="git-line ok"><b>${esc(quiltName(st))}.</b> This session's code is committed and merged in Seam; the session itself is Quilt. Everyone's folder follows its branch's square: people and their AIs commit there and merge onto main, and GitHub gets main from Seam by itself.</p>
186
+ <span class="hint">Git and GitHub settings are put away while the session's code is in Seam. If its owner moves it back to its branches (quilt session migrate --back), they come back as they were.</span>
187
+ </div></div>`
188
+ }
189
+
190
+ /**
191
+ * The left sidebar in Seam, in place of boxes (there are none: every edit is live in its square): each
192
+ * square with whose folders are on it, and, for yours, how many files it holds not committed yet.
193
+ * `st.squares` is branch -> square name; people are you (`st.square`) and `st.peers` by their branch.
194
+ */
195
+ export function squaresSideHtml (st) {
196
+ const squares = st.squares || {}
197
+ const branchOf = Object.fromEntries(Object.entries(squares).map(([b, sq]) => [sq, b]))
198
+ const groups = new Map()
199
+ const add = (sq, who) => { if (!sq) return; if (!groups.has(sq)) groups.set(sq, []); if (!groups.get(sq).includes(who)) groups.get(sq).push(who) }
200
+ const mine = squareName(st)
201
+ add(mine, 'you')
202
+ 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
+ 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><b>${esc(sq)}</b>${branchOf[sq] ? ` <span class="hint">${esc(branchOf[sq])}</span>` : ''}</div><div class="hint">${esc(who.join(', '))}${sq === mine ? ` · ${n ? `${files(n)} not committed` : 'all committed'}` : ''}</div></li>`).join('')
205
+ // The pane's own title says Squares already (session.js, as it says Boxes outside Seam): no second heading.
206
+ return rows ? `<ul class="sq-side">${rows}</ul>` : ''
207
+ }
208
+
209
+ /** The sidebar's line under it in Seam. */
210
+ export const SEAM_SIDE_HINT = 'In Seam nothing waits in boxes: everyone on a square sees every edit as it is made. Commit and merge your square from the square button at the top.'
211
+
212
+ /**
213
+ * What the square panel's Commit or Merge onto main did, for a person: from the Seam tool's own record
214
+ * (`structuredContent`), never its tool text (ids, `held by person:…`, CLI hints). `text` is the tool's
215
+ * text, read only for what has no record: nothing to commit, the folder carrying on in a new square.
216
+ */
217
+ export function seamResultWords (tool, { structuredContent: rec = null, text = '' } = {}) {
218
+ const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`
219
+ const names = (list) => list.length > 4 ? `${list.slice(0, 4).join(', ')} and ${list.length - 4} more` : list.join(', ')
220
+ const careful = (rec && rec.warnings || []).map((w) => w.kind === 'markers_in_file' ? `${w.path} still has conflict markers in it.` : `${w.path} looks like it holds a secret: check it before you merge.`)
221
+ if (tool === 'seam_commit') {
222
+ if (/^Nothing to commit/.test(text)) return 'Nothing to commit: everything in this square is committed already.'
223
+ const c = rec && rec.result
224
+ if (!c) return text
225
+ const n = Array.isArray(c.changes) ? c.changes.length : 0
226
+ return [`Committed "${String(c.message || '').split('\n')[0]}"${n ? `: ${plural(n, 'file')}` : ''}.`, ...careful].join(' ')
227
+ }
228
+ if (tool === 'seam_merge') {
229
+ if (!rec) return text
230
+ const moved = /carries on in a new square/.test(text) ? 'Your folder carries on in a new square on main\'s new version: keep working as before.' : ''
231
+ if (rec.ok && rec.merged) {
232
+ const m = rec.merged
233
+ const combined = (rec.combined || []).map((c) => c.path)
234
+ return [`Merged onto ${m.line || 'main'} as patch ${m.patch_no}${m.subject ? ` "${m.subject}"` : ''}.`, combined.length ? `Combined with changes already on ${m.line || 'main'} in ${names(combined)}.` : '', ...careful, moved].filter(Boolean).join(' ')
235
+ }
236
+ const terms = (rec.terms || []).map((t) => t.path)
237
+ const why = rec.refused ? rec.refused.detail : rec.error || 'Seam refused'
238
+ return [`Not merged: ${why}.`, terms.length ? `${plural(terms.length, 'file')} ${terms.length === 1 ? 'conflicts' : 'conflict'} with main and ${terms.length === 1 ? 'is' : 'are'} marked in your folder (${names(terms)}): settle ${terms.length === 1 ? 'it' : 'them'}, commit, and merge again.` : ''].filter(Boolean).join(' ')
239
+ }
240
+ return text
241
+ }
242
+
243
+ /**
244
+ * The words for a session that stopped by itself (the relay sent this app away, its owner ended it, ...):
245
+ * `session` the app's record of it (for its name), `why` what Quilt was told.
246
+ */
247
+ export function stoppedWords (session, why) {
248
+ const st = session && session.status
249
+ const name = (st && st.sessionName) || (session && session.dir ? String(session.dir).split(/[\\/]/).filter(Boolean).pop() : '') || 'The session'
250
+ return `${name} stopped: ${String(why || '').replace(/^Relay refused connection: /, '')}`
251
+ }