Commit Graph

23 Commits

Author SHA1 Message Date
DaiChaoXiong 9a2d50ee58 fix(core): stream tool output live via ToolExecutionUpdate events
The agent passed None for on_update, so tools (e.g. shell) never forwarded
their incremental output — the UI only saw the final result. Now:

- EventSink: Send + Sync (the forwarding closure must satisfy ToolUpdateSink)
- ToolUpdateSink becomes lifetime-parameterized (bare trait-object aliases
  default to 'static, which forbade borrowing the caller's sink); all tool
  impls updated to &ToolUpdateSink<'_>
- Agent::execute_single forwards each tool update as a ToolExecutionUpdate
  event (Mutex provides interior mutability inside the Fn closure)
- TUI: running tools show the tail of the output live (newest lines), done
  tools show the head + remaining-line hint

regression test: a streaming tool's on_update texts appear as agent events in
order.
2026-08-09 22:55:16 +08:00
DaiChaoXiong 7d289580b5 fix(tui): merged tool request+result blocks; fix wheel-down bottom jump
1. wheel-down jumped to the bottom because max_scroll() derived the limit from
   geometry, which only covers expandable blocks — a text-only conversation
   left it at 0, so one down-notch clamped scroll to 0 with follow=true and
   the next draw snapped to the real bottom. The limit now uses
   content_lines (the full rendered line count, stored per draw).

2. tool request and result are now one block ('requested where it returns'):
   UiBlock::ToolCall/ToolResult/ToolExec are unified into UiBlock::Tool, built
   by merging each assistant tool call with its matching tool_result message
   (looked up by call_id). Collapsed shows the concrete summary + duration +
   status; expanded shows human-readable args, then '── 结果 ──', then the
   output (two-level: partial preview with remaining-line hint, then full).
   Live running executions reuse the same block. ToolResult messages are no
   longer rendered standalone.

tests updated for the merged model; wheel test now drives content_lines.
2026-08-09 22:43:01 +08:00
DaiChaoXiong acddf7be1b fix(tui): correct wheel/PageUp scroll direction and line-step
The scroll offset semantics were inverted for the wheel and PageUp:
is the content offset (larger = newer), so scrolling up must DECREASE it, but
ScrollUp and PageUp both INCREASED it — every wheel event (either direction)
moved toward the bottom, and at the bottom (follow=true) an up-notch added 3
then got clamped back to max, making the wheel dead until keyboard-scrolling
away. Now:

- wheel ScrollUp/ScrollLeft: offset -1, follow cleared (works from the bottom)
- wheel ScrollDown/ScrollRight: offset +1, follow restored at the bottom
- PageUp: offset - view_height; PageDown: offset + view_height
- one line per wheel notch (was 3) for line-by-line scrolling

regression test drives handle_mouse with synthetic events (direction,
line-step, no underflow, follow transitions).
2026-08-09 22:34:51 +08:00
DaiChaoXiong 4b5f4cf52b feat(tui): cross-platform wheel scroll and two-level expansion
1. wheel: scrolling up now clears follow (the real bug — with follow=true every
   draw snapped back to the bottom, so the wheel appeared dead on both Linux
   and Windows); crossterm already normalizes xterm (Linux) and ConPTY
   (Windows) wheel events to ScrollUp/ScrollDown, and ScrollLeft/ScrollRight
   are handled too
2. expansion becomes three-state (collapsed → partial → full → collapsed):
   the first expand shows the first 20 lines with a '… 还有 N 行(再次点击/Tab
   展开全部)' hint; the second expand reveals everything. Applies to thinking,
   tool-execution output and tool-result content. ToolCall args stay
   single-level human-readable.

tests: three-state cycle, full-flag propagation
2026-08-09 22:30:04 +08:00
DaiChaoXiong c6660e0b84 feat(tui): markdown rendering, real-time transcript, scrolling, human args
1. scrolling: ↑/↓ scroll the messages when the input is empty (otherwise the
   caret moves), Ctrl+U scrolls up line-by-line, PageUp/PageDown page, wheel
   scrolls; follow-to-bottom restored on reaching the end
2. markdown: hand-rolled renderer (headers, bold/italic, inline code, fenced
   code blocks, lists, quotes, links, rules) applied to assistant/user text
3. ordering/refresh: messages are now committed into the transcript in real
   time at MessageEnd (assistant + tool results), so earlier turns never
   vanish mid-run and the latest content stays at the bottom; the live view
   shows only the streaming partial plus running tool executions (finished
   ones are presented by their tool_result messages, with duration)
4/5. expanded tool calls show human-readable arguments (command:/path:/edits
   as key-value lines) instead of raw JSON; tool results show duration

new tests: markdown renderer, format_args_human, commit_partial, live
running-only execs, tool-duration propagation
2026-08-09 22:21:30 +08:00
DaiChaoXiong 1d1f50d1b1 fix(providers): keep the API's real tool-call ids across turns
Multi-turn tool calls collided on replay: the reducer regenerated call_0,
call_1, ... every turn while ignoring the API's real call ids, so replayed
history had duplicate function_call/function_call_output ids that the API
cannot pair (visible once a conversation has two tool-using turns).

