@phnx-labs/agents-cli 1.22.30 → 1.22.31
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 +6 -0
- package/README.md +5 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/focus.d.ts +4 -1
- package/dist/commands/focus.js +19 -4
- 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 +10 -8
- package/dist/commands/sessions.js +70 -55
- package/dist/index.js +1 -1
- 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.js +6 -0
- package/dist/lib/picker.d.ts +6 -3
- package/dist/lib/picker.js +7 -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/session/{favorites.d.ts → bookmarks.d.ts} +14 -14
- package/dist/lib/session/{favorites.js → bookmarks.js} +22 -22
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -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.
|
|
@@ -55,8 +55,8 @@ import { setHelpSections } from '../lib/help.js';
|
|
|
55
55
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
56
56
|
import { registerSessionsResumeCommand } from './sessions-resume.js';
|
|
57
57
|
import { registerSessionsForkCommand } from './fork.js';
|
|
58
|
-
import {
|
|
59
|
-
import {
|
|
58
|
+
import { registerSessionsBookmarkCommand } from './sessions-bookmark.js';
|
|
59
|
+
import { isBookmarked, listBookmarks } from '../lib/session/bookmarks.js';
|
|
60
60
|
import { registerGoCommand } from './go.js';
|
|
61
61
|
import { registerFocusCommand } from './focus.js';
|
|
62
62
|
import { registerReconnectCommand } from './reconnect.js';
|
|
@@ -1361,26 +1361,25 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
1361
1361
|
const self = machineId();
|
|
1362
1362
|
const gathered = await gatherActiveSessions(opts);
|
|
1363
1363
|
const { remoteDeviceCount } = gathered;
|
|
1364
|
-
//
|
|
1364
|
+
// Backfill agent version, refs, created time, and routine provenance from the
|
|
1365
|
+
// historical index. Routine filtering needs that provenance before it narrows
|
|
1366
|
+
// the live pool; doing it here also enriches both JSON and human output.
|
|
1367
|
+
backfillActiveRowsFromIndex(gathered.sessions);
|
|
1368
|
+
// --bookmarks narrows the live view too. Applied HERE, not only in the
|
|
1365
1369
|
// browser: the browser is skipped for --json, --waiting, a pipe, a multi-host
|
|
1366
1370
|
// scope, and an SSH-fanout peer, and the flag silently did nothing on every
|
|
1367
|
-
// one of those paths — including `--active --
|
|
1371
|
+
// one of those paths — including `--active --bookmarks --json`, which is
|
|
1368
1372
|
// exactly what the browser's own `y` copy-cmd hands to an agent.
|
|
1369
|
-
const merged = opts.
|
|
1370
|
-
? gathered.sessions.filter((s) => !!s.sessionId &&
|
|
1373
|
+
const merged = opts.bookmarksOnly
|
|
1374
|
+
? gathered.sessions.filter((s) => !!s.sessionId && listBookmarks().has(s.sessionId))
|
|
1371
1375
|
: gathered.sessions;
|
|
1376
|
+
const routineFiltered = filterActiveSessionsByRoutine(merged, opts.routine);
|
|
1372
1377
|
// Status flags form a union. --waiting additionally retains its scriptable
|
|
1373
1378
|
// gate: exit non-zero when the union contains a session awaiting the user.
|
|
1374
1379
|
const statusFiltered = opts.statuses?.length
|
|
1375
|
-
?
|
|
1376
|
-
:
|
|
1380
|
+
? routineFiltered.filter((session) => opts.statuses.some((status) => matchesLiveStatus(session, status)))
|
|
1381
|
+
: routineFiltered.filter(isRunningLiveSession);
|
|
1377
1382
|
const sessions = statusFiltered;
|
|
1378
|
-
// Backfill agent version + ticket/PR/label/created onto the live rows from the
|
|
1379
|
-
// historical index (RUSH-2205) — a running process reports none of these, and
|
|
1380
|
-
// an orphan row usually lacks them. Done before both the JSON and human paths
|
|
1381
|
-
// so every consumer (incl. the SSH fan-out's remote --json) sees enriched rows;
|
|
1382
|
-
// transcripts sync across the fleet, so a remote row resolves from the local DB.
|
|
1383
|
-
backfillActiveRowsFromIndex(sessions);
|
|
1384
1383
|
if (asJson) {
|
|
1385
1384
|
// Resolve who is watching each local tmux pane before serializing: `viewingIn`
|
|
1386
1385
|
// is how a consumer distinguishes a session someone is looking at from one
|
|
@@ -1426,6 +1425,19 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
1426
1425
|
if (waitingOnly && sessions.some(isAwaitingUser))
|
|
1427
1426
|
process.exitCode = 1;
|
|
1428
1427
|
}
|
|
1428
|
+
/** Apply the routine selector to enriched live rows for every non-browser active view. */
|
|
1429
|
+
export function filterActiveSessionsByRoutine(sessions, routine) {
|
|
1430
|
+
if (!routine)
|
|
1431
|
+
return sessions;
|
|
1432
|
+
const routineSessions = sessions.filter((session) => session.origin === 'routine' || !!session.routineName);
|
|
1433
|
+
if (typeof routine !== 'string')
|
|
1434
|
+
return routineSessions;
|
|
1435
|
+
const names = [...new Set(routineSessions.map((session) => session.routineName).filter((name) => !!name))];
|
|
1436
|
+
const selected = resolveRoutineName(routine, names);
|
|
1437
|
+
return selected
|
|
1438
|
+
? routineSessions.filter((session) => session.routineName === selected)
|
|
1439
|
+
: [];
|
|
1440
|
+
}
|
|
1429
1441
|
/** Match the status words users see, preserving activity's richer working signal. */
|
|
1430
1442
|
export function matchesLiveStatus(session, status) {
|
|
1431
1443
|
if (status === 'working')
|
|
@@ -1492,7 +1504,9 @@ export function isBareBrowserListing(options, query) {
|
|
|
1492
1504
|
*/
|
|
1493
1505
|
export function hasNoBrowserDisqualifyingFlags(options, query) {
|
|
1494
1506
|
return (!query &&
|
|
1495
|
-
|
|
1507
|
+
// A named team view is a flat selectable pool; the bare --teams report
|
|
1508
|
+
// stays grouped and printed because the browser cannot preserve its shape.
|
|
1509
|
+
(!options.teams || !!options.inTeam) &&
|
|
1496
1510
|
!options.flat &&
|
|
1497
1511
|
!options.tree &&
|
|
1498
1512
|
!options.markdown &&
|
|
@@ -1657,8 +1671,8 @@ function canonicalSessionsCommand(query, options) {
|
|
|
1657
1671
|
a.push('--local');
|
|
1658
1672
|
if (options.waiting)
|
|
1659
1673
|
a.push('--waiting');
|
|
1660
|
-
if (options.
|
|
1661
|
-
a.push('--
|
|
1674
|
+
if (options.bookmarks)
|
|
1675
|
+
a.push('--bookmarks');
|
|
1662
1676
|
const q = (query ?? '').trim();
|
|
1663
1677
|
if (q)
|
|
1664
1678
|
a.push(JSON.stringify(q));
|
|
@@ -1768,7 +1782,7 @@ export async function renderSessionPreview(query, scope) {
|
|
|
1768
1782
|
}));
|
|
1769
1783
|
return;
|
|
1770
1784
|
}
|
|
1771
|
-
const headline = formatLiveStatusHeadline(live,
|
|
1785
|
+
const headline = formatLiveStatusHeadline(live, isBookmarked(session.id));
|
|
1772
1786
|
if (headline)
|
|
1773
1787
|
console.log(headline);
|
|
1774
1788
|
console.log(buildPreview(session));
|
|
@@ -1783,12 +1797,12 @@ export async function renderSessionPreview(query, scope) {
|
|
|
1783
1797
|
* reads "orphan" and knows it means "still running in tmux with no window
|
|
1784
1798
|
* attached", so those two spell it out.
|
|
1785
1799
|
*/
|
|
1786
|
-
export function formatLiveStatusHeadline(live,
|
|
1787
|
-
const star =
|
|
1800
|
+
export function formatLiveStatusHeadline(live, bookmarked = false) {
|
|
1801
|
+
const star = bookmarked ? chalk.yellow('★ ') : '';
|
|
1788
1802
|
// With no live row there is no status to lead with, so the star has to say
|
|
1789
1803
|
// what it means on its own — a bare `★` above a preview reads as noise.
|
|
1790
1804
|
if (!live)
|
|
1791
|
-
return
|
|
1805
|
+
return bookmarked ? chalk.yellow('★ bookmarked') : '';
|
|
1792
1806
|
const { glyph } = liveGlyphAndPreview(live);
|
|
1793
1807
|
const word = liveStatusWord(live) || live.status;
|
|
1794
1808
|
// One definition, shared with `--waiting`. A second local copy drifted: the
|
|
@@ -2140,7 +2154,8 @@ limitSource) {
|
|
|
2140
2154
|
host: options.host,
|
|
2141
2155
|
since: options.since,
|
|
2142
2156
|
all: options.all,
|
|
2143
|
-
|
|
2157
|
+
bookmarks: options.bookmarks,
|
|
2158
|
+
routine: options.routine,
|
|
2144
2159
|
}), { local: options.local === true, hosts: options.host });
|
|
2145
2160
|
return;
|
|
2146
2161
|
}
|
|
@@ -2150,8 +2165,9 @@ limitSource) {
|
|
|
2150
2165
|
await renderActiveSessions(options.json === true, options.waiting === true, {
|
|
2151
2166
|
local: forceLocal,
|
|
2152
2167
|
hosts: options.host,
|
|
2153
|
-
|
|
2168
|
+
bookmarksOnly: options.bookmarks === true,
|
|
2154
2169
|
statuses: liveStatuses,
|
|
2170
|
+
routine: options.routine,
|
|
2155
2171
|
});
|
|
2156
2172
|
return;
|
|
2157
2173
|
}
|
|
@@ -2165,12 +2181,10 @@ limitSource) {
|
|
|
2165
2181
|
// printed/render paths (agents and scripts unaffected). An explicit --since seeds
|
|
2166
2182
|
// the browser's window so the flag is honored, not swallowed.
|
|
2167
2183
|
//
|
|
2168
|
-
// `--teams` diverts to the printed team-grouped report
|
|
2169
|
-
//
|
|
2170
|
-
//
|
|
2171
|
-
|
|
2172
|
-
// the picker so `<term> --teams` still searches.
|
|
2173
|
-
if (isBareBrowserListing(options, query) && !options.teams) {
|
|
2184
|
+
// Bare `--teams` still diverts to the printed team-grouped report because the
|
|
2185
|
+
// flat picker cannot preserve that shape. `--in-team <name> --teams` is one
|
|
2186
|
+
// flat lineage, so it qualifies through hasNoBrowserDisqualifyingFlags.
|
|
2187
|
+
if (isBareBrowserListing(options, query)) {
|
|
2174
2188
|
const { runSessionBrowser, bareBrowserSeed } = await import('./sessions-browser.js');
|
|
2175
2189
|
await runSessionBrowser(bareBrowserSeed({
|
|
2176
2190
|
teams: options.teams,
|
|
@@ -2179,7 +2193,8 @@ limitSource) {
|
|
|
2179
2193
|
since: options.since,
|
|
2180
2194
|
host: options.host,
|
|
2181
2195
|
inTeam: options.inTeam,
|
|
2182
|
-
|
|
2196
|
+
bookmarks: options.bookmarks,
|
|
2197
|
+
routine: options.routine,
|
|
2183
2198
|
}), { local: options.local === true, hosts: options.host });
|
|
2184
2199
|
return;
|
|
2185
2200
|
}
|
|
@@ -2349,11 +2364,11 @@ limitSource) {
|
|
|
2349
2364
|
// read. Match either, after that pass has populated `teamOrigin`.
|
|
2350
2365
|
if (options.inTeam)
|
|
2351
2366
|
sessions = sessions.filter((s) => matchesTeam(s, options.inTeam));
|
|
2352
|
-
// --
|
|
2353
|
-
// emit, so `--
|
|
2354
|
-
if (options.
|
|
2355
|
-
const
|
|
2356
|
-
sessions = sessions.filter((s) =>
|
|
2367
|
+
// --bookmarks narrows to the bookmarked set. Applied here, before the JSON
|
|
2368
|
+
// emit, so `--bookmarks --json` is the machine-readable twin of the `b` key.
|
|
2369
|
+
if (options.bookmarks) {
|
|
2370
|
+
const bookmarks = listBookmarks();
|
|
2371
|
+
sessions = sessions.filter((s) => bookmarks.has(s.id));
|
|
2357
2372
|
}
|
|
2358
2373
|
if (toolEvidenceMode) {
|
|
2359
2374
|
const self = toolSelf;
|
|
@@ -2659,7 +2674,7 @@ function timeCell(age, topicSlack) {
|
|
|
2659
2674
|
* (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
|
|
2660
2675
|
* when `showTicket` — otherwise a listing with no refs would waste a column of
|
|
2661
2676
|
* dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
|
|
2662
|
-
export function flatSessionRow(session, live, showTicket = false, cols = {},
|
|
2677
|
+
export function flatSessionRow(session, live, showTicket = false, cols = {}, bookmarked = false) {
|
|
2663
2678
|
const agentColor = colorAgent(session.agent);
|
|
2664
2679
|
const age = sessionAgeParts(session.timestamp, session.lastActivity);
|
|
2665
2680
|
const project = session.project || '-';
|
|
@@ -2696,18 +2711,18 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}, fav
|
|
|
2696
2711
|
const width = terminalWidth();
|
|
2697
2712
|
const requestedModelW = cols.showModel ? (cols.modelWidth ?? PICKER_MODEL_MAX) : 0;
|
|
2698
2713
|
// Same conditional 2 cells as the picker's marker, for the same reason.
|
|
2699
|
-
const
|
|
2700
|
-
const
|
|
2714
|
+
const bookmarkW = cols.showBookmark ? 2 : 0;
|
|
2715
|
+
const bookmarkCell = cols.showBookmark ? (bookmarked ? chalk.yellow('★ ') : ' ') : '';
|
|
2701
2716
|
// Sized against the last-activity label alone, so the creation field is an
|
|
2702
2717
|
// additive decision the row makes only once it knows what space is left.
|
|
2703
|
-
const fixedW =
|
|
2718
|
+
const fixedW = bookmarkW + (10 + 9 + 8 + 16) + glyphW + statusW + machineW + ticketW + wtW + team.width + stringWidth(age.last) + 1;
|
|
2704
2719
|
const modelSlack = width - fixedW - MIN_TOPIC_W;
|
|
2705
2720
|
const modelW = requestedModelW <= modelSlack
|
|
2706
2721
|
? requestedModelW
|
|
2707
2722
|
: modelSlack >= PICKER_MODEL_MIN ? modelSlack : 0;
|
|
2708
2723
|
const when = timeCell(age, width - fixedW - modelW);
|
|
2709
2724
|
const topicW = Math.max(MIN_TOPIC_W, width - fixedW - modelW - when.extraW);
|
|
2710
|
-
return (
|
|
2725
|
+
return (bookmarkCell +
|
|
2711
2726
|
chalk.white(padToWidth(truncateToWidth(session.shortId, 9), 10)) +
|
|
2712
2727
|
agentColor(padToWidth(truncateToWidth(session.agent, 8), 9)) +
|
|
2713
2728
|
chalk.yellow(padToWidth(truncateToWidth(session.version || '-', 7), 8)) +
|
|
@@ -2907,9 +2922,9 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
|
2907
2922
|
// column (and its compact labels) is computed the same way the picker does it.
|
|
2908
2923
|
const showTicket = sessions.some((s) => ticketLabel(s) !== '');
|
|
2909
2924
|
const cols = pickerColumnsFor(sessions);
|
|
2910
|
-
const
|
|
2925
|
+
const bookmarks = listBookmarks();
|
|
2911
2926
|
for (const session of sessions) {
|
|
2912
|
-
console.log(flatSessionRow(session, liveIndex?.get(session.id), showTicket, cols,
|
|
2927
|
+
console.log(flatSessionRow(session, liveIndex?.get(session.id), showTicket, cols, bookmarks.has(session.id)));
|
|
2913
2928
|
}
|
|
2914
2929
|
const countLine = `${sessions.length} session${sessions.length === 1 ? '' : 's'}.`;
|
|
2915
2930
|
console.log(chalk.gray(`\n${countLine}`));
|
|
@@ -3252,10 +3267,10 @@ export function pickerColumnsFor(sessions) {
|
|
|
3252
3267
|
showModel: sessions.some((s) => !!s.model),
|
|
3253
3268
|
modelWidth: modelColumnWidth(sessions),
|
|
3254
3269
|
showTicket: sessions.some((s) => ticketLabel(s) !== ''),
|
|
3255
|
-
|
|
3270
|
+
showBookmark: (() => {
|
|
3256
3271
|
// One read of the store per pool, not one per row.
|
|
3257
|
-
const
|
|
3258
|
-
return
|
|
3272
|
+
const bookmarks = listBookmarks();
|
|
3273
|
+
return bookmarks.size > 0 && sessions.some((s) => bookmarks.has(s.id));
|
|
3259
3274
|
})(),
|
|
3260
3275
|
};
|
|
3261
3276
|
}
|
|
@@ -3275,7 +3290,7 @@ export function liveHostLabel(a) {
|
|
|
3275
3290
|
const viewer = a.viewingIn?.app;
|
|
3276
3291
|
return viewer && viewer !== a.host ? `${a.host}→${viewer}` : a.host;
|
|
3277
3292
|
}
|
|
3278
|
-
export function formatPickerLabel(s, query, cols = {}, ssh, host = '',
|
|
3293
|
+
export function formatPickerLabel(s, query, cols = {}, ssh, host = '', bookmarked = false, live) {
|
|
3279
3294
|
const agentColor = colorAgent(s.agent);
|
|
3280
3295
|
const age = sessionAgeParts(s.timestamp, s.lastActivity);
|
|
3281
3296
|
const project = s.project || '-';
|
|
@@ -3317,11 +3332,11 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '', favorite
|
|
|
3317
3332
|
const ticketW = cols.showTicket ? TICKET_W + 1 : 0;
|
|
3318
3333
|
const hostW = cols.showHost ? PICKER_HOST_W : 0;
|
|
3319
3334
|
const wtW = wt ? stringWidth(wt) + 1 : 0;
|
|
3320
|
-
// Within a pool that HAS
|
|
3321
|
-
// row is
|
|
3322
|
-
// drops the column entirely (`
|
|
3323
|
-
const
|
|
3324
|
-
const
|
|
3335
|
+
// Within a pool that HAS bookmarked rows the marker holds its 2 cells whether
|
|
3336
|
+
// this row is bookmarked or not, so the columns after it never jog; a pool
|
|
3337
|
+
// with none drops the column entirely (`showBookmark`) and costs nothing.
|
|
3338
|
+
const bookmarkW = cols.showBookmark ? 2 : 0;
|
|
3339
|
+
const bookmarkCell = cols.showBookmark ? (bookmarked ? chalk.yellow('★ ') : ' ') : '';
|
|
3325
3340
|
// The same status word the flat listing shows, so a session that is `orphan`
|
|
3326
3341
|
// or `crashed` reads that way in the browser too — not only in its preview.
|
|
3327
3342
|
// Constant width whenever the column is on. `liveStatusCell` already pads its
|
|
@@ -3333,10 +3348,10 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '', favorite
|
|
|
3333
3348
|
const statusCell = cols.showStatus ? (status.cell || ' '.repeat(LIVE_STATUS_W)) : '';
|
|
3334
3349
|
// Sized against the last-activity label alone; the creation field is then an
|
|
3335
3350
|
// additive decision made against whatever width is left (see the flat listing).
|
|
3336
|
-
const baseTopicW = terminalWidth() - gutter -
|
|
3351
|
+
const baseTopicW = terminalWidth() - gutter - bookmarkW - statusW - (10 + 9 + 8 + 16) - machineColW - hostW - ticketW - wtW - sshW - team.width - stringWidth(age.last) - 1;
|
|
3337
3352
|
const when = timeCell(age, baseTopicW);
|
|
3338
3353
|
const topicW = Math.max(MIN_TOPIC_W, baseTopicW - when.extraW);
|
|
3339
|
-
return (
|
|
3354
|
+
return (bookmarkCell +
|
|
3340
3355
|
// Truncated, not just padded: an indexed shortId is always 8 chars, but a
|
|
3341
3356
|
// live row with no session id is named by its pid or cloud task, which can
|
|
3342
3357
|
// run past the column and shunt every later column out of alignment.
|
|
@@ -4459,6 +4474,7 @@ export function registerSessionsCommands(program) {
|
|
|
4459
4474
|
.option('--grok', 'Shorthand for --agent grok')
|
|
4460
4475
|
.option('--opencode', 'Shorthand for --agent opencode')
|
|
4461
4476
|
.option('--all', 'Widen every non-status filter to "all": every directory (not just this project) and all time (no window cap). Status filters like --active still compose; -a/--device/--since still narrow their axis.')
|
|
4477
|
+
.option('--bookmarks', 'Show only bookmarked sessions — bookmark them with `*` in the browser or `agents sessions bookmark <id>`')
|
|
4462
4478
|
.option('--unmanaged', "Also show sessions from your own ~/.<agent> installs (hidden once agents-cli manages that agent)")
|
|
4463
4479
|
.option('--team, --teams', 'Show team-spawned sessions (hidden by default), grouped by team — each team names its spawner and spawn time, teammates show their mode + handle, and team-flagged spawns with no teammate record sink into a (no team) bucket. --flat/--tree keep the plain inline table')
|
|
4464
4480
|
.option('--in-team <name>', "Only this team: the session that spawned it plus (with --teams) its teammates. Spans every directory and all time, since a team's worktrees and history sit outside the default window.")
|
|
@@ -4492,7 +4508,6 @@ export function registerSessionsCommands(program) {
|
|
|
4492
4508
|
.option('--abandoned', 'Show sessions with no transcript progress for the abandonment window (implies --active)')
|
|
4493
4509
|
.option('--queued', 'Show queued sessions that have not started running (implies --active)')
|
|
4494
4510
|
.option('--unknown', 'Show sessions whose live state cannot be determined (implies --active)')
|
|
4495
|
-
.option('--favorites', 'Show only favorited sessions — favorite them with `*` in the browser or `agents sessions favorite <id>`')
|
|
4496
4511
|
.option('--tree', 'Group the listing by directory; drops the id/version columns for readability')
|
|
4497
4512
|
.option('--flat', 'Plain flat table (one row per session) instead of the grouped project overview')
|
|
4498
4513
|
.option('--no-live', 'Do not enrich the listing with live status/preview for running sessions')
|
|
@@ -4662,7 +4677,7 @@ export function registerSessionsCommands(program) {
|
|
|
4662
4677
|
registerSessionsTailCommand(sessionsCmd);
|
|
4663
4678
|
registerSessionsResumeCommand(sessionsCmd);
|
|
4664
4679
|
registerSessionsForkCommand(sessionsCmd);
|
|
4665
|
-
|
|
4680
|
+
registerSessionsBookmarkCommand(sessionsCmd);
|
|
4666
4681
|
registerGoCommand(sessionsCmd);
|
|
4667
4682
|
registerFocusCommand(sessionsCmd);
|
|
4668
4683
|
registerReconnectCommand(sessionsCmd);
|
package/dist/index.js
CHANGED
|
@@ -1247,7 +1247,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
|
|
|
1247
1247
|
// Bumping the suffix re-runs migrations for every user; binary releases that
|
|
1248
1248
|
// don't change the schema must NOT re-run (they would destroy user content
|
|
1249
1249
|
// when migration steps overlap with user-authored paths). See issue #20.
|
|
1250
|
-
const sentinelValue = '
|
|
1250
|
+
const sentinelValue = 'v18';
|
|
1251
1251
|
let needRun = true;
|
|
1252
1252
|
try {
|
|
1253
1253
|
if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
|
|
Binary file
|
|
Binary file
|
package/dist/lib/migrate.js
CHANGED
|
@@ -1015,6 +1015,11 @@ function migrateRuntimeToHistory() {
|
|
|
1015
1015
|
catch { /* best-effort */ }
|
|
1016
1016
|
}
|
|
1017
1017
|
}
|
|
1018
|
+
/** Rename the session marker store after the product vocabulary changed from
|
|
1019
|
+
* favorite to bookmark. The destination wins if a newer CLI already wrote it. */
|
|
1020
|
+
function migrateLegacySessionMarkersToBookmarks() {
|
|
1021
|
+
moveFileOnce(path.join(HISTORY_DIR, 'favorites.json'), path.join(HISTORY_DIR, 'bookmarks.json'));
|
|
1022
|
+
}
|
|
1018
1023
|
/**
|
|
1019
1024
|
* Restore plugins from the cache bucket back to the user-root.
|
|
1020
1025
|
*
|
|
@@ -2178,6 +2183,7 @@ export async function runMigration() {
|
|
|
2178
2183
|
migrateSplitDeviceLocalMeta();
|
|
2179
2184
|
// Bucket moves: collapse runtime state into ~/.agents/.history and ~/.agents/.cache.
|
|
2180
2185
|
migrateRuntimeToHistory();
|
|
2186
|
+
migrateLegacySessionMarkersToBookmarks();
|
|
2181
2187
|
migrateRuntimeToCache();
|
|
2182
2188
|
// Restore plugins (user-authored) from cache back to user-root. Runs AFTER
|
|
2183
2189
|
// migrateRuntimeToCache so any legacy plugins/ still at the user-root from
|