@windowkit/agent-chat 0.1.0 → 0.4.0
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/core/driver.d.ts +13 -0
- package/dist/core/driver.d.ts.map +1 -1
- package/dist/core/driver.js.map +1 -1
- package/dist/core/markdown.js +29 -0
- package/dist/core/markdown.js.map +1 -1
- package/dist/core/reducer.d.ts.map +1 -1
- package/dist/core/reducer.js +16 -0
- package/dist/core/reducer.js.map +1 -1
- package/dist/core/session.d.ts +3 -0
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +17 -1
- package/dist/core/session.js.map +1 -1
- package/dist/core/types.d.ts +43 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/drivers/claude/index.d.ts.map +1 -1
- package/dist/drivers/claude/index.js +56 -8
- package/dist/drivers/claude/index.js.map +1 -1
- package/dist/drivers/claude/results.d.ts +40 -0
- package/dist/drivers/claude/results.d.ts.map +1 -0
- package/dist/drivers/claude/results.js +235 -0
- package/dist/drivers/claude/results.js.map +1 -0
- package/dist/drivers/claude/tools.d.ts +3 -1
- package/dist/drivers/claude/tools.d.ts.map +1 -1
- package/dist/drivers/claude/tools.js +7 -1
- package/dist/drivers/claude/tools.js.map +1 -1
- package/dist/view/AgentChat.d.ts.map +1 -1
- package/dist/view/AgentChat.js +14 -3
- package/dist/view/AgentChat.js.map +1 -1
- package/dist/view/ArtifactCard.d.ts +6 -0
- package/dist/view/ArtifactCard.d.ts.map +1 -0
- package/dist/view/ArtifactCard.js +6 -0
- package/dist/view/ArtifactCard.js.map +1 -0
- package/dist/view/AttachmentCard.d.ts +8 -0
- package/dist/view/AttachmentCard.d.ts.map +1 -0
- package/dist/view/AttachmentCard.js +55 -0
- package/dist/view/AttachmentCard.js.map +1 -0
- package/dist/view/CodeBlock.d.ts.map +1 -1
- package/dist/view/CodeBlock.js +18 -11
- package/dist/view/CodeBlock.js.map +1 -1
- package/dist/view/Composer.d.ts.map +1 -1
- package/dist/view/Composer.js +9 -1
- package/dist/view/Composer.js.map +1 -1
- package/dist/view/DataPartView.d.ts +6 -0
- package/dist/view/DataPartView.d.ts.map +1 -0
- package/dist/view/DataPartView.js +23 -0
- package/dist/view/DataPartView.js.map +1 -0
- package/dist/view/ImageView.d.ts +18 -0
- package/dist/view/ImageView.d.ts.map +1 -0
- package/dist/view/ImageView.js +35 -0
- package/dist/view/ImageView.js.map +1 -0
- package/dist/view/LinkCard.d.ts +14 -0
- package/dist/view/LinkCard.d.ts.map +1 -0
- package/dist/view/LinkCard.js +40 -0
- package/dist/view/LinkCard.js.map +1 -0
- package/dist/view/MessageList.d.ts.map +1 -1
- package/dist/view/MessageList.js +10 -10
- package/dist/view/MessageList.js.map +1 -1
- package/dist/view/PlanChecklist.d.ts +6 -0
- package/dist/view/PlanChecklist.d.ts.map +1 -0
- package/dist/view/PlanChecklist.js +16 -0
- package/dist/view/PlanChecklist.js.map +1 -0
- package/dist/view/PullRequestCard.d.ts +6 -0
- package/dist/view/PullRequestCard.d.ts.map +1 -0
- package/dist/view/PullRequestCard.js +12 -0
- package/dist/view/PullRequestCard.js.map +1 -0
- package/dist/view/SessionCard.d.ts +6 -0
- package/dist/view/SessionCard.d.ts.map +1 -0
- package/dist/view/SessionCard.js +6 -0
- package/dist/view/SessionCard.js.map +1 -0
- package/dist/view/SuggestedReplies.d.ts +5 -0
- package/dist/view/SuggestedReplies.d.ts.map +1 -0
- package/dist/view/SuggestedReplies.js +10 -0
- package/dist/view/SuggestedReplies.js.map +1 -0
- package/dist/view/TextBlock.d.ts.map +1 -1
- package/dist/view/TextBlock.js +4 -1
- package/dist/view/TextBlock.js.map +1 -1
- package/dist/view/ToolCard.d.ts.map +1 -1
- package/dist/view/ToolCard.js +4 -1
- package/dist/view/ToolCard.js.map +1 -1
- package/dist/view/Transcript.d.ts.map +1 -1
- package/dist/view/Transcript.js +18 -2
- package/dist/view/Transcript.js.map +1 -1
- package/dist/view/context.d.ts +9 -0
- package/dist/view/context.d.ts.map +1 -1
- package/dist/view/context.js +9 -0
- package/dist/view/context.js.map +1 -1
- package/dist/view/look.d.ts +21 -0
- package/dist/view/look.d.ts.map +1 -1
- package/dist/view/look.js +17 -0
- package/dist/view/look.js.map +1 -1
- package/package.json +3 -3
- package/src/core/driver.ts +4 -0
- package/src/core/markdown.ts +34 -0
- package/src/core/reducer.ts +25 -0
- package/src/core/session.ts +17 -1
- package/src/core/types.ts +51 -1
- package/src/drivers/claude/index.ts +62 -7
- package/src/drivers/claude/results.ts +252 -0
- package/src/drivers/claude/tools.ts +7 -1
- package/src/view/AgentChat.tsx +20 -2
- package/src/view/ArtifactCard.tsx +18 -0
- package/src/view/AttachmentCard.tsx +103 -0
- package/src/view/CodeBlock.tsx +18 -11
- package/src/view/Composer.tsx +10 -1
- package/src/view/DataPartView.tsx +40 -0
- package/src/view/ImageView.tsx +64 -0
- package/src/view/LinkCard.tsx +99 -0
- package/src/view/MessageList.tsx +21 -39
- package/src/view/PlanChecklist.tsx +49 -0
- package/src/view/PullRequestCard.tsx +28 -0
- package/src/view/SessionCard.tsx +22 -0
- package/src/view/SuggestedReplies.tsx +34 -0
- package/src/view/TextBlock.tsx +4 -1
- package/src/view/ToolCard.tsx +10 -1
- package/src/view/Transcript.tsx +20 -1
- package/src/view/context.ts +19 -0
- package/src/view/look.ts +38 -0
package/src/core/types.ts
CHANGED
|
@@ -58,7 +58,14 @@ export type ToolOutput =
|
|
|
58
58
|
| { type: 'terminal'; text: string; exitCode?: number }
|
|
59
59
|
/** A file's change. `oldText` null for a new file. */
|
|
60
60
|
| { type: 'diff'; path: string; oldText: string | null; newText: string }
|
|
61
|
-
|
|
61
|
+
/** `width` and `height` in pixels, where the harness says. */
|
|
62
|
+
| {
|
|
63
|
+
type: 'image';
|
|
64
|
+
mime: string;
|
|
65
|
+
data: string /* base64 */;
|
|
66
|
+
width?: number;
|
|
67
|
+
height?: number;
|
|
68
|
+
}
|
|
62
69
|
/** What an app tool's `run` returned, as it returned it. */
|
|
63
70
|
| { type: 'value'; value: unknown };
|
|
64
71
|
|
|
@@ -161,6 +168,49 @@ export interface DataPart {
|
|
|
161
168
|
value: unknown;
|
|
162
169
|
}
|
|
163
170
|
|
|
171
|
+
/** `DataPart.name`s a driver of this package writes, and their values. */
|
|
172
|
+
export interface DataValues {
|
|
173
|
+
/** A pull request a command opened, edited or merged. */
|
|
174
|
+
'pull-request': PullRequestData;
|
|
175
|
+
/** An agent session started elsewhere: a cloud session, a workflow. */
|
|
176
|
+
session: SessionLinkData;
|
|
177
|
+
/** A page the agent published. */
|
|
178
|
+
artifact: ArtifactData;
|
|
179
|
+
/** Files the agent sent the user. */
|
|
180
|
+
attachments: AttachmentsData;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface PullRequestData {
|
|
184
|
+
number: number;
|
|
185
|
+
url?: string;
|
|
186
|
+
/** `'created'`, `'edited'`, `'merged'`, or what else the harness says. */
|
|
187
|
+
action: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface SessionLinkData {
|
|
191
|
+
url: string;
|
|
192
|
+
/** What the session was started to do. */
|
|
193
|
+
title?: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface ArtifactData {
|
|
197
|
+
url: string;
|
|
198
|
+
title?: string;
|
|
199
|
+
/** The local file it was published from. */
|
|
200
|
+
path?: string;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface AttachmentsData {
|
|
204
|
+
caption?: string;
|
|
205
|
+
files: readonly {
|
|
206
|
+
path: string;
|
|
207
|
+
/** Bytes. */
|
|
208
|
+
size?: number;
|
|
209
|
+
mime?: string;
|
|
210
|
+
isImage: boolean;
|
|
211
|
+
}[];
|
|
212
|
+
}
|
|
213
|
+
|
|
164
214
|
export type Part =
|
|
165
215
|
| TextPart
|
|
166
216
|
| ReasoningPart
|
|
@@ -36,6 +36,7 @@ import type {
|
|
|
36
36
|
ToolCall,
|
|
37
37
|
} from '../../core/types.js';
|
|
38
38
|
import { InputQueue } from './input.js';
|
|
39
|
+
import { TASK_TOOLS, TaskFold, dataOf, imagesOf } from './results.js';
|
|
39
40
|
import { incomingOf, originOf, peerEnvelope } from './peers.js';
|
|
40
41
|
import { shapeFromJsonSchema, zodShapeOf } from './schema.js';
|
|
41
42
|
import {
|
|
@@ -171,7 +172,11 @@ async function startClaude(
|
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
// --- tool calls, as the session sees them --------------------------------
|
|
174
|
-
const seen = new Map<
|
|
175
|
+
const seen = new Map<
|
|
176
|
+
string,
|
|
177
|
+
{ name: string; input: unknown; parent?: string }
|
|
178
|
+
>();
|
|
179
|
+
const tasks = new TaskFold();
|
|
175
180
|
const denied = new Set<string>();
|
|
176
181
|
const hidden = new Set<string>();
|
|
177
182
|
|
|
@@ -271,6 +276,9 @@ async function startClaude(
|
|
|
271
276
|
// already the default for the models that think; `driverOptions.thinking`
|
|
272
277
|
// overrides it, for an older model or for none.
|
|
273
278
|
thinking: { type: 'adaptive', display: 'summarized' },
|
|
279
|
+
// a reply the user might send next, after each turn: drawn as a chip
|
|
280
|
+
// that fills the composer
|
|
281
|
+
promptSuggestions: true,
|
|
274
282
|
hooks: { PreToolUse: [{ hooks: [preToolUse] }] },
|
|
275
283
|
canUseTool,
|
|
276
284
|
env: {
|
|
@@ -345,6 +353,11 @@ async function startClaude(
|
|
|
345
353
|
}
|
|
346
354
|
case 'result':
|
|
347
355
|
return onResult(m);
|
|
356
|
+
case 'prompt_suggestion':
|
|
357
|
+
if (typeof m.suggestion === 'string') {
|
|
358
|
+
host.emit({ type: 'suggestion', text: m.suggestion });
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
348
361
|
}
|
|
349
362
|
};
|
|
350
363
|
|
|
@@ -407,7 +420,11 @@ async function startClaude(
|
|
|
407
420
|
const id = String(block.id);
|
|
408
421
|
const name = String(block.name);
|
|
409
422
|
blocks.set(index, { kind: 'tool', id });
|
|
410
|
-
seen.set(id, {
|
|
423
|
+
seen.set(id, {
|
|
424
|
+
name,
|
|
425
|
+
input: undefined,
|
|
426
|
+
...(parent ? { parent } : {}),
|
|
427
|
+
});
|
|
411
428
|
if (HIDDEN_TOOLS.has(name)) {
|
|
412
429
|
hidden.add(id);
|
|
413
430
|
return;
|
|
@@ -486,7 +503,11 @@ async function startClaude(
|
|
|
486
503
|
} else if (block.type === 'tool_use') {
|
|
487
504
|
const id = String(block.id);
|
|
488
505
|
const name = String(block.name);
|
|
489
|
-
seen.set(id, {
|
|
506
|
+
seen.set(id, {
|
|
507
|
+
name,
|
|
508
|
+
input: block.input,
|
|
509
|
+
...(parent ? { parent } : {}),
|
|
510
|
+
});
|
|
490
511
|
if (name === 'TodoWrite') {
|
|
491
512
|
hidden.add(id);
|
|
492
513
|
host.emit({
|
|
@@ -519,12 +540,29 @@ async function startClaude(
|
|
|
519
540
|
const onToolResults = (m: SdkMessage) => {
|
|
520
541
|
const msg = m.message as { content?: unknown } | undefined;
|
|
521
542
|
if (!Array.isArray(msg?.content)) return;
|
|
522
|
-
|
|
523
|
-
|
|
543
|
+
const results = (msg.content as Record<string, unknown>[]).filter(
|
|
544
|
+
(b) => b.type === 'tool_result',
|
|
545
|
+
);
|
|
546
|
+
// the tool's own output object, which is one per message: a message
|
|
547
|
+
// with several results says nothing of which it belongs to
|
|
548
|
+
const structured = results.length === 1 ? m.tool_use_result : undefined;
|
|
549
|
+
for (const block of results) {
|
|
524
550
|
const id = String(block.tool_use_id);
|
|
525
|
-
if (hidden.has(id)) continue;
|
|
526
551
|
const call = seen.get(id);
|
|
527
552
|
const isError = block.is_error === true;
|
|
553
|
+
if (
|
|
554
|
+
call &&
|
|
555
|
+
TASK_TOOLS.has(call.name) &&
|
|
556
|
+
!isError &&
|
|
557
|
+
tasks.apply(call.name, call.input, structured)
|
|
558
|
+
) {
|
|
559
|
+
host.emit({
|
|
560
|
+
type: 'plan',
|
|
561
|
+
entries: tasks.entries(),
|
|
562
|
+
...(call.parent ? { parent: call.parent } : {}),
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
if (hidden.has(id)) continue;
|
|
528
566
|
const text = resultText(block.content);
|
|
529
567
|
const name = call?.name ?? '';
|
|
530
568
|
const app = isAppTool(aliases[name] ?? name);
|
|
@@ -538,8 +576,25 @@ async function startClaude(
|
|
|
538
576
|
state: isError ? 'error' : 'done',
|
|
539
577
|
...(isError ? { error: text } : {}),
|
|
540
578
|
// an app tool's outputs are the session's, from the value `run` returned
|
|
541
|
-
...(app
|
|
579
|
+
...(app
|
|
580
|
+
? {}
|
|
581
|
+
: {
|
|
582
|
+
output: [
|
|
583
|
+
...outputOf(name, call?.input, text, isError),
|
|
584
|
+
...imagesOf(block.content, structured),
|
|
585
|
+
],
|
|
586
|
+
}),
|
|
542
587
|
});
|
|
588
|
+
if (isError || app) continue;
|
|
589
|
+
for (const d of dataOf(name, call?.input, structured)) {
|
|
590
|
+
host.emit({
|
|
591
|
+
type: 'data',
|
|
592
|
+
id: `${id}:${d.key}`,
|
|
593
|
+
name: d.name,
|
|
594
|
+
value: d.value,
|
|
595
|
+
...(call?.parent ? { parent: call.parent } : {}),
|
|
596
|
+
});
|
|
597
|
+
}
|
|
543
598
|
}
|
|
544
599
|
};
|
|
545
600
|
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// What Claude Code's structured tool results say beyond their text.
|
|
2
|
+
//
|
|
3
|
+
// A tool's result reaches the client twice: as the `tool_result` block the
|
|
4
|
+
// model reads, and as the user message's `tool_use_result`, the tool's own
|
|
5
|
+
// output object (typed `unknown` by the SDK; the shapes here are its
|
|
6
|
+
// `sdk-tools.d.ts`). The text is what a card shows. The object is where the
|
|
7
|
+
// rest is: the task list's changes, the pull request a command opened, the
|
|
8
|
+
// page the agent published, the files it sent, the pixels of an image.
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ArtifactData,
|
|
12
|
+
AttachmentsData,
|
|
13
|
+
PlanEntry,
|
|
14
|
+
PullRequestData,
|
|
15
|
+
SessionLinkData,
|
|
16
|
+
ToolOutput,
|
|
17
|
+
} from '../../core/types.js';
|
|
18
|
+
|
|
19
|
+
type Obj = Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
const obj = (v: unknown): Obj | undefined =>
|
|
22
|
+
v && typeof v === 'object' && !Array.isArray(v) ? (v as Obj) : undefined;
|
|
23
|
+
const str = (v: unknown): string | undefined =>
|
|
24
|
+
typeof v === 'string' && v !== '' ? v : undefined;
|
|
25
|
+
const num = (v: unknown): number | undefined =>
|
|
26
|
+
typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
27
|
+
|
|
28
|
+
/** The tools whose calls are the plan, not cards. */
|
|
29
|
+
export const TASK_TOOLS: ReadonlySet<string> = new Set([
|
|
30
|
+
'TaskCreate',
|
|
31
|
+
'TaskUpdate',
|
|
32
|
+
'TaskList',
|
|
33
|
+
'TaskGet',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
type Status = PlanEntry['status'];
|
|
37
|
+
const statusOf = (v: unknown): Status | undefined =>
|
|
38
|
+
v === 'pending' || v === 'in_progress' || v === 'completed' ? v : undefined;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Claude Code's task list, as the fold of every `TaskCreate` and
|
|
42
|
+
* `TaskUpdate` this session saw, corrected by what `TaskList` and `TaskGet`
|
|
43
|
+
* report. It lives as long as the session: a task made in one turn is
|
|
44
|
+
* updated in the next.
|
|
45
|
+
*/
|
|
46
|
+
export class TaskFold {
|
|
47
|
+
readonly #tasks = new Map<string, { subject: string; status: Status }>();
|
|
48
|
+
|
|
49
|
+
/** Fold one finished call in. True if the list changed. */
|
|
50
|
+
apply(name: string, input: unknown, result: unknown): boolean {
|
|
51
|
+
const i = obj(input) ?? {};
|
|
52
|
+
const r = obj(result) ?? {};
|
|
53
|
+
switch (name) {
|
|
54
|
+
case 'TaskCreate': {
|
|
55
|
+
const task = obj(r.task);
|
|
56
|
+
const id = str(task?.id);
|
|
57
|
+
const subject = str(task?.subject) ?? str(i.subject);
|
|
58
|
+
if (!id || !subject) return false;
|
|
59
|
+
this.#tasks.set(id, { subject, status: 'pending' });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
case 'TaskUpdate': {
|
|
63
|
+
if (r.success === false) return false;
|
|
64
|
+
const id = str(r.taskId) ?? str(i.taskId);
|
|
65
|
+
if (!id) return false;
|
|
66
|
+
if (i.status === 'deleted') return this.#tasks.delete(id);
|
|
67
|
+
const known = this.#tasks.get(id);
|
|
68
|
+
const subject = str(i.subject) ?? known?.subject;
|
|
69
|
+
// a task from before this session, named by nothing here
|
|
70
|
+
if (!subject) return false;
|
|
71
|
+
const status =
|
|
72
|
+
statusOf(obj(r.statusChange)?.to) ??
|
|
73
|
+
statusOf(i.status) ??
|
|
74
|
+
known?.status ??
|
|
75
|
+
'pending';
|
|
76
|
+
if (known && known.subject === subject && known.status === status) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
this.#tasks.set(id, { subject, status });
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
case 'TaskList': {
|
|
83
|
+
if (!Array.isArray(r.tasks)) return false;
|
|
84
|
+
const before = JSON.stringify(this.entries());
|
|
85
|
+
this.#tasks.clear();
|
|
86
|
+
for (const t of r.tasks as unknown[]) {
|
|
87
|
+
const task = obj(t);
|
|
88
|
+
const id = str(task?.id);
|
|
89
|
+
const subject = str(task?.subject);
|
|
90
|
+
if (id && subject) {
|
|
91
|
+
this.#tasks.set(id, {
|
|
92
|
+
subject,
|
|
93
|
+
status: statusOf(task?.status) ?? 'pending',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return JSON.stringify(this.entries()) !== before;
|
|
98
|
+
}
|
|
99
|
+
case 'TaskGet': {
|
|
100
|
+
const task = obj(r.task);
|
|
101
|
+
const id = str(task?.id);
|
|
102
|
+
const subject = str(task?.subject);
|
|
103
|
+
if (!id || !subject) return false;
|
|
104
|
+
const status = statusOf(task?.status) ?? 'pending';
|
|
105
|
+
const known = this.#tasks.get(id);
|
|
106
|
+
if (known && known.subject === subject && known.status === status) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
this.#tasks.set(id, { subject, status });
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
entries(): PlanEntry[] {
|
|
117
|
+
return [...this.#tasks.values()].map((t) => ({
|
|
118
|
+
text: t.subject,
|
|
119
|
+
status: t.status,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A data part a finished call's result is worth, besides its card. */
|
|
125
|
+
export type ResultData =
|
|
126
|
+
| { name: 'pull-request'; key: string; value: PullRequestData }
|
|
127
|
+
| { name: 'session'; key: string; value: SessionLinkData }
|
|
128
|
+
| { name: 'artifact'; key: string; value: ArtifactData }
|
|
129
|
+
| { name: 'attachments'; key: string; value: AttachmentsData };
|
|
130
|
+
|
|
131
|
+
export function dataOf(
|
|
132
|
+
name: string,
|
|
133
|
+
input: unknown,
|
|
134
|
+
result: unknown,
|
|
135
|
+
): ResultData[] {
|
|
136
|
+
const r = obj(result);
|
|
137
|
+
if (!r) return [];
|
|
138
|
+
const i = obj(input) ?? {};
|
|
139
|
+
switch (name) {
|
|
140
|
+
case 'Bash': {
|
|
141
|
+
const pr = obj(obj(r.gitOperation)?.pr);
|
|
142
|
+
const number = num(pr?.number);
|
|
143
|
+
if (!pr || number === undefined) return [];
|
|
144
|
+
const url = str(pr.url);
|
|
145
|
+
return [
|
|
146
|
+
{
|
|
147
|
+
name: 'pull-request',
|
|
148
|
+
key: 'pr',
|
|
149
|
+
value: {
|
|
150
|
+
number,
|
|
151
|
+
...(url ? { url } : {}),
|
|
152
|
+
action: str(pr.action) ?? 'created',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
case 'Agent':
|
|
158
|
+
case 'Task':
|
|
159
|
+
case 'Workflow': {
|
|
160
|
+
const url = str(r.sessionUrl);
|
|
161
|
+
if (!url) return [];
|
|
162
|
+
const title = str(r.description) ?? str(i.description);
|
|
163
|
+
return [
|
|
164
|
+
{
|
|
165
|
+
name: 'session',
|
|
166
|
+
key: 'session',
|
|
167
|
+
value: { url, ...(title ? { title } : {}) },
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
case 'Artifact': {
|
|
172
|
+
// a publish: list, read and the rest return other shapes
|
|
173
|
+
const url = str(r.url);
|
|
174
|
+
const path = str(r.path);
|
|
175
|
+
if (!url || !path) return [];
|
|
176
|
+
const title = str(r.title);
|
|
177
|
+
return [
|
|
178
|
+
{
|
|
179
|
+
name: 'artifact',
|
|
180
|
+
key: 'artifact',
|
|
181
|
+
value: { url, path, ...(title ? { title } : {}) },
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
case 'SendUserFile': {
|
|
186
|
+
if (!Array.isArray(r.attachments)) return [];
|
|
187
|
+
const files = (r.attachments as unknown[]).flatMap((a) => {
|
|
188
|
+
const f = obj(a);
|
|
189
|
+
const path = str(f?.path);
|
|
190
|
+
if (!f || !path) return [];
|
|
191
|
+
const size = num(f.size);
|
|
192
|
+
const mime = str(f.media_type);
|
|
193
|
+
return [
|
|
194
|
+
{
|
|
195
|
+
path,
|
|
196
|
+
...(size !== undefined ? { size } : {}),
|
|
197
|
+
...(mime ? { mime } : {}),
|
|
198
|
+
isImage: f.isImage === true || !!mime?.startsWith('image/'),
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
});
|
|
202
|
+
if (!files.length) return [];
|
|
203
|
+
const caption = str(r.caption) ?? str(i.caption);
|
|
204
|
+
return [
|
|
205
|
+
{
|
|
206
|
+
name: 'attachments',
|
|
207
|
+
key: 'files',
|
|
208
|
+
value: { files, ...(caption ? { caption } : {}) },
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The images a result carries: `image` blocks in the `tool_result` (an MCP
|
|
218
|
+
* tool's screenshot), or else Read's own of an image file.
|
|
219
|
+
*/
|
|
220
|
+
export function imagesOf(content: unknown, result: unknown): ToolOutput[] {
|
|
221
|
+
const out: ToolOutput[] = [];
|
|
222
|
+
if (Array.isArray(content)) {
|
|
223
|
+
for (const c of content as unknown[]) {
|
|
224
|
+
const block = obj(c);
|
|
225
|
+
if (block?.type !== 'image') continue;
|
|
226
|
+
const source = obj(block.source);
|
|
227
|
+
const data = str(source?.data);
|
|
228
|
+
if (source?.type !== 'base64' || !data) continue;
|
|
229
|
+
out.push({
|
|
230
|
+
type: 'image',
|
|
231
|
+
mime: str(source.media_type) ?? 'image/png',
|
|
232
|
+
data,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (out.length) return out;
|
|
237
|
+
const r = obj(result);
|
|
238
|
+
const file = obj(r?.file);
|
|
239
|
+
const data = str(file?.base64);
|
|
240
|
+
if (r?.type !== 'image' || !file || !data) return [];
|
|
241
|
+
const dims = obj(file.dimensions);
|
|
242
|
+
const width = num(dims?.displayWidth) ?? num(dims?.originalWidth);
|
|
243
|
+
const height = num(dims?.displayHeight) ?? num(dims?.originalHeight);
|
|
244
|
+
return [
|
|
245
|
+
{
|
|
246
|
+
type: 'image',
|
|
247
|
+
mime: str(file.type) ?? 'image/png',
|
|
248
|
+
data,
|
|
249
|
+
...(width !== undefined && height !== undefined ? { width, height } : {}),
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
}
|
|
@@ -39,9 +39,15 @@ const KINDS: Readonly<Record<string, ToolKind>> = {
|
|
|
39
39
|
ExitPlanMode: 'think',
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
/** The tools this driver draws as something other than a tool card
|
|
42
|
+
/** The tools this driver draws as something other than a tool card: the
|
|
43
|
+
* plan (`TodoWrite`, and the task tools Claude Code plans with now) and the
|
|
44
|
+
* question card. */
|
|
43
45
|
export const HIDDEN_TOOLS: ReadonlySet<string> = new Set([
|
|
44
46
|
'TodoWrite',
|
|
47
|
+
'TaskCreate',
|
|
48
|
+
'TaskUpdate',
|
|
49
|
+
'TaskList',
|
|
50
|
+
'TaskGet',
|
|
45
51
|
'AskUserQuestion',
|
|
46
52
|
]);
|
|
47
53
|
|
package/src/view/AgentChat.tsx
CHANGED
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
// `onToolCall`). The top rung lifts the session out (`useAgent`, then
|
|
8
8
|
// `session`) with the same options, moved rather than rewritten.
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
useMemo,
|
|
12
|
+
useRef,
|
|
13
|
+
useState,
|
|
14
|
+
type ReactElement,
|
|
15
|
+
type ReactNode,
|
|
16
|
+
} from 'react';
|
|
11
17
|
import type { StyleProp } from 'react-x11';
|
|
12
18
|
|
|
13
19
|
import {
|
|
@@ -21,6 +27,7 @@ import { Composer, type ComposerOptions } from './Composer.js';
|
|
|
21
27
|
import {
|
|
22
28
|
ChatContext,
|
|
23
29
|
componentMap,
|
|
30
|
+
composerChannel,
|
|
24
31
|
fenceMap,
|
|
25
32
|
type ChatContextValue,
|
|
26
33
|
} from './context.js';
|
|
@@ -126,6 +133,7 @@ function ChatView({
|
|
|
126
133
|
const look = useLook();
|
|
127
134
|
const { fences, components, renderTool, codeActions, onLink, fontSize } =
|
|
128
135
|
view;
|
|
136
|
+
const [composer] = useState(composerChannel);
|
|
129
137
|
const context = useMemo<ChatContextValue>(
|
|
130
138
|
() => ({
|
|
131
139
|
session,
|
|
@@ -135,8 +143,18 @@ function ChatView({
|
|
|
135
143
|
codeActions: codeActions ?? [copyCode],
|
|
136
144
|
onLink,
|
|
137
145
|
fontSize,
|
|
146
|
+
composer,
|
|
138
147
|
}),
|
|
139
|
-
[
|
|
148
|
+
[
|
|
149
|
+
session,
|
|
150
|
+
fences,
|
|
151
|
+
components,
|
|
152
|
+
renderTool,
|
|
153
|
+
codeActions,
|
|
154
|
+
onLink,
|
|
155
|
+
fontSize,
|
|
156
|
+
composer,
|
|
157
|
+
],
|
|
140
158
|
);
|
|
141
159
|
|
|
142
160
|
if (snap.status === 'unavailable' && view.fallback !== undefined) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// A page the agent published: the Artifact tool's `url`, `title` and the
|
|
2
|
+
// file it came from.
|
|
3
|
+
|
|
4
|
+
import type { ReactElement } from 'react';
|
|
5
|
+
|
|
6
|
+
import type { ArtifactData } from '../core/types.js';
|
|
7
|
+
import { LinkCard, shortUrl } from './LinkCard.js';
|
|
8
|
+
|
|
9
|
+
export function ArtifactCard({ value }: { value: ArtifactData }): ReactElement {
|
|
10
|
+
return (
|
|
11
|
+
<LinkCard
|
|
12
|
+
kind="Published"
|
|
13
|
+
title={value.title ?? value.path ?? shortUrl(value.url)}
|
|
14
|
+
detail={[value.path, shortUrl(value.url)].filter(Boolean).join(' · ')}
|
|
15
|
+
href={value.url}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Files the agent sent the user (`SendUserFile`): an image is drawn, read
|
|
2
|
+
// from where the agent left it; any other file is a row with its size and,
|
|
3
|
+
// where the app takes links, an Open button with its `file:` URL.
|
|
4
|
+
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import type { ReactElement } from 'react';
|
|
8
|
+
import { Button } from 'react-x11';
|
|
9
|
+
|
|
10
|
+
import type { AttachmentsData } from '../core/types.js';
|
|
11
|
+
import { useChat } from './context.js';
|
|
12
|
+
import { ImageView } from './ImageView.js';
|
|
13
|
+
import { useLook } from './look.js';
|
|
14
|
+
import { useSessionSnapshot } from './use-agent.js';
|
|
15
|
+
|
|
16
|
+
export function AttachmentCard({
|
|
17
|
+
value,
|
|
18
|
+
}: {
|
|
19
|
+
value: AttachmentsData;
|
|
20
|
+
}): ReactElement {
|
|
21
|
+
const { session, onLink } = useChat();
|
|
22
|
+
const cwd = useSessionSnapshot(session).info.cwd;
|
|
23
|
+
const look = useLook();
|
|
24
|
+
return (
|
|
25
|
+
<box
|
|
26
|
+
style={{
|
|
27
|
+
flexDirection: 'column',
|
|
28
|
+
gap: look.gap,
|
|
29
|
+
padding: look.inset,
|
|
30
|
+
borderWidth: 1,
|
|
31
|
+
borderColor: look.border,
|
|
32
|
+
borderRadius: look.radius,
|
|
33
|
+
}}
|
|
34
|
+
aria-label="Attachments"
|
|
35
|
+
>
|
|
36
|
+
{value.caption ? (
|
|
37
|
+
<text style={{ color: look.text }}>{value.caption}</text>
|
|
38
|
+
) : null}
|
|
39
|
+
{value.files.map((f, i) => {
|
|
40
|
+
const name = path.basename(f.path);
|
|
41
|
+
if (f.isImage) {
|
|
42
|
+
return (
|
|
43
|
+
<box key={i} style={{ flexDirection: 'column', gap: 2 }}>
|
|
44
|
+
<ImageView
|
|
45
|
+
source={cwd ? { path: f.path, cwd } : { path: f.path }}
|
|
46
|
+
alt={name}
|
|
47
|
+
/>
|
|
48
|
+
<text style={{ color: look.muted, fontSize: look.smallSize }}>
|
|
49
|
+
{[name, sizeOf(f.size)].filter(Boolean).join(' · ')}
|
|
50
|
+
</text>
|
|
51
|
+
</box>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const full = cwd ? path.resolve(cwd, f.path) : f.path;
|
|
55
|
+
return (
|
|
56
|
+
<box
|
|
57
|
+
key={i}
|
|
58
|
+
style={{
|
|
59
|
+
flexDirection: 'row',
|
|
60
|
+
alignItems: 'center',
|
|
61
|
+
gap: look.gap,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<text style={{ color: look.text, flexShrink: 1, maxLines: 1 }}>
|
|
65
|
+
{name}
|
|
66
|
+
</text>
|
|
67
|
+
<text
|
|
68
|
+
style={{
|
|
69
|
+
color: look.muted,
|
|
70
|
+
fontSize: look.smallSize,
|
|
71
|
+
flexGrow: 1,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
{sizeOf(f.size)}
|
|
75
|
+
</text>
|
|
76
|
+
{onLink && path.isAbsolute(full) ? (
|
|
77
|
+
<Button
|
|
78
|
+
size="small"
|
|
79
|
+
variant="ghost"
|
|
80
|
+
label="Open"
|
|
81
|
+
onPress={() => onLink(pathToFileURL(full).href)}
|
|
82
|
+
/>
|
|
83
|
+
) : null}
|
|
84
|
+
</box>
|
|
85
|
+
);
|
|
86
|
+
})}
|
|
87
|
+
</box>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `2 KB`, `1.4 MB`. */
|
|
92
|
+
export function sizeOf(bytes: number | undefined): string {
|
|
93
|
+
if (bytes === undefined) return '';
|
|
94
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
95
|
+
const units = ['KB', 'MB', 'GB'];
|
|
96
|
+
let n = bytes / 1024;
|
|
97
|
+
let u = 0;
|
|
98
|
+
while (n >= 1024 && u < units.length - 1) {
|
|
99
|
+
n /= 1024;
|
|
100
|
+
u++;
|
|
101
|
+
}
|
|
102
|
+
return `${n < 10 ? n.toFixed(1).replace(/\.0$/, '') : Math.round(n)} ${units[u]}`;
|
|
103
|
+
}
|