@celljs/ta-web 4.7.0 → 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 (272) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  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/app-view.js +10 -3
  13. package/esm/browser/components/app-view.js.map +1 -1
  14. package/esm/browser/components/composer-box.d.ts +6 -0
  15. package/esm/browser/components/composer-box.js +50 -13
  16. package/esm/browser/components/composer-box.js.map +1 -1
  17. package/esm/browser/components/composer.js +1 -1
  18. package/esm/browser/components/composer.js.map +1 -1
  19. package/esm/browser/components/dialog-view.d.ts +4 -1
  20. package/esm/browser/components/dialog-view.js +11 -7
  21. package/esm/browser/components/dialog-view.js.map +1 -1
  22. package/esm/browser/components/notice-center.js +3 -1
  23. package/esm/browser/components/notice-center.js.map +1 -1
  24. package/esm/browser/components/session-view.js +48 -4
  25. package/esm/browser/components/session-view.js.map +1 -1
  26. package/esm/browser/components/terminal-art.d.ts +7 -0
  27. package/esm/browser/components/terminal-art.js +12 -0
  28. package/esm/browser/components/terminal-art.js.map +1 -0
  29. package/esm/browser/components/tool-view.js +1 -1
  30. package/esm/browser/components/tool-view.js.map +1 -1
  31. package/esm/browser/components/web-composer.js +1 -1
  32. package/esm/browser/components/web-composer.js.map +1 -1
  33. package/esm/browser/extension/icons.js +4 -1
  34. package/esm/browser/extension/icons.js.map +1 -1
  35. package/esm/browser/extension/web-extension-api.d.ts +117 -1
  36. package/esm/browser/extension/web-extensions.d.ts +3 -0
  37. package/esm/browser/extension/web-extensions.js +22 -0
  38. package/esm/browser/extension/web-extensions.js.map +1 -1
  39. package/esm/browser/i18n/locales.d.ts +13 -1
  40. package/esm/browser/i18n/messages.d.ts +12 -0
  41. package/esm/browser/i18n/messages.js +24 -0
  42. package/esm/browser/i18n/messages.js.map +1 -1
  43. package/esm/browser/notices/notices-protocol.d.ts +3 -0
  44. package/esm/browser/notices/notices.d.ts +3 -0
  45. package/esm/browser/notices/notices.js +15 -0
  46. package/esm/browser/notices/notices.js.map +1 -1
  47. package/esm/browser/style/conversation.d.ts +1 -1
  48. package/esm/browser/style/conversation.js +16 -1
  49. package/esm/browser/style/conversation.js.map +1 -1
  50. package/esm/browser/style/layout.d.ts +1 -1
  51. package/esm/browser/style/layout.js +5 -0
  52. package/esm/browser/style/layout.js.map +1 -1
  53. package/esm/browser/style/shell.d.ts +1 -1
  54. package/esm/browser/style/shell.js +1 -0
  55. package/esm/browser/style/shell.js.map +1 -1
  56. package/esm/browser/terminal/canvas-font.d.ts +10 -0
  57. package/esm/browser/terminal/canvas-font.js +34 -0
  58. package/esm/browser/terminal/canvas-font.js.map +1 -0
  59. package/esm/browser/terminal/terminal-links.d.ts +18 -0
  60. package/esm/browser/terminal/terminal-links.js +72 -0
  61. package/esm/browser/terminal/terminal-links.js.map +1 -0
  62. package/esm/browser/terminal/terminal-theme.d.ts +8 -0
  63. package/esm/browser/terminal/terminal-theme.js +65 -0
  64. package/esm/browser/terminal/terminal-theme.js.map +1 -0
  65. package/esm/browser/terminal/web-terminal.d.ts +74 -0
  66. package/esm/browser/terminal/web-terminal.js +300 -0
  67. package/esm/browser/terminal/web-terminal.js.map +1 -0
  68. package/esm/browser/terminal/xterm.d.ts +5 -0
  69. package/esm/browser/terminal/xterm.js +9 -0
  70. package/esm/browser/terminal/xterm.js.map +1 -0
  71. package/esm/browser/util/desktop.d.ts +43 -0
  72. package/esm/browser/util/desktop.js +55 -0
  73. package/esm/browser/util/desktop.js.map +1 -0
  74. package/esm/browser/util/file-drag.d.ts +26 -0
  75. package/esm/browser/util/file-drag.js +73 -0
  76. package/esm/browser/util/file-drag.js.map +1 -0
  77. package/esm/browser/util/terminal-text.d.ts +33 -0
  78. package/esm/browser/util/terminal-text.js +209 -0
  79. package/esm/browser/util/terminal-text.js.map +1 -0
  80. package/esm/browser/util/tool.js +16 -0
  81. package/esm/browser/util/tool.js.map +1 -1
  82. package/lib/browser/chat/chat-protocol.d.ts +4 -0
  83. package/lib/browser/chat/chat-protocol.d.ts.map +1 -1
  84. package/lib/browser/chat/chat-view.d.ts.map +1 -1
  85. package/lib/browser/chat/chat-view.js +4 -1
  86. package/lib/browser/chat/chat-view.js.map +1 -1
  87. package/lib/browser/chat/chat.d.ts +8 -1
  88. package/lib/browser/chat/chat.d.ts.map +1 -1
  89. package/lib/browser/chat/chat.js +34 -15
  90. package/lib/browser/chat/chat.js.map +1 -1
  91. package/lib/browser/chat/chat.spec.js +30 -0
  92. package/lib/browser/chat/chat.spec.js.map +1 -1
  93. package/lib/browser/chat/draft-model.d.ts +16 -0
  94. package/lib/browser/chat/draft-model.d.ts.map +1 -0
  95. package/lib/browser/chat/draft-model.js +90 -0
  96. package/lib/browser/chat/draft-model.js.map +1 -0
  97. package/lib/browser/chat/draft-model.spec.d.ts +2 -0
  98. package/lib/browser/chat/draft-model.spec.d.ts.map +1 -0
  99. package/lib/browser/chat/draft-model.spec.js +31 -0
  100. package/lib/browser/chat/draft-model.spec.js.map +1 -0
  101. package/lib/browser/components/app-view.d.ts.map +1 -1
  102. package/lib/browser/components/app-view.js +10 -3
  103. package/lib/browser/components/app-view.js.map +1 -1
  104. package/lib/browser/components/composer-box.d.ts +6 -0
  105. package/lib/browser/components/composer-box.d.ts.map +1 -1
  106. package/lib/browser/components/composer-box.js +51 -14
  107. package/lib/browser/components/composer-box.js.map +1 -1
  108. package/lib/browser/components/composer.d.ts.map +1 -1
  109. package/lib/browser/components/composer.js +1 -1
  110. package/lib/browser/components/composer.js.map +1 -1
  111. package/lib/browser/components/dialog-view.d.ts +4 -1
  112. package/lib/browser/components/dialog-view.d.ts.map +1 -1
  113. package/lib/browser/components/dialog-view.js +11 -7
  114. package/lib/browser/components/dialog-view.js.map +1 -1
  115. package/lib/browser/components/notice-center.d.ts.map +1 -1
  116. package/lib/browser/components/notice-center.js +3 -1
  117. package/lib/browser/components/notice-center.js.map +1 -1
  118. package/lib/browser/components/plus-menu.spec.d.ts +2 -0
  119. package/lib/browser/components/plus-menu.spec.d.ts.map +1 -0
  120. package/lib/browser/components/plus-menu.spec.js +18 -0
  121. package/lib/browser/components/plus-menu.spec.js.map +1 -0
  122. package/lib/browser/components/session-view.d.ts.map +1 -1
  123. package/lib/browser/components/session-view.js +48 -4
  124. package/lib/browser/components/session-view.js.map +1 -1
  125. package/lib/browser/components/terminal-art.d.ts +8 -0
  126. package/lib/browser/components/terminal-art.d.ts.map +1 -0
  127. package/lib/browser/components/terminal-art.js +48 -0
  128. package/lib/browser/components/terminal-art.js.map +1 -0
  129. package/lib/browser/components/tool-view.js +1 -1
  130. package/lib/browser/components/tool-view.js.map +1 -1
  131. package/lib/browser/components/web-composer.d.ts.map +1 -1
  132. package/lib/browser/components/web-composer.js +1 -1
  133. package/lib/browser/components/web-composer.js.map +1 -1
  134. package/lib/browser/extension/icons.d.ts.map +1 -1
  135. package/lib/browser/extension/icons.js +3 -0
  136. package/lib/browser/extension/icons.js.map +1 -1
  137. package/lib/browser/extension/web-extension-api.d.ts +117 -1
  138. package/lib/browser/extension/web-extension-api.d.ts.map +1 -1
  139. package/lib/browser/extension/web-extensions.d.ts +3 -0
  140. package/lib/browser/extension/web-extensions.d.ts.map +1 -1
  141. package/lib/browser/extension/web-extensions.js +22 -0
  142. package/lib/browser/extension/web-extensions.js.map +1 -1
  143. package/lib/browser/i18n/locales.d.ts +13 -1
  144. package/lib/browser/i18n/locales.d.ts.map +1 -1
  145. package/lib/browser/i18n/messages.d.ts +12 -0
  146. package/lib/browser/i18n/messages.d.ts.map +1 -1
  147. package/lib/browser/i18n/messages.js +24 -0
  148. package/lib/browser/i18n/messages.js.map +1 -1
  149. package/lib/browser/notices/notices-protocol.d.ts +3 -0
  150. package/lib/browser/notices/notices-protocol.d.ts.map +1 -1
  151. package/lib/browser/notices/notices.d.ts +3 -0
  152. package/lib/browser/notices/notices.d.ts.map +1 -1
  153. package/lib/browser/notices/notices.js +15 -0
  154. package/lib/browser/notices/notices.js.map +1 -1
  155. package/lib/browser/notices/notices.spec.js +7 -2
  156. package/lib/browser/notices/notices.spec.js.map +1 -1
  157. package/lib/browser/style/conversation.d.ts +1 -1
  158. package/lib/browser/style/conversation.d.ts.map +1 -1
  159. package/lib/browser/style/conversation.js +16 -1
  160. package/lib/browser/style/conversation.js.map +1 -1
  161. package/lib/browser/style/layout.d.ts +1 -1
  162. package/lib/browser/style/layout.d.ts.map +1 -1
  163. package/lib/browser/style/layout.js +5 -0
  164. package/lib/browser/style/layout.js.map +1 -1
  165. package/lib/browser/style/shell.d.ts +1 -1
  166. package/lib/browser/style/shell.d.ts.map +1 -1
  167. package/lib/browser/style/shell.js +1 -0
  168. package/lib/browser/style/shell.js.map +1 -1
  169. package/lib/browser/terminal/canvas-font.d.ts +11 -0
  170. package/lib/browser/terminal/canvas-font.d.ts.map +1 -0
  171. package/lib/browser/terminal/canvas-font.js +37 -0
  172. package/lib/browser/terminal/canvas-font.js.map +1 -0
  173. package/lib/browser/terminal/canvas-font.spec.d.ts +2 -0
  174. package/lib/browser/terminal/canvas-font.spec.d.ts.map +1 -0
  175. package/lib/browser/terminal/canvas-font.spec.js +32 -0
  176. package/lib/browser/terminal/canvas-font.spec.js.map +1 -0
  177. package/lib/browser/terminal/terminal-links.d.ts +19 -0
  178. package/lib/browser/terminal/terminal-links.d.ts.map +1 -0
  179. package/lib/browser/terminal/terminal-links.js +75 -0
  180. package/lib/browser/terminal/terminal-links.js.map +1 -0
  181. package/lib/browser/terminal/terminal-links.spec.d.ts +2 -0
  182. package/lib/browser/terminal/terminal-links.spec.d.ts.map +1 -0
  183. package/lib/browser/terminal/terminal-links.spec.js +30 -0
  184. package/lib/browser/terminal/terminal-links.spec.js.map +1 -0
  185. package/lib/browser/terminal/terminal-theme.d.ts +9 -0
  186. package/lib/browser/terminal/terminal-theme.d.ts.map +1 -0
  187. package/lib/browser/terminal/terminal-theme.js +68 -0
  188. package/lib/browser/terminal/terminal-theme.js.map +1 -0
  189. package/lib/browser/terminal/terminal-theme.spec.d.ts +2 -0
  190. package/lib/browser/terminal/terminal-theme.spec.d.ts.map +1 -0
  191. package/lib/browser/terminal/terminal-theme.spec.js +24 -0
  192. package/lib/browser/terminal/terminal-theme.spec.js.map +1 -0
  193. package/lib/browser/terminal/web-terminal.d.ts +75 -0
  194. package/lib/browser/terminal/web-terminal.d.ts.map +1 -0
  195. package/lib/browser/terminal/web-terminal.js +304 -0
  196. package/lib/browser/terminal/web-terminal.js.map +1 -0
  197. package/lib/browser/terminal/xterm.d.ts +6 -0
  198. package/lib/browser/terminal/xterm.d.ts.map +1 -0
  199. package/lib/browser/terminal/xterm.js +16 -0
  200. package/lib/browser/terminal/xterm.js.map +1 -0
  201. package/lib/browser/util/desktop.d.ts +44 -0
  202. package/lib/browser/util/desktop.d.ts.map +1 -0
  203. package/lib/browser/util/desktop.js +58 -0
  204. package/lib/browser/util/desktop.js.map +1 -0
  205. package/lib/browser/util/desktop.spec.d.ts +2 -0
  206. package/lib/browser/util/desktop.spec.d.ts.map +1 -0
  207. package/lib/browser/util/desktop.spec.js +67 -0
  208. package/lib/browser/util/desktop.spec.js.map +1 -0
  209. package/lib/browser/util/file-drag.d.ts +27 -0
  210. package/lib/browser/util/file-drag.d.ts.map +1 -0
  211. package/lib/browser/util/file-drag.js +76 -0
  212. package/lib/browser/util/file-drag.js.map +1 -0
  213. package/lib/browser/util/file-drag.spec.d.ts +2 -0
  214. package/lib/browser/util/file-drag.spec.d.ts.map +1 -0
  215. package/lib/browser/util/file-drag.spec.js +46 -0
  216. package/lib/browser/util/file-drag.spec.js.map +1 -0
  217. package/lib/browser/util/terminal-text.d.ts +34 -0
  218. package/lib/browser/util/terminal-text.d.ts.map +1 -0
  219. package/lib/browser/util/terminal-text.js +212 -0
  220. package/lib/browser/util/terminal-text.js.map +1 -0
  221. package/lib/browser/util/terminal-text.spec.d.ts +2 -0
  222. package/lib/browser/util/terminal-text.spec.d.ts.map +1 -0
  223. package/lib/browser/util/terminal-text.spec.js +54 -0
  224. package/lib/browser/util/terminal-text.spec.js.map +1 -0
  225. package/lib/browser/util/tool.d.ts.map +1 -1
  226. package/lib/browser/util/tool.js +16 -0
  227. package/lib/browser/util/tool.js.map +1 -1
  228. package/lib/browser/util/tool.spec.js +12 -0
  229. package/lib/browser/util/tool.spec.js.map +1 -1
  230. package/package.json +11 -7
  231. package/src/browser/chat/chat-protocol.ts +4 -0
  232. package/src/browser/chat/chat-view.tsx +4 -0
  233. package/src/browser/chat/chat.spec.ts +31 -0
  234. package/src/browser/chat/chat.ts +36 -14
  235. package/src/browser/chat/draft-model.spec.tsx +31 -0
  236. package/src/browser/chat/draft-model.tsx +63 -0
  237. package/src/browser/components/app-view.tsx +10 -3
  238. package/src/browser/components/composer-box.tsx +52 -6
  239. package/src/browser/components/composer.tsx +1 -0
  240. package/src/browser/components/dialog-view.tsx +16 -7
  241. package/src/browser/components/notice-center.tsx +7 -1
  242. package/src/browser/components/plus-menu.spec.ts +16 -0
  243. package/src/browser/components/session-view.tsx +65 -4
  244. package/src/browser/components/terminal-art.tsx +21 -0
  245. package/src/browser/components/tool-view.tsx +1 -1
  246. package/src/browser/components/web-composer.tsx +1 -0
  247. package/src/browser/extension/icons.ts +6 -0
  248. package/src/browser/extension/web-extension-api.ts +131 -1
  249. package/src/browser/extension/web-extensions.ts +36 -1
  250. package/src/browser/i18n/messages.ts +24 -0
  251. package/src/browser/notices/notices-protocol.ts +3 -0
  252. package/src/browser/notices/notices.spec.ts +8 -2
  253. package/src/browser/notices/notices.ts +17 -1
  254. package/src/browser/style/conversation.ts +16 -1
  255. package/src/browser/style/layout.ts +5 -0
  256. package/src/browser/style/shell.ts +1 -0
  257. package/src/browser/terminal/canvas-font.spec.ts +29 -0
  258. package/src/browser/terminal/canvas-font.ts +32 -0
  259. package/src/browser/terminal/terminal-links.spec.ts +32 -0
  260. package/src/browser/terminal/terminal-links.ts +75 -0
  261. package/src/browser/terminal/terminal-theme.spec.ts +23 -0
  262. package/src/browser/terminal/terminal-theme.ts +66 -0
  263. package/src/browser/terminal/web-terminal.ts +330 -0
  264. package/src/browser/terminal/xterm.ts +10 -0
  265. package/src/browser/util/desktop.spec.ts +68 -0
  266. package/src/browser/util/desktop.ts +96 -0
  267. package/src/browser/util/file-drag.spec.ts +48 -0
  268. package/src/browser/util/file-drag.ts +78 -0
  269. package/src/browser/util/terminal-text.spec.ts +60 -0
  270. package/src/browser/util/terminal-text.ts +223 -0
  271. package/src/browser/util/tool.spec.ts +12 -0
  272. package/src/browser/util/tool.ts +17 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celljs/ta-web",
