feat(dashboard): recent files list in Files view#9331
Conversation
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.
…apshot whenever it loads successfully.
# 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
| # 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}, |
There was a problem hiding this comment.
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.
| # 清理该会话下的所有 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}") | ||
|
|
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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| # 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 |
There was a problem hiding this comment.
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 NoneYou could then call this helper in both places to simplify the execute method.
References
- 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.
| 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 |
There was a problem hiding this comment.
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
passThis would replace the current regex-based loop and provide stronger protection.
|
神必loop事故请无视本PR |
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.