glad-web 1.0.33 → 1.0.35

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.
@@ -4,6 +4,8 @@ const readline = require('readline');
4
4
  const crypto = require('crypto');
5
5
  const PTYManager = require('../session/pty-manager');
6
6
 
7
+ const CODEX_MESSAGE_PAGE_BYTES = 200 * 1024;
8
+
7
9
  const PERMISSION_MODES = new Set(['untrusted', 'on-request', 'never']);
8
10
  const SANDBOX_MODES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
9
11
  const FALLBACK_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'ultra'];
@@ -250,14 +252,60 @@ class CodexStructuredSession extends EventEmitter {
250
252
  }
251
253
 
252
254
  snapshot() {
255
+ const historyPage = this.getMessagePage();
256
+ const { messages, ...historyPageMeta } = historyPage;
253
257
  return { id: this.id, name: this.name, tool: this.tool.displayName, toolKey: this.tool.key,
254
- status: this.status, state: this.getControlState(), messages: this.messages,
258
+ status: this.status, state: this.getControlState(), messages, historyPage: historyPageMeta,
255
259
  pendingPermissions: [
256
260
  ...this.completedPermissions,
257
261
  ...Array.from(this.pendingPermissions.values()).map(item => item.public)
258
262
  ] };
259
263
  }
260
264
 
265
+ getMessagePage(beforeId = null, maxBytes = CODEX_MESSAGE_PAGE_BYTES) {
266
+ const requestedBeforeId = beforeId == null ? '' : String(beforeId);
267
+ let end = this.messages.length;
268
+ if (requestedBeforeId) {
269
+ const beforeIndex = this.messages.findIndex(item => String(item.id || '') === requestedBeforeId);
270
+ if (beforeIndex < 0) {
271
+ return { messages: [], hasMore: false, beforeId: null, bytes: 2, maxBytes };
272
+ }
273
+ end = beforeIndex;
274
+ }
275
+
276
+ if (end <= 0) return { messages: [], hasMore: false, beforeId: null, bytes: 2, maxBytes };
277
+
278
+ const groupStarts = [0];
279
+ for (let i = 1; i < end; i++) {
280
+ const item = this.messages[i];
281
+ if (item.kind === 'turn-start' && (!item.threadId || item.threadId === this.threadId)) {
282
+ groupStarts.push(i);
283
+ }
284
+ }
285
+ groupStarts.push(end);
286
+
287
+ let start = end;
288
+ let selectedBytes = 2;
289
+ for (let groupIndex = groupStarts.length - 2; groupIndex >= 0; groupIndex--) {
290
+ const candidateStart = groupStarts[groupIndex];
291
+ const candidate = this.messages.slice(candidateStart, end);
292
+ const candidateBytes = Buffer.byteLength(JSON.stringify(candidate), 'utf8');
293
+ if (start < end && candidateBytes > maxBytes) break;
294
+ start = candidateStart;
295
+ selectedBytes = candidateBytes;
296
+ if (candidateBytes >= maxBytes) break;
297
+ }
298
+
299
+ const messages = this.messages.slice(start, end);
300
+ return {
301
+ messages,
302
+ hasMore: start > 0,
303
+ beforeId: messages[0]?.id || null,
304
+ bytes: selectedBytes,
305
+ maxBytes
306
+ };
307
+ }
308
+
261
309
  getControlState() {
262
310
  const activeSubagentCount = Array.from(this.threadTurns.entries())
263
311
  .filter(([threadId, turn]) => threadId !== this.threadId && turn?.status === 'running').length;
@@ -1088,7 +1136,9 @@ class CodexStructuredSession extends EventEmitter {
1088
1136
  ...(completedAtMs ? { createdAt: completedAtMs } : {}) });
1089
1137
  }
1090
1138
  if (eventText) this.append({ kind: 'event', level: 'info', text: eventText });
1091
- this.emitEvent({ type: 'history-reset', messages: this.messages });
1139
+ const historyPage = this.getMessagePage();
1140
+ const { messages, ...historyPageMeta } = historyPage;
1141
+ this.emitEvent({ type: 'history-reset', messages, historyPage: historyPageMeta });
1092
1142
  this.emitEvent({ type: 'state', state: this.getControlState() });
1093
1143
  }
1094
1144
 
