Improvements to Windows shell execution for OpenCode and other shell-based tools:
1. **Binary detection priority**: Changed Windows detection to prefer extensionless
shell scripts (like opencode) over .cmd wrappers. The .cmd files execute through
cmd.exe which has command line length limits and less robust script handling.
2. **Auto-detect shell scripts**: Added shebang detection in ChildProcessSpawner to
automatically enable shell execution for POSIX scripts, preventing ENOENT errors
when trying to execute shell scripts without explicit shell.
3. **Use PowerShell by default**: Changed Windows agent execution to prefer PowerShell
over cmd.exe when available via PSHOME environment variable. PowerShell provides:
- Better handling of POSIX shell scripts (with shebangs)
- Avoids cmd.exe command line length limitations
- Better compatibility with modern tooling
Falls back to cmd.exe if PowerShell unavailable, respects user customizations.
4. **Improved path detection**: Updated Windows path probing to correctly prefer
.exe > extensionless scripts > .cmd in the selection order.
Verifies that nvm4w Node.js installation paths are properly included in
the expanded PATH environment on Windows, ensuring OpenCode and other
npm-installed tools are detected correctly.
OpenCode installed via npm in nvm4w Node installations was not being
detected because C:\nvm4w\nodejs\ path was not included in the expanded
PATH environment. This fix adds support for nvm4w and fallback paths so
OpenCode can be properly discovered and selected as a group chat moderator.
The native HTML select element's dropdown menu was being rendered behind
the modal overlay, making it impossible to interact with the options. This
was caused by a z-index stacking context issue where the modal's overlay
had a higher z-index than the select element.
Fixed by explicitly setting zIndex: 10000 on the select container and
select element, and zIndex: 10001 on the icon, ensuring the dropdown
renders above the modal overlay (z-index: 9999).
Fixes the issue in both NewGroupChatModal and EditGroupChatModal.
- Add customModel field to ModeratorConfig type for storing selected model
- Update NewGroupChatModal to pass agentConfig.model through moderatorConfig
- Update App.tsx handleCreateGroupChat to accept customModel parameter
- Update spawnModeratorSynthesis to pass sessionCustomModel from moderatorConfig
- Update initial moderator spawn to pass sessionCustomModel for both spawns
- Verify modelArgs config produces correct --model flag in arguments
This enables users to select a custom model when creating a group chat,
and ensures the selected model is applied when spawning the OpenCode moderator.
- Symphony modal now receives sessions list for richer contribution context 🧩
- Active contributions show clickable session name with new Terminal icon 🖥️
- Added “navigate to session” flow directly from contribution cards 🧭
- Selecting a session updates active session and auto-closes modal ⚡
- Session lookup wires contributions to matching session IDs seamlessly 🔗
- Renamed empty-state action from “Select Folder” to “Change Folder” for clarity 🧭
- Refreshed empty-state helper text to match the new folder-change flow 📝
- Cleaned up `EditGroupChatModal` icon imports by dropping the unused `X` 🎛️
- Symphony IPC now validates active contributions against stored sessions 🧩
- Orphaned contributions auto-filtered when sessions disappear, keeping UI clean 🧹
- `symphony:getState` returns only session-backed active items for accuracy 🎯
- `symphony:getActive` now excludes contributions tied to missing sessions 🚫
- Added reusable `filterOrphanedContributions` helper with detailed logging 🪵
- Wired `sessionsStore` dependency through main + handler registration flow 🔌
- Integration tests now mock sessions store for realistic state scenarios 🧪
- Expanded handler tests to cover missing-session filtering behavior 🛡️
Merges OpenCode fixes and SSH stdin improvements:
- Use stdin passthrough for all SSH prompts (simplifies escaping)
- Add question:deny to OpenCode permission block for robust tool disabling
- Simplify stdin prompt delivery by appending after exec (no heredoc needed)
Per OpenCode GitHub issue workaround, add "question": "deny" to the
permission block in addition to the existing "tools":{"question":false}.
This ensures the question tool is disabled via both configuration methods,
preventing stdin hangs in batch mode.
Config now: {"permission":{"*":"allow","external_directory":"allow","question":"deny"},"tools":{"question":false}}
Replace heredoc-based prompt delivery with simpler stdin passthrough.
How it works:
1. Bash script is sent via stdin to /bin/bash on the remote
2. Script sets up PATH, cd, env vars, then calls `exec <agent>`
3. The `exec` replaces bash with the agent process
4. The agent inherits stdin and reads the remaining content (the prompt)
Benefits:
- No heredoc syntax needed
- No delimiter collision detection
- No prompt escaping required - prompt is never parsed by any shell
- Works with any prompt content (quotes, newlines, $, backticks, etc.)
- Simpler, more maintainable code
Changes:
- Remove heredoc logic from buildSshCommandWithStdin()
- Update process.ts to use stdin passthrough for ALL agents over SSH
(not just OpenCode - all agents benefit from this approach)
- Update tests to verify stdin passthrough behavior
Verified locally that both OpenCode and Claude Code read prompts from stdin.
When Claude Code performs multi-tool turns (many internal API calls),
accumulated token values cause estimateContextUsage to return null,
freezing the context gauge. This adds estimateAccumulatedGrowth which
provides conservative 1-3% per-turn growth estimates so the gauge
keeps moving during tool-heavy sessions.
Safety: App.tsx caps all estimates at yellowThreshold - 5, guaranteeing
that estimates can never trigger compact warnings — only real
measurements from non-accumulated turns can.
Update SummaryCards to accept sessions prop and filter out terminal-only
sessions when calculating agent count for consistent metrics display.
This ensures queries-per-session and total sessions accurately reflect
AI agent sessions rather than including terminal sessions.
Replace process.platform with navigator.platform for Windows detection
in the inline wizard conversation service, as process.platform is not
available in the browser/renderer context.
Fixes an issue in PR #288's Windows shell fix where full paths like
'C:\Program Files\Git\bin\git' weren't recognized as known commands.
Changes:
- Extract command basename using regex for both Unix and Windows separators
- Change from array to Set for O(1) lookup performance
- Add additional common commands: npx, pnpm, pip, pip3
- Export needsWindowsShell for testability
- Add comprehensive test suite for needsWindowsShell function
IMPORTANT: Prompts must be passed via stdin to avoid CLI argument length
limits. Prompts can be huge and contain arbitrary characters that would
break if passed as command-line arguments.
Changes:
- Add stdinInput parameter to buildSshCommandWithStdin for heredoc-based
prompt delivery
- Use MAESTRO_PROMPT_EOF delimiter with collision detection (appends _N
suffix if prompt contains the delimiter)
- OpenCode prompts now always sent via stdin heredoc, not CLI args
- Add comprehensive tests for heredoc behavior and delimiter collision
- Add comment in process.ts documenting this requirement to prevent
regressions
The heredoc approach: exec opencode 'run' <<'MAESTRO_PROMPT_EOF'
ensures prompts of any size with any characters work correctly.
This is a complete rewrite of SSH remote command execution that eliminates
all shell escaping issues by sending the entire script via stdin.
Previously, the SSH command was built as:
ssh host '/bin/bash -c '\''cd /path && VAR='\''value'\'' cmd arg'\'''
This required complex nested escaping that broke with:
- Heredocs (cat << 'EOF')
- Long prompts (command line length limits)
- Special characters in prompts
Now the SSH command is simply:
ssh host /bin/bash
And the entire script is piped via stdin:
export PATH="$HOME/.local/bin:..."
cd '/project/path'
export OPENCODE_CONFIG_CONTENT='{"permission":...}'
exec opencode run --format json 'prompt here'
Benefits:
- No shell escaping layers (stdin is binary-safe)
- No command line length limits
- Works with any remote shell (bash, zsh, fish)
- Handles any prompt content (quotes, newlines, $, etc.)
- Much simpler to debug and maintain
Changes:
- Add buildSshCommandWithStdin() in ssh-command-builder.ts
- Update process.ts to use stdin-based SSH for all agents
- Add sshStdinScript to ProcessConfig type
- Update ChildProcessSpawner to send stdin script
- Add comprehensive tests for new function
The heredoc syntax (cat << 'EOF' ... EOF) was breaking when passed
through buildSshCommand's single-quote escaping. The '\'' escape
pattern was being applied to the heredoc delimiters, producing
invalid shell syntax like cat << '\''EOF'\''.
Solution: Embed OpenCode prompts directly as positional arguments.
The prompt will be properly escaped by buildRemoteCommand using
shellEscape(), which handles the single-quote escaping correctly
for bash -c command execution.
This was the root cause of SSH remote execution failures with
OpenCode - the OPENCODE_CONFIG_CONTENT env var escaping was
correct, but the heredoc escaping was not.
Changed from centering the tab to using scrollIntoView with 'nearest' option.
This ensures the entire tab including the close button is visible, rather than
potentially cutting off the right edge when near container boundaries.
Add symphony:manualCredit IPC handler to allow crediting contributions
made outside the Symphony workflow (e.g., manual PRs, external
contributors). This enables proper tracking of all contributions
regardless of how they were created.
- Add symphony:manualCredit handler with full validation
- Add preload API for manual credit
- Support all contribution fields (tokens, time, merged status, etc.)
- Prevent duplicate PR credits
- Update contributor stats (streak, repos, totals)
- Add comprehensive tests for validation and success cases
When Claude Code performs multi-tool turns (many internal API calls),
accumulated token values cause estimateContextUsage to return null,
freezing the context gauge. This adds estimateAccumulatedGrowth which
provides conservative 1-3% per-turn growth estimates so the gauge
keeps moving during tool-heavy sessions.
Safety: App.tsx caps all estimates at yellowThreshold - 5, guaranteeing
that estimates can never trigger compact warnings — only real
measurements from non-accumulated turns can.
On Windows, when execFile detects 'git' without extension, it enables shell
mode for PATHEXT resolution. However, shell mode interprets '%' characters in
arguments as environment variable expansions, causing 'git log --pretty=format:%an'
to fail with 'Der Befehl "%an" ist entweder falsch geschrieben...'
The fix adds 'git' (and other known .exe commands) to a list of exceptions
that don't require shell mode, allowing the format string to pass through
to git unchanged. This works because these commands have .exe variants on
Windows and don't need PATHEXT resolution.
IMPORTANT: Prompts must be passed via stdin to avoid CLI argument length
limits. Prompts can be huge and contain arbitrary characters that would
break if passed as command-line arguments.
Changes:
- Add stdinInput parameter to buildSshCommandWithStdin for heredoc-based
prompt delivery
- Use MAESTRO_PROMPT_EOF delimiter with collision detection (appends _N
suffix if prompt contains the delimiter)
- OpenCode prompts now always sent via stdin heredoc, not CLI args
- Add comprehensive tests for heredoc behavior and delimiter collision
- Add comment in process.ts documenting this requirement to prevent
regressions
The heredoc approach: exec opencode 'run' <<'MAESTRO_PROMPT_EOF'
ensures prompts of any size with any characters work correctly.
This is a complete rewrite of SSH remote command execution that eliminates
all shell escaping issues by sending the entire script via stdin.
Previously, the SSH command was built as:
ssh host '/bin/bash -c '\''cd /path && VAR='\''value'\'' cmd arg'\'''
This required complex nested escaping that broke with:
- Heredocs (cat << 'EOF')
- Long prompts (command line length limits)
- Special characters in prompts
Now the SSH command is simply:
ssh host /bin/bash
And the entire script is piped via stdin:
export PATH="$HOME/.local/bin:..."
cd '/project/path'
export OPENCODE_CONFIG_CONTENT='{"permission":...}'
exec opencode run --format json 'prompt here'
Benefits:
- No shell escaping layers (stdin is binary-safe)
- No command line length limits
- Works with any remote shell (bash, zsh, fish)
- Handles any prompt content (quotes, newlines, $, etc.)
- Much simpler to debug and maintain
Changes:
- Add buildSshCommandWithStdin() in ssh-command-builder.ts
- Update process.ts to use stdin-based SSH for all agents
- Add sshStdinScript to ProcessConfig type
- Update ChildProcessSpawner to send stdin script
- Add comprehensive tests for new function
The heredoc syntax (cat << 'EOF' ... EOF) was breaking when passed
through buildSshCommand's single-quote escaping. The '\'' escape
pattern was being applied to the heredoc delimiters, producing
invalid shell syntax like cat << '\''EOF'\''.
Solution: Embed OpenCode prompts directly as positional arguments.
The prompt will be properly escaped by buildRemoteCommand using
shellEscape(), which handles the single-quote escaping correctly
for bash -c command execution.
This was the root cause of SSH remote execution failures with
OpenCode - the OPENCODE_CONFIG_CONTENT env var escaping was
correct, but the heredoc escaping was not.
Changed from centering the tab to using scrollIntoView with 'nearest' option.
This ensures the entire tab including the close button is visible, rather than
potentially cutting off the right edge when near container boundaries.
Add symphony:manualCredit IPC handler to allow crediting contributions
made outside the Symphony workflow (e.g., manual PRs, external
contributors). This enables proper tracking of all contributions
regardless of how they were created.
- Add symphony:manualCredit handler with full validation
- Add preload API for manual credit
- Support all contribution fields (tokens, time, merged status, etc.)
- Prevent duplicate PR credits
- Update contributor stats (streak, repos, totals)
- Add comprehensive tests for validation and success cases
The fileTree prop was passed as `activeSession?.fileTree || []` which
creates a new array reference on every render, defeating React.memo()
on FilePreview during agent activity. Memoize it with useMemo so the
reference only changes when the actual fileTree changes.
Update test expectations to match the actual extractTabName logic:
- Lines > 40 chars are filtered out (not truncated), returning null
- Lines starting with quotes are filtered as example inputs
- Use period separator to test multi-line filtering correctly
- Preamble test uses exact pattern that regex matches
Remove overflow-hidden from parent containers that was clipping the
absolutely positioned tooltip overlays. The responsive text truncation
is now handled by individual element classes (truncate, max-w-[120px])
rather than container-level overflow clipping.
Changes:
- MainPanel.tsx: Remove overflow-hidden from flex containers, add truncate
to session name, use shrink-0 on git badge container
- GitStatusWidget.tsx: Use shrink-0 instead of overflow-visible
The git branch badge and GitStatusWidget hover overlays were being clipped
by overflow-hidden on their parent containers. Added overflow-visible to
the tooltip container divs while keeping overflow-hidden on the outer
parent containers to preserve responsive truncation behavior.
Changes:
- MainPanel.tsx: Added overflow-visible to git badge's relative container
- GitStatusWidget.tsx: Added overflow-visible to widget's relative container