claude-simple 0.2.7 → 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.
Files changed (59) hide show
  1. package/README.md +0 -0
  2. package/dist/app.0.2.8.min.js +1080 -0
  3. package/dist/index.html +2 -2
  4. package/dist/{style.0.2.7.min.css → style.0.2.8.min.css} +1 -1
  5. package/gateway.js +0 -0
  6. package/package.json +1 -1
  7. package/public/addon-fit.min.js +0 -0
  8. package/public/agent-panel.js +0 -0
  9. package/public/api.js +2 -0
  10. package/public/app.js +0 -0
  11. package/public/base.css +0 -0
  12. package/public/cache.js +0 -0
  13. package/public/chat.js +37 -6
  14. package/public/files.js +0 -0
  15. package/public/git.js +0 -0
  16. package/public/icons/icon-180.png +0 -0
  17. package/public/icons/icon-192.png +0 -0
  18. package/public/icons/icon-512.png +0 -0
  19. package/public/icons/icon-maskable-512.png +0 -0
  20. package/public/index.html +0 -0
  21. package/public/lib.js +0 -0
  22. package/public/manifest.webmanifest +0 -0
  23. package/public/mobile.js +0 -0
  24. package/public/notifications.js +215 -0
  25. package/public/offline.html +0 -0
  26. package/public/pwa.js +0 -0
  27. package/public/router.js +0 -0
  28. package/public/settings.js +0 -0
  29. package/public/shell/boot.js +0 -0
  30. package/public/shell/dashboard.js +46 -26
  31. package/public/shell/hub.js +0 -0
  32. package/public/shell/layout.js +2 -3
  33. package/public/shell/notes.js +0 -0
  34. package/public/shell/session-context.js +0 -0
  35. package/public/shell/session-list.js +5 -14
  36. package/public/shell/session-nav.js +18 -1
  37. package/public/shell/templates.js +18 -0
  38. package/public/shell/util.js +0 -0
  39. package/public/shell.js +24 -6
  40. package/public/state.js +49 -7
  41. package/public/style.css +0 -0
  42. package/public/sw.js +1 -1
  43. package/public/terminal.js +0 -0
  44. package/public/wake-lock.js +0 -0
  45. package/public/xterm.min.css +0 -0
  46. package/public/xterm.min.js +0 -0
  47. package/src/agents/claude.js +0 -0
  48. package/src/agents/codex.js +0 -0
  49. package/src/agents/grok.js +180 -299
  50. package/src/agents/meta-agent.js +0 -0
  51. package/src/agents/models.js +0 -0
  52. package/src/agents/sessions.js +0 -0
  53. package/src/agents/stream-batch.js +0 -0
  54. package/src/app.js +0 -0
  55. package/src/auto-update.js +0 -0
  56. package/src/runtime/approvals.js +0 -0
  57. package/src/runtime/outbox.js +0 -0
  58. package/src/runtime/protocol.js +0 -0
  59. package/dist/app.0.2.7.min.js +0 -1039
@@ -1,327 +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
- * Run Grok CLI headlessly with streaming-json over WebSocket (not HTTP SSE).
8
- *
9
- * New turn:
10
- * grok -p <prompt> --output-format streaming-json --cwd <dir> ...
11
- * Resume:
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 mapPermission(permissionMode, allowBypass) {
24
- if (allowBypass || permissionMode === 'bypassPermissions') return 'bypassPermissions';
25
- if (permissionMode == null) return 'bypassPermissions';
26
- if (permissionMode === 'default') return null;
27
- return permissionMode;
12
+ function profileKey(options) {
13
+ return `${options.model || ''}\u001f${options.effort || ''}`;
28
14
  }
29
15
 
30
- function buildArgs(command, options) {
31
- const args = [];
32
-
33
- if (options.sessionId) {
34
- args.push('--resume', options.sessionId);
35
- }
36
-
37
- // -p = headless single-turn (required for non-TTY streaming-json)
38
- args.push('-p', command || '');
39
- args.push('--output-format', 'streaming-json');
40
-
41
- if (options.cwd) args.push('--cwd', options.cwd);
42
- if (options.model) args.push('--model', options.model);
43
- if (options.effort) args.push('--reasoning-effort', options.effort);
44
-
45
- const perm = mapPermission(options.permissionMode, options.allowDangerouslySkipPermissions);
46
- if (perm) args.push('--permission-mode', perm);
47
- if (options.allowDangerouslySkipPermissions || options.permissionMode === 'bypassPermissions') {
48
- args.push('--always-approve');
49
- }
50
-
51
- if (options.newSessionId && !options.sessionId) {
52
- 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));
53
36
  }
