mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(studio): a running call with no output yet opens and says what it is doing
In the activity group a running call that had printed nothing was a row that did nothing when clicked, and a long command was clipped to one line, so a call waiting out a timeout looked frozen. Such a row now opens to its input, says it has no output yet and for how long it has run, names the command's own timeout when it carries one, and its hover text holds the whole command.
This commit is contained in:
1 parent
18965b946b
commit
c842f014f4
10 files changed
+137
-4
No files matched your search
@@ -68,6 +68,7 @@ node perf/find.mjs # 对话内查找:快捷键穿过输入框、跳到
|
||||
node perf/focus.mjs # 专注的守卫:拿走外围之后,退出时世界是否原样
|
||||
node perf/tooltip.mjs # 控件提示的守卫:文字不溢出提示框、不在半透明祖先之下、对比度达 AA(深浅×中英×宽窄)
|
||||
node perf/navhint.mjs # Icon-rail hover hints: drawn, inside the window, clipped by no ancestor, painted over by nothing (light/dark × on/collapsed)
|
||||
node perf/runningtool.mjs # 还没输出的运行中调用:行能展开、写明仍在运行与已运行多久、写出命令自带的超时
|
||||
node perf/gutterhit.mjs # 分栏拖拽手柄不压在相邻滚动条上:滚动条拇指处按下命中的不是手柄,握把仍可拖开收起的栏
|
||||
node perf/dock.mjs # 停靠的浏览器列从 pane 顶部起、与导航行并排,取消停靠后行归还(浅/深 × 宽/窄 × 首开/重载)
|
||||
```
|
||||
|
||||
@@ -6,6 +6,7 @@ import "../src/styles/app.css";
|
||||
import "../src/styles/dock.css";
|
||||
import "../src/styles/studio.css";
|
||||
import "../src/styles/tips.css";
|
||||
import "../src/styles/waitnote.css";
|
||||
import "../src/styles/feedback.css";
|
||||
import { App } from "../src/ui/App";
|
||||
import { MockHub } from "../src/port/mock_hub";
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// A running call with no output yet: its row opens, says it is still running and for how long, and names its own timeout.
|
||||
import { chromium } from "playwright";
|
||||
const fails = [];
|
||||
const check = (name, ok, detail = "") => { console.log(`${ok ? " ok" : "FAIL"} ${name}${detail ? " — " + detail : ""}`); if (!ok) fails.push(name); };
|
||||
const [,, out, w] = process.argv;
|
||||
const b = await chromium.launch();
|
||||
const p = await b.newPage({ viewport: { width: +w || 1100, height: 700 } });
|
||||
await p.goto((process.env.PERF_URL ?? "http://localhost:4521/perf.html") + "?pref=zh", { waitUntil: "networkidle" }); await p.waitForSelector(".app"); await p.waitForTimeout(400);
|
||||
await p.evaluate(async () => {
|
||||
window.__feed({ kind: "turn_started", authoredTurn: 1, msgIndex: 0, text: "list them" });
|
||||
window.__feed({ kind: "text", text: "我来看一下目录。" });
|
||||
const cmd = 'cd /Users/me/Documents/AI项目/音频搜索 && for f in *.md; do echo "=== $f ($(wc -l < "$f") 行)"; head -20 "$f"; done; ls -la /Users/me/Documents/ ; echo ---; find /Users/me/Documents -maxdepth 3 -name "*.md" | head -50';
|
||||
window.__feed({ kind: "tool_dispatch", tool: { id: "b1", name: "bash", args: JSON.stringify({ command: cmd, timeout_seconds: 900 }) } });
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
});
|
||||
await p.waitForTimeout(600);
|
||||
if (out) await p.screenshot({ path: out + "-1.png" });
|
||||
const row = p.locator(".activity-group .call").first();
|
||||
check("the running row is a disclosure", (await row.locator("details.tool-disclosure").count()) === 1);
|
||||
await row.locator("summary").click().catch(() => {});
|
||||
await p.waitForTimeout(1500);
|
||||
const g = await p.evaluate(() => ({ note: document.querySelector(".activity-call-wait")?.textContent ?? "", input: !!document.querySelector(".activity-call-input"), title: document.querySelector(".activity-group .arg")?.getAttribute("title") ?? "" }));
|
||||
check("opened, it shows the whole command", g.input && g.title.includes("maxdepth 3"));
|
||||
check("and says it is still running with no output, and for how long", /命令仍在运行/.test(g.note) && /已运行/.test(g.note), g.note);
|
||||
check("and names the command's own timeout", /超时上限 900 秒/.test(g.note), g.note);
|
||||
if (out) await p.screenshot({ path: out + "-2.png" });
|
||||
await b.close();
|
||||
console.log(fails.length ? `\n${fails.length} failed` : "\nall ok");
|
||||
process.exit(fails.length ? 1 : 0);
|
||||
@@ -1044,6 +1044,9 @@ export const EN: Record<string, string> = {
|
||||
"确定": "OK",
|
||||
"重试": "Retry",
|
||||
"设置": "Settings",
|
||||
"还没有输出,命令仍在运行": "No output yet; the command is still running",
|
||||
"已运行 {time}": "running for {time}",
|
||||
"这条命令自带的超时上限 {n} 秒": "this command's own timeout is {n}s",
|
||||
"专注": "Focus",
|
||||
"退出专注": "Leave focus",
|
||||
"专注:收起两侧栏": "Focus: put the side columns away",
|
||||
|
||||
@@ -8,6 +8,7 @@ import "./styles/app.css";
|
||||
import "./styles/dock.css";
|
||||
import "./styles/studio.css";
|
||||
import "./styles/tips.css";
|
||||
import "./styles/waitnote.css";
|
||||
import "./styles/feedback.css";
|
||||
import "./styles/community.css";
|
||||
import "./styles/sponsor.css";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.activity-call-wait { margin: 6px 0; color: var(--faint); font: var(--w-reg) var(--fs-meta) var(--ui); }
|
||||
@@ -22,7 +22,7 @@ function tookWidth(ms: number) {
|
||||
// counts on the window's own clock from the moment the window saw the call
|
||||
// running, and says nothing about a call that was already running when it
|
||||
// arrived, because that is a duration this window does not know.
|
||||
function useWatched(running: boolean): number {
|
||||
export function useWatched(running: boolean): number {
|
||||
const from = useRef(0);
|
||||
const [now, setNow] = useState(0);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Cost } from "../Cost";
|
||||
import { seconds, tokens } from "../../i18n/format";
|
||||
import { currentStep, parsePlan, stepDone } from "../../state/session";
|
||||
import { DiffView } from "./DiffView";
|
||||
import { WaitNote } from "./WaitNote";
|
||||
import { Term, ToolOutput, BoundNote } from "./ToolOutput";
|
||||
import { ExtensionView } from "./ExtensionView";
|
||||
import { toolChangedFile, toolFailed, toolFailureLabel, toolRefusalReason } from "./outcome";
|
||||
@@ -117,11 +118,13 @@ export function ToolCard({
|
||||
}, [bad]);
|
||||
const open = touched ?? start;
|
||||
const changes = changeCounts(tool);
|
||||
const activityArgs = activity ? prettyArgs(tool.args) : "";
|
||||
// A call being watched in the activity group opens to its input even with no
|
||||
// output yet: a row that cannot open is the one that looks stuck.
|
||||
const hasBody = Boolean(
|
||||
takeover || tool.diff || goal || tool.name === "todo_write" ||
|
||||
(tool.output && !echoed && children.length === 0) || tool.err || children.length,
|
||||
(tool.output && !echoed && children.length === 0) || tool.err || children.length || (activity && running && activityArgs),
|
||||
);
|
||||
const activityArgs = activity ? prettyArgs(tool.args) : "";
|
||||
const activityResult = Boolean(
|
||||
takeover || tool.diff || goal || tool.name === "todo_write" || tool.output || tool.err || children.length,
|
||||
);
|
||||
@@ -133,7 +136,7 @@ export function ToolCard({
|
||||
{who && <span className="who" title={t("按技能 {name} 的设定运行的子代理", { name: who })}>{who}</span>}
|
||||
{from && <span className="src" title={t("外部服务 {name} 提供的工具", { name: from.server })}>{from.server}</span>}
|
||||
{tag && <span className="tag" title={tagHint(tool)}>{tag}</span>}
|
||||
{arg && <span className={streaming ? "arg shim" : "arg"}>{arg}</span>}
|
||||
{arg && <span className={streaming ? "arg shim" : "arg"} title={streaming ? undefined : prettyArgs(tool.args)}>{arg}</span>}
|
||||
{changes && (
|
||||
<span className="tool-delta" aria-label={t("新增 {added} 行,删除 {removed} 行", changes)}>
|
||||
<b>+{changes.added}</b><i>−{changes.removed}</i>
|
||||
@@ -166,6 +169,7 @@ export function ToolCard({
|
||||
<pre>{activityArgs}</pre>
|
||||
</section>
|
||||
)}
|
||||
{running && !tool.output && !tool.err && children.length === 0 && <WaitNote args={tool.args} />}
|
||||
{activityResult && (
|
||||
<div className="activity-call-result">
|
||||
<b>{t("结果")}</b>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act } from "react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import "../testkit";
|
||||
import { ToolCard } from "./ToolCard";
|
||||
|
||||
beforeEach(() => vi.useFakeTimers());
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const bash = (over: Record<string, unknown> = {}) => ({ id: "b1", name: "bash", args: JSON.stringify({ command: "sleep 600" }), readOnly: false, ...over }) as Parameters<typeof ToolCard>[0]["tool"];
|
||||
const note = (c: HTMLElement) => c.querySelector(".activity-call-wait");
|
||||
const open = (c: HTMLElement) => {
|
||||
const d = c.querySelector("details")!;
|
||||
d.open = true;
|
||||
act(() => { d.dispatchEvent(new Event("toggle")); });
|
||||
};
|
||||
|
||||
describe("a running call that has printed nothing", () => {
|
||||
it("says it is still running instead of sitting blank, and counts how long", () => {
|
||||
const { container } = render(<ToolCard tool={bash()} running activity />);
|
||||
open(container);
|
||||
expect(note(container)?.textContent).toContain("命令仍在运行");
|
||||
act(() => vi.advanceTimersByTime(5000));
|
||||
expect(note(container)?.textContent).toMatch(/已运行 5/);
|
||||
});
|
||||
|
||||
it("names the timeout only when the command carries one", () => {
|
||||
const withLimit = render(<ToolCard tool={bash({ args: JSON.stringify({ command: "make", timeout_seconds: 300 }) })} running activity />);
|
||||
open(withLimit.container);
|
||||
expect(note(withLimit.container)?.textContent).toContain("超时上限 300 秒");
|
||||
cleanup();
|
||||
const without = render(<ToolCard tool={bash()} running activity />);
|
||||
open(without.container);
|
||||
expect(note(without.container)?.textContent).not.toContain("超时");
|
||||
});
|
||||
|
||||
it("goes away once output arrives, and is not drawn for a finished call or outside the detail view", () => {
|
||||
const { container, rerender } = render(<ToolCard tool={bash()} running activity />);
|
||||
open(container);
|
||||
rerender(<ToolCard tool={bash({ output: "building…\n" })} running activity />);
|
||||
expect(note(container)).toBeNull();
|
||||
rerender(<ToolCard tool={bash({ output: "done", durationMs: 1200 })} running={false} activity />);
|
||||
expect(note(container)).toBeNull();
|
||||
rerender(<ToolCard tool={bash()} running />);
|
||||
expect(note(container)).toBeNull();
|
||||
});
|
||||
|
||||
it("opens at all: a running call with no output yet used to be a row that did nothing", () => {
|
||||
const { container } = render(<ToolCard tool={bash()} running activity />);
|
||||
expect(container.querySelector("details.tool-disclosure")).not.toBeNull();
|
||||
expect(container.querySelector(".activity-call-input")).not.toBeNull();
|
||||
cleanup();
|
||||
expect(render(<ToolCard tool={bash()} running />).container.querySelector("details")).toBeNull();
|
||||
});
|
||||
|
||||
it("puts the whole command in the row's hover text, since the row clips it to one line", () => {
|
||||
const long = "cd /a/very/long/path && for f in *.md; do wc -l \"$f\"; done; find . -maxdepth 3";
|
||||
const { container } = render(<ToolCard tool={bash({ args: JSON.stringify({ command: long }) })} running activity />);
|
||||
expect(container.querySelector(".arg")?.getAttribute("title")).toContain("maxdepth 3");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { t } from "../../i18n";
|
||||
import { seconds } from "../../i18n/format";
|
||||
import { useWatched } from "../Cost";
|
||||
|
||||
// The only timeout the page can state is the one the call carries itself; the
|
||||
// host's own default is configuration this window does not hold.
|
||||
function ownTimeout(args: string | undefined): number {
|
||||
try {
|
||||
const n = Number((JSON.parse(args ?? "{}") as { timeout_seconds?: unknown }).timeout_seconds);
|
||||
return Number.isFinite(n) && n > 0 ? n : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** What a running call with no output yet says about itself, so a card that sits
|
||||
* still reads as "still running, nothing printed" rather than as stuck. */
|
||||
export function WaitNote({ args }: { args?: string }) {
|
||||
const watched = useWatched(true);
|
||||
const limit = ownTimeout(args);
|
||||
return (
|
||||
<div className="activity-call-wait" role="status">
|
||||
{t("还没有输出,命令仍在运行")}
|
||||
{watched >= 1000 && <> · {t("已运行 {time}", { time: seconds(watched, 0) })}</>}
|
||||
{limit > 0 && <> · {t("这条命令自带的超时上限 {n} 秒", { n: limit })}</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user