|
@@ -115,6 +115,8 @@ export class ZellijMultiplexer implements Multiplexer {
|
|
|
this.firstPaneUsed = true;
|
|
this.firstPaneUsed = true;
|
|
|
return { success: true, paneId: this.firstPaneId };
|
|
return { success: true, paneId: this.firstPaneId };
|
|
|
}
|
|
}
|
|
|
|
|
+ // Reuse failed — don't keep retrying a known-bad pane
|
|
|
|
|
+ this.firstPaneUsed = true;
|
|
|
// fall through to createPaneInAgentTab on failure
|
|
// fall through to createPaneInAgentTab on failure
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -323,7 +325,7 @@ export class ZellijMultiplexer implements Multiplexer {
|
|
|
|
|
|
|
|
private async ensureAgentTab(
|
|
private async ensureAgentTab(
|
|
|
zellij: string,
|
|
zellij: string,
|
|
|
- ): Promise<{ tabId: string; firstPaneId: string } | null> {
|
|
|
|
|
|
|
+ ): Promise<{ tabId: string; firstPaneId: string | null } | null> {
|
|
|
try {
|
|
try {
|
|
|
// Try to find existing tab
|
|
// Try to find existing tab
|
|
|
const existingTab = await this.findTabByName(zellij, 'opencode-agents');
|
|
const existingTab = await this.findTabByName(zellij, 'opencode-agents');
|
|
@@ -334,13 +336,10 @@ export class ZellijMultiplexer implements Multiplexer {
|
|
|
);
|
|
);
|
|
|
return {
|
|
return {
|
|
|
tabId: existingTab.tabId,
|
|
tabId: existingTab.tabId,
|
|
|
- firstPaneId: firstPane || 'terminal_0',
|
|
|
|
|
|
|
+ firstPaneId: firstPane,
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Get panes before creating tab
|
|
|
|
|
- const beforePanes = await this.listPanes(zellij);
|
|
|
|
|
-
|
|
|
|
|
// Create new tab
|
|
// Create new tab
|
|
|
const createProc = crossSpawn(
|
|
const createProc = crossSpawn(
|
|
|
[zellij, 'action', 'new-tab', '--name', 'opencode-agents'],
|
|
[zellij, 'action', 'new-tab', '--name', 'opencode-agents'],
|
|
@@ -353,11 +352,25 @@ export class ZellijMultiplexer implements Multiplexer {
|
|
|
const newTab = await this.findTabByName(zellij, 'opencode-agents');
|
|
const newTab = await this.findTabByName(zellij, 'opencode-agents');
|
|
|
if (!newTab) return null;
|
|
if (!newTab) return null;
|
|
|
|
|
|
|
|
- // Get the new pane
|
|
|
|
|
- const afterPanes = await this.listPanes(zellij);
|
|
|
|
|
- const newPane = afterPanes.find((p) => !beforePanes.includes(p));
|
|
|
|
|
|
|
+ // Get the default pane in the new tab
|
|
|
|
|
+ const firstPane = await this.getFirstPaneInTab(zellij, newTab.tabId);
|
|
|
|
|
+ return { tabId: newTab.tabId, firstPaneId: firstPane };
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return { tabId: newTab.tabId, firstPaneId: newPane || 'terminal_0' };
|
|
|
|
|
|
|
+ private async listPanesJson(
|
|
|
|
|
+ zellij: string,
|
|
|
|
|
+ ): Promise<ZellijPaneInfo[] | null> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const proc = crossSpawn(
|
|
|
|
|
+ [zellij, 'action', 'list-panes', '--json', '--tab', '--all'],
|
|
|
|
|
+ { stdout: 'pipe', stderr: 'pipe' },
|
|
|
|
|
+ );
|
|
|
|
|
+ if ((await proc.exited) !== 0) return null;
|
|
|
|
|
+ const stdout = await proc.stdout();
|
|
|
|
|
+ return JSON.parse(stdout) as ZellijPaneInfo[];
|
|
|
} catch {
|
|
} catch {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
@@ -367,26 +380,17 @@ export class ZellijMultiplexer implements Multiplexer {
|
|
|
zellij: string,
|
|
zellij: string,
|
|
|
tabId: string,
|
|
tabId: string,
|
|
|
): Promise<string | null> {
|
|
): Promise<string | null> {
|
|
|
- const originalTab = await this.getCurrentTabId(zellij);
|
|
|
|
|
- await crossSpawn([zellij, 'action', 'go-to-tab-by-id', tabId], {
|
|
|
|
|
- stdout: 'ignore',
|
|
|
|
|
- stderr: 'ignore',
|
|
|
|
|
- }).exited;
|
|
|
|
|
-
|
|
|
|
|
- const panes = await this.listPanes(zellij);
|
|
|
|
|
-
|
|
|
|
|
- // Restore original tab
|
|
|
|
|
- if (originalTab) {
|
|
|
|
|
- await crossSpawn(
|
|
|
|
|
- [zellij, 'action', 'go-to-tab-by-id', String(originalTab)],
|
|
|
|
|
- {
|
|
|
|
|
- stdout: 'ignore',
|
|
|
|
|
- stderr: 'ignore',
|
|
|
|
|
- },
|
|
|
|
|
- ).exited;
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ const panes = await this.listPanesJson(zellij);
|
|
|
|
|
+ if (!panes) return null;
|
|
|
|
|
+ const pane = panes.find(
|
|
|
|
|
+ (candidate) =>
|
|
|
|
|
+ !candidate.is_plugin && candidate.tab_id === Number(tabId),
|
|
|
|
|
+ );
|
|
|
|
|
+ return pane ? `terminal_${pane.id}` : null;
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return panes[0] || null;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private async findTabByName(
|
|
private async findTabByName(
|
|
@@ -473,27 +477,6 @@ export class ZellijMultiplexer implements Multiplexer {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private async listPanes(zellij: string): Promise<string[]> {
|
|
|
|
|
- try {
|
|
|
|
|
- const proc = crossSpawn([zellij, 'action', 'list-panes'], {
|
|
|
|
|
- stdout: 'pipe',
|
|
|
|
|
- stderr: 'pipe',
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- const exitCode = await proc.exited;
|
|
|
|
|
- if (exitCode !== 0) return [];
|
|
|
|
|
-
|
|
|
|
|
- const stdout = await proc.stdout();
|
|
|
|
|
- return stdout
|
|
|
|
|
- .split('\n')
|
|
|
|
|
- .slice(1)
|
|
|
|
|
- .map((line) => line.trim().split(/\s+/)[0])
|
|
|
|
|
- .filter((id) => id?.startsWith('terminal_'));
|
|
|
|
|
- } catch {
|
|
|
|
|
- return [];
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
async closePane(paneId: string): Promise<boolean> {
|
|
async closePane(paneId: string): Promise<boolean> {
|
|
|
const zellij = await this.getBinary();
|
|
const zellij = await this.getBinary();
|
|
|
return gracefulClosePane(zellij, paneId, {
|
|
return gracefulClosePane(zellij, paneId, {
|
|
@@ -541,24 +524,13 @@ export class ZellijMultiplexer implements Multiplexer {
|
|
|
paneId: string,
|
|
paneId: string,
|
|
|
): Promise<string | null> {
|
|
): Promise<string | null> {
|
|
|
try {
|
|
try {
|
|
|
- const proc = crossSpawn(
|
|
|
|
|
- [zellij, 'action', 'list-panes', '--json', '--tab', '--all'],
|
|
|
|
|
- {
|
|
|
|
|
- stdout: 'pipe',
|
|
|
|
|
- stderr: 'pipe',
|
|
|
|
|
- },
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- if ((await proc.exited) !== 0) return null;
|
|
|
|
|
-
|
|
|
|
|
- const stdout = await proc.stdout();
|
|
|
|
|
- const panes: ZellijPaneInfo[] = JSON.parse(stdout);
|
|
|
|
|
|
|
+ const panes = await this.listPanesJson(zellij);
|
|
|
|
|
+ if (!panes) return null;
|
|
|
const normalizedPaneId = normalizePaneId(paneId);
|
|
const normalizedPaneId = normalizePaneId(paneId);
|
|
|
const pane = panes.find(
|
|
const pane = panes.find(
|
|
|
(candidate) =>
|
|
(candidate) =>
|
|
|
!candidate.is_plugin && String(candidate.id) === normalizedPaneId,
|
|
!candidate.is_plugin && String(candidate.id) === normalizedPaneId,
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
return pane?.tab_id === undefined ? null : String(pane.tab_id);
|
|
return pane?.tab_id === undefined ? null : String(pane.tab_id);
|
|
|
} catch {
|
|
} catch {
|
|
|
return null;
|
|
return null;
|