feat(web-shell): expose Agent management in sidebar - #11140
Draft
yiliang114 wants to merge 1 commit into
Draft
Conversation
Collaborator
Author
E2E test reportResult: PASS on macOS with the built bundle. BaselineStarted the global Qwen Code Built-branch verification
Automated regression coverage
|
This was referenced Sep 6, 2026
yiliang114
added a commit
that referenced
this pull request
Sep 7, 2026
registerMeshRoutes is mounted beside the goals surface, so the daemon actually answers what the pages ask for. ThreadsRoute owns the loading. Its one non-obvious rule is that every preview response is stamped with the draft it answered and discarded if the draft has moved on: the whole value of previewing routing is that it describes the post about to be sent, so a slow answer to an older draft overwriting the current one would make it lie at exactly the moment someone relies on it. The preview is debounced on a settled draft rather than fired per keypress. An unreachable daemon says what failed and what to do about it rather than rendering an empty list that looks like a workspace with no threads. Still not mounted into App.tsx's panel switch: the way to open the page is the absorbed sidebar entry from #11140, and adding a second one here would collide with it.
yiliang114
added a commit
that referenced
this pull request
Sep 7, 2026
* feat(mesh): add per-turn run binding and the turn prompt envelope Step 5a. The ambient binding is an AsyncLocalStorage frame established per turn, not per lifetime: a mesh body works many threads in sequence, so a frame wrapped around the launch would pin every later turn to the first thread. Nesting a different run throws rather than shadowing, because that can only mean the frame was established at the wrong level. The prompt envelope restates thread identity, title, body, status and a bounded recent window on every turn, because auto-compaction or a transcript-backed cold revive may have removed the previous frame. A delta is additional context after the agent's committed watermark, never the sole context; retention loss and a replayed delivery are labelled rather than left for the model to infer. Post text is indented past column zero so author-controlled content cannot forge a section header — that bounds structure spoofing only, not the instructions inside a post (§9.1). resolveTargets' third parameter becomes required: defaulting it to message.mentions.length > 0 re-encoded the unknown-mention fallback that the admission foundation fixed, since an unknown @name resolves to no id yet must still suppress the assignee. * feat(mesh): derive thread status from every run's close obligation Several agents work one thread. If each stamped the status when its own run ended, the last to finish would decide: an agent reviewing its part would hide another still working, and a blocker raised by one would be erased by another's clean exit. No run writes the status now. Each leaves a durable close obligation and the status is recomputed from the ones outstanding, so no ordering of concurrent completions can leave a stale state behind. Three rules exist because a review found each missing, and each failure left a thread nobody could clear. A same-thread wait is discharged by any later close, so 'A waits for B, B reviews without @-ing A' reports in_review rather than blocked. Any later successful booking discharges an earlier failure or unclosed return, so one launch failure no longer pins the thread after another agent did the work. A quiescent thread whose last admission booked nothing becomes blocked instead of sitting in in_progress with no live run and no explanation. Whether a human reply should discharge a blocker raised by an agent it did not address is 9.11 and stays open; until it is decided the selector discharges everything, and narrowing it is a change to that predicate rather than to its callers. * feat(mesh): record run closes and derive the thread state from them Closing is two writes because a closing tool is called mid-turn and cannot mark its own still-executing runtime finished. The tool records what the run is closing as and moves it to finishing, which ends the turn; the runtime callback records the terminal state, and only there is the thread's status recomputed. A crash between the two leaves a finishing run with a closeKind, which is a complete instruction for restart reconciliation — a status written before the runtime actually stopped would be a lie the next reader cannot detect. A wait is refused when nothing could wake it, and the dependency is walked over parentThreadId rather than rootThreadId so a sibling sub-thread does not count as this thread's delegation. Any close discharges peers' waits on the same thread. A clean exit that never called a closing tool is recorded as unclosed rather than as implicit success. finishRun now delegates here so a run has exactly one way to end, and postMessage discharges outstanding obligations when it books work before applying the aggregate status. Without those two producers the resolver's rules had no writer: an obsolete failure kept the thread blocked after later work succeeded, and a post that booked nothing left it in in_progress with no live run and no explanation. * refactor(mesh): carry system-trigger provenance on posts, drop dead exports A structured assignment or parent report is system-authored but must keep the run or human action that caused it, so it is charged as unattended work without being suppressed as an ordinary self-authored post. PostMessageInput now carries authorKind, sourceRunId and triggerKind; all three are derived by the server and none is accepted from a model. Removes three exports from run-lifecycle that had no reader: the booking acknowledger duplicated what postMessage already calls directly, and the live run listing and re-exported author id were never read. * feat(mesh): add the six thread tools, trusting only ambient identity No mutating tool accepts a thread, author, run, or idempotency id from the model. A mesh agent is one long-lived body working many threads in sequence, so an id in a tool argument is a value the model reconstructs from memory that may have been compacted, or copied from another thread's frame. Multica hit the same class of bug with resumed sessions carrying a previous turn's parent id and fixed it server-side rather than trusting the argument. Here identity comes from the ambient run frame, and every mutating call re-reads the store to confirm that frame still names a running run of that agent on that thread — the frame says what the dispatcher intended, the store says what is still true, and they diverge after a cancellation or a replay. thread_read is the one tool that takes an id, because it only reads; what it returns is still other participants' text rather than instructions. thread_create builds the sub-thread and its assignment trigger in one transaction. Two would leave a crash window in which an assigned sub-thread exists with nothing scheduled to work it. The trigger goes through ordinary admission, so assigning cannot bypass budgets, the queue limit, or the outcome model. * feat(mesh): add the minimal dispatcher that turns booked work into runs Admission decides whether a run exists; this decides when it starts and on which body. The split matters because a booked run is a durable fact that stays true until something changes it under the lock, while 'this agent is free right now' expires the moment it is read. So capacity backpressure and a runtime that reports the agent busy both leave the run queued with its attempt unspent, and the next pass re-reads them rather than persisting a decision that was already stale when written. Selection is by the lock-issued queueSequence, never queuedAt and never file order: posts arrive from processes whose wall clocks can disagree, and directory order would starve one thread behind another purely because of how its id sorts. The four runtime entry points stay four operations rather than one with a flag, because a resident continuation reuses the live chat, a resume restarts a paused entry, a revive rebuilds from the transcript, and a launch builds the persona from scratch. A failed or unavailable start is terminal with a typed failureStage and releases the queue slot; leaving it queued would make one broken agent definition look like an agent that is merely slow. Parent reports drain with the event id as the idempotency key. Outbox reconciliation now takes a filter so an event kind whose consumer does not exist yet stays visibly pending instead of being acknowledged into silence. * feat(mesh): bind the dispatcher to the background-agent runtime Writing this adapter corrected the dispatcher's idle path from four branches to three. Whether a completed body still has a resident runtime is not a choice the dispatcher can make: only the registry knows, and it already reports its own fallback as a typed outcome. A dispatcher picking between 'continue resident' and 'cold revive' would be guessing at state it cannot see, and would cold-revive a body that was still live. The three entry points it does choose between are genuinely distinct — build the persona, restart a paused entry the revive path would reject, or continue a completed one. Capacity is reported before any mutation, so a saturated registry costs a run nothing; an entry that changed state under the adapter is left for the next pass rather than forced; and a thrown runtime error becomes a typed failure instead of a start nobody performed. This is the single place that knows how a Qwen Code body is started, which is also where a non-local runtime would be substituted. * docs(plans): settle the Web Shell design direction before step 9 starts Thread status here is an aggregate over outstanding obligations, not a state somebody set. The obvious UI — a chat log with a status badge — would hide the one thing this system knows that Slack or Linear do not: who owes what, and what the thread is waiting on. So the thread view is a ledger of obligations with the conversation as evidence underneath it: the header is the resolver's own reason sentence, each agent that worked the thread gets a lane, and system triggers read as ledger entries rather than as someone talking. Inherits Web Shell's existing tokens and adds no colour or typeface. A downloaded display face would cost startup, break offline use, and clash with every neighbouring panel in a local-first tool. Monospace appears only where characters must align in a column or be copied exactly. blocked and in_review deliberately share one attention treatment. They are opposite in valence but they are the same query for the reader — this is waiting on me — and two colours would split that scan in two. They are told apart by the sentence, not the hue. * docs(plans): ground the Web Shell design in Multica's shipped UI Read against multica@7a438bd5b rather than imagined. Four of their decisions are adopted outright: runs live in a side panel with active pinned and past collapsed, and the row carries no availability dot because the run's status is the story; the live working signal sits in the header, not a body card that competes with content and scrolls away; a list beats a minimap rail for finding; and the composer previews routing before sending, lit for will-trigger and dimmed for suppressed, with unknown mentions named rather than failing silently after send. That last one transfers exactly because decideDispatch is pure — the composer can run the real rules on a draft. What Multica cannot give us is the aggregate. Its issue status is set by a person and its runs are per-(agent, issue), so it never has to answer 'one agent submitted a summary while another is still working'. Ours does, and three round-two defects were threads stuck in a state nobody could explain. So the design budget goes there: the header is the resolver's own reason sentence, and every run row carries its close obligation where Multica carries a task status. Also borrowed: a label must not assert a cause the reason code does not carry. They keep runtime_offline, agent_runtime_required and runtime_unusable apart because the fix differs, and conflating them sent people to reconnect a machine that was already connected. Our nine skip reasons get the same treatment, each naming its own fix, shared between the composer preview and the post-send result. * fix(mesh): stop teaching the internal codename to the model 'mesh' is this subsystem's module name, inherited from the branch it landed on. It had leaked into two places an agent actually reads: the run envelope's header and the shell refusal reasons. The design's own naming rule is that users and models see agents and threads; the codename stays in file paths. The envelope header now says what the block is for — this part is authenticated by the runtime, the rest is not — rather than naming a subsystem the reader has no way to know about. * feat(mesh): serve the threads surface and build its two pages Three properties live on the server because a browser cannot hold them. The thread's state is resolved on read and travels with the sentence explaining it, so the client never derives a second status word that would drift from the resolver and win by being the one on screen. The composer's routing preview runs the real parseMentions and decideDispatch — the same pure functions admission runs — and writes nothing, so looking at the consequences of a reply does not cost what accepting them costs. A post's author comes from the authenticated surface; there is no body field that can claim to be an agent. The UI follows Multica's shipped shape where it earned it: runs in a side panel with live pinned and past collapsed behind a count, the live signal in the header rather than a body card that scrolls away, and the routing preview before sending rather than a surprise after. Where Multica has nothing to copy is the aggregate — its issue status is set by a person — so that is where this design spends: the header is the resolver's own sentence and every run row carries its close obligation. The thread list groups by what each thread needs rather than by recency, which buries the two that need a person under twenty that do not. blocked and in_review share one attention treatment because they are the same reader query; the sentence tells them apart. Each refusal names its own fix, and reasons whose fixes differ are never merged into one label. * feat(mesh): bind each turn to its thread, and register the thread tools A shared-thread body works many threads in sequence, so the turn seam has to be told which one this turn is for. AgentMeta carries the binding; the dispatcher rewrites it immediately before each non-launch start, and on the launch path it travels with the launch options into the first record, because a turn that began before a patch would have run with no frame at all. Both real seams re-read it once, synchronously, and open the AsyncLocalStorage frame around the turn body. Re-reading per turn is the whole point: a frame opened around the lifetime would pin the body to whichever thread it started on, and a process-global variable would leak across concurrent turns. The six thread tools are registered process-wide so there is one place that knows a tool exists, and hidden from every agent not on a thread. They already refuse without a frame, but a refusal the model can only discover by calling costs a turn and teaches a schema that lies. * feat(mesh): serve the threads routes and load them from one container registerMeshRoutes is mounted beside the goals surface, so the daemon actually answers what the pages ask for. ThreadsRoute owns the loading. Its one non-obvious rule is that every preview response is stamped with the draft it answered and discarded if the draft has moved on: the whole value of previewing routing is that it describes the post about to be sent, so a slow answer to an older draft overwriting the current one would make it lie at exactly the moment someone relies on it. The preview is debounced on a settled draft rather than fired per keypress. An unreachable daemon says what failed and what to do about it rather than rendering an empty list that looks like a workspace with no threads. Still not mounted into App.tsx's panel switch: the way to open the page is the absorbed sidebar entry from #11140, and adding a second one here would collide with it. * Revert "feat(mesh): serve the threads routes and load them from one container" This reverts commit ff54de8. * Revert "feat(mesh): bind each turn to its thread, and register the thread tools" This reverts commit c608c77. * Revert "feat(mesh): serve the threads surface and build its two pages" This reverts commit d95f411. * fix(mesh): claim runs before agent startup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
This stacked PR adds a first-class Agents entry to the WebShell primary sidebar. The entry opens the existing Agent definition manager instead of introducing another management surface. The manager now also explains that Qwen can coordinate specialized Agents in an Agent Team.
Why it's needed
WebShell already supports creating and managing real Agent definitions, but the manager is only discoverable through
/agentsor a nested Plugins tab. A visible workspace-level entry makes the capability understandable without implying durable Team Run management that the daemon does not yet provide.Reviewer Test Plan
How to verify
Start WebShell for a workspace and confirm that Agents appears between New Task and Plugins. Select it and verify that the existing Agent manager loads, lists Agent definitions, shows the Agent Team collaboration explanation, and opens the existing creation form. Disable workspace project features or omit
agentsfrom the configured primary navigation and confirm that the entry is hidden.Evidence (Before & After)
Before: the primary sidebar exposed New Task, Plugins, Channels, Scheduled Tasks, and Goals; Agent management required
/agentsor Plugins > Agents.After: the primary sidebar exposes Agents, selecting it opens the existing manager, and Create opens the established Agent editor with Basic Information, System Prompt, Tools, MCP, and Hooks sections.
Tested on
Environment (optional)
Built and bundled locally, then started with
node dist/cli.js servein an isolated tmux session and exercised through Chrome. Full WebShell App tests passed (746/746), full sidebar tests passed (119/119), and build, typecheck, bundle, Prettier, and focused ESLint passed.Risk & Scope
Linked Issues
Depends on #11072. This PR is intentionally based on
codex/agent-team-roster-web-shell; merge #11072 first, then retarget this PR tomainif GitHub does not do so automatically.中文说明
本 PR 做了什么
这个堆叠 PR 在 WebShell 一级侧栏中增加了“智能体”入口。它直接打开现有智能体定义管理器,没有再造一个管理界面。管理页还增加了一句说明,让用户明确知道 Qwen 可以通过 Agent Team 协调专业智能体。
为什么需要
WebShell 已经支持创建和管理真实的智能体定义,但此前只能通过
/agents或“插件”中的二级标签发现。工作空间级的可见入口能让用户直接理解这项能力,同时不会暗示 Daemon 已经提供尚不存在的持久化 Team Run 管理。Reviewer 验证计划
如何验证
为一个工作空间启动 WebShell,确认“智能体”显示在“新建任务”和“插件”之间。点击后确认现有智能体管理器能够加载和列出定义,显示 Agent Team 协作说明,并能打开现有创建表单。关闭工作空间项目能力,或从一级导航配置中省略
agents,确认该入口会隐藏。前后对比证据
之前:一级侧栏只显示新建任务、插件、频道、定时任务和目标;智能体管理需要使用
/agents或进入“插件 > 智能体”。之后:一级侧栏直接显示“智能体”;点击后打开现有管理器,“创建”会进入已有的智能体编辑器,其中包含基本信息、系统提示词、工具、MCP 和 Hooks。
测试平台
环境(可选)
本地完成 build 和 bundle 后,在隔离的 tmux 会话中使用
node dist/cli.js serve启动,并通过 Chrome 操作。WebShell App 全量测试 746/746、侧栏全量测试 119/119 通过;build、typecheck、bundle、Prettier 和定向 ESLint 均通过。风险与范围
关联事项
依赖 #11072。本 PR 有意基于
codex/agent-team-roster-web-shell;请先合并 #11072,之后如果 GitHub 没有自动调整 base,再将本 PR 改为main。