@lucascouts/claude-agent-acp-plus 0.24.0 → 0.25.1
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 +23 -0
- package/dist/acp-agent.d.ts +21 -2
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +129 -32
- package/dist/classifier-escalation.d.ts +158 -0
- package/dist/classifier-escalation.d.ts.map +1 -0
- package/dist/classifier-escalation.js +562 -0
- package/dist/permissions/options/shared.d.ts +8 -0
- package/dist/permissions/options/shared.d.ts.map +1 -1
- package/dist/permissions/options/shared.js +18 -0
- package/dist/permissions/presentation.d.ts +1 -0
- package/dist/permissions/presentation.d.ts.map +1 -1
- package/dist/permissions/presentation.js +1 -1
- package/dist/permissions/response.d.ts +6 -0
- package/dist/permissions/response.d.ts.map +1 -1
- package/dist/permissions/response.js +19 -0
- package/dist/task-feed.d.ts +41 -3
- package/dist/task-feed.d.ts.map +1 -1
- package/dist/task-feed.js +148 -18
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { applyClaudePermissionSelection, parseClaudePermissionSelection } from "./effects.js";
|
|
2
|
+
import { PERMISSION_OPTION_ID } from "./options/shared.js";
|
|
2
3
|
/** Decode, validate, and interpret an ACP response exactly once. */
|
|
3
4
|
export function decodeClaudePermissionResponse(response, toolName, input, toolUseID, offeredOptions, durableChangeSet) {
|
|
4
5
|
const selection = parseClaudePermissionSelection(response, toolName);
|
|
@@ -17,3 +18,21 @@ export function decodeClaudePermissionResponse(response, toolName, input, toolUs
|
|
|
17
18
|
...(selection.contextResetMode ? { contextResetMode: selection.contextResetMode } : {}),
|
|
18
19
|
};
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Map the answer to a classifier-denial request. Only the three ids that
|
|
23
|
+
* request offers are accepted; anything else is an error, never an approval.
|
|
24
|
+
*/
|
|
25
|
+
export function decodeClassifierEscalationResponse(response) {
|
|
26
|
+
if (response.outcome.outcome === "cancelled")
|
|
27
|
+
return "cancelled";
|
|
28
|
+
switch (response.outcome.optionId) {
|
|
29
|
+
case PERMISSION_OPTION_ID.classifierAllowOnce:
|
|
30
|
+
return "once";
|
|
31
|
+
case PERMISSION_OPTION_ID.classifierAllowSession:
|
|
32
|
+
return "session";
|
|
33
|
+
case PERMISSION_OPTION_ID.reject:
|
|
34
|
+
return "reject";
|
|
35
|
+
default:
|
|
36
|
+
throw new Error(`Classifier escalation option was not offered: ${response.outcome.optionId}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/task-feed.d.ts
CHANGED
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
* progress, at once on an ending, and once more after a burst so the latest
|
|
20
20
|
* values are not lost.
|
|
21
21
|
*
|
|
22
|
+
* A foreground shell task (`local_bash`, not backgrounded) is held back for
|
|
23
|
+
* `FOREGROUND_SHELL_HOLD_MS`: Claude Code registers every shell command as a
|
|
24
|
+
* task, so only one still running after the hold, or moved to the background,
|
|
25
|
+
* is published. One that ends while held is dropped without a trace. The feed
|
|
26
|
+
* stays clock-driven: the publisher's timer calls `promoteDue()`.
|
|
27
|
+
*
|
|
22
28
|
* Neither class ever copies a command line (`SDKTaskStartedMessage.prompt`)
|
|
23
29
|
* into an entry or a log line.
|
|
24
30
|
*/
|
|
@@ -50,7 +56,9 @@ export type TasksSnapshot = {
|
|
|
50
56
|
tasks: TaskEntry[];
|
|
51
57
|
};
|
|
52
58
|
/** What a message did to the table; the publisher decides timing from it. */
|
|
53
|
-
export type TaskChange = "none" | "progress" | "terminal";
|
|
59
|
+
export type TaskChange = "none" | "progress" | "terminal" | "held";
|
|
60
|
+
/** How long a foreground shell task is held back before it is published. */
|
|
61
|
+
export declare const FOREGROUND_SHELL_HOLD_MS = 5000;
|
|
54
62
|
/** How long a running background task may be missing from the level before
|
|
55
63
|
* it is ended as `interrupted`. The level and the bookends race (the level
|
|
56
64
|
* may arrive first), so a real ending gets this long to land. */
|
|
@@ -85,8 +93,21 @@ export declare class TaskFeed {
|
|
|
85
93
|
private readonly snapshotLives;
|
|
86
94
|
/** task id → time after which a missing bookend means `interrupted`. */
|
|
87
95
|
private readonly deadlines;
|
|
96
|
+
/** Foreground shell tasks not published yet: see `promoteDue`. */
|
|
97
|
+
private readonly held;
|
|
98
|
+
/** Shell task ids labelled by their tool call: a patch never relabels them. */
|
|
99
|
+
private readonly toolDescribed;
|
|
88
100
|
constructor(clock: () => number, log?: TaskFeedLog, sessionId?: string);
|
|
89
|
-
|
|
101
|
+
/**
|
|
102
|
+
* `toolDescription` is the spawning tool call's own `description` input. A
|
|
103
|
+
* shell task carries it when it is non-empty; otherwise the SDK's, which
|
|
104
|
+
* for a shell is the command text. Other task types keep the SDK's.
|
|
105
|
+
*/
|
|
106
|
+
onStarted(msg: SDKTaskStartedMessage, toolDescription?: string): TaskChange;
|
|
107
|
+
/** Publishes every held shell task whose hold has run out. */
|
|
108
|
+
promoteDue(): TaskChange;
|
|
109
|
+
/** When the earliest held shell task is due for promotion; null if none is held. */
|
|
110
|
+
nextHoldDue(): number | null;
|
|
90
111
|
onProgress(msg: SDKTaskProgressMessage): TaskChange;
|
|
91
112
|
onUpdated(msg: SDKTaskUpdatedMessage): TaskChange;
|
|
92
113
|
onNotification(msg: SDKTaskNotificationMessage): TaskChange;
|
|
@@ -102,7 +123,8 @@ export declare class TaskFeed {
|
|
|
102
123
|
nextReconcileAt(): number | null;
|
|
103
124
|
/** Ends as `interrupted` every task whose deadline passed with no bookend. */
|
|
104
125
|
reconcile(now: number): TaskChange;
|
|
105
|
-
/** The CLI process restarted: whatever was running did not survive it.
|
|
126
|
+
/** The CLI process restarted: whatever was running did not survive it. A
|
|
127
|
+
* held shell task was never shown, so it is dropped rather than ended. */
|
|
106
128
|
onProcessRestart(): TaskChange;
|
|
107
129
|
/** True when the feed holds `taskId` as running — the stop request's gate. */
|
|
108
130
|
isRunning(taskId: string): boolean;
|
|
@@ -116,6 +138,15 @@ export declare class TaskFeed {
|
|
|
116
138
|
* dropped; the ids are remembered so a late bookend cannot resurrect them. */
|
|
117
139
|
markPublished(endedIds: string[]): void;
|
|
118
140
|
private running;
|
|
141
|
+
/** Puts a running entry into the table. A known id starting again is a new
|
|
142
|
+
* life of the same task — a resumed subagent is re-registered under its
|
|
143
|
+
* agent id. It replaces the ended entry, published or not: the running
|
|
144
|
+
* state supersedes an ending the client has not seen yet, and a published
|
|
145
|
+
* id must be shown again. */
|
|
146
|
+
private admit;
|
|
147
|
+
/** Applies a `task_updated` patch's label and placement; a label taken from
|
|
148
|
+
* the tool call is never replaced. */
|
|
149
|
+
private patch;
|
|
119
150
|
private end;
|
|
120
151
|
}
|
|
121
152
|
export type PublisherTimers = {
|
|
@@ -139,6 +170,9 @@ export declare class TaskFeedPublisher {
|
|
|
139
170
|
private pending;
|
|
140
171
|
private reconcileTimer;
|
|
141
172
|
private reconcileAt;
|
|
173
|
+
/** Promotes held foreground shells: armed at the feed's `nextHoldDue()`. */
|
|
174
|
+
private holdTimer;
|
|
175
|
+
private holdAt;
|
|
142
176
|
private closed;
|
|
143
177
|
/** Set by `dispose({ flush: true })`: a publish queued behind an in-flight
|
|
144
178
|
* send still goes out once — a retirement's `interrupted` endings. */
|
|
@@ -152,6 +186,10 @@ export declare class TaskFeedPublisher {
|
|
|
152
186
|
private settle;
|
|
153
187
|
private arm;
|
|
154
188
|
private cancelPending;
|
|
189
|
+
/** One timer for every held shell, at the earliest due time. A held shell
|
|
190
|
+
* that ended early leaves the timer armed: it fires, promotes nothing, and
|
|
191
|
+
* re-arms for the next one still held. */
|
|
192
|
+
private armHold;
|
|
155
193
|
private armReconcile;
|
|
156
194
|
}
|
|
157
195
|
//# sourceMappingURL=task-feed.d.ts.map
|
package/dist/task-feed.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-feed.d.ts","sourceRoot":"","sources":["../src/task-feed.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"task-feed.d.ts","sourceRoot":"","sources":["../src/task-feed.ts"],"names":[],"mappings":"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"}
|
package/dist/task-feed.js
CHANGED
|
@@ -19,11 +19,19 @@
|
|
|
19
19
|
* progress, at once on an ending, and once more after a burst so the latest
|
|
20
20
|
* values are not lost.
|
|
21
21
|
*
|
|
22
|
+
* A foreground shell task (`local_bash`, not backgrounded) is held back for
|
|
23
|
+
* `FOREGROUND_SHELL_HOLD_MS`: Claude Code registers every shell command as a
|
|
24
|
+
* task, so only one still running after the hold, or moved to the background,
|
|
25
|
+
* is published. One that ends while held is dropped without a trace. The feed
|
|
26
|
+
* stays clock-driven: the publisher's timer calls `promoteDue()`.
|
|
27
|
+
*
|
|
22
28
|
* Neither class ever copies a command line (`SDKTaskStartedMessage.prompt`)
|
|
23
29
|
* into an entry or a log line.
|
|
24
30
|
*/
|
|
25
31
|
/** The `session_info_update._meta` key the snapshot is published under. */
|
|
26
32
|
export const TASKS_META_KEY = "_claude/tasks";
|
|
33
|
+
/** How long a foreground shell task is held back before it is published. */
|
|
34
|
+
export const FOREGROUND_SHELL_HOLD_MS = 5000;
|
|
27
35
|
/** How long a running background task may be missing from the level before
|
|
28
36
|
* it is ended as `interrupted`. The level and the bookends race (the level
|
|
29
37
|
* may arrive first), so a real ending gets this long to land. */
|
|
@@ -92,29 +100,39 @@ export class TaskFeed {
|
|
|
92
100
|
snapshotLives = new Map();
|
|
93
101
|
/** task id → time after which a missing bookend means `interrupted`. */
|
|
94
102
|
deadlines = new Map();
|
|
103
|
+
/** Foreground shell tasks not published yet: see `promoteDue`. */
|
|
104
|
+
held = new Map();
|
|
105
|
+
/** Shell task ids labelled by their tool call: a patch never relabels them. */
|
|
106
|
+
toolDescribed = new Set();
|
|
95
107
|
constructor(clock, log = {}, sessionId = "") {
|
|
96
108
|
this.clock = clock;
|
|
97
109
|
this.log = log;
|
|
98
110
|
this.sessionId = sessionId;
|
|
99
111
|
}
|
|
100
|
-
|
|
112
|
+
/**
|
|
113
|
+
* `toolDescription` is the spawning tool call's own `description` input. A
|
|
114
|
+
* shell task carries it when it is non-empty; otherwise the SDK's, which
|
|
115
|
+
* for a shell is the command text. Other task types keep the SDK's.
|
|
116
|
+
*/
|
|
117
|
+
onStarted(msg, toolDescription) {
|
|
101
118
|
if (msg.ambient) {
|
|
102
119
|
this.ambient.add(msg.task_id);
|
|
103
120
|
return "none";
|
|
104
121
|
}
|
|
105
|
-
if (this.tasks.get(msg.task_id)?.status === "running")
|
|
122
|
+
if (this.tasks.get(msg.task_id)?.status === "running" || this.held.has(msg.task_id)) {
|
|
106
123
|
return "none";
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
}
|
|
125
|
+
const shell = msg.task_type === "local_bash";
|
|
126
|
+
const fromTool = shell && typeof toolDescription === "string" && toolDescription.trim() !== "";
|
|
127
|
+
if (fromTool)
|
|
128
|
+
this.toolDescribed.add(msg.task_id);
|
|
129
|
+
else
|
|
130
|
+
this.toolDescribed.delete(msg.task_id);
|
|
131
|
+
const entry = {
|
|
114
132
|
id: msg.task_id,
|
|
115
133
|
toolCallId: msg.tool_use_id ?? null,
|
|
116
134
|
type: normaliseType(msg.task_type),
|
|
117
|
-
description: msg.description,
|
|
135
|
+
description: fromTool ? toolDescription : msg.description,
|
|
118
136
|
background: msg.is_backgrounded ?? false,
|
|
119
137
|
depth: msg.spawn_depth ?? 0,
|
|
120
138
|
status: "running",
|
|
@@ -123,27 +141,71 @@ export class TaskFeed {
|
|
|
123
141
|
usage: null,
|
|
124
142
|
lastTool: null,
|
|
125
143
|
summary: null,
|
|
126
|
-
}
|
|
144
|
+
};
|
|
145
|
+
if (shell && msg.is_backgrounded !== true) {
|
|
146
|
+
this.held.set(msg.task_id, entry);
|
|
147
|
+
return "held";
|
|
148
|
+
}
|
|
149
|
+
this.admit(entry);
|
|
127
150
|
return "progress";
|
|
128
151
|
}
|
|
152
|
+
/** Publishes every held shell task whose hold has run out. */
|
|
153
|
+
promoteDue() {
|
|
154
|
+
const now = this.clock();
|
|
155
|
+
let change = "none";
|
|
156
|
+
for (const [id, entry] of this.held) {
|
|
157
|
+
if (entry.startedAt + FOREGROUND_SHELL_HOLD_MS > now)
|
|
158
|
+
continue;
|
|
159
|
+
this.held.delete(id);
|
|
160
|
+
this.admit(entry);
|
|
161
|
+
change = "progress";
|
|
162
|
+
}
|
|
163
|
+
return change;
|
|
164
|
+
}
|
|
165
|
+
/** When the earliest held shell task is due for promotion; null if none is held. */
|
|
166
|
+
nextHoldDue() {
|
|
167
|
+
let next = null;
|
|
168
|
+
for (const entry of this.held.values()) {
|
|
169
|
+
const due = entry.startedAt + FOREGROUND_SHELL_HOLD_MS;
|
|
170
|
+
if (next === null || due < next)
|
|
171
|
+
next = due;
|
|
172
|
+
}
|
|
173
|
+
return next;
|
|
174
|
+
}
|
|
129
175
|
onProgress(msg) {
|
|
130
|
-
const
|
|
176
|
+
const heldEntry = this.held.get(msg.task_id);
|
|
177
|
+
const task = heldEntry ?? this.running(msg.task_id, "task_progress");
|
|
131
178
|
if (!task)
|
|
132
179
|
return "none";
|
|
133
180
|
task.usage = usageOf(msg.usage);
|
|
134
181
|
task.lastTool = msg.last_tool_name ?? task.lastTool;
|
|
135
182
|
task.summary = msg.summary ?? task.summary;
|
|
136
|
-
|
|
183
|
+
// Kept for the promotion, but nobody has been shown the task yet.
|
|
184
|
+
return heldEntry ? "none" : "progress";
|
|
137
185
|
}
|
|
138
186
|
onUpdated(msg) {
|
|
187
|
+
const heldEntry = this.held.get(msg.task_id);
|
|
188
|
+
if (heldEntry) {
|
|
189
|
+
if (msg.patch.is_backgrounded === true) {
|
|
190
|
+
// Moved to the background: published at once, then patched as usual.
|
|
191
|
+
this.held.delete(msg.task_id);
|
|
192
|
+
this.admit(heldEntry);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
if (normaliseStatus(msg.patch.status)) {
|
|
196
|
+
this.held.delete(msg.task_id);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
this.patch(heldEntry, msg.patch);
|
|
200
|
+
}
|
|
201
|
+
return "none";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
139
204
|
const task = this.running(msg.task_id, "task_updated");
|
|
140
205
|
if (!task)
|
|
141
206
|
return "none";
|
|
142
207
|
const ending = normaliseStatus(msg.patch.status);
|
|
143
|
-
|
|
144
|
-
task.description = msg.patch.description;
|
|
145
|
-
if (msg.patch.is_backgrounded !== undefined)
|
|
146
|
-
task.background = msg.patch.is_backgrounded;
|
|
208
|
+
this.patch(task, msg.patch);
|
|
147
209
|
if (ending) {
|
|
148
210
|
this.end(task, ending);
|
|
149
211
|
return "terminal";
|
|
@@ -151,6 +213,9 @@ export class TaskFeed {
|
|
|
151
213
|
return "progress";
|
|
152
214
|
}
|
|
153
215
|
onNotification(msg) {
|
|
216
|
+
// A held shell task that ends was never shown: its ending is not news.
|
|
217
|
+
if (this.held.delete(msg.task_id))
|
|
218
|
+
return "none";
|
|
154
219
|
const task = this.running(msg.task_id, "task_notification");
|
|
155
220
|
if (!task)
|
|
156
221
|
return "none";
|
|
@@ -205,8 +270,10 @@ export class TaskFeed {
|
|
|
205
270
|
}
|
|
206
271
|
return change;
|
|
207
272
|
}
|
|
208
|
-
/** The CLI process restarted: whatever was running did not survive it.
|
|
273
|
+
/** The CLI process restarted: whatever was running did not survive it. A
|
|
274
|
+
* held shell task was never shown, so it is dropped rather than ended. */
|
|
209
275
|
onProcessRestart() {
|
|
276
|
+
this.held.clear();
|
|
210
277
|
let change = "none";
|
|
211
278
|
for (const task of this.tasks.values()) {
|
|
212
279
|
if (task.status !== "running")
|
|
@@ -256,6 +323,25 @@ export class TaskFeed {
|
|
|
256
323
|
}
|
|
257
324
|
return undefined;
|
|
258
325
|
}
|
|
326
|
+
/** Puts a running entry into the table. A known id starting again is a new
|
|
327
|
+
* life of the same task — a resumed subagent is re-registered under its
|
|
328
|
+
* agent id. It replaces the ended entry, published or not: the running
|
|
329
|
+
* state supersedes an ending the client has not seen yet, and a published
|
|
330
|
+
* id must be shown again. */
|
|
331
|
+
admit(entry) {
|
|
332
|
+
this.published.delete(entry.id);
|
|
333
|
+
this.lives.set(entry.id, (this.lives.get(entry.id) ?? 0) + 1);
|
|
334
|
+
this.tasks.set(entry.id, entry);
|
|
335
|
+
}
|
|
336
|
+
/** Applies a `task_updated` patch's label and placement; a label taken from
|
|
337
|
+
* the tool call is never replaced. */
|
|
338
|
+
patch(task, patch) {
|
|
339
|
+
if (patch.description !== undefined && !this.toolDescribed.has(task.id)) {
|
|
340
|
+
task.description = patch.description;
|
|
341
|
+
}
|
|
342
|
+
if (patch.is_backgrounded !== undefined)
|
|
343
|
+
task.background = patch.is_backgrounded;
|
|
344
|
+
}
|
|
259
345
|
end(task, status, at = this.clock()) {
|
|
260
346
|
task.status = status;
|
|
261
347
|
task.endedAt = at;
|
|
@@ -283,6 +369,9 @@ export class TaskFeedPublisher {
|
|
|
283
369
|
pending = null;
|
|
284
370
|
reconcileTimer = null;
|
|
285
371
|
reconcileAt = null;
|
|
372
|
+
/** Promotes held foreground shells: armed at the feed's `nextHoldDue()`. */
|
|
373
|
+
holdTimer = null;
|
|
374
|
+
holdAt = null;
|
|
286
375
|
closed = false;
|
|
287
376
|
/** Set by `dispose({ flush: true })`: a publish queued behind an in-flight
|
|
288
377
|
* send still goes out once — a retirement's `interrupted` endings. */
|
|
@@ -306,6 +395,9 @@ export class TaskFeedPublisher {
|
|
|
306
395
|
else if (this.pending === null)
|
|
307
396
|
this.arm(wait);
|
|
308
397
|
}
|
|
398
|
+
else if (change === "held") {
|
|
399
|
+
this.armHold();
|
|
400
|
+
}
|
|
309
401
|
this.armReconcile();
|
|
310
402
|
}
|
|
311
403
|
dispose(options = {}) {
|
|
@@ -316,6 +408,10 @@ export class TaskFeedPublisher {
|
|
|
316
408
|
this.timers.clearTimeout(this.reconcileTimer);
|
|
317
409
|
this.reconcileTimer = null;
|
|
318
410
|
this.reconcileAt = null;
|
|
411
|
+
if (this.holdTimer !== null)
|
|
412
|
+
this.timers.clearTimeout(this.holdTimer);
|
|
413
|
+
this.holdTimer = null;
|
|
414
|
+
this.holdAt = null;
|
|
319
415
|
}
|
|
320
416
|
publishNow() {
|
|
321
417
|
this.cancelPending();
|
|
@@ -382,6 +478,40 @@ export class TaskFeedPublisher {
|
|
|
382
478
|
this.timers.clearTimeout(this.pending);
|
|
383
479
|
this.pending = null;
|
|
384
480
|
}
|
|
481
|
+
/** One timer for every held shell, at the earliest due time. A held shell
|
|
482
|
+
* that ended early leaves the timer armed: it fires, promotes nothing, and
|
|
483
|
+
* re-arms for the next one still held. */
|
|
484
|
+
armHold() {
|
|
485
|
+
const at = this.feed.nextHoldDue();
|
|
486
|
+
if (at === null)
|
|
487
|
+
return;
|
|
488
|
+
if (this.holdAt !== null && this.holdAt <= at)
|
|
489
|
+
return;
|
|
490
|
+
if (this.holdTimer !== null)
|
|
491
|
+
this.timers.clearTimeout(this.holdTimer);
|
|
492
|
+
this.holdAt = at;
|
|
493
|
+
this.holdTimer = this.timers.setTimeout(() => {
|
|
494
|
+
this.holdTimer = null;
|
|
495
|
+
this.holdAt = null;
|
|
496
|
+
if (this.closed)
|
|
497
|
+
return;
|
|
498
|
+
// Outside the SDK loop nobody handles an error: a failed send is
|
|
499
|
+
// caught in `publishNow`, and anything else thrown is reported here.
|
|
500
|
+
try {
|
|
501
|
+
this.notify(this.feed.promoteDue());
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
try {
|
|
505
|
+
this.onSendError(error);
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
// Re-arming matters more than the log line.
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (!this.closed)
|
|
512
|
+
this.armHold();
|
|
513
|
+
}, Math.max(0, at - this.timers.now()));
|
|
514
|
+
}
|
|
385
515
|
armReconcile() {
|
|
386
516
|
const at = this.feed.nextReconcileAt();
|
|
387
517
|
if (at === null || at === this.reconcileAt)
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.25.1",
|
|
7
7
|
"description": "Claude Code in Zed and other ACP clients with the VS Code extension's UX — multi-select questions, refusal consent, live quota windows, background tasks, Ultracode effort and session notices. Fork of claude-agent-acp.",
|
|
8
8
|
"main": "dist/lib.js",
|
|
9
9
|
"types": "dist/lib.d.ts",
|