@leftoff/cli 0.9.8-beta.13 → 0.9.8-beta.15
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 +298 -298
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/core.js +10 -0
- package/mod/hooks/register.js +1 -1
- package/package.json +1 -1
- package/ui/app.css +7 -1
- package/ui/app.js +5 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leftoff",
|
|
3
|
-
"version": "0.9.8-beta.
|
|
3
|
+
"version": "0.9.8-beta.15",
|
|
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/core.js
CHANGED
|
@@ -310,6 +310,16 @@ export function wasLiveSession(session) {
|
|
|
310
310
|
if (!session || isCodex(session) || (session.backend && session.backend.kind === 'ollama')) return false
|
|
311
311
|
return !!(session.wasLive || (session.liveLink && session.liveLink.on))
|
|
312
312
|
}
|
|
313
|
+
// 0.9.8: the same resume, but as Live (Leftoff's channel), for a Claude Code
|
|
314
|
+
// session that wasn't Live: exit it, then run this. null when it already resumes
|
|
315
|
+
// Live, or can't be Live (Codex, Ollama).
|
|
316
|
+
export function liveResumeCommand(session) {
|
|
317
|
+
if (!session || isCodex(session) || (session.backend && session.backend.kind === 'ollama') || wasLiveSession(session)) return null
|
|
318
|
+
const id = session.id || session.sessionId
|
|
319
|
+
const cwd = resumeDir(session)
|
|
320
|
+
const wt = resumeWorktreeName(session)
|
|
321
|
+
return (cwd ? 'cd ' + shArg(cwd) + ' && ' : '') + liveInvocation([...(wt ? ['--worktree', wt] : []), '--resume', id])
|
|
322
|
+
}
|
|
313
323
|
export function liveInvocation(args = []) { return 'leftoff live --' + (args.length ? ' ' + args.map(shArg).join(' ') : '') }
|
|
314
324
|
|
|
315
325
|
// 0.8.0: a Codex session (its rollout under ~/.codex/sessions) resumes with `codex resume <id>`.
|
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.15' // 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.15",
|
|
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
|
@@ -2383,7 +2383,8 @@ mark.tx, .co-snip mark, .tx-snip mark, .tx-msg-text mark { background: var(--tx-
|
|
|
2383
2383
|
@media (max-width: 640px) {
|
|
2384
2384
|
/* a phone: the buttons are a row under the title that wraps, each part of a row sharing its width */
|
|
2385
2385
|
.pagehead.s-head > .s-actions { flex: 1 1 100%; }
|
|
2386
|
-
.s-actions > .btn, .s-actions > .split { flex: 1 1
|
|
2386
|
+
.s-actions > .btn, .s-actions > .split { flex: 1 1 calc(50% - 4px); min-width: 0; } /* two equal halves per row */
|
|
2387
|
+
.s-actions > .btn.icon-btn { flex: none; }
|
|
2387
2388
|
.s-actions .split .split-main { flex: 1 1 auto; }
|
|
2388
2389
|
.s-actions > .more-wrap { flex: none; }
|
|
2389
2390
|
}
|
|
@@ -2609,3 +2610,8 @@ html.pv-open, html.pv-open body { overflow: hidden; }
|
|
|
2609
2610
|
.updatebar .ub-go:focus-visible, .updatebar .ub-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
2610
2611
|
@media (min-width: 700px) { .updatebar { left: calc(50% + var(--rail-w, 0px) / 2); } }
|
|
2611
2612
|
@media (prefers-reduced-motion: reduce) { .updatebar { transition: none; } }
|
|
2613
|
+
|
|
2614
|
+
/* 0.9.8: a non-Live session's Live box offers the Live resume command */
|
|
2615
|
+
.live-box.off .live-resume-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 6px; }
|
|
2616
|
+
.live-box.off .live-resume-row code { flex: 1 1 220px; min-width: 0; overflow-wrap: anywhere; font-size: 12.5px; padding: 8px 10px; border-radius: 8px; background: var(--card2); border: 1px solid var(--line); }
|
|
2617
|
+
.live-box.off .live-resume-row .btn { min-height: 44px; gap: 7px; }
|
package/ui/app.js
CHANGED
|
@@ -1400,7 +1400,7 @@ function sessionPageHtml(s, { spv = null, live: liveV = null, lists = null } = {
|
|
|
1400
1400
|
? '<div class="title s-title" data-id="' + esc(s.id) + '"><h1><span class="s-title-text" role="button" tabindex="0" data-action="rename" data-id="' + esc(s.id) + '" data-title="' + esc(s.title || '') + '" title="Rename">' + titleHtml(sessionTitle(s), esc) + '</span></h1><button type="button" class="icon rename-pen" data-action="rename" data-id="' + esc(s.id) + '" data-title="' + esc(s.title || '') + '" aria-label="Rename" title="Rename">' + icon('pencil') + '</button></div>'
|
|
1401
1401
|
: '<h1>' + titleHtml(sessionTitle(s), esc) + '</h1>') + '<div class="sub">' + statusPill(s) + liveBadge(s) + ' ' + spaceChip(s, { force: true }) + projChip(s) + wtChips(s) + (canProject(s) ? '<button type="button" class="chip proj-add" data-action="project-menu" data-key="' + esc(s.repoKey) + '" data-repo="' + esc(s.repoLabel || s.repoName || '') + '" title="' + esc(s.repoProject ? 'Move ' + (s.repoLabel || 'this repo') + ' to another project, or out of it' : 'Add ' + (s.repoLabel || 'this repo') + ' to a project') + '">' + (s.repoProject ? 'Change project' : '+ Project') + '</button>' : '') + ' ' + esc(s.titleSource ? (s.titleSource === 'prompt' ? 'named from the first prompt' : s.titleSource + ' title') : '') + '</div></div>' +
|
|
1402
1402
|
// 0.9.8: one line each, 44px, an icon and a short label; the group drops under the title as a whole when it doesn't fit beside it
|
|
1403
|
-
'<div class="actions s-actions">' + goButton(s, { page: true }) + (s.status === 'ended' || s.live === 'closed' ? resumeButton(s, s.viewer || { local: state.viewerLocal !== false, ghostty: !!state.ghostty }) : '') + '<a class="btn" href="#/timeline?session=' + encodeURIComponent(s.id) + '" title="Open in Timeline" aria-label="Open in Timeline">' + icon('history') + 'Timeline</a><button type="button" class="btn" data-action="resume" data-id="' + esc(s.id) + '" title="' + esc(resumeTip(s, 'Copy resume command')) + '">' + icon('copy') + 'Copy resume</button>' + (reachOf(s) === 'local' || reachOf(s) === 'peer' ? checkpointSplit(s) : reachOf(s) === 'away' ? '<span class="more-wrap">' + moreMenu(s, awayMenuItems(s), { project: false, title: s.title || 'This session' }) + '</span>' : '') + '</div></div>'
|
|
1403
|
+
'<div class="actions s-actions">' + goButton(s, { page: true }) + (s.status === 'ended' || s.live === 'closed' ? resumeButton(s, s.viewer || { local: state.viewerLocal !== false, ghostty: !!state.ghostty }) : '') + '<a class="btn" href="#/timeline?session=' + encodeURIComponent(s.id) + '" title="Open in Timeline" aria-label="Open in Timeline">' + icon('history') + 'Timeline</a><button type="button" class="btn" data-action="resume" data-id="' + esc(s.id) + '" title="' + esc(resumeTip(s, 'Copy resume command')) + '">' + icon('copy') + 'Copy resume</button>' + (s.liveResumeCommand ? '<button type="button" class="btn" data-action="copy-cmd" data-cmd="' + esc(s.liveResumeCommand) + '" title="Exit the session in its terminal, then run this to bring it back Live">' + icon('copy') + 'Copy Live resume</button>' : '') + (reachOf(s) === 'local' || reachOf(s) === 'peer' ? checkpointSplit(s) : reachOf(s) === 'away' ? '<span class="more-wrap">' + moreMenu(s, awayMenuItems(s), { project: false, title: s.title || 'This session' }) + '</span>' : '') + '</div></div>'
|
|
1404
1404
|
html += ownerOfflineNote(s) // 0.9.8: before the facts, where it's seen
|
|
1405
1405
|
html += '<div class="facts">' +
|
|
1406
1406
|
fact('Machine', mEmoji(s.machine) + ' ' + s.machine + (thisOneSuffix(s.machine, state.health) || (s.isLocal ? '' : ' (synced)'))) + fact('Repo', (s.repoName || '—') + (branch ? ' · ' + branch : '')) + (s.repoProject ? fact('Project', projectLabel(s.repoProject)) : '') + (s.worktree ? fact('Worktree', '⑂ ' + s.worktree.name + (s.worktree.branch ? ' · ' + s.worktree.branch : ''), 'wrap') : '') + (s.startedIn ? fact('Started in', s.startedIn) : '') + fact('Directory', s.cwd || '—') + fact('Last activity', ago(s.lastActivityAt)) + fact('Started', clock(s.startedAt)) +
|
|
@@ -1483,7 +1483,8 @@ function liveBoxHtml(s, v) {
|
|
|
1483
1483
|
const how = state.liveHow
|
|
1484
1484
|
return '<section class="live-box off" aria-label="Live link"><div class="live-head"><h2>Live <span class="pill soon">Beta</span></h2></div>' +
|
|
1485
1485
|
'<p class="faint">To send this session messages from here or your phone, and answer its permission prompts, start it with <code>leftoff live</code>' +
|
|
1486
|
-
(how && how.line ? ' (or <code>' + esc(how.line) + '</code>)' : '') + '. Claude Code asks once to load the channel; channels are a research preview.</p
|
|
1486
|
+
(how && how.line ? ' (or <code>' + esc(how.line) + '</code>)' : '') + '. Claude Code asks once to load the channel; channels are a research preview.</p>' +
|
|
1487
|
+
(s.liveResumeCommand ? '<p class="faint">Already running? Exit it in its terminal, then paste this to bring it back Live:</p><div class="live-resume-row"><code class="mono">' + esc(s.liveResumeCommand) + '</code><button type="button" class="btn" data-action="copy-cmd" data-cmd="' + esc(s.liveResumeCommand) + '">' + icon('copy') + 'Copy Live resume</button></div>' : '') + '</section>'
|
|
1487
1488
|
}
|
|
1488
1489
|
const msgs = (v && v.messages) || []
|
|
1489
1490
|
const may = state.mayAnswer !== false
|
|
@@ -3875,7 +3876,7 @@ function paintListsCount(lists) {
|
|
|
3875
3876
|
// The session page's Lists section: saved lists, the hint, and "Find lists in this session".
|
|
3876
3877
|
function sessionListsSection(s) {
|
|
3877
3878
|
const f = listUi.found[s.id]
|
|
3878
|
-
let h = '<h2>Lists <span class="count">' + ((s.lists || []).length) + '</span><button type="button" class="helpq" data-action="list-help-
|
|
3879
|
+
let h = '<h2>Lists <span class="count">' + ((s.lists || []).length) + '</span><button type="button" class="helpq" data-action="list-help-inline" aria-expanded="' + (listUi.sHelp ? 'true' : 'false') + '" aria-controls="sListsHelp" title="How lists work">?</button></h2>' + (listUi.sHelp ? listsHelp().replace('id="listsHelp"', 'id="sListsHelp"') : '')
|
|
3879
3880
|
if (s.lists && s.lists.length) h += '<div class="lists">' + s.lists.map((x) => (state.listFull && state.listFull[x.ref] ? listCard(state.listFull[x.ref], { session: false }) : '')).join('') + '</div>'
|
|
3880
3881
|
if (s.listHint) h += '<div class="lfound hint"><div><strong>Claude wrote a ' + esc(s.listHint.count) + '-step list</strong>: ' + esc(s.listHint.name) + '</div><div class="row"><button type="button" class="btn primary" data-action="list-save-hint" data-id="' + esc(s.id) + '">Save</button><button type="button" class="btn" data-action="list-dismiss" data-id="' + esc(s.id) + '">Dismiss</button></div></div>'
|
|
3881
3882
|
h += '<div class="lfind"><button type="button" class="btn" data-action="list-find" data-id="' + esc(s.id) + '"' + (listUi.finding[s.id] ? ' disabled' : '') + '>' + icon('list-checks') + (listUi.finding[s.id] ? 'Reading the transcript…' : 'Find lists in this session') + '</button>' +
|
|
@@ -4950,6 +4951,7 @@ document.addEventListener('click', async (ev) => {
|
|
|
4950
4951
|
if (action === 'list-showall') { listUi.showAll = !listUi.showAll; return render() }
|
|
4951
4952
|
if (action === 'list-help') { listUi.help = !listUi.help; return render() }
|
|
4952
4953
|
if (action === 'list-help-go') { listUi.help = true; location.hash = '#/lists'; return }
|
|
4954
|
+
if (action === 'list-help-inline') { listUi.sHelp = !listUi.sHelp; render(); return }
|
|
4953
4955
|
if (action === 'remote-link' || action === 'remote-code') {
|
|
4954
4956
|
const l = await api('POST', '/v1/remote/link', action === 'remote-link' ? { full: true } : undefined)
|
|
4955
4957
|
if (l.needed === false) { toast('No sign-in needed (remoteRequireToken is off)'); return }
|