@hwip/cli 0.1.2 → 0.1.4
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/README.md +33 -11
- package/dist/agents.d.ts +10 -0
- package/dist/agents.js +12 -1
- package/dist/codex-config.d.ts +8 -5
- package/dist/codex-config.js +121 -12
- package/dist/command-scan.d.ts +7 -3
- package/dist/command-scan.js +59 -15
- package/dist/installer.js +7 -1
- package/dist/listener.d.ts +19 -0
- package/dist/listener.js +78 -17
- package/dist/mcp-command.d.ts +13 -1
- package/dist/mcp-command.js +18 -5
- package/dist/pane-fit.d.ts +39 -11
- package/dist/pane-fit.js +162 -29
- package/dist/remote-agents.d.ts +64 -12
- package/dist/remote-agents.js +141 -18
- package/dist/session-registry.d.ts +22 -0
- package/dist/session-registry.js +50 -3
- package/dist/session-title.d.ts +46 -0
- package/dist/session-title.js +103 -0
- package/dist/templates.d.ts +1 -0
- package/dist/templates.js +41 -1
- package/dist/verify.d.ts +3 -0
- package/dist/verify.js +33 -8
- package/package.json +1 -1
package/dist/remote-agents.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'child_process';
|
|
2
2
|
import { type TranscriptProjector, type TranscriptReader } from './transcript-tail.js';
|
|
3
|
+
import { type ProviderName } from './session-title.js';
|
|
3
4
|
export interface RemoteAgent {
|
|
4
5
|
/** Wire value for AgentSession.agentKind (server/phone contract). */
|
|
5
6
|
kind: string;
|
|
@@ -57,8 +58,11 @@ export interface RemoteAgent {
|
|
|
57
58
|
* carries this only once that agent's name store is confirmed. Cursor and
|
|
58
59
|
* Gemini never will — their state files hold ids, cwds and timestamps and no
|
|
59
60
|
* name at all, so there is nothing to read and the computed label stands.
|
|
61
|
+
*
|
|
62
|
+
* The answer says who chose the name (`source`) when the store can tell:
|
|
63
|
+
* the user setting that hides agent-made names hides `'auto'` only.
|
|
60
64
|
*/
|
|
61
|
-
resolveSessionName?: (paneCwd: string, panePid?: number) =>
|
|
65
|
+
resolveSessionName?: (paneCwd: string, panePid?: number) => ProviderName | null;
|
|
62
66
|
/**
|
|
63
67
|
* Resolve the file this agent is writing its session transcript to, for the
|
|
64
68
|
* live chat timeline (`turn-watch`).
|
|
@@ -117,6 +121,11 @@ export interface PidSessionRecord {
|
|
|
117
121
|
* without it, and a record without a name is still a valid id match.
|
|
118
122
|
*/
|
|
119
123
|
name?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Who chose `name`: `"derived"` (from the cwd — CC's placeholder) or
|
|
126
|
+
* `"user"` (`/rename`). Absent from an older CC, which cannot say.
|
|
127
|
+
*/
|
|
128
|
+
nameSource?: string;
|
|
120
129
|
}
|
|
121
130
|
export interface ProcTable {
|
|
122
131
|
/** ppid → its direct children, for walking a pane's process tree. */
|
|
@@ -208,12 +217,21 @@ export declare const pidSessionRecordsExist: () => boolean;
|
|
|
208
217
|
export declare const detectClaudeSessionByPid: (panePid: number, paneCwd: string | null, deps?: PidResolveDeps) => PidSessionRecord | null;
|
|
209
218
|
export declare const detectClaudeSessionIdByPid: (panePid: number, paneCwd: string | null, deps?: PidResolveDeps) => string | null;
|
|
210
219
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
220
|
+
* What the phone should call the Claude Code session in this pane, and who
|
|
221
|
+
* chose it.
|
|
222
|
+
*
|
|
223
|
+
* A `/rename` (`nameSource: "user"`) wins. Otherwise the newest `ai-title` CC
|
|
224
|
+
* wrote into the transcript — its own summary of the work, which is what tells
|
|
225
|
+
* parallel sessions apart. A derived name (`hybrid-shell-8f`, from the cwd)
|
|
226
|
+
* is never sent: it says no more than the tmux name the phone falls back to.
|
|
227
|
+
* A record from an older CC carries a name but no `nameSource`; that one is
|
|
228
|
+
* sent as before, with no source, ahead of any ai-title, since it may be the
|
|
229
|
+
* user's rename. No mtime
|
|
230
|
+
* fallback: only the pid match says which session this pane is.
|
|
215
231
|
*/
|
|
216
|
-
export declare const
|
|
232
|
+
export declare const detectClaudeProviderName: (panePid: number, paneCwd: string | null, deps?: PidResolveDeps & {
|
|
233
|
+
transcriptOf?: (cwd: string, sessionId: string) => string | null;
|
|
234
|
+
}) => ProviderName | null;
|
|
217
235
|
/**
|
|
218
236
|
* The transcript file for the Claude Code session running in a tmux pane.
|
|
219
237
|
*
|
|
@@ -241,6 +259,15 @@ export declare const claudeTranscriptPath: (paneCwd: string | null, panePid?: nu
|
|
|
241
259
|
* until the TTL ran out.
|
|
242
260
|
*/
|
|
243
261
|
export declare const piTranscriptPath: (paneCwd: string | null, panePid?: number) => string | null;
|
|
262
|
+
/**
|
|
263
|
+
* The name a pi pane's session goes by, and who chose it: the newest
|
|
264
|
+
* `session_info` name, which is the agent's (`'auto'`) when the hwip
|
|
265
|
+
* extension's tool set it — the tool leaves a marker entry with the same name
|
|
266
|
+
* (`PI_TITLE_MARKER`) — and the user's (`/name`) otherwise. Read from the
|
|
267
|
+
* transcript tail, so a name written before the last 256 KB of a session the
|
|
268
|
+
* listener first meets late is not found until the next one is written.
|
|
269
|
+
*/
|
|
270
|
+
export declare const detectPiSessionName: (paneCwd: string, panePid: number | undefined, transcriptOf?: (paneCwd: string, panePid?: number) => string | null) => ProviderName | null;
|
|
244
271
|
/**
|
|
245
272
|
* The row whose timestamp is closest to when one of the pane's processes started,
|
|
246
273
|
* within {@link PROC_MATCH_TOLERANCE_MS}. Null when nothing qualifies — including
|
|
@@ -248,6 +275,18 @@ export declare const piTranscriptPath: (paneCwd: string | null, panePid?: number
|
|
|
248
275
|
* are collected before any row is compared rather than defaulted to zero.
|
|
249
276
|
*/
|
|
250
277
|
export declare const pickRowByProcStart: <T>(rows: readonly T[], tsMsOf: (row: T) => number | null, startTimes: Map<number, number>, pids: Set<number>) => T | null;
|
|
278
|
+
/**
|
|
279
|
+
* From when a conversation written to is this pane's: its earliest process start, less {@link PANE_WRITE_MARGIN_MS}. A
|
|
280
|
+
* resume writes to the conversation it picked back up, so that one is written to after the start; a fresh start
|
|
281
|
+
* has not written yet, so the folder's older conversations are all before it. Null when no start is known.
|
|
282
|
+
*/
|
|
283
|
+
export declare const paneSince: (panePid: number, deps?: StoreResolveDeps) => number | null;
|
|
284
|
+
/**
|
|
285
|
+
* The conversation a transcript file holds — its name without `.jsonl` — when it was written to since `since`.
|
|
286
|
+
* For the agents whose resume appends to the file it picks back up (pi `-c`, Codex `resume`, Gemini `--resume`,
|
|
287
|
+
* measured 2026-10-20): the file is the conversation.
|
|
288
|
+
*/
|
|
289
|
+
export declare const transcriptIdSince: (path: string | null, since: number | null) => string | null;
|
|
251
290
|
/**
|
|
252
291
|
* Read rows from a SQLite file as JSON. Null on every failure — no `sqlite3` on
|
|
253
292
|
* PATH, a locked or corrupt database, a schema that moved — because a missing
|
|
@@ -267,7 +306,12 @@ export interface StoreResolveDeps {
|
|
|
267
306
|
startTimes?: Map<number, number>;
|
|
268
307
|
descendants?: Set<number>;
|
|
269
308
|
}
|
|
270
|
-
export declare const detectHermesSessionName: (paneCwd: string, panePid?: number, deps?: StoreResolveDeps) =>
|
|
309
|
+
export declare const detectHermesSessionName: (paneCwd: string, panePid?: number, deps?: StoreResolveDeps) => ProviderName | null;
|
|
310
|
+
/**
|
|
311
|
+
* The Hermes session this pane carries on: in its directory, with a message since the pane started — the newest.
|
|
312
|
+
* A `-c` picks back up a session started long before the pane, which the start-time match above cannot see.
|
|
313
|
+
*/
|
|
314
|
+
export declare const detectHermesResumeId: (paneCwd: string, panePid?: number, deps?: StoreResolveDeps) => string | null;
|
|
271
315
|
/**
|
|
272
316
|
* The highest-numbered match, numerically. Codex versions its state file
|
|
273
317
|
* (`state_5.sqlite` today) and moves to the next number on a schema migration,
|
|
@@ -286,7 +330,7 @@ export declare const newestVersionedDb: (entries: readonly string[], pattern: Re
|
|
|
286
330
|
* AND a creation time within the tolerance of a process in this pane. Add the
|
|
287
331
|
* filter once the values can be observed.
|
|
288
332
|
*/
|
|
289
|
-
export declare const detectCodexSessionName: (paneCwd: string, panePid?: number, deps?: StoreResolveDeps) =>
|
|
333
|
+
export declare const detectCodexSessionName: (paneCwd: string, panePid?: number, deps?: StoreResolveDeps) => ProviderName | null;
|
|
290
334
|
export declare const codexTranscriptPath: (paneCwd: string | null, panePid?: number) => string | null;
|
|
291
335
|
/**
|
|
292
336
|
* The transcript a Gemini pane is writing, or null when there is none to follow.
|
|
@@ -322,7 +366,7 @@ declare const REMOTE_AGENT_TABLE: readonly [{
|
|
|
322
366
|
readonly subcommands: readonly ["cc", "claude"];
|
|
323
367
|
readonly quitCommand: "/exit";
|
|
324
368
|
readonly resolveSessionId: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
325
|
-
readonly resolveSessionName: (paneCwd: string, panePid: number | undefined) =>
|
|
369
|
+
readonly resolveSessionName: (paneCwd: string, panePid: number | undefined) => ProviderName | null;
|
|
326
370
|
readonly resolveTranscript: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
327
371
|
readonly projectTranscript: TranscriptProjector;
|
|
328
372
|
readonly resolveResumeId: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
@@ -332,9 +376,10 @@ declare const REMOTE_AGENT_TABLE: readonly [{
|
|
|
332
376
|
readonly displayName: "Codex CLI";
|
|
333
377
|
readonly binary: "codex";
|
|
334
378
|
readonly subcommands: readonly ["codex"];
|
|
335
|
-
readonly resolveSessionName: (paneCwd: string, panePid?: number, deps?: StoreResolveDeps) =>
|
|
379
|
+
readonly resolveSessionName: (paneCwd: string, panePid?: number, deps?: StoreResolveDeps) => ProviderName | null;
|
|
336
380
|
readonly resolveTranscript: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
337
381
|
readonly projectTranscript: TranscriptProjector;
|
|
382
|
+
readonly resolveResumeId: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
338
383
|
}, {
|
|
339
384
|
readonly kind: "cursor";
|
|
340
385
|
readonly displayName: "Cursor CLI";
|
|
@@ -347,12 +392,14 @@ declare const REMOTE_AGENT_TABLE: readonly [{
|
|
|
347
392
|
readonly subcommands: readonly ["gemini"];
|
|
348
393
|
readonly resolveTranscript: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
349
394
|
readonly projectTranscript: TranscriptProjector;
|
|
395
|
+
readonly resolveResumeId: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
350
396
|
}, {
|
|
351
397
|
readonly kind: "hermes";
|
|
352
398
|
readonly displayName: "Hermes";
|
|
353
399
|
readonly binary: "hermes";
|
|
354
400
|
readonly subcommands: readonly ["hermes"];
|
|
355
|
-
readonly resolveSessionName: (paneCwd: string, panePid?: number, deps?: StoreResolveDeps) =>
|
|
401
|
+
readonly resolveSessionName: (paneCwd: string, panePid?: number, deps?: StoreResolveDeps) => ProviderName | null;
|
|
402
|
+
readonly resolveResumeId: (paneCwd: string, panePid?: number, deps?: StoreResolveDeps) => string | null;
|
|
356
403
|
}, {
|
|
357
404
|
readonly kind: "pi";
|
|
358
405
|
readonly displayName: "Pi";
|
|
@@ -360,6 +407,8 @@ declare const REMOTE_AGENT_TABLE: readonly [{
|
|
|
360
407
|
readonly subcommands: readonly ["pi"];
|
|
361
408
|
readonly resolveTranscript: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
362
409
|
readonly projectTranscript: TranscriptProjector;
|
|
410
|
+
readonly resolveResumeId: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
411
|
+
readonly resolveSessionName: (paneCwd: string, panePid: number | undefined) => ProviderName | null;
|
|
363
412
|
}, {
|
|
364
413
|
readonly kind: "opencode";
|
|
365
414
|
readonly displayName: "OpenCode";
|
|
@@ -367,7 +416,10 @@ declare const REMOTE_AGENT_TABLE: readonly [{
|
|
|
367
416
|
readonly subcommands: readonly ["opencode"];
|
|
368
417
|
readonly quitCommand: "/exit";
|
|
369
418
|
readonly resolveSessionId: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
370
|
-
readonly resolveSessionName: (paneCwd: string, panePid: number | undefined) =>
|
|
419
|
+
readonly resolveSessionName: (paneCwd: string, panePid: number | undefined) => {
|
|
420
|
+
name: string;
|
|
421
|
+
source: "auto";
|
|
422
|
+
} | null;
|
|
371
423
|
readonly resolveTranscript: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
372
424
|
readonly projectTranscript: TranscriptProjector;
|
|
373
425
|
readonly readTranscript: (path: string, prev: import("./transcript-tail.js").TailState | null) => import("./transcript-tail.js").TailRead | null;
|
package/dist/remote-agents.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.matchAgentByPaneCommand = exports.findAgentBySubcommand = exports.REMOTE_AGENTS = exports.opencodeTranscriptPath = exports.detectOpencodeSession = exports.geminiTranscriptPath = exports.codexTranscriptPath = exports.detectCodexSessionName = exports.newestVersionedDb = exports.detectHermesSessionName = exports.sqliteJson = exports.pickRowByProcStart = exports.piTranscriptPath = exports.claudeTranscriptPath = exports.
|
|
3
|
+
exports.matchAgentByPaneCommand = exports.findAgentBySubcommand = exports.REMOTE_AGENTS = exports.opencodeTranscriptPath = exports.detectOpencodeSession = exports.geminiTranscriptPath = exports.codexTranscriptPath = exports.detectCodexSessionName = exports.newestVersionedDb = exports.detectHermesResumeId = exports.detectHermesSessionName = exports.sqliteJson = exports.transcriptIdSince = exports.paneSince = exports.pickRowByProcStart = exports.detectPiSessionName = exports.piTranscriptPath = exports.claudeTranscriptPath = exports.detectClaudeProviderName = exports.detectClaudeSessionIdByPid = exports.detectClaudeSessionByPid = exports.pidSessionRecordsExist = exports.collectDescendantPids = exports.psStartTimes = exports.foregroundAgentFor = exports.readProcTable = exports.parseProcTable = exports.detectClaudeSessionId = exports.claudeProjectDirName = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Remote-control agent registry — the single table behind every
|
|
6
6
|
* `hwip <agent>` subcommand, the listener's pane matching, and the
|
|
@@ -22,6 +22,7 @@ const path_1 = require("path");
|
|
|
22
22
|
const child_process_1 = require("child_process");
|
|
23
23
|
const transcript_tail_js_1 = require("./transcript-tail.js");
|
|
24
24
|
const pi_transcript_js_1 = require("./pi-transcript.js");
|
|
25
|
+
const session_title_js_1 = require("./session-title.js");
|
|
25
26
|
const codex_transcript_js_1 = require("./codex-transcript.js");
|
|
26
27
|
const gemini_transcript_js_1 = require("./gemini-transcript.js");
|
|
27
28
|
const opencode_transcript_js_1 = require("./opencode-transcript.js");
|
|
@@ -158,6 +159,7 @@ const readPidSessionRecords = () => {
|
|
|
158
159
|
sessionId: raw.sessionId,
|
|
159
160
|
cwd: raw.cwd,
|
|
160
161
|
...(typeof raw.name === 'string' ? { name: raw.name } : {}),
|
|
162
|
+
...(typeof raw.nameSource === 'string' ? { nameSource: raw.nameSource } : {}),
|
|
161
163
|
});
|
|
162
164
|
}
|
|
163
165
|
}
|
|
@@ -395,14 +397,35 @@ const detectClaudeSessionByPid = (panePid, paneCwd, deps = {}) => {
|
|
|
395
397
|
exports.detectClaudeSessionByPid = detectClaudeSessionByPid;
|
|
396
398
|
const detectClaudeSessionIdByPid = (panePid, paneCwd, deps = {}) => (0, exports.detectClaudeSessionByPid)(panePid, paneCwd, deps)?.sessionId ?? null;
|
|
397
399
|
exports.detectClaudeSessionIdByPid = detectClaudeSessionIdByPid;
|
|
400
|
+
/** Claude transcripts whose latest `ai-title` the listener keeps, read incrementally. */
|
|
401
|
+
const claudeTitles = (0, session_title_js_1.createTailWatch)(session_title_js_1.latestClaudeAiTitle);
|
|
398
402
|
/**
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
+
* What the phone should call the Claude Code session in this pane, and who
|
|
404
|
+
* chose it.
|
|
405
|
+
*
|
|
406
|
+
* A `/rename` (`nameSource: "user"`) wins. Otherwise the newest `ai-title` CC
|
|
407
|
+
* wrote into the transcript — its own summary of the work, which is what tells
|
|
408
|
+
* parallel sessions apart. A derived name (`hybrid-shell-8f`, from the cwd)
|
|
409
|
+
* is never sent: it says no more than the tmux name the phone falls back to.
|
|
410
|
+
* A record from an older CC carries a name but no `nameSource`; that one is
|
|
411
|
+
* sent as before, with no source, ahead of any ai-title, since it may be the
|
|
412
|
+
* user's rename. No mtime
|
|
413
|
+
* fallback: only the pid match says which session this pane is.
|
|
403
414
|
*/
|
|
404
|
-
const
|
|
405
|
-
exports.
|
|
415
|
+
const detectClaudeProviderName = (panePid, paneCwd, deps = {}) => {
|
|
416
|
+
const record = (0, exports.detectClaudeSessionByPid)(panePid, paneCwd, deps);
|
|
417
|
+
if (!record)
|
|
418
|
+
return null;
|
|
419
|
+
const recordName = nonBlank(record.name);
|
|
420
|
+
if (recordName && record.nameSource === 'user')
|
|
421
|
+
return { name: recordName, source: 'user' };
|
|
422
|
+
if (recordName && record.nameSource === undefined)
|
|
423
|
+
return { name: recordName };
|
|
424
|
+
const transcript = (deps.transcriptOf ?? claudeConversationFile)(record.cwd, record.sessionId);
|
|
425
|
+
const title = transcript ? claudeTitles.read(transcript) : undefined;
|
|
426
|
+
return title ? { name: title, source: 'auto' } : null;
|
|
427
|
+
};
|
|
428
|
+
exports.detectClaudeProviderName = detectClaudeProviderName;
|
|
406
429
|
/**
|
|
407
430
|
* The transcript file for the Claude Code session running in a tmux pane.
|
|
408
431
|
*
|
|
@@ -559,6 +582,23 @@ const piTranscriptPath = (paneCwd, panePid) => {
|
|
|
559
582
|
return piSessionCache.set(key, doDetectPiTranscript(paneCwd));
|
|
560
583
|
};
|
|
561
584
|
exports.piTranscriptPath = piTranscriptPath;
|
|
585
|
+
const piNames = (0, session_title_js_1.createTailWatch)(session_title_js_1.piNamesStep);
|
|
586
|
+
/**
|
|
587
|
+
* The name a pi pane's session goes by, and who chose it: the newest
|
|
588
|
+
* `session_info` name, which is the agent's (`'auto'`) when the hwip
|
|
589
|
+
* extension's tool set it — the tool leaves a marker entry with the same name
|
|
590
|
+
* (`PI_TITLE_MARKER`) — and the user's (`/name`) otherwise. Read from the
|
|
591
|
+
* transcript tail, so a name written before the last 256 KB of a session the
|
|
592
|
+
* listener first meets late is not found until the next one is written.
|
|
593
|
+
*/
|
|
594
|
+
const detectPiSessionName = (paneCwd, panePid, transcriptOf = exports.piTranscriptPath) => {
|
|
595
|
+
const file = transcriptOf(paneCwd, panePid);
|
|
596
|
+
const names = file ? piNames.read(file) : undefined;
|
|
597
|
+
if (!names?.name)
|
|
598
|
+
return null;
|
|
599
|
+
return { name: names.name, source: names.marked === names.name ? 'auto' : 'user' };
|
|
600
|
+
};
|
|
601
|
+
exports.detectPiSessionName = detectPiSessionName;
|
|
562
602
|
// ── Agents whose session store keeps no pid ──────────────────────
|
|
563
603
|
//
|
|
564
604
|
// Hermes and Codex both record a session per cwd with a creation timestamp and
|
|
@@ -596,6 +636,43 @@ const pickRowByProcStart = (rows, tsMsOf, startTimes, pids) => {
|
|
|
596
636
|
return best?.row ?? null;
|
|
597
637
|
};
|
|
598
638
|
exports.pickRowByProcStart = pickRowByProcStart;
|
|
639
|
+
/**
|
|
640
|
+
* How far before the pane's start a write may land and still be the pane's: `ps` `lstart` is whole seconds. Not
|
|
641
|
+
* {@link PROC_MATCH_TOLERANCE_MS} — the last sitting's final write lands seconds before an `exit` and a fresh
|
|
642
|
+
* start, and a margin that wide would read the fresh pane as carrying it on.
|
|
643
|
+
*/
|
|
644
|
+
const PANE_WRITE_MARGIN_MS = 2_000;
|
|
645
|
+
/**
|
|
646
|
+
* From when a conversation written to is this pane's: its earliest process start, less {@link PANE_WRITE_MARGIN_MS}. A
|
|
647
|
+
* resume writes to the conversation it picked back up, so that one is written to after the start; a fresh start
|
|
648
|
+
* has not written yet, so the folder's older conversations are all before it. Null when no start is known.
|
|
649
|
+
*/
|
|
650
|
+
const paneSince = (panePid, deps = {}) => {
|
|
651
|
+
const startTimes = deps.startTimes ?? (0, exports.psStartTimes)();
|
|
652
|
+
const starts = [...(deps.descendants ?? (0, exports.collectDescendantPids)(panePid))]
|
|
653
|
+
.map((pid) => startTimes.get(pid))
|
|
654
|
+
.filter((t) => t !== undefined && Number.isFinite(t));
|
|
655
|
+
return starts.length === 0 ? null : Math.min(...starts) - PANE_WRITE_MARGIN_MS;
|
|
656
|
+
};
|
|
657
|
+
exports.paneSince = paneSince;
|
|
658
|
+
/**
|
|
659
|
+
* The conversation a transcript file holds — its name without `.jsonl` — when it was written to since `since`.
|
|
660
|
+
* For the agents whose resume appends to the file it picks back up (pi `-c`, Codex `resume`, Gemini `--resume`,
|
|
661
|
+
* measured 2026-10-20): the file is the conversation.
|
|
662
|
+
*/
|
|
663
|
+
const transcriptIdSince = (path, since) => {
|
|
664
|
+
if (!path || since === null)
|
|
665
|
+
return null;
|
|
666
|
+
try {
|
|
667
|
+
return (0, fs_1.statSync)(path).mtimeMs >= since ? (0, path_1.basename)(path, '.jsonl') : null;
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
exports.transcriptIdSince = transcriptIdSince;
|
|
674
|
+
/** {@link transcriptIdSince} for a pane: what `resolveResumeId` answers for an agent whose transcript is a file. */
|
|
675
|
+
const paneTranscriptId = (path, panePid) => panePid === undefined ? null : (0, exports.transcriptIdSince)(path, (0, exports.paneSince)(panePid));
|
|
599
676
|
/**
|
|
600
677
|
* Read rows from a SQLite file as JSON. Null on every failure — no `sqlite3` on
|
|
601
678
|
* PATH, a locked or corrupt database, a schema that moved — because a missing
|
|
@@ -652,10 +729,44 @@ const detectHermesSessionName = (paneCwd, panePid, deps = {}) => {
|
|
|
652
729
|
return null;
|
|
653
730
|
const row = (0, exports.pickRowByProcStart)(rows, (r) => (typeof r.started_at === 'number' ? r.started_at * 1_000 : null), deps.startTimes ?? (0, exports.psStartTimes)(), deps.descendants ?? (0, exports.collectDescendantPids)(panePid));
|
|
654
731
|
// The auto-title is written asynchronously after the first response, so an
|
|
655
|
-
// untitled row is a young session, not a broken one.
|
|
656
|
-
|
|
732
|
+
// untitled row is a young session, not a broken one. `title` is Hermes'
|
|
733
|
+
// own; `display_name` comes from outside its titler (a gateway fills it
|
|
734
|
+
// from the chat it serves), so it is never one to hide.
|
|
735
|
+
const title = nonBlank(row?.title ?? undefined);
|
|
736
|
+
if (title)
|
|
737
|
+
return { name: title, source: 'auto' };
|
|
738
|
+
const display = nonBlank(row?.display_name ?? undefined);
|
|
739
|
+
return display ? { name: display, source: 'user' } : null;
|
|
657
740
|
};
|
|
658
741
|
exports.detectHermesSessionName = detectHermesSessionName;
|
|
742
|
+
const readHermesActive = ttlMemo(SNAPSHOT_TTL_MS, () => (0, exports.sqliteJson)(HERMES_STATE_DB, `SELECT s.id AS id, s.cwd AS cwd, (SELECT max(m.timestamp) FROM messages m WHERE m.session_id = s.id) AS active
|
|
743
|
+
FROM sessions s WHERE s.source = 'cli' ORDER BY s.started_at DESC LIMIT ${STORE_ROW_LIMIT}`));
|
|
744
|
+
/**
|
|
745
|
+
* The Hermes session this pane carries on: in its directory, with a message since the pane started — the newest.
|
|
746
|
+
* A `-c` picks back up a session started long before the pane, which the start-time match above cannot see.
|
|
747
|
+
*/
|
|
748
|
+
const detectHermesResumeId = (paneCwd, panePid, deps = {}) => {
|
|
749
|
+
if (panePid === undefined)
|
|
750
|
+
return null;
|
|
751
|
+
const raw = deps.rows !== undefined ? deps.rows : readHermesActive();
|
|
752
|
+
if (raw === null)
|
|
753
|
+
return null;
|
|
754
|
+
const since = (0, exports.paneSince)(panePid, deps);
|
|
755
|
+
if (since === null)
|
|
756
|
+
return null;
|
|
757
|
+
let best = null;
|
|
758
|
+
// The query's own columns; each is checked with typeof below before it is used.
|
|
759
|
+
for (const row of raw) {
|
|
760
|
+
if (row.cwd !== paneCwd || typeof row.id !== 'string' || !row.id)
|
|
761
|
+
continue;
|
|
762
|
+
if (typeof row.active !== 'number' || row.active * 1_000 < since)
|
|
763
|
+
continue;
|
|
764
|
+
if (!best || row.active > (best.active ?? 0))
|
|
765
|
+
best = row;
|
|
766
|
+
}
|
|
767
|
+
return best?.id ?? null;
|
|
768
|
+
};
|
|
769
|
+
exports.detectHermesResumeId = detectHermesResumeId;
|
|
659
770
|
// ── Codex ────────────────────────────────────────────────────────
|
|
660
771
|
const CODEX_DIR = (0, path_1.join)((0, os_1.homedir)(), '.codex');
|
|
661
772
|
const CODEX_STATE_DB = /^state_(\d+)\.sqlite$/;
|
|
@@ -715,7 +826,13 @@ const detectCodexSessionName = (paneCwd, panePid, deps = {}) => {
|
|
|
715
826
|
if (rows.length === 0)
|
|
716
827
|
return null;
|
|
717
828
|
const row = (0, exports.pickRowByProcStart)(rows, (r) => r.created_at_ms ?? (typeof r.created_at === 'number' ? r.created_at * 1_000 : null), deps.startTimes ?? (0, exports.psStartTimes)(), deps.descendants ?? (0, exports.collectDescendantPids)(panePid));
|
|
718
|
-
|
|
829
|
+
// `name` is what the user typed (`/rename`); `title` is Codex's own —
|
|
830
|
+
// the first prompt, verbatim.
|
|
831
|
+
const name = nonBlank(row?.name ?? undefined);
|
|
832
|
+
if (name)
|
|
833
|
+
return { name, source: 'user' };
|
|
834
|
+
const title = nonBlank(row?.title ?? undefined);
|
|
835
|
+
return title ? { name: title, source: 'auto' } : null;
|
|
719
836
|
};
|
|
720
837
|
exports.detectCodexSessionName = detectCodexSessionName;
|
|
721
838
|
/**
|
|
@@ -1031,13 +1148,9 @@ const detectOpencodeSession = (paneCwd, panePid, deps = {}) => {
|
|
|
1031
1148
|
const raw = deps.rows !== undefined ? deps.rows : readOpencodeSessions();
|
|
1032
1149
|
if (raw === null)
|
|
1033
1150
|
return null;
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
.map((pid) => startTimes.get(pid))
|
|
1037
|
-
.filter((t) => t !== undefined && Number.isFinite(t));
|
|
1038
|
-
if (starts.length === 0)
|
|
1151
|
+
const since = (0, exports.paneSince)(panePid, deps);
|
|
1152
|
+
if (since === null)
|
|
1039
1153
|
return null;
|
|
1040
|
-
const since = Math.min(...starts) - PROC_MATCH_TOLERANCE_MS;
|
|
1041
1154
|
const dir = realDir(paneCwd);
|
|
1042
1155
|
let best = null;
|
|
1043
1156
|
for (const row of raw) {
|
|
@@ -1079,7 +1192,7 @@ const REMOTE_AGENT_TABLE = [
|
|
|
1079
1192
|
// (older CC without ~/.claude/sessions, or ps failure).
|
|
1080
1193
|
resolveSessionId: (paneCwd, panePid) => (panePid !== undefined ? (0, exports.detectClaudeSessionIdByPid)(panePid, paneCwd) : null)
|
|
1081
1194
|
?? (0, exports.detectClaudeSessionId)(paneCwd),
|
|
1082
|
-
resolveSessionName: (paneCwd, panePid) => panePid !== undefined ? (0, exports.
|
|
1195
|
+
resolveSessionName: (paneCwd, panePid) => panePid !== undefined ? (0, exports.detectClaudeProviderName)(panePid, paneCwd) : null,
|
|
1083
1196
|
resolveTranscript: (paneCwd, panePid) => (0, exports.claudeTranscriptPath)(paneCwd, panePid),
|
|
1084
1197
|
projectTranscript: transcript_tail_js_1.projectTranscriptEntries,
|
|
1085
1198
|
// The pid match alone: the mtime fallback above can name another
|
|
@@ -1098,6 +1211,8 @@ const REMOTE_AGENT_TABLE = [
|
|
|
1098
1211
|
resolveSessionName: exports.detectCodexSessionName,
|
|
1099
1212
|
resolveTranscript: (paneCwd, panePid) => (0, exports.codexTranscriptPath)(paneCwd, panePid),
|
|
1100
1213
|
projectTranscript: codex_transcript_js_1.projectCodexEntries,
|
|
1214
|
+
// Read only to tell a resumed conversation's run (no `resumeArgs`): the rollout a resume appends to.
|
|
1215
|
+
resolveResumeId: (paneCwd, panePid) => paneTranscriptId((0, exports.codexTranscriptPath)(paneCwd, panePid), panePid),
|
|
1101
1216
|
},
|
|
1102
1217
|
{
|
|
1103
1218
|
// The pane binary is `cursor-agent`, Cursor's terminal TUI — NOT the
|
|
@@ -1118,6 +1233,7 @@ const REMOTE_AGENT_TABLE = [
|
|
|
1118
1233
|
// limit pi has (newest file wins).
|
|
1119
1234
|
resolveTranscript: (paneCwd, panePid) => (0, exports.geminiTranscriptPath)(paneCwd, panePid),
|
|
1120
1235
|
projectTranscript: gemini_transcript_js_1.projectGeminiEntries,
|
|
1236
|
+
resolveResumeId: (paneCwd, panePid) => paneTranscriptId((0, exports.geminiTranscriptPath)(paneCwd, panePid), panePid),
|
|
1121
1237
|
},
|
|
1122
1238
|
{
|
|
1123
1239
|
// `hermes` on PATH is a bash script that execs a venv Python, so the
|
|
@@ -1133,6 +1249,7 @@ const REMOTE_AGENT_TABLE = [
|
|
|
1133
1249
|
// No `resolveSessionId` counterpart: the store's own id is a
|
|
1134
1250
|
// `<timestamp>_<hash>` string, and nothing on the wire consumes it yet.
|
|
1135
1251
|
resolveSessionName: exports.detectHermesSessionName,
|
|
1252
|
+
resolveResumeId: exports.detectHermesResumeId,
|
|
1136
1253
|
},
|
|
1137
1254
|
{
|
|
1138
1255
|
kind: 'pi',
|
|
@@ -1141,6 +1258,8 @@ const REMOTE_AGENT_TABLE = [
|
|
|
1141
1258
|
subcommands: ['pi'],
|
|
1142
1259
|
resolveTranscript: (paneCwd, panePid) => (0, exports.piTranscriptPath)(paneCwd, panePid),
|
|
1143
1260
|
projectTranscript: pi_transcript_js_1.projectPiEntries,
|
|
1261
|
+
resolveResumeId: (paneCwd, panePid) => paneTranscriptId((0, exports.piTranscriptPath)(paneCwd, panePid), panePid),
|
|
1262
|
+
resolveSessionName: (paneCwd, panePid) => (0, exports.detectPiSessionName)(paneCwd, panePid),
|
|
1144
1263
|
},
|
|
1145
1264
|
{
|
|
1146
1265
|
kind: 'opencode',
|
|
@@ -1150,7 +1269,11 @@ const REMOTE_AGENT_TABLE = [
|
|
|
1150
1269
|
// Typed into a 2.0.24 TUI pane: `/exit` + Enter closed it.
|
|
1151
1270
|
quitCommand: '/exit',
|
|
1152
1271
|
resolveSessionId: (paneCwd, panePid) => (0, exports.detectOpencodeSession)(paneCwd, panePid)?.id ?? null,
|
|
1153
|
-
|
|
1272
|
+
// opencode titles a session itself after the first exchange.
|
|
1273
|
+
resolveSessionName: (paneCwd, panePid) => {
|
|
1274
|
+
const title = (0, exports.detectOpencodeSession)(paneCwd, panePid)?.title;
|
|
1275
|
+
return title ? { name: title, source: 'auto' } : null;
|
|
1276
|
+
},
|
|
1154
1277
|
resolveTranscript: (paneCwd, panePid) => (0, exports.opencodeTranscriptPath)(paneCwd, panePid),
|
|
1155
1278
|
projectTranscript: opencode_transcript_js_1.projectOpencodeEntries,
|
|
1156
1279
|
readTranscript: (path, prev) => (0, opencode_transcript_js_1.readOpencodeTranscript)(path, prev, { query: queryOpencodeTranscript }),
|
|
@@ -37,6 +37,12 @@ export interface KnownSession {
|
|
|
37
37
|
* agent whose id this machine cannot read, and in files written before it.
|
|
38
38
|
*/
|
|
39
39
|
agentSessionId?: string;
|
|
40
|
+
/**
|
|
41
|
+
* The later tmux sessions this run went on in (`session_created`, as
|
|
42
|
+
* `runId`): the same conversation carried on after an `exit` is the same
|
|
43
|
+
* run (`continuedRunId`). Absent for a run that has had one tmux session.
|
|
44
|
+
*/
|
|
45
|
+
tmuxRunIds?: string[];
|
|
40
46
|
/** Last time this machine saw the session running. */
|
|
41
47
|
lastSeenAt: string;
|
|
42
48
|
}
|
|
@@ -86,6 +92,20 @@ export declare const recallSession: (name: string, now?: number) => KnownSession
|
|
|
86
92
|
* decides whether to fall back to the newest run or refuse.
|
|
87
93
|
*/
|
|
88
94
|
export declare const recallRun: (name: string, agentKind: string, now?: number) => KnownSession | null;
|
|
95
|
+
/** tmux sessions remembered per run: an `exit` and `-c` every few minutes all day stays a short list. */
|
|
96
|
+
export declare const MAX_TMUX_RUNS_PER_RUN = 10;
|
|
97
|
+
/**
|
|
98
|
+
* Which run the tmux session `tmuxRunId` of `name` is (hwip docs/session-runs.md §3).
|
|
99
|
+
*
|
|
100
|
+
* Its own, unless it carries on an earlier run: it was already joined to one
|
|
101
|
+
* (a sweep or a listener restart since), or the agent's conversation
|
|
102
|
+
* `conversationId` is the one an earlier run of the same name and agent last
|
|
103
|
+
* had. A tmux session that has had a conversation of its own stays its own run
|
|
104
|
+
* — a fresh start `/resume`d into an old conversation keeps what it wrote first.
|
|
105
|
+
*/
|
|
106
|
+
export declare const continuedRunId: (entries: readonly KnownSession[], name: string, agentKind: string, tmuxRunId: string, conversationId: string | null | undefined) => string;
|
|
107
|
+
/** The run going on in tmux session `tmuxRunId` of `name`: the one it was joined to, else its own. */
|
|
108
|
+
export declare const runOfTmuxSession: (name: string, tmuxRunId: string, now?: number) => string;
|
|
89
109
|
/**
|
|
90
110
|
* Make this process a reader of the registry: `rememberSessions` writes nothing from here on. For the
|
|
91
111
|
* CLI commands that look at the sweep (`hwip agent list|read`) — the file is the listener's resume
|
|
@@ -110,6 +130,8 @@ export declare const rememberSessions: (live: Array<{
|
|
|
110
130
|
label?: string | null;
|
|
111
131
|
runId?: string;
|
|
112
132
|
startedAt?: string;
|
|
133
|
+
/** The tmux session it runs in; one other than `runId` means the run goes on in it (`continuedRunId`). */
|
|
134
|
+
tmuxRunId?: string;
|
|
113
135
|
/**
|
|
114
136
|
* The conversation a resume may pick up. Absent: not readable this
|
|
115
137
|
* sweep, so the run keeps the one it had. Null: read, but not this
|
package/dist/session-registry.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* another program.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.sessionDirectoryExists = exports.knownSessionsPath = exports.isKnownSession = exports.forgetEnded = exports.forgetSession = exports.rememberSessions = exports.keepRegistryUntouched = exports.recallRun = exports.recallSession = exports.knownSessions = exports.runKey = exports.KNOWN_SESSION_TTL_MS = exports.MAX_RUNS_PER_NAME = exports.MAX_KNOWN_SESSIONS = void 0;
|
|
19
|
+
exports.sessionDirectoryExists = exports.knownSessionsPath = exports.isKnownSession = exports.forgetEnded = exports.forgetSession = exports.rememberSessions = exports.keepRegistryUntouched = exports.runOfTmuxSession = exports.continuedRunId = exports.MAX_TMUX_RUNS_PER_RUN = exports.recallRun = exports.recallSession = exports.knownSessions = exports.runKey = exports.KNOWN_SESSION_TTL_MS = exports.MAX_RUNS_PER_NAME = exports.MAX_KNOWN_SESSIONS = void 0;
|
|
20
20
|
const fs_1 = require("fs");
|
|
21
21
|
const path_1 = require("path");
|
|
22
22
|
const turn_ring_js_1 = require("./turn-ring.js");
|
|
@@ -35,6 +35,7 @@ exports.MAX_RUNS_PER_NAME = 10;
|
|
|
35
35
|
* likely to have moved than to be what the user meant. */
|
|
36
36
|
exports.KNOWN_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
37
37
|
const registryPath = () => (0, path_1.join)((0, gate_js_1.stateDir)(), 'known-sessions.json');
|
|
38
|
+
const isTmuxRunIds = (v) => v === undefined || (Array.isArray(v) && v.every((id) => typeof id === 'string'));
|
|
38
39
|
const readAll = () => {
|
|
39
40
|
try {
|
|
40
41
|
const raw = (0, fs_1.readFileSync)(registryPath(), 'utf-8');
|
|
@@ -49,7 +50,8 @@ const readAll = () => {
|
|
|
49
50
|
typeof e.cwd === 'string' &&
|
|
50
51
|
typeof e.agentKind === 'string' &&
|
|
51
52
|
typeof e.lastSeenAt === 'string' &&
|
|
52
|
-
(e.runId === undefined || typeof e.runId === 'string')
|
|
53
|
+
(e.runId === undefined || typeof e.runId === 'string') &&
|
|
54
|
+
isTmuxRunIds(e.tmuxRunIds));
|
|
53
55
|
}
|
|
54
56
|
catch {
|
|
55
57
|
return [];
|
|
@@ -109,6 +111,41 @@ exports.recallSession = recallSession;
|
|
|
109
111
|
*/
|
|
110
112
|
const recallRun = (name, agentKind, now = Date.now()) => (0, exports.knownSessions)(now).find((e) => e.name === name && e.agentKind === agentKind) ?? null;
|
|
111
113
|
exports.recallRun = recallRun;
|
|
114
|
+
/** tmux sessions remembered per run: an `exit` and `-c` every few minutes all day stays a short list. */
|
|
115
|
+
exports.MAX_TMUX_RUNS_PER_RUN = 10;
|
|
116
|
+
/**
|
|
117
|
+
* Agents whose own resume is measured to keep the conversation id: Claude Code 2.1.300's `-c`, opencode 2.0.29's
|
|
118
|
+
* `-c`, pi 1.2.0's `-c`, Codex 0.163.0's `resume --last`, Gemini 0.46.0's `--resume latest` and Hermes 0.20.0's
|
|
119
|
+
* `-c` (2026-10-20). Cursor's was not measured (signed out) and the others are not installed here.
|
|
120
|
+
*/
|
|
121
|
+
const CONTINUING_KINDS = new Set(['claude', 'opencode', 'pi', 'codex', 'gemini', 'hermes']);
|
|
122
|
+
/**
|
|
123
|
+
* Which run the tmux session `tmuxRunId` of `name` is (hwip docs/session-runs.md §3).
|
|
124
|
+
*
|
|
125
|
+
* Its own, unless it carries on an earlier run: it was already joined to one
|
|
126
|
+
* (a sweep or a listener restart since), or the agent's conversation
|
|
127
|
+
* `conversationId` is the one an earlier run of the same name and agent last
|
|
128
|
+
* had. A tmux session that has had a conversation of its own stays its own run
|
|
129
|
+
* — a fresh start `/resume`d into an old conversation keeps what it wrote first.
|
|
130
|
+
*/
|
|
131
|
+
const continuedRunId = (entries, name, agentKind, tmuxRunId, conversationId) => {
|
|
132
|
+
const ofName = entries.filter((e) => e.name === name && e.agentKind === agentKind && e.runId);
|
|
133
|
+
const joined = ofName.find((e) => e.runId !== tmuxRunId && e.tmuxRunIds?.includes(tmuxRunId));
|
|
134
|
+
if (joined?.runId)
|
|
135
|
+
return joined.runId;
|
|
136
|
+
if (!conversationId || !CONTINUING_KINDS.has(agentKind))
|
|
137
|
+
return tmuxRunId;
|
|
138
|
+
if (ofName.some((e) => e.runId === tmuxRunId && e.agentSessionId))
|
|
139
|
+
return tmuxRunId;
|
|
140
|
+
const earlier = ofName
|
|
141
|
+
.filter((e) => e.agentSessionId === conversationId && Number(e.runId) < Number(tmuxRunId))
|
|
142
|
+
.sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt))[0];
|
|
143
|
+
return earlier?.runId ?? tmuxRunId;
|
|
144
|
+
};
|
|
145
|
+
exports.continuedRunId = continuedRunId;
|
|
146
|
+
/** The run going on in tmux session `tmuxRunId` of `name`: the one it was joined to, else its own. */
|
|
147
|
+
const runOfTmuxSession = (name, tmuxRunId, now = Date.now()) => (0, exports.knownSessions)(now).find((e) => e.name === name && e.tmuxRunIds?.includes(tmuxRunId))?.runId ?? tmuxRunId;
|
|
148
|
+
exports.runOfTmuxSession = runOfTmuxSession;
|
|
112
149
|
let readOnly = false;
|
|
113
150
|
/**
|
|
114
151
|
* Make this process a reader of the registry: `rememberSessions` writes nothing from here on. For the
|
|
@@ -146,6 +183,10 @@ const rememberSessions = (live, now = Date.now()) => {
|
|
|
146
183
|
if (before && before.lastSeenAt >= s.startedAt)
|
|
147
184
|
byRun.delete((0, exports.runKey)(s.name, s.agentKind));
|
|
148
185
|
}
|
|
186
|
+
// Carried on in a new tmux session: what an earlier sweep wrote under that session is this run.
|
|
187
|
+
const continued = !!s.runId && !!s.tmuxRunId && s.tmuxRunId !== s.runId;
|
|
188
|
+
if (continued)
|
|
189
|
+
byRun.delete((0, exports.runKey)(s.name, s.agentKind, s.tmuxRunId));
|
|
149
190
|
const key = (0, exports.runKey)(s.name, s.agentKind, s.runId);
|
|
150
191
|
// A sweep that could not read the id — the agent has not written its
|
|
151
192
|
// record yet — keeps the one this run already had. Keyed by run and
|
|
@@ -154,6 +195,11 @@ const rememberSessions = (live, now = Date.now()) => {
|
|
|
154
195
|
const prev = byRun.get(key);
|
|
155
196
|
const keepPrev = s.agentSessionId === undefined && prev?.cwd === s.cwd;
|
|
156
197
|
const agentSessionId = s.agentSessionId || (keepPrev ? prev?.agentSessionId : undefined);
|
|
198
|
+
// A run keeps its first start whichever tmux session it goes on in.
|
|
199
|
+
const startedAt = continued ? prev?.startedAt ?? s.startedAt : s.startedAt;
|
|
200
|
+
const tmuxRunIds = continued && s.tmuxRunId
|
|
201
|
+
? [...(prev?.tmuxRunIds ?? []).filter((id) => id !== s.tmuxRunId), s.tmuxRunId].slice(-exports.MAX_TMUX_RUNS_PER_RUN)
|
|
202
|
+
: prev?.tmuxRunIds;
|
|
157
203
|
byRun.set(key, {
|
|
158
204
|
name: s.name,
|
|
159
205
|
cwd: s.cwd,
|
|
@@ -161,8 +207,9 @@ const rememberSessions = (live, now = Date.now()) => {
|
|
|
161
207
|
...(s.project ? { project: s.project } : {}),
|
|
162
208
|
...(s.label ? { label: s.label } : {}),
|
|
163
209
|
...(s.runId ? { runId: s.runId } : {}),
|
|
164
|
-
...(
|
|
210
|
+
...(startedAt ? { startedAt } : {}),
|
|
165
211
|
...(agentSessionId ? { agentSessionId } : {}),
|
|
212
|
+
...(tmuxRunIds?.length ? { tmuxRunIds } : {}),
|
|
166
213
|
lastSeenAt: seenAt,
|
|
167
214
|
});
|
|
168
215
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The latest value a session transcript states for something — Claude Code's
|
|
3
|
+
* `ai-title`, pi's `session_info` name — kept current without re-reading the
|
|
4
|
+
* file every report cycle.
|
|
5
|
+
*
|
|
6
|
+
* The listener resolves every session's name every ~5 s, and a transcript runs
|
|
7
|
+
* to tens of megabytes. So each file is read the way the chat tail reads it:
|
|
8
|
+
* the last `TRANSCRIPT_BACKFILL_BYTES` once, then only what was appended, and
|
|
9
|
+
* nothing at all while the file is unchanged. `step` folds each new line into
|
|
10
|
+
* the value, so a chunk with no line it cares about leaves the value as it was.
|
|
11
|
+
*/
|
|
12
|
+
import { type TailRead, type TailState } from './transcript-tail.js';
|
|
13
|
+
/** Who named a session: someone in the agent (`/rename`, `/name`), or the agent itself. */
|
|
14
|
+
export type ProviderNameSource = 'user' | 'auto';
|
|
15
|
+
/** A session name as an agent reports it. No `source` when the agent's store does not say. */
|
|
16
|
+
export interface ProviderName {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly source?: ProviderNameSource;
|
|
19
|
+
}
|
|
20
|
+
export declare const createTailWatch: <T>(step: (value: T | undefined, line: string) => T | undefined, read?: (path: string, prev: TailState | null) => TailRead | null, capacity?: number) => {
|
|
21
|
+
read: (path: string) => T | undefined;
|
|
22
|
+
size: () => number;
|
|
23
|
+
has: (path: string) => boolean;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The title out of a Claude Code `{"type":"ai-title","aiTitle":"…"}` line.
|
|
27
|
+
* The substring test runs first so the other few thousand lines of a
|
|
28
|
+
* transcript are never handed to `JSON.parse`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const claudeAiTitleOf: (line: string) => string | undefined;
|
|
31
|
+
/** The newest ai-title so far: a line without one keeps the last. */
|
|
32
|
+
export declare const latestClaudeAiTitle: (title: string | undefined, line: string) => string | undefined;
|
|
33
|
+
/** The custom entry the hwip pi extension leaves beside a name its tool set (PI_EXTENSION). */
|
|
34
|
+
export declare const PI_TITLE_MARKER = "hwip-session-name";
|
|
35
|
+
/**
|
|
36
|
+
* A pi session's naming state: the newest `session_info` name (`/name`, or the
|
|
37
|
+
* hwip tool through `pi.setSessionName`) and the newest hwip marker's name.
|
|
38
|
+
* pi records no author for a name, so the marker is how the tool's own are
|
|
39
|
+
* told apart: the name is the agent's only while it equals the marker's.
|
|
40
|
+
*/
|
|
41
|
+
export interface PiNames {
|
|
42
|
+
readonly name?: string;
|
|
43
|
+
readonly marked?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare const piNamesStep: (names: PiNames | undefined, line: string) => PiNames | undefined;
|
|
46
|
+
//# sourceMappingURL=session-title.d.ts.map
|