@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leftoff",
3
- "version": "0.9.8-beta.20",
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"
@@ -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.20' // keep in step with .claude-plugin/plugin.json
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.20",
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) while you type in the
877
- // search box keeps the box focused, with what you typed and the caret.
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 keep = old && document.activeElement === old ? { v: old.value, pos: old.selectionStart } : null
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.pos, keep.pos) } catch {}
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
- const pos = q.selectionStart
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