@leftoff/cli 0.9.18-beta.9 → 0.9.18
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 +25 -2
- package/bin/leftoff-statusline.js +7 -7
- package/bin/leftoff.js +792 -677
- package/docs/SECURITY.md +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/core.js +259 -17
- package/mod/hooks/register.js +18 -4
- package/package.json +1 -1
- package/ui/app.css +204 -5
- package/ui/app.js +621 -189
- package/ui/chip-fade.js +1 -1
- package/ui/cp-fold.js +42 -0
- package/ui/decisions.js +81 -21
- package/ui/digest-versions.js +57 -0
- package/ui/fs-side.js +29 -0
- package/ui/html-preview.html +12 -0
- package/ui/html-preview.js +136 -0
- package/ui/icons.js +1 -0
- package/ui/live-status.js +44 -3
- package/ui/live-view.js +36 -5
- package/ui/live-wake.js +47 -0
- package/ui/mcp-setup.js +26 -0
- package/ui/prs.js +28 -10
- package/ui/pull.js +30 -3
- package/ui/relay-client.js +2 -0
- package/ui/render-digest.js +98 -0
- package/ui/render-export.js +128 -0
- package/ui/semdiff.js +58 -27
- package/ui/session-title.js +88 -0
- package/ui/settings-nav.js +1 -1
- package/ui/sglyph.js +2 -2
- package/ui/slash-commands.js +89 -0
- package/ui/slash.js +82 -0
- package/ui/subagents-view.js +41 -0
- package/ui/summary-examples.js +1 -1
- package/ui/tabs.js +10 -3
- package/ui/viewport-fit.js +29 -3
- package/ui/walk-spots.js +35 -1
package/ui/viewport-fit.js
CHANGED
|
@@ -28,6 +28,13 @@ export const KEYBOARD_MIN_PX = 120 // less than this is the browser's own bars,
|
|
|
28
28
|
// (a keyboard swap reports a passing full-height or tiny keyboard on the way; following each report
|
|
29
29
|
// means the next one puts it right). Nothing is remembered from when the box took focus. `kb` (the
|
|
30
30
|
// keyboard's own look: no home-indicator gap) still needs a keyboard's worth of height.
|
|
31
|
+
// 2026-11-07 (the maintainer's screen recording, iPhone, stock and four third-party keyboards): the
|
|
32
|
+
// 64px kept clear for iOS 26's ^ v ✓ bar on 11-06 was pure gap. In the recording the reply box's
|
|
33
|
+
// focus ring ends ~129 video px (≈76pt) above the bar's top edge on every keyboard: 64 kept + the
|
|
34
|
+
// box's 8px bottom padding + the bar's drawn edge. So the bar sits outside the visual viewport iOS
|
|
35
|
+
// reports, as before iOS 26, and full screen is the reported viewport exactly, with nothing kept.
|
|
36
|
+
// The 11-06 clipping was the fit measuring before iOS's last resize (switching keyboards fixed it:
|
|
37
|
+
// a fresh report); the board now looks again on every report and at timed beats after focus.
|
|
31
38
|
export function fullViewportFit({ innerHeight, vv, typing = false } = {}) {
|
|
32
39
|
if (!vv || !(Number(innerHeight) > 0)) return null
|
|
33
40
|
const ih = Number(innerHeight)
|
|
@@ -54,6 +61,25 @@ export function outOfView(rect, vv, slack = 1) {
|
|
|
54
61
|
// When to look again after a text box lets go (the keyboard takes ~0.3–0.5 s to
|
|
55
62
|
// close, and iOS's reports during it can be stale or missing), in ms.
|
|
56
63
|
export const SETTLE_AFTER_BLUR_MS = [0, 120, 350, 700, 1100]
|
|
57
|
-
// The
|
|
58
|
-
//
|
|
59
|
-
|
|
64
|
+
// The beats after a text box takes focus and after every report while it has focus: a keyboard's
|
|
65
|
+
// last resize (a third-party one coming up late, a swap, the ^ v ✓ bar) can come with no event of
|
|
66
|
+
// its own, so the board looks again at each of these, sizing full screen to that moment's numbers.
|
|
67
|
+
export const SETTLE_WHILE_TYPING_MS = [0, 100, 300, 600, 1000]
|
|
68
|
+
// And every frame for this long after the box takes focus and after each report: iOS animates the
|
|
69
|
+
// keyboard (and pans) for ~0.3–0.5 s, part of it with no event, and its numbers settle a frame or
|
|
70
|
+
// two after the event that announced them (WebKit bug 237851: offsetTop read 0 mid-animation).
|
|
71
|
+
export const FOLLOW_FRAMES_MS = 1000
|
|
72
|
+
|
|
73
|
+
// 2026-11-06 (the phone's session sheet covered the reply box while typing; "sometimes I have to
|
|
74
|
+
// scroll that down, and then the message box appears"): the sheet went away only when the board
|
|
75
|
+
// measured a keyboard's worth of lost height (lf-kb), and some keyboards (a third-party one coming up
|
|
76
|
+
// late or in steps, or iOS shrinking the window along with the keyboard) never report that. Now the
|
|
77
|
+
// sheet steps aside whenever a text box outside the sheet has focus, measured or not, and comes back
|
|
78
|
+
// a beat after it lets go (so the tap that closed the keyboard lands where it was aimed, not on a
|
|
79
|
+
// sheet sliding in under the finger). A text box inside the sheet keeps it.
|
|
80
|
+
export const SHEET_BACK_AFTER_BLUR_MS = 400
|
|
81
|
+
// typing: a text box has focus; inSheet: that box is in the sheet; blurredMs: ms since a text box outside the sheet let go (null: none lately)
|
|
82
|
+
export function sheetAside({ typing = false, inSheet = false, blurredMs = null } = {}) {
|
|
83
|
+
if (typing) return !inSheet
|
|
84
|
+
return blurredMs != null && blurredMs >= 0 && blurredMs < SHEET_BACK_AFTER_BLUR_MS
|
|
85
|
+
}
|
package/ui/walk-spots.js
CHANGED
|
@@ -58,7 +58,7 @@ export function spotsFor(data, order) {
|
|
|
58
58
|
snips.forEach((sn, i) => {
|
|
59
59
|
const here = []
|
|
60
60
|
const lineIn = (side, line) => parsePatch(sn.patch).rows.some((r) => isLine(r) && (side === 'old' ? r.o : r.n) === line)
|
|
61
|
-
for (const c of st.callouts || []) if (c.hunk === sn.hunk && lineIn(c.side, c.line)) here.push({ side: c.side, line: c.line, why: SPOT_WORDS[c.kind] || SPOT_WORDS.look, kind: 'callout' })
|
|
61
|
+
for (const c of st.callouts || []) if (c.hunk === sn.hunk && lineIn(c.side, c.line)) here.push({ side: c.side, line: c.line, why: SPOT_WORDS[c.kind] || SPOT_WORDS.look, kind: 'callout', note: c.text || '' })
|
|
62
62
|
for (const m of model) if (m.step === st.id && m.hunk === sn.hunk && lineIn(m.side, m.line) && !here.some((x) => x.side === m.side && x.line === m.line)) here.push({ side: m.side, line: m.line, why: m.why || 'Worth a look', kind: 'model' })
|
|
63
63
|
if (!here.length && !model.length) {
|
|
64
64
|
const r = snippetRisk(sn.patch, sn.file)
|
|
@@ -76,6 +76,40 @@ export function spotsFor(data, order) {
|
|
|
76
76
|
return main.length ? main : sup
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// 2026-11-06 (Joel: "if used midway through will just start from the first, not the next
|
|
80
|
+
// from where you are"): where Next spot goes from where you're reading. rank: a step id ->
|
|
81
|
+
// its place in reading order; at: the place you're on (-1 the overview, Infinity past the
|
|
82
|
+
// last step). Carrying on from the spot you were last at when it's on this step; else the
|
|
83
|
+
// first spot on this step or after it (dir 1), or the last one before it (dir -1), wrapping.
|
|
84
|
+
export function spotFrom(spots, rank, at, cur, dir = 1) {
|
|
85
|
+
const n = spots.length
|
|
86
|
+
if (!n) return -1
|
|
87
|
+
const r = (sp) => { const v = rank(sp.step); return v == null || v < 0 ? Infinity : v }
|
|
88
|
+
if (cur != null && cur >= 0 && cur < n && r(spots[cur]) === at) return nextSpotIndex(n, cur, dir)
|
|
89
|
+
if (dir > 0) { const i = spots.findIndex((sp) => r(sp) >= at); return i < 0 ? 0 : i }
|
|
90
|
+
for (let i = n - 1; i >= 0; i--) if (r(spots[i]) < at) return i
|
|
91
|
+
return n - 1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2026-11-06 (Joel: "it needs a tooltip / popover explaining why it's a spot"): a spot's chip
|
|
95
|
+
// explains itself: who picked it and what to check. Plain text.
|
|
96
|
+
const RISK_WHY = {
|
|
97
|
+
'Removed logic': 'Several lines of code go away with little added in their place. Check nothing still relies on them.',
|
|
98
|
+
'Auth / permissions': 'A changed line touches auth or permissions (a token, a role, a session, a check). Check who can do what now.',
|
|
99
|
+
'Error handling': 'A changed line handles errors. Check what happens now when this fails.',
|
|
100
|
+
Config: 'A config file. A change here affects every run, not one code path.',
|
|
101
|
+
Migration: 'A migration. Check it runs once, on real data, and what undoing it takes.',
|
|
102
|
+
}
|
|
103
|
+
export function spotTip(sp) {
|
|
104
|
+
if (!sp) return ''
|
|
105
|
+
const why = String(sp.why || '')
|
|
106
|
+
if (sp.kind === 'callout') return 'The walkthrough flags this line (' + why + ')' + (sp.note ? ': ' + sp.note : '.')
|
|
107
|
+
if (sp.kind === 'model') return 'The model that wrote this walkthrough picked this line as worth a real look: ' + (why || 'no reason given') + '.'
|
|
108
|
+
if (sp.kind === 'risk') return why + ', found by Leftoff\'s own check (no model). ' + (RISK_WHY[why] || '')
|
|
109
|
+
if (sp.kind === 'core') return 'The first change in a Core step: the heart of what this change does.'
|
|
110
|
+
return 'Nothing riskier was found, so this is the next Supporting change to read.'
|
|
111
|
+
}
|
|
112
|
+
|
|
79
113
|
// The spot after (dir 1) or before (-1) index cur, wrapping; -1 when there are none.
|
|
80
114
|
export function nextSpotIndex(n, cur, dir = 1) {
|
|
81
115
|
if (!n) return -1
|