- ProviderEventReducer::tool_call_start_with_id(index, name, id) keeps the
  API's id; tool_call_start delegates with an auto-generated fallback
- Responses API: use item.call_id; Chat Completions: use tool_calls[].id
- regressions: real-log test asserts the captured ids, agent replay test
  asserts ids stay unique across three turns; reducer id-override test
2026-08-09 22:07:18 +08:00
DaiChaoXiong 6257f4f6b8 test(providers): real-endpoint-log regressions for Responses reasoning
Based on the user's actual deepseek-v4-flash debug log (Responses protocol):

- real_log_captures_reasoning_and_tools: replays turn 1 of the real SSE
  (reasoning item + reasoning_text.delta + two function calls) and asserts the
  Done message is [Thinking, ToolCall, ToolCall] with usage mapped
  (input 850 / output 157 / cache-read 768)
- agent_multi_turn_replays_reasoning: drives a 3-turn agent over the real
  protocol (turn 1 + turn 2 from the log, plus a terminating text turn),
  asserting thinking lands in the transcript and the second request's replay
  carries a reasoning item with the captured text and matching call ids

MockTransport gains requests() to inspect all recorded requests.
2026-08-09 22:04:11 +08:00
DaiChaoXiong 80333ee127 fix(providers): capture and replay Responses API reasoning items
The real endpoint log (deepseek-v4-flash) shows the Responses protocol: the
thinking arrives as a 'reasoning' output item plus response.reasoning_text.delta
events, which we ignored — so the thinking never reached the transcript and
the replayed history omitted it ('reasoning_text must be passed back', 400).

- output_item.added with item.type 'reasoning' opens a thinking block;
  response.reasoning_text.delta appends to it
- messages_to_responses now replays assistant thinking as a 'reasoning' item
  with a reasoning_text part (before the text item)
- regression tests: reasoning capture and reasoning-item replay
- (the earlier chat-completions reasoning_content/text fixes remain for the
  chat protocol)
2026-08-09 21:58:16 +08:00
DaiChaoXiong fc6b15a407 fix(providers): capture reasoning from multiple field names and echo both
The replay error ('reasoning_text in thinking mode must be passed back')
persisted because the endpoint emits reasoning under a name other than
reasoning_content — e.g. reasoning_text — which we never captured, so the
thinking never reached the transcript.

- capture reasoning from reasoning_content / reasoning_text / reasoning /
  thinking (first non-empty wins)
- echo the captured reasoning back as BOTH reasoning_content and
  reasoning_text on assistant messages
- FOCUS_DEBUG_FILE=<path> appends every raw network chunk to a file for
  diagnosing provider-protocol mismatches (both providers)
- regression tests: reasoning_text capture, dual-field replay
2026-08-09 21:54:07 +08:00
DaiChaoXiong 107f1f9f42 fix(providers): echo reasoning_content back for thinking-mode APIs
DeepSeek's thinking mode rejects a replayed assistant message that omits its
reasoning ('the reasoning_text must be passed back to the API', HTTP 400).
The Chat Completions serializer now includes thinking blocks as
reasoning_content on assistant messages; OpenAI official never produces
thinking blocks, so the field is only sent when present — safe for both.
Regression test: a replayed assistant carries reasoning_content.
2026-08-09 21:48:29 +08:00
DaiChaoXiong a9de6fe24e fix(tui): surface concrete run errors instead of a bare error header
StreamEvent::Error is encoded by the agent loop as an assistant message with
stop_reason=error and error_message, but the UI only rendered the header and
the (empty) content, hiding the actual message (e.g. a provider HTTP 400
detail). Now:

- UiBlock::Error renders error_message in red, for both committed and live
  assistant messages
- on job Done, the error is also surfaced into the status bar and a note,
  even when the job itself reported no error (it was encoded as a message)
- regression test: an error message renders its concrete text
2026-08-09 21:48:29 +08:00
DaiChaoXiong 4ffd82b096 feat(tui): animated working indicator with phase detection
- Braille spinner driven by a per-frame counter, shown in both the status
  bar (bottom) and at the bottom of the message area while a job runs
- phase detection (RunState::working_phase): running tool > thinking >
  streaming > generic, so the hint reads e.g. "⠹ shell: cargo test…"
- testable via the injected clock (tests/state_tests.rs)
2026-08-09 21:43:28 +08:00
DaiChaoXiong 676dc1503e feat(tui): add focus-tui terminal UI
- ratatui + crossterm chat UI: streaming assistant text, multi-line input,
  bottom status bar (model / protocol / session id / context usage)
- thinking blocks collapse to a summary with duration + estimated tokens;
  tool calls collapse to a concrete summary (path / command) + duration;
  click or Tab expands the details
