@electerm/electerm-react 2.3.198 → 2.4.16

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 (72) hide show
  1. package/client/common/clipboard.js +1 -1
  2. package/client/common/constants.js +2 -2
  3. package/client/common/download.jsx +3 -2
  4. package/client/common/error-handler.jsx +5 -9
  5. package/client/common/fetch-from-server.js +1 -1
  6. package/client/common/fetch.jsx +5 -5
  7. package/client/common/icon-helpers.jsx +16 -0
  8. package/client/common/parse-json-safe.js +1 -1
  9. package/client/common/pre.js +0 -7
  10. package/client/common/sftp.js +1 -1
  11. package/client/common/terminal-theme.js +1 -1
  12. package/client/common/transfer.js +2 -2
  13. package/client/common/upgrade.js +2 -2
  14. package/client/components/ai/ai-chat.jsx +10 -1
  15. package/client/components/auth/login.jsx +1 -1
  16. package/client/components/bg/css-overwrite.jsx +1 -1
  17. package/client/components/bookmark-form/form-renderer.jsx +3 -2
  18. package/client/components/common/input-auto-focus.jsx +1 -1
  19. package/client/components/common/message.jsx +131 -0
  20. package/client/components/common/message.styl +58 -0
  21. package/client/components/common/modal.jsx +176 -0
  22. package/client/components/common/modal.styl +22 -0
  23. package/client/components/common/notification-with-details.jsx +1 -1
  24. package/client/components/common/notification.jsx +94 -0
  25. package/client/components/common/notification.styl +51 -0
  26. package/client/components/main/connection-hopping-warnning.jsx +1 -3
  27. package/client/components/main/error-wrapper.jsx +3 -2
  28. package/client/components/main/main.jsx +4 -11
  29. package/client/components/main/upgrade.jsx +6 -4
  30. package/client/components/profile/profile-form-elem.jsx +1 -1
  31. package/client/components/quick-commands/quick-commands-box.jsx +5 -2
  32. package/client/components/quick-commands/quick-commands-form-elem.jsx +1 -1
  33. package/client/components/rdp/rdp-session.jsx +2 -2
  34. package/client/components/session/session.jsx +4 -9
  35. package/client/components/setting-panel/deep-link-control.jsx +2 -1
  36. package/client/components/setting-panel/keyword-input.jsx +60 -0
  37. package/client/components/setting-panel/keywords-form.jsx +2 -7
  38. package/client/components/setting-panel/setting-common.jsx +1 -1
  39. package/client/components/setting-panel/setting-terminal.jsx +1 -1
  40. package/client/components/setting-panel/tab-settings.jsx +1 -1
  41. package/client/components/setting-sync/setting-sync-form.jsx +53 -3
  42. package/client/components/setting-sync/setting-sync.jsx +2 -1
  43. package/client/components/sftp/owner-list.js +6 -6
  44. package/client/components/sftp/sftp-entry.jsx +6 -4
  45. package/client/components/shortcuts/shortcut-editor.jsx +2 -2
  46. package/client/components/ssh-config/ssh-config-load-notify.jsx +3 -2
  47. package/client/components/tabs/tab.jsx +1 -1
  48. package/client/components/tabs/workspace-save-modal.jsx +2 -1
  49. package/client/components/terminal/attach-addon-custom.js +142 -26
  50. package/client/components/terminal/command-tracker-addon.js +164 -53
  51. package/client/components/terminal/highlight-addon.js +84 -43
  52. package/client/components/terminal/shell.js +138 -0
  53. package/client/components/terminal/terminal-command-dropdown.jsx +3 -0
  54. package/client/components/terminal/terminal.jsx +165 -103
  55. package/client/components/theme/theme-form.jsx +2 -1
  56. package/client/components/tree-list/bookmark-transport.jsx +27 -5
  57. package/client/components/vnc/vnc-session.jsx +1 -1
  58. package/client/components/widgets/widget-notification-with-details.jsx +1 -1
  59. package/client/store/common.js +5 -2
  60. package/client/store/db-upgrade.js +1 -1
  61. package/client/store/init-state.js +2 -1
  62. package/client/store/load-data.js +2 -2
  63. package/client/store/mcp-handler.js +9 -50
  64. package/client/store/setting.js +1 -3
  65. package/client/store/store.js +2 -1
  66. package/client/store/sync.js +14 -8
  67. package/client/store/system-menu.js +2 -1
  68. package/client/store/tab.js +1 -1
  69. package/client/store/widgets.js +1 -3
  70. package/package.json +1 -1
  71. package/client/common/track.js +0 -7
  72. package/client/components/batch-op/batch-op-entry.jsx +0 -13
