hjworktree-cli 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.context-snapshots/context-snapshot-20250107-221530.md +95 -0
  2. package/.context-snapshots/context-snapshot-20260106-211500.md +85 -0
  3. package/README.md +39 -10
  4. package/bin/cli.js +64 -2
  5. package/dist/server/index.d.ts.map +1 -1
  6. package/dist/server/index.js +10 -0
  7. package/dist/server/index.js.map +1 -1
  8. package/dist/server/socketHandlers.d.ts.map +1 -1
  9. package/dist/server/socketHandlers.js +19 -18
  10. package/dist/server/socketHandlers.js.map +1 -1
  11. package/dist/shared/constants.d.ts +2 -3
  12. package/dist/shared/constants.d.ts.map +1 -1
  13. package/dist/shared/constants.js +2 -24
  14. package/dist/shared/constants.js.map +1 -1
  15. package/dist/shared/types/index.d.ts +3 -15
  16. package/dist/shared/types/index.d.ts.map +1 -1
  17. package/dist/shared/types/index.js +1 -1
  18. package/dist/shared/types/index.js.map +1 -1
  19. package/dist/web/assets/index-Dgl6wRHk.css +32 -0
  20. package/dist/web/assets/index-Jm7djWxU.js +53 -0
  21. package/dist/web/assets/index-Jm7djWxU.js.map +1 -0
  22. package/dist/web/index.html +2 -2
  23. package/package.json +2 -1
  24. package/server/index.ts +11 -0
  25. package/server/socketHandlers.ts +24 -23
  26. package/shared/constants.ts +2 -27
  27. package/shared/types/index.ts +6 -21
  28. package/web/src/App.tsx +8 -6
  29. package/web/src/components/Layout/LeftNavBar.tsx +6 -17
  30. package/web/src/components/Modals/AddWorktreeModal.tsx +1 -21
  31. package/web/src/components/Steps/WorktreeStep.tsx +1 -8
  32. package/web/src/components/Terminal/SplitTerminalView.tsx +64 -0
  33. package/web/src/components/Terminal/TerminalPanel.tsx +3 -69
  34. package/web/src/components/Terminal/XTerminal.tsx +4 -6
  35. package/web/src/stores/useAppStore.ts +77 -35
  36. package/web/src/styles/global.css +127 -77
  37. package/dist/web/assets/index-CsixHL-D.css +0 -32
  38. package/dist/web/assets/index-De6xm4hO.js +0 -53
  39. package/dist/web/assets/index-De6xm4hO.js.map +0 -1
  40. package/web/src/components/Setup/AgentSelector.tsx +0 -27
  41. package/web/src/components/Steps/AgentStep.tsx +0 -20
@@ -7,8 +7,8 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
- <script type="module" crossorigin src="/assets/index-De6xm4hO.js"></script>
11
- <link rel="stylesheet" crossorigin href="/assets/index-CsixHL-D.css">
10
+ <script type="module" crossorigin src="/assets/index-Jm7djWxU.js"></script>
11
+ <link rel="stylesheet" crossorigin href="/assets/index-Dgl6wRHk.css">
12
12
  </head>
13
13
  <body>
14
14
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hjworktree-cli",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Web-based git worktree parallel AI coding agent runner",
5
5
  "type": "module",
6
6
  "main": "dist/server/index.js",