3
- "version": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "description": "The web client of ta serve: sessions, streaming messages, tool runs with diffs, and dialogs",
5
5
  "keywords": [
6
6
  "cell-component"
@@ -31,12 +31,16 @@
31
31
  "watch": "tsc -b -w --preserveWatchOutput"
32
32
  },
33
33
  "dependencies": {
34
- "@celljs/core": "4.7.0",
35
- "@celljs/react": "4.7.0",
36
- "@celljs/ta-client": "4.7.0",
37
- "@celljs/ta-protocol": "4.7.0",
38
- "@celljs/ta-relay": "4.7.0",
39
- "@celljs/widget": "4.7.0",
34
+ "@celljs/core": "4.8.0",
35
+ "@celljs/react": "4.8.0",
36
+ "@celljs/ta-client": "4.8.0",
37
+ "@celljs/ta-protocol": "4.8.0",
38
+ "@celljs/ta-relay": "4.8.0",
39
+ "@celljs/widget": "4.8.0",
40
+ "@xterm/addon-fit": "^0.11.0",
41
+ "@xterm/addon-unicode11": "^0.9.0",
42
+ "@xterm/addon-webgl": "^0.19.0",
43
+ "@xterm/xterm": "^6.0.0",
40
44
  "katex": "^0.18.11",
41
45
  "lucide-react": "^1.70.0",
42
46
  "mermaid": "^12.3.0",
@@ -7,6 +7,8 @@ export const TaChat = Symbol('TaChat');
7
7
  export interface ChatDraft {
8
8
  /** Where it will work. */
9
9
  cwd: string;
10
+ /** `<provider>/<model id>` it will start on; none: the default model of the settings. */
11
+ model?: string;
10
12
  }
11
13
 
12
14
  export interface ChatState {
@@ -34,6 +36,8 @@ export interface TaChat {
34
36
  draft(): Promise<void>;
35
37
  /** Moves the new conversation to another directory; the next new ones start there too. */
36
38
  setDraftCwd(cwd: string): void;
39
+ /** The model the new conversation starts on; none: the default one. */
40
+ setDraftModel(model: string | undefined): void;
37
41
  /** Opens the session of the new conversation and sends its first message; fails as the opening fails. */
38
42
  submit(message: string, images?: ImageContent[]): Promise<void>;
39
43
  show(sessionId: string): Promise<void>;
@@ -15,6 +15,7 @@ import { useNotices, useWorkbench } from '../hooks';
15
15
  import { useT } from '../i18n/use-t';
16
16
  import { MessageUtil } from '../util/message';
17
17
  import { type ChatDraft, type ChatState, TaChat } from './chat-protocol';
18
+ import { useDraftModel } from './draft-model';
18
19
 
19
20
  const SUGGESTIONS: HeroSuggestion[] = [
20
21
  { picture: 'card', label: 'ta.chat.suggest.1.label', text: 'ta.chat.suggest.1' },
@@ -69,6 +70,8 @@ function DraftView({ draft, menu, aboveComposer, belowComposer }: DraftViewProps
69
70
  const fail = (cause: unknown) => notices.error(cause);
70
71
  const submit = (message: string, images?: ImageContent[]) => chat.submit(message, images);
71
72
  const list = React.useCallback((directory: string) => workbench.workspace.listDirectory('.', directory), [workbench]);
73
+ const pickModel = React.useCallback((model: string | undefined) => chat.setDraftModel(model), [chat]);
74
+ const model = useDraftModel(workbench.settings, draft, pickModel, t);
72
75
  return (
73
76
  <section className="ta-session">
74
77
  <header className="ta-session-header">
@@ -93,6 +96,7 @@ function DraftView({ draft, menu, aboveComposer, belowComposer }: DraftViewProps
93
96
  cwd={draft.cwd || undefined}
94
97
  onSubmit={submit}
95
98
  onError={fail}
99
+ actions={model ? [[model]] : undefined}
96
100
  beside={<DirectoryButton cwd={draft.cwd} list={list} remote={workbench} onPick={cwd => chat.setDraftCwd(cwd)} />}
97
101
  />
98
102
  {belowComposer}
@@ -137,6 +137,37 @@ describe('TaChatImpl', () => {
137
137
  expect(env.chat.state.store?.sessionId).toBe('s1');
138
138
  });
139
139
 
140
+ it('does not open again from its file a conversation of a package that is closed: its package opens it with its role', async () => {
141
+ await (env.chat as any).workbench.server.openSession({ cwd: '/agent' });
142
+ env.sessions[0].group = '@celljs/ta-channels';
143
+ await env.chat.start();
144
+ await env.chat.show('s1');
145
+ expect(env.storage.get('ta.chat.grouped')).toBe(true as unknown as string);
146
+ await env.chat.close('s1');
147
+ const again = setup();
148
+ again.storage.set('ta.chat.file', env.storage.get('ta.chat.file') as string);
149
+ again.storage.set('ta.chat.grouped', true as unknown as string);
150
+ await again.chat.start();
151
+ expect(again.opened).toEqual([]);
152
+ expect(again.chat.state.draft).toEqual({ cwd: '/home' });
153
+ });
154
+
155
+ it('keeps the conversation shown until the next is loaded, and of quick switches shows only the last', async () => {
156
+ const server = (env.chat as any).workbench.server;
157
+ await server.openSession({ cwd: '/a' });
158
+ await server.openSession({ cwd: '/b' });
159
+ await server.openSession({ cwd: '/c' });
160
+ await env.chat.start();
161
+ await env.chat.show('s1');
162
+ const shown: (string | undefined)[] = [];
163
+ env.chat.subscribe(state => shown.push(state.store?.sessionId));
164
+ await Promise.all([env.chat.show('s2'), env.chat.show('s3')]);
165
+ expect(env.chat.state.store?.sessionId).toBe('s3');
166
+ expect(env.stores.map(store => store.sessionId)).toEqual(['s1', 's3']);
167
+ expect(shown.filter(id => id !== 's3')).toEqual([undefined]);
168
+ expect(env.extensions.connected.filter(Boolean).at(-1)).toEqual({ cwd: '/c', sessionId: 's3' });
169
+ });
170
+
140
171
  it('opens a new conversation in the directory of the last one, and shows a new one when the last open closes', async () => {
141
172
  await env.chat.start();
142
173
  env.chat.setDraftCwd('/work');
@@ -10,8 +10,12 @@ import { type ChatState, TaChat } from './chat-protocol';
10
10
 
11
11
  /** The file of the conversation shown last, in any browser: the page opens it again. */
12
12
  const FILE: StateSpec<string> = { key: 'chat.file', tier: 'user', initial: '', parse: StateUtil.string };
13
+ /** That conversation is of a package's group: the page shows it again only while it is open, as its package opens it with a role of its own. */
14
+ const GROUPED: StateSpec<boolean> = { key: 'chat.grouped', tier: 'user', initial: false, parse: StateUtil.boolean };
13
15
  /** The directory of the last new conversation. */
14
16
  const CWD: StateSpec<string> = { key: 'chat.cwd', tier: 'user', initial: '', parse: StateUtil.string };
17
+ /** The model the user picked for the last new conversation. */
18
+ const MODEL: StateSpec<string> = { key: 'chat.model', tier: 'user', initial: '', parse: StateUtil.string };
15
19
  /** A file in a directory of packages: `extensions/<name>/...`, of the user or a project. */
16
20
  const PACKAGE_FILE = /(^|[\\/])extensions[\\/][^\\/]+[\\/]/;
17
21
 
@@ -62,6 +66,8 @@ export class TaChatImpl implements TaChat {
62
66
  protected watching = false;
63
67
  /** A web extension changed packages during a run: its end loads them again. */
64
68
  protected reloadPending = false;
69
+ /** The conversation asked for last: of quick switches, only the last is shown. */
70
+ protected wanted?: string;
65
71
 
66
72
  get state(): ChatState {
67
73
  return this.current;
@@ -86,7 +92,8 @@ export class TaChatImpl implements TaChat {
86
92
  await this.workbench.refresh();
87
93
  const file = (await this.persisted.load(FILE)) || undefined;
88
94
  const open = file && this.workbench.state.sessions.find(session => session.sessionFile === file);
89
- const sessionId = open ? open.sessionId : file ? await this.open({ sessionFile: file }).catch(() => undefined) : undefined;
95
+ const reopen = file && !(await this.persisted.load(GROUPED));
96
+ const sessionId = open ? open.sessionId : reopen ? await this.open({ sessionFile: file }).catch(() => undefined) : undefined;
90
97
  if (sessionId) {
91
98
  await this.attach(sessionId);
92
99
  } else {
@@ -107,18 +114,25 @@ export class TaChatImpl implements TaChat {
107
114
  setDraftCwd(cwd: string): void {
108
115
  if (this.current.draft) {
109
116
  this.persisted.set(CWD, cwd);
110
- this.update({ draft: { cwd } });
117
+ this.update({ draft: { ...this.current.draft, cwd } });
111
118
  this.extensions.connect({ cwd });
112
119
  }
113
120
  }
114
121
 
122
+ setDraftModel(model: string | undefined): void {
123
+ if (this.current.draft && this.current.draft.model !== model) {
124
+ this.persisted.set(MODEL, model ?? '');
125
+ this.update({ draft: { ...this.current.draft, model } });
126
+ }
127
+ }
128
+
115
129
  submit(message: string, images?: ImageContent[]): Promise<void> {
116
130
  const next = this.queue.then(async () => {
117
131
  const { draft } = this.current;
118
132
  if (!draft) {
119
133
  return;
120
134
  }
121
- const sessionId = await this.open({ cwd: draft.cwd });
135
+ const sessionId = await this.open({ cwd: draft.cwd, model: draft.model });
122
136
  if (draft.cwd) {
123
137
  this.persisted.set(CWD, draft.cwd);
124
138
  }
@@ -130,8 +144,9 @@ export class TaChatImpl implements TaChat {
130
144
  }
131
145
 
132
146
  show(sessionId: string): Promise<void> {
147
+ this.wanted = sessionId;
133
148
  return this.enqueue(async () => {
134
- if (this.current.store?.sessionId !== sessionId) {
149
+ if (this.wanted === sessionId && this.current.store?.sessionId !== sessionId) {
135
150
  await this.attach(sessionId);
136
151
  }
137
152
  });
@@ -179,18 +194,24 @@ export class TaChatImpl implements TaChat {
179
194
  }
180
195
 
181
196
  /** Follows a session, running the web tools it asks for. */
197
+ /** The conversation shown stays until the next one is loaded, so that switching does not flash an empty page. */
182
198
  protected async attach(sessionId: string): Promise<void> {
183
- this.leave();
184
199
  const store = this.workbench.createStore(sessionId, { clientTools: this.extensions.state.tools.map(tool => this.clientTool(tool.name)) });
185
- await store.attach();
200
+ try {
201
+ await store.attach();
202
+ // The list has the sessions opened since, as those of agents: web extensions get the directory of this one.
203
+ await this.workbench.refresh();
204
+ } catch (error) {
205
+ store.detach().catch(() => undefined);
206
+ throw error;
207
+ }
208
+ this.leave();
186
209
  this.seenEdits.clear();
187
210
  for (const id of Object.keys(store.state.tools)) {
188
211
  this.seenEdits.add(id);
189
212
  }
190
- // The list has the sessions opened since, as those of agents: web extensions get the directory of this one.
191
- await this.workbench.refresh();
192
213
  this.follow(store);
193
- this.remember(this.workbench.state.sessions.find(session => session.sessionId === sessionId)?.sessionFile);
214
+ this.remember(this.workbench.state.sessions.find(session => session.sessionId === sessionId));
194
215
  }
195
216
 
196
217
  /** The newest other open session, or a new conversation. */
@@ -207,7 +228,7 @@ export class TaChatImpl implements TaChat {
207
228
 
208
229
  protected showDraft(): void {
209
230
  const cwd = this.persisted.get(CWD) || this.workbench.state.server?.cwd || '';
210
- this.update({ draft: { cwd }, error: undefined });
231
+ this.update({ draft: { cwd, model: this.persisted.get(MODEL) || undefined }, error: undefined });
211
232
  this.extensions.connect(cwd ? { cwd } : undefined);
212
233
  }
213
234
 
@@ -261,7 +282,7 @@ export class TaChatImpl implements TaChat {
261
282
 
262
283
  protected onEvent(event: TaEvent): void {
263
284
  if (event.type === 'session_replaced') {
264
- this.remember(event.info.sessionFile);
285
+ this.remember(event.info);
265
286
  }
266
287
  this.extensions.emit(event);
267
288
  }
@@ -320,9 +341,10 @@ export class TaChatImpl implements TaChat {
320
341
  await this.attach(await this.open({ sessionFile, cwd }));
321
342
  }
322
343
 
323
- protected remember(sessionFile: string | undefined): void {
324
- if (sessionFile) {
325
- this.persisted.set(FILE, sessionFile);
344
+ protected remember(info: { sessionFile?: string; group?: string } | undefined): void {
345
+ if (info?.sessionFile) {
346
+ this.persisted.set(FILE, info.sessionFile);
347
+ this.persisted.set(GROUPED, !!info.group);
326
348
  }
327
349
  }
328
350
 
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import type { TaSettingsSnapshot } from '@celljs/ta-protocol';
3
+ import { DraftModelUtil } from './draft-model';
4
+
5
+ const t = ((id: string) => id) as never;
6
+
7
+ function snapshot(fields: Partial<TaSettingsSnapshot>): TaSettingsSnapshot {
8
+ return { user: {}, project: {}, cwd: '/w', userFile: 'u', projectFile: 'p', projectTrusted: true, models: [], ...fields };
9
+ }
10
+
11
+ describe('DraftModelUtil', () => {
12
+ it('lists the models by name, the picked one checked, else the one of the settings', () => {
13
+ const picked: string[] = [];
14
+ const settings = snapshot({
15
+ models: ['anthropic/claude-x', 'codebuddy-code/default', 'codebuddy-code/opus'],
16
+ modelNames: { 'codebuddy-code/default': 'CodeBuddy Code', 'codebuddy-code/opus': 'Opus' },
17
+ user: { model: 'anthropic/claude-x' },
18
+ });
19
+ const action = DraftModelUtil.action(settings, undefined, model => picked.push(model), t);
20
+ expect(action.detail).toBe('claude-x');
21
+ expect(action.items?.map(item => [item.label, item.detail, !!item.checked])).toEqual([
22
+ ['claude-x', 'anthropic', true],
23
+ ['CodeBuddy Code', 'codebuddy-code', false],
24
+ ['Opus', 'codebuddy-code', false],
25
+ ]);
26
+ action.items?.[2].run?.();
27
+ expect(picked).toEqual(['codebuddy-code/opus']);
28
+ expect(DraftModelUtil.action(settings, 'codebuddy-code/default', () => undefined, t).detail).toBe('CodeBuddy Code');
29
+ expect(DraftModelUtil.settingsModel(snapshot({ user: { model: 'a/b' }, project: { model: 'c/d' } }))).toBe('c/d');
30
+ });
31
+ });
@@ -0,0 +1,63 @@
1
+ import type { TaSettingsServer, TaSettingsSnapshot } from '@celljs/ta-protocol';
2
+ import { Box } from 'lucide-react';
3
+ import * as React from 'react';
4
+ import type { ComposerAction } from '../components/composer-box';
5
+ import { Icon } from '../components/icon';
6
+ import type { Translate } from '../i18n/use-t';
7
+ import type { ChatDraft } from './chat-protocol';
8
+
9
+ export namespace DraftModelUtil {
10
+ /** The model the settings start a conversation in `snapshot`'s directory on: the project's, else the user's. */
11
+ export function settingsModel(snapshot: TaSettingsSnapshot): string | undefined {
12
+ const model = snapshot.project.model ?? snapshot.user.model;
13
+ return typeof model === 'string' && model ? model : undefined;
14
+ }
15
+
16
+ /** The model entry of the + of a new conversation: the models with credentials, the picked one checked. */
17
+ export function action(snapshot: TaSettingsSnapshot, picked: string | undefined, pick: (model: string) => void, t: Translate): ComposerAction {
18
+ const current = picked ?? settingsModel(snapshot);
19
+ const name = (ref: string) => snapshot.modelNames?.[ref] ?? ref.slice(ref.indexOf('/') + 1);
20
+ return {
21
+ key: 'model',
22
+ label: t('ta.session.model'),
23
+ icon: <Icon icon={Box} size={16} />,
24
+ detail: current ? name(current) : t('ta.session.noModel'),
25
+ items: snapshot.models.map(ref => ({
26
+ key: ref,
27
+ label: name(ref),
28
+ detail: ref.slice(0, ref.indexOf('/')),
29
+ checked: ref === current,
30
+ run: () => pick(ref),
31
+ })),
32
+ };
33
+ }
34
+ }
35
+
36
+ /**
37
+ * The model of a new conversation in its + menu: the models of the settings of its directory, those
38
+ * of pi packages included. A model picked before that is gone is forgotten.
39
+ */
40
+ export function useDraftModel(
41
+ settings: TaSettingsServer,
42
+ draft: ChatDraft,
43
+ pick: (model: string | undefined) => void,
44
+ t: Translate,
45
+ ): ComposerAction | undefined {
46
+ const [snapshot, setSnapshot] = React.useState<TaSettingsSnapshot>();
47
+ React.useEffect(() => {
48
+ let live = true;
49
+ settings.getSettings(draft.cwd || undefined).then(
50
+ next => live && setSnapshot(next),
51
+ () => undefined,
52
+ );
53
+ return () => {
54
+ live = false;
55
+ };
56
+ }, [settings, draft.cwd]);
57
+ React.useEffect(() => {
58
+ if (snapshot && draft.model && !snapshot.models.includes(draft.model)) {
59
+ pick(undefined);
60
+ }
61
+ }, [snapshot, draft.model, pick]);
62
+ return snapshot?.models.length ? DraftModelUtil.action(snapshot, draft.model, pick, t) : undefined;
63
+ }
@@ -288,7 +288,7 @@ export function AppView() {
288
288
  const closeSettings = React.useCallback(() => setSettings(undefined), []);
289
289
  const [rightOpen, toggleRight, revealRight] = useRight();
290
290
  const [maximized, setMaximized] = React.useState(false);
291
- const [{ theme }, settingsStore] = useSettings();
291
+ const [{ theme, language }, settingsStore] = useSettings();
292
292
  const sidebar = usePane(SIDEBAR);
293
293
  const editor = usePane(EDITOR);
294
294
  const online = workbench.status === 'online';
@@ -297,6 +297,12 @@ export function AppView() {
297
297
  const openLink = useOpenLink(undefined);
298
298
  useDocument();
299
299
  useRefresh(online);
300
+ // What the server and its packages say (errors, questions) is in the language of the page.
301
+ React.useEffect(() => {
302
+ if (online) {
303
+ bench.settings.setLanguage(language).catch(() => undefined);
304
+ }
305
+ }, [online, language, bench]);
300
306
  React.useEffect(() => {
301
307
  void bench.start();
302
308
  }, [bench]);
@@ -334,14 +340,15 @@ export function AppView() {
334
340
  // biome-ignore lint/correctness/useExhaustiveDependencies: follows what the main area shows
335
341
  React.useEffect(() => setSettings(undefined), [page, shownSession]);
336
342
  // Another conversation shown that no sidebar view lists (not that of an agent, which is in a group), by a page,
337
- // a link or a fork: the sidebar turns to the chats, where it is selected, to be found again. Not the one the page opens with.
343
+ // a link or a fork: the sidebar turns to the chats, where it is selected, to be found again. Not the one the page opens with,
344
+ // nor one the list does not have yet: it may be an agent's.
338
345
  // biome-ignore lint/correctness/useExhaustiveDependencies: follows the conversation shown
339
346
  React.useEffect(() => {
340
347
  if (!shownSession || !started.current) {
341
348
  return;
342
349
  }
343
350
  const info = workbench.sessions.find(session => session.sessionId === shownSession);
344
- if (!info?.group) {
351
+ if (info && !info.group) {
345
352
  setSideId('');
346
353
  }
347
354
  }, [shownSession]);
@@ -2,6 +2,7 @@ import { ArrowUp, AtSign, Check, ChevronLeft, ChevronRight, ImagePlus, Plus, Sla
2
2
  import * as React from 'react';
3
3
  import { useT } from '../i18n/use-t';
4
4
  import { type ComposerAttachment, type ComposerImage, ComposerUtil, IMAGE_TYPES } from '../util/composer';
5
+ import { FileDragUtil } from '../util/file-drag';
5
6
  import { ComposerMenu, type ComposerMenuItem } from './composer-menu';
6
7
  import { Icon, IconButton } from './icon';
7
8
 
@@ -59,6 +60,8 @@ export interface ComposerBoxProps {
59
60
  /** Controls on the right, before the send button, as the microphone of the double. */
60
61
  after?: React.ReactNode;
61
62
  autoFocus?: boolean;
63
+ /** The directory of the conversation: files dragged in from it become `@` references relative to it. */
64
+ cwd?: string;
62
65
  onError(error: unknown): void;
63
66
  }
64
67
 
@@ -102,13 +105,31 @@ export function Attachments({ attachments, onRemove }: { attachments: ComposerAt
102
105
  );
103
106
  }
104
107
 
105
- /** The + on the left of the box: what is not typing, in one place. A list opens in place of the menu. */
108
+ /** A list of more entries than this gets a box that narrows it. */
109
+ const SEARCH_FROM = 8;
110
+
111
+ export namespace PlusMenuUtil {
112
+ /** The entries whose label or detail has every word of `query`, in any case. */
113
+ export function filter(items: ComposerAction[], query: string): ComposerAction[] {
114
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
115
+ return words.length ? items.filter(item => words.every(word => `${item.label} ${item.detail ?? ''}`.toLowerCase().includes(word))) : items;
116
+ }
117
+ }
118
+
119
+ /** The + on the left of the box: what is not typing, in one place. A list opens in place of the menu, with a box to narrow a long one. */
106
120
  function PlusMenu({ sections, onClose }: { sections: ComposerAction[][]; onClose(): void }) {
121
+ const t = useT();
107
122
  const [open, setOpen] = React.useState<ComposerAction>();
108
- const shown = open?.items ? [open.items] : sections;
123
+ const [query, setQuery] = React.useState('');
124
+ const searchable = (open?.items?.length ?? 0) > SEARCH_FROM;
125
+ const shown = open?.items ? [searchable ? PlusMenuUtil.filter(open.items, query) : open.items] : sections;
126
+ const enter = (action: ComposerAction | undefined) => {
127
+ setQuery('');
128
+ setOpen(action);
129
+ };
109
130
  const pick = (action: ComposerAction) => {
110
131
  if (action.items) {
111
- setOpen(action);
132
+ enter(action);
112
133
  return;
113
134
  }
114
135
  onClose();
@@ -117,12 +138,30 @@ function PlusMenu({ sections, onClose }: { sections: ComposerAction[][]; onClose
117
138
  return (
118
139
  <div className="ta-plus-menu" role="menu" aria-label={open?.label}>
119
140
  {open && (
120
- <button type="button" className="ta-plus-back" onClick={() => setOpen(undefined)}>
141
+ <button type="button" className="ta-plus-back" onClick={() => enter(undefined)}>
121
142
  <Icon icon={ChevronLeft} size={15} />
122
143
  <span>{open.label}</span>
123
144
  </button>
124
145
  )}
146
+ {searchable && (
147
+ <input
148
+ className="ta-plus-search"
149
+ autoFocus
150
+ value={query}
151
+ placeholder={t('ta.composer.filter')}
152
+ aria-label={t('ta.composer.filter')}
153
+ onChange={event => setQuery(event.target.value)}
154
+ onKeyDown={event => {
155
+ const first = shown[0]?.[0];
156
+ if (event.key === 'Enter' && first && !event.nativeEvent.isComposing) {
157
+ event.preventDefault();
158
+ pick(first);
159
+ }
160
+ }}
161
+ />
162
+ )}
125
163
  <div className="ta-plus-items">
164
+ {searchable && !shown[0].length && <div className="ta-plus-empty">{t('ta.composer.noMatch')}</div>}
126
165
  {shown.map((section, index) => (
127
166
  <React.Fragment key={index}>
128
167
  {index > 0 && <hr className="ta-plus-line" />}
@@ -169,6 +208,7 @@ export const ComposerBox = React.forwardRef<ComposerBoxHandle, ComposerBoxProps>
169
208
  beside,
170
209
  after,
171
210
  autoFocus,
211
+ cwd,
172
212
  onError,
173
213
  } = props;
174
214
  const t = useT();
@@ -361,6 +401,12 @@ export const ComposerBox = React.forwardRef<ComposerBoxHandle, ComposerBoxProps>
361
401
 
362
402
  const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
363
403
  setDragging(false);
404
+ const files = FileDragUtil.get(event.dataTransfer);
405
+ if (files) {
406
+ event.preventDefault();
407
+ edit(...ComposerUtil.insert(text, caret, `${FileDragUtil.references(files, cwd)} `));
408
+ return;
409
+ }
364
410
  if (takesImages && event.dataTransfer.files.length) {
365
411
  event.preventDefault();
366
412
  addFiles(event.dataTransfer.files);
@@ -390,11 +436,11 @@ export const ComposerBox = React.forwardRef<ComposerBoxHandle, ComposerBoxProps>
390
436
  <div className="ta-console-composer">
391
437
  {(items.length > 0 || note) && <ComposerMenu id={menuId} items={items} selected={selected} note={note} onPick={pick} onHover={setSelected} />}
392
438
  {above}
393
- {/* biome-ignore lint/a11y/noStaticElementInteractions: dropping images is a shortcut; the + menu does the same */}
439
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: dropping images and files is a shortcut; the + menu does the same */}
394
440
  <div
395
441
  className={`ta-console-box ${dragging ? 'ta-dragging' : ''}`}
396
442
  onDragOver={event => {
397
- if (takesImages && event.dataTransfer.types.includes('Files')) {
443
+ if (FileDragUtil.has(event.dataTransfer) || (takesImages && event.dataTransfer.types.includes('Files'))) {
398
444
  event.preventDefault();
399
445
  setDragging(true);
400
446
  }
@@ -144,6 +144,7 @@ export const Composer = React.forwardRef<ComposerBoxHandle, ComposerProps>(funct
144
144
  beside={beside}
145
145
  after={after}
146
146
  autoFocus
147
+ cwd={cwd}
147
148
  onError={onError}
148
149
  above={
149
150
  <>
@@ -1,13 +1,17 @@
1
1
  import type { RpcExtensionUIRequest, RpcExtensionUIResponse } from '@celljs/ta-protocol';
2
2
  import * as React from 'react';
3
3
  import { useT } from '../i18n/use-t';
4
+ import { TerminalText } from '../util/terminal-text';
4
5
 
5
6
  type Respond = (response: RpcExtensionUIResponse) => void;
6
7
 
7
- /** The dialog an extension opened, such as the permission gate's question; one at a time, oldest first. */
8
+ /**
9
+ * The dialog an extension opened, such as the permission gate's question; one at a time, oldest first. Its texts
10
+ * are written for pi's terminal: shown without its escapes, while an option goes back as it came.
11
+ */
8
12
  export function DialogView({ request, respond }: { request: RpcExtensionUIRequest; respond: Respond }) {
9
13
  const t = useT();
10
- const [value, setValue] = React.useState(() => ('prefill' in request ? (request.prefill ?? '') : ''));
14
+ const [value, setValue] = React.useState(() => ('prefill' in request ? TerminalText.plain(request.prefill ?? '') : ''));
11
15
  const cancel = () => respond({ type: 'extension_ui_response', id: request.id, cancelled: true });
12
16
  const answer = (response: Omit<RpcExtensionUIResponse, 'type' | 'id'>) =>
13
17
  respond({ type: 'extension_ui_response', id: request.id, ...response } as RpcExtensionUIResponse);
@@ -16,7 +20,7 @@ export function DialogView({ request, respond }: { request: RpcExtensionUIReques
16
20
  case 'confirm':
17
21
  body = (
18
22
  <>
19
- <p className="ta-dialog-message">{request.message}</p>
23
+ <p className="ta-dialog-message">{TerminalText.plain(request.message)}</p>
20
24
  <div className="ta-dialog-actions">
21
25
  <button type="button" className="ta-button" onClick={() => answer({ confirmed: false })}>
22
26
  {t('ta.no')}
@@ -33,7 +37,7 @@ export function DialogView({ request, respond }: { request: RpcExtensionUIReques
33
37
  <div className="ta-dialog-options">
34
38
  {request.options.map((option, index) => (
35
39
  <button key={option} type="button" className="ta-option" autoFocus={index === 0} onClick={() => answer({ value: option })}>
36
- {option}
40
+ {TerminalText.plain(option)}
37
41
  </button>
38
42
  ))}
39
43
  </div>
@@ -52,7 +56,7 @@ export function DialogView({ request, respond }: { request: RpcExtensionUIReques
52
56
  className="ta-input"
53
57
  autoFocus
54
58
  value={value}
55
- placeholder={request.placeholder}
59
+ placeholder={request.placeholder && TerminalText.plain(request.placeholder)}
56
60
  onChange={event => setValue(event.target.value)}
57
61
  />
58
62
  ) : (
@@ -74,8 +78,13 @@ export function DialogView({ request, respond }: { request: RpcExtensionUIReques
74
78
  return null;
75
79
  }
76
80
  return (
77
- <div className="ta-dialog ta-glass" role="dialog" aria-label={request.title} onKeyDown={event => event.key === 'Escape' && cancel()}>
78
- <div className="ta-dialog-title">{request.title}</div>
81
+ <div
82
+ className="ta-dialog ta-glass"
83
+ role="dialog"
84
+ aria-label={TerminalText.plain(request.title)}
85
+ onKeyDown={event => event.key === 'Escape' && cancel()}
86
+ >
87
+ <div className="ta-dialog-title">{TerminalText.plain(request.title)}</div>
79
88
  {body}
80
89
  </div>
81
90
  );
@@ -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
+ });