- 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
Add a new context menu option for files that opens them in the system's
default application. The option appears in the top section of the menu,
after Preview and Document Graph, as the third item.
Tab naming was failing with 'zsh:23: unmatched' errors when running
via SSH because the prompt was being passed through multiple layers
of shell escaping (local spawn -> SSH -> remote zsh -> bash -c).
Fix by detecting SSH remote execution and sending the prompt via stdin
with --input-format stream-json instead of embedding it in the command
line. This completely bypasses shell escaping issues.
Changes:
- Add shouldSendPromptViaStdin flag when SSH remote is configured
- Pass useStdin option to buildSshCommand for proper SSH configuration
- Add sendPromptViaStdin to ProcessManager spawn call
- Add test case for SSH remote tab naming
The tab naming prompt was too verbose, causing agents to output full
sentences instead of short 2-4 word names.
Changes:
- Simplified tab-naming.md prompt to be more concise and direct
- Improved extractTabName() to filter out bad output:
- Removes markdown headers (##, ###)
- Removes common preamble phrases ("Here's", "Tab name:")
- Filters lines containing "example", "message:", "rules:"
- Removes trailing punctuation (periods, colons)
- Reduced max length from 50 to 40 characters
- Added tests for new extraction behaviors
The execRemoteCommand() function was logging every SSH command at
DEBUG level, which created continuous log spam when operating over
SSH connections. This log fired for every remote filesystem operation
(directory listings, file stats, reads, etc.).
The transient error retry logging is preserved so SSH connection
issues are still visible in debug output.
Added test suite covering:
- Triggers tab naming for new AI sessions with text messages
- Respects automaticTabNamingEnabled setting
- Skips existing sessions (with agentSessionId)
- Skips tabs with custom names
- Skips terminal mode
- Skips empty/image-only messages
- Sets isGeneratingName flag during naming
- Handles naming failures gracefully
Moved tab naming logic from onSessionId callback to useInputProcessing
hook. Tab naming now starts immediately when the user sends their first
message, running in parallel with the actual agent request instead of
waiting for the agent to respond with a session ID.
Changes:
- Added automaticTabNamingEnabled prop to UseInputProcessingDeps
- Tab naming triggered in processInput for new AI sessions with text
- Removed ~140 lines of tab naming code from onSessionId handler
- Removed unused automaticTabNamingEnabledRef
The spinner was hidden when tab.state === 'busy', but when a user sends
a message the tab immediately enters busy state. Since tab naming runs
in parallel with the main request, the spinner should show regardless
of the busy state to indicate the naming process is working.
The tabNaming:generateTabName IPC handler was defined in registerAllHandlers()
but that function was never called. Instead, handlers are registered individually
in main/index.ts. Added registerTabNamingHandlers import and call with required
dependencies (processManager, agentDetector, agentConfigsStore, settingsStore).
Also removed debug console.log statements that were added during investigation.