glad-web 1.0.31 → 1.0.33

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 : [];
@@ -85,6 +100,27 @@ function recentUserQuestions(thread, limit = 2) {
85
100
  return questions;
86
101
  }
87
102
 
103
+ function userPromptsFromThread(thread, fallbackTimestamp = null) {
104
+ const prompts = [];
105
+ const threadId = String(thread?.id || '');
106
+ for (const turn of Array.isArray(thread?.turns) ? thread.turns : []) {
107
+ const turnTimestamp = toTimestampMs(turn.startedAt || turn.createdAt || turn.completedAt || turn.updatedAt)
108
+ || fallbackTimestamp;
109
+ for (const item of Array.isArray(turn?.items) ? turn.items : []) {
110
+ if (item?.type !== 'userMessage') continue;
111
+ const prompt = (textFromInputItems(item.content) || item.text || '').trim();
112
+ if (!prompt) continue;
113
+ prompts.push({
114
+ id: String(item.id || `${threadId}:${turn.id || 'turn'}:${prompts.length}`),
115
+ threadId,
116
+ text: prompt,
117
+ createdAt: toTimestampMs(item.createdAt || item.updatedAt) || turnTimestamp || null
118
+ });
119
+ }
120
+ }
121
+ return prompts;
122
+ }
123
+
88
124
  function toolDetails(raw) {
89
125
  if (raw.type === 'commandExecution') {
90
126
  return {
@@ -195,6 +231,8 @@ class CodexStructuredSession extends EventEmitter {
195
231
  this.inputSeq = 0;
196
232
  this.completionReadInputSeq = 0;
197
233
  this.timedInputs = new Map();
234
+ this.promptHistoryCache = null;
235
+ this.deferredWarnings = null;
198
236
  this.ptyManager = {
199
237
  workingDir,
200
238
  isRunning: () => this.isRunning(),
@@ -491,7 +529,9 @@ class CodexStructuredSession extends EventEmitter {
491
529
  return;
492
530
  }
493
531
  if (method === 'warning' || method === 'guardianWarning') {
494
- this.append({ kind: 'event', level: 'warning', text: params.message || params.warning || 'Codex warning.' });
532
+ const warning = { kind: 'event', level: 'warning', text: params.message || params.warning || 'Codex warning.' };
533
+ if (this.deferredWarnings) this.deferredWarnings.push(warning);
534
+ else this.append(warning);
495
535
  return;
496
536
  }
497
537
  if (method === 'item/commandExecution/outputDelta' || method === 'item/fileChange/outputDelta') {
@@ -572,7 +612,9 @@ class CodexStructuredSession extends EventEmitter {
572
612
  ...timing, toolStatus: inferredToolStatus || raw.status || 'running' }
573
613
  : kind === 'compaction' ? { providerId, threadId, turnId, ...timing,
574
614
  compactionStatus: inferredStatus || raw.status || 'running' }
575
- : { text, threadId, turnId, streaming: false, ...(completedAtMs ? { completedAtMs } : {}) };
615
+ : { text, threadId, turnId, streaming: false,
616
+ ...(kind === 'user' ? { skills: skillsFromInputItems(raw.content) } : {}),
617
+ ...(completedAtMs ? { completedAtMs } : {}) };
576
618
  if (existing) {
577
619
  this.patch(existing.id, patch);
578
620
  } else if (kind === 'user') {
@@ -605,6 +647,37 @@ class CodexStructuredSession extends EventEmitter {
605
647
  return models;
606
648
  }
607
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
+
608
681
  async refreshConfigDefaults() {
609
682
  const result = await this.request('config/read', { cwd: this.workingDir, includeLayers: false });
610
683
  const config = result?.config || {};
@@ -655,6 +728,68 @@ class CodexStructuredSession extends EventEmitter {
655
728
  return items;
656
729
  }
657
730
 
731
+ async listPromptHistory({ offset = 0, limit = 30 } = {}) {
732
+ await this.ensureProcess();
733
+ const safeOffset = Math.max(0, Math.min(199, Number(offset) || 0));
734
+ const safeLimit = Math.max(1, Math.min(30, Number(limit) || 30));
735
+ const cacheFresh = this.promptHistoryCache
736
+ && Date.now() - this.promptHistoryCache.loadedAt < 15000;
737
+
738
+ if (!cacheFresh) {
739
+ const prompts = [];
740
+ let cursor = null;
741
+ let pageCount = 0;
742
+ let capped = false;
743
+ do {
744
+ const result = await this.request('thread/list', {
745
+ cursor,
746
+ limit: 20,
747
+ sortKey: 'updated_at',
748
+ sortDirection: 'desc',
749
+ archived: false,
750
+ cwd: this.workingDir
751
+ });
752
+ const threads = (result?.data || []).filter(item => !item.parentThreadId);
753
+ const histories = await Promise.all(threads.map(async item => {
754
+ try {
755
+ const history = await this.request('thread/read', { threadId: item.id, includeTurns: true });
756
+ const fallbackTimestamp = toTimestampMs(item.updatedAt || item.createdAt);
757
+ return userPromptsFromThread(history?.thread || { id: item.id, turns: [] }, fallbackTimestamp)
758
+ .map(prompt => ({ ...prompt, threadId: prompt.threadId || item.id }));
759
+ } catch (error) {
760
+ this.logger.debugInfo?.(`[codex-app-server] unable to read prompt history for ${item.id}: ${error.message}`);
761
+ return [];
762
+ }
763
+ }));
764
+ prompts.push(...histories.flat());
765
+ cursor = result?.nextCursor || null;
766
+ pageCount += 1;
767
+ if (prompts.length >= 200 || pageCount >= 5) {
768
+ capped = Boolean(cursor) || prompts.length > 200;
769
+ break;
770
+ }
771
+ } while (cursor);
772
+
773
+ prompts.sort((a, b) => Number(b.createdAt || 0) - Number(a.createdAt || 0));
774
+ this.promptHistoryCache = {
775
+ loadedAt: Date.now(),
776
+ items: prompts.slice(0, 200),
777
+ capped
778
+ };
779
+ }
780
+
781
+ const items = this.promptHistoryCache.items.slice(safeOffset, safeOffset + safeLimit);
782
+ const nextOffset = safeOffset + items.length;
783
+ return {
784
+ items,
785
+ offset: safeOffset,
786
+ nextOffset,
787
+ total: this.promptHistoryCache.items.length,
788
+ hasMore: nextOffset < this.promptHistoryCache.items.length,
789
+ capped: this.promptHistoryCache.capped
790
+ };
791
+ }
792
+
658
793
  contextStatus(tokenUsage = this.tokenUsage) {
659
794
  const usage = tokenUsage || {};
660
795
  const selectedModel = this.models.find(item => item.id === this.model);
@@ -751,16 +886,20 @@ class CodexStructuredSession extends EventEmitter {
751
886
  return this.getControlState();
752
887
  }
753
888
 
754
- async sendUserMessage(text, attachments = []) {
889
+ async sendUserMessage(text, attachments = [], skills = []) {
755
890
  const prompt = String(text || '').trim();
756
891
  const images = (Array.isArray(attachments) ? attachments : [])
757
892
  .filter(item => item && typeof item.path === 'string' && item.path);
758
893
  if ((!prompt && images.length === 0) || this.presentation !== 'structured' || this.status !== 'idle') return false;
759
894
  this.hasUnreadCompletion = false;
895
+ this.promptHistoryCache = null;
760
896
  this.append({
761
897
  kind: 'user',
762
898
  text: prompt || '📷 Image attachment',
763
- 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)
764
903
  });
765
904
  try {
766
905
  await this.ensureProcess();
@@ -779,6 +918,7 @@ class CodexStructuredSession extends EventEmitter {
779
918
  }
780
919
  this.setStatus('running');
781
920
  const input = [];
921
+ input.push(...await this.resolveSkillInputs(skills));
782
922
  if (prompt) input.push({ type: 'text', text: prompt });
783
923
  for (const image of images) input.push({ type: 'localImage', path: image.path });
784
924
  const params = { threadId: this.threadId, input, cwd: this.workingDir, summary: 'auto' };
@@ -882,25 +1022,46 @@ class CodexStructuredSession extends EventEmitter {
882
1022
  const target = String(threadId || this.threadId || '').trim();
883
1023
  if (!target || this.presentation !== 'structured' || this.status !== 'idle') return false;
884
1024
  await this.ensureProcess();
1025
+ const selectedModel = this.model;
1026
+ const selectedEffort = this.effort;
1027
+ const resumeWithModelOverride = Boolean(this.hasModelOverride && selectedModel);
1028
+ const resumeWithEffortOverride = Boolean(this.hasEffortOverride && selectedEffort);
885
1029
  const params = { threadId: target, cwd: this.workingDir };
886
1030
  if (this.permissionMode) params.approvalPolicy = this.permissionMode;
887
1031
  if (this.sandboxMode) params.sandbox = this.sandboxMode;
888
- const result = await this.request('thread/resume', params);
889
- this.threadId = result.thread?.id || target;
890
- this.hasModelOverride = false;
891
- this.hasEffortOverride = false;
892
- this.model = result.model || result.thread?.model || this.model;
893
- this.effort = result.reasoningEffort || result.thread?.reasoningEffort || this.effort;
894
- this.effectivePermissionMode = result.approvalPolicy || this.effectivePermissionMode;
895
- this.effectiveSandboxMode = sandboxModeFromPolicy(result.sandbox) || this.effectiveSandboxMode;
896
- const history = await this.request('thread/read', { threadId: this.threadId, includeTurns: true });
897
- this.restoreThreadHistory(history?.thread, `Resumed Codex thread ${this.threadId}`);
898
- return true;
1032
+ if (resumeWithModelOverride) params.model = selectedModel;
1033
+ if (resumeWithEffortOverride) params.config = { model_reasoning_effort: selectedEffort };
1034
+ this.deferredWarnings = [];
1035
+ try {
1036
+ const result = await this.request('thread/resume', params);
1037
+ this.threadId = result.thread?.id || target;
1038
+ this.hasModelOverride = resumeWithModelOverride;
1039
+ this.hasEffortOverride = resumeWithEffortOverride;
1040
+ this.model = result.model || result.thread?.model || selectedModel;
1041
+ this.effort = result.reasoningEffort || result.thread?.reasoningEffort || selectedEffort;
1042
+ this.effectivePermissionMode = result.approvalPolicy || this.effectivePermissionMode;
1043
+ this.effectiveSandboxMode = sandboxModeFromPolicy(result.sandbox) || this.effectiveSandboxMode;
1044
+ const history = await this.request('thread/read', { threadId: this.threadId, includeTurns: true });
1045
+ this.restoreThreadHistory(history?.thread, `Resumed Codex thread ${this.threadId}`, {
1046
+ preserveModel: resumeWithModelOverride,
1047
+ preserveEffort: resumeWithEffortOverride
1048
+ });
1049
+ const warnings = this.deferredWarnings;
1050
+ this.deferredWarnings = null;
1051
+ for (const warning of warnings) this.append(warning);
1052
+ this.promptHistoryCache = null;
1053
+ return true;
1054
+ } catch (error) {
1055
+ const warnings = this.deferredWarnings || [];
1056
+ this.deferredWarnings = null;
1057
+ for (const warning of warnings) this.append(warning);
1058
+ throw error;
1059
+ }
899
1060
  }
900
1061
 
901
- restoreThreadHistory(thread, eventText = '') {
902
- this.model = thread?.model || this.model;
903
- this.effort = thread?.reasoningEffort || thread?.reasoning_effort || this.effort;
1062
+ restoreThreadHistory(thread, eventText = '', options = {}) {
1063
+ if (!options.preserveModel) this.model = thread?.model || this.model;
1064
+ if (!options.preserveEffort) this.effort = thread?.reasoningEffort || thread?.reasoning_effort || this.effort;
904
1065
  this.tokenUsage = thread?.tokenUsage || thread?.token_usage || this.tokenUsage;
905
1066
  this.messages = [];
906
1067
  this.completedPermissions = [];
@@ -948,6 +1109,7 @@ class CodexStructuredSession extends EventEmitter {
948
1109
  this.effectivePermissionMode = result.approvalPolicy || this.effectivePermissionMode;
949
1110
  this.effectiveSandboxMode = sandboxModeFromPolicy(result.sandbox) || this.effectiveSandboxMode;
950
1111
  this.restoreThreadHistory(forkedThread, `Forked from Codex thread ${sourceThreadId}`);
1112
+ this.promptHistoryCache = null;
951
1113
  return { threadId: this.threadId };
952
1114
  }
953
1115
 
@@ -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') {
@@ -55,6 +55,29 @@ function registerProviderRoutes(app, { sessionManager }) {
55
55
  }
56
56
  });
57
57
 
58
+ app.get('/api/sessions/:id/codex-prompts', async (req, res) => {
59
+ try {
60
+ const result = await sessionManager.listCodexPrompts(req.params.id, {
61
+ offset: req.query?.offset,
62
+ limit: req.query?.limit
63
+ });
64
+ if (!result) return res.status(404).json({ error: 'Codex session not found' });
65
+ res.json({ success: true, ...result });
66
+ } catch (error) {
67
+ res.status(400).json({ error: error.message });
68
+ }
69
+ });
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
+
58
81
  app.post('/api/sessions/:id/codex-abort', (req, res) => {
59
82
  const success = sessionManager.abortCodex(req.params.id);
60
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
  }
@@ -431,6 +431,18 @@ class SessionManager extends EventEmitter {
431
431
  return session.listResumeThreads();
432
432
  }
433
433
 
434
+ listCodexPrompts(id, options) {
435
+ const session = this.get(id);
436
+ if (!session || session.kind !== 'codex-structured') return null;
437
+ return session.listPromptHistory(options);
438
+ }
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
+
434
446
  switchCodexPresentation(id, presentation) {
435
447
  const session = this.get(id);
436
448
  if (!session || session.kind !== 'codex-structured') return Promise.resolve(false);
package/lib/web/codex.js CHANGED
@@ -103,6 +103,9 @@
103
103
  const detail = running ? 'Compacting context…' : ['Completed', time].filter(Boolean).join(' · ');
104
104
  return `<div class="codex-compaction-card${running ? ' running' : ''}" data-codex-key="compaction-${escapeHtml(item.id || item.providerId || '')}"><span class="codex-compaction-icon" aria-hidden="true">⇣</span><div><div class="codex-compaction-title">${running ? 'Compacting context' : 'Context compacted'}</div><div class="codex-compaction-meta">${escapeHtml(detail)}</div></div></div>`;
105
105
  }
106
+ function renderCodexWarning(item) {
107
+ return `<div class="codex-warning-card" data-codex-key="warning-${escapeHtml(item.id || '')}" role="status"><span class="codex-warning-icon" aria-hidden="true">!</span><div><div class="codex-warning-title">Codex warning</div><div class="codex-warning-text">${codexText(item.text || 'Codex reported a warning.')}</div></div></div>`;
108
+ }
106
109
  function renderCodexTool(item, permission = null) {
107
110
  const status = codexToolStatus(item);
108
111
  const isError = status === 'failed' || Boolean(item.error) || (item.exitCode != null && item.exitCode !== 0);
@@ -196,10 +199,15 @@
196
199
  const label = `${formatCodexTokens(remaining)} / ${formatCodexTokens(total)}(${Math.round(percent)}%)`;
197
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>`;
198
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
+ }
199
206
  function renderCodexMessageMeta(item, finalOnly = false, context = null) {
200
207
  const time = renderCodexMessageTime(item, finalOnly);
208
+ const skills = renderCodexMessageSkills(item);
201
209
  const meter = renderCodexContextMeter(context);
202
- 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>` : '';
203
211
  }
204
212
  function syncCodexDom(current, next) {
205
213
  if (!current || !next) return;
@@ -297,13 +305,17 @@
297
305
  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>`);
298
306
  else if (item.kind === 'compaction') parts.push(renderCodexCompaction(item));
299
307
  else if (item.kind === 'status') parts.push(renderCodexStatus(item));
308
+ else if (item.kind === 'event' && item.level === 'warning') parts.push(renderCodexWarning(item));
300
309
  else parts.push(`<div class="codex-message ${escapeHtml(item.level === 'error' ? 'event error' : item.kind || 'event')}">${codexText(item.text)}</div>`);
301
310
  i += 1;
302
311
  }
303
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
+ : '';
304
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>`;
305
317
  const template = document.createElement('template');
306
- 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>`;
307
319
  const next = template.content.firstElementChild;
308
320
  const current = container.firstElementChild;
309
321
  if (!current) container.appendChild(next);
@@ -338,6 +350,8 @@
338
350
  if (abort) abort.disabled = !codexState.canAbort;
339
351
  const compact = document.getElementById('codex-compact-btn');
340
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)); }
341
355
  const fork = document.getElementById('codex-fork-btn');
342
356
  if (fork) fork.disabled = !(codexState.presentation === 'structured' && codexState.status === 'idle');
343
357
  const terminal = document.getElementById('codex-terminal-switch');
@@ -395,9 +409,13 @@
395
409
  codexModelPanelOpen = !codexModelPanelOpen;
396
410
  codexResumePanelOpen = false;
397
411
  codexForkPanelOpen = false;
412
+ codexPromptPanelOpen = false;
413
+ codexSkillPanelOpen = false;
398
414
  codexModelCandidate = codexState.model || codexState.models?.[0]?.id || null;
399
415
  document.getElementById('codex-resume-panel').classList.remove('active');
400
416
  document.getElementById('codex-fork-panel').classList.remove('active');
417
+ document.getElementById('codex-prompt-panel').classList.remove('active');
418
+ document.getElementById('codex-skill-panel').classList.remove('active');
401
419
  renderCodexModelPanel();
402
420
  updateTerminalControlsHeight();
403
421
  }
@@ -453,8 +471,12 @@
453
471
  codexResumePanelOpen = !codexResumePanelOpen;
454
472
  codexModelPanelOpen = false;
455
473
  codexForkPanelOpen = false;
474
+ codexPromptPanelOpen = false;
475
+ codexSkillPanelOpen = false;
456
476
  document.getElementById('codex-model-panel').classList.remove('active');
457
477
  document.getElementById('codex-fork-panel').classList.remove('active');
478
+ document.getElementById('codex-prompt-panel').classList.remove('active');
479
+ document.getElementById('codex-skill-panel').classList.remove('active');
458
480
  const panel = document.getElementById('codex-resume-panel');
459
481
  panel.classList.toggle('active', codexResumePanelOpen);
460
482
  updateTerminalControlsHeight();
@@ -466,8 +488,12 @@
466
488
  codexForkPanelOpen = !codexForkPanelOpen;
467
489
  codexModelPanelOpen = false;
468
490
  codexResumePanelOpen = false;
491
+ codexPromptPanelOpen = false;
492
+ codexSkillPanelOpen = false;
469
493
  document.getElementById('codex-model-panel').classList.remove('active');
470
494
  document.getElementById('codex-resume-panel').classList.remove('active');
495
+ document.getElementById('codex-prompt-panel').classList.remove('active');
496
+ document.getElementById('codex-skill-panel').classList.remove('active');
471
497
  const panel = document.getElementById('codex-fork-panel');
472
498
  panel.classList.toggle('active', codexForkPanelOpen);
473
499
  updateTerminalControlsHeight();
@@ -489,6 +515,157 @@
489
515
  } catch (e) { panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(e.message)}</div>`; }
490
516
  updateTerminalControlsHeight();
491
517
  }
518
+ function formatCodexPromptTime(timestamp) {
519
+ const value = Number(timestamp || 0);
520
+ if (!value) return '';
521
+ const date = new Date(value);
522
+ return Number.isNaN(date.getTime()) ? '' : date.toLocaleString([], {
523
+ month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
524
+ });
525
+ }
526
+ function renderCodexPromptPanel(error = '') {
527
+ const panel = document.getElementById('codex-prompt-panel');
528
+ if (!panel) return;
529
+ const previousScrollTop = panel.scrollTop;
530
+ panel.classList.toggle('active', codexPromptPanelOpen);
531
+ if (!codexPromptPanelOpen) return;
532
+ const countLabel = codexPromptTotal ? `${codexPromptItems.length} / ${codexPromptTotal}${codexPromptTotal >= 200 ? ' max' : ''}` : '';
533
+ const items = codexPromptItems.map((item, index) => {
534
+ const expanded = codexExpandedPrompts.has(index);
535
+ return `<div class="codex-prompt-item"><button type="button" class="codex-prompt-text${expanded ? ' expanded' : ''}" onclick="toggleCodexPromptExpanded(${index})" title="${expanded ? 'Collapse prompt' : 'Expand prompt'}">${escapeHtml(item.text || '')}</button><div class="codex-prompt-actions"><span class="codex-prompt-time">${escapeHtml(formatCodexPromptTime(item.createdAt))}</span><button type="button" class="small-btn codex-prompt-copy" data-codex-prompt-copy="${index}" onclick="copyCodexPrompt(${index})">Copy</button></div></div>`;
536
+ }).join('');
537
+ const empty = !items && !codexPromptLoading && !error
538
+ ? '<div class="claude-resume-meta" style="padding:12px;">No text prompts found for this folder.</div>' : '';
539
+ const footer = codexPromptHasMore || codexPromptLoading
540
+ ? `<div class="codex-prompt-footer"><button type="button" class="small-btn primary" onclick="loadMoreCodexPrompts()"${codexPromptLoading ? ' disabled' : ''}>${codexPromptLoading ? 'Loading…' : 'Load more'}</button></div>` : '';
541
+ panel.innerHTML = `<div class="codex-prompt-header"><span>Prompt history</span><span>${escapeHtml(countLabel)}</span></div>${error ? `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(error)}</div>` : ''}${items}${empty}${footer}`;
542
+ panel.scrollTop = previousScrollTop;
543
+ updateTerminalControlsHeight();
544
+ }
545
+ async function toggleCodexPromptPanel() {
546
+ codexPromptPanelOpen = !codexPromptPanelOpen;
547
+ codexModelPanelOpen = false;
548
+ codexResumePanelOpen = false;
549
+ codexForkPanelOpen = false;
550
+ codexSkillPanelOpen = false;
551
+ document.getElementById('codex-model-panel').classList.remove('active');
552
+ document.getElementById('codex-resume-panel').classList.remove('active');
553
+ document.getElementById('codex-fork-panel').classList.remove('active');
554
+ document.getElementById('codex-skill-panel').classList.remove('active');
555
+ if (!codexPromptPanelOpen) {
556
+ document.getElementById('codex-prompt-panel').classList.remove('active');
557
+ updateTerminalControlsHeight();
558
+ return;
559
+ }
560
+ codexPromptItems = [];
561
+ codexPromptNextOffset = 0;
562
+ codexPromptHasMore = false;
563
+ codexPromptTotal = 0;
564
+ codexExpandedPrompts = new Set();
565
+ renderCodexPromptPanel();
566
+ await loadMoreCodexPrompts();
567
+ }
568
+ async function loadMoreCodexPrompts() {
569
+ if (codexPromptLoading || !codexPromptPanelOpen || codexPromptNextOffset >= 200) return;
570
+ codexPromptLoading = true;
571
+ renderCodexPromptPanel();
572
+ try {
573
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-prompts?offset=${codexPromptNextOffset}&limit=30`, {}, 60000);
574
+ const data = await res.json();
575
+ if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load prompt history');
576
+ codexPromptItems.push(...(data.items || []));
577
+ codexPromptNextOffset = Number(data.nextOffset || codexPromptItems.length);
578
+ codexPromptHasMore = Boolean(data.hasMore) && codexPromptNextOffset < 200;
579
+ codexPromptTotal = Math.min(200, Number(data.total || codexPromptItems.length));
580
+ codexPromptLoading = false;
581
+ renderCodexPromptPanel();
582
+ } catch (error) {
583
+ codexPromptLoading = false;
584
+ renderCodexPromptPanel(error.message);
585
+ }
586
+ }
587
+ function toggleCodexPromptExpanded(index) {
588
+ if (codexExpandedPrompts.has(index)) codexExpandedPrompts.delete(index);
589
+ else codexExpandedPrompts.add(index);
590
+ renderCodexPromptPanel();
591
+ }
592
+ async function copyCodexPrompt(index) {
593
+ const prompt = codexPromptItems[index]?.text;
594
+ if (!prompt) return;
595
+ try {
596
+ await copyTextToClipboard(prompt);
597
+ const button = document.querySelector(`[data-codex-prompt-copy="${index}"]`);
598
+ if (!button) return;
599
+ button.textContent = 'Copied';
600
+ setTimeout(() => { if (button.isConnected) button.textContent = 'Copy'; }, 1200);
601
+ } catch (_) {
602
+ alert('Copy failed.');
603
+ }
604
+ }
605
+
606
+ function renderCodexSkillPanel(error = '') {
607
+ const panel = document.getElementById('codex-skill-panel');
608
+ if (!panel) return;
609
+ panel.classList.toggle('active', codexSkillPanelOpen);
610
+ 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}`;
619
+ updateTerminalControlsHeight();
620
+ }
621
+
622
+ async function toggleCodexSkillPanel() {
623
+ if (!(codexState.presentation === 'structured' && codexState.status === 'idle')) return;
624
+ codexSkillPanelOpen = !codexSkillPanelOpen;
625
+ codexModelPanelOpen = false;
626
+ codexResumePanelOpen = false;
627
+ codexForkPanelOpen = false;
628
+ codexPromptPanelOpen = false;
629
+ document.getElementById('codex-model-panel').classList.remove('active');
630
+ document.getElementById('codex-resume-panel').classList.remove('active');
631
+ document.getElementById('codex-fork-panel').classList.remove('active');
632
+ document.getElementById('codex-prompt-panel').classList.remove('active');
633
+ if (!codexSkillPanelOpen) {
634
+ renderCodexSkillPanel();
635
+ updateTerminalControlsHeight();
636
+ return;
637
+ }
638
+ codexSkillLoading = true;
639
+ codexSkillItems = [];
640
+ renderCodexSkillPanel();
641
+ try {
642
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-skills?forceReload=true`, {}, 30000);
643
+ const data = await res.json();
644
+ if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load skills');
645
+ codexSkillItems = data.skills || [];
646
+ codexSkillLoading = false;
647
+ renderCodexSkillPanel((data.errors || []).map(item => item.message || String(item)).filter(Boolean).join(' · '));
648
+ } catch (error) {
649
+ codexSkillLoading = false;
650
+ renderCodexSkillPanel(error.message);
651
+ }
652
+ }
653
+
654
+ function selectCodexSkill(index) {
655
+ const skill = codexSkillItems[index];
656
+ if (!skill) return;
657
+ const alreadySelected = selectedCodexSkill?.name === skill.name && selectedCodexSkill?.path === skill.path;
658
+ selectedCodexSkill = alreadySelected ? null : { name: skill.name, path: skill.path };
659
+ codexSkillPanelOpen = false;
660
+ renderCodexSkillPanel();
661
+ applyCodexState({});
662
+ updateTerminalControlsHeight();
663
+ }
664
+
665
+ function clearCodexSkillSelection() {
666
+ selectedCodexSkill = null;
667
+ applyCodexState({});
668
+ }
492
669
  async function selectCodexResumeThread(threadId) {
493
670
  const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threadId }) }, 30000);
494
671
  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
@@ -37,6 +37,17 @@
37
37
  let codexModelCandidate = null;
38
38
  let codexResumePanelOpen = false;
39
39
  let codexForkPanelOpen = false;
40
+ let codexPromptPanelOpen = false;
41
+ let codexPromptItems = [];
42
+ let codexPromptNextOffset = 0;
43
+ let codexPromptHasMore = false;
44
+ let codexPromptTotal = 0;
45
+ let codexPromptLoading = false;
46
+ let codexExpandedPrompts = new Set();
47
+ let codexSkillPanelOpen = false;
48
+ let codexSkillItems = [];
49
+ let codexSkillLoading = false;
50
+ let selectedCodexSkill = null;
40
51
  let codexRenderFrame = null;
41
52
  let codexApprovalJumpIndex = 0;
42
53
  const modifiers = { ctrl: false };
@@ -122,6 +122,7 @@
122
122
  <button id="codex-fork-btn" class="claude-ctrl-btn primary" onclick="toggleCodexForkPanel()" title="Fork a Codex thread in this conversation">Fork</button>
123
123
  </div>
124
124
  <div class="codex-control-page">
125
+ <button id="codex-prompts-btn" class="claude-ctrl-btn" onclick="toggleCodexPromptPanel()" title="Browse and copy recent prompts">Prompts</button>
125
126
  <button id="codex-compact-btn" class="claude-ctrl-btn" onclick="compactCodexContext()" title="Compact the current Codex context">Compact</button>
126
127
  <label class="codex-select-control" title="Sandbox mode">
127
128
  <span class="codex-select-label" aria-hidden="true">Sandbox</span>
@@ -135,12 +136,15 @@
135
136
  <option value="default">Default</option><option value="untrusted">Untrusted</option><option value="on-request">On request</option><option value="never">Never ask</option>
136
137
  </select>
137
138
  </label>
139
+ <button id="codex-skills-btn" class="claude-ctrl-btn" onclick="toggleCodexSkillPanel()" title="Choose a skill for the next message">Skills</button>
138
140
  </div>
139
141
  </div>
140
142
  <div id="codex-state-bar"></div>
141
143
  <div id="codex-model-panel"></div>
142
144
  <div id="codex-resume-panel"></div>
143
145
  <div id="codex-fork-panel"></div>
146
+ <div id="codex-prompt-panel"></div>
147
+ <div id="codex-skill-panel"></div>
144
148
  </div>
145
149
  <div id="timed-send-panel">
146
150
  <div class="timed-row">
@@ -130,9 +130,22 @@
130
130
  codexModelCandidate = null;
131
131
  codexResumePanelOpen = false;
132
132
  codexForkPanelOpen = false;
133
+ codexPromptPanelOpen = false;
134
+ codexPromptItems = [];
135
+ codexPromptNextOffset = 0;
136
+ codexPromptHasMore = false;
137
+ codexPromptTotal = 0;
138
+ codexPromptLoading = false;
139
+ codexExpandedPrompts = new Set();
140
+ codexSkillPanelOpen = false;
141
+ codexSkillItems = [];
142
+ codexSkillLoading = false;
143
+ selectedCodexSkill = null;
133
144
  document.getElementById('codex-model-panel').classList.remove('active');
134
145
  document.getElementById('codex-resume-panel').classList.remove('active');
135
146
  document.getElementById('codex-fork-panel').classList.remove('active');
147
+ document.getElementById('codex-prompt-panel').classList.remove('active');
148
+ document.getElementById('codex-skill-panel').classList.remove('active');
136
149
  document.getElementById('codex-control-rail').scrollLeft = 0;
137
150
  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 };
138
151
  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; }
@@ -105,6 +108,10 @@
105
108
  .codex-compaction-title { font-size: 12px; font-weight: 800; }
106
109
  .codex-compaction-meta { margin-top: 3px; color: #a9a9b0; font-size: 10px; font-variant-numeric: tabular-nums; }
107
110
  .codex-compaction-card.running .codex-compaction-icon { animation: codex-pulse 1.1s ease-in-out infinite alternate; }
111
+ .codex-warning-card { display: flex; align-items: flex-start; gap: 10px; margin: 10px 0 14px; padding: 10px 12px; border: 1px solid rgba(255,204,0,.34); border-radius: 10px; background: linear-gradient(135deg, rgba(255,204,0,.13), rgba(255,159,10,.07)); color: #f5f5f7; }
112
+ .codex-warning-icon { display: grid; width: 28px; height: 28px; flex: 0 0 28px; place-items: center; border-radius: 50%; background: rgba(255,204,0,.16); color: #ffd60a; font-size: 15px; font-weight: 900; }
113
+ .codex-warning-title { color: #ffe680; font-size: 12px; font-weight: 800; }
114
+ .codex-warning-text { margin-top: 3px; color: #d8d3bf; font-size: 11px; line-height: 1.4; overflow-wrap: anywhere; }
108
115
  .codex-status-card { margin: 10px 0 14px; border: 1px solid rgba(100,210,255,.2); border-radius: 10px; background: rgba(28,28,30,.72); padding: 11px; color: #f5f5f7; }
109
116
  .codex-status-title { margin-bottom: 9px; color: #64d2ff; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
110
117
  .codex-status-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
@@ -173,8 +180,8 @@
173
180
  .codex-select-control:focus-within .codex-select-label { background: rgba(255,255,255,.14); border-color: rgba(0,122,255,.45); }
174
181
  .codex-select { appearance: none; position: absolute; inset: 0; width: 100%; min-width: 0; height: 100%; opacity: 0; cursor: pointer; }
175
182
  .codex-select option { background: #1c1c1e; color: #fff; }
176
- #codex-model-panel, #codex-resume-panel, #codex-fork-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; }
177
- #codex-model-panel.active, #codex-resume-panel.active, #codex-fork-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; }
178
185
  #codex-model-panel { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); max-height: min(300px, 42dvh); }
179
186
  #codex-model-panel.active { display: grid; }
180
187
  .codex-picker-column { min-width: 0; overflow-y: auto; }
@@ -184,6 +191,24 @@
184
191
  #codex-state-bar { display: flex; align-items: center; gap: 7px; min-height: 24px; margin-top: 8px; color: var(--text-dim); font-size: 11px; overflow-x: auto; scrollbar-width: none; white-space: nowrap; }
185
192
  #codex-state-bar::-webkit-scrollbar { display: none; }
186
193
  #codex-resume-panel, #codex-fork-panel { max-height: min(280px, 38dvh); overflow-y: auto; }
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: 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; }
197
+ .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
+ .codex-skill-item.selected { background: rgba(0,122,255,.18); }
199
+ .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; }
201
+ .codex-skill-description { display: block; margin-top: 4px; color: #a9a9b0; font-size: 11px; line-height: 1.35; white-space: normal; overflow-wrap: anywhere; }
202
+ .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
+ .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
+ .codex-prompt-item:last-of-type { border-bottom: 0; }
205
+ .codex-prompt-text { display: -webkit-box; width: 100%; min-width: 0; max-height: calc(1.42em * 4); overflow: hidden; padding: 0; border: 0; background: transparent; color: #f5f5f7; font: 12px/1.42 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; text-align: left; white-space: pre-wrap; overflow-wrap: anywhere; -webkit-box-orient: vertical; -webkit-line-clamp: 4; cursor: pointer; }
206
+ .codex-prompt-text.expanded { display: block; max-height: none; -webkit-line-clamp: initial; }
207
+ .codex-prompt-actions { display: flex; min-width: 64px; flex-direction: column; align-items: flex-end; gap: 6px; }
208
+ .codex-prompt-time { color: #77777e; font-size: 10px; font-variant-numeric: tabular-nums; white-space: nowrap; }
209
+ .codex-prompt-copy { min-width: 54px; min-height: 28px; padding: 0 9px; }
210
+ .codex-prompt-footer { padding: 9px 11px; text-align: center; }
211
+ .codex-prompt-footer .small-btn { min-width: 112px; }
187
212
  .claude-message { max-width: 92%; margin: 0 0 10px 0; padding: 10px 12px; border-radius: 8px; overflow-wrap: anywhere; line-height: 1.45; font-size: 14px; }
188
213
  .claude-message-block { max-width: 100%; margin: 0 0 12px; }
189
214
  .claude-message-block > .claude-message { margin-bottom: 0; }
@@ -191,6 +216,7 @@
191
216
  .claude-message-block.user > .claude-message { max-width: 100%; }
192
217
  .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; }
193
218
  .claude-message-block.user .claude-message-time { margin-left: auto; }
219
+ .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; }
194
220
  .claude-message-attachments { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
195
221
  .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; }
196
222
  .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.31",
3
+ "version": "1.0.33",
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"