Skip to content

Startup commands frequently don't run on launch (panes processed before PTY session is ready; background workspace never connects) #14

Open

Description

@frd1201

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 a claude --rc "…" startup command

Steps to reproduce

  1. Create two workspaces, each with multiple panes and a startupCommand, both launchOnStartup: true.
  2. Start Tabby (cold).
  3. 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 ms processChildTabs call 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 until session.output$ actually exists, then wait for the shell to be ready before sendInput. The most robust readiness signal I found is prompt detection: watch output$, 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. Open launchOnStartup workspaces 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

  1. frd1201 commented on Jun 22, 2026

    @frd1201
    Author

    Opened #16 (against dev) implementing all three fixes described above:

    1. Poll for panes instead of the single 300 ms getAllTabs() shot — panes constructed late are now picked up.
    2. Wait for real readiness — poll until session.output$ exists, then wait for the shell prompt to render (ANSI-stripped prompt detection with output-settle + hard-cap fallbacks) before sending.
    3. Serialize startup launches — registerCommands() returns a promise that resolves once the batch is delivered, and openWorkspace() 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions