@electerm/electerm-react 5.3.26 → 5.5.9

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 (146) hide show
  1. package/README.md +246 -12
  2. package/client/common/choose-save-folder.js +21 -2
  3. package/client/common/constants.js +20 -6
  4. package/client/common/create-title.jsx +6 -0
  5. package/client/common/default-setting.js +15 -0
  6. package/client/common/init-setting-item.js +5 -0
  7. package/client/common/platform.js +24 -0
  8. package/client/components/ai/agent-tool-call-card.jsx +10 -2
  9. package/client/components/ai/agent.js +4 -1
  10. package/client/components/ai/ai-attachments.js +87 -0
  11. package/client/components/ai/ai-chat-empty.jsx +43 -0
  12. package/client/components/ai/ai-chat-entry.jsx +13 -4
  13. package/client/components/ai/ai-chat-history-item.jsx +28 -4
  14. package/client/components/ai/ai-chat-history.jsx +2 -1
  15. package/client/components/ai/ai-chat.jsx +104 -13
  16. package/client/components/ai/ai-config-props.js +4 -130
  17. package/client/components/ai/ai-config.jsx +135 -15
  18. package/client/components/ai/ai-presets.js +173 -0
  19. package/client/components/ai/ai.styl +83 -0
  20. package/client/components/bg/css-overwrite.jsx +11 -0
  21. package/client/components/bookmark-form/bookmark-schema.js +4 -0
  22. package/client/components/bookmark-form/common/fields.jsx +3 -0
  23. package/client/components/bookmark-form/config/common-fields.js +12 -0
  24. package/client/components/bookmark-form/config/local.js +3 -2
  25. package/client/components/bookmark-form/config/serial.js +3 -2
  26. package/client/components/bookmark-form/config/ssh.js +2 -1
  27. package/client/components/bookmark-form/config/telnet.js +3 -2
  28. package/client/components/common/item-filter.jsx +76 -0
  29. package/client/components/common/item-filter.styl +54 -0
  30. package/client/components/common/lazy-boundary.jsx +34 -0
  31. package/client/components/common/opacity.jsx +3 -1
  32. package/client/components/footer/cmd-history.jsx +132 -45
  33. package/client/components/footer/cmd-history.styl +42 -4
  34. package/client/components/footer/footer-entry.jsx +35 -5
  35. package/client/components/footer/footer.styl +8 -1
  36. package/client/components/footer/multi-tab-run-modal.jsx +83 -0
  37. package/client/components/footer/tab-select.jsx +34 -17
  38. package/client/components/layout/layout.jsx +23 -31
  39. package/client/components/layout/pixed.js +3 -0
  40. package/client/components/main/main.jsx +55 -28
  41. package/client/components/main/term-fullscreen.styl +9 -3
  42. package/client/components/quick-commands/qm.styl +3 -2
  43. package/client/components/quick-commands/quick-command-create-modal.jsx +60 -0
  44. package/client/components/quick-commands/quick-commands-form-elem.jsx +10 -4
  45. package/client/components/rdp/file-transfer.js +4 -4
  46. package/client/components/remote-monitor/monitor-details.jsx +458 -0
  47. package/client/components/remote-monitor/monitor-details.styl +87 -0
  48. package/client/components/remote-monitor/monitor-model.js +764 -0
  49. package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
  50. package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
  51. package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
  52. package/client/components/remote-monitor/session-monitor.js +361 -0
  53. package/client/components/remote-monitor/use-monitor-details.js +10 -0
  54. package/client/components/remote-monitor/visibility.js +18 -0
  55. package/client/components/session/session-control.jsx +29 -2
  56. package/client/components/session/session.jsx +40 -18
  57. package/client/components/session/sessions.jsx +5 -2
  58. package/client/components/setting-panel/setting-common.jsx +25 -56
  59. package/client/components/setting-panel/setting-modal.jsx +57 -42
  60. package/client/components/setting-panel/setting-terminal.jsx +8 -2
  61. package/client/components/setting-panel/setting.styl +1 -1
  62. package/client/components/setting-panel/tab-triggers.jsx +71 -0
  63. package/client/components/sftp/file-compare-modal.jsx +6 -3
  64. package/client/components/sftp/file-item.jsx +5 -3
  65. package/client/components/sftp/owner-list.js +11 -2
  66. package/client/components/sftp/sftp-entry.jsx +4 -0
  67. package/client/components/side-panel-r/side-panel-r.jsx +2 -2
  68. package/client/components/sidebar/side-panel.jsx +1 -1
  69. package/client/components/sys-menu/menu-btn.jsx +30 -1
  70. package/client/components/tabs/index.jsx +2 -2
  71. package/client/components/tabs/tab-title.jsx +3 -2
  72. package/client/components/tabs/tab.jsx +5 -1
  73. package/client/components/terminal/attach-addon-custom.js +295 -49
  74. package/client/components/terminal/automation/index.js +64 -0
  75. package/client/components/terminal/automation/keys.js +49 -0
  76. package/client/components/terminal/automation/stream-matcher.js +82 -0
  77. package/client/components/terminal/automation/strip-ansi.js +19 -0
  78. package/client/components/terminal/automation/trigger-engine.js +233 -0
  79. package/client/components/terminal/automation/trigger-presets.js +110 -0
  80. package/client/components/terminal/command-tracker-addon.js +31 -1
  81. package/client/components/terminal/mixins/index.js +27 -0
  82. package/client/components/terminal/mixins/term-attach.js +123 -0
  83. package/client/components/terminal/mixins/term-config.jsx +84 -0
  84. package/client/components/terminal/mixins/term-content.js +126 -0
  85. package/client/components/terminal/mixins/term-context-menu.jsx +323 -0
  86. package/client/components/terminal/mixins/term-file-drop.js +149 -0
  87. package/client/components/terminal/mixins/term-init.js +122 -0
  88. package/client/components/terminal/mixins/term-log.jsx +96 -0
  89. package/client/components/terminal/mixins/term-resize.js +87 -0
  90. package/client/components/terminal/mixins/term-socket.js +433 -0
  91. package/client/components/terminal/mixins/term-suggestions.js +224 -0
  92. package/client/components/terminal/mixins/term-theme.js +190 -0
  93. package/client/components/terminal/mixins/term-touch.js +523 -0
  94. package/client/components/terminal/shell-detect.js +37 -0
  95. package/client/components/terminal/shell.js +0 -28
  96. package/client/components/terminal/shortcut-bar-defs.js +30 -4
  97. package/client/components/terminal/shortcut-bar-edit.jsx +40 -9
  98. package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
  99. package/client/components/terminal/shortcut-bar.jsx +17 -10
  100. package/client/components/terminal/shortcut-bar.styl +4 -0
  101. package/client/components/terminal/ssh-reload-state.js +96 -0
  102. package/client/components/terminal/startup-queue.js +433 -0
  103. package/client/components/terminal/terminal-interactive.jsx +32 -0
  104. package/client/components/terminal/terminal-select-text.jsx +69 -0
  105. package/client/components/terminal/terminal.jsx +227 -2064
  106. package/client/components/terminal/terminal.styl +61 -0
  107. package/client/components/terminal/transfer-client-base.js +5 -29
  108. package/client/components/terminal/trzsz-client.js +1 -3
  109. package/client/components/terminal/xmodem-client.js +1 -3
  110. package/client/components/terminal/xterm-loader.js +7 -0
  111. package/client/components/terminal/zmodem-client.js +1 -3
  112. package/client/components/terminal-info/base.jsx +18 -70
  113. package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
  114. package/client/components/terminal-info/terminal-info.jsx +50 -59
  115. package/client/components/terminal-info/terminal-info.styl +6 -49
  116. package/client/components/text-editor/open-text-editor.js +30 -0
  117. package/client/components/text-editor/text-editor-entry.jsx +17 -6
  118. package/client/components/text-editor/text-editor.jsx +14 -0
  119. package/client/components/triggers/trigger-editor.jsx +246 -0
  120. package/client/components/triggers/trigger-form.jsx +200 -0
  121. package/client/components/triggers/trigger-session-modal.jsx +199 -0
  122. package/client/components/triggers/trigger-setting-list.jsx +89 -0
  123. package/client/components/triggers/trigger-transport.jsx +60 -0
  124. package/client/components/triggers/triggers-field.jsx +19 -0
  125. package/client/components/widgets/widgets-list.jsx +11 -1
  126. package/client/css/mobile.styl +29 -8
  127. package/client/store/common.js +12 -1
  128. package/client/store/init-state.js +7 -0
  129. package/client/store/mcp-handler.js +158 -30
  130. package/client/store/setting.js +9 -0
  131. package/client/store/store.js +2 -0
  132. package/client/store/sync.js +2 -0
  133. package/client/store/tab.js +42 -3
  134. package/client/store/trigger.js +118 -0
  135. package/client/store/watch.js +13 -5
  136. package/client/views/index.pug +3 -0
  137. package/package.json +1 -1
  138. package/client/components/terminal-info/activity.jsx +0 -57
  139. package/client/components/terminal-info/data-cols-parser.jsx +0 -54
  140. package/client/components/terminal-info/disk.jsx +0 -45
  141. package/client/components/terminal-info/format-disks.js +0 -45
  142. package/client/components/terminal-info/network.jsx +0 -121
  143. package/client/components/terminal-info/resource.jsx +0 -99
  144. package/client/components/terminal-info/run-cmd.jsx +0 -263
  145. package/client/components/terminal-info/sys-info.jsx +0 -56
  146. package/client/components/terminal-info/up.jsx +0 -17
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Remote shell detection.
3
+ *
4
+ * Kept out of shell.js on purpose: it needs the network (runCmd), which drags
5
+ * the whole app runtime into the import graph. shell.js stays pure so the
6
+ * startup queue can be imported (and unit tested) on its own.
7
+ */
8
+ import { runCmd } from './terminal-apis.js'
9
+ import { detectShellType } from './shell.js'
10
+
11
+ export async function detectRemoteShell (pid) {
12
+ // SSH exec runs under the account shell, so prefer the configured shell path
13
+ // instead of probing for any shell binary installed on the host.
14
+ const cmd = 'printf "%s\n" "$SHELL"'
15
+
16
+ // { silent: true } so this best-effort probe does not emit a transport-level
17
+ // fetch warning; a single, clearer warning is logged below if it fails.
18
+ const r = await runCmd(pid, cmd, { silent: true })
19
+ .catch((err) => {
20
+ // Non-fatal: the interactive shell already opened, so the terminal keeps
21
+ // working. We just can't inject OSC 633 shell integration for command
22
+ // tracking. This commonly happens when the server limits concurrent
23
+ // sessions (sshd MaxSessions) or the account uses a forced/restricted
24
+ // command, which rejects the auxiliary exec channel with
25
+ // "(SSH) Channel open failure: open failed".
26
+ console.warn('detectRemoteShell: exec channel rejected by server, shell integration disabled, falling back to sh —', err?.message || err)
27
+ return 'sh'
28
+ })
29
+
30
+ const shell = r.trim().toLowerCase()
31
+
32
+ if (!shell) {
33
+ return 'sh'
34
+ }
35
+
36
+ return detectShellType(shell)
37
+ }
@@ -15,7 +15,6 @@
15
15
  */
