Skip to content

feat(dashboard): recent files list in Files view#9331

Closed
elecvoid243 wants to merge 702 commits into
AstrBotDevs:masterfrom
elecvoid243:feat/recent-files
Closed

feat(dashboard): recent files list in Files view#9331
elecvoid243 wants to merge 702 commits into
AstrBotDevs:masterfrom
elecvoid243:feat/recent-files

Conversation

@elecvoid243

Copy link
Copy Markdown
Contributor

Implements docs/superpowers/specs/2026-07-20-recent-files-design.md. New (default-collapsed) above FileTreeList. Per-worktree 50-entry localStorage bucket; recordOpen on fileBrowserPreviewPath changes; row click mirrors onFileOpen; × button per row; Clear link with secondary-confirm dialog. No new deps. 28 new unit tests.

This reverts commit e943191.
…reen overlay, i18n keys, spec, plan, and preserve files_changed fix
# Conflicts:
#	tests/test_computer_fs_tools.py
When the page is served over an insecure context (e.g. plain HTTP

in dev, or localhost in some browser configs) navigator.clipboard

is undefined. The copy button silently did nothing. Wire it to the

existing copyToClipboard util, surface success / failure via the

button color, add a copyFail i18n key (en / zh / ru) and clean up

the reset timer on unmount.
The diff toolbar's "N files selected" counter stayed frozen at the

pre-action count after a successful bulk stage / unstage because

the child GitDiffBodyContent owns the selection Set — the parent

could only mutate stagedFiles, leaving the visible counter stale

and risking re-staging of already-staged files on the next click.

Expose clearSelection() from the child, invoke it from the parent's

onStagePaths / onUnstagePaths success branches only (failures keep

the user's selection so they can retry without re-ticking).
Symmetric to unified mode: split-view hunks are now foldable via a

button on the hunk header, with chevron + rows-length badge. Shares

the same collapsedHunks Set as unified, so a fold decision carries

over when the user toggles view modes mid-review. v-show (not v-if)

keeps row DOM in place so scroll position and comment hover state

survive toggles. Mirrored in the fullscreen overlay.
Wire shiki/langs/matlab.mjs into the limited bundle (whitelist-based,

so without this import matlab code falls back to plain text in

codeToHtml). Map the .m and .matlab extensions to "matlab" in both

EXT_TO_LANG tables (FileBrowserFilePreview, ToolResultView) so

file_read_tool and the git diff file preview recognize MATLAB

files. .m is also the Objective-C extension but objective-c is not

in the shiki whitelist, so claiming it here does not collide.
When AstrBot is launched via pythonw.exe (GUI subsystem), spawning a
python.exe child already used `creationflags=CREATE_NO_WINDOW` so the
direct child stays windowless. But a user-side `subprocess.run` /
`subprocess.Popen` invoked from inside the spawned interpreter will
still allocate a brand-new console window for its CUI grandchildren
because the current python.exe has no inherited console to hand out —
the user-reported nested "弹出 cmd 黑框" case.

Fix: prepend an idempotent preamble (`_PYTHON_SUBPROCESS_PREAMBLE`)
to the user code handed to the spawned interpreter. The preamble
replaces `subprocess.Popen` with a subclass that always injects
`creationflags=CREATE_NO_WINDOW` and a `STARTUPINFO` with
`wShowWindow = SW_HIDE`. The patch is guarded by an
`_ab_no_window_patched` flag so repeated executions cannot
stack-wrap the wrapper class.

Out-of-scope (unchanged, documented in the constant's docstring):
`from subprocess import Popen` captured before the preamble runs,
and direct `os.spawn*` / `os.system` / `os.popen` / ctypes
`CreateProcessW` calls.

The wrapping happens inside `LocalPythonComponent.exec`, so the
`sb.python.exec(code=...)` call site still receives the original
code string — no upstream signature or test change required.
Adds a styled result card for spcode plugin's `code_format` tool,
covering all four ok x changed x check state combinations.

- Reuses `DiffPreview` for unified diff rendering from `diff_summary`
- Reuses `CopyableText` for file-path hover-to-copy affordance
- Status row uses 4-color variant (success / warn / error / info)
- Integrates with `SpcodeToolResultView` dispatch chain
- Registers icon in `SPCODE_ICONS` for ToolCallCard header
- Refreshes MDI icon subset to include `mdi-format-paint`

Verified by `pnpm typecheck` (zero errors) and `npm run build`
(2174 modules transformed in 40.86s, exit 0).

Author: elecvoid243
Date: 2026-07-01
…tered snapshot

Reset's URL is identical to the very first history-tab load (?ref=HEAD&n=20,

no author/path/since), so without intervention the backend returned 304

Not Modified and the client-side 304 branch replayed prevSnapshot — which

was overwritten by the user's most recent filter (e.g. author=alice) and

therefore showed the filtered list instead of the reset state.

Three layered changes:

* useSpcodeGitLog: expose invalidateEtagFor(filter) that drops the ETag

  entry for exactly one filter tuple (others are preserved so author=bob

  etc. still hit cache). refresh(override, { forceLoading }) lets the caller

  force the loading transition when the previous state was already ok, so

  the spinner shows during the request — fixes the parallel UX bug where

  reset looked like nothing happened until the response arrived.

* GitLogView: split reset into its own emit("reset") event so the parent

  can run reset-specific prep instead of overloading apply.

* GitDiffSidebar: wire onLogReset(filter) that invalidates the ETag first,

  then refreshes with forceLoading.
renderWindow: widen context from ±1 to ±3 (CONTEXT_LINES) and fill middle
non-commented lines from contentCache so merged windows are no longer
peppered with empty rows. Adds EOF clamp via totalLines.

formatForLLM: derive MERGE_DISTANCE = 2 × CONTEXT_LINES so any two
non-merged windows are physically non-overlapping in the LLM-facing
output.

spec: sync the file-browser-inline-comments-design doc (§3 decision table,
§4.1, §5.1 examples + key conventions, §5.3 algorithm, §6 trade-off AstrBotDevs#2
marked resolved).
CommentsPreviewDialog: each comment now has a chevron toggle (next to
the delete button) that expands a code-context block below the comment
text. The expanded view mirrors formatForLLM's two code paths so users
can spot-check exactly what the LLM will receive for that comment:

  - File-browser comments: ±CONTEXT_LINES rows pulled from the shared
    content cache. The commented line gets a `>` marker and primary
    highlight; remaining lines render their current on-disk content
    (or empty if the cache hasn't registered the file yet).
  - Diff comments: the full hunk with the commented new-side line
    marked with `>`. Unified-diff prefixes (+/-/space) are preserved
    so the user sees the exact hunk shape.

Expanded state is a per-dialog Set<string> of comment ids. The state
is dialog-local and resets when the dialog unmounts (consistent with
the dialog's "view-only" model — see spec §4.2).

useFileComments: expose getFileContent(filePath) on the store so the
dialog can read the content cache without a new file fetch. The
closure-scoped CONTEXT_LINES is mirrored as a local const in the
dialog with a comment explaining the linkage.

i18n: 2 new keys per locale (expandContext / collapseContext) across
zh-CN / en-US / ru-RU.
Single-endpoint feature: POST /spcode/file-search in the spcode toolkit
plugin (F:\github\astrbot_plugin_spcode_toolkit) with ripgrep primary
plus a pure-Python fallback when rg is missing. Pairs with a frontend
SearchPanel + useSpcodeFileSearch composable mounted in GitDiffSidebar.

Spec covers:
- Backend handler + rg availability probe at plugin init
- Frontend composable, SearchPanel component, GitDiffSidebar integration
- Interface contract: request/response/reason codes with examples
- Config schema change: new search.rg_path field
- 20 backend unit tests + frontend state machine tests
- 8 risks + 5 trade-offs documented

Out of scope for v1 (deferred to v2): file outline, recent-files chip,
blame chip, AI-edited filter. Search alone solves ~80% of the "find
code" UX gap.
8 bite-sized TDD tasks covering the full /spcode/file-search feature:

  1. Backend scaffolding (ReasonCode, rg probe, config, route stub)
  2. Backend rg primary path with 12 failing tests
  3. Backend Python fallback with 6 failing tests
  4. Frontend useSpcodeFileSearch composable (vitest if available)
  5. Frontend SearchPanel.vue component
  6. Frontend GitDiffSidebar integration (button + Cmd-F + localStorage)
  7. Frontend FileBrowserView integration (mount SearchPanel)
  8. Frontend i18n keys (zh-CN / en-US / ru-RU)

Each task is a self-contained deliverable with its own commit. TDD
where pytest/vitest is set up; vue-tsc + prettier for frontend.
Follows the conventions established in 2026-06-20-file-browser-
endpoint-design and 2026-06-24-chatui-git-workflow-controls-design.
Input row with magnify icon + close button; 300ms debounce on query
GitDiffSidebar gains a search button (Files toolbar) + Cmd/Ctrl-F
shortcut (only when sidebar visible + viewMode=files) + Escape
fallback close. searchOpen state persisted to localStorage.
FileBrowserView now receives search-open / umo / worktree props and
emits open-file; sidebar sets preview path on result click so the
existing FileBrowserView content fetch + line scroll kicks in.
Adds spcodeProjectLoad.diffSidebar.search.* across zh-CN / en-US /
ru-RU: button, placeholder, hint, searching, resultCount, truncated,
toolbarActive, fallbackHint, plus 10 error.* keys covering every
ReasonCode in the backend.
User feedback: ripgrep invocation should go through the python_ripgrep
PyPI package (already an AstrBot dependency, used by astrbot_grep_tool
at astrbot/core/computer/booters/local.py:13) instead of the rg CLI
subprocess + Python fallback design. Rationale:

  - No rg availability probe needed (library handles it)
  - No rg_path config field needed (library bundles rg)
  - No Python fallback needed (library is the unified path)
  - Mirrors the implementation path of astrbot_grep_tool

Spec changes (file structure + §1.2 + §2.1 + §3.4 + §3.5 + §3.6 +
§5.2 + §5.3 reason table + changelog):
  - Delete main.py rg probe (§3.4) — python_ripgrep handles
  - Delete _conf_schema.json search.rg_path field (§3.6)
  - Drop backend field from response (§5.2) — only one backend now
  - SEARCH_UNAVAILABLE reason semantic narrowed to "library missing
    or internal rg call failed" (not "fallback also failed")
  - Other 4 SEARCH_* ReasonCode entries retained unchanged

The 8 task implementation plan still applies with the rg-specific
steps (rg probe, rg --json parsing, Python fallback) replaced by
python_ripgrep.search invocations. Plan document not updated in this
commit — the spec is the authoritative source of design intent, and
the plan's task briefs already cover the new pattern via implementer
prompt context.
Backend refactor collapsed ripgrep and Python-fallback into a single
python_ripgrep path, so the response no longer carries a backend
discriminator. Drops SearchBackend type, the backend field on
SearchState.ok, the 🐢 fallback chip in SearchPanel, and the
fallbackHint i18n key across all 3 locales.
Adds applyInteractiveChoiceResolved dispatcher that writes the request_id into cancelledStates (mirrors the Bug Y1 contract from applyInteractiveChoiceSse for missing umo). Routes interactive_choice_resolved in useMessages.processStreamPayload.
…elled'

Without this filter, the v1.0 success-path reason='submitted' events would also write to cancelledStates, flipping boxes to the cancelled visual after successful submissions. Reviewer flag from F3.
Fifth box state 'cancelled' added per v1.2 spec §4.4: state priority submissionState > cancelledState > isIgnored > pending; new .choice-header--cancelled template branch with mdi-close-circle-outline icon and i18n label; CSS mirrors .is-ignored styling via consolidated selector list.
# Conflicts:
#	README.md
#	README_zh.md
- switch FileBrowserBreadcrumb emit to a { dirPath, previewPath } payload so a typed file path can navigate to its parent dir and select the file
- translate the new payload in DocumentManager and DocumentTreePanel using projectRelativePath / docsRootRelativePath, preserving project-relative state
- guard dirty editor state with a confirm dialog (mirrors FileBrowserView.confirmLeaveEditing) before applying typed-path navigation
- add the corresponding i18n strings in en-US, ru-RU and zh-CN
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Jul 20, 2026

@sourcery-ai sourcery-ai 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.

Sorry, we are unable to review this pull request

The GitHub API does not allow us to fetch diffs exceeding 300 files, and this pull request has 313

@dosubot dosubot Bot added the area:webui The bug / feature is about webui(dashboard) of astrbot. label Jul 20, 2026
# 404 from upstream → 404; everything else (5xx / network / parse) → 502.
status_code = 404 if "没有公告" in message else 502
return JSONResponse(
{"status": "error", "message": message, "data": None},
@elecvoid243
elecvoid243 deleted the feat/recent-files branch July 20, 2026 12:43

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces comprehensive support for subagent orchestration, including a SubAgent Manager, a DAG orchestration engine, and associated tools (e.g., dynamic creation, task waiting, and shared context). It also enhances the local booter with Windows console suppression and host process termination protection, integrates a robust file editing engine with history rollback, and updates the dashboard with project indicators, a plan/build toggle, a draggable todo summary bar, and a CodeMirror editor. Feedback on the changes highlights a duplicate code block in the conversation reset command, a magic number that should be defined as a constant, duplicated subagent status-checking logic that should be refactored, and a regex-based process termination check that should be made more robust using shlex.split().

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +205 to 216
# 清理该会话下的所有 subagent
try:
from astrbot.core.subagent_manager import SubAgentManager

cleanup_result = await SubAgentManager.cleanup_session(umo)
if cleanup_result["status"] == "cleaned":
cleaned_count = len(cleanup_result["cleaned_agents"])
if cleaned_count > 0:
ret += f" 🧹 Also cleaned {cleaned_count} subagent(s): {', '.join(cleanup_result['cleaned_agents'])}."
except Exception as e:
logger.warning(f"[SubAgent] Failed to cleanup subagents on /reset: {e}")

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.

high

This block of code to clean up subagents is a duplicate of the one above (lines 193-203). This causes SubAgentManager.cleanup_session(umo) to be called twice and appends a second cleanup message to the result string, one in Chinese and one in English.

This is likely a copy-paste error and should be removed to avoid redundant operations and confusing output.

if model_info:
max_tokens = model_info["limit"]["context"]
else:
max_tokens = 128000

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.

medium

The value 128000 is a magic number used as a fallback for max_context_tokens. To improve maintainability and readability, it's best to define this as a named constant at the top of the file or in a relevant constants module.

For example:

# At the top of the file
DEFAULT_MAX_CONTEXT_TOKENS = 128_000

# In this function
...
max_tokens = DEFAULT_MAX_CONTEXT_TOKENS

Comment on lines +218 to +275
# Reject delegation while the target subagent is already running to
# keep each subagent strictly single-task at a time.
resolved_agent_name = getattr(handoff_tool.agent, "name", None)
if (
resolved_agent_name
and SubAgentManager.get_subagent_status(
run_context.context.event.unified_msg_origin, resolved_agent_name
)
== SubAgentStatus.RUNNING
):
yield mcp.types.CallToolResult(
content=[
mcp.types.TextContent(
type="text",
text=(
f"Error: SubAgent '{resolved_agent_name}' is currently RUNNING. "
"Wait for it to finish (e.g. via wait_for_subagent) or pick another subagent."
),
)
]
)
return

is_bg = tool_args.pop("background_task", False)
if is_bg:
async for r in cls._execute_handoff_background(
handoff_tool, run_context, **tool_args
):
yield r
else:
async for r in cls._execute_handoff(
handoff_tool, run_context, **tool_args
):
yield r
return

if isinstance(tool, HandoffTool):
# Same single-task guard as above for direct HandoffTool invocations.
direct_agent_name = getattr(tool.agent, "name", None)
if (
direct_agent_name
and SubAgentManager.get_subagent_status(
run_context.context.event.unified_msg_origin, direct_agent_name
)
== SubAgentStatus.RUNNING
):
yield mcp.types.CallToolResult(
content=[
mcp.types.TextContent(
type="text",
text=(
f"Error: SubAgent '{direct_agent_name}' is currently RUNNING. "
"Wait for it to finish (e.g. via wait_for_subagent) or pick another subagent."
),
)
]
)
return

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.

medium

The logic to check if a subagent is currently running is duplicated for transfer_to_subagent (lines 220-239) and for direct HandoffTool invocations (lines 256-275). This redundancy makes the code harder to maintain.

To avoid duplication, you could extract this check into a helper method within the FunctionToolExecutor class. For example:

    @classmethod
    def _check_and_yield_if_subagent_running(cls, run_context, agent_name: str | None):
        if not agent_name:
            return None
        if SubAgentManager.get_subagent_status(
            run_context.context.event.unified_msg_origin, agent_name
        ) == SubAgentStatus.RUNNING:
            return mcp.types.CallToolResult(
                content=[
                    mcp.types.TextContent(
                        type="text",
                        text=(
                            f"Error: SubAgent '{agent_name}' is currently RUNNING. "
                            "Wait for it to finish (e.g. via wait_for_subagent) or pick another subagent."
                        ),
                    )
                ]
            )
        return None

You could then call this helper in both places to simplify the execute method.

References
  1. When implementing similar functionality for different cases (e.g., direct vs. quoted attachments), refactor the logic into a shared helper function to avoid code duplication.

Comment on lines +173 to +179
for match in re.finditer(r"\bkill\b\s+((?:-\w+\s+)*)(\d+)", lowered):
try:
pid = int(match.group(2))
except ValueError:
continue
if pid in protected:
return 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.

medium

The regex used to detect kill commands with numeric PIDs is not robust enough. It can be bypassed with different argument formats, for example kill -s TERM 1234.

For better security, consider using shlex.split() to parse the command and then check for numeric arguments that match protected PIDs. This is more reliable than a regex for shell command parsing.

Example:

import shlex
...
    # In _would_kill_self function
    try:
        parts = shlex.split(lowered)
        if parts and parts[0] == 'kill':
            # This handles PIDs and negative PIDs (process groups)
            pids_in_cmd = [int(p) for p in parts if p.lstrip('-').isdigit()]
            if any(abs(pid) in protected for pid in pids_in_cmd):
                return True
    except ValueError:
        # Ignore commands with syntax errors like unclosed quotes
        pass

This would replace the current regex-based loop and provide stronger protection.

@elecvoid243

Copy link
Copy Markdown
Contributor Author

神必loop事故请无视本PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:webui The bug / feature is about webui(dashboard) of astrbot. size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants