mstro-app 0.4.3 → 0.4.4

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 (306) hide show
  1. package/dist/server/cli/headless/claude-invoker-process.d.ts +11 -0
  2. package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -0
  3. package/dist/server/cli/headless/claude-invoker-process.js +140 -0
  4. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -0
  5. package/dist/server/cli/headless/claude-invoker-stall.d.ts +40 -0
  6. package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -0
  7. package/dist/server/cli/headless/claude-invoker-stall.js +98 -0
  8. package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -0
  9. package/dist/server/cli/headless/claude-invoker-stream.d.ts +44 -0
  10. package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -0
  11. package/dist/server/cli/headless/claude-invoker-stream.js +276 -0
  12. package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -0
  13. package/dist/server/cli/headless/claude-invoker-tools.d.ts +21 -0
  14. package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -0
  15. package/dist/server/cli/headless/claude-invoker-tools.js +137 -0
  16. package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -0
  17. package/dist/server/cli/headless/claude-invoker.d.ts +6 -4
  18. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  19. package/dist/server/cli/headless/claude-invoker.js +10 -807
  20. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  21. package/dist/server/cli/headless/haiku-assessments.d.ts +62 -0
  22. package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -0
  23. package/dist/server/cli/headless/haiku-assessments.js +281 -0
  24. package/dist/server/cli/headless/haiku-assessments.js.map +1 -0
  25. package/dist/server/cli/headless/headless-logger.d.ts +3 -2
  26. package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
  27. package/dist/server/cli/headless/headless-logger.js +28 -5
  28. package/dist/server/cli/headless/headless-logger.js.map +1 -1
  29. package/dist/server/cli/headless/native-timeout-detector.d.ts +44 -0
  30. package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -0
  31. package/dist/server/cli/headless/native-timeout-detector.js +99 -0
  32. package/dist/server/cli/headless/native-timeout-detector.js.map +1 -0
  33. package/dist/server/cli/headless/stall-assessor.d.ts +2 -110
  34. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  35. package/dist/server/cli/headless/stall-assessor.js +65 -457
  36. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  37. package/dist/server/cli/improvisation-attachments.d.ts +21 -0
  38. package/dist/server/cli/improvisation-attachments.d.ts.map +1 -0
  39. package/dist/server/cli/improvisation-attachments.js +116 -0
  40. package/dist/server/cli/improvisation-attachments.js.map +1 -0
  41. package/dist/server/cli/improvisation-retry.d.ts +52 -0
  42. package/dist/server/cli/improvisation-retry.d.ts.map +1 -0
  43. package/dist/server/cli/improvisation-retry.js +434 -0
  44. package/dist/server/cli/improvisation-retry.js.map +1 -0
  45. package/dist/server/cli/improvisation-session-manager.d.ts +10 -266
  46. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  47. package/dist/server/cli/improvisation-session-manager.js +117 -1079
  48. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  49. package/dist/server/cli/improvisation-types.d.ts +86 -0
  50. package/dist/server/cli/improvisation-types.d.ts.map +1 -0
  51. package/dist/server/cli/improvisation-types.js +10 -0
  52. package/dist/server/cli/improvisation-types.js.map +1 -0
  53. package/dist/server/cli/prompt-builders.d.ts +68 -0
  54. package/dist/server/cli/prompt-builders.d.ts.map +1 -0
  55. package/dist/server/cli/prompt-builders.js +312 -0
  56. package/dist/server/cli/prompt-builders.js.map +1 -0
  57. package/dist/server/index.js +33 -212
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/mcp/bouncer-haiku.d.ts +10 -0
  60. package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -0
  61. package/dist/server/mcp/bouncer-haiku.js +152 -0
  62. package/dist/server/mcp/bouncer-haiku.js.map +1 -0
  63. package/dist/server/mcp/bouncer-integration.d.ts +3 -4
  64. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  65. package/dist/server/mcp/bouncer-integration.js +50 -196
  66. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  67. package/dist/server/mcp/security-analysis.d.ts +38 -0
  68. package/dist/server/mcp/security-analysis.d.ts.map +1 -0
  69. package/dist/server/mcp/security-analysis.js +183 -0
  70. package/dist/server/mcp/security-analysis.js.map +1 -0
  71. package/dist/server/mcp/security-audit.d.ts +1 -1
  72. package/dist/server/mcp/security-audit.d.ts.map +1 -1
  73. package/dist/server/mcp/security-patterns.d.ts +1 -25
  74. package/dist/server/mcp/security-patterns.d.ts.map +1 -1
  75. package/dist/server/mcp/security-patterns.js +55 -260
  76. package/dist/server/mcp/security-patterns.js.map +1 -1
  77. package/dist/server/server-setup.d.ts +22 -0
  78. package/dist/server/server-setup.d.ts.map +1 -0
  79. package/dist/server/server-setup.js +101 -0
  80. package/dist/server/server-setup.js.map +1 -0
  81. package/dist/server/services/file-explorer-ops.d.ts +24 -0
  82. package/dist/server/services/file-explorer-ops.d.ts.map +1 -0
  83. package/dist/server/services/file-explorer-ops.js +211 -0
  84. package/dist/server/services/file-explorer-ops.js.map +1 -0
  85. package/dist/server/services/files.d.ts +2 -85
  86. package/dist/server/services/files.d.ts.map +1 -1
  87. package/dist/server/services/files.js +7 -427
  88. package/dist/server/services/files.js.map +1 -1
  89. package/dist/server/services/plan/composer.d.ts.map +1 -1
  90. package/dist/server/services/plan/composer.js +2 -1
  91. package/dist/server/services/plan/composer.js.map +1 -1
  92. package/dist/server/services/plan/executor.d.ts.map +1 -1
  93. package/dist/server/services/plan/executor.js +3 -1
  94. package/dist/server/services/plan/executor.js.map +1 -1
  95. package/dist/server/services/plan/parser-core.d.ts +20 -0
  96. package/dist/server/services/plan/parser-core.d.ts.map +1 -0
  97. package/dist/server/services/plan/parser-core.js +350 -0
  98. package/dist/server/services/plan/parser-core.js.map +1 -0
  99. package/dist/server/services/plan/parser-migration.d.ts +5 -0
  100. package/dist/server/services/plan/parser-migration.d.ts.map +1 -0
  101. package/dist/server/services/plan/parser-migration.js +124 -0
  102. package/dist/server/services/plan/parser-migration.js.map +1 -0
  103. package/dist/server/services/plan/parser.d.ts +0 -8
  104. package/dist/server/services/plan/parser.d.ts.map +1 -1
  105. package/dist/server/services/plan/parser.js +50 -569
  106. package/dist/server/services/plan/parser.js.map +1 -1
  107. package/dist/server/services/plan/review-gate.d.ts +2 -0
  108. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  109. package/dist/server/services/plan/review-gate.js +2 -2
  110. package/dist/server/services/plan/review-gate.js.map +1 -1
  111. package/dist/server/services/plan/types.d.ts +2 -0
  112. package/dist/server/services/plan/types.d.ts.map +1 -1
  113. package/dist/server/services/platform-credentials.d.ts +24 -0
  114. package/dist/server/services/platform-credentials.d.ts.map +1 -0
  115. package/dist/server/services/platform-credentials.js +68 -0
  116. package/dist/server/services/platform-credentials.js.map +1 -0
  117. package/dist/server/services/platform.d.ts +1 -31
  118. package/dist/server/services/platform.d.ts.map +1 -1
  119. package/dist/server/services/platform.js +10 -119
  120. package/dist/server/services/platform.js.map +1 -1
  121. package/dist/server/services/terminal/pty-manager.d.ts +7 -97
  122. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  123. package/dist/server/services/terminal/pty-manager.js +53 -266
  124. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  125. package/dist/server/services/terminal/pty-utils.d.ts +57 -0
  126. package/dist/server/services/terminal/pty-utils.d.ts.map +1 -0
  127. package/dist/server/services/terminal/pty-utils.js +141 -0
  128. package/dist/server/services/terminal/pty-utils.js.map +1 -0
  129. package/dist/server/services/websocket/file-definition-handlers.d.ts +4 -0
  130. package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -0
  131. package/dist/server/services/websocket/file-definition-handlers.js +153 -0
  132. package/dist/server/services/websocket/file-definition-handlers.js.map +1 -0
  133. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
  134. package/dist/server/services/websocket/file-explorer-handlers.js +52 -391
  135. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  136. package/dist/server/services/websocket/file-search-handlers.d.ts +5 -0
  137. package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -0
  138. package/dist/server/services/websocket/file-search-handlers.js +238 -0
  139. package/dist/server/services/websocket/file-search-handlers.js.map +1 -0
  140. package/dist/server/services/websocket/file-utils.js +3 -3
  141. package/dist/server/services/websocket/file-utils.js.map +1 -1
  142. package/dist/server/services/websocket/git-branch-handlers.d.ts +7 -0
  143. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -0
  144. package/dist/server/services/websocket/git-branch-handlers.js +110 -0
  145. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -0
  146. package/dist/server/services/websocket/git-diff-handlers.d.ts +6 -0
  147. package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -0
  148. package/dist/server/services/websocket/git-diff-handlers.js +123 -0
  149. package/dist/server/services/websocket/git-diff-handlers.js.map +1 -0
  150. package/dist/server/services/websocket/git-handlers.d.ts +2 -31
  151. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
  152. package/dist/server/services/websocket/git-handlers.js +35 -541
  153. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  154. package/dist/server/services/websocket/git-log-handlers.d.ts +6 -0
  155. package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -0
  156. package/dist/server/services/websocket/git-log-handlers.js +128 -0
  157. package/dist/server/services/websocket/git-log-handlers.js.map +1 -0
  158. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
  159. package/dist/server/services/websocket/git-pr-handlers.js +13 -53
  160. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
  161. package/dist/server/services/websocket/git-tag-handlers.d.ts +6 -0
  162. package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -0
  163. package/dist/server/services/websocket/git-tag-handlers.js +76 -0
  164. package/dist/server/services/websocket/git-tag-handlers.js.map +1 -0
  165. package/dist/server/services/websocket/git-utils.d.ts +43 -0
  166. package/dist/server/services/websocket/git-utils.d.ts.map +1 -0
  167. package/dist/server/services/websocket/git-utils.js +201 -0
  168. package/dist/server/services/websocket/git-utils.js.map +1 -0
  169. package/dist/server/services/websocket/handler.d.ts +2 -0
  170. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  171. package/dist/server/services/websocket/handler.js +37 -126
  172. package/dist/server/services/websocket/handler.js.map +1 -1
  173. package/dist/server/services/websocket/plan-board-handlers.d.ts +11 -0
  174. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -0
  175. package/dist/server/services/websocket/plan-board-handlers.js +218 -0
  176. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -0
  177. package/dist/server/services/websocket/plan-execution-handlers.d.ts +9 -0
  178. package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -0
  179. package/dist/server/services/websocket/plan-execution-handlers.js +142 -0
  180. package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -0
  181. package/dist/server/services/websocket/plan-handlers.d.ts +7 -2
  182. package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
  183. package/dist/server/services/websocket/plan-handlers.js +6 -925
  184. package/dist/server/services/websocket/plan-handlers.js.map +1 -1
  185. package/dist/server/services/websocket/plan-helpers.d.ts +19 -0
  186. package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -0
  187. package/dist/server/services/websocket/plan-helpers.js +199 -0
  188. package/dist/server/services/websocket/plan-helpers.js.map +1 -0
  189. package/dist/server/services/websocket/plan-issue-handlers.d.ts +12 -0
  190. package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -0
  191. package/dist/server/services/websocket/plan-issue-handlers.js +162 -0
  192. package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -0
  193. package/dist/server/services/websocket/plan-sprint-handlers.d.ts +7 -0
  194. package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -0
  195. package/dist/server/services/websocket/plan-sprint-handlers.js +206 -0
  196. package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -0
  197. package/dist/server/services/websocket/quality-complexity.d.ts +14 -0
  198. package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -0
  199. package/dist/server/services/websocket/quality-complexity.js +262 -0
  200. package/dist/server/services/websocket/quality-complexity.js.map +1 -0
  201. package/dist/server/services/websocket/quality-fix-agent.d.ts +16 -0
  202. package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -0
  203. package/dist/server/services/websocket/quality-fix-agent.js +140 -0
  204. package/dist/server/services/websocket/quality-fix-agent.js.map +1 -0
  205. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  206. package/dist/server/services/websocket/quality-handlers.js +34 -346
  207. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  208. package/dist/server/services/websocket/quality-linting.d.ts +9 -0
  209. package/dist/server/services/websocket/quality-linting.d.ts.map +1 -0
  210. package/dist/server/services/websocket/quality-linting.js +178 -0
  211. package/dist/server/services/websocket/quality-linting.js.map +1 -0
  212. package/dist/server/services/websocket/quality-review-agent.d.ts +19 -0
  213. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -0
  214. package/dist/server/services/websocket/quality-review-agent.js +206 -0
  215. package/dist/server/services/websocket/quality-review-agent.js.map +1 -0
  216. package/dist/server/services/websocket/quality-service.d.ts +3 -51
  217. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  218. package/dist/server/services/websocket/quality-service.js +9 -651
  219. package/dist/server/services/websocket/quality-service.js.map +1 -1
  220. package/dist/server/services/websocket/quality-tools.d.ts +23 -0
  221. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -0
  222. package/dist/server/services/websocket/quality-tools.js +208 -0
  223. package/dist/server/services/websocket/quality-tools.js.map +1 -0
  224. package/dist/server/services/websocket/quality-types.d.ts +59 -0
  225. package/dist/server/services/websocket/quality-types.d.ts.map +1 -0
  226. package/dist/server/services/websocket/quality-types.js +101 -0
  227. package/dist/server/services/websocket/quality-types.js.map +1 -0
  228. package/dist/server/services/websocket/session-handlers.d.ts +3 -4
  229. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  230. package/dist/server/services/websocket/session-handlers.js +3 -378
  231. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  232. package/dist/server/services/websocket/session-history.d.ts +4 -0
  233. package/dist/server/services/websocket/session-history.d.ts.map +1 -0
  234. package/dist/server/services/websocket/session-history.js +208 -0
  235. package/dist/server/services/websocket/session-history.js.map +1 -0
  236. package/dist/server/services/websocket/session-initialization.d.ts +5 -0
  237. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -0
  238. package/dist/server/services/websocket/session-initialization.js +163 -0
  239. package/dist/server/services/websocket/session-initialization.js.map +1 -0
  240. package/dist/server/services/websocket/types.d.ts +12 -2
  241. package/dist/server/services/websocket/types.d.ts.map +1 -1
  242. package/package.json +1 -1
  243. package/server/cli/headless/claude-invoker-process.ts +204 -0
  244. package/server/cli/headless/claude-invoker-stall.ts +164 -0
  245. package/server/cli/headless/claude-invoker-stream.ts +353 -0
  246. package/server/cli/headless/claude-invoker-tools.ts +187 -0
  247. package/server/cli/headless/claude-invoker.ts +15 -1096
  248. package/server/cli/headless/haiku-assessments.ts +365 -0
  249. package/server/cli/headless/headless-logger.ts +26 -5
  250. package/server/cli/headless/native-timeout-detector.ts +117 -0
  251. package/server/cli/headless/stall-assessor.ts +65 -618
  252. package/server/cli/improvisation-attachments.ts +148 -0
  253. package/server/cli/improvisation-retry.ts +602 -0
  254. package/server/cli/improvisation-session-manager.ts +140 -1349
  255. package/server/cli/improvisation-types.ts +98 -0
  256. package/server/cli/prompt-builders.ts +370 -0
  257. package/server/index.ts +35 -246
  258. package/server/mcp/bouncer-haiku.ts +182 -0
  259. package/server/mcp/bouncer-integration.ts +87 -248
  260. package/server/mcp/security-analysis.ts +217 -0
  261. package/server/mcp/security-audit.ts +1 -1
  262. package/server/mcp/security-patterns.ts +60 -283
  263. package/server/server-setup.ts +114 -0
  264. package/server/services/file-explorer-ops.ts +293 -0
  265. package/server/services/files.ts +20 -532
  266. package/server/services/plan/composer.ts +2 -1
  267. package/server/services/plan/executor.ts +3 -1
  268. package/server/services/plan/parser-core.ts +406 -0
  269. package/server/services/plan/parser-migration.ts +128 -0
  270. package/server/services/plan/parser.ts +52 -620
  271. package/server/services/plan/review-gate.ts +4 -2
  272. package/server/services/plan/types.ts +2 -0
  273. package/server/services/platform-credentials.ts +83 -0
  274. package/server/services/platform.ts +15 -141
  275. package/server/services/terminal/pty-manager.ts +66 -313
  276. package/server/services/terminal/pty-utils.ts +176 -0
  277. package/server/services/websocket/file-definition-handlers.ts +165 -0
  278. package/server/services/websocket/file-explorer-handlers.ts +37 -452
  279. package/server/services/websocket/file-search-handlers.ts +291 -0
  280. package/server/services/websocket/file-utils.ts +3 -3
  281. package/server/services/websocket/git-branch-handlers.ts +130 -0
  282. package/server/services/websocket/git-diff-handlers.ts +140 -0
  283. package/server/services/websocket/git-handlers.ts +40 -625
  284. package/server/services/websocket/git-log-handlers.ts +149 -0
  285. package/server/services/websocket/git-pr-handlers.ts +17 -62
  286. package/server/services/websocket/git-tag-handlers.ts +91 -0
  287. package/server/services/websocket/git-utils.ts +230 -0
  288. package/server/services/websocket/handler.ts +39 -126
  289. package/server/services/websocket/plan-board-handlers.ts +277 -0
  290. package/server/services/websocket/plan-execution-handlers.ts +184 -0
  291. package/server/services/websocket/plan-handlers.ts +8 -1114
  292. package/server/services/websocket/plan-helpers.ts +215 -0
  293. package/server/services/websocket/plan-issue-handlers.ts +204 -0
  294. package/server/services/websocket/plan-sprint-handlers.ts +252 -0
  295. package/server/services/websocket/quality-complexity.ts +294 -0
  296. package/server/services/websocket/quality-fix-agent.ts +181 -0
  297. package/server/services/websocket/quality-handlers.ts +36 -404
  298. package/server/services/websocket/quality-linting.ts +187 -0
  299. package/server/services/websocket/quality-review-agent.ts +246 -0
  300. package/server/services/websocket/quality-service.ts +11 -762
  301. package/server/services/websocket/quality-tools.ts +209 -0
  302. package/server/services/websocket/quality-types.ts +169 -0
  303. package/server/services/websocket/session-handlers.ts +5 -437
  304. package/server/services/websocket/session-history.ts +222 -0
  305. package/server/services/websocket/session-initialization.ts +209 -0
  306. package/server/services/websocket/types.ts +17 -0
