@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.
- package/dist/acp-agent.d.ts +39 -2
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +120 -35
- package/dist/air-extension.d.ts +23 -0
- package/dist/air-extension.d.ts.map +1 -1
- package/dist/air-extension.js +23 -0
- package/dist/diff.d.ts +2 -0
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +37 -1
- package/dist/mcp-command.d.ts.map +1 -1
- package/dist/mcp-command.js +5 -0
- package/dist/session-index/activity.d.ts +56 -0
- package/dist/session-index/activity.d.ts.map +1 -0
- package/dist/session-index/activity.js +96 -0
- package/dist/session-index/archive-title.d.ts +52 -0
- package/dist/session-index/archive-title.d.ts.map +1 -0
- package/dist/session-index/archive-title.js +77 -0
- package/dist/session-index/connection.d.ts +152 -0
- package/dist/session-index/connection.d.ts.map +1 -0
- package/dist/session-index/connection.js +310 -0
- package/dist/session-index/dir-listing.d.ts +30 -0
- package/dist/session-index/dir-listing.d.ts.map +1 -0
- package/dist/session-index/dir-listing.js +110 -0
- package/dist/session-index/explicit-title.d.ts +34 -0
- package/dist/session-index/explicit-title.d.ts.map +1 -0
- package/dist/session-index/explicit-title.js +65 -0
- package/dist/session-index/first-prompt.d.ts +37 -0
- package/dist/session-index/first-prompt.d.ts.map +1 -0
- package/dist/session-index/first-prompt.js +195 -0
- package/dist/session-index/list-subscriptions.d.ts +190 -0
- package/dist/session-index/list-subscriptions.d.ts.map +1 -0
- package/dist/session-index/list-subscriptions.js +1163 -0
- package/dist/session-index/live-registry.d.ts +72 -0
- package/dist/session-index/live-registry.d.ts.map +1 -0
- package/dist/session-index/live-registry.js +240 -0
- package/dist/session-index/project-dirs.d.ts +45 -0
- package/dist/session-index/project-dirs.d.ts.map +1 -0
- package/dist/session-index/project-dirs.js +122 -0
- package/dist/session-index/service.d.ts +158 -0
- package/dist/session-index/service.d.ts.map +1 -0
- package/dist/session-index/service.js +546 -0
- package/dist/session-index/session-index.d.ts +201 -0
- package/dist/session-index/session-index.d.ts.map +1 -0
- package/dist/session-index/session-index.js +740 -0
- package/dist/session-index/session-info.d.ts +16 -0
- package/dist/session-index/session-info.d.ts.map +1 -0
- package/dist/session-index/session-info.js +51 -0
- package/dist/session-index/transcript-scan.d.ts +137 -0
- package/dist/session-index/transcript-scan.d.ts.map +1 -0
- package/dist/session-index/transcript-scan.js +545 -0
- package/dist/session-index/worktrees.d.ts +25 -0
- package/dist/session-index/worktrees.d.ts.map +1 -0
- package/dist/session-index/worktrees.js +128 -0
- package/dist/session-titles.d.ts +21 -0
- package/dist/session-titles.d.ts.map +1 -1
- package/dist/session-titles.js +101 -5
- package/dist/utils.d.ts +31 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +60 -2
- package/dist/v2/agent.d.ts.map +1 -1
- package/dist/v2/agent.js +33 -4
- package/package.json +1 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The git worktrees of a working directory, read from the repository files.
|
|
3
|
+
*
|
|
4
|
+
* The SDK `listSessions({ dir })` includes the sessions of every worktree of
|
|
5
|
+
* the repository and runs `git worktree list` for that on each call. This
|
|
6
|
+
* module reads the same list without spawning git, the way the Codex TUI does:
|
|
7
|
+
* the main worktree is the parent of the common `.git` directory, and each
|
|
8
|
+
* linked worktree is named by `<common-dir>/worktrees/<name>/gitdir`.
|
|
9
|
+
* A worktree whose directory no longer exists is dropped. Nothing is cached:
|
|
10
|
+
* `git worktree move` rewrites a `gitdir` file without touching the
|
|
11
|
+
* `worktrees` directory, so a cache keyed on it would miss the move.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from "node:fs/promises";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { normalizePath } from "./project-dirs.js";
|
|
16
|
+
async function statOrUndefined(target) {
|
|
17
|
+
try {
|
|
18
|
+
return await fs.stat(target);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** The git dir of the repository that contains `cwd`, or undefined. */
|
|
25
|
+
async function findGitDir(cwd) {
|
|
26
|
+
let current = path.resolve(cwd);
|
|
27
|
+
for (;;) {
|
|
28
|
+
const candidate = path.join(current, ".git");
|
|
29
|
+
const stats = await statOrUndefined(candidate);
|
|
30
|
+
if (stats?.isDirectory())
|
|
31
|
+
return candidate;
|
|
32
|
+
if (stats?.isFile()) {
|
|
33
|
+
try {
|
|
34
|
+
const text = await fs.readFile(candidate, "utf8");
|
|
35
|
+
const match = /^gitdir:\s*(.+)$/m.exec(text);
|
|
36
|
+
if (match)
|
|
37
|
+
return path.resolve(current, match[1].trim());
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// An unreadable `.git` file is not a repository.
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const parent = path.dirname(current);
|
|
45
|
+
if (parent === current)
|
|
46
|
+
return undefined;
|
|
47
|
+
current = parent;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function commonDirOf(gitDir) {
|
|
51
|
+
try {
|
|
52
|
+
const text = await fs.readFile(path.join(gitDir, "commondir"), "utf8");
|
|
53
|
+
const value = text.trim();
|
|
54
|
+
if (value)
|
|
55
|
+
return path.resolve(gitDir, value);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// No `commondir`: the git dir is the common dir.
|
|
59
|
+
}
|
|
60
|
+
return gitDir;
|
|
61
|
+
}
|
|
62
|
+
async function linkedWorktrees(commonDir) {
|
|
63
|
+
const worktreesDir = path.join(commonDir, "worktrees");
|
|
64
|
+
const stats = await statOrUndefined(worktreesDir);
|
|
65
|
+
if (!stats?.isDirectory())
|
|
66
|
+
return [];
|
|
67
|
+
let names;
|
|
68
|
+
try {
|
|
69
|
+
names = await fs.readdir(worktreesDir);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
return (await Promise.all(names.map(async (name) => {
|
|
75
|
+
try {
|
|
76
|
+
const text = await fs.readFile(path.join(worktreesDir, name, "gitdir"), "utf8");
|
|
77
|
+
const gitFile = text.trim();
|
|
78
|
+
return gitFile ? path.dirname(path.resolve(worktreesDir, name, gitFile)) : undefined;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}))).filter((value) => value !== undefined);
|
|
84
|
+
}
|
|
85
|
+
/** The root of the worktree that contains `cwd`: the directory of its
|
|
86
|
+
* `.git` (a directory in the main worktree, a file in a linked one). */
|
|
87
|
+
async function worktreeRootOf(cwd) {
|
|
88
|
+
let current = path.resolve(cwd);
|
|
89
|
+
for (;;) {
|
|
90
|
+
if (await statOrUndefined(path.join(current, ".git")))
|
|
91
|
+
return current;
|
|
92
|
+
const parent = path.dirname(current);
|
|
93
|
+
if (parent === current)
|
|
94
|
+
return undefined;
|
|
95
|
+
current = parent;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The same subdirectory as `cwd` in every existing worktree of its
|
|
100
|
+
* repository (the main one and the linked ones), as the Codex TUI expands a
|
|
101
|
+
* cwd: for `/repo/packages/a`, `/wt1/packages/a` if it exists. For a cwd at a
|
|
102
|
+
* worktree root, the worktree roots. Empty outside a repository.
|
|
103
|
+
*/
|
|
104
|
+
export async function worktreeCounterparts(cwd) {
|
|
105
|
+
const root = await worktreeRootOf(cwd);
|
|
106
|
+
if (!root)
|
|
107
|
+
return [];
|
|
108
|
+
const relative = path.relative(root, path.resolve(cwd));
|
|
109
|
+
const candidates = (await repositoryWorktrees(cwd)).map((worktree) => relative ? path.join(worktree, relative) : worktree);
|
|
110
|
+
const existing = await Promise.all(candidates.map(async (candidate) => (await statOrUndefined(candidate))?.isDirectory() ? normalizePath(candidate) : undefined));
|
|
111
|
+
return [...new Set(existing.filter((value) => value !== undefined))];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Every existing worktree of the repository that contains `cwd`: the main
|
|
115
|
+
* worktree and the linked ones. Empty outside a repository.
|
|
116
|
+
*/
|
|
117
|
+
export async function repositoryWorktrees(cwd) {
|
|
118
|
+
const gitDir = await findGitDir(cwd);
|
|
119
|
+
if (!gitDir)
|
|
120
|
+
return [];
|
|
121
|
+
const commonDir = await commonDirOf(gitDir);
|
|
122
|
+
const candidates = [];
|
|
123
|
+
if (path.basename(commonDir) === ".git")
|
|
124
|
+
candidates.push(path.dirname(commonDir));
|
|
125
|
+
candidates.push(...(await linkedWorktrees(commonDir)));
|
|
126
|
+
const existing = await Promise.all(candidates.map(async (candidate) => (await statOrUndefined(candidate))?.isDirectory() ? normalizePath(candidate) : undefined));
|
|
127
|
+
return [...new Set(existing.filter((value) => value !== undefined))];
|
|
128
|
+
}
|
package/dist/session-titles.d.ts
CHANGED
|
@@ -36,6 +36,18 @@ export declare class SessionTitles {
|
|
|
36
36
|
* the same field as a user `/rename`, so re-titling could silently overwrite
|
|
37
37
|
* one. Released by {@link reset}, and when generation yields nothing. */
|
|
38
38
|
private settled;
|
|
39
|
+
/** The title a client set, which no generated title replaces. */
|
|
40
|
+
private readonly explicit;
|
|
41
|
+
/** The title last stored through {@link setExplicitTitle}, as stored
|
|
42
|
+
* (with the archive prefix of an archived session): the title the CLI
|
|
43
|
+
* holds. */
|
|
44
|
+
private persistedTitle?;
|
|
45
|
+
/** Client title changes in flight, and the title state before the first. */
|
|
46
|
+
private explicitChanges;
|
|
47
|
+
private beforeExplicit?;
|
|
48
|
+
get storedTitle(): string | undefined;
|
|
49
|
+
/** The title last published, as the client shows it. */
|
|
50
|
+
get shownTitle(): string | undefined;
|
|
39
51
|
constructor(agent: ClaudeAcpAgent, sessionId: string);
|
|
40
52
|
/** Collect a prompt's own text for the title, skipping openers not worth
|
|
41
53
|
* titling after. No-op once the title is settled. */
|
|
@@ -58,6 +70,15 @@ export declare class SessionTitles {
|
|
|
58
70
|
* not possible do we fall back to `summary`, which for an SDK-driven session
|
|
59
71
|
* is just the raw first prompt. */
|
|
60
72
|
onTurnEnd(session: Session): Promise<void>;
|
|
73
|
+
/** Applies a title that the client chose (a rename, an archive or an
|
|
74
|
+
* unarchive). Settles the title for good, waits for a generation in flight
|
|
75
|
+
* so that its persisted title cannot land after this one, persists the
|
|
76
|
+
* title with `persist`, which returns what it stored, if anything, and
|
|
77
|
+
* publishes `title` unless it is undefined. A change that stored and
|
|
78
|
+
* publishes nothing leaves the title state as it was. */
|
|
79
|
+
setExplicitTitle(publish: string | ((stored: string | undefined) => string) | undefined, persist: () => Promise<string | undefined | void>): Promise<void>;
|
|
80
|
+
/** Records a title the CLI took, as soon as it took it. */
|
|
81
|
+
rememberStoredTitle(title: string): void;
|
|
61
82
|
/** Read the SDK's stored info for this session. A missing session file or read
|
|
62
83
|
* error is non-fatal: the title is best-effort and another turn will retry. */
|
|
63
84
|
private readSessionInfo;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-titles.d.ts","sourceRoot":"","sources":["../src/session-titles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE5E,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"session-titles.d.ts","sourceRoot":"","sources":["../src/session-titles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE5E,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AA+B9D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqBlD;AAED;gEACgE;AAChE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGrF;AAOD;;iDAEiD;AACjD,qBAAa,aAAa;IAqCtB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IArC5B;sEACkE;IAClE,OAAO,CAAC,SAAS,CAAC,CAAS;IAE3B;yEACqE;IACrE,OAAO,CAAC,OAAO,CAAC,CAAS;IAEzB;;;8EAG0E;IAC1E,OAAO,CAAC,OAAO,CAAS;IAExB,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAEhD;;iBAEa;IACb,OAAO,CAAC,cAAc,CAAC,CAAS;IAEhC,4EAA4E;IAC5E,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,cAAc,CAAC,CAAoD;IAE3E,IAAI,WAAW,IAAI,MAAM,GAAG,SAAS,CAEpC;IAED,wDAAwD;IACxD,IAAI,UAAU,IAAI,MAAM,GAAG,SAAS,CAEnC;gBAGkB,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,MAAM;IAGpC;0DACsD;IACtD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,GAAG,IAAI;IAW/C;kFAC8E;IAC9E,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAO5C;iEAC6D;IAC7D,KAAK,IAAI,IAAI;IAQb;;;;;;;;;;wCAUoC;IAC9B,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAoDhD;;;;;8DAK0D;IACpD,gBAAgB,CACpB,OAAO,EAAE,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,CAAC,GAAG,SAAS,EACtE,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC,GAChD,OAAO,CAAC,IAAI,CAAC;IAkChB,2DAA2D;IAC3D,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIxC;oFACgF;YAClE,eAAe;IAS7B,uEAAuE;YACzD,OAAO;IAgBrB;iFAC6E;IAC7E,OAAO,CAAC,UAAU;IAUlB;;;;;kBAKc;YACA,oBAAoB;CAoCnC"}
|
package/dist/session-titles.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* {@link SessionTitles.reset} on `conversation_reset`.
|
|
15
15
|
*/
|
|
16
16
|
import { getSessionInfo } from "@anthropic-ai/claude-agent-sdk";
|
|
17
|
+
import { ExplicitTitle } from "./session-index/explicit-title.js";
|
|
17
18
|
const MAX_TITLE_LENGTH = 256;
|
|
18
19
|
/** How much session text the title is generated from. The CLI caps its own
|
|
19
20
|
* title input at the same 1000 trailing characters. */
|
|
@@ -21,7 +22,21 @@ const MAX_TITLE_CONTEXT_LENGTH = 1000;
|
|
|
21
22
|
/** Below this the generator returns null without calling the model, so there is
|
|
22
23
|
* nothing to gain by asking yet. */
|
|
23
24
|
const MIN_TITLE_CONTEXT_LENGTH = 10;
|
|
25
|
+
/** How much of a long text {@link sanitizeTitle} reads first. Collapsed, it
|
|
26
|
+
* is longer than a title unless it is mostly whitespace. */
|
|
27
|
+
const TITLE_SCAN_LENGTH = 4096;
|
|
24
28
|
export function sanitizeTitle(text) {
|
|
29
|
+
if (text.length > TITLE_SCAN_LENGTH) {
|
|
30
|
+
// A stored summary can be a whole first prompt of megabytes. The title
|
|
31
|
+
// depends only on the start of the text: collapsing the whitespace of a
|
|
32
|
+
// prefix gives a prefix of the collapsed text. Two more characters than a
|
|
33
|
+
// title hold at least one that is not a space (spaces are collapsed), so
|
|
34
|
+
// the full text is longer than a title and gets the same cut.
|
|
35
|
+
const head = text.slice(0, TITLE_SCAN_LENGTH).replace(/\s+/g, " ").trimStart();
|
|
36
|
+
if (head.length >= MAX_TITLE_LENGTH + 2) {
|
|
37
|
+
return head.slice(0, MAX_TITLE_LENGTH - 1) + "…";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
25
40
|
// Replace newlines and collapse whitespace
|
|
26
41
|
const sanitized = text
|
|
27
42
|
.replace(/[\r\n]+/g, " ")
|
|
@@ -59,6 +74,22 @@ export class SessionTitles {
|
|
|
59
74
|
* the same field as a user `/rename`, so re-titling could silently overwrite
|
|
60
75
|
* one. Released by {@link reset}, and when generation yields nothing. */
|
|
61
76
|
settled = false;
|
|
77
|
+
/** The title a client set, which no generated title replaces. */
|
|
78
|
+
explicit = new ExplicitTitle();
|
|
79
|
+
/** The title last stored through {@link setExplicitTitle}, as stored
|
|
80
|
+
* (with the archive prefix of an archived session): the title the CLI
|
|
81
|
+
* holds. */
|
|
82
|
+
persistedTitle;
|
|
83
|
+
/** Client title changes in flight, and the title state before the first. */
|
|
84
|
+
explicitChanges = 0;
|
|
85
|
+
beforeExplicit;
|
|
86
|
+
get storedTitle() {
|
|
87
|
+
return this.persistedTitle;
|
|
88
|
+
}
|
|
89
|
+
/** The title last published, as the client shows it. */
|
|
90
|
+
get shownTitle() {
|
|
91
|
+
return this.lastTitle;
|
|
92
|
+
}
|
|
62
93
|
constructor(agent, sessionId) {
|
|
63
94
|
this.agent = agent;
|
|
64
95
|
this.sessionId = sessionId;
|
|
@@ -86,8 +117,10 @@ export class SessionTitles {
|
|
|
86
117
|
* `conversation_reset`, which mounts a fresh transcript. */
|
|
87
118
|
reset() {
|
|
88
119
|
this.settled = false;
|
|
120
|
+
this.explicit.reset();
|
|
89
121
|
this.context = undefined;
|
|
90
122
|
this.lastTitle = undefined;
|
|
123
|
+
this.persistedTitle = undefined;
|
|
91
124
|
}
|
|
92
125
|
/** Turn-end title handling. `idle` is the SDK's turn-over signal, so it is
|
|
93
126
|
* when a title may have landed or become generatable.
|
|
@@ -101,7 +134,26 @@ export class SessionTitles {
|
|
|
101
134
|
* not possible do we fall back to `summary`, which for an SDK-driven session
|
|
102
135
|
* is just the raw first prompt. */
|
|
103
136
|
async onTurnEnd(session) {
|
|
137
|
+
if (this.explicit.settledByClient) {
|
|
138
|
+
// No title is generated over the one the client set, but a custom title
|
|
139
|
+
// stored since by someone else (a `/rename`) is adopted, as before.
|
|
140
|
+
const renames = this.explicit.renames;
|
|
141
|
+
const info = await this.readSessionInfo(session);
|
|
142
|
+
// No client change ran during the read, so the stored title is current;
|
|
143
|
+
// an unchanged one is not published again.
|
|
144
|
+
const stored = info?.customTitle;
|
|
145
|
+
if (stored && this.explicit.settledByClient && this.explicit.renames === renames) {
|
|
146
|
+
await this.publish(stored, info.lastModified);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// While a client title change is in flight, the session info read below
|
|
151
|
+
// could still hold the previous title.
|
|
152
|
+
if (this.explicit.active)
|
|
153
|
+
return;
|
|
104
154
|
const info = await this.readSessionInfo(session);
|
|
155
|
+
if (this.explicit.active)
|
|
156
|
+
return;
|
|
105
157
|
if (info?.customTitle) {
|
|
106
158
|
this.settled = true;
|
|
107
159
|
this.context = undefined;
|
|
@@ -113,9 +165,9 @@ export class SessionTitles {
|
|
|
113
165
|
: undefined;
|
|
114
166
|
if (this.canRequest(session)) {
|
|
115
167
|
this.settled = true;
|
|
116
|
-
|
|
168
|
+
this.explicit.track(this.requestGenerateTitle(session, fallback).catch((error) => {
|
|
117
169
|
this.agent.logger.error(`Session ${this.sessionId}: session title update failed: ${error}`);
|
|
118
|
-
});
|
|
170
|
+
}));
|
|
119
171
|
return;
|
|
120
172
|
}
|
|
121
173
|
// Only while this session has no title of its own: `summary` degrades to the
|
|
@@ -125,6 +177,50 @@ export class SessionTitles {
|
|
|
125
177
|
await this.publish(fallback.title, fallback.lastModified);
|
|
126
178
|
}
|
|
127
179
|
}
|
|
180
|
+
/** Applies a title that the client chose (a rename, an archive or an
|
|
181
|
+
* unarchive). Settles the title for good, waits for a generation in flight
|
|
182
|
+
* so that its persisted title cannot land after this one, persists the
|
|
183
|
+
* title with `persist`, which returns what it stored, if anything, and
|
|
184
|
+
* publishes `title` unless it is undefined. A change that stored and
|
|
185
|
+
* publishes nothing leaves the title state as it was. */
|
|
186
|
+
async setExplicitTitle(publish, persist) {
|
|
187
|
+
// The state before the first of overlapping changes, which a change that
|
|
188
|
+
// fails or names nothing puts back.
|
|
189
|
+
if (this.explicitChanges++ === 0) {
|
|
190
|
+
this.beforeExplicit = { settled: this.settled, context: this.context };
|
|
191
|
+
}
|
|
192
|
+
const previous = this.beforeExplicit;
|
|
193
|
+
this.settled = true;
|
|
194
|
+
this.context = undefined;
|
|
195
|
+
let stored;
|
|
196
|
+
try {
|
|
197
|
+
await this.explicit.apply(async () => {
|
|
198
|
+
stored = (await persist()) ?? undefined;
|
|
199
|
+
if (stored !== undefined)
|
|
200
|
+
this.persistedTitle = stored;
|
|
201
|
+
return stored !== undefined || publish !== undefined;
|
|
202
|
+
}, () => {
|
|
203
|
+
this.settled = previous.settled;
|
|
204
|
+
this.context ??= previous.context;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
if (--this.explicitChanges === 0)
|
|
209
|
+
this.beforeExplicit = undefined;
|
|
210
|
+
}
|
|
211
|
+
const title = typeof publish === "function" ? publish(stored) : publish;
|
|
212
|
+
if (title === undefined)
|
|
213
|
+
return;
|
|
214
|
+
this.lastTitle = title;
|
|
215
|
+
await this.agent.client.sessionUpdate({
|
|
216
|
+
sessionId: this.sessionId,
|
|
217
|
+
update: { sessionUpdate: "session_info_update", title },
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/** Records a title the CLI took, as soon as it took it. */
|
|
221
|
+
rememberStoredTitle(title) {
|
|
222
|
+
this.persistedTitle = title;
|
|
223
|
+
}
|
|
128
224
|
/** Read the SDK's stored info for this session. A missing session file or read
|
|
129
225
|
* error is non-fatal: the title is best-effort and another turn will retry. */
|
|
130
226
|
async readSessionInfo(session) {
|
|
@@ -138,7 +234,7 @@ export class SessionTitles {
|
|
|
138
234
|
}
|
|
139
235
|
/** Notify the client of a title, unless it is the one we last sent. */
|
|
140
236
|
async publish(rawTitle, lastModified) {
|
|
141
|
-
const title = sanitizeTitle(rawTitle);
|
|
237
|
+
const title = sanitizeTitle(this.agent.clientTitle?.(rawTitle) ?? rawTitle);
|
|
142
238
|
if (title === this.lastTitle) {
|
|
143
239
|
return;
|
|
144
240
|
}
|
|
@@ -182,8 +278,8 @@ export class SessionTitles {
|
|
|
182
278
|
this.agent.logger.error(`Session ${this.sessionId}: failed to generate a session title: ${error}`);
|
|
183
279
|
}
|
|
184
280
|
// A session torn down or replaced while the title was in flight must not
|
|
185
|
-
// adopt it.
|
|
186
|
-
if (this.agent.sessions[this.sessionId] !== session) {
|
|
281
|
+
// adopt it, and a title the client set meanwhile wins.
|
|
282
|
+
if (this.agent.sessions[this.sessionId] !== session || this.explicit.active) {
|
|
187
283
|
return;
|
|
188
284
|
}
|
|
189
285
|
if (!title) {
|
package/dist/utils.d.ts
CHANGED
|
@@ -12,6 +12,37 @@ export declare class Pushable<T> implements AsyncIterable<T> {
|
|
|
12
12
|
export declare function nodeToWebWritable(nodeStream: Writable): WritableStream<Uint8Array>;
|
|
13
13
|
export declare function nodeToWebReadable(nodeStream: Readable): ReadableStream<Uint8Array>;
|
|
14
14
|
export declare function unreachable(value: never, logger?: Logger): void;
|
|
15
|
+
/**
|
|
16
|
+
* Yields to the event loop when the current macrotask has run for too long.
|
|
17
|
+
*
|
|
18
|
+
* An await of an already-resolved promise continues in a microtask, so a loop
|
|
19
|
+
* that drains a backlog of buffered messages never lets timers or I/O run:
|
|
20
|
+
* incoming requests, `session/cancel` among them, wait until the backlog is
|
|
21
|
+
* gone. A loop calls {@link maybeYield} once per item. Within the budget it
|
|
22
|
+
* returns undefined and the loop goes on at once. Past the budget it returns a
|
|
23
|
+
* promise that resolves in the check phase of the event loop, after pending
|
|
24
|
+
* I/O has run.
|
|
25
|
+
*
|
|
26
|
+
* The budget is measured from the first call since the event loop last
|
|
27
|
+
* reached its check phase, so a loop that already waits on real I/O never
|
|
28
|
+
* yields, and all loops that share one instance share one budget per
|
|
29
|
+
* event-loop iteration.
|
|
30
|
+
*
|
|
31
|
+
* A test that fakes `setImmediate` and drives such a loop for longer than the
|
|
32
|
+
* budget must advance the fake timers, or the loop waits for them.
|
|
33
|
+
*/
|
|
34
|
+
export declare class EventLoopYielder {
|
|
35
|
+
private readonly budgetMs;
|
|
36
|
+
private readonly now;
|
|
37
|
+
/** Whether a check-phase marker is pending. While it is, the event loop has
|
|
38
|
+
* not reached its check phase since {@link sliceStart}. */
|
|
39
|
+
private markerPending;
|
|
40
|
+
private sliceStart;
|
|
41
|
+
constructor(budgetMs: number, now?: () => number);
|
|
42
|
+
maybeYield(): Promise<void> | undefined;
|
|
43
|
+
}
|
|
44
|
+
/** The yielder shared by the loops that forward messages to the client. */
|
|
45
|
+
export declare const messageLoopYielder: EventLoopYielder;
|
|
15
46
|
export declare function sleep(time: number): Promise<void>;
|
|
16
47
|
/** The outcome of {@link raceTimeoutAndAbort}. */
|
|
17
48
|
export type RaceOutcome<T> = {
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAGxC,qBAAa,QAAQ,CAAC,CAAC,CAAE,YAAW,aAAa,CAAC,CAAC,CAAC;IAClD,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,IAAI,CAAS;IAErB,IAAI,CAAC,IAAI,EAAE,CAAC;IASZ,GAAG;IAQH,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC;CAgB3C;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAGxC,qBAAa,QAAQ,CAAC,CAAC,CAAE,YAAW,aAAa,CAAC,CAAC,CAAC;IAClD,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,IAAI,CAAS;IAErB,IAAI,CAAC,IAAI,EAAE,CAAC;IASZ,GAAG;IAQH,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC;CAgB3C;AAKD,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,QAAQ,GAAG,cAAc,CAAC,UAAU,CAAC,CAoBlF;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,QAAQ,GAAG,cAAc,CAAC,UAAU,CAAC,CAUlF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,GAAE,MAAgB,QAQjE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,gBAAgB;IAOzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB;gEAC4D;IAC5D,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,UAAU,CAAK;gBAGJ,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,MAAgC;IAG9D,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS;CAaxC;AAMD,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,kBAA+C,CAAC;AAE/E,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEjD;AAED,kDAAkD;AAClD,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEpG;kFACkF;AAClF,wBAAsB,mBAAmB,CAAC,CAAC,EACzC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAoBzB"}
|
package/dist/utils.js
CHANGED
|
@@ -38,12 +38,20 @@ export class Pushable {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
// Helper to convert Node.js streams to Web Streams
|
|
41
|
+
// Helper to convert Node.js streams to Web Streams. A byte stream gets each
|
|
42
|
+
// chunk without a copy, so the caller must not change a chunk until its write
|
|
43
|
+
// resolves.
|
|
42
44
|
export function nodeToWebWritable(nodeStream) {
|
|
43
45
|
return new WritableStream({
|
|
44
46
|
write(chunk) {
|
|
45
47
|
return new Promise((resolve, reject) => {
|
|
46
|
-
|
|
48
|
+
// A Uint8Array is written as it is: Node wraps it in a Buffer view
|
|
49
|
+
// without a copy. The ACP encoder hands each message a fresh array,
|
|
50
|
+
// so nothing changes the bytes while the write is pending. An
|
|
51
|
+
// object-mode stream would get the array itself, so it keeps
|
|
52
|
+
// getting a Buffer copy, as before.
|
|
53
|
+
const data = nodeStream.writableObjectMode ? Buffer.from(chunk) : chunk;
|
|
54
|
+
nodeStream.write(data, (err) => {
|
|
47
55
|
if (err) {
|
|
48
56
|
reject(err);
|
|
49
57
|
}
|
|
@@ -76,6 +84,56 @@ export function unreachable(value, logger = console) {
|
|
|
76
84
|
}
|
|
77
85
|
logger.error(`Unexpected case: ${valueAsString}`);
|
|
78
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Yields to the event loop when the current macrotask has run for too long.
|
|
89
|
+
*
|
|
90
|
+
* An await of an already-resolved promise continues in a microtask, so a loop
|
|
91
|
+
* that drains a backlog of buffered messages never lets timers or I/O run:
|
|
92
|
+
* incoming requests, `session/cancel` among them, wait until the backlog is
|
|
93
|
+
* gone. A loop calls {@link maybeYield} once per item. Within the budget it
|
|
94
|
+
* returns undefined and the loop goes on at once. Past the budget it returns a
|
|
95
|
+
* promise that resolves in the check phase of the event loop, after pending
|
|
96
|
+
* I/O has run.
|
|
97
|
+
*
|
|
98
|
+
* The budget is measured from the first call since the event loop last
|
|
99
|
+
* reached its check phase, so a loop that already waits on real I/O never
|
|
100
|
+
* yields, and all loops that share one instance share one budget per
|
|
101
|
+
* event-loop iteration.
|
|
102
|
+
*
|
|
103
|
+
* A test that fakes `setImmediate` and drives such a loop for longer than the
|
|
104
|
+
* budget must advance the fake timers, or the loop waits for them.
|
|
105
|
+
*/
|
|
106
|
+
export class EventLoopYielder {
|
|
107
|
+
budgetMs;
|
|
108
|
+
now;
|
|
109
|
+
/** Whether a check-phase marker is pending. While it is, the event loop has
|
|
110
|
+
* not reached its check phase since {@link sliceStart}. */
|
|
111
|
+
markerPending = false;
|
|
112
|
+
sliceStart = 0;
|
|
113
|
+
constructor(budgetMs, now = () => performance.now()) {
|
|
114
|
+
this.budgetMs = budgetMs;
|
|
115
|
+
this.now = now;
|
|
116
|
+
}
|
|
117
|
+
maybeYield() {
|
|
118
|
+
const time = this.now();
|
|
119
|
+
if (!this.markerPending) {
|
|
120
|
+
this.markerPending = true;
|
|
121
|
+
this.sliceStart = time;
|
|
122
|
+
setImmediate(() => {
|
|
123
|
+
this.markerPending = false;
|
|
124
|
+
});
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (time - this.sliceStart < this.budgetMs)
|
|
128
|
+
return undefined;
|
|
129
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** The longest stretch, in milliseconds, that a message loop keeps the event
|
|
133
|
+
* loop before it yields. */
|
|
134
|
+
const MESSAGE_LOOP_BUDGET_MS = 8;
|
|
135
|
+
/** The yielder shared by the loops that forward messages to the client. */
|
|
136
|
+
export const messageLoopYielder = new EventLoopYielder(MESSAGE_LOOP_BUDGET_MS);
|
|
79
137
|
export function sleep(time) {
|
|
80
138
|
return new Promise((resolve) => setTimeout(resolve, time));
|
|
81
139
|
}
|
package/dist/v2/agent.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/v2/agent.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,0CAA0C,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAkB,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/v2/agent.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,0CAA0C,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAkB,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAmB9E;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GACvC,EAAE,CAAC,QAAQ,CAoEb"}
|
package/dist/v2/agent.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ClaudeAcpAgent } from "../acp-agent.js";
|
|
|
3
3
|
import { v1InitializeRequest, v2InitializeResponse } from "./initialize.js";
|
|
4
4
|
import { v1PermissionResponse, v2PermissionRequest } from "./permission.js";
|
|
5
5
|
import { v2Prompt } from "./prompt.js";
|
|
6
|
-
import { v1ForkSessionRequests, v1NewSessionRequest, v1RestoreSessionRequest, v1SetSessionConfigOptionRequest, v2ConfigOptions, v2NewSessionResponse, v2ResumeSessionResponse, } from "./session.js";
|
|
6
|
+
import { v1ForkSessionRequests, v1NewSessionRequest, v1RestoreSessionRequest, v1SetSessionConfigOptionRequest, v2AvailableCommands, v2ConfigOptions, v2NewSessionResponse, v2ResumeSessionResponse, } from "./session.js";
|
|
7
7
|
import { v2SessionUpdate } from "./session-update.js";
|
|
8
8
|
import { V2Terminals } from "./terminal.js";
|
|
9
9
|
/**
|
|
@@ -31,13 +31,23 @@ export function v2AgentApp(logger, onAgent) {
|
|
|
31
31
|
.onRequest(v2.methods.agent.providers.list, ({ params }) => agent.unstable_listProviders(params))
|
|
32
32
|
.onRequest(v2.methods.agent.providers.set, ({ params }) => agent.unstable_setProvider(params))
|
|
33
33
|
.onRequest(v2.methods.agent.providers.disable, ({ params }) => agent.unstable_disableProvider(params))
|
|
34
|
-
.onRequest(v2.methods.agent.session.new, async ({ params }) =>
|
|
34
|
+
.onRequest(v2.methods.agent.session.new, async ({ params }) => {
|
|
35
|
+
const response = await agent.newSession(v1NewSessionRequest(params));
|
|
36
|
+
return {
|
|
37
|
+
...v2NewSessionResponse(response),
|
|
38
|
+
...(await setupCommands(agent, response.sessionId, logger)),
|
|
39
|
+
};
|
|
40
|
+
})
|
|
35
41
|
.onRequest(v2.methods.agent.session.list, ({ params }) => agent.listSessions(params))
|
|
36
42
|
.onRequest(v2.methods.agent.session.resume, async ({ params }) => {
|
|
37
43
|
const restore = v1RestoreSessionRequest(params);
|
|
38
|
-
|
|
44
|
+
const response = restore.method === "resume"
|
|
39
45
|
? await agent.resumeSession(restore.request)
|
|
40
|
-
: await client.replaying(params.sessionId, () => agent.loadSession(restore.request))
|
|
46
|
+
: await client.replaying(params.sessionId, () => agent.loadSession(restore.request));
|
|
47
|
+
return {
|
|
48
|
+
...v2ResumeSessionResponse(response),
|
|
49
|
+
...(await setupCommands(agent, params.sessionId, logger)),
|
|
50
|
+
};
|
|
41
51
|
})
|
|
42
52
|
.onRequest(v2.methods.agent.session.fork, async ({ params }) => {
|
|
43
53
|
const requests = v1ForkSessionRequests(params);
|
|
@@ -45,6 +55,7 @@ export function v2AgentApp(logger, onAgent) {
|
|
|
45
55
|
return {
|
|
46
56
|
sessionId,
|
|
47
57
|
...v2ResumeSessionResponse(await agent.resumeSession(requests.resume(sessionId))),
|
|
58
|
+
...(await setupCommands(agent, sessionId, logger)),
|
|
48
59
|
};
|
|
49
60
|
})
|
|
50
61
|
.onRequest(v2.methods.agent.session.close, async ({ params }) => {
|
|
@@ -62,6 +73,24 @@ export function v2AgentApp(logger, onAgent) {
|
|
|
62
73
|
}))
|
|
63
74
|
.onNotification(v2.methods.agent.session.cancel, ({ params }) => agent.cancel(params));
|
|
64
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The `availableCommands` of a setup response, so a client can show the
|
|
78
|
+
* session's commands without waiting for an update. The agent sends a v1
|
|
79
|
+
* client the same list as an `available_commands_update` after the response,
|
|
80
|
+
* and a v2 client none (`ClaudeAcpAgent.afterSetupResponse`). Later changes
|
|
81
|
+
* still arrive as updates. An empty list is left out, as the draft asks. The
|
|
82
|
+
* commands are optional, so a failure to read them does not fail the setup.
|
|
83
|
+
*/
|
|
84
|
+
async function setupCommands(agent, sessionId, logger) {
|
|
85
|
+
try {
|
|
86
|
+
const commands = await agent.availableCommands(sessionId);
|
|
87
|
+
return commands.length > 0 ? { availableCommands: v2AvailableCommands(commands) } : {};
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
(logger ?? console).error(`Session ${sessionId}: failed to read the session's commands`, error);
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
65
94
|
/**
|
|
66
95
|
* The {@link AcpClient} of an ACP v2 connection: it sends the v1 messages of
|
|
67
96
|
* the agent as v2 messages.
|
package/package.json
CHANGED