@@ -377,6 +377,12 @@ async function webCommand(options) {
377
377
  if (payload.type === 'codex-compact') {
378
378
  sessionManager.compactCodexContext(sessionId).catch(error => logger.error(`Codex compact error: ${error.message}`));
379
379
  }
380
+ if (payload.type === 'codex-history-before') {
381
+ const codex = sessionManager.get(sessionId);
382
+ if (codex && codex.kind === 'codex-structured' && codex.presentation === 'structured') {
383
+ ws.send(JSON.stringify({ type: 'codex-history-page', page: codex.getMessagePage(payload.beforeId) }));
384
+ }
385
+ }
380
386
  if (payload.type === 'codex-abort') {
381
387
  sessionManager.abortCodex(sessionId);
382
388
  }
package/lib/web/codex.js CHANGED
@@ -320,7 +320,46 @@
320
320
  const current = container.firstElementChild;
321
321
  if (!current) container.appendChild(next);
322
322
  else syncCodexDom(current, next);
323
- container.scrollTop = shouldStickToBottom ? container.scrollHeight : previousScrollTop;
323
+ if (codexHistoryPrependAnchor) {
324
+ const anchor = codexHistoryPrependAnchor;
325
+ codexHistoryPrependAnchor = null;
326
+ container.scrollTop = anchor.scrollTop + Math.max(0, container.scrollHeight - anchor.scrollHeight);
327
+ } else {
328
+ container.scrollTop = shouldStickToBottom ? container.scrollHeight : previousScrollTop;
329
+ }
330
+ }
331
+
332
+ function applyCodexHistoryPageMeta(page = {}) {
333
+ codexHistoryBeforeId = page?.beforeId || codexMessages[0]?.id || null;
334
+ codexHistoryHasMore = Boolean(page?.hasMore);
335
+ codexHistoryLoading = false;
336
+ }
337
+
338
+ function handleCodexHistoryScroll(event) {
339
+ if (event?.isTrusted) codexHistoryUserScrolled = true;
340
+ if (!codexHistoryUserScrolled || !codexHistoryHasMore || codexHistoryLoading) return;
341
+ const container = event?.currentTarget || document.getElementById('codex-chat-container');
342
+ const scrollRange = Math.max(0, container.scrollHeight - container.clientHeight);
343
+ if (container.scrollTop > scrollRange * 0.5) return;
344
+ if (!codexHistoryBeforeId || currentSocket?.readyState !== 1) return;
345
+ codexHistoryLoading = true;
346
+ currentSocket.send(JSON.stringify({ type: 'codex-history-before', beforeId: codexHistoryBeforeId }));
347
+ }
348
+
349
+ function applyCodexHistoryPage(page = {}) {
350
+ const older = Array.isArray(page.messages) ? page.messages : [];
351
+ const knownIds = new Set(codexMessages.map(item => String(item.id || '')));
352
+ const uniqueOlder = older.filter(item => item?.id && !knownIds.has(String(item.id)));
353
+ const container = document.getElementById('codex-chat-container');
354
+ if (uniqueOlder.length && container) {
355
+ codexHistoryPrependAnchor = {
356
+ scrollTop: container.scrollTop,
357
+ scrollHeight: container.scrollHeight
358
+ };
359
+ codexMessages = [...uniqueOlder, ...codexMessages];
360
+ }
361
+ applyCodexHistoryPageMeta(page);
362
+ if (uniqueOlder.length) renderCodexChat();
324
363
  }
325
364
 
326
365
  function applyCodexState(state = {}) {
@@ -445,7 +484,12 @@
445
484
  if (!event) return;
446
485
  if (event.type === 'message' && event.message) codexMessages.push(event.message);
447
486
  else if (event.type === 'message-updated' && event.message) { const i = codexMessages.findIndex(item => item.id === event.message.id); if (i >= 0) codexMessages[i] = event.message; else codexMessages.push(event.message); }
448
- else if (event.type === 'history-reset') codexMessages = event.messages || [];
487
+ else if (event.type === 'history-reset') {
488
+ codexMessages = event.messages || [];
489
+ codexHistoryUserScrolled = false;
490
+ codexHistoryPrependAnchor = null;
491
+ applyCodexHistoryPageMeta(event.historyPage);
492
+ }
449
493
  else if (event.type === 'permission-request' && event.request) { codexPendingPermissions = [...codexPendingPermissions.filter(item => item.id !== event.request.id), event.request]; }
450
494
  else if (event.type === 'permission-updated' && event.request) codexPendingPermissions = codexPendingPermissions.map(item => item.id === event.request.id ? event.request : item);
451
495
  if (event.state) applyCodexState(event.state); else renderCodexChat();
@@ -603,19 +647,157 @@
603
647
  }
