@celljs/ta-web 4.6.1 → 4.7.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 (147) hide show
  1. package/README.en.md +90 -0
  2. package/README.md +12 -6
  3. package/esm/browser/chat/chat-view.js +1 -1
  4. package/esm/browser/chat/chat-view.js.map +1 -1
  5. package/esm/browser/components/account-menu.d.ts +3 -2
  6. package/esm/browser/components/account-menu.js +21 -16
  7. package/esm/browser/components/account-menu.js.map +1 -1
  8. package/esm/browser/components/app-view.js +3 -1
  9. package/esm/browser/components/app-view.js.map +1 -1
  10. package/esm/browser/components/chat-list.d.ts +3 -1
  11. package/esm/browser/components/chat-list.js +6 -1
  12. package/esm/browser/components/chat-list.js.map +1 -1
  13. package/esm/browser/components/folder-picker.d.ts +37 -6
  14. package/esm/browser/components/folder-picker.js +155 -33
  15. package/esm/browser/components/folder-picker.js.map +1 -1
  16. package/esm/browser/components/remote-hosts.d.ts +7 -0
  17. package/esm/browser/components/remote-hosts.js +94 -0
  18. package/esm/browser/components/remote-hosts.js.map +1 -0
  19. package/esm/browser/components/remote-prompts.d.ts +7 -0
  20. package/esm/browser/components/remote-prompts.js +72 -0
  21. package/esm/browser/components/remote-prompts.js.map +1 -0
  22. package/esm/browser/components/settings-view.js +8 -1
  23. package/esm/browser/components/settings-view.js.map +1 -1
  24. package/esm/browser/components/web-composer.d.ts +4 -1
  25. package/esm/browser/components/web-composer.js +2 -2
  26. package/esm/browser/components/web-composer.js.map +1 -1
  27. package/esm/browser/components/working-directory.js +1 -1
  28. package/esm/browser/components/working-directory.js.map +1 -1
  29. package/esm/browser/extension/icons.js +3 -1
  30. package/esm/browser/extension/icons.js.map +1 -1
  31. package/esm/browser/extension/web-extension-api.d.ts +4 -2
  32. package/esm/browser/extension/web-extensions.js +11 -1
  33. package/esm/browser/extension/web-extensions.js.map +1 -1
  34. package/esm/browser/i18n/locales.d.ts +32 -1
  35. package/esm/browser/i18n/messages.d.ts +31 -0
  36. package/esm/browser/i18n/messages.js +75 -13
  37. package/esm/browser/i18n/messages.js.map +1 -1
  38. package/esm/browser/style/base.d.ts +1 -1
  39. package/esm/browser/style/base.js +6 -1
  40. package/esm/browser/style/base.js.map +1 -1
  41. package/esm/browser/style/layout.d.ts +1 -1
  42. package/esm/browser/style/layout.js +48 -15
  43. package/esm/browser/style/layout.js.map +1 -1
  44. package/esm/browser/util/message.d.ts +1 -0
  45. package/esm/browser/util/message.js +7 -5
  46. package/esm/browser/util/message.js.map +1 -1
  47. package/esm/browser/workbench-protocol.d.ts +7 -1
  48. package/esm/browser/workbench-protocol.js.map +1 -1
  49. package/esm/browser/workbench.d.ts +7 -1
  50. package/esm/browser/workbench.js +23 -0
  51. package/esm/browser/workbench.js.map +1 -1
  52. package/lib/browser/chat/chat-view.js +1 -1
  53. package/lib/browser/chat/chat-view.js.map +1 -1
  54. package/lib/browser/components/account-menu.d.ts +3 -2
  55. package/lib/browser/components/account-menu.d.ts.map +1 -1
  56. package/lib/browser/components/account-menu.js +21 -16
  57. package/lib/browser/components/account-menu.js.map +1 -1
  58. package/lib/browser/components/app-view.d.ts.map +1 -1
  59. package/lib/browser/components/app-view.js +3 -1
  60. package/lib/browser/components/app-view.js.map +1 -1
  61. package/lib/browser/components/chat-list.d.ts +3 -1
  62. package/lib/browser/components/chat-list.d.ts.map +1 -1
  63. package/lib/browser/components/chat-list.js +6 -1
  64. package/lib/browser/components/chat-list.js.map +1 -1
  65. package/lib/browser/components/folder-picker.d.ts +37 -6
  66. package/lib/browser/components/folder-picker.d.ts.map +1 -1
  67. package/lib/browser/components/folder-picker.js +156 -33
  68. package/lib/browser/components/folder-picker.js.map +1 -1
  69. package/lib/browser/components/folder-picker.spec.js +15 -0
  70. package/lib/browser/components/folder-picker.spec.js.map +1 -1
  71. package/lib/browser/components/remote-hosts.d.ts +8 -0
  72. package/lib/browser/components/remote-hosts.d.ts.map +1 -0
  73. package/lib/browser/components/remote-hosts.js +130 -0
  74. package/lib/browser/components/remote-hosts.js.map +1 -0
  75. package/lib/browser/components/remote-prompts.d.ts +8 -0
  76. package/lib/browser/components/remote-prompts.d.ts.map +1 -0
  77. package/lib/browser/components/remote-prompts.js +108 -0
  78. package/lib/browser/components/remote-prompts.js.map +1 -0
  79. package/lib/browser/components/settings-view.d.ts.map +1 -1
  80. package/lib/browser/components/settings-view.js +7 -0
  81. package/lib/browser/components/settings-view.js.map +1 -1
  82. package/lib/browser/components/web-composer.d.ts +4 -1
  83. package/lib/browser/components/web-composer.d.ts.map +1 -1
  84. package/lib/browser/components/web-composer.js +2 -2
  85. package/lib/browser/components/web-composer.js.map +1 -1
  86. package/lib/browser/components/working-directory.js +1 -1
  87. package/lib/browser/components/working-directory.js.map +1 -1
  88. package/lib/browser/extension/icons.d.ts.map +1 -1
  89. package/lib/browser/extension/icons.js +2 -0
  90. package/lib/browser/extension/icons.js.map +1 -1
  91. package/lib/browser/extension/web-extension-api.d.ts +4 -2
  92. package/lib/browser/extension/web-extension-api.d.ts.map +1 -1
  93. package/lib/browser/extension/web-extensions.d.ts.map +1 -1
  94. package/lib/browser/extension/web-extensions.js +11 -1
  95. package/lib/browser/extension/web-extensions.js.map +1 -1
  96. package/lib/browser/i18n/locales.d.ts +32 -1
  97. package/lib/browser/i18n/locales.d.ts.map +1 -1
  98. package/lib/browser/i18n/messages.d.ts +31 -0
  99. package/lib/browser/i18n/messages.d.ts.map +1 -1
  100. package/lib/browser/i18n/messages.js +75 -13
  101. package/lib/browser/i18n/messages.js.map +1 -1
  102. package/lib/browser/notices/notices.spec.js +1 -1
  103. package/lib/browser/notices/notices.spec.js.map +1 -1
  104. package/lib/browser/style/base.d.ts +1 -1
  105. package/lib/browser/style/base.d.ts.map +1 -1
  106. package/lib/browser/style/base.js +6 -1
  107. package/lib/browser/style/base.js.map +1 -1
  108. package/lib/browser/style/layout.d.ts +1 -1
  109. package/lib/browser/style/layout.d.ts.map +1 -1
  110. package/lib/browser/style/layout.js +48 -15
  111. package/lib/browser/style/layout.js.map +1 -1
  112. package/lib/browser/util/message.d.ts +1 -0
  113. package/lib/browser/util/message.d.ts.map +1 -1
  114. package/lib/browser/util/message.js +7 -5
  115. package/lib/browser/util/message.js.map +1 -1
  116. package/lib/browser/util/message.spec.js +2 -0
  117. package/lib/browser/util/message.spec.js.map +1 -1
  118. package/lib/browser/workbench-protocol.d.ts +7 -1
  119. package/lib/browser/workbench-protocol.d.ts.map +1 -1
  120. package/lib/browser/workbench-protocol.js.map +1 -1
  121. package/lib/browser/workbench.d.ts +7 -1
  122. package/lib/browser/workbench.d.ts.map +1 -1
  123. package/lib/browser/workbench.js +23 -0
  124. package/lib/browser/workbench.js.map +1 -1
  125. package/package.json +7 -7
  126. package/src/browser/chat/chat-view.tsx +1 -1
  127. package/src/browser/components/account-menu.tsx +23 -20
  128. package/src/browser/components/app-view.tsx +2 -0
  129. package/src/browser/components/chat-list.tsx +9 -2
  130. package/src/browser/components/folder-picker.spec.ts +17 -0
  131. package/src/browser/components/folder-picker.tsx +221 -39
  132. package/src/browser/components/remote-hosts.tsx +149 -0
  133. package/src/browser/components/remote-prompts.tsx +111 -0
  134. package/src/browser/components/settings-view.tsx +8 -0
  135. package/src/browser/components/web-composer.tsx +17 -3
  136. package/src/browser/components/working-directory.tsx +1 -1
  137. package/src/browser/extension/icons.ts +4 -0
  138. package/src/browser/extension/web-extension-api.ts +4 -2
  139. package/src/browser/extension/web-extensions.ts +12 -2
  140. package/src/browser/i18n/messages.ts +75 -13
  141. package/src/browser/notices/notices.spec.ts +1 -1
  142. package/src/browser/style/base.ts +6 -1
  143. package/src/browser/style/layout.ts +48 -15
  144. package/src/browser/util/message.spec.ts +2 -0
  145. package/src/browser/util/message.ts +7 -4
  146. package/src/browser/workbench-protocol.ts +8 -0
  147. package/src/browser/workbench.ts +29 -0
