automagik-forge 0.1.13 → 0.1.14

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 (301) hide show
  1. package/README.md +143 -447
  2. package/dist/linux-x64/automagik-forge-mcp.zip +0 -0
  3. package/{npx-cli/automagik-forge-0.0.55.tgz → dist/linux-x64/automagik-forge.zip} +0 -0
  4. package/package.json +13 -23
  5. package/.cargo/config.toml +0 -13
  6. package/.claude/commands/commit.md +0 -376
  7. package/.claude/commands/prompt.md +0 -871
  8. package/.env.example +0 -20
  9. package/.github/actions/setup-node/action.yml +0 -29
  10. package/.github/images/automagik-logo.png +0 -0
  11. package/.github/workflows/pre-release.yml +0 -470
  12. package/.github/workflows/publish.yml +0 -145
  13. package/.github/workflows/test.yml +0 -63
  14. package/.mcp.json +0 -57
  15. package/AGENT.md +0 -40
  16. package/CLAUDE.md +0 -40
  17. package/CODE-OF-CONDUCT.md +0 -89
  18. package/Cargo.toml +0 -19
  19. package/Dockerfile +0 -43
  20. package/LICENSE +0 -201
  21. package/Makefile +0 -97
  22. package/backend/.sqlx/query-01b7e2bac1261d8be3d03c03df3e5220590da6c31c77f161074fc62752d63881.json +0 -12
  23. package/backend/.sqlx/query-03f2b02ba6dc5ea2b3cf6b1004caea0ad6bcc10ebd63f441d321a389f026e263.json +0 -12
  24. package/backend/.sqlx/query-0923b77d137a29fc54d399a873ff15fc4af894490bc65a4d344a7575cb0d8643.json +0 -12
  25. package/backend/.sqlx/query-0f808bcdb63c5f180836e448dd64c435c51758b2fc54a52ce9e67495b1ab200e.json +0 -68
  26. package/backend/.sqlx/query-1268afe9ca849daa6722e3df7ca8e9e61f0d37052e782bb5452ab8e1018d9b63.json +0 -12
  27. package/backend/.sqlx/query-1b082630a9622f8667ee7a9aba2c2d3176019a68c6bb83d33008594821415a57.json +0 -12
  28. package/backend/.sqlx/query-1c7b06ba1e112abf6b945a2ff08a0b40ec23f3738c2e7399f067b558cf8d490e.json +0 -12
  29. package/backend/.sqlx/query-1f619f01f46859a64ded531dd0ef61abacfe62e758abe7030a6aa745140b95ca.json +0 -104
  30. package/backend/.sqlx/query-1fca1ce14b4b20205364cd1f1f45ebe1d2e30cd745e59e189d56487b5639dfbb.json +0 -12
  31. package/backend/.sqlx/query-212828320e8d871ab9d83705a040b23bcf0393dc7252177fc539a74657f578ef.json +0 -32
  32. package/backend/.sqlx/query-290ce5c152be8d36e58ff42570f9157beb07ab9e77a03ec6fc30b4f56f9b8f6b.json +0 -56
  33. package/backend/.sqlx/query-2b471d2c2e8ffbe0cd42d2a91b814c0d79f9d09200f147e3cea33ba4ce673c8a.json +0 -68
  34. package/backend/.sqlx/query-354a48c705bb9bb2048c1b7f10fcb714e23f9db82b7a4ea6932486197b2ede6a.json +0 -92
  35. package/backend/.sqlx/query-36c9e3dd10648e94b949db5c91a774ecb1e10a899ef95da74066eccedca4d8b2.json +0 -12
  36. package/backend/.sqlx/query-36e4ba7bbd81b402d5a20b6005755eafbb174c8dda442081823406ac32809a94.json +0 -56
  37. package/backend/.sqlx/query-3a5b3c98a55ca183ab20c74708e3d7e579dda37972c059e7515c4ceee4bd8dd3.json +0 -62
  38. package/backend/.sqlx/query-3d0a1cabf2a52e9d90cdfd29c509ca89aeb448d0c1d2446c65cd43db40735e86.json +0 -62
  39. package/backend/.sqlx/query-3d6bd16fbce59efe30b7f67ea342e0e4ea6d1432389c02468ad79f1f742d4031.json +0 -56
  40. package/backend/.sqlx/query-4049ca413b285a05aca6b25385e9c8185575f01e9069e4e8581aa45d713f612f.json +0 -32
  41. package/backend/.sqlx/query-412bacd3477d86369082e90f52240407abce436cb81292d42b2dbe1e5c18eea1.json +0 -104
  42. package/backend/.sqlx/query-417a8b1ff4e51de82aea0159a3b97932224dc325b23476cb84153d690227fd8b.json +0 -62
  43. package/backend/.sqlx/query-461cc1b0bb6fd909afc9dd2246e8526b3771cfbb0b22ae4b5d17b51af587b9e2.json +0 -56
  44. package/backend/.sqlx/query-58408c7a8cdeeda0bef359f1f9bd91299a339dc2b191462fc58c9736a56d5227.json +0 -92
  45. package/backend/.sqlx/query-5a886026d75d515c01f347cc203c8d99dd04c61dc468e2e4c5aa548436d13834.json +0 -62
  46. package/backend/.sqlx/query-5b902137b11022d2e1a5c4f6a9c83fec1a856c6a710aff831abd2382ede76b43.json +0 -12
  47. package/backend/.sqlx/query-5ed1238e52e59bb5f76c0f153fd99a14093f7ce2585bf9843585608f17ec575b.json +0 -104
  48. package/backend/.sqlx/query-6e8b860b14decfc2227dc57213f38442943d3fbef5c8418fd6b634c6e0f5e2ea.json +0 -104
  49. package/backend/.sqlx/query-6ec414276994c4ccb2433eaa5b1b342168557d17ddf5a52dac84cb1b59b9de8f.json +0 -68
  50. package/backend/.sqlx/query-6ecfa16d0cf825aacf233544b5baf151e9adfdca26c226ad71020d291fd802d5.json +0 -62
  51. package/backend/.sqlx/query-72509d252c39fce77520aa816cb2acbc1fb35dc2605e7be893610599b2427f2e.json +0 -62
  52. package/backend/.sqlx/query-75239b2da188f749707d77f3c1544332ca70db3d6d6743b2601dc0d167536437.json +0 -62
  53. package/backend/.sqlx/query-83d10e29f8478aff33434f9ac67068e013b888b953a2657e2bb72a6f619d04f2.json +0 -50
  54. package/backend/.sqlx/query-8610803360ea18b9b9d078a6981ea56abfbfe84e6354fc1d5ae4c622e01410ed.json +0 -68
  55. package/backend/.sqlx/query-86d03eb70eef39c59296416867f2ee66c9f7cd8b7f961fbda2f89fc0a1c442c2.json +0 -12
  56. package/backend/.sqlx/query-87d0feb5a6b442bad9c60068ea7569599cc6fc91a0e2692ecb42e93b03201b9d.json +0 -68
  57. package/backend/.sqlx/query-8a67b3b3337248f06a57bdf8a908f7ef23177431eaed82dc08c94c3e5944340e.json +0 -12
  58. package/backend/.sqlx/query-8f01ebd64bdcde6a090479f14810d73ba23020e76fd70854ac57f2da251702c3.json +0 -12
  59. package/backend/.sqlx/query-90fd607fcb2dca72239ff25e618e21e174b195991eaa33722cbf5f76da84cfab.json +0 -62
  60. package/backend/.sqlx/query-92e8bdbcd80c5ff3db7a35cf79492048803ef305cbdef0d0a1fe5dc881ca8c71.json +0 -104
  61. package/backend/.sqlx/query-93a1605f90e9672dad29b472b6ad85fa9a55ea3ffa5abcb8724b09d61be254ca.json +0 -20
  62. package/backend/.sqlx/query-9472c8fb477958167f5fae40b85ac44252468c5226b2cdd7770f027332eed6d7.json +0 -104
  63. package/backend/.sqlx/query-96036c4f9e0f48bdc5a4a4588f0c5f288ac7aaa5425cac40fc33f337e1a351f2.json +0 -56
  64. package/backend/.sqlx/query-9edb2c01e91fd0f0fe7b56e988c7ae0393150f50be3f419a981e035c0121dfc7.json +0 -104
  65. package/backend/.sqlx/query-a157cf00616f703bfba21927f1eb1c9eec2a81c02da15f66efdba0b6c375de1b.json +0 -26
  66. package/backend/.sqlx/query-a31fff84f3b8e532fd1160447d89d700f06ae08821fee00c9a5b60492b05259c.json +0 -62
  67. package/backend/.sqlx/query-a5ba908419fb3e456bdd2daca41ba06cc3212ffffb8520fc7dbbcc8b60ada314.json +0 -12
  68. package/backend/.sqlx/query-a6d2961718dbc3b1a925e549f49a159c561bef58c105529275f274b27e2eba5b.json +0 -104
  69. package/backend/.sqlx/query-a9e93d5b09b29faf66e387e4d7596a792d81e75c4d3726e83c2963e8d7c9b56f.json +0 -104
  70. package/backend/.sqlx/query-ac5247c8d7fb86e4650c4b0eb9420031614c831b7b085083bac20c1af314c538.json +0 -12
  71. package/backend/.sqlx/query-afef9467be74c411c4cb119a8b2b1aea53049877dfc30cc60b486134ba4b4c9f.json +0 -68
  72. package/backend/.sqlx/query-b2b2c6b4d0b1a347b5c4cb63c3a46a265d4db53be9554989a814b069d0af82f2.json +0 -62
  73. package/backend/.sqlx/query-c50d2ff0b12e5bcc81e371089ee2d007e233e7db93aefba4fef08e7aa68f5ab7.json +0 -20
  74. package/backend/.sqlx/query-c614e6056b244ca07f1b9d44e7edc9d5819225c6f8d9e077070c6e518a17f50b.json +0 -12
  75. package/backend/.sqlx/query-c67259be8bf4ee0cfd32167b2aa3b7fe9192809181a8171bf1c2d6df731967ae.json +0 -12
  76. package/backend/.sqlx/query-d2d0a1b985ebbca6a2b3e882a221a219f3199890fa640afc946ef1a792d6d8de.json +0 -12
  77. package/backend/.sqlx/query-d30aa5786757f32bf2b9c5fe51a45e506c71c28c5994e430d9b0546adb15ffa2.json +0 -20
  78. package/backend/.sqlx/query-d3b9ea1de1576af71b312924ce7f4ea8ae5dbe2ac138ea3b4470f2d5cd734846.json +0 -12
  79. package/backend/.sqlx/query-ed8456646fa69ddd412441955f06ff22bfb790f29466450735e0b8bb1bc4ec94.json +0 -12
  80. package/backend/Cargo.toml +0 -71
  81. package/backend/build.rs +0 -32
  82. package/backend/migrations/20250617183714_init.sql +0 -44
  83. package/backend/migrations/20250620212427_execution_processes.sql +0 -25
  84. package/backend/migrations/20250620214100_remove_stdout_stderr_from_task_attempts.sql +0 -28
  85. package/backend/migrations/20250621120000_relate_activities_to_execution_processes.sql +0 -23
  86. package/backend/migrations/20250623120000_executor_sessions.sql +0 -17
  87. package/backend/migrations/20250623130000_add_executor_type_to_execution_processes.sql +0 -4
  88. package/backend/migrations/20250625000000_add_dev_script_to_projects.sql +0 -4
  89. package/backend/migrations/20250701000000_add_branch_to_task_attempts.sql +0 -2
  90. package/backend/migrations/20250701000001_add_pr_tracking_to_task_attempts.sql +0 -5
  91. package/backend/migrations/20250701120000_add_assistant_message_to_executor_sessions.sql +0 -2
  92. package/backend/migrations/20250708000000_add_base_branch_to_task_attempts.sql +0 -2
  93. package/backend/migrations/20250709000000_add_worktree_deleted_flag.sql +0 -2
  94. package/backend/migrations/20250710000000_add_setup_completion.sql +0 -3
  95. package/backend/migrations/20250715154859_add_task_templates.sql +0 -25
  96. package/backend/migrations/20250716143725_add_default_templates.sql +0 -174
  97. package/backend/migrations/20250716161432_update_executor_names_to_kebab_case.sql +0 -20
  98. package/backend/migrations/20250716170000_add_parent_task_to_tasks.sql +0 -7
  99. package/backend/migrations/20250717000000_drop_task_attempt_activities.sql +0 -9
  100. package/backend/migrations/20250719000000_add_cleanup_script_to_projects.sql +0 -2
  101. package/backend/migrations/20250720000000_add_cleanupscript_to_process_type_constraint.sql +0 -25
  102. package/backend/migrations/20250723000000_add_wish_to_tasks.sql +0 -7
  103. package/backend/migrations/20250724000000_remove_unique_wish_constraint.sql +0 -5
  104. package/backend/scripts/toast-notification.ps1 +0 -23
  105. package/backend/sounds/abstract-sound1.wav +0 -0
  106. package/backend/sounds/abstract-sound2.wav +0 -0
  107. package/backend/sounds/abstract-sound3.wav +0 -0
  108. package/backend/sounds/abstract-sound4.wav +0 -0
  109. package/backend/sounds/cow-mooing.wav +0 -0
  110. package/backend/sounds/phone-vibration.wav +0 -0
  111. package/backend/sounds/rooster.wav +0 -0
  112. package/backend/src/app_state.rs +0 -218
  113. package/backend/src/bin/generate_types.rs +0 -189
  114. package/backend/src/bin/mcp_task_server.rs +0 -191
  115. package/backend/src/execution_monitor.rs +0 -1193
  116. package/backend/src/executor.rs +0 -1053
  117. package/backend/src/executors/amp.rs +0 -697
  118. package/backend/src/executors/ccr.rs +0 -91
  119. package/backend/src/executors/charm_opencode.rs +0 -113
  120. package/backend/src/executors/claude.rs +0 -887
  121. package/backend/src/executors/cleanup_script.rs +0 -124
  122. package/backend/src/executors/dev_server.rs +0 -53
  123. package/backend/src/executors/echo.rs +0 -79
  124. package/backend/src/executors/gemini/config.rs +0 -67
  125. package/backend/src/executors/gemini/streaming.rs +0 -363
  126. package/backend/src/executors/gemini.rs +0 -765
  127. package/backend/src/executors/mod.rs +0 -23
  128. package/backend/src/executors/opencode_ai.rs +0 -113
  129. package/backend/src/executors/setup_script.rs +0 -130
  130. package/backend/src/executors/sst_opencode/filter.rs +0 -184
  131. package/backend/src/executors/sst_opencode/tools.rs +0 -139
  132. package/backend/src/executors/sst_opencode.rs +0 -756
  133. package/backend/src/lib.rs +0 -45
  134. package/backend/src/main.rs +0 -324
  135. package/backend/src/mcp/mod.rs +0 -1
  136. package/backend/src/mcp/task_server.rs +0 -850
  137. package/backend/src/middleware/mod.rs +0 -3
  138. package/backend/src/middleware/model_loaders.rs +0 -242
  139. package/backend/src/models/api_response.rs +0 -36
  140. package/backend/src/models/config.rs +0 -375
  141. package/backend/src/models/execution_process.rs +0 -430
  142. package/backend/src/models/executor_session.rs +0 -225
  143. package/backend/src/models/mod.rs +0 -12
  144. package/backend/src/models/project.rs +0 -356
  145. package/backend/src/models/task.rs +0 -345
  146. package/backend/src/models/task_attempt.rs +0 -1214
  147. package/backend/src/models/task_template.rs +0 -146
  148. package/backend/src/openapi.rs +0 -93
  149. package/backend/src/routes/auth.rs +0 -297
  150. package/backend/src/routes/config.rs +0 -385
  151. package/backend/src/routes/filesystem.rs +0 -228
  152. package/backend/src/routes/health.rs +0 -16
  153. package/backend/src/routes/mod.rs +0 -9
  154. package/backend/src/routes/projects.rs +0 -562
  155. package/backend/src/routes/stream.rs +0 -244
  156. package/backend/src/routes/task_attempts.rs +0 -1172
  157. package/backend/src/routes/task_templates.rs +0 -229
  158. package/backend/src/routes/tasks.rs +0 -353
  159. package/backend/src/services/analytics.rs +0 -216
  160. package/backend/src/services/git_service.rs +0 -1321
  161. package/backend/src/services/github_service.rs +0 -307
  162. package/backend/src/services/mod.rs +0 -13
  163. package/backend/src/services/notification_service.rs +0 -263
  164. package/backend/src/services/pr_monitor.rs +0 -214
  165. package/backend/src/services/process_service.rs +0 -940
  166. package/backend/src/utils/path.rs +0 -96
  167. package/backend/src/utils/shell.rs +0 -19
  168. package/backend/src/utils/text.rs +0 -24
  169. package/backend/src/utils/worktree_manager.rs +0 -578
  170. package/backend/src/utils.rs +0 -125
  171. package/backend/test.db +0 -0
  172. package/build-npm-package.sh +0 -61
  173. package/dev_assets_seed/config.json +0 -19
  174. package/frontend/.eslintrc.json +0 -25
  175. package/frontend/.prettierrc.json +0 -8
  176. package/frontend/components.json +0 -17
  177. package/frontend/index.html +0 -19
  178. package/frontend/package-lock.json +0 -7321
  179. package/frontend/package.json +0 -61
  180. package/frontend/postcss.config.js +0 -6
  181. package/frontend/public/android-chrome-192x192.png +0 -0
  182. package/frontend/public/android-chrome-512x512.png +0 -0
  183. package/frontend/public/apple-touch-icon.png +0 -0
  184. package/frontend/public/automagik-forge-logo-dark.svg +0 -3
  185. package/frontend/public/automagik-forge-logo.svg +0 -3
  186. package/frontend/public/automagik-forge-screenshot-overview.png +0 -0
  187. package/frontend/public/favicon-16x16.png +0 -0
  188. package/frontend/public/favicon-32x32.png +0 -0
  189. package/frontend/public/favicon.ico +0 -0
  190. package/frontend/public/site.webmanifest +0 -1
  191. package/frontend/public/viba-kanban-favicon.png +0 -0
  192. package/frontend/src/App.tsx +0 -157
  193. package/frontend/src/components/DisclaimerDialog.tsx +0 -106
  194. package/frontend/src/components/GitHubLoginDialog.tsx +0 -314
  195. package/frontend/src/components/OnboardingDialog.tsx +0 -185
  196. package/frontend/src/components/PrivacyOptInDialog.tsx +0 -130
  197. package/frontend/src/components/ProvidePatDialog.tsx +0 -98
  198. package/frontend/src/components/TaskTemplateManager.tsx +0 -336
  199. package/frontend/src/components/config-provider.tsx +0 -119
  200. package/frontend/src/components/context/TaskDetailsContextProvider.tsx +0 -470
  201. package/frontend/src/components/context/taskDetailsContext.ts +0 -125
  202. package/frontend/src/components/keyboard-shortcuts-demo.tsx +0 -35
  203. package/frontend/src/components/layout/navbar.tsx +0 -86
  204. package/frontend/src/components/logo.tsx +0 -44
  205. package/frontend/src/components/projects/ProjectCard.tsx +0 -155
  206. package/frontend/src/components/projects/project-detail.tsx +0 -251
  207. package/frontend/src/components/projects/project-form-fields.tsx +0 -238
  208. package/frontend/src/components/projects/project-form.tsx +0 -301
  209. package/frontend/src/components/projects/project-list.tsx +0 -200
  210. package/frontend/src/components/projects/projects-page.tsx +0 -20
  211. package/frontend/src/components/tasks/BranchSelector.tsx +0 -169
  212. package/frontend/src/components/tasks/DeleteFileConfirmationDialog.tsx +0 -94
  213. package/frontend/src/components/tasks/EditorSelectionDialog.tsx +0 -119
  214. package/frontend/src/components/tasks/TaskCard.tsx +0 -154
  215. package/frontend/src/components/tasks/TaskDetails/CollapsibleToolbar.tsx +0 -33
  216. package/frontend/src/components/tasks/TaskDetails/DiffCard.tsx +0 -109
  217. package/frontend/src/components/tasks/TaskDetails/DiffChunkSection.tsx +0 -135
  218. package/frontend/src/components/tasks/TaskDetails/DiffFile.tsx +0 -296
  219. package/frontend/src/components/tasks/TaskDetails/DiffTab.tsx +0 -32
  220. package/frontend/src/components/tasks/TaskDetails/DisplayConversationEntry.tsx +0 -392
  221. package/frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx +0 -256
  222. package/frontend/src/components/tasks/TaskDetails/LogsTab/ConversationEntry.tsx +0 -56
  223. package/frontend/src/components/tasks/TaskDetails/LogsTab/NormalizedConversationViewer.tsx +0 -92
  224. package/frontend/src/components/tasks/TaskDetails/LogsTab/Prompt.tsx +0 -22
  225. package/frontend/src/components/tasks/TaskDetails/LogsTab/SetupScriptRunning.tsx +0 -49
  226. package/frontend/src/components/tasks/TaskDetails/LogsTab.tsx +0 -186
  227. package/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx +0 -288
  228. package/frontend/src/components/tasks/TaskDetails/RelatedTasksTab.tsx +0 -216
  229. package/frontend/src/components/tasks/TaskDetails/TabNavigation.tsx +0 -93
  230. package/frontend/src/components/tasks/TaskDetailsHeader.tsx +0 -169
  231. package/frontend/src/components/tasks/TaskDetailsPanel.tsx +0 -126
  232. package/frontend/src/components/tasks/TaskDetailsToolbar.tsx +0 -302
  233. package/frontend/src/components/tasks/TaskFollowUpSection.tsx +0 -130
  234. package/frontend/src/components/tasks/TaskFormDialog.tsx +0 -400
  235. package/frontend/src/components/tasks/TaskKanbanBoard.tsx +0 -180
  236. package/frontend/src/components/tasks/Toolbar/CreateAttempt.tsx +0 -259
  237. package/frontend/src/components/tasks/Toolbar/CreatePRDialog.tsx +0 -243
  238. package/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx +0 -899
  239. package/frontend/src/components/tasks/index.ts +0 -2
  240. package/frontend/src/components/theme-provider.tsx +0 -82
  241. package/frontend/src/components/theme-toggle.tsx +0 -36
  242. package/frontend/src/components/ui/alert.tsx +0 -59
  243. package/frontend/src/components/ui/auto-expanding-textarea.tsx +0 -70
  244. package/frontend/src/components/ui/badge.tsx +0 -36
  245. package/frontend/src/components/ui/button.tsx +0 -56
  246. package/frontend/src/components/ui/card.tsx +0 -86
  247. package/frontend/src/components/ui/checkbox.tsx +0 -44
  248. package/frontend/src/components/ui/chip.tsx +0 -25
  249. package/frontend/src/components/ui/dialog.tsx +0 -124
  250. package/frontend/src/components/ui/dropdown-menu.tsx +0 -198
  251. package/frontend/src/components/ui/file-search-textarea.tsx +0 -292
  252. package/frontend/src/components/ui/folder-picker.tsx +0 -279
  253. package/frontend/src/components/ui/input.tsx +0 -25
  254. package/frontend/src/components/ui/label.tsx +0 -24
  255. package/frontend/src/components/ui/loader.tsx +0 -26
  256. package/frontend/src/components/ui/markdown-renderer.tsx +0 -75
  257. package/frontend/src/components/ui/select.tsx +0 -160
  258. package/frontend/src/components/ui/separator.tsx +0 -31
  259. package/frontend/src/components/ui/shadcn-io/kanban/index.tsx +0 -185
  260. package/frontend/src/components/ui/table.tsx +0 -117
  261. package/frontend/src/components/ui/tabs.tsx +0 -53
  262. package/frontend/src/components/ui/textarea.tsx +0 -22
  263. package/frontend/src/components/ui/tooltip.tsx +0 -28
  264. package/frontend/src/hooks/useNormalizedConversation.ts +0 -440
  265. package/frontend/src/index.css +0 -225
  266. package/frontend/src/lib/api.ts +0 -630
  267. package/frontend/src/lib/keyboard-shortcuts.ts +0 -266
  268. package/frontend/src/lib/responsive-config.ts +0 -70
  269. package/frontend/src/lib/types.ts +0 -39
  270. package/frontend/src/lib/utils.ts +0 -10
  271. package/frontend/src/main.tsx +0 -50
  272. package/frontend/src/pages/McpServers.tsx +0 -418
  273. package/frontend/src/pages/Settings.tsx +0 -610
  274. package/frontend/src/pages/project-tasks.tsx +0 -575
  275. package/frontend/src/pages/projects.tsx +0 -18
  276. package/frontend/src/vite-env.d.ts +0 -1
  277. package/frontend/tailwind.config.js +0 -125
  278. package/frontend/tsconfig.json +0 -26
  279. package/frontend/tsconfig.node.json +0 -10
  280. package/frontend/vite.config.ts +0 -33
  281. package/npx-cli/README.md +0 -159
  282. package/npx-cli/automagik-forge-0.1.0.tgz +0 -0
  283. package/npx-cli/automagik-forge-0.1.10.tgz +0 -0
  284. package/npx-cli/package.json +0 -17
  285. package/npx-cli/vibe-kanban-0.0.55.tgz +0 -0
  286. package/pnpm-workspace.yaml +0 -2
  287. package/rust-toolchain.toml +0 -11
  288. package/rustfmt.toml +0 -3
  289. package/scripts/load-env.js +0 -43
  290. package/scripts/mcp_test.js +0 -374
  291. package/scripts/prepare-db.js +0 -45
  292. package/scripts/setup-dev-environment.js +0 -274
  293. package/scripts/start-mcp-sse.js +0 -70
  294. package/scripts/test-debug.js +0 -32
  295. package/scripts/test-mcp-sse.js +0 -138
  296. package/scripts/test-simple.js +0 -44
  297. package/scripts/test-wish-final.js +0 -179
  298. package/scripts/test-wish-system.js +0 -221
  299. package/shared/types.ts +0 -182
  300. package/test-npm-package.sh +0 -42
  301. /package/{npx-cli/bin → bin}/cli.js +0 -0