- config modal (/config): provider, OpenAI protocol, baseUrl, apiKey, model,
  contextWindow — persisted to ~/.focus/config.json; session list (/sessions)
  on the harness JSONL tree; /new, /compact, /help
- auto-compaction (80% threshold, no confirmation) + dynamic system prompt
  with usage info run in a background job (fake-provider-testable)
- Esc aborts a run (abandons the job thread; bounded by the provider timeout)
- tests: config codec, summaries/durations/wrap/caret, UI state machine with
  an injected clock, job flow with a fake provider, session persistence
2026-08-09 21:35:58 +08:00
DaiChaoXiong f695acc845 refactor(test): move all unit tests from src/ into tests/<module>_tests.rs
Per AGENTS.md §5.1 every test must live under crates/<name>/tests/, so the
inline #[cfg(test)] modules in harness/tools/providers/transport are gone:

- harness: session_tests / compaction_tests / prompt_tests (days_to_ymd made pub)
- tools: read / write / edit / shell tests (Tool trait imported explicitly)
- providers: config_tests; request-body and stop-reason coverage folded into
  integration tests via mock-recorded requests and SSE events (private builders
  no longer tested directly)
- transport: ChunkedDecoder made pub (with Default), decoder tests moved into
  transport_tests.rs

The only #[cfg(test)] left in src/ is the test-only TLS infra in tls.rs.
2026-08-09 21:09:01 +08:00
DaiChaoXiong 7c4c465a18 chore(workspace): sync manifests, docs and lockfile
- drop focus-cli from workspace members; remove tokio from focus-tools and
  focus-harness (sync std I/O); add focus-json to focus-tools
- AGENTS.md: reflect 6-crate layout, focus-cli removal, TUI coming later
- lockfile updated for the new dependency graph
2026-08-09 21:00:04 +08:00
DaiChaoXiong 647cf1ba83 feat(harness): session tree, compaction plans, system prompt templates
- SessionStore/SessionTree: JSONL append-only tree (id + parentId) under
  ~/.focus/sessions, branching and continuation queries
- compaction: lightweight token estimation; scheme D+E plans (summary +
  key-facts, keep recent messages, tool_call/tool_result pairs kept intact);
  auto-trigger threshold (default 80%, configurable); apply_summary writes back
- prompts: system template with cwd/os/date/shell + context occupancy and
  last-turn usage (scheme E); pure-std date formatting
2026-08-09 20:59:59 +08:00
DaiChaoXiong ee165235ec feat(tools): add cross-platform read/write/edit/shell tools
- read: line-range reads, binary detection
- write: overwrite with parent-dir creation
- edit: pi-style exact oldString/newString replacement with occurrence
- shell: bash on Linux, PowerShell (cmd fallback) on Windows; streaming
  line updates via ToolUpdateSink, kill-on-timeout
- sync std I/O consistent with the core Tool trait; no tokio needed
2026-08-09 20:59:54 +08:00
DaiChaoXiong 8ed56c3952 feat(providers): rewrite Anthropic and OpenAI providers
- Anthropic Messages API (2023-06-01) SSE streaming: text/thinking/tool_use
  blocks, cache usage, stop-reason mapping, errors encoded as events
- OpenAI Responses API + Chat Completions (switchable), tool_calls delta
  accumulation, include_usage, reasoning_content -> thinking
- ProviderConfig with base_url/api_key/context_window/timeout; known-model
  context-window table with three-layer resolution
- background current-thread runtime bridges sync agent loop to async I/O
- mock-transport replay tests (chunks straddle SSE boundaries) + full agent
  integration test
2026-08-09 20:59:49 +08:00
DaiChaoXiong 112342c42e feat(core): agent message setters, thinking support, reducer arg-slot fix
- Agent::set_system_prompt / replace_messages (dynamic prompts + compaction)
- ProviderEventReducer::thinking_delta with per-block tracking
- fix: interleaved text/tool-call streams no longer misroute partial args
  (args aligned per content slot; regression tests added)
2026-08-09 20:59:39 +08:00
DaiChaoXiong 7f00886150 feat(transport): add incremental streaming body reader and custom ports
- Transport::stream_request returns a ChunkStream for true SSE streaming
  (head read incrementally, body drained chunk by chunk)
- transparent incremental chunked-transfer decoding (ChunkedDecoder)
- HttpRequest.port for non-standard base_url ports (Host header + TLS connect)
- per-read timeouts on the streaming path
2026-08-09 20:59:33 +08:00
DaiChaoXiong 32b630e440 chore: remove focus-cli and obsolete provider sources
focus-cli is deleted entirely (the TUI will be a new crate later); the old
Anthropic/OpenAI/Zai provider sources and the core mock module are removed as
part of the rewrite.
2026-08-09 20:59:29 +08:00
DaiChaoXiong 5364ca004c ADD: 注释 2026-07-17 11:19:22 +08:00
DaiChaoXiong a7ddba035a INIT 2026-07-16 17:59:16 +08:00