glad-web 1.0.20 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/claude/structured-session.js +198 -7
- package/lib/commands/web.js +1 -1
- package/lib/session/session-manager.js +2 -2
- package/lib/web/index.html +215 -41
- package/package.json +1 -1
|
@@ -3,6 +3,9 @@ const crypto = require('crypto');
|
|
|
3
3
|
const { normalizeEffort, normalizeModel, resolveClaudeModel } = require('./config');
|
|
4
4
|
|
|
5
5
|
const PERMISSION_MODES = new Set(['default', 'acceptEdits', 'bypassPermissions', 'plan']);
|
|
6
|
+
const EDIT_TOOLS = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit']);
|
|
7
|
+
const EXIT_PLAN_TOOLS = new Set(['exit_plan_mode', 'ExitPlanMode']);
|
|
8
|
+
const DENY_PERMISSION_MESSAGE = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
6
9
|
|
|
7
10
|
function normalizePermissionMode(value) {
|
|
8
11
|
const mode = String(value || 'default');
|
|
@@ -88,10 +91,14 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
88
91
|
this.permissionMode = normalizePermissionMode(options.permissionMode);
|
|
89
92
|
this.model = normalizeModel(options.model);
|
|
90
93
|
this.effort = normalizeEffort(options.effort);
|
|
94
|
+
this.allowedTools = new Set();
|
|
95
|
+
this.allowedBashLiterals = new Set();
|
|
96
|
+
this.allowedBashPrefixes = new Set();
|
|
91
97
|
this.resumeSessionId = options.resume || null;
|
|
92
98
|
this.claudeSessionId = options.resume || null;
|
|
93
99
|
this.activeOptionSignature = null;
|
|
94
100
|
this.contextRemaining = null;
|
|
101
|
+
this.latestUsage = null;
|
|
95
102
|
|
|
96
103
|
// Compatibility with existing session-scoped Git/file APIs.
|
|
97
104
|
this.ptyManager = {
|
|
@@ -140,6 +147,7 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
140
147
|
claudeSessionId: this.claudeSessionId || null,
|
|
141
148
|
resumeSessionId: this.resumeSessionId || null,
|
|
142
149
|
contextRemaining: this.contextRemaining,
|
|
150
|
+
latestUsage: this.latestUsage,
|
|
143
151
|
canAbort: this.status === 'thinking',
|
|
144
152
|
pendingPermissionCount: this.pendingPermissions.size
|
|
145
153
|
};
|
|
@@ -238,7 +246,7 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
238
246
|
|
|
239
247
|
if (changed) {
|
|
240
248
|
if (this.query && typeof this.query.setPermissionMode === 'function') {
|
|
241
|
-
Promise.resolve(this.query.setPermissionMode(this.
|
|
249
|
+
Promise.resolve(this.query.setPermissionMode(this.getSdkPermissionMode())).catch(error => {
|
|
242
250
|
this.logger.debugInfo?.(`[claude-structured] setPermissionMode failed: ${error.message}`);
|
|
243
251
|
});
|
|
244
252
|
}
|
|
@@ -298,18 +306,161 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
298
306
|
this.abortController = null;
|
|
299
307
|
}
|
|
300
308
|
|
|
301
|
-
|
|
309
|
+
finishRunner() {
|
|
310
|
+
this.runnerStarted = false;
|
|
311
|
+
this.inputQueue?.close();
|
|
312
|
+
this.inputQueue = null;
|
|
313
|
+
this.query = null;
|
|
314
|
+
this.abortController = null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
respondPermission(id, approved, action = null) {
|
|
302
318
|
const pending = this.pendingPermissions.get(id);
|
|
303
319
|
if (!pending) return false;
|
|
304
320
|
this.pendingPermissions.delete(id);
|
|
305
|
-
const
|
|
321
|
+
const normalizedAction = this.normalizePermissionAction(action, approved);
|
|
322
|
+
const allowedTools = this.getAllowedToolsForAction(normalizedAction, pending);
|
|
323
|
+
const nextMode = this.getPermissionModeForAction(normalizedAction);
|
|
324
|
+
if (allowedTools.length > 0) this.addAllowedTools(allowedTools);
|
|
325
|
+
if (nextMode) {
|
|
326
|
+
this.permissionMode = nextMode;
|
|
327
|
+
if (this.query && typeof this.query.setPermissionMode === 'function') {
|
|
328
|
+
Promise.resolve(this.query.setPermissionMode(this.getSdkPermissionMode())).catch(error => {
|
|
329
|
+
this.logger.debugInfo?.(`[claude-structured] setPermissionMode failed: ${error.message}`);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const publicRequest = {
|
|
334
|
+
...pending.public,
|
|
335
|
+
status: approved ? 'approved' : 'denied',
|
|
336
|
+
action: normalizedAction,
|
|
337
|
+
mode: nextMode || undefined,
|
|
338
|
+
allowedTools: allowedTools.length > 0 ? allowedTools : undefined
|
|
339
|
+
};
|
|
306
340
|
this.emitEvent({ type: 'permission-updated', request: publicRequest });
|
|
307
341
|
pending.resolve(approved
|
|
308
|
-
? {
|
|
309
|
-
|
|
342
|
+
? {
|
|
343
|
+
behavior: 'allow',
|
|
344
|
+
updatedInput: pending.input || {},
|
|
345
|
+
updatedPermissions: this.getPermissionUpdatesForAction(normalizedAction, pending),
|
|
346
|
+
toolUseID: pending.toolUseID,
|
|
347
|
+
decisionClassification: normalizedAction === 'allow-once' ? 'user_temporary' : 'user_permanent'
|
|
348
|
+
}
|
|
349
|
+
: { behavior: 'deny', message: DENY_PERMISSION_MESSAGE, interrupt: true, toolUseID: pending.toolUseID, decisionClassification: 'user_reject' });
|
|
350
|
+
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
310
351
|
return true;
|
|
311
352
|
}
|
|
312
353
|
|
|
354
|
+
normalizePermissionAction(action, approved) {
|
|
355
|
+
if (!approved) return 'deny';
|
|
356
|
+
const value = String(action || '').trim();
|
|
357
|
+
if (['allow-once', 'allow-tool', 'allow-edits', 'bypass'].includes(value)) return value;
|
|
358
|
+
return 'allow-once';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
getPermissionModeForAction(action) {
|
|
362
|
+
if (action === 'allow-edits') return 'acceptEdits';
|
|
363
|
+
if (action === 'bypass') return 'bypassPermissions';
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
getSdkPermissionMode() {
|
|
368
|
+
// Claude CLI refuses --dangerously-skip-permissions under root/sudo. Glad
|
|
369
|
+
// keeps bypass as local state and auto-allows through canUseTool instead.
|
|
370
|
+
if (this.permissionMode === 'bypassPermissions') return 'default';
|
|
371
|
+
return this.permissionMode;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
getAllowedToolsForAction(action, pending) {
|
|
375
|
+
if (action !== 'allow-tool') return [];
|
|
376
|
+
const toolName = pending.toolName || pending.public.toolName;
|
|
377
|
+
if (!toolName) return [];
|
|
378
|
+
if (toolName === 'Bash') {
|
|
379
|
+
const command = pending.input && typeof pending.input.command === 'string'
|
|
380
|
+
? pending.input.command
|
|
381
|
+
: '';
|
|
382
|
+
return command ? [`Bash(${command})`] : ['Bash'];
|
|
383
|
+
}
|
|
384
|
+
return [toolName];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
getPermissionUpdatesForAction(action, pending) {
|
|
388
|
+
const mode = this.getPermissionModeForAction(action);
|
|
389
|
+
const allowedTools = this.getAllowedToolsForAction(action, pending);
|
|
390
|
+
const updates = [];
|
|
391
|
+
if (allowedTools.length > 0) {
|
|
392
|
+
updates.push({
|
|
393
|
+
type: 'addRules',
|
|
394
|
+
rules: allowedTools.map(tool => this.permissionRuleFromTool(tool)),
|
|
395
|
+
behavior: 'allow',
|
|
396
|
+
destination: 'session'
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if (mode === 'acceptEdits') {
|
|
400
|
+
updates.push({
|
|
401
|
+
type: 'addRules',
|
|
402
|
+
rules: Array.from(EDIT_TOOLS).map(toolName => ({ toolName })),
|
|
403
|
+
behavior: 'allow',
|
|
404
|
+
destination: 'session'
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return updates.length > 0 ? updates : undefined;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
permissionRuleFromTool(tool) {
|
|
411
|
+
const match = String(tool || '').match(/^Bash\(([\s\S]*)\)$/);
|
|
412
|
+
if (match) return { toolName: 'Bash', ruleContent: match[1] };
|
|
413
|
+
return { toolName: String(tool || '') };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
addAllowedTools(tools) {
|
|
417
|
+
for (const tool of tools) {
|
|
418
|
+
if (tool === 'Bash') {
|
|
419
|
+
this.allowedTools.add(tool);
|
|
420
|
+
} else if (tool.startsWith('Bash(')) {
|
|
421
|
+
this.parseBashPermission(tool);
|
|
422
|
+
} else {
|
|
423
|
+
this.allowedTools.add(tool);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
parseBashPermission(permission) {
|
|
429
|
+
const match = String(permission || '').match(/^Bash\(([\s\S]*)\)$/);
|
|
430
|
+
if (!match) return;
|
|
431
|
+
const command = match[1];
|
|
432
|
+
if (command.endsWith(':*')) {
|
|
433
|
+
this.allowedBashPrefixes.add(command.slice(0, -2));
|
|
434
|
+
} else {
|
|
435
|
+
this.allowedBashLiterals.add(command);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
isToolAllowed(toolName, input) {
|
|
440
|
+
if (toolName === 'Bash') {
|
|
441
|
+
if (this.allowedTools.has('Bash')) return true;
|
|
442
|
+
const command = input && typeof input.command === 'string' ? input.command : '';
|
|
443
|
+
if (command && this.allowedBashLiterals.has(command)) return true;
|
|
444
|
+
for (const prefix of this.allowedBashPrefixes) {
|
|
445
|
+
if (command.startsWith(prefix)) return true;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
return this.allowedTools.has(toolName);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
shouldAutoAllowTool(toolName, input) {
|
|
453
|
+
if (this.isToolAllowed(toolName, input)) return true;
|
|
454
|
+
if (this.permissionMode === 'bypassPermissions' && !EXIT_PLAN_TOOLS.has(toolName)) return true;
|
|
455
|
+
if (this.permissionMode === 'acceptEdits' && EDIT_TOOLS.has(toolName)) return true;
|
|
456
|
+
if (this.permissionMode === 'plan' && !this.isDangerousTool(toolName)) return true;
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
isDangerousTool(toolName) {
|
|
461
|
+
return toolName === 'Bash' || EDIT_TOOLS.has(toolName) || EXIT_PLAN_TOOLS.has(toolName);
|
|
462
|
+
}
|
|
463
|
+
|
|
313
464
|
markCompletionRead() {
|
|
314
465
|
this.hasUnreadCompletion = false;
|
|
315
466
|
this.completionReadInputSeq = this.inputSeq || 0;
|
|
@@ -330,7 +481,8 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
330
481
|
const options = {
|
|
331
482
|
cwd: this.workingDir,
|
|
332
483
|
resume: this.resumeSessionId || undefined,
|
|
333
|
-
permissionMode: this.
|
|
484
|
+
permissionMode: this.getSdkPermissionMode(),
|
|
485
|
+
allowDangerouslySkipPermissions: false,
|
|
334
486
|
effort: this.effort,
|
|
335
487
|
tools: { type: 'preset', preset: 'claude_code' },
|
|
336
488
|
env: {
|
|
@@ -349,10 +501,12 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
349
501
|
for await (const message of this.query) {
|
|
350
502
|
this.handleSdkMessage(message);
|
|
351
503
|
}
|
|
504
|
+
this.finishRunner();
|
|
352
505
|
this.setStatus('idle');
|
|
353
506
|
} catch (error) {
|
|
354
507
|
if (!this.running) return;
|
|
355
508
|
if (this.abortRequested) {
|
|
509
|
+
this.finishRunner();
|
|
356
510
|
this.setStatus('idle');
|
|
357
511
|
return;
|
|
358
512
|
}
|
|
@@ -375,12 +529,26 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
375
529
|
}
|
|
376
530
|
|
|
377
531
|
requestPermission(toolName, input, options = {}) {
|
|
532
|
+
if (toolName !== 'AskUserQuestion' && this.shouldAutoAllowTool(toolName, input)) {
|
|
533
|
+
return Promise.resolve({
|
|
534
|
+
behavior: 'allow',
|
|
535
|
+
updatedInput: input || {},
|
|
536
|
+
toolUseID: options.toolUseID,
|
|
537
|
+
decisionClassification: 'user_permanent'
|
|
538
|
+
});
|
|
539
|
+
}
|
|
378
540
|
const id = crypto.randomUUID();
|
|
379
541
|
const request = {
|
|
380
542
|
id,
|
|
381
543
|
toolName,
|
|
382
544
|
title: options.title || `${toolName} requires approval`,
|
|
383
|
-
|
|
545
|
+
displayName: options.displayName || '',
|
|
546
|
+
description: options.description || '',
|
|
547
|
+
reason: options.decisionReason || options.description || '',
|
|
548
|
+
blockedPath: options.blockedPath || null,
|
|
549
|
+
canAllowTool: Boolean(toolName && !EDIT_TOOLS.has(toolName) && !EXIT_PLAN_TOOLS.has(toolName)),
|
|
550
|
+
canAllowEdits: EDIT_TOOLS.has(toolName) || EXIT_PLAN_TOOLS.has(toolName),
|
|
551
|
+
canBypass: EXIT_PLAN_TOOLS.has(toolName),
|
|
384
552
|
input,
|
|
385
553
|
createdAt: Date.now(),
|
|
386
554
|
status: 'pending'
|
|
@@ -390,6 +558,8 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
390
558
|
this.pendingPermissions.set(id, {
|
|
391
559
|
public: request,
|
|
392
560
|
resolve,
|
|
561
|
+
input,
|
|
562
|
+
toolName,
|
|
393
563
|
toolUseID: options.toolUseID
|
|
394
564
|
});
|
|
395
565
|
});
|
|
@@ -412,6 +582,8 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
412
582
|
}
|
|
413
583
|
|
|
414
584
|
if (message.type === 'assistant') {
|
|
585
|
+
const usage = message.message && message.message.usage;
|
|
586
|
+
if (usage) this.updateLatestUsageFromClaudeUsage(usage, message.message.model);
|
|
415
587
|
const content = message.message && message.message.content;
|
|
416
588
|
const text = textFromContent(content).trim();
|
|
417
589
|
const toolBlocks = Array.isArray(content)
|
|
@@ -467,6 +639,25 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
467
639
|
const usage = message && (message.usage || message.total_usage || message.result && message.result.usage);
|
|
468
640
|
const context = usage && (usage.context_remaining || usage.contextRemaining || usage.remaining_context || usage.remainingContext);
|
|
469
641
|
this.contextRemaining = typeof context === 'number' ? context : null;
|
|
642
|
+
if (usage) this.updateLatestUsageFromClaudeUsage(usage, message.model || message.result && message.result.model);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
updateLatestUsageFromClaudeUsage(usage, model = null) {
|
|
646
|
+
const inputTokens = Number(usage.input_tokens || usage.inputTokens || 0);
|
|
647
|
+
const outputTokens = Number(usage.output_tokens || usage.outputTokens || 0);
|
|
648
|
+
const cacheCreation = Number(usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0);
|
|
649
|
+
const cacheRead = Number(usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0);
|
|
650
|
+
const contextSize = inputTokens + cacheCreation + cacheRead;
|
|
651
|
+
this.latestUsage = {
|
|
652
|
+
inputTokens,
|
|
653
|
+
outputTokens,
|
|
654
|
+
cacheCreation,
|
|
655
|
+
cacheRead,
|
|
656
|
+
totalTokens: inputTokens + outputTokens + cacheCreation + cacheRead,
|
|
657
|
+
contextSize,
|
|
658
|
+
model: model || null,
|
|
659
|
+
updatedAt: Date.now()
|
|
660
|
+
};
|
|
470
661
|
}
|
|
471
662
|
|
|
472
663
|
summarizeToolInput(input) {
|
package/lib/commands/web.js
CHANGED
|
@@ -459,7 +459,7 @@ async function webCommand(options) {
|
|
|
459
459
|
sessionManager.sendClaudeInput(sessionId, payload.text || '');
|
|
460
460
|
}
|
|
461
461
|
if (payload.type === 'claude-permission') {
|
|
462
|
-
sessionManager.respondClaudePermission(sessionId, payload.id, Boolean(payload.approved));
|
|
462
|
+
sessionManager.respondClaudePermission(sessionId, payload.id, Boolean(payload.approved), payload.action || null);
|
|
463
463
|
}
|
|
464
464
|
if (payload.type === 'claude-settings') {
|
|
465
465
|
sessionManager.updateClaudeSettings(sessionId, payload.settings || {});
|
|
@@ -177,10 +177,10 @@ class SessionManager extends EventEmitter {
|
|
|
177
177
|
return session.sendUserMessage(text);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
respondClaudePermission(id, permissionId, approved) {
|
|
180
|
+
respondClaudePermission(id, permissionId, approved, action = null) {
|
|
181
181
|
const session = this.get(id);
|
|
182
182
|
if (!session || session.kind !== 'claude-structured') return false;
|
|
183
|
-
return session.respondPermission(permissionId, approved);
|
|
183
|
+
return session.respondPermission(permissionId, approved, action);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
updateClaudeSettings(id, settings) {
|
package/lib/web/index.html
CHANGED
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
#terminal-container { flex: 1; min-height: 0; width: 100%; background: #000; position: relative; overflow: hidden; overflow-anchor: none; }
|
|
79
79
|
#terminal { height: 100%; width: 100%; }
|
|
80
80
|
#claude-chat-container { display: none; flex: 1; min-height: 0; overflow-y: auto; background: #050505; padding: 12px 12px 24px 12px; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
|
|
81
|
+
.claude-context-size-badge { position: sticky; top: 0; z-index: 2; width: max-content; max-width: 100%; margin: 0 0 8px auto; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.94); border-radius: 999px; padding: 4px 9px; color: #d1d5db; font-size: 11px; font-weight: 800; box-shadow: 0 8px 18px rgba(0,0,0,0.24); }
|
|
81
82
|
.claude-message { max-width: 92%; margin: 0 0 10px 0; padding: 10px 12px; border-radius: 8px; overflow-wrap: anywhere; line-height: 1.45; font-size: 14px; }
|
|
82
83
|
.claude-message.user { margin-left: auto; background: rgba(0,122,255,0.24); border: 1px solid rgba(0,122,255,0.32); color: #fff; border-radius: 12px; padding-top: 7px; padding-bottom: 7px; }
|
|
83
84
|
.claude-message.assistant { max-width: 100%; background: transparent; border: 0; color: #f5f5f7; padding: 0 4px; margin: 0 0 12px 0; }
|
|
@@ -127,11 +128,12 @@
|
|
|
127
128
|
.claude-work-group-title { flex: 1; min-width: 0; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
128
129
|
.claude-work-group-body { padding: 2px 8px 8px 8px; }
|
|
129
130
|
.claude-permission { border-color: rgba(255,204,0,0.4); background: rgba(255,204,0,0.08); }
|
|
130
|
-
.claude-permission-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; }
|
|
131
|
+
.claude-permission-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; flex-wrap: wrap; }
|
|
132
|
+
.claude-permission-actions .small-btn { min-height: 30px; max-width: 100%; white-space: normal; line-height: 1.2; }
|
|
131
133
|
.claude-status { color: var(--text-dim); font-size: 12px; text-align: center; padding: 6px 0 0 0; }
|
|
132
134
|
#claude-control-panel { display: none; padding: 0 14px 10px 14px; box-sizing: border-box; }
|
|
133
|
-
.claude-control-row { display: grid; grid-template-columns: repeat(
|
|
134
|
-
.claude-select, .claude-ctrl-btn { min-width: 0; height: 32px; border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; background: rgba(255,255,255,0.08); color: #f5f5f7; font-size: 11px; font-weight: 800; letter-spacing: 0; padding: 0
|
|
135
|
+
.claude-control-row { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 7px; }
|
|
136
|
+
.claude-select, .claude-ctrl-btn { min-width: 0; height: 32px; border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; background: rgba(255,255,255,0.08); color: #f5f5f7; font-size: 11px; font-weight: 800; letter-spacing: 0; padding: 0 10px; outline: none; box-sizing: border-box; }
|
|
135
137
|
.claude-select { appearance: none; -webkit-appearance: none; text-align: center; text-align-last: center; }
|
|
136
138
|
.claude-picker-wrap { min-width: 0; position: relative; }
|
|
137
139
|
.claude-picker-wrap select { display: none; }
|
|
@@ -139,11 +141,19 @@
|
|
|
139
141
|
.claude-picker-btn.active { background: rgba(0,122,255,0.22); border-color: rgba(0,122,255,0.5); }
|
|
140
142
|
#claude-picker-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.99); border-radius: 12px; max-height: min(280px, 42dvh); overflow-y: auto; box-shadow: 0 16px 36px rgba(0,0,0,0.34); }
|
|
141
143
|
#claude-picker-panel.active { display: block; }
|
|
144
|
+
#claude-usage-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.99); border-radius: 12px; padding: 10px 11px; box-shadow: 0 16px 36px rgba(0,0,0,0.34); color: #f5f5f7; }
|
|
145
|
+
#claude-usage-panel.active { display: block; }
|
|
146
|
+
.claude-usage-title { color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 9px; }
|
|
147
|
+
.claude-usage-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
|
148
|
+
.claude-usage-item { border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.045); border-radius: 8px; padding: 8px; min-width: 0; }
|
|
149
|
+
.claude-usage-label { color: var(--text-dim); font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
150
|
+
.claude-usage-value { margin-top: 4px; color: #f5f5f7; font-size: 13px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
142
151
|
.claude-picker-title { padding: 9px 11px 7px 11px; color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid rgba(255,255,255,0.06); }
|
|
143
152
|
.claude-picker-option { width: 100%; border: 0; border-bottom: 1px solid rgba(255,255,255,0.06); background: transparent; color: #f5f5f7; padding: 10px 11px; text-align: left; cursor: pointer; box-sizing: border-box; }
|
|
144
153
|
.claude-picker-option:last-child { border-bottom: 0; }
|
|
145
154
|
.claude-picker-option:active, .claude-picker-option.selected { background: rgba(0,122,255,0.18); }
|
|
146
155
|
.claude-picker-option-main { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 13px; font-weight: 800; }
|
|
156
|
+
.claude-picker-option-main .selected-label { color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; flex: 0 0 auto; }
|
|
147
157
|
.claude-picker-option-value { min-width: 0; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: #d1d5db; font-size: 12px; margin-top: 3px; line-height: 1.35; }
|
|
148
158
|
.claude-ctrl-btn { cursor: pointer; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap; }
|
|
149
159
|
.claude-ctrl-btn:active, .claude-select:focus { background: rgba(255,255,255,0.14); border-color: rgba(0,122,255,0.45); }
|
|
@@ -154,6 +164,10 @@
|
|
|
154
164
|
#claude-state-bar::-webkit-scrollbar { display: none; }
|
|
155
165
|
.claude-state-pill { display: inline-flex; align-items: center; gap: 4px; border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.045); border-radius: 999px; padding: 3px 8px; flex: 0 0 auto; }
|
|
156
166
|
.claude-state-pill.warn { color: #ffcc00; border-color: rgba(255,204,0,0.24); background: rgba(255,204,0,0.08); }
|
|
167
|
+
.claude-state-pill.perm { margin-left: auto; }
|
|
168
|
+
.claude-state-pill.perm.acceptEdits { color: #34c759; border-color: rgba(52,199,89,0.26); background: rgba(52,199,89,0.08); }
|
|
169
|
+
.claude-state-pill.perm.bypassPermissions { color: #ff9f0a; border-color: rgba(255,159,10,0.26); background: rgba(255,159,10,0.08); }
|
|
170
|
+
.claude-state-pill.perm.plan { color: #64d2ff; border-color: rgba(100,210,255,0.26); background: rgba(100,210,255,0.08); }
|
|
157
171
|
#claude-resume-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.98); border-radius: 12px; max-height: 180px; overflow-y: auto; }
|
|
158
172
|
#claude-resume-panel.active { display: block; }
|
|
159
173
|
.claude-resume-item { width: 100%; text-align: left; border: 0; border-bottom: 1px solid rgba(255,255,255,0.06); background: transparent; color: #fff; padding: 10px 11px; cursor: pointer; box-sizing: border-box; }
|
|
@@ -275,10 +289,10 @@
|
|
|
275
289
|
<div class="claude-control-row">
|
|
276
290
|
<div class="claude-picker-wrap">
|
|
277
291
|
<select id="claude-permission-select" class="claude-select" title="Permission mode" onchange="updateClaudeSettingsFromControls()">
|
|
278
|
-
<option value="default">
|
|
279
|
-
<option value="acceptEdits">
|
|
280
|
-
<option value="plan">
|
|
281
|
-
<option value="bypassPermissions">
|
|
292
|
+
<option value="default">Default</option>
|
|
293
|
+
<option value="acceptEdits">Accept edits</option>
|
|
294
|
+
<option value="plan">Plan mode</option>
|
|
295
|
+
<option value="bypassPermissions">Bypass</option>
|
|
282
296
|
</select>
|
|
283
297
|
<button id="claude-permission-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('permission')" title="Permission mode">P</button>
|
|
284
298
|
</div>
|
|
@@ -298,11 +312,13 @@
|
|
|
298
312
|
</select>
|
|
299
313
|
<button id="claude-effort-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('effort')" title="Effort">E</button>
|
|
300
314
|
</div>
|
|
315
|
+
<button id="claude-usage-btn" class="claude-ctrl-btn" onclick="toggleClaudeUsagePanel()" title="Usage">Usage</button>
|
|
301
316
|
<button id="claude-abort-btn" class="claude-ctrl-btn danger" onclick="abortClaudeSession()" title="Abort current Claude turn">Abort</button>
|
|
302
317
|
<button id="claude-resume-btn" class="claude-ctrl-btn primary" onclick="toggleClaudeResumePanel()" title="Choose a Claude session to resume">Resume</button>
|
|
303
318
|
</div>
|
|
304
319
|
<div id="claude-state-bar"></div>
|
|
305
320
|
<div id="claude-picker-panel"></div>
|
|
321
|
+
<div id="claude-usage-panel"></div>
|
|
306
322
|
<div id="claude-resume-panel"></div>
|
|
307
323
|
</div>
|
|
308
324
|
<div id="timed-send-panel">
|
|
@@ -478,11 +494,14 @@
|
|
|
478
494
|
let claudeStatus = 'idle';
|
|
479
495
|
let claudeRuntimeConfig = null;
|
|
480
496
|
let claudePickerOpen = null;
|
|
497
|
+
let claudeUsagePanelOpen = false;
|
|
498
|
+
const CLAUDE_CONTEXT_SIZE = 190000;
|
|
481
499
|
let claudeState = {
|
|
482
500
|
permissionMode: 'default',
|
|
483
501
|
model: 'default',
|
|
484
502
|
effort: 'medium',
|
|
485
503
|
contextRemaining: null,
|
|
504
|
+
latestUsage: null,
|
|
486
505
|
claudeSessionId: null,
|
|
487
506
|
resumeSessionId: null,
|
|
488
507
|
canAbort: false,
|
|
@@ -785,8 +804,7 @@
|
|
|
785
804
|
document.getElementById('terminal-container').style.display = enabled ? 'none' : '';
|
|
786
805
|
document.getElementById('claude-chat-container').style.display = enabled ? 'block' : 'none';
|
|
787
806
|
document.getElementById('claude-control-panel').style.display = enabled ? 'block' : 'none';
|
|
788
|
-
document.getElementById('timer-btn').style.display =
|
|
789
|
-
if (enabled) closeTimedSendPanel();
|
|
807
|
+
document.getElementById('timer-btn').style.display = '';
|
|
790
808
|
document.getElementById('shortcut-rail').style.display = enabled ? 'none' : '';
|
|
791
809
|
document.getElementById('scroll-controls').style.display = enabled ? 'none' : '';
|
|
792
810
|
document.getElementById('cmd-input').placeholder = enabled ? 'Message Claude...' : 'Type a message...';
|
|
@@ -794,12 +812,13 @@
|
|
|
794
812
|
}
|
|
795
813
|
|
|
796
814
|
function updateTerminalControlsHeight() {
|
|
815
|
+
const timedOpen = document.getElementById('timed-send-panel')?.classList.contains('active');
|
|
797
816
|
if (!isClaudeSession()) {
|
|
798
|
-
document.documentElement.style.setProperty('--terminal-controls-rest-height', '108px');
|
|
817
|
+
document.documentElement.style.setProperty('--terminal-controls-rest-height', timedOpen ? '252px' : '108px');
|
|
799
818
|
return;
|
|
800
819
|
}
|
|
801
|
-
const
|
|
802
|
-
document.documentElement.style.setProperty('--terminal-controls-rest-height',
|
|
820
|
+
const baseHeight = claudeResumePanelOpen ? 308 : ((claudePickerOpen || claudeUsagePanelOpen) ? 322 : 154);
|
|
821
|
+
document.documentElement.style.setProperty('--terminal-controls-rest-height', `${baseHeight + (timedOpen ? 144 : 0)}px`);
|
|
803
822
|
}
|
|
804
823
|
|
|
805
824
|
function shortModelLabel(value, resolved) {
|
|
@@ -817,6 +836,35 @@
|
|
|
817
836
|
return `M:${compact || source.slice(0, 9)}`;
|
|
818
837
|
}
|
|
819
838
|
|
|
839
|
+
function fullModelLabel(value, resolved, label) {
|
|
840
|
+
const source = String(value || 'default');
|
|
841
|
+
if (source === 'default') return 'Default';
|
|
842
|
+
if (source === 'env') return resolved ? `Environment (${resolved})` : 'Environment';
|
|
843
|
+
if (label && label !== value) return label;
|
|
844
|
+
return String(resolved || value || 'Model');
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function permissionModeLabel(value) {
|
|
848
|
+
const labels = {
|
|
849
|
+
default: 'Default',
|
|
850
|
+
acceptEdits: 'Accept edits',
|
|
851
|
+
plan: 'Plan mode',
|
|
852
|
+
bypassPermissions: 'Bypass'
|
|
853
|
+
};
|
|
854
|
+
return labels[value] || String(value || 'Default');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function effortLabel(value) {
|
|
858
|
+
const labels = {
|
|
859
|
+
low: 'Low',
|
|
860
|
+
medium: 'Medium',
|
|
861
|
+
high: 'High',
|
|
862
|
+
xhigh: 'Extra high',
|
|
863
|
+
max: 'Max'
|
|
864
|
+
};
|
|
865
|
+
return labels[value] || String(value || 'Medium');
|
|
866
|
+
}
|
|
867
|
+
|
|
820
868
|
function pickerLabelFromOption(option) {
|
|
821
869
|
return option ? (option.textContent || option.value || '') : '';
|
|
822
870
|
}
|
|
@@ -828,19 +876,19 @@
|
|
|
828
876
|
|
|
829
877
|
function syncClaudePickerButtons() {
|
|
830
878
|
const mappings = [
|
|
831
|
-
['permission', 'claude-permission-select', 'claude-permission-picker-btn', '
|
|
832
|
-
['model', 'claude-model-select', 'claude-model-picker-btn', '
|
|
833
|
-
['effort', 'claude-effort-select', 'claude-effort-picker-btn', '
|
|
879
|
+
['permission', 'claude-permission-select', 'claude-permission-picker-btn', 'Perm'],
|
|
880
|
+
['model', 'claude-model-select', 'claude-model-picker-btn', 'Model'],
|
|
881
|
+
['effort', 'claude-effort-select', 'claude-effort-picker-btn', 'Effort']
|
|
834
882
|
];
|
|
835
|
-
mappings.forEach(([type, selectId, buttonId,
|
|
883
|
+
mappings.forEach(([type, selectId, buttonId, prefix]) => {
|
|
836
884
|
const select = document.getElementById(selectId);
|
|
837
885
|
const button = document.getElementById(buttonId);
|
|
838
886
|
if (!select || !button) return;
|
|
839
887
|
const option = select.selectedOptions && select.selectedOptions[0];
|
|
840
888
|
const label = pickerLabelFromOption(option);
|
|
841
889
|
const fullValue = pickerFullValue(option);
|
|
842
|
-
button.textContent =
|
|
843
|
-
button.title = fullValue || label ||
|
|
890
|
+
button.textContent = prefix;
|
|
891
|
+
button.title = fullValue || label || prefix;
|
|
844
892
|
button.classList.toggle('active', claudePickerOpen === type);
|
|
845
893
|
});
|
|
846
894
|
}
|
|
@@ -856,6 +904,13 @@
|
|
|
856
904
|
updateTerminalControlsHeight();
|
|
857
905
|
}
|
|
858
906
|
|
|
907
|
+
function closeClaudeUsagePanel() {
|
|
908
|
+
claudeUsagePanelOpen = false;
|
|
909
|
+
const panel = document.getElementById('claude-usage-panel');
|
|
910
|
+
if (panel) panel.classList.remove('active');
|
|
911
|
+
updateTerminalControlsHeight();
|
|
912
|
+
}
|
|
913
|
+
|
|
859
914
|
function pickerTitle(type) {
|
|
860
915
|
if (type === 'permission') return 'Permission mode';
|
|
861
916
|
if (type === 'model') return 'Model';
|
|
@@ -881,7 +936,7 @@
|
|
|
881
936
|
return `<button class="claude-picker-option${selected ? ' selected' : ''}" onclick="chooseClaudePickerOption('${type}', decodePathValue('${encodePathValue(option.value)}'))">
|
|
882
937
|
<div class="claude-picker-option-main">
|
|
883
938
|
<span>${escapeHtml(option.textContent || option.value)}</span>
|
|
884
|
-
${selected ? '<span>Selected</span>' : ''}
|
|
939
|
+
${selected ? '<span class="selected-label">Selected</span>' : ''}
|
|
885
940
|
</div>
|
|
886
941
|
${full ? `<div class="claude-picker-option-value">${escapeHtml(full)}</div>` : ''}
|
|
887
942
|
</button>`;
|
|
@@ -897,6 +952,7 @@
|
|
|
897
952
|
return;
|
|
898
953
|
}
|
|
899
954
|
if (type === 'model') await refreshClaudeRuntimeConfig();
|
|
955
|
+
closeClaudeUsagePanel();
|
|
900
956
|
claudeResumePanelOpen = false;
|
|
901
957
|
const resumePanel = document.getElementById('claude-resume-panel');
|
|
902
958
|
if (resumePanel) resumePanel.classList.remove('active');
|
|
@@ -920,9 +976,10 @@
|
|
|
920
976
|
const current = modelEl.value || claudeState.model || config.defaultModel || 'default';
|
|
921
977
|
modelEl.innerHTML = '';
|
|
922
978
|
config.models.forEach(item => {
|
|
923
|
-
const option = new Option(
|
|
979
|
+
const option = new Option(fullModelLabel(item.value, item.resolved, item.label), item.value);
|
|
924
980
|
option.title = item.resolved ? `${item.label}: ${item.resolved}` : item.label;
|
|
925
981
|
option.dataset.resolved = item.resolved || '';
|
|
982
|
+
option.dataset.shortLabel = shortModelLabel(item.value, item.resolved);
|
|
926
983
|
modelEl.add(option);
|
|
927
984
|
});
|
|
928
985
|
const next = Array.from(modelEl.options).some(option => option.value === current)
|
|
@@ -977,6 +1034,8 @@
|
|
|
977
1034
|
const abortBtn = document.getElementById('claude-abort-btn');
|
|
978
1035
|
if (abortBtn) abortBtn.disabled = !(claudeState.canAbort || claudeStatus === 'thinking');
|
|
979
1036
|
renderClaudeStateBar();
|
|
1037
|
+
renderClaudeUsagePanel();
|
|
1038
|
+
renderClaudeChat();
|
|
980
1039
|
}
|
|
981
1040
|
|
|
982
1041
|
function shortValue(value, fallback = 'N/A') {
|
|
@@ -984,26 +1043,90 @@
|
|
|
984
1043
|
return text.length > 16 ? text.slice(0, 13) + '...' : text;
|
|
985
1044
|
}
|
|
986
1045
|
|
|
1046
|
+
function formatTokenCount(value) {
|
|
1047
|
+
const number = Number(value || 0);
|
|
1048
|
+
if (number >= 1000000) return `${(number / 1000000).toFixed(2)}M`;
|
|
1049
|
+
if (number >= 1000) return `${(number / 1000).toFixed(1)}K`;
|
|
1050
|
+
return String(Math.round(number));
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function formatContextK(value) {
|
|
1054
|
+
return `${(Number(value || 0) / 1000).toFixed(1)}K`;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function contextRemainingPercent(usage) {
|
|
1058
|
+
if (!usage || typeof usage.contextSize !== 'number') return null;
|
|
1059
|
+
const used = Math.max(0, usage.contextSize);
|
|
1060
|
+
return Math.max(0, Math.min(100, 100 - (used / CLAUDE_CONTEXT_SIZE) * 100));
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function renderClaudeContextSizeBadge() {
|
|
1064
|
+
const usage = claudeState.latestUsage;
|
|
1065
|
+
if (!usage || typeof usage.contextSize !== 'number') return '';
|
|
1066
|
+
return `<div class="claude-context-size-badge">Context ${escapeHtml(formatContextK(usage.contextSize))}</div>`;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
987
1069
|
function renderClaudeStateBar() {
|
|
988
1070
|
const el = document.getElementById('claude-state-bar');
|
|
989
1071
|
if (!el) return;
|
|
990
|
-
const modelEl = document.getElementById('claude-model-select');
|
|
991
|
-
const modelOption = modelEl && modelEl.selectedOptions && modelEl.selectedOptions[0];
|
|
992
|
-
const modelLabel = modelOption && modelOption.dataset.resolved
|
|
993
|
-
? modelOption.dataset.resolved
|
|
994
|
-
: (claudeState.model || 'default');
|
|
995
|
-
const context = typeof claudeState.contextRemaining === 'number'
|
|
996
|
-
? `Ctx ${claudeState.contextRemaining}`
|
|
997
|
-
: 'Ctx N/A';
|
|
998
1072
|
const pending = Number(claudeState.pendingPermissionCount || claudePendingPermissions.filter(item => item.status === 'pending').length) || 0;
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
`<span class="claude-state-pill">${
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1073
|
+
const mode = claudeState.permissionMode || 'default';
|
|
1074
|
+
const parts = [];
|
|
1075
|
+
if (pending) {
|
|
1076
|
+
parts.push(`<span class="claude-state-pill warn">${pending} approval${pending > 1 ? 's' : ''}</span>`);
|
|
1077
|
+
} else if (claudeStatus === 'thinking') {
|
|
1078
|
+
parts.push('<span class="claude-state-pill">Working</span>');
|
|
1079
|
+
} else if (claudeStatus && !['idle', 'stopped'].includes(claudeStatus)) {
|
|
1080
|
+
parts.push(`<span class="claude-state-pill warn">${escapeHtml(shortValue(claudeStatus))}</span>`);
|
|
1081
|
+
}
|
|
1082
|
+
const remainingPercent = contextRemainingPercent(claudeState.latestUsage);
|
|
1083
|
+
if (remainingPercent !== null && remainingPercent <= 10) {
|
|
1084
|
+
parts.push(`<span class="claude-state-pill warn">${Math.round(remainingPercent)}% left</span>`);
|
|
1085
|
+
}
|
|
1086
|
+
if (mode !== 'default') {
|
|
1087
|
+
parts.push(`<span class="claude-state-pill perm ${escapeHtml(mode)}">${escapeHtml(permissionModeLabel(mode))}</span>`);
|
|
1088
|
+
}
|
|
1089
|
+
el.innerHTML = parts.join('');
|
|
1090
|
+
el.style.display = parts.length ? 'flex' : 'none';
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function usageItem(label, value) {
|
|
1094
|
+
return `<div class="claude-usage-item">
|
|
1095
|
+
<div class="claude-usage-label">${escapeHtml(label)}</div>
|
|
1096
|
+
<div class="claude-usage-value">${escapeHtml(value)}</div>
|
|
1097
|
+
</div>`;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function renderClaudeUsagePanel() {
|
|
1101
|
+
const panel = document.getElementById('claude-usage-panel');
|
|
1102
|
+
if (!panel) return;
|
|
1103
|
+
const usage = claudeState.latestUsage;
|
|
1104
|
+
if (!usage) {
|
|
1105
|
+
panel.innerHTML = '<div class="claude-usage-title">Usage</div><div class="claude-resume-meta">No usage yet.</div>';
|
|
1106
|
+
} else {
|
|
1107
|
+
panel.innerHTML = `<div class="claude-usage-title">Usage</div>
|
|
1108
|
+
<div class="claude-usage-grid">
|
|
1109
|
+
${usageItem('Total', formatTokenCount(usage.totalTokens))}
|
|
1110
|
+
${usageItem('Context', formatContextK(usage.contextSize))}
|
|
1111
|
+
${usageItem('Input', formatTokenCount(usage.inputTokens))}
|
|
1112
|
+
${usageItem('Output', formatTokenCount(usage.outputTokens))}
|
|
1113
|
+
${usageItem('Cache write', formatTokenCount(usage.cacheCreation))}
|
|
1114
|
+
${usageItem('Cache read', formatTokenCount(usage.cacheRead))}
|
|
1115
|
+
</div>`;
|
|
1116
|
+
}
|
|
1117
|
+
panel.classList.toggle('active', claudeUsagePanelOpen);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function toggleClaudeUsagePanel() {
|
|
1121
|
+
claudeUsagePanelOpen = !claudeUsagePanelOpen;
|
|
1122
|
+
if (claudeUsagePanelOpen) {
|
|
1123
|
+
closeClaudePicker();
|
|
1124
|
+
claudeResumePanelOpen = false;
|
|
1125
|
+
const resumePanel = document.getElementById('claude-resume-panel');
|
|
1126
|
+
if (resumePanel) resumePanel.classList.remove('active');
|
|
1127
|
+
}
|
|
1128
|
+
renderClaudeUsagePanel();
|
|
1129
|
+
updateTerminalControlsHeight();
|
|
1007
1130
|
}
|
|
1008
1131
|
|
|
1009
1132
|
async function updateClaudeSettingsFromControls() {
|
|
@@ -1027,6 +1150,7 @@
|
|
|
1027
1150
|
async function toggleClaudeResumePanel() {
|
|
1028
1151
|
claudeResumePanelOpen = !claudeResumePanelOpen;
|
|
1029
1152
|
if (claudeResumePanelOpen) closeClaudePicker();
|
|
1153
|
+
if (claudeResumePanelOpen) closeClaudeUsagePanel();
|
|
1030
1154
|
const panel = document.getElementById('claude-resume-panel');
|
|
1031
1155
|
panel.classList.toggle('active', claudeResumePanelOpen);
|
|
1032
1156
|
updateTerminalControlsHeight();
|
|
@@ -1360,9 +1484,41 @@
|
|
|
1360
1484
|
return '';
|
|
1361
1485
|
}
|
|
1362
1486
|
|
|
1487
|
+
function isClaudeEditTool(name) {
|
|
1488
|
+
return ['Edit', 'MultiEdit', 'Write', 'NotebookEdit'].includes(name || '');
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function isClaudeExitPlanTool(name) {
|
|
1492
|
+
return name === 'exit_plan_mode' || name === 'ExitPlanMode';
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function claudeAllowToolLabel(req) {
|
|
1496
|
+
if (req.toolName === 'Bash' && req.input && typeof req.input.command === 'string') return 'Allow command';
|
|
1497
|
+
return 'Allow tool';
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function renderClaudePermissionActions(req) {
|
|
1501
|
+
const id = escapeHtml(req.id);
|
|
1502
|
+
const toolName = req.toolName || '';
|
|
1503
|
+
const parts = [
|
|
1504
|
+
`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'allow-once')">Yes</button>`
|
|
1505
|
+
];
|
|
1506
|
+
if (isClaudeEditTool(toolName) || isClaudeExitPlanTool(toolName) || req.canAllowEdits) {
|
|
1507
|
+
parts.push(`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'allow-edits')">Allow edits</button>`);
|
|
1508
|
+
}
|
|
1509
|
+
if (isClaudeExitPlanTool(toolName) || req.canBypass) {
|
|
1510
|
+
parts.push(`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'bypass')">Allow all</button>`);
|
|
1511
|
+
}
|
|
1512
|
+
if (toolName && !isClaudeEditTool(toolName) && !isClaudeExitPlanTool(toolName) && req.canAllowTool !== false) {
|
|
1513
|
+
parts.push(`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'allow-tool')">${escapeHtml(claudeAllowToolLabel(req))}</button>`);
|
|
1514
|
+
}
|
|
1515
|
+
parts.push(`<button class="small-btn danger" onclick="respondClaudePermission('${id}', 'deny')">Deny</button>`);
|
|
1516
|
+
return parts.join('');
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1363
1519
|
function renderClaudeChat() {
|
|
1364
1520
|
const container = document.getElementById('claude-chat-container');
|
|
1365
|
-
const parts = buildClaudeDisplayItems(claudeMessages).map(renderDisplayItem);
|
|
1521
|
+
const parts = [renderClaudeContextSizeBadge(), ...buildClaudeDisplayItems(claudeMessages).map(renderDisplayItem)].filter(Boolean);
|
|
1366
1522
|
for (const req of claudePendingPermissions.filter(item => item.status === 'pending')) {
|
|
1367
1523
|
parts.push(`<div class="claude-tool claude-permission">
|
|
1368
1524
|
<div class="claude-tool-header">
|
|
@@ -1371,10 +1527,10 @@
|
|
|
1371
1527
|
</div>
|
|
1372
1528
|
<div class="claude-tool-body">
|
|
1373
1529
|
${req.reason ? `<div>${escapeHtml(req.reason)}</div>` : ''}
|
|
1530
|
+
${req.blockedPath ? `<div class="claude-resume-meta">${escapeHtml(req.blockedPath)}</div>` : ''}
|
|
1374
1531
|
${renderToolSection('Input', req.input || {})}
|
|
1375
1532
|
<div class="claude-permission-actions">
|
|
1376
|
-
|
|
1377
|
-
<button class="small-btn primary" onclick="respondClaudePermission('${req.id}', true)">Allow</button>
|
|
1533
|
+
${renderClaudePermissionActions(req)}
|
|
1378
1534
|
</div>
|
|
1379
1535
|
</div>
|
|
1380
1536
|
</div>`);
|
|
@@ -1411,9 +1567,17 @@
|
|
|
1411
1567
|
renderClaudeChat();
|
|
1412
1568
|
}
|
|
1413
1569
|
|
|
1414
|
-
function respondClaudePermission(id,
|
|
1570
|
+
function respondClaudePermission(id, actionOrApproved) {
|
|
1415
1571
|
if (!currentSocket || currentSocket.readyState !== 1) return;
|
|
1416
|
-
|
|
1572
|
+
const action = typeof actionOrApproved === 'string'
|
|
1573
|
+
? actionOrApproved
|
|
1574
|
+
: (actionOrApproved ? 'allow-once' : 'deny');
|
|
1575
|
+
currentSocket.send(JSON.stringify({
|
|
1576
|
+
type: 'claude-permission',
|
|
1577
|
+
id,
|
|
1578
|
+
action,
|
|
1579
|
+
approved: action !== 'deny'
|
|
1580
|
+
}));
|
|
1417
1581
|
}
|
|
1418
1582
|
|
|
1419
1583
|
async function createSession(toolKey, sessionName) {
|
|
@@ -1809,6 +1973,7 @@
|
|
|
1809
1973
|
model: (runtimeConfig && runtimeConfig.defaultModel) || 'default',
|
|
1810
1974
|
effort: (runtimeConfig && runtimeConfig.defaultEffort) || 'medium',
|
|
1811
1975
|
contextRemaining: null,
|
|
1976
|
+
latestUsage: null,
|
|
1812
1977
|
claudeSessionId: null,
|
|
1813
1978
|
resumeSessionId: null,
|
|
1814
1979
|
canAbort: false,
|
|
@@ -2142,6 +2307,7 @@
|
|
|
2142
2307
|
document.getElementById('timer-btn').classList.remove('active');
|
|
2143
2308
|
editingTimedInputId = null;
|
|
2144
2309
|
renderTimedTags();
|
|
2310
|
+
updateTerminalControlsHeight();
|
|
2145
2311
|
}
|
|
2146
2312
|
|
|
2147
2313
|
function stopTimedInputTimers() {
|
|
@@ -2202,6 +2368,7 @@
|
|
|
2202
2368
|
document.getElementById('timer-btn').classList.add('active');
|
|
2203
2369
|
updateTimedSendPreview();
|
|
2204
2370
|
renderTimedTags();
|
|
2371
|
+
updateTerminalControlsHeight();
|
|
2205
2372
|
}
|
|
2206
2373
|
|
|
2207
2374
|
async function saveTimedSend() {
|
|
@@ -2250,12 +2417,19 @@
|
|
|
2250
2417
|
const isOpen = panel.classList.toggle('active');
|
|
2251
2418
|
document.getElementById('timer-btn').classList.toggle('active', isOpen);
|
|
2252
2419
|
if (isOpen) {
|
|
2420
|
+
if (isClaudeSession()) {
|
|
2421
|
+
closeClaudePicker();
|
|
2422
|
+
closeClaudeUsagePanel();
|
|
2423
|
+
claudeResumePanelOpen = false;
|
|
2424
|
+
document.getElementById('claude-resume-panel').classList.remove('active');
|
|
2425
|
+
}
|
|
2253
2426
|
initTimedDelaySelectors();
|
|
2254
2427
|
resetTimedEditor({ keepInput: true });
|
|
2255
2428
|
updateTimedSendPreview();
|
|
2256
2429
|
loadTimedInputs();
|
|
2257
2430
|
}
|
|
2258
2431
|
else closeTimedSendPanel();
|
|
2432
|
+
updateTerminalControlsHeight();
|
|
2259
2433
|
});
|
|
2260
2434
|
inputEl.addEventListener('keydown', (e) => {
|
|
2261
2435
|
if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); performSend(); }
|