k0-board 0.4.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 +361 -1
- package/README.md +931 -111
- package/bin/k0-board.js +79 -11
- package/menubar/K0MenuBar.swift +35 -5
- package/package.json +13 -1
- package/platform/contract.js +30 -5
- package/platform/darwin/terminal.js +230 -56
- package/platform/index.js +2 -1
- package/platform/linux/index.js +3 -0
- 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 +5 -2
- 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 +60 -21
- package/server/git.js +53 -14
- package/server/idle.js +15 -13
- package/server/index.js +1251 -186
- 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 +396 -16
- package/web/board.js +1182 -152
- package/web/changelog.js +3 -3
- package/web/files.js +73 -16
- package/web/index.html +109 -12
- 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/changelog.js
CHANGED
|
@@ -98,8 +98,8 @@ function pending(r) {
|
|
|
98
98
|
if (r.unpushedTotal) lines.push(`${plural(r.unpushedTotal, 'commit')} still on this machine`)
|
|
99
99
|
if (r.dirtyTotal) lines.push(`${plural(r.dirtyTotal, 'file')} changed and never committed`)
|
|
100
100
|
if (r.unreleased) lines.push(`${plural(r.unreleased.filter((l) => l.startsWith('-')).length, 'changelog line')} not yet released`)
|
|
101
|
-
const open = r.
|
|
102
|
-
if (open.length) lines.push(`${plural(open.length, '
|
|
101
|
+
const open = r.stories.filter((c) => !c.done)
|
|
102
|
+
if (open.length) lines.push(`${plural(open.length, 'story', 'stories')} still open`)
|
|
103
103
|
if (!lines.length) return null
|
|
104
104
|
const p = document.createElement('p')
|
|
105
105
|
p.className = 'pending'
|
|
@@ -116,7 +116,7 @@ function factsBlock(r) {
|
|
|
116
116
|
if (r.commits.length) bits.push(plural(r.commits.length, 'commit'))
|
|
117
117
|
if (local) bits.push(`${local} not out yet`)
|
|
118
118
|
if (r.dirty.length) bits.push(plural(r.dirty.length, 'file touched', 'files touched'))
|
|
119
|
-
if (r.
|
|
119
|
+
if (r.stories.length) bits.push(plural(r.stories.length, 'story', 'stories'))
|
|
120
120
|
s.textContent = bits.join(' · ') || 'nothing recorded'
|
|
121
121
|
d.append(s)
|
|
122
122
|
if (r.commits.length) d.append(commitList(r))
|
package/web/files.js
CHANGED
|
@@ -6,10 +6,11 @@ import { mentions, countIn } from '/mentions.js'
|
|
|
6
6
|
import { resolveRel, index as refIndexOf, resolve as resolveRef, candidates, worth, trim, TOKEN_SRC } from '/refs.js'
|
|
7
7
|
import { bytes } from '/units.js'
|
|
8
8
|
import { setFavicon } from '/favicon.js'
|
|
9
|
+
import { whileVisible } from '/awake.js'
|
|
9
10
|
|
|
10
11
|
// ── The viewer ────────────────────────────────────────────────────────
|
|
11
12
|
// k0's second tab: the files on the left, what they say on the right. It opens from the mark on
|
|
12
|
-
// a
|
|
13
|
+
// a post-it and does not take the board away, which stays where it is.
|
|
13
14
|
//
|
|
14
15
|
// There is no index: the listing arrives from the server ready-made and the search runs here, on
|
|
15
16
|
// the names. First the files the session touched, then the recently modified ones — which is also
|
|
@@ -24,7 +25,9 @@ import { setFavicon } from '/favicon.js'
|
|
|
24
25
|
const $ = (s) => document.querySelector(s)
|
|
25
26
|
const params = new URLSearchParams(location.search)
|
|
26
27
|
const REPO = params.get('repo') || ''
|
|
27
|
-
|
|
28
|
+
// `card` is the name this parameter had before stories were called stories, and somebody
|
|
29
|
+
// somewhere has this address bookmarked: both are read, only the new one is ever written.
|
|
30
|
+
const STORY = params.get('story') || params.get('card') || ''
|
|
28
31
|
/** Nobody is watching: the page is open only to be printed into a PDF. */
|
|
29
32
|
const HEADLESS = params.has('pdf')
|
|
30
33
|
|
|
@@ -36,7 +39,7 @@ const api = async (url, opts) => {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
const qs = (extra = {}) =>
|
|
39
|
-
new URLSearchParams({ repo: REPO, ...(
|
|
42
|
+
new URLSearchParams({ repo: REPO, ...(STORY ? { story: STORY } : {}), ...extra }).toString()
|
|
40
43
|
|
|
41
44
|
/** The address of a file's real bytes: images and PDFs come through here. */
|
|
42
45
|
const rawUrl = (p) => `/api/file/raw?${qs({ path: p })}`
|
|
@@ -209,12 +212,34 @@ function hiddenByTheSwitch(q) {
|
|
|
209
212
|
*/
|
|
210
213
|
let memo = { text: null, from: null, out: null }
|
|
211
214
|
function mentioned() {
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
const from = searchable()
|
|
216
|
+
if (memo.text !== scope.text || memo.from !== from) {
|
|
217
|
+
memo = { text: scope.text, from, out: mentions(scope.text, from) }
|
|
214
218
|
}
|
|
215
219
|
return memo.out
|
|
216
220
|
}
|
|
217
221
|
|
|
222
|
+
/**
|
|
223
|
+
* The files the pasted text is read against: the listing, plus the ones the disk confirmed when
|
|
224
|
+
* the text named something the listing does not carry — an HTML beside its PDF, anything under an
|
|
225
|
+
* ignored directory. They are handed over as ordinary files on purpose: order, the "7 more" fold,
|
|
226
|
+
* the count in the chip and the first one opening by itself all go on working without knowing
|
|
227
|
+
* where a row came from. `listed()` is untouched, so the listing on the left does not grow with
|
|
228
|
+
* what was merely named.
|
|
229
|
+
*/
|
|
230
|
+
let found = []
|
|
231
|
+
let searchMemo = { from: null, with: null, out: null }
|
|
232
|
+
function searchable() {
|
|
233
|
+
if (!found.length) return all
|
|
234
|
+
if (searchMemo.from !== all || searchMemo.with !== found) {
|
|
235
|
+
// Rebuilt only when one of the two changes: `paint()` runs every three seconds, and a fresh
|
|
236
|
+
// array each round would throw the memo above away every time.
|
|
237
|
+
const have = new Set(all.map((f) => f.p))
|
|
238
|
+
searchMemo = { from: all, with: found, out: all.concat(found.filter((f) => !have.has(f.p))) }
|
|
239
|
+
}
|
|
240
|
+
return searchMemo.out
|
|
241
|
+
}
|
|
242
|
+
|
|
218
243
|
/** How many files the text touches, open or closed: the number must not jump about. */
|
|
219
244
|
const howMany = (out) => countIn(out.named) + countIn(out.maybe)
|
|
220
245
|
|
|
@@ -499,10 +524,15 @@ function openPaste(text) {
|
|
|
499
524
|
* Takes the text and narrows the listing to what it names. If it names nothing the dialog does
|
|
500
525
|
* **not** close and the listing is not thrown away: it says what happened, so the text can be
|
|
501
526
|
* corrected where it is instead of being pasted again.
|
|
527
|
+
*
|
|
528
|
+
* Almost always it finishes without asking anybody anything. The one question goes out when the
|
|
529
|
+
* text writes a document's name in full and the listing has no such file — then the disk is asked,
|
|
530
|
+
* once, about those names and no others.
|
|
502
531
|
*/
|
|
503
|
-
function applyPaste(text) {
|
|
532
|
+
async function applyPaste(text) {
|
|
504
533
|
const t = String(text ?? '').trim()
|
|
505
|
-
|
|
534
|
+
found = await confirmed(mentions(t, all).unknown)
|
|
535
|
+
const out = mentions(t, searchable())
|
|
506
536
|
if (!howMany(out)) {
|
|
507
537
|
const note = $('#p-note')
|
|
508
538
|
note.hidden = false
|
|
@@ -512,7 +542,7 @@ function applyPaste(text) {
|
|
|
512
542
|
return
|
|
513
543
|
}
|
|
514
544
|
scope = { kind: 'text', text: t }
|
|
515
|
-
memo = { text: t, from:
|
|
545
|
+
memo = { text: t, from: searchable(), out }
|
|
516
546
|
opened = new Set()
|
|
517
547
|
active = -1
|
|
518
548
|
$('#q').value = ''
|
|
@@ -526,11 +556,26 @@ function applyPaste(text) {
|
|
|
526
556
|
}
|
|
527
557
|
|
|
528
558
|
/**
|
|
529
|
-
*
|
|
559
|
+
* The names the listing does not have, handed to the disk and given back as rows.
|
|
530
560
|
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
561
|
+
* Only the ones that look like a document are worth a question: `example.com` is a website and
|
|
562
|
+
* `v1.5` is a number. The same rules as the server's, written a second time on purpose — the
|
|
563
|
+
* server checks them again anyway, this only keeps the question short. Change one, change the
|
|
564
|
+
* other.
|
|
533
565
|
*/
|
|
566
|
+
const DOC_EXT = /\.(md|markdown|mdx|html?|txt|rtf|pdf|docx?|odt|pages)$/i
|
|
567
|
+
async function confirmed(unknown) {
|
|
568
|
+
const paths = (unknown || []).filter((t) => DOC_EXT.test(t)).slice(0, MAX_ASK)
|
|
569
|
+
if (!paths.length) return []
|
|
570
|
+
try {
|
|
571
|
+
const r = await api('/api/files/exist', { method: 'POST', body: JSON.stringify({ repo: REPO, paths }) })
|
|
572
|
+
return r.files || []
|
|
573
|
+
} catch {
|
|
574
|
+
// The server is not answering: what is left is what the listing could answer on its own.
|
|
575
|
+
return []
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
534
579
|
/**
|
|
535
580
|
* Into a folder. The file open on the right stays open: you go looking for the next one without
|
|
536
581
|
* losing the one you were reading.
|
|
@@ -549,9 +594,17 @@ function enterDir(d) {
|
|
|
549
594
|
history.replaceState(null, '', open ? `?${qs({ dir: d, f: open.path })}` : dirUrl(d))
|
|
550
595
|
}
|
|
551
596
|
|
|
597
|
+
/**
|
|
598
|
+
* Back to the whole listing. The file open on the right stays where it is.
|
|
599
|
+
*
|
|
600
|
+
* A directory is also in the address, because it is a page that can be reloaded and sent: on the
|
|
601
|
+
* way out it has to be taken off, otherwise a reload would put you back inside.
|
|
602
|
+
*/
|
|
552
603
|
function clearScope() {
|
|
553
604
|
const wasDir = scope?.kind === 'dir'
|
|
554
605
|
scope = null
|
|
606
|
+
// What the disk confirmed belonged to that text and to no other: it goes out with it.
|
|
607
|
+
found = []
|
|
555
608
|
opened = new Set()
|
|
556
609
|
active = -1
|
|
557
610
|
paint()
|
|
@@ -579,8 +632,8 @@ const dirUrl = (d) => `?${qs({ dir: d })}`
|
|
|
579
632
|
/**
|
|
580
633
|
* What this document names that can really be opened: from a written name to an address. Almost
|
|
581
634
|
* always it finishes without asking anybody anything, because the file listing is already here;
|
|
582
|
-
* the server is only reached for the names the listing does not have
|
|
583
|
-
*
|
|
635
|
+
* the server is only reached for the names the listing does not have — the page beside a finished
|
|
636
|
+
* PDF, anything under a directory git was told to ignore.
|
|
584
637
|
*/
|
|
585
638
|
async function refsIn(docPath, text) {
|
|
586
639
|
const ix = refIndex()
|
|
@@ -1136,7 +1189,7 @@ function keys() {
|
|
|
1136
1189
|
|
|
1137
1190
|
async function boot() {
|
|
1138
1191
|
if (!REPO) {
|
|
1139
|
-
document.body.innerHTML = '<p class="blank">No repository. Open this from a
|
|
1192
|
+
document.body.innerHTML = '<p class="blank">No repository. Open this from a story on the dashboard.</p>'
|
|
1140
1193
|
return
|
|
1141
1194
|
}
|
|
1142
1195
|
split(Number(localStorage.getItem('k0-files-split')) || 28)
|
|
@@ -1270,9 +1323,13 @@ async function boot() {
|
|
|
1270
1323
|
// When it is the server printing us, the page has nothing to do but show itself: interrogating
|
|
1271
1324
|
// git every three seconds while a sheet is being waited for is of no use to anybody.
|
|
1272
1325
|
if (HEADLESS) return
|
|
1273
|
-
|
|
1326
|
+
// And only while the tab is in front of you: both of these run git, and a viewer left open in a
|
|
1327
|
+
// window you are not in has nothing to show anybody. See `whileVisible`.
|
|
1328
|
+
// Ten seconds, not three. Every round of this runs two git processes against the repository you
|
|
1329
|
+
// are reading, and you are reading it, not watching a counter for the moment a file changes.
|
|
1330
|
+
whileVisible(poll, 10000)
|
|
1274
1331
|
// The full listing ages slowly, but the dates do not: half a minute is enough.
|
|
1275
|
-
|
|
1332
|
+
whileVisible(() => loadAll().catch(() => {}), 30000)
|
|
1276
1333
|
}
|
|
1277
1334
|
|
|
1278
1335
|
boot()
|
package/web/index.html
CHANGED
|
@@ -10,18 +10,61 @@
|
|
|
10
10
|
<body>
|
|
11
11
|
<header class="topbar">
|
|
12
12
|
<h1>k0</h1>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
<!-- The one mark on this bar that appears by itself, and only for one of two reasons: k0
|
|
14
|
+
moved on under you while you were not looking, or npm says there is a newer version than
|
|
15
|
+
the one installed here. It never opens the page — it sits next to the name and waits. -->
|
|
16
|
+
<a class="whatsnew" id="whatsnew" href="/whatsnew.html" hidden>
|
|
17
|
+
<span id="whatsnew-dot" aria-hidden="true"></span>
|
|
18
|
+
</a>
|
|
19
|
+
<!-- The "+" is a menu now, because there are two things it can make and they are made in
|
|
20
|
+
two different ways: a story is typed here, an epic is told to Claude in a terminal. With
|
|
21
|
+
the backlog off there is only one of them and the menu never opens — the plus is the
|
|
22
|
+
plus it has always been. -->
|
|
23
|
+
<div class="new-wrap">
|
|
24
|
+
<button class="new" id="new-story" title="New story" aria-label="New story"
|
|
25
|
+
aria-haspopup="true" aria-expanded="false">
|
|
26
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
|
27
|
+
</button>
|
|
28
|
+
<div class="menu" id="new-menu" role="menu" hidden>
|
|
29
|
+
<button type="button" role="menuitem" data-new="story">Story<em>a post-it, written here</em></button>
|
|
30
|
+
<button type="button" role="menuitem" data-new="epic">Epic<em>talked through in a terminal</em></button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
16
33
|
<!-- Next to the plus, because they are the two ends of the same thing: one starts a piece
|
|
17
34
|
of work, the other tells you how the ones before it went. A plain link, and a quiet
|
|
18
35
|
one — no dot, no badge. It is there when you want it. -->
|
|
19
36
|
<a class="log" href="/changelog.html" title="What you have been doing" aria-label="What you have been doing">
|
|
20
37
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h11M4 17h7"/></svg>
|
|
21
38
|
</a>
|
|
22
|
-
|
|
39
|
+
<!-- Its neighbour, and just as quiet: what those sessions consumed, a week at a time. -->
|
|
40
|
+
<a class="log" href="/usage.html" title="What your sessions consumed" aria-label="What your sessions consumed">
|
|
41
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 20V11M12 20V5M19 20v-6"/></svg>
|
|
42
|
+
</a>
|
|
43
|
+
<!-- The state pills, the repository and the shape of the view: one group, because they are
|
|
44
|
+
one question. The pills and the repository say WHAT you are looking at; Kanban or List
|
|
45
|
+
says what shape it comes in. Switching shape never changes what is in scope. -->
|
|
46
|
+
<div class="filterbar">
|
|
47
|
+
<div class="filters" id="filters"></div>
|
|
48
|
+
<!-- Two answers and no third: the board as it has always been, or the same stories as rows.
|
|
49
|
+
It is only here when there is a backlog behind the board — with the feature off there is
|
|
50
|
+
one answer, and a switch offering it would be a switch that does nothing. -->
|
|
51
|
+
<div class="columns" id="columns" role="radiogroup" aria-label="How to look at it" hidden>
|
|
52
|
+
<button type="button" role="radio" data-columns="kanban" aria-checked="true">Kanban</button>
|
|
53
|
+
<button type="button" role="radio" data-columns="list" aria-checked="false">List</button>
|
|
54
|
+
</div>
|
|
55
|
+
<!-- Which repository, for both views. Empty means all of them. -->
|
|
56
|
+
<div class="picker repo-filter" id="repo-filter" hidden>
|
|
57
|
+
<button type="button" id="repo-filter-btn" aria-haspopup="listbox" aria-expanded="false"
|
|
58
|
+
aria-label="Which repository">All repositories</button>
|
|
59
|
+
<div class="picker-list" id="repo-filter-list" hidden>
|
|
60
|
+
<input id="repo-filter-q" autocomplete="off" spellcheck="false" placeholder="search a repository…"
|
|
61
|
+
aria-label="Search a repository">
|
|
62
|
+
<div id="repo-filter-rows" role="listbox"></div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
23
66
|
|
|
24
|
-
<!-- How the computer is doing. Always visible; clicking it switches off the weights on the
|
|
67
|
+
<!-- How the computer is doing. Always visible; clicking it switches off the weights on the stories. -->
|
|
25
68
|
<button class="gauge" id="gauge" type="button" hidden></button>
|
|
26
69
|
|
|
27
70
|
<!-- The four modes: how awake the machine has to stay. It is a scale, not four switches —
|
|
@@ -44,9 +87,22 @@
|
|
|
44
87
|
</div>
|
|
45
88
|
</header>
|
|
46
89
|
|
|
90
|
+
<!-- Inside an epic. The board is that epic and nothing else, and this strip is the only thing
|
|
91
|
+
that says so: which one, how far it has got, and the way back out. It is not on the page
|
|
92
|
+
until you have gone in — an epic you are not in has nothing to say up here. -->
|
|
93
|
+
<div class="lane" id="epic-lane" hidden>
|
|
94
|
+
<span class="key" id="epic-key"></span>
|
|
95
|
+
<span class="name" id="epic-name"></span>
|
|
96
|
+
<span class="repo" id="epic-repo"></span>
|
|
97
|
+
<span class="bar"><i id="epic-fill"></i></span>
|
|
98
|
+
<span class="count" id="epic-count"></span>
|
|
99
|
+
<span class="spacer"></span>
|
|
100
|
+
<button type="button" id="epic-out">Leave the epic</button>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
47
103
|
<main id="viewport" class="viewport">
|
|
48
104
|
<div id="board" class="board"></div>
|
|
49
|
-
<p class="empty" id="empty">The dashboard is empty. Add your first
|
|
105
|
+
<p class="empty" id="empty">The dashboard is empty. Add your first story.</p>
|
|
50
106
|
|
|
51
107
|
<!-- Where the board continues past the edge: with no scrollbars, it is the only way to tell. -->
|
|
52
108
|
<div class="edges" id="edges" aria-hidden="true"><i class="l"></i><i class="r"></i><i class="t"></i><i class="b"></i></div>
|
|
@@ -68,7 +124,7 @@
|
|
|
68
124
|
<dialog id="editor">
|
|
69
125
|
<form id="editor-form">
|
|
70
126
|
<div class="head">
|
|
71
|
-
<h2 id="editor-title">New
|
|
127
|
+
<h2 id="editor-title">New story</h2>
|
|
72
128
|
<button type="button" id="f-close" class="x" title="Close" aria-label="Close">
|
|
73
129
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
|
74
130
|
</button>
|
|
@@ -81,20 +137,61 @@
|
|
|
81
137
|
</div>
|
|
82
138
|
</label>
|
|
83
139
|
|
|
84
|
-
<label>Title
|
|
140
|
+
<label>Title <small class="aside">(this is also the session name)</small>
|
|
85
141
|
<input id="f-title" required autocomplete="off" spellcheck="false" placeholder="Fix-Login-Bug">
|
|
86
|
-
<small>this is also the session name</small>
|
|
87
142
|
</label>
|
|
88
143
|
|
|
89
|
-
|
|
90
|
-
|
|
144
|
+
<!-- Notes, and only notes: they stay on the note and on the post-it, and nothing ever sends
|
|
145
|
+
them to a terminal. A new story does not have them at all — at that moment there is
|
|
146
|
+
nothing to note down yet, and a box asking to be filled in is a box that delays the
|
|
147
|
+
only two things worth doing with a new story, which are the two buttons at the bottom. -->
|
|
148
|
+
<label id="f-notes-row" hidden>Notes
|
|
149
|
+
<textarea id="f-notes" rows="6" placeholder="- what you want to remember about it - one per line"></textarea>
|
|
150
|
+
<small>they sit on the post-it for you to read — they are never sent to Claude Code</small>
|
|
91
151
|
</label>
|
|
92
152
|
|
|
153
|
+
<!-- The epic this story belongs to. A list of the ones this repository already has, or a
|
|
154
|
+
name typed in that is made on the spot: an epic somebody wanted while writing a story
|
|
155
|
+
should not cost a trip to another screen. Only here when there is a backlog. -->
|
|
156
|
+
<label id="f-epic-row" hidden>Epic
|
|
157
|
+
<input id="f-epic" list="f-epics" autocomplete="off" placeholder="none — or type a new one">
|
|
158
|
+
<datalist id="f-epics"></datalist>
|
|
159
|
+
</label>
|
|
160
|
+
|
|
161
|
+
|
|
93
162
|
<footer>
|
|
94
163
|
<button type="button" id="f-delete" class="danger">Delete</button>
|
|
95
164
|
<span class="spacer"></span>
|
|
96
165
|
<button type="submit" id="f-save">Save</button>
|
|
97
|
-
<button type="button" id="f-
|
|
166
|
+
<button type="button" id="f-discuss">Discuss</button>
|
|
167
|
+
<button type="button" id="f-start" class="primary">Quick Start</button>
|
|
168
|
+
</footer>
|
|
169
|
+
</form>
|
|
170
|
+
</dialog>
|
|
171
|
+
|
|
172
|
+
<!-- A new epic. It asks for the repository and nothing else, then opens a terminal: an epic is
|
|
173
|
+
born by being told, not by being typed into a form. -->
|
|
174
|
+
<dialog id="epic-new">
|
|
175
|
+
<form id="epic-form">
|
|
176
|
+
<div class="head">
|
|
177
|
+
<h2>New epic</h2>
|
|
178
|
+
<button type="button" id="e-close" class="x" title="Close" aria-label="Close">
|
|
179
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<label>Repository
|
|
184
|
+
<select id="e-project" required></select>
|
|
185
|
+
</label>
|
|
186
|
+
|
|
187
|
+
<p class="note">k0 opens a terminal and <code>/k0-epic</code> asks what this is, a round of
|
|
188
|
+
questions at a time, until nothing is left that would change the work. It writes the epic
|
|
189
|
+
down itself, and the stories under it, once it knows.</p>
|
|
190
|
+
|
|
191
|
+
<footer>
|
|
192
|
+
<span class="spacer"></span>
|
|
193
|
+
<button type="button" id="e-cancel">Cancel</button>
|
|
194
|
+
<button type="submit" class="primary">Start the discussion</button>
|
|
98
195
|
</footer>
|
|
99
196
|
</form>
|
|
100
197
|
</dialog>
|