hjworktree-cli 2.2.0 → 2.4.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 (37) hide show
  1. package/.claude/settings.local.json +11 -2
  2. package/.context-snapshots/context-snapshot-20260106-211500.md +85 -0
  3. package/README.md +26 -10
  4. package/dist/server/socketHandlers.d.ts.map +1 -1
  5. package/dist/server/socketHandlers.js +58 -23
  6. package/dist/server/socketHandlers.js.map +1 -1
  7. package/dist/shared/constants.d.ts +1 -3
  8. package/dist/shared/constants.d.ts.map +1 -1
  9. package/dist/shared/constants.js +1 -24
  10. package/dist/shared/constants.js.map +1 -1
  11. package/dist/shared/types/index.d.ts +3 -15
  12. package/dist/shared/types/index.d.ts.map +1 -1
  13. package/dist/shared/types/index.js +1 -1
  14. package/dist/shared/types/index.js.map +1 -1
  15. package/dist/web/assets/index-D-hASqdI.js +53 -0
  16. package/dist/web/assets/index-D-hASqdI.js.map +1 -0
  17. package/dist/web/assets/index-Dgl6wRHk.css +32 -0
  18. package/dist/web/index.html +2 -2
  19. package/package.json +3 -2
  20. package/scripts/fix-pty-permissions.js +89 -0
  21. package/server/socketHandlers.ts +66 -28
  22. package/shared/constants.ts +1 -27
  23. package/shared/types/index.ts +6 -21
  24. package/web/src/App.tsx +8 -6
  25. package/web/src/components/Layout/LeftNavBar.tsx +6 -17
  26. package/web/src/components/Modals/AddWorktreeModal.tsx +1 -21
  27. package/web/src/components/Steps/WorktreeStep.tsx +1 -8
  28. package/web/src/components/Terminal/SplitTerminalView.tsx +64 -0
  29. package/web/src/components/Terminal/TerminalPanel.tsx +3 -69
  30. package/web/src/components/Terminal/XTerminal.tsx +4 -6
  31. package/web/src/stores/useAppStore.ts +77 -35
  32. package/web/src/styles/global.css +127 -77
  33. package/dist/web/assets/index-CsixHL-D.css +0 -32
  34. package/dist/web/assets/index-D8dr9mJa.js +0 -53
  35. package/dist/web/assets/index-D8dr9mJa.js.map +0 -1
  36. package/web/src/components/Setup/AgentSelector.tsx +0 -27
  37. package/web/src/components/Steps/AgentStep.tsx +0 -20
