@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.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/esm/browser/chat/chat-protocol.d.ts +4 -0
- package/esm/browser/chat/chat-view.js +4 -1
- package/esm/browser/chat/chat-view.js.map +1 -1
- package/esm/browser/chat/chat.d.ts +8 -1
- package/esm/browser/chat/chat.js +34 -15
- package/esm/browser/chat/chat.js.map +1 -1
- package/esm/browser/chat/draft-model.d.ts +15 -0
- package/esm/browser/chat/draft-model.js +53 -0
- package/esm/browser/chat/draft-model.js.map +1 -0
- package/esm/browser/components/app-view.js +10 -3
- package/esm/browser/components/app-view.js.map +1 -1
- package/esm/browser/components/composer-box.d.ts +6 -0
- package/esm/browser/components/composer-box.js +50 -13
- package/esm/browser/components/composer-box.js.map +1 -1
- package/esm/browser/components/composer.js +1 -1
- package/esm/browser/components/composer.js.map +1 -1
- package/esm/browser/components/dialog-view.d.ts +4 -1
- package/esm/browser/components/dialog-view.js +11 -7
- package/esm/browser/components/dialog-view.js.map +1 -1
- package/esm/browser/components/notice-center.js +3 -1
- package/esm/browser/components/notice-center.js.map +1 -1
- package/esm/browser/components/session-view.js +48 -4
- package/esm/browser/components/session-view.js.map +1 -1
- package/esm/browser/components/terminal-art.d.ts +7 -0
- package/esm/browser/components/terminal-art.js +12 -0
- package/esm/browser/components/terminal-art.js.map +1 -0
- package/esm/browser/components/tool-view.js +1 -1
- package/esm/browser/components/tool-view.js.map +1 -1
- package/esm/browser/components/web-composer.js +1 -1
- package/esm/browser/components/web-composer.js.map +1 -1
- package/esm/browser/extension/icons.js +4 -1
- package/esm/browser/extension/icons.js.map +1 -1
- package/esm/browser/extension/web-extension-api.d.ts +117 -1
- package/esm/browser/extension/web-extensions.d.ts +3 -0
- package/esm/browser/extension/web-extensions.js +22 -0
- package/esm/browser/extension/web-extensions.js.map +1 -1
- package/esm/browser/i18n/locales.d.ts +13 -1
- package/esm/browser/i18n/messages.d.ts +12 -0
- package/esm/browser/i18n/messages.js +24 -0
- package/esm/browser/i18n/messages.js.map +1 -1
- package/esm/browser/notices/notices-protocol.d.ts +3 -0
- package/esm/browser/notices/notices.d.ts +3 -0
- package/esm/browser/notices/notices.js +15 -0
- package/esm/browser/notices/notices.js.map +1 -1
- package/esm/browser/style/conversation.d.ts +1 -1
- package/esm/browser/style/conversation.js +16 -1
- package/esm/browser/style/conversation.js.map +1 -1
- package/esm/browser/style/layout.d.ts +1 -1
- package/esm/browser/style/layout.js +5 -0
- package/esm/browser/style/layout.js.map +1 -1
- package/esm/browser/style/shell.d.ts +1 -1
- package/esm/browser/style/shell.js +1 -0
- package/esm/browser/style/shell.js.map +1 -1
- package/esm/browser/terminal/canvas-font.d.ts +10 -0
- package/esm/browser/terminal/canvas-font.js +34 -0
- package/esm/browser/terminal/canvas-font.js.map +1 -0
- package/esm/browser/terminal/terminal-links.d.ts +18 -0
- package/esm/browser/terminal/terminal-links.js +72 -0
- package/esm/browser/terminal/terminal-links.js.map +1 -0
- package/esm/browser/terminal/terminal-theme.d.ts +8 -0
- package/esm/browser/terminal/terminal-theme.js +65 -0
- package/esm/browser/terminal/terminal-theme.js.map +1 -0
- package/esm/browser/terminal/web-terminal.d.ts +74 -0
- package/esm/browser/terminal/web-terminal.js +300 -0
- package/esm/browser/terminal/web-terminal.js.map +1 -0
- package/esm/browser/terminal/xterm.d.ts +5 -0
- package/esm/browser/terminal/xterm.js +9 -0
- package/esm/browser/terminal/xterm.js.map +1 -0
- package/esm/browser/util/desktop.d.ts +43 -0
- package/esm/browser/util/desktop.js +55 -0
- package/esm/browser/util/desktop.js.map +1 -0
- package/esm/browser/util/file-drag.d.ts +26 -0
- package/esm/browser/util/file-drag.js +73 -0
- package/esm/browser/util/file-drag.js.map +1 -0
- package/esm/browser/util/terminal-text.d.ts +33 -0
- package/esm/browser/util/terminal-text.js +209 -0
- package/esm/browser/util/terminal-text.js.map +1 -0
- package/esm/browser/util/tool.js +16 -0
- package/esm/browser/util/tool.js.map +1 -1
- package/lib/browser/chat/chat-protocol.d.ts +4 -0
- package/lib/browser/chat/chat-protocol.d.ts.map +1 -1
- package/lib/browser/chat/chat-view.d.ts.map +1 -1
- package/lib/browser/chat/chat-view.js +4 -1
- package/lib/browser/chat/chat-view.js.map +1 -1
- package/lib/browser/chat/chat.d.ts +8 -1
- package/lib/browser/chat/chat.d.ts.map +1 -1
- package/lib/browser/chat/chat.js +34 -15
- package/lib/browser/chat/chat.js.map +1 -1
- package/lib/browser/chat/chat.spec.js +30 -0
- package/lib/browser/chat/chat.spec.js.map +1 -1
- package/lib/browser/chat/draft-model.d.ts +16 -0
- package/lib/browser/chat/draft-model.d.ts.map +1 -0
- package/lib/browser/chat/draft-model.js +90 -0
- package/lib/browser/chat/draft-model.js.map +1 -0
- package/lib/browser/chat/draft-model.spec.d.ts +2 -0
- package/lib/browser/chat/draft-model.spec.d.ts.map +1 -0
- package/lib/browser/chat/draft-model.spec.js +31 -0
- package/lib/browser/chat/draft-model.spec.js.map +1 -0
- package/lib/browser/components/app-view.d.ts.map +1 -1
- package/lib/browser/components/app-view.js +10 -3
- package/lib/browser/components/app-view.js.map +1 -1
- package/lib/browser/components/composer-box.d.ts +6 -0
- package/lib/browser/components/composer-box.d.ts.map +1 -1
- package/lib/browser/components/composer-box.js +51 -14
- package/lib/browser/components/composer-box.js.map +1 -1
- package/lib/browser/components/composer.d.ts.map +1 -1
- package/lib/browser/components/composer.js +1 -1
- package/lib/browser/components/composer.js.map +1 -1
- package/lib/browser/components/dialog-view.d.ts +4 -1
- package/lib/browser/components/dialog-view.d.ts.map +1 -1
- package/lib/browser/components/dialog-view.js +11 -7
- package/lib/browser/components/dialog-view.js.map +1 -1
- package/lib/browser/components/notice-center.d.ts.map +1 -1
- package/lib/browser/components/notice-center.js +3 -1
- package/lib/browser/components/notice-center.js.map +1 -1
- package/lib/browser/components/plus-menu.spec.d.ts +2 -0
- package/lib/browser/components/plus-menu.spec.d.ts.map +1 -0
- package/lib/browser/components/plus-menu.spec.js +18 -0
- package/lib/browser/components/plus-menu.spec.js.map +1 -0
- package/lib/browser/components/session-view.d.ts.map +1 -1
- package/lib/browser/components/session-view.js +48 -4
- package/lib/browser/components/session-view.js.map +1 -1
- package/lib/browser/components/terminal-art.d.ts +8 -0
- package/lib/browser/components/terminal-art.d.ts.map +1 -0
- package/lib/browser/components/terminal-art.js +48 -0
- package/lib/browser/components/terminal-art.js.map +1 -0
- package/lib/browser/components/tool-view.js +1 -1
- package/lib/browser/components/tool-view.js.map +1 -1
- package/lib/browser/components/web-composer.d.ts.map +1 -1
- package/lib/browser/components/web-composer.js +1 -1
- package/lib/browser/components/web-composer.js.map +1 -1
- package/lib/browser/extension/icons.d.ts.map +1 -1
- package/lib/browser/extension/icons.js +3 -0
- package/lib/browser/extension/icons.js.map +1 -1
- package/lib/browser/extension/web-extension-api.d.ts +117 -1
- package/lib/browser/extension/web-extension-api.d.ts.map +1 -1
- package/lib/browser/extension/web-extensions.d.ts +3 -0
- package/lib/browser/extension/web-extensions.d.ts.map +1 -1
- package/lib/browser/extension/web-extensions.js +22 -0
- package/lib/browser/extension/web-extensions.js.map +1 -1
- package/lib/browser/i18n/locales.d.ts +13 -1
- package/lib/browser/i18n/locales.d.ts.map +1 -1
- package/lib/browser/i18n/messages.d.ts +12 -0
- package/lib/browser/i18n/messages.d.ts.map +1 -1
- package/lib/browser/i18n/messages.js +24 -0
- package/lib/browser/i18n/messages.js.map +1 -1
- package/lib/browser/notices/notices-protocol.d.ts +3 -0
- package/lib/browser/notices/notices-protocol.d.ts.map +1 -1
- package/lib/browser/notices/notices.d.ts +3 -0
- package/lib/browser/notices/notices.d.ts.map +1 -1
- package/lib/browser/notices/notices.js +15 -0
- package/lib/browser/notices/notices.js.map +1 -1
- package/lib/browser/notices/notices.spec.js +7 -2
- package/lib/browser/notices/notices.spec.js.map +1 -1
- package/lib/browser/style/conversation.d.ts +1 -1
- package/lib/browser/style/conversation.d.ts.map +1 -1
- package/lib/browser/style/conversation.js +16 -1
- package/lib/browser/style/conversation.js.map +1 -1
- package/lib/browser/style/layout.d.ts +1 -1
- package/lib/browser/style/layout.d.ts.map +1 -1
- package/lib/browser/style/layout.js +5 -0
- package/lib/browser/style/layout.js.map +1 -1
- package/lib/browser/style/shell.d.ts +1 -1
- package/lib/browser/style/shell.d.ts.map +1 -1
- package/lib/browser/style/shell.js +1 -0
- package/lib/browser/style/shell.js.map +1 -1
- package/lib/browser/terminal/canvas-font.d.ts +11 -0
- package/lib/browser/terminal/canvas-font.d.ts.map +1 -0
- package/lib/browser/terminal/canvas-font.js +37 -0
- package/lib/browser/terminal/canvas-font.js.map +1 -0
- package/lib/browser/terminal/canvas-font.spec.d.ts +2 -0
- package/lib/browser/terminal/canvas-font.spec.d.ts.map +1 -0
- package/lib/browser/terminal/canvas-font.spec.js +32 -0
- package/lib/browser/terminal/canvas-font.spec.js.map +1 -0
- package/lib/browser/terminal/terminal-links.d.ts +19 -0
- package/lib/browser/terminal/terminal-links.d.ts.map +1 -0
- package/lib/browser/terminal/terminal-links.js +75 -0
- package/lib/browser/terminal/terminal-links.js.map +1 -0
- package/lib/browser/terminal/terminal-links.spec.d.ts +2 -0
- package/lib/browser/terminal/terminal-links.spec.d.ts.map +1 -0
- package/lib/browser/terminal/terminal-links.spec.js +30 -0
- package/lib/browser/terminal/terminal-links.spec.js.map +1 -0
- package/lib/browser/terminal/terminal-theme.d.ts +9 -0
- package/lib/browser/terminal/terminal-theme.d.ts.map +1 -0
- package/lib/browser/terminal/terminal-theme.js +68 -0
- package/lib/browser/terminal/terminal-theme.js.map +1 -0
- package/lib/browser/terminal/terminal-theme.spec.d.ts +2 -0
- package/lib/browser/terminal/terminal-theme.spec.d.ts.map +1 -0
- package/lib/browser/terminal/terminal-theme.spec.js +24 -0
- package/lib/browser/terminal/terminal-theme.spec.js.map +1 -0
- package/lib/browser/terminal/web-terminal.d.ts +75 -0
- package/lib/browser/terminal/web-terminal.d.ts.map +1 -0
- package/lib/browser/terminal/web-terminal.js +304 -0
- package/lib/browser/terminal/web-terminal.js.map +1 -0
- package/lib/browser/terminal/xterm.d.ts +6 -0
- package/lib/browser/terminal/xterm.d.ts.map +1 -0
- package/lib/browser/terminal/xterm.js +16 -0
- package/lib/browser/terminal/xterm.js.map +1 -0
- package/lib/browser/util/desktop.d.ts +44 -0
- package/lib/browser/util/desktop.d.ts.map +1 -0
- package/lib/browser/util/desktop.js +58 -0
- package/lib/browser/util/desktop.js.map +1 -0
- package/lib/browser/util/desktop.spec.d.ts +2 -0
- package/lib/browser/util/desktop.spec.d.ts.map +1 -0
- package/lib/browser/util/desktop.spec.js +67 -0
- package/lib/browser/util/desktop.spec.js.map +1 -0
- package/lib/browser/util/file-drag.d.ts +27 -0
- package/lib/browser/util/file-drag.d.ts.map +1 -0
- package/lib/browser/util/file-drag.js +76 -0
- package/lib/browser/util/file-drag.js.map +1 -0
- package/lib/browser/util/file-drag.spec.d.ts +2 -0
- package/lib/browser/util/file-drag.spec.d.ts.map +1 -0
- package/lib/browser/util/file-drag.spec.js +46 -0
- package/lib/browser/util/file-drag.spec.js.map +1 -0
- package/lib/browser/util/terminal-text.d.ts +34 -0
- package/lib/browser/util/terminal-text.d.ts.map +1 -0
- package/lib/browser/util/terminal-text.js +212 -0
- package/lib/browser/util/terminal-text.js.map +1 -0
- package/lib/browser/util/terminal-text.spec.d.ts +2 -0
- package/lib/browser/util/terminal-text.spec.d.ts.map +1 -0
- package/lib/browser/util/terminal-text.spec.js +54 -0
- package/lib/browser/util/terminal-text.spec.js.map +1 -0
- package/lib/browser/util/tool.d.ts.map +1 -1
- package/lib/browser/util/tool.js +16 -0
- package/lib/browser/util/tool.js.map +1 -1
- package/lib/browser/util/tool.spec.js +12 -0
- package/lib/browser/util/tool.spec.js.map +1 -1
- package/package.json +11 -7
- package/src/browser/chat/chat-protocol.ts +4 -0
- package/src/browser/chat/chat-view.tsx +4 -0
- package/src/browser/chat/chat.spec.ts +31 -0
- package/src/browser/chat/chat.ts +36 -14
- package/src/browser/chat/draft-model.spec.tsx +31 -0
- package/src/browser/chat/draft-model.tsx +63 -0
- package/src/browser/components/app-view.tsx +10 -3
- package/src/browser/components/composer-box.tsx +52 -6
- package/src/browser/components/composer.tsx +1 -0
- package/src/browser/components/dialog-view.tsx +16 -7
- package/src/browser/components/notice-center.tsx +7 -1
- package/src/browser/components/plus-menu.spec.ts +16 -0
- package/src/browser/components/session-view.tsx +65 -4
- package/src/browser/components/terminal-art.tsx +21 -0
- package/src/browser/components/tool-view.tsx +1 -1
- package/src/browser/components/web-composer.tsx +1 -0
- package/src/browser/extension/icons.ts +6 -0
- package/src/browser/extension/web-extension-api.ts +131 -1
- package/src/browser/extension/web-extensions.ts +36 -1
- package/src/browser/i18n/messages.ts +24 -0
- package/src/browser/notices/notices-protocol.ts +3 -0
- package/src/browser/notices/notices.spec.ts +8 -2
- package/src/browser/notices/notices.ts +17 -1
- package/src/browser/style/conversation.ts +16 -1
- package/src/browser/style/layout.ts +5 -0
- package/src/browser/style/shell.ts +1 -0
- package/src/browser/terminal/canvas-font.spec.ts +29 -0
- package/src/browser/terminal/canvas-font.ts +32 -0
- package/src/browser/terminal/terminal-links.spec.ts +32 -0
- package/src/browser/terminal/terminal-links.ts +75 -0
- package/src/browser/terminal/terminal-theme.spec.ts +23 -0
- package/src/browser/terminal/terminal-theme.ts +66 -0
- package/src/browser/terminal/web-terminal.ts +330 -0
- package/src/browser/terminal/xterm.ts +10 -0
- package/src/browser/util/desktop.spec.ts +68 -0
- package/src/browser/util/desktop.ts +96 -0
- package/src/browser/util/file-drag.spec.ts +48 -0
- package/src/browser/util/file-drag.ts +78 -0
- package/src/browser/util/terminal-text.spec.ts +60 -0
- package/src/browser/util/terminal-text.ts +223 -0
- package/src/browser/util/tool.spec.ts +12 -0
- 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.
|
|
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.
|
|
35
|
-
"@celljs/react": "4.
|
|
36
|
-
"@celljs/ta-client": "4.
|
|
37
|
-
"@celljs/ta-protocol": "4.
|
|
38
|
-
"@celljs/ta-relay": "4.
|
|
39
|
-
"@celljs/widget": "4.
|
|
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');
|
package/src/browser/chat/chat.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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={() =>
|
|
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
|
}
|
|
@@ -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
|
-
/**
|
|
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
|
|
78
|
-
|
|
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
|
|
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
|
+
});
|