@coda-console/claude 1.7.1 → 1.7.6
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/agent/permissions.mjs +3 -2
- package/agent/session.mjs +15 -8
- package/agent/tools.mjs +102 -0
- package/package.json +1 -1
package/agent/permissions.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// can change Claude's permissions.
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
|
+
import { toolCallFields } from './tools.mjs';
|
|
6
7
|
|
|
7
8
|
const deny = { behavior: 'deny', message: '用户未批准此操作' };
|
|
8
9
|
|
|
@@ -12,8 +13,8 @@ export function permissionRequest({ sessionId, toolName, input, context = {} })
|
|
|
12
13
|
const options = [{ optionId: 'allow-once', name: '仅允许本次', kind: 'allow_once' },
|
|
13
14
|
...suggestions.map((change, i) => ({ optionId: `suggestion-${i}`, name: describeChange(change, input), kind: 'allow_always' })),
|
|
14
15
|
{ optionId: 'deny', name: '拒绝', kind: 'reject_once' }];
|
|
15
|
-
const params = { sessionId, options, toolCall: { toolCallId: context.toolUseID ?? randomUUID(),
|
|
16
|
-
...(context.decisionReason ?
|
|
16
|
+
const params = { sessionId, options, toolCall: { toolCallId: context.toolUseID ?? randomUUID(), status: 'pending',
|
|
17
|
+
...toolCallFields(toolName, input, context.decisionReason ? [{ type: 'content', content: { type: 'text', text: context.decisionReason } }] : []) } };
|
|
17
18
|
const answer = response => {
|
|
18
19
|
const { outcome, optionId } = response?.outcome ?? {};
|
|
19
20
|
if (outcome !== 'selected') return deny;
|
package/agent/session.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { chosenServices, listedModels, serviceSettings } from './providers.mjs';
|
|
|
17
17
|
import { SessionTitle } from './titles.mjs';
|
|
18
18
|
import { MODEL_EFFORT, MODES, claudeModels, configOptions, reportedModel } from './models.mjs';
|
|
19
19
|
import { permissionRequest } from './permissions.mjs';
|
|
20
|
+
import { toolCallFields, ToolCallCache } from './tools.mjs';
|
|
20
21
|
import { elicitationForm, questionForm } from './questions.mjs';
|
|
21
22
|
import { ClaudeWaits } from './waits.mjs';
|
|
22
23
|
import { CacheUse } from './compaction.mjs';
|
|
@@ -81,7 +82,7 @@ export class ClaudeSession {
|
|
|
81
82
|
this.services = chosenServices(env); this.serviceModel = null;
|
|
82
83
|
this.models = []; this.commands = []; this.terminal = null; this.current = { mode: 'default', model: undefined, effort: null };
|
|
83
84
|
// Streamed text by API message, so that the complete message adds only what was not streamed; tool calls said.
|
|
84
|
-
this.streams = new Map(); this.streamed = new Map(); this.tools = new
|
|
85
|
+
this.streams = new Map(); this.streamed = new Map(); this.tools = new ToolCallCache(); this.children = new Set(); this.early = new Map(); this.compaction = null;
|
|
85
86
|
this.waits = new ClaudeWaits({ say: (params, sessionId = this.sessionId) => this.notify('_coda/wait', { sessionId, ...params }),
|
|
86
87
|
going: () => this.prompts.size > 0 || this.waits.handling, registered: sessionId => this.registered(sessionId) });
|
|
87
88
|
// What is told of the main thread's cache (compaction.mjs); while a compaction Coda asked for goes, its input and answer; and
|
|
@@ -547,8 +548,9 @@ export class ClaudeSession {
|
|
|
547
548
|
}
|
|
548
549
|
for (const block of m.message?.content ?? []) {
|
|
549
550
|
if (block.type === 'tool_use') {
|
|
550
|
-
|
|
551
|
-
this.
|
|
551
|
+
const fields = toolCallFields(block.name, block.input);
|
|
552
|
+
this.tools.start(block.id, block.name, fields.content ?? []);
|
|
553
|
+
this.update(sessionId, { sessionUpdate: 'tool_call', toolCallId: block.id, ...fields, status: 'in_progress' });
|
|
552
554
|
} else if (['text', 'thinking'].includes(block.type)) {
|
|
553
555
|
const said = block.text ?? block.thinking ?? '';
|
|
554
556
|
// One complete packet carries one block of the message: the first streamed one of its kind not yet confirmed.
|
|
@@ -561,17 +563,20 @@ export class ClaudeSession {
|
|
|
561
563
|
}
|
|
562
564
|
toolResults(m, parent) {
|
|
563
565
|
for (const block of Array.isArray(m.message?.content) ? m.message.content : []) {
|
|
564
|
-
if (block.type !== 'tool_result'
|
|
566
|
+
if (block.type !== 'tool_result') continue;
|
|
567
|
+
const content = this.tools.finish(block.tool_use_id, m.tool_use_result, block.is_error);
|
|
568
|
+
if (!content) continue;
|
|
565
569
|
const said = text(block.content);
|
|
566
570
|
this.update(parent ?? this.sessionId, { sessionUpdate: 'tool_call_update', toolCallId: block.tool_use_id, status: block.is_error ? 'failed' : 'completed',
|
|
567
|
-
|
|
571
|
+
// ACP content updates replace the list: keep the latest diffs alongside the result text.
|
|
572
|
+
rawOutput: m.tool_use_result ?? block.content, content: [...content, { type: 'content', content: { type: 'text', text: said } }] });
|
|
568
573
|
// Claude Code says where a command it put in the background writes in the launch's result.
|
|
569
574
|
const path = said.match(/Output is being written to: (\S+?\.output)\b/)?.[1];
|
|
570
575
|
if (path) this.waits.outputFile(block.tool_use_id, path);
|
|
571
576
|
}
|
|
572
577
|
}
|
|
573
578
|
result(m) {
|
|
574
|
-
const
|
|
579
|
+
const windows = Object.values(m.modelUsage ?? {});
|
|
575
580
|
const ids = takenIn(m);
|
|
576
581
|
// The compaction Coda asked for (compact) is the turn that names its input. Any other is the session's own, whose cache runs
|
|
577
582
|
// from its last request. Coda is told what is known of that cache (compaction.mjs).
|
|
@@ -581,8 +586,10 @@ export class ClaudeSession {
|
|
|
581
586
|
this.turnCompaction = {};
|
|
582
587
|
if (!compaction) { this.cacheUse.ended(); for (const output of turn.outputs ?? []) this.commandOutput(output); }
|
|
583
588
|
const told = this.cacheUse.told();
|
|
584
|
-
|
|
585
|
-
|
|
589
|
+
// The result's usage sums every request in the turn. The window holds only the last main-thread request, or what
|
|
590
|
+
// compaction left. With no measurement, report no usable window; lifecycle metadata is still told.
|
|
591
|
+
this.update(this.sessionId, { sessionUpdate: 'usage_update', used: this.cacheUse.context ?? 0,
|
|
592
|
+
size: this.cacheUse.context === null ? 0 : Math.max(0, ...windows.map(w => w.contextWindow ?? 0)), cost: { amount: m.total_cost_usd ?? 0, currency: 'USD' },
|
|
586
593
|
...(Object.keys(told).length ? { _meta: { coda: told } } : {}) });
|
|
587
594
|
this.turning = false;
|
|
588
595
|
// The compaction's: Coda is answered, and nothing is taken up.
|
package/agent/tools.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Claude Agent SDK 0.3.296's sdk-tools.d.ts, plus names used by older Claude Code sessions.
|
|
2
|
+
// Only native tool names carry these meanings; MCP tools and unknown tools stay `other`.
|
|
3
|
+
const kinds = new Map(Object.entries({
|
|
4
|
+
execute: ['Bash', 'BashOutput', 'KillBash', 'KillShell', 'TaskOutput', 'TaskStop', 'Monitor'],
|
|
5
|
+
read: ['Read', 'NotebookRead'],
|
|
6
|
+
edit: ['Edit', 'MultiEdit', 'Write', 'NotebookEdit'],
|
|
7
|
+
search: ['Grep', 'Glob', 'LS'],
|
|
8
|
+
fetch: ['WebFetch', 'WebSearch'],
|
|
9
|
+
think: ['TodoWrite', 'EnterPlanMode', 'ExitPlanMode', 'TaskCreate', 'TaskGet', 'TaskUpdate', 'TaskList', 'ReportFindings', 'ProposeGoal'],
|
|
10
|
+
}).flatMap(([kind, names]) => names.map(name => [name, kind])));
|
|
11
|
+
|
|
12
|
+
const string = value => typeof value === 'string';
|
|
13
|
+
const firstLine = value => string(value) ? value.split(/\r\n|\r|\n/, 1)[0].trim() : '';
|
|
14
|
+
const short = value => { const chars = Array.from(firstLine(value)); return chars.length > 200 ? `${chars.slice(0, 199).join('')}…` : chars.join(''); };
|
|
15
|
+
|
|
16
|
+
// Pure presentation shared by calls and permission requests. rawInput is the SDK's original object.
|
|
17
|
+
// Existing content (e.g. why permission is needed) accompanies the diffs, never replaces them.
|
|
18
|
+
export function toolCallFields(name, rawInput, content = []) {
|
|
19
|
+
const input = rawInput ?? {}, kind = kinds.get(name) ?? 'other';
|
|
20
|
+
const path = ['read', 'edit'].includes(kind) ? (input.file_path ?? input.notebook_path) : undefined;
|
|
21
|
+
const hasPath = string(path) && path.length > 0;
|
|
22
|
+
let title = name;
|
|
23
|
+
if (kind === 'execute') title = short(input.command) || name;
|
|
24
|
+
if (kind === 'search') title = short(input.pattern ?? input.query ?? input.path) || name;
|
|
25
|
+
if (kind === 'fetch') title = short(input.url ?? input.query) || name;
|
|
26
|
+
if (hasPath) title = short(`${kind === 'read' ? '读取' : '编辑'} ${path}`);
|
|
27
|
+
|
|
28
|
+
const diffs = [];
|
|
29
|
+
if (hasPath) {
|
|
30
|
+
if (name === 'Write' && string(input.content)) diffs.push({ type: 'diff', path, oldText: null, newText: input.content });
|
|
31
|
+
const edits = name === 'Edit' ? [input] : name === 'MultiEdit' && Array.isArray(input.edits) ? input.edits : [];
|
|
32
|
+
for (const edit of edits) {
|
|
33
|
+
if (string(edit?.old_string) && string(edit?.new_string)) diffs.push({ type: 'diff', path, oldText: edit.old_string, newText: edit.new_string });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const combined = [...diffs, ...content];
|
|
37
|
+
return { title, kind, rawInput,
|
|
38
|
+
...(hasPath ? { locations: [{ path, ...(name === 'Read' && Number.isInteger(input.offset) && input.offset >= 0 ? { line: Math.max(1, input.offset) } : {}) }] } : {}),
|
|
39
|
+
...(combined.length ? { content: combined } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A patch line includes its newline unless the immediately following marker says otherwise.
|
|
44
|
+
// Context belongs to both sides; a marker after '-' or '+' only applies to that side.
|
|
45
|
+
function patchText(lines, side) {
|
|
46
|
+
return lines.map((line, i) => line.startsWith(side) || line.startsWith(' ')
|
|
47
|
+
? line.slice(1) + (lines[i + 1] === '\' ? '' : '\n') : '').join('');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A successful SDK result is authoritative: a Write may overwrite a file, and an Edit may replace
|
|
51
|
+
// many occurrences or be changed by the user. Keep previews only when no structured result is given.
|
|
52
|
+
export function toolResultContent(name, content, result, failed = false) {
|
|
53
|
+
if (failed || !['Edit', 'MultiEdit', 'Write'].includes(name) || !string(result?.filePath)) return content;
|
|
54
|
+
let diffs;
|
|
55
|
+
if (result.staged === true) diffs = []; // SDK: held for review, the file is unchanged.
|
|
56
|
+
else if (name === 'Write' && string(result.content)
|
|
57
|
+
&& (string(result.originalFile) || (result.type === 'create' && result.originalFile === null))) {
|
|
58
|
+
diffs = [{ type: 'diff', path: result.filePath, oldText: result.originalFile, newText: result.content }];
|
|
59
|
+
} else if (Array.isArray(result.structuredPatch)
|
|
60
|
+
&& result.structuredPatch.every(hunk => Array.isArray(hunk?.lines) && hunk.lines.every(string))) {
|
|
61
|
+
// Each hunk carries both sides, including context. Do not merge distant hunks: that would
|
|
62
|
+
// invent adjacent lines and can undercount independent replacements.
|
|
63
|
+
diffs = result.structuredPatch.map(hunk => ({ type: 'diff', path: result.filePath,
|
|
64
|
+
oldText: patchText(hunk.lines, '-'), newText: patchText(hunk.lines, '+'),
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
// An update whose original text was too large and whose patch is empty has no measurable diff;
|
|
68
|
+
// in particular, do not keep the input preview that would report it as a newly created file.
|
|
69
|
+
return diffs === undefined ? content : [...diffs, ...content.filter(block => block.type !== 'diff')];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Pending calls must survive other tools finishing. Completed calls only serve repeated SDK results:
|
|
73
|
+
// keep the most recently completed 128 calls, with at most 4 MiB of estimated string storage.
|
|
74
|
+
// Evicted results are ignored, so they cannot overwrite the diff already sent to the client.
|
|
75
|
+
export class ToolCallCache {
|
|
76
|
+
#pending = new Map(); #completed = new Map(); #bytes = 0;
|
|
77
|
+
constructor({ maxCompleted = 128, maxBytes = 4 * 1024 * 1024 } = {}) {
|
|
78
|
+
this.maxCompleted = maxCompleted; this.maxBytes = maxBytes;
|
|
79
|
+
}
|
|
80
|
+
#forget(id) {
|
|
81
|
+
const entry = this.#completed.get(id);
|
|
82
|
+
if (entry) { this.#bytes -= entry.bytes; this.#completed.delete(id); }
|
|
83
|
+
}
|
|
84
|
+
start(id, name, content) {
|
|
85
|
+
this.#forget(id);
|
|
86
|
+
this.#pending.set(id, { name, content });
|
|
87
|
+
}
|
|
88
|
+
finish(id, result, failed = false) {
|
|
89
|
+
const previous = this.#pending.get(id) ?? this.#completed.get(id)?.tool;
|
|
90
|
+
if (!previous) return undefined;
|
|
91
|
+
const tool = { name: previous.name, content: toolResultContent(previous.name, previous.content, result, failed) };
|
|
92
|
+
this.#pending.delete(id); this.#forget(id);
|
|
93
|
+
// Two bytes per UTF-16 code unit, including ID and serialized field names; the count limit also
|
|
94
|
+
// bounds object overhead. An oversized result is delivered once without evicting smaller entries.
|
|
95
|
+
const bytes = 2 * (id.length + JSON.stringify(tool).length);
|
|
96
|
+
if (bytes <= this.maxBytes) {
|
|
97
|
+
this.#completed.set(id, { tool, bytes }); this.#bytes += bytes;
|
|
98
|
+
while (this.#completed.size > this.maxCompleted || this.#bytes > this.maxBytes) this.#forget(this.#completed.keys().next().value);
|
|
99
|
+
}
|
|
100
|
+
return tool.content;
|
|
101
|
+
}
|
|
102
|
+
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "claude",
|
|
4
4
|
"displayName": "Claude",
|
|
5
5
|
"description": "Claude SDK Agent:Coda 基于 Claude Agent SDK 写的 ACP 入口,一个进程一个 SDK 会话;资源页的「Claude」面板显示会话报告的额度,可从 Coda 登录。",
|
|
6
|
-
"version": "1.7.
|
|
6
|
+
"version": "1.7.6",
|
|
7
7
|
"categories": ["Agent"],
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@agentclientprotocol/sdk": "1.4.0",
|