@celljs/ta-web 4.6.1 → 4.8.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 (361) hide show
  1. package/README.en.md +90 -0
  2. package/README.md +13 -7
  3. package/esm/browser/chat/chat-protocol.d.ts +4 -0
  4. package/esm/browser/chat/chat-view.js +4 -1
  5. package/esm/browser/chat/chat-view.js.map +1 -1
  6. package/esm/browser/chat/chat.d.ts +8 -1
  7. package/esm/browser/chat/chat.js +34 -15
  8. package/esm/browser/chat/chat.js.map +1 -1
  9. package/esm/browser/chat/draft-model.d.ts +15 -0
  10. package/esm/browser/chat/draft-model.js +53 -0
  11. package/esm/browser/chat/draft-model.js.map +1 -0
  12. package/esm/browser/components/account-menu.d.ts +3 -2
  13. package/esm/browser/components/account-menu.js +21 -16
  14. package/esm/browser/components/account-menu.js.map +1 -1
  15. package/esm/browser/components/app-view.js +13 -4
  16. package/esm/browser/components/app-view.js.map +1 -1
  17. package/esm/browser/components/chat-list.d.ts +3 -1
  18. package/esm/browser/components/chat-list.js +6 -1
  19. package/esm/browser/components/chat-list.js.map +1 -1
  20. package/esm/browser/components/composer-box.d.ts +6 -0
  21. package/esm/browser/components/composer-box.js +50 -13
  22. package/esm/browser/components/composer-box.js.map +1 -1
  23. package/esm/browser/components/composer.js +1 -1
  24. package/esm/browser/components/composer.js.map +1 -1
  25. package/esm/browser/components/dialog-view.d.ts +4 -1
  26. package/esm/browser/components/dialog-view.js +11 -7
  27. package/esm/browser/components/dialog-view.js.map +1 -1
  28. package/esm/browser/components/folder-picker.d.ts +37 -6
  29. package/esm/browser/components/folder-picker.js +155 -33
  30. package/esm/browser/components/folder-picker.js.map +1 -1
  31. package/esm/browser/components/notice-center.js +3 -1
  32. package/esm/browser/components/notice-center.js.map +1 -1
  33. package/esm/browser/components/remote-hosts.d.ts +7 -0
  34. package/esm/browser/components/remote-hosts.js +94 -0
  35. package/esm/browser/components/remote-hosts.js.map +1 -0
  36. package/esm/browser/components/remote-prompts.d.ts +7 -0
  37. package/esm/browser/components/remote-prompts.js +72 -0
  38. package/esm/browser/components/remote-prompts.js.map +1 -0
  39. package/esm/browser/components/session-view.js +48 -4
  40. package/esm/browser/components/session-view.js.map +1 -1
  41. package/esm/browser/components/settings-view.js +8 -1
  42. package/esm/browser/components/settings-view.js.map +1 -1
  43. package/esm/browser/components/terminal-art.d.ts +7 -0
  44. package/esm/browser/components/terminal-art.js +12 -0
  45. package/esm/browser/components/terminal-art.js.map +1 -0
  46. package/esm/browser/components/tool-view.js +1 -1
  47. package/esm/browser/components/tool-view.js.map +1 -1
  48. package/esm/browser/components/web-composer.d.ts +4 -1
  49. package/esm/browser/components/web-composer.js +2 -2
  50. package/esm/browser/components/web-composer.js.map +1 -1
  51. package/esm/browser/components/working-directory.js +1 -1
  52. package/esm/browser/components/working-directory.js.map +1 -1
  53. package/esm/browser/extension/icons.js +6 -1
  54. package/esm/browser/extension/icons.js.map +1 -1
  55. package/esm/browser/extension/web-extension-api.d.ts +120 -2
  56. package/esm/browser/extension/web-extensions.d.ts +3 -0
  57. package/esm/browser/extension/web-extensions.js +33 -1
  58. package/esm/browser/extension/web-extensions.js.map +1 -1
  59. package/esm/browser/i18n/locales.d.ts +44 -1
  60. package/esm/browser/i18n/messages.d.ts +43 -0
  61. package/esm/browser/i18n/messages.js +99 -13
  62. package/esm/browser/i18n/messages.js.map +1 -1
  63. package/esm/browser/notices/notices-protocol.d.ts +3 -0
  64. package/esm/browser/notices/notices.d.ts +3 -0
  65. package/esm/browser/notices/notices.js +15 -0
  66. package/esm/browser/notices/notices.js.map +1 -1
  67. package/esm/browser/style/base.d.ts +1 -1
  68. package/esm/browser/style/base.js +6 -1
  69. package/esm/browser/style/base.js.map +1 -1
  70. package/esm/browser/style/conversation.d.ts +1 -1
  71. package/esm/browser/style/conversation.js +16 -1
  72. package/esm/browser/style/conversation.js.map +1 -1
  73. package/esm/browser/style/layout.d.ts +1 -1
  74. package/esm/browser/style/layout.js +53 -15
  75. package/esm/browser/style/layout.js.map +1 -1
  76. package/esm/browser/style/shell.d.ts +1 -1
  77. package/esm/browser/style/shell.js +1 -0
  78. package/esm/browser/style/shell.js.map +1 -1
  79. package/esm/browser/terminal/canvas-font.d.ts +10 -0
  80. package/esm/browser/terminal/canvas-font.js +34 -0
  81. package/esm/browser/terminal/canvas-font.js.map +1 -0
  82. package/esm/browser/terminal/terminal-links.d.ts +18 -0
  83. package/esm/browser/terminal/terminal-links.js +72 -0
  84. package/esm/browser/terminal/terminal-links.js.map +1 -0
  85. package/esm/browser/terminal/terminal-theme.d.ts +8 -0
  86. package/esm/browser/terminal/terminal-theme.js +65 -0
  87. package/esm/browser/terminal/terminal-theme.js.map +1 -0
  88. package/esm/browser/terminal/web-terminal.d.ts +74 -0
  89. package/esm/browser/terminal/web-terminal.js +300 -0
  90. package/esm/browser/terminal/web-terminal.js.map +1 -0
  91. package/esm/browser/terminal/xterm.d.ts +5 -0
  92. package/esm/browser/terminal/xterm.js +9 -0
  93. package/esm/browser/terminal/xterm.js.map +1 -0
  94. package/esm/browser/util/desktop.d.ts +43 -0
  95. package/esm/browser/util/desktop.js +55 -0
  96. package/esm/browser/util/desktop.js.map +1 -0
  97. package/esm/browser/util/file-drag.d.ts +26 -0
  98. package/esm/browser/util/file-drag.js +73 -0
  99. package/esm/browser/util/file-drag.js.map +1 -0
  100. package/esm/browser/util/message.d.ts +1 -0
  101. package/esm/browser/util/message.js +7 -5
  102. package/esm/browser/util/message.js.map +1 -1
  103. package/esm/browser/util/terminal-text.d.ts +33 -0
  104. package/esm/browser/util/terminal-text.js +209 -0
  105. package/esm/browser/util/terminal-text.js.map +1 -0
  106. package/esm/browser/util/tool.js +16 -0
  107. package/esm/browser/util/tool.js.map +1 -1
  108. package/esm/browser/workbench-protocol.d.ts +7 -1
  109. package/esm/browser/workbench-protocol.js.map +1 -1
  110. package/esm/browser/workbench.d.ts +7 -1
  111. package/esm/browser/workbench.js +23 -0
  112. package/esm/browser/workbench.js.map +1 -1
  113. package/lib/browser/chat/chat-protocol.d.ts +4 -0
  114. package/lib/browser/chat/chat-protocol.d.ts.map +1 -1
  115. package/lib/browser/chat/chat-view.d.ts.map +1 -1
  116. package/lib/browser/chat/chat-view.js +4 -1
  117. package/lib/browser/chat/chat-view.js.map +1 -1
  118. package/lib/browser/chat/chat.d.ts +8 -1
  119. package/lib/browser/chat/chat.d.ts.map +1 -1
  120. package/lib/browser/chat/chat.js +34 -15
  121. package/lib/browser/chat/chat.js.map +1 -1
  122. package/lib/browser/chat/chat.spec.js +30 -0
  123. package/lib/browser/chat/chat.spec.js.map +1 -1
  124. package/lib/browser/chat/draft-model.d.ts +16 -0
  125. package/lib/browser/chat/draft-model.d.ts.map +1 -0
  126. package/lib/browser/chat/draft-model.js +90 -0
  127. package/lib/browser/chat/draft-model.js.map +1 -0
  128. package/lib/browser/chat/draft-model.spec.d.ts +2 -0
  129. package/lib/browser/chat/draft-model.spec.d.ts.map +1 -0
  130. package/lib/browser/chat/draft-model.spec.js +31 -0
  131. package/lib/browser/chat/draft-model.spec.js.map +1 -0
  132. package/lib/browser/components/account-menu.d.ts +3 -2
  133. package/lib/browser/components/account-menu.d.ts.map +1 -1
  134. package/lib/browser/components/account-menu.js +21 -16
  135. package/lib/browser/components/account-menu.js.map +1 -1
  136. package/lib/browser/components/app-view.d.ts.map +1 -1
  137. package/lib/browser/components/app-view.js +13 -4
  138. package/lib/browser/components/app-view.js.map +1 -1
  139. package/lib/browser/components/chat-list.d.ts +3 -1
  140. package/lib/browser/components/chat-list.d.ts.map +1 -1
  141. package/lib/browser/components/chat-list.js +6 -1
  142. package/lib/browser/components/chat-list.js.map +1 -1
  143. package/lib/browser/components/composer-box.d.ts +6 -0
  144. package/lib/browser/components/composer-box.d.ts.map +1 -1
  145. package/lib/browser/components/composer-box.js +51 -14
  146. package/lib/browser/components/composer-box.js.map +1 -1
  147. package/lib/browser/components/composer.d.ts.map +1 -1
  148. package/lib/browser/components/composer.js +1 -1
  149. package/lib/browser/components/composer.js.map +1 -1
  150. package/lib/browser/components/dialog-view.d.ts +4 -1
  151. package/lib/browser/components/dialog-view.d.ts.map +1 -1
  152. package/lib/browser/components/dialog-view.js +11 -7
  153. package/lib/browser/components/dialog-view.js.map +1 -1
  154. package/lib/browser/components/folder-picker.d.ts +37 -6
  155. package/lib/browser/components/folder-picker.d.ts.map +1 -1
  156. package/lib/browser/components/folder-picker.js +156 -33
  157. package/lib/browser/components/folder-picker.js.map +1 -1
  158. package/lib/browser/components/folder-picker.spec.js +15 -0
  159. package/lib/browser/components/folder-picker.spec.js.map +1 -1
  160. package/lib/browser/components/notice-center.d.ts.map +1 -1
  161. package/lib/browser/components/notice-center.js +3 -1
  162. package/lib/browser/components/notice-center.js.map +1 -1
  163. package/lib/browser/components/plus-menu.spec.d.ts +2 -0
  164. package/lib/browser/components/plus-menu.spec.d.ts.map +1 -0
  165. package/lib/browser/components/plus-menu.spec.js +18 -0
  166. package/lib/browser/components/plus-menu.spec.js.map +1 -0
  167. package/lib/browser/components/remote-hosts.d.ts +8 -0
  168. package/lib/browser/components/remote-hosts.d.ts.map +1 -0
  169. package/lib/browser/components/remote-hosts.js +130 -0
  170. package/lib/browser/components/remote-hosts.js.map +1 -0
  171. package/lib/browser/components/remote-prompts.d.ts +8 -0
  172. package/lib/browser/components/remote-prompts.d.ts.map +1 -0
  173. package/lib/browser/components/remote-prompts.js +108 -0
  174. package/lib/browser/components/remote-prompts.js.map +1 -0
  175. package/lib/browser/components/session-view.d.ts.map +1 -1
  176. package/lib/browser/components/session-view.js +48 -4
  177. package/lib/browser/components/session-view.js.map +1 -1
  178. package/lib/browser/components/settings-view.d.ts.map +1 -1
  179. package/lib/browser/components/settings-view.js +7 -0
  180. package/lib/browser/components/settings-view.js.map +1 -1
  181. package/lib/browser/components/terminal-art.d.ts +8 -0
  182. package/lib/browser/components/terminal-art.d.ts.map +1 -0
  183. package/lib/browser/components/terminal-art.js +48 -0
  184. package/lib/browser/components/terminal-art.js.map +1 -0
  185. package/lib/browser/components/tool-view.js +1 -1
  186. package/lib/browser/components/tool-view.js.map +1 -1
  187. package/lib/browser/components/web-composer.d.ts +4 -1
  188. package/lib/browser/components/web-composer.d.ts.map +1 -1
  189. package/lib/browser/components/web-composer.js +2 -2
  190. package/lib/browser/components/web-composer.js.map +1 -1
  191. package/lib/browser/components/working-directory.js +1 -1
  192. package/lib/browser/components/working-directory.js.map +1 -1
  193. package/lib/browser/extension/icons.d.ts.map +1 -1
  194. package/lib/browser/extension/icons.js +5 -0
  195. package/lib/browser/extension/icons.js.map +1 -1
  196. package/lib/browser/extension/web-extension-api.d.ts +120 -2
  197. package/lib/browser/extension/web-extension-api.d.ts.map +1 -1
  198. package/lib/browser/extension/web-extensions.d.ts +3 -0
  199. package/lib/browser/extension/web-extensions.d.ts.map +1 -1
  200. package/lib/browser/extension/web-extensions.js +33 -1
  201. package/lib/browser/extension/web-extensions.js.map +1 -1
  202. package/lib/browser/i18n/locales.d.ts +44 -1
  203. package/lib/browser/i18n/locales.d.ts.map +1 -1
  204. package/lib/browser/i18n/messages.d.ts +43 -0
  205. package/lib/browser/i18n/messages.d.ts.map +1 -1
  206. package/lib/browser/i18n/messages.js +99 -13
  207. package/lib/browser/i18n/messages.js.map +1 -1
  208. package/lib/browser/notices/notices-protocol.d.ts +3 -0
  209. package/lib/browser/notices/notices-protocol.d.ts.map +1 -1
  210. package/lib/browser/notices/notices.d.ts +3 -0
  211. package/lib/browser/notices/notices.d.ts.map +1 -1
  212. package/lib/browser/notices/notices.js +15 -0
  213. package/lib/browser/notices/notices.js.map +1 -1
  214. package/lib/browser/notices/notices.spec.js +8 -3
  215. package/lib/browser/notices/notices.spec.js.map +1 -1
  216. package/lib/browser/style/base.d.ts +1 -1
  217. package/lib/browser/style/base.d.ts.map +1 -1
  218. package/lib/browser/style/base.js +6 -1
  219. package/lib/browser/style/base.js.map +1 -1
  220. package/lib/browser/style/conversation.d.ts +1 -1
  221. package/lib/browser/style/conversation.d.ts.map +1 -1
  222. package/lib/browser/style/conversation.js +16 -1
  223. package/lib/browser/style/conversation.js.map +1 -1
  224. package/lib/browser/style/layout.d.ts +1 -1
  225. package/lib/browser/style/layout.d.ts.map +1 -1
  226. package/lib/browser/style/layout.js +53 -15
  227. package/lib/browser/style/layout.js.map +1 -1
  228. package/lib/browser/style/shell.d.ts +1 -1
  229. package/lib/browser/style/shell.d.ts.map +1 -1
  230. package/lib/browser/style/shell.js +1 -0
  231. package/lib/browser/style/shell.js.map +1 -1
  232. package/lib/browser/terminal/canvas-font.d.ts +11 -0
  233. package/lib/browser/terminal/canvas-font.d.ts.map +1 -0
  234. package/lib/browser/terminal/canvas-font.js +37 -0
  235. package/lib/browser/terminal/canvas-font.js.map +1 -0
  236. package/lib/browser/terminal/canvas-font.spec.d.ts +2 -0
  237. package/lib/browser/terminal/canvas-font.spec.d.ts.map +1 -0
  238. package/lib/browser/terminal/canvas-font.spec.js +32 -0
  239. package/lib/browser/terminal/canvas-font.spec.js.map +1 -0
  240. package/lib/browser/terminal/terminal-links.d.ts +19 -0
  241. package/lib/browser/terminal/terminal-links.d.ts.map +1 -0
  242. package/lib/browser/terminal/terminal-links.js +75 -0
  243. package/lib/browser/terminal/terminal-links.js.map +1 -0
  244. package/lib/browser/terminal/terminal-links.spec.d.ts +2 -0
  245. package/lib/browser/terminal/terminal-links.spec.d.ts.map +1 -0
  246. package/lib/browser/terminal/terminal-links.spec.js +30 -0
  247. package/lib/browser/terminal/terminal-links.spec.js.map +1 -0
  248. package/lib/browser/terminal/terminal-theme.d.ts +9 -0
  249. package/lib/browser/terminal/terminal-theme.d.ts.map +1 -0
  250. package/lib/browser/terminal/terminal-theme.js +68 -0
  251. package/lib/browser/terminal/terminal-theme.js.map +1 -0
  252. package/lib/browser/terminal/terminal-theme.spec.d.ts +2 -0
  253. package/lib/browser/terminal/terminal-theme.spec.d.ts.map +1 -0
  254. package/lib/browser/terminal/terminal-theme.spec.js +24 -0
  255. package/lib/browser/terminal/terminal-theme.spec.js.map +1 -0
  256. package/lib/browser/terminal/web-terminal.d.ts +75 -0
  257. package/lib/browser/terminal/web-terminal.d.ts.map +1 -0
  258. package/lib/browser/terminal/web-terminal.js +304 -0
  259. package/lib/browser/terminal/web-terminal.js.map +1 -0
  260. package/lib/browser/terminal/xterm.d.ts +6 -0
  261. package/lib/browser/terminal/xterm.d.ts.map +1 -0
  262. package/lib/browser/terminal/xterm.js +16 -0
  263. package/lib/browser/terminal/xterm.js.map +1 -0
  264. package/lib/browser/util/desktop.d.ts +44 -0
  265. package/lib/browser/util/desktop.d.ts.map +1 -0
  266. package/lib/browser/util/desktop.js +58 -0
  267. package/lib/browser/util/desktop.js.map +1 -0
  268. package/lib/browser/util/desktop.spec.d.ts +2 -0
  269. package/lib/browser/util/desktop.spec.d.ts.map +1 -0
  270. package/lib/browser/util/desktop.spec.js +67 -0
  271. package/lib/browser/util/desktop.spec.js.map +1 -0
  272. package/lib/browser/util/file-drag.d.ts +27 -0
  273. package/lib/browser/util/file-drag.d.ts.map +1 -0
  274. package/lib/browser/util/file-drag.js +76 -0
  275. package/lib/browser/util/file-drag.js.map +1 -0
  276. package/lib/browser/util/file-drag.spec.d.ts +2 -0
  277. package/lib/browser/util/file-drag.spec.d.ts.map +1 -0
  278. package/lib/browser/util/file-drag.spec.js +46 -0
  279. package/lib/browser/util/file-drag.spec.js.map +1 -0
  280. package/lib/browser/util/message.d.ts +1 -0
  281. package/lib/browser/util/message.d.ts.map +1 -1
  282. package/lib/browser/util/message.js +7 -5
  283. package/lib/browser/util/message.js.map +1 -1
  284. package/lib/browser/util/message.spec.js +2 -0
  285. package/lib/browser/util/message.spec.js.map +1 -1
  286. package/lib/browser/util/terminal-text.d.ts +34 -0
  287. package/lib/browser/util/terminal-text.d.ts.map +1 -0
  288. package/lib/browser/util/terminal-text.js +212 -0
  289. package/lib/browser/util/terminal-text.js.map +1 -0
  290. package/lib/browser/util/terminal-text.spec.d.ts +2 -0
  291. package/lib/browser/util/terminal-text.spec.d.ts.map +1 -0
  292. package/lib/browser/util/terminal-text.spec.js +54 -0
  293. package/lib/browser/util/terminal-text.spec.js.map +1 -0
  294. package/lib/browser/util/tool.d.ts.map +1 -1
  295. package/lib/browser/util/tool.js +16 -0
  296. package/lib/browser/util/tool.js.map +1 -1
  297. package/lib/browser/util/tool.spec.js +12 -0
  298. package/lib/browser/util/tool.spec.js.map +1 -1
  299. package/lib/browser/workbench-protocol.d.ts +7 -1
  300. package/lib/browser/workbench-protocol.d.ts.map +1 -1
  301. package/lib/browser/workbench-protocol.js.map +1 -1
  302. package/lib/browser/workbench.d.ts +7 -1
  303. package/lib/browser/workbench.d.ts.map +1 -1
  304. package/lib/browser/workbench.js +23 -0
  305. package/lib/browser/workbench.js.map +1 -1
  306. package/package.json +11 -7
  307. package/src/browser/chat/chat-protocol.ts +4 -0
  308. package/src/browser/chat/chat-view.tsx +5 -1
  309. package/src/browser/chat/chat.spec.ts +31 -0
  310. package/src/browser/chat/chat.ts +36 -14
  311. package/src/browser/chat/draft-model.spec.tsx +31 -0
  312. package/src/browser/chat/draft-model.tsx +63 -0
  313. package/src/browser/components/account-menu.tsx +23 -20
  314. package/src/browser/components/app-view.tsx +12 -3
  315. package/src/browser/components/chat-list.tsx +9 -2
  316. package/src/browser/components/composer-box.tsx +52 -6
  317. package/src/browser/components/composer.tsx +1 -0
  318. package/src/browser/components/dialog-view.tsx +16 -7
  319. package/src/browser/components/folder-picker.spec.ts +17 -0
  320. package/src/browser/components/folder-picker.tsx +221 -39
  321. package/src/browser/components/notice-center.tsx +7 -1
  322. package/src/browser/components/plus-menu.spec.ts +16 -0
  323. package/src/browser/components/remote-hosts.tsx +149 -0
  324. package/src/browser/components/remote-prompts.tsx +111 -0
  325. package/src/browser/components/session-view.tsx +65 -4
  326. package/src/browser/components/settings-view.tsx +8 -0
  327. package/src/browser/components/terminal-art.tsx +21 -0
  328. package/src/browser/components/tool-view.tsx +1 -1
  329. package/src/browser/components/web-composer.tsx +18 -3
  330. package/src/browser/components/working-directory.tsx +1 -1
  331. package/src/browser/extension/icons.ts +10 -0
  332. package/src/browser/extension/web-extension-api.ts +134 -2
  333. package/src/browser/extension/web-extensions.ts +47 -2
  334. package/src/browser/i18n/messages.ts +99 -13
  335. package/src/browser/notices/notices-protocol.ts +3 -0
  336. package/src/browser/notices/notices.spec.ts +9 -3
  337. package/src/browser/notices/notices.ts +17 -1
  338. package/src/browser/style/base.ts +6 -1
  339. package/src/browser/style/conversation.ts +16 -1
  340. package/src/browser/style/layout.ts +53 -15
  341. package/src/browser/style/shell.ts +1 -0
  342. package/src/browser/terminal/canvas-font.spec.ts +29 -0
  343. package/src/browser/terminal/canvas-font.ts +32 -0
  344. package/src/browser/terminal/terminal-links.spec.ts +32 -0
  345. package/src/browser/terminal/terminal-links.ts +75 -0
  346. package/src/browser/terminal/terminal-theme.spec.ts +23 -0
  347. package/src/browser/terminal/terminal-theme.ts +66 -0
  348. package/src/browser/terminal/web-terminal.ts +330 -0
  349. package/src/browser/terminal/xterm.ts +10 -0
  350. package/src/browser/util/desktop.spec.ts +68 -0
  351. package/src/browser/util/desktop.ts +96 -0
  352. package/src/browser/util/file-drag.spec.ts +48 -0
  353. package/src/browser/util/file-drag.ts +78 -0
  354. package/src/browser/util/message.spec.ts +2 -0
  355. package/src/browser/util/message.ts +7 -4
  356. package/src/browser/util/terminal-text.spec.ts +60 -0
  357. package/src/browser/util/terminal-text.ts +223 -0
  358. package/src/browser/util/tool.spec.ts +12 -0
  359. package/src/browser/util/tool.ts +17 -0
  360. package/src/browser/workbench-protocol.ts +8 -0
  361. package/src/browser/workbench.ts +29 -0
