glad-web 1.0.44 → 1.0.46

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/web/claude.js CHANGED
@@ -7,29 +7,34 @@
7
7
  }
8
8
 
9
9
  function setClaudeModeEnabled(enabled) {
10
- const codexChat = isCodexSession() && codexState.presentation === 'structured';
10
+ const codexChat = isCodexSession();
11
11
  const structured = enabled || codexChat;
12
12
  const actionRail = enabled
13
13
  ? document.querySelector('.claude-control-rail')
14
14
  : codexChat
15
15
  ? document.getElementById('codex-control-rail')
16
16
  : document.getElementById('shortcut-rail');
17
- const attachButton = document.getElementById('attach-image-btn');
17
+ const attachmentButton = document.getElementById('attachment-btn');
18
18
  const scheduleButton = document.getElementById('schedule-send-btn');
19
- if (actionRail && attachButton && scheduleButton) {
19
+ if (actionRail && attachmentButton && scheduleButton) {
20
20
  actionRail.prepend(scheduleButton);
21
- actionRail.prepend(attachButton);
21
+ actionRail.prepend(attachmentButton);
22
22
  }
23
23
  document.getElementById('terminal-container').style.display = structured ? 'none' : '';
24
24
  document.getElementById('claude-chat-container').style.display = enabled ? 'block' : 'none';
25
25
  document.getElementById('codex-chat-container').style.display = codexChat ? 'block' : 'none';
26
26
  document.getElementById('claude-control-panel').style.display = enabled ? 'flex' : 'none';
27
27
  document.getElementById('codex-control-panel').style.display = codexChat ? 'flex' : 'none';
28
- document.getElementById('codex-terminal-switch').style.display = isCodexSession() ? '' : 'none';
29
- document.getElementById('attach-image-btn').style.display = structured ? '' : 'none';
28
+ document.getElementById('attachment-btn').style.display = '';
30
29
  document.getElementById('shortcut-rail').style.display = structured ? 'none' : '';
31
30
  document.getElementById('scroll-controls').style.display = structured ? 'none' : '';
32
31
  document.getElementById('cmd-input').placeholder = enabled ? 'Message Claude...' : codexChat ? 'Message Codex...' : 'Type a message...';
32
+ if (!codexChat) {
33
+ const skillPrefix = document.getElementById('composer-skill-prefix');
34
+ skillPrefix.classList.remove('active');
35
+ skillPrefix.innerHTML = '';
36
+ }
37
+ if (!structured) renderSessionAttention();
33
38
  updateTerminalControlsHeight();
34
39
  }
35
40
 
@@ -242,9 +247,12 @@
242
247
  }
243
248
  }
244
249
 
245
- function applyClaudeState(state = {}) {
250
+ function applyClaudeState(state = {}, options = {}) {
246
251
  claudeState = { ...claudeState, ...state };
247
252
  claudeStatus = claudeState.status || claudeStatus;
253
+ if (typeof syncComposerSendState === 'function') {
254
+ syncComposerSendState({ acknowledgeProviderState: Boolean(options.providerStateReceived) });
255
+ }
248
256
  const permissionEl = document.getElementById('claude-permission-select');
249
257
  const modelEl = document.getElementById('claude-model-select');
250
258
  const effortEl = document.getElementById('claude-effort-select');
@@ -314,8 +322,9 @@
314
322
  const pending = Number(claudeState.pendingPermissionCount || claudePendingPermissions.filter(item => item.status === 'pending').length) || 0;
315
323
  const mode = claudeState.permissionMode || 'default';
316
324
  const parts = [];
325
+ const attention = [];
317
326
  if (pending) {
318
- parts.push(`<button type="button" class="claude-state-pill warn claude-approval-jump" onclick="jumpToClaudeApproval()" title="Jump to pending approval" aria-label="Jump to pending Claude approval">${pending} approval${pending > 1 ? 's' : ''}<span aria-hidden="true">↓</span></button>`);
327
+ attention.push(`<button type="button" class="session-attention-pill approval claude-approval-jump" onclick="jumpToClaudeApproval()" title="Jump to pending approval" aria-label="Jump to pending Claude approval"><span aria-hidden="true">!</span>${pending} approval${pending > 1 ? 's' : ''}<span aria-hidden="true">↓</span></button>`);
319
328
  } else if (claudeStatus && !['idle', 'stopped', 'thinking'].includes(claudeStatus)) {
320
329
  parts.push(`<span class="claude-state-pill warn">${escapeHtml(shortValue(claudeStatus))}</span>`);
321
330
  }
@@ -324,6 +333,7 @@
324
333
  }
325
334
  el.innerHTML = parts.join('');
326
335
  el.style.display = parts.length ? 'flex' : 'none';
336
+ renderSessionAttention(attention);
327
337
  }
