Skip to content

feat(desktop): React frontend + Tauri shell + bridge rewrite#684

Open
abraxas914 wants to merge 881 commits into
lsdefine:mainfrom
abraxas914:main
Open

feat(desktop): React frontend + Tauri shell + bridge rewrite#684
abraxas914 wants to merge 881 commits into
lsdefine:mainfrom
abraxas914:main

Conversation

@abraxas914

Copy link
Copy Markdown
Contributor

Summary

Complete desktop client implementation for GenericAgent — a React + Tauri hybrid app providing native GUI access to the agent system.

What's included

Frontend (frontends/desktop/)

  • React scaffold: Vite + Zustand + Semi UI, bilingual i18n (zh/en), dark/light theme
  • Chat UI: Composer, MessageList, SessionList, streaming partial updates via WebSocket
  • Thread rendering: Markdown pipeline, thinking disclosure, tool call rows, progressive disclosure (L1/L2/L3)
  • Settings & model management: CRUD model profiles, provider presets, per-session model binding
  • Token stats: Per-prompt cost tracking, time filters, cache rate formula
  • Notifications & connection status: Toast system, offline overlay, bridge status panel
  • Data maintenance: Local repo connection, memory import, key configuration
  • Platform adaptation: macOS traffic lights, Windows frameless chrome, event-delegation drag

Tauri shell (frontends/desktop/src-tauri/)

  • Tauri v2 config, native commands, window management
  • macOS DMG repackage script with Finder layout
  • Windows UCRT bundling, shortcut prompt, tray lifecycle

Bridge & backend (frontends/)

  • Full desktop bridge rewrite: session persistence, image/file injection, streaming
  • Conductor enhanced orchestration with error feedback
  • Model routing isolation (session-level binding)

CI (.github/workflows/)

  • Desktop release workflow: 3-platform build matrix (macOS/Windows/Linux)
  • Frontend test workflow (vitest)

Docs & specs (frontends/desktop/spec/, DESIGN.md)

  • Product design spec: feature architecture, progressive disclosure, microcopy, visual language
  • Spec docs: model selection, microcopy reference, local repo connection, memory import

Testing

  • 328 frontend unit tests (vitest) — all passing
  • 114 bridge integration tests (pytest) — all passing
  • Manual testing on macOS and Windows

Notes

  • Desktop layer is fully additive — no modifications to upstream agent core (agentmain, llmcore, agent_loop)
  • Bridge communicates with agent via existing Python API; frontend connects to bridge over localhost HTTP + WS

shenhao-stu and others added 30 commits May 28, 2026 04:11
Per user screenshot 040748, the committed `! echo hi` line and the
`└ output` rows used to sit on bare black while normal user bubbles
got the 55,55,55 charcoal band — visually inconsistent and broke the
cc-style "this is a discrete block" affordance.

New `_TILE_SHELL` (65,60,65 bg, light fg) wraps both halves via
`_tile()`, so the pink `!` prompt and dim `└` glyph inherit the band
across the full row width.  Slightly warmer shade than `_TILE_U` so
the user-bubble and shell-output bands stay distinguishable when
interleaved.  Black-terminal only — matches user spec.
Previous commit's _TILE_SHELL forced a light fg across both the `└`
gutter and the command output, erasing the dim styling on the gutter
and overwriting any ANSI the shell command emitted into stdout.

Make the tile bg-only (`\x1b[48;2;65;60;65m`).  Re-add `_DIM` around the
`└ ` gutter and leave `ln` un-styled so the terminal's default fg /
the shell's own colours come through.  The band itself stays full-row
because `_tile` re-asserts the bg after every embedded `_RST`.
Last commit wrapped both the `! cmd` echo AND the `└ output` rows in
the charcoal tile.  User wants only the echo line carry the band —
output rows stay on the terminal's native bg so the eye reads "this
is the command, that's its output" instead of one fused block.
…, and GUI links

Merged pull request lsdefine#511 from shenhao-stu:main
…, ! shell magic, /resume

Merged pull request lsdefine#513 from shenhao-stu:tui-v2-v3-fixes-pending-shell
…lone

`tuiapp_v2.py:27` imports `fmt_key`, `fmt_keys` from `keysym`, but the
module file was never tracked. PR lsdefine#513 (tui v2/v3 fixes) is unusable on a
fresh checkout without this. Adds the cross-platform shortcut formatter
that turns binding strings into mac/Win-Linux key labels (e.g.
`"ctrl+b"` → `"⌃B"` / `"Ctrl+B"`), honoring `GA_KEYSYM_STYLE`.
… ImportError on fresh clone

