@phnx-labs/agents-cli 1.22.109 → 1.22.110
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 +44 -0
- package/dist/bootstrap.js +3 -2
- package/dist/commands/browser-sessions-picker.js +2 -1
- package/dist/commands/computer-sessions-picker.js +2 -1
- package/dist/commands/cost.js +2 -1
- package/dist/commands/fork.js +6 -4
- package/dist/commands/logs.js +2 -1
- package/dist/commands/sessions-backfill.d.ts +30 -0
- package/dist/commands/sessions-backfill.js +72 -0
- package/dist/commands/sessions-inject.d.ts +3 -3
- package/dist/commands/sessions-inject.js +5 -11
- package/dist/commands/sessions-picker.js +10 -7
- package/dist/commands/sessions-resume.d.ts +3 -1
- package/dist/commands/sessions-resume.js +32 -3
- package/dist/commands/sessions.js +44 -24
- package/dist/commands/utils.d.ts +9 -0
- package/dist/commands/utils.js +18 -0
- package/dist/commands/watchdog.js +4 -1
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +17 -1
- package/dist/lib/accounting/usage-sync.d.ts +22 -0
- package/dist/lib/accounting/usage-sync.js +70 -6
- package/dist/lib/accounting/usage.d.ts +32 -0
- package/dist/lib/accounting/usage.js +37 -2
- package/dist/lib/auth-health.js +19 -0
- package/dist/lib/claude-statusline.js +5 -0
- package/dist/lib/computer/sessions-list.js +2 -1
- package/dist/lib/daemon/auth-sync-service.d.ts +2 -0
- package/dist/lib/daemon/auth-sync-service.js +2 -2
- package/dist/lib/daemon/daemon.js +7 -0
- package/dist/lib/daemon/session-title-service.d.ts +41 -0
- package/dist/lib/daemon/session-title-service.js +76 -0
- package/dist/lib/daemon/usage-sync-service.d.ts +9 -1
- package/dist/lib/daemon/usage-sync-service.js +10 -2
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/daemon-ticks.js +6 -0
- package/dist/lib/fleet-shared-state.d.ts +2 -0
- package/dist/lib/hooks/install.js +14 -7
- package/dist/lib/mailbox-target.js +2 -1
- package/dist/lib/session/active.d.ts +93 -16
- package/dist/lib/session/active.js +80 -14
- package/dist/lib/session/db.d.ts +44 -1
- package/dist/lib/session/db.js +122 -17
- package/dist/lib/session/fork.d.ts +10 -2
- package/dist/lib/session/fork.js +11 -2
- package/dist/lib/session/live-metadata.js +6 -0
- package/dist/lib/session/mirror.d.ts +3 -2
- package/dist/lib/session/mirror.js +5 -2
- package/dist/lib/session/remote/watch.js +18 -8
- package/dist/lib/session/title.d.ts +256 -0
- package/dist/lib/session/title.js +312 -0
- package/dist/lib/session/tool-index.d.ts +5 -0
- package/dist/lib/session/tool-index.js +1 -0
- package/dist/lib/session/types.d.ts +11 -0
- package/dist/lib/startup/root-command.d.ts +2 -0
- package/dist/lib/startup/root-command.js +22 -0
- package/dist/lib/traces/sync.js +1 -0
- package/dist/lib/usage-refresh.d.ts +58 -7
- package/dist/lib/usage-refresh.js +145 -23
- package/dist/lib/watchdog/runner.d.ts +3 -0
- package/dist/lib/watchdog/runner.js +1 -0
- package/dist/session-tracker/dist/install-hook.js +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.110
|
|
4
|
+
|
|
5
|
+
- **Session rows are named by what YOU asked for, not by the agent's latest
|
|
6
|
+
message (PHNX-3797).** Every headline in `agents sessions`, the picker, and the
|
|
7
|
+
AGI EXT Sessions/Fleet list used to be the agent's last transcript line
|
|
8
|
+
("Both seams verified on the real shipped artifacts…") — verbose, rolling, and
|
|
9
|
+
unrecognizable. The headline ladder is now `/rename` label → a
|
|
10
|
+
daemon-generated title → your own first message; the agent's live line stays in
|
|
11
|
+
the separate preview slot where a rolling status belongs. The new
|
|
12
|
+
`session-title` daemon service generates a short, descriptive **action + object
|
|
13
|
+
headline** ("Triage the AGI board", not just "Triage") with a cheap model
|
|
14
|
+
through a swappable `SessionTitleProvider` (cloud default; a local ollama-style
|
|
15
|
+
backend can be dropped in later without touching the tick), read-only plan mode,
|
|
16
|
+
**once** per session and
|
|
17
|
+
persists it in the session index against a hash of the message it came from, so
|
|
18
|
+
a titled session costs no further model calls and only re-titles when that first
|
|
19
|
+
message changes. Nothing generates per client or per tick: the value rides the
|
|
20
|
+
existing `sessions watch --json` stream and the fleet session mirror, so a
|
|
21
|
+
remote box's rows show the same title with no per-row SSH. Until the titler
|
|
22
|
+
reaches a session it shows your own words — never an agent line.
|
|
23
|
+
`agents sessions backfill titles [--session <id>] [--refresh]` runs it on
|
|
24
|
+
demand; `agents daemon services` can turn the service off.
|
|
25
|
+
- **Session rows carry a ranked secondary line (PHNX-3797).** Beside the bold
|
|
26
|
+
headline, every live row now folds on `importantMessage` — the single most
|
|
27
|
+
important recent agent message, ranked so a pending **question** or a
|
|
28
|
+
**needs-you** block (plan review, permission, input-required) beats generic
|
|
29
|
+
current activity. It rides the same `sessions watch --json` / mirror feed as the
|
|
30
|
+
title (via the row spread), so AGI EXT renders a bold title over a dim secondary
|
|
31
|
+
line with no extra query; `agents sessions --active` prints the question /
|
|
32
|
+
needs-you line dim beneath the row.
|
|
33
|
+
Source: `src/lib/session/title.ts`, `src/lib/daemon/session-title-service.ts`,
|
|
34
|
+
`src/lib/session/active.ts`, `src/lib/session/db.ts`, `src/lib/session/mirror.ts`,
|
|
35
|
+
`src/commands/sessions.ts`, `src/commands/sessions-backfill.ts`.
|
|
36
|
+
|
|
37
|
+
- Codex account sessions record their originating account after hook sync, including macOS homes relocated for Unix socket limits. Registration refreshes trust for the runtime home and removes the obsolete builtin tracker that could overwrite account metadata; unrelated hooks and explicit disabled states are preserved.
|
|
38
|
+
|
|
39
|
+
- **Session resume preserves device and picker filters (PHNX-3940).** `sessions resume --device <host> <id>` and the equivalent device-after-ID form now open on the selected origin device. Agent, account, directory, time, and limit filters reach the shared picker; an unspecified limit remains 200. Repeated different devices produce an error.
|
|
40
|
+
|
|
41
|
+
- **Usage snapshots stay fresh enough for balanced to auto-pick (PHNX-3940 W3).** A headed box polls only the accounts it holds native logins for; a setup-token-only worker never hits the usage endpoint. A changed snapshot is published immediately (statusline ingest or poll) instead of waiting for the 15-minute tick. `isUsageVerified` trusts a row that arrived via sync from the account's poller for that 15-minute cadence, and keeps the 5-minute bar for a local capture. `auth-sync` and `usage-sync` take the shared-repo lock on different offsets so they no longer starve each other. Source: `cli/src/lib/usage-refresh.ts`, `cli/src/lib/accounting/rotate.ts`, `cli/src/lib/accounting/usage-sync.ts`.
|
|
42
|
+
|
|
3
43
|
## 1.22.109
|
|
4
44
|
|
|
5
45
|
- **`agents sessions backup-setup` provisions the managed session-backup endpoint (PHNX-3726).** The operator command that deploys `sessions.agents-cli.sh` — the Cloudflare Worker + R2 bucket a signed-in user's `agents sessions export --to-r2` backs up to with no `r2.backups` bucket of their own. Mirrors `agents traces setup`: reads Cloudflare credentials from the `cloudflare` secrets bundle (`agents secrets exec cloudflare -- agents sessions backup-setup`), idempotent (re-running redeploys the current Worker template in place). It is NOT a per-user step — signing in with `agents auth login` is all a user does; this is how the first-party endpoint itself is deployed. The Worker it deploys is Phoenix-only with no static token (PHNX-3726). Source: `cli/src/commands/sessions-backup-setup.ts`, `cli/src/lib/session/sync/provision.ts` (+ `provision.test.ts`), `cli/docs/sessions.md`.
|
|
@@ -39,6 +79,10 @@
|
|
|
39
79
|
|
|
40
80
|
- **`agents view` shows OpenCode's real model, account, plan, and last-active (PHNX-3982).** The OpenCode row was the only one in the table with nothing usable in it — `1.18.15 (default) default id:meta+openai+opencode-go` — while every other harness showed a model, an email, and a plan. Three separate reads were pointed at the wrong place. (1) The model came from `<home>/.opencode/settings.json`, which is agents-cli's own plugin-enablement file; OpenCode reads `~/.config/opencode/opencode.{jsonc,json}`, and ships no catalog default, so the row fell through to the literal placeholder `default`. Both spellings are now read comment-tolerantly, and when nothing is configured the row shows the model OpenCode itself will start with, from the selection it persists to `$XDG_STATE_HOME/opencode/model.json`. (2) The account showed only the provider join because the code asserted OpenCode's `auth.json` carries no identity — it does, for `type: 'oauth'` providers, in the same namespaced JWT claims `case 'codex'` already decodes, so the email and plan now render (secrets are never read out, only claims). (3) `lastActive` was blank because the per-file session walk finds nothing for a harness that keeps every session in one sqlite file; it now dates from `opencode.db`. Live usage *windows* remain unavailable for OpenCode — it records no rate-limit state locally and each provider would need its own reader. Source: `cli/src/lib/agent-spec/agents.ts`, `cli/src/lib/models.ts`.
|
|
41
81
|
|
|
82
|
+
- **Codex trace error rates now reflect failed tool calls (PHNX-3761).** The tool-call index recognizes Codex's script completion and failure markers, preserves text across input blocks, and re-indexes older rows instead of reporting failed calls as unknown.
|
|
83
|
+
|
|
84
|
+
- **`agents sessions --cloud <id>` and `agents cloud view` open OpenCode cloud transcripts (PHNX-3845).** Cloud discovery and fetch accept OpenCode sessions, and normalized cloud transcripts are parsed separately from local SQLite sessions. Both `bash` and `shell` tool names appear as command events.
|
|
85
|
+
|
|
42
86
|
## 1.22.108
|
|
43
87
|
|
|
44
88
|
- Account sync now refreshes the native session-tracking hook in every account home, replacing stale registrations from previous CLI installations so new sessions retain their account identity. Codex tracker installation also refreshes hook trust so headless runs can execute the registered hook.
|
package/dist/bootstrap.js
CHANGED
|
@@ -19,7 +19,7 @@ import * as fs from 'fs';
|
|
|
19
19
|
import * as path from 'path';
|
|
20
20
|
import { fileURLToPath } from 'url';
|
|
21
21
|
import { detectDevBuild } from './lib/startup/dev-build.js';
|
|
22
|
-
import { configureRootCommand } from './lib/startup/root-command.js';
|
|
22
|
+
import { configureRootCommand, normalizeResumeDeviceArgs } from './lib/startup/root-command.js';
|
|
23
23
|
import { bootMark } from './lib/boot-profile.js';
|
|
24
24
|
// `ora`, `@inquirer/prompts`, `./commands/utils.js`, and the agents/versions/shims
|
|
25
25
|
// modules are imported dynamically at their use sites: they are needed only on
|
|
@@ -877,7 +877,8 @@ program.on('command:*', (operands) => {
|
|
|
877
877
|
// Parse the invocation shape up front: the first non-flag token is the command,
|
|
878
878
|
// and the doc flags (--version/--help/-h) drive both the registration strategy
|
|
879
879
|
// and whether the update check + background sync run at all.
|
|
880
|
-
const passedArgs = process.argv.slice(2);
|
|
880
|
+
const passedArgs = normalizeResumeDeviceArgs(process.argv.slice(2));
|
|
881
|
+
process.argv.splice(2, process.argv.length - 2, ...passedArgs);
|
|
881
882
|
// Commander owns `--version` on the root command and otherwise intercepts it
|
|
882
883
|
// even after `sessions`, before the subcommand can parse its version filter.
|
|
883
884
|
// Rewrite only that value-taking nested form; bare `agents --version` and every
|
|
@@ -18,6 +18,7 @@ import { buildPreview } from './sessions-picker.js';
|
|
|
18
18
|
import { createSessionsPickerCommand } from './sessions-picker-factory.js';
|
|
19
19
|
import { runBrowserSessions, buildBrowserSessionRows, matchesBrowserSessionRow, formatBytes, } from '../lib/browser/sessions-list.js';
|
|
20
20
|
import { formatRelativeTime } from '../lib/session/relative-time.js';
|
|
21
|
+
import { sessionHeadline } from '../lib/session/title.js';
|
|
21
22
|
const KIND_LABEL = {
|
|
22
23
|
screenshot: 'shots',
|
|
23
24
|
pdf: 'pdf',
|
|
@@ -37,7 +38,7 @@ function rowLinkSummary(row) {
|
|
|
37
38
|
return '';
|
|
38
39
|
if (row.linkStatus === 'linked' && row.linkedSession) {
|
|
39
40
|
const s = row.linkedSession;
|
|
40
|
-
return chalk.cyan(s.agent) + ' — ' + (s
|
|
41
|
+
return chalk.cyan(s.agent) + ' — ' + (sessionHeadline(s) || s.shortId);
|
|
41
42
|
}
|
|
42
43
|
if (row.linkStatus === 'unresolved') {
|
|
43
44
|
return chalk.yellow(`owner ${row.owner ?? 'unknown'} (session not indexed here)`);
|
|
@@ -21,10 +21,11 @@ import { buildPreview } from './sessions-picker.js';
|
|
|
21
21
|
import { createSessionsPickerCommand } from './sessions-picker-factory.js';
|
|
22
22
|
import { runComputerSessions, printComputerSessionRows, buildComputerSessionRows, matchesComputerSessionRow, formatRowActions, } from '../lib/computer/sessions-list.js';
|
|
23
23
|
import { formatRelativeTime } from '../lib/session/relative-time.js';
|
|
24
|
+
import { sessionHeadline } from '../lib/session/title.js';
|
|
24
25
|
function rowLinkSummary(row) {
|
|
25
26
|
if (row.linkStatus === 'linked' && row.linkedSession) {
|
|
26
27
|
const s = row.linkedSession;
|
|
27
|
-
return chalk.cyan(s.agent) + ' — ' + (s
|
|
28
|
+
return chalk.cyan(s.agent) + ' — ' + (sessionHeadline(s) || s.shortId);
|
|
28
29
|
}
|
|
29
30
|
if (row.linkStatus === 'unresolved') {
|
|
30
31
|
return chalk.yellow(`owner ${row.agent ?? 'unknown'} (session not indexed here)`);
|
package/dist/commands/cost.js
CHANGED
|
@@ -5,6 +5,7 @@ import { queryUsageRollup, topSessionsByCost, } from '../lib/session/db.js';
|
|
|
5
5
|
import { formatUsd, PRICING_VERSION } from '../lib/pricing/index.js';
|
|
6
6
|
import { formatDuration } from '../lib/session/render.js';
|
|
7
7
|
import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
|
|
8
|
+
import { sessionHeadline } from '../lib/session/title.js';
|
|
8
9
|
/** Register `agents insights cost` under the insights parent. */
|
|
9
10
|
export function registerCostCommand(insightsCmd) {
|
|
10
11
|
addHostOption(insightsCmd.command('cost'))
|
|
@@ -99,7 +100,7 @@ async function costAction(options) {
|
|
|
99
100
|
for (const t of top) {
|
|
100
101
|
const cost = formatUsd(t.costUsd).padStart(costW);
|
|
101
102
|
const dur = t.durationMs > 0 ? formatDuration(t.durationMs) : '—';
|
|
102
|
-
const label = (t.meta
|
|
103
|
+
const label = (sessionHeadline(t.meta) || '(untitled)').replace(/\s+/g, ' ').trim();
|
|
103
104
|
const proj = showProject && t.meta.project ? chalk.gray(` ${t.meta.project}`) : '';
|
|
104
105
|
const prefix = ` ${chalk.green(cost)} ${chalk.gray(t.meta.shortId)} ${chalk.cyan(t.meta.agent.padEnd(7))} `;
|
|
105
106
|
const suffix = proj + chalk.gray(` ${dur}`);
|
package/dist/commands/fork.js
CHANGED
|
@@ -97,10 +97,12 @@ export async function runFork(sessionArg, options, deps = defaultDeps()) {
|
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
const digest = data?.preview ?? undefined;
|
|
100
|
-
// Most sessions have no explicit --name label; fall back to the
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
100
|
+
// Most sessions have no explicit --name label; fall back to the shared headline
|
|
101
|
+
// ladder the rest of the CLI shows, not the raw short id. Pass `source` whole
|
|
102
|
+
// rather than re-listing fields: an object literal that omits `generatedTitle`
|
|
103
|
+
// still type-checks (the key is optional) and would silently drop the
|
|
104
|
+
// daemon-generated title rung — the PHNX-3797 bug shape.
|
|
105
|
+
const label = forkLabelFor(source);
|
|
104
106
|
const recap = buildForkRecap({
|
|
105
107
|
agent: source.agent,
|
|
106
108
|
label,
|
package/dist/commands/logs.js
CHANGED
|
@@ -29,6 +29,7 @@ import { listTasks } from '../lib/hosts/tasks.js';
|
|
|
29
29
|
import { itemPicker } from '../lib/picker.js';
|
|
30
30
|
import { stats, getLogsPath, rotate, } from '../lib/feed/events.js';
|
|
31
31
|
import { addEventsReadOptions, runEventsCommand } from './events.js';
|
|
32
|
+
import { sessionHeadline } from '../lib/session/title.js';
|
|
32
33
|
/** Compact one-line label used by both the picker and the non-TTY list. */
|
|
33
34
|
function candidateLabel(c) {
|
|
34
35
|
if (c.kind === 'task') {
|
|
@@ -40,7 +41,7 @@ function candidateLabel(c) {
|
|
|
40
41
|
}
|
|
41
42
|
const s = c.session;
|
|
42
43
|
const ver = s.version ? chalk.gray(`@${s.version}`) : '';
|
|
43
|
-
const title = s
|
|
44
|
+
const title = sessionHeadline(s) || '';
|
|
44
45
|
return `${chalk.gray('sess')} ${s.shortId.padEnd(9)} ${(s.agent + ver).padEnd(14)} ${chalk.gray(s.timestamp.slice(0, 16))} ${title.slice(0, 40)}`;
|
|
45
46
|
}
|
|
46
47
|
/** Emit a host-dispatch task's log as JSON: `{ kind, task, log }`. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { type ToolIndexCoverage } from '../lib/session/tool-index.js';
|
|
3
|
+
import { type SessionTitleRunner } from '../lib/session/title.js';
|
|
3
4
|
export interface ToolBackfillMachineResult {
|
|
4
5
|
machine: string;
|
|
5
6
|
indexedFiles: number;
|
|
@@ -61,5 +62,34 @@ export interface ResourceBackfillEnvelope {
|
|
|
61
62
|
* resource_scan_ledger so reruns skip completed transcripts.
|
|
62
63
|
*/
|
|
63
64
|
export declare function runResourceBackfill(options: ResourceBackfillOptions): Promise<ResourceBackfillEnvelope>;
|
|
65
|
+
interface TitleBackfillOptions {
|
|
66
|
+
session?: string;
|
|
67
|
+
limit?: string;
|
|
68
|
+
refresh?: boolean;
|
|
69
|
+
json?: boolean;
|
|
70
|
+
/** The injectable model call (`SessionTitleRunner`); not a CLI flag — tests pass it. */
|
|
71
|
+
run?: SessionTitleRunner;
|
|
72
|
+
}
|
|
73
|
+
export interface TitleBackfillEnvelope {
|
|
74
|
+
schemaVersion: 1;
|
|
75
|
+
kind: 'titles-backfill';
|
|
76
|
+
generatedAt: string;
|
|
77
|
+
machine: string;
|
|
78
|
+
scanned: number;
|
|
79
|
+
cached: number;
|
|
80
|
+
generated: number;
|
|
81
|
+
failed: number;
|
|
82
|
+
titles: Array<{
|
|
83
|
+
id: string;
|
|
84
|
+
title: string;
|
|
85
|
+
}>;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Generate session headlines NOW instead of waiting for the daemon's sweep — the
|
|
89
|
+
* explicit-refresh half of PHNX-3797. Same code path the `session-title` service
|
|
90
|
+
* ticks, so there is one generator: this only changes when it runs, how many it
|
|
91
|
+
* does, and (with `--refresh`) whether an already-current title is regenerated.
|
|
92
|
+
*/
|
|
93
|
+
export declare function runTitlesBackfill(options: TitleBackfillOptions): Promise<TitleBackfillEnvelope>;
|
|
64
94
|
export declare function registerSessionsBackfillCommand(sessionsCmd: Command): void;
|
|
65
95
|
export {};
|
|
@@ -7,6 +7,7 @@ import { SESSION_AGENTS } from '../lib/session/types.js';
|
|
|
7
7
|
import { ensureToolIndex, readToolIndexCoverage } from '../lib/session/tool-index.js';
|
|
8
8
|
import { NO_FANOUT_ENV } from '../lib/session/remote-active.js';
|
|
9
9
|
import { backfillResourceUsage } from '../lib/session/db.js';
|
|
10
|
+
import { runSessionTitleTick, SESSION_TITLE_MAX_PER_TICK } from '../lib/session/title.js';
|
|
10
11
|
const BACKFILL_REMOTE_TIMEOUT_MS = 10 * 60_000;
|
|
11
12
|
function parseAgent(value) {
|
|
12
13
|
if (!value)
|
|
@@ -181,6 +182,31 @@ export async function runResourceBackfill(options) {
|
|
|
181
182
|
...result,
|
|
182
183
|
};
|
|
183
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Generate session headlines NOW instead of waiting for the daemon's sweep — the
|
|
187
|
+
* explicit-refresh half of PHNX-3797. Same code path the `session-title` service
|
|
188
|
+
* ticks, so there is one generator: this only changes when it runs, how many it
|
|
189
|
+
* does, and (with `--refresh`) whether an already-current title is regenerated.
|
|
190
|
+
*/
|
|
191
|
+
export async function runTitlesBackfill(options) {
|
|
192
|
+
const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : undefined;
|
|
193
|
+
if (options.limit !== undefined && (!Number.isFinite(parsedLimit) || parsedLimit < 1)) {
|
|
194
|
+
throw new Error(`--limit must be a positive integer (got "${options.limit}")`);
|
|
195
|
+
}
|
|
196
|
+
const result = await runSessionTitleTick({
|
|
197
|
+
...(options.session ? { id: options.session } : {}),
|
|
198
|
+
...(options.run ? { run: options.run } : {}),
|
|
199
|
+
limit: parsedLimit ?? (options.session ? 1 : SESSION_TITLE_MAX_PER_TICK * 5),
|
|
200
|
+
force: Boolean(options.refresh),
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
schemaVersion: 1,
|
|
204
|
+
kind: 'titles-backfill',
|
|
205
|
+
generatedAt: new Date().toISOString(),
|
|
206
|
+
machine: machineId(),
|
|
207
|
+
...result,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
184
210
|
export function registerSessionsBackfillCommand(sessionsCmd) {
|
|
185
211
|
const backfill = sessionsCmd.command('backfill').description('Populate derived session data explicitly.');
|
|
186
212
|
const tools = backfill.command('tools').description('Parse historical tool calls once into the local SQLite index.');
|
|
@@ -265,4 +291,50 @@ export function registerSessionsBackfillCommand(sessionsCmd) {
|
|
|
265
291
|
process.exitCode = 1;
|
|
266
292
|
}
|
|
267
293
|
});
|
|
294
|
+
const titles = backfill
|
|
295
|
+
.command('titles')
|
|
296
|
+
.description('Generate the session-row headline (a short technical title) now, instead of waiting for the daemon.')
|
|
297
|
+
.option('--session <id>', 'Title one session (full or short id), even if it is old or already titled')
|
|
298
|
+
.option('--limit <n>', 'Maximum titles to generate in this run')
|
|
299
|
+
.option('--refresh', 'Regenerate even when the stored title still matches the session\'s first user message')
|
|
300
|
+
.option('--json', 'Emit the machine-readable result');
|
|
301
|
+
setHelpSections(titles, {
|
|
302
|
+
examples: `
|
|
303
|
+
# Catch this machine up now (the daemon otherwise does a couple every 2 min)
|
|
304
|
+
agents sessions backfill titles
|
|
305
|
+
|
|
306
|
+
# Re-title one session after correcting its first message
|
|
307
|
+
agents sessions backfill titles --session 6fc1db18 --refresh
|
|
308
|
+
|
|
309
|
+
# Machine-readable
|
|
310
|
+
agents sessions backfill titles --limit 20 --json
|
|
311
|
+
`,
|
|
312
|
+
notes: `
|
|
313
|
+
- The headline ladder is: \`/rename\` label > this generated title > the user's first message. It is never the agent's latest turn.
|
|
314
|
+
- One cheap-model call per session (\`--model cheap\`, read-only plan mode), generated ONCE and persisted; a session whose first message has not changed is a pure cache hit.
|
|
315
|
+
- Local-only: each box titles its own sessions and publishes them to the fleet via the session mirror, so a peer's rows already carry their titles.
|
|
316
|
+
- Best-effort. With no signed-in harness nothing is generated and rows keep showing the user's own words.
|
|
317
|
+
`,
|
|
318
|
+
});
|
|
319
|
+
titles.action(async (_options, command) => {
|
|
320
|
+
const options = command.optsWithGlobals();
|
|
321
|
+
try {
|
|
322
|
+
const envelope = await runTitlesBackfill(options);
|
|
323
|
+
if (options.json)
|
|
324
|
+
process.stdout.write(JSON.stringify(envelope, null, 2) + '\n');
|
|
325
|
+
else {
|
|
326
|
+
console.log(`${envelope.machine}: generated ${envelope.generated.toLocaleString()} title${envelope.generated === 1 ? '' : 's'}; ` +
|
|
327
|
+
`${envelope.cached.toLocaleString()} already current, ${envelope.scanned.toLocaleString()} scanned.`);
|
|
328
|
+
for (const { id, title } of envelope.titles)
|
|
329
|
+
console.log(` ${chalk.cyan(id.slice(0, 8))} ${title}`);
|
|
330
|
+
if (envelope.failed > 0) {
|
|
331
|
+
console.log(chalk.yellow(`${envelope.failed.toLocaleString()} session${envelope.failed === 1 ? '' : 's'} produced no usable title; rerun to retry.`));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
337
|
+
process.exitCode = 1;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
268
340
|
}
|
|
@@ -43,9 +43,9 @@ export declare function matchInjectSelector(session: ActiveSession, token: strin
|
|
|
43
43
|
* merges the parent `sessions` command's variadic `-D, --device <target...>` over
|
|
44
44
|
* this subcommand's scalar `--device`, so a single `--device box` arrives as
|
|
45
45
|
* `['box']` — which flowed straight into `sshExec` and crashed on
|
|
46
|
-
* `host.startsWith` (PHNX-3688).
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* `host.startsWith` (PHNX-3688). Delegates to the shared
|
|
47
|
+
* {@link normalizeSingleDeviceOption} (also used by `sessions resume`,
|
|
48
|
+
* PHNX-3940) rather than re-implementing the array/scalar coercion here.
|
|
49
49
|
*/
|
|
50
50
|
export declare function normalizeInjectDevice(value: string | string[] | undefined): string | undefined;
|
|
51
51
|
/**
|
|
@@ -19,6 +19,7 @@ import { sshExec, shellQuote } from '../lib/ssh-exec.js';
|
|
|
19
19
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
20
20
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
21
21
|
import { setHelpSections } from '../lib/help.js';
|
|
22
|
+
import { normalizeSingleDeviceOption } from './utils.js';
|
|
22
23
|
/**
|
|
23
24
|
* Whether an active session is the one `sessions inject <token>` means. Matches
|
|
24
25
|
* a resolvable session id (exact or unique prefix) AND — for a tmux-hosted row
|
|
@@ -47,19 +48,12 @@ export function matchInjectSelector(session, token) {
|
|
|
47
48
|
* merges the parent `sessions` command's variadic `-D, --device <target...>` over
|
|
48
49
|
* this subcommand's scalar `--device`, so a single `--device box` arrives as
|
|
49
50
|
* `['box']` — which flowed straight into `sshExec` and crashed on
|
|
50
|
-
* `host.startsWith` (PHNX-3688).
|
|
51
|
-
*
|
|
52
|
-
*
|
|
51
|
+
* `host.startsWith` (PHNX-3688). Delegates to the shared
|
|
52
|
+
* {@link normalizeSingleDeviceOption} (also used by `sessions resume`,
|
|
53
|
+
* PHNX-3940) rather than re-implementing the array/scalar coercion here.
|
|
53
54
|
*/
|
|
54
55
|
export function normalizeInjectDevice(value) {
|
|
55
|
-
|
|
56
|
-
const hosts = list.map((v) => String(v).trim()).filter((v) => v.length > 0);
|
|
57
|
-
if (hosts.length === 0)
|
|
58
|
-
return undefined;
|
|
59
|
-
if (hosts.length > 1) {
|
|
60
|
-
throw new Error(`sessions inject targets a single device, but --device named ${hosts.length}: ${hosts.join(', ')}.`);
|
|
61
|
-
}
|
|
62
|
-
return hosts[0];
|
|
56
|
+
return normalizeSingleDeviceOption(value, 'sessions inject');
|
|
63
57
|
}
|
|
64
58
|
/**
|
|
65
59
|
* The `agents sessions inject` argv to re-run ON a device (its tmux panes live
|
|
@@ -106,6 +106,7 @@ function sanitizeMeta(s) {
|
|
|
106
106
|
topic: clean(s.topic),
|
|
107
107
|
firstUserMessage: clean(s.firstUserMessage),
|
|
108
108
|
label: clean(s.label),
|
|
109
|
+
generatedTitle: clean(s.generatedTitle),
|
|
109
110
|
ticketId: clean(s.ticketId),
|
|
110
111
|
prUrl: clean(s.prUrl),
|
|
111
112
|
// A remote row's meta is peer-supplied JSON that parseRemoteList hands over
|
|
@@ -303,6 +304,7 @@ function previewCacheKey(session, remote) {
|
|
|
303
304
|
// independently (label/ticket/PR/live scanner enrichment) also rides the key.
|
|
304
305
|
return JSON.stringify([
|
|
305
306
|
remote ?? '', remoteDigestState, session.id, fileStamp, session.lastActivity, session.label,
|
|
307
|
+
session.generatedTitle,
|
|
306
308
|
session.topic, session.ticketId, session.prUrl, session.messageCount,
|
|
307
309
|
session.tokenCount, session.model, session.todos, session.plan,
|
|
308
310
|
session.recentDirectoriesTouched, session.skillsUsed,
|
|
@@ -489,13 +491,14 @@ function formatHeader(session, events) {
|
|
|
489
491
|
line4.push(chalk.blue(linkUrl(session.prUrl, label)));
|
|
490
492
|
}
|
|
491
493
|
// Lead with the session's human title: `session.label` — an agent-generated
|
|
492
|
-
// name / `/rename`, else the `--name` launch handle
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
494
|
+
// name / `/rename`, else the `--name` launch handle — else the daemon-generated
|
|
495
|
+
// title (PHNX-3797), which is a short technical NAME, not a restatement of the
|
|
496
|
+
// prompt. NOT `session.topic`: the topic is the derived first-prompt, already
|
|
497
|
+
// shown on the `Prompt:` line, so using it here too would print the same text
|
|
498
|
+
// twice. A session with neither keeps that `Prompt:` line as its topic
|
|
499
|
+
// indicator and simply leads with the agent line. Wrapped to the pane (the
|
|
500
|
+
// header sits at column 0, full terminal width).
|
|
501
|
+
const title = (session.label || session.generatedTitle || '').trim();
|
|
499
502
|
const titleLines = title
|
|
500
503
|
? wrapToWidth(title, terminalWidth()).map(l => chalk.bold.white(l))
|
|
501
504
|
: [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Command } from 'commander';
|
|
2
2
|
import { type SessionMeta } from '../lib/session/types.js';
|
|
3
3
|
import { type Backend, type SurfaceItem, type EngineContext, type Packing } from '../lib/terminal/index.js';
|
|
4
4
|
import { type StrictResumeOptions } from './resume.js';
|
|
@@ -24,6 +24,8 @@ export interface ResumeOptions extends StrictResumeOptions {
|
|
|
24
24
|
runArgs?: string[];
|
|
25
25
|
}
|
|
26
26
|
export declare function registerSessionsResumeCommand(sessionsCmd: Command): void;
|
|
27
|
+
/** Keep explicit parent options without replacing resume's own defaults. */
|
|
28
|
+
export declare function resolveResumeOptions(cmd: Command, local: ResumeOptions): ResumeOptions;
|
|
27
29
|
export declare function sessionsResumeAction(query: string | undefined, prompt: string | undefined, options: ResumeOptions): Promise<void>;
|
|
28
30
|
/** Preserve run options and lifecycle intent when the picker opens its selected rows. */
|
|
29
31
|
export declare function buildSelectedResumeArgs(id: string, prompt: string | undefined, options: ResumeOptions): string[];
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import chalk from 'chalk';
|
|
15
|
+
import { Option } from 'commander';
|
|
15
16
|
import { isAgentTmuxAlias } from '../lib/session/types.js';
|
|
16
17
|
import { discoverSessions } from '../lib/session/discover.js';
|
|
17
18
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
@@ -20,7 +21,7 @@ import { buildPreview } from './sessions-picker.js';
|
|
|
20
21
|
import { formatPickerLabel, pickerColumnsFor, resolveSessionMetadataValue, parseAgentFilter, } from './sessions.js';
|
|
21
22
|
import { sessionMatchesQuery } from './sessions-browser.js';
|
|
22
23
|
import { openSurfaces, availableBackends, detectCurrentBackend, currentContext, shellQuote, } from '../lib/terminal/index.js';
|
|
23
|
-
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
24
|
+
import { isInteractiveTerminal, isPromptCancelled, normalizeSingleDeviceOption } from './utils.js';
|
|
24
25
|
import { setHelpSections } from '../lib/help.js';
|
|
25
26
|
import { confirm } from '@inquirer/prompts';
|
|
26
27
|
import { spawn } from 'node:child_process';
|
|
@@ -30,6 +31,7 @@ import { sessionOriginDevice, sessionRecoveryDestinationMatches, sessionRecovery
|
|
|
30
31
|
import { buildResumeRemoteArgs, runStrictResume, wantsStrictResume } from './resume.js';
|
|
31
32
|
import { toRemotePortable } from '../lib/project-root.js';
|
|
32
33
|
import { attachLocalLiveSelector } from '../lib/session/local-tmux-attach.js';
|
|
34
|
+
import { sessionHeadline } from '../lib/session/title.js';
|
|
33
35
|
/** Opening more than this many live sessions at once asks for confirmation first. */
|
|
34
36
|
export const CONFIRM_THRESHOLD = 5;
|
|
35
37
|
export function registerSessionsResumeCommand(sessionsCmd) {
|
|
@@ -45,6 +47,8 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
45
47
|
.option('--teams', 'Include team-spawned sessions (hidden by default)')
|
|
46
48
|
.option('--since <time>', 'Only sessions newer than this (e.g., 2h, 7d, 4w, or ISO date)')
|
|
47
49
|
.option('-n, --limit <n>', 'Maximum number of sessions to load into the picker', '200')
|
|
50
|
+
.addOption(new Option('--resume-device <alias>').hideHelp()
|
|
51
|
+
.argParser((value, previous = []) => [...previous, value]))
|
|
48
52
|
.option('--device <alias>', 'Open on the session origin device over SSH; the device must match every selected session')
|
|
49
53
|
.option('--iterm', 'Force the iTerm backend')
|
|
50
54
|
.option('--ghostty', 'Force the Ghostty backend')
|
|
@@ -104,9 +108,34 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
104
108
|
`,
|
|
105
109
|
});
|
|
106
110
|
cmd.action(async (query, prompt, options) => {
|
|
107
|
-
await sessionsResumeAction(query, prompt, options);
|
|
111
|
+
await sessionsResumeAction(query, prompt, resolveResumeOptions(cmd, options));
|
|
108
112
|
});
|
|
109
113
|
}
|
|
114
|
+
/** Flag names `sessions resume` declares that also exist on its parent `sessions`
|
|
115
|
+
* command. Booleans and strings only — `device` collides too but needs its own
|
|
116
|
+
* array-merge handling below, so it is not in this list. */
|
|
117
|
+
const RESUME_PARENT_COLLISION_FLAGS = ['agent', 'all', 'teams', 'since', 'limit', 'local'];
|
|
118
|
+
/** Keep explicit parent options without replacing resume's own defaults. */
|
|
119
|
+
export function resolveResumeOptions(cmd, local) {
|
|
120
|
+
const parent = cmd.parent;
|
|
121
|
+
const { resumeDevice, ...resolved } = local;
|
|
122
|
+
const parentOpts = parent?.opts();
|
|
123
|
+
for (const key of RESUME_PARENT_COLLISION_FLAGS) {
|
|
124
|
+
if (parent?.getOptionValueSource(key) === 'cli') {
|
|
125
|
+
resolved[key] = parentOpts?.[key];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const rawDeviceTargets = [
|
|
129
|
+
...(resumeDevice ?? []),
|
|
130
|
+
...(local.device ? [local.device] : []),
|
|
131
|
+
...(parentOpts?.device ?? []),
|
|
132
|
+
...(parentOpts?.devices ?? []),
|
|
133
|
+
];
|
|
134
|
+
if (rawDeviceTargets.length > 0) {
|
|
135
|
+
resolved.device = normalizeSingleDeviceOption(rawDeviceTargets, 'sessions resume');
|
|
136
|
+
}
|
|
137
|
+
return resolved;
|
|
138
|
+
}
|
|
110
139
|
export async function sessionsResumeAction(query, prompt, options) {
|
|
111
140
|
if (options.attachOnly && (prompt !== undefined || options.agent || options.account || options.model || options.mode || options.interactive || options.headless || options.cwd || options.here)) {
|
|
112
141
|
throw new Error('--attach-only cannot be combined with --agent, a follow-up prompt, or options that change the running session. Pass the session ID without filters.');
|
|
@@ -285,7 +314,7 @@ export async function sessionsResumeAction(query, prompt, options) {
|
|
|
285
314
|
command: it.command,
|
|
286
315
|
agent: it.session.agent || undefined,
|
|
287
316
|
sessionId: it.session.id || undefined,
|
|
288
|
-
title: it.session
|
|
317
|
+
title: sessionHeadline(it.session),
|
|
289
318
|
})), { backend, host: options.device, packing });
|
|
290
319
|
let opened = 0;
|
|
291
320
|
results.forEach((r, i) => {
|
|
@@ -37,6 +37,7 @@ import { inferSessionState } from '../lib/session/state.js';
|
|
|
37
37
|
import { discoverSessions, queryIndexedSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, looksLikeSessionId, searchContentIndex, getSessionRoots, scopeToManaged } from '../lib/session/discover.js';
|
|
38
38
|
import { findSessionsById, querySessions, readSessionContent, readArchivedSessionPreview } from '../lib/session/db.js';
|
|
39
39
|
import { liveSessionMetas, fleetExecutionMachineById, reconcileLiveMetaMachine } from '../lib/session/live-metadata.js';
|
|
40
|
+
import { sessionHeadline } from '../lib/session/title.js';
|
|
40
41
|
import { filterTeamSessions, shouldShowTeamSessions, safeTeamText, groupSessionsByTeam, NO_TEAM_GROUP_KEY, } from '../lib/session/team-filter.js';
|
|
41
42
|
import { parseSession } from '../lib/session/parse.js';
|
|
42
43
|
import { runRemoteSessions, buildForwardedArgs, ensureWholeIndex } from '../lib/session/remote.js';
|
|
@@ -351,6 +352,7 @@ export function buildSessionDescription(s) {
|
|
|
351
352
|
return cleanPreview(parts.filter(Boolean).join(' · '));
|
|
352
353
|
}
|
|
353
354
|
// Terminal, headless, or sub-agent: todos + live preview, then label, then topic.
|
|
355
|
+
// ladder-exempt: the compact --active preview BASE (a live snippet), not the row's headline title.
|
|
354
356
|
const base = s.preview || s.label || s.topic || '';
|
|
355
357
|
return cleanPreview([todo, base].filter(Boolean).join(' · '));
|
|
356
358
|
}
|
|
@@ -759,22 +761,39 @@ export function renderActiveRowLines(s, indent, termW) {
|
|
|
759
761
|
// Line 2: label/topic + checklist (the identity, no longer buried) then the
|
|
760
762
|
// jump locator. Skipped entirely when there is nothing to say. desc may carry a
|
|
761
763
|
// clickable project link, so it also goes through fitCell.
|
|
764
|
+
const contIndent = indent + ' '.repeat(ROW_ID_W);
|
|
762
765
|
const desc = formatActiveRowDescription(s);
|
|
763
766
|
const loc = locatorBadge(s);
|
|
764
|
-
if (
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
767
|
+
if (desc || loc) {
|
|
768
|
+
const room2 = Math.max(0, termW - stringWidth(contIndent) - 2);
|
|
769
|
+
const locCell = fitCell(loc, room2);
|
|
770
|
+
const locW = stringWidth(locCell);
|
|
771
|
+
const descRoom = Math.max(0, room2 - (locW ? locW + 2 : 0));
|
|
772
|
+
const descCell = chalk.white(fitCell(desc || '-', descRoom));
|
|
773
|
+
let line2 = contIndent + chalk.dim('└ ') + descCell;
|
|
774
|
+
if (locCell)
|
|
775
|
+
line2 += ' ' + locCell;
|
|
776
|
+
if (stringWidth(line2) > termW)
|
|
777
|
+
line2 = truncateToWidth(line2, termW);
|
|
778
|
+
lines.push(line2);
|
|
779
|
+
}
|
|
780
|
+
// Secondary line (PHNX-3797 owner feedback): surface the most important recent
|
|
781
|
+
// agent message when it is a pending QUESTION or a NEEDS-YOU block — the urgency
|
|
782
|
+
// a rolling activity preview buries. Plain `activity` is already the preview on
|
|
783
|
+
// line 2, so it earns no extra line here; the full ranked message still rides
|
|
784
|
+
// the JSON/mirror feed for AGI EXT via `s.importantMessage`.
|
|
785
|
+
const important = s.importantMessage;
|
|
786
|
+
if (important && (important.kind === 'question' || important.kind === 'needs_you')) {
|
|
787
|
+
const glyph = important.kind === 'question' ? '? ' : '! ';
|
|
788
|
+
const room3 = Math.max(0, termW - stringWidth(contIndent) - 2 - glyph.length);
|
|
789
|
+
const msgCell = fitCell(cleanPreview(important.text), room3);
|
|
790
|
+
if (msgCell) {
|
|
791
|
+
let line3 = contIndent + chalk.dim(glyph + msgCell);
|
|
792
|
+
if (stringWidth(line3) > termW)
|
|
793
|
+
line3 = truncateToWidth(line3, termW);
|
|
794
|
+
lines.push(line3);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
778
797
|
return lines;
|
|
779
798
|
}
|
|
780
799
|
/** Render a single agent-session row inside an already-printed group header. */
|
|
@@ -2200,7 +2219,7 @@ export function printToolSearch(envelope) {
|
|
|
2200
2219
|
? truncate(sanitizeForTerminal(session.machine).replace(/\s+/g, ' '), 80)
|
|
2201
2220
|
: '';
|
|
2202
2221
|
const machine = machineName ? ` @ ${machineName}` : '';
|
|
2203
|
-
const rawHeading = session
|
|
2222
|
+
const rawHeading = sessionHeadline(session) || session.project || session.shortId;
|
|
2204
2223
|
const heading = truncate(sanitizeForTerminal(rawHeading).replace(/\s+/g, ' '), Math.max(30, terminalWidth() - 20));
|
|
2205
2224
|
console.log(`${chalk.cyan(session.shortId)}${chalk.gray(machine)} ${heading}`);
|
|
2206
2225
|
for (const call of session.calls) {
|
|
@@ -3374,7 +3393,7 @@ function printTeamsView(pool, liveIndex, hiddenUnmanaged = 0) {
|
|
|
3374
3393
|
// degradation the --active teams rows use (active.ts resolveOrchestratorLabels).
|
|
3375
3394
|
const labelById = new Map();
|
|
3376
3395
|
for (const s of pool) {
|
|
3377
|
-
const label = s
|
|
3396
|
+
const label = sessionHeadline(s);
|
|
3378
3397
|
if (label)
|
|
3379
3398
|
labelById.set(s.id, cleanPreview(label));
|
|
3380
3399
|
}
|
|
@@ -3504,7 +3523,7 @@ function renderArchivedSession(session, mode, options = {}) {
|
|
|
3504
3523
|
const shown = sessionDisplayAgent(session);
|
|
3505
3524
|
const agentColor = colorAgent(shown);
|
|
3506
3525
|
const absTime = formatAbsoluteTime(session.timestamp);
|
|
3507
|
-
const title = session
|
|
3526
|
+
const title = sessionHeadline(session);
|
|
3508
3527
|
console.log('');
|
|
3509
3528
|
if (title)
|
|
3510
3529
|
console.log(chalk.bold.white(title));
|
|
@@ -3562,9 +3581,10 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
3562
3581
|
const modelStr = stats.models.length > 0 ? chalk.yellow(` ${stats.models.join(', ')}`) : '';
|
|
3563
3582
|
const branchStr = session.gitBranch ? chalk.gray(` (${session.gitBranch})`) : '';
|
|
3564
3583
|
const absTime = formatAbsoluteTime(session.timestamp);
|
|
3565
|
-
// Auto-inferred title headline (user /rename > Claude ai-title >
|
|
3566
|
-
// topic) — the fastest way to
|
|
3567
|
-
|
|
3584
|
+
// Auto-inferred title headline (user /rename > Claude ai-title >
|
|
3585
|
+
// daemon-generated title > first-prompt topic) — the fastest way to
|
|
3586
|
+
// recognize which task this session is.
|
|
3587
|
+
const title = sessionHeadline(session);
|
|
3568
3588
|
if (title) {
|
|
3569
3589
|
const badges = signalBadges(metaSignals(session));
|
|
3570
3590
|
console.log(chalk.bold.white(title) + (badges ? ' ' + badges : ''));
|
|
@@ -4456,7 +4476,7 @@ async function renderArtifactsGlobal(query, listAll, name, scope) {
|
|
|
4456
4476
|
spinner.stop();
|
|
4457
4477
|
console.error(chalk.red(`Multiple sessions match "${query}":`));
|
|
4458
4478
|
for (const m of queryMatches.slice(0, 10)) {
|
|
4459
|
-
console.error(chalk.cyan(` ${m.shortId} ${m.id} ${m
|
|
4479
|
+
console.error(chalk.cyan(` ${m.shortId} ${m.id} ${sessionHeadline(m) ?? ''}`));
|
|
4460
4480
|
}
|
|
4461
4481
|
console.error(chalk.gray(ambiguityHint(byId, completeId)));
|
|
4462
4482
|
process.exit(1);
|
|
@@ -4613,7 +4633,7 @@ async function renderOneSession(query, mode, scope) {
|
|
|
4613
4633
|
spinner.stop();
|
|
4614
4634
|
console.error(chalk.red(`Multiple sessions match "${query}":`));
|
|
4615
4635
|
for (const match of queryMatches.slice(0, 10)) {
|
|
4616
|
-
console.error(chalk.cyan(` ${match.shortId} ${match.id} ${match
|
|
4636
|
+
console.error(chalk.cyan(` ${match.shortId} ${match.id} ${sessionHeadline(match) ?? ''}`));
|
|
4617
4637
|
}
|
|
4618
4638
|
console.error(chalk.gray(ambiguityHint(byId, completeId)));
|
|
4619
4639
|
process.exit(1);
|
|
@@ -5067,7 +5087,7 @@ export async function resolveSessionMetadata(selector, scope, deps = { gatherRem
|
|
|
5067
5087
|
for (const candidate of outcome.candidates) {
|
|
5068
5088
|
const session = candidate.hits[0].session;
|
|
5069
5089
|
const machines = candidate.hits.map(hit => hit.machine).join(', ');
|
|
5070
|
-
console.error(chalk.cyan(` ${session.shortId} ${session.id}`) + chalk.gray(` ${machines} ${session
|
|
5090
|
+
console.error(chalk.cyan(` ${session.shortId} ${session.id}`) + chalk.gray(` ${machines} ${sessionHeadline(session) ?? ''}`));
|
|
5071
5091
|
}
|
|
5072
5092
|
console.error(chalk.gray(looksLikeSessionId(selector) ? 'Pass a longer ID to narrow it down.' : 'Narrow the keywords to one session.'));
|
|
5073
5093
|
process.exit(1);
|
|
@@ -5107,7 +5127,7 @@ export async function resolveSessionAcrossFleet(query, mode, hosts, deps = { gat
|
|
|
5107
5127
|
console.error(chalk.red(`Multiple sessions match "${query}" across the fleet:`));
|
|
5108
5128
|
for (const candidate of candidates) {
|
|
5109
5129
|
const s = candidate.hits[0].session;
|
|
5110
|
-
const label = s
|
|
5130
|
+
const label = sessionHeadline(s) ?? '';
|
|
5111
5131
|
const machines = candidate.hits.map(hit => hit.machine).join(', ');
|
|
5112
5132
|
console.error(chalk.cyan(` ${s.shortId} ${s.id}`) + chalk.gray(` ${machines} ${s.agent}${s.version ? ` ${s.version}` : ''} ${label}`));
|
|
5113
5133
|
}
|