@@ -1,75 +1,230 @@
1
- import type { TaDirEntry } from '@celljs/ta-protocol';
2
- import { ChevronDown, ChevronRight, CornerLeftUp, Folder, X } from 'lucide-react';
1
+ import { type TaDirEntry, TaLocation, type TaRemoteEvent, type TaRemoteHost, type TaRemoteServer } from '@celljs/ta-protocol';
2
+ import { ChevronDown, ChevronRight, CornerLeftUp, Folder, Monitor, Plus, Server, X } from 'lucide-react';
3
3
  import * as React from 'react';
4
4
  import { createPortal } from 'react-dom';
5
- import { useT } from '../i18n/use-t';
5
+ import { type Translate, useT } from '../i18n/use-t';
6
6
  import { MessageUtil } from '../util/message';
7
7
  import { Icon, IconButton } from './icon';
8
8
 
9
+ /** The hosts of ssh, for a picker that offers them: TaWorkbench has it all (TaRemoteServer and its events). */
10
+ export interface FolderPickerRemote {
11
+ readonly remote: Pick<TaRemoteServer, 'listHosts' | 'resolveLocation'>;
12
+ readonly state: { server?: { cwd: string } };
13
+ onRemoteEvent(listener: (event: TaRemoteEvent) => void): () => void;
14
+ }
15
+
16
+ export namespace RemoteHostsUtil {
17
+ /** Why what the user typed is no host, in their words. */
18
+ export function problem(read: Exclude<TaLocation.HostInput, { host: string }>, input: string, t: Translate): string {
19
+ return read.error === 'options' ? t('ta.remote.options', { options: read.options.join(' ') }) : t('ta.remote.invalid', { host: input.trim() });
20
+ }
21
+ }
22
+
9
23
  export namespace FolderPickerUtil {
10
- /** The directory above `path`; undefined at a root (`/`, `C:\\`). */
24
+ /** The directory above; undefined at a root (`/`, `C:\`, the root of a host). */
11
25
  export function parent(path: string): string | undefined {
12
- const trimmed = path.replace(/[\\/]+$/, '');
13
- const at = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
14
- if (at < 0) {
15
- return undefined;
16
- }
17
- const head = trimmed.slice(0, at);
18
- return head === '' || /^[A-Za-z]:$/.test(head) ? head + trimmed[at] : head;
26
+ return TaLocation.parent(path);
19
27
  }
20
28
 
21
29
  export function child(path: string, name: string): string {
22
- const separator = path.includes('/') || !path.includes('\\') ? '/' : '\\';
23
- return path.endsWith(separator) ? path + name : path + separator + name;
30
+ return TaLocation.child(path, name);
24
31
  }
25
32
 
26
33
  /** The folders to offer: no files, no hidden ones (a path typed in still opens those). */
27
34
  export function folders(entries: TaDirEntry[]): TaDirEntry[] {
28
35
  return entries.filter(entry => entry.kind === 'directory' && !entry.name.startsWith('.'));
29
36
  }
37
+
38
+ /** Where the tab of this computer starts: `initial` when it is a path of it, else the server's, else the root. */
39
+ export function localStart(initial: string, serverCwd: string | undefined): string {
40
+ if (!TaLocation.isRemote(initial)) {
41
+ return initial;
42
+ }
43
+ return serverCwd && !TaLocation.isRemote(serverCwd) ? serverCwd : '/';
44
+ }
45
+
46
+ /** What the user typed, as a location of the machine shown: a path there, or a whole location. */
47
+ export function typed(input: string, host: string | undefined): string {
48
+ const text = input.trim();
49
+ return host && !TaLocation.isRemote(text) ? TaLocation.ssh(host, text.startsWith('/') ? text : `/${text}`) : text;
50
+ }
51
+ }
52
+
53
+ /** What the host of the picker is doing while it connects, from the events of TaRemoteServer. */
54
+ function useConnecting(bench: FolderPickerRemote | undefined, host: string | undefined): string | undefined {
55
+ const t = useT();
56
+ const [step, setStep] = React.useState<string>();
57
+ React.useEffect(() => {
58
+ setStep(undefined);
59
+ return bench?.onRemoteEvent((event: TaRemoteEvent) => {
60
+ if (event.type === 'progress' && event.host === host) {
61
+ setStep(t(`ta.remote.step.${event.step}`, { host, detail: event.detail ?? '' }));
62
+ } else if (event.type === 'connection' && event.host === host) {
63
+ setStep(undefined);
64
+ }
65
+ });
66
+ }, [bench, host, t]);
67
+ return step;
68
+ }
69
+
70
+ interface MachinesProps {
71
+ host: string | undefined;
72
+ hosts: TaRemoteHost[];
73
+ onPick(host: string | undefined): void;
74
+ }
75
+
76
+ /** This computer and the hosts of ssh, as tabs; `+` names another host. */
77
+ function Machines({ host, hosts, onPick }: MachinesProps) {
78
+ const t = useT();
79
+ const [adding, setAdding] = React.useState(false);
80
+ const [name, setName] = React.useState('');
81
+ const shown = host && !hosts.some(known => known.host === host) ? [{ host, source: 'recent' as const, connected: false }, ...hosts] : hosts;
82
+ const [problem, setProblem] = React.useState<string>();
83
+ const add = () => {
84
+ const read = TaLocation.readHost(name);
85
+ if ('error' in read) {
86
+ setProblem(RemoteHostsUtil.problem(read, name, t));
87
+ return;
88
+ }
89
+ setAdding(false);
90
+ setName('');
91
+ setProblem(undefined);
92
+ onPick(read.host);
93
+ };
94
+ return (
95
+ <>
96
+ <div className="ta-chips ta-folder-machines" role="tablist" aria-label={t('ta.remote.local')}>
97
+ <button type="button" role="tab" aria-selected={!host} className="ta-chip-filter" onClick={() => onPick(undefined)}>
98
+ <Icon icon={Monitor} size={13} />
99
+ {t('ta.remote.local')}
100
+ </button>
101
+ {shown.map(known => (
102
+ <button
103
+ key={known.host}
104
+ type="button"
105
+ role="tab"
106
+ aria-selected={known.host === host}
107
+ className="ta-chip-filter"
108
+ title={known.connected ? `${known.host} · ${t('ta.remote.connected')}` : known.host}
109
+ onClick={() => onPick(known.host)}
110
+ >
111
+ <Icon icon={Server} size={13} />
112
+ {known.host}
113
+ {known.connected && <span className="ta-remote-dot" />}
114
+ </button>
115
+ ))}
116
+ {adding ? (
117
+ <form
118
+ className="ta-folder-add"
119
+ onSubmit={event => {
120
+ event.preventDefault();
121
+ add();
122
+ }}
123
+ >
124
+ <input
125
+ autoFocus
126
+ spellCheck={false}
127
+ value={name}
128
+ placeholder={t('ta.remote.addHint')}
129
+ aria-label={t('ta.remote.add')}
130
+ aria-invalid={problem ? true : undefined}
131
+ onChange={event => {
132
+ setName(event.target.value);
133
+ setProblem(undefined);
134
+ }}
135
+ onBlur={() => !name.trim() && setAdding(false)}
136
+ onKeyDown={event => {
137
+ if (event.key === 'Escape') {
138
+ event.stopPropagation();
139
+ setAdding(false);
140
+ }
141
+ }}
142
+ />
143
+ </form>
144
+ ) : (
145
+ <IconButton icon={Plus} label={t('ta.remote.add')} onClick={() => setAdding(true)} />
146
+ )}
147
+ </div>
148
+ {problem && <div className="ta-folder-failed">{problem}</div>}
149
+ </>
150
+ );
30
151
  }