54
37
 
55
- return args;
56
- }
57
-
58
- function parseMaybeJson(v) {
59
- if (v == null) return {};
60
- if (typeof v === 'object') return v;
61
- if (typeof v === 'string') {
62
- try { return JSON.parse(v); } catch { return { input: v }; }
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 } });
63
49
  }
64
- return { value: v };
65
- }
66
-
67
- export function runGrok(command, options, send, { cliPath, activeSessions }) {
68
- return new Promise((resolve) => {
69
- const sessionKey = options.sessionId || crypto.randomUUID();
70
- const bin = cliPath || 'grok';
71
- const args = buildArgs(command, options);
72
-
73
- const env = { ...process.env };
74
- env.CI = env.CI || '1';
75
- // Reduce pipe buffering if the runtime honors it
76
- env.PYTHONUNBUFFERED = '1';
77
50
 
78
- let child;
79
- try {
80
- child = spawn(bin, args, {
81
- cwd: options.cwd || process.cwd(),
82
- env,
83
- stdio: ['ignore', 'pipe', 'pipe'],
84
- });
85
- } catch (e) {
86
- send({ type: 'error', message: `Failed to start grok: ${e.message}`, agent: 'grok' });
87
- 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 || {});
88
59
  return;
89
60
  }
90
-
91
- // Unbuffer stdout as much as possible
92
- if (child.stdout?.setEncoding) child.stdout.setEncoding('utf8');
93
-
94
- const entry = { kind: 'grok', child, startTime: Date.now(), busy: true, key: sessionKey, sessionId: options.sessionId || null };
95
- activeSessions.set(sessionKey, entry);
96
-
97
- let sessionId = options.sessionId || null;
98
- let aborted = false;
99
- let stderr = '';
100
- let gotEnd = false;
101
- let streamOpen = false; // live assistant bubble on the client
102
- let thoughtOpen = false;
103
- let textChars = 0;
104
- let thoughtChars = 0;
105
-
106
- const textBatch = createDeltaBatcher(send, 'assistant_delta', { agent: 'grok' }, { maxWait: 48, maxSize: 480 });
107
- const thoughtBatch = createDeltaBatcher(send, 'thought_delta', { agent: 'grok' }, { maxWait: 140, maxSize: 600 });
108
-
109
- child.stderr.on('data', d => { stderr += d.toString(); });
110
-
111
- // Use 'readable' + line split for lower latency than waiting for large chunks
112
- const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
113
-
114
- function ensureStream() {
115
- if (!streamOpen) {
116
- send({ type: 'assistant_stream_start', agent: 'grok', ts: Date.now() });
117
- streamOpen = true;
118
- }
119
- }
120
-
121
- function endStream() {
122
- textBatch.flush();
123
- if (streamOpen) {
124
- send({ type: 'assistant_stream_end', agent: 'grok' });
125
- streamOpen = false;
126
- }
127
- }
128
-
129
- function flushThought() {
130
- if (!thoughtBatch.pending()) return;
131
- if (!thoughtOpen) {
132
- send({ type: 'thought_stream_start', agent: 'grok' });
133
- thoughtOpen = true;
134
- }
135
- const n = thoughtBatch.pending();
136
- thoughtBatch.flush();
137
- 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;
138
67
  }
139
-
140
- function emitTool(raw) {
141
- // Finalize any open text/thought before tool card
142
- flushThought();
143
- if (thoughtOpen) {
144
- send({ type: 'thought_stream_end', agent: 'grok' });
145
- thoughtOpen = false;
146
- }
147
- endStream();
148
-
149
- const name = raw.name || raw.tool || raw.toolName || raw.tool_name
150
- || raw.title || raw.data?.name || 'tool';
151
- let input = raw.input ?? raw.arguments ?? raw.args ?? raw.rawInput
152
- ?? raw.data?.input ?? raw.data?.arguments ?? raw.data ?? {};
153
- input = parseMaybeJson(input);
154
-
155
- send({
156
- type: 'assistant',
157
- message: { content: [{ type: 'tool_use', name, input }] },
158
- agent: 'grok',
159
- ts: Date.now(),
160
- });
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 || {});
161
72
  }
73
+ }
162
74
 
