Skip to content

fix(wework): reduce abnormal CPU usage from composer input - #2407

Closed
sdadunderscoresdad wants to merge 22 commits into
wecode-ai:mainfrom
sdadunderscoresdad:fix/wework-abnormal-cpu-usage
Closed

sdadunderscoresdad wants to merge 22 commits into
wecode-ai:mainfrom
sdadunderscoresdad:fix/wework-abnormal-cpu-usage

Conversation

@sdadunderscoresdad

@sdadunderscoresdad sdadunderscoresdad commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

What changed

  • Reduce abnormal CPU usage and input lag in the desktop composer by keeping typing off the React render path:
    • ChatInput, ProjectChatComposer, and ComposerTextarea expose ref-based handles via forwardRef + useImperativeHandle, so handlers read the latest value without a re-render on every keystroke.
    • Defer parent onChange propagation while keeping a local draft so composer state stays responsive, and keep ref-based typing performance in BufferedChatInput with only the needed state sync.
  • Visual / command-label polish (secondary):
    • Semantic shell command labels in running-tool blocks, unwrapping quoted pwsh -Command wrappers, cd && prefixes, and leading $ prompts.
    • Cross-platform native git runner in the executor that invokes git directly via argv on Windows, avoiding cmd /C hangs; Windows git_* device commands run off the tokio worker threads.

Why

Typing in the composer triggered per-keystroke React re-renders and state churn that caused high CPU usage and input lag, especially in long conversations. Running-tool blocks also showed raw PowerShell wrapper commands instead of readable labels, and Windows git device commands could hang under cmd /C.

Impact

  • Composer input stays responsive and CPU usage drops while typing.
  • Running-tool blocks show readable semantic command labels on Windows.
  • Windows git_* device commands run through a native git runner without cmd /C hangs.

Validation

  • pnpm --filter wework typecheck
  • pnpm --filter wework test — focused chat/composer suites (ChatInput, ComposerToolbar, ToolBlockItem, ToolBlocksDisplay, BufferedChatInput)
  • Prettier and ESLint on changed files
  • Pre-push Wework checks passed (formatter, translation checker, typography / task-lifecycle lint)

Summary by CodeRabbit

  • New Features

    • Expanded Git support for branch and remote queries, checkout, staging, commits, pushes, and multiple diff views.
    • Added clearer activity labels for Git status, file viewing, and renamed files.
    • Improved recognition of PowerShell and shell-wrapped commands on Windows and macOS.
  • Bug Fixes

    • Improved chat draft persistence, submission handling, and text synchronization.
    • Fixed placeholder, blur, composition, and resizing behavior in chat inputs.
  • Performance

    • Reduced unnecessary updates while resizing inputs and refreshing workbench state.

Video and others added 18 commits July 31, 2026 11:51
- Unwrap Windows PowerShell wrapper commands in toolBlockActivity so that
  collapsed tool labels show search/read summaries instead of the raw pwsh path.
- Add semantic command labels for file reads and git status in ToolBlockItem.
- Introduce executor/src/local/git.rs with a cross-platform GitCommandRunner
  that invokes git directly via argv on Windows, avoiding cmd /C hangs.
- Route all git_* device commands through native git calls on Windows and
  extend PATH with common Git installation directories.
- Add i18n keys and unit tests for both frontend label summarization and
  executor git detection.
- Remove dead run_git_async and fix git_stdout to run git only once.
- Scope Windows-only PathBuf import and git path helpers with cfg(windows).
- Keep DEFAULT_DEVICE_ID cross-platform and use NUL on Windows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Introduce getInnerShellCommand to unify shell wrapper unwrapping.
- Share regex constants and improve Get-ChildItem classification.
- Update stale test expectations and clean up blank lines.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Avoid syncing draft input to React state on every keystroke.
- Sync draft to parent on blur, compositionend, and submit instead.
- Defer autocomplete trigger detection and debug snapshot updates.
- Use ComposerTextarea ref for submit value so stale value prop does not matter.
- Fix placeholder visibility by tracking editor internal content state.
- Wrap non-urgent state updates in startTransition.
The deferred sync introduced in the composer perf commit caused the
controlled value prop to drift from ProseMirror's internal state. This
broke insertion appending, scope-switch composer reset, and submit
clearing. Restore per-keystroke state and onChange sync while keeping
the ComposerTextarea-level optimizations intact.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Keep BufferedChatInput's local draftState synchronous so the controlled
value prop stays in sync with ProseMirror. Defer the parent onChange via
a 300ms debounce to avoid re-rendering the Workbench tree on every
keystroke. Flush the draft immediately on blur, compositionend, submit,
and scope change.

Update the one test that expected per-keystroke onChange to trigger blur
and wait for the flush.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eact render path

Expose getValue/setValue through forwardRef on ChatInput, CompactChatComposer,
and ProjectChatComposer so BufferedChatInput can update the editor directly
without forcing parent re-renders on every keystroke. Keeps the existing tests
passing by syncing external value changes and flushing pending drafts on blur.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- BufferedChatInput: keep local draft state in sync while still debouncing
  parent onChange; flush pending draft only on scope change/unmount.
