diff --git a/plugins/thread-hover-cards/app.tsx b/plugins/thread-hover-cards/app.tsx index 7fda893..4327513 100644 --- a/plugins/thread-hover-cards/app.tsx +++ b/plugins/thread-hover-cards/app.tsx @@ -14,6 +14,7 @@ import { Loading03Icon, OpenAiIcon, PiIcon, + SecurityCheckIcon, SourceCodeIcon, SquareUnlock02Icon, ViewIcon, @@ -483,6 +484,27 @@ function refreshRunTime(card: HTMLElement): void { } function formatModelLabel(value: string, providerId: string): string { + if (providerId === "claude-code") { + const contextMatch = value.match(/^(.*)\[(\d+(?:\.\d+)?[km])\]$/i); + const modelId = contextMatch?.[1] ?? value; + const context = contextMatch?.[2]?.toUpperCase(); + const formatted = modelId + .replace(/^claude[-_\s]+/i, "") + .split(/[-_]/) + .map((part) => { + if (/^\d+(\.\d+)*$/.test(part)) return part; + if (/^[a-z]+$/i.test(part)) { + return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(); + } + return part; + }) + .join("-") + .replace(/-(\d+)-(\d+)(?=-|$)/, "-$1.$2") + .split("-") + .join(" "); + return context ? `${formatted} (${context})` : formatted; + } + const formatted = value .split("-") .map((part) => { @@ -496,9 +518,6 @@ function formatModelLabel(value: string, providerId: string): string { .join("-"); if (providerId === "codex") return formatted.replace(/^GPT-/i, ""); - if (providerId === "claude-code") { - return formatted.replace(/^Claude\s+/i, ""); - } return formatted; } @@ -526,19 +545,25 @@ function permissionMetadata(summary: ThreadSummary): HTMLSpanElement | null { : summary.permissionMode === "accept-edits" || summary.permissionMode === "workspace-write" ? { definition: FolderEditIcon, name: "FolderEditIcon" } - : { definition: ViewIcon, name: "ViewIcon" }; + : summary.permissionMode === "auto" + ? { definition: SecurityCheckIcon, name: "SecurityCheckIcon" } + : summary.permissionMode === "readonly" + ? { definition: ViewIcon, name: "ViewIcon" } + : null; const access = element("span", "bb-thread-hover-card__access"); access.dataset.permissionMode = summary.permissionMode!; access.setAttribute("aria-label", `Permission: ${label}`); access.title = `Permission: ${label}`; - access.append( - icon( - permissionIcon.definition, - permissionIcon.name, - "bb-thread-hover-card__icon bb-thread-hover-card__permission-icon", - ), - document.createTextNode(label), - ); + if (permissionIcon) { + access.append( + icon( + permissionIcon.definition, + permissionIcon.name, + "bb-thread-hover-card__icon bb-thread-hover-card__permission-icon", + ), + ); + } + access.append(document.createTextNode(label)); return access; } @@ -807,8 +832,8 @@ function renderSummary(card: HTMLElement, summary: ThreadSummary): void { ? REASONING_LABELS[summary.provider.reasoningLevel] : null; provider.title = reasoningLabel - ? `${summary.provider.displayName} · ${modelLabel} · ${reasoningLabel} reasoning` - : `${summary.provider.displayName} · ${modelLabel}`; + ? `${summary.provider.displayName}: ${modelLabel} · ${reasoningLabel} reasoning` + : `${summary.provider.displayName}: ${modelLabel}`; const providerIdentity = element( "div", "bb-thread-hover-card__provider-identity", diff --git a/plugins/thread-hover-cards/dist/app.js b/plugins/thread-hover-cards/dist/app.js index ccd26d2..ff0c961 100644 --- a/plugins/thread-hover-cards/dist/app.js +++ b/plugins/thread-hover-cards/dist/app.js @@ -241,6 +241,30 @@ var SquareUnlock02Icon = [ } ] ]; +var SecurityCheckIcon = [ + [ + "path", + { + d: "M18.7088 3.49534C16.8165 2.55382 14.5009 2 12 2C9.4991 2 7.1835 2.55382 5.29116 3.49534C4.36318 3.95706 3.89919 4.18792 3.4496 4.91378C3 5.63965 3 6.34248 3 7.74814V11.2371C3 16.9205 7.54236 20.0804 10.173 21.4338C10.9067 21.8113 11.2735 22 12 22C12.7265 22 13.0933 21.8113 13.8269 21.4338C16.4576 20.0804 21 16.9205 21 11.2371L21 7.74814C21 6.34249 21 5.63966 20.5504 4.91378C20.1008 4.18791 19.6368 3.95706 18.7088 3.49534Z", + stroke: "currentColor", + strokeLinecap: "round", + strokeLinejoin: "round", + strokeWidth: "1.5", + key: "0" + } + ], + [ + "path", + { + d: "M9 11.5C9 11.5 10.4079 11.7519 11 13.5C11 13.5 12.5 10.5 15 9.5", + stroke: "currentColor", + strokeLinecap: "round", + strokeLinejoin: "round", + strokeWidth: "1.5", + key: "1" + } + ] +]; var FolderEditIcon = [ [ "path", @@ -613,7 +637,7 @@ var HOVER_CARD_CSS = String.raw` .bb-thread-hover-card__provider { flex: 1 1 auto; - gap: 0.3125rem; + gap: 0.25rem; color: var(--muted-foreground); } @@ -625,10 +649,20 @@ var HOVER_CARD_CSS = String.raw` overflow: hidden; } -.bb-thread-hover-card__reasoning { +.bb-thread-hover-card__provider-model, +.bb-thread-hover-card__reasoning, +.bb-thread-hover-card__access { + font-size: 0.75rem; + line-height: 1.25; +} + +.bb-thread-hover-card__reasoning, +.bb-thread-hover-card__access { flex: none; - color: color-mix(in srgb, var(--muted-foreground) 76%, transparent); - font-size: 0.625rem; + color: var( + --subtle-foreground, + color-mix(in srgb, var(--muted-foreground) 76%, transparent) + ); white-space: nowrap; } @@ -715,9 +749,9 @@ var HOVER_CARD_CSS = String.raw` } .bb-thread-hover-card__provider-icon { - width: 0.8125rem; - height: 0.8125rem; - color: color-mix(in srgb, var(--muted-foreground) 82%, transparent); + width: 1rem; + height: 1rem; + color: var(--muted-foreground); object-fit: contain; } @@ -821,19 +855,22 @@ var HOVER_CARD_CSS = String.raw` } .bb-thread-hover-card__access { - flex: none; gap: 0.1875rem; - color: color-mix(in srgb, var(--muted-foreground) 76%, transparent); - font-size: 0.625rem; - white-space: nowrap; + margin-left: 0.25rem; } .bb-thread-hover-card__permission-icon { - width: 0.625rem; - height: 0.625rem; + width: 0.75rem; + height: 0.75rem; color: currentColor; } +.bb-thread-hover-card__access[data-permission-mode="accept-edits"], +.bb-thread-hover-card__access[data-permission-mode="workspace-write"], +.bb-thread-hover-card__access[data-permission-mode="auto"] { + color: color-mix(in srgb, var(--muted-foreground) 72%, transparent); +} + .bb-thread-hover-card__access[data-permission-mode="full"] { color: color-mix( in srgb, @@ -1492,6 +1529,19 @@ function refreshRunTime(card) { } } function formatModelLabel(value, providerId) { + if (providerId === "claude-code") { + const contextMatch = value.match(/^(.*)\[(\d+(?:\.\d+)?[km])\]$/i); + const modelId = contextMatch?.[1] ?? value; + const context = contextMatch?.[2]?.toUpperCase(); + const formatted2 = modelId.replace(/^claude[-_\s]+/i, "").split(/[-_]/).map((part) => { + if (/^\d+(\.\d+)*$/.test(part)) return part; + if (/^[a-z]+$/i.test(part)) { + return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(); + } + return part; + }).join("-").replace(/-(\d+)-(\d+)(?=-|$)/, "-$1.$2").split("-").join(" "); + return context ? `${formatted2} (${context})` : formatted2; + } const formatted = value.split("-").map((part) => { if (part.toLowerCase() === "gpt") return "GPT"; if (/^\d+(\.\d+)*$/.test(part)) return part; @@ -1501,9 +1551,6 @@ function formatModelLabel(value, providerId) { return part; }).join("-"); if (providerId === "codex") return formatted.replace(/^GPT-/i, ""); - if (providerId === "claude-code") { - return formatted.replace(/^Claude\s+/i, ""); - } return formatted; } function permissionLabel(permissionMode) { @@ -1520,19 +1567,21 @@ function permissionMetadata(summary) { const permissionIcon = summary.permissionMode === "full" ? { definition: SquareUnlock02Icon, name: "SquareUnlock02Icon" - } : summary.permissionMode === "accept-edits" || summary.permissionMode === "workspace-write" ? { definition: FolderEditIcon, name: "FolderEditIcon" } : { definition: ViewIcon, name: "ViewIcon" }; + } : summary.permissionMode === "accept-edits" || summary.permissionMode === "workspace-write" ? { definition: FolderEditIcon, name: "FolderEditIcon" } : summary.permissionMode === "auto" ? { definition: SecurityCheckIcon, name: "SecurityCheckIcon" } : summary.permissionMode === "readonly" ? { definition: ViewIcon, name: "ViewIcon" } : null; const access = element("span", "bb-thread-hover-card__access"); access.dataset.permissionMode = summary.permissionMode; access.setAttribute("aria-label", `Permission: ${label}`); access.title = `Permission: ${label}`; - access.append( - icon( - permissionIcon.definition, - permissionIcon.name, - "bb-thread-hover-card__icon bb-thread-hover-card__permission-icon" - ), - document.createTextNode(label) - ); + if (permissionIcon) { + access.append( + icon( + permissionIcon.definition, + permissionIcon.name, + "bb-thread-hover-card__icon bb-thread-hover-card__permission-icon" + ) + ); + } + access.append(document.createTextNode(label)); return access; } function nextInlinePattern(source) { @@ -1737,7 +1786,7 @@ function renderSummary(card, summary) { summary.provider.id ); const reasoningLabel = summary.provider.reasoningLevel ? REASONING_LABELS[summary.provider.reasoningLevel] : null; - provider.title = reasoningLabel ? `${summary.provider.displayName} \xB7 ${modelLabel} \xB7 ${reasoningLabel} reasoning` : `${summary.provider.displayName} \xB7 ${modelLabel}`; + provider.title = reasoningLabel ? `${summary.provider.displayName}: ${modelLabel} \xB7 ${reasoningLabel} reasoning` : `${summary.provider.displayName}: ${modelLabel}`; const providerIdentity = element( "div", "bb-thread-hover-card__provider-identity" diff --git a/plugins/thread-hover-cards/icons.ts b/plugins/thread-hover-cards/icons.ts index b483d7c..6b4e384 100644 --- a/plugins/thread-hover-cards/icons.ts +++ b/plugins/thread-hover-cards/icons.ts @@ -224,6 +224,31 @@ export const SquareUnlock02Icon = [ ], ] as const; +export const SecurityCheckIcon = [ + [ + "path", + { + d: "M18.7088 3.49534C16.8165 2.55382 14.5009 2 12 2C9.4991 2 7.1835 2.55382 5.29116 3.49534C4.36318 3.95706 3.89919 4.18792 3.4496 4.91378C3 5.63965 3 6.34248 3 7.74814V11.2371C3 16.9205 7.54236 20.0804 10.173 21.4338C10.9067 21.8113 11.2735 22 12 22C12.7265 22 13.0933 21.8113 13.8269 21.4338C16.4576 20.0804 21 16.9205 21 11.2371L21 7.74814C21 6.34249 21 5.63966 20.5504 4.91378C20.1008 4.18791 19.6368 3.95706 18.7088 3.49534Z", + stroke: "currentColor", + strokeLinecap: "round", + strokeLinejoin: "round", + strokeWidth: "1.5", + key: "0", + }, + ], + [ + "path", + { + d: "M9 11.5C9 11.5 10.4079 11.7519 11 13.5C11 13.5 12.5 10.5 15 9.5", + stroke: "currentColor", + strokeLinecap: "round", + strokeLinejoin: "round", + strokeWidth: "1.5", + key: "1", + }, + ], +] as const; + export const FolderEditIcon = [ [ "path", diff --git a/plugins/thread-hover-cards/styles.ts b/plugins/thread-hover-cards/styles.ts index eed88c2..b2cdd97 100644 --- a/plugins/thread-hover-cards/styles.ts +++ b/plugins/thread-hover-cards/styles.ts @@ -77,7 +77,7 @@ export const HOVER_CARD_CSS = String.raw` .bb-thread-hover-card__provider { flex: 1 1 auto; - gap: 0.3125rem; + gap: 0.25rem; color: var(--muted-foreground); } @@ -89,10 +89,20 @@ export const HOVER_CARD_CSS = String.raw` overflow: hidden; } -.bb-thread-hover-card__reasoning { +.bb-thread-hover-card__provider-model, +.bb-thread-hover-card__reasoning, +.bb-thread-hover-card__access { + font-size: 0.75rem; + line-height: 1.25; +} + +.bb-thread-hover-card__reasoning, +.bb-thread-hover-card__access { flex: none; - color: color-mix(in srgb, var(--muted-foreground) 76%, transparent); - font-size: 0.625rem; + color: var( + --subtle-foreground, + color-mix(in srgb, var(--muted-foreground) 76%, transparent) + ); white-space: nowrap; } @@ -179,9 +189,9 @@ export const HOVER_CARD_CSS = String.raw` } .bb-thread-hover-card__provider-icon { - width: 0.8125rem; - height: 0.8125rem; - color: color-mix(in srgb, var(--muted-foreground) 82%, transparent); + width: 1rem; + height: 1rem; + color: var(--muted-foreground); object-fit: contain; } @@ -285,19 +295,22 @@ export const HOVER_CARD_CSS = String.raw` } .bb-thread-hover-card__access { - flex: none; gap: 0.1875rem; - color: color-mix(in srgb, var(--muted-foreground) 76%, transparent); - font-size: 0.625rem; - white-space: nowrap; + margin-left: 0.25rem; } .bb-thread-hover-card__permission-icon { - width: 0.625rem; - height: 0.625rem; + width: 0.75rem; + height: 0.75rem; color: currentColor; } +.bb-thread-hover-card__access[data-permission-mode="accept-edits"], +.bb-thread-hover-card__access[data-permission-mode="workspace-write"], +.bb-thread-hover-card__access[data-permission-mode="auto"] { + color: color-mix(in srgb, var(--muted-foreground) 72%, transparent); +} + .bb-thread-hover-card__access[data-permission-mode="full"] { color: color-mix( in srgb, diff --git a/plugins/thread-hover-cards/test/app.test.mjs b/plugins/thread-hover-cards/test/app.test.mjs index 51beb9e..256bdfc 100644 --- a/plugins/thread-hover-cards/test/app.test.mjs +++ b/plugins/thread-hover-cards/test/app.test.mjs @@ -211,6 +211,8 @@ globalThis.fetch = async (url, init) => { ); } const isLocal = request.threadId === "thr_local"; + const isClaude = request.threadId === "thr_claude"; + const isClaudeVersion = request.threadId === "thr_claude_version"; const hasNoPullRequest = request.threadId === "thr_no_pr"; const pullRequestUnavailable = request.threadId === "thr_pr_unavailable"; const isDraftPullRequest = request.threadId === "thr_draft_pr"; @@ -393,7 +395,7 @@ globalThis.fetch = async (url, init) => { : hasNoPullRequest ? null : "**Agent update**—implementing concise hover cards for foo_bar_baz and \\_literal\\_", - permissionMode: isLocal + permissionMode: isLocal || isClaude || isClaudeVersion ? "auto" : isDraftPullRequest ? "accept-edits" @@ -403,11 +405,15 @@ globalThis.fetch = async (url, init) => { ? { kind: "absent" } : { kind: "pending" }, provider: { - displayName: "Codex", - id: "codex", + displayName: isClaude || isClaudeVersion ? "Claude" : "Codex", + id: isClaude || isClaudeVersion ? "claude-code" : "codex", logoUrl: null, - model: "GPT-5.6-Sol", - reasoningLevel: "xhigh", + model: isClaudeVersion + ? "claude-opus-4-8[1m]" + : isClaude + ? "claude-sonnet-5" + : "gpt-5.6-sol", + reasoningLevel: isClaude || isClaudeVersion ? "medium" : "xhigh", }, repository: isLocal ? { @@ -516,6 +522,18 @@ assert.match( style.textContent, /\.bb-thread-hover-card__provider-model\.bb-thread-hover-card__truncate \{[\s\S]*?flex: 0 1 auto/, ); +assert.match( + style.textContent, + /\.bb-thread-hover-card__provider-model,[\s\S]*?\.bb-thread-hover-card__reasoning,[\s\S]*?\.bb-thread-hover-card__access \{[\s\S]*?font-size: 0\.75rem;[\s\S]*?line-height: 1\.25/, +); +assert.match( + style.textContent, + /\.bb-thread-hover-card__reasoning,[\s\S]*?\.bb-thread-hover-card__access \{[\s\S]*?--subtle-foreground/, +); +assert.match( + style.textContent, + /\.bb-thread-hover-card__provider-icon \{[\s\S]*?width: 1rem;[\s\S]*?height: 1rem;[\s\S]*?color: var\(--muted-foreground\)/, +); assert.doesNotMatch(style.textContent, /--font-mono/); assert.match(style.textContent, /\.bb-thread-hover-card__context/); assert.match( @@ -541,7 +559,19 @@ assert.match( assert.match(style.textContent, /\.bb-thread-hover-card__pr-status/); assert.match( style.textContent, - /\.bb-thread-hover-card__access \{[\s\S]*?flex: none;[\s\S]*?white-space: nowrap/, + /\.bb-thread-hover-card__reasoning,[\s\S]*?\.bb-thread-hover-card__access \{[\s\S]*?flex: none;[\s\S]*?--subtle-foreground,[\s\S]*?white-space: nowrap/, +); +assert.match( + style.textContent, + /\.bb-thread-hover-card__access \{[\s\S]*?gap: 0\.1875rem;[\s\S]*?margin-left: 0\.25rem/, +); +assert.match( + style.textContent, + /\.bb-thread-hover-card__permission-icon \{[\s\S]*?width: 0\.75rem;[\s\S]*?height: 0\.75rem/, +); +assert.match( + style.textContent, + /\.bb-thread-hover-card__access\[data-permission-mode="accept-edits"\],[\s\S]*?\.bb-thread-hover-card__access\[data-permission-mode="auto"\] \{[\s\S]*?var\(--muted-foreground\) 72%, transparent/, ); assert.match( style.textContent, @@ -658,6 +688,10 @@ assert.equal( card.querySelector(".bb-thread-hover-card__reasoning")?.textContent, "Extra High", ); +assert.equal( + card.querySelector(".bb-thread-hover-card__provider")?.title, + "Codex: 5.6-Sol · Extra High reasoning", +); assert.equal( card.querySelector(".bb-thread-hover-card__provider-model")?.parentElement, card.querySelector(".bb-thread-hover-card__provider-identity"), @@ -1067,10 +1101,12 @@ assert.equal( card.querySelector(".bb-thread-hover-card__access")?.textContent, "Auto", ); -assert.ok( +assert.equal( card .querySelector(".bb-thread-hover-card__access") - ?.querySelector('[data-icon="ViewIcon"]'), + ?.querySelector("[data-icon]") + ?.getAttribute("data-icon"), + "SecurityCheckIcon", ); assert.equal( card.querySelector(".bb-thread-hover-card__access")?.parentElement, @@ -1933,6 +1969,55 @@ assert.equal( false, ); +nestedThread.dispatchEvent( + new window.PointerEvent("pointerout", { + bubbles: true, + pointerType: "mouse", + relatedTarget: window.document.body, + }), +); +await new Promise((resolve) => setTimeout(resolve, 140)); +nestedThread.dataset.sidebarThreadId = "thr_claude"; +hoverOver(nestedThread); +await new Promise((resolve) => setTimeout(resolve, 20)); +assert.equal( + window.document.querySelector(".bb-thread-hover-card__provider-model") + ?.textContent, + "Sonnet 5", +); +assert.equal( + window.document.querySelector(".bb-thread-hover-card__reasoning")?.textContent, + "Medium", +); +assert.equal( + window.document.querySelector(".bb-thread-hover-card__access")?.textContent, + "Auto", +); +assert.equal( + window.document + .querySelector(".bb-thread-hover-card__access") + ?.querySelector("[data-icon]") + ?.getAttribute("data-icon"), + "SecurityCheckIcon", +); + +nestedThread.dispatchEvent( + new window.PointerEvent("pointerout", { + bubbles: true, + pointerType: "mouse", + relatedTarget: window.document.body, + }), +); +await new Promise((resolve) => setTimeout(resolve, 140)); +nestedThread.dataset.sidebarThreadId = "thr_claude_version"; +hoverOver(nestedThread); +await new Promise((resolve) => setTimeout(resolve, 20)); +assert.equal( + window.document.querySelector(".bb-thread-hover-card__provider-model") + ?.textContent, + "Opus 4.8 (1M)", +); + globalThis.__bbThreadHoverCards?.dispose(); assert.equal(window.document.getElementById("bb-thread-hover-card-styles"), null); assert.equal(window.document.getElementById("bb-section-hover-card-styles"), null);