From e5ec559abe193b52d21cc913de77985ae123665b Mon Sep 17 00:00:00 2001 From: Optuber01 Date: Wed, 15 Apr 2026 18:28:47 +0300 Subject: [PATCH] feat: show shell-specific labels for terminal tabs --- src/main/agent-manager.ts | 3 +- src/main/ipc-handlers.ts | 5 +-- src/main/pty-manager.ts | 4 +-- src/preload/index.ts | 2 +- .../components/SplitPane/PaneWrapper.tsx | 1 + .../components/SplitPane/SurfaceTabBar.tsx | 23 +++++++++--- src/renderer/hooks/useTerminal.ts | 36 +++++++++++++++++-- src/renderer/store/surface-slice.ts | 16 +++++++++ src/shared/types.ts | 1 + 9 files changed, 79 insertions(+), 12 deletions(-) diff --git a/src/main/agent-manager.ts b/src/main/agent-manager.ts index ee832c6..6c4f308 100644 --- a/src/main/agent-manager.ts +++ b/src/main/agent-manager.ts @@ -31,11 +31,12 @@ export class AgentManager { spawn(params: AgentSpawnParams & { paneId: PaneId; workspaceId: WorkspaceId }): { agentId: AgentId; surfaceId: SurfaceId } { const agentId: AgentId = `agent-${uuid()}`; - const surfaceId = this.ptyManager.create({ + const created = this.ptyManager.create({ shell: '', // Use default shell (resolves to pwsh/powershell/bash, not hardcoded cmd.exe) cwd: params.cwd || process.env.USERPROFILE || 'C:\\', env: { ...(params.env || {}), WMUX_AGENT_ID: agentId, WMUX_AGENT_LABEL: params.label }, }); + const surfaceId = created.id; setTimeout(() => { if (this.ptyManager.has(surfaceId)) { diff --git a/src/main/ipc-handlers.ts b/src/main/ipc-handlers.ts index e15eda1..aea08c8 100644 --- a/src/main/ipc-handlers.ts +++ b/src/main/ipc-handlers.ts @@ -40,7 +40,8 @@ export function registerIpcHandlers(windowManager: WindowManager, cdpProxyInstan ...options, cwd: options.cwd || process.env.USERPROFILE || 'C:\\', }; - const id = ptyManager.create(resolvedOptions); + const created = ptyManager.create(resolvedOptions); + const id = created.id; const window = BrowserWindow.fromWebContents(_event.sender); const unsubData = ptyManager.onData(id, (data) => { if (window && !window.isDestroyed()) { @@ -57,7 +58,7 @@ export function registerIpcHandlers(windowManager: WindowManager, cdpProxyInstan unsubData(); unsubExit(); }); - return id; + return created; } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); throw new Error(`Failed to create terminal: ${msg}`); diff --git a/src/main/pty-manager.ts b/src/main/pty-manager.ts index 98834cc..1622d87 100644 --- a/src/main/pty-manager.ts +++ b/src/main/pty-manager.ts @@ -105,7 +105,7 @@ export interface CreateOptions { export class PtyManager { private ptys = new Map(); - create(options: CreateOptions): SurfaceId { + create(options: CreateOptions): { id: SurfaceId; shell: string } { const id: SurfaceId = options.surfaceId ?? `surf-${uuidv4()}` as SurfaceId; const shell = resolveShell(options.shell); @@ -165,7 +165,7 @@ export class PtyManager { }); this.ptys.set(id, entry); - return id; + return { id, shell }; } write(id: SurfaceId, data: string): void { diff --git a/src/preload/index.ts b/src/preload/index.ts index d54abd6..3f385a5 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -4,7 +4,7 @@ import { IPC_CHANNELS } from '../shared/types'; contextBridge.exposeInMainWorld('wmux', { pty: { create: (options: { shell: string; cwd: string; env: Record; surfaceId?: string }) => - ipcRenderer.invoke(IPC_CHANNELS.PTY_CREATE, options), + ipcRenderer.invoke(IPC_CHANNELS.PTY_CREATE, options) as Promise<{ id: string; shell: string }>, write: (id: string, data: string) => ipcRenderer.send(IPC_CHANNELS.PTY_WRITE, id, data), resize: (id: string, cols: number, rows: number) => diff --git a/src/renderer/components/SplitPane/PaneWrapper.tsx b/src/renderer/components/SplitPane/PaneWrapper.tsx index 2fd94ff..83ee44f 100644 --- a/src/renderer/components/SplitPane/PaneWrapper.tsx +++ b/src/renderer/components/SplitPane/PaneWrapper.tsx @@ -294,6 +294,7 @@ export default function PaneWrapper({ leaf, workspaceId, isFocused }: PaneWrappe
void; @@ -29,11 +30,24 @@ function surfaceIcon(type: string, isAgent: boolean): string { } } -function surfaceLabel(surface: SurfaceRef, agentLabel?: string): string { +function getShellLabel(shell?: string): string | null { + if (!shell) return null; + const normalized = shell.replace(/\\/g, '/').split('/').pop()?.toLowerCase() || shell.toLowerCase(); + if (normalized === 'pwsh.exe' || normalized === 'pwsh') return 'PowerShell'; + if (normalized === 'powershell.exe' || normalized === 'powershell') return 'Windows PowerShell'; + if (normalized === 'cmd.exe' || normalized === 'cmd') return 'Command Prompt'; + if (normalized === 'bash.exe' || normalized === 'bash') return 'Bash'; + if (normalized === 'zsh' || normalized === 'zsh.exe') return 'Zsh'; + if (normalized === 'wsl.exe' || normalized === 'wsl') return 'WSL'; + if (normalized === 'git-bash.exe') return 'Git Bash'; + return normalized.replace(/\.exe$/i, '').replace(/[-_]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()); +} + +function surfaceLabel(surface: SurfaceRef, agentLabel?: string, workspaceShell?: string): string { if (surface.customTitle) return surface.customTitle; if (agentLabel) return agentLabel; switch (surface.type) { - case 'terminal': return 'Terminal'; + case 'terminal': return getShellLabel(surface.shell || workspaceShell) || 'Terminal'; case 'browser': return 'Browser'; case 'markdown': return 'Markdown'; case 'diff': return 'Diff'; @@ -43,6 +57,7 @@ function surfaceLabel(surface: SurfaceRef, agentLabel?: string): string { export default function SurfaceTabBar({ paneId, + workspaceShell, surfaces, activeSurfaceIndex, onSelect, @@ -199,10 +214,10 @@ export default function SurfaceTabBar({ }} onBlur={commitRename} onClick={(e) => e.stopPropagation()} - placeholder={surfaceLabel(surface, agentMeta?.label)} + placeholder={surfaceLabel(surface, agentMeta?.label, workspaceShell)} /> ) : ( - {surfaceLabel(surface, agentMeta?.label)} + {surfaceLabel(surface, agentMeta?.label, workspaceShell)} )} {surfaces.length > 1 && !isRenaming && (