claude-code-remote-pilot 0.4.4 → 0.4.6

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.
package/lib/ui.html CHANGED
@@ -1,12 +1,7 @@
1
1
  <!doctype html>
2
2
  <html lang="en">
3
3
  <head><script>(function(){
4
- function makeStore(){
5
- var data={};
6
- var api={getItem:function(k){return Object.prototype.hasOwnProperty.call(data,k)?data[k]:null;},setItem:function(k,v){data[k]=String(v);},removeItem:function(k){delete data[k];},clear:function(){data={};},key:function(i){return Object.keys(data)[i]||null;}};
7
- Object.defineProperty(api,'length',{get:function(){return Object.keys(data).length;}});
8
- return api;
9
- }
4
+ function makeStore(){var data={};var api={getItem:function(k){return Object.prototype.hasOwnProperty.call(data,k)?data[k]:null;},setItem:function(k,v){data[k]=String(v);},removeItem:function(k){delete data[k];},clear:function(){data={};},key:function(i){return Object.keys(data)[i]||null;}};Object.defineProperty(api,'length',{get:function(){return Object.keys(data).length;}});return api;}
10
5
  function tryShim(name){var works=false;try{works=!!window[name]&&typeof window[name].getItem==='function';void window[name].length;}catch(_){works=false;}if(works)return;try{Object.defineProperty(window,name,{configurable:true,value:makeStore()});}catch(_){try{window[name]=makeStore();}catch(__){}}};
11
6
  tryShim('localStorage');tryShim('sessionStorage');
12
7
  })();</script>
@@ -29,7 +24,6 @@
29
24
  --border: oklch(90% 0.014 70);
30
25
  --accent: oklch(64% 0.13 28);
31
26
  --accent-soft: oklch(64% 0.13 28 / 0.08);
32
- --accent-medium: oklch(64% 0.13 28 / 0.15);
33
27
  --success: oklch(62% 0.14 145);
34
28
  --success-soft: oklch(62% 0.14 145 / 0.1);
35
29
  --warning: oklch(68% 0.16 75);
@@ -61,7 +55,6 @@
61
55
  --border: oklch(32% 0.02 50);
62
56
  --accent: oklch(70% 0.14 32);
63
57
  --accent-soft: oklch(70% 0.14 32 / 0.12);
64
- --accent-medium: oklch(70% 0.14 32 / 0.2);
65
58
  --success: oklch(68% 0.15 150);
66
59
  --success-soft: oklch(68% 0.15 150 / 0.12);
67
60
  --warning: oklch(72% 0.16 78);
@@ -75,155 +68,169 @@
75
68
  --shadow-inner: inset 0 1px 3px oklch(0% 0 0 / 0.15);
76
69
  }
77
70
 
