@fnclaude/renderer 0.0.1 → 2.0.1

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -0
  3. package/package.json +47 -3
  4. package/src/App.test.tsx +130 -0
  5. package/src/App.tsx +321 -0
  6. package/src/__fixtures__/events.ts +66 -0
  7. package/src/__fixtures__/multi-turn.ndjson +11 -0
  8. package/src/__fixtures__/slash-command-turn.ndjson +3 -0
  9. package/src/__fixtures__/text-turn.ndjson +4 -0
  10. package/src/__fixtures__/tool-use-turn.ndjson +6 -0
  11. package/src/__fixtures__/unknown-slash-command-turn.ndjson +2 -0
  12. package/src/claude-process.test.ts +196 -0
  13. package/src/claude-process.ts +138 -0
  14. package/src/event-parser.test.ts +271 -0
  15. package/src/event-parser.ts +89 -0
  16. package/src/filter-state.test.ts +189 -0
  17. package/src/filter-state.ts +110 -0
  18. package/src/index.tsx +11 -0
  19. package/src/keybinds.test.ts +148 -0
  20. package/src/keybinds.ts +75 -0
  21. package/src/renderers/BashInput.test.tsx +61 -0
  22. package/src/renderers/BashInput.tsx +44 -0
  23. package/src/renderers/BashOutput.test.tsx +44 -0
  24. package/src/renderers/BashOutput.tsx +36 -0
  25. package/src/renderers/EditDiff.test.tsx +65 -0
  26. package/src/renderers/EditDiff.tsx +73 -0
  27. package/src/renderers/ErrorRenderer.test.tsx +25 -0
  28. package/src/renderers/ErrorRenderer.tsx +22 -0
  29. package/src/renderers/ReadContent.test.tsx +46 -0
  30. package/src/renderers/ReadContent.tsx +37 -0
  31. package/src/renderers/ReadInput.test.tsx +18 -0
  32. package/src/renderers/ReadInput.tsx +19 -0
  33. package/src/renderers/ResultRenderer.test.tsx +52 -0
  34. package/src/renderers/ResultRenderer.tsx +26 -0
  35. package/src/renderers/SystemInit.test.tsx +33 -0
  36. package/src/renderers/SystemInit.tsx +22 -0
  37. package/src/renderers/TaskNested.test.tsx +58 -0
  38. package/src/renderers/TaskNested.tsx +49 -0
  39. package/src/renderers/TextRenderer.test.tsx +37 -0
  40. package/src/renderers/TextRenderer.tsx +80 -0
  41. package/src/renderers/ThinkingRenderer.test.tsx +45 -0
  42. package/src/renderers/ThinkingRenderer.tsx +52 -0
  43. package/src/renderers/ToolResultRenderer.test.tsx +105 -0
  44. package/src/renderers/ToolResultRenderer.tsx +66 -0
  45. package/src/renderers/ToolUseRenderer.test.tsx +99 -0
  46. package/src/renderers/ToolUseRenderer.tsx +112 -0
  47. package/src/renderers/WriteContent.test.tsx +53 -0
  48. package/src/renderers/WriteContent.tsx +47 -0
  49. package/src/renderers/index.ts +50 -0
  50. package/src/renderers/summarize.ts +27 -0
  51. package/src/types/events.test.ts +43 -0
  52. package/src/types/events.ts +145 -0
