claude-simple 0.2.6 → 0.2.8
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/README.md +2 -0
- package/dist/app.0.2.8.min.js +1080 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.8.min.css +1598 -0
- package/gateway.js +0 -0
- package/package.json +1 -1
- package/public/addon-fit.min.js +0 -0
- package/public/agent-panel.js +245 -1
- package/public/api.js +4 -0
- package/public/app.js +0 -0
- package/public/base.css +0 -0
- package/public/cache.js +0 -0
- package/public/chat.js +118 -17
- package/public/files.js +0 -0
- package/public/git.js +0 -0
- package/public/icons/icon-180.png +0 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon-maskable-512.png +0 -0
- package/public/index.html +2 -2
- package/public/lib.js +0 -0
- package/public/manifest.webmanifest +0 -0
- package/public/mobile.js +0 -0
- package/public/notifications.js +215 -0
- package/public/offline.html +0 -0
- package/public/pwa.js +0 -0
- package/public/router.js +0 -0
- package/public/settings.js +0 -0
- package/public/shell/boot.js +0 -0
- package/public/shell/dashboard.js +72 -23
- package/public/shell/hub.js +0 -0
- package/public/shell/layout.js +41 -12
- package/public/shell/notes.js +0 -0
- package/public/shell/session-context.js +0 -0
- package/public/shell/session-list.js +27 -28
- package/public/shell/session-nav.js +36 -1
- package/public/shell/templates.js +45 -7
- package/public/shell/util.js +0 -0
- package/public/shell.js +56 -7
- package/public/state.js +53 -13
- package/public/style.css +100 -6
- package/public/sw.js +2 -2
- package/public/terminal.js +77 -3
- package/public/wake-lock.js +0 -0
- package/public/xterm.min.css +0 -0
- package/public/xterm.min.js +0 -0
- package/src/agents/claude.js +1 -1
- package/src/agents/codex.js +1 -0
- package/src/agents/grok.js +180 -298
- package/src/agents/meta-agent.js +332 -1
- package/src/agents/models.js +0 -0
- package/src/agents/sessions.js +106 -26
- package/src/agents/stream-batch.js +0 -0
- package/src/app.js +88 -11
- package/src/auto-update.js +0 -0
- package/src/runtime/approvals.js +0 -0
- package/src/runtime/outbox.js +0 -0
- package/src/runtime/protocol.js +0 -0
- package/dist/app.0.2.6.min.js +0 -953
- package/dist/style.0.2.6.min.css +0 -1504
package/public/terminal.js
CHANGED
|
@@ -11,6 +11,37 @@ let lastMachine = null;
|
|
|
11
11
|
let shellGen = 0;
|
|
12
12
|
let shellReconnectTimer = null;
|
|
13
13
|
let shellWanted = false;
|
|
14
|
+
let terminalEventsBound = false;
|
|
15
|
+
const mobileKeyData = {
|
|
16
|
+
'ctrl-c': '\x03', escape: '\x1b', tab: '\t',
|
|
17
|
+
up: '\x1b[A', down: '\x1b[B', left: '\x1b[D', right: '\x1b[C'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function sendInput(data) {
|
|
21
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
22
|
+
ws.send(JSON.stringify({ type: 'input', data }));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isMobileShell() {
|
|
27
|
+
return window.matchMedia('(max-width: 640px), (hover: none) and (pointer: coarse)').matches;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Mobile IMEs do not reliably drive xterm's off-screen textarea. Use a real
|
|
31
|
+
* input there so composition, prediction and paste remain visible and intact. */
|
|
32
|
+
function focusTerminal() {
|
|
33
|
+
if (isMobileShell()) {
|
|
34
|
+
document.getElementById('term-mobile-input')?.focus({ preventScroll: true });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
term?.focus();
|
|
38
|
+
const input = document.querySelector('#terminal-container .xterm-helper-textarea');
|
|
39
|
+
if (input) {
|
|
40
|
+
input.focus({ preventScroll: true });
|
|
41
|
+
// iOS may ignore the first focus while its visual viewport is settling.
|
|
42
|
+
requestAnimationFrame(() => input.focus({ preventScroll: true }));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
14
45
|
|
|
15
46
|
function initXterm() {
|
|
16
47
|
if (term) return;
|
|
@@ -59,15 +90,35 @@ function initXterm() {
|
|
|
59
90
|
fitTerminal();
|
|
60
91
|
|
|
61
92
|
term.onData(data => {
|
|
62
|
-
|
|
63
|
-
ws.send(JSON.stringify({ type: 'input', data }));
|
|
64
|
-
}
|
|
93
|
+
sendInput(data);
|
|
65
94
|
});
|
|
66
95
|
|
|
96
|
+
// On phones this focuses the native command input; desktop keeps xterm's
|
|
97
|
+
// direct keyboard interaction.
|
|
98
|
+
container.addEventListener('pointerdown', () => focusTerminal(), { passive: true });
|
|
99
|
+
container.addEventListener('touchend', () => focusTerminal(), { passive: true });
|
|
100
|
+
|
|
67
101
|
window.addEventListener('resize', () => {
|
|
68
102
|
fitTerminal();
|
|
69
103
|
sendResize();
|
|
70
104
|
});
|
|
105
|
+
|
|
106
|
+
const vv = window.visualViewport;
|
|
107
|
+
if (vv) {
|
|
108
|
+
let raf = 0;
|
|
109
|
+
const fitForVisualViewport = () => {
|
|
110
|
+
if (raf) cancelAnimationFrame(raf);
|
|
111
|
+
raf = requestAnimationFrame(() => {
|
|
112
|
+
raf = 0;
|
|
113
|
+
if (currentTab.peek() === 'shell') {
|
|
114
|
+
fitTerminal();
|
|
115
|
+
sendResize();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
vv.addEventListener('resize', fitForVisualViewport);
|
|
120
|
+
vv.addEventListener('scroll', fitForVisualViewport);
|
|
121
|
+
}
|
|
71
122
|
}
|
|
72
123
|
|
|
73
124
|
function fitTerminal() {
|
|
@@ -177,6 +228,29 @@ function activateShellTab() {
|
|
|
177
228
|
}
|
|
178
229
|
|
|
179
230
|
export function initTerminal() {
|
|
231
|
+
if (!terminalEventsBound) {
|
|
232
|
+
terminalEventsBound = true;
|
|
233
|
+
document.getElementById('term-keyboard')?.addEventListener('click', focusTerminal);
|
|
234
|
+
document.getElementById('term-mobile-composer')?.addEventListener('submit', event => {
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
const input = document.getElementById('term-mobile-input');
|
|
237
|
+
if (!input) return;
|
|
238
|
+
sendInput(`${input.value}\r`);
|
|
239
|
+
input.value = '';
|
|
240
|
+
input.focus({ preventScroll: true });
|
|
241
|
+
});
|
|
242
|
+
document.getElementById('term-mobile-keys')?.addEventListener('pointerdown', event => {
|
|
243
|
+
// Keep the native command input focused while using the touch shortcut row.
|
|
244
|
+
if (event.target.closest('[data-term-key]')) event.preventDefault();
|
|
245
|
+
});
|
|
246
|
+
document.getElementById('term-mobile-keys')?.addEventListener('click', event => {
|
|
247
|
+
const key = event.target.closest('[data-term-key]')?.dataset.termKey;
|
|
248
|
+
if (!key || !mobileKeyData[key]) return;
|
|
249
|
+
sendInput(mobileKeyData[key]);
|
|
250
|
+
focusTerminal();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
180
254
|
document.addEventListener('visibilitychange', () => {
|
|
181
255
|
if (document.visibilityState !== 'visible') return;
|
|
182
256
|
if (currentTab.peek() !== 'shell' || !shellWanted) return;
|
package/public/wake-lock.js
CHANGED
|
File without changes
|
package/public/xterm.min.css
CHANGED
|
File without changes
|
package/public/xterm.min.js
CHANGED
|
File without changes
|
package/src/agents/claude.js
CHANGED
|
@@ -23,7 +23,7 @@ export async function runClaude(command, options, send, { cliPath, activeSession
|
|
|
23
23
|
const env = { ...process.env };
|
|
24
24
|
if (options.configDir) env.CLAUDE_CONFIG_DIR = options.configDir;
|
|
25
25
|
|
|
26
|
-
const permissionMode = options.permissionMode || '
|
|
26
|
+
const permissionMode = options.permissionMode || 'bypassPermissions';
|
|
27
27
|
const autoApprove = permissionMode === 'bypassPermissions'
|
|
28
28
|
|| permissionMode === 'acceptEdits'
|
|
29
29
|
|| permissionMode === 'auto'
|
package/src/agents/codex.js
CHANGED
package/src/agents/grok.js
CHANGED
|
@@ -1,326 +1,208 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import readline from 'readline';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
|
-
import { createDeltaBatcher } from './stream-batch.js';
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* grok --resume <sessionId> -p <prompt> --output-format streaming-json ...
|
|
13
|
-
*
|
|
14
|
-
* Stream events (line-delimited JSON):
|
|
15
|
-
* { type: "thought", data: "..." }
|
|
16
|
-
* { type: "text", data: "..." }
|
|
17
|
-
* { type: "tool_*", ... } // when CLI emits them
|
|
18
|
-
* { type: "end", sessionId, usage, ... }
|
|
19
|
-
*
|
|
20
|
-
* We forward deltas immediately so the UI updates live instead of dumping at end.
|
|
21
|
-
*/
|
|
5
|
+
// Grok's ACP transport is a long-lived JSON-RPC process. Keeping it alive is
|
|
6
|
+
// important: it owns the real Grok session and lets the edge, rather than an
|
|
7
|
+
// individual browser tab, serialize follow-up prompts.
|
|
8
|
+
const clients = new Map();
|
|
9
|
+
const queues = new Map();
|
|
10
|
+
const queueAliases = new Map();
|
|
22
11
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
if (permissionMode && permissionMode !== 'default') return permissionMode;
|
|
26
|
-
return null;
|
|
12
|
+
function profileKey(options) {
|
|
13
|
+
return `${options.model || ''}\u001f${options.effort || ''}`;
|
|
27
14
|
}
|
|
28
15
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
args.push('--
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (options.newSessionId && !options.sessionId) {
|
|
51
|
-
args.push('--session-id', options.newSessionId);
|
|
16
|
+
class GrokACPClient {
|
|
17
|
+
constructor({ cliPath, model, effort }) {
|
|
18
|
+
const args = ['agent', '--always-approve'];
|
|
19
|
+
if (model) args.push('--model', model);
|
|
20
|
+
if (effort) args.push('--reasoning-effort', effort);
|
|
21
|
+
args.push('stdio');
|
|
22
|
+
this.child = spawn(cliPath || 'grok', args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
23
|
+
this.pending = new Map();
|
|
24
|
+
this.listeners = new Map();
|
|
25
|
+
this.nextID = 1;
|
|
26
|
+
this.ready = this.initialize();
|
|
27
|
+
this.child.stderr.on('data', d => console.warn(`[grok acp] ${String(d).trim()}`));
|
|
28
|
+
this.child.on('close', () => {
|
|
29
|
+
for (const { reject } of this.pending.values()) reject(new Error('Grok ACP process stopped'));
|
|
30
|
+
this.pending.clear();
|
|
31
|
+
for (const fn of this.listeners.values()) fn({ type: 'error', message: 'Grok ACP process stopped', agent: 'grok' });
|
|
32
|
+
this.listeners.clear();
|
|
33
|
+
});
|
|
34
|
+
const rl = readline.createInterface({ input: this.child.stdout, crlfDelay: Infinity });
|
|
35
|
+
rl.on('line', line => this.receive(line));
|
|
52
36
|
}
|
|
53
37
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
38
|
+
async initialize() {
|
|
39
|
+
const init = await this.request('initialize', {
|
|
40
|
+
protocolVersion: 1,
|
|
41
|
+
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, terminal: true },
|
|
42
|
+
clientInfo: { name: 'claude-simple-ui', version: '0.2.7' },
|
|
43
|
+
});
|
|
44
|
+
const methods = new Set((init.authMethods || []).map(x => x?.id));
|
|
45
|
+
const methodId = process.env.XAI_API_KEY && methods.has('xai.api_key')
|
|
46
|
+
? 'xai.api_key'
|
|
47
|
+
: (methods.has('cached_token') ? 'cached_token' : null);
|
|
48
|
+
if (methodId) await this.request('authenticate', { methodId, _meta: { headless: true } });
|
|
62
49
|
}
|
|
63
|
-
return { value: v };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function runGrok(command, options, send, { cliPath, activeSessions }) {
|
|
67
|
-
return new Promise((resolve) => {
|
|
68
|
-
const sessionKey = options.sessionId || crypto.randomUUID();
|
|
69
|
-
const bin = cliPath || 'grok';
|
|
70
|
-
const args = buildArgs(command, options);
|
|
71
|
-
|
|
72
|
-
const env = { ...process.env };
|
|
73
|
-
env.CI = env.CI || '1';
|
|
74
|
-
// Reduce pipe buffering if the runtime honors it
|
|
75
|
-
env.PYTHONUNBUFFERED = '1';
|
|
76
50
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
send({ type: 'error', message: `Failed to start grok: ${e.message}`, agent: 'grok' });
|
|
86
|
-
resolve();
|
|
51
|
+
receive(line) {
|
|
52
|
+
let msg;
|
|
53
|
+
try { msg = JSON.parse(line); } catch { return; }
|
|
54
|
+
if (msg.id != null && this.pending.has(msg.id)) {
|
|
55
|
+
const pending = this.pending.get(msg.id);
|
|
56
|
+
this.pending.delete(msg.id);
|
|
57
|
+
if (msg.error) pending.reject(new Error(msg.error.message || 'Grok ACP request failed'));
|
|
58
|
+
else pending.resolve(msg.result || {});
|
|
87
59
|
return;
|
|
88
60
|
}
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
let sessionId = options.sessionId || null;
|
|
97
|
-
let aborted = false;
|
|
98
|
-
let stderr = '';
|
|
99
|
-
let gotEnd = false;
|
|
100
|
-
let streamOpen = false; // live assistant bubble on the client
|
|
101
|
-
let thoughtOpen = false;
|
|
102
|
-
let textChars = 0;
|
|
103
|
-
let thoughtChars = 0;
|
|
104
|
-
|
|
105
|
-
const textBatch = createDeltaBatcher(send, 'assistant_delta', { agent: 'grok' }, { maxWait: 48, maxSize: 480 });
|
|
106
|
-
const thoughtBatch = createDeltaBatcher(send, 'thought_delta', { agent: 'grok' }, { maxWait: 140, maxSize: 600 });
|
|
107
|
-
|
|
108
|
-
child.stderr.on('data', d => { stderr += d.toString(); });
|
|
109
|
-
|
|
110
|
-
// Use 'readable' + line split for lower latency than waiting for large chunks
|
|
111
|
-
const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
112
|
-
|
|
113
|
-
function ensureStream() {
|
|
114
|
-
if (!streamOpen) {
|
|
115
|
-
send({ type: 'assistant_stream_start', agent: 'grok', ts: Date.now() });
|
|
116
|
-
streamOpen = true;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function endStream() {
|
|
121
|
-
textBatch.flush();
|
|
122
|
-
if (streamOpen) {
|
|
123
|
-
send({ type: 'assistant_stream_end', agent: 'grok' });
|
|
124
|
-
streamOpen = false;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function flushThought() {
|
|
129
|
-
if (!thoughtBatch.pending()) return;
|
|
130
|
-
if (!thoughtOpen) {
|
|
131
|
-
send({ type: 'thought_stream_start', agent: 'grok' });
|
|
132
|
-
thoughtOpen = true;
|
|
133
|
-
}
|
|
134
|
-
const n = thoughtBatch.pending();
|
|
135
|
-
thoughtBatch.flush();
|
|
136
|
-
thoughtChars += n;
|
|
61
|
+
// Grok's documented ACP client sample does not need client-side tool RPC.
|
|
62
|
+
// Return a structured error for optional extension calls instead of leaving
|
|
63
|
+
// an agent request hanging forever.
|
|
64
|
+
if (msg.id != null && msg.method) {
|
|
65
|
+
this.write({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: `Unsupported client method: ${msg.method}` } });
|
|
66
|
+
return;
|
|
137
67
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (thoughtOpen) {
|
|
143
|
-
send({ type: 'thought_stream_end', agent: 'grok' });
|
|
144
|
-
thoughtOpen = false;
|
|
145
|
-
}
|
|
146
|
-
endStream();
|
|
147
|
-
|
|
148
|
-
const name = raw.name || raw.tool || raw.toolName || raw.tool_name
|
|
149
|
-
|| raw.title || raw.data?.name || 'tool';
|
|
150
|
-
let input = raw.input ?? raw.arguments ?? raw.args ?? raw.rawInput
|
|
151
|
-
?? raw.data?.input ?? raw.data?.arguments ?? raw.data ?? {};
|
|
152
|
-
input = parseMaybeJson(input);
|
|
153
|
-
|
|
154
|
-
send({
|
|
155
|
-
type: 'assistant',
|
|
156
|
-
message: { content: [{ type: 'tool_use', name, input }] },
|
|
157
|
-
agent: 'grok',
|
|
158
|
-
ts: Date.now(),
|
|
159
|
-
});
|
|
68
|
+
if (msg.method === 'session/update') {
|
|
69
|
+
const sid = msg.params?.sessionId;
|
|
70
|
+
const fn = sid && this.listeners.get(sid);
|
|
71
|
+
if (fn) fn(msg.params?.update || {});
|
|
160
72
|
}
|
|
73
|
+
}
|
|
161
74
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// Non-JSON progress line — surface lightly
|
|
167
|
-
send({ type: 'status', message: line.slice(0, 200), agent: 'grok' });
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const t = String(raw.type || '').toLowerCase();
|
|
172
|
-
|
|
173
|
-
// ── Text tokens (batched ~50ms) ──────────────────────────────────────
|
|
174
|
-
if ((t === 'text' || t === 'message' || t === 'output_text') && raw.data != null) {
|
|
175
|
-
flushThought();
|
|
176
|
-
if (thoughtOpen) {
|
|
177
|
-
send({ type: 'thought_stream_end', agent: 'grok' });
|
|
178
|
-
thoughtOpen = false;
|
|
179
|
-
}
|
|
180
|
-
const chunk = String(raw.data);
|
|
181
|
-
if (!chunk) return;
|
|
182
|
-
ensureStream();
|
|
183
|
-
textChars += chunk.length;
|
|
184
|
-
textBatch.push(chunk);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ── Thinking tokens (batched stream) ────────────────────────────────
|
|
189
|
-
if ((t === 'thought' || t === 'thinking' || t === 'reasoning') && raw.data != null) {
|
|
190
|
-
if (!thoughtOpen) {
|
|
191
|
-
send({ type: 'thought_stream_start', agent: 'grok' });
|
|
192
|
-
thoughtOpen = true;
|
|
193
|
-
}
|
|
194
|
-
thoughtBatch.push(String(raw.data));
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ── Tools ───────────────────────────────────────────────────────────
|
|
199
|
-
if (
|
|
200
|
-
t === 'tool_use' || t === 'tool_call' || t === 'tool_start'
|
|
201
|
-
|| t === 'tool' || t === 'function_call' || t === 'tool_call_start'
|
|
202
|
-
|| t.startsWith('tool_')
|
|
203
|
-
) {
|
|
204
|
-
// Avoid treating tool_result as a new call
|
|
205
|
-
if (t === 'tool_result' || t === 'tool_call_update' || t === 'tool_end' || t === 'tool_call_end') {
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
emitTool(raw);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ── Session id early (some builds emit before end) ───────────────────
|
|
213
|
-
if ((t === 'session' || t === 'session_started') && (raw.sessionId || raw.session_id)) {
|
|
214
|
-
sessionId = raw.sessionId || raw.session_id;
|
|
215
|
-
entry.sessionId = sessionId;
|
|
216
|
-
activeSessions.set(sessionId, entry);
|
|
217
|
-
if (!options.sessionId) {
|
|
218
|
-
send({ type: 'session-created', sessionId, agent: 'grok' });
|
|
219
|
-
}
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ── End of turn ─────────────────────────────────────────────────────
|
|
224
|
-
if (t === 'end' || t === 'result' || t === 'done') {
|
|
225
|
-
gotEnd = true;
|
|
226
|
-
flushThought();
|
|
227
|
-
if (thoughtOpen) {
|
|
228
|
-
send({ type: 'thought_stream_end', agent: 'grok' });
|
|
229
|
-
thoughtOpen = false;
|
|
230
|
-
}
|
|
231
|
-
endStream();
|
|
232
|
-
|
|
233
|
-
if (raw.sessionId || raw.session_id) {
|
|
234
|
-
sessionId = raw.sessionId || raw.session_id;
|
|
235
|
-
entry.sessionId = sessionId;
|
|
236
|
-
activeSessions.set(sessionId, entry);
|
|
237
|
-
if (!options.sessionId) {
|
|
238
|
-
send({ type: 'session-created', sessionId, agent: 'grok' });
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Unlock UI immediately — process may linger after the turn ends.
|
|
243
|
-
entry.busy = false;
|
|
244
|
-
|
|
245
|
-
const u = raw.usage || {};
|
|
246
|
-
const costTicks = raw.total_cost_usd_ticks ?? u.cost_usd_ticks ?? u.costUsdTicks;
|
|
247
|
-
const costUSD = raw.total_cost_usd ?? u.cost_usd ?? u.costUSD
|
|
248
|
-
?? (typeof costTicks === 'number' ? costTicks / 1e10 : undefined);
|
|
75
|
+
write(data) {
|
|
76
|
+
if (!this.child.stdin?.writable) throw new Error('Grok ACP stdin is unavailable');
|
|
77
|
+
this.child.stdin.write(JSON.stringify(data) + '\n');
|
|
78
|
+
}
|
|
249
79
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
totalTokens: u.total_tokens || u.totalTokens
|
|
259
|
-
|| ((u.input_tokens || u.inputTokens || 0) + (u.output_tokens || u.outputTokens || 0)),
|
|
260
|
-
reasoningTokens: u.reasoning_tokens || u.reasoningTokens || 0,
|
|
261
|
-
costUSD,
|
|
262
|
-
},
|
|
263
|
-
sessionId: sid,
|
|
264
|
-
agent: 'grok',
|
|
265
|
-
stats: { textChars, thoughtChars },
|
|
266
|
-
});
|
|
267
|
-
// Early complete so client can send next turn without waiting for process exit.
|
|
268
|
-
send({ type: 'complete', exitCode: 0, sessionId: sid, agent: 'grok' });
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
80
|
+
request(method, params) {
|
|
81
|
+
const id = this.nextID++;
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
this.pending.set(id, { resolve, reject });
|
|
84
|
+
try { this.write({ jsonrpc: '2.0', id, method, params }); }
|
|
85
|
+
catch (e) { this.pending.delete(id); reject(e); }
|
|
86
|
+
});
|
|
87
|
+
}
|
|
271
88
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
89
|
+
async ensureSession(options) {
|
|
90
|
+
await this.ready;
|
|
91
|
+
if (options.sessionId) {
|
|
92
|
+
try {
|
|
93
|
+
await this.request('session/load', { sessionId: options.sessionId, cwd: options.cwd, mcpServers: [] });
|
|
94
|
+
} catch {
|
|
95
|
+
await this.request('session/resume', { sessionId: options.sessionId, cwd: options.cwd, mcpServers: [] });
|
|
275
96
|
}
|
|
97
|
+
return options.sessionId;
|
|
98
|
+
}
|
|
99
|
+
const result = await this.request('session/new', {
|
|
100
|
+
cwd: options.cwd || process.cwd(),
|
|
101
|
+
mcpServers: [],
|
|
102
|
+
_meta: { yoloMode: true },
|
|
276
103
|
});
|
|
104
|
+
if (!result.sessionId) throw new Error('Grok ACP did not return a session id');
|
|
105
|
+
return result.sessionId;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
277
108
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// Skip duplicate complete if turn already finished via streaming-json "end".
|
|
288
|
-
if (gotEnd && !aborted) {
|
|
289
|
-
resolve();
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
109
|
+
function clientFor(options, cliPath) {
|
|
110
|
+
const key = profileKey(options);
|
|
111
|
+
let client = clients.get(key);
|
|
112
|
+
if (!client || client.child.exitCode != null) {
|
|
113
|
+
client = new GrokACPClient({ cliPath, model: options.model, effort: options.effort });
|
|
114
|
+
clients.set(key, client);
|
|
115
|
+
}
|
|
116
|
+
return client;
|
|
117
|
+
}
|
|
292
118
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
119
|
+
function emitUpdate(update, send, sessionId, state) {
|
|
120
|
+
const kind = update?.sessionUpdate;
|
|
121
|
+
if (kind === 'agent_message_chunk' && update.content?.text) {
|
|
122
|
+
if (!state.textOpen) { send({ type: 'assistant_stream_start', agent: 'grok', ts: Date.now() }); state.textOpen = true; }
|
|
123
|
+
send({ type: 'assistant_delta', data: update.content.text, agent: 'grok' });
|
|
124
|
+
} else if (kind === 'agent_thought_chunk' && update.content?.text) {
|
|
125
|
+
if (!state.thoughtOpen) { send({ type: 'thought_stream_start', agent: 'grok' }); state.thoughtOpen = true; }
|
|
126
|
+
send({ type: 'thought_delta', data: update.content.text, agent: 'grok' });
|
|
127
|
+
} else if (kind === 'tool_call' || kind === 'tool_call_update') {
|
|
128
|
+
if (state.textOpen) { send({ type: 'assistant_stream_end', agent: 'grok' }); state.textOpen = false; }
|
|
129
|
+
if (state.thoughtOpen) { send({ type: 'thought_stream_end', agent: 'grok' }); state.thoughtOpen = false; }
|
|
130
|
+
if (kind === 'tool_call') {
|
|
131
|
+
send({ type: 'assistant', agent: 'grok', message: { content: [{
|
|
132
|
+
type: 'tool_use', name: update.title || update.kind || 'tool', input: update.rawInput || update.input || {},
|
|
133
|
+
}] } });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
308
137
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
138
|
+
async function runOne(item, { cliPath, activeSessions, queueKey }) {
|
|
139
|
+
const { command, options, send } = item;
|
|
140
|
+
const client = clientFor(options, cliPath);
|
|
141
|
+
const localKey = options.sessionId || crypto.randomUUID();
|
|
142
|
+
const entry = { kind: 'grok-acp', startTime: Date.now(), busy: true, key: localKey, sessionId: options.sessionId || null };
|
|
143
|
+
activeSessions.set(localKey, entry);
|
|
144
|
+
const state = { textOpen: false, thoughtOpen: false };
|
|
145
|
+
let sessionId = options.sessionId;
|
|
146
|
+
try {
|
|
147
|
+
sessionId = await client.ensureSession(options);
|
|
148
|
+
queueAliases.set(sessionId, queueKey);
|
|
149
|
+
entry.sessionId = sessionId;
|
|
150
|
+
activeSessions.set(sessionId, entry);
|
|
151
|
+
entry.abort = () => { client.write({ jsonrpc: '2.0', method: 'session/cancel', params: { sessionId } }); };
|
|
152
|
+
if (!options.sessionId) send({ type: 'session-created', sessionId, agent: 'grok' });
|
|
153
|
+
client.listeners.set(sessionId, update => emitUpdate(update, send, sessionId, state));
|
|
154
|
+
const result = await client.request('session/prompt', {
|
|
155
|
+
sessionId,
|
|
156
|
+
prompt: [{ type: 'text', text: command }],
|
|
315
157
|
});
|
|
158
|
+
if (state.textOpen) send({ type: 'assistant_stream_end', agent: 'grok' });
|
|
159
|
+
if (state.thoughtOpen) send({ type: 'thought_stream_end', agent: 'grok' });
|
|
160
|
+
entry.busy = false;
|
|
161
|
+
send({ type: 'result', is_error: result.stopReason === 'error', usage: result._meta?.usage || null, sessionId, agent: 'grok' });
|
|
162
|
+
send({ type: 'complete', exitCode: result.stopReason === 'cancelled' ? 130 : 0, aborted: result.stopReason === 'cancelled', sessionId, agent: 'grok' });
|
|
163
|
+
} catch (e) {
|
|
164
|
+
entry.busy = false;
|
|
165
|
+
send({ type: 'error', message: e.message, sessionId, agent: 'grok' });
|
|
166
|
+
send({ type: 'complete', exitCode: 1, sessionId, agent: 'grok' });
|
|
167
|
+
} finally {
|
|
168
|
+
if (sessionId) client.listeners.delete(sessionId);
|
|
169
|
+
activeSessions.delete(localKey);
|
|
170
|
+
if (sessionId) activeSessions.delete(sessionId);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
316
173
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
174
|
+
/** Queue all Grok follow-ups at the edge. The promise resolves after this item runs. */
|
|
175
|
+
export function runGrok(command, options, send, { cliPath, activeSessions }) {
|
|
176
|
+
const key = (options.sessionId && queueAliases.get(options.sessionId)) || options.sessionId || `new:${options.cwd || ''}`;
|
|
177
|
+
const queue = queues.get(key) || { items: [], running: false };
|
|
178
|
+
queues.set(key, queue);
|
|
179
|
+
return new Promise(resolve => {
|
|
180
|
+
queue.items.push({ command, options, send, resolve });
|
|
181
|
+
if (queue.running) {
|
|
182
|
+
send({ type: 'status', message: `Grok follow-up queued (${queue.items.length - 1} ahead)`, sessionId: options.sessionId, agent: 'grok' });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
queue.running = true;
|
|
186
|
+
const drain = async () => {
|
|
187
|
+
while (queue.items.length) {
|
|
188
|
+
const item = queue.items.shift();
|
|
189
|
+
await runOne(item, { cliPath, activeSessions, queueKey: key });
|
|
190
|
+
item.resolve();
|
|
191
|
+
}
|
|
192
|
+
queue.running = false;
|
|
193
|
+
queues.delete(key);
|
|
194
|
+
for (const [sid, alias] of queueAliases) if (alias === key) queueAliases.delete(sid);
|
|
321
195
|
};
|
|
322
|
-
|
|
323
|
-
// Tell UI a run started (typing indicator already on; optional status)
|
|
324
|
-
send({ type: 'status', message: 'Grok running…', agent: 'grok' });
|
|
196
|
+
void drain();
|
|
325
197
|
});
|
|
326
198
|
}
|
|
199
|
+
|
|
200
|
+
export async function interruptGrok(sessionId) {
|
|
201
|
+
for (const client of clients.values()) {
|
|
202
|
+
if (client.listeners.has(sessionId)) {
|
|
203
|
+
client.write({ jsonrpc: '2.0', method: 'session/cancel', params: { sessionId } });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|