78
- body { font-family:var(--font-body);background:var(--bg);color:var(--fg);line-height:1.5;font-size:14px;-webkit-font-smoothing:antialiased; }
79
-
80
- #root { height:100vh;display:flex;flex-direction:column; }
81
- .app-shell { display:flex;flex-direction:column;height:100vh;overflow:hidden; }
82
-
83
- .sidebar { position:fixed;top:0;left:0;width:100%;height:auto;max-height:85vh;background:var(--surface);border-bottom:1px solid var(--border);z-index:100;transform:translateY(-100%);transition:transform 0.25s ease;border-radius:0 0 var(--radius-lg) var(--radius-lg);overflow-y:auto; }
84
- .sidebar.open { transform:translateY(0); }
85
- .sidebar-overlay { position:fixed;inset:0;background:oklch(0% 0 0 / 0.3);z-index:99;opacity:0;pointer-events:none;transition:opacity 0.25s ease; }
86
- .sidebar-overlay.open { opacity:1;pointer-events:auto; }
87
- .sidebar-header { padding:14px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px; }
88
- .sidebar-close { margin-left:auto;background:none;border:none;cursor:pointer;color:var(--muted);padding:4px;display:flex; }
89
- .logo { display:flex;align-items:center;gap:10px;text-decoration:none;color:var(--fg); }
90
- .logo-mark { width:28px;height:28px;background:var(--accent);border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;color:var(--surface);font-weight:700;font-size:13px; }
91
- .logo-text { font-family:var(--font-display);font-size:16px;font-weight:600;letter-spacing:-0.01em; }
92
- .logo-badge { font-size:10px;font-family:var(--font-mono);color:var(--muted);background:var(--accent-soft);padding:2px 6px;border-radius:4px; }
93
- .sidebar-nav { padding:12px 10px; }
94
- .nav-section-label { font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--muted);padding:8px 8px 4px;margin-top:8px; }
95
- .nav-item { display:flex;align-items:center;gap:10px;padding:10px;border-radius:var(--radius-sm);color:var(--fg-secondary);cursor:pointer;font-size:14px;font-weight:500;transition:background 0.15s,color 0.15s;user-select:none;border:none;background:none;width:100%;text-align:left; }
96
- .nav-item:hover { background:var(--surface-hover);color:var(--fg); }
97
- .nav-item.active { background:var(--accent-soft);color:var(--fg);font-weight:600; }
98
- .nav-icon { width:18px;height:18px;opacity:0.7;flex-shrink:0; }
99
- .nav-item.active .nav-icon { opacity:1; }
100
- .nav-count { margin-left:auto;font-size:11px;font-family:var(--font-mono);color:var(--muted);background:var(--surface-hover);padding:1px 6px;border-radius:10px;min-width:20px;text-align:center; }
101
- .sidebar-footer { padding:12px 10px;border-top:1px solid var(--border);margin-top:auto; }
102
-
103
- .main { flex:1;display:flex;flex-direction:column;overflow:hidden; }
104
- .mobile-header { height:var(--header-h);border-bottom:1px solid var(--border);background:var(--surface);display:flex;align-items:center;padding:0 16px;gap:12px;flex-shrink:0; }
105
- .menu-btn { background:none;border:none;cursor:pointer;color:var(--fg);padding:6px;display:flex; }
106
- .header { height:var(--header-h);border-bottom:1px solid var(--border);background:var(--surface);display:none;align-items:center;padding:0 24px;gap:16px;flex-shrink:0; }
107
- .mobile-title { font-family:var(--font-display);font-size:15px;font-weight:600;flex:1; }
108
- .header-title { font-family:var(--font-display);font-size:15px;font-weight:600; }
109
- .header-spacer { flex:1; }
110
- .theme-toggle { display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg);color:var(--fg-secondary);cursor:pointer;font-size:12px;font-family:var(--font-body);transition:border-color 0.15s; }
111
- .theme-toggle:hover { border-color:var(--muted); }
112
-
113
- .content { flex:1;overflow-y:auto;padding:16px; }
114
-
115
- .session-cards { display:grid;grid-template-columns:1fr;gap:12px; }
116
- .session-card { background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);padding:16px;cursor:pointer;transition:border-color 0.15s,box-shadow 0.15s; }
117
- .session-card:hover { border-color:var(--muted);box-shadow:var(--shadow-md); }
118
- .session-card.offline { opacity:0.6; }
119
- .session-card-header { display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px; }
120
- .session-card-name { font-weight:600;font-size:14px;color:var(--fg);line-height:1.3; }
121
- .session-card-meta { font-size:12px;color:var(--muted);margin-top:4px; }
122
- .session-card-body { display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid var(--border); }
123
- .session-card-field { font-size:11px; }
124
- .session-card-field-label { color:var(--muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:2px; }
125
- .session-card-field-value { font-family:var(--font-mono);font-size:12px;color:var(--fg-secondary); }
126
-
127
- .status-pill { display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:3px 8px;border-radius:10px;font-family:var(--font-mono);letter-spacing:0.02em; }
128
- .status-dot { width:6px;height:6px;border-radius:50%; }
129
- .status-running { background:var(--success-soft);color:var(--success); }
130
- .status-running .status-dot { background:var(--success); }
131
- .status-idle { background:var(--idle-soft);color:var(--idle); }
132
- .status-idle .status-dot { background:var(--idle); }
133
- .status-error { background:var(--error-soft);color:var(--error); }
134
- .status-error .status-dot { background:var(--error); }
135
- .status-warn { background:var(--warning-soft);color:var(--warning); }
136
- .status-warn .status-dot { background:var(--warning); }
137
-
138
- .card { background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-sm); }
139
-
140
- .btn { display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:var(--radius-sm);font-size:13px;font-weight:500;font-family:var(--font-body);cursor:pointer;border:1px solid var(--border);background:var(--surface);color:var(--fg);transition:background 0.15s,border-color 0.15s;white-space:nowrap; }
141
- .btn:hover { background:var(--surface-hover); }
142
- .btn:disabled { opacity:0.5;cursor:not-allowed; }
143
- .btn-primary { background:var(--accent);color:oklch(99% 0.008 70);border-color:var(--accent); }
144
- .btn-primary:hover { filter:brightness(1.05); }
145
- .btn-sm { padding:4px 10px;font-size:12px; }
146
-
147
- .terminal { background:oklch(14% 0.015 50);color:oklch(82% 0.015 70);font-family:var(--font-mono);font-size:13px;line-height:1.6;border-radius:var(--radius-md);overflow:hidden;border:1px solid oklch(28% 0.02 50); }
148
- .terminal-header { display:flex;align-items:center;gap:8px;padding:10px 14px;background:oklch(18% 0.018 50);border-bottom:1px solid oklch(28% 0.02 50); }
149
- .terminal-dot { width:10px;height:10px;border-radius:50%; }
150
- .terminal-title { font-size:12px;color:oklch(65% 0.018 50);margin-left:8px; }
151
- .terminal-body { padding:16px;min-height:320px;max-height:420px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;font-family:var(--font-mono);font-size:12px;line-height:1.5; }
152
-
153
- .form-group { margin-bottom:18px; }
154
- .form-label { display:block;font-size:12px;font-weight:600;color:var(--fg);margin-bottom:6px; }
155
- .form-hint { font-size:11px;color:var(--muted);margin-top:4px; }
156
- .form-input,.form-select,.form-textarea { width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);color:var(--fg);font-size:13px;font-family:var(--font-body);transition:border-color 0.15s; }
157
- .form-input:focus,.form-select:focus,.form-textarea:focus { outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft); }
158
- .form-textarea { resize:vertical;min-height:80px; }
159
- .form-actions { display:flex;gap:10px;justify-content:flex-end;padding-top:20px;border-top:1px solid var(--border);margin-top:8px; }
160
-
161
- .stat-row { display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:20px; }
162
- .stat-card { padding:14px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-inner); }
163
- .stat-label { font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:var(--muted);margin-bottom:6px; }
164
- .stat-value { font-size:22px;font-weight:700;font-family:var(--font-display);color:var(--fg);line-height:1.2; }
165
- .stat-sub { font-size:11px;color:var(--muted);margin-top:4px; }
166
-
167
- .activity-list { list-style:none; }
168
- .activity-item { display:flex;align-items:flex-start;gap:12px;padding:10px 0;border-bottom:1px solid var(--border);font-size:13px; }
169
- .activity-item:last-child { border-bottom:none; }
170
- .activity-time { font-size:11px;color:var(--muted);font-family:var(--font-mono);white-space:nowrap;margin-top:2px; }
171
- .activity-text { color:var(--fg-secondary); }
172
- .activity-text strong { color:var(--fg);font-weight:600; }
173
-
174
- .section-header { display:flex;align-items:center;justify-content:space-between;margin-bottom:16px; }
175
- .section-title { font-family:var(--font-display);font-size:15px;font-weight:600; }
176
-
177
- .back-link { display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--muted);cursor:pointer;margin-bottom:16px;font-weight:500;border:none;background:none;padding:0;font-family:var(--font-body); }
178
- .back-link:hover { color:var(--fg); }
179
-
180
- .detail-header { display:flex;flex-direction:column;align-items:flex-start;gap:12px;margin-bottom:20px; }
181
- .detail-title { font-family:var(--font-display);font-size:18px;font-weight:600;line-height:1.3; }
182
- .detail-meta { font-size:12px;color:var(--muted);margin-top:4px; }
183
- .detail-actions { display:flex;gap:8px;margin-left:0;flex-wrap:wrap; }
184
-
185
- .detail-grid { display:grid;grid-template-columns:1fr;gap:16px; }
186
- .detail-sidebar .card { padding:16px; }
187
- .detail-sidebar h3 { font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:var(--muted);margin-bottom:12px; }
188
-
189
- .info-row { display:flex;justify-content:space-between;padding:6px 0;font-size:12px; }
190
- .info-label { color:var(--muted); }
191
- .info-value { font-weight:600;font-family:var(--font-mono); }
192
-
193
- .empty-state { text-align:center;padding:48px 24px;color:var(--muted); }
194
- .empty-state-icon { font-size:32px;margin-bottom:12px;opacity:0.5; }
195
- .empty-state-title { font-family:var(--font-display);font-size:15px;font-weight:600;color:var(--fg-secondary);margin-bottom:6px; }
196
- .empty-state-desc { font-size:13px; }
197
-
198
- .create-layout { max-width:640px; }
199
-
200
- .content::-webkit-scrollbar { width:6px; }
201
- .content::-webkit-scrollbar-track { background:transparent; }
202
- .content::-webkit-scrollbar-thumb { background:var(--border);border-radius:3px; }
203
- .terminal-body::-webkit-scrollbar { width:6px; }
204
- .terminal-body::-webkit-scrollbar-track { background:transparent; }
205
- .terminal-body::-webkit-scrollbar-thumb { background:oklch(30% 0.02 50);border-radius:3px; }
206
-
207
- .conn-dot { width:8px;height:8px;border-radius:50%;background:var(--success);display:inline-block; }
208
- .conn-dot.disconnected { background:var(--error); }
71
+ body{font-family:var(--font-body);background:var(--bg);color:var(--fg);line-height:1.5;font-size:14px;-webkit-font-smoothing:antialiased;}
72
+ #root{height:100vh;display:flex;flex-direction:column;}
73
+ .app-shell{display:flex;flex-direction:column;height:100vh;overflow:hidden;}
74
+
75
+ .sidebar{position:fixed;top:0;left:0;width:100%;height:auto;max-height:85vh;background:var(--surface);border-bottom:1px solid var(--border);z-index:100;transform:translateY(-100%);transition:transform 0.25s ease;border-radius:0 0 var(--radius-lg) var(--radius-lg);overflow-y:auto;}
76
+ .sidebar.open{transform:translateY(0);}
77
+ .sidebar-overlay{position:fixed;inset:0;background:oklch(0% 0 0 / 0.3);z-index:99;opacity:0;pointer-events:none;transition:opacity 0.25s ease;}
78
+ .sidebar-overlay.open{opacity:1;pointer-events:auto;}
79
+ .sidebar-header{padding:14px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;}
80
+ .sidebar-close{margin-left:auto;background:none;border:none;cursor:pointer;color:var(--muted);padding:4px;display:flex;}
81
+ .logo{display:flex;align-items:center;gap:10px;text-decoration:none;color:var(--fg);}
82
+ .logo-mark{width:28px;height:28px;background:var(--accent);border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;color:var(--surface);font-weight:700;font-size:13px;}
83
+ .logo-text{font-family:var(--font-display);font-size:16px;font-weight:600;letter-spacing:-0.01em;}
84
+ .logo-badge{font-size:10px;font-family:var(--font-mono);color:var(--muted);background:var(--accent-soft);padding:2px 6px;border-radius:4px;}
85
+ .sidebar-nav{padding:12px 10px;}
86
+ .nav-section-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--muted);padding:8px 8px 4px;margin-top:8px;}
87
+ .nav-item{display:flex;align-items:center;gap:10px;padding:10px;border-radius:var(--radius-sm);color:var(--fg-secondary);cursor:pointer;font-size:14px;font-weight:500;transition:background 0.15s,color 0.15s;user-select:none;border:none;background:none;width:100%;text-align:left;}
88
+ .nav-item:hover{background:var(--surface-hover);color:var(--fg);}
89
+ .nav-item.active{background:var(--accent-soft);color:var(--fg);font-weight:600;}
90
+ .nav-icon{width:18px;height:18px;opacity:0.7;flex-shrink:0;}
91
+ .nav-item.active .nav-icon{opacity:1;}
92
+ .nav-count{margin-left:auto;font-size:11px;font-family:var(--font-mono);color:var(--muted);background:var(--surface-hover);padding:1px 6px;border-radius:10px;min-width:20px;text-align:center;}
93
+ .sidebar-footer{padding:12px 10px;border-top:1px solid var(--border);margin-top:auto;}
94
+
95
+ .main{flex:1;display:flex;flex-direction:column;overflow:hidden;}
96
+ .mobile-header{height:var(--header-h);border-bottom:1px solid var(--border);background:var(--surface);display:flex;align-items:center;padding:0 16px;gap:12px;flex-shrink:0;}
97
+ .menu-btn{background:none;border:none;cursor:pointer;color:var(--fg);padding:6px;display:flex;}
98
+ .header{height:var(--header-h);border-bottom:1px solid var(--border);background:var(--surface);display:none;align-items:center;padding:0 24px;gap:16px;flex-shrink:0;}
99
+ .mobile-title{font-family:var(--font-display);font-size:15px;font-weight:600;flex:1;}
100
+ .header-title{font-family:var(--font-display);font-size:15px;font-weight:600;}
101
+ .header-spacer{flex:1;}
102
+ .theme-toggle{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg);color:var(--fg-secondary);cursor:pointer;font-size:12px;font-family:var(--font-body);transition:border-color 0.15s;}
103
+ .theme-toggle:hover{border-color:var(--muted);}
104
+
105
+ .content{flex:1;overflow-y:auto;padding:16px;}
106
+
107
+ .session-cards{display:grid;grid-template-columns:1fr;gap:12px;}
108
+ .session-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);padding:16px;cursor:pointer;transition:border-color 0.15s,box-shadow 0.15s;}
109
+ .session-card:hover{border-color:var(--muted);box-shadow:var(--shadow-md);}
110
+ .session-card.offline{opacity:0.6;}
111
+ .session-card-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px;}
112
+ .session-card-name{font-weight:600;font-size:14px;color:var(--fg);line-height:1.3;}
113
+ .session-card-meta{font-size:12px;color:var(--muted);margin-top:4px;}
114
+ .session-card-body{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid var(--border);}
115
+ .session-card-field{font-size:11px;}
116
+ .session-card-field-label{color:var(--muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:2px;}
117
+ .session-card-field-value{font-family:var(--font-mono);font-size:12px;color:var(--fg-secondary);}
118
+
119
+ .status-pill{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:3px 8px;border-radius:10px;font-family:var(--font-mono);letter-spacing:0.02em;}
120
+ .status-dot{width:6px;height:6px;border-radius:50%;}
121
+ .status-running{background:var(--success-soft);color:var(--success);}
122
+ .status-running .status-dot{background:var(--success);}
123
+ .status-idle{background:var(--idle-soft);color:var(--idle);}
124
+ .status-idle .status-dot{background:var(--idle);}
125
+ .status-error{background:var(--error-soft);color:var(--error);}
126
+ .status-error .status-dot{background:var(--error);}
127
+ .status-warn{background:var(--warning-soft);color:var(--warning);}
128
+ .status-warn .status-dot{background:var(--warning);}
129
+
130
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-sm);}
131
+
132
+ .btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:var(--radius-sm);font-size:13px;font-weight:500;font-family:var(--font-body);cursor:pointer;border:1px solid var(--border);background:var(--surface);color:var(--fg);transition:background 0.15s,border-color 0.15s;white-space:nowrap;}
133
+ .btn:hover{background:var(--surface-hover);}
134
+ .btn:disabled{opacity:0.5;cursor:not-allowed;}
135
+ .btn-primary{background:var(--accent);color:oklch(99% 0.008 70);border-color:var(--accent);}
136
+ .btn-primary:hover{filter:brightness(1.05);}
137
+ .btn-sm{padding:4px 10px;font-size:12px;}
138
+
139
+ /* Terminal — flex column so body grows and footer stays pinned */
140
+ .terminal{background:oklch(14% 0.015 50);color:oklch(82% 0.015 70);font-family:var(--font-mono);font-size:13px;line-height:1.6;border-radius:var(--radius-md);overflow:hidden;border:1px solid oklch(28% 0.02 50);display:flex;flex-direction:column;}
141
+ .terminal-header{display:flex;align-items:center;gap:8px;padding:10px 14px;background:oklch(18% 0.018 50);border-bottom:1px solid oklch(28% 0.02 50);flex-shrink:0;}
142
+ .terminal-dot{width:10px;height:10px;border-radius:50%;}
143
+ .terminal-title{font-size:12px;color:oklch(65% 0.018 50);margin-left:8px;}
144
+ .terminal-body{flex:1;min-height:0;padding:16px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;font-family:var(--font-mono);font-size:12px;line-height:1.5;}
145
+ .terminal-footer{display:flex;align-items:center;gap:8px;padding:8px 14px;background:oklch(18% 0.018 50);border-top:1px solid oklch(28% 0.02 50);flex-shrink:0;}
146
+ .terminal-prompt-char{color:oklch(72% 0.16 145);font-family:var(--font-mono);font-size:13px;flex-shrink:0;line-height:1;}
147
+ .terminal-input{flex:1;background:transparent;border:none;outline:none;color:oklch(82% 0.015 70);font-family:var(--font-mono);font-size:12px;padding:2px 0;min-width:0;}
148
+ .terminal-input::placeholder{color:oklch(42% 0.018 50);}
149
+ .btn-key{background:oklch(26% 0.02 50);border:1px solid oklch(35% 0.02 50);color:oklch(65% 0.018 50);font-family:var(--font-mono);font-size:11px;padding:2px 7px;border-radius:4px;cursor:pointer;flex-shrink:0;transition:background 0.12s;}
150
+ .btn-key:hover{background:oklch(32% 0.02 50);color:oklch(78% 0.018 50);}
151
+
152
+ .form-group{margin-bottom:18px;}
153
+ .form-label{display:block;font-size:12px;font-weight:600;color:var(--fg);margin-bottom:6px;}
154
+ .form-hint{font-size:11px;color:var(--muted);margin-top:4px;}
155
+ .form-input,.form-select,.form-textarea{width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);color:var(--fg);font-size:13px;font-family:var(--font-body);transition:border-color 0.15s;}
156
+ .form-input:focus,.form-select:focus,.form-textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);}
157
+ .form-textarea{resize:vertical;min-height:80px;}
158
+ .form-actions{display:flex;gap:10px;justify-content:flex-end;padding-top:20px;border-top:1px solid var(--border);margin-top:8px;}
159
+
160
+ .stat-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:20px;}
161
+ .stat-card{padding:14px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-inner);}
162
+ .stat-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:var(--muted);margin-bottom:6px;}
163
+ .stat-value{font-size:22px;font-weight:700;font-family:var(--font-display);color:var(--fg);line-height:1.2;}
164
+ .stat-sub{font-size:11px;color:var(--muted);margin-top:4px;}
165
+
166
+ .activity-list{list-style:none;}
167
+ .activity-item{display:flex;align-items:flex-start;gap:12px;padding:10px 0;border-bottom:1px solid var(--border);font-size:13px;}
168
+ .activity-item:last-child{border-bottom:none;}
169
+ .activity-time{font-size:11px;color:var(--muted);font-family:var(--font-mono);white-space:nowrap;margin-top:2px;}
170
+ .activity-text{color:var(--fg-secondary);}
171
+ .activity-text strong{color:var(--fg);font-weight:600;}
172
+
173
+ .section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;}
174
+ .section-title{font-family:var(--font-display);font-size:15px;font-weight:600;}
175
+
176
+ .back-link{display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--muted);cursor:pointer;margin-bottom:16px;font-weight:500;border:none;background:none;padding:0;font-family:var(--font-body);}
177
+ .back-link:hover{color:var(--fg);}
178
+
179
+ .detail-header{display:flex;flex-direction:column;align-items:flex-start;gap:12px;margin-bottom:16px;}
180
+ .detail-title{font-family:var(--font-display);font-size:18px;font-weight:600;line-height:1.3;}
181
+ .detail-meta{font-size:12px;color:var(--muted);margin-top:4px;}
182
+ .detail-actions{display:flex;gap:8px;flex-wrap:wrap;}
183
+
184
+ .detail-grid{display:grid;grid-template-columns:1fr;gap:16px;}
185
+ .detail-sidebar .card{padding:16px;}
186
+ .detail-sidebar h3{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:var(--muted);margin-bottom:12px;}
187
+
188
+ .info-row{display:flex;justify-content:space-between;padding:6px 0;font-size:12px;}
189
+ .info-label{color:var(--muted);}
190
+ .info-value{font-weight:600;font-family:var(--font-mono);}
191
+
192
+ .empty-state{text-align:center;padding:48px 24px;color:var(--muted);}
193
+ .empty-state-icon{font-size:32px;margin-bottom:12px;opacity:0.5;}
194
+ .empty-state-title{font-family:var(--font-display);font-size:15px;font-weight:600;color:var(--fg-secondary);margin-bottom:6px;}
195
+ .empty-state-desc{font-size:13px;}
196
+
197
+ .create-layout{max-width:640px;}
198
+
199
+ /* Login screen */
200
+ .login-wrap{display:flex;justify-content:center;align-items:center;height:100vh;background:var(--bg);}
201
+ .login-card{width:320px;padding:32px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-md);}
202
+ .login-logo{text-align:center;margin-bottom:24px;}
203
+ .login-mark{width:44px;height:44px;background:var(--accent);border-radius:var(--radius-sm);display:inline-flex;align-items:center;justify-content:center;color:white;font-weight:700;font-size:18px;margin-bottom:12px;}
204
+ .login-title{font-family:var(--font-display);font-size:18px;font-weight:600;margin-bottom:4px;}
205
+ .login-sub{font-size:13px;color:var(--muted);}
206
+
207
+ .conn-dot{width:8px;height:8px;border-radius:50%;background:var(--success);display:inline-block;}
208
+ .conn-dot.disconnected{background:var(--error);}
209
+
210
+ .content::-webkit-scrollbar{width:6px;}
211
+ .content::-webkit-scrollbar-track{background:transparent;}
212
+ .content::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px;}
213
+ .terminal-body::-webkit-scrollbar{width:6px;}
214
+ .terminal-body::-webkit-scrollbar-track{background:transparent;}
215
+ .terminal-body::-webkit-scrollbar-thumb{background:oklch(30% 0.02 50);border-radius:3px;}
209
216
 