@@ -0,0 +1,11 @@
1
+ {"type":"system","subtype":"init","cwd":"/home/tom/src/fnclaude-renderer@fnrhombus+agent-a47e07f49dcd06f94","session_id":"27ef1601-e3b8-470f-8c70-ab2fd9888b06","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","Monitor","NotebookEdit","PushNotification","Read","RemoteTrigger","ScheduleWakeup","SendMessage","ShareOnboardingGuide","Skill","TaskCreate","TaskGet","TaskList","TaskOutput","TaskStop","TaskUpdate","TeamCreate","TeamDelete","ToolSearch","WebFetch","WebSearch","Write"],"mcp_servers":[],"model":"claude-opus-4-7[1m]","permissionMode":"bypassPermissions","slash_commands":["done","getitdone","dots","check-usage","update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api","clear","compact","context","heapdump","init","review","security-review","usage-credits","extra-usage","usage","insights","goal","team-onboarding"],"apiKeySource":"none","claude_code_version":"2.1.144","output_style":"default","agents":["claude","Explore","general-purpose","Plan","statusline-setup"],"skills":["done","getitdone","dots","check-usage","update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api"],"plugins":[{"name":"claude-code-worktree-paths","path":"/home/tom/.claude/plugins/cache/fnrhombus-plugins/claude-code-worktree-paths/2.0.0","source":"claude-code-worktree-paths@fnrhombus-plugins"}],"analytics_disabled":false,"uuid":"ca679b84-cbe8-41cf-9d47-9aedfe9ad2cf","memory_paths":{"auto":"/home/tom/.claude/projects/-home-tom-src-fnclaude-renderer-fnrhombus/memory/"},"fast_mode_state":"off"}
2
+ {"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1779189600,"rateLimitType":"five_hour","overageStatus":"rejected","overageDisabledReason":"org_level_disabled","isUsingOverage":false},"uuid":"a9f65ca8-97ac-4076-9bdd-f661f78359b7","session_id":"27ef1601-e3b8-470f-8c70-ab2fd9888b06"}
3
+ {"type":"assistant","message":{"model":"claude-opus-4-7","id":"msg_01BJKEQWvicEMbQYuFvpSGxD","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"EvYCClkIDRgCKkBRKvnQ22VmB93LO/oyn6B1oeaqrvtdwBY9qCcq2m8JF9dLWoqNfV+n8fYYIqspFBKbE5u3gMc/R7NpMuJQ/zzQMg9jbGF1ZGUtb3B1cy00LTc4ABIMGYtCp0XOp70z9gbQGgwWba91nWNpbNXWQPoiMCzaj0pEniRQTCAFEPfCfQMgaMEYgKbDKE+0jFmsdD9IzJnjuxR8bC7/sm8I/PqEwCrKATJHtd5XmpywZppkdcLxy5m3OZx/6NA2vKWF59D7c8HNoEkPVT8uXKt9GIAXUCJErDy1/tS3zZBgXIGkqdsRQE2kDtMbpJ5gQw1HutXjeWOcM1QMXkousQhMDHomhKB/0npE2K8WdydpQaMLoebp0AQKC+SWdh47NJAbBT7u+jQlZIsVaImYkKDuEOlmiiEUh+10mPDzGMXBSkL1lUCWfNicsJyugzlik2Ss9ssPUBhHSUtdKw4mR7BVR45MOyBVP33WSNP0ZSk+kKQYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":15625,"cache_read_input_tokens":18675,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":15625},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"27ef1601-e3b8-470f-8c70-ab2fd9888b06","uuid":"6719c203-e388-428a-b1e4-25de020e86fa","request_id":"req_011CbBespu3Jkgva2Qfs6M5h"}
4
+ {"type":"assistant","message":{"model":"claude-opus-4-7","id":"msg_01BJKEQWvicEMbQYuFvpSGxD","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DEeiUjnGW5zCMBaGwtJ9Kh","name":"Write","input":{"file_path":"/home/tom/.claude/projects/-home-tom-src-fnclaude-renderer-fnrhombus/memory/remembered-number.md","content":"---\nname: remembered-number\ndescription: User asked to remember the number 42\nmetadata:\n type: project\n---\n\nThe user asked to remember the number 42.\n\n**Why:** Explicit user request to remember this value.\n**How to apply:** If the user asks \"what number did I tell you to remember?\" or similar, the answer is 42.\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":15625,"cache_read_input_tokens":18675,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":15625},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"27ef1601-e3b8-470f-8c70-ab2fd9888b06","uuid":"8264789a-d8ce-453c-bb04-5650f735f1fb","request_id":"req_011CbBespu3Jkgva2Qfs6M5h"}
5
+ {"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DEeiUjnGW5zCMBaGwtJ9Kh","type":"tool_result","content":"File created successfully at: /home/tom/.claude/projects/-home-tom-src-fnclaude-renderer-fnrhombus/memory/remembered-number.md (file state is current in your context — no need to Read it back)"}]},"parent_tool_use_id":null,"session_id":"27ef1601-e3b8-470f-8c70-ab2fd9888b06","uuid":"cdbff9d7-194a-4b1b-8068-e0b3337b812b","timestamp":"2026-05-19T09:00:19.950Z","tool_use_result":{"type":"create","filePath":"/home/tom/.claude/projects/-home-tom-src-fnclaude-renderer-fnrhombus/memory/remembered-number.md","content":"---\nname: remembered-number\ndescription: User asked to remember the number 42\nmetadata: \n node_type: memory\n type: project\n originSessionId: 27ef1601-e3b8-470f-8c70-ab2fd9888b06\n---\n\nThe user asked to remember the number 42.\n\n**Why:** Explicit user request to remember this value.\n**How to apply:** If the user asks \"what number did I tell you to remember?\" or similar, the answer is 42.\n","structuredPatch":[],"originalFile":null,"userModified":false}}
6
+ {"type":"assistant","message":{"model":"claude-opus-4-7","id":"msg_01BJKEQWvicEMbQYuFvpSGxD","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_0125Vp8P7JzfkA6Kr3QrmnZE","name":"Read","input":{"file_path":"/home/tom/.claude/projects/-home-tom-src-fnclaude-renderer-fnrhombus/memory/MEMORY.md"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":15625,"cache_read_input_tokens":18675,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":15625},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"27ef1601-e3b8-470f-8c70-ab2fd9888b06","uuid":"e57801f6-55d8-46b7-8008-a35df9d1d8eb","request_id":"req_011CbBespu3Jkgva2Qfs6M5h"}
7
+ {"type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"File does not exist. Note: your current working directory is /home/tom/src/fnclaude-renderer@fnrhombus+agent-a47e07f49dcd06f94.","is_error":true,"tool_use_id":"toolu_0125Vp8P7JzfkA6Kr3QrmnZE"}]},"parent_tool_use_id":null,"session_id":"27ef1601-e3b8-470f-8c70-ab2fd9888b06","uuid":"381f4671-88c2-4d61-ab9c-b52f9fbc28e2","timestamp":"2026-05-19T09:00:20.532Z","tool_use_result":"Error: File does not exist. Note: your current working directory is /home/tom/src/fnclaude-renderer@fnrhombus+agent-a47e07f49dcd06f94."}
8
+ {"type":"assistant","message":{"model":"claude-opus-4-7","id":"msg_01DvVr6gkjbq5NsQg7FU9kSu","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_015JJBSAArUEic6FD8HipCd4","name":"Write","input":{"file_path":"/home/tom/.claude/projects/-home-tom-src-fnclaude-renderer-fnrhombus/memory/MEMORY.md","content":"- [Remembered number](remembered-number.md) — user asked to remember the number 42\n"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":671,"cache_read_input_tokens":34300,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":671},"output_tokens":67,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"27ef1601-e3b8-470f-8c70-ab2fd9888b06","uuid":"951ff48a-b307-4db8-b23b-7e8af3791e8f","request_id":"req_011CbBetNhEJrsYKWX4AUk8K"}
9
+ {"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_015JJBSAArUEic6FD8HipCd4","type":"tool_result","content":"File created successfully at: /home/tom/.claude/projects/-home-tom-src-fnclaude-renderer-fnrhombus/memory/MEMORY.md (file state is current in your context — no need to Read it back)"}]},"parent_tool_use_id":null,"session_id":"27ef1601-e3b8-470f-8c70-ab2fd9888b06","uuid":"12b32c64-124e-4493-83c0-2cdf462c6ef8","timestamp":"2026-05-19T09:00:23.763Z","tool_use_result":{"type":"create","filePath":"/home/tom/.claude/projects/-home-tom-src-fnclaude-renderer-fnrhombus/memory/MEMORY.md","content":"- [Remembered number](remembered-number.md) — user asked to remember the number 42\n","structuredPatch":[],"originalFile":null,"userModified":false}}
10
+ {"type":"assistant","message":{"model":"claude-opus-4-7","id":"msg_01BDEB7efPq3RYycwnecqF2c","type":"message","role":"assistant","content":[{"type":"text","text":"42"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":243,"cache_read_input_tokens":34971,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":243},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"27ef1601-e3b8-470f-8c70-ab2fd9888b06","uuid":"18fea679-6337-4e12-835e-f7b0c9917bd6","request_id":"req_011CbBetcREL6niwAiuUnpXt"}
11
+ {"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":12854,"duration_api_ms":14498,"ttft_ms":4843,"num_turns":4,"result":"42","stop_reason":"end_turn","session_id":"27ef1601-e3b8-470f-8c70-ab2fd9888b06","total_cost_usd":0.16186275,"usage":{"input_tokens":8,"cache_creation_input_tokens":16539,"cache_read_input_tokens":87946,"output_tokens":559,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":16539,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":1,"output_tokens":5,"cache_read_input_tokens":34971,"cache_creation_input_tokens":243,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":243},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-haiku-4-5-20251001":{"inputTokens":441,"outputTokens":13,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.0005059999999999999,"contextWindow":200000,"maxOutputTokens":32000},"claude-opus-4-7[1m]":{"inputTokens":8,"outputTokens":559,"cacheReadInputTokens":87946,"cacheCreationInputTokens":16539,"webSearchRequests":0,"costUSD":0.16135675,"contextWindow":1000000,"maxOutputTokens":64000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"80ee8191-81c7-40c3-9dd7-4f000a36406e"}
@@ -0,0 +1,3 @@
1
+ {"type":"system","subtype":"init","cwd":"/home/tom/src/fnclaude-renderer@fnrhombus+agent-a47e07f49dcd06f94","session_id":"71f6c824-8978-4f51-8146-12ac68f81faf","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","Monitor","NotebookEdit","PushNotification","Read","RemoteTrigger","ScheduleWakeup","SendMessage","ShareOnboardingGuide","Skill","TaskCreate","TaskGet","TaskList","TaskOutput","TaskStop","TaskUpdate","TeamCreate","TeamDelete","ToolSearch","WebFetch","WebSearch","Write"],"mcp_servers":[],"model":"claude-opus-4-7[1m]","permissionMode":"bypassPermissions","slash_commands":["done","getitdone","dots","check-usage","update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api","clear","compact","context","heapdump","init","review","security-review","usage-credits","extra-usage","usage","insights","goal","team-onboarding"],"apiKeySource":"none","claude_code_version":"2.1.144","output_style":"default","agents":["claude","Explore","general-purpose","Plan","statusline-setup"],"skills":["done","getitdone","dots","check-usage","update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api"],"plugins":[{"name":"claude-code-worktree-paths","path":"/home/tom/.claude/plugins/cache/fnrhombus-plugins/claude-code-worktree-paths/2.0.0","source":"claude-code-worktree-paths@fnrhombus-plugins"}],"analytics_disabled":false,"uuid":"708db9b4-b131-4e45-91e6-e0501230f645","memory_paths":{"auto":"/home/tom/.claude/projects/-home-tom-src-fnclaude-renderer-fnrhombus/memory/"},"fast_mode_state":"off"}
2
+ {"type":"assistant","message":{"id":"e80f788f-d31f-44a7-aacd-77b8ee63da94","container":null,"model":"<synthetic>","role":"assistant","stop_details":null,"stop_reason":"stop_sequence","stop_sequence":"","type":"message","usage":{"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":null,"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":null,"iterations":null,"speed":null},"content":[{"type":"text","text":"You are currently using your subscription to power your Claude Code usage"}],"context_management":null},"parent_tool_use_id":null,"session_id":"71f6c824-8978-4f51-8146-12ac68f81faf","uuid":"78a39e62-3c83-4b6d-9c4c-e7a3d19e399d"}
3
+ {"type":"result","subtype":"success","is_error":false,"duration_ms":26,"duration_api_ms":0,"num_turns":0,"result":"You are currently using your subscription to power your Claude Code usage","stop_reason":null,"session_id":"71f6c824-8978-4f51-8146-12ac68f81faf","total_cost_usd":0,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{},"permission_denials":[],"fast_mode_state":"off","uuid":"375b7195-e704-47fa-a01a-1a2c461b35ff"}
@@ -0,0 +1,4 @@
1
+ {"type":"system","subtype":"init","cwd":"/home/tom/src/fnclaude-renderer@fnrhombus+agent-a47e07f49dcd06f94","session_id":"4fccc451-18ae-4b51-8af7-0773b6ec0782","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","Monitor","NotebookEdit","PushNotification","Read","RemoteTrigger","ScheduleWakeup","SendMessage","ShareOnboardingGuide","Skill","TaskCreate","TaskGet","TaskList","TaskOutput","TaskStop","TaskUpdate","TeamCreate","TeamDelete","ToolSearch","WebFetch","WebSearch","Write"],"mcp_servers":[],"model":"claude-opus-4-7[1m]","permissionMode":"bypassPermissions","slash_commands":["done","getitdone","dots","check-usage","update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api","clear","compact","context","heapdump","init","review","security-review","usage-credits","extra-usage","usage","insights","goal","team-onboarding"],"apiKeySource":"none","claude_code_version":"2.1.144","output_style":"default","agents":["claude","Explore","general-purpose","Plan","statusline-setup"],"skills":["done","getitdone","dots","check-usage","update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api"],"plugins":[{"name":"claude-code-worktree-paths","path":"/home/tom/.claude/plugins/cache/fnrhombus-plugins/claude-code-worktree-paths/2.0.0","source":"claude-code-worktree-paths@fnrhombus-plugins"}],"analytics_disabled":false,"uuid":"3353b9c2-fefd-4d1e-a712-819525fc58fd","memory_paths":{"auto":"/home/tom/.claude/projects/-home-tom-src-fnclaude-renderer-fnrhombus/memory/"},"fast_mode_state":"off"}
2
+ {"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1779189600,"rateLimitType":"five_hour","overageStatus":"rejected","overageDisabledReason":"org_level_disabled","isUsingOverage":false},"uuid":"fed98079-e644-4633-92f2-55699274cefd","session_id":"4fccc451-18ae-4b51-8af7-0773b6ec0782"}
3
+ {"type":"assistant","message":{"model":"claude-opus-4-7","id":"msg_012WRAoEU6bvxXL4KifvTuFZ","type":"message","role":"assistant","content":[{"type":"text","text":"Hi there friend"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":15630,"cache_read_input_tokens":18675,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":15630},"output_tokens":5,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"4fccc451-18ae-4b51-8af7-0773b6ec0782","uuid":"fdb973c5-09f1-4b11-9bb3-c130ca19a691","request_id":"req_011CbBepqYuCdeLezyedJPoa"}
4
+ {"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2352,"duration_api_ms":3536,"ttft_ms":2265,"num_turns":1,"result":"Hi there friend","stop_reason":"end_turn","session_id":"4fccc451-18ae-4b51-8af7-0773b6ec0782","total_cost_usd":0.10781400000000001,"usage":{"input_tokens":6,"cache_creation_input_tokens":15630,"cache_read_input_tokens":18675,"output_tokens":10,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":15630,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":6,"output_tokens":10,"cache_read_input_tokens":18675,"cache_creation_input_tokens":15630,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":15630},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-haiku-4-5-20251001":{"inputTokens":444,"outputTokens":13,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.000509,"contextWindow":200000,"maxOutputTokens":32000},"claude-opus-4-7[1m]":{"inputTokens":6,"outputTokens":10,"cacheReadInputTokens":18675,"cacheCreationInputTokens":15630,"webSearchRequests":0,"costUSD":0.10730500000000001,"contextWindow":1000000,"maxOutputTokens":64000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"216fbabf-dd6c-47e9-9329-74c48b9cff8d"}
@@ -0,0 +1,6 @@
1
+ {"type":"system","subtype":"init","cwd":"/home/tom/src/fnclaude-renderer@fnrhombus+agent-a47e07f49dcd06f94","session_id":"21548375-838d-4a0b-b5e2-bd8b66f16da0","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","Monitor","NotebookEdit","PushNotification","Read","RemoteTrigger","ScheduleWakeup","SendMessage","ShareOnboardingGuide","Skill","TaskCreate","TaskGet","TaskList","TaskOutput","TaskStop","TaskUpdate","TeamCreate","TeamDelete","ToolSearch","WebFetch","WebSearch","Write"],"mcp_servers":[],"model":"claude-opus-4-7[1m]","permissionMode":"bypassPermissions","slash_commands":["done","getitdone","dots","check-usage","update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api","clear","compact","context","heapdump","init","review","security-review","usage-credits","extra-usage","usage","insights","goal","team-onboarding"],"apiKeySource":"none","claude_code_version":"2.1.144","output_style":"default","agents":["claude","Explore","general-purpose","Plan","statusline-setup"],"skills":["done","getitdone","dots","check-usage","update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api"],"plugins":[{"name":"claude-code-worktree-paths","path":"/home/tom/.claude/plugins/cache/fnrhombus-plugins/claude-code-worktree-paths/2.0.0","source":"claude-code-worktree-paths@fnrhombus-plugins"}],"analytics_disabled":false,"uuid":"36e7e37d-6997-4ff7-99d3-6e2a6ba02550","memory_paths":{"auto":"/home/tom/.claude/projects/-home-tom-src-fnclaude-renderer-fnrhombus/memory/"},"fast_mode_state":"off"}
2
+ {"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1779189600,"rateLimitType":"five_hour","overageStatus":"rejected","overageDisabledReason":"org_level_disabled","isUsingOverage":false},"uuid":"5728cf2d-aaa7-4f51-967a-44339ecfd3ca","session_id":"21548375-838d-4a0b-b5e2-bd8b66f16da0"}
3
+ {"type":"assistant","message":{"model":"claude-opus-4-7","id":"msg_01DP8fNkG5jYxtVsnSf9ziWm","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01UtPPEinrH1vYAXrzbGrS3y","name":"Bash","input":{"command":"echo hello_tool_test","description":"Echo test string"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":15630,"cache_read_input_tokens":18675,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":15630},"output_tokens":5,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"21548375-838d-4a0b-b5e2-bd8b66f16da0","uuid":"577095eb-8631-464c-9d70-1ac3cfdd7430","request_id":"req_011CbBeqQDg5T2vXSFpLjTSB"}
4
+ {"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01UtPPEinrH1vYAXrzbGrS3y","type":"tool_result","content":"hello_tool_test","is_error":false}]},"parent_tool_use_id":null,"session_id":"21548375-838d-4a0b-b5e2-bd8b66f16da0","uuid":"d4a4322f-3ae1-40dd-abd2-817ec0b54df8","timestamp":"2026-05-19T08:59:44.686Z","tool_use_result":{"stdout":"hello_tool_test","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}
5
+ {"type":"assistant","message":{"model":"claude-opus-4-7","id":"msg_01RcKpMFWehHWKcuutpXCFEb","type":"message","role":"assistant","content":[{"type":"text","text":"hello_tool_test"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":121,"cache_read_input_tokens":34305,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":121},"output_tokens":7,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"21548375-838d-4a0b-b5e2-bd8b66f16da0","uuid":"6626b0a0-ceaf-4bde-9746-7893bfe4164b","request_id":"req_011CbBeqj9xi6mzu3TrkdLeT"}
6
+ {"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":6762,"duration_api_ms":7076,"ttft_ms":3259,"num_turns":2,"result":"hello_tool_test","stop_reason":"end_turn","session_id":"21548375-838d-4a0b-b5e2-bd8b66f16da0","total_cost_usd":0.12816775,"usage":{"input_tokens":7,"cache_creation_input_tokens":15751,"cache_read_input_tokens":52980,"output_tokens":107,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":15751,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":1,"output_tokens":11,"cache_read_input_tokens":34305,"cache_creation_input_tokens":121,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":121},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-haiku-4-5-20251001":{"inputTokens":444,"outputTokens":16,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.0005239999999999999,"contextWindow":200000,"maxOutputTokens":32000},"claude-opus-4-7[1m]":{"inputTokens":7,"outputTokens":107,"cacheReadInputTokens":52980,"cacheCreationInputTokens":15751,"webSearchRequests":0,"costUSD":0.12764375,"contextWindow":1000000,"maxOutputTokens":64000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"64058024-128a-4af4-b73f-c7ce7a6ce74f"}
@@ -0,0 +1,2 @@
1
+ {"type":"system","subtype":"init","cwd":"/home/tom/src/fnclaude-renderer@fnrhombus+agent-a47e07f49dcd06f94","session_id":"6aa97dd9-5b26-489f-ac5c-87978420be75","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","Monitor","NotebookEdit","PushNotification","Read","RemoteTrigger","ScheduleWakeup","SendMessage","ShareOnboardingGuide","Skill","TaskCreate","TaskGet","TaskList","TaskOutput","TaskStop","TaskUpdate","TeamCreate","TeamDelete","ToolSearch","WebFetch","WebSearch","Write"],"mcp_servers":[],"model":"claude-opus-4-7[1m]","permissionMode":"bypassPermissions","slash_commands":["done","getitdone","dots","check-usage","update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api","clear","compact","context","heapdump","init","review","security-review","usage-credits","extra-usage","usage","insights","goal","team-onboarding"],"apiKeySource":"none","claude_code_version":"2.1.144","output_style":"default","agents":["claude","Explore","general-purpose","Plan","statusline-setup"],"skills":["done","getitdone","dots","check-usage","update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api"],"plugins":[{"name":"claude-code-worktree-paths","path":"/home/tom/.claude/plugins/cache/fnrhombus-plugins/claude-code-worktree-paths/2.0.0","source":"claude-code-worktree-paths@fnrhombus-plugins"}],"analytics_disabled":false,"uuid":"7eb1461b-03fd-458c-9932-482168a53bf2","memory_paths":{"auto":"/home/tom/.claude/projects/-home-tom-src-fnclaude-renderer-fnrhombus/memory/"},"fast_mode_state":"off"}
2
+ {"type":"result","subtype":"success","is_error":false,"duration_ms":26,"duration_api_ms":0,"num_turns":0,"result":"Unknown command: /foo. Did you mean /loop?","stop_reason":null,"session_id":"6aa97dd9-5b26-489f-ac5c-87978420be75","total_cost_usd":0,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{},"permission_denials":[],"fast_mode_state":"off","uuid":"f974f8e2-ced3-4403-b326-cacb63e93302"}
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Tests for the claude subprocess driver.
3
+ *
4
+ * Strategy: inject a mock spawn function rather than invoking the real claude
5
+ * binary. This makes the tests hermetic and fast — no network calls, no
6
+ * dependency on a live claude installation, and safe to run in CI where the
7
+ * binary may not be present or authenticated.
8
+ *
9
+ * The mock spawn writes a real fixture file's bytes to a fake stdout pipe
10
+ * (ReadableStream), then closes it, simulating what claude would emit. The
11
+ * driver reads from that stream via the parser. We also test that
12
+ * sendUserTurn serialises the UserTurn shape correctly by inspecting what
13
+ * was written to the mock stdin.
14
+ *
15
+ * The mock surface: `subscribeToClaude` accepts an optional `spawnFn`
16
+ * parameter of type `SpawnFn`. Callers that omit it get the real
17
+ * `Bun.spawn`-backed implementation; test code passes a mock.
18
+ */
19
+ import { describe, expect, test } from "bun:test";
20
+ import { readFileSync } from "node:fs";
21
+ import { join } from "node:path";
22
+ import type { SpawnFn } from "./claude-process.ts";
23
+ import { subscribeToClaude } from "./claude-process.ts";
24
+ import type { AssistantEvent, ResultEvent, SystemEvent } from "./types/events.ts";
25
+
26
+ const fixtureDir = join(import.meta.dir, "__fixtures__");
27
+
28
+ function makeFixtureSpawn(fixtureName: string): {
29
+ spawnFn: SpawnFn;
30
+ getStdinWrites: () => string[];
31
+ } {
32
+ const stdinWrites: string[] = [];
33
+
34
+ const spawnFn: SpawnFn = (_cmd, _opts) => {
35
+ const fixtureBytes = readFileSync(join(fixtureDir, fixtureName));
36
+
37
+ // Fake stdout: a ReadableStream that immediately emits the fixture.
38
+ const stdout = new ReadableStream<Uint8Array>({
39
+ start(controller) {
40
+ controller.enqueue(new Uint8Array(fixtureBytes));
41
+ controller.close();
42
+ },
43
+ });
44
+
45
+ // Fake stdin: a WritableStream that records written strings.
46
+ const stdin = new WritableStream<Uint8Array>({
47
+ write(chunk) {
48
+ stdinWrites.push(new TextDecoder().decode(chunk));
49
+ },
50
+ });
51
+
52
+ return {
53
+ stdout,
54
+ stdin,
55
+ exited: Promise.resolve(0),
56
+ kill: () => undefined,
57
+ };
58
+ };
59
+
60
+ return { spawnFn, getStdinWrites: () => stdinWrites };
61
+ }
62
+
63
+ describe("subscribeToClaude — text turn", () => {
64
+ test("yields system, assistant, result events from fixture", async () => {
65
+ const { spawnFn } = makeFixtureSpawn("text-turn.ndjson");
66
+ const { events } = subscribeToClaude({ spawnFn });
67
+
68
+ const collected = [];
69
+ for await (const ev of events) {
70
+ collected.push(ev);
71
+ }
72
+
73
+ const types = collected.map((e) => e.type);
74
+ expect(types).toContain("system");
75
+ expect(types).toContain("assistant");
76
+ expect(types).toContain("result");
77
+ });
78
+
79
+ test("system event has session_id and subtype init", async () => {
80
+ const { spawnFn } = makeFixtureSpawn("text-turn.ndjson");
81
+ const { events } = subscribeToClaude({ spawnFn });
82
+
83
+ for await (const ev of events) {
84
+ if (ev.type === "system") {
85
+ const sys = ev as SystemEvent;
86
+ expect(sys.subtype).toBe("init");
87
+ expect(typeof sys.session_id).toBe("string");
88
+ return;
89
+ }
90
+ }
91
+ throw new Error("no system event");
92
+ });
93
+
94
+ test("result event is success", async () => {
95
+ const { spawnFn } = makeFixtureSpawn("text-turn.ndjson");
96
+ const { events } = subscribeToClaude({ spawnFn });
97
+
98
+ for await (const ev of events) {
99
+ if (ev.type === "result") {
100
+ const res = ev as ResultEvent;
101
+ expect(res.subtype).toBe("success");
102
+ expect(res.is_error).toBe(false);
103
+ return;
104
+ }
105
+ }
106
+ throw new Error("no result event");
107
+ });
108
+ });
109
+
110
+ describe("subscribeToClaude — sendUserTurn", () => {
111
+ test("sendUserTurn writes valid UserTurn JSON followed by newline", async () => {
112
+ const { spawnFn, getStdinWrites } = makeFixtureSpawn("text-turn.ndjson");
113
+ const { events, sendUserTurn } = subscribeToClaude({ spawnFn });
114
+
115
+ sendUserTurn("hello world");
116
+
117
+ // Drain events so the mock finishes.
118
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
119
+ for await (const _ev of events) {
120
+ /* drain */
121
+ }
122
+
123
+ const writes = getStdinWrites();
124
+ expect(writes.length).toBeGreaterThan(0);
125
+ const allWritten = writes.join("");
126
+ // Must end with a newline.
127
+ expect(allWritten.endsWith("\n")).toBe(true);
128
+ // Must parse as valid JSON.
129
+ const parsed = JSON.parse(allWritten.trim()) as Record<string, unknown>;
130
+ expect(parsed.type).toBe("user");
131
+ const msg = parsed.message as Record<string, unknown>;
132
+ expect(msg.role).toBe("user");
133
+ const content = msg.content as Array<Record<string, unknown>>;
134
+ expect(content[0]?.type).toBe("text");
135
+ expect(content[0]?.text).toBe("hello world");
136
+ });
137
+ });
138
+
139
+ describe("subscribeToClaude — close", () => {
140
+ test("close() resolves and process exits cleanly", async () => {
141
+ const { spawnFn } = makeFixtureSpawn("text-turn.ndjson");
142
+ const { events, close } = subscribeToClaude({ spawnFn });
143
+
144
+ // Drain so we know the mock stream closed.
145
+ for await (const _ev of events) {
146
+ /* drain */
147
+ }
148
+
149
+ const exitCode = await close();
150
+ expect(exitCode).toBe(0);
151
+ });
152
+ });
153
+
154
+ describe("subscribeToClaude — tool-use fixture", () => {
155
+ test("emits assistant with tool_use and user with tool_result", async () => {
156
+ const { spawnFn } = makeFixtureSpawn("tool-use-turn.ndjson");
157
+ const { events } = subscribeToClaude({ spawnFn });
158
+
159
+ let sawToolUse = false;
160
+ let sawToolResult = false;
161
+
162
+ for await (const ev of events) {
163
+ if (ev.type === "assistant") {
164
+ const ast = ev as AssistantEvent;
165
+ for (const block of ast.message.content) {
166
+ if (block.type === "tool_use") sawToolUse = true;
167
+ }
168
+ }
169
+ if (ev.type === "user") {
170
+ const content = Array.isArray(ev.message.content) ? ev.message.content : [];
171
+ for (const block of content) {
172
+ if (block.type === "tool_result") sawToolResult = true;
173
+ }
174
+ }
175
+ }
176
+
177
+ expect(sawToolUse).toBe(true);
178
+ expect(sawToolResult).toBe(true);
179
+ });
180
+ });
181
+
182
+ describe("subscribeToClaude — slash command fixture", () => {
183
+ test("assistant model is <synthetic>", async () => {
184
+ const { spawnFn } = makeFixtureSpawn("slash-command-turn.ndjson");
185
+ const { events } = subscribeToClaude({ spawnFn });
186
+
187
+ for await (const ev of events) {
188
+ if (ev.type === "assistant") {
189
+ const ast = ev as AssistantEvent;
190
+ expect(ast.message.model).toBe("<synthetic>");
191
+ return;
192
+ }
193
+ }
194
+ throw new Error("no assistant event");
195
+ });
196
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Claude subprocess driver.
3
+ *
4
+ * Spawns `claude --print --verbose --input-format stream-json
5
+ * --output-format stream-json` via Bun.spawn and exposes an async-iterable
6
+ * event stream plus a `sendUserTurn` writer.
7
+ *
8
+ * Design notes:
9
+ * - stdin MUST be a pipe, not a file redirect. Passing a file to claude's
10
+ * stdin silently produces no output and exits 0 — a painful gotcha
11
+ * documented in docs/stream-json-findings.md.
12
+ * - `SpawnFn` is injectable so tests can pipe fixture bytes through the real
13
+ * parser without touching the live binary. Production callers omit it.
14
+ * - `close()` closes stdin (signals EOF to claude) and awaits the process
15
+ * exit. The process exits cleanly ~250ms after stdin close per the findings.
16
+ */
17
+ import { parseNdjsonStream } from "./event-parser.ts";
18
+ import type { ClaudeEvent, UserTurn } from "./types/events.ts";
19
+
20
+ export interface SpawnResult {
21
+ stdout: ReadableStream<Uint8Array>;
22
+ stdin: WritableStream<Uint8Array>;
23
+ exited: Promise<number>;
24
+ kill: () => void;
25
+ }
26
+
27
+ export type SpawnFn = (cmd: string[], opts: { cwd?: string }) => SpawnResult;
28
+
29
+ export interface SubscribeOptions {
30
+ /** Working directory for the claude process. Defaults to process.cwd(). */
31
+ cwd?: string;
32
+ /** Extra CLI arguments appended after the required flags. */
33
+ extraArgs?: string[];
34
+ /**
35
+ * Injectable spawn function — omit in production, pass a mock in tests.
36
+ * See module header comment for rationale.
37
+ */
38
+ spawnFn?: SpawnFn;
39
+ }
40
+
41
+ export interface ClaudeSubscription {
42
+ events: AsyncIterable<ClaudeEvent>;
43
+ sendUserTurn: (text: string) => void;
44
+ close: () => Promise<number>;
45
+ }
46
+
47
+ const REQUIRED_ARGS = [
48
+ "--print",
49
+ "--verbose",
50
+ "--input-format",
51
+ "stream-json",
52
+ "--output-format",
53
+ "stream-json",
54
+ ];
55
+
56
+ export function subscribeToClaude(opts: SubscribeOptions = {}): ClaudeSubscription {
57
+ const { cwd, extraArgs = [], spawnFn } = opts;
58
+
59
+ const spawn = spawnFn ?? defaultSpawn;
60
+ const spawnOpts = cwd !== undefined ? { cwd } : {};
61
+ const proc = spawn(["claude", ...REQUIRED_ARGS, ...extraArgs], spawnOpts);
62
+
63
+ const encoder = new TextEncoder();
64
+ let stdinWriter: WritableStreamDefaultWriter<Uint8Array> | null = proc.stdin.getWriter();
65
+
66
+ function sendUserTurn(text: string): void {
67
+ if (stdinWriter === null) return;
68
+ const turn: UserTurn = {
69
+ type: "user",
70
+ message: {
71
+ role: "user",
72
+ content: [{ type: "text", text }],
73
+ },
74
+ };
75
+ const line = `${JSON.stringify(turn)}\n`;
76
+ // Fire-and-forget write; errors surface as unhandled promise rejections
77
+ // which is acceptable for a stream writer (the process exiting will close
78
+ // the stream cleanly).
79
+ stdinWriter.write(encoder.encode(line)).catch(() => undefined);
80
+ }
81
+
82
+ async function close(): Promise<number> {
83
+ if (stdinWriter !== null) {
84
+ try {
85
+ await stdinWriter.close();
86
+ } catch {
87
+ // Already closed — ignore.
88
+ }
89
+ stdinWriter = null;
90
+ }
91
+ return proc.exited;
92
+ }
93
+
94
+ return {
95
+ events: parseNdjsonStream(proc.stdout),
96
+ sendUserTurn,
97
+ close,
98
+ };
99
+ }
100
+
101
+ function defaultSpawn(cmd: string[], opts: { cwd?: string }): SpawnResult {
102
+ const bunOpts =
103
+ opts.cwd !== undefined
104
+ ? {
105
+ stdin: "pipe" as const,
106
+ stdout: "pipe" as const,
107
+ stderr: "inherit" as const,
108
+ cwd: opts.cwd,
109
+ }
110
+ : { stdin: "pipe" as const, stdout: "pipe" as const, stderr: "inherit" as const };
111
+ const proc = Bun.spawn(cmd, bunOpts);
112
+
113
+ // Wrap Bun's ReadableStream and WritableStream in the standard Web streams
114
+ // API shapes that SpawnResult declares.
115
+ const stdout = proc.stdout as ReadableStream<Uint8Array>;
116
+ // Bun.spawn returns a FileSink for stdin when stdin: "pipe".
117
+ // Wrap it in a WritableStream so the surface is consistent.
118
+ const sink = proc.stdin;
119
+
120
+ const stdin = new WritableStream<Uint8Array>({
121
+ async write(chunk) {
122
+ await sink.write(chunk);
123
+ },
124
+ async close() {
125
+ await sink.end();
126
+ },
127
+ abort() {
128
+ sink.end();
129
+ },
130
+ });
131
+
132
+ return {
133
+ stdout,
134
+ stdin,
135
+ exited: proc.exited,
136
+ kill: () => proc.kill(),
137
+ };
138
+ }