@leftoff/cli 0.9.17-beta.9 → 0.9.17

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/ui/diff-view.js CHANGED
@@ -238,19 +238,80 @@ export function renderDiff(doc, text, { label = '', numbers = true } = {}) {
238
238
  const mk = el('span', 'gd-m', MARK[r.k])
239
239
  mk.setAttribute('aria-label', r.k === 'add' ? 'added' : r.k === 'del' ? 'removed' : 'unchanged')
240
240
  row.appendChild(mk)
241
+ row.appendChild(lineCell(doc, el, r))
242
+ }
243
+ root.appendChild(row)
244
+ }
245
+ return root
246
+ }
247
+ // a line's text, the changed words marked (textContent only)
248
+ function lineCell(doc, el, r) {
249
+ const c = el('span', 'gd-c')
250
+ if (r.w && r.w.length) {
251
+ let at = 0
252
+ for (const [s, e] of r.w) {
253
+ if (s > at) c.appendChild(doc.createTextNode(r.t.slice(at, s)))
254
+ c.appendChild(el(r.k === 'add' ? 'ins' : 'del', 'gd-x', r.t.slice(s, e)))
255
+ at = e
256
+ }
257
+ if (at < r.t.length) c.appendChild(doc.createTextNode(r.t.slice(at)))
258
+ } else if (r.t) c.textContent = r.t
259
+ else c.textContent = '\u200b' // keeps an empty line's height
260
+ return c
261
+ }
262
+
263
+ // 2026-10-30 (walkthrough round 3) the same patch side by side: old on the left,
264
+ // new on the right, a run of removed lines beside the added lines that replaced
265
+ // it (paired in order), unchanged lines on both sides, the @@ bars across. -> the
266
+ // split rows: [{ k: 'line', l: rowIndex | null, r: rowIndex | null } | { k: 'span', i }],
267
+ // indexes into parsePatch's rows (so a picked line means the same in both views).
268
+ export function splitRows(rows) {
269
+ const out = []
270
+ let dels = [], adds = []
271
+ const flush = () => { const n = Math.max(dels.length, adds.length); for (let i = 0; i < n; i++) out.push({ k: 'line', l: i < dels.length ? dels[i] : null, r: i < adds.length ? adds[i] : null }); dels = []; adds = [] }
272
+ rows.forEach((r, i) => {
273
+ if (r.k === 'del') { if (adds.length) flush(); dels.push(i) } else if (r.k === 'add') adds.push(i)
274
+ else { flush(); out.push(r.k === 'ctx' ? { k: 'line', l: i, r: i } : { k: 'span', i }) }
275
+ })
276
+ flush()
277
+ return out
278
+ }
279
+ export function renderSplit(doc, text, { label = '' } = {}) {
280
+ const { rows, maxLine } = parsePatch(text)
281
+ const el = (tag, cls, txt) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (txt != null && txt !== '') e.textContent = txt; return e }
282
+ const root = el('div', 'gd gd-split gd-w' + Math.min(6, Math.max(2, String(maxLine || 1).length)))
283
+ if (label) root.setAttribute('aria-label', label)
284
+ const half = (i, side) => {
285
+ if (i == null) { const h = el('div', 'gd-half gd-h' + side + ' gd-empty'); h.setAttribute('aria-hidden', 'true'); return h }
286
+ const r = rows[i]
287
+ const k = r.k === 'ctx' ? 'ctx' : r.k
288
+ const h = el('div', 'gd-half gd-h' + side + ' gd-' + k)
289
+ h.dataset.ri = String(i)
290
+ const num = side === 'l' ? r.o : r.n
291
+ h.appendChild(el('span', 'gd-gut ' + (side === 'l' ? 'gd-o' : 'gd-n'), num == null ? '' : String(num)))
292
+ const mk = el('span', 'gd-m', MARK[k])
293
+ mk.setAttribute('aria-label', k === 'add' ? 'added' : k === 'del' ? 'removed' : 'unchanged')
294
+ h.appendChild(mk)
295
+ h.appendChild(lineCell(doc, el, r))
296
+ return h
297
+ }
298
+ for (const s of splitRows(rows)) {
299
+ if (s.k === 'span') {
300
+ const r = rows[s.i]
301
+ const row = el('div', 'gd-row gd-' + r.k)
302
+ row.dataset.ri = String(s.i)
303
+ row.appendChild(el('span', 'gd-gut gd-span', r.k === 'gap' ? '⋯' : ''))
241
304
  const c = el('span', 'gd-c')
242
- if (r.w && r.w.length) {
243
- let at = 0
244
- for (const [s, e] of r.w) {
245
- if (s > at) c.appendChild(doc.createTextNode(r.t.slice(at, s)))
246
- c.appendChild(el(r.k === 'add' ? 'ins' : 'del', 'gd-x', r.t.slice(s, e)))
247
- at = e
248
- }
249
- if (at < r.t.length) c.appendChild(doc.createTextNode(r.t.slice(at)))
250
- } else if (r.t) c.textContent = r.t
251
- else c.textContent = '\u200b' // keeps an empty line's height
305
+ if (r.k === 'gap') c.textContent = r.n + ' unchanged line' + (r.n === 1 ? '' : 's')
306
+ else if (r.k === 'hunk') { c.appendChild(el('span', 'gd-range', r.range)); if (r.ctx) { c.appendChild(doc.createTextNode(' ')); c.appendChild(el('span', 'gd-ctx', r.ctx)) } }
307
+ else c.textContent = r.t
252
308
  row.appendChild(c)
309
+ root.appendChild(row)
310
+ continue
253
311
  }
312
+ const row = el('div', 'gd-srow')
313
+ row.appendChild(half(s.l, 'l'))
314
+ row.appendChild(half(s.r, 'r'))
254
315
  root.appendChild(row)
255
316
  }