@@ -1,765 +0,0 @@
1
- //! Gemini executor implementation
2
- //!
3
- //! This module provides Gemini CLI-based task execution with streaming support.
4
-
5
- mod config;
6
- mod streaming;
7
-
8
- use std::{process::Stdio, time::Instant};
9
-
10
- use async_trait::async_trait;
11
- use command_group::{AsyncCommandGroup, AsyncGroupChild};
12
- use config::{
13
- max_chunk_size, max_display_size, max_latency_ms, max_message_size, GeminiStreamConfig,
14
- };
15
- // Re-export for external use
16
- use serde_json::Value;
17
- pub use streaming::GeminiPatchBatch;
18
- use streaming::GeminiStreaming;
19
- use tokio::{io::AsyncWriteExt, process::Command};
20
- use uuid::Uuid;
21
-
22
- use crate::{
23
- executor::{
24
- Executor, ExecutorError, NormalizedConversation, NormalizedEntry, NormalizedEntryType,
25
- },
26
- models::task::Task,
27
- utils::shell::get_shell_command,
28
- };
29
-
30
- /// An executor that uses Gemini CLI to process tasks
31
- pub struct GeminiExecutor;
32
-
33
- #[async_trait]
34
- impl Executor for GeminiExecutor {
35
- async fn spawn(
36
- &self,
37
- pool: &sqlx::SqlitePool,
38
- task_id: Uuid,
39
- worktree_path: &str,
40
- ) -> Result<AsyncGroupChild, ExecutorError> {
41
- // Get the task to fetch its description
42
- let task = Task::find_by_id(pool, task_id)
43
- .await?
44
- .ok_or(ExecutorError::TaskNotFound)?;
45
-
46
- let prompt = if let Some(task_description) = task.description {
47
- format!(
48
- r#"project_id: {}
49
-
50
- Task title: {}
51
- Task description: {}"#,
52
- task.project_id, task.title, task_description
53
- )
54
- } else {
55
- format!(
56
- r#"project_id: {}
57
-
58
- Task title: {}"#,
59
- task.project_id, task.title
60
- )
61
- };
62
-
63
- let mut command = Self::create_gemini_command(worktree_path);
64
-
65
- let mut child = command
66
- .group_spawn() // Create new process group so we can kill entire tree
67
- .map_err(|e| {
68
- crate::executor::SpawnContext::from_command(&command, "Gemini")
69
- .with_task(task_id, Some(task.title.clone()))
70
- .with_context("Gemini CLI execution for new task")
71
- .spawn_error(e)
72
- })?;
73
-
74
- // Write prompt to stdin
75
- if let Some(mut stdin) = child.inner().stdin.take() {
76
- tracing::debug!(
77
- "Writing prompt to Gemini stdin for task {}: {:?}",
78
- task_id,
79
- prompt
80
- );
81
- stdin.write_all(prompt.as_bytes()).await.map_err(|e| {
82
- let context = crate::executor::SpawnContext::from_command(&command, "Gemini")
83
- .with_task(task_id, Some(task.title.clone()))
84
- .with_context("Failed to write prompt to Gemini CLI stdin");
85
- ExecutorError::spawn_failed(e, context)
86
- })?;
87
- stdin.shutdown().await.map_err(|e| {
88
- let context = crate::executor::SpawnContext::from_command(&command, "Gemini")
89
- .with_task(task_id, Some(task.title.clone()))
90
- .with_context("Failed to close Gemini CLI stdin");
91
- ExecutorError::spawn_failed(e, context)
92
- })?;
93
- tracing::info!(
94
- "Successfully sent prompt to Gemini stdin for task {}",
95
- task_id
96
- );
97
- }
98
-
99
- Ok(child)
100
- }
101
-
102
- async fn execute_streaming(
103
- &self,
104
- pool: &sqlx::SqlitePool,
105
- task_id: Uuid,
106
- attempt_id: Uuid,
107
- execution_process_id: Uuid,
108
- worktree_path: &str,
109
- ) -> Result<AsyncGroupChild, ExecutorError> {
110
- tracing::info!(
111
- "Starting Gemini execution for task {} attempt {}",
112
- task_id,
113
- attempt_id
114
- );
115
-
116
- Self::update_session_id(pool, execution_process_id, &attempt_id.to_string()).await;
117
-
118
- let mut child = self.spawn(pool, task_id, worktree_path).await?;
119
-
120
- tracing::info!(
121
- "Gemini process spawned successfully for attempt {}, PID: {:?}",
122
- attempt_id,
123
- child.inner().id()
124
- );
125
-
126
- Self::setup_streaming(pool, &mut child, attempt_id, execution_process_id);
127
-
128
- Ok(child)
129
- }
130
-
131
- async fn spawn_followup(
132
- &self,
133
- pool: &sqlx::SqlitePool,
134
- task_id: Uuid,
135
- session_id: &str,
136
- prompt: &str,
137
- worktree_path: &str,
138
- ) -> Result<AsyncGroupChild, ExecutorError> {
139
- // For Gemini, session_id is the attempt_id
140
- let attempt_id = Uuid::parse_str(session_id)
141
- .map_err(|_| ExecutorError::InvalidSessionId(session_id.to_string()))?;
142
-
143
- let task = self.load_task(pool, task_id).await?;
144
- let resume_context = self.collect_resume_context(pool, &task, attempt_id).await?;
145
- let comprehensive_prompt = self.build_comprehensive_prompt(&task, &resume_context, prompt);
146
- self.spawn_process(worktree_path, &comprehensive_prompt, attempt_id)
147
- .await
148
- }
149
-
150
- async fn execute_followup_streaming(
151
- &self,
152
- pool: &sqlx::SqlitePool,
153
- task_id: Uuid,
154
- attempt_id: Uuid,
155
- execution_process_id: Uuid,
156
- session_id: &str,
157
- prompt: &str,
158
- worktree_path: &str,
159
- ) -> Result<AsyncGroupChild, ExecutorError> {
160
- tracing::info!(
161
- "Starting Gemini follow-up execution for attempt {} (session {})",
162
- attempt_id,
163
- session_id
164
- );
165
-
166
- // For Gemini, session_id is the attempt_id - update it in the database
167
- Self::update_session_id(pool, execution_process_id, session_id).await;
168
-
169
- let mut child = self
170
- .spawn_followup(pool, task_id, session_id, prompt, worktree_path)
171
- .await?;
172
-
173
- tracing::info!(
174
- "Gemini follow-up process spawned successfully for attempt {}, PID: {:?}",
175
- attempt_id,
176
- child.inner().id()
177
- );
178
-
179
- Self::setup_streaming(pool, &mut child, attempt_id, execution_process_id);
180
-
181
- Ok(child)
182
- }
183
-
184
- fn normalize_logs(
185
- &self,
186
- logs: &str,
187
- _worktree_path: &str,
188
- ) -> Result<NormalizedConversation, String> {
189
- let mut entries: Vec<NormalizedEntry> = Vec::new();
190
- let mut parse_errors = Vec::new();
191
-
192
- for (line_num, line) in logs.lines().enumerate() {
193
- let trimmed = line.trim();
194
- if trimmed.is_empty() {
195
- continue;
196
- }
197
-
198
- // Try to parse as JSON first (for NormalizedEntry format)
199
- if trimmed.starts_with('{') {
200
- match serde_json::from_str::<NormalizedEntry>(trimmed) {
201
- Ok(entry) => {
202
- entries.push(entry);
203
- }
204
- Err(e) => {
205
- tracing::warn!(
206
- "Failed to parse JSONL line {} in Gemini logs: {} - Line: {}",
207
- line_num + 1,
208
- e,
209
- trimmed
210
- );
211
- parse_errors.push(format!("Line {}: {}", line_num + 1, e));
212
-
213
- // Create a fallback entry for unrecognized JSON
214
- let fallback_entry = NormalizedEntry {
215
- timestamp: Some(chrono::Utc::now().to_rfc3339()),
216
- entry_type: NormalizedEntryType::SystemMessage,
217
- content: format!("Raw output: {}", trimmed),
218
- metadata: None,
219
- };
220
- entries.push(fallback_entry);
221
- }
222
- }
223
- } else {
224
- // For non-JSON lines, treat as plain text content
225
- let text_entry = NormalizedEntry {
226
- timestamp: Some(chrono::Utc::now().to_rfc3339()),
227
- entry_type: NormalizedEntryType::AssistantMessage,
228
- content: trimmed.to_string(),
229
- metadata: None,
230
- };
231
- entries.push(text_entry);
232
- }
233
- }
234
-
235
- if !parse_errors.is_empty() {
236
- tracing::warn!(
237
- "Gemini normalize_logs encountered {} parse errors: {}",
238
- parse_errors.len(),
239
- parse_errors.join("; ")
240
- );
241
- }
242
-
243
- tracing::debug!(
244
- "Gemini normalize_logs processed {} lines, created {} entries",
245
- logs.lines().count(),
246
- entries.len()
247
- );
248
-
249
- Ok(NormalizedConversation {
250
- entries,
251
- session_id: None, // Session ID is managed directly via database, not extracted from logs
252
- executor_type: "gemini".to_string(),
253
- prompt: None,
254
- summary: None,
255
- })
256
- }
257
-
258
- // Note: Gemini streaming is handled by the Gemini-specific WAL system.
259
- // See emit_content_batch() method which calls GeminiExecutor::push_patch().
260
- }
261
-
262
- impl GeminiExecutor {
263
- /// Create a standardized Gemini CLI command
264
- fn create_gemini_command(worktree_path: &str) -> Command {
265
- let (shell_cmd, shell_arg) = get_shell_command();
266
- let gemini_command = "npx @google/gemini-cli@latest --yolo";
267
-
268
- let mut command = Command::new(shell_cmd);
269
- command
270
- .kill_on_drop(true)
271
- .stdin(Stdio::piped())
272
- .stdout(Stdio::piped())
273
- .stderr(Stdio::piped())
274
- .current_dir(worktree_path)
275
- .arg(shell_arg)
276
- .arg(gemini_command)
277
- .env("NODE_NO_WARNINGS", "1");
278
- command
279
- }
280
-
281
- /// Update executor session ID with error handling
282
- async fn update_session_id(
283
- pool: &sqlx::SqlitePool,
284
- execution_process_id: Uuid,
285
- session_id: &str,
286
- ) {
287
- if let Err(e) = crate::models::executor_session::ExecutorSession::update_session_id(
288
- pool,
289
- execution_process_id,
290
- session_id,
291
- )
292
- .await
293
- {
294
- tracing::error!(
295
- "Failed to update session ID for Gemini execution process {}: {}",
296
- execution_process_id,
297
- e
298
- );
299
- } else {
300
- tracing::info!(
301
- "Updated session ID {} for Gemini execution process {}",
302
- session_id,
303
- execution_process_id
304
- );
305
- }
306
- }
307
-
308
- /// Setup streaming for both stdout and stderr
309
- fn setup_streaming(
310
- pool: &sqlx::SqlitePool,
311
- child: &mut AsyncGroupChild,
312
- attempt_id: Uuid,
313
- execution_process_id: Uuid,
314
- ) {
315
- // Take stdout and stderr pipes for streaming
316
- let stdout = child
317
- .inner()
318
- .stdout
319
- .take()
320
- .expect("Failed to take stdout from child process");
321
- let stderr = child
322
- .inner()
323
- .stderr
324
- .take()
325
- .expect("Failed to take stderr from child process");
326
-
327
- // Start streaming tasks with Gemini-specific line-based message updates
328
- let pool_clone1 = pool.clone();
329
- let pool_clone2 = pool.clone();
330
-
331
- tokio::spawn(Self::stream_gemini_chunked(
332
- stdout,
333
- pool_clone1,
334
- attempt_id,
335
- execution_process_id,
336
- ));
337
- // Use default stderr streaming (no custom parsing)
338
- tokio::spawn(crate::executor::stream_output_to_db(
339
- stderr,
340
- pool_clone2,
341
- attempt_id,
342
- execution_process_id,
343
- false,
344
- ));
345
- }
346
-
347
- /// Push patches to the Gemini WAL system
348
- pub fn push_patch(execution_process_id: Uuid, patches: Vec<Value>, content_length: usize) {
349
- GeminiStreaming::push_patch(execution_process_id, patches, content_length);
350
- }
351
-
352
- /// Get WAL batches for an execution process, optionally filtering by cursor
353
- pub fn get_wal_batches(
354
- execution_process_id: Uuid,
355
- after_batch_id: Option<u64>,
356
- ) -> Option<Vec<GeminiPatchBatch>> {
357
- GeminiStreaming::get_wal_batches(execution_process_id, after_batch_id)
358
- }
359
-
360
- /// Clean up WAL when execution process finishes
361
- pub async fn finalize_execution(
362
- pool: &sqlx::SqlitePool,
363
- execution_process_id: Uuid,
364
- final_buffer: &str,
365
- ) {
366
- GeminiStreaming::finalize_execution(pool, execution_process_id, final_buffer).await;
367
- }
368
-
369
- /// Find the best boundary to split a chunk (newline preferred, sentence fallback)
370
- pub fn find_chunk_boundary(buffer: &str, max_size: usize) -> usize {
371
- GeminiStreaming::find_chunk_boundary(buffer, max_size)
372
- }
373
-
374
- /// Conditionally flush accumulated content to database in chunks
375
- pub async fn maybe_flush_chunk(
376
- pool: &sqlx::SqlitePool,
377
- execution_process_id: Uuid,
378
- buffer: &mut String,
379
- config: &GeminiStreamConfig,
380
- ) {
381
- GeminiStreaming::maybe_flush_chunk(pool, execution_process_id, buffer, config).await;
382
- }
383
-
384
- /// Emit JSON patch for current message state - either "replace" for growing message or "add" for new message.
385
- fn emit_message_patch(
386
- execution_process_id: Uuid,
387
- current_message: &str,
388
- entry_count: &mut usize,
389
- force_new_message: bool,
390
- ) {
391
- if current_message.is_empty() {
392
- return;
393
- }
394
-
395
- if force_new_message && *entry_count > 0 {
396
- // Start new message: add new entry to array
397
- *entry_count += 1;
398
- let patch_vec = vec![serde_json::json!({
399
- "op": "add",
400
- "path": format!("/entries/{}", *entry_count - 1),
401
- "value": {
402
- "timestamp": chrono::Utc::now().to_rfc3339(),
403
- "entry_type": {"type": "assistant_message"},
404
- "content": current_message,
405
- "metadata": null,
406
- }
407
- })];
408
-
409
- Self::push_patch(execution_process_id, patch_vec, current_message.len());
410
- } else {
411
- // Growing message: replace current entry
412
- if *entry_count == 0 {
413
- *entry_count = 1; // Initialize first message
414
- }
415
-
416
- let patch_vec = vec![serde_json::json!({
417
- "op": "replace",
418
- "path": format!("/entries/{}", *entry_count - 1),
419
- "value": {
420
- "timestamp": chrono::Utc::now().to_rfc3339(),
421
- "entry_type": {"type": "assistant_message"},
422
- "content": current_message,
423
- "metadata": null,
424
- }
425
- })];
426
-
427
- Self::push_patch(execution_process_id, patch_vec, current_message.len());
428
- }
429
- }
430
-
431
- /// Emit final content when stream ends
432
- async fn emit_final_content(
433
- execution_process_id: Uuid,
434
- remaining_content: &str,
435
- entry_count: &mut usize,
436
- ) {
437
- if !remaining_content.trim().is_empty() {
438
- Self::emit_message_patch(
439
- execution_process_id,
440
- remaining_content,
441
- entry_count,
442
- false, // Don't force new message for final content
443
- );
444
- }
445
- }
446
-
447
- async fn load_task(
448
- &self,
449
- pool: &sqlx::SqlitePool,
450
- task_id: Uuid,
451
- ) -> Result<Task, ExecutorError> {
452
- Task::find_by_id(pool, task_id)
453
- .await?
454
- .ok_or(ExecutorError::TaskNotFound)
455
- }
456
-
457
- async fn collect_resume_context(
458
- &self,
459
- pool: &sqlx::SqlitePool,
460
- task: &Task,
461
- attempt_id: Uuid,
462
- ) -> Result<crate::models::task_attempt::AttemptResumeContext, ExecutorError> {
463
- crate::models::task_attempt::TaskAttempt::get_attempt_resume_context(
464
- pool,
465
- attempt_id,
466
- task.id,
467
- task.project_id,
468
- )
469
- .await
470
- .map_err(ExecutorError::from)
471
- }
472
-
473
- fn build_comprehensive_prompt(
474
- &self,
475
- task: &Task,
476
- resume_context: &crate::models::task_attempt::AttemptResumeContext,
477
- prompt: &str,
478
- ) -> String {
479
- format!(
480
- r#"RESUME CONTEXT FOR CONTINUING TASK
481
- === TASK INFORMATION ===
482
- Project ID: {}
483
- Task ID: {}
484
- Task Title: {}
485
- Task Description: {}
486
- === EXECUTION HISTORY ===
487
- The following is the execution history from this task attempt:
488
- {}
489
- === CURRENT CHANGES ===
490
- The following git diff shows changes made from the base branch to the current state:
491
- ```diff
492
- {}
493
- ```
494
- === CURRENT REQUEST ===
495
- {}
496
- === INSTRUCTIONS ===
497
- You are continuing work on the above task. The execution history shows what has been done previously, and the git diff shows the current state of all changes. Please continue from where the previous execution left off, taking into account all the context provided above.
498
- "#,
499
- task.project_id,
500
- task.id,
501
- task.title,
502
- task.description
503
- .as_deref()
504
- .unwrap_or("No description provided"),
505
- if resume_context.execution_history.trim().is_empty() {
506
- "(No previous execution history)"
507
- } else {
508
- &resume_context.execution_history
509
- },
510
- if resume_context.cumulative_diffs.trim().is_empty() {
511
- "(No changes detected)"
512
- } else {
513
- &resume_context.cumulative_diffs
514
- },
515
- prompt
516
- )
517
- }
518
-
519
- async fn spawn_process(
520
- &self,
521
- worktree_path: &str,
522
- comprehensive_prompt: &str,
523
- attempt_id: Uuid,
524
- ) -> Result<AsyncGroupChild, ExecutorError> {
525
- tracing::info!(
526
- "Spawning Gemini followup execution for attempt {} with resume context ({} chars)",
527
- attempt_id,
528
- comprehensive_prompt.len()
529
- );
530
-
531
- let mut command = GeminiExecutor::create_gemini_command(worktree_path);
532
-
533
- let mut child = command.group_spawn().map_err(|e| {
534
- crate::executor::SpawnContext::from_command(&command, "Gemini")
535
- .with_context(format!(
536
- "Gemini CLI followup execution with context for attempt {}",
537
- attempt_id
538
- ))
539
- .spawn_error(e)
540
- })?;
541
-
542
- self.send_prompt_to_stdin(&mut child, &command, comprehensive_prompt, attempt_id)
543
- .await?;
544
- Ok(child)
545
- }
546
-
547
- async fn send_prompt_to_stdin(
548
- &self,
549
- child: &mut AsyncGroupChild,
550
- command: &Command,
551
- comprehensive_prompt: &str,
552
- attempt_id: Uuid,
553
- ) -> Result<(), ExecutorError> {
554
- if let Some(mut stdin) = child.inner().stdin.take() {
555
- tracing::debug!(
556
- "Sending resume context to Gemini for attempt {}: {} characters",
557
- attempt_id,
558
- comprehensive_prompt.len()
559
- );
560
-
561
- stdin
562
- .write_all(comprehensive_prompt.as_bytes())
563
- .await
564
- .map_err(|e| {
565
- let context = crate::executor::SpawnContext::from_command(command, "Gemini")
566
- .with_context(format!(
567
- "Failed to write resume prompt to Gemini CLI stdin for attempt {}",
568
- attempt_id
569
- ));
570
- ExecutorError::spawn_failed(e, context)
571
- })?;
572
-
573
- stdin.shutdown().await.map_err(|e| {
574
- let context = crate::executor::SpawnContext::from_command(command, "Gemini")
575
- .with_context(format!(
576
- "Failed to close Gemini CLI stdin for attempt {}",
577
- attempt_id
578
- ));
579
- ExecutorError::spawn_failed(e, context)
580
- })?;
581
-
582
- tracing::info!(
583
- "Successfully sent resume context to Gemini for attempt {}",
584
- attempt_id
585
- );
586
- }
587
-
588
- Ok(())
589
- }
590
-
591
- /// Format Gemini CLI output by inserting line breaks where periods are directly
592
- /// followed by capital letters (common Gemini CLI formatting issue).
593
- /// Handles both intra-chunk and cross-chunk period-to-capital transitions.
594
- fn format_gemini_output(content: &str, accumulated_message: &str) -> String {
595
- let mut result = String::with_capacity(content.len() + 100); // Reserve some extra space for potential newlines
596
- let chars: Vec<char> = content.chars().collect();
597
-
598
- // Check for cross-chunk boundary: previous chunk ended with period, current starts with capital
599
- if !accumulated_message.is_empty() && !content.is_empty() {
600
- let ends_with_period = accumulated_message.ends_with('.');
601
- let starts_with_capital = chars
602
- .first()
603
- .map(|&c| c.is_uppercase() && c.is_alphabetic())
604
- .unwrap_or(false);
605
-
606
- if ends_with_period && starts_with_capital {
607
- result.push('\n');
608
- }
609
- }
610
-
611
- // Handle intra-chunk period-to-capital transitions
612
- for i in 0..chars.len() {
613
- result.push(chars[i]);
614
-
615
- // Check if current char is '.' and next char is uppercase letter (no space between)
616
- if chars[i] == '.' && i + 1 < chars.len() {
617
- let next_char = chars[i + 1];
618
- if next_char.is_uppercase() && next_char.is_alphabetic() {
619
- result.push('\n');
620
- }
621
- }
622
- }
623
-
624
- result
625
- }
626
-
627
- /// Stream Gemini output with dual-buffer approach: chunks for UI updates, messages for storage.
628
- ///
629
- /// **Chunks** (~2KB): Frequent UI updates using "replace" patches for smooth streaming
630
- /// **Messages** (~8KB): Logical boundaries using "add" patches for new entries
631
- /// **Consistent WAL/DB**: Both systems see same message structure via JSON patches
632
- pub async fn stream_gemini_chunked(
633
- mut output: impl tokio::io::AsyncRead + Unpin,
634
- pool: sqlx::SqlitePool,
635
- attempt_id: Uuid,
636
- execution_process_id: Uuid,
637
- ) {
638
- use tokio::io::{AsyncReadExt, BufReader};
639
-
640
- let chunk_limit = max_chunk_size();
641
- let display_chunk_size = max_display_size(); // ~2KB for UI updates
642
- let message_boundary_size = max_message_size(); // ~8KB for new message boundaries
643
- let max_latency = std::time::Duration::from_millis(max_latency_ms());
644
-
645
- let mut reader = BufReader::new(&mut output);
646
-
647
- // Dual buffers: chunk buffer for UI, message buffer for DB
648
- let mut current_message = String::new(); // Current assistant message content
649
- let mut db_buffer = String::new(); // Buffer for database storage (using ChunkStore)
650
- let mut entry_count = 0usize; // Track assistant message entries
651
-
652
- let mut read_buf = vec![0u8; chunk_limit.min(max_chunk_size())]; // Use configurable chunk limit, capped for memory efficiency
653
- let mut last_chunk_emit = Instant::now();
654
-
655
- // Configuration for WAL and DB management
656
- let config = GeminiStreamConfig::default();
657
-
658
- tracing::info!(
659
- "Starting dual-buffer Gemini streaming for attempt {} (chunks: {}B, messages: {}B)",
660
- attempt_id,
661
- display_chunk_size,
662
- message_boundary_size
663
- );
664
-
665
- loop {
666
- match reader.read(&mut read_buf).await {
667
- Ok(0) => {
668
- // EOF: emit final content and flush to database
669
- Self::emit_final_content(
670
- execution_process_id,
671
- &current_message,
672
- &mut entry_count,
673
- )
674
- .await;
675
-
676
- // Flush any remaining database buffer
677
- Self::finalize_execution(&pool, execution_process_id, &db_buffer).await;
678
- break;
679
- }
680
- Ok(n) => {
681
- // Convert bytes to string and apply Gemini-specific formatting
682
- let raw_chunk = String::from_utf8_lossy(&read_buf[..n]);
683
- let formatted_chunk = Self::format_gemini_output(&raw_chunk, &current_message);
684
-
685
- // Add to both buffers
686
- current_message.push_str(&formatted_chunk);
687
- db_buffer.push_str(&formatted_chunk);
688
-
689
- // 1. Check for chunk emission (frequent UI updates ~2KB)
690
- let should_emit_chunk = current_message.len() >= display_chunk_size
691
- || (last_chunk_emit.elapsed() >= max_latency
692
- && !current_message.is_empty());
693
-
694
- if should_emit_chunk {
695
- // Emit "replace" patch for growing message (smooth UI)
696
- Self::emit_message_patch(
697
- execution_process_id,
698
- &current_message,
699
- &mut entry_count,
700
- false, // Not forcing new message
701
- );
702
- last_chunk_emit = Instant::now();
703
- }
704
-
705
- // 2. Check for message boundary (new assistant message ~8KB)
706
- let should_start_new_message = current_message.len() >= message_boundary_size;
707
-
708
- if should_start_new_message {
709
- // Find optimal boundary for new message
710
- let boundary =
711
- Self::find_chunk_boundary(&current_message, message_boundary_size);
712
-
713
- if boundary > 0 && boundary < current_message.len() {
714
- // Split at boundary: complete current message, start new one
715
- let completed_message = current_message[..boundary].to_string();
716
- let remaining_content = current_message[boundary..].to_string();
717
-
718
- // CRITICAL FIX: Only emit "replace" patch to complete current message
719
- // Do NOT emit "add" patch as it shifts existing database entries
720
- Self::emit_message_patch(
721
- execution_process_id,
722
- &completed_message,
723
- &mut entry_count,
724
- false, // Complete current message
725
- );
726
-
727
- // Store the completed message to database
728
- // This ensures the database gets the completed content at the boundary
729
- Self::maybe_flush_chunk(
730
- &pool,
731
- execution_process_id,
732
- &mut db_buffer,
733
- &config,
734
- )
735
- .await;
736
-
737
- // Start fresh message with remaining content (no WAL patch yet)
738
- // Next chunk emission will create "replace" patch for entry_count + 1
739
- current_message = remaining_content;
740
- entry_count += 1; // Move to next entry index for future patches
741
- }
742
- }
743
-
744
- // 3. Flush to database (same boundary detection)
745
- Self::maybe_flush_chunk(&pool, execution_process_id, &mut db_buffer, &config)
746
- .await;
747
- }
748
- Err(e) => {
749
- tracing::error!(
750
- "Error reading stdout for Gemini attempt {}: {}",
751
- attempt_id,
752
- e
753
- );
754
- break;
755
- }
756
- }
757
- }
758
-
759
- tracing::info!(
760
- "Dual-buffer Gemini streaming completed for attempt {} ({} messages)",
761
- attempt_id,
762
- entry_count
763
- );
764
- }
765
- }