Merged pull request lsdefine#514 from shenhao-stu:tui-v2-v3-fixes-pending-shell
…unch title

`app.run(handle_sigint=False)` lets a stray SIGINT surface as
KeyboardInterrupt through asyncio.runners — catch it in run() so the
shell sees a clean exit. Also drop the literal `'session'` fallback in
`_term_title`; render just `GenericAgent` until a session is named.
…imer)

Previous attempts used a rolling/hard-cap timer to gate the file write
to `_intervene`.  Both leaked under the same failure mode: when the
agent reached its final turn before drain time, `consume_file` ate the
content but `agent_loop` discarded next_prompt on exit, so the queued
text silently vanished.

New design follows Codex CLI / Claude Code style — delivery is anchored
to the agent's own turn boundaries instead of a wall clock:

  Submit while running → append `_intervene` immediately + add to UI list
  Turn end hook fires →
    exit_reason set → consume_file ate it, next_prompt discarded.
                      Re-route the combined text via `agent.put_task`
                      so the outer `while True: task_queue.get()` loop
                      picks it up as the next user turn.
    not exit       → content was prepended to next_prompt as `[MASTER]
                      ...` — just clear the UI mirror.
  Esc → `clear_intervene` deletes the file + clears the list.

No timers, no cooldowns.  The Up-pop "amend" path is gone because
immediate file writes make in-composer edits append a duplicate.

Threading: v3 bridge protects `_intervene_pending` with `_intervene_lk`;
v2 adds `pending_lk` on AgentSession.  v2's hook closure uses
`call_from_thread` for the UI refresh hop.

Right-click in v2 (`InputArea._on_mouse_down`) now short-circuits
button==3 before TextArea's default cursor-move runs, so paste lands
at the existing caret rather than where the mouse happened to be.

Refs: openai/codex#15842, anthropics/claude-code#44851
`subprocess.run(cmd, shell=True)` picked the OS default — cmd.exe on
Windows.  `!ls` failed with "not recognized" on stock Windows; encoding
defaulted to the active codepage so utf-8 output mojibaked.

Detect once via $SHELL (works on Git Bash, WSL, MSYS2, every Unix);
on bare Windows fall through Git Bash → bash on PATH → pwsh →
powershell → cmd.  Matches Claude Code / Codex CLI behaviour where
`!cmd` inherits the launching shell.

Display name surfaces in the agent's history line: `[!shell bash]` /
`[!shell pwsh]` so a follow-up like "rerun that with -v" can pick the
right syntax.

Refs: code.claude.com docs (bash mode); developers.openai.com/codex/cli/features (Tab/! prefix)
…eturn type, broadcast stream/read_timeout in MixinSession
…attern)

Evidence: model_responses_042511.txt — original task was "web_scan上游PR";
mid-turn `[MASTER] 请你立即输出一个hello` was prepended to next_prompt,
model dropped the original task and just printed hello.  Codex
(`maybe_send_next_queued_input`, chatwidget.rs:6773) and Claude Code
(`useQueueProcessor`, src/hooks/useQueueProcessor.ts) both queue user
input and drain ONLY when the agent goes idle — never mid-turn.

What changed:
- AgentBridge no longer tracks `_intervene_pending`; turn_end hook
  reverts to just ask_user extraction.
- Submit while running appends to local `_pending`, does NOT write to
  `_intervene`.
- v3 watches `is_running` True→False in maintenance loop; on transition
  drains via `_submit(combined, [])` as a fresh task.
- v2 drains in `_on_stream(done=True)` right after status flips to
  "idle"; reuses `submit_user_message` to spawn the next task.
- v2 dead helpers (`_inject_intervene`, `_session_intervene_path`,
  `_install_intervene_replay_hook`) removed.
- `inject_intervene` retained on v3 bridge for `!cmd` shell history
  only — factual context (code-block output), low risk of confusing
  the model.

Net: -109 lines.  Esc still cancels the queue.  Ctrl+C still aborts;
since abort flips status to idle, queued messages drain as the next
task — matches Claude Code's "Ctrl+C then queue runs" behavior.

Refs: openai/codex/codex-rs/tui/src/chatwidget.rs:6773;
anthropics/claude-code/src/hooks/useQueueProcessor.ts
Re-research revealed both mainstream TUIs DO mid-turn inject — my
previous read of "Codex/CC queue post-turn only" was wrong.

