Skip to content

feat: 参考 claude code 官方实现,改进 sub agent 以及 fork agent 的渲染方式#416

Merged
claude-code-best merged 2 commits intoclaude-code-best:mainfrom
znygugeyx-ctrl:feat/subagent-fork-render
May 6, 2026
Merged

feat: 参考 claude code 官方实现,改进 sub agent 以及 fork agent 的渲染方式#416
claude-code-best merged 2 commits intoclaude-code-best:mainfrom
znygugeyx-ctrl:feat/subagent-fork-render

Conversation

@znygugeyx-ctrl
Copy link
Copy Markdown
Contributor

@znygugeyx-ctrl znygugeyx-ctrl commented May 5, 2026

Summary

参考 Claude Code 官方实现,改进 sub-agent 和 fork agent 的渲染方式。

当前版本 sub-agent 渲染只显示概要,无法查看完整上下文。本 PR 改造后:

  • 选中某个 sub-agent 后,可查看该 agent 的完整对话上下文
  • 支持在 sub-agent 视图内继续向该 agent 提问
  • 同步调整 fork agent 的渲染,保持体验一致

详细功能说明见新增的 docs/features/background-agent-selector.md

主要改动

  • 新增 BackgroundAgentSelector 组件,承载 agent 列表与详情切换
  • 新增 useBackgroundAgentTasks hook,集中管理后台 agent 任务状态
  • REPL.tsx / PromptInput.tsx 集成选择器与继续提问入口
  • UserForkBoilerplateMessage / UserTextMessage / AssistantToolUseMessage 渲染适配
  • AppStateStore 增加对应状态字段
  • 新增文档 docs/features/background-agent-selector.md

改动统计:11 files changed, +482 / -27

Test plan

  • bun run precheck 通过(typecheck + lint + test 零错误)
  • 本地启动后触发 sub-agent 任务,打开选择器确认能看到完整上下文
  • 在 sub-agent 视图内继续提问,验证消息正确投递与渲染
  • fork 命令触发后,boilerplate 消息渲染正常
  • 无后台 agent 任务时选择器不干扰主 REPL 流程

View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Summary by CodeRabbit

  • New Features

    • Added a background agent selector footer pill for viewing and switching between backgrounded agent tasks with keyboard navigation and visual highlights.
    • Improved fork subagent prompt display to render as a clean, standalone user message in the REPL.
  • Bug Fixes

    • Ensured the fork command follows the fork execution path.
  • Documentation

    • Added comprehensive documentation for the Background Agent Selector.

当前的sub agent渲染无法看到完整的上下文。这个pr参考claude code官方的效果,修改了sub agent的渲染,选择agent后能看到sub-agent完整的上下文,同时也支持sub agent继续提问。具体功能见docs/features/background-agent-selector.md
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

📝 Walkthrough

Walkthrough

Adds a Background Agent Selector UI and state for navigating backgrounded local agent tasks, plus supporting hook, REPL mounting, PromptInput footer integration and fork-prompt message normalization; includes docs and minor message/component tweaks.

Changes

Background Agent Selector Feature

Layer / File(s) Summary
State Definitions
src/state/AppStateStore.ts
Adds FooterItem variant 'bg_agent' and AppState.selectedBgAgentIndex: number with default -1.
Task Filtering Logic
src/hooks/useBackgroundAgentTasks.ts
Adds useBackgroundAgentTasks() to derive background local agent tasks (exclude main-session, respect isBackgrounded, apply evictAfter expiry, sort by startTime).
Selector Component
src/components/tasks/BackgroundAgentSelector.tsx
New BackgroundAgentSelector renders agent rows, elapsed time, token progress, running-state coloring, and context-sensitive hint text; computes highlighted row from app state and pill focus.
PromptInput Footer Integration
src/components/PromptInput/PromptInput.tsx
Adds bg_agent footer pill conditional on useBackgroundAgentTasks(); wires selectedBgAgentIndex into app state; extends footer:up/down/openSelected behavior to navigate selector and enter/exit teammate view while retaining pill focus.
REPL Display & Mounting
src/screens/REPL.tsx
Mounts <BackgroundAgentSelector /> in main UI; adds message transformation for fork subagents to strip boilerplate and synthesize a user prompt message (displayedAgentMessages) when viewing forked tasks.
Fork Command & Messages
src/commands/fork/fork.tsx, src/components/messages/UserForkBoilerplateMessage.tsx, src/components/messages/UserTextMessage.tsx, src/components/messages/AssistantToolUseMessage.tsx
/fork now passes fork: true and uses fixed description; message components unconditionally detect fork-boilerplate tag and render via UserPromptMessage (with optional isTranscriptMode/timestamp); AssistantToolUseMessage adds optional defaultCollapsed prop gating in-progress UI.
State Initialization
src/main.tsx
Initializes selectedBgAgentIndex: -1 in interactive REPL initialState.
Documentation
docs/features/background-agent-selector.md
New specification covering rendering conditions, keyboard routing, data/state architecture, lifecycle/eviction, design decisions, file index, and known limitations.

Sequence Diagram

sequenceDiagram
    actor User
    participant PromptInput
    participant BackgroundAgentSelector
    participant AppState
    participant REPL
    participant TeammateView

    User->>PromptInput: Focus bg_agent footer pill
    PromptInput->>AppState: setSelectedBgAgentIndex(index)
    AppState->>BackgroundAgentSelector: state change -> recompute highlight
    BackgroundAgentSelector-->>User: show highlighted row + hint

    User->>PromptInput: Press Enter on bg_agent pill
    PromptInput->>AppState: setViewingAgentTaskId(task.id)
    AppState->>REPL: viewingAgentTaskId changed
    REPL->>REPL: compute displayedAgentMessages (strip boilerplate, insert prompt)
    REPL-->>User: render agent view
    REPL->>BackgroundAgentSelector: update context (viewing vs pill focused)
    BackgroundAgentSelector-->>User: update hint (Esc to exit)

    User->>REPL: Press Esc
    REPL->>AppState: exitTeammateView()
    AppState->>PromptInput: restore/retain pill focus
    PromptInput-->>User: bg_agent pill focused
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • KonghaYao

Poem

🐰 I hop to find the hidden threads,
A pill that points where background treads,
Fork prompts tidy, messages bright,
Keys navigate from left to right,
The rabbit cheers — tasks in sight.

🚥 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 Title is in Chinese and references improving sub-agent and fork-agent rendering per official Claude Code implementation, which directly aligns with the documented changes across multiple components.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/messages/AssistantToolUseMessage.tsx (1)

99-114: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

defaultCollapsed is not applied in the isTransparentWrapper branch

The transparent-wrapper path (lines 99–114) unconditionally calls renderToolUseProgressMessage, bypassing the defaultCollapsed flag. REPLTool is a concrete transparent wrapper that returns true for isTransparentWrapper(), and if this component is mounted in a collapsed background-agent view, its progress output will still be rendered despite defaultCollapsed === true.

If the intent is to suppress all in-progress UI in the collapsed view, the guard should also be applied here:

