@phnx-labs/agents-cli 1.20.74 → 1.20.77
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 +1055 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +83 -13
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +286 -15
- package/dist/commands/secrets.js +102 -37
- package/dist/commands/sessions-browser.js +12 -2
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.d.ts +14 -1
- package/dist/commands/sessions-picker.js +184 -16
- package/dist/commands/sessions.d.ts +56 -14
- package/dist/commands/sessions.js +322 -62
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +110 -37
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +17 -6
- package/dist/lib/activity.js +466 -40
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.d.ts +10 -1
- package/dist/lib/daemon.js +114 -11
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +37 -1
- package/dist/lib/devices/fleet.js +36 -2
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +82 -0
- package/dist/lib/exec.js +218 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +16 -0
- package/dist/lib/hosts/dispatch.js +26 -4
- package/dist/lib/hosts/passthrough.js +9 -2
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +32 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +17 -5
- package/dist/lib/hosts/run-target.js +30 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +40 -5
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +29 -0
- package/dist/lib/menubar/install-menubar.js +59 -1
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +6 -2
- package/dist/lib/overdue.js +10 -36
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +94 -0
- package/dist/lib/routines.js +93 -9
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +242 -20
- package/dist/lib/sandbox.js +10 -0
- package/dist/lib/secrets/account-token.d.ts +20 -0
- package/dist/lib/secrets/account-token.js +64 -0
- package/dist/lib/secrets/agent.d.ts +74 -4
- package/dist/lib/secrets/agent.js +181 -107
- package/dist/lib/secrets/bundles.d.ts +54 -6
- package/dist/lib/secrets/bundles.js +230 -34
- package/dist/lib/secrets/index.d.ts +26 -3
- package/dist/lib/secrets/index.js +42 -12
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/session-store.d.ts +6 -2
- package/dist/lib/secrets/session-store.js +65 -15
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +124 -7
- package/dist/lib/session/active.js +390 -56
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +54 -0
- package/dist/lib/session/db.js +320 -19
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +478 -3
- package/dist/lib/session/discover.js +1740 -472
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +141 -32
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +40 -26
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.d.ts +4 -0
- package/dist/lib/session/state.js +99 -13
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +8 -4
- package/dist/lib/state.js +21 -37
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +47 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
|
@@ -26,27 +26,33 @@ import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.
|
|
|
26
26
|
import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
|
|
27
27
|
import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
|
|
28
28
|
import { inferSessionState } from '../lib/session/state.js';
|
|
29
|
-
import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex, getSessionRoots } from '../lib/session/discover.js';
|
|
29
|
+
import { discoverSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, searchContentIndex, getSessionRoots } from '../lib/session/discover.js';
|
|
30
|
+
import { findSessionsById } from '../lib/session/db.js';
|
|
30
31
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
31
32
|
import { parseSession } from '../lib/session/parse.js';
|
|
32
33
|
import { runRemoteSessions, buildForwardedArgs, ensureWholeIndex } from '../lib/session/remote.js';
|
|
33
34
|
import { formatRelativeTime } from '../lib/session/relative-time.js';
|
|
34
|
-
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList } from '../lib/session/render.js';
|
|
35
|
+
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList, linkUrl } from '../lib/session/render.js';
|
|
36
|
+
import { linearIssueUrl } from '../lib/session/linear.js';
|
|
35
37
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
36
|
-
import { colorAgent, resolveAgentName } from '../lib/agents.js';
|
|
38
|
+
import { AGENTS, colorAgent, resolveAgentName } from '../lib/agents.js';
|
|
39
|
+
import { getShimsDir } from '../lib/state.js';
|
|
37
40
|
import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
|
|
38
41
|
import { resolveVersionAliasLoose } from '../lib/versions.js';
|
|
39
42
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
40
|
-
import { sessionPicker, buildPreview } from './sessions-picker.js';
|
|
43
|
+
import { sessionPicker, buildPreview, formatTodoCompact, githubRepoUrlFromCwd, } from './sessions-picker.js';
|
|
41
44
|
import { setHelpSections } from '../lib/help.js';
|
|
42
45
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
43
46
|
import { registerSessionsSyncCommand } from './sessions-sync.js';
|
|
44
47
|
import { registerSessionsResumeCommand } from './sessions-resume.js';
|
|
45
48
|
import { registerGoCommand } from './go.js';
|
|
46
49
|
import { registerFocusCommand } from './focus.js';
|
|
50
|
+
import { registerDetachCommand } from './detach.js';
|
|
51
|
+
import { registerAttachCommand } from './attach.js';
|
|
47
52
|
import { registerSessionsInjectCommand } from './sessions-inject.js';
|
|
48
53
|
import { registerSessionsExportCommand } from './sessions-export.js';
|
|
49
54
|
import { registerSessionsImportCommand } from './sessions-import.js';
|
|
55
|
+
import { registerSessionsMigrateCommand, registerSessionsMigrationsCommand } from './sessions-migrate.js';
|
|
50
56
|
import { runBrowserSessions } from '../lib/browser/sessions-list.js';
|
|
51
57
|
const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
|
|
52
58
|
/**
|
|
@@ -176,6 +182,9 @@ function statusColor(status) {
|
|
|
176
182
|
case 'idle': return chalk.gray;
|
|
177
183
|
case 'queued': return chalk.blue;
|
|
178
184
|
case 'input_required': return chalk.yellow;
|
|
185
|
+
// Alive but un-introspectable (a harness whose transcript we can't parse).
|
|
186
|
+
// Magenta so it never reads as the gray "idle" it used to be faked as.
|
|
187
|
+
case 'unknown': return chalk.magenta;
|
|
179
188
|
}
|
|
180
189
|
}
|
|
181
190
|
function contextColor(context) {
|
|
@@ -218,15 +227,24 @@ export function cleanPreview(text) {
|
|
|
218
227
|
.trim();
|
|
219
228
|
}
|
|
220
229
|
/**
|
|
221
|
-
* Build the live description for an active session:
|
|
222
|
-
* preview (the latest turn),
|
|
230
|
+
* Build the live description for an active session: checklist progress (when
|
|
231
|
+
* present) plus the state engine's preview (the latest turn), a user label, or
|
|
232
|
+
* the first-prompt topic. Used by both the flat listing's `doing` cell and as
|
|
233
|
+
* the snippet half of the --active row (identity is layered on in printActiveRow).
|
|
234
|
+
*
|
|
235
|
+
* Covers every ActiveSession context: terminal (interactive), headless, teams,
|
|
236
|
+
* cloud, and sub-agent rows that share the same ActiveSession.todos field.
|
|
223
237
|
*/
|
|
224
238
|
function buildSessionDescription(s) {
|
|
239
|
+
const todo = formatTodoCompact(s.todos);
|
|
225
240
|
if (s.context === 'cloud') {
|
|
226
|
-
|
|
241
|
+
const base = s.preview || `${s.cloudProvider ?? ''}${s.cloudTaskId ? ` · ${s.cloudTaskId.slice(0, 12)}` : ''}`;
|
|
242
|
+
return cleanPreview([todo, base].filter(Boolean).join(' · '));
|
|
227
243
|
}
|
|
228
244
|
if (s.context === 'teams') {
|
|
229
245
|
const parts = [s.teamName];
|
|
246
|
+
if (todo)
|
|
247
|
+
parts.push(todo);
|
|
230
248
|
if (s.preview)
|
|
231
249
|
parts.push(s.preview);
|
|
232
250
|
else if (s.label)
|
|
@@ -235,8 +253,53 @@ function buildSessionDescription(s) {
|
|
|
235
253
|
parts.push(s.topic);
|
|
236
254
|
return cleanPreview(parts.filter(Boolean).join(' · '));
|
|
237
255
|
}
|
|
238
|
-
// Terminal or
|
|
239
|
-
|
|
256
|
+
// Terminal, headless, or sub-agent: todos + live preview, then label, then topic.
|
|
257
|
+
const base = s.preview || s.label || s.topic || '';
|
|
258
|
+
return cleanPreview([todo, base].filter(Boolean).join(' · '));
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Identity + checklist + live snippet for an --active / cross-machine row.
|
|
262
|
+
* Surfaces agent-adjacent identity the flat table already has (label, project)
|
|
263
|
+
* with a clickable project when a GitHub URL is resolvable, then the checklist
|
|
264
|
+
* tally and the latest-turn snippet.
|
|
265
|
+
*
|
|
266
|
+
* Free-text fields are cleaned individually; OSC 8 hyperlinks are applied
|
|
267
|
+
* *after* cleaning so `cleanPreview` does not strip the clickable targets
|
|
268
|
+
* (RUSH-2045 review). Ticket is clickable via {@link signalBadges}, not here,
|
|
269
|
+
* so the id is not printed twice.
|
|
270
|
+
*/
|
|
271
|
+
export function formatActiveRowDescription(s) {
|
|
272
|
+
const parts = [];
|
|
273
|
+
const pushText = (t) => {
|
|
274
|
+
const c = t ? cleanPreview(t) : '';
|
|
275
|
+
if (c)
|
|
276
|
+
parts.push(c);
|
|
277
|
+
};
|
|
278
|
+
if (s.context === 'teams' && s.teamName)
|
|
279
|
+
pushText(s.teamName);
|
|
280
|
+
if (s.label)
|
|
281
|
+
pushText(s.label);
|
|
282
|
+
// Project = basename(cwd), same derivation as serializeActiveSessionsForJson.
|
|
283
|
+
const project = s.cwd ? path.basename(s.cwd) : '';
|
|
284
|
+
if (project && project !== s.label && project !== s.teamName) {
|
|
285
|
+
const label = cleanPreview(project);
|
|
286
|
+
const repoUrl = githubRepoUrlFromCwd(s.cwd);
|
|
287
|
+
parts.push(repoUrl ? linkUrl(repoUrl, label) : label);
|
|
288
|
+
}
|
|
289
|
+
const todo = formatTodoCompact(s.todos);
|
|
290
|
+
if (todo)
|
|
291
|
+
parts.push(todo);
|
|
292
|
+
// Latest-turn snippet — avoid repeating label/topic when already used as identity.
|
|
293
|
+
if (s.context === 'cloud') {
|
|
294
|
+
pushText(s.preview || `${s.cloudProvider ?? ''}${s.cloudTaskId ? ` · ${s.cloudTaskId.slice(0, 12)}` : ''}`);
|
|
295
|
+
}
|
|
296
|
+
else if (s.preview) {
|
|
297
|
+
pushText(s.preview);
|
|
298
|
+
}
|
|
299
|
+
else if (!s.label && s.topic) {
|
|
300
|
+
pushText(s.topic);
|
|
301
|
+
}
|
|
302
|
+
return parts.filter(Boolean).join(' · ');
|
|
240
303
|
}
|
|
241
304
|
/** Short human word for a session's activity (falls back to the coarse status). */
|
|
242
305
|
function activityLabel(s) {
|
|
@@ -274,7 +337,10 @@ export function liveGlyphAndPreview(a) {
|
|
|
274
337
|
return { glyph: '', preview: '' };
|
|
275
338
|
const waiting = a.status === 'input_required' || a.activity === 'waiting_input';
|
|
276
339
|
const running = a.status === 'running' || a.activity === 'working';
|
|
277
|
-
|
|
340
|
+
// `◌` (dotted) = alive but un-introspectable — visually distinct from `○` idle
|
|
341
|
+
// so an opaque harness is never mistaken for a finished one.
|
|
342
|
+
const unknown = a.status === 'unknown';
|
|
343
|
+
const shape = waiting ? '◐' : running ? '●' : unknown ? '◌' : '○';
|
|
278
344
|
return { glyph: statusColor(a.status)(shape), preview: buildSessionDescription(a) };
|
|
279
345
|
}
|
|
280
346
|
/**
|
|
@@ -285,6 +351,22 @@ export function liveGlyphAndPreview(a) {
|
|
|
285
351
|
export function ticketLabel(s) {
|
|
286
352
|
return s.ticketId ?? (s.prNumber ? `PR#${s.prNumber}` : '');
|
|
287
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* The row shape `agents sessions --active --json` emits. RUSH-1981: a watcher
|
|
356
|
+
* joins active sessions on ticketId + project, but the raw ActiveSession nests
|
|
357
|
+
* the ticket (`ticket.id`) and carries no `project` at all — so a naive join
|
|
358
|
+
* silently drops every row. Emit both as flat, always-present top-level keys
|
|
359
|
+
* (null when unknown) alongside the raw fields, so every active row is joinable.
|
|
360
|
+
* `project` uses the same derivation SessionMeta does — basename(cwd) (see
|
|
361
|
+
* discover.ts) — so the active view and the history view join identically.
|
|
362
|
+
*/
|
|
363
|
+
export function serializeActiveSessionsForJson(sessions) {
|
|
364
|
+
return sessions.map((s) => ({
|
|
365
|
+
...s,
|
|
366
|
+
ticketId: s.ticket?.id ?? null,
|
|
367
|
+
project: s.cwd ? path.basename(s.cwd) : null,
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
288
370
|
/**
|
|
289
371
|
* Compact, colour-coded badges for the durable/awaiting signals. Text-only (no
|
|
290
372
|
* emoji, per repo convention): `plan` / `ask` / `perm` for why it's waiting,
|
|
@@ -298,10 +380,15 @@ function signalBadges(s) {
|
|
|
298
380
|
parts.push(chalk.yellow('ask'));
|
|
299
381
|
else if (s.awaitingReason === 'permission')
|
|
300
382
|
parts.push(chalk.yellow('perm'));
|
|
301
|
-
if (s.ticket)
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
parts.push(chalk.
|
|
383
|
+
if (s.ticket) {
|
|
384
|
+
// Clickable when Linear workspace is resolvable (same helper as the picker header).
|
|
385
|
+
const url = linearIssueUrl(s.ticket.id);
|
|
386
|
+
parts.push(chalk.cyan(url ? linkUrl(url, s.ticket.id) : s.ticket.id));
|
|
387
|
+
}
|
|
388
|
+
if (s.pr) {
|
|
389
|
+
const label = `PR#${s.pr.number ?? '?'}`;
|
|
390
|
+
parts.push(chalk.blue(s.pr.url ? linkUrl(s.pr.url, label) : label));
|
|
391
|
+
}
|
|
305
392
|
if (s.worktree)
|
|
306
393
|
parts.push(chalk.magenta(`wt:${s.worktree.slug}`));
|
|
307
394
|
return parts.join(' ');
|
|
@@ -316,8 +403,10 @@ function signalBadges(s) {
|
|
|
316
403
|
function locatorBadge(s) {
|
|
317
404
|
const p = s.provenance;
|
|
318
405
|
const parts = [];
|
|
406
|
+
// An ssh-launched session shows where it was launched FROM when the client IP
|
|
407
|
+
// resolves to a registered device (`ssh←zion`); bare `ssh` when it doesn't.
|
|
319
408
|
if (p?.transport === 'ssh')
|
|
320
|
-
parts.push(chalk.red('ssh'));
|
|
409
|
+
parts.push(chalk.red(p.origin ? `ssh←${p.origin.device}` : 'ssh'));
|
|
321
410
|
if (p?.mux?.kind === 'tmux' && (s.tmuxTarget || p.mux.pane)) {
|
|
322
411
|
parts.push(chalk.green(s.tmuxTarget ?? p.mux.pane));
|
|
323
412
|
// For a tmux-hosted session, say which app+tab is looking at it right now
|
|
@@ -346,13 +435,17 @@ function locatorBadge(s) {
|
|
|
346
435
|
* terminal width so the row never wraps.
|
|
347
436
|
*/
|
|
348
437
|
function printActiveRow(s, indent) {
|
|
438
|
+
// shortId (8-char) · agent · host · status · badges · identity+todos+snippet
|
|
349
439
|
const idCol = chalk.dim(padToWidth((s.sessionId?.slice(0, 8)) ?? '-', 9));
|
|
350
440
|
const kindCol = colorAgent(s.kind)(padToWidth(truncateToWidth(s.kind, 8), 9));
|
|
351
441
|
const hostCol = chalk.gray(padToWidth(truncateToWidth(s.host ?? '-', 8), 9));
|
|
352
442
|
const statusCol = statusColor(s.status)(padToWidth(truncateToWidth(activityLabel(s), 8), 9));
|
|
353
443
|
const fork = s.pidCount && s.pidCount > 1 ? chalk.dim(`×${s.pidCount} `) : '';
|
|
354
444
|
const badges = (fork ? fork : '') + [signalBadges(s), locatorBadge(s)].filter(Boolean).join(' ');
|
|
355
|
-
|
|
445
|
+
// Identity (label/project/ticket clickable) + checklist + live snippet.
|
|
446
|
+
// Cross-machine rows use the same path — remote ActiveSession fields arrive
|
|
447
|
+
// via the SSH fan-out already populated (including todos when the peer has them).
|
|
448
|
+
const desc = formatActiveRowDescription(s) || '-';
|
|
356
449
|
// Fill the remaining width with the preview so nothing wraps under tmux/SSH.
|
|
357
450
|
const fixed = stringWidth(indent) + 9 + 9 + 9 + 9 + (badges ? stringWidth(badges) + 1 : 0);
|
|
358
451
|
const room = Math.max(12, terminalWidth() - fixed - 1);
|
|
@@ -573,15 +666,18 @@ async function runRemoteSessionsJson(hosts) {
|
|
|
573
666
|
process.stdout.write(serializeSessionsJson(sessions));
|
|
574
667
|
}
|
|
575
668
|
/**
|
|
576
|
-
* `running N · idle N · waiting N · queued N` for a bucket of
|
|
577
|
-
* buckets omitted). Same bucketing as the grand-total summary so
|
|
578
|
-
* counts reconcile with the `(total)` beside the header
|
|
669
|
+
* `running N · idle N · waiting N · queued N · unknown N` for a bucket of
|
|
670
|
+
* sessions (zero buckets omitted). Same bucketing as the grand-total summary so
|
|
671
|
+
* per-group counts reconcile with the `(total)` beside the header — the `unknown`
|
|
672
|
+
* bucket is what keeps an alive-but-opaque row from silently vanishing from the
|
|
673
|
+
* tally. Empty when nothing.
|
|
579
674
|
*/
|
|
580
675
|
function groupTally(sessions) {
|
|
581
676
|
const running = sessions.filter(s => s.status === 'running').length;
|
|
582
677
|
const idle = sessions.filter(s => s.status === 'idle').length;
|
|
583
678
|
const waiting = sessions.filter(s => s.status === 'input_required').length;
|
|
584
679
|
const queued = sessions.filter(s => s.status === 'queued').length;
|
|
680
|
+
const unknown = sessions.filter(s => s.status === 'unknown').length;
|
|
585
681
|
const parts = [];
|
|
586
682
|
if (running)
|
|
587
683
|
parts.push(`${running} running`);
|
|
@@ -591,6 +687,8 @@ function groupTally(sessions) {
|
|
|
591
687
|
parts.push(`${waiting} waiting`);
|
|
592
688
|
if (queued)
|
|
593
689
|
parts.push(`${queued} queued`);
|
|
690
|
+
if (unknown)
|
|
691
|
+
parts.push(`${unknown} unknown`);
|
|
594
692
|
return parts.join(' · ');
|
|
595
693
|
}
|
|
596
694
|
/** Print one machine's workspace tree, indented under its machine header. */
|
|
@@ -747,7 +845,7 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
747
845
|
// present so a supervising agent or hook can poll it as a gate.
|
|
748
846
|
const sessions = waitingOnly ? merged.filter(s => s.status === 'input_required') : merged;
|
|
749
847
|
if (asJson) {
|
|
750
|
-
process.stdout.write(JSON.stringify(sessions, null, 2) + '\n');
|
|
848
|
+
process.stdout.write(JSON.stringify(serializeActiveSessionsForJson(sessions), null, 2) + '\n');
|
|
751
849
|
if (waitingOnly && sessions.length > 0)
|
|
752
850
|
process.exitCode = 1;
|
|
753
851
|
return;
|
|
@@ -835,10 +933,15 @@ function canonicalSessionsCommand(query, options) {
|
|
|
835
933
|
async function renderSessionPreview(query, scope) {
|
|
836
934
|
const discovered = await discoverSessions({ all: true, cwd: process.cwd(), limit: 5000 });
|
|
837
935
|
const pool = applyScopeFilters(discovered, scope);
|
|
838
|
-
const matches =
|
|
839
|
-
const session =
|
|
936
|
+
const { matches, completeId } = resolveSessionQuery(pool, query);
|
|
937
|
+
const session = matches[0];
|
|
840
938
|
if (!session) {
|
|
841
|
-
|
|
939
|
+
// A complete id that missed is not "no match for this text" — say which, and
|
|
940
|
+
// give the same fleet pointer the render paths give.
|
|
941
|
+
if (completeId)
|
|
942
|
+
notFoundByIdMessage(query).forEach(l => console.log(l));
|
|
943
|
+
else
|
|
944
|
+
console.log(chalk.gray(`No session matches "${query}".`));
|
|
842
945
|
return;
|
|
843
946
|
}
|
|
844
947
|
console.log(buildPreview(session));
|
|
@@ -1043,11 +1146,14 @@ async function sessionsAction(query, options) {
|
|
|
1043
1146
|
sortBy,
|
|
1044
1147
|
origin: options.routine ? 'routine' : undefined,
|
|
1045
1148
|
};
|
|
1149
|
+
let hiddenUnmanaged = 0;
|
|
1046
1150
|
let sessions = await discoverSessions({
|
|
1047
1151
|
...scope,
|
|
1048
1152
|
limit,
|
|
1049
1153
|
excludeTeamOrigin: !options.teams,
|
|
1050
1154
|
onProgress: tracker.onProgress,
|
|
1155
|
+
includeUnmanaged: options.unmanaged,
|
|
1156
|
+
onHiddenUnmanaged: (n) => { hiddenUnmanaged = n; },
|
|
1051
1157
|
});
|
|
1052
1158
|
tracker.stop();
|
|
1053
1159
|
spinner?.stop();
|
|
@@ -1121,6 +1227,9 @@ async function sessionsAction(query, options) {
|
|
|
1121
1227
|
if (hiddenCount > 0) {
|
|
1122
1228
|
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
1123
1229
|
}
|
|
1230
|
+
if (hiddenUnmanaged > 0) {
|
|
1231
|
+
console.log(chalk.gray(formatUnmanagedHiddenFooter(hiddenUnmanaged)));
|
|
1232
|
+
}
|
|
1124
1233
|
return;
|
|
1125
1234
|
}
|
|
1126
1235
|
// The grouped project overview is the bare interactive default: a scannable
|
|
@@ -1130,7 +1239,7 @@ async function sessionsAction(query, options) {
|
|
|
1130
1239
|
const liveIndex = await maybeLiveIndex(options);
|
|
1131
1240
|
// Per-project row cap is fixed (--limit carries a default of 50 and drives
|
|
1132
1241
|
// the fetch pool, not the display); `--all` expands every group instead.
|
|
1133
|
-
printSessionOverview(sessions, hiddenCount, liveIndex, { perProjectCap: OVERVIEW_ROWS_PER_PROJECT, expand: !!options.all });
|
|
1242
|
+
printSessionOverview(sessions, hiddenCount, liveIndex, { perProjectCap: OVERVIEW_ROWS_PER_PROJECT, expand: !!options.all, hiddenUnmanaged });
|
|
1134
1243
|
return;
|
|
1135
1244
|
}
|
|
1136
1245
|
// --tree / --flat are printed listings, not an interactive pick — render them
|
|
@@ -1150,6 +1259,10 @@ async function sessionsAction(query, options) {
|
|
|
1150
1259
|
const filtered = searchQuery ? filterSessionsByQuery(sessions, searchQuery) : sessions;
|
|
1151
1260
|
const liveIndex = await maybeLiveIndex(options);
|
|
1152
1261
|
printSessionTable(filtered, hiddenCount, options.tree === true, liveIndex);
|
|
1262
|
+
// Every listing path must say what it dropped — a hidden default that stays
|
|
1263
|
+
// silent in one render mode is the failure this footer exists to prevent.
|
|
1264
|
+
if (hiddenUnmanaged > 0)
|
|
1265
|
+
console.log(chalk.gray(formatUnmanagedHiddenFooter(hiddenUnmanaged)));
|
|
1153
1266
|
}
|
|
1154
1267
|
catch (err) {
|
|
1155
1268
|
tracker.stop();
|
|
@@ -1158,8 +1271,14 @@ async function sessionsAction(query, options) {
|
|
|
1158
1271
|
process.exit(1);
|
|
1159
1272
|
}
|
|
1160
1273
|
}
|
|
1274
|
+
/** Whether a query should route to the single-session render rather than the
|
|
1275
|
+
* listing. The hex-ish test catches a bare id prefix; `isCompleteSessionId`
|
|
1276
|
+
* additionally catches the prefixed whole ids (`session_…`, `ses_…`) that the
|
|
1277
|
+
* hex test rejects — without it those never reach the id-only resolution and
|
|
1278
|
+
* still content-search. */
|
|
1161
1279
|
function looksLikeSessionId(query) {
|
|
1162
|
-
|
|
1280
|
+
const trimmed = query.trim();
|
|
1281
|
+
return /^[0-9a-f-]{6,}$/i.test(trimmed) || isCompleteSessionId(trimmed);
|
|
1163
1282
|
}
|
|
1164
1283
|
function teamTag(session) {
|
|
1165
1284
|
const origin = session.teamOrigin;
|
|
@@ -1194,9 +1313,12 @@ function flatSessionRow(session, live, showTicket = false, cols = {}) {
|
|
|
1194
1313
|
const tag = originTag(session) || teamTag(session);
|
|
1195
1314
|
const label = session.label;
|
|
1196
1315
|
const { glyph, preview } = liveGlyphAndPreview(live);
|
|
1197
|
-
// A running session's live preview
|
|
1198
|
-
//
|
|
1199
|
-
|
|
1316
|
+
// A running session's live preview (via liveGlyphAndPreview → buildSessionDescription)
|
|
1317
|
+
// already folds in ActiveSession.todos. For resting / not-live rows, surface
|
|
1318
|
+
// SessionMeta.todos when the scan attached it.
|
|
1319
|
+
const restingTodo = !live ? formatTodoCompact(session.todos) : '';
|
|
1320
|
+
const topicBase = tag ? `${tag}${session.topic ?? ''}` : session.topic;
|
|
1321
|
+
const doing = [restingTodo, preview || topicBase].filter(Boolean).join(' · ') || undefined;
|
|
1200
1322
|
const wt = session.worktreeSlug ? chalk.magenta(`wt:${session.worktreeSlug}`) : '';
|
|
1201
1323
|
// The machine column only earns its width when the listing spans more than one
|
|
1202
1324
|
// box (i.e. the cross-machine fan-out folded remotes in) — same rule and
|
|
@@ -1232,7 +1354,11 @@ function treeSessionRow(session, live) {
|
|
|
1232
1354
|
const tag = originTag(session) || teamTag(session);
|
|
1233
1355
|
const label = session.label;
|
|
1234
1356
|
const { glyph, preview } = liveGlyphAndPreview(live);
|
|
1235
|
-
|
|
1357
|
+
// Match flatSessionRow: live preview already folds ActiveSession.todos; resting
|
|
1358
|
+
// rows surface SessionMeta.todos when present (RUSH-2045).
|
|
1359
|
+
const restingTodo = !live ? formatTodoCompact(session.todos) : '';
|
|
1360
|
+
const topicBase = preview || (tag ? `${tag}${session.topic ?? ''}` : session.topic);
|
|
1361
|
+
const topic = [restingTodo, topicBase].filter(Boolean).join(' · ') || '-';
|
|
1236
1362
|
const badges = signalBadges(metaSignals(session));
|
|
1237
1363
|
const badgeW = badges ? stringWidth(badges) + 1 : 0;
|
|
1238
1364
|
const head = label ? `${label} · ${topic}` : topic;
|
|
@@ -1337,6 +1463,8 @@ function printSessionOverview(pool, hiddenCount, liveIndex, opts) {
|
|
|
1337
1463
|
console.log(parts.join(chalk.gray(' · ')));
|
|
1338
1464
|
if (hiddenCount > 0)
|
|
1339
1465
|
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
1466
|
+
if (opts.hiddenUnmanaged)
|
|
1467
|
+
console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
|
|
1340
1468
|
}
|
|
1341
1469
|
function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
1342
1470
|
if (tree) {
|
|
@@ -1597,10 +1725,18 @@ export function pickerColumnsFor(sessions) {
|
|
|
1597
1725
|
showTicket: sessions.some((s) => ticketLabel(s) !== ''),
|
|
1598
1726
|
};
|
|
1599
1727
|
}
|
|
1600
|
-
export function formatPickerLabel(s, query, cols = {}) {
|
|
1728
|
+
export function formatPickerLabel(s, query, cols = {}, ssh) {
|
|
1601
1729
|
const agentColor = colorAgent(s.agent);
|
|
1602
1730
|
const when = formatRelativeTime(s.lastActivity ?? s.timestamp);
|
|
1603
1731
|
const project = s.project || '-';
|
|
1732
|
+
// SSH-launch origin (live rows only): mirrors the flat listing's `ssh←<device>`
|
|
1733
|
+
// badge. Rendered as its OWN red segment before the topic cell — folding it into
|
|
1734
|
+
// the topic string loses the colour, because renderTopicCell strips ANSI and
|
|
1735
|
+
// re-whitens every slice. Its width is reserved from the topic budget below
|
|
1736
|
+
// (exactly like `wt`), so the fixed-width columns stay aligned.
|
|
1737
|
+
const sshPlain = ssh ? (ssh.device ? `ssh←${ssh.device} ` : 'ssh ') : '';
|
|
1738
|
+
const sshSeg = sshPlain ? chalk.red(sshPlain) : '';
|
|
1739
|
+
const sshW = sshPlain ? stringWidth(sshPlain) : 0;
|
|
1604
1740
|
const tag = originTag(s) || teamTag(s);
|
|
1605
1741
|
const label = s.label;
|
|
1606
1742
|
const topic = tag ? `${tag}${s.topic ?? ''}` : s.topic;
|
|
@@ -1621,12 +1757,13 @@ export function formatPickerLabel(s, query, cols = {}) {
|
|
|
1621
1757
|
const machineColW = cols.showMachine ? machineW : 0;
|
|
1622
1758
|
const ticketW = cols.showTicket ? TICKET_W + 1 : 0;
|
|
1623
1759
|
const wtW = wt ? stringWidth(wt) + 1 : 0;
|
|
1624
|
-
const topicW = Math.max(16, terminalWidth() - gutter - (10 + 9 + 8 + 16) - machineColW - ticketW - wtW - stringWidth(when) - 1);
|
|
1760
|
+
const topicW = Math.max(16, terminalWidth() - gutter - (10 + 9 + 8 + 16) - machineColW - ticketW - wtW - sshW - stringWidth(when) - 1);
|
|
1625
1761
|
return (chalk.white(padRight(s.shortId, 10)) +
|
|
1626
1762
|
agentColor(padRight(truncate(s.agent, 8), 9)) +
|
|
1627
1763
|
chalk.yellow(padRight(truncate(versionStr, 7), 8)) +
|
|
1628
1764
|
machineCell +
|
|
1629
1765
|
chalk.cyan(padRight(truncate(project, 14), 16)) +
|
|
1766
|
+
sshSeg +
|
|
1630
1767
|
renderTopicCell(label, topic, query, topicW, topicW) +
|
|
1631
1768
|
ticketCell +
|
|
1632
1769
|
(wt ? wt + ' ' : '') +
|
|
@@ -1720,8 +1857,8 @@ export async function handlePickedSession(picked) {
|
|
|
1720
1857
|
* Resume a session in the current terminal — a foreground takeover of this
|
|
1721
1858
|
* process. Used by the single-select picker and by `sessions resume` when the
|
|
1722
1859
|
* chosen destination is "in place" (unknown emulator / off-macOS, single pick).
|
|
1723
|
-
* Falls back to the current version
|
|
1724
|
-
*
|
|
1860
|
+
* Falls back to the same resume invocation against the current version when the
|
|
1861
|
+
* version-pinned launcher is genuinely missing.
|
|
1725
1862
|
*/
|
|
1726
1863
|
export async function resumeSessionInPlace(session) {
|
|
1727
1864
|
const cwd = session.cwd && fs.existsSync(session.cwd)
|
|
@@ -1738,11 +1875,17 @@ export async function resumeSessionInPlace(session) {
|
|
|
1738
1875
|
// agent shim is a `.cmd`/`.ps1` and, under the shell needed to run it (see
|
|
1739
1876
|
// spawnResumeCommand), a missing command exits non-zero rather than emitting
|
|
1740
1877
|
// an ENOENT `error` event — so detect a removed version here instead of
|
|
1741
|
-
// relying on that event, keeping the
|
|
1742
|
-
|
|
1878
|
+
// relying on that event, keeping the fallback working on every OS.
|
|
1879
|
+
// `resume[0]` is an absolute alias path when one exists on disk, so only a bare
|
|
1880
|
+
// name still needs a PATH lookup. Checking existsSync first is what keeps an
|
|
1881
|
+
// isolated install (shims deliberately off PATH) out of the fallback.
|
|
1882
|
+
const launcherFound = path.isAbsolute(resume[0])
|
|
1883
|
+
? fs.existsSync(resume[0])
|
|
1884
|
+
: !!findExecutable(resume[0]);
|
|
1885
|
+
if (!launcherFound && session.version) {
|
|
1743
1886
|
const fallback = buildFallbackCommand(session);
|
|
1744
1887
|
if (fallback) {
|
|
1745
|
-
console.log(chalk.gray(`Version ${session.version} is not installed.
|
|
1888
|
+
console.log(chalk.gray(`Version ${session.version} is not installed. Resuming with the current version instead...`));
|
|
1746
1889
|
await spawnResumeCommand(fallback, cwd);
|
|
1747
1890
|
return;
|
|
1748
1891
|
}
|
|
@@ -1819,18 +1962,55 @@ function spawnResumeCommand(cmd, cwd) {
|
|
|
1819
1962
|
* If the versioned binary is missing (version was removed), the ENOENT
|
|
1820
1963
|
* handler in handlePickedSession retries via buildFallbackCommand.
|
|
1821
1964
|
*/
|
|
1965
|
+
/**
|
|
1966
|
+
* The agent's own resume invocation, given whichever launcher we resolved.
|
|
1967
|
+
* Keeping the verb in one place is what lets the version-pinned and fallback
|
|
1968
|
+
* paths stay in agreement — they previously drifted into `/continue`, which is
|
|
1969
|
+
* not a command either CLI has.
|
|
1970
|
+
*/
|
|
1971
|
+
function resumeArgv(agent, id, launcher) {
|
|
1972
|
+
switch (agent) {
|
|
1973
|
+
case 'claude': return [launcher, '--resume', id];
|
|
1974
|
+
case 'codex': return [launcher, 'resume', id];
|
|
1975
|
+
case 'opencode': return [launcher, '--session', id];
|
|
1976
|
+
default: return null;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Absolute path of the on-disk versioned alias, or null when it isn't there.
|
|
1981
|
+
*
|
|
1982
|
+
* Resume must not resolve `<cli>@<version>` by bare name. The shims directory is
|
|
1983
|
+
* deliberately absent from PATH for an isolated install — that is precisely what
|
|
1984
|
+
* `--isolated` promises — so a PATH lookup can never find the alias, and resume
|
|
1985
|
+
* degraded to the fallback 100% of the time for isolated copies. Mirrors the
|
|
1986
|
+
* resolution `buildExecCommand` already does for `agents run`.
|
|
1987
|
+
*/
|
|
1988
|
+
function versionedAliasIfPresent(agent, version) {
|
|
1989
|
+
const cli = AGENTS[agent]?.cliCommand ?? agent;
|
|
1990
|
+
const base = path.join(getShimsDir(), `${cli}@${version}`);
|
|
1991
|
+
if (process.platform === 'win32' && fs.existsSync(`${base}.cmd`))
|
|
1992
|
+
return `${base}.cmd`;
|
|
1993
|
+
if (fs.existsSync(base))
|
|
1994
|
+
return base;
|
|
1995
|
+
return null;
|
|
1996
|
+
}
|
|
1822
1997
|
export function buildResumeCommand(session) {
|
|
1823
1998
|
switch (session.agent) {
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
return [`claude@${session.version}`, '--resume', session.id];
|
|
1827
|
-
return ['claude', '--resume', session.id];
|
|
1828
|
-
case 'codex':
|
|
1829
|
-
if (session.version)
|
|
1830
|
-
return [`codex@${session.version}`, 'resume', session.id];
|
|
1831
|
-
return ['codex', 'resume', session.id];
|
|
1999
|
+
// opencode sessions are shared across versions, so resume is deliberately NOT
|
|
2000
|
+
// version-pinned — it always goes through the plain launcher.
|
|
1832
2001
|
case 'opencode':
|
|
1833
|
-
return
|
|
2002
|
+
return resumeArgv('opencode', session.id, 'opencode');
|
|
2003
|
+
case 'claude':
|
|
2004
|
+
case 'codex': {
|
|
2005
|
+
const cli = AGENTS[session.agent]?.cliCommand ?? session.agent;
|
|
2006
|
+
if (session.version) {
|
|
2007
|
+
const alias = versionedAliasIfPresent(session.agent, session.version);
|
|
2008
|
+
// Absolute path when the alias exists; otherwise the bare versioned name,
|
|
2009
|
+
// which still resolves for a non-isolated install whose shims are on PATH.
|
|
2010
|
+
return resumeArgv(session.agent, session.id, alias ?? `${cli}@${session.version}`);
|
|
2011
|
+
}
|
|
2012
|
+
return resumeArgv(session.agent, session.id, cli);
|
|
2013
|
+
}
|
|
1834
2014
|
case 'gemini':
|
|
1835
2015
|
case 'antigravity':
|
|
1836
2016
|
case 'openclaw':
|
|
@@ -1843,13 +2023,18 @@ export function buildResumeCommand(session) {
|
|
|
1843
2023
|
return null;
|
|
1844
2024
|
}
|
|
1845
2025
|
}
|
|
1846
|
-
/**
|
|
2026
|
+
/**
|
|
2027
|
+
* Fallback when the pinned version really is gone: the same resume invocation
|
|
2028
|
+
* against the current version.
|
|
2029
|
+
*
|
|
2030
|
+
* This used to spawn `<cli> "/continue <id>"`, feeding a slash command into the
|
|
2031
|
+
* TUI as a prompt. Neither CLI has `/continue` — codex documents `/resume` — so
|
|
2032
|
+
* the agent received an unrecognised command and the session was not resumed at
|
|
2033
|
+
* all. Reusing resumeArgv keeps the two paths from drifting apart again.
|
|
2034
|
+
*/
|
|
1847
2035
|
function buildFallbackCommand(session) {
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
case 'codex': return ['codex', `/continue ${session.id}`];
|
|
1851
|
-
default: return null;
|
|
1852
|
-
}
|
|
2036
|
+
const cli = AGENTS[session.agent]?.cliCommand ?? session.agent;
|
|
2037
|
+
return resumeArgv(session.agent, session.id, cli);
|
|
1853
2038
|
}
|
|
1854
2039
|
// ---------------------------------------------------------------------------
|
|
1855
2040
|
// Cloud session source (--cloud)
|
|
@@ -1956,6 +2141,56 @@ function formatSearchMessage(options) {
|
|
|
1956
2141
|
return 'Search sessions:';
|
|
1957
2142
|
return `Search sessions (${filters.join(', ')}):`;
|
|
1958
2143
|
}
|
|
2144
|
+
/**
|
|
2145
|
+
* The single entry point for turning a `sessions <query>` argument into rows.
|
|
2146
|
+
*
|
|
2147
|
+
* An id-shaped query — a complete id OR a hex short-id/prefix (looksLikeSessionId)
|
|
2148
|
+
* — resolves by id alone, through the index, and never falls back to content
|
|
2149
|
+
* search (a bare id must not surface every transcript that merely mentions it).
|
|
2150
|
+
* A genuine search phrase keeps the ranked metadata+content search.
|
|
2151
|
+
*/
|
|
2152
|
+
export function resolveSessionQuery(pool, query) {
|
|
2153
|
+
// Normalize ONCE here. isCompleteSessionId trims but resolveSessionById does
|
|
2154
|
+
// not, so a padded id ("<uuid> ", e.g. pasted from a terminal) would classify
|
|
2155
|
+
// as complete and then miss the id lookup — reporting a session that IS on
|
|
2156
|
+
// this machine as absent.
|
|
2157
|
+
const normalized = query.trim();
|
|
2158
|
+
const completeId = isCompleteSessionId(normalized);
|
|
2159
|
+
const byIdMatches = resolveSessionById(pool, normalized);
|
|
2160
|
+
if (byIdMatches.length > 0)
|
|
2161
|
+
return { matches: byIdMatches, byId: true, completeId };
|
|
2162
|
+
if (looksLikeSessionId(normalized)) {
|
|
2163
|
+
// Any id-shaped query — a complete id OR a bare hex short-id/prefix —
|
|
2164
|
+
// resolves by id ONLY, never by content. The pool is a minority of the
|
|
2165
|
+
// index (measured: 2,798 of 7,614 rows) because it re-reads live agent homes
|
|
2166
|
+
// and skips whole classes of indexed session, so a pool miss isn't absence:
|
|
2167
|
+
// ask the index directly, the same authoritative lookup `fork` and `exec`
|
|
2168
|
+
// use. And an id that resolves to nothing must report "no session with that
|
|
2169
|
+
// id" — NOT fall back to fuzzy content search. A short id like "d3470b57"
|
|
2170
|
+
// otherwise surfaces every transcript that merely MENTIONS the string (a
|
|
2171
|
+
// resume prompt echoes the parent id into the body of many later sessions).
|
|
2172
|
+
return { matches: findSessionsById(normalized), byId: true, completeId };
|
|
2173
|
+
}
|
|
2174
|
+
return { matches: filterSessionsByQuery(pool, normalized), byId: false, completeId };
|
|
2175
|
+
}
|
|
2176
|
+
/** Explain an ambiguous resolution. Only a short id can be lengthened: a complete
|
|
2177
|
+
* id is already maximal, and a search phrase was never an id to begin with. */
|
|
2178
|
+
function ambiguityHint(byId, completeId) {
|
|
2179
|
+
if (completeId)
|
|
2180
|
+
return 'That is already a complete id — these rows share it as a prefix.';
|
|
2181
|
+
return byId
|
|
2182
|
+
? 'Pass a longer ID to narrow it down.'
|
|
2183
|
+
: 'That matched on text, not an id. Pass a session id, or narrow the search.';
|
|
2184
|
+
}
|
|
2185
|
+
/** Explain a complete-id miss, which no local rephrasing can fix. Echoes the
|
|
2186
|
+
* normalized id so a pasted, padded argument doesn't produce an unrunnable hint. */
|
|
2187
|
+
function notFoundByIdMessage(query) {
|
|
2188
|
+
const id = query.trim();
|
|
2189
|
+
return [
|
|
2190
|
+
chalk.red(`No session with id ${id} on this machine.`),
|
|
2191
|
+
chalk.gray(`Search the fleet with: agents sessions ${id} --device <host>`),
|
|
2192
|
+
];
|
|
2193
|
+
}
|
|
1959
2194
|
/** Filter and rank sessions by a multi-term search query across metadata and content. */
|
|
1960
2195
|
export function filterSessionsByQuery(sessions, query) {
|
|
1961
2196
|
const trimmed = query?.trim().toLowerCase() || '';
|
|
@@ -2086,11 +2321,13 @@ async function renderArtifactsGlobal(query, listAll, name, scope) {
|
|
|
2086
2321
|
});
|
|
2087
2322
|
tracker.stop();
|
|
2088
2323
|
const allSessions = applyScopeFilters(discovered, scope);
|
|
2089
|
-
const matches =
|
|
2090
|
-
const queryMatches = matches.length > 0 ? matches : filterSessionsByQuery(allSessions, query);
|
|
2324
|
+
const { matches: queryMatches, byId, completeId } = resolveSessionQuery(allSessions, query);
|
|
2091
2325
|
if (queryMatches.length === 0) {
|
|
2092
2326
|
spinner.stop();
|
|
2093
|
-
|
|
2327
|
+
if (byId)
|
|
2328
|
+
notFoundByIdMessage(query).forEach(l => console.error(l));
|
|
2329
|
+
else
|
|
2330
|
+
console.error(chalk.red(`No session found matching: ${query}`));
|
|
2094
2331
|
process.exit(1);
|
|
2095
2332
|
}
|
|
2096
2333
|
if (queryMatches.length > 1) {
|
|
@@ -2099,7 +2336,7 @@ async function renderArtifactsGlobal(query, listAll, name, scope) {
|
|
|
2099
2336
|
for (const m of queryMatches.slice(0, 10)) {
|
|
2100
2337
|
console.error(chalk.cyan(` ${m.shortId} ${m.id} ${m.label ?? m.topic ?? ''}`));
|
|
2101
2338
|
}
|
|
2102
|
-
console.error(chalk.gray(
|
|
2339
|
+
console.error(chalk.gray(ambiguityHint(byId, completeId)));
|
|
2103
2340
|
process.exit(1);
|
|
2104
2341
|
}
|
|
2105
2342
|
spinner.stop();
|
|
@@ -2127,13 +2364,23 @@ async function renderOneSession(query, mode, scope) {
|
|
|
2127
2364
|
tracker.stop();
|
|
2128
2365
|
const allSessions = applyScopeFilters(discovered, scope);
|
|
2129
2366
|
let session;
|
|
2130
|
-
const
|
|
2131
|
-
let queryMatches =
|
|
2132
|
-
|
|
2367
|
+
const resolution = resolveSessionQuery(allSessions, query);
|
|
2368
|
+
let queryMatches = resolution.matches;
|
|
2369
|
+
let byId = resolution.byId;
|
|
2370
|
+
const completeId = resolution.completeId;
|
|
2371
|
+
// Widen to the transcript content index only for a genuine search phrase.
|
|
2372
|
+
// ANY id-shaped query (a complete id OR a hex short-id/prefix) names a
|
|
2373
|
+
// specific session; widening could only surface a DIFFERENT session that
|
|
2374
|
+
// happens to MENTION the id — which is what made `sessions <uuid>` render an
|
|
2375
|
+
// unrelated transcript and `sessions <shortid>` list every session that
|
|
2376
|
+
// echoes the id in a resume prompt. Gate on looksLikeSessionId, not just
|
|
2377
|
+
// completeId, so a short id resolves to "no match" rather than fuzzy content.
|
|
2378
|
+
if (queryMatches.length === 0 && !looksLikeSessionId(query)) {
|
|
2133
2379
|
const contentResults = searchContentIndex(allSessions, query);
|
|
2134
2380
|
if (contentResults.size > 0) {
|
|
2135
2381
|
const matchedSessions = Array.from(contentResults.values())
|
|
2136
2382
|
.sort((a, b) => (b._bm25Score ?? 0) - (a._bm25Score ?? 0));
|
|
2383
|
+
byId = false;
|
|
2137
2384
|
if (matchedSessions.length === 1) {
|
|
2138
2385
|
session = matchedSessions[0];
|
|
2139
2386
|
}
|
|
@@ -2155,6 +2402,10 @@ async function renderOneSession(query, mode, scope) {
|
|
|
2155
2402
|
process.exit(1);
|
|
2156
2403
|
}
|
|
2157
2404
|
}
|
|
2405
|
+
else if (byId) {
|
|
2406
|
+
notFoundByIdMessage(query).forEach(l => console.error(l));
|
|
2407
|
+
process.exit(1);
|
|
2408
|
+
}
|
|
2158
2409
|
else {
|
|
2159
2410
|
console.error(chalk.red(`No session found matching: ${query}`));
|
|
2160
2411
|
console.error(chalk.gray('Run "agents sessions" to browse sessions.'));
|
|
@@ -2168,7 +2419,7 @@ async function renderOneSession(query, mode, scope) {
|
|
|
2168
2419
|
for (const match of queryMatches.slice(0, 10)) {
|
|
2169
2420
|
console.error(chalk.cyan(` ${match.shortId} ${match.id} ${match.label ?? match.topic ?? ''}`));
|
|
2170
2421
|
}
|
|
2171
|
-
console.error(chalk.gray(
|
|
2422
|
+
console.error(chalk.gray(ambiguityHint(byId, completeId)));
|
|
2172
2423
|
process.exit(1);
|
|
2173
2424
|
}
|
|
2174
2425
|
else {
|
|
@@ -2205,6 +2456,7 @@ export function registerSessionsCommands(program) {
|
|
|
2205
2456
|
.option('--grok', 'Shorthand for --agent grok')
|
|
2206
2457
|
.option('--opencode', 'Shorthand for --agent opencode')
|
|
2207
2458
|
.option('--all', 'Include sessions from every directory (not just current project)')
|
|
2459
|
+
.option('--unmanaged', "Also show sessions from your own ~/.<agent> installs (hidden once agents-cli manages that agent)")
|
|
2208
2460
|
.option('--teams', 'Include team-spawned sessions (hidden by default)')
|
|
2209
2461
|
.option('--routine', 'Show only sessions archived from routine runs')
|
|
2210
2462
|
.option('-p, --project <name>', 'Filter by project name (searches across all directories)')
|
|
@@ -2215,10 +2467,10 @@ export function registerSessionsCommands(program) {
|
|
|
2215
2467
|
.option('--markdown', 'Render the session as markdown (user, assistant, thinking, tool calls)')
|
|
2216
2468
|
.option('--no-redact', 'Disable default secret redaction in rendered session output (--markdown and --json)')
|
|
2217
2469
|
.option('--json', 'Output JSON (session list when browsing, event array when rendering one session)')
|
|
2218
|
-
.option('--include <roles>', 'Only include these roles (comma-separated): user, assistant, thinking, tools')
|
|
2470
|
+
.option('--include <roles>', 'Only include these roles (comma-separated): user, assistant, thinking, tools. "user" is genuine user turns only, not harness-injected scaffolding (bash-input, system-reminder)')
|
|
2219
2471
|
.option('--exclude <roles>', 'Exclude these roles (comma-separated): user, assistant, thinking, tools')
|
|
2220
|
-
.option('--first <n>', 'Keep only the first N turns (a turn starts at each user message)')
|
|
2221
|
-
.option('--last <n>', 'Keep only the last N turns (a turn starts at each user message)')
|
|
2472
|
+
.option('--first <n>', 'Keep only the first N turns (a turn starts at each genuine user message, not harness-injected scaffolding)')
|
|
2473
|
+
.option('--last <n>', 'Keep only the last N turns (a turn starts at each genuine user message, not harness-injected scaffolding)')
|
|
2222
2474
|
.option('--artifacts', 'List all files written or edited during a session')
|
|
2223
2475
|
.option('--artifact <name>', 'Read a specific artifact by filename or path (outputs to stdout)')
|
|
2224
2476
|
.option('--active', 'Show only sessions running right now across terminals, teams, cloud, and headless agents')
|
|
@@ -2293,9 +2545,13 @@ export function registerSessionsCommands(program) {
|
|
|
2293
2545
|
registerSessionsResumeCommand(sessionsCmd);
|
|
2294
2546
|
registerGoCommand(sessionsCmd);
|
|
2295
2547
|
registerFocusCommand(sessionsCmd);
|
|
2548
|
+
registerDetachCommand(sessionsCmd);
|
|
2549
|
+
registerAttachCommand(sessionsCmd);
|
|
2296
2550
|
registerSessionsInjectCommand(sessionsCmd);
|
|
2297
2551
|
registerSessionsExportCommand(sessionsCmd);
|
|
2298
2552
|
registerSessionsImportCommand(sessionsCmd);
|
|
2553
|
+
registerSessionsMigrateCommand(sessionsCmd);
|
|
2554
|
+
registerSessionsMigrationsCommand(sessionsCmd);
|
|
2299
2555
|
}
|
|
2300
2556
|
function formatNoSessionsMessage(showAll, project) {
|
|
2301
2557
|
const projectQuery = project?.trim();
|
|
@@ -2307,6 +2563,10 @@ function formatNoSessionsMessage(showAll, project) {
|
|
|
2307
2563
|
const command = 'agents sessions --all';
|
|
2308
2564
|
return `No sessions found for ${process.cwd()}. Run "${command}" to see sessions from every directory.`;
|
|
2309
2565
|
}
|
|
2566
|
+
function formatUnmanagedHiddenFooter(hiddenCount) {
|
|
2567
|
+
const noun = hiddenCount === 1 ? 'session' : 'sessions';
|
|
2568
|
+
return `(${hiddenCount} ${noun} from your own unmanaged installs hidden — use --unmanaged to show)`;
|
|
2569
|
+
}
|
|
2310
2570
|
function formatTeamHiddenFooter(hiddenCount) {
|
|
2311
2571
|
const noun = hiddenCount === 1 ? 'team session' : 'team sessions';
|
|
2312
2572
|
return `(${hiddenCount} ${noun} hidden — use --teams to show, or \`agents teams status\`)`;
|