@assistkick/create 1.10.0 → 1.12.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 (209) hide show
  1. package/dist/src/scaffolder.d.ts +12 -1
  2. package/dist/src/scaffolder.js +40 -3
  3. package/dist/src/scaffolder.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/assistkick-product-system/package.json +1 -1
  6. package/templates/assistkick-product-system/packages/backend/package.json +1 -0
  7. package/templates/assistkick-product-system/packages/backend/src/mcp/permission_mcp_server.ts +196 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +31 -7
  9. package/templates/assistkick-product-system/packages/backend/src/routes/auth.ts +15 -12
  10. package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.test.ts +95 -0
  11. package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.ts +97 -0
  12. package/templates/assistkick-product-system/packages/backend/src/routes/chat_permission.ts +94 -0
  13. package/templates/assistkick-product-system/packages/backend/src/routes/chat_sessions.ts +189 -0
  14. package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.test.ts +131 -0
  15. package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.ts +94 -0
  16. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +12 -3
  17. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +2 -2
  18. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +390 -22
  19. package/templates/assistkick-product-system/packages/backend/src/routes/git_branches.test.ts +306 -0
  20. package/templates/assistkick-product-system/packages/backend/src/routes/git_connect.test.ts +133 -0
  21. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +66 -9
  22. package/templates/assistkick-product-system/packages/backend/src/routes/preview.ts +204 -0
  23. package/templates/assistkick-product-system/packages/backend/src/routes/projects.test.ts +205 -0
  24. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +37 -9
  25. package/templates/assistkick-product-system/packages/backend/src/routes/skills.test.ts +139 -0
  26. package/templates/assistkick-product-system/packages/backend/src/routes/skills.ts +95 -0
  27. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +5 -4
  28. package/templates/assistkick-product-system/packages/backend/src/routes/users.ts +4 -4
  29. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +8 -8
  30. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +5 -5
  31. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +6 -6
  32. package/templates/assistkick-product-system/packages/backend/src/server.ts +107 -27
  33. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +105 -203
  34. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +76 -266
  35. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.test.ts +427 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +345 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.test.ts +170 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.ts +106 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.test.ts +217 -0
  40. package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.ts +188 -0
  41. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.test.ts +1243 -0
  42. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +894 -0
  43. package/templates/assistkick-product-system/packages/backend/src/services/coherence-review.ts +3 -3
  44. package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.test.ts +85 -0
  45. package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.ts +54 -0
  46. package/templates/assistkick-product-system/packages/backend/src/services/email_service.ts +13 -10
  47. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +11 -3
  48. package/templates/assistkick-product-system/packages/backend/src/services/invitation_service.ts +1 -1
  49. package/templates/assistkick-product-system/packages/backend/src/services/password_reset_service.ts +1 -1
  50. package/templates/assistkick-product-system/packages/backend/src/services/permission_service.test.ts +243 -0
  51. package/templates/assistkick-product-system/packages/backend/src/services/permission_service.ts +259 -0
  52. package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.test.ts +172 -0
  53. package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.ts +225 -0
  54. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +29 -0
  55. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +17 -0
  56. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +255 -0
  57. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +300 -25
  58. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +44 -0
  59. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +62 -7
  60. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +77 -6
  61. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +129 -8
  62. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +2 -1
  63. package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.test.ts +45 -0
  64. package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.ts +157 -0
  65. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +4 -3
  66. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +3 -3
  67. package/templates/assistkick-product-system/packages/frontend/package.json +5 -0
  68. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +2 -0
  69. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +336 -5
  70. package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +192 -12
  71. package/templates/assistkick-product-system/packages/frontend/src/components/AttachmentPreviewList.tsx +98 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/AutocompleteDropdown.tsx +65 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ChatAttachButton.tsx +56 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ChatDropZone.tsx +80 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx +155 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx +182 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageInput.tsx +233 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ChatSessionSidebar.tsx +218 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ChatStopButton.tsx +32 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ChatTodoSidebar.tsx +113 -0
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +842 -0
  82. package/templates/assistkick-product-system/packages/frontend/src/components/CommitMessageModal.tsx +82 -0
  83. package/templates/assistkick-product-system/packages/frontend/src/components/DiagramOverlay.tsx +160 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +5 -5
  85. package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +9 -10
  86. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +5 -5
  87. package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +112 -41
  88. package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +2 -2
  89. package/templates/assistkick-product-system/packages/frontend/src/components/HighlightedText.tsx +87 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/ImageLightbox.tsx +192 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +2 -2
  92. package/templates/assistkick-product-system/packages/frontend/src/components/MentionPill.tsx +33 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/MermaidBlock.tsx +148 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/PermissionDialog.tsx +91 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/PermissionModeSelector.tsx +229 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +249 -83
  97. package/templates/assistkick-product-system/packages/frontend/src/components/QueuedMessageBubble.tsx +38 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +212 -117
  99. package/templates/assistkick-product-system/packages/frontend/src/components/SystemPromptAccordion.tsx +48 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/TaskIcon.tsx +11 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +25 -9
  102. package/templates/assistkick-product-system/packages/frontend/src/components/ToolDiffView.tsx +114 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/ToolResultCard.tsx +87 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +149 -0
  105. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +25 -8
  106. package/templates/assistkick-product-system/packages/frontend/src/components/UnifiedGitWidget.tsx +722 -0
  107. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +2 -0
  108. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +2 -1
  109. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/ProgrammableNode.tsx +178 -0
  110. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +3 -0
  111. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +103 -9
  112. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +26 -2
  113. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +42 -1
  114. package/templates/assistkick-product-system/packages/frontend/src/hooks/useDocumentTitle.ts +11 -0
  115. package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +1 -0
  116. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_chat_stream.ts +826 -0
  117. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_file_tree_cache.ts +69 -0
  118. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_mention_autocomplete.ts +284 -0
  119. package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.test.ts +183 -0
  120. package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.ts +150 -0
  121. package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.test.ts +305 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.ts +113 -0
  123. package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.test.ts +157 -0
  124. package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.ts +95 -0
  125. package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.test.ts +65 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.ts +110 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/lib/message_queue.ts +66 -0
  128. package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.test.ts +124 -0
  129. package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.ts +112 -0
  130. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +2 -0
  131. package/templates/assistkick-product-system/packages/frontend/src/routes/ChatRoute.tsx +8 -0
  132. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +2 -0
  133. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +0 -4
  134. package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +2 -0
  135. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +2 -0
  136. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -0
  137. package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +2 -0
  138. package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +2 -0
  139. package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +2 -0
  140. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +2 -0
  141. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +2 -0
  142. package/templates/assistkick-product-system/packages/frontend/src/routes/accept_invitation.tsx +2 -0
  143. package/templates/assistkick-product-system/packages/frontend/src/routes/forgot_password.tsx +2 -0
  144. package/templates/assistkick-product-system/packages/frontend/src/routes/login.tsx +2 -0
  145. package/templates/assistkick-product-system/packages/frontend/src/routes/register.tsx +2 -0
  146. package/templates/assistkick-product-system/packages/frontend/src/routes/reset_password.tsx +2 -0
  147. package/templates/assistkick-product-system/packages/frontend/src/stores/useAttachmentStore.ts +66 -0
  148. package/templates/assistkick-product-system/packages/frontend/src/stores/useChatSessionStore.ts +107 -0
  149. package/templates/assistkick-product-system/packages/frontend/src/stores/useMessageQueueStore.ts +110 -0
  150. package/templates/assistkick-product-system/packages/frontend/src/stores/usePreviewStore.ts +78 -0
  151. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +7 -0
  152. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +6 -1
  153. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +30 -357
  154. package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.test.ts +115 -0
  155. package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.ts +91 -0
  156. package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.test.ts +30 -0
  157. package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.ts +3 -0
  158. package/templates/assistkick-product-system/packages/shared/db/migrate.ts +82 -0
  159. package/templates/assistkick-product-system/packages/shared/db/migrations/0000_outgoing_ultron.sql +277 -0
  160. package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
  161. package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
  162. package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
  163. package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
  164. package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
  165. package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
  166. package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
  167. package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
  168. package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
  169. package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
  170. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0000_snapshot.json +972 -22
  171. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
  172. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
  173. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
  174. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
  175. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
  176. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
  177. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
  178. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
  179. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
  180. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
  181. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +2 -100
  182. package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
  183. package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
  184. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
  185. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
  186. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
  187. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
  188. package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
  189. package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
  190. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
  191. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
  192. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
  193. package/templates/assistkick-product-system/packages/shared/package.json +2 -1
  194. package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
  195. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
  196. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
  197. package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
  198. package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
  199. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
  200. package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
  201. package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
  202. package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
  203. package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
  204. package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
  205. package/templates/skills/assistkick-debugger/SKILL.md +23 -23
  206. package/templates/skills/assistkick-developer/SKILL.md +59 -63
  207. package/templates/skills/assistkick-interview/SKILL.md +26 -26
  208. package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
  209. package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
