Add mcode-island plugin: Windows Dynamic Island status pill for MiniMax Code agents - #17
Add mcode-island plugin: Windows Dynamic Island status pill for MiniMax Code agents#17antianqi wants to merge 6 commits into
Conversation
…ax Code agents Adds a Skill-first plugin that surfaces the agent working state in a 320x60 WPF pill anchored to the top center of the primary display, so the user can leave the terminal in the background and still watch progress. States: idle / thinking / working / waiting / done / error. Includes wrap-tool.ps1, a thin bash wrapper that pushes working / done / error / waiting based on $LASTEXITCODE, so the user does not have to remember to call notify-island.ps1 for every shell command.
Adds a 1-second-polling daemon that reads the active mcode session messages.jsonl and infers the agent state (idle/thinking/working/done/error) without requiring the agent to call notify-island.ps1. State mapping: role=user -> idle role=assistant + toolCall -> working "<tool>: <args>" role=assistant + thinking -> thinking role=assistant + text -> idle (just replied) role=toolResult + !isError -> done "<tool> 完成" role=toolResult + isError -> error "<tool> 失败" mcode 进程不在 -> error "mcode 进程已退出" 60s 无新事件 -> idle 兑底 Priority logic: agent-pushed states (with Message) are preserved; detector takes over only for settle states (idle / error). Tested on Windows 11 24H2 + PowerShell 5.1 against a live mcode session. All 6 state transitions verified, including mcode exit and recovery.
hetaoBackend
left a comment
There was a problem hiding this comment.
Review result: do not approve / do not merge yet.
The repository check passes (27 tests), but the core Windows detector is not ready:
mcode-status-detect.ps1:79-80hard-codesC:\Users\Administrator\...and scansmessages.jsonl, while the repository runtime uses resolved data directories andledger.jsonl. Ordinary installations therefore reportmcode 已退出instead of detecting state.mcode-status-detect.ps1:117-118,228-243returns no message when the file mtime is unchanged, so the advertised 60-second idle fallback is unreachable during inactivity.start-island.ps1:15-22,stop-island.ps1, and the detector start/stop scripts trust stale PID files and can refuse startup orStop-Process -Forcean unrelated process after PID reuse. Validate executable/command-line identity before acting.wrap-tool.ps1:47-56advertises a bash wrapper but executes-CommandwithInvoke-Expressionas PowerShell code, creating an injection/shell-semantics boundary that should be removed or explicitly documented.- The quick-start commands are invalid PowerShell:
README.md:82-90uses%PLUGIN_DIR%and-Enable, butautostart.ps1:7-10only supports-Action Enable. start-island.ps1:40-44waits forabout to ShowDialog, a log message the widget never emits, so readiness is always reported as waiting.
Please fix the detector data-path/session contract, idle logic, PID validation, and launch/docs issues before requesting another review.
Fixes for review comments from hetaoBackend (commit fce7c5f): MiniMax-AI#1 detector hard-coded path: resolve the [userprofile]/.minimax-code directory at runtime via the mcode node process cmdline (regex on @minimax-ai/code/cli.js), with fallbacks to $env:USERPROFILE/.minimax-code, $env:APPDATA/minimax-code, and the current working directory. Override with -Root [path]. MiniMax-AI#2 idle fallback unreachable: mtime cache now returns the last inferred message instead of null, so the 60s stale -> idle branch fires every poll. Verified locally: idle :: already idle 195s after 65s of inactivity. #2b session log: prefer ledger.jsonl (mcode v2 event stream) and fall back to messages.jsonl when ledger is missing. Both formats are handled in Infer-State (kind/phase for ledger, message.role for messages). MiniMax-AI#3 PID reuse safety: start/stop-{island,detect-island}.ps1 now verify the target PID command line contains the expected script path before acting. Stale PIDs and PID-reused processes are refused with a REFUSED log line instead of being killed. MiniMax-AI#4 wrap-tool.ps1 shell-injection: removed Invoke-Expression entirely. The wrapper is now status-only; the agent runs the command via mcode's own bash tool and passes -ExitCode to publish the outcome. Documented in README + SKILL.md. MiniMax-AI#5 README: -Enable -> -Action Enable to match autostart.ps1 parameter set. MiniMax-AI#6 start-island.ps1 readiness: dropped the 'about to ShowDialog' log wait (which was never emitted). Now polls MainWindowHandle != 0 every 500ms for up to 8s. Tests: validator reports OK plugin antianqi/mcode-island. wrap-tool 6-state matrix verified locally (working / done / waiting / error).
|
Thanks for the review. Pushed Code fixes
Local verification
Not in this push (out of scope of the review)
Ready for another pass. |
This standalone mirror is now in lockstep with the in-flight PR #17 (MiniMax-AI/MiniMax-Code-Plugins#17), commit bad0868. Changes since v0.1.0: + mcode-status-detect.ps1 v0.2 detector daemon + start-detect-island.ps1 + stop-detect-island.ps1 + status-detect-island.ps1 M README.md detector + wrap-tool new API + -Action Enable M mcode-island.cmd detect-on / detect-off / detect-status subcommands M skills/mcode-island/SKILL.md detector + new wrap-tool two-step pattern M start-island.ps1 PID + cmdline check; readiness via MainWindowHandle M stop-island.ps1 PID + cmdline check (refuse on PID reuse) M wrap-tool.ps1 removed Invoke-Expression; status-only; -ExitCode arg Detector resolves the mcode install root at startup by regexing the mcode node process command line (matched on @minimax-ai/code/cli.js), with fallbacks to $env:USERPROFILE/.minimax-code, $env:APPDATA/minimax-code, and the current working directory. Validator: OK plugin antianqi/mcode-island (same as PR #17 head).
… messages)
Get-LatestSessionFile always preferred ledger.jsonl when present, regardless
of which file was more recently written. On systems where mcode v0.2.x left
behind a stale ledger.jsonl from a previous session, the detector would
read the old ledger every poll, the 60s idle-fallback would fire against
an ancient mtime, and the widget would stay stuck on "已静默 NNNNNs"
forever (verified: 49549s = 13.76h against a ledger that was actually
{"action":"test ledger 1"} test residue).
Fix: compare mtimes and pick whichever is newer. Fall back to ledger if
messages is absent (original fallback contract), but never let a stale
ledger shadow a live messages.jsonl.
Triggered by PR MiniMax-AI#17 review testing: 9 hours of "idle :: 已静默 49549s"
on a fresh detector after the v0.2.1 fixes were deployed.
|
One more fix on top of the v0.2.1 review fixes (commit Bug:
Verified in real use: the v0.2.1 detector was running for 9 hours showing exactly that — Fix: compare - # 优先 ledger.jsonl 最新的;如果同 session 有 ledger 就用 ledger
- $ledger = $all | Where-Object { $_.Name -eq $FNAME_LEDGER } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
- if ($ledger) { return $ledger }
- $msgs = $all | Where-Object { $_.Name -eq $FNAME_MESSAGES } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
- return $msgs
+ # mcode v0.2.x writes messages.jsonl live; ledger.jsonl is best-effort and may
+ # be left behind by an old session. Always pick whichever file is most
+ # recently touched, otherwise a stale ledger would dominate and the
+ # 60s-idle fallback would fire against ancient timestamps.
+ $ledger = $all | Where-Object { $_.Name -eq $FNAME_LEDGER } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
+ $msgs = $all | Where-Object { $_.Name -eq $FNAME_MESSAGES } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
+ if ($ledger -and $msgs) {
+ if ($ledger.LastWriteTime -ge $msgs.LastWriteTime) { return $ledger }
+ return $msgs
+ }
+ if ($ledger) { return $ledger }
+ if ($msgs) { return $msgs }
+ return $nullVerified live: after restart the detector now flips through Pushed as |
notify-island.ps1 was writing status.json with only {state, message,
progress, ts} and no source field. The detector's takeover logic keys
off `cur.source -eq 'detector'` to decide whether the live entry is its
own or an externally-pushed one. With no source field on agent-pushed
states, the detector treated every agent push as "no current status" and
immediately overwrote it with whatever it had just inferred — most often
idle (60s fallback), even when the agent had just pushed `working` or
`thinking`.
Concretely: pushing `notify-island.ps1 -State working` would survive for
roughly 1 second before the detector's next poll clobbered it back to
idle. This made the manual notify tool useless for any state the detector
cares about, and made the `wrap-tool.ps1 -State working` wrap pattern
invisible on the pill.
Fix: add `source = 'agent'` to the payload. With it set, the detector's
existing precedence rules work as documented:
- agent push of working/thinking/done → preserved (not overwritten by
the same-state detector inference, since detector-inferred
working/thinking/done is not "settled" and does not trigger the
takeover branch when the current entry is not the detector's own);
- agent push of idle/error → can be taken over by detector's
idle/error inference, matching the original "detector settles agent"
contract.
Verified live: `notify-island.ps1 -State thinking` now persists across
multiple detector polls (ts unchanged after 3.5s, message intact,
source field present).
Pushed on top of 6e99c0b on add-mcode-island.
|
One more on top of Bug: Concretely: Fix: add
Verified live: $payload = [PSCustomObject]@{
state = $State
message = $Message
progress = $Progress
ts = $ts
+ source = 'agent'
} | ConvertTo-Json -CompressSo on top of the v0.2.1 review fixes there are now two late commits:
|
The detector polled once per second, and every poll walked ~15 pipeline
cmdlets: Get-ChildItem -Recurse | Where-Object | Sort-Object |
Select-Object (×2), Get-Content -Raw | ConvertFrom-Json (×3-4),
$collection | Where-Object (×3), Get-Process (×1-2), etc. PS 5.1 hidden
window has a known issue where completed pipeline tasks aren't
immediately released back to the Runspace thread pool — the pool backs
up over multi-hour runs. After ~9 hours of polling, the process was
holding ~30k threads and Get-ChildItem was effectively starved:
status.json stopped updating, island.log stopped appending, the
process looked alive but the loop was no longer advancing. Only a
restart recovered it.
Fix in three layers:
1. Replace the most expensive pipeline calls with direct .NET method
calls so no Runspace hop is incurred:
- Get-LatestSessionFile: Get-ChildItem -Recurse | Where-Object |
Sort-Object | Select-Object → a single
[System.IO.Directory]::EnumerateFiles + manual mtime scan
- Get-McodePid: Get-ChildItem | foreach { Get-Content |
ConvertFrom-Json | Get-Process } → EnumerateFiles + File.ReadAllText
+ Process.GetProcessById
- Read-LastMessage: Get-Item → [System.IO.FileInfo]::new(...)
- Read-StatusObj: Get-Content -Raw → File.ReadAllText
- Infer-State (assistant branch): $m.content | Where-Object ×3 →
one foreach loop with early exit (toolCall wins, no need to scan
the rest)
2. Add a 5s TTL cache for both `mcodePid` and `latestSessionFilePath`
in the main loop. mcode doesn't churn sub-second, and a fresh
session log only shows up when mcode itself starts a new session,
which is also a sub-5s event in practice. 5s is a comfortable
upper bound that cuts the heavy directory enumeration to once per
5s without losing visible state fidelity (the existing mtime gate
in Read-LastMessage already gates re-parse on real content
changes, so cache staleness is invisible to the user).
3. Verified live: after the fix, restarting the detector and running
for 30s reports 18-28 threads (was previously climbing into the
thousands within minutes). State transitions (working → done →
working) still fire correctly. The 60s-idle fallback still fires
correctly.
Side benefit: the refactor also fixes a tiny correctness wart in
Get-McodePid — when multiple .json files happen to coexist in
.mcode-active (e.g. during a restart overlap), the previous code
returned the first hit; the new code picks the most-recently-touched
one, which matches what Get-LatestSessionFile does on the messages
side.
Pushed on top of db73c11 on add-mcode-island.
|
One more on top of Bug: the detector polled once per second and every poll walked ~15 pipeline cmdlets: (The user-visible symptom masked as "9 hours of Fix in three layers:
Side benefit: the refactor also fixes a tiny correctness wart in - $candidates = Get-ChildItem -Path $mcodeActiveDir -Filter '*.json' -ErrorAction SilentlyContinue
- foreach ($f in $candidates) {
- $j = Get-Content $f.FullName -Raw | ConvertFrom-Json
- $targetPid = [int]$j.pid
- $proc = Get-Process -Id $targetPid -ErrorAction SilentlyContinue
+ foreach ($f in [System.IO.Directory]::EnumerateFiles($mcodeActiveDir, '*.json')) {
+ $raw = [System.IO.File]::ReadAllText($f)
+ $j = $raw | ConvertFrom-Json
+ $targetPid = [int]$j.pid
+ $proc = [System.Diagnostics.Process]::GetProcessById($targetPid)So on top of the v0.2.1 review fixes there are now three late commits:
|
mcode-island — Windows Dynamic Island for MiniMax Code agents
A Skill-first Plugin that surfaces the agent's working state in a small WPF pill
anchored to the top center of the primary display, so the user can leave the
terminal in the background and still see exactly what the agent is doing.
The agent's only contract with the widget is: write JSON to
%APPDATA%\mcode-island\status.json(or call thenotify-island.ps1helperthat does that for you). The widget polls that file every 400 ms.
The problem this solves
While the agent runs a long tool call (compile, install, test, refactor), the
user often switches away from the terminal to read code, check docs, or browse
the web. There is no visible progress signal. The agent may also be paused on
a permission prompt, or have failed silently.
mcode-islandmakes all ofthat visible at a glance, without forcing the user to switch back.
Copyable example
In the agent loop, wrap every
bashcall through the bundled wrapper:State flow this triggers automatically:
working — bash: run tests(pushed before the command runs)done — bash 完成waiting — bash 等待审批 (exit=1)error — bash 失败 (exit=N)For other tools (
read/write/edit), the agent pushes state directlyvia
notify-island.ps1before and after each tool call. The Skill body inskills/mcode-island/SKILL.mddocuments the exact timing.Expected result
After each push the widget on the user's primary display updates within
~400 ms (one polling cycle). On click, the originating terminal tab regains
focus. The widget is intentionally hard to kill:
Alt+F4hides it (notcloses), and
mcode-island showre-raises the hidden window in under onesecond.
Requirements
user32/kernel32)Bypassfor this directory; not changed globallyThe plugin contains no
node_modules, no native binaries, no symlinks, noinstallers, no private endpoints, no telemetry.
Network and data behavior
%APPDATA%\mcode-island\:status.json,caller.json,config.json,widget.pid,island.log,widget.log,show.signal.HKCU\Software\Microsoft\Windows\CurrentVersion\Runfor logon auto-start (opt-in, user runs
autostart.ps1 -Enable).Test evidence
This plugin was exercised on Windows 11 24H2 with PowerShell 5.1 against a
live MiniMax Code session. Concrete observations captured during development:
island.logover a multi-hour session(21
working/ 14done/ 11idle/ 6waiting/ 5thinking/ 1error).assets/state-*.png).wrap-demo.png:thinking→working→waiting→working→doneon a realbash npm testrun.focus jumps to the originating Windows Terminal tab (HWND consistent).
wrap-tool.ps1exit-code semantics: 0 →done, 1 →waiting(default,configurable via
-WaitingExitCodes), other →error.npm run checkresultValidator output for the hosted plugin directory:
The 8 unrelated
FAILlines innpm run checkare pre-existing onupstream/main(other contributors' hosted plugins missing YAMLfrontmatter); this PR does not touch them. The single
npm testfailure(
hosted-plugins.test.mjs:39) is a Windows-only path-separator mismatchin the upstream test (
plugins\alice\hello-worldvs/plugins\/alice\/hello-world/)and is unrelated to this PR.
Package contents
Limitations
wrap-tool.ps1only wrapsbash; forread/write/editthe agentcalls
notify-island.ps1directly.Tauri rewrite).
License
Apache-2.0.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.