jonsoc 1.1.49 → 1.1.50

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 (421) hide show
  1. package/bin/jonsoc +264 -256
  2. package/package.json +8 -142
  3. package/postinstall.mjs +15 -0
  4. package/AGENTS.md +0 -27
  5. package/Dockerfile +0 -18
  6. package/PUBLISHING_GUIDE.md +0 -151
  7. package/README.md +0 -58
  8. package/bunfig.toml +0 -7
  9. package/package.json.placeholder +0 -11
  10. package/parsers-config.ts +0 -253
  11. package/script/build.ts +0 -115
  12. package/script/publish-registries.ts +0 -197
  13. package/script/publish.ts +0 -110
  14. package/script/schema.ts +0 -47
  15. package/script/seed-e2e.ts +0 -50
  16. package/src/acp/README.md +0 -164
  17. package/src/acp/agent.ts +0 -1437
  18. package/src/acp/session.ts +0 -105
  19. package/src/acp/types.ts +0 -22
  20. package/src/agent/agent.ts +0 -347
  21. package/src/agent/generate.txt +0 -75
  22. package/src/agent/prompt/compaction.txt +0 -12
  23. package/src/agent/prompt/explore.txt +0 -18
  24. package/src/agent/prompt/summary.txt +0 -11
  25. package/src/agent/prompt/title.txt +0 -44
  26. package/src/auth/index.ts +0 -73
  27. package/src/brand/index.ts +0 -89
  28. package/src/bun/index.ts +0 -139
  29. package/src/bus/bus-event.ts +0 -43
  30. package/src/bus/global.ts +0 -10
  31. package/src/bus/index.ts +0 -105
  32. package/src/cli/bootstrap.ts +0 -17
  33. package/src/cli/cmd/acp.ts +0 -69
  34. package/src/cli/cmd/agent.ts +0 -257
  35. package/src/cli/cmd/auth.ts +0 -405
  36. package/src/cli/cmd/cmd.ts +0 -7
  37. package/src/cli/cmd/debug/agent.ts +0 -166
  38. package/src/cli/cmd/debug/config.ts +0 -16
  39. package/src/cli/cmd/debug/file.ts +0 -97
  40. package/src/cli/cmd/debug/index.ts +0 -48
  41. package/src/cli/cmd/debug/lsp.ts +0 -52
  42. package/src/cli/cmd/debug/ripgrep.ts +0 -87
  43. package/src/cli/cmd/debug/scrap.ts +0 -16
  44. package/src/cli/cmd/debug/skill.ts +0 -16
  45. package/src/cli/cmd/debug/snapshot.ts +0 -52
  46. package/src/cli/cmd/export.ts +0 -88
  47. package/src/cli/cmd/generate.ts +0 -38
  48. package/src/cli/cmd/github.ts +0 -1548
  49. package/src/cli/cmd/import.ts +0 -99
  50. package/src/cli/cmd/mcp.ts +0 -765
  51. package/src/cli/cmd/models.ts +0 -77
  52. package/src/cli/cmd/pr.ts +0 -112
  53. package/src/cli/cmd/run.ts +0 -395
  54. package/src/cli/cmd/serve.ts +0 -20
  55. package/src/cli/cmd/session.ts +0 -135
  56. package/src/cli/cmd/stats.ts +0 -402
  57. package/src/cli/cmd/tui/app.tsx +0 -923
  58. package/src/cli/cmd/tui/attach.ts +0 -39
  59. package/src/cli/cmd/tui/component/border.tsx +0 -21
  60. package/src/cli/cmd/tui/component/dialog-agent.tsx +0 -31
  61. package/src/cli/cmd/tui/component/dialog-command.tsx +0 -162
  62. package/src/cli/cmd/tui/component/dialog-error-log.tsx +0 -155
  63. package/src/cli/cmd/tui/component/dialog-mcp.tsx +0 -86
  64. package/src/cli/cmd/tui/component/dialog-model.tsx +0 -234
  65. package/src/cli/cmd/tui/component/dialog-provider.tsx +0 -256
  66. package/src/cli/cmd/tui/component/dialog-session-list.tsx +0 -114
  67. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +0 -31
  68. package/src/cli/cmd/tui/component/dialog-stash.tsx +0 -87
  69. package/src/cli/cmd/tui/component/dialog-status.tsx +0 -164
  70. package/src/cli/cmd/tui/component/dialog-tag.tsx +0 -44
  71. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +0 -50
  72. package/src/cli/cmd/tui/component/dynamic-layout.tsx +0 -86
  73. package/src/cli/cmd/tui/component/inspector-overlay.tsx +0 -247
  74. package/src/cli/cmd/tui/component/logo.tsx +0 -88
  75. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +0 -653
  76. package/src/cli/cmd/tui/component/prompt/frecency.tsx +0 -89
  77. package/src/cli/cmd/tui/component/prompt/history.tsx +0 -108
  78. package/src/cli/cmd/tui/component/prompt/index.tsx +0 -1347
  79. package/src/cli/cmd/tui/component/prompt/stash.tsx +0 -101
  80. package/src/cli/cmd/tui/component/textarea-keybindings.ts +0 -73
  81. package/src/cli/cmd/tui/component/tips.tsx +0 -153
  82. package/src/cli/cmd/tui/component/todo-item.tsx +0 -32
  83. package/src/cli/cmd/tui/context/args.tsx +0 -14
  84. package/src/cli/cmd/tui/context/directory.ts +0 -13
  85. package/src/cli/cmd/tui/context/error-log.tsx +0 -56
  86. package/src/cli/cmd/tui/context/exit.tsx +0 -26
  87. package/src/cli/cmd/tui/context/helper.tsx +0 -25
  88. package/src/cli/cmd/tui/context/inspector.tsx +0 -57
  89. package/src/cli/cmd/tui/context/keybind.tsx +0 -108
  90. package/src/cli/cmd/tui/context/kv.tsx +0 -53
  91. package/src/cli/cmd/tui/context/layout.tsx +0 -240
  92. package/src/cli/cmd/tui/context/local.tsx +0 -402
  93. package/src/cli/cmd/tui/context/prompt.tsx +0 -18
  94. package/src/cli/cmd/tui/context/route.tsx +0 -51
  95. package/src/cli/cmd/tui/context/sdk.tsx +0 -94
  96. package/src/cli/cmd/tui/context/sync.tsx +0 -449
  97. package/src/cli/cmd/tui/context/theme/aura.json +0 -69
  98. package/src/cli/cmd/tui/context/theme/ayu.json +0 -80
  99. package/src/cli/cmd/tui/context/theme/carbonfox.json +0 -248
  100. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +0 -233
  101. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +0 -233
  102. package/src/cli/cmd/tui/context/theme/catppuccin.json +0 -112
  103. package/src/cli/cmd/tui/context/theme/cobalt2.json +0 -228
  104. package/src/cli/cmd/tui/context/theme/cursor.json +0 -249
  105. package/src/cli/cmd/tui/context/theme/dracula.json +0 -219
  106. package/src/cli/cmd/tui/context/theme/everforest.json +0 -241
  107. package/src/cli/cmd/tui/context/theme/flexoki.json +0 -237
  108. package/src/cli/cmd/tui/context/theme/github.json +0 -233
  109. package/src/cli/cmd/tui/context/theme/gruvbox.json +0 -242
  110. package/src/cli/cmd/tui/context/theme/jonsoc.json +0 -245
  111. package/src/cli/cmd/tui/context/theme/kanagawa.json +0 -77
  112. package/src/cli/cmd/tui/context/theme/lucent-orng.json +0 -237
  113. package/src/cli/cmd/tui/context/theme/material.json +0 -235
  114. package/src/cli/cmd/tui/context/theme/matrix.json +0 -77
  115. package/src/cli/cmd/tui/context/theme/mercury.json +0 -252
  116. package/src/cli/cmd/tui/context/theme/monokai.json +0 -221
  117. package/src/cli/cmd/tui/context/theme/nightowl.json +0 -221
  118. package/src/cli/cmd/tui/context/theme/nord.json +0 -223
  119. package/src/cli/cmd/tui/context/theme/one-dark.json +0 -84
  120. package/src/cli/cmd/tui/context/theme/orng.json +0 -249
  121. package/src/cli/cmd/tui/context/theme/osaka-jade.json +0 -93
  122. package/src/cli/cmd/tui/context/theme/palenight.json +0 -222
  123. package/src/cli/cmd/tui/context/theme/rosepine.json +0 -234
  124. package/src/cli/cmd/tui/context/theme/solarized.json +0 -223
  125. package/src/cli/cmd/tui/context/theme/synthwave84.json +0 -226
  126. package/src/cli/cmd/tui/context/theme/tokyonight.json +0 -243
  127. package/src/cli/cmd/tui/context/theme/vercel.json +0 -245
  128. package/src/cli/cmd/tui/context/theme/vesper.json +0 -218
  129. package/src/cli/cmd/tui/context/theme/zenburn.json +0 -223
  130. package/src/cli/cmd/tui/context/theme.tsx +0 -1153
  131. package/src/cli/cmd/tui/event.ts +0 -48
  132. package/src/cli/cmd/tui/hooks/use-command-registry.tsx +0 -184
  133. package/src/cli/cmd/tui/routes/home.tsx +0 -198
  134. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +0 -64
  135. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +0 -109
  136. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +0 -26
  137. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +0 -47
  138. package/src/cli/cmd/tui/routes/session/footer.tsx +0 -91
  139. package/src/cli/cmd/tui/routes/session/git-commit.tsx +0 -59
  140. package/src/cli/cmd/tui/routes/session/git-history.tsx +0 -122
  141. package/src/cli/cmd/tui/routes/session/header.tsx +0 -185
  142. package/src/cli/cmd/tui/routes/session/index.tsx +0 -2363
  143. package/src/cli/cmd/tui/routes/session/navigator-ui.tsx +0 -214
  144. package/src/cli/cmd/tui/routes/session/navigator.tsx +0 -1124
  145. package/src/cli/cmd/tui/routes/session/panel-explorer.tsx +0 -553
  146. package/src/cli/cmd/tui/routes/session/panel-viewer.tsx +0 -386
  147. package/src/cli/cmd/tui/routes/session/permission.tsx +0 -501
  148. package/src/cli/cmd/tui/routes/session/question.tsx +0 -507
  149. package/src/cli/cmd/tui/routes/session/sidebar.tsx +0 -365
  150. package/src/cli/cmd/tui/routes/session/vcs-diff-viewer.tsx +0 -37
  151. package/src/cli/cmd/tui/routes/ui-settings.tsx +0 -449
  152. package/src/cli/cmd/tui/thread.ts +0 -172
  153. package/src/cli/cmd/tui/ui/dialog-alert.tsx +0 -90
  154. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +0 -83
  155. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +0 -204
  156. package/src/cli/cmd/tui/ui/dialog-help.tsx +0 -38
  157. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +0 -77
  158. package/src/cli/cmd/tui/ui/dialog-select.tsx +0 -384
  159. package/src/cli/cmd/tui/ui/dialog.tsx +0 -170
  160. package/src/cli/cmd/tui/ui/link.tsx +0 -28
  161. package/src/cli/cmd/tui/ui/spinner.ts +0 -375
  162. package/src/cli/cmd/tui/ui/toast.tsx +0 -100
  163. package/src/cli/cmd/tui/util/clipboard.ts +0 -255
  164. package/src/cli/cmd/tui/util/editor.ts +0 -32
  165. package/src/cli/cmd/tui/util/signal.ts +0 -7
  166. package/src/cli/cmd/tui/util/terminal.ts +0 -114
  167. package/src/cli/cmd/tui/util/transcript.ts +0 -98
  168. package/src/cli/cmd/tui/worker.ts +0 -152
  169. package/src/cli/cmd/uninstall.ts +0 -362
  170. package/src/cli/cmd/upgrade.ts +0 -73
  171. package/src/cli/cmd/web.ts +0 -81
  172. package/src/cli/error.ts +0 -57
  173. package/src/cli/network.ts +0 -53
  174. package/src/cli/ui.ts +0 -119
  175. package/src/cli/upgrade.ts +0 -25
  176. package/src/command/index.ts +0 -131
  177. package/src/command/template/initialize.txt +0 -10
  178. package/src/command/template/review.txt +0 -99
  179. package/src/config/config.ts +0 -1404
  180. package/src/config/markdown.ts +0 -93
  181. package/src/env/index.ts +0 -26
  182. package/src/file/ignore.ts +0 -83
  183. package/src/file/index.ts +0 -432
  184. package/src/file/ripgrep.ts +0 -407
  185. package/src/file/time.ts +0 -69
  186. package/src/file/watcher.ts +0 -127
  187. package/src/flag/flag.ts +0 -80
  188. package/src/format/formatter.ts +0 -357
  189. package/src/format/index.ts +0 -137
  190. package/src/global/index.ts +0 -58
  191. package/src/id/id.ts +0 -83
  192. package/src/ide/index.ts +0 -76
  193. package/src/index.ts +0 -208
  194. package/src/installation/index.ts +0 -258
  195. package/src/lsp/client.ts +0 -252
  196. package/src/lsp/index.ts +0 -485
  197. package/src/lsp/language.ts +0 -119
  198. package/src/lsp/server.ts +0 -2046
  199. package/src/mcp/auth.ts +0 -135
  200. package/src/mcp/index.ts +0 -934
  201. package/src/mcp/oauth-callback.ts +0 -200
  202. package/src/mcp/oauth-provider.ts +0 -155
  203. package/src/patch/index.ts +0 -680
  204. package/src/permission/arity.ts +0 -163
  205. package/src/permission/index.ts +0 -210
  206. package/src/permission/next.ts +0 -280
  207. package/src/plugin/codex.ts +0 -500
  208. package/src/plugin/copilot.ts +0 -283
  209. package/src/plugin/index.ts +0 -135
  210. package/src/project/bootstrap.ts +0 -35
  211. package/src/project/instance.ts +0 -91
  212. package/src/project/project.ts +0 -371
  213. package/src/project/state.ts +0 -66
  214. package/src/project/vcs.ts +0 -151
  215. package/src/provider/auth.ts +0 -147
  216. package/src/provider/models-macro.ts +0 -14
  217. package/src/provider/models.ts +0 -114
  218. package/src/provider/provider.ts +0 -1220
  219. package/src/provider/sdk/openai-compatible/src/README.md +0 -5
  220. package/src/provider/sdk/openai-compatible/src/index.ts +0 -2
  221. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +0 -100
  222. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +0 -303
  223. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +0 -22
  224. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +0 -18
  225. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +0 -22
  226. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +0 -207
  227. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +0 -1732
  228. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +0 -177
  229. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +0 -1
  230. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +0 -88
  231. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +0 -128
  232. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +0 -115
  233. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +0 -65
  234. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +0 -104
  235. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +0 -103
  236. package/src/provider/transform.ts +0 -742
  237. package/src/pty/index.ts +0 -241
  238. package/src/question/index.ts +0 -176
  239. package/src/scheduler/index.ts +0 -61
  240. package/src/server/error.ts +0 -36
  241. package/src/server/event.ts +0 -7
  242. package/src/server/mdns.ts +0 -59
  243. package/src/server/routes/config.ts +0 -92
  244. package/src/server/routes/experimental.ts +0 -208
  245. package/src/server/routes/file.ts +0 -227
  246. package/src/server/routes/global.ts +0 -135
  247. package/src/server/routes/mcp.ts +0 -225
  248. package/src/server/routes/permission.ts +0 -68
  249. package/src/server/routes/project.ts +0 -82
  250. package/src/server/routes/provider.ts +0 -165
  251. package/src/server/routes/pty.ts +0 -169
  252. package/src/server/routes/question.ts +0 -98
  253. package/src/server/routes/session.ts +0 -939
  254. package/src/server/routes/tui.ts +0 -379
  255. package/src/server/server.ts +0 -663
  256. package/src/session/compaction.ts +0 -225
  257. package/src/session/index.ts +0 -498
  258. package/src/session/llm.ts +0 -288
  259. package/src/session/message-v2.ts +0 -740
  260. package/src/session/message.ts +0 -189
  261. package/src/session/processor.ts +0 -406
  262. package/src/session/prompt/anthropic-20250930.txt +0 -168
  263. package/src/session/prompt/anthropic.txt +0 -172
  264. package/src/session/prompt/anthropic_spoof.txt +0 -1
  265. package/src/session/prompt/beast.txt +0 -149
  266. package/src/session/prompt/build-switch.txt +0 -5
  267. package/src/session/prompt/codex_header.txt +0 -81
  268. package/src/session/prompt/copilot-gpt-5.txt +0 -145
  269. package/src/session/prompt/gemini.txt +0 -157
  270. package/src/session/prompt/max-steps.txt +0 -16
  271. package/src/session/prompt/plan-reminder-anthropic.txt +0 -67
  272. package/src/session/prompt/plan.txt +0 -26
  273. package/src/session/prompt/qwen.txt +0 -111
  274. package/src/session/prompt.ts +0 -1815
  275. package/src/session/retry.ts +0 -90
  276. package/src/session/revert.ts +0 -121
  277. package/src/session/status.ts +0 -76
  278. package/src/session/summary.ts +0 -150
  279. package/src/session/system.ts +0 -156
  280. package/src/session/todo.ts +0 -37
  281. package/src/share/share-next.ts +0 -204
  282. package/src/share/share.ts +0 -95
  283. package/src/shell/shell.ts +0 -67
  284. package/src/skill/index.ts +0 -1
  285. package/src/skill/skill.ts +0 -135
  286. package/src/snapshot/index.ts +0 -236
  287. package/src/storage/storage.ts +0 -227
  288. package/src/tool/apply_patch.ts +0 -279
  289. package/src/tool/apply_patch.txt +0 -33
  290. package/src/tool/bash.ts +0 -258
  291. package/src/tool/bash.txt +0 -115
  292. package/src/tool/batch.ts +0 -175
  293. package/src/tool/batch.txt +0 -24
  294. package/src/tool/codesearch.ts +0 -132
  295. package/src/tool/codesearch.txt +0 -12
  296. package/src/tool/edit.ts +0 -645
  297. package/src/tool/edit.txt +0 -10
  298. package/src/tool/external-directory.ts +0 -32
  299. package/src/tool/glob.ts +0 -77
  300. package/src/tool/glob.txt +0 -6
  301. package/src/tool/grep.ts +0 -154
  302. package/src/tool/grep.txt +0 -8
  303. package/src/tool/invalid.ts +0 -17
  304. package/src/tool/ls.ts +0 -121
  305. package/src/tool/ls.txt +0 -1
  306. package/src/tool/lsp.ts +0 -96
  307. package/src/tool/lsp.txt +0 -19
  308. package/src/tool/multiedit.ts +0 -46
  309. package/src/tool/multiedit.txt +0 -41
  310. package/src/tool/plan-enter.txt +0 -14
  311. package/src/tool/plan-exit.txt +0 -13
  312. package/src/tool/plan.ts +0 -130
  313. package/src/tool/question.ts +0 -33
  314. package/src/tool/question.txt +0 -10
  315. package/src/tool/read.ts +0 -202
  316. package/src/tool/read.txt +0 -12
  317. package/src/tool/registry.ts +0 -162
  318. package/src/tool/skill.ts +0 -82
  319. package/src/tool/task.ts +0 -188
  320. package/src/tool/task.txt +0 -60
  321. package/src/tool/todo.ts +0 -53
  322. package/src/tool/todoread.txt +0 -14
  323. package/src/tool/todowrite.txt +0 -167
  324. package/src/tool/tool.ts +0 -88
  325. package/src/tool/truncation.ts +0 -106
  326. package/src/tool/webfetch.ts +0 -182
  327. package/src/tool/webfetch.txt +0 -13
  328. package/src/tool/websearch.ts +0 -150
  329. package/src/tool/websearch.txt +0 -14
  330. package/src/tool/write.ts +0 -80
  331. package/src/tool/write.txt +0 -8
  332. package/src/util/archive.ts +0 -16
  333. package/src/util/color.ts +0 -19
  334. package/src/util/context.ts +0 -25
  335. package/src/util/defer.ts +0 -12
  336. package/src/util/eventloop.ts +0 -20
  337. package/src/util/filesystem.ts +0 -93
  338. package/src/util/fn.ts +0 -11
  339. package/src/util/format.ts +0 -20
  340. package/src/util/iife.ts +0 -3
  341. package/src/util/keybind.ts +0 -103
  342. package/src/util/lazy.ts +0 -18
  343. package/src/util/locale.ts +0 -81
  344. package/src/util/lock.ts +0 -98
  345. package/src/util/log.ts +0 -180
  346. package/src/util/queue.ts +0 -32
  347. package/src/util/rpc.ts +0 -66
  348. package/src/util/scrap.ts +0 -10
  349. package/src/util/signal.ts +0 -12
  350. package/src/util/timeout.ts +0 -14
  351. package/src/util/token.ts +0 -7
  352. package/src/util/wildcard.ts +0 -56
  353. package/src/worktree/index.ts +0 -524
  354. package/sst-env.d.ts +0 -9
  355. package/test/acp/agent-interface.test.ts +0 -51
  356. package/test/acp/event-subscription.test.ts +0 -436
  357. package/test/agent/agent.test.ts +0 -638
  358. package/test/bun.test.ts +0 -53
  359. package/test/cli/cmd/tui/fileref.test.ts +0 -30
  360. package/test/cli/github-action.test.ts +0 -129
  361. package/test/cli/github-remote.test.ts +0 -80
  362. package/test/cli/tui/navigator_logic.test.ts +0 -99
  363. package/test/cli/tui/transcript.test.ts +0 -297
  364. package/test/cli/ui.test.ts +0 -80
  365. package/test/config/agent-color.test.ts +0 -66
  366. package/test/config/config.test.ts +0 -1613
  367. package/test/config/fixtures/empty-frontmatter.md +0 -4
  368. package/test/config/fixtures/frontmatter.md +0 -28
  369. package/test/config/fixtures/no-frontmatter.md +0 -1
  370. package/test/config/markdown.test.ts +0 -192
  371. package/test/file/ignore.test.ts +0 -10
  372. package/test/file/path-traversal.test.ts +0 -198
  373. package/test/fixture/fixture.ts +0 -45
  374. package/test/fixture/lsp/fake-lsp-server.js +0 -77
  375. package/test/ide/ide.test.ts +0 -82
  376. package/test/keybind.test.ts +0 -421
  377. package/test/lsp/client.test.ts +0 -95
  378. package/test/mcp/headers.test.ts +0 -153
  379. package/test/mcp/oauth-browser.test.ts +0 -261
  380. package/test/patch/patch.test.ts +0 -348
  381. package/test/permission/arity.test.ts +0 -33
  382. package/test/permission/next.test.ts +0 -690
  383. package/test/permission-task.test.ts +0 -319
  384. package/test/plugin/codex.test.ts +0 -123
  385. package/test/preload.ts +0 -67
  386. package/test/project/project.test.ts +0 -120
  387. package/test/provider/amazon-bedrock.test.ts +0 -268
  388. package/test/provider/gitlab-duo.test.ts +0 -286
  389. package/test/provider/provider.test.ts +0 -2149
  390. package/test/provider/transform.test.ts +0 -1631
  391. package/test/question/question.test.ts +0 -300
  392. package/test/scheduler.test.ts +0 -73
  393. package/test/server/session-list.test.ts +0 -39
  394. package/test/server/session-select.test.ts +0 -78
  395. package/test/session/compaction.test.ts +0 -293
  396. package/test/session/llm.test.ts +0 -90
  397. package/test/session/message-v2.test.ts +0 -786
  398. package/test/session/retry.test.ts +0 -131
  399. package/test/session/revert-compact.test.ts +0 -285
  400. package/test/session/session.test.ts +0 -71
  401. package/test/skill/skill.test.ts +0 -185
  402. package/test/snapshot/snapshot.test.ts +0 -939
  403. package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
  404. package/test/tool/apply_patch.test.ts +0 -499
  405. package/test/tool/bash.test.ts +0 -320
  406. package/test/tool/external-directory.test.ts +0 -126
  407. package/test/tool/fixtures/large-image.png +0 -0
  408. package/test/tool/fixtures/models-api.json +0 -33453
  409. package/test/tool/grep.test.ts +0 -109
  410. package/test/tool/question.test.ts +0 -105
  411. package/test/tool/read.test.ts +0 -332
  412. package/test/tool/registry.test.ts +0 -76
  413. package/test/tool/truncation.test.ts +0 -159
  414. package/test/util/filesystem.test.ts +0 -39
  415. package/test/util/format.test.ts +0 -59
  416. package/test/util/iife.test.ts +0 -36
  417. package/test/util/lazy.test.ts +0 -50
  418. package/test/util/lock.test.ts +0 -72
  419. package/test/util/timeout.test.ts +0 -21
  420. package/test/util/wildcard.test.ts +0 -75
  421. package/tsconfig.json +0 -16