210
217
  @media (min-width:768px) {
211
- .app-shell { flex-direction:row; }
212
- .sidebar { position:static;width:var(--sidebar-w);height:100vh;max-height:none;transform:none;border-bottom:none;border-right:1px solid var(--border);border-radius:0;display:flex;flex-direction:column; }
213
- .sidebar-close,.sidebar-overlay { display:none; }
214
- .mobile-header { display:none; }
215
- .header { display:flex; }
216
- .content { padding:24px; }
217
- .session-cards { grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); }
218
- .stat-row { grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:24px; }
219
- .detail-grid { grid-template-columns:1fr 300px;gap:20px; }
220
- .detail-header { flex-direction:row;align-items:flex-start;gap:16px;margin-bottom:24px; }
221
- .detail-title { font-size:20px; }
222
- .detail-actions { margin-left:auto; }
218
+ .app-shell{flex-direction:row;}
219
+ .sidebar{position:static;width:var(--sidebar-w);height:100vh;max-height:none;transform:none;border-bottom:none;border-right:1px solid var(--border);border-radius:0;display:flex;flex-direction:column;}
220
+ .sidebar-close,.sidebar-overlay{display:none;}
221
+ .mobile-header{display:none;}
222
+ .header{display:flex;}
223
+ .content{padding:24px;}
224
+ .session-cards{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));}
225
+ .stat-row{grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:24px;}
226
+ .detail-grid{grid-template-columns:1fr 280px;gap:20px;}
227
+ .detail-header{flex-direction:row;align-items:flex-start;gap:16px;margin-bottom:20px;}
228
+ .detail-title{font-size:20px;}
229
+ .detail-actions{margin-left:auto;}
223
230
  }
