mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
feat(studio): a reply whose layout fails shows its source with an as-written tag, and a tool card that cannot be drawn falls back to a simplified line, as the recovery board draws them
This commit is contained in:
1 parent
e4c97fdfab
commit
d437997362
8 files changed
+121
-2
No files matched your search
@@ -65,3 +65,10 @@
|
||||
.themeacts .btn.sm, .themeacts .btn.sm:hover { height: 24px; padding: 0 10px; border: 1px solid var(--border); border-radius: var(--r-pill); background: var(--raised); color: var(--text); font: var(--w-reg) var(--fs-meta)/1 var(--ui); }
|
||||
.themefail { display: flex; align-items: center; margin-top: 8px; padding: 10px 12px; border: 0; border-radius: var(--r-md); background: color-mix(in srgb, var(--warn) 12%, transparent); color: var(--text); font: var(--w-reg) var(--fs-ui)/1.55 var(--ui); }
|
||||
.pal-rm[data-confirming], .pal-rm.btn.sm[aria-busy] { color: var(--err-ink); }
|
||||
.srcfb { font: var(--w-reg) var(--fs-ui)/1.7 var(--ui); }
|
||||
.srcfb .srcfb-hd { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; color: var(--faint); font-size: var(--fs-meta); }
|
||||
.srcfb .srcfb-tag { display: inline-grid; place-items: center; height: 20px; padding: 0 7px; border-radius: var(--r-pill); background: var(--overlay); color: var(--muted); font: var(--w-reg) var(--fs-micro)/1 var(--ui); white-space: nowrap; }
|
||||
.srcfb .srcfb-src { padding: 10px 12px; border-radius: var(--r-md); background: var(--page); font: var(--w-reg) var(--fs-ui)/1.7 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.toolfb { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--r-md); font: var(--w-reg) var(--fs-ui)/1.5 var(--ui); color: var(--text); }
|
||||
.toolfb .toolfb-nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.toolfb .srcfb-tag { display: inline-grid; place-items: center; height: 20px; padding: 0 7px; border-radius: var(--r-pill); background: var(--overlay); color: var(--muted); font: var(--w-reg) var(--fs-micro)/1 var(--ui); white-space: nowrap; }
|
||||
@@ -52,3 +52,18 @@ describe("the import results follow the board", () => {
|
||||
expect(rule(".ns-dlg .ns-drop")).toMatch(/border:\s*1px dashed var\(--border\)/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the as-written fallback follows the board", () => {
|
||||
it("tags the source in a 20px pill and sets it in the mono face on the page surface", () => {
|
||||
expect(rule(".srcfb .srcfb-tag")).toMatch(/height:\s*20px/);
|
||||
expect(rule(".srcfb .srcfb-src")).toMatch(/var\(--mono\)/);
|
||||
expect(rule(".srcfb .srcfb-src")).toMatch(/background:\s*var\(--page\)/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the simplified tool card follows the board", () => {
|
||||
it("is a one-line hairline row with the tag at its end", () => {
|
||||
expect(rule(".toolfb")).toMatch(/border-radius:\s*var\(--r-md\)/);
|
||||
expect(rule(".toolfb .toolfb-nm")).toMatch(/white-space:\s*nowrap/);
|
||||
});
|
||||
});
|
||||
@@ -204,6 +204,8 @@ export const CHAT: Messages = {
|
||||
"chat.invoke_a_skill_from_the": { zh: "调用系统提示里技能索引中的一份技能手册。", en: "Invoke a skill from the skills index in the system prompt." },
|
||||
"chat.invoke_a_skill_read_only": { zh: "以只读方式调用一个技能;子代理类技能在只读的隔离会话里运行。", en: "Invoke a skill read-only; sub-agent skills run in an isolated read-only session." },
|
||||
"chat.it_can_read_code_research": { zh: "可读取代码、查找资料、运行工具并修改文件;每一步都能在执行过程里展开查看。", en: "It can read code, research, run tools and edit files; every step can be opened in the run." },
|
||||
"chat.shown_as_written": { zh: "按原文显示", en: "Shown as written" },
|
||||
"chat.simplified_view": { zh: "简化显示", en: "Simplified view" },
|
||||
"chat.starter_explain_the_project": { zh: "解释这个项目的结构", en: "Explain this project's structure" },
|
||||
"chat.starter_find_the_regression": { zh: "找出最近的回归", en: "Find the latest regression" },
|
||||
"chat.starter_write_tests": { zh: "给改动写测试", en: "Write tests for the changes" },
|
||||
@@ -475,6 +477,7 @@ export const CHAT: Messages = {
|
||||
"chat.this_file_exceeds_the_preview": { zh: "文件超过预览大小限制", en: "This file exceeds the preview size limit." },
|
||||
"chat.this_file_has_changed_again": { zh: "该文件在检查点之后再次被修改。", en: "This file has changed again since the checkpoint." },
|
||||
"chat.this_is_a_question_from": { zh: "这是插件「{id}」的提问;回答它不会批准任何工具调用或权限。", en: "This is a question from the plugin \"{id}\"; answering it does not approve any tool call or permission." },
|
||||
"chat.this_layout_did_not_show": { zh: "这段排版没能显示,内容一字不少", en: "This layout could not be shown; nothing is missing" },
|
||||
"chat.this_link_targets_this_device": { zh: "此链接指向本机或私有网络。打开前请确认你信任该服务器和目标地址。", en: "This link targets this device or a private network. Open it only if you trust the server and the destination." },
|
||||
"chat.this_operates_another_application_on": { zh: "这会操作电脑上的另一个应用,拿到的是那个应用本身的权限。Windows 只把按键送给最前面的窗口,所以输入文字、按键和粘贴会先把这个应用切到前台。自动放行和宽泛规则都不能代你回答;要么为这个应用批准,要么切到全部放行。", en: "This operates another application on the computer, with that application's own access. Windows sends keys only to the window in front, so typing, key presses and pastes bring this application forward first. Auto and broad rules do not answer for you; approve it for this application, or switch to full access." },
|
||||
"chat.this_pass_folded_nothing": { zh: "本次未折叠任何内容", en: "This pass folded nothing" },
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ExtensionSurface } from "../port/wire";
|
||||
import type { AgentPort, ApprovalVerdict, Checkpoint, RewindPlan, RewindResult, RewindScope } from "../port/port";
|
||||
import { Hero } from "./Hero";
|
||||
import { Await } from "./Await";
|
||||
import { ToolCard } from "./cards/ToolCard";
|
||||
import { ToolCard } from "./cards/ToolCardSafe";
|
||||
import { Boundary } from "./Boundary";
|
||||
import { LazyChart } from "./chart/LazyChart";
|
||||
import { chartOfTool } from "./chart/spec";
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import "../testkit";
|
||||
import { SayCard } from "./SayCard";
|
||||
import type { Item } from "../../state/session";
|
||||
|
||||
vi.mock("../LazyMarkdown", () => ({
|
||||
LazyMarkdown: () => { throw new Error("typeset failed"); },
|
||||
}));
|
||||
|
||||
afterEach(() => { cleanup(); vi.restoreAllMocks(); });
|
||||
|
||||
describe("a reply whose layout cannot be drawn", () => {
|
||||
it("falls back to its text with a tag saying so, losing nothing", () => {
|
||||
vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const item = { t: "say", id: "s", text: "| 列 | 说明 |\n|---|---|\n| retry | 退避次数 |", done: true } as Extract<Item, { t: "say" }>;
|
||||
const { container } = render(<SayCard item={item} />);
|
||||
expect(container.querySelector(".srcfb-tag")?.textContent).toBe("按原文显示");
|
||||
expect(container.querySelector(".srcfb-hd")?.textContent).toContain("内容一字不少");
|
||||
expect(container.querySelector(".srcfb-src")?.textContent).toBe(item.text);
|
||||
});
|
||||
});
|
||||
@@ -217,7 +217,7 @@ export function SayCard({ item, afterAnswer, settled = true, reply, quiet }: { i
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<Boundary retryKey={item.text} fallback={<div className="md" style={{ whiteSpace: "pre-wrap" }}>{item.text}</div>}>
|
||||
<Boundary retryKey={item.text} fallback={<div className="srcfb"><div className="srcfb-hd"><span className="srcfb-tag">{t("chat.shown_as_written")}</span><span>{t("chat.this_layout_did_not_show")}</span></div><div className="md srcfb-src">{item.text}</div></div>}>
|
||||
<LazyMarkdown text={item.text} streaming={!item.done} local={reply?.files} />
|
||||
</Boundary>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import "../testkit";
|
||||
import { ToolCard } from "./ToolCardSafe";
|
||||
|
||||
vi.mock("./ToolCard", () => ({
|
||||
ToolCard: () => { throw new Error("card failed"); },
|
||||
}));
|
||||
|
||||
afterEach(() => { cleanup(); vi.restoreAllMocks(); });
|
||||
|
||||
const draw = (args: string | undefined) => {
|
||||
vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
return render(<ToolCard tool={{ name: "edit_file", args, readOnly: false }} running={false} />).container;
|
||||
};
|
||||
|
||||
describe("a tool card that cannot be drawn", () => {
|
||||
it("falls back to its name and target on one line, tagged as simplified", () => {
|
||||
const c = draw(JSON.stringify({ path: "internal/retry.go", old: "a" }));
|
||||
expect(c.querySelector(".toolfb-nm")?.textContent).toBe("edit_file · internal/retry.go");
|
||||
expect(c.querySelector(".srcfb-tag")?.textContent).toBe("简化显示");
|
||||
});
|
||||
|
||||
it("names only the call when its arguments do not parse", () => {
|
||||
const c = draw("{not json");
|
||||
expect(c.querySelector(".toolfb-nm")?.textContent).toBe("edit_file");
|
||||
});
|
||||
|
||||
it("names only the call when it was given nothing", () => {
|
||||
expect(draw(undefined).querySelector(".toolfb-nm")?.textContent).toBe("edit_file");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { t } from "../../i18n";
|
||||
import type { StateTool } from "../../state/outputcap";
|
||||
import { Boundary } from "../Boundary";
|
||||
import { ToolCard as Card } from "./ToolCard";
|
||||
|
||||
// The first string the call was given: the path or command it acted on.
|
||||
function target(args: string | undefined): string {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(args ?? "");
|
||||
if (parsed && typeof parsed === "object") {
|
||||
for (const value of Object.values(parsed)) if (typeof value === "string" && value) return value.split("\n")[0].slice(0, 120);
|
||||
}
|
||||
} catch {
|
||||
// arguments that do not parse have no target to name
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function Simplified({ tool }: { tool: StateTool }) {
|
||||
const to = target(tool.args);
|
||||
return (
|
||||
<div className="toolfb" role="group">
|
||||
<span className="toolfb-nm">{tool.name}{to && <> · {to}</>}</span>
|
||||
<span className="srcfb-tag">{t("chat.simplified_view")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** One call's card, falling back to a single line when drawing it throws, so a
|
||||
* card that cannot be drawn costs that call its detail and nothing else. */
|
||||
export function ToolCard(props: ComponentProps<typeof Card>) {
|
||||
return (
|
||||
<Boundary retryKey={[props.tool.args, props.tool.output, props.running]} fallback={<Simplified tool={props.tool} />}>
|
||||
<Card {...props} />
|
||||
</Boundary>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user