@@ -1,1347 +0,0 @@
1
- import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg } from "@opentui/core"
2
- import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, Show, Switch, Match, on } from "solid-js"
3
- import "opentui-spinner/solid"
4
- import { useLocal } from "@tui/context/local"
5
- import { useTheme } from "@tui/context/theme"
6
- import { EmptyBorder } from "@tui/component/border"
7
- import { useSDK } from "@tui/context/sdk"
8
- import { useRoute } from "@tui/context/route"
9
- import { useSync } from "@tui/context/sync"
10
- import { Identifier } from "@/id/id"
11
- import { createStore, produce } from "solid-js/store"
12
- import { useKeybind } from "@tui/context/keybind"
13
- import { usePromptHistory, type PromptInfo } from "./history"
14
- import { usePromptStash } from "./stash"
15
- import { DialogStash } from "../dialog-stash"
16
- import { type AutocompleteRef, Autocomplete } from "./autocomplete"
17
- import { useCommandDialog } from "../dialog-command"
18
- import { useRenderer } from "@opentui/solid"
19
- import { Editor } from "@tui/util/editor"
20
- import { useExit } from "../../context/exit"
21
- import { Clipboard } from "../../util/clipboard"
22
- import type { FilePart } from "@jonsoc/sdk/v2"
23
- import { TuiEvent } from "../../event"
24
- import { iife } from "@/util/iife"
25
- import { Locale } from "@/util/locale"
26
- import { formatDuration } from "@/util/format"
27
- import { createColors, createFrames } from "../../ui/spinner.ts"
28
- import { useDialog } from "@tui/ui/dialog"
29
- import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
30
- import { DialogAlert } from "../../ui/dialog-alert"
31
- import { useToast } from "../../ui/toast"
32
- import { useKV } from "../../context/kv"
33
- import { useTextareaKeybindings } from "../textarea-keybindings"
34
- import path from "path"
35
- import { mkdir } from "fs/promises"
36
- import { tmpdir } from "os"
37
-
38
- export type PromptProps = {
39
- sessionID?: string
40
- visible?: boolean
41
- disabled?: boolean
42
- onSubmit?: () => void
43
- ref?: (ref: PromptRef) => void
44
- hint?: JSX.Element
45
- showPlaceholder?: boolean
46
- }
47
-
48
- export type PromptRef = {
49
- focused: boolean
50
- current: PromptInfo
51
- set(prompt: PromptInfo): void
52
- reset(): void
53
- blur(): void
54
- focus(): void
55
- submit(): void
56
- }
57
-
58
- const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"]
59
-
60
- export function Prompt(props: PromptProps) {
61
- let input: TextareaRenderable
62
- let anchor: BoxRenderable
63
- let autocomplete: AutocompleteRef
64
-
65
- const keybind = useKeybind()
66
- const local = useLocal()
67
- const sdk = useSDK()
68
- const route = useRoute()
69
- const sync = useSync()
70
- const dialog = useDialog()
71
- const toast = useToast()
72
- const status = createMemo(() => sync.data.session_status?.[props.sessionID ?? ""] ?? { type: "idle" })
73
- const history = usePromptHistory()
74
- const stash = usePromptStash()
75
- const command = useCommandDialog()
76
- const renderer = useRenderer()
77
- const { theme, syntax } = useTheme()
78
- const kv = useKV()
79
-
80
- async function pasteImagePath(filepath: string, event: PasteEvent) {
81
- const full = path.isAbsolute(filepath) ? filepath : path.resolve(filepath)
82
- const file = Bun.file(full)
83
- if (file.type === "image/svg+xml") {
84
- const content = await file.text().catch(() => { })
85
- if (!content) return false
86
- event.preventDefault()
87
- pasteText(content, `[SVG: ${file.name ?? "image"}]`)
88
- return true
89
- }
90
- if (file.type.startsWith("image/")) {
91
- const content = await file
92
- .arrayBuffer()
93
- .then((buffer) => Buffer.from(buffer).toString("base64"))
94
- .catch(() => { })
95
- if (!content) return false
96
- event.preventDefault()
97
- await pasteImage({ filename: file.name, mime: file.type, content, path: full })
98
- return true
99
- }
100
- return false
101
- }
102
-
103
- function imageExt(mime: string) {
104
- return iife(() => {
105
- if (mime === "image/png") return "png"
106
- if (mime === "image/jpeg") return "jpg"
107
- if (mime === "image/jpg") return "jpg"
108
- if (mime === "image/webp") return "webp"
109
- if (mime === "image/gif") return "gif"
110
- if (mime === "image/svg+xml") return "svg"
111
- return "png"
112
- })
113
- }
114
-
115
- async function persistImage(file: { content: string; mime: string; filename?: string }) {
116
- const dir = path.join(tmpdir(), "jonsoc", "attachments")
117
- await mkdir(dir, { recursive: true })
118
- const parsed = file.filename ? path.parse(file.filename) : undefined
119
- const baseRaw = parsed?.name ?? "image"
120
- const base = baseRaw.replace(/[<>:"/\\|?*]+/g, "-")
121
- const ext = parsed?.ext ? parsed.ext.slice(1) : imageExt(file.mime)
122
- const name = `${base}-${crypto.randomUUID()}.${ext}`
123
- const filepath = path.join(dir, name)
124
- const buffer = Buffer.from(file.content, "base64")
125
- await Bun.write(filepath, buffer)
126
- return { path: filepath, filename: name }
127
- }
128
-
129
- async function readLatestScreenClipOnce() {
130
- if (process.platform !== "win32") return
131
- const local = process.env["LOCALAPPDATA"]
132
- if (!local) return
133
- const cwd = path.join(local, "Packages")
134
- const glob = new Bun.Glob("{MicrosoftWindows.Client.CBS_*,Microsoft.ScreenSketch_*}/TempState/ScreenClip/*.png")
135
- const entries: Array<{ path: string; time: number }> = []
136
- for await (const item of glob.scan({ cwd, absolute: true })) {
137
- const file = Bun.file(item)
138
- const stat = await file.stat().catch(() => { })
139
- if (!stat?.isFile()) continue
140
- const time = typeof stat.mtimeMs === "number" ? stat.mtimeMs : (stat.mtime?.getTime?.() ?? 0)
141
- entries.push({ path: item, time })
142
- }
143
- if (entries.length === 0) return
144
- const latest = entries.reduce((acc, entry) => (entry.time > acc.time ? entry : acc), {
145
- path: "",
146
- time: -1,
147
- })
148
- if (latest.time < 0) return
149
- const file = Bun.file(latest.path)
150
- if (!file.type.startsWith("image/")) return
151
- const content = await file
152
- .arrayBuffer()
153
- .then((buffer) => Buffer.from(buffer).toString("base64"))
154
- .catch(() => { })
155
- if (!content) return
156
- return { filename: file.name, mime: file.type, content, path: latest.path }
157
- }
158
-
159
- async function readLatestScreenClip() {
160
- const delays = [0, 50, 150]
161
- for (const delay of delays) {
162
- if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay))
163
- const result = await readLatestScreenClipOnce()
164
- if (result) return result
165
- }
166
- }
167
-
168
- async function readClipboardImage(opts?: { allowScreenClip?: boolean }) {
169
- const content = await Clipboard.read()
170
- if (content?.mime.startsWith("image/")) {
171
- const saved = await persistImage({ content: content.data, mime: content.mime, filename: "clipboard" }).catch(
172
- () => undefined,
173
- )
174
- const savedPath = saved?.path
175
- const savedName = saved?.filename ?? "clipboard"
176
- return {
177
- filename: savedName,
178
- mime: content.mime,
179
- content: content.data,
180
- path: savedPath,
181
- source: "clipboard" as const,
182
- }
183
- }
184
- if (content) return
185
- if (!opts?.allowScreenClip) return
186
- const screenClip = await readLatestScreenClip()
187
- if (screenClip) return { ...screenClip, source: "screenclip" as const }
188
- }
189
-
190
- function promptModelWarning() {
191
- toast.show({
192
- variant: "warning",
193
- message: "Connect a provider to send prompts",
194
- duration: 3000,
195
- })
196
- if (sync.data.provider.length === 0) {
197
- dialog.replace(() => <DialogProviderConnect />)
198
- }
199
- }
200
-
201
- const textareaKeybindings = useTextareaKeybindings()
202
-
203
- const fileStyleId = syntax().getStyleId("extmark.file")!
204
- const agentStyleId = syntax().getStyleId("extmark.agent")!
205
- const pasteStyleId = syntax().getStyleId("extmark.paste")!
206
- let promptPartTypeId = 0
207
-
208
- sdk.event.on(TuiEvent.PromptAppend.type, (evt) => {
209
- input.insertText(evt.properties.text)
210
- setTimeout(() => {
211
- input.getLayoutNode().markDirty()
212
- input.gotoBufferEnd()
213
- renderer.requestRender()
214
- }, 0)
215
- })
216
-
217
- createEffect(() => {
218
- if (props.disabled) input.cursorColor = theme.backgroundElement
219
- if (!props.disabled) input.cursorColor = theme.text
220
- })
221
-
222
- const lastUserMessage = createMemo(() => {
223
- if (!props.sessionID) return undefined
224
- const messages = sync.data.message[props.sessionID]
225
- if (!messages) return undefined
226
- return messages.findLast((m) => m.role === "user")
227
- })
228
-
229
- const [store, setStore] = createStore<{
230
- prompt: PromptInfo
231
- mode: "normal" | "shell"
232
- extmarkToPartIndex: Map<number, number>
233
- interrupt: number
234
- placeholder: number
235
- }>({
236
- placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
237
- prompt: {
238
- input: "",
239
- parts: [],
240
- },
241
- mode: "normal",
242
- extmarkToPartIndex: new Map(),
243
- interrupt: 0,
244
- })
245
-
246
- const [escClearAt, setEscClearAt] = createSignal<number | undefined>()
247
- const escClearWindowMs = 1500
248
-
249
- // Initialize agent/model/variant from last user message when session changes
250
- let syncedSessionID: string | undefined
251
- createEffect(() => {
252
- const sessionID = props.sessionID
253
- const msg = lastUserMessage()
254
-
255
- if (sessionID !== syncedSessionID) {
256
- if (!sessionID || !msg) return
257
-
258
- syncedSessionID = sessionID
259
-
260
- // Only set agent if it's a primary agent (not a subagent)
261
- const isPrimaryAgent = local.agent.list().some((x) => x.name === msg.agent)
262
- if (msg.agent && isPrimaryAgent) {
263
- local.agent.set(msg.agent)
264
- if (msg.model) local.model.set(msg.model)
265
- if (msg.variant) local.model.variant.set(msg.variant)
266
- }
267
- }
268
- })
269
-
270
- createEffect(() => {
271
- if (store.prompt.input === "") setEscClearAt(undefined)
272
- })
273
-
274
- command.register(() => {
275
- return [
276
- {
277
- title: "Clear prompt",
278
- value: "prompt.clear",
279
- category: "Prompt",
280
- hidden: true,
281
- onSelect: (dialog) => {
282
- clearPromptInput()
283
- dialog.clear()
284
- },
285
- },
286
- {
287
- title: "Submit prompt",
288
- value: "prompt.submit",
289
- keybind: "input_submit",
290
- category: "Prompt",
291
- hidden: true,
292
- onSelect: (dialog) => {
293
- if (!input.focused) return
294
- submit()
295
- dialog.clear()
296
- },
297
- },
298
- {
299
- title: "Paste",
300
- value: "prompt.paste",
301
- keybind: "input_paste",
302
- category: "Prompt",
303
- hidden: false,
304
- onSelect: async () => {
305
- const allowClipboardImage = sync.data.config.experimental?.paste_clipboard_image !== false
306
- const text = await Clipboard.readText()
307
- if (text) {
308
- insertPlainText(text)
309
- return
310
- }
311
- if (!allowClipboardImage) {
312
- toast.show({
313
- variant: "warning",
314
- message: "No text in clipboard",
315
- })
316
- return
317
- }
318
- const content = await readClipboardImage({ allowScreenClip: true })
319
- if (!content) {
320
- toast.show({
321
- variant: "warning",
322
- message: "No image in clipboard",
323
- })
324
- return
325
- }
326
- await pasteImage({
327
- filename: content.filename,
328
- mime: content.mime,
329
- content: content.content,
330
- path: content.path,
331
- })
332
- },
333
- },
334
- {
335
- title: "Interrupt session",
336
- value: "session.interrupt",
337
- keybind: "session_interrupt",
338
- category: "Session",
339
- hidden: true,
340
- enabled: status().type !== "idle",
341
- onSelect: (dialog) => {
342
- if (autocomplete.visible) return
343
- if (!input.focused) return
344
- // TODO: this should be its own command
345
- if (store.mode === "shell") {
346
- setStore("mode", "normal")
347
- return
348
- }
349
- if (!props.sessionID) return
350
-
351
- setStore("interrupt", store.interrupt + 1)
352
-
353
- setTimeout(() => {
354
- setStore("interrupt", 0)
355
- }, 5000)
356
-
357
- if (store.interrupt >= 2) {
358
- sdk.client.session.abort({
359
- sessionID: props.sessionID,
360
- })
361
- setStore("interrupt", 0)
362
- }
363
- dialog.clear()
364
- },
365
- },
366
- {
367
- title: "Open editor",
368
- category: "Session",
369
- keybind: "editor_open",
370
- value: "prompt.editor",
371
- slash: {
372
- name: "editor",
373
- },
374
- onSelect: async (dialog) => {
375
- dialog.clear()
376
-
377
- // replace summarized text parts with the actual text
378
- const text = store.prompt.parts
379
- .filter((p) => p.type === "text")
380
- .reduce((acc, p) => {
381
- if (!p.source) return acc
382
- return acc.replace(p.source.text.value, p.text)
383
- }, store.prompt.input)
384
-
385
- const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text")
386
-
387
- const value = text
388
- const content = await Editor.open({ value, renderer })
389
- if (!content) return
390
-
391
- input.setText(content)
392
-
393
- // Update positions for nonTextParts based on their location in new content
394
- // Filter out parts whose virtual text was deleted
395
- // this handles a case where the user edits the text in the editor
396
- // such that the virtual text moves around or is deleted
397
- const updatedNonTextParts = nonTextParts
398
- .map((part) => {
399
- let virtualText = ""
400
- if (part.type === "file" && part.source?.text) {
401
- virtualText = part.source.text.value
402
- } else if (part.type === "agent" && part.source) {
403
- virtualText = part.source.value
404
- }
405
-
406
- if (!virtualText) return part
407
-
408
- const newStart = content.indexOf(virtualText)
409
- // if the virtual text is deleted, remove the part
410
- if (newStart === -1) return null
411
-
412
- const newEnd = newStart + virtualText.length
413
-
414
- if (part.type === "file" && part.source?.text) {
415
- return {
416
- ...part,
417
- source: {
418
- ...part.source,
419
- text: {
420
- ...part.source.text,
421
- start: newStart,
422
- end: newEnd,
423
- },
424
- },
425
- }
426
- }
427
-
428
- if (part.type === "agent" && part.source) {
429
- return {
430
- ...part,
431
- source: {
432
- ...part.source,
433
- start: newStart,
434
- end: newEnd,
435
- },
436
- }
437
- }
438
-
439
- return part
440
- })
441
- .filter((part) => part !== null)
442
-
443
- setStore("prompt", {
444
- input: content,
445
- // keep only the non-text parts because the text parts were
446
- // already expanded inline
447
- parts: updatedNonTextParts,
448
- })
449
- restoreExtmarksFromParts(updatedNonTextParts)
450
- input.cursorOffset = Bun.stringWidth(content)
451
- },
452
- },
453
- ]
454
- })
455
-
456
- const ref: PromptRef = {
457
- get focused() {
458
- return input.focused
459
- },
460
- get current() {
461
- return store.prompt
462
- },
463
- focus() {
464
- input.focus()
465
- },
466
- blur() {
467
- input.blur()
468
- },
469
- set(prompt) {
470
- input.setText(prompt.input)
471
- setStore("prompt", prompt)
472
- restoreExtmarksFromParts(prompt.parts)
473
- input.gotoBufferEnd()
474
- },
475
- reset() {
476
- input.clear()
477
- input.extmarks.clear()
478
- setStore("prompt", {
479
- input: "",
480
- parts: [],
481
- })
482
- setStore("extmarkToPartIndex", new Map())
483
- },
484
- submit() {
485
- submit()
486
- },
487
- }
488
-
489
- createEffect(
490
- on(
491
- () => props.visible !== false,
492
- (visible, prev) => {
493
- if (visible && !prev) input?.focus()
494
- if (!visible) input?.blur()
495
- },
496
- ),
497
- )
498
-
499
- function restoreExtmarksFromParts(parts: PromptInfo["parts"]) {
500
- input.extmarks.clear()
501
- const newMap = new Map<number, number>()
502
-
503
- parts.forEach((part, partIndex) => {
504
- let start = 0
505
- let end = 0
506
- let virtualText = ""
507
- let styleId: number | undefined
508
-
509
- if (part.type === "file" && part.source?.text) {
510
- start = part.source.text.start
511
- end = part.source.text.end
512
- virtualText = part.source.text.value
513
- styleId = fileStyleId
514
- } else if (part.type === "agent" && part.source) {
515
- start = part.source.start
516
- end = part.source.end
517
- virtualText = part.source.value
518
- styleId = agentStyleId
519
- } else if (part.type === "text" && part.source?.text) {
520
- start = part.source.text.start
521
- end = part.source.text.end
522
- virtualText = part.source.text.value
523
- styleId = pasteStyleId
524
- }
525
-
526
- if (virtualText) {
527
- const extmarkId = input.extmarks.create({
528
- start,
529
- end,
530
- virtual: true,
531
- styleId,
532
- typeId: promptPartTypeId,
533
- })
534
- newMap.set(extmarkId, partIndex)
535
- }
536
- })
537
-
538
- setStore("extmarkToPartIndex", newMap)
539
- }
540
-
541
- function syncExtmarksWithPromptParts() {
542
- const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId)
543
- const currentMap = store.extmarkToPartIndex
544
- const currentParts = store.prompt.parts
545
-
546
- setStore(
547
- produce((draft) => {
548
- const newMap = new Map<number, number>()
549
- const newParts: typeof draft.prompt.parts = []
550
-
551
- for (const extmark of allExtmarks) {
552
- const partIndex = currentMap.get(extmark.id)
553
- if (partIndex !== undefined) {
554
- const part = currentParts[partIndex]
555
- if (part) {
556
- // Create a deep copy to avoid mutating the original
557
- const updatedPart = { ...part }
558
- if (part.type === "agent" && part.source) {
559
- updatedPart.source = {
560
- ...part.source,
561
- start: extmark.start,
562
- end: extmark.end,
563
- }
564
- } else if (part.type === "file" && part.source?.text) {
565
- updatedPart.source = {
566
- ...part.source,
567
- text: {
568
- ...part.source.text,
569
- start: extmark.start,
570
- end: extmark.end,
571
- },
572
- }
573
- } else if (part.type === "text" && part.source?.text) {
574
- updatedPart.source = {
575
- ...part.source,
576
- text: {
577
- ...part.source.text,
578
- start: extmark.start,
579
- end: extmark.end,
580
- },
581
- }
582
- }
583
- newMap.set(extmark.id, newParts.length)
584
- newParts.push(updatedPart)
585
- }
586
- }
587
- }
588
-
589
- draft.extmarkToPartIndex = newMap
590
- draft.prompt.parts = newParts
591
- }),
592
- )
593
- }
594
-
595
- command.register(() => [
596
- {
597
- title: "Stash prompt",
598
- value: "prompt.stash",
599
- category: "Prompt",
600
- enabled: !!store.prompt.input,
601
- onSelect: (dialog) => {
602
- if (!store.prompt.input) return
603
- stash.push({
604
- input: store.prompt.input,
605
- parts: store.prompt.parts,
606
- })
607
- input.extmarks.clear()
608
- input.clear()
609
- setStore("prompt", { input: "", parts: [] })
610
- setStore("extmarkToPartIndex", new Map())
611
- dialog.clear()
612
- },
613
- },
614
- {
615
- title: "Stash pop",
616
- value: "prompt.stash.pop",
617
- category: "Prompt",
618
- enabled: stash.list().length > 0,
619
- onSelect: (dialog) => {
620
- const entry = stash.pop()
621
- if (entry) {
622
- input.setText(entry.input)
623
- setStore("prompt", { input: entry.input, parts: entry.parts })
624
- restoreExtmarksFromParts(entry.parts)
625
- input.gotoBufferEnd()
626
- }
627
- dialog.clear()
628
- },
629
- },
630
- {
631
- title: "Stash list",
632
- value: "prompt.stash.list",
633
- category: "Prompt",
634
- enabled: stash.list().length > 0,
635
- onSelect: (dialog) => {
636
- dialog.replace(() => (
637
- <DialogStash
638
- onSelect={(entry) => {
639
- input.setText(entry.input)
640
- setStore("prompt", { input: entry.input, parts: entry.parts })
641
- restoreExtmarksFromParts(entry.parts)
642
- input.gotoBufferEnd()
643
- }}
644
- />
645
- ))
646
- },
647
- },
648
- ])
649
-
650
- async function submit() {
651
- if (props.disabled) return
652
- if (autocomplete?.visible) return
653
- if (!store.prompt.input) return
654
- const trimmed = store.prompt.input.trim()
655
- if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
656
- exit()
657
- return
658
- }
659
- const selectedModel = local.model.current()
660
- if (!selectedModel) {
661
- promptModelWarning()
662
- return
663
- }
664
- const sessionID = props.sessionID
665
- ? props.sessionID
666
- : await (async () => {
667
- const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id)
668
- return sessionID
669
- })()
670
- const messageID = Identifier.ascending("message")
671
- let inputText = store.prompt.input
672
-
673
- // Expand pasted text inline before submitting
674
- const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId)
675
- const sortedExtmarks = allExtmarks.sort((a: { start: number }, b: { start: number }) => b.start - a.start)
676
-
677
- for (const extmark of sortedExtmarks) {
678
- const partIndex = store.extmarkToPartIndex.get(extmark.id)
679
- if (partIndex !== undefined) {
680
- const part = store.prompt.parts[partIndex]
681
- if (part?.type === "text" && part.text) {
682
- const before = inputText.slice(0, extmark.start)
683
- const after = inputText.slice(extmark.end)
684
- inputText = before + part.text + after
685
- }
686
- }
687
- }
688
-
689
- // Filter out text parts (pasted content) since they're now expanded inline
690
- const nonTextParts = store.prompt.parts.filter((part) => part.type !== "text")
691
-
692
- // Capture mode before it gets reset
693
- const currentMode = store.mode
694
- const variant = local.model.variant.current()
695
-
696
- if (store.mode === "shell") {
697
- sdk.client.session.shell({
698
- sessionID,
699
- agent: local.agent.current().name,
700
- model: {
701
- providerID: selectedModel.providerID,
702
- modelID: selectedModel.modelID,
703
- },
704
- command: inputText,
705
- })
706
- setStore("mode", "normal")
707
- } else if (
708
- inputText.startsWith("/") &&
709
- iife(() => {
710
- const firstLine = inputText.split("\n")[0]
711
- const command = firstLine.split(" ")[0].slice(1)
712
- return sync.data.command.some((x) => x.name === command)
713
- })
714
- ) {
715
- // Parse command from first line, preserve multi-line content in arguments
716
- const firstLineEnd = inputText.indexOf("\n")
717
- const firstLine = firstLineEnd === -1 ? inputText : inputText.slice(0, firstLineEnd)
718
- const [command, ...firstLineArgs] = firstLine.split(" ")
719
- const restOfInput = firstLineEnd === -1 ? "" : inputText.slice(firstLineEnd + 1)
720
- const args = firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "")
721
-
722
- sdk.client.session.command({
723
- sessionID,
724
- command: command.slice(1),
725
- arguments: args,
726
- agent: local.agent.current().name,
727
- model: `${selectedModel.providerID}/${selectedModel.modelID}`,
728
- messageID,
729
- variant,
730
- parts: nonTextParts
731
- .filter((x) => x.type === "file")
732
- .map((x) => ({
733
- id: Identifier.ascending("part"),
734
- ...x,
735
- })),
736
- })
737
- } else {
738
- sdk.client.session
739
- .prompt({
740
- sessionID,
741
- ...selectedModel,
742
- messageID,
743
- agent: local.agent.current().name,
744
- model: selectedModel,
745
- variant,
746
- parts: [
747
- {
748
- id: Identifier.ascending("part"),
749
- type: "text",
750
- text: inputText,
751
- },
752
- ...nonTextParts.map((x) => ({
753
- id: Identifier.ascending("part"),
754
- ...x,
755
- })),
756
- ],
757
- })
758
- .catch(() => { })
759
- }
760
- history.append({
761
- ...store.prompt,
762
- mode: currentMode,
763
- })
764
- input.extmarks.clear()
765
- setStore("prompt", {
766
- input: "",
767
- parts: [],
768
- })
769
- setStore("extmarkToPartIndex", new Map())
770
- props.onSubmit?.()
771
-
772
- // temporary hack to make sure the message is sent
773
- if (!props.sessionID)
774
- setTimeout(() => {
775
- route.navigate({
776
- type: "session",
777
- sessionID,
778
- })
779
- }, 50)
780
- input.clear()
781
- }
782
- const exit = useExit()
783
-
784
- function pasteText(text: string, virtualText: string) {
785
- const currentOffset = input.visualCursor.offset
786
- const extmarkStart = currentOffset
787
- const extmarkEnd = extmarkStart + virtualText.length
788
-
789
- input.insertText(virtualText + " ")
790
-
791
- const extmarkId = input.extmarks.create({
792
- start: extmarkStart,
793
- end: extmarkEnd,
794
- virtual: true,
795
- styleId: pasteStyleId,
796
- typeId: promptPartTypeId,
797
- })
798
-
799
- setStore(
800
- produce((draft) => {
801
- const partIndex = draft.prompt.parts.length
802
- draft.prompt.parts.push({
803
- type: "text" as const,
804
- text,
805
- source: {
806
- text: {
807
- start: extmarkStart,
808
- end: extmarkEnd,
809
- value: virtualText,
810
- },
811
- },
812
- })
813
- draft.extmarkToPartIndex.set(extmarkId, partIndex)
814
- }),
815
- )
816
- }
817
-
818
- function clearPromptInput() {
819
- input.clear()
820
- input.extmarks.clear()
821
- setStore("prompt", {
822
- input: "",
823
- parts: [],
824
- })
825
- setStore("extmarkToPartIndex", new Map())
826
- setEscClearAt(undefined)
827
- }
828
-
829
- function insertPlainText(text: string) {
830
- if (!text) return
831
- input.focus()
832
- input.insertText(text)
833
- input.gotoBufferEnd()
834
- const value = input.plainText
835
- setStore("prompt", "input", value)
836
- autocomplete.onInput(value)
837
- syncExtmarksWithPromptParts()
838
- setTimeout(() => {
839
- input.getLayoutNode().markDirty()
840
- renderer.requestRender()
841
- }, 0)
842
- }
843
-
844
- async function pasteImage(file: { filename?: string; content: string; mime: string; path?: string }) {
845
- const currentOffset = input.visualCursor.offset
846
- const extmarkStart = currentOffset
847
- const count = store.prompt.parts.filter((x) => x.type === "file").length
848
- const virtualText = file.path ? `[Image ${count + 1}: ${file.path}]` : `[Image ${count + 1}]`
849
- const extmarkEnd = extmarkStart + virtualText.length
850
- const textToInsert = virtualText + " "
851
-
852
- input.insertText(textToInsert)
853
-
854
- const extmarkId = input.extmarks.create({
855
- start: extmarkStart,
856
- end: extmarkEnd,
857
- virtual: true,
858
- styleId: pasteStyleId,
859
- typeId: promptPartTypeId,
860
- })
861
-
862
- const sourcePath = file.path ?? file.filename ?? ""
863
- const part: Omit<FilePart, "id" | "messageID" | "sessionID"> = {
864
- type: "file" as const,
865
- mime: file.mime,
866
- filename: file.filename,
867
- url: `data:${file.mime};base64,${file.content}`,
868
- source: {
869
- type: "file",
870
- path: sourcePath,
871
- text: {
872
- start: extmarkStart,
873
- end: extmarkEnd,
874
- value: virtualText,
875
- },
876
- },
877
- }
878
- setStore(
879
- produce((draft) => {
880
- const partIndex = draft.prompt.parts.length
881
- draft.prompt.parts.push(part)
882
- draft.extmarkToPartIndex.set(extmarkId, partIndex)
883
- }),
884
- )
885
- return
886
- }
887
-
888
- const highlight = createMemo(() => {
889
- if (keybind.leader) return theme.border
890
- if (store.mode === "shell") return theme.primary
891
- return local.agent.color(local.agent.current().name)
892
- })
893
-
894
- const showVariant = createMemo(() => {
895
- const variants = local.model.variant.list()
896
- if (variants.length === 0) return false
897
- const current = local.model.variant.current()
898
- return !!current
899
- })
900
-
901
- const spinnerDef = createMemo(() => {
902
- const color = local.agent.color(local.agent.current().name)
903
- return {
904
- frames: createFrames({
905
- color,
906
- style: "custom",
907
- activeChar: "▣",
908
- inactiveChar: "·",
909
- trailSteps: 1,
910
- inactiveFactor: 0.6,
911
- // enableFading: false,
912
- minAlpha: 0.3,
913
- }),
914
- color: createColors({
915
- color,
916
- style: "custom",
917
- activeChar: "▣",
918
- inactiveChar: "·",
919
- trailSteps: 1,
920
- inactiveFactor: 0.6,
921
- // enableFading: false,
922
- minAlpha: 0.3,
923
- }),
924
- }
925
- })
926
-
927
- return (
928
- <>
929
- <Autocomplete
930
- sessionID={props.sessionID}
931
- ref={(r) => (autocomplete = r)}
932
- anchor={() => anchor}
933
- input={() => input}
934
- setPrompt={(cb) => {
935
- setStore("prompt", produce(cb))
936
- }}
937
- setExtmark={(partIndex, extmarkId) => {
938
- setStore("extmarkToPartIndex", (map: Map<number, number>) => {
939
- const newMap = new Map(map)
940
- newMap.set(extmarkId, partIndex)
941
- return newMap
942
- })
943
- }}
944
- value={store.prompt.input}
945
- fileStyleId={fileStyleId}
946
- agentStyleId={agentStyleId}
947
- promptPartTypeId={() => promptPartTypeId}
948
- />
949
- <box ref={(r) => (anchor = r)} visible={props.visible !== false}>
950
- <Show when={status().type !== "idle"}>
951
- <box flexDirection="row" height={1} marginLeft={1}>
952
- <text fg={store.interrupt > 0 ? theme.primary : theme.text}>
953
- esc{" "}
954
- <span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
955
- {store.interrupt > 0 ? "again to interrupt" : "interrupt"}
956
- </span>
957
- </text>
958
- </box>
959
- </Show>
960
- <box
961
- border={["left"]}
962
- borderColor={highlight()}
963
- customBorderChars={{
964
- ...EmptyBorder,
965
- vertical: "┃",
966
- bottomLeft: "╹",
967
- }}
968
- >
969
- <box
970
- paddingLeft={2}
971
- paddingRight={2}
972
- paddingTop={1}
973
- flexShrink={0}
974
- backgroundColor={theme.backgroundElement}
975
- flexGrow={1}
976
- >
977
- <textarea
978
- placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
979
- textColor={keybind.leader ? theme.textMuted : theme.text}
980
- focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
981
- minHeight={1}
982
- maxHeight={6}
983
- onContentChange={() => {
984
- const value = input.plainText
985
- setStore("prompt", "input", value)
986
- autocomplete.onInput(value)
987
- syncExtmarksWithPromptParts()
988
- }}
989
- keyBindings={textareaKeybindings()}
990
- onKeyDown={async (e) => {
991
- if (props.disabled) {
992
- e.preventDefault()
993
- return
994
- }
995
- if (e.name === "escape" && store.mode === "normal" && store.prompt.input !== "") {
996
- const now = Date.now()
997
- const last = escClearAt()
998
- const within = last !== undefined && now - last < escClearWindowMs
999
- if (within && status().type === "idle") {
1000
- clearPromptInput()
1001
- e.preventDefault()
1002
- return
1003
- }
1004
- if (!within) {
1005
- setEscClearAt(now)
1006
- toast.show({
1007
- variant: "warning",
1008
- message: "Press esc again to clear input",
1009
- duration: escClearWindowMs,
1010
- })
1011
- if (status().type === "idle") {
1012
- e.preventDefault()
1013
- return
1014
- }
1015
- }
1016
- }
1017
- if (keybind.match("input_clear", e) && store.prompt.input !== "") {
1018
- clearPromptInput()
1019
- return
1020
- }
1021
- if (keybind.match("app_exit", e)) {
1022
- if (store.prompt.input === "") {
1023
- await exit()
1024
- // Don't preventDefault - let textarea potentially handle the event
1025
- e.preventDefault()
1026
- return
1027
- }
1028
- }
1029
- if (e.name === "!" && input.visualCursor.offset === 0) {
1030
- setStore("mode", "shell")
1031
- e.preventDefault()
1032
- return
1033
- }
1034
- if (store.mode === "shell") {
1035
- if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
1036
- setStore("mode", "normal")
1037
- e.preventDefault()
1038
- return
1039
- }
1040
- }
1041
- if (store.mode === "normal") autocomplete.onKeyDown(e)
1042
- if (!autocomplete.visible) {
1043
- if (
1044
- (keybind.match("history_previous", e) && input.cursorOffset === 0) ||
1045
- (keybind.match("history_next", e) && input.cursorOffset === input.plainText.length)
1046
- ) {
1047
- const direction = keybind.match("history_previous", e) ? -1 : 1
1048
- const item = history.move(direction, input.plainText)
1049
-
1050
- if (item) {
1051
- input.setText(item.input)
1052
- setStore("prompt", item)
1053
- setStore("mode", item.mode ?? "normal")
1054
- restoreExtmarksFromParts(item.parts)
1055
- e.preventDefault()
1056
- if (direction === -1) input.cursorOffset = 0
1057
- if (direction === 1) input.cursorOffset = input.plainText.length
1058
- }
1059
- return
1060
- }
1061
-
1062
- if (keybind.match("history_previous", e) && input.visualCursor.visualRow === 0) input.cursorOffset = 0
1063
- if (keybind.match("history_next", e) && input.visualCursor.visualRow === input.height - 1)
1064
- input.cursorOffset = input.plainText.length
1065
- }
1066
- }}
1067
- onSubmit={submit}
1068
- onPaste={async (event: PasteEvent) => {
1069
- if (props.disabled) {
1070
- event.preventDefault()
1071
- return
1072
- }
1073
-
1074
- const clipboardText = async () => {
1075
- return await Clipboard.readText()
1076
- }
1077
-
1078
- // Normalize line endings at the boundary
1079
- // Windows ConPTY/Terminal often sends CR-only newlines in bracketed paste
1080
- // Replace CRLF first, then any remaining CR
1081
- const normalizedText = event.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\0/g, "")
1082
- const allowClipboardImage = sync.data.config.experimental?.paste_clipboard_image !== false
1083
- const clipboardFallback = normalizedText.trim() ? undefined : await clipboardText()
1084
- const text = clipboardFallback ? clipboardFallback : normalizedText
1085
- const pastedContent = text.trim()
1086
- if (!pastedContent) {
1087
- if (allowClipboardImage) {
1088
- const clipboard = await readClipboardImage({ allowScreenClip: true })
1089
- if (clipboard) {
1090
- event.preventDefault()
1091
- await pasteImage(clipboard)
1092
- return
1093
- }
1094
- }
1095
- return
1096
- }
1097
-
1098
- if (allowClipboardImage) {
1099
- const dataImage = Clipboard.parseImageDataUrl(pastedContent)
1100
- if (dataImage) {
1101
- event.preventDefault()
1102
- await pasteImage({
1103
- filename: "clipboard",
1104
- mime: dataImage.mime,
1105
- content: dataImage.data,
1106
- })
1107
- return
1108
- }
1109
- }
1110
-
1111
- // Some terminals paste images as a placeholder string like:
1112
- // "img 483795798.png". On Win11, Snipping Tool also writes a temp file
1113
- // under Packages/*/TempState/ScreenClip. Prefer that when present.
1114
- if (allowClipboardImage && pastedContent.toLowerCase().startsWith("img ")) {
1115
- const name = pastedContent
1116
- .slice(4)
1117
- .trim()
1118
- .replace(/^'+|'+$/g, "")
1119
- const fromPath = path.isAbsolute(name) ? name : ""
1120
- if (fromPath) {
1121
- const handled = await pasteImagePath(fromPath, event)
1122
- if (handled) return
1123
- }
1124
- if (name.endsWith(".png")) {
1125
- const local = process.env["LOCALAPPDATA"]
1126
- if (local) {
1127
- const cwd = path.join(local, "Packages")
1128
- const glob = new Bun.Glob(
1129
- `{MicrosoftWindows.Client.CBS_*,Microsoft.ScreenSketch_*}/TempState/ScreenClip/${name}`,
1130
- )
1131
- for await (const item of glob.scan({ cwd, absolute: true })) {
1132
- const file = Bun.file(item)
1133
- if (file.type.startsWith("image/")) {
1134
- const content = await file
1135
- .arrayBuffer()
1136
- .then((buffer) => Buffer.from(buffer).toString("base64"))
1137
- .catch(() => { })
1138
- if (content) {
1139
- event.preventDefault()
1140
- await pasteImage({ filename: file.name, mime: file.type, content })
1141
- return
1142
- }
1143
- }
1144
- }
1145
- }
1146
- }
1147
-
1148
- if (allowClipboardImage) {
1149
- const content = await Clipboard.read()
1150
- if (content?.mime.startsWith("image/")) {
1151
- event.preventDefault()
1152
- await pasteImage({
1153
- filename: "clipboard",
1154
- mime: content.mime,
1155
- content: content.data,
1156
- })
1157
- return
1158
- }
1159
- }
1160
- }
1161
-
1162
- // trim ' from the beginning and end of the pasted content. just
1163
- // ' and nothing else
1164
- const filepath = pastedContent.replace(/^'+|'+$/g, "").replace(/\\ /g, " ")
1165
- const isUrl = /^(https?):\/\//.test(filepath)
1166
- if (!isUrl) {
1167
- const handled = await pasteImagePath(filepath, event)
1168
- if (handled) return
1169
- }
1170
-
1171
- const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1
1172
- if (
1173
- (lineCount >= 3 || pastedContent.length > 150) &&
1174
- !sync.data.config.experimental?.disable_paste_summary
1175
- ) {
1176
- event.preventDefault()
1177
- pasteText(pastedContent, `[Pasted ~${lineCount} lines]`)
1178
- return
1179
- }
1180
-
1181
- if (clipboardFallback) {
1182
- event.preventDefault()
1183
- insertPlainText(text)
1184
- return
1185
- }
1186
-
1187
- // Force layout update and render for the pasted content
1188
- setTimeout(() => {
1189
- input.getLayoutNode().markDirty()
1190
- renderer.requestRender()
1191
- }, 0)
1192
- }}
1193
- ref={(r: TextareaRenderable) => {
1194
- input = r
1195
- if (promptPartTypeId === 0) {
1196
- promptPartTypeId = input.extmarks.registerType("prompt-part")
1197
- }
1198
- props.ref?.(ref)
1199
- setTimeout(() => {
1200
- input.cursorColor = theme.text
1201
- }, 0)
1202
- }}
1203
- onMouseDown={(r: MouseEvent) => r.target?.focus()}
1204
- focusedBackgroundColor={theme.backgroundElement}
1205
- cursorColor={theme.text}
1206
- syntaxStyle={syntax()}
1207
- />
1208
- <box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
1209
- <text fg={highlight()}>
1210
- {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
1211
- </text>
1212
- <Show when={store.mode === "normal"}>
1213
- <box flexDirection="row" gap={1}>
1214
- <text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
1215
- {local.model.parsed().model}
1216
- </text>
1217
- <text fg={theme.textMuted}>{local.model.parsed().provider}</text>
1218
- <Show when={showVariant()}>
1219
- <text>
1220
- <span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
1221
- </text>
1222
- </Show>
1223
- </box>
1224
- </Show>
1225
- </box>
1226
- </box>
1227
- </box>
1228
- <box
1229
- height={1}
1230
- border={["left"]}
1231
- borderColor={highlight()}
1232
- customBorderChars={{
1233
- ...EmptyBorder,
1234
- vertical: theme.backgroundElement.a !== 0 ? "╹" : " ",
1235
- }}
1236
- >
1237
- <box
1238
- height={1}
1239
- border={["bottom"]}
1240
- borderColor={theme.backgroundElement}
1241
- customBorderChars={
1242
- theme.backgroundElement.a !== 0
1243
- ? {
1244
- ...EmptyBorder,
1245
- horizontal: "▀",
1246
- }
1247
- : {
1248
- ...EmptyBorder,
1249
- horizontal: " ",
1250
- }
1251
- }
1252
- />
1253
- </box>
1254
- <box flexDirection="row" justifyContent="space-between">
1255
- <Show when={status().type === "retry"} fallback={<text />}>
1256
- <box flexDirection="row" gap={1} flexGrow={1} justifyContent="space-between">
1257
- <box flexShrink={0} flexDirection="row" gap={1}>
1258
- <box flexDirection="row" gap={1} flexShrink={0}>
1259
- {(() => {
1260
- const retry = createMemo(() => {
1261
- const s = status()
1262
- if (s.type !== "retry") return
1263
- return s
1264
- })
1265
- const message = createMemo(() => {
1266
- const r = retry()
1267
- if (!r) return
1268
- if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
1269
- return "gemini is way too hot right now"
1270
- if (r.message.length > 80) return r.message.slice(0, 80) + "..."
1271
- return r.message
1272
- })
1273
- const isTruncated = createMemo(() => {
1274
- const r = retry()
1275
- if (!r) return false
1276
- return r.message.length > 120
1277
- })
1278
- const [seconds, setSeconds] = createSignal(0)
1279
- onMount(() => {
1280
- const timer = setInterval(() => {
1281
- const next = retry()?.next
1282
- if (next) setSeconds(Math.round((next - Date.now()) / 1000))
1283
- }, 1000)
1284
-
1285
- onCleanup(() => {
1286
- clearInterval(timer)
1287
- })
1288
- })
1289
- const handleMessageClick = () => {
1290
- const r = retry()
1291
- if (!r) return
1292
- if (isTruncated()) {
1293
- DialogAlert.show(dialog, "Retry Error", r.message)
1294
- }
1295
- }
1296
-
1297
- const retryText = () => {
1298
- const r = retry()
1299
- if (!r) return ""
1300
- const baseMessage = message()
1301
- const truncatedHint = isTruncated() ? " (click to expand)" : ""
1302
- const duration = formatDuration(seconds())
1303
- const retryInfo = ` [retrying ${duration ? `in ${duration} ` : ""}attempt #${r.attempt}]`
1304
- return baseMessage + truncatedHint + retryInfo
1305
- }
1306
-
1307
- return (
1308
- <Show when={retry()}>
1309
- <box onMouseUp={handleMessageClick}>
1310
- <text fg={theme.error}>{retryText()}</text>
1311
- </box>
1312
- </Show>
1313
- )
1314
- })()}
1315
- </box>
1316
- </box>
1317
- </box>
1318
- </Show>
1319
- <Show when={status().type !== "retry"}>
1320
- <box gap={2} flexDirection="row">
1321
- <Switch>
1322
- <Match when={store.mode === "normal"}>
1323
- <Show when={local.model.variant.list().length > 0}>
1324
- <text fg={theme.text}>
1325
- {keybind.print("variant_cycle")} <span style={{ fg: theme.textMuted }}>variants</span>
1326
- </text>
1327
- </Show>
1328
- <text fg={theme.text}>
1329
- {keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>agents</span>
1330
- </text>
1331
- <text fg={theme.text}>
1332
- {keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
1333
- </text>
1334
- </Match>
1335
- <Match when={store.mode === "shell"}>
1336
- <text fg={theme.text}>
1337
- esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
1338
- </text>
1339
- </Match>
1340
- </Switch>
1341
- </box>
1342
- </Show>
1343
- </box>
1344
- </box>
1345
- </>
1346
- )
1347
- }