@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.
- package/CHANGELOG.md +72 -0
- package/README.md +13 -6
- package/dist/bin/agents +0 -0
- package/dist/commands/daemon.js +52 -12
- package/dist/commands/doctor.d.ts +19 -0
- package/dist/commands/doctor.js +119 -17
- package/dist/commands/focus.d.ts +4 -1
- package/dist/commands/focus.js +19 -4
- package/dist/commands/routines.js +164 -36
- package/dist/commands/secrets.d.ts +20 -0
- package/dist/commands/secrets.js +59 -93
- package/dist/commands/{sessions-favorite.d.ts → sessions-bookmark.d.ts} +7 -7
- package/dist/commands/{sessions-favorite.js → sessions-bookmark.js} +38 -38
- package/dist/commands/sessions-browser.d.ts +10 -8
- package/dist/commands/sessions-browser.js +61 -32
- package/dist/commands/sessions-stats.js +1 -1
- package/dist/commands/sessions.d.ts +11 -9
- package/dist/commands/sessions.js +114 -65
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +148 -0
- package/dist/index.js +4 -2
- package/dist/lib/catchup.js +4 -1
- package/dist/lib/daemon.d.ts +17 -0
- package/dist/lib/daemon.js +69 -3
- package/dist/lib/devices/doctor-findings.d.ts +7 -2
- package/dist/lib/devices/doctor-findings.js +53 -2
- package/dist/lib/devices/doctor-overview-cache.d.ts +7 -0
- package/dist/lib/devices/doctor-overview-cache.js +15 -0
- package/dist/lib/devices/fleet-divergence.d.ts +11 -0
- package/dist/lib/devices/fleet-divergence.js +6 -0
- package/dist/lib/devices/fleet-inventory.js +16 -2
- package/dist/lib/drift.d.ts +6 -1
- package/dist/lib/drift.js +9 -0
- package/dist/lib/hooks/cache.js +20 -1
- package/dist/lib/hooks.d.ts +91 -1
- package/dist/lib/hooks.js +289 -3
- package/dist/lib/hosts/passthrough.js +3 -0
- package/dist/lib/installations/index.d.ts +14 -0
- package/dist/lib/installations/index.js +14 -0
- package/dist/lib/installations/resolve.d.ts +43 -0
- package/dist/lib/installations/resolve.js +93 -0
- package/dist/lib/installations/store.d.ts +56 -0
- package/dist/lib/installations/store.js +196 -0
- package/dist/lib/installations/strategies.d.ts +73 -0
- package/dist/lib/installations/strategies.js +293 -0
- package/dist/lib/installations/types.d.ts +78 -0
- package/dist/lib/installations/types.js +8 -0
- package/dist/lib/installations/update.d.ts +40 -0
- package/dist/lib/installations/update.js +131 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +27 -0
- package/dist/lib/migrate.js +118 -2
- package/dist/lib/picker.d.ts +6 -3
- package/dist/lib/picker.js +7 -2
- package/dist/lib/routine-context.d.ts +144 -0
- package/dist/lib/routine-context.js +268 -0
- package/dist/lib/routine-readiness.d.ts +47 -0
- package/dist/lib/routine-readiness.js +239 -0
- package/dist/lib/routines.d.ts +97 -1
- package/dist/lib/routines.js +107 -1
- package/dist/lib/runner.d.ts +18 -4
- package/dist/lib/runner.js +291 -98
- package/dist/lib/scheduler.d.ts +7 -1
- package/dist/lib/scheduler.js +5 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +0 -1
- package/dist/lib/secrets/agent.js +0 -4
- package/dist/lib/secrets/session-store.d.ts +0 -4
- package/dist/lib/secrets/session-store.js +0 -5
- package/dist/lib/self-heal/checks/hook-runtime.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-runtime.js +16 -0
- package/dist/lib/self-heal/registry.js +5 -2
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/{favorites.d.ts → bookmarks.d.ts} +14 -14
- package/dist/lib/session/{favorites.js → bookmarks.js} +22 -22
- package/dist/lib/session/state.js +4 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/versions.d.ts +24 -0
- package/dist/lib/versions.js +49 -16
- package/package.json +2 -2
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `agents sessions
|
|
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
|
|
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/
|
|
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 {
|
|
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:
|
|
17
|
-
* the wrong session is invisible until the user wonders where their
|
|
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
|
|
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 `*`
|
|
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
|
|
36
|
+
export function registerSessionsBookmarkCommand(sessionsCmd) {
|
|
37
37
|
const cmd = sessionsCmd
|
|
38
|
-
.command('
|
|
39
|
-
.argument('[ids...]', 'Session ids to
|
|
40
|
-
.description('
|
|
41
|
-
.option('--remove', 'Remove the given sessions from
|
|
42
|
-
.option('--list', 'List the
|
|
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
|
-
#
|
|
47
|
-
agents sessions
|
|
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
|
|
50
|
-
agents sessions
|
|
49
|
+
# See what is bookmarked
|
|
50
|
+
agents sessions bookmark --list
|
|
51
51
|
|
|
52
|
-
# Browse only the
|
|
53
|
-
agents sessions --
|
|
52
|
+
# Browse only the bookmarked ones
|
|
53
|
+
agents sessions --bookmarks
|
|
54
54
|
|
|
55
|
-
# Remove it from
|
|
56
|
-
agents sessions
|
|
55
|
+
# Remove it from bookmarks again
|
|
56
|
+
agents sessions bookmark 26c27162 --remove
|
|
57
57
|
`,
|
|
58
58
|
notes: `
|
|
59
|
-
In the interactive browser (\`agents sessions\`), \`*\`
|
|
60
|
-
session and \`
|
|
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
|
-
|
|
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
|
|
77
|
+
const bookmarked = [...listBookmarks()].sort();
|
|
78
78
|
if (json) {
|
|
79
|
-
process.stdout.write(JSON.stringify({
|
|
79
|
+
process.stdout.write(JSON.stringify({ bookmarks: bookmarked }, null, 2) + '\n');
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
|
-
if (
|
|
83
|
-
console.log(chalk.gray('No
|
|
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
|
|
86
|
+
for (const id of bookmarked)
|
|
87
87
|
console.log(`${chalk.yellow('★')} ${id}`);
|
|
88
|
-
console.log(chalk.gray(`\n${
|
|
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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
102
|
-
results.push({ query: idQuery, id: 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.
|
|
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 "
|
|
116
|
-
// from a zero exit when nothing was
|
|
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
|
-
/**
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 /
|
|
160
|
-
export declare function applyFilters(rows: SessionMeta[], live: Map<string, ActiveSession>, f: BrowserFilter, self: string,
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
132
|
-
a.push('--
|
|
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
|
-
|
|
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
|
-
|
|
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 /
|
|
457
|
-
export function applyFilters(rows, live, f, self,
|
|
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
|
-
//
|
|
461
|
-
// be
|
|
462
|
-
if (f.
|
|
463
|
-
out = out.filter((r) =>
|
|
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,
|
|
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.
|
|
556
|
-
bits.push('
|
|
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 ·
|
|
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
|
|
593
|
-
//
|
|
594
|
-
let
|
|
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
|
-
|
|
656
|
-
const filtered = applyFilters(rows, live ?? new Map(), f, self,
|
|
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)),
|
|
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),
|
|
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
|
-
|
|
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
|
|
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
|
|
707
|
-
const on =
|
|
708
|
-
//
|
|
709
|
-
return { flash: on ? `★
|
|
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-
|
|
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
|
|
63
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
578
|
+
* user who has never bookmarked anything pays nothing for the feature.
|
|
577
579
|
*/
|
|
578
|
-
|
|
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,
|
|
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.
|