agentgui 1.0.796 → 1.0.798
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/model-download-machine.js +107 -0
- package/lib/rate-limit-machine.js +99 -0
- package/lib/script-machine.js +81 -0
- package/lib/ws-handlers-scripts.js +16 -9
- package/package.json +1 -1
- package/static/index.html +2 -0
- package/static/js/recording-machine.js +49 -0
- package/static/js/stt-handler.js +12 -3
- package/static/js/terminal-machine.js +51 -0
- package/static/js/terminal.js +5 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createMachine, createActor, assign } from 'xstate';
|
|
2
|
+
|
|
3
|
+
const machine = createMachine({
|
|
4
|
+
id: 'model-download',
|
|
5
|
+
initial: 'idle',
|
|
6
|
+
context: {
|
|
7
|
+
progress: null,
|
|
8
|
+
error: null,
|
|
9
|
+
startTime: null,
|
|
10
|
+
waiters: [],
|
|
11
|
+
},
|
|
12
|
+
states: {
|
|
13
|
+
idle: {
|
|
14
|
+
on: {
|
|
15
|
+
START: {
|
|
16
|
+
target: 'downloading',
|
|
17
|
+
actions: assign({ startTime: () => Date.now(), error: null }),
|
|
18
|
+
},
|
|
19
|
+
WAIT: {
|
|
20
|
+
actions: assign(({ context, event }) => ({
|
|
21
|
+
waiters: [...context.waiters, event.resolve],
|
|
22
|
+
})),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
downloading: {
|
|
27
|
+
on: {
|
|
28
|
+
PROGRESS: {
|
|
29
|
+
actions: assign(({ event }) => ({ progress: event.progress })),
|
|
30
|
+
},
|
|
31
|
+
COMPLETE: { target: 'complete' },
|
|
32
|
+
ERROR: {
|
|
33
|
+
target: 'error',
|
|
34
|
+
actions: assign(({ event }) => ({ error: event.error })),
|
|
35
|
+
},
|
|
36
|
+
WAIT: {
|
|
37
|
+
actions: assign(({ context, event }) => ({
|
|
38
|
+
waiters: [...context.waiters, event.resolve],
|
|
39
|
+
})),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
complete: {
|
|
44
|
+
entry: assign(({ context }) => {
|
|
45
|
+
for (const resolve of context.waiters) resolve(true);
|
|
46
|
+
return { waiters: [] };
|
|
47
|
+
}),
|
|
48
|
+
on: {
|
|
49
|
+
RESET: { target: 'idle' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
error: {
|
|
53
|
+
entry: assign(({ context }) => {
|
|
54
|
+
for (const resolve of context.waiters) resolve(false);
|
|
55
|
+
return { waiters: [] };
|
|
56
|
+
}),
|
|
57
|
+
on: {
|
|
58
|
+
RESET: { target: 'idle' },
|
|
59
|
+
START: {
|
|
60
|
+
target: 'downloading',
|
|
61
|
+
actions: assign({ startTime: () => Date.now(), error: null }),
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let actor = null;
|
|
69
|
+
|
|
70
|
+
export function getActor() {
|
|
71
|
+
if (!actor) {
|
|
72
|
+
actor = createActor(machine);
|
|
73
|
+
actor.start();
|
|
74
|
+
}
|
|
75
|
+
return actor;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function send(event) {
|
|
79
|
+
return getActor().send(event);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function snapshot() {
|
|
83
|
+
return getActor().getSnapshot();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isDownloading() {
|
|
87
|
+
const s = snapshot();
|
|
88
|
+
return s.value === 'downloading';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isComplete() {
|
|
92
|
+
const s = snapshot();
|
|
93
|
+
return s.value === 'complete';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function isError() {
|
|
97
|
+
const s = snapshot();
|
|
98
|
+
return s.value === 'error';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getState() {
|
|
102
|
+
return snapshot().value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getContext() {
|
|
106
|
+
return snapshot().context;
|
|
107
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createMachine, createActor, assign } from 'xstate';
|
|
2
|
+
|
|
3
|
+
const machine = createMachine({
|
|
4
|
+
id: 'rate-limit',
|
|
5
|
+
initial: 'normal',
|
|
6
|
+
context: {
|
|
7
|
+
retryCount: 0,
|
|
8
|
+
retryAt: null,
|
|
9
|
+
cooldownMs: null,
|
|
10
|
+
isStreamDetected: false,
|
|
11
|
+
},
|
|
12
|
+
states: {
|
|
13
|
+
normal: {
|
|
14
|
+
entry: assign({ retryCount: 0, retryAt: null, cooldownMs: null, isStreamDetected: false }),
|
|
15
|
+
on: {
|
|
16
|
+
HIT: {
|
|
17
|
+
target: 'limited',
|
|
18
|
+
actions: assign(({ context, event }) => ({
|
|
19
|
+
retryCount: context.retryCount + 1,
|
|
20
|
+
retryAt: event.retryAt,
|
|
21
|
+
cooldownMs: event.cooldownMs,
|
|
22
|
+
isStreamDetected: event.isStreamDetected || false,
|
|
23
|
+
})),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
limited: {
|
|
28
|
+
on: {
|
|
29
|
+
CLEAR: [
|
|
30
|
+
{ guard: ({ context }) => context.retryCount >= 3, target: 'exceeded' },
|
|
31
|
+
{ target: 'normal' },
|
|
32
|
+
],
|
|
33
|
+
HIT: {
|
|
34
|
+
actions: assign(({ context, event }) => ({
|
|
35
|
+
retryCount: context.retryCount + 1,
|
|
36
|
+
retryAt: event.retryAt,
|
|
37
|
+
cooldownMs: event.cooldownMs,
|
|
38
|
+
isStreamDetected: event.isStreamDetected || false,
|
|
39
|
+
})),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
exceeded: {
|
|
44
|
+
on: {
|
|
45
|
+
RESET: { target: 'normal' },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const actors = new Map();
|
|
52
|
+
|
|
53
|
+
export function getOrCreate(convId) {
|
|
54
|
+
if (actors.has(convId)) return actors.get(convId);
|
|
55
|
+
const actor = createActor(machine);
|
|
56
|
+
actor.start();
|
|
57
|
+
actors.set(convId, actor);
|
|
58
|
+
return actor;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function get(convId) {
|
|
62
|
+
return actors.get(convId) || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function remove(convId) {
|
|
66
|
+
const actor = actors.get(convId);
|
|
67
|
+
if (actor) { actor.stop(); actors.delete(convId); }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function snapshot(convId) {
|
|
71
|
+
const actor = actors.get(convId);
|
|
72
|
+
return actor ? actor.getSnapshot() : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function send(convId, event) {
|
|
76
|
+
const actor = getOrCreate(convId);
|
|
77
|
+
actor.send(event);
|
|
78
|
+
return actor.getSnapshot();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isLimited(convId) {
|
|
82
|
+
const s = snapshot(convId);
|
|
83
|
+
return s ? s.value === 'limited' : false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isExceeded(convId) {
|
|
87
|
+
const s = snapshot(convId);
|
|
88
|
+
return s ? s.value === 'exceeded' : false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getContext(convId) {
|
|
92
|
+
const s = snapshot(convId);
|
|
93
|
+
return s ? s.context : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function has(convId) {
|
|
97
|
+
const s = snapshot(convId);
|
|
98
|
+
return s ? s.value !== 'normal' : false;
|
|
99
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createMachine, createActor, assign } from 'xstate';
|
|
2
|
+
|
|
3
|
+
const machine = createMachine({
|
|
4
|
+
id: 'script',
|
|
5
|
+
initial: 'idle',
|
|
6
|
+
context: {
|
|
7
|
+
process: null,
|
|
8
|
+
script: null,
|
|
9
|
+
startTime: null,
|
|
10
|
+
pid: null,
|
|
11
|
+
},
|
|
12
|
+
states: {
|
|
13
|
+
idle: {
|
|
14
|
+
entry: assign({ process: null, script: null, startTime: null, pid: null }),
|
|
15
|
+
on: {
|
|
16
|
+
START: {
|
|
17
|
+
target: 'running',
|
|
18
|
+
actions: assign(({ event }) => ({
|
|
19
|
+
process: event.process,
|
|
20
|
+
script: event.script,
|
|
21
|
+
startTime: Date.now(),
|
|
22
|
+
pid: event.pid || null,
|
|
23
|
+
})),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
running: {
|
|
28
|
+
on: {
|
|
29
|
+
STOP: { target: 'stopping' },
|
|
30
|
+
CLOSE: { target: 'idle' },
|
|
31
|
+
ERROR: { target: 'idle' },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
stopping: {
|
|
35
|
+
on: {
|
|
36
|
+
CLOSE: { target: 'idle' },
|
|
37
|
+
ERROR: { target: 'idle' },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const actors = new Map();
|
|
44
|
+
|
|
45
|
+
export function getOrCreate(convId) {
|
|
46
|
+
if (actors.has(convId)) return actors.get(convId);
|
|
47
|
+
const actor = createActor(machine);
|
|
48
|
+
actor.start();
|
|
49
|
+
actors.set(convId, actor);
|
|
50
|
+
return actor;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function get(convId) {
|
|
54
|
+
return actors.get(convId) || null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function remove(convId) {
|
|
58
|
+
const actor = actors.get(convId);
|
|
59
|
+
if (actor) { actor.stop(); actors.delete(convId); }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function snapshot(convId) {
|
|
63
|
+
const actor = actors.get(convId);
|
|
64
|
+
return actor ? actor.getSnapshot() : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function send(convId, event) {
|
|
68
|
+
const actor = getOrCreate(convId);
|
|
69
|
+
actor.send(event);
|
|
70
|
+
return actor.getSnapshot();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isRunning(convId) {
|
|
74
|
+
const s = snapshot(convId);
|
|
75
|
+
return s ? s.value === 'running' || s.value === 'stopping' : false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getContext(convId) {
|
|
79
|
+
const s = snapshot(convId);
|
|
80
|
+
return s ? s.context : null;
|
|
81
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { spawn } from 'child_process';
|
|
5
|
+
import * as scriptMachine from './script-machine.js';
|
|
5
6
|
|
|
6
7
|
function err(code, message) { const e = new Error(message); e.code = code; throw e; }
|
|
7
8
|
|
|
@@ -19,15 +20,16 @@ export function register(router, deps) {
|
|
|
19
20
|
hasStart = !!scripts.start;
|
|
20
21
|
hasDev = !!scripts.dev;
|
|
21
22
|
} catch {}
|
|
22
|
-
const running =
|
|
23
|
-
const
|
|
23
|
+
const running = scriptMachine.isRunning(p.id);
|
|
24
|
+
const ctx = scriptMachine.getContext(p.id);
|
|
25
|
+
const runningScript = running ? ctx?.script : null;
|
|
24
26
|
return { hasStart, hasDev, running, runningScript };
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
router.handle('conv.run-script', (p) => {
|
|
28
30
|
const conv = queries.getConversation(p.id);
|
|
29
31
|
if (!conv) err(404, 'Not found');
|
|
30
|
-
if (
|
|
32
|
+
if (scriptMachine.isRunning(p.id)) err(409, 'Script already running');
|
|
31
33
|
const script = p.script;
|
|
32
34
|
if (script !== 'start' && script !== 'dev') err(400, 'Invalid script');
|
|
33
35
|
const wd = conv.workingDirectory || STARTUP_CWD;
|
|
@@ -39,21 +41,26 @@ export function register(router, deps) {
|
|
|
39
41
|
delete childEnv.PORT; delete childEnv.BASE_URL; delete childEnv.HOT_RELOAD;
|
|
40
42
|
const isWindows = os.platform() === 'win32';
|
|
41
43
|
const child = spawn('npm', ['run', script], { cwd: wd, stdio: ['ignore', 'pipe', 'pipe'], detached: true, env: childEnv, shell: isWindows });
|
|
44
|
+
scriptMachine.send(p.id, { type: 'START', process: child, script, pid: child.pid });
|
|
42
45
|
activeScripts.set(p.id, { process: child, script, startTime: Date.now() });
|
|
43
46
|
broadcastSync({ type: 'script_started', conversationId: p.id, script, timestamp: Date.now() });
|
|
44
47
|
const onData = (stream) => (chunk) => broadcastSync({ type: 'script_output', conversationId: p.id, data: chunk.toString(), stream, timestamp: Date.now() });
|
|
45
48
|
child.stdout.on('data', onData('stdout'));
|
|
46
49
|
child.stderr.on('data', onData('stderr'));
|
|
47
|
-
child.on('error', (e) => { activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: 1, error: e.message, timestamp: Date.now() }); });
|
|
48
|
-
child.on('close', (code) => { activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: code || 0, timestamp: Date.now() }); });
|
|
50
|
+
child.on('error', (e) => { scriptMachine.send(p.id, { type: 'ERROR' }); activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: 1, error: e.message, timestamp: Date.now() }); });
|
|
51
|
+
child.on('close', (code) => { scriptMachine.send(p.id, { type: 'CLOSE' }); activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: code || 0, timestamp: Date.now() }); });
|
|
49
52
|
return { ok: true, script, pid: child.pid };
|
|
50
53
|
});
|
|
51
54
|
|
|
52
55
|
router.handle('conv.stop-script', (p) => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
if (!scriptMachine.isRunning(p.id)) err(404, 'No running script');
|
|
57
|
+
const ctx = scriptMachine.getContext(p.id);
|
|
58
|
+
const proc = ctx?.process;
|
|
59
|
+
scriptMachine.send(p.id, { type: 'STOP' });
|
|
60
|
+
if (proc) {
|
|
61
|
+
try { process.kill(-proc.pid, 'SIGTERM'); } catch { try { proc.kill('SIGTERM'); } catch {} }
|
|
62
|
+
setTimeout(() => { try { process.kill(-proc.pid, 'SIGKILL'); } catch { try { proc.kill('SIGKILL'); } catch {} } }, 5000);
|
|
63
|
+
}
|
|
57
64
|
return { ok: true };
|
|
58
65
|
});
|
|
59
66
|
}
|
package/package.json
CHANGED
package/static/index.html
CHANGED
|
@@ -290,6 +290,8 @@
|
|
|
290
290
|
<script defer src="/gm/js/voice-machine.js"></script>
|
|
291
291
|
<script defer src="/gm/js/conv-list-machine.js"></script>
|
|
292
292
|
<script defer src="/gm/js/prompt-machine.js"></script>
|
|
293
|
+
<script defer src="/gm/js/recording-machine.js"></script>
|
|
294
|
+
<script defer src="/gm/js/terminal-machine.js"></script>
|
|
293
295
|
<script defer src="/gm/js/conversations.js"></script>
|
|
294
296
|
<script defer src="/gm/lib/msgpackr.min.js"></script>
|
|
295
297
|
<script defer src="/gm/js/websocket-manager.js"></script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
const { createMachine, createActor, assign } = XState;
|
|
3
|
+
|
|
4
|
+
const recordingMachine = createMachine({
|
|
5
|
+
id: 'recording',
|
|
6
|
+
initial: 'idle',
|
|
7
|
+
context: {
|
|
8
|
+
error: null,
|
|
9
|
+
},
|
|
10
|
+
states: {
|
|
11
|
+
idle: {
|
|
12
|
+
entry: assign({ error: null }),
|
|
13
|
+
on: {
|
|
14
|
+
START: { target: 'recording' },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
recording: {
|
|
18
|
+
on: {
|
|
19
|
+
STOP: { target: 'processing' },
|
|
20
|
+
ERROR: {
|
|
21
|
+
target: 'idle',
|
|
22
|
+
actions: assign(({ event }) => ({ error: event.error || 'Recording failed' })),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
processing: {
|
|
27
|
+
on: {
|
|
28
|
+
COMPLETE: { target: 'idle' },
|
|
29
|
+
ERROR: {
|
|
30
|
+
target: 'idle',
|
|
31
|
+
actions: assign(({ event }) => ({ error: event.error || 'Processing failed' })),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const actor = createActor(recordingMachine);
|
|
39
|
+
actor.start();
|
|
40
|
+
|
|
41
|
+
window.recordingMachineAPI = {
|
|
42
|
+
send: function(event) { actor.send(event); },
|
|
43
|
+
getState: function() { return actor.getSnapshot().value; },
|
|
44
|
+
isRecording: function() { return actor.getSnapshot().value === 'recording'; },
|
|
45
|
+
isProcessing: function() { return actor.getSnapshot().value === 'processing'; },
|
|
46
|
+
subscribe: function(fn) { return actor.subscribe(fn); },
|
|
47
|
+
};
|
|
48
|
+
window.__recordingMachine = actor;
|
|
49
|
+
})();
|
package/static/js/stt-handler.js
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
var recordedChunks = [];
|
|
8
8
|
var TARGET_SAMPLE_RATE = 16000;
|
|
9
9
|
|
|
10
|
+
function rmApi() { return window.recordingMachineAPI; }
|
|
11
|
+
|
|
10
12
|
window.STTHandler = {
|
|
11
|
-
isRecording: function() { return isRecording; },
|
|
13
|
+
isRecording: function() { return rmApi()?.isRecording() || isRecording; },
|
|
12
14
|
|
|
13
15
|
startRecording: async function() {
|
|
14
|
-
if (isRecording) return;
|
|
16
|
+
if (isRecording || rmApi()?.isRecording()) return;
|
|
15
17
|
try {
|
|
16
18
|
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
17
19
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
@@ -24,16 +26,19 @@
|
|
|
24
26
|
};
|
|
25
27
|
source.connect(workletNode);
|
|
26
28
|
isRecording = true;
|
|
29
|
+
if (rmApi()) rmApi().send({ type: 'START' });
|
|
27
30
|
return { success: true };
|
|
28
31
|
} catch (e) {
|
|
29
32
|
isRecording = false;
|
|
33
|
+
if (rmApi()) rmApi().send({ type: 'ERROR', error: e.message });
|
|
30
34
|
return { success: false, error: e.message };
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
|
|
34
38
|
stopRecording: async function() {
|
|
35
|
-
if (!isRecording) return { success: false, error: 'Not recording' };
|
|
39
|
+
if (!isRecording && !rmApi()?.isRecording()) return { success: false, error: 'Not recording' };
|
|
36
40
|
isRecording = false;
|
|
41
|
+
if (rmApi()) rmApi().send({ type: 'STOP' });
|
|
37
42
|
|
|
38
43
|
if (workletNode) {
|
|
39
44
|
workletNode.port.postMessage('stop');
|
|
@@ -77,13 +82,17 @@
|
|
|
77
82
|
});
|
|
78
83
|
var data = await resp.json();
|
|
79
84
|
if (data.text) {
|
|
85
|
+
if (rmApi()) rmApi().send({ type: 'COMPLETE' });
|
|
80
86
|
return { success: true, text: data.text };
|
|
81
87
|
} else if (data.error) {
|
|
88
|
+
if (rmApi()) rmApi().send({ type: 'ERROR', error: data.error });
|
|
82
89
|
return { success: false, error: data.error };
|
|
83
90
|
} else {
|
|
91
|
+
if (rmApi()) rmApi().send({ type: 'ERROR', error: 'No transcription returned' });
|
|
84
92
|
return { success: false, error: 'No transcription returned' };
|
|
85
93
|
}
|
|
86
94
|
} catch (e) {
|
|
95
|
+
if (rmApi()) rmApi().send({ type: 'ERROR', error: e.message });
|
|
87
96
|
return { success: false, error: 'Transcription failed: ' + e.message };
|
|
88
97
|
}
|
|
89
98
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
const { createMachine, createActor, assign } = XState;
|
|
3
|
+
|
|
4
|
+
const terminalMachine = createMachine({
|
|
5
|
+
id: 'terminal',
|
|
6
|
+
initial: 'inactive',
|
|
7
|
+
context: {
|
|
8
|
+
cwd: null,
|
|
9
|
+
},
|
|
10
|
+
states: {
|
|
11
|
+
inactive: {
|
|
12
|
+
on: {
|
|
13
|
+
START: { target: 'initializing' },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
initializing: {
|
|
17
|
+
on: {
|
|
18
|
+
READY: { target: 'active' },
|
|
19
|
+
ERROR: { target: 'inactive' },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
active: {
|
|
23
|
+
on: {
|
|
24
|
+
STOP: { target: 'inactive' },
|
|
25
|
+
SESSION_EXIT: { target: 'restarting' },
|
|
26
|
+
SET_CWD: {
|
|
27
|
+
actions: assign(({ event }) => ({ cwd: event.cwd })),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
restarting: {
|
|
32
|
+
on: {
|
|
33
|
+
READY: { target: 'active' },
|
|
34
|
+
ERROR: { target: 'inactive' },
|
|
35
|
+
STOP: { target: 'inactive' },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const actor = createActor(terminalMachine);
|
|
42
|
+
actor.start();
|
|
43
|
+
|
|
44
|
+
window.terminalMachineAPI = {
|
|
45
|
+
send: function(event) { actor.send(event); },
|
|
46
|
+
getState: function() { return actor.getSnapshot().value; },
|
|
47
|
+
isActive: function() { var s = actor.getSnapshot().value; return s === 'active' || s === 'restarting'; },
|
|
48
|
+
subscribe: function(fn) { return actor.subscribe(fn); },
|
|
49
|
+
};
|
|
50
|
+
window.__terminalMachine = actor;
|
|
51
|
+
})();
|
package/static/js/terminal.js
CHANGED
|
@@ -101,10 +101,14 @@
|
|
|
101
101
|
var cwd = getCwd();
|
|
102
102
|
var dims = term ? { cols: term.cols, rows: term.rows } : { cols: 80, rows: 24 };
|
|
103
103
|
wsSend({ type: 'terminal_start', cwd: cwd, cols: dims.cols, rows: dims.rows });
|
|
104
|
+
if (tmApi()) tmApi().send({ type: 'READY' });
|
|
104
105
|
setTimeout(function() { if (term && term.focus) term.focus(); }, 100);
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
function tmApi() { return window.terminalMachineAPI; }
|
|
109
|
+
|
|
107
110
|
function startTerminal() {
|
|
111
|
+
if (tmApi()) tmApi().send({ type: 'START' });
|
|
108
112
|
if (!ensureTerm()) {
|
|
109
113
|
setTimeout(startTerminal, 200);
|
|
110
114
|
return;
|
|
@@ -124,6 +128,7 @@
|
|
|
124
128
|
|
|
125
129
|
function stopTerminal() {
|
|
126
130
|
termActive = false;
|
|
131
|
+
if (tmApi()) tmApi().send({ type: 'STOP' });
|
|
127
132
|
if (_wsListener && window.wsManager) { window.wsManager.off('message', _wsListener); _wsListener = null; }
|
|
128
133
|
wsSend({ type: 'terminal_stop' });
|
|
129
134
|
}
|