@windowkit/agent-chat 0.3.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.
Files changed (110) hide show
  1. package/dist/core/driver.d.ts +13 -0
  2. package/dist/core/driver.d.ts.map +1 -1
  3. package/dist/core/driver.js.map +1 -1
  4. package/dist/core/markdown.js +29 -0
  5. package/dist/core/markdown.js.map +1 -1
  6. package/dist/core/reducer.d.ts.map +1 -1
  7. package/dist/core/reducer.js +16 -0
  8. package/dist/core/reducer.js.map +1 -1
  9. package/dist/core/session.d.ts +3 -0
  10. package/dist/core/session.d.ts.map +1 -1
  11. package/dist/core/session.js +17 -1
  12. package/dist/core/session.js.map +1 -1
  13. package/dist/core/types.d.ts +43 -1
  14. package/dist/core/types.d.ts.map +1 -1
  15. package/dist/core/types.js.map +1 -1
  16. package/dist/drivers/claude/index.d.ts.map +1 -1
  17. package/dist/drivers/claude/index.js +56 -8
  18. package/dist/drivers/claude/index.js.map +1 -1
  19. package/dist/drivers/claude/results.d.ts +40 -0
  20. package/dist/drivers/claude/results.d.ts.map +1 -0
  21. package/dist/drivers/claude/results.js +235 -0
  22. package/dist/drivers/claude/results.js.map +1 -0
  23. package/dist/drivers/claude/tools.d.ts +3 -1
  24. package/dist/drivers/claude/tools.d.ts.map +1 -1
  25. package/dist/drivers/claude/tools.js +7 -1
  26. package/dist/drivers/claude/tools.js.map +1 -1
  27. package/dist/view/AgentChat.d.ts.map +1 -1
  28. package/dist/view/AgentChat.js +14 -3
  29. package/dist/view/AgentChat.js.map +1 -1
  30. package/dist/view/ArtifactCard.d.ts +6 -0
  31. package/dist/view/ArtifactCard.d.ts.map +1 -0
  32. package/dist/view/ArtifactCard.js +6 -0
  33. package/dist/view/ArtifactCard.js.map +1 -0
  34. package/dist/view/AttachmentCard.d.ts +8 -0
  35. package/dist/view/AttachmentCard.d.ts.map +1 -0
  36. package/dist/view/AttachmentCard.js +55 -0
  37. package/dist/view/AttachmentCard.js.map +1 -0
  38. package/dist/view/Composer.d.ts.map +1 -1
  39. package/dist/view/Composer.js +9 -1
  40. package/dist/view/Composer.js.map +1 -1
  41. package/dist/view/DataPartView.d.ts +6 -0
  42. package/dist/view/DataPartView.d.ts.map +1 -0
  43. package/dist/view/DataPartView.js +23 -0
  44. package/dist/view/DataPartView.js.map +1 -0
  45. package/dist/view/ImageView.d.ts +18 -0
  46. package/dist/view/ImageView.d.ts.map +1 -0
  47. package/dist/view/ImageView.js +35 -0
  48. package/dist/view/ImageView.js.map +1 -0
  49. package/dist/view/LinkCard.d.ts +14 -0
  50. package/dist/view/LinkCard.d.ts.map +1 -0
  51. package/dist/view/LinkCard.js +40 -0
  52. package/dist/view/LinkCard.js.map +1 -0
  53. package/dist/view/MessageList.d.ts.map +1 -1
  54. package/dist/view/MessageList.js +10 -10
  55. package/dist/view/MessageList.js.map +1 -1
  56. package/dist/view/PlanChecklist.d.ts +6 -0
  57. package/dist/view/PlanChecklist.d.ts.map +1 -0
  58. package/dist/view/PlanChecklist.js +16 -0
  59. package/dist/view/PlanChecklist.js.map +1 -0
  60. package/dist/view/PullRequestCard.d.ts +6 -0
  61. package/dist/view/PullRequestCard.d.ts.map +1 -0
  62. package/dist/view/PullRequestCard.js +12 -0
  63. package/dist/view/PullRequestCard.js.map +1 -0
  64. package/dist/view/SessionCard.d.ts +6 -0
  65. package/dist/view/SessionCard.d.ts.map +1 -0
  66. package/dist/view/SessionCard.js +6 -0
  67. package/dist/view/SessionCard.js.map +1 -0
  68. package/dist/view/SuggestedReplies.d.ts +5 -0
  69. package/dist/view/SuggestedReplies.d.ts.map +1 -0
  70. package/dist/view/SuggestedReplies.js +10 -0
  71. package/dist/view/SuggestedReplies.js.map +1 -0
  72. package/dist/view/ToolCard.d.ts.map +1 -1
  73. package/dist/view/ToolCard.js +4 -1
  74. package/dist/view/ToolCard.js.map +1 -1
  75. package/dist/view/Transcript.d.ts.map +1 -1
  76. package/dist/view/Transcript.js +8 -1
  77. package/dist/view/Transcript.js.map +1 -1
  78. package/dist/view/context.d.ts +9 -0
  79. package/dist/view/context.d.ts.map +1 -1
  80. package/dist/view/context.js +9 -0
  81. package/dist/view/context.js.map +1 -1
  82. package/dist/view/look.d.ts +14 -0
  83. package/dist/view/look.d.ts.map +1 -1
  84. package/dist/view/look.js +7 -0
  85. package/dist/view/look.js.map +1 -1
  86. package/package.json +1 -1
  87. package/src/core/driver.ts +4 -0
  88. package/src/core/markdown.ts +34 -0
  89. package/src/core/reducer.ts +25 -0
  90. package/src/core/session.ts +17 -1
  91. package/src/core/types.ts +51 -1
  92. package/src/drivers/claude/index.ts +62 -7
  93. package/src/drivers/claude/results.ts +252 -0
  94. package/src/drivers/claude/tools.ts +7 -1
  95. package/src/view/AgentChat.tsx +20 -2
  96. package/src/view/ArtifactCard.tsx +18 -0
  97. package/src/view/AttachmentCard.tsx +103 -0
  98. package/src/view/Composer.tsx +10 -1
  99. package/src/view/DataPartView.tsx +40 -0
  100. package/src/view/ImageView.tsx +64 -0
  101. package/src/view/LinkCard.tsx +99 -0
  102. package/src/view/MessageList.tsx +21 -39
  103. package/src/view/PlanChecklist.tsx +49 -0
  104. package/src/view/PullRequestCard.tsx +28 -0
  105. package/src/view/SessionCard.tsx +22 -0
  106. package/src/view/SuggestedReplies.tsx +34 -0
  107. package/src/view/ToolCard.tsx +10 -1
  108. package/src/view/Transcript.tsx +9 -0
  109. package/src/view/context.ts +19 -0
  110. package/src/view/look.ts +21 -0
