@phenx-inc/ctlsurf 0.16.1 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ctlsurf-worker.js +39 -6
- package/out/headless/index.mjs +549 -116
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +416 -38
- package/out/preload/index.js +3 -0
- package/out/renderer/assets/{cssMode-DwHD6JSn.js → cssMode-BtQxI2Eb.js} +3 -3
- package/out/renderer/assets/{freemarker2-Na9-bXYw.js → freemarker2-DRW5dmY5.js} +1 -1
- package/out/renderer/assets/{handlebars-D7gzKLS8.js → handlebars-CFLF3cP7.js} +1 -1
- package/out/renderer/assets/{html-DDK7xN27.js → html-DtnBOkwp.js} +1 -1
- package/out/renderer/assets/{htmlMode-BfbLv-NB.js → htmlMode-DMVysDCg.js} +3 -3
- package/out/renderer/assets/{index-DID1LlMU.css → index-CEjwKjtd.css} +45 -0
- package/out/renderer/assets/{index-HoDaBul3.js → index-Dm70hk5s.js} +252 -26
- package/out/renderer/assets/{javascript-DJjWOxID.js → javascript-ChlyA1sf.js} +2 -2
- package/out/renderer/assets/{jsonMode-BileWbig.js → jsonMode-D1_oHzeN.js} +3 -3
- package/out/renderer/assets/{liquid-DvNU_dI9.js → liquid-D4ftdy1G.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Bb27EY8c.js → lspLanguageFeatures-Ckqu5ubG.js} +1 -1
- package/out/renderer/assets/{mdx-CLkNPVgD.js → mdx-B9wsVYSC.js} +1 -1
- package/out/renderer/assets/{python-DOJdL4kC.js → python-e23tD6IS.js} +1 -1
- package/out/renderer/assets/{razor-CSr8UlJc.js → razor-CrDyXj3d.js} +1 -1
- package/out/renderer/assets/{tsMode-sbsAvk21.js → tsMode-BwGC5o4O.js} +1 -1
- package/out/renderer/assets/{typescript-r5QB4yRW.js → typescript-Do-98d_O.js} +1 -1
- package/out/renderer/assets/{xml-CYhKh3Pi.js → xml-Dpdu5R3m.js} +1 -1
- package/out/renderer/assets/{yaml-B8DsoaYW.js → yaml-B0Zt7LCJ.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/scripts/extract-electron.js +105 -0
- package/src/main/headless.ts +12 -0
- package/src/main/index.ts +8 -0
- package/src/main/monitor.ts +540 -0
- package/src/main/orchestrator.ts +10 -0
- package/src/main/pty.ts +5 -0
- package/src/main/tui.ts +37 -3
- package/src/preload/index.ts +4 -0
- package/src/renderer/App.tsx +9 -1
- package/src/renderer/components/Resources.tsx +217 -0
- package/src/renderer/components/StatusBar.tsx +6 -1
- package/src/renderer/styles.css +45 -0
package/src/renderer/App.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
findPaneIds,
|
|
15
15
|
} from './components/PaneLayout'
|
|
16
16
|
import { StatusBar } from './components/StatusBar'
|
|
17
|
+
import type { ResourceSnapshot } from './components/Resources'
|
|
17
18
|
import { SettingsDialog } from './components/SettingsDialog'
|
|
18
19
|
import { TicketPane } from './components/TicketPanel'
|
|
19
20
|
import { ObserverPane } from './components/ObserverPanel'
|
|
@@ -98,6 +99,7 @@ declare global {
|
|
|
98
99
|
getVersion: () => Promise<string>
|
|
99
100
|
getUpdateInfo: () => Promise<UpdateInfo>
|
|
100
101
|
onUpdateInfo: (callback: (info: UpdateInfo) => void) => () => void
|
|
102
|
+
getResourceSnapshot: (fast: boolean) => Promise<ResourceSnapshot>
|
|
101
103
|
copyEmailTable: (html: string, text: string) => Promise<{ ok: boolean }>
|
|
102
104
|
copyText: (text: string) => Promise<{ ok: boolean }>
|
|
103
105
|
}
|
|
@@ -778,7 +780,13 @@ export default function App() {
|
|
|
778
780
|
)}
|
|
779
781
|
</div>
|
|
780
782
|
|
|
781
|
-
<StatusBar
|
|
783
|
+
<StatusBar
|
|
784
|
+
wsStatus={wsStatus}
|
|
785
|
+
cwd={cwd}
|
|
786
|
+
onChangeCwd={handleChangeCwd}
|
|
787
|
+
updateInfo={updateInfo}
|
|
788
|
+
resourceKey={`${activeTabId}:${tabs.find(t => t.id === activeTabId)?.agent?.id ?? ''}`}
|
|
789
|
+
/>
|
|
782
790
|
<SettingsDialog open={showSettings} onClose={() => setShowSettings(false)} />
|
|
783
791
|
|
|
784
792
|
{showAgentPicker && agents.length > 0 && (
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/** Mirrors MonitorSnapshot in src/main/monitor.ts */
|
|
4
|
+
type Service = 'claude' | 'codex'
|
|
5
|
+
interface UsageWindow { label: string; short: string | null; pct: number; resets: string | null }
|
|
6
|
+
interface PlanUsage { ok: boolean; windows: UsageWindow[]; plan?: string | null; source: string; ts: string; error?: string }
|
|
7
|
+
interface AgentProcess {
|
|
8
|
+
tabId: string; name: string; active: boolean; remote: boolean; service: Service | null
|
|
9
|
+
pid: number | null; cpu: number | null; mem: number | null; procs: number
|
|
10
|
+
}
|
|
11
|
+
export interface ResourceSnapshot {
|
|
12
|
+
machine: {
|
|
13
|
+
ok: boolean; error?: string; ts: string; label: string
|
|
14
|
+
cpu?: { pct: number | null; cores: number; load: number[] }
|
|
15
|
+
mem?: { total: number; used: number; pct: number }
|
|
16
|
+
disk?: { total: number; free: number } | null
|
|
17
|
+
gpu?: number | null
|
|
18
|
+
history: { t: number; cpu: number | null; mem: number | null; gpu: number | null }[]
|
|
19
|
+
}
|
|
20
|
+
agents: AgentProcess[]
|
|
21
|
+
activeService: Service | null
|
|
22
|
+
usage: Partial<Record<Service, PlanUsage>>
|
|
23
|
+
interval: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SERVICE_NAME: Record<Service, string> = { claude: 'Claude', codex: 'Codex' }
|
|
27
|
+
|
|
28
|
+
const gb = (bytes: number | null | undefined) => bytes == null ? '—' : `${(bytes / 2 ** 30).toFixed(bytes < 10 * 2 ** 30 ? 1 : 0)} GB`
|
|
29
|
+
const pct = (x: number | null | undefined) => x == null ? '—' : `${Math.round(x)}%`
|
|
30
|
+
/** fills (memory, quota): green → yellow → red; load: neutral */
|
|
31
|
+
const fillTone = (x: number | null | undefined) => x == null ? 'var(--text-dim)' : x < 70 ? 'var(--success)' : x < 90 ? 'var(--warning)' : 'var(--danger)'
|
|
32
|
+
|
|
33
|
+
function ago(iso: string): string {
|
|
34
|
+
const s = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 1000))
|
|
35
|
+
if (s < 60) return `${s}s ago`
|
|
36
|
+
if (s < 3600) return `${Math.round(s / 60)}m ago`
|
|
37
|
+
if (s < 86400) return `${Math.round(s / 3600)}h ago`
|
|
38
|
+
return `${Math.round(s / 86400)}d ago`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Poll the main-process monitor: every 3 s while the panel is open, 10 s
|
|
42
|
+
* otherwise, and right away when the active tab or its agent changes. */
|
|
43
|
+
function useResourceSnapshot(open: boolean, refreshKey: string): ResourceSnapshot | null {
|
|
44
|
+
const [data, setData] = useState<ResourceSnapshot | null>(null)
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
let cancelled = false
|
|
47
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
48
|
+
const poll = async () => {
|
|
49
|
+
try {
|
|
50
|
+
const snap = await window.worker.getResourceSnapshot(open)
|
|
51
|
+
if (!cancelled) setData(snap)
|
|
52
|
+
} catch { /* keep the last snapshot */ }
|
|
53
|
+
if (!cancelled) timer = setTimeout(poll, open ? 3000 : 10000)
|
|
54
|
+
}
|
|
55
|
+
void poll()
|
|
56
|
+
return () => { cancelled = true; clearTimeout(timer) }
|
|
57
|
+
}, [open, refreshKey])
|
|
58
|
+
return data
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function Gauge({ label, value, tone, title }: { label: string; value: number | null | undefined; tone: string; title: string }) {
|
|
62
|
+
const v = Math.max(0, Math.min(100, value ?? 0))
|
|
63
|
+
return (
|
|
64
|
+
<span className="res-gauge" title={title}>
|
|
65
|
+
<span className="res-gauge-bar"><span style={{ height: `${v}%`, background: value == null ? 'transparent' : tone }} /></span>
|
|
66
|
+
<span className="res-gauge-k">{label}</span><span className="res-gauge-v">{pct(value)}</span>
|
|
67
|
+
</span>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Status-bar gauges: this machine's CPU/RAM and the active agent's plan
|
|
72
|
+
* quota. Click to open the details panel. */
|
|
73
|
+
export function ResourceBar({ refreshKey }: { refreshKey: string }) {
|
|
74
|
+
const [open, setOpen] = useState(false)
|
|
75
|
+
const data = useResourceSnapshot(open, refreshKey)
|
|
76
|
+
const m = data?.machine
|
|
77
|
+
return (
|
|
78
|
+
<div className="res-bar" onClick={() => setOpen(!open)} title="Resources and plan quota: click for details">
|
|
79
|
+
{m?.ok && (
|
|
80
|
+
<span className="res-group">
|
|
81
|
+
<Gauge label="CPU" value={m.cpu?.pct} tone="var(--accent)" title={`CPU ${pct(m.cpu?.pct)} of ${m.cpu?.cores} cores`} />
|
|
82
|
+
<Gauge label="RAM" value={m.mem?.pct} tone={fillTone(m.mem?.pct)} title={`RAM ${gb(m.mem?.used)} of ${gb(m.mem?.total)}`} />
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
{data?.activeService && <QuotaGauges service={data.activeService} u={data.usage[data.activeService]} />}
|
|
86
|
+
{open && <ResourcePanel data={data} close={() => setOpen(false)} />}
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Quota for the service the active tab's agent runs on. */
|
|
92
|
+
function QuotaGauges({ service, u }: { service: Service; u?: PlanUsage }) {
|
|
93
|
+
const name = SERVICE_NAME[service]
|
|
94
|
+
if (!u) return null // first fetch still running
|
|
95
|
+
const wins = u.windows.filter((w) => w.short)
|
|
96
|
+
return (
|
|
97
|
+
<span className="res-group">
|
|
98
|
+
<span className="res-name">{name}</span>
|
|
99
|
+
{u.ok && wins.length
|
|
100
|
+
? wins.map((w) => (
|
|
101
|
+
<Gauge key={w.label} label={w.short!} value={w.pct} tone={fillTone(w.pct)}
|
|
102
|
+
title={`${name} ${w.label}: ${pct(w.pct)} used${w.resets ? ` · resets ${w.resets}` : ''}`} />
|
|
103
|
+
))
|
|
104
|
+
: <span className="res-off" title={u.error ?? 'no usage reported'}>quota unavailable</span>}
|
|
105
|
+
</span>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function Bar({ value, tone }: { value: number | null | undefined; tone: string }) {
|
|
110
|
+
return <span className="res-rbar"><span style={{ width: `${Math.max(0, Math.min(100, value ?? 0))}%`, background: tone }} /></span>
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function Spark({ values, tone, width = 120, height = 22 }: { values: (number | null)[]; tone: string; width?: number; height?: number }) {
|
|
114
|
+
const pts = values.map((v, i) => [values.length < 2 ? width : (i / (values.length - 1)) * width, height - ((v ?? 0) / 100) * (height - 2) - 1])
|
|
115
|
+
if (pts.length < 2) return <svg width={width} height={height} className="res-spark" />
|
|
116
|
+
const line = pts.map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(' ')
|
|
117
|
+
return (
|
|
118
|
+
<svg width={width} height={height} className="res-spark">
|
|
119
|
+
<polygon points={`0,${height} ${line} ${width},${height}`} fill={tone} opacity={0.15} />
|
|
120
|
+
<polyline points={line} fill="none" stroke={tone} strokeWidth={1.4} />
|
|
121
|
+
</svg>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function UsageView({ service, u, active }: { service: Service; u: PlanUsage; active: boolean }) {
|
|
126
|
+
return (
|
|
127
|
+
<div className="res-card">
|
|
128
|
+
<div className="res-head">
|
|
129
|
+
<b>{SERVICE_NAME[service]} plan</b>{active && <span className="res-tag">active tab</span>}
|
|
130
|
+
<span className="res-spacer" /><span className="res-muted">as of {ago(u.ts)}</span>
|
|
131
|
+
</div>
|
|
132
|
+
{u.ok ? <>
|
|
133
|
+
{u.windows.map((w) => (
|
|
134
|
+
<div key={w.label} className="res-row">
|
|
135
|
+
<span className="res-k wide">{w.label}</span><Bar value={w.pct} tone={fillTone(w.pct)} />
|
|
136
|
+
<span className="res-v narrow">{pct(w.pct)}</span>
|
|
137
|
+
<span className="res-muted res-resets">{w.resets ? `resets ${w.resets}` : ''}</span>
|
|
138
|
+
</div>
|
|
139
|
+
))}
|
|
140
|
+
<div className="res-muted res-note">from {u.source}{u.plan ? ` · plan ${u.plan}` : ''}</div>
|
|
141
|
+
</> : <div className="res-muted">{u.error ?? 'no usage reported'}</div>}
|
|
142
|
+
</div>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function MachineView({ m }: { m: ResourceSnapshot['machine'] }) {
|
|
147
|
+
const h = m.history
|
|
148
|
+
return (
|
|
149
|
+
<div className="res-card">
|
|
150
|
+
<div className="res-head">
|
|
151
|
+
<span className={`res-dot ${m.ok ? 'ok' : 'bad'}`} /><b>{m.label}</b>
|
|
152
|
+
<span className="res-spacer" /><span className="res-muted">updated {ago(m.ts)}</span>
|
|
153
|
+
</div>
|
|
154
|
+
{!m.ok ? <div className="res-err">{m.error ?? 'unavailable'}</div> : <>
|
|
155
|
+
<div className="res-row"><span className="res-k">CPU</span><Bar value={m.cpu?.pct} tone="var(--accent)" />
|
|
156
|
+
<span className="res-v">{pct(m.cpu?.pct)}</span><Spark values={h.map((p) => p.cpu)} tone="var(--accent)" /></div>
|
|
157
|
+
<div className="res-row"><span className="res-k" />
|
|
158
|
+
<span className="res-muted">{m.cpu?.cores} cores · load {m.cpu?.load.map((x) => x.toFixed(2)).join(' ')}</span></div>
|
|
159
|
+
<div className="res-row"><span className="res-k">RAM</span><Bar value={m.mem?.pct} tone={fillTone(m.mem?.pct)} />
|
|
160
|
+
<span className="res-v">{gb(m.mem?.used)} / {gb(m.mem?.total)}</span><Spark values={h.map((p) => p.mem)} tone={fillTone(m.mem?.pct)} /></div>
|
|
161
|
+
{m.gpu != null && <div className="res-row"><span className="res-k">GPU</span><Bar value={m.gpu} tone="var(--sev-digest)" />
|
|
162
|
+
<span className="res-v">{pct(m.gpu)}</span><Spark values={h.map((p) => p.gpu)} tone="var(--sev-digest)" /></div>}
|
|
163
|
+
{m.disk && <div className="res-row"><span className="res-k">Disk</span>
|
|
164
|
+
<Bar value={100 * (1 - m.disk.free / m.disk.total)} tone={fillTone(100 * (1 - m.disk.free / m.disk.total))} />
|
|
165
|
+
<span className="res-v">{gb(m.disk.free)} free</span></div>}
|
|
166
|
+
</>}
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function AgentsView({ agents }: { agents: AgentProcess[] }) {
|
|
172
|
+
return (
|
|
173
|
+
<div className="res-card">
|
|
174
|
+
<div className="res-head"><b>Agents in tabs</b></div>
|
|
175
|
+
{agents.length ? agents.map((a) => (
|
|
176
|
+
<div key={a.tabId} className="res-agent">
|
|
177
|
+
<span className={a.active ? 'res-agent-name active' : 'res-agent-name'}>{a.name}</span>
|
|
178
|
+
{a.active && <span className="res-tag">active</span>}
|
|
179
|
+
<div className="res-muted">
|
|
180
|
+
{a.pid == null ? 'exited' : <>
|
|
181
|
+
pid {a.pid} · {a.procs} process{a.procs === 1 ? '' : 'es'}
|
|
182
|
+
{a.cpu != null ? ` · CPU ${a.cpu.toFixed(0)}%` : ''}{a.mem != null ? ` · ${gb(a.mem)}` : ''}
|
|
183
|
+
</>}
|
|
184
|
+
{a.remote ? ' · local ssh client (agent runs remotely)' : ''}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
)) : <div className="res-muted">no agents running</div>}
|
|
188
|
+
</div>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function ResourcePanel({ data, close }: { data: ResourceSnapshot | null; close: () => void }) {
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') close() }
|
|
195
|
+
window.addEventListener('keydown', onKey)
|
|
196
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
197
|
+
}, [close])
|
|
198
|
+
// Active tab's service first, then any other service an open tab uses.
|
|
199
|
+
const services = (Object.keys(data?.usage ?? {}) as Service[])
|
|
200
|
+
.sort((a, b) => Number(b === data?.activeService) - Number(a === data?.activeService))
|
|
201
|
+
return (
|
|
202
|
+
<div className="res-panel" onClick={(e) => e.stopPropagation()}>
|
|
203
|
+
<div className="res-panel-head">
|
|
204
|
+
<h2>Resources</h2><span className="res-muted">plan quota and this machine · every 3 s</span>
|
|
205
|
+
<span className="res-spacer" /><button className="res-close" onClick={close} aria-label="Close">✕</button>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="res-panel-body">
|
|
208
|
+
{!data ? <div className="res-muted">sampling…</div> : <>
|
|
209
|
+
{services.map((s) => <UsageView key={s} service={s} u={data.usage[s]!} active={s === data.activeService} />)}
|
|
210
|
+
{data.activeService && !data.usage[data.activeService] && <div className="res-muted">checking {SERVICE_NAME[data.activeService]} quota…</div>}
|
|
211
|
+
<MachineView m={data.machine} />
|
|
212
|
+
<AgentsView agents={data.agents} />
|
|
213
|
+
</>}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
+
import { ResourceBar } from './Resources'
|
|
2
3
|
|
|
3
4
|
interface UpdateInfo {
|
|
4
5
|
current: string
|
|
@@ -12,9 +13,12 @@ interface StatusBarProps {
|
|
|
12
13
|
cwd: string | null
|
|
13
14
|
onChangeCwd: () => void
|
|
14
15
|
updateInfo: UpdateInfo | null
|
|
16
|
+
// Changes when the active tab or its agent does, so the quota gauges
|
|
17
|
+
// switch to the new agent's service right away.
|
|
18
|
+
resourceKey: string
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
export function StatusBar({ wsStatus, cwd, onChangeCwd, updateInfo }: StatusBarProps) {
|
|
21
|
+
export function StatusBar({ wsStatus, cwd, onChangeCwd, updateInfo, resourceKey }: StatusBarProps) {
|
|
18
22
|
const [creating, setCreating] = useState(false)
|
|
19
23
|
|
|
20
24
|
const wsLabel = {
|
|
@@ -63,6 +67,7 @@ export function StatusBar({ wsStatus, cwd, onChangeCwd, updateInfo }: StatusBarP
|
|
|
63
67
|
</span>
|
|
64
68
|
</div>
|
|
65
69
|
<div className="statusbar-section">
|
|
70
|
+
<ResourceBar refreshKey={resourceKey} />
|
|
66
71
|
<span className={`status-dot ${wsDotClass}`} />
|
|
67
72
|
{isNoProject ? (
|
|
68
73
|
<>
|
package/src/renderer/styles.css
CHANGED
|
@@ -960,6 +960,51 @@ html, body, #root {
|
|
|
960
960
|
font-weight: 500;
|
|
961
961
|
}
|
|
962
962
|
|
|
963
|
+
/* Resources + plan quota (status bar gauges and details panel) */
|
|
964
|
+
.res-bar { display: flex; align-items: center; gap: 12px; cursor: pointer; padding: 1px 6px; margin-right: 8px; border-radius: 4px; }
|
|
965
|
+
.res-bar:hover { background: var(--bg-surface); }
|
|
966
|
+
.res-group { display: inline-flex; align-items: center; gap: 7px; }
|
|
967
|
+
.res-group + .res-group { padding-left: 10px; border-left: 1px solid var(--border-subtle); }
|
|
968
|
+
.res-name { color: var(--text-soft); }
|
|
969
|
+
.res-off { color: var(--text-muted); font-style: italic; }
|
|
970
|
+
.res-gauge { display: inline-flex; align-items: center; gap: 3px; }
|
|
971
|
+
.res-gauge-bar { width: 5px; height: 13px; background: var(--bg-surface-hi); border-radius: 2px; display: flex; align-items: flex-end; overflow: hidden; }
|
|
972
|
+
.res-gauge-bar > span { width: 100%; border-radius: 2px; transition: height .6s ease; }
|
|
973
|
+
.res-gauge-k { color: var(--text-muted); }
|
|
974
|
+
.res-gauge-v { color: var(--text-bright); font-family: 'JetBrains Mono', 'Fira Code', monospace; min-width: 26px; }
|
|
975
|
+
|
|
976
|
+
.res-panel { position: fixed; right: 0; bottom: 25px; width: 440px; max-height: 75vh; z-index: 50; cursor: default;
|
|
977
|
+
background: var(--bg-raised); border: 1px solid var(--border); border-right: none; border-radius: 8px 0 0 0;
|
|
978
|
+
box-shadow: -12px -12px 40px var(--shadow-color); display: flex; flex-direction: column; animation: res-rise .18s ease-out; }
|
|
979
|
+
@keyframes res-rise { from { transform: translateY(20px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
980
|
+
.res-panel-head { display: flex; align-items: baseline; gap: 10px; padding: 12px 14px 6px; }
|
|
981
|
+
.res-panel-head h2 { margin: 0; font-size: 14px; color: var(--text-bright); }
|
|
982
|
+
.res-close { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 12px; }
|
|
983
|
+
.res-close:hover { color: var(--text-bright); }
|
|
984
|
+
.res-panel-body { overflow: auto; padding: 4px 14px 14px; display: flex; flex-direction: column; gap: 10px; font-size: 12px; color: var(--text); }
|
|
985
|
+
.res-card { background: var(--bg-surface); border-radius: 6px; padding: 8px 10px; display: flex; flex-direction: column; gap: 5px; }
|
|
986
|
+
.res-head { display: flex; align-items: center; gap: 8px; color: var(--text-bright); }
|
|
987
|
+
.res-spacer { flex: 1; }
|
|
988
|
+
.res-muted { color: var(--text-muted); font-size: 11.5px; }
|
|
989
|
+
.res-note { margin-top: 2px; }
|
|
990
|
+
.res-err { color: var(--danger); }
|
|
991
|
+
.res-tag { font-size: 10.5px; color: var(--accent); border: 1px solid var(--border); border-radius: 3px; padding: 0 4px; }
|
|
992
|
+
.res-dot { width: 7px; height: 7px; border-radius: 50%; }
|
|
993
|
+
.res-dot.ok { background: var(--success); }
|
|
994
|
+
.res-dot.bad { background: var(--danger); }
|
|
995
|
+
.res-row { display: flex; align-items: center; gap: 8px; }
|
|
996
|
+
.res-k { width: 40px; color: var(--text-soft); flex: none; }
|
|
997
|
+
.res-k.wide { width: 112px; }
|
|
998
|
+
.res-v { font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 11.5px; min-width: 104px; }
|
|
999
|
+
.res-v.narrow { min-width: 36px; }
|
|
1000
|
+
.res-resets { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1001
|
+
.res-rbar { flex: 1; height: 6px; background: var(--bg-surface-hi); border-radius: 3px; overflow: hidden; min-width: 50px; }
|
|
1002
|
+
.res-rbar > span { display: block; height: 100%; border-radius: 3px; transition: width .6s ease; }
|
|
1003
|
+
.res-spark { flex: none; }
|
|
1004
|
+
.res-agent { padding: 2px 0; }
|
|
1005
|
+
.res-agent-name { color: var(--text); }
|
|
1006
|
+
.res-agent-name.active { color: var(--text-bright); }
|
|
1007
|
+
|
|
963
1008
|
.tracking-dot {
|
|
964
1009
|
width: 6px;
|
|
965
1010
|
height: 6px;
|