Codex (codex-rs/core/src/session/mod.rs:2986): `steer_input` → mailbox
`push_pending_input` → `session/turn.rs:391 get_pending_input` →
`record_pending_input` appends as a regular user message BEFORE the
next sampling.  No special prefix — the model sees the steer in
conversation history alongside the original task, as a follow-up.

Claude Code (utils/messages.ts:5510): drains queue after each tool
iteration and wraps the user's text:

  The user sent a new message while you were working:
  {raw}

  IMPORTANT: After completing your current task, you MUST address
  the user's message above. Do not ignore it.

The actual bug in model_responses_042511.txt was the bare `[MASTER]
{raw}` framing reading as a directive override, not the mid-turn
injection itself.

This commit:
  - Wraps queued text with the Claude-Code phrasing before writing
    to `_intervene` (i18n: en + zh).
  - Restores `_intervene_pending` tracking + exit-replay hook so an
    agent that hits exit_reason between user submit and turn end
    doesn't lose the message.
  - Adds `inject_intervene(text, track=True)` opt-in tracking so
    `!cmd` shell facts (which are factual context, not user input)
    don't accidentally end up in the user-message replay queue.
  - v2 mirrors with `_INTERVENE_WRAP_EN/_ZH` constants picked via
    `$GA_LANG`, `sess.pending_wrapped` parallel to `sess.pending`,
    and `_install_intervene_replay_hook` per-session.
  - Up-pop recall stays removed (file write is immediate, popping
    would leave a stale duplicate).

Refs: codex-rs/core/src/session/mod.rs:2986; claude-code/src/utils/messages.ts:5496
…clear)

Root cause of the lingering Ctrl+S freeze even when the session is
idle: `InputArea.reset()` calls `self.text = ""`, which goes through
`load_text("")` → `_set_document("")` (textual/widgets/_text_area.py
:1181-1186).  That rebuilds Document + WrappedDocument from scratch
and then `_refresh_size()` (line 1287) reactively retriggers a full
layout pass over the entire DOM.  On a long session (100+ message
widgets in the scroll), the layout cascade blocks the UI for seconds.

Switch to `TextArea.clear()` which goes through the edit pipeline:
`delete((0,0), document.end)` → `wrapped_document.wrap_range(...)` —
no Document rebuild, range-scoped wrap only.  Guarded with
`if self.document.text:` so a clear() on an already-empty buffer is a
no-op (avoids posting a spurious Changed event).

Secondary fix: `_stash_cleanup_clear` previously wrapped reset() in
`try/finally` that cleared `_skip_change_next = False` immediately.
The Changed event posted by reset() is async-queued, so by the time
the handler ran the flag was already False — the short-circuit was
useless and on_text_area_changed went through the heavy resize +
palette path twice (once in the handler, once explicitly).  Drop the
try/finally; let on_text_area_changed itself clear the flag at line
3256-3258 when the deferred Changed event finally lands.

Also scrubs external TUI-project names from comments per request.
- /export clip 现在通过 _copy_to_clipboard 直接写系统剪贴板:
  Win32 ctypes(CF_UNICODETEXT,避开控制台 codepage)/ macOS pbcopy /
  Wayland wl-copy / X11 xclip 或 xsel,失败再退回手动复制。
- InputArea 在 Windows 上对 Enter 事件用 GetAsyncKeyState(VK_SHIFT) 兜底,
  终端把 Shift+Enter 报成普通 Enter 时也能正确插入换行而非提交。
- watch_theme 漏清 _seg_render_cache 导致主题切换后 Markdown 的
  h1/列表 bullet/code 块/link 等仍是上一主题的色 —— 缓存 key 不含
  theme,且 ANSI 颜色被烤进了 _MdRender。补上 clear()。

Refs:
- Shift+Enter 物理检测思路 from upstream PR lsdefine#519 by @jlu005807
  lsdefine#519
- _ptk_keypress_to_bytes 在 Windows 上对 Enter 收到 \r 时用
  GetAsyncKeyState(VK_SHIFT) 物理检测 Shift,按下时返回 \n 以触发换行
  (PTK 在某些 Windows 终端区分不出 Shift+Enter 与裸 Enter)。
- 新增 _line_region / _logical_visual_range 辅助函数,↑/↓ 按键:
  不在该逻辑行的视觉首/末行 → 视觉滚行;在视觉首行且光标在行首 →
  历史导航,否则先跳逻辑行首/尾,下次按键再跨行/进历史。多行粘贴
  内部导航更符合直觉。

