@phnx-labs/agents-cli 1.22.30 → 1.22.32

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.
Files changed (83) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +13 -6
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/daemon.js +52 -12
  5. package/dist/commands/doctor.d.ts +19 -0
  6. package/dist/commands/doctor.js +119 -17
  7. package/dist/commands/focus.d.ts +4 -1
  8. package/dist/commands/focus.js +19 -4
  9. package/dist/commands/routines.js +164 -36
  10. package/dist/commands/secrets.d.ts +20 -0
  11. package/dist/commands/secrets.js +59 -93
  12. package/dist/commands/{sessions-favorite.d.ts → sessions-bookmark.d.ts} +7 -7
  13. package/dist/commands/{sessions-favorite.js → sessions-bookmark.js} +38 -38
  14. package/dist/commands/sessions-browser.d.ts +10 -8
  15. package/dist/commands/sessions-browser.js +61 -32
  16. package/dist/commands/sessions-stats.js +1 -1
  17. package/dist/commands/sessions.d.ts +11 -9
  18. package/dist/commands/sessions.js +114 -65
  19. package/dist/commands/update.d.ts +2 -0
  20. package/dist/commands/update.js +148 -0
  21. package/dist/index.js +4 -2
  22. package/dist/lib/catchup.js +4 -1
  23. package/dist/lib/daemon.d.ts +17 -0
  24. package/dist/lib/daemon.js +69 -3
  25. package/dist/lib/devices/doctor-findings.d.ts +7 -2
  26. package/dist/lib/devices/doctor-findings.js +53 -2
  27. package/dist/lib/devices/doctor-overview-cache.d.ts +7 -0
  28. package/dist/lib/devices/doctor-overview-cache.js +15 -0
  29. package/dist/lib/devices/fleet-divergence.d.ts +11 -0
  30. package/dist/lib/devices/fleet-divergence.js +6 -0
  31. package/dist/lib/devices/fleet-inventory.js +16 -2
  32. package/dist/lib/drift.d.ts +6 -1
  33. package/dist/lib/drift.js +9 -0
  34. package/dist/lib/hooks/cache.js +20 -1
  35. package/dist/lib/hooks.d.ts +91 -1
  36. package/dist/lib/hooks.js +289 -3
  37. package/dist/lib/hosts/passthrough.js +3 -0
  38. package/dist/lib/installations/index.d.ts +14 -0
  39. package/dist/lib/installations/index.js +14 -0
  40. package/dist/lib/installations/resolve.d.ts +43 -0
  41. package/dist/lib/installations/resolve.js +93 -0
  42. package/dist/lib/installations/store.d.ts +56 -0
  43. package/dist/lib/installations/store.js +196 -0
  44. package/dist/lib/installations/strategies.d.ts +73 -0
  45. package/dist/lib/installations/strategies.js +293 -0
  46. package/dist/lib/installations/types.d.ts +78 -0
  47. package/dist/lib/installations/types.js +8 -0
  48. package/dist/lib/installations/update.d.ts +40 -0
  49. package/dist/lib/installations/update.js +131 -0
  50. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  51. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  52. package/dist/lib/migrate.d.ts +27 -0
  53. package/dist/lib/migrate.js +118 -2
  54. package/dist/lib/picker.d.ts +6 -3
  55. package/dist/lib/picker.js +7 -2
  56. package/dist/lib/routine-context.d.ts +144 -0
  57. package/dist/lib/routine-context.js +268 -0
  58. package/dist/lib/routine-readiness.d.ts +47 -0
  59. package/dist/lib/routine-readiness.js +239 -0
  60. package/dist/lib/routines.d.ts +97 -1
  61. package/dist/lib/routines.js +107 -1
  62. package/dist/lib/runner.d.ts +18 -4
  63. package/dist/lib/runner.js +291 -98
  64. package/dist/lib/scheduler.d.ts +7 -1
  65. package/dist/lib/scheduler.js +5 -2
  66. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  67. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  68. package/dist/lib/secrets/agent.d.ts +0 -1
  69. package/dist/lib/secrets/agent.js +0 -4
  70. package/dist/lib/secrets/session-store.d.ts +0 -4
  71. package/dist/lib/secrets/session-store.js +0 -5
  72. package/dist/lib/self-heal/checks/hook-runtime.d.ts +2 -0
  73. package/dist/lib/self-heal/checks/hook-runtime.js +16 -0
  74. package/dist/lib/self-heal/registry.js +5 -2
  75. package/dist/lib/self-heal/types.d.ts +1 -1
  76. package/dist/lib/session/{favorites.d.ts → bookmarks.d.ts} +14 -14
  77. package/dist/lib/session/{favorites.js → bookmarks.js} +22 -22
  78. package/dist/lib/session/state.js +4 -1
  79. package/dist/lib/startup/command-registry.d.ts +1 -0
  80. package/dist/lib/startup/command-registry.js +2 -0
  81. package/dist/lib/versions.d.ts +24 -0
  82. package/dist/lib/versions.js +49 -16
  83. package/package.json +2 -2
