cli-jaw 1.7.34 → 1.7.36

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 (169) hide show
  1. package/dist/bin/cli-jaw.js +5 -1
  2. package/dist/bin/cli-jaw.js.map +1 -1
  3. package/dist/bin/commands/dashboard.js +78 -0
  4. package/dist/bin/commands/dashboard.js.map +1 -0
  5. package/dist/lib/quota-copilot.js +5 -0
  6. package/dist/lib/quota-copilot.js.map +1 -1
  7. package/dist/src/agent/args.js +2 -4
  8. package/dist/src/agent/args.js.map +1 -1
  9. package/dist/src/agent/events.js +43 -1
  10. package/dist/src/agent/events.js.map +1 -1
  11. package/dist/src/manager/constants.js +9 -0
  12. package/dist/src/manager/constants.js.map +1 -0
  13. package/dist/src/manager/metadata.js +35 -0
  14. package/dist/src/manager/metadata.js.map +1 -0
  15. package/dist/src/manager/proxy.js +134 -0
  16. package/dist/src/manager/proxy.js.map +1 -0
  17. package/dist/src/manager/scan.js +111 -0
  18. package/dist/src/manager/scan.js.map +1 -0
  19. package/dist/src/manager/server.js +85 -0
  20. package/dist/src/manager/server.js.map +1 -0
  21. package/dist/src/manager/types.js +2 -0
  22. package/dist/src/manager/types.js.map +1 -0
  23. package/dist/src/routes/quota.js +134 -3
  24. package/dist/src/routes/quota.js.map +1 -1
  25. package/dist/src/routes/settings.js +5 -4
  26. package/dist/src/routes/settings.js.map +1 -1
  27. package/package.json +6 -1
  28. package/public/css/chat.css +27 -0
  29. package/public/css/diagram.css +48 -2
  30. package/public/dist/assets/api-Cl2oljO1.js +1 -0
  31. package/public/dist/assets/{index-wUWc2M5K.js → app-58Z9R8qM.js} +4 -4
  32. package/public/dist/assets/app-DWH_NZRE.css +1 -0
  33. package/public/dist/assets/architecture-YZFGNWBL-D4g_rKok.js +1 -0
  34. package/public/dist/assets/architectureDiagram-Q4EWVU46-DVbii_Pt.js +1 -0
  35. package/public/dist/assets/blockDiagram-DXYQGD6D-BStipezp.js +1 -0
  36. package/public/dist/assets/c4Diagram-AHTNJAMY-BYno4Snw.js +1 -0
  37. package/public/dist/assets/classDiagram-6PBFFD2Q-QzqRo_9X.js +1 -0
  38. package/public/dist/assets/classDiagram-v2-HSJHXN6E-CchGqJUH.js +1 -0
  39. package/public/dist/assets/{constants-BU8a_R5s.js → constants-fUXYKkoK.js} +1 -1
  40. package/public/dist/assets/cose-bilkent-S5V4N54A-TJS1iaqp.js +1 -0
  41. package/public/dist/assets/dagre-KV5264BT-C_DCJZ7t.js +1 -0
  42. package/public/dist/assets/diagram-5BDNPKRD-CKix1NRO.js +1 -0
  43. package/public/dist/assets/diagram-G4DWMVQ6-CoDTDNnx.js +1 -0
  44. package/public/dist/assets/diagram-MMDJMWI5-DYx0PwrY.js +1 -0
  45. package/public/dist/assets/diagram-TYMM5635-Bx1779dG.js +1 -0
  46. package/public/dist/assets/{employees-p53cgGmH.js → employees-CnA_uLbJ.js} +1 -1
  47. package/public/dist/assets/erDiagram-SMLLAGMA-C0INYE-d.js +1 -0
  48. package/public/dist/assets/flowDiagram-DWJPFMVM-DfQIgEdB.js +1 -0
  49. package/public/dist/assets/ganttDiagram-T4ZO3ILL-B8o_Hblo.js +1 -0
  50. package/public/dist/assets/gitGraph-7Q5UKJZL-DBMf1L33.js +1 -0
  51. package/public/dist/assets/gitGraphDiagram-UUTBAWPF-B2nqnLHi.js +1 -0
  52. package/public/dist/assets/idb-cache-C5ilDI6r.js +1 -0
  53. package/public/dist/assets/info-OMHHGYJF-C3gtg5Od.js +1 -0
  54. package/public/dist/assets/infoDiagram-42DDH7IO-Brz9iwrL.js +1 -0
  55. package/public/dist/assets/ishikawaDiagram-UXIWVN3A-C0YeQJh0.js +1 -0
  56. package/public/dist/assets/journeyDiagram-VCZTEJTY-BypDthzR.js +1 -0
  57. package/public/dist/assets/kanban-definition-6JOO6SKY-LkyDUgUP.js +1 -0
  58. package/public/dist/assets/katex-BL3z5Bli.js +1 -0
  59. package/public/dist/assets/manager-D4L7R_7R.css +1 -0
  60. package/public/dist/assets/manager-DtTAKPgY.js +9 -0
  61. package/public/dist/assets/memory-BN3grLR7.js +1 -0
  62. package/public/dist/assets/{memory-C2i7ZIvv.js → memory-RKpZbPTW.js} +1 -1
  63. package/public/dist/assets/mermaid.core-D6PDYVgC.js +1 -0
  64. package/public/dist/assets/mindmap-definition-QFDTVHPH-rV1Hipx-.js +1 -0
  65. package/public/dist/assets/packet-4T2RLAQJ-BuK6y2B7.js +1 -0
  66. package/public/dist/assets/pie-ZZUOXDRM-x8CPu972.js +1 -0
  67. package/public/dist/assets/pieDiagram-DEJITSTG-DpxKH4h4.js +1 -0
  68. package/public/dist/assets/quadrantDiagram-34T5L4WZ-BkRLuqoa.js +1 -0
  69. package/public/dist/assets/radar-PYXPWWZC-CsTAKlcV.js +1 -0
  70. package/public/dist/assets/render-D9bnLVR_.js +30 -0
  71. package/public/dist/assets/requirementDiagram-MS252O5E-BdqZlJ0O.js +1 -0
  72. package/public/dist/assets/sankeyDiagram-XADWPNL6-b3SwMAxC.js +1 -0
  73. package/public/dist/assets/sequenceDiagram-FGHM5R23-BmjI6n81.js +1 -0
  74. package/public/dist/assets/{settings-BUEiZgkm.js → settings-B8oUfDm4.js} +8 -8
  75. package/public/dist/assets/settings-DvGS6X_5.js +1 -0
  76. package/public/dist/assets/{skills-CSuSbBWa.js → skills-CHB_WsxF.js} +1 -1
  77. package/public/dist/assets/skills-WhnGcvvJ.js +1 -0
  78. package/public/dist/assets/{slash-commands-D-v0DlbY.js → slash-commands-CbsN2LqT.js} +1 -1
  79. package/public/dist/assets/slash-commands-DOh3zvJ7.js +1 -0
  80. package/public/dist/assets/stateDiagram-FHFEXIEX-DRsIDvF8.js +1 -0
  81. package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-IJMg9fYC.js +1 -0
  82. package/public/dist/assets/timeline-definition-GMOUNBTQ-B-0E30rh.js +1 -0
  83. package/public/dist/assets/treeView-SZITEDCU-CRgtpI0K.js +1 -0
  84. package/public/dist/assets/treemap-W4RFUUIX-DB5B-smF.js +1 -0
  85. package/public/dist/assets/ui-Cg8zuDaT.js +131 -0
  86. package/public/dist/assets/ui-HckqJaBX.js +1 -0
  87. package/public/dist/assets/{vendor-mermaid-UktBx7L0.js → vendor-mermaid-DHVxHqbi.js} +6 -6
  88. package/public/dist/assets/{vendor-render-D2YP6GiF.js → vendor-render-BaFgDHeH.js} +1 -1
  89. package/public/dist/assets/vennDiagram-DHZGUBPP-BBd9TC4D.js +1 -0
  90. package/public/dist/assets/wardley-RL74JXVD-BzCH0WF9.js +1 -0
  91. package/public/dist/assets/wardleyDiagram-NUSXRM2D-DgenAPEz.js +1 -0
  92. package/public/dist/assets/ws-DSntry14.js +14 -0
  93. package/public/dist/assets/xychartDiagram-5P7HB3ND-BmvcJ6sg.js +1 -0
  94. package/public/dist/index.html +2 -2
  95. package/public/dist/manager/index.html +14 -0
  96. package/public/js/diagram/iframe-renderer.ts +196 -77
  97. package/public/js/features/process-block.ts +2 -2
  98. package/public/js/features/settings-cli-status.ts +20 -1
  99. package/public/js/features/settings-types.ts +1 -1
  100. package/public/js/render.ts +16 -3
  101. package/public/js/ui.ts +42 -0
  102. package/public/js/ws.ts +20 -4
  103. package/public/manager/index.html +13 -0
  104. package/public/manager/src/App.tsx +185 -0
  105. package/public/manager/src/InstancePreview.tsx +72 -0
  106. package/public/manager/src/api.ts +8 -0
  107. package/public/manager/src/main.tsx +9 -0
  108. package/public/manager/src/preview.ts +35 -0
  109. package/public/manager/src/styles.css +309 -0
  110. package/public/manager/src/types.ts +44 -0
  111. package/public/dist/assets/api-COrKYKcO.js +0 -1
  112. package/public/dist/assets/architecture-YZFGNWBL-B2Nc8YC_.js +0 -1
  113. package/public/dist/assets/architectureDiagram-Q4EWVU46-EML5rtmP.js +0 -1
  114. package/public/dist/assets/blockDiagram-DXYQGD6D-Ds9bBl-N.js +0 -1
  115. package/public/dist/assets/c4Diagram-AHTNJAMY-BVpUT3Om.js +0 -1
  116. package/public/dist/assets/classDiagram-6PBFFD2Q-DG8IYzhr.js +0 -1
  117. package/public/dist/assets/classDiagram-v2-HSJHXN6E-B5j0d5gs.js +0 -1
  118. package/public/dist/assets/cose-bilkent-S5V4N54A-kNrexiHE.js +0 -1
  119. package/public/dist/assets/dagre-KV5264BT-Ylb1W0yQ.js +0 -1
  120. package/public/dist/assets/diagram-5BDNPKRD-CPxqMVrp.js +0 -1
  121. package/public/dist/assets/diagram-G4DWMVQ6-Dm1xfj8I.js +0 -1
  122. package/public/dist/assets/diagram-MMDJMWI5-VsjfxFEK.js +0 -1
  123. package/public/dist/assets/diagram-TYMM5635-CrMZri_r.js +0 -1
  124. package/public/dist/assets/erDiagram-SMLLAGMA-Felxus25.js +0 -1
  125. package/public/dist/assets/flowDiagram-DWJPFMVM-kQrkSkw2.js +0 -1
  126. package/public/dist/assets/ganttDiagram-T4ZO3ILL-CulD-LkD.js +0 -1
  127. package/public/dist/assets/gitGraph-7Q5UKJZL-DbbbZYM5.js +0 -1
  128. package/public/dist/assets/gitGraphDiagram-UUTBAWPF-oTUYJsKL.js +0 -1
  129. package/public/dist/assets/idb-cache-C7z4qE00.js +0 -1
  130. package/public/dist/assets/index-CLKLbGzn.css +0 -1
  131. package/public/dist/assets/info-OMHHGYJF-auV0JwD8.js +0 -1
  132. package/public/dist/assets/infoDiagram-42DDH7IO-B2XOJXrL.js +0 -1
  133. package/public/dist/assets/ishikawaDiagram-UXIWVN3A-CsmszxH_.js +0 -1
  134. package/public/dist/assets/journeyDiagram-VCZTEJTY-MmzEBROj.js +0 -1
  135. package/public/dist/assets/kanban-definition-6JOO6SKY-DcbxzAKu.js +0 -1
  136. package/public/dist/assets/katex-Bh3QhMKY.js +0 -1
  137. package/public/dist/assets/memory-6zLEr-qI.js +0 -1
  138. package/public/dist/assets/mermaid.core-CoRN09Dx.js +0 -1
  139. package/public/dist/assets/mindmap-definition-QFDTVHPH-CMqZWcUf.js +0 -1
  140. package/public/dist/assets/packet-4T2RLAQJ-C_PFKo5G.js +0 -1
  141. package/public/dist/assets/pie-ZZUOXDRM-CcqLxwbG.js +0 -1
  142. package/public/dist/assets/pieDiagram-DEJITSTG-C9r3BQ9f.js +0 -1
  143. package/public/dist/assets/quadrantDiagram-34T5L4WZ-Du4pEL4T.js +0 -1
  144. package/public/dist/assets/radar-PYXPWWZC-BWOF5omS.js +0 -1
  145. package/public/dist/assets/render-CulTuvJs.js +0 -30
  146. package/public/dist/assets/requirementDiagram-MS252O5E-Dyz9eEFP.js +0 -1
  147. package/public/dist/assets/sankeyDiagram-XADWPNL6-DLgUCKs2.js +0 -1
  148. package/public/dist/assets/sequenceDiagram-FGHM5R23-c0nA9K_Q.js +0 -1
  149. package/public/dist/assets/settings-BhrOslae.js +0 -1
  150. package/public/dist/assets/skills-CgwxEvFx.js +0 -1
  151. package/public/dist/assets/slash-commands-Bo8jvBfI.js +0 -1
  152. package/public/dist/assets/stateDiagram-FHFEXIEX-D2aVOdby.js +0 -1
  153. package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-DuwdzBLR.js +0 -1
  154. package/public/dist/assets/timeline-definition-GMOUNBTQ-PuaZYI31.js +0 -1
  155. package/public/dist/assets/treeView-SZITEDCU-D8mODguI.js +0 -1
  156. package/public/dist/assets/treemap-W4RFUUIX-DqqBuiM1.js +0 -1
  157. package/public/dist/assets/ui-4JiRyxJy.js +0 -131
  158. package/public/dist/assets/ui-Dx0MwI23.js +0 -1
  159. package/public/dist/assets/vennDiagram-DHZGUBPP-4blQ8E1z.js +0 -1
  160. package/public/dist/assets/wardley-RL74JXVD-Dn1IRCw2.js +0 -1
  161. package/public/dist/assets/wardleyDiagram-NUSXRM2D-OUBAKNJu.js +0 -1
  162. package/public/dist/assets/ws-DKtFfZsY.js +0 -14
  163. package/public/dist/assets/xychartDiagram-5P7HB3ND-CyZD7ovz.js +0 -1
  164. /package/public/dist/assets/{api-DygAf_G_.js → api-CnBOYa3F.js} +0 -0
  165. /package/public/dist/assets/{idb-cache-DbK81tgv.js → idb-cache-CjnC_K0x.js} +0 -0
  166. /package/public/dist/assets/{locale-CxI5nTcf.js → locale-heAuYK-3.js} +0 -0
  167. /package/public/dist/assets/{rolldown-runtime-FhOqtrmT.js → rolldown-runtime-DE9SaGGd.js} +0 -0
  168. /package/public/dist/assets/{state-O6NVkWcL.js → state-CPF7TImo.js} +0 -0
  169. /package/public/dist/assets/{vendor-icons-Bs4t7RP2.js → vendor-icons-D2l6ddC0.js} +0 -0