- CompactChatComposer: use the composer handle value for interrupt-and-send.
- toolBlockActivity: loop stripShellPrefix to handle quoted paths and chains.
- ToolBlockItem: unwrap the full shell command instead of only the first line.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add async wrappers around the synchronous git runner and await them in
the Windows builtin device command handler. This prevents git subprocess
I/O from blocking the tokio runtime threads.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… typing performance

The synchronous setDraftState update in the previous commit reintroduced
per-keystroke React re-renders and undid the ref-based composer handle
optimization, causing input lag. Restore the version that keeps typing off
the render path while keeping the other composer/label fixes intact.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-cpu-usage

# Conflicts:
#	wework/src/components/chat/ChatInput.tsx
#	wework/src/components/chat/composer/ProjectChatComposer.tsx
@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds asynchronous Git handling for Windows, ref-based chat composer synchronization, PowerShell and Git command classification, debounced draft and workbench snapshot updates, localized activity labels, and Tauri configuration reformatting.

Changes

Local Git Execution

Layer / File(s) Summary
Git helper APIs and execution
executor/src/local/git.rs, executor/src/local/mod.rs
Adds sanitized synchronous and asynchronous Git execution, worktree and merge-base helpers, Windows PATH resolution, result conversion, and tests.
Windows Git built-ins
executor/src/local/app_ipc.rs
Routes Windows branch, status, remote, checkout, staging, commit, push, and diff handlers through asynchronous Git helpers. Detached-head push errors remain explicit.

Chat Composer State

Layer / File(s) Summary
Composer editor contract and lifecycle
composerTextareaTypes.ts, ComposerProseMirrorEditor.tsx, ComposerTextarea.tsx, useAutoResizeTextarea.ts
Adds lifecycle callbacks, content tracking, imperative editor access, and cancellable scheduled editor updates.
Composer ref forwarding and submission
ChatInput.tsx, ProjectChatComposer.tsx, CompactChatComposer.tsx
Forwards composer refs, exposes getValue and setValue, synchronizes local text state, and submits live textarea values.
Buffered draft synchronization
BufferedChatInput.tsx, BufferedChatInput.test.tsx
Debounces draft persistence for 300 ms and flushes drafts on blur, composition end, external changes, unmount, and submission.

Tool Activity Classification

Layer / File(s) Summary
Shell unwrapping and command classification
toolBlockActivity.ts, toolBlockActivity.test.ts
Adds shell-prefix removal, Bash, Zsh, PowerShell, and Windows wrapper handling, plus PowerShell search and file command parsing.
Tool command labels and validation
ToolBlockItem.tsx, ToolBlockItem.test.tsx, ToolBlocksDisplay.test.tsx, toolBlockActivity.test.ts, en/chat.json, zh-CN/chat.json
Adds Git status and file-view labels, localized strings, and coverage for normalized command summaries.

Workbench and Configuration

Layer / File(s) Summary
Coalesced debug snapshot updates
useWorkbenchPaneSession.ts, WorkbenchProvider.tsx
Defers debug snapshot updates through coalesced 100 ms timers with cleanup.
Compact configuration arrays
wework/src-tauri/tauri.conf.json
Reformats three JSON arrays without changing their values.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WindowsBuiltins
  participant GitHelpers
  participant GitProcess
  WindowsBuiltins->>GitHelpers: run_git_async(arguments, environment)
  GitHelpers->>GitProcess: execute Git with sanitized environment
  GitProcess-->>GitHelpers: stdout, stderr, exit status, duration
  GitHelpers-->>WindowsBuiltins: CommandResult
Loading
sequenceDiagram
  participant ChatInput
  participant Composer
  participant ComposerTextarea
  participant ProseMirrorEditor
  ChatInput->>Composer: forward value and lifecycle callbacks
  Composer->>ComposerTextarea: forward ref and callbacks
  ComposerTextarea->>ProseMirrorEditor: read or set editor value
  ProseMirrorEditor-->>ComposerTextarea: content snapshot and blur event
  ComposerTextarea-->>ChatInput: imperative value handle
Loading

Possibly related PRs

Suggested reviewers: qdaxb

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: reducing abnormal CPU usage from composer input.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (8)
executor/src/local/git.rs (3)

132-138: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

is_worktree returns true for locations that are not work trees.

The || fallback accepts any path where rev-parse --git-dir succeeds. That includes a bare repository and a plain directory nested under an unrelated repository, because Git walks up to the parent. The first check already answers the question, so the fallback only widens the result.

If the fallback exists to cover an older Git version, document that reason. Otherwise rely on --is-inside-work-tree alone.

♻️ Proposed simplification
 pub fn is_worktree(path: &str, extra_env: &HashMap<String, String>) -> bool {
     git_stdout(path, &["rev-parse", "--is-inside-work-tree"], extra_env)
         .map(|output| output == "true")
         .unwrap_or(false)
-        || git_stdout(path, &["rev-parse", "--git-dir"], extra_env).is_some()
 }