604
648
  }
605
649
 
606
- function renderCodexSkillPanel(error = '') {
650
+ const CODEX_RECENT_SKILLS_KEY = 'glad.codex.recentSkills';
651
+
652
+ function codexSkillIdentity(skill) {
653
+ return `${String(skill?.name || '')}\n${String(skill?.path || '')}`;
654
+ }
655
+
656
+ function codexSkillDisplayName(skill) {
657
+ return String(skill?.interface?.displayName || skill?.name || 'Unknown skill');
658
+ }
659
+
660
+ function codexSkillDescription(skill) {
661
+ return String(skill?.interface?.shortDescription || skill?.shortDescription || skill?.description || '');
662
+ }
663
+
664
+ function codexSkillDirectory(skill) {
665
+ const path = String(skill?.path || '').replace(/\\/g, '/');
666
+ const separator = path.lastIndexOf('/');
667
+ const directory = separator > 0 ? path.slice(0, separator) : path;
668
+ const skillRoot = directory.toLowerCase().lastIndexOf('/skills/');
669
+ return skillRoot >= 0 ? directory.slice(skillRoot + '/skills/'.length) : directory.split('/').filter(Boolean).pop() || directory;
670
+ }
671
+
672
+ function codexSkillSource(skill) {
673
+ const scope = String(skill?.scope || '').toLowerCase();
674
+ const path = String(skill?.path || '').replace(/\\/g, '/').toLowerCase();
675
+ if (['repo', 'project', 'workspace'].includes(scope)) return { group: 'project', label: 'Project' };
676
+ if (['user', 'personal'].includes(scope)) return { group: 'personal', label: 'Personal' };
677
+ if (scope === 'plugin') return { group: 'other', label: 'Plugin' };
678
+ if (['system', 'admin', 'builtin', 'built-in'].includes(scope)) return { group: 'other', label: 'System' };
679
+ if (path.includes('/plugins/')) return { group: 'other', label: 'Plugin' };
680
+ if (path.includes('/.system/')) return { group: 'other', label: 'System' };
681
+ if (path.includes('/.codex/skills/')) return { group: 'personal', label: 'Personal' };
682
+ if (path.includes('/.agents/skills/')) return { group: 'project', label: 'Project' };
683
+ return { group: 'other', label: scope ? scope[0].toUpperCase() + scope.slice(1) : 'Other' };
684
+ }
685
+
686
+ function compareCodexSkills(left, right) {
687
+ return codexSkillDisplayName(left).localeCompare(codexSkillDisplayName(right), undefined, {
688
+ sensitivity: 'base',
689
+ numeric: true
690
+ }) || String(left.name || '').localeCompare(String(right.name || ''), undefined, { sensitivity: 'base', numeric: true })
691
+ || String(left.path || '').localeCompare(String(right.path || ''), undefined, { sensitivity: 'base', numeric: true });
692
+ }
693
+
694
+ function readRecentCodexSkills() {
695
+ try {
696
+ const value = JSON.parse(localStorage.getItem(CODEX_RECENT_SKILLS_KEY) || '[]');
697
+ return Array.isArray(value) ? value.filter(item => item?.name && item?.path).slice(0, 3) : [];
698
+ } catch (_) {
699
+ return [];
700
+ }
701
+ }
702
+
703
+ function rememberCodexSkill(skill) {
704
+ const key = codexSkillIdentity(skill);
705
+ const recent = readRecentCodexSkills().filter(item => codexSkillIdentity(item) !== key);
706
+ recent.unshift({ name: skill.name, path: skill.path });
707
+ try { localStorage.setItem(CODEX_RECENT_SKILLS_KEY, JSON.stringify(recent.slice(0, 3))); } catch (_) {}
708
+ }
709
+
710
+ function scoreCodexSkillSearch(skill, query) {
711
+ const tokens = String(query || '').toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
712
+ if (!tokens.length) return 0;
713
+ const source = codexSkillSource(skill);
714
+ const fields = [
715
+ String(skill.name || '').toLocaleLowerCase(),
716
+ codexSkillDisplayName(skill).toLocaleLowerCase(),
717
+ codexSkillDescription(skill).toLocaleLowerCase(),
718
+ source.label.toLocaleLowerCase(),
719
+ String(skill.scope || '').toLocaleLowerCase(),
720
+ String(skill.path || '').toLocaleLowerCase()
721
+ ];
722
+ let score = 0;
723
+ for (const token of tokens) {
724
+ let tokenScore = Number.POSITIVE_INFINITY;
725
+ fields.forEach((field, fieldIndex) => {
726
+ const position = field.indexOf(token);
727
+ if (position < 0) return;
728
+ const exactBonus = field === token ? -4 : position === 0 ? -2 : 0;
729
+ tokenScore = Math.min(tokenScore, fieldIndex * 10 + position + exactBonus);
730
+ });
731
+ if (!Number.isFinite(tokenScore)) return null;
732
+ score += tokenScore;
733
+ }
734
+ return score;
735
+ }
736
+
737
+ function codexSkillItemHtml(item, index) {
738
+ const selected = selectedCodexSkill?.name === item.name && selectedCodexSkill?.path === item.path;
739
+ const displayName = codexSkillDisplayName(item);
740
+ const description = codexSkillDescription(item);
741
+ const source = codexSkillSource(item);
742
+ const fullName = String(item.name || displayName);
743
+ const directory = codexSkillDirectory(item);
744
+ 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>`;
745
+ }
746
+
747
+ function codexSkillSectionHtml(title, entries) {
748
+ if (!entries.length) return '';
749
+ 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>`;
750
+ }
751
+
752
+ function codexSkillResultsHtml() {
753
+ const entries = codexSkillItems.map((item, index) => ({ item, index }));
754
+ const query = codexSkillQuery.trim();
755
+ let sections = '';
756
+ if (query) {
757
+ const matches = entries.map(entry => ({ ...entry, score: scoreCodexSkillSearch(entry.item, query) }))
758
+ .filter(entry => entry.score != null)
759
+ .sort((left, right) => left.score - right.score || compareCodexSkills(left.item, right.item));
760
+ sections = codexSkillSectionHtml('Search results', matches);
761
+ } else {
762
+ const byIdentity = new Map(entries.map(entry => [codexSkillIdentity(entry.item), entry]));
763
+ const recent = readRecentCodexSkills().map(item => byIdentity.get(codexSkillIdentity(item))).filter(Boolean);
764
+ const recentKeys = new Set(recent.map(entry => codexSkillIdentity(entry.item)));
765
+ const remaining = entries.filter(entry => !recentKeys.has(codexSkillIdentity(entry.item)));
766
+ const group = name => remaining.filter(entry => codexSkillSource(entry.item).group === name)
767
+ .sort((left, right) => compareCodexSkills(left.item, right.item));
768
+ sections = codexSkillSectionHtml('Recently used', recent)
769
+ + codexSkillSectionHtml('Current project', group('project'))
770
+ + codexSkillSectionHtml('Personal', group('personal'))
771
+ + codexSkillSectionHtml('Other', group('other'));
772
+ }
773
+ const emptyText = query ? 'No skills match your search.' : 'No enabled skills found for this folder.';
774
+ const empty = !sections && !codexSkillLoading && !codexSkillError
775
+ ? `<div class="claude-resume-meta codex-skill-empty">${emptyText}</div>` : '';
776
+ return `${codexSkillError ? `<div class="claude-resume-meta codex-skill-error">${escapeHtml(codexSkillError)}</div>` : ''}${sections}${empty}`;
777
+ }
778
+
779
+ function renderCodexSkillPanel(focusSearch = false) {
607
780
  const panel = document.getElementById('codex-skill-panel');
608
781
  if (!panel) return;
609
782
  panel.classList.toggle('active', codexSkillPanelOpen);
610
783
  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}`;
784
+ 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>`;
785
+ if (focusSearch) {
786
+ requestAnimationFrame(() => {
787
+ const search = document.getElementById('codex-skill-search');
788
+ if (!search) return;
789
+ search.focus({ preventScroll: true });
790
+ search.setSelectionRange(search.value.length, search.value.length);
791
+ });
792
+ }
793
+ updateTerminalControlsHeight();
794
+ }
795
+
796
+ function updateCodexSkillQuery(value) {
797
+ codexSkillQuery = String(value || '');
798
+ const results = document.getElementById('codex-skill-results');
799
+ if (results) results.innerHTML = codexSkillResultsHtml();
800
+ else renderCodexSkillPanel(true);
619
801
  updateTerminalControlsHeight();
620
802
  }
