@leftoff/cli 0.7.1 → 0.8.0
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 +6 -0
- package/bin/leftoff.js +285 -273
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/core.js +10 -4
- package/mod/hooks/register.js +1 -1
- package/package.json +1 -1
- package/ui/app.css +3 -0
- package/ui/app.js +8 -3
- package/ui/needs.js +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leftoff",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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
|
@@ -285,9 +285,13 @@ const CP_ORDER = ['id', 'session_id', 'session_title', 'created_at', 'trigger',
|
|
|
285
285
|
export function resumeCommand(session) {
|
|
286
286
|
const id = session.id || session.sessionId
|
|
287
287
|
const cwd = resumeDir(session)
|
|
288
|
-
return (cwd ? 'cd ' + shArg(cwd) + ' && ' : '') + claudeInvocation(session, ['--resume', id])
|
|
288
|
+
return (cwd ? 'cd ' + shArg(cwd) + ' && ' : '') + (isCodex(session) ? codexInvocation(id) : claudeInvocation(session, ['--resume', id]))
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
// 0.8.0: a Codex session (its rollout under ~/.codex/sessions) resumes with `codex resume <id>`.
|
|
292
|
+
export function isCodex(session) { return !!session && session.codingAgent === 'codex' }
|
|
293
|
+
export function codexInvocation(id) { return 'codex resume ' + shArg(id) }
|
|
294
|
+
|
|
291
295
|
// 0.7.1: where to run `claude --resume`: the session's folder, or, inside a git
|
|
292
296
|
// worktree, that worktree's own top folder (Claude Code declines to re-enter a
|
|
293
297
|
// `git worktree add` worktree from one of its subfolders; code.claude.com/docs/en/worktrees).
|
|
@@ -508,7 +512,7 @@ export const SESSION_EXTRA = ['inTurn', 'needsInput', 'backgroundTasks', 'turnsS
|
|
|
508
512
|
|
|
509
513
|
export const SESSION_FIELDS = ['id', 'title', 'titleSource', 'machine', 'cwd', 'repo', 'transcriptPath', 'model', 'surface', 'capture',
|
|
510
514
|
'status', 'startedAt', 'lastActivityAt', 'lastHeartbeatAt', 'endedAt', 'endReason', 'turns', 'lastCheckpointId',
|
|
511
|
-
'lastCheckpointAt', 'parentSessionId', 'pinned', 'archived', 'ignored']
|
|
515
|
+
'lastCheckpointAt', 'parentSessionId', 'pinned', 'archived', 'ignored', 'codingAgent']
|
|
512
516
|
|
|
513
517
|
export function renderSessionMd(session, checkpoints = []) {
|
|
514
518
|
const fm = {}
|
|
@@ -710,7 +714,8 @@ export function computeStatus(s, now, cfg = DEFAULT_CONFIG) {
|
|
|
710
714
|
// Hooks-only capture sees no event when a turn is interrupted (Stop does not
|
|
711
715
|
// fire), so a turn that has been silent for twice the dormancy window is
|
|
712
716
|
// treated as over. The mod sees turn.complete for aborted turns too.
|
|
713
|
-
|
|
717
|
+
// 0.8.0: a Codex rollout shows no end for a turn Codex was killed in, either.
|
|
718
|
+
if ((s.capture === 'hooks' || s.capture === 'codex') && idle >= 2 * dormantAfter) return 'dormant'
|
|
714
719
|
return 'active'
|
|
715
720
|
}
|
|
716
721
|
if (s.backgroundTasks > 0) return 'background'
|
|
@@ -782,6 +787,7 @@ export function applyEvent(s, ev) {
|
|
|
782
787
|
// 0.6.8: where the model runs. A capture that says null (Anthropic) clears it; no key leaves it.
|
|
783
788
|
if ('backend' in d) { const b = cleanBackend(d.backend); if (b) s.backend = b; else delete s.backend }
|
|
784
789
|
if (d.surface) s.surface = d.surface
|
|
790
|
+
if (d.codingAgent) s.codingAgent = String(d.codingAgent) // 0.8.0: 'codex'; Claude Code sessions have none
|
|
785
791
|
if (d.repo && typeof d.repo === 'object') s.repo = { ...(s.repo || {}), ...stripNulls(d.repo) }
|
|
786
792
|
// A title carrying our own tab marker came from the mod's /rename: it is the
|
|
787
793
|
// tab label, not a name the user chose, so it never overwrites the title.
|
|
@@ -807,7 +813,7 @@ export function applyEvent(s, ev) {
|
|
|
807
813
|
break
|
|
808
814
|
case 'activity':
|
|
809
815
|
activity()
|
|
810
|
-
if (d.kind === 'tool' && d.phase === 'post' && s.needsInput && s.lastNotification === 'permission_prompt') s.needsInput = false
|
|
816
|
+
if (d.kind === 'tool' && d.phase === 'post' && s.needsInput && (s.lastNotification === 'permission_prompt' || s.capture === 'codex')) { s.needsInput = false; if (s.capture === 'codex') s.lastNotification = null }
|
|
811
817
|
if (d.kind === 'prompt') {
|
|
812
818
|
s.needsInput = false
|
|
813
819
|
s.question = null
|
package/mod/hooks/register.js
CHANGED
|
@@ -27,7 +27,7 @@ const END_TIMEOUT_MS = 1000
|
|
|
27
27
|
const MODEL_TIMEOUT_MS = 20000
|
|
28
28
|
|
|
29
29
|
// Module state. $.state is not used: the pane redraws with $.ui.invalidate.
|
|
30
|
-
const MOD_VERSION = '0.
|
|
30
|
+
const MOD_VERSION = '0.8.0' // keep in step with .claude-plugin/plugin.json
|
|
31
31
|
|
|
32
32
|
const st = {
|
|
33
33
|
internal: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leftoff/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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
|
@@ -344,6 +344,9 @@ h2 .count { color: var(--faint); font-weight: 500; }
|
|
|
344
344
|
.chip.repo-link:hover { color: var(--text); border-color: var(--line-strong); }
|
|
345
345
|
/* 0.7.1: git worktree marker and "started in" */
|
|
346
346
|
.chip.wt { color: var(--text2); max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
347
|
+
.chip.agent-badge { font-size: 11px; font-weight: 600; letter-spacing: .02em; }
|
|
348
|
+
.chip.agent-badge.codex { color: #5eead4; border-color: rgba(94,234,212,.35); }
|
|
349
|
+
[data-theme="light"] .chip.agent-badge.codex { color: #0f766e; border-color: rgba(15,118,110,.35); }
|
|
347
350
|
.chip.started-in { color: var(--muted); border-style: dashed; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
348
351
|
.linkish { background: none; border: 0; padding: 0; color: var(--accent); cursor: pointer; font: inherit; }
|
|
349
352
|
.linkish:hover { text-decoration: underline; }
|
package/ui/app.js
CHANGED
|
@@ -139,10 +139,15 @@ function agentChip(a, { title } = {}) {
|
|
|
139
139
|
return '<span class="chip machine agent' + (a.ok === false ? ' stale' : '') + '" title="' + esc(title || ('external agent · ' + (a.pullsHere ? 'pulled by this Mac' : 'pulled by ' + (a.puller || 'another Mac')) + (a.lastSuccessAt ? ' · last pull ' + ago(a.lastSuccessAt) : ''))) + '">' + esc(a.icon || '◆') + ' ' + esc(a.display || a.name) + '</span>'
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// 0.8.0: a Codex session wears a small "Codex" badge; Claude Code's wear none.
|
|
143
|
+
function codexBadge(s) {
|
|
144
|
+
return s && s.codingAgent === 'codex' ? '<span class="chip agent-badge codex" title="' + esc('OpenAI Codex session' + (s.surface === 'codex-ide' ? ' (IDE)' : s.surface === 'codex-app' ? ' (app)' : '')) + '">Codex</span>' : ''
|
|
145
|
+
}
|
|
146
|
+
|
|
142
147
|
// 0.7.1: a session in a git worktree says which ("⑂ fix-login"); one filed
|
|
143
148
|
// where its edits went says where it started.
|
|
144
149
|
function wtChips(s) {
|
|
145
|
-
let h =
|
|
150
|
+
let h = codexBadge(s)
|
|
146
151
|
if (s.worktree && s.worktree.name) h += '<span class="chip mono wt" title="' + esc('git worktree · ' + (s.worktree.path || s.worktree.name)) + '">⑂ ' + esc(s.worktree.name) + '</span>'
|
|
147
152
|
if (s.startedIn) h += '<span class="chip started-in" title="' + esc('Started in ' + s.startedIn + '; its edits went to ' + (s.repoLabel || s.repoName || 'this repo')) + '">started in ' + esc(s.startedIn) + '</span>'
|
|
148
153
|
return h
|
|
@@ -497,7 +502,7 @@ function resumeButton(s, viewer = {}) {
|
|
|
497
502
|
const busy = closedUi.busy.has(s.id)
|
|
498
503
|
if (s.isLocal) {
|
|
499
504
|
if (!viewer.ghostty) return ''
|
|
500
|
-
return '<button type="button" class="btn primary" data-action="resume-open" data-id="' + esc(s.id) + '"' + (busy ? ' disabled' : '') + ' title="Open it in a new Ghostty tab: claude --resume">' + icon('rotate-ccw') + (busy ? 'Opening…' : 'Resume') + '</button>'
|
|
505
|
+
return '<button type="button" class="btn primary" data-action="resume-open" data-id="' + esc(s.id) + '"' + (busy ? ' disabled' : '') + ' title="Open it in a new Ghostty tab: ' + (s.codingAgent === 'codex' ? 'codex resume' : 'claude --resume') + '">' + icon('rotate-ccw') + (busy ? 'Opening…' : 'Resume') + '</button>'
|
|
501
506
|
}
|
|
502
507
|
if (!(s.peer && s.peer.ok)) return '' // that Mac can't be reached from here: Copy resume
|
|
503
508
|
return '<button type="button" class="btn primary" data-action="resume-open" data-id="' + esc(s.id) + '"' + (busy ? ' disabled' : '') + ' title="' + esc('Open it in a new Ghostty tab on ' + s.machine) + '">' + icon('rotate-ccw') + (busy ? 'Opening…' : 'Resume on ' + esc(s.machine)) + '</button>'
|
|
@@ -765,7 +770,7 @@ async function viewSession(id) {
|
|
|
765
770
|
fact('Machine', s.machine + (s.isLocal ? ' (this one)' : ' (synced)')) + fact('Repo', (s.repoName || '—') + (branch ? ' · ' + branch : '')) + (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)) +
|
|
766
771
|
fact('Turns', s.turns + ' (' + s.turnsSinceCheckpoint + ' since checkpoint)' + (s.historyTurns ? ' · resumed, ' + s.historyTurns + ' before' : '')) + fact('Capture', s.capture || '—') + fact('Model', s.modelLabel || s.model || '—', 'wrap') + fact('Session id', s.id) +
|
|
767
772
|
fact('Open?', s.live === 'open' ? 'open' + (s.liveVia === 'pid' ? ' (process ' + s.pid + ' running)' : ' (heartbeat)') : s.live === 'closed' ? 'closed ' + ago(s.closedAt) : 'unknown') +
|
|
768
|
-
fact('Terminal', (s.terminal ? s.terminal.label : '—') + (s.tty ? ' · ' + s.tty.replace('/dev/', '') : '') + ' · tab cp:' + (s.shortId || '')) + '</div>'
|
|
773
|
+
fact('Terminal', (s.terminal ? s.terminal.label : s.codingAgent === 'codex' ? ({ 'codex-ide': 'Codex in the IDE', 'codex-app': 'Codex app', 'codex-exec': 'codex exec' }[s.surface] || 'Codex CLI') : '—') + (s.tty ? ' · ' + s.tty.replace('/dev/', '') : '') + ' · tab cp:' + (s.shortId || '')) + '</div>'
|
|
769
774
|
html += spacePicker(s, spv, 'session', s.id)
|
|
770
775
|
html += '<section class="sprs" id="prs">' + sessionPrsSection(s, { mayEdit: !s.agent }) + '</section>'
|
|
771
776
|
// 0.7.0: steps handed back to you, and the docs, plans and artifacts it made
|
package/ui/needs.js
CHANGED
|
@@ -47,7 +47,7 @@ const KIND_LABEL = { permission: 'Needs your OK', question: 'Question', terminal
|
|
|
47
47
|
|
|
48
48
|
export function cardBadge(n) {
|
|
49
49
|
if (!n) return ''
|
|
50
|
-
return '<div class="need-badge k-' + esc(n.kind) + (n.blocking ? ' blocking' : '') + '" title="' + esc(needsLine(n)) + '"><span class="nb-k">' + esc(KIND_LABEL[n.kind] || 'Needs you') + '</span>' +
|
|
50
|
+
return '<div class="need-badge k-' + esc(n.kind) + (n.blocking ? ' blocking' : '') + '" title="' + esc(needsLine(n)) + '"><span class="nb-k">' + esc(n.label || KIND_LABEL[n.kind] || 'Needs you') + '</span>' +
|
|
51
51
|
'<span class="nb-t">' + esc(needsLine(n)) + '</span>' + (n.blocking && n.waitSec != null ? '<span class="nb-w">' + esc(waitLabel(n.waitSec).replace('waiting ', '')) + '</span>' : '') + '</div>'
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -71,7 +71,7 @@ export function needPanel(n, opts = {}) {
|
|
|
71
71
|
h += '<div class="need-head"><span class="need-dot" aria-hidden="true"></span>' +
|
|
72
72
|
(opts.compact ? '' : '<a class="need-title" href="#/s/' + encodeURIComponent(s.id) + '">' + esc(s.title || 'Untitled session') + '</a>' + (s.repoLabel ? '<span class="chip mono">' + esc(s.repoLabel) + '</span>' : '')) +
|
|
73
73
|
(n.peer || (s.isLocal === false && s.machine) ? '<span class="chip machine remote" title="answered on that machine, through its Leftoff">⌂ ' + esc(n.peer || s.machine) + '</span>' : '') +
|
|
74
|
-
'<span class="need-kind">' + esc(KIND_LABEL[n.kind] || '') + '</span>' +
|
|
74
|
+
'<span class="need-kind">' + esc(n.label || KIND_LABEL[n.kind] || '') + '</span>' +
|
|
75
75
|
(n.waitSec != null ? '<span class="need-wait" title="since ' + esc(n.since || '') + '">' + esc(waitLabel(n.waitSec)) + '</span>' : '') + dismissButton(n) + '</div>'
|
|
76
76
|
h += needContextHtml(n, opts) + prCardHtml(n.pr)
|
|
77
77
|
if (n.kind === 'permission' && n.ask) {
|