package/public/js/ws.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // ── WebSocket Connection ──
2
2
  import { state } from './state.js';
3
- import { setStatus, updateQueueBadge, addSystemMsg, appendAgentText, finalizeAgent, addMessage, showProcessStep, cleanupToolActivity, applyQueuedOverlay, hydrateActiveRun, reconcileChatBottomAfterRestore } from './ui.js';
3
+ import { setStatus, updateQueueBadge, addSystemMsg, appendAgentText, finalizeAgent, addMessage, showProcessStep, cleanupToolActivity, applyQueuedOverlay, hydrateActiveRun, reconcileChatBottomAfterRestore, showChatRestoreIndicator } from './ui.js';
4
4
  import { renderPendingQueue } from './features/pending-queue.js';
5
5
  import { t, getLang } from './features/i18n.js';
6
6
  import { getVirtualScroll } from './virtual-scroll.js';
@@ -118,20 +118,36 @@ export function syncOrchestrateSnapshot(reason = 'manual', options: { hydrateRun
118
118
  return refreshRuntimeSnapshot(options);
119
119
  }
120
120
 
121
+ function syncAfterBrowserRestore(reason: string): void {
122
+ showChatRestoreIndicator(reason);
123
+ syncOrchestrateSnapshot(reason)
124
+ .finally(() => {
125
+ reconcileChatBottomAfterRestore(reason);
126
+ })
127
+ .catch(() => {});
128
+ }
129
+
121
130
  function registerOrchestrateRestoreHooks(): void {
122
131
  if (restoreHooksRegistered) return;
123
132
  restoreHooksRegistered = true;
124
133
  window.addEventListener('focus', () => {
125
- syncOrchestrateSnapshot('focus').catch(() => {});
134
+ syncAfterBrowserRestore('focus');
126
135
  });
127
136
  window.addEventListener('pageshow', () => {
128
- syncOrchestrateSnapshot('pageshow').catch(() => {});
137
+ syncAfterBrowserRestore('pageshow');
129
138
  });
130
139
  document.addEventListener('visibilitychange', () => {
131
140
  if (document.visibilityState === 'visible') {
132
- syncOrchestrateSnapshot('visibilitychange').catch(() => {});
141
+ syncAfterBrowserRestore('visibilitychange');
133
142
  }
134
143
  });
144
+ document.addEventListener('resume', () => {
145
+ syncAfterBrowserRestore('resume');
146
+ });
147
+ if ('wasDiscarded' in document
148
+ && Boolean((document as Document & { wasDiscarded?: boolean }).wasDiscarded)) {
149
+ syncAfterBrowserRestore('discard');
150
+ }
135
151
  }
136
152
 
137
153
  /** Hydrate agent phase cache from snapshot (used after reconnect) */
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Jaw Manager</title>
7
+ </head>
8
+ <body>
9
+ <div id="manager-root"></div>
10
+ <script type="module" src="/manager/src/main.tsx"></script>
11
+ </body>
12
+ </html>
13
+
@@ -0,0 +1,185 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { fetchInstances } from './api';
3
+ import { InstancePreview } from './InstancePreview';
4
+ import type { DashboardInstance, DashboardInstanceStatus, DashboardPreviewMode, DashboardScanResult } from './types';
5
+
6
+ const STATUS_OPTIONS: Array<'all' | DashboardInstanceStatus> = ['all', 'online', 'offline', 'timeout', 'error', 'unknown'];
7
+
8
+ function formatUptime(seconds: number | null): string {
9
+ if (seconds == null) return 'n/a';
10
+ const minutes = Math.floor(seconds / 60);
11
+ if (minutes < 1) return `${Math.round(seconds)}s`;
12
+ const hours = Math.floor(minutes / 60);
13
+ if (hours < 1) return `${minutes}m`;
14
+ return `${hours}h ${minutes % 60}m`;
15
+ }
16
+
17
+ function instanceLabel(instance: DashboardInstance): string {
18
+ return instance.instanceId || instance.homeDisplay || `port-${instance.port}`;
19
+ }
20
+
21
+ function statusClass(status: DashboardInstanceStatus): string {
22
+ return `status status-${status}`;
23
+ }
24
+
25
+ export function App() {
26
+ const [data, setData] = useState<DashboardScanResult | null>(null);
27
+ const [loading, setLoading] = useState(true);
28
+ const [error, setError] = useState<string | null>(null);
29
+ const [query, setQuery] = useState('');
30
+ const [status, setStatus] = useState<'all' | DashboardInstanceStatus>('all');
31
+ const [selectedPort, setSelectedPort] = useState<number | null>(null);
32
+ const [previewMode, setPreviewMode] = useState<DashboardPreviewMode>('proxy');
33
+ const [previewEnabled, setPreviewEnabled] = useState(true);
34
+
35
+ async function load(): Promise<void> {
36
+ setLoading(true);
37
+ setError(null);
38
+ try {
39
+ setData(await fetchInstances());
40
+ } catch (err) {
41
+ setError((err as Error).message);
42
+ } finally {
43
+ setLoading(false);
44
+ }
45
+ }
46
+
47
+ useEffect(() => {
48
+ void load();
49
+ }, []);
50
+
51
+ const instances = data?.instances || [];
52
+ const summary = useMemo(() => {
53
+ return instances.reduce((acc, instance) => {
54
+ acc.total += 1;
55
+ acc[instance.status] = (acc[instance.status] || 0) + 1;
56
+ return acc;
57
+ }, { total: 0 } as Record<string, number>);
58
+ }, [instances]);
59
+
60
+ const filtered = useMemo(() => {
61
+ const needle = query.trim().toLowerCase();
62
+ return instances.filter((instance) => {
63
+ if (status !== 'all' && instance.status !== status) return false;
64
+ if (!needle) return true;
65
+ return [
66
+ String(instance.port),
67
+ instance.url,
68
+ instanceLabel(instance),
69
+ instance.version,
70
+ instance.workingDir,
71
+ instance.currentCli,
72
+ instance.currentModel,
73
+ instance.healthReason,
74
+ ].some(value => String(value || '').toLowerCase().includes(needle));
75
+ });
76
+ }, [instances, query, status]);
77
+
78
+ const selectedInstance = useMemo(() => {
79
+ if (selectedPort == null) return filtered.find(instance => instance.ok) || null;
80
+ return instances.find(instance => instance.port === selectedPort) || null;
81
+ }, [filtered, instances, selectedPort]);
82
+
83
+ return (
84
+ <main className="dashboard-shell">
85
+ <header className="dashboard-topbar">
86
+ <div>
87
+ <p className="eyebrow">Jaw Manager</p>
88
+ <h1>Instance dashboard</h1>
89
+ </div>
90
+ <div className="topbar-meta">
91
+ <span>Manager {data?.manager.port || 24576}</span>
92
+ <span>Scan {data ? `${data.manager.rangeFrom}-${data.manager.rangeTo}` : '3457-3506'}</span>
93
+ <button type="button" onClick={() => void load()} disabled={loading}>
94
+ {loading ? 'Scanning' : 'Refresh'}
95
+ </button>
96
+ </div>
97
+ </header>
98
+
99
+ <section className="summary-grid" aria-label="Instance summary">
100
+ <div><span>Total</span><strong>{summary.total || 0}</strong></div>
101
+ <div><span>Online</span><strong>{summary.online || 0}</strong></div>
102
+ <div><span>Offline</span><strong>{summary.offline || 0}</strong></div>
103
+ <div><span>Timeout</span><strong>{summary.timeout || 0}</strong></div>
104
+ </section>
105
+
106
+ <section className="toolbar" aria-label="Filters">
107
+ <input
108
+ value={query}
109
+ onChange={event => setQuery(event.target.value)}
110
+ placeholder="Search port, home, CLI, model"
111
+ aria-label="Search instances"
112
+ />
113
+ <select
114
+ value={status}
115
+ onChange={event => setStatus(event.target.value as 'all' | DashboardInstanceStatus)}
116
+ aria-label="Filter by status"
117
+ >
118
+ {STATUS_OPTIONS.map(option => <option key={option} value={option}>{option}</option>)}
119
+ </select>
120
+ </section>
121
+
122
+ {error && <section className="state error-state">Scan failed: {error}</section>}
123
+ {!error && loading && <section className="state">Scanning local Jaw instances...</section>}
124
+ {!error && !loading && filtered.length === 0 && (
125
+ <section className="state">No matching instances found.</section>
126
+ )}
127
+
128
+ {!error && filtered.length > 0 && (
129
+ <section className="dashboard-layout">
130
+ <section className="instance-table" aria-label="Jaw instances">
131
+ <div className="table-head">
132
+ <span>Status</span>
133
+ <span>Instance</span>
134
+ <span>Runtime</span>
135
+ <span>Last checked</span>
136
+ <span>Action</span>
137
+ </div>
138
+ {filtered.map(instance => (
139
+ <article
140
+ className={`instance-row ${selectedInstance?.port === instance.port ? 'is-selected' : ''}`}
141
+ key={instance.port}
142
+ >
143
+ <div>
144
+ <span className={statusClass(instance.status)}>{instance.status}</span>
145
+ <span className="port">:{instance.port}</span>
146
+ </div>
147
+ <div>
148
+ <strong>{instanceLabel(instance)}</strong>
149
+ <span>{instance.workingDir || instance.url}</span>
150
+ </div>
151
+ <div>
152
+ <span>{instance.currentCli || 'cli n/a'} / {instance.currentModel || 'model n/a'}</span>
153
+ <span>v{instance.version || 'n/a'} · {formatUptime(instance.uptime)}</span>
154
+ </div>
155
+ <div>
156
+ <span>{new Date(instance.lastCheckedAt).toLocaleTimeString()}</span>
157
+ <span>{instance.healthReason || 'ok'}</span>
158
+ </div>
159
+ <div className="instance-actions">
160
+ <button
161
+ type="button"
162
+ onClick={() => setSelectedPort(instance.port)}
163
+ disabled={!instance.ok}
164
+ >
165
+ Preview
166
+ </button>
167
+ <a className="open-link" href={instance.url} target="_blank" rel="noreferrer">Open</a>
168
+ </div>
169
+ </article>
170
+ ))}
171
+ </section>
172
+
173
+ <InstancePreview
174
+ instance={selectedInstance}
175
+ data={data}
176
+ mode={previewMode}
177
+ previewEnabled={previewEnabled}
178
+ onModeChange={setPreviewMode}
179
+ onPreviewEnabledChange={setPreviewEnabled}
180
+ />
181
+ </section>
182
+ )}
183
+ </main>
184
+ );
185
+ }
@@ -0,0 +1,72 @@
1
+ import { buildPreviewState } from './preview';
2
+ import type { DashboardInstance, DashboardPreviewMode, DashboardScanResult } from './types';
3
+
4
+ type InstancePreviewProps = {
5
+ instance: DashboardInstance | null;
6
+ data: DashboardScanResult | null;
7
+ mode: DashboardPreviewMode;
8
+ previewEnabled: boolean;
9
+ onModeChange: (mode: DashboardPreviewMode) => void;
10
+ onPreviewEnabledChange: (enabled: boolean) => void;
11
+ };
12
+
13
+ export function InstancePreview(props: InstancePreviewProps) {
14
+ const state = buildPreviewState(
15
+ props.previewEnabled ? props.instance : null,
16
+ props.data,
17
+ props.mode,
18
+ );
19
+
20
+ return (
21
+ <aside className="preview-panel" aria-label="Instance preview">
22
+ <div className="preview-header">
23
+ <div>
24
+ <p className="eyebrow">Preview</p>
25
+ <h2>{props.instance ? `:${props.instance.port}` : 'No instance selected'}</h2>
26
+ </div>
27
+ {props.instance && (
28
+ <a className="open-link" href={props.instance.url} target="_blank" rel="noreferrer">
29
+ Open
30
+ </a>
31
+ )}
32
+ </div>
33
+
34
+ <div className="preview-controls" aria-label="Preview controls">
35
+ <label className="preview-toggle">
36
+ <input
37
+ type="checkbox"
38
+ checked={props.previewEnabled}
39
+ onChange={event => props.onPreviewEnabledChange(event.target.checked)}
40
+ />
41
+ Enable preview
42
+ </label>
43
+
44
+ <select
45
+ value={props.mode}
46
+ onChange={event => props.onModeChange(event.target.value as DashboardPreviewMode)}
47
+ aria-label="Preview mode"
48
+ >
49
+ <option value="proxy">Proxy preview</option>
50
+ <option value="direct">Direct iframe</option>
51
+ </select>
52
+ </div>
53
+
54
+ {!state.canPreview && <div className="preview-empty">{state.reason}</div>}
55
+
56
+ {state.canPreview && state.src && (
57
+ <iframe
58
+ title={`Jaw instance ${props.instance?.port} preview`}
59
+ className="preview-frame"
60
+ src={state.src}
61
+ sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts"
62
+ />
63
+ )}
64
+
65
+ {props.mode === 'direct' && (
66
+ <p className="preview-note">
67
+ Preview may be blocked by frame policy. Open the instance in a new tab or use proxy preview.
68
+ </p>
69
+ )}
70
+ </aside>
71
+ );
72
+ }
@@ -0,0 +1,8 @@
1
+ import type { DashboardScanResult } from './types';
2
+
3
+ export async function fetchInstances(): Promise<DashboardScanResult> {
4
+ const response = await fetch('/api/dashboard/instances');
5
+ if (!response.ok) throw new Error(`scan failed: ${response.status}`);
6
+ return await response.json() as DashboardScanResult;
7
+ }
8
+
@@ -0,0 +1,9 @@
1
+ import { createRoot } from 'react-dom/client';
2
+ import { App } from './App';
3
+ import './styles.css';
4
+
5
+ const root = document.getElementById('manager-root');
6
+ if (!root) throw new Error('manager-root not found');
7
+
8
+ createRoot(root).render(<App />);
9
+
@@ -0,0 +1,35 @@
1
+ import type { DashboardInstance, DashboardPreviewMode, DashboardScanResult } from './types';
2
+
3
+ export type PreviewState = {
4
+ canPreview: boolean;
5
+ src: string | null;
6
+ reason: string | null;
7
+ };
8
+
9
+ export function buildPreviewState(
10
+ instance: DashboardInstance | null,
11
+ data: DashboardScanResult | null,
12
+ mode: DashboardPreviewMode,
13
+ ): PreviewState {
14
+ if (!instance) {
15
+ return { canPreview: false, src: null, reason: 'Select an online instance to preview.' };
16
+ }
17
+
18
+ if (!instance.ok) {
19
+ return { canPreview: false, src: null, reason: 'Preview is only available for online instances.' };
20
+ }
21
+
22
+ if (mode === 'proxy') {
23
+ const proxy = data?.manager.proxy;
24
+ if (!proxy?.enabled) {
25
+ return { canPreview: false, src: null, reason: 'Proxy preview is not available.' };
26
+ }
27
+ if (instance.port < proxy.allowedFrom || instance.port > proxy.allowedTo) {
28
+ return { canPreview: false, src: null, reason: 'This port is outside the proxy allowlist.' };
29
+ }
30
+ const basePath = proxy.basePath || '/i';
31
+ return { canPreview: true, src: `${basePath}/${instance.port}/`, reason: null };
32
+ }
33
+
34
+ return { canPreview: true, src: instance.url, reason: null };
35
+ }
@@ -0,0 +1,309 @@
1
+ :root {
2
+ color-scheme: dark;
3
+ font-family: "Outfit", "Geist", ui-sans-serif, system-ui, sans-serif;
4
+ background: #111318;
5
+ color: #eef1f5;
6
+ }
7
+
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ min-width: 320px;
15
+ background: #111318;
16
+ }
17
+
18
+ button,
19
+ input,
20
+ select {
21
+ font: inherit;
22
+ }
23
+
24
+ .dashboard-shell {
25
+ min-height: 100dvh;
26
+ padding: 18px;
27
+ }
28
+
29
+ .dashboard-topbar {
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: space-between;
33
+ gap: 16px;
34
+ padding-bottom: 16px;
35
+ border-bottom: 1px solid #2b303a;
36
+ }
37
+
38
+ .eyebrow {
39
+ margin: 0 0 4px;
40
+ color: #7fd7ff;
41
+ font-size: 12px;
42
+ }
43
+
44
+ h1 {
45
+ margin: 0;
46
+ font-size: 24px;
47
+ font-weight: 500;
48
+ letter-spacing: 0;
49
+ }
50
+
51
+ .topbar-meta,
52
+ .toolbar {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 10px;
56
+ flex-wrap: wrap;
57
+ }
58
+
59
+ .topbar-meta span,
60
+ .summary-grid div,
61
+ .toolbar input,
62
+ .toolbar select,
63
+ .state,
64
+ .instance-row,
65
+ .table-head {
66
+ border: 1px solid #2b303a;
67
+ background: #181c24;
68
+ border-radius: 8px;
69
+ }
70
+
71
+ .topbar-meta span {
72
+ padding: 8px 10px;
73
+ color: #aab3c2;
74
+ font-size: 13px;
75
+ }
76
+
77
+ button,
78
+ .open-link {
79
+ min-height: 38px;
80
+ border: 1px solid #236987;
81
+ background: #123244;
82
+ color: #d7f4ff;
83
+ border-radius: 7px;
84
+ padding: 8px 12px;
85
+ text-decoration: none;
86
+ display: inline-flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ }
90
+
91
+ button:disabled {
92
+ opacity: 0.55;
93
+ }
94
+
95
+ .summary-grid {
96
+ display: grid;
97
+ grid-template-columns: repeat(4, minmax(0, 1fr));
98
+ gap: 10px;
99
+ margin: 16px 0;
100
+ }
101
+
102
+ .summary-grid div {
103
+ padding: 12px;
104
+ }
105
+
106
+ .summary-grid span,
107
+ .instance-row span,
108
+ .table-head {
109
+ color: #9aa3b2;
110
+ font-size: 12px;
111
+ }
112
+
113
+ .summary-grid strong {
114
+ display: block;
115
+ margin-top: 6px;
116
+ font-size: 24px;
117
+ font-weight: 500;
118
+ }
119
+
120
+ .toolbar {
121
+ margin-bottom: 12px;
122
+ }
123
+
124
+ .toolbar input,
125
+ .toolbar select {
126
+ min-height: 40px;
127
+ color: #eef1f5;
128
+ padding: 8px 10px;
129
+ }
130
+
131
+ .toolbar input {
132
+ flex: 1;
133
+ min-width: 220px;
134
+ }
135
+
136
+ .state {
137
+ padding: 18px;
138
+ color: #aab3c2;
139
+ }
140
+
141
+ .error-state {
142
+ border-color: #6c2d35;
143
+ color: #ffb9c0;
144
+ }
145
+
146
+ .instance-table {
147
+ display: grid;
148
+ gap: 8px;
149
+ }
150
+
151
+ .dashboard-layout {
152
+ display: grid;
153
+ grid-template-columns: minmax(0, 1fr) minmax(320px, 42vw);
154
+ gap: 12px;
155
+ align-items: start;
156
+ }
157
+
158
+ .table-head,
159
+ .instance-row {
160
+ display: grid;
161
+ grid-template-columns: 150px minmax(180px, 1.4fr) minmax(180px, 1fr) minmax(160px, 0.9fr) 90px;
162
+ gap: 12px;
163
+ align-items: center;
164
+ padding: 10px 12px;
165
+ }
166
+
167
+ .instance-row strong,
168
+ .instance-row span {
169
+ display: block;
170
+ overflow-wrap: anywhere;
171
+ }
172
+
173
+ .instance-row.is-selected {
174
+ border-color: #7fd7ff;
175
+ }
176
+
177
+ .instance-actions {
178
+ display: flex;
179
+ gap: 8px;
180
+ flex-wrap: wrap;
181
+ }
182
+
183
+ .status {
184
+ width: fit-content;
185
+ padding: 4px 8px;
186
+ border-radius: 999px;
187
+ color: #111318;
188
+ }
189
+
190
+ .status-online {
191
+ background: #65d58d;
192
+ }
193
+
194
+ .status-offline,
195
+ .status-unknown {
196
+ background: #858e9c;
197
+ }
198
+
199
+ .status-timeout {
200
+ background: #f0bd62;
201
+ }
202
+
203
+ .status-error {
204
+ background: #ef717b;
205
+ }
206
+
207
+ .port {
208
+ margin-top: 5px;
209
+ }
210
+
211
+ .preview-panel {
212
+ position: sticky;
213
+ top: 12px;
214
+ border: 1px solid #2b303a;
215
+ background: #181c24;
216
+ border-radius: 8px;
217
+ padding: 12px;
218
+ min-height: 520px;
219
+ }
220
+
221
+ .preview-header,
222
+ .preview-controls {
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: space-between;
226
+ gap: 10px;
227
+ flex-wrap: wrap;
228
+ }
229
+
230
+ .preview-header h2 {
231
+ margin: 0;
232
+ font-size: 20px;
233
+ font-weight: 500;
234
+ letter-spacing: 0;
235
+ }
236
+
237
+ .preview-controls {
238
+ margin: 12px 0;
239
+ }
240
+
241
+ .preview-controls select {
242
+ min-height: 40px;
243
+ color: #eef1f5;
244
+ padding: 8px 10px;
245
+ border: 1px solid #2b303a;
246
+ background: #181c24;
247
+ border-radius: 8px;
248
+ }
249
+
250
+ .preview-toggle {
251
+ min-height: 40px;
252
+ display: inline-flex;
253
+ align-items: center;
254
+ gap: 8px;
255
+ color: #aab3c2;
256
+ font-size: 13px;
257
+ }
258
+
259
+ .preview-frame {
260
+ width: 100%;
261
+ height: min(68dvh, 720px);
262
+ border: 1px solid #2b303a;
263
+ border-radius: 8px;
264
+ background: #0f1218;
265
+ }
266
+
267
+ .preview-empty {
268
+ min-height: 360px;
269
+ display: grid;
270
+ place-items: center;
271
+ border: 1px dashed #2b303a;
272
+ border-radius: 8px;
273
+ padding: 18px;
274
+ }
275
+
276
+ .preview-empty,
277
+ .preview-note {
278
+ color: #aab3c2;
279
+ font-size: 13px;
280
+ }
281
+
282
+ @media (max-width: 860px) {
283
+ .dashboard-topbar {
284
+ align-items: flex-start;
285
+ flex-direction: column;
286
+ }
287
+
288
+ .summary-grid {
289
+ grid-template-columns: repeat(2, minmax(0, 1fr));
290
+ }
291
+
292
+ .table-head {
293
+ display: none;
294
+ }
295
+
296
+ .instance-row {
297
+ grid-template-columns: 1fr;
298
+ }
299
+ }
300
+
301
+ @media (max-width: 1080px) {
302
+ .dashboard-layout {
303
+ grid-template-columns: 1fr;
304
+ }
305
+
306
+ .preview-panel {
307
+ position: static;
308
+ }
309
+ }