




















@@ -480,7 +480,14 @@ def run_claude(args: argparse.Namespace, repo: Path, prompt: str) -> str:
480480cmd.extend(["--model", args.model])
481481if args.thinking:
482482cmd.extend(["--effort", args.thinking])
483-result = run_with_heartbeat(cmd, repo, input_text=prompt, label="claude", stream_output=args.stream_engine_output)
483+result = run_with_heartbeat(
484+cmd,
485+repo,
486+input_text=prompt,
487+label="claude",
488+stream_output=args.stream_engine_output,
489+stream_display=ClaudeStreamDisplay() if args.stream_engine_output else None,
490+ )
484491if result.returncode != 0:
485492raise SystemExit(f"claude engine failed ({result.returncode})\n{result.stderr or result.stdout}")
486493return result.stdout
@@ -598,6 +605,80 @@ class CodexStreamDisplay:
598605return text
599606600607608+class ClaudeStreamDisplay:
609+def __init__(self, *, activity_seconds: int = 20) -> None:
610+self.activity_seconds = activity_seconds
611+self.hidden_events = 0
612+self.last_visible = time.monotonic()
613+self.started = False
614+615+def __call__(self, name: str, line: str) -> str | None:
616+if name != "stdout":
617+return line
618+try:
619+event = json.loads(line)
620+except json.JSONDecodeError:
621+return self.visible(line)
622+event_type = event.get("type")
623+if event_type == "system" and not self.started:
624+self.started = True
625+return self.visible("claude turn started\n")
626+if event_type == "assistant":
627+return self.assistant_message(event)
628+if event_type == "result":
629+return self.visible(self.flush_hidden() + self.result_summary(event))
630+return self.hidden_activity()
631+632+def assistant_message(self, event: dict[str, Any]) -> str | None:
633+message = event.get("message")
634+if not isinstance(message, dict):
635+return self.hidden_activity()
636+chunks: list[str] = []
637+for item in message.get("content", []):
638+if not isinstance(item, dict):
639+continue
640+if item.get("type") == "text" and isinstance(item.get("text"), str):
641+chunks.append(item["text"].rstrip())
642+if chunks:
643+return self.visible(self.flush_hidden() + "\n".join(chunks) + "\n")
644+return self.hidden_activity()
645+646+def result_summary(self, event: dict[str, Any]) -> str:
647+usage = event.get("usage")
648+fields: list[str] = []
649+if isinstance(usage, dict):
650+for key in (
651+"input_tokens",
652+"cache_read_input_tokens",
653+"cache_creation_input_tokens",
654+"output_tokens",
655+ ):
656+value = usage.get(key)
657+if isinstance(value, int):
658+fields.append(f"{key}={value}")
659+cost = event.get("total_cost_usd")
660+if isinstance(cost, (int, float)) and not isinstance(cost, bool):
661+fields.append(f"cost_usd={cost:.6f}")
662+return "claude usage: " + " ".join(fields) + "\n" if fields else "claude turn completed\n"
663+664+def hidden_activity(self) -> str | None:
665+self.hidden_events += 1
666+if time.monotonic() - self.last_visible < self.activity_seconds:
667+return None
668+return self.visible(self.flush_hidden())
669+670+def flush_hidden(self) -> str:
671+if not self.hidden_events:
672+return ""
673+count = self.hidden_events
674+self.hidden_events = 0
675+return f"claude activity: {count} hidden tool/status events\n"
676+677+def visible(self, text: str) -> str:
678+self.last_visible = time.monotonic()
679+return text
680+681+601682def format_codex_usage(usage: dict[str, Any]) -> str:
602683fields = [
603684"input_tokens",
@@ -646,7 +727,7 @@ def extract_json(text: str) -> dict[str, Any]:
646727647728648729def extract_json_from_jsonl(text: str) -> dict[str, Any] | None:
649-candidates: list[str] = []
730+candidates: list[str | dict[str, Any]] = []
650731for line in text.splitlines():
651732line = line.strip()
652733if not line:
@@ -665,7 +746,13 @@ def extract_json_from_jsonl(text: str) -> dict[str, Any] | None:
665746candidates.append(data["content"])
666747if isinstance(event.get("result"), str):
667748candidates.append(event["result"])
749+if isinstance(event.get("structured_output"), dict):
750+candidates.append(event["structured_output"])
668751for candidate in reversed(candidates):
752+if isinstance(candidate, dict):
753+if "findings" in candidate:
754+return candidate
755+continue
669756parsed = parse_json_candidate(candidate)
670757if isinstance(parsed, dict) and "findings" in parsed:
671758return parsed
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。