@leftoff/cli 0.9.18-beta.7 → 0.9.18-beta.8
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 +1 -0
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/register.js +1 -1
- package/package.json +1 -1
- package/ui/app.css +1 -0
- package/ui/app.js +41 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,7 @@ What changed in Leftoff (called Checkpoints until 0.4.5), newest first. The boar
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
- **Photos picked on a phone attach again.** On iPhone, choosing a photo from the paperclip's picker closed the picker and nothing showed: coming back from the picker counts as a wake-up, which redraws the page, and iOS hands over the photo only after that, to the old picker the redraw had thrown away. The picker now lives outside the page, so a redraw can't lose what you picked, and a pick that still can't attach says why. Full screen also measures again as a phone's keyboard comes up, for keyboards (Raycast, Gboard, SwiftKey) that report late. A board reload is enough.
|
|
7
8
|
- **Walkthroughs say whether a step follows or departs from a decision.** A decision chip on a step now reads ≠ Departs when the step goes against it, and the step always gets a Look closely note naming the decision; from there the decision viewer offers to link one of the session's own decisions as its replacement. A session's own decisions show in its walkthrough right away, marked ◌ Proposed, with Accept and Reject in the viewer. Needs `leftoff restart` and `leftoff upgrade --beta`.
|
|
8
9
|
- **Weak tests caught without the model.** Walk me through it flags a test change that removes assertions and adds none, adds a skip or .only, asserts a value against itself, or mocks the module the test is for, and gives it a Look closely note even if the model put it under Safe to skip. Needs `leftoff restart`.
|
|
9
10
|
- **Test plans from merged PRs.** A project's page, its decision log and the PRs page (pick merged PRs, then Test plan for these) make a short manual script for what merged lately: up to about 12 checks that fit 15, 30 or 60 minutes, each with where to go, the steps, what you should see and why it's on the list, ordered by risk. Risky callouts, weak tests and changed decisions always become checks or are listed under Not covered with the reason. Tick checks off, note what failed and propose a decision from it, then Copy as Markdown or Download .html. `leftoff testplan --project NAME` prints it. Needs `leftoff restart`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leftoff",
|
|
3
|
-
"version": "0.9.18-beta.
|
|
3
|
+
"version": "0.9.18-beta.8",
|
|
4
4
|
"description": "Leftoff (formerly Checkpoints): WTF was I working on? Tracks every Claude Code session on this machine, writes a 3-line checkpoint when one goes quiet, and adds /cp (checkpoint now), /cp-name and a /leftoff pane. Talks to the local Leftoff daemon on 127.0.0.1:7429 and falls back to writing markdown in ~/checkpoints.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "One Silly App"
|
package/mod/hooks/register.js
CHANGED
|
@@ -32,7 +32,7 @@ const START_SKIP_GRACE_MS = 15000 // the same for SessionStart (a cold start can
|
|
|
32
32
|
const PERM_GRACE_MS = 1500 // how long a permission prompt tool.check saw waits for classic.PermissionRequest before the mod reports it
|
|
33
33
|
|
|
34
34
|
// Module state. $.state is not used: the pane redraws with $.ui.invalidate.
|
|
35
|
-
const MOD_VERSION = '0.9.18-beta.
|
|
35
|
+
const MOD_VERSION = '0.9.18-beta.8' // keep in step with .claude-plugin/plugin.json
|
|
36
36
|
|
|
37
37
|
const st = {
|
|
38
38
|
internal: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leftoff/cli",
|
|
3
|
-
"version": "0.9.18-beta.
|
|
3
|
+
"version": "0.9.18-beta.8",
|
|
4
4
|
"description": "Leftoff: WTF was I working on? Tracks every Claude Code session, writes a 3-line checkpoint when one goes quiet, and gives you a daily digest and a board, on your Mac and (optionally) your phone.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/ui/app.css
CHANGED
|
@@ -2495,6 +2495,7 @@ mark.tx, .co-snip mark, .tx-snip mark, .tx-msg-text mark { background: var(--tx-
|
|
|
2495
2495
|
.composer .att-chips[hidden] { display: none; }
|
|
2496
2496
|
.composer.att-drop { border-color: var(--accent); box-shadow: 0 0 0 3px var(--focus); background: color-mix(in srgb, var(--accent) 7%, var(--card2)); }
|
|
2497
2497
|
.att-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; overflow: hidden; clip: rect(0 0 0 0); }
|
|
2498
|
+
.att-picker { position: fixed; left: 0; bottom: 0; } /* 0.9.18: the paperclip's picker, outside the page so a redraw can't drop what you picked */
|
|
2498
2499
|
.composer-attach { flex: none; width: 44px; height: 44px; margin: 0; padding: 0; border: 0; background: transparent; display: grid; place-items: center; cursor: pointer; color: var(--muted); -webkit-tap-highlight-color: transparent; }
|
|
2499
2500
|
.composer-clip { width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; transition: background-color .15s, color .15s; }
|
|
2500
2501
|
.composer-clip .ic { width: 19px; height: 19px; }
|
package/ui/app.js
CHANGED
|
@@ -3459,6 +3459,15 @@ document.addEventListener('focusin', (e) => {
|
|
|
3459
3459
|
if (!isTypingTarget(e.target) || !document.documentElement.classList.contains('live-full')) return
|
|
3460
3460
|
clearTimeout(lfKb.settle)
|
|
3461
3461
|
if (lfKb.scroll == null) lfKb.scroll = window.scrollY
|
|
3462
|
+
// 2026-11-04: a third-party keyboard (Raycast, Gboard, SwiftKey) can come up late, or change the
|
|
3463
|
+
// visual viewport without a resize event of its own: measure again as it opens, as on the way down
|
|
3464
|
+
let i = 0
|
|
3465
|
+
const step = () => {
|
|
3466
|
+
if (!isTypingTarget(document.activeElement)) return
|
|
3467
|
+
fitFullViewport()
|
|
3468
|
+
if (++i < SETTLE_AFTER_BLUR_MS.length) lfKb.settle = setTimeout(step, SETTLE_AFTER_BLUR_MS[i] - SETTLE_AFTER_BLUR_MS[i - 1])
|
|
3469
|
+
}
|
|
3470
|
+
lfKb.settle = setTimeout(step, SETTLE_AFTER_BLUR_MS[0])
|
|
3462
3471
|
})
|
|
3463
3472
|
// A text box let go (a send closes the phone's keyboard, or a tap away): look again as the keyboard
|
|
3464
3473
|
// goes down, whatever iOS reports or doesn't on the way.
|
|
@@ -3718,11 +3727,40 @@ function composerFor(el) {
|
|
|
3718
3727
|
const box = (el && el.closest && el.closest('.composer')) || document.querySelector('#app .composer[data-sid]')
|
|
3719
3728
|
return box && !box.classList.contains('off') ? box : null
|
|
3720
3729
|
}
|
|
3730
|
+
// The picker the paperclip opens lives outside the page (0.9.18). A phone's
|
|
3731
|
+
// photo picker covers the board for seconds; coming back is a wake-up (focus
|
|
3732
|
+
// after more than AWAY_MS away) that redraws the page, and iOS hands the
|
|
3733
|
+
// picked photo over only after that, to the <input> it opened from. A picker
|
|
3734
|
+
// inside the composer was gone by then (the redraw made a new one), so its
|
|
3735
|
+
// change event went nowhere and the photo never showed. This one is never
|
|
3736
|
+
// redrawn; the composer's own hidden input stays for drops and tests.
|
|
3737
|
+
let attPicker = null
|
|
3738
|
+
function attachPicker() {
|
|
3739
|
+
if (attPicker && attPicker.isConnected) return attPicker
|
|
3740
|
+
attPicker = document.createElement('input')
|
|
3741
|
+
attPicker.type = 'file'; attPicker.multiple = true; attPicker.tabIndex = -1
|
|
3742
|
+
attPicker.className = 'att-input att-picker'; attPicker.id = 'att-picker'
|
|
3743
|
+
attPicker.setAttribute('aria-hidden', 'true')
|
|
3744
|
+
document.body.appendChild(attPicker)
|
|
3745
|
+
return attPicker
|
|
3746
|
+
}
|
|
3747
|
+
function openPicker(sid) {
|
|
3748
|
+
if (!sid) return
|
|
3749
|
+
const pk = attachPicker()
|
|
3750
|
+
pk.dataset.sid = sid
|
|
3751
|
+
pk.value = ''
|
|
3752
|
+
pk.click() // still inside the tap: iOS opens a picker only from a user gesture
|
|
3753
|
+
}
|
|
3754
|
+
function takePicked(inp) {
|
|
3755
|
+
const files = Array.from(inp.files || [])
|
|
3756
|
+
if (!files.length) return
|
|
3757
|
+
inp.value = '' // the same file can be picked again
|
|
3758
|
+
try { addFiles(inp.dataset.sid, files) } catch (e) { toast('Couldn’t attach ' + (files.length === 1 ? files[0].name || 'that file' : files.length + ' files') + ': ' + (e.message || e), 6000) }
|
|
3759
|
+
}
|
|
3721
3760
|
document.addEventListener('change', (ev) => {
|
|
3722
3761
|
const inp = ev.target
|
|
3723
3762
|
if (!inp || !inp.classList || !inp.classList.contains('att-input')) return
|
|
3724
|
-
|
|
3725
|
-
inp.value = '' // the same file can be picked again
|
|
3763
|
+
takePicked(inp)
|
|
3726
3764
|
})
|
|
3727
3765
|
document.addEventListener('paste', (ev) => {
|
|
3728
3766
|
const t = ev.target
|
|
@@ -9481,7 +9519,7 @@ document.addEventListener('click', async (ev) => {
|
|
|
9481
9519
|
if (action === 'lv-queued-cancel' || action === 'lv-queued-edit') return queuedClick(btn, action === 'lv-queued-edit')
|
|
9482
9520
|
if (action === 'lv-queued-now') return sendNowClick(btn)
|
|
9483
9521
|
if (action === 'lv-queued-more') { if (ev.target.closest('a[href]')) return; const open = !btn.classList.contains('open'); btn.classList.toggle('open', open); btn.setAttribute('aria-expanded', open ? 'true' : 'false'); return }
|
|
9484
|
-
if (action === 'attach') { const inp = btn.closest('.composer') && btn.closest('.composer').querySelector('.att-input'); if (inp && !inp.disabled) inp.
|
|
9522
|
+
if (action === 'attach') { const inp = btn.closest('.composer') && btn.closest('.composer').querySelector('.att-input'); if (inp && !inp.disabled) openPicker(inp.dataset.sid); return }
|
|
9485
9523
|
if (action === 'att-remove') return removeAttachment(btn.dataset.lid)
|
|
9486
9524
|
if (action === 'att-retry') return retryAttachment(btn.dataset.lid)
|
|
9487
9525
|
if (action === 'att-open') return openAttachment(btn)
|