@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leftoff",
3
- "version": "0.7.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.cwd
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
- 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'
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
- return !(Number(s.turns) > 0) && !s.title && !(Number(checkpointCount) > 0) && !s.lastCheckpointId &&
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
@@ -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.0' // 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,
@@ -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) => { await toolRan($, e); return next(e) })
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.7.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 }) + ' &nbsp;' + 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) + ' &nbsp;' + 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
- return loose && cands.some((c) => c.includes(tok))
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.