glad-web 1.0.33 → 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.
package/lib/web/codex.js CHANGED
@@ -603,19 +603,157 @@
603
603
  }
604
604
  }
605
605
 
606
- function renderCodexSkillPanel(error = '') {
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) {
607
736
  const panel = document.getElementById('codex-skill-panel');
608
737
  if (!panel) return;
609
738
  panel.classList.toggle('active', codexSkillPanelOpen);
610
739
  if (!codexSkillPanelOpen) { panel.innerHTML = ''; return; }
611
- const items = codexSkillItems.map((item, index) => {
612
- const selected = selectedCodexSkill?.name === item.name && selectedCodexSkill?.path === item.path;
613
- const description = item.interface?.shortDescription || item.shortDescription || item.description || '';
614
- return `<button type="button" class="codex-skill-item${selected ? ' selected' : ''}" onclick="selectCodexSkill(${index})"><span class="codex-skill-item-name"><span>${escapeHtml(item.interface?.displayName || item.name)}</span><span class="codex-skill-scope">${escapeHtml(item.scope || '')}</span></span>${description ? `<span class="codex-skill-description">${escapeHtml(description)}</span>` : ''}</button>`;
615
- }).join('');
616
- const empty = !items && !codexSkillLoading && !error
617
- ? '<div class="claude-resume-meta" style="padding:12px;">No enabled skills found for this folder.</div>' : '';
618
- panel.innerHTML = `<div class="codex-skill-header"><span>Available skills</span><span>${codexSkillLoading ? 'Loading…' : codexSkillItems.length}</span></div>${error ? `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(error)}</div>` : ''}${items}${empty}`;
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);
619
757
  updateTerminalControlsHeight();
620
758
  }
621
759
 
@@ -636,18 +774,22 @@
636
774
  return;
637
775
  }
638
776
  codexSkillLoading = true;
777
+ codexSkillQuery = '';
778
+ codexSkillError = '';
639
779
  codexSkillItems = [];
640
- renderCodexSkillPanel();
780
+ renderCodexSkillPanel(true);
641
781
  try {
642
782
  const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-skills?forceReload=true`, {}, 30000);
643
783
  const data = await res.json();
644
784
  if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load skills');
645
785
  codexSkillItems = data.skills || [];
646
786
  codexSkillLoading = false;
647
- renderCodexSkillPanel((data.errors || []).map(item => item.message || String(item)).filter(Boolean).join(' · '));
787
+ codexSkillError = (data.errors || []).map(item => item.message || String(item)).filter(Boolean).join(' · ');
788
+ renderCodexSkillPanel(true);
648
789
  } catch (error) {
649
790
  codexSkillLoading = false;
650
- renderCodexSkillPanel(error.message);
791
+ codexSkillError = error.message;
792
+ renderCodexSkillPanel(true);
651
793
  }
652
794
  }
653
795
 
@@ -656,6 +798,7 @@
656
798
  if (!skill) return;
657
799
  const alreadySelected = selectedCodexSkill?.name === skill.name && selectedCodexSkill?.path === skill.path;
658
800
  selectedCodexSkill = alreadySelected ? null : { name: skill.name, path: skill.path };
801
+ if (selectedCodexSkill) rememberCodexSkill(skill);
659
802
  codexSkillPanelOpen = false;
660
803
  renderCodexSkillPanel();
661
804
  applyCodexState({});
package/lib/web/core.js CHANGED
@@ -47,6 +47,8 @@
47
47
  let codexSkillPanelOpen = false;
48
48
  let codexSkillItems = [];
49
49
  let codexSkillLoading = false;
50
+ let codexSkillQuery = '';
51
+ let codexSkillError = '';
50
52
  let selectedCodexSkill = null;
51
53
  let codexRenderFrame = null;
52
54
  let codexApprovalJumpIndex = 0;
@@ -140,6 +140,8 @@
140
140
  codexSkillPanelOpen = false;
141
141
  codexSkillItems = [];
142
142
  codexSkillLoading = false;
143
+ codexSkillQuery = '';
144
+ codexSkillError = '';
143
145
  selectedCodexSkill = null;
144
146
  document.getElementById('codex-model-panel').classList.remove('active');
145
147
  document.getElementById('codex-resume-panel').classList.remove('active');
@@ -193,12 +193,22 @@
193
193
  #codex-resume-panel, #codex-fork-panel { max-height: min(280px, 38dvh); overflow-y: auto; }
194
194
  #codex-prompt-panel { max-height: min(440px, 52dvh); overflow-y: auto; }
195
195
  #codex-skill-panel { max-height: min(360px, 46dvh); overflow-y: auto; }
196
- .codex-skill-header { position: sticky; top: 0; z-index: 2; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 9px 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; }
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; }
197
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; }
198
204
  .codex-skill-item.selected { background: rgba(0,122,255,.18); }
199
205
  .codex-skill-item-name { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; font-weight: 800; }
200
- .codex-skill-scope { color: #79b8ff; font-size: 10px; text-transform: uppercase; }
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; }
201
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; }
202
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; }
203
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); }
204
214
  .codex-prompt-item:last-of-type { border-bottom: 0; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glad-web",
3
- "version": "1.0.33",
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"