@@ -1,15 +1,16 @@
1
1
  // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
2
  // Licensed under the MIT License. See LICENSE file for details.
3
3
 
4
- import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
5
- import { join } from 'node:path';
6
- import { type FileAttachment, ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
4
+ import type { FileAttachment, ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
7
5
  import { getModel } from '../settings.js';
8
6
  import type { HandlerContext } from './handler-context.js';
9
7
  import { runQualityScan } from './quality-service.js';
10
- import type { SessionRegistry } from './session-registry.js';
11
8
  import type { WebSocketMessage, WSContext } from './types.js';
12
9
 
10
+ // Re-export from extracted modules for backward compatibility
11
+ export { handleHistoryMessage } from './session-history.js';
12
+ export { initializeTab, resumeHistoricalSession } from './session-initialization.js';
13
+
13
14
  const WRITE_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
14
15
 
15
16
  function movementHadWrites(movement: Record<string, unknown>): boolean {
@@ -225,436 +226,3 @@ export function handleSessionMessage(ctx: HandlerContext, ws: WSContext, msg: We
225
226
  }
226
227
  }
227
228
  }
228
-
229
- export function handleHistoryMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): void {
230
- switch (msg.type) {
231
- case 'getSessions': {
232
- const result = getSessionsList(workingDir, msg.data?.limit ?? 20, msg.data?.offset ?? 0);
233
- ctx.send(ws, { type: 'sessions', tabId, data: result });
234
- break;
235
- }
236
- case 'getSessionsCount':
237
- ctx.send(ws, { type: 'sessionsCount', tabId, data: { total: getSessionsCount(workingDir) } });
238
- break;
239
- case 'getSessionById':
240
- if (!msg.data?.sessionId) throw new Error('Session ID is required');
241
- ctx.send(ws, { type: 'sessionData', tabId, data: getSessionById(workingDir, msg.data.sessionId) });
242
- break;
243
- case 'deleteSession':
244
- if (!msg.data?.sessionId) throw new Error('Session ID is required');
245
- ctx.send(ws, { type: 'sessionDeleted', tabId, data: deleteSession(workingDir, msg.data.sessionId) });
246
- break;
247
- case 'clearHistory':
248
- ctx.send(ws, { type: 'historyCleared', tabId, data: clearAllSessions(workingDir) });
249
- break;
250
- case 'searchHistory': {
251
- if (!msg.data?.query) throw new Error('Search query is required');
252
- const result = searchSessions(workingDir, msg.data.query, msg.data?.limit ?? 20, msg.data?.offset ?? 0);
253
- ctx.send(ws, { type: 'searchResults', tabId, data: { ...result, query: msg.data.query } });
254
- break;
255
- }
256
- }
257
- }
258
-
259
- function tryResumeFromDisk(
260
- ctx: HandlerContext,
261
- ws: WSContext,
262
- tabId: string,
263
- workingDir: string,
264
- registrySessionId: string,
265
- tabMap: Map<string, string> | undefined,
266
- registry: SessionRegistry
267
- ): boolean {
268
- try {
269
- const diskSession = ImprovisationSessionManager.resumeFromHistory(workingDir, registrySessionId);
270
- setupSessionListeners(ctx, diskSession, ws, tabId);
271
- const diskSessionId = diskSession.getSessionInfo().sessionId;
272
- ctx.sessions.set(diskSessionId, diskSession);
273
- if (tabMap) tabMap.set(tabId, diskSessionId);
274
- registry.touchTab(tabId);
275
-
276
- // Restore worktree state from registry
277
- const regTab = registry.getTab(tabId);
278
- if (regTab?.worktreePath && !ctx.gitDirectories.has(tabId)) {
279
- ctx.gitDirectories.set(tabId, regTab.worktreePath);
280
- if (regTab.worktreeBranch) ctx.gitBranches.set(tabId, regTab.worktreeBranch);
281
- }
282
- const worktreePath = ctx.gitDirectories.get(tabId);
283
- const worktreeBranch = ctx.gitBranches.get(tabId);
284
-
285
- ctx.send(ws, {
286
- type: 'tabInitialized',
287
- tabId,
288
- data: {
289
- ...diskSession.getSessionInfo(),
290
- outputHistory: buildOutputHistory(diskSession),
291
- ...(worktreePath ? { worktreePath, worktreeBranch } : {}),
292
- }
293
- });
294
- return true;
295
- } catch {
296
- return false;
297
- }
298
- }
299
-
300
- export async function initializeTab(ctx: HandlerContext, ws: WSContext, tabId: string, workingDir: string, tabName?: string): Promise<void> {
301
- const tabMap = ctx.connections.get(ws);
302
- const registry = ctx.getRegistry(workingDir);
303
-
304
- // 1. Check per-connection map (same WS reconnect)
305
- const existingSessionId = tabMap?.get(tabId);
306
- if (existingSessionId) {
307
- const existingSession = ctx.sessions.get(existingSessionId);
308
- if (existingSession) {
309
- reattachSession(ctx, existingSession, ws, tabId, registry);
310
- return;
311
- }
312
- }
313
-
314
- // 2. Check session registry (cross-connection reattach)
315
- const registrySessionId = registry.getTabSession(tabId);
316
- if (registrySessionId) {
317
- const inMemorySession = ctx.sessions.get(registrySessionId);
318
- if (inMemorySession) {
319
- reattachSession(ctx, inMemorySession, ws, tabId, registry);
320
- return;
321
- }
322
-
323
- if (tryResumeFromDisk(ctx, ws, tabId, workingDir, registrySessionId, tabMap, registry)) {
324
- return;
325
- }
326
- }
327
-
328
- // 3. Create new session
329
- const session = new ImprovisationSessionManager({ workingDir, model: getModel() });
330
- setupSessionListeners(ctx, session, ws, tabId);
331
-
332
- const sessionId = session.getSessionInfo().sessionId;
333
- ctx.sessions.set(sessionId, session);
334
-
335
- if (tabMap) {
336
- tabMap.set(tabId, sessionId);
337
- }
338
-
339
- registry.registerTab(tabId, sessionId, tabName);
340
- const registeredTab = registry.getTab(tabId);
341
- ctx.broadcastToAll({
342
- type: 'tabCreated',
343
- data: { tabId, tabName: registeredTab?.tabName || 'Chat', createdAt: registeredTab?.createdAt, order: registeredTab?.order, sessionInfo: session.getSessionInfo() }
344
- });
345
-
346
- ctx.send(ws, {
347
- type: 'tabInitialized',
348
- tabId,
349
- data: session.getSessionInfo()
350
- });
351
- }
352
-
353
- export async function resumeHistoricalSession(
354
- ctx: HandlerContext,
355
- ws: WSContext,
356
- tabId: string,
357
- workingDir: string,
358
- historicalSessionId: string
359
- ): Promise<void> {
360
- const tabMap = ctx.connections.get(ws);
361
- const registry = ctx.getRegistry(workingDir);
362
-
363
- const existingSessionId = tabMap?.get(tabId);
364
- if (existingSessionId) {
365
- const existingSession = ctx.sessions.get(existingSessionId);
366
- if (existingSession) {
367
- reattachSession(ctx, existingSession, ws, tabId, registry);
368
- return;
369
- }
370
- }
371
-
372
- const registrySessionId = registry.getTabSession(tabId);
373
- if (registrySessionId) {
374
- const inMemorySession = ctx.sessions.get(registrySessionId);
375
- if (inMemorySession) {
376
- reattachSession(ctx, inMemorySession, ws, tabId, registry);
377
- return;
378
- }
379
- }
380
-
381
- let session: ImprovisationSessionManager;
382
- let isNewSession = false;
383
-
384
- try {
385
- session = ImprovisationSessionManager.resumeFromHistory(workingDir, historicalSessionId, { model: getModel() });
386
- } catch (error: unknown) {
387
- console.warn(`[WebSocketImproviseHandler] Could not resume session ${historicalSessionId}: ${error instanceof Error ? error.message : String(error)}. Creating new session.`);
388
- session = new ImprovisationSessionManager({ workingDir, model: getModel() });
389
- isNewSession = true;
390
- }
391
-
392
- setupSessionListeners(ctx, session, ws, tabId);
393
-
394
- const sessionId = session.getSessionInfo().sessionId;
395
- ctx.sessions.set(sessionId, session);
396
-
397
- if (tabMap) {
398
- tabMap.set(tabId, sessionId);
399
- }
400
-
401
- registry.registerTab(tabId, sessionId);
402
-
403
- ctx.send(ws, {
404
- type: 'tabInitialized',
405
- tabId,
406
- data: {
407
- ...session.getSessionInfo(),
408
- outputHistory: buildOutputHistory(session),
409
- resumeFailed: isNewSession,
410
- originalSessionId: isNewSession ? historicalSessionId : undefined
411
- }
412
- });
413
- }
414
-
415
- function reattachSession(
416
- ctx: HandlerContext,
417
- session: ImprovisationSessionManager,
418
- ws: WSContext,
419
- tabId: string,
420
- registry: SessionRegistry
421
- ): void {
422
- setupSessionListeners(ctx, session, ws, tabId);
423
-
424
- const tabMap = ctx.connections.get(ws);
425
- const sessionId = session.getSessionInfo().sessionId;
426
- if (tabMap) tabMap.set(tabId, sessionId);
427
- registry.touchTab(tabId);
428
-
429
- // Restore worktree state from registry if not already in memory
430
- const regTab = registry.getTab(tabId);
431
- if (regTab?.worktreePath && !ctx.gitDirectories.has(tabId)) {
432
- ctx.gitDirectories.set(tabId, regTab.worktreePath);
433
- if (regTab.worktreeBranch) ctx.gitBranches.set(tabId, regTab.worktreeBranch);
434
- }
435
-
436
- const outputHistory = buildOutputHistory(session);
437
-
438
- const executionEvents = session.isExecuting
439
- ? session.getExecutionEventLog()
440
- : undefined;
441
-
442
- const worktreePath = ctx.gitDirectories.get(tabId);
443
- const worktreeBranch = ctx.gitBranches.get(tabId);
444
-
445
- ctx.send(ws, {
446
- type: 'tabInitialized',
447
- tabId,
448
- data: {
449
- ...session.getSessionInfo(),
450
- outputHistory,
451
- isExecuting: session.isExecuting,
452
- executionEvents,
453
- ...(session.isExecuting && session.executionStartTimestamp ? { executionStartTimestamp: session.executionStartTimestamp } : {}),
454
- ...(worktreePath ? { worktreePath, worktreeBranch } : {}),
455
- }
456
- });
457
- }
458
-
459
- // ============================================
460
- // History persistence functions
461
- // ============================================
462
-
463
- function getSessionsCount(workingDir: string): number {
464
- const sessionsDir = join(workingDir, '.mstro', 'history');
465
- if (!existsSync(sessionsDir)) return 0;
466
- return readdirSync(sessionsDir).filter((name: string) => name.endsWith('.json')).length;
467
- }
468
-
469
- function getSessionsList(workingDir: string, limit: number = 20, offset: number = 0): { sessions: Array<Record<string, unknown> | null>; total: number; hasMore: boolean } {
470
- const sessionsDir = join(workingDir, '.mstro', 'history');
471
-
472
- if (!existsSync(sessionsDir)) {
473
- return { sessions: [], total: 0, hasMore: false };
474
- }
475
-
476
- const historyFiles = readdirSync(sessionsDir)
477
- .filter((name: string) => name.endsWith('.json'))
478
- .sort((a: string, b: string) => {
479
- const timestampA = parseInt(a.replace('.json', ''), 10);
480
- const timestampB = parseInt(b.replace('.json', ''), 10);
481
- return timestampB - timestampA;
482
- });
483
-
484
- const total = historyFiles.length;
485
- const pageFiles = historyFiles.slice(offset, offset + limit);
486
-
487
- const sessions = pageFiles.map((filename: string) => {
488
- const historyPath = join(sessionsDir, filename);
489
- try {
490
- const historyData = JSON.parse(readFileSync(historyPath, 'utf-8'));
491
- const firstPrompt = historyData.movements?.[0]?.userPrompt || '';
492
-
493
- const movementPreviews = (historyData.movements || []).slice(0, 3).map((m: Record<string, unknown>) => ({
494
- userPrompt: (typeof m.userPrompt === 'string' ? m.userPrompt : '').slice(0, 100) || ''
495
- }));
496
-
497
- return {
498
- sessionId: historyData.sessionId,
499
- startedAt: historyData.startedAt,
500
- lastActivityAt: historyData.lastActivityAt,
501
- totalTokens: historyData.totalTokens,
502
- movementCount: historyData.movements?.length || 0,
503
- title: firstPrompt.slice(0, 80) + (firstPrompt.length > 80 ? '...' : ''),
504
- movements: movementPreviews
505
- };
506
- } catch {
507
- return null;
508
- }
509
- }).filter(Boolean);
510
-
511
- return { sessions, total, hasMore: offset + limit < total };
512
- }
513
-
514
- function getSessionById(workingDir: string, sessionId: string): Record<string, unknown> | null {
515
- const sessionsDir = join(workingDir, '.mstro', 'history');
516
- if (!existsSync(sessionsDir)) return null;
517
-
518
- const historyFiles = readdirSync(sessionsDir).filter((name: string) => name.endsWith('.json'));
519
-
520
- for (const filename of historyFiles) {
521
- const historyPath = join(sessionsDir, filename);
522
- try {
523
- const historyData = JSON.parse(readFileSync(historyPath, 'utf-8'));
524
- if (historyData.sessionId === sessionId) {
525
- const firstPrompt = historyData.movements?.[0]?.userPrompt || '';
526
- return {
527
- sessionId: historyData.sessionId,
528
- startedAt: historyData.startedAt,
529
- lastActivityAt: historyData.lastActivityAt,
530
- totalTokens: historyData.totalTokens,
531
- movementCount: historyData.movements?.length || 0,
532
- title: firstPrompt.slice(0, 80) + (firstPrompt.length > 80 ? '...' : ''),
533
- movements: historyData.movements || [],
534
- };
535
- }
536
- } catch {
537
- // Skip files that can't be parsed
538
- }
539
- }
540
-
541
- return null;
542
- }
543
-
544
- function deleteSession(workingDir: string, sessionId: string): { sessionId: string; success: boolean } {
545
- const sessionsDir = join(workingDir, '.mstro', 'history');
546
- if (!existsSync(sessionsDir)) return { sessionId, success: false };
547
-
548
- try {
549
- const historyFiles = readdirSync(sessionsDir).filter((name: string) => name.endsWith('.json'));
550
-
551
- for (const filename of historyFiles) {
552
- const historyPath = join(sessionsDir, filename);
553
- try {
554
- const historyData = JSON.parse(readFileSync(historyPath, 'utf-8'));
555
- if (historyData.sessionId === sessionId) {
556
- unlinkSync(historyPath);
557
- return { sessionId, success: true };
558
- }
559
- } catch {
560
- // Skip files that can't be parsed
561
- }
562
- }
563
-
564
- return { sessionId, success: false };
565
- } catch (error) {
566
- console.error('[WebSocketImproviseHandler] Error deleting session:', error);
567
- return { sessionId, success: false };
568
- }
569
- }
570
-
571
- function clearAllSessions(workingDir: string): { success: boolean; deletedCount: number } {
572
- const sessionsDir = join(workingDir, '.mstro', 'history');
573
- if (!existsSync(sessionsDir)) return { success: true, deletedCount: 0 };
574
-
575
- try {
576
- const historyFiles = readdirSync(sessionsDir).filter((name: string) => name.endsWith('.json'));
577
-
578
- let deletedCount = 0;
579
- for (const filename of historyFiles) {
580
- const historyPath = join(sessionsDir, filename);
581
- try {
582
- unlinkSync(historyPath);
583
- deletedCount++;
584
- } catch {
585
- // Skip files that can't be deleted
586
- }
587
- }
588
-
589
- return { success: true, deletedCount };
590
- } catch (error) {
591
- console.error('[WebSocketImproviseHandler] Error clearing sessions:', error);
592
- return { success: false, deletedCount: 0 };
593
- }
594
- }
595
-
596
- function movementMatchesQuery(movements: Array<Record<string, unknown>> | undefined, lowerQuery: string): boolean {
597
- if (!movements) return false;
598
- return movements.some((m: Record<string, unknown>) =>
599
- (typeof m.userPrompt === 'string' && m.userPrompt.toLowerCase().includes(lowerQuery)) ||
600
- (typeof m.summary === 'string' && m.summary.toLowerCase().includes(lowerQuery)) ||
601
- (typeof m.assistantResponse === 'string' && m.assistantResponse.toLowerCase().includes(lowerQuery))
602
- );
603
- }
604
-
605
- function buildSessionSummary(historyData: Record<string, unknown>): Record<string, unknown> {
606
- const movements = historyData.movements as Array<Record<string, unknown>> | undefined;
607
- const firstPrompt = (typeof movements?.[0]?.userPrompt === 'string' ? movements[0].userPrompt : '') || '';
608
- const movementPreviews = (movements || []).slice(0, 3).map((m: Record<string, unknown>) => ({
609
- userPrompt: (typeof m.userPrompt === 'string' ? m.userPrompt : '').slice(0, 100) || ''
610
- }));
611
- return {
612
- sessionId: historyData.sessionId,
613
- startedAt: historyData.startedAt,
614
- lastActivityAt: historyData.lastActivityAt,
615
- totalTokens: historyData.totalTokens,
616
- movementCount: movements?.length || 0,
617
- title: firstPrompt.slice(0, 80) + (firstPrompt.length > 80 ? '...' : ''),
618
- movements: movementPreviews
619
- };
620
- }
621
-
622
- function searchSessions(workingDir: string, query: string, limit: number = 20, offset: number = 0): { sessions: Array<Record<string, unknown>>; total: number; hasMore: boolean } {
623
- const sessionsDir = join(workingDir, '.mstro', 'history');
624
- if (!existsSync(sessionsDir)) return { sessions: [], total: 0, hasMore: false };
625
-
626
- const lowerQuery = query.toLowerCase();
627
-
628
- try {
629
- const historyFiles = readdirSync(sessionsDir)
630
- .filter((name: string) => name.endsWith('.json'))
631
- .sort((a: string, b: string) => {
632
- const timestampA = parseInt(a.replace('.json', ''), 10);
633
- const timestampB = parseInt(b.replace('.json', ''), 10);
634
- return timestampB - timestampA;
635
- });
636
-
637
- const allMatches: Array<Record<string, unknown>> = [];
638
- for (const filename of historyFiles) {
639
- try {
640
- const content = readFileSync(join(sessionsDir, filename), 'utf-8');
641
- const historyData = JSON.parse(content);
642
- if (movementMatchesQuery(historyData.movements, lowerQuery)) {
643
- allMatches.push(buildSessionSummary(historyData));
644
- }
645
- } catch {
646
- // Skip files that can't be parsed
647
- }
648
- }
649
-
650
- const total = allMatches.length;
651
- return {
652
- sessions: allMatches.slice(offset, offset + limit),
653
- total,
654
- hasMore: offset + limit < total
655
- };
656
- } catch (error) {
657
- console.error('[WebSocketImproviseHandler] Error searching sessions:', error);
658
- return { sessions: [], total: 0, hasMore: false };
659
- }
660
- }
@@ -0,0 +1,222 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import type { HandlerContext } from './handler-context.js';
7
+ import type { WebSocketMessage, WSContext } from './types.js';
8
+
9
+ export function handleHistoryMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): void {
10
+ switch (msg.type) {
11
+ case 'getSessions': {
12
+ const result = getSessionsList(workingDir, msg.data?.limit ?? 20, msg.data?.offset ?? 0);
13
+ ctx.send(ws, { type: 'sessions', tabId, data: result });
14
+ break;
15
+ }
16
+ case 'getSessionsCount':
17
+ ctx.send(ws, { type: 'sessionsCount', tabId, data: { total: getSessionsCount(workingDir) } });
18
+ break;
19
+ case 'getSessionById':
20
+ if (!msg.data?.sessionId) throw new Error('Session ID is required');
21
+ ctx.send(ws, { type: 'sessionData', tabId, data: getSessionById(workingDir, msg.data.sessionId) });
22
+ break;
23
+ case 'deleteSession':
24
+ if (!msg.data?.sessionId) throw new Error('Session ID is required');
25
+ ctx.send(ws, { type: 'sessionDeleted', tabId, data: deleteSession(workingDir, msg.data.sessionId) });
26
+ break;
27
+ case 'clearHistory':
28
+ ctx.send(ws, { type: 'historyCleared', tabId, data: clearAllSessions(workingDir) });
29
+ break;
30
+ case 'searchHistory': {
31
+ if (!msg.data?.query) throw new Error('Search query is required');
32
+ const result = searchSessions(workingDir, msg.data.query, msg.data?.limit ?? 20, msg.data?.offset ?? 0);
33
+ ctx.send(ws, { type: 'searchResults', tabId, data: { ...result, query: msg.data.query } });
34
+ break;
35
+ }
36
+ }
37
+ }
38
+
39
+ function getSessionsCount(workingDir: string): number {
40
+ const sessionsDir = join(workingDir, '.mstro', 'history');
41
+ if (!existsSync(sessionsDir)) return 0;
42
+ return readdirSync(sessionsDir).filter((name: string) => name.endsWith('.json')).length;
43
+ }
44
+
45
+ function getSessionsList(workingDir: string, limit: number = 20, offset: number = 0): { sessions: Array<Record<string, unknown> | null>; total: number; hasMore: boolean } {
46
+ const sessionsDir = join(workingDir, '.mstro', 'history');
47
+
48
+ if (!existsSync(sessionsDir)) {
49
+ return { sessions: [], total: 0, hasMore: false };
50
+ }
51
+
52
+ const historyFiles = readdirSync(sessionsDir)
53
+ .filter((name: string) => name.endsWith('.json'))
54
+ .sort((a: string, b: string) => {
55
+ const timestampA = parseInt(a.replace('.json', ''), 10);
56
+ const timestampB = parseInt(b.replace('.json', ''), 10);
57
+ return timestampB - timestampA;
58
+ });
59
+
60
+ const total = historyFiles.length;
61
+ const pageFiles = historyFiles.slice(offset, offset + limit);
62
+
63
+ const sessions = pageFiles.map((filename: string) => {
64
+ const historyPath = join(sessionsDir, filename);
65
+ try {
66
+ const historyData = JSON.parse(readFileSync(historyPath, 'utf-8'));
67
+ return buildSessionSummary(historyData);
68
+ } catch {
69
+ return null;
70
+ }
71
+ }).filter(Boolean);
72
+
73
+ return { sessions, total, hasMore: offset + limit < total };
74
+ }
75
+
76
+ function getSessionById(workingDir: string, sessionId: string): Record<string, unknown> | null {
77
+ const sessionsDir = join(workingDir, '.mstro', 'history');
78
+ if (!existsSync(sessionsDir)) return null;
79
+
80
+ const historyFiles = readdirSync(sessionsDir).filter((name: string) => name.endsWith('.json'));
81
+
82
+ for (const filename of historyFiles) {
83
+ const historyPath = join(sessionsDir, filename);
84
+ try {
85
+ const historyData = JSON.parse(readFileSync(historyPath, 'utf-8'));
86
+ if (historyData.sessionId === sessionId) {
87
+ const firstPrompt = historyData.movements?.[0]?.userPrompt || '';
88
+ return {
89
+ sessionId: historyData.sessionId,
90
+ startedAt: historyData.startedAt,
91
+ lastActivityAt: historyData.lastActivityAt,
92
+ totalTokens: historyData.totalTokens,
93
+ movementCount: historyData.movements?.length || 0,
94
+ title: firstPrompt.slice(0, 80) + (firstPrompt.length > 80 ? '...' : ''),
95
+ movements: historyData.movements || [],
96
+ };
97
+ }
98
+ } catch {
99
+ // Skip files that can't be parsed
100
+ }
101
+ }
102
+
103
+ return null;
104
+ }
105
+
106
+ function deleteSession(workingDir: string, sessionId: string): { sessionId: string; success: boolean } {
107
+ const sessionsDir = join(workingDir, '.mstro', 'history');
108
+ if (!existsSync(sessionsDir)) return { sessionId, success: false };
109
+
110
+ try {
111
+ const historyFiles = readdirSync(sessionsDir).filter((name: string) => name.endsWith('.json'));
112
+
113
+ for (const filename of historyFiles) {
114
+ const historyPath = join(sessionsDir, filename);
115
+ try {
116
+ const historyData = JSON.parse(readFileSync(historyPath, 'utf-8'));
117
+ if (historyData.sessionId === sessionId) {
118
+ unlinkSync(historyPath);
119
+ return { sessionId, success: true };
120
+ }
121
+ } catch {
122
+ // Skip files that can't be parsed
123
+ }
124
+ }
125
+
126
+ return { sessionId, success: false };
127
+ } catch (error) {
128
+ console.error('[WebSocketImproviseHandler] Error deleting session:', error);
129
+ return { sessionId, success: false };
130
+ }
131
+ }
132
+
133
+ function clearAllSessions(workingDir: string): { success: boolean; deletedCount: number } {
134
+ const sessionsDir = join(workingDir, '.mstro', 'history');
135
+ if (!existsSync(sessionsDir)) return { success: true, deletedCount: 0 };
136
+
137
+ try {
138
+ const historyFiles = readdirSync(sessionsDir).filter((name: string) => name.endsWith('.json'));
139
+
140
+ let deletedCount = 0;
141
+ for (const filename of historyFiles) {
142
+ const historyPath = join(sessionsDir, filename);
143
+ try {
144
+ unlinkSync(historyPath);
145
+ deletedCount++;
146
+ } catch {
147
+ // Skip files that can't be deleted
148
+ }
149
+ }
150
+
151
+ return { success: true, deletedCount };
152
+ } catch (error) {
153
+ console.error('[WebSocketImproviseHandler] Error clearing sessions:', error);
154
+ return { success: false, deletedCount: 0 };
155
+ }
156
+ }
157
+
158
+ function movementMatchesQuery(movements: Array<Record<string, unknown>> | undefined, lowerQuery: string): boolean {
159
+ if (!movements) return false;
160
+ return movements.some((m: Record<string, unknown>) =>
161
+ (typeof m.userPrompt === 'string' && m.userPrompt.toLowerCase().includes(lowerQuery)) ||
162
+ (typeof m.summary === 'string' && m.summary.toLowerCase().includes(lowerQuery)) ||
163
+ (typeof m.assistantResponse === 'string' && m.assistantResponse.toLowerCase().includes(lowerQuery))
164
+ );
165
+ }
166
+
167
+ function buildSessionSummary(historyData: Record<string, unknown>): Record<string, unknown> {
168
+ const movements = historyData.movements as Array<Record<string, unknown>> | undefined;
169
+ const firstPrompt = (typeof movements?.[0]?.userPrompt === 'string' ? movements[0].userPrompt : '') || '';
170
+ const movementPreviews = (movements || []).slice(0, 3).map((m: Record<string, unknown>) => ({
171
+ userPrompt: (typeof m.userPrompt === 'string' ? m.userPrompt : '').slice(0, 100) || ''
172
+ }));
173
+ return {
174
+ sessionId: historyData.sessionId,
175
+ startedAt: historyData.startedAt,
176
+ lastActivityAt: historyData.lastActivityAt,
177
+ totalTokens: historyData.totalTokens,
178
+ movementCount: movements?.length || 0,
179
+ title: firstPrompt.slice(0, 80) + (firstPrompt.length > 80 ? '...' : ''),
180
+ movements: movementPreviews
181
+ };
182
+ }
183
+
184
+ function searchSessions(workingDir: string, query: string, limit: number = 20, offset: number = 0): { sessions: Array<Record<string, unknown>>; total: number; hasMore: boolean } {
185
+ const sessionsDir = join(workingDir, '.mstro', 'history');
186
+ if (!existsSync(sessionsDir)) return { sessions: [], total: 0, hasMore: false };
187
+
188
+ const lowerQuery = query.toLowerCase();
189
+
190
+ try {
191
+ const historyFiles = readdirSync(sessionsDir)
192
+ .filter((name: string) => name.endsWith('.json'))
193
+ .sort((a: string, b: string) => {
194
+ const timestampA = parseInt(a.replace('.json', ''), 10);
195
+ const timestampB = parseInt(b.replace('.json', ''), 10);
196
+ return timestampB - timestampA;
197
+ });
198
+
199
+ const allMatches: Array<Record<string, unknown>> = [];
200
+ for (const filename of historyFiles) {
201
+ try {
202
+ const content = readFileSync(join(sessionsDir, filename), 'utf-8');
203
+ const historyData = JSON.parse(content);
204
+ if (movementMatchesQuery(historyData.movements, lowerQuery)) {
205
+ allMatches.push(buildSessionSummary(historyData));
206
+ }
207
+ } catch {
208
+ // Skip files that can't be parsed
209
+ }
210
+ }
211
+
212
+ const total = allMatches.length;
213
+ return {
214
+ sessions: allMatches.slice(offset, offset + limit),
215
+ total,
216
+ hasMore: offset + limit < total
217
+ };
218
+ } catch (error) {
219
+ console.error('[WebSocketImproviseHandler] Error searching sessions:', error);
220
+ return { sessions: [], total: 0, hasMore: false };
221
+ }
222
+ }