@windowkit/agent-chat 0.0.0-stage → 0.1.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 (164) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -2
  3. package/dist/core/driver.d.ts +183 -0
  4. package/dist/core/driver.d.ts.map +1 -0
  5. package/dist/core/driver.js +12 -0
  6. package/dist/core/driver.js.map +1 -0
  7. package/dist/core/index.d.ts +8 -0
  8. package/dist/core/index.d.ts.map +1 -0
  9. package/dist/core/index.js +11 -0
  10. package/dist/core/index.js.map +1 -0
  11. package/dist/core/markdown.d.ts +7 -0
  12. package/dist/core/markdown.d.ts.map +1 -0
  13. package/dist/core/markdown.js +53 -0
  14. package/dist/core/markdown.js.map +1 -0
  15. package/dist/core/policy.d.ts +18 -0
  16. package/dist/core/policy.d.ts.map +1 -0
  17. package/dist/core/policy.js +67 -0
  18. package/dist/core/policy.js.map +1 -0
  19. package/dist/core/reducer.d.ts +61 -0
  20. package/dist/core/reducer.d.ts.map +1 -0
  21. package/dist/core/reducer.js +391 -0
  22. package/dist/core/reducer.js.map +1 -0
  23. package/dist/core/session.d.ts +105 -0
  24. package/dist/core/session.d.ts.map +1 -0
  25. package/dist/core/session.js +648 -0
  26. package/dist/core/session.js.map +1 -0
  27. package/dist/core/tools.d.ts +120 -0
  28. package/dist/core/tools.d.ts.map +1 -0
  29. package/dist/core/tools.js +120 -0
  30. package/dist/core/tools.js.map +1 -0
  31. package/dist/core/types.d.ts +295 -0
  32. package/dist/core/types.d.ts.map +1 -0
  33. package/dist/core/types.js +21 -0
  34. package/dist/core/types.js.map +1 -0
  35. package/dist/drivers/claude/index.d.ts +20 -0
  36. package/dist/drivers/claude/index.d.ts.map +1 -0
  37. package/dist/drivers/claude/index.js +727 -0
  38. package/dist/drivers/claude/index.js.map +1 -0
  39. package/dist/drivers/claude/input.d.ts +7 -0
  40. package/dist/drivers/claude/input.d.ts.map +1 -0
  41. package/dist/drivers/claude/input.js +46 -0
  42. package/dist/drivers/claude/input.js.map +1 -0
  43. package/dist/drivers/claude/peers.d.ts +16 -0
  44. package/dist/drivers/claude/peers.d.ts.map +1 -0
  45. package/dist/drivers/claude/peers.js +137 -0
  46. package/dist/drivers/claude/peers.js.map +1 -0
  47. package/dist/drivers/claude/schema.d.ts +5 -0
  48. package/dist/drivers/claude/schema.d.ts.map +1 -0
  49. package/dist/drivers/claude/schema.js +77 -0
  50. package/dist/drivers/claude/schema.js.map +1 -0
  51. package/dist/drivers/claude/sdk.d.ts +86 -0
  52. package/dist/drivers/claude/sdk.d.ts.map +1 -0
  53. package/dist/drivers/claude/sdk.js +47 -0
  54. package/dist/drivers/claude/sdk.js.map +1 -0
  55. package/dist/drivers/claude/tools.d.ts +18 -0
  56. package/dist/drivers/claude/tools.d.ts.map +1 -0
  57. package/dist/drivers/claude/tools.js +119 -0
  58. package/dist/drivers/claude/tools.js.map +1 -0
  59. package/dist/index.d.ts +8 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +12 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/testing/fake.d.ts +63 -0
  64. package/dist/testing/fake.d.ts.map +1 -0
  65. package/dist/testing/fake.js +270 -0
  66. package/dist/testing/fake.js.map +1 -0
  67. package/dist/testing/index.d.ts +2 -0
  68. package/dist/testing/index.d.ts.map +1 -0
  69. package/dist/testing/index.js +4 -0
  70. package/dist/testing/index.js.map +1 -0
  71. package/dist/view/AgentChat.d.ts +53 -0
  72. package/dist/view/AgentChat.d.ts.map +1 -0
  73. package/dist/view/AgentChat.js +87 -0
  74. package/dist/view/AgentChat.js.map +1 -0
  75. package/dist/view/CodeBlock.d.ts +9 -0
  76. package/dist/view/CodeBlock.d.ts.map +1 -0
  77. package/dist/view/CodeBlock.js +60 -0
  78. package/dist/view/CodeBlock.js.map +1 -0
  79. package/dist/view/Composer.d.ts +24 -0
  80. package/dist/view/Composer.d.ts.map +1 -0
  81. package/dist/view/Composer.js +144 -0
  82. package/dist/view/Composer.js.map +1 -0
  83. package/dist/view/Header.d.ts +3 -0
  84. package/dist/view/Header.d.ts.map +1 -0
  85. package/dist/view/Header.js +30 -0
  86. package/dist/view/Header.js.map +1 -0
  87. package/dist/view/MessageList.d.ts +9 -0
  88. package/dist/view/MessageList.d.ts.map +1 -0
  89. package/dist/view/MessageList.js +111 -0
  90. package/dist/view/MessageList.js.map +1 -0
  91. package/dist/view/QuestionCard.d.ts +6 -0
  92. package/dist/view/QuestionCard.d.ts.map +1 -0
  93. package/dist/view/QuestionCard.js +39 -0
  94. package/dist/view/QuestionCard.js.map +1 -0
  95. package/dist/view/TextBlock.d.ts +9 -0
  96. package/dist/view/TextBlock.d.ts.map +1 -0
  97. package/dist/view/TextBlock.js +22 -0
  98. package/dist/view/TextBlock.js.map +1 -0
  99. package/dist/view/ToolCard.d.ts +7 -0
  100. package/dist/view/ToolCard.d.ts.map +1 -0
  101. package/dist/view/ToolCard.js +155 -0
  102. package/dist/view/ToolCard.js.map +1 -0
  103. package/dist/view/Transcript.d.ts +5 -0
  104. package/dist/view/Transcript.d.ts.map +1 -0
  105. package/dist/view/Transcript.js +47 -0
  106. package/dist/view/Transcript.js.map +1 -0
  107. package/dist/view/context.d.ts +21 -0
  108. package/dist/view/context.d.ts.map +1 -0
  109. package/dist/view/context.js +37 -0
  110. package/dist/view/context.js.map +1 -0
  111. package/dist/view/fences.d.ts +13 -0
  112. package/dist/view/fences.d.ts.map +1 -0
  113. package/dist/view/fences.js +59 -0
  114. package/dist/view/fences.js.map +1 -0
  115. package/dist/view/files.d.ts +11 -0
  116. package/dist/view/files.d.ts.map +1 -0
  117. package/dist/view/files.js +109 -0
  118. package/dist/view/files.js.map +1 -0
  119. package/dist/view/look.d.ts +18 -0
  120. package/dist/view/look.d.ts.map +1 -0
  121. package/dist/view/look.js +37 -0
  122. package/dist/view/look.js.map +1 -0
  123. package/dist/view/types.d.ts +50 -0
  124. package/dist/view/types.d.ts.map +1 -0
  125. package/dist/view/types.js +14 -0
  126. package/dist/view/types.js.map +1 -0
  127. package/dist/view/use-agent.d.ts +11 -0
  128. package/dist/view/use-agent.d.ts.map +1 -0
  129. package/dist/view/use-agent.js +33 -0
  130. package/dist/view/use-agent.js.map +1 -0
  131. package/package.json +92 -4
  132. package/src/core/driver.ts +191 -0
  133. package/src/core/index.ts +54 -0
  134. package/src/core/markdown.ts +56 -0
  135. package/src/core/policy.ts +77 -0
  136. package/src/core/reducer.ts +490 -0
  137. package/src/core/session.ts +824 -0
  138. package/src/core/tools.ts +233 -0
  139. package/src/core/types.ts +321 -0
  140. package/src/drivers/claude/index.ts +825 -0
  141. package/src/drivers/claude/input.ts +47 -0
  142. package/src/drivers/claude/peers.ts +153 -0
  143. package/src/drivers/claude/schema.ts +99 -0
  144. package/src/drivers/claude/sdk.ts +138 -0
  145. package/src/drivers/claude/tools.ts +141 -0
  146. package/src/index.ts +51 -0
  147. package/src/testing/fake.ts +374 -0
  148. package/src/testing/index.ts +11 -0
  149. package/src/view/AgentChat.tsx +204 -0
  150. package/src/view/CodeBlock.tsx +108 -0
  151. package/src/view/Composer.tsx +230 -0
  152. package/src/view/Header.tsx +73 -0
  153. package/src/view/MessageList.tsx +290 -0
  154. package/src/view/QuestionCard.tsx +107 -0
  155. package/src/view/TextBlock.tsx +58 -0
  156. package/src/view/ToolCard.tsx +283 -0
  157. package/src/view/Transcript.tsx +81 -0
  158. package/src/view/context.ts +70 -0
  159. package/src/view/dom-globals.d.ts +10 -0
  160. package/src/view/fences.ts +73 -0
  161. package/src/view/files.ts +111 -0
  162. package/src/view/look.ts +53 -0
  163. package/src/view/types.ts +71 -0
  164. package/src/view/use-agent.ts +40 -0
@@ -0,0 +1,37 @@
1
+ // The palette the chat draws with, read from the theme in force, so the chat
2
+ // takes the app's look and the desktop's light or dark scheme.
3
+ import { useMemo } from 'react';
4
+ import { useTheme } from 'react-x11';
5
+ import { tint } from 'react-x11/style';
6
+ export function useLook() {
7
+ const theme = useTheme();
8
+ return useMemo(() => {
9
+ const text = String(theme.text ?? '#2d3436');
10
+ return {
11
+ text,
12
+ muted: String(theme.textMuted ?? '#7f8c8d'),
13
+ border: String(theme.border ?? '#b2bec3'),
14
+ surface: String(theme.surface ?? '#f1f2f6'),
15
+ background: String(theme.background ?? 'white'),
16
+ accent: String(theme.accent ?? '#2980b9'),
17
+ danger: String(theme.danger ?? '#c0392b'),
18
+ success: String(theme.success ?? '#27ae60'),
19
+ warning: String(theme.warning ?? '#e67e22'),
20
+ mono: String(theme.monoFamily ?? 'monospace'),
21
+ bubble: tint(text, 0.07),
22
+ };
23
+ }, [theme]);
24
+ }
25
+ /** At most `max` lines: the first and last halves, and how many were cut. */
26
+ export function clipLines(text, max = 200) {
27
+ const lines = text.split('\n');
28
+ if (lines.length <= max)
29
+ return text;
30
+ const half = Math.floor(max / 2);
31
+ return [
32
+ ...lines.slice(0, half),
33
+ `… ${lines.length - max} more lines …`,
34
+ ...lines.slice(-half),
35
+ ].join('\n');
36
+ }
37
+ //# sourceMappingURL=look.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"look.js","sourceRoot":"","sources":["../../src/view/look.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,+DAA+D;AAE/D,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAiBvC,MAAM,UAAU,OAAO;IACrB,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,OAAO,OAAO,CAAC,GAAG,EAAE;QAClB,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,IAAI,SAAS,CAAC,CAAC;QAC7C,OAAO;YACL,IAAI;YACJ,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,IAAI,SAAS,CAAC;YAC3C,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,IAAI,SAAS,CAAC;YACzC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,SAAS,CAAC;YAC3C,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,UAAU,IAAI,OAAO,CAAC;YAC/C,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,IAAI,SAAS,CAAC;YACzC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,IAAI,SAAS,CAAC;YACzC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,SAAS,CAAC;YAC3C,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,SAAS,CAAC;YAC3C,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,UAAU,IAAI,WAAW,CAAC;YAC7C,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC;SACzB,CAAC;IACJ,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;AACd,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,GAAG,GAAG,GAAG;IAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IACjC,OAAO;QACL,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC;QACvB,KAAK,KAAK,CAAC,MAAM,GAAG,GAAG,eAAe;QACtC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC;KACtB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
@@ -0,0 +1,50 @@
1
+ import type { ComponentType, ReactNode } from 'react';
2
+ import type { IconName } from 'react-x11';
3
+ import type { FenceInfo } from '@react-x11/components/markdown';
4
+ import type { AgentSession } from '../core/session.js';
5
+ import type { ToolDef, ToolParameters, InferInput, ToolRenderInfo } from '../core/tools.js';
6
+ import type { AgentMessage } from '../core/types.js';
7
+ export type { FenceInfo };
8
+ /**
9
+ * How a fenced block with this language is drawn: a function, or the
10
+ * function and a line that tells the agent the format exists.
11
+ */
12
+ export type FenceEntry = ((fence: FenceInfo) => ReactNode) | {
13
+ render: (fence: FenceInfo) => ReactNode;
14
+ describe?: string;
15
+ };
16
+ /**
17
+ * A component a reply may name as a tag on its own line: `<Ticket id="12"/>`.
18
+ * A tag is a component iff its name is a key here; nothing is evaluated.
19
+ */
20
+ export type ComponentEntry = ComponentType<Record<string, unknown>> | {
21
+ component: ComponentType<Record<string, unknown>>;
22
+ describe?: string;
23
+ };
24
+ /** What a code action is handed besides the fence. */
25
+ export interface CodeActionContext {
26
+ session: AgentSession;
27
+ message: AgentMessage;
28
+ /** Put text on the clipboard. */
29
+ copy(text: string): Promise<void>;
30
+ }
31
+ /** A button on a fenced code block's header. */
32
+ export interface CodeAction {
33
+ id: string;
34
+ label: string;
35
+ icon?: IconName;
36
+ /** Shown for a moment after `run` resolves: `'Copied'`. */
37
+ doneLabel?: string;
38
+ /** Default: every block. */
39
+ when?: (fence: FenceInfo) => boolean;
40
+ run(fence: FenceInfo, ctx: CodeActionContext): void | Promise<void>;
41
+ }
42
+ /** The built-in Copy. The default `codeActions` is `[copyCode]`. */
43
+ export declare const copyCode: CodeAction;
44
+ /** Draws a tool call: the part, its parsed input, and what `run` returned. */
45
+ export type ToolRenderer = (info: ToolRenderInfo) => ReactNode;
46
+ /** `defineTool`, with `render` typed for this view. */
47
+ export declare function defineTool<P extends ToolParameters, O>(def: ToolDef<InferInput<P>, O, ReactNode> & {
48
+ parameters: P;
49
+ }): ToolDef<InferInput<P>, O, ReactNode>;
50
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/view/types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EACV,OAAO,EACP,cAAc,EACd,UAAU,EACV,cAAc,EACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B;;;GAGG;AACH,MAAM,MAAM,UAAU,GAClB,CAAC,CAAC,KAAK,EAAE,SAAS,KAAK,SAAS,CAAC,GACjC;IAAE,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,SAAS,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACtC;IAAE,SAAS,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7E,sDAAsD;AACtD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,YAAY,CAAC;IACtB,iCAAiC;IACjC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAED,gDAAgD;AAChD,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC;IACrC,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,iBAAiB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrE;AAED,oEAAoE;AACpE,eAAO,MAAM,QAAQ,EAAE,UAKrB,CAAC;AAEH,8EAA8E;AAC9E,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,cAAc,KAAK,SAAS,CAAC;AAE/D,uDAAuD;AACvD,wBAAgB,UAAU,CAAC,CAAC,SAAS,cAAc,EAAE,CAAC,EACpD,GAAG,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,GAAG;IAAE,UAAU,EAAE,CAAC,CAAA;CAAE,GAC5D,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,CAEtC"}
@@ -0,0 +1,14 @@
1
+ // The view's own props: renderers and actions. Nothing here reaches the
2
+ // session, but `describe` does reach the agent, as instructions.
3
+ /** The built-in Copy. The default `codeActions` is `[copyCode]`. */
4
+ export const copyCode = Object.freeze({
5
+ id: 'copy',
6
+ label: 'Copy',
7
+ doneLabel: 'Copied',
8
+ run: (fence, ctx) => ctx.copy(fence.text),
9
+ });
10
+ /** `defineTool`, with `render` typed for this view. */
11
+ export function defineTool(def) {
12
+ return def;
13
+ }
14
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/view/types.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,iEAAiE;AAqDjE,oEAAoE;AACpE,MAAM,CAAC,MAAM,QAAQ,GAAe,MAAM,CAAC,MAAM,CAAC;IAChD,EAAE,EAAE,MAAM;IACV,KAAK,EAAE,MAAM;IACb,SAAS,EAAE,QAAQ;IACnB,GAAG,EAAE,CAAC,KAAgB,EAAE,GAAsB,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;CACxE,CAAC,CAAC;AAKH,uDAAuD;AACvD,MAAM,UAAU,UAAU,CACxB,GAA6D;IAE7D,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { type AgentOptions, type AgentSession, type SessionSnapshot } from '../core/session.js';
2
+ /**
3
+ * A session for this component's life: made at the first render from
4
+ * `options`, which are read once (give the component a new `key` to start
5
+ * over), and closed when the component unmounts. The component re-renders
6
+ * whenever the session changes.
7
+ */
8
+ export declare function useAgent(options: AgentOptions): AgentSession;
9
+ /** The session's current snapshot, re-rendering as it changes. */
10
+ export declare function useSessionSnapshot(session: AgentSession): SessionSnapshot;
11
+ //# sourceMappingURL=use-agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-agent.d.ts","sourceRoot":"","sources":["../../src/view/use-agent.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AAE5B;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,YAAY,CAiB5D;AAED,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,eAAe,CAEzE"}
@@ -0,0 +1,33 @@
1
+ // The session as React state.
2
+ import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
3
+ import { createAgentSession, } from '../core/session.js';
4
+ /**
5
+ * A session for this component's life: made at the first render from
6
+ * `options`, which are read once (give the component a new `key` to start
7
+ * over), and closed when the component unmounts. The component re-renders
8
+ * whenever the session changes.
9
+ */
10
+ export function useAgent(options) {
11
+ const [session] = useState(() => createAgentSession(options));
12
+ useSessionSnapshot(session);
13
+ // Closed a turn after the unmount, not at it: StrictMode's development
14
+ // double mount unmounts and mounts again at once, and the session should
15
+ // survive that.
16
+ const mounted = useRef(0);
17
+ useEffect(() => {
18
+ mounted.current++;
19
+ return () => {
20
+ const at = --mounted.current;
21
+ setTimeout(() => {
22
+ if (mounted.current === at && at === 0)
23
+ void session.close();
24
+ }, 0);
25
+ };
26
+ }, [session]);
27
+ return session;
28
+ }
29
+ /** The session's current snapshot, re-rendering as it changes. */
30
+ export function useSessionSnapshot(session) {
31
+ return useSyncExternalStore(session.subscribe, () => session.snapshot);
32
+ }
33
+ //# sourceMappingURL=use-agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-agent.js","sourceRoot":"","sources":["../../src/view/use-agent.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAE9B,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE1E,OAAO,EACL,kBAAkB,GAInB,MAAM,oBAAoB,CAAC;AAE5B;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAqB;IAC5C,MAAM,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9D,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC5B,uEAAuE;IACvE,yEAAyE;IACzE,gBAAgB;IAChB,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IAC1B,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,CAAC,OAAO,EAAE,CAAC;QAClB,OAAO,GAAG,EAAE;YACV,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC;YAC7B,UAAU,CAAC,GAAG,EAAE;gBACd,IAAI,OAAO,CAAC,OAAO,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC;oBAAE,KAAK,OAAO,CAAC,KAAK,EAAE,CAAC;YAC/D,CAAC,EAAE,CAAC,CAAC,CAAC;QACR,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACd,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,kBAAkB,CAAC,OAAqB;IACtD,OAAO,oBAAoB,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;AACzE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,94 @@
1
1
  {
2
2
  "name": "@windowkit/agent-chat",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "An embeddable chat UI for local AI agent harnesses, for react-x11: the Claude Agent SDK first, your own tools as functions, your own renderers, and every action interceptable.",
5
+ "keywords": [
6
+ "react-x11",
7
+ "agent",
8
+ "claude",
9
+ "chat",
10
+ "ai",
11
+ "desktop"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/windowkit/agent-chat.git"
17
+ },
18
+ "homepage": "https://github.com/windowkit/agent-chat#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/windowkit/agent-chat/issues"
21
+ },
22
+ "type": "module",
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
26
+ "sideEffects": false,
27
+ "files": [
28
+ "dist",
29
+ "src"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "./core": {
37
+ "types": "./dist/core/index.d.ts",
38
+ "default": "./dist/core/index.js"
39
+ },
40
+ "./drivers/claude": {
41
+ "types": "./dist/drivers/claude/index.d.ts",
42
+ "default": "./dist/drivers/claude/index.js"
43
+ },
44
+ "./testing": {
45
+ "types": "./dist/testing/index.d.ts",
46
+ "default": "./dist/testing/index.js"
47
+ },
48
+ "./package.json": "./package.json"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "scripts": {
54
+ "prebuild": "rm -rf dist",
55
+ "build": "tsc -p tsconfig.build.json",
56
+ "prepack": "npm run build",
57
+ "test": "tsx --test test/*.test.ts test/*.test.tsx",
58
+ "typecheck": "tsc -p tsconfig.json",
59
+ "format": "prettier --write .",
60
+ "format:check": "prettier --check .",
61
+ "check:package": "tsx scripts/check-package.ts",
62
+ "examples:demo": "tsx examples/demo.tsx",
63
+ "examples:claude": "tsx examples/claude.tsx",
64
+ "examples:tools": "tsx examples/tools.tsx",
65
+ "examples:intercept": "tsx examples/intercept.tsx",
66
+ "examples:peers": "tsx examples/peers.tsx",
67
+ "screenshots": "tsx scripts/screenshot.tsx"
68
+ },
69
+ "peerDependencies": {
70
+ "@anthropic-ai/claude-agent-sdk": ">=0.3.300 <0.4.0",
71
+ "@react-x11/components": ">=0.38.0 <0.44.0",
72
+ "react": "^19.0.0",
73
+ "react-x11": "^2.56.3"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "@anthropic-ai/claude-agent-sdk": {
77
+ "optional": true
78
+ }
79
+ },
80
+ "devDependencies": {
81
+ "@anthropic-ai/claude-agent-sdk": "^0.3.319",
82
+ "@react-x11/components": "^0.43.0",
83
+ "@types/node": "^26.2.0",
84
+ "@types/pngjs": "^6.0.5",
85
+ "@types/react": "^19.2.17",
86
+ "pngjs": "^7.0.0",
87
+ "prettier": "^3.6.0",
88
+ "react": "^19.2.8",
89
+ "react-x11": "^2.58.1",
90
+ "tsx": "^4.23.1",
91
+ "typescript": "^7.0.2",
92
+ "zod": "^4.1.0"
93
+ }
94
+ }
@@ -0,0 +1,191 @@
1
+ // The seam between the session and a harness.
2
+ //
3
+ // A driver turns one harness's protocol into `DriverEvent`s, and asks the
4
+ // session — through `DriverHost` — every question the protocol asks the
5
+ // client: may this tool run, what did the user answer, run the app's tool.
6
+ // It is public for the reason `ProcessHost` is in `@react-x11/components`:
7
+ // driving a harness nobody wrapped yet should not need a fork.
8
+
9
+ import type { AnyToolDef, ToolResult } from './tools.js';
10
+ import type {
11
+ Attachment,
12
+ Capabilities,
13
+ CoreToolKey,
14
+ MessageSource,
15
+ PeerSource,
16
+ PermissionPolicy,
17
+ PlanEntry,
18
+ Question,
19
+ SessionInfo,
20
+ ToolCall,
21
+ ToolKind,
22
+ ToolOutput,
23
+ ToolState,
24
+ Usage,
25
+ } from './types.js';
26
+
27
+ /**
28
+ * What a driver reports. Deltas, never snapshots: the reducer accumulates
29
+ * them, so a tool part cannot vanish because a driver forgot to repeat it.
30
+ * `parent` names the tool call a subagent's event belongs under.
31
+ */
32
+ export type DriverEvent =
33
+ /** Assistant text: `delta` appends, `text` replaces (a finished block). */
34
+ | {
35
+ type: 'text';
36
+ id: string;
37
+ delta?: string;
38
+ text?: string;
39
+ done?: boolean;
40
+ reasoning?: boolean;
41
+ parent?: string;
42
+ }
43
+ | {
44
+ type: 'tool-start';
45
+ id: string;
46
+ name: string;
47
+ label?: string;
48
+ kind?: ToolKind;
49
+ title?: string;
50
+ input?: unknown;
51
+ app?: boolean;
52
+ parent?: string;
53
+ }
54
+ /** A piece of the arguments' JSON. */
55
+ | { type: 'tool-input'; id: string; delta: string }
56
+ | {
57
+ type: 'tool-update';
58
+ id: string;
59
+ input?: unknown;
60
+ title?: string;
61
+ kind?: ToolKind;
62
+ state?: ToolState;
63
+ /** Replaces the outputs. */
64
+ output?: readonly ToolOutput[];
65
+ /** Appended to them. */
66
+ append?: ToolOutput;
67
+ progress?: string;
68
+ }
69
+ | {
70
+ type: 'tool-end';
71
+ id: string;
72
+ state: 'done' | 'error' | 'denied' | 'cancelled';
73
+ output?: readonly ToolOutput[];
74
+ error?: string;
75
+ }
76
+ | { type: 'plan'; entries: readonly PlanEntry[]; parent?: string }
77
+ /**
78
+ * A message the harness took from someone other than the user — another
79
+ * session, a channel — and is acting on. A turn starts with it if none is
80
+ * running, as one does for `send`.
81
+ */
82
+ | { type: 'message'; id: string; text: string; from: MessageSource }
83
+ | { type: 'notice'; level: 'info' | 'warning' | 'error'; text: string }
84
+ | { type: 'info'; info: Partial<SessionInfo> }
85
+ | { type: 'usage'; usage: Usage }
86
+ | {
87
+ type: 'turn-end';
88
+ reason: 'done' | 'stopped' | 'error' | 'limit';
89
+ error?: string;
90
+ };
91
+
92
+ /** The policy's answer, fast, with no user involved. */
93
+ export type GateDecision =
94
+ | { behavior: 'allow'; input?: unknown }
95
+ | { behavior: 'deny'; message: string }
96
+ /** The user must decide: the driver routes the call to `approve`. */
97
+ | { behavior: 'ask' }
98
+ /** No opinion: the harness's own permission mode decides. */
99
+ | { behavior: 'defer' };
100
+
101
+ /** The user's answer, through the approval row. */
102
+ export type ApprovalResult =
103
+ | { behavior: 'allow'; input?: unknown }
104
+ | { behavior: 'deny'; message: string; interrupt?: boolean };
105
+
106
+ /** What a driver may ask of the session. */
107
+ export interface DriverHost {
108
+ readonly config: DriverConfig;
109
+ emit(event: DriverEvent): void;
110
+ /** Before a tool runs: the app's `onToolCall`, then the policy. Never
111
+ * waits on the user, so a harness hook with a timeout can call it. */
112
+ gate(call: ToolCall): Promise<GateDecision>;
113
+ /** The approval row: waits on the user, and while the session is paused.
114
+ * Has no timeout. */
115
+ approve(call: ToolCall, signal?: AbortSignal): Promise<ApprovalResult>;
116
+ /** A question for the user (`AskUserQuestion`). `null` if dismissed. */
117
+ ask(
118
+ id: string,
119
+ questions: readonly Question[],
120
+ signal?: AbortSignal,
121
+ ): Promise<Record<string, string> | null>;
122
+ /** Run one of the app's tools. Never throws: a failure is a result. */
123
+ runTool(
124
+ name: string,
125
+ input: unknown,
126
+ ctx: { callId?: string; signal?: AbortSignal },
127
+ ): Promise<ToolResult>;
128
+ }
129
+
130
+ /** The session's options, resolved for a driver. */
131
+ export interface DriverConfig {
132
+ cwd: string;
133
+ folders: readonly string[];
134
+ /** Appended to the harness's own system prompt. Empty for none. */
135
+ instructions: string;
136
+ /** Replaces the harness's system prompt. */
137
+ systemPrompt?: string;
138
+ /** Categories and harness names switched off. */
139
+ removed: readonly string[];
140
+ /** Categories and harness names that always ask. */
141
+ asking: readonly string[];
142
+ /** Categories replaced by one of `tools`, by the tool's name. */
143
+ replaced: Readonly<Partial<Record<CoreToolKey, string>>>;
144
+ /** The app's tools, replacements included, by name. */
145
+ tools: ReadonlyMap<string, AnyToolDef>;
146
+ permissions: PermissionPolicy;
147
+ model?: string;
148
+ mode?: string;
149
+ /** A harness session id to resume. */
150
+ resume?: string;
151
+ settings: 'cli' | 'isolated';
152
+ driverOptions: Readonly<Record<string, unknown>>;
153
+ }
154
+
155
+ /** A message on its way to the harness. */
156
+ export interface OutgoingMessage {
157
+ /** The transcript's id for it. */
158
+ id: string;
159
+ text: string;
160
+ attachments: readonly Attachment[];
161
+ /** Sent while a turn runs; the driver delivers it as steering. */
162
+ steer: boolean;
163
+ /** Sent on another session's behalf. Absent: the user's own. */
164
+ from?: PeerSource;
165
+ }
166
+
167
+ export interface DriverSession {
168
+ readonly capabilities: Capabilities;
169
+ send(message: OutgoingMessage): void;
170
+ /** Stop the turn. The driver ends it with `turn-end` `'stopped'`. */
171
+ interrupt(): Promise<void>;
172
+ setModel?(id: string): Promise<void>;
173
+ setMode?(id: string): Promise<void>;
174
+ close(): Promise<void>;
175
+ }
176
+
177
+ export interface AgentDriver {
178
+ /** `'claude'`, `'fake'`: shown, and `SessionInfo.agent`. */
179
+ readonly name: string;
180
+ /**
181
+ * Start the harness. Throw `AgentUnavailableError` when it cannot run
182
+ * here — no SDK, no binary, no key — which is an ordinary state of a
183
+ * healthy machine, and the view shows the `fallback`.
184
+ */
185
+ start(host: DriverHost): Promise<DriverSession>;
186
+ }
187
+
188
+ /** The harness cannot run here. Not a bug: the view shows its `fallback`. */
189
+ export class AgentUnavailableError extends Error {
190
+ override readonly name = 'AgentUnavailableError';
191
+ }
@@ -0,0 +1,54 @@
1
+ // `@windowkit/agent-chat/core`: the session, the message model, the driver
2
+ // seam and the tool helpers. No React, and no renderer: a DOM or terminal
3
+ // front end can drive the same session.
4
+
5
+ export {
6
+ createAgentSession,
7
+ describeRenderers,
8
+ type AgentOptions,
9
+ type AgentSession,
10
+ type CoreToolSetting,
11
+ type SessionSnapshot,
12
+ } from './session.js';
13
+ export {
14
+ AgentUnavailableError,
15
+ type AgentDriver,
16
+ type ApprovalResult,
17
+ type DriverConfig,
18
+ type DriverEvent,
19
+ type DriverHost,
20
+ type DriverSession,
21
+ type GateDecision,
22
+ type OutgoingMessage,
23
+ } from './driver.js';
24
+ export {
25
+ defineTool,
26
+ errorResult,
27
+ isStandardSchema,
28
+ jsonSchemaOf,
29
+ normalizeResult,
30
+ validateInput,
31
+ type AnyToolDef,
32
+ type InferInput,
33
+ type JsonObjectSchema,
34
+ type StandardSchemaLike,
35
+ type ToolContext,
36
+ type ToolDef,
37
+ type ToolParameters,
38
+ type ToolRenderInfo,
39
+ type ToolResult,
40
+ } from './tools.js';
41
+ export {
42
+ reduce,
43
+ restore,
44
+ findLivePart,
45
+ findPart,
46
+ type TranscriptAction,
47
+ } from './reducer.js';
48
+ export { categoryOf, policyDecision } from './policy.js';
49
+ export {
50
+ messageMarkdown,
51
+ sourceLabel,
52
+ transcriptMarkdown,
53
+ } from './markdown.js';
54
+ export * from './types.js';
@@ -0,0 +1,56 @@
1
+ // A transcript as markdown: what Copy puts on the clipboard, and what an app
2
+ // saves. Text is written as the agent wrote it; a tool call is one line,
3
+ // since its output is the harness's and usually long.
4
+
5
+ import type { AgentMessage, MessageSource, Part } from './types.js';
6
+
7
+ /** What a message's author is called: a peer's name, or its id; a
8
+ * channel's server. */
9
+ export function sourceLabel(from: MessageSource): string {
10
+ return from.kind === 'peer' ? from.name || from.id : from.server;
11
+ }
12
+
13
+ export function messageMarkdown(message: AgentMessage): string {
14
+ return message.parts.map(partMarkdown).filter(Boolean).join('\n\n');
15
+ }
16
+
17
+ export function transcriptMarkdown(messages: readonly AgentMessage[]): string {
18
+ return messages
19
+ .map((m) => {
20
+ const body = messageMarkdown(m);
21
+ if (!body) return '';
22
+ if (m.role !== 'user') return body;
23
+ const who = m.from ? `From ${sourceLabel(m.from)}` : 'You';
24
+ return `**${who}:**\n\n${body}`;
25
+ })
26
+ .filter(Boolean)
27
+ .join('\n\n---\n\n');
28
+ }
29
+
30
+ function partMarkdown(part: Part): string {
31
+ switch (part.type) {
32
+ case 'text':
33
+ return part.text;
34
+ case 'reasoning':
35
+ return '';
36
+ case 'tool':
37
+ return `> ${part.title}${part.state === 'done' ? '' : ` (${part.state})`}`;
38
+ case 'plan':
39
+ return part.entries
40
+ .map((e) => `- [${e.status === 'completed' ? 'x' : ' '}] ${e.text}`)
41
+ .join('\n');
42
+ case 'question':
43
+ return part.questions
44
+ .map((q) => {
45
+ const a = part.answers?.[q.question];
46
+ return a ? `> ${q.question}\n>\n> ${a}` : `> ${q.question}`;
47
+ })
48
+ .join('\n\n');
49
+ case 'file':
50
+ return `[${part.label}]`;
51
+ case 'notice':
52
+ return `_${part.text}_`;
53
+ case 'data':
54
+ return '';
55
+ }
56
+ }
@@ -0,0 +1,77 @@
1
+ // The permission policy: what runs without asking, before the app's
2
+ // `onToolCall` has had a say. Pure, so the table below is the whole story.
3
+
4
+ import type { DriverConfig, GateDecision } from './driver.js';
5
+ import type { CoreToolKey, ToolCall, ToolKind } from './types.js';
6
+
7
+ /** The `coreTools` category a tool kind belongs to. */
8
+ export function categoryOf(kind: ToolKind): CoreToolKey | undefined {
9
+ switch (kind) {
10
+ case 'execute':
11
+ return 'shell';
12
+ case 'read':
13
+ case 'search':
14
+ return 'read';
15
+ case 'edit':
16
+ case 'delete':
17
+ case 'move':
18
+ return 'edit';
19
+ case 'fetch':
20
+ return 'web';
21
+ default:
22
+ return undefined;
23
+ }
24
+ }
25
+
26
+ const READS: ReadonlySet<ToolKind> = new Set(['read', 'search', 'think']);
27
+ const EDITS: ReadonlySet<ToolKind> = new Set(['edit', 'move']);
28
+
29
+ /**
30
+ * | policy | reads | edits | anything else |
31
+ * | ------------- | ----- | ----- | ------------- |
32
+ * | `'ask'` | run | ask | ask |
33
+ * | `'edits'` | run | run | ask |
34
+ * | `'read-only'` | run | deny | deny |
35
+ * | `'auto'` | the harness decides |
36
+ *
37
+ * Over all of it: an app tool runs unless its `ask` says otherwise, a
38
+ * category or name in `coreTools` set to `'ask'` always asks, and one set to
39
+ * `false` that a harness could not remove is denied.
40
+ */
41
+ export function policyDecision(
42
+ call: ToolCall,
43
+ config: DriverConfig,
44
+ ): GateDecision {
45
+ const category = categoryOf(call.kind);
46
+ const named = (list: readonly string[]) =>
47
+ list.includes(call.label) ||
48
+ (!call.app && !!category && list.includes(category));
49
+
50
+ if (named(config.removed)) {
51
+ return { behavior: 'deny', message: `${call.label} is turned off here.` };
52
+ }
53
+ if (call.app) {
54
+ const tool = config.tools.get(call.label);
55
+ return tool?.ask ? { behavior: 'ask' } : { behavior: 'allow' };
56
+ }
57
+ if (named(config.asking)) return { behavior: 'ask' };
58
+
59
+ switch (config.permissions) {
60
+ case 'auto':
61
+ return { behavior: 'defer' };
62
+ case 'read-only':
63
+ return READS.has(call.kind)
64
+ ? { behavior: 'allow' }
65
+ : {
66
+ behavior: 'deny',
67
+ message: 'This session is read-only: nothing may be changed.',
68
+ };
69
+ case 'edits':
70
+ return READS.has(call.kind) || EDITS.has(call.kind)
71
+ ? { behavior: 'allow' }
72
+ : { behavior: 'ask' };
73
+ case 'ask':
74
+ default:
75
+ return READS.has(call.kind) ? { behavior: 'allow' } : { behavior: 'ask' };
76
+ }
77
+ }