Apply the same change to is_worktree_async at lines 216-224.

As per coding guidelines: "Delete dead code and do not add compatibility shims or fallback paths without agreement; correct the primary path."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/src/local/git.rs` around lines 132 - 138, Remove the
`git_stdout(..., &["rev-parse", "--git-dir"], ...)` fallback from both
`is_worktree` and `is_worktree_async`, relying solely on `rev-parse
--is-inside-work-tree` so bare repositories and directories nested under
unrelated repositories return false.

Source: Coding guidelines


319-335: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename run_git_or_panic and make setup failures visible.

run_git_or_panic never panics. It returns stdout and discards the exit code and stderr. If git init fails, detects_worktree fails later with no diagnostic output.

Assert success in the helper so the failure names the failing Git command.

💚 Proposed test helper fix
-    fn run_git_or_panic(cwd: &str, args: &[&str]) -> String {
-        run_git(cwd, args, &HashMap::new()).stdout
-    }
+    fn run_git_or_panic(cwd: &str, args: &[&str]) -> String {
+        let output = run_git(cwd, args, &HashMap::new());
+        assert!(
+            output.success(),
+            "git {args:?} failed: exit={:?} stderr={}",
+            output.exit_code,
+            output.stderr
+        );
+        output.stdout
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/src/local/git.rs` around lines 319 - 335, Update the test helper
run_git_or_panic to assert that the Git command succeeds, including the
command’s stderr or equivalent diagnostic in the assertion, and rename it to
reflect that it validates success rather than panicking. Keep detects_worktree
using this helper for git init so setup failures identify the failing command.

119-173: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused synchronous Git helpers

git_stdout, is_worktree, resolve_merge_base, and git_rev_parse_verify are only used within executor/src/local/git.rs tests or by the async wrapper paths. Keep the async Git helpers plus run_git, and remove the synchronous helpers so callers do not maintain duplicated logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/src/local/git.rs` around lines 119 - 173, Remove the synchronous
helper functions git_stdout, is_worktree, resolve_merge_base, and
git_rev_parse_verify from the Git module, along with any tests or wrapper paths
that depend exclusively on them. Preserve run_git and the async Git helpers, and
update remaining callers to use the async implementations rather than duplicated
synchronous logic.

Source: Coding guidelines

executor/src/local/app_ipc.rs (2)

1594-1601: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add the -- separator consistently to the merge-base diff arguments.

In git_branch_diff_shortstat the None branch uses ["diff", "--shortstat", "HEAD", "--"], but the Some(base) branch omits --. git_branch_diff at lines 1619-1622 repeats the same asymmetry. Without --, Git can treat the revision argument ambiguously when a path of the same name exists.

Append "--" in both Some(base) branches so all four argument lists follow one form.

♻️ Proposed change
             let args: Vec<&str> = match base.as_deref() {
-                Some(base) => vec!["diff", "--shortstat", base],
+                Some(base) => vec!["diff", "--shortstat", base, "--"],
                 None => vec!["diff", "--shortstat", "HEAD", "--"],
             };

Also applies to: 1619-1622

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/src/local/app_ipc.rs` around lines 1594 - 1601, Update both
`git_branch_diff_shortstat` and `git_branch_diff` so their `Some(base)` argument
lists append the `"--"` separator after the merge-base revision, matching the
existing `None` branches and ensuring all diff invocations use the same
revision/path boundary.

1631-1648: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the untracked-file diff loop into one helper.

Lines 1631-1648 in git_branch_diff and lines 1670-1687 in git_diff are identical: list untracked files, then run one git diff --no-index per file and append successful output. The rev-parse --verify HEAD probe plus tracked-diff selection is also near-identical.

Extract a single async helper, for example append_untracked_diffs(path, env, &mut output), and call it from both arms. That keeps one code path when the diff format changes.

Note one scaling property while you are there: the loop spawns one Git process per untracked file. A repository with many unignored files produces many sequential process spawns on a single IPC request. Consider capping the number of files or the total output size.

As per coding guidelines: "Before adding code, search for and reuse existing components, services, utilities, and patterns; extract shared logic instead of duplicating it."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/src/local/app_ipc.rs` around lines 1631 - 1648, Extract the
duplicated untracked-file processing from git_branch_diff and git_diff into one
async append_untracked_diffs helper that lists untracked files, runs the
no-index diff, and appends successful output. Replace both inline loops with
calls to this helper, preserving existing behavior, and add a reasonable cap on
processed files or accumulated output to prevent excessive sequential Git
processes per IPC request.

Source: Coding guidelines

wework/src/components/layout/BufferedChatInput.test.tsx (1)

69-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the debounce window and for composition end.

This test now proves that blur flushes the draft. Two new behaviors remain untested: onChange must not fire during the 300 ms window, and onCompositionEnd must flush. The debounce is the central behavior of this PR, so a regression that removes the delay or the composition flush would pass the current suite.

Add one test with fake timers that types, asserts onChange was not called, advances 300 ms, and then asserts the call. Add a second test that fires compositionEnd instead of blur.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/layout/BufferedChatInput.test.tsx` around lines 69 -
75, Add coverage in the BufferedChatInput tests for both debounce timing and
composition flushing: use fake timers to type a draft, verify onChange has not
fired during the 300 ms window, advance 300 ms, then verify the change; add a
separate test that triggers compositionEnd instead of blur and verifies onChange
receives the draft.
wework/src/components/layout/BufferedChatInput.tsx (1)

