bonecode 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (575) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -50
  3. package/bone/output/agent/.dockerignore +7 -7
  4. package/bone/output/agent/.env.example +36 -36
  5. package/bone/output/agent/.github/workflows/ci.yaml +58 -58
  6. package/bone/output/agent/AgentDomain.bone.map +349 -349
  7. package/bone/output/agent/AgentDomain.postman_collection.json +957 -957
  8. package/bone/output/agent/Dockerfile +22 -22
  9. package/bone/output/agent/README.md +47 -47
  10. package/bone/output/agent/admin/index.html +739 -739
  11. package/bone/output/agent/docker-compose.yaml +22 -22
  12. package/bone/output/agent/k8s/deployment.yaml +75 -75
  13. package/bone/output/agent/migrations/agent.sql +36 -36
  14. package/bone/output/agent/migrations/agent_instance.sql +36 -36
  15. package/bone/output/agent/migrations/audit_log.sql +18 -18
  16. package/bone/output/agent/migrations/build_step.sql +34 -34
  17. package/bone/output/agent/migrations/event_outbox.sql +31 -31
  18. package/bone/output/agent/migrations/plan.sql +30 -30
  19. package/bone/output/agent/migrations/task.sql +30 -30
  20. package/bone/output/agent/migrations/tool_call.sql +33 -33
  21. package/bone/output/agent/openapi.yaml +1116 -1116
  22. package/bone/output/agent/package.json +35 -35
  23. package/bone/output/agent/schema.graphql +233 -233
  24. package/bone/output/agent/sdk/client.ts +231 -231
  25. package/bone/output/agent/src/algorithms.ts +2 -2
  26. package/bone/output/agent/src/audit.ts +44 -44
  27. package/bone/output/agent/src/auth.ts +57 -57
  28. package/bone/output/agent/src/cron.ts +12 -12
  29. package/bone/output/agent/src/db.ts +31 -31
  30. package/bone/output/agent/src/debug.ts +66 -66
  31. package/bone/output/agent/src/events.ts +243 -243
  32. package/bone/output/agent/src/extensions.ts +54 -54
  33. package/bone/output/agent/src/failure_rules.ts +322 -322
  34. package/bone/output/agent/src/flows.ts +168 -168
  35. package/bone/output/agent/src/health.ts +43 -43
  36. package/bone/output/agent/src/index.ts +99 -99
  37. package/bone/output/agent/src/logger.ts +69 -66
  38. package/bone/output/agent/src/metrics.ts +75 -75
  39. package/bone/output/agent/src/migrate.ts +351 -351
  40. package/bone/output/agent/src/migration_diff.ts +108 -108
  41. package/bone/output/agent/src/notify.ts +125 -125
  42. package/bone/output/agent/src/routes/plan.ts +91 -91
  43. package/bone/output/agent/src/routes/task.ts +105 -105
  44. package/bone/output/agent/src/routes/tool_call.ts +166 -166
  45. package/bone/output/agent/src/schemas.ts +384 -384
  46. package/bone/output/agent/src/state_machines/agent_instance.ts +24 -24
  47. package/bone/output/agent/src/state_machines/build_step.ts +22 -22
  48. package/bone/output/agent/src/state_machines/plan.ts +22 -22
  49. package/bone/output/agent/src/state_machines/task.ts +22 -22
  50. package/bone/output/agent/src/state_machines/tool_call.ts +22 -22
  51. package/bone/output/agent/src/tests.ts +361 -361
  52. package/bone/output/agent/src/websocket.ts +200 -200
  53. package/bone/output/agent/tsconfig.json +24 -24
  54. package/bone/output/rag/.dockerignore +7 -7
  55. package/bone/output/rag/.env.example +36 -36
  56. package/bone/output/rag/.github/workflows/ci.yaml +58 -58
  57. package/bone/output/rag/Dockerfile +22 -22
  58. package/bone/output/rag/RAGDomain.bone.map +286 -286
  59. package/bone/output/rag/RAGDomain.postman_collection.json +922 -922
  60. package/bone/output/rag/README.md +47 -47
  61. package/bone/output/rag/admin/index.html +817 -817
  62. package/bone/output/rag/docker-compose.yaml +22 -22
  63. package/bone/output/rag/k8s/deployment.yaml +75 -75
  64. package/bone/output/rag/migrations/audit_log.sql +18 -18
  65. package/bone/output/rag/migrations/code_chunk.sql +34 -34
  66. package/bone/output/rag/migrations/code_file.sql +33 -33
  67. package/bone/output/rag/migrations/event_outbox.sql +31 -31
  68. package/bone/output/rag/migrations/indexing_job.sql +33 -33
  69. package/bone/output/rag/migrations/knowledge_base.sql +35 -35
  70. package/bone/output/rag/migrations/memory_entry.sql +34 -34
  71. package/bone/output/rag/openapi.yaml +1097 -1097
  72. package/bone/output/rag/package.json +35 -35
  73. package/bone/output/rag/schema.graphql +245 -245
  74. package/bone/output/rag/sdk/client.ts +234 -234
  75. package/bone/output/rag/src/algorithms.ts +2 -2
  76. package/bone/output/rag/src/audit.ts +37 -37
  77. package/bone/output/rag/src/auth.ts +57 -57
  78. package/bone/output/rag/src/cron.ts +12 -12
  79. package/bone/output/rag/src/db.ts +31 -31
  80. package/bone/output/rag/src/debug.ts +66 -66
  81. package/bone/output/rag/src/events.ts +243 -243
  82. package/bone/output/rag/src/extensions.ts +350 -350
  83. package/bone/output/rag/src/failure_rules.ts +314 -314
  84. package/bone/output/rag/src/flows.ts +239 -239
  85. package/bone/output/rag/src/health.ts +43 -43
  86. package/bone/output/rag/src/index.ts +94 -94
  87. package/bone/output/rag/src/logger.ts +69 -66
  88. package/bone/output/rag/src/metrics.ts +75 -75
  89. package/bone/output/rag/src/migrate.ts +363 -363
  90. package/bone/output/rag/src/migration_diff.ts +108 -108
  91. package/bone/output/rag/src/notify.ts +99 -99
  92. package/bone/output/rag/src/routes/code_chunk.ts +75 -75
  93. package/bone/output/rag/src/routes/code_file.ts +101 -101
  94. package/bone/output/rag/src/routes/indexing_job.ts +87 -87
  95. package/bone/output/rag/src/routes/knowledge_base.ts +230 -230
  96. package/bone/output/rag/src/routes/memory_entry.ts +87 -87
  97. package/bone/output/rag/src/schemas.ts +394 -394
  98. package/bone/output/rag/src/state_machines/code_file.ts +23 -23
  99. package/bone/output/rag/src/state_machines/indexing_job.ts +22 -22
  100. package/bone/output/rag/src/state_machines/knowledge_base.ts +23 -23
  101. package/bone/output/rag/src/state_machines/memory_entry.ts +20 -20
  102. package/bone/output/rag/src/tests.ts +339 -339
  103. package/bone/output/rag/tsconfig.json +24 -24
  104. package/bone/output/session/.dockerignore +7 -7
  105. package/bone/output/session/.env.example +36 -36
  106. package/bone/output/session/.github/workflows/ci.yaml +58 -58
  107. package/bone/output/session/Dockerfile +22 -22
  108. package/bone/output/session/README.md +47 -47
  109. package/bone/output/session/SessionDomain.bone.map +349 -349
  110. package/bone/output/session/SessionDomain.postman_collection.json +957 -957
  111. package/bone/output/session/admin/index.html +666 -666
  112. package/bone/output/session/docker-compose.yaml +22 -22
  113. package/bone/output/session/k8s/deployment.yaml +75 -75
  114. package/bone/output/session/migrations/audit_log.sql +18 -18
  115. package/bone/output/session/migrations/event_outbox.sql +31 -31
  116. package/bone/output/session/migrations/message.sql +31 -31
  117. package/bone/output/session/migrations/part.sql +28 -28
  118. package/bone/output/session/migrations/permission.sql +28 -28
  119. package/bone/output/session/migrations/project.sql +28 -28
  120. package/bone/output/session/migrations/session.sql +38 -38
  121. package/bone/output/session/openapi.yaml +1101 -1101
  122. package/bone/output/session/package.json +35 -35
  123. package/bone/output/session/schema.graphql +222 -222
  124. package/bone/output/session/sdk/client.ts +225 -225
  125. package/bone/output/session/src/algorithms.ts +2 -2
  126. package/bone/output/session/src/audit.ts +44 -44
  127. package/bone/output/session/src/auth.ts +57 -57
  128. package/bone/output/session/src/cron.ts +12 -12
  129. package/bone/output/session/src/db.ts +31 -31
  130. package/bone/output/session/src/debug.ts +66 -66
  131. package/bone/output/session/src/events.ts +270 -270
  132. package/bone/output/session/src/extensions.ts +215 -215
  133. package/bone/output/session/src/failure_rules.ts +283 -283
  134. package/bone/output/session/src/flows.ts +168 -168
  135. package/bone/output/session/src/health.ts +43 -43
  136. package/bone/output/session/src/index.ts +99 -99
  137. package/bone/output/session/src/logger.ts +67 -66
  138. package/bone/output/session/src/metrics.ts +75 -75
  139. package/bone/output/session/src/migrate.ts +331 -331
  140. package/bone/output/session/src/migration_diff.ts +108 -108
  141. package/bone/output/session/src/notify.ts +112 -112
  142. package/bone/output/session/src/routes/message.ts +93 -93
  143. package/bone/output/session/src/routes/part.ts +79 -79
  144. package/bone/output/session/src/routes/permission.ts +79 -79
  145. package/bone/output/session/src/routes/project.ts +79 -79
  146. package/bone/output/session/src/routes/session.ts +294 -294
  147. package/bone/output/session/src/schemas.ts +357 -357
  148. package/bone/output/session/src/state_machines/session.ts +23 -23
  149. package/bone/output/session/src/tests.ts +325 -325
  150. package/bone/output/session/src/websocket.ts +223 -200
  151. package/bone/output/session/tsconfig.json +24 -24
  152. package/bone/output/workspace/.dockerignore +7 -7
  153. package/bone/output/workspace/.env.example +36 -36
  154. package/bone/output/workspace/.github/workflows/ci.yaml +58 -58
  155. package/bone/output/workspace/Dockerfile +22 -22
  156. package/bone/output/workspace/README.md +45 -45
  157. package/bone/output/workspace/WorkspaceDomain.bone.map +188 -188
  158. package/bone/output/workspace/WorkspaceDomain.postman_collection.json +620 -620
  159. package/bone/output/workspace/admin/index.html +484 -484
  160. package/bone/output/workspace/docker-compose.yaml +22 -22
  161. package/bone/output/workspace/k8s/deployment.yaml +75 -75
  162. package/bone/output/workspace/migrations/audit_log.sql +18 -18
  163. package/bone/output/workspace/migrations/codebase.sql +34 -34
  164. package/bone/output/workspace/migrations/event_outbox.sql +31 -31
  165. package/bone/output/workspace/migrations/snapshot.sql +32 -32
  166. package/bone/output/workspace/migrations/workspace.sql +33 -33
  167. package/bone/output/workspace/openapi.yaml +721 -721
  168. package/bone/output/workspace/package.json +35 -35
  169. package/bone/output/workspace/schema.graphql +153 -153
  170. package/bone/output/workspace/sdk/client.ts +155 -155
  171. package/bone/output/workspace/src/algorithms.ts +2 -2
  172. package/bone/output/workspace/src/audit.ts +37 -37
  173. package/bone/output/workspace/src/auth.ts +57 -57
  174. package/bone/output/workspace/src/cron.ts +12 -12
  175. package/bone/output/workspace/src/db.ts +31 -31
  176. package/bone/output/workspace/src/debug.ts +66 -66
  177. package/bone/output/workspace/src/events.ts +243 -243
  178. package/bone/output/workspace/src/extensions.ts +44 -44
  179. package/bone/output/workspace/src/failure_rules.ts +152 -152
  180. package/bone/output/workspace/src/health.ts +43 -43
  181. package/bone/output/workspace/src/index.ts +88 -88
  182. package/bone/output/workspace/src/logger.ts +69 -66
  183. package/bone/output/workspace/src/metrics.ts +75 -75
  184. package/bone/output/workspace/src/migrate.ts +219 -219
  185. package/bone/output/workspace/src/migration_diff.ts +108 -108
  186. package/bone/output/workspace/src/notify.ts +73 -73
  187. package/bone/output/workspace/src/routes/codebase.ts +87 -87
  188. package/bone/output/workspace/src/routes/snapshot.ts +127 -127
  189. package/bone/output/workspace/src/routes/workspace.ts +190 -190
  190. package/bone/output/workspace/src/schemas.ts +231 -231
  191. package/bone/output/workspace/src/state_machines/codebase.ts +21 -21
  192. package/bone/output/workspace/src/state_machines/snapshot.ts +20 -20
  193. package/bone/output/workspace/src/state_machines/workspace.ts +21 -21
  194. package/bone/output/workspace/src/tests.ts +248 -248
  195. package/bone/output/workspace/tsconfig.json +24 -24
  196. package/compat/opencode_adapter.ts +94 -17
  197. package/package.json +15 -2
  198. package/src/cli.ts +66 -107
  199. package/src/db_adapter.ts +354 -0
  200. package/src/engine/account/account.sql.ts +39 -39
  201. package/src/engine/account/account.ts +456 -456
  202. package/src/engine/account/repo.ts +166 -166
  203. package/src/engine/account/schema.ts +99 -99
  204. package/src/engine/account/url.ts +8 -8
  205. package/src/engine/acp/README.md +174 -174
  206. package/src/engine/acp/agent.ts +1968 -1968
  207. package/src/engine/acp/runtime.ts +22 -22
  208. package/src/engine/acp/session.ts +122 -122
  209. package/src/engine/acp/types.ts +24 -24
  210. package/src/engine/agent/agent.ts +463 -463
  211. package/src/engine/agent/generate.txt +75 -75
  212. package/src/engine/agent/prompt/compaction.txt +9 -9
  213. package/src/engine/agent/prompt/explore.txt +18 -18
  214. package/src/engine/agent/prompt/scout.txt +36 -36
  215. package/src/engine/agent/prompt/summary.txt +11 -11
  216. package/src/engine/agent/prompt/title.txt +44 -44
  217. package/src/engine/agent/subagent-permissions.ts +34 -34
  218. package/src/engine/auth/index.ts +96 -96
  219. package/src/engine/background/background/job.ts +200 -200
  220. package/src/engine/background/job.ts +200 -200
  221. package/src/engine/bus/bus-event.ts +45 -45
  222. package/src/engine/bus/global.ts +22 -22
  223. package/src/engine/bus/index.ts +203 -203
  224. package/src/engine/command/command/index.ts +181 -181
  225. package/src/engine/command/command/template/initialize.txt +66 -66
  226. package/src/engine/command/command/template/review.txt +101 -101
  227. package/src/engine/command/index.ts +181 -181
  228. package/src/engine/command/template/initialize.txt +66 -66
  229. package/src/engine/command/template/review.txt +101 -101
  230. package/src/engine/config/agent.ts +172 -172
  231. package/src/engine/config/attachment.ts +25 -25
  232. package/src/engine/config/command.ts +62 -62
  233. package/src/engine/config/config.ts +833 -833
  234. package/src/engine/config/console-state.ts +14 -14
  235. package/src/engine/config/entry-name.ts +16 -16
  236. package/src/engine/config/error.ts +23 -23
  237. package/src/engine/config/formatter.ts +13 -13
  238. package/src/engine/config/layout.ts +6 -6
  239. package/src/engine/config/lsp.ts +43 -43
  240. package/src/engine/config/managed.ts +71 -71
  241. package/src/engine/config/markdown.ts +96 -96
  242. package/src/engine/config/mcp.ts +56 -56
  243. package/src/engine/config/model-id.ts +5 -5
  244. package/src/engine/config/parse.ts +79 -79
  245. package/src/engine/config/paths.ts +45 -45
  246. package/src/engine/config/permission.ts +58 -58
  247. package/src/engine/config/plugin.ts +84 -84
  248. package/src/engine/config/provider.ts +111 -111
  249. package/src/engine/config/reference.ts +23 -23
  250. package/src/engine/config/server.ts +19 -19
  251. package/src/engine/config/skills.ts +14 -14
  252. package/src/engine/config/variable.ts +90 -90
  253. package/src/engine/control-plane/adapters/index.ts +41 -41
  254. package/src/engine/control-plane/adapters/worktree.ts +96 -96
  255. package/src/engine/control-plane/dev/README.md +19 -19
  256. package/src/engine/control-plane/dev/debug-workspace-plugin.ts +73 -73
  257. package/src/engine/control-plane/schema.ts +14 -14
  258. package/src/engine/control-plane/types.ts +59 -59
  259. package/src/engine/control-plane/util.ts +39 -39
  260. package/src/engine/control-plane/workspace-adapter-runtime.ts +51 -51
  261. package/src/engine/control-plane/workspace-context.ts +26 -26
  262. package/src/engine/control-plane/workspace.sql.ts +20 -20
  263. package/src/engine/control-plane/workspace.ts +1072 -1072
  264. package/src/engine/data-migration.ts +161 -161
  265. package/src/engine/effect/app-runtime.ts +143 -143
  266. package/src/engine/effect/bootstrap-runtime.ts +29 -29
  267. package/src/engine/effect/bridge.ts +84 -84
  268. package/src/engine/effect/config-service.ts +67 -67
  269. package/src/engine/effect/instance-ref.ts +11 -11
  270. package/src/engine/effect/instance-registry.ts +12 -12
  271. package/src/engine/effect/instance-state.ts +72 -72
  272. package/src/engine/effect/promise.ts +17 -17
  273. package/src/engine/effect/run-service.ts +47 -47
  274. package/src/engine/effect/runner.ts +217 -217
  275. package/src/engine/effect/runtime-flags.ts +74 -74
  276. package/src/engine/effect/service-use.ts +38 -38
  277. package/src/engine/env/index.ts +37 -37
  278. package/src/engine/event-v2-bridge.ts +89 -89
  279. package/src/engine/file/file/ignore.ts +81 -81
  280. package/src/engine/file/file/index.ts +651 -651
  281. package/src/engine/file/file/protected.ts +59 -59
  282. package/src/engine/file/file/ripgrep.ts +481 -481
  283. package/src/engine/file/file/watcher.ts +167 -167
  284. package/src/engine/file/ignore.ts +81 -81
  285. package/src/engine/file/index.ts +651 -651
  286. package/src/engine/file/protected.ts +59 -59
  287. package/src/engine/file/ripgrep.ts +481 -481
  288. package/src/engine/file/watcher.ts +167 -167
  289. package/src/engine/format/format/formatter.ts +404 -404
  290. package/src/engine/format/format/index.ts +209 -209
  291. package/src/engine/format/formatter.ts +404 -404
  292. package/src/engine/format/index.ts +209 -209
  293. package/src/engine/git/git/index.ts +347 -347
  294. package/src/engine/git/index.ts +347 -347
  295. package/src/engine/id/id.ts +80 -80
  296. package/src/engine/ide/index.ts +70 -70
  297. package/src/engine/image/image/image.ts +176 -176
  298. package/src/engine/image/image.ts +176 -176
  299. package/src/engine/index.ts +251 -251
  300. package/src/engine/installation/index.ts +327 -327
  301. package/src/engine/lsp/client.ts +707 -707
  302. package/src/engine/lsp/diagnostic.ts +29 -29
  303. package/src/engine/lsp/language.ts +121 -121
  304. package/src/engine/lsp/launch.ts +21 -21
  305. package/src/engine/lsp/lsp/client.ts +707 -707
  306. package/src/engine/lsp/lsp/diagnostic.ts +29 -29
  307. package/src/engine/lsp/lsp/language.ts +121 -121
  308. package/src/engine/lsp/lsp/launch.ts +21 -21
  309. package/src/engine/lsp/lsp/lsp.ts +507 -507
  310. package/src/engine/lsp/lsp/server.ts +2064 -2064
  311. package/src/engine/lsp/lsp.ts +507 -507
  312. package/src/engine/lsp/server.ts +2064 -2064
  313. package/src/engine/mcp/auth.ts +146 -146
  314. package/src/engine/mcp/index.ts +958 -958
  315. package/src/engine/mcp/mcp/auth.ts +146 -146
  316. package/src/engine/mcp/mcp/index.ts +958 -958
  317. package/src/engine/mcp/mcp/oauth-callback.ts +232 -232
  318. package/src/engine/mcp/mcp/oauth-provider.ts +214 -214
  319. package/src/engine/mcp/oauth-callback.ts +232 -232
  320. package/src/engine/mcp/oauth-provider.ts +214 -214
  321. package/src/engine/node.ts +6 -6
  322. package/src/engine/patch/index.ts +689 -689
  323. package/src/engine/patch/patch/index.ts +689 -689
  324. package/src/engine/permission/arity.ts +163 -163
  325. package/src/engine/permission/evaluate.ts +15 -15
  326. package/src/engine/permission/index.ts +306 -306
  327. package/src/engine/permission/permission/arity.ts +163 -163
  328. package/src/engine/permission/permission/evaluate.ts +15 -15
  329. package/src/engine/permission/permission/index.ts +306 -306
  330. package/src/engine/permission/permission/schema.ts +13 -13
  331. package/src/engine/permission/schema.ts +13 -13
  332. package/src/engine/plugin/azure.ts +26 -26
  333. package/src/engine/plugin/cloudflare.ts +76 -76
  334. package/src/engine/plugin/codex.ts +622 -622
  335. package/src/engine/plugin/digitalocean.ts +411 -411
  336. package/src/engine/plugin/github-copilot/copilot.ts +394 -394
  337. package/src/engine/plugin/github-copilot/models.ts +196 -196
  338. package/src/engine/plugin/index.ts +295 -295
  339. package/src/engine/plugin/install.ts +439 -439
  340. package/src/engine/plugin/loader.ts +216 -216
  341. package/src/engine/plugin/meta.ts +188 -188
  342. package/src/engine/plugin/shared.ts +323 -323
  343. package/src/engine/project/bootstrap-service.ts +9 -9
  344. package/src/engine/project/bootstrap.ts +75 -75
  345. package/src/engine/project/instance-context.ts +24 -24
  346. package/src/engine/project/instance-layer.ts +11 -11
  347. package/src/engine/project/instance-runtime.ts +16 -16
  348. package/src/engine/project/instance-store.ts +193 -193
  349. package/src/engine/project/project.sql.ts +17 -17
  350. package/src/engine/project/project.ts +537 -537
  351. package/src/engine/project/schema.ts +13 -13
  352. package/src/engine/project/vcs.ts +405 -405
  353. package/src/engine/provider/auth.ts +225 -225
  354. package/src/engine/provider/error.ts +204 -204
  355. package/src/engine/provider/model-status.ts +8 -8
  356. package/src/engine/provider/provider.ts +1843 -1843
  357. package/src/engine/provider/schema.ts +30 -30
  358. package/src/engine/provider/transform.ts +1376 -1376
  359. package/src/engine/pty/index.ts +365 -365
  360. package/src/engine/pty/input.ts +24 -24
  361. package/src/engine/pty/pty/index.ts +365 -365
  362. package/src/engine/pty/pty/input.ts +24 -24
  363. package/src/engine/pty/pty/pty.bun.ts +26 -26
  364. package/src/engine/pty/pty/pty.node.ts +27 -27
  365. package/src/engine/pty/pty/pty.ts +25 -25
  366. package/src/engine/pty/pty/schema.ts +14 -14
  367. package/src/engine/pty/pty/ticket.ts +68 -68
  368. package/src/engine/pty/pty.bun.ts +26 -26
  369. package/src/engine/pty/pty.node.ts +27 -27
  370. package/src/engine/pty/pty.ts +25 -25
  371. package/src/engine/pty/schema.ts +14 -14
  372. package/src/engine/pty/ticket.ts +68 -68
  373. package/src/engine/question/index.ts +213 -213
  374. package/src/engine/question/question/index.ts +213 -213
  375. package/src/engine/question/question/schema.ts +10 -10
  376. package/src/engine/question/schema.ts +10 -10
  377. package/src/engine/reference/reference/reference.ts +241 -241
  378. package/src/engine/reference/reference/repository-cache.ts +147 -147
  379. package/src/engine/reference/reference.ts +241 -241
  380. package/src/engine/reference/repository-cache.ts +147 -147
  381. package/src/engine/session/compaction.ts +651 -651
  382. package/src/engine/session/instruction.ts +238 -238
  383. package/src/engine/session/llm.ts +459 -459
  384. package/src/engine/session/message-error.ts +14 -14
  385. package/src/engine/session/message-v2.ts +1202 -1202
  386. package/src/engine/session/message.ts +146 -146
  387. package/src/engine/session/overflow.ts +32 -32
  388. package/src/engine/session/processor.ts +823 -823
  389. package/src/engine/session/prompt/anthropic.txt +105 -105
  390. package/src/engine/session/prompt/beast.txt +147 -147
  391. package/src/engine/session/prompt/build-switch.txt +5 -5
  392. package/src/engine/session/prompt/codex.txt +79 -79
  393. package/src/engine/session/prompt/copilot-gpt-5.txt +143 -143
  394. package/src/engine/session/prompt/default.txt +105 -105
  395. package/src/engine/session/prompt/gemini.txt +155 -155
  396. package/src/engine/session/prompt/gpt.txt +107 -107
  397. package/src/engine/session/prompt/kimi.txt +95 -95
  398. package/src/engine/session/prompt/max-steps.txt +15 -15
  399. package/src/engine/session/prompt/plan-reminder-anthropic.txt +67 -67
  400. package/src/engine/session/prompt/plan.txt +26 -26
  401. package/src/engine/session/prompt/trinity.txt +97 -97
  402. package/src/engine/session/prompt.ts +66 -9
  403. package/src/engine/session/retry.ts +200 -200
  404. package/src/engine/session/revert.ts +162 -162
  405. package/src/engine/session/run-state.ts +153 -153
  406. package/src/engine/session/schema.ts +26 -26
  407. package/src/engine/session/session.sql.ts +137 -137
  408. package/src/engine/session/session.ts +1011 -1011
  409. package/src/engine/session/status.ts +94 -94
  410. package/src/engine/session/summary.ts +164 -164
  411. package/src/engine/session/system.ts +84 -84
  412. package/src/engine/session/todo.ts +81 -81
  413. package/src/engine/share/session.ts +61 -61
  414. package/src/engine/share/share-next.ts +376 -376
  415. package/src/engine/share/share.sql.ts +13 -13
  416. package/src/engine/shell/shell/shell.ts +215 -215
  417. package/src/engine/shell/shell.ts +215 -215
  418. package/src/engine/skill/discovery.ts +116 -116
  419. package/src/engine/skill/index.ts +336 -336
  420. package/src/engine/skill/prompt/customize-opencode.md +377 -377
  421. package/src/engine/skill/skill/discovery.ts +116 -116
  422. package/src/engine/skill/skill/index.ts +336 -336
  423. package/src/engine/skill/skill/prompt/customize-opencode.md +377 -377
  424. package/src/engine/snapshot/index.ts +762 -762
  425. package/src/engine/snapshot/snapshot/index.ts +762 -762
  426. package/src/engine/sync/README.md +179 -179
  427. package/src/engine/sync/event.sql.ts +17 -17
  428. package/src/engine/sync/index.ts +410 -410
  429. package/src/engine/sync/schema.ts +11 -11
  430. package/src/engine/temporary.ts +33 -33
  431. package/src/engine/tool/apply_patch.ts +313 -313
  432. package/src/engine/tool/apply_patch.txt +33 -33
  433. package/src/engine/tool/edit.ts +711 -711
  434. package/src/engine/tool/edit.txt +10 -10
  435. package/src/engine/tool/external-directory.ts +49 -49
  436. package/src/engine/tool/glob.ts +103 -103
  437. package/src/engine/tool/glob.txt +6 -6
  438. package/src/engine/tool/grep.ts +156 -156
  439. package/src/engine/tool/grep.txt +8 -8
  440. package/src/engine/tool/invalid.ts +21 -21
  441. package/src/engine/tool/json-schema.ts +164 -164
  442. package/src/engine/tool/lsp.ts +113 -113
  443. package/src/engine/tool/lsp.txt +24 -24
  444. package/src/engine/tool/mcp-websearch.ts +96 -96
  445. package/src/engine/tool/plan-enter.txt +14 -14
  446. package/src/engine/tool/plan-exit.txt +13 -13
  447. package/src/engine/tool/plan.ts +78 -78
  448. package/src/engine/tool/question.ts +44 -44
  449. package/src/engine/tool/question.txt +10 -10
  450. package/src/engine/tool/read.ts +337 -337
  451. package/src/engine/tool/read.txt +14 -14
  452. package/src/engine/tool/registry.ts +472 -472
  453. package/src/engine/tool/repo_clone.ts +80 -80
  454. package/src/engine/tool/repo_clone.txt +5 -5
  455. package/src/engine/tool/repo_overview.ts +279 -279
  456. package/src/engine/tool/repo_overview.txt +4 -4
  457. package/src/engine/tool/schema.ts +14 -14
  458. package/src/engine/tool/shell/id.ts +19 -19
  459. package/src/engine/tool/shell/prompt.ts +295 -295
  460. package/src/engine/tool/shell/shell.txt +77 -77
  461. package/src/engine/tool/shell.ts +647 -647
  462. package/src/engine/tool/skill.ts +75 -75
  463. package/src/engine/tool/skill.txt +5 -5
  464. package/src/engine/tool/task.ts +337 -337
  465. package/src/engine/tool/task.txt +58 -58
  466. package/src/engine/tool/task_status.ts +179 -179
  467. package/src/engine/tool/task_status.txt +13 -13
  468. package/src/engine/tool/todo.ts +57 -57
  469. package/src/engine/tool/todowrite.txt +167 -167
  470. package/src/engine/tool/tool/apply_patch.ts +313 -313
  471. package/src/engine/tool/tool/apply_patch.txt +33 -33
  472. package/src/engine/tool/tool/edit.ts +711 -711
  473. package/src/engine/tool/tool/edit.txt +10 -10
  474. package/src/engine/tool/tool/external-directory.ts +49 -49
  475. package/src/engine/tool/tool/glob.ts +103 -103
  476. package/src/engine/tool/tool/glob.txt +6 -6
  477. package/src/engine/tool/tool/grep.ts +156 -156
  478. package/src/engine/tool/tool/grep.txt +8 -8
  479. package/src/engine/tool/tool/invalid.ts +21 -21
  480. package/src/engine/tool/tool/json-schema.ts +164 -164
  481. package/src/engine/tool/tool/lsp.ts +113 -113
  482. package/src/engine/tool/tool/lsp.txt +24 -24
  483. package/src/engine/tool/tool/mcp-websearch.ts +96 -96
  484. package/src/engine/tool/tool/plan-enter.txt +14 -14
  485. package/src/engine/tool/tool/plan-exit.txt +13 -13
  486. package/src/engine/tool/tool/plan.ts +78 -78
  487. package/src/engine/tool/tool/question.ts +44 -44
  488. package/src/engine/tool/tool/question.txt +10 -10
  489. package/src/engine/tool/tool/read.ts +337 -337
  490. package/src/engine/tool/tool/read.txt +14 -14
  491. package/src/engine/tool/tool/registry.ts +472 -472
  492. package/src/engine/tool/tool/repo_clone.ts +80 -80
  493. package/src/engine/tool/tool/repo_clone.txt +5 -5
  494. package/src/engine/tool/tool/repo_overview.ts +279 -279
  495. package/src/engine/tool/tool/repo_overview.txt +4 -4
  496. package/src/engine/tool/tool/schema.ts +14 -14
  497. package/src/engine/tool/tool/shell/id.ts +19 -19
  498. package/src/engine/tool/tool/shell/prompt.ts +295 -295
  499. package/src/engine/tool/tool/shell/shell.txt +77 -77
  500. package/src/engine/tool/tool/shell.ts +647 -647
  501. package/src/engine/tool/tool/skill.ts +75 -75
  502. package/src/engine/tool/tool/skill.txt +5 -5
  503. package/src/engine/tool/tool/task.ts +337 -337
  504. package/src/engine/tool/tool/task.txt +58 -58
  505. package/src/engine/tool/tool/task_status.ts +179 -179
  506. package/src/engine/tool/tool/task_status.txt +13 -13
  507. package/src/engine/tool/tool/todo.ts +57 -57
  508. package/src/engine/tool/tool/todowrite.txt +167 -167
  509. package/src/engine/tool/tool/tool.ts +164 -164
  510. package/src/engine/tool/tool/truncate.ts +160 -160
  511. package/src/engine/tool/tool/truncation-dir.ts +4 -4
  512. package/src/engine/tool/tool/webfetch.ts +192 -192
  513. package/src/engine/tool/tool/webfetch.txt +13 -13
  514. package/src/engine/tool/tool/websearch.ts +143 -143
  515. package/src/engine/tool/tool/websearch.txt +14 -14
  516. package/src/engine/tool/tool/write.ts +104 -104
  517. package/src/engine/tool/tool/write.txt +8 -8
  518. package/src/engine/tool/tool.ts +164 -164
  519. package/src/engine/tool/truncate.ts +160 -160
  520. package/src/engine/tool/truncation-dir.ts +4 -4
  521. package/src/engine/tool/webfetch.ts +192 -192
  522. package/src/engine/tool/webfetch.txt +13 -13
  523. package/src/engine/tool/websearch.ts +143 -143
  524. package/src/engine/tool/websearch.txt +14 -14
  525. package/src/engine/tool/write.ts +104 -104
  526. package/src/engine/tool/write.txt +8 -8
  527. package/src/engine/util/archive.ts +17 -17
  528. package/src/engine/util/bom.ts +31 -31
  529. package/src/engine/util/data-url.ts +9 -9
  530. package/src/engine/util/defer.ts +10 -10
  531. package/src/engine/util/effect-http-client.ts +11 -11
  532. package/src/engine/util/error.ts +88 -88
  533. package/src/engine/util/filesystem.ts +252 -252
  534. package/src/engine/util/format.ts +20 -20
  535. package/src/engine/util/iife.ts +3 -3
  536. package/src/engine/util/lazy.ts +20 -20
  537. package/src/engine/util/local-context.ts +25 -25
  538. package/src/engine/util/locale.ts +86 -86
  539. package/src/engine/util/media.ts +26 -26
  540. package/src/engine/util/process.ts +176 -176
  541. package/src/engine/util/queue.ts +32 -32
  542. package/src/engine/util/record.ts +3 -3
  543. package/src/engine/util/repository.ts +158 -158
  544. package/src/engine/util/rpc.ts +66 -66
  545. package/src/engine/util/signal.ts +12 -12
  546. package/src/engine/util/timeout.ts +13 -13
  547. package/src/engine/util/token.ts +7 -7
  548. package/src/engine/util/util/archive.ts +17 -17
  549. package/src/engine/util/util/bom.ts +31 -31
  550. package/src/engine/util/util/data-url.ts +9 -9
  551. package/src/engine/util/util/defer.ts +10 -10
  552. package/src/engine/util/util/effect-http-client.ts +11 -11
  553. package/src/engine/util/util/error.ts +88 -88
  554. package/src/engine/util/util/filesystem.ts +252 -252
  555. package/src/engine/util/util/format.ts +20 -20
  556. package/src/engine/util/util/iife.ts +3 -3
  557. package/src/engine/util/util/lazy.ts +20 -20
  558. package/src/engine/util/util/local-context.ts +25 -25
  559. package/src/engine/util/util/locale.ts +86 -86
  560. package/src/engine/util/util/media.ts +26 -26
  561. package/src/engine/util/util/process.ts +176 -176
  562. package/src/engine/util/util/queue.ts +32 -32
  563. package/src/engine/util/util/record.ts +3 -3
  564. package/src/engine/util/util/repository.ts +158 -158
  565. package/src/engine/util/util/rpc.ts +66 -66
  566. package/src/engine/util/util/signal.ts +12 -12
  567. package/src/engine/util/util/timeout.ts +13 -13
  568. package/src/engine/util/util/token.ts +7 -7
  569. package/src/engine/util/util/which.ts +14 -14
  570. package/src/engine/util/util/wildcard.ts +59 -59
  571. package/src/engine/util/which.ts +14 -14
  572. package/src/engine/util/wildcard.ts +59 -59
  573. package/src/engine/worktree/index.ts +621 -621
  574. package/src/server.ts +121 -158
  575. package/src/tui.ts +485 -502
package/src/tui.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  /**
2
- * BoneCode Interactive Terminal UI
2
+ * BoneCode TUI styled after OpenCode's terminal interface
3
3
  *
4
- * Split-footer terminal session:
5
- * - Scrollback output fills the top of the terminal
6
- * - A persistent prompt sits at the bottom
7
- * - Streaming LLM output appears above the prompt in real time
8
- * - Ctrl+C interrupts the current request (does not exit)
9
- * - /new starts a fresh session, /exit or Ctrl+D quits
10
- * - Up/Down arrows navigate prompt history (readline built-in)
11
- * - @<path> autocomplete for files in the current worktree
12
- *
13
- * Built on Node's built-in readline — no external TUI framework needed.
4
+ * Layout:
5
+ * ┌─────────────────────────────────────────────────┐
6
+ * │ [message history scrolls up] │
7
+ * │ ┃ User message (left border, panel bg) │
8
+ * │ ← tool $ shell → read ✱ glob │
9
+ * │ response text │
10
+ * │ ▣ Build · model · 2.1s │
11
+ * ├─────────────────────────────────────────────────┤
12
+ * │ ┃ [input textarea] │
13
+ * │ ┃ Build · model │
14
+ * │ ╹▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ │
15
+ * │ ~/project/dir esc interrupt │
16
+ * └─────────────────────────────────────────────────┘
14
17
  */
15
18
 
16
19
  import * as path from "path";
@@ -18,27 +21,47 @@ import * as fs from "fs";
18
21
  import * as readline from "readline";
19
22
  import * as http from "http";
20
23
 
21
- // ─── ANSI helpers ─────────────────────────────────────────────────────────────
24
+ // ─── ANSI ─────────────────────────────────────────────────────────────────────
22
25
 
23
26
  const ESC = "\x1b";
24
- const RESET = `${ESC}[0m`;
27
+ const R = `${ESC}[0m`; // reset
25
28
  const BOLD = `${ESC}[1m`;
26
- const DIM = `${ESC}[2m`;
27
- const CYAN = `${ESC}[36m`;
28
- const GREEN = `${ESC}[32m`;
29
- const YELLOW = `${ESC}[33m`;
30
- const RED = `${ESC}[31m`;
31
- const CLEAR_LINE = `${ESC}[2K\r`;
32
- const CURSOR_UP = `${ESC}[1A`;
33
-
34
- function stripAnsi(str: string): string {
35
- return str.replace(/\x1b\[[0-9;]*[mGKHF]/g, "");
36
- }
37
-
38
- function truncate(str: string, width: number): string {
39
- const clean = stripAnsi(str);
40
- if (clean.length <= width) return str;
41
- return str.slice(0, width - 1) + "…";
29
+ const DIM = `${ESC}[2m`;
30
+ const CYAN = `${ESC}[96m`; // bright cyan — primary accent
31
+ const GREEN= `${ESC}[92m`; // bright green — success
32
+ const YELLOW=`${ESC}[93m`; // warning
33
+ const RED = `${ESC}[91m`; // error
34
+ const GRAY = `${ESC}[90m`; // textMuted
35
+ const WHITE= `${ESC}[97m`; // text
36
+ const BG_PANEL = `${ESC}[48;5;235m`; // backgroundPanel
37
+ const BG_ELEM = `${ESC}[48;5;237m`; // backgroundElement (input area)
38
+
39
+ // Box-drawing — OpenCode uses exactly these
40
+ const VERT = "┃"; // U+2503 heavy vertical — left border on messages/prompt
41
+ const CORN = "╹"; // U+2579 heavy up — bottom-left corner of prompt box
42
+ const SHADE = "▀"; // upper half block — decorative shadow line under prompt
43
+ const BLOCK = "▣"; // U+25A3 — end-of-turn marker
44
+ const ARROW_R = "→"; // read tool
45
+ const ARROW_L = "←"; // write/edit tool
46
+ const DOLLAR = "$"; // shell tool
47
+ const STAR = "✱"; // glob/grep tool
48
+ const PCT = "%"; // webfetch tool
49
+ const GEAR = "⚙"; // generic tool
50
+ const THINK = "▶"; // collapsed thinking
51
+
52
+ function cols() { return process.stdout.columns || 80; }
53
+ function out(s: string) { process.stdout.write(s); }
54
+ function nl(s = "") { process.stdout.write(s + "\n"); }
55
+
56
+ // ─── Logo ─────────────────────────────────────────────────────────────────────
57
+ // "BONECODE" in block characters
58
+
59
+ function printLogo() {
60
+ nl();
61
+ nl(`${GRAY}${BOLD}█▀▀▄ █▀▀█ █▀▀█ █▀▀▀ █▀▀▀ █▀▀█ █▀▀▄ █▀▀▀${R}`);
62
+ nl(`${GRAY}${BOLD}█▀▀▄ █ █ █ █ █▀▀ █ █ █ █ █ █▀▀ ${R}`);
63
+ nl(`${GRAY}${BOLD}▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀▀${R}`);
64
+ nl();
42
65
  }
43
66
 
44
67
  // ─── Config ───────────────────────────────────────────────────────────────────
@@ -46,39 +69,81 @@ function truncate(str: string, width: number): string {
46
69
  const PKG_ROOT = path.resolve(__dirname, "..");
47
70
 
48
71
  function getVersion(): string {
49
- try {
50
- return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8")).version;
51
- } catch {
52
- return "0.0.0";
72
+ try { return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8")).version; }
73
+ catch { return "0.0.0"; }
74
+ }
75
+
76
+ // ─── @file autocomplete ───────────────────────────────────────────────────────
77
+
78
+ const IGNORED_DIRS = new Set([
79
+ "node_modules", ".git", "dist", "build", ".next", "__pycache__",
80
+ ".venv", "venv", "target", "vendor", ".cache", "coverage",
81
+ ]);
82
+ const CODE_EXTS = new Set([
83
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs",
84
+ ".java", ".kt", ".cs", ".cpp", ".c", ".h", ".rb", ".php", ".swift",
85
+ ".md", ".mdx", ".json", ".yaml", ".yml", ".toml", ".env", ".sql", ".sh", ".graphql",
86
+ ]);
87
+
88
+ function listFiles(worktree: string, prefix: string): string[] {
89
+ const results: string[] = [];
90
+ function walk(dir: string, depth: number) {
91
+ if (results.length >= 50 || depth > 4) return;
92
+ let entries: fs.Dirent[];
93
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
94
+ for (const e of entries) {
95
+ if (results.length >= 50) break;
96
+ if (IGNORED_DIRS.has(e.name) || e.name.startsWith(".")) continue;
97
+ const rel = path.relative(worktree, path.join(dir, e.name));
98
+ if (e.isDirectory()) {
99
+ if (!prefix || rel.startsWith(prefix) || prefix.startsWith(rel)) {
100
+ results.push(rel + "/");
101
+ walk(path.join(dir, e.name), depth + 1);
102
+ }
103
+ } else if (CODE_EXTS.has(path.extname(e.name).toLowerCase())) {
104
+ if (!prefix || rel.startsWith(prefix)) results.push(rel);
105
+ }
106
+ }
53
107
  }
108
+ walk(worktree, 0);
109
+ return results.sort();
54
110
  }
55
111
 
56
- // ─── TUI State ────────────────────────────────────────────────────────────────
57
-
58
- interface TUIState {
59
- sessionId: string | null;
60
- model: string;
61
- provider: string;
62
- port: number;
63
- token: string;
64
- worktree: string;
65
- promptHistory: string[];
66
- streaming: boolean;
67
- abortController: AbortController | null;
68
- turnCount: number;
69
- totalTokens: number;
112
+ // ─── Completers ───────────────────────────────────────────────────────────────
113
+
114
+ const COMMANDS = [
115
+ "/new", "/session", "/model", "/provider", "/providers",
116
+ "/clear", "/history", "/help", "/exit", "/quit",
117
+ ];
118
+
119
+ function buildCompleter(worktree: string) {
120
+ return (line: string): [string[], string] => {
121
+ // /command autocomplete
122
+ if (line.startsWith("/")) {
123
+ const matches = COMMANDS.filter(c => c.startsWith(line));
124
+ return [matches, line];
125
+ }
126
+
127
+ // @file autocomplete
128
+ const atIdx = line.lastIndexOf("@");
129
+ if (atIdx !== -1) {
130
+ const prefix = line.slice(atIdx + 1);
131
+ const completions = listFiles(worktree, prefix).map(f => line.slice(0, atIdx + 1) + f);
132
+ return [completions, line];
133
+ }
134
+
135
+ return [[], line];
136
+ };
70
137
  }
71
138
 
72
- // ─── Server health check ──────────────────────────────────────────────────────
139
+ // ─── Server health ────────────────────────────────────────────────────────────
73
140
 
74
- async function waitForServer(port: number, maxWaitMs = 30_000): Promise<boolean> {
141
+ async function waitForServer(port: number, maxMs = 30_000): Promise<boolean> {
75
142
  const start = Date.now();
76
- while (Date.now() - start < maxWaitMs) {
143
+ while (Date.now() - start < maxMs) {
77
144
  try {
78
145
  const ok = await new Promise<boolean>((resolve) => {
79
- const req = http.get(`http://localhost:${port}/health`, (res) => {
80
- resolve(res.statusCode === 200);
81
- });
146
+ const req = http.get(`http://localhost:${port}/health`, (res) => resolve(res.statusCode === 200));
82
147
  req.on("error", () => resolve(false));
83
148
  req.setTimeout(1000, () => { req.destroy(); resolve(false); });
84
149
  });
@@ -89,7 +154,7 @@ async function waitForServer(port: number, maxWaitMs = 30_000): Promise<boolean>
89
154
  return false;
90
155
  }
91
156
 
92
- // ─── API helpers ──────────────────────────────────────────────────────────────
157
+ // ─── API ──────────────────────────────────────────────────────────────────────
93
158
 
94
159
  async function apiPost(url: string, body: object, token: string): Promise<Response> {
95
160
  return fetch(url, {
@@ -99,538 +164,456 @@ async function apiPost(url: string, body: object, token: string): Promise<Respon
99
164
  });
100
165
  }
101
166
 
102
- async function apiGetJson<T = unknown>(url: string, token: string): Promise<T> {
167
+ async function apiGet<T>(url: string, token: string): Promise<T> {
103
168
  const r = await fetch(url, { headers: { "Authorization": `Bearer ${token}` } });
104
- if (!r.ok) throw new Error(`API error ${r.status}: ${await r.text()}`);
169
+ if (!r.ok) throw new Error(`API ${r.status}`);
105
170
  return r.json() as Promise<T>;
106
171
  }
107
172
 
108
- // ─── Session management ───────────────────────────────────────────────────────
109
-
110
- async function createSession(state: TUIState, title: string): Promise<string> {
111
- const r = await apiPost(
112
- `http://localhost:${state.port}/v2/session`,
113
- { title, directory: state.worktree },
114
- state.token
115
- );
173
+ async function createSession(port: number, token: string, worktree: string, title: string): Promise<string> {
174
+ const r = await apiPost(`http://localhost:${port}/v2/session`, { title, directory: worktree }, token);
116
175
  if (!r.ok) throw new Error(`Failed to create session: ${await r.text()}`);
117
176
  const sess = await r.json() as { id?: string };
118
- if (!sess.id) throw new Error("Server returned session without ID");
177
+ if (!sess.id) throw new Error("No session ID returned");
119
178
  return sess.id;
120
179
  }
121
180
 
122
- // ─── @file autocomplete ───────────────────────────────────────────────────────
181
+ // ─── Streaming ────────────────────────────────────────────────────────────────
123
182
 
124
- const IGNORED_DIRS = new Set([
125
- "node_modules", ".git", "dist", "build", ".next", "__pycache__",
126
- ".venv", "venv", "target", "vendor", ".cache", "coverage",
127
- ]);
128
-
129
- const CODE_EXTS = new Set([
130
- ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
131
- ".py", ".go", ".rs", ".java", ".kt", ".cs", ".cpp", ".c", ".h",
132
- ".rb", ".php", ".swift", ".scala",
133
- ".md", ".mdx", ".json", ".yaml", ".yml", ".toml", ".env",
134
- ".sql", ".sh", ".bash", ".graphql",
135
- ]);
136
-
137
- function listFilesForAutocomplete(worktree: string, prefix: string): string[] {
138
- const results: string[] = [];
139
- const MAX = 50;
140
-
141
- function walk(dir: string, depth: number) {
142
- if (results.length >= MAX || depth > 4) return;
143
- let entries: fs.Dirent[];
144
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
145
- catch { return; }
146
-
147
- for (const e of entries) {
148
- if (results.length >= MAX) break;
149
- if (IGNORED_DIRS.has(e.name) || e.name.startsWith(".")) continue;
150
- const rel = path.relative(worktree, path.join(dir, e.name));
151
- if (e.isDirectory()) {
152
- if (!prefix || rel.startsWith(prefix) || prefix.startsWith(rel)) {
153
- results.push(rel + "/");
154
- walk(path.join(dir, e.name), depth + 1);
155
- }
156
- } else if (CODE_EXTS.has(path.extname(e.name).toLowerCase())) {
157
- if (!prefix || rel.startsWith(prefix)) {
158
- results.push(rel);
159
- }
160
- }
161
- }
162
- }
163
-
164
- walk(worktree, 0);
165
- return results.sort();
183
+ interface StreamResult {
184
+ text: string;
185
+ tokens: number;
186
+ elapsedMs: number;
187
+ error?: string;
188
+ interrupted: boolean;
166
189
  }
167
190
 
168
- function buildCompleter(worktree: string) {
169
- return function completer(line: string): [string[], string] {
170
- // Only trigger on @ mentions
171
- const atIdx = line.lastIndexOf("@");
172
- if (atIdx === -1) return [[], line];
173
-
174
- const prefix = line.slice(atIdx + 1);
175
- const files = listFilesForAutocomplete(worktree, prefix);
176
- const completions = files.map(f => line.slice(0, atIdx + 1) + f);
177
- return [completions, line];
178
- };
179
- }
180
-
181
- // ─── Streaming output ─────────────────────────────────────────────────────────
182
-
183
- async function streamPrompt(
184
- state: TUIState,
185
- message: string,
186
- onDelta: (text: string) => void,
187
- onDone: (info: { tokens?: number; error?: string }) => void
188
- ): Promise<void> {
189
- if (!state.sessionId) throw new Error("No active session");
190
-
191
- const ctrl = new AbortController();
192
- state.abortController = ctrl;
191
+ async function streamPrompt(opts: {
192
+ port: number; token: string; sessionId: string;
193
+ model: string; provider: string; message: string;
194
+ abortSignal: AbortSignal; onDelta: (t: string) => void;
195
+ }): Promise<StreamResult> {
196
+ const { port, token, sessionId, model, provider, message, abortSignal, onDelta } = opts;
197
+ const t0 = Date.now();
198
+ let fullText = "";
199
+ let tokens = 0;
193
200
 
194
201
  try {
195
- const r = await fetch(`http://localhost:${state.port}/v2/session/${state.sessionId}/prompt`, {
202
+ const r = await fetch(`http://localhost:${port}/v2/session/${sessionId}/prompt`, {
196
203
  method: "POST",
197
- headers: {
198
- "Content-Type": "application/json",
199
- "Authorization": `Bearer ${state.token}`,
200
- },
201
- body: JSON.stringify({ content: message, modelID: state.model, providerID: state.provider }),
202
- signal: ctrl.signal,
204
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
205
+ body: JSON.stringify({ content: message, modelID: model, providerID: provider }),
206
+ signal: abortSignal,
203
207
  });
204
-
205
- if (!r.ok) {
206
- onDone({ error: `HTTP ${r.status}: ${await r.text()}` });
207
- return;
208
- }
208
+ if (!r.ok) return { text: "", tokens: 0, elapsedMs: Date.now() - t0, error: `HTTP ${r.status}: ${await r.text()}`, interrupted: false };
209
209
 
210
210
  const reader = r.body!.getReader();
211
- const decoder = new TextDecoder();
212
- let buffer = "";
213
-
211
+ const dec = new TextDecoder();
212
+ let buf = "";
214
213
  while (true) {
215
214
  const { value, done } = await reader.read();
216
215
  if (done) break;
217
- buffer += decoder.decode(value, { stream: true });
218
-
219
- const lines = buffer.split("\n");
220
- buffer = lines.pop() || "";
221
-
222
- for (const line of lines) {
223
- if (!line.startsWith("data: ")) continue;
216
+ buf += dec.decode(value, { stream: true });
217
+ const lines = buf.split("\n");
218
+ buf = lines.pop() || "";
219
+ for (const raw of lines) {
220
+ if (!raw.startsWith("data: ")) continue;
221
+ const json = raw.slice(6).trim();
222
+ if (!json || json === "[DONE]") continue;
224
223
  try {
225
- const event = JSON.parse(line.slice(6));
226
- if (event.type === "part.delta" && event.delta?.type === "text") {
227
- onDelta(event.delta.text);
224
+ const ev = JSON.parse(json);
225
+ if (ev.type === "part.delta" && ev.delta?.type === "text") {
226
+ fullText += ev.delta.text;
227
+ onDelta(ev.delta.text);
228
228
  }
229
- if (event.type === "error") {
230
- onDone({ error: event.properties?.message || "Unknown error" });
231
- return;
232
- }
233
- } catch { /* non-JSON SSE line */ }
229
+ if (ev.type === "error") return { text: fullText, tokens, elapsedMs: Date.now() - t0, error: ev.properties?.message || "error", interrupted: false };
230
+ } catch {}
234
231
  }
235
232
  }
236
233
 
237
- // Poll for final stored response to get token counts
238
- await new Promise(r => setTimeout(r, 800));
239
234
  try {
240
- const msgs = await apiGetJson<any[]>(
241
- `http://localhost:${state.port}/v2/session/${state.sessionId}/message`,
242
- state.token
243
- );
244
- const lastAssistant = msgs.filter(m => m.role === "assistant").slice(-1)[0];
245
- const tokens = (lastAssistant?.tokens?.input || 0) + (lastAssistant?.tokens?.output || 0);
246
- onDone({ tokens });
247
- } catch {
248
- onDone({});
249
- }
235
+ const msgs = await apiGet<any[]>(`http://localhost:${port}/v2/session/${sessionId}/message`, token);
236
+ const last = msgs.filter((m: any) => m.role === "assistant").slice(-1)[0];
237
+ tokens = (last?.tokens?.input || 0) + (last?.tokens?.output || 0);
238
+ } catch {}
239
+
240
+ return { text: fullText, tokens, elapsedMs: Date.now() - t0, interrupted: false };
250
241
  } catch (e: any) {
251
- if (e.name === "AbortError") {
252
- onDone({ error: "interrupted" });
253
- } else {
254
- onDone({ error: e.message });
255
- }
256
- } finally {
257
- state.abortController = null;
242
+ if (e.name === "AbortError") return { text: fullText, tokens, elapsedMs: Date.now() - t0, interrupted: true };
243
+ return { text: fullText, tokens, elapsedMs: Date.now() - t0, error: e.message, interrupted: false };
258
244
  }
259
245
  }
260
246
 
261
- // ─── Terminal rendering ───────────────────────────────────────────────────────
262
-
263
- class TUIRenderer {
264
- private promptLine = "";
265
- private statusLine = "";
266
- private streamBuffer = "";
267
- private cols: number;
268
-
269
- constructor() {
270
- this.cols = process.stdout.columns || 80;
271
- process.stdout.on("resize", () => {
272
- this.cols = process.stdout.columns || 80;
273
- });
274
- }
275
-
276
- print(text: string, newline = true): void {
277
- this.clearPromptArea();
278
- process.stdout.write(text + (newline ? "\n" : ""));
279
- this.redrawPromptArea();
280
- }
281
-
282
- delta(text: string): void {
283
- this.clearPromptArea();
284
- process.stdout.write(text);
285
- this.streamBuffer += text;
286
- this.redrawPromptArea();
287
- }
247
+ // ─── Rendering helpers ────────────────────────────────────────────────────────
288
248
 
289
- endStream(): void {
290
- if (this.streamBuffer && !this.streamBuffer.endsWith("\n")) {
291
- process.stdout.write("\n");
292
- }
293
- this.streamBuffer = "";
294
- this.redrawPromptArea();
249
+ // User message: left border, panel background
250
+ function renderUserMessage(text: string) {
251
+ nl();
252
+ const lines = text.split("\n");
253
+ for (const l of lines) {
254
+ nl(`${GRAY}${VERT}${R} ${WHITE}${l}${R}`);
295
255
  }
256
+ nl();
257
+ }
296
258
 
297
- setStatus(text: string): void {
298
- this.statusLine = text;
299
- this.redrawPromptArea();
300
- }
259
+ // Tool call line — inline style matching OpenCode
260
+ function renderToolCall(toolName: string, summary: string, done: boolean) {
261
+ const icon = toolIcon(toolName);
262
+ const color = done ? GRAY : WHITE;
263
+ nl(` ${color}${icon} ${summary}${R}`);
264
+ }
301
265
 
302
- setPrompt(text: string): void {
303
- this.promptLine = text;
304
- this.redrawPromptArea();
305
- }
266
+ function toolIcon(name: string): string {
267
+ const n = name.toLowerCase();
268
+ if (n === "write" || n === "edit" || n === "apply_patch") return ARROW_L;
269
+ if (n === "read") return ARROW_R;
270
+ if (n === "shell" || n === "bash") return DOLLAR;
271
+ if (n === "glob" || n === "grep") return STAR;
272
+ if (n === "webfetch") return PCT;
273
+ return GEAR;
274
+ }
306
275
 
307
- private clearPromptArea(): void {
308
- process.stdout.write(CLEAR_LINE);
309
- process.stdout.write(CURSOR_UP + CLEAR_LINE);
310
- }
276
+ // End-of-turn marker: ▣ Build · model · 2.1s
277
+ function renderTurnEnd(model: string, elapsedMs: number, interrupted: boolean) {
278
+ const elapsed = (elapsedMs / 1000).toFixed(1);
279
+ const dur = interrupted ? `${YELLOW}interrupted${R}` : `${GRAY}${elapsed}s${R}`;
280
+ nl();
281
+ nl(` ${CYAN}${BLOCK}${R} ${WHITE}Build${R}${GRAY} · ${model} · ${R}${dur}`);
282
+ }
311
283
 
312
- private redrawPromptArea(): void {
313
- const statusText = truncate(this.statusLine, this.cols - 2);
314
- const promptText = truncate(this.promptLine, this.cols - 2);
315
- process.stdout.write(`${DIM}${statusText}${RESET}\n`);
316
- process.stdout.write(promptText);
317
- }
284
+ // Prompt box — matches OpenCode's input area design
285
+ function renderPromptBox(model: string, provider: string, sessionId: string, worktree: string) {
286
+ const width = cols();
287
+ const dir = worktree.replace(/\\/g, "/").replace(/^.*\/([^/]+\/[^/]+)$/, "$1"); // last 2 path segments
288
+ const modelLabel = `${WHITE}Build${R}${GRAY} · ${model}${R}`;
289
+ const shadow = SHADE.repeat(Math.min(width - 2, 60));
290
+
291
+ nl();
292
+ nl(`${GRAY}${VERT}${R}`);
293
+ nl(`${GRAY}${VERT}${R} ${GRAY}${DIM}Type a message...${R}`);
294
+ nl(`${GRAY}${VERT}${R}`);
295
+ nl(`${GRAY}${VERT}${R} ${modelLabel}`);
296
+ nl(`${GRAY}${CORN}${shadow}${R}`);
297
+ nl(`${GRAY}${dir}${R} ${GRAY}esc ${DIM}interrupt${R}`);
298
+ }
318
299
 
319
- initPromptArea(): void {
320
- process.stdout.write("\n\n");
321
- this.clearPromptArea();
322
- this.redrawPromptArea();
323
- }
300
+ // Help text
301
+ function printHelp() {
302
+ nl();
303
+ nl(`${CYAN}${BOLD}BoneCode${R} ${GRAY}commands${R}`);
304
+ nl();
305
+ nl(` ${CYAN}/new${R} New session`);
306
+ nl(` ${CYAN}/session${R} Show session ID`);
307
+ nl(` ${CYAN}/model <id>${R} Switch model`);
308
+ nl(` ${CYAN}/provider <id>${R} Switch provider`);
309
+ nl(` ${CYAN}/providers${R} List all providers`);
310
+ nl(` ${CYAN}/clear${R} Clear screen`);
311
+ nl(` ${CYAN}/history${R} Last 10 prompts`);
312
+ nl(` ${CYAN}/exit${R} Exit`);
313
+ nl();
314
+ nl(` ${CYAN}Ctrl+C${R} Interrupt current request`);
315
+ nl(` ${CYAN}Ctrl+D${R} Exit`);
316
+ nl(` ${CYAN}↑ / ↓${R} Prompt history`);
317
+ nl(` ${CYAN}@<path>${R} Attach file — Tab to autocomplete`);
318
+ nl();
319
+ nl(` ${GRAY}Model shorthand: /model groq/llama-3.3-70b-versatile${R}`);
320
+ nl(` ${GRAY}Local config: /model local${R}`);
321
+ nl();
322
+ }
324
323
 
325
- separator(char = "─"): void {
326
- this.print(`${DIM}${char.repeat(Math.min(this.cols, 60))}${RESET}`);
327
- }
324
+ async function fetchProviders(port: number, token: string): Promise<any[]> {
325
+ try { return await apiGet<any[]>(`http://localhost:${port}/v2/provider`, token); }
326
+ catch { return []; }
327
+ }
328
328
 
329
- banner(model: string, worktree: string): void {
330
- const VERSION = getVersion();
331
- const dir = path.basename(worktree) || worktree;
332
- this.print(`\n${CYAN}${BOLD}BoneCode${RESET} ${DIM}v${VERSION}${RESET}`);
333
- this.print(`${DIM} model: ${RESET}${model}`);
334
- this.print(`${DIM} dir: ${RESET}${dir}`);
335
- this.print(`${DIM} type ${RESET}${CYAN}/help${RESET}${DIM} for commands · ${RESET}${CYAN}Ctrl+C${RESET}${DIM} to interrupt · ${RESET}${CYAN}Ctrl+D${RESET}${DIM} to exit${RESET}`);
336
- this.separator();
329
+ function printProviders(providers: any[], currentProvider: string) {
330
+ const width = cols();
331
+ nl();
332
+ nl(`${CYAN}${BOLD}Providers${R}`);
333
+ nl(`${GRAY}${"─".repeat(Math.min(width, 56))}${R}`);
334
+ for (const p of providers) {
335
+ const active = p.id === currentProvider;
336
+ const dot = active ? `${GREEN}●${R}` : `${GRAY}○${R}`;
337
+ const free = p.free ? ` ${GREEN}free${R}` : "";
338
+ const local = p.id === "local" ? ` ${CYAN}local config${R}` : "";
339
+ nl(` ${dot} ${active ? CYAN + BOLD : WHITE}${p.id}${R}${free}${local} ${GRAY}${p.name}${R}`);
340
+ if (p.freeNote) nl(` ${GRAY}${p.freeNote}${R}`);
341
+ if (p.models?.length) nl(` ${GRAY}${p.models.slice(0, 3).join(" ")}${R}`);
342
+ if (p.keyEnv) nl(` ${GRAY}${p.keyEnv}${R}`);
337
343
  }
344
+ nl(`${GRAY}${"─".repeat(Math.min(width, 56))}${R}`);
345
+ nl(` ${GRAY}/provider <id> /model <id>${R}`);
346
+ nl();
338
347
  }
339
348
 
340
- // ─── Input handling ───────────────────────────────────────────────────────────
341
-
342
- class TUIInput {
343
- private rl: readline.Interface;
344
- private sigintHandler: (() => void) | null = null;
345
-
346
- constructor(
347
- private renderer: TUIRenderer,
348
- private state: TUIState,
349
- private onSubmit: (text: string) => Promise<void>
350
- ) {
351
- this.rl = readline.createInterface({
352
- input: process.stdin,
353
- output: process.stdout,
354
- terminal: true,
355
- historySize: 100,
356
- completer: buildCompleter(state.worktree),
357
- });
358
-
359
- // Seed readline history from state
360
- for (const h of state.promptHistory.slice().reverse()) {
361
- (this.rl as any).history?.unshift(h);
362
- }
349
+ // ─── Main TUI ─────────────────────────────────────────────────────────────────
363
350
 
364
- this.rl.on("line", async (line) => {
365
- const text = line.trim();
366
- if (text) {
367
- state.promptHistory.push(text);
368
- if (state.promptHistory.length > 100) state.promptHistory.shift();
369
- }
370
- await this.onSubmit(text);
371
- this.prompt();
372
- });
373
-
374
- this.rl.on("close", () => {
375
- this.cleanup();
376
- renderer.print(`\n${DIM}Goodbye.${RESET}`);
377
- process.exit(0);
378
- });
351
+ export async function runTUI(opts: {
352
+ port: number; token: string; model: string;
353
+ provider: string; worktree: string; sessionId?: string;
354
+ }): Promise<void> {
355
+ let { model, provider } = opts;
356
+ const { port, token, worktree } = opts;
357
+ let sessionId = opts.sessionId || null;
358
+ let turnCount = 0;
359
+ const history: string[] = [];
360
+ let abort: AbortController | null = null;
361
+ let streaming = false;
379
362
 
380
- // Register SIGINT handler — stored so we can remove it on cleanup
381
- this.sigintHandler = () => {
382
- if (state.abortController) {
383
- state.abortController.abort();
384
- renderer.print(`\n${YELLOW}⟳ Interrupted${RESET}`);
385
- this.prompt();
386
- } else {
387
- renderer.print(`\n${DIM}(Use Ctrl+D or /exit to quit)${RESET}`);
388
- this.prompt();
389
- }
390
- };
391
- process.on("SIGINT", this.sigintHandler);
363
+ // Create initial session
364
+ if (!sessionId) {
365
+ try { sessionId = await createSession(port, token, worktree, "BoneCode Session"); }
366
+ catch (e: any) { process.stderr.write(`${RED}Failed to create session: ${e.message}${R}\n`); process.exit(1); }
392
367
  }
393
368
 
394
- prompt(): void {
395
- const sessionLabel = this.state.sessionId
396
- ? `${DIM}[${this.state.sessionId.slice(0, 8)}]${RESET} `
397
- : "";
398
- const promptStr = `${CYAN}${BOLD}>${RESET} `;
399
- const full = `${sessionLabel}${promptStr}`;
400
- this.renderer.setPrompt(full);
401
- this.rl.setPrompt(full);
402
- this.rl.prompt(true);
403
- }
369
+ // Header
370
+ printLogo();
371
+ const VERSION = getVersion();
372
+ nl(` ${GRAY}v${VERSION} · ${model} · ${path.basename(worktree)}${R}`);
373
+ nl(` ${GRAY}session ${sessionId!.slice(0, 8)}${R}`);
374
+ nl();
375
+ nl(` ${GRAY}/help for commands · Ctrl+C interrupt · Ctrl+D exit${R}`);
376
+ nl();
377
+ nl(); // extra blank line — ghost hint uses the line above the prompt
378
+
379
+ const rl = readline.createInterface({
380
+ input: process.stdin, output: process.stdout,
381
+ terminal: true, historySize: 200,
382
+ completer: buildCompleter(worktree),
383
+ });
404
384
 
405
- cleanup(): void {
406
- if (this.sigintHandler) {
407
- process.removeListener("SIGINT", this.sigintHandler);
408
- this.sigintHandler = null;
409
- }
410
- }
385
+ const promptStr = () => `${CYAN}${BOLD}>${R} `;
411
386
 
412
- close(): void {
413
- this.cleanup();
414
- this.rl.close();
415
- }
416
- }
387
+ const sigint = () => {
388
+ if (streaming && abort) {
389
+ abort.abort();
390
+ nl(`\n${YELLOW}interrupted${R}`);
391
+ } else {
392
+ nl(`\n${GRAY}(Ctrl+D or /exit to quit)${R}`);
393
+ }
394
+ rl.setPrompt(promptStr());
395
+ rl.prompt();
396
+ };
397
+ process.on("SIGINT", sigint);
417
398
 
418
- // ─── Command handlers ─────────────────────────────────────────────────────────
419
-
420
- async function handleCommand(cmd: string, state: TUIState, renderer: TUIRenderer): Promise<void> {
421
- const parts = cmd.slice(1).trim().split(/\s+/);
422
- const name = parts[0]?.toLowerCase() || "";
423
- const args = parts.slice(1);
424
-
425
- switch (name) {
426
- case "help":
427
- case "h":
428
- renderer.print(`
429
- ${CYAN}${BOLD}BoneCode Commands${RESET}
430
- ${CYAN}/new${RESET} Start a new session
431
- ${CYAN}/session${RESET} Show current session ID
432
- ${CYAN}/model <id>${RESET} Switch model (e.g. /model gpt-4o or /model anthropic/claude-sonnet-4-5)
433
- ${CYAN}/clear${RESET} Clear the screen
434
- ${CYAN}/history${RESET} Show last 10 prompts
435
- ${CYAN}/exit${RESET}, ${CYAN}/quit${RESET} Exit BoneCode
436
- ${CYAN}Ctrl+C${RESET} Interrupt current request (stays in session)
437
- ${CYAN}Ctrl+D${RESET} Exit BoneCode
438
- ${CYAN}↑ / ↓${RESET} Navigate prompt history
439
- ${CYAN}@<path>${RESET} Attach a file — press Tab to autocomplete`);
440
- break;
441
-
442
- case "new":
443
- renderer.print(`${DIM}Starting new session...${RESET}`);
444
- try {
445
- state.sessionId = await createSession(state, "New Session");
446
- state.turnCount = 0;
447
- renderer.print(`${GREEN}✓ New session: ${state.sessionId.slice(0, 8)}...${RESET}`);
448
- renderer.separator();
449
- } catch (e: any) {
450
- renderer.print(`${RED}✗ Failed to create session: ${e.message}${RESET}`);
451
- }
452
- break;
399
+ rl.on("close", () => {
400
+ process.removeListener("SIGINT", sigint);
401
+ nl(`\n${GRAY}Goodbye.${R}`);
402
+ process.exit(0);
403
+ });
453
404
 
454
- case "session":
455
- if (state.sessionId) {
456
- renderer.print(`${DIM}Session: ${RESET}${state.sessionId}`);
457
- } else {
458
- renderer.print(`${YELLOW}No active session${RESET}`);
459
- }
460
- break;
461
-
462
- case "model":
463
- if (args[0]) {
464
- if (args[0].includes("/")) {
465
- const slashIdx = args[0].indexOf("/");
466
- state.provider = args[0].slice(0, slashIdx);
467
- state.model = args[0].slice(slashIdx + 1);
468
- } else {
469
- state.model = args[0];
405
+ rl.setPrompt(promptStr());
406
+ rl.prompt();
407
+
408
+ // Ghost-text hint: when user types /x show matching commands above prompt
409
+ let lastHint = "";
410
+ (rl as any).input?.on("keypress", (_: any, key: any) => {
411
+ if (!key) return;
412
+ // Small delay so readline has updated its line buffer
413
+ setImmediate(() => {
414
+ const current: string = (rl as any).line || "";
415
+ if (!current.startsWith("/") || current.includes(" ")) {
416
+ if (lastHint) {
417
+ // Clear the hint line
418
+ process.stdout.write(`${ESC}[s`); // save cursor
419
+ process.stdout.write(`${ESC}[1A${ESC}[2K`); // up 1, clear line
420
+ process.stdout.write(`${ESC}[u`); // restore cursor
421
+ lastHint = "";
470
422
  }
471
- renderer.print(`${GREEN}✓ Model: ${state.provider}/${state.model}${RESET}`);
472
- renderer.setStatus(`${DIM}${state.provider}/${state.model}${RESET}`);
473
- } else {
474
- renderer.print(`${DIM}Current model: ${RESET}${state.provider}/${state.model}`);
423
+ return;
475
424
  }
476
- break;
477
-
478
- case "clear":
479
- process.stdout.write(`${ESC}[2J${ESC}[H`);
480
- renderer.banner(`${state.provider}/${state.model}`, state.worktree);
481
- renderer.initPromptArea();
482
- break;
483
-
484
- case "history":
485
- if (state.promptHistory.length === 0) {
486
- renderer.print(`${DIM}No history yet${RESET}`);
487
- } else {
488
- renderer.print(`${DIM}Recent prompts:${RESET}`);
489
- state.promptHistory.slice(-10).forEach((h, i) => {
490
- renderer.print(` ${DIM}${i + 1}.${RESET} ${truncate(h, 70)}`);
491
- });
425
+ const matches = COMMANDS.filter(c => c.startsWith(current) && c !== current);
426
+ const hint = matches.length ? matches[0] : "";
427
+ if (hint === lastHint) return;
428
+ lastHint = hint;
429
+ // Print hint above the prompt line
430
+ process.stdout.write(`${ESC}[s`); // save cursor
431
+ process.stdout.write(`${ESC}[1A${ESC}[2K`); // up 1, clear line
432
+ if (hint) {
433
+ // Show current typed part normal, rest in grey
434
+ const ghost = hint.slice(current.length);
435
+ process.stdout.write(`${promptStr()}${current}${GRAY}${ghost}${R}`);
492
436
  }
493
- break;
437
+ process.stdout.write(`${ESC}[u`); // restore cursor
438
+ });
439
+ });
494
440
 
495
- case "exit":
496
- case "quit":
497
- case "q":
498
- renderer.print(`\n${DIM}Goodbye.${RESET}`);
499
- process.exit(0);
500
- break;
441
+ for await (const rawLine of rl) {
442
+ const text = rawLine.trim();
501
443
 
502
- default:
503
- renderer.print(`${YELLOW}Unknown command: /${name}${RESET} — type ${CYAN}/help${RESET} for commands`);
504
- }
505
- }
444
+ if (!text) { rl.setPrompt(promptStr()); rl.prompt(); continue; }
506
445
 
507
- // ─── Main TUI loop ────────────────────────────────────────────────────────────
446
+ history.push(text);
447
+ if (history.length > 200) history.shift();
508
448
 
509
- export async function runTUI(opts: {
510
- port: number;
511
- token: string;
512
- model: string;
513
- provider: string;
514
- worktree: string;
515
- sessionId?: string;
516
- }): Promise<void> {
517
- const state: TUIState = {
518
- sessionId: opts.sessionId || null,
519
- model: opts.model,
520
- provider: opts.provider,
521
- port: opts.port,
522
- token: opts.token,
523
- worktree: opts.worktree,
524
- promptHistory: [],
525
- streaming: false,
526
- abortController: null,
527
- turnCount: 0,
528
- totalTokens: 0,
529
- };
449
+ // ── Commands ──────────────────────────────────────────────────────────────
450
+ if (text.startsWith("/")) {
451
+ const parts = text.slice(1).trim().split(/\s+/);
452
+ const cmd = parts[0]?.toLowerCase() || "";
453
+ const args = parts.slice(1);
454
+
455
+ switch (cmd) {
456
+ case "help": case "h":
457
+ printHelp();
458
+ break;
459
+
460
+ case "new":
461
+ try {
462
+ sessionId = await createSession(port, token, worktree, "New Session");
463
+ turnCount = 0;
464
+ nl(`${GREEN}✓${R} ${GRAY}session ${sessionId.slice(0, 8)}${R}`);
465
+ } catch (e: any) { nl(`${RED}✗ ${e.message}${R}`); }
466
+ break;
467
+
468
+ case "session":
469
+ nl(`${GRAY}${sessionId}${R}`);
470
+ break;
471
+
472
+ case "model":
473
+ if (args[0]) {
474
+ if (args[0] === "local") {
475
+ provider = process.env.DEFAULT_PROVIDER || "openai_compatible";
476
+ model = process.env.DEFAULT_MODEL || "local-model";
477
+ nl(`${GREEN}✓${R} ${WHITE}${provider}/${model}${R}`);
478
+ } else if (args[0].includes("/")) {
479
+ const i = args[0].indexOf("/");
480
+ provider = args[0].slice(0, i);
481
+ model = args[0].slice(i + 1);
482
+ nl(`${GREEN}✓${R} ${WHITE}${provider}/${model}${R}`);
483
+ } else {
484
+ model = args[0];
485
+ nl(`${GREEN}✓${R} ${WHITE}${provider}/${model}${R}`);
486
+ }
487
+ } else {
488
+ nl(`${GRAY}${provider}/${model}${R}`);
489
+ }
490
+ break;
491
+
492
+ case "provider":
493
+ if (args[0]) {
494
+ if (args[0] === "local") {
495
+ provider = process.env.DEFAULT_PROVIDER || "openai_compatible";
496
+ model = process.env.DEFAULT_MODEL || "local-model";
497
+ nl(`${GREEN}✓${R} ${WHITE}${provider}/${model}${R}`);
498
+ } else {
499
+ provider = args[0];
500
+ nl(`${GREEN}✓${R} ${WHITE}${provider}${R} ${GRAY}use /model <id> to set model${R}`);
501
+ }
502
+ } else {
503
+ nl(`${GRAY}${provider}${R}`);
504
+ }
505
+ break;
530
506
 
531
- const renderer = new TUIRenderer();
507
+ case "providers": {
508
+ const list = await fetchProviders(port, token);
509
+ if (list.length) printProviders(list, provider);
510
+ else nl(`${YELLOW}Could not fetch providers${R}`);
511
+ break;
512
+ }
532
513
 
533
- // Create initial session
534
- if (!state.sessionId) {
535
- try {
536
- state.sessionId = await createSession(state, "BoneCode Session");
537
- } catch (e: any) {
538
- process.stderr.write(`${RED}Failed to create session: ${e.message}${RESET}\n`);
539
- process.exit(1);
540
- }
541
- }
514
+ case "clear":
515
+ process.stdout.write(`${ESC}[2J${ESC}[H`);
516
+ printLogo();
517
+ break;
542
518
 
543
- renderer.banner(`${state.provider}/${state.model}`, state.worktree);
544
- renderer.setStatus(`${DIM}${state.provider}/${state.model} · session ${state.sessionId!.slice(0, 8)}${RESET}`);
545
- renderer.initPromptArea();
519
+ case "history":
520
+ if (!history.length) { nl(`${GRAY}No history${R}`); }
521
+ else history.slice(-10).forEach((h, i) => nl(` ${GRAY}${i + 1}.${R} ${h.slice(0, 80)}`));
522
+ break;
546
523
 
547
- const tuiInput = new TUIInput(renderer, state, async (text: string) => {
548
- if (!text) return;
524
+ case "exit": case "quit": case "q":
525
+ nl(`\n${GRAY}Goodbye.${R}`);
526
+ process.exit(0);
527
+ break;
549
528
 
550
- if (text.startsWith("/")) {
551
- await handleCommand(text, state, renderer);
552
- return;
529
+ default:
530
+ nl(`${YELLOW}Unknown: /${cmd}${R} ${GRAY}/help${R}`);
531
+ }
532
+
533
+ rl.setPrompt(promptStr());
534
+ rl.prompt();
535
+ continue;
553
536
  }
554
537
 
555
- // Ensure session exists
556
- if (!state.sessionId) {
557
- try {
558
- state.sessionId = await createSession(state, text.slice(0, 50));
559
- } catch (e: any) {
560
- renderer.print(`${RED}✗ Failed to create session: ${e.message}${RESET}`);
561
- return;
562
- }
538
+ // ── Prompt ────────────────────────────────────────────────────────────────
539
+
540
+ if (!sessionId) {
541
+ try { sessionId = await createSession(port, token, worktree, text.slice(0, 50)); }
542
+ catch (e: any) { nl(`${RED}✗ ${e.message}${R}`); rl.setPrompt(promptStr()); rl.prompt(); continue; }
563
543
  }
564
544
 
565
- // Resolve @file mentions to absolute paths
566
- const resolvedText = text.replace(/@([\w./\-]+)/g, (match, filePath) => {
567
- const abs = path.isAbsolute(filePath)
568
- ? filePath
569
- : path.resolve(state.worktree, filePath);
545
+ // Resolve @file mentions
546
+ const resolved = text.replace(/@([\w./\-]+)/g, (match, fp) => {
547
+ const abs = path.isAbsolute(fp) ? fp : path.resolve(worktree, fp);
570
548
  return fs.existsSync(abs) ? `@${abs}` : match;
571
549
  });
572
550
 
573
- renderer.print(`\n${CYAN}${BOLD}You${RESET}`);
574
- renderer.print(resolvedText !== text ? `${text} ${DIM}(files resolved)${RESET}` : text);
575
- renderer.separator("·");
576
- renderer.print(`${GREEN}${BOLD}BoneCode${RESET} ${DIM}(${state.model})${RESET}`);
577
-
578
- state.streaming = true;
579
- const startMs = Date.now();
580
- renderer.setStatus(`${YELLOW}⟳ thinking...${RESET}`);
581
-
582
- await streamPrompt(
583
- state,
584
- resolvedText,
585
- (delta) => {
586
- // Strip markdown fences — they don't render well in terminals
551
+ // Render user message with ┃ border
552
+ renderUserMessage(resolved !== text ? `${text} ${GRAY}(files resolved)${R}` : text);
553
+
554
+ // Thinking indicator
555
+ out(` ${GRAY}⋯${R}`);
556
+
557
+ rl.pause();
558
+ streaming = true;
559
+ abort = new AbortController();
560
+ let firstDelta = true;
561
+
562
+ const result = await streamPrompt({
563
+ port, token, sessionId: sessionId!, model, provider,
564
+ message: resolved, abortSignal: abort.signal,
565
+ onDelta: (delta) => {
566
+ if (firstDelta) {
567
+ // Clear the thinking indicator, print indent once
568
+ out(`\r${ESC}[2K`);
569
+ nl();
570
+ out(` `);
571
+ firstDelta = false;
572
+ }
573
+ // Strip markdown fences for terminal rendering
587
574
  const clean = delta.replace(/^```\w*\n?/gm, "").replace(/^```\n?/gm, "");
588
- if (clean) renderer.delta(clean);
575
+ if (clean) out(clean);
589
576
  },
590
- (info) => {
591
- renderer.endStream();
592
- state.streaming = false;
593
- state.turnCount++;
577
+ });
594
578
 
595
- const elapsed = ((Date.now() - startMs) / 1000).toFixed(1);
596
- if (info.tokens) state.totalTokens += info.tokens;
579
+ streaming = false;
580
+ abort = null;
597
581
 
598
- if (info.error && info.error !== "interrupted") {
599
- renderer.print(`${RED}✗ ${info.error}${RESET}`);
600
- }
582
+ if (firstDelta) {
583
+ // No deltas arrived — clear thinking indicator
584
+ out(`\r${ESC}[2K`);
585
+ }
601
586
 
602
- renderer.separator();
587
+ if (result.text && !result.text.endsWith("\n")) nl();
603
588
 
604
- const tokenStr = info.tokens ? ` · ${info.tokens} tok` : "";
605
- const statusStr = info.error === "interrupted"
606
- ? `${YELLOW}interrupted${RESET}`
607
- : `${DIM}${elapsed}s${tokenStr}${RESET}`;
608
- renderer.setStatus(
609
- `${DIM}${state.provider}/${state.model} · turn ${state.turnCount}${RESET} · ${statusStr}`
610
- );
611
- }
612
- );
613
- });
589
+ turnCount++;
590
+
591
+ // End-of-turn marker: ▣ Build · model · 2.1s
592
+ renderTurnEnd(model, result.elapsedMs, result.interrupted);
614
593
 
615
- tuiInput.prompt();
594
+ if (result.error && !result.interrupted) {
595
+ nl(` ${RED}✗ ${result.error}${R}`);
596
+ }
616
597
 
617
- // Keep process alive until readline closes
618
- await new Promise<void>(() => {});
598
+ nl();
599
+
600
+ rl.resume();
601
+ rl.setPrompt(promptStr());
602
+ nl(); // blank line for ghost hint
603
+ rl.prompt();
604
+ }
619
605
  }
620
606
 
621
607
  // ─── Entry point ──────────────────────────────────────────────────────────────
622
608
 
623
609
  export async function startInteractiveTUI(opts: {
624
- port: number;
625
- token: string;
626
- model: string;
627
- provider: string;
628
- worktree: string;
610
+ port: number; token: string; model: string;
611
+ provider: string; worktree: string;
629
612
  }): Promise<void> {
630
- const serverReady = await waitForServer(opts.port, 30_000);
631
- if (!serverReady) {
632
- process.stderr.write(`${RED}Error: BoneCode server not responding on port ${opts.port}${RESET}\n`);
633
- process.stderr.write(`${DIM}Start it with: bonecode serve${RESET}\n`);
613
+ const ready = await waitForServer(opts.port, 30_000);
614
+ if (!ready) {
615
+ process.stderr.write(`${RED}BoneCode server not responding on port ${opts.port}${R}\n`);
616
+ process.stderr.write(`${GRAY}Start with: bonecode serve${R}\n`);
634
617
  process.exit(1);
635
618
  }
636
619
  await runTUI(opts);