diff --git a/plugins/antianqi/mcode-island/LICENSE b/plugins/antianqi/mcode-island/LICENSE
new file mode 100644
index 0000000..c26546e
--- /dev/null
+++ b/plugins/antianqi/mcode-island/LICENSE
@@ -0,0 +1,190 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for describing the origin of the Work and
+ reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Support. While redistributing the Work or
+ Derivative Works thereof, You may choose to offer, and charge a
+ fee for, acceptance of support, warranty, indemnity, or other
+ liability obligations and/or rights consistent with this License.
+ However, in accepting such obligations, You may act only on Your
+ own behalf and on Your sole responsibility, not on behalf of any
+ other Contributor, and only if You agree to indemnify, defend,
+ and hold each Contributor harmless for any liability incurred by,
+ or claims asserted against, such Contributor by reason of your
+ accepting any such warranty or support.
+
+ END OF TERMS AND CONDITIONS
+
+ Copyright 2026 antianqi
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/plugins/antianqi/mcode-island/README.md b/plugins/antianqi/mcode-island/README.md
new file mode 100644
index 0000000..3b9705c
--- /dev/null
+++ b/plugins/antianqi/mcode-island/README.md
@@ -0,0 +1,240 @@
+# mcode-island
+
+A Windows desktop **Dynamic Island** for MiniMax Code agents. A small pill anchored to the
+top center of your primary display mirrors the agent's working state in real time
+(`idle` / `thinking` / `working` / `waiting` / `done` / `error`). You can leave
+the terminal in the background and still see exactly what your agent is doing.
+
+## Demo
+
+| idle | thinking | working | waiting | done | error |
+|:----:|:--------:|:-------:|:-------:|:----:|:-----:|
+|  |  |  |  |  |  |
+
+The pill is a single 320×60 WPF window, always-on-top, click-to-focus. The
+widget polls `%APPDATA%\mcode-island\status.json` every 400 ms; the agent (or a
+thin wrapper) just writes JSON to that file.
+
+## What problem this solves
+
+While the agent works on a long task (compile, install, test, refactor) the user
+often switches away from the terminal to read code, check docs, browse the web.
+There is no visible progress signal. The agent may also be paused on a
+permission prompt or have failed silently. `mcode-island` makes all of that
+visible at a glance, without forcing the user to switch back.
+
+## Copyable example
+
+### One-line install and run
+
+```cmd
+:: 1. install the plugin into your mcode plugins directory
+:: (the location printed by `mcode config` for `plugin_dir`)
+set PLUGIN_DIR=%USERPROFILE%\.minimax\plugins
+if not exist "%PLUGIN_DIR%\mcode-island" mkdir "%PLUGIN_DIR%\mcode-island"
+xcopy /E /I "%~dp0" "%PLUGIN_DIR%\mcode-island"
+
+:: 2. start the widget
+"%PLUGIN_DIR%\mcode-island\mcode-island.cmd" start
+```
+
+### From a MiniMax Code session
+
+Ask the agent to read a large file. The agent's `read` invocation triggers
+two state pushes — `working` while the tool runs, `done` when it returns:
+
+```text
+> read C:\path\to\very-large-file.cs and summarize the public API
+
+[widget pulses blue: ⚙ working — read: very-large-file.cs]
+[widget goes green: ✓ done — read: very-large-file.cs]
+```
+
+If the file does not exist:
+
+```text
+> read C:\path\to\missing.cs
+
+[widget pulses blue: ⚙ working — read: missing.cs]
+[widget goes red: ✕ error — read: missing.cs]
+```
+
+### Wrap every bash call (recommended for power users)
+
+The shipped `wrap-tool.ps1` is a **status-only wrapper**: it never executes the
+command itself (v0.2.1 removed the prior `Invoke-Expression` path to avoid
+shell-injection ambiguity). The agent runs the command via mcode's own bash
+tool, then calls `wrap-tool.ps1` to publish the state. Two-step pattern:
+
+```powershell
+# 1. Push "working" before the command
+& "%PLUGIN_DIR%\mcode-island\wrap-tool.ps1" `
+ -Tool bash -Command "npm test" -Description "run tests"
+
+# 2. After mcode's bash tool returns, push the outcome
+& "%PLUGIN_DIR%\mcode-island\wrap-tool.ps1" `
+ -Tool bash -Command "npm test" -ExitCode $LASTEXITCODE
+```
+
+`$LASTEXITCODE` is interpreted as: `0` → `done`, codes in `-WaitingExitCodes`
+(default `[1]`) → `waiting`, anything else → `error`. The wrapper returns the
+exit code unchanged so the calling shell still sees it.
+
+If you do not need a custom message, `notify-island.ps1` is a leaner direct
+alternative:
+
+```powershell
+& "%PLUGIN_DIR%\mcode-island\notify-island.ps1" -State working -Message "bash : npm test"
+& "%PLUGIN_DIR%\mcode-island\notify-island.ps1" -State done -Message "npm test passed"
+& "%PLUGIN_DIR%\mcode-island\notify-island.ps1" -State error -Message "npm test failed"
+```
+
+## Quick start
+
+1. **Install** — copy this folder into your `~/.minimax/plugins/mcode-island/`
+ (or any directory you want; the scripts only need to live together).
+2. **Start the widget**:
+ ```cmd
+ mcode-island.cmd start
+ ```
+ You should see a small dark pill appear at the top center of the screen.
+3. **Test a state push** from a new terminal:
+ ```powershell
+ & "%PLUGIN_DIR%\mcode-island\notify-island.ps1" -State working -Message "demo"
+ ```
+ The pill should turn blue and pulse for as long as you don't push another state.
+4. **Enable logon auto-start** (optional):
+ ```powershell
+ & "%PLUGIN_DIR%\mcode-island\autostart.ps1" -Action Enable
+ ```
+ This writes to `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`. No
+ admin rights required.
+5. **Stop when done**:
+ ```cmd
+ mcode-island.cmd stop
+ ```
+
+## What is in this package
+
+```
+mcode-island/
+├── plugin.json # plugin manifest (official 1.0 schema)
+├── README.md # this file
+├── LICENSE # Apache-2.0
+├── mcode-island.ps1 # WPF widget main loop
+├── mcode-island.cmd # CLI shim: start/stop/status/show/pin/...
+├── start-island.ps1 # launcher (forces STA + hidden console)
+├── stop-island.ps1 # stop the widget
+├── status-island.ps1 # print widget PID + recent log
+├── show-island.ps1 # re-raise hidden widget
+├── pin-island.ps1 # lock click-to-focus target
+├── autostart.ps1 # register / unregister Windows logon
+├── notify-island.ps1 # state-push helper (agents call this)
+├── wrap-tool.ps1 # all-in-one bash wrapper
+├── skills/mcode-island/SKILL.md # Skill consumed by the agent
+└── assets/ # screenshots embedded above
+```
+
+The whole package is a single portable directory. No installer, no native
+binary, no symlink, no `node_modules`.
+
+## Requirements
+
+| requirement | version / note |
+| ----------------- | ----------------------------------------------------- |
+| Windows | 10 1809+ or 11 (uses WPF, `user32` `kernel32`) |
+| PowerShell | 5.1 (ships with Windows 10/11) or PowerShell 7 |
+| .NET WPF runtime | 4.x (ships with Windows 10/11) |
+| execution policy | `Bypass` for this directory; not changed globally |
+| network access | **none** — widget does not make any network request |
+| accounts | **none** |
+| paid services | **none** |
+
+## Data use
+
+| data | location | lifetime | purpose |
+| ---------------- | ------------------------------------- | ------------------------- | ------------------------------------ |
+| `status.json` | `%APPDATA%\mcode-island\` | rewritten every transition | widget polling |
+| `caller.json` | `%APPDATA%\mcode-island\` | rewritten every transition | click-to-focus target HWND |
+| `config.json` | `%APPDATA%\mcode-island\` | rewritten on drag | pill position, size, opacity |
+| `widget.pid` | `%APPDATA%\mcode-island\` | rewritten on start | widget process PID |
+| `island.log` | `%APPDATA%\mcode-island\` | append-only, never pruned | state transition history |
+| `widget.log` | `%APPDATA%\mcode-island\` | append-only, never pruned | widget internal debug |
+| `show.signal` | `%APPDATA%\mcode-island\` | transient | "raise hidden window" signal |
+| `HKCU\...\Run` | Windows registry | until disabled | logon auto-start |
+
+**No data leaves the local machine. No telemetry. No network requests.**
+
+## CLI reference
+
+```cmd
+mcode-island REM start the widget (no-op if already running)
+mcode-island stop REM stop the widget
+mcode-island status REM show widget PID, session, last 5 log lines
+mcode-island show REM re-raise a hidden widget
+mcode-island pin REM lock click-to-focus target to current foreground
+mcode-island unpin REM clear focus target
+mcode-island autostart-on REM register for Windows logon
+mcode-island autostart-off REM unregister
+```
+
+The `mcode-island` command is the `.cmd` shim. The Skill tells the agent to call
+`notify-island.ps1` directly, not the shim, because PowerShell agents can
+invoke `.ps1` more easily than `.cmd`.
+
+## How click-to-focus works
+
+When the agent calls `notify-island.ps1`, the helper probes the process chain
+for an attached console window, and falls back to the first ancestor with a
+`MainWindowHandle`. The discovered HWND / PID is written to `caller.json`. When
+the user clicks the pill, the widget uses `SetForegroundWindow` on that HWND.
+
+If the auto-detected HWND is wrong (common on Windows Terminal with multiple
+tabs), run `mcode-island pin` from inside the tab you want to return to. The
+CLI grabs the foreground window *before* the widget steals focus, so this is
+the most reliable fix.
+
+The widget is also intentionally hard to kill. Alt+F4 sends `WM_CLOSE`, which
+the widget intercepts in `WndProc` and treats as Hide rather than Close. To
+re-raise the hidden window: `mcode-island show`. To kill it for real:
+`mcode-island stop`.
+
+## Test evidence
+
+This plugin has been exercised on Windows 11 24H2 with PowerShell 5.1, against
+a live MiniMax Code session. Empirical evidence (captured during development):
+
+- 59 state transitions recorded in `island.log` over a multi-hour session
+ (21 `working` / 14 `done` / 11 `idle` / 6 `waiting` / 5 `thinking` / 1 `error`).
+- All 6 states screenshot-verified (`assets/state-*.png`).
+- Full state-machine demo (`assets/demo-*.png`): `thinking` → `working` →
+ `waiting` → `working` → `done` on a real `bash npm test` run.
+- Click-to-focus round-trip verified: from a Feishu tab, click the pill, focus
+ jumps to the originating Windows Terminal tab (HWND consistent).
+- `wrap-tool.ps1` exit-code semantics: 0 → `done`, 1 → `waiting` (default;
+ configurable via `-WaitingExitCodes`), other → `error`. As of v0.2.1 the
+ wrapper no longer executes the command itself; the agent runs the command
+ through mcode's own bash tool and passes the exit code to `wrap-tool.ps1
+ -ExitCode`.
+
+## Limitations
+
+- Windows only. The widget uses WPF, `user32`, and `kernel32` P/Invoke.
+- One widget per user session.
+- `wrap-tool.ps1` only wraps `bash`. Other tools need direct
+ `notify-island.ps1` calls.
+- The widget does not show a progress percentage, token usage, or per-tool
+ output. v0.2 will.
+- File-system polling at 400 ms is not the most efficient design (FileSystemWatcher
+ was unstable inside WPF in our tests), but it is robust against any kind of
+ writer and never misses an event.
+
+## Roadmap
+
+v0.2 — Tauri (Rust + WebView) rewrite for richer UI, hover-expand, click-through,
+ per-step progress, media-control integration.
+v0.3 — Multi-tab awareness, terminal-agnostic focus restore.
+
+## License
+
+Apache-2.0. See [LICENSE](LICENSE).
diff --git a/plugins/antianqi/mcode-island/assets/state-done.png b/plugins/antianqi/mcode-island/assets/state-done.png
new file mode 100644
index 0000000..b49a138
Binary files /dev/null and b/plugins/antianqi/mcode-island/assets/state-done.png differ
diff --git a/plugins/antianqi/mcode-island/assets/state-error.png b/plugins/antianqi/mcode-island/assets/state-error.png
new file mode 100644
index 0000000..a3da040
Binary files /dev/null and b/plugins/antianqi/mcode-island/assets/state-error.png differ
diff --git a/plugins/antianqi/mcode-island/assets/state-idle.png b/plugins/antianqi/mcode-island/assets/state-idle.png
new file mode 100644
index 0000000..5252bb2
Binary files /dev/null and b/plugins/antianqi/mcode-island/assets/state-idle.png differ
diff --git a/plugins/antianqi/mcode-island/assets/state-thinking.png b/plugins/antianqi/mcode-island/assets/state-thinking.png
new file mode 100644
index 0000000..909ae36
Binary files /dev/null and b/plugins/antianqi/mcode-island/assets/state-thinking.png differ
diff --git a/plugins/antianqi/mcode-island/assets/state-waiting.png b/plugins/antianqi/mcode-island/assets/state-waiting.png
new file mode 100644
index 0000000..23a9a16
Binary files /dev/null and b/plugins/antianqi/mcode-island/assets/state-waiting.png differ
diff --git a/plugins/antianqi/mcode-island/assets/state-working.png b/plugins/antianqi/mcode-island/assets/state-working.png
new file mode 100644
index 0000000..2c51aab
Binary files /dev/null and b/plugins/antianqi/mcode-island/assets/state-working.png differ
diff --git a/plugins/antianqi/mcode-island/assets/wrap-demo.png b/plugins/antianqi/mcode-island/assets/wrap-demo.png
new file mode 100644
index 0000000..d6087d9
Binary files /dev/null and b/plugins/antianqi/mcode-island/assets/wrap-demo.png differ
diff --git a/plugins/antianqi/mcode-island/autostart.ps1 b/plugins/antianqi/mcode-island/autostart.ps1
new file mode 100644
index 0000000..51ed2dd
--- /dev/null
+++ b/plugins/antianqi/mcode-island/autostart.ps1
@@ -0,0 +1,45 @@
+# mcode-island - 开机自启管理
+# 用法:
+# autostart.ps1 -Enable # 注册到 HKCU\...\Run,开机自动起
+# autostart.ps1 -Disable # 取消
+# autostart.ps1 -Status # 看当前状态
+
+param(
+ [ValidateSet('Enable','Disable','Status')]
+ [string]$Action = 'Status'
+)
+
+$ErrorActionPreference = 'Stop'
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+$runKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'
+$entryName = 'mcode-island'
+$launcher = Join-Path $PSScriptRoot 'start-island.ps1'
+$command = "powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$launcher`""
+
+switch ($Action) {
+ 'Enable' {
+ New-Item -Path $runKey -Force | Out-Null
+ Set-ItemProperty -Path $runKey -Name $entryName -Value $command
+ Write-Output "ENABLED: 开机自启已注册"
+ Write-Output " Key: $runKey\$entryName"
+ Write-Output " Value: $command"
+ }
+ 'Disable' {
+ if (Get-ItemProperty -Path $runKey -Name $entryName -ErrorAction SilentlyContinue) {
+ Remove-ItemProperty -Path $runKey -Name $entryName
+ Write-Output 'DISABLED: 开机自启已取消'
+ } else {
+ Write-Output 'DISABLED: 本来就没注册'
+ }
+ }
+ 'Status' {
+ $existing = Get-ItemProperty -Path $runKey -Name $entryName -ErrorAction SilentlyContinue
+ if ($existing) {
+ Write-Output 'ENABLED'
+ Write-Output " Value: $($existing.$entryName)"
+ } else {
+ Write-Output 'DISABLED'
+ }
+ }
+}
diff --git a/plugins/antianqi/mcode-island/mcode-island.cmd b/plugins/antianqi/mcode-island/mcode-island.cmd
new file mode 100644
index 0000000..e817ce0
--- /dev/null
+++ b/plugins/antianqi/mcode-island/mcode-island.cmd
@@ -0,0 +1,55 @@
+@echo off
+chcp 65001 >nul
+setlocal
+set "SCRIPT_DIR=%~dp0"
+set "PS=powershell.exe -NoProfile -ExecutionPolicy Bypass"
+
+if "%1"=="" goto :start
+if /i "%1"=="start" goto :start
+if /i "%1"=="stop" goto :stop
+if /i "%1"=="status" goto :status
+if /i "%1"=="show" goto :show
+if /i "%1"=="hide" goto :hide
+if /i "%1"=="pin" goto :pin
+if /i "%1"=="unpin" goto :unpin
+if /i "%1"=="autostart-on" goto :autostart_on
+if /i "%1"=="autostart-off" goto :autostart_off
+
+echo Usage: mcode-island {start ^| stop ^| status ^| show ^| hide ^| pin ^| unpin ^| autostart-on ^| autostart-off}
+exit /b 1
+
+:start
+%PS% -File "%SCRIPT_DIR%start-island.ps1"
+exit /b %errorlevel%
+
+:stop
+%PS% -File "%SCRIPT_DIR%stop-island.ps1"
+exit /b %errorlevel%
+
+:status
+%PS% -File "%SCRIPT_DIR%status-island.ps1"
+exit /b %errorlevel%
+
+:show
+%PS% -File "%SCRIPT_DIR%show-island.ps1"
+exit /b %errorlevel%
+
+:hide
+echo (Alt+F4 now hides the widget; use "mcode-island show" to bring it back)
+exit /b 0
+
+:pin
+%PS% -File "%SCRIPT_DIR%pin-island.ps1"
+exit /b %errorlevel%
+
+:unpin
+%PS% -Command "Remove-Item -Path (Join-Path $env:APPDATA 'mcode-island\caller.json') -ErrorAction SilentlyContinue; Write-Output 'unpinned: caller.json cleared'"
+exit /b %errorlevel%
+
+:autostart_on
+%PS% -File "%SCRIPT_DIR%autostart.ps1" -Action Enable
+exit /b %errorlevel%
+
+:autostart_off
+%PS% -File "%SCRIPT_DIR%autostart.ps1" -Action Disable
+exit /b %errorlevel%
diff --git a/plugins/antianqi/mcode-island/mcode-island.ps1 b/plugins/antianqi/mcode-island/mcode-island.ps1
new file mode 100644
index 0000000..cd4334f
--- /dev/null
+++ b/plugins/antianqi/mcode-island/mcode-island.ps1
@@ -0,0 +1,509 @@
+# mcode 灵动岛 v1 - WPF + PowerShell
+# 用法:右键 → 用 PowerShell 运行;或通过 start-island.ps1 启动
+
+$ErrorActionPreference = 'Stop'
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+# 调试日志(写到 %APPDATA%\mcode-island\widget.log,最后 1KB 即可)
+$script:dbg = Join-Path $env:APPDATA 'mcode-island\widget.log'
+function Dbg($msg) {
+ $ts = (Get-Date).ToString('HH:mm:ss.fff')
+ "[$ts] $msg" | Add-Content -Path $script:dbg -Encoding UTF8
+}
+Dbg "PID=$PID APART=$([System.Threading.Thread]::CurrentThread.ApartmentState)"
+
+# WPF 必须在 STA 线程运行。如果当前不是 STA,自动重启。
+if ([System.Threading.Thread]::CurrentThread.ApartmentState -ne 'STA') {
+ Dbg 'relaunching in STA'
+ $args2 = @('-NoProfile', '-STA', '-ExecutionPolicy', 'Bypass', '-File', $PSCommandPath) + $args
+ Start-Process powershell.exe -ArgumentList $args2 -WindowStyle Hidden
+ exit
+}
+
+# 加载 WPF
+Dbg 'loading WPF assemblies'
+try {
+ Add-Type -AssemblyName PresentationFramework
+ Add-Type -AssemblyName PresentationCore
+ Add-Type -AssemblyName WindowsBase
+ Add-Type -AssemblyName System.Xaml
+ Dbg 'WPF loaded'
+} catch {
+ Dbg "WPF LOAD FAIL: $($_.Exception.Message)"
+ throw
+}
+
+# P/Invoke:切回 mcode 窗口
+Dbg 'loading WinAPI'
+Add-Type @"
+using System;
+using System.Runtime.InteropServices;
+public class WinAPI {
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
+ [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
+ [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr hWnd);
+ [DllImport("user32.dll")] public static extern bool AllowSetForegroundWindow(uint dwProcessId);
+ [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
+ [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr hWnd);
+ [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
+ [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
+ [DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr hWnd);
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
+ [DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc lpEnumFunc, IntPtr lParam);
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
+ public delegate bool EnumProc(IntPtr hWnd, IntPtr lParam);
+ public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
+ public const uint SWP_NOACTIVATE = 0x0010;
+
+ // 找 pid 的第一个可见窗口
+ public static IntPtr FindVisibleWindowForPid(uint targetPid) {
+ IntPtr found = IntPtr.Zero;
+ EnumWindows((h, l) => {
+ uint p; GetWindowThreadProcessId(h, out p);
+ if (p == targetPid && IsWindowVisible(h)) {
+ int len = GetWindowTextLength(h);
+ if (len > 0) { found = h; return false; }
+ }
+ return true;
+ }, IntPtr.Zero);
+ return found;
+ }
+}
+"@ -ErrorAction SilentlyContinue
+Dbg 'WinAPI loaded'
+
+# 配置目录
+$configDir = Join-Path $env:APPDATA 'mcode-island'
+if (!(Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Force | Out-Null }
+$configFile = Join-Path $configDir 'config.json'
+$statusFile = Join-Path $configDir 'status.json'
+$logFile = Join-Path $configDir 'island.log'
+
+# 加载配置(位置等)
+$defaultConfig = [PSCustomObject]@{
+ x = -1
+ y = -1
+ width = 320
+ height = 60
+ opacity = 0.95
+ autostart = $false
+}
+if (Test-Path $configFile) {
+ try { $cfg = Get-Content $configFile -Raw -Encoding UTF8 | ConvertFrom-Json }
+ catch { $cfg = $defaultConfig }
+ foreach ($p in @('x','y','width','height','opacity','autostart')) {
+ if (-not (Get-Member -InputObject $cfg -Name $p -ErrorAction SilentlyContinue)) {
+ Add-Member -InputObject $cfg -NotePropertyName $p -NotePropertyValue $defaultConfig.$p
+ }
+ }
+} else {
+ $cfg = $defaultConfig
+ $cfg | ConvertTo-Json | Out-File -FilePath $configFile -Encoding UTF8
+}
+
+# XAML —— 灵动岛 pill
+$xaml = @'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.5,0.5
+
+
+
+
+
+
+
+
+
+
+
+
+
+'@
+
+# 加载 XAML
+Dbg 'parsing XAML'
+try {
+ $reader = New-Object System.Xml.XmlNodeReader ([xml]$xaml)
+ $window = [Windows.Markup.XamlReader]::Load($reader)
+ Dbg 'XAML loaded'
+} catch {
+ Dbg "XAML FAIL: $($_.Exception.Message)"
+ throw
+}
+
+# 取控件
+$statusDot = $window.FindName('StatusDot')
+$pulseRing = $window.FindName('PulseRing')
+$pulseScale = $window.FindName('PulseScale')
+$stateText = $window.FindName('StateText')
+$messageText = $window.FindName('MessageText')
+$actionIcon = $window.FindName('ActionIcon')
+$pill = $window.FindName('Pill')
+
+# 状态配色
+$stateMap = @{
+ idle = @{ dot='#FF6B7280'; ring='#FF6B7280'; label='mcode'; icon='' }
+ thinking = @{ dot='#FFEAB308'; ring='#FFEAB308'; label='mcode · 思考中'; icon='' }
+ working = @{ dot='#FF3B82F6'; ring='#FF3B82F6'; label='mcode · 执行中'; icon='⚙' }
+ waiting = @{ dot='#FFF59E0B'; ring='#FFF59E0B'; label='mcode · 等待审批'; icon='?' }
+ done = @{ dot='#FF22C55E'; ring='#FF22C55E'; label='mcode · 已完成'; icon='✓' }
+ error = @{ dot='#FFEF4444'; ring='#FFEF4444'; label='mcode · 出错'; icon='✕' }
+}
+
+# 颜色转 brush
+function C($hex) { return (New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.ColorConverter]::ConvertFromString($hex))) }
+
+# 脉冲动画
+$hasPulse = $false
+$storyboard = $null
+function Start-Pulse {
+ if ($script:hasPulse) { return }
+ $script:hasPulse = $true
+ $animX = New-Object System.Windows.Media.Animation.DoubleAnimation
+ $animX.From = 1.0; $animX.To = 1.9
+ $animX.Duration = [TimeSpan]::FromSeconds(1.0)
+ $animX.AutoReverse = $true
+ $animX.RepeatBehavior = [System.Windows.Media.Animation.RepeatBehavior]::Forever
+ [System.Windows.Media.Animation.Storyboard]::SetTarget($animX, $script:pulseScale)
+ [System.Windows.Media.Animation.Storyboard]::SetTargetProperty($animX, (New-Object System.Windows.PropertyPath('ScaleX')))
+ $animY = New-Object System.Windows.Media.Animation.DoubleAnimation
+ $animY.From = 1.0; $animY.To = 1.9
+ $animY.Duration = [TimeSpan]::FromSeconds(1.0)
+ $animY.AutoReverse = $true
+ $animY.RepeatBehavior = [System.Windows.Media.Animation.RepeatBehavior]::Forever
+ [System.Windows.Media.Animation.Storyboard]::SetTarget($animY, $script:pulseScale)
+ [System.Windows.Media.Animation.Storyboard]::SetTargetProperty($animY, (New-Object System.Windows.PropertyPath('ScaleY')))
+
+ $opacityAnim = New-Object System.Windows.Media.Animation.DoubleAnimation
+ $opacityAnim.From = 0.55; $opacityAnim.To = 0.0
+ $opacityAnim.Duration = [TimeSpan]::FromSeconds(1.0)
+ $opacityAnim.AutoReverse = $true
+ $opacityAnim.RepeatBehavior = [System.Windows.Media.Animation.RepeatBehavior]::Forever
+ [System.Windows.Media.Animation.Storyboard]::SetTarget($opacityAnim, $script:pulseRing)
+ [System.Windows.Media.Animation.Storyboard]::SetTargetProperty($opacityAnim, (New-Object System.Windows.PropertyPath('Opacity')))
+
+ $sb = New-Object System.Windows.Media.Animation.Storyboard
+ $sb.Children.Add($animX) | Out-Null
+ $sb.Children.Add($animY) | Out-Null
+ $sb.Children.Add($opacityAnim) | Out-Null
+ $script:storyboard = $sb
+ $sb.Begin($window)
+}
+function Stop-Pulse {
+ $script:hasPulse = $false
+ if ($script:storyboard) { $script:storyboard.Stop($window) }
+ $script:pulseScale.ScaleX = 1.0
+ $script:pulseScale.ScaleY = 1.0
+ $script:pulseRing.Opacity = 0.0
+}
+
+# 状态更新
+function Update-State {
+ param([string]$State, [string]$Message)
+ $s = $script:stateMap[$State]
+ if (!$s) { $s = $script:stateMap['idle'] }
+ $script:statusDot.Fill = C $s.dot
+ $script:pulseRing.Fill = C $s.ring
+ $script:stateText.Text = $s.label
+ $script:messageText.Text = if ($Message) { $Message } else { '' }
+ $script:actionIcon.Text = $s.icon
+
+ if ($State -in @('thinking','working','waiting')) { Start-Pulse } else { Stop-Pulse }
+
+ # 写入 log
+ $ts = (Get-Date).ToString('HH:mm:ss')
+ "[$ts] $State :: $Message" | Add-Content -Path $script:logFile -Encoding UTF8
+}
+
+# 切回调用方窗口(点击 pill 时调用)
+function Focus-CallerWindow {
+ $callerFile = Join-Path $env:APPDATA 'mcode-island\caller.json'
+ if (!(Test-Path $callerFile)) { Dbg 'FOCUS: no caller file'; return }
+
+ $hwnd = [IntPtr]::Zero
+ $targetPid = 0
+ $targetExe = ''
+ try {
+ $data = Get-Content $callerFile -Raw -Encoding UTF8 | ConvertFrom-Json
+ $hwnd = [IntPtr]::new([int64]$data.targetHwnd)
+ $targetPid = [int]$data.targetPid
+ $targetExe = if ($data.targetExe) { [string]$data.targetExe } else { '' }
+ } catch {
+ Dbg "FOCUS: caller.json parse error"
+ return
+ }
+ if ($targetPid -le 0) { Dbg 'FOCUS: no target'; return }
+
+ # 1) 检查 hwnd 是否还活着
+ if ($hwnd -ne [IntPtr]::Zero -and -not [WinAPI]::IsWindow($hwnd)) {
+ Dbg "FOCUS: hwnd $hwnd dead, re-resolving"
+ $hwnd = [IntPtr]::Zero
+ }
+
+ # 2) 进程死了 → 找它的父进程(terminal)兜底
+ $proc = Get-Process -Id $targetPid -ErrorAction SilentlyContinue
+ if (-not $proc) {
+ Dbg "FOCUS: target PID $targetPid gone, finding parent (terminal)"
+ $parent = Get-CimInstance Win32_Process -Filter "ProcessId=$targetPid" -ErrorAction SilentlyContinue
+ if ($parent -and $parent.ParentProcessId -and $parent.ParentProcessId -gt 0) {
+ $parentProc = Get-Process -Id ([int]$parent.ParentProcessId) -ErrorAction SilentlyContinue
+ if ($parentProc) {
+ $targetPid = $parentProc.Id
+ $targetExe = $parentProc.ProcessName
+ # 优先用 MainWindowHandle,失败就用第一个可见窗口
+ if ($parentProc.MainWindowHandle -ne [IntPtr]::Zero) {
+ $hwnd = $parentProc.MainWindowHandle
+ } else {
+ $hwnd = [WinAPI]::FindVisibleWindowForPid([uint32]$targetPid)
+ }
+ Dbg "FOCUS: fall back to parent $($parentProc.ProcessName) PID=$targetPid hwnd=$hwnd"
+ }
+ }
+ if ($hwnd -eq [IntPtr]::Zero) {
+ Dbg 'FOCUS: no parent fallback available'
+ return
+ }
+ }
+ # 3) 进程还在但 hwnd 死了(被销毁/重建)→ 找进程的第一个可见窗口
+ if ($hwnd -eq [IntPtr]::Zero -or -not [WinAPI]::IsWindow($hwnd)) {
+ Dbg "FOCUS: hwnd invalid, finding new visible window for PID $targetPid ($targetExe)"
+ $hwnd = [WinAPI]::FindVisibleWindowForPid([uint32]$targetPid)
+ if ($hwnd -eq [IntPtr]::Zero) {
+ Dbg 'FOCUS: no visible window found for target process'
+ return
+ }
+ Dbg "FOCUS: re-resolved to hwnd $hwnd"
+ }
+
+ try {
+ # 1) 授权目标进程可以切前台(modern Windows 强制)
+ [WinAPI]::AllowSetForegroundWindow([uint32]$targetPid) | Out-Null
+ # 2) 最小化就还原
+ if ([WinAPI]::IsIconic($hwnd)) {
+ [WinAPI]::ShowWindow($hwnd, 9) | Out-Null # SW_RESTORE
+ }
+ # 3) 设顶
+ [WinAPI]::SetWindowPos($hwnd, [WinAPI]::HWND_TOPMOST, 0, 0, 0, 0, [WinAPI]::SWP_NOACTIVATE) | Out-Null
+ [WinAPI]::SetWindowPos($hwnd, [IntPtr]::new(-2), 0, 0, 0, 0, [WinAPI]::SWP_NOACTIVATE) | Out-Null # HWND_NOTOPMOST
+ # 4) 抢焦点
+ [WinAPI]::BringWindowToTop($hwnd) | Out-Null
+ [WinAPI]::SetForegroundWindow($hwnd) | Out-Null
+ $proc2 = Get-Process -Id $targetPid -ErrorAction SilentlyContinue
+ Dbg ("FOCUS OK: target=" + $proc2.ProcessName + " PID=" + $targetPid + " hwnd=" + $hwnd)
+ } catch {
+ Dbg "FOCUS FAIL: $($_.Exception.Message)"
+ }
+}
+
+# 手动设置焦点目标(右键菜单调用):把当前前台窗口记为 focus target
+function Set-FocusTarget-Current {
+ Add-Type @"
+using System;
+using System.Runtime.InteropServices;
+public class FG2 {
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
+}
+"@ -ErrorAction SilentlyContinue
+ $hwnd = [FG2]::GetForegroundWindow()
+ if ($hwnd -eq [IntPtr]::Zero) { Dbg 'PIN: no foreground'; return }
+ $ownerPid = 0
+ [FG2]::GetWindowThreadProcessId($hwnd, [ref]$ownerPid) | Out-Null
+ $proc = Get-Process -Id $ownerPid -ErrorAction SilentlyContinue
+ if (-not $proc) { Dbg 'PIN: target process gone'; return }
+ $myPid = [int](Get-Process -Id $PID).Id
+ $mySession = (Get-Process -Id $myPid).SessionId
+ $callerFile = Join-Path $env:APPDATA 'mcode-island\caller.json'
+ $payload = [PSCustomObject]@{
+ callerPid = $myPid
+ callerSession = $mySession
+ targetPid = $proc.Id
+ targetHwnd = [int64]$hwnd
+ targetTitle = $proc.MainWindowTitle
+ targetExe = $proc.ProcessName
+ source = 'manual-pin'
+ ts = (Get-Date).ToString('o')
+ } | ConvertTo-Json -Compress
+ [System.IO.File]::WriteAllText($callerFile, $payload, [System.Text.Encoding]::UTF8)
+ Dbg ("PIN: target=" + $proc.ProcessName + " PID=" + $proc.Id + " hwnd=" + $hwnd)
+}
+
+# 点击高亮(scale 1.0 -> 0.95 -> 1.0)
+function Flash-Click {
+ $sb = New-Object System.Windows.Media.Animation.Storyboard
+ $sx = New-Object System.Windows.Media.Animation.DoubleAnimation
+ $sx.From = 1.0; $sx.To = 0.95; $sx.Duration = [TimeSpan]::FromMilliseconds(80)
+ $sx.AutoReverse = $true
+ [System.Windows.Media.Animation.Storyboard]::SetTarget($sx, $script:pill)
+ [System.Windows.Media.Animation.Storyboard]::SetTargetProperty($sx, (New-Object System.Windows.PropertyPath '(UIElement.RenderTransform).(ScaleTransform.ScaleX)'))
+ # 用 transient transform
+ $sy = New-Object System.Windows.Media.Animation.DoubleAnimation
+ $sy.From = 1.0; $sy.To = 0.95; $sy.Duration = [TimeSpan]::FromMilliseconds(80)
+ $sy.AutoReverse = $true
+ [System.Windows.Media.Animation.Storyboard]::SetTarget($sy, $script:pill)
+ [System.Windows.Media.Animation.Storyboard]::SetTargetProperty($sy, (New-Object System.Windows.PropertyPath '(UIElement.RenderTransform).(ScaleTransform.ScaleY)'))
+ $sb.Children.Add($sx) | Out-Null
+ $sb.Children.Add($sy) | Out-Null
+ $sb.Begin($window)
+}
+
+# 窗口定位
+$screen = [System.Windows.SystemParameters]::WorkArea
+if ($cfg.x -lt 0) {
+ $window.Left = [Math]::Floor(($screen.Width - $cfg.width) / 2) + $screen.Left
+ $window.Top = $screen.Top + 14
+} else {
+ $window.Left = $cfg.x
+ $window.Top = $cfg.y
+}
+$window.Width = $cfg.width
+$window.Height = $cfg.height
+$window.Opacity = $cfg.opacity
+
+# 右键被禁用(用 CLI 'mcode-island pin' 替代,更稳)
+$window.Add_MouseRightButtonDown({
+ $null = $_.Handled
+ Dbg 'RIGHT-CLICK ignored, use "mcode-island pin" in mcode terminal'
+})
+
+# 点击 vs 拖动
+$script:dragStart = $null
+$script:didDrag = $false
+$script:pill.RenderTransform = New-Object System.Windows.Media.ScaleTransform(1, 1)
+$script:pill.RenderTransformOrigin = New-Object System.Windows.Point(0.5, 0.5)
+$window.Add_MouseLeftButtonDown({
+ $script:dragStart = $_.GetPosition($window)
+ $script:didDrag = $false
+})
+$window.Add_MouseMove({
+ if ($script:dragStart -and -not $script:didDrag) {
+ $cur = $_.GetPosition($window)
+ $dx = [Math]::Abs($cur.X - $script:dragStart.X)
+ $dy = [Math]::Abs($cur.Y - $script:dragStart.Y)
+ if ($dx + $dy -gt 5) {
+ $script:didDrag = $true
+ try { $window.DragMove() } catch {}
+ }
+ }
+})
+$window.Add_MouseLeftButtonUp({
+ try {
+ if ($script:dragStart -and -not $script:didDrag) {
+ Dbg 'CLICK detected'
+ Flash-Click
+ Focus-CallerWindow
+ }
+ } catch {
+ Dbg "CLICK FAIL: $($_.Exception.Message)"
+ }
+ $script:dragStart = $null
+ $script:didDrag = $false
+})
+
+
+# FileSystemWatcher 监听 status.json(轮询版本,FSW 在 WPF 应用里经常抽风)
+$script:lastStatusMtime = $null
+$script:lastStatusSig = $null
+$showSignalFile = Join-Path $configDir 'show.signal'
+$timer = New-Object System.Windows.Threading.DispatcherTimer
+$timer.Interval = [TimeSpan]::FromMilliseconds(400)
+$timer.Add_Tick({
+ # 1) 处理 "show" 信号("mcode-island show" 创建这个文件)
+ if (Test-Path $showSignalFile) {
+ try { Remove-Item $showSignalFile -Force -ErrorAction SilentlyContinue } catch {}
+ $window.Dispatcher.Invoke([Action]{
+ $window.Show()
+ $window.WindowState = [System.Windows.WindowState]::Normal
+ $window.Activate()
+ Dbg 'SHOWN via signal file'
+ })
+ }
+ # 2) 处理 status.json
+ if (!(Test-Path $statusFile)) { return }
+ try {
+ $fi = Get-Item $statusFile
+ $mtime = $fi.LastWriteTimeUtc.Ticks
+ if ($mtime -eq $script:lastStatusMtime) { return }
+ $script:lastStatusMtime = $mtime
+ $data = Get-Content $statusFile -Raw -Encoding UTF8 | ConvertFrom-Json
+ $sig = "$($data.state)|$($data.message)|$($data.ts)"
+ if ($sig -eq $script:lastStatusSig) { return }
+ $script:lastStatusSig = $sig
+ Dbg "POLL: $($data.state) :: $($data.message)"
+ Update-State -State $data.state -Message $data.message
+ } catch {
+ Dbg "POLL ERR: $($_.Exception.Message)"
+ }
+})
+$timer.Start()
+Dbg 'poll timer started'
+
+# 启动时读一次 status.json(如果存在)
+if (Test-Path $statusFile) {
+ try {
+ $init = Get-Content $statusFile -Raw -Encoding UTF8 | ConvertFrom-Json
+ $script:lastStatusSig = "$($init.state)|$($init.message)|$($init.ts)"
+ $script:lastStatusMtime = (Get-Item $statusFile).LastWriteTimeUtc.Ticks
+ Update-State -State $init.state -Message $init.message
+ } catch {}
+} else {
+ Update-State -State 'idle' -Message ''
+}
+Dbg 'initial state set'
+
+
+# 关闭事件:保存位置 + 隐藏窗口(不真关进程)
+$script:realQuit = $false
+$window.Add_Closing({
+ $args[1].Cancel = $true
+ Dbg 'Closing event -> hide'
+ try {
+ $cur = Get-Content $script:configFile -Raw -Encoding UTF8 | ConvertFrom-Json
+ $cur.x = $window.Left
+ $cur.y = $window.Top
+ $cur | ConvertTo-Json | Out-File -Path $script:configFile -Encoding UTF8
+ } catch {}
+ $window.Hide()
+})
+# 用 Application.Run 启动消息循环(同时支持 Show + Hide + 重新 Show)
+Dbg 'starting Application.Run'
+$app = New-Object System.Windows.Application
+$app.ShutdownMode = [System.Windows.ShutdownMode]::OnExplicitShutdown # 别自己关
+$app.Run($window) | Out-Null
+Dbg 'Application.Run returned'
+
+# 保持进程运行:Application.Run 在 pump 消息,窗口 hide 不会让 Run 返回
+# 真退出时由 mcode-island stop 强杀进程,或显式调 app.Shutdown()
+
+Dbg 'realQuit set, exiting'
diff --git a/plugins/antianqi/mcode-island/mcode-status-detect.ps1 b/plugins/antianqi/mcode-island/mcode-status-detect.ps1
new file mode 100644
index 0000000..96d2257
--- /dev/null
+++ b/plugins/antianqi/mcode-island/mcode-status-detect.ps1
@@ -0,0 +1,465 @@
+# mcode-status-detect.ps1 - 后台守护进程 (v0.2.1)
+# 监听 mcode session log,实时推断 agent 状态,写到 status.json
+# 让 widget 不依赖 agent 主动调 notify-island.ps1 也能跟着动
+#
+# 路径解析(顺序):
+# 1. 从 mcode node 进程 cmdline 提取 /.minimax-code
+# 2. 探测 $env:USERPROFILE/.minimax-code
+# 3. 探测 $env:USERPROFILE/AppData/Roaming/minimax-code/.minimax-code
+# 4. 探测 /.minimax-code
+#
+# session log(顺序):
+# 1. /.minimax/v2/sessions//*/ledger.jsonl(首选,mcode v2 事件流)
+# 2. /.minimax/v2/sessions//*/messages.jsonl(fallback)
+#
+# 状态映射:
+# role=user → idle (等用户)
+# role=assistant + toolCall → working ": "
+# role=assistant + thinking → thinking
+# role=assistant + text → idle (刚说完,等用户)
+# role=toolResult + !isError → done " 完成"
+# role=toolResult + isError → error " 失败"
+# mcode 进程不在 → error "mcode 已退出"
+# 60s 无新事件 → idle 兜底
+#
+# 优先级:
+# agent 主动推的状态(status.json 中 source != detector)保留
+# detector 推的 idle / error 可以覆盖 agent 推的(兜底)
+# detector 推的 working/thinking/done 不覆盖 agent 推的
+#
+# PS 5.1 parser quirk:
+# 短 ASCII 字符串字面量在 -eq / state= 后会被解析为 cmdlet 参数
+# 所以所有 ASCII 字符串都用 _s (byte[]) helper 构造
+
+param(
+ [switch]$Once,
+ [string]$Root = '' # override: /.minimax-code and /.minimax/v2/...
+)
+
+$ErrorActionPreference = 'Stop'
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+chcp 65001 | Out-Null
+
+function _s { param([byte[]]$b) [System.Text.Encoding]::UTF8.GetString($b) }
+
+# role names
+$S_USER = _s (0x75,0x73,0x65,0x72)
+$S_ASSISTANT = _s (0x61,0x73,0x73,0x69,0x73,0x74,0x61,0x6E,0x74)
+$S_TOOLRESULT = _s (0x74,0x6F,0x6F,0x6C,0x52,0x65,0x73,0x75,0x6C,0x74)
+$S_COMPACTION = _s (0x63,0x6F,0x6D,0x70,0x61,0x63,0x74,0x69,0x6F,0x6E,0x53,0x75,0x6D,0x6D,0x61,0x72,0x79)
+# state values
+$S_IDLE = _s (0x69,0x64,0x6C,0x65)
+$S_THINKING = _s (0x74,0x68,0x69,0x6E,0x6B,0x69,0x6E,0x67)
+$S_WORKING = _s (0x77,0x6F,0x72,0x6B,0x69,0x6E,0x67)
+$S_DONE = _s (0x64,0x6F,0x6E,0x65)
+$S_ERROR = _s (0x65,0x72,0x72,0x6F,0x72)
+# content type discriminators
+$S_TOOLCALL = _s (0x74,0x6F,0x6F,0x6C,0x43,0x61,0x6C,0x6C)
+$S_TEXT = _s (0x74,0x65,0x78,0x74)
+$S_THINK_BLK = _s (0x74,0x68,0x69,0x6E,0x6B,0x69,0x6E,0x67)
+# source tag
+$S_DETECTOR = _s (0x64,0x65,0x74,0x65,0x63,0x74,0x6F,0x72)
+# messages (UTF-8 encoded Chinese)
+$MSG_USER_WAIT = _s (0xE7,0xAD,0x89,0xE7,0x94,0xA8,0xE6,0x88,0xB7) # 等用户
+$MSG_AGENT_DONE = _s (0x61,0x67,0x65,0x6E,0x74,0x20,0xE5,0x88,0x9A,0xE5,0x9B,0x9E,0xE5,0xA4,0x8D,0xEF,0xBC,0x8C,0xE7,0xAD,0x89,0xE7,0x94,0xA8,0xE6,0x88,0xB7) # agent 刚回复,等用户
+$MSG_FAIL = _s (0xE5,0xA4,0xB1,0xE8,0xB4,0xA5) # 失败
+$MSG_OK = _s (0xE5,0xAE,0x8C,0xE6,0x88,0x90) # 完成
+$MSG_COMPACT = _s (0x73,0x65,0x73,0x73,0x69,0x6F,0x6E,0x20,0xE5,0x8E,0x8B,0xE7,0xBC,0xA9) # session 压缩
+$MSG_MCODE_EXIT = _s (0x6D,0x63,0x6F,0x64,0x65,0x20,0xE8,0xBF,0x9B,0xE7,0xA8,0x8B,0xE5,0xB7,0xB2,0xE9,0x80,0x80,0xE5,0x87,0xBA) # mcode 进程已退出
+$MSG_IDLE_FMT = _s (0xE5,0xB7,0xB2,0xE9,0x9D,0x99,0xE9,0x9C,0xA8,0x20,0x7B,0x30,0x7D,0x73) # 已静默 {0}s
+$DOTS = _s (0x2E,0x2E,0x2E) # ...
+$FMT_O = _s (0x6F) # 'o' (ISO 8601 timestamp)
+$FMT_O_FULL = _s (0x4F) # 'O' (ISO 8601 full)
+# log reason tags
+$R_NO_CURRENT = _s (0x6E,0x6F,0x20,0x63,0x75,0x72,0x72,0x65,0x6E,0x74,0x20,0x73,0x74,0x61,0x74,0x75,0x73)
+$R_OWN_CHANGED = _s (0x6F,0x77,0x6E,0x20,0x73,0x74,0x61,0x74,0x65,0x20,0x63,0x68,0x61,0x6E,0x67,0x65,0x64)
+$R_OWN_MSG = _s (0x6F,0x77,0x6E,0x20,0x6D,0x65,0x73,0x73,0x61,0x67,0x65,0x20,0x63,0x68,0x61,0x6E,0x67,0x65,0x64)
+$R_TAKEOVER = _s (0x74,0x61,0x6B,0x65,0x6F,0x76,0x65,0x72,0x20,0x74,0x6F,0x20,0x73,0x65,0x74,0x74,0x6C,0x65)
+# session log file names (优先 ledger)
+$FNAME_LEDGER = _s (0x6C,0x65,0x64,0x67,0x65,0x72,0x2E,0x6A,0x73,0x6F,0x6E,0x6C) # ledger.jsonl
+$FNAME_MESSAGES = _s (0x6D,0x65,0x73,0x73,0x61,0x67,0x65,0x73,0x2E,0x6A,0x73,0x6F,0x6E,0x6C) # messages.jsonl
+# mcode node cli.js path fragment for cmdline match
+$CLI_FRAGMENT = _s (0x40,0x6D,0x69,0x6E,0x69,0x6D,0x61,0x78,0x2D,0x61,0x69,0x2F,0x63,0x6F,0x64,0x65,0x2F,0x63,0x6C,0x69,0x2E,0x6A,0x73) # @minimax-ai/code/cli.js
+# .mcode-active directory name
+$ACTIVE_DIR = _s (0x2E,0x6D,0x63,0x6F,0x64,0x65,0x2D,0x61,0x63,0x74,0x69,0x76,0x65) # .mcode-active
+# .minimax-code directory name (parent of .mcode-active)
+$MINIMAX_CODE = _s (0x2E,0x6D,0x69,0x6E,0x69,0x6D,0x61,0x78,0x2D,0x63,0x6F,0x64,0x65) # .minimax-code
+# v2 sessions relative to user profile
+$V2_SESSIONS_REL = _s (0x2E,0x6D,0x69,0x6E,0x69,0x6D,0x61,0x78,0x5C,0x76,0x32,0x5C,0x73,0x65,0x73,0x73,0x69,0x6F,0x6E,0x73) # .minimax\v2\sessions
+
+$configDir = Join-Path $env:APPDATA 'mcode-island'
+if (!(Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Force | Out-Null }
+$statusFile = Join-Path $configDir 'status.json'
+$logFile = Join-Path $configDir 'island.log'
+$pidFile = Join-Path $configDir 'detect.pid'
+
+# 解析 mcode 安装根目录(/.minimax-code)
+function Find-McodeRoot {
+ # 1) 用户显式 -Root 覆盖
+ if ($script:optRoot -and (Test-Path $script:optRoot)) { return $script:optRoot }
+
+ # 2) 从 mcode node 进程 cmdline 提取
+ $procs = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
+ Where-Object { $_.CommandLine -and $_.CommandLine.Contains($CLI_FRAGMENT) }
+ foreach ($p in $procs) {
+ try {
+ $m = [regex]::Match($p.CommandLine, '([A-Za-z]:\\[^"'']*?)\.minimax-code\\')
+ if ($m.Success) {
+ $root = $m.Groups[1].Value + $MINIMAX_CODE
+ if (Test-Path $root) { return $root }
+ }
+ } catch {}
+ }
+
+ # 3) 探测 $env:USERPROFILE/.minimax-code
+ if ($env:USERPROFILE) {
+ $candidate = Join-Path $env:USERPROFILE $MINIMAX_CODE
+ if (Test-Path $candidate) { return $candidate }
+ }
+
+ # 4) 探测 $env:USERPROFILE/AppData/Roaming/minimax-code/.minimax-code
+ if ($env:APPDATA) {
+ $candidate = Join-Path (Join-Path $env:APPDATA 'minimax-code') $MINIMAX_CODE
+ if (Test-Path $candidate) { return $candidate }
+ }
+
+ # 5) 探测 /.minimax-code
+ try {
+ $cwd = (Get-Location).Path
+ $candidate = Join-Path $cwd $MINIMAX_CODE
+ if (Test-Path $candidate) { return $candidate }
+ } catch {}
+
+ return $null
+}
+
+# sessions 根目录(优先 mcode 安装目录上一级,再 fallback userprofile)
+function Get-SessionsRoot($mcodeRoot) {
+ if ($mcodeRoot) {
+ # .minimax-code 和 .minimax 平级,都在 下
+ $parent = Split-Path -Parent $mcodeRoot
+ $candidate = Join-Path $parent (Join-Path '.minimax' (Join-Path 'v2' 'sessions'))
+ if (Test-Path $candidate) { return $candidate }
+ }
+ if ($env:USERPROFILE) {
+ $candidate = Join-Path $env:USERPROFILE $V2_SESSIONS_REL
+ if (Test-Path $candidate) { return $candidate }
+ }
+ return $null
+}
+
+$script:optRoot = $Root
+$mcodeRoot = Find-McodeRoot
+if (-not $mcodeRoot) {
+ Write-Error "Cannot find mcode install root (.minimax-code). Pass -Root or ensure mcode is running."
+ exit 2
+}
+$mcodeActiveDir = Join-Path $mcodeRoot $ACTIVE_DIR
+$sessionsRoot = Get-SessionsRoot $mcodeRoot
+if (-not (Test-Path $mcodeActiveDir)) {
+ Write-Warning ".mcode-active not found at $mcodeActiveDir — mcode not running or different layout"
+}
+
+# 全局状态
+$script:lastDetectedState = ''
+$script:lastMsgsSnapshot = @{ mtime = ''; lastInferred = $null }
+
+# 5s 缓存:mcode 进程 PID + 最新 session log 路径
+# 避免每秒调 Get-McodePid / Get-LatestSessionFile(每次都涉及 Get-ChildItem
+# 之类的 cmdlet,PS 5.1 hidden window 下会累积 Runspace 线程,9 小时后
+# 池子被占满 → poll 不再前进 → widget 卡死)。
+$CACHE_TTL = [TimeSpan]::FromSeconds(5)
+$script:lastMcodePid = $null
+$script:lastMcodePidAt = [DateTime]::MinValue
+$script:lastLatestFile = $null
+$script:lastLatestFileAt = [DateTime]::MinValue
+
+function Log-Line($msg) {
+ $ts = (Get-Date).ToString('HH:mm:ss')
+ $line = "[$ts] [detect] $msg"
+ Add-Content -Path $logFile -Value $line -Encoding UTF8
+ if (-not $Once) { Write-Output $line }
+}
+
+function Get-McodePid {
+ if (-not (Test-Path $mcodeActiveDir)) { return $null }
+ # Use [System.IO.Directory]::EnumerateFiles instead of Get-ChildItem + foreach
+ # to avoid the PS 5.1 pipeline-thread leak that built up over multi-hour
+ # runs and eventually stalled the detector. See: ~30k leaked threads
+ # observed after 9h of polling.
+ $bestPid = $null
+ $bestMtime = [DateTime]::MinValue
+ foreach ($f in [System.IO.Directory]::EnumerateFiles($mcodeActiveDir, '*.json')) {
+ try {
+ $raw = [System.IO.File]::ReadAllText($f)
+ $j = $raw | ConvertFrom-Json
+ $targetPid = [int]$j.pid
+ $proc = [System.Diagnostics.Process]::GetProcessById($targetPid)
+ if ($proc -and -not $proc.HasExited) {
+ # .mcode-active dir is tiny (one or two .json files); prefer the most
+ # recently started one if multiple happen to exist.
+ $mtime = [System.IO.File]::GetLastWriteTime($f)
+ if ($mtime -gt $bestMtime) {
+ $bestMtime = $mtime
+ $bestPid = $targetPid
+ }
+ }
+ } catch {}
+ }
+ return $bestPid
+}
+
+function Get-LatestSessionFile {
+ if (-not $sessionsRoot -or -not (Test-Path $sessionsRoot)) { return $null }
+ # Replace Get-ChildItem -Recurse | Where-Object | Sort-Object | Select-Object
+ # (a 4-stage pipeline) with a single .NET enumeration + manual mtime scan.
+ # Same reason as Get-McodePid: avoid the PS 5.1 pipeline-thread leak.
+ $best = $null
+ $bestMtime = [DateTime]::MinValue
+ $bestIsLedger = $false
+ foreach ($f in [System.IO.Directory]::EnumerateFiles($sessionsRoot, '*.jsonl', 'AllDirectories')) {
+ $name = [System.IO.Path]::GetFileName($f)
+ $isLedger = ($name -eq $FNAME_LEDGER)
+ $isMsgs = ($name -eq $FNAME_MESSAGES)
+ if (-not $isLedger -and -not $isMsgs) { continue }
+ try {
+ $mtime = [System.IO.File]::GetLastWriteTime($f)
+ } catch {
+ continue
+ }
+ # 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.
+ if ($mtime -gt $bestMtime) {
+ $bestMtime = $mtime
+ $best = $f
+ $bestIsLedger = $isLedger
+ }
+ }
+ return $best
+}
+
+function Read-LastMessage($file) {
+ if (-not $file) { return $null }
+ try {
+ $fi = [System.IO.FileInfo]::new($file)
+ $mtime = $fi.LastWriteTime.ToString($FMT_O_FULL)
+ if ($script:lastMsgsSnapshot.mtime -eq $mtime) {
+ # mtime 没变:返回上次推断结果(保持 idle 兜底可达)
+ return $script:lastMsgsSnapshot.lastInferred
+ }
+ $script:lastMsgsSnapshot.mtime = $mtime
+ $fs = [System.IO.File]::Open($fi.FullName, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
+ try {
+ $len = $fs.Length
+ if ($len -eq 0) { return $null }
+ $lookback = [Math]::Min($len, 65536)
+ $fs.Seek(-$lookback, [System.IO.SeekOrigin]::End) | Out-Null
+ $buf = New-Object byte[] $lookback
+ $fs.Read($buf, 0, $lookback) | Out-Null
+ $text = [System.Text.Encoding]::UTF8.GetString($buf)
+ $parts = $text -split "`n"
+ for ($i = $parts.Length - 1; $i -ge 0; $i--) {
+ $line = $parts[$i].TrimEnd("`r")
+ if ($line -and $line.Length -gt 10) {
+ # ConvertFrom-Json is gated by the mtime cache above, so this
+ # pipeline only fires when the file actually grew. Acceptable.
+ $msg = ($line | ConvertFrom-Json)
+ $script:lastMsgsSnapshot.lastInferred = $msg
+ return $msg
+ }
+ }
+ return $null
+ } finally {
+ $fs.Close()
+ }
+ } catch {
+ return $script:lastMsgsSnapshot.lastInferred
+ }
+}
+
+function Infer-State($msg) {
+ if (-not $msg) { return $null }
+ # ledger.jsonl 事件格式:{kind, phase, action, ...}
+ # messages.jsonl 消息格式:{message: {role, content, ...}, ...}
+ $m = $msg.message
+ if (-not $m) {
+ # ledger 事件:用 kind/phase 推断
+ $kind = [string]$msg.kind
+ $phase = [string]$msg.phase
+ if ($kind -eq $S_TOOLRESULT) {
+ if ($msg.isError -eq $true) { return @{ state=$S_ERROR; message="$($msg.toolName) $MSG_FAIL" } }
+ return @{ state=$S_DONE; message="$($msg.toolName) $MSG_OK" }
+ }
+ if ($kind -eq 'toolCall' -or $kind -eq $S_TOOLCALL) {
+ return @{ state=$S_WORKING; message="$($msg.toolName) : $($msg.action)" }
+ }
+ if ($kind -eq $S_ASSISTANT -and $phase -eq $S_THINKING) {
+ return @{ state=$S_THINKING; message='' }
+ }
+ return $null
+ }
+
+ $role = [string]$m.role
+ if ($role -eq $S_USER) { return @{ state=$S_IDLE; message=$MSG_USER_WAIT } }
+
+ if ($role -eq $S_ASSISTANT) {
+ # Use .Where({}) method on the collection instead of | Where-Object ...
+ # | Select-Object pipelines: each pipeline is a thread-pool hop on
+ # PS 5.1 and the completed tasks accumulate over hours-long runs.
+ $hasTool = $null
+ $hasThink = $null
+ $hasText = $null
+ if ($m.content) {
+ foreach ($c in @($m.content)) {
+ if ($hasTool -eq $null -and $c.type -eq $S_TOOLCALL) { $hasTool = $c }
+ elseif ($hasThink -eq $null -and $c.type -eq $S_THINK_BLK) { $hasThink = $c }
+ elseif ($hasText -eq $null -and $c.type -eq $S_TEXT) { $hasText = $c }
+ if ($hasTool) { break } # toolCall wins; we can stop scanning
+ }
+ }
+
+ if ($hasTool) {
+ $tool = $hasTool.name
+ $args = $hasTool.arguments
+ if ($args) {
+ # ConvertTo-Json is a single .NET call; keep it (no pipeline leak).
+ $argsJson = $args | ConvertTo-Json -Compress -Depth 2 -WarningAction SilentlyContinue
+ if ($argsJson.Length -gt 60) { $argsJson = $argsJson.Substring(0, 57) + $DOTS }
+ } else { $argsJson = '' }
+ return @{ state=$S_WORKING; message="$tool : $argsJson" }
+ }
+ if ($hasThink -and -not $hasText) { return @{ state=$S_THINKING; message='' } }
+ if ($hasText) { return @{ state=$S_IDLE; message=$MSG_AGENT_DONE } }
+ return @{ state=$S_IDLE; message='' }
+ }
+
+ if ($role -eq $S_TOOLRESULT) {
+ $tool = $m.toolName
+ $isErr = $m.isError -eq $true
+ if ($isErr) { return @{ state=$S_ERROR; message="$tool $MSG_FAIL" } }
+ return @{ state=$S_DONE; message="$tool $MSG_OK" }
+ }
+
+ if ($role -eq $S_COMPACTION) { return @{ state=$S_IDLE; message=$MSG_COMPACT } }
+
+ return $null
+}
+
+function Write-Status($state, $message) {
+ $tmp = "$statusFile.tmp"
+ $payload = [PSCustomObject]@{
+ state = $state
+ message = $message
+ progress = -1
+ ts = (Get-Date).ToString($FMT_O)
+ source = $S_DETECTOR
+ } | ConvertTo-Json -Compress
+ [System.IO.File]::WriteAllText($tmp, $payload, [System.Text.Encoding]::UTF8)
+ Move-Item -Path $tmp -Destination $statusFile -Force
+}
+
+function Read-StatusObj {
+ if (!(Test-Path $statusFile)) { return $null }
+ try { return ([System.IO.File]::ReadAllText($statusFile) | ConvertFrom-Json) } catch { return $null }
+}
+
+# === 主循环 ===
+if (-not $Once) {
+ Set-Content -Path $pidFile -Value $PID -Encoding ASCII
+ Log-Line "detector started (PID $PID) mcodeRoot=$mcodeRoot"
+}
+
+try {
+ while ($true) {
+ $now = Get-Date
+ $inferred = $null
+ $latestFile = $null
+
+ # 1) mcode 进程健康:5s 缓存(避免每秒 Get-ChildItem + Get-Process)
+ if (($now - $script:lastMcodePidAt) -lt $CACHE_TTL) {
+ $mcodePid = $script:lastMcodePid
+ } else {
+ $mcodePid = Get-McodePid
+ $script:lastMcodePid = $mcodePid
+ $script:lastMcodePidAt = $now
+ }
+ if (-not $mcodePid) {
+ $inferred = @{ state=$S_ERROR; message=$MSG_MCODE_EXIT }
+ } else {
+ # 2) 最新 session log:5s 缓存(避免每秒 EnumerateFiles 几千个 .jsonl)
+ if (($now - $script:lastLatestFileAt) -lt $CACHE_TTL) {
+ $latestFile = $script:lastLatestFile
+ } else {
+ $latestFile = Get-LatestSessionFile
+ $script:lastLatestFile = $latestFile
+ $script:lastLatestFileAt = $now
+ }
+ if ($latestFile) {
+ $msg = Read-LastMessage $latestFile
+ if ($msg) {
+ $inferred = Infer-State $msg
+ }
+ }
+ }
+
+ # 3) 60s 无活动 → idle 兜底(每次循环都跑,不再被 mtime 缓存屏蔽)
+ if ($inferred -and $latestFile) {
+ $curState = [string]$inferred.state
+ $isSettled = ($curState -eq $S_IDLE) -or ($curState -eq $S_ERROR)
+ if (-not $isSettled) {
+ $age = ($now - [System.IO.File]::GetLastWriteTime($latestFile)).TotalSeconds
+ if ($age -gt 60) {
+ $inferred = @{ state=$S_IDLE; message=($MSG_IDLE_FMT -f [int]$age) }
+ }
+ }
+ }
+
+ if ($inferred) {
+ $cur = Read-StatusObj
+ $curSource = if ($cur) { [string]$cur.source } else { '' }
+ $curState = if ($cur) { [string]$cur.state } else { '' }
+ $newState = [string]$inferred.state
+ $newMsg = [string]$inferred.message
+
+ $isOwn = ($curSource -eq $S_DETECTOR)
+ $stateChanged = ($curState -ne $newState)
+ $isSettleNew = ($newState -eq $S_IDLE) -or ($newState -eq $S_ERROR)
+
+ $shouldWrite = $false
+ $reason = ''
+
+ if (-not $cur) {
+ $shouldWrite = $true
+ $reason = $R_NO_CURRENT
+ } elseif ($isOwn -and $stateChanged) {
+ $shouldWrite = $true
+ $reason = $R_OWN_CHANGED
+ } elseif ($isOwn -and ($cur.message -ne $newMsg) -and $newMsg) {
+ $shouldWrite = $true
+ $reason = $R_OWN_MSG
+ } elseif (-not $isOwn -and $isSettleNew) {
+ $shouldWrite = $true
+ $reason = $R_TAKEOVER
+ }
+
+ if ($shouldWrite) {
+ Write-Status $inferred.state $inferred.message
+ if ($script:lastDetectedState -ne $newState) {
+ Log-Line "$newState :: $newMsg ($reason)"
+ $script:lastDetectedState = $newState
+ }
+ }
+ }
+
+ if ($Once) { break }
+ Start-Sleep -Milliseconds 1000
+ }
+} finally {
+ if (-not $Once -and (Test-Path $pidFile)) {
+ Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
+ }
+ $STOPPED_MSG = _s (0x64,0x65,0x74,0x65,0x63,0x74,0x6F,0x72,0x20,0x73,0x74,0x6F,0x70,0x70,0x65,0x64)
+ Log-Line $STOPPED_MSG
+}
diff --git a/plugins/antianqi/mcode-island/notify-island.ps1 b/plugins/antianqi/mcode-island/notify-island.ps1
new file mode 100644
index 0000000..f20451e
--- /dev/null
+++ b/plugins/antianqi/mcode-island/notify-island.ps1
@@ -0,0 +1,149 @@
+# mcode 灵动岛 - 状态推送 + 调用方追踪
+# 用法:
+# notify-island.ps1 -State working -Message "..."
+#
+# 副作用:
+# 1. 写 status.json(widget 轮询)
+# 2. 写 caller.json(widget 点击切回时用)—— 记录调用方进程链和窗口句柄
+
+param(
+ [ValidateSet('idle','thinking','working','waiting','done','error')]
+ [string]$State = 'idle',
+ [string]$Message = '',
+ [int]$Progress = -1
+)
+
+$ErrorActionPreference = 'Stop'
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+chcp 65001 | Out-Null
+
+$configDir = Join-Path $env:APPDATA 'mcode-island'
+if (!(Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Force | Out-Null }
+$statusFile = Join-Path $configDir 'status.json'
+$callerFile = Join-Path $configDir 'caller.json'
+
+# 找当前 console 关联的窗口(这是 Windows Terminal 里那个具体 tab 的 HWND)
+Add-Type @"
+using System;
+using System.Runtime.InteropServices;
+public class ConsoleWin {
+ [DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();
+ [DllImport("kernel32.dll")] public static extern bool AttachConsole(uint dwProcessId);
+ [DllImport("kernel32.dll")] public static extern bool FreeConsole();
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
+}
+"@ -ErrorAction SilentlyContinue
+
+function Get-CallerFocusInfo {
+ # 1) 直接拿自己的 console window
+ $hwnd = [ConsoleWin]::GetConsoleWindow()
+ if ($hwnd -ne [IntPtr]::Zero) {
+ $ownerPid = 0
+ [ConsoleWin]::GetWindowThreadProcessId($hwnd, [ref]$ownerPid) | Out-Null
+ return @{
+ targetHwnd = [int64]$hwnd
+ targetPid = [int]$ownerPid
+ targetExe = (Get-Process -Id $ownerPid -ErrorAction SilentlyContinue).ProcessName
+ targetTitle = ''
+ source = 'console-window'
+ }
+ }
+
+ # 2) 临时附到父进程的 console(notify-island 自己是 NonInteractive 启动时)
+ $parentProc = Get-CimInstance Win32_Process -Filter "ProcessId=$PID" -ErrorAction SilentlyContinue
+ if ($parentProc -and $parentProc.ParentProcessId -and $parentProc.ParentProcessId -gt 0) {
+ # 沿链向上找有 console 的祖先
+ $current = [int]$parentProc.ParentProcessId
+ $visited = @()
+ for ($i = 0; $i -lt 6; $i++) {
+ if ($visited -contains $current) { break }
+ $visited += $current
+ $attached = [ConsoleWin]::AttachConsole([uint32]$current)
+ if ($attached) {
+ $hwnd = [ConsoleWin]::GetConsoleWindow()
+ [ConsoleWin]::FreeConsole() | Out-Null
+ if ($hwnd -ne [IntPtr]::Zero) {
+ $ownerPid = 0
+ [ConsoleWin]::GetWindowThreadProcessId($hwnd, [ref]$ownerPid) | Out-Null
+ return @{
+ targetHwnd = [int64]$hwnd
+ targetPid = [int]$ownerPid
+ targetExe = (Get-Process -Id $ownerPid -ErrorAction SilentlyContinue).ProcessName
+ targetTitle = ''
+ source = 'attached-console'
+ }
+ }
+ }
+ $gp = Get-CimInstance Win32_Process -Filter "ProcessId=$current" -ErrorAction SilentlyContinue
+ if (-not $gp -or -not $gp.ParentProcessId) { break }
+ $current = [int]$gp.ParentProcessId
+ }
+ }
+
+ # 3) fallback: 进程链里第一个有主窗口的祖先
+ $current = $PID
+ $visited = @()
+ for ($i = 0; $i -lt 6; $i++) {
+ if ($visited -contains $current) { return $null }
+ $visited += $current
+ $proc = Get-Process -Id $current -ErrorAction SilentlyContinue
+ if (-not $proc) { return $null }
+ if ([int64]$proc.MainWindowHandle -ne 0) {
+ return @{
+ targetHwnd = [int64]$proc.MainWindowHandle
+ targetPid = $proc.Id
+ targetExe = $proc.ProcessName
+ targetTitle = $proc.MainWindowTitle
+ source = 'parent-chain'
+ }
+ }
+ $parent = Get-CimInstance Win32_Process -Filter "ProcessId=$current" -ErrorAction SilentlyContinue
+ if (-not $parent -or -not $parent.ParentProcessId -or $parent.ParentProcessId -eq 0) { return $null }
+ $current = [int]$parent.ParentProcessId
+ }
+ return $null
+}
+
+$ts = (Get-Date).ToString('o')
+
+# 写 status.json(widget 轮询用)
+# 加 source='agent' 让 detector 识别这是 agent 主动推的,保留不被 working/thinking/done
+# 覆盖(detector 推 idle/error 兜底仍然可以接管,符合 takeover 语义)。
+$payload = [PSCustomObject]@{
+ state = $State
+ message = $Message
+ progress = $Progress
+ ts = $ts
+ source = 'agent'
+} | ConvertTo-Json -Compress
+
+# 写 caller.json(widget 点击切回用)
+$myPid = $PID
+$focusInfo = Get-CallerFocusInfo
+$callerData = [PSCustomObject]@{
+ callerPid = $myPid
+ callerSession = (Get-Process -Id $myPid).SessionId
+ targetPid = if ($focusInfo) { $focusInfo.targetPid } else { 0 }
+ targetHwnd = if ($focusInfo) { $focusInfo.targetHwnd } else { 0 }
+ targetTitle = if ($focusInfo) { $focusInfo.targetTitle } else { '' }
+ targetExe = if ($focusInfo) { $focusInfo.targetExe } else { '' }
+ source = if ($focusInfo) { $focusInfo.source } else { 'none' }
+ ts = $ts
+}
+
+# 原子写
+$tmpStatus = "$statusFile.tmp"
+$tmpCaller = "$callerFile.tmp"
+try {
+ [System.IO.File]::WriteAllText($tmpStatus, $payload, [System.Text.Encoding]::UTF8)
+ Move-Item -Path $tmpStatus -Destination $statusFile -Force
+ [System.IO.File]::WriteAllText($tmpCaller, ($callerData | ConvertTo-Json -Compress), [System.Text.Encoding]::UTF8)
+ Move-Item -Path $tmpCaller -Destination $callerFile -Force
+ Write-Output "OK: $State - $Message"
+ if ($focusInfo) {
+ Write-Output (" (caller via " + $focusInfo.source + ": " + $focusInfo.targetExe + " PID=" + $focusInfo.targetPid + " hwnd=" + $focusInfo.targetHwnd + ")")
+ }
+} catch {
+ Write-Output "FAIL: $($_.Exception.Message)"
+ exit 1
+}
diff --git a/plugins/antianqi/mcode-island/pin-island.ps1 b/plugins/antianqi/mcode-island/pin-island.ps1
new file mode 100644
index 0000000..36c5f98
--- /dev/null
+++ b/plugins/antianqi/mcode-island/pin-island.ps1
@@ -0,0 +1,53 @@
+# mcode 灵动岛 - 把当前前台窗口 pin 为点击目标
+# 在 mcode 终端里直接运行:mcode-island pin
+# 效果:把当前前台窗口(也就是 mcode 这个 tab)的 HWND 记下来
+# 之后点 pill,会稳稳切回这里
+
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+Add-Type @"
+using System;
+using System.Runtime.InteropServices;
+public class PinW {
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
+}
+"@ -ErrorAction SilentlyContinue
+
+$hwnd = [PinW]::GetForegroundWindow()
+if ($hwnd -eq [IntPtr]::Zero) {
+ Write-Output "FAIL: 当前没有前台窗口"
+ exit 1
+}
+$ownerPid = 0
+[PinW]::GetWindowThreadProcessId($hwnd, [ref]$ownerPid) | Out-Null
+$proc = Get-Process -Id $ownerPid -ErrorAction SilentlyContinue
+if (-not $proc) {
+ Write-Output "FAIL: 前台窗口进程已退出"
+ exit 1
+}
+
+$configDir = Join-Path $env:APPDATA 'mcode-island'
+if (!(Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Force | Out-Null }
+$callerFile = Join-Path $configDir 'caller.json'
+
+$myPid = [int](Get-Process -Id $PID).Id
+$mySession = (Get-Process -Id $myPid).SessionId
+$payload = [PSCustomObject]@{
+ callerPid = $myPid
+ callerSession = $mySession
+ targetPid = $proc.Id
+ targetHwnd = [int64]$hwnd
+ targetTitle = $proc.MainWindowTitle
+ targetExe = $proc.ProcessName
+ source = 'cli-pin'
+ ts = (Get-Date).ToString('o')
+} | ConvertTo-Json -Compress
+
+$tmpFile = "$callerFile.tmp"
+[System.IO.File]::WriteAllText($tmpFile, $payload, [System.Text.Encoding]::UTF8)
+Move-Item -Path $tmpFile -Destination $callerFile -Force
+
+Write-Output ("OK: pin'd " + $proc.ProcessName + " (PID=" + $proc.Id + ", hwnd=" + $hwnd + ")")
+Write-Output (" title: " + $proc.MainWindowTitle)
+Write-Output " 之后点 pill 就切回这个窗口"
diff --git a/plugins/antianqi/mcode-island/plugin.json b/plugins/antianqi/mcode-island/plugin.json
new file mode 100644
index 0000000..f32b034
--- /dev/null
+++ b/plugins/antianqi/mcode-island/plugin.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
+ "name": "mcode-island",
+ "version": "0.2.0",
+ "description": "Windows 桌面灵动岛 (Dynamic Island) 状态窗口:让 mcode agent 把工作状态(idle/thinking/working/waiting/done/error)实时推送到屏幕顶部悬浮 pill,agent 自己忙的时候用户不用切回 mcode 也能看到进度。",
+ "author": {
+ "name": "antianqi",
+ "url": "https://github.com/antianqi"
+ },
+ "homepage": "https://github.com/MiniMax-AI/MiniMax-Code-Plugins/tree/main/plugins/antianqi/mcode-island",
+ "repository": "https://github.com/MiniMax-AI/MiniMax-Code-Plugins/tree/main/plugins/antianqi/mcode-island",
+ "license": "Apache-2.0",
+ "keywords": [
+ "mcode",
+ "windows",
+ "wpf",
+ "powershell",
+ "status",
+ "ui",
+ "dynamic-island"
+ ]
+}
diff --git a/plugins/antianqi/mcode-island/show-island.ps1 b/plugins/antianqi/mcode-island/show-island.ps1
new file mode 100644
index 0000000..ab963a7
--- /dev/null
+++ b/plugins/antianqi/mcode-island/show-island.ps1
@@ -0,0 +1,30 @@
+# mcode 灵动岛 - 把隐藏的 widget 重新叫出来
+# 用法:mcode-island show
+
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+# 看 widget 在不在
+$pidFile = Join-Path $env:APPDATA 'mcode-island\widget.pid'
+$widgetPid = (Get-Content $pidFile -ErrorAction SilentlyContinue) -as [int]
+if (-not $widgetPid -or $widgetPid -le 0) {
+ Write-Output 'NOT RUNNING: widget 没在跑,先 mcode-island start'
+ exit 0
+}
+$proc = Get-Process -Id $widgetPid -ErrorAction SilentlyContinue
+if (-not $proc -or $proc.HasExited) {
+ Write-Output "NOT RUNNING: PID $widgetPid 是死的,先 mcode-island start"
+ exit 0
+}
+
+# 创建 show 信号(widget 每 400ms 轮询,看到这个就 Show())
+$configDir = Join-Path $env:APPDATA 'mcode-island'
+if (!(Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Force | Out-Null }
+$signalFile = Join-Path $configDir 'show.signal'
+
+try {
+ [System.IO.File]::WriteAllText($signalFile, (Get-Date).ToString('o'), [System.Text.Encoding]::ASCII)
+ Write-Output "OK: signal sent (widget PID $widgetPid should show up within 1s)"
+} catch {
+ Write-Output "FAIL: $($_.Exception.Message)"
+ exit 1
+}
diff --git a/plugins/antianqi/mcode-island/skills/mcode-island/SKILL.md b/plugins/antianqi/mcode-island/skills/mcode-island/SKILL.md
new file mode 100644
index 0000000..b906066
--- /dev/null
+++ b/plugins/antianqi/mcode-island/skills/mcode-island/SKILL.md
@@ -0,0 +1,154 @@
+---
+name: mcode-island
+description: Push the user's terminal out of focus to a Windows desktop Dynamic Island pill so the user can watch your work without switching back to mcode. Use when starting long-running bash/edit/read operations, when a tool needs user approval (waiting), on success (done), or on failure (error). Pair every agent bash / read / write / edit call with a corresponding `notify-island.ps1` state push.
+license: Apache-2.0
+compatibility: Requires Windows 10/11 with PowerShell 5.1+ and the mcode-island widget running (started via `mcode-island start` or `autostart.ps1 -Enable`).
+metadata:
+ author: antianqi
+ version: "0.1.0"
+---
+
+# mcode-island — 桌面灵动岛状态通知
+
+让用户在不切回 mcode 窗口的情况下,从桌面顶部悬浮 pill 上看到你(agent)的实时工作状态。
+
+## What it looks like
+
+A 320×60 pill anchored to the top center of the primary display, always-on-top, dark
+theme. Six states with distinct color and motion:
+
+| state | color | icon | meaning |
+| ---------- | -------------- | ---- | -------------------------------- |
+| `idle` | gray | — | waiting for user input |
+| `thinking` | yellow pulse | — | reasoning, no tool call yet |
+| `working` | blue pulse | ⚙ | actively running a tool |
+| `waiting` | orange | ? | tool needs user approval / input |
+| `done` | green | ✓ | step finished, more to do |
+| `error` | red | ✕ | tool or step failed |
+
+Click the pill to switch focus back to the originating terminal tab. Run
+`mcode-island pin` from inside a terminal to fix the focus target explicitly
+(useful when the auto-detected HWND is wrong, e.g. Windows Terminal multi-tab).
+
+## When to push each state
+
+| moment | state | example message |
+| ----------------------------------------------------- | --------- | ------------------------------ |
+| receive user task, start reasoning | `thinking`| (none) |
+| about to invoke any tool | `working` | `"bash: npm test"` |
+| tool returned 0, before reporting back | `done` | `"3 files modified"` |
+| tool needs approval (e.g. permission prompt) | `waiting` | `"bash: needs approval"` |
+| tool failed / threw / non-zero exit | `error` | `"compile failed: missing import"` |
+| conversation idle, waiting for user | `idle` | (none) |
+
+**Never push the same state twice in a row** — the widget de-duplicates by
+state+message. Push only on transitions, or include a fresh message each time.
+
+## Copyable example (agent side)
+
+The plugin ships a thin wrapper `wrap-tool.ps1` that **publishes state only**
+(it does NOT execute the command). Run the command via mcode's own bash tool,
+then call `wrap-tool.ps1` to publish the outcome:
+
+```powershell
+# Step 1: announce "working" before invoking mcode's bash tool
+& "\wrap-tool.ps1" -Tool bash -Command "npm test" -Description "run tests"
+
+# Step 2: after mcode's bash tool returns, publish the outcome
+& "\wrap-tool.ps1" -Tool bash -Command "npm test" -ExitCode $LASTEXITCODE
+```
+
+`$LASTEXITCODE` is interpreted as: `0` → `done`, codes in `-WaitingExitCodes`
+(default `[1]`) → `waiting`, anything else → `error`. The wrapper returns the
+exit code unchanged so the calling shell still sees it.
+
+For other tools (read/write/edit) — and for any state push that is not a single
+command — call `notify-island.ps1` directly:
+
+```powershell
+$plugin = "" # directory that contains notify-island.ps1
+& "$plugin\notify-island.ps1" -State thinking
+& "$plugin\notify-island.ps1" -State working -Message "read source tree"
+& "$plugin\notify-island.ps1" -State done -Message "indexed 142 files"
+& "$plugin\notify-island.ps1" -State error -Message "compile failed: missing import"
+& "$plugin\notify-island.ps1" -State waiting -Message "permission prompt"
+```
+
+`` is the directory that contains `notify-island.ps1`.
+Substitute the absolute path your user installed the plugin at. The Skill body
+deliberately avoids hard-coded paths so any user / any install location works.
+
+## 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+F4 hides it, not closes
+it, and `mcode-island show` re-raises the hidden window in under 1 second.
+
+## User-side management
+
+```cmd
+mcode-island REM start the widget (idempotent)
+mcode-island stop REM stop the widget
+mcode-island status REM show PID + recent log
+mcode-island show REM re-raise hidden window
+mcode-island pin REM lock focus target to current foreground window
+mcode-island unpin REM clear focus target
+mcode-island autostart-on REM register for Windows logon
+mcode-island autostart-off REM unregister
+```
+
+To enable login auto-start, the user runs once:
+
+```powershell
+& "\autostart.ps1" -Enable
+```
+
+This writes to `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` — no admin
+rights required.
+
+## Runtime data
+
+All widget state lives under `%APPDATA%\mcode-island\`:
+
+| file | purpose |
+| ---------------- | ----------------------------------------------------- |
+| `status.json` | current state (widget polls this every 400 ms) |
+| `caller.json` | originating terminal HWND / PID (for click-to-focus) |
+| `config.json` | pill position, size, opacity (saved on drag) |
+| `widget.pid` | widget process PID (used by start/stop/status) |
+| `island.log` | append-only state transition history |
+| `widget.log` | widget internal debug log |
+| `show.signal` | transient file written by `mcode-island show` |
+
+No data leaves the local machine. The plugin does not make any network request.
+
+## What is in this package
+
+```
+mcode-island/
+├── plugin.json # plugin manifest
+├── README.md # full user-facing docs
+├── LICENSE # Apache-2.0
+├── mcode-island.ps1 # WPF widget main loop
+├── mcode-island.cmd # CLI shim (start/stop/status/...)
+├── start-island.ps1 # launch the widget in STA
+├── stop-island.ps1 # stop the widget
+├── status-island.ps1 # print widget state
+├── show-island.ps1 # re-raise hidden widget
+├── pin-island.ps1 # lock focus target to foreground
+├── autostart.ps1 # register/unregister Windows logon
+├── notify-island.ps1 # state-push helper (agents call this)
+├── wrap-tool.ps1 # all-in-one bash wrapper
+├── skills/mcode-island/SKILL.md # this file
+└── assets/ # screenshots used in the README
+```
+
+## Limitations and known constraints
+
+- Windows 10/11 only (uses WPF and `presentationframework`).
+- Single widget per user session.
+- No hover-expand, no media-control integration yet — see the `v0.2` roadmap in
+ the upstream issue tracker.
+- `wrap-tool.ps1` only wraps `bash`. For `read` / `write` / `edit` the agent
+ must call `notify-island.ps1` itself before and after the tool call.
diff --git a/plugins/antianqi/mcode-island/start-detect-island.ps1 b/plugins/antianqi/mcode-island/start-detect-island.ps1
new file mode 100644
index 0000000..e86b46d
--- /dev/null
+++ b/plugins/antianqi/mcode-island/start-detect-island.ps1
@@ -0,0 +1,32 @@
+# 启动 detector 守护进程(独立进程跑 mcode-status-detect.ps1)
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+$configDir = Join-Path $env:APPDATA 'mcode-island'
+if (!(Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Force | Out-Null }
+$pidFile = Join-Path $configDir 'detect.pid'
+$detectScript = Join-Path $PSScriptRoot 'mcode-status-detect.ps1'
+
+# 防重复:校验已有 PID 是否真在跑 detector
+function Test-IsDetectorProcess($proc, $detectPath) {
+ if (-not $proc -or $proc.HasExited) { return $false }
+ $cmd = (Get-CimInstance Win32_Process -Filter "ProcessId=$($proc.Id)" -ErrorAction SilentlyContinue).CommandLine
+ if (-not $cmd) { return $false }
+ return $cmd.IndexOf($detectPath, [System.StringComparison]::OrdinalIgnoreCase) -ge 0
+}
+
+if (Test-Path $pidFile) {
+ $old = (Get-Content $pidFile -ErrorAction SilentlyContinue) -as [int]
+ if ($old -and $old -gt 0) {
+ $existing = Get-Process -Id $old -ErrorAction SilentlyContinue
+ if ($existing -and (Test-IsDetectorProcess $existing $detectScript)) {
+ Write-Output ("detector already running (PID " + $old + ")")
+ exit 0
+ }
+ Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
+ }
+}
+
+$args = @('-NoProfile', '-STA', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass', '-File', "`"$detectScript`"")
+$proc = Start-Process powershell.exe -ArgumentList $args -PassThru
+Set-Content -Path $pidFile -Value $proc.Id -Encoding ASCII
+Write-Output ("detector started (PID " + $proc.Id + ")")
diff --git a/plugins/antianqi/mcode-island/start-island.ps1 b/plugins/antianqi/mcode-island/start-island.ps1
new file mode 100644
index 0000000..c0b47a6
--- /dev/null
+++ b/plugins/antianqi/mcode-island/start-island.ps1
@@ -0,0 +1,61 @@
+# mcode 灵动岛 - 启动器
+# 用法:双击这个脚本,或命令行执行
+# 效果:在新进程中以 STA 模式启动 widget,自身立即退出,不留黑窗
+# 副作用:把 widget 的 PID 写到 %APPDATA%\mcode-island\widget.pid(status/stop 用)
+
+$ErrorActionPreference = 'Stop'
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+$configDir = Join-Path $env:APPDATA 'mcode-island'
+if (!(Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Force | Out-Null }
+
+$widget = Join-Path $PSScriptRoot 'mcode-island.ps1'
+$pidFile = Join-Path $configDir 'widget.pid'
+
+# 防止重复启动:先校验已有 PID 是否真的在跑 widget(不是 PID 复用)
+function Test-IsWidgetProcess($proc, $widgetPath) {
+ if (-not $proc -or $proc.HasExited) { return $false }
+ $cmd = (Get-CimInstance Win32_Process -Filter "ProcessId=$($proc.Id)" -ErrorAction SilentlyContinue).CommandLine
+ if (-not $cmd) { return $false }
+ # CommandLine contains the widget script path (case-insensitive)
+ return $cmd.IndexOf($widgetPath, [System.StringComparison]::OrdinalIgnoreCase) -ge 0
+}
+
+if (Test-Path $pidFile) {
+ $old = Get-Content $pidFile -ErrorAction SilentlyContinue
+ if ($old) {
+ $existing = Get-Process -Id ([int]$old) -ErrorAction SilentlyContinue
+ if ($existing -and (Test-IsWidgetProcess $existing $widget)) {
+ Write-Output ("已在运行(PID " + $old + ")")
+ exit 0
+ }
+ # Stale PID file (process dead or PID reused) — clear and continue
+ Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
+ }
+}
+
+# 新进程启动 widget
+$args = @('-NoProfile', '-STA', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass', '-File', "`"$widget`"")
+$proc = Start-Process powershell.exe -ArgumentList $args -PassThru
+
+# 写 PID(先写,后面 status / stop 都靠这个)
+Set-Content -Path $pidFile -Value $proc.Id -Encoding ASCII
+
+# 等待 widget 起来(最多 8 秒),用 .ps1 已加载 WPF 窗口作为 readiness 信号
+$widgetWindowClass = 'HwndWrapper[DefaultDomain*' # WPF 窗口类名前缀
+$ok = $false
+for ($i = 0; $i -lt 16; $i++) {
+ Start-Sleep -Milliseconds 500
+ try {
+ $p = Get-Process -Id $proc.Id -ErrorAction Stop
+ if ($p.HasExited) { break }
+ # WPF widget 启动后会创建主窗口;MainWindowHandle 非 0 即就绪
+ if ($p.MainWindowHandle -ne 0) { $ok = $true; break }
+ } catch {}
+}
+
+if ($ok) {
+ Write-Output ("mcode-island 已启动 (PID " + $proc.Id + ",已就绪)")
+} else {
+ Write-Output ("mcode-island 已启动 (PID " + $proc.Id + ",等待中...)")
+}
diff --git a/plugins/antianqi/mcode-island/status-detect-island.ps1 b/plugins/antianqi/mcode-island/status-detect-island.ps1
new file mode 100644
index 0000000..6e7d9e1
--- /dev/null
+++ b/plugins/antianqi/mcode-island/status-detect-island.ps1
@@ -0,0 +1,17 @@
+# 看 detector 状态
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+$pidFile = Join-Path $env:APPDATA 'mcode-island\detect.pid'
+if (!(Test-Path $pidFile)) { Write-Output 'NOT RUNNING'; exit 0 }
+$targetPid = (Get-Content $pidFile -ErrorAction SilentlyContinue) -as [int]
+$p = Get-Process -Id $targetPid -ErrorAction SilentlyContinue
+if ($p -and -not $p.HasExited) {
+ Write-Output ("RUNNING PID=" + $p.Id + " StartTime=" + $p.StartTime.ToString('HH:mm:ss'))
+ $sf = Join-Path $env:APPDATA 'mcode-island\status.json'
+ if (Test-Path $sf) {
+ Write-Output '--- last status.json ---'
+ Get-Content $sf -Raw
+ }
+} else {
+ Write-Output ("NOT RUNNING (stale pid " + $targetPid + ")")
+}
diff --git a/plugins/antianqi/mcode-island/status-island.ps1 b/plugins/antianqi/mcode-island/status-island.ps1
new file mode 100644
index 0000000..e71cf37
--- /dev/null
+++ b/plugins/antianqi/mcode-island/status-island.ps1
@@ -0,0 +1,26 @@
+# 看 widget 状态
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+$pidFile = Join-Path $env:APPDATA 'mcode-island\widget.pid'
+if (!(Test-Path $pidFile)) {
+ Write-Output 'NOT RUNNING (no pid file)'
+ exit 0
+}
+
+$widgetPid = (Get-Content $pidFile -ErrorAction SilentlyContinue) -as [int]
+if (-not $widgetPid -or $widgetPid -le 0) {
+ Write-Output 'NOT RUNNING (invalid pid)'
+ exit 0
+}
+
+$proc = Get-Process -Id $widgetPid -ErrorAction SilentlyContinue
+if ($proc -and -not $proc.HasExited) {
+ Write-Output ("RUNNING PID=" + $proc.Id + " Session=" + $proc.SessionId + " StartTime=" + $proc.StartTime.ToString('HH:mm:ss'))
+ $logFile = Join-Path $env:APPDATA 'mcode-island\island.log'
+ if (Test-Path $logFile) {
+ Write-Output '--- recent log ---'
+ Get-Content $logFile -Tail 5 -Encoding UTF8
+ }
+} else {
+ Write-Output ("NOT RUNNING (stale pid $widgetPid)")
+}
diff --git a/plugins/antianqi/mcode-island/stop-detect-island.ps1 b/plugins/antianqi/mcode-island/stop-detect-island.ps1
new file mode 100644
index 0000000..fee98d7
--- /dev/null
+++ b/plugins/antianqi/mcode-island/stop-detect-island.ps1
@@ -0,0 +1,23 @@
+# 停掉 detector(校验 PID + cmdline 防误杀)
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+$pidFile = Join-Path $env:APPDATA 'mcode-island\detect.pid'
+$detectScript = 'mcode-status-detect.ps1' # cmdline 匹配
+if (!(Test-Path $pidFile)) { Write-Output 'NOT RUNNING'; exit 0 }
+
+$targetPid = (Get-Content $pidFile -ErrorAction SilentlyContinue) -as [int]
+if ($targetPid -and $targetPid -gt 0) {
+ $p = Get-Process -Id $targetPid -ErrorAction SilentlyContinue
+ if ($p -and -not $p.HasExited) {
+ $cmd = (Get-CimInstance Win32_Process -Filter "ProcessId=$targetPid" -ErrorAction SilentlyContinue).CommandLine
+ if ($cmd -and $cmd.IndexOf($detectScript, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
+ Stop-Process -Id $targetPid -Force
+ Write-Output ("stopped PID " + $targetPid)
+ } else {
+ Write-Output ("REFUSED: PID " + $targetPid + " is alive but not the detector (PID reuse suspected). Refusing to kill.")
+ }
+ } else {
+ Write-Output ("NOT RUNNING (stale pid " + $targetPid + ")")
+ }
+}
+Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
diff --git a/plugins/antianqi/mcode-island/stop-island.ps1 b/plugins/antianqi/mcode-island/stop-island.ps1
new file mode 100644
index 0000000..c5a803e
--- /dev/null
+++ b/plugins/antianqi/mcode-island/stop-island.ps1
@@ -0,0 +1,32 @@
+# 停掉 widget
+# 校验 PID 文件 + cmdline,防止误杀 PID 复用的不相关进程
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+$configDir = Join-Path $env:APPDATA 'mcode-island'
+$pidFile = Join-Path $configDir 'widget.pid'
+$widgetScript = 'mcode-island.ps1' # 用于 cmdline 匹配
+
+if (!(Test-Path $pidFile)) {
+ Write-Output 'NOT RUNNING'
+ exit 0
+}
+
+$widgetPid = (Get-Content $pidFile -ErrorAction SilentlyContinue) -as [int]
+if ($widgetPid -and $widgetPid -gt 0) {
+ $proc = Get-Process -Id $widgetPid -ErrorAction SilentlyContinue
+ if ($proc -and !$proc.HasExited) {
+ # 验证 cmdline 含 widget 脚本名(防 PID 复用)
+ $cmd = (Get-CimInstance Win32_Process -Filter "ProcessId=$widgetPid" -ErrorAction SilentlyContinue).CommandLine
+ if ($cmd -and $cmd.IndexOf($widgetScript, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
+ Stop-Process -Id $widgetPid -Force
+ Write-Output ("stopped PID " + $widgetPid)
+ } else {
+ Write-Output ("REFUSED: PID " + $widgetPid + " is alive but not the widget (PID reuse suspected). Refusing to kill.")
+ }
+ } else {
+ Write-Output ("NOT RUNNING (stale pid " + $widgetPid + ")")
+ }
+} else {
+ Write-Output 'NOT RUNNING'
+}
+Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
diff --git a/plugins/antianqi/mcode-island/wrap-tool.ps1 b/plugins/antianqi/mcode-island/wrap-tool.ps1
new file mode 100644
index 0000000..77960b5
--- /dev/null
+++ b/plugins/antianqi/mcode-island/wrap-tool.ps1
@@ -0,0 +1,64 @@
+# mcode 灵动岛 - 工具调用 wrapper (v0.2.1: status-only)
+# 用法(在 mcode 里):
+# wrap-tool.ps1 -Tool bash -Command "npm test" # 推 working
+# wrap-tool.ps1 -Tool bash -Command "npm test" -ExitCode 0 # 推 done
+# wrap-tool.ps1 -Tool bash -Command "npm test" -ExitCode 1 # 推 waiting
+# wrap-tool.ps1 -Tool bash -Command "npm test" -ExitCode 2 # 推 error
+#
+# 设计:v0.2.1 改为"只发通知不执行命令"。v0.1 的 Invoke-Expression 在 review 中
+# 被指出 injection 风险(任意 PS 代码执行)。现在 agent 自己在 mcode bash tool
+# 里跑命令,然后调本脚本只发状态,shell 边界由 mcode 自己处理。
+#
+# 行为:
+# 1. 推 working -Message " " (如果没传 -ExitCode)
+# 2. 推 done/error/waiting based on -ExitCode (如果传了 -ExitCode)
+# 3. 退出码原样返回(done→0, waiting→waiting-exit-code, error→error-exit-code)
+
+param(
+ [Parameter(Mandatory=$true)]
+ [ValidateSet('bash','read','write','edit','glob','grep','web','task','notebook')]
+ [string]$Tool,
+
+ [string]$Command = '',
+ [string]$Path = '',
+ [string]$Pattern = '',
+ [string]$Glob = '',
+ [string]$Description = '',
+ [int]$ExitCode = -1, # -1 = not yet run, push working; 0/非0 = push done/waiting/error
+ [int[]]$WaitingExitCodes = @(1) # 这些退出码视为 "waiting"(需要审批)
+)
+
+$ErrorActionPreference = 'Continue'
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+chcp 65001 > $null
+
+$notify = Join-Path $PSScriptRoot 'notify-island.ps1'
+
+# 拼一个"简述"显示在 widget 上
+$brief = $Description
+if (-not $brief) {
+ if ($Command) { $brief = ($Command.Substring(0, [Math]::Min(60, $Command.Length))) }
+ elseif ($Path) { $brief = $Path }
+ elseif ($Pattern) { $brief = $Pattern }
+ elseif ($Glob) { $brief = $Glob }
+ else { $brief = '' }
+}
+$brief = $brief -replace "`r`n", ' ' -replace "`n", ' '
+
+if ($ExitCode -lt 0) {
+ # 还没跑完:推 working
+ & $notify -State working -Message "$Tool : $brief" | Out-Null
+ exit 0
+}
+
+# 跑完了:根据退出码推 done/waiting/error
+if ($ExitCode -eq 0) {
+ & $notify -State done -Message "$Tool 完成" | Out-Null
+ exit 0
+} elseif ($WaitingExitCodes -contains $ExitCode) {
+ & $notify -State waiting -Message "$Tool 等待审批 (exit=$ExitCode)" | Out-Null
+ exit $ExitCode
+} else {
+ & $notify -State error -Message "$Tool 失败 (exit=$ExitCode)" | Out-Null
+ exit $ExitCode
+}