76-85: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

This cleanup runs on every value change, so it emits a duplicate onChange after each flush.

The effect lists value in its dependencies, so React runs the cleanup on every value change and not only on unmount. Trace one flush: the timer calls onChange(d); the parent re-renders with value === d; the cleanup for the previous value then compares that older value against draftRef.current, which already holds d, so it calls onChange(d) a second time.

The extra call is idempotent, so no draft is lost. It does add one redundant parent update per flush, which works against the CPU goal of this PR. Read value through a ref and keep the effect dependencies empty so the flush runs only on unmount.

♻️ Proposed refactor
+  const valueRef = useRef(value)
+  valueRef.current = value
+
   // Flush pending draft when value changes or on unmount.
   useEffect(() => {
     return () => {
       const pendingDraft = draftRef.current
-      if (pendingDraft !== value) {
+      if (pendingDraft !== valueRef.current) {
         cancelPendingFlush()
-        onChange(pendingDraft)
+        onChangeRef.current(pendingDraft)
       }
     }
-  }, [cancelPendingFlush, onChange, value])
+  }, [cancelPendingFlush])

This needs an onChangeRef that mirrors onChange, so the unmount path always calls the latest callback. Also update the comment, because the effect then flushes on unmount only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/layout/BufferedChatInput.tsx` around lines 76 - 85,
Update the cleanup effect in BufferedChatInput to run only on unmount by
removing value-dependent reruns and using a ref that mirrors the latest value
for comparison; also add an onChangeRef mirroring the latest callback so unmount
uses current props. Update the comment to state that pending drafts flush on
unmount only, while preserving cancelPendingFlush and avoiding duplicate
onChange calls after timer flushes.
wework/src/components/chat/ChatInput.tsx (1)

297-305: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Three forwarded handles depend on [value] only to support a pre-mount fallback. Each useImperativeHandle reads composerRef.current first and falls back to value. The child composer is always mounted after the first commit, so the fallback only applies before mount, yet the [value] dependency rebuilds the handle object on every value change. ComposerTextarea at lines 127-137 already shows the stable pattern: keep the latest value in a ref and use an empty dependency array.

  • wework/src/components/chat/ChatInput.tsx#L297-L305: store value in a valueRef, read it in getValue, and change the dependency array to [].
  • wework/src/components/chat/composer/ProjectChatComposer.tsx#L174-L182: apply the same valueRef fallback and empty dependency array.
  • wework/src/components/chat/composer/CompactChatComposer.tsx#L156-L164: apply the same valueRef fallback and empty dependency array.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/chat/ChatInput.tsx` around lines 297 - 305, Stabilize