@@ -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(() => {
@@ -1,5 +1,5 @@
1
1
  import { create } from 'zustand';
2
- import type { Branch, TerminalInfo, AgentId, AgentStatus, NavigationStep, StepStatus, ModalType, SessionGroup } from '../../../shared/types/index.js';
2
+ import type { Branch, TerminalInfo, TerminalStatus, NavigationStep, StepStatus, ModalType, SessionGroup } from '../../../shared/types/index.js';
3
3
  import { STEP_ORDER } from '../../../shared/types/index.js';
4
4
  import { DEFAULT_PARALLEL_COUNT, MIN_PARALLEL_COUNT, MAX_PARALLEL_COUNT } from '../../../shared/constants.js';
5
5
 
@@ -21,9 +21,6 @@ interface AppState {
21
21
  branches: Branch[];
22
22
  selectedBranch: string | null;
23
23
 
24
- // Agent selection
25
- selectedAgent: AgentId;
26
-
27
24
  // Worktree count
28
25
  worktreeCount: number;
29
26
 
@@ -31,15 +28,15 @@ interface AppState {
31
28
  terminals: TerminalInfo[];
32
29
  activeTerminalIndex: number;
33
30
 
34
- // Session management (new)
31
+ // Session management
35
32
  activeSessionId: string | null;
36
33
  selectedSessionIds: string[];
37
34
 
38
- // LNB state (new)
35
+ // LNB state
39
36
  isSetupCollapsed: boolean;
40
37
  collapsedBranches: string[];
41
38
 
42
- // Modal state (new)
39
+ // Modal state
43
40
  modalType: ModalType;
44
41
  modalData: { sessionIds?: string[]; branchName?: string } | null;
45
42
 
@@ -70,9 +67,6 @@ interface AppActions {
70
67
  setSelectedBranch: (branch: string | null) => void;
71
68
  fetchBranches: () => Promise<void>;
72
69
 
73
- // Agent selection
74
- setSelectedAgent: (agent: AgentId) => void;
75
-
76
70
  // Worktree count
77
71
  setWorktreeCount: (count: number) => void;
78
72
  incrementWorktreeCount: () => void;
@@ -80,12 +74,12 @@ interface AppActions {
80
74
 
81
75
  // Terminals
82
76
  addTerminal: (terminal: TerminalInfo) => void;
83
- updateTerminalStatus: (sessionId: string, status: AgentStatus) => void;
77
+ updateTerminalStatus: (sessionId: string, status: TerminalStatus) => void;
84
78
  removeTerminal: (sessionId: string) => void;
85
79
  setActiveTerminal: (index: number) => void;
86
80
  clearAllTerminals: () => void;
87
81
 
88
- // Session management (new)
82
+ // Session management
89
83
  setActiveSession: (sessionId: string | null) => void;
90
84
  toggleSessionSelection: (sessionId: string) => void;
91
85
  selectAllSessions: () => void;
@@ -93,16 +87,19 @@ interface AppActions {
93
87
  removeSelectedSessions: () => Promise<void>;
94
88
  getSessionGroups: () => SessionGroup[];
95
89
 
96
- // LNB state (new)
90
+ // LNB state
97
91
  toggleSetupCollapse: () => void;
98
92
  toggleBranchCollapse: (branchName: string) => void;
99
93
 
100
- // Modal (new)
94
+ // Modal
101
95
  openModal: (type: ModalType, data?: { sessionIds?: string[]; branchName?: string }) => void;
102
96
  closeModal: () => void;
103
97
 
104
- // Single worktree creation (new)
105
- createSingleWorktree: (branch: string, agentType: AgentId) => Promise<void>;
98
+ // Single worktree creation
99
+ createSingleWorktree: (branch: string) => Promise<void>;
100
+
101
+ // Restore existing worktrees on page load
102
+ restoreExistingWorktrees: () => Promise<void>;
106
103
 
107
104
  // Execute
108
105
  execute: () => Promise<void>;
@@ -125,7 +122,6 @@ const initialState: AppState = {
125
122
  projectInfo: null,
126
123
  branches: [],
127
124
  selectedBranch: null,
128
- selectedAgent: 'claude',
129
125
  worktreeCount: DEFAULT_PARALLEL_COUNT,
130
126
  terminals: [],
131
127
  activeTerminalIndex: 0,
@@ -206,13 +202,11 @@ export const useAppStore = create<AppStore>((set, get) => ({
206
202
  },
207
203
 
208
204
  isStepComplete: (checkStep) => {
209
- const { selectedBranch, selectedAgent, worktreeCount, terminals } = get();
205
+ const { selectedBranch, worktreeCount, terminals } = get();
210
206
 
211
207
  switch (checkStep) {
212
208
  case 'branch':
213
209
  return selectedBranch !== null;
214
- case 'agent':
215
- return selectedAgent !== null;
216
210
  case 'worktree':
217
211
  return worktreeCount >= MIN_PARALLEL_COUNT;
218
212
  case 'running':
@@ -268,9 +262,6 @@ export const useAppStore = create<AppStore>((set, get) => ({
268
262
  }
269
263
  },
270
264
 
271
- // Agent selection
272
- setSelectedAgent: (agent) => set({ selectedAgent: agent }),
273
-
274
265
  // Worktree count
275
266
  setWorktreeCount: (count) => set({
276
267
  worktreeCount: Math.max(MIN_PARALLEL_COUNT, Math.min(MAX_PARALLEL_COUNT, count))
@@ -291,9 +282,13 @@ export const useAppStore = create<AppStore>((set, get) => ({
291
282
  },
292
283
 
293
284
  // Terminals
294
- addTerminal: (terminal) => set((state) => ({
295
- terminals: [...state.terminals, terminal]
296
- })),
285
+ addTerminal: (terminal) => set((state) => {
286
+ // 이미 같은 sessionId가 있으면 무시
287
+ if (state.terminals.some(t => t.sessionId === terminal.sessionId)) {
288
+ return state;
289
+ }
290
+ return { terminals: [...state.terminals, terminal] };
291
+ }),
297
292
 
298
293
  updateTerminalStatus: (sessionId, status) => set((state) => ({
299
294
  terminals: state.terminals.map((t) =>
@@ -426,7 +421,7 @@ export const useAppStore = create<AppStore>((set, get) => ({
426
421
  closeModal: () => set({ modalType: null, modalData: null }),
427
422
 
428
423
  // Single worktree creation
429
- createSingleWorktree: async (branch, agentType) => {
424
+ createSingleWorktree: async (branch) => {
430
425
  const { setLoading, setError, addTerminal, setActiveSession } = get();
431
426
 
432
427
  setLoading(true, 'Creating worktree...');
@@ -436,7 +431,7 @@ export const useAppStore = create<AppStore>((set, get) => ({
436
431
  const response = await fetch('/api/worktrees/single', {
437
432
  method: 'POST',
438
433
  headers: { 'Content-Type': 'application/json' },
439
- body: JSON.stringify({ branch, agentType }),
434
+ body: JSON.stringify({ branch }),
440
435
  });
441
436
 
442
437
  if (!response.ok) {
@@ -445,14 +440,13 @@ export const useAppStore = create<AppStore>((set, get) => ({
445
440
  }
446
441
 
447
442
  const { worktree } = await response.json();
448
- const sessionId = `${worktree.name}-${Date.now()}`;
443
+ const sessionId = worktree.name;
449
444
 
450
445
  addTerminal({
451
446
  sessionId,
452
447
  worktreePath: worktree.path,
453
448
  worktreeName: worktree.name,
454
449
  branchName: worktree.branch,
455
- agentType,
456
450
  status: 'initializing',
457
451
  });
458
452
 
@@ -473,9 +467,59 @@ export const useAppStore = create<AppStore>((set, get) => ({
473
467
  }
474
468
  },
475
469
 
470
+ // Restore existing worktrees on page load
471
+ restoreExistingWorktrees: async () => {
472
+ const { terminals } = get();
473
+
474
+ // 이미 터미널이 복구되어 있으면 실행하지 않음
475
+ if (terminals.length > 0) {
476
+ return;
477
+ }
478
+
479
+ try {
480
+ const response = await fetch('/api/worktrees');
481
+ if (!response.ok) return;
482
+
483
+ const worktrees = await response.json();
484
+
485
+ // Filter to only project worktrees (exclude main worktree)
486
+ const projectWorktrees = worktrees.filter(
487
+ (wt: { isMainWorktree: boolean; name: string }) =>
488
+ !wt.isMainWorktree && wt.name.includes('-project-')
489
+ );
490
+
491
+ if (projectWorktrees.length === 0) return;
492
+
493
+ const { addTerminal } = get();
494
+
495
+ // Restore terminals for each project worktree
496
+ for (const wt of projectWorktrees) {
497
+ addTerminal({
498
+ sessionId: wt.name,
499
+ worktreePath: wt.path,
500
+ worktreeName: wt.name,
501
+ branchName: wt.branch,
502
+ status: 'running',
503
+ });
504
+ }
505
+
506
+ // Transition to running state
507
+ set({
508
+ step: 'running',
509
+ completedSteps: ['branch', 'worktree'],
510
+ isSetupCollapsed: true,
511
+ activeSessionId: projectWorktrees[0].name,
512
+ });
513
+
514
+ console.log(`Restored ${projectWorktrees.length} worktree sessions`);
515
+ } catch (error) {
516
+ console.error('Failed to restore worktrees:', error);
517
+ }
518
+ },
519
+
476
520
  // Execute
477
521
  execute: async () => {
478
- const { selectedBranch, worktreeCount, selectedAgent, setLoading, setError, setStep, addTerminal } = get();
522
+ const { selectedBranch, worktreeCount, setLoading, setError, addTerminal } = get();
479
523
 
480
524
  if (!selectedBranch) {
481
525
  setError('Please select a branch');
@@ -492,7 +536,6 @@ export const useAppStore = create<AppStore>((set, get) => ({
492
536
  body: JSON.stringify({
493
537
  branch: selectedBranch,
494
538
  count: worktreeCount,
495
- agentType: selectedAgent,
496
539
  }),
497
540
  });
498
541
 
@@ -505,13 +548,12 @@ export const useAppStore = create<AppStore>((set, get) => ({
505
548
 
506
549
  // Create terminal info for each worktree
507
550
  for (const worktree of worktrees) {
508
- const sessionId = `${worktree.name}-${Date.now()}`;
551
+ const sessionId = worktree.name;
509
552
  addTerminal({
510
553
  sessionId,
511
554
  worktreePath: worktree.path,
512
555
  worktreeName: worktree.name,
513
556
  branchName: worktree.branch,
514
- agentType: selectedAgent,
515
557
  status: 'initializing',
516
558
  });
517
559
  }
@@ -520,7 +562,7 @@ export const useAppStore = create<AppStore>((set, get) => ({
520
562
  const { terminals: updatedTerminals } = get();
521
563
  set({
522
564
  step: 'running',
523
- completedSteps: ['branch', 'agent', 'worktree'],
565
+ completedSteps: ['branch', 'worktree'],
524
566
  isSetupCollapsed: true,
525
567
  activeSessionId: updatedTerminals.length > 0 ? updatedTerminals[0].sessionId : null,
526
568
  });
@@ -169,42 +169,6 @@ body {
169
169
  text-align: center;
170
170
  }
171
171
 
172
- /* Agent cards */
173
- .agent-cards {
174
- display: grid;
175
- grid-template-columns: repeat(3, 1fr);
176
- gap: 12px;
177
- }
178
-
179
- .agent-card {
180
- padding: 16px;
181
- border: 2px solid var(--border-color);
182
- border-radius: 8px;
183
- background-color: var(--bg-tertiary);
184
- cursor: pointer;
185
- transition: all 0.2s;
186
- }
187
-
188
- .agent-card:hover {
189
- border-color: var(--accent-blue);
190
- }
191
-
192
- .agent-card.selected {
193
- border-color: var(--accent-blue);
194
- background-color: rgba(88, 166, 255, 0.1);
195
- }
196
-
197
- .agent-card h4 {
198
- font-size: 14px;
199
- font-weight: 600;
200
- margin-bottom: 4px;
201
- }
202
-
203
- .agent-card p {
204
- font-size: 12px;
205
- color: var(--text-muted);
206
- }
207
-
208
172
  /* Execute button */
209
173
  .execute-button {
210
174
  padding: 14px 48px;
@@ -852,15 +816,6 @@ body {
852
816
  white-space: nowrap;
853
817
  }
854
818
 
855
- .session-agent {
856
- font-size: 10px;
857
- font-weight: 600;
858
- color: var(--text-muted);
859
- background-color: var(--bg-tertiary);
860
- padding: 2px 6px;
861
- border-radius: 4px;
862
- }
863
-
864
819
  .session-close {
865
820
  padding: 2px 6px;
866
821
  background: transparent;
@@ -1059,38 +1014,6 @@ body {
1059
1014
  border-color: var(--accent-blue);
1060
1015
  }
1061
1016
 
1062
- /* Agent Options in Modal */
1063
- .agent-options {
1064
- display: flex;
1065
- gap: 8px;
1066
- }
1067
-
1068
- .agent-option {
1069
- flex: 1;
1070
- padding: 12px;
1071
- background-color: var(--bg-tertiary);
1072
- border: 2px solid var(--border-color);
1073
- border-radius: 6px;
1074
- cursor: pointer;
1075
- transition: all 0.2s;
1076
- text-align: center;
1077
- }
1078
-
1079
- .agent-option:hover {
1080
- border-color: var(--accent-blue);
1081
- }
1082
-
1083
- .agent-option.selected {
1084
- border-color: var(--accent-blue);
1085
- background-color: rgba(88, 166, 255, 0.1);
1086
- }
1087
-
1088
- .agent-option .agent-name {
1089
- font-size: 12px;
1090
- font-weight: 500;
1091
- color: var(--text-primary);
1092
- }
1093
-
1094
1017
  /* Modal Buttons */
1095
1018
  .btn-primary {
1096
1019
  padding: 10px 20px;
@@ -1215,3 +1138,130 @@ body {
1215
1138
  font-size: 16px;
1216
1139
  color: var(--text-secondary);
1217
1140
  }
1141
+
1142
+ /* ========================================
1143
+ Split Terminal View Styles
1144
+ ======================================== */
1145
+
1146
+ /* Terminal Header for Split View */
1147
+ .terminal-header {
1148
+ display: flex;
1149
+ align-items: center;
1150
+ gap: 12px;
1151
+ padding: 8px 16px;
1152
+ background-color: var(--bg-secondary);
1153
+ border-bottom: 1px solid var(--border-color);
1154
+ }
1155
+
1156
+ /* Split Terminal Grid Container */
1157
+ .terminal-grid-container {
1158
+ flex: 1;
1159
+ display: flex;
1160
+ flex-direction: column;
1161
+ overflow: hidden;
1162
+ padding: 8px;
1163
+ background-color: var(--bg-primary);
1164
+ }
1165
+
1166
+ .terminal-grid-container.scrollable {
1167
+ overflow-y: auto;
1168
+ }
1169
+
1170
+ /* Terminal Grid */
1171
+ .terminal-grid {
1172
+ flex: 1;
1173
+ display: grid;
1174
+ gap: 8px;
1175
+ min-height: 0;
1176
+ }
1177
+
1178
+ /* Grid layout - fixed 2 columns */
1179
+ .terminal-grid.cols-2 { grid-template-columns: repeat(2, 1fr); }
1180
+
1181
+ /* Scrollable grid for many sessions */
1182
+ .terminal-grid-container.scrollable .terminal-grid {
1183
+ grid-auto-rows: minmax(350px, 1fr);
1184
+ }
1185
+
1186
+ /* Terminal Pane */
1187
+ .terminal-pane {
1188
+ position: relative;
1189
+ display: flex;
1190
+ flex-direction: column;
1191
+ border: 2px solid var(--border-color);
1192
+ border-radius: 8px;
1193
+ overflow: hidden;
1194
+ background-color: var(--bg-primary);
1195
+ min-height: 300px;
1196
+ transition: border-color 0.2s, box-shadow 0.2s;
1197
+ }
1198
+
1199
+ .terminal-pane:hover {
1200
+ border-color: var(--text-muted);
1201
+ }
1202
+
1203
+ .terminal-pane.active {
1204
+ border-color: var(--accent-green);
1205
+ }
1206
+
1207
+
1208
+ /* Pane Header */
1209
+ .pane-header {
1210
+ display: flex;
1211
+ align-items: center;
1212
+ gap: 8px;
1213
+ padding: 6px 10px;
1214
+ background-color: var(--bg-secondary);
1215
+ border-bottom: 1px solid var(--border-color);
1216
+ min-height: 32px;
1217
+ }
1218
+
1219
+ .pane-status {
1220
+ width: 8px;
1221
+ height: 8px;
1222
+ border-radius: 50%;
1223
+ flex-shrink: 0;
1224
+ }
1225
+
1226
+ .pane-status.running { background-color: var(--accent-green); }
1227
+ .pane-status.initializing { background-color: var(--accent-yellow); }
1228
+ .pane-status.stopped { background-color: var(--text-muted); }
1229
+ .pane-status.error { background-color: var(--accent-red); }
1230
+
1231
+ .pane-title {
1232
+ flex: 1;
1233
+ font-size: 11px;
1234
+ font-weight: 500;
1235
+ color: var(--text-primary);
1236
+ white-space: nowrap;
1237
+ overflow: hidden;
1238
+ text-overflow: ellipsis;
1239
+ }
1240
+
1241
+ /* Pane Terminal Container */
1242
+ .pane-terminal {
1243
+ flex: 1;
1244
+ position: relative;
1245
+ overflow: hidden;
1246
+ }
1247
+
1248
+ .pane-terminal .terminal {
1249
+ height: 100%;
1250
+ width: 100%;
1251
+ padding: 4px;
1252
+ }
1253
+
1254
+ /* Empty Grid Message */
1255
+ .empty-grid-message {
1256
+ display: flex;
1257
+ flex-direction: column;
1258
+ align-items: center;
1259
+ justify-content: center;
1260
+ height: 100%;
1261
+ color: var(--text-muted);
1262
+ gap: 16px;
1263
+ }
1264
+
1265
+ .empty-grid-message p {
1266
+ font-size: 14px;
1267
+ }