@@ -38,6 +38,7 @@
38
38
  "dependencies": {
39
39
  "cors": "^2.8.5",
40
40
  "express": "^4.21.0",
41
+ "get-port": "^7.1.0",
41
42
  "node-pty": "^1.1.0",
42
43
  "open": "^10.1.0",
43
44
  "simple-git": "^3.27.0",
package/server/index.ts CHANGED
@@ -55,6 +55,17 @@ process.on('SIGTERM', () => {
55
55
  process.exit(0);
56
56
  });
57
57
 
58
+ // Handle port in use error (race condition protection)
59
+ httpServer.on('error', (error: NodeJS.ErrnoException) => {
60
+ if (error.code === 'EADDRINUSE') {
61
+ console.error(`\nError: Port ${PORT} is already in use.`);
62
+ console.error('This may happen if another process started using the port just now.');
63
+ console.error('Please try again or specify a different port: PORT=4000 hjWorktree');
64
+ process.exit(1);
65
+ }
66
+ throw error;
67
+ });
68
+
58
69
  // Start server
59
70
  httpServer.listen(PORT, () => {
60
71
  console.log(`
@@ -6,9 +6,7 @@ import type {
6
6
  TerminalInputData,
7
7
  TerminalResizeData,
8
8
  TerminalKillData,
9
- AgentId
10
9
  } from '../shared/types/index.js';
11
- import { AI_AGENTS } from '../shared/constants.js';
12
10
  import fs, { realpathSync } from 'fs';
13
11
  import path from 'path';
14
12
  import { promisify } from 'util';
@@ -108,17 +106,11 @@ async function waitForSpawnInterval(): Promise<void> {
108
106
  interface TerminalSession {
109
107
  pty: IPty;
110
108
  worktreePath: string;
111
- agentType: AgentId;
112
- socketId: string;
109
+ socketId: string | null; // null when disconnected but session alive
113
110
  }
114
111
 
115
112
  const sessions = new Map<string, TerminalSession>();
116
113
 
117
- function getAgentCommand(agentType: AgentId): string {
118
- const agent = AI_AGENTS.find(a => a.id === agentType);
119
- return agent?.command || 'bash';
120
- }
121
-
122
114
  function getShell(): string {
123
115
  if (process.platform === 'win32') {
124
116
  return 'powershell.exe';
@@ -142,13 +134,29 @@ export function setupSocketHandlers(io: Server, cwd: string) {
142
134
 
143
135
  // Terminal create
144
136
  socket.on('terminal:create', async (data: TerminalCreateData) => {
145
- const { sessionId, worktreePath, agentType } = data;
137
+ const { sessionId, worktreePath } = data;
138
+
139
+ console.log(`Creating terminal session: ${sessionId} at ${worktreePath}`);
140
+
141
+ // Check if session already exists (reattach scenario)
142
+ const existingSession = sessions.get(sessionId);
143
+ if (existingSession && existingSession.pty) {
144
+ console.log(`Reattaching to existing session: ${sessionId}`);
146
145
 
147
- console.log(`Creating terminal session: ${sessionId} at ${worktreePath} with ${agentType}`);
146
+ // Update socketId for the existing session
147
+ existingSession.socketId = socket.id;
148
+
149
+ // Re-register output handler for new socket
150
+ existingSession.pty.onData((output: string) => {
151
+ socket.emit('terminal:output', { sessionId, data: output });
152
+ });
153
+
154
+ socket.emit('terminal:created', { sessionId, reattached: true });
155
+ return;
156
+ }
148
157
 
149
158
  try {
150
159
  const shell = getShell();
151
- const agentCommand = getAgentCommand(agentType);
152
160
 
153
161
  // Validate path before spawning
154
162
  const pathValidation = await validatePath(worktreePath);
@@ -184,7 +192,6 @@ export function setupSocketHandlers(io: Server, cwd: string) {
184
192
  sessions.set(sessionId, {
185
193
  pty: ptyProcess,
186
194
  worktreePath,
187
- agentType,
188
195
  socketId: socket.id,
189
196
  });
190
197
 
@@ -200,11 +207,6 @@ export function setupSocketHandlers(io: Server, cwd: string) {
200
207
  sessions.delete(sessionId);
201
208
  });
202
209
 
203
- // Start the AI agent after a short delay
204
- setTimeout(() => {
205
- ptyProcess.write(`${agentCommand}\r`);
206
- }, 500);
207
-
208
210
  socket.emit('terminal:created', { sessionId });
209
211
  } catch (error) {
210
212
  console.error(`Failed to create terminal session: ${sessionId}`, error);
@@ -252,7 +254,7 @@ export function setupSocketHandlers(io: Server, cwd: string) {
252
254
  const socketSessions = Array.from(sessions.entries())
253
255
  .filter(([, session]) => session.socketId === socket.id);
254
256
 
255
- for (const [sessionId, session] of socketSessions) {
257
+ for (const [, session] of socketSessions) {
256
258
  session.pty.write(data.data);
257
259
  }
258
260
  });
@@ -261,14 +263,13 @@ export function setupSocketHandlers(io: Server, cwd: string) {
261
263
  socket.on('disconnect', () => {
262
264
  console.log(`Client disconnected: ${socket.id}`);
263
265
 
264
- // Kill all sessions owned by this socket
266
+ // Keep sessions alive but mark socketId as null for potential reattach
265
267
  const socketSessions = Array.from(sessions.entries())
266
268
  .filter(([, session]) => session.socketId === socket.id);
267
269
 
268
270
  for (const [sessionId, session] of socketSessions) {
269
- console.log(`Killing orphaned terminal session: ${sessionId}`);
270
- session.pty.kill();
271
- sessions.delete(sessionId);
271
+ console.log(`Detaching terminal session (keeping alive): ${sessionId}`);
272
+ session.socketId = null;
272
273
  }
273
274
  });
274
275
  });
@@ -1,29 +1,3 @@
1
- import type { AgentType } from './types/index.js';
2
-
3
- export const AI_AGENTS: AgentType[] = [
4
- {
5
- id: 'codex',
6
- name: 'Codex CLI',
7
- command: 'codex',
8
- installCommand: 'npm install -g @openai/codex',
9
- description: 'OpenAI Codex CLI - AI-powered coding assistant'
10
- },
11
- {
12
- id: 'claude',
13
- name: 'Claude Code',
14
- command: 'claude',
15
- installCommand: 'npm install -g @anthropic-ai/claude-code',
16
- description: 'Anthropic Claude Code - Advanced AI coding assistant'
17
- },
18
- {
19
- id: 'gemini',
20
- name: 'Gemini CLI',
21
- command: 'gemini',
22
- installCommand: 'npm install -g @google/gemini-cli',
23
- description: 'Google Gemini CLI - Multi-modal AI assistant'
24
- }
25
- ];
26
-
27
1
  export const MAX_PARALLEL_COUNT = 10;
28
2
  export const MIN_PARALLEL_COUNT = 1;
29
3
  export const DEFAULT_PARALLEL_COUNT = 3;
@@ -31,5 +5,6 @@ export const DEFAULT_PARALLEL_COUNT = 3;
31
5
  export const BRANCH_POLL_INTERVAL = 5000; // 5 seconds
32
6
 
33
7
  export const APP_NAME = 'hjWorktree CLI';
34
- export const APP_VERSION = '2.3.0';
8
+ export const APP_VERSION = '2.5.0';
35
9
  export const DEFAULT_PORT = 3847;
10
+ export const PORT_RANGE_SIZE = 10; // Will try ports 3847-3856
@@ -1,5 +1,5 @@
1
- // Navigation step types (4-step wizard)
2
- export type NavigationStep = 'branch' | 'agent' | 'worktree' | 'running';
1
+ // Navigation step types (3-step wizard)
2
+ export type NavigationStep = 'branch' | 'worktree' | 'running';
3
3
 
4
4
  // Step status for LNB display
5
5
  export type StepStatus = 'pending' | 'current' | 'completed';
@@ -13,18 +13,7 @@ export interface StepConfig {
13
13
  }
14
14
 
15
15
  // Step order constant
16
- export const STEP_ORDER: NavigationStep[] = ['branch', 'agent', 'worktree', 'running'];
17
-
18
- // Agent types
19
- export type AgentId = 'codex' | 'claude' | 'gemini';
20
-
21
- export interface AgentType {
22
- id: AgentId;
23
- name: string;
24
- command: string;
25
- installCommand: string;
26
- description: string;
27
- }
16
+ export const STEP_ORDER: NavigationStep[] = ['branch', 'worktree', 'running'];
28
17
 
29
18
  // Branch types
30
19
  export interface Branch {
@@ -34,16 +23,15 @@ export interface Branch {
34
23
  lastCommit?: string;
35
24
  }
36
25
 
37
- // Running agent types
38
- export type AgentStatus = 'initializing' | 'installing' | 'running' | 'stopped' | 'error';
26
+ // Terminal status types
27
+ export type TerminalStatus = 'initializing' | 'running' | 'stopped' | 'error';
39
28
 
40
29
  export interface TerminalInfo {
41
30
  sessionId: string;
42
31
  worktreePath: string;
43
32
  worktreeName: string;
44
33
  branchName: string;
45
- agentType: AgentId;
46
- status: AgentStatus;
34
+ status: TerminalStatus;
47
35
  }
48
36
 
49
37
  // Worktree types
@@ -58,7 +46,6 @@ export interface Worktree {
58
46
  export interface TerminalCreateData {
59
47
  sessionId: string;
60
48
  worktreePath: string;
61
- agentType: AgentId;
62
49
  }
63
50
 
64
51
  export interface TerminalOutputData {
@@ -85,7 +72,6 @@ export interface TerminalKillData {
85
72
  export interface CreateWorktreesRequest {
86
73
  branch: string;
87
74
  count: number;
88
- agentType: AgentId;
89
75
  }
90
76
 
91
77
  export interface CreateWorktreesResponse {
@@ -95,7 +81,6 @@ export interface CreateWorktreesResponse {
95
81
  // Single worktree creation
96
82
  export interface CreateSingleWorktreeRequest {
97
83
  branch: string;
98
- agentType: AgentId;
99
84
  }
100
85
 
101
86
  export interface CreateSingleWorktreeResponse {
package/web/src/App.tsx CHANGED
@@ -4,7 +4,6 @@ import { useSocket } from './hooks/useSocket.js';
4
4
  import Header from './components/Layout/Header.js';
5
5
  import MainLayout from './components/Layout/MainLayout.js';
6
6
  import BranchStep from './components/Steps/BranchStep.js';
7
- import AgentStep from './components/Steps/AgentStep.js';
8
7
  import WorktreeStep from './components/Steps/WorktreeStep.js';
9
8
  import TerminalPanel from './components/Terminal/TerminalPanel.js';
10
9
  import ModalContainer from './components/Modals/ModalContainer.js';
@@ -13,18 +12,21 @@ function App() {
13
12
  const { step, isLoading, loadingMessage, error } = useAppStore();
14
13
  const { connected } = useSocket();
15
14
 
16
- // Fetch initial data
15
+ // Fetch initial data and restore existing sessions
17
16
  useEffect(() => {
18
- useAppStore.getState().fetchProjectInfo();
19
- useAppStore.getState().fetchBranches();
17
+ const initApp = async () => {
18
+ await useAppStore.getState().fetchProjectInfo();
19
+ await useAppStore.getState().fetchBranches();
20
+ // Restore existing worktrees if any (for page refresh persistence)
21
+ await useAppStore.getState().restoreExistingWorktrees();
22
+ };
23
+ initApp();
20
24
  }, []);
21
25
 
22
26
  const renderStepContent = () => {
23
27
  switch (step) {
24
28
  case 'branch':
25
29
  return <BranchStep />;
26
- case 'agent':
27
- return <AgentStep />;
28
30
  case 'worktree':
29
31
  return <WorktreeStep />;
30
32
  case 'running':
@@ -1,13 +1,11 @@
1
1
  import React from 'react';
2
2
  import { useAppStore } from '../../stores/useAppStore.js';
3
- import type { NavigationStep, StepStatus, AgentStatus } from '../../../../shared/types/index.js';
4
- import { AI_AGENTS } from '../../../../shared/constants.js';
3
+ import type { NavigationStep, StepStatus, TerminalStatus } from '../../../../shared/types/index.js';
5
4
 
6
5
  // Step configuration
7
6
  const STEP_CONFIG = [
8
7
  { id: 'branch' as const, number: 1, label: 'Branch' },
9
- { id: 'agent' as const, number: 2, label: 'Agent' },
10
- { id: 'worktree' as const, number: 3, label: 'Count' },
8
+ { id: 'worktree' as const, number: 2, label: 'Count' },
11
9
  ];
12
10
 
13
11
  interface StepItemProps {
@@ -79,7 +77,7 @@ function SetupSection() {
79
77
  onClick={() => goToStep('running')}
80
78
  type="button"
81
79
  >
82
- <span className="step-number">4</span>
80
+ <span className="step-number">3</span>
83
81
  <span className="step-label">Running</span>
84
82
  </button>
85
83
  )}
@@ -90,10 +88,9 @@ function SetupSection() {
90
88
  }
91
89
 
92
90
  // Status indicator component
93
- function StatusIndicator({ status }: { status: AgentStatus }) {
94
- const statusColors: Record<AgentStatus, string> = {
91
+ function StatusIndicator({ status }: { status: TerminalStatus }) {
92
+ const statusColors: Record<TerminalStatus, string> = {
95
93
  initializing: '#f59e0b',
96
- installing: '#f59e0b',
97
94
  running: '#22c55e',
98
95
  stopped: '#6b7280',
99
96
  error: '#ef4444',
@@ -112,8 +109,7 @@ function StatusIndicator({ status }: { status: AgentStatus }) {
112
109
  interface SessionItemProps {
113
110
  sessionId: string;
114
111
  worktreeName: string;
115
- agentType: string;
116
- status: AgentStatus;
112
+ status: TerminalStatus;
117
113
  isActive: boolean;
118
114
  isSelected: boolean;
119
115
  onSelect: () => void;
@@ -124,7 +120,6 @@ interface SessionItemProps {
124
120
  function SessionItem({
125
121
  sessionId,
126
122
  worktreeName,
127
- agentType,
128
123
  status,
129
124
  isActive,
130
125
  isSelected,
@@ -132,8 +127,6 @@ function SessionItem({
132
127
  onToggleSelect,
133
128
  onClose,
134
129
  }: SessionItemProps) {
135
- const agent = AI_AGENTS.find(a => a.id === agentType);
136
-
137
130
  return (
138
131
  <div
139
132
  className={`session-item ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''}`}
@@ -152,9 +145,6 @@ function SessionItem({
152
145
  <span className="session-name" title={worktreeName}>
153
146
  {worktreeName}
154
147
  </span>
155
- <span className="session-agent" title={agent?.name}>
156
- {agentType.charAt(0).toUpperCase()}
157
- </span>
158
148
  <button
159
149
  className="session-close"
160
150
  onClick={onClose}
@@ -256,7 +246,6 @@ function SessionsSection() {
256
246
  key={session.sessionId}
257
247
  sessionId={session.sessionId}
258
248
  worktreeName={session.worktreeName}
259
- agentType={session.agentType}
260
249
  status={session.status}
261
250
  isActive={session.sessionId === activeSessionId}
262
251
  isSelected={selectedSessionIds.includes(session.sessionId)}
@@ -1,16 +1,13 @@
1
1
  import React, { useState } from 'react';
2
2
  import { useAppStore } from '../../stores/useAppStore.js';
3
- import { AI_AGENTS } from '../../../../shared/constants.js';
4
- import type { AgentId } from '../../../../shared/types/index.js';
5
3
 
6
4
  function AddWorktreeModal() {
7
5
  const { branches, closeModal, createSingleWorktree, isLoading } = useAppStore();
8
6
  const [selectedBranch, setSelectedBranch] = useState<string>('');
9
- const [selectedAgent, setSelectedAgent] = useState<AgentId>('claude');
10
7
 
11
8
  const handleCreate = async () => {
12
9
  if (!selectedBranch) return;
13
- await createSingleWorktree(selectedBranch, selectedAgent);
10
+ await createSingleWorktree(selectedBranch);
14
11
  };
15
12
 
16
13
  return (
@@ -42,23 +39,6 @@ function AddWorktreeModal() {
42
39
  ))}
43
40
  </select>
44
41
  </div>
45
-
46
- <div className="form-group">
47
- <label>Agent</label>
48
- <div className="agent-options">
49
- {AI_AGENTS.map((agent) => (
50
- <button
51
- key={agent.id}
52
- type="button"
53
- className={`agent-option ${selectedAgent === agent.id ? 'selected' : ''}`}
54
- onClick={() => setSelectedAgent(agent.id)}
55
- disabled={isLoading}
56
- >
57
- <span className="agent-name">{agent.name}</span>
58
- </button>
59
- ))}
60
- </div>
61
- </div>
62
42
  </div>
63
43
 
64
44
  <div className="modal-footer">
@@ -2,12 +2,9 @@ import React from 'react';
2
2
  import StepContainer from '../Layout/StepContainer.js';
3
3
  import WorktreeCountSelector from '../Setup/WorktreeCountSelector.js';
4
4
  import { useAppStore } from '../../stores/useAppStore.js';
5
- import { AI_AGENTS } from '../../../../shared/constants.js';
6
5
 
7
6
  function WorktreeStep() {
8
- const { execute, isLoading, selectedBranch, selectedAgent, worktreeCount } = useAppStore();
9
-
10
- const agentName = AI_AGENTS.find(a => a.id === selectedAgent)?.name || selectedAgent;
7
+ const { execute, isLoading, selectedBranch, worktreeCount } = useAppStore();
11
8
 
12
9
  return (
13
10
  <StepContainer
@@ -25,10 +22,6 @@ function WorktreeStep() {
25
22
  <span className="summary-label">Branch:</span>
26
23
  <span className="summary-value">{selectedBranch}</span>
27
24
  </div>
28
- <div className="summary-item">
29
- <span className="summary-label">Agent:</span>
30
- <span className="summary-value">{agentName}</span>
31
- </div>
32
25
  <div className="summary-item">
33
26
  <span className="summary-label">Worktrees:</span>
34
27
  <span className="summary-value">{worktreeCount}</span>
@@ -0,0 +1,64 @@
1
+ import React, { useCallback } from 'react';
2
+ import { useAppStore } from '../../stores/useAppStore.js';
3
+ import XTerminal from './XTerminal.js';
4
+ import type { TerminalInfo } from '../../../../shared/types/index.js';
5
+
6
+ function SplitTerminalView() {
7
+ const {
8
+ terminals,
9
+ activeSessionId,
10
+ setActiveSession,
11
+ } = useAppStore();
12
+
13
+ // Handle single click to activate
14
+ const handleClick = useCallback((sessionId: string) => {
15
+ setActiveSession(sessionId);
16
+ }, [setActiveSession]);
17
+
18
+ // Render a terminal pane
19
+ const renderPane = (terminal: TerminalInfo) => {
20
+ const isActive = terminal.sessionId === activeSessionId;
21
+
22
+ return (
23
+ <div
24
+ key={terminal.sessionId}
25
+ className={`terminal-pane ${isActive ? 'active' : ''}`}
26
+ onClick={() => handleClick(terminal.sessionId)}
27
+ >
28
+ <div className="pane-header">
29
+ <span className={`pane-status ${terminal.status}`} />
30
+ <span className="pane-title">{terminal.worktreeName}</span>
31
+ </div>
32
+ <div className="pane-terminal">
33
+ <XTerminal
34
+ sessionId={terminal.sessionId}
35
+ worktreePath={terminal.worktreePath}
36
+ isActive={isActive}
37
+ />
38
+ </div>
39
+ </div>
40
+ );
41
+ };
42
+
43
+ // Render split grid view
44
+ if (terminals.length === 0) {
45
+ return (
46
+ <div className="terminal-grid-container">
47
+ <div className="empty-grid-message">
48
+ <p>No active sessions</p>
49
+ <p>Use the Setup wizard or click "+ Add Session" in the sidebar</p>
50
+ </div>
51
+ </div>
52
+ );
53
+ }
54
+
55
+ return (
56
+ <div className="terminal-grid-container scrollable">
57
+ <div className="terminal-grid cols-2">
58
+ {terminals.map(terminal => renderPane(terminal))}
59
+ </div>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ export default SplitTerminalView;
@@ -1,52 +1,18 @@
1
1
  import React from 'react';
2
2
  import { useAppStore } from '../../stores/useAppStore.js';
3
- import { useSocket } from '../../hooks/useSocket.js';
4
- import XTerminal from './XTerminal.js';
3
+ import SplitTerminalView from './SplitTerminalView.js';
5
4
 
6
5
  function TerminalPanel() {
7
6
  const {
8
7
  terminals,
9
- activeTerminalIndex,
10
- activeSessionId,
11
- setActiveSession,
12
- removeTerminal,
13
- clearAllTerminals,
14
8
  openModal,
15
9
  } = useAppStore();
16
- const { emit } = useSocket();
17
-
18
- const handleCloseTerminal = async (sessionId: string, e: React.MouseEvent) => {
19
- e.stopPropagation();
20
-
21
- // Kill the terminal via socket
22
- emit('terminal:kill', { sessionId });
23
-
24
- // Find the worktree name to delete
25
- const terminal = terminals.find(t => t.sessionId === sessionId);
26
- if (terminal) {
27
- try {
28
- await fetch(`/api/worktrees/${terminal.worktreeName}`, {
29
- method: 'DELETE',
30
- });
31
- } catch (error) {
32
- console.error('Failed to delete worktree:', error);
33
- }
34
- }
35
-
36
- removeTerminal(sessionId);
37
- };
38
10
 
39
11
  const handleCloseAll = () => {
40
12
  const sessionIds = terminals.map(t => t.sessionId);
41
13
  openModal('confirmDelete', { sessionIds });
42
14
  };
43
15
 
44
- // Find active index based on sessionId
45
- const currentActiveIndex = activeSessionId
46
- ? terminals.findIndex(t => t.sessionId === activeSessionId)
47
- : activeTerminalIndex;
48
- const effectiveActiveIndex = currentActiveIndex >= 0 ? currentActiveIndex : 0;
49
-
50
16
  if (terminals.length === 0) {
51
17
  return (
52
18
  <div className="terminal-panel empty">
@@ -60,25 +26,7 @@ function TerminalPanel() {
60
26
 
61
27
  return (
62
28
  <div className="terminal-panel">
63
- <div className="terminal-tabs">
64
- {terminals.map((terminal, index) => (
65
- <button
66
- key={terminal.sessionId}
67
- className={`terminal-tab ${terminal.sessionId === activeSessionId || index === effectiveActiveIndex ? 'active' : ''}`}
68
- onClick={() => setActiveSession(terminal.sessionId)}
69
- >
70
- <span className={`status ${terminal.status}`} />
71
- <span>{terminal.worktreeName}</span>
72
- <button
73
- className="close-btn"
74
- onClick={(e) => handleCloseTerminal(terminal.sessionId, e)}
75
- title="Close terminal"
76
- >
77
- &times;
78
- </button>
79
- </button>
80
- ))}
81
-
29
+ <div className="terminal-header">
82
30
  <div className="terminal-actions">
83
31
  <button className="danger" onClick={handleCloseAll}>
84
32
  Close All
@@ -86,21 +34,7 @@ function TerminalPanel() {
86
34
  </div>
87
35
  </div>
88
36
 
89
- <div className="terminal-container">
90
- {terminals.map((terminal, index) => (
91
- <div
92
- key={terminal.sessionId}
93
- className={`terminal-wrapper ${index !== effectiveActiveIndex ? 'hidden' : ''}`}
94
- >
95
- <XTerminal
96
- sessionId={terminal.sessionId}
97
- worktreePath={terminal.worktreePath}
98
- agentType={terminal.agentType}
99
- isActive={index === effectiveActiveIndex}
100
- />
101
- </div>
102
- ))}
103
- </div>
37
+ <SplitTerminalView />
104
38
  </div>
105
39
  );
106
40
  }
@@ -1,19 +1,18 @@
1
- import React, { useEffect, useRef, useCallback } from 'react';
1
+ import React, { useEffect, useRef } from 'react';
2
2
  import { Terminal } from '@xterm/xterm';
3
3
  import { FitAddon } from '@xterm/addon-fit';
4
4
  import { useSocket } from '../../hooks/useSocket';
5
5
  import { useAppStore } from '../../stores/useAppStore';
6
- import type { AgentId, TerminalOutputData } from '../../../../shared/types/index';
6
+ import type { TerminalOutputData } from '../../../../shared/types/index';
7
7
  import '@xterm/xterm/css/xterm.css';
8
8
 
9
9
  interface XTerminalProps {
10
10
  sessionId: string;
11
11
  worktreePath: string;
12
- agentType: AgentId;
13
12
  isActive: boolean;
14
13
  }
15
14
 
16
- function XTerminal({ sessionId, worktreePath, agentType, isActive }: XTerminalProps) {
15
+ function XTerminal({ sessionId, worktreePath, isActive }: XTerminalProps) {
17
16
  const terminalRef = useRef<HTMLDivElement>(null);
18
17
  const termRef = useRef<Terminal | null>(null);
19
18
  const fitAddonRef = useRef<FitAddon | null>(null);
@@ -129,7 +128,6 @@ function XTerminal({ sessionId, worktreePath, agentType, isActive }: XTerminalPr
129
128
  emit('terminal:create', {
130
129
  sessionId,
131
130
  worktreePath,
132
- agentType,
133
131
  });
134
132
 
135
133
  // Send initial resize
@@ -148,7 +146,7 @@ function XTerminal({ sessionId, worktreePath, agentType, isActive }: XTerminalPr
148
146
  off('terminal:error', handleError);
149
147
  onDataDisposable.dispose();
150
148
  };
151
- }, [sessionId, worktreePath, agentType, connected, emit, on, off, updateTerminalStatus]);
149
+ }, [sessionId, worktreePath, connected, emit, on, off, updateTerminalStatus]);
152
150
 
153
151
  // Handle resize
154
152
  useEffect(() => {