all three forwarded imperative handles by storing the latest value in a valueRef
and reading that ref only for the pre-mount fallback, then use an empty
dependency array. Apply this in ChatInput.tsx lines 297-305,
ProjectChatComposer.tsx lines 174-182, and CompactChatComposer.tsx lines
156-164; update each getValue implementation while preserving the
composerRef.current-first behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@executor/src/local/app_ipc.rs`:
- Around line 1543-1563: Update the git_push branch-discovery flow around
run_git_async so it checks the branch command’s success status before treating
empty stdout as detached HEAD. For command failures, return the actual Git error
through the existing command-result path; only emit “Cannot push detached HEAD”
when the command succeeds with an empty branch name.

In `@executor/src/local/git.rs`:
- Around line 29-32: Harden run_git so Git commands cannot block indefinitely:
spawn the process, poll for completion with an explicit deadline, and on expiry
kill it and return a timed-out GitOutput. Configure the command with
stdin(Stdio::null()) and GIT_TERMINAL_PROMPT=0 to prevent interactive credential
prompts, and remove or update the stale timeout documentation above
WINDOWS_GIT_PATHS to match the implemented behavior.
- Around line 91-97: The prepend_windows_git_paths function reads from the
executor process PATH via std::env::var, which overwrites the PATH set by
build_env(extra_env). Refactor prepend_windows_git_paths to accept the command
as a parameter and read the current PATH value from the command's environment
(which was set by build_env), then append the Git directories and set the
combined value back on the command, avoiding any read from the executor's own
environment variables.

In `@wework/src/components/chat/blocks/toolBlockActivity.ts`:
- Around line 727-729: Update SHELL_EXECUTABLE_RE in
wework/src/components/chat/blocks/toolBlockActivity.ts:727-729 to accept both
forward- and backslash-separated shell paths while preserving existing
executable matching. In
wework/src/components/chat/blocks/toolBlockActivity.test.ts:220-232, replace the
duplicate PowerShell assertions with coverage for /bin/zsh -c or /bin/bash -c
wrappers.

In `@wework/src/components/chat/blocks/ToolBlockItem.tsx`:
- Around line 1011-1017: Update the Git command parsing near the subcommand
detection to tokenize with splitShellWords instead of trim().split(/\s+/), then
skip supported Git global options and their associated values before assigning
subcommand. Preserve the dedicated genericLabels.gitStatus result for status
commands and compact formatting for other commands, including paths containing
spaces.

In `@wework/src/components/chat/composer/CompactChatComposer.tsx`:
- Around line 533-536: Use separate refs for the compact and fullscreen
ComposerTextarea instances instead of sharing composerRef. Add a
fullscreen-specific composer handle ref for the instance near
fullscreenInputOpen, then select that ref when fullscreenInputOpen is true and
composerRef otherwise so parent methods always target the active editor,
including submit and setValue operations.

In `@wework/src/components/chat/composer/ProjectChatComposer.tsx`:
- Around line 227-252: The insertion handlers must use the live composer content
instead of the debounced value prop. In
wework/src/components/chat/composer/ProjectChatComposer.tsx lines 227-252,
update handleShowTextAttachment and handleQuickPhraseSelect to read
composerRef.current?.getValue() ?? value before appending, and use that value
for applyWorkspacePathTransfer in handleDrop at line 224. In
wework/src/components/chat/composer/CompactChatComposer.tsx lines 192-217, apply
the same live-value lookup in the attachment and quick-phrase insertion
handlers.
- Around line 306-318: Update the ProjectChatComposer onSubmit handler to
compute the exact value from composerRef.current?.getValue() ?? value before
calling debugComposerEvent, then use that same submitted value for
submittedValue and the subsequent onSubmit call. Keep the existing canSend guard
and other diagnostics unchanged.

---

Nitpick comments:
In `@executor/src/local/app_ipc.rs`:
- Around line 1594-1601: Update both `git_branch_diff_shortstat` and
`git_branch_diff` so their `Some(base)` argument lists append the `"--"`
separator after the merge-base revision, matching the existing `None` branches
and ensuring all diff invocations use the same revision/path boundary.
- Around line 1631-1648: Extract the duplicated untracked-file processing from
git_branch_diff and git_diff into one async append_untracked_diffs helper that
lists untracked files, runs the no-index diff, and appends successful output.
Replace both inline loops with calls to this helper, preserving existing
behavior, and add a reasonable cap on processed files or accumulated output to
prevent excessive sequential Git processes per IPC request.

In `@executor/src/local/git.rs`:
- Around line 132-138: Remove the `git_stdout(..., &["rev-parse", "--git-dir"],
...)` fallback from both `is_worktree` and `is_worktree_async`, relying solely
on `rev-parse --is-inside-work-tree` so bare repositories and directories nested
under unrelated repositories return false.
- Around line 319-335: Update the test helper run_git_or_panic to assert that
the Git command succeeds, including the command’s stderr or equivalent
diagnostic in the assertion, and rename it to reflect that it validates success
rather than panicking. Keep detects_worktree using this helper for git init so
setup failures identify the failing command.
- Around line 119-173: Remove the synchronous helper functions git_stdout,
is_worktree, resolve_merge_base, and git_rev_parse_verify from the Git module,
along with any tests or wrapper paths that depend exclusively on them. Preserve
run_git and the async Git helpers, and update remaining callers to use the async
implementations rather than duplicated synchronous logic.

In `@wework/src/components/chat/ChatInput.tsx`:
- Around line 297-305: Stabilize all three forwarded imperative handles by
storing the latest value in a valueRef and reading that ref only for the
pre-mount fallback, then use an empty dependency array. Apply this in
ChatInput.tsx lines 297-305, ProjectChatComposer.tsx lines 174-182, and
CompactChatComposer.tsx lines 156-164; update each getValue implementation while
preserving the composerRef.current-first behavior.

In `@wework/src/components/layout/BufferedChatInput.test.tsx`:
- Around line 69-75: Add coverage in the BufferedChatInput tests for both
debounce timing and composition flushing: use fake timers to type a draft,
verify onChange has not fired during the 300 ms window, advance 300 ms, then
verify the change; add a separate test that triggers compositionEnd instead of
blur and verifies onChange receives the draft.

In `@wework/src/components/layout/BufferedChatInput.tsx`:
- Around line 76-85: Update the cleanup effect in BufferedChatInput to run only
on unmount by removing value-dependent reruns and using a ref that mirrors the
latest value for comparison; also add an onChangeRef mirroring the latest
callback so unmount uses current props. Update the comment to state that pending
drafts flush on unmount only, while preserving cancelPendingFlush and avoiding
duplicate onChange calls after timer flushes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c01ed3a-85a9-493a-ba12-09047d8131ce

📥 Commits

Reviewing files that changed from the base of the PR and between 76758c6 and f2dd301.

📒 Files selected for processing (22)
  • executor/src/local/app_ipc.rs
  • executor/src/local/git.rs
  • executor/src/local/mod.rs
  • wework/src-tauri/tauri.conf.json
  • wework/src/components/chat/ChatInput.tsx
  • wework/src/components/chat/blocks/ToolBlockItem.test.tsx
  • wework/src/components/chat/blocks/ToolBlockItem.tsx
  • wework/src/components/chat/blocks/ToolBlocksDisplay.test.tsx
  • wework/src/components/chat/blocks/toolBlockActivity.test.ts
  • wework/src/components/chat/blocks/toolBlockActivity.ts
  • wework/src/components/chat/composer/CompactChatComposer.tsx
  • wework/src/components/chat/composer/ComposerProseMirrorEditor.tsx
  • wework/src/components/chat/composer/ComposerTextarea.tsx
  • wework/src/components/chat/composer/ProjectChatComposer.tsx
  • wework/src/components/chat/composer/composerTextareaTypes.ts
  • wework/src/components/chat/composer/useAutoResizeTextarea.ts
  • wework/src/components/layout/BufferedChatInput.test.tsx
  • wework/src/components/layout/BufferedChatInput.tsx
  • wework/src/components/layout/useWorkbenchPaneSession.ts
  • wework/src/features/workbench/WorkbenchProvider.tsx
  • wework/src/i18n/locales/en/chat.json
  • wework/src/i18n/locales/zh-CN/chat.json

Comment thread executor/src/local/app_ipc.rs Resolved
Comment thread executor/src/local/git.rs Outdated
Comment on lines +29 to +32
/// Maximum time we are willing to wait for a single Git device command.
/// Common installation directories for Git on Windows. These are appended to the process PATH
/// so that `git` can be resolved even when the executor is started from an environment that
/// does not include them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Remove the stale timeout doc line, or add the timeout it describes.

Line 29 documents a "Maximum time we are willing to wait for a single Git device command", but no timeout constant exists and run_git calls command.output() without any deadline. The doc comment also attaches to WINDOWS_GIT_PATHS, which is unrelated.

This is more than a comment defect. The module header states the previous failure mode was hanging Git calls and "command timed out" errors. run_git can now block forever, for example when a credential helper waits for input. Through run_git_async each hang consumes a Tokio blocking-pool thread permanently, so repeated Git device commands can exhaust the pool.

Add an explicit wait bound, for example spawn with Command::spawn, poll with a deadline, then kill and report a timed-out GitOutput. Also set stdin(Stdio::null()) and GIT_TERMINAL_PROMPT=0 so Git fails fast instead of waiting for input.

🔧 Minimum hardening for the spawn path
     let mut command = Command::new("git");
     command
         .arg("-C")
         .arg(cwd)
         .args(args)
         .env_clear()
         .envs(build_env(extra_env))
+        .env("GIT_TERMINAL_PROMPT", "0")
+        .stdin(Stdio::null())
         .stdout(Stdio::piped())
         .stderr(Stdio::piped());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/src/local/git.rs` around lines 29 - 32, Harden run_git so Git
