glad-web 1.0.46 → 2.0.2
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/README.md +4 -192
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/bin/glad.cjs +56 -0
- package/package.json +19 -61
- package/README.zh-CN.md +0 -198
- package/assets/logo.svg +0 -43
- package/bin/cli.js +0 -65
- package/lib/ai-tools/demo/enhanced-demo.js +0 -625
- package/lib/ai-tools/demo/index.js +0 -24
- package/lib/ai-tools/demo/responses.js +0 -88
- package/lib/ai-tools/detector.js +0 -76
- package/lib/ai-tools/registry.js +0 -300
- package/lib/claude/cli-usage.js +0 -95
- package/lib/claude/config.js +0 -82
- package/lib/claude/structured-session.js +0 -884
- package/lib/claude/transcript-repository.js +0 -216
- package/lib/codex/image-store.js +0 -174
- package/lib/codex/structured-session.js +0 -1590
- package/lib/commands/config.js +0 -78
- package/lib/commands/tools.js +0 -128
- package/lib/commands/web.js +0 -605
- package/lib/config/constants.js +0 -17
- package/lib/config/manager.js +0 -108
- package/lib/git/service.js +0 -83
- package/lib/notifications/message-formatter.js +0 -94
- package/lib/notifications/notification-service.js +0 -143
- package/lib/notifications/serverchan-client.js +0 -58
- package/lib/notifications/serverchan-settings-store.js +0 -115
- package/lib/schedule/job-runner.js +0 -162
- package/lib/schedule/job-store.js +0 -167
- package/lib/schedule/key-sequences.js +0 -49
- package/lib/schedule/scheduler-service.js +0 -39
- package/lib/server/routes/notifications.js +0 -52
- package/lib/server/routes/providers.js +0 -114
- package/lib/server/routes/schedules.js +0 -54
- package/lib/server/routes/skillhub.js +0 -104
- package/lib/server/routes/usage.js +0 -23
- package/lib/server/routes/workspace.js +0 -77
- package/lib/session/buffer.js +0 -102
- package/lib/session/file-attachment-store.js +0 -168
- package/lib/session/pty-manager.js +0 -255
- package/lib/session/rendered-history.js +0 -225
- package/lib/session/session-manager.js +0 -1032
- package/lib/session/text-history.js +0 -274
- package/lib/skillhub/client.js +0 -121
- package/lib/skillhub/settings-store.js +0 -168
- package/lib/skillhub/skill-installer.js +0 -320
- package/lib/usage/ccusage-runner.js +0 -128
- package/lib/usage/source-catalog.js +0 -26
- package/lib/usage/usage-service.js +0 -226
- package/lib/utils/logger.js +0 -74
- package/lib/utils/pid.js +0 -67
- package/lib/utils/validation.js +0 -53
- package/lib/web/bootstrap.js +0 -34
- package/lib/web/claude.js +0 -1150
- package/lib/web/codex.js +0 -1045
- package/lib/web/composer.js +0 -493
- package/lib/web/core.js +0 -385
- package/lib/web/git.js +0 -535
- package/lib/web/gitgraph.js +0 -293
- package/lib/web/index.html +0 -547
- package/lib/web/layout.js +0 -69
- package/lib/web/notifications.js +0 -164
- package/lib/web/schedules.js +0 -245
- package/lib/web/session.js +0 -361
- package/lib/web/shell.js +0 -74
- package/lib/web/skillhub.js +0 -197
- package/lib/web/styles.css +0 -932
- package/lib/web/terminal-scroll.js +0 -81
- package/lib/web/theme.js +0 -60
- package/lib/web/timed-inputs.js +0 -216
- package/lib/web/usage.js +0 -323
- package/lib/workspace/service.js +0 -77
- package/scripts/check-syntax.js +0 -26
package/lib/web/codex.js
DELETED
|
@@ -1,1045 +0,0 @@
|
|
|
1
|
-
function codexText(text) { return escapeHtml(text || '').replace(/\n/g, '<br>'); }
|
|
2
|
-
function codexReadyForInput() {
|
|
3
|
-
return codexState.status === 'idle'
|
|
4
|
-
&& !codexState.aborting && !codexState.resuming && !codexResumeInFlight;
|
|
5
|
-
}
|
|
6
|
-
function codexJson(value) {
|
|
7
|
-
if (typeof value === 'string') return value;
|
|
8
|
-
try { return JSON.stringify(value || {}, null, 2); } catch (_) { return String(value || ''); }
|
|
9
|
-
}
|
|
10
|
-
function codexToolStatus(item) {
|
|
11
|
-
const status = item.toolStatus || 'running';
|
|
12
|
-
if (status === 'inProgress') return 'running';
|
|
13
|
-
if (status === 'failed' || status === 'declined') return status;
|
|
14
|
-
if (status === 'completed') return item.exitCode && item.exitCode !== 0 ? 'failed' : 'completed';
|
|
15
|
-
return status;
|
|
16
|
-
}
|
|
17
|
-
function codexMessageNeedsDetail(item) {
|
|
18
|
-
if (!item?.hasDetail) return false;
|
|
19
|
-
const loadedRevision = Number(codexDetailRevisions.get(String(item.id)) || 0);
|
|
20
|
-
return !item.detailLoaded || loadedRevision < Number(item.detailRevision || 0);
|
|
21
|
-
}
|
|
22
|
-
function formatCodexDuration(durationMs) {
|
|
23
|
-
const value = Number(durationMs || 0);
|
|
24
|
-
if (!(value > 0)) return '';
|
|
25
|
-
if (value < 1000) return `${Math.round(value)}ms`;
|
|
26
|
-
const seconds = value / 1000;
|
|
27
|
-
if (seconds < 10) return `${seconds.toFixed(1).replace(/\.0$/, '')}s`;
|
|
28
|
-
if (seconds < 60) return `${Math.round(seconds)}s`;
|
|
29
|
-
const minutes = Math.floor(seconds / 60);
|
|
30
|
-
return `${minutes}m ${Math.round(seconds % 60)}s`;
|
|
31
|
-
}
|
|
32
|
-
function renderCodexDiff(diff) {
|
|
33
|
-
return `<div class="codex-diff">${String(diff || '').split('\n').map(line => {
|
|
34
|
-
const type = line.startsWith('+++') || line.startsWith('---') ? 'hunk' : line.startsWith('+') ? 'add' : line.startsWith('-') ? 'del' : line.startsWith('@@') ? 'hunk' : '';
|
|
35
|
-
return `<div class="codex-diff-line ${type}">${escapeHtml(line || ' ')}</div>`;
|
|
36
|
-
}).join('')}</div>`;
|
|
37
|
-
}
|
|
38
|
-
function normalizeCodexChanges(changes) {
|
|
39
|
-
if (Array.isArray(changes)) return changes.filter(Boolean).map(change => [change.path || 'File', change]);
|
|
40
|
-
if (changes && typeof changes === 'object') return Object.entries(changes);
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
function codexChangeDiff(change = {}) {
|
|
44
|
-
if (typeof change.diff === 'string') return change.diff;
|
|
45
|
-
if (typeof change.unified_diff === 'string') return change.unified_diff;
|
|
46
|
-
const kind = change.kind && typeof change.kind === 'object' ? change.kind : {};
|
|
47
|
-
const type = change.type || kind.type || '';
|
|
48
|
-
const oldText = change.modify?.old_content ?? change.old_content ?? change.oldContent ?? (type === 'delete' ? change.content : change.delete?.content) ?? '';
|
|
49
|
-
const newText = change.modify?.new_content ?? change.new_content ?? change.newContent ?? (type === 'add' ? change.content : change.add?.content) ?? '';
|
|
50
|
-
if (!oldText && !newText) return '';
|
|
51
|
-
return `${String(oldText).split('\n').map(line => `-${line}`).join('\n')}\n${String(newText).split('\n').map(line => `+${line}`).join('\n')}`;
|
|
52
|
-
}
|
|
53
|
-
function renderCodexPatch(item) {
|
|
54
|
-
const entries = normalizeCodexChanges(item.changes || item.input?.changes);
|
|
55
|
-
if (!entries.length) return `<div class="codex-tool-body"><pre class="codex-tool-code">${escapeHtml(codexJson(item.input))}</pre></div>`;
|
|
56
|
-
return entries.map(([path, change]) => {
|
|
57
|
-
const kind = change?.type || change?.kind?.type || 'edit';
|
|
58
|
-
const move = change?.move_path || change?.kind?.move_path;
|
|
59
|
-
const diff = codexChangeDiff(change);
|
|
60
|
-
return `<details class="codex-patch-file"><summary><span class="path">${escapeHtml(path)}${move ? ` → ${escapeHtml(move)}` : ''}</span><span class="codex-patch-kind">${escapeHtml(kind)}</span></summary>${diff ? renderCodexDiff(diff) : ''}</details>`;
|
|
61
|
-
}).join('');
|
|
62
|
-
}
|
|
63
|
-
function renderCodexPermission(request, inline = false) {
|
|
64
|
-
if (!request) return '';
|
|
65
|
-
const pending = request.status === 'pending';
|
|
66
|
-
const permissionId = escapeHtml(String(request.id || ''));
|
|
67
|
-
const labels = { approved: 'Allowed once', approved_for_session: 'Allowed for session', denied: 'Denied', abort: 'Stopped' };
|
|
68
|
-
const detail = request.reason || (request.input && Object.keys(request.input).length ? codexJson(request.input) : '');
|
|
69
|
-
const content = `${inline ? `<div class="codex-inline-permission-title">${escapeHtml(request.title || 'Permission required')}</div>` : ''}${detail ? `<div>${codexText(detail)}</div>` : ''}${pending ? `<div class="claude-permission-actions"><button class="small-btn primary" onclick="respondCodexPermission('${escapeHtml(request.id)}', 'approved')">Yes</button><button class="small-btn" onclick="respondCodexPermission('${escapeHtml(request.id)}', 'approved_for_session')">Yes, for session</button><button class="small-btn danger" onclick="respondCodexPermission('${escapeHtml(request.id)}', 'abort')">Stop and explain</button></div>` : `<div class="codex-permission-result">${escapeHtml(labels[request.decision] || request.status)}</div>`}`;
|
|
70
|
-
if (inline) return `<div class="codex-inline-permission" data-codex-permission-id="${permissionId}">${content}</div>`;
|
|
71
|
-
return `<div class="claude-tool claude-permission" data-codex-permission-id="${permissionId}"><div class="claude-tool-header"><strong>${escapeHtml(request.title || 'Permission required')}</strong></div><div class="claude-tool-body">${content}</div></div>`;
|
|
72
|
-
}
|
|
73
|
-
function formatCodexReset(timestamp) {
|
|
74
|
-
const value = Number(timestamp || 0);
|
|
75
|
-
return value ? new Date(value * 1000).toLocaleString() : '';
|
|
76
|
-
}
|
|
77
|
-
function formatCodexTokens(value) {
|
|
78
|
-
const count = Number(value || 0);
|
|
79
|
-
return count >= 1000000 ? `${(count / 1000000).toFixed(1)}M`
|
|
80
|
-
: count >= 1000 ? `${Math.round(count / 1000)}K` : String(count);
|
|
81
|
-
}
|
|
82
|
-
function codexStatusItem(label, value) {
|
|
83
|
-
if (value == null || value === '') return '';
|
|
84
|
-
return `<div class="codex-status-item"><div class="codex-status-label">${escapeHtml(label)}</div><div class="codex-status-value">${escapeHtml(value)}</div></div>`;
|
|
85
|
-
}
|
|
86
|
-
function renderCodexStatus(item) {
|
|
87
|
-
const account = item.account || {};
|
|
88
|
-
const limit = item.rateLimits || {};
|
|
89
|
-
const primary = limit.primary;
|
|
90
|
-
const secondary = limit.secondary;
|
|
91
|
-
const context = item.context;
|
|
92
|
-
const accountLabel = account.type === 'chatgpt'
|
|
93
|
-
? [account.email, account.planType].filter(Boolean).join(' · ')
|
|
94
|
-
: account.type === 'apiKey' ? 'API key' : account.type || 'Not signed in';
|
|
95
|
-
const fiveHour = primary ? `${Math.max(0, 100 - Number(primary.usedPercent || 0))}% left${primary.resetsAt ? ` · resets ${formatCodexReset(primary.resetsAt)}` : ''}` : '';
|
|
96
|
-
const weekly = secondary ? `${Math.max(0, 100 - Number(secondary.usedPercent || 0))}% left${secondary.resetsAt ? ` · resets ${formatCodexReset(secondary.resetsAt)}` : ''}` : '';
|
|
97
|
-
const contextLabel = context ? `${context.remainingPercent}% left${context.contextWindow ? ` · ${formatCodexTokens(context.remainingTokens)} / ${formatCodexTokens(context.contextWindow)}` : ''}` : 'Available after the first usage update';
|
|
98
|
-
return `<div class="codex-status-card"><div class="codex-status-title">${escapeHtml(item.title || 'Codex status')}</div><div class="codex-status-grid">
|
|
99
|
-
${codexStatusItem('Account', accountLabel)}
|
|
100
|
-
${codexStatusItem('Model', [item.model, item.effort].filter(Boolean).join(' · '))}
|
|
101
|
-
${codexStatusItem('5h limit', fiveHour)}
|
|
102
|
-
${codexStatusItem('Weekly limit', weekly)}
|
|
103
|
-
${codexStatusItem('Context', contextLabel)}
|
|
104
|
-
</div></div>`;
|
|
105
|
-
}
|
|
106
|
-
function renderCodexCompaction(item) {
|
|
107
|
-
const running = item.compactionStatus === 'running';
|
|
108
|
-
const timestamp = Number(item.completedAtMs || item.updatedAt || item.createdAt || 0);
|
|
109
|
-
const date = timestamp > 0 ? new Date(timestamp) : null;
|
|
110
|
-
const time = date && !Number.isNaN(date.getTime())
|
|
111
|
-
? date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
|
|
112
|
-
const detail = running ? 'Compacting context…' : ['Completed', time].filter(Boolean).join(' · ');
|
|
113
|
-
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>`;
|
|
114
|
-
}
|
|
115
|
-
function renderCodexWarning(item) {
|
|
116
|
-
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>`;
|
|
117
|
-
}
|
|
118
|
-
function renderCodexTool(item, permission = null) {
|
|
119
|
-
const status = codexToolStatus(item);
|
|
120
|
-
const isError = status === 'failed' || Boolean(item.error) || Boolean(item.hasError) || (item.exitCode != null && item.exitCode !== 0);
|
|
121
|
-
const runningClass = status === 'running' ? ' running' : '';
|
|
122
|
-
if (item.name === 'CodexPatch') {
|
|
123
|
-
return `<div class="codex-tool${isError ? ' error' : ''}">${renderCodexPatch(item)}${permission ? renderCodexPermission(permission, true) : ''}</div>`;
|
|
124
|
-
}
|
|
125
|
-
const command = item.name === 'CodexBash' ? item.command : item.title || item.tool || '';
|
|
126
|
-
const icon = item.name === 'CodexBash' ? '>_' : item.name === 'McpTool' ? 'MCP' : item.name === 'Agent' ? 'A' : '•';
|
|
127
|
-
const title = item.name === 'CodexBash' ? 'Command' : item.title || item.name || 'Tool';
|
|
128
|
-
const hasInput = item.input && typeof item.input === 'object' && Object.keys(item.input).length > 0;
|
|
129
|
-
const result = item.result || ((item.name === 'McpTool' || item.name === 'Agent') && hasInput ? codexJson(item.input) : '');
|
|
130
|
-
const duration = status === 'running' ? '' : formatCodexDuration(item.durationMs);
|
|
131
|
-
return `<details class="codex-tool${isError ? ' error' : ''}" data-codex-key="tool-${escapeHtml(item.id || item.providerId || '')}"><summary class="codex-tool-header"><span class="codex-tool-icon">${escapeHtml(icon)}</span><span class="codex-tool-title">${escapeHtml(title)}</span>${command && command !== title ? `<span class="codex-tool-command">${escapeHtml(command)}</span>` : '<span class="codex-tool-command"></span>'}${duration ? `<span class="codex-tool-duration">${escapeHtml(duration)}</span>` : ''}<span class="codex-tool-state${runningClass}">${escapeHtml(status === 'completed' ? '' : status)}</span></summary>${result ? `<div class="codex-tool-body"><pre class="codex-tool-code">${escapeHtml(result)}</pre></div>` : ''}${permission ? renderCodexPermission(permission, true) : ''}</details>`;
|
|
132
|
-
}
|
|
133
|
-
function renderCodexToolGroup(items, permissionById, usedPermissions, turnEnd = null) {
|
|
134
|
-
const running = items.some(item => codexToolStatus(item) === 'running');
|
|
135
|
-
const failed = items.some(item => codexToolStatus(item) === 'failed' || item.error);
|
|
136
|
-
const startedAt = Math.min(...items.map(item => Number(item.startedAtMs || item.createdAt || Date.now())));
|
|
137
|
-
const itemCompletedAt = Math.max(...items.map(item => Number(item.completedAtMs || 0)));
|
|
138
|
-
const storedDuration = Number(turnEnd?.durationMs || 0);
|
|
139
|
-
const completedAt = Number(turnEnd?.createdAt || 0);
|
|
140
|
-
const durationMs = itemCompletedAt >= startedAt ? itemCompletedAt - startedAt : storedDuration > 0 ? storedDuration
|
|
141
|
-
: (!running && completedAt >= startedAt ? completedAt - startedAt : 0);
|
|
142
|
-
const duration = formatCodexDuration(durationMs);
|
|
143
|
-
const tools = items.map(item => {
|
|
144
|
-
const permission = permissionById.get(String(item.providerId || ''));
|
|
145
|
-
if (permission) usedPermissions.add(permission.id);
|
|
146
|
-
return renderCodexTool(item, permission);
|
|
147
|
-
}).join('');
|
|
148
|
-
const label = running ? 'Working' : failed ? 'Work finished with errors' : duration ? `Worked for ${duration}` : 'Worked';
|
|
149
|
-
const key = items.map(item => item.id || item.providerId || '').join('-');
|
|
150
|
-
const allDetailIds = items.filter(item => item.hasDetail).map(item => item.id).filter(Boolean);
|
|
151
|
-
const detailIds = items.filter(codexMessageNeedsDetail).map(item => item.id).filter(Boolean);
|
|
152
|
-
const lazy = detailIds.length
|
|
153
|
-
? '<div class="codex-lazy-detail">Open to load tool details…</div>' : '';
|
|
154
|
-
return `<details class="codex-work-group" data-codex-key="group-${escapeHtml(key)}"${allDetailIds.length ? ` data-codex-detail-ids="${escapeHtml(allDetailIds.join(','))}"` : ''}><summary>${label} · ${items.length} ${items.length === 1 ? 'tool' : 'tools'}</summary><div class="codex-work-group-body">${tools}${lazy}</div></details>`;
|
|
155
|
-
}
|
|
156
|
-
function isCodexSubagentItem(item) {
|
|
157
|
-
return Boolean(item?.threadId && codexState.threadId && item.threadId !== codexState.threadId);
|
|
158
|
-
}
|
|
159
|
-
function renderCodexSubagentGroup(threadId, items, permissionById, usedPermissions) {
|
|
160
|
-
const turnEnds = items.filter(item => item.kind === 'turn-end');
|
|
161
|
-
const content = items.filter(item => !['reasoning', 'turn-start', 'turn-end'].includes(item.kind));
|
|
162
|
-
const tools = content.filter(item => item.kind === 'tool');
|
|
163
|
-
const messages = content.filter(item => item.kind === 'assistant' || item.kind === 'user');
|
|
164
|
-
const running = tools.some(item => codexToolStatus(item) === 'running')
|
|
165
|
-
|| items.filter(item => item.kind === 'turn-start').length > turnEnds.length;
|
|
166
|
-
const startedAt = Math.min(...items.map(item => Number(item.startedAtMs || item.createdAt || Infinity)));
|
|
167
|
-
const completedAt = Math.max(...turnEnds.map(item => Number(item.createdAt || 0)),
|
|
168
|
-
...content.map(item => Number(item.completedAtMs || item.updatedAt || 0)));
|
|
169
|
-
const duration = formatCodexDuration(Number.isFinite(startedAt) && completedAt >= startedAt
|
|
170
|
-
? completedAt - startedAt : 0);
|
|
171
|
-
const counts = [];
|
|
172
|
-
if (tools.length) counts.push(`${tools.length} ${tools.length === 1 ? 'tool' : 'tools'}`);
|
|
173
|
-
if (messages.length) counts.push(`${messages.length} ${messages.length === 1 ? 'message' : 'messages'}`);
|
|
174
|
-
const label = running ? 'Subagent working' : duration ? `Subagent worked for ${duration}` : 'Subagent worked';
|
|
175
|
-
const needsDetail = items.some(codexMessageNeedsDetail);
|
|
176
|
-
const deferContent = needsDetail && !items.some(item => item.detailLoaded);
|
|
177
|
-
const body = [];
|
|
178
|
-
if (needsDetail) {
|
|
179
|
-
body.push(`<div class="codex-lazy-detail">${deferContent ? 'Open to load subagent details…' : 'Updating subagent details…'}</div>`);
|
|
180
|
-
}
|
|
181
|
-
for (let i = 0; !deferContent && i < content.length;) {
|
|
182
|
-
const item = content[i];
|
|
183
|
-
if (item.kind === 'tool') {
|
|
184
|
-
const group = [];
|
|
185
|
-
const turnId = item.turnId;
|
|
186
|
-
while (i < content.length && content[i].kind === 'tool' && content[i].turnId === turnId) group.push(content[i++]);
|
|
187
|
-
const turnEnd = turnEnds.find(candidate => candidate.turnId === turnId) || null;
|
|
188
|
-
body.push(renderCodexToolGroup(group, permissionById, usedPermissions, turnEnd));
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
if (item.kind === 'assistant' || item.kind === 'user') {
|
|
192
|
-
if (item.text) body.push(`<div class="codex-subagent-message${item.kind === 'user' ? ' task' : ''}">${renderMarkdown(item.text)}</div>`);
|
|
193
|
-
} else if (item.text) {
|
|
194
|
-
body.push(`<div class="codex-subagent-message">${codexText(item.text)}</div>`);
|
|
195
|
-
}
|
|
196
|
-
i += 1;
|
|
197
|
-
}
|
|
198
|
-
const suffix = counts.length ? ` · ${counts.join(' · ')}` : '';
|
|
199
|
-
return `<details class="codex-work-group codex-subagent-group" data-codex-key="subagent-${escapeHtml(threadId)}" data-codex-thread-id="${escapeHtml(threadId)}"><summary>${escapeHtml(label + suffix)}</summary><div class="codex-work-group-body">${body.join('')}</div></details>`;
|
|
200
|
-
}
|
|
201
|
-
function renderCodexMessageTime(item, finalOnly = false) {
|
|
202
|
-
if (!item || (finalOnly && item.streaming)) return '';
|
|
203
|
-
const timestamp = Number(finalOnly ? (item.completedAtMs || item.updatedAt || item.createdAt) : item.createdAt);
|
|
204
|
-
if (!Number.isFinite(timestamp) || timestamp <= 0) return '';
|
|
205
|
-
const date = new Date(timestamp);
|
|
206
|
-
if (Number.isNaN(date.getTime())) return '';
|
|
207
|
-
const label = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
208
|
-
return `<time class="codex-message-time" datetime="${escapeHtml(date.toISOString())}" title="${escapeHtml(date.toLocaleString())}">${escapeHtml(label)}</time>`;
|
|
209
|
-
}
|
|
210
|
-
function renderCodexContextMeter(context) {
|
|
211
|
-
const total = Number(context?.contextWindow || 0);
|
|
212
|
-
const remaining = Number(context?.remainingTokens || 0);
|
|
213
|
-
if (!(total > 0) || !Number.isFinite(remaining)) return '';
|
|
214
|
-
const reportedPercent = Number(context.remainingPercent ?? Math.round(remaining / total * 100));
|
|
215
|
-
const percent = Math.max(0, Math.min(100, Number.isFinite(reportedPercent) ? reportedPercent : 0));
|
|
216
|
-
const level = percent <= 20 ? ' danger' : percent <= 40 ? ' warn' : '';
|
|
217
|
-
const label = `${formatCodexTokens(remaining)} / ${formatCodexTokens(total)}(${Math.round(percent)}%)`;
|
|
218
|
-
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>`;
|
|
219
|
-
}
|
|
220
|
-
function renderCodexMessageSkills(item) {
|
|
221
|
-
const skills = Array.isArray(item?.skills) ? item.skills : [];
|
|
222
|
-
if (!skills.length) return '';
|
|
223
|
-
return `<div class="codex-message-skills">${skills.map(skill => `<span class="codex-message-skill" title="${escapeHtml(skill.path || skill.name || '')}">Skill · ${escapeHtml(skill.name || 'unknown')}</span>`).join('')}</div>`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function renderComposerSkillPrefix() {
|
|
227
|
-
const container = document.getElementById('composer-skill-prefix');
|
|
228
|
-
if (!container) return;
|
|
229
|
-
const visible = selectedCodexSkill && codexReadyForInput();
|
|
230
|
-
const wasVisible = container.classList.contains('active');
|
|
231
|
-
container.classList.toggle('active', Boolean(visible));
|
|
232
|
-
container.innerHTML = visible
|
|
233
|
-
? `<div class="codex-skill-bubble" role="status" aria-label="Selected skill: ${escapeHtml(selectedCodexSkill.name)}"><span class="codex-skill-bubble-name">Skill · ${escapeHtml(selectedCodexSkill.name)}</span><button type="button" class="codex-skill-bubble-close" onclick="clearCodexSkillSelection()" title="Remove selected skill" aria-label="Remove selected skill">×</button></div>`
|
|
234
|
-
: '';
|
|
235
|
-
if (wasVisible !== Boolean(visible)) updateTerminalControlsHeight();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function renderCodexMessageAttachments(item) {
|
|
239
|
-
if (!Array.isArray(item.attachments) || !item.attachments.length) return '';
|
|
240
|
-
return `<div class="claude-message-attachments">${item.attachments.map(attachment => `<span class="claude-message-attachment" title="${escapeHtml(attachment.name || 'Attachment')}"><svg class="message-attachment-icon action-icon" aria-hidden="true"><use href="#icon-${attachment.kind === 'file' ? 'file' : 'image'}"></use></svg>${escapeHtml(attachment.name || 'Attachment')}</span>`).join('')}</div>`;
|
|
241
|
-
}
|
|
242
|
-
function renderCodexMessageMeta(item, finalOnly = false, context = null) {
|
|
243
|
-
const time = renderCodexMessageTime(item, finalOnly);
|
|
244
|
-
const meter = renderCodexContextMeter(context);
|
|
245
|
-
return time || meter ? `<div class="codex-message-meta">${time}${meter}</div>` : '';
|
|
246
|
-
}
|
|
247
|
-
function syncCodexDom(current, next) {
|
|
248
|
-
if (!current || !next) return;
|
|
249
|
-
if (current.nodeType !== next.nodeType || current.nodeName !== next.nodeName) {
|
|
250
|
-
current.replaceWith(next.cloneNode(true));
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
if (current.nodeType === Node.TEXT_NODE) {
|
|
254
|
-
if (current.nodeValue !== next.nodeValue) current.nodeValue = next.nodeValue;
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
const currentKey = current.getAttribute?.('data-codex-key');
|
|
258
|
-
const nextKey = next.getAttribute?.('data-codex-key');
|
|
259
|
-
if (currentKey && nextKey && currentKey !== nextKey) {
|
|
260
|
-
current.replaceWith(next.cloneNode(true));
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const preserveOpen = current.tagName === 'DETAILS' && currentKey === nextKey;
|
|
264
|
-
const wasOpen = preserveOpen ? current.open : false;
|
|
265
|
-
for (const attribute of Array.from(current.attributes || [])) {
|
|
266
|
-
if (!next.hasAttribute(attribute.name) && !(preserveOpen && attribute.name === 'open')) current.removeAttribute(attribute.name);
|
|
267
|
-
}
|
|
268
|
-
for (const attribute of Array.from(next.attributes || [])) {
|
|
269
|
-
if (!(preserveOpen && attribute.name === 'open') && current.getAttribute(attribute.name) !== attribute.value) {
|
|
270
|
-
current.setAttribute(attribute.name, attribute.value);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
if (preserveOpen) current.open = wasOpen;
|
|
274
|
-
const currentChildren = Array.from(current.childNodes);
|
|
275
|
-
const nextChildren = Array.from(next.childNodes);
|
|
276
|
-
const shared = Math.min(currentChildren.length, nextChildren.length);
|
|
277
|
-
for (let i = 0; i < shared; i++) syncCodexDom(currentChildren[i], nextChildren[i]);
|
|
278
|
-
for (let i = current.childNodes.length - 1; i >= nextChildren.length; i--) current.childNodes[i].remove();
|
|
279
|
-
for (let i = shared; i < nextChildren.length; i++) current.appendChild(nextChildren[i].cloneNode(true));
|
|
280
|
-
}
|
|
281
|
-
function renderCodexChat() {
|
|
282
|
-
if (codexRenderFrame != null) return;
|
|
283
|
-
codexRenderFrame = requestAnimationFrame(() => {
|
|
284
|
-
codexRenderFrame = null;
|
|
285
|
-
commitCodexChatRender();
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
function commitCodexChatRender() {
|
|
289
|
-
const container = document.getElementById('codex-chat-container');
|
|
290
|
-
if (!container) return;
|
|
291
|
-
const wasEmpty = !container.firstElementChild;
|
|
292
|
-
const previousScrollTop = container.scrollTop;
|
|
293
|
-
const distanceFromBottom = container.scrollHeight - container.clientHeight - previousScrollTop;
|
|
294
|
-
const shouldStickToBottom = wasEmpty || distanceFromBottom <= 64;
|
|
295
|
-
const parts = [];
|
|
296
|
-
const permissionById = new Map(codexPendingPermissions.map(item => [String(item.id), item]));
|
|
297
|
-
const usedPermissions = new Set();
|
|
298
|
-
const turnEndById = new Map(codexMessages.filter(item => item.kind === 'turn-end' && item.turnId)
|
|
299
|
-
.map(item => [String(item.turnId), item]));
|
|
300
|
-
const lastAssistantByTurn = new Map();
|
|
301
|
-
for (const item of codexMessages) {
|
|
302
|
-
if (item.kind === 'assistant' && item.turnId && !isCodexSubagentItem(item)) {
|
|
303
|
-
lastAssistantByTurn.set(String(item.turnId), item.id);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
const subagentItems = new Map();
|
|
307
|
-
for (const item of codexMessages) {
|
|
308
|
-
if (!isCodexSubagentItem(item)) continue;
|
|
309
|
-
const threadId = String(item.threadId);
|
|
310
|
-
if (!subagentItems.has(threadId)) subagentItems.set(threadId, []);
|
|
311
|
-
subagentItems.get(threadId).push(item);
|
|
312
|
-
}
|
|
313
|
-
const renderedSubagents = new Set();
|
|
314
|
-
const visible = codexMessages.filter(item => !['reasoning', 'turn-start', 'turn-end'].includes(item.kind));
|
|
315
|
-
for (let i = 0; i < visible.length;) {
|
|
316
|
-
const item = visible[i];
|
|
317
|
-
if (isCodexSubagentItem(item)) {
|
|
318
|
-
const threadId = String(item.threadId);
|
|
319
|
-
if (!renderedSubagents.has(threadId)) {
|
|
320
|
-
renderedSubagents.add(threadId);
|
|
321
|
-
parts.push(renderCodexSubagentGroup(threadId, subagentItems.get(threadId) || [], permissionById, usedPermissions));
|
|
322
|
-
}
|
|
323
|
-
i += 1;
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
if (item.kind === 'tool') {
|
|
327
|
-
const tools = [];
|
|
328
|
-
const turnId = item.turnId;
|
|
329
|
-
while (i < visible.length && visible[i].kind === 'tool' && visible[i].turnId === turnId) tools.push(visible[i++]);
|
|
330
|
-
parts.push(renderCodexToolGroup(tools, permissionById, usedPermissions,
|
|
331
|
-
turnEndById.get(String(turnId || ''))));
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
if (item.kind === 'assistant') {
|
|
335
|
-
const turnId = String(item.turnId || '');
|
|
336
|
-
const turnEnd = turnEndById.get(turnId);
|
|
337
|
-
const context = lastAssistantByTurn.get(turnId) === item.id ? turnEnd?.context : null;
|
|
338
|
-
parts.push(`<div class="codex-message-block assistant" data-codex-key="message-${escapeHtml(item.id || '')}"><div class="codex-message assistant claude-md">${renderMarkdown(item.text || '')}</div>${renderCodexMessageMeta(item, true, context)}</div>`);
|
|
339
|
-
}
|
|
340
|
-
else if (item.kind === 'user') parts.push(`<div class="codex-message-block user" data-codex-key="message-${escapeHtml(item.id || '')}">${renderCodexMessageSkills(item)}<div class="codex-message user claude-md">${renderMarkdown(item.text || '')}${renderCodexMessageAttachments(item)}</div>${renderCodexMessageMeta(item)}</div>`);
|
|
341
|
-
else if (item.kind === 'compaction') parts.push(renderCodexCompaction(item));
|
|
342
|
-
else if (item.kind === 'status') parts.push(renderCodexStatus(item));
|
|
343
|
-
else if (item.kind === 'event' && item.level === 'warning') parts.push(renderCodexWarning(item));
|
|
344
|
-
else parts.push(`<div class="codex-message ${escapeHtml(item.level === 'error' ? 'event error' : item.kind || 'event')}">${codexText(item.text)}</div>`);
|
|
345
|
-
i += 1;
|
|
346
|
-
}
|
|
347
|
-
for (const request of codexPendingPermissions) if (!usedPermissions.has(request.id)) parts.push(renderCodexPermission(request));
|
|
348
|
-
const working = `<div class="codex-working-indicator" role="status" aria-label="Codex is working" title="Codex is working"${codexState.status === 'running' ? '' : ' style="display:none"'}></div>`;
|
|
349
|
-
const template = document.createElement('template');
|
|
350
|
-
template.innerHTML = `<div class="codex-conversation">${working}${parts.join('') || '<div class="codex-message event">Send a message to start Codex.</div>'}</div>`;
|
|
351
|
-
const next = template.content.firstElementChild;
|
|
352
|
-
const current = container.firstElementChild;
|
|
353
|
-
if (!current) container.appendChild(next);
|
|
354
|
-
else syncCodexDom(current, next);
|
|
355
|
-
renderComposerSkillPrefix();
|
|
356
|
-
container.scrollTop = shouldStickToBottom ? container.scrollHeight : previousScrollTop;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function installCodexLazyDetailHandler() {
|
|
360
|
-
const container = document.getElementById('codex-chat-container');
|
|
361
|
-
if (!container || container.dataset.lazyDetailHandler === 'true') return;
|
|
362
|
-
container.dataset.lazyDetailHandler = 'true';
|
|
363
|
-
container.addEventListener('toggle', handleCodexLazyToggle, true);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function handleCodexLazyToggle(event) {
|
|
367
|
-
const group = event.target;
|
|
368
|
-
if (!(group instanceof HTMLDetailsElement) || !group.open || !group.classList.contains('codex-work-group')) return;
|
|
369
|
-
const threadId = group.dataset.codexThreadId || '';
|
|
370
|
-
const ids = String(group.dataset.codexDetailIds || '').split(',').filter(Boolean)
|
|
371
|
-
.filter(id => codexMessageNeedsDetail(codexMessages.find(item => item.id === id)));
|
|
372
|
-
if (threadId && codexMessages.some(item => item.threadId === threadId && codexMessageNeedsDetail(item))) {
|
|
373
|
-
void requestCodexDetails({ threadId });
|
|
374
|
-
}
|
|
375
|
-
else if (ids.length) void requestCodexDetails({ ids });
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function requestCodexDetails({ ids = [], threadId = '' } = {}) {
|
|
379
|
-
if (currentSocket?.readyState !== 1) return Promise.resolve(false);
|
|
380
|
-
const normalizedIds = Array.from(new Set(ids.map(value => String(value || '')).filter(Boolean))).sort();
|
|
381
|
-
const key = threadId ? `thread:${threadId}` : `ids:${normalizedIds.join(',')}`;
|
|
382
|
-
for (const pending of codexDetailRequests.values()) {
|
|
383
|
-
if (pending.key === key) return pending.promise;
|
|
384
|
-
}
|
|
385
|
-
const requestId = `detail-${++codexDetailRequestSeq}`;
|
|
386
|
-
let resolveRequest;
|
|
387
|
-
const promise = new Promise(resolve => { resolveRequest = resolve; });
|
|
388
|
-
const timer = setTimeout(() => {
|
|
389
|
-
const pending = codexDetailRequests.get(requestId);
|
|
390
|
-
if (!pending) return;
|
|
391
|
-
codexDetailRequests.delete(requestId);
|
|
392
|
-
pending.resolve(false);
|
|
393
|
-
}, 15000);
|
|
394
|
-
codexDetailRequests.set(requestId, { key, promise, resolve: resolveRequest, timer });
|
|
395
|
-
currentSocket.send(JSON.stringify({
|
|
396
|
-
type: 'codex-detail-request',
|
|
397
|
-
requestId,
|
|
398
|
-
...(threadId ? { threadId } : { ids: normalizedIds })
|
|
399
|
-
}));
|
|
400
|
-
return promise;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function applyCodexDetailResponse(response) {
|
|
404
|
-
const pending = codexDetailRequests.get(response.requestId);
|
|
405
|
-
if (!pending) return;
|
|
406
|
-
const detail = response.detail || {};
|
|
407
|
-
for (const message of detail.messages || []) {
|
|
408
|
-
const index = codexMessages.findIndex(item => item.id === message.id);
|
|
409
|
-
if (index >= 0) codexMessages[index] = { ...codexMessages[index], ...message, detailLoaded: true };
|
|
410
|
-
else codexMessages.push({ ...message, detailLoaded: true });
|
|
411
|
-
codexDetailRevisions.set(String(message.id), Number(message.updatedAt || message.createdAt || 0));
|
|
412
|
-
}
|
|
413
|
-
clearTimeout(pending.timer);
|
|
414
|
-
codexDetailRequests.delete(response.requestId);
|
|
415
|
-
pending.resolve(true);
|
|
416
|
-
renderCodexChat();
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function codexDetailIsOpen(message) {
|
|
420
|
-
if (!message?.id) return false;
|
|
421
|
-
if (isCodexSubagentItem(message)) {
|
|
422
|
-
return Array.from(document.querySelectorAll('.codex-subagent-group[open]'))
|
|
423
|
-
.some(group => group.dataset.codexKey === `subagent-${message.threadId}`);
|
|
424
|
-
}
|
|
425
|
-
return Array.from(document.querySelectorAll('.codex-work-group[open][data-codex-detail-ids]'))
|
|
426
|
-
.some(group => String(group.dataset.codexDetailIds || '').split(',').includes(String(message.id)));
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function scheduleCodexDetailRefresh(message) {
|
|
430
|
-
if (!message || !codexDetailIsOpen(message)) return;
|
|
431
|
-
if (!message.id || (!message.detailLoaded && !isCodexSubagentItem(message))) return;
|
|
432
|
-
codexDetailRefreshIds.add(String(message.id));
|
|
433
|
-
clearTimeout(codexDetailRefreshTimer);
|
|
434
|
-
codexDetailRefreshTimer = setTimeout(() => {
|
|
435
|
-
const ids = Array.from(codexDetailRefreshIds);
|
|
436
|
-
codexDetailRefreshIds.clear();
|
|
437
|
-
if (ids.length) void requestCodexDetails({ ids });
|
|
438
|
-
}, 300);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function applyCodexState(state = {}, options = {}) {
|
|
442
|
-
codexState = { ...codexState, ...state };
|
|
443
|
-
if (typeof syncComposerSendState === 'function') {
|
|
444
|
-
syncComposerSendState({ acknowledgeProviderState: Boolean(options.providerStateReceived) });
|
|
445
|
-
}
|
|
446
|
-
const permission = document.getElementById('codex-permission-select');
|
|
447
|
-
if (permission) {
|
|
448
|
-
const defaultOption = permission.querySelector('option[value="default"]');
|
|
449
|
-
const labels = { untrusted: 'Untrusted', 'on-request': 'On request', never: 'Never ask' };
|
|
450
|
-
if (defaultOption) defaultOption.textContent = codexState.effectivePermissionMode
|
|
451
|
-
? `Default (${labels[codexState.effectivePermissionMode] || codexState.effectivePermissionMode})`
|
|
452
|
-
: 'Default';
|
|
453
|
-
permission.value = codexState.permissionMode || 'default';
|
|
454
|
-
}
|
|
455
|
-
const sandbox = document.getElementById('codex-sandbox-select');
|
|
456
|
-
if (sandbox) {
|
|
457
|
-
const defaultOption = sandbox.querySelector('option[value="default"]');
|
|
458
|
-
const labels = { 'read-only': 'Read only', 'workspace-write': 'Workspace write', 'danger-full-access': 'Full access' };
|
|
459
|
-
if (defaultOption) defaultOption.textContent = codexState.effectiveSandboxMode
|
|
460
|
-
? `Default (${labels[codexState.effectiveSandboxMode] || codexState.effectiveSandboxMode})`
|
|
461
|
-
: 'Default';
|
|
462
|
-
sandbox.value = codexState.sandboxMode || 'default';
|
|
463
|
-
}
|
|
464
|
-
const modelButton = document.getElementById('codex-model-btn');
|
|
465
|
-
if (modelButton) setActionButtonLabel(modelButton, 'Model');
|
|
466
|
-
const abort = document.getElementById('codex-abort-btn');
|
|
467
|
-
if (abort) {
|
|
468
|
-
abort.disabled = !codexState.canAbort;
|
|
469
|
-
setActionButtonLabel(abort, codexState.aborting ? 'Aborting…' : 'Abort');
|
|
470
|
-
}
|
|
471
|
-
const compact = document.getElementById('codex-compact-btn');
|
|
472
|
-
if (compact) { compact.disabled = !codexState.canCompact; setActionButtonLabel(compact, codexState.compacting ? 'Compacting' : 'Compact'); }
|
|
473
|
-
const skills = document.getElementById('codex-skills-btn');
|
|
474
|
-
if (skills) { skills.disabled = !codexReadyForInput(); skills.classList.toggle('primary', Boolean(selectedCodexSkill)); }
|
|
475
|
-
const resume = document.getElementById('codex-resume-btn');
|
|
476
|
-
if (resume) resume.disabled = !codexReadyForInput();
|
|
477
|
-
const fork = document.getElementById('codex-fork-btn');
|
|
478
|
-
if (fork) fork.disabled = !codexReadyForInput();
|
|
479
|
-
renderCodexStateBar();
|
|
480
|
-
renderCodexModelPanel();
|
|
481
|
-
renderCodexChat();
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function renderCodexStateBar() {
|
|
485
|
-
const el = document.getElementById('codex-state-bar');
|
|
486
|
-
if (!el) return;
|
|
487
|
-
const pending = Number(codexState.pendingPermissionCount || codexPendingPermissions.filter(item => item.status === 'pending').length) || 0;
|
|
488
|
-
const subagents = Number(codexState.activeSubagentCount || 0) || 0;
|
|
489
|
-
const parts = [];
|
|
490
|
-
const attention = [];
|
|
491
|
-
if (codexState.aborting) parts.push('<span class="claude-state-pill warn">Stopping Codex…</span>');
|
|
492
|
-
else if (codexState.resuming || codexResumeInFlight) parts.push('<span class="claude-state-pill">Resuming conversation…</span>');
|
|
493
|
-
if (pending || codexState.status === 'waiting_approval') attention.push(`<button type="button" class="session-attention-pill approval codex-approval-jump" onclick="jumpToCodexApproval()" title="Jump to pending approval" aria-label="Jump to pending approval"><span aria-hidden="true">!</span>${pending || 1} approval${pending === 1 ? '' : 's'}<span aria-hidden="true">↓</span></button>`);
|
|
494
|
-
if (subagents) attention.push(`<button type="button" class="session-attention-pill subagent codex-subagent-jump" onclick="jumpToActiveCodexSubagent()" title="Jump to active subagent" aria-label="Jump to active subagent"><span aria-hidden="true">↳</span>${subagents} subagent${subagents === 1 ? '' : 's'} running</button>`);
|
|
495
|
-
el.innerHTML = parts.join('');
|
|
496
|
-
el.style.display = parts.length ? 'flex' : 'none';
|
|
497
|
-
renderSessionAttention(attention);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function jumpToActiveCodexSubagent(retry = true) {
|
|
501
|
-
const groups = Array.from(document.querySelectorAll('.codex-subagent-group'));
|
|
502
|
-
const target = groups[groups.length - 1];
|
|
503
|
-
if (!target) {
|
|
504
|
-
if (!retry) return false;
|
|
505
|
-
commitCodexChatRender();
|
|
506
|
-
requestAnimationFrame(() => jumpToActiveCodexSubagent(false));
|
|
507
|
-
return false;
|
|
508
|
-
}
|
|
509
|
-
target.open = true;
|
|
510
|
-
target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
|
511
|
-
target.classList.remove('codex-approval-focus');
|
|
512
|
-
void target.offsetWidth;
|
|
513
|
-
target.classList.add('codex-approval-focus');
|
|
514
|
-
setTimeout(() => target.classList.remove('codex-approval-focus'), 1800);
|
|
515
|
-
return true;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function jumpToCodexApproval() {
|
|
519
|
-
const pending = codexPendingPermissions.filter(item => item && item.status === 'pending');
|
|
520
|
-
if (!pending.length) return false;
|
|
521
|
-
const request = pending[codexApprovalJumpIndex % pending.length];
|
|
522
|
-
codexApprovalJumpIndex = (codexApprovalJumpIndex + 1) % pending.length;
|
|
523
|
-
void loadAndFocusCodexApproval(request);
|
|
524
|
-
return true;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
async function loadAndFocusCodexApproval(request) {
|
|
528
|
-
const permissionId = String(request?.id || '');
|
|
529
|
-
const message = codexMessages.find(item => String(item.providerId || item.id || '') === permissionId);
|
|
530
|
-
if (message && codexMessageNeedsDetail(message)) {
|
|
531
|
-
if (isCodexSubagentItem(message)) await requestCodexDetails({ threadId: String(message.threadId) });
|
|
532
|
-
else await requestCodexDetails({ ids: [String(message.id)] });
|
|
533
|
-
}
|
|
534
|
-
requestAnimationFrame(() => focusCodexApproval(permissionId));
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function focusCodexApproval(permissionId, retry = true) {
|
|
538
|
-
const target = Array.from(document.querySelectorAll('[data-codex-permission-id]'))
|
|
539
|
-
.find(element => element.dataset.codexPermissionId === permissionId);
|
|
540
|
-
if (!target) {
|
|
541
|
-
if (!retry) return false;
|
|
542
|
-
commitCodexChatRender();
|
|
543
|
-
requestAnimationFrame(() => focusCodexApproval(permissionId, false));
|
|
544
|
-
return false;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
for (let parent = target.parentElement; parent; parent = parent.parentElement) {
|
|
548
|
-
if (parent.tagName === 'DETAILS') parent.open = true;
|
|
549
|
-
}
|
|
550
|
-
target.classList.remove('codex-approval-focus');
|
|
551
|
-
void target.offsetWidth;
|
|
552
|
-
target.classList.add('codex-approval-focus');
|
|
553
|
-
target.setAttribute('tabindex', '-1');
|
|
554
|
-
target.focus({ preventScroll: true });
|
|
555
|
-
target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
|
556
|
-
setTimeout(() => target.classList.remove('codex-approval-focus'), 1800);
|
|
557
|
-
return true;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function toggleCodexModelPanel() {
|
|
561
|
-
codexModelPanelOpen = !codexModelPanelOpen;
|
|
562
|
-
codexResumePanelOpen = false;
|
|
563
|
-
codexForkPanelOpen = false;
|
|
564
|
-
codexPromptPanelOpen = false;
|
|
565
|
-
codexSkillPanelOpen = false;
|
|
566
|
-
codexModelCandidate = codexState.model || codexState.models?.[0]?.id || null;
|
|
567
|
-
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
568
|
-
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
569
|
-
document.getElementById('codex-prompt-panel').classList.remove('active');
|
|
570
|
-
document.getElementById('codex-skill-panel').classList.remove('active');
|
|
571
|
-
renderCodexModelPanel();
|
|
572
|
-
updateTerminalControlsHeight();
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function renderCodexModelPanel() {
|
|
576
|
-
const panel = document.getElementById('codex-model-panel');
|
|
577
|
-
if (!panel) return;
|
|
578
|
-
panel.classList.toggle('active', codexModelPanelOpen);
|
|
579
|
-
if (!codexModelPanelOpen) { panel.innerHTML = ''; return; }
|
|
580
|
-
const models = codexState.models || [];
|
|
581
|
-
const candidate = models.find(item => item.id === codexModelCandidate) || models.find(item => item.id === codexState.model) || models[0];
|
|
582
|
-
if (!candidate) { panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading models...</div>'; return; }
|
|
583
|
-
codexModelCandidate = candidate.id;
|
|
584
|
-
const efforts = candidate.efforts?.length ? candidate.efforts : [candidate.defaultEffort || 'medium'];
|
|
585
|
-
panel.innerHTML = `<div class="codex-picker-column">${models.map(item => `<button class="codex-picker-option${item.id === candidate.id ? ' selected' : ''}" onclick="selectCodexModelCandidate(decodePathValue('${encodePathValue(item.id)}'))">${escapeHtml(item.label || item.id)}</button>`).join('')}</div><div class="codex-picker-column">${efforts.map(effort => `<button class="codex-picker-option${candidate.id === codexState.model && effort === codexState.effort ? ' selected' : ''}" onclick="chooseCodexModelEffort(decodePathValue('${encodePathValue(candidate.id)}'), decodePathValue('${encodePathValue(effort)}'))">${escapeHtml(effort)}</button>`).join('')}</div>`;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
function selectCodexModelCandidate(model) { codexModelCandidate = model; renderCodexModelPanel(); }
|
|
589
|
-
function chooseCodexModelEffort(model, effort) {
|
|
590
|
-
codexModelPanelOpen = false;
|
|
591
|
-
applyCodexState({ model, effort });
|
|
592
|
-
sendCodexSettings({ model, effort });
|
|
593
|
-
updateTerminalControlsHeight();
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function applyCodexEvent(event) {
|
|
597
|
-
if (!event) return;
|
|
598
|
-
if (event.type === 'message' && event.message) {
|
|
599
|
-
codexMessages.push(event.message);
|
|
600
|
-
scheduleCodexDetailRefresh(event.message);
|
|
601
|
-
}
|
|
602
|
-
else if (event.type === 'message-updated' && event.message) {
|
|
603
|
-
const i = codexMessages.findIndex(item => item.id === event.message.id);
|
|
604
|
-
if (i >= 0) {
|
|
605
|
-
const existing = codexMessages[i];
|
|
606
|
-
codexMessages[i] = existing.detailLoaded
|
|
607
|
-
? { ...existing, ...event.message, detailLoaded: true }
|
|
608
|
-
: event.message;
|
|
609
|
-
scheduleCodexDetailRefresh(codexMessages[i]);
|
|
610
|
-
} else codexMessages.push(event.message);
|
|
611
|
-
}
|
|
612
|
-
else if (event.type === 'history-reset') {
|
|
613
|
-
codexMessages = event.messages || [];
|
|
614
|
-
codexDetailRevisions.clear();
|
|
615
|
-
clearTimeout(codexDetailRefreshTimer);
|
|
616
|
-
codexDetailRefreshTimer = null;
|
|
617
|
-
codexDetailRefreshIds.clear();
|
|
618
|
-
for (const pending of codexDetailRequests.values()) {
|
|
619
|
-
clearTimeout(pending.timer);
|
|
620
|
-
pending.resolve(false);
|
|
621
|
-
}
|
|
622
|
-
codexDetailRequests.clear();
|
|
623
|
-
document.getElementById('codex-chat-container')?.replaceChildren();
|
|
624
|
-
}
|
|
625
|
-
else if (event.type === 'permission-request' && event.request) { codexPendingPermissions = [...codexPendingPermissions.filter(item => item.id !== event.request.id), event.request]; }
|
|
626
|
-
else if (event.type === 'permission-updated' && event.request) codexPendingPermissions = codexPendingPermissions.map(item => item.id === event.request.id ? event.request : item);
|
|
627
|
-
if (event.state) applyCodexState(event.state, { providerStateReceived: true }); else renderCodexChat();
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function sendCodexSettings(settings) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-settings', settings })); }
|
|
631
|
-
function updateCodexSettingsFromControls() {
|
|
632
|
-
const permissionMode = document.getElementById('codex-permission-select').value;
|
|
633
|
-
const sandboxMode = document.getElementById('codex-sandbox-select').value;
|
|
634
|
-
applyCodexState({ permissionMode, sandboxMode });
|
|
635
|
-
sendCodexSettings({ permissionMode, sandboxMode });
|
|
636
|
-
}
|
|
637
|
-
function abortCodexSession() {
|
|
638
|
-
if (!codexState.canAbort || codexState.aborting || currentSocket?.readyState !== 1) return false;
|
|
639
|
-
currentSocket.send(JSON.stringify({ type: 'codex-abort' }));
|
|
640
|
-
applyCodexState({ aborting: true, canAbort: false });
|
|
641
|
-
return true;
|
|
642
|
-
}
|
|
643
|
-
function requestCodexStatus() { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-status' })); }
|
|
644
|
-
function compactCodexContext() {
|
|
645
|
-
if (!codexState.canCompact || currentSocket?.readyState !== 1) return false;
|
|
646
|
-
currentSocket.send(JSON.stringify({ type: 'codex-compact' }));
|
|
647
|
-
applyCodexState({ canCompact: false, compacting: true });
|
|
648
|
-
return true;
|
|
649
|
-
}
|
|
650
|
-
function respondCodexPermission(id, decision) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-permission', id, decision })); }
|
|
651
|
-
async function toggleCodexResumePanel() {
|
|
652
|
-
if (!codexReadyForInput()) return;
|
|
653
|
-
const panel = document.getElementById('codex-resume-panel');
|
|
654
|
-
const retryAfterFailure = panel.dataset.resumeError === 'true';
|
|
655
|
-
codexResumePanelOpen = retryAfterFailure ? true : !codexResumePanelOpen;
|
|
656
|
-
delete panel.dataset.resumeError;
|
|
657
|
-
codexModelPanelOpen = false;
|
|
658
|
-
codexForkPanelOpen = false;
|
|
659
|
-
codexPromptPanelOpen = false;
|
|
660
|
-
codexSkillPanelOpen = false;
|
|
661
|
-
document.getElementById('codex-model-panel').classList.remove('active');
|
|
662
|
-
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
663
|
-
document.getElementById('codex-prompt-panel').classList.remove('active');
|
|
664
|
-
document.getElementById('codex-skill-panel').classList.remove('active');
|
|
665
|
-
panel.classList.toggle('active', codexResumePanelOpen);
|
|
666
|
-
updateTerminalControlsHeight();
|
|
667
|
-
if (!codexResumePanelOpen) return;
|
|
668
|
-
await loadCodexThreadPanel(panel, 'resume');
|
|
669
|
-
}
|
|
670
|
-
async function toggleCodexForkPanel() {
|
|
671
|
-
if (!codexReadyForInput()) return;
|
|
672
|
-
codexForkPanelOpen = !codexForkPanelOpen;
|
|
673
|
-
codexModelPanelOpen = false;
|
|
674
|
-
codexResumePanelOpen = false;
|
|
675
|
-
codexPromptPanelOpen = false;
|
|
676
|
-
codexSkillPanelOpen = false;
|
|
677
|
-
document.getElementById('codex-model-panel').classList.remove('active');
|
|
678
|
-
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
679
|
-
document.getElementById('codex-prompt-panel').classList.remove('active');
|
|
680
|
-
document.getElementById('codex-skill-panel').classList.remove('active');
|
|
681
|
-
const panel = document.getElementById('codex-fork-panel');
|
|
682
|
-
panel.classList.toggle('active', codexForkPanelOpen);
|
|
683
|
-
updateTerminalControlsHeight();
|
|
684
|
-
if (!codexForkPanelOpen) return;
|
|
685
|
-
await loadCodexThreadPanel(panel, 'fork');
|
|
686
|
-
}
|
|
687
|
-
async function loadCodexThreadPanel(panel, action) {
|
|
688
|
-
delete panel.dataset.resumeError;
|
|
689
|
-
panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading sessions...</div>';
|
|
690
|
-
try {
|
|
691
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume-threads`, {}, 30000);
|
|
692
|
-
const data = await res.json();
|
|
693
|
-
if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load Codex sessions');
|
|
694
|
-
const items = data.items || [];
|
|
695
|
-
panel.innerHTML = items.length ? items.map(item => {
|
|
696
|
-
const questions = Array.isArray(item.questions) ? item.questions : [];
|
|
697
|
-
const handler = action === 'fork' ? 'selectCodexForkThread' : 'selectCodexResumeThread';
|
|
698
|
-
return `<button class="claude-resume-item" onclick="${handler}(decodePathValue('${encodePathValue(item.id)}'))"><div class="claude-resume-title"><span>${escapeHtml((questions[0] || 'Codex session').slice(0, 120))}${item.current ? ' · current' : ''}</span><span>${escapeHtml(item.updatedAt ? new Date(item.updatedAt).toLocaleString() : '')}</span></div><div class="codex-resume-question-secondary">${escapeHtml((questions[1] || '').slice(0, 120))}</div></button>`;
|
|
699
|
-
}).join('') : '<div class="claude-resume-meta" style="padding:12px;">No Codex sessions found for this folder.</div>';
|
|
700
|
-
} catch (e) { panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(e.message)}</div>`; }
|
|
701
|
-
updateTerminalControlsHeight();
|
|
702
|
-
}
|
|
703
|
-
function formatCodexPromptTime(timestamp) {
|
|
704
|
-
const value = Number(timestamp || 0);
|
|
705
|
-
if (!value) return '';
|
|
706
|
-
const date = new Date(value);
|
|
707
|
-
return Number.isNaN(date.getTime()) ? '' : date.toLocaleString([], {
|
|
708
|
-
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
function renderCodexPromptPanel(error = '') {
|
|
712
|
-
const panel = document.getElementById('codex-prompt-panel');
|
|
713
|
-
if (!panel) return;
|
|
714
|
-
const previousScrollTop = panel.scrollTop;
|
|
715
|
-
panel.classList.toggle('active', codexPromptPanelOpen);
|
|
716
|
-
if (!codexPromptPanelOpen) return;
|
|
717
|
-
const countLabel = codexPromptTotal ? `${codexPromptItems.length} / ${codexPromptTotal}${codexPromptTotal >= 200 ? ' max' : ''}` : '';
|
|
718
|
-
const items = codexPromptItems.map((item, index) => {
|
|
719
|
-
const expanded = codexExpandedPrompts.has(index);
|
|
720
|
-
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>`;
|
|
721
|
-
}).join('');
|
|
722
|
-
const empty = !items && !codexPromptLoading && !error
|
|
723
|
-
? '<div class="claude-resume-meta" style="padding:12px;">No text prompts found for this folder.</div>' : '';
|
|
724
|
-
const footer = codexPromptHasMore || codexPromptLoading
|
|
725
|
-
? `<div class="codex-prompt-footer"><button type="button" class="small-btn primary" onclick="loadMoreCodexPrompts()"${codexPromptLoading ? ' disabled' : ''}>${codexPromptLoading ? 'Loading…' : 'Load more'}</button></div>` : '';
|
|
726
|
-
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}`;
|
|
727
|
-
panel.scrollTop = previousScrollTop;
|
|
728
|
-
updateTerminalControlsHeight();
|
|
729
|
-
}
|
|
730
|
-
async function toggleCodexPromptPanel() {
|
|
731
|
-
codexPromptPanelOpen = !codexPromptPanelOpen;
|
|
732
|
-
codexModelPanelOpen = false;
|
|
733
|
-
codexResumePanelOpen = false;
|
|
734
|
-
codexForkPanelOpen = false;
|
|
735
|
-
codexSkillPanelOpen = false;
|
|
736
|
-
document.getElementById('codex-model-panel').classList.remove('active');
|
|
737
|
-
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
738
|
-
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
739
|
-
document.getElementById('codex-skill-panel').classList.remove('active');
|
|
740
|
-
if (!codexPromptPanelOpen) {
|
|
741
|
-
document.getElementById('codex-prompt-panel').classList.remove('active');
|
|
742
|
-
updateTerminalControlsHeight();
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
codexPromptItems = [];
|
|
746
|
-
codexPromptNextOffset = 0;
|
|
747
|
-
codexPromptHasMore = false;
|
|
748
|
-
codexPromptTotal = 0;
|
|
749
|
-
codexExpandedPrompts = new Set();
|
|
750
|
-
renderCodexPromptPanel();
|
|
751
|
-
await loadMoreCodexPrompts();
|
|
752
|
-
}
|
|
753
|
-
async function loadMoreCodexPrompts() {
|
|
754
|
-
if (codexPromptLoading || !codexPromptPanelOpen || codexPromptNextOffset >= 200) return;
|
|
755
|
-
codexPromptLoading = true;
|
|
756
|
-
renderCodexPromptPanel();
|
|
757
|
-
try {
|
|
758
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-prompts?offset=${codexPromptNextOffset}&limit=30`, {}, 60000);
|
|
759
|
-
const data = await res.json();
|
|
760
|
-
if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load prompt history');
|
|
761
|
-
codexPromptItems.push(...(data.items || []));
|
|
762
|
-
codexPromptNextOffset = Number(data.nextOffset || codexPromptItems.length);
|
|
763
|
-
codexPromptHasMore = Boolean(data.hasMore) && codexPromptNextOffset < 200;
|
|
764
|
-
codexPromptTotal = Math.min(200, Number(data.total || codexPromptItems.length));
|
|
765
|
-
codexPromptLoading = false;
|
|
766
|
-
renderCodexPromptPanel();
|
|
767
|
-
} catch (error) {
|
|
768
|
-
codexPromptLoading = false;
|
|
769
|
-
renderCodexPromptPanel(error.message);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
function toggleCodexPromptExpanded(index) {
|
|
773
|
-
if (codexExpandedPrompts.has(index)) codexExpandedPrompts.delete(index);
|
|
774
|
-
else codexExpandedPrompts.add(index);
|
|
775
|
-
renderCodexPromptPanel();
|
|
776
|
-
}
|
|
777
|
-
async function copyCodexPrompt(index) {
|
|
778
|
-
const prompt = codexPromptItems[index]?.text;
|
|
779
|
-
if (!prompt) return;
|
|
780
|
-
try {
|
|
781
|
-
await copyTextToClipboard(prompt);
|
|
782
|
-
const button = document.querySelector(`[data-codex-prompt-copy="${index}"]`);
|
|
783
|
-
if (!button) return;
|
|
784
|
-
button.textContent = 'Copied';
|
|
785
|
-
setTimeout(() => { if (button.isConnected) button.textContent = 'Copy'; }, 1200);
|
|
786
|
-
} catch (_) {
|
|
787
|
-
alert('Copy failed.');
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
const CODEX_RECENT_SKILLS_KEY = 'glad.codex.recentSkills';
|
|
792
|
-
|
|
793
|
-
function codexSkillIdentity(skill) {
|
|
794
|
-
return `${String(skill?.name || '')}\n${String(skill?.path || '')}`;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
function codexSkillDisplayName(skill) {
|
|
798
|
-
return String(skill?.interface?.displayName || skill?.name || 'Unknown skill');
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
function codexSkillDescription(skill) {
|
|
802
|
-
return String(skill?.interface?.shortDescription || skill?.shortDescription || skill?.description || '');
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function codexSkillDirectory(skill) {
|
|
806
|
-
const path = String(skill?.path || '').replace(/\\/g, '/');
|
|
807
|
-
const separator = path.lastIndexOf('/');
|
|
808
|
-
const directory = separator > 0 ? path.slice(0, separator) : path;
|
|
809
|
-
const skillRoot = directory.toLowerCase().lastIndexOf('/skills/');
|
|
810
|
-
return skillRoot >= 0 ? directory.slice(skillRoot + '/skills/'.length) : directory.split('/').filter(Boolean).pop() || directory;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function codexSkillSource(skill) {
|
|
814
|
-
const scope = String(skill?.scope || '').toLowerCase();
|
|
815
|
-
const path = String(skill?.path || '').replace(/\\/g, '/').toLowerCase();
|
|
816
|
-
if (['repo', 'project', 'workspace'].includes(scope)) return { group: 'project', label: 'Project' };
|
|
817
|
-
if (['user', 'personal'].includes(scope)) return { group: 'personal', label: 'Personal' };
|
|
818
|
-
if (scope === 'plugin') return { group: 'other', label: 'Plugin' };
|
|
819
|
-
if (['system', 'admin', 'builtin', 'built-in'].includes(scope)) return { group: 'other', label: 'System' };
|
|
820
|
-
if (path.includes('/plugins/')) return { group: 'other', label: 'Plugin' };
|
|
821
|
-
if (path.includes('/.system/')) return { group: 'other', label: 'System' };
|
|
822
|
-
if (path.includes('/.codex/skills/')) return { group: 'personal', label: 'Personal' };
|
|
823
|
-
if (path.includes('/.agents/skills/')) return { group: 'project', label: 'Project' };
|
|
824
|
-
return { group: 'other', label: scope ? scope[0].toUpperCase() + scope.slice(1) : 'Other' };
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
function compareCodexSkills(left, right) {
|
|
828
|
-
return codexSkillDisplayName(left).localeCompare(codexSkillDisplayName(right), undefined, {
|
|
829
|
-
sensitivity: 'base',
|
|
830
|
-
numeric: true
|
|
831
|
-
}) || String(left.name || '').localeCompare(String(right.name || ''), undefined, { sensitivity: 'base', numeric: true })
|
|
832
|
-
|| String(left.path || '').localeCompare(String(right.path || ''), undefined, { sensitivity: 'base', numeric: true });
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function readRecentCodexSkills() {
|
|
836
|
-
try {
|
|
837
|
-
const value = JSON.parse(localStorage.getItem(CODEX_RECENT_SKILLS_KEY) || '[]');
|
|
838
|
-
return Array.isArray(value) ? value.filter(item => item?.name && item?.path).slice(0, 3) : [];
|
|
839
|
-
} catch (_) {
|
|
840
|
-
return [];
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function rememberCodexSkill(skill) {
|
|
845
|
-
const key = codexSkillIdentity(skill);
|
|
846
|
-
const recent = readRecentCodexSkills().filter(item => codexSkillIdentity(item) !== key);
|
|
847
|
-
recent.unshift({ name: skill.name, path: skill.path });
|
|
848
|
-
try { localStorage.setItem(CODEX_RECENT_SKILLS_KEY, JSON.stringify(recent.slice(0, 3))); } catch (_) {}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function scoreCodexSkillSearch(skill, query) {
|
|
852
|
-
const tokens = String(query || '').toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
853
|
-
if (!tokens.length) return 0;
|
|
854
|
-
const source = codexSkillSource(skill);
|
|
855
|
-
const fields = [
|
|
856
|
-
String(skill.name || '').toLocaleLowerCase(),
|
|
857
|
-
codexSkillDisplayName(skill).toLocaleLowerCase(),
|
|
858
|
-
codexSkillDescription(skill).toLocaleLowerCase(),
|
|
859
|
-
source.label.toLocaleLowerCase(),
|
|
860
|
-
String(skill.scope || '').toLocaleLowerCase(),
|
|
861
|
-
String(skill.path || '').toLocaleLowerCase()
|
|
862
|
-
];
|
|
863
|
-
let score = 0;
|
|
864
|
-
for (const token of tokens) {
|
|
865
|
-
let tokenScore = Number.POSITIVE_INFINITY;
|
|
866
|
-
fields.forEach((field, fieldIndex) => {
|
|
867
|
-
const position = field.indexOf(token);
|
|
868
|
-
if (position < 0) return;
|
|
869
|
-
const exactBonus = field === token ? -4 : position === 0 ? -2 : 0;
|
|
870
|
-
tokenScore = Math.min(tokenScore, fieldIndex * 10 + position + exactBonus);
|
|
871
|
-
});
|
|
872
|
-
if (!Number.isFinite(tokenScore)) return null;
|
|
873
|
-
score += tokenScore;
|
|
874
|
-
}
|
|
875
|
-
return score;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function codexSkillItemHtml(item, index) {
|
|
879
|
-
const selected = selectedCodexSkill?.name === item.name && selectedCodexSkill?.path === item.path;
|
|
880
|
-
const displayName = codexSkillDisplayName(item);
|
|
881
|
-
const description = codexSkillDescription(item);
|
|
882
|
-
const source = codexSkillSource(item);
|
|
883
|
-
const fullName = String(item.name || displayName);
|
|
884
|
-
const directory = codexSkillDirectory(item);
|
|
885
|
-
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>`;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function codexSkillSectionHtml(title, entries) {
|
|
889
|
-
if (!entries.length) return '';
|
|
890
|
-
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>`;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
function codexSkillResultsHtml() {
|
|
894
|
-
const entries = codexSkillItems.map((item, index) => ({ item, index }));
|
|
895
|
-
const query = codexSkillQuery.trim();
|
|
896
|
-
let sections = '';
|
|
897
|
-
if (query) {
|
|
898
|
-
const matches = entries.map(entry => ({ ...entry, score: scoreCodexSkillSearch(entry.item, query) }))
|
|
899
|
-
.filter(entry => entry.score != null)
|
|
900
|
-
.sort((left, right) => left.score - right.score || compareCodexSkills(left.item, right.item));
|
|
901
|
-
sections = codexSkillSectionHtml('Search results', matches);
|
|
902
|
-
} else {
|
|
903
|
-
const byIdentity = new Map(entries.map(entry => [codexSkillIdentity(entry.item), entry]));
|
|
904
|
-
const recent = readRecentCodexSkills().map(item => byIdentity.get(codexSkillIdentity(item))).filter(Boolean);
|
|
905
|
-
const recentKeys = new Set(recent.map(entry => codexSkillIdentity(entry.item)));
|
|
906
|
-
const remaining = entries.filter(entry => !recentKeys.has(codexSkillIdentity(entry.item)));
|
|
907
|
-
const group = name => remaining.filter(entry => codexSkillSource(entry.item).group === name)
|
|
908
|
-
.sort((left, right) => compareCodexSkills(left.item, right.item));
|
|
909
|
-
sections = codexSkillSectionHtml('Recently used', recent)
|
|
910
|
-
+ codexSkillSectionHtml('Current project', group('project'))
|
|
911
|
-
+ codexSkillSectionHtml('Personal', group('personal'))
|
|
912
|
-
+ codexSkillSectionHtml('Other', group('other'));
|
|
913
|
-
}
|
|
914
|
-
const emptyText = query ? 'No skills match your search.' : 'No enabled skills found for this folder.';
|
|
915
|
-
const empty = !sections && !codexSkillLoading && !codexSkillError
|
|
916
|
-
? `<div class="claude-resume-meta codex-skill-empty">${emptyText}</div>` : '';
|
|
917
|
-
return `${codexSkillError ? `<div class="claude-resume-meta codex-skill-error">${escapeHtml(codexSkillError)}</div>` : ''}${sections}${empty}`;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
function renderCodexSkillPanel(focusSearch = false) {
|
|
921
|
-
const panel = document.getElementById('codex-skill-panel');
|
|
922
|
-
if (!panel) return;
|
|
923
|
-
panel.classList.toggle('active', codexSkillPanelOpen);
|
|
924
|
-
if (!codexSkillPanelOpen) { panel.innerHTML = ''; return; }
|
|
925
|
-
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>`;
|
|
926
|
-
if (focusSearch) {
|
|
927
|
-
requestAnimationFrame(() => {
|
|
928
|
-
const search = document.getElementById('codex-skill-search');
|
|
929
|
-
if (!search) return;
|
|
930
|
-
search.focus({ preventScroll: true });
|
|
931
|
-
search.setSelectionRange(search.value.length, search.value.length);
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
updateTerminalControlsHeight();
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
function updateCodexSkillQuery(value) {
|
|
938
|
-
codexSkillQuery = String(value || '');
|
|
939
|
-
const results = document.getElementById('codex-skill-results');
|
|
940
|
-
if (results) results.innerHTML = codexSkillResultsHtml();
|
|
941
|
-
else renderCodexSkillPanel(true);
|
|
942
|
-
updateTerminalControlsHeight();
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
async function toggleCodexSkillPanel() {
|
|
946
|
-
if (!codexReadyForInput()) return;
|
|
947
|
-
codexSkillPanelOpen = !codexSkillPanelOpen;
|
|
948
|
-
codexModelPanelOpen = false;
|
|
949
|
-
codexResumePanelOpen = false;
|
|
950
|
-
codexForkPanelOpen = false;
|
|
951
|
-
codexPromptPanelOpen = false;
|
|
952
|
-
document.getElementById('codex-model-panel').classList.remove('active');
|
|
953
|
-
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
954
|
-
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
955
|
-
document.getElementById('codex-prompt-panel').classList.remove('active');
|
|
956
|
-
if (!codexSkillPanelOpen) {
|
|
957
|
-
renderCodexSkillPanel();
|
|
958
|
-
updateTerminalControlsHeight();
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
codexSkillLoading = true;
|
|
962
|
-
codexSkillQuery = '';
|
|
963
|
-
codexSkillError = '';
|
|
964
|
-
codexSkillItems = [];
|
|
965
|
-
renderCodexSkillPanel(true);
|
|
966
|
-
try {
|
|
967
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-skills?forceReload=true`, {}, 30000);
|
|
968
|
-
const data = await res.json();
|
|
969
|
-
if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load skills');
|
|
970
|
-
codexSkillItems = data.skills || [];
|
|
971
|
-
codexSkillLoading = false;
|
|
972
|
-
codexSkillError = (data.errors || []).map(item => item.message || String(item)).filter(Boolean).join(' · ');
|
|
973
|
-
renderCodexSkillPanel(true);
|
|
974
|
-
} catch (error) {
|
|
975
|
-
codexSkillLoading = false;
|
|
976
|
-
codexSkillError = error.message;
|
|
977
|
-
renderCodexSkillPanel(true);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
function selectCodexSkill(index) {
|
|
982
|
-
const skill = codexSkillItems[index];
|
|
983
|
-
if (!skill) return;
|
|
984
|
-
const alreadySelected = selectedCodexSkill?.name === skill.name && selectedCodexSkill?.path === skill.path;
|
|
985
|
-
selectedCodexSkill = alreadySelected ? null : { name: skill.name, path: skill.path };
|
|
986
|
-
if (selectedCodexSkill) rememberCodexSkill(skill);
|
|
987
|
-
codexSkillPanelOpen = false;
|
|
988
|
-
renderCodexSkillPanel();
|
|
989
|
-
applyCodexState({});
|
|
990
|
-
updateTerminalControlsHeight();
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function clearCodexSkillSelection() {
|
|
994
|
-
selectedCodexSkill = null;
|
|
995
|
-
applyCodexState({});
|
|
996
|
-
}
|
|
997
|
-
async function selectCodexResumeThread(threadId) {
|
|
998
|
-
if (codexResumeInFlight || codexState.resuming) return;
|
|
999
|
-
codexResumeInFlight = true;
|
|
1000
|
-
const panel = document.getElementById('codex-resume-panel');
|
|
1001
|
-
panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Resuming conversation…</div>';
|
|
1002
|
-
applyCodexState({ resuming: true, canAbort: true });
|
|
1003
|
-
updateTerminalControlsHeight();
|
|
1004
|
-
let failure = '';
|
|
1005
|
-
try {
|
|
1006
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume`, {
|
|
1007
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threadId })
|
|
1008
|
-
}, 70000);
|
|
1009
|
-
const data = await res.json();
|
|
1010
|
-
if (!res.ok || !data.success) throw new Error(data.error || 'Unable to resume Codex thread');
|
|
1011
|
-
codexResumePanelOpen = false;
|
|
1012
|
-
panel.classList.remove('active');
|
|
1013
|
-
} catch (error) {
|
|
1014
|
-
failure = /resume aborted/i.test(error.message || '')
|
|
1015
|
-
? 'Conversation recovery stopped.'
|
|
1016
|
-
: (error.message || 'Unable to resume Codex thread');
|
|
1017
|
-
} finally {
|
|
1018
|
-
codexResumeInFlight = false;
|
|
1019
|
-
// 本地 in-flight 也参与控件状态,清除后必须主动重绘。
|
|
1020
|
-
applyCodexState({ resuming: false, canAbort: false });
|
|
1021
|
-
if (failure) {
|
|
1022
|
-
codexResumePanelOpen = true;
|
|
1023
|
-
panel.classList.add('active');
|
|
1024
|
-
panel.dataset.resumeError = 'true';
|
|
1025
|
-
panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(failure)}</div>`;
|
|
1026
|
-
}
|
|
1027
|
-
updateTerminalControlsHeight();
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
async function selectCodexForkThread(threadId) {
|
|
1031
|
-
const panel = document.getElementById('codex-fork-panel');
|
|
1032
|
-
panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Forking and switching this conversation...</div>';
|
|
1033
|
-
updateTerminalControlsHeight();
|
|
1034
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-fork`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threadId }) }, 60000);
|
|
1035
|
-
const data = await res.json();
|
|
1036
|
-
if (!res.ok || !data.success) {
|
|
1037
|
-
panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(data.error || 'Unable to fork Codex thread')}</div>`;
|
|
1038
|
-
updateTerminalControlsHeight();
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
codexForkPanelOpen = false;
|
|
1042
|
-
panel.classList.remove('active');
|
|
1043
|
-
panel.innerHTML = '';
|
|
1044
|
-
updateTerminalControlsHeight();
|
|
1045
|
-
}
|