cli-jaw 2.17.30 → 2.17.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +2 -0
  2. package/dist/src/agent/codex-app-client.js +69 -1
  3. package/dist/src/agent/codex-app-client.js.map +1 -1
  4. package/dist/src/agent/cursor-runtime.js +9 -0
  5. package/dist/src/agent/cursor-runtime.js.map +1 -1
  6. package/dist/src/agent/pi-runtime.js +1 -1
  7. package/dist/src/agent/pi-runtime.js.map +1 -1
  8. package/dist/src/agent/prompt-context.js +39 -0
  9. package/dist/src/agent/prompt-context.js.map +1 -1
  10. package/dist/src/agent/spawn.js +138 -20
  11. package/dist/src/agent/spawn.js.map +1 -1
  12. package/dist/src/browser/web-ai/capability-observation-presets.js +4 -0
  13. package/dist/src/browser/web-ai/capability-observation-presets.js.map +1 -1
  14. package/dist/src/browser/web-ai/chatgpt-model.js +9 -5
  15. package/dist/src/browser/web-ai/chatgpt-model.js.map +1 -1
  16. package/dist/src/browser/web-ai/grok-model.js +18 -5
  17. package/dist/src/browser/web-ai/grok-model.js.map +1 -1
  18. package/dist/src/cli/claude-models.js +6 -0
  19. package/dist/src/cli/claude-models.js.map +1 -1
  20. package/dist/src/cli/handlers-runtime.js +29 -4
  21. package/dist/src/cli/handlers-runtime.js.map +1 -1
  22. package/dist/src/cli/registry.js +56 -15
  23. package/dist/src/cli/registry.js.map +1 -1
  24. package/dist/src/code-mode/model-options.js +4 -4
  25. package/dist/src/code-mode/model-options.js.map +1 -1
  26. package/dist/src/core/config.js +1 -1
  27. package/dist/src/core/config.js.map +1 -1
  28. package/dist/src/core/db.js +19 -0
  29. package/dist/src/core/db.js.map +1 -1
  30. package/dist/src/core/runtime-settings.js +11 -2
  31. package/dist/src/core/runtime-settings.js.map +1 -1
  32. package/dist/src/orchestrator/attestation.js +66 -0
  33. package/dist/src/orchestrator/attestation.js.map +1 -1
  34. package/dist/src/orchestrator/gateway.js +11 -3
  35. package/dist/src/orchestrator/gateway.js.map +1 -1
  36. package/dist/src/orchestrator/pipeline.js +5 -0
  37. package/dist/src/orchestrator/pipeline.js.map +1 -1
  38. package/dist/src/routes/command.js +4 -1
  39. package/dist/src/routes/command.js.map +1 -1
  40. package/dist/src/routes/orchestrate.js +14 -2
  41. package/dist/src/routes/orchestrate.js.map +1 -1
  42. package/dist/src/slack/commands.js +1 -0
  43. package/dist/src/slack/commands.js.map +1 -1
  44. package/dist/src/telegram/bot.js +3 -2
  45. package/dist/src/telegram/bot.js.map +1 -1
  46. package/package.json +5 -1
  47. package/public/dist/assets/{Agent-BjxJRsKG.js → Agent-kyyjcnqC.js} +1 -1
  48. package/public/dist/assets/BrowserPanel-0b3deLGR.js +3 -0
  49. package/public/dist/assets/BrowserPanel-D7SnTts8.css +1 -0
  50. package/public/dist/assets/{Heartbeat-Ca8DmJcw.js → Heartbeat-CD1R95Kv.js} +1 -1
  51. package/public/dist/assets/{Memory-BY88ur_h.js → Memory-CpZIdE1o.js} +1 -1
  52. package/public/dist/assets/{ModelProvider-mtAtYLmW.js → ModelProvider-tL42E5dy.js} +1 -1
  53. package/public/dist/assets/agent-meta-KPnVam9G.js +1 -0
  54. package/public/dist/assets/{app-CYayckBa.js → app-BvRV-aEt.js} +4 -4
  55. package/public/dist/assets/bgtask-badge-DLqpImpn.js +1 -0
  56. package/public/dist/assets/{bgtask-badge-CT6s6rND.js → bgtask-badge-FiEB3FJm.js} +1 -1
  57. package/public/dist/assets/constants-BfYYzHNr.js +1 -0
  58. package/public/dist/assets/{employees-DCju7V1x.js → employees-86F_D_Ce.js} +1 -1
  59. package/public/dist/assets/iframe-renderer-C_swzquc.js +1 -0
  60. package/public/dist/assets/{iframe-renderer-LZp7Nqm1.js → iframe-renderer-eozJpgjk.js} +2 -2
  61. package/public/dist/assets/manager-BnmWx9xL.js +13 -0
  62. package/public/dist/assets/manager-Cmu0aKPb.css +1 -0
  63. package/public/dist/assets/{memory-V5caYbpS.js → memory-DX7U3WV_.js} +1 -1
  64. package/public/dist/assets/memory-DfPbqPTD.js +1 -0
  65. package/public/dist/assets/{message-history-Bp_sUDSx.js → message-history-B_fIGorC.js} +1 -1
  66. package/public/dist/assets/message-history-BhSsLjis.js +1 -0
  67. package/public/dist/assets/{render-Cd4frZ2U.js → render-fKAuISeM.js} +1 -1
  68. package/public/dist/assets/settings-BNqSR9Zr.js +1 -0
  69. package/public/dist/assets/{settings-a0blK1fr.js → settings-CH6exiZf.js} +1 -1
  70. package/public/dist/assets/{settings-core-C5qo8giT.js → settings-core-DqKyyxbQ.js} +1 -1
  71. package/public/dist/assets/settings-core-x7CmBZq1.js +1 -0
  72. package/public/dist/assets/{sidebar-FbI4udzs.js → sidebar-qyLy9Og8.js} +3 -3
  73. package/public/dist/assets/skills-C7Yi3LIq.js +1 -0
  74. package/public/dist/assets/{skills-DeG9lWF2.js → skills-DAhOtbfP.js} +1 -1
  75. package/public/dist/assets/{slash-commands-FoQuZXga.js → slash-commands-BYH6YVS4.js} +1 -1
  76. package/public/dist/assets/slash-commands-DEbJk3sj.js +1 -0
  77. package/public/dist/assets/{trace-drawer-CWs8ydMO.js → trace-drawer-nyAJ3Bm_.js} +1 -1
  78. package/public/dist/assets/{ui-Cd8etFwN.js → ui-C62w6bJS.js} +1 -1
  79. package/public/dist/assets/ui-CBErZPBp.js +1 -0
  80. package/public/dist/index.html +1 -1
  81. package/public/dist/manager/index.html +2 -2
  82. package/public/js/constants.ts +22 -5
  83. package/public/js/features/settings-types.ts +5 -0
  84. package/public/manager/src/App.tsx +1 -1
  85. package/public/manager/src/AppChrome.tsx +18 -3
  86. package/public/manager/src/SidebarRailRouter.tsx +47 -1
  87. package/public/manager/src/browser-panel/BrowserPanel.tsx +151 -57
  88. package/public/manager/src/browser-panel/browser-address-bar.tsx +53 -0
  89. package/public/manager/src/browser-panel/browser-address-state.ts +41 -0
  90. package/public/manager/src/browser-panel/browser-favicon.ts +18 -0
  91. package/public/manager/src/browser-panel/browser-history-store.ts +80 -0
  92. package/public/manager/src/browser-panel/browser-panel.css +141 -24
  93. package/public/manager/src/components/CommandBar.tsx +17 -5
  94. package/public/manager/src/components/InstanceGroups.tsx +143 -23
  95. package/public/manager/src/components/InstanceNavigator.tsx +55 -2
  96. package/public/manager/src/components/InstanceRow.tsx +51 -3
  97. package/public/manager/src/components/SidebarRail.tsx +10 -10
  98. package/public/manager/src/components/SidebarResizeHandle.tsx +22 -0
  99. package/public/manager/src/components/WorkspaceLayout.tsx +22 -8
  100. package/public/manager/src/components/instance-row-status.ts +48 -0
  101. package/public/manager/src/components/sidebar-keyboard.ts +129 -0
  102. package/public/manager/src/dashboard-settings/DashboardSettingsWorkspace.tsx +42 -0
  103. package/public/manager/src/help/helpContent.tsx +1 -0
  104. package/public/manager/src/hooks/useSidebarGroupCollapse.ts +46 -0
  105. package/public/manager/src/hooks/useSidebarWidth.ts +88 -0
  106. package/public/manager/src/hooks/useWindowFullscreen.ts +23 -0
  107. package/public/manager/src/manager-components.css +206 -31
  108. package/public/manager/src/manager-layout.css +30 -19
  109. package/public/manager/src/manager-p0-1-1.css +72 -36
  110. package/public/manager/src/manager-polish.css +6 -45
  111. package/public/manager/src/manager-shortcut-runner.ts +37 -1
  112. package/public/manager/src/manager-shortcuts.ts +21 -0
  113. package/public/manager/src/manager-tokens.css +190 -127
  114. package/public/manager/src/panels/PanelResizer.tsx +18 -2
  115. package/public/manager/src/panels/desktop-bridge.ts +12 -1
  116. package/public/manager/src/panels/panels.css +32 -32
  117. package/public/manager/src/settings/pages/Agent.tsx +26 -0
  118. package/public/manager/src/settings/pages/components/agent/agent-meta.ts +6 -5
  119. package/public/manager/src/styles.css +15 -7
  120. package/public/manager/src/types.ts +5 -1
  121. package/public/dist/assets/BrowserPanel-CsYKexNf.js +0 -3
  122. package/public/dist/assets/BrowserPanel-DpntR8EJ.css +0 -1
  123. package/public/dist/assets/agent-meta-_Idad3qq.js +0 -1
  124. package/public/dist/assets/bgtask-badge-CidVvjWb.js +0 -1
  125. package/public/dist/assets/constants-DK-21dMG.js +0 -1
  126. package/public/dist/assets/iframe-renderer-aFXUtTXX.js +0 -1
  127. package/public/dist/assets/manager-CwqrZ2MH.js +0 -13
  128. package/public/dist/assets/manager-LkGs6qEJ.css +0 -1
  129. package/public/dist/assets/memory-BcZdf_oz.js +0 -1
  130. package/public/dist/assets/message-history-DrEPUdTy.js +0 -1
  131. package/public/dist/assets/settings-core-Cm-CmVLA.js +0 -1
  132. package/public/dist/assets/settings-mbGEtP5B.js +0 -1
  133. package/public/dist/assets/skills-G8KUeHBV.js +0 -1
  134. package/public/dist/assets/slash-commands-BYv3yboz.js +0 -1
  135. package/public/dist/assets/ui-yrn3s72A.js +0 -1
@@ -16,10 +16,10 @@
16
16
  .browser-tab-strip {
17
17
  display: flex;
18
18
  align-items: center;
19
- gap: 3px;
19
+ gap: 4px;
20
20
  min-width: 0;
21
21
  min-height: 32px;
22
- padding: 4px 6px 0;
22
+ padding: 5px 6px 0;
23
23
  overflow-x: auto;
24
24
  border-bottom: 1px solid var(--border-subtle);
25
25
  background: var(--bg-panel);
@@ -45,14 +45,34 @@
45
45
  .browser-tab {
46
46
  all: unset;
47
47
  cursor: pointer;
48
+ display: inline-flex;
49
+ align-items: center;
50
+ gap: 6px;
48
51
  flex: 1 1 auto;
49
52
  min-width: 0;
50
53
  padding: 4px 4px 4px 7px;
51
54
  color: var(--text-dim);
52
55
  font-size: 11px;
53
56
  }
57
+ .browser-tab-favicon {
58
+ flex: 0 0 auto;
59
+ width: 16px;
60
+ height: 16px;
61
+ border-radius: 3px;
62
+ object-fit: contain;
63
+ background: var(--bg-raised);
64
+ }
65
+ .browser-tab-favicon.is-fallback {
66
+ display: inline-flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ font-size: 9px;
70
+ font-weight: 600;
71
+ color: var(--text-primary);
72
+ background: var(--sidebar-row-hover);
73
+ }
54
74
  .browser-tab-item.is-active .browser-tab {
55
- color: var(--text-base);
75
+ color: var(--text-primary);
56
76
  }
57
77
  .browser-tab-title {
58
78
  display: block;
@@ -84,8 +104,8 @@
84
104
  .browser-tab-add:focus-visible,
85
105
  .browser-collapse-button:hover,
86
106
  .browser-collapse-button:focus-visible {
87
- color: var(--text-base);
88
- background: var(--bg-hover);
107
+ color: var(--text-primary);
108
+ background: var(--sidebar-row-hover);
89
109
  }
90
110
  .browser-tab-add {
91
111
  margin-left: 1px;
@@ -120,7 +140,7 @@
120
140
  font-size: 13px;
121
141
  color: var(--text-dim);
122
142
  }
123
- .browser-nav-btn:hover { background: var(--bg-hover); color: var(--text-base); }
143
+ .browser-nav-btn:hover { background: var(--sidebar-row-hover); color: var(--text-primary); }
124
144
  .browser-nav-btn:disabled {
125
145
  cursor: default;
126
146
  opacity: 0.38;
@@ -136,23 +156,23 @@
136
156
  border: 1px solid var(--border-subtle);
137
157
  border-radius: 4px;
138
158
  background: var(--canvas-deep);
139
- color: var(--text-base);
159
+ color: var(--text-primary);
140
160
  font-size: 11px;
141
- outline: none;
142
161
  }
143
- .browser-url-input:focus { border-color: var(--accent-primary); }
162
+ .browser-url-input:focus { border-color: var(--accent-primary); outline: none; }
163
+ .browser-url-input:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }
144
164
  .browser-go-btn {
145
165
  all: unset;
146
166
  cursor: pointer;
147
167
  padding: 3px 8px;
148
168
  border-radius: 4px;
149
169
  border: 1px solid var(--border-subtle);
150
- color: var(--text-base);
170
+ color: var(--text-primary);
151
171
  background: var(--bg-raised);
152
172
  font-size: 11px;
153
173
  }
154
174
  .browser-go-btn:hover {
155
- background: var(--bg-hover);
175
+ background: var(--sidebar-row-hover);
156
176
  border-color: var(--border-strong);
157
177
  }
158
178
  .browser-toolbar [data-tooltip] {
@@ -170,7 +190,7 @@
170
190
  border: 1px solid var(--border-strong);
171
191
  border-radius: 5px;
172
192
  background: var(--bg-raised);
173
- color: var(--text-base);
193
+ color: var(--text-primary);
174
194
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
175
195
  font-size: 11px;
176
196
  line-height: 1.2;
@@ -196,6 +216,90 @@
196
216
  color: var(--status-error);
197
217
  background: color-mix(in srgb, var(--status-error) 10%, transparent);
198
218
  }
219
+ .browser-loading-track {
220
+ flex-shrink: 0;
221
+ height: 2px;
222
+ overflow: hidden;
223
+ background: transparent;
224
+ }
225
+ .browser-loading-bar {
226
+ height: 2px;
227
+ width: 100%;
228
+ transform-origin: left center;
229
+ background: var(--accent-primary);
230
+ box-shadow: 0 0 6px 1px var(--accent-primary);
231
+ opacity: 0;
232
+ transform: scaleX(1);
233
+ transition: transform 150ms ease-out, opacity 150ms ease-out 220ms;
234
+ }
235
+ .browser-loading-bar[data-loading="true"] {
236
+ opacity: 1;
237
+ animation: browser-loading-progress 5.3s cubic-bezier(0.1, 0.5, 0.2, 1) forwards;
238
+ }
239
+ @keyframes browser-loading-progress {
240
+ from { transform: scaleX(0.04); }
241
+ to { transform: scaleX(0.9); }
242
+ }
243
+ @media (prefers-reduced-motion: reduce) {
244
+ .browser-loading-bar,
245
+ .browser-loading-bar[data-loading="true"] {
246
+ transition: none;
247
+ animation: none;
248
+ }
249
+ .browser-loading-bar[data-loading="true"] {
250
+ transform: scaleX(0.9);
251
+ opacity: 1;
252
+ }
253
+ }
254
+ .browser-history-empty {
255
+ position: absolute;
256
+ inset: 0;
257
+ z-index: 4;
258
+ overflow: auto;
259
+ padding: 16px;
260
+ background: var(--canvas-deep);
261
+ }
262
+ .browser-history-item {
263
+ all: unset;
264
+ box-sizing: border-box;
265
+ display: grid;
266
+ gap: 2px;
267
+ width: 100%;
268
+ padding: 8px 10px;
269
+ border-radius: var(--radius-sm);
270
+ color: var(--text-primary);
271
+ font-size: 12px;
272
+ cursor: pointer;
273
+ }
274
+ .browser-history-item:hover,
275
+ .browser-history-item:focus-visible {
276
+ background: var(--sidebar-row-hover);
277
+ }
278
+ .browser-history-item:focus-visible { outline: var(--focus-ring); outline-offset: -2px; }
279
+ .browser-history-title {
280
+ font-weight: 600;
281
+ overflow: hidden;
282
+ text-overflow: ellipsis;
283
+ white-space: nowrap;
284
+ }
285
+ .browser-history-url {
286
+ color: var(--text-dim);
287
+ font-size: 11px;
288
+ font-family: var(--font-mono);
289
+ overflow: hidden;
290
+ text-overflow: ellipsis;
291
+ white-space: nowrap;
292
+ }
293
+ .browser-history-empty::before {
294
+ content: 'Recent';
295
+ display: block;
296
+ margin: 0 10px 8px;
297
+ color: var(--text-dim);
298
+ font-size: 10px;
299
+ font-weight: 700;
300
+ letter-spacing: 0.04em;
301
+ text-transform: uppercase;
302
+ }
199
303
  .browser-webview-stack {
200
304
  display: flex;
201
305
  flex: 1 1 0;
@@ -243,7 +347,7 @@
243
347
  }
244
348
 
245
349
  .browser-external-title {
246
- color: var(--text-base);
350
+ color: var(--text-primary);
247
351
  font-size: 13px;
248
352
  font-weight: 600;
249
353
  }
@@ -262,14 +366,14 @@
262
366
  padding: 5px 9px;
263
367
  border: 1px solid var(--border-subtle);
264
368
  border-radius: 4px;
265
- color: var(--text-base);
369
+ color: var(--text-primary);
266
370
  background: var(--bg-raised);
267
371
  font-size: 11px;
268
372
  }
269
373
 
270
374
  .browser-external-open:hover,
271
375
  .browser-external-open:focus-visible {
272
- background: var(--bg-hover);
376
+ background: var(--sidebar-row-hover);
273
377
  border-color: var(--border-strong);
274
378
  }
275
379
 
@@ -297,8 +401,8 @@
297
401
  }
298
402
  .browser-action-btn:hover,
299
403
  .browser-action-btn:focus-visible {
300
- background: var(--bg-hover);
301
- color: var(--text-base);
404
+ background: var(--sidebar-row-hover);
405
+ color: var(--text-primary);
302
406
  }
303
407
  .browser-action-btn:focus-visible {
304
408
  outline: 2px solid var(--focus-ring);
@@ -306,7 +410,7 @@
306
410
  }
307
411
  .browser-action-btn.is-active {
308
412
  background: var(--bg-raised);
309
- color: var(--text-base);
413
+ color: var(--text-primary);
310
414
  }
311
415
  .browser-action-btn.is-shared {
312
416
  color: var(--accent, #7aa2ff);
@@ -339,13 +443,26 @@
339
443
  width: 100%;
340
444
  padding: 7px 10px;
341
445
  border-radius: 6px;
342
- color: var(--text-base);
446
+ color: var(--text-primary);
343
447
  font-size: 12px;
344
448
  cursor: pointer;
345
449
  }
346
450
  .browser-more-item:hover,
347
451
  .browser-more-item:focus-visible {
348
- background: var(--bg-hover);
452
+ background: var(--sidebar-row-hover);
453
+ }
454
+ .browser-more-zoom {
455
+ display: flex;
456
+ align-items: center;
457
+ gap: 4px;
458
+ padding: 4px;
459
+ }
460
+ .browser-more-zoom-label {
461
+ min-width: 3.5ch;
462
+ text-align: center;
463
+ font-variant-numeric: tabular-nums;
464
+ font-size: 12px;
465
+ color: var(--text-dim);
349
466
  }
350
467
 
351
468
  /* Transparent click-catcher for element inspect picking */
@@ -441,7 +558,7 @@
441
558
  border: 1px solid var(--border-subtle);
442
559
  border-radius: 8px;
443
560
  background: var(--bg-panel);
444
- color: var(--text-base);
561
+ color: var(--text-primary);
445
562
  font-size: 12px;
446
563
  }
