glad-web 1.0.32 → 1.0.34

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.
@@ -69,6 +69,21 @@ function textFromInputItems(content) {
69
69
  .join('\n');
70
70
  }
71
71
 
72
+ function skillsFromInputItems(content) {
73
+ const seen = new Set();
74
+ const skills = [];
75
+ for (const item of Array.isArray(content) ? content : []) {
76
+ if (!item || item.type !== 'skill') continue;
77
+ const name = String(item.name || '').trim();
78
+ const path = String(item.path || '').trim();
79
+ const key = `${name}\n${path}`;
80
+ if (!name || !path || seen.has(key)) continue;
81
+ seen.add(key);
82
+ skills.push({ name, path });
83
+ }
84
+ return skills;
85
+ }
86
+
72
87
  function recentUserQuestions(thread, limit = 2) {
73
88
  const questions = [];
74
89
  const turns = Array.isArray(thread?.turns) ? thread.turns : [];
@@ -597,7 +612,9 @@ class CodexStructuredSession extends EventEmitter {
597
612
  ...timing, toolStatus: inferredToolStatus || raw.status || 'running' }
598
613
  : kind === 'compaction' ? { providerId, threadId, turnId, ...timing,
599
614
  compactionStatus: inferredStatus || raw.status || 'running' }
600
- : { text, threadId, turnId, streaming: false, ...(completedAtMs ? { completedAtMs } : {}) };
615
+ : { text, threadId, turnId, streaming: false,
616
+ ...(kind === 'user' ? { skills: skillsFromInputItems(raw.content) } : {}),
617
+ ...(completedAtMs ? { completedAtMs } : {}) };
601
618
  if (existing) {
602
619
  this.patch(existing.id, patch);
603
620
  } else if (kind === 'user') {
@@ -630,6 +647,37 @@ class CodexStructuredSession extends EventEmitter {
630
647
  return models;
631
648
  }
632
649
 
650
+ async listSkills(forceReload = false) {
651
+ await this.ensureProcess();
652
+ const result = await this.request('skills/list', {
653
+ cwds: [this.workingDir],
654
+ forceReload: Boolean(forceReload)
655
+ });
656
+ const entries = Array.isArray(result?.data) ? result.data : [];
657
+ const entry = entries.find(item => item?.cwd === this.workingDir) || entries[0] || {};
658
+ return {
659
+ skills: (Array.isArray(entry.skills) ? entry.skills : []).filter(item => item?.enabled !== false),
660
+ errors: Array.isArray(entry.errors) ? entry.errors : []
661
+ };
662
+ }
663
+
664
+ async resolveSkillInputs(skills) {
665
+ const requested = (Array.isArray(skills) ? skills : []).slice(0, 8);
666
+ if (!requested.length) return [];
667
+ const available = await this.listSkills(false);
668
+ const allowed = new Map(available.skills.map(item => [`${item.name}\n${item.path}`, item]));
669
+ const seen = new Set();
670
+ const resolved = [];
671
+ for (const item of requested) {
672
+ const key = `${String(item?.name || '')}\n${String(item?.path || '')}`;
673
+ const skill = allowed.get(key);
674
+ if (!skill || seen.has(key)) continue;
675
+ seen.add(key);
676
+ resolved.push({ type: 'skill', name: skill.name, path: skill.path });
677
+ }
678
+ return resolved;
679
+ }
680
+
633
681
  async refreshConfigDefaults() {
634
682
  const result = await this.request('config/read', { cwd: this.workingDir, includeLayers: false });
635
683
  const config = result?.config || {};
@@ -838,7 +886,7 @@ class CodexStructuredSession extends EventEmitter {
838
886
  return this.getControlState();
839
887
  }
840
888
 
841
- async sendUserMessage(text, attachments = []) {
889
+ async sendUserMessage(text, attachments = [], skills = []) {
842
890
  const prompt = String(text || '').trim();
843
891
  const images = (Array.isArray(attachments) ? attachments : [])
844
892
  .filter(item => item && typeof item.path === 'string' && item.path);
@@ -848,7 +896,10 @@ class CodexStructuredSession extends EventEmitter {
848
896
  this.append({
849
897
  kind: 'user',
850
898
  text: prompt || '📷 Image attachment',
851
- attachments: images.map(item => ({ id: item.id, name: item.name || 'image' }))
899
+ attachments: images.map(item => ({ id: item.id, name: item.name || 'image' })),
900
+ skills: (Array.isArray(skills) ? skills : []).map(item => ({
901
+ name: String(item?.name || ''), path: String(item?.path || '')
902
+ })).filter(item => item.name && item.path)
852
903
  });
853
904
  try {
854
905
  await this.ensureProcess();
@@ -867,6 +918,7 @@ class CodexStructuredSession extends EventEmitter {
867
918
  }
868
919
  this.setStatus('running');
869
920
  const input = [];
921
+ input.push(...await this.resolveSkillInputs(skills));
870
922
  if (prompt) input.push({ type: 'text', text: prompt });
871
923
  for (const image of images) input.push({ type: 'localImage', path: image.path });
872
924
  const params = { threadId: this.threadId, input, cwd: this.workingDir, summary: 'auto' };
@@ -359,7 +359,7 @@ async function webCommand(options) {
359
359
  sessionManager.abortClaude(sessionId);
360
360
  }
361
361
  if (payload.type === 'codex-input') {
362
- sessionManager.sendCodexInput(sessionId, payload.text || '', payload.attachmentIds || [])
362
+ sessionManager.sendCodexInput(sessionId, payload.text || '', payload.attachmentIds || [], payload.skills || [])
363
363
  .catch(error => logger.error(`Codex input error: ${error.message}`));
364
364
  }
365
365
  if (payload.type === 'codex-permission') {
@@ -68,6 +68,16 @@ function registerProviderRoutes(app, { sessionManager }) {
68
68
  }
69
69
  });
70
70
 
71
+ app.get('/api/sessions/:id/codex-skills', async (req, res) => {
72
+ try {
73
+ const result = await sessionManager.listCodexSkills(req.params.id, req.query?.forceReload === 'true');
74
+ if (!result) return res.status(404).json({ error: 'Codex session not found' });
75
+ res.json({ success: true, ...result });
76
+ } catch (error) {
77
+ res.status(400).json({ error: error.message });
78
+ }
79
+ });
80
+
71
81
  app.post('/api/sessions/:id/codex-abort', (req, res) => {
72
82
  const success = sessionManager.abortCodex(req.params.id);
73
83
  if (!success) return res.status(409).json({ error: 'Codex session is idle or unavailable' });
@@ -294,14 +294,14 @@ class SessionManager extends EventEmitter {
294
294
  this.scheduleImageCleanup(id, attachmentIds);
295
295
  }
296
296
 
297
- async sendCodexInput(id, text, attachmentIds = []) {
297
+ async sendCodexInput(id, text, attachmentIds = [], skills = []) {
298
298
  const session = this.get(id);
299
299
  if (!session || session.kind !== 'codex-structured') return false;
300
300
  const attachments = this.getCodexImageAttachments(id, attachmentIds);
301
301
  const prompt = String(text || '');
302
302
  if (!prompt.trim() && attachments.length === 0) return false;
303
303
  this.markSessionInput(session, prompt || '[image attachment]');
304
- const sent = await session.sendUserMessage(prompt, attachments);
304
+ const sent = await session.sendUserMessage(prompt, attachments, skills);
305
305
  if (sent && attachments.length) this.scheduleCodexImageCleanup(id, attachments.map(item => item.id));
306
306
  return sent;
307
307
  }
@@ -437,6 +437,12 @@ class SessionManager extends EventEmitter {
437
437
  return session.listPromptHistory(options);
438
438
  }
439
439
 
440
+ listCodexSkills(id, forceReload = false) {
441
+ const session = this.get(id);
442
+ if (!session || session.kind !== 'codex-structured') return null;
443
+ return session.listSkills(forceReload);
444
+ }
445
+
440
446
  switchCodexPresentation(id, presentation) {
441
447
  const session = this.get(id);
442
448
  if (!session || session.kind !== 'codex-structured') return Promise.resolve(false);
package/lib/web/codex.js CHANGED
@@ -199,10 +199,15 @@
199
199
  const label = `${formatCodexTokens(remaining)} / ${formatCodexTokens(total)}(${Math.round(percent)}%)`;
200
200
  return `<span class="codex-context-meter${level}" style="--context-remaining:${percent}%" title="${escapeHtml(`Context remaining: ${label}`)}" aria-label="${escapeHtml(`Context remaining: ${label}`)}">${escapeHtml(label)}</span>`;
201
201
  }
202
+ function renderCodexMessageSkills(item) {
203
+ const skills = Array.isArray(item?.skills) ? item.skills : [];
204
+ return skills.map(skill => `<span class="codex-message-skill" title="${escapeHtml(skill.path || skill.name || '')}">Skill · ${escapeHtml(skill.name || 'unknown')}</span>`).join('');
205
+ }
202
206
  function renderCodexMessageMeta(item, finalOnly = false, context = null) {
203
207
  const time = renderCodexMessageTime(item, finalOnly);
208
+ const skills = renderCodexMessageSkills(item);
204
209
  const meter = renderCodexContextMeter(context);
205
- return time || meter ? `<div class="codex-message-meta">${time}${meter}</div>` : '';
210
+ return time || skills || meter ? `<div class="codex-message-meta">${time}${skills}${meter}</div>` : '';
206
211
  }
207
212
  function syncCodexDom(current, next) {
208
213
  if (!current || !next) return;
@@ -305,9 +310,12 @@
305
310
  i += 1;
306
311
  }
307
312
  for (const request of codexPendingPermissions) if (!usedPermissions.has(request.id)) parts.push(renderCodexPermission(request));
313
+ const skillBubble = selectedCodexSkill && codexState.status === 'idle'
314
+ ? `<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>`
315
+ : '';
308
316
  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>`;
309
317
  const template = document.createElement('template');
310
- template.innerHTML = `<div class="codex-conversation">${working}${parts.join('') || '<div class="codex-message event">Send a message to start Codex.</div>'}</div>`;
318
+ template.innerHTML = `<div class="codex-conversation">${skillBubble}${working}${parts.join('') || '<div class="codex-message event">Send a message to start Codex.</div>'}</div>`;
311
319
  const next = template.content.firstElementChild;
312
320
  const current = container.firstElementChild;
313
321
  if (!current) container.appendChild(next);
@@ -342,6 +350,8 @@
342
350
  if (abort) abort.disabled = !codexState.canAbort;
343
351
  const compact = document.getElementById('codex-compact-btn');
344
352
  if (compact) { compact.disabled = !codexState.canCompact; compact.textContent = codexState.compacting ? 'Compacting' : 'Compact'; }
353
+ const skills = document.getElementById('codex-skills-btn');
354
+ if (skills) { skills.disabled = !(codexState.presentation === 'structured' && codexState.status === 'idle'); skills.classList.toggle('primary', Boolean(selectedCodexSkill)); }
345
355
  const fork = document.getElementById('codex-fork-btn');
346
356
  if (fork) fork.disabled = !(codexState.presentation === 'structured' && codexState.status === 'idle');
347
357
  const terminal = document.getElementById('codex-terminal-switch');
@@ -400,10 +410,12 @@
400
410
  codexResumePanelOpen = false;
401
411
  codexForkPanelOpen = false;
402
412
  codexPromptPanelOpen = false;
413
+ codexSkillPanelOpen = false;
403
414
  codexModelCandidate = codexState.model || codexState.models?.[0]?.id || null;
404
415
  document.getElementById('codex-resume-panel').classList.remove('active');
405
416
  document.getElementById('codex-fork-panel').classList.remove('active');
406
417
  document.getElementById('codex-prompt-panel').classList.remove('active');
418
+ document.getElementById('codex-skill-panel').classList.remove('active');
407
419
  renderCodexModelPanel();
408
420
  updateTerminalControlsHeight();
409
421
  }
@@ -460,9 +472,11 @@
460
472
  codexModelPanelOpen = false;
461
473
  codexForkPanelOpen = false;
462
474
  codexPromptPanelOpen = false;
475
+ codexSkillPanelOpen = false;
463
476
  document.getElementById('codex-model-panel').classList.remove('active');
464
477
  document.getElementById('codex-fork-panel').classList.remove('active');
465
478
  document.getElementById('codex-prompt-panel').classList.remove('active');
479
+ document.getElementById('codex-skill-panel').classList.remove('active');
466
480
  const panel = document.getElementById('codex-resume-panel');
467
481
  panel.classList.toggle('active', codexResumePanelOpen);
468
482
  updateTerminalControlsHeight();
@@ -475,9 +489,11 @@
475
489
  codexModelPanelOpen = false;
476
490
  codexResumePanelOpen = false;
477
491
  codexPromptPanelOpen = false;
492
+ codexSkillPanelOpen = false;
478
493
  document.getElementById('codex-model-panel').classList.remove('active');
479
494
  document.getElementById('codex-resume-panel').classList.remove('active');
480
495
  document.getElementById('codex-prompt-panel').classList.remove('active');
496
+ document.getElementById('codex-skill-panel').classList.remove('active');
481
497
  const panel = document.getElementById('codex-fork-panel');
482
498
  panel.classList.toggle('active', codexForkPanelOpen);
483
499
  updateTerminalControlsHeight();
@@ -531,9 +547,11 @@
531
547
  codexModelPanelOpen = false;
532
548
  codexResumePanelOpen = false;
533
549
  codexForkPanelOpen = false;
550
+ codexSkillPanelOpen = false;
534
551
  document.getElementById('codex-model-panel').classList.remove('active');
535
552
  document.getElementById('codex-resume-panel').classList.remove('active');
536
553
  document.getElementById('codex-fork-panel').classList.remove('active');
554
+ document.getElementById('codex-skill-panel').classList.remove('active');
537
555
  if (!codexPromptPanelOpen) {
538
556
  document.getElementById('codex-prompt-panel').classList.remove('active');
539
557
  updateTerminalControlsHeight();
@@ -584,6 +602,213 @@
584
602
  alert('Copy failed.');
585
603
  }
586
604
  }
605
+
606
+ const CODEX_RECENT_SKILLS_KEY = 'glad.codex.recentSkills';
607
+
608
+ function codexSkillIdentity(skill) {
609
+ return `${String(skill?.name || '')}\n${String(skill?.path || '')}`;
610
+ }
611
+
612
+ function codexSkillDisplayName(skill) {
613
+ return String(skill?.interface?.displayName || skill?.name || 'Unknown skill');
614
+ }
615
+
616
+ function codexSkillDescription(skill) {
617
+ return String(skill?.interface?.shortDescription || skill?.shortDescription || skill?.description || '');
618
+ }
619
+
620
+ function codexSkillDirectory(skill) {
621
+ const path = String(skill?.path || '').replace(/\\/g, '/');
622
+ const separator = path.lastIndexOf('/');
623
+ const directory = separator > 0 ? path.slice(0, separator) : path;
624
+ const skillRoot = directory.toLowerCase().lastIndexOf('/skills/');
625
+ return skillRoot >= 0 ? directory.slice(skillRoot + '/skills/'.length) : directory.split('/').filter(Boolean).pop() || directory;
626
+ }
627
+
628
+ function codexSkillSource(skill) {
629
+ const scope = String(skill?.scope || '').toLowerCase();
630
+ const path = String(skill?.path || '').replace(/\\/g, '/').toLowerCase();
631
+ if (['repo', 'project', 'workspace'].includes(scope)) return { group: 'project', label: 'Project' };
632
+ if (['user', 'personal'].includes(scope)) return { group: 'personal', label: 'Personal' };
633
+ if (scope === 'plugin') return { group: 'other', label: 'Plugin' };
634
+ if (['system', 'admin', 'builtin', 'built-in'].includes(scope)) return { group: 'other', label: 'System' };
635
+ if (path.includes('/plugins/')) return { group: 'other', label: 'Plugin' };
636
+ if (path.includes('/.system/')) return { group: 'other', label: 'System' };
637
+ if (path.includes('/.codex/skills/')) return { group: 'personal', label: 'Personal' };
638
+ if (path.includes('/.agents/skills/')) return { group: 'project', label: 'Project' };
639
+ return { group: 'other', label: scope ? scope[0].toUpperCase() + scope.slice(1) : 'Other' };
640
+ }
641
+
642
+ function compareCodexSkills(left, right) {
643
+ return codexSkillDisplayName(left).localeCompare(codexSkillDisplayName(right), undefined, {
644
+ sensitivity: 'base',
645
+ numeric: true
646
+ }) || String(left.name || '').localeCompare(String(right.name || ''), undefined, { sensitivity: 'base', numeric: true })
647
+ || String(left.path || '').localeCompare(String(right.path || ''), undefined, { sensitivity: 'base', numeric: true });
648
+ }
649
+
650
+ function readRecentCodexSkills() {
651
+ try {
652
+ const value = JSON.parse(localStorage.getItem(CODEX_RECENT_SKILLS_KEY) || '[]');
653
+ return Array.isArray(value) ? value.filter(item => item?.name && item?.path).slice(0, 3) : [];
654
+ } catch (_) {
655
+ return [];
656
+ }
657
+ }
658
+
659
+ function rememberCodexSkill(skill) {
660
+ const key = codexSkillIdentity(skill);
661
+ const recent = readRecentCodexSkills().filter(item => codexSkillIdentity(item) !== key);
662
+ recent.unshift({ name: skill.name, path: skill.path });
663
+ try { localStorage.setItem(CODEX_RECENT_SKILLS_KEY, JSON.stringify(recent.slice(0, 3))); } catch (_) {}
664
+ }
665
+
666
+ function scoreCodexSkillSearch(skill, query) {
667
+ const tokens = String(query || '').toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
668
+ if (!tokens.length) return 0;
669
+ const source = codexSkillSource(skill);
670
+ const fields = [
671
+ String(skill.name || '').toLocaleLowerCase(),
672
+ codexSkillDisplayName(skill).toLocaleLowerCase(),
673
+ codexSkillDescription(skill).toLocaleLowerCase(),
674
+ source.label.toLocaleLowerCase(),
675
+ String(skill.scope || '').toLocaleLowerCase(),
676
+ String(skill.path || '').toLocaleLowerCase()
677
+ ];
678
+ let score = 0;
679
+ for (const token of tokens) {
680
+ let tokenScore = Number.POSITIVE_INFINITY;
681
+ fields.forEach((field, fieldIndex) => {
682
+ const position = field.indexOf(token);
683
+ if (position < 0) return;
684
+ const exactBonus = field === token ? -4 : position === 0 ? -2 : 0;
685
+ tokenScore = Math.min(tokenScore, fieldIndex * 10 + position + exactBonus);
686
+ });
687
+ if (!Number.isFinite(tokenScore)) return null;
688
+ score += tokenScore;
689
+ }
690
+ return score;
691
+ }
692
+
693
+ function codexSkillItemHtml(item, index) {
694
+ const selected = selectedCodexSkill?.name === item.name && selectedCodexSkill?.path === item.path;
695
+ const displayName = codexSkillDisplayName(item);
696
+ const description = codexSkillDescription(item);
697
+ const source = codexSkillSource(item);
698
+ const fullName = String(item.name || displayName);
699
+ const directory = codexSkillDirectory(item);
700
+ return `<button type="button" class="codex-skill-item${selected ? ' selected' : ''}" data-codex-skill-path="${escapeHtml(item.path || '')}" title="${escapeHtml(item.path || displayName)}" onclick="selectCodexSkill(${index})"><span class="codex-skill-item-name"><span>${escapeHtml(fullName)}</span><span class="codex-skill-source">${escapeHtml(source.label)}</span></span>${description ? `<span class="codex-skill-description">${escapeHtml(description)}</span>` : ''}<span class="codex-skill-meta"><span class="codex-skill-directory" title="${escapeHtml(directory)}">Dir · ${escapeHtml(directory)}</span></span></button>`;
701
+ }
702
+
703
+ function codexSkillSectionHtml(title, entries) {
704
+ if (!entries.length) return '';
705
+ return `<section class="codex-skill-section"><div class="codex-skill-section-title"><span>${escapeHtml(title)}</span><span>${entries.length}</span></div>${entries.map(entry => codexSkillItemHtml(entry.item, entry.index)).join('')}</section>`;
706
+ }
707
+
708
+ function codexSkillResultsHtml() {
709
+ const entries = codexSkillItems.map((item, index) => ({ item, index }));
710
+ const query = codexSkillQuery.trim();
711
+ let sections = '';
712
+ if (query) {
713
+ const matches = entries.map(entry => ({ ...entry, score: scoreCodexSkillSearch(entry.item, query) }))
714
+ .filter(entry => entry.score != null)
715
+ .sort((left, right) => left.score - right.score || compareCodexSkills(left.item, right.item));
716
+ sections = codexSkillSectionHtml('Search results', matches);
717
+ } else {
718
+ const byIdentity = new Map(entries.map(entry => [codexSkillIdentity(entry.item), entry]));
719
+ const recent = readRecentCodexSkills().map(item => byIdentity.get(codexSkillIdentity(item))).filter(Boolean);
720
+ const recentKeys = new Set(recent.map(entry => codexSkillIdentity(entry.item)));
721
+ const remaining = entries.filter(entry => !recentKeys.has(codexSkillIdentity(entry.item)));
722
+ const group = name => remaining.filter(entry => codexSkillSource(entry.item).group === name)
723
+ .sort((left, right) => compareCodexSkills(left.item, right.item));
724
+ sections = codexSkillSectionHtml('Recently used', recent)
725
+ + codexSkillSectionHtml('Current project', group('project'))
726
+ + codexSkillSectionHtml('Personal', group('personal'))
727
+ + codexSkillSectionHtml('Other', group('other'));
728
+ }
729
+ const emptyText = query ? 'No skills match your search.' : 'No enabled skills found for this folder.';
730
+ const empty = !sections && !codexSkillLoading && !codexSkillError
731
+ ? `<div class="claude-resume-meta codex-skill-empty">${emptyText}</div>` : '';
732
+ return `${codexSkillError ? `<div class="claude-resume-meta codex-skill-error">${escapeHtml(codexSkillError)}</div>` : ''}${sections}${empty}`;
733
+ }
734
+
735
+ function renderCodexSkillPanel(focusSearch = false) {
736
+ const panel = document.getElementById('codex-skill-panel');
737
+ if (!panel) return;
738
+ panel.classList.toggle('active', codexSkillPanelOpen);
739
+ if (!codexSkillPanelOpen) { panel.innerHTML = ''; return; }
740
+ panel.innerHTML = `<div class="codex-skill-header"><div class="codex-skill-header-row"><span>Available skills</span><span>${codexSkillLoading ? 'Loading…' : codexSkillItems.length}</span></div><input id="codex-skill-search" class="codex-skill-search" type="search" aria-label="Search skills" placeholder="Search skills…" value="${escapeHtml(codexSkillQuery)}" oninput="updateCodexSkillQuery(this.value)" autocomplete="off" spellcheck="false"></div><div id="codex-skill-results">${codexSkillResultsHtml()}</div>`;
741
+ if (focusSearch) {
742
+ requestAnimationFrame(() => {
743
+ const search = document.getElementById('codex-skill-search');
744
+ if (!search) return;
745
+ search.focus({ preventScroll: true });
746
+ search.setSelectionRange(search.value.length, search.value.length);
747
+ });
748
+ }
749
+ updateTerminalControlsHeight();
750
+ }
751
+
752
+ function updateCodexSkillQuery(value) {
753
+ codexSkillQuery = String(value || '');
754
+ const results = document.getElementById('codex-skill-results');
755
+ if (results) results.innerHTML = codexSkillResultsHtml();
756
+ else renderCodexSkillPanel(true);
757
+ updateTerminalControlsHeight();
758
+ }
759
+
760
+ async function toggleCodexSkillPanel() {
761
+ if (!(codexState.presentation === 'structured' && codexState.status === 'idle')) return;
762
+ codexSkillPanelOpen = !codexSkillPanelOpen;
763
+ codexModelPanelOpen = false;
764
+ codexResumePanelOpen = false;
765
+ codexForkPanelOpen = false;
766
+ codexPromptPanelOpen = false;
767
+ document.getElementById('codex-model-panel').classList.remove('active');
768
+ document.getElementById('codex-resume-panel').classList.remove('active');
769
+ document.getElementById('codex-fork-panel').classList.remove('active');
770
+ document.getElementById('codex-prompt-panel').classList.remove('active');
771
+ if (!codexSkillPanelOpen) {
772
+ renderCodexSkillPanel();
773
+ updateTerminalControlsHeight();
774
+ return;
775
+ }
776
+ codexSkillLoading = true;
777
+ codexSkillQuery = '';
778
+ codexSkillError = '';
779
+ codexSkillItems = [];
780
+ renderCodexSkillPanel(true);
781
+ try {
782
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-skills?forceReload=true`, {}, 30000);
783
+ const data = await res.json();
784
+ if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load skills');
785
+ codexSkillItems = data.skills || [];
786
+ codexSkillLoading = false;
787
+ codexSkillError = (data.errors || []).map(item => item.message || String(item)).filter(Boolean).join(' · ');
788
+ renderCodexSkillPanel(true);
789
+ } catch (error) {
790
+ codexSkillLoading = false;
791
+ codexSkillError = error.message;
792
+ renderCodexSkillPanel(true);
793
+ }
794
+ }
795
+
796
+ function selectCodexSkill(index) {
797
+ const skill = codexSkillItems[index];
798
+ if (!skill) return;
799
+ const alreadySelected = selectedCodexSkill?.name === skill.name && selectedCodexSkill?.path === skill.path;
800
+ selectedCodexSkill = alreadySelected ? null : { name: skill.name, path: skill.path };
801
+ if (selectedCodexSkill) rememberCodexSkill(skill);
802
+ codexSkillPanelOpen = false;
803
+ renderCodexSkillPanel();
804
+ applyCodexState({});
805
+ updateTerminalControlsHeight();
806
+ }
807
+
808
+ function clearCodexSkillSelection() {
809
+ selectedCodexSkill = null;
810
+ applyCodexState({});
811
+ }
587
812
  async function selectCodexResumeThread(threadId) {
588
813
  const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threadId }) }, 30000);
589
814
  if (!res.ok) { alert((await res.json()).error || 'Unable to resume Codex thread'); return; }
@@ -210,13 +210,16 @@
210
210
  currentSocket.send(JSON.stringify({
211
211
  type: 'codex-input',
212
212
  text: val,
213
- attachmentIds: readyImageAttachments.map(item => item.id)
213
+ attachmentIds: readyImageAttachments.map(item => item.id),
214
+ skills: selectedCodexSkill ? [{ name: selectedCodexSkill.name, path: selectedCodexSkill.path }] : []
214
215
  }));
215
216
  }
216
217
  inputEl.value = '';
217
218
  inputEl.style.height = '38px';
218
219
  selectedImageAttachments = [];
220
+ selectedCodexSkill = null;
219
221
  renderImageAttachments();
222
+ renderCodexChat();
220
223
  return;
221
224
  }
222
225
  if (val) {
package/lib/web/core.js CHANGED
@@ -44,6 +44,12 @@
44
44
  let codexPromptTotal = 0;
45
45
  let codexPromptLoading = false;
46
46
  let codexExpandedPrompts = new Set();
47
+ let codexSkillPanelOpen = false;
48
+ let codexSkillItems = [];
49
+ let codexSkillLoading = false;
50
+ let codexSkillQuery = '';
51
+ let codexSkillError = '';
52
+ let selectedCodexSkill = null;
47
53
  let codexRenderFrame = null;
48
54
  let codexApprovalJumpIndex = 0;
49
55
  const modifiers = { ctrl: false };
@@ -136,6 +136,7 @@
136
136
  <option value="default">Default</option><option value="untrusted">Untrusted</option><option value="on-request">On request</option><option value="never">Never ask</option>
137
137
  </select>
138
138
  </label>
139
+ <button id="codex-skills-btn" class="claude-ctrl-btn" onclick="toggleCodexSkillPanel()" title="Choose a skill for the next message">Skills</button>
139
140
  </div>
140
141
  </div>
141
142
  <div id="codex-state-bar"></div>
@@ -143,6 +144,7 @@
143
144
  <div id="codex-resume-panel"></div>
144
145
  <div id="codex-fork-panel"></div>
145
146
  <div id="codex-prompt-panel"></div>
147
+ <div id="codex-skill-panel"></div>
146
148
  </div>
147
149
  <div id="timed-send-panel">
148
150
  <div class="timed-row">
@@ -137,10 +137,17 @@
137
137
  codexPromptTotal = 0;
138
138
  codexPromptLoading = false;
139
139
  codexExpandedPrompts = new Set();
140
+ codexSkillPanelOpen = false;
141
+ codexSkillItems = [];
142
+ codexSkillLoading = false;
143
+ codexSkillQuery = '';
144
+ codexSkillError = '';
145
+ selectedCodexSkill = null;
140
146
  document.getElementById('codex-model-panel').classList.remove('active');
141
147
  document.getElementById('codex-resume-panel').classList.remove('active');
142
148
  document.getElementById('codex-fork-panel').classList.remove('active');
143
149
  document.getElementById('codex-prompt-panel').classList.remove('active');
150
+ document.getElementById('codex-skill-panel').classList.remove('active');
144
151
  document.getElementById('codex-control-rail').scrollLeft = 0;
145
152
  codexState = { permissionMode: 'default', sandboxMode: 'default', effectivePermissionMode: null, effectiveSandboxMode: null, model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canCompact: false, compacting: false, canSwitchToTerminal: false, canSwitchToStructured: false };
146
153
  setClaudeModeEnabled(false);
@@ -88,12 +88,15 @@
88
88
  .codex-conversation { width: min(100%, var(--chat-content-max)); margin: 0 auto; }
89
89
  .codex-working-indicator, .claude-working-indicator { position: sticky; top: 0; z-index: 4; width: 28px; height: 28px; margin: 0 0 -28px auto; border: 1px solid rgba(255,255,255,.1); border-radius: 50%; background: rgba(28,28,30,.68); box-shadow: 0 5px 16px rgba(0,0,0,.24); backdrop-filter: blur(8px); pointer-events: none; }
90
90
  .codex-working-indicator::after, .claude-working-indicator::after { content: ''; position: absolute; inset: 8px; border: 1.5px solid rgba(255,255,255,.7); border-top-color: transparent; border-radius: 50%; animation: codex-spin .8s linear infinite; }
91
+ .codex-skill-bubble { position: sticky; top: 0; z-index: 5; display: flex; width: max-content; max-width: min(280px, 72vw); min-height: 28px; margin: 0 0 -30px auto; padding: 0 4px 0 10px; align-items: center; gap: 7px; border: 1px solid rgba(0,122,255,.42); border-radius: 15px; background: rgba(21,47,78,.92); color: #eaf3ff; box-shadow: 0 5px 18px rgba(0,0,0,.3); backdrop-filter: blur(10px); font-size: 11px; font-weight: 750; }
92
+ .codex-skill-bubble-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
93
+ .codex-skill-bubble-close { width: 23px; height: 23px; padding: 0; border: 0; border-radius: 50%; background: rgba(255,255,255,.1); color: #fff; cursor: pointer; font-size: 16px; line-height: 1; }
91
94
  .codex-message-block { max-width: 100%; margin: 0 0 12px; }
92
95
  .codex-message-block.user { max-width: 92%; margin-left: auto; }
93
96
  .codex-message { max-width: 100%; margin: 0; color: #f5f5f7; font-size: 14px; line-height: 1.45; overflow-wrap: anywhere; }
94
97
  .codex-message.user { padding: 8px 12px; border: 1px solid rgba(0,122,255,.32); border-radius: 12px; background: rgba(0,122,255,.24); }
95
98
  .codex-message.event { color: var(--text-dim); text-align: center; font-size: 12px; }
96
- .codex-message-meta { display: flex; width: 100%; align-items: center; gap: 7px; margin-top: 4px; padding: 0 2px; box-sizing: border-box; }
99
+ .codex-message-meta { display: flex; width: 100%; align-items: center; gap: 7px; flex-wrap: wrap; margin-top: 4px; padding: 0 2px; box-sizing: border-box; }
97
100
  .codex-message-block.user .codex-message-meta { justify-content: flex-end; }
98
101
  .codex-message-time { display: block; flex: 0 0 auto; width: max-content; color: #8e8e93; font-size: 10px; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
99
102
  .codex-context-meter { --context-color: #30d158; position: relative; isolation: isolate; display: inline-flex; min-width: 128px; height: 16px; align-items: center; justify-content: center; overflow: hidden; border: 1px solid color-mix(in srgb, var(--context-color) 42%, transparent); border-radius: 8px; background: rgba(255,255,255,.055); color: #d9fbe2; font: 700 9px/1 ui-monospace, SFMono-Regular, Menlo, monospace; font-variant-numeric: tabular-nums; white-space: nowrap; }
@@ -177,8 +180,8 @@
177
180
  .codex-select-control:focus-within .codex-select-label { background: rgba(255,255,255,.14); border-color: rgba(0,122,255,.45); }
178
181
  .codex-select { appearance: none; position: absolute; inset: 0; width: 100%; min-width: 0; height: 100%; opacity: 0; cursor: pointer; }
179
182
  .codex-select option { background: #1c1c1e; color: #fff; }
180
- #codex-model-panel, #codex-resume-panel, #codex-fork-panel, #codex-prompt-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,.1); background: rgba(28,28,30,.99); border-radius: 8px; box-shadow: 0 16px 36px rgba(0,0,0,.34); overflow: hidden; }
181
- #codex-model-panel.active, #codex-resume-panel.active, #codex-fork-panel.active, #codex-prompt-panel.active { display: block; }
183
+ #codex-model-panel, #codex-resume-panel, #codex-fork-panel, #codex-prompt-panel, #codex-skill-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,.1); background: rgba(28,28,30,.99); border-radius: 8px; box-shadow: 0 16px 36px rgba(0,0,0,.34); overflow: hidden; }
184
+ #codex-model-panel.active, #codex-resume-panel.active, #codex-fork-panel.active, #codex-prompt-panel.active, #codex-skill-panel.active { display: block; }
182
185
  #codex-model-panel { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); max-height: min(300px, 42dvh); }
183
186
  #codex-model-panel.active { display: grid; }
184
187
  .codex-picker-column { min-width: 0; overflow-y: auto; }
@@ -189,6 +192,23 @@
189
192
  #codex-state-bar::-webkit-scrollbar { display: none; }
190
193
  #codex-resume-panel, #codex-fork-panel { max-height: min(280px, 38dvh); overflow-y: auto; }
191
194
  #codex-prompt-panel { max-height: min(440px, 52dvh); overflow-y: auto; }
195
+ #codex-skill-panel { max-height: min(360px, 46dvh); overflow-y: auto; }
196
+ .codex-skill-header { position: sticky; top: 0; z-index: 3; padding: 9px 11px 10px; border-bottom: 1px solid rgba(255,255,255,.08); background: rgba(28,28,30,.97); color: var(--text-dim); font-size: 11px; font-weight: 800; }
197
+ .codex-skill-header-row, .codex-skill-section-title { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
198
+ .codex-skill-search { width: 100%; height: 34px; margin-top: 8px; padding: 0 11px; border: 1px solid rgba(255,255,255,.12); border-radius: 9px; outline: none; background: rgba(255,255,255,.07); color: #f5f5f7; font-size: 12px; box-sizing: border-box; }
199
+ .codex-skill-search::placeholder { color: #77777e; }
200
+ .codex-skill-search:focus { border-color: rgba(0,122,255,.65); background: rgba(255,255,255,.1); box-shadow: 0 0 0 2px rgba(0,122,255,.14); }
201
+ .codex-skill-section + .codex-skill-section { border-top: 1px solid rgba(255,255,255,.09); }
202
+ .codex-skill-section-title { position: sticky; top: 62px; z-index: 2; min-height: 26px; padding: 0 11px; background: rgba(36,36,39,.98); color: #8e8e93; font-size: 9px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }
203
+ .codex-skill-item { display: block; width: 100%; padding: 10px 11px; border: 0; border-bottom: 1px solid rgba(255,255,255,.06); background: transparent; color: #f5f5f7; text-align: left; cursor: pointer; }
204
+ .codex-skill-item.selected { background: rgba(0,122,255,.18); }
205
+ .codex-skill-item-name { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; font-weight: 800; }
206
+ .codex-skill-source { flex: 0 0 auto; padding: 2px 6px; border: 1px solid rgba(0,122,255,.28); border-radius: 999px; background: rgba(0,122,255,.1); color: #79b8ff; font-size: 9px; text-transform: uppercase; }
207
+ .codex-skill-description { display: block; margin-top: 4px; color: #a9a9b0; font-size: 11px; line-height: 1.35; white-space: normal; overflow-wrap: anywhere; }
208
+ .codex-skill-meta { display: flex; min-width: 0; margin-top: 7px; gap: 5px; flex-wrap: wrap; }
209
+ .codex-skill-directory { display: block; min-width: 0; max-width: 100%; flex: 1 1 180px; padding: 2px 6px; overflow: hidden; border: 1px solid rgba(255,255,255,.1); border-radius: 999px; background: rgba(255,255,255,.055); color: #8e8e93; font: 9px/1.25 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; text-overflow: ellipsis; white-space: nowrap; box-sizing: border-box; }
210
+ .codex-skill-empty, .codex-skill-error { padding: 12px; }
211
+ .codex-skill-error { color: #ff6b61; }
192
212
  .codex-prompt-header { position: sticky; top: 0; z-index: 2; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 11px; border-bottom: 1px solid rgba(255,255,255,.08); background: rgba(28,28,30,.97); color: var(--text-dim); font-size: 11px; font-weight: 800; }
193
213
  .codex-prompt-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: end; padding: 10px 11px; border-bottom: 1px solid rgba(255,255,255,.06); }
194
214
  .codex-prompt-item:last-of-type { border-bottom: 0; }
@@ -206,6 +226,7 @@
206
226
  .claude-message-block.user > .claude-message { max-width: 100%; }
207
227
  .claude-message-time { display: block; width: max-content; margin-top: 4px; padding: 0 2px; color: #8e8e93; font-size: 10px; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
208
228
  .claude-message-block.user .claude-message-time { margin-left: auto; }
229
+ .codex-message-skill { display: inline-flex; max-width: min(220px, 58vw); min-height: 16px; align-items: center; padding: 1px 6px; border: 1px solid rgba(0,122,255,.32); border-radius: 999px; background: rgba(0,122,255,.12); color: #8fc5ff; font-size: 9px; font-weight: 750; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
209
230
  .claude-message-attachments { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
210
231
  .claude-message-attachment { max-width: 100%; border: 1px solid rgba(255,255,255,.14); border-radius: 999px; padding: 3px 7px; color: #d1d5db; background: rgba(255,255,255,.07); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
211
232
  .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; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glad-web",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "description": "Glad transforms terminal-based AI coding tools into a polished, mobile-friendly local Web interface.",
5
5
  "bin": {
6
6
  "glad": "bin/cli.js"