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
@@ -1,6 +1,7 @@
1
- import { useState } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
2
  import type { FormEvent, MouseEvent } from 'react';
3
3
  import type { DashboardInstance, DashboardLifecycleAction, DashboardProfile } from '../types';
4
+ import { composeInstanceRowTitle, formatWorkingDurationLabel, resolveInstanceRowStatus } from './instance-row-status';
4
5
 
5
6
  type InstanceRowProps = {
6
7
  instance: DashboardInstance;
@@ -28,6 +29,7 @@ type InstanceRowProps = {
28
29
  onMarkActivitySeen: (port: number) => void;
29
30
  onInstanceLabelSave: (port: number, label: string | null) => Promise<void>;
30
31
  onLifecycle: (action: DashboardLifecycleAction, instance: DashboardInstance) => void;
32
+ jumpHint?: string | null;
31
33
  };
32
34
 
33
35
  const TRANSITION_LABELS: Record<DashboardLifecycleAction, string> = {
@@ -63,6 +65,21 @@ const ChevronIcon = ({ open }: { open: boolean }) => (
63
65
  </svg>
64
66
  );
65
67
 
68
+ function WorkingDuration(props: { startedAtMs: number | null }) {
69
+ const [, setTick] = useState(0);
70
+ useEffect(() => {
71
+ if (props.startedAtMs == null) return undefined;
72
+ const id = window.setInterval(() => setTick(tick => tick + 1), 1000);
73
+ return () => window.clearInterval(id);
74
+ }, [props.startedAtMs]);
75
+ if (props.startedAtMs == null) return null;
76
+ return (
77
+ <span className="instance-row-working-duration">
78
+ {formatWorkingDurationLabel(Date.now() - props.startedAtMs)}
79
+ </span>
80
+ );
81
+ }
82
+
66
83
  export function InstanceRow(props: InstanceRowProps) {
67
84
  const lifecycle = props.instance.lifecycle;
68
85
  const reason = lifecycle?.reason || props.instance.healthReason || 'ok';
@@ -76,7 +93,24 @@ export function InstanceRow(props: InstanceRowProps) {
76
93
  }
77
94
 
78
95
  const transitionLabel = props.transitioning ? TRANSITION_LABELS[props.transitioning] : null;
79
- const dotClass = `${statusClass(props.instance.status)}${transitionLabel ? ' is-transitioning' : ''}${props.agentBusy ? ' is-busy' : ''}`;
96
+ const rowStatus = resolveInstanceRowStatus(props.instance, {
97
+ busy: Boolean(props.agentBusy),
98
+ transitioning: props.transitioning || null,
99
+ });
100
+ const startedAtRef = useRef<number | null>(null);
101
+ if (rowStatus === 'working') {
102
+ if (startedAtRef.current == null) startedAtRef.current = Date.now();
103
+ } else {
104
+ startedAtRef.current = null;
105
+ }
106
+ const statusLabel = rowStatus === 'transitioning' && transitionLabel
107
+ ? transitionLabel
108
+ : rowStatus === 'working' ? 'Working'
109
+ : rowStatus === 'offline' ? 'Offline'
110
+ : rowStatus === 'attention' ? 'Attention'
111
+ : 'Online';
112
+ const hideStatusLine = props.density === 'compact' || props.density === 'rail';
113
+ const dotClass = `${statusClass(props.instance.status)}${transitionLabel ? ' is-transitioning' : ''}${props.agentBusy ? ' is-busy' : ''} is-${rowStatus}`;
80
114
  const primaryLabel = props.instance.label || props.profile?.label || props.label;
81
115
  async function submitLabel(event: FormEvent<HTMLFormElement>): Promise<void> {
82
116
  event.preventDefault();
@@ -94,10 +128,15 @@ export function InstanceRow(props: InstanceRowProps) {
94
128
  }
95
129
 
96
130
  return (
97
- <article className={`instance-row density-${props.density || 'comfortable'} priority-${props.priority || 'normal'} ${props.selected ? 'is-selected' : ''}${transitionLabel ? ' is-transitioning-row' : ''}`}>
131
+ <article
132
+ className={`instance-row density-${props.density || 'comfortable'} priority-${props.priority || 'normal'} ${props.selected ? 'is-selected' : ''}${transitionLabel ? ' is-transitioning-row' : ''} is-${rowStatus}`}
133
+ title={composeInstanceRowTitle(props.instance)}
134
+ aria-current={props.selected ? 'true' : undefined}
135
+ >
98
136
  <button
99
137
  className="instance-row-select"
100
138
  type="button"
139
+ data-instance-port={props.instance.port}
101
140
  aria-pressed={props.selected}
102
141
  onClick={() => {
103
142
  props.onSelect(props.instance);
@@ -106,6 +145,14 @@ export function InstanceRow(props: InstanceRowProps) {
106
145
  <div className="instance-row-main">
107
146
  <span className={dotClass} aria-label={props.instance.status} />
108
147
  <div className="instance-row-title">
148
+ {!hideStatusLine ? (
149
+ <div className="instance-row-status-line" data-status={rowStatus}>
150
+ <span className={`instance-row-status-pill is-${rowStatus} health-${props.instance.status}`}>
151
+ {statusLabel}
152
+ </span>
153
+ {rowStatus === 'working' ? <WorkingDuration startedAtMs={startedAtRef.current} /> : null}
154
+ </div>
155
+ ) : null}
109
156
  <div className="instance-row-title-line">
110
157
  <strong>{props.instance.favorite ? `Pinned ${primaryLabel}` : primaryLabel}</strong>
111
158
  {props.activityUnreadCount ? (
@@ -219,6 +266,7 @@ export function InstanceRow(props: InstanceRowProps) {
219
266
  </svg>
220
267
  </button>
221
268
  ) : null}
269
+ {props.jumpHint ? <span className="instance-jump-hint" aria-hidden="true">{props.jumpHint}</span> : null}
222
270
  {props.showSelectedActions !== false && (
223
271
  <div className="instance-actions">
224
272
  <button
@@ -18,11 +18,11 @@ function ChevronIcon({ direction }: { direction: 'left' | 'right' }) {
18
18
  <svg
19
19
  className="rail-collapse-chevron"
20
20
  viewBox="0 0 16 20"
21
- width="14"
22
- height="14"
21
+ width="18"
22
+ height="18"
23
23
  fill="none"
24
24
  stroke="currentColor"
25
- strokeWidth="1.6"
25
+ strokeWidth="1.5"
26
26
  strokeLinecap="round"
27
27
  strokeLinejoin="round"
28
28
  aria-hidden="true"
@@ -35,7 +35,7 @@ function ChevronIcon({ direction }: { direction: 'left' | 'right' }) {
35
35
 
36
36
  function MonitorIcon() {
37
37
  return (
38
- <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">
38
+ <svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" focusable="false">
39
39
  <rect x="3" y="4" width="14" height="10" rx="1.5" />
40
40
  <path d="M8 17h4M10 14v3" />
41
41
  </svg>
@@ -44,7 +44,7 @@ function MonitorIcon() {
44
44
 
45
45
  function NoteIcon() {
46
46
  return (
47
- <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">
47
+ <svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" focusable="false">
48
48
  <path d="M6 3h6l3 3v11H6z" />
49
49
  <path d="M12 3v4h3M8 10h5M8 13h5" />
50
50
  </svg>
@@ -53,7 +53,7 @@ function NoteIcon() {
53
53
 
54
54
  function BoardIcon() {
55
55
  return (
56
- <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">
56
+ <svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" focusable="false">
57
57
  <rect x="3" y="4" width="14" height="12" rx="1.5" />
58
58
  <path d="M7 4v12M13 4v12" />
59
59
  </svg>
@@ -62,7 +62,7 @@ function BoardIcon() {
62
62
 
63
63
  function ScheduleIcon() {
64
64
  return (
65
- <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">
65
+ <svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" focusable="false">
66
66
  <rect x="3" y="5" width="14" height="12" rx="1.5" />
67
67
  <path d="M3 9h14M7 3v4M13 3v4" />
68
68
  </svg>
@@ -71,7 +71,7 @@ function ScheduleIcon() {
71
71
 
72
72
  function ReminderIcon() {
73
73
  return (
74
- <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">
74
+ <svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" focusable="false">
75
75
  <path d="M5 4h10v13H5z" />
76
76
  <path d="M8 2v4M12 2v4M8 10h4M8 13h3" />
77
77
  </svg>
@@ -80,7 +80,7 @@ function ReminderIcon() {
80
80
 
81
81
  function SettingsIcon() {
82
82
  return (
83
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" focusable="false">
83
+ <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" focusable="false">
84
84
  <path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
85
85
  <circle cx="12" cy="12" r="3" />
86
86
  </svg>
@@ -89,7 +89,7 @@ function SettingsIcon() {
89
89
 
90
90
  function HelpIcon() {
91
91
  return (
92
- <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">
92
+ <svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" focusable="false">
93
93
  <circle cx="10" cy="10" r="7" />
94
94
  <path d="M7.6 7.6a2.4 2.4 0 0 1 4.8 0c0 1.4-1.6 1.7-2.2 2.5-.3.4-.3.8-.3 1.2" />
95
95
  <circle cx="10" cy="14.2" r="0.6" fill="currentColor" stroke="none" />
@@ -0,0 +1,22 @@
1
+ import { PanelResizer } from '../panels/PanelResizer';
2
+
3
+ type SidebarResizeHandleProps = {
4
+ width: number;
5
+ onDelta: (delta: number) => void;
6
+ onEnd: () => void;
7
+ onDoubleClick: () => void;
8
+ };
9
+
10
+ export function SidebarResizeHandle(props: SidebarResizeHandleProps) {
11
+ return (
12
+ <PanelResizer
13
+ direction="horizontal"
14
+ onDelta={props.onDelta}
15
+ onEnd={props.onEnd}
16
+ onDoubleClick={props.onDoubleClick}
17
+ className="sidebar-resize-handle"
18
+ ariaLabel="Resize sidebar"
19
+ ariaValueNow={props.width}
20
+ />
21
+ );
22
+ }
@@ -1,4 +1,6 @@
1
1
  import { useEffect, useState, type CSSProperties, type ReactNode } from 'react';
2
+ import { SidebarResizeHandle } from './SidebarResizeHandle';
3
+ import { SIDEBAR_COLLAPSED_WIDTH } from '../hooks/useSidebarWidth';
2
4
 
3
5
  type WorkspaceLayoutProps = {
4
6
  navigator: ReactNode;
@@ -19,11 +21,15 @@ type WorkspaceLayoutProps = {
19
21
  bottomPanelContent?: ReactNode;
20
22
  bottomPanelHeight?: number;
21
23
  bottomPanelOpen?: boolean;
24
+ sidebarWidth: number;
25
+ onSidebarWidthDelta: (delta: number) => void;
26
+ onSidebarWidthEnd: () => void;
27
+ onSidebarWidthReset: () => void;
22
28
  };
23
29
 
24
30
  type WorkspaceLayoutStyle = CSSProperties & {
25
31
  '--activity-dock-height': string;
26
- '--sidebar-width'?: string | undefined;
32
+ '--sidebar-width': string;
27
33
  '--right-panel-width'?: string | undefined;
28
34
  '--bottom-panel-height'?: string | undefined;
29
35
  };
@@ -48,13 +54,10 @@ function useViewportWidth(): number {
48
54
  return viewportWidth;
49
55
  }
50
56
 
51
- function clampRightPanelRenderWidth(width: number | undefined, sidebarCollapsed: boolean, viewportWidth: number): number {
57
+ function clampRightPanelRenderWidth(width: number | undefined, sidebarWidth: number, viewportWidth: number): number {
52
58
  const desired = typeof width === 'number' && Number.isFinite(width)
53
59
  ? Math.round(width)
54
60
  : 480;
55
- const sidebarWidth = sidebarCollapsed
56
- ? 56
57
- : viewportWidth >= 1440 ? 360 : 300;
58
61
  const maxByViewport = Math.max(
59
62
  RIGHT_PANEL_RENDER_MIN_WIDTH,
60
63
  viewportWidth - sidebarWidth - WORKSPACE_CENTER_MIN_WIDTH,
@@ -67,13 +70,14 @@ function clampRightPanelRenderWidth(width: number | undefined, sidebarCollapsed:
67
70
 
68
71
  export function WorkspaceLayout(props: WorkspaceLayoutProps) {
69
72
  const viewportWidth = useViewportWidth();
73
+ const sidebarRenderWidth = props.sidebarCollapsed ? SIDEBAR_COLLAPSED_WIDTH : props.sidebarWidth;
70
74
  const rightPanelWidth = props.rightPanelOpen
71
- ? clampRightPanelRenderWidth(props.rightPanelWidth, props.sidebarCollapsed, viewportWidth)
75
+ ? clampRightPanelRenderWidth(props.rightPanelWidth, sidebarRenderWidth, viewportWidth)
72
76
  : 0;
73
77
 
74
78
  const style: WorkspaceLayoutStyle = {
75
79
  '--activity-dock-height': `${props.inspectorHeight}px`,
76
- '--sidebar-width': props.sidebarCollapsed ? '56px' : undefined,
80
+ '--sidebar-width': props.sidebarCollapsed ? `${SIDEBAR_COLLAPSED_WIDTH}px` : `${props.sidebarWidth}px`,
77
81
  '--right-panel-width': props.rightPanelOpen
78
82
  ? `${rightPanelWidth}px`
79
83
  : props.sidePanel ? undefined : '0px',
@@ -97,7 +101,17 @@ export function WorkspaceLayout(props: WorkspaceLayoutProps) {
97
101
  return (
98
102
  <div className={cls} style={style}>
99
103
  {props.drawerOpen && <div className="drawer-backdrop" onClick={props.onCloseDrawer} />}
100
- <aside className="manager-sidebar" aria-label={props.navigatorLabel ?? 'Jaw instances'}>{props.navigator}</aside>
104
+ <aside className="manager-sidebar" aria-label={props.navigatorLabel ?? 'Jaw instances'}>
105
+ {props.navigator}
106
+ </aside>
107
+ {!props.sidebarCollapsed && viewportWidth >= 1024 && (
108
+ <SidebarResizeHandle
109
+ width={props.sidebarWidth}
110
+ onDelta={props.onSidebarWidthDelta}
111
+ onEnd={props.onSidebarWidthEnd}
112
+ onDoubleClick={props.onSidebarWidthReset}
113
+ />
114
+ )}
101
115
  <section className="manager-detail" aria-label="Manager workbench">{props.workbench}</section>
102
116
  <section className="manager-activity" aria-label="Manager inspector">{props.inspector}</section>
103
117
  {props.sidePanel && <aside className="manager-ceo-panel" aria-label="Jaw CEO console">{props.sidePanel}</aside>}
@@ -0,0 +1,48 @@
1
+ import type { DashboardInstance, DashboardLifecycleAction } from '../types';
2
+
3
+ export type InstanceRowStatus =
4
+ | 'working'
5
+ | 'online'
6
+ | 'offline'
7
+ | 'attention'
8
+ | 'transitioning';
9
+
10
+ export function resolveInstanceRowStatus(
11
+ instance: Pick<DashboardInstance, 'status'>,
12
+ flags: { busy?: boolean; transitioning?: DashboardLifecycleAction | null } = {},
13
+ ): InstanceRowStatus {
14
+ if (flags.transitioning) return 'transitioning';
15
+ if (flags.busy) return 'working';
16
+ if (instance.status === 'offline') return 'offline';
17
+ if (instance.status === 'online') return 'online';
18
+ return 'attention';
19
+ }
20
+
21
+ export function formatWorkingDurationLabel(elapsedMs: number): string {
22
+ const seconds = Number.isFinite(elapsedMs) ? Math.max(0, Math.floor(elapsedMs / 1000)) : 0;
23
+ if (seconds < 60) return `${seconds}s`;
24
+ const minutes = Math.floor(seconds / 60);
25
+ if (minutes < 60) return `${minutes}m`;
26
+ return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
27
+ }
28
+
29
+ export function composeInstanceRowTitle(instance: Pick<DashboardInstance, 'port' | 'homeDisplay' | 'currentCli' | 'currentModel' | 'version'>): string {
30
+ return [
31
+ `:${instance.port}`,
32
+ instance.homeDisplay || 'home n/a',
33
+ instance.currentCli || 'cli n/a',
34
+ instance.currentModel || 'model n/a',
35
+ `v${instance.version || 'n/a'}`,
36
+ ].join(' · ');
37
+ }
38
+
39
+ export function comparePinnedThenLabel<T extends Pick<DashboardInstance, 'favorite' | 'label' | 'port'>>(
40
+ a: T,
41
+ b: T,
42
+ labelOf: (instance: T) => string,
43
+ ): number {
44
+ const fav = Number(Boolean(b.favorite)) - Number(Boolean(a.favorite));
45
+ if (fav !== 0) return fav;
46
+ const byLabel = labelOf(a).localeCompare(labelOf(b), undefined, { sensitivity: 'base' });
47
+ return byLabel !== 0 ? byLabel : a.port - b.port;
48
+ }
@@ -0,0 +1,129 @@
1
+ export const INSTANCE_JUMP_HINT_SHOW_DELAY_MS = 200;
2
+ export const SETTLED_TAIL_INITIAL_COUNT = 10;
3
+ export const SETTLED_TAIL_PAGE_COUNT = 25;
4
+
5
+ export type TraverseDir = 'previous' | 'next';
6
+ export type InstanceJumpSelector = (port: number) => void;
7
+
8
+ export function resolveAdjacentPort(list: readonly number[], current: number | null, dir: TraverseDir): number | null {
9
+ if (list.length === 0) return null;
10
+ if (current == null) return dir === 'previous' ? (list[list.length - 1] ?? null) : (list[0] ?? null);
11
+ const idx = list.indexOf(current);
12
+ if (idx === -1) return null;
13
+ if (dir === 'previous') return idx > 0 ? (list[idx - 1] ?? null) : null;
14
+ return idx < list.length - 1 ? (list[idx + 1] ?? null) : null;
15
+ }
16
+
17
+ export function jumpInstanceIndexFromAction(action: string): number | null {
18
+ const match = /^jumpInstance([1-9])$/.exec(action);
19
+ return match ? Number(match[1]!) - 1 : null;
20
+ }
21
+
22
+ export function isSettledStatus(status: string): boolean {
23
+ return status === 'offline' || status === 'unknown' || status === 'timeout';
24
+ }
25
+
26
+ export function pageSettledPorts(settledPorts: readonly number[], visibleCount: number, selectedPort: number | null): number[] {
27
+ const visible = settledPorts.slice(0, Math.max(0, visibleCount));
28
+ if (selectedPort != null && settledPorts.includes(selectedPort) && !visible.includes(selectedPort)) {
29
+ return [...visible, selectedPort];
30
+ }
31
+ return visible;
32
+ }
33
+
34
+ export function createJumpHintVisibilityController(input: {
35
+ delayMs: number;
36
+ onVisibilityChange: (visible: boolean) => void;
37
+ setTimeoutFn?: typeof setTimeout;
38
+ clearTimeoutFn?: typeof clearTimeout;
39
+ }): { sync: (shouldShow: boolean) => void; dispose: () => void } {
40
+ const setTimeoutFn = input.setTimeoutFn ?? setTimeout;
41
+ const clearTimeoutFn = input.clearTimeoutFn ?? clearTimeout;
42
+ let isVisible = false;
43
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
44
+ const clearPending = (): void => {
45
+ if (timeoutId == null) return;
46
+ clearTimeoutFn(timeoutId);
47
+ timeoutId = null;
48
+ };
49
+ return {
50
+ sync(shouldShow: boolean): void {
51
+ if (!shouldShow) {
52
+ clearPending();
53
+ if (isVisible) {
54
+ isVisible = false;
55
+ input.onVisibilityChange(false);
56
+ }
57
+ return;
58
+ }
59
+ if (isVisible || timeoutId != null) return;
60
+ timeoutId = setTimeoutFn(() => {
61
+ timeoutId = null;
62
+ isVisible = true;
63
+ input.onVisibilityChange(true);
64
+ }, input.delayMs);
65
+ },
66
+ dispose(): void { clearPending(); },
67
+ };
68
+ }
69
+
70
+ let jumpHintsVisible = false;
71
+ const jumpHintListeners = new Set<() => void>();
72
+
73
+ export function subscribeJumpHints(cb: () => void): () => void {
74
+ jumpHintListeners.add(cb);
75
+ return () => { jumpHintListeners.delete(cb); };
76
+ }
77
+
78
+ export function getJumpHintsVisible(): boolean {
79
+ return jumpHintsVisible;
80
+ }
81
+
82
+ export function setJumpHintsVisible(next: boolean): void {
83
+ if (jumpHintsVisible === next) return;
84
+ jumpHintsVisible = next;
85
+ jumpHintListeners.forEach(listener => listener());
86
+ }
87
+
88
+ export function shouldArmJumpHint(event: Pick<KeyboardEvent, 'altKey' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'key'>): boolean {
89
+ return event.altKey && !event.metaKey && !event.ctrlKey && !event.shiftKey;
90
+ }
91
+
92
+ export function readRenderedInstancePorts(root: ParentNode): number[] {
93
+ return Array.from(root.querySelectorAll<HTMLElement>('[data-instance-port]'))
94
+ .map(el => Number(el.dataset['instancePort']))
95
+ .filter(port => Number.isInteger(port));
96
+ }
97
+
98
+ export function handleInstanceListKeyDown(event: KeyboardEvent, root: ParentNode, onSelectPort: (port: number) => void): void {
99
+ if (event.isComposing || event.keyCode === 229) return;
100
+ const buttons = Array.from(root.querySelectorAll<HTMLElement>('[data-instance-port]'));
101
+ if (buttons.length === 0) return;
102
+ const ports = buttons.map(el => Number(el.dataset['instancePort']));
103
+ const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
104
+ const currentEl = active?.closest('[data-instance-port]');
105
+ const currentPort = currentEl instanceof HTMLElement ? Number(currentEl.dataset['instancePort']) : null;
106
+ let next: number | null = null;
107
+ if (event.key === 'ArrowDown') next = resolveAdjacentPort(ports, currentPort, 'next');
108
+ else if (event.key === 'ArrowUp') next = resolveAdjacentPort(ports, currentPort, 'previous');
109
+ else if (event.key === 'Home') next = ports[0] ?? null;
110
+ else if (event.key === 'End') next = ports[ports.length - 1] ?? null;
111
+ else if (event.key === 'Enter' && currentPort != null) {
112
+ event.preventDefault();
113
+ onSelectPort(currentPort);
114
+ return;
115
+ }
116
+ if (next == null) return;
117
+ event.preventDefault();
118
+ buttons.find(el => Number(el.dataset['instancePort']) === next)?.focus();
119
+ }
120
+
121
+ let instanceJumpSelector: InstanceJumpSelector | null = null;
122
+
123
+ export function registerInstanceJumpSelector(fn: InstanceJumpSelector | null): void {
124
+ instanceJumpSelector = fn;
125
+ }
126
+
127
+ export function getInstanceJumpSelector(): InstanceJumpSelector | null {
128
+ return instanceJumpSelector;
129
+ }
@@ -100,6 +100,16 @@ const COPY = {
100
100
  scope: '단축키',
101
101
  description: '현재 필터 목록에서 다음 인스턴스를 선택합니다.',
102
102
  },
103
+ shortcutResetSidebarWidth: {
104
+ label: '사이드바 폭 리셋',
105
+ scope: '단축키',
106
+ description: '기본 300으로 되돌리고 \'jaw.sidebarWidth\' 키를 지운다.',
107
+ },
108
+ shortcutJumpInstance: {
109
+ label: '인스턴스 점프',
110
+ scope: '단축키',
111
+ description: '렌더된 사이드바 행 1–9로 이동합니다. Meta+1–4는 탭 전환에 남아 있어 Alt를 씁니다.',
112
+ },
103
113
  },
104
114
  support: {
105
115
  ariaLabel: '작업 제목 출처 준비 상태',
@@ -176,6 +186,16 @@ const COPY = {
176
186
  scope: 'Shortcut',
177
187
  description: 'Select the next instance in the current filtered list.',
178
188
  },
189
+ shortcutResetSidebarWidth: {
190
+ label: 'Reset sidebar width',
191
+ scope: 'Shortcut',
192
+ description: 'Restore the default 300px width and delete the \'jaw.sidebarWidth\' key.',
193
+ },
194
+ shortcutJumpInstance: {
195
+ label: 'Jump to instance',
196
+ scope: 'Shortcut',
197
+ description: 'Jump to rendered sidebar rows 1–9. Meta+1–4 stay on tab switching, so these use Alt.',
198
+ },
179
199
  },
180
200
  support: {
181
201
  ariaLabel: 'Activity title source readiness',
@@ -252,6 +272,16 @@ const COPY = {
252
272
  scope: '快捷键',
253
273
  description: '选择当前筛选列表中的下一个实例。',
254
274
  },
275
+ shortcutResetSidebarWidth: {
276
+ label: '重置侧边栏宽度',
277
+ scope: '快捷键',
278
+ description: '恢复默认 300px 宽度并删除 \'jaw.sidebarWidth\' 键。',
279
+ },
280
+ shortcutJumpInstance: {
281
+ label: '跳转到实例',
282
+ scope: '快捷键',
283
+ description: '跳转到已渲染的侧边栏第 1–9 行。Meta+1–4 仍用于切换标签,因此这里使用 Alt。',
284
+ },
255
285
  },
256
286
  support: {
257
287
  ariaLabel: '活动标题来源就绪状态',
@@ -328,6 +358,16 @@ const COPY = {
328
358
  scope: 'ショートカット',
329
359
  description: '現在のフィルタ一覧で次のインスタンスを選択します。',
330
360
  },
361
+ shortcutResetSidebarWidth: {
362
+ label: 'サイドバー幅をリセット',
363
+ scope: 'ショートカット',
364
+ description: '既定の 300px に戻し、\'jaw.sidebarWidth\' キーを削除します。',
365
+ },
366
+ shortcutJumpInstance: {
367
+ label: 'インスタンスへジャンプ',
368
+ scope: 'ショートカット',
369
+ description: '描画済みサイドバー行 1–9 へ移動します。Meta+1–4 はタブ切替のままなので Alt を使います。',
370
+ },
331
371
  },
332
372
  support: {
333
373
  ariaLabel: 'アクティビティタイトル取得元の準備状態',
@@ -445,6 +485,8 @@ function shortcutCopyKey(action: DashboardShortcutAction): keyof typeof COPY.ko.
445
485
  if (action === 'focusActiveSession') return 'shortcutFocusActiveSession';
446
486
  if (action === 'focusNotes') return 'shortcutFocusNotes';
447
487
  if (action === 'previousInstance') return 'shortcutPreviousInstance';
488
+ if (action === 'resetSidebarWidth') return 'shortcutResetSidebarWidth';
489
+ if (action.startsWith('jumpInstance')) return 'shortcutJumpInstance';
448
490
  return 'shortcutNextInstance';
449
491
  }
450
492
 
@@ -148,6 +148,7 @@ const ShortcutsHelp = (
148
148
  <li><kbd>{formatShortcut(DEFAULT_MANAGER_SHORTCUT_KEYMAP.focusActiveSession)}</kbd>는 선택된 인스턴스의 Preview 탭으로 이동해요.</li>
149
149
  <li><kbd>{formatShortcut(DEFAULT_MANAGER_SHORTCUT_KEYMAP.focusNotes)}</kbd>는 Notes workspace로 이동해요.</li>
150
150
  <li><kbd>{formatShortcut(DEFAULT_MANAGER_SHORTCUT_KEYMAP.previousInstance)}</kbd> / <kbd>{formatShortcut(DEFAULT_MANAGER_SHORTCUT_KEYMAP.nextInstance)}</kbd>는 현재 필터된 인스턴스 목록에서 이전/다음 행을 선택해요.</li>
151
+ <li><kbd>Alt+1</kbd> … <kbd>Alt+9</kbd>는 렌더된 사이드바 행으로 점프하고, Alt를 200ms 누르면 행 오른쪽에 점프 힌트가 떠요.</li>
151
152
  <li><kbd>⌘/Ctrl + S</kbd>는 Notes와 Settings처럼 저장 가능한 화면에서 현재 편집 내용을 저장해요.</li>
152
153
  <li><kbd>⌘/Ctrl + E</kbd>는 Notes 편집 모드를 순환해요.</li>
153
154
  <li>Manager 단축키는 Dashboard settings에서 켜고 끄거나 keymap을 바꿀 수 있어요.</li>
@@ -0,0 +1,46 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ export const SIDEBAR_GROUP_COLLAPSED_KEY = 'jaw.sidebarGroupCollapsed';
4
+
5
+ function readMap(): Record<string, boolean> {
6
+ try {
7
+ if (typeof localStorage === 'undefined') return {};
8
+ const raw = localStorage.getItem(SIDEBAR_GROUP_COLLAPSED_KEY);
9
+ if (!raw) return {};
10
+ const parsed: unknown = JSON.parse(raw);
11
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
12
+ const next: Record<string, boolean> = {};
13
+ for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
14
+ if (typeof value === 'boolean') next[key] = value;
15
+ }
16
+ return next;
17
+ } catch {
18
+ return {};
19
+ }
20
+ }
21
+
22
+ function writeMap(value: Record<string, boolean>): void {
23
+ try {
24
+ if (typeof localStorage === 'undefined') return;
25
+ localStorage.setItem(SIDEBAR_GROUP_COLLAPSED_KEY, JSON.stringify(value));
26
+ } catch {
27
+ // private mode
28
+ }
29
+ }
30
+
31
+ export function useSidebarGroupCollapse(): {
32
+ isCollapsed: (groupKey: string) => boolean;
33
+ toggle: (groupKey: string) => void;
34
+ toggleGroup: (groupKey: string) => void;
35
+ } {
36
+ const [collapsedByKey, setCollapsedByKey] = useState<Record<string, boolean>>(readMap);
37
+ const isCollapsed = useCallback((groupKey: string) => collapsedByKey[groupKey] === true, [collapsedByKey]);
38
+ const toggle = useCallback((groupKey: string) => {
39
+ setCollapsedByKey(prev => {
40
+ const next = { ...prev, [groupKey]: !prev[groupKey] };
41
+ writeMap(next);
42
+ return next;
43
+ });
44
+ }, []);
45
+ return { isCollapsed, toggle, toggleGroup: toggle };
46
+ }