@@ -1,26 +1,26 @@
1
1
  /**
2
- * `agents sessions favorite` — the non-TTY half of the favorite hotkey.
2
+ * `agents sessions bookmark` — the non-TTY half of the bookmark hotkey.
3
3
  *
4
- * The `*` hotkey in the interactive browser is how a human favorites a session;
4
+ * The `*` hotkey in the interactive browser is how a human bookmarks a session;
5
5
  * this is how a script, an agent, or a machine without a TTY does the same thing,
6
6
  * and it is what makes the feature testable end to end without driving a terminal
7
- * UI. Both write the one store in `lib/session/favorites.ts`.
7
+ * UI. Both write the one store in `lib/session/bookmarks.ts`.
8
8
  */
9
9
  import chalk from 'chalk';
10
10
  import { setHelpSections } from '../lib/help.js';
11
11
  import { findSessionsById } from '../lib/session/db.js';
12
12
  import { isCompleteSessionId } from '../lib/session/discover.js';
13
- import { isFavorite, listFavorites, setFavorite } from '../lib/session/favorites.js';
13
+ import { isBookmarked, listBookmarks, setBookmark } from '../lib/session/bookmarks.js';
14
14
  /**
15
15
  * Resolve one user-typed id (usually the 8-char short id the listing prints) to
16
- * a full session id. Ambiguity is an ERROR, not a silent first-match: favoriting
17
- * the wrong session is invisible until the user wonders where their favorite went.
16
+ * a full session id. Ambiguity is an ERROR, not a silent first-match: bookmarking
17
+ * the wrong session is invisible until the user wonders where their bookmark went.
18
18
  */
