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