328
338
 
329
339
  function jumpToClaudeApproval() {
@@ -841,7 +851,7 @@
841
851
  const parsed = parseLocalCommandMessage(textFromClaudeMessage(message));
842
852
  if (parsed.kind === 'hidden') return '';
843
853
  const attachments = Array.isArray(message.attachments) && message.attachments.length
844
- ? `<div class="claude-message-attachments">${message.attachments.map(item => `<span class="claude-message-attachment" title="${escapeHtml(item.name || 'Image')}">▧ ${escapeHtml(item.name || 'Image')}</span>`).join('')}</div>` : '';
854
+ ? `<div class="claude-message-attachments">${message.attachments.map(item => `<span class="claude-message-attachment" title="${escapeHtml(item.name || 'Attachment')}"><svg class="message-attachment-icon action-icon" aria-hidden="true"><use href="#icon-${item.kind === 'file' ? 'file' : 'image'}"></use></svg>${escapeHtml(item.name || 'Attachment')}</span>`).join('')}</div>` : '';
845
855
  if (parsed.kind === 'command') {
846
856
  const args = parsed.args ? `<div class="claude-message user">${renderMarkdown(parsed.args)}</div>` : '';
847
857
  return `${args}<div class="claude-message user"><span class="claude-command-chip">/${escapeHtml(parsed.commandName)}</span>${attachments}</div>`;
@@ -1078,13 +1088,13 @@
1078
1088
  } else if (event.type === 'history-reset' && Array.isArray(event.messages)) {
1079
1089
  claudeMessages = event.messages;
1080
1090
  } else if (event.type === 'state' && event.state) {
1081
- applyClaudeState(event.state);
1091
+ applyClaudeState(event.state, { providerStateReceived: true });
1082
1092
  }
1083
1093
  if (event.type !== 'state') applyClaudeState({
1084
1094
  status: claudeStatus,
1085
1095
  pendingPermissionCount: claudePendingPermissions.filter(item => item.status === 'pending').length,
1086
1096
  canAbort: claudeStatus === 'thinking'
1087
- });
1097
+ }, { providerStateReceived: event.type === 'status' });
1088
1098
  renderClaudeChat();
1089
1099
  }
1090
1100
 
@@ -1102,6 +1112,7 @@
1102
1112
  }
1103
1113
 
1104
1114
  async function createSession(toolKey, sessionName) {
1115
+ const selectedSkill = window.pendingSkillHubSkill || null;
1105
1116
  try {
1106
1117
  const workingDirectory = document.getElementById('cwd-field').value;
1107
1118
  const runtimeConfig = toolKey === 'claude-code' ? await refreshClaudeRuntimeConfig() : null;
@@ -1109,14 +1120,31 @@
1109
1120
  model: runtimeConfig.defaultModel || 'default',
1110
1121
  effort: runtimeConfig.defaultEffort || 'medium'
1111
1122
  } : undefined;
1112
- const res = await fetchWithTimeout('/api/sessions', {
1123
+ const endpoint = selectedSkill ? '/api/skillhub/sessions' : '/api/sessions';
1124
+ const body = selectedSkill ? {
1125
+ toolKey,
1126
+ workingDirectory,
1127
+ skill: {
1128
+ id: selectedSkill.id,
1129
+ version: selectedSkill.version,
1130
+ digest: selectedSkill.digest
1131
+ }
1132
+ } : { toolKey, workingDirectory, claudeOptions };
1133
+ const list = document.getElementById('tools-list');
1134
+ if (selectedSkill) list.innerHTML = '<p class="skill-hall-status">Downloading and verifying Skill…</p>';
1135
+ const res = await fetchWithTimeout(endpoint, {
1113
1136
  method: 'POST',
1114
1137
  headers: { 'Content-Type': 'application/json' },
1115
- body: JSON.stringify({ toolKey, workingDirectory, claudeOptions })
1116
- });
1138
+ body: JSON.stringify(body)
1139
+ }, selectedSkill ? 70000 : 30000);
1117
1140
  const data = await res.json();
1118
1141
  if (!res.ok || !data.id) throw new Error(data.error || 'Failed to create session');
1119
1142
  document.getElementById('modal-overlay').style.display = 'none';
1120
- refreshSessionsNow();
1121
- } catch (e) { alert('Failed to create session: ' + e.message); }
1143
+ window.pendingSkillHubSkill = null;
1144
+ if (selectedSkill) joinSession(data.id, data.name || selectedSkill.name, 'codex');
1145
+ else refreshSessionsNow();
1146
+ } catch (e) {
1147
+ alert('Failed to create session: ' + e.message);
1148
+ if (selectedSkill) await showToolModal(selectedSkill);
1149
+ }
1122
1150
  }