256
317
  return root
package/ui/icons.js CHANGED
@@ -3,6 +3,10 @@
3
3
  // so they follow light and dark; always aria-hidden next to a text label.
4
4
  // index.html carries the nav's copies inline (no script needed to draw them).
5
5
  export const ICONS = {
6
+ "crosshair": "<circle cx=\"12\" cy=\"12\" r=\"10\"/> <path d=\"M22 12h-4M6 12H2M12 6V2M12 22v-4\"/>",
7
+ "columns-2": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <path d=\"M12 3v18\"/>",
8
+ "chevrons-down-up": "<path d=\"m7 20 5-5 5 5\"/> <path d=\"m7 4 5 5 5-5\"/>",
9
+ "chevrons-up-down": "<path d=\"m7 15 5 5 5-5\"/> <path d=\"m7 9 5-5 5 5\"/>",
6
10
  "panel-left-close": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <path d=\"M9 3v18\"/> <path d=\"m16 15-3-3 3-3\"/>",
7
11
  "panel-left-open": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <path d=\"M9 3v18\"/> <path d=\"m14 9 3 3-3 3\"/>",
8
12
  "menu": "<path d=\"M4 5h16\"/> <path d=\"M4 12h16\"/> <path d=\"M4 19h16\"/>",
package/ui/recap-line.js CHANGED
@@ -3,18 +3,21 @@
3
3
  // says where the session is (the daemon drops it once you prompt again).
4
4
  // Before /clear: what this tab was doing before a /clear, with a link to it.
5
5
  // Pure: the board passes its own esc / ago / clock, so this runs under node too.
6
+ // 0.9.19: the words are Claude's, so `code` and **bold** render (escaped first, no links).
7
+ import { mdInline } from './changelog-md.js'
8
+ const md = (t) => mdInline(t, { links: false })
6
9
 
7
10
  export function recapLineHtml(s, { esc, ago = () => '', clock = () => '' }) {
8
11
  const r = s && s.recap
9
12
  if (!r || !r.text) return ''
10
13
  const tip = 'Claude Code’s recap' + (r.at ? ' · ' + ago(r.at) + (clock(r.at) ? ' (' + clock(r.at) + ')' : '') : '')
11
- return '<p class="recap-line" data-tip="' + esc(tip) + '"><span class="rl-k">Recap</span> ' + esc(r.text) + '</p>'
14
+ return '<p class="recap-line" data-tip="' + esc(tip) + '"><span class="rl-k">Recap</span> ' + md(r.text) + '</p>'
12
15
  }
13
16
 
14
17
  export function beforeClearHtml(s, { esc }) {
15
18
  const b = s && s.beforeClear
16
19
  if (!b || !b.goal) return ''
17
20
  const href = b.sessionId ? '#/s/' + encodeURIComponent(b.sessionId) : null
18
- const words = esc(b.goal) + (b.stoppedAt ? ' <span class="bc-stop">· stopped at: ' + esc(b.stoppedAt) + '</span>' : '')
21
+ const words = md(b.goal) + (b.stoppedAt ? ' <span class="bc-stop">· stopped at: ' + md(b.stoppedAt) + '</span>' : '')
19
22
  return '<p class="before-clear"><span class="rl-k">Before /clear</span> ' + (href ? '<a href="' + esc(href) + '" data-tip="The session before /clear">' + words + '</a>' : words) + '</p>'
20
23
  }