glad-web 1.0.18 → 1.0.20

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 CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  Glad is a local-first Web interface for terminal-based AI coding tools.
7
7
 
8
- It lets you run interactive CLI tools such as **Claude Code**, **Aider**, **GitHub Copilot CLI**, and **Gemini CLI** on your machine, then access them through a clean browser UI from desktop or mobile devices on your local network.
8
+ It lets you run interactive CLI tools such as **Claude Code**, **Aider**, **GitHub Copilot CLI**, and **Codex** on your machine, then access them through a clean browser UI from desktop or mobile devices on your local network.
9
9
 
10
10
  ![Glad AI mobile interface](./assets/demo.jpg)
11
11
 
@@ -122,7 +122,7 @@ glad tools detect
122
122
 
123
123
  ## Supported Tools
124
124
 
125
- Glad currently auto-detects the 21 terminal AI tools defined in the code registry. The names below are the registry `displayName` values used by Glad:
125
+ Glad currently auto-detects the 20 terminal AI tools defined in the code registry. The names below are the registry `displayName` values used by Glad:
126
126
 
127
127
  | Tool | Detected command |
128
128
  | --- | --- |
@@ -131,7 +131,6 @@ Glad currently auto-detects the 21 terminal AI tools defined in the code registr
131
131
  | Codex | `codex` |
132
132
  | Copilot | `copilot` |
133
133
  | Cody | `cody chat` |
134
- | Gemini | `gemini` |
135
134
  | Antigravity | `agy` |
136
135
  | Continue | `cn` |
137
136
  | Cursor | `cursor-agent` |
package/README.zh-CN.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  Glad 是一个面向终端 AI 编码工具的本地优先 Web 界面。
7
7
 
8
- 它让你可以在自己的机器上运行 **Claude Code**、**Aider**、**GitHub Copilot CLI**、**Gemini CLI** 等交互式命令行工具,并通过一个适合桌面和移动端访问的浏览器界面来使用它们。
8
+ 它让你可以在自己的机器上运行 **Claude Code**、**Aider**、**GitHub Copilot CLI**、**Codex** 等交互式命令行工具,并通过一个适合桌面和移动端访问的浏览器界面来使用它们。
9
9
 
10
10
  ![Glad AI 移动端界面](./assets/demo.jpg)
11
11
 
@@ -122,7 +122,7 @@ glad tools detect
122
122
 
123
123
  ## 支持的工具
124
124
 
125
- Glad 当前会自动检测代码注册表中定义的 21 个终端 AI 工具。下面的名称严格使用 Glad 注册表里的 `displayName`:
125
+ Glad 当前会自动检测代码注册表中定义的 20 个终端 AI 工具。下面的名称严格使用 Glad 注册表里的 `displayName`:
126
126
 
127
127
  | 工具 | 检测命令 |
128
128
  | --- | --- |
@@ -131,7 +131,6 @@ Glad 当前会自动检测代码注册表中定义的 21 个终端 AI 工具。
131
131
  | Codex | `codex` |
132
132
  | Copilot | `copilot` |
133
133
  | Cody | `cody chat` |
134
- | Gemini | `gemini` |
135
134
  | Antigravity | `agy` |
136
135
  | Continue | `cn` |
137
136
  | Cursor | `cursor-agent` |
@@ -59,15 +59,6 @@ const AI_TOOLS = {
59
59
  website: 'https://sourcegraph.com/cody',
60
60
  checkInstalled: async () => await commandExists('cody')
61
61
  },