package/lib/web/codex.js CHANGED
@@ -1,6 +1,6 @@
1
1
  function codexText(text) { return escapeHtml(text || '').replace(/\n/g, '<br>'); }
2
2
  function codexReadyForInput() {
3
- return codexState.presentation === 'structured' && codexState.status === 'idle'
3
+ return codexState.status === 'idle'
4
4
  && !codexState.aborting && !codexState.resuming && !codexResumeInFlight;
5
5
  }
6
6
  function codexJson(value) {
@@ -219,13 +219,30 @@
219
219
  }
220
220
  function renderCodexMessageSkills(item) {
221
221
  const skills = Array.isArray(item?.skills) ? item.skills : [];
222
- return skills.map(skill => `<span class="codex-message-skill" title="${escapeHtml(skill.path || skill.name || '')}">Skill · ${escapeHtml(skill.name || 'unknown')}</span>`).join('');
222
+ if (!skills.length) return '';
223
+ return `<div class="codex-message-skills">${skills.map(skill => `<span class="codex-message-skill" title="${escapeHtml(skill.path || skill.name || '')}">Skill · ${escapeHtml(skill.name || 'unknown')}</span>`).join('')}</div>`;
224
+ }
225
+
226
+ function renderComposerSkillPrefix() {
227
+ const container = document.getElementById('composer-skill-prefix');
228
+ if (!container) return;
229
+ const visible = selectedCodexSkill && codexReadyForInput();
230
+ const wasVisible = container.classList.contains('active');
231
+ container.classList.toggle('active', Boolean(visible));
232
+ container.innerHTML = visible
233
+ ? `<div class="codex-skill-bubble" role="status" aria-label="Selected skill: ${escapeHtml(selectedCodexSkill.name)}"><span class="codex-skill-bubble-name">Skill · ${escapeHtml(selectedCodexSkill.name)}</span><button type="button" class="codex-skill-bubble-close" onclick="clearCodexSkillSelection()" title="Remove selected skill" aria-label="Remove selected skill">×</button></div>`
234
+ : '';
235
+ if (wasVisible !== Boolean(visible)) updateTerminalControlsHeight();
236
+ }
237
+
238
+ function renderCodexMessageAttachments(item) {
239
+ if (!Array.isArray(item.attachments) || !item.attachments.length) return '';
240
+ return `<div class="claude-message-attachments">${item.attachments.map(attachment => `<span class="claude-message-attachment" title="${escapeHtml(attachment.name || 'Attachment')}"><svg class="message-attachment-icon action-icon" aria-hidden="true"><use href="#icon-${attachment.kind === 'file' ? 'file' : 'image'}"></use></svg>${escapeHtml(attachment.name || 'Attachment')}</span>`).join('')}</div>`;
223
241
  }
224
242
  function renderCodexMessageMeta(item, finalOnly = false, context = null) {
225
243
  const time = renderCodexMessageTime(item, finalOnly);
226
- const skills = renderCodexMessageSkills(item);
227
244
  const meter = renderCodexContextMeter(context);
228
- return time || skills || meter ? `<div class="codex-message-meta">${time}${skills}${meter}</div>` : '';
245
+ return time || meter ? `<div class="codex-message-meta">${time}${meter}</div>` : '';
229
246
  }
