claude-simple 0.2.8 → 0.2.9
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/dist/{app.0.2.8.min.js → app.0.2.9.min.js} +101 -101
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/public/chat.js +9 -6
- package/public/shell/session-list.js +17 -4
- package/public/state.js +15 -0
- package/src/agents/grok.js +300 -37
- package/src/agents/models.js +2 -1
- package/src/app.js +12 -2
- /package/dist/{style.0.2.8.min.css → style.0.2.9.min.css} +0 -0
package/dist/index.html
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
6
6
|
<title>Claude Code</title>
|
|
7
|
-
<link rel="stylesheet" href="/style.0.2.
|
|
7
|
+
<link rel="stylesheet" href="/style.0.2.9.min.css">
|
|
8
8
|
<link rel="stylesheet" href="/highlight.min.css">
|
|
9
9
|
<script src="/marked.min.js"></script>
|
|
10
10
|
<script src="/highlight.min.js"></script>
|
|
11
11
|
</head>
|
|
12
12
|
<body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
|
|
13
13
|
<div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
|
|
14
|
-
<script type="module" src="/app.0.2.
|
|
14
|
+
<script type="module" src="/app.0.2.9.min.js"></script>
|
|
15
15
|
</body>
|
|
16
16
|
</html>
|
package/package.json
CHANGED
package/public/chat.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { watch, delegate, esc, $ } from './lib.js';
|
|
3
3
|
import { ctx, isProcessing, currentProject, currentTab, currentModel, currentEffort, currentPermission,
|
|
4
4
|
currentAgent, setAgent, AGENT_LABELS, getDefaultModel, getModelsForAgent, getEffortsForModel,
|
|
5
|
-
addCustomModel, setCurrentModel, chatDensity, wsStatus } from './state.js';
|
|
5
|
+
getDefaultEffort, addCustomModel, setCurrentModel, chatDensity, wsStatus } from './state.js';
|
|
6
6
|
import { sendWs, api, authHeaders, flushWsQueue, clearWsQueue } from './api.js';
|
|
7
7
|
import { initWakeLock, setWakeLockDesired } from './wake-lock.js';
|
|
8
8
|
import { pushNotification } from './notifications.js';
|
|
@@ -773,16 +773,17 @@ export function sendMessage() {
|
|
|
773
773
|
|
|
774
774
|
// While a turn is running, queue the next message (common Grok complaint).
|
|
775
775
|
if (isProcessing.peek()) {
|
|
776
|
-
//
|
|
777
|
-
//
|
|
776
|
+
// Send immediately so a page reload cannot discard this follow-up in the
|
|
777
|
+
// tab. In hub mode the hub serializes it; standalone uses the edge queue.
|
|
778
778
|
if (currentAgent.peek() === 'grok' && ctx.sessionId) {
|
|
779
779
|
const agent = 'grok';
|
|
780
|
+
const effort = currentEffort.peek() || getDefaultEffort(agent, currentModel.peek()) || 'low';
|
|
780
781
|
const ok = sendWs({
|
|
781
782
|
type: 'command', cmd: 'turn.start', agent, command: text, sessionId: ctx.sessionId,
|
|
782
783
|
options: {
|
|
783
784
|
agent, cwd: currentProject.peek().path, sessionId: ctx.sessionId,
|
|
784
785
|
configDir: ctx.configDir, model: currentModel.peek(),
|
|
785
|
-
|
|
786
|
+
effort,
|
|
786
787
|
...(currentPermission.peek() && { permissionMode: currentPermission.peek() }),
|
|
787
788
|
...(currentPermission.peek() === 'bypassPermissions' && { allowDangerouslySkipPermissions: true }),
|
|
788
789
|
},
|
|
@@ -831,9 +832,11 @@ export function sendMessage() {
|
|
|
831
832
|
} else {
|
|
832
833
|
flushToolBatch();
|
|
833
834
|
appendMsg('user', 'You', text, { ts: now });
|
|
834
|
-
const effort = currentEffort.peek();
|
|
835
|
-
const permission = currentPermission.peek();
|
|
836
835
|
const agent = currentAgent.peek() || 'claude';
|
|
836
|
+
const effort = currentEffort.peek()
|
|
837
|
+
|| getDefaultEffort(agent, currentModel.peek())
|
|
838
|
+
|| (agent === 'grok' ? 'low' : '');
|
|
839
|
+
const permission = currentPermission.peek();
|
|
837
840
|
// Existing sessions retain their own picker value. New sessions are bound
|
|
838
841
|
// on the session-created event above.
|
|
839
842
|
if (ctx.sessionId) setCurrentModel(currentModel.peek(), agent, ctx.sessionId);
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
sidebarView, agentFilter, timeRange, activityHits, activityLoading,
|
|
8
8
|
sessionMetaMap, LOW_TURN_THRESHOLD, metaKey, sessionDisplayTitle,
|
|
9
9
|
hubMode, selectedMachineId, agentsMeta, currentEffort,
|
|
10
|
-
getModelsForAgent, getDefaultModel, getEffortsForModel,
|
|
10
|
+
getModelsForAgent, getDefaultModel, getEffortsForModel, getDefaultEffort,
|
|
11
11
|
addCustomModel, removeCustomModel, getCustomModels, setCurrentModel, ctx,
|
|
12
12
|
} from '../state.js';
|
|
13
13
|
import { api, NOT_MODIFIED } from '../api.js';
|
|
@@ -174,15 +174,28 @@ export function refreshEffortSelect() {
|
|
|
174
174
|
const agent = currentAgent.peek() || 'claude';
|
|
175
175
|
const model = currentModel.peek();
|
|
176
176
|
const efforts = getEffortsForModel(agent, model);
|
|
177
|
-
|
|
177
|
+
let cur = currentEffort.peek() || '';
|
|
178
|
+
if (!cur) {
|
|
179
|
+
const def = getDefaultEffort(agent, model);
|
|
180
|
+
if (def && efforts.includes(def)) {
|
|
181
|
+
cur = def;
|
|
182
|
+
currentEffort.value = def;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
178
185
|
const opts = [`<option value="">effort: off</option>`]
|
|
179
186
|
.concat(efforts.map(e =>
|
|
180
187
|
`<option value="${esc(e)}" ${e === cur ? 'selected' : ''}>${esc(e)}</option>`
|
|
181
188
|
));
|
|
182
189
|
sel.innerHTML = opts.join('');
|
|
183
190
|
if (cur && !efforts.includes(cur)) {
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
const def = getDefaultEffort(agent, model);
|
|
192
|
+
if (def && efforts.includes(def)) {
|
|
193
|
+
currentEffort.value = def;
|
|
194
|
+
sel.value = def;
|
|
195
|
+
} else {
|
|
196
|
+
currentEffort.value = '';
|
|
197
|
+
sel.value = '';
|
|
198
|
+
}
|
|
186
199
|
} else {
|
|
187
200
|
sel.value = cur;
|
|
188
201
|
}
|
package/public/state.js
CHANGED
|
@@ -285,6 +285,18 @@ export function getEffortsForModel(agent, modelId) {
|
|
|
285
285
|
return ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/** Default reasoning effort for agent/model (Grok → low). */
|
|
289
|
+
export function getDefaultEffort(agent, modelId) {
|
|
290
|
+
const a = (agent || 'claude').toLowerCase();
|
|
291
|
+
const meta = agentsMeta.peek()?.defaults?.[a];
|
|
292
|
+
if (meta?.effortDefault) return String(meta.effortDefault);
|
|
293
|
+
const models = getModelsForAgent(a);
|
|
294
|
+
const m = models.find(x => x.value === modelId);
|
|
295
|
+
if (m?.defaultEffort) return String(m.defaultEffort);
|
|
296
|
+
if (a === 'grok') return 'low';
|
|
297
|
+
return '';
|
|
298
|
+
}
|
|
299
|
+
|
|
288
300
|
export function setAgent(agent) {
|
|
289
301
|
const a = (agent || 'claude').toLowerCase();
|
|
290
302
|
currentAgent.value = a;
|
|
@@ -300,6 +312,9 @@ export function setAgent(agent) {
|
|
|
300
312
|
} else {
|
|
301
313
|
setCurrentModel(getDefaultModel(a), a);
|
|
302
314
|
}
|
|
315
|
+
const defEffort = getDefaultEffort(a, currentModel.peek());
|
|
316
|
+
if (defEffort) currentEffort.value = defEffort;
|
|
317
|
+
else if (a !== 'grok') currentEffort.value = '';
|
|
303
318
|
}
|
|
304
319
|
|
|
305
320
|
function matchesAgentFilter(s, filter) {
|
package/src/agents/grok.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
2
4
|
import readline from 'readline';
|
|
3
5
|
import crypto from 'crypto';
|
|
4
6
|
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// individual browser tab, serialize follow-up prompts.
|
|
7
|
+
// Persistent ACP (JSON-RPC over stdio). Advertise terminal+fs so Grok routes
|
|
8
|
+
// shell/file tools through client RPCs instead of its own broken terminal path.
|
|
8
9
|
const clients = new Map();
|
|
9
10
|
const queues = new Map();
|
|
10
11
|
const queueAliases = new Map();
|
|
@@ -13,6 +14,147 @@ function profileKey(options) {
|
|
|
13
14
|
return `${options.model || ''}\u001f${options.effort || ''}`;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
function envFromParams(envList) {
|
|
18
|
+
const env = { ...process.env };
|
|
19
|
+
if (!Array.isArray(envList)) return env;
|
|
20
|
+
for (const item of envList) {
|
|
21
|
+
if (!item || typeof item.name !== 'string') continue;
|
|
22
|
+
env[item.name] = item.value == null ? '' : String(item.value);
|
|
23
|
+
}
|
|
24
|
+
return env;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class TerminalHost {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.terminals = new Map();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
create(params = {}) {
|
|
33
|
+
const terminalId = `term_${crypto.randomUUID()}`;
|
|
34
|
+
const command = String(params.command || '');
|
|
35
|
+
const args = Array.isArray(params.args) ? params.args.map(String) : [];
|
|
36
|
+
const cwd = params.cwd || process.cwd();
|
|
37
|
+
const limit = Number.isFinite(params.outputByteLimit) && params.outputByteLimit > 0
|
|
38
|
+
? params.outputByteLimit
|
|
39
|
+
: 1024 * 1024;
|
|
40
|
+
const env = envFromParams(params.env);
|
|
41
|
+
|
|
42
|
+
let child;
|
|
43
|
+
try {
|
|
44
|
+
child = spawn(command, args, {
|
|
45
|
+
cwd,
|
|
46
|
+
env,
|
|
47
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
48
|
+
shell: args.length === 0,
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const err = new Error(error.message || 'Failed to start terminal');
|
|
52
|
+
err.code = -32000;
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const entry = {
|
|
57
|
+
id: terminalId,
|
|
58
|
+
child,
|
|
59
|
+
output: '',
|
|
60
|
+
truncated: false,
|
|
61
|
+
limit,
|
|
62
|
+
exitCode: null,
|
|
63
|
+
signal: null,
|
|
64
|
+
exited: false,
|
|
65
|
+
waiters: [],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const append = chunk => {
|
|
69
|
+
entry.output += chunk;
|
|
70
|
+
if (Buffer.byteLength(entry.output, 'utf8') > entry.limit) {
|
|
71
|
+
// Truncate from the start at a UTF-8-safe boundary.
|
|
72
|
+
let buf = Buffer.from(entry.output, 'utf8');
|
|
73
|
+
buf = buf.subarray(buf.length - entry.limit);
|
|
74
|
+
while (buf.length && (buf[0] & 0xc0) === 0x80) buf = buf.subarray(1);
|
|
75
|
+
entry.output = buf.toString('utf8');
|
|
76
|
+
entry.truncated = true;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
child.stdout?.setEncoding('utf8');
|
|
81
|
+
child.stderr?.setEncoding('utf8');
|
|
82
|
+
child.stdout?.on('data', append);
|
|
83
|
+
child.stderr?.on('data', append);
|
|
84
|
+
child.on('error', err => {
|
|
85
|
+
append(`\n[terminal error] ${err.message}\n`);
|
|
86
|
+
if (!entry.exited) {
|
|
87
|
+
entry.exited = true;
|
|
88
|
+
entry.exitCode = 1;
|
|
89
|
+
entry.signal = null;
|
|
90
|
+
for (const resolve of entry.waiters) resolve({ exitCode: 1, signal: null });
|
|
91
|
+
entry.waiters = [];
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
child.on('close', (code, signal) => {
|
|
95
|
+
entry.exited = true;
|
|
96
|
+
entry.exitCode = code;
|
|
97
|
+
entry.signal = signal || null;
|
|
98
|
+
for (const resolve of entry.waiters) resolve({ exitCode: code, signal: signal || null });
|
|
99
|
+
entry.waiters = [];
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.terminals.set(terminalId, entry);
|
|
103
|
+
return { terminalId };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get(terminalId) {
|
|
107
|
+
const entry = this.terminals.get(terminalId);
|
|
108
|
+
if (!entry) {
|
|
109
|
+
const err = new Error(`Unknown terminal: ${terminalId}`);
|
|
110
|
+
err.code = -32000;
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
return entry;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
output(terminalId) {
|
|
117
|
+
const entry = this.get(terminalId);
|
|
118
|
+
const result = {
|
|
119
|
+
output: entry.output,
|
|
120
|
+
truncated: entry.truncated,
|
|
121
|
+
};
|
|
122
|
+
if (entry.exited) {
|
|
123
|
+
result.exitStatus = { exitCode: entry.exitCode, signal: entry.signal };
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
waitForExit(terminalId) {
|
|
129
|
+
const entry = this.get(terminalId);
|
|
130
|
+
if (entry.exited) return Promise.resolve({ exitCode: entry.exitCode, signal: entry.signal });
|
|
131
|
+
return new Promise(resolve => entry.waiters.push(resolve));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
kill(terminalId) {
|
|
135
|
+
const entry = this.get(terminalId);
|
|
136
|
+
if (!entry.exited && entry.child?.pid) {
|
|
137
|
+
try { entry.child.kill('SIGTERM'); } catch { /* already gone */ }
|
|
138
|
+
}
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
release(terminalId) {
|
|
143
|
+
const entry = this.terminals.get(terminalId);
|
|
144
|
+
if (!entry) return {};
|
|
145
|
+
if (!entry.exited && entry.child?.pid) {
|
|
146
|
+
try { entry.child.kill('SIGTERM'); } catch { /* already gone */ }
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
if (!entry.exited && entry.child?.pid) {
|
|
149
|
+
try { entry.child.kill('SIGKILL'); } catch { /* already gone */ }
|
|
150
|
+
}
|
|
151
|
+
}, 800).unref?.();
|
|
152
|
+
}
|
|
153
|
+
this.terminals.delete(terminalId);
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
16
158
|
class GrokACPClient {
|
|
17
159
|
constructor({ cliPath, model, effort }) {
|
|
18
160
|
const args = ['agent', '--always-approve'];
|
|
@@ -22,13 +164,17 @@ class GrokACPClient {
|
|
|
22
164
|
this.child = spawn(cliPath || 'grok', args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
23
165
|
this.pending = new Map();
|
|
24
166
|
this.listeners = new Map();
|
|
25
|
-
|
|
167
|
+
// High ids avoid colliding with agent-originated request ids (often 0..N).
|
|
168
|
+
this.nextID = 100000;
|
|
169
|
+
this.terminals = new TerminalHost();
|
|
26
170
|
this.ready = this.initialize();
|
|
27
171
|
this.child.stderr.on('data', d => console.warn(`[grok acp] ${String(d).trim()}`));
|
|
28
172
|
this.child.on('close', () => {
|
|
29
173
|
for (const { reject } of this.pending.values()) reject(new Error('Grok ACP process stopped'));
|
|
30
174
|
this.pending.clear();
|
|
31
|
-
for (const fn of this.listeners.values())
|
|
175
|
+
for (const fn of this.listeners.values()) {
|
|
176
|
+
fn({ type: 'error', message: 'Grok ACP process stopped', agent: 'grok' });
|
|
177
|
+
}
|
|
32
178
|
this.listeners.clear();
|
|
33
179
|
});
|
|
34
180
|
const rl = readline.createInterface({ input: this.child.stdout, crlfDelay: Infinity });
|
|
@@ -38,19 +184,82 @@ class GrokACPClient {
|
|
|
38
184
|
async initialize() {
|
|
39
185
|
const init = await this.request('initialize', {
|
|
40
186
|
protocolVersion: 1,
|
|
41
|
-
clientCapabilities: {
|
|
42
|
-
|
|
187
|
+
clientCapabilities: {
|
|
188
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
189
|
+
terminal: true,
|
|
190
|
+
},
|
|
191
|
+
clientInfo: { name: 'claude-simple-ui', version: '0.2.9' },
|
|
43
192
|
});
|
|
44
193
|
const methods = new Set((init.authMethods || []).map(x => x?.id));
|
|
45
194
|
const methodId = process.env.XAI_API_KEY && methods.has('xai.api_key')
|
|
46
195
|
? 'xai.api_key'
|
|
47
196
|
: (methods.has('cached_token') ? 'cached_token' : null);
|
|
48
197
|
if (methodId) await this.request('authenticate', { methodId, _meta: { headless: true } });
|
|
198
|
+
return init;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async handleClientRequest(msg) {
|
|
202
|
+
const method = msg.method;
|
|
203
|
+
const params = msg.params || {};
|
|
204
|
+
try {
|
|
205
|
+
let result = {};
|
|
206
|
+
if (method === 'fs/read_text_file') {
|
|
207
|
+
const filePath = String(params.path || '');
|
|
208
|
+
const content = await fs.promises.readFile(filePath, 'utf8');
|
|
209
|
+
const limit = Number.isFinite(params.limit) ? params.limit : null;
|
|
210
|
+
const line = Number.isFinite(params.line) ? params.line : null;
|
|
211
|
+
if (line != null || limit != null) {
|
|
212
|
+
const lines = content.split('\n');
|
|
213
|
+
const start = Math.max(0, (line || 1) - 1);
|
|
214
|
+
const end = limit != null ? start + limit : lines.length;
|
|
215
|
+
result = { content: lines.slice(start, end).join('\n') };
|
|
216
|
+
} else {
|
|
217
|
+
result = { content };
|
|
218
|
+
}
|
|
219
|
+
} else if (method === 'fs/write_text_file') {
|
|
220
|
+
const filePath = String(params.path || '');
|
|
221
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
222
|
+
await fs.promises.writeFile(filePath, params.content ?? '', 'utf8');
|
|
223
|
+
result = {};
|
|
224
|
+
} else if (method === 'terminal/create') {
|
|
225
|
+
result = this.terminals.create(params);
|
|
226
|
+
} else if (method === 'terminal/output') {
|
|
227
|
+
result = this.terminals.output(params.terminalId);
|
|
228
|
+
} else if (method === 'terminal/wait_for_exit') {
|
|
229
|
+
result = await this.terminals.waitForExit(params.terminalId);
|
|
230
|
+
} else if (method === 'terminal/kill') {
|
|
231
|
+
result = this.terminals.kill(params.terminalId);
|
|
232
|
+
} else if (method === 'terminal/release') {
|
|
233
|
+
result = this.terminals.release(params.terminalId);
|
|
234
|
+
} else {
|
|
235
|
+
this.write({
|
|
236
|
+
jsonrpc: '2.0',
|
|
237
|
+
id: msg.id,
|
|
238
|
+
error: { code: -32601, message: `Unsupported client method: ${method}` },
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this.write({ jsonrpc: '2.0', id: msg.id, result });
|
|
243
|
+
} catch (error) {
|
|
244
|
+
this.write({
|
|
245
|
+
jsonrpc: '2.0',
|
|
246
|
+
id: msg.id,
|
|
247
|
+
error: { code: error.code || -32000, message: error.message || 'Client request failed' },
|
|
248
|
+
});
|
|
249
|
+
}
|
|
49
250
|
}
|
|
50
251
|
|
|
51
252
|
receive(line) {
|
|
52
253
|
let msg;
|
|
53
254
|
try { msg = JSON.parse(line); } catch { return; }
|
|
255
|
+
|
|
256
|
+
// Agent→client requests always carry a method. Check before pending so
|
|
257
|
+
// overlapping numeric ids cannot steal a client RPC as our response.
|
|
258
|
+
if (msg.id != null && msg.method) {
|
|
259
|
+
void this.handleClientRequest(msg);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
54
263
|
if (msg.id != null && this.pending.has(msg.id)) {
|
|
55
264
|
const pending = this.pending.get(msg.id);
|
|
56
265
|
this.pending.delete(msg.id);
|
|
@@ -58,13 +267,7 @@ class GrokACPClient {
|
|
|
58
267
|
else pending.resolve(msg.result || {});
|
|
59
268
|
return;
|
|
60
269
|
}
|
|
61
|
-
|
|
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;
|
|
67
|
-
}
|
|
270
|
+
|
|
68
271
|
if (msg.method === 'session/update') {
|
|
69
272
|
const sid = msg.params?.sessionId;
|
|
70
273
|
const fn = sid && this.listeners.get(sid);
|
|
@@ -109,37 +312,68 @@ class GrokACPClient {
|
|
|
109
312
|
function clientFor(options, cliPath) {
|
|
110
313
|
const key = profileKey(options);
|
|
111
314
|
let client = clients.get(key);
|
|
112
|
-
if (!client || client.child.exitCode != null) {
|
|
315
|
+
if (!client || client.child.exitCode != null || client.child.signalCode != null) {
|
|
113
316
|
client = new GrokACPClient({ cliPath, model: options.model, effort: options.effort });
|
|
114
317
|
clients.set(key, client);
|
|
115
318
|
}
|
|
116
319
|
return client;
|
|
117
320
|
}
|
|
118
321
|
|
|
119
|
-
function emitUpdate(update, send,
|
|
322
|
+
function emitUpdate(update, send, state) {
|
|
120
323
|
const kind = update?.sessionUpdate;
|
|
121
324
|
if (kind === 'agent_message_chunk' && update.content?.text) {
|
|
122
|
-
if (
|
|
325
|
+
if (state.thoughtOpen) {
|
|
326
|
+
send({ type: 'thought_stream_end', agent: 'grok' });
|
|
327
|
+
state.thoughtOpen = false;
|
|
328
|
+
}
|
|
329
|
+
if (!state.textOpen) {
|
|
330
|
+
send({ type: 'assistant_stream_start', agent: 'grok', ts: Date.now() });
|
|
331
|
+
state.textOpen = true;
|
|
332
|
+
}
|
|
123
333
|
send({ type: 'assistant_delta', data: update.content.text, agent: 'grok' });
|
|
124
334
|
} else if (kind === 'agent_thought_chunk' && update.content?.text) {
|
|
125
|
-
if (!state.thoughtOpen) {
|
|
335
|
+
if (!state.thoughtOpen) {
|
|
336
|
+
send({ type: 'thought_stream_start', agent: 'grok' });
|
|
337
|
+
state.thoughtOpen = true;
|
|
338
|
+
}
|
|
126
339
|
send({ type: 'thought_delta', data: update.content.text, agent: 'grok' });
|
|
127
340
|
} else if (kind === 'tool_call' || kind === 'tool_call_update') {
|
|
128
|
-
if (state.textOpen) {
|
|
129
|
-
|
|
341
|
+
if (state.textOpen) {
|
|
342
|
+
send({ type: 'assistant_stream_end', agent: 'grok' });
|
|
343
|
+
state.textOpen = false;
|
|
344
|
+
}
|
|
345
|
+
if (state.thoughtOpen) {
|
|
346
|
+
send({ type: 'thought_stream_end', agent: 'grok' });
|
|
347
|
+
state.thoughtOpen = false;
|
|
348
|
+
}
|
|
130
349
|
if (kind === 'tool_call') {
|
|
131
|
-
send({
|
|
132
|
-
type: '
|
|
133
|
-
|
|
350
|
+
send({
|
|
351
|
+
type: 'assistant',
|
|
352
|
+
agent: 'grok',
|
|
353
|
+
message: {
|
|
354
|
+
content: [{
|
|
355
|
+
type: 'tool_use',
|
|
356
|
+
name: update.title || update.kind || 'tool',
|
|
357
|
+
input: update.rawInput || update.input || {},
|
|
358
|
+
}],
|
|
359
|
+
},
|
|
360
|
+
});
|
|
134
361
|
}
|
|
135
362
|
}
|
|
136
363
|
}
|
|
137
364
|
|
|
138
365
|
async function runOne(item, { cliPath, activeSessions, queueKey }) {
|
|
139
366
|
const { command, options, send } = item;
|
|
140
|
-
const
|
|
367
|
+
const effort = options.effort || 'low';
|
|
368
|
+
const client = clientFor({ ...options, effort }, cliPath);
|
|
141
369
|
const localKey = options.sessionId || crypto.randomUUID();
|
|
142
|
-
const entry = {
|
|
370
|
+
const entry = {
|
|
371
|
+
kind: 'grok-acp',
|
|
372
|
+
startTime: Date.now(),
|
|
373
|
+
busy: true,
|
|
374
|
+
key: localKey,
|
|
375
|
+
sessionId: options.sessionId || null,
|
|
376
|
+
};
|
|
143
377
|
activeSessions.set(localKey, entry);
|
|
144
378
|
const state = { textOpen: false, thoughtOpen: false };
|
|
145
379
|
let sessionId = options.sessionId;
|
|
@@ -148,9 +382,14 @@ async function runOne(item, { cliPath, activeSessions, queueKey }) {
|
|
|
148
382
|
queueAliases.set(sessionId, queueKey);
|
|
149
383
|
entry.sessionId = sessionId;
|
|
150
384
|
activeSessions.set(sessionId, entry);
|
|
151
|
-
entry.abort = () => {
|
|
385
|
+
entry.abort = () => {
|
|
386
|
+
try {
|
|
387
|
+
client.write({ jsonrpc: '2.0', method: 'session/cancel', params: { sessionId } });
|
|
388
|
+
} catch { /* process may already be gone */ }
|
|
389
|
+
};
|
|
152
390
|
if (!options.sessionId) send({ type: 'session-created', sessionId, agent: 'grok' });
|
|
153
|
-
client.listeners.set(sessionId, update => emitUpdate(update, send,
|
|
391
|
+
client.listeners.set(sessionId, update => emitUpdate(update, send, state));
|
|
392
|
+
send({ type: 'status', message: 'Grok ACP running…', agent: 'grok', sessionId });
|
|
154
393
|
const result = await client.request('session/prompt', {
|
|
155
394
|
sessionId,
|
|
156
395
|
prompt: [{ type: 'text', text: command }],
|
|
@@ -158,10 +397,24 @@ async function runOne(item, { cliPath, activeSessions, queueKey }) {
|
|
|
158
397
|
if (state.textOpen) send({ type: 'assistant_stream_end', agent: 'grok' });
|
|
159
398
|
if (state.thoughtOpen) send({ type: 'thought_stream_end', agent: 'grok' });
|
|
160
399
|
entry.busy = false;
|
|
161
|
-
send({
|
|
162
|
-
|
|
400
|
+
send({
|
|
401
|
+
type: 'result',
|
|
402
|
+
is_error: result.stopReason === 'error',
|
|
403
|
+
usage: result._meta?.usage || null,
|
|
404
|
+
sessionId,
|
|
405
|
+
agent: 'grok',
|
|
406
|
+
});
|
|
407
|
+
send({
|
|
408
|
+
type: 'complete',
|
|
409
|
+
exitCode: result.stopReason === 'cancelled' ? 130 : 0,
|
|
410
|
+
aborted: result.stopReason === 'cancelled',
|
|
411
|
+
sessionId,
|
|
412
|
+
agent: 'grok',
|
|
413
|
+
});
|
|
163
414
|
} catch (e) {
|
|
164
415
|
entry.busy = false;
|
|
416
|
+
if (state.textOpen) send({ type: 'assistant_stream_end', agent: 'grok' });
|
|
417
|
+
if (state.thoughtOpen) send({ type: 'thought_stream_end', agent: 'grok' });
|
|
165
418
|
send({ type: 'error', message: e.message, sessionId, agent: 'grok' });
|
|
166
419
|
send({ type: 'complete', exitCode: 1, sessionId, agent: 'grok' });
|
|
167
420
|
} finally {
|
|
@@ -171,19 +424,26 @@ async function runOne(item, { cliPath, activeSessions, queueKey }) {
|
|
|
171
424
|
}
|
|
172
425
|
}
|
|
173
426
|
|
|
174
|
-
/** Queue all Grok follow-ups at the edge.
|
|
427
|
+
/** Queue all Grok follow-ups at the edge. Promise resolves after this item runs. */
|
|
175
428
|
export function runGrok(command, options, send, { cliPath, activeSessions }) {
|
|
176
|
-
const key = (options.sessionId && queueAliases.get(options.sessionId))
|
|
429
|
+
const key = (options.sessionId && queueAliases.get(options.sessionId))
|
|
430
|
+
|| options.sessionId
|
|
431
|
+
|| `new:${options.cwd || ''}`;
|
|
177
432
|
const queue = queues.get(key) || { items: [], running: false };
|
|
178
433
|
queues.set(key, queue);
|
|
179
434
|
return new Promise(resolve => {
|
|
180
435
|
queue.items.push({ command, options, send, resolve });
|
|
181
436
|
if (queue.running) {
|
|
182
|
-
send({
|
|
437
|
+
send({
|
|
438
|
+
type: 'status',
|
|
439
|
+
message: `Grok follow-up queued (${queue.items.length - 1} ahead)`,
|
|
440
|
+
sessionId: options.sessionId,
|
|
441
|
+
agent: 'grok',
|
|
442
|
+
});
|
|
183
443
|
return;
|
|
184
444
|
}
|
|
185
445
|
queue.running = true;
|
|
186
|
-
|
|
446
|
+
void (async () => {
|
|
187
447
|
while (queue.items.length) {
|
|
188
448
|
const item = queue.items.shift();
|
|
189
449
|
await runOne(item, { cliPath, activeSessions, queueKey: key });
|
|
@@ -192,16 +452,19 @@ export function runGrok(command, options, send, { cliPath, activeSessions }) {
|
|
|
192
452
|
queue.running = false;
|
|
193
453
|
queues.delete(key);
|
|
194
454
|
for (const [sid, alias] of queueAliases) if (alias === key) queueAliases.delete(sid);
|
|
195
|
-
};
|
|
196
|
-
void drain();
|
|
455
|
+
})();
|
|
197
456
|
});
|
|
198
457
|
}
|
|
199
458
|
|
|
200
459
|
export async function interruptGrok(sessionId) {
|
|
201
460
|
for (const client of clients.values()) {
|
|
202
461
|
if (client.listeners.has(sessionId)) {
|
|
203
|
-
|
|
204
|
-
|
|
462
|
+
try {
|
|
463
|
+
client.write({ jsonrpc: '2.0', method: 'session/cancel', params: { sessionId } });
|
|
464
|
+
return true;
|
|
465
|
+
} catch {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
205
468
|
}
|
|
206
469
|
}
|
|
207
470
|
return false;
|
package/src/agents/models.js
CHANGED
|
@@ -304,7 +304,8 @@ async function discoverGrok(grokHome) {
|
|
|
304
304
|
|
|
305
305
|
const active = models.find(m => m.value === defaultModel);
|
|
306
306
|
const efforts = active?.efforts || DEFAULT_EFFORTS;
|
|
307
|
-
|
|
307
|
+
// This UI defaults Grok reasoning effort to low.
|
|
308
|
+
const effortDefault = 'low';
|
|
308
309
|
|
|
309
310
|
return {
|
|
310
311
|
model: defaultModel,
|
package/src/app.js
CHANGED
|
@@ -1485,8 +1485,18 @@ export function createApp() {
|
|
|
1485
1485
|
}
|
|
1486
1486
|
agentSend(data);
|
|
1487
1487
|
};
|
|
1488
|
-
|
|
1489
|
-
|
|
1488
|
+
try {
|
|
1489
|
+
await dispatchAgent(agent, cmd.command || '', options, runSend);
|
|
1490
|
+
} finally {
|
|
1491
|
+
// `complete` is a UI-facing agent event and is not a reliable
|
|
1492
|
+
// scheduling boundary (an adapter may emit it early or more than
|
|
1493
|
+
// once). The hub advances its Grok FIFO only after dispatchAgent has
|
|
1494
|
+
// actually returned from the ACP request.
|
|
1495
|
+
if (agent === 'grok') {
|
|
1496
|
+
runOutbox.send({ type: 'hub-turn-finished', agent, sessionId: runSessionId });
|
|
1497
|
+
}
|
|
1498
|
+
runOutbox.flush();
|
|
1499
|
+
}
|
|
1490
1500
|
return;
|
|
1491
1501
|
}
|
|
1492
1502
|
|
|
File without changes
|