@leftoff/cli 0.9.18-beta.4 → 0.9.18-beta.6
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/CHANGELOG.md +7 -1
- package/bin/leftoff.js +601 -557
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/core.js +163 -7
- package/mod/hooks/register.js +144 -6
- package/mod/skills/walkthrough/SKILL.md +5 -3
- package/package.json +1 -1
- package/ui/app.css +96 -0
- package/ui/app.js +154 -2
- package/ui/ask.js +4 -2
- package/ui/decisions.js +133 -0
- package/ui/hook-fix.js +109 -0
- package/ui/semdiff.js +23 -4
package/ui/semdiff.js
CHANGED
|
@@ -216,7 +216,20 @@ export function alsoIn(st, g, shared) {
|
|
|
216
216
|
}
|
|
217
217
|
export const CHANGED_TIP = 'Its files changed since this walkthrough was written'
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
// 2026-11-03 decision memory: the accepted decisions a step cites, as small chips that open the decision with its chain
|
|
220
|
+
export function decisionsOf(data) { return new Map(((data && data.decisions) || []).map((d) => [d.id, d])) }
|
|
221
|
+
export function stepDecisionsHtml(st, data, { esc }) {
|
|
222
|
+
const ids = Array.isArray(st.decisions) ? st.decisions : []
|
|
223
|
+
if (!ids.length) return ''
|
|
224
|
+
const by = decisionsOf(data)
|
|
225
|
+
const gone = new Set((data && data.stale && data.stale.decisions) || [])
|
|
226
|
+
return '<div class="wt-decs" role="group" aria-label="Decisions behind this step">' + ids.map((id) => {
|
|
227
|
+
const d = by.get(id)
|
|
228
|
+
return '<button type="button" class="dec-chip wt-dec' + (gone.has(id) ? ' wt-dec-gone' : '') + '" data-action="wt-dec" data-id="' + esc(id) + '"' + (data && data.decisionRepo ? ' data-repo="' + esc(data.decisionRepo) + '"' : '') +
|
|
229
|
+
' data-tip="' + esc((gone.has(id) ? 'Changed since this walkthrough · ' : '') + (d ? d.text : 'Decision ' + id)) + '"><span class="wt-dec-id">' + esc(id) + '</span>' + (d ? '<span class="wt-dec-t">' + esc(d.text) + '</span>' : '') + (gone.has(id) ? '<span class="wt-dec-w"> · changed since</span>' : '') + '</button>'
|
|
230
|
+
}).join('') + '</div>'
|
|
231
|
+
}
|
|
232
|
+
function stepHtml(st, { esc, icon, done, cur, open, sid, sf = {}, skip = null, stat = null, shared = null, foldShared = true, data = null }) {
|
|
220
233
|
const core = st.tag === 'Core'
|
|
221
234
|
// 2026-10-29 several repos: the step's repo leads ("cube_cloud · src/x.ts"); one repo: the file names
|
|
222
235
|
const rp = st.repo ? [st.repo] : []
|
|
@@ -246,7 +259,7 @@ function stepHtml(st, { esc, icon, done, cur, open, sid, sf = {}, skip = null, s
|
|
|
246
259
|
: '<button type="button" class="wt-showc" data-action="wt-code" data-step="' + esc(st.id) + '" aria-expanded="false">' + icon('chevron-right') + 'Show code <span class="faint">· ' + plural(lines, 'line') + '</span></button>'
|
|
247
260
|
// callouts whose line isn't in a drawn snippet (folded code) still read here
|
|
248
261
|
const loose = !show && st.callouts.length ? '<div class="wt-cos">' + st.callouts.map((c) => calloutHtml(c, { esc, icon })).join('') + '</div>' : ''
|
|
249
|
-
return '<article class="wt-st ' + (core ? 'wt-core' : 'wt-sup') + (done ? ' done' : '') + (cur ? ' cur' : '') + '" id="wt-' + esc(st.id) + '" data-step="' + esc(st.id) + '" tabindex="-1" aria-label="' + esc('Step ' + st.n + ': ' + mdPlain(st.title)) + '">' + head + why + loose + code + '</article>'
|
|
262
|
+
return '<article class="wt-st ' + (core ? 'wt-core' : 'wt-sup') + (done ? ' done' : '') + (cur ? ' cur' : '') + '" id="wt-' + esc(st.id) + '" data-step="' + esc(st.id) + '" tabindex="-1" aria-label="' + esc('Step ' + st.n + ': ' + mdPlain(st.title)) + '">' + head + why + stepDecisionsHtml(st, data, { esc }) + loose + code + '</article>'
|
|
250
263
|
}
|
|
251
264
|
|
|
252
265
|
function groupHtml(g, { esc, icon, open, data = null }) {
|
|
@@ -389,6 +402,8 @@ function modeSegHtml(show, split) {
|
|
|
389
402
|
// "3 files changed since this walkthrough · Update"
|
|
390
403
|
export function staleWords(st) {
|
|
391
404
|
if (!st) return ''
|
|
405
|
+
// 2026-11-03 a decision it cites has changed (superseded, rejected or edited) since it was written
|
|
406
|
+
if (Array.isArray(st.decisions) && st.decisions.length && !st.count) return (st.decisions.length === 1 ? 'Decision ' + st.decisions[0] + ' has' : 'Decisions ' + st.decisions.join(', ') + ' have') + ' changed since this walkthrough'
|
|
392
407
|
if (st.count == null) return 'The change has moved on since this walkthrough'
|
|
393
408
|
if (!st.count) return 'The change has moved on since this walkthrough'
|
|
394
409
|
return st.count + (st.count === 1 ? ' file changed' : ' files changed') + ' since this walkthrough'
|
|
@@ -477,7 +492,7 @@ export function walkthroughHtml({ s, data, ui, reviewed = new Set(), esc, icon,
|
|
|
477
492
|
const core = steps.filter((x) => x.tag === 'Core' && vis.has(x.id)), sup = steps.filter((x) => x.tag !== 'Core' && vis.has(x.id))
|
|
478
493
|
const bd = data.stats.breakdown || {}
|
|
479
494
|
const shared = sharedSnips(data), skip = skipFiles(data), stat = statusMap(data)
|
|
480
|
-
const one = (st) => stepHtml(st, { esc, icon, done: reviewed.has(st.id), cur: ui.cur === st.id, open: ui.open && ui.open.has(st.id), sid, sf: ui.sf || {}, skip, stat, shared })
|
|
495
|
+
const one = (st) => stepHtml(st, { esc, icon, done: reviewed.has(st.id), cur: ui.cur === st.id, open: ui.open && ui.open.has(st.id), sid, sf: ui.sf || {}, skip, stat, shared, data })
|
|
481
496
|
const repos = walkRepos(data)
|
|
482
497
|
// 2026-10-29 several repos: one section per repo (its header with counts), its steps in reading order
|
|
483
498
|
if (repos.length) {
|
|
@@ -565,7 +580,7 @@ function deckHtml(data, ui, { esc, icon, reviewed, sid }) {
|
|
|
565
580
|
slides.forEach((s, i) => {
|
|
566
581
|
if (s.kind === 'step') {
|
|
567
582
|
const x = s.step
|
|
568
|
-
h += sec(i, stepHtml(x, { esc, icon, done: reviewed.has(x.id), cur: false, open: true, sid, sf: ui.sf || {}, skip: skipFiles(data), stat: statusMap(data), shared, foldShared: false /* a slide shows one step: the earlier one isn't on screen
|
|
583
|
+
h += sec(i, stepHtml(x, { esc, icon, done: reviewed.has(x.id), cur: false, open: true, sid, sf: ui.sf || {}, skip: skipFiles(data), stat: statusMap(data), shared, foldShared: false /* a slide shows one step: the earlier one isn't on screen */, data }), 'Step ' + x.n + ' of ' + steps.length + ': ' + mdPlain(x.title), ' data-step="' + esc(x.id) + '"')
|
|
569
584
|
} else if (s.kind === 'skip') {
|
|
570
585
|
const groups = data.groups || []
|
|
571
586
|
const n = groups.reduce((a, g) => a + g.files.length + (g.kind === 'repeated' ? 1 : 0), 0), l = groups.reduce((a, g) => a + g.lines, 0)
|
|
@@ -808,6 +823,7 @@ export function toMarkdown(data, { title = '' } = {}) {
|
|
|
808
823
|
if (repos.length && st.repo && st.repo !== lastRepo) { lastRepo = st.repo; const r = repos.find((x) => x.label === st.repo); L.push(''); L.push('### ' + st.repo + (r ? ' (' + repoHeadWords(r, data.steps || []) + ')' : '')) }
|
|
809
824
|
L.push(''); L.push((repos.length ? '#### ' : '### ') + st.n + '. ' + st.title + ' (' + st.tag + ')'); L.push('')
|
|
810
825
|
if (st.why) L.push(st.why)
|
|
826
|
+
if (Array.isArray(st.decisions) && st.decisions.length) { const by = decisionsOf(data); L.push(''); L.push('Decisions: ' + st.decisions.map((id) => '`' + id + '`' + (by.get(id) ? ' ' + by.get(id).text : '')).join('; ')) } // 2026-11-03
|
|
811
827
|
for (const c of st.callouts || []) { L.push(''); L.push('> **' + (CALLOUT[c.kind] || CALLOUT.look).word + '** (`' + pathLabel(c.file, data) + ':' + c.line + '`): ' + c.text) }
|
|
812
828
|
for (const sn of st.snippets || []) {
|
|
813
829
|
const fence = /```/.test(sn.patch) ? '~~~~' : '```'
|
|
@@ -820,6 +836,7 @@ export function toMarkdown(data, { title = '' } = {}) {
|
|
|
820
836
|
for (const g of groups) L.push('- ' + g.title + (g.note ? ' (' + g.note + ')' : '') + ': ' + plural(g.files.length + (g.kind === 'repeated' ? 1 : 0), 'file') + ', ' + plural(g.lines, 'line'))
|
|
821
837
|
}
|
|
822
838
|
if (data.coverage) { L.push(''); L.push('_Every changed line is in a step or a group: ' + n0(data.coverage.covered) + ' of ' + n0(data.coverage.total) + '._') }
|
|
839
|
+
if (Array.isArray(data.decisions) && data.decisions.length) { L.push(''); L.push('### Decisions cited'); L.push(''); for (const d of data.decisions) L.push('- `' + d.id + '` ' + d.text + (d.why ? ' (' + d.why + ')' : '')) }
|
|
823
840
|
return L.join('\n') + '\n'
|
|
824
841
|
}
|
|
825
842
|
|
|
@@ -865,6 +882,7 @@ export function toHtmlDoc(data, { title = '', repo = '', when = '' } = {}) {
|
|
|
865
882
|
const agHead = (s) => { if (!repos.length || !s.st || !s.st.repo || s.st.repo === agRepo) return ''; agRepo = s.st.repo; const r = repos.find((x) => x.label === s.st.repo); return '<li class="rh"><h3>' + escH(s.st.repo) + (r ? ' <span class="f">· ' + repoHeadWords(r, steps) + '</span>' : '') + '</h3></li>' }
|
|
866
883
|
o += '<h2>Reading order</h2><ol class="ag">' + slides.map((s, i) => i === 0 ? '' : agHead(s) + '<li><label for="s' + i + '">' + (s.st ? s.st.n + '. ' : '') + walkInline(s.t) + (s.st ? ' <span class="tag ' + (s.st.tag === 'Core' ? 'core' : '') + '">' + escH(s.st.tag) + '</span>' : '') + '</label></li>').join('') + '</ol>'
|
|
867
884
|
if (data.coverage) o += '<p class="f">Every changed line is in a step or a group: ' + n0(data.coverage.covered) + ' of ' + n0(data.coverage.total) + '.</p>'
|
|
885
|
+
if (Array.isArray(data.decisions) && data.decisions.length) o += '<h2>Decisions cited</h2><ul>' + data.decisions.map((d) => '<li><code>' + escH(d.id) + '</code> ' + escH(d.text) + (d.why ? ' <span class="f">(' + escH(d.why) + ')</span>' : '') + '</li>').join('') + '</ul>' // 2026-11-03
|
|
868
886
|
b += '<section class="sl" data-i="0">' + o + nav(0) + '</section>'
|
|
869
887
|
slides.forEach((s, i) => {
|
|
870
888
|
if (!i) return
|
|
@@ -873,6 +891,7 @@ export function toHtmlDoc(data, { title = '', repo = '', when = '' } = {}) {
|
|
|
873
891
|
const st = s.st
|
|
874
892
|
if (repos.length && st.repo) { const r = repos.find((y) => y.label === st.repo); x += '<h3 class="rp">' + escH(st.repo) + (r ? ' <span class="f">· ' + repoHeadWords(r, steps) + '</span>' : '') + '</h3>' } // 2026-10-29 the step's repo
|
|
875
893
|
x += '<h2>' + st.n + '. ' + walkInline(st.title) + '<span class="tag ' + (st.tag === 'Core' ? 'core' : '') + '">' + escH(st.tag) + '</span></h2>' + (st.why ? '<div class="why">' + walkBlock(st.why, { exported: true }) + '</div>' : '')
|
|
894
|
+
if (Array.isArray(st.decisions) && st.decisions.length) { const by = decisionsOf(data); x += '<p class="f decs">Decisions: ' + st.decisions.map((id) => '<code>' + escH(id) + '</code>' + (by.get(id) ? ' ' + escH(by.get(id).text) : '')).join('; ') + '</p>' } // 2026-11-03
|
|
876
895
|
const drawn = new Set()
|
|
877
896
|
for (const sn of st.snippets || []) {
|
|
878
897
|
const cs = (st.callouts || []).filter((c) => c.hunk === sn.hunk)
|