@@ -1,6 +1,39 @@
1
1
  export class KeywordHighlighterAddon {
2
2
  constructor (keywords) {
3
- this.keywords = keywords
3
+ this.keywords = keywords || []
4
+ this.compiledPatterns = this.compilePatterns()
5
+ }
6
+
7
+ // Pre-compile all regex patterns once for better performance
8
+ compilePatterns = () => {
9
+ const patterns = []
10
+ for (const obj of this.keywords) {
11
+ const { keyword, color = 'red' } = obj || {}
12
+ if (keyword) {
13
+ try {
14
+ patterns.push({
15
+ regex: new RegExp(keyword, 'gi'),
16
+ colorCode: this.getColorCode(color)
17
+ })
18
+ } catch (e) {
19
+ console.error('Invalid keyword regex:', keyword, e)
20
+ }
21
+ }
22
+ }
23
+ return patterns
24
+ }
25
+
26
+ getColorCode = (color) => {
27
+ const colorMap = {
28
+ green: '\u001b[32m',
29
+ yellow: '\u001b[33m',
30
+ blue: '\u001b[34m',
31
+ magenta: '\u001b[35m',
32
+ cyan: '\u001b[36m',
33
+ white: '\u001b[37m',
34
+ red: '\u001b[31m'
35
+ }
36
+ return colorMap[color] || colorMap.red
4
37
  }
5
38
 
6
39
  escape = str => {
@@ -8,63 +41,71 @@ export class KeywordHighlighterAddon {
8
41
  .replace(/\033/g, '\\033')
9
42
  }
10
43
 
11
- colorize = (color) => {
12
- // Use a switch statement to map color names to ANSI codes
13
- switch (color) {
14
- case 'green':
15
- return '\u001b[32m$&\u001b[0m'
16
- case 'yellow':
17
- return '\u001b[33m$&\u001b[0m'
18
- case 'blue':
19
- return '\u001b[34m$&\u001b[0m'
20
- case 'magenta':
21
- return '\u001b[35m$&\u001b[0m'
22
- case 'cyan':
23
- return '\u001b[36m$&\u001b[0m'
24
- case 'white':
25
- return '\u001b[37m$&\u001b[0m'
26
- default:
27
- return '\u001b[31m$&\u001b[0m'
44
+ highlightKeywords = (text) => {
45
+ // Early exit if no patterns
46
+ if (this.compiledPatterns.length === 0) {
47
+ return text
28
48
  }
29
- }
30
49
 
31
- highlightKeywords = (text) => {
32
- for (const obj of this.keywords) {
33
- const {
34
- keyword,
35
- color = 'red'
36
- } = obj || {}
37
- if (keyword) {
38
- try {
39
- const regex = new RegExp(`(${keyword})`, 'gi')
40
- if (regex.test(text)) {
41
- return text.replace(regex, this.colorize(color))
42
- }
43
- } catch (e) {
44
- window.store.onError(e)
45
- }
50
+ // Split text into segments: ANSI/OSC sequences vs plain text
51
+ // Match OSC sequences (ESC ] ... BEL or ESC ] ... ESC \) and CSI sequences (ESC [ ... letter)
52
+ // Use String.fromCharCode to avoid lint warnings about control characters
53
+ const ESC = String.fromCharCode(27) // \x1b
54
+ const BEL = String.fromCharCode(7) // \x07
55
+ // eslint-disable-next-line no-control-regex
56
+ const ansiPattern = new RegExp('(' + ESC + '\\][^' + BEL + ESC + ']*(?:' + BEL + '|' + ESC + '\\\\)|' + ESC + '\\[[0-9;]*[A-Za-z])', 'g')
57
+
58
+ const segments = []
59
+ let lastIndex = 0
60
+ let match
61
+
62
+ while ((match = ansiPattern.exec(text)) !== null) {
63
+ // Add plain text before this sequence
64
+ if (match.index > lastIndex) {
65
+ segments.push({ type: 'text', content: text.slice(lastIndex, match.index) })
46
66
  }
67
+ // Add the ANSI sequence (don't highlight)
68
+ segments.push({ type: 'ansi', content: match[0] })
69
+ lastIndex = ansiPattern.lastIndex
70
+ }
71
+
72
+ // Add remaining plain text
73
+ if (lastIndex < text.length) {
74
+ segments.push({ type: 'text', content: text.slice(lastIndex) })
47
75
  }
48
- return text
76
+
77
+ // Highlight only plain text segments
78
+ const result = segments.map(seg => {
79
+ if (seg.type === 'ansi') {
80
+ return seg.content
81
+ }
82
+ let content = seg.content
83
+ for (const { regex, colorCode } of this.compiledPatterns) {
84
+ regex.lastIndex = 0
85
+ content = content.replace(regex, (m) => `${colorCode}${m}\u001b[0m`)
86
+ }
87
+ return content
88
+ }).join('')
89
+
90
+ return result
49
91
  }
50
92
 
51
93
  activate (terminal) {
52
94
  this.terminal = terminal
53
- // Override the write method to automatically highlight keywords
54
- const originalWrite = terminal.write
55
- terminal.write = (data) => {
56
- originalWrite.call(
57
- terminal,
58
- terminal.displayRaw ? this.escape(data) : this.highlightKeywords(data)
95
+ // Store the original write method properly bound to terminal
96
+ this.originalWrite = terminal.write.bind(terminal)
97
+ const self = this
98
+ terminal.write = function (data) {
99
+ self.originalWrite(
100
+ terminal.displayRaw ? self.escape(data) : self.highlightKeywords(data)
59
101
  )
60
102
  }
61
- this.originalWrite = originalWrite
62
103
  }
63
104
 
64
105
  dispose () {
65
106
  // Restore the original write method when disposing the addon
66
107
  this.terminal.write = this.originalWrite
67
108
  this.originalWrite = null
68
- this.term = null
109
+ this.terminal = null
69
110
  }
70
111
  }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Client-side Shell Integration Commands
3
+ *
4
+ * These are minimal shell integration commands that can be sent directly
5
+ * to a local or remote shell from the frontend after connection.
6
+ * They enable OSC 633 command tracking without needing server-side file sourcing.
7
+ *
8
+ * OSC 633 Protocol:
9
+ * - OSC 633 ; A - Prompt started
10
+ * - OSC 633 ; B - Command input started (ready for typing)
11
+ * - OSC 633 ; C - Command execution started
12
+ * - OSC 633 ; D ; <exitCode> - Command finished
13
+ * - OSC 633 ; E ; <command> - Command line being executed
14
+ * - OSC 633 ; P ; Cwd=<path> - Current working directory
15
+ */
16
+
17
+ /* eslint-disable no-template-curly-in-string, no-useless-escape */
18
+
19
+ /**
20
+ * Get inline shell integration command for bash (one-liner format)
21
+ * Properly formatted for semicolon joining
22
+ */
23
+ function getBashInlineIntegration () {
24
+ // Each statement is complete and can be joined with semicolons
25
+ return [
26
+ 'if [[ $- == *i* ]] && [[ -z "${ELECTERM_SHELL_INTEGRATION:-}" ]]',
27
+ 'then export ELECTERM_SHELL_INTEGRATION=1',
28
+ '__e_esc() { local v="$1"; v="${v//\\\\/\\\\\\\\}"; v="${v//;/\\\\x3b}"; printf \'%s\' "$v"; }',
29
+ '__e_pre() { [[ "$BASH_COMMAND" == "$PROMPT_COMMAND" ]] && return; [[ "$BASH_COMMAND" == "__e_"* ]] && return; [[ "${__e_in:-0}" == "0" ]] && { __e_in=1; printf \'\\e]633;E;%s\\a\\e]633;C\\a\' "$(__e_esc "$BASH_COMMAND")"; }; }',
30
+ '__e_cmd() { local c="$?"; [[ "${__e_in:-0}" == "1" ]] && { printf \'\\e]633;D;%s\\a\' "$c"; __e_in=0; }; printf \'\\e]633;P;Cwd=%s\\a\\e]633;A\\a\' "$(__e_esc "$PWD")"; return "$c"; }',
31
+ 'trap \'__e_pre\' DEBUG',
32
+ 'PROMPT_COMMAND="__e_cmd${PROMPT_COMMAND:+; $PROMPT_COMMAND}"',
33
+ 'fi'
34
+ ].join('; ')
35
+ }
36
+
37
+ /**
38
+ * Get inline shell integration command for zsh (one-liner format)
39
+ * Properly formatted for semicolon joining
40
+ */
41
+ function getZshInlineIntegration () {
42
+ // Each statement is complete and can be joined with semicolons
43
+ // Note: 'then' must have a space/newline before the next command, not semicolon
44
+ return [
45
+ 'if [[ -o interactive ]] && [[ -z "${ELECTERM_SHELL_INTEGRATION:-}" ]]',
46
+ 'then export ELECTERM_SHELL_INTEGRATION=1',
47
+ '__e_esc() { local v="$1"; v="${v//\\\\/\\\\\\\\}"; v="${v//;/\\\\x3b}"; builtin printf \'%s\' "$v"; }',
48
+ '__e_preexec() { __e_cmd="$1"; builtin printf \'\\e]633;E;%s\\a\\e]633;C\\a\' "$(__e_esc "$1")"; }',
49
+ '__e_precmd() { local c="$?"; [[ -n "$__e_cmd" ]] && builtin printf \'\\e]633;D;%s\\a\' "$c"; __e_cmd=""; builtin printf \'\\e]633;P;Cwd=%s\\a\\e]633;A\\a\' "$(__e_esc "$PWD")"; }',
50
+ 'autoload -Uz add-zsh-hook',
51
+ 'add-zsh-hook precmd __e_precmd',
52
+ 'add-zsh-hook preexec __e_preexec',
53
+ 'fi'
54
+ ].join('; ')
55
+ }
56
+
57
+ /**
58
+ * Get inline shell integration command for fish (one-liner format)
59
+ */
60
+ function getFishInlineIntegration () {
61
+ return [
62
+ 'if status is-interactive; and not set -q ELECTERM_SHELL_INTEGRATION',
63
+ 'set -g ELECTERM_SHELL_INTEGRATION 1',
64
+ 'function __e_esc; echo $argv | string replace -a \'\\\\\' \'\\\\\\\\\' | string replace -a \';\' \'\\\\x3b\'; end',
65
+ 'function __e_prompt --on-event fish_prompt; printf \'\\e]633;A\\a\\e]633;P;Cwd=%s\\a\' (__e_esc "$PWD"); end',
66
+ 'function __e_preexec --on-event fish_preexec; printf \'\\e]633;E;%s\\a\\e]633;C\\a\' (__e_esc "$argv"); end',
67
+ 'function __e_postexec --on-event fish_postexec; printf \'\\e]633;D;%s\\a\' $status; end',
68
+ 'end'
69
+ ].join('; ')
70
+ }
71
+
72
+ /**
73
+ * Get shell integration command based on detected shell type
74
+ * @param {string} shellType - 'bash', 'zsh', or 'fish'
75
+ * @returns {string} Shell integration command to send
76
+ */
77
+ export function getInlineShellIntegration (shellType) {
78
+ switch (shellType) {
79
+ case 'bash':
80
+ return getBashInlineIntegration()
81
+ case 'zsh':
82
+ return getZshInlineIntegration()
83
+ case 'fish':
84
+ return getFishInlineIntegration()
85
+ default:
86
+ // Try bash as default for sh-compatible shells
87
+ return getBashInlineIntegration()
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Wrap shell integration command for execution
93
+ * Now simplified since output suppression is handled at the attach addon level
94
+ * @param {string} cmd - Shell integration command
95
+ * @param {string} shellType - Shell type (unused, kept for API compatibility)
96
+ * @returns {string} Command ready to send to terminal
97
+ */
98
+ export function wrapSilent (cmd, shellType) {
99
+ // Escape single quotes for embedding in single-quoted string
100
+ const escaped = cmd.replace(/'/g, "'\\''")
101
+ // The leading space prevents the command from being saved to history
102
+ // The eval wrapper ensures proper execution
103
+ return ` eval '${escaped}' 2>/dev/null\r`
104
+ }
105
+
106
+ /**
107
+ * Get complete shell integration command ready to send
108
+ * @param {string} shellType - 'bash', 'zsh', or 'fish'
109
+ * @returns {string} Complete command to send to terminal
110
+ */
111
+ export function getShellIntegrationCommand (shellType = 'bash') {
112
+ const cmd = getInlineShellIntegration(shellType)
113
+ return wrapSilent(cmd, shellType)
114
+ }
115
+
116
+ /**
117
+ * Detect shell type from shell path or login script
118
+ * @param {string} shellPath - Path to shell executable or login script
119
+ * @returns {string} Shell type: 'bash', 'zsh', 'fish', or 'bash' (default)
120
+ */
121
+ export function detectShellType (shellPath = '') {
122
+ if (!shellPath) return 'bash'
123
+
124
+ const normalizedPath = shellPath.toLowerCase()
125
+
126
+ if (normalizedPath.includes('zsh')) {
127
+ return 'zsh'
128
+ } else if (normalizedPath.includes('fish')) {
129
+ return 'fish'
130
+ } else if (normalizedPath.includes('bash')) {
131
+ return 'bash'
132
+ } else if (normalizedPath.includes('sh')) {
133
+ // Generic sh, try bash compatibility
134
+ return 'bash'
135
+ }
136
+
137
+ return 'bash'
138
+ }
@@ -171,6 +171,7 @@ export default class TerminalCmdSuggestions extends Component {
171
171
  const { activeTabId } = window.store
172
172
  const terminal = refs.get('term-' + activeTabId)
173
173
  if (!terminal) {
174
+ console.log('No active terminal found')
174
175
  return
175
176
  }
176
177
 
@@ -186,6 +187,8 @@ export default class TerminalCmdSuggestions extends Component {
186
187
  txt = pre + command
187
188
  }
188
189
  terminal.attachAddon._sendData(txt)
190
+ // Update the terminal's currentInput to reflect the full command
191
+ terminal.setCurrentInput(command)
189
192
  terminal.term.focus()
190
193
  this.closeSuggestions()
191
194
  }