harness-mix 0.1.2 → 0.1.3
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/CHANGELOG.md +10 -1
- package/README.md +60 -15
- package/docs/images/codex-desktop-home.png +0 -0
- package/docs/images/codex-desktop-session.png +0 -0
- package/docs/multi-agent-collaboration.md +68 -68
- package/output/native-build/renderer-extension.js +2225 -1761
- package/package.json +1 -2
- package/scripts/collaboration-recovery-test.cjs +22 -2
- package/scripts/collaboration-ui-smoke.cjs +10 -5
- package/scripts/native-protocol-test.cjs +20 -0
- package/scripts/workspace-test.cjs +2 -14
- package/src/main/adapters/antigravity.js +49 -18
- package/src/main/adapters/claude.js +3 -2
- package/src/main/adapters/codex.js +10 -8
- package/src/main/adapters/dsh.js +3 -2
- package/src/main/adapters/opencode.js +21 -13
- package/src/main/adapters/pi-family.js +3 -3
- package/src/main/host/collaboration-worktree.js +63 -4
- package/src/main/host/collaboration.js +28 -2
- package/src/main/host/runtime.js +22 -1
- package/src/main/native/protocol.js +250 -24
- package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/index.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts +40 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-credits-control.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +8 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-usage-control.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/connections-page.d.ts +3 -1
- package/src/native-ui/renderer-extension/dist/types/settings/connections-page.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/harness-accounts.d.ts +8 -1
- package/src/native-ui/renderer-extension/dist/types/settings/harness-accounts.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +16 -0
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/src/assets/README.md +8 -38
- package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +9 -1
- package/src/native-ui/renderer-extension/src/index.ts +9 -0
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +67 -6
- package/src/native-ui/renderer-extension/src/renderer-collab-cards.ts +198 -0
- package/src/native-ui/renderer-extension/src/renderer-credits-control.ts +32 -12
- package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +139 -46
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +7 -2
- package/src/native-ui/renderer-extension/src/renderer-usage-control.ts +34 -3
- package/src/native-ui/renderer-extension/src/settings/accounts.css +80 -4
- package/src/native-ui/renderer-extension/src/settings/connections-page.ts +68 -29
- package/src/native-ui/renderer-extension/src/settings/harness-accounts.ts +89 -28
- package/src/native-ui/renderer-extension/src/settings/localization.ts +48 -0
- package/src/native-ui/renderer-extension/src/settings/pages.ts +19 -2
- package/src/native-ui/renderer-extension/src/settings/shell.css +62 -0
- package/src/native-ui/renderer-extension/src/settings/shell.ts +1 -1
- package/src/native-ui/renderer-extension/test/renderer-collab-cards.test.ts +45 -0
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +1 -0
- package/src/native-ui/shared-contracts/dist/harness-accounts.d.ts +20 -6
- package/src/native-ui/shared-contracts/dist/harness-accounts.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/harness-accounts.js +4 -1
- package/src/native-ui/shared-contracts/dist/harness-accounts.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/harness-models.d.ts +22 -0
- package/src/native-ui/shared-contracts/dist/harness-models.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/harness-models.js +2 -1
- package/src/native-ui/shared-contracts/dist/harness-models.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/shared-contracts/src/harness-accounts.ts +4 -1
- package/src/native-ui/shared-contracts/src/harness-models.ts +6 -1
- package/docs/images/harness-mix-home.png +0 -0
- package/docs/images/harness-mix-session.png +0 -0
- package/scripts/git-test.cjs +0 -33
- package/src/assets/icons/activity-compact.svg +0 -1
- package/src/assets/icons/activity-edit.svg +0 -1
- package/src/assets/icons/activity-terminal.svg +0 -1
- package/src/assets/icons/commands.svg +0 -1
- package/src/assets/icons/home-chat.svg +0 -1
- package/src/assets/icons/home-folder.svg +0 -1
- package/src/assets/icons/thread-fork.svg +0 -1
- package/src/main/workspace/git.js +0 -73
- package/src/main/workspace/ipc.js +0 -50
- package/src/main/workspace/terminal.js +0 -34
- package/src/native-ui/renderer-extension/src/assets/claude-agent.svg +0 -3
- package/src/native-ui/renderer-extension/src/assets/codex-logo-bright.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codex-logo-transparent.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codex-logo.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codexhost-icon.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codexhost-logo-transparent.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codexhost-logo.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codexhost-readme.svg +0 -8
- package/src/native-ui/renderer-extension/src/assets/pi-agent.svg +0 -4
- package/src/native-ui/renderer-extension/src/settings/accounts-quota-interactive.prototype.html +0 -1418
- package/src/native-ui/renderer-extension/src/settings/update-notes.preview.html +0 -491
- package/src/native-ui/renderer-extension/src/settings/windows-update.preview.html +0 -2135
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { type HostThreadId, hostThreadIdSchema } from '@codexhost/shared-contracts';
|
|
2
|
+
import { openRendererThread } from './renderer-fork-control.js';
|
|
3
|
+
import { collaborationIcon } from './collaboration-icon.js';
|
|
4
|
+
|
|
5
|
+
export interface CollabCardPayload {
|
|
6
|
+
task_id?: string;
|
|
7
|
+
parent_thread_id?: string;
|
|
8
|
+
child_thread_id?: string;
|
|
9
|
+
agent_type?: string;
|
|
10
|
+
status?: string;
|
|
11
|
+
display_status?: string;
|
|
12
|
+
task?: string;
|
|
13
|
+
result?: string;
|
|
14
|
+
diff?: string;
|
|
15
|
+
digest?: string;
|
|
16
|
+
branch?: string;
|
|
17
|
+
workspace?: { mode: string; cwd?: string; branch?: string };
|
|
18
|
+
applied?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CollabCardOptions {
|
|
22
|
+
openThread?: (threadId: HostThreadId) => Promise<void>;
|
|
23
|
+
reviewWorkspace?: (threadId: string) => Promise<{ patch: string; digest: string; hasConflict?: boolean; conflictingFiles?: string[] }>;
|
|
24
|
+
applyWorkspace?: (threadId: string, digest?: string) => Promise<{ patch: string; digest: string }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseCollabPayload(text: string): CollabCardPayload | null {
|
|
28
|
+
if (!text || (!text.includes('child_thread_id') && !text.includes('task_id') && !text.includes('Agent 协作'))) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const match = /\{[\s\S]*?"task_id"[\s\S]*?\}/.exec(text);
|
|
32
|
+
if (!match) return null;
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(match[0]);
|
|
35
|
+
if (parsed && (parsed.task_id || parsed.child_thread_id)) return parsed as CollabCardPayload;
|
|
36
|
+
} catch {}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function formatDiffHtml(diff: string): string {
|
|
41
|
+
if (!diff) return '<div style="padding:8px;color:#888;font-size:12px">暂无文件改动</div>';
|
|
42
|
+
const lines = diff.split(/\r?\n/);
|
|
43
|
+
const rows = lines.map(line => {
|
|
44
|
+
let bg = 'transparent';
|
|
45
|
+
let color = 'inherit';
|
|
46
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
47
|
+
bg = '#22c55e1a';
|
|
48
|
+
color = '#16a34a';
|
|
49
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
50
|
+
bg = '#ef44441a';
|
|
51
|
+
color = '#dc2626';
|
|
52
|
+
} else if (line.startsWith('@@')) {
|
|
53
|
+
bg = '#8888881a';
|
|
54
|
+
color = '#64748b';
|
|
55
|
+
}
|
|
56
|
+
const escaped = line.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
57
|
+
return `<div style="background:${bg};color:${color};padding:1px 8px;font-family:ui-monospace,monospace;white-space:pre">${escaped || ' '}</div>`;
|
|
58
|
+
});
|
|
59
|
+
return `<div style="font-size:12px;line-height:1.45;max-height:360px;overflow:auto;border-radius:6px;border:1px solid #8883;background:#0001;margin-top:6px">${rows.join('')}</div>`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function installCollabCards(options: CollabCardOptions = {}) {
|
|
63
|
+
if (typeof document === 'undefined') {
|
|
64
|
+
return { scan: () => {}, dispose: () => {} };
|
|
65
|
+
}
|
|
66
|
+
let disposed = false;
|
|
67
|
+
const enhancedElements = new WeakSet<Element>();
|
|
68
|
+
const openFn = options.openThread ?? ((threadId: HostThreadId) => openRendererThread(threadId));
|
|
69
|
+
|
|
70
|
+
const enhanceElement = (card: HTMLElement, payload: CollabCardPayload) => {
|
|
71
|
+
if (enhancedElements.has(card)) return;
|
|
72
|
+
enhancedElements.add(card);
|
|
73
|
+
|
|
74
|
+
const strip = document.createElement('div');
|
|
75
|
+
strip.className = 'harness-mix-collab-actions';
|
|
76
|
+
strip.style.cssText = 'display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px;padding-top:8px;border-top:1px solid #8882;font:13px system-ui';
|
|
77
|
+
|
|
78
|
+
const agent = payload.agent_type || 'agent';
|
|
79
|
+
const icon = collaborationIcon(agent, agent, 18);
|
|
80
|
+
strip.append(icon);
|
|
81
|
+
|
|
82
|
+
if (payload.child_thread_id) {
|
|
83
|
+
const childId = payload.child_thread_id;
|
|
84
|
+
const jumpBtn = document.createElement('button');
|
|
85
|
+
jumpBtn.type = 'button';
|
|
86
|
+
jumpBtn.className = 'harness-mix-collab-jump';
|
|
87
|
+
jumpBtn.style.cssText = 'display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;border:1px solid #8884;background:#8881;color:inherit;font:inherit;cursor:pointer;font-size:12px';
|
|
88
|
+
jumpBtn.innerHTML = `↗ 查看 <b>@${agent}</b> 会话`;
|
|
89
|
+
jumpBtn.title = `切换定位至子任务会话 (${childId})`;
|
|
90
|
+
jumpBtn.addEventListener('click', async (e) => {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
const parsed = hostThreadIdSchema.safeParse(childId);
|
|
94
|
+
if (parsed.success) {
|
|
95
|
+
try {
|
|
96
|
+
await openFn(parsed.data);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.warn('[CollabCard] Jump to thread failed:', err);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
strip.append(jumpBtn);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (payload.diff || payload.workspace?.mode === 'worktree') {
|
|
106
|
+
const diffContainer = document.createElement('div');
|
|
107
|
+
diffContainer.className = 'harness-mix-collab-diff-wrap';
|
|
108
|
+
diffContainer.style.cssText = 'width:100%;display:none;margin-top:6px';
|
|
109
|
+
if (payload.diff) {
|
|
110
|
+
diffContainer.innerHTML = formatDiffHtml(payload.diff);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const toggleDiffBtn = document.createElement('button');
|
|
114
|
+
toggleDiffBtn.type = 'button';
|
|
115
|
+
toggleDiffBtn.className = 'harness-mix-collab-diff-toggle';
|
|
116
|
+
toggleDiffBtn.style.cssText = 'display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;border:1px solid #8884;background:#8881;color:inherit;font:inherit;cursor:pointer;font-size:12px';
|
|
117
|
+
toggleDiffBtn.textContent = '🔍 查看产物 Diff';
|
|
118
|
+
toggleDiffBtn.addEventListener('click', async (e) => {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
e.stopPropagation();
|
|
121
|
+
const isHidden = diffContainer.style.display === 'none';
|
|
122
|
+
if (isHidden && !diffContainer.innerHTML.trim() && payload.child_thread_id && options.reviewWorkspace) {
|
|
123
|
+
toggleDiffBtn.textContent = '加载 Diff 中…';
|
|
124
|
+
try {
|
|
125
|
+
const review = await options.reviewWorkspace(payload.child_thread_id);
|
|
126
|
+
diffContainer.innerHTML = formatDiffHtml(review.patch);
|
|
127
|
+
} catch (err: any) {
|
|
128
|
+
diffContainer.innerHTML = `<div style="padding:8px;color:#ef4444;font-size:12px">加载 Diff 失败: ${err?.message || err}</div>`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
diffContainer.style.display = isHidden ? 'block' : 'none';
|
|
132
|
+
toggleDiffBtn.textContent = isHidden ? '收起产物 Diff' : '🔍 查看产物 Diff';
|
|
133
|
+
});
|
|
134
|
+
strip.append(toggleDiffBtn);
|
|
135
|
+
strip.append(diffContainer);
|
|
136
|
+
|
|
137
|
+
if (payload.workspace?.mode === 'worktree' && !payload.applied && options.applyWorkspace && payload.child_thread_id) {
|
|
138
|
+
const applyBtn = document.createElement('button');
|
|
139
|
+
applyBtn.type = 'button';
|
|
140
|
+
applyBtn.className = 'harness-mix-collab-apply';
|
|
141
|
+
applyBtn.style.cssText = 'display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;border:1px solid #16a34a88;background:#16a34a1a;color:#16a34a;font:inherit;cursor:pointer;font-size:12px;font-weight:500';
|
|
142
|
+
applyBtn.textContent = '✓ 合并改动到主项目';
|
|
143
|
+
applyBtn.title = '将该子任务的独立隔离分支改动应用到主工作区';
|
|
144
|
+
applyBtn.addEventListener('click', async (e) => {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
e.stopPropagation();
|
|
147
|
+
applyBtn.disabled = true;
|
|
148
|
+
applyBtn.textContent = '合并中…';
|
|
149
|
+
try {
|
|
150
|
+
await options.applyWorkspace!(payload.child_thread_id!, payload.digest);
|
|
151
|
+
applyBtn.textContent = '✓ 已应用';
|
|
152
|
+
applyBtn.style.border = '1px solid #8884';
|
|
153
|
+
applyBtn.style.color = '#888';
|
|
154
|
+
payload.applied = true;
|
|
155
|
+
} catch (err: any) {
|
|
156
|
+
applyBtn.disabled = false;
|
|
157
|
+
applyBtn.textContent = '合并失败(重试)';
|
|
158
|
+
alert(`合并失败:${err?.message || err}`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
strip.append(applyBtn);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
card.append(strip);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const scan = () => {
|
|
169
|
+
if (disposed) return;
|
|
170
|
+
const candidates = document.querySelectorAll<HTMLElement>(
|
|
171
|
+
'[data-local-conversation-item-target-ids], [data-turn-key], [data-testid*="tool"], .prose, pre'
|
|
172
|
+
);
|
|
173
|
+
for (const el of candidates) {
|
|
174
|
+
if (enhancedElements.has(el)) continue;
|
|
175
|
+
const text = el.textContent || '';
|
|
176
|
+
const payload = parseCollabPayload(text);
|
|
177
|
+
if (payload) {
|
|
178
|
+
enhanceElement(el, payload);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const observer = new MutationObserver(() => scan());
|
|
184
|
+
observer.observe(document.body || document.documentElement, {
|
|
185
|
+
childList: true,
|
|
186
|
+
subtree: true,
|
|
187
|
+
});
|
|
188
|
+
scan();
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
scan,
|
|
192
|
+
dispose() {
|
|
193
|
+
disposed = true;
|
|
194
|
+
observer.disconnect();
|
|
195
|
+
document.querySelectorAll('.harness-mix-collab-actions').forEach(el => el.remove());
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -114,13 +114,20 @@ function productLabel(product: string, locale: RendererSettingsLocale): string {
|
|
|
114
114
|
if (product === "GrokImagine") return "Imagine";
|
|
115
115
|
if (product === "GrokVoice") return "Voice";
|
|
116
116
|
const messages = rendererCreditsMessages(locale);
|
|
117
|
-
if (product === "5-hour window") return messages.fiveHour;
|
|
118
|
-
if (product === "7-day window") return messages.sevenDay;
|
|
119
|
-
if (product
|
|
120
|
-
|
|
117
|
+
if (product === "5-hour window" || product === "5h window") return messages.fiveHour;
|
|
118
|
+
if (product === "7-day window" || product === "7d window") return messages.sevenDay;
|
|
119
|
+
if (product === "Weekly window" || product === "weekly window") return messages.weekly;
|
|
120
|
+
if (product.endsWith(" · 5-hour window") || product.endsWith(" · 5h window")) {
|
|
121
|
+
const prefix = product.replace(/ · 5(-hour|h) window$/i, "");
|
|
122
|
+
return `${prefix} · ${messages.fiveHour}`;
|
|
121
123
|
}
|
|
122
|
-
if (product.endsWith(" · 7-day window")) {
|
|
123
|
-
|
|
124
|
+
if (product.endsWith(" · 7-day window") || product.endsWith(" · 7d window")) {
|
|
125
|
+
const prefix = product.replace(/ · 7(-day|d) window$/i, "");
|
|
126
|
+
return `${prefix} · ${messages.sevenDay}`;
|
|
127
|
+
}
|
|
128
|
+
if (product.endsWith(" · Weekly window") || product.endsWith(" · weekly window")) {
|
|
129
|
+
const prefix = product.replace(/ · (weekly|Weekly) window$/i, "");
|
|
130
|
+
return `${prefix} · ${messages.weekly}`;
|
|
124
131
|
}
|
|
125
132
|
return product;
|
|
126
133
|
}
|
|
@@ -140,7 +147,7 @@ function renderCreditsBar(usagePercent: number, color: string): HTMLDivElement {
|
|
|
140
147
|
track.dataset.codexhostCreditsBar = "";
|
|
141
148
|
track.style.height = "6px";
|
|
142
149
|
track.style.borderRadius = "9999px";
|
|
143
|
-
track.style.background = "color-mix(in srgb, currentColor
|
|
150
|
+
track.style.background = "color-mix(in srgb, currentColor 12%, transparent)";
|
|
144
151
|
track.style.overflow = "hidden";
|
|
145
152
|
const fill = document.createElement("span");
|
|
146
153
|
fill.style.display = "block";
|
|
@@ -148,6 +155,7 @@ function renderCreditsBar(usagePercent: number, color: string): HTMLDivElement {
|
|
|
148
155
|
fill.style.borderRadius = "9999px";
|
|
149
156
|
fill.style.width = `${Math.min(100, Math.max(0, usagePercent))}%`;
|
|
150
157
|
fill.style.background = color;
|
|
158
|
+
fill.style.transition = "width 0.25s ease";
|
|
151
159
|
track.append(fill);
|
|
152
160
|
return track;
|
|
153
161
|
}
|
|
@@ -167,21 +175,25 @@ function renderCreditsHeader(
|
|
|
167
175
|
messages: RendererCreditsMessages,
|
|
168
176
|
): HTMLDivElement {
|
|
169
177
|
const wrapper = document.createElement("div");
|
|
170
|
-
wrapper.style.marginBottom = "
|
|
178
|
+
wrapper.style.marginBottom = "10px";
|
|
179
|
+
wrapper.style.padding = "8px 10px";
|
|
180
|
+
wrapper.style.borderRadius = "10px";
|
|
181
|
+
wrapper.style.background = "color-mix(in srgb, currentColor 5%, transparent)";
|
|
182
|
+
wrapper.style.border = "1px solid color-mix(in srgb, currentColor 7%, transparent)";
|
|
171
183
|
|
|
172
184
|
const top = document.createElement("div");
|
|
173
185
|
top.style.display = "flex";
|
|
174
186
|
top.style.alignItems = "flex-start";
|
|
175
187
|
top.style.justifyContent = "space-between";
|
|
176
188
|
top.style.gap = "12px";
|
|
177
|
-
top.style.marginBottom = "
|
|
189
|
+
top.style.marginBottom = "6px";
|
|
178
190
|
|
|
179
191
|
// Same left-label / right-percent column order as each tile below, so the
|
|
180
192
|
// reset line always lands under its own label instead of zig-zagging sides.
|
|
181
193
|
const left = document.createElement("div");
|
|
182
194
|
const label = document.createElement("div");
|
|
183
195
|
label.textContent = creditsPeriodLabel(credits.periodType, locale);
|
|
184
|
-
label.style.fontSize = "
|
|
196
|
+
label.style.fontSize = "13px";
|
|
185
197
|
label.style.fontWeight = "600";
|
|
186
198
|
left.append(label);
|
|
187
199
|
if (credits.resetsAt) {
|
|
@@ -189,6 +201,7 @@ function renderCreditsHeader(
|
|
|
189
201
|
reset.textContent = resetLabel(credits.resetsAt, locale, messages);
|
|
190
202
|
reset.style.fontSize = "11px";
|
|
191
203
|
reset.style.color = "color-mix(in srgb, currentColor 62%, transparent)";
|
|
204
|
+
reset.style.marginTop = "2px";
|
|
192
205
|
left.append(reset);
|
|
193
206
|
}
|
|
194
207
|
|
|
@@ -207,7 +220,7 @@ function renderCreditsHeader(
|
|
|
207
220
|
remainingLabel.style.opacity = "0.8";
|
|
208
221
|
const remainingValue = document.createElement("span");
|
|
209
222
|
remainingValue.textContent = formatRendererCreditsPercent(remaining);
|
|
210
|
-
remainingValue.style.fontSize = "
|
|
223
|
+
remainingValue.style.fontSize = "24px";
|
|
211
224
|
remainingValue.style.fontWeight = "700";
|
|
212
225
|
remainingValue.style.fontVariantNumeric = "tabular-nums";
|
|
213
226
|
percent.append(remainingLabel, remainingValue);
|
|
@@ -229,7 +242,11 @@ function renderCreditsTile(
|
|
|
229
242
|
const remaining = remainingPercent(usagePercent);
|
|
230
243
|
|
|
231
244
|
const tile = document.createElement("div");
|
|
232
|
-
tile.style.marginBottom = "
|
|
245
|
+
tile.style.marginBottom = "7px";
|
|
246
|
+
tile.style.padding = "7px 9px";
|
|
247
|
+
tile.style.borderRadius = "8px";
|
|
248
|
+
tile.style.background = "color-mix(in srgb, currentColor 4%, transparent)";
|
|
249
|
+
tile.style.border = "1px solid color-mix(in srgb, currentColor 6%, transparent)";
|
|
233
250
|
|
|
234
251
|
const top = document.createElement("div");
|
|
235
252
|
top.style.display = "flex";
|
|
@@ -242,18 +259,21 @@ function renderCreditsTile(
|
|
|
242
259
|
const name = document.createElement("span");
|
|
243
260
|
name.textContent = label;
|
|
244
261
|
name.style.fontSize = "12px";
|
|
262
|
+
name.style.fontWeight = "500";
|
|
245
263
|
left.append(name);
|
|
246
264
|
if (resetsAt) {
|
|
247
265
|
const reset = document.createElement("div");
|
|
248
266
|
reset.textContent = resetLabel(resetsAt, locale, messages);
|
|
249
267
|
reset.style.fontSize = "10.5px";
|
|
250
268
|
reset.style.color = "color-mix(in srgb, currentColor 62%, transparent)";
|
|
269
|
+
reset.style.marginTop = "2px";
|
|
251
270
|
left.append(reset);
|
|
252
271
|
}
|
|
253
272
|
|
|
254
273
|
const percent = document.createElement("span");
|
|
255
274
|
percent.textContent = `${messages.remaining} ${formatRendererCreditsPercent(remaining)}`;
|
|
256
275
|
percent.style.fontSize = "12px";
|
|
276
|
+
percent.style.fontWeight = "600";
|
|
257
277
|
percent.style.fontVariantNumeric = "tabular-nums";
|
|
258
278
|
percent.style.color = color;
|
|
259
279
|
top.append(left, percent);
|
|
@@ -4,6 +4,13 @@ export interface CollaborationSession { id: string; title: string; harnessId: st
|
|
|
4
4
|
export interface CollaborationMentionCatalog { agents: CollaborationAgent[]; sessions: CollaborationSession[]; canDelegate?: boolean }
|
|
5
5
|
type MentionEntry = ({ kind: 'agent' } & CollaborationAgent) | ({ kind: 'session' } & CollaborationSession);
|
|
6
6
|
|
|
7
|
+
const entryKey = (entry: MentionEntry): string => entry.kind === 'agent' ? `agent:${entry.id}` : `session:${entry.id}`;
|
|
8
|
+
|
|
9
|
+
// Encode an entry as the @agent literal the Host expects in the submitted text.
|
|
10
|
+
const entryLiteral = (entry: MentionEntry): string => entry.kind === 'agent'
|
|
11
|
+
? `@${entry.id} `
|
|
12
|
+
: `@[${entry.title.replace(/[\[\]()\r\n]/g, '')}](harness-mix://session/${entry.id}) `;
|
|
13
|
+
|
|
7
14
|
/** Adds native-Harness suggestions while preserving the original composer editor. */
|
|
8
15
|
export function installHarnessMentions(load: (editor: Element, query: string) => Promise<CollaborationMentionCatalog>) {
|
|
9
16
|
const menu = document.createElement('div');
|
|
@@ -26,68 +33,108 @@ export function installHarnessMentions(load: (editor: Element, query: string) =>
|
|
|
26
33
|
let matches: MentionEntry[] = [], activeKind: 'agent' | 'session' = 'agent', canDelegate = true;
|
|
27
34
|
const catalogs = new Map<HTMLElement, CollaborationMentionCatalog>();
|
|
28
35
|
const badges = new Map<HTMLElement, HTMLElement>();
|
|
36
|
+
|
|
37
|
+
// Per-editor mention state, decoupled from editor text so the literal
|
|
38
|
+
// @agent handles never leak into the visible composer.
|
|
39
|
+
const selections = new Map<HTMLElement, Map<string, MentionEntry>>();
|
|
40
|
+
const selectionsFor = (target: HTMLElement): Map<string, MentionEntry> => {
|
|
41
|
+
let perEditor = selections.get(target);
|
|
42
|
+
if (!perEditor) {
|
|
43
|
+
perEditor = new Map();
|
|
44
|
+
selections.set(target, perEditor);
|
|
45
|
+
}
|
|
46
|
+
return perEditor;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const replaceSelectionWith = (target: HTMLElement, replacement: string): boolean => {
|
|
50
|
+
if (target instanceof HTMLTextAreaElement) {
|
|
51
|
+
target.setSelectionRange(start, end);
|
|
52
|
+
target.setRangeText(replacement, start, end, 'end');
|
|
53
|
+
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
// ProseMirror may replace text nodes while its own suggestions render.
|
|
57
|
+
// Rebuild the saved text offsets instead of reusing a live, stale Range.
|
|
58
|
+
const fresh = document.createRange();
|
|
59
|
+
const walker = document.createTreeWalker(target, NodeFilter.SHOW_TEXT);
|
|
60
|
+
let offset = 0, foundStart = false, foundEnd = false;
|
|
61
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
62
|
+
const length = node.textContent?.length ?? 0;
|
|
63
|
+
if (!foundStart && start <= offset + length) { fresh.setStart(node, start - offset); foundStart = true; }
|
|
64
|
+
if (foundStart && end <= offset + length) { fresh.setEnd(node, end - offset); foundEnd = true; break; }
|
|
65
|
+
offset += length;
|
|
66
|
+
}
|
|
67
|
+
if (!foundEnd) return false;
|
|
68
|
+
const selection = window.getSelection();
|
|
69
|
+
selection?.removeAllRanges(); selection?.addRange(fresh);
|
|
70
|
+
document.dispatchEvent(new Event('selectionchange'));
|
|
71
|
+
document.execCommand('insertText', false, replacement);
|
|
72
|
+
return true;
|
|
73
|
+
};
|
|
74
|
+
|
|
29
75
|
const syncBadges = (target: HTMLElement) => {
|
|
30
76
|
for (const [old, strip] of badges) if (!old.isConnected) { strip.remove(); badges.delete(old); catalogs.delete(old); }
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const catalog = catalogs.get(target) ?? { agents: [], sessions: [] };
|
|
34
|
-
const selectedAgents = catalog.agents.filter(a => ids.has(a.id));
|
|
35
|
-
const sessionIds = new Set([...text.matchAll(/\]\(harness-mix:\/\/session\/([A-Za-z0-9_-]+)\)/g)].map(match => match[1]));
|
|
36
|
-
const selectedSessions = catalog.sessions.filter(session => sessionIds.has(session.id));
|
|
77
|
+
const perEditor = selections.get(target);
|
|
78
|
+
const hasAny = !!perEditor && perEditor.size > 0;
|
|
37
79
|
let strip = badges.get(target);
|
|
38
|
-
if (!strip && !
|
|
80
|
+
if (!strip && !hasAny) return;
|
|
39
81
|
if (!strip) {
|
|
40
82
|
strip = document.createElement('div'); strip.dataset.harnessMixSelectedMentions = 'true';
|
|
41
83
|
strip.setAttribute('aria-label', '已提及的协作者');
|
|
42
84
|
strip.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;padding:6px 0';
|
|
43
85
|
target.after(strip); badges.set(target, strip);
|
|
44
86
|
}
|
|
45
|
-
strip.replaceChildren(); strip.style.display =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
badge.style.cssText = 'display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border:1px solid #8883;border-radius:8px;font:12px inherit;background:#8881';
|
|
49
|
-
badge.append(collaborationIcon(agent.id, agent.name, 16), document.createTextNode(agent.name));
|
|
50
|
-
badge.title = `@${agent.id}${agent.available ? '' : ' · 未就绪'}`; strip.append(badge);
|
|
51
|
-
}
|
|
52
|
-
for (const session of selectedSessions) {
|
|
87
|
+
strip.replaceChildren(); strip.style.display = hasAny ? 'flex' : 'none';
|
|
88
|
+
if (!perEditor) return;
|
|
89
|
+
for (const entry of perEditor.values()) {
|
|
53
90
|
const badge = document.createElement('span');
|
|
91
|
+
badge.dataset.harnessMixMentionBadge = entry.kind === 'agent' ? `agent:${entry.id}` : `session:${entry.id}`;
|
|
54
92
|
badge.style.cssText = 'display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border:1px solid #8883;border-radius:8px;font:12px inherit;background:#8881';
|
|
55
|
-
|
|
56
|
-
|
|
93
|
+
const iconWrap = document.createElement('span');
|
|
94
|
+
iconWrap.style.cssText = 'display:inline-flex;align-items:center;gap:6px';
|
|
95
|
+
iconWrap.append(
|
|
96
|
+
collaborationIcon(entry.kind === 'agent' ? entry.id : entry.harnessId, entry.kind === 'agent' ? entry.name : entry.title, 16),
|
|
97
|
+
document.createTextNode(entry.kind === 'agent' ? entry.name : entry.title),
|
|
98
|
+
);
|
|
99
|
+
const remove = document.createElement('button');
|
|
100
|
+
remove.type = 'button';
|
|
101
|
+
remove.dataset.harnessMixMentionRemove = entry.kind === 'agent' ? `agent:${entry.id}` : `session:${entry.id}`;
|
|
102
|
+
const removeLabel = entry.kind === 'agent' ? `移除 ${entry.name}` : `移除会话 ${entry.title}`;
|
|
103
|
+
remove.setAttribute('aria-label', removeLabel);
|
|
104
|
+
remove.title = removeLabel;
|
|
105
|
+
remove.textContent = '×';
|
|
106
|
+
remove.style.cssText = 'border:0;background:transparent;color:inherit;cursor:pointer;padding:0 0 0 2px;margin:0;font:600 14px/1 inherit;opacity:.55;line-height:1;display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border-radius:50%';
|
|
107
|
+
remove.addEventListener('mouseenter', () => { remove.style.opacity = '1'; remove.style.background = '#8883'; });
|
|
108
|
+
remove.addEventListener('mouseleave', () => { remove.style.opacity = '.55'; remove.style.background = 'transparent'; });
|
|
109
|
+
remove.addEventListener('mousedown', event => { event.preventDefault(); event.stopPropagation(); });
|
|
110
|
+
remove.addEventListener('click', event => {
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
event.stopPropagation();
|
|
113
|
+
const live = selections.get(target);
|
|
114
|
+
if (!live) return;
|
|
115
|
+
live.delete(entryKey(entry));
|
|
116
|
+
syncBadges(target);
|
|
117
|
+
});
|
|
118
|
+
badge.append(iconWrap, remove);
|
|
119
|
+
badge.title = entry.kind === 'agent' ? `@${entry.id}${entry.available ? '' : ' · 未就绪'}` : `历史会话 · ${entry.cwd}`;
|
|
120
|
+
strip.append(badge);
|
|
57
121
|
}
|
|
58
122
|
};
|
|
59
123
|
const close = () => { generation++; menu.hidden = true; activeComposer?.removeAttribute('data-harness-mix-mention-active'); activeComposer = null; };
|
|
60
124
|
const choose = (entry: MentionEntry) => {
|
|
61
125
|
if (!editor || (entry.kind === 'agent' ? !entry.available || !canDelegate : entry.running === true)) return;
|
|
62
126
|
const target = editor;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const replacement = document.createRange();
|
|
75
|
-
const walker = document.createTreeWalker(target, NodeFilter.SHOW_TEXT);
|
|
76
|
-
let offset = 0, foundStart = false, foundEnd = false;
|
|
77
|
-
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
78
|
-
const length = node.textContent?.length ?? 0;
|
|
79
|
-
if (!foundStart && start <= offset + length) { replacement.setStart(node, start - offset); foundStart = true; }
|
|
80
|
-
if (foundStart && end <= offset + length) { replacement.setEnd(node, end - offset); foundEnd = true; break; }
|
|
81
|
-
offset += length;
|
|
82
|
-
}
|
|
83
|
-
if (!foundEnd) return;
|
|
84
|
-
const selection = window.getSelection();
|
|
85
|
-
selection?.removeAllRanges(); selection?.addRange(replacement);
|
|
86
|
-
document.dispatchEvent(new Event('selectionchange'));
|
|
87
|
-
document.execCommand('insertText', false, inserted);
|
|
88
|
-
}
|
|
89
|
-
// Dismiss the native search opened by the inserted literal @ mention.
|
|
90
|
-
target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true, cancelable: true }));
|
|
127
|
+
close();
|
|
128
|
+
target.focus();
|
|
129
|
+
// Drop the @query trigger text the user typed to open the picker so the
|
|
130
|
+
// editor stays free of literal @agent handles. The chip below carries
|
|
131
|
+
// the same intent and is the only visible signal.
|
|
132
|
+
replaceSelectionWith(target, '');
|
|
133
|
+
// Toggle the entry in the per-editor selections; a second click removes it.
|
|
134
|
+
const perEditor = selectionsFor(target);
|
|
135
|
+
const key = entryKey(entry);
|
|
136
|
+
if (perEditor.has(key)) perEditor.delete(key);
|
|
137
|
+
else perEditor.set(key, entry);
|
|
91
138
|
syncBadges(target);
|
|
92
139
|
};
|
|
93
140
|
const render = () => {
|
|
@@ -198,16 +245,62 @@ export function installHarnessMentions(load: (editor: Element, query: string) =>
|
|
|
198
245
|
}
|
|
199
246
|
};
|
|
200
247
|
const outside = (event: Event) => { if (event.target instanceof Node && !menu.contains(event.target)) close(); };
|
|
248
|
+
|
|
249
|
+
// The Host detects mentions by parsing `@agent` literals out of the submitted
|
|
250
|
+
// text. The composer no longer carries those literals while the user is
|
|
251
|
+
// typing, so before the native form submit reads the editor we re-attach
|
|
252
|
+
// the mentions at the start of the value. The editor then clears itself
|
|
253
|
+
// after Codex Desktop finishes dispatching the turn, hiding the literal
|
|
254
|
+
// text again.
|
|
255
|
+
const findEditorIn = (root: Element | null): HTMLElement | null => {
|
|
256
|
+
if (!root) return null;
|
|
257
|
+
if (root.matches('textarea,[contenteditable="true"]')) return root as HTMLElement;
|
|
258
|
+
return root.querySelector<HTMLElement>('textarea,[contenteditable="true"]');
|
|
259
|
+
};
|
|
260
|
+
const injectMentionsInto = (target: HTMLElement): boolean => {
|
|
261
|
+
const perEditor = selections.get(target);
|
|
262
|
+
if (!perEditor || perEditor.size === 0) return false;
|
|
263
|
+
const text = [...perEditor.values()].map(entryLiteral).join('');
|
|
264
|
+
if (!text) return false;
|
|
265
|
+
target.focus();
|
|
266
|
+
if (target instanceof HTMLTextAreaElement) {
|
|
267
|
+
target.value = `${text}${target.value}`;
|
|
268
|
+
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
269
|
+
target.dispatchEvent(new Event('change', { bubbles: true }));
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
const selection = window.getSelection();
|
|
273
|
+
const fresh = document.createRange();
|
|
274
|
+
fresh.selectNodeContents(target);
|
|
275
|
+
fresh.collapse(true);
|
|
276
|
+
selection?.removeAllRanges();
|
|
277
|
+
selection?.addRange(fresh);
|
|
278
|
+
document.dispatchEvent(new Event('selectionchange'));
|
|
279
|
+
document.execCommand('insertText', false, text);
|
|
280
|
+
return true;
|
|
281
|
+
};
|
|
282
|
+
const onSubmitCapture = (event: Event) => {
|
|
283
|
+
const composerRoot = (event.target instanceof Element ? event.target : null)?.closest?.('[data-codex-composer-root]');
|
|
284
|
+
const candidate = composerRoot ?? (event.target instanceof Element ? event.target : null);
|
|
285
|
+
const editorEl = findEditorIn(candidate);
|
|
286
|
+
if (!editorEl) return;
|
|
287
|
+
if (!editorEl.closest('[data-codex-composer-root]')) return;
|
|
288
|
+
injectMentionsInto(editorEl);
|
|
289
|
+
};
|
|
290
|
+
|
|
201
291
|
document.addEventListener('input', input, true);
|
|
202
292
|
document.addEventListener('keydown', keydown, true);
|
|
203
293
|
document.addEventListener('mousedown', outside);
|
|
294
|
+
document.addEventListener('submit', onSubmitCapture, true);
|
|
204
295
|
window.addEventListener('blur', close);
|
|
205
296
|
return { dispose() {
|
|
206
297
|
disposed = true; close(); menu.remove(); style.remove();
|
|
207
298
|
for (const strip of badges.values()) strip.remove(); badges.clear(); catalogs.clear();
|
|
299
|
+
for (const [target, perEditor] of selections) if (!target.isConnected) selections.delete(target);
|
|
208
300
|
document.removeEventListener('input', input, true);
|
|
209
301
|
document.removeEventListener('keydown', keydown, true);
|
|
210
302
|
document.removeEventListener('mousedown', outside);
|
|
303
|
+
document.removeEventListener('submit', onSubmitCapture, true);
|
|
211
304
|
window.removeEventListener('blur', close);
|
|
212
305
|
} };
|
|
213
|
-
}
|
|
306
|
+
}
|
|
@@ -179,7 +179,8 @@ export interface RendererModelClient extends Partial<RendererSessionImportClient
|
|
|
179
179
|
forkThread(input: ExternalThreadForkParams): Promise<ExternalThreadForkResult>;
|
|
180
180
|
switchHarness(input: ThreadHarnessSwitchParams): Promise<ThreadHarnessSwitchResult>;
|
|
181
181
|
inspectHarness(input: HarnessInspectParams): Promise<HarnessInspection>;
|
|
182
|
-
installHarness?(input: { harnessId: string }): Promise<{ success: boolean; command?: string; stdout?: string; stderr?: string; error?: string }>;
|
|
182
|
+
installHarness?(input: { harnessId: string; terminal?: boolean | undefined }): Promise<{ success: boolean; command?: string; stdout?: string; stderr?: string; error?: string }>;
|
|
183
|
+
loginHarnessAccount?(input: { harnessId: string }): Promise<{ success: boolean; command?: string; error?: string }>;
|
|
183
184
|
openHarnessWebUi?(input: HarnessWebUiOpenParams): Promise<void>;
|
|
184
185
|
inspectThread(input: ThreadInspectionParams): Promise<ThreadInspection>;
|
|
185
186
|
inspectHarnessCommands(input: HarnessCommandsInspectParams): Promise<HarnessCommandCatalog>;
|
|
@@ -356,10 +357,14 @@ export function createRendererModelClient(
|
|
|
356
357
|
return threadDelegationResultSchema.parse(result);
|
|
357
358
|
},
|
|
358
359
|
inspectHarness,
|
|
359
|
-
async installHarness(input: { harnessId: string }): Promise<{ success: boolean; command?: string; stdout?: string; stderr?: string; error?: string }> {
|
|
360
|
+
async installHarness(input: { harnessId: string; terminal?: boolean | undefined }): Promise<{ success: boolean; command?: string; stdout?: string; stderr?: string; error?: string }> {
|
|
360
361
|
const result = await manager.sendRequest(HARNESS_INSTALL_METHOD, input);
|
|
361
362
|
return result as { success: boolean; command?: string; stdout?: string; stderr?: string; error?: string };
|
|
362
363
|
},
|
|
364
|
+
async loginHarnessAccount(input: { harnessId: string }): Promise<{ success: boolean; command?: string; error?: string }> {
|
|
365
|
+
const result = await manager.sendRequest("codexhost/harness/account/login", input);
|
|
366
|
+
return result as { success: boolean; command?: string; error?: string };
|
|
367
|
+
},
|
|
363
368
|
async listHarnessPlugins(): Promise<HarnessPluginListResult> {
|
|
364
369
|
return harnessPluginListResultSchema.parse(
|
|
365
370
|
await manager.sendRequest(HARNESS_PLUGIN_LIST_METHOD, {}),
|
|
@@ -435,13 +435,19 @@ export function mountRendererUsageControl(
|
|
|
435
435
|
trigger.style.fontVariantNumeric = "tabular-nums";
|
|
436
436
|
trigger.style.letterSpacing = "0";
|
|
437
437
|
|
|
438
|
+
const ringSlot = document.createElement("span");
|
|
439
|
+
ringSlot.dataset.codexhostUsageRing = "";
|
|
440
|
+
ringSlot.style.display = "inline-flex";
|
|
441
|
+
ringSlot.style.flex = "0 0 auto";
|
|
442
|
+
ringSlot.style.alignItems = "center";
|
|
443
|
+
|
|
438
444
|
const label = document.createElement("span");
|
|
439
445
|
label.style.display = "inline-block";
|
|
440
446
|
label.style.maxWidth = "100%";
|
|
441
447
|
label.style.overflow = "hidden";
|
|
442
448
|
label.style.textOverflow = "ellipsis";
|
|
443
449
|
label.style.whiteSpace = "nowrap";
|
|
444
|
-
trigger.append(label);
|
|
450
|
+
trigger.append(ringSlot, label);
|
|
445
451
|
|
|
446
452
|
const popover = document.createElement("div");
|
|
447
453
|
popover.id = `${composerId}-usage-popover`;
|
|
@@ -561,7 +567,33 @@ export function renderRendererUsageControl(
|
|
|
561
567
|
return false;
|
|
562
568
|
}
|
|
563
569
|
|
|
570
|
+
const ringSlot = control.trigger.querySelector<HTMLElement>("[data-codexhost-usage-ring]");
|
|
571
|
+
const contextPercent =
|
|
572
|
+
usage?.contextUsagePercent !== undefined
|
|
573
|
+
? usage.contextUsagePercent
|
|
574
|
+
: (hasContext && usage?.contextWindowTokens !== undefined && usage.contextWindowTokens > 0
|
|
575
|
+
? Math.round((100 * (usage.contextUsedTokens ?? 0) / usage.contextWindowTokens) * 10) / 10
|
|
576
|
+
: undefined);
|
|
577
|
+
|
|
578
|
+
if (contextPercent !== undefined) {
|
|
579
|
+
const tone = contextPercent >= 90 ? "hot" : contextPercent >= 70 ? "warn" : "ok";
|
|
580
|
+
const color = tone === "hot" ? "#c45c4a" : tone === "warn" ? "#c9a227" : "#3d9a64";
|
|
581
|
+
if (ringSlot) {
|
|
582
|
+
ringSlot.replaceChildren(
|
|
583
|
+
createRendererUsageRing(contextPercent, {
|
|
584
|
+
size: 14,
|
|
585
|
+
strokeWidth: 2.4,
|
|
586
|
+
color,
|
|
587
|
+
}),
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
} else if (ringSlot) {
|
|
591
|
+
ringSlot.replaceChildren();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const percentLabel = contextPercent !== undefined ? `${decimal(contextPercent, 1)}%` : null;
|
|
564
595
|
const summary = [
|
|
596
|
+
percentLabel,
|
|
565
597
|
usage?.totalCredits !== undefined ? formatRendererCredits(usage.totalCredits) : null,
|
|
566
598
|
cacheHitRatePercent !== undefined ? formatRendererCacheHitRate(cacheHitRatePercent) : null,
|
|
567
599
|
outputTokensPerSecond !== undefined
|
|
@@ -569,8 +601,7 @@ export function renderRendererUsageControl(
|
|
|
569
601
|
: null,
|
|
570
602
|
totalCostUsd !== undefined ? formatRendererCost(totalCostUsd) : null,
|
|
571
603
|
].filter((value): value is string => value !== null);
|
|
572
|
-
|
|
573
|
-
if (contextPercent !== undefined && summary.length === 0) summary.push(messages.usage);
|
|
604
|
+
|
|
574
605
|
if (
|
|
575
606
|
summary.length === 0 &&
|
|
576
607
|
hasContext &&
|