163
- rl.on('line', (line) => {
164
- if (!line.trim()) return;
165
- let raw;
166
- try { raw = JSON.parse(line); } catch {
167
- // Non-JSON progress line — surface lightly
168
- send({ type: 'status', message: line.slice(0, 200), agent: 'grok' });
169
- return;
170
- }
171
-
172
- const t = String(raw.type || '').toLowerCase();
173
-
174
- // ── Text tokens (batched ~50ms) ──────────────────────────────────────
175
- if ((t === 'text' || t === 'message' || t === 'output_text') && raw.data != null) {
176
- flushThought();
177
- if (thoughtOpen) {
178
- send({ type: 'thought_stream_end', agent: 'grok' });
179
- thoughtOpen = false;
180
- }
181
- const chunk = String(raw.data);
182
- if (!chunk) return;
183
- ensureStream();
184
- textChars += chunk.length;
185
- textBatch.push(chunk);
186
- return;
187
- }
188
-
189
- // ── Thinking tokens (batched stream) ────────────────────────────────
190
- if ((t === 'thought' || t === 'thinking' || t === 'reasoning') && raw.data != null) {
191
- if (!thoughtOpen) {
192
- send({ type: 'thought_stream_start', agent: 'grok' });
193
- thoughtOpen = true;
194
- }
195
- thoughtBatch.push(String(raw.data));
196
- return;
197
- }
198
-
199
- // ── Tools ───────────────────────────────────────────────────────────
200
- if (
201
- t === 'tool_use' || t === 'tool_call' || t === 'tool_start'
202
- || t === 'tool' || t === 'function_call' || t === 'tool_call_start'
203
- || t.startsWith('tool_')
204
- ) {
205
- // Avoid treating tool_result as a new call
206
- if (t === 'tool_result' || t === 'tool_call_update' || t === 'tool_end' || t === 'tool_call_end') {
207
- return;
208
- }
209
- emitTool(raw);
210
- return;
211
- }
212
-
213
- // ── Session id early (some builds emit before end) ───────────────────
214
- if ((t === 'session' || t === 'session_started') && (raw.sessionId || raw.session_id)) {
215
- sessionId = raw.sessionId || raw.session_id;
216
- entry.sessionId = sessionId;
217
- activeSessions.set(sessionId, entry);
218
- if (!options.sessionId) {
219
- send({ type: 'session-created', sessionId, agent: 'grok' });
220
- }
221
- return;
222
- }
223
-
224
- // ── End of turn ─────────────────────────────────────────────────────
225
- if (t === 'end' || t === 'result' || t === 'done') {
226
- gotEnd = true;
227
- flushThought();
228
- if (thoughtOpen) {
229
- send({ type: 'thought_stream_end', agent: 'grok' });
230
- thoughtOpen = false;
231
- }
232
- endStream();
233
-
234
- if (raw.sessionId || raw.session_id) {
235
- sessionId = raw.sessionId || raw.session_id;
236
- entry.sessionId = sessionId;
237
- activeSessions.set(sessionId, entry);
238
- if (!options.sessionId) {
239
- send({ type: 'session-created', sessionId, agent: 'grok' });
240
- }
241
- }
242
-
243
- // Unlock UI immediately — process may linger after the turn ends.
244
- entry.busy = false;
245
-
246
- const u = raw.usage || {};
247
- const costTicks = raw.total_cost_usd_ticks ?? u.cost_usd_ticks ?? u.costUsdTicks;
248
- const costUSD = raw.total_cost_usd ?? u.cost_usd ?? u.costUSD
249
- ?? (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
+ }
250
79
 
251
- const sid = sessionId || sessionKey;
252
- send({
253
- type: 'result',
254
- is_error: false,
255
- usage: {
256
- inputTokens: u.input_tokens || u.inputTokens || 0,
257
- outputTokens: u.output_tokens || u.outputTokens || 0,
258
- cacheReadInputTokens: u.cache_read_input_tokens || u.cachedReadTokens || 0,
259
- totalTokens: u.total_tokens || u.totalTokens
260
- || ((u.input_tokens || u.inputTokens || 0) + (u.output_tokens || u.outputTokens || 0)),
261
- reasoningTokens: u.reasoning_tokens || u.reasoningTokens || 0,
262
- costUSD,
263
- },
264
- sessionId: sid,
265
- agent: 'grok',
266
- stats: { textChars, thoughtChars },
267
- });
268
- // Early complete so client can send next turn without waiting for process exit.
269
- send({ type: 'complete', exitCode: 0, sessionId: sid, agent: 'grok' });
270
- return;
271
- }
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
+ }
272
88
 
273
- if (t === 'error') {
274
- entry.busy = false;
275
- send({ type: 'error', message: raw.message || raw.data || 'grok error', agent: 'grok' });
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: [] });
276
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 },
277
103
  });
