sessionboard 0.0.0-stage → 0.1.0
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/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +7 -0
- package/LICENSE +9 -0
- package/README.md +64 -2
- package/bin/sessionboard.mjs +1038 -0
- package/commands/dashboard.md +9 -0
- package/commands/share.md +10 -0
- package/commands/top.md +7 -0
- package/hooks/hooks.json +11 -0
- package/lib/dashboard.html +313 -0
- package/package.json +34 -4
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Open the sessionboard dashboard in your browser
|
|
3
|
+
allowed-tools: Bash(node:*)
|
|
4
|
+
---
|
|
5
|
+
Start the dashboard in the background with the Bash tool (`run_in_background: true`):
|
|
6
|
+
|
|
7
|
+
`node "${CLAUDE_PLUGIN_ROOT}/bin/sessionboard.mjs" dashboard`
|
|
8
|
+
|
|
9
|
+
It opens http://localhost:4317 (or the next free port) in the browser. Tell the user the URL from its output, and that it stops when this session ends.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Export one of your top sessions as a redacted, shareable page (default rank 1)
|
|
3
|
+
argument-hint: "[rank|session-id] [--html] [--copy] [--gist]"
|
|
4
|
+
allowed-tools: Bash(node:*)
|
|
5
|
+
---
|
|
6
|
+
Run this and report the output path to the user:
|
|
7
|
+
|
|
8
|
+
`node "${CLAUDE_PLUGIN_ROOT}/bin/sessionboard.mjs" share $ARGUMENTS`
|
|
9
|
+
|
|
10
|
+
If no format flag was given, add `--html`. Only pass `--gist` if the user explicitly asked for a gist, since it uploads to GitHub. Remind them to skim the export before sending it; redaction catches common secrets, not everything.
|
package/commands/top.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show your top-rated agent sessions
|
|
3
|
+
allowed-tools: Bash(node:*)
|
|
4
|
+
---
|
|
5
|
+
!`node "${CLAUDE_PLUGIN_ROOT}/bin/sessionboard.mjs" top`
|
|
6
|
+
|
|
7
|
+
Show the leaderboard above to the user as-is. If it says AI rating is off, tell them to run `node "${CLAUDE_PLUGIN_ROOT}/bin/sessionboard.mjs" init` in a terminal (it asks a couple of setup questions).
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>sessionboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root{
|
|
9
|
+
--bg:#f7f6f3;--fg:#1d1c1a;--mute:#6f6b64;--faint:#a29d94;--card:#fff;--line:#e5e1da;--hover:#f1eee8;
|
|
10
|
+
--accent:#c4643f;--accent-soft:#f6e6de;--gold:#b8860b;--silver:#7d8590;--bronze:#a0522d;--err:#b3261e;--ok:#2f7d4f;
|
|
11
|
+
--shadow:0 1px 2px rgba(0,0,0,.04),0 4px 16px rgba(0,0,0,.04);
|
|
12
|
+
}
|
|
13
|
+
@media (prefers-color-scheme:dark){:root{
|
|
14
|
+
--bg:#171614;--fg:#ecebe7;--mute:#a19c93;--faint:#6d6961;--card:#211f1d;--line:#34312d;--hover:#2a2825;
|
|
15
|
+
--accent:#e08560;--accent-soft:#3a2a22;--gold:#e0b440;--silver:#aab2bd;--bronze:#d08a5c;--err:#f2b8b5;--ok:#7fd1a0;
|
|
16
|
+
--shadow:none;
|
|
17
|
+
}}
|
|
18
|
+
*{box-sizing:border-box}
|
|
19
|
+
body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;-webkit-font-smoothing:antialiased}
|
|
20
|
+
button{font:inherit;color:inherit;cursor:pointer}
|
|
21
|
+
code,.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
22
|
+
.wrap{max-width:1120px;margin:0 auto;padding:32px 16px 80px}
|
|
23
|
+
header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:28px}
|
|
24
|
+
.brand{display:flex;align-items:baseline;gap:10px}
|
|
25
|
+
.brand h1{font-size:22px;margin:0;letter-spacing:-.01em}
|
|
26
|
+
.brand .sub{color:var(--mute);font-size:13px}
|
|
27
|
+
.btn{border:1px solid var(--line);background:var(--card);border-radius:8px;padding:6px 12px;font-size:13px;display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
|
|
28
|
+
.btn:hover{background:var(--hover)}
|
|
29
|
+
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
30
|
+
.btn.primary:hover{filter:brightness(1.05)}
|
|
31
|
+
.btn.ghost{border-color:transparent;background:transparent;padding:4px 8px;color:var(--mute)}
|
|
32
|
+
.btn.ghost:hover{background:var(--hover);color:var(--fg)}
|
|
33
|
+
.btn[disabled]{opacity:.5;cursor:default}
|
|
34
|
+
.status{color:var(--mute);font-size:12px}
|
|
35
|
+
.notice{background:var(--accent-soft);border:1px solid var(--line);border-radius:10px;padding:10px 14px;margin-bottom:20px;font-size:13px}
|
|
36
|
+
|
|
37
|
+
h2{font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--mute);margin:0 0 12px}
|
|
38
|
+
.podium{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin-bottom:40px}
|
|
39
|
+
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:18px;box-shadow:var(--shadow);display:flex;flex-direction:column;gap:10px;min-width:0}
|
|
40
|
+
.card .top{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
41
|
+
.rank{font-weight:700;font-size:13px;display:inline-flex;align-items:center;gap:6px}
|
|
42
|
+
.rank .medal{width:22px;height:22px;border-radius:50%;display:inline-grid;place-items:center;color:#fff;font-size:12px}
|
|
43
|
+
.m1{background:var(--gold)}.m2{background:var(--silver)}.m3{background:var(--bronze)}.mx{background:var(--faint)}
|
|
44
|
+
.score{font-weight:700;font-size:22px;letter-spacing:-.02em}
|
|
45
|
+
.score small{font-size:13px;color:var(--mute);font-weight:500}
|
|
46
|
+
.card h3{margin:0;font-size:16px;line-height:1.35;overflow-wrap:anywhere}
|
|
47
|
+
.card .summary{margin:0;color:var(--fg)}
|
|
48
|
+
.card .why{margin:0;color:var(--mute);font-size:13px}
|
|
49
|
+
.meta{color:var(--mute);font-size:12px;display:flex;flex-wrap:wrap;gap:4px 10px}
|
|
50
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px}
|
|
51
|
+
.chip{font-size:12px;background:var(--hover);border-radius:999px;padding:2px 9px;color:var(--mute)}
|
|
52
|
+
.card .actions{display:flex;gap:6px;margin-top:auto;padding-top:6px;flex-wrap:wrap}
|
|
53
|
+
.tag{font-size:11px;border-radius:999px;padding:1px 8px;border:1px solid var(--line);color:var(--mute)}
|
|
54
|
+
.tag.warn{color:var(--err);border-color:currentColor}
|
|
55
|
+
|
|
56
|
+
.tablecard{background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);overflow:hidden}
|
|
57
|
+
.toolbar{display:flex;gap:10px;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid var(--line);flex-wrap:wrap}
|
|
58
|
+
.toolbar input{border:1px solid var(--line);background:var(--bg);color:var(--fg);border-radius:8px;padding:6px 10px;font:inherit;font-size:13px;min-width:0;flex:1;max-width:280px}
|
|
59
|
+
.toolbar label{font-size:13px;color:var(--mute);display:flex;align-items:center;gap:6px}
|
|
60
|
+
.tscroll{overflow-x:auto}
|
|
61
|
+
table{width:100%;border-collapse:collapse;font-size:13px}
|
|
62
|
+
th{text-align:left;font-weight:600;color:var(--mute);font-size:12px;padding:10px 14px;border-bottom:1px solid var(--line);white-space:nowrap;cursor:pointer;user-select:none}
|
|
63
|
+
th:hover{color:var(--fg)}
|
|
64
|
+
th.num,td.num{text-align:right}
|
|
65
|
+
td{padding:10px 14px;border-bottom:1px solid var(--line);vertical-align:top}
|
|
66
|
+
tr:last-child td{border-bottom:0}
|
|
67
|
+
tbody tr{cursor:pointer}
|
|
68
|
+
tbody tr:hover{background:var(--hover)}
|
|
69
|
+
tr.dim td{color:var(--faint)}
|
|
70
|
+
td .t{font-weight:500;color:var(--fg);overflow-wrap:anywhere}
|
|
71
|
+
tr.dim td .t{color:var(--faint)}
|
|
72
|
+
td .s{color:var(--mute);font-size:12px;margin-top:2px;max-width:520px}
|
|
73
|
+
.bar{display:inline-block;width:44px;height:6px;border-radius:3px;background:var(--hover);vertical-align:middle;margin-left:8px;overflow:hidden}
|
|
74
|
+
.bar i{display:block;height:100%;background:var(--accent)}
|
|
75
|
+
|
|
76
|
+
/* drawer */
|
|
77
|
+
.scrim{position:fixed;inset:0;background:rgba(0,0,0,.35);opacity:0;pointer-events:none;transition:opacity .15s}
|
|
78
|
+
.scrim.open{opacity:1;pointer-events:auto}
|
|
79
|
+
.drawer{position:fixed;top:0;right:0;bottom:0;width:min(760px,100%);background:var(--bg);border-left:1px solid var(--line);transform:translateX(100%);transition:transform .2s ease;display:flex;flex-direction:column;z-index:2}
|
|
80
|
+
.drawer.open{transform:none}
|
|
81
|
+
.dhead{padding:18px 20px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:8px;background:var(--card)}
|
|
82
|
+
.dhead .row{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}
|
|
83
|
+
.dhead h3{margin:0;font-size:18px;line-height:1.3;overflow-wrap:anywhere}
|
|
84
|
+
.dbody{overflow-y:auto;padding:16px 20px 60px;flex:1}
|
|
85
|
+
.turn{margin:12px 0;padding:12px 14px;border-radius:12px;border:1px solid var(--line);background:var(--card)}
|
|
86
|
+
.turn.user{background:var(--accent-soft)}
|
|
87
|
+
.who{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--accent);margin-bottom:4px}
|
|
88
|
+
.txt{white-space:pre-wrap;overflow-wrap:anywhere;margin:4px 0}
|
|
89
|
+
.tool{font-size:12px;color:var(--mute);margin:3px 0;overflow-wrap:anywhere}
|
|
90
|
+
.tool b{font-weight:600;margin-right:6px}
|
|
91
|
+
.tool.err b{color:var(--err)}
|
|
92
|
+
.menu{position:relative;display:inline-block}
|
|
93
|
+
.menu .pop{position:absolute;right:0;top:calc(100% + 4px);background:var(--card);border:1px solid var(--line);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.12);padding:4px;min-width:210px;display:none;z-index:5}
|
|
94
|
+
.menu.open .pop{display:block}
|
|
95
|
+
.pop button{display:block;width:100%;text-align:left;border:0;background:transparent;padding:7px 10px;border-radius:6px;font-size:13px}
|
|
96
|
+
.pop button:hover{background:var(--hover)}
|
|
97
|
+
.pop .hint{font-size:11px;color:var(--mute);padding:6px 10px 4px}
|
|
98
|
+
.toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(20px);background:var(--fg);color:var(--bg);padding:8px 14px;border-radius:8px;font-size:13px;opacity:0;transition:all .2s;pointer-events:none;z-index:10}
|
|
99
|
+
.toast.show{opacity:1;transform:translateX(-50%)}
|
|
100
|
+
.empty{padding:40px 16px;text-align:center;color:var(--mute)}
|
|
101
|
+
@media (max-width:640px){.hide-sm{display:none}}
|
|
102
|
+
</style>
|
|
103
|
+
</head>
|
|
104
|
+
<body>
|
|
105
|
+
<div class="wrap">
|
|
106
|
+
<header>
|
|
107
|
+
<div class="brand"><h1>sessionboard</h1><span class="sub" id="sub">your best agent sessions</span></div>
|
|
108
|
+
<div style="display:flex;gap:10px;align-items:center">
|
|
109
|
+
<span class="status" id="status"></span>
|
|
110
|
+
<button class="btn" id="refresh">↻ Refresh</button>
|
|
111
|
+
</div>
|
|
112
|
+
</header>
|
|
113
|
+
<div id="notice"></div>
|
|
114
|
+
|
|
115
|
+
<h2 id="podiumTitle">Top sessions</h2>
|
|
116
|
+
<div class="podium" id="podium"></div>
|
|
117
|
+
|
|
118
|
+
<h2>All sessions</h2>
|
|
119
|
+
<div class="tablecard">
|
|
120
|
+
<div class="toolbar">
|
|
121
|
+
<input id="q" placeholder="Search title, summary, project…" type="search">
|
|
122
|
+
<label><input type="checkbox" id="showAll"> show trivial & hidden</label>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="tscroll"><table>
|
|
125
|
+
<thead><tr>
|
|
126
|
+
<th data-k="rank" class="num">#</th>
|
|
127
|
+
<th data-k="title">Session</th>
|
|
128
|
+
<th data-k="score" class="num">Score</th>
|
|
129
|
+
<th data-k="date" class="hide-sm">Date</th>
|
|
130
|
+
<th data-k="tools" class="num hide-sm">Tools</th>
|
|
131
|
+
<th data-k="files" class="num hide-sm">Files</th>
|
|
132
|
+
<th data-k="active" class="num hide-sm">Active</th>
|
|
133
|
+
</tr></thead>
|
|
134
|
+
<tbody id="rows"></tbody>
|
|
135
|
+
</table></div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="scrim" id="scrim"></div>
|
|
140
|
+
<aside class="drawer" id="drawer" aria-hidden="true">
|
|
141
|
+
<div class="dhead" id="dhead"></div>
|
|
142
|
+
<div class="dbody" id="dbody"></div>
|
|
143
|
+
</aside>
|
|
144
|
+
<div class="toast" id="toast"></div>
|
|
145
|
+
|
|
146
|
+
<script>
|
|
147
|
+
const $ = (s) => document.querySelector(s);
|
|
148
|
+
const esc = (t) => String(t ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
149
|
+
const api = (p, opts = {}) => fetch(p, { ...opts, headers: { 'x-sessionboard': '1', ...(opts.headers || {}) } });
|
|
150
|
+
let state = { sessions: [], topN: 3 }, sortKey = 'rank', sortDir = 1, current = null, poll = null;
|
|
151
|
+
|
|
152
|
+
function toast(msg) { const t = $('#toast'); t.textContent = msg; t.classList.add('show'); clearTimeout(t._h); t._h = setTimeout(() => t.classList.remove('show'), 2200); }
|
|
153
|
+
|
|
154
|
+
async function load() {
|
|
155
|
+
const r = await api('/api/sessions');
|
|
156
|
+
state = await r.json();
|
|
157
|
+
render();
|
|
158
|
+
// keep polling while a judge run is in progress
|
|
159
|
+
clearTimeout(poll);
|
|
160
|
+
if (state.judging) poll = setTimeout(load, 2500);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const scoreOf = (s) => s.judge ? s.judge.score : null;
|
|
164
|
+
const medal = (r) => `<span class="medal ${r <= 3 ? 'm' + r : 'mx'}">${r}</span>`;
|
|
165
|
+
const chips = (s) => {
|
|
166
|
+
const c = [`${s.stats.toolCalls} tool calls`];
|
|
167
|
+
if (s.stats.files) c.push(`${s.stats.files} files`);
|
|
168
|
+
if (s.stats.tests) c.push(`${s.stats.tests} test/build runs`);
|
|
169
|
+
if (s.stats.commits) c.push(`${s.stats.commits} commits`);
|
|
170
|
+
if (s.stats.prs) c.push(`${s.stats.prs} PRs`);
|
|
171
|
+
c.push(`${s.stats.prompts} prompts`, `${s.active} active`);
|
|
172
|
+
return c.map((x) => `<span class="chip">${esc(x)}</span>`).join('');
|
|
173
|
+
};
|
|
174
|
+
const tags = (s) => [
|
|
175
|
+
s.pinned && '<span class="tag">📌 pinned</span>',
|
|
176
|
+
s.hidden && '<span class="tag">hidden</span>',
|
|
177
|
+
s.missing && '<span class="tag warn">transcript deleted</span>',
|
|
178
|
+
state.judging === s.id && '<span class="tag">judging…</span>',
|
|
179
|
+
].filter(Boolean).join(' ');
|
|
180
|
+
|
|
181
|
+
function shareMenu(s) {
|
|
182
|
+
if (s.missing) return '';
|
|
183
|
+
return `<div class="menu" data-menu="${s.id}">
|
|
184
|
+
<button class="btn primary" data-act="menu">Share ▾</button>
|
|
185
|
+
<div class="pop">
|
|
186
|
+
<div class="hint">Redacted: keys, tokens, secrets, emails, home paths</div>
|
|
187
|
+
<button data-act="dl-html">Download HTML page</button>
|
|
188
|
+
<button data-act="dl-md">Download Markdown</button>
|
|
189
|
+
<button data-act="copy-md">Copy Markdown</button>
|
|
190
|
+
<button data-act="open-html">Open shareable page</button>
|
|
191
|
+
</div></div>`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function render() {
|
|
195
|
+
const ranked = state.sessions.filter((s) => s.rank).sort((a, b) => a.rank - b.rank);
|
|
196
|
+
const top = ranked.slice(0, state.topN);
|
|
197
|
+
$('#podiumTitle').textContent = `Top ${state.topN} sessions`;
|
|
198
|
+
$('#sub').textContent = `${state.sessions.length} sessions · ${ranked.filter((s) => s.judge).length} rated`;
|
|
199
|
+
$('#status').textContent = state.judging === 'all' ? 'Rating new sessions…' : state.judging ? 'Rating a session…' : state.updatedAt ? `updated ${new Date(state.updatedAt).toLocaleTimeString()}` : '';
|
|
200
|
+
$('#notice').innerHTML = !state.judgeEnabled ? '<div class="notice">AI rating is off. Run <code>sessionboard config judge=true</code> to enable it.</div>'
|
|
201
|
+
: ranked.length && !ranked.some((s) => s.judge) ? '<div class="notice">Sessions aren\'t rated yet. Run <code>sessionboard init</code> in your terminal to set up rating.</div>' : '';
|
|
202
|
+
|
|
203
|
+
$('#podium').innerHTML = top.length ? top.map((s) => `
|
|
204
|
+
<article class="card" data-id="${s.id}">
|
|
205
|
+
<div class="top">
|
|
206
|
+
<span class="rank">${medal(s.rank)} ${tags(s)}</span>
|
|
207
|
+
<span class="score">${scoreOf(s) ?? '–'}<small>/10</small></span>
|
|
208
|
+
</div>
|
|
209
|
+
<h3>${esc(s.title)}</h3>
|
|
210
|
+
${s.judge?.summary ? `<p class="summary">${esc(s.judge.summary)}</p>` : ''}
|
|
211
|
+
${s.judge?.reason ? `<p class="why">${esc(s.judge.reason)}</p>` : ''}
|
|
212
|
+
<div class="meta"><span>${esc(s.date)}</span><span class="mono">${esc(s.project || '')}</span></div>
|
|
213
|
+
<div class="chips">${chips(s)}</div>
|
|
214
|
+
<div class="actions">
|
|
215
|
+
${shareMenu(s)}
|
|
216
|
+
<button class="btn" data-act="view">Preview</button>
|
|
217
|
+
<button class="btn ghost" data-act="${s.pinned ? 'unpin' : 'pin'}">${s.pinned ? 'Unpin' : 'Pin'}</button>
|
|
218
|
+
<button class="btn ghost" data-act="hide">Hide</button>
|
|
219
|
+
</div>
|
|
220
|
+
</article>`).join('') : '<div class="empty">No eligible sessions yet. Do some real work with Claude Code, then refresh.</div>';
|
|
221
|
+
|
|
222
|
+
const q = $('#q').value.trim().toLowerCase();
|
|
223
|
+
const showAll = $('#showAll').checked;
|
|
224
|
+
let rows = state.sessions.filter((s) => showAll || s.rank);
|
|
225
|
+
if (q) rows = rows.filter((s) => [s.title, s.project, s.judge?.summary, s.judge?.reason].join(' ').toLowerCase().includes(q));
|
|
226
|
+
const key = {
|
|
227
|
+
rank: (s) => s.rank ?? 1e9, title: (s) => s.title.toLowerCase(), score: (s) => -(scoreOf(s) ?? -1) * 1000 - s.heuristic,
|
|
228
|
+
date: (s) => -(Date.parse(s.start) || 0), tools: (s) => -s.stats.toolCalls, files: (s) => -s.stats.files, active: (s) => -s.activeMinutes,
|
|
229
|
+
}[sortKey];
|
|
230
|
+
rows.sort((a, b) => (key(a) > key(b) ? 1 : key(a) < key(b) ? -1 : 0) * sortDir);
|
|
231
|
+
$('#rows').innerHTML = rows.length ? rows.map((s) => {
|
|
232
|
+
const sc = scoreOf(s);
|
|
233
|
+
return `<tr data-id="${s.id}" class="${s.rank ? '' : 'dim'}">
|
|
234
|
+
<td class="num">${s.rank ?? '–'}</td>
|
|
235
|
+
<td><div class="t">${esc(s.title)} ${tags(s)}</div><div class="s">${esc(s.judge?.summary || s.project || '')}</div></td>
|
|
236
|
+
<td class="num" style="white-space:nowrap">${sc ?? '–'}${sc != null ? `<span class="bar"><i style="width:${sc * 10}%"></i></span>` : ''}</td>
|
|
237
|
+
<td class="hide-sm" style="white-space:nowrap">${esc(s.date)}</td>
|
|
238
|
+
<td class="num hide-sm">${s.stats.toolCalls}</td>
|
|
239
|
+
<td class="num hide-sm">${s.stats.files}</td>
|
|
240
|
+
<td class="num hide-sm" style="white-space:nowrap">${esc(s.active)}</td>
|
|
241
|
+
</tr>`;
|
|
242
|
+
}).join('') : '<tr><td colspan="7" class="empty">Nothing matches.</td></tr>';
|
|
243
|
+
|
|
244
|
+
if (current) renderDrawerHead(state.sessions.find((s) => s.id === current));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function renderDrawerHead(s) {
|
|
248
|
+
if (!s) return;
|
|
249
|
+
$('#dhead').innerHTML = `
|
|
250
|
+
<div class="row"><h3>${esc(s.title)}</h3><button class="btn ghost" data-act="close" aria-label="Close">✕</button></div>
|
|
251
|
+
<div class="meta"><span>${s.rank ? '#' + s.rank : 'unranked'}</span><span><b>${scoreOf(s) ?? '–'}</b>/10</span><span>${esc(s.date)}</span><span class="mono">${esc(s.project || '')}</span>${s.model ? `<span>${esc(s.model)}</span>` : ''}</div>
|
|
252
|
+
${s.judge?.summary ? `<div>${esc(s.judge.summary)}</div>` : ''}
|
|
253
|
+
${s.judge?.reason ? `<div class="why" style="color:var(--mute);font-size:13px">why: ${esc(s.judge.reason)}</div>` : ''}
|
|
254
|
+
<div class="chips">${chips(s)} ${tags(s)}</div>
|
|
255
|
+
<div class="actions" style="display:flex;gap:6px;flex-wrap:wrap" data-id="${s.id}">
|
|
256
|
+
${shareMenu(s)}
|
|
257
|
+
<button class="btn" data-act="rejudge" ${state.judging || s.missing ? 'disabled' : ''}>Re-rate</button>
|
|
258
|
+
<button class="btn ghost" data-act="${s.pinned ? 'unpin' : 'pin'}">${s.pinned ? 'Unpin' : 'Pin'}</button>
|
|
259
|
+
<button class="btn ghost" data-act="${s.hidden ? 'unhide' : 'hide'}">${s.hidden ? 'Unhide' : 'Hide'}</button>
|
|
260
|
+
</div>`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function openDrawer(id) {
|
|
264
|
+
current = id;
|
|
265
|
+
renderDrawerHead(state.sessions.find((s) => s.id === id));
|
|
266
|
+
$('#dbody').innerHTML = '<div class="empty">Loading transcript…</div>';
|
|
267
|
+
$('#drawer').classList.add('open'); $('#scrim').classList.add('open'); $('#drawer').setAttribute('aria-hidden', 'false');
|
|
268
|
+
const r = await api(`/api/sessions/${id}/transcript`);
|
|
269
|
+
const data = await r.json();
|
|
270
|
+
if (current !== id) return;
|
|
271
|
+
if (!r.ok) { $('#dbody').innerHTML = `<div class="empty">${esc(data.error)}</div>`; return; }
|
|
272
|
+
$('#dbody').innerHTML = data.turns.map((t) => `<div class="turn ${t.role}"><div class="who">${t.role === 'user' ? 'You' : 'Claude'}</div>${
|
|
273
|
+
t.parts.map((p) => p.kind === 'tool'
|
|
274
|
+
? `<div class="tool ${p.error ? 'err' : ''}"><b>${esc(p.name)}</b><code>${esc(p.text.slice(0, 180))}</code></div>`
|
|
275
|
+
: `<div class="txt">${esc(p.text)}</div>`).join('')}</div>`).join('');
|
|
276
|
+
}
|
|
277
|
+
function closeDrawer() { current = null; $('#drawer').classList.remove('open'); $('#scrim').classList.remove('open'); $('#drawer').setAttribute('aria-hidden', 'true'); }
|
|
278
|
+
|
|
279
|
+
async function act(action, id, el) {
|
|
280
|
+
const base = `/api/sessions/${id}`;
|
|
281
|
+
if (action === 'menu') { const m = el.closest('.menu'); const open = m.classList.contains('open'); document.querySelectorAll('.menu.open').forEach((x) => x.classList.remove('open')); if (!open) m.classList.add('open'); return; }
|
|
282
|
+
document.querySelectorAll('.menu.open').forEach((x) => x.classList.remove('open'));
|
|
283
|
+
if (action === 'view') return openDrawer(id);
|
|
284
|
+
if (action === 'close') return closeDrawer();
|
|
285
|
+
if (action === 'dl-html' || action === 'dl-md') { location.href = `${base}/export?format=${action === 'dl-html' ? 'html' : 'md'}&download=1`; return toast('Downloading…'); }
|
|
286
|
+
if (action === 'open-html') return window.open(`${base}/export?format=html`, '_blank');
|
|
287
|
+
if (action === 'copy-md') {
|
|
288
|
+
const r = await api(`${base}/export?format=md`);
|
|
289
|
+
if (!r.ok) return toast((await r.json()).error);
|
|
290
|
+
await navigator.clipboard.writeText(await r.text());
|
|
291
|
+
return toast('Markdown copied. Skim it before sharing.');
|
|
292
|
+
}
|
|
293
|
+
if (['pin', 'unpin', 'hide', 'unhide'].includes(action)) { await api(`${base}/${action}`, { method: 'POST' }); toast(`${action[0].toUpperCase() + action.slice(1)}ned`.replace('Hidened', 'Hidden').replace('Unhidened', 'Unhidden')); return load(); }
|
|
294
|
+
if (action === 'rejudge') { const r = await api(`${base}/rejudge`, { method: 'POST' }); if (!r.ok) return toast((await r.json()).error); toast('Re-rating… (takes ~10s)'); return load(); }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
document.addEventListener('click', (e) => {
|
|
298
|
+
const btn = e.target.closest('[data-act]');
|
|
299
|
+
if (btn) { e.stopPropagation(); const id = btn.closest('[data-menu]')?.dataset.menu || btn.closest('[data-id]')?.dataset.id || current; return act(btn.dataset.act, id, btn); }
|
|
300
|
+
if (!e.target.closest('.menu')) document.querySelectorAll('.menu.open').forEach((x) => x.classList.remove('open'));
|
|
301
|
+
const row = e.target.closest('tbody tr[data-id], .card[data-id]');
|
|
302
|
+
if (row && !e.target.closest('.menu')) openDrawer(row.dataset.id);
|
|
303
|
+
});
|
|
304
|
+
$('#scrim').addEventListener('click', closeDrawer);
|
|
305
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeDrawer(); });
|
|
306
|
+
$('#q').addEventListener('input', render);
|
|
307
|
+
$('#showAll').addEventListener('change', render);
|
|
308
|
+
document.querySelectorAll('th[data-k]').forEach((th) => th.addEventListener('click', () => { sortDir = sortKey === th.dataset.k ? -sortDir : 1; sortKey = th.dataset.k; render(); }));
|
|
309
|
+
$('#refresh').addEventListener('click', async () => { $('#refresh').disabled = true; await api('/api/refresh', { method: 'POST' }); await load(); $('#refresh').disabled = false; toast(state.judging ? 'Rescanned; rating new or grown sessions…' : 'Rescanned; everything is up to date'); });
|
|
310
|
+
load();
|
|
311
|
+
</script>
|
|
312
|
+
</body>
|
|
313
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sessionboard",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An AI-judged leaderboard of your Claude Code sessions. Keeps your top 3 current and exports them, redacted, for sharing.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sessionboard": "bin/sessionboard.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib",
|
|
12
|
+
"commands",
|
|
13
|
+
"hooks",
|
|
14
|
+
".claude-plugin",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"claude-code",
|
|
22
|
+
"claude",
|
|
23
|
+
"agent",
|
|
24
|
+
"ai",
|
|
25
|
+
"session",
|
|
26
|
+
"portfolio",
|
|
27
|
+
"leaderboard"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/kbiam/sessionboard.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/kbiam/sessionboard",
|
|
35
|
+
"author": "Kush Bang"
|
|
36
|
+
}
|