k0-board 0.3.0 → 0.5.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/skills/k0-changelog/SKILL.md +2 -2
- package/.claude/skills/k0-discuss/SKILL.md +105 -0
- package/.claude/skills/k0-discuss/references/rounds.md +280 -0
- package/.claude/skills/k0-epic/SKILL.md +196 -0
- package/.claude/skills/k0-links/.claude-plugin/plugin.json +6 -0
- package/.claude/skills/k0-links/hooks/hooks.json +1 -0
- package/.claude/skills/k0-links/hooks/links.js +128 -0
- package/.claude/skills/k0-links/hooks/register.js +204 -0
- package/.claude/skills/k0-next/SKILL.md +82 -0
- package/.claude/skills/k0-order/SKILL.md +123 -0
- package/.claude/skills/k0-plan/SKILL.md +156 -0
- package/.claude/skills/k0-split/SKILL.md +169 -0
- package/.claude/skills/k0-story/SKILL.md +108 -0
- package/.claude/skills/k0-ultracode/SKILL.md +251 -0
- package/.claude/skills/k0-ultracode/references/agent-brief.md +71 -0
- package/.claude/skills/k0-ultracode/references/modes.md +49 -0
- package/.claude/skills/k0-verify/SKILL.md +219 -0
- package/.claude/skills/k0-whatsnew/SKILL.md +95 -0
- package/.claude/skills/k0-work/SKILL.md +212 -0
- package/CHANGELOG.md +426 -1
- package/README.md +1021 -119
- package/bin/k0-board.js +81 -11
- package/menubar/K0MenuBar.swift +35 -5
- package/package.json +13 -1
- package/platform/contract.js +37 -6
- package/platform/darwin/index.js +1 -0
- package/platform/darwin/shell.js +4 -1
- package/platform/darwin/terminal.js +230 -56
- package/platform/index.js +3 -1
- package/platform/linux/index.js +14 -0
- package/platform/linux/shell.js +10 -1
- package/platform/linux/tray.py +10 -10
- package/platform/shared/command.js +19 -3
- package/platform/shared/tmux.js +32 -14
- package/platform/win32/index.js +6 -2
- package/platform/win32/shell.js +4 -1
- package/platform/win32/terminal.js +24 -8
- package/platform/win32/tray.ps1 +13 -13
- package/server/backlog.js +618 -0
- package/server/changelog.js +13 -13
- package/server/db.js +1800 -146
- package/server/files.js +156 -26
- package/server/git.js +53 -14
- package/server/idle.js +15 -13
- package/server/index.js +1286 -189
- package/server/launcher.js +122 -14
- package/server/machine.js +10 -1
- package/server/mirror.js +1048 -0
- package/server/prices.js +59 -0
- package/server/projects.js +57 -12
- package/server/servers.js +22 -1
- package/server/sessions.js +5 -5
- package/server/settings.js +25 -2
- package/server/update.js +324 -0
- package/server/usage.js +642 -0
- package/server/watcher.js +67 -13
- package/server/worktree.js +600 -0
- package/server/writer.js +98 -26
- package/web/awake.js +62 -0
- package/web/base.css +4 -0
- package/web/board.css +404 -16
- package/web/board.js +1182 -152
- package/web/changelog.js +3 -3
- package/web/conf.js +118 -0
- package/web/files.css +206 -1
- package/web/files.html +10 -0
- package/web/files.js +533 -75
- package/web/fuzzy.js +73 -21
- package/web/index.html +109 -12
- package/web/json.js +145 -0
- package/web/list.css +536 -0
- package/web/list.js +1015 -0
- package/web/mentions.js +27 -9
- package/web/recency.js +10 -10
- package/web/units.js +1 -1
- package/web/usage.css +169 -0
- package/web/usage.html +78 -0
- package/web/usage.js +447 -0
- package/web/view.js +22 -3
- package/web/whatsnew.css +211 -0
- package/web/whatsnew.html +49 -0
- package/web/whatsnew.js +339 -0
package/web/mentions.js
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
// is why it costs nothing and never goes near the server: the file listing is already in the
|
|
9
9
|
// browser.
|
|
10
10
|
//
|
|
11
|
+
// The listing is not quite everything there is, though, so what it could not place it hands back
|
|
12
|
+
// under `unknown` rather than swallowing it. Asking the disk about those names is somebody
|
|
13
|
+
// else's job; this module stays a pure function of a text and a list.
|
|
14
|
+
//
|
|
11
15
|
// What is written out in full and what is merely guessed at stay separate, because they are
|
|
12
16
|
// two different degrees of certainty: mixing them would make a word that is only a hunch look
|
|
13
17
|
// like a fact.
|
|
@@ -59,13 +63,17 @@ function index(files) {
|
|
|
59
63
|
return { byName, byStem, byDir, dirTail, exts, stems: [...byStem.keys()] }
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* The pieces of text worth trying, in the order they appear: each one lower case for the
|
|
68
|
+
* matching, and as it was written for whoever has to go and look on a disk — there are file
|
|
69
|
+
* systems where `Report.pdf` and `report.pdf` are two different files.
|
|
70
|
+
*/
|
|
63
71
|
function tokens(text) {
|
|
64
72
|
const out = []
|
|
65
73
|
for (const m of String(text ?? '').matchAll(TOKEN)) {
|
|
66
74
|
// Punctuation stuck on the end is not part of the name: "…it is in from-the-call.md."
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
75
|
+
const raw = m[0].replace(/[._/-]+$/, '')
|
|
76
|
+
if (raw) out.push([low(raw), raw])
|
|
69
77
|
}
|
|
70
78
|
return out
|
|
71
79
|
}
|
|
@@ -110,6 +118,12 @@ function dirFor(token, ix) {
|
|
|
110
118
|
* ("summary"), or the whole beginning of a name ("survey" for `survey-round-two.md`).
|
|
111
119
|
* - `missing` — names written with a dot that do not exist in here. Only those with an
|
|
112
120
|
* extension the repository actually uses, otherwise `example.com` would look like a lost file.
|
|
121
|
+
* - `unknown` — the same names, all of them, before that judgement is made. The listing is not
|
|
122
|
+
* everything there is: a generated directory gives it only its finished documents, an ignored
|
|
123
|
+
* one gives it nothing, and `out/report.html` is in neither while being written out in full
|
|
124
|
+
* right there in the text. Whoever calls this can go and ask the disk about them; existence is
|
|
125
|
+
* a better answer than a guess about extensions, and it is the one thing this module cannot
|
|
126
|
+
* reach by itself.
|
|
113
127
|
*
|
|
114
128
|
* Every entry is `{ token, file, more }` for a file — `file` is the most recently touched one
|
|
115
129
|
* when a name points at several, `more` are the rest — or `{ token, dir, files }` for a
|
|
@@ -123,6 +137,7 @@ export function mentions(text, files) {
|
|
|
123
137
|
const named = []
|
|
124
138
|
const maybe = []
|
|
125
139
|
const missing = []
|
|
140
|
+
const unknown = []
|
|
126
141
|
const missed = new Set()
|
|
127
142
|
|
|
128
143
|
/** Drops the ones already listed and puts the most recent first. */
|
|
@@ -133,13 +148,16 @@ export function mentions(text, files) {
|
|
|
133
148
|
}
|
|
134
149
|
|
|
135
150
|
// First pass: what is written out in full.
|
|
136
|
-
for (const t of toks) {
|
|
151
|
+
for (const [t, raw] of toks) {
|
|
137
152
|
if (EXT.test(t)) {
|
|
138
153
|
const hits = filesFor(t, ix)
|
|
139
154
|
if (!hits) {
|
|
140
|
-
if (
|
|
155
|
+
if (!missed.has(t)) {
|
|
141
156
|
missed.add(t)
|
|
142
|
-
|
|
157
|
+
unknown.push(raw)
|
|
158
|
+
// The listing knows this extension and still has no such file: that is a name worth
|
|
159
|
+
// saying out loud. Otherwise silence — `example.com` is not a lost document.
|
|
160
|
+
if (ix.exts.has(EXT.exec(t)[1])) missing.push(t)
|
|
143
161
|
}
|
|
144
162
|
continue
|
|
145
163
|
}
|
|
@@ -159,7 +177,7 @@ export function mentions(text, files) {
|
|
|
159
177
|
|
|
160
178
|
// File names first: the whole name without the dot, failing that the whole beginning of a
|
|
161
179
|
// name cut at a dash — "survey" for `survey-round-two.md`.
|
|
162
|
-
for (const t of toks) {
|
|
180
|
+
for (const [t] of toks) {
|
|
163
181
|
if (!guess(t)) continue
|
|
164
182
|
const hits =
|
|
165
183
|
ix.byStem.get(t) ||
|
|
@@ -172,14 +190,14 @@ export function mentions(text, files) {
|
|
|
172
190
|
// clue there is: "onboarding" in a text about onboarding is an ordinary word, and if it came
|
|
173
191
|
// first it would carry off `interviews/onboarding/summary.md`, leaving "summary" to show the
|
|
174
192
|
// summary of something else entirely.
|
|
175
|
-
for (const t of toks) {
|
|
193
|
+
for (const [t] of toks) {
|
|
176
194
|
if (!guess(t)) continue
|
|
177
195
|
const d = dirFor(t, ix)
|
|
178
196
|
const f = d ? fresh(ix.byDir.get(d)) : []
|
|
179
197
|
if (f.length) keep(maybe, { token: t, dir: d, files: f }, f)
|
|
180
198
|
}
|
|
181
199
|
|
|
182
|
-
return { named, maybe, missing }
|
|
200
|
+
return { named, maybe, missing, unknown }
|
|
183
201
|
}
|
|
184
202
|
|
|
185
203
|
/** How many files a group of entries opens: a directory counts for what is inside it. */
|
package/web/recency.js
CHANGED
|
@@ -14,19 +14,19 @@ export const DAY = 24 * 60 * 60 * 1000
|
|
|
14
14
|
*
|
|
15
15
|
* `IDLE` is deliberately not one of them, and it is the only debatable line in this file. It
|
|
16
16
|
* means "your turn", so it looks like it belongs — but it is also the resting state of every
|
|
17
|
-
* terminal you have left open, and on a real board nearly every live
|
|
17
|
+
* terminal you have left open, and on a real board nearly every live story is sitting in it. Let
|
|
18
18
|
* it hold a column and nothing ever folds: a session you finished with three weeks ago and never
|
|
19
19
|
* closed would keep its column at full width for ever. So `IDLE` gets the same treatment as
|
|
20
20
|
* everything else — recent, and it stays; a fortnight old, and it goes to `Old`, where it is one
|
|
21
|
-
* click away. Nothing is closed either way: the terminal is still open, and the
|
|
21
|
+
* click away. Nothing is closed either way: the terminal is still open, and the story is still
|
|
22
22
|
* exactly where you left it.
|
|
23
23
|
*/
|
|
24
24
|
const DEMANDING = new Set(['ASK', 'PLANNED', 'WORKING', 'PLANNING'])
|
|
25
25
|
|
|
26
|
-
/** The last time each repository was touched: the freshest of its
|
|
27
|
-
export function lastTouched(
|
|
26
|
+
/** The last time each repository was touched: the freshest of its stories. */
|
|
27
|
+
export function lastTouched(stories) {
|
|
28
28
|
const map = new Map()
|
|
29
|
-
for (const c of
|
|
29
|
+
for (const c of stories) {
|
|
30
30
|
const t = Number(c.updated_at) || 0
|
|
31
31
|
if (t > (map.get(c.project_path) ?? 0)) map.set(c.project_path, t)
|
|
32
32
|
}
|
|
@@ -34,9 +34,9 @@ export function lastTouched(cards) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/** The repositories with something going on in them right now. */
|
|
37
|
-
export function busy(
|
|
37
|
+
export function busy(stories) {
|
|
38
38
|
const set = new Set()
|
|
39
|
-
for (const c of
|
|
39
|
+
for (const c of stories) if (DEMANDING.has(c.status)) set.add(c.project_path)
|
|
40
40
|
return set
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -71,10 +71,10 @@ export function cutoff(touched) {
|
|
|
71
71
|
*
|
|
72
72
|
* The order of `paths` is kept: whoever asked has already sorted them.
|
|
73
73
|
*/
|
|
74
|
-
export function split({ paths,
|
|
75
|
-
const touched = lastTouched(
|
|
74
|
+
export function split({ paths, stories, folded = new Set(), held = new Set() }) {
|
|
75
|
+
const touched = lastTouched(stories)
|
|
76
76
|
const line = cutoff(touched)
|
|
77
|
-
const alive = busy(
|
|
77
|
+
const alive = busy(stories)
|
|
78
78
|
const open = []
|
|
79
79
|
const old = []
|
|
80
80
|
for (const p of paths) {
|
package/web/units.js
CHANGED
|
@@ -9,7 +9,7 @@ const GB = 1073741824
|
|
|
9
9
|
/**
|
|
10
10
|
* What a session weighs, rounded in steps: tens of MB below a gigabyte, tenths of a GB above.
|
|
11
11
|
*
|
|
12
|
-
* The rounding is not decoration. It is the rounded number — the one you read on the
|
|
12
|
+
* The rounding is not decoration. It is the rounded number — the one you read on the post-it —
|
|
13
13
|
* that goes into the signature of the refresh round: that way the board only redraws when the
|
|
14
14
|
* figure on screen really changes, instead of on every sample, throwing away the pointer and
|
|
15
15
|
* the buttons that appear under it.
|
package/web/usage.css
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/* Usage: what the sessions consumed, a week at a time. Colours and fonts live in base.css.
|
|
2
|
+
|
|
3
|
+
One wide column. It is a page of numbers and charts read on a wide screen, so the container
|
|
4
|
+
takes the width and everything inside it — paragraphs included — goes edge to edge. */
|
|
5
|
+
@import url('/base.css');
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
/* The categorical order is fixed and follows the entity, never its rank: a model keeps its
|
|
9
|
+
colour from one week to the next. Validated for colour blindness on --paper. */
|
|
10
|
+
--s1: #2a78d6;
|
|
11
|
+
--s2: #eb6834;
|
|
12
|
+
--s3: #1baf7a;
|
|
13
|
+
--s4: #eda100;
|
|
14
|
+
--s5: #e87ba4;
|
|
15
|
+
--s6: #008300;
|
|
16
|
+
--other: #a8a293;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
margin: 0;
|
|
21
|
+
background: var(--bg);
|
|
22
|
+
color: var(--ink);
|
|
23
|
+
font-family: var(--sans);
|
|
24
|
+
font-size: 1rem;
|
|
25
|
+
line-height: 1.5;
|
|
26
|
+
-webkit-font-smoothing: antialiased;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ── The top bar ─────────────────────────────────────────────────────── */
|
|
30
|
+
.topbar {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 14px;
|
|
34
|
+
padding: 10px 18px;
|
|
35
|
+
background: var(--paper);
|
|
36
|
+
border-bottom: 1px solid var(--line);
|
|
37
|
+
flex-wrap: wrap;
|
|
38
|
+
}
|
|
39
|
+
.topbar .home {
|
|
40
|
+
font: 700 1.2rem/1 var(--mono);
|
|
41
|
+
letter-spacing: 1px;
|
|
42
|
+
color: var(--ink);
|
|
43
|
+
text-decoration: none;
|
|
44
|
+
}
|
|
45
|
+
.topbar .home:hover { color: var(--muted); }
|
|
46
|
+
.topbar h1 { margin: 0; font: 600 1rem/1 var(--sans); color: var(--muted); flex: 1; }
|
|
47
|
+
|
|
48
|
+
.week { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: .9rem; }
|
|
49
|
+
.week button { padding: 3px 11px; font-size: 1rem; line-height: 1.2; }
|
|
50
|
+
.week button:disabled { opacity: .35; cursor: default; }
|
|
51
|
+
#span { min-width: 11rem; text-align: center; }
|
|
52
|
+
|
|
53
|
+
.topbar select {
|
|
54
|
+
font: inherit;
|
|
55
|
+
font-size: .82rem;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
color: var(--muted);
|
|
58
|
+
border: 1px solid var(--line);
|
|
59
|
+
background: var(--paper);
|
|
60
|
+
border-radius: 8px;
|
|
61
|
+
padding: 5px 9px;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ── The sheet ───────────────────────────────────────────────────────── */
|
|
66
|
+
.sheet {
|
|
67
|
+
max-width: 1280px;
|
|
68
|
+
margin: 0 auto;
|
|
69
|
+
padding: 20px 18px 60px;
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
gap: 18px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.notes:empty { display: none; }
|
|
76
|
+
.notes { display: flex; flex-direction: column; gap: 8px; }
|
|
77
|
+
.note {
|
|
78
|
+
margin: 0;
|
|
79
|
+
padding: 10px 14px;
|
|
80
|
+
border-radius: 10px;
|
|
81
|
+
background: var(--paper);
|
|
82
|
+
border: 1px solid var(--line);
|
|
83
|
+
font-size: .9rem;
|
|
84
|
+
color: var(--muted);
|
|
85
|
+
}
|
|
86
|
+
.note b { color: var(--ink); }
|
|
87
|
+
.note code { font: .82rem var(--mono); background: var(--bg); padding: 1px 5px; border-radius: 4px; }
|
|
88
|
+
|
|
89
|
+
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; }
|
|
90
|
+
.tile {
|
|
91
|
+
background: var(--paper);
|
|
92
|
+
border: 1px solid var(--line);
|
|
93
|
+
border-radius: 12px;
|
|
94
|
+
padding: 12px 16px;
|
|
95
|
+
}
|
|
96
|
+
.tile .v { font: 700 1.6rem/1.2 var(--sans); font-variant-numeric: tabular-nums; }
|
|
97
|
+
.tile .k { font-size: .8rem; color: var(--muted); }
|
|
98
|
+
.tile .s { font-size: .78rem; color: var(--muted); margin-top: 2px; }
|
|
99
|
+
|
|
100
|
+
.panel {
|
|
101
|
+
background: var(--paper);
|
|
102
|
+
border: 1px solid var(--line);
|
|
103
|
+
border-radius: 12px;
|
|
104
|
+
padding: 14px 18px;
|
|
105
|
+
min-width: 0;
|
|
106
|
+
}
|
|
107
|
+
.panel h2 { margin: 0 0 4px; font: 600 .95rem/1.3 var(--sans); }
|
|
108
|
+
.caption { margin: 0 0 10px; font-size: .82rem; color: var(--muted); }
|
|
109
|
+
|
|
110
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 18px; }
|
|
111
|
+
|
|
112
|
+
/* ── Charts ──────────────────────────────────────────────────────────── */
|
|
113
|
+
.legend { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: .8rem; color: var(--muted); margin-bottom: 6px; }
|
|
114
|
+
.legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
115
|
+
.legend i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
|
|
116
|
+
.chart svg { display: block; width: 100%; height: auto; overflow: visible; }
|
|
117
|
+
.chart .grid-line { stroke: var(--line); stroke-width: 1; }
|
|
118
|
+
.chart .axis { fill: var(--muted); font-size: 11px; font-family: var(--sans); }
|
|
119
|
+
.chart .label { fill: var(--ink); font-size: 11px; font-weight: 600; font-family: var(--sans); }
|
|
120
|
+
.chart .empty { fill: var(--muted); font-size: 13px; font-family: var(--sans); }
|
|
121
|
+
.chart .hit { fill: transparent; cursor: default; }
|
|
122
|
+
.chart .hit:hover + .bar-outline, .chart .col:hover .seg { opacity: .85; }
|
|
123
|
+
.chart .line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
|
|
124
|
+
.chart .dot { stroke: var(--paper); stroke-width: 2; }
|
|
125
|
+
|
|
126
|
+
.tip {
|
|
127
|
+
position: fixed;
|
|
128
|
+
z-index: 10;
|
|
129
|
+
pointer-events: none;
|
|
130
|
+
background: var(--ink);
|
|
131
|
+
color: var(--paper);
|
|
132
|
+
font-size: .78rem;
|
|
133
|
+
line-height: 1.4;
|
|
134
|
+
padding: 7px 10px;
|
|
135
|
+
border-radius: 8px;
|
|
136
|
+
max-width: 320px;
|
|
137
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, .18);
|
|
138
|
+
}
|
|
139
|
+
.tip i { width: 8px; height: 8px; border-radius: 2px; display: inline-block; margin-right: 6px; }
|
|
140
|
+
|
|
141
|
+
/* ── Tables ──────────────────────────────────────────────────────────── */
|
|
142
|
+
table { width: 100%; border-collapse: collapse; font-size: .84rem; font-variant-numeric: tabular-nums; }
|
|
143
|
+
th {
|
|
144
|
+
text-align: left;
|
|
145
|
+
font-weight: 600;
|
|
146
|
+
color: var(--muted);
|
|
147
|
+
font-size: .75rem;
|
|
148
|
+
padding: 6px 8px;
|
|
149
|
+
border-bottom: 1px solid var(--line);
|
|
150
|
+
white-space: nowrap;
|
|
151
|
+
}
|
|
152
|
+
td { padding: 6px 8px; border-bottom: 1px solid #efebe0; vertical-align: top; }
|
|
153
|
+
tr:last-child td { border-bottom: none; }
|
|
154
|
+
td.n, th.n { text-align: right; white-space: nowrap; }
|
|
155
|
+
td .sub { display: block; color: var(--muted); font-size: .75rem; }
|
|
156
|
+
td a { color: var(--ink); }
|
|
157
|
+
.swatch { width: 9px; height: 9px; border-radius: 2px; display: inline-block; margin-right: 7px; }
|
|
158
|
+
.muted { color: var(--muted); }
|
|
159
|
+
.warn { color: var(--warn); font-weight: 600; }
|
|
160
|
+
.scroll { overflow-x: auto; }
|
|
161
|
+
.sessions td:first-child { min-width: 16rem; }
|
|
162
|
+
.key { font: 600 .75rem var(--mono); color: var(--muted); margin-right: 6px; }
|
|
163
|
+
#more { margin-top: 10px; }
|
|
164
|
+
|
|
165
|
+
@media (max-width: 600px) {
|
|
166
|
+
.sheet { padding: 14px 16px 40px; }
|
|
167
|
+
.grid { grid-template-columns: 1fr; }
|
|
168
|
+
#span { min-width: 0; }
|
|
169
|
+
}
|
package/web/usage.html
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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>k0 — Usage</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
8
|
+
<link rel="stylesheet" href="/usage.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header class="topbar">
|
|
12
|
+
<a class="home" href="/" title="Back to the dashboard">k0</a>
|
|
13
|
+
<h1>Usage</h1>
|
|
14
|
+
|
|
15
|
+
<!-- A week at a time, because the limit that matters is a weekly one. -->
|
|
16
|
+
<div class="week" role="group" aria-label="Week">
|
|
17
|
+
<button type="button" id="prev" aria-label="Previous week">‹</button>
|
|
18
|
+
<span id="span"></span>
|
|
19
|
+
<button type="button" id="next" aria-label="Next week">›</button>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<select id="account" aria-label="Account"></select>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<main class="sheet">
|
|
26
|
+
<div class="notes" id="notes"></div>
|
|
27
|
+
|
|
28
|
+
<section class="tiles" id="tiles" aria-label="The week in numbers"></section>
|
|
29
|
+
|
|
30
|
+
<section class="panel">
|
|
31
|
+
<h2>What each day would have cost at API prices</h2>
|
|
32
|
+
<p class="caption">Stacked by model. With a subscription you do not pay this: it is the one scale on which a
|
|
33
|
+
Fable hour and a Sonnet hour can be compared.</p>
|
|
34
|
+
<div class="legend" id="cost-legend"></div>
|
|
35
|
+
<div class="chart" id="cost-chart"></div>
|
|
36
|
+
</section>
|
|
37
|
+
|
|
38
|
+
<section class="panel">
|
|
39
|
+
<h2>How full the weekly limit was</h2>
|
|
40
|
+
<p class="caption" id="limits-caption"></p>
|
|
41
|
+
<div class="legend" id="limits-legend"></div>
|
|
42
|
+
<div class="chart" id="limits-chart"></div>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<div class="grid">
|
|
46
|
+
<section class="panel">
|
|
47
|
+
<h2>By model and effort</h2>
|
|
48
|
+
<table id="t-models"></table>
|
|
49
|
+
</section>
|
|
50
|
+
<section class="panel">
|
|
51
|
+
<h2>Who did the work</h2>
|
|
52
|
+
<table id="t-agents"></table>
|
|
53
|
+
</section>
|
|
54
|
+
<section class="panel">
|
|
55
|
+
<h2>By repository</h2>
|
|
56
|
+
<table id="t-repos"></table>
|
|
57
|
+
</section>
|
|
58
|
+
<section class="panel">
|
|
59
|
+
<h2>By account</h2>
|
|
60
|
+
<table id="t-accounts"></table>
|
|
61
|
+
</section>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<section class="panel">
|
|
65
|
+
<h2>Sessions, the most expensive first</h2>
|
|
66
|
+
<p class="caption">“Worked” is the time Claude spent on your turns, not the time the window was open. The share of
|
|
67
|
+
the weekly limit is an estimate: the statusline says how full the week is, not who filled it, so each rise is
|
|
68
|
+
shared among the sessions that ran in the same hour.</p>
|
|
69
|
+
<div class="scroll"><table id="t-sessions" class="sessions"></table></div>
|
|
70
|
+
<button type="button" id="more" hidden></button>
|
|
71
|
+
</section>
|
|
72
|
+
</main>
|
|
73
|
+
|
|
74
|
+
<div class="tip" id="tip" role="tooltip" hidden></div>
|
|
75
|
+
|
|
76
|
+
<script type="module" src="/usage.js"></script>
|
|
77
|
+
</body>
|
|
78
|
+
</html>
|