@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leftoff",
3
- "version": "0.7.1",
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
- if (s.capture === 'hooks' && idle >= 2 * dormantAfter) return 'dormant'
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
@@ -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.7.1' // keep in step with .claude-plugin/plugin.json
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.7.1",
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) {