16
16
 
17
17
  /* eslint-disable no-template-curly-in-string, no-useless-escape */
18
- import { runCmd } from './terminal-apis.js'
19
18
 
20
19
  /**
21
20
  * Get inline shell integration command for bash (one-liner format)
@@ -153,30 +152,3 @@ export function getShellIntegrationCommand (shellType = 'bash') {
153
152
  const cmd = getInlineShellIntegration(shellType)
154
153
  return wrapSilent(cmd, shellType)
155
154
  }
156
- export async function detectRemoteShell (pid) {
157
- // SSH exec runs under the account shell, so prefer the configured shell path
158
- // instead of probing for any shell binary installed on the host.
159
- const cmd = 'printf "%s\n" "$SHELL"'
160
-
161
- // { silent: true } so this best-effort probe does not emit a transport-level
162
- // fetch warning; a single, clearer warning is logged below if it fails.
163
- const r = await runCmd(pid, cmd, { silent: true })
164
- .catch((err) => {
165
- // Non-fatal: the interactive shell already opened, so the terminal keeps
166
- // working. We just can't inject OSC 633 shell integration for command
167
- // tracking. This commonly happens when the server limits concurrent
168
- // sessions (sshd MaxSessions) or the account uses a forced/restricted
169
- // command, which rejects the auxiliary exec channel with
170
- // "(SSH) Channel open failure: open failed".
171
- console.warn('detectRemoteShell: exec channel rejected by server, shell integration disabled, falling back to sh —', err?.message || err)
172
- return 'sh'
173
- })
174
-
175
- const shell = r.trim().toLowerCase()
176
-
177
- if (!shell) {
178
- return 'sh'
179
- }
180
-
181
- return detectShellType(shell)
182
- }
@@ -100,9 +100,17 @@ const modifierOptions = [
100
100
  { id: 'meta', label: 'Meta' }
101
101
  ]
102
102
 
103
+ const MODIFIER_IDS = new Set(modifierOptions.map(m => m.id))
104
+
105
+ // the builder's second slot accepts either a modifier (Ctrl/Alt/Shift/Meta)
106
+ // or a bare key, so we need to tell them apart.
107
+ function isModifier (id) {
108
+ return MODIFIER_IDS.has(id)
109
+ }
110
+
103
111
  // compose the bytes to send for a (mods, key) combo.
104
112
  // Ctrl+letter -> the control byte; Alt -> ESC prefix; Shift+letter -> uppercase
105
- function buildComboData (mods, key) {
113
+ function applyMods (mods, key) {
106
114
  const ctrl = mods.includes('ctrl')
107
115
  const alt = mods.includes('alt')
108
116
  const shift = mods.includes('shift')
@@ -118,10 +126,27 @@ function buildComboData (mods, key) {
118
126
  return data
119
127
  }
120
128
 
121
- // human-readable label, e.g. mods ['ctrl','shift'] + key A -> "Ctrl+Shift+A"
122
- function buildComboLabel (mods, key) {
129
+ // `prefix` is an optional first key typed BEFORE `key` — that's how you build
130
+ // two-step combos such as tmux's Ctrl+A then D: the modifiers apply to the
131
+ // prefix only, the trailing key is sent plain.
132
+ // mods [ctrl], prefix A, key D -> Ctrl+A then D
133
+ // mods [ctrl, shift], key D -> Ctrl+Shift+D
134
+ function buildComboData (mods, key, prefix) {
135
+ if (prefix) {
136
+ return applyMods(mods, prefix) + key.data
137
+ }
138
+ return applyMods(mods, key)
139
+ }
140
+
141
+ // human-readable label, e.g.
142
+ // mods ['ctrl','shift'] + key A -> "Ctrl+Shift+A"
143
+ // mods ['ctrl'] + prefix A + key D -> "Ctrl+A D"
144
+ function buildComboLabel (mods, key, prefix) {
123
145
  const byId = new Map(modifierOptions.map(m => [m.id, m.label]))
124
- return [...mods.map(m => byId.get(m) || m), key.label].join('+')
146
+ const head = mods.map(m => byId.get(m) || m)
147
+ return prefix
148
+ ? [...head, prefix.label].join('+') + ' ' + key.label
149
+ : [...head, key.label].join('+')
125
150
  }
126
151
 
127
152
  // full library, de-duplicated by id
@@ -196,6 +221,7 @@ export {
196
221
  candidates,
197
222
  keyOptions,
198
223
  modifierOptions,
224
+ isModifier,
199
225
  buildComboData,
200
226
  buildComboLabel
201
227
  }
@@ -26,6 +26,7 @@ import {
26
26
  defaultActive,
27
27
  keyOptions,
28
28
  modifierOptions,
29
+ isModifier,
29
30
  buildComboData,
30
31
  buildComboLabel
31
32
  } from './shortcut-bar-defs'
@@ -43,6 +44,9 @@ export default function ShortcutBarEdit (props) {
43
44
 
44
45
  const [keyword, setKeyword] = useState('')
45
46
  const [customMod1, setCustomMod1] = useState(undefined)
47
+ // second slot takes a modifier OR a bare key: a modifier stacks onto the
48
+ // first one (Ctrl+Shift+D), a key becomes a prefix typed before the third
49
+ // slot's key (Ctrl+A then D).
46
50
  const [customMod2, setCustomMod2] = useState(undefined)
47
51
  const [customKey, setCustomKey] = useState(undefined)
48
52
 
@@ -70,13 +74,21 @@ export default function ShortcutBarEdit (props) {
70
74
  if (!customKey) {
71
75
  return
72
76
  }
73
- const mods = [customMod1, customMod2].filter(Boolean)
74
77
  const key = keyOptions.find(k => k.id === customKey)
75
78
  if (!key) {
76
79
  return
77
80
  }
78
- const label = buildComboLabel(mods, key)
79
- const data = buildComboData(mods, key)
81
+ const mods = [customMod1].filter(Boolean)
82
+ let prefix
83
+ if (customMod2) {
84
+ if (isModifier(customMod2)) {
85
+ mods.push(customMod2)
86
+ } else {
87
+ prefix = keyOptions.find(k => k.id === customMod2)
88
+ }
89
+ }
90
+ const label = buildComboLabel(mods, key, prefix)
91
+ const data = buildComboData(mods, key, prefix)
80
92
  const id = 'custom-' + label + '-' + Date.now()
81
93
  onSave([...active, { id, label, data, custom: true }])
82
94
  setCustomMod1(undefined)
@@ -88,6 +100,26 @@ export default function ShortcutBarEdit (props) {
88
100
  onSave(defaultActive())
89
101
  }
90
102
 
103
+ // second slot option list: modifiers first, then the same bare-key list the
104
+ // third slot offers (so Ctrl+A then D is expressible).
105
+ const secondOptions = [
106
+ {
107
+ label: e('modifier'),
108
+ options: modifierOptions.map(m => ({
109
+ value: m.id,
110
+ label: m.label,
111
+ disabled: m.id === customMod1
112
+ }))
113
+ },
114
+ {
115
+ label: e('key'),
116
+ options: keyOptions.map(k => ({
117
+ value: k.id,
118
+ label: k.label
119
+ }))
120
+ }
121
+ ]
122
+
91
123
  const kw = keyword.trim().toLowerCase()
92
124
  const filtered = allCandidates.filter(b => {
93
125
  if (!kw) {
@@ -195,15 +227,13 @@ export default function ShortcutBarEdit (props) {
195
227
  <Select
196
228
  value={customMod2}
197
229
  onChange={setCustomMod2}
198
- placeholder={e('modifier') + ' (' + e('optional') + ')'}
230
+ placeholder={e('modifier') + '/' + e('key') + ' (' + e('optional') + ')'}
199
231
  className='shortcut-custom-select'
200
232
  allowClear
233
+ showSearch
234
+ optionFilterProp='label'
201
235
  getPopupContainer={popupContainer}
202
- options={modifierOptions.map(m => ({
203
- value: m.id,
204
- label: m.label,
205
- disabled: m.id === customMod1
206
- }))}
236
+ options={secondOptions}
207
237
  />
208
238
  <Select
209
239
  value={customKey}
@@ -212,6 +242,7 @@ export default function ShortcutBarEdit (props) {
212
242
  className='shortcut-custom-select shortcut-custom-key'
213
243
  allowClear
214
244
  showSearch
245
+ optionFilterProp='label'
215
246
  getPopupContainer={popupContainer}
216
247
  options={keyOptions.map(k => ({
217
248
  value: k.id,
@@ -38,6 +38,7 @@
38
38
 
39
39
  import { lazy, Suspense, useEffect, useState } from 'react'
40
40
  import { auto } from 'manate/react'
41
+ import LazyBoundary from '../common/lazy-boundary'
41
42
 
42
43
  const ShortcutBar = lazy(() => import('./shortcut-bar'))
43
44
 
@@ -166,9 +167,11 @@ function ShortcutBarEntry (props) {
166
167
  return null
167
168
  }
168
169
  return (
169
- <Suspense fallback={null}>
170
- <ShortcutBar {...props} />
171
- </Suspense>
170
+ <LazyBoundary>
171
+ <Suspense fallback={null}>
172
+ <ShortcutBar {...props} />
173
+ </Suspense>
174
+ </LazyBoundary>
172
175
  )
173
176
  }
174
177
 
@@ -56,11 +56,20 @@ function sendToTerminal (data) {
56
56
  }
57
57
  }
58
58
 
59
- // honor the active terminal's DECCKM state for the cursor / Home / End keys
59
+ // honor the active terminal's DECCKM state for the cursor / Home / End keys.
60
+ // custom combos can be multi-key sequences (Ctrl+A then ↑), so match the
61
+ // trailing key too — not only the whole payload.
60
62
  function resolveCursorMode (data, term) {
61
- const app = appCursorMap[data]
62
- if (app && term?.term?.modes?.applicationCursorKeysMode) {
63
- return app
63
+ if (!term?.term?.modes?.applicationCursorKeysMode) {
64
+ return data
65
+ }
66
+ for (const normal in appCursorMap) {
67
+ if (data === normal) {
68
+ return appCursorMap[normal]
69
+ }
70
+ if (data.length > normal.length && data.endsWith(normal)) {
71
+ return data.slice(0, data.length - normal.length) + appCursorMap[normal]
72
+ }
64
73
  }
65
74
  return data
66
75
  }
@@ -135,12 +144,10 @@ function ShortcutBar (props) {
135
144
  // keep the offset in a CSS var consumed by shortcut-bar.styl, and expose the
136
145
  // lifted height so layout.jsx can shrink the terminal accordingly.
137
146
  useEffect(() => {
138
- document.documentElement.style.setProperty(
139
- '--shortcut-bar-kb-offset',
140
- kbOffset ? kbOffset + 'px' : '0px'
141
- )
142
- store.shortcutBarKbOffset = kbOffset
143
- }, [kbOffset])
147
+ const offset = store.shortcutBarVisible ? kbOffset : 0
148
+ document.documentElement.style.setProperty('--shortcut-bar-kb-offset', offset + 'px')
149
+ store.shortcutBarKbOffset = offset
150
+ }, [kbOffset, store.shortcutBarVisible])
144
151
 
145
152
  // reserve layout space + lift the footer while the bar is shown.
146
153
  // expose the bar height as a CSS var so the .styl + footer-lift rule stay
@@ -79,6 +79,10 @@
79
79
  .shortcut-bar-on
80
80
  .main-footer
81
81
  bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
82
+ .remote-monitor-bar
83
+ bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px) + 36px)
84
+ .qm-wrap-tooltip
85
+ bottom calc(var(--footer-stack-height, 36px) + var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
82
86
 
83
87
  // --- edit modal -----------------------------------------------------------
84
88
 
@@ -0,0 +1,96 @@
1
+ const maxCwdLength = 4096
2
+
3
+ // Shell-backed terminal types whose cwd + screen can be restored on reload.
4
+ // Graphical / file-transfer tabs (rdp, vnc, web, ftp, spice, sftp, ...) are
5
+ // intentionally excluded: they have no shell cwd to `cd` back to.
6
+ export const restorableTerminalTypes = new Set([
7
+ 'ssh',
8
+ 'local',
9
+ 'telnet',
10
+ 'serial'
11
+ ])
12
+
13
+ export function isSshTab (tab = {}) {
14
+ return !!tab.host && (!tab.type || tab.type === 'ssh')
15
+ }
16
+
17
+ export function isRestorableTerminalTab (tab = {}) {
18
+ if (!tab.type) {
19
+ // Legacy tabs without an explicit type are ssh (host set) or local
20
+ // (no host) - both are shell terminals.
21
+ return true
22
+ }
23
+ return restorableTerminalTypes.has(tab.type)
24
+ }
25
+
26
+ export function sanitizeSshCwd (cwd) {
27
+ if (typeof cwd !== 'string' || !cwd || cwd.length > maxCwdLength) {
28
+ return ''
29
+ }
30
+ if (/[\0\r\n]/.test(cwd)) {
31
+ return ''
32
+ }
33
+ return cwd
34
+ }
35
+
36
+ export const sanitizeTerminalCwd = sanitizeSshCwd
37
+
38
+ export function quotePosixShellArg (value) {
39
+ const quote = '\''
40
+ const escapedQuote = quote + '"' + quote + '"' + quote
41
+ return quote + value.replace(/'/g, escapedQuote) + quote
42
+ }
43
+
44
+ export function createRestoreCwdCommand (cwd) {
45
+ const safeCwd = sanitizeSshCwd(cwd)
46
+ return safeCwd ? `cd -- ${quotePosixShellArg(safeCwd)}` : ''
47
+ }
48
+
49
+ export function createWindowsRestoreCwdCommand (cwd) {
50
+ const safeCwd = sanitizeSshCwd(cwd)
51
+ if (!safeCwd) {
52
+ return ''
53
+ }
54
+ // Strip control chars already handled by sanitize; escape embedded double
55
+ // quotes for cmd/powershell (`""` is accepted by both as a literal quote).
56
+ const escaped = safeCwd.replace(/"/g, '""')
57
+ return `cd /d "${escaped}"`
58
+ }
59
+
60
+ export function shouldCaptureTerminalReloadState (tab, config = {}) {
61
+ return !!config.restoreTerminalSessionOnReload && isRestorableTerminalTab(tab)
62
+ }
63
+
64
+ // Kept for backward compatibility - now covers all shell terminals
65
+ // (ssh, local, telnet, serial), not just ssh.
66
+ export function shouldCaptureSshReloadState (tab, config = {}) {
67
+ return shouldCaptureTerminalReloadState(tab, config)
68
+ }
69
+
70
+ export function createSshReloadState ({ cwd, screen } = {}) {
71
+ const safeCwd = sanitizeSshCwd(cwd)
72
+ const safeScreen = typeof screen === 'string' ? screen : ''
73
+ if (!safeCwd && !safeScreen) {
74
+ return undefined
75
+ }
76
+ return {
77
+ cwd: safeCwd,
78
+ screen: safeScreen
79
+ }
80
+ }
81
+
82
+ export const createTerminalReloadState = createSshReloadState
83
+
84
+ export function getAlternateBufferSnapshot (buffer) {
85
+ if (!buffer || buffer.type !== 'alternate') {
86
+ return ''
87
+ }
88
+ const lines = []
89
+ for (let i = 0; i < buffer.length; i++) {
90
+ lines.push(buffer.getLine(i)?.translateToString(true) || '')
91
+ }
92
+ while (lines.length && !lines[lines.length - 1]) {
93
+ lines.pop()
94
+ }
95
+ return lines.join('\r\n')
96
+ }