Repository navigation
Startup commands frequently don't run on launch (panes processed before PTY session is ready; background workspace never connects) #14
Description
Summary
Per-pane startup commands frequently don't run when a workspace launches — intermittently for a single workspace, and almost always for the second workspace when two have launchOnStartup: true. On Windows the delivery rate I measured was only ~11% of panes on cold start.
I instrumented this fairly thoroughly (replaced startup commands with marker-file writes to measure per-pane delivery objectively, and captured the plugin's own console.log output from the renderer via the Chrome DevTools Protocol). There are three independent root causes, all timing/lifecycle related.
Environment
- TabbySpaces 0.2.7
- Tabby 1.0.2xx, Windows 11
- Profiles in use:
clink,cmd, and PowerShell (default) - Two workspaces with
launchOnStartup: true, 6 panes total, each running aclaude --rc "…"startup command
Steps to reproduce
- Create two workspaces, each with multiple panes and a
startupCommand, bothlaunchOnStartup: true. - Start Tabby (cold).
- Observe: many panes never execute their startup command. The second workspace's panes fail almost every time.
Root causes (with evidence)
1. Pane discovery is a single 300 ms shot.
StartupCommandService.onTabOpened() does setTimeout(() => this.processChildTabs(tab), 300) and calls getAllTabs() exactly once. If the split panes aren't constructed yet at 300 ms, getAllTabs() returns 0 and every command for that workspace is silently skipped — no retry.
Observed log: Found child tabs: 0.
2. The command is sent before the PTY session exists.
In processTerminalTab() the code checks terminalTab.session?.output$. At processing time the session is usually still null, so it takes the else branch and blind-fires setTimeout(sendCommand, 500) into a shell that isn't ready yet → the typed input is dropped (clink/conpty in particular drop input typed during shell init).
Observed log: No session.output$, falling back to timeout for every pane, followed by sends that never land.
3. Background-workspace panes never connect their session.
When two launchOnStartup workspaces open, only the active tab initializes its PTYs. The workspace that ends up in the background never gets a session at all, so there is nothing to send to.
Observed log: every background pane ends in a permanent "no session" state.
(Side note: the cwd-not-applied reports in #10/#12 may share root cause #2 — the pane isn't actually ready when the plugin acts on it.)
Proposed fix
Three coordinated changes (I have a working implementation and ~100% delivery after applying all three; happy to open a PR):
src/services/startupCommand.service.ts— poll for panes. Replace the single 300 msprocessChildTabscall with a short re-scan loop that processes panes as they appear and stops only when all pending commands are handled (or a sane cap). Fixes Welcome to TabbySpaces Discussions! #1.src/services/startupCommand.service.ts— wait for real readiness. Poll untilsession.output$actually exists, then wait for the shell to be ready beforesendInput. The most robust readiness signal I found is prompt detection: watchoutput$, strip ANSI, and fire once the output ends in a prompt char (>/$/#/❯), with an output-settle timeout only as a fallback. This replaces the blind 500 ms / first-output heuristic. Fixes The title and color set on Workspace Editor can be shown on tabs #2.src/providers/toolbar.provider.ts— serialize startup launches. OpenlaunchOnStartupworkspaces one at a time, keeping each foreground until its sessions are connected and its commands sent, then open the next. Fixes Release v0.2.0 #3.
A cleaner long-term option
The fundamental fragility is typing the command into the terminal after the prompt appears. The fully race-free approach is to pass the command via the shell's own launch arguments instead — e.g. cmd /k "<cmd>", pwsh -NoExit -Command "<cmd>", bash -c '<cmd>; exec bash' — so the shell runs it itself when it's ready. That removes the timing heuristics and the need to serialize launches. (clink makes the cmd case a bit trickier since it's already injected via args.)
Measured impact of the typing-based fix above: ~11% → 100% pane delivery across cold starts on Windows (clink/cmd/PowerShell), including under load.
Happy to submit a PR if this direction sounds good.
Activity
frd1201 commentedon Jun 22, 2026
Opened #16 (against
dev) implementing all three fixes described above:getAllTabs()shot — panes constructed late are now picked up.session.output$exists, then wait for the shell prompt to render (ANSI-stripped prompt detection with output-settle + hard-cap fallbacks) before sending.registerCommands()returns a promise that resolves once the batch is delivered, andopenWorkspace()awaits it so each workspace stays foreground until its sessions connect.Each change is capped/timeout-guarded so one stuck pane can't block the rest. Production build passes; I wasn't able to runtime-test on Windows myself, so a cold-start verification there before merge would be ideal.