230
247
  function syncCodexDom(current, next) {
231
248
  if (!current || !next) return;
@@ -320,7 +337,7 @@
320
337
  const context = lastAssistantByTurn.get(turnId) === item.id ? turnEnd?.context : null;
321
338
  parts.push(`<div class="codex-message-block assistant" data-codex-key="message-${escapeHtml(item.id || '')}"><div class="codex-message assistant claude-md">${renderMarkdown(item.text || '')}</div>${renderCodexMessageMeta(item, true, context)}</div>`);
322
339
  }
323
- else if (item.kind === 'user') parts.push(`<div class="codex-message-block user" data-codex-key="message-${escapeHtml(item.id || '')}"><div class="codex-message user claude-md">${renderMarkdown(item.text || '')}</div>${renderCodexMessageMeta(item)}</div>`);
340
+ else if (item.kind === 'user') parts.push(`<div class="codex-message-block user" data-codex-key="message-${escapeHtml(item.id || '')}">${renderCodexMessageSkills(item)}<div class="codex-message user claude-md">${renderMarkdown(item.text || '')}${renderCodexMessageAttachments(item)}</div>${renderCodexMessageMeta(item)}</div>`);
324
341
  else if (item.kind === 'compaction') parts.push(renderCodexCompaction(item));
325
342
  else if (item.kind === 'status') parts.push(renderCodexStatus(item));
326
343
  else if (item.kind === 'event' && item.level === 'warning') parts.push(renderCodexWarning(item));
@@ -328,16 +345,14 @@
328
345
  i += 1;
329
346
  }
330
347
  for (const request of codexPendingPermissions) if (!usedPermissions.has(request.id)) parts.push(renderCodexPermission(request));
331
- const skillBubble = selectedCodexSkill && codexReadyForInput()
332
- ? `<div class="codex-skill-bubble" role="status" aria-label="Selected skill: ${escapeHtml(selectedCodexSkill.name)}"><span class="codex-skill-bubble-name">Skill · ${escapeHtml(selectedCodexSkill.name)}</span><button type="button" class="codex-skill-bubble-close" onclick="clearCodexSkillSelection()" title="Remove selected skill" aria-label="Remove selected skill">×</button></div>`
333
- : '';
334
348
  const working = `<div class="codex-working-indicator" role="status" aria-label="Codex is working" title="Codex is working"${codexState.status === 'running' ? '' : ' style="display:none"'}></div>`;
335
349
  const template = document.createElement('template');
336
- template.innerHTML = `<div class="codex-conversation">${skillBubble}${working}${parts.join('') || '<div class="codex-message event">Send a message to start Codex.</div>'}</div>`;
350
+ template.innerHTML = `<div class="codex-conversation">${working}${parts.join('') || '<div class="codex-message event">Send a message to start Codex.</div>'}</div>`;
337
351
  const next = template.content.firstElementChild;
338
352
  const current = container.firstElementChild;
339
353
  if (!current) container.appendChild(next);
340
354
  else syncCodexDom(current, next);
355
+ renderComposerSkillPrefix();
341
356
  container.scrollTop = shouldStickToBottom ? container.scrollHeight : previousScrollTop;
342
357
  }
343
358
 
@@ -423,9 +438,11 @@
423
438
  }, 300);
424
439
  }
425
440
 