@@ -5,7 +5,7 @@ import { useT } from '../i18n/use-t';
5
5
  import type { TaNotices } from '../notices/notices-protocol';
6
6
  import { ComposerUtil } from '../util/composer';
7
7
  import { ComposerBox } from './composer-box';
8
- import { DirectoryButton } from './folder-picker';
8
+ import { DirectoryButton, type FolderPickerRemote } from './folder-picker';
9
9
 
10
10
  export interface WebComposerHostProps extends WebComposerProps {
11
11
  /** Where a failure to send goes. */
@@ -14,10 +14,24 @@ export interface WebComposerHostProps extends WebComposerProps {
14
14
  listDirectory?(directory: string): Promise<TaDirEntry[]>;
15
15
  /** The directory of ta serve: that of a `cwd` left empty. */
16
16
  serverCwd?: string;
17
+ /** The hosts of ssh, which the button offers too. */
18
+ remote?: FolderPickerRemote;
17
19
  }
18
20
 
19
21
  /** The message box as web extensions get it (`pi.ui.Composer`): text only, `@` for the names they give; a failure is a notice of the page. */
20
- export function WebComposer({ notices, placeholder, onSend, mentions, mentionLabel, actions, cwd, onPickCwd, listDirectory, serverCwd }: WebComposerHostProps) {
22
+ export function WebComposer({
23
+ notices,
24
+ placeholder,
25
+ onSend,
26
+ mentions,
27
+ mentionLabel,
28
+ actions,
29
+ cwd,
30
+ onPickCwd,
31
+ listDirectory,
32
+ serverCwd,
33
+ remote,
34
+ }: WebComposerHostProps) {
21
35
  const t = useT();
22
36
  const [query, setQuery] = React.useState<string>();
23
37
  return (
@@ -40,7 +54,7 @@ export function WebComposer({ notices, placeholder, onSend, mentions, mentionLab
40
54
  }
41
55
  mentionLabel={mentions ? mentionLabel : undefined}
42
56
  actions={actions}
43
- beside={onPickCwd && listDirectory && <DirectoryButton cwd={cwd || serverCwd || ''} list={listDirectory} onPick={onPickCwd} />}
57
+ beside={onPickCwd && listDirectory && <DirectoryButton cwd={cwd || serverCwd || ''} list={listDirectory} remote={remote} onPick={onPickCwd} />}
44
58
  onError={cause => notices.error(cause)}
45
59
  />
46
60
  );
@@ -18,5 +18,5 @@ export function WorkingDirectoryPicker({ onClose }: { onClose(): void }) {
18
18
  notices.error(error);
19
19
  }
20
20
  };
21
- return <FolderPicker list={list} initial={workbench.server?.cwd ?? '.'} onPick={pick} onClose={onClose} />;
21
+ return <FolderPicker list={list} initial={workbench.server?.cwd ?? '.'} remote={bench} onPick={pick} onClose={onClose} />;
22
22
  }
@@ -62,11 +62,13 @@ import {
62
62
  Save,
63
63
  ScrollText,
64
64
  Search,
65
+ Server,
65
66
  Sparkles,
66
67
  Square,
67
68
  SquareMinus,
68
69
  SquarePlus,
69
70
  SquareTerminal,
71
+ Store,
70
72
  Text,
71
73
  Trash2,
72
74
  Undo2,
@@ -144,11 +146,13 @@ export const WEB_ICONS: Readonly<Record<string, WebIcon>> = Object.freeze({
144
146
  Save,
145
147
  ScrollText,
146
148
  Search,
149
+ Server,
147
150
  Sparkles,
148
151
  Square,
149
152
  SquareMinus,
150
153
  SquarePlus,
151
154
  SquareTerminal,
155
+ Store,
152
156
  Text,
153
157
  Trash2,
154
158
  Undo2,
@@ -1,4 +1,4 @@
1
- import type { TaEvent, TaPackageServer, TaServer, TaSessionInfo, TaTerminalServer, TaWorkspaceServer } from '@celljs/ta-protocol';
1
+ import type { TaEvent, TaPackageServer, TaRemoteServer, TaServer, TaSessionInfo, TaTerminalServer, TaWorkspaceServer } from '@celljs/ta-protocol';
2
2
  import type * as React from 'react';
3
3
  import type { Mood } from '../avatar/dino-avatar';
4
4
  import type { Language, Theme } from '../settings/settings-protocol';
@@ -13,12 +13,14 @@ export interface WebExtensionAPI {
13
13
  readonly React: typeof React;
14
14
  /** The sessions of ta serve, for what the rest of the API does not do: the commands of pi's RPC mode. */
15
15
  readonly server: TaServer;
16
- /** The files and programs of a directory on the machine of ta serve. */
16
+ /** The files and programs of a directory, on the machine of ta serve or of an `ssh://` directory. */
17
17
  readonly workspace: TaWorkspaceServer;
18
18
  /** Shells; their output comes by {@link onTerminalData}. */
19
19
  readonly terminals: TaTerminalServer;
20
20
  /** The packages pi loads; for the data of the extension's own package, {@link storage}. */
21
21
  readonly packages: TaPackageServer;
22
+ /** The other machines of `ssh://` directories (TaLocation): their ports, for a page served there. */
23
+ readonly remote: TaRemoteServer;
22
24
  readonly ui: WebExtensionUI;
23
25
  /** A view of its own: a page or a sidebar view, in the navigation on the left, or a tool of the workbench on the right. */
24
26
  registerView(name: string, view: WebView): void;
@@ -1,5 +1,5 @@
1
1
  import { Autowired, Component, Optional, PostConstruct } from '@celljs/core';
2
- import type { TaEvent, TaPackageServer, TaServer, TaTerminalServer, TaWebExtensionInfo, TaWorkspaceServer } from '@celljs/ta-protocol';
2
+ import type { TaEvent, TaPackageServer, TaRemoteServer, TaServer, TaTerminalServer, TaWebExtensionInfo, TaWorkspaceServer } from '@celljs/ta-protocol';
3
3
  import * as React from 'react';
4
4
  import { CodeEditor } from '../components/code-editor';
5
5
  import { DiffEditor } from '../components/diff-editor';
@@ -474,11 +474,18 @@ export class TaWebExtensionsImpl implements TaWebExtensions {
474
474
  protected readonly listDirectory = (directory: string) => this.workbench.workspace.listDirectory('.', directory);
475
475
  /** The message box, whose button of the directory lists those of the server. */
476
476
  protected readonly composer = (props: WebComposerProps) =>
477
- React.createElement(WebComposer, { ...props, notices: this.notices, listDirectory: this.listDirectory, serverCwd: this.workbench.state.server?.cwd });
477
+ React.createElement(WebComposer, {
478
+ ...props,
479
+ notices: this.notices,
480
+ listDirectory: this.listDirectory,
481
+ serverCwd: this.workbench.state.server?.cwd,
482
+ remote: this.workbench,
483
+ });
478
484
  protected readonly folderPicker = ({ initial, onPick, onClose }: WebFolderPickerProps) =>
479
485
  React.createElement(FolderPicker, {
480
486
  list: this.listDirectory,
481
487
  initial: initial || this.workbench.state.server?.cwd || '.',
488
+ remote: this.workbench,
482
489
  onPick: async (path: string) => onPick(path),
483
490
  onClose,
484
491
  });
@@ -508,6 +515,9 @@ export class TaWebExtensionsImpl implements TaWebExtensions {
508
515
  get packages(): TaPackageServer {
509
516
  return workbench.packages;
510
517
  },
518
+ get remote(): TaRemoteServer {
519
+ return workbench.remote;
520
+ },
511
521
  ui: {
512
522
  setWidget: (key, component, options = {}) =>
513
523
  register(() => (component ? run.widgets.set(key, { component, options }) : run.widgets.delete(key))),
@@ -44,15 +44,15 @@ export const EN_MESSAGES = {
44
44
  'ta.editor.discardClose': 'Close without saving',
45
45
  'ta.online': 'Online',
46
46
  'ta.reconnecting': 'Reconnecting',
47
- 'ta.unreachable': 'ta serve is not running. Start it with ta serve; this page connects by itself.',
47
+ 'ta.unreachable': 'The Ta service is not running. Start it with ta serve in a terminal; this page connects by itself.',
48
48
  'ta.pwa.install': 'Install as an app',
49
- // The connection to ta serve, and the notices of the page
50
- 'ta.connection.lost': 'The connection to ta serve dropped.',
49
+ // The connection to the Ta service (ta serve), and the notices of the page
50
+ 'ta.connection.lost': 'The connection to the Ta service dropped.',
51
51
  'ta.connection.retryIn': 'Trying again in {seconds}s',
52
52
  'ta.connection.retrying': 'Connecting…',
53
53
  'ta.connection.retryNow': 'Retry now',
54
54
  'ta.connection.back': 'Connected again',
55
- 'ta.connection.notDone': 'Not done: ta serve is not connected',
55
+ 'ta.connection.notDone': 'Not done: the Ta service is not connected',
56
56
  'ta.connection.notDoneHint': 'Try again once the connection is back.',
57
57
  'ta.notices': 'Notifications',
58
58
  'ta.packages.removeDetail': 'Its pages, cards and commands go with it.',
@@ -62,10 +62,10 @@ export const EN_MESSAGES = {
62
62
  'ta.home': 'Home',
63
63
  'ta.menu': 'Menu',
64
64
  'ta.signin.title': 'Connect to Ta',
65
- 'ta.signin.hint': 'Run ta serve --web, which opens this page signed in, or paste the token from ~/.ta/agent/serve.json.',
65
+ 'ta.signin.hint': 'Run ta serve --web in a terminal, which opens this page signed in, or paste the token from ~/.ta/agent/serve.json.',
66
66
  'ta.signin.token': 'Access token',
67
67
  'ta.signin.connect': 'Connect',
68
- 'ta.signin.refused': 'ta serve did not accept the token, or it is not running.',
68
+ 'ta.signin.refused': 'The Ta service did not accept the token, or it is not running.',
69
69
  'ta.signin.pairHint': 'On your computer, run ta pair <relay> and paste the link it prints here. What goes through the relay is encrypted end to end.',
70
70
  'ta.signin.pairing': 'Pairing link',
71
71
  'ta.signin.pairRefused': 'Your Ta does not know this device: it may have been unpaired. Run ta pair again.',
@@ -197,6 +197,36 @@ export const EN_MESSAGES = {
197
197
  'ta.folder.empty': 'No folders here.',
198
198
  'ta.folder.failed': 'Cannot open {path}.',
199
199
  'ta.folder.open': 'Use this directory',
200
+ 'ta.remote.local': 'This computer',
201
+ 'ta.remote.add': 'Add a host',
202
+ 'ta.remote.addHint': 'A Host of ~/.ssh/config, user@host:port, or an ssh command',
203
+ 'ta.remote.connected': 'Connected',
204
+ 'ta.remote.failed': 'Cannot connect to {host}',
205
+ 'ta.remote.title': 'Remote hosts',
206
+ 'ta.remote.description': 'Conversations can work in directories of these machines. Ta connects with the ssh of this computer and its ~/.ssh/config.',
207
+ 'ta.remote.none': 'No hosts yet.',
208
+ 'ta.remote.invalid': '{host} names no host: write a Host of ~/.ssh/config, user@host:port, or ssh user@host -p port.',
209
+ 'ta.remote.options': 'Ta takes no {options} here: put it in a Host of ~/.ssh/config, and add that Host.',
210
+ 'ta.remote.disconnected': 'Not connected',
211
+ 'ta.remote.fromConfig': 'From ~/.ssh/config',
212
+ 'ta.remote.added': 'Added',
213
+ 'ta.remote.connect': 'Connect',
214
+ 'ta.remote.disconnect': 'Disconnect',
215
+ 'ta.remote.connectedTo': 'Connected to {host}',
216
+ 'ta.remote.remove': 'Remove {host}',
217
+ 'ta.remote.port': 'Port {port} of {host}',
218
+ 'ta.remote.closePort': 'Stop forwarding',
219
+ 'ta.remote.connecting': 'Connecting to {host}…',
220
+ 'ta.remote.step.connecting': 'Connecting to {host}…',
221
+ 'ta.remote.step.checking': 'Looking at {host}…',
222
+ 'ta.remote.step.installing-node': 'Installing Node.js {detail} on {host}…',
223
+ 'ta.remote.step.copying-server': 'Copying Ta to {host}…',
224
+ 'ta.remote.step.starting': 'Starting on {host}…',
225
+ 'ta.remote.lost': 'Lost the connection to {host}; it comes back with the next step.',
226
+ 'ta.remote.prompt.title': 'Connect to {host}',
227
+ 'ta.remote.prompt.trust': 'Trust and connect',
228
+ 'ta.remote.prompt.continue': 'Continue',
229
+ 'ta.remote.forwarded': 'Port {port} of {host} is at {local} here',
200
230
  'ta.chat.search': 'Search chats',
201
231
  'ta.chat.all': 'All',
202
232
  'ta.chat.noMatch': 'No chat matches.',
@@ -286,6 +316,7 @@ export const EN_MESSAGES = {
286
316
  'ta.composer.queue.takeBack': 'Take back',
287
317
  'ta.composer.queue.takeBackTitle': 'Take the waiting messages back into the box',
288
318
  'ta.account': 'Account',
319
+ 'ta.account.service': 'Ta service',
289
320
  // The settings view and the menu of the gear
290
321
  'ta.manage': 'Manage',
291
322
  'ta.manage.packages': 'Extensions',
@@ -303,7 +334,7 @@ export const EN_MESSAGES = {
303
334
  'ta.settings.scopeHint.user': 'In {file}, for every project.',
304
335
  'ta.settings.scopeHint.project': 'In {file}; overrides the user settings in this project.',
305
336
  'ta.settings.untrusted': 'Ta does not trust this project yet, so it ignores its settings. Run ta in it once and trust it to change them here.',
306
- 'ta.settings.offline': 'Connect to ta serve to see these settings.',
337
+ 'ta.settings.offline': 'Connect to the Ta service to see these settings.',
307
338
  'ta.settings.loading': 'Reading the settings…',
308
339
  'ta.settings.failed': 'Not saved: {error}',
309
340
  'ta.settings.default': 'Default ({value})',
@@ -475,14 +506,14 @@ export const ZH_MESSAGES: Record<MessageId, string> = {
475
506
  'ta.editor.discardClose': '不保存并关闭',
476
507
  'ta.online': '在线',
477
508
  'ta.reconnecting': '重连中',
478
- 'ta.unreachable': 'ta serve 没有在运行。运行 ta serve 后,本页会自动连上。',
509
+ 'ta.unreachable': 'Ta 服务没有在运行。在终端运行 ta serve 启动它,本页会自动连上。',
479
510
  'ta.pwa.install': '安装为应用',
480
- 'ta.connection.lost': '与 ta serve 的连接断开了',
511
+ 'ta.connection.lost': '与 Ta 服务的连接断开了',
481
512
  'ta.connection.retryIn': '{seconds} 秒后重试',
482
513
  'ta.connection.retrying': '正在连接…',
483
514
  'ta.connection.retryNow': '立即重试',
484
515
  'ta.connection.back': '已重新连接',
485
- 'ta.connection.notDone': '没有完成:ta serve 未连接',
516
+ 'ta.connection.notDone': '没有完成:未连接到 Ta 服务',
486
517
  'ta.connection.notDoneHint': '连接恢复后再试一次。',
487
518
  'ta.notices': '通知',
488
519
  'ta.packages.removeDetail': '它的页面、卡片和命令也会一起移除。',
@@ -492,10 +523,10 @@ export const ZH_MESSAGES: Record<MessageId, string> = {
492
523
  'ta.home': '首页',
493
524
  'ta.menu': '菜单',
494
525
  'ta.signin.title': '连接 Ta',
495
- 'ta.signin.hint': '运行 ta serve --web 会自动以登录状态打开本页;也可以粘贴 ~/.ta/agent/serve.json 里的 token。',
526
+ 'ta.signin.hint': '在终端运行 ta serve --web,会自动打开本页并登录;也可以粘贴 ~/.ta/agent/serve.json 里的 token。',
496
527
  'ta.signin.token': '访问 token',
497
528
  'ta.signin.connect': '连接',
498
- 'ta.signin.refused': 'ta serve 没有接受这个 token,或者它没有在运行。',
529
+ 'ta.signin.refused': 'Ta 服务没有接受这个 token,或者它没有在运行。',
499
530
  'ta.signin.pairHint': '在电脑上运行 ta pair <Relay 地址>,把它打印的链接粘贴到这里。经 Relay 的内容端到端加密。',
500
531
  'ta.signin.pairing': '配对链接',
501
532
  'ta.signin.pairRefused': '你的 Ta 不认识这台设备,可能已取消配对。请重新运行 ta pair。',
@@ -626,6 +657,36 @@ export const ZH_MESSAGES: Record<MessageId, string> = {
626
657
  'ta.folder.empty': '这里没有子目录。',
627
658
  'ta.folder.failed': '打不开 {path}。',
628
659
  'ta.folder.open': '使用此目录',
660
+ 'ta.remote.local': '本机',
661
+ 'ta.remote.add': '添加主机',
662
+ 'ta.remote.addHint': '~/.ssh/config 里的主机、user@host:port,或 ssh 命令',
663
+ 'ta.remote.connected': '已连接',
664
+ 'ta.remote.failed': '连不上 {host}',
665
+ 'ta.remote.title': '远程主机',
666
+ 'ta.remote.description': '对话可以在这些机器的目录里工作。Ta 用本机的 ssh 和 ~/.ssh/config 连接。',
667
+ 'ta.remote.none': '还没有主机。',
668
+ 'ta.remote.invalid': '{host} 不是主机:写 ~/.ssh/config 里的 Host、user@host:port,或 ssh user@host -p 端口。',
669
+ 'ta.remote.options': '这里不支持 {options}:把它写进 ~/.ssh/config 的一个 Host 里,再添加那个 Host。',
670
+ 'ta.remote.disconnected': '未连接',
671
+ 'ta.remote.fromConfig': '来自 ~/.ssh/config',
672
+ 'ta.remote.added': '手动添加',
673
+ 'ta.remote.connect': '连接',
674
+ 'ta.remote.disconnect': '断开',
675
+ 'ta.remote.connectedTo': '已连接 {host}',
676
+ 'ta.remote.remove': '移除 {host}',
677
+ 'ta.remote.port': '{host} 的 {port} 端口',
678
+ 'ta.remote.closePort': '停止转发',
679
+ 'ta.remote.connecting': '正在连接 {host}…',
680
+ 'ta.remote.step.connecting': '正在连接 {host}…',
681
+ 'ta.remote.step.checking': '正在检查 {host}…',
682
+ 'ta.remote.step.installing-node': '正在为 {host} 安装 Node.js {detail}…',
683
+ 'ta.remote.step.copying-server': '正在把 Ta 复制到 {host}…',
684
+ 'ta.remote.step.starting': '正在 {host} 上启动…',
685
+ 'ta.remote.lost': '与 {host} 的连接断开了,下次操作时会自动重连。',
686
+ 'ta.remote.prompt.title': '连接 {host}',
687
+ 'ta.remote.prompt.trust': '信任并连接',
688
+ 'ta.remote.prompt.continue': '继续',
689
+ 'ta.remote.forwarded': '{host} 的 {port} 端口已转发到本机 {local}',
629
690
  'ta.chat.search': '搜索对话',
630
691
  'ta.chat.all': '全部',
631
692
  'ta.chat.noMatch': '没有匹配的对话。',
@@ -715,6 +776,7 @@ export const ZH_MESSAGES: Record<MessageId, string> = {
715
776
  'ta.composer.queue.takeBack': '撤回',
716
777
  'ta.composer.queue.takeBackTitle': '把排队的消息取回输入框',
717
778
  'ta.account': '账号',
779
+ 'ta.account.service': 'Ta 服务',
718
780
  // The settings view and the menu of the gear
719
781
  'ta.manage': '管理',
720
782
  'ta.manage.packages': '扩展',
@@ -732,7 +794,7 @@ export const ZH_MESSAGES: Record<MessageId, string> = {
732
794
  'ta.settings.scopeHint.user': '保存在 {file},对所有项目生效。',
733
795
  'ta.settings.scopeHint.project': '保存在 {file},在此项目中覆盖用户设置。',
734
796
  'ta.settings.untrusted': 'Ta 还没有信任这个项目,因此不会读取它的设置。先在该目录运行一次 ta 并信任它,再回来修改。',
735
- 'ta.settings.offline': '连接 ta serve 后才能查看这些设置。',
797
+ 'ta.settings.offline': '连接到 Ta 服务后才能查看这些设置。',
736
798
  'ta.settings.loading': '正在读取设置…',
737
799
  'ta.settings.failed': '未保存:{error}',
738
800
  'ta.settings.default': '默认({value})',
@@ -55,7 +55,7 @@ describe('TaNoticesImpl', () => {
55
55
  notices.error(new TaRemoteError('The connection to ta serve was lost.', TaErrorCode.CONNECTION_LOST));
56
56
  expect(notices.state.notices.map(({ message, detail, level, count }) => ({ message, detail, level, count }))).toEqual([
57
57
  { message: 'Locked.', detail: 'Close it there first.', level: 'error', count: 1 },
58
- { message: '没有完成:ta serve 未连接', detail: '连接恢复后再试一次。', level: 'error', count: 2 },
58
+ { message: '没有完成:未连接到 Ta 服务', detail: '连接恢复后再试一次。', level: 'error', count: 2 },
59
59
  ]);
60
60
  });
61
61
 
@@ -80,7 +80,12 @@ export const BASE_STYLE = `
80
80
  }
81
81
  * { box-sizing: border-box; }
82
82
  html, body, #cell-root, #root { height: 100%; margin: 0; }
83
- * { overscroll-behavior: none; }
83
+ /* The page and its scrolling areas do not bounce at the ends. Only scrolling areas of their own: on any
84
+ other element (one with overflow: hidden is a scroll container too) it stops the wheel from reaching
85
+ the area under it, and nothing scrolls while the pointer is over a card or a code block. */
86
+ html, body, .ta-scroll, .ta-chat-scroll, .ta-side-scroll, .ta-rail-items, .ta-side-view-body, .ta-editor-body,
87
+ .ta-workbench-launcher, .ta-workbench-menu, .ta-settings-scroll, .ta-settings-toc, .ta-plus-items, .ta-commands,
88
+ .ta-folder-list, .ta-auth-list { overscroll-behavior: none; }
84
89
  body { background: var(--ta-main); color: var(--ta-text); font: 14px/1.6 var(--ta-sans); -webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: transparent; }
85
90
  button, input, select, textarea { font: inherit; color: inherit; }
86
91
  button { cursor: pointer; }
@@ -169,20 +169,25 @@ export const LAYOUT_STYLE = `
169
169
  .ta-queue-back { align-self: flex-end; color: var(--ta-muted); }
170
170
  .ta-queue-back:hover { color: var(--ta-text); }
171
171
  .ta-account { position: relative; display: grid; place-items: center; width: 40px; height: 40px; flex: none; }
172
- .ta-account-button { position: relative; width: 32px; height: 32px; display: grid; place-items: center; border-radius: 50%; border: 1px solid var(--ta-border-field); background: var(--ta-raised); color: var(--ta-secondary); padding: 0; transition: background 120ms, color 120ms, border-color 120ms; }
173
- .ta-account-button:hover, .ta-account-button.ta-on { color: var(--ta-text); border-color: var(--ta-border-focus); }
174
- .ta-account-dot { position: absolute; right: -1px; bottom: -1px; width: 9px; height: 9px; border-radius: 50%; box-shadow: 0 0 0 2px var(--ta-side); background: var(--ta-muted); }
175
- .ta-account-dot.ta-ok { background: var(--ta-green); }
176
- .ta-account-dot.ta-warn { background: var(--ta-yellow); }
177
- .ta-account-menu { position: absolute; left: calc(100% + 10px); bottom: 0; z-index: 45; width: 264px; display: flex; flex-direction: column; gap: 2px; padding: 6px; border-radius: 12px; border: 1px solid var(--ta-border); background: var(--ta-side); box-shadow: var(--ta-shadow-pop); animation: ta-fade 120ms ease-out; }
178
- .ta-account-head { display: flex; align-items: center; gap: 10px; padding: 6px 8px; min-width: 0; }
179
- .ta-account-figure { width: 32px; height: 32px; flex: none; display: grid; place-items: center; border-radius: 50%; background: var(--ta-raised); color: var(--ta-secondary); }
180
- .ta-account-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.35; }
181
- .ta-account-title { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; }
182
- .ta-account-state { display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; font-weight: 400; color: var(--ta-muted); }
183
- .ta-account-sub { font-size: 11.5px; color: var(--ta-muted); }
172
+ .ta-account-button { position: relative; width: 40px; height: 40px; display: grid; place-items: center; border: 0; border-radius: 10px; background: transparent; padding: 0; transition: background 150ms; }
173
+ .ta-account-button:hover, .ta-account-button.ta-on { background: var(--ta-hover); }
174
+ .ta-account-figure { width: 28px; height: 28px; flex: none; display: grid; place-items: center; border-radius: 50%; background: color-mix(in srgb, var(--ta-green) 14%, var(--ta-main)); color: color-mix(in srgb, var(--ta-green) 75%, var(--ta-text)); font-size: 13px; font-weight: 600; line-height: 1; letter-spacing: 0; user-select: none; }
175
+ .ta-account-figure.ta-large { width: 36px; height: 36px; font-size: 15px; }
176
+ .ta-account-dot { position: absolute; right: 5px; bottom: 5px; width: 8px; height: 8px; border-radius: 50%; background: var(--ta-yellow); box-shadow: 0 0 0 2px var(--ta-side); }
177
+ .ta-account-menu { position: absolute; left: calc(100% + 10px); bottom: 0; z-index: 45; width: 248px; display: flex; flex-direction: column; gap: 2px; padding: 6px; border-radius: 12px; border: 1px solid var(--ta-border); background: var(--ta-side); box-shadow: var(--ta-shadow-pop); animation: ta-fade 120ms ease-out; }
178
+ .ta-account-head { display: flex; align-items: center; gap: 10px; padding: 8px 8px 10px; min-width: 0; }
179
+ .ta-account-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; line-height: 1.35; }
180
+ .ta-account-title { font-size: 13.5px; font-weight: 600; color: var(--ta-text); }
181
+ .ta-account-sub { font-size: 12px; color: var(--ta-muted); }
182
+ .ta-account-service { display: flex; align-items: center; gap: 8px; height: 32px; margin: 0 2px 2px; padding: 0 10px; border-radius: 8px; background: var(--ta-raised); font-size: 12.5px; color: var(--ta-secondary); }
183
+ .ta-account-service-name { flex: 1; min-width: 0; }
184
+ .ta-account-service-state { flex: none; font-size: 12px; color: var(--ta-muted); font-variant-numeric: tabular-nums; }
185
+ .ta-account-state { width: 7px; height: 7px; flex: none; border-radius: 50%; background: var(--ta-muted); }
186
+ .ta-account-state.ta-ok { background: var(--ta-green); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ta-green) 18%, transparent); }
187
+ .ta-account-state.ta-warn { background: var(--ta-yellow); }
184
188
  .ta-account-sep { height: 1px; margin: 4px 2px; background: var(--ta-border-muted); }
185
- .ta-account-item { display: flex; align-items: center; gap: 10px; height: 32px; padding: 0 8px; border: 0; border-radius: 8px; background: transparent; text-align: left; font-size: 13px; color: var(--ta-text); }
189
+ .ta-account-item { display: flex; align-items: center; gap: 10px; height: 32px; padding: 0 10px; border: 0; border-radius: 8px; background: transparent; text-align: left; font-size: 13px; color: var(--ta-text); }
190
+ .ta-account-item > svg, .ta-account-item > .ta-icon { flex: none; color: var(--ta-secondary); }
186
191
  .ta-account-item:hover { background: var(--ta-hover); }
187
192
  .ta-account-item.ta-danger { color: var(--ta-red); }
188
193
  @media (max-width: 860px) {
@@ -250,7 +255,7 @@ export const LAYOUT_STYLE = `
250
255
  .ta-web-dialog .ta-modal-title { font-size: 16px; }
251
256
  .ta-folder-bar { display: flex; align-items: center; gap: 6px; }
252
257
  .ta-folder-bar .ta-field input { font-family: var(--ta-mono); font-size: 12.5px; }
253
- .ta-folder-list { list-style: none; flex: 1; min-height: 180px; overflow-y: auto; margin: 0; padding: 4px; border: 1px solid var(--ta-border-muted); border-radius: 10px; }
258
+ .ta-folder-list { list-style: none; flex: none; height: min(340px, 44vh); overflow-y: auto; margin: 0; padding: 4px; border: 1px solid var(--ta-border-muted); border-radius: 10px; }
254
259
  .ta-folder-item { display: flex; align-items: center; gap: 10px; width: 100%; height: 34px; padding: 0 10px; border: 0; border-radius: 8px; background: transparent; color: var(--ta-body); font-size: 13px; text-align: left; }
255
260
  .ta-folder-item:hover { background: var(--ta-hover); color: var(--ta-text); }
256
261
  .ta-folder-item > .ta-icon:first-child, .ta-folder-chevron { color: var(--ta-muted); }
@@ -258,7 +263,35 @@ export const LAYOUT_STYLE = `
258
263
  .ta-folder-item:hover .ta-folder-chevron { opacity: 1; }
259
264
  .ta-folder-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
260
265
  .ta-folder-note { display: flex; justify-content: center; padding: 28px 8px; color: var(--ta-muted); font-size: 12.5px; }
261
- .ta-folder-failed { color: var(--ta-red); font-size: 12.5px; }
266
+ .ta-folder-failed { color: var(--ta-red); font-size: 12.5px; overflow-wrap: anywhere; }
267
+ .ta-folder-note { gap: 8px; align-items: center; }
268
+ .ta-folder-step { color: var(--ta-secondary); }
269
+ .ta-folder-unreachable { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 36px 16px; text-align: center; font-size: 13px; color: var(--ta-text); }
270
+ .ta-folder-unreachable span { max-width: 100%; color: var(--ta-muted); font-family: var(--ta-mono); font-size: 12px; overflow-wrap: anywhere; }
271
+ /* The machines of the picker: this one, the hosts of ssh, + another */
272
+ .ta-modal.ta-folder { width: min(640px, 100%); }
273
+ .ta-folder-machines { flex-wrap: nowrap; overflow-x: auto; margin: 0; padding: 0 0 2px; gap: 6px; scrollbar-width: none; }
274
+ .ta-folder-machines::-webkit-scrollbar { display: none; }
275
+ .ta-folder-machines .ta-icon-button { flex: none; width: 28px; height: 28px; border-radius: 14px; }
276
+ .ta-folder-add input { width: 230px; height: 28px; padding: 0 11px; border-radius: 14px; border: 1px solid var(--ta-border-field); background: var(--ta-input); color: var(--ta-text); font-size: 12.5px; outline: none; }
277
+ .ta-folder-add input:focus { border-color: var(--ta-border-focus); }
278
+ .ta-folder-add input[aria-invalid='true'] { border-color: var(--ta-red); }
279
+ .ta-folder-add input { width: 300px; }
280
+ .ta-remote-state { display: inline-flex; align-items: center; gap: 6px; }
281
+ .ta-remote-light { width: 7px; height: 7px; border-radius: 50%; background: var(--ta-border-field); flex: none; }
282
+ .ta-remote-light.ta-on { background: var(--ta-green); }
283
+ .ta-remote-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ta-green); flex: none; }
284
+ /* The host of a directory of another machine, before its name */
285
+ .ta-host-tag { display: inline-flex; align-items: center; flex: none; max-width: 120px; height: 17px; padding: 0 6px; margin-right: 5px; border-radius: 5px; background: var(--ta-hover-strong); color: var(--ta-secondary); font-size: 11px; font-weight: 500; line-height: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: 1px; }
286
+ .ta-cwd-chip .ta-host-tag { margin: 0 0 0 2px; }
287
+ .ta-cwd-chip .ta-ellipsis { min-width: 0; }
288
+ /* What ssh asks, and the line while a host connects */
289
+ .ta-remote-prompt { width: min(440px, 100%); gap: 12px; }
290
+ .ta-remote-prompt .ta-confirm-title { display: flex; align-items: center; gap: 8px; }
291
+ .ta-remote-message { margin: 0; padding: 8px 10px; border-radius: 8px; background: var(--ta-hover); color: var(--ta-body); font-family: var(--ta-mono); font-size: 12px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 160px; overflow-y: auto; }
292
+ .ta-remote-status { position: fixed; left: 50%; top: calc(12px + env(safe-area-inset-top) + var(--ta-titlebar, 0px)); z-index: 65; transform: translateX(-50%); display: inline-flex; align-items: center; gap: 8px; max-width: calc(100vw - 32px); height: 32px; padding: 0 14px; border-radius: 16px; border: 1px solid var(--ta-border); background: var(--ta-side); box-shadow: var(--ta-shadow-pop); color: var(--ta-secondary); font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; animation: ta-fade 160ms ease-out; }
293
+ /* The picker says it itself */
294
+ body:has(.ta-folder) .ta-remote-status { display: none; }
262
295
  @media (max-width: 860px) {
263
296
  .ta-modal-scrim { align-items: flex-end; padding: 0; }
264
297
  .ta-modal { max-height: 88dvh; border-radius: 16px 16px 0 0; padding-bottom: calc(14px + env(safe-area-inset-bottom)); animation: ta-up 260ms var(--ta-ease); }
@@ -26,6 +26,8 @@ describe('MessageUtil', () => {
26
26
  expect(MessageUtil.basename('/home/me/project/')).toBe('project');
27
27
  expect(MessageUtil.shortPath('/home/me/work/project')).toBe('…/work/project');
28
28
  expect(MessageUtil.shortPath('/srv')).toBe('/srv');
29
+ expect(MessageUtil.shortPath('ssh://dev/data/my/workspace')).toBe('dev:…/my/workspace');
30
+ expect(MessageUtil.shortPath('ssh://dev/srv')).toBe('dev:/srv');
29
31
  });
30
32
 
31
33
  it('finds the entry of a message by its role and time, the newest first', () => {
@@ -1,4 +1,5 @@
1
1
  import type { SessionMessage, ToolRun } from '@celljs/ta-client';
2
+ import { TaLocation } from '@celljs/ta-protocol';
2
3
 
3
4
  export interface ContentBlock {
4
5
  type: string;
@@ -177,12 +178,14 @@ export namespace MessageUtil {
177
178
 
178
179
  /** The last two parts of a path: enough to recognize a project. */
179
180
  export function shortPath(path: string): string {
180
- const parts = path.split(/[\\/]/).filter(Boolean);
181
- return parts.length > 2 ? `…/${parts.slice(-2).join('/')}` : path;
181
+ const remote = TaLocation.parse(path);
182
+ const parts = (remote ? remote.path : path).split(/[\\/]/).filter(Boolean);
183
+ const short = parts.length > 2 ? `…/${parts.slice(-2).join('/')}` : remote ? remote.path || '~' : path;
184
+ return remote ? `${remote.host}:${short}` : short;
182
185
  }
183
186
 
187
+ /** The last name of a path, or of an `ssh://` location (TaLocation). */
184
188
  export function basename(path: string): string {
185
- const parts = path.split(/[\\/]/).filter(Boolean);
186
- return parts[parts.length - 1] ?? path;
189
+ return TaLocation.basename(path);
187
190
  }
188
191
  }
@@ -4,6 +4,8 @@ import type {
4
4
  TaAuthServer,
5
5
  TaPackageServer,
6
6
  TaRelayServer,
7
+ TaRemoteEvent,
8
+ TaRemoteServer,
7
9
  TaServer,
8
10
  TaServerInfo,
9
11
  TaSessionInfo,
@@ -42,6 +44,7 @@ export interface TaWorkbench {
42
44
  readonly packages: TaPackageServer;
43
45
  readonly auth: TaAuthServer;
44
46
  readonly relay: TaRelayServer;
47
+ readonly remote: TaRemoteServer;
45
48
  subscribe(listener: (state: WorkbenchState) => void): () => void;
46
49
  /** Signs in with the token of the URL fragment (`ta serve --web`) or the one this page used before. */
47
50
  start(): Promise<void>;
@@ -63,4 +66,9 @@ export interface TaWorkbench {
63
66
  onExtensionData(listener: (name: string, keys: string[], cwd?: string) => void): () => void;
64
67
  /** What happens during the sign-ins of this page (TaAuthServer.login); the listener stays across reconnections. */
65
68
  onAuthEvent(listener: (loginId: string, event: TaAuthEvent) => void): () => void;
69
+ /**
70
+ * What happens with the remote machines (TaRemoteServer.watch, which the page calls on each connection):
71
+ * the prompts that wait come again as `prompt` events after a reconnection. The listener stays.
72
+ */
73
+ onRemoteEvent(listener: (event: TaRemoteEvent) => void): () => void;
66
74
  }
@@ -6,6 +6,8 @@ import {
6
6
  type TaAuthServer,
7
7
  type TaPackageServer,
8
8
  type TaRelayServer,
9
+ type TaRemoteEvent,
10
+ type TaRemoteServer,
9
11
  type TaServer,
10
12
  type TaSettingsServer,
11
13
  type TaTerminalServer,
@@ -83,6 +85,7 @@ export class TaWorkbenchImpl implements TaWorkbench {
83
85
  protected readonly terminalListeners = new Set<(terminalId: string, data: string, exitCode?: number) => void>();
84
86
  protected readonly extensionDataListeners = new Set<(name: string, keys: string[], cwd?: string) => void>();
85
87
  protected readonly authListeners = new Set<(loginId: string, event: TaAuthEvent) => void>();
88
+ protected readonly remoteListeners = new Set<(event: TaRemoteEvent) => void>();
86
89
 
87
90
  get state(): WorkbenchState {
88
91
  return this.current;
@@ -116,6 +119,10 @@ export class TaWorkbenchImpl implements TaWorkbench {
116
119
  return this.signedIn().relay;
117
120
  }
118
121
 
122
+ get remote(): TaRemoteServer {
123
+ return this.signedIn().remote;
124
+ }
125
+
119
126
  protected signedIn(): TaConnection {
120
127
  if (!this.connection) {
121
128
  throw new Error('Not signed in to ta serve.');
@@ -138,6 +145,11 @@ export class TaWorkbenchImpl implements TaWorkbench {
138
145
  return () => this.authListeners.delete(listener);
139
146
  }
140
147
 
148
+ onRemoteEvent(listener: (event: TaRemoteEvent) => void): () => void {
149
+ this.remoteListeners.add(listener);
150
+ return () => this.remoteListeners.delete(listener);
151
+ }
152
+
141
153
  async setWorkingDirectory(cwd: string): Promise<void> {
142
154
  this.update({ server: await this.server.setWorkingDirectory(cwd) });
143
155
  }
@@ -239,12 +251,15 @@ export class TaWorkbenchImpl implements TaWorkbench {
239
251
  listener(loginId, event);
240
252
  }
241
253
  });
254
+ connection.onRemoteEvent(event => this.emitRemote(event));
242
255
  connection.onClose(() => this.update({ status: 'offline' }));
243
256
  connection.onReconnecting(delayMs => this.update({ retryAt: Date.now() + delayMs }));
244
257
  connection.onOpen(() => {
245
258
  this.update({ status: 'online', retryAt: undefined });
246
259
  void this.refresh();
260
+ void this.watchRemote(connection);
247
261
  });
262
+ void this.watchRemote(connection);
248
263
  this.update({ status: 'online', error: undefined, retryAt: undefined, server: await connection.server.getServerInfo() });
249
264
  await this.refresh();
250
265
  }
@@ -280,6 +295,20 @@ export class TaWorkbenchImpl implements TaWorkbench {
280
295
  return new SessionStore(this.connection, sessionId, options);
281
296
  }
282
297
 
298
+ /** The prompts of ssh that wait for an answer, from now and those already there. */
299
+ protected async watchRemote(connection: TaConnection): Promise<void> {
300
+ const prompts = await connection.remote.watch().catch(() => []);
301
+ for (const prompt of prompts) {
302
+ this.emitRemote({ type: 'prompt', prompt });
303
+ }
304
+ }
305
+
306
+ protected emitRemote(event: TaRemoteEvent): void {
307
+ for (const listener of this.remoteListeners) {
308
+ listener(event);
309
+ }
310
+ }
311
+
283
312
  protected stopRetrying(): void {
284
313
  this.generation++;
285
314
  clearTimeout(this.retryTimer);