@@ -6,8 +6,11 @@ import { memo, useState, type ReactElement } from 'react';
6
6
  import { Button, Icon, useClipboard } from 'react-x11';
7
7
 
8
8
  import { messageMarkdown, sourceLabel } from '../core/markdown.js';
9
- import type { AgentMessage, Part, PlanPart } from '../core/types.js';
9
+ import type { AgentMessage, FilePart, Part } from '../core/types.js';
10
+ import { DataPartView } from './DataPartView.js';
11
+ import { ImageView } from './ImageView.js';
10
12
  import { useLook, type ChatLook } from './look.js';
13
+ import { PlanChecklist } from './PlanChecklist.js';
11
14
  import { QuestionCard } from './QuestionCard.js';
12
15
  import { TextBlock } from './TextBlock.js';
13
16
  import { ToolCard } from './ToolCard.js';
@@ -89,6 +92,14 @@ function UserMessage({
89
92
  streaming={false}
90
93
  message={message}
91
94
  />
95
+ ) : p.type === 'file' && isImage(p) ? (
96
+ // a pasted screenshot is the picture, not its token
97
+ <ImageView
98
+ key={p.id}
99
+ source={{ data: p.data! }}
100
+ maxHeight={look.thumbHeight}
101
+ alt={p.label}
102
+ />
92
103
  ) : p.type === 'file' && !textOf(message).includes(`[${p.label}]`) ? (
93
104
  // an attachment the composer made is already in the text as its
94
105
  // token; one sent some other way gets a line of its own
@@ -157,7 +168,7 @@ function PartView({
157
168
  case 'question':
158
169
  return <QuestionCard part={part} />;
159
170
  case 'plan':
160
- return <Plan part={part} look={look} />;
171
+ return <PlanChecklist part={part} />;
161
172
  case 'notice':
162
173
  return (
163
174
  <text
@@ -175,13 +186,15 @@ function PartView({
175
186
  </text>
176
187
  );
177
188
  case 'file':
178
- return (
189
+ return isImage(part) ? (
190
+ <ImageView source={{ data: part.data! }} alt={part.label} />
191
+ ) : (
179
192
  <text
180
193
  style={{ color: look.muted, fontSize: 11 }}
181
194
  >{`[${part.label}]`}</text>
182
195
  );
183
196
  case 'data':
184
- return null;
197
+ return <DataPartView part={part} />;
185
198
  }
186
199
  }
187
200
 
@@ -232,41 +245,6 @@ function Reasoning({
232
245
  );
233
246
  }
234
247
 
235
- function Plan({
236
- part,
237
- look,
238
- }: {
239
- part: PlanPart;
240
- look: ChatLook;
241
- }): ReactElement {
242
- return (
243
- <box style={{ flexDirection: 'column', gap: 2 }}>
244
- {part.entries.map((e, i) => (
245
- <box
246
- key={i}
247
- style={{ flexDirection: 'row', gap: 6, alignItems: 'center' }}
248
- >
249
- {e.status === 'completed' ? (
250
- <Icon name="check" size={12} color={look.success} />
251
- ) : e.status === 'in_progress' ? (
252
- <Icon name="dot" size={12} color={look.accent} />
253
- ) : (
254
- <Icon name="dash" size={12} color={look.muted} />
255
- )}
256
- <text
257
- style={{
258
- color: e.status === 'completed' ? look.muted : look.text,
259
- fontWeight: e.status === 'in_progress' ? 'bold' : 'normal',
260
- }}
261
- >
262
- {e.text}
263
- </text>
264
- </box>
265
- ))}
266
- </box>
267
- );
268
- }
269
-
270
248
  function CopyButton({ message }: { message: AgentMessage }): ReactElement {
271
249
  const clipboard = useClipboard();
272
250
  const [copied, setCopied] = useState(false);
@@ -285,6 +263,10 @@ function CopyButton({ message }: { message: AgentMessage }): ReactElement {
285
263
  );
286
264
  }
287
265
 
266
+ function isImage(p: FilePart): boolean {
267
+ return p.mime.startsWith('image/') && !!p.data;
268
+ }
269
+
288
270
  function textOf(message: AgentMessage): string {
289
271
  return message.parts.map((p) => (p.type === 'text' ? p.text : '')).join('');
290
272
  }
@@ -0,0 +1,49 @@
1
+ // The agent's plan as a checklist: Claude Code's task list (or a
2
+ // `TodoWrite`), kept current as it works — what is done in the muted
3
+ // colour, what is under way in bold, and how far along it is.
4
+
5
+ import type { ReactElement } from 'react';
6
+ import { Icon } from 'react-x11';
7
+
8
+ import type { PlanPart } from '../core/types.js';
9
+ import { useLook } from './look.js';
10
+
11
+ export function PlanChecklist({ part }: { part: PlanPart }): ReactElement {
12
+ const look = useLook();
13
+ const done = part.entries.filter((e) => e.status === 'completed').length;
14
+ return (
15
+ <box style={{ flexDirection: 'column', gap: 2 }} aria-label="Plan">
16
+ <text
17
+ style={{
18
+ color: look.muted,
19
+ fontSize: look.smallSize,
20
+ fontWeight: 'bold',
21
+ }}
22
+ >
23
+ {`PLAN · ${done} OF ${part.entries.length} DONE`}
24
+ </text>
25
+ {part.entries.map((e, i) => (
26
+ <box
27
+ key={i}
28
+ style={{ flexDirection: 'row', gap: look.gap, alignItems: 'center' }}
29
+ >
30
+ {e.status === 'completed' ? (
31
+ <Icon name="check" size={look.captionSize} color={look.success} />
32
+ ) : e.status === 'in_progress' ? (
33
+ <Icon name="dot" size={look.captionSize} color={look.accent} />
34
+ ) : (
35
+ <Icon name="dash" size={look.captionSize} color={look.muted} />
36
+ )}
37
+ <text
38
+ style={{
39
+ color: e.status === 'completed' ? look.muted : look.text,
40
+ fontWeight: e.status === 'in_progress' ? 'bold' : 'normal',
41
+ }}
42
+ >
43
+ {e.text}
44
+ </text>
45
+ </box>
46
+ ))}
47
+ </box>
48
+ );
49
+ }
@@ -0,0 +1,28 @@
1
+ // A pull request a command opened, edited or merged: Bash's
2
+ // `gitOperation.pr`.
3
+
4
+ import type { ReactElement } from 'react';
5
+
6
+ import type { PullRequestData } from '../core/types.js';
7
+ import { LinkCard, shortUrl } from './LinkCard.js';
8
+
9
+ export function PullRequestCard({
10
+ value,
11
+ }: {
12
+ value: PullRequestData;
13
+ }): ReactElement {
14
+ const action = value.action.replace(/-/g, ' ');
15
+ return (
16
+ <LinkCard
17
+ kind="Pull request"
18
+ title={`#${value.number}`}
19
+ detail={[
20
+ action.charAt(0).toUpperCase() + action.slice(1),
21
+ value.url ? shortUrl(value.url) : '',
22
+ ]
23
+ .filter(Boolean)
24
+ .join(' · ')}
25
+ {...(value.url ? { href: value.url } : {})}
26
+ />
27
+ );
28
+ }
@@ -0,0 +1,22 @@
1
+ // An agent session started somewhere else — a cloud session the Agent tool
2
+ // launched, a workflow — which the user can open and follow.
3
+
4
+ import type { ReactElement } from 'react';
5
+
6
+ import type { SessionLinkData } from '../core/types.js';
7
+ import { LinkCard, shortUrl } from './LinkCard.js';
8
+
9
+ export function SessionCard({
10
+ value,
11
+ }: {
12
+ value: SessionLinkData;
13
+ }): ReactElement {
14
+ return (
15
+ <LinkCard
16
+ kind="Session started"
17
+ title={value.title ?? 'A new session'}
18
+ detail={shortUrl(value.url)}
19
+ href={value.url}
20
+ />
21
+ );
22
+ }
@@ -0,0 +1,34 @@
1
+ // What the harness suggests the user might say next, as chips under the
2
+ // last reply. Pressing one puts it in the composer to edit or send; it never
3
+ // sends by itself.
4
+
5
+ import type { ReactElement } from 'react';
6
+ import { Button } from 'react-x11';
7
+
8
+ import { useChat } from './context.js';
9
+ import { useLook } from './look.js';
10
+
11
+ export function SuggestedReplies({
12
+ suggestions,
13
+ }: {
14
+ suggestions: readonly string[];
15
+ }): ReactElement {
16
+ const { composer } = useChat();
17
+ const look = useLook();
18
+ return (
19
+ <box
20
+ style={{ flexDirection: 'row', flexWrap: 'wrap', gap: look.gap }}
21
+ aria-label="Suggested replies"
22
+ >
23
+ {suggestions.map((s) => (
24
+ <Button
25
+ key={s}
26
+ size="small"
27
+ variant="outline"
28
+ label={s}
29
+ onPress={() => composer.fill(s)}
30
+ />
31
+ ))}
32
+ </box>
33
+ );
34
+ }
@@ -12,6 +12,7 @@ import { TerminalOutput } from '@react-x11/components/terminal-output';
12
12
 
13
13
  import type { ToolOutput, ToolPart } from '../core/types.js';
14
14
  import { useChat } from './context.js';
15
+ import { ImageView } from './ImageView.js';
15
16
  import { clipLines, useLook, type ChatLook } from './look.js';
16
17
  import { MessageList } from './MessageList.js';
17
18
 
@@ -235,7 +236,15 @@ function Output({
235
236
  case 'diff':
236
237
  return <Diff output={output} />;
237
238
  case 'image':
238
- return <text style={{ color: look.muted }}>[image, {output.mime}]</text>;
239
+ return (
240
+ <ImageView
241
+ source={{ data: output.data }}
242
+ {...(output.width && output.height
243
+ ? { width: output.width, height: output.height }
244
+ : {})}
245
+ alt={`An image, ${output.mime}`}
246
+ />
247
+ );
239
248
  case 'value':
240
249
  return (
241
250
  <Code source={clipLines(json(output.value), 80)} lang="json" wrap />
@@ -15,6 +15,7 @@ import type { ScrollableNode, ScrollEvent } from 'react-x11';
15
15
  import { useChat } from './context.js';
16
16
  import { useLook } from './look.js';
17
17
  import { MessageList } from './MessageList.js';
18
+ import { SuggestedReplies } from './SuggestedReplies.js';
18
19
  import { useSessionSnapshot } from './use-agent.js';
19
20
 
20
21
  /** Within this many pixels of the end counts as at the end. */
@@ -45,6 +46,13 @@ export function Transcript({ empty }: { empty?: ReactNode }): ReactElement {
45
46
  };
46
47
 
47
48
  const activity = activityOf(snap.status, snap.messages);
49
+ // offered once the turn is over, under the reply they follow
50
+ const suggestions =
51
+ snap.status === 'idle' &&
52
+ snap.suggestions.length &&
53
+ snap.messages[snap.messages.length - 1]?.role === 'assistant'
54
+ ? snap.suggestions
55
+ : null;
48
56
  return (
49
57
  <box
50
58
  ref={pane}
@@ -59,6 +67,7 @@ export function Transcript({ empty }: { empty?: ReactNode }): ReactElement {
59
67
  ) : (
60
68
  (empty ?? null)
61
69
  )}
70
+ {suggestions ? <SuggestedReplies suggestions={suggestions} /> : null}
62
71
  {activity ? (
63
72
  <text style={{ color: look.muted, fontSize: 12 }}>{activity}</text>
64
73
  ) : null}
@@ -33,6 +33,25 @@ export interface ChatContextValue {
33
33
  onLink: ((href: string) => void) | undefined;
34
34
  /** Base text size, the theme's unless the app said. */
35
35
  fontSize: number | undefined;
36
+ /** The message box, for a part that puts text in it without sending. */
37
+ composer: ComposerChannel;
38
+ }
39
+
40
+ /** How a part reaches the composer: it attaches while mounted. */
41
+ export interface ComposerChannel {
42
+ /** Replace what is in the box, and focus it. Sends nothing. */
43
+ fill(text: string): void;
44
+ attach(fill: ((text: string) => void) | null): void;
45
+ }
46
+
47
+ export function composerChannel(): ComposerChannel {
48
+ let current: ((text: string) => void) | null = null;
49
+ return {
50
+ fill: (text) => current?.(text),
51
+ attach: (fill) => {
52
+ current = fill;
53
+ },
54
+ };
36
55
  }
37
56
 
38
57
  export const ChatContext = createContext<ChatContextValue | null>(null);
package/src/view/look.ts CHANGED
@@ -22,6 +22,20 @@ export interface ChatLook {
22
22
  mono: string;
23
23
  /** The user's bubble: the text colour, faintly, so it shows on any ground. */
24
24
  bubble: string;
25
+ /** Labels, states and captions. */
26
+ smallSize: number;
27
+ /** A secondary line: a thought, a notice, a card's detail. */
28
+ captionSize: number;
29
+ /** A card's corners. */
30
+ radius: number;
31
+ /** Inside a card, from its edge. */
32
+ inset: number;
33
+ /** Between a card's pieces. */
34
+ gap: number;
35
+ /** The tallest an image in a reply or a tool's output is drawn. */
36
+ imageHeight: number;
37
+ /** The tallest an image in the user's own message is drawn. */
38
+ thumbHeight: number;
25
39
  }
26
40
 
27
41
  export function useLook(): ChatLook {
@@ -40,6 +54,13 @@ export function useLook(): ChatLook {
40
54
  warning: String(theme.warning ?? '#e67e22'),
41
55
  mono: String(theme.monoFamily ?? 'monospace'),
42
56
  bubble: tint(text, 0.07),
57
+ smallSize: 11,
58
+ captionSize: 12,
59
+ radius: 6,
60
+ inset: 8,
61
+ gap: 6,
62
+ imageHeight: 320,
63
+ thumbHeight: 160,
43
64
  };
44
65
  }, [theme]);
45
66
  }