Skip to content

feat: Declarative pane pinning via RunPane CLI #357

Description

@parsakhaz

type: feature-ticket
id: pane-pinning-cli
status: ready
zone: 2
pr:
github: #357

Feature: Declarative pane pinning via RunPane CLI

Intent

An orchestrator (Pane Chat) can create background/no-focus panes through RunPane, but
cannot set or read the Pane UI's pinned state. In a four-workstream dogfood run
(issue #353), all four panes were created correctly in the background — and the user
still had to find and pin each one manually in the UI. The orchestrator should be able
to declare the desired workspace layout (pinned, unfocused) at creation time and
reconcile it later, without stealing the user's focus.

Peeled from #353 during capture of epic #356 as a standalone quick win with its own PR;
nothing in the epic depends on it.

Desired end state

  • runpane panes create ... --pinned --no-focus --json creates a pane that is already
    pinned and not focused; the JSON response carries the effective pinned state.
  • An existing pane can be pinned/unpinned through RunPane, idempotently.
  • Pane list/status responses expose pinned, so an orchestrator can reconcile desired
    vs actual layout.
  • The state persists and matches the Pane UI after app restart.

Before: orchestrator creates 4 panes → user manually pins 4 panes in the UI.
After: orchestrator creates 4 pinned background panes → user's workspace is already
arranged.

Key architecture directions

  • D1 — The public CLI contract is a declarative set (pinned: true|false),
    never a toggle: repeating the same request is idempotent. The existing
    sessions:toggle-favorite command may be reused internally, but toggle semantics
    must not leak into the CLI surface (an orchestrator retrying after a transport
    failure must not flip the state back).
  • D2 — Pinning never implies focus; --pinned --no-focus is a supported and
    primary combination.
  • D3 — Additive and parity-kept: existing commands/JSON untouched, npm CLI /
    daemon schema / generated contracts / Python wrapper updated together
    (scripts/test-runpane-contract.js), machine-readable output stays stdout-clean.
    (Matches epic feat: Event-driven RunPane orchestration #356 D7.)

Proposed approach

  • The persistence already exists: sessions.is_favorite + favorite_pinned_at
    (main/src/database/schema.sql:28), and session creation already accepts
    startPinned (main/src/services/sessionManager.ts:322) — create-time --pinned
    is mostly plumbing that flag through the RunPane create path.
  • sessions:toggle-favorite is registered in the shared command registry
    (main/src/ipc/session.ts:1461), updates the DB and emits session-updated, and is
    daemon-reachable today; add a set-style command (or extend with a target-state
    parameter) rather than exposing the toggle.
  • Surface the new option/command in the RunPane channel list
    (main/src/ipc/runpane.ts:70) and both wrapper dispatch tables
    (packages/runpane/src/cli.ts, packages/runpane-py/src/runpane/cli.py), and add
    pinned to pane list/status serialization.
  • Verb shape (panes update --pinned true|false vs panes pin/panes unpin) is
    /do's call — pick whichever fits the existing command design; feat: add event-driven RunPane orchestration and reliable agent startup #353 accepted either.
  • Note from the gate: the internal naming is "favorite" (is_favorite,
    favorite_pinned_at) while the CLI says "pinned" — pick the mapping deliberately and
    note it in contract help text; and implement the set-style command as a direct DB
    write, not a read-then-toggle wrapper (racy against concurrent UI toggles).

Verification criteria

  • AC1 — WHEN panes create --pinned --no-focus --json completes, the response
    shall include pinned: true and focused: false, and the created session row shall
    have is_favorite set with a non-null favorite_pinned_at.
  • AC2 — WHEN the pin mutation sets pinned true on an unpinned pane, the response
    shall report pinned: true and the DB row shall be updated.
  • AC3 — WHEN the same pin state is requested twice in a row, the second request
    shall succeed and leave the state unchanged (declarative idempotency, D1).
  • AC4 — WHEN panes are listed or queried for status through RunPane, each pane's
    effective pinned state shall be present in the JSON output.
  • AC5 — WHEN the Pane app is restarted after a CLI pin, the pane shall remain
    pinned (state read back from the DB matches the last CLI-declared state).
Criterion Method Command / flow
AC1, AC2, AC3 automated main-process integration tests (pattern: main/src/ipc/runpane.test.ts) [ ]
AC4 automated list/status serialization test [ ]
AC5 automated persistence assert against sessions row across manager re-init [ ]
all automated scripts/test-runpane-contract.js npm/Python parity run [ ]

Out of scope

  • Panel-level pinning (ToolPanelState.isPinned — editor-panel state only today).
  • Any Pane UI changes — the UI already renders favorite/pinned state.
  • Pin-related lifecycle events (epic feat: Event-driven RunPane orchestration #356 phase 2 owns event emission).
  • Changing archive/unpin interactions or pin ordering semantics.

Justification

  • Fast pass, zero questions (Socratic gate, 2026-07-21). What convinced it: a concrete
    recurring incident (four hand-pinned panes), every technical claim file:line-verified,
    the one real design risk (toggle vs declarative) pre-argued in D1 with the
    retry-flip rationale, and scope boundaries explicitly drawn. Socrates independently
    verified in-repo that --no-focus/focused already exist end-to-end and that the
    toggle handler has no focus side effect (so D2 holds on the mutation path).

References (in refs/)

  • refs/discussion.md — decision log from the 2026-07-21 discussion (split decision,
    research findings with file:line evidence).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions