@agentclientprotocol/claude-agent-acp 0.88.1-preview.5 → 0.88.1-preview.7

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.
Files changed (62) hide show
  1. package/dist/acp-agent.d.ts +39 -2
  2. package/dist/acp-agent.d.ts.map +1 -1
  3. package/dist/acp-agent.js +120 -35
  4. package/dist/air-extension.d.ts +23 -0
  5. package/dist/air-extension.d.ts.map +1 -1
  6. package/dist/air-extension.js +23 -0
  7. package/dist/diff.d.ts +2 -0
  8. package/dist/diff.d.ts.map +1 -1
  9. package/dist/diff.js +37 -1
  10. package/dist/mcp-command.d.ts.map +1 -1
  11. package/dist/mcp-command.js +5 -0
  12. package/dist/session-index/activity.d.ts +56 -0
  13. package/dist/session-index/activity.d.ts.map +1 -0
  14. package/dist/session-index/activity.js +96 -0
  15. package/dist/session-index/archive-title.d.ts +52 -0
  16. package/dist/session-index/archive-title.d.ts.map +1 -0
  17. package/dist/session-index/archive-title.js +77 -0
  18. package/dist/session-index/connection.d.ts +152 -0
  19. package/dist/session-index/connection.d.ts.map +1 -0
  20. package/dist/session-index/connection.js +310 -0
  21. package/dist/session-index/dir-listing.d.ts +30 -0
  22. package/dist/session-index/dir-listing.d.ts.map +1 -0
  23. package/dist/session-index/dir-listing.js +110 -0
  24. package/dist/session-index/explicit-title.d.ts +34 -0
  25. package/dist/session-index/explicit-title.d.ts.map +1 -0
  26. package/dist/session-index/explicit-title.js +65 -0
  27. package/dist/session-index/first-prompt.d.ts +37 -0
  28. package/dist/session-index/first-prompt.d.ts.map +1 -0
  29. package/dist/session-index/first-prompt.js +195 -0
  30. package/dist/session-index/list-subscriptions.d.ts +190 -0
  31. package/dist/session-index/list-subscriptions.d.ts.map +1 -0
  32. package/dist/session-index/list-subscriptions.js +1163 -0
  33. package/dist/session-index/live-registry.d.ts +72 -0
  34. package/dist/session-index/live-registry.d.ts.map +1 -0
  35. package/dist/session-index/live-registry.js +240 -0
  36. package/dist/session-index/project-dirs.d.ts +45 -0
  37. package/dist/session-index/project-dirs.d.ts.map +1 -0
  38. package/dist/session-index/project-dirs.js +122 -0
  39. package/dist/session-index/service.d.ts +158 -0
  40. package/dist/session-index/service.d.ts.map +1 -0
  41. package/dist/session-index/service.js +546 -0
  42. package/dist/session-index/session-index.d.ts +201 -0
  43. package/dist/session-index/session-index.d.ts.map +1 -0
  44. package/dist/session-index/session-index.js +740 -0
  45. package/dist/session-index/session-info.d.ts +16 -0
  46. package/dist/session-index/session-info.d.ts.map +1 -0
  47. package/dist/session-index/session-info.js +51 -0
  48. package/dist/session-index/transcript-scan.d.ts +137 -0
  49. package/dist/session-index/transcript-scan.d.ts.map +1 -0
  50. package/dist/session-index/transcript-scan.js +545 -0
  51. package/dist/session-index/worktrees.d.ts +25 -0
  52. package/dist/session-index/worktrees.d.ts.map +1 -0
  53. package/dist/session-index/worktrees.js +128 -0
  54. package/dist/session-titles.d.ts +21 -0
  55. package/dist/session-titles.d.ts.map +1 -1
  56. package/dist/session-titles.js +101 -5
  57. package/dist/utils.d.ts +31 -0
  58. package/dist/utils.d.ts.map +1 -1
  59. package/dist/utils.js +60 -2
  60. package/dist/v2/agent.d.ts.map +1 -1
  61. package/dist/v2/agent.js +33 -4
  62. package/package.json +1 -1
