@leftoff/cli 0.7.0 → 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 +13 -0
- package/bin/leftoff.js +285 -273
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/core.js +24 -7
- package/mod/hooks/register.js +22 -4
- package/package.json +1 -1
- package/ui/app.css +6 -0
- package/ui/app.js +21 -7
- package/ui/needs.js +2 -2
- package/ui/repos.js +3 -1
|
@@ -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
|
@@ -284,8 +284,22 @@ const CP_ORDER = ['id', 'session_id', 'session_title', 'created_at', 'trigger',
|
|
|
284
284
|
|
|
285
285
|
export function resumeCommand(session) {
|
|
286
286
|
const id = session.id || session.sessionId
|
|
287
|
-
const cwd = session
|
|
288
|
-
return (cwd ? 'cd ' + shArg(cwd) + ' && ' : '') + claudeInvocation(session, ['--resume', id])
|
|
287
|
+
const cwd = resumeDir(session)
|
|
288
|
+
return (cwd ? 'cd ' + shArg(cwd) + ' && ' : '') + (isCodex(session) ? codexInvocation(id) : claudeInvocation(session, ['--resume', id]))
|
|
289
|
+
}
|
|
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
|
+
|
|
295
|
+
// 0.7.1: where to run `claude --resume`: the session's folder, or, inside a git
|
|
296
|
+
// worktree, that worktree's own top folder (Claude Code declines to re-enter a
|
|
297
|
+
// `git worktree add` worktree from one of its subfolders; code.claude.com/docs/en/worktrees).
|
|
298
|
+
export function resumeDir(session) {
|
|
299
|
+
const cwd = session && session.cwd
|
|
300
|
+
const wt = session && session.repo && session.repo.worktree
|
|
301
|
+
if (cwd && wt && typeof wt === 'string' && (cwd === wt || String(cwd).startsWith(wt.replace(/\/+$/, '') + '/'))) return wt
|
|
302
|
+
return cwd || null
|
|
289
303
|
}
|
|
290
304
|
|
|
291
305
|
// POSIX single quotes when needed (also right in fish).
|
|
@@ -494,11 +508,11 @@ const SESSION_ORDER = ['id', 'title', 'title_source', 'machine', 'cwd', 'repo',
|
|
|
494
508
|
export const SESSION_EXTRA = ['inTurn', 'needsInput', 'backgroundTasks', 'turnsSinceCheckpoint', 'dormantStretch',
|
|
495
509
|
'finalCheckpointDone', 'firstPrompt', 'lastPreview', 'lastNotification', 'lastEventAt', 'statusChanges', 'folder',
|
|
496
510
|
'pid', 'tty', 'pidStartedAt', 'pidSource', 'term', 'closedAt', 'answers', 'lastNotificationMessage', 'needsInputAt', 'question', 'questionAt',
|
|
497
|
-
'startSource', 'historyTurns', 'healedAt', 'healedVia', 'archivedReason', 'backend', 'prLinks', 'prUnlinked', 'todos', 'files']
|
|
511
|
+
'startSource', 'historyTurns', 'healedAt', 'healedVia', 'archivedReason', 'backend', 'prLinks', 'prUnlinked', 'todos', 'files', 'startRepo', 'editedRepos']
|
|
498
512
|
|
|
499
513
|
export const SESSION_FIELDS = ['id', 'title', 'titleSource', 'machine', 'cwd', 'repo', 'transcriptPath', 'model', 'surface', 'capture',
|
|
500
514
|
'status', 'startedAt', 'lastActivityAt', 'lastHeartbeatAt', 'endedAt', 'endReason', 'turns', 'lastCheckpointId',
|
|
501
|
-
'lastCheckpointAt', 'parentSessionId', 'pinned', 'archived', 'ignored']
|
|
515
|
+
'lastCheckpointAt', 'parentSessionId', 'pinned', 'archived', 'ignored', 'codingAgent']
|
|
502
516
|
|
|
503
517
|
export function renderSessionMd(session, checkpoints = []) {
|
|
504
518
|
const fm = {}
|
|
@@ -700,7 +714,8 @@ export function computeStatus(s, now, cfg = DEFAULT_CONFIG) {
|
|
|
700
714
|
// Hooks-only capture sees no event when a turn is interrupted (Stop does not
|
|
701
715
|
// fire), so a turn that has been silent for twice the dormancy window is
|
|
702
716
|
// treated as over. The mod sees turn.complete for aborted turns too.
|
|
703
|
-
|
|
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'
|
|
704
719
|
return 'active'
|
|
705
720
|
}
|
|
706
721
|
if (s.backgroundTasks > 0) return 'background'
|
|
@@ -745,7 +760,8 @@ export function checkpointDue(s, now, cfg = DEFAULT_CONFIG) {
|
|
|
745
760
|
// "N empty sessions" row until their first prompt; the digest and restore skip them.
|
|
746
761
|
export function isEmptySession(s, checkpointCount = 0) {
|
|
747
762
|
if (!s) return true
|
|
748
|
-
|
|
763
|
+
// 0.7.1: a prompt counts. A long first turn (a worktree bug fix) has no completed turn yet.
|
|
764
|
+
return !(Number(s.turns) > 0) && !s.title && !s.firstPrompt && !(Number(checkpointCount) > 0) && !s.lastCheckpointId &&
|
|
749
765
|
!(Number(s.historyTurns) > 0) && s.startSource !== 'resume'
|
|
750
766
|
}
|
|
751
767
|
|
|
@@ -771,6 +787,7 @@ export function applyEvent(s, ev) {
|
|
|
771
787
|
// 0.6.8: where the model runs. A capture that says null (Anthropic) clears it; no key leaves it.
|
|
772
788
|
if ('backend' in d) { const b = cleanBackend(d.backend); if (b) s.backend = b; else delete s.backend }
|
|
773
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
|
|
774
791
|
if (d.repo && typeof d.repo === 'object') s.repo = { ...(s.repo || {}), ...stripNulls(d.repo) }
|
|
775
792
|
// A title carrying our own tab marker came from the mod's /rename: it is the
|
|
776
793
|
// tab label, not a name the user chose, so it never overwrites the title.
|
|
@@ -796,7 +813,7 @@ export function applyEvent(s, ev) {
|
|
|
796
813
|
break
|
|
797
814
|
case 'activity':
|
|
798
815
|
activity()
|
|
799
|
-
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 }
|
|
800
817
|
if (d.kind === 'prompt') {
|
|
801
818
|
s.needsInput = false
|
|
802
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,
|
|
@@ -828,6 +828,18 @@ function startReplyLoop($) {
|
|
|
828
828
|
loop().catch(() => { st.replyLoop = false })
|
|
829
829
|
}
|
|
830
830
|
|
|
831
|
+
// 0.7.1: Claude moved (cd, or into a git worktree). Settings-hook payloads carry
|
|
832
|
+
// the session's cwd and transcript path, which both follow it into a worktree
|
|
833
|
+
// (code.claude.com/docs/en/worktrees), so pass them on when they change.
|
|
834
|
+
function moved(e) {
|
|
835
|
+
const out = {}
|
|
836
|
+
const r = st.record || {}
|
|
837
|
+
if (e && typeof e.cwd === 'string' && e.cwd && e.cwd !== r.cwd) out.cwd = e.cwd
|
|
838
|
+
if (e && typeof e.transcript_path === 'string' && e.transcript_path && e.transcript_path !== r.transcriptPath) out.transcriptPath = e.transcript_path
|
|
839
|
+
if (out.cwd && st.hello) st.hello = { ...st.hello, cwd: out.cwd, repo: null } // a hello said again says where it is now
|
|
840
|
+
return out
|
|
841
|
+
}
|
|
842
|
+
|
|
831
843
|
export function register(on, options) {
|
|
832
844
|
on('session.start', async ($, e, next) => {
|
|
833
845
|
try {
|
|
@@ -890,7 +902,7 @@ export function register(on, options) {
|
|
|
890
902
|
})
|
|
891
903
|
|
|
892
904
|
on('classic.UserPromptSubmit', async ($, e, next) => {
|
|
893
|
-
await sendOrSpool($, 'activity', { kind: 'prompt', prompt: clip(e.prompt || '', 300), sessionTitle: e.session_title && e.session_title === st.tabTitle ? null : e.session_title }) // our own /rename isn't a name you chose
|
|
905
|
+
await sendOrSpool($, 'activity', { kind: 'prompt', prompt: clip(e.prompt || '', 300), sessionTitle: e.session_title && e.session_title === st.tabTitle ? null : e.session_title, ...moved(e) }) // our own /rename isn't a name you chose
|
|
894
906
|
return next(e)
|
|
895
907
|
})
|
|
896
908
|
|
|
@@ -920,6 +932,7 @@ export function register(on, options) {
|
|
|
920
932
|
// board's card offers "Claude wrote a 7-step list · Save". Nothing is saved here.
|
|
921
933
|
const listHint = listHintFrom(e.last_assistant_message || '', { fallback: st.daemonTitle || null })
|
|
922
934
|
await sendOrSpool($, 'turn_completed', {
|
|
935
|
+
...moved(e),
|
|
923
936
|
lastAssistantPreview: clip(e.last_assistant_message || '', 280),
|
|
924
937
|
question: trailingQuestion(e.last_assistant_message || ''),
|
|
925
938
|
...(listHint ? { listHint } : {}),
|
|
@@ -937,13 +950,18 @@ export function register(on, options) {
|
|
|
937
950
|
})
|
|
938
951
|
|
|
939
952
|
on('classic.Notification', async ($, e, next) => {
|
|
940
|
-
await sendOrSpool($, 'notification', { notificationType: e.notification_type || null, message: clip(e.message || '', 200) })
|
|
953
|
+
await sendOrSpool($, 'notification', { notificationType: e.notification_type || null, message: clip(e.message || '', 200), ...moved(e) })
|
|
941
954
|
return next(e)
|
|
942
955
|
})
|
|
943
956
|
|
|
944
957
|
on('classic.PermissionRequest', permissionHook)
|
|
945
958
|
|
|
946
|
-
on('classic.PostToolUse', async ($, e, next) => {
|
|
959
|
+
on('classic.PostToolUse', async ($, e, next) => {
|
|
960
|
+
await toolRan($, e)
|
|
961
|
+
const m = moved(e) // EnterWorktree, or a cd: say so now, not at the end of the turn
|
|
962
|
+
if (m.cwd || m.transcriptPath) await sendOrSpool($, 'activity', { kind: 'cwd', ...m })
|
|
963
|
+
return next(e)
|
|
964
|
+
})
|
|
947
965
|
|
|
948
966
|
on('classic.PostToolUseFailure', async ($, e, next) => { await toolRan($, e); return next(e) })
|
|
949
967
|
|
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
|
@@ -342,6 +342,12 @@ h2 .count { color: var(--faint); font-weight: 500; }
|
|
|
342
342
|
.repomore .opt[hidden] { display: none; }
|
|
343
343
|
.chip.repo-link { cursor: pointer; font: inherit; font-family: var(--mono); font-size: 11.5px; }
|
|
344
344
|
.chip.repo-link:hover { color: var(--text); border-color: var(--line-strong); }
|
|
345
|
+
/* 0.7.1: git worktree marker and "started in" */
|
|
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); }
|
|
350
|
+
.chip.started-in { color: var(--muted); border-style: dashed; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
345
351
|
.linkish { background: none; border: 0; padding: 0; color: var(--accent); cursor: pointer; font: inherit; }
|
|
346
352
|
.linkish:hover { text-decoration: underline; }
|
|
347
353
|
.digest-repos { margin: 0 0 14px; }
|
package/ui/app.js
CHANGED
|
@@ -139,11 +139,25 @@ 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
|
+
|
|
147
|
+
// 0.7.1: a session in a git worktree says which ("⑂ fix-login"); one filed
|
|
148
|
+
// where its edits went says where it started.
|
|
149
|
+
function wtChips(s) {
|
|
150
|
+
let h = codexBadge(s)
|
|
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>'
|
|
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>'
|
|
153
|
+
return h
|
|
154
|
+
}
|
|
155
|
+
|
|
142
156
|
function where(s) {
|
|
143
157
|
if (s.agent) return (s.project ? '<button type="button" class="chip mono repo-link" data-action="repo" data-repo="' + esc(s.repoLabel) + '" data-key="' + esc(s.repoKey || '') + '" title="' + esc(s.repoLabel + ' · show only this project') + '">' + esc(s.repoLabel) + '</button>' : '') + agentChip(s.agent)
|
|
144
158
|
const branch = s.repo && s.repo.branch
|
|
145
159
|
const label = s.repoLabel || s.repoName || 'no repo'
|
|
146
|
-
return '<button type="button" class="chip mono repo-link" data-action="repo" data-repo="' + esc(label) + '" data-key="' + esc(s.repoKey || '') + '" title="' + esc((s.repoKey || label) + ' · show only this repo') + '">' + esc(label) + (branch ? ' · ' + esc(branch) : '') + '</button>' +
|
|
160
|
+
return '<button type="button" class="chip mono repo-link" data-action="repo" data-repo="' + esc(label) + '" data-key="' + esc(s.repoKey || '') + '" title="' + esc((s.repoKey || label) + ' · show only this repo') + '">' + esc(label) + (branch ? ' · ' + esc(branch) : '') + '</button>' + wtChips(s) +
|
|
147
161
|
(multi() || !s.isLocal ? '<span class="chip machine' + (s.isLocal ? '' : ' remote') + '" title="' + (s.isLocal ? 'this machine' : 'synced from another machine') + '">⌂ ' + esc(s.machine) + '</span>' : '') + modelChip(s)
|
|
148
162
|
}
|
|
149
163
|
|
|
@@ -488,7 +502,7 @@ function resumeButton(s, viewer = {}) {
|
|
|
488
502
|
const busy = closedUi.busy.has(s.id)
|
|
489
503
|
if (s.isLocal) {
|
|
490
504
|
if (!viewer.ghostty) return ''
|
|
491
|
-
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>'
|
|
492
506
|
}
|
|
493
507
|
if (!(s.peer && s.peer.ok)) return '' // that Mac can't be reached from here: Copy resume
|
|
494
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>'
|
|
@@ -500,7 +514,7 @@ function closedCard(s, viewer) {
|
|
|
500
514
|
return '<article class="card ended is-closed closed-card" data-id="' + esc(s.id) + '" data-href="' + esc(href) + '">' +
|
|
501
515
|
'<div class="row1"><div class="title"><a href="' + href + '">' + esc(s.title || 'Untitled session') + '</a></div><span class="pill s-closed">closed</span></div>' +
|
|
502
516
|
'<div class="meta">' + (s.space && s.space !== s.machine ? spaceChip(s) : '') +
|
|
503
|
-
'<button type="button" class="chip mono repo-link" data-action="repo" data-repo="' + esc(label) + '" data-key="' + esc(s.repoKey || '') + '" title="' + esc((s.repoKey || label) + ' · show only this repo') + '">' + esc(label) + (branch ? ' · ' + esc(branch) : '') + '</button>' +
|
|
517
|
+
'<button type="button" class="chip mono repo-link" data-action="repo" data-repo="' + esc(label) + '" data-key="' + esc(s.repoKey || '') + '" title="' + esc((s.repoKey || label) + ' · show only this repo') + '">' + esc(label) + (branch ? ' · ' + esc(branch) : '') + '</button>' + wtChips(s) +
|
|
504
518
|
'<span class="chip machine' + (s.isLocal ? '' : ' remote') + '" title="' + (s.isLocal ? 'this machine' : 'another machine') + '">⌂ ' + esc(s.machine) + '</span>' + modelChip(s) +
|
|
505
519
|
'<span title="' + esc(clock(s.endedAt)) + '">ended ' + esc(ago(s.endedAt)) + '</span><span>·</span><span>' + esc(s.turns) + (s.turns === 1 ? ' turn' : ' turns') + '</span></div>' +
|
|
506
520
|
cardHandoffChips(s) + cpBlock(s.latestCheckpoint, { showWhen: false }) +
|
|
@@ -542,7 +556,7 @@ async function resumeOpen(btn, id) {
|
|
|
542
556
|
btn.disabled = true; btn.textContent = 'Opening…'
|
|
543
557
|
try {
|
|
544
558
|
const r = await api('POST', '/v1/sessions/' + encodeURIComponent(id) + '/resume', {}, { timeoutMs: 60000 })
|
|
545
|
-
toast(r.peer ? 'Resumed on ' + r.peer + ' in a new Ghostty tab' : 'Resumed in a new Ghostty tab')
|
|
559
|
+
toast(r.note ? r.note : r.peer ? 'Resumed on ' + r.peer + ' in a new Ghostty tab' : 'Resumed in a new Ghostty tab', r.note ? 6000 : undefined)
|
|
546
560
|
return true
|
|
547
561
|
} catch (e) {
|
|
548
562
|
toast(e.message, 6000)
|
|
@@ -750,13 +764,13 @@ async function viewSession(id) {
|
|
|
750
764
|
const branch = s.repo && s.repo.branch
|
|
751
765
|
const spv = await api('GET', '/v1/spaces').catch(() => null)
|
|
752
766
|
let html = '<a class="back" href="#/">← All sessions</a>'
|
|
753
|
-
html += '<div class="pagehead"><div><h1>' + esc(s.title || 'Untitled session') + '</h1><div class="sub">' + pill(s.status) + ' ' + spaceChip(s, { force: true }) + ' ' + esc(s.titleSource ? s.titleSource + ' title' : '') + '</div></div>' +
|
|
767
|
+
html += '<div class="pagehead"><div><h1>' + esc(s.title || 'Untitled session') + '</h1><div class="sub">' + pill(s.status) + ' ' + spaceChip(s, { force: true }) + wtChips(s) + ' ' + esc(s.titleSource ? s.titleSource + ' title' : '') + '</div></div>' +
|
|
754
768
|
'<div class="actions">' + goButton(s) + (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) + '">' + icon('history') + 'Open in Timeline</a><button type="button" class="btn" data-action="resume" data-id="' + esc(s.id) + '">Copy resume</button>' + (s.isLocal ? '<button type="button" class="btn primary" data-action="checkpoint" data-id="' + esc(s.id) + '">Checkpoint now</button>' : '') + '</div></div>'
|
|
755
769
|
html += '<div class="facts">' +
|
|
756
|
-
fact('Machine', s.machine + (s.isLocal ? ' (this one)' : ' (synced)')) + fact('Repo', (s.repoName || '—') + (branch ? ' · ' + branch : '')) + fact('Directory', s.cwd || '—') + fact('Last activity', ago(s.lastActivityAt)) + fact('Started', clock(s.startedAt)) +
|
|
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)) +
|
|
757
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) +
|
|
758
772
|
fact('Open?', s.live === 'open' ? 'open' + (s.liveVia === 'pid' ? ' (process ' + s.pid + ' running)' : ' (heartbeat)') : s.live === 'closed' ? 'closed ' + ago(s.closedAt) : 'unknown') +
|
|
759
|
-
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>'
|
|
760
774
|
html += spacePicker(s, spv, 'session', s.id)
|
|
761
775
|
html += '<section class="sprs" id="prs">' + sessionPrsSection(s, { mayEdit: !s.agent }) + '</section>'
|
|
762
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) {
|
package/ui/repos.js
CHANGED
|
@@ -77,7 +77,9 @@ export function repoMatches(s, token, { loose = false } = {}) {
|
|
|
77
77
|
if (cands.includes(tok)) return true
|
|
78
78
|
// "parent/base" given for a key that is ".../parent/base"
|
|
79
79
|
if (tok.includes('/') && String(key).toLowerCase().endsWith('/' + tok)) return true
|
|
80
|
-
|
|
80
|
+
if (loose && cands.some((c) => c.includes(tok))) return true
|
|
81
|
+
// 0.7.1: the other repos it belongs to (started in, edited in) find it too
|
|
82
|
+
return Array.isArray(s.alsoRepos) && s.alsoRepos.some((r) => r && r.key && repoMatches({ repoKey: r.key, repoLabel: r.label }, token, { loose }))
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
// The CLI's --repo: exact matches win; with none, a substring.
|