19
- export function resolveFavoriteTarget(idQuery) {
19
+ export function resolveBookmarkTarget(idQuery) {
20
20
  const matches = findSessionsById(idQuery);
21
21
  // A COMPLETE id needs no index entry: the id is the key the store is built on,
22
22
  // and requiring a transcript row would refuse exactly the newest sessions — a
23
- // live one that has not been indexed yet. The browser's `*` favorites those
23
+ // live one that has not been indexed yet. The browser's `*` bookmarks those
24
24
  // from the live row, so demanding a DB hit here would make the two disagree.
25
25
  if (matches.length === 0) {
26
26
  return isCompleteSessionId(idQuery.trim())
@@ -33,33 +33,33 @@ export function resolveFavoriteTarget(idQuery) {
33
33
  }
34
34
  return { id: matches[0].id };
35
35
  }
36
- export function registerSessionsFavoriteCommand(sessionsCmd) {
36
+ export function registerSessionsBookmarkCommand(sessionsCmd) {
37
37
  const cmd = sessionsCmd
38
- .command('favorite')
39
- .argument('[ids...]', 'Session ids to favorite (full or short id prefix)')
40
- .description('Favorite sessions so they are easy to find again — list them with --favorites, or `f` in the browser.')
41
- .option('--remove', 'Remove the given sessions from favorites instead of adding them')
42
- .option('--list', 'List the favorited sessions (the default when no ids are given)')
38
+ .command('bookmark')
39
+ .argument('[ids...]', 'Session ids to bookmark (full or short id prefix)')
40
+ .description('Bookmark sessions so they are easy to find again — list them with --bookmarks, or `b` in the browser.')
41
+ .option('--remove', 'Remove the given sessions from bookmarks instead of adding them')
42
+ .option('--list', 'List the bookmarked sessions (the default when no ids are given)')
43
43
  .option('--json', 'Output JSON');
44
44
  setHelpSections(cmd, {
45
45
  examples: `
46
- # Favorite a session by its short id (the 8 chars the listing prints)
47
- agents sessions favorite 26c27162
46
+ # Bookmark a session by its short id (the 8 chars the listing prints)
47
+ agents sessions bookmark 26c27162
48
48
 
49
- # See what is favorited
50
- agents sessions favorite --list
49
+ # See what is bookmarked
50
+ agents sessions bookmark --list
51
51
 
52
- # Browse only the favorited ones
53
- agents sessions --favorites
52
+ # Browse only the bookmarked ones
53
+ agents sessions --bookmarks
54
54
 
55
- # Remove it from favorites again
56
- agents sessions favorite 26c27162 --remove
55
+ # Remove it from bookmarks again
56
+ agents sessions bookmark 26c27162 --remove
57
57
  `,
58
58
  notes: `
59
- In the interactive browser (\`agents sessions\`), \`*\` favorites the highlighted
60
- session and \`f\` filters the list down to the favorited ones.
59
+ In the interactive browser (\`agents sessions\`), \`*\` bookmarks the highlighted
60
+ session and \`b\` filters the list down to the bookmarked ones.
61
61
 
62
- Favorites live in ~/.agents/.history/favorites.json, keyed by session id, so
62
+ Bookmarks live in ~/.agents/.history/bookmarks.json, keyed by session id, so
63
63
  they survive a reindex of the session cache. They are per-machine: session
64
64
  sync carries transcripts, not this file.
65
65
  `,
@@ -74,32 +74,32 @@ export function registerSessionsFavoriteCommand(sessionsCmd) {
74
74
  // owns; it is still declared on this command so `--help` documents it.
75
75
  const json = self.optsWithGlobals().json === true;
76
76
  if (options.list || ids.length === 0) {
77
- const favorited = [...listFavorites()].sort();
77
+ const bookmarked = [...listBookmarks()].sort();
78
78
  if (json) {
79
- process.stdout.write(JSON.stringify({ favorites: favorited }, null, 2) + '\n');
79
+ process.stdout.write(JSON.stringify({ bookmarks: bookmarked }, null, 2) + '\n');
80
80
  return;
81
81
  }
82
- if (favorited.length === 0) {
83
- console.log(chalk.gray('No favorited sessions. Favorite one with `agents sessions favorite <id>`.'));
82
+ if (bookmarked.length === 0) {
83
+ console.log(chalk.gray('No bookmarked sessions. Bookmark one with `agents sessions bookmark <id>`.'));
84
84
  return;
85
85
  }
86
- for (const id of favorited)
86
+ for (const id of bookmarked)
87
87
  console.log(`${chalk.yellow('★')} ${id}`);
88
- console.log(chalk.gray(`\n${favorited.length} favorite${favorited.length === 1 ? '' : 's'}.`));
88
+ console.log(chalk.gray(`\n${bookmarked.length} bookmark${bookmarked.length === 1 ? '' : 's'}.`));
89
89
  return;
90
90
  }
91
91
  const on = !options.remove;
92
92
  const results = [];
93
93
  for (const idQuery of ids) {
94
- const resolved = resolveFavoriteTarget(idQuery);
94
+ const resolved = resolveBookmarkTarget(idQuery);
95
95
  if ('error' in resolved) {
96
96
  results.push({ query: idQuery, error: resolved.error });
97
97
  continue;
98
98
  }
99
- // Unfavoriting something that was never favorited, or favoriting it twice,
99
+ // Removing a bookmark that does not exist, or bookmarking it twice,
100
100
  // is a no-op the store already short-circuits — report the resulting state.
101
- setFavorite(resolved.id, on);
102
- results.push({ query: idQuery, id: resolved.id, favorite: isFavorite(resolved.id) });
101
+ setBookmark(resolved.id, on);
102
+ results.push({ query: idQuery, id: resolved.id, bookmark: isBookmarked(resolved.id) });
103
103
  }
104
104
  if (json) {
105
105
  process.stdout.write(JSON.stringify({ results }, null, 2) + '\n');
@@ -109,11 +109,11 @@ export function registerSessionsFavoriteCommand(sessionsCmd) {
109
109
  if (r.error)
110
110
  console.error(chalk.red(r.error));
111
111
  else
112
- console.log(`${r.favorite ? chalk.yellow('★ favorited') : chalk.gray('☆ unfavorited')} ${r.id}`);
112
+ console.log(`${r.bookmark ? chalk.yellow('★ bookmarked') : chalk.gray('☆ unbookmarked')} ${r.id}`);
113
113
  }
114
114
  }
115
- // A failed lookup is a failed command — a script must not read "favorited"
116
- // from a zero exit when nothing was favorited.
115
+ // A failed lookup is a failed command — a script must not read "bookmarked"
116
+ // from a zero exit when nothing was bookmarked.
117
117
  if (results.some((r) => r.error))
118
118
  process.exitCode = 1;
119
119
  });
@@ -27,8 +27,8 @@ export interface BrowserFilter {
27
27
  device?: string;
28
28
  /** filter to one team's lineage, or all — the `T` key / `--in-team`. */
29
29
  team?: string;
30
- /** favorited-only — the `f` key / `--favorites`. */
31
- favorites: boolean;
30
+ /** bookmarked-only — the `b` key / `--bookmarks`. */
31
+ bookmarks: boolean;
32
32
  /** this-repo subtree vs every directory — the `P` key / `--all`. */
33
33
  projectScope: 'repo' | 'all';
34
34
  /** time window (undefined = all time) — the `W` key / `--since`. */
@@ -39,8 +39,8 @@ export interface BrowserFilter {
39
39
  project?: string;
40
40
  /** upper time bound from --until. */
41
41
  until?: string;
42
- /** retain only routine-origin sessions. */
43
- routine: boolean;
42
+ /** retain only routine-origin sessions, optionally narrowed to one routine. */
43
+ routine: boolean | string;
44
44
  /** indexed resource filters shared by the flag and focus surfaces. */
45
45
  skill?: string;
46
46
  plugin?: string;
@@ -97,7 +97,8 @@ export declare function activeBrowserSeed(opts: {
97
97
  host?: string[];
98
98
  since?: string;
99
99
  all?: boolean;
100
- favorites?: boolean;
100
+ bookmarks?: boolean;
101
+ routine?: boolean | string;
101
102
  }): Partial<BrowserFilter>;
102
103
  /**
103
104
  * The initial filter for the bare interactive listing: current-repo subtree by
@@ -113,7 +114,8 @@ export declare function bareBrowserSeed(opts: {
113
114
  since?: string;
114
115
  host?: string[];
115
116
  inTeam?: string;
116
- favorites?: boolean;
117
+ bookmarks?: boolean;
118
+ routine?: boolean | string;
117
119
  }): Partial<BrowserFilter>;
118
120
  /** Build the static peer query. Keeping it pure pins the per-device alias
119
121
  * contract: latest/oldest travel unresolved and are resolved by each peer. */
@@ -156,8 +158,8 @@ export declare function mergeLiveIntoPool(rows: SessionMeta[], live: Map<string,
156
158
  * listing that has no live rows to explain it.
157
159
  */
158
160
  export declare function shouldShowHostColumn(f: BrowserFilter, live: Map<string, ActiveSession> | null, rows: SessionMeta[]): boolean;
159
- /** Apply the cheap in-memory filters (agent / device / project / running / favorites). */
160
- export declare function applyFilters(rows: SessionMeta[], live: Map<string, ActiveSession>, f: BrowserFilter, self: string, favorites: Set<string>): SessionMeta[];
161
+ /** Apply the cheap in-memory filters (agent / device / project / running / bookmarks). */
162
+ export declare function applyFilters(rows: SessionMeta[], live: Map<string, ActiveSession>, f: BrowserFilter, self: string, bookmarks: Set<string>): SessionMeta[];
161
163
  /** One-shot form of the browser's canonical candidate pipeline. Focus uses this
162
164
  * instead of maintaining a second discovery/filter implementation. */
163
165
  export declare function collectSessionCandidates(initial: Partial<BrowserFilter>, opts?: {
@@ -19,10 +19,10 @@ import { gatherRemoteList } from '../lib/session/remote-list.js';
19
19
  import { resolveVersionAliasLoose } from '../lib/versions.js';
20
20
  import { AGENTS } from '../lib/agents.js';
21
21
  import { enrichTeamOrigins, safeTeamText } from '../lib/session/team-filter.js';
22
- import { listFavorites, toggleFavorite } from '../lib/session/favorites.js';
22
+ import { listBookmarks, toggleBookmark } from '../lib/session/bookmarks.js';
23
23
  import { machineId, normalizeHost } from '../lib/session/sync/config.js';
24
24
  import { buildPreview } from './sessions-picker.js';
25
- import { formatPickerLabel, pickerColumnsFor, ticketLabel, mergeLocalFirst, gatherActiveSessions, liveHostLabel, LIVE_ROW_PREFIX, cleanPreview, handlePickedSession, shouldIncludeLocal, remoteHostsToDial, matchesTeam, formatLiveStatusHeadline, isRunningLiveSession, matchesLiveStatus, parseAgentFilter, } from './sessions.js';
25
+ import { formatPickerLabel, pickerColumnsFor, ticketLabel, mergeLocalFirst, gatherActiveSessions, liveHostLabel, LIVE_ROW_PREFIX, cleanPreview, handlePickedSession, shouldIncludeLocal, remoteHostsToDial, matchesTeam, formatLiveStatusHeadline, isRunningLiveSession, matchesLiveStatus, parseAgentFilter, resolveRoutineName, } from './sessions.js';
26
26
  /**
27
27
  * Complete a seed into the filter the picker actually runs on.
28
28
  *
@@ -40,7 +40,7 @@ export function buildInitialFilter(initial) {
40
40
  return {
41
41
  running: initial.running ?? false,
42
42
  teams: initial.teams ?? false,
43
- favorites: initial.favorites ?? false,
43
+ bookmarks: initial.bookmarks ?? false,
44
44
  agent: initial.agent,
45
45
  device: initial.device,
46
46
  team: initial.team,
@@ -128,8 +128,8 @@ export function browserFilterToArgv(f, query = '') {
128
128
  a.push(`--${status === 'orphaned' ? 'orphan' : status}`);
129
129
  if (f.teams)
130
130
  a.push('--teams');
131
- if (f.favorites)
132
- a.push('--favorites');
131
+ if (f.bookmarks)
132
+ a.push('--bookmarks');
133
133
  if (f.agent)
134
134
  a.push('-a', f.agent);
135
135
  if (f.device)
@@ -144,8 +144,11 @@ export function browserFilterToArgv(f, query = '') {
144
144
  a.push('--until', f.until);
145
145
  if (f.project)
146
146
  a.push('--project', f.project);
147
- if (f.routine)
147
+ if (f.routine) {
148
148
  a.push('--routine');
149
+ if (typeof f.routine === 'string')
150
+ a.push(f.routine);
151
+ }
149
152
  if (f.skill)
150
153
  a.push('--skill', f.skill);
151
154
  if (f.plugin)
@@ -179,7 +182,8 @@ export function activeBrowserSeed(opts) {
179
182
  return {
180
183
  running: true,
181
184
  teams: !!opts.teams,
182
- favorites: !!opts.favorites,
185
+ bookmarks: !!opts.bookmarks,
186
+ routine: opts.routine ?? false,
183
187
  agent: opts.agent,
184
188
  projectScope: 'all',
185
189
  device: normalizeDeviceSeed(opts.host?.[0]),
@@ -211,7 +215,8 @@ export function bareBrowserSeed(opts) {
211
215
  const wholeTeam = !!opts.inTeam;
212
216
  return {
213
217
  teams: !!opts.teams,
214
- favorites: !!opts.favorites,
218
+ bookmarks: !!opts.bookmarks,
219
+ routine: opts.routine ?? false,
215
220
  agent: opts.agent,
216
221
  // The filter carries one device; seed it only when the scope names exactly
217
222
  // one, so a two-device scope isn't narrowed to the first of them.
@@ -339,8 +344,11 @@ export function remotePoolArgs(f, fixedFilters) {
339
344
  forwarded.push('--until', f.until);
340
345
  if (f.project)
341
346
  forwarded.push('--project', f.project);
342
- if (f.routine)
347
+ if (f.routine) {
343
348
  forwarded.push('--routine');
349
+ if (typeof f.routine === 'string')
350
+ forwarded.push(f.routine);
351
+ }
344
352
  if (f.skill)
345
353
  forwarded.push('--skill', f.skill);
346
354
  if (f.plugin)
@@ -417,6 +425,8 @@ export function liveSessionToMeta(a, self) {
417
425
  prNumber: a.pr?.number,
418
426
  ticketId: a.ticket?.id,
419
427
  worktreeSlug: a.worktree?.slug,
428
+ origin: a.origin,
429
+ routineName: a.routineName,
420
430
  };
421
431
  }
422
432
  /**
@@ -453,14 +463,22 @@ export function shouldShowHostColumn(f, live, rows) {
453
463
  return false;
454
464
  return rows.some((r) => liveHostLabel(live.get(r.id)) !== '');
455
465
  }
456
- /** Apply the cheap in-memory filters (agent / device / project / running / favorites). */
457
- export function applyFilters(rows, live, f, self, favorites) {
466
+ /** Apply the cheap in-memory filters (agent / device / project / running / bookmarks). */
467
+ export function applyFilters(rows, live, f, self, bookmarks) {
458
468
  let out = rows;
459
469
  // A projected live row is keyed by pid/task when it has no session id, and a
460
- // favorite is always keyed by a real session id — so an id-less row can never
461
- // be favorited and correctly drops out here.
462
- if (f.favorites)
463
- out = out.filter((r) => favorites.has(r.id));
470
+ // A bookmark is always keyed by a real session id — so an id-less row can
471
+ // never be bookmarked and correctly drops out here.
472
+ if (f.bookmarks)
473
+ out = out.filter((r) => bookmarks.has(r.id));
474
+ if (f.routine) {
475
+ out = out.filter((r) => r.origin === 'routine' || !!r.routineName);
476
+ if (typeof f.routine === 'string') {
477
+ const routineNames = distinct(out.map((r) => r.routineName));
478
+ const selected = resolveRoutineName(f.routine, routineNames);
479
+ out = selected ? out.filter((r) => r.routineName === selected) : [];
480
+ }
481
+ }
464
482
  if (f.agent) {
465
483
  const { agent, version: rawVersion } = parseAgentFilter(f.agent);
466
484
  const localVersion = agent && agent in AGENTS
@@ -527,7 +545,7 @@ export async function collectSessionCandidates(initial, opts = {}) {
527
545
  const rows = filter.running || opts.includeLive
528
546
  ? mergeLiveIntoPool(pool.rows, liveById, self, includeUnindexedLive)
529
547
  : pool.rows;
530
- const sessions = applyFilters(rows, liveById, filter, self, listFavorites());
548
+ const sessions = applyFilters(rows, liveById, filter, self, listBookmarks());
531
549
  return { sessions, liveById, self, unreachable: pool.unreachable };
532
550
  }
533
551
  /** Derive the SSH-launch origin tag for a picker row from the live index. Set
@@ -552,15 +570,17 @@ function headerFor(f) {
552
570
  bits.push('running');
553
571
  if (f.teams)
554
572
  bits.push('teams');
555
- if (f.favorites)
556
- bits.push('favorites');
573
+ if (f.routine)
574
+ bits.push(`routine:${typeof f.routine === 'string' ? f.routine : 'all'}`);
575
+ if (f.bookmarks)
576
+ bits.push('bookmarks');
557
577
  return bits.join(' · ');
558
578
  }
559
579
  function helpFor(_f, mode) {
560
580
  if (mode === 'search') {
561
581
  return 'type to filter · ↑↓ navigate · esc exit search · ⏎ resume';
562
582
  }
563
- return 's search · r running · f favorites · * favorite · c teams · t team · a agent · d device · p project · w window · tab preview · y copy-cmd · ⏎ resume · esc quit';
583
+ return 's search · r running · b bookmarks · * bookmark · f focus · c teams · t team · a agent · d device · p project · w window · tab preview · y copy-cmd · ⏎ resume · esc quit';
564
584
  }
565
585
  /**
566
586
  * Launch the interactive session browser. `initial` seeds the filter (e.g.
@@ -588,10 +608,11 @@ export async function runSessionBrowser(initial = {}, opts = {}) {
588
608
  // The live index is slow (a full ps/tmux scan) and only the running filter
589
609
  // needs it — fetch it once, lazily, the first time running is toggled on.
590
610
  let liveCache = null;
611
+ const liveFor = (id) => liveCache?.get(id);
591
612
  // Re-read every load (it's an mtime-memoized parse of one small file), so the
592
- // `*` key's reload picks up the favorite it just wrote — and so does one
593
- // favorited by another session on this machine.
594
- let favorites = new Set();
613
+ // `*` key's reload picks up the bookmark it just wrote — and so does one
614
+ // bookmarked by another session on this machine.
615
+ let bookmarks = new Set();
595
616
  // Generation guard: two quick keypresses can start overlapping loads whose
596
617
  // SSH fan-outs settle out of order. dynamicPicker's own gen ref guards which
597
618
  // rows become `items`, but the shared closure state below (cols / cycle pools /
@@ -652,8 +673,8 @@ export async function runSessionBrowser(initial = {}, opts = {}) {
652
673
  ...rows.map((r) => safeTeamText(r.spawnedTeam)),
653
674
  ...rows.map((r) => safeTeamText(r.teamOrigin?.team)),
654
675
  ]);
655
- favorites = listFavorites();
656
- const filtered = applyFilters(rows, live ?? new Map(), f, self, favorites);
676
+ bookmarks = listBookmarks();
677
+ const filtered = applyFilters(rows, live ?? new Map(), f, self, bookmarks);
657
678
  cols = pickerColumnsFor(filtered);
658
679
  cols.showHost = shouldShowHostColumn(f, live, filtered);
659
680
  // Status rides the same gate as the host column: both come from the live
@@ -667,14 +688,14 @@ export async function runSessionBrowser(initial = {}, opts = {}) {
667
688
  initialFilter,
668
689
  load,
669
690
  keyFor: (s) => s.id,
670
- labelFor: (s, q) => formatPickerLabel(s, q, cols, sshOriginTagFor(liveCache, s.id), liveHostLabel(liveCache?.get(s.id)), favorites.has(s.id), liveCache?.get(s.id)),
691
+ labelFor: (s, q) => formatPickerLabel(s, q, cols, sshOriginTagFor(liveCache, s.id), liveHostLabel(liveCache?.get(s.id)), bookmarks.has(s.id), liveCache?.get(s.id)),
671
692
  matches: sessionMatchesQuery,
672
693
  // Lead the preview with the live status banner — the one place a `crashed` /
673
694
  // `orphaned` session gets a sentence instead of a glyph. `buildPreview` is
674
695
  // memoized per session, so the volatile live half is prepended here rather
675
696
  // than baked into the cached body.
676
697
  buildPreview: (s) => {
677
- const headline = formatLiveStatusHeadline(liveCache?.get(s.id), favorites.has(s.id));
698
+ const headline = formatLiveStatusHeadline(liveCache?.get(s.id), bookmarks.has(s.id));
678
699
  const body = buildPreview(s);
679
700
  return headline ? `${headline}\n${body}` : body;
680
701
  },
@@ -685,9 +706,10 @@ export async function runSessionBrowser(initial = {}, opts = {}) {
685
706
  enterHint: 'resume',
686
707
  emptyMessage: 'No sessions match this filter.',
687
708
  loadingMessage: local ? 'Loading…' : 'Loading (reaching other machines)…',
709
+ submitKeys: { f: 'focus' },
688
710
  keyBindings: {
689
711
  r: (f) => ({ ...f, running: !f.running }),
690
- f: (f) => ({ ...f, favorites: !f.favorites }),
712
+ b: (f) => ({ ...f, bookmarks: !f.bookmarks }),
691
713
  c: (f) => ({ ...f, teams: !f.teams }),
692
714
  a: (f) => ({ ...f, agent: cycle(f.agent, agentsInPool) }),
693
715
  d: (f) => ({ ...f, device: cycle(f.device, devicesInPool) }),
@@ -700,13 +722,13 @@ export async function runSessionBrowser(initial = {}, opts = {}) {
700
722
  },
701
723
  onKey: (name, f, active, query) => {
702
724
  if (name === '*') {
703
- // Only a row with a real session id can be favorited: a projected live row
725
+ // Only a row with a real session id can be bookmarked: a projected live row
704
726
  // with no id is keyed by pid, which is gone the moment the process is.
705
727
  if (!active || active.id.startsWith(LIVE_ROW_PREFIX))
706
- return 'nothing to favorite on this row';
707
- const on = toggleFavorite(active.id);
708
- // reload so the row's favorite glyph is repainted — labels are memoized per row.
709
- return { flash: on ? `★ favorited ${active.shortId}` : `☆ unfavorited ${active.shortId}`, reload: true };
728
+ return 'nothing to bookmark on this row';
729
+ const on = toggleBookmark(active.id);
730
+ // Reload so the row's bookmark glyph is repainted — labels are memoized per row.
731
+ return { flash: on ? `★ bookmarked ${active.shortId}` : `☆ unbookmarked ${active.shortId}`, reload: true };
710
732
  }
711
733
  // Both cases: `hotkeyToken` hands `onKey` the literal character, and this
712
734
  // key worked with caps lock on before it existed.
@@ -722,5 +744,12 @@ export async function runSessionBrowser(initial = {}, opts = {}) {
722
744
  });
723
745
  if (!picked)
724
746
  return;
747
+ if (picked.action === 'focus') {
748
+ // `focus.ts` consumes this browser for its selector path, so import only
749
+ // after selection to keep the shared picker/focus dependency acyclic.
750
+ const { focusSelectedSession } = await import('./focus.js');
751
+ await focusSelectedSession(picked.item, liveFor(picked.item.id), self);
752
+ return;
753
+ }
725
754
  await handlePickedSession({ session: picked.item, action: 'resume' });
726
755
  }
@@ -95,7 +95,7 @@ async function statsAction(cmd) {
95
95
  // The parent `sessions` command owns --agent/--project/--plugin/--since/--json
96
96
  // and keeps parsing them past the subcommand name (it has a positional
97
97
  // [query]), binding them to the PARENT — so read the merged view, not the
98
- // action's own options. Same reason sessions-favorite.ts uses optsWithGlobals.
98
+ // action's own options. Same reason sessions-bookmark.ts uses optsWithGlobals.
99
99
  const opts = cmd.optsWithGlobals();
100
100
  const kind = normalizeKind(opts.kind);
101
101
  const sinceMs = opts.since ? parseTimeFilter(opts.since) : undefined;
@@ -59,8 +59,8 @@ interface SessionsOptions extends SessionFilterOptions {
59
59
  abandoned?: boolean;
60
60
  queued?: boolean;
61
61
  unknown?: boolean;
62
- /** Show only favorited sessions — the `f` key's flag twin. */
63
- favorites?: boolean;
62
+ /** Show only bookmarked sessions — the `b` key's flag twin. */
63
+ bookmarks?: boolean;
64
64
  /** Enrich the listing with live glyphs/preview for running rows. Default on;
65
65
  * `--no-live` sets this false. Commander's `--no-` convention. */
66
66
  live?: boolean;
@@ -399,6 +399,8 @@ export declare function gatherActiveSessions(opts?: {
399
399
  sessions: ActiveSession[];
400
400
  remoteDeviceCount: number;
401
401
  }>;
402
+ /** Apply the routine selector to enriched live rows for every non-browser active view. */
403
+ export declare function filterActiveSessionsByRoutine(sessions: ActiveSession[], routine: boolean | string | undefined): ActiveSession[];
402
404
  export type LiveStatusFilter = 'working' | 'idle' | 'waiting' | 'orphaned' | 'crashed' | 'closed' | 'abandoned' | 'queued' | 'unknown';
403
405
  /** Match the status words users see, preserving activity's richer working signal. */
404
406
  export declare function matchesLiveStatus(session: ActiveSession, status: LiveStatusFilter): boolean;
@@ -432,7 +434,7 @@ export interface RoutineChoice {
432
434
  latestRunSessionCount: number;
433
435
  }
434
436
  export declare function buildRoutineRunGroups(sessions: SessionMeta[]): RoutineRunGroup[];
435
- export declare function buildRoutineChoices(sessions: SessionMeta[]): RoutineChoice[];
437
+ export declare function buildRoutineChoices(sessions: SessionMeta[], runOnlyNames?: string[]): RoutineChoice[];
436
438
  export declare function filterSessionsByRoutine(sessions: SessionMeta[], routine: boolean | string, interactive: boolean): Promise<SessionMeta[] | null>;
437
439
  /** Resolve a session by id/query globally and print its compact preview (no pager).
438
440
  * Backs `--preview` — the fast path for the "peek before resume" hot loop. */
@@ -453,7 +455,7 @@ export declare function renderSessionPreview(query: string, scope: {
453
455
  * reads "orphan" and knows it means "still running in tmux with no window
454
456
  * attached", so those two spell it out.
455
457
  */
456
- export declare function formatLiveStatusHeadline(live: ActiveSession | undefined, favorite?: boolean): string;
458
+ export declare function formatLiveStatusHeadline(live: ActiveSession | undefined, bookmarked?: boolean): string;
457
459
  /** Merge local and peer envelopes without changing the versioned JSON shape. */
458
460
  export declare function mergeToolSearchEnvelopes(local: ToolSearchEnvelope, remotes: ToolSearchEnvelope[]): ToolSearchEnvelope;
459
461
  export declare function mergeToolProgramCountEnvelopes(local: ToolProgramCountEnvelope, remotes: ToolProgramCountEnvelope[]): ToolProgramCountEnvelope;
@@ -487,7 +489,7 @@ export declare function teamBadge(session: SessionMeta): {
487
489
  * (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
488
490
  * when `showTicket` — otherwise a listing with no refs would waste a column of
489
491
  * dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
490
- export declare function flatSessionRow(session: SessionMeta, live?: ActiveSession, showTicket?: boolean, cols?: PickerColumns, favorite?: boolean): string;
492
+ export declare function flatSessionRow(session: SessionMeta, live?: ActiveSession, showTicket?: boolean, cols?: PickerColumns, bookmarked?: boolean): string;
491
493
  /**
492
494
  * Live-session index for enriching the default listing, or undefined when
493
495
  * enrichment is off (`--no-live`) or irrelevant (`--json`, which serializes
@@ -571,11 +573,11 @@ export interface PickerColumns {
571
573
  */
572
574
  showHost?: boolean;
573
575
  /**
574
- * Render the favorite marker column. Like every other conditional column here,
576
+ * Render the bookmark marker column. Like every other conditional column here,
575
577
  * it earns its 2 cells only when some row in the pool is actually starred — a
576
- * user who has never favorited anything pays nothing for the feature.
578
+ * user who has never bookmarked anything pays nothing for the feature.
577
579
  */
578
- showFavorite?: boolean;
580
+ showBookmark?: boolean;
579
581
  /**
580
582
  * Render the live status column (`working` / `waiting` / `orphan` / `crashed`).
581
583
  * Live-only, gated the same way as {@link showHost}: it comes from the
@@ -612,7 +614,7 @@ export declare function pickerColumnsFor(sessions: SessionMeta[]): PickerColumns
612
614
  * resolvable host (cloud rows, an unreadable process env).
613
615
  */
614
616
  export declare function liveHostLabel(a: ActiveSession | undefined): string;
615
- export declare function formatPickerLabel(s: SessionMeta, query: string, cols?: PickerColumns, ssh?: SshOriginTag, host?: string, favorite?: boolean, live?: ActiveSession): string;
617
+ export declare function formatPickerLabel(s: SessionMeta, query: string, cols?: PickerColumns, ssh?: SshOriginTag, host?: string, bookmarked?: boolean, live?: ActiveSession): string;
616
618
  /**
617
619
  * Pick a hint to show above the picker. Deterministic (keys off the pool size)
618
620
  * so it stays fixed across the picker's re-renders within a single run.