31
152
 
32
153
  export interface FolderPickerProps {
33
154
  /** The entries of a directory of the server. */
34
155
  list(directory: string): Promise<TaDirEntry[]>;
35
- /** Where it opens. */
156
+ /** Where it opens: a path, or an `ssh://` location. */
36
157
  initial: string;
37
158
  onPick(path: string): Promise<void>;
38
159
  onClose(): void;
160
+ /** Offers the hosts of ssh too. */
161
+ remote?: FolderPickerRemote;
39
162
  }
40
163
 
41
- /** Picks a directory of the server, folder by folder or by a path typed in, to open a conversation there. */
42
- export function FolderPicker({ list, initial, onPick, onClose }: FolderPickerProps): React.ReactElement {
164
+ /**
165
+ * Picks a directory, of the machine of the server or of a host it reaches with ssh, folder by folder or by
166
+ * a path typed in, to open a conversation there.
167
+ */
168
+ export function FolderPicker({ list, initial, onPick, onClose, remote: bench }: FolderPickerProps): React.ReactElement {
43
169
  const t = useT();
44
170
  const [path, setPath] = React.useState(initial);
45
- const [input, setInput] = React.useState(initial);
171
+ const [input, setInput] = React.useState(TaLocation.path(initial));
46
172
  const [folders, setFolders] = React.useState<TaDirEntry[]>();
47
173
  const [failed, setFailed] = React.useState<string>();
48
174
  const [busy, setBusy] = React.useState(false);
175
+ const [hosts, setHosts] = React.useState<TaRemoteHost[]>([]);
176
+ /** The machine being shown, even before its first directory came. */
177
+ const [host, setHost] = React.useState(TaLocation.host(initial));
49
178
  /** A directory is being read: picking now would take the one before. */
50
179
  const [loading, setLoading] = React.useState(true);
180
+ const connecting = useConnecting(bench, loading ? host : undefined);
51
181
  const latest = React.useRef(0);
182
+ /** The machine whose folders are listed: a failure on another one leaves none listed. */
183
+ const listed = React.useRef<string | undefined>(undefined);
184
+ /** Where `This computer` goes: the directory of this machine shown last. */
185
+ const local = React.useRef(FolderPickerUtil.localStart(initial, bench?.state.server?.cwd));
52
186
  const go = React.useCallback(
53
187
  (next: string) => {
54
188
  const request = ++latest.current;
55
189
  setFailed(undefined);
56
190
  setLoading(true);
57
- list(next)
191
+ setHost(TaLocation.host(next));
192
+ setInput(TaLocation.path(next));
193
+ // A host's home directory is known once connected.
194
+ const resolved = bench && TaLocation.parse(next)?.path === '' ? bench.remote.resolveLocation(next) : Promise.resolve(next);
195
+ resolved
196
+ .then(async location => ({ location, entries: await list(location) }))
58
197
  .then(
59
- entries => {
198
+ ({ location, entries }) => {
60
199
  if (request === latest.current) {
61
- setPath(next);
62
- setInput(next);
200
+ setPath(location);
201
+ setInput(TaLocation.path(location));
63
202
  setFolders(FolderPickerUtil.folders(entries));
203
+ listed.current = TaLocation.host(location);
204
+ if (!TaLocation.isRemote(location)) {
205
+ local.current = location;
206
+ }
64
207
  }
65
208
  },
66
- () => request === latest.current && setFailed(next),
209
+ error => {
210
+ if (request !== latest.current) {
211
+ return;
212
+ }
213
+ const host = TaLocation.host(next);
214
+ if (host !== listed.current) {
215
+ setFolders(undefined);
216
+ }
217
+ setFailed(host && error instanceof Error ? error.message : t('ta.folder.failed', { path: next }));
218
+ },
67
219
  )
68
220
  .finally(() => request === latest.current && setLoading(false));
69
221
  },
70
- [list],
222
+ [bench, list, t],
71
223
  );
72
224
  React.useEffect(() => go(initial), [go, initial]);
225
+ React.useEffect(() => {
226
+ bench?.remote.listHosts().then(setHosts, () => undefined);
227
+ }, [bench]);
73
228
  const up = FolderPickerUtil.parent(path);
74
229
  const pick = () => {
75
230
  setBusy(true);
@@ -89,12 +244,13 @@ export function FolderPicker({ list, initial, onPick, onClose }: FolderPickerPro
89
244
  <span className="ta-modal-title">{t('ta.folder.title')}</span>
90
245
  <IconButton icon={X} label={t('ta.close')} onClick={onClose} />
91
246
  </div>
247
+ {bench && <Machines host={host} hosts={hosts} onPick={next => go(next ? TaLocation.ssh(next) : local.current)} />}
92
248
  <form
93
249
  className="ta-folder-bar"
94
250
  onSubmit={event => {
95
251
  event.preventDefault();
96
252
  if (input.trim()) {
97
- go(input.trim());
253
+ go(FolderPickerUtil.typed(input, host));
98
254
  }
99
255
  }}
100
256
  >
@@ -111,28 +267,41 @@ export function FolderPicker({ list, initial, onPick, onClose }: FolderPickerPro
111
267
  </label>
112
268
  </form>
113
269
  <ul className="ta-folder-list">
114
- {folders === undefined && !failed && (
270
+ {(folders === undefined || (loading && connecting)) && !failed && (
115
271
  <li className="ta-folder-note">
116
272
  <span className="ta-spinner" />
273
+ {connecting && <span className="ta-folder-step">{connecting}</span>}
117
274
  </li>
118
275
  )}
119
- {folders?.map(folder => (
120
- <li key={folder.name}>
121
- <button type="button" className="ta-folder-item" onClick={() => go(FolderPickerUtil.child(path, folder.name))}>
122
- <Icon icon={Folder} size={16} />
123
- <span className="ta-folder-name">{folder.name}</span>
124
- <Icon icon={ChevronRight} size={14} className="ta-folder-chevron" />
125
- </button>
276
+ {!(loading && connecting) &&
277
+ folders?.map(folder => (
278
+ <li key={folder.name}>
279
+ <button type="button" className="ta-folder-item" onClick={() => go(FolderPickerUtil.child(path, folder.name))}>
280
+ <Icon icon={Folder} size={16} />
281
+ <span className="ta-folder-name">{folder.name}</span>
282
+ <Icon icon={ChevronRight} size={14} className="ta-folder-chevron" />
283
+ </button>
284
+ </li>
285
+ ))}
286
+ {folders?.length === 0 && !connecting && <li className="ta-folder-note">{t('ta.folder.empty')}</li>}
287
+ {folders === undefined && failed && host && (
288
+ <li className="ta-folder-unreachable">
289
+ <strong>{t('ta.remote.failed', { host })}</strong>
290
+ <span>{failed}</span>
126
291
  </li>
127
- ))}
128
- {folders?.length === 0 && <li className="ta-folder-note">{t('ta.folder.empty')}</li>}
292
+ )}
129
293
  </ul>
130
- {failed && <div className="ta-folder-failed">{t('ta.folder.failed', { path: failed })}</div>}
294
+ {failed && !(folders === undefined && host) && <div className="ta-folder-failed">{failed}</div>}
131
295
  <div className="ta-dialog-actions">
132
296
  <button type="button" className="ta-button" onClick={onClose}>
133
297
  {t('ta.cancel')}
134
298
  </button>
135
- <button type="button" className="ta-button ta-primary" disabled={busy || loading || folders === undefined} onClick={pick}>
299
+ <button
300
+ type="button"
301
+ className="ta-button ta-primary"
302
+ disabled={busy || loading || folders === undefined || TaLocation.host(path) !== host}
303
+ onClick={pick}
304
+ >
136
305
  {t('ta.folder.open')}
137
306
  </button>
138
307
  </div>
@@ -142,29 +311,42 @@ export function FolderPicker({ list, initial, onPick, onClose }: FolderPickerPro
142
311
  );
143
312
  }
144
313
 
314
+ /** The name of a directory, with the host of one of another machine. */
315
+ export function DirectoryName({ cwd }: { cwd: string }): React.ReactElement {
316
+ const host = TaLocation.host(cwd);
317
+ return (
318
+ <>
319
+ <Icon icon={host ? Server : Folder} size={14} />
320
+ <span className="ta-ellipsis">{MessageUtil.basename(cwd) || cwd}</span>
321
+ {host && <span className="ta-host-tag">{host}</span>}
322
+ </>
323
+ );
324
+ }
325
+
145
326
  export interface DirectoryButtonProps {
146
327
  /** The directory now. */
147
328
  cwd: string;
148
329
  /** The entries of a directory of the server, the same function from one render to the next. */
149
330
  list(directory: string): Promise<TaDirEntry[]>;
150
331
  onPick(cwd: string): void | Promise<void>;
332
+ remote?: FolderPickerRemote;
151
333
  }
152
334
 
153
335
  /** The directory a new conversation will work in, beside the + of its message box: a click picks another. */
154
- export function DirectoryButton({ cwd, list, onPick }: DirectoryButtonProps): React.ReactElement {
336
+ export function DirectoryButton({ cwd, list, onPick, remote }: DirectoryButtonProps): React.ReactElement {
155
337
  const t = useT();
156
338
  const [picking, setPicking] = React.useState(false);
157
339
  return (
158
340
  <>
159
341
  <button type="button" className="ta-cwd-chip" title={t('ta.chat.cwdTitle', { path: cwd })} onClick={() => setPicking(true)}>
160
- <Icon icon={Folder} size={14} />
161
- <span className="ta-ellipsis">{MessageUtil.basename(cwd) || cwd}</span>
342
+ <DirectoryName cwd={cwd} />
162
343
  <Icon icon={ChevronDown} size={13} />
163
344
  </button>
164
345
  {picking && (
165
346
  <FolderPicker
166
347
  list={list}
167
348
  initial={cwd}
349
+ remote={remote}
168
350
  onClose={() => setPicking(false)}
169
351
  onPick={async path => {
170
352
  await onPick(path);
@@ -7,6 +7,7 @@ import { NoticesUtil } from '../notices/notices';
7
7
  import type { ConfirmRequest, InputRequest, Notice, TaNotices } from '../notices/notices-protocol';
8
8
  import { LEVEL_ICONS } from './callout';
9
9
  import { Icon, IconButton } from './icon';
10
+ import { TerminalArt } from './terminal-art';
10
11
 
11
12
  /** How long a notice takes to go, in style.ts. */
12
13
  const LEAVE_MS = 160;
@@ -53,7 +54,11 @@ function Toast({ notice, notices }: { notice: Notice; notices: TaNotices }) {
53
54
  }, [notices, notice.id]);
54
55
  const hold = useLifetime(notice, leave);
55
56
  return (
56
- <div className={`ta-toast ta-level-${notice.level}${leaving ? ' ta-leaving' : ''}`} role={notice.level === 'error' ? 'alert' : 'status'} {...hold}>
57
+ <div
58
+ className={`ta-toast ta-level-${notice.level}${notice.art ? ' ta-toast-with-art' : ''}${leaving ? ' ta-leaving' : ''}`}
59
+ role={notice.level === 'error' ? 'alert' : 'status'}
60
+ {...hold}
61
+ >
57
62
  <Icon icon={LEVEL_ICONS[notice.level]} size={16} className="ta-level-icon" />
58
63
  <div className="ta-toast-text">
59
64
  <div className="ta-toast-message">
@@ -61,6 +66,7 @@ function Toast({ notice, notices }: { notice: Notice; notices: TaNotices }) {
61
66
  {notice.count > 1 && <span className="ta-toast-count">×{notice.count}</span>}
62
67
  </div>
63
68
  {notice.detail && <div className="ta-toast-detail">{notice.detail}</div>}
69
+ {notice.art && <TerminalArt art={notice.art} className="ta-toast-art" label={t('ta.notice.picture')} />}
64
70
  {notice.action && (
65
71
  <button
66
72
  type="button"
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { PlusMenuUtil } from './composer-box';
3
+
4
+ describe('PlusMenuUtil', () => {
5
+ it('narrows a list to the entries with every word, in their label or detail', () => {
6
+ const items = [
7
+ { key: 'a', label: 'Claude-Opus-5.5', detail: 'codebuddy-code' },
8
+ { key: 'b', label: 'GPT-6-Sol', detail: 'codebuddy-code' },
9
+ { key: 'c', label: 'claude-opus-5-5', detail: 'claude-code' },
10
+ ];
11
+ expect(PlusMenuUtil.filter(items, '').map(item => item.key)).toEqual(['a', 'b', 'c']);
12
+ expect(PlusMenuUtil.filter(items, 'OPUS').map(item => item.key)).toEqual(['a', 'c']);
13
+ expect(PlusMenuUtil.filter(items, 'opus claude-code').map(item => item.key)).toEqual(['c']);
14
+ expect(PlusMenuUtil.filter(items, 'gemini')).toEqual([]);
15
+ });
16
+ });
@@ -0,0 +1,149 @@
1
+ import { type TaForwardedPort, TaLocation, type TaRemoteHost } from '@celljs/ta-protocol';
2
+ import { Plug, Plus, Server, Trash2, Unplug, X } from 'lucide-react';
3
+ import * as React from 'react';
4
+ import { useNotices, useWorkbench } from '../hooks';
5
+ import { useT } from '../i18n/use-t';
6
+ import { RemoteHostsUtil } from './folder-picker';
7
+ import { Icon, IconButton } from './icon';
8
+ import { Row } from './settings-controls';
9
+
10
+ /** The hosts and forwarded ports of TaRemoteServer, read again when a host connects or disconnects. */
11
+ function useRemote() {
12
+ const [workbench, bench] = useWorkbench();
13
+ const [hosts, setHosts] = React.useState<TaRemoteHost[]>([]);
14
+ const [ports, setPorts] = React.useState<TaForwardedPort[]>([]);
15
+ const read = React.useCallback(async () => {
16
+ const [listed, forwarded] = await Promise.all([bench.remote.listHosts(), bench.remote.listForwardedPorts()]);
17
+ setHosts(listed);
18
+ setPorts(forwarded);
19
+ }, [bench]);
20
+ React.useEffect(() => {
21
+ if (workbench.status !== 'online') {
22
+ return;
23
+ }
24
+ read().catch(() => undefined);
25
+ return bench.onRemoteEvent(event => event.type === 'connection' && void read().catch(() => undefined));
26
+ }, [workbench.status, bench, read]);
27
+ return { bench, hosts, ports, read };
28
+ }
29
+
30
+ /**
31
+ * The machines conversations can work on (`ssh://` directories): those of ~/.ssh/config and those added,
32
+ * whether Ta is connected to each, and the ports forwarded from them. Connecting checks a host the way a
33
+ * conversation would: ssh signs in, Ta installs what it needs there.
34
+ */
35
+ export function RemoteHosts() {
36
+ const t = useT();
37
+ const [, notices] = useNotices();
38
+ const { bench, hosts, ports, read } = useRemote();
39
+ const [text, setText] = React.useState('');
40
+ const [busy, setBusy] = React.useState<string>();
41
+ const run = async (host: string, action: () => Promise<unknown>) => {
42
+ setBusy(host);
43
+ try {
44
+ await action();
45
+ } catch (error) {
46
+ notices.error(error);
47
+ } finally {
48
+ setBusy(undefined);
49
+ await read().catch(() => undefined);
50
+ }
51
+ };
52
+ const add = () => {
53
+ const read = TaLocation.readHost(text);
54
+ if ('error' in read) {
55
+ notices.notify(RemoteHostsUtil.problem(read, text, t), { level: 'warning' });
56
+ return;
57
+ }
58
+ setText('');
59
+ void run(read.host, () => bench.remote.addHost(read.host));
60
+ };
61
+ return (
62
+ <>
63
+ <Row label={t('ta.remote.add')} description={t('ta.remote.description')} stacked>
64
+ <form
65
+ className="ta-trusted-add"
66
+ onSubmit={event => {
67
+ event.preventDefault();
68
+ add();
69
+ }}
70
+ >
71
+ <input
72
+ className="ta-input"
73
+ value={text}
74
+ placeholder={t('ta.remote.addHint')}
75
+ aria-label={t('ta.remote.add')}
76
+ spellCheck={false}
77
+ onChange={event => setText(event.target.value)}
78
+ />
79
+ <button type="submit" className="ta-button" disabled={!text.trim()}>
80
+ <Icon icon={Plus} size={14} /> {t('ta.remote.add')}
81
+ </button>
82
+ </form>
83
+ </Row>
84
+ {hosts.length === 0 && <Row label={t('ta.remote.none')} />}
85
+ {hosts.map(host => (
86
+ <Row
87
+ key={host.host}
88
+ label={host.host}
89
+ description={
90
+ <span className="ta-remote-state">
91
+ <span className={`ta-remote-light${host.connected ? ' ta-on' : ''}`} />
92
+ {[
93
+ t(host.connected ? 'ta.remote.connected' : 'ta.remote.disconnected'),
94
+ t(host.source === 'config' ? 'ta.remote.fromConfig' : 'ta.remote.added'),
95
+ ].join(' · ')}
96
+ </span>
97
+ }
98
+ >
99
+ {host.connected ? (
100
+ <button
101
+ type="button"
102
+ className="ta-button"
103
+ disabled={busy === host.host}
104
+ onClick={() => run(host.host, () => bench.remote.disconnect(host.host))}
105
+ >
106
+ <Icon icon={Unplug} size={14} /> {t('ta.remote.disconnect')}
107
+ </button>
108
+ ) : (
109
+ <button
110
+ type="button"
111
+ className="ta-button"
112
+ disabled={busy === host.host}
113
+ onClick={() =>
114
+ run(host.host, async () => {
115
+ await bench.remote.resolveLocation(TaLocation.ssh(host.host));
116
+ notices.notify(t('ta.remote.connectedTo', { host: host.host }), { level: 'success' });
117
+ })
118
+ }
119
+ >
120
+ {busy === host.host ? <span className="ta-spinner" /> : <Icon icon={Plug} size={14} />} {t('ta.remote.connect')}
121
+ </button>
122
+ )}
123
+ {host.source === 'recent' && (
124
+ <IconButton
125
+ icon={Trash2}
126
+ label={t('ta.remote.remove', { host: host.host })}
127
+ disabled={busy === host.host}
128
+ onClick={() => run(host.host, () => bench.remote.removeHost(host.host))}
129
+ />
130
+ )}
131
+ </Row>
132
+ ))}
133
+ {ports.map(port => (
134
+ <Row
135
+ key={`${port.host}:${port.remotePort}`}
136
+ label={t('ta.remote.port', { host: port.host, port: port.remotePort })}
137
+ description={`localhost:${port.localPort}`}
138
+ >
139
+ <Icon icon={Server} size={14} className="ta-muted" />
140
+ <IconButton
141
+ icon={X}
142
+ label={t('ta.remote.closePort')}
143
+ onClick={() => run(port.host, () => bench.remote.closeForwardedPort(port.host, port.remotePort))}
144
+ />
145
+ </Row>
146
+ ))}
147
+ </>
148
+ );
149
+ }