commands cannot block indefinitely: spawn the process, poll for completion with
an explicit deadline, and on expiry kill it and return a timed-out GitOutput.
Configure the command with stdin(Stdio::null()) and GIT_TERMINAL_PROMPT=0 to
prevent interactive credential prompts, and remove or update the stale timeout
documentation above WINDOWS_GIT_PATHS to match the implemented behavior.

Comment thread executor/src/local/git.rs
Comment on lines +91 to +97
.env_clear()
.envs(build_env(extra_env))
.stdout(Stdio::piped())
.stderr(Stdio::piped());

#[cfg(windows)]
prepend_windows_git_paths(&mut command);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect build_env to see how it handles PATH.
fd -t f 'command.rs' executor/src | while IFS= read -r f; do
  ast-grep outline "$f" --match 'build_env'
done
rg -nP -C 15 'fn build_env' executor/src
rg -nP -C 3 '"PATH"|PATH' executor/src/local/command.rs

Repository: wecode-ai/Wegent

Length of output: 2198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate process_env implementation"
rg -n -C 15 'process_environment::process_env|fn process_env|process_env\(' executor/src

echo
echo "Search path handling across executor/src"
rg -n -C 5 'PATH|path|Paths|prepend_windows_git_paths|build_env' executor/src -g '*.rs'

echo
echo "Inspect executor/src/local/git.rs around run_git"
sed -n '1,150p' executor/src/local/git.rs

echo
echo "Inspect executor/src/local/command.rs around process_command/build_env"
sed -n '200,260p' executor/src/local/command.rs

Repository: wecode-ai/Wegent

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files containing process_environment"
rg -n --hidden 'process_environment' Cargo.toml Cargo.lock executor/Cargo.toml 2>/dev/null || true
rg -n 'process_environment::process_env|use std::process::environment|process_environment' Cargo.lock 2>/dev/null | head -50

