feat(agent): 诊断 run 取消/终止原因,取代笼统的中断提示

- 新增 AbortCause(cancelcause.go):各取消点用 WithCancelCause/WithTimeoutCause
  附上原因(暂停/删除/kill_work/目标达成/drain 兜底/聊天停止/进程关闭/硬超时…),
  captureRun 用 context.Cause 还原出到底是谁、为何取消。
- 新增 terminalreason.go:把终端状态转成可读的一行摘要 + 多行详情(取消原因、
  终止 reason、轮数、卡住的工具),替换原来固定那句猜测式提示。
- engine/worker/planner/capture 及 conversations/orchestration/server 接入;
  cmd/artex/main.go、前端 llm/main-content 相应调整。
- 配套测试 terminalreason_test.go / engine_cancelcause_test.go。
This commit is contained in:
Autumn-27 committed 2026-08-19 09:20:10 -04:00
1 parent 8e9b126972
commit a6bd18050c
16 files changed
+624 -60

No files matched your search

@@ -62,7 +62,7 @@ export function MainContent({ children }: { children: ReactNode }) {
</div>
<div className="flex items-center gap-2">
{version && (
<span className="text-muted-foreground text-xs font-medium tabular-nums">版本·v{version}</span>
<span className="text-muted-foreground text-xs font-medium tabular-nums">版本 · {version}</span>
)}
<LayoutControls />
<ThemeSwitcher />
+10 -5
View File
@@ -183,7 +183,12 @@ function NewProfileDialog({ onCreated }: { onCreated: (id: string) => void }) {
value={model}
onChange={(e) => setModel(e.target.value)}
/>
<Popover open={modelsOpen} onOpenChange={setModelsOpen}>
{/* modal: this Popover lives inside a Dialog, and its content is
portaled to <body> — outside the Dialog's react-remove-scroll
lock, which then cancels every wheel event over it (the list
rendered but refused to scroll). modal makes the Popover own
the topmost scroll lock, so its own list scrolls again. */}
<Popover open={modelsOpen} onOpenChange={setModelsOpen} modal>
<PopoverTrigger asChild>
<Button
type="button"
@@ -198,12 +203,12 @@ function NewProfileDialog({ onCreated }: { onCreated: (id: string) => void }) {
</Button>
</PopoverTrigger>
{models.length > 0 && (
<PopoverContent className="w-72 max-h-64 overflow-y-auto p-1" align="end">
<PopoverContent className="max-h-72 w-72 gap-0 overflow-y-auto overscroll-contain p-1" align="end">
{models.map((m) => (
<button
key={m}
type="button"
className="w-full rounded-md px-2 py-1.5 text-left font-mono text-xs hover:bg-accent hover:text-accent-foreground"
className="w-full shrink-0 rounded-md px-2 py-1.5 text-left font-mono text-xs hover:bg-accent hover:text-accent-foreground"
onClick={() => {
setModel(m);
setModelsOpen(false);
@@ -567,12 +572,12 @@ export default function LLMPage() {
</Button>
</PopoverTrigger>
{models.length > 0 && (
<PopoverContent className="w-72 max-h-64 overflow-y-auto p-1" align="end">
<PopoverContent className="max-h-72 w-72 gap-0 overflow-y-auto overscroll-contain p-1" align="end">
{models.map((m) => (
<button
key={m}
type="button"
className="w-full rounded-md px-2 py-1.5 text-left font-mono text-xs hover:bg-accent hover:text-accent-foreground"
className="w-full shrink-0 rounded-md px-2 py-1.5 text-left font-mono text-xs hover:bg-accent hover:text-accent-foreground"
onClick={() => {
setModel(m);
setModelsOpen(false);