package/dist/diff.js CHANGED
@@ -456,11 +456,47 @@ function textLines(text) {
456
456
  * its time budget.
457
457
  */
458
458
  function filePatch(oldText, newText) {
459
- const hunks = diffHunks(oldText, newText);
459
+ const hunks = memoizedDiffHunks(oldText, newText);
460
460
  if (!hunks || hunks.length === 0)
461
461
  return undefined;
462
462
  return { change: oldText === null ? "create" : "update", hunks };
463
463
  }
464
+ /** The number of recent line diffs that {@link memoizedDiffHunks} keeps. */
465
+ const DIFF_MEMO_SIZE = 4;
466
+ /** Recent line diffs not read again yet, the most recent last. A text of at
467
+ * most {@link MAX_PATCH_FILE_BYTES} is held until it is read again or later
468
+ * diffs push it out. */
469
+ const diffMemo = [];
470
+ /**
471
+ * {@link diffHunks} of a recent pair of texts again, without a second diff.
472
+ *
473
+ * The approval of an Edit or Write diffs the file against the predicted text,
474
+ * and the PostToolUse hook diffs the same texts once Claude wrote them. On a
475
+ * large file each diff can block the event loop for its whole time budget.
476
+ * Only a diff that completed is remembered: one that ran out of its budget
477
+ * says nothing about the texts, so a later call diffs them again. The hunks
478
+ * are shared, and no caller changes them.
479
+ */
480
+ function memoizedDiffHunks(oldText, newText) {
481
+ const index = diffMemo.findIndex((entry) => entry.oldText === oldText && entry.newText === newText);
482
+ if (index >= 0) {
483
+ // The hook is normally the last reader of a pair, so the entry goes: the
484
+ // memo does not hold two large texts longer than needed.
485
+ const [entry] = diffMemo.splice(index, 1);
486
+ return entry.hunks;
487
+ }
488
+ const hunks = diffHunks(oldText, newText);
489
+ if (hunks) {
490
+ diffMemo.push({ oldText, newText, hunks });
491
+ if (diffMemo.length > DIFF_MEMO_SIZE)
492
+ diffMemo.shift();
493
+ }
494
+ return hunks;
495
+ }
496
+ /** Forget the remembered line diffs. For tests. */
497
+ export function clearDiffMemo() {
498
+ diffMemo.length = 0;
499
+ }
464
500
  /**
465
501
  * Reads a file as patch input.
466
502
  *
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-command.d.ts","sourceRoot":"","sources":["../src/mcp-command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AAI7E;;2BAE2B;AAC3B,eAAO,MAAM,qBAAqB,EAAE,gBAInC,CAAC;AAQF;;6EAE6E;AAC7E,MAAM,MAAM,UAAU,GAClB;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,GACpB;IAAE,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAElF;;+DAE+D;AAC/D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAW/D;AAED;;0CAE0C;AAC1C,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,iBAAiB,CAAC,EACrC,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAClC,OAAO,CAAC,eAAe,EAAE,GAAG,IAAI,CAAC,CAkBnC;AAuED;;;;kBAIkB;AAClB,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiBnD;AA2DD;;8BAE8B;AAC9B,wBAAgB,eAAe,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,MAAM,CA6BnE;AAED,8CAA8C;AAC9C,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,KAAK,EACL,iBAAiB,GAAG,oBAAoB,GAAG,iBAAiB,CAC7D,CAAC;AAEF;;kCAEkC;AAClC,MAAM,MAAM,iBAAiB,GAAG;IAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AA4DnF;;;;;sDAKsD;AACtD,wBAAsB,aAAa,CACjC,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,UAAU,EACnB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAClC,OAAO,CAAC,iBAAiB,CAAC,CAoD5B"}
1
+ {"version":3,"file":"mcp-command.d.ts","sourceRoot":"","sources":["../src/mcp-command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AAI7E;;2BAE2B;AAC3B,eAAO,MAAM,qBAAqB,EAAE,gBAInC,CAAC;AAQF;;6EAE6E;AAC7E,MAAM,MAAM,UAAU,GAClB;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,GACpB;IAAE,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAElF;;+DAE+D;AAC/D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAe/D;AAED;;0CAE0C;AAC1C,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,iBAAiB,CAAC,EACrC,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAClC,OAAO,CAAC,eAAe,EAAE,GAAG,IAAI,CAAC,CAkBnC;AAuED;;;;kBAIkB;AAClB,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiBnD;AA2DD;;8BAE8B;AAC9B,wBAAgB,eAAe,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,MAAM,CA6BnE;AAED,8CAA8C;AAC9C,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,KAAK,EACL,iBAAiB,GAAG,oBAAoB,GAAG,iBAAiB,CAC7D,CAAC;AAEF;;kCAEkC;AAClC,MAAM,MAAM,iBAAiB,GAAG;IAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AA4DnF;;;;;sDAKsD;AACtD,wBAAsB,aAAa,CACjC,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,UAAU,EACnB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAClC,OAAO,CAAC,iBAAiB,CAAC,CAoD5B"}
@@ -16,6 +16,11 @@ const MCP_ACTION_TIMEOUT_MS = 30_000;
16
16
  * or `/mcp disable` with an optional server name or `all`. Any other `/mcp`
17
17
  * argument gives null, and the prompt gets no replacement. */
18
18
  export function parseMcpCommand(text) {
19
+ // The first word is `/mcp` only when the text starts with it after the
20
+ // leading whitespace. Checking that first keeps a long prompt from being
21
+ // split into words. `trimStart` removes the characters that `\s` matches.
22
+ if (!text.trimStart().startsWith("/mcp"))
23
+ return null;
19
24
  const words = text.trim().split(/\s+/);
20
25
  if (words[0] !== "/mcp")
21
26
  return null;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The `activity` and `cost` of a session list row.
3
+ *
4
+ * A session that this connection runs reports the SDK's own state, and
5
+ * `error` when it is idle and its last turn failed with an error (not a
6
+ * cancel). Another session reports what the CLI registry and the transcript
7
+ * tell:
8
+ *
9
+ * - no live process holds it: `idle`, or `error` when its last turn ended
10
+ * with an API error;
11
+ * - an interactive CLI that is not driven by an SDK holds it, and its registry
12
+ * status is newer than the transcript: the registry status (an idle one is
13
+ * `error` after such a turn);
14
+ * - otherwise the transcript tail: a finished turn is `idle` (`error` after
15
+ * an API error), an unfinished turn written in the last 10 minutes is
16
+ * `running`, and anything else has no known state.
17
+ *
18
+ * A user interrupt is no error. A new turn clears it.
19
+ *
20
+ * The SDK-driven CLIs (`entrypoint: sdk-*`, which includes the ones this
21
+ * adapter starts) keep `busy` in the registry long after a turn ends, so their
22
+ * registry status is not used.
23
+ */
24
+ import type { LiveRecord } from "./live-registry.js";
25
+ import type { TranscriptFacts } from "./transcript-scan.js";
26
+ export type ActivityState = "running" | "idle" | "requires_action" | "error";
27
+ export type SessionActivity = {
28
+ state?: ActivityState;
29
+ lastTurnEndedAt?: string;
30
+ };
31
+ /** What this connection knows about a session it runs. */
32
+ export type OwnSessionState = {
33
+ state?: ActivityState;
34
+ /** Epoch ms of the end of the last turn. */
35
+ lastTurnEndedAt?: number;
36
+ /** `total_cost_usd` of the last result. */
37
+ costUsd?: number;
38
+ /** The last turn failed with an error (not a cancel). */
39
+ lastTurnFailed?: boolean;
40
+ /** The query of the session failed here at this time (epoch ms) and no
41
+ * longer runs: the session is reported as any other one, `error` until
42
+ * its transcript shows a prompt or a turn end after it. */
43
+ queryFailedAt?: number;
44
+ };
45
+ export declare function deriveActivity(input: {
46
+ own?: OwnSessionState;
47
+ live?: LiveRecord;
48
+ facts: TranscriptFacts;
49
+ transcriptMtimeMs: number;
50
+ now: number;
51
+ }): SessionActivity | undefined;
52
+ /** The cost of a session this connection runs: the `total_cost_usd` the SDK
53
+ * gave in its last result. Only a positive amount; none for any other
54
+ * session. */
55
+ export declare function selectCost(own: OwnSessionState | undefined): number | undefined;
56
+ //# sourceMappingURL=activity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../../src/session-index/activity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,iBAAiB,GAAG,OAAO,CAAC;AAE7E,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,0DAA0D;AAC1D,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,4CAA4C;IAC5C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;gEAE4D;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AA+BF,wBAAgB,cAAc,CAAC,KAAK,EAAE;IACpC,GAAG,CAAC,EAAE,eAAe,CAAC;IACtB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,eAAe,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,eAAe,GAAG,SAAS,CAqC9B;AAED;;eAEe;AACf,wBAAgB,UAAU,CAAC,GAAG,EAAE,eAAe,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAG/E"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The `activity` and `cost` of a session list row.
3
+ *
4
+ * A session that this connection runs reports the SDK's own state, and
5
+ * `error` when it is idle and its last turn failed with an error (not a
6
+ * cancel). Another session reports what the CLI registry and the transcript
7
+ * tell:
8
+ *
9
+ * - no live process holds it: `idle`, or `error` when its last turn ended
10
+ * with an API error;
11
+ * - an interactive CLI that is not driven by an SDK holds it, and its registry
12
+ * status is newer than the transcript: the registry status (an idle one is
13
+ * `error` after such a turn);
14
+ * - otherwise the transcript tail: a finished turn is `idle` (`error` after
15
+ * an API error), an unfinished turn written in the last 10 minutes is
16
+ * `running`, and anything else has no known state.
17
+ *
18
+ * A user interrupt is no error. A new turn clears it.
19
+ *
20
+ * The SDK-driven CLIs (`entrypoint: sdk-*`, which includes the ones this
21
+ * adapter starts) keep `busy` in the registry long after a turn ends, so their
22
+ * registry status is not used.
23
+ */
24
+ const RECENT_UNFINISHED_TURN_MS = 10 * 60 * 1000;
25
+ function registryState(status) {
26
+ switch (status) {
27
+ case "busy":
28
+ case "shell":
29
+ return "running";
30
+ case "waiting":
31
+ return "requires_action";
32
+ case "idle":
33
+ return "idle";
34
+ default:
35
+ return undefined;
36
+ }
37
+ }
38
+ function usesRegistryStatus(record, transcriptMtimeMs) {
39
+ return (record.kind === "interactive" &&
40
+ !(record.entrypoint ?? "").startsWith("sdk-") &&
41
+ record.statusUpdatedAt !== undefined &&
42
+ record.statusUpdatedAt > transcriptMtimeMs);
43
+ }
44
+ function iso(ms) {
45
+ return ms === undefined || !Number.isFinite(ms) ? undefined : new Date(ms).toISOString();
46
+ }
47
+ export function deriveActivity(input) {
48
+ const { live, facts, transcriptMtimeMs, now } = input;
49
+ const failedAt = input.own?.queryFailedAt;
50
+ const own = failedAt === undefined ? input.own : undefined;
51
+ let state;
52
+ // A finished turn that ended with an API error, or this connection's query
53
+ // of the session failed and nothing was written since.
54
+ const failed = (facts.turnState === "finished" && facts.lastTurnError === true) ||
55
+ (failedAt !== undefined &&
56
+ !((facts.lastPromptAt ?? 0) > failedAt) &&
57
+ !((facts.lastTurnEndedAt ?? 0) > failedAt));
58
+ if (own) {
59
+ state = own.state ?? "idle";
60
+ if (state === "idle" && own.lastTurnFailed)
61
+ state = "error";
62
+ }
63
+ else if (!live) {
64
+ state = failed ? "error" : "idle";
65
+ }
66
+ else if (usesRegistryStatus(live, transcriptMtimeMs)) {
67
+ // A busy or waiting CLI wins; an idle one shows how its turn ended.
68
+ state = registryState(live.status);
69
+ if (state === "idle" && failed)
70
+ state = "error";
71
+ }
72
+ else if (failed) {
73
+ state = "error";
74
+ }
75
+ else if (facts.turnState === "finished") {
76
+ state = "idle";
77
+ }
78
+ else if (facts.turnState === "unfinished" &&
79
+ now - transcriptMtimeMs < RECENT_UNFINISHED_TURN_MS) {
80
+ state = "running";
81
+ }
82
+ const lastTurnEndedAt = iso(own?.lastTurnEndedAt ?? facts.lastTurnEndedAt);
83
+ if (state === undefined && lastTurnEndedAt === undefined)
84
+ return undefined;
85
+ return {
86
+ ...(state !== undefined && { state }),
87
+ ...(lastTurnEndedAt !== undefined && { lastTurnEndedAt }),
88
+ };
89
+ }
90
+ /** The cost of a session this connection runs: the `total_cost_usd` the SDK
91
+ * gave in its last result. Only a positive amount; none for any other
92
+ * session. */
93
+ export function selectCost(own) {
94
+ const amount = own?.costUsd;
95
+ return amount !== undefined && Number.isFinite(amount) && amount > 0 ? amount : undefined;
96
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The session archive, in the on-disk format of JetBrains AIR's own Claude
3
+ * integration: a session is archived when its title carries the
4
+ * `[archived] ` prefix, and archive, unarchive and rename append the title as
5
+ * a pair of records to the transcript:
6
+ *
7
+ * ```
8
+ * {"type":"custom-title","customTitle":"[archived] Title","sessionId":"…"}
9
+ * {"type":"agent-name","agentName":"[archived] Title","sessionId":"…"}
10
+ * ```
11
+ *
12
+ * The archive state follows AIR's native Claude provider (see
13
+ * {@link effectiveTitle}): the last agent name decides, else the custom title
14
+ * the SDK reports (`SDKSessionInfo.customTitle`). Archive writes the prefix
15
+ * and the current title without it; unarchive writes the title without the
16
+ * prefix. Both are cut to the CLI's 200-character title limit. The records
17
+ * are metadata: they move neither `updatedAt` nor the list order. The
18
+ * `claude --resume` picker shows the prefix.
19
+ */
20
+ /** The title prefix of an archived session. */
21
+ export declare const ARCHIVED_TITLE_PREFIX = "[archived] ";
22
+ /** Whitespace collapsed to single spaces and trimmed, as AIR stores a title. */
23
+ export declare function normalizeStoredTitle(title: string): string;
24
+ /** Whether a session name (see {@link effectiveTitle}) marks its session
25
+ * archived. */
26
+ export declare function isArchivedTitle(title: string | undefined): boolean;
27
+ /** The title shown for a stored title: without the archive prefix,
28
+ * removed once, as AIR shows it. */
29
+ export declare function visibleTitle(title: string): string;
30
+ /** The title of a session that has none at all, as AIR names it. */
31
+ export declare function defaultSessionTitle(sessionId: string): string;
32
+ /** The title that `title` is stored as in the given archive state: the
33
+ * visible title, with the prefix when archived, normalized and cut to the
34
+ * CLI limit. */
35
+ export declare function storedTitle(title: string, archived: boolean, sessionId: string): string;
36
+ /**
37
+ * The title of a session and its archive state, by the rule of AIR's native
38
+ * Claude provider: the session's name is its last agent name, else its custom
39
+ * title, and the session is archived exactly when that name starts with the
40
+ * prefix. The title is that agent name, else `summary`, the title the SDK
41
+ * gives the session; it is stored, so it may carry the prefix.
42
+ */
43
+ export declare function effectiveTitle(agentName: string | undefined, { customTitle, summary }: {
44
+ customTitle?: string;
45
+ summary?: string;
46
+ }): {
47
+ title: string | undefined;
48
+ archived: boolean;
49
+ };
50
+ /** The `custom-title` and `agent-name` records of `title`, one per line. */
51
+ export declare function titleRecords(sessionId: string, title: string): string;
52
+ //# sourceMappingURL=archive-title.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-title.d.ts","sourceRoot":"","sources":["../../src/session-index/archive-title.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,+CAA+C;AAC/C,eAAO,MAAM,qBAAqB,gBAAgB,CAAC;AAenD,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D;AAOD;gBACgB;AAChB,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAElE;AAED;qCACqC;AACrC,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;iBAEiB;AACjB,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAKvF;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACnE;IAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAElD;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAOrE"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * The session archive, in the on-disk format of JetBrains AIR's own Claude
3
+ * integration: a session is archived when its title carries the
4
+ * `[archived] ` prefix, and archive, unarchive and rename append the title as
5
+ * a pair of records to the transcript:
6
+ *
7
+ * ```
8
+ * {"type":"custom-title","customTitle":"[archived] Title","sessionId":"…"}
9
+ * {"type":"agent-name","agentName":"[archived] Title","sessionId":"…"}
10
+ * ```
11
+ *
12
+ * The archive state follows AIR's native Claude provider (see
13
+ * {@link effectiveTitle}): the last agent name decides, else the custom title
14
+ * the SDK reports (`SDKSessionInfo.customTitle`). Archive writes the prefix
15
+ * and the current title without it; unarchive writes the title without the
16
+ * prefix. Both are cut to the CLI's 200-character title limit. The records
17
+ * are metadata: they move neither `updatedAt` nor the list order. The
18
+ * `claude --resume` picker shows the prefix.
19
+ */
20
+ /** The title prefix of an archived session. */
21
+ export const ARCHIVED_TITLE_PREFIX = "[archived] ";
22
+ /** What the CLI keeps of a stored title: `title.slice(0, 200).trim()`. */
23
+ const CLI_TITLE_LIMIT = 200;
24
+ /** A title once normalized starts with the prefix: leading space (what a
25
+ * trim removes), the marker, a run of white space, then a title character.
26
+ * The white space AIR collapses is ASCII white space, as in Java's `\s`. */
27
+ const ARCHIVED_PATTERN = /^\s*\[archived\][ \t\n\v\f\r]+(?=[^ \t\n\v\f\r])/;
28
+ /** Every leading archive prefix at once. */
29
+ const ARCHIVED_PREFIXES = /^(?:\s*\[archived\][ \t\n\v\f\r]+(?=[^ \t\n\v\f\r]))+/;
30
+ const WHITESPACE_RUN = /[ \t\n\v\f\r]+/g;
31
+ /** Whitespace collapsed to single spaces and trimmed, as AIR stores a title. */
32
+ export function normalizeStoredTitle(title) {
33
+ return title.replace(WHITESPACE_RUN, " ").trim();
34
+ }
35
+ /** Cut to what the CLI keeps of a title. */
36
+ function capTitle(title) {
37
+ return title.length <= CLI_TITLE_LIMIT ? title : title.slice(0, CLI_TITLE_LIMIT).trim();
38
+ }
39
+ /** Whether a session name (see {@link effectiveTitle}) marks its session
40
+ * archived. */
41
+ export function isArchivedTitle(title) {
42
+ return title !== undefined && ARCHIVED_PATTERN.test(title);
43
+ }
44
+ /** The title shown for a stored title: without the archive prefix,
45
+ * removed once, as AIR shows it. */
46
+ export function visibleTitle(title) {
47
+ return title.replace(ARCHIVED_PATTERN, "");
48
+ }
49
+ /** The title of a session that has none at all, as AIR names it. */
50
+ export function defaultSessionTitle(sessionId) {
51
+ return `Session ${sessionId.slice(0, 8)}`;
52
+ }
53
+ /** The title that `title` is stored as in the given archive state: the
54
+ * visible title, with the prefix when archived, normalized and cut to the
55
+ * CLI limit. */
56
+ export function storedTitle(title, archived, sessionId) {
57
+ // Every prefix: a title stored twice prefixed must not stay archived.
58
+ const visible = normalizeStoredTitle(title.replace(ARCHIVED_PREFIXES, "")) || defaultSessionTitle(sessionId);
59
+ return capTitle(archived ? ARCHIVED_TITLE_PREFIX + visible : visible);
60
+ }
61
+ /**
62
+ * The title of a session and its archive state, by the rule of AIR's native
63
+ * Claude provider: the session's name is its last agent name, else its custom
64
+ * title, and the session is archived exactly when that name starts with the
65
+ * prefix. The title is that agent name, else `summary`, the title the SDK
66
+ * gives the session; it is stored, so it may carry the prefix.
67
+ */
68
+ export function effectiveTitle(agentName, { customTitle, summary }) {
69
+ return { title: agentName ?? summary, archived: isArchivedTitle(agentName ?? customTitle) };
70
+ }
71
+ /** The `custom-title` and `agent-name` records of `title`, one per line. */
72
+ export function titleRecords(sessionId, title) {
73
+ return (JSON.stringify({ type: "custom-title", customTitle: title, sessionId }) +
74
+ "\n" +
75
+ JSON.stringify({ type: "agent-name", agentName: title, sessionId }) +
76
+ "\n");
77
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The session index of one ACP connection: what `ClaudeAcpAgent` does for a
3
+ * client that declared `sessionIndex`, and the archive of an AIR client
4
+ * without it. The agent keeps thin call sites; everything else
5
+ * lives here.
6
+ *
7
+ * Wire contract: docs/air-extensions.md, "Session index".
8
+ */
9
+ import { type AgentApp, type DeleteSessionRequest, type DeleteSessionResponse, type InitializeRequest, type ListSessionsRequest, type ListSessionsResponse } from "@agentclientprotocol/sdk";
10
+ import type { AcpClient, Logger, Session } from "../acp-agent.js";
11
+ import { type ListSubscribeRequest, type ListSubscribeResponse, type ListUnsubscribeRequest } from "./list-subscriptions.js";
12
+ import { SessionIndexService, type RenameSessionRequest, type SessionIdRequest } from "./service.js";
13
+ /** What the session index keeps on each `Session`. */
14
+ export type SessionIndexFields = {
15
+ /** When the last turn ended (the last `session_state_changed: idle`), epoch
16
+ * ms. Reported as `lastTurnEndedAt` in the session index. */
17
+ lastTurnEndedAt?: number;
18
+ /** The last turn failed with an error, not a cancel; a new turn clears
19
+ * it. Reported as the `error` state in the session index. */
20
+ lastTurnFailed?: boolean;
21
+ /** `total_cost_usd` of the last result, reported as the cost of the session
22
+ * in the session index. */
23
+ lastTotalCostUsd?: number;
24
+ /** The query resumed a stored conversation (load, resume, fork): the
25
+ * session has a transcript, unlike a new one before its first turn. */
26
+ resumedFromHistory?: boolean;
27
+ };
28
+ /** Records the end of a turn at `session_state_changed`. */
29
+ export declare function noteSessionState(session: Session, previous: Session["lastSessionState"], state: Session["lastSessionState"]): void;
30
+ /** What the connection needs from the agent. */
31
+ export type SessionIndexHost = {
32
+ /** Read when used: the agent may still be under construction. */
33
+ agent: {
34
+ readonly sessions: {
35
+ [key: string]: Session;
36
+ };
37
+ readonly client: AcpClient;
38
+ readonly logger: Logger;
39
+ };
40
+ /** Whether the client is an AIR client. */
41
+ isAirClient(): boolean;
42
+ /** Cancels the running turns of a session loaded here and sends its CLI
43
+ * the interrupt, without awaiting the reply. */
44
+ interruptSession(sessionId: string): Promise<void>;
45
+ /** Closes a session loaded here, as `session/close` does. */
46
+ teardownSession(sessionId: string): Promise<void>;
47
+ };
48
+ type EmptyResponse = Record<string, never>;
49
+ export declare class SessionIndexConnection {
50
+ private readonly host;
51
+ /**
52
+ * The session index of a client that declared `sessionIndex`. Undefined for
53
+ * every other client, which keeps the session list, delete and the watchers
54
+ * exactly as before.
55
+ */
56
+ service?: SessionIndexService;
57
+ /** The archive behind `session/delete` of an AIR client without
58
+ * `sessionIndex`: the same writes, without a list or a watcher. */
59
+ private archiver?;
60
+ /** When the query of a session failed here, by session id. */
61
+ private readonly failedQueries;
62
+ constructor(host: SessionIndexHost);
63
+ /** Sets the index up for the client of `initialize`. ACP v2 does not route
64
+ * the session index methods yet. */
65
+ negotiate(request: InitializeRequest, options: {
66
+ v2: boolean;
67
+ }): void;
68
+ /** A stored title as the client shows it: an AIR client gets the title
69
+ * of an archived session without the archive prefix, as in its list;
70
+ * every other client the title as stored. */
71
+ clientTitle(title: string): string;
72
+ /** The AIR capabilities the agent advertises for the index: the index
73
+ * itself, archive, rename and the list subscription, all only to a client
74
+ * that declared `sessionIndex`. */
75
+ capabilities(): string[];
76
+ dispose(): void;
77
+ /** The query of a session that ran here failed (not a cancel): its row
78
+ * shows `error` until its transcript changes. */
79
+ onQueryFailed(sessionId: string): void;
80
+ /** A turn of a session started here: an earlier failed query of it no
81
+ * longer counts. */
82
+ onTurnStarted(sessionId: string): void;
83
+ onTeardown(sessionId: string): void;
84
+ /** A session this connection runs changed its state, turn end or cost:
85
+ * the list subscriptions send it at once. */
86
+ onOwnSessionChanged(sessionId: string): void;
87
+ /** `_session/list/subscribe`. */
88
+ subscribeList(params: ListSubscribeRequest): Promise<ListSubscribeResponse>;
89
+ /** `_session/list/unsubscribe`. Idempotent. */
90
+ unsubscribeList(params: ListUnsubscribeRequest): EmptyResponse;
91
+ /** `session/list` of a `sessionIndex` client; undefined for another one. */
92
+ list(params: ListSessionsRequest): Promise<ListSessionsResponse> | undefined;
93
+ /** An AIR client archives with session/delete (see {@link deleteSession}):
94
+ * its archived sessions (an archived custom title) stay hidden from the
95
+ * old list, as when the delete removed them. */
96
+ hideArchived<T extends {
97
+ customTitle?: string;
98
+ }>(sessions: T[]): T[];
99
+ /** `_session/rename`: names a session; no generated title replaces it.
100
+ * An archived session stays archived. */
101
+ rename(request: RenameSessionRequest): Promise<EmptyResponse>;
102
+ /** `_session/archive`: hides a session from the default list and stops
103
+ * it. Idempotent; the session need not be loaded. A session loaded here
104
+ * is interrupted, retitled through its CLI while that still runs, then
105
+ * closed as `session/close` closes it. */
106
+ archive(params: SessionIdRequest): Promise<EmptyResponse>;
107
+ /** `_session/unarchive`. Idempotent; it does not load the session. */
108
+ unarchive(params: SessionIdRequest): Promise<EmptyResponse>;
109
+ /**
110
+ * Retitles a session (see {@link SessionIndexService.retitle}). A session
111
+ * this connection runs is titled through its CLI (`rename_session`). A
112
+ * loaded session's title state takes the change as a client title, so no
113
+ * generated title replaces it; `publish` is the title it then publishes.
114
+ */
115
+ private retitle;
116
+ /**
117
+ * `session/delete`:
118
+ * - A `sessionIndex` client deletes for real: every transcript, whoever
119
+ * else has the session open, as the CLI does.
120
+ * - Another AIR client uses delete to mark a session done, and may reopen
121
+ * it later: the adapter archives it instead (as `_session/archive` does,
122
+ * after the CLI closed), so the transcript survives.
123
+ * - Every other client: the SDK delete, as before.
124
+ */
125
+ deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse>;
126
+ private airArchiver;
127
+ private requireService;
128
+ /** What the session index reports for a session this connection runs. */
129
+ private ownSessionState;
130
+ /** A new session this connection runs that may have no transcript yet: it
131
+ * resumed no stored conversation and no turn of it has ended. A session
132
+ * whose query ended, or that has history, needs its transcript. */
133
+ private isUnwrittenSession;
134
+ /** Tells the client the archive state of a session that was loaded on
135
+ * this connection (`session_info_update` with
136
+ * `_meta.jetbrains.air.archived`, RFD #2161's `archived` field), also
137
+ * once the archive closed it. */
138
+ private reportArchived;
139
+ }
140
+ /** The agent methods behind the session index extension methods. */
141
+ type SessionIndexMethods = {
142
+ subscribeSessionList(params: ListSubscribeRequest): Promise<ListSubscribeResponse>;
143
+ unsubscribeSessionList(params: ListUnsubscribeRequest): Promise<EmptyResponse>;
144
+ renameSessionTitle(params: RenameSessionRequest): Promise<EmptyResponse>;
145
+ archiveSession(params: SessionIdRequest): Promise<EmptyResponse>;
146
+ unarchiveSession(params: SessionIdRequest): Promise<EmptyResponse>;
147
+ };
148
+ /** Routes `_session/list/subscribe`, `_session/list/unsubscribe`,
149
+ * `_session/rename`, `_session/archive` and `_session/unarchive`. */
150
+ export declare function onSessionIndexRequests(app: AgentApp, agent: () => SessionIndexMethods): AgentApp;
151
+ export {};
152
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/session-index/connection.ts"],"names":[],"mappings":"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"}