447
564
  .browser-comment-actions {
@@ -457,8 +574,8 @@
457
574
  padding: 5px 10px;
458
575
  border-radius: 6px;
459
576
  font-size: 12px;
460
- color: var(--text-base);
461
- background: var(--bg-hover);
577
+ color: var(--text-primary);
578
+ background: var(--sidebar-row-hover);
462
579
  cursor: pointer;
463
580
  }
464
581
  .browser-comment-save:disabled {
@@ -522,7 +639,7 @@
522
639
  font-size: 11px;
523
640
  }
524
641
  .browser-picked-selector {
525
- font-family: var(--mono, ui-monospace, monospace);
642
+ font-family: var(--font-mono);
526
643
  color: #6fa8dc;
527
644
  overflow: hidden;
528
645
  text-overflow: ellipsis;
@@ -19,8 +19,10 @@ export function CommandBar(props: CommandBarProps) {
19
19
  return (
20
20
  <CommandCenter
21
21
  mobileMenuButton={(
22
- <button className="drawer-trigger" type="button" onClick={props.onOpenDrawer} aria-label="Open sidebar">
23
- ☰
22
+ <button className="drawer-trigger command-icon-button" type="button" onClick={props.onOpenDrawer} aria-label="Open sidebar">
23
+ <svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true" focusable="false">
24
+ <path d="M4 6h12M4 10h12M4 14h12" />
25
+ </svg>
24
26
  </button>
25
27
  )}
26
28
  title={(
@@ -30,7 +32,7 @@ export function CommandBar(props: CommandBarProps) {
30
32
  </h1>
31
33
  )}
32
34
  search={(
33
- <div className="search-input-wrapper">
35
+ <div className="search-input-wrapper is-ghost">
34
36
  <svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
35
37
  <input
36
38
  value={props.query}
@@ -54,8 +56,18 @@ export function CommandBar(props: CommandBarProps) {
54
56
  <span aria-hidden="true">⌘K</span>
55
57
  </button>
56
58
  <ThemeSwitch theme={props.theme} onChange={props.onThemeChange} />
57
- <button type="button" onClick={props.onRefresh} disabled={props.loading}>
58
- {props.loading ? 'Scanning' : 'Refresh'}
59
+ <button
60
+ type="button"
61
+ className="command-icon-button"
62
+ onClick={props.onRefresh}
63
+ disabled={props.loading}
64
+ aria-label={props.loading ? 'Scanning' : 'Refresh'}
65
+ title={props.loading ? 'Scanning' : 'Refresh'}
66
+ >
67
+ <svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" focusable="false">
68
+ <path d="M16 10a6 6 0 1 1-2-4.5" />
69
+ <path d="M16 4v4h-4" />
70
+ </svg>
59
71
  </button>
60
72
  </div>
61
73
  )}
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import { useEffect, useState, useSyncExternalStore, type ReactNode } from 'react';
2
2
  import { InstanceRow } from './InstanceRow';
3
3
  import type {
4
4
  DashboardInstance,
@@ -6,6 +6,16 @@ import type {
6
6
  DashboardLifecycleAction,
7
7
  DashboardProfile,
8
8
  } from '../types';
9
+ import { comparePinnedThenLabel } from './instance-row-status';
10
+ import { useSidebarGroupCollapse } from '../hooks/useSidebarGroupCollapse';
11
+ import {
12
+ SETTLED_TAIL_INITIAL_COUNT,
13
+ SETTLED_TAIL_PAGE_COUNT,
14
+ getJumpHintsVisible,
15
+ isSettledStatus,
16
+ pageSettledPorts,
17
+ subscribeJumpHints,
18
+ } from './sidebar-keyboard';
9
19
 
10
20
  type InstanceGroupsProps = {
11
21
  instances: DashboardInstance[];
@@ -21,6 +31,7 @@ type InstanceGroupsProps = {
21
31
  showInlineLabelEditor?: boolean;
22
32
  showSidebarRuntimeLine?: boolean;
23
33
  showSelectedRowActions?: boolean;
34
+ density?: 'compact' | 'comfortable' | 'rail';
24
35
  /** Session disclosure for the Active row (devlog 260806 D2). */
25
36
  activeSessionCount?: number;
26
37
  activeSessionsOpen?: boolean;
@@ -35,6 +46,18 @@ type InstanceGroupsProps = {
35
46
  onLifecycle: (action: DashboardLifecycleAction, instance: DashboardInstance) => void;
36
47
  };
37
48
 
49
+ type InstanceGroupSectionProps = {
50
+ group: DashboardInstanceGroup;
51
+ props: InstanceGroupsProps;
52
+ profileMap: Map<string, DashboardProfile>;
53
+ collapsed: boolean;
54
+ onToggle: () => void;
55
+ jumpStartIndex: number;
56
+ showJumpHints: boolean;
57
+ settledVisibleCount: number;
58
+ onShowMoreSettled: () => void;
59
+ };
60
+
38
61
  function withoutPorts(instances: DashboardInstance[], used: Set<number>): DashboardInstance[] {
39
62
  return instances.filter(instance => !used.has(instance.port));
40
63
  }
@@ -57,8 +80,14 @@ function groupInstances(instances: DashboardInstance[], selectedPort: number | n
57
80
 
58
81
  const remaining = withoutPorts(instances, used);
59
82
  const running = remaining.filter(instance => instance.status === 'online');
60
- const attention = remaining.filter(instance => ['timeout', 'error', 'unknown'].includes(instance.status));
61
- const offline = remaining.filter(instance => instance.status === 'offline');
83
+ const attention = remaining.filter(instance => instance.status === 'error');
84
+ const settled = remaining.filter(instance => isSettledStatus(instance.status));
85
+ const labelOf = (instance: Pick<DashboardInstance, 'label' | 'port'>) => instance.label || String(instance.port);
86
+ favorites.sort((a, b) => comparePinnedThenLabel(a, b, labelOf));
87
+ running.sort((a, b) => comparePinnedThenLabel(a, b, labelOf));
88
+ attention.sort((a, b) => comparePinnedThenLabel(a, b, labelOf));
89
+ settled.sort((a, b) => comparePinnedThenLabel(a, b, labelOf));
90
+ for (const group of userGroups.values()) group.sort((a, b) => comparePinnedThenLabel(a, b, labelOf));
62
91
 
63
92
  const groups: DashboardInstanceGroup[] = [
64
93
  { id: 'active', label: 'Active', instances: selected },
@@ -70,7 +99,7 @@ function groupInstances(instances: DashboardInstance[], selectedPort: number | n
70
99
  })),
71
100
  { id: 'running', label: 'Running', instances: running },
72
101
  { id: 'attention', label: 'Attention', instances: attention },
73
- { id: 'offline', label: 'Offline', instances: offline },
102
+ { id: 'settled', label: 'Settled', instances: settled },
74
103
  ];
75
104
 
76
105
  return groups.filter(group => group.instances.length > 0);
@@ -81,6 +110,7 @@ function renderInstanceRow(
81
110
  instance: DashboardInstance,
82
111
  profile?: DashboardProfile,
83
112
  priority: 'active' | 'normal' = 'normal',
113
+ jumpHint?: string | null,
84
114
  ) {
85
115
  return (
86
116
  <InstanceRow
@@ -97,6 +127,7 @@ function renderInstanceRow(
97
127
  {...(props.showInlineLabelEditor !== undefined ? { showInlineLabelEditor: props.showInlineLabelEditor } : {})}
98
128
  {...(props.showSidebarRuntimeLine !== undefined ? { showRuntimeLine: props.showSidebarRuntimeLine } : {})}
99
129
  {...(props.showSelectedRowActions !== undefined ? { showSelectedActions: props.showSelectedRowActions } : {})}
130
+ {...(props.density !== undefined ? { density: props.density } : {})}
100
131
  {...(priority === 'active' && props.activeSessionCount !== undefined ? { sessionCount: props.activeSessionCount } : {})}
101
132
  {...(priority === 'active' && props.activeSessionsOpen !== undefined ? { sessionsOpen: props.activeSessionsOpen } : {})}
102
133
  {...(priority === 'active' && props.onToggleActiveSessions ? { onToggleSessions: props.onToggleActiveSessions } : {})}
@@ -108,51 +139,140 @@ function renderInstanceRow(
108
139
  onMarkActivitySeen={props.onMarkActivitySeen}
109
140
  onInstanceLabelSave={props.onInstanceLabelSave}
110
141
  onLifecycle={props.onLifecycle}
142
+ jumpHint={jumpHint ?? null}
111
143
  />
112
144
  );
113
145
  }
114
146
 
115
- function renderRows(
116
- props: InstanceGroupsProps,
117
- instances: DashboardInstance[],
118
- profileMap: Map<string, DashboardProfile> = new Map(),
119
- ) {
120
- return groupInstances(instances, props.selectedPort).map(group => (
147
+ function visibleGroupInstances(
148
+ group: DashboardInstanceGroup,
149
+ selectedPort: number | null,
150
+ collapsed: boolean,
151
+ settledVisibleCount: number,
152
+ ): DashboardInstance[] {
153
+ const selected = group.instances.find(instance => instance.port === selectedPort);
154
+ const pagedSettled = group.id === 'settled'
155
+ ? pageSettledPorts(
156
+ group.instances.map(instance => instance.port),
157
+ settledVisibleCount,
158
+ selectedPort,
159
+ )
160
+ : [];
161
+ const settledByPort = new Map(group.instances.map(instance => [instance.port, instance]));
162
+ const expandedInstances = group.id === 'settled'
163
+ ? pagedSettled.map(port => settledByPort.get(port)).filter((instance): instance is DashboardInstance => instance != null)
164
+ : group.instances;
165
+ return group.id === 'active' || !collapsed
166
+ ? expandedInstances
167
+ : selected ? [selected] : [];
168
+ }
169
+
170
+ function InstanceGroupSection(section: InstanceGroupSectionProps) {
171
+ const { group, props, profileMap, collapsed, onToggle, jumpStartIndex, showJumpHints, settledVisibleCount, onShowMoreSettled } = section;
172
+ const settledExpanded = group.id !== 'settled' || !collapsed;
173
+ const visible = visibleGroupInstances(group, props.selectedPort, collapsed, settledVisibleCount);
174
+ const hiddenSettled = group.id === 'settled' && settledExpanded
175
+ ? Math.max(0, group.instances.length - settledVisibleCount)
176
+ : 0;
177
+ const header = group.id === 'active' ? (
178
+ <div className="instance-group-header">
179
+ <span>{group.label}</span>
180
+ <strong>{group.instances.length}</strong>
181
+ </div>
182
+ ) : (
183
+ <button
184
+ type="button"
185
+ className="instance-group-header instance-group-toggle"
186
+ aria-expanded={!collapsed}
187
+ aria-controls={`instance-group-body-${group.id}`}
188
+ onClick={onToggle}
189
+ >
190
+ <span>
191
+ <svg className="instance-group-chevron" viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M3.5 6l4.5 4.5L12.5 6" /></svg>
192
+ {' '}{group.label}
193
+ </span>
194
+ <strong>{group.instances.length}</strong>
195
+ </button>
196
+ );
197
+
198
+ return (
121
199
  <section className="instance-group" key={group.id} aria-label={`${group.label} instances`}>
122
- <div className="instance-group-header">
123
- <span>{group.label}</span>
124
- <strong>{group.instances.length}</strong>
200
+ {header}
201
+ <div id={`instance-group-body-${group.id}`} hidden={collapsed && visible.length === 0}>
202
+ {visible.map((instance, rowIndex) => {
203
+ const jumpIndex = jumpStartIndex + rowIndex;
204
+ const jumpHint = showJumpHints && jumpIndex < 9 ? String(jumpIndex + 1) : null;
205
+ return renderInstanceRow(
206
+ props,
207
+ instance,
208
+ instance.profileId ? profileMap.get(instance.profileId) : undefined,
209
+ group.id === 'active' ? 'active' : 'normal',
210
+ jumpHint,
211
+ );
212
+ })}
213
+ {group.id === 'active' && visible[0] ? props.renderActiveSessionList?.(visible[0].port) : null}
214
+ {group.id === 'settled' && settledExpanded && hiddenSettled > 0 ? (
215
+ <button
216
+ type="button"
217
+ className="instance-settled-more"
218
+ onClick={onShowMoreSettled}
219
+ >
220
+ Show {Math.min(hiddenSettled, SETTLED_TAIL_PAGE_COUNT)} more
221
+ </button>
222
+ ) : null}
125
223
  </div>
126
- {group.instances.map(instance => renderInstanceRow(
127
- props,
128
- instance,
129
- instance.profileId ? profileMap.get(instance.profileId) : undefined,
130
- group.id === 'active' ? 'active' : 'normal',
131
- ))}
132
- {group.id === 'active' && group.instances[0] ? props.renderActiveSessionList?.(group.instances[0].port) : null}
133
224
  </section>
134
- ));
225
+ );
135
226
  }
136
227
 
137
228
  export function InstanceGroups(props: InstanceGroupsProps) {
138
229
  const groups = groupInstances(props.instances, props.selectedPort);
139
230
  const profileMap = new Map((props.profiles || []).map(profile => [profile.profileId, profile]));
231
+ const { isCollapsed, toggle: toggleGroup } = useSidebarGroupCollapse();
232
+ const showJumpHints = useSyncExternalStore(subscribeJumpHints, getJumpHintsVisible);
233
+ const [settledVisibleCount, setSettledVisibleCount] = useState(SETTLED_TAIL_INITIAL_COUNT);
234
+
235
+ useEffect(() => {
236
+ setSettledVisibleCount(SETTLED_TAIL_INITIAL_COUNT);
237
+ }, [props.instances]);
140
238
 
141
239
  if (groups.length === 0 && profileMap.size === 0) {
142
240
  return <section className="state">No matching instances found.</section>;
143
241
  }
144
242
 
243
+ let jumpCursor = 0;
244
+ const sections = groups.map(group => {
245
+ const collapsed = group.id === 'active' ? false : isCollapsed(group.id);
246
+ const visibleCount = visibleGroupInstances(group, props.selectedPort, collapsed, settledVisibleCount).length;
247
+ const jumpStartIndex = jumpCursor;
248
+ jumpCursor += visibleCount;
249
+ return (
250
+ <InstanceGroupSection
251
+ key={group.id}
252
+ group={group}
253
+ props={props}
254
+ profileMap={profileMap}
255
+ collapsed={collapsed}
256
+ onToggle={() => toggleGroup(group.id)}
257
+ jumpStartIndex={jumpStartIndex}
258
+ showJumpHints={showJumpHints}
259
+ settledVisibleCount={settledVisibleCount}
260
+ onShowMoreSettled={() => setSettledVisibleCount(count => count + SETTLED_TAIL_PAGE_COUNT)}
261
+ />
262
+ );
263
+ });
264
+
145
265
  if (profileMap.size > 0) {
146
266
  return (
147
267
  <div className="instance-groups profile-instance-groups is-profile-merged">
148
- {renderRows(props, props.instances, profileMap)}
268
+ {sections}
149
269
  </div>
150
270
  );
151
271
  }
152
272
 
153
273
  return (
154
274
  <div className="instance-groups">
155
- {renderRows(props, props.instances)}
275
+ {sections}
156
276
  </div>
157
277
  );
158
278
  }
@@ -1,14 +1,45 @@
1
- import type { ReactNode } from 'react';
1
+ import { useEffect, type ReactNode } from 'react';
2
2
  import type { DashboardInstance } from '../types';
3
+ import {
4
+ INSTANCE_JUMP_HINT_SHOW_DELAY_MS,
5
+ createJumpHintVisibilityController,
6
+ handleInstanceListKeyDown,
7
+ setJumpHintsVisible,
8
+ shouldArmJumpHint,
9
+ } from './sidebar-keyboard';
3
10
 
4
11
  type InstanceNavigatorProps = {
5
12
  active: DashboardInstance | null;
6
13
  hiddenCount: number;
7
14
  collapsed: boolean;
8
15
  children: ReactNode;
16
+ query: string;
17
+ onQueryChange: (value: string) => void;
18
+ onSelectPort: (port: number) => void;
9
19
  };
10
20
 
11
21
  export function InstanceNavigator(props: InstanceNavigatorProps) {
22
+ useEffect(() => {
23
+ const controller = createJumpHintVisibilityController({
24
+ delayMs: INSTANCE_JUMP_HINT_SHOW_DELAY_MS,
25
+ onVisibilityChange: setJumpHintsVisible,
26
+ });
27
+ const onKeyDown = (event: KeyboardEvent) => {
28
+ controller.sync(shouldArmJumpHint(event));
29
+ };
30
+ const onKeyUp = (event: KeyboardEvent) => {
31
+ controller.sync(shouldArmJumpHint(event));
32
+ };
33
+ window.addEventListener('keydown', onKeyDown);
34
+ window.addEventListener('keyup', onKeyUp);
35
+ return () => {
36
+ window.removeEventListener('keydown', onKeyDown);
37
+ window.removeEventListener('keyup', onKeyUp);
38
+ controller.dispose();
39
+ setJumpHintsVisible(false);
40
+ };
41
+ }, []);
42
+
12
43
  if (props.collapsed) {
13
44
  return <div className="instance-navigator is-collapsed">{props.children}</div>;
14
45
  }
@@ -21,8 +52,30 @@ export function InstanceNavigator(props: InstanceNavigatorProps) {
21
52
  <strong>{props.active ? `:${props.active.port}` : 'No active target'}</strong>
22
53
  </div>
23
54
  <span>{props.hiddenCount} hidden</span>
55
+ <label className="instance-navigator-search">
56
+ <span className="visually-hidden">Search instances</span>
57
+ <input
58
+ id="manager-sidebar-search"
59
+ value={props.query}
60
+ placeholder="Search instances"
61
+ aria-label="Search instances"
62
+ onChange={event => props.onQueryChange(event.currentTarget.value)}
63
+ onKeyDown={event => {
64
+ if (event.nativeEvent.isComposing || event.keyCode === 229) return;
65
+ if (event.key === 'Escape') {
66
+ event.preventDefault();
67
+ props.onQueryChange('');
68
+ }
69
+ }}
70
+ />
71
+ </label>
24
72
  </header>
25
- <div className="instance-navigator-scroll">{props.children}</div>
73
+ <div
74
+ className="instance-navigator-scroll"
75
+ onKeyDown={event => handleInstanceListKeyDown(event.nativeEvent, event.currentTarget, props.onSelectPort)}
76
+ >
77
+ {props.children}
78
+ </div>
26
79
  </section>
27
80
  );
28
81
  }