Refs:
- Shift+Enter 物理检测 from upstream PR lsdefine#519 by @jlu005807
  lsdefine#519
- 多行输入逻辑行边界导航思路 from upstream PR lsdefine#520 by @jlu005807
  lsdefine#520
之前在任意逻辑行的视觉首/末行都会做"先跳行头/尾再跨行/进历史"的
两段式,中间行的两段式不直观。改成:中间逻辑行的视觉首/末行直接
做视觉上下移;只有第一逻辑行的视觉首行/最末逻辑行的视觉末行,才
触发"先跳到行头/尾,下次按键再进历史"。`_logical_visual_range`
因此变成死代码,顺手删掉。
…rrow-nav/Shift+EnterFeat/tui v3 improvements

Merged via GitHub API as requested.
之前 `_extract_ask_user` 对无候选项的 ask_user 事件也照常入队,
触发 `_enter_ask` → 自由文本输入的 ask 卡片路径,在某些终端下整个
live region 冻住。v2 (`tuiapp_v2.py:2909-2910`) 是显式 `if not cands: return`,
让 "Waiting for your answer ..." 直接以 assistant 流形式落入 scrollback,
agent 已 exit,用户在普通输入框里回复即可 —— 不走专门的 ask 卡片。

对齐 v2 行为:有候选才入队、才弹卡片;无候选 fallthrough 到普通流。
abraxas914 and others added 29 commits July 10, 2026 18:17
fix: isolate Session and Conductor model routing
- Remove internal tool/project references from DESIGN.md
- Copy microcopy, local-repo-connection, and memory-import specs to docs/spec/
- Move HANDOFF-white-screen-debug.md into docs/ for tidiness

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ntend/backend separation

Documents the authoritative model selection contract: frontend reads and
requests, bridge owns truth, per-session binding with global fallback,
conductor independently configured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move specs from docs/spec/ to spec/ (the canonical location)
- Delete docs/hermes-reference/ (4 files, all Hermes-saturated)
- Delete docs/ADAPTATION-NOTES.md and docs/session-row-action-menu.md
- Update DESIGN.md appendix paths to spec/
- Retain clean debug docs (THREAD-BLEED, WHITE-SCREEN, HANDOFF) in docs/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ode)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
v0.2.0 is a stable release, not pre-release.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ompt

The /update prompt reconciles the working tree "upstream-first" file by
file. That silently drops fork-local additions to files BOTH sides edit —
most painfully .gitignore, whose local ignore rules vanish, so a fork's
private dirs resurface as untracked noise (and can get committed by
accident).

Treat files carrying a `Fork-only local overlay` marker (e.g. .gitignore)
as a MERGE: take upstream as the base, then re-append everything under the
marker verbatim, and verify the marker survived. Along the way, tighten
both the EN and ZH prompts (75 -> 58 lines) without dropping any operative
step.
…erlay-merge

fix(slash_cmds): merge additive-overlay files (e.g. .gitignore) on /update
…OP); ga.py: file_patch/file_write 保持原文件换行符, 修复LF文件被污染成CRLF; _arg 类型强转修复字符串false等
Add maxlen_multiplier derived from context_win (capped 3x); drives per-tool
truncation caps and history compression interval. WebScan uses half-increment
growth to avoid saturating its 35000 base. Docs updated for the new coupling.
Hard-delete path can preserve the first K history messages, insert a "..." gap, and strip dangling tool_use at the cut. Default 0 keeps previous behavior.
Ignore assets/glm52_instruction.txt and assets/gpt55_instruction.txt
so local customizations stay out of the repo.
…#681)

The agent's default cwd is the temp/ subdirectory, where git resolves
'-- <file>' pathspecs relative to cwd. Fed the root-relative paths that
'git diff --name-only' prints, 'git checkout upstream/main -- <file>'
fails with "pathspec did not match", breaking the working-tree
reconcile steps of /update. The prompt now tells the agent to run every
git command from the repo root (_ROOT), in both EN and ZH.

Co-authored-by: Shen Hao <shenhao-stu@users.noreply.github.com>
* feat(desktop): add isolated e2e control interfaces

* test(desktop): add WebdriverIO automation harness

* ci(desktop): gate deterministic desktop journeys

* ci(desktop): harden release gates

* fix(ci): install Tauri libraries for Rust contracts

* fix(e2e): wait past stale Windows retry state

* fix(packaging): retry transient DMG creation

* fix(release): emit portable Windows checksums
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.