@electerm/electerm-react 3.12.0 → 3.15.28

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.
@@ -6,6 +6,7 @@
6
6
  import uid from '../common/uid'
7
7
  import { settingMap } from '../common/constants'
8
8
  import { refs, refsTabs } from '../components/common/ref'
9
+ import { runCmd } from '../components/terminal/terminal-apis'
9
10
  import deepCopy from 'json-deep-copy'
10
11
  import {
11
12
  getLocalFileInfo,
@@ -118,6 +119,26 @@ export default Store => {
118
119
  case 'wait_for_terminal_idle':
119
120
  result = await store.mcpWaitForTerminalIdle(args)
120
121
  break
122
+ case 'get_terminal_status':
123
+ result = store.mcpGetTerminalStatus(args)
124
+ break
125
+ case 'cancel_terminal_command':
126
+ result = store.mcpCancelTerminalCommand(args)
127
+ break
128
+
129
+ // Background task operations
130
+ case 'run_background_command':
131
+ result = store.mcpRunBackgroundCommand(args)
132
+ break
133
+ case 'get_background_task_status':
134
+ result = await store.mcpGetBackgroundTaskStatus(args)
135
+ break
136
+ case 'get_background_task_log':
137
+ result = await store.mcpGetBackgroundTaskLog(args)
138
+ break
139
+ case 'cancel_background_task':
140
+ result = await store.mcpCancelBackgroundTask(args)
141
+ break
121
142
 
122
143
  // SFTP operations
123
144
  case 'sftp_list':
@@ -652,6 +673,216 @@ export default Store => {
652
673
  }
653
674
  }
654
675
 
676
+ // ==================== Terminal Status & Cancel ====================
677
+
678
+ Store.prototype.mcpGetTerminalStatus = function (args) {
679
+ const { store } = window
680
+ const tabId = args.tabId || store.activeTabId
681
+ if (!tabId) {
682
+ throw new Error('No active terminal')
683
+ }
684
+
685
+ const tabRef = refsTabs.get('tab-' + tabId)
686
+ const onData = tabRef?.state.terminalOnData || ''
687
+ const term = refs.get('term-' + tabId)
688
+
689
+ let output = ''
690
+ let lineCount = 0
691
+ if (term && term.term) {
692
+ const buffer = term.term.buffer.active
693
+ if (buffer) {
694
+ const lines = []
695
+ const cursorY = buffer.cursorY || 0
696
+ const baseY = buffer.baseY || 0
697
+ const totalLines = buffer.length || 0
698
+ const end = baseY + cursorY + 1
699
+ const start = Math.max(0, end - 20)
700
+ for (let i = start; i < Math.min(totalLines, end); i++) {
701
+ const line = buffer.getLine(i)
702
+ if (line) {
703
+ lines.push(line.translateToString(true))
704
+ }
705
+ }
706
+ output = lines.join('\n')
707
+ lineCount = lines.length
708
+ }
709
+ }
710
+
711
+ return {
712
+ tabId,
713
+ isRunning: onData === 'feed',
714
+ hasPasswordPrompt: onData === 'password',
715
+ isIdle: !onData,
716
+ output,
717
+ lineCount
718
+ }
719
+ }
720
+
721
+ Store.prototype.mcpCancelTerminalCommand = function (args) {
722
+ const { store } = window
723
+ const tabId = args.tabId || store.activeTabId
724
+ if (!tabId) {
725
+ throw new Error('No active terminal')
726
+ }
727
+
728
+ const term = refs.get('term-' + tabId)
729
+ if (!term || !term.attachAddon) {
730
+ throw new Error('Terminal not found')
731
+ }
732
+
733
+ term.attachAddon._sendData('\x03')
734
+ return {
735
+ success: true,
736
+ message: 'Sent Ctrl+C to terminal',
737
+ tabId
738
+ }
739
+ }
740
+
741
+ // ==================== Background Task Management ====================
742
+
743
+ const backgroundTasks = new Map()
744
+ let bgTaskCounter = 0
745
+
746
+ async function runMonitorCmd (tabId, cmd) {
747
+ try {
748
+ const result = await runCmd(tabId, cmd)
749
+ return result
750
+ } catch (e) {
751
+ // Fallback: send via terminal and wait for idle
752
+ const { store } = window
753
+ store.mcpSendTerminalCommand({ command: cmd, tabId })
754
+ const idle = await store.mcpWaitForTerminalIdle({
755
+ tabId, timeout: 10000, lines: 10, minWait: 500
756
+ })
757
+ return idle.output || ''
758
+ }
759
+ }
760
+
761
+ Store.prototype.mcpRunBackgroundCommand = function (args) {
762
+ const { store } = window
763
+ const tabId = args.tabId || store.activeTabId
764
+ if (!tabId) {
765
+ throw new Error('No active terminal')
766
+ }
767
+ if (!args.command) {
768
+ throw new Error('No command provided')
769
+ }
770
+
771
+ const taskId = `bg-${Date.now()}-${++bgTaskCounter}`
772
+ const logFile = `/tmp/electerm-${taskId}.log`
773
+ const pidFile = `/tmp/electerm-${taskId}.pid`
774
+ const exitFile = `/tmp/electerm-${taskId}.exit`
775
+
776
+ // Encode command as base64 to avoid all quote-escaping issues.
777
+ // The subshell runs the user's command, captures its exit code, then cleans up the PID file.
778
+ const b64 = btoa(args.command)
779
+ const inner = `eval "$(echo ${b64} | base64 --decode)" > ${logFile} 2>&1; e=$?; echo $e > ${exitFile}; rm -f ${pidFile}`
780
+ const wrapped = `nohup bash -c '${inner}' & echo $! > ${pidFile}; disown`
781
+
782
+ store.mcpSendTerminalCommand({ command: wrapped, tabId, inputOnly: false })
783
+
784
+ const task = {
785
+ id: taskId,
786
+ command: args.command,
787
+ tabId,
788
+ startTime: Date.now(),
789
+ logFile,
790
+ pidFile,
791
+ exitFile,
792
+ status: 'started'
793
+ }
794
+ backgroundTasks.set(taskId, task)
795
+
796
+ return {
797
+ taskId,
798
+ tabId,
799
+ logFile,
800
+ pidFile,
801
+ exitFile,
802
+ message: 'Command started in background. Use get_background_task_status to check.'
803
+ }
804
+ }
805
+
806
+ Store.prototype.mcpGetBackgroundTaskStatus = async function (args) {
807
+ const task = backgroundTasks.get(args.taskId)
808
+ if (!task) {
809
+ throw new Error(`Task ${args.taskId} not found`)
810
+ }
811
+
812
+ const pidOutput = await runMonitorCmd(task.tabId,
813
+ `cat ${task.pidFile} 2>/dev/null`)
814
+ const pid = pidOutput.trim()
815
+
816
+ if (!pid) {
817
+ return { ...task, status: 'unknown', message: 'PID file not found' }
818
+ }
819
+
820
+ const aliveCheck = await runMonitorCmd(task.tabId,
821
+ `kill -0 ${pid} 2>/dev/null && echo alive || echo dead`)
822
+
823
+ if (aliveCheck.trim() === 'alive') {
824
+ task.status = 'running'
825
+ return { ...task, pid, status: 'running' }
826
+ }
827
+
828
+ // Process exited — read exit code
829
+ const exitOutput = await runMonitorCmd(task.tabId,
830
+ `cat ${task.exitFile} 2>/dev/null`)
831
+ const exitCode = exitOutput.trim()
832
+
833
+ task.status = 'completed'
834
+ task.exitCode = exitCode !== '' ? parseInt(exitCode, 10) : null
835
+ task.endTime = Date.now()
836
+ return { ...task, pid, status: 'completed', exitCode: task.exitCode }
837
+ }
838
+
839
+ Store.prototype.mcpGetBackgroundTaskLog = async function (args) {
840
+ const task = backgroundTasks.get(args.taskId)
841
+ if (!task) {
842
+ throw new Error(`Task ${args.taskId} not found`)
843
+ }
844
+
845
+ const lines = args.lines || 100
846
+ const output = await runMonitorCmd(task.tabId,
847
+ `tail -n ${lines} ${task.logFile} 2>/dev/null || echo '(no output yet)'`)
848
+
849
+ return {
850
+ taskId: task.id,
851
+ output: output.trim(),
852
+ lines
853
+ }
854
+ }
855
+
856
+ Store.prototype.mcpCancelBackgroundTask = async function (args) {
857
+ const task = backgroundTasks.get(args.taskId)
858
+ if (!task) {
859
+ throw new Error(`Task ${args.taskId} not found`)
860
+ }
861
+
862
+ const pidOutput = await runMonitorCmd(task.tabId,
863
+ `cat ${task.pidFile} 2>/dev/null`)
864
+ const pid = pidOutput.trim()
865
+
866
+ if (pid) {
867
+ await runMonitorCmd(task.tabId,
868
+ `kill ${pid} 2>/dev/null; echo $? > ${task.exitFile}`)
869
+ task.status = 'cancelled'
870
+ task.endTime = Date.now()
871
+ return {
872
+ taskId: task.id,
873
+ pid,
874
+ status: 'cancelled',
875
+ message: 'Process killed'
876
+ }
877
+ }
878
+
879
+ return {
880
+ taskId: task.id,
881
+ status: 'unknown',
882
+ message: 'PID not found, task may have already finished'
883
+ }
884
+ }
885
+
655
886
  // ==================== Settings APIs ====================
656
887
 
657
888
  Store.prototype.mcpGetSettings = function () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.12.0",
3
+ "version": "3.15.28",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",