104
+ if (!result.sessionId) throw new Error('Grok ACP did not return a session id');
105
+ return result.sessionId;
106
+ }
107
+ }
278
108
 
279
- child.on('close', (code) => {
280
- flushThought();
281
- if (thoughtOpen) send({ type: 'thought_stream_end', agent: 'grok' });
282
- endStream();
283
- rl.close();
284
- entry.busy = false;
285
- activeSessions.delete(sessionKey);
286
- if (sessionId) activeSessions.delete(sessionId);
287
-
288
- // Skip duplicate complete if turn already finished via streaming-json "end".
289
- if (gotEnd && !aborted) {
290
- resolve();
291
- return;
292
- }
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
+ }
293
118
 
294
- if (aborted) {
295
- send({ type: 'complete', exitCode: 130, aborted: true, sessionId: sessionId || sessionKey, agent: 'grok' });
296
- } else if (code !== 0 && code != null && !gotEnd) {
297
- const errMsg = stderr.trim().split('\n').filter(l => !/stdin is not a terminal/i.test(l)).join('\n')
298
- || `grok exited with code ${code}`;
299
- send({ type: 'error', message: errMsg, agent: 'grok' });
300
- send({ type: 'complete', exitCode: code, sessionId: sessionId || sessionKey, agent: 'grok' });
301
- } else {
302
- if (sessionId && !options.sessionId && !gotEnd) {
303
- send({ type: 'session-created', sessionId, agent: 'grok' });
304
- }
305
- send({ type: 'complete', exitCode: code ?? 0, sessionId: sessionId || sessionKey, agent: 'grok' });
306
- }
307
- resolve();
308
- });
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
+ }
309
137
 
310
- child.on('error', (e) => {
311
- entry.busy = false;
312
- activeSessions.delete(sessionKey);
313
- if (sessionId) activeSessions.delete(sessionId);
314
- send({ type: 'error', message: e.message, agent: 'grok' });
315
- resolve();
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 }],
316
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
+ }
317
173
 
318
- entry.abort = () => {
319
- aborted = true;
320
- try { child.kill('SIGTERM'); } catch {}
321
- setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000);
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);
322
195
  };
323
-
324
- // Tell UI a run started (typing indicator already on; optional status)
325
- send({ type: 'status', message: 'Grok running…', agent: 'grok' });
196
+ void drain();
326
197
  });
327
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
+ }
File without changes
File without changes
File without changes
File without changes
package/src/app.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes