claude-code-remote-pilot 0.4.3 → 0.4.5
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/CHANGELOG.md +16 -0
- package/bin/claude-pilot.js +17 -3
- package/lib/WebServer.js +30 -3
- package/lib/ui.html +412 -421
- package/package.json +1 -1
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.sidebar
|
|
84
|
-
.sidebar
|
|
85
|
-
.sidebar-overlay
|
|
86
|
-
.sidebar-
|
|
87
|
-
.sidebar-
|
|
88
|
-
.
|
|
89
|
-
.logo
|
|
90
|
-
.logo-
|
|
91
|
-
.logo-
|
|
92
|
-
.
|
|
93
|
-
.
|
|
94
|
-
.nav-
|
|
95
|
-
.nav-item
|
|
96
|
-
.nav-item
|
|
97
|
-
.nav-
|
|
98
|
-
.nav-icon
|
|
99
|
-
.nav-
|
|
100
|
-
.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
.
|
|
104
|
-
.
|
|
105
|
-
.
|
|
106
|
-
.
|
|
107
|
-
.
|
|
108
|
-
.header-
|
|
109
|
-
.
|
|
110
|
-
.theme-toggle
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.session-
|
|
116
|
-
.session-card
|
|
117
|
-
.session-card
|
|
118
|
-
.session-card
|
|
119
|
-
.session-card-
|
|
120
|
-
.session-card-
|
|
121
|
-
.session-card-
|
|
122
|
-
.session-card-
|
|
123
|
-
.session-card-field
|
|
124
|
-
.session-card-field-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
.status-
|
|
128
|
-
.status-
|
|
129
|
-
.status-running {
|
|
130
|
-
.status-
|
|
131
|
-
.status-idle {
|
|
132
|
-
.status-
|
|
133
|
-
.status-error {
|
|
134
|
-
.status-
|
|
135
|
-
.status-warn {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.btn
|
|
141
|
-
.btn:
|
|
142
|
-
.btn:
|
|
143
|
-
.btn-primary
|
|
144
|
-
.btn-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
.terminal
|
|
148
|
-
.terminal-header
|
|
149
|
-
.terminal-dot
|
|
150
|
-
.terminal-title
|
|
151
|
-
.terminal-body
|
|
152
|
-
|
|
153
|
-
.
|
|
154
|
-
.
|
|
155
|
-
.
|
|
156
|
-
.
|
|
157
|
-
.
|
|
158
|
-
|
|
159
|
-
.form-
|
|
160
|
-
|
|
161
|
-
.
|
|
162
|
-
.
|
|
163
|
-
.
|
|
164
|
-
.
|
|
165
|
-
.
|
|
166
|
-
|
|
167
|
-
.
|
|
168
|
-
.
|
|
169
|
-
.
|
|
170
|
-
.
|
|
171
|
-
.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
.
|
|
175
|
-
.
|
|
176
|
-
|
|
177
|
-
.
|
|
178
|
-
.
|
|
179
|
-
|
|
180
|
-
.
|
|
181
|
-
.
|
|
182
|
-
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.detail-
|
|
187
|
-
.detail-
|
|
188
|
-
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
.
|
|
192
|
-
|
|
193
|
-
.
|
|
194
|
-
|
|
195
|
-
.
|
|
196
|
-
.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
.
|
|
201
|
-
.
|
|
202
|
-
.
|
|
203
|
-
|
|
204
|
-
.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
.
|
|
208
|
-
.
|
|
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
|
|
212
|
-
.sidebar
|
|
213
|
-
.sidebar-close,.sidebar-overlay
|
|
214
|
-
.mobile-header
|
|
215
|
-
.header
|
|
216
|
-
.content
|
|
217
|
-
.session-cards
|
|
218
|
-
.stat-row
|
|
219
|
-
.detail-grid
|
|
220
|
-
.detail-header
|
|
221
|
-
.detail-title
|
|
222
|
-
.detail-actions
|
|
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
|
|
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
|
-
|
|
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(
|
|
276
|
-
|
|
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
|
-
|
|
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 ${
|
|
385
|
+
<span className={`status-pill ${statusCls(status)}`} style={status === 'offline' ? { opacity: 0.55 } : {}}>
|
|
304
386
|
<span className="status-dot" />
|
|
305
|
-
{
|
|
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.
|
|
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:
|
|
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' : '
|
|
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
|
|
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 :
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
512
|
+
if (isOffline) return;
|
|
451
513
|
const poll = () => {
|
|
452
|
-
|
|
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,
|
|
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 ||
|
|
530
|
+
if (!msg.trim() || sending || isOffline) return;
|
|
470
531
|
setSending(true);
|
|
471
532
|
try {
|
|
472
|
-
await
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
|
|
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
|
|
554
|
-
<
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
585
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
{
|
|
708
|
-
|
|
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:
|
|
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
|
|
717
|
-
onMouseLeave={e => e.currentTarget.style.background
|
|
718
|
-
<td style={{ padding:
|
|
719
|
-
<td style={{ padding:
|
|
720
|
-
<td style={{ padding:
|
|
721
|
-
<td style={{ padding:
|
|
722
|
-
<td style={{ padding:
|
|
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
|
-
//
|
|
771
|
+
// Wire up unauth callback
|
|
752
772
|
useEffect(() => {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
814
|
-
case '
|
|
815
|
-
|
|
816
|
-
|
|
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
|
);
|