@leftoff/cli 0.9.8-beta.20 → 0.9.8-beta.21
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 +4 -0
- package/bin/leftoff.js +91 -91
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/register.js +1 -1
- package/package.json +1 -1
- package/ui/app.js +35 -10
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leftoff",
|
|
3
|
-
"version": "0.9.8-beta.
|
|
3
|
+
"version": "0.9.8-beta.21",
|
|
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": "Joel"
|
package/mod/hooks/register.js
CHANGED
|
@@ -28,7 +28,7 @@ const END_TIMEOUT_MS = 1000
|
|
|
28
28
|
const MODEL_TIMEOUT_MS = 20000
|
|
29
29
|
|
|
30
30
|
// Module state. $.state is not used: the pane redraws with $.ui.invalidate.
|
|
31
|
-
const MOD_VERSION = '0.9.8-beta.
|
|
31
|
+
const MOD_VERSION = '0.9.8-beta.21' // keep in step with .claude-plugin/plugin.json
|
|
32
32
|
|
|
33
33
|
const st = {
|
|
34
34
|
internal: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leftoff/cli",
|
|
3
|
-
"version": "0.9.8-beta.
|
|
3
|
+
"version": "0.9.8-beta.21",
|
|
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.js
CHANGED
|
@@ -840,6 +840,7 @@ async function viewBoard() {
|
|
|
840
840
|
}
|
|
841
841
|
html += repoRow(inSpace, f)
|
|
842
842
|
html += boardSearch(bq, closedHits, list.length)
|
|
843
|
+
if ((state.boardQ || '').trim() !== bq) return // you typed on while this one loaded: the newer search paints
|
|
843
844
|
if (bq) {
|
|
844
845
|
if (list.length) html += '<div class="grid mt">' + list.map(card).join('') + '</div>'
|
|
845
846
|
paintBoardKeepingSearch(banners() + html)
|
|
@@ -873,39 +874,63 @@ function boardSearch(q, closedHits, n) {
|
|
|
873
874
|
(closedHits ? ' <button type="button" class="linkish" data-action="closed-search" data-q="' + esc(q) + '">' + esc(closedHits + ' closed ' + (closedHits === 1 ? 'match' : 'matches')) + ' →</button>' : '') +
|
|
874
875
|
' <a class="linkish tx-board-link" href="#/search?q=' + encodeURIComponent(q) + '">' + icon('message-square') + 'Search what was said →</a></div>' : '') + '</div>'
|
|
875
876
|
}
|
|
876
|
-
// A repaint from a live update (or the 30 s refresh
|
|
877
|
-
// search
|
|
877
|
+
// A repaint from a live update (or the 30 s refresh, or your own typing's
|
|
878
|
+
// search) never rebuilds the search box: everything around it is replaced and
|
|
879
|
+
// the box itself stays the same element, so the caret, a selection, a word
|
|
880
|
+
// being composed and the focus are exactly as you left them. (Rebuilding it
|
|
881
|
+
// and putting the caret back moved the caret while you typed: the typing's
|
|
882
|
+
// own search put back where the caret was when it started, after you'd typed on.)
|
|
878
883
|
function paintBoardKeepingSearch(h) {
|
|
879
884
|
if (parseRoute().name === 's') return // 0.9.8: a slow board answer never paints over the session you opened
|
|
880
885
|
const old = document.getElementById('boardQ')
|
|
881
|
-
const
|
|
886
|
+
const row = old && old.closest('.board-search-row')
|
|
887
|
+
if (row && row.parentNode === $app) {
|
|
888
|
+
const tpl = document.createElement('template')
|
|
889
|
+
tpl.innerHTML = h
|
|
890
|
+
const nrow = tpl.content.querySelector('.board-search-row')
|
|
891
|
+
const nbox = nrow && nrow.querySelector('#boardQ')
|
|
892
|
+
if (nbox && nrow.parentNode === tpl.content) {
|
|
893
|
+
const kids = [...tpl.content.childNodes], at = kids.indexOf(nrow)
|
|
894
|
+
while ($app.firstChild && $app.firstChild !== row) $app.removeChild($app.firstChild)
|
|
895
|
+
while (row.nextSibling) $app.removeChild(row.nextSibling)
|
|
896
|
+
row.before(...kids.slice(0, at))
|
|
897
|
+
row.after(...kids.slice(at + 1))
|
|
898
|
+
// the note under the box (n matches, closed matches) follows the new answer
|
|
899
|
+
for (const n of [...row.childNodes]) if (!n.contains(old)) row.removeChild(n)
|
|
900
|
+
for (const n of [...nrow.childNodes]) if (!n.contains(nbox)) row.appendChild(n)
|
|
901
|
+
// the words change only when you aren't typing (the address changed, Esc cleared it)
|
|
902
|
+
if (document.activeElement !== old && !boardQTimer && old.value !== nbox.value) old.value = nbox.value
|
|
903
|
+
return
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const keep = old && document.activeElement === old ? { v: old.value, a: old.selectionStart, b: old.selectionEnd, d: old.selectionDirection } : null
|
|
882
907
|
$app.innerHTML = h
|
|
883
908
|
if (!keep) return
|
|
884
909
|
const q = document.getElementById('boardQ')
|
|
885
910
|
if (!q) return
|
|
886
911
|
if (q.value !== keep.v) q.value = keep.v
|
|
887
|
-
q.focus(); try { q.setSelectionRange(keep.
|
|
912
|
+
q.focus({ preventScroll: true }); try { q.setSelectionRange(keep.a, keep.b, keep.d || 'none') } catch {}
|
|
888
913
|
}
|
|
889
914
|
let boardQTimer = null
|
|
890
915
|
function boardSearchWire() {
|
|
891
916
|
const q = document.getElementById('boardQ')
|
|
892
|
-
if (!q) return
|
|
917
|
+
if (!q || q.dataset.wired) return // the same box across repaints: wired once
|
|
918
|
+
q.dataset.wired = '1'
|
|
893
919
|
q.addEventListener('input', () => {
|
|
894
920
|
clearTimeout(boardQTimer)
|
|
895
921
|
boardQTimer = setTimeout(async () => {
|
|
896
|
-
|
|
922
|
+
boardQTimer = null
|
|
923
|
+
if (!q.isConnected) return
|
|
897
924
|
state.boardQ = q.value
|
|
898
925
|
try { history.replaceState(null, '', hashFor(state.boardFilter || { repos: [], status: null })) } catch {}
|
|
899
926
|
state.route = parseRoute()
|
|
900
|
-
await viewBoard()
|
|
901
|
-
const again = document.getElementById('boardQ')
|
|
902
|
-
if (again) { again.focus(); try { again.setSelectionRange(pos, pos) } catch {} }
|
|
927
|
+
await viewBoard() // the box isn't rebuilt, so the caret stays wherever you've typed to since
|
|
903
928
|
}, 160)
|
|
904
929
|
})
|
|
905
930
|
q.addEventListener('keydown', (ev) => {
|
|
906
931
|
if (ev.key === 'Escape' && q.value) { ev.preventDefault(); q.value = ''; q.dispatchEvent(new Event('input')) }
|
|
907
932
|
// 0.9.6: Enter (Search on the phone's keyboard) opens the full results, transcripts included
|
|
908
|
-
else if (ev.key === 'Enter' && q.value.trim() && !ev.isComposing) { ev.preventDefault(); clearTimeout(boardQTimer); location.hash = '#/search?q=' + encodeURIComponent(q.value.trim()) }
|
|
933
|
+
else if (ev.key === 'Enter' && q.value.trim() && !ev.isComposing) { ev.preventDefault(); clearTimeout(boardQTimer); boardQTimer = null; location.hash = '#/search?q=' + encodeURIComponent(q.value.trim()) }
|
|
909
934
|
})
|
|
910
935
|
}
|
|
911
936
|
|