224
231
 
225
232
  @media (min-width:1200px) {
226
- .session-cards { grid-template-columns:repeat(auto-fill,minmax(340px,1fr)); }
233
+ .session-cards{grid-template-columns:repeat(auto-fill,minmax(340px,1fr));}
227
234
  }
228
235
  </style>
229
236
  </head>
@@ -233,12 +240,40 @@
233
240
  <script type="text/babel">
234
241
  const { useState, useEffect, useRef, useCallback } = React;
235
242
 
243
+ /* --- Safe storage --- */
236
244
  const memStore = new Map();
237
245
  const storage = {
238
246
  getItem(key) { try { return localStorage.getItem(key); } catch { return memStore.has(key) ? memStore.get(key) : null; } },
239
247
  setItem(key, val) { try { localStorage.setItem(key, val); } catch { memStore.set(key, val); } },
240
248
  };
241
249
 
250
+ /* --- Auth module-level helpers --- */
251
+ let _authToken = storage.getItem('ccp-token') || '';
252
+ let _onUnauth = () => {};
253
+
254
+ function setAuthToken(t) {
255
+ _authToken = t;
256
+ storage.setItem('ccp-token', t || '');
257
+ }
258
+
259
+ function apiFetch(url, opts = {}) {
260
+ const headers = { ...opts.headers };
261
+ if (_authToken) headers['Authorization'] = `Bearer ${_authToken}`;
262
+ return fetch(url, { ...opts, headers }).then(res => {
263
+ if (res.status === 401) {
264
+ setAuthToken('');
265
+ _onUnauth();
266
+ throw new Error('Unauthorized');
267
+ }
268
+ return res;
269
+ });
270
+ }
271
+
272
+ function sseUrl() {
273
+ return _authToken ? `/events?token=${encodeURIComponent(_authToken)}` : '/events';
274
+ }
275
+
276
+ /* --- Formatters --- */
242
277
  function relativeTime(dateStr) {
243
278
  if (!dateStr) return '—';
244
279
  const secs = Math.max(0, Math.floor((Date.now() - new Date(dateStr)) / 1000));
@@ -253,8 +288,7 @@ function formatUptime(startedAt) {
253
288
  const secs = Math.max(0, Math.floor((Date.now() - new Date(startedAt)) / 1000));
254
289
  const h = Math.floor(secs / 3600);
255
290
  const m = Math.floor((secs % 3600) / 60);
256
- if (h > 0) return `${h}h ${m}m`;
257
- return `${m}m`;
291
+ return h > 0 ? `${h}h ${m}m` : `${m}m`;
258
292
  }
259
293
 
260
294
  function formatTokens(tokens) {
@@ -272,9 +306,8 @@ function statusCls(status) {
272
306
  }
273
307
  }
274
308
 
275
- function statusLabel(status) {
276
- if (status === 'needs-response') return 'needs input';
277
- return status;
309
+ function statusLabel(s) {
310
+ return s === 'needs-response' ? 'needs input' : s;
278
311
  }
279
312
 
280
313
  /* --- Icons --- */
@@ -282,27 +315,76 @@ const Icons = {
282
315
  dashboard: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="2" y="2" width="6" height="6" rx="1"/><rect x="10" y="2" width="6" height="6" rx="1"/><rect x="2" y="10" width="6" height="6" rx="1"/><rect x="10" y="10" width="6" height="6" rx="1"/></svg>,
283
316
  sessions: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="2" y="3" width="14" height="12" rx="1.5"/><line x1="2" y1="7" x2="16" y2="7"/><line x1="5" y1="11" x2="9" y2="11"/></svg>,
284
317
  plus: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><line x1="9" y1="3" x2="9" y2="15"/><line x1="3" y1="9" x2="15" y2="9"/></svg>,
285
- settings: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="9" cy="9" r="3"/><path d="M9 1v2M9 15v2M1 9h2M15 9h2M3.3 3.3l1.4 1.4M13.3 13.3l1.4 1.4M3.3 14.7l1.4-1.4M13.3 4.7l1.4-1.4"/></svg>,
286
318
  sun: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="9" cy="9" r="4"/><line x1="9" y1="1" x2="9" y2="3"/><line x1="9" y1="15" x2="9" y2="17"/><line x1="1" y1="9" x2="3" y2="9"/><line x1="15" y1="9" x2="17" y2="9"/></svg>,
287
319
  moon: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M15 10.5a6.5 6.5 0 0 1-8-8A6.5 6.5 0 1 0 15 10.5Z"/></svg>,
288
320
  arrow: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><polyline points="11,4 6,9 11,14"/></svg>,
289
- terminal: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><polyline points="4,5 8,9 4,13"/><line x1="10" y1="13" x2="14" y2="13"/></svg>,
290
321
  trash: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M3 5h12M7 5V3.5A1.5 1.5 0 0 1 8.5 2h1A1.5 1.5 0 0 1 11 3.5V5M6 8v5M9 8v5M12 8v5"/><path d="M4.5 5l.7 9.8a1.5 1.5 0 0 0 1.5 1.2h6.6a1.5 1.5 0 0 0 1.5-1.2L13.5 5"/></svg>,
291
- refresh: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M3 9a6 6 0 0 1 10.9-2.5M15 9a6 6 0 0 1-10.9 2.5"/><polyline points="3,4 3,9 8,9"/><polyline points="15,14 15,9 10,9"/></svg>,
292
322
  menu: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><line x1="3" y1="5" x2="15" y2="5"/><line x1="3" y1="9" x2="15" y2="9"/><line x1="3" y1="13" x2="15" y2="13"/></svg>,
293
323
  close: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><line x1="4" y1="4" x2="14" y2="14"/><line x1="14" y1="4" x2="4" y2="14"/></svg>,
294
- send: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><line x1="2" y1="9" x2="16" y2="9"/><polyline points="10,3 16,9 10,15"/></svg>,
324
+ lock: <svg className="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="3" y="8" width="12" height="9" rx="1.5"/><path d="M6 8V5.5a3 3 0 0 1 6 0V8"/></svg>,
295
325
  };
296
326
 
327
+ /* --- Login Screen --- */
328
+ function LoginScreen({ onLogin }) {
329
+ const [password, setPassword] = useState('');
330
+ const [error, setError] = useState('');
331
+ const [loading, setLoading] = useState(false);
332
+
333
+ const handleLogin = async () => {
334
+ if (!password) return;
335
+ setLoading(true);
336
+ setError('');
337
+ try {
338
+ const res = await fetch('/api/login', {
339
+ method: 'POST',
340
+ headers: { 'Content-Type': 'application/json' },
341
+ body: JSON.stringify({ password }),
342
+ });
343
+ const data = await res.json();
344
+ if (!res.ok) { setError(data.error || 'Wrong password'); return; }
345
+ setAuthToken(data.token);
346
+ onLogin();
347
+ } catch {
348
+ setError('Network error — is the pilot running?');
349
+ } finally {
350
+ setLoading(false);
351
+ }
352
+ };
353
+
354
+ return (
355
+ <div className="login-wrap">
356
+ <div className="login-card">
357
+ <div className="login-logo">
358
+ <div className="login-mark">C</div>
359
+ <div className="login-title">Code Pilot</div>
360
+ <div className="login-sub">Enter password to continue</div>
361
+ </div>
362
+ <div className="form-group">
363
+ <input
364
+ className="form-input"
365
+ type="password"
366
+ placeholder="Password"
367
+ value={password}
368
+ onChange={e => setPassword(e.target.value)}
369
+ onKeyDown={e => e.key === 'Enter' && handleLogin()}
370
+ autoFocus
371
+ />
372
+ </div>
373
+ {error && <div style={{ color: 'var(--error)', fontSize: 13, marginBottom: 12 }}>{error}</div>}
374
+ <button className="btn btn-primary" style={{ width: '100%' }} onClick={handleLogin} disabled={loading || !password}>
375
+ {Icons.lock} {loading ? 'Signing in…' : 'Sign in'}
376
+ </button>
377
+ </div>
378
+ </div>
379
+ );
380
+ }
381
+
297
382
  /* --- Status Pill --- */
298
383
  function StatusPill({ status }) {
299
- const cls = statusCls(status);
300
- const label = statusLabel(status);
301
- const dim = status === 'offline';
302
384
  return (
303
- <span className={`status-pill ${cls}`} style={dim ? { opacity: 0.55 } : {}}>
385
+ <span className={`status-pill ${statusCls(status)}`} style={status === 'offline' ? { opacity: 0.55 } : {}}>
304
386
  <span className="status-dot" />
305
- {label}
387
+ {statusLabel(status)}
306
388
  </span>
307
389
  );
308
390
  }
@@ -318,7 +400,7 @@ function Sidebar({ currentScreen, onNavigate, sessionCount, open, onClose, conne
318
400
  <div className="logo-mark">C</div>
319
401
  <span className="logo-text">Code Pilot</span>
320
402
  </a>
321
- <span className="logo-badge">v0.4.0</span>
403
+ <span className="logo-badge">v0.4.5</span>
322
404
  <button className="sidebar-close" onClick={onClose}>{Icons.close}</button>
323
405
  </div>
324
406
  <nav className="sidebar-nav">
@@ -334,9 +416,9 @@ function Sidebar({ currentScreen, onNavigate, sessionCount, open, onClose, conne
334
416
  </button>
335
417
  </nav>
336
418
  <div className="sidebar-footer">
337
- <div style={{ padding: '4px 8px', fontSize: '11px', color: 'var(--muted)', fontFamily: 'var(--font-mono)', display: 'flex', alignItems: 'center', gap: 6 }}>
419
+ <div style={{ padding: '4px 8px', fontSize: 11, color: 'var(--muted)', fontFamily: 'var(--font-mono)', display: 'flex', alignItems: 'center', gap: 6 }}>
338
420
  <span className={`conn-dot ${connected ? '' : 'disconnected'}`} />
339
- {connected ? 'Connected' : 'Disconnected'}
421
+ {connected ? 'Connected' : 'Reconnecting…'}
340
422
  </div>
341
423
  </div>
342
424
  </aside>
@@ -344,43 +426,25 @@ function Sidebar({ currentScreen, onNavigate, sessionCount, open, onClose, conne
344
426
  );
345
427
  }
346
428
 
347
- /* --- Dashboard Screen --- */
429
+ /* --- Dashboard --- */
348
430
  function DashboardScreen({ onNavigate, sessions, activity, serverStatus }) {
349
- const active = sessions.filter(s => s.status !== 'offline');
350
431
  const running = sessions.filter(s => s.status === 'running');
432
+ const active = sessions.filter(s => s.status !== 'offline');
351
433
  const uptime = serverStatus ? formatUptime(serverStatus.startedAt) : '—';
352
- const port = serverStatus ? serverStatus.port : 3742;
434
+ const port = serverStatus ? serverStatus.port : '—';
353
435
 
354
436
  return (
355
437
  <div>
356
438
  <div className="stat-row">
357
- <div className="stat-card">
358
- <div className="stat-label">Running</div>
359
- <div className="stat-value">{running.length}</div>
360
- <div className="stat-sub">of {sessions.length} total</div>
361
- </div>
362
- <div className="stat-card">
363
- <div className="stat-label">Active</div>
364
- <div className="stat-value">{active.length}</div>
365
- <div className="stat-sub">tmux sessions</div>
366
- </div>
367
- <div className="stat-card">
368
- <div className="stat-label">Supervisor</div>
369
- <div className="stat-value" style={{ color: 'var(--success)', fontSize: 16 }}>Online</div>
370
- <div className="stat-sub">:{port}</div>
371
- </div>
372
- <div className="stat-card">
373
- <div className="stat-label">Uptime</div>
374
- <div className="stat-value" style={{ fontSize: 16 }}>{uptime}</div>
375
- <div className="stat-sub">since last start</div>
376
- </div>
439
+ <div className="stat-card"><div className="stat-label">Running</div><div className="stat-value">{running.length}</div><div className="stat-sub">of {sessions.length} total</div></div>
440
+ <div className="stat-card"><div className="stat-label">Active</div><div className="stat-value">{active.length}</div><div className="stat-sub">tmux sessions</div></div>
441
+ <div className="stat-card"><div className="stat-label">Supervisor</div><div className="stat-value" style={{ color: 'var(--success)', fontSize: 15 }}>Online</div><div className="stat-sub">:{port}</div></div>
442
+ <div className="stat-card"><div className="stat-label">Uptime</div><div className="stat-value" style={{ fontSize: 16 }}>{uptime}</div><div className="stat-sub">since last start</div></div>
377
443
  </div>
378
444
 
379
445
  <div className="section-header">
380
446
  <h2 className="section-title">Sessions</h2>
381
- <button className="btn btn-sm" onClick={() => onNavigate('create')}>
382
- {Icons.plus} New
383
- </button>
447
+ <button className="btn btn-sm" onClick={() => onNavigate('create')}>{Icons.plus} New</button>
384
448
  </div>
385
449
 
386
450
  {sessions.length === 0 ? (
@@ -401,14 +465,8 @@ function DashboardScreen({ onNavigate, sessions, activity, serverStatus }) {
401
465
  <StatusPill status={s.status} />
402
466
  </div>
403
467
  <div className="session-card-body">
404
- <div className="session-card-field">
405
- <div className="session-card-field-label">Started</div>
406
- <div className="session-card-field-value">{relativeTime(s.startedAt)}</div>
407
- </div>
408
- <div className="session-card-field">
409
- <div className="session-card-field-label">Tokens</div>
410
- <div className="session-card-field-value">{formatTokens(s.tokens)}</div>
411
- </div>
468
+ <div className="session-card-field"><div className="session-card-field-label">Started</div><div className="session-card-field-value">{relativeTime(s.startedAt)}</div></div>
469
+ <div className="session-card-field"><div className="session-card-field-label">Tokens</div><div className="session-card-field-value">{formatTokens(s.tokens)}</div></div>
412
470
  </div>
413
471
  </div>
414
472
  ))}
@@ -417,17 +475,13 @@ function DashboardScreen({ onNavigate, sessions, activity, serverStatus }) {
417
475
 
418
476
  {activity.length > 0 && (
419
477
  <div style={{ marginTop: 24 }}>
420
- <div className="section-header">
421
- <h2 className="section-title">Recent Activity</h2>
422
- </div>
478
+ <div className="section-header"><h2 className="section-title">Recent Activity</h2></div>
423
479
  <div className="card" style={{ padding: '14px 18px' }}>
424
480
  <ul className="activity-list">
425
481
  {activity.map((a, i) => (
426
482
  <li key={i} className="activity-item">
427
483
  <span className="activity-time">{relativeTime(a.time)}</span>
428
- <span className="activity-text">
429
- <strong>{a.name}</strong> — {a.from} → {a.to}
430
- </span>
484
+ <span className="activity-text"><strong>{a.name}</strong> — {a.from} → {a.to}</span>
431
485
  </li>
432
486
  ))}
433
487
  </ul>
@@ -438,18 +492,26 @@ function DashboardScreen({ onNavigate, sessions, activity, serverStatus }) {
438
492
  );
439
493
  }
440
494
 
441
- /* --- Session Detail Screen --- */
495
+ /* --- Session Detail --- */
442
496
  function SessionDetailScreen({ session, onBack, onKilled }) {
443
497
  const [output, setOutput] = useState('');
444
498
  const [msg, setMsg] = useState('');
445
499
  const [sending, setSending] = useState(false);
446
500
  const [killing, setKilling] = useState(false);
447
501
  const terminalRef = useRef(null);
502
+ const inputRef = useRef(null);
503
+ const isOffline = session.status === 'offline';
504
+
505
+ // Auto-focus input on mount and when session changes
506
+ useEffect(() => {
507
+ if (!isOffline) setTimeout(() => inputRef.current?.focus(), 50);
508
+ }, [session.name, isOffline]);
448
509
 
510
+ // Poll terminal output every 2s
449
511
  useEffect(() => {
450
- if (session.status === 'offline') return;
512
+ if (isOffline) return;
451
513
  const poll = () => {
452
- fetch(`/api/sessions/${encodeURIComponent(session.name)}/output`)
514
+ apiFetch(`/api/sessions/${encodeURIComponent(session.name)}/output`)
453
515
  .then(r => r.json())
454
516
  .then(d => setOutput(d.output || ''))
455
517
  .catch(() => {});
@@ -457,19 +519,18 @@ function SessionDetailScreen({ session, onBack, onKilled }) {
457
519
  poll();
458
520
  const t = setInterval(poll, 2000);
459
521
  return () => clearInterval(t);
460
- }, [session.name, session.status]);
522
+ }, [session.name, isOffline]);
461
523
 
524
+ // Auto-scroll terminal to bottom
462
525
  useEffect(() => {
463
- if (terminalRef.current) {
464
- terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
465
- }
526
+ if (terminalRef.current) terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
466
527
  }, [output]);
467
528
 
468
529
  const sendMessage = async () => {
469
- if (!msg.trim() || sending || session.status === 'offline') return;
530
+ if (!msg.trim() || sending || isOffline) return;
470
531
  setSending(true);
471
532
  try {
472
- await fetch(`/api/sessions/${encodeURIComponent(session.name)}/send`, {
533
+ await apiFetch(`/api/sessions/${encodeURIComponent(session.name)}/send`, {
473
534
  method: 'POST',
474
535
  headers: { 'Content-Type': 'application/json' },
475
536
  body: JSON.stringify({ message: msg }),
@@ -478,44 +539,45 @@ function SessionDetailScreen({ session, onBack, onKilled }) {
478
539
  } catch {
479
540
  } finally {
480
541
  setSending(false);
481
- }
482
- };
483
-
484
- const handleKeyDown = (e) => {
485
- if (e.key === 'Enter' && !e.shiftKey) {
486
- e.preventDefault();
487
- sendMessage();
542
+ inputRef.current?.focus();
488
543
  }
489
544
  };
490
545
 
491
546
  const sendKey = async (key) => {
492
547
  try {
493
- await fetch(`/api/sessions/${encodeURIComponent(session.name)}/send`, {
548
+ await apiFetch(`/api/sessions/${encodeURIComponent(session.name)}/send`, {
494
549
  method: 'POST',
495
550
  headers: { 'Content-Type': 'application/json' },
496
551
  body: JSON.stringify({ key }),
497
552
  });
498
553
  } catch {}
554
+ inputRef.current?.focus();
555
+ };
556
+
557
+ const handleKeyDown = (e) => {
558
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
499
559
  };
500
560
 
501
561
  const handleKill = async () => {
502
562
  if (!confirm(`End session "${session.name}"?`)) return;
503
563
  setKilling(true);
504
564
  try {
505
- await fetch(`/api/sessions/${encodeURIComponent(session.name)}`, { method: 'DELETE' });
565
+ await apiFetch(`/api/sessions/${encodeURIComponent(session.name)}`, { method: 'DELETE' });
506
566
  onKilled();
507
567
  } catch {
508
568
  setKilling(false);
509
569
  }
510
570
  };
511
571
 
512
- const isOffline = session.status === 'offline';
572
+ // Terminal height: fill viewport minus chrome
573
+ const terminalStyle = {
574
+ height: 'calc(100vh - 210px)',
575
+ minHeight: 360,
576
+ };
513
577
 
514
578
  return (
515
579
  <div>
516
- <button className="back-link" onClick={onBack}>
517
- {Icons.arrow} Back
518
- </button>
580
+ <button className="back-link" onClick={onBack}>{Icons.arrow} Back</button>
519
581
 
520
582
  <div className="detail-header">
521
583
  <div>
@@ -525,85 +587,65 @@ function SessionDetailScreen({ session, onBack, onKilled }) {
525
587
  <div className="detail-actions">
526
588
  {!isOffline && (
527
589
  <button className="btn btn-sm" style={{ color: 'var(--error)' }} onClick={handleKill} disabled={killing}>
528
- {Icons.trash} {killing ? 'Ending…' : 'End Session'}
590
+ {Icons.trash} {killing ? 'Ending…' : 'End'}
529
591
  </button>
530
592
  )}
531
593
  </div>
532
594
  </div>
533
595
 
534
596
  <div className="detail-grid">
535
- <div>
536
- <div className="terminal">
537
- <div className="terminal-header">
538
- <span className="terminal-dot" style={{ background: '#ff5f57' }} />
539
- <span className="terminal-dot" style={{ background: '#febc2e' }} />
540
- <span className="terminal-dot" style={{ background: '#28c840' }} />
541
- <span className="terminal-title">{session.name}</span>
542
- </div>
543
- <div className="terminal-body" ref={terminalRef}>
544
- {isOffline
545
- ? <span style={{ color: 'oklch(50% 0.018 50)' }}>Session is offline — no output available.</span>
546
- : (output || <span style={{ color: 'oklch(50% 0.018 50)' }}>Loading…</span>)
547
- }
548
- {!isOffline && output && <span style={{ opacity: 0.5 }}>▊</span>}
549
- </div>
597
+ {/* Terminal — fills available height, input pinned at bottom */}
598
+ <div className="terminal" style={terminalStyle}>
599
+ <div className="terminal-header">
600
+ <span className="terminal-dot" style={{ background: '#ff5f57' }} />
601
+ <span className="terminal-dot" style={{ background: '#febc2e' }} />
602
+ <span className="terminal-dot" style={{ background: '#28c840' }} />
603
+ <span className="terminal-title">{session.name}</span>
604
+ <span style={{ marginLeft: 'auto' }}><StatusPill status={session.status} /></span>
605
+ </div>
606
+
607
+ <div className="terminal-body" ref={terminalRef}>
608
+ {isOffline
609
+ ? <span style={{ color: 'oklch(50% 0.018 50)' }}>Session is offline — no output available.</span>
610
+ : output
611
+ ? <>{output}<span style={{ opacity: 0.4 }}>▊</span></>
612
+ : <span style={{ color: 'oklch(50% 0.018 50)' }}>Connecting…</span>
613
+ }
550
614
  </div>
551
615
 
552
616
  {!isOffline && (
553
- <div style={{ marginTop: 16 }}>
554
- <div className="form-group" style={{ marginBottom: 0 }}>
555
- <label className="form-label">Send message to Claude</label>
556
- <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
557
- <input
558
- className="form-input"
559
- placeholder="Type a message…"
560
- value={msg}
561
- onChange={e => setMsg(e.target.value)}
562
- onKeyDown={handleKeyDown}
563
- style={{ flex: 1 }}
564
- disabled={sending}
565
- />
566
- <button className="btn btn-primary" onClick={sendMessage} disabled={sending || !msg.trim()}>
567
- {Icons.send} {sending ? 'Sending…' : 'Send'}
568
- </button>
569
- </div>
570
- <div style={{ display: 'flex', gap: 6 }}>
571
- <button className="btn btn-sm" onClick={() => sendKey('Escape')} title="Send Escape key">Esc</button>
572
- <button className="btn btn-sm" onClick={() => sendKey('C-c')} title="Send Ctrl+C">Ctrl+C</button>
573
- <button className="btn btn-sm" onClick={() => sendKey('C-d')} title="Send Ctrl+D">Ctrl+D</button>
574
- </div>
575
- </div>
617
+ <div className="terminal-footer">
618
+ <span className="terminal-prompt-char">❯</span>
619
+ <input
620
+ ref={inputRef}
621
+ className="terminal-input"
622
+ placeholder="send a message to Claude…"
623
+ value={msg}
624
+ onChange={e => setMsg(e.target.value)}
625
+ onKeyDown={handleKeyDown}
626
+ disabled={sending}
627
+ spellCheck={false}
628
+ autoComplete="off"
629
+ autoCorrect="off"
630
+ />
631
+ <button className="btn-key" onClick={() => sendKey('Escape')} title="Send Escape">Esc</button>
632
+ <button className="btn-key" onClick={() => sendKey('C-c')} title="Send Ctrl+C">^C</button>
633
+ <button className="btn-key" onClick={() => sendKey('C-d')} title="Send Ctrl+D">^D</button>
576
634
  </div>
577
635
  )}
578
636
  </div>
579
637
 
638
+ {/* Info sidebar */}
580
639
  <div className="detail-sidebar">
581
640
  <div className="card">
582
641
  <h3>Session Info</h3>
583
- <div className="info-row">
584
- <span className="info-label">Status</span>
585
- <StatusPill status={session.status} />
586
- </div>
587
- <div className="info-row">
588
- <span className="info-label">Started</span>
589
- <span className="info-value" style={{ fontSize: 11 }}>{relativeTime(session.startedAt)}</span>
590
- </div>
591
- <div className="info-row">
592
- <span className="info-label">Tokens</span>
593
- <span className="info-value" style={{ fontSize: 11 }}>{formatTokens(session.tokens)}</span>
594
- </div>
642
+ <div className="info-row"><span className="info-label">Status</span><StatusPill status={session.status} /></div>
643
+ <div className="info-row"><span className="info-label">Started</span><span className="info-value" style={{ fontSize: 11 }}>{relativeTime(session.startedAt)}</span></div>
644
+ <div className="info-row"><span className="info-label">Tokens</span><span className="info-value" style={{ fontSize: 11 }}>{formatTokens(session.tokens)}</span></div>
595
645
  {session.status === 'limit' && session.resumeAt && (
596
- <div className="info-row">
597
- <span className="info-label">Resumes</span>
598
- <span className="info-value" style={{ fontSize: 11, color: 'var(--warning)' }}>
599
- {relativeTime(session.resumeAt)}
600
- </span>
601
- </div>
646
+ <div className="info-row"><span className="info-label">Resumes</span><span className="info-value" style={{ fontSize: 11, color: 'var(--warning)' }}>{relativeTime(session.resumeAt)}</span></div>
602
647
  )}
603
- <div className="info-row">
604
- <span className="info-label">tmux</span>
605
- <span className="info-value" style={{ fontSize: 11 }}>{session.name}</span>
606
- </div>
648
+ <div className="info-row"><span className="info-label">tmux</span><span className="info-value" style={{ fontSize: 11 }}>{session.name}</span></div>
607
649
  </div>
608
650
  </div>
609
651
  </div>
@@ -611,7 +653,7 @@ function SessionDetailScreen({ session, onBack, onKilled }) {
611
653
  );
612
654
  }
613
655
 
614
- /* --- Create Session Screen --- */
656
+ /* --- Create Session --- */
615
657
  function CreateSessionScreen({ onBack, onCreated }) {
616
658
  const [name, setName] = useState('');
617
659
  const [path, setPath] = useState('');
@@ -620,14 +662,11 @@ function CreateSessionScreen({ onBack, onCreated }) {
620
662
  const [loading, setLoading] = useState(false);
621
663
 
622
664
  const handleCreate = async () => {
623
- if (!name.trim() || !path.trim()) {
624
- setError('Session name and working directory are required.');
625
- return;
626
- }
665
+ if (!name.trim() || !path.trim()) { setError('Session name and working directory are required.'); return; }
627
666
  setError('');
628
667
  setLoading(true);
629
668
  try {
630
- const res = await fetch('/api/sessions', {
669
+ const res = await apiFetch('/api/sessions', {
631
670
  method: 'POST',
632
671
  headers: { 'Content-Type': 'application/json' },
633
672
  body: JSON.stringify({ name: name.trim(), path: path.trim(), prompt: prompt.trim() || undefined }),
@@ -635,8 +674,8 @@ function CreateSessionScreen({ onBack, onCreated }) {
635
674
  const data = await res.json();
636
675
  if (!res.ok) { setError(data.error || 'Failed to create session.'); return; }
637
676
  onCreated(data);
638
- } catch {
639
- setError('Network error. Is the supervisor running?');
677
+ } catch (e) {
678
+ if (e.message !== 'Unauthorized') setError('Network error.');
640
679
  } finally {
641
680
  setLoading(false);
642
681
  }
@@ -644,10 +683,7 @@ function CreateSessionScreen({ onBack, onCreated }) {
644
683
 
645
684
  return (
646
685
  <div className="create-layout">
647
- <button className="back-link" onClick={onBack}>
648
- {Icons.arrow} Back
649
- </button>
650
-
686
+ <button className="back-link" onClick={onBack}>{Icons.arrow} Back</button>
651
687
  <h2 className="section-title" style={{ marginBottom: 20, fontSize: 18 }}>New Session</h2>
652
688
 
653
689
  <div className="form-group">
@@ -655,36 +691,25 @@ function CreateSessionScreen({ onBack, onCreated }) {
655
691
  <input className="form-input" placeholder="e.g. refactor-auth-flow" value={name} onChange={e => setName(e.target.value)} />
656
692
  <div className="form-hint">Used as the tmux session name. Lowercase with hyphens.</div>
657
693
  </div>
658
-
659
694
  <div className="form-group">
660
695
  <label className="form-label">Working directory</label>
661
696
  <input className="form-input" placeholder="~/projects/app" value={path} onChange={e => setPath(e.target.value)} />
662
- <div className="form-hint">Absolute path or ~ home-relative path where Claude will run.</div>
663
697
  </div>
664
-
665
698
  <div className="form-group">
666
699
  <label className="form-label">Initial prompt <span style={{ fontWeight: 400, color: 'var(--muted)' }}>(optional)</span></label>
667
700
  <textarea className="form-textarea" placeholder="What should Claude work on?" value={prompt} onChange={e => setPrompt(e.target.value)} rows={4} />
668
701
  <div className="form-hint">Sent to Claude 2 seconds after the session starts.</div>
669
702
  </div>
670
-
671
- {error && (
672
- <div style={{ marginBottom: 16, padding: '10px 14px', background: 'var(--error-soft)', color: 'var(--error)', borderRadius: 'var(--radius-sm)', fontSize: 13 }}>
673
- {error}
674
- </div>
675
- )}
676
-
703
+ {error && <div style={{ marginBottom: 16, padding: '10px 14px', background: 'var(--error-soft)', color: 'var(--error)', borderRadius: 'var(--radius-sm)', fontSize: 13 }}>{error}</div>}
677
704
  <div className="form-actions">
678
705
  <button className="btn" onClick={onBack}>Cancel</button>
679
- <button className="btn btn-primary" onClick={handleCreate} disabled={loading}>
680
- {loading ? 'Creating…' : 'Create & Start Session'}
681
- </button>
706
+ <button className="btn btn-primary" onClick={handleCreate} disabled={loading}>{loading ? 'Creating…' : 'Create & Start'}</button>
682
707
  </div>
683
708
  </div>
684
709
  );
685
710
  }
686
711
 
687
- /* --- Sessions List Screen --- */
712
+ /* --- Sessions List --- */
688
713
  function SessionsScreen({ sessions, onNavigate }) {
689
714
  return (
690
715
  <div>
@@ -692,34 +717,27 @@ function SessionsScreen({ sessions, onNavigate }) {
692
717
  <h2 className="section-title">All Sessions</h2>
693
718
  <button className="btn btn-sm" onClick={() => onNavigate('create')}>{Icons.plus} New</button>
694
719
  </div>
695
-
696
720
  {sessions.length === 0 ? (
697
- <div className="empty-state">
698
- <div className="empty-state-icon">⌘</div>
699
- <div className="empty-state-title">No sessions</div>
700
- <div className="empty-state-desc">Spawn a session to get started.</div>
701
- </div>
721
+ <div className="empty-state"><div className="empty-state-icon">⌘</div><div className="empty-state-title">No sessions</div></div>
702
722
  ) : (
703
723
  <div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)' }}>
704
724
  <table style={{ width: '100%', borderCollapse: 'collapse' }}>
705
725
  <thead>
706
- <tr>
707
- {['Name', 'Path', 'Status', 'Started', 'Tokens'].map(h => (
708
- <th key={h} style={{ textAlign: 'left', padding: '10px 14px', fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--muted)', borderBottom: '1px solid var(--border)' }}>{h}</th>
709
- ))}
710
- </tr>
726
+ <tr>{['Name','Path','Status','Started','Tokens'].map(h => (
727
+ <th key={h} style={{ textAlign:'left',padding:'10px 14px',fontSize:11,fontWeight:600,textTransform:'uppercase',letterSpacing:'0.05em',color:'var(--muted)',borderBottom:'1px solid var(--border)' }}>{h}</th>
728
+ ))}</tr>
711
729
  </thead>
712
730
  <tbody>
713
731
  {sessions.map(s => (
714
- <tr key={s.id} style={{ cursor: 'pointer', opacity: s.status === 'offline' ? 0.6 : 1, transition: 'background 0.12s' }}
732
+ <tr key={s.id} style={{ cursor:'pointer',opacity:s.status==='offline'?0.6:1,transition:'background 0.12s' }}
715
733
  onClick={() => onNavigate('detail', s)}
716
- onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-hover)'}
717
- onMouseLeave={e => e.currentTarget.style.background = ''}>
718
- <td style={{ padding: '12px 14px', fontWeight: 600, fontSize: 13, borderBottom: '1px solid var(--border)' }}>{s.name}</td>
719
- <td style={{ padding: '12px 14px', fontSize: 12, color: 'var(--muted)', borderBottom: '1px solid var(--border)', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{s.path}</td>
720
- <td style={{ padding: '12px 14px', borderBottom: '1px solid var(--border)' }}><StatusPill status={s.status} /></td>
721
- <td style={{ padding: '12px 14px', fontSize: 12, color: 'var(--muted)', fontFamily: 'var(--font-mono)', borderBottom: '1px solid var(--border)' }}>{relativeTime(s.startedAt)}</td>
722
- <td style={{ padding: '12px 14px', fontSize: 12, fontFamily: 'var(--font-mono)', color: 'var(--fg-secondary)', borderBottom: '1px solid var(--border)' }}>{formatTokens(s.tokens)}</td>
734
+ onMouseEnter={e => e.currentTarget.style.background='var(--surface-hover)'}
735
+ onMouseLeave={e => e.currentTarget.style.background=''}>
736
+ <td style={{ padding:'12px 14px',fontWeight:600,fontSize:13,borderBottom:'1px solid var(--border)' }}>{s.name}</td>
737
+ <td style={{ padding:'12px 14px',fontSize:12,color:'var(--muted)',borderBottom:'1px solid var(--border)',maxWidth:200,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap' }}>{s.path}</td>
738
+ <td style={{ padding:'12px 14px',borderBottom:'1px solid var(--border)' }}><StatusPill status={s.status} /></td>
739
+ <td style={{ padding:'12px 14px',fontSize:12,color:'var(--muted)',fontFamily:'var(--font-mono)',borderBottom:'1px solid var(--border)' }}>{relativeTime(s.startedAt)}</td>
740
+ <td style={{ padding:'12px 14px',fontSize:12,fontFamily:'var(--font-mono)',color:'var(--fg-secondary)',borderBottom:'1px solid var(--border)' }}>{formatTokens(s.tokens)}</td>
723
741
  </tr>
724
742
  ))}
725
743
  </tbody>
@@ -741,127 +759,100 @@ function App() {
741
759
  const [serverStatus, setServerStatus] = useState(null);
742
760
  const [activity, setActivity] = useState([]);
743
761
  const [connected, setConnected] = useState(false);
762
+ const [needsLogin, setNeedsLogin] = useState(false);
744
763
  const prevStatusRef = useRef({});
764
+ const esRef = useRef(null);
745
765
 
746
766
  useEffect(() => {
747
767
  document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
748
768
  storage.setItem('ccp-theme', dark ? 'dark' : 'light');
749
769
  }, [dark]);
750
770
 
751
- // SSE for live sessions
771
+ // Wire up unauth callback
752
772
  useEffect(() => {
753
- let es;
754
- let retryTimer;
755
- const connect = () => {
756
- es = new EventSource('/events');
757
- es.onopen = () => setConnected(true);
758
- es.onerror = () => {
759
- setConnected(false);
760
- es.close();
761
- retryTimer = setTimeout(connect, 5000);
762
- };
763
- es.onmessage = (e) => {
764
- const incoming = JSON.parse(e.data);
765
- setSessions(incoming);
766
-
767
- const now = new Date();
768
- const prev = prevStatusRef.current;
769
- const newEntries = [];
770
- for (const s of incoming) {
771
- if (prev[s.name] && prev[s.name] !== s.status) {
772
- newEntries.push({ time: now, name: s.name, from: prev[s.name], to: s.status });
773
- }
774
- prev[s.name] = s.status;
775
- }
776
- if (newEntries.length) {
777
- setActivity(a => [...newEntries, ...a].slice(0, 20));
778
- }
779
-
780
- // Update selected session data if viewing detail
781
- setSelectedSession(sel => {
782
- if (!sel) return sel;
783
- const updated = incoming.find(s => s.name === sel.name);
784
- return updated || sel;
785
- });
786
- };
773
+ _onUnauth = () => setNeedsLogin(true);
774
+ return () => { _onUnauth = () => {}; };
775
+ }, []);
776
+
777
+ const connectSSE = useCallback(() => {
778
+ if (esRef.current) esRef.current.close();
779
+ const es = new EventSource(sseUrl());
780
+ esRef.current = es;
781
+ es.onopen = () => setConnected(true);
782
+ es.onerror = () => {
783
+ setConnected(false);
784
+ es.close();
785
+ setTimeout(connectSSE, 5000);
786
+ };
787
+ es.onmessage = (e) => {
788
+ const incoming = JSON.parse(e.data);
789
+ setSessions(incoming);
790
+ const now = new Date();
791
+ const prev = prevStatusRef.current;
792
+ const newEntries = [];
793
+ for (const s of incoming) {
794
+ if (prev[s.name] && prev[s.name] !== s.status)
795
+ newEntries.push({ time: now, name: s.name, from: prev[s.name], to: s.status });
796
+ prev[s.name] = s.status;
797
+ }
798
+ if (newEntries.length) setActivity(a => [...newEntries, ...a].slice(0, 20));
799
+ setSelectedSession(sel => {
800
+ if (!sel) return sel;
801
+ return incoming.find(s => s.name === sel.name) || sel;
802
+ });
787
803
  };
788
- connect();
789
- return () => { clearTimeout(retryTimer); if (es) es.close(); };
790
804
  }, []);
791
805
 
792
- // Server status
793
806
  useEffect(() => {
794
- fetch('/api/status').then(r => r.json()).then(setServerStatus).catch(() => {});
807
+ // Check auth / fetch status
808
+ apiFetch('/api/status')
809
+ .then(r => r.json())
810
+ .then(data => { setServerStatus(data); setNeedsLogin(false); connectSSE(); })
811
+ .catch(e => { if (e.message !== 'Unauthorized') connectSSE(); });
812
+ return () => { if (esRef.current) esRef.current.close(); };
795
813
  }, []);
796
814
 
815
+ const handleLogin = useCallback(() => {
816
+ setNeedsLogin(false);
817
+ apiFetch('/api/status').then(r => r.json()).then(setServerStatus).catch(() => {});
818
+ connectSSE();
819
+ }, [connectSSE]);
820
+
797
821
  const navigate = useCallback((target, session) => {
798
822
  setScreen(target);
799
823
  if (session) setSelectedSession(session);
800
824
  setSidebarOpen(false);
801
825
  }, []);
802
826
 
803
- const screenTitle = {
804
- dashboard: 'Dashboard',
805
- sessions: 'Sessions',
806
- create: 'New Session',
807
- detail: selectedSession ? selectedSession.name : 'Session',
808
- }[screen] || 'Dashboard';
827
+ if (needsLogin) return <LoginScreen onLogin={handleLogin} />;
828
+
829
+ const screenTitle = { dashboard: 'Dashboard', sessions: 'Sessions', create: 'New Session', detail: selectedSession?.name || 'Session' }[screen] || 'Dashboard';
809
830
 
810
831
  const renderScreen = () => {
811
832
  switch (screen) {
812
- case 'dashboard':
813
- return <DashboardScreen onNavigate={navigate} sessions={sessions} activity={activity} serverStatus={serverStatus} />;
814
- case 'sessions':
815
- return <SessionsScreen sessions={sessions} onNavigate={navigate} />;
816
- case 'create':
817
- return (
818
- <CreateSessionScreen
819
- onBack={() => navigate('dashboard')}
820
- onCreated={(s) => navigate('detail', s)}
821
- />
822
- );
823
- case 'detail':
824
- return (
825
- <SessionDetailScreen
826
- session={selectedSession}
827
- onBack={() => navigate('dashboard')}
828
- onKilled={() => navigate('sessions')}
829
- />
830
- );
831
- default:
832
- return <DashboardScreen onNavigate={navigate} sessions={sessions} activity={activity} serverStatus={serverStatus} />;
833
+ case 'dashboard': return <DashboardScreen onNavigate={navigate} sessions={sessions} activity={activity} serverStatus={serverStatus} />;
834
+ case 'sessions': return <SessionsScreen sessions={sessions} onNavigate={navigate} />;
835
+ case 'create': return <CreateSessionScreen onBack={() => navigate('dashboard')} onCreated={s => navigate('detail', s)} />;
836
+ case 'detail': return <SessionDetailScreen session={selectedSession} onBack={() => navigate('dashboard')} onKilled={() => navigate('sessions')} />;
837
+ default: return <DashboardScreen onNavigate={navigate} sessions={sessions} activity={activity} serverStatus={serverStatus} />;
833
838
  }
834
839
  };
835
840
 
836
841
  return (
837
842
  <div className="app-shell">
838
- <Sidebar
839
- currentScreen={screen}
840
- onNavigate={navigate}
841
- sessionCount={sessions.length}
842
- open={sidebarOpen}
843
- onClose={() => setSidebarOpen(false)}
844
- connected={connected}
845
- />
843
+ <Sidebar currentScreen={screen} onNavigate={navigate} sessionCount={sessions.length} open={sidebarOpen} onClose={() => setSidebarOpen(false)} connected={connected} />
846
844
  <div className="main">
847
845
  <div className="mobile-header">
848
846
  <button className="menu-btn" onClick={() => setSidebarOpen(true)}>{Icons.menu}</button>
849
847
  <span className="mobile-title">{screenTitle}</span>
850
- <button className="theme-toggle" onClick={() => setDark(d => !d)}>
851
- {dark ? Icons.sun : Icons.moon}
852
- </button>
848
+ <button className="theme-toggle" onClick={() => setDark(d => !d)}>{dark ? Icons.sun : Icons.moon}</button>
853
849
  </div>
854
850
  <header className="header">
855
851
  <h1 className="header-title">{screenTitle}</h1>
856
852
  <div className="header-spacer" />
857
- <button className="theme-toggle" onClick={() => setDark(d => !d)}>
858
- {dark ? Icons.sun : Icons.moon}
859
- {dark ? 'Light' : 'Dark'}
860
- </button>
853
+ <button className="theme-toggle" onClick={() => setDark(d => !d)}>{dark ? Icons.sun : Icons.moon} {dark ? 'Light' : 'Dark'}</button>
861
854
  </header>
862
- <div className="content">
863
- {renderScreen()}
864
- </div>
855
+ <div className="content">{renderScreen()}</div>
865
856
  </div>
866
857
  </div>
867
858
  );