Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions contracts/runpane/contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,18 @@
"paneRenameResult"
]
},
{
"name": "panes focus",
"summary": "Raise the Pane window and select a Pane (and optionally one of its panels) on explicit user request.",
"usage": [
"runpane panes focus --pane <pane-id> [--panel <panel-id>] --source user|agent --yes [--json]"
],
"mutates": true,
"jsonSchemas": [
"paneFocusRequest",
"paneFocusResult"
]
},
{
"name": "panels create",
"summary": "Create a terminal-backed tool panel inside an existing Pane session.",
Expand Down Expand Up @@ -861,6 +873,20 @@
" --dry-run Validate and preview without renaming the pane",
" --yes Skip confirmation for mutating commands"
],
"panes focus": [
"Usage:",
" runpane panes focus --pane <pane-id> [--panel <panel-id>] --source user|agent --yes [--json]",
"",
"Raises the Pane window and selects a Pane (and optionally one of its panels) exactly like clicking it in the UI. Steals window focus, so run it only on an explicit user request to open, focus, or show a Pane, never proactively.",
"",
"Options:",
" --pane <pane-id> Pane/session id to focus",
" --panel <panel-id> Optional panel/tab id inside the Pane to select",
" --source <user|agent> Mutation source; does not change focus behavior",
" --pane-dir <path> Connect to a specific Pane data directory",
" --json Print machine-readable output",
" --yes Skip confirmation for mutating commands"
],
"panels": [
"Terminal-backed panel commands.",
"",
Expand Down Expand Up @@ -1287,6 +1313,20 @@
" --dry-run",
" --yes"
],
"panes focus": [
"Usage:",
" runpane panes focus --pane <pane-id> [--panel <panel-id>] --source user|agent --yes [--json]",
"",
"Raises the Pane window and selects a Pane (and optionally one of its panels) exactly like clicking it in the UI. Steals window focus, so run it only on an explicit user request to open, focus, or show a Pane, never proactively.",
"",
"Options:",
" --pane <pane-id>",
" --panel <panel-id>",
" --source <user|agent>",
" --pane-dir <path>",
" --json",
" --yes"
],
"panels": [
"Terminal-backed panel commands.",
"",
Expand Down Expand Up @@ -1546,6 +1586,7 @@
"For `panes create --wait-ready`, `initialInput.verifiedSubmitted: true` is reported only after argument attachment or composer-clear plus activity evidence. Routing input does not by itself verify submission.",
"`runpane panes archive` archives a Pane exactly like the UI Archive action, including removal of its Pane-managed git worktree, and refuses (unless `--force`) when the pane's branch has uncommitted, untracked, or unpushed-to-remote changes. It waits for worktree removal to finish before returning and reports the outcome in `worktreeCleanup`.",
"`runpane panes rename` trims and updates a Pane's display name without changing its worktree, branch, panels, or focus, and returns the updated pane summary.",
"`runpane panes focus` raises the Pane window and selects a Pane (and optionally one of its panels) exactly like clicking it in the UI. Because it steals the user's window focus, run it only on an explicit user request to open, focus, show, or switch to a Pane; never focus a Pane proactively, the same doctrine that keeps `panes create` background/no-focus for `--source agent`.",
"`runpane panels list` lists tool panels inside one Pane session.",
"`runpane panels output` reads bounded recent terminal output from one panel and strips common terminal control noise for agent use.",
"`runpane panels input` sends exact input bytes to one terminal panel. Prefer `--input-file` for newlines, Ctrl-C, quotes, or shell-sensitive text.",
Expand Down Expand Up @@ -1746,6 +1787,18 @@
"--yes",
"--json"
],
[
"panes",
"focus",
"--pane",
"session-1",
"--panel",
"panel-1",
"--source",
"user",
"--yes",
"--json"
],
[
"panels",
"list",
Expand Down Expand Up @@ -3059,6 +3112,50 @@
},
"additionalProperties": false
},
"paneFocusRequest": {
"type": "object",
"required": [
"paneId"
],
"properties": {
"paneId": {
"type": "string"
},
"panelId": {
"type": "string"
},
"source": {
"enum": [
"user",
"agent"
]
}
},
"additionalProperties": false
},
"paneFocusResult": {
"type": "object",
"required": [
"ok",
"paneId",
"focused"
],
"properties": {
"ok": {
"const": true
},
"paneId": {
"type": "string"
},
"panelId": {
"type": "string"
},
"focused": {
"const": true
}
},
"additionalProperties": false
},
"paneArchiveResult": {
"oneOf": [
{
Expand Down Expand Up @@ -4272,6 +4369,17 @@
"--json"
]
},
{
"name": "panes focus",
"summary": "Raise the Pane window and select a Pane (and optionally a panel) on explicit user request.",
"arguments": [
"--pane <pane-id>",
"--panel <panel-id>",
"--source <user|agent>",
"--yes",
"--json"
]
},
{
"name": "panels create",
"summary": "Create reviewer/helper terminal tabs inside an existing Pane; they share that Pane's worktree.",
Expand Down Expand Up @@ -5046,6 +5154,56 @@
"The JSON result includes the updated pane summary so callers do not need a second panes list request."
]
},
"panes focus": {
"name": "panes focus",
"summary": "Raise the Pane window and select a Pane (and optionally one of its panels).",
"details": "Brings the Pane desktop window to the foreground and selects the requested Pane exactly like clicking it in the sidebar; when --panel is given it also selects that panel/tab inside the Pane. This steals window focus, so it must only be run on an explicit user request to open, focus, or show a Pane, never proactively. Focusing does not change the Pane's worktree, branch, panels, or pinned state.",
"requiresPaneDaemon": true,
"mutates": true,
"arguments": [
{
"name": "--pane",
"value": "<pane-id>",
"required": true,
"description": "Pane/session id to focus. Must be an active (non-archived) Pane."
},
{
"name": "--panel",
"value": "<panel-id>",
"required": false,
"description": "Optional panel/tab id inside the Pane to select; must belong to the focused Pane."
},
{
"name": "--source",
"value": "<user|agent>",
"required": false,
"description": "Mutation source; does not change focus behavior."
},
{
"name": "--yes",
"required": false,
"description": "Skip confirmation for mutating commands; required in non-interactive contexts because focusing steals window focus."
},
{
"name": "--json",
"required": false,
"description": "Print machine-readable output."
}
],
"examples": [
"runpane panes focus --pane <pane-id> --source user --yes --json",
"runpane panes focus --pane <pane-id> --panel <panel-id> --source user --yes --json"
],
"jsonSchemas": [
"paneFocusRequest",
"paneFocusResult"
],
"notes": [
"Run this only when the user explicitly asks to open, focus, show, or switch to a Pane; never focus a Pane proactively because it steals the user's window focus.",
"Archived Panes cannot be focused; the command fails clearly instead of falling back to focusing another Pane.",
"If --panel does not belong to the focused Pane, the command fails clearly rather than selecting a different panel."
]
},
"panels list": {
"name": "panels list",
"summary": "List tool panels inside a Pane session.",
Expand Down
4 changes: 4 additions & 0 deletions contracts/runpane/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@
"panes pin",
"panes unpin",
"panes rename",
"panes focus",
"panels",
"panels create",
"panels list",
Expand Down Expand Up @@ -417,6 +418,9 @@
"panes rename": {
"$ref": "#/$defs/helpLines"
},
"panes focus": {
"$ref": "#/$defs/helpLines"
},
"panels": {
"$ref": "#/$defs/helpLines"
},
Expand Down
3 changes: 3 additions & 0 deletions docs/RUNPANE_CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ For `panes create --wait-ready`, `initialInput.verifiedSubmitted: true` is repor

`runpane panes rename` trims and updates a Pane's display name without changing its worktree, branch, panels, or focus, and returns the updated pane summary.

`runpane panes focus` raises the Pane window and selects a Pane (and optionally one of its panels) exactly like clicking it in the UI. Because it steals the user's window focus, run it only on an explicit user request to open, focus, show, or switch to a Pane; never focus a Pane proactively, the same doctrine that keeps `panes create` background/no-focus for `--source agent`.

`runpane panels list` lists tool panels inside one Pane session.

`runpane panels output` reads bounded recent terminal output from one panel and strips common terminal control noise for agent use.
Expand Down Expand Up @@ -200,6 +202,7 @@ Brief tools:
- `panes pin`: Declaratively pin a Pane (the Pane UI's favorite/pin star) without changing focus.
- `panes unpin`: Declaratively unpin a Pane (the Pane UI's favorite/pin star) without changing focus.
- `panes rename`: Rename a Pane without changing its worktree, branch, panels, or focus.
- `panes focus`: Raise the Pane window and select a Pane (and optionally a panel) on explicit user request.
- `panels create`: Create reviewer/helper terminal tabs inside an existing Pane; they share that Pane's worktree.
- `panels list`: List tool panels inside a Pane session.
- `panels output`: Read recent terminal output from a panel.
Expand Down
17 changes: 14 additions & 3 deletions frontend/src/hooks/useIPCEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,20 @@ export function useIPCEvents() {
});
unsubscribeFunctions.push(unsubscribeSessionUpdated);

const unsubscribeSessionDeleted = window.electronAPI.events.onSessionDeleted((sessionData) => {
devLog.debug('[useIPCEvents] Session deleted:', sessionData);
const sessionId = sessionData.id;
const unsubscribePaneFocusRequested = window.electronAPI.events.onPaneFocusRequested(({ paneId, panelId }) => {
console.log('[useIPCEvents] Pane focus requested:', { paneId, panelId });
void useSessionStore.getState().setActiveSession(paneId).then(() => {
if (panelId) {
usePanelStore.getState().setActivePanel(paneId, panelId);
}
});
});
unsubscribeFunctions.push(unsubscribePaneFocusRequested);

const unsubscribeSessionDeleted = window.electronAPI.events.onSessionDeleted((sessionData: SessionDeletedEventData | string) => {
console.log('[useIPCEvents] Session deleted:', sessionData);
// The backend sends just { id } for deleted sessions
const sessionId = typeof sessionData === 'string' ? sessionData : sessionData.id || sessionData.sessionId;

// Drain any pending throttled git status calls for this session
if (sessionId) {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/types/electron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,8 @@ interface ElectronAPI {
onPermissionResolved: (callback: (event: PanePermissionResolvedEvent) => void) => () => void;
onSessionCreated: (callback: (session: Session) => void) => () => void;
onSessionUpdated: (callback: (session: Session) => void) => () => void;
onSessionDeleted: (callback: (session: Pick<Session, 'id'>) => void) => () => void;
onPaneFocusRequested: (callback: (data: { paneId: string; panelId?: string }) => void) => () => void;
onSessionDeleted: (callback: (session: Session) => void) => () => void;
onSessionsLoaded: (callback: (sessions: Session[]) => void) => () => void;
onSessionOutput: (callback: (output: SessionOutput) => void) => () => void;
onSessionLog: (callback: (data: { sessionId: string; entry: LogEntry }) => void) => () => void;
Expand Down
Loading