1342 lines
34 KiB
HTML
1342 lines
34 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Cline Hub Monitor</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: dark;
|
|
--panel: rgba(18, 26, 35, 0.82);
|
|
--panel-strong: rgba(13, 19, 27, 0.92);
|
|
--panel-soft: rgba(255, 255, 255, 0.055);
|
|
--line: rgba(181, 213, 236, 0.18);
|
|
--line-strong: rgba(181, 213, 236, 0.3);
|
|
--text: #ecf3f8;
|
|
--muted: #aeb8c4;
|
|
--faint: #6f7d8a;
|
|
--green: #61d87d;
|
|
--blue: #63a9ff;
|
|
--amber: #e8a842;
|
|
--red: #ff615b;
|
|
--violet: #9b7dff;
|
|
--shadow: 0 28px 70px rgba(0, 0, 0, 0.48);
|
|
font-family:
|
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
"Segoe UI", sans-serif;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
color: var(--text);
|
|
background: #07111d;
|
|
}
|
|
|
|
button,
|
|
input {
|
|
font: inherit;
|
|
}
|
|
|
|
.desktop {
|
|
position: relative;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
background:
|
|
linear-gradient(180deg, rgba(7, 17, 31, 0.15), rgba(4, 9, 14, 0.86)),
|
|
radial-gradient(circle at 22% 14%, rgba(67, 112, 157, 0.55), transparent 33%),
|
|
linear-gradient(160deg, #15283d 0%, #09131e 48%, #05080d 100%);
|
|
}
|
|
|
|
.scene {
|
|
position: absolute;
|
|
z-index: 2;
|
|
top: var(--scene-y, 0px);
|
|
left: var(--scene-x, 0px);
|
|
width: 1600px;
|
|
height: 900px;
|
|
transform: scale(var(--scene-scale, 1));
|
|
transform-origin: 0 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.desktop::before,
|
|
.desktop::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: auto -6vw 0;
|
|
height: 46vh;
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.desktop::before {
|
|
background:
|
|
linear-gradient(145deg, transparent 0 32%, rgba(5, 10, 15, 0.55) 33% 58%, transparent 59%),
|
|
linear-gradient(25deg, transparent 0 24%, rgba(9, 18, 25, 0.72) 25% 48%, transparent 49%),
|
|
linear-gradient(180deg, rgba(139, 166, 190, 0.13), rgba(2, 5, 8, 0.86));
|
|
filter: blur(1px);
|
|
transform: translateY(-10vh);
|
|
}
|
|
|
|
.desktop::after {
|
|
height: 36vh;
|
|
background:
|
|
linear-gradient(110deg, transparent 0 18%, rgba(2, 7, 11, 0.86) 19% 44%, transparent 45%),
|
|
linear-gradient(35deg, transparent 0 30%, rgba(0, 4, 7, 0.9) 31% 64%, transparent 65%),
|
|
linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.92));
|
|
}
|
|
|
|
.system-bar {
|
|
position: relative;
|
|
z-index: 5;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
height: 38px;
|
|
padding: 0 24px;
|
|
width: 1600px;
|
|
color: rgba(255, 255, 255, 0.92);
|
|
background: rgba(6, 10, 16, 0.64);
|
|
backdrop-filter: blur(24px);
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.menu-left,
|
|
.menu-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 22px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.apple-dot {
|
|
display: grid;
|
|
width: 20px;
|
|
height: 20px;
|
|
place-items: center;
|
|
border-radius: 8px;
|
|
background: rgba(255, 255, 255, 0.11);
|
|
color: #fff;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.bar-icon {
|
|
display: grid;
|
|
width: 26px;
|
|
height: 24px;
|
|
place-items: center;
|
|
border-radius: 8px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: #f4f7fb;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.stage {
|
|
position: relative;
|
|
z-index: 2;
|
|
width: 1600px;
|
|
height: 862px;
|
|
padding: 0;
|
|
}
|
|
|
|
.hub-card,
|
|
.inspector {
|
|
position: absolute;
|
|
border: 1px solid var(--line-strong);
|
|
background:
|
|
linear-gradient(180deg, rgba(255, 255, 255, 0.045), transparent 20%),
|
|
var(--panel);
|
|
box-shadow: var(--shadow);
|
|
backdrop-filter: blur(30px) saturate(135%);
|
|
}
|
|
|
|
.hub-card {
|
|
left: 304px;
|
|
top: 18px;
|
|
width: 414px;
|
|
border-radius: 14px;
|
|
padding: 18px;
|
|
}
|
|
|
|
.hub-card::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: -16px;
|
|
left: 50%;
|
|
width: 30px;
|
|
height: 30px;
|
|
border-left: 1px solid var(--line-strong);
|
|
border-top: 1px solid var(--line-strong);
|
|
background: rgba(18, 26, 35, 0.78);
|
|
transform: translateX(-50%) rotate(45deg);
|
|
}
|
|
|
|
.inspector {
|
|
left: 740px;
|
|
top: 66px;
|
|
width: 804px;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card-top,
|
|
.inspector-top {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
}
|
|
|
|
.card-title,
|
|
.window-title {
|
|
font-size: 16px;
|
|
font-weight: 650;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.status-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
height: 28px;
|
|
padding: 0 10px;
|
|
border: 1px solid rgba(97, 216, 125, 0.2);
|
|
border-radius: 8px;
|
|
background: rgba(97, 216, 125, 0.12);
|
|
color: var(--green);
|
|
font-size: 13px;
|
|
font-weight: 650;
|
|
}
|
|
|
|
.dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 99px;
|
|
background: var(--faint);
|
|
}
|
|
|
|
.dot.green {
|
|
background: var(--green);
|
|
box-shadow: 0 0 18px rgba(97, 216, 125, 0.55);
|
|
}
|
|
|
|
.dot.blue {
|
|
background: var(--blue);
|
|
box-shadow: 0 0 18px rgba(99, 169, 255, 0.45);
|
|
}
|
|
|
|
.dot.amber {
|
|
background: var(--amber);
|
|
box-shadow: 0 0 18px rgba(232, 168, 66, 0.42);
|
|
}
|
|
|
|
.dot.red {
|
|
background: var(--red);
|
|
box-shadow: 0 0 18px rgba(255, 97, 91, 0.42);
|
|
}
|
|
|
|
.icon-button,
|
|
.window-button {
|
|
display: grid;
|
|
width: 30px;
|
|
height: 30px;
|
|
place-items: center;
|
|
border: 0;
|
|
border-radius: 8px;
|
|
color: #c8d1dc;
|
|
background: rgba(255, 255, 255, 0.055);
|
|
}
|
|
|
|
.metric-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 10px;
|
|
margin-top: 16px;
|
|
padding: 18px 16px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 10px;
|
|
background: rgba(8, 14, 21, 0.38);
|
|
}
|
|
|
|
.metric {
|
|
display: grid;
|
|
grid-template-columns: 34px 1fr;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.metric-icon {
|
|
display: grid;
|
|
width: 34px;
|
|
height: 34px;
|
|
place-items: center;
|
|
border-radius: 10px;
|
|
background: rgba(99, 169, 255, 0.12);
|
|
color: var(--blue);
|
|
font-weight: 800;
|
|
}
|
|
|
|
.metric:nth-child(1) .metric-icon {
|
|
background: rgba(97, 216, 125, 0.13);
|
|
color: var(--green);
|
|
}
|
|
|
|
.metric:nth-child(3) .metric-icon {
|
|
background: rgba(232, 168, 66, 0.13);
|
|
color: var(--amber);
|
|
}
|
|
|
|
.metric-value {
|
|
font-size: 25px;
|
|
line-height: 1;
|
|
font-weight: 760;
|
|
}
|
|
|
|
.metric-label,
|
|
.small-label {
|
|
color: var(--muted);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.uptime {
|
|
margin-top: 4px;
|
|
padding: 12px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 9px;
|
|
background: rgba(255, 255, 255, 0.035);
|
|
}
|
|
|
|
.uptime-row,
|
|
.section-title,
|
|
.list-row,
|
|
.event-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.progress-track {
|
|
position: relative;
|
|
height: 5px;
|
|
margin-top: 14px;
|
|
border-radius: 99px;
|
|
background: rgba(160, 177, 195, 0.22);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
position: absolute;
|
|
inset: 0 auto 0 0;
|
|
width: 82%;
|
|
border-radius: inherit;
|
|
background: linear-gradient(90deg, #5edc7b, #91e0ff, #d6b34c);
|
|
}
|
|
|
|
.section {
|
|
margin-top: 18px;
|
|
}
|
|
|
|
.section-title {
|
|
margin-bottom: 10px;
|
|
color: #dce5ec;
|
|
font-size: 15px;
|
|
font-weight: 650;
|
|
}
|
|
|
|
.link {
|
|
border: 0;
|
|
background: transparent;
|
|
color: var(--blue);
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.list,
|
|
.events {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
|
|
.list-row,
|
|
.event-row {
|
|
min-height: 28px;
|
|
color: #e5edf5;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.left-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.truncate {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.row-muted {
|
|
color: var(--muted);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.card-actions {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 14px;
|
|
margin-top: 18px;
|
|
padding-top: 16px;
|
|
border-top: 1px solid var(--line);
|
|
}
|
|
|
|
.action {
|
|
height: 38px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
color: #e7eff7;
|
|
background: rgba(255, 255, 255, 0.045);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.action.primary {
|
|
border-color: rgba(99, 169, 255, 0.75);
|
|
background: linear-gradient(180deg, #54a6ff, #2d73dd);
|
|
box-shadow: inset 0 1px rgba(255, 255, 255, 0.28);
|
|
}
|
|
|
|
.inspector-top {
|
|
height: 52px;
|
|
padding: 0 16px;
|
|
border-bottom: 1px solid var(--line);
|
|
background: rgba(10, 16, 23, 0.55);
|
|
}
|
|
|
|
.window-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.session-head {
|
|
padding: 22px 24px 14px;
|
|
}
|
|
|
|
.session-title-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.session-title {
|
|
font-size: 24px;
|
|
line-height: 1.1;
|
|
font-weight: 760;
|
|
}
|
|
|
|
.session-meta {
|
|
margin-top: 8px;
|
|
color: var(--muted);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: 14px;
|
|
margin-top: 18px;
|
|
}
|
|
|
|
.tab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
height: 38px;
|
|
padding: 0 12px;
|
|
border: 1px solid transparent;
|
|
border-radius: 8px;
|
|
color: var(--muted);
|
|
background: transparent;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.tab.active {
|
|
border-color: var(--line);
|
|
color: var(--text);
|
|
background: rgba(255, 255, 255, 0.055);
|
|
}
|
|
|
|
.inspector-body {
|
|
display: grid;
|
|
grid-template-columns: 180px minmax(280px, 1fr) 268px;
|
|
gap: 8px;
|
|
padding: 0 16px 16px;
|
|
}
|
|
|
|
.pane {
|
|
height: 420px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 7px;
|
|
background: rgba(7, 12, 18, 0.42);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.pane-title {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
height: 44px;
|
|
padding: 0 12px;
|
|
border-bottom: 1px solid rgba(181, 213, 236, 0.1);
|
|
background: rgba(255, 255, 255, 0.025);
|
|
font-size: 14px;
|
|
font-weight: 620;
|
|
}
|
|
|
|
.overview-content {
|
|
display: grid;
|
|
gap: 14px;
|
|
padding: 12px;
|
|
}
|
|
|
|
.kv-label {
|
|
color: var(--faint);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.kv-value {
|
|
margin-top: 4px;
|
|
color: #e4ecf4;
|
|
font-size: 13px;
|
|
line-height: 1.35;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.token-bar {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 3px;
|
|
height: 5px;
|
|
margin-top: 8px;
|
|
border-radius: 99px;
|
|
overflow: hidden;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.token-bar span:first-child {
|
|
background: var(--green);
|
|
}
|
|
|
|
.token-bar span:last-child {
|
|
background: rgba(157, 126, 255, 0.8);
|
|
}
|
|
|
|
.agent-dots {
|
|
display: flex;
|
|
gap: 7px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.agent-dot {
|
|
display: grid;
|
|
width: 22px;
|
|
height: 22px;
|
|
place-items: center;
|
|
border-radius: 99px;
|
|
color: #111821;
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
background: var(--amber);
|
|
}
|
|
|
|
.agent-dot:nth-child(2) {
|
|
background: var(--violet);
|
|
}
|
|
|
|
.agent-dot:nth-child(3) {
|
|
background: #49b9ef;
|
|
}
|
|
|
|
.agent-dot:nth-child(4) {
|
|
background: #f2cf62;
|
|
}
|
|
|
|
.transcript,
|
|
.timeline {
|
|
padding: 10px 12px 14px;
|
|
}
|
|
|
|
.transcript-row {
|
|
display: grid;
|
|
grid-template-columns: 54px 116px 1fr;
|
|
gap: 10px;
|
|
padding: 5px 0;
|
|
color: #c9d3de;
|
|
font-family: "SF Mono", "Cascadia Code", ui-monospace, monospace;
|
|
font-size: 12px;
|
|
line-height: 1.42;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.transcript-row span:last-child {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.role {
|
|
font-weight: 800;
|
|
}
|
|
|
|
.role.architect {
|
|
color: #72b8ff;
|
|
}
|
|
|
|
.role.security {
|
|
color: #eab85a;
|
|
}
|
|
|
|
.role.pragmatist {
|
|
color: #5edf78;
|
|
}
|
|
|
|
.role.skeptic {
|
|
color: #ac84ff;
|
|
}
|
|
|
|
.role.tool {
|
|
color: #76d794;
|
|
}
|
|
|
|
.stream-dot {
|
|
display: inline-block;
|
|
width: 7px;
|
|
height: 7px;
|
|
margin-right: 8px;
|
|
border-radius: 99px;
|
|
background: #ffd85f;
|
|
box-shadow: 0 0 12px rgba(255, 216, 95, 0.6);
|
|
}
|
|
|
|
.timeline-row {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: 58px 18px 1fr;
|
|
gap: 9px;
|
|
min-height: 36px;
|
|
color: #cbd5df;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.timeline-row::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 18px;
|
|
bottom: -18px;
|
|
left: 66px;
|
|
width: 1px;
|
|
background: rgba(181, 213, 236, 0.2);
|
|
}
|
|
|
|
.timeline-row:last-child::before {
|
|
display: none;
|
|
}
|
|
|
|
.timeline-node {
|
|
width: 10px;
|
|
height: 10px;
|
|
margin-top: 4px;
|
|
border: 2px solid rgba(176, 199, 218, 0.55);
|
|
border-radius: 99px;
|
|
background: #15202d;
|
|
}
|
|
|
|
.timeline-row.active .timeline-node {
|
|
border-color: var(--blue);
|
|
background: var(--blue);
|
|
box-shadow: 0 0 16px rgba(99, 169, 255, 0.7);
|
|
}
|
|
|
|
.inspector-footer {
|
|
display: grid;
|
|
grid-template-columns: 1fr 176px;
|
|
gap: 12px;
|
|
padding: 0 16px 18px;
|
|
}
|
|
|
|
.footer-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, 1fr);
|
|
border: 1px solid var(--line);
|
|
border-radius: 7px;
|
|
background: rgba(8, 13, 19, 0.48);
|
|
}
|
|
|
|
.stat {
|
|
min-width: 0;
|
|
padding: 13px 16px;
|
|
border-right: 1px solid var(--line);
|
|
}
|
|
|
|
.stat:last-child {
|
|
border-right: 0;
|
|
}
|
|
|
|
.stat-value {
|
|
margin-top: 6px;
|
|
font-size: 20px;
|
|
font-weight: 740;
|
|
}
|
|
|
|
.stat-sub {
|
|
margin-top: 3px;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.stat-sub.success {
|
|
color: var(--green);
|
|
}
|
|
|
|
.stat-sub.danger {
|
|
color: var(--red);
|
|
}
|
|
|
|
.stop {
|
|
border: 1px solid rgba(255, 97, 91, 0.62);
|
|
border-radius: 8px;
|
|
color: white;
|
|
background: linear-gradient(180deg, #ff625d, #ce3b34);
|
|
font-weight: 680;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.preview-chip {
|
|
display: none;
|
|
margin-left: 8px;
|
|
padding: 3px 7px;
|
|
border-radius: 999px;
|
|
color: #1a2230;
|
|
background: #f2cf62;
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.demo .preview-chip {
|
|
display: inline-flex;
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="desktop" data-tauri-drag-region>
|
|
<div class="scene" id="scene">
|
|
<header class="system-bar" data-tauri-drag-region>
|
|
<div class="menu-left">
|
|
<span class="apple-dot">C</span>
|
|
<span>Cline</span>
|
|
<span>File</span>
|
|
<span>Edit</span>
|
|
<span>View</span>
|
|
<span>Window</span>
|
|
<span>Help</span>
|
|
</div>
|
|
<div class="menu-right">
|
|
<span class="bar-icon">H</span>
|
|
<span>Wi-Fi</span>
|
|
<span>100%</span>
|
|
<span id="clock">Tue 9:41 AM</span>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="stage">
|
|
<aside class="hub-card" id="hubCard">
|
|
<div class="card-top">
|
|
<div class="card-title">Cline Hub Monitor</div>
|
|
<div style="display: flex; align-items: center; gap: 10px">
|
|
<div class="status-pill" id="hubStatus">
|
|
<span class="dot green"></span>
|
|
<span>Hub Connected</span>
|
|
</div>
|
|
<button type="button" class="icon-button" title="Settings">s</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="metric-grid">
|
|
<div class="metric">
|
|
<div class="metric-icon">C</div>
|
|
<div>
|
|
<div class="metric-value" id="clientCount">4</div>
|
|
<div class="metric-label">Clients</div>
|
|
</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-icon">S</div>
|
|
<div>
|
|
<div class="metric-value" id="sessionCount">6</div>
|
|
<div class="metric-label">Sessions</div>
|
|
</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-icon">R</div>
|
|
<div>
|
|
<div class="metric-value" id="runningCount">2</div>
|
|
<div class="metric-label">Running</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="uptime">
|
|
<div class="uptime-row">
|
|
<div class="small-label">Hub Uptime</div>
|
|
<div class="small-label" id="uptime">3h 42m 18s</div>
|
|
</div>
|
|
<div class="progress-track"><div class="progress-fill"></div></div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">
|
|
<span>Active Sessions</span>
|
|
<button type="button" class="link">View all</button>
|
|
</div>
|
|
<div class="list" id="clientList"></div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">
|
|
<span>Recent Events</span>
|
|
<button type="button" class="link">View all</button>
|
|
</div>
|
|
<div class="events" id="eventList"></div>
|
|
</div>
|
|
|
|
<div class="card-actions">
|
|
<button type="button" class="action" id="openDashboard">Open Dashboard</button>
|
|
<button type="button" class="action primary" id="newSession">New Session</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<section class="inspector" id="inspector">
|
|
<div class="inspector-top" data-tauri-drag-region>
|
|
<div class="window-title">Session Inspector</div>
|
|
<div class="window-controls">
|
|
<button type="button" class="window-button">-</button>
|
|
<button type="button" class="window-button">[]</button>
|
|
<button type="button" class="window-button">x</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="session-head">
|
|
<div class="session-title-row">
|
|
<span class="dot green" id="sessionDot"></span>
|
|
<div class="session-title">
|
|
<span id="sessionTitle">refactor-auth</span>
|
|
<span class="preview-chip">Preview data</span>
|
|
</div>
|
|
<div class="status-pill" id="sessionStatus">Running</div>
|
|
</div>
|
|
<div class="session-meta" id="sessionMeta">
|
|
dev-studio - Started 8m ago - ID: sess_8f3c2a7d
|
|
</div>
|
|
<nav class="tabs">
|
|
<button type="button" class="tab active">Overview</button>
|
|
<button type="button" class="tab">Timeline</button>
|
|
<button type="button" class="tab">Files</button>
|
|
<button type="button" class="tab">Context</button>
|
|
<button type="button" class="tab">Settings</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="inspector-body">
|
|
<div class="pane">
|
|
<div class="pane-title">Overview</div>
|
|
<div class="overview-content" id="overview"></div>
|
|
</div>
|
|
|
|
<div class="pane">
|
|
<div class="pane-title">Live Transcript (preview)</div>
|
|
<div class="transcript" id="transcript"></div>
|
|
</div>
|
|
|
|
<div class="pane">
|
|
<div class="pane-title">
|
|
<span>Timeline</span>
|
|
<button type="button" class="link">View full timeline</button>
|
|
</div>
|
|
<div class="timeline" id="timeline"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="inspector-footer">
|
|
<div class="footer-stats">
|
|
<div class="stat">
|
|
<div class="small-label">Progress</div>
|
|
<div class="stat-value" id="progressValue">62%</div>
|
|
<div class="stat-sub" id="progressSub">12 / 19 tasks</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="small-label">Tests</div>
|
|
<div class="stat-value" id="testsValue">24/24</div>
|
|
<div class="stat-sub success">Passing</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="small-label">Files Changed</div>
|
|
<div class="stat-value" id="filesValue">8</div>
|
|
<div class="stat-sub success">+142 <span class="danger">-23</span></div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="small-label">Issues</div>
|
|
<div class="stat-value" id="issuesValue">0</div>
|
|
<div class="stat-sub">Open</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="small-label">Duration</div>
|
|
<div class="stat-value" id="durationValue">8m 41s</div>
|
|
<div class="stat-sub">Elapsed</div>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="stop" id="stopButton">Stop Session</button>
|
|
</div>
|
|
</section>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
const sampleState = {
|
|
connected: true,
|
|
hubUptime: "3h 42m 18s",
|
|
clientSummaries: [
|
|
{ label: "Cline", name: "ara-mbp", sessionCount: 2 },
|
|
{ label: "Cline", name: "dev-studio", sessionCount: 2 },
|
|
{ label: "Runner", name: "ci-runner-01", sessionCount: 1 },
|
|
{ label: "Laptop", name: "laptop-16", sessionCount: 1 },
|
|
],
|
|
sessionSummaries: [
|
|
{
|
|
sessionId: "sess_8f3c2a7d",
|
|
title: "refactor-auth",
|
|
status: "running",
|
|
workspaceRoot: "~/Projects/atlas",
|
|
workspaceName: "dev-studio",
|
|
cwd: "~/Projects/atlas/server",
|
|
model: "claude-sonnet-4.6",
|
|
provider: "anthropic",
|
|
createdAt: Date.now() - 8 * 60 * 1000,
|
|
updatedAt: Date.now(),
|
|
inputTokens: 124800,
|
|
outputTokens: 56300,
|
|
totalCost: 0.036,
|
|
agentCount: 4,
|
|
},
|
|
],
|
|
events: [
|
|
{
|
|
title: "dev-studio",
|
|
body: "Started session \"refactor-auth\"",
|
|
severity: "success",
|
|
timestamp: Date.now() - 20 * 1000,
|
|
},
|
|
{
|
|
title: "ara-mbp",
|
|
body: "Completed session \"fix-payments\"",
|
|
severity: "success",
|
|
timestamp: Date.now() - 2 * 60 * 1000,
|
|
},
|
|
{
|
|
title: "ci-runner-01",
|
|
body: "Error in session \"build-check\"",
|
|
severity: "warn",
|
|
timestamp: Date.now() - 5 * 60 * 1000,
|
|
},
|
|
{
|
|
title: "laptop-16",
|
|
body: "Started session \"docs-update\"",
|
|
severity: "info",
|
|
timestamp: Date.now() - 11 * 60 * 1000,
|
|
},
|
|
],
|
|
};
|
|
|
|
const invoke = window.__TAURI__?.core?.invoke;
|
|
let lastState = sampleState;
|
|
|
|
function fitScene() {
|
|
const scale = Math.min(window.innerWidth / 1600, window.innerHeight / 900);
|
|
const safeScale = Math.min(1, Math.max(0.35, scale));
|
|
document.documentElement.style.setProperty("--scene-scale", String(safeScale));
|
|
document.documentElement.style.setProperty(
|
|
"--scene-x",
|
|
`${Math.max(0, (window.innerWidth - 1600 * safeScale) / 2)}px`,
|
|
);
|
|
document.documentElement.style.setProperty(
|
|
"--scene-y",
|
|
"0px",
|
|
);
|
|
}
|
|
|
|
function shortId(id) {
|
|
return id ? (id.length > 12 ? id.slice(0, 12) : id) : "unknown";
|
|
}
|
|
|
|
function fmtNumber(value) {
|
|
const n = Number(value || 0);
|
|
if (n >= 1000000) return `${(n / 1000000).toFixed(1)}m`;
|
|
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
return `${n}`;
|
|
}
|
|
|
|
function fmtCost(value) {
|
|
return `$${Number(value || 0).toFixed(3)}`;
|
|
}
|
|
|
|
function timeAgo(timestamp) {
|
|
const delta = Math.max(0, Date.now() - Number(timestamp || Date.now()));
|
|
const seconds = Math.floor(delta / 1000);
|
|
if (seconds < 60) return "now";
|
|
const minutes = Math.floor(seconds / 60);
|
|
if (minutes < 60) return `${minutes}m ago`;
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return `${hours}h ago`;
|
|
return `${Math.floor(hours / 24)}d ago`;
|
|
}
|
|
|
|
function elapsed(timestamp) {
|
|
const delta = Math.max(0, Date.now() - Number(timestamp || Date.now()));
|
|
const minutes = Math.floor(delta / 60000);
|
|
const seconds = Math.floor((delta % 60000) / 1000);
|
|
if (minutes < 60) return `${minutes}m ${seconds}s`;
|
|
const hours = Math.floor(minutes / 60);
|
|
return `${hours}h ${minutes % 60}m`;
|
|
}
|
|
|
|
function statusColor(status, severity) {
|
|
const value = String(status || severity || "").toLowerCase();
|
|
if (value.includes("error") || value.includes("fail")) return "red";
|
|
if (value.includes("warn") || value.includes("idle")) return "amber";
|
|
if (value.includes("info")) return "blue";
|
|
return "green";
|
|
}
|
|
|
|
function normalize(state) {
|
|
const hasLiveSessions = Array.isArray(state?.sessionSummaries) && state.sessionSummaries.length > 0;
|
|
const live = state && typeof state === "object" ? state : {};
|
|
return {
|
|
...sampleState,
|
|
...live,
|
|
clientSummaries:
|
|
Array.isArray(live.clientSummaries) && live.clientSummaries.length > 0
|
|
? live.clientSummaries
|
|
: sampleState.clientSummaries,
|
|
sessionSummaries: hasLiveSessions
|
|
? live.sessionSummaries
|
|
: sampleState.sessionSummaries,
|
|
events:
|
|
Array.isArray(live.events) && live.events.length > 0
|
|
? live.events
|
|
: sampleState.events,
|
|
isPreview: !hasLiveSessions,
|
|
};
|
|
}
|
|
|
|
function renderClients(state) {
|
|
const clients = state.clientSummaries.slice(0, 4);
|
|
document.getElementById("clientList").innerHTML = clients
|
|
.map((client, index) => {
|
|
const dot = index < 2 ? "green" : index === 2 ? "amber" : "";
|
|
const status = index < 2 ? "Running" : "Idle";
|
|
return `
|
|
<div class="list-row">
|
|
<div class="left-row">
|
|
<span class="dot ${dot}"></span>
|
|
<span class="truncate">${client.name || client.label || "Client"}</span>
|
|
</div>
|
|
<span class="row-muted">${client.sessionCount || 0} ${(client.sessionCount || 0) === 1 ? "session" : "sessions"}</span>
|
|
<span style="color: ${status === "Running" ? "var(--green)" : "var(--blue)"}">${status}</span>
|
|
</div>
|
|
`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
function renderEvents(state) {
|
|
document.getElementById("eventList").innerHTML = state.events
|
|
.slice(0, 4)
|
|
.map((event) => `
|
|
<div class="event-row">
|
|
<div class="left-row">
|
|
<span class="dot ${statusColor("", event.severity)}"></span>
|
|
<span class="truncate">${event.title || "Event"}</span>
|
|
</div>
|
|
<span class="truncate row-muted" style="max-width: 180px">${event.body || ""}</span>
|
|
<span class="row-muted">${timeAgo(event.timestamp)}</span>
|
|
</div>
|
|
`)
|
|
.join("");
|
|
}
|
|
|
|
function renderOverview(session) {
|
|
const input = fmtNumber(session.inputTokens || 124800);
|
|
const output = fmtNumber(session.outputTokens || 56300);
|
|
const agents = Math.max(1, Math.min(5, Number(session.agentCount || 4)));
|
|
document.getElementById("overview").innerHTML = `
|
|
<div>
|
|
<div class="kv-label">Model</div>
|
|
<div class="kv-value">${session.model || "claude-sonnet-4.6"}</div>
|
|
</div>
|
|
<div>
|
|
<div class="kv-label">Workspace</div>
|
|
<div class="kv-value">${session.workspaceRoot || "~/Projects/atlas"}</div>
|
|
</div>
|
|
<div>
|
|
<div class="kv-label">CWD</div>
|
|
<div class="kv-value">${session.cwd || session.workspaceRoot || "~/Projects/atlas/server"}</div>
|
|
</div>
|
|
<div>
|
|
<div class="kv-label">Tokens</div>
|
|
<div class="kv-value">${input} input ${output} output</div>
|
|
<div class="token-bar"><span></span><span></span></div>
|
|
</div>
|
|
<div>
|
|
<div class="kv-label">Cost (est.)</div>
|
|
<div class="kv-value">${fmtCost(session.totalCost || 0.036)}</div>
|
|
</div>
|
|
<div>
|
|
<div class="kv-label">Agents</div>
|
|
<div class="agent-dots">
|
|
${Array.from({ length: agents }, (_, i) => `<span class="agent-dot">${["A", "S", "P", "Sk", "+1"][i] || "A"}</span>`).join("")}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderTranscript(session, state) {
|
|
const subject = session.title || "session";
|
|
const rows = [
|
|
["9:33:12", "architect", `Analyzing ${subject} dependencies and session flow...`],
|
|
["9:33:15", "security", "Checking for vulnerabilities in auth middleware..."],
|
|
["9:33:18", "pragmatist", "Proposing simplified token refresh logic..."],
|
|
["9:33:21", "skeptic", "Could this break existing mobile clients?"],
|
|
["9:33:24", "architect", "Not with the compatibility layer I am adding."],
|
|
["9:33:27", "pragmatist", `Implementing changes in ${session.cwd || "workspace"}`],
|
|
["9:33:34", "tool", "Read file services/auth.service.ts"],
|
|
["9:33:41", "tool", "Wrote file services/auth.service.ts"],
|
|
];
|
|
document.getElementById("transcript").innerHTML =
|
|
rows
|
|
.map(
|
|
([time, role, text]) => `
|
|
<div class="transcript-row">
|
|
<span>${time}</span>
|
|
<span class="role ${role}">${role[0].toUpperCase()}${role.slice(1)}</span>
|
|
<span>${text}</span>
|
|
</div>
|
|
`,
|
|
)
|
|
.join("") +
|
|
`<div class="row-muted" style="margin-top: 10px"><span class="stream-dot"></span>${state.isPreview ? "Previewing layout..." : "Streaming live..."}</div>`;
|
|
}
|
|
|
|
function renderTimeline(session) {
|
|
const items = [
|
|
["9:32 AM", "Session started"],
|
|
["9:32 AM", "Analyzing codebase"],
|
|
["9:32 AM", "Scanned files"],
|
|
["9:33 AM", "Built implementation plan"],
|
|
["9:33 AM", "Modified services/auth.service.ts"],
|
|
["9:34 AM", "Ran unit tests"],
|
|
["9:34 AM", "24/24 tests passed"],
|
|
["9:35 AM", "Updating docs"],
|
|
["", String(session.status || "running").toLowerCase() === "running" ? "In progress..." : "Idle"],
|
|
];
|
|
document.getElementById("timeline").innerHTML = items
|
|
.map((item, index) => `
|
|
<div class="timeline-row ${index === items.length - 1 ? "active" : ""}">
|
|
<span>${item[0]}</span>
|
|
<span class="timeline-node"></span>
|
|
<span class="truncate">${item[1]}</span>
|
|
</div>
|
|
`)
|
|
.join("");
|
|
}
|
|
|
|
function renderInspector(state) {
|
|
const session = state.sessionSummaries[0] || sampleState.sessionSummaries[0];
|
|
document.getElementById("inspector").classList.toggle("demo", Boolean(state.isPreview));
|
|
document.getElementById("sessionTitle").textContent = session.title || "No active session";
|
|
document.getElementById("sessionStatus").textContent =
|
|
String(session.status || "running").replace(/^\w/, (c) => c.toUpperCase());
|
|
document.getElementById("sessionDot").className = `dot ${statusColor(session.status)}`;
|
|
document.getElementById("sessionMeta").textContent =
|
|
`${session.workspaceName || "workspace"} - Started ${timeAgo(session.createdAt)} - ID: ${shortId(session.sessionId)}`;
|
|
renderOverview(session);
|
|
renderTranscript(session, state);
|
|
renderTimeline(session);
|
|
|
|
const duration = elapsed(session.createdAt);
|
|
const progress = String(session.status || "").toLowerCase() === "idle" ? 100 : 62;
|
|
document.getElementById("progressValue").textContent = `${progress}%`;
|
|
document.getElementById("progressSub").textContent = progress === 100 ? "Complete" : "12 / 19 tasks";
|
|
document.getElementById("testsValue").textContent = progress === 100 ? "24/24" : "24/24";
|
|
document.getElementById("filesValue").textContent = String(Math.max(1, session.agentCount || 4) * 2);
|
|
document.getElementById("durationValue").textContent = duration;
|
|
}
|
|
|
|
function render(rawState) {
|
|
const state = normalize(rawState);
|
|
lastState = state;
|
|
const clients = state.clientSummaries.length;
|
|
const sessions = state.sessionSummaries.length;
|
|
const running = state.sessionSummaries.filter((s) =>
|
|
String(s.status || "").toLowerCase().includes("running"),
|
|
).length;
|
|
|
|
document.getElementById("clientCount").textContent = String(clients);
|
|
document.getElementById("sessionCount").textContent = String(sessions);
|
|
document.getElementById("runningCount").textContent = String(Math.max(1, running));
|
|
document.getElementById("uptime").textContent = state.hubUptime || sampleState.hubUptime;
|
|
document.getElementById("hubStatus").innerHTML = `
|
|
<span class="dot ${state.connected ? "green" : "red"}"></span>
|
|
<span>${state.connected ? "Hub Connected" : "Hub Offline"}</span>
|
|
`;
|
|
renderClients(state);
|
|
renderEvents(state);
|
|
renderInspector(state);
|
|
}
|
|
|
|
function addLocalSession(prompt) {
|
|
const now = Date.now();
|
|
const title = prompt.length > 28 ? `${prompt.slice(0, 25)}...` : prompt;
|
|
const next = normalize(lastState);
|
|
next.isPreview = false;
|
|
next.sessionSummaries = [
|
|
{
|
|
sessionId: `local_${now.toString(16)}`,
|
|
title,
|
|
status: "running",
|
|
workspaceRoot: "~/Projects/atlas",
|
|
workspaceName: "local-preview",
|
|
cwd: "~/Projects/atlas/server",
|
|
model: "claude-sonnet-4.6",
|
|
provider: "anthropic",
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
inputTokens: 12800,
|
|
outputTokens: 2400,
|
|
totalCost: 0.012,
|
|
agentCount: 4,
|
|
},
|
|
...next.sessionSummaries,
|
|
];
|
|
next.events = [
|
|
{
|
|
title: "local-preview",
|
|
body: `Started session "${title}"`,
|
|
severity: "success",
|
|
timestamp: now,
|
|
},
|
|
...next.events,
|
|
];
|
|
next.clientSummaries = next.clientSummaries.map((client, index) =>
|
|
index === 0
|
|
? { ...client, sessionCount: Number(client.sessionCount || 0) + 1 }
|
|
: client,
|
|
);
|
|
render(next);
|
|
}
|
|
|
|
function stopLocalSession() {
|
|
const next = normalize(lastState);
|
|
next.isPreview = false;
|
|
const [current, ...rest] = next.sessionSummaries;
|
|
if (!current) return;
|
|
const stopped = { ...current, status: "idle", updatedAt: Date.now() };
|
|
next.sessionSummaries = [stopped, ...rest];
|
|
next.events = [
|
|
{
|
|
title: "local-preview",
|
|
body: `Stopped session "${stopped.title}"`,
|
|
severity: "warn",
|
|
timestamp: Date.now(),
|
|
},
|
|
...next.events,
|
|
];
|
|
render(next);
|
|
}
|
|
|
|
async function refresh() {
|
|
if (!invoke) {
|
|
render(sampleState);
|
|
return;
|
|
}
|
|
try {
|
|
const state = await invoke("get_hub_state");
|
|
render(state);
|
|
} catch (error) {
|
|
render(lastState || sampleState);
|
|
}
|
|
}
|
|
|
|
function updateClock() {
|
|
const now = new Date();
|
|
document.getElementById("clock").textContent = now.toLocaleString(undefined, {
|
|
weekday: "short",
|
|
hour: "numeric",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
|
|
document.getElementById("newSession").addEventListener("click", async () => {
|
|
const prompt = window.prompt("Start a background Cline session", "Summarize the current workspace and suggest the next code task.");
|
|
if (!prompt) return;
|
|
if (!invoke) {
|
|
addLocalSession(prompt);
|
|
return;
|
|
}
|
|
try {
|
|
await invoke("start_new_session", { prompt });
|
|
await refresh();
|
|
} catch (error) {
|
|
window.alert(String(error));
|
|
}
|
|
});
|
|
|
|
document.getElementById("openDashboard").addEventListener("click", () => {
|
|
refresh();
|
|
});
|
|
|
|
document.getElementById("stopButton").addEventListener("click", async () => {
|
|
const session = normalize(lastState).sessionSummaries[0];
|
|
if (!session?.sessionId) return;
|
|
if (!invoke) {
|
|
stopLocalSession();
|
|
return;
|
|
}
|
|
try {
|
|
await invoke("abort_session", { sessionId: session.sessionId });
|
|
stopLocalSession();
|
|
await refresh();
|
|
} catch (error) {
|
|
window.alert(String(error));
|
|
}
|
|
});
|
|
|
|
fitScene();
|
|
window.addEventListener("resize", fitScene);
|
|
updateClock();
|
|
render(sampleState);
|
|
refresh();
|
|
setInterval(updateClock, 15000);
|
|
setInterval(refresh, 1000);
|
|
</script>
|
|
</body>
|
|
</html>
|