From cd840a0599ba13462b89d4fb3311009224c45960 Mon Sep 17 00:00:00 2001 From: antianqi <75944423+antianqi@users.noreply.github.com> Date: Sat, 22 Aug 2026 20:34:22 +0800 Subject: [PATCH 1/3] Add mcode-island plugin: Windows Dynamic Island status pill for MiniMax 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. --- plugins/antianqi/mcode-island/LICENSE | 190 +++++++ plugins/antianqi/mcode-island/README.md | 218 ++++++++ .../mcode-island/assets/state-done.png | Bin 0 -> 6894 bytes .../mcode-island/assets/state-error.png | Bin 0 -> 6758 bytes .../mcode-island/assets/state-idle.png | Bin 0 -> 6010 bytes .../mcode-island/assets/state-thinking.png | Bin 0 -> 6691 bytes .../mcode-island/assets/state-waiting.png | Bin 0 -> 15746 bytes .../mcode-island/assets/state-working.png | Bin 0 -> 7314 bytes .../mcode-island/assets/wrap-demo.png | Bin 0 -> 4372 bytes plugins/antianqi/mcode-island/autostart.ps1 | 45 ++ .../antianqi/mcode-island/mcode-island.cmd | 55 ++ .../antianqi/mcode-island/mcode-island.ps1 | 509 ++++++++++++++++++ .../antianqi/mcode-island/notify-island.ps1 | 146 +++++ plugins/antianqi/mcode-island/pin-island.ps1 | 53 ++ plugins/antianqi/mcode-island/plugin.json | 22 + plugins/antianqi/mcode-island/show-island.ps1 | 30 ++ .../mcode-island/skills/mcode-island/SKILL.md | 148 +++++ .../antianqi/mcode-island/start-island.ps1 | 52 ++ .../antianqi/mcode-island/status-island.ps1 | 26 + plugins/antianqi/mcode-island/stop-island.ps1 | 22 + plugins/antianqi/mcode-island/wrap-tool.ps1 | 79 +++ 21 files changed, 1595 insertions(+) create mode 100644 plugins/antianqi/mcode-island/LICENSE create mode 100644 plugins/antianqi/mcode-island/README.md create mode 100644 plugins/antianqi/mcode-island/assets/state-done.png create mode 100644 plugins/antianqi/mcode-island/assets/state-error.png create mode 100644 plugins/antianqi/mcode-island/assets/state-idle.png create mode 100644 plugins/antianqi/mcode-island/assets/state-thinking.png create mode 100644 plugins/antianqi/mcode-island/assets/state-waiting.png create mode 100644 plugins/antianqi/mcode-island/assets/state-working.png create mode 100644 plugins/antianqi/mcode-island/assets/wrap-demo.png create mode 100644 plugins/antianqi/mcode-island/autostart.ps1 create mode 100644 plugins/antianqi/mcode-island/mcode-island.cmd create mode 100644 plugins/antianqi/mcode-island/mcode-island.ps1 create mode 100644 plugins/antianqi/mcode-island/notify-island.ps1 create mode 100644 plugins/antianqi/mcode-island/pin-island.ps1 create mode 100644 plugins/antianqi/mcode-island/plugin.json create mode 100644 plugins/antianqi/mcode-island/show-island.ps1 create mode 100644 plugins/antianqi/mcode-island/skills/mcode-island/SKILL.md create mode 100644 plugins/antianqi/mcode-island/start-island.ps1 create mode 100644 plugins/antianqi/mcode-island/status-island.ps1 create mode 100644 plugins/antianqi/mcode-island/stop-island.ps1 create mode 100644 plugins/antianqi/mcode-island/wrap-tool.ps1 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..14efacb --- /dev/null +++ b/plugins/antianqi/mcode-island/README.md @@ -0,0 +1,218 @@ +# 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 | +|:----:|:--------:|:-------:|:-------:|:----:|:-----:| +| ![idle](assets/state-idle.png) | ![thinking](assets/state-thinking.png) | ![working](assets/state-working.png) | ![waiting](assets/state-waiting.png) | ![done](assets/state-done.png) | ![error](assets/state-error.png) | + +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 one-stop wrapper: it pushes `working` before +the command, then `done` / `error` / `waiting` based on `$LASTEXITCODE`, all +without changing the command's exit code: + +```powershell +& "%PLUGIN_DIR%\mcode-island\wrap-tool.ps1" ` + -Tool bash -Command "npm test" -Description "run tests" +``` + +## 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" -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`. + +## 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 0000000000000000000000000000000000000000..b49a138283c1291566c9820e3218008b1c97765c GIT binary patch literal 6894 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a3da040bdc662269303d19121725b56c6a082368 GIT binary patch literal 6758 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..23a9a16393d8172775713c7bbf6dc69c48ba3834 GIT binary patch literal 15746 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≻A?BLVWXQ|w_ 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2c51aab37a1cb1c37c3e70fe49bffb004ffe060b GIT binary patch literal 7314 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 Lh8m^nj>!K3i;Zdl literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d6087d9a7b2e707ba13a6c07452b21b52a3606bc GIT binary patch literal 4372 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 literal 0 HcmV?d00001 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/notify-island.ps1 b/plugins/antianqi/mcode-island/notify-island.ps1 new file mode 100644 index 0000000..67e443f --- /dev/null +++ b/plugins/antianqi/mcode-island/notify-island.ps1 @@ -0,0 +1,146 @@ +# 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 轮询用) +$payload = [PSCustomObject]@{ + state = $State + message = $Message + progress = $Progress + ts = $ts +} | 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..68d71b6 --- /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.1.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..1bc4920 --- /dev/null +++ b/plugins/antianqi/mcode-island/skills/mcode-island/SKILL.md @@ -0,0 +1,148 @@ +--- +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 handles state transitions +automatically. Wrap every `bash` invocation through it: + +```powershell +# Instead of: bash "npm test" +& "$PSScriptRoot\wrap-tool.ps1" -Tool bash -Command "npm test" -Description "run tests" + +# state flow this triggers: working("bash: run tests") → done("bash 完成") | error(...) | waiting(...) +``` + +For other tools (read/write/edit) — and for any state push that is not a single +command — call `notify-island.ps1` directly: + +```powershell +$plugin = Split-Path -Parent $PSScriptRoot # +& "$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-island.ps1 b/plugins/antianqi/mcode-island/start-island.ps1 new file mode 100644 index 0000000..8cc02a1 --- /dev/null +++ b/plugins/antianqi/mcode-island/start-island.ps1 @@ -0,0 +1,52 @@ +# 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' + +# 防止重复启动 +if (Test-Path $pidFile) { + $oldPid = Get-Content $pidFile -ErrorAction SilentlyContinue + if ($oldPid -and (Get-Process -Id $oldPid -ErrorAction SilentlyContinue)) { + Write-Output ("已在运行(PID $oldPid)") + exit 0 + } else { + 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 秒) +$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 } + $log = Join-Path $configDir 'widget.log' + if ((Test-Path $log) -and ((Get-Item $log).Length -gt 0)) { + $content = Get-Content $log -Tail 5 -ErrorAction SilentlyContinue + if ($content -match 'about to ShowDialog') { $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-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-island.ps1 b/plugins/antianqi/mcode-island/stop-island.ps1 new file mode 100644 index 0000000..ef12e95 --- /dev/null +++ b/plugins/antianqi/mcode-island/stop-island.ps1 @@ -0,0 +1,22 @@ +# 停掉 widget +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +$pidFile = Join-Path $env:APPDATA 'mcode-island\widget.pid' +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) { + Stop-Process -Id $widgetPid -Force + Write-Output "stopped PID $widgetPid" + } 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..62f235b --- /dev/null +++ b/plugins/antianqi/mcode-island/wrap-tool.ps1 @@ -0,0 +1,79 @@ +# mcode 灵动岛 - 工具调用 wrapper +# 用法(在 mcode 里): +# wrap-tool.ps1 -Tool "bash" -Command "npm test" +# wrap-tool.ps1 -Tool "read" -Path "C:\foo.txt" +# wrap-tool.ps1 -Tool "write" -Path "..." -Command "..." +# wrap-tool.ps1 -Tool "edit" -Path "..." -Command "..." +# +# 行为: +# 1. 推 working -Message " " +# 2. 执行命令 +# 3. 根据退出码推 done (0) / error (≠0) / waiting (特殊) +# 4. 退出码原样返回(不吞) + +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[]]$WaitingExitCodes = @(1) # 这些退出码视为 "waiting"(需要审批) +) + +$ErrorActionPreference = 'Continue' +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +chcp 65001 > $null + +$notify = Join-Path $PSScriptRoot 'notify-island.ps1' + +# 1) 拼一个"简述"显示在 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", ' ' + +# 2) 推 working +& $notify -State working -Message "$Tool : $brief" | Out-Null + +# 3) 执行命令(这里只支持 bash,其他工具类型让外层 PS 直接处理) +$exitCode = 0 +$timedOut = $false + +try { + if ($Tool -eq 'bash') { + if (-not $Command) { Write-Error "bash 工具需要 -Command"; exit 2 } + # 用 Out-Default 让 stdout 流到 mcode 的终端 + Invoke-Expression $Command + $exitCode = $LASTEXITCODE + if (-not $exitCode) { $exitCode = 0 } + } else { + # 其他工具:直接交给外层 PS 执行(这个 wrapper 设计上不接管) + Write-Warning "wrap-tool: '$Tool' 类型请在调用方直接处理,wrapper 不接管" + & $notify -State error -Message "$Tool : wrapper 不支持此类工具" | Out-Null + exit 2 + } +} catch { + Write-Output ("[wrap-tool error] " + $_.Exception.Message) + & $notify -State error -Message "$Tool : 异常" | Out-Null + exit 99 +} + +# 4) 根据退出码推状态 +if ($exitCode -eq 0) { + & $notify -State done -Message "$Tool 完成" | Out-Null +} elseif ($WaitingExitCodes -contains $exitCode) { + & $notify -State waiting -Message "$Tool 等待审批 (exit=$exitCode)" | Out-Null +} else { + & $notify -State error -Message "$Tool 失败 (exit=$exitCode)" | Out-Null +} + +exit $exitCode From fce7c5f5e490674dfd9989ecb7fe5183867e2b5e Mon Sep 17 00:00:00 2001 From: antianqi <75944423+antianqi@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:35:00 +0800 Subject: [PATCH 2/3] Add mcode-status-detect v0.2.0: state inference from mcode session log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ": " role=assistant + thinking -> thinking role=assistant + text -> idle (just replied) role=toolResult + !isError -> done " 完成" role=toolResult + isError -> error " 失败" 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. --- .../mcode-island/mcode-status-detect.ps1 | 293 ++++++++++++++++++ plugins/antianqi/mcode-island/plugin.json | 2 +- .../mcode-island/start-detect-island.ps1 | 22 ++ .../mcode-island/status-detect-island.ps1 | 17 + .../mcode-island/stop-detect-island.ps1 | 17 + 5 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 plugins/antianqi/mcode-island/mcode-status-detect.ps1 create mode 100644 plugins/antianqi/mcode-island/start-detect-island.ps1 create mode 100644 plugins/antianqi/mcode-island/status-detect-island.ps1 create mode 100644 plugins/antianqi/mcode-island/stop-detect-island.ps1 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..242cec2 --- /dev/null +++ b/plugins/antianqi/mcode-island/mcode-status-detect.ps1 @@ -0,0 +1,293 @@ +# mcode-status-detect.ps1 - 后台守护进程 +# 监听 mcode session log,实时推断 agent 状态,写到 status.json +# 让 widget 不依赖 agent 主动调 notify-island.ps1 也能跟着动 +# +# 信号源(每 1s 检查一次): +# 1. mcode 主进程(PID 从 .mcode-active/.json 取)是否还活 +# 2. .minimax/v2/sessions//*/messages.jsonl 最后一条 message +# 3. status.json 当前的 source(detector / agent 主动推) +# +# 状态映射: +# 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 +) + +$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) # no current status +$R_OWN_CHANGED = _s (0x6F,0x77,0x6E,0x20,0x73,0x74,0x61,0x74,0x65,0x20,0x63,0x68,0x61,0x6E,0x67,0x65,0x64) # own state changed +$R_OWN_MSG = _s (0x6F,0x77,0x6E,0x20,0x6D,0x65,0x73,0x73,0x61,0x67,0x65,0x20,0x63,0x68,0x61,0x6E,0x67,0x65,0x64) # own message changed +$R_TAKEOVER = _s (0x74,0x61,0x6B,0x65,0x6F,0x76,0x65,0x72,0x20,0x74,0x6F,0x20,0x73,0x65,0x74,0x74,0x6C,0x65) # takeover to settle + +$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' + +$mcodeActiveDir = 'C:\Users\Administrator\.minimax-code\.mcode-active' +$sessionsRoot = 'C:\Users\Administrator\.minimax\v2\sessions' + +# 全局状态 +$script:lastDetectedState = '' +$script:lastMsgsSnapshot = @{ mtime = '' } + +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 (!(Test-Path $mcodeActiveDir)) { return $null } + $candidates = Get-ChildItem -Path $mcodeActiveDir -Filter '*.json' -ErrorAction SilentlyContinue + foreach ($f in $candidates) { + try { + $j = Get-Content $f.FullName -Raw | ConvertFrom-Json + $targetPid = [int]$j.pid + $proc = Get-Process -Id $targetPid -ErrorAction SilentlyContinue + if ($proc -and -not $proc.HasExited) { return $targetPid } + } catch {} + } + return $null +} + +function Get-LatestMessagesFile { + if (!(Test-Path $sessionsRoot)) { return $null } + Get-ChildItem -Path $sessionsRoot -Recurse -Filter 'messages.jsonl' -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 +} + +function Read-LastMessage($file) { + try { + $st = Get-Item $file -ErrorAction Stop + if ($script:lastMsgsSnapshot.mtime -eq $st.LastWriteTime.ToString($FMT_O_FULL)) { return $null } + $script:lastMsgsSnapshot.mtime = $st.LastWriteTime.ToString($FMT_O_FULL) + # Use FileStream with FileShare.ReadWrite so we never block mcode's writer. + # Read backwards from end of file to find the last newline, then read forward. + $fs = [System.IO.File]::Open($file, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) + try { + $len = $fs.Length + if ($len -eq 0) { return $null } + # Read up to last 64KB (single message is rarely > 64KB) + $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) + # Find last non-empty line + $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) { + return ($line | ConvertFrom-Json) + } + } + return $null + } finally { + $fs.Close() + } + } catch { + return $null + } +} + +function Infer-State($msg) { + if (-not $msg -or -not $msg.message) { return $null } + $m = $msg.message + $role = [string]$m.role + + if ($role -eq $S_USER) { return @{ state=$S_IDLE; message=$MSG_USER_WAIT } } + + if ($role -eq $S_ASSISTANT) { + $hasTool = @($m.content | Where-Object { $_.type -eq $S_TOOLCALL }) | Select-Object -First 1 + $hasThink = @($m.content | Where-Object { $_.type -eq $S_THINK_BLK }) | Select-Object -First 1 + $hasText = @($m.content | Where-Object { $_.type -eq $S_TEXT }) | Select-Object -First 1 + + if ($hasTool) { + $tool = $hasTool.name + $args = $hasTool.arguments + if ($args) { + $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 (Get-Content $statusFile -Raw | ConvertFrom-Json) } catch { return $null } +} + +# === 主循环 === +if (-not $Once) { + Set-Content -Path $pidFile -Value $PID -Encoding ASCII + Log-Line "detector started (PID $PID)" +} + +try { + while ($true) { + $now = Get-Date + $inferred = $null + $latestFile = $null + + # 1) mcode 进程健康:每次都查(不靠 messages.jsonl 缓存) + $mcodePid = Get-McodePid + if (-not $mcodePid) { + $inferred = @{ state=$S_ERROR; message=$MSG_MCODE_EXIT } + } else { + # 2) 读 messages.jsonl 推断 + $latestFile = Get-LatestMessagesFile + if ($latestFile) { + $msg = Read-LastMessage $latestFile.FullName + if ($msg) { + $inferred = Infer-State $msg + } + } + } + + # 60s 无活动 → idle 兜底(但仅在推断状态不是 idle/error 时) + if ($inferred -and $latestFile) { + $curState = [string]$inferred.state + $isSettled = ($curState -eq $S_IDLE) -or ($curState -eq $S_ERROR) + if (-not $isSettled) { + $age = ($now - $latestFile.LastWriteTime).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,0x20,0x5B,0x6E,0x6F,0x2D,0x72,0x75,0x6E,0x5D) + Log-Line $STOPPED_MSG +} diff --git a/plugins/antianqi/mcode-island/plugin.json b/plugins/antianqi/mcode-island/plugin.json index 68d71b6..f32b034 100644 --- a/plugins/antianqi/mcode-island/plugin.json +++ b/plugins/antianqi/mcode-island/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "mcode-island", - "version": "0.1.0", + "version": "0.2.0", "description": "Windows 桌面灵动岛 (Dynamic Island) 状态窗口:让 mcode agent 把工作状态(idle/thinking/working/waiting/done/error)实时推送到屏幕顶部悬浮 pill,agent 自己忙的时候用户不用切回 mcode 也能看到进度。", "author": { "name": "antianqi", 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..409572d --- /dev/null +++ b/plugins/antianqi/mcode-island/start-detect-island.ps1 @@ -0,0 +1,22 @@ +# 启动 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' + +# 防重复 +if (Test-Path $pidFile) { + $old = (Get-Content $pidFile -ErrorAction SilentlyContinue) -as [int] + if ($old -and (Get-Process -Id $old -ErrorAction SilentlyContinue)) { + 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/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/stop-detect-island.ps1 b/plugins/antianqi/mcode-island/stop-detect-island.ps1 new file mode 100644 index 0000000..be8c488 --- /dev/null +++ b/plugins/antianqi/mcode-island/stop-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] +if ($targetPid -and $targetPid -gt 0) { + $p = Get-Process -Id $targetPid -ErrorAction SilentlyContinue + if ($p -and -not $p.HasExited) { + Stop-Process -Id $targetPid -Force + Write-Output ("stopped PID " + $targetPid) + } else { + Write-Output ("NOT RUNNING (stale pid " + $targetPid + ")") + } +} +Remove-Item $pidFile -Force -ErrorAction SilentlyContinue From bad0868720f46335f23dd55ba1f4184f68d4c538 Mon Sep 17 00:00:00 2001 From: antianqi <75944423+antianqi@users.noreply.github.com> Date: Sun, 23 Aug 2026 02:27:24 +0800 Subject: [PATCH 3/3] fix: address review feedback on PR #17 (v0.2.1) Fixes for review comments from hetaoBackend (commit fce7c5f): #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]. #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). #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. #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. #5 README: -Enable -> -Action Enable to match autostart.ps1 parameter set. #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). --- plugins/antianqi/mcode-island/README.md | 32 ++- .../mcode-island/mcode-status-detect.ps1 | 188 ++++++++++++++---- .../mcode-island/skills/mcode-island/SKILL.md | 18 +- .../mcode-island/start-detect-island.ps1 | 22 +- .../antianqi/mcode-island/start-island.ps1 | 37 ++-- .../mcode-island/stop-detect-island.ps1 | 12 +- plugins/antianqi/mcode-island/stop-island.ps1 | 18 +- plugins/antianqi/mcode-island/wrap-tool.ps1 | 69 +++---- 8 files changed, 273 insertions(+), 123 deletions(-) diff --git a/plugins/antianqi/mcode-island/README.md b/plugins/antianqi/mcode-island/README.md index 14efacb..3b9705c 100644 --- a/plugins/antianqi/mcode-island/README.md +++ b/plugins/antianqi/mcode-island/README.md @@ -61,13 +61,32 @@ If the file does not exist: ### Wrap every bash call (recommended for power users) -The shipped `wrap-tool.ps1` is a one-stop wrapper: it pushes `working` before -the command, then `done` / `error` / `waiting` based on `$LASTEXITCODE`, all -without changing the command's exit code: +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 @@ -86,7 +105,7 @@ without changing the command's exit code: 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" -Enable + & "%PLUGIN_DIR%\mcode-island\autostart.ps1" -Action Enable ``` This writes to `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`. No admin rights required. @@ -193,7 +212,10 @@ a live MiniMax Code session. Empirical evidence (captured during development): - 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`. + 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 diff --git a/plugins/antianqi/mcode-island/mcode-status-detect.ps1 b/plugins/antianqi/mcode-island/mcode-status-detect.ps1 index 242cec2..5904df5 100644 --- a/plugins/antianqi/mcode-island/mcode-status-detect.ps1 +++ b/plugins/antianqi/mcode-island/mcode-status-detect.ps1 @@ -1,11 +1,16 @@ -# mcode-status-detect.ps1 - 后台守护进程 +# mcode-status-detect.ps1 - 后台守护进程 (v0.2.1) # 监听 mcode session log,实时推断 agent 状态,写到 status.json # 让 widget 不依赖 agent 主动调 notify-island.ps1 也能跟着动 # -# 信号源(每 1s 检查一次): -# 1. mcode 主进程(PID 从 .mcode-active/.json 取)是否还活 -# 2. .minimax/v2/sessions//*/messages.jsonl 最后一条 message -# 3. status.json 当前的 source(detector / agent 主动推) +# 路径解析(顺序): +# 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 (等用户) @@ -27,7 +32,8 @@ # 所以所有 ASCII 字符串都用 _s (byte[]) helper 构造 param( - [switch]$Once + [switch]$Once, + [string]$Root = '' # override: /.minimax-code and /.minimax/v2/... ) $ErrorActionPreference = 'Stop' @@ -65,10 +71,21 @@ $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) # no current status -$R_OWN_CHANGED = _s (0x6F,0x77,0x6E,0x20,0x73,0x74,0x61,0x74,0x65,0x20,0x63,0x68,0x61,0x6E,0x67,0x65,0x64) # own state changed -$R_OWN_MSG = _s (0x6F,0x77,0x6E,0x20,0x6D,0x65,0x73,0x73,0x61,0x67,0x65,0x20,0x63,0x68,0x61,0x6E,0x67,0x65,0x64) # own message changed -$R_TAKEOVER = _s (0x74,0x61,0x6B,0x65,0x6F,0x76,0x65,0x72,0x20,0x74,0x6F,0x20,0x73,0x65,0x74,0x74,0x6C,0x65) # takeover to settle +$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 } @@ -76,12 +93,76 @@ $statusFile = Join-Path $configDir 'status.json' $logFile = Join-Path $configDir 'island.log' $pidFile = Join-Path $configDir 'detect.pid' -$mcodeActiveDir = 'C:\Users\Administrator\.minimax-code\.mcode-active' -$sessionsRoot = 'C:\Users\Administrator\.minimax\v2\sessions' +# 解析 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 = '' } +$script:lastMsgsSnapshot = @{ mtime = ''; lastInferred = $null } function Log-Line($msg) { $ts = (Get-Date).ToString('HH:mm:ss') @@ -91,7 +172,7 @@ function Log-Line($msg) { } function Get-McodePid { - if (!(Test-Path $mcodeActiveDir)) { return $null } + if (-not (Test-Path $mcodeActiveDir)) { return $null } $candidates = Get-ChildItem -Path $mcodeActiveDir -Filter '*.json' -ErrorAction SilentlyContinue foreach ($f in $candidates) { try { @@ -104,36 +185,43 @@ function Get-McodePid { return $null } -function Get-LatestMessagesFile { - if (!(Test-Path $sessionsRoot)) { return $null } - Get-ChildItem -Path $sessionsRoot -Recurse -Filter 'messages.jsonl' -ErrorAction SilentlyContinue | - Sort-Object LastWriteTime -Descending | - Select-Object -First 1 +function Get-LatestSessionFile { + if (-not $sessionsRoot -or -not (Test-Path $sessionsRoot)) { return $null } + $all = Get-ChildItem -Path $sessionsRoot -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.PSIsContainer -eq $false -and ($_.Name -eq $FNAME_LEDGER -or $_.Name -eq $FNAME_MESSAGES) } + if (-not $all) { return $null } + # 优先 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 } function Read-LastMessage($file) { try { $st = Get-Item $file -ErrorAction Stop - if ($script:lastMsgsSnapshot.mtime -eq $st.LastWriteTime.ToString($FMT_O_FULL)) { return $null } - $script:lastMsgsSnapshot.mtime = $st.LastWriteTime.ToString($FMT_O_FULL) - # Use FileStream with FileShare.ReadWrite so we never block mcode's writer. - # Read backwards from end of file to find the last newline, then read forward. + $mtime = $st.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($file, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) try { $len = $fs.Length if ($len -eq 0) { return $null } - # Read up to last 64KB (single message is rarely > 64KB) $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) - # Find last non-empty line $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) { - return ($line | ConvertFrom-Json) + $msg = ($line | ConvertFrom-Json) + $script:lastMsgsSnapshot.lastInferred = $msg + return $msg } } return $null @@ -141,16 +229,34 @@ function Read-LastMessage($file) { $fs.Close() } } catch { - return $null + return $script:lastMsgsSnapshot.lastInferred } } function Infer-State($msg) { - if (-not $msg -or -not $msg.message) { return $null } + if (-not $msg) { return $null } + # ledger.jsonl 事件格式:{kind, phase, action, ...} + # messages.jsonl 消息格式:{message: {role, content, ...}, ...} $m = $msg.message - $role = [string]$m.role + 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 + } - if ($role -eq $S_USER) { return @{ state=$S_IDLE; message=$MSG_USER_WAIT } } + $role = [string]$m.role + if ($role -eq $S_USER) { return @{ state=$S_IDLE; message=$MSG_USER_WAIT } } if ($role -eq $S_ASSISTANT) { $hasTool = @($m.content | Where-Object { $_.type -eq $S_TOOLCALL }) | Select-Object -First 1 @@ -164,14 +270,10 @@ function Infer-State($msg) { $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_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='' } } @@ -208,7 +310,7 @@ function Read-StatusObj { # === 主循环 === if (-not $Once) { Set-Content -Path $pidFile -Value $PID -Encoding ASCII - Log-Line "detector started (PID $PID)" + Log-Line "detector started (PID $PID) mcodeRoot=$mcodeRoot" } try { @@ -217,13 +319,13 @@ try { $inferred = $null $latestFile = $null - # 1) mcode 进程健康:每次都查(不靠 messages.jsonl 缓存) + # 1) mcode 进程健康:每次都查 $mcodePid = Get-McodePid if (-not $mcodePid) { $inferred = @{ state=$S_ERROR; message=$MSG_MCODE_EXIT } } else { - # 2) 读 messages.jsonl 推断 - $latestFile = Get-LatestMessagesFile + # 2) 读 session log 推断 + $latestFile = Get-LatestSessionFile if ($latestFile) { $msg = Read-LastMessage $latestFile.FullName if ($msg) { @@ -232,7 +334,7 @@ try { } } - # 60s 无活动 → idle 兜底(但仅在推断状态不是 idle/error 时) + # 3) 60s 无活动 → idle 兜底(每次循环都跑,不再被 mtime 缓存屏蔽) if ($inferred -and $latestFile) { $curState = [string]$inferred.state $isSettled = ($curState -eq $S_IDLE) -or ($curState -eq $S_ERROR) @@ -288,6 +390,6 @@ try { 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,0x20,0x5B,0x6E,0x6F,0x2D,0x72,0x75,0x6E,0x5D) + $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/skills/mcode-island/SKILL.md b/plugins/antianqi/mcode-island/skills/mcode-island/SKILL.md index 1bc4920..b906066 100644 --- a/plugins/antianqi/mcode-island/skills/mcode-island/SKILL.md +++ b/plugins/antianqi/mcode-island/skills/mcode-island/SKILL.md @@ -46,21 +46,27 @@ 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 handles state transitions -automatically. Wrap every `bash` invocation through it: +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 -# Instead of: bash "npm test" -& "$PSScriptRoot\wrap-tool.ps1" -Tool bash -Command "npm test" -Description "run tests" +# Step 1: announce "working" before invoking mcode's bash tool +& "\wrap-tool.ps1" -Tool bash -Command "npm test" -Description "run tests" -# state flow this triggers: working("bash: run tests") → done("bash 完成") | error(...) | waiting(...) +# 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 = Split-Path -Parent $PSScriptRoot # +$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" diff --git a/plugins/antianqi/mcode-island/start-detect-island.ps1 b/plugins/antianqi/mcode-island/start-detect-island.ps1 index 409572d..e86b46d 100644 --- a/plugins/antianqi/mcode-island/start-detect-island.ps1 +++ b/plugins/antianqi/mcode-island/start-detect-island.ps1 @@ -6,17 +6,27 @@ if (!(Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Fo $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 (Get-Process -Id $old -ErrorAction SilentlyContinue)) { - Write-Output "detector already running (PID $old)" - exit 0 + 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 } - 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))" +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 index 8cc02a1..c0b47a6 100644 --- a/plugins/antianqi/mcode-island/start-island.ps1 +++ b/plugins/antianqi/mcode-island/start-island.ps1 @@ -12,13 +12,24 @@ if (!(Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Fo $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) { - $oldPid = Get-Content $pidFile -ErrorAction SilentlyContinue - if ($oldPid -and (Get-Process -Id $oldPid -ErrorAction SilentlyContinue)) { - Write-Output ("已在运行(PID $oldPid)") - exit 0 - } else { + $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 } } @@ -30,23 +41,21 @@ $proc = Start-Process powershell.exe -ArgumentList $args -PassThru # 写 PID(先写,后面 status / stop 都靠这个) Set-Content -Path $pidFile -Value $proc.Id -Encoding ASCII -# 等待 widget 起来(最多 8 秒) +# 等待 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 } - $log = Join-Path $configDir 'widget.log' - if ((Test-Path $log) -and ((Get-Item $log).Length -gt 0)) { - $content = Get-Content $log -Tail 5 -ErrorAction SilentlyContinue - if ($content -match 'about to ShowDialog') { $ok = $true; break } - } + # WPF widget 启动后会创建主窗口;MainWindowHandle 非 0 即就绪 + if ($p.MainWindowHandle -ne 0) { $ok = $true; break } } catch {} } if ($ok) { - Write-Output "mcode-island 已启动 (PID $($proc.Id),已就绪)" + Write-Output ("mcode-island 已启动 (PID " + $proc.Id + ",已就绪)") } else { - Write-Output "mcode-island 已启动 (PID $($proc.Id),等待中...)" + Write-Output ("mcode-island 已启动 (PID " + $proc.Id + ",等待中...)") } diff --git a/plugins/antianqi/mcode-island/stop-detect-island.ps1 b/plugins/antianqi/mcode-island/stop-detect-island.ps1 index be8c488..fee98d7 100644 --- a/plugins/antianqi/mcode-island/stop-detect-island.ps1 +++ b/plugins/antianqi/mcode-island/stop-detect-island.ps1 @@ -1,15 +1,21 @@ -# 停掉 detector +# 停掉 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) { - Stop-Process -Id $targetPid -Force - Write-Output ("stopped PID " + $targetPid) + $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 + ")") } diff --git a/plugins/antianqi/mcode-island/stop-island.ps1 b/plugins/antianqi/mcode-island/stop-island.ps1 index ef12e95..c5a803e 100644 --- a/plugins/antianqi/mcode-island/stop-island.ps1 +++ b/plugins/antianqi/mcode-island/stop-island.ps1 @@ -1,7 +1,11 @@ # 停掉 widget +# 校验 PID 文件 + cmdline,防止误杀 PID 复用的不相关进程 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -$pidFile = Join-Path $env:APPDATA 'mcode-island\widget.pid' +$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 @@ -11,10 +15,16 @@ $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) { - Stop-Process -Id $widgetPid -Force - Write-Output "stopped PID $widgetPid" + # 验证 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)" + Write-Output ("NOT RUNNING (stale pid " + $widgetPid + ")") } } else { Write-Output 'NOT RUNNING' diff --git a/plugins/antianqi/mcode-island/wrap-tool.ps1 b/plugins/antianqi/mcode-island/wrap-tool.ps1 index 62f235b..77960b5 100644 --- a/plugins/antianqi/mcode-island/wrap-tool.ps1 +++ b/plugins/antianqi/mcode-island/wrap-tool.ps1 @@ -1,15 +1,18 @@ -# mcode 灵动岛 - 工具调用 wrapper +# mcode 灵动岛 - 工具调用 wrapper (v0.2.1: status-only) # 用法(在 mcode 里): -# wrap-tool.ps1 -Tool "bash" -Command "npm test" -# wrap-tool.ps1 -Tool "read" -Path "C:\foo.txt" -# wrap-tool.ps1 -Tool "write" -Path "..." -Command "..." -# wrap-tool.ps1 -Tool "edit" -Path "..." -Command "..." +# 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 " " -# 2. 执行命令 -# 3. 根据退出码推 done (0) / error (≠0) / waiting (特殊) -# 4. 退出码原样返回(不吞) +# 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)] @@ -21,7 +24,8 @@ param( [string]$Pattern = '', [string]$Glob = '', [string]$Description = '', - [int[]]$WaitingExitCodes = @(1) # 这些退出码视为 "waiting"(需要审批) + [int]$ExitCode = -1, # -1 = not yet run, push working; 0/非0 = push done/waiting/error + [int[]]$WaitingExitCodes = @(1) # 这些退出码视为 "waiting"(需要审批) ) $ErrorActionPreference = 'Continue' @@ -30,7 +34,7 @@ chcp 65001 > $null $notify = Join-Path $PSScriptRoot 'notify-island.ps1' -# 1) 拼一个"简述"显示在 widget 上 +# 拼一个"简述"显示在 widget 上 $brief = $Description if (-not $brief) { if ($Command) { $brief = ($Command.Substring(0, [Math]::Min(60, $Command.Length))) } @@ -41,39 +45,20 @@ if (-not $brief) { } $brief = $brief -replace "`r`n", ' ' -replace "`n", ' ' -# 2) 推 working -& $notify -State working -Message "$Tool : $brief" | Out-Null - -# 3) 执行命令(这里只支持 bash,其他工具类型让外层 PS 直接处理) -$exitCode = 0 -$timedOut = $false - -try { - if ($Tool -eq 'bash') { - if (-not $Command) { Write-Error "bash 工具需要 -Command"; exit 2 } - # 用 Out-Default 让 stdout 流到 mcode 的终端 - Invoke-Expression $Command - $exitCode = $LASTEXITCODE - if (-not $exitCode) { $exitCode = 0 } - } else { - # 其他工具:直接交给外层 PS 执行(这个 wrapper 设计上不接管) - Write-Warning "wrap-tool: '$Tool' 类型请在调用方直接处理,wrapper 不接管" - & $notify -State error -Message "$Tool : wrapper 不支持此类工具" | Out-Null - exit 2 - } -} catch { - Write-Output ("[wrap-tool error] " + $_.Exception.Message) - & $notify -State error -Message "$Tool : 异常" | Out-Null - exit 99 +if ($ExitCode -lt 0) { + # 还没跑完:推 working + & $notify -State working -Message "$Tool : $brief" | Out-Null + exit 0 } -# 4) 根据退出码推状态 -if ($exitCode -eq 0) { +# 跑完了:根据退出码推 done/waiting/error +if ($ExitCode -eq 0) { & $notify -State done -Message "$Tool 完成" | Out-Null -} elseif ($WaitingExitCodes -contains $exitCode) { - & $notify -State waiting -Message "$Tool 等待审批 (exit=$exitCode)" | 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 + & $notify -State error -Message "$Tool 失败 (exit=$ExitCode)" | Out-Null + exit $ExitCode } - -exit $exitCode