621
803
 
@@ -636,18 +818,22 @@
636
818
  return;
637
819
  }
638
820
  codexSkillLoading = true;
821
+ codexSkillQuery = '';
822
+ codexSkillError = '';
639
823
  codexSkillItems = [];
640
- renderCodexSkillPanel();
824
+ renderCodexSkillPanel(true);
641
825
  try {
642
826
  const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-skills?forceReload=true`, {}, 30000);
643
827
  const data = await res.json();
644
828
  if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load skills');
645
829
  codexSkillItems = data.skills || [];
646
830
  codexSkillLoading = false;
647
- renderCodexSkillPanel((data.errors || []).map(item => item.message || String(item)).filter(Boolean).join(' · '));
831
+ codexSkillError = (data.errors || []).map(item => item.message || String(item)).filter(Boolean).join(' · ');
832
+ renderCodexSkillPanel(true);
648
833
  } catch (error) {
649
834
  codexSkillLoading = false;
650
- renderCodexSkillPanel(error.message);
835
+ codexSkillError = error.message;
836
+ renderCodexSkillPanel(true);
651
837
  }
652
838
  }
653
839
 
@@ -656,6 +842,7 @@
656
842
  if (!skill) return;
657
843
  const alreadySelected = selectedCodexSkill?.name === skill.name && selectedCodexSkill?.path === skill.path;
658
844
  selectedCodexSkill = alreadySelected ? null : { name: skill.name, path: skill.path };
845
+ if (selectedCodexSkill) rememberCodexSkill(skill);
659
846
  codexSkillPanelOpen = false;
660
847
  renderCodexSkillPanel();
661
848
  applyCodexState({});
package/lib/web/core.js CHANGED
@@ -47,9 +47,16 @@
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;
55
+ let codexHistoryBeforeId = null;
56
+ let codexHistoryHasMore = false;
57
+ let codexHistoryLoading = false;
58
+ let codexHistoryUserScrolled = false;
59
+ let codexHistoryPrependAnchor = null;
53
60
  const modifiers = { ctrl: false };
54
61
 
55
62
  function log(msg) {
@@ -140,7 +140,14 @@
140
140
  codexSkillPanelOpen = false;
141
141
  codexSkillItems = [];
142
142
  codexSkillLoading = false;
143
+ codexSkillQuery = '';
144
+ codexSkillError = '';
143
145
  selectedCodexSkill = null;
146
+ codexHistoryBeforeId = null;
147
+ codexHistoryHasMore = false;
148
+ codexHistoryLoading = false;
149
+ codexHistoryUserScrolled = false;
150
+ codexHistoryPrependAnchor = null;
144
151
  document.getElementById('codex-model-panel').classList.remove('active');
145
152
  document.getElementById('codex-resume-panel').classList.remove('active');
146
153
  document.getElementById('codex-fork-panel').classList.remove('active');
@@ -150,6 +157,7 @@
150
157
  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 };
151
158
  setClaudeModeEnabled(false);
152
159
  applyCodexState(codexState);
160
+ document.getElementById('codex-chat-container').onscroll = handleCodexHistoryScroll;
153
161
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
154
162
  currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
155
163
  currentSocket.onmessage = (e) => {
@@ -157,8 +165,10 @@
157
165
  if (msg.type === 'codex-snapshot' && msg.snapshot) {
158
166
  codexMessages = msg.snapshot.messages || [];
159
167
  codexPendingPermissions = msg.snapshot.pendingPermissions || [];
168
+ applyCodexHistoryPageMeta(msg.snapshot.historyPage);
160
169
  applyCodexState(msg.snapshot.state || {});
161
170
  }
171
+ if (msg.type === 'codex-history-page' && msg.page) applyCodexHistoryPage(msg.page);
162
172
  if (msg.type === 'codex-event') {
163
173
  applyCodexEvent(msg.event);
164
174
  if (msg.event?.type === 'presentation' && msg.event.presentation === 'terminal') {
@@ -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.35",
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"