426
- function applyCodexState(state = {}) {
427
- const presentationChanged = state.presentation !== undefined && state.presentation !== codexState.presentation;
441
+ function applyCodexState(state = {}, options = {}) {
428
442
  codexState = { ...codexState, ...state };
443
+ if (typeof syncComposerSendState === 'function') {
444
+ syncComposerSendState({ acknowledgeProviderState: Boolean(options.providerStateReceived) });
445
+ }
429
446
  const permission = document.getElementById('codex-permission-select');
430
447
  if (permission) {
431
448
  const defaultOption = permission.querySelector('option[value="default"]');
@@ -459,11 +476,8 @@
459
476
  if (resume) resume.disabled = !codexReadyForInput();
460
477
  const fork = document.getElementById('codex-fork-btn');
461
478
  if (fork) fork.disabled = !codexReadyForInput();
462
- const terminal = document.getElementById('codex-terminal-switch');
463
- if (terminal) { terminal.textContent = codexState.presentation === 'terminal' ? 'CHAT' : 'TERM'; terminal.disabled = codexState.presentation === 'structured' && !codexState.canSwitchToTerminal; terminal.title = codexState.presentation === 'terminal' ? 'Return to Codex chat' : 'Switch to Codex terminal'; }
464
479
  renderCodexStateBar();
465
480
  renderCodexModelPanel();
466
- if (presentationChanged) setClaudeModeEnabled(isClaudeSession());
467
481
  renderCodexChat();
468
482
  }
469
483
 
@@ -473,12 +487,32 @@
473
487
  const pending = Number(codexState.pendingPermissionCount || codexPendingPermissions.filter(item => item.status === 'pending').length) || 0;
474
488
  const subagents = Number(codexState.activeSubagentCount || 0) || 0;
475
489
  const parts = [];
490
+ const attention = [];
476
491
  if (codexState.aborting) parts.push('<span class="claude-state-pill warn">Stopping Codex…</span>');
477
492
  else if (codexState.resuming || codexResumeInFlight) parts.push('<span class="claude-state-pill">Resuming conversation…</span>');
478
- if (pending || codexState.status === 'waiting_approval') parts.push(`<button type="button" class="claude-state-pill warn codex-approval-jump" onclick="jumpToCodexApproval()" title="Jump to pending approval" aria-label="Jump to pending approval">${pending || 1} approval${pending === 1 ? '' : 's'}<span aria-hidden="true">↓</span></button>`);
479
- if (subagents) parts.push(`<span class="claude-state-pill">${subagents} subagent${subagents === 1 ? '' : 's'} running</span>`);
493
+ if (pending || codexState.status === 'waiting_approval') attention.push(`<button type="button" class="session-attention-pill approval codex-approval-jump" onclick="jumpToCodexApproval()" title="Jump to pending approval" aria-label="Jump to pending approval"><span aria-hidden="true">!</span>${pending || 1} approval${pending === 1 ? '' : 's'}<span aria-hidden="true">↓</span></button>`);
494
+ if (subagents) attention.push(`<button type="button" class="session-attention-pill subagent codex-subagent-jump" onclick="jumpToActiveCodexSubagent()" title="Jump to active subagent" aria-label="Jump to active subagent"><span aria-hidden="true">↳</span>${subagents} subagent${subagents === 1 ? '' : 's'} running</button>`);
480
495
  el.innerHTML = parts.join('');
481
496
  el.style.display = parts.length ? 'flex' : 'none';
497
+ renderSessionAttention(attention);
498
+ }
499
+
500
+ function jumpToActiveCodexSubagent(retry = true) {
501
+ const groups = Array.from(document.querySelectorAll('.codex-subagent-group'));
502
+ const target = groups[groups.length - 1];
503
+ if (!target) {
504
+ if (!retry) return false;
505
+ commitCodexChatRender();
506
+ requestAnimationFrame(() => jumpToActiveCodexSubagent(false));
507
+ return false;
508
+ }
509
+ target.open = true;
510
+ target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
511
+ target.classList.remove('codex-approval-focus');
512
+ void target.offsetWidth;
513
+ target.classList.add('codex-approval-focus');
514
+ setTimeout(() => target.classList.remove('codex-approval-focus'), 1800);
515
+ return true;
482
516
  }
483
517
 
484
518
  function jumpToCodexApproval() {
@@ -590,7 +624,7 @@
590
624
  }
591
625
  else if (event.type === 'permission-request' && event.request) { codexPendingPermissions = [...codexPendingPermissions.filter(item => item.id !== event.request.id), event.request]; }
592
626
  else if (event.type === 'permission-updated' && event.request) codexPendingPermissions = codexPendingPermissions.map(item => item.id === event.request.id ? event.request : item);
593
- if (event.state) applyCodexState(event.state); else renderCodexChat();
627
+ if (event.state) applyCodexState(event.state, { providerStateReceived: true }); else renderCodexChat();
594
628
  }
595
629
 
596
630
  function sendCodexSettings(settings) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-settings', settings })); }
@@ -1009,8 +1043,3 @@
1009
1043
  panel.innerHTML = '';
1010
1044
  updateTerminalControlsHeight();
1011
1045
  }
1012
- async function toggleCodexPresentation() {
1013
- const presentation = codexState.presentation === 'terminal' ? 'structured' : 'terminal';
1014
- const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-presentation`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ presentation }) }, 30000);
1015
- if (!res.ok) alert((await res.json()).error || 'Unable to switch Codex interface');
1016
- }