mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(desktop): streamline task and toolbar controls (#8392)
Problem: The task monitor exposed duplicate stop and cancel actions with a cramped confirmation flow, while the topic toolbar repeated command and shortcut entry points and placed the workspace control inconsistently. Root cause: The desktop UI surfaced backend control aliases as separate primary actions and accumulated utility controls without a clear priority order or complete localization. Fix: Merge active-task termination into a single accessible stop flow, localize task state and session-summary labels, remove redundant toolbar entry points, and place session summary before the workspace toggle. Keep the underlying cancel APIs and keyboard/settings access paths intact. Verification: - pnpm exec tsx src/components/TaskMonitorPanel.test.tsx - pnpm exec tsx src/__tests__/app-chrome-tabs.test.ts - pnpm check:css - pnpm typecheck - Browser verification at 127.0.0.1:5174
This commit is contained in:
1 parent
ee17c62901
commit
6d262926e0
8 files changed
+194
-91
No files matched your search
@@ -2,7 +2,6 @@ import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState, type
|
||||
import { ShellExpandProvider, useShellExpand } from "./lib/shellExpand";
|
||||
import {
|
||||
Activity,
|
||||
CircleHelp,
|
||||
Command,
|
||||
Copy as RestoreIcon,
|
||||
Download,
|
||||
@@ -4759,33 +4758,15 @@ export default function App() {
|
||||
{!sidebarCreation && shouldMountExternalOpener(activeTab, Boolean(sidebarImDetailConnection)) && activeTab && (
|
||||
<ExternalOpener key={activeTab.id} tabId={activeTab.id} dismissSignal={transientOverlayDismissSignal} />
|
||||
)}
|
||||
<Tooltip label={t("shortcuts.cheatsheetTitle")}>
|
||||
<Tooltip label={t("summary.session")}>
|
||||
<button
|
||||
className="topicbar__action-btn topicbar__action-btn--icon topicbar__action-btn--utility"
|
||||
className={`topicbar__action-btn topicbar__action-btn--icon topicbar__action-btn--utility${tasksOpen ? " topicbar__action-btn--active" : ""}`}
|
||||
type="button"
|
||||
aria-label={t("shortcuts.cheatsheetTitle")}
|
||||
onClick={() => {
|
||||
closeTransientOverlays();
|
||||
setSettingsFocus(null);
|
||||
setSettingsTarget("shortcuts");
|
||||
}}
|
||||
aria-label={t("summary.session")}
|
||||
aria-expanded={tasksOpen}
|
||||
onClick={() => setTasksOpen((open) => !open)}
|
||||
>
|
||||
<CircleHelp size={14} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label={t("topicBar.command")}>
|
||||
<button
|
||||
className={
|
||||
sidebarCreation
|
||||
? "topicbar__action-btn topicbar__action-btn--icon topicbar__action-btn--utility"
|
||||
: "topicbar__action-btn topicbar__action-btn--label topicbar__action-btn--accent"
|
||||
}
|
||||
type="button"
|
||||
aria-label={t("topicBar.command")}
|
||||
onClick={() => void openPalette()}
|
||||
>
|
||||
<Command size={14} />
|
||||
{!sidebarCreation && <span>{t("topicBar.command")}</span>}
|
||||
<Activity size={14} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{(sidebarCreation || workbenchChromeHidden) && (
|
||||
@@ -4806,19 +4787,8 @@ export default function App() {
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label="Session summary">
|
||||
<button
|
||||
className={`topicbar__action-btn topicbar__action-btn--icon topicbar__action-btn--utility${tasksOpen ? " topicbar__action-btn--active" : ""}`}
|
||||
type="button"
|
||||
aria-label="Session summary"
|
||||
aria-expanded={tasksOpen}
|
||||
onClick={() => setTasksOpen((open) => !open)}
|
||||
>
|
||||
<Activity size={14} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{tasksOpen && (
|
||||
<div className="taskmonitor-popover" role="dialog" aria-label="Session summary">
|
||||
<div className="taskmonitor-popover" role="dialog" aria-label={t("summary.session")}>
|
||||
<Suspense fallback={null}>
|
||||
<TaskMonitorPanel
|
||||
key={`${activeTab?.id || activeTabId || "none"}:${activeTab?.workspaceRoot || "global"}:${activeTab?.sessionPath || ""}`}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Run: tsx src/__tests__/topicbar-controls.test.ts
|
||||
|
||||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const testDir = dirname(fileURLToPath(import.meta.url));
|
||||
const appSource = readFileSync(resolve(testDir, "../App.tsx"), "utf8");
|
||||
|
||||
assert.doesNotMatch(appSource, /t\("shortcuts\.cheatsheetTitle"\)|t\("topicBar\.command"\)/);
|
||||
|
||||
const taskSummaryControlIndex = appSource.indexOf('<Tooltip label={t("summary.session")}>');
|
||||
const workspaceToggleIndex = appSource.indexOf('<Tooltip label={workspacePanelRenderable ? t("rightDock.collapse") : t("rightDock.expand")}>');
|
||||
assert.ok(taskSummaryControlIndex >= 0, "topic bar renders the localized Session summary control");
|
||||
assert.ok(workspaceToggleIndex > taskSummaryControlIndex, "Session summary precedes the right-edge workspace toggle");
|
||||
assert.ok(!appSource.includes('aria-label="Session summary"'), "Session summary does not use a hard-coded English label");
|
||||
|
||||
process.stdout.write("topicbar controls: 2 contracts passed\n");
|
||||
@@ -68,6 +68,7 @@ let listTasksImpl: () => Promise<Task[]> = async () => [];
|
||||
let listEventsImpl: () => Promise<Event[]> = async () => [];
|
||||
const listTaskTabIDs: string[] = [];
|
||||
const listEventCalls: unknown[][] = [];
|
||||
const stopCalls: unknown[][] = [];
|
||||
const requeueCalls: unknown[][] = [];
|
||||
const mockApp = {
|
||||
ListTasks: () => listTasksImpl(),
|
||||
@@ -97,7 +98,10 @@ const mockApp = {
|
||||
listEventCalls.push(args);
|
||||
return listEventsImpl();
|
||||
},
|
||||
StopTaskForTab: async () => ({ schema_version: 1, command: "stop", task_id: "", accepted: true, idempotent: false }),
|
||||
StopTaskForTab: async (...args: unknown[]) => {
|
||||
stopCalls.push(args);
|
||||
return { schema_version: 1, command: "stop", task_id: "", accepted: true, idempotent: false };
|
||||
},
|
||||
CancelTaskForTab: async () => ({ schema_version: 1, command: "cancel", task_id: "", accepted: true, idempotent: false }),
|
||||
RequeueTaskForTab: async (...args: unknown[]) => {
|
||||
requeueCalls.push(args);
|
||||
@@ -154,6 +158,7 @@ async function cleanup() {
|
||||
listEventsImpl = async () => [];
|
||||
listTaskTabIDs.length = 0;
|
||||
listEventCalls.length = 0;
|
||||
stopCalls.length = 0;
|
||||
requeueCalls.length = 0;
|
||||
}
|
||||
|
||||
@@ -178,6 +183,13 @@ async function click(button: HTMLButtonElement) {
|
||||
});
|
||||
}
|
||||
|
||||
async function keyDown(element: HTMLElement, key: string) {
|
||||
await act(async () => {
|
||||
element.dispatchEvent(new dom.window.KeyboardEvent("keydown", { key, bubbles: true }));
|
||||
await flush();
|
||||
});
|
||||
}
|
||||
|
||||
async function openPanel() {
|
||||
await click(buttonByLabel("Expand tasks"));
|
||||
}
|
||||
@@ -238,6 +250,62 @@ await check("separates lifecycle state from runtime liveness", async () => {
|
||||
return text.includes("Exited") && text.includes("Runtime unknown");
|
||||
});
|
||||
|
||||
await check("offers one working stop action for active tasks", async () => {
|
||||
listTasksImpl = async () => [snap()];
|
||||
await renderPanel();
|
||||
await openPanel();
|
||||
await click(buttonByLabel("Task task-000 — Running"));
|
||||
const actionLabels = Array.from(document.querySelectorAll<HTMLButtonElement>(".taskmonitor__actions button"))
|
||||
.map((button) => button.textContent?.trim());
|
||||
const hasMergedActions = actionLabels.filter((label) => label === "Stop").length === 1
|
||||
&& !actionLabels.includes("Cancel")
|
||||
&& actionLabels.includes("Open session");
|
||||
await click(buttonByText("Stop"));
|
||||
const confirmStop = buttonByText("Stop");
|
||||
const detailValues = Array.from(document.querySelectorAll<HTMLElement>(".taskmonitor__detail dd"))
|
||||
.map((value) => value.textContent?.trim());
|
||||
const hasReplacementConfirmation = document.querySelector(".taskmonitor__actions") === null
|
||||
&& document.activeElement === confirmStop
|
||||
&& detailValues.includes("Running")
|
||||
&& !detailValues.includes("running");
|
||||
await click(confirmStop);
|
||||
return hasMergedActions
|
||||
&& hasReplacementConfirmation
|
||||
&& JSON.stringify(stopCalls[0]) === JSON.stringify([
|
||||
"tab-a",
|
||||
"task-0001",
|
||||
1,
|
||||
"desktop request",
|
||||
"desktop-stop-task-0001-1",
|
||||
]);
|
||||
});
|
||||
|
||||
await check("dismisses stop confirmation with Escape and restores focus", async () => {
|
||||
listTasksImpl = async () => [snap()];
|
||||
await renderPanel();
|
||||
await openPanel();
|
||||
await click(buttonByLabel("Task task-000 — Running"));
|
||||
await click(buttonByText("Stop"));
|
||||
await keyDown(buttonByText("Stop"), "Escape");
|
||||
const restoredStop = buttonByText("Stop");
|
||||
return document.querySelector(".taskmonitor__confirm") === null
|
||||
&& document.activeElement === restoredStop
|
||||
&& stopCalls.length === 0;
|
||||
});
|
||||
|
||||
await check("dismisses stop confirmation when polling observes a terminal task", async () => {
|
||||
listTasksImpl = async () => [snap()];
|
||||
await renderPanel();
|
||||
await openPanel();
|
||||
await click(buttonByLabel("Task task-000 — Running"));
|
||||
await click(buttonByText("Stop"));
|
||||
listTasksImpl = async () => [snap({ state: "succeeded", runtime_state: "exited", version: 2 })];
|
||||
await click(buttonByLabel("Refresh"));
|
||||
return document.querySelector(".taskmonitor__confirm") === null
|
||||
&& document.body.textContent?.includes("Succeeded") === true
|
||||
&& stopCalls.length === 0;
|
||||
});
|
||||
|
||||
await check("requeues failed exited tasks", async () => {
|
||||
listTasksImpl = async () => [snap({ task_id: "failed-1", state: "failed", runtime_state: "exited", version: 7 })];
|
||||
await renderPanel();
|
||||
|
||||
@@ -58,6 +58,10 @@ function isTerminalState(state: string): boolean {
|
||||
return state === "succeeded" || state === "failed" || state === "cancelled" || state === "stale";
|
||||
}
|
||||
|
||||
function isStoppableState(state: string): boolean {
|
||||
return state === "queued" || state === "running" || state === "waiting";
|
||||
}
|
||||
|
||||
function elapsed(task: TaskTimerSnapshot, nowMs: number): string {
|
||||
if (!task.created_at) return "—";
|
||||
const startMs = new Date(task.created_at).getTime();
|
||||
@@ -90,7 +94,7 @@ function eventSummary(ev: TaskEvent, t: ReturnType<typeof useT>): string {
|
||||
if (ev.error_code) return t("task.event.error", { code: ev.error_code });
|
||||
switch (ev.event_type) {
|
||||
case "state_change":
|
||||
return t("task.event.stateChange", { state: ev.state, runtime: runtimeConfig(ev.runtime_state, t).label });
|
||||
return t("task.event.stateChange", { state: stateConfig(ev.state, t).label, runtime: runtimeConfig(ev.runtime_state, t).label });
|
||||
case "error":
|
||||
return ev.error_summary || t("task.error");
|
||||
default:
|
||||
@@ -127,7 +131,9 @@ export function TaskMonitorPanel({
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const [actionMessage, setActionMessage] = useState<string | null>(null);
|
||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||
const [pendingAction, setPendingAction] = useState<{ task: TaskSnapshot; action: "stop" | "cancel" } | null>(null);
|
||||
const [pendingStop, setPendingStop] = useState<TaskSnapshot | null>(null);
|
||||
const stopButtonRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
|
||||
const confirmStopRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
// Per-task event state
|
||||
const [taskEvents, setTaskEvents] = useState<Map<string, TaskEvent[]>>(
|
||||
@@ -216,6 +222,24 @@ export function TaskMonitorPanel({
|
||||
return () => clearInterval(interval);
|
||||
}, [tasks]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingStop) confirmStopRef.current?.focus();
|
||||
}, [pendingStop]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingStop) return;
|
||||
const current = tasks.find((task) => task.task_id === pendingStop.task_id);
|
||||
if (!current || !isStoppableState(current.state)) setPendingStop(null);
|
||||
}, [pendingStop, tasks]);
|
||||
|
||||
const dismissStopConfirmation = () => {
|
||||
const taskID = pendingStop?.task_id;
|
||||
setPendingStop(null);
|
||||
if (taskID) {
|
||||
requestAnimationFrame(() => stopButtonRefs.current.get(taskID)?.focus());
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTask = (id: string) => {
|
||||
setExpanded((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -232,12 +256,8 @@ export function TaskMonitorPanel({
|
||||
});
|
||||
};
|
||||
|
||||
const controlTask = async (task: TaskSnapshot, action: "stop" | "cancel" | "requeue" | "open") => {
|
||||
if ((action === "stop" || action === "cancel") && (!pendingAction || pendingAction.task.task_id !== task.task_id || pendingAction.action !== action)) {
|
||||
setPendingAction({ task, action });
|
||||
return;
|
||||
}
|
||||
setPendingAction(null);
|
||||
const controlTask = async (task: TaskSnapshot, action: "stop" | "requeue" | "open") => {
|
||||
setPendingStop(null);
|
||||
setActionTask(task.task_id);
|
||||
setActionError(null);
|
||||
setActionMessage(null);
|
||||
@@ -249,11 +269,9 @@ export function TaskMonitorPanel({
|
||||
}
|
||||
const result = action === "stop"
|
||||
? await app.StopTaskForTab(tabID, task.task_id, task.version, "desktop request", `desktop-${action}-${task.task_id}-${task.version}`)
|
||||
: action === "cancel"
|
||||
? await app.CancelTaskForTab(tabID, task.task_id, task.version, "desktop request", `desktop-${action}-${task.task_id}-${task.version}`)
|
||||
: action === "requeue"
|
||||
? await app.RequeueTaskForTab(tabID, task.task_id, task.version, `desktop-${action}-${task.task_id}-${task.version}`)
|
||||
: await app.OpenTaskSessionForTab(tabID, task.task_id);
|
||||
: action === "requeue"
|
||||
? await app.RequeueTaskForTab(tabID, task.task_id, task.version, `desktop-${action}-${task.task_id}-${task.version}`)
|
||||
: await app.OpenTaskSessionForTab(tabID, task.task_id);
|
||||
if (result.error) {
|
||||
setActionError(`${result.error.code}: ${result.error.message}`);
|
||||
} else if (action === "open") {
|
||||
@@ -408,7 +426,7 @@ export function TaskMonitorPanel({
|
||||
<dt>{t("summary.sessionId")}</dt>
|
||||
<dd>{task.session_id || "—"}</dd>
|
||||
<dt>{t("summary.state")}</dt>
|
||||
<dd>{task.state}</dd>
|
||||
<dd>{cfg.label}</dd>
|
||||
<dt>{t("summary.runtime")}</dt>
|
||||
<dd>{runtime.label}</dd>
|
||||
<dt>{t("summary.updated")}</dt>
|
||||
@@ -485,23 +503,51 @@ export function TaskMonitorPanel({
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="taskmonitor__actions">
|
||||
{(task.state === "queued" || task.state === "running" || task.state === "waiting") && (
|
||||
<>
|
||||
<button disabled={actionTask === task.task_id} onClick={() => void controlTask(task, "stop")}>{t("summary.stop")}</button>
|
||||
<button disabled={actionTask === task.task_id} onClick={() => void controlTask(task, "cancel")}>{t("summary.cancel")}</button>
|
||||
</>
|
||||
)}
|
||||
{(task.state === "failed" || task.state === "stale") && (
|
||||
<button disabled={actionTask === task.task_id || task.runtime_state === "alive"} onClick={() => void controlTask(task, "requeue")}>{t("summary.requeue")}</button>
|
||||
)}
|
||||
<button disabled={actionTask === task.task_id} onClick={() => void controlTask(task, "open")}>{t("summary.openSession")}</button>
|
||||
</div>
|
||||
{pendingAction?.task.task_id === task.task_id && (
|
||||
<div className="taskmonitor__confirm">
|
||||
<span>{t(pendingAction.action === "stop" ? "summary.confirmStop" : "summary.confirmCancel")}</span>
|
||||
<button type="button" onClick={() => void controlTask(task, pendingAction.action)}>{t("common.confirm")}</button>
|
||||
<button type="button" onClick={() => setPendingAction(null)}>{t("summary.keep")}</button>
|
||||
{pendingStop?.task_id === task.task_id ? (
|
||||
<div
|
||||
className="taskmonitor__confirm"
|
||||
role="group"
|
||||
aria-label={t("summary.confirmStop")}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
dismissStopConfirmation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="taskmonitor__confirm-copy">{t("summary.confirmStop")}</span>
|
||||
<div className="taskmonitor__confirm-actions">
|
||||
<button
|
||||
ref={confirmStopRef}
|
||||
type="button"
|
||||
className="taskmonitor__confirm-stop"
|
||||
disabled={actionTask === task.task_id}
|
||||
onClick={() => void controlTask(task, "stop")}
|
||||
>
|
||||
{t("summary.stop")}
|
||||
</button>
|
||||
<button type="button" onClick={dismissStopConfirmation}>{t("summary.keep")}</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="taskmonitor__actions">
|
||||
{isStoppableState(task.state) && (
|
||||
<button
|
||||
ref={(node) => {
|
||||
if (node) stopButtonRefs.current.set(task.task_id, node);
|
||||
else stopButtonRefs.current.delete(task.task_id);
|
||||
}}
|
||||
className="taskmonitor__stop"
|
||||
disabled={actionTask === task.task_id}
|
||||
onClick={() => setPendingStop(task)}
|
||||
>
|
||||
{t("summary.stop")}
|
||||
</button>
|
||||
)}
|
||||
{(task.state === "failed" || task.state === "stale") && (
|
||||
<button disabled={actionTask === task.task_id || task.runtime_state === "alive"} onClick={() => void controlTask(task, "requeue")}>{t("summary.requeue")}</button>
|
||||
)}
|
||||
<button disabled={actionTask === task.task_id} onClick={() => void controlTask(task, "open")}>{t("summary.openSession")}</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -75,12 +75,10 @@ export const en = {
|
||||
"summary.loadingEvents": "Loading events…",
|
||||
"summary.noEvents": "No events yet",
|
||||
"summary.stop": "Stop",
|
||||
"summary.cancel": "Cancel",
|
||||
"summary.requeue": "Requeue",
|
||||
"summary.openSession": "Open session",
|
||||
"summary.confirmStop": "Stop this task?",
|
||||
"summary.confirmCancel": "Cancel this task?",
|
||||
"summary.keep": "Keep",
|
||||
"summary.keep": "Keep running",
|
||||
"task.error": "Error",
|
||||
"task.event.error": "Error: {code}",
|
||||
"task.event.stateChange": "State → {state} · Runtime → {runtime}",
|
||||
@@ -188,7 +186,6 @@ export const en = {
|
||||
// topic bar
|
||||
"topicBar.renameSession": "Rename session",
|
||||
"topicBar.more": "More",
|
||||
"topicBar.command": "Command",
|
||||
"topicBar.copyAll": "Copy session",
|
||||
"topicBar.export": "Export session",
|
||||
"topicBar.exportMarkdown": "Export Markdown",
|
||||
|
||||
@@ -66,16 +66,14 @@ export const zhTW: Record<DictKey, string> = {
|
||||
"summary.loadingEvents": "載入…",
|
||||
"summary.noEvents": "無事件",
|
||||
"summary.stop": "停止",
|
||||
"summary.cancel": "取消",
|
||||
"summary.requeue": "重新排隊",
|
||||
"summary.openSession": "開啟",
|
||||
"summary.confirmStop": "停止?",
|
||||
"summary.confirmCancel": "取消?",
|
||||
"summary.keep": "保留",
|
||||
"summary.openSession": "開啟會話",
|
||||
"summary.confirmStop": "停止此任務?",
|
||||
"summary.keep": "繼續執行",
|
||||
"task.error": "錯誤",
|
||||
"task.event.error": "錯誤:{code}",
|
||||
"task.event.stateChange": "狀態 {state} · 執行時 {runtime}",
|
||||
"task.runtime.live": "執行中",
|
||||
"task.runtime.live": "在線",
|
||||
"task.runtime.exited": "已退出",
|
||||
"task.runtime.unknown": "執行時未知",
|
||||
"task.state.queued": "排隊中",
|
||||
@@ -2330,7 +2328,6 @@ export const zhTW: Record<DictKey, string> = {
|
||||
"botDetail.flowReceive": "IM 訊息進入本地話題",
|
||||
"botDetail.flowDesktop": "桌面端處理模型和工具",
|
||||
"botDetail.flowReply": "階段結果回傳到 IM",
|
||||
"topicBar.command": "命令",
|
||||
"topicBar.exportPdf": "匯出 PDF",
|
||||
"topicBar.exportImage": "匯出圖片",
|
||||
"topicBar.exportSuccess": "已匯出 {count} 個檔案。",
|
||||
|
||||
@@ -76,16 +76,14 @@ export const zh: Record<DictKey, string> = {
|
||||
"summary.loadingEvents": "加载中…",
|
||||
"summary.noEvents": "暂无事件",
|
||||
"summary.stop": "停止",
|
||||
"summary.cancel": "取消",
|
||||
"summary.requeue": "重新排队",
|
||||
"summary.openSession": "打开",
|
||||
"summary.confirmStop": "停止?",
|
||||
"summary.confirmCancel": "取消?",
|
||||
"summary.keep": "保留",
|
||||
"summary.openSession": "打开会话",
|
||||
"summary.confirmStop": "停止此任务?",
|
||||
"summary.keep": "继续运行",
|
||||
"task.error": "错误",
|
||||
"task.event.error": "错误:{code}",
|
||||
"task.event.stateChange": "状态 {state} · 运行 {runtime}",
|
||||
"task.runtime.live": "运行中",
|
||||
"task.runtime.live": "在线",
|
||||
"task.runtime.exited": "已退出",
|
||||
"task.runtime.unknown": "未知",
|
||||
"task.state.queued": "排队中",
|
||||
@@ -189,7 +187,6 @@ export const zh: Record<DictKey, string> = {
|
||||
// 话题栏
|
||||
"topicBar.renameSession": "重命名会话",
|
||||
"topicBar.more": "更多",
|
||||
"topicBar.command": "命令",
|
||||
"topicBar.copyAll": "复制会话",
|
||||
"topicBar.export": "导出会话",
|
||||
"topicBar.exportMarkdown": "导出 Markdown",
|
||||
|
||||
@@ -37668,8 +37668,8 @@ body > .mermaid-diagram--fullscreen {
|
||||
.taskmonitor { background:var(--bg,#111); }
|
||||
.taskmonitor--popover { max-height:inherit; }
|
||||
.taskmonitor__head { display:flex; align-items:center; gap:6px; width:100%; padding:8px 12px; font-size:13px; }
|
||||
.taskmonitor__toggle,.taskmonitor__refresh,.taskmonitor__close { background:none; border:0; color:inherit; cursor:pointer; }
|
||||
.taskmonitor__toggle { display:flex; padding:0; }
|
||||
.taskmonitor__toggle,.taskmonitor__refresh,.taskmonitor__close { display:inline-flex; align-items:center; justify-content:center; width:28px; height:28px; padding:0; border:0; border-radius:6px; background:none; color:inherit; cursor:pointer; }
|
||||
.taskmonitor__toggle:hover,.taskmonitor__refresh:hover,.taskmonitor__close:hover,.taskmonitor__toggle:focus-visible,.taskmonitor__refresh:focus-visible,.taskmonitor__close:focus-visible { background:var(--bg-elev-2); outline:none; }
|
||||
.taskmonitor__title { flex:1; text-align:left; font-weight:600; }
|
||||
.taskmonitor__count { background:var(--muted,#555); color:#fff; border-radius:10px; padding:0 6px; font-size:11px; line-height:18px; }
|
||||
.taskmonitor__body { max-height:360px; overflow-y:auto; }
|
||||
@@ -37679,7 +37679,8 @@ body > .mermaid-diagram--fullscreen {
|
||||
.taskmonitor__state--error { color:#ef4444; }
|
||||
.taskmonitor__task { border-bottom:1px solid var(--border,#2a2a2a); }
|
||||
.taskmonitor__task-head { display:flex; align-items:center; }
|
||||
.taskmonitor__expand { display:flex; align-items:center; gap:6px; width:100%; padding:6px 12px; background:none; border:0; color:inherit; cursor:pointer; font-size:12px; }
|
||||
.taskmonitor__expand { display:flex; align-items:center; gap:6px; width:100%; min-height:32px; padding:6px 12px; background:none; border:0; color:inherit; cursor:pointer; font-size:12px; }
|
||||
.taskmonitor__expand:hover,.taskmonitor__expand:focus-visible { background:var(--bg-elev-2); outline:none; }
|
||||
.taskmonitor__dot { font-size:10px; width:14px; text-align:center; }
|
||||
.taskmonitor__id { font-family:monospace; font-size:11px; color:var(--muted,#888); }
|
||||
.taskmonitor__badge { font-size:10px; padding:1px 6px; border-radius:8px; font-weight:600; text-transform:uppercase; }
|
||||
@@ -37692,7 +37693,15 @@ body > .mermaid-diagram--fullscreen {
|
||||
.taskmonitor__events { margin-top:8px; border-top:1px solid var(--border,#333); padding-top:6px; }
|
||||
.taskmonitor__event { display:flex; gap:8px; line-height:20px; }
|
||||
.taskmonitor__actions { display:flex; gap:8px; margin-top:10px; }
|
||||
.taskmonitor__actions button,.taskmonitor__confirm button { padding:4px 10px; border:1px solid var(--border,#555); border-radius:4px; background:var(--surface,#222); color:inherit; cursor:pointer; }
|
||||
.taskmonitor__actions button,.taskmonitor__confirm button { min-height:32px; padding:0 12px; border:1px solid var(--border,#555); border-radius:6px; background:var(--surface,#222); color:inherit; cursor:pointer; }
|
||||
.taskmonitor__actions button:hover:not(:disabled),.taskmonitor__confirm button:hover:not(:disabled),.taskmonitor__actions button:focus-visible,.taskmonitor__confirm button:focus-visible { border-color:var(--accent); outline:2px solid color-mix(in srgb,var(--accent) 38%,transparent); outline-offset:1px; }
|
||||
.taskmonitor__actions button:disabled,.taskmonitor__confirm button:disabled { cursor:not-allowed; opacity:.55; }
|
||||
.taskmonitor__actions .taskmonitor__stop { border-color:color-mix(in srgb,var(--danger,#ef4444) 48%,var(--border)); color:var(--danger,#ef4444); }
|
||||
.taskmonitor__confirm { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:10px; padding:10px 12px; border:1px solid color-mix(in srgb,var(--danger,#ef4444) 30%,var(--border)); border-radius:8px; background:color-mix(in srgb,var(--danger,#ef4444) 7%,var(--surface,#222)); }
|
||||
.taskmonitor__confirm-copy { flex:1 1 150px; font-weight:600; line-height:1.4; }
|
||||
.taskmonitor__confirm-actions { display:flex; gap:8px; }
|
||||
.taskmonitor__confirm .taskmonitor__confirm-stop { border-color:var(--danger,#ef4444); background:var(--danger,#ef4444); color:#fff; font-weight:650; }
|
||||
.taskmonitor__confirm .taskmonitor__confirm-stop:hover:not(:disabled),.taskmonitor__confirm .taskmonitor__confirm-stop:focus-visible { border-color:var(--danger,#ef4444); outline-color:color-mix(in srgb,var(--danger,#ef4444) 42%,transparent); }
|
||||
.taskmonitor__error { color:#ef4444; margin-top:6px; }
|
||||
.taskmonitor__message { color:#4ade80; margin-top:6px; }
|
||||
|
||||
|
||||
Reference in new issue
Block a user