Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions plugins/antianqi/mcode-island/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,28 @@ alternative:
& "%PLUGIN_DIR%\mcode-island\notify-island.ps1" -State error -Message "npm test failed"
```

For sub-step progress (Computer Use iterative loops, multi-step plans),
pass `-Step` / `-Total` / `-Detail` so the pill shows what the agent is
doing *right now*:

```powershell
& "%PLUGIN_DIR%\mcode-island\notify-island.ps1" -State working -Message "Computer Use" `
-Step 3 -Total 12 -Detail "fill username field"
# → pill renders: "step 3/12 · fill username field"

& "%PLUGIN_DIR%\mcode-island\notify-island.ps1" -State working -Message "Bash" `
-Step 5 -Detail "npm install"
# → pill renders: "step 5 · npm install"

& "%PLUGIN_DIR%\mcode-island\notify-island.ps1" -State done -Message "Bash ok"
# → pill renders: "Bash ok" (no step → legacy behavior, backward compat)
```

All three params are optional and backward compatible. The detail field
replaces the message in the rendered pill when present (avoids stacking
"Bash ok · fill username"). See `skills/mcode-island/SKILL.md` for the
full semantics and the contract with the widget renderer.

## Quick start

1. **Install** — copy this folder into your `~/.minimax/plugins/mcode-island/`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ function Push-Island {
[ValidateSet('idle','thinking','working','waiting','done','error')]
[string]$State,

[string]$Message = ''
[string]$Message = '',
[int]$Step = -1,
[int]$Total = -1,
[string]$Detail = ''
)
if (-not (Test-Path -LiteralPath $script:NotifyIsland)) {
# Widget is not installed yet — silent no-op. The plugin's
Expand All @@ -52,7 +55,7 @@ function Push-Island {
return
}
try {
& $script:NotifyIsland -State $State -Message $Message 2>$null | Out-Null
& $script:NotifyIsland -State $State -Message $Message -Step $Step -Total $Total -Detail $Detail 2>$null | Out-Null
} catch {
# Hook must never block the agent on a notification failure.
}
Expand Down Expand Up @@ -97,6 +100,26 @@ function Format-ToolSummary {
'WebSearch' { $detail = [string]$Event.tool_input.query }
'Task' { $detail = [string]$Event.tool_input.description }
'NotebookEdit' { $detail = [string]$Event.tool_input.notebook_path }
# mcode-internal: Computer Use 抽 action + coordinate/text
# 例: "mcode-computer-use : click at (1024,768)"
# "mcode-computer-use : type 'hello'"
# 注意:coordinate 是 array,PowerShell 默认 $OFS=' ' 会让
# "$coord" 渲染成 "(1024 768)" 不是 "(1024,768)"。必须
# 显式 -join ','。' ' 在 pill 上看起来像数字被截断,
# 影响用户判断坐标。
'mcode-computer-use' {
$act = if ($Event.tool_input.action) { [string]$Event.tool_input.action } else { '' }
if ($Event.tool_input.coordinate) {
$coord = $Event.tool_input.coordinate
$coordStr = "($($coord -join ','))"
$detail = "$act at $coordStr"
} elseif ($Event.tool_input.text) {
$txt = [string]$Event.tool_input.text
$detail = "$act '$txt'"
} else {
$detail = $act
}
}
default { $detail = '' }
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
# Note: Fires after every tool call returns. Heuristic: if the
# tool_result is empty or matches an error pattern, push
# error; otherwise push done. Self-push calls are filtered.
# Per-tool summary (Format-ToolSummary) is split into
# Message="<tool> ok|failed" and Detail=<the rest>, so the pill
# renders "Bash ok · ls -la /tmp" instead of just "Bash ok".
. "$PSScriptRoot\_lib.ps1"
$evt = Read-HookStdin
if (Test-IsSelfPush $evt) { exit 0 }
Expand All @@ -20,9 +23,18 @@ if ($null -eq $result) {
elseif ($s -match '^\s*(Error|ERROR|✕|Error:|\[ERROR\])') { $isError = $true }
}

# Format-ToolSummary 抽 detail,但要剥掉 "tool : " 前缀,只留后半段
$summary = Format-ToolSummary $evt
$detail = ''
if ($summary -and $summary.StartsWith("$tool : ")) {
$detail = $summary.Substring($tool.Length + 3)
} elseif ($summary -and $summary -ne $tool) {
$detail = $summary
}

if ($isError) {
Push-Island -State error -Message "$tool failed"
Push-Island -State error -Message "$tool failed" -Detail $detail
} else {
Push-Island -State done -Message "$tool ok"
Push-Island -State done -Message "$tool ok" -Detail $detail
}
exit 0
66 changes: 59 additions & 7 deletions plugins/antianqi/mcode-island/mcode-island.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -370,13 +370,49 @@ function Stop-IndeterminateShimmer {
$script:progressShimmerTransform.X = -130 # 重置到起点
}

# Sub-step 渲染("step N[/M] · detail"):
# Step > 0 + Total > 0 → "step 3/12 · fill username"
# Step > 0 + Total <= 0 → "step 3 · fill username"
# Step > 0 + Detail 空 → "step 3/12"
# Step <= 0 → 原 Message 字段
# 这样 message 字段保持"工具名"("Bash ok"),detail 字段填具体动作,
# 渲染时拼成 "step 3/12 · Bash ok · fill username" 或者更精确的
# "step 3/12 · fill username"(detail 存在时优先覆盖 message)。
# 注意:detail 非空时**完全替换** message,避免双重信息("Bash ok · ls -la")。
function Build-DisplayMessage {
param(
[string]$Message,
[int]$Step,
[int]$Total,
[string]$Detail
)
$base = if ($Message) { $Message } else { '' }
if ($Step -le 0) { return $base }

$stepStr = if ($Total -gt 0) { "step $Step/$Total" } else { "step $Step" }
if ($Detail) {
# detail 非空时优先用 detail(agent 已经表达了"我在做什么")
return "$stepStr · $Detail"
}
# detail 空但 step 给出 → 只显示 step,避免重复 message 造成噪声
return $stepStr
}

# 状态更新
# Progress 取值约定(跟 notify-island.ps1 / detector 对齐):
# -1 → 没有进度信息,进度条隐藏
# 0..100 → 百分比,0=空条,100=满条;超出范围会被 clamp
# Usage5h:剩余百分比(0..100);-2 = 未提供
# Usage5hResetMs:距下次 5h 刷新的毫秒数;0 = 未知
# TodoProgress:todowrite 列表的完成百分比(0..100);-2 = 未提供
# Step/Total/Detail:sub-step 进度(agent 自报),参 Build-DisplayMessage
# - 优先级:显式 Progress > TodoProgress > shimmer
# - 即:agent 直接传 progress 最高;否则如果有 todo 列表就用 todo 完成度;都没就 shimmer 动画
# -1 → 没有进度信息,进度条隐藏
# 0..100 → 百分比,0=空条,100=满条;超出范围会被 clamp
# Usage5h:剩余百分比(0..100);-2 = 未提供
# Usage5hResetMs:距下次 5h 刷新的毫秒数;0 = 未知
# TodoProgress:todowrite 列表的完成百分比(0..100);-2 = 未提供
# - 优先级:显式 Progress > TodoProgress > shimmer
# - 即:agent 直接传 progress 最高;否则如果有 todo 列表就用 todo 完成度;都没就 shimmer 动画
function Update-State {
Expand All @@ -386,14 +422,17 @@ function Update-State {
[int]$Progress = -1,
[int]$Usage5h = -2,
[int]$Usage5hResetMs = 0,
[int]$TodoProgress = -2
[int]$TodoProgress = -2,
[int]$Step = -1,
[int]$Total = -1,
[string]$Detail = ''
)
$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:messageText.Text = Build-DisplayMessage -Message $Message -Step $Step -Total $Total -Detail $Detail
$script:actionIcon.Text = $s.icon

if ($State -in @('thinking','working','waiting')) { Start-Pulse } else { Stop-Pulse }
Expand Down Expand Up @@ -689,18 +728,25 @@ $timer.Add_Tick({
$script:lastStatusMtime = $mtime
$data = Get-Content $statusFile -Raw -Encoding UTF8 | ConvertFrom-Json
# progress 也要进 sig,否则 agent 连续推 working+相同 message+不同 progress 会被去重
# step/total/detail 也要进 sig,否则连续推同 state 但不同 step 会被去重
$prog = if ($data.PSObject.Properties['progress']) { [int]$data.progress } else { -1 }
$usage = $null
$resetMs = 0
$todoP = -2
$step = -1
$total = -1
$detail = ''
if ($data.PSObject.Properties['usage5h'] -and $null -ne $data.usage5h) { $usage = [int]$data.usage5h }
if ($data.PSObject.Properties['usage5hResetMs'] -and $null -ne $data.usage5hResetMs) { $resetMs = [int]$data.usage5hResetMs }
if ($data.PSObject.Properties['todoProgress'] -and $null -ne $data.todoProgress) { $todoP = [int]$data.todoProgress }
$sig = "$($data.state)|$($data.message)|$prog|$usage|$resetMs|$todoP|$($data.ts)"
if ($data.PSObject.Properties['step'] -and $null -ne $data.step) { $step = [int]$data.step }
if ($data.PSObject.Properties['total'] -and $null -ne $data.total) { $total = [int]$data.total }
if ($data.PSObject.Properties['detail'] -and $null -ne $data.detail) { $detail = [string]$data.detail }
$sig = "$($data.state)|$($data.message)|$prog|$usage|$resetMs|$todoP|$step|$total|$detail|$($data.ts)"
if ($sig -eq $script:lastStatusSig) { return }
$script:lastStatusSig = $sig
Dbg "POLL: $($data.state) :: $($data.message) (progress=$prog usage5h=$usage resetMs=$resetMs todoProgress=$todoP)"
Update-State -State $data.state -Message $data.message -Progress $prog -Usage5h $usage -Usage5hResetMs $resetMs -TodoProgress $todoP
Dbg "POLL: $($data.state) :: $($data.message) step=$step/$total detail=$detail (progress=$prog usage5h=$usage resetMs=$resetMs todoProgress=$todoP)"
Update-State -State $data.state -Message $data.message -Progress $prog -Usage5h $usage -Usage5hResetMs $resetMs -TodoProgress $todoP -Step $step -Total $total -Detail $detail
} catch {
Dbg "POLL ERR: $($_.Exception.Message)"
}
Expand All @@ -717,12 +763,18 @@ if (Test-Path $statusFile) {
$initUsage = $null
$initReset = 0
$initTodo = -2
$initStep = -1
$initTotal = -1
$initDetail = ''
if ($init.PSObject.Properties['usage5h'] -and $null -ne $init.usage5h) { $initUsage = [int]$init.usage5h }
if ($init.PSObject.Properties['usage5hResetMs'] -and $null -ne $init.usage5hResetMs) { $initReset = [int]$init.usage5hResetMs }
if ($init.PSObject.Properties['todoProgress'] -and $null -ne $init.todoProgress) { $initTodo = [int]$init.todoProgress }
$script:lastStatusSig = "$($init.state)|$($init.message)|$initProg|$initUsage|$initReset|$initTodo|$($init.ts)"
if ($init.PSObject.Properties['step'] -and $null -ne $init.step) { $initStep = [int]$init.step }
if ($init.PSObject.Properties['total'] -and $null -ne $init.total) { $initTotal = [int]$init.total }
if ($init.PSObject.Properties['detail'] -and $null -ne $init.detail) { $initDetail = [string]$init.detail }
$script:lastStatusSig = "$($init.state)|$($init.message)|$initProg|$initUsage|$initReset|$initTodo|$initStep|$initTotal|$initDetail|$($init.ts)"
$script:lastStatusMtime = (Get-Item $statusFile).LastWriteTimeUtc.Ticks
Update-State -State $init.state -Message $init.message -Progress $initProg -Usage5h $initUsage -Usage5hResetMs $initReset -TodoProgress $initTodo
Update-State -State $init.state -Message $init.message -Progress $initProg -Usage5h $initUsage -Usage5hResetMs $initReset -TodoProgress $initTodo -Step $initStep -Total $initTotal -Detail $initDetail
} catch {}
} else {
Update-State -State 'idle' -Message ''
Expand Down
8 changes: 7 additions & 1 deletion plugins/antianqi/mcode-island/notify-island.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ param(
[ValidateSet('idle','thinking','working','waiting','done','error')]
[string]$State = 'idle',
[string]$Message = '',
[int]$Progress = -1
[int]$Progress = -1,
[int]$Step = -1,
[int]$Total = -1,
[string]$Detail = ''
)

$ErrorActionPreference = 'Stop'
Expand Down Expand Up @@ -113,6 +116,9 @@ $payload = [PSCustomObject]@{
state = $State
message = $Message
progress = $Progress
step = $Step
total = $Total
detail = $Detail
ts = $ts
source = 'agent'
} | ConvertTo-Json -Compress
Expand Down
4 changes: 2 additions & 2 deletions plugins/antianqi/mcode-island/plugin.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "mcode-island",
"version": "0.3.0",
"description": "Windows 桌面灵动岛 (Dynamic Island) 状态窗口:让 mcode agent 把工作状态(idle/thinking/working/waiting/done/error)实时推送到屏幕顶部悬浮 pill,agent 自己忙的时候用户不用切回 mcode 也能看到进度。v0.3.0 增加 io.minimax.mcode 客户端扩展(Hooks 草案),与 MiniMax-Code-Plugins PR #20 的 portable Hooks 提案对齐;mcode 0.2.4+ Runtime 触发,registry 接受后零改动生效。",
"version": "0.4.0",
"description": "Windows 桌面灵动岛 (Dynamic Island) 状态窗口:让 mcode agent 把工作状态(idle/thinking/working/waiting/done/error)实时推送到屏幕顶部悬浮 pill,agent 自己忙的时候用户不用切回 mcode 也能看到进度。v0.4.0 扩展 status.json schema 增加 step/total/detail 三个字段并更新 widget 渲染:agent 现在可以推送 'step 3/12 · fill username field' 这类 sub-step 进度,Computer Use 多步循环、长任务分阶段展示。Backward compat:所有新字段 optional。v0.3.0 增加 io.minimax.mcode 客户端扩展(Hooks 草案),与 MiniMax-Code-Plugins PR #20 的 portable Hooks 提案对齐;mcode 0.2.4+ Runtime 触发,registry 接受后零改动生效。",
"author": {
"name": "antianqi",
"url": "https://github.com/antianqi"
Expand Down
58 changes: 55 additions & 3 deletions plugins/antianqi/mcode-island/scripts/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ const checkEntry = async (event, entry) => {
};

const main = async () => {
console.log(`mcode-island v0.3.0 self-check`);
console.log(`mcode-island v0.4.0 self-check`);
console.log(`plugin root: ${PLUGIN_ROOT}`);
console.log('-'.repeat(60));

Expand All @@ -182,8 +182,8 @@ const main = async () => {
} else {
out('PASS', `plugin.json: name is "${plugin.name}"`);
}
if (plugin.version !== '0.3.0') {
out('FAIL', `plugin.json: version is "${plugin.version}", expected "0.3.0"`);
if (plugin.version !== '0.4.0') {
out('FAIL', `plugin.json: version is "${plugin.version}", expected "0.4.0"`);
} else {
out('PASS', `plugin.json: version is "${plugin.version}"`);
}
Expand Down Expand Up @@ -311,6 +311,58 @@ const main = async () => {
out('PASS', '_lib.ps1: shared helper present');
}

// 5c1. Sub-step progress extension (round-13 refactor).
// notify-island.ps1 must accept -Step/-Total/-Detail and write them
// into status.json. mcode-island.ps1 widget must define
// Build-DisplayMessage and pass step/total/detail to it.
// _lib.ps1 Format-ToolSummary must extract mcode-computer-use
// action+coordinate; Push-Island must forward the new fields.
// The detailed functional tests live in test-substep-progress.mjs;
// here we lock the surface contract so a future refactor that
// drops the params surfaces in smoke (fast path) before reaching
// the slower pwsh-spawned tests.
const substepNotifyPath = join(PLUGIN_ROOT, 'notify-island.ps1');
const substepWidgetPath = join(PLUGIN_ROOT, 'mcode-island.ps1');
if (!(await exists(substepNotifyPath))) {
out('FAIL', 'notify-island.ps1 missing (sub-step lock skipped)');
} else {
const notify = await readFile(substepNotifyPath, 'utf8');
if (!/\[int\]\$Step\s*=\s*-1/.test(notify) ||
!/\[int\]\$Total\s*=\s*-1/.test(notify) ||
!/\[string\]\$Detail\s*=\s*''/.test(notify)) {
out('FAIL', 'notify-island.ps1: missing -Step/-Total/-Detail params');
} else {
out('PASS', 'notify-island.ps1: declares -Step -Total -Detail');
}
if (!/step\s*=\s*\$Step/.test(notify) ||
!/total\s*=\s*\$Total/.test(notify) ||
!/detail\s*=\s*\$Detail/.test(notify)) {
out('FAIL', 'notify-island.ps1: status.json payload missing step/total/detail fields');
} else {
out('PASS', 'notify-island.ps1: writes step/total/detail to status.json');
}
}
if (await exists(substepWidgetPath)) {
const widget = await readFile(substepWidgetPath, 'utf8');
if (!/function Build-DisplayMessage/.test(widget)) {
out('FAIL', 'mcode-island.ps1: Build-DisplayMessage function missing');
} else {
out('PASS', 'mcode-island.ps1: Build-DisplayMessage function present');
}
if (!/\[int\]\$Step\s*=\s*-1/.test(widget) ||
!/\[int\]\$Total\s*=\s*-1/.test(widget)) {
out('FAIL', 'mcode-island.ps1: Update-State missing Step/Total params');
} else {
out('PASS', 'mcode-island.ps1: Update-State accepts Step/Total/Detail');
}
}
const substepTestPath = join(PLUGIN_ROOT, 'scripts', 'test-substep-progress.mjs');
if (!(await exists(substepTestPath))) {
out('WARN', 'scripts/test-substep-progress.mjs missing (sub-step detailed tests not run)');
} else {
out('PASS', 'scripts/test-substep-progress.mjs exists (run separately for full suite)');
}

// 5b. Drift lock: permission-request.ps1 must emit `{"decision":"ask"}`,
// not `allow` or `deny`. The 0.2.4 Runtime default for PermissionRequest
// is fail-closed; an observer Hook that returns `allow` or `deny`
Expand Down
Loading
Loading