Files
multica/server/pkg/agent/pi_invocation_windows.go
Bohan Jiang 2bda4065d0 MUL-2708: fix(agent): preserve multi-line Pi prompt on Windows by bypassing the .cmd shim (#3417)
Pi is installed on Windows via npm, which lays down `pi.cmd` → `pi.ps1`
→ `node_modules/@mariozechner/pi-coding-agent/dist/cli.js`. The daemon
spawns Pi with `exec.Command("pi", ...)`; PATHEXT resolves that to
`pi.cmd`, and cmd.exe expands `%*` in the shim by re-tokenising the
original command line, which truncates any argv containing newlines.

buildPiArgs passes the full prompt as the last positional argv, so the
multi-line system+user prompt is silently cut at the first newline
before it reaches the JS entrypoint. The session JSONL then records
only the first line ("You are running as a chat assistant for a Multica
workspace.") and Pi replies as if the user message were missing
(GitHub multica-ai/multica#3306).

Mirror the existing cursor-agent fix: when LookPath resolves Pi to a
.cmd/.bat launcher and a sibling pi.ps1 exists, invoke PowerShell with
`-File <ps1>` directly and forward each arg as a discrete token. This
keeps us on the official launch path while skipping the cmd.exe %*
re-expansion. Falls back to the original launcher when pi.ps1 or
PowerShell can't be located.

The Windows test asserts the rewrite produces the expected argv and
that the multi-line positional prompt survives unchanged.

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-28 12:36:16 +08:00

61 lines
1.6 KiB
Go

//go:build windows
package agent
import (
"log/slog"
"os"
"path/filepath"
"strings"
)
// platformPiInvocation rewrites the pi invocation on Windows when the
// resolved executable is the npm-installed pi.cmd launcher (or a .bat
// alias) that delegates to pi.ps1.
//
// We replace
//
// pi.cmd <args...>
//
// with
//
// powershell.exe -NoProfile -ExecutionPolicy Bypass -File pi.ps1 <args...>
//
// which is what the .cmd does internally, but lets Go pass each arg as
// a discrete token instead of routing through cmd.exe's %* re-expansion
// (which mangles the multi-line positional prompt the daemon builds in
// buildPiArgs — see #3306).
//
// powerShellLookup is shared with the cursor backend: both npm shims
// have the same launcher shape and need the same PowerShell host on the
// same Windows installation.
func platformPiInvocation(lookedUp string, args []string, logger *slog.Logger) (string, []string, bool) {
ext := strings.ToLower(filepath.Ext(lookedUp))
if ext != ".cmd" && ext != ".bat" {
return "", nil, false
}
dir := filepath.Dir(lookedUp)
ps1 := filepath.Join(dir, "pi.ps1")
if st, err := os.Stat(ps1); err != nil || st.IsDir() {
return "", nil, false
}
psExe, ok := powerShellLookup()
if !ok {
return "", nil, false
}
full := make([]string, 0, 5+len(args))
full = append(full, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ps1)
full = append(full, args...)
if logger != nil {
logger.Info("pi: routing through powershell -File to preserve argv tokens",
"powershell", psExe,
"ps1", ps1,
"original", lookedUp,
)
}
return psExe, full, true
}