@cuongtran001/kanna 1.63.2 → 1.64.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 (211) hide show
  1. package/README.md +5 -9
  2. package/dist/client/assets/{BoardsPage-BJalB-bc.js → BoardsPage--7VsnbXM.js} +1 -1
  3. package/dist/client/assets/BoardsRoutePage-Hkl_oQv0.js +1 -0
  4. package/dist/client/assets/ChatPage-h-VTkkkt.js +93 -0
  5. package/dist/client/assets/{CronJobRow-CPbO_jwU.js → CronJobRow-eb86NFF7.js} +1 -1
  6. package/dist/client/assets/{CronJobsPage-Ck3SNgGi.js → CronJobsPage-B0NHa3ss.js} +1 -1
  7. package/dist/client/assets/{FilePreviewSheet-XEuUtDPn.js → FilePreviewSheet-DOEK5SRs.js} +2 -2
  8. package/dist/client/assets/{FlowSurface-BCbeMTJe.js → FlowSurface-DPotbbWr.js} +1 -1
  9. package/dist/client/assets/{GenUIBlock-BJy_dfs-.js → GenUIBlock-BNZfT-Vk.js} +2 -2
  10. package/dist/client/assets/{KannaSocketProvider-D7Lzlt35.js → KannaSocketProvider-DtKR165f.js} +1 -1
  11. package/dist/client/assets/{LocalProjectsPage-9LJkQjhT.js → LocalProjectsPage-DNLMYHFL.js} +1 -1
  12. package/dist/client/assets/SettingsPage-B3bF5Xse.js +9 -0
  13. package/dist/client/assets/{StackBoardsRoutePage-t_EaFNG4.js → StackBoardsRoutePage-DplbkiEr.js} +1 -1
  14. package/dist/client/assets/{ToolCallMessage-RbF9XT2B.js → ToolCallMessage-BJQRDH0E.js} +1 -1
  15. package/dist/client/assets/{VChartSurface-CKuPelsn.js → VChartSurface-gDrFdsRD.js} +1 -1
  16. package/dist/client/assets/{WorkflowsPage-B6qUmQA3.js → WorkflowsPage-RDNDgswE.js} +1 -1
  17. package/dist/client/assets/{WorkflowsSection-DxO0S4An.js → WorkflowsSection-8tzLFcmi.js} +1 -1
  18. package/dist/client/assets/browser-uLao0xGx.js +3 -0
  19. package/dist/client/assets/{chart-renderer-DjYZvNxh.js → chart-renderer-Bp9ekEsh.js} +1 -1
  20. package/dist/client/assets/{chatSounds-FD2DQzF6.js → chatSounds-4LPsoFE0.js} +1 -1
  21. package/dist/client/assets/chevron-left-BXDenZrj.js +1 -0
  22. package/dist/client/assets/{clipboard.adapter-GN9nnYak.js → clipboard.adapter-V4RFuBQg.js} +1 -1
  23. package/dist/client/assets/{columnStyle-DjA7hA9k.js → columnStyle-CPsUAD1y.js} +1 -1
  24. package/dist/client/assets/{createScopedStore-BoLB9Rhc.js → createScopedStore-vi_qBrnv.js} +1 -1
  25. package/dist/client/assets/{dist-CimtOoGK.js → dist-CQv2I5w0.js} +1 -1
  26. package/dist/client/assets/editor-icons-DSEWhYdt.js +1 -0
  27. package/dist/client/assets/{ellipsis-Cd7sO0LO.js → ellipsis-Dw447mDf.js} +1 -1
  28. package/dist/client/assets/{flow-kinds-CW2TU_83.js → flow-kinds-Dctbq8tY.js} +1 -1
  29. package/dist/client/assets/{folder-C7sXCaHj.js → folder-K_gAJ1DI.js} +1 -1
  30. package/dist/client/assets/{formatters-wSVyvDFZ.js → formatters-BGORekd-.js} +1 -1
  31. package/dist/client/assets/{genui-EzBIAYFE.js → genui-buyzerHE.js} +1 -1
  32. package/dist/client/assets/{globe-xmRpmpma.js → globe-D0uBqjk3.js} +1 -1
  33. package/dist/client/assets/{host-DCaA9W-Z.js → host-Bmj1x1Ur.js} +1 -1
  34. package/dist/client/assets/{index-CDqjh7nt.js → index-BioMVpYd.js} +7 -7
  35. package/dist/client/assets/index-CxDE9VOy.css +1 -0
  36. package/dist/client/assets/{info-D4MMvZFe.js → info-XTOdbyHd.js} +1 -1
  37. package/dist/client/assets/{input-C4fvDiQ4.js → input-BdIYegqO.js} +1 -1
  38. package/dist/client/assets/{lexicalToReact-sPUyOWk6.js → lexicalToReact-DqgmPzHx.js} +3 -3
  39. package/dist/client/assets/{log-aTaDVF_e.js → log-CKbWpKMT.js} +1 -1
  40. package/dist/client/assets/{monitor-_CRKcV1h.js → monitor-CGybOpir.js} +1 -1
  41. package/dist/client/assets/{pencil-fNBeUUCf.js → pencil-B8yLmanR.js} +1 -1
  42. package/dist/client/assets/{pendingActionsStore-COTh4Ztq.js → pendingActionsStore-CdwEvP-X.js} +1 -1
  43. package/dist/client/assets/{play-BAmkJfn6.js → play-BmOQO8Wo.js} +1 -1
  44. package/dist/client/assets/{plus-c75CkNEO.js → plus-wpoGLqmM.js} +1 -1
  45. package/dist/client/assets/{popover-CHM4skzh.js → popover-B9W07k-v.js} +1 -1
  46. package/dist/client/assets/{pushClient-BJH_ITRB.js → pushClient-C3BFEIhM.js} +1 -1
  47. package/dist/client/assets/runDetached-B3NSxNTV.js +1 -0
  48. package/dist/client/assets/{segmented-control-DNOwRupT.js → segmented-control-D1eTj6vZ.js} +1 -1
  49. package/dist/client/assets/{select-dLHiAls6.js → select-rChS1ZvA.js} +1 -1
  50. package/dist/client/assets/{settings-header-button-CGKCzFyk.js → settings-header-button-D0i3JySg.js} +1 -1
  51. package/dist/client/assets/{sidebarPendingActions-Dyp80Lv_.js → sidebarPendingActions-BKojWYqD.js} +1 -1
  52. package/dist/client/assets/{square-pen-B1IxmScM.js → square-pen-C_M6NWE6.js} +1 -1
  53. package/dist/client/assets/{state-mark-BhTZfcYe.js → state-mark-D71tSKL1.js} +1 -1
  54. package/dist/client/assets/{status-pill-CwhJhaMy.js → status-pill-IuanKijZ.js} +1 -1
  55. package/dist/client/assets/{terminal-Bnj7qFGw.js → terminal-ChU65qN4.js} +1 -1
  56. package/dist/client/assets/{timer.adapter-DHt2zwsL.js → timer.adapter-DxStl6YQ.js} +1 -1
  57. package/dist/client/assets/{tools-3gJcj3hK.js → tools-DcwSZn_Y.js} +1 -1
  58. package/dist/client/assets/{triangle-alert-CWrc0CIO.js → triangle-alert-CVNaunqf.js} +1 -1
  59. package/dist/client/assets/useAppGlobalState-MUKjQL55.js +1 -0
  60. package/dist/client/assets/useKannaState-DiOIS_Z6.js +5 -0
  61. package/dist/client/assets/useTheme-DpVQlqks.js +1 -0
  62. package/dist/client/index.html +35 -35
  63. package/package.json +5 -2
  64. package/src/server/agent-coordinator-types.ts +0 -6
  65. package/src/server/agent-coordinator.ts +6 -45
  66. package/src/server/agent.compaction.test.ts +1 -13
  67. package/src/server/agent.oauth-account-info.test.ts +0 -3
  68. package/src/server/agent.oauth-release.test.ts +0 -2
  69. package/src/server/agent.oauth-rotation.test.ts +0 -9
  70. package/src/server/agent.oauth-token-share-cap.test.ts +0 -1
  71. package/src/server/agent.openrouter-model.test.ts +0 -2
  72. package/src/server/agent.openrouter-watchdog.test.ts +0 -3
  73. package/src/server/agent.sdk-workflow-register.test.ts +2 -64
  74. package/src/server/agent.test.ts +1 -319
  75. package/src/server/app-settings.test.ts +17 -20
  76. package/src/server/app-settings.ts +22 -37
  77. package/src/server/auth.test.ts +5 -1
  78. package/src/server/auto-continue/e2e.test.ts +0 -1
  79. package/src/server/background-task-output-registry.test.ts +1 -1
  80. package/src/server/background-task-output-registry.ts +1 -1
  81. package/src/server/claude-cancel-handler.test.ts +0 -58
  82. package/src/server/claude-cancel-handler.ts +1 -17
  83. package/src/server/claude-harness-stream.test.ts +134 -0
  84. package/src/server/{claude-pty/jsonl-path.test.ts → claude-projects-path.adapter.test.ts} +1 -19
  85. package/src/server/{claude-pty/jsonl-path.adapter.ts → claude-projects-path.adapter.ts} +0 -11
  86. package/src/server/claude-send-command.test.ts +0 -1
  87. package/src/server/claude-session-config-helpers.test.ts +1 -90
  88. package/src/server/claude-session-config-helpers.ts +1 -38
  89. package/src/server/claude-session-error-handler.test.ts +0 -1
  90. package/src/server/claude-session-lifecycle.test.ts +2 -36
  91. package/src/server/claude-session-lifecycle.ts +2 -8
  92. package/src/server/claude-session-runner.test.ts +8 -36
  93. package/src/server/claude-session-runner.ts +1 -26
  94. package/src/server/claude-session-spawner.test.ts +5 -64
  95. package/src/server/claude-session-spawner.ts +49 -115
  96. package/src/server/claude-session-start.test.ts +1 -14
  97. package/src/server/claude-session-start.ts +0 -8
  98. package/src/server/claude-session-state-queries.test.ts +1 -2
  99. package/src/server/claude-session-state.test.ts +0 -1
  100. package/src/server/claude-session-state.ts +0 -4
  101. package/src/server/claude-subagent-wiring.test.ts +2 -68
  102. package/src/server/claude-subagent-wiring.ts +3 -50
  103. package/src/server/claude-turn-starter-types.ts +0 -3
  104. package/src/server/claude-turn-starter.test.ts +0 -28
  105. package/src/server/claude-turn-starter.ts +0 -5
  106. package/src/server/event-store.test.ts +0 -2
  107. package/src/server/events.ts +1 -2
  108. package/src/server/harness-types.ts +1 -2
  109. package/src/server/http-api-routes.ts +27 -103
  110. package/src/server/http-dispatcher.test.ts +7 -9
  111. package/src/server/http-dispatcher.ts +16 -3
  112. package/src/server/http-file-response.ts +92 -0
  113. package/src/server/import-subagent-drill.e2e.test.ts +1 -1
  114. package/src/server/imported-subagents-dir.test.ts +1 -1
  115. package/src/server/imported-subagents-dir.ts +1 -1
  116. package/src/server/kanna-mcp.test.ts +0 -75
  117. package/src/server/kanna-mcp.ts +1 -47
  118. package/src/server/server.test.ts +5 -7
  119. package/src/server/server.ts +3 -22
  120. package/src/server/subagent-orchestrator.test.ts +0 -1
  121. package/src/server/subagent-provider-run.test.ts +48 -10
  122. package/src/server/subagent-provider-run.ts +12 -3
  123. package/src/server/tool-callback.test.ts +1 -1
  124. package/src/server/tus-uploads.adapter.ts +142 -0
  125. package/src/server/uploads.adapter.ts +133 -9
  126. package/src/server/uploads.test.ts +295 -120
  127. package/src/server/uploads.ts +1 -1
  128. package/src/server/ws-router-agent-ctrl.test.ts +0 -45
  129. package/src/server/ws-router-agent-ctrl.ts +1 -31
  130. package/src/server/ws-router-broadcast.ts +0 -27
  131. package/src/server/ws-router-defaults.test.ts +2 -3
  132. package/src/server/ws-router-defaults.ts +2 -3
  133. package/src/server/ws-router-envelope.ts +0 -15
  134. package/src/server/ws-router-observability.test.ts +1 -1
  135. package/src/server/ws-router-settings.test.ts +2 -2
  136. package/src/server/ws-router.test.ts +2 -2
  137. package/src/server/ws-router.ts +0 -8
  138. package/src/shared/app-settings-types.ts +9 -20
  139. package/src/shared/claude-api-error-classification.ts +1 -1
  140. package/src/shared/protocol.ts +1 -11
  141. package/src/shared/settings/uploads.test.ts +8 -0
  142. package/src/shared/settings/uploads.ts +2 -2
  143. package/dist/client/assets/BoardsRoutePage-BTTjJX-Y.js +0 -1
  144. package/dist/client/assets/ChatPage-DH8wtQyD.js +0 -93
  145. package/dist/client/assets/SettingsPage-CKrZ6dGQ.js +0 -9
  146. package/dist/client/assets/chevron-left-CJ0qv9kF.js +0 -1
  147. package/dist/client/assets/editor-icons-CONDQZof.js +0 -1
  148. package/dist/client/assets/index-C-UYaDk6.css +0 -1
  149. package/dist/client/assets/runDetached-DqQ3k2ji.js +0 -1
  150. package/dist/client/assets/useAppGlobalState-DKOGrO5l.js +0 -1
  151. package/dist/client/assets/useKannaState-D7R_-dM6.js +0 -5
  152. package/dist/client/assets/useTheme-B57LIKyx.js +0 -1
  153. package/src/server/agent.pty-rotation.test.ts +0 -322
  154. package/src/server/claude-pty/auth.test.ts +0 -38
  155. package/src/server/claude-pty/auth.ts +0 -17
  156. package/src/server/claude-pty/claude-session-registry.adapter.ts +0 -68
  157. package/src/server/claude-pty/claude-session-registry.test.ts +0 -149
  158. package/src/server/claude-pty/driver.test.ts +0 -1153
  159. package/src/server/claude-pty/driver.ts +0 -829
  160. package/src/server/claude-pty/env.test.ts +0 -79
  161. package/src/server/claude-pty/env.ts +0 -20
  162. package/src/server/claude-pty/jsonl-to-event.test.ts +0 -882
  163. package/src/server/claude-pty/jsonl-to-event.ts +0 -357
  164. package/src/server/claude-pty/parity-matrix.test.ts +0 -437
  165. package/src/server/claude-pty/pid-registry.adapter.ts +0 -164
  166. package/src/server/claude-pty/pid-registry.test.ts +0 -178
  167. package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +0 -20
  168. package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +0 -32
  169. package/src/server/claude-pty/pty-instance-registry.test.ts +0 -208
  170. package/src/server/claude-pty/pty-instance-registry.ts +0 -172
  171. package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +0 -103
  172. package/src/server/claude-pty/pty-memory-sampler.adapter.ts +0 -86
  173. package/src/server/claude-pty/pty-process.adapter.ts +0 -60
  174. package/src/server/claude-pty/pty-process.test.ts +0 -49
  175. package/src/server/claude-pty/resolve-binary.adapter.ts +0 -106
  176. package/src/server/claude-pty/resolve-binary.test.ts +0 -118
  177. package/src/server/claude-pty/runtime-dir.adapter.ts +0 -19
  178. package/src/server/claude-pty/sandbox/detect.adapter.ts +0 -17
  179. package/src/server/claude-pty/sandbox/detect.test.ts +0 -17
  180. package/src/server/claude-pty/sandbox/platform.test.ts +0 -35
  181. package/src/server/claude-pty/sandbox/platform.ts +0 -27
  182. package/src/server/claude-pty/sandbox/preflight-io.adapter.ts +0 -14
  183. package/src/server/claude-pty/sandbox/preflight.test.ts +0 -69
  184. package/src/server/claude-pty/sandbox/preflight.ts +0 -44
  185. package/src/server/claude-pty/sandbox/profile-linux.adapter.ts +0 -63
  186. package/src/server/claude-pty/sandbox/profile-linux.test.ts +0 -90
  187. package/src/server/claude-pty/sandbox/profile-macos.adapter.ts +0 -72
  188. package/src/server/claude-pty/sandbox/profile-macos.test.ts +0 -68
  189. package/src/server/claude-pty/sandbox/wrap.adapter.ts +0 -58
  190. package/src/server/claude-pty/sandbox/wrap.test.ts +0 -82
  191. package/src/server/claude-pty/settings-writer.adapter.ts +0 -23
  192. package/src/server/claude-pty/settings-writer.test.ts +0 -22
  193. package/src/server/claude-pty/smoke-test-io.adapter.ts +0 -28
  194. package/src/server/claude-pty/smoke-test.test.ts +0 -199
  195. package/src/server/claude-pty/smoke-test.ts +0 -182
  196. package/src/server/claude-pty/tui-control.test.ts +0 -300
  197. package/src/server/claude-pty/tui-control.ts +0 -174
  198. package/src/server/claude-pty/tui-source.adapter.test.ts +0 -374
  199. package/src/server/claude-pty/tui-source.adapter.ts +0 -294
  200. package/src/server/http-server.adapter.test.ts +0 -18
  201. package/src/server/http-server.adapter.ts +0 -39
  202. package/src/server/kanna-mcp-http.test.ts +0 -274
  203. package/src/server/kanna-mcp-http.ts +0 -194
  204. package/src/server/kanna-mcp-tools/ask-user-question.test.ts +0 -108
  205. package/src/server/kanna-mcp-tools/ask-user-question.ts +0 -55
  206. package/src/server/kanna-mcp-tools/exit-plan-mode.test.ts +0 -61
  207. package/src/server/kanna-mcp-tools/exit-plan-mode.ts +0 -49
  208. package/src/server/mcp-zod-compat.adapter.ts +0 -20
  209. package/src/shared/pty-instance.ts +0 -43
  210. /package/src/server/{claude-pty/output-ring.test.ts → output-ring.test.ts} +0 -0
  211. /package/src/server/{claude-pty/output-ring.ts → output-ring.ts} +0 -0