Proposed fix
  if (isTransparentWrapper) {
    if (isQueued || isResolved) return null;
+   if (defaultCollapsed) return null;
    return (
      <Box flexDirection="column" width="100%" backgroundColor={bg}>
        {renderToolUseProgressMessage(
🤖 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 `@src/components/messages/AssistantToolUseMessage.tsx` around lines 99 - 114,
The transparent-wrapper branch (isTransparentWrapper) currently always renders
progress via renderToolUseProgressMessage and ignores the defaultCollapsed flag,
so when defaultCollapsed is true (collapsed background-agent view) progress UI
still appears; update the isTransparentWrapper branch in AssistantToolUseMessage
to check defaultCollapsed (and/or collapsedView equivalent) alongside
isQueued/isResolved and return null when defaultCollapsed is true, ensuring
renderToolUseProgressMessage (and its args like tool, tools, lookups, param.id,
progressMessagesForMessage, { verbose, inProgressToolCallCount, isTranscriptMode
}, terminalSize) is only called when not collapsed.
🧹 Nitpick comments (4)
src/hooks/useBackgroundAgentTasks.ts (1)

2-6: ⚡ Quick win

Use src/* path aliases for imports in TS files.

The new relative imports should be switched to src/... aliases for consistency with repository rules.

♻️ Suggested import update
-import { useAppState } from '../state/AppState.js'
+import { useAppState } from 'src/state/AppState.js'
 import {
   isLocalAgentTask,
   type LocalAgentTaskState,
-} from '../tasks/LocalAgentTask/LocalAgentTask.js'
+} from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
As per coding guidelines, "Use `src/*` path alias in imports; do not use relative paths like `../../../`".
🤖 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 `@src/hooks/useBackgroundAgentTasks.ts` around lines 2 - 6, Replace the
relative import paths with the repository's src/* path aliases: update the
import of useAppState to import from "src/state/AppState" and update the imports
of isLocalAgentTask and LocalAgentTaskState to import from
"src/tasks/LocalAgentTask/LocalAgentTask"; keep the same exported symbols
(useAppState, isLocalAgentTask, LocalAgentTaskState) and file extensions
consistent with the codebase.
src/components/PromptInput/PromptInput.tsx (1)

29-29: ⚡ Quick win

Use src/* alias for the new hook import.

This new import should follow the repo import alias convention to avoid adding new relative-path debt.

Suggested change
-import { useBackgroundAgentTasks } from '../../hooks/useBackgroundAgentTasks.js';
+import { useBackgroundAgentTasks } from 'src/hooks/useBackgroundAgentTasks.js';

As per coding guidelines, "Use src/* path alias in imports; do not use relative paths like ../../../."

🤖 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 `@src/components/PromptInput/PromptInput.tsx` at line 29, The import in
PromptInput.tsx uses a relative path for the new hook; change the import of
useBackgroundAgentTasks to use the repo alias (src/*) instead of
'../../hooks/...'. Locate the import statement that currently reads import {
useBackgroundAgentTasks } from '../../hooks/useBackgroundAgentTasks.js' and
update it to import from 'src/hooks/useBackgroundAgentTasks' (remove the
relative path and .js extension if project convention omits it).
src/components/tasks/BackgroundAgentSelector.tsx (1)

2-6: ⚡ Quick win

Switch new local imports to src/* aliases.

These newly added relative imports should use the project alias style.

Suggested change
-import { useBackgroundAgentTasks } from '../../hooks/useBackgroundAgentTasks.js';
-import { useElapsedTime } from '../../hooks/useElapsedTime.js';
-import { useAppState } from '../../state/AppState.js';
-import type { LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
-import { formatTokens } from '../../utils/format.js';
+import { useBackgroundAgentTasks } from 'src/hooks/useBackgroundAgentTasks.js';
+import { useElapsedTime } from 'src/hooks/useElapsedTime.js';
+import { useAppState } from 'src/state/AppState.js';
+import type { LocalAgentTaskState } from 'src/tasks/LocalAgentTask/LocalAgentTask.js';
+import { formatTokens } from 'src/utils/format.js';

As per coding guidelines, "Use src/* path alias in imports; do not use relative paths like ../../../."

🤖 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 `@src/components/tasks/BackgroundAgentSelector.tsx` around lines 2 - 6, Update
the new relative imports in BackgroundAgentSelector.tsx to use the project
path-alias style (src/*) instead of relative paths: replace the imports for
useBackgroundAgentTasks, useElapsedTime, useAppState, the LocalAgentTaskState
type, and formatTokens so they import from their src/* alias locations; ensure
the imported symbol names (useBackgroundAgentTasks, useElapsedTime, useAppState,
LocalAgentTaskState, formatTokens) remain unchanged so existing references in
the file keep working.
src/screens/REPL.tsx (1)

247-248: ⚡ Quick win

Use src/* aliases for the new internal imports.

These additions reintroduce relative paths in newly touched code. Please switch the new imports to the src/* alias form instead of adding more ../… references.

Suggested cleanup
-import { BASH_INPUT_TAG, COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG, FORK_BOILERPLATE_TAG, LOCAL_COMMAND_STDOUT_TAG } from '../constants/xml.js';
+import { BASH_INPUT_TAG, COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG, FORK_BOILERPLATE_TAG, LOCAL_COMMAND_STDOUT_TAG } from 'src/constants/xml.js';
...
-import { BackgroundAgentSelector } from '../components/tasks/BackgroundAgentSelector.js';
+import { BackgroundAgentSelector } from 'src/components/tasks/BackgroundAgentSelector.js';

As per coding guidelines, "Use src/* path alias in imports; do not use relative paths like ../../../."

Also applies to: 340-340

🤖 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 `@src/screens/REPL.tsx` around lines 247 - 248, Replace
relative/new-package-style imports with the project alias form: update the
import that brings in BASH_INPUT_TAG, COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG,
FORK_BOILERPLATE_TAG, LOCAL_COMMAND_STDOUT_TAG (currently from
'../constants/xml.js') to use the src/* alias (e.g., import from
'src/constants/xml'), and change the FORK_SUBAGENT_TYPE import (currently from
'@claude-code-best/builtin-tools/tools/AgentTool/forkSubagent.js') to the
corresponding src alias path (e.g.,
'src/builtin-tools/tools/AgentTool/forkSubagent'); keep the same named symbols
(BASH_INPUT_TAG, COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG, FORK_BOILERPLATE_TAG,
LOCAL_COMMAND_STDOUT_TAG, FORK_SUBAGENT_TYPE) so only the module paths are
replaced.
🤖 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 `@docs/features/background-agent-selector.md`:
- Around line 28-36: The fenced code block that begins with "claude-code | Opus
4.7 (1M context) | ctx:4%" lacks a language specifier; update the opening
backticks (the triple-``` fence) to include a language such as "text" (e.g.,
```text) so the block renders and lints correctly and leave the block contents
unchanged.

In `@src/components/PromptInput/PromptInput.tsx`:
- Around line 515-517: When the background-agent list (useBackgroundAgentTasks
-> bgAgentList) shrinks the current selectedBgAgentIndex can exceed the new
length; add a clamp step whenever bgAgentList changes to
setSelectedBgAgentIndex(prev => Math.max(0, Math.min(prev, bgAgentList.length -
1))) (or clear to -1 if you use -1 for no selection) so Enter still targets a
valid item. Implement this in an effect that depends on bgAgentList (or inside
the reducer/handler that mutates bgAgentList) and reference
selectedBgAgentIndex, bgAgentList, useBackgroundAgentTasks, and
setSelectedBgAgentIndex to locate where to apply the clamp.

In `@src/components/tasks/BackgroundAgentSelector.tsx`:
- Line 35: The component BackgroundAgentSelector currently uses the type
React.ReactNode without importing React and uses relative imports; fix by
importing the React type (e.g., import type { ReactNode } from 'react' and
change the signature to return ReactNode, or import * as React) and replace all
relative module paths with the project's src aliases for the referenced
symbols—update imports that bring in useBackgroundAgentTasks, useAppState,
LocalAgentTaskState and any other same-folder imports to use 'src/...' paths
(e.g., import { useBackgroundAgentTasks } from
'src/hooks/useBackgroundAgentTasks.js', import { useAppState } from
'src/state/AppState.js', import type { LocalAgentTaskState } from
'src/tasks/LocalAgentTask/LocalAgentTask.js').

In `@src/hooks/useBackgroundAgentTasks.ts`:
- Around line 10-18: The memoized list of background tasks uses a captured
Date.now() ('now' in useBackgroundAgentTasks) but only depends on 'tasks', so
expirations (evictAfter) won't re-evaluate; update the hook to make the memo
also depend on a time tick (e.g., a small interval-based state like 'now' or
useNow hook) or include a computed time bucket in the dependency array, then use
that 'now' when filtering by evictAfter so tasks are recomputed when they expire
(reference useBackgroundAgentTasks, the 'now' variable, and the 'evictAfter'
filter).

In `@src/screens/REPL.tsx`:
- Around line 5591-5600: The duplicate-prompt check only inspects the first text
block; change it so promptAlreadyRendered is set if any text block in a user
message equals trimmedPrompt (not just the first), and also handle messages
whose content is a plain string by comparing m.message.content ===
trimmedPrompt. Update the branch that uses Array.isArray(m.message?.content) to
use Array.prototype.find (or some) to locate any block where b.type === 'text'
&& typeof b.text === 'string' && b.text.trim() === trimmedPrompt, and in the
non-array path check the raw string content; keep existing references to
isForkBoilerplateTextBlock, boilerplateIndex, and promptAlreadyRendered
unchanged.

---

Outside diff comments:
In `@src/components/messages/AssistantToolUseMessage.tsx`:
- Around line 99-114: The transparent-wrapper branch (isTransparentWrapper)
currently always renders progress via renderToolUseProgressMessage and ignores
the defaultCollapsed flag, so when defaultCollapsed is true (collapsed
background-agent view) progress UI still appears; update the
isTransparentWrapper branch in AssistantToolUseMessage to check defaultCollapsed
(and/or collapsedView equivalent) alongside isQueued/isResolved and return null
when defaultCollapsed is true, ensuring renderToolUseProgressMessage (and its
args like tool, tools, lookups, param.id, progressMessagesForMessage, { verbose,
inProgressToolCallCount, isTranscriptMode }, terminalSize) is only called when
not collapsed.

---

Nitpick comments:
In `@src/components/PromptInput/PromptInput.tsx`:
- Line 29: The import in PromptInput.tsx uses a relative path for the new hook;
change the import of useBackgroundAgentTasks to use the repo alias (src/*)
instead of '../../hooks/...'. Locate the import statement that currently reads
import { useBackgroundAgentTasks } from '../../hooks/useBackgroundAgentTasks.js'
and update it to import from 'src/hooks/useBackgroundAgentTasks' (remove the
relative path and .js extension if project convention omits it).

In `@src/components/tasks/BackgroundAgentSelector.tsx`:
- Around line 2-6: Update the new relative imports in
BackgroundAgentSelector.tsx to use the project path-alias style (src/*) instead
of relative paths: replace the imports for useBackgroundAgentTasks,
useElapsedTime, useAppState, the LocalAgentTaskState type, and formatTokens so
they import from their src/* alias locations; ensure the imported symbol names
(useBackgroundAgentTasks, useElapsedTime, useAppState, LocalAgentTaskState,
formatTokens) remain unchanged so existing references in the file keep working.

In `@src/hooks/useBackgroundAgentTasks.ts`:
- Around line 2-6: Replace the relative import paths with the repository's src/*
path aliases: update the import of useAppState to import from
"src/state/AppState" and update the imports of isLocalAgentTask and
LocalAgentTaskState to import from "src/tasks/LocalAgentTask/LocalAgentTask";
keep the same exported symbols (useAppState, isLocalAgentTask,
LocalAgentTaskState) and file extensions consistent with the codebase.

In `@src/screens/REPL.tsx`:
- Around line 247-248: Replace relative/new-package-style imports with the
project alias form: update the import that brings in BASH_INPUT_TAG,
COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG, FORK_BOILERPLATE_TAG,
LOCAL_COMMAND_STDOUT_TAG (currently from '../constants/xml.js') to use the src/*
alias (e.g., import from 'src/constants/xml'), and change the FORK_SUBAGENT_TYPE
import (currently from
'@claude-code-best/builtin-tools/tools/AgentTool/forkSubagent.js') to the
corresponding src alias path (e.g.,
'src/builtin-tools/tools/AgentTool/forkSubagent'); keep the same named symbols
(BASH_INPUT_TAG, COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG, FORK_BOILERPLATE_TAG,
LOCAL_COMMAND_STDOUT_TAG, FORK_SUBAGENT_TYPE) so only the module paths are
replaced.
🪄 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

Run ID: 58221a21-a1dd-4a83-9772-60ac83782dcc

📥 Commits

Reviewing files that changed from the base of the PR and between 872ee28 and d1211f7.

📒 Files selected for processing (11)
  • docs/features/background-agent-selector.md
  • src/commands/fork/fork.tsx
  • src/components/PromptInput/PromptInput.tsx
  • src/components/messages/AssistantToolUseMessage.tsx
  • src/components/messages/UserForkBoilerplateMessage.tsx
  • src/components/messages/UserTextMessage.tsx
  • src/components/tasks/BackgroundAgentSelector.tsx
  • src/hooks/useBackgroundAgentTasks.ts
  • src/main.tsx
  • src/screens/REPL.tsx
  • src/state/AppStateStore.ts

Comment on lines +28 to +36
```
claude-code | Opus 4.7 (1M context) | ctx:4%
▶▶ bypass permissions on (shift+tab to cycle)

○ main ↑/↓ to select · Enter to view
● Explore Research src/hooks 23s · ↓ 10.9k tokens
○ Explore Research src/components 22s · ↓ 9.5k tokens
○ Explore Research src/utils 21s · ↓ 13.6k tokens
```
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add language specifier to code fence.

The fenced code block should specify a language for proper rendering and linting compliance.

📝 Proposed fix
-```
+```text
   claude-code | Opus 4.7 (1M context) | ctx:4%
   ▶▶ bypass permissions on (shift+tab to cycle)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
claude-code | Opus 4.7 (1M context) | ctx:4%
▶▶ bypass permissions on (shift+tab to cycle)
○ main ↑/↓ to select · Enter to view
● Explore Research src/hooks 23s · ↓ 10.9k tokens
○ Explore Research src/components 22s · ↓ 9.5k tokens
○ Explore Research src/utils 21s · ↓ 13.6k tokens
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 28-28: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@docs/features/background-agent-selector.md` around lines 28 - 36, The fenced
code block that begins with "claude-code | Opus 4.7 (1M context) | ctx:4%" lacks
a language specifier; update the opening backticks (the triple-``` fence) to
include a language such as "text" (e.g., ```text) so the block renders and lints
correctly and leave the block contents unchanged.

Comment on lines +515 to 517
const bgAgentList = useBackgroundAgentTasks();
const bgAgentFooterVisible = bgAgentList.length > 0;

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clamp selectedBgAgentIndex when the background-agent list shrinks.

If tasks complete while the pill stays focused, the index can point past the end of bgAgentList, leaving no valid selection and making Enter a no-op until manual recovery.

Suggested change
   const bgAgentList = useBackgroundAgentTasks();
   const bgAgentFooterVisible = bgAgentList.length > 0;
+  useEffect(() => {
+    setSelectedBgAgentIndex(prev => {
+      const max = bgAgentList.length - 1;
+      const clamped = Math.max(-1, Math.min(prev, max));
+      return clamped;
+    });
+  }, [bgAgentList.length, setSelectedBgAgentIndex]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const bgAgentList = useBackgroundAgentTasks();
const bgAgentFooterVisible = bgAgentList.length > 0;
const bgAgentList = useBackgroundAgentTasks();
const bgAgentFooterVisible = bgAgentList.length > 0;
useEffect(() => {
setSelectedBgAgentIndex(prev => {
const max = bgAgentList.length - 1;
const clamped = Math.max(-1, Math.min(prev, max));
return clamped;
});
}, [bgAgentList.length, setSelectedBgAgentIndex]);
🤖 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 `@src/components/PromptInput/PromptInput.tsx` around lines 515 - 517, When the
background-agent list (useBackgroundAgentTasks -> bgAgentList) shrinks the
current selectedBgAgentIndex can exceed the new length; add a clamp step
whenever bgAgentList changes to setSelectedBgAgentIndex(prev => Math.max(0,
Math.min(prev, bgAgentList.length - 1))) (or clear to -1 if you use -1 for no
selection) so Enter still targets a valid item. Implement this in an effect that
depends on bgAgentList (or inside the reducer/handler that mutates bgAgentList)
and reference selectedBgAgentIndex, bgAgentList, useBackgroundAgentTasks, and
setSelectedBgAgentIndex to locate where to apply the clamp.

return viewedTask.status === 'running' ? 'shift+↓ to manage · x to stop' : 'shift+↓ to manage · x to clear';
}

export function BackgroundAgentSelector(): React.ReactNode {
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether this file relies on React namespace without import,
# and whether tsconfig permits UMD globals.
set -euo pipefail

echo "=== File evidence ==="
sed -n '1,80p' src/components/tasks/BackgroundAgentSelector.tsx | nl -ba | sed -n '1,60p'
echo
echo "=== React namespace usage ==="
rg -n '\bReact\.ReactNode\b' src/components/tasks/BackgroundAgentSelector.tsx
echo
echo "=== React import in file ==="
rg -n "from 'react'|from \"react\"" src/components/tasks/BackgroundAgentSelector.tsx || true
echo
echo "=== allowUmdGlobalAccess in tsconfig files ==="
fd -i '^tsconfig.*\.json$' -x sh -c 'echo "--- $1"; cat "$1" | jq ".compilerOptions.allowUmdGlobalAccess // \"<unset>\""' sh {}

Repository: claude-code-best/claude-code

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== File content (BackgroundAgentSelector.tsx) ==="
cat -n src/components/tasks/BackgroundAgentSelector.tsx | head -80

echo ""
echo "=== React.ReactNode usage ==="
rg -n 'React\.ReactNode' src/components/tasks/BackgroundAgentSelector.tsx || echo "No match found"

echo ""
echo "=== React imports in file ==="
rg -n "from ['\"]react['\"]" src/components/tasks/BackgroundAgentSelector.tsx || echo "No React import found"

echo ""
echo "=== Check tsconfig.json for strict and module settings ==="
cat -n tsconfig.json | head -30

Repository: claude-code-best/claude-code

Length of output: 4716


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full tsconfig.json ==="
cat tsconfig.json

echo ""
echo "=== Run TypeScript strict check on this file ==="
bunx tsc --noEmit src/components/tasks/BackgroundAgentSelector.tsx 2>&1 || true

echo ""
echo "=== Check other React component files for pattern ==="
rg -l 'React\.ReactNode' src/components/ | head -5

echo ""
echo "=== Check if any component files import React ==="
rg "import.*from ['\"]react['\"]" src/components/ | head -3 || echo "No React imports found in components"

Repository: claude-code-best/claude-code

Length of output: 1894


React.ReactNode requires React import; also replace relative imports with src/* aliases.

Line 35 uses React.ReactNode without importing React. With strict: true and no allowUmdGlobalAccess in tsconfig, this fails TypeScript compilation. Import React explicitly: import type { ReactNode } from 'react' and return ReactNode, or use import * as React from 'react'.

Additionally, lines 2–6 use relative paths (../../hooks, ../../state, etc.) instead of the required src/* path aliases. Update imports to:

  • import { useBackgroundAgentTasks } from 'src/hooks/useBackgroundAgentTasks.js'
  • import { useAppState } from 'src/state/AppState.js'
  • import type { LocalAgentTaskState } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
  • (and similar for remaining imports)
🤖 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 `@src/components/tasks/BackgroundAgentSelector.tsx` at line 35, The component
BackgroundAgentSelector currently uses the type React.ReactNode without
importing React and uses relative imports; fix by importing the React type
(e.g., import type { ReactNode } from 'react' and change the signature to return
ReactNode, or import * as React) and replace all relative module paths with the
project's src aliases for the referenced symbols—update imports that bring in
useBackgroundAgentTasks, useAppState, LocalAgentTaskState and any other
same-folder imports to use 'src/...' paths (e.g., import {
useBackgroundAgentTasks } from 'src/hooks/useBackgroundAgentTasks.js', import {
useAppState } from 'src/state/AppState.js', import type { LocalAgentTaskState }
from 'src/tasks/LocalAgentTask/LocalAgentTask.js').

Comment on lines +10 to +18
return useMemo(() => {
const now = Date.now()
return Object.values(tasks)
.filter(isLocalAgentTask)
.filter(t => t.agentType !== 'main-session')
.filter(t => t.isBackgrounded !== false)
.filter(t => t.evictAfter === undefined || t.evictAfter > now)
.sort((a, b) => a.startTime - b.startTime)
}, [tasks])
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Expired tasks can remain visible until tasks mutates.

Line 11 computes Date.now() inside a useMemo that only depends on tasks (Line 18), so evictAfter expiry won’t trigger recompute on its own.

💡 Suggested fix
-import { useMemo } from 'react'
+import { useEffect, useMemo, useState } from 'react'
@@
 export function useBackgroundAgentTasks(): LocalAgentTaskState[] {
   const tasks = useAppState(s => s.tasks)
+  const [now, setNow] = useState(() => Date.now())
+
+  useEffect(() => {
+    const timer = setInterval(() => setNow(Date.now()), 1000)
+    return () => clearInterval(timer)
+  }, [])
+
   return useMemo(() => {
-    const now = Date.now()
     return Object.values(tasks)
@@
-  }, [tasks])
+  }, [tasks, now])
 }
🤖 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 `@src/hooks/useBackgroundAgentTasks.ts` around lines 10 - 18, The memoized list
of background tasks uses a captured Date.now() ('now' in
useBackgroundAgentTasks) but only depends on 'tasks', so expirations
(evictAfter) won't re-evaluate; update the hook to make the memo also depend on
a time tick (e.g., a small interval-based state like 'now' or useNow hook) or
include a computed time bucket in the dependency array, then use that 'now' when
filtering by evictAfter so tasks are recomputed when they expire (reference
useBackgroundAgentTasks, the 'now' variable, and the 'evictAfter' filter).

Comment thread src/screens/REPL.tsx
Comment on lines +5591 to +5600
if (m.type === 'user' && Array.isArray(m.message?.content)) {
const hasBoilerplate = m.message.content.some(isForkBoilerplateTextBlock);
if (hasBoilerplate) {
boilerplateIndex = i;
} else if (!promptAlreadyRendered) {
const firstText = m.message.content.find(b => b.type === 'text' && typeof b.text === 'string') as
| { type: 'text'; text: string }
| undefined;
if (firstText && firstText.text.trim() === trimmedPrompt) promptAlreadyRendered = true;
}
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Broaden the duplicate-prompt check before inserting the synthetic fork bubble.

Line 5596 only looks at the first text block on array-backed user messages. If a retained fork transcript already stores the prompt as plain string content, or in a later text block, this path still inserts the synthetic bubble and the selector shows the prompt twice.

Suggested fix
+function userMessageContainsPrompt(message: MessageType, prompt: string): boolean {
+  if (message.type !== 'user') return false;
+  const content = message.message?.content;
+  if (typeof content === 'string') return content.trim() === prompt;
+  if (!Array.isArray(content)) return false;
+  return content.some(block => block.type === 'text' && typeof block.text === 'string' && block.text.trim() === prompt);
+}
+
 ...
-        } else if (!promptAlreadyRendered) {
-          const firstText = m.message.content.find(b => b.type === 'text' && typeof b.text === 'string') as
-            | { type: 'text'; text: string }
-            | undefined;
-          if (firstText && firstText.text.trim() === trimmedPrompt) promptAlreadyRendered = true;
+        } else if (!promptAlreadyRendered && userMessageContainsPrompt(m, trimmedPrompt)) {
+          promptAlreadyRendered = true;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (m.type === 'user' && Array.isArray(m.message?.content)) {
const hasBoilerplate = m.message.content.some(isForkBoilerplateTextBlock);
if (hasBoilerplate) {
boilerplateIndex = i;
} else if (!promptAlreadyRendered) {
const firstText = m.message.content.find(b => b.type === 'text' && typeof b.text === 'string') as
| { type: 'text'; text: string }
| undefined;
if (firstText && firstText.text.trim() === trimmedPrompt) promptAlreadyRendered = true;
}
function userMessageContainsPrompt(message: MessageType, prompt: string): boolean {
if (message.type !== 'user') return false;
const content = message.message?.content;
if (typeof content === 'string') return content.trim() === prompt;
if (!Array.isArray(content)) return false;
return content.some(block => block.type === 'text' && typeof block.text === 'string' && block.text.trim() === prompt);
}
if (m.type === 'user' && Array.isArray(m.message?.content)) {
const hasBoilerplate = m.message.content.some(isForkBoilerplateTextBlock);
if (hasBoilerplate) {
boilerplateIndex = i;
} else if (!promptAlreadyRendered && userMessageContainsPrompt(m, trimmedPrompt)) {
promptAlreadyRendered = true;
}
🤖 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 `@src/screens/REPL.tsx` around lines 5591 - 5600, The duplicate-prompt check
only inspects the first text block; change it so promptAlreadyRendered is set if
any text block in a user message equals trimmedPrompt (not just the first), and
also handle messages whose content is a plain string by comparing
m.message.content === trimmedPrompt. Update the branch that uses
Array.isArray(m.message?.content) to use Array.prototype.find (or some) to
locate any block where b.type === 'text' && typeof b.text === 'string' &&
b.text.trim() === trimmedPrompt, and in the non-array path check the raw string
content; keep existing references to isForkBoilerplateTextBlock,
boilerplateIndex, and promptAlreadyRendered unchanged.

@znygugeyx-ctrl
Copy link
Copy Markdown
Contributor Author

znygugeyx-ctrl commented May 5, 2026

sub agent效果图:

sub-agent-3

fork agent效果图:

fork-agent

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/screens/REPL.tsx (2)

247-253: ⚡ Quick win

Use src/* alias for the newly added imports.

The newly introduced imports in this patch are relative-path imports.

Suggested change
 import {
   BASH_INPUT_TAG,
   COMMAND_MESSAGE_TAG,
   COMMAND_NAME_TAG,
   FORK_BOILERPLATE_TAG,
   LOCAL_COMMAND_STDOUT_TAG,
-} from '../constants/xml.js';
+} from 'src/constants/xml.js';
 ...
-import { BackgroundAgentSelector } from '../components/tasks/BackgroundAgentSelector.js';
+import { BackgroundAgentSelector } from 'src/components/tasks/BackgroundAgentSelector.js';

As per coding guidelines, "Use src/* path alias in imports; do not use relative paths like ../../../".

Also applies to: 346-346

🤖 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 `@src/screens/REPL.tsx` around lines 247 - 253, Replace the relative-path
imports in REPL.tsx with the project path alias by importing BASH_INPUT_TAG,
COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG, FORK_BOILERPLATE_TAG, and
LOCAL_COMMAND_STDOUT_TAG from "src/constants/xml" instead of the relative
'../constants/xml.js' (and update the other occurrence where the same module is
imported around the later usage of these tags); ensure any import extensions
(.js) match the project's alias conventions.

5602-5605: ⚡ Quick win

Replace the forced type cast with a type predicate for better type safety.

The .find() call on line 5602 uses a forced as assertion that bypasses TypeScript's type narrowing. Since the predicate properly narrows { type: string; [key: string]: unknown } to { type: 'text'; text: string }, use a type predicate instead:

Suggested change
-          const firstText = m.message.content.find(b => b.type === 'text' && typeof b.text === 'string') as
-            | { type: 'text'; text: string }
-            | undefined;
+          const firstText = m.message.content.find(
+            (b): b is { type: 'text'; text: string } => b.type === 'text' && typeof b.text === 'string',
+          );
           if (firstText && firstText.text.trim() === trimmedPrompt) promptAlreadyRendered = true;
🤖 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 `@src/screens/REPL.tsx` around lines 5602 - 5605, The current .find() call
assigns to firstText using a forced cast which bypasses TypeScript narrowing;
replace that cast by using a type predicate function (e.g., isTextBlock) that
asserts b is { type: 'text'; text: string } and pass that predicate to
m.message.content.find; then keep the existing check if (firstText &&
firstText.text.trim() === trimmedPrompt) promptAlreadyRendered = true so
TypeScript knows firstText.text is a string without the unsafe as cast
(referencing firstText, m.message.content, promptAlreadyRendered,
trimmedPrompt).
🤖 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.

Nitpick comments:
In `@src/screens/REPL.tsx`:
- Around line 247-253: Replace the relative-path imports in REPL.tsx with the
project path alias by importing BASH_INPUT_TAG, COMMAND_MESSAGE_TAG,
COMMAND_NAME_TAG, FORK_BOILERPLATE_TAG, and LOCAL_COMMAND_STDOUT_TAG from
"src/constants/xml" instead of the relative '../constants/xml.js' (and update
the other occurrence where the same module is imported around the later usage of
these tags); ensure any import extensions (.js) match the project's alias
conventions.
- Around line 5602-5605: The current .find() call assigns to firstText using a
forced cast which bypasses TypeScript narrowing; replace that cast by using a
type predicate function (e.g., isTextBlock) that asserts b is { type: 'text';
text: string } and pass that predicate to m.message.content.find; then keep the
existing check if (firstText && firstText.text.trim() === trimmedPrompt)
promptAlreadyRendered = true so TypeScript knows firstText.text is a string
without the unsafe as cast (referencing firstText, m.message.content,
promptAlreadyRendered, trimmedPrompt).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ee78aef2-7534-4c91-bfc3-8cea25e47522

📥 Commits

Reviewing files that changed from the base of the PR and between d1211f7 and 9788df7.

📒 Files selected for processing (1)
  • src/screens/REPL.tsx

@claude-code-best claude-code-best merged commit 5c107e5 into claude-code-best:main May 6, 2026
3 checks passed
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.

2 participants