@wangyaoshen/remux 0.3.8-dev.bab6c95 → 0.3.10-dev.19fb76c
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/.github/workflows/publish.yml +191 -17
- package/apps/ios/Remux.xcodeproj/project.pbxproj +21 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/Contents.json +23 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_120x120.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_152x152.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_167x167.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_180x180.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_20x20.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_29x29.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_40x40.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_58x58.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_60x60.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_76x76.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_80x80.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_87x87.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/Contents.json +6 -0
- package/apps/ios/Sources/Remux/RootView.swift +2 -2
- package/apps/ios/Sources/Remux/Views/Settings/MeView.swift +11 -4
- package/apps/macos/Package.swift +5 -0
- package/apps/macos/Sources/Remux/AppCommand.swift +114 -0
- package/apps/macos/Sources/Remux/AppDelegate.swift +26 -0
- package/apps/macos/Sources/Remux/MainContentView.swift +56 -0
- package/apps/macos/Sources/Remux/MenuBarManager.swift +18 -26
- package/apps/macos/Sources/Remux/NotificationManager.swift +52 -7
- package/apps/macos/Sources/Remux/Views/ConnectionView.swift +4 -8
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitView.swift +1 -1
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeView.swift +10 -4
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalContainerView.swift +35 -5
- package/apps/macos/Sources/Remux/WindowObserver.swift +38 -0
- package/apps/macos/Tests/RemuxTests/AppCommandTests.swift +30 -0
- package/apps/macos/Tests/RemuxTests/NotificationManagerTests.swift +28 -0
- package/package.json +1 -1
- package/packages/RemuxKit/Sources/RemuxKit/Models/ProtocolModels.swift +64 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/MessageRouter.swift +88 -9
- package/packages/RemuxKit/Sources/RemuxKit/Networking/RemuxConnection.swift +47 -8
- package/packages/RemuxKit/Sources/RemuxKit/State/RemuxState.swift +81 -8
- package/packages/RemuxKit/Sources/RemuxKit/Storage/KeychainStore.swift +20 -1
- package/packages/RemuxKit/Tests/RemuxKitTests/ConnectionIntegrationTest.swift +16 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/KeychainStoreTests.swift +26 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ProtocolModelsTests.swift +41 -7
- package/packages/RemuxKit/Tests/RemuxKitTests/RemuxStateTests.swift +20 -2
- package/pty-daemon.js +17 -11
- package/scripts/setup-ci-secrets.sh +80 -0
- package/scripts/upload-testflight.sh +100 -0
- package/server.js +146 -872
- package/src/pty-daemon.ts +17 -11
- package/src/server.ts +96 -859
- package/src/session.ts +42 -4
- package/tests/auth.test.js +1 -1
- package/tests/e2e/app.spec.js +44 -4
- package/tests/pty-daemon.test.js +20 -1
- package/tests/server.test.js +50 -12
- package/vitest.config.js +1 -0
package/src/server.ts
CHANGED
|
@@ -172,7 +172,7 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
172
172
|
<html lang="en" data-theme="dark">
|
|
173
173
|
<head>
|
|
174
174
|
<meta charset="UTF-8" />
|
|
175
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0,
|
|
175
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
176
176
|
<title>Remux</title>
|
|
177
177
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⬛</text></svg>">
|
|
178
178
|
<style>
|
|
@@ -241,6 +241,17 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
241
241
|
font-size: 18px; line-height: 1; padding: 2px 6px; border-radius: 4px; }
|
|
242
242
|
.sidebar-header button:hover { color: var(--text-bright); background: var(--compose-bg); }
|
|
243
243
|
|
|
244
|
+
.session-composer { display: none; padding: 0 6px 8px; gap: 6px; }
|
|
245
|
+
.session-composer.visible { display: flex; }
|
|
246
|
+
.session-composer input { flex: 1; min-width: 0; padding: 6px 8px; font-size: 12px; font-family: inherit;
|
|
247
|
+
background: var(--compose-bg); border: 1px solid var(--compose-border); border-radius: 4px;
|
|
248
|
+
color: var(--text-bright); outline: none; }
|
|
249
|
+
.session-composer input:focus { border-color: var(--accent); }
|
|
250
|
+
.session-composer button { padding: 6px 10px; font-size: 11px; font-family: inherit;
|
|
251
|
+
border-radius: 4px; border: 1px solid var(--compose-border); cursor: pointer; }
|
|
252
|
+
.session-composer button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
253
|
+
.session-composer button.secondary { background: var(--compose-bg); color: var(--text-bright); }
|
|
254
|
+
|
|
244
255
|
.session-list { flex: 1; overflow-y: auto; padding: 4px 6px; }
|
|
245
256
|
.session-item { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 4px;
|
|
246
257
|
font-size: 13px; cursor: pointer; color: var(--text); border: none; background: none;
|
|
@@ -256,9 +267,8 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
256
267
|
.session-item .del:hover { color: var(--dot-err); background: var(--compose-bg); }
|
|
257
268
|
|
|
258
269
|
.sidebar-footer { padding: 8px 12px; border-top: 1px solid var(--border);
|
|
259
|
-
display: flex;
|
|
270
|
+
display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
260
271
|
.sidebar-footer .version { font-size: 10px; color: var(--text-dim); }
|
|
261
|
-
.sidebar-footer .footer-row { display: flex; align-items: center; gap: 8px; }
|
|
262
272
|
.sidebar-footer .status { font-size: 11px; color: var(--text-muted); display: flex; align-items: center; gap: 6px; }
|
|
263
273
|
.status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-muted); flex-shrink: 0; }
|
|
264
274
|
.status-dot.connected { background: var(--dot-ok); }
|
|
@@ -280,6 +290,9 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
280
290
|
|
|
281
291
|
/* -- Main -- */
|
|
282
292
|
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
293
|
+
.main-toolbar { display: flex; align-items: center; gap: 8px; padding: 6px 10px;
|
|
294
|
+
border-bottom: 1px solid var(--border); background: var(--bg); min-height: 40px; }
|
|
295
|
+
.main-toolbar .toolbar-spacer { flex: 1; }
|
|
283
296
|
|
|
284
297
|
/* -- Tab bar (Chrome-style) -- */
|
|
285
298
|
.tab-bar { background: var(--bg-tab-bar); display: flex; align-items: flex-end; flex-shrink: 0;
|
|
@@ -334,161 +347,6 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
334
347
|
padding: 12px 16px; -webkit-overflow-scrolling: touch; }
|
|
335
348
|
#inspect.visible { display: block; }
|
|
336
349
|
|
|
337
|
-
/* -- Workspace -- */
|
|
338
|
-
#workspace { flex: 1; background: var(--bg); overflow: auto; display: none;
|
|
339
|
-
padding: 12px 16px; -webkit-overflow-scrolling: touch; }
|
|
340
|
-
#workspace.visible { display: block; }
|
|
341
|
-
.ws-section { margin-bottom: 16px; }
|
|
342
|
-
.ws-section-title { font-size: 12px; font-weight: 600; color: var(--text-muted);
|
|
343
|
-
text-transform: uppercase; letter-spacing: .5px; margin-bottom: 8px;
|
|
344
|
-
display: flex; align-items: center; justify-content: space-between; }
|
|
345
|
-
.ws-section-title button { background: none; border: 1px solid var(--border);
|
|
346
|
-
color: var(--text-muted); font-size: 11px; padding: 2px 8px; border-radius: 4px;
|
|
347
|
-
cursor: pointer; font-family: inherit; }
|
|
348
|
-
.ws-section-title button:hover { color: var(--text-bright); border-color: var(--text-muted); }
|
|
349
|
-
.ws-empty { font-size: 12px; color: var(--text-dim); padding: 8px 0; }
|
|
350
|
-
.ws-card { background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: 6px;
|
|
351
|
-
padding: 8px 12px; margin-bottom: 6px; }
|
|
352
|
-
.ws-card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
|
353
|
-
.ws-card-title { font-size: 13px; color: var(--text-bright); font-weight: 500; flex: 1;
|
|
354
|
-
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
355
|
-
.ws-card-meta { font-size: 10px; color: var(--text-dim); }
|
|
356
|
-
.ws-card-desc { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
|
|
357
|
-
.ws-badge { display: inline-block; font-size: 10px; padding: 1px 6px; border-radius: 8px;
|
|
358
|
-
font-weight: 500; }
|
|
359
|
-
.ws-badge.running { background: #1a3a5c; color: #4da6ff; }
|
|
360
|
-
.ws-badge.completed { background: #1a3c1a; color: #4dff4d; }
|
|
361
|
-
.ws-badge.failed { background: #3c1a1a; color: #ff4d4d; }
|
|
362
|
-
.ws-badge.pending { background: #3c3a1a; color: #ffbd2e; }
|
|
363
|
-
.ws-badge.approved { background: #1a3c1a; color: #4dff4d; }
|
|
364
|
-
.ws-badge.rejected { background: #3c1a1a; color: #ff4d4d; }
|
|
365
|
-
.ws-badge.snapshot { background: #1a2a3c; color: #88bbdd; }
|
|
366
|
-
.ws-badge.command-card { background: #2a1a3c; color: #bb88dd; }
|
|
367
|
-
.ws-badge.note { background: #1a3c2a; color: #88ddbb; }
|
|
368
|
-
.ws-badge.diff { background: #2a2a1a; color: #ddbb55; }
|
|
369
|
-
.ws-badge.markdown { background: #1a2a2a; color: #55bbdd; }
|
|
370
|
-
.ws-badge.ansi { background: #2a1a2a; color: #dd88bb; }
|
|
371
|
-
.ws-card-actions { display: flex; gap: 4px; margin-top: 6px; }
|
|
372
|
-
.ws-card-actions button { background: none; border: 1px solid var(--border);
|
|
373
|
-
color: var(--text-muted); font-size: 11px; padding: 3px 10px; border-radius: 4px;
|
|
374
|
-
cursor: pointer; font-family: inherit; }
|
|
375
|
-
.ws-card-actions button:hover { color: var(--text-bright); border-color: var(--text-muted); }
|
|
376
|
-
.ws-card-actions button.approve { border-color: #27c93f; color: #27c93f; }
|
|
377
|
-
.ws-card-actions button.approve:hover { background: #27c93f22; }
|
|
378
|
-
.ws-card-actions button.reject { border-color: #ff5f56; color: #ff5f56; }
|
|
379
|
-
.ws-card-actions button.reject:hover { background: #ff5f5622; }
|
|
380
|
-
.ws-card .del-topic { opacity: 0; background: none; border: none; color: var(--text-dim);
|
|
381
|
-
cursor: pointer; font-size: 14px; padding: 0 4px; font-family: inherit; border-radius: 3px; }
|
|
382
|
-
.ws-card:hover .del-topic { opacity: 1; }
|
|
383
|
-
.ws-card .del-topic:hover { color: var(--dot-err); }
|
|
384
|
-
|
|
385
|
-
/* -- Search bar -- */
|
|
386
|
-
.ws-search { display: flex; gap: 8px; margin-bottom: 16px; }
|
|
387
|
-
.ws-search input { flex: 1; padding: 6px 10px; font-size: 13px; font-family: inherit;
|
|
388
|
-
background: var(--compose-bg); border: 1px solid var(--compose-border); border-radius: 6px;
|
|
389
|
-
color: var(--text-bright); outline: none; }
|
|
390
|
-
.ws-search input:focus { border-color: var(--accent); }
|
|
391
|
-
.ws-search input::placeholder { color: var(--text-dim); }
|
|
392
|
-
.ws-search-results { margin-bottom: 12px; }
|
|
393
|
-
.ws-search-result { padding: 6px 10px; margin-bottom: 4px; background: var(--bg-sidebar);
|
|
394
|
-
border: 1px solid var(--border); border-radius: 4px; cursor: pointer; }
|
|
395
|
-
.ws-search-result:hover { border-color: var(--accent); }
|
|
396
|
-
.ws-search-result .sr-type { font-size: 10px; color: var(--text-dim); text-transform: uppercase; }
|
|
397
|
-
.ws-search-result .sr-title { font-size: 12px; color: var(--text-bright); }
|
|
398
|
-
|
|
399
|
-
/* -- Notes -- */
|
|
400
|
-
.ws-note { background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: 6px;
|
|
401
|
-
padding: 8px 12px; margin-bottom: 6px; position: relative; }
|
|
402
|
-
.ws-note.pinned { border-color: var(--accent); }
|
|
403
|
-
.ws-note-content { font-size: 12px; color: var(--text-bright); white-space: pre-wrap; word-break: break-word; }
|
|
404
|
-
.ws-note-actions { display: flex; gap: 4px; margin-top: 4px; }
|
|
405
|
-
.ws-note-actions button { background: none; border: none; color: var(--text-dim);
|
|
406
|
-
font-size: 11px; cursor: pointer; padding: 2px 6px; border-radius: 3px; font-family: inherit; }
|
|
407
|
-
.ws-note-actions button:hover { color: var(--text-bright); background: var(--bg-hover); }
|
|
408
|
-
.ws-note-input { display: flex; gap: 6px; margin-bottom: 8px; }
|
|
409
|
-
.ws-note-input input { flex: 1; padding: 6px 10px; font-size: 12px; font-family: inherit;
|
|
410
|
-
background: var(--compose-bg); border: 1px solid var(--compose-border); border-radius: 4px;
|
|
411
|
-
color: var(--text-bright); outline: none; }
|
|
412
|
-
.ws-note-input input:focus { border-color: var(--accent); }
|
|
413
|
-
.ws-note-input button { padding: 4px 12px; font-size: 12px; font-family: inherit;
|
|
414
|
-
background: var(--accent); color: #fff; border: none; border-radius: 4px; cursor: pointer; }
|
|
415
|
-
|
|
416
|
-
/* -- Commands -- */
|
|
417
|
-
.ws-cmd { background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: 6px;
|
|
418
|
-
padding: 6px 12px; margin-bottom: 4px; display: flex; align-items: center; gap: 8px; }
|
|
419
|
-
.ws-cmd-text { font-size: 12px; color: var(--text-bright); font-family: 'Menlo','Monaco',monospace;
|
|
420
|
-
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
421
|
-
.ws-cmd-exit { font-size: 11px; font-weight: 600; }
|
|
422
|
-
.ws-cmd-exit.ok { color: #27c93f; }
|
|
423
|
-
.ws-cmd-exit.err { color: #ff5f56; }
|
|
424
|
-
.ws-cmd-meta { font-size: 10px; color: var(--text-dim); white-space: nowrap; }
|
|
425
|
-
|
|
426
|
-
/* -- Handoff -- */
|
|
427
|
-
.ws-handoff { background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: 6px;
|
|
428
|
-
padding: 12px; margin-bottom: 12px; display: none; }
|
|
429
|
-
.ws-handoff.visible { display: block; }
|
|
430
|
-
.ws-handoff-section { margin-bottom: 8px; }
|
|
431
|
-
.ws-handoff-label { font-size: 11px; color: var(--text-dim); text-transform: uppercase;
|
|
432
|
-
letter-spacing: .5px; margin-bottom: 4px; }
|
|
433
|
-
.ws-handoff-list { font-size: 12px; color: var(--text-muted); padding-left: 12px; }
|
|
434
|
-
.ws-handoff-list li { margin-bottom: 2px; }
|
|
435
|
-
|
|
436
|
-
/* -- Rich content rendering (diff, markdown, ANSI) -- */
|
|
437
|
-
.ws-card-content { margin-top: 6px; font-size: 12px; max-height: 200px; overflow: auto;
|
|
438
|
-
border-top: 1px solid var(--border); padding-top: 6px; }
|
|
439
|
-
.ws-card-content.expanded { max-height: none; }
|
|
440
|
-
.ws-card-toggle { font-size: 11px; color: var(--text-dim); background: none; border: none;
|
|
441
|
-
cursor: pointer; padding: 2px 6px; font-family: inherit; border-radius: 3px; }
|
|
442
|
-
.ws-card-toggle:hover { color: var(--text-bright); background: var(--bg-hover); }
|
|
443
|
-
|
|
444
|
-
/* Diff */
|
|
445
|
-
.diff-container { font-family: 'Menlo','Monaco','Courier New',monospace; font-size: 11px;
|
|
446
|
-
line-height: 1.5; overflow-x: auto; }
|
|
447
|
-
.diff-container > div { padding: 0 8px; white-space: pre; }
|
|
448
|
-
.diff-add { background: #1a3a1a; color: #4eff4e; }
|
|
449
|
-
.diff-del { background: #3a1a1a; color: #ff4e4e; }
|
|
450
|
-
.diff-hunk { color: #6a9eff; font-style: italic; }
|
|
451
|
-
.diff-header { color: #888; font-style: italic; }
|
|
452
|
-
.diff-ctx { color: var(--text-muted); }
|
|
453
|
-
.diff-line-num { display: inline-block; width: 32px; text-align: right; margin-right: 8px;
|
|
454
|
-
color: var(--text-dim); user-select: none; }
|
|
455
|
-
|
|
456
|
-
/* Markdown */
|
|
457
|
-
.rendered-md { font-size: 13px; line-height: 1.6; color: var(--text-bright); }
|
|
458
|
-
.rendered-md h1 { font-size: 18px; margin: 0.5em 0 0.3em; border-bottom: 1px solid var(--border); padding-bottom: 4px; }
|
|
459
|
-
.rendered-md h2 { font-size: 15px; margin: 0.5em 0 0.3em; }
|
|
460
|
-
.rendered-md h3 { font-size: 13px; margin: 0.5em 0 0.3em; font-weight: 600; }
|
|
461
|
-
.rendered-md p { margin: 0.4em 0; }
|
|
462
|
-
.rendered-md code { background: #2a2a2a; padding: 2px 6px; border-radius: 3px;
|
|
463
|
-
font-family: 'Menlo','Monaco',monospace; font-size: 11px; }
|
|
464
|
-
.rendered-md pre { background: #1e1e1e; padding: 12px; border-radius: 6px;
|
|
465
|
-
overflow-x: auto; margin: 0.4em 0; }
|
|
466
|
-
.rendered-md pre code { background: none; padding: 0; font-size: 11px; }
|
|
467
|
-
.rendered-md blockquote { border-left: 3px solid #555; padding-left: 12px; color: #aaa;
|
|
468
|
-
margin: 0.4em 0; }
|
|
469
|
-
.rendered-md ul, .rendered-md ol { padding-left: 20px; margin: 0.3em 0; }
|
|
470
|
-
.rendered-md li { margin: 0.15em 0; }
|
|
471
|
-
.rendered-md a { color: var(--accent); text-decoration: none; }
|
|
472
|
-
.rendered-md a:hover { text-decoration: underline; }
|
|
473
|
-
.rendered-md hr { border: none; border-top: 1px solid var(--border); margin: 0.5em 0; }
|
|
474
|
-
.rendered-md strong { color: var(--text-on-active); }
|
|
475
|
-
|
|
476
|
-
/* ANSI */
|
|
477
|
-
.ansi-bold { font-weight: bold; }
|
|
478
|
-
.ansi-dim { opacity: 0.6; }
|
|
479
|
-
.ansi-italic { font-style: italic; }
|
|
480
|
-
.ansi-underline { text-decoration: underline; }
|
|
481
|
-
|
|
482
|
-
/* Light theme overrides */
|
|
483
|
-
[data-theme="light"] .diff-add { background: #e6ffec; color: #1a7f37; }
|
|
484
|
-
[data-theme="light"] .diff-del { background: #ffebe9; color: #cf222e; }
|
|
485
|
-
[data-theme="light"] .diff-hunk { color: #0969da; }
|
|
486
|
-
[data-theme="light"] .diff-header { color: #6e7781; }
|
|
487
|
-
[data-theme="light"] .diff-ctx { color: #57606a; }
|
|
488
|
-
[data-theme="light"] .rendered-md code { background: #eee; }
|
|
489
|
-
[data-theme="light"] .rendered-md pre { background: #f6f8fa; }
|
|
490
|
-
[data-theme="light"] .rendered-md blockquote { border-left-color: #ccc; color: #666; }
|
|
491
|
-
|
|
492
350
|
#inspect-content { font-family: 'Menlo','Monaco','Courier New',monospace; font-size: 13px;
|
|
493
351
|
line-height: 1.5; color: var(--text-bright); white-space: pre-wrap; word-break: break-all;
|
|
494
352
|
tab-size: 8; user-select: text; -webkit-user-select: text; }
|
|
@@ -513,6 +371,7 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
513
371
|
.compose-bar { display: none; background: var(--bg-sidebar); border-top: 1px solid var(--border);
|
|
514
372
|
padding: 5px 8px; gap: 5px; flex-shrink: 0; overflow-x: auto; flex-wrap: wrap;
|
|
515
373
|
-webkit-overflow-scrolling: touch; }
|
|
374
|
+
body.touch-device .compose-bar.visible { display: flex; }
|
|
516
375
|
.compose-bar button { padding: 8px 12px; font-size: 14px;
|
|
517
376
|
font-family: 'Menlo','Monaco',monospace; color: var(--text-bright); background: var(--compose-bg);
|
|
518
377
|
border: 1px solid var(--compose-border); border-radius: 5px; cursor: pointer; white-space: nowrap;
|
|
@@ -520,66 +379,6 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
520
379
|
min-width: 40px; text-align: center; user-select: none; }
|
|
521
380
|
.compose-bar button:active { background: var(--compose-border); }
|
|
522
381
|
.compose-bar button.active { background: #4a6a9a; border-color: #6a9ade; }
|
|
523
|
-
@media (hover: none) and (pointer: coarse) { .compose-bar { display: flex; } }
|
|
524
|
-
|
|
525
|
-
/* -- Tab rename input -- */
|
|
526
|
-
.tab .rename-input { background: var(--bg); border: 1px solid var(--accent); border-radius: 3px;
|
|
527
|
-
color: var(--text-bright); font-size: 12px; font-family: inherit; padding: 1px 4px;
|
|
528
|
-
outline: none; width: 80px; }
|
|
529
|
-
|
|
530
|
-
/* -- Devices section -- */
|
|
531
|
-
.devices-section { border-top: 1px solid var(--border); }
|
|
532
|
-
.devices-header { padding: 8px 12px; font-size: 11px; font-weight: 600; color: var(--text-muted);
|
|
533
|
-
text-transform: uppercase; letter-spacing: .5px; cursor: pointer; display: flex;
|
|
534
|
-
align-items: center; justify-content: space-between; user-select: none; }
|
|
535
|
-
.devices-header:hover { color: var(--text-bright); }
|
|
536
|
-
.devices-toggle { font-size: 8px; transition: transform .2s; }
|
|
537
|
-
.devices-toggle.collapsed { transform: rotate(-90deg); }
|
|
538
|
-
.devices-list { padding: 2px 6px; max-height: 200px; overflow-y: auto; }
|
|
539
|
-
.devices-list.collapsed { display: none; }
|
|
540
|
-
.device-item { display: flex; align-items: center; gap: 6px; padding: 5px 8px; border-radius: 4px;
|
|
541
|
-
font-size: 12px; color: var(--text); }
|
|
542
|
-
.device-item:hover { background: var(--bg-hover); }
|
|
543
|
-
.device-item .device-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
|
544
|
-
.device-dot.trusted { background: var(--dot-ok); }
|
|
545
|
-
.device-dot.untrusted { background: var(--dot-warn); }
|
|
546
|
-
.device-dot.blocked { background: var(--dot-err); }
|
|
547
|
-
.device-item .device-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
548
|
-
.device-item .device-self { font-size: 9px; color: var(--accent); margin-left: 2px; }
|
|
549
|
-
.device-item .device-actions { display: flex; gap: 2px; opacity: 0; }
|
|
550
|
-
.device-item:hover .device-actions { opacity: 1; }
|
|
551
|
-
.device-actions button { background: none; border: none; color: var(--text-dim); cursor: pointer;
|
|
552
|
-
font-size: 11px; padding: 1px 4px; border-radius: 3px; font-family: inherit; }
|
|
553
|
-
.device-actions button:hover { color: var(--text-bright); background: var(--compose-bg); }
|
|
554
|
-
.devices-actions { padding: 4px 12px 8px; }
|
|
555
|
-
.pair-btn { width: 100%; padding: 5px 8px; font-size: 11px; font-family: inherit;
|
|
556
|
-
color: var(--text-bright); background: var(--compose-bg); border: 1px solid var(--compose-border);
|
|
557
|
-
border-radius: 4px; cursor: pointer; margin-bottom: 4px; }
|
|
558
|
-
.pair-btn:hover { background: var(--compose-border); }
|
|
559
|
-
.pair-code-display { text-align: center; padding: 6px; }
|
|
560
|
-
.pair-code { font-family: 'Menlo','Monaco',monospace; font-size: 24px; font-weight: bold;
|
|
561
|
-
color: var(--accent); letter-spacing: 4px; }
|
|
562
|
-
.pair-expires { display: block; font-size: 10px; color: var(--text-dim); margin-top: 2px; }
|
|
563
|
-
.pair-input-area { display: flex; gap: 4px; }
|
|
564
|
-
.pair-input-area input { flex: 1; min-width: 0; padding: 5px 8px; font-size: 13px; font-family: 'Menlo','Monaco',monospace;
|
|
565
|
-
background: var(--bg); border: 1px solid var(--compose-border); border-radius: 4px;
|
|
566
|
-
color: var(--text); outline: none; text-align: center; letter-spacing: 2px; }
|
|
567
|
-
.pair-input-area input:focus { border-color: var(--accent); }
|
|
568
|
-
.pair-input-area .pair-btn { flex-shrink: 0; width: auto; }
|
|
569
|
-
|
|
570
|
-
/* -- Push notification section -- */
|
|
571
|
-
.push-section { padding: 4px 12px 8px; border-top: 1px solid var(--border); }
|
|
572
|
-
.push-toggle { width: 100%; padding: 5px 8px; font-size: 11px; font-family: inherit;
|
|
573
|
-
color: var(--text-bright); background: var(--compose-bg); border: 1px solid var(--compose-border);
|
|
574
|
-
border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 6px;
|
|
575
|
-
justify-content: center; }
|
|
576
|
-
.push-toggle:hover { background: var(--compose-border); }
|
|
577
|
-
.push-toggle.subscribed { background: var(--accent); border-color: var(--accent); }
|
|
578
|
-
.push-toggle .push-icon { font-size: 14px; }
|
|
579
|
-
.push-test-btn { width: 100%; padding: 4px 8px; font-size: 10px; font-family: inherit;
|
|
580
|
-
color: var(--text-muted); background: none; border: 1px solid var(--border);
|
|
581
|
-
border-radius: 4px; cursor: pointer; margin-top: 4px; }
|
|
582
|
-
.push-test-btn:hover { color: var(--text-bright); border-color: var(--compose-border); }
|
|
583
382
|
|
|
584
383
|
/* -- Mobile -- */
|
|
585
384
|
@media (max-width: 768px) {
|
|
@@ -594,6 +393,14 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
594
393
|
.tab-bar { overflow-x: auto; }
|
|
595
394
|
.session-item { min-height: 44px; } /* touch-friendly */
|
|
596
395
|
.tab { min-height: 36px; }
|
|
396
|
+
.main-toolbar { padding-left: 8px; padding-right: 8px; }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
@media (hover: none), (pointer: coarse) {
|
|
400
|
+
.session-item .del,
|
|
401
|
+
.tab .close {
|
|
402
|
+
opacity: 1;
|
|
403
|
+
}
|
|
597
404
|
}
|
|
598
405
|
</style>
|
|
599
406
|
</head>
|
|
@@ -604,48 +411,14 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
604
411
|
<span>Sessions</span>
|
|
605
412
|
<button id="btn-new-session" title="New session">+</button>
|
|
606
413
|
</div>
|
|
607
|
-
<div class="session-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
<div class="devices-header" id="devices-header">
|
|
612
|
-
<span>Devices</span>
|
|
613
|
-
<span class="devices-toggle" id="devices-toggle">▼</span>
|
|
614
|
-
</div>
|
|
615
|
-
<div class="devices-list" id="devices-list"></div>
|
|
616
|
-
<div class="devices-actions" id="devices-actions" style="display:none">
|
|
617
|
-
<button class="pair-btn" id="btn-pair">Generate Pair Code</button>
|
|
618
|
-
<div class="pair-code-display" id="pair-code-display" style="display:none">
|
|
619
|
-
<span class="pair-code" id="pair-code-value"></span>
|
|
620
|
-
<span class="pair-expires" id="pair-expires"></span>
|
|
621
|
-
</div>
|
|
622
|
-
<div class="pair-input-area" id="pair-input-area" style="display:none">
|
|
623
|
-
<input type="text" id="pair-code-input" placeholder="Enter 6-digit code" maxlength="6" />
|
|
624
|
-
<button class="pair-btn" id="btn-submit-pair">Pair</button>
|
|
625
|
-
</div>
|
|
626
|
-
</div>
|
|
627
|
-
</div>
|
|
628
|
-
|
|
629
|
-
<!-- Push notifications -->
|
|
630
|
-
<div class="push-section" id="push-section" style="display:none">
|
|
631
|
-
<button class="push-toggle" id="btn-push-toggle">
|
|
632
|
-
<span class="push-icon">🔔</span>
|
|
633
|
-
<span id="push-label">Enable Notifications</span>
|
|
634
|
-
</button>
|
|
635
|
-
<button class="push-test-btn" id="btn-push-test" style="display:none">Send Test</button>
|
|
414
|
+
<div class="session-composer" id="session-composer">
|
|
415
|
+
<input type="text" id="new-session-input" placeholder="New session name" />
|
|
416
|
+
<button class="primary" id="btn-create-session">Add</button>
|
|
417
|
+
<button class="secondary" id="btn-cancel-session">Cancel</button>
|
|
636
418
|
</div>
|
|
419
|
+
<div class="session-list" id="session-list"></div>
|
|
637
420
|
|
|
638
421
|
<div class="sidebar-footer">
|
|
639
|
-
<div class="role-indicator" id="role-indicator">
|
|
640
|
-
<span id="role-dot"></span>
|
|
641
|
-
<span id="role-text"></span>
|
|
642
|
-
<button class="role-btn" id="btn-role" style="display:none"></button>
|
|
643
|
-
</div>
|
|
644
|
-
<button id="btn-theme" class="theme-toggle" title="Toggle theme">☀</button>
|
|
645
|
-
<div class="status">
|
|
646
|
-
<div class="status-dot connecting" id="status-dot"></div>
|
|
647
|
-
<span id="status-text">...</span>
|
|
648
|
-
</div>
|
|
649
422
|
<div class="version">v${VERSION}</div>
|
|
650
423
|
</div>
|
|
651
424
|
</aside>
|
|
@@ -657,9 +430,21 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
657
430
|
<div class="view-switch">
|
|
658
431
|
<button id="btn-live" class="active">Live</button>
|
|
659
432
|
<button id="btn-inspect">Inspect</button>
|
|
660
|
-
<button id="btn-workspace">Workspace</button>
|
|
661
433
|
</div>
|
|
662
434
|
</div>
|
|
435
|
+
<div class="main-toolbar">
|
|
436
|
+
<div class="status">
|
|
437
|
+
<div class="status-dot connecting" id="status-dot"></div>
|
|
438
|
+
<span id="status-text">Connecting...</span>
|
|
439
|
+
</div>
|
|
440
|
+
<div class="role-indicator" id="role-indicator">
|
|
441
|
+
<span id="role-dot"></span>
|
|
442
|
+
<span id="role-text"></span>
|
|
443
|
+
</div>
|
|
444
|
+
<button class="role-btn" id="btn-role" style="display:none"></button>
|
|
445
|
+
<div class="toolbar-spacer"></div>
|
|
446
|
+
<button id="btn-theme" class="theme-toggle" title="Toggle theme">☀</button>
|
|
447
|
+
</div>
|
|
663
448
|
<div id="terminal"></div>
|
|
664
449
|
<div id="inspect">
|
|
665
450
|
<div id="inspect-header">
|
|
@@ -671,56 +456,6 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
671
456
|
</div>
|
|
672
457
|
<pre id="inspect-content"></pre>
|
|
673
458
|
</div>
|
|
674
|
-
<div id="workspace">
|
|
675
|
-
<div class="ws-search">
|
|
676
|
-
<input type="text" id="ws-search-input" placeholder="Search topics, artifacts, runs..." />
|
|
677
|
-
</div>
|
|
678
|
-
<div id="ws-search-results" class="ws-search-results"></div>
|
|
679
|
-
<div id="ws-handoff" class="ws-handoff"></div>
|
|
680
|
-
<div class="ws-section" id="ws-notes-section">
|
|
681
|
-
<div class="ws-section-title">
|
|
682
|
-
<span>Notes</span>
|
|
683
|
-
<button id="btn-handoff">Handoff</button>
|
|
684
|
-
</div>
|
|
685
|
-
<div class="ws-note-input">
|
|
686
|
-
<input type="text" id="ws-note-input" placeholder="Add a note..." />
|
|
687
|
-
<button id="btn-add-note">Add</button>
|
|
688
|
-
</div>
|
|
689
|
-
<div id="ws-notes"></div>
|
|
690
|
-
</div>
|
|
691
|
-
<div class="ws-section">
|
|
692
|
-
<div class="ws-section-title">
|
|
693
|
-
<span>Pending Approvals</span>
|
|
694
|
-
</div>
|
|
695
|
-
<div id="ws-approvals"></div>
|
|
696
|
-
</div>
|
|
697
|
-
<div class="ws-section">
|
|
698
|
-
<div class="ws-section-title">
|
|
699
|
-
<span>Topics</span>
|
|
700
|
-
<button id="btn-new-topic">+ New</button>
|
|
701
|
-
</div>
|
|
702
|
-
<div id="ws-topics"></div>
|
|
703
|
-
</div>
|
|
704
|
-
<div class="ws-section">
|
|
705
|
-
<div class="ws-section-title">
|
|
706
|
-
<span>Active Runs</span>
|
|
707
|
-
</div>
|
|
708
|
-
<div id="ws-runs"></div>
|
|
709
|
-
</div>
|
|
710
|
-
<div class="ws-section">
|
|
711
|
-
<div class="ws-section-title">
|
|
712
|
-
<span>Recent Artifacts</span>
|
|
713
|
-
<button id="btn-capture-snapshot">Capture Snapshot</button>
|
|
714
|
-
</div>
|
|
715
|
-
<div id="ws-artifacts"></div>
|
|
716
|
-
</div>
|
|
717
|
-
<div class="ws-section">
|
|
718
|
-
<div class="ws-section-title">
|
|
719
|
-
<span>Commands</span>
|
|
720
|
-
</div>
|
|
721
|
-
<div id="ws-commands"></div>
|
|
722
|
-
</div>
|
|
723
|
-
</div>
|
|
724
459
|
<div class="compose-bar" id="compose-bar">
|
|
725
460
|
<button data-seq="esc">Esc</button>
|
|
726
461
|
<button data-seq="tab">Tab</button>
|
|
@@ -809,6 +544,7 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
809
544
|
let _pendingFit = false;
|
|
810
545
|
let fitDebounceTimer = null;
|
|
811
546
|
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
547
|
+
document.body.classList.toggle('touch-device', isTouchDevice);
|
|
812
548
|
function safeFit() {
|
|
813
549
|
if (_isComposing) { _pendingFit = true; return; }
|
|
814
550
|
if (fitAddon) fitAddon.fit();
|
|
@@ -956,8 +692,10 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
956
692
|
function renderSessions() {
|
|
957
693
|
const list = $('session-list'); list.innerHTML = '';
|
|
958
694
|
sessions.forEach(s => {
|
|
959
|
-
const el = document.createElement('
|
|
695
|
+
const el = document.createElement('div');
|
|
960
696
|
el.className = 'session-item' + (s.name === currentSession ? ' active' : '');
|
|
697
|
+
el.tabIndex = 0;
|
|
698
|
+
el.setAttribute('role', 'button');
|
|
961
699
|
const live = s.tabs.filter(t => !t.ended).length;
|
|
962
700
|
el.innerHTML = '<span class="dot"></span><span class="name">' + esc(s.name)
|
|
963
701
|
+ '</span><span class="count">' + live + '</span>'
|
|
@@ -965,7 +703,6 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
965
703
|
el.addEventListener('pointerdown', e => {
|
|
966
704
|
if (e.target.dataset.del) {
|
|
967
705
|
e.stopPropagation(); e.preventDefault();
|
|
968
|
-
if (!confirm('Delete session "' + e.target.dataset.del + '"? All tabs will be closed.')) return;
|
|
969
706
|
sendCtrl({ type: 'delete_session', name: e.target.dataset.del });
|
|
970
707
|
// if deleting current, switch to another or create fresh
|
|
971
708
|
if (e.target.dataset.del === currentSession) {
|
|
@@ -984,6 +721,13 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
984
721
|
selectSession(s.name);
|
|
985
722
|
closeSidebarMobile();
|
|
986
723
|
});
|
|
724
|
+
el.addEventListener('keydown', e => {
|
|
725
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
726
|
+
e.preventDefault();
|
|
727
|
+
selectSession(s.name);
|
|
728
|
+
closeSidebarMobile();
|
|
729
|
+
}
|
|
730
|
+
});
|
|
987
731
|
list.appendChild(el);
|
|
988
732
|
});
|
|
989
733
|
}
|
|
@@ -994,8 +738,10 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
994
738
|
const sess = sessions.find(s => s.name === currentSession);
|
|
995
739
|
if (!sess) return;
|
|
996
740
|
sess.tabs.forEach(t => {
|
|
997
|
-
const el = document.createElement('
|
|
741
|
+
const el = document.createElement('div');
|
|
998
742
|
el.className = 'tab' + (t.id === currentTabId ? ' active' : '');
|
|
743
|
+
el.tabIndex = 0;
|
|
744
|
+
el.setAttribute('role', 'button');
|
|
999
745
|
const clientCount = t.clients || 0;
|
|
1000
746
|
const countBadge = clientCount > 1 ? '<span class="client-count">' + clientCount + '</span>' : '';
|
|
1001
747
|
el.innerHTML = '<span class="title">' + esc(t.title) + '</span>' + countBadge
|
|
@@ -1010,6 +756,12 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
1010
756
|
e.preventDefault();
|
|
1011
757
|
if (t.id !== currentTabId) attachTab(t.id);
|
|
1012
758
|
});
|
|
759
|
+
el.addEventListener('keydown', e => {
|
|
760
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
761
|
+
e.preventDefault();
|
|
762
|
+
if (t.id !== currentTabId) attachTab(t.id);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
1013
765
|
list.appendChild(el);
|
|
1014
766
|
});
|
|
1015
767
|
}
|
|
@@ -1072,10 +824,34 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
1072
824
|
e.preventDefault();
|
|
1073
825
|
sendCtrl({ type: 'new_tab', session: currentSession, cols: term.cols, rows: term.rows });
|
|
1074
826
|
});
|
|
827
|
+
function openSessionComposer() {
|
|
828
|
+
$('session-composer').classList.add('visible');
|
|
829
|
+
$('new-session-input').focus();
|
|
830
|
+
$('new-session-input').select();
|
|
831
|
+
}
|
|
832
|
+
function closeSessionComposer() {
|
|
833
|
+
$('session-composer').classList.remove('visible');
|
|
834
|
+
$('new-session-input').value = '';
|
|
835
|
+
}
|
|
1075
836
|
$('btn-new-session').addEventListener('pointerdown', e => {
|
|
1076
837
|
e.preventDefault();
|
|
1077
|
-
|
|
1078
|
-
|
|
838
|
+
openSessionComposer();
|
|
839
|
+
});
|
|
840
|
+
$('btn-create-session').addEventListener('click', () => {
|
|
841
|
+
const name = $('new-session-input').value.trim();
|
|
842
|
+
if (!name) return;
|
|
843
|
+
sendCtrl({ type: 'new_session', name, cols: term.cols, rows: term.rows });
|
|
844
|
+
closeSessionComposer();
|
|
845
|
+
});
|
|
846
|
+
$('btn-cancel-session').addEventListener('click', closeSessionComposer);
|
|
847
|
+
$('new-session-input').addEventListener('keydown', e => {
|
|
848
|
+
if (e.key === 'Enter') {
|
|
849
|
+
e.preventDefault();
|
|
850
|
+
$('btn-create-session').click();
|
|
851
|
+
} else if (e.key === 'Escape') {
|
|
852
|
+
e.preventDefault();
|
|
853
|
+
closeSessionComposer();
|
|
854
|
+
}
|
|
1079
855
|
});
|
|
1080
856
|
|
|
1081
857
|
// -- E2EE client (Web Crypto API) --
|
|
@@ -1260,10 +1036,6 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
1260
1036
|
}
|
|
1261
1037
|
// Let server pick the session if we have none (bootstrap flow)
|
|
1262
1038
|
sendCtrl({ type: 'attach_first', session: currentSession || undefined, cols: term.cols, rows: term.rows });
|
|
1263
|
-
// Request device list (works with or without auth)
|
|
1264
|
-
sendCtrl({ type: 'list_devices' });
|
|
1265
|
-
// Request VAPID key for push notifications
|
|
1266
|
-
sendCtrl({ type: 'get_vapid_key' });
|
|
1267
1039
|
};
|
|
1268
1040
|
ws.onmessage = e => {
|
|
1269
1041
|
lastMessageAt = Date.now();
|
|
@@ -1308,10 +1080,6 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
1308
1080
|
// Track timestamp for session recovery on reconnect
|
|
1309
1081
|
lastReceivedTimestamp = Date.now();
|
|
1310
1082
|
if (msg.type === 'auth_ok') {
|
|
1311
|
-
if (msg.deviceId) myDeviceId = msg.deviceId;
|
|
1312
|
-
// Request device list and workspace data after auth
|
|
1313
|
-
sendCtrl({ type: 'list_devices' });
|
|
1314
|
-
sendCtrl({ type: 'list_notes' });
|
|
1315
1083
|
return;
|
|
1316
1084
|
}
|
|
1317
1085
|
if (msg.type === 'bootstrap') {
|
|
@@ -1327,61 +1095,6 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
1327
1095
|
alert('Error: ' + (msg.reason || 'unknown error'));
|
|
1328
1096
|
return;
|
|
1329
1097
|
}
|
|
1330
|
-
if (msg.type === 'device_list') {
|
|
1331
|
-
devicesList = msg.devices || [];
|
|
1332
|
-
renderDevices(); return;
|
|
1333
|
-
}
|
|
1334
|
-
if (msg.type === 'pair_code') {
|
|
1335
|
-
const display = $('pair-code-display');
|
|
1336
|
-
if (display) {
|
|
1337
|
-
display.style.display = 'block';
|
|
1338
|
-
$('pair-code-value').textContent = msg.code;
|
|
1339
|
-
const remaining = Math.max(0, Math.ceil((msg.expiresAt - Date.now()) / 1000));
|
|
1340
|
-
$('pair-expires').textContent = 'Expires in ' + Math.ceil(remaining / 60) + ' min';
|
|
1341
|
-
setTimeout(() => { display.style.display = 'none'; }, remaining * 1000);
|
|
1342
|
-
}
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
if (msg.type === 'pair_result') {
|
|
1346
|
-
if (msg.success) {
|
|
1347
|
-
$('pair-code-input').value = '';
|
|
1348
|
-
sendCtrl({ type: 'list_devices' });
|
|
1349
|
-
} else {
|
|
1350
|
-
alert('Pairing failed: ' + (msg.reason || 'invalid code'));
|
|
1351
|
-
}
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
if (msg.type === 'vapid_key') {
|
|
1355
|
-
pushVapidKey = msg.publicKey;
|
|
1356
|
-
showPushSection();
|
|
1357
|
-
// Check current push status
|
|
1358
|
-
sendCtrl({ type: 'get_push_status' });
|
|
1359
|
-
return;
|
|
1360
|
-
}
|
|
1361
|
-
if (msg.type === 'push_subscribed') {
|
|
1362
|
-
pushSubscribed = msg.success;
|
|
1363
|
-
updatePushUI();
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
if (msg.type === 'push_unsubscribed') {
|
|
1367
|
-
pushSubscribed = false;
|
|
1368
|
-
updatePushUI();
|
|
1369
|
-
return;
|
|
1370
|
-
}
|
|
1371
|
-
if (msg.type === 'push_status') {
|
|
1372
|
-
pushSubscribed = msg.subscribed;
|
|
1373
|
-
updatePushUI();
|
|
1374
|
-
return;
|
|
1375
|
-
}
|
|
1376
|
-
if (msg.type === 'push_test_result') {
|
|
1377
|
-
// Brief visual feedback
|
|
1378
|
-
const testBtn = $('btn-push-test');
|
|
1379
|
-
if (testBtn) {
|
|
1380
|
-
testBtn.textContent = msg.sent ? 'Sent!' : 'Failed';
|
|
1381
|
-
setTimeout(() => { testBtn.textContent = 'Send Test'; }, 2000);
|
|
1382
|
-
}
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
1098
|
if (msg.type === 'state') {
|
|
1386
1099
|
sessions = msg.sessions || [];
|
|
1387
1100
|
clientsList = msg.clients || [];
|
|
@@ -1396,7 +1109,7 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
1396
1109
|
currentTabId = msg.tabId; currentSession = msg.session;
|
|
1397
1110
|
if (msg.clientId) myClientId = msg.clientId;
|
|
1398
1111
|
if (msg.role) myRole = msg.role;
|
|
1399
|
-
setStatus('connected',
|
|
1112
|
+
setStatus('connected', 'Connected'); renderSessions(); renderTabs(); renderRole(); stabilizeFit(); return;
|
|
1400
1113
|
}
|
|
1401
1114
|
if (msg.type === 'role_changed') {
|
|
1402
1115
|
if (msg.clientId === myClientId) myRole = msg.role;
|
|
@@ -1420,42 +1133,6 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
1420
1133
|
applyInspectSearch();
|
|
1421
1134
|
return;
|
|
1422
1135
|
}
|
|
1423
|
-
// Workspace message handlers
|
|
1424
|
-
if (msg.type === 'topic_list') { wsTopics = msg.topics || []; renderWorkspaceTopics(); return; }
|
|
1425
|
-
if (msg.type === 'topic_created') {
|
|
1426
|
-
// Optimistic render: add topic directly
|
|
1427
|
-
if (msg.id && msg.title) wsTopics.unshift({ id: msg.id, sessionName: msg.sessionName, title: msg.title, createdAt: msg.createdAt, updatedAt: msg.updatedAt });
|
|
1428
|
-
renderWorkspaceTopics();
|
|
1429
|
-
return;
|
|
1430
|
-
}
|
|
1431
|
-
if (msg.type === 'topic_deleted') { refreshWorkspace(); return; }
|
|
1432
|
-
if (msg.type === 'run_list') { wsRuns = msg.runs || []; renderWorkspaceRuns(); return; }
|
|
1433
|
-
if (msg.type === 'run_created' || msg.type === 'run_updated') { if (currentView === 'workspace') refreshWorkspace(); return; }
|
|
1434
|
-
if (msg.type === 'artifact_list') { wsArtifacts = msg.artifacts || []; renderWorkspaceArtifacts(); return; }
|
|
1435
|
-
if (msg.type === 'snapshot_captured') {
|
|
1436
|
-
// Optimistic render: add artifact directly (with server-rendered HTML)
|
|
1437
|
-
if (msg.id) wsArtifacts.unshift({ id: msg.id, type: 'snapshot', title: msg.title || 'Snapshot', content: msg.content, contentType: msg.contentType || 'plain', renderedHtml: msg.renderedHtml, createdAt: msg.createdAt || Date.now() });
|
|
1438
|
-
renderWorkspaceArtifacts();
|
|
1439
|
-
return;
|
|
1440
|
-
}
|
|
1441
|
-
if (msg.type === 'approval_list') { wsApprovals = msg.approvals || []; renderWorkspaceApprovals(); return; }
|
|
1442
|
-
if (msg.type === 'approval_created') { if (currentView === 'workspace') refreshWorkspace(); return; }
|
|
1443
|
-
if (msg.type === 'approval_resolved') { if (currentView === 'workspace') refreshWorkspace(); return; }
|
|
1444
|
-
// Search results
|
|
1445
|
-
if (msg.type === 'search_results') { renderSearchResults(msg.results || []); return; }
|
|
1446
|
-
// Handoff bundle
|
|
1447
|
-
if (msg.type === 'handoff_bundle') { renderHandoffBundle(msg); return; }
|
|
1448
|
-
// Notes
|
|
1449
|
-
if (msg.type === 'note_list') { wsNotes = msg.notes || []; renderNotes(); return; }
|
|
1450
|
-
if (msg.type === 'note_created') {
|
|
1451
|
-
// Optimistic render: add note directly without waiting for list refresh
|
|
1452
|
-
if (msg.id && msg.content) wsNotes.unshift({ id: msg.id, content: msg.content, pinned: msg.pinned || false, createdAt: msg.createdAt, updatedAt: msg.updatedAt });
|
|
1453
|
-
renderNotes();
|
|
1454
|
-
return;
|
|
1455
|
-
}
|
|
1456
|
-
if (msg.type === 'note_updated' || msg.type === 'note_deleted' || msg.type === 'note_pinned') { sendCtrl({ type: 'list_notes' }); return; }
|
|
1457
|
-
// Commands
|
|
1458
|
-
if (msg.type === 'command_list') { wsCommands = msg.commands || []; renderCommands(); return; }
|
|
1459
1136
|
// Unrecognized enveloped control message — discard, never write to terminal
|
|
1460
1137
|
if (parsed.v === 1) {
|
|
1461
1138
|
console.warn('[remux] unhandled message type:', msg.type);
|
|
@@ -1519,32 +1196,26 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
1519
1196
|
});
|
|
1520
1197
|
|
|
1521
1198
|
// -- Inspect view --
|
|
1522
|
-
let currentView = 'live', inspectTimer = null
|
|
1199
|
+
let currentView = 'live', inspectTimer = null;
|
|
1200
|
+
function syncComposeBar() {
|
|
1201
|
+
$('compose-bar').classList.toggle('visible', isTouchDevice && currentView === 'live');
|
|
1202
|
+
}
|
|
1523
1203
|
function setView(mode) {
|
|
1524
1204
|
currentView = mode;
|
|
1525
1205
|
$('btn-live').classList.toggle('active', mode === 'live');
|
|
1526
1206
|
$('btn-inspect').classList.toggle('active', mode === 'inspect');
|
|
1527
|
-
$('btn-workspace').classList.toggle('active', mode === 'workspace');
|
|
1528
1207
|
$('terminal').classList.toggle('hidden', mode !== 'live');
|
|
1529
1208
|
$('inspect').classList.toggle('visible', mode === 'inspect');
|
|
1530
|
-
$('workspace').classList.toggle('visible', mode === 'workspace');
|
|
1531
|
-
// Inspect auto-refresh
|
|
1532
1209
|
if (inspectTimer) { clearInterval(inspectTimer); inspectTimer = null; }
|
|
1533
1210
|
if (mode === 'inspect') {
|
|
1534
1211
|
sendCtrl({ type: 'inspect' });
|
|
1535
1212
|
inspectTimer = setInterval(() => sendCtrl({ type: 'inspect' }), 3000);
|
|
1536
1213
|
}
|
|
1537
|
-
|
|
1538
|
-
if (wsRefreshTimer) { clearInterval(wsRefreshTimer); wsRefreshTimer = null; }
|
|
1539
|
-
if (mode === 'workspace') {
|
|
1540
|
-
refreshWorkspace();
|
|
1541
|
-
wsRefreshTimer = setInterval(refreshWorkspace, 5000);
|
|
1542
|
-
}
|
|
1214
|
+
syncComposeBar();
|
|
1543
1215
|
if (mode === 'live') { term.focus(); stabilizeFit(); }
|
|
1544
1216
|
}
|
|
1545
1217
|
$('btn-live').addEventListener('pointerdown', e => { e.preventDefault(); closeSidebarMobile(); setView('live'); });
|
|
1546
1218
|
$('btn-inspect').addEventListener('pointerdown', e => { e.preventDefault(); closeSidebarMobile(); setView('inspect'); });
|
|
1547
|
-
$('btn-workspace').addEventListener('pointerdown', e => { e.preventDefault(); closeSidebarMobile(); setView('workspace'); });
|
|
1548
1219
|
|
|
1549
1220
|
// -- Inspect search --
|
|
1550
1221
|
function applyInspectSearch() {
|
|
@@ -1555,8 +1226,6 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
1555
1226
|
$('inspect-match-count').textContent = '';
|
|
1556
1227
|
return;
|
|
1557
1228
|
}
|
|
1558
|
-
// Simple case-insensitive text search with <mark> highlighting
|
|
1559
|
-
// Work on raw text to avoid HTML entity issues, then escape each fragment
|
|
1560
1229
|
const esc = t => t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1561
1230
|
const q = query.toLowerCase();
|
|
1562
1231
|
const lower = text.toLowerCase();
|
|
@@ -1571,439 +1240,7 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
1571
1240
|
$('inspect-match-count').textContent = count > 0 ? count + ' match' + (count !== 1 ? 'es' : '') : 'No matches';
|
|
1572
1241
|
}
|
|
1573
1242
|
$('inspect-search-input').addEventListener('input', applyInspectSearch);
|
|
1574
|
-
|
|
1575
|
-
// -- Devices section --
|
|
1576
|
-
let devicesList = [], myDeviceId = null, devicesCollapsed = false;
|
|
1577
|
-
|
|
1578
|
-
function renderDevices() {
|
|
1579
|
-
const list = $('devices-list');
|
|
1580
|
-
const actions = $('devices-actions');
|
|
1581
|
-
if (!list) return;
|
|
1582
|
-
list.innerHTML = '';
|
|
1583
|
-
devicesList.forEach(d => {
|
|
1584
|
-
const el = document.createElement('div');
|
|
1585
|
-
el.className = 'device-item';
|
|
1586
|
-
const isSelf = d.id === myDeviceId;
|
|
1587
|
-
el.innerHTML = '<span class="device-dot ' + esc(d.trust) + '"></span>'
|
|
1588
|
-
+ '<span class="device-name">' + esc(d.name) + (isSelf ? ' <span class="device-self">(you)</span>' : '') + '</span>'
|
|
1589
|
-
+ '<span class="device-actions">'
|
|
1590
|
-
+ (d.trust !== 'trusted' ? '<button data-trust="' + d.id + '" title="Trust">✓</button>' : '')
|
|
1591
|
-
+ (d.trust !== 'blocked' ? '<button data-block="' + d.id + '" title="Block">✗</button>' : '')
|
|
1592
|
-
+ '<button data-rename-dev="' + d.id + '" title="Rename">✎</button>'
|
|
1593
|
-
+ (!isSelf ? '<button data-revoke="' + d.id + '" title="Revoke">🗑</button>' : '')
|
|
1594
|
-
+ '</span>';
|
|
1595
|
-
el.addEventListener('click', e => {
|
|
1596
|
-
const btn = e.target.closest('button');
|
|
1597
|
-
if (!btn) return;
|
|
1598
|
-
if (btn.dataset.trust) sendCtrl({ type: 'trust_device', deviceId: btn.dataset.trust });
|
|
1599
|
-
if (btn.dataset.block) sendCtrl({ type: 'block_device', deviceId: btn.dataset.block });
|
|
1600
|
-
if (btn.dataset.renameDev) {
|
|
1601
|
-
const newName = prompt('Device name:', d.name);
|
|
1602
|
-
if (newName && newName.trim()) sendCtrl({ type: 'rename_device', deviceId: btn.dataset.renameDev, name: newName.trim() });
|
|
1603
|
-
}
|
|
1604
|
-
if (btn.dataset.revoke) {
|
|
1605
|
-
if (confirm('Revoke device "' + d.name + '"?')) sendCtrl({ type: 'revoke_device', deviceId: btn.dataset.revoke });
|
|
1606
|
-
}
|
|
1607
|
-
});
|
|
1608
|
-
list.appendChild(el);
|
|
1609
|
-
});
|
|
1610
|
-
|
|
1611
|
-
// Show actions only for trusted devices; untrusted see pair input instead
|
|
1612
|
-
const isTrusted = devicesList.find(d => d.id === myDeviceId && d.trust === 'trusted');
|
|
1613
|
-
if (actions) {
|
|
1614
|
-
actions.style.display = 'block';
|
|
1615
|
-
const btnPair = $('btn-pair');
|
|
1616
|
-
if (btnPair) {
|
|
1617
|
-
btnPair.disabled = !isTrusted;
|
|
1618
|
-
btnPair.title = isTrusted ? '' : 'Only trusted devices can generate pair codes';
|
|
1619
|
-
}
|
|
1620
|
-
// Show pair input for untrusted devices
|
|
1621
|
-
const pairInput = $('pair-input-area');
|
|
1622
|
-
if (pairInput) pairInput.style.display = isTrusted ? 'none' : 'flex';
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
$('devices-header').addEventListener('click', () => {
|
|
1627
|
-
devicesCollapsed = !devicesCollapsed;
|
|
1628
|
-
$('devices-list').classList.toggle('collapsed', devicesCollapsed);
|
|
1629
|
-
$('devices-toggle').classList.toggle('collapsed', devicesCollapsed);
|
|
1630
|
-
if ($('devices-actions')) $('devices-actions').style.display = devicesCollapsed ? 'none' : '';
|
|
1631
|
-
});
|
|
1632
|
-
|
|
1633
|
-
$('btn-pair').addEventListener('click', () => {
|
|
1634
|
-
sendCtrl({ type: 'generate_pair_code' });
|
|
1635
|
-
});
|
|
1636
|
-
|
|
1637
|
-
$('btn-submit-pair').addEventListener('click', () => {
|
|
1638
|
-
const code = $('pair-code-input').value.trim();
|
|
1639
|
-
if (!/^\d{6}$/.test(code)) { alert('Please enter a 6-digit pair code'); return; }
|
|
1640
|
-
sendCtrl({ type: 'pair', code });
|
|
1641
|
-
});
|
|
1642
|
-
|
|
1643
|
-
$('pair-code-input').addEventListener('keydown', e => {
|
|
1644
|
-
if (e.key === 'Enter') { e.preventDefault(); $('btn-submit-pair').click(); }
|
|
1645
|
-
});
|
|
1646
|
-
|
|
1647
|
-
// -- Push notifications --
|
|
1648
|
-
let pushSubscribed = false;
|
|
1649
|
-
let pushVapidKey = null;
|
|
1650
|
-
|
|
1651
|
-
function showPushSection() {
|
|
1652
|
-
// Show only if browser supports push + service workers
|
|
1653
|
-
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
|
|
1654
|
-
$('push-section').style.display = 'block';
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
function updatePushUI() {
|
|
1658
|
-
const btn = $('btn-push-toggle');
|
|
1659
|
-
const label = $('push-label');
|
|
1660
|
-
const testBtn = $('btn-push-test');
|
|
1661
|
-
if (pushSubscribed) {
|
|
1662
|
-
btn.classList.add('subscribed');
|
|
1663
|
-
label.textContent = 'Notifications On';
|
|
1664
|
-
testBtn.style.display = 'block';
|
|
1665
|
-
} else {
|
|
1666
|
-
btn.classList.remove('subscribed');
|
|
1667
|
-
label.textContent = 'Enable Notifications';
|
|
1668
|
-
testBtn.style.display = 'none';
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
function urlBase64ToUint8Array(base64String) {
|
|
1673
|
-
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
|
1674
|
-
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
1675
|
-
const rawData = atob(base64);
|
|
1676
|
-
const outputArray = new Uint8Array(rawData.length);
|
|
1677
|
-
for (let i = 0; i < rawData.length; ++i) outputArray[i] = rawData.charCodeAt(i);
|
|
1678
|
-
return outputArray;
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
async function subscribePush() {
|
|
1682
|
-
if (!pushVapidKey) return;
|
|
1683
|
-
try {
|
|
1684
|
-
const reg = await navigator.serviceWorker.register('/sw.js');
|
|
1685
|
-
await navigator.serviceWorker.ready;
|
|
1686
|
-
const sub = await reg.pushManager.subscribe({
|
|
1687
|
-
userVisibleOnly: true,
|
|
1688
|
-
applicationServerKey: urlBase64ToUint8Array(pushVapidKey),
|
|
1689
|
-
});
|
|
1690
|
-
const subJson = sub.toJSON();
|
|
1691
|
-
sendCtrl({
|
|
1692
|
-
type: 'subscribe_push',
|
|
1693
|
-
subscription: {
|
|
1694
|
-
endpoint: subJson.endpoint,
|
|
1695
|
-
keys: { p256dh: subJson.keys.p256dh, auth: subJson.keys.auth },
|
|
1696
|
-
},
|
|
1697
|
-
});
|
|
1698
|
-
} catch (err) {
|
|
1699
|
-
console.error('[push] subscribe failed:', err);
|
|
1700
|
-
if (Notification.permission === 'denied') {
|
|
1701
|
-
$('push-label').textContent = 'Permission Denied';
|
|
1702
|
-
} else {
|
|
1703
|
-
$('push-label').textContent = 'Not Available';
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
async function unsubscribePush() {
|
|
1709
|
-
try {
|
|
1710
|
-
const reg = await navigator.serviceWorker.getRegistration();
|
|
1711
|
-
if (reg) {
|
|
1712
|
-
const sub = await reg.pushManager.getSubscription();
|
|
1713
|
-
if (sub) await sub.unsubscribe();
|
|
1714
|
-
}
|
|
1715
|
-
sendCtrl({ type: 'unsubscribe_push' });
|
|
1716
|
-
} catch (err) {
|
|
1717
|
-
console.error('[push] unsubscribe failed:', err);
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
$('btn-push-toggle').addEventListener('click', async () => {
|
|
1722
|
-
if (pushSubscribed) {
|
|
1723
|
-
await unsubscribePush();
|
|
1724
|
-
pushSubscribed = false;
|
|
1725
|
-
} else {
|
|
1726
|
-
await subscribePush();
|
|
1727
|
-
}
|
|
1728
|
-
updatePushUI();
|
|
1729
|
-
});
|
|
1730
|
-
|
|
1731
|
-
$('btn-push-test').addEventListener('click', () => {
|
|
1732
|
-
sendCtrl({ type: 'test_push' });
|
|
1733
|
-
});
|
|
1734
|
-
|
|
1735
|
-
// -- Workspace view --
|
|
1736
|
-
let wsTopics = [], wsRuns = [], wsArtifacts = [], wsApprovals = [];
|
|
1737
|
-
let wsNotes = [], wsCommands = [];
|
|
1738
|
-
|
|
1739
|
-
function refreshWorkspace() {
|
|
1740
|
-
if (!currentSession) return; // Wait until bootstrap resolves a session
|
|
1741
|
-
sendCtrl({ type: 'list_topics', sessionName: currentSession });
|
|
1742
|
-
sendCtrl({ type: 'list_runs' });
|
|
1743
|
-
sendCtrl({ type: 'list_artifacts', sessionName: currentSession });
|
|
1744
|
-
sendCtrl({ type: 'list_approvals' });
|
|
1745
|
-
sendCtrl({ type: 'list_notes' }); // Notes are global workspace memory, not session-scoped
|
|
1746
|
-
sendCtrl({ type: 'list_commands' });
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
function timeAgo(ts) {
|
|
1750
|
-
const s = Math.floor((Date.now() - ts) / 1000);
|
|
1751
|
-
if (s < 60) return s + 's ago';
|
|
1752
|
-
if (s < 3600) return Math.floor(s / 60) + 'm ago';
|
|
1753
|
-
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
|
|
1754
|
-
return Math.floor(s / 86400) + 'd ago';
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
function renderWorkspaceApprovals() {
|
|
1758
|
-
const el = $('ws-approvals');
|
|
1759
|
-
if (!el) return;
|
|
1760
|
-
const pending = wsApprovals.filter(a => a.status === 'pending');
|
|
1761
|
-
if (pending.length === 0) { el.innerHTML = '<div class="ws-empty">No pending approvals</div>'; return; }
|
|
1762
|
-
el.innerHTML = pending.map(a =>
|
|
1763
|
-
'<div class="ws-card">' +
|
|
1764
|
-
'<div class="ws-card-header">' +
|
|
1765
|
-
'<span class="ws-badge pending">pending</span>' +
|
|
1766
|
-
'<span class="ws-card-title">' + esc(a.title) + '</span>' +
|
|
1767
|
-
'<span class="ws-card-meta">' + timeAgo(a.createdAt) + '</span>' +
|
|
1768
|
-
'</div>' +
|
|
1769
|
-
(a.description ? '<div class="ws-card-desc">' + esc(a.description) + '</div>' : '') +
|
|
1770
|
-
'<div class="ws-card-actions">' +
|
|
1771
|
-
'<button class="approve" data-approve-id="' + a.id + '">Approve</button>' +
|
|
1772
|
-
'<button class="reject" data-reject-id="' + a.id + '">Reject</button>' +
|
|
1773
|
-
'</div>' +
|
|
1774
|
-
'</div>'
|
|
1775
|
-
).join('');
|
|
1776
|
-
el.querySelectorAll('[data-approve-id]').forEach(btn => {
|
|
1777
|
-
btn.addEventListener('click', () => sendCtrl({ type: 'resolve_approval', approvalId: btn.dataset.approveId, status: 'approved' }));
|
|
1778
|
-
});
|
|
1779
|
-
el.querySelectorAll('[data-reject-id]').forEach(btn => {
|
|
1780
|
-
btn.addEventListener('click', () => sendCtrl({ type: 'resolve_approval', approvalId: btn.dataset.rejectId, status: 'rejected' }));
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
function renderWorkspaceTopics() {
|
|
1785
|
-
const el = $('ws-topics');
|
|
1786
|
-
if (!el) return;
|
|
1787
|
-
if (wsTopics.length === 0) { el.innerHTML = '<div class="ws-empty">No topics yet</div>'; return; }
|
|
1788
|
-
el.innerHTML = wsTopics.map(t =>
|
|
1789
|
-
'<div class="ws-card">' +
|
|
1790
|
-
'<div class="ws-card-header">' +
|
|
1791
|
-
'<span class="ws-card-title">' + esc(t.title) + '</span>' +
|
|
1792
|
-
'<span class="ws-card-meta">' + timeAgo(t.createdAt) + '</span>' +
|
|
1793
|
-
'<button class="del-topic" data-del-topic="' + t.id + '" title="Delete">×</button>' +
|
|
1794
|
-
'</div>' +
|
|
1795
|
-
'<div class="ws-card-meta">' + esc(t.sessionName) + '</div>' +
|
|
1796
|
-
'</div>'
|
|
1797
|
-
).join('');
|
|
1798
|
-
el.querySelectorAll('[data-del-topic]').forEach(btn => {
|
|
1799
|
-
btn.addEventListener('click', () => {
|
|
1800
|
-
sendCtrl({ type: 'delete_topic', topicId: btn.dataset.delTopic });
|
|
1801
|
-
setTimeout(refreshWorkspace, 200);
|
|
1802
|
-
});
|
|
1803
|
-
});
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
function renderWorkspaceRuns() {
|
|
1807
|
-
const el = $('ws-runs');
|
|
1808
|
-
if (!el) return;
|
|
1809
|
-
const active = wsRuns.filter(r => r.status === 'running');
|
|
1810
|
-
const recent = wsRuns.filter(r => r.status !== 'running').slice(-5).reverse();
|
|
1811
|
-
const all = [...active, ...recent];
|
|
1812
|
-
if (all.length === 0) { el.innerHTML = '<div class="ws-empty">No runs</div>'; return; }
|
|
1813
|
-
el.innerHTML = all.map(r =>
|
|
1814
|
-
'<div class="ws-card">' +
|
|
1815
|
-
'<div class="ws-card-header">' +
|
|
1816
|
-
'<span class="ws-badge ' + r.status + '">' + r.status + '</span>' +
|
|
1817
|
-
'<span class="ws-card-title">' + esc(r.command || '(no command)') + '</span>' +
|
|
1818
|
-
'<span class="ws-card-meta">' + timeAgo(r.startedAt) + '</span>' +
|
|
1819
|
-
'</div>' +
|
|
1820
|
-
(r.exitCode !== null ? '<div class="ws-card-meta">Exit: ' + r.exitCode + '</div>' : '') +
|
|
1821
|
-
'</div>'
|
|
1822
|
-
).join('');
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
// Track which artifact IDs are expanded (persists across re-renders)
|
|
1826
|
-
const _expandedArtifacts = new Set();
|
|
1827
|
-
|
|
1828
|
-
function renderWorkspaceArtifacts() {
|
|
1829
|
-
const el = $('ws-artifacts');
|
|
1830
|
-
if (!el) return;
|
|
1831
|
-
// Artifacts are already filtered by session_name on the server side
|
|
1832
|
-
const recent = wsArtifacts.slice(-10).reverse();
|
|
1833
|
-
if (recent.length === 0) { el.innerHTML = '<div class="ws-empty">No artifacts</div>'; return; }
|
|
1834
|
-
el.innerHTML = recent.map((a) => {
|
|
1835
|
-
var hasContent = a.content && a.content.trim();
|
|
1836
|
-
var ct = a.contentType || 'plain';
|
|
1837
|
-
var badge = (ct !== 'plain') ? ' <span class="ws-badge ' + esc(ct) + '">' + esc(ct) + '</span>' : '';
|
|
1838
|
-
var rendered = a.renderedHtml || (hasContent ? '<pre style="margin:0;font-size:11px;color:var(--text-muted);white-space:pre-wrap;word-break:break-word">' + esc(a.content) + '</pre>' : '');
|
|
1839
|
-
var isExpanded = _expandedArtifacts.has(a.id);
|
|
1840
|
-
return '<div class="ws-card">' +
|
|
1841
|
-
'<div class="ws-card-header">' +
|
|
1842
|
-
'<span class="ws-badge ' + esc(a.type) + '">' + esc(a.type) + '</span>' +
|
|
1843
|
-
badge +
|
|
1844
|
-
'<span class="ws-card-title">' + esc(a.title) + '</span>' +
|
|
1845
|
-
'<span class="ws-card-meta">' + timeAgo(a.createdAt) + '</span>' +
|
|
1846
|
-
(hasContent ? '<button class="ws-card-toggle" data-toggle-art="' + esc(a.id) + '">' + (isExpanded ? 'Hide' : 'Show') + '</button>' : '') +
|
|
1847
|
-
'</div>' +
|
|
1848
|
-
(hasContent ? '<div class="ws-card-content" data-art-content="' + esc(a.id) + '" style="display:' + (isExpanded ? 'block' : 'none') + '">' + rendered + '</div>' : '') +
|
|
1849
|
-
'</div>';
|
|
1850
|
-
}).join('');
|
|
1851
|
-
// Wire up toggle buttons
|
|
1852
|
-
el.querySelectorAll('[data-toggle-art]').forEach(function(btn) {
|
|
1853
|
-
btn.addEventListener('click', function() {
|
|
1854
|
-
var artId = btn.getAttribute('data-toggle-art');
|
|
1855
|
-
var contentEl = el.querySelector('[data-art-content="' + artId + '"]');
|
|
1856
|
-
if (!contentEl) return;
|
|
1857
|
-
var visible = contentEl.style.display !== 'none';
|
|
1858
|
-
contentEl.style.display = visible ? 'none' : 'block';
|
|
1859
|
-
btn.textContent = visible ? 'Show' : 'Hide';
|
|
1860
|
-
if (visible) _expandedArtifacts.delete(artId); else _expandedArtifacts.add(artId);
|
|
1861
|
-
});
|
|
1862
|
-
});
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
$('btn-new-topic').addEventListener('click', () => {
|
|
1866
|
-
const title = prompt('Topic title:');
|
|
1867
|
-
if (title && title.trim()) {
|
|
1868
|
-
sendCtrl({ type: 'create_topic', sessionName: currentSession, title: title.trim() });
|
|
1869
|
-
// Optimistic render in topic_created handler, no delayed refresh needed
|
|
1870
|
-
}
|
|
1871
|
-
});
|
|
1872
|
-
|
|
1873
|
-
$('btn-capture-snapshot').addEventListener('click', () => {
|
|
1874
|
-
sendCtrl({ type: 'capture_snapshot' });
|
|
1875
|
-
// Optimistic render in snapshot_captured handler, no delayed refresh needed
|
|
1876
|
-
});
|
|
1877
|
-
|
|
1878
|
-
// -- Search --
|
|
1879
|
-
let searchDebounce = null;
|
|
1880
|
-
$('ws-search-input').addEventListener('input', () => {
|
|
1881
|
-
clearTimeout(searchDebounce);
|
|
1882
|
-
const q = $('ws-search-input').value.trim();
|
|
1883
|
-
if (!q) { $('ws-search-results').innerHTML = ''; return; }
|
|
1884
|
-
searchDebounce = setTimeout(() => sendCtrl({ type: 'search', query: q }), 200);
|
|
1885
|
-
});
|
|
1886
|
-
|
|
1887
|
-
function renderSearchResults(results) {
|
|
1888
|
-
const el = $('ws-search-results');
|
|
1889
|
-
if (!el) return;
|
|
1890
|
-
if (results.length === 0) {
|
|
1891
|
-
const q = ($('ws-search-input') || {}).value || '';
|
|
1892
|
-
el.innerHTML = q ? '<div class="ws-empty">No results</div>' : '';
|
|
1893
|
-
return;
|
|
1894
|
-
}
|
|
1895
|
-
const esc = t => (t || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1896
|
-
el.innerHTML = results.map(r =>
|
|
1897
|
-
'<div class="ws-search-result">' +
|
|
1898
|
-
'<span class="sr-type">' + esc(r.entityType) + '</span> ' +
|
|
1899
|
-
'<span class="sr-title">' + esc(r.title) + '</span>' +
|
|
1900
|
-
'</div>'
|
|
1901
|
-
).join('');
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
// -- Handoff --
|
|
1905
|
-
$('btn-handoff').addEventListener('click', () => {
|
|
1906
|
-
const el = $('ws-handoff');
|
|
1907
|
-
if (el.classList.contains('visible')) { el.classList.remove('visible'); return; }
|
|
1908
|
-
sendCtrl({ type: 'get_handoff' });
|
|
1909
|
-
});
|
|
1910
|
-
|
|
1911
|
-
function renderHandoffBundle(bundle) {
|
|
1912
|
-
const el = $('ws-handoff');
|
|
1913
|
-
if (!el) return;
|
|
1914
|
-
el.classList.add('visible');
|
|
1915
|
-
const esc = t => (t || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1916
|
-
let html = '<div class="ws-handoff-section"><div class="ws-handoff-label">Sessions</div>';
|
|
1917
|
-
html += '<ul class="ws-handoff-list">';
|
|
1918
|
-
(bundle.sessions || []).forEach(s => {
|
|
1919
|
-
html += '<li>' + esc(s.name) + ' (' + s.activeTabs + ' active tabs)</li>';
|
|
1920
|
-
});
|
|
1921
|
-
html += '</ul></div>';
|
|
1922
|
-
if ((bundle.activeTopics || []).length > 0) {
|
|
1923
|
-
html += '<div class="ws-handoff-section"><div class="ws-handoff-label">Active Topics (24h)</div>';
|
|
1924
|
-
html += '<ul class="ws-handoff-list">';
|
|
1925
|
-
bundle.activeTopics.forEach(t => { html += '<li>' + esc(t.title) + '</li>'; });
|
|
1926
|
-
html += '</ul></div>';
|
|
1927
|
-
}
|
|
1928
|
-
if ((bundle.pendingApprovals || []).length > 0) {
|
|
1929
|
-
html += '<div class="ws-handoff-section"><div class="ws-handoff-label">Pending Approvals</div>';
|
|
1930
|
-
html += '<ul class="ws-handoff-list">';
|
|
1931
|
-
bundle.pendingApprovals.forEach(a => { html += '<li>' + esc(a.title) + '</li>'; });
|
|
1932
|
-
html += '</ul></div>';
|
|
1933
|
-
}
|
|
1934
|
-
html += '<div class="ws-handoff-section"><div class="ws-handoff-label">Recent Runs (' + (bundle.recentRuns || []).length + ')</div></div>';
|
|
1935
|
-
html += '<div class="ws-handoff-section"><div class="ws-handoff-label">Key Artifacts (' + (bundle.keyArtifacts || []).length + ')</div></div>';
|
|
1936
|
-
el.innerHTML = html;
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
// -- Notes --
|
|
1940
|
-
function renderNotes() {
|
|
1941
|
-
const el = $('ws-notes');
|
|
1942
|
-
if (!el) return;
|
|
1943
|
-
if (wsNotes.length === 0) { el.innerHTML = '<div class="ws-empty">No notes yet</div>'; return; }
|
|
1944
|
-
const esc = t => (t || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1945
|
-
el.innerHTML = wsNotes.map(n =>
|
|
1946
|
-
'<div class="ws-note' + (n.pinned ? ' pinned' : '') + '">' +
|
|
1947
|
-
'<div class="ws-note-content">' + esc(n.content) + '</div>' +
|
|
1948
|
-
'<div class="ws-note-actions">' +
|
|
1949
|
-
'<button data-pin-note="' + n.id + '">' + (n.pinned ? 'Unpin' : 'Pin') + '</button>' +
|
|
1950
|
-
'<button data-del-note="' + n.id + '">Delete</button>' +
|
|
1951
|
-
'</div>' +
|
|
1952
|
-
'</div>'
|
|
1953
|
-
).join('');
|
|
1954
|
-
el.querySelectorAll('[data-pin-note]').forEach(btn => {
|
|
1955
|
-
btn.addEventListener('click', () => sendCtrl({ type: 'pin_note', noteId: btn.dataset.pinNote }));
|
|
1956
|
-
});
|
|
1957
|
-
el.querySelectorAll('[data-del-note]').forEach(btn => {
|
|
1958
|
-
btn.addEventListener('click', () => sendCtrl({ type: 'delete_note', noteId: btn.dataset.delNote }));
|
|
1959
|
-
});
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
$('btn-add-note').addEventListener('click', () => {
|
|
1963
|
-
const input = $('ws-note-input');
|
|
1964
|
-
const content = input.value.trim();
|
|
1965
|
-
if (!content) return;
|
|
1966
|
-
sendCtrl({ type: 'create_note', content });
|
|
1967
|
-
input.value = '';
|
|
1968
|
-
// Feedback: show saving indicator, revert if no response in 3s
|
|
1969
|
-
const el = $('ws-notes');
|
|
1970
|
-
const prevHtml = el.innerHTML;
|
|
1971
|
-
el.innerHTML = '<div class="ws-empty">Saving...</div>';
|
|
1972
|
-
setTimeout(() => {
|
|
1973
|
-
if (el.innerHTML.includes('Saving...')) {
|
|
1974
|
-
el.innerHTML = '<div class="ws-empty" style="color:var(--text-dim)">Note may not have saved — check server logs</div>';
|
|
1975
|
-
}
|
|
1976
|
-
}, 3000);
|
|
1977
|
-
});
|
|
1978
|
-
$('ws-note-input').addEventListener('keydown', e => {
|
|
1979
|
-
if (e.key === 'Enter') { e.preventDefault(); $('btn-add-note').click(); }
|
|
1980
|
-
});
|
|
1981
|
-
|
|
1982
|
-
// -- Commands --
|
|
1983
|
-
function formatDuration(startedAt, endedAt) {
|
|
1984
|
-
if (!endedAt) return 'running';
|
|
1985
|
-
const ms = endedAt - startedAt;
|
|
1986
|
-
if (ms < 1000) return ms + 'ms';
|
|
1987
|
-
if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
|
|
1988
|
-
return Math.floor(ms / 60000) + 'm ' + Math.floor((ms % 60000) / 1000) + 's';
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
function renderCommands() {
|
|
1992
|
-
const el = $('ws-commands');
|
|
1993
|
-
if (!el) return;
|
|
1994
|
-
if (wsCommands.length === 0) { el.innerHTML = '<div class="ws-empty">No commands detected (requires shell integration)</div>'; return; }
|
|
1995
|
-
const esc = t => (t || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1996
|
-
el.innerHTML = wsCommands.slice(0, 20).map(c => {
|
|
1997
|
-
const exitClass = c.exitCode === null ? '' : (c.exitCode === 0 ? 'ok' : 'err');
|
|
1998
|
-
const exitSymbol = c.exitCode === null ? '' : (c.exitCode === 0 ? '✓' : '✗ ' + c.exitCode);
|
|
1999
|
-
return '<div class="ws-cmd">' +
|
|
2000
|
-
'<span class="ws-cmd-text">' + esc(c.command || '(unknown)') + '</span>' +
|
|
2001
|
-
(exitSymbol ? '<span class="ws-cmd-exit ' + exitClass + '">' + exitSymbol + '</span>' : '') +
|
|
2002
|
-
'<span class="ws-cmd-meta">' + formatDuration(c.startedAt, c.endedAt) + '</span>' +
|
|
2003
|
-
(c.cwd ? '<span class="ws-cmd-meta">' + esc(c.cwd) + '</span>' : '') +
|
|
2004
|
-
'</div>';
|
|
2005
|
-
}).join('');
|
|
2006
|
-
}
|
|
1243
|
+
syncComposeBar();
|
|
2007
1244
|
|
|
2008
1245
|
// -- Tab rename (double-click) --
|
|
2009
1246
|
$('tab-list').addEventListener('dblclick', e => {
|