62
- 'gemini': {
63
- key: 'gemini',
64
- command: 'gemini',
65
- args: [],
66
- displayName: 'Gemini',
67
- description: 'Official Google Gemini CLI with 1M token context',
68
- website: 'https://developers.google.com/gemini-code-assist',
69
- checkInstalled: async () => await commandExists('gemini')
70
- },
71
62
  'antigravity': {
72
63
  key: 'antigravity',
73
64
  command: 'agy',
@@ -0,0 +1,82 @@
1
+ const MODEL_ALIASES = new Set(['default', 'env', 'sonnet', 'opus', 'haiku']);
2
+ const EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
3
+
4
+ function cleanEnvValue(value) {
5
+ const text = String(value || '').trim();
6
+ return text || null;
7
+ }
8
+
9
+ function readClaudeEnv(env = process.env) {
10
+ return {
11
+ anthropicModel: cleanEnvValue(env.ANTHROPIC_MODEL),
12
+ sonnetModel: cleanEnvValue(env.ANTHROPIC_DEFAULT_SONNET_MODEL),
13
+ opusModel: cleanEnvValue(env.ANTHROPIC_DEFAULT_OPUS_MODEL),
14
+ haikuModel: cleanEnvValue(env.ANTHROPIC_DEFAULT_HAIKU_MODEL),
15
+ subagentModel: cleanEnvValue(env.CLAUDE_CODE_SUBAGENT_MODEL),
16
+ effort: normalizeEffort(env.CLAUDE_CODE_EFFORT_LEVEL)
17
+ };
18
+ }
19
+
20
+ function normalizeModel(value, env = process.env) {
21
+ const model = String(value || '').trim();
22
+ if (model) return model;
23
+ return readClaudeEnv(env).anthropicModel ? 'env' : 'sonnet';
24
+ }
25
+
26
+ function normalizeEffort(value, env = process.env) {
27
+ const effort = String(value || '').trim().toLowerCase();
28
+ if (EFFORT_LEVELS.has(effort)) return effort;
29
+ const envEffort = String(env.CLAUDE_CODE_EFFORT_LEVEL || '').trim().toLowerCase();
30
+ return EFFORT_LEVELS.has(envEffort) ? envEffort : 'medium';
31
+ }
32
+
33
+ function resolveClaudeModel(value, env = process.env) {
34
+ const selected = normalizeModel(value, env);
35
+ const config = readClaudeEnv(env);
36
+ if (selected === 'default') return undefined;
37
+ if (selected === 'env') return config.anthropicModel || undefined;
38
+ if (selected === 'sonnet') return config.sonnetModel || 'sonnet';
39
+ if (selected === 'opus') return config.opusModel || 'opus';
40
+ if (selected === 'haiku') return config.haikuModel || 'haiku';
41
+ return selected;
42
+ }
43
+
44
+ function modelOption(value, label, resolved, source) {
45
+ return {
46
+ value,
47
+ label,
48
+ resolved: resolved || null,
49
+ source
50
+ };
51
+ }
52
+
53
+ function getClaudeRuntimeConfig(env = process.env) {
54
+ const config = readClaudeEnv(env);
55
+ const options = [
56
+ modelOption('default', 'Default', null, 'Claude default')
57
+ ];
58
+ if (config.anthropicModel) {
59
+ options.push(modelOption('env', 'Env', config.anthropicModel, 'ANTHROPIC_MODEL'));
60
+ }
61
+ options.push(
62
+ modelOption('sonnet', 'Sonnet', config.sonnetModel || 'sonnet', 'ANTHROPIC_DEFAULT_SONNET_MODEL'),
63
+ modelOption('opus', 'Opus', config.opusModel || 'opus', 'ANTHROPIC_DEFAULT_OPUS_MODEL'),
64
+ modelOption('haiku', 'Haiku', config.haikuModel || 'haiku', 'ANTHROPIC_DEFAULT_HAIKU_MODEL')
65
+ );
66
+
67
+ return {
68
+ defaultModel: config.anthropicModel ? 'env' : 'sonnet',
69
+ defaultEffort: config.effort,
70
+ models: options,
71
+ env: config
72
+ };
73
+ }
74
+
75
+ module.exports = {
76
+ EFFORT_LEVELS,
77
+ MODEL_ALIASES,
78
+ getClaudeRuntimeConfig,
79
+ normalizeEffort,
80
+ normalizeModel,
81
+ resolveClaudeModel
82
+ };
@@ -0,0 +1,516 @@
1
+ const { EventEmitter } = require('events');
2
+ const crypto = require('crypto');
3
+ const { normalizeEffort, normalizeModel, resolveClaudeModel } = require('./config');
4
+
5
+ const PERMISSION_MODES = new Set(['default', 'acceptEdits', 'bypassPermissions', 'plan']);
6
+
7
+ function normalizePermissionMode(value) {
8
+ const mode = String(value || 'default');
9
+ return PERMISSION_MODES.has(mode) ? mode : 'default';
10
+ }
11
+
12
+ function textFromContent(content) {
13
+ if (typeof content === 'string') return content;
14
+ if (!Array.isArray(content)) return '';
15
+ return content
16
+ .map(item => {
17
+ if (!item || typeof item !== 'object') return '';
18
+ if (item.type === 'text' && typeof item.text === 'string') return item.text;
19
+ if (item.type === 'tool_result') {
20
+ if (typeof item.content === 'string') return item.content;
21
+ if (Array.isArray(item.content)) return textFromContent(item.content);
22
+ }
23
+ return '';
24
+ })
25
+ .filter(Boolean)
26
+ .join('\n');
27
+ }
28
+
29
+ class AsyncMessageQueue {
30
+ constructor() {
31
+ this.items = [];
32
+ this.waiters = [];
33
+ this.closed = false;
34
+ }
35
+
36
+ push(item) {
37
+ if (this.closed) return false;
38
+ const waiter = this.waiters.shift();
39
+ if (waiter) waiter({ value: item, done: false });
40
+ else this.items.push(item);
41
+ return true;
42
+ }
43
+
44
+ close() {
45
+ this.closed = true;
46
+ while (this.waiters.length > 0) {
47
+ this.waiters.shift()({ value: undefined, done: true });
48
+ }
49
+ }
50
+
51
+ [Symbol.asyncIterator]() {
52
+ return {
53
+ next: () => {
54
+ if (this.items.length > 0) {
55
+ return Promise.resolve({ value: this.items.shift(), done: false });
56
+ }
57
+ if (this.closed) return Promise.resolve({ value: undefined, done: true });
58
+ return new Promise(resolve => this.waiters.push(resolve));
59
+ }
60
+ };
61
+ }
62
+ }
63
+
64
+ class ClaudeStructuredSession extends EventEmitter {
65
+ constructor({ id, tool, workingDir, name, logger, options = {} }) {
66
+ super();
67
+ this.id = id;
68
+ this.tool = tool;
69
+ this.name = name || tool.displayName;
70
+ this.workingDir = workingDir;
71
+ this.logger = logger || console;
72
+ this.kind = 'claude-structured';
73
+ this.startTime = Date.now();
74
+ this.running = true;
75
+ this.status = 'idle';
76
+ this.messages = [];
77
+ this.pendingPermissions = new Map();
78
+ this.pendingInput = '';
79
+ this.inputSeq = 0;
80
+ this.hasUnreadCompletion = false;
81
+ this.completionReadInputSeq = 0;
82
+ this.timedInputs = new Map();
83
+ this.abortController = null;
84
+ this.inputQueue = null;
85
+ this.query = null;
86
+ this.runnerStarted = false;
87
+ this.abortRequested = false;
88
+ this.permissionMode = normalizePermissionMode(options.permissionMode);
89
+ this.model = normalizeModel(options.model);
90
+ this.effort = normalizeEffort(options.effort);
91
+ this.resumeSessionId = options.resume || null;
92
+ this.claudeSessionId = options.resume || null;
93
+ this.activeOptionSignature = null;
94
+ this.contextRemaining = null;
95
+
96
+ // Compatibility with existing session-scoped Git/file APIs.
97
+ this.ptyManager = {
98
+ workingDir,
99
+ isRunning: () => this.isRunning(),
100
+ write: data => this.write(data),
101
+ kill: () => this.kill(),
102
+ resize: () => {},
103
+ redraw: () => false
104
+ };
105
+ }
106
+
107
+ toListItem() {
108
+ return {
109
+ id: this.id,
110
+ name: this.name,
111
+ tool: this.tool.displayName,
112
+ startTime: this.startTime,
113
+ toolKey: this.tool.key,
114
+ workingDirectory: this.workingDir,
115
+ hasUnreadCompletion: Boolean(this.hasUnreadCompletion),
116
+ timedInputCount: Array.from(this.timedInputs.values()).filter(item => item.sendAt > Date.now()).length,
117
+ mode: 'structured'
118
+ };
119
+ }
120
+
121
+ snapshot() {
122
+ return {
123
+ id: this.id,
124
+ name: this.name,
125
+ tool: this.tool.displayName,
126
+ toolKey: this.tool.key,
127
+ status: this.status,
128
+ state: this.getControlState(),
129
+ messages: this.messages,
130
+ pendingPermissions: Array.from(this.pendingPermissions.values()).map(item => item.public)
131
+ };
132
+ }
133
+
134
+ getControlState() {
135
+ return {
136
+ permissionMode: this.permissionMode,
137
+ model: this.model,
138
+ effort: this.effort,
139
+ status: this.status,
140
+ claudeSessionId: this.claudeSessionId || null,
141
+ resumeSessionId: this.resumeSessionId || null,
142
+ contextRemaining: this.contextRemaining,
143
+ canAbort: this.status === 'thinking',
144
+ pendingPermissionCount: this.pendingPermissions.size
145
+ };
146
+ }
147
+
148
+ getHistory() {
149
+ const lines = [];
150
+ for (const message of this.messages) {
151
+ if (message.kind === 'user') lines.push(`User: ${message.text}`);
152
+ if (message.kind === 'assistant') lines.push(`Claude: ${message.text}`);
153
+ if (message.kind === 'tool') lines.push(`Tool ${message.name}: ${message.summary}`);
154
+ if (message.kind === 'event') lines.push(message.text);
155
+ }
156
+ const text = lines.join('\n\n');
157
+ return {
158
+ success: true,
159
+ sessionId: this.id,
160
+ sessionName: this.name,
161
+ tool: this.tool.displayName,
162
+ historyMode: 'structured',
163
+ text,
164
+ updatedAt: Date.now(),
165
+ truncated: false,
166
+ bytes: Buffer.byteLength(text, 'utf8'),
167
+ lines: text ? text.split('\n').length : 0
168
+ };
169
+ }
170
+
171
+ getCatchupOutput() {
172
+ return {
173
+ source: 'claude-structured',
174
+ items: this.messages.length,
175
+ data: ''
176
+ };
177
+ }
178
+
179
+ write(data) {
180
+ if (!this.running) return false;
181
+ const text = String(data || '');
182
+ if (!text) return true;
183
+
184
+ this.pendingInput += text.replace(/\r/g, '\n');
185
+ if (!this.pendingInput.includes('\n')) return true;
186
+
187
+ const parts = this.pendingInput.split('\n');
188
+ this.pendingInput = parts.pop() || '';
189
+ const prompt = parts.join('\n').trim();
190
+ if (prompt) this.sendUserMessage(prompt);
191
+ return true;
192
+ }
193
+
194
+ sendUserMessage(text) {
195
+ if (!this.running) return false;
196
+ const prompt = String(text || '').trim();
197
+ if (!prompt) return false;
198
+ this.hasUnreadCompletion = false;
199
+ this.appendMessage({ kind: 'user', text: prompt });
200
+
201
+ const sdkMessage = {
202
+ type: 'user',
203
+ parent_tool_use_id: null,
204
+ message: {
205
+ role: 'user',
206
+ content: prompt
207
+ }
208
+ };
209
+
210
+ if (!this.runnerStarted) {
211
+ this.startRunner(sdkMessage);
212
+ } else if (this.inputQueue) {
213
+ this.inputQueue.push(sdkMessage);
214
+ }
215
+ return true;
216
+ }
217
+
218
+ createMessageItem(message) {
219
+ return {
220
+ id: crypto.randomUUID(),
221
+ createdAt: Date.now(),
222
+ ...message
223
+ };
224
+ }
225
+
226
+ updateSettings(settings = {}) {
227
+ const next = {
228
+ permissionMode: settings.permissionMode === undefined ? this.permissionMode : normalizePermissionMode(settings.permissionMode),
229
+ model: settings.model === undefined ? this.model : normalizeModel(settings.model),
230
+ effort: settings.effort === undefined ? this.effort : normalizeEffort(settings.effort)
231
+ };
232
+ const changed = next.permissionMode !== this.permissionMode
233
+ || next.model !== this.model
234
+ || next.effort !== this.effort;
235
+ this.permissionMode = next.permissionMode;
236
+ this.model = next.model;
237
+ this.effort = next.effort;
238
+
239
+ if (changed) {
240
+ if (this.query && typeof this.query.setPermissionMode === 'function') {
241
+ Promise.resolve(this.query.setPermissionMode(this.permissionMode)).catch(error => {
242
+ this.logger.debugInfo?.(`[claude-structured] setPermissionMode failed: ${error.message}`);
243
+ });
244
+ }
245
+ if (this.runnerStarted && this.status === 'idle' && this.activeOptionSignature !== this.getOptionSignature()) {
246
+ this.resetRunnerForNextTurn();
247
+ }
248
+ this.emitEvent({ type: 'state', state: this.getControlState() });
249
+ }
250
+ return this.getControlState();
251
+ }
252
+
253
+ selectResumeSession(resumeSessionId, historyMessages = null) {
254
+ const id = String(resumeSessionId || '').trim();
255
+ if (!id) return false;
256
+ if (this.status === 'thinking') this.abort('Switching resume target');
257
+ this.resumeSessionId = id;
258
+ this.claudeSessionId = id;
259
+ this.resetRunnerForNextTurn();
260
+ const eventMessage = this.createMessageItem({
261
+ kind: 'event',
262
+ level: 'info',
263
+ text: `Resume target selected: ${id}`
264
+ });
265
+ if (Array.isArray(historyMessages)) {
266
+ this.messages = [...historyMessages, eventMessage];
267
+ this.emitEvent({ type: 'history-reset', messages: this.messages });
268
+ } else {
269
+ this.messages.push(eventMessage);
270
+ this.emitEvent({ type: 'message', message: eventMessage });
271
+ }
272
+ this.emitEvent({ type: 'state', state: this.getControlState() });
273
+ return true;
274
+ }
275
+
276
+ abort(reason = 'Aborted by user') {
277
+ if (!this.runnerStarted && this.status !== 'thinking') return false;
278
+ this.abortRequested = true;
279
+ this.inputQueue?.close();
280
+ this.query?.close?.();
281
+ this.abortController?.abort();
282
+ this.runnerStarted = false;
283
+ this.inputQueue = null;
284
+ this.query = null;
285
+ this.abortController = null;
286
+ this.setStatus('idle');
287
+ this.appendMessage({ kind: 'event', level: 'info', text: reason });
288
+ this.emitEvent({ type: 'state', state: this.getControlState() });
289
+ return true;
290
+ }
291
+
292
+ resetRunnerForNextTurn() {
293
+ this.inputQueue?.close();
294
+ this.query?.close?.();
295
+ this.runnerStarted = false;
296
+ this.inputQueue = null;
297
+ this.query = null;
298
+ this.abortController = null;
299
+ }
300
+
301
+ respondPermission(id, approved) {
302
+ const pending = this.pendingPermissions.get(id);
303
+ if (!pending) return false;
304
+ this.pendingPermissions.delete(id);
305
+ const publicRequest = { ...pending.public, status: approved ? 'approved' : 'denied' };
306
+ this.emitEvent({ type: 'permission-updated', request: publicRequest });
307
+ pending.resolve(approved
308
+ ? { behavior: 'allow', toolUseID: pending.toolUseID, decisionClassification: 'user_temporary' }
309
+ : { behavior: 'deny', message: 'Denied by user', interrupt: true, toolUseID: pending.toolUseID, decisionClassification: 'user_reject' });
310
+ return true;
311
+ }
312
+
313
+ markCompletionRead() {
314
+ this.hasUnreadCompletion = false;
315
+ this.completionReadInputSeq = this.inputSeq || 0;
316
+ }
317
+
318
+ async startRunner(initialMessage) {
319
+ this.runnerStarted = true;
320
+ this.inputQueue = new AsyncMessageQueue();
321
+ this.inputQueue.push(initialMessage);
322
+ this.abortController = new AbortController();
323
+ this.abortRequested = false;
324
+ this.activeOptionSignature = this.getOptionSignature();
325
+ this.setStatus('thinking');
326
+
327
+ try {
328
+ const sdk = await import('@anthropic-ai/claude-agent-sdk');
329
+ const resolvedModel = resolveClaudeModel(this.model, process.env);
330
+ const options = {
331
+ cwd: this.workingDir,
332
+ resume: this.resumeSessionId || undefined,
333
+ permissionMode: this.permissionMode,
334
+ effort: this.effort,
335
+ tools: { type: 'preset', preset: 'claude_code' },
336
+ env: {
337
+ ...process.env,
338
+ CLAUDE_AGENT_SDK_CLIENT_APP: 'glad-web'
339
+ },
340
+ abortController: this.abortController,
341
+ canUseTool: (toolName, input, options) => this.requestPermission(toolName, input, options)
342
+ };
343
+ if (resolvedModel) options.model = resolvedModel;
344
+ this.query = sdk.query({
345
+ prompt: this.inputQueue,
346
+ options
347
+ });
348
+
349
+ for await (const message of this.query) {
350
+ this.handleSdkMessage(message);
351
+ }
352
+ this.setStatus('idle');
353
+ } catch (error) {
354
+ if (!this.running) return;
355
+ if (this.abortRequested) {
356
+ this.setStatus('idle');
357
+ return;
358
+ }
359
+ this.appendMessage({
360
+ kind: 'event',
361
+ level: 'error',
362
+ text: `Claude session error: ${error && error.message ? error.message : String(error)}`
363
+ });
364
+ this.setStatus('error');
365
+ }
366
+ }
367
+
368
+ getOptionSignature() {
369
+ return JSON.stringify({
370
+ permissionMode: this.permissionMode,
371
+ model: this.model,
372
+ effort: this.effort,
373
+ resume: this.resumeSessionId || null
374
+ });
375
+ }
376
+
377
+ requestPermission(toolName, input, options = {}) {
378
+ const id = crypto.randomUUID();
379
+ const request = {
380
+ id,
381
+ toolName,
382
+ title: options.title || `${toolName} requires approval`,
383
+ reason: options.decisionReason || '',
384
+ input,
385
+ createdAt: Date.now(),
386
+ status: 'pending'
387
+ };
388
+ this.emitEvent({ type: 'permission-request', request });
389
+ return new Promise(resolve => {
390
+ this.pendingPermissions.set(id, {
391
+ public: request,
392
+ resolve,
393
+ toolUseID: options.toolUseID
394
+ });
395
+ });
396
+ }
397
+
398
+ handleSdkMessage(message) {
399
+ if (!message || typeof message !== 'object') return;
400
+
401
+ if (message.type === 'system' && message.subtype === 'init') {
402
+ this.claudeSessionId = message.session_id || this.claudeSessionId;
403
+ this.resumeSessionId = this.claudeSessionId || this.resumeSessionId;
404
+ if (message.model) this.model = String(message.model);
405
+ this.appendMessage({
406
+ kind: 'event',
407
+ level: 'info',
408
+ text: `Claude ready${message.model ? ` (${message.model})` : ''}`
409
+ });
410
+ this.emitEvent({ type: 'state', state: this.getControlState() });
411
+ return;
412
+ }
413
+
414
+ if (message.type === 'assistant') {
415
+ const content = message.message && message.message.content;
416
+ const text = textFromContent(content).trim();
417
+ const toolBlocks = Array.isArray(content)
418
+ ? content.filter(item => item && item.type === 'tool_use')
419
+ : [];
420
+ if (text) {
421
+ this.appendMessage({ kind: 'assistant', text, raw: message });
422
+ }
423
+ for (const block of toolBlocks) {
424
+ this.appendMessage({
425
+ kind: 'tool',
426
+ name: block.name || 'tool',
427
+ summary: this.summarizeToolInput(block.input),
428
+ input: block.input,
429
+ toolUseId: block.id
430
+ });
431
+ }
432
+ this.setStatus('thinking');
433
+ return;
434
+ }
435
+
436
+ if (message.type === 'user') {
437
+ const content = message.message && message.message.content;
438
+ if (Array.isArray(content)) {
439
+ for (const item of content) {
440
+ if (item && item.type === 'tool_result') {
441
+ this.appendMessage({
442
+ kind: 'tool-result',
443
+ toolUseId: item.tool_use_id,
444
+ text: textFromContent([item]).trim(),
445
+ isError: Boolean(item.is_error)
446
+ });
447
+ }
448
+ }
449
+ }
450
+ return;
451
+ }
452
+
453
+ if (message.type === 'result') {
454
+ this.updateUsageFromResult(message);
455
+ this.setStatus('idle');
456
+ if (this.inputSeq > this.completionReadInputSeq) {
457
+ this.hasUnreadCompletion = true;
458
+ }
459
+ this.emit('complete');
460
+ if (this.activeOptionSignature !== this.getOptionSignature()) {
461
+ this.resetRunnerForNextTurn();
462
+ }
463
+ }
464
+ }
465
+
466
+ updateUsageFromResult(message) {
467
+ const usage = message && (message.usage || message.total_usage || message.result && message.result.usage);
468
+ const context = usage && (usage.context_remaining || usage.contextRemaining || usage.remaining_context || usage.remainingContext);
469
+ this.contextRemaining = typeof context === 'number' ? context : null;
470
+ }
471
+
472
+ summarizeToolInput(input) {
473
+ if (!input || typeof input !== 'object') return '';
474
+ if (typeof input.command === 'string') return input.command;
475
+ if (typeof input.file_path === 'string') return input.file_path;
476
+ if (typeof input.path === 'string') return input.path;
477
+ const serialized = JSON.stringify(input);
478
+ return serialized.length > 240 ? serialized.slice(0, 240) + '...' : serialized;
479
+ }
480
+
481
+ appendMessage(message) {
482
+ const item = this.createMessageItem(message);
483
+ this.messages.push(item);
484
+ this.emitEvent({ type: 'message', message: item });
485
+ }
486
+
487
+ setStatus(status) {
488
+ if (this.status === status) return;
489
+ this.status = status;
490
+ this.emitEvent({ type: 'status', status });
491
+ this.emitEvent({ type: 'state', state: this.getControlState() });
492
+ }
493
+
494
+ emitEvent(event) {
495
+ this.emit('event', event);
496
+ }
497
+
498
+ isRunning() {
499
+ return this.running;
500
+ }
501
+
502
+ kill() {
503
+ this.running = false;
504
+ this.inputQueue?.close();
505
+ this.query?.close?.();
506
+ this.abortController?.abort();
507
+ for (const pending of this.pendingPermissions.values()) {
508
+ pending.resolve({ behavior: 'deny', message: 'Session ended', interrupt: true, toolUseID: pending.toolUseID });
509
+ }
510
+ this.pendingPermissions.clear();
511
+ this.setStatus('stopped');
512
+ this.emit('exit', { exitCode: 0 });
513
+ }
514
+ }
515
+
516
+ module.exports = ClaudeStructuredSession;