@fromsko/chat-ui 0.0.0-stage → 0.1.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.
Files changed (248) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -2
  3. package/lib/adapters/event-bus.d.ts +80 -0
  4. package/lib/adapters/event-bus.js +73 -0
  5. package/lib/adapters/index.d.ts +3 -0
  6. package/lib/adapters/index.js +3 -0
  7. package/lib/adapters/labels.d.ts +6 -0
  8. package/lib/adapters/labels.js +17 -0
  9. package/lib/adapters/stream-bridge.d.ts +9 -0
  10. package/lib/adapters/stream-bridge.js +7 -0
  11. package/lib/blocks/cards/AskCard.d.ts +8 -0
  12. package/lib/blocks/cards/AskCard.js +97 -0
  13. package/lib/blocks/cards/CompactionCard.d.ts +5 -0
  14. package/lib/blocks/cards/CompactionCard.js +5 -0
  15. package/lib/blocks/cards/ContextUsage.d.ts +14 -0
  16. package/lib/blocks/cards/ContextUsage.js +136 -0
  17. package/lib/blocks/cards/DisplayCard.d.ts +7 -0
  18. package/lib/blocks/cards/DisplayCard.js +27 -0
  19. package/lib/blocks/cards/MemoryPill.d.ts +5 -0
  20. package/lib/blocks/cards/MemoryPill.js +5 -0
  21. package/lib/blocks/cards/OutputCard.d.ts +5 -0
  22. package/lib/blocks/cards/OutputCard.js +13 -0
  23. package/lib/blocks/cards/PermissionCard.d.ts +21 -0
  24. package/lib/blocks/cards/PermissionCard.js +26 -0
  25. package/lib/blocks/cards/SpringCheck.d.ts +9 -0
  26. package/lib/blocks/cards/SpringCheck.js +17 -0
  27. package/lib/blocks/cards/TodoCard.d.ts +19 -0
  28. package/lib/blocks/cards/TodoCard.js +28 -0
  29. package/lib/blocks/cards/index.d.ts +10 -0
  30. package/lib/blocks/cards/index.js +10 -0
  31. package/lib/blocks/composer/Composer.d.ts +66 -0
  32. package/lib/blocks/composer/Composer.js +73 -0
  33. package/lib/blocks/composer/ComposerAttachments.d.ts +17 -0
  34. package/lib/blocks/composer/ComposerAttachments.js +10 -0
  35. package/lib/blocks/composer/ComposerGrip.d.ts +1 -0
  36. package/lib/blocks/composer/ComposerGrip.js +39 -0
  37. package/lib/blocks/composer/ComposerInput.d.ts +18 -0
  38. package/lib/blocks/composer/ComposerInput.js +60 -0
  39. package/lib/blocks/composer/ComposerMenus.d.ts +10 -0
  40. package/lib/blocks/composer/ComposerMenus.js +78 -0
  41. package/lib/blocks/composer/ComposerToolbar.d.ts +27 -0
  42. package/lib/blocks/composer/ComposerToolbar.js +16 -0
  43. package/lib/blocks/composer/MentionPicker.d.ts +13 -0
  44. package/lib/blocks/composer/MentionPicker.js +71 -0
  45. package/lib/blocks/composer/PromptPop.d.ts +7 -0
  46. package/lib/blocks/composer/PromptPop.js +40 -0
  47. package/lib/blocks/composer/QueuedPrompts.d.ts +14 -0
  48. package/lib/blocks/composer/QueuedPrompts.js +21 -0
  49. package/lib/blocks/composer/SlingButton.d.ts +10 -0
  50. package/lib/blocks/composer/SlingButton.js +112 -0
  51. package/lib/blocks/composer/index.d.ts +7 -0
  52. package/lib/blocks/composer/index.js +1 -0
  53. package/lib/blocks/conversation/ChatEmptyScene.d.ts +4 -0
  54. package/lib/blocks/conversation/ChatEmptyScene.js +21 -0
  55. package/lib/blocks/conversation/ChatHeader.d.ts +13 -0
  56. package/lib/blocks/conversation/ChatHeader.js +20 -0
  57. package/lib/blocks/conversation/ChatPane.d.ts +90 -0
  58. package/lib/blocks/conversation/ChatPane.js +60 -0
  59. package/lib/blocks/conversation/Conversation.d.ts +11 -0
  60. package/lib/blocks/conversation/Conversation.js +41 -0
  61. package/lib/blocks/conversation/LineSidebar.d.ts +14 -0
  62. package/lib/blocks/conversation/LineSidebar.js +42 -0
  63. package/lib/blocks/conversation/index.d.ts +5 -0
  64. package/lib/blocks/conversation/index.js +5 -0
  65. package/lib/blocks/index.d.ts +9 -0
  66. package/lib/blocks/index.js +9 -0
  67. package/lib/blocks/message/DayDivider.d.ts +10 -0
  68. package/lib/blocks/message/DayDivider.js +29 -0
  69. package/lib/blocks/message/DiffView.d.ts +5 -0
  70. package/lib/blocks/message/DiffView.js +18 -0
  71. package/lib/blocks/message/MermaidDiagram.d.ts +4 -0
  72. package/lib/blocks/message/MermaidDiagram.js +43 -0
  73. package/lib/blocks/message/Message.d.ts +17 -0
  74. package/lib/blocks/message/Message.js +45 -0
  75. package/lib/blocks/message/MessageActionBar.d.ts +18 -0
  76. package/lib/blocks/message/MessageActionBar.js +15 -0
  77. package/lib/blocks/message/MessageArtifacts.d.ts +13 -0
  78. package/lib/blocks/message/MessageArtifacts.js +9 -0
  79. package/lib/blocks/message/MessageAttachments.d.ts +5 -0
  80. package/lib/blocks/message/MessageAttachments.js +28 -0
  81. package/lib/blocks/message/MessageBubble.d.ts +11 -0
  82. package/lib/blocks/message/MessageBubble.js +31 -0
  83. package/lib/blocks/message/MessageRow.d.ts +20 -0
  84. package/lib/blocks/message/MessageRow.js +49 -0
  85. package/lib/blocks/message/MessageStream.d.ts +23 -0
  86. package/lib/blocks/message/MessageStream.js +80 -0
  87. package/lib/blocks/message/MessageTrace.d.ts +16 -0
  88. package/lib/blocks/message/MessageTrace.js +39 -0
  89. package/lib/blocks/message/PartView.d.ts +10 -0
  90. package/lib/blocks/message/PartView.js +29 -0
  91. package/lib/blocks/message/code-text.d.ts +15 -0
  92. package/lib/blocks/message/code-text.js +17 -0
  93. package/lib/blocks/message/index.d.ts +12 -0
  94. package/lib/blocks/message/index.js +12 -0
  95. package/lib/blocks/reasoning/Reasoning.d.ts +7 -0
  96. package/lib/blocks/reasoning/Reasoning.js +26 -0
  97. package/lib/blocks/reasoning/ReasoningBlock.d.ts +6 -0
  98. package/lib/blocks/reasoning/ReasoningBlock.js +5 -0
  99. package/lib/blocks/reasoning/index.d.ts +2 -0
  100. package/lib/blocks/reasoning/index.js +2 -0
  101. package/lib/blocks/selection/SelectionActions.d.ts +12 -0
  102. package/lib/blocks/selection/SelectionActions.js +29 -0
  103. package/lib/blocks/selection/SelectionBar.d.ts +10 -0
  104. package/lib/blocks/selection/SelectionBar.js +6 -0
  105. package/lib/blocks/selection/index.d.ts +2 -0
  106. package/lib/blocks/selection/index.js +2 -0
  107. package/lib/blocks/settings/DisplaySettingsPanel.d.ts +5 -0
  108. package/lib/blocks/settings/DisplaySettingsPanel.js +33 -0
  109. package/lib/blocks/settings/SquishSwitch.d.ts +8 -0
  110. package/lib/blocks/settings/SquishSwitch.js +21 -0
  111. package/lib/blocks/settings/index.d.ts +2 -0
  112. package/lib/blocks/settings/index.js +2 -0
  113. package/lib/blocks/status/LatticeLoader.d.ts +12 -0
  114. package/lib/blocks/status/LatticeLoader.js +22 -0
  115. package/lib/blocks/status/TypingIndicator.d.ts +5 -0
  116. package/lib/blocks/status/TypingIndicator.js +5 -0
  117. package/lib/blocks/status/index.d.ts +2 -0
  118. package/lib/blocks/status/index.js +2 -0
  119. package/lib/blocks/tool/SandboxCard.d.ts +5 -0
  120. package/lib/blocks/tool/SandboxCard.js +10 -0
  121. package/lib/blocks/tool/ToolCard.d.ts +10 -0
  122. package/lib/blocks/tool/ToolCard.js +32 -0
  123. package/lib/blocks/tool/ToolGroup.d.ts +6 -0
  124. package/lib/blocks/tool/ToolGroup.js +23 -0
  125. package/lib/blocks/tool/exec-envelope.d.ts +7 -0
  126. package/lib/blocks/tool/exec-envelope.js +43 -0
  127. package/lib/blocks/tool/index.d.ts +4 -0
  128. package/lib/blocks/tool/index.js +4 -0
  129. package/lib/domain/ask.d.ts +14 -0
  130. package/lib/domain/ask.js +50 -0
  131. package/lib/domain/attachments.d.ts +16 -0
  132. package/lib/domain/attachments.js +126 -0
  133. package/lib/domain/attribution.d.ts +4 -0
  134. package/lib/domain/attribution.js +11 -0
  135. package/lib/domain/composer-palette.d.ts +15 -0
  136. package/lib/domain/composer-palette.js +131 -0
  137. package/lib/domain/index.d.ts +12 -0
  138. package/lib/domain/index.js +12 -0
  139. package/lib/domain/mention.d.ts +17 -0
  140. package/lib/domain/mention.js +95 -0
  141. package/lib/domain/message-blocks.d.ts +29 -0
  142. package/lib/domain/message-blocks.js +63 -0
  143. package/lib/domain/message-menu.d.ts +4 -0
  144. package/lib/domain/message-menu.js +46 -0
  145. package/lib/domain/queue.d.ts +23 -0
  146. package/lib/domain/queue.js +52 -0
  147. package/lib/domain/reducer.d.ts +137 -0
  148. package/lib/domain/reducer.js +442 -0
  149. package/lib/domain/selection-actions.d.ts +13 -0
  150. package/lib/domain/selection-actions.js +56 -0
  151. package/lib/domain/selection-gates.d.ts +6 -0
  152. package/lib/domain/selection-gates.js +36 -0
  153. package/lib/domain/slash-commands.d.ts +16 -0
  154. package/lib/domain/slash-commands.js +142 -0
  155. package/lib/hooks/index.d.ts +8 -0
  156. package/lib/hooks/index.js +8 -0
  157. package/lib/hooks/useComposerPalette.d.ts +35 -0
  158. package/lib/hooks/useComposerPalette.js +120 -0
  159. package/lib/hooks/useComposerSubmit.d.ts +31 -0
  160. package/lib/hooks/useComposerSubmit.js +135 -0
  161. package/lib/hooks/useInputHistory.d.ts +14 -0
  162. package/lib/hooks/useInputHistory.js +52 -0
  163. package/lib/hooks/usePendingAsk.d.ts +11 -0
  164. package/lib/hooks/usePendingAsk.js +33 -0
  165. package/lib/hooks/usePendingPermissions.d.ts +10 -0
  166. package/lib/hooks/usePendingPermissions.js +26 -0
  167. package/lib/hooks/useQueuedPrompts.d.ts +12 -0
  168. package/lib/hooks/useQueuedPrompts.js +91 -0
  169. package/lib/hooks/useSelectionPop.d.ts +13 -0
  170. package/lib/hooks/useSelectionPop.js +130 -0
  171. package/lib/hooks/useStreamBridge.d.ts +1 -0
  172. package/lib/hooks/useStreamBridge.js +23 -0
  173. package/lib/index.d.ts +10 -0
  174. package/lib/index.js +10 -0
  175. package/lib/primitives/Badge.d.ts +7 -0
  176. package/lib/primitives/Badge.js +13 -0
  177. package/lib/primitives/Button.d.ts +9 -0
  178. package/lib/primitives/Button.js +21 -0
  179. package/lib/primitives/Collapsible.d.ts +12 -0
  180. package/lib/primitives/Collapsible.js +14 -0
  181. package/lib/primitives/Dialog.d.ts +10 -0
  182. package/lib/primitives/Dialog.js +19 -0
  183. package/lib/primitives/DropdownMenu.d.ts +28 -0
  184. package/lib/primitives/DropdownMenu.js +37 -0
  185. package/lib/primitives/Empty.d.ts +8 -0
  186. package/lib/primitives/Empty.js +6 -0
  187. package/lib/primitives/Input.d.ts +3 -0
  188. package/lib/primitives/Input.js +6 -0
  189. package/lib/primitives/Kbd.d.ts +2 -0
  190. package/lib/primitives/Kbd.js +5 -0
  191. package/lib/primitives/Popover.d.ts +15 -0
  192. package/lib/primitives/Popover.js +32 -0
  193. package/lib/primitives/ScrollArea.d.ts +5 -0
  194. package/lib/primitives/ScrollArea.js +5 -0
  195. package/lib/primitives/Separator.d.ts +5 -0
  196. package/lib/primitives/Separator.js +5 -0
  197. package/lib/primitives/Spinner.d.ts +5 -0
  198. package/lib/primitives/Spinner.js +5 -0
  199. package/lib/primitives/Textarea.d.ts +3 -0
  200. package/lib/primitives/Textarea.js +6 -0
  201. package/lib/primitives/Tooltip.d.ts +8 -0
  202. package/lib/primitives/Tooltip.js +7 -0
  203. package/lib/primitives/index.d.ts +14 -0
  204. package/lib/primitives/index.js +14 -0
  205. package/lib/reactbits/Motion.d.ts +24 -0
  206. package/lib/reactbits/Motion.js +52 -0
  207. package/lib/reactbits/index.d.ts +1 -0
  208. package/lib/reactbits/index.js +1 -0
  209. package/lib/stores/chat-store.d.ts +33 -0
  210. package/lib/stores/chat-store.js +93 -0
  211. package/lib/stores/display-store.d.ts +15 -0
  212. package/lib/stores/display-store.js +116 -0
  213. package/lib/stores/index.d.ts +3 -0
  214. package/lib/stores/index.js +3 -0
  215. package/lib/stores/theme-store.d.ts +8 -0
  216. package/lib/stores/theme-store.js +29 -0
  217. package/lib/tokens/index.d.ts +21 -0
  218. package/lib/tokens/index.js +66 -0
  219. package/lib/types/agent.d.ts +62 -0
  220. package/lib/types/agent.js +1 -0
  221. package/lib/types/callbacks.d.ts +55 -0
  222. package/lib/types/callbacks.js +1 -0
  223. package/lib/types/composer.d.ts +116 -0
  224. package/lib/types/composer.js +1 -0
  225. package/lib/types/conversation.d.ts +54 -0
  226. package/lib/types/conversation.js +1 -0
  227. package/lib/types/display.d.ts +25 -0
  228. package/lib/types/display.js +40 -0
  229. package/lib/types/index.d.ts +8 -0
  230. package/lib/types/index.js +8 -0
  231. package/lib/types/message.d.ts +100 -0
  232. package/lib/types/message.js +1 -0
  233. package/lib/types/selection.d.ts +60 -0
  234. package/lib/types/selection.js +1 -0
  235. package/lib/types/theme.d.ts +100 -0
  236. package/lib/types/theme.js +1 -0
  237. package/lib/utils/clipboard.d.ts +4 -0
  238. package/lib/utils/clipboard.js +41 -0
  239. package/lib/utils/cn.d.ts +2 -0
  240. package/lib/utils/cn.js +14 -0
  241. package/lib/utils/index.d.ts +3 -0
  242. package/lib/utils/index.js +3 -0
  243. package/lib/utils/storage.d.ts +11 -0
  244. package/lib/utils/storage.js +48 -0
  245. package/package.json +93 -4
  246. package/src/styles/blocks.css +2439 -0
  247. package/src/styles/index.css +3 -0
  248. package/src/tokens/theme.css +464 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chat-ui contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,101 @@