@@ -1,174 +0,0 @@
1
- import type { PtyProcess } from "./pty-process.adapter"
2
- import type { OutputRing } from "./output-ring"
3
-
4
- export const TRUST_DIALOG_MARKER = "trust this folder"
5
- export const DEV_CHANNELS_DIALOG_MARKER = "local channel development"
6
- export const TUI_READY_MARKER = "❯ "
7
- export const TUI_READY_HARD_CAP_DEFAULT_MS = 3000
8
- export const TUI_READY_QUIET_DEFAULT_MS = 300
9
-
10
- function stripAnsi(s: string): string {
11
- return s
12
- .replace(/\x1b\[[0-9;]*[A-Za-z]/g, " ")
13
- .replace(/\x1b./g, "")
14
- .replace(/\u00a0/g, " ")
15
- }
16
-
17
- export interface WaitForTuiReadyOpts {
18
- hardCapMs?: number
19
- pollMs?: number
20
- quietPeriodMs?: number
21
- }
22
-
23
- export async function waitForTuiReady(
24
- ring: OutputRing,
25
- opts: WaitForTuiReadyOpts = {},
26
- ): Promise<"marker" | "timeout"> {
27
- const hardCapMs = opts.hardCapMs ?? TUI_READY_HARD_CAP_DEFAULT_MS
28
- const pollMs = opts.pollMs ?? 50
29
- const quietPeriodMs = opts.quietPeriodMs ?? TUI_READY_QUIET_DEFAULT_MS
30
- const start = Date.now()
31
- while (true) {
32
- if (stripAnsi(ring.tail()).includes(TUI_READY_MARKER)) {
33
- await waitForRingQuiet(ring, { quietMs: quietPeriodMs, pollMs, deadline: start + hardCapMs })
34
- return "marker"
35
- }
36
- if (Date.now() - start >= hardCapMs) return "timeout"
37
- await new Promise((r) => setTimeout(r, pollMs))
38
- }
39
- }
40
-
41
- async function waitForRingQuiet(
42
- ring: OutputRing,
43
- opts: { quietMs: number; pollMs: number; deadline: number },
44
- ): Promise<void> {
45
- if (opts.quietMs <= 0) return
46
- let lastLength = ring.tail().length
47
- let quietStart = Date.now()
48
- while (Date.now() - quietStart < opts.quietMs) {
49
- if (Date.now() >= opts.deadline) return
50
- await new Promise((r) => setTimeout(r, opts.pollMs))
51
- const currentLength = ring.tail().length
52
- if (currentLength !== lastLength) {
53
- lastLength = currentLength
54
- quietStart = Date.now()
55
- }
56
- }
57
- }
58
-
59
- export async function dismissTrustDialogIfPresent(
60
- pty: PtyProcess,
61
- ring: OutputRing,
62
- ): Promise<boolean> {
63
- if (!stripAnsi(ring.tail()).includes(TRUST_DIALOG_MARKER)) return false
64
- await pty.sendInput("\r")
65
- return true
66
- }
67
-
68
- export async function dismissDevChannelsDialogIfPresent(
69
- pty: PtyProcess,
70
- ring: OutputRing,
71
- ): Promise<boolean> {
72
- if (!stripAnsi(ring.tail()).includes(DEV_CHANNELS_DIALOG_MARKER)) return false
73
- await pty.sendInput("\r")
74
- return true
75
- }
76
-
77
- export interface WaitForTuiReadyWithTrustDismissOpts {
78
- hardCapMs?: number
79
- pollMs?: number
80
- quietPeriodMs?: number
81
- }
82
-
83
- export async function waitForTuiReadyWithTrustDismiss(
84
- pty: PtyProcess,
85
- ring: OutputRing,
86
- opts: WaitForTuiReadyWithTrustDismissOpts = {},
87
- ): Promise<"ready" | "timeout"> {
88
- const hardCapMs = opts.hardCapMs ?? 15_000
89
- const pollMs = opts.pollMs ?? 50
90
- const quietPeriodMs = opts.quietPeriodMs ?? TUI_READY_QUIET_DEFAULT_MS
91
- const start = Date.now()
92
- let trustDismissed = false
93
- let postDismissOffset = 0
94
-
95
- while (Date.now() - start < hardCapMs) {
96
- const raw = ring.tail()
97
- if (!trustDismissed && stripAnsi(raw).includes(TRUST_DIALOG_MARKER)) {
98
- postDismissOffset = raw.length
99
- await pty.sendInput("\r")
100
- trustDismissed = true
101
- } else {
102
- const checkWindow = trustDismissed ? raw.slice(postDismissOffset) : raw
103
- if (stripAnsi(checkWindow).includes(TUI_READY_MARKER)) {
104
- await waitForRingQuiet(ring, { quietMs: quietPeriodMs, pollMs, deadline: start + hardCapMs })
105
- return "ready"
106
- }
107
- }
108
- await new Promise((r) => setTimeout(r, pollMs))
109
- }
110
- return "timeout"
111
- }
112
-
113
- export async function waitForTuiReadyDismissingDialogs(
114
- pty: PtyProcess,
115
- ring: OutputRing,
116
- opts: { hardCapMs?: number; pollMs?: number } = {},
117
- ): Promise<"marker" | "timeout"> {
118
- const hardCapMs = opts.hardCapMs ?? TUI_READY_HARD_CAP_DEFAULT_MS + 5_000
119
- const pollMs = opts.pollMs ?? 50
120
- const start = Date.now()
121
- let trustDone = false
122
- let devDone = false
123
- let postDismissOffset = 0
124
- while (Date.now() - start < hardCapMs) {
125
- const raw = ring.tail()
126
- const view = stripAnsi(raw)
127
- if (!trustDone && view.includes(TRUST_DIALOG_MARKER)) {
128
- postDismissOffset = raw.length
129
- await pty.sendInput("\r")
130
- trustDone = true
131
- await new Promise((r) => setTimeout(r, pollMs))
132
- continue
133
- }
134
- if (!devDone && view.includes(DEV_CHANNELS_DIALOG_MARKER)) {
135
- postDismissOffset = raw.length
136
- await pty.sendInput("\r")
137
- devDone = true
138
- await new Promise((r) => setTimeout(r, pollMs))
139
- continue
140
- }
141
- if (devDone && stripAnsi(raw.slice(postDismissOffset)).includes(TUI_READY_MARKER)) {
142
- return "marker"
143
- }
144
- await new Promise((r) => setTimeout(r, pollMs))
145
- }
146
- return "timeout"
147
- }
148
-
149
- export interface SendUserPromptOpts {
150
- commitTimeoutMs?: number
151
- pollMs?: number
152
- }
153
-
154
- export async function sendUserPrompt(
155
- pty: PtyProcess,
156
- ring: OutputRing,
157
- text: string,
158
- opts: SendUserPromptOpts = {},
159
- ): Promise<void> {
160
- const commitTimeoutMs = opts.commitTimeoutMs ?? 2_000
161
- const pollMs = opts.pollMs ?? 10
162
- const baseline = ring.tail().length
163
- await pty.sendInput(`\x1b[200~${text}\x1b[201~`)
164
- const deadline = Date.now() + commitTimeoutMs
165
- while (Date.now() < deadline) {
166
- if (ring.tail().length > baseline) break
167
- await new Promise((r) => setTimeout(r, pollMs))
168
- }
169
- await pty.sendInput("\r")
170
- }
171
-
172
- export async function sendExitCommand(pty: PtyProcess): Promise<void> {
173
- await pty.sendInput("/exit\r")
174
- }
@@ -1,374 +0,0 @@
1
- import { describe, expect, test, beforeEach, afterEach } from "bun:test"
2
- import { mkdtemp, rm, writeFile, mkdir, appendFile } from "node:fs/promises"
3
- import { tmpdir } from "node:os"
4
- import path from "node:path"
5
- import {
6
- findLatestTranscript,
7
- startTranscriptStream,
8
- waitForResultEntry,
9
- } from "./tui-source.adapter"
10
- import { encodeCwd } from "./jsonl-path.adapter"
11
-
12
- let workHome: string
13
- let projectDir: string
14
-
15
- beforeEach(async () => {
16
- workHome = await mkdtemp(path.join(tmpdir(), "kanna-tui-source-"))
17
- projectDir = path.join(workHome, ".claude", "projects", "fake-cwd")
18
- await mkdir(projectDir, { recursive: true })
19
- })
20
-
21
- afterEach(async () => {
22
- await rm(workHome, { recursive: true, force: true })
23
- })
24
-
25
- describe("findLatestTranscript", () => {
26
- test("returns null when project dir empty", async () => {
27
- const result = await findLatestTranscript(projectDir)
28
- expect(result).toBeNull()
29
- })
30
-
31
- test("returns path of newest .jsonl file", async () => {
32
- const fileA = path.join(projectDir, "aaa.jsonl")
33
- const fileB = path.join(projectDir, "bbb.jsonl")
34
- await writeFile(fileA, "{}\n")
35
- await new Promise((r) => setTimeout(r, 20))
36
- await writeFile(fileB, "{}\n")
37
- const result = await findLatestTranscript(projectDir)
38
- expect(result).toBe(fileB)
39
- })
40
-
41
- test("ignores non-.jsonl files", async () => {
42
- await writeFile(path.join(projectDir, "notes.txt"), "hello")
43
- const result = await findLatestTranscript(projectDir)
44
- expect(result).toBeNull()
45
- })
46
-
47
- test("returns null when project dir does not exist", async () => {
48
- const result = await findLatestTranscript(path.join(workHome, "no-such-dir"))
49
- expect(result).toBeNull()
50
- })
51
-
52
- test("minMtimeMs filter skips JSONLs older than the floor", async () => {
53
- const stale = path.join(projectDir, "stale.jsonl")
54
- const fresh = path.join(projectDir, "fresh.jsonl")
55
- await writeFile(stale, "{}\n")
56
- await new Promise((r) => setTimeout(r, 20))
57
- const floor = Date.now()
58
- await new Promise((r) => setTimeout(r, 20))
59
- await writeFile(fresh, "{}\n")
60
- const result = await findLatestTranscript(projectDir, { minMtimeMs: floor })
61
- expect(result).toBe(fresh)
62
- })
63
-
64
- test("minMtimeMs returns null when every JSONL is older than the floor", async () => {
65
- const a = path.join(projectDir, "a.jsonl")
66
- await writeFile(a, "{}\n")
67
- await new Promise((r) => setTimeout(r, 20))
68
- const result = await findLatestTranscript(projectDir, { minMtimeMs: Date.now() })
69
- expect(result).toBeNull()
70
- })
71
- })
72
-
73
- describe("startTranscriptStream (dir-watch)", () => {
74
- test("picks up file written after stream start", async () => {
75
- const stream = await startTranscriptStream({ projectDir, firstFileTimeoutMs: 2000 })
76
- const filePath = path.join(projectDir, "new.jsonl")
77
- setTimeout(() => writeFile(filePath, '{"type":"hello"}\n'), 100)
78
- const resolved = await stream.filePath
79
- expect(resolved).toBe(filePath)
80
- stream.close()
81
- }, 5000)
82
-
83
- test("opens existing file when present at start", async () => {
84
- const filePath = path.join(projectDir, "existing.jsonl")
85
- await writeFile(filePath, '{"type":"hello"}\n')
86
- const stream = await startTranscriptStream({ projectDir, firstFileTimeoutMs: 2000 })
87
- const resolved = await stream.filePath
88
- expect(resolved).toBe(filePath)
89
- stream.close()
90
- }, 5000)
91
-
92
- test("emits complete lines as they are appended", async () => {
93
- const filePath = path.join(projectDir, "stream.jsonl")
94
- await writeFile(filePath, '{"type":"one"}\n')
95
- const stream = await startTranscriptStream({ projectDir, firstFileTimeoutMs: 2000 })
96
- const iter = stream.lines[Symbol.asyncIterator]()
97
- const first = await iter.next()
98
- expect(first.value).toBe('{"type":"one"}')
99
- setTimeout(() => writeFile(filePath, '{"type":"one"}\n{"type":"two"}\n'), 100)
100
- const second = await iter.next()
101
- expect(second.value).toBe('{"type":"two"}')
102
- stream.close()
103
- }, 5000)
104
-
105
- test("holds partial line across writes", async () => {
106
- const filePath = path.join(projectDir, "partial.jsonl")
107
- await writeFile(filePath, '{"type":')
108
- const stream = await startTranscriptStream({
109
- projectDir,
110
- firstFileTimeoutMs: 2000,
111
- pollIntervalMs: 30,
112
- })
113
- const iter = stream.lines[Symbol.asyncIterator]()
114
- let resolved = false
115
- const firstPromise = iter.next().then((r: IteratorResult<string>) => { resolved = true; return r })
116
- await new Promise((r) => setTimeout(r, 200))
117
- expect(resolved).toBe(false)
118
- await writeFile(filePath, '{"type":"one"}\n')
119
- const first = await firstPromise
120
- expect(first.value).toBe('{"type":"one"}')
121
- stream.close()
122
- }, 5000)
123
-
124
- test("times out when no file appears within firstFileTimeoutMs", async () => {
125
- const stream = await startTranscriptStream({ projectDir, firstFileTimeoutMs: 200 })
126
- await expect(stream.filePath).rejects.toThrow(/transcript file did not appear/)
127
- stream.close()
128
- }, 5000)
129
-
130
- test("knownFilePath skips dir-watch", async () => {
131
- const filePath = path.join(projectDir, "known.jsonl")
132
- await writeFile(filePath, '{"type":"hello"}\n')
133
- const stream = await startTranscriptStream({
134
- projectDir,
135
- knownFilePath: filePath,
136
- firstFileTimeoutMs: 500,
137
- })
138
- const resolved = await stream.filePath
139
- expect(resolved).toBe(filePath)
140
- stream.close()
141
- }, 5000)
142
- })
143
-
144
- describe("startTranscriptStream (poll follower)", () => {
145
- test("emits lines via the tail-poll", async () => {
146
- const stream = await startTranscriptStream({
147
- projectDir,
148
- pollIntervalMs: 30,
149
- firstFileTimeoutMs: 2000,
150
- })
151
- const filePath = path.join(projectDir, "poll.jsonl")
152
- setTimeout(() => writeFile(filePath, '{"type":"polled"}\n'), 100)
153
- const iter = stream.lines[Symbol.asyncIterator]()
154
- const first = await iter.next()
155
- expect(first.value).toBe('{"type":"polled"}')
156
- stream.close()
157
- }, 5000)
158
-
159
- test("close() stops the poll timer (no leak)", async () => {
160
- const filePath = path.join(projectDir, "leak.jsonl")
161
- await writeFile(filePath, '{"type":"one"}\n')
162
- const stream = await startTranscriptStream({
163
- projectDir,
164
- knownFilePath: filePath,
165
- pollIntervalMs: 20,
166
- firstFileTimeoutMs: 500,
167
- })
168
- const iter = stream.lines[Symbol.asyncIterator]()
169
- expect((await iter.next()).value).toBe('{"type":"one"}')
170
- stream.close()
171
- await appendFile(filePath, '{"type":"two"}\n')
172
- await new Promise((r) => setTimeout(r, 120))
173
- const next = await Promise.race([
174
- iter.next(),
175
- new Promise<{ value: undefined; done: true }>((r) => setTimeout(() => r({ value: undefined, done: true }), 150)),
176
- ])
177
- expect(next.done).toBe(true)
178
- }, 5000)
179
- })
180
-
181
- describe("startTranscriptStream (registry resolution)", () => {
182
- test("registry resolves but JSONL missing — never falls back to other JSONL in same dir", async () => {
183
- const pid = 99001
184
- const realCwd = await mkdtemp(path.join(workHome, "real-cwd-"))
185
- const encoded = encodeCwd(realCwd)
186
- const ownProjectDir = path.join(workHome, ".claude", "projects", encoded)
187
- await mkdir(ownProjectDir, { recursive: true })
188
- const sessionsDir = path.join(workHome, ".claude", "sessions")
189
- await mkdir(sessionsDir, { recursive: true })
190
- const ownSessionId = "our-session-aaaaaaaaaa"
191
- await writeFile(
192
- path.join(sessionsDir, `${pid}.json`),
193
- JSON.stringify({ pid, sessionId: ownSessionId, cwd: realCwd, kind: "interactive", startedAt: Date.now() }),
194
- )
195
- const strangerFile = path.join(ownProjectDir, "stranger-session.jsonl")
196
- await writeFile(strangerFile, '{"type":"assistant","message":{"content":[{"type":"text","text":"NOT OURS"}]}}\n')
197
-
198
- const stream = await startTranscriptStream({
199
- projectDir: ownProjectDir,
200
- homeDir: workHome,
201
- claudeChildPid: pid,
202
- sessionRegistryTimeoutMs: 300,
203
- firstFileTimeoutMs: 5_000,
204
- pollIntervalMs: 20,
205
- })
206
-
207
- const beforeWrite = await Promise.race([
208
- stream.filePath
209
- .then((fp) => ({ kind: "resolved" as const, fp }))
210
- .catch((err: Error) => ({ kind: "rejected" as const, err })),
211
- new Promise<{ kind: "pending" }>((r) => setTimeout(() => r({ kind: "pending" }), 600)),
212
- ])
213
- expect(beforeWrite.kind).toBe("pending")
214
-
215
- const ownFile = path.join(ownProjectDir, `${ownSessionId}.jsonl`)
216
- await writeFile(ownFile, '{"type":"assistant","message":{"content":[{"type":"text","text":"ours"}]}}\n')
217
- const resolved = await stream.filePath
218
- expect(resolved).toBe(ownFile)
219
- expect(resolved).not.toBe(strangerFile)
220
- stream.close()
221
- }, 5000)
222
-
223
- test("registry resolved but JSONL never appears — filePath rejects after firstFileTimeoutMs", async () => {
224
- const pid = 99003
225
- const realCwd = await mkdtemp(path.join(workHome, "real-cwd-"))
226
- const encoded = encodeCwd(realCwd)
227
- const ownProjectDir = path.join(workHome, ".claude", "projects", encoded)
228
- await mkdir(ownProjectDir, { recursive: true })
229
- const sessionsDir = path.join(workHome, ".claude", "sessions")
230
- await mkdir(sessionsDir, { recursive: true })
231
- const ownSessionId = "our-session-cccccccccc"
232
- await writeFile(
233
- path.join(sessionsDir, `${pid}.json`),
234
- JSON.stringify({ pid, sessionId: ownSessionId, cwd: realCwd, kind: "interactive", startedAt: Date.now() }),
235
- )
236
-
237
- const stream = await startTranscriptStream({
238
- projectDir: ownProjectDir,
239
- homeDir: workHome,
240
- claudeChildPid: pid,
241
- sessionRegistryTimeoutMs: 300,
242
- firstFileTimeoutMs: 150,
243
- pollIntervalMs: 20,
244
- })
245
-
246
- const start = Date.now()
247
- await expect(stream.filePath).rejects.toThrow(/did not appear in 150ms/)
248
- const elapsed = Date.now() - start
249
- expect(elapsed).toBeLessThan(1_500)
250
- stream.close()
251
- }, 5000)
252
-
253
- test("registry resolves with existing JSONL — returns registry path immediately", async () => {
254
- const pid = 99002
255
- const realCwd = await mkdtemp(path.join(workHome, "real-cwd-"))
256
- const encoded = encodeCwd(realCwd)
257
- const ownProjectDir = path.join(workHome, ".claude", "projects", encoded)
258
- await mkdir(ownProjectDir, { recursive: true })
259
- const sessionsDir = path.join(workHome, ".claude", "sessions")
260
- await mkdir(sessionsDir, { recursive: true })
261
- const ownSessionId = "our-session-bbbbbbbbbb"
262
- const ownFile = path.join(ownProjectDir, `${ownSessionId}.jsonl`)
263
- await writeFile(ownFile, "{}\n")
264
- await writeFile(
265
- path.join(sessionsDir, `${pid}.json`),
266
- JSON.stringify({ pid, sessionId: ownSessionId, cwd: realCwd, kind: "interactive", startedAt: Date.now() }),
267
- )
268
- await new Promise((r) => setTimeout(r, 20))
269
- await writeFile(path.join(ownProjectDir, "stranger.jsonl"), "{}\n")
270
-
271
- const stream = await startTranscriptStream({
272
- projectDir: ownProjectDir,
273
- homeDir: workHome,
274
- claudeChildPid: pid,
275
- })
276
- const resolved = await stream.filePath
277
- expect(resolved).toBe(ownFile)
278
- stream.close()
279
- }, 5000)
280
- })
281
-
282
- describe("startTranscriptStream (rapid turn-end appends)", () => {
283
- test("delivers all rows appended in rapid succession after stream setup", async () => {
284
- const filePath = path.join(projectDir, "watched.jsonl")
285
- await writeFile(filePath, '{"type":"system","subtype":"init"}\n')
286
- const stream = await startTranscriptStream({
287
- projectDir,
288
- knownFilePath: filePath,
289
- firstFileTimeoutMs: 500,
290
- })
291
- const iter = stream.lines[Symbol.asyncIterator]()
292
- const first = await iter.next()
293
- expect(first.value).toContain('"system"')
294
-
295
- await appendFile(filePath, '{"type":"assistant","message":{"content":[{"type":"text","text":"a"}]}}\n')
296
- await appendFile(filePath, '{"type":"assistant","message":{"content":[{"type":"text","text":"b"}]}}\n')
297
- await appendFile(filePath, '{"type":"system","subtype":"turn_duration","durationMs":42}\n')
298
-
299
- const collected: string[] = []
300
- const deadline = Date.now() + 2_000
301
- while (collected.length < 3 && Date.now() < deadline) {
302
- const nxt = await Promise.race([
303
- iter.next(),
304
- new Promise<{ value: undefined; done: true }>((r) => setTimeout(() => r({ value: undefined, done: true }), 1_500)),
305
- ])
306
- if (nxt.done) break
307
- collected.push(nxt.value)
308
- }
309
- expect(collected.length).toBe(3)
310
- expect(collected[0]).toContain('"a"')
311
- expect(collected[1]).toContain('"b"')
312
- expect(collected[2]).toContain("turn_duration")
313
- stream.close()
314
- }, 8000)
315
- })
316
-
317
- describe("waitForResultEntry", () => {
318
- test("resolves on first result line", async () => {
319
- const filePath = path.join(projectDir, "result.jsonl")
320
- await writeFile(filePath, '{"type":"system"}\n{"type":"assistant"}\n')
321
- const stream = await startTranscriptStream({ projectDir, firstFileTimeoutMs: 2000 })
322
- setTimeout(() => writeFile(filePath, '{"type":"system"}\n{"type":"assistant"}\n{"type":"result","subtype":"success"}\n'), 100)
323
- const entry = await waitForResultEntry(stream, { timeoutMs: 2000 })
324
- expect(entry.parsed.type).toBe("result")
325
- stream.close()
326
- }, 5000)
327
-
328
- test("resolves on assistant row with terminal stop_reason (new CLI format)", async () => {
329
- const filePath = path.join(projectDir, "stop-reason.jsonl")
330
- await writeFile(filePath, '{"type":"user","message":{"role":"user","content":"hi"}}\n')
331
- const stream = await startTranscriptStream({ projectDir, firstFileTimeoutMs: 2000 })
332
- const endTurnRow = JSON.stringify({
333
- type: "assistant",
334
- message: { id: "msg_end", role: "assistant", stop_reason: "end_turn", content: [{ type: "text", text: "done" }] },
335
- })
336
- setTimeout(() => writeFile(
337
- filePath,
338
- `{"type":"user","message":{"role":"user","content":"hi"}}\n${endTurnRow}\n`,
339
- ), 100)
340
- const entry = await waitForResultEntry(stream, { timeoutMs: 2000 })
341
- expect(entry.parsed.type).toBe("assistant")
342
- stream.close()
343
- }, 5000)
344
-
345
- test("does NOT resolve on assistant row with tool_use stop_reason", async () => {
346
- const filePath = path.join(projectDir, "stop-reason-tooluse.jsonl")
347
- const toolUseRow = JSON.stringify({
348
- type: "assistant",
349
- message: { id: "msg_t", role: "assistant", stop_reason: "tool_use", content: [{ type: "tool_use", id: "t1", name: "Read", input: {} }] },
350
- })
351
- await writeFile(filePath, `${toolUseRow}\n`)
352
- const stream = await startTranscriptStream({ projectDir, firstFileTimeoutMs: 2000 })
353
- await expect(waitForResultEntry(stream, { timeoutMs: 300 })).rejects.toThrow(/timed out/i)
354
- stream.close()
355
- }, 5000)
356
-
357
- test("rejects on abort signal", async () => {
358
- const filePath = path.join(projectDir, "abort.jsonl")
359
- await writeFile(filePath, '{"type":"system"}\n')
360
- const stream = await startTranscriptStream({ projectDir, firstFileTimeoutMs: 2000 })
361
- const ctrl = new AbortController()
362
- setTimeout(() => ctrl.abort(), 50)
363
- await expect(waitForResultEntry(stream, { signal: ctrl.signal })).rejects.toThrow(/aborted/i)
364
- stream.close()
365
- }, 5000)
366
-
367
- test("rejects on timeout", async () => {
368
- const filePath = path.join(projectDir, "timeout.jsonl")
369
- await writeFile(filePath, '{"type":"system"}\n')
370
- const stream = await startTranscriptStream({ projectDir, firstFileTimeoutMs: 2000 })
371
- await expect(waitForResultEntry(stream, { timeoutMs: 100 })).rejects.toThrow(/timed out/i)
372
- stream.close()
373
- }, 5000)
374
- })