echo
echo "Process env implementation if present in lock"
rg -n --hidden 'name = "process_environment"|version =.' Cargo.lock | head -100

echo
echo "Locate git.rs"
fd -t f 'git.rs' executor/src
echo
if [ -f executor/src/local/git.rs ]; then
  wc -l executor/src/local/git.rs
  sed -n '1,140p' executor/src/local/git.rs
fi

echo
echo "Local command.rs env/build_env context"
if [ -f executor/src/local/command.rs ]; then
  sed -n '1,80p;220,245p;300,380p' executor/src/local/command.rs
fi

echo
echo "Exact prepend_windows_git_paths references"
rg -n -C 8 'prepend_windows_git_paths|run_git|env_clear|build_env' executor/src -g '*.rs' | head -200

Repository: wecode-ai/Wegent

Length of output: 191


🌐 Web query:

Rust std::process::Command::envs HashMap iter order envs before env PATH overwrites

💡 Result:

In Rust's std::process::Command, environment variables are managed internally using a BTreeMap, not a HashMap [1][2][3]. Because it uses a BTreeMap, the order of environment variables is deterministic (sorted by key), rather than insertion-ordered [1][2][3]. Regarding overwriting behavior, explicit changes made via.env or.envs always take precedence over inherited environment variables [4][5][6]. When you use.envs, you are providing a collection of key-value pairs that are inserted into or updated within the internal BTreeMap [4][5][3]. If multiple keys exist in your input to.envs, or if you call.env multiple times for the same key, the last value set for that specific key will be the one used by the spawned process [4][5][6]. Specifically concerning the PATH environment variable: 1. Overwriting: If you explicitly set PATH using.env("PATH",...) or via.envs, this value will completely override the parent process's PATH in the child environment [4][5][6]. 2. Resolution: The standard library tracks whether the PATH environment variable has been modified (via a saw_path flag in the internal CommandEnv struct) [1][2][3]. This is relevant because, on some platforms (notably Windows), the standard library uses this flag to decide whether to search the system-defined paths or rely solely on the provided PATH variable when spawning a child process [7][3]. If you need to preserve the parent's PATH while adding to it, you must manually fetch the current PATH, append your new entries, and set the full string explicitly: use std::env; use std::process::Command; let current_path = env::var_os("PATH").unwrap_or_default; let mut new_path = current_path; // Append your new paths here... Command::new("your_command").env("PATH", new_path).spawn?;

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf 'working dir\n'
pwd
printf 'tracked git.rs candidates\n'
git ls-files | rg '(^|/)git\.rs$|local/command\.rs$|Cargo\.lock$' || true
printf 'process_environment exact candidates\n'
rg -n --hidden -C 8 'process_environment|process_env\(' . -g '*.rs' -g '*.toml' | head -200 || true
printf 'git.rs snippets\n'
for f in $(git ls-files | rg 'git\.rs$'); do
  echo "--- $f"
  rg -n -C 8 'prepend_windows_git_paths|run_git|env_clear|build_env' "$f" || true
done
printf 'local command build_env snippets\n'
for f in $(git ls-files | rg 'command\.rs$'); do
  echo "--- $f"
  rg -n -C 15 'fn build_env|build_env\(|process_env' "$f" || true
done

Repository: wecode-ai/Wegent

Length of output: 25048


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf 'wework process_environment files\n'
git ls-files wework/src-tauri/src | rg 'process_environment|local_executor|lib\.rs' || true
printf 'process_environment.rs\n'
if [ -f wework/src-tauri/src/process_environment.rs ]; then
  wc -l wework/src-tauri/src/process_environment.rs
  rg -n -C 8 'pub.*process_env|pub.*normalized_current_path|pub.*sanitized_current_environment|normalized_current_path|sanitized_current_environment|PATH|path' wework/src-tauri/src/process_environment.rs
  sed -n '220,380p' wework/src-tauri/src/process_environment.rs
fi

Repository: wecode-ai/Wegent

Length of output: 4535


Use the command PATH for Windows Git path prepending.