@@ -0,0 +1,305 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ buildToolResultMap,
5
+ extractMessageText,
6
+ countToolUses,
7
+ countToolErrors,
8
+ isEmptyMessage,
9
+ buildStreamOptions,
10
+ prepareMergedQueueText,
11
+ markOrCreateStreamingAssistant,
12
+ } from './chat_message_helpers.ts';
13
+ import type { ChatMessage, ContentBlock } from '../hooks/use_chat_stream.ts';
14
+ import type { QueuedMessage } from './message_queue.ts';
15
+
16
+ describe('buildToolResultMap', () => {
17
+ it('builds empty map from empty blocks', () => {
18
+ const map = buildToolResultMap([]);
19
+ assert.equal(map.size, 0);
20
+ });
21
+
22
+ it('builds map from tool_result blocks', () => {
23
+ const blocks: ContentBlock[] = [
24
+ { type: 'text', text: 'hello' },
25
+ { type: 'tool_result', toolUseId: 'tu_1', content: 'result1', isError: false },
26
+ { type: 'tool_use', id: 'tu_1', name: 'Read', input: {} },
27
+ { type: 'tool_result', toolUseId: 'tu_2', content: 'result2', isError: true },
28
+ ];
29
+ const map = buildToolResultMap(blocks);
30
+ assert.equal(map.size, 2);
31
+ assert.equal(map.get('tu_1')?.content, 'result1');
32
+ assert.equal(map.get('tu_2')?.isError, true);
33
+ });
34
+
35
+ it('ignores non-tool-result blocks', () => {
36
+ const blocks: ContentBlock[] = [
37
+ { type: 'text', text: 'hello' },
38
+ { type: 'tool_use', id: 'tu_1', name: 'Read', input: {} },
39
+ ];
40
+ const map = buildToolResultMap(blocks);
41
+ assert.equal(map.size, 0);
42
+ });
43
+ });
44
+
45
+ describe('extractMessageText', () => {
46
+ it('extracts text from user message', () => {
47
+ const msg: ChatMessage = {
48
+ id: 'msg_1',
49
+ role: 'user',
50
+ content: [{ type: 'text', text: 'Hello Claude' }],
51
+ isStreaming: false,
52
+ };
53
+ assert.equal(extractMessageText(msg), 'Hello Claude');
54
+ });
55
+
56
+ it('concatenates multiple text blocks', () => {
57
+ const msg: ChatMessage = {
58
+ id: 'msg_2',
59
+ role: 'assistant',
60
+ content: [
61
+ { type: 'text', text: 'First part' },
62
+ { type: 'tool_use', id: 'tu_1', name: 'Read', input: {} },
63
+ { type: 'text', text: 'Second part' },
64
+ ],
65
+ isStreaming: false,
66
+ };
67
+ assert.equal(extractMessageText(msg), 'First part\nSecond part');
68
+ });
69
+
70
+ it('returns empty string for messages with no text blocks', () => {
71
+ const msg: ChatMessage = {
72
+ id: 'msg_3',
73
+ role: 'assistant',
74
+ content: [
75
+ { type: 'tool_use', id: 'tu_1', name: 'Bash', input: { command: 'ls' } },
76
+ ],
77
+ isStreaming: false,
78
+ };
79
+ assert.equal(extractMessageText(msg), '');
80
+ });
81
+ });
82
+
83
+ describe('countToolUses', () => {
84
+ it('returns 0 for messages with no tool uses', () => {
85
+ const msg: ChatMessage = {
86
+ id: 'msg_1',
87
+ role: 'assistant',
88
+ content: [{ type: 'text', text: 'hello' }],
89
+ isStreaming: false,
90
+ };
91
+ assert.equal(countToolUses(msg), 0);
92
+ });
93
+
94
+ it('counts tool use blocks', () => {
95
+ const msg: ChatMessage = {
96
+ id: 'msg_2',
97
+ role: 'assistant',
98
+ content: [
99
+ { type: 'text', text: 'hello' },
100
+ { type: 'tool_use', id: 'tu_1', name: 'Read', input: {} },
101
+ { type: 'tool_use', id: 'tu_2', name: 'Write', input: {} },
102
+ { type: 'tool_result', toolUseId: 'tu_1', content: 'ok', isError: false },
103
+ ],
104
+ isStreaming: false,
105
+ };
106
+ assert.equal(countToolUses(msg), 2);
107
+ });
108
+ });
109
+
110
+ describe('countToolErrors', () => {
111
+ it('returns 0 when no errors', () => {
112
+ const msg: ChatMessage = {
113
+ id: 'msg_1',
114
+ role: 'assistant',
115
+ content: [
116
+ { type: 'tool_result', toolUseId: 'tu_1', content: 'ok', isError: false },
117
+ ],
118
+ isStreaming: false,
119
+ };
120
+ assert.equal(countToolErrors(msg), 0);
121
+ });
122
+
123
+ it('counts error results', () => {
124
+ const msg: ChatMessage = {
125
+ id: 'msg_2',
126
+ role: 'assistant',
127
+ content: [
128
+ { type: 'tool_result', toolUseId: 'tu_1', content: 'ok', isError: false },
129
+ { type: 'tool_result', toolUseId: 'tu_2', content: 'fail', isError: true },
130
+ { type: 'tool_result', toolUseId: 'tu_3', content: 'also fail', isError: true },
131
+ ],
132
+ isStreaming: false,
133
+ };
134
+ assert.equal(countToolErrors(msg), 2);
135
+ });
136
+ });
137
+
138
+ describe('isEmptyMessage', () => {
139
+ it('returns true for empty content', () => {
140
+ const msg: ChatMessage = { id: 'msg_1', role: 'assistant', content: [], isStreaming: true };
141
+ assert.equal(isEmptyMessage(msg), true);
142
+ });
143
+
144
+ it('returns false for non-empty content', () => {
145
+ const msg: ChatMessage = {
146
+ id: 'msg_2',
147
+ role: 'assistant',
148
+ content: [{ type: 'text', text: 'hello' }],
149
+ isStreaming: false,
150
+ };
151
+ assert.equal(isEmptyMessage(msg), false);
152
+ });
153
+ });
154
+
155
+ describe('buildStreamOptions', () => {
156
+ it('builds options with skip mode', () => {
157
+ const opts = buildStreamOptions({
158
+ projectId: 'proj_1',
159
+ claudeSessionId: 'sess_1',
160
+ isNewSession: true,
161
+ permissionMode: 'skip',
162
+ allowedTools: ['Read'],
163
+ attachmentPaths: [],
164
+ });
165
+ assert.equal(opts.projectId, 'proj_1');
166
+ assert.equal(opts.claudeSessionId, 'sess_1');
167
+ assert.equal(opts.isNewSession, true);
168
+ assert.equal(opts.permissionMode, 'skip');
169
+ assert.equal(opts.allowedTools, undefined);
170
+ assert.equal(opts.attachments, undefined);
171
+ });
172
+
173
+ it('includes allowedTools only in allowed_tools mode', () => {
174
+ const opts = buildStreamOptions({
175
+ projectId: 'proj_1',
176
+ claudeSessionId: 'sess_1',
177
+ isNewSession: false,
178
+ permissionMode: 'allowed_tools',
179
+ allowedTools: ['Read', 'Grep'],
180
+ attachmentPaths: [],
181
+ });
182
+ assert.deepEqual(opts.allowedTools, ['Read', 'Grep']);
183
+ });
184
+
185
+ it('includes attachments when paths provided', () => {
186
+ const opts = buildStreamOptions({
187
+ projectId: 'proj_1',
188
+ claudeSessionId: 'sess_1',
189
+ isNewSession: false,
190
+ permissionMode: 'skip',
191
+ allowedTools: [],
192
+ attachmentPaths: ['uploads/file.txt', 'uploads/img.png'],
193
+ });
194
+ assert.deepEqual(opts.attachments, ['uploads/file.txt', 'uploads/img.png']);
195
+ });
196
+
197
+ it('omits attachments when paths empty', () => {
198
+ const opts = buildStreamOptions({
199
+ projectId: 'proj_1',
200
+ claudeSessionId: 'sess_1',
201
+ isNewSession: false,
202
+ permissionMode: 'prompt',
203
+ allowedTools: [],
204
+ attachmentPaths: [],
205
+ });
206
+ assert.equal(opts.attachments, undefined);
207
+ });
208
+ });
209
+
210
+ describe('prepareMergedQueueText', () => {
211
+ it('returns empty string for empty queue', () => {
212
+ assert.equal(prepareMergedQueueText([]), '');
213
+ });
214
+
215
+ it('returns single message text', () => {
216
+ const messages: QueuedMessage[] = [
217
+ { id: 'q_1', text: 'hello', createdAt: 1000 },
218
+ ];
219
+ assert.equal(prepareMergedQueueText(messages), 'hello');
220
+ });
221
+
222
+ it('joins multiple messages with double newline', () => {
223
+ const messages: QueuedMessage[] = [
224
+ { id: 'q_1', text: 'first message', createdAt: 1000 },
225
+ { id: 'q_2', text: 'second message', createdAt: 2000 },
226
+ { id: 'q_3', text: 'third message', createdAt: 3000 },
227
+ ];
228
+ assert.equal(
229
+ prepareMergedQueueText(messages),
230
+ 'first message\n\nsecond message\n\nthird message',
231
+ );
232
+ });
233
+ });
234
+
235
+ describe('markOrCreateStreamingAssistant', () => {
236
+ it('marks existing assistant message as streaming', () => {
237
+ const messages: ChatMessage[] = [
238
+ { id: 'msg_1', role: 'user', content: [{ type: 'text', text: 'hi' }], isStreaming: false },
239
+ { id: 'msg_2', role: 'assistant', content: [{ type: 'text', text: 'hello' }], isStreaming: false },
240
+ ];
241
+ const result = markOrCreateStreamingAssistant(messages, 'msg_new');
242
+ assert.equal(result.length, 2);
243
+ assert.equal(result[1].isStreaming, true);
244
+ assert.equal(result[1].id, 'msg_2');
245
+ assert.equal(result[1].content.length, 1);
246
+ });
247
+
248
+ it('marks the LAST assistant message as streaming when multiple exist', () => {
249
+ const messages: ChatMessage[] = [
250
+ { id: 'msg_1', role: 'user', content: [{ type: 'text', text: 'hi' }], isStreaming: false },
251
+ { id: 'msg_2', role: 'assistant', content: [{ type: 'text', text: 'first' }], isStreaming: false },
252
+ { id: 'msg_3', role: 'user', content: [{ type: 'text', text: 'follow up' }], isStreaming: false },
253
+ { id: 'msg_4', role: 'assistant', content: [{ type: 'text', text: 'second' }], isStreaming: false },
254
+ ];
255
+ const result = markOrCreateStreamingAssistant(messages, 'msg_new');
256
+ assert.equal(result.length, 4);
257
+ assert.equal(result[1].isStreaming, false);
258
+ assert.equal(result[3].isStreaming, true);
259
+ assert.equal(result[3].id, 'msg_4');
260
+ });
261
+
262
+ it('creates new empty assistant message when none exists', () => {
263
+ const messages: ChatMessage[] = [
264
+ { id: 'msg_1', role: 'user', content: [{ type: 'text', text: 'hi' }], isStreaming: false },
265
+ ];
266
+ const result = markOrCreateStreamingAssistant(messages, 'msg_new');
267
+ assert.equal(result.length, 2);
268
+ assert.equal(result[1].id, 'msg_new');
269
+ assert.equal(result[1].role, 'assistant');
270
+ assert.deepEqual(result[1].content, []);
271
+ assert.equal(result[1].isStreaming, true);
272
+ });
273
+
274
+ it('creates new assistant message when messages array is empty', () => {
275
+ const result = markOrCreateStreamingAssistant([], 'msg_new');
276
+ assert.equal(result.length, 1);
277
+ assert.equal(result[0].role, 'assistant');
278
+ assert.equal(result[0].isStreaming, true);
279
+ assert.deepEqual(result[0].content, []);
280
+ });
281
+
282
+ it('does not mutate the original array', () => {
283
+ const original: ChatMessage[] = [
284
+ { id: 'msg_1', role: 'assistant', content: [{ type: 'text', text: 'hello' }], isStreaming: false },
285
+ ];
286
+ const result = markOrCreateStreamingAssistant(original, 'msg_new');
287
+ assert.equal(original[0].isStreaming, false);
288
+ assert.equal(result[0].isStreaming, true);
289
+ assert.notEqual(original, result);
290
+ });
291
+
292
+ it('preserves content when marking existing assistant as streaming', () => {
293
+ const content: ContentBlock[] = [
294
+ { type: 'text', text: 'some response' },
295
+ { type: 'tool_use', id: 'tu_1', name: 'Read', input: { path: '/file.ts' } },
296
+ ];
297
+ const messages: ChatMessage[] = [
298
+ { id: 'msg_1', role: 'user', content: [{ type: 'text', text: 'hi' }], isStreaming: false },
299
+ { id: 'msg_2', role: 'assistant', content, isStreaming: false },
300
+ ];
301
+ const result = markOrCreateStreamingAssistant(messages, 'msg_new');
302
+ assert.equal(result[1].content.length, 2);
303
+ assert.deepEqual(result[1].content, content);
304
+ });
305
+ });
@@ -0,0 +1,113 @@
1
+ /**
2
+ * chat_message_helpers — pure utility functions for Chat v2 message processing.
3
+ *
4
+ * Extracts testable logic from the ChatView and related components.
5
+ * Includes stream options building, message filtering, and text extraction.
6
+ */
7
+
8
+ import type { ChatMessage, ContentBlock, TextBlock, ToolResultBlock } from '../hooks/use_chat_stream';
9
+ import type { QueuedMessage } from './message_queue';
10
+
11
+ /**
12
+ * Build a lookup map from toolUseId → ToolResultBlock for matching
13
+ * tool results to their corresponding tool_use blocks.
14
+ */
15
+ export const buildToolResultMap = (blocks: ContentBlock[]): Map<string, ToolResultBlock> => {
16
+ const map = new Map<string, ToolResultBlock>();
17
+ for (const block of blocks) {
18
+ if (block.type === 'tool_result') {
19
+ map.set(block.toolUseId, block);
20
+ }
21
+ }
22
+ return map;
23
+ };
24
+
25
+ /**
26
+ * Extract plain text from a ChatMessage's content blocks.
27
+ * Returns the concatenated text of all TextBlock entries.
28
+ */
29
+ export const extractMessageText = (message: ChatMessage): string => {
30
+ return message.content
31
+ .filter((b): b is TextBlock => b.type === 'text')
32
+ .map((b) => b.text)
33
+ .join('\n');
34
+ };
35
+
36
+ /**
37
+ * Count tool use blocks in a message's content.
38
+ */
39
+ export const countToolUses = (message: ChatMessage): number => {
40
+ return message.content.filter((b) => b.type === 'tool_use').length;
41
+ };
42
+
43
+ /**
44
+ * Count tool result blocks that are errors.
45
+ */
46
+ export const countToolErrors = (message: ChatMessage): number => {
47
+ return message.content.filter(
48
+ (b) => b.type === 'tool_result' && b.isError,
49
+ ).length;
50
+ };
51
+
52
+ /**
53
+ * Determine if a message has any content to display.
54
+ * Empty streaming assistant messages show a "Thinking…" indicator.
55
+ */
56
+ export const isEmptyMessage = (message: ChatMessage): boolean => {
57
+ return message.content.length === 0;
58
+ };
59
+
60
+ /**
61
+ * Build the stream options object for sending a message.
62
+ */
63
+ export const buildStreamOptions = (opts: {
64
+ projectId: string;
65
+ claudeSessionId: string;
66
+ isNewSession: boolean;
67
+ permissionMode: 'skip' | 'allowed_tools' | 'prompt';
68
+ allowedTools: string[];
69
+ attachmentPaths: string[];
70
+ }): {
71
+ projectId: string;
72
+ claudeSessionId: string;
73
+ isNewSession: boolean;
74
+ permissionMode: 'skip' | 'allowed_tools' | 'prompt';
75
+ allowedTools?: string[];
76
+ attachments?: string[];
77
+ } => {
78
+ return {
79
+ projectId: opts.projectId,
80
+ claudeSessionId: opts.claudeSessionId,
81
+ isNewSession: opts.isNewSession,
82
+ permissionMode: opts.permissionMode,
83
+ allowedTools: opts.permissionMode === 'allowed_tools' ? opts.allowedTools : undefined,
84
+ attachments: opts.attachmentPaths.length > 0 ? opts.attachmentPaths : undefined,
85
+ };
86
+ };
87
+
88
+ /**
89
+ * Prepare merged text from queued messages for sending as a single CLI prompt.
90
+ */
91
+ export const prepareMergedQueueText = (messages: QueuedMessage[]): string => {
92
+ return messages.map((m) => m.text).join('\n\n');
93
+ };
94
+
95
+ /**
96
+ * Mark the last assistant message as streaming, or create a new empty one if none exists.
97
+ * Used by the stream_resumed handler when re-attaching to an in-flight stream.
98
+ * Returns a new array (does not mutate the input).
99
+ */
100
+ export const markOrCreateStreamingAssistant = (
101
+ messages: ChatMessage[],
102
+ nextId: string,
103
+ ): ChatMessage[] => {
104
+ const updated = [...messages];
105
+ for (let i = updated.length - 1; i >= 0; i--) {
106
+ if (updated[i].role === 'assistant') {
107
+ updated[i] = { ...updated[i], isStreaming: true };
108
+ return updated;
109
+ }
110
+ }
111
+ updated.push({ id: nextId, role: 'assistant', content: [], isStreaming: true });
112
+ return updated;
113
+ };
@@ -0,0 +1,157 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { extractContextUsage, formatTokenCount, buildContextSegments } from './context_usage_helpers.ts';
4
+
5
+ describe('extractContextUsage', () => {
6
+ it('extracts all token fields from a full usage object', () => {
7
+ const usage = {
8
+ input_tokens: 2000,
9
+ output_tokens: 3000,
10
+ cache_creation_input_tokens: 5000,
11
+ cache_read_input_tokens: 40000,
12
+ };
13
+ const result = extractContextUsage(usage);
14
+ assert.equal(result.inputTokens, 2000);
15
+ assert.equal(result.outputTokens, 3000);
16
+ assert.equal(result.cacheCreationTokens, 5000);
17
+ assert.equal(result.cacheReadTokens, 40000);
18
+ // totalTokens = input context only (no output)
19
+ assert.equal(result.totalTokens, 47000);
20
+ assert.equal(result.contextWindow, null);
21
+ });
22
+
23
+ it('defaults cache fields to 0 when absent', () => {
24
+ const usage = {
25
+ input_tokens: 1000,
26
+ output_tokens: 500,
27
+ };
28
+ const result = extractContextUsage(usage);
29
+ assert.equal(result.cacheCreationTokens, 0);
30
+ assert.equal(result.cacheReadTokens, 0);
31
+ // totalTokens = input only, output excluded
32
+ assert.equal(result.totalTokens, 1000);
33
+ });
34
+
35
+ it('defaults all fields to 0 for empty usage object', () => {
36
+ const result = extractContextUsage({});
37
+ assert.equal(result.inputTokens, 0);
38
+ assert.equal(result.outputTokens, 0);
39
+ assert.equal(result.cacheCreationTokens, 0);
40
+ assert.equal(result.cacheReadTokens, 0);
41
+ assert.equal(result.totalTokens, 0);
42
+ });
43
+
44
+ it('ignores non-number token values', () => {
45
+ const usage = {
46
+ input_tokens: 'not a number',
47
+ output_tokens: null,
48
+ cache_creation_input_tokens: undefined,
49
+ cache_read_input_tokens: true,
50
+ };
51
+ const result = extractContextUsage(usage as Record<string, unknown>);
52
+ assert.equal(result.totalTokens, 0);
53
+ });
54
+
55
+ it('computes correct total with only cache tokens (no raw input)', () => {
56
+ const usage = {
57
+ input_tokens: 0,
58
+ output_tokens: 1000,
59
+ cache_creation_input_tokens: 0,
60
+ cache_read_input_tokens: 45000,
61
+ };
62
+ const result = extractContextUsage(usage);
63
+ // totalTokens = input context only (cache_read), output excluded
64
+ assert.equal(result.totalTokens, 45000);
65
+ });
66
+ });
67
+
68
+ describe('formatTokenCount', () => {
69
+ it('formats small numbers as-is', () => {
70
+ assert.equal(formatTokenCount(0), '0');
71
+ assert.equal(formatTokenCount(500), '500');
72
+ assert.equal(formatTokenCount(999), '999');
73
+ });
74
+
75
+ it('formats thousands with k suffix', () => {
76
+ assert.equal(formatTokenCount(1000), '1k');
77
+ assert.equal(formatTokenCount(1500), '2k');
78
+ assert.equal(formatTokenCount(45000), '45k');
79
+ assert.equal(formatTokenCount(200000), '200k');
80
+ });
81
+
82
+ it('formats millions with M suffix', () => {
83
+ assert.equal(formatTokenCount(1000000), '1.0M');
84
+ assert.equal(formatTokenCount(1500000), '1.5M');
85
+ assert.equal(formatTokenCount(2000000), '2.0M');
86
+ });
87
+ });
88
+
89
+ describe('buildContextSegments', () => {
90
+ it('builds segments for typical usage with all categories', () => {
91
+ const segments = buildContextSegments({
92
+ inputTokens: 100,
93
+ outputTokens: 500,
94
+ cacheCreationTokens: 10000,
95
+ cacheReadTokens: 40000,
96
+ totalTokens: 50100,
97
+ contextWindow: 200000,
98
+ });
99
+
100
+ // Should have: cached context, new cached, uncached input, free
101
+ assert.equal(segments.length, 4);
102
+ assert.equal(segments[0].label, 'Cached context');
103
+ assert.equal(segments[0].tokens, 40000);
104
+ assert.equal(segments[1].label, 'New cached');
105
+ assert.equal(segments[1].tokens, 10000);
106
+ assert.equal(segments[2].label, 'Uncached input');
107
+ assert.equal(segments[2].tokens, 100);
108
+ assert.equal(segments[3].label, 'Free');
109
+ assert.equal(segments[3].tokens, 149900);
110
+ });
111
+
112
+ it('defaults contextWindow to 200k when null', () => {
113
+ const segments = buildContextSegments({
114
+ inputTokens: 0,
115
+ outputTokens: 0,
116
+ cacheCreationTokens: 0,
117
+ cacheReadTokens: 100000,
118
+ totalTokens: 100000,
119
+ contextWindow: null,
120
+ });
121
+
122
+ const free = segments.find(s => s.label === 'Free');
123
+ assert.ok(free);
124
+ assert.equal(free.tokens, 100000); // 200k - 100k
125
+ });
126
+
127
+ it('omits zero-token segments', () => {
128
+ const segments = buildContextSegments({
129
+ inputTokens: 0,
130
+ outputTokens: 0,
131
+ cacheCreationTokens: 0,
132
+ cacheReadTokens: 50000,
133
+ totalTokens: 50000,
134
+ contextWindow: 200000,
135
+ });
136
+
137
+ const labels = segments.map(s => s.label);
138
+ assert.ok(labels.includes('Cached context'));
139
+ assert.ok(labels.includes('Free'));
140
+ assert.ok(!labels.includes('New cached'));
141
+ assert.ok(!labels.includes('Uncached input'));
142
+ });
143
+
144
+ it('shows no free space when context is full', () => {
145
+ const segments = buildContextSegments({
146
+ inputTokens: 0,
147
+ outputTokens: 100,
148
+ cacheCreationTokens: 0,
149
+ cacheReadTokens: 200000,
150
+ totalTokens: 200000,
151
+ contextWindow: 200000,
152
+ });
153
+
154
+ const free = segments.find(s => s.label === 'Free');
155
+ assert.equal(free, undefined);
156
+ });
157
+ });
@@ -0,0 +1,95 @@
1
+ import type { ContextUsage } from '../hooks/use_chat_stream';
2
+
3
+ /**
4
+ * Extract context usage from a stream event's usage object.
5
+ * Includes cache_creation_input_tokens and cache_read_input_tokens
6
+ * to accurately reflect total context window usage when prompt caching is active.
7
+ */
8
+ export const extractContextUsage = (usage: Record<string, unknown>): ContextUsage => {
9
+ const input = typeof usage.input_tokens === 'number' ? usage.input_tokens : 0;
10
+ const output = typeof usage.output_tokens === 'number' ? usage.output_tokens : 0;
11
+ const cacheCreation = typeof usage.cache_creation_input_tokens === 'number' ? usage.cache_creation_input_tokens : 0;
12
+ const cacheRead = typeof usage.cache_read_input_tokens === 'number' ? usage.cache_read_input_tokens : 0;
13
+ return {
14
+ inputTokens: input,
15
+ outputTokens: output,
16
+ cacheCreationTokens: cacheCreation,
17
+ cacheReadTokens: cacheRead,
18
+ // totalTokens = input context only (output doesn't count toward context window)
19
+ totalTokens: input + cacheCreation + cacheRead,
20
+ contextWindow: null,
21
+ };
22
+ };
23
+
24
+ /**
25
+ * Format a token count as a compact string (e.g. 45000 → "45k").
26
+ */
27
+ export const formatTokenCount = (tokens: number): string => {
28
+ if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
29
+ if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k`;
30
+ return String(tokens);
31
+ };
32
+
33
+ /** Segment for the color-coded context usage bar. */
34
+ export interface ContextSegment {
35
+ label: string;
36
+ tokens: number;
37
+ percent: number;
38
+ color: string;
39
+ }
40
+
41
+ /**
42
+ * Build color-coded segments for the context usage bar.
43
+ * Shows input context categories as portions of the context window:
44
+ * - Cached context (cache read — system prompt, tools, skills, prior messages)
45
+ * - New cached (cache creation — newly cached content this turn)
46
+ * - Uncached input (fresh input tokens, typically the latest user message)
47
+ * - Free space (remaining context window)
48
+ *
49
+ * Output tokens are NOT included — they don't consume context window space.
50
+ */
51
+ export const buildContextSegments = (usage: ContextUsage): ContextSegment[] => {
52
+ const contextWindow = usage.contextWindow ?? 200_000;
53
+ const inputTotal = usage.inputTokens + usage.cacheCreationTokens + usage.cacheReadTokens;
54
+ const free = Math.max(0, contextWindow - inputTotal);
55
+
56
+ const segments: ContextSegment[] = [];
57
+
58
+ if (usage.cacheReadTokens > 0) {
59
+ segments.push({
60
+ label: 'Cached context',
61
+ tokens: usage.cacheReadTokens,
62
+ percent: (usage.cacheReadTokens / contextWindow) * 100,
63
+ color: '#6366f1', // indigo
64
+ });
65
+ }
66
+
67
+ if (usage.cacheCreationTokens > 0) {
68
+ segments.push({
69
+ label: 'New cached',
70
+ tokens: usage.cacheCreationTokens,
71
+ percent: (usage.cacheCreationTokens / contextWindow) * 100,
72
+ color: '#8b5cf6', // violet
73
+ });
74
+ }
75
+
76
+ if (usage.inputTokens > 0) {
77
+ segments.push({
78
+ label: 'Uncached input',
79
+ tokens: usage.inputTokens,
80
+ percent: (usage.inputTokens / contextWindow) * 100,
81
+ color: '#06b6d4', // cyan
82
+ });
83
+ }
84
+
85
+ if (free > 0) {
86
+ segments.push({
87
+ label: 'Free',
88
+ tokens: free,
89
+ percent: (free / contextWindow) * 100,
90
+ color: 'transparent',
91
+ });
92
+ }
93
+
94
+ return segments;
95
+ };