feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1

Merged
Blizzard merged 181 commits from feat/wails3 into main 2026-07-17 01:12:32 +00:00
Showing only changes of commit 429b04d64a - Show all commits
@@ -1,8 +1,9 @@
import { useState } from "react"; import { useCallback, useRef, useState } from "react";
import { ChevronDown, ChevronUp, Wrench, ShieldCheck, Check, X } from "lucide-react"; import { ChevronDown, ChevronUp, Wrench, ShieldCheck, Check, X, Maximize2, Minimize2 } from "lucide-react";
import { deriveNodes, pendingApproval, type RunState } from "../lib/run"; import { deriveNodes, pendingApproval, type RunState } from "../lib/run";
import { ExecTrace } from "../components/ExecTrace"; import { ExecTrace } from "../components/ExecTrace";
import { ChartView } from "../components/ChartView"; import { ChartView } from "../components/ChartView";
import { Markdown } from "../components/Markdown";
import { extractChartBlocks, hasChart } from "../lib/chartspec"; import { extractChartBlocks, hasChart } from "../lib/chartspec";
import { approveTask } from "../lib/api"; import { approveTask } from "../lib/api";
import { Tabs, Badge, cn, type TabDef } from "../ui"; import { Tabs, Badge, cn, type TabDef } from "../ui";
@@ -13,6 +14,35 @@ type Tab = "output" | "trace" | "tools" | "cite" | "eval";
export function BottomDrawer({ run }: { run: RunState }) { export function BottomDrawer({ run }: { run: RunState }) {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
const [tab, setTab] = useState<Tab>("output"); const [tab, setTab] = useState<Tab>("output");
const [height, setHeight] = useState(240); // 可拖拽调节的抽屉高度
const prevH = useRef(240);
// 拖拽顶边调高:监听 window,高度 = 视口高 - 鼠标 Y,夹在 [140, 85vh]。
const startResize = useCallback(() => {
const onMove = (e: MouseEvent) => {
const h = Math.min(window.innerHeight * 0.85, Math.max(140, window.innerHeight - e.clientY));
setOpen(true);
setHeight(h);
};
const onUp = () => {
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
document.body.style.userSelect = "";
};
document.body.style.userSelect = "none";
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp);
}, []);
const maximized = height >= window.innerHeight * 0.8;
const toggleMax = () => {
if (maximized) setHeight(prevH.current || 240);
else {
prevH.current = height;
setHeight(Math.round(window.innerHeight * 0.82));
}
setOpen(true);
};
const nodes = deriveNodes(run.exec); const nodes = deriveNodes(run.exec);
const toolCount = nodes.filter((n) => n.kind === "tool").length; const toolCount = nodes.filter((n) => n.kind === "tool").length;
@@ -38,6 +68,8 @@ export function BottomDrawer({ run }: { run: RunState }) {
return ( return (
<div className="shrink-0 border-t border-line bg-ink-900"> <div className="shrink-0 border-t border-line bg-ink-900">
{approval && run.taskId && <ApprovalBar taskId={run.taskId} node={approval.node} title={approval.title} summary={approval.summary} />} {approval && run.taskId && <ApprovalBar taskId={run.taskId} node={approval.node} title={approval.title} summary={approval.summary} />}
{/* 拖拽调高手柄(横跨顶边) */}
{open && <div onMouseDown={startResize} className="h-1 cursor-ns-resize bg-transparent hover:bg-brand/40 transition" title="拖动调整高度" />}
<div className="flex items-center border-b border-line px-2"> <div className="flex items-center border-b border-line px-2">
<Tabs <Tabs
tabs={tabs} tabs={tabs}
@@ -48,13 +80,20 @@ export function BottomDrawer({ run }: { run: RunState }) {
}} }}
/> />
<span className={cn("ml-2 text-[11px]", statusCls)}>{statusText}</span> <span className={cn("ml-2 text-[11px]", statusCls)}>{statusText}</span>
<button onClick={() => setOpen((o) => !o)} className="ml-auto flex items-center gap-1 px-2 py-2 text-xs text-slate-500 hover:text-slate-300"> <div className="ml-auto flex items-center">
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronUp className="h-3.5 w-3.5" />} {open && (
{open ? "收起" : "展开"} <button onClick={toggleMax} className="flex items-center px-2 py-2 text-slate-500 hover:text-slate-300" title={maximized ? "还原" : "最大化"}>
</button> {maximized ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
</button>
)}
<button onClick={() => setOpen((o) => !o)} className="flex items-center gap-1 px-2 py-2 text-xs text-slate-500 hover:text-slate-300">
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronUp className="h-3.5 w-3.5" />}
{open ? "收起" : "展开"}
</button>
</div>
</div> </div>
{open && ( {open && (
<div className="h-44 overflow-auto p-3 text-xs"> <div className="overflow-auto p-3 text-xs" style={{ height }}>
{tab === "output" && <OutputView output={run.output} />} {tab === "output" && <OutputView output={run.output} />}
{tab === "trace" && <ExecTrace events={run.exec} phase={run.phase} />} {tab === "trace" && <ExecTrace events={run.exec} phase={run.phase} />}
{tab === "tools" && <ToolCalls run={run} />} {tab === "tools" && <ToolCalls run={run} />}
@@ -123,26 +162,19 @@ function ApprovalBar({ taskId, node, title, summary }: { taskId: string; node: s
// OutputView:渲染模型输出。含 ```chart 块时分段渲染(文本 + SVG 图表),否则纯文本。 // OutputView:渲染模型输出。含 ```chart 块时分段渲染(文本 + SVG 图表),否则纯文本。
function OutputView({ output }: { output: string }) { function OutputView({ output }: { output: string }) {
if (!output) { if (!output) {
return ( return <div className="text-xs leading-relaxed text-slate-500"> Markdown </div>;
<pre className="whitespace-pre-wrap font-mono leading-relaxed text-emerald-300">
token
</pre>
);
} }
if (!hasChart(output)) { if (!hasChart(output)) {
return <pre className="whitespace-pre-wrap font-mono leading-relaxed text-emerald-300">{output}</pre>; return <Markdown text={output} className="text-[13px]" />;
} }
// 含 ```chart 块:文本段渲染 Markdown,图表段渲染 SVG,保持原顺序。
return ( return (
<div> <div>
{extractChartBlocks(output).map((seg, i) => {extractChartBlocks(output).map((seg, i) =>
seg.kind === "chart" ? ( seg.kind === "chart" ? (
<ChartView key={i} spec={seg.spec} /> <ChartView key={i} spec={seg.spec} />
) : ( ) : (
seg.text.trim() && ( seg.text.trim() && <Markdown key={i} text={seg.text} className="text-[13px]" />
<pre key={i} className="whitespace-pre-wrap font-mono leading-relaxed text-emerald-300">
{seg.text}
</pre>
)
), ),
)} )}
</div> </div>