prepend_windows_git_paths reads std::env::var("PATH") and calls command.env("PATH", joined), overwriting PATH set by build_env(extra_env). Get the PATH from build_env(extra_env) directly, append the Git directories, and set that value instead of reading the executor process PATH.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/src/local/git.rs` around lines 91 - 97, The
prepend_windows_git_paths function reads from the executor process PATH via
std::env::var, which overwrites the PATH set by build_env(extra_env). Refactor
prepend_windows_git_paths to accept the command as a parameter and read the
current PATH value from the command's environment (which was set by build_env),
then append the Git directories and set the combined value back on the command,
avoiding any read from the executor's own environment variables.

jiaqi62 added 2 commits August 4, 2026 18:01
Address CodeRabbit review feedback on PR 2407:
- BufferedChatInput: flush pending drafts on scope switch or unmount
  only, using a committed-value baseline to avoid a duplicate onChange
  after every debounced flush.
- CompactChatComposer: route parent value reads through the active
  (compact or fullscreen) composer handle instead of a shared ref.
- ProjectChatComposer/CompactChatComposer: read the live editor value
  in attachment, quick-phrase, and drop insertion handlers instead of
  the debounced value prop, and log the actual submitted value.
- ToolBlockItem: tokenize git commands with splitShellWords and skip git
  global options so `git -C <path> status` keeps its status label.
- toolBlockActivity: recognize absolute POSIX shell paths (/bin/bash -c)
  when unwrapping shell wrappers; add debounce and composition-end
  BufferedChatInput tests.
Address CodeRabbit review feedback on PR 2407:
- git_push surfaces the real git error when `branch --show-current` fails
  instead of misreporting a detached HEAD.
- run_git sets GIT_TERMINAL_PROMPT=0 so git fails fast instead of waiting
  for interactive input, and drops the stale timeout doc line.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wework/src/components/layout/BufferedChatInput.test.tsx`:
- Around line 91-102: Update the “flushes the draft on composition end” test to
control or mock the debounce timer, keep it pending past its deadline, dispatch
compositionEnd, and assert onChange receives “draft” before the debounce timer
fires, proving the composition-end handler performs the flush.
- Around line 78-89: Update the test “debounces onChange during the 300ms
window” to use controlled/fake timers, assert onChange has not been called
before 300 ms, then advance the timer through the 300 ms boundary and assert it
receives “draft”. Remove the waitFor-based eventual-only verification while
preserving the existing typing setup.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 15345ce1-2f5d-4389-b17f-62ed9b2ed54a

📥 Commits

Reviewing files that changed from the base of the PR and between f2dd301 and a4a00d8.

📒 Files selected for processing (9)
  • executor/src/local/app_ipc.rs
  • executor/src/local/git.rs
  • wework/src/components/chat/blocks/ToolBlockItem.tsx
  • wework/src/components/chat/blocks/toolBlockActivity.test.ts
  • wework/src/components/chat/blocks/toolBlockActivity.ts
  • wework/src/components/chat/composer/CompactChatComposer.tsx
  • wework/src/components/chat/composer/ProjectChatComposer.tsx
  • wework/src/components/layout/BufferedChatInput.test.tsx
  • wework/src/components/layout/BufferedChatInput.tsx
🚧 Files skipped from review as they are similar to previous changes (8)
  • wework/src/components/chat/composer/ProjectChatComposer.tsx
  • wework/src/components/chat/blocks/toolBlockActivity.ts
  • executor/src/local/app_ipc.rs
  • wework/src/components/chat/blocks/ToolBlockItem.tsx
  • wework/src/components/chat/blocks/toolBlockActivity.test.ts
  • wework/src/components/chat/composer/CompactChatComposer.tsx
  • wework/src/components/layout/BufferedChatInput.tsx
  • executor/src/local/git.rs

Comment on lines +78 to +89
test('debounces onChange during the 300ms window', async () => {
const onChange = vi.fn()
render(<BufferedChatInput value="" onChange={onChange} onSubmit={vi.fn()} disabled={false} />)

await userEvent.type(screen.getByTestId('chat-message-input'), 'draft')
// onChange must not fire synchronously with typing; it is deferred by the debounce.
expect(onChange).not.toHaveBeenCalled()

await waitFor(() => {
expect(onChange).toHaveBeenCalledWith('draft')
})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Verify the 300 ms debounce boundary.

waitFor only proves eventual delivery. It does not prove that onChange remains deferred for 300 ms. Control the timer, assert no call before the deadline, then assert the callback after the deadline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/layout/BufferedChatInput.test.tsx` around lines 78 -
89, Update the test “debounces onChange during the 300ms window” to use
controlled/fake timers, assert onChange has not been called before 300 ms, then
advance the timer through the 300 ms boundary and assert it receives “draft”.
Remove the waitFor-based eventual-only verification while preserving the
existing typing setup.

Comment on lines +91 to 102
test('flushes the draft on composition end', async () => {
const onChange = vi.fn()
render(<BufferedChatInput value="" onChange={onChange} onSubmit={vi.fn()} disabled={false} />)

const input = screen.getByTestId('chat-message-input')
await userEvent.type(input, 'draft')
fireEvent.compositionEnd(input)

await waitFor(() => {
expect(onChange).toHaveBeenLastCalledWith('draft')
})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Isolate the composition-end flush from the normal debounce.

This test can pass when compositionEnd is ignored. Hold the debounce timer below its deadline, dispatch compositionEnd, and assert that onChange is called before the timer would expire.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/layout/BufferedChatInput.test.tsx` around lines 91 -
102, Update the “flushes the draft on composition end” test to control or mock
the debounce timer, keep it pending past its deadline, dispatch compositionEnd,
and assert onChange receives “draft” before the debounce timer fires, proving
the composition-end handler performs the flush.

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.

1 participant