mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(studio): the session overview and context details cards follow the board's content, not only its sizes
Overview: four figures (requests with the main/sub split, cost, tokens, cache hit), each source as a bar with its cost, and a footer with the work time and a link to the context details; the context window is no longer drawn twice. Context details: the compact action, the MCP count and the manage link share one footer row instead of a server list, and a row with a breakdown shows its figure and a chevron, not a bordered pill.
This commit is contained in:
1 parent
de226cae50
commit
ff1ba768f0
7 files changed
+75
-80
No files matched your search
@@ -159,7 +159,6 @@ export const COMPOSER: Messages = {
|
||||
"composer.no_git_repository": { zh: "非 Git 仓库", en: "No Git repository" },
|
||||
"composer.no_images": { zh: "不发送图片", en: "No images" },
|
||||
"composer.no_matching_branches": { zh: "没有匹配的分支", en: "No matching branches" },
|
||||
"composer.no_mcp_services_enabled": { zh: "尚未启用 MCP 服务", en: "No MCP services enabled" },
|
||||
"composer.no_model_service_is_connected": { zh: "还没有连接模型服务,发不出去", en: "No model service is connected, so nothing can be sent" },
|
||||
"composer.no_models_match": { zh: "没有匹配的模型。", en: "No models match." },
|
||||
"composer.no_observable_progress_for_n": { zh: "已连续 {n} 轮没有可观察的进展", en: "No observable progress for {n} rounds" },
|
||||
|
||||
@@ -70,7 +70,6 @@ export const SESSION: Messages = {
|
||||
"session.folder_is_gone": { zh: "文件夹已不在磁盘上", en: "Folder is gone" },
|
||||
"session.folder_path_to_add": { zh: "要添加的文件夹路径", en: "Folder path to add" },
|
||||
"session.folders_are_managed_in_the": { zh: "在左栏管理文件夹:底部添加,展开后可新建会话。会话归属于创建它的文件夹,不会移动到其他位置。", en: "Folders are managed in the left column: add at the bottom, expand to start a session. A session belongs to the folder it was created in and does not move." },
|
||||
"session.folds_automatically_at_n": { zh: "自动压缩于 {n}", en: "Folds automatically at {n}" },
|
||||
"session.generate_a_title_from_the": { zh: "根据全部用户消息拼接后的最后 300 个字符生成标题", en: "Generate a title from the last 300 characters of all user messages joined together" },
|
||||
"session.goal_evaluation": { zh: "目标评估", en: "Goal evaluation" },
|
||||
"session.handing_over_the_task_and": { zh: "任务和附件正在交接…", en: "Handing over the task and attachments…" },
|
||||
@@ -84,6 +83,7 @@ export const SESSION: Messages = {
|
||||
"session.loading": { zh: "正在加载…", en: "Loading…" },
|
||||
"session.local_workspace": { zh: "本地工作区", en: "Local workspace" },
|
||||
"session.main_agent": { zh: "主代理", en: "Main agent" },
|
||||
"session.main_n_sub_m": { zh: "主代理 {a} · 子代理 {b}", en: "Main agent {a} · subagents {b}" },
|
||||
"session.model_responses_that_reported_usage": { zh: "返回了用量与价格的模型响应", en: "Model responses that reported usage and a price" },
|
||||
"session.move": { zh: "移动", en: "Move" },
|
||||
"session.move_name_to_another_project": { zh: "把「{name}」移到另一个项目", en: "Move \"{name}\" to another project" },
|
||||
@@ -209,7 +209,6 @@ export const SESSION: Messages = {
|
||||
"session.waiting_for_confirmation": { zh: "正在等待状态确认", en: "Waiting for confirmation" },
|
||||
"session.waiting_n_s": { zh: "已等待 {n} 秒", en: "Waiting {n}s" },
|
||||
"session.wallet_and_usage": { zh: "钱包与用量", en: "Wallet and usage" },
|
||||
"session.what_the_next_request_would": { zh: "下一次请求会带上的内容,是当前值,不是累计", en: "What the next request would carry: a current reading, not a running total" },
|
||||
"session.what_the_old_project_allowed": { zh: "原项目的权限与目标设置不会跟着走,新项目按它自己的规则重新判断。不随会话移动的内容会从原项目移除。", en: "What the old project allowed does not travel; the new project decides that by its own rules. What stays behind is removed from the old project." },
|
||||
"session.where_it_runs": { zh: "运行环境", en: "Where it runs" },
|
||||
"session.where_this_session_writes": { zh: "会话写入位置", en: "Where this session writes" },
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
.studio-overview-grid dt { color: var(--ghost); font: var(--w-reg) var(--fs-micro)/1.3 var(--ui); }
|
||||
.studio-overview-grid dd { margin: 0; color: var(--text); font: var(--w-med) var(--fs-ui)/1.2 var(--mono); }
|
||||
.studio-overview-grid small { color: var(--ghost); font: var(--w-reg) var(--fs-micro)/1.4 var(--ui); }
|
||||
.studio-overview-row { display: grid; grid-template-columns: minmax(0, 1.3fr) 3.2ch 5ch 4.5ch minmax(0, 1.2fr); align-items: center; gap: 8px; min-height: 22px; padding: 0 5px; border-radius: var(--r-xs); }
|
||||
.studio-overview-row:hover { background: var(--overlay); }
|
||||
.studio-overview-row { display: grid; grid-template-columns: 64px minmax(0, 1fr) auto; align-items: center; gap: 8px; min-height: 26px; padding: 4px 0; }
|
||||
.studio-overview-row > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
|
||||
.studio-overview-row > b { color: var(--muted); font: var(--w-reg) var(--fs-micro)/1 var(--mono); text-align: right; }
|
||||
.studio-overview-row > small { text-align: right; font-family: var(--mono); }
|
||||
.studio-overview-row > b { min-width: 52px; color: var(--muted); font: var(--w-reg) var(--fs-meta)/1 var(--ui); text-align: right; }
|
||||
.studio-overview-row > i { display: block; height: 6px; overflow: hidden; border-radius: var(--r-xs); background: var(--overlay); }
|
||||
.studio-overview-row > i > u { display: block; height: 100%; background: var(--net); text-decoration: none; }
|
||||
@media (hover: hover) {
|
||||
.studio-overview-anchor:hover .studio-overview-popover { opacity: 1; visibility: visible; pointer-events: auto; transform: none; transition-delay: 0s; }
|
||||
.studio-meterrail:has(.studio-context-anchor[data-open]) .studio-overview-anchor:not([data-open]):hover .studio-overview-popover { opacity: 0; visibility: hidden; pointer-events: none; }
|
||||
@@ -82,3 +82,16 @@
|
||||
.errbar[data-cause="root-missing"] > span > b { font-weight: var(--w-reg); }
|
||||
.errbar[data-cause="root-missing"] > span > small { color: var(--muted); font: var(--w-reg) var(--fs-meta)/1.4 var(--ui); }
|
||||
.errbar[data-cause="root-missing"] > button { height: 32px; padding: 0 14px; border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--raised); color: var(--text); font: var(--w-reg) var(--fs-ui) var(--ui); text-decoration: none; white-space: nowrap; }
|
||||
.studio-overview-foot { display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 10px 14px; border-top: 1px solid var(--hair); color: var(--muted); font: var(--w-reg) var(--fs-meta)/1.3 var(--ui); }
|
||||
.studio-overview-foot > button { margin-left: auto; padding: 0; border: 0; background: none; color: var(--accent-ink); font: inherit; cursor: pointer; }
|
||||
.studio-overview-foot > button:hover { text-decoration: underline; text-underline-offset: 2px; }
|
||||
.chrome-context-card:not(.studio-overview-card) > .chrome-context-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; padding: 10px 14px; border-top: 1px solid var(--hair); color: var(--muted); font: var(--w-reg) var(--fs-meta)/1.3 var(--ui); }
|
||||
.chrome-context-foot > span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.chrome-context-foot > .manage { padding: 0; border: 0; background: none; color: var(--accent-ink); font: inherit; cursor: pointer; }
|
||||
.chrome-context-foot > .manage:hover { text-decoration: underline; text-underline-offset: 2px; }
|
||||
.chrome-context-foot .compact-now:has(.wsconfirm) { order: 5; flex: 1 0 100%; }
|
||||
.chrome-context-foot .compact-now > .btn { height: 24px; padding: 0 10px; border: 1px solid transparent; border-radius: var(--r-pill); background: transparent; color: var(--muted); font: var(--w-reg) var(--fs-meta)/1 var(--ui); }
|
||||
.chrome-context-foot .compact-now > .btn:hover { background: var(--overlay); color: var(--text); }
|
||||
.chrome-context-card .chrome-context-more { display: inline-flex; align-items: center; gap: 5px; height: auto; padding: 0; border: 0; background: none; cursor: pointer; }
|
||||
.chrome-context-card .chrome-context-more::after { content: ""; width: 5px; height: 5px; border-top: 1.45px solid var(--faint); border-right: 1.45px solid var(--faint); transform: rotate(45deg); }
|
||||
.chrome-context-card .chrome-context-more[aria-expanded="true"]::after { transform: rotate(135deg); }
|
||||
@@ -57,13 +57,13 @@ export function ContextSummaryCard({ context, mcp, percent, onManage, compact: a
|
||||
)}
|
||||
</>
|
||||
) : <p>{t("composer.no_context_data_yet")}</p>}
|
||||
{action && <CompactNow action={action} usage={context ? { used: context.used, window: context.window } : undefined} />}
|
||||
<section>
|
||||
<div><b>MCP</b><small>{ready.length} / {active.length} {t("composer.connected")} · {tools} {t("composer.tools")}</small></div>
|
||||
{active.slice(0, 4).map((server) => <div className="chrome-mcp-row" key={server.name}><i data-ready={server.state === "ready" ? "" : undefined} /><span>{server.name}</span><small>{server.state === "ready" ? `${server.tools} ${t("composer.tools")}` : t("composer.waiting_to_connect")}</small></div>)}
|
||||
{!active.length && <p>{t("composer.no_mcp_services_enabled")}</p>}
|
||||
<button data-action="settings.section" data-value="ext" onClick={onManage}>{t("composer.manage_mcp_and_tools")}</button>
|
||||
</section>
|
||||
<footer className="chrome-context-foot">
|
||||
<span title={active.map((server) => `${server.name} · ${server.state === "ready" ? `${server.tools || 0} ${t("composer.tools")}` : t("composer.waiting_to_connect")}`).join("\n") || undefined}>
|
||||
MCP · {ready.length} / {active.length} {t("composer.connected")} · {tools} {t("composer.tools")}
|
||||
</span>
|
||||
{action && <CompactNow action={action} usage={context ? { used: context.used, window: context.window } : undefined} />}
|
||||
<button type="button" className="manage" data-action="settings.section" data-value="ext" onClick={onManage}>{t("composer.manage_mcp_and_tools")}</button>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -67,10 +67,15 @@ describe("the session overview behind the cache meter", () => {
|
||||
const view = within(card());
|
||||
expect(view.getByText("自本次打开此会话起;恢复或分叉会重新计数")).toBeTruthy();
|
||||
expect(view.getByText("请求").closest("div")!.textContent).toContain("3");
|
||||
expect(view.getByText("请求").closest("div")!.textContent).toContain("返回了用量与价格的模型响应");
|
||||
expect(view.getByText("请求").closest("div")!.textContent).toContain("主代理 2 · 子代理 0");
|
||||
expect(view.getByText("Token").closest("div")!.textContent).toContain("8.5k");
|
||||
expect(view.getByText("工作时长").closest("div")!.textContent).toContain("1 分 12 秒");
|
||||
expect(view.getByText("工作时长").closest("div")!.textContent).toContain("含审批与工具等待");
|
||||
});
|
||||
|
||||
it("puts the work time in the footer, with what it adds up on hover", () => {
|
||||
rail();
|
||||
const foot = card().querySelector(".studio-overview-foot")!;
|
||||
expect(foot.textContent).toContain("工作时长 1 分 12 秒");
|
||||
expect(foot.querySelector("span")?.getAttribute("title")).toContain("含审批与工具等待");
|
||||
});
|
||||
|
||||
it("states the hit rate over what the endpoint split, and says what it left out", () => {
|
||||
@@ -90,12 +95,13 @@ describe("the session overview behind the cache meter", () => {
|
||||
|
||||
it("lists each source with its own cache and cost, and an unpriced one as unknown", () => {
|
||||
rail();
|
||||
const rows = [...card().querySelectorAll(".studio-overview-row")].map((r) => r.textContent);
|
||||
const lines = [...card().querySelectorAll(".studio-overview-row")];
|
||||
const rows = lines.map((r) => r.textContent);
|
||||
expect(rows[0]).toContain("压缩");
|
||||
expect(rows[0]).toContain("—");
|
||||
expect(rows[1]).toContain("主代理");
|
||||
expect(rows[1]).toContain("76%");
|
||||
expect(rows[1]).toContain("¥3.20");
|
||||
expect(lines[1]!.getAttribute("title")).toContain("76%");
|
||||
const price = card().querySelectorAll(".studio-overview-row")[0]!.querySelector("b[title]");
|
||||
expect(price?.getAttribute("title")).toBe("价格未知:no_rate");
|
||||
});
|
||||
@@ -130,30 +136,16 @@ describe("the session overview behind the cache meter", () => {
|
||||
expect(button().textContent).toContain("76%");
|
||||
});
|
||||
|
||||
it("shows the context window in the same card, as the next request's content and not a running total", () => {
|
||||
it("does not repeat the context window, which has a card of its own", () => {
|
||||
rail();
|
||||
const section = within(card()).getByRole("region", { name: "上下文窗口" });
|
||||
expect(section.textContent).toContain("42k");
|
||||
expect(section.textContent).toContain("128k");
|
||||
expect(section.textContent).toContain("33%");
|
||||
expect(section.textContent).toContain("下一次请求会带上的内容,是当前值,不是累计");
|
||||
expect(section.querySelectorAll("[data-part]").length).toBe(5);
|
||||
});
|
||||
|
||||
it("states the automatic fold line only when the kernel has one", () => {
|
||||
rail();
|
||||
expect(within(card()).getByRole("region", { name: "上下文窗口" }).textContent).toContain("自动压缩于 96k");
|
||||
cleanup();
|
||||
rail({ ctx: { ...context, compact_at: 0 } });
|
||||
expect(within(card()).getByRole("region", { name: "上下文窗口" }).textContent).not.toContain("自动压缩于");
|
||||
});
|
||||
|
||||
it("keeps the window section when no request was billed, and omits it when the kernel sent none", () => {
|
||||
rail({ usage: { ...usage, requests: 0, sources: [] } });
|
||||
expect(within(card()).getByRole("region", { name: "上下文窗口" })).toBeTruthy();
|
||||
cleanup();
|
||||
rail({ ctx: null });
|
||||
expect(within(card()).queryByRole("region", { name: "上下文窗口" })).toBeNull();
|
||||
expect(card().querySelector("[data-part]")).toBeNull();
|
||||
});
|
||||
|
||||
it("draws each source as a bar sized by what it cost", () => {
|
||||
rail();
|
||||
const bars = [...card().querySelectorAll(".studio-overview-row > i > u")].map((e) => (e as HTMLElement).style.width);
|
||||
expect(bars).toEqual(["0%", "100%"]);
|
||||
});
|
||||
|
||||
it("hands over to the context details without leaving both cards open", async () => {
|
||||
|
||||
@@ -92,7 +92,7 @@ export function MeterRail({ tps, trail, running, speed, metrics, ctx, mcp, compa
|
||||
<button className="studio-meter-cache" type="button" data-action="metrics.details" data-value="overview" aria-expanded={overviewOpen} aria-haspopup="dialog" aria-label={t("composer.open_the_session_overview")} title={cacheRate === null ? t("composer.no_cache_data_yet") : t("composer.hit_hit_miss_miss", { hit: metrics.hit.toLocaleString(), miss: metrics.miss.toLocaleString() })} onClick={() => { setMeterOpen(false); setOverviewOpen((open) => !open); }}>
|
||||
<StudioIcon name="bolt" /><span>{t("composer.cache")}</span><b>{cacheRate === null ? "—" : `${cacheRate}%`}</b>
|
||||
</button>
|
||||
<SessionOverviewCard className="studio-overview-popover" usage={usage} quote={quote} ctx={ctx} hideAmounts={hideAmounts} onContext={ctx && ctx.window > 0 ? () => { setOverviewOpen(false); setMeterOpen(true); } : undefined} />
|
||||
<SessionOverviewCard className="studio-overview-popover" usage={usage} quote={quote} hideAmounts={hideAmounts} onContext={ctx && ctx.window > 0 ? () => { setOverviewOpen(false); setMeterOpen(true); } : undefined} />
|
||||
</div>
|
||||
{ctx && ctx.window > 0 && (
|
||||
<div className="studio-context-anchor" data-open={meterOpen ? "" : undefined}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { t } from "../i18n";
|
||||
import { money } from "../i18n/format";
|
||||
import type { ContextBreakdown } from "../port/shell";
|
||||
import type { SessionUsage } from "../port/session";
|
||||
import type { CostQuote } from "../port/wire";
|
||||
import { cacheRate, costSay, sessionTokens, sourceTokens, type CostSay } from "../state/overview";
|
||||
@@ -48,9 +47,8 @@ function cost(say: CostSay, hide: boolean): { text: string; note: string } {
|
||||
// The session as the kernel's ledger holds it, each figure beside what it counts.
|
||||
// Everything here is since this session was opened in this run: a resumed or
|
||||
// forked session counts again from the start.
|
||||
export function SessionOverviewCard({ usage, quote, ctx, hideAmounts, onContext, className = "" }: {
|
||||
export function SessionOverviewCard({ usage, quote, hideAmounts, onContext, className = "" }: {
|
||||
usage?: SessionUsage;
|
||||
ctx?: ContextBreakdown | null;
|
||||
onContext?: () => void;
|
||||
quote?: CostQuote;
|
||||
hideAmounts: boolean;
|
||||
@@ -58,6 +56,10 @@ export function SessionOverviewCard({ usage, quote, ctx, hideAmounts, onContext,
|
||||
}) {
|
||||
const total = cost(costSay(quote), hideAmounts);
|
||||
const unknown = usage?.cacheUnknown ?? 0;
|
||||
const sources = usage?.sources ?? [];
|
||||
const asked = (id: string) => sources.find((src) => src.source === id)?.requests ?? 0;
|
||||
const priced = sources.map((src) => { const say = costSay(src.cost); return say.kind === "exact" ? say.amount : 0; });
|
||||
const pricedTotal = priced.reduce((n, v) => n + v, 0);
|
||||
return (
|
||||
<div className={["chrome-context-card", "studio-overview-card", className].filter(Boolean).join(" ")} role="dialog" aria-label={t("session.session_overview")}>
|
||||
<header><span><b>{t("session.session_overview")}</b><small>{t("session.since_this_session_was_opened")}</small></span></header>
|
||||
@@ -66,49 +68,39 @@ export function SessionOverviewCard({ usage, quote, ctx, hideAmounts, onContext,
|
||||
) : (
|
||||
<>
|
||||
<dl className="studio-overview-grid">
|
||||
<div><dt>{t("common.requests")}</dt><dd>{usage.requests}</dd><small>{t("session.model_responses_that_reported_usage")}</small></div>
|
||||
<div>
|
||||
<dt>{t("common.requests")}</dt><dd>{usage.requests}</dd>
|
||||
<small>{asked("executor") + asked("subagent") > 0 ? t("session.main_n_sub_m", { a: asked("executor"), b: asked("subagent") }) : t("session.model_responses_that_reported_usage")}</small>
|
||||
</div>
|
||||
<div><dt>{t("session.cost")}</dt><dd>{total.text}</dd>{total.note && <small>{total.note}</small>}</div>
|
||||
<div><dt>{t("Token")}</dt><dd>{tokens(sessionTokens(usage))}</dd><small>{t("session.in_a_out_b", { a: tokens(usage.promptTokens), b: tokens(usage.completionTokens) })}</small></div>
|
||||
<div>
|
||||
<dt>{t("common.cache_hit")}</dt><dd>{percent(cacheRate(usage.cacheHit, usage.cacheMiss, unknown))}</dd>
|
||||
<small>{unknown > 0 ? t("session.hit_hit_miss_leaves_out", { n: tokens(unknown) }) : t("session.hit_hit_miss")}</small>
|
||||
</div>
|
||||
<div><dt>{t("session.work_time")}</dt><dd>{clock(usage.workMs)}</dd><small>{t("session.sum_of_the_turns_that")}</small></div>
|
||||
<div><dt>{t("session.cost")}</dt><dd>{total.text}</dd>{total.note && <small>{total.note}</small>}</div>
|
||||
</dl>
|
||||
<section aria-label={t("common.by_source")}>
|
||||
<div><b>{t("common.by_source")}</b><small>{t("session.requests_tokens_cache_cost")}</small></div>
|
||||
{usage.sources.map((src) => {
|
||||
const c = cost(costSay(src.cost), hideAmounts);
|
||||
return (
|
||||
<div className="studio-overview-row" key={src.source}>
|
||||
<span>{sourceName(src.source)}</span>
|
||||
<small>{src.requests}</small>
|
||||
<b>{tokens(sourceTokens(src))}</b>
|
||||
<small>{percent(cacheRate(src.cacheHit, src.cacheMiss, src.cacheUnknown ?? 0))}</small>
|
||||
<b title={c.note || undefined}>{c.text}</b>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
{sources.length > 0 && (
|
||||
<section aria-label={t("common.by_source")}>
|
||||
<div><b>{t("common.by_source")}</b></div>
|
||||
{sources.map((src, i) => {
|
||||
const c = cost(costSay(src.cost), hideAmounts);
|
||||
const detail = `${t("session.requests_tokens_cache_cost")}: ${src.requests} · ${tokens(sourceTokens(src))} · ${percent(cacheRate(src.cacheHit, src.cacheMiss, src.cacheUnknown ?? 0))} · ${c.text}`;
|
||||
return (
|
||||
<div className="studio-overview-row" key={src.source} title={detail}>
|
||||
<span>{sourceName(src.source)}</span>
|
||||
<i aria-hidden="true"><u style={{ width: `${pricedTotal > 0 ? Math.max(priced[i] > 0 ? 2 : 0, (priced[i] / pricedTotal) * 100) : 0}%` }} /></i>
|
||||
<b title={c.note || undefined}>{c.text}</b>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
)}
|
||||
<footer className="studio-overview-foot">
|
||||
<span title={t("session.sum_of_the_turns_that")}>{t("session.work_time")} {clock(usage.workMs)}</span>
|
||||
{onContext && <button type="button" data-action="metrics.details" data-value="context-from-overview" onClick={onContext}>{t("common.context_details")}</button>}
|
||||
</footer>
|
||||
</>
|
||||
)}
|
||||
{ctx && ctx.window > 0 && <ContextWindow ctx={ctx} onContext={onContext} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PARTS = [["sys", "system"], ["tool", "tools"], ["user", "user"], ["reply", "reply"], ["output", "output"]] as const;
|
||||
|
||||
// The window is a present reading, not a running total: what the next request would carry.
|
||||
function ContextWindow({ ctx, onContext }: { ctx: ContextBreakdown; onContext?: () => void }) {
|
||||
return (
|
||||
<section aria-label={t("common.context_window")}>
|
||||
<div><b>{t("common.context_window")}</b><b>{Math.min(100, Math.round((ctx.used / ctx.window) * 100))}%</b></div>
|
||||
<small>{t("session.what_the_next_request_would")}</small>
|
||||
<div className="chrome-context-total"><span>{t("common.used")} <b>{tokens(ctx.used)}</b></span><span>{t("common.model_capacity")} <b>{tokens(ctx.window)}</b></span></div>
|
||||
<div className="chrome-context-bar">{PARTS.map(([key, field]) => <i key={key} data-part={key} style={{ width: `${ctx.used ? Math.max(1, (ctx[field] / ctx.used) * 100) : 0}%` }} />)}</div>
|
||||
{ctx.compact_at > 0 && <small>{t("session.folds_automatically_at_n", { n: tokens(ctx.compact_at) })}</small>}
|
||||
{onContext && <button type="button" data-action="metrics.details" data-value="context-from-overview" onClick={onContext}>{t("common.context_details")}</button>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user