1
- # Temporary Holding Version
1
+ # chat-ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React components for AI chat surfaces: a message stream, composer, tool-call cards, ask/permission
4
+ prompts, selection actions and a token-driven theme layer. Composable blocks, no backend, no routing,
5
+ no data fetching — you own the state and the transport.
6
+
7
+ - **Composable blocks** — `ChatPane` for the whole surface, or assemble message / composer / card
8
+ blocks yourself.
9
+ - **Token-driven theming** — CSS variables plus light / dark / glass presets, overridable per host.
10
+ - **Streaming-ready** — message parts, reasoning blocks, tool envelopes and queued prompts render
11
+ incrementally.
12
+ - **Typed end to end** — strict TypeScript, exported types for every prop and adapter contract.
13
+ - **Playwright screenshot check** — CI renders the demo and captures full-page screenshots.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @fromsko/chat-ui
19
+ ```
20
+
21
+ Peer dependencies: `react@^19`, `react-dom@^19`. `gsap` is optional (only used by a few
22
+ decorative blocks). Tailwind CSS v4 is expected in the host app for the shipped stylesheet.
23
+
24
+ ## Usage
25
+
26
+ ```tsx
27
+ import { ChatPane, applyTheme } from "@fromsko/chat-ui";
28
+ import "@fromsko/chat-ui/styles.css";
29
+ import { useEffect, useState } from "react";
30
+ import type { Agent, ChatMessage, Conversation } from "@fromsko/chat-ui";
31
+
32
+ export function Chat() {
33
+ const [draft, setDraft] = useState("");
34
+ const [messages] = useState<ChatMessage[]>([]);
35
+
36
+ useEffect(() => {
37
+ applyTheme("dark");
38
+ }, []);
39
+
40
+ return (
41
+ <ChatPane
42
+ conversation={{} as Conversation}
43
+ members={[] as Agent[]}
44
+ messages={messages}
45
+ status="idle"
46
+ draft={draft}
47
+ onDraftChange={setDraft}
48
+ onSend={(text) => {
49
+ setDraft("");
50
+ void text;
51
+ }}
52
+ onOpenDetail={() => {}}
53
+ onBack={() => {}}
54
+ />
55
+ );
56
+ }
57
+ ```
58
+
59
+ Hooks cover the parts that are tedious to re-implement: `useComposerSubmit`, `useComposerPalette`,
60
+ `useQueuedPrompts`, `useInputHistory`, `useSelectionPop`, `usePendingAsk`, `usePendingPermissions`.
61
+
62
+ ## Theming
63
+
64
+ ```ts
65
+ import { applyTheme, readTheme, tokens } from "@fromsko/chat-ui";
66
+
67
+ applyTheme("dark");
68
+ applyTheme("light");
69
+ ```
70
+
71
+ All colors, radii, spacing and motion values come from CSS variables under
72
+ `src/tokens/theme.css`. Override them on `:root` or a subtree to re-skin the blocks without touching
73
+ component code.
74
+
75
+ ## Entry points
76
+
77
+ | Specifier | Contents |
78
+ | --- | --- |
79
+ | `@fromsko/chat-ui` | All components, hooks, stores, adapters and types |
80
+ | `@fromsko/chat-ui/types` | Type-only surface |
81
+ | `@fromsko/chat-ui/tokens` | Design tokens and theme helpers |
82
+ | `@fromsko/chat-ui/styles.css` | Stylesheet (Tailwind CSS v4 source) |
83
+
84
+ ## Development
85
+
86
+ ```bash
87
+ bun install
88
+ bun run dev # demo app on the Rsbuild dev server
89
+ bun run typecheck # tsc --noEmit
90
+ bun run lint # rslint
91
+ bun run build # demo site -> dist/
92
+ bun run build:lib # package build -> lib/ (JS + .d.ts)
93
+ bun run check # typecheck + lint + build
94
+ ```
95
+
96
+ The demo under `src/demo` is the reference host: it wires the blocks to in-memory state and is what
97
+ the screenshot check renders.
98
+
99
+ ## License
100
+
101
+ MIT
@@ -0,0 +1,80 @@
1
+ import type { ChatStore } from "../stores/chat-store";
2
+ export interface StreamEventMap {
3
+ "stream:delta": {
4
+ requestId: string;
5
+ text: string;
6
+ };
7
+ "stream:reasoning": {
8
+ requestId: string;
9
+ text: string;
10
+ };
11
+ "stream:tool-call": {
12
+ requestId: string;
13
+ toolCallId: string;
14
+ name: string;
15
+ args: unknown;
16
+ };
17
+ "stream:tool-result": {
18
+ requestId: string;
19
+ toolCallId: string;
20
+ result: string;
21
+ isError: boolean;
22
+ };
23
+ "stream:usage": {
24
+ requestId: string;
25
+ inputTokens: number;
26
+ outputTokens: number;
27
+ window: number;
28
+ };
29
+ "stream:memory": {
30
+ requestId: string;
31
+ count: number;
32
+ excerpt: string;
33
+ };
34
+ "stream:compaction": {
35
+ requestId: string | null;
36
+ beforeTokens: number;
37
+ afterTokens: number;
38
+ reason: string;
39
+ };
40
+ "stream:end": {
41
+ requestId: string;
42
+ outcome: "done" | "error";
43
+ error?: string;
44
+ };
45
+ "stream:bind": {
46
+ requestId: string;
47
+ };
48
+ }
49
+ export interface DisplayEventMap {
50
+ "display:render": {
51
+ requestId: string;
52
+ view: string;
53
+ title?: string;
54
+ payload?: unknown;
55
+ };
56
+ }
57
+ export interface OutputEventMap {
58
+ "output:emit": {
59
+ requestId: string;
60
+ outputKind: "text" | "file" | "link" | "json";
61
+ label?: string;
62
+ value: string;
63
+ };
64
+ }
65
+ export interface ChatEventMap extends StreamEventMap, DisplayEventMap, OutputEventMap {
66
+ }
67
+ export type ChatEventType = keyof ChatEventMap;
68
+ export type ChatEventHandler<K extends ChatEventType> = (payload: ChatEventMap[K]) => void;
69
+ export declare class ChatEventBus {
70
+ private handlers;
71
+ on<K extends ChatEventType>(type: K, handler: ChatEventHandler<K>): () => void;
72
+ off<K extends ChatEventType>(type: K, handler: ChatEventHandler<K>): void;
73
+ emit<K extends ChatEventType>(type: K, payload: ChatEventMap[K]): void;
74
+ clear(): void;
75
+ }
76
+ export declare const chatEventBus: ChatEventBus;
77
+ export declare function bindBusToStore(bus: ChatEventBus, store: {
78
+ getState: () => ChatStore;
79
+ }): () => void;
80
+ export declare function bridgeCallbacksToBus(bus: ChatEventBus): import("../types/callbacks").StreamEventCallbacks;
@@ -0,0 +1,73 @@
1
+ export class ChatEventBus {
2
+ handlers = new Map();
3
+ on(type, handler) {
4
+ let set = this.handlers.get(type);
5
+ if (!set) {
6
+ set = new Set();
7
+ this.handlers.set(type, set);
8
+ }
9
+ set.add(handler);
10
+ return () => this.off(type, handler);
11
+ }
12
+ off(type, handler) {
13
+ this.handlers.get(type)?.delete(handler);
14
+ }
15
+ emit(type, payload) {
16
+ const set = this.handlers.get(type);
17
+ if (!set)
18
+ return;
19
+ for (const handler of [...set]) {
20
+ handler(payload);
21
+ }
22
+ }
23
+ clear() {
24
+ this.handlers.clear();
25
+ }
26
+ }
27
+ export const chatEventBus = new ChatEventBus();
28
+ export function bindBusToStore(bus, store) {
29
+ const s = () => store.getState();
30
+ const offs = [
31
+ bus.on("stream:bind", ({ requestId }) => s().bindRequest(requestId)),
32
+ bus.on("stream:delta", ({ requestId, text }) => s().streamDelta(requestId, text)),
33
+ bus.on("stream:reasoning", ({ requestId, text }) => s().streamReasoning(requestId, text)),
34
+ bus.on("stream:tool-call", ({ requestId, toolCallId, name, args }) => s().streamToolCall(requestId, toolCallId, name, args)),
35
+ bus.on("stream:tool-result", ({ requestId, toolCallId, result, isError }) => s().streamToolResult(requestId, toolCallId, result, isError)),
36
+ bus.on("stream:usage", ({ requestId, inputTokens, outputTokens, window }) => s().streamContextUsage(requestId, inputTokens, outputTokens, window)),
37
+ bus.on("stream:memory", ({ requestId, count, excerpt }) => s().streamMemory(requestId, count, excerpt)),
38
+ bus.on("stream:compaction", ({ requestId, beforeTokens, afterTokens, reason }) => s().streamCompaction(requestId, beforeTokens, afterTokens, reason)),
39
+ bus.on("stream:end", ({ requestId, outcome, error }) => s().streamEnd(requestId, outcome, error)),
40
+ bus.on("display:render", ({ requestId, view, title, payload }) => s().streamDisplay(requestId, view, title, payload)),
41
+ bus.on("output:emit", ({ requestId, outputKind, label, value }) => s().streamOutput(requestId, outputKind, label, value)),
42
+ ];
43
+ return () => offs.forEach((off) => off());
44
+ }
45
+ export function bridgeCallbacksToBus(bus) {
46
+ return {
47
+ onDelta: (requestId, text) => bus.emit("stream:delta", { requestId, text }),
48
+ onReasoning: (requestId, text) => bus.emit("stream:reasoning", { requestId, text }),
49
+ onToolCall: (requestId, toolCallId, name, args) => bus.emit("stream:tool-call", { requestId, toolCallId, name, args }),
50
+ onToolResult: (requestId, toolCallId, result, isError) => bus.emit("stream:tool-result", { requestId, toolCallId, result, isError }),
51
+ onContextUsage: (requestId, inputTokens, outputTokens, window) => bus.emit("stream:usage", {
52
+ requestId,
53
+ inputTokens,
54
+ outputTokens,
55
+ window,
56
+ }),
57
+ onMemory: (requestId, count, excerpt) => bus.emit("stream:memory", { requestId, count, excerpt }),
58
+ onCompaction: (requestId, beforeTokens, afterTokens, reason) => bus.emit("stream:compaction", {
59
+ requestId,
60
+ beforeTokens,
61
+ afterTokens,
62
+ reason,
63
+ }),
64
+ onEnd: (requestId, outcome, error) => bus.emit("stream:end", { requestId, outcome, error }),
65
+ onDisplay: (requestId, view, title, payload) => bus.emit("display:render", { requestId, view, title, payload }),
66
+ onOutput: (requestId, outputKind, label, value) => bus.emit("output:emit", {
67
+ requestId,
68
+ outputKind,
69
+ label,
70
+ value: value ?? "",
71
+ }),
72
+ };
73
+ }
@@ -0,0 +1,3 @@
1
+ export { setToolLabel, toolLabel, setClockLabel, clockLabel, type ToolLabelFn, type ClockLabelFn, } from "./labels";
2
+ export { setStreamBridge, getStreamBridge, type StreamBridge, } from "./stream-bridge";
3
+ export { ChatEventBus, chatEventBus, bindBusToStore, bridgeCallbacksToBus, type ChatEventMap, type ChatEventType, type ChatEventHandler, type StreamEventMap, type DisplayEventMap, type OutputEventMap, } from "./event-bus";
@@ -0,0 +1,3 @@
1
+ export { setToolLabel, toolLabel, setClockLabel, clockLabel, } from "./labels";
2
+ export { setStreamBridge, getStreamBridge, } from "./stream-bridge";
3
+ export { ChatEventBus, chatEventBus, bindBusToStore, bridgeCallbacksToBus, } from "./event-bus";
@@ -0,0 +1,6 @@
1
+ export type ToolLabelFn = (tool: string) => string;
2
+ export declare function setToolLabel(fn: ToolLabelFn): void;
3
+ export declare function toolLabel(tool: string): string;
4
+ export type ClockLabelFn = (isoOrNow?: string) => string;
5
+ export declare function setClockLabel(fn: ClockLabelFn): void;
6
+ export declare function clockLabel(isoOrNow?: string): string;
@@ -0,0 +1,17 @@
1
+ let toolLabelImpl = (tool) => tool;
2
+ export function setToolLabel(fn) {
3
+ toolLabelImpl = fn;
4
+ }
5
+ export function toolLabel(tool) {
6
+ return toolLabelImpl(tool);
7
+ }
8
+ let clockLabelImpl = () => {
9
+ const d = new Date();
10
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
11
+ };
12
+ export function setClockLabel(fn) {
13
+ clockLabelImpl = fn;
14
+ }
15
+ export function clockLabel(isoOrNow) {
16
+ return clockLabelImpl(isoOrNow);
17
+ }
@@ -0,0 +1,9 @@
1
+ import type { StreamEventCallbacks } from "../types/callbacks";
2
+ export interface StreamBridge {
3
+ subscribe(callbacks: StreamEventCallbacks): () => void;
4
+ cancel?(requestId: string): Promise<void> | void;
5
+ respondAsk?(turnId: string, callId: string, answer: string): Promise<void> | void;
6
+ respondPermission?(permissionId: string, approved: boolean, remembered: boolean): Promise<void> | void;
7
+ }
8
+ export declare function setStreamBridge(next: StreamBridge | null): void;
9
+ export declare function getStreamBridge(): StreamBridge | null;
@@ -0,0 +1,7 @@
1
+ let bridge = null;
2
+ export function setStreamBridge(next) {
3
+ bridge = next;
4
+ }
5
+ export function getStreamBridge() {
6
+ return bridge;
7
+ }
@@ -0,0 +1,8 @@
1
+ import type { PendingAsk } from "../../types/conversation";
2
+ export interface AskCardProps {
3
+ ask: PendingAsk;
4
+ queued: number;
5
+ onAnswer: (text: string) => void;
6
+ onSkip?: () => void;
7
+ }
8
+ export declare function AskCard({ ask, queued, onAnswer, onSkip }: AskCardProps): import("react").JSX.Element;
@@ -0,0 +1,97 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { ArrowRight, Check, CircleHelp, Sparkles, Square, SquareCheck, X } from "lucide-react";
4
+ import { MAX_ASK_QUESTIONS, answeredCount, batchAnswersOf, canDefaultAll, defaultAnswersOf, isBatchAsk, } from "../../domain/ask";
5
+ import { cn } from "../../utils/cn";
6
+ function toQuestions(ask) {
7
+ if (isBatchAsk(ask))
8
+ return ask.questions.slice(0, MAX_ASK_QUESTIONS);
9
+ return [
10
+ {
11
+ id: ask.callId,
12
+ prompt: ask.prompt,
13
+ choices: ask.choices,
14
+ default: ask.default,
15
+ },
16
+ ];
17
+ }
18
+ export function AskCard({ ask, queued, onAnswer, onSkip }) {
19
+ const questions = useMemo(() => toQuestions(ask), [ask]);
20
+ const batch = isBatchAsk(ask);
21
+ const [answers, setAnswers] = useState({});
22
+ const [freeOpen, setFreeOpen] = useState({});
23
+ const answered = answeredCount(questions, answers);
24
+ const total = questions.length;
25
+ const canDefault = canDefaultAll(questions);
26
+ const hiddenOverflow = batch ? ask.questions.length - questions.length : 0;
27
+ const selectedOf = (q) => {
28
+ const raw = answers[q.id] ?? "";
29
+ if (!q.multiSelect)
30
+ return raw ? [raw] : [];
31
+ return raw ? raw.split(", ").filter(Boolean) : [];
32
+ };
33
+ const pick = (q, choice) => {
34
+ if (q.multiSelect) {
35
+ const cur = selectedOf(q);
36
+ const next = cur.includes(choice)
37
+ ? cur.filter((c) => c !== choice)
38
+ : [...cur, choice];
39
+ setAnswers((prev) => ({ ...prev, [q.id]: next.join(", ") }));
40
+ return;
41
+ }
42
+ setAnswers((prev) => ({ ...prev, [q.id]: choice }));
43
+ if (!batch)
44
+ onAnswer(choice);
45
+ };
46
+ const useAllDefaults = () => {
47
+ const defaults = defaultAnswersOf(questions);
48
+ if (!batch) {
49
+ const only = questions[0];
50
+ const v = defaults[only.id];
51
+ if (v)
52
+ onAnswer(v);
53
+ return;
54
+ }
55
+ onAnswer(batchAnswersOf(questions, defaults));
56
+ };
57
+ return (_jsxs("div", { className: "ac-ask-sheet", "data-cmp": "ac/ask-card", "data-id": "ask-sheet", "data-state": "waiting", "data-mode": batch ? "batch" : "single", "aria-label": "Agent \u63D0\u95EE", children: [_jsxs("div", { className: "ac-ask-sheet__head", children: [_jsx(CircleHelp, { className: "ac-ask-sheet__head-icon" }), _jsx("span", { className: "ac-ask-sheet__who", children: ask.agent ? `${ask.agent} 想确认` : "Agent 想确认" }), total > 1 ? (_jsxs("span", { className: "ac-ask-sheet__progress", "data-cmp": "ac/ask-progress", children: [answered, "/", total] })) : null, queued > 0 ? (_jsxs("span", { className: "ac-ask-sheet__queued", children: ["+", queued, " \u5F85\u7B54"] })) : null, onSkip ? (_jsxs("button", { type: "button", "data-cmp": "ac/ask-skip", className: "ac-ask-sheet__skip", title: "\u8DF3\u8FC7\uFF08Agent \u5C06\u81EA\u884C\u51B3\u65AD\u5E76\u7EE7\u7EED\uFF09", onClick: onSkip, children: [_jsx(X, { className: "size-3" }), "\u8DF3\u8FC7"] })) : null] }), _jsxs("div", { className: "ac-ask-sheet__body", children: [questions.map((q, i) => {
58
+ const chosen = answers[q.id] ?? "";
59
+ const def = q.default ?? q.choices[0] ?? null;
60
+ const picked = selectedOf(q);
61
+ const isFree = Boolean(freeOpen[q.id]) ||
62
+ (chosen.length > 0 &&
63
+ !picked.every((p) => q.choices.includes(p)));
64
+ const freeText = picked
65
+ .filter((p) => !q.choices.includes(p))
66
+ .join(", ");
67
+ return (_jsxs("div", { className: "ac-ask-q", "data-cmp": "ac/ask-question", "data-index": i + 1, "data-answered": chosen ? "true" : "false", "data-multi": q.multiSelect ? "true" : "false", children: [_jsxs("p", { className: "ac-ask-q__prompt", "data-cmp": "ac/ask-prompt", children: [total > 1 ? (_jsx("span", { className: "ac-ask-q__index", children: i + 1 })) : null, q.prompt, q.multiSelect ? (_jsx("span", { className: "ac-ask-q__multi-tag", children: "\u53EF\u591A\u9009" })) : null] }), _jsxs("div", { className: "ac-ask-q__choices", children: [q.choices.map((choice) => {
68
+ const recommended = choice === def;
69
+ const isPicked = picked.includes(choice);
70
+ return (_jsxs("button", { type: "button", "data-cmp": "ac/ask-choice", "data-state": isPicked
71
+ ? "chosen"
72
+ : recommended
73
+ ? "recommended"
74
+ : "idle", className: "ac-ask-q__chip", title: recommended ? "推荐答案" : "点击采用", onClick: () => pick(q, choice), children: [q.multiSelect ? (isPicked ? (_jsx(SquareCheck, { className: "size-3" })) : (_jsx(Square, { className: "size-3 opacity-50" }))) : isPicked ? (_jsx(Check, { className: "size-3" })) : recommended ? (_jsx(Sparkles, { className: "size-3" })) : null, _jsx("span", { children: choice })] }, choice));
75
+ }), _jsx("button", { type: "button", "data-cmp": "ac/ask-other", className: "ac-ask-q__chip ac-ask-q__chip--other", "data-state": isFree ? "chosen" : "idle", title: "\u8F93\u5165\u81EA\u5DF1\u7684\u7B54\u6848", onClick: () => setFreeOpen((prev) => ({ ...prev, [q.id]: !prev[q.id] })), children: "\u5176\u4ED6\u2026" })] }), isFree ? (_jsx("input", { "data-cmp": "ac/ask-input", className: "ac-ask-q__input", "aria-label": `第 ${i + 1} 题自由作答`, placeholder: "\u8F93\u5165\u4F60\u7684\u7B54\u6848\u2026", autoFocus: true, value: freeText, onChange: (e) => {
76
+ const keep = picked.filter((p) => q.choices.includes(p));
77
+ const next = e.target.value
78
+ ? [...keep, e.target.value]
79
+ : keep;
80
+ setAnswers((prev) => ({
81
+ ...prev,
82
+ [q.id]: next.join(", "),
83
+ }));
84
+ }, onKeyDown: (e) => {
85
+ if (e.key === "Enter") {
86
+ e.preventDefault();
87
+ const v = (answers[q.id] ?? "").trim();
88
+ if (!v)
89
+ return;
90
+ if (!batch)
91
+ onAnswer(v);
92
+ else if (total === 1)
93
+ onAnswer(v);
94
+ }
95
+ } })) : null] }, q.id));
96
+ }), hiddenOverflow > 0 ? (_jsxs("p", { className: "ac-ask-sheet__overflow", children: ["\u53E6\u6709 ", hiddenOverflow, " \u4E2A\u95EE\u9898\uFF0C\u5C06\u5728\u672C\u8F6E\u7B54\u590D\u540E\u7EE7\u7EED"] })) : null] }), total > 1 ? (_jsxs("div", { className: "ac-ask-sheet__actions", children: [_jsxs("button", { type: "button", "data-cmp": "ac/ask-default-all", className: "ac-ask-sheet__default", disabled: !canDefault, title: canDefault ? "全部采用推荐答案" : "存在无默认项的问题,需手动作答", onClick: useAllDefaults, children: [_jsx(Sparkles, { className: "size-3" }), "\u5168\u90E8\u7528\u9ED8\u8BA4"] }), _jsxs("button", { type: "button", "data-cmp": "ac/ask-submit", className: cn("ac-ask-sheet__submit", answered === 0 && "is-empty"), disabled: answered === 0, title: answered === 0 ? "至少答一题才能提交" : "提交全部答案", onClick: () => onAnswer(batchAnswersOf(questions, answers)), children: ["\u63D0\u4EA4 ", answered, "/", total, _jsx(ArrowRight, { className: "size-3" })] })] })) : null] }));
97
+ }
@@ -0,0 +1,5 @@
1
+ import type { CompactionPart } from "../../types/message";
2
+ export interface CompactionCardProps {
3
+ part: CompactionPart;
4
+ }
5
+ export declare function CompactionCard({ part }: CompactionCardProps): import("react").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Minimize2 } from "lucide-react";
3
+ export function CompactionCard({ part }) {
4
+ return (_jsxs("div", { className: "ac-compact", "data-cmp": "ac/compact", "data-state": "done", children: [_jsx(Minimize2, { "aria-hidden": "true", size: 12 }), _jsx("span", { className: "ac-compact__title", children: "\u4E0A\u4E0B\u6587\u538B\u7F29" }), _jsxs("span", { className: "ac-compact__tokens", children: [part.beforeTokens, " \u2192 ", part.afterTokens, " tokens"] }), _jsx("span", { className: "ac-compact__reason", title: part.reason, children: part.reason })] }));
5
+ }
@@ -0,0 +1,14 @@
1
+ import type { ContextUsageSegment, ContextWindowPreset } from "../../types/message";
2
+ export declare function formatTokens(n: number): string;
3
+ export interface ContextUsageProps {
4
+ input: number;
5
+ output?: number;
6
+ window: number;
7
+ breakdown?: readonly ContextUsageSegment[];
8
+ windowPresets?: readonly ContextWindowPreset[];
9
+ onWindowChange?: (window: number) => void;
10
+ className?: string;
11
+ side?: "top" | "bottom";
12
+ onClose?: () => void;
13
+ }
14
+ export declare function ContextUsage({ input, output, window: ctxWindow, breakdown, windowPresets, onWindowChange, className, side, onClose, }: ContextUsageProps): import("react").JSX.Element | null;
@@ -0,0 +1,136 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useRef, useState } from "react";
3
+ import { AlertTriangle, X } from "lucide-react";
4
+ import { Popover } from "../../primitives/Popover";
5
+ import { cn } from "../../utils/cn";
6
+ const LEVEL_COLOR = {
7
+ comfortable: "text-[var(--accent-success)]",
8
+ warning: "text-[var(--accent-warning)]",
9
+ critical: "text-[var(--accent-danger)]",
10
+ };
11
+ const LEVEL_LABEL = {
12
+ comfortable: "充裕",
13
+ warning: "偏紧",
14
+ critical: "告急",
15
+ };
16
+ const SEGMENT_FALLBACK = [
17
+ "#818cf8",
18
+ "#34d399",
19
+ "#fbbf24",
20
+ "#a78bfa",
21
+ "#f472b6",
22
+ ];
23
+ export function formatTokens(n) {
24
+ if (!Number.isFinite(n) || n < 0)
25
+ return "0";
26
+ if (n < 1000)
27
+ return String(n);
28
+ if (n < 1_000_000) {
29
+ const k = n / 1000;
30
+ return k < 10 ? `${k.toFixed(1)}K` : `${Math.round(k)}K`;
31
+ }
32
+ return `${(n / 1_000_000).toFixed(1)}M`;
33
+ }
34
+ function Ring({ ratio, level }) {
35
+ const size = 20;
36
+ const stroke = 2.5;
37
+ const r = (size - stroke) / 2;
38
+ const circumference = 2 * Math.PI * r;
39
+ const color = level === "critical"
40
+ ? "var(--accent-danger)"
41
+ : level === "warning"
42
+ ? "var(--accent-warning)"
43
+ : "var(--accent-primary)";
44
+ return (_jsxs("svg", { width: size, height: size, "aria-hidden": "true", className: "shrink-0", children: [_jsx("circle", { cx: size / 2, cy: size / 2, r: r, fill: "none", stroke: "var(--surface-700)", strokeWidth: stroke }), _jsx("circle", { cx: size / 2, cy: size / 2, r: r, fill: "none", stroke: color, strokeWidth: stroke, strokeLinecap: "round", strokeDasharray: circumference, strokeDashoffset: circumference * (1 - Math.min(1, Math.max(0, ratio))), transform: `rotate(-90 ${size / 2} ${size / 2})` })] }));
45
+ }
46
+ function SegmentedBar({ segments, total, level, usedRatio, }) {
47
+ if (segments.length === 0) {
48
+ return (_jsx("div", { className: "flex h-2 w-full overflow-hidden rounded-full bg-secondary", children: _jsx("div", { className: "h-full rounded-full transition-[width] duration-300", style: {
49
+ width: `${Math.max(usedRatio * 100, usedRatio > 0 ? 2 : 0)}%`,
50
+ background: level === "critical"
51
+ ? "var(--accent-danger)"
52
+ : level === "warning"
53
+ ? "var(--accent-warning)"
54
+ : "var(--accent-primary)",
55
+ } }) }));
56
+ }
57
+ return (_jsx("div", { className: "flex h-2 w-full overflow-hidden rounded-full bg-secondary", children: segments.map((seg, i) => (_jsx("div", { title: `${seg.label} ${formatTokens(seg.tokens)}`, className: "h-full transition-[width] duration-300", style: {
58
+ width: `${total > 0 ? (seg.tokens / total) * 100 : 0}%`,
59
+ background: seg.color,
60
+ } }, `${seg.label}-${i}`))) }));
61
+ }
62
+ function WindowSlider({ presets, current, onChange, }) {
63
+ const idx = Math.max(0, presets.findIndex((p) => p.window === current));
64
+ const active = presets[idx] ?? presets[0];
65
+ const interactive = onChange !== undefined;
66
+ const trackRef = useRef(null);
67
+ const dragging = useRef(false);
68
+ const pickAt = (clientX) => {
69
+ if (!interactive || !trackRef.current)
70
+ return;
71
+ const rect = trackRef.current.getBoundingClientRect();
72
+ const frac = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
73
+ const next = Math.round(frac * (presets.length - 1));
74
+ if (presets[next] && presets[next].window !== current) {
75
+ onChange(presets[next].window);
76
+ }
77
+ };
78
+ return (_jsxs("div", { className: "border-t border-border px-3 py-2.5", children: [_jsxs("div", { className: "mb-1.5 text-center text-body font-semibold", children: [formatTokens(active.window), " \u4E0A\u4E0B\u6587 \u00B7 ", active.label] }), _jsx("div", { ref: trackRef, role: interactive ? "slider" : undefined, "aria-valuemin": 0, "aria-valuemax": presets.length - 1, "aria-valuenow": idx, "aria-valuetext": `${formatTokens(active.window)} 上下文 · ${active.label}`, "aria-label": "\u4E0A\u4E0B\u6587\u7A97\u53E3\u6863\u4F4D", tabIndex: interactive ? 0 : undefined, className: cn("flex items-center gap-2 py-1", interactive && "cursor-pointer touch-none select-none"), onPointerDown: (e) => {
79
+ if (!interactive)
80
+ return;
81
+ dragging.current = true;
82
+ e.currentTarget.setPointerCapture(e.pointerId);
83
+ pickAt(e.clientX);
84
+ }, onPointerMove: (e) => {
85
+ if (!dragging.current)
86
+ return;
87
+ pickAt(e.clientX);
88
+ }, onPointerUp: (e) => {
89
+ dragging.current = false;
90
+ e.currentTarget.releasePointerCapture(e.pointerId);
91
+ }, onKeyDown: (e) => {
92
+ if (!interactive)
93
+ return;
94
+ if (e.key === "ArrowLeft" && idx > 0) {
95
+ e.preventDefault();
96
+ onChange(presets[idx - 1].window);
97
+ }
98
+ if (e.key === "ArrowRight" && idx < presets.length - 1) {
99
+ e.preventDefault();
100
+ onChange(presets[idx + 1].window);
101
+ }
102
+ }, children: _jsxs("span", { className: "relative flex h-3 flex-1 items-center", children: [_jsx("span", { className: "h-1 w-full rounded-full bg-secondary" }), _jsx("span", { className: "absolute left-0 h-1 rounded-full bg-[var(--accent-primary)]", style: {
103
+ width: `${presets.length > 1 ? (idx / (presets.length - 1)) * 100 : 100}%`,
104
+ } }), presets.map((p, i) => (_jsx("span", { className: "absolute size-2.5 rounded-full border transition-colors", style: {
105
+ left: `calc(${presets.length > 1 ? (i / (presets.length - 1)) * 100 : 50}% - 5px)`,
106
+ background: i <= idx ? "var(--accent-primary)" : "var(--surface-600)",
107
+ borderColor: i <= idx ? "var(--accent-primary)" : "var(--border-600)",
108
+ } }, p.label)))] }) })] }));
109
+ }
110
+ export function ContextUsage({ input, output, window: ctxWindow, breakdown, windowPresets, onWindowChange, className, side = "top", onClose, }) {
111
+ const barId = useId();
112
+ const [open, setOpen] = useState(false);
113
+ if (!Number.isFinite(ctxWindow) || ctxWindow <= 0)
114
+ return null;
115
+ const used = input + (output ?? 0);
116
+ const usedPercent = Math.min(100, (used / ctxWindow) * 100);
117
+ const remainingPercent = Math.max(0, 100 - usedPercent);
118
+ const level = remainingPercent <= 10
119
+ ? "critical"
120
+ : remainingPercent <= 25
121
+ ? "warning"
122
+ : "comfortable";
123
+ const ratio = used / ctxWindow;
124
+ const segments = (breakdown ?? []).map((seg, i) => ({
125
+ label: seg.label,
126
+ tokens: seg.tokens,
127
+ color: seg.color || SEGMENT_FALLBACK[i % SEGMENT_FALLBACK.length],
128
+ }));
129
+ return (_jsx(Popover, { side: side, align: "end", open: open, onOpenChange: (next) => {
130
+ setOpen(next);
131
+ if (!next)
132
+ onClose?.();
133
+ }, trigger: ({ open: triggerOpen }) => (_jsxs("span", { role: "button", tabIndex: 0, "data-cmp": "ai/context-usage", "data-id": "usage-ring", "data-state": level, "data-level": level, "data-open": triggerOpen ? "true" : "false", "aria-label": `上下文用量:已用 ${usedPercent.toFixed(1)}%,剩余 ${remainingPercent.toFixed(1)}%`, title: `${LEVEL_LABEL[level]} · 已用 ${usedPercent.toFixed(1)}%`, className: cn("inline-flex h-[var(--control-sm)] min-w-7 cursor-pointer items-center gap-1.5 rounded-full px-1.5 transition-colors hover:bg-secondary", LEVEL_COLOR[level], className), children: [_jsx(Ring, { ratio: ratio, level: level }), _jsxs("span", { className: "text-micro tabular-nums", children: [usedPercent.toFixed(0), "%"] }), level === "critical" ? _jsx(AlertTriangle, { className: "size-3" }) : null] })), children: _jsxs("div", { "data-cmp": "ai/context-usage-panel", "data-open": open ? "true" : "false", className: "w-72 p-0", children: [_jsxs("div", { className: "flex items-start justify-between px-3.5 pt-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-body font-semibold", children: "\u4E0A\u4E0B\u6587\u7528\u91CF" }), _jsxs("div", { className: "mt-1 flex items-baseline gap-1.5", children: [_jsxs("span", { className: "text-heading font-semibold tabular-nums", children: [usedPercent.toFixed(1), "%"] }), _jsxs("span", { className: "text-meta text-muted-foreground", children: ["\u5DF2\u4F7F\u7528 ", formatTokens(used), "/ ", formatTokens(ctxWindow)] })] })] }), _jsx("button", { type: "button", "aria-label": "\u5173\u95ED\u7528\u91CF\u9762\u677F", className: "rounded p-0.5 text-muted-foreground hover:text-foreground", onClick: () => setOpen(false), children: _jsx(X, { className: "size-4" }) })] }), _jsx("div", { className: "px-3.5 pt-2.5", children: _jsx(SegmentedBar, { segments: segments, total: ctxWindow, level: level, usedRatio: ratio }, barId) }), segments.length > 0 ? (_jsx("div", { className: "px-3.5 pt-2.5 pb-1", children: segments.map((seg, i) => (_jsxs("div", { className: "flex items-center gap-2 py-0.5 text-meta", children: [_jsx("span", { className: "size-2 shrink-0 rounded-full", style: { background: seg.color } }), _jsx("span", { className: "text-foreground", children: seg.label }), _jsx("span", { className: "ml-auto tabular-nums text-muted-foreground", children: ctxWindow > 0
134
+ ? `${((seg.tokens / ctxWindow) * 100).toFixed(1)}%`
135
+ : "0%" })] }, `${seg.label}-${i}`))) })) : (_jsxs("div", { className: "px-3.5 pt-2 pb-1", children: [_jsxs("div", { className: "flex items-center justify-between py-0.5 text-meta", children: [_jsx("span", { className: "text-muted-foreground", children: "\u8F93\u5165" }), _jsx("span", { className: "tabular-nums text-foreground", children: formatTokens(input) })] }), output !== undefined ? (_jsxs("div", { className: "flex items-center justify-between py-0.5 text-meta", children: [_jsx("span", { className: "text-muted-foreground", children: "\u8F93\u51FA" }), _jsx("span", { className: "tabular-nums text-foreground", children: formatTokens(output) })] })) : null] })), windowPresets && windowPresets.length > 1 ? (_jsx(WindowSlider, { presets: windowPresets, current: ctxWindow, onChange: onWindowChange })) : null] }) }));
136
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ import type { DisplayPart } from "../../types/message";
3
+ export interface DisplayCardProps {
4
+ part: DisplayPart;
5
+ renderDisplay?: (part: DisplayPart) => ReactNode;
6
+ }
7
+ export declare function DisplayCard({ part, renderDisplay }: DisplayCardProps): import("react").JSX.Element;