feat: add file edit diff progress view

Capture file edit snapshots through runner tool lifecycle hooks and render unified diffs in the WebUI with folding and truncation controls.
This commit is contained in:
chengyongru
2026-07-09 10:42:43 +08:00
committed by Xubin Ren
parent 207813d3b5
commit 7768672c5b
40 changed files with 2224 additions and 1753 deletions
+53
View File
@@ -73,6 +73,9 @@ async def test_composite_fans_out_all_async_methods():
async def emit_reasoning(self, reasoning_content: str | None) -> None:
events.append(f"emit_reasoning:{reasoning_content}")
async def emit_reasoning_end(self) -> None:
events.append("emit_reasoning_end")
async def on_stream(self, context: AgentHookContext, delta: str) -> None:
events.append(f"on_stream:{delta}")
@@ -82,6 +85,15 @@ async def test_composite_fans_out_all_async_methods():
async def before_execute_tools(self, context: AgentHookContext) -> None:
events.append("before_execute_tools")
async def before_execute_tool(self, context, tool_call, tool, params) -> None:
events.append("before_execute_tool")
async def after_execute_tool(self, context, tool_call, tool, params, result) -> None:
events.append("after_execute_tool")
async def on_execute_tool_error(self, context, tool_call, tool, params, error) -> None:
events.append("on_execute_tool_error")
async def after_iteration(self, context: AgentHookContext) -> None:
events.append("after_iteration")
@@ -101,9 +113,13 @@ async def test_composite_fans_out_all_async_methods():
await hook.before_run(run_ctx)
await hook.before_iteration(ctx)
await hook.emit_reasoning("thinking...")
await hook.emit_reasoning_end()
await hook.on_stream(ctx, "hi")
await hook.on_stream_end(ctx, resuming=True)
await hook.before_execute_tools(ctx)
await hook.before_execute_tool(ctx, object(), object(), {})
await hook.after_execute_tool(ctx, object(), object(), {}, "ok")
await hook.on_execute_tool_error(ctx, object(), object(), {}, "err")
await hook.after_iteration(ctx)
await hook.after_run(run_ctx)
await hook.on_error(run_ctx)
@@ -113,9 +129,13 @@ async def test_composite_fans_out_all_async_methods():
"before_run", "before_run",
"before_iteration", "before_iteration",
"emit_reasoning:thinking...", "emit_reasoning:thinking...",
"emit_reasoning_end", "emit_reasoning_end",
"on_stream:hi", "on_stream:hi",
"on_stream_end:True", "on_stream_end:True",
"before_execute_tools", "before_execute_tools",
"before_execute_tool", "before_execute_tool",
"after_execute_tool", "after_execute_tool",
"on_execute_tool_error", "on_execute_tool_error",
"after_iteration", "after_iteration",
"after_run", "after_run",
"on_error", "on_error",
@@ -172,10 +192,20 @@ async def test_composite_error_isolation_all_async():
raise RuntimeError("err")
async def emit_reasoning(self, reasoning_content):
raise RuntimeError("err")
async def emit_reasoning_end(self):
raise RuntimeError("err")
async def on_stream(self, context, delta):
raise RuntimeError("err")
async def on_stream_end(self, context, *, resuming):
raise RuntimeError("err")
async def before_execute_tools(self, context):
raise RuntimeError("err")
async def before_execute_tool(self, context, tool_call, tool, params):
raise RuntimeError("err")
async def after_execute_tool(self, context, tool_call, tool, params, result):
raise RuntimeError("err")
async def on_execute_tool_error(self, context, tool_call, tool, params, error):
raise RuntimeError("err")
async def after_iteration(self, context):
raise RuntimeError("err")
async def after_run(self, context):
@@ -190,10 +220,20 @@ async def test_composite_error_isolation_all_async():
calls.append("before_run")
async def emit_reasoning(self, reasoning_content):
calls.append("emit_reasoning")
async def emit_reasoning_end(self):
calls.append("emit_reasoning_end")
async def on_stream(self, context, delta):
calls.append("on_stream")
async def on_stream_end(self, context, *, resuming):
calls.append("on_stream_end")
async def before_execute_tools(self, context):
calls.append("before_execute_tools")
async def before_execute_tool(self, context, tool_call, tool, params):
calls.append("before_execute_tool")
async def after_execute_tool(self, context, tool_call, tool, params, result):
calls.append("after_execute_tool")
async def on_execute_tool_error(self, context, tool_call, tool, params, error):
calls.append("on_execute_tool_error")
async def after_iteration(self, context):
calls.append("after_iteration")
async def after_run(self, context):
@@ -208,8 +248,13 @@ async def test_composite_error_isolation_all_async():
run_ctx = _run_ctx()
await hook.before_run(run_ctx)
await hook.emit_reasoning("test")
await hook.emit_reasoning_end()
await hook.on_stream(ctx, "delta")
await hook.on_stream_end(ctx, resuming=False)
await hook.before_execute_tools(ctx)
await hook.before_execute_tool(ctx, object(), object(), {})
await hook.after_execute_tool(ctx, object(), object(), {}, "ok")
await hook.on_execute_tool_error(ctx, object(), object(), {}, "err")
await hook.after_iteration(ctx)
await hook.after_run(run_ctx)
await hook.on_error(run_ctx)
@@ -217,8 +262,13 @@ async def test_composite_error_isolation_all_async():
assert calls == [
"before_run",
"emit_reasoning",
"emit_reasoning_end",
"on_stream",
"on_stream_end",
"before_execute_tools",
"before_execute_tool",
"after_execute_tool",
"on_execute_tool_error",
"after_iteration",
"after_run",
"on_error",
@@ -313,6 +363,9 @@ async def test_composite_empty_hooks_no_ops():
await hook.on_stream(ctx, "delta")
await hook.on_stream_end(ctx, resuming=False)
await hook.before_execute_tools(ctx)
await hook.before_execute_tool(ctx, object(), object(), {})
await hook.after_execute_tool(ctx, object(), object(), {}, None)
await hook.on_execute_tool_error(ctx, object(), object(), {}, "err")
await hook.after_iteration(ctx)
await hook.after_run(run_ctx)
await hook.on_error(run_ctx)