kaizen-agent 1.23.0 → 1.25.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/README.md +19 -242
- package/agents/kaizen-reviewer.md +3 -1
- package/cli/build-web.ts +16 -9
- package/cli/install.ts +27 -6
- package/cli/notes.test.ts +18 -4
- package/cli/pty.test.ts +133 -0
- package/cli/pty.ts +111 -0
- package/cli/settings.ts +2 -1
- package/cli/sources.test.ts +170 -25
- package/cli/sources.ts +152 -28
- package/cli/state.ts +33 -6
- package/cli/web.ts +124 -18
- package/commands/kaizen-discover.md +5 -0
- package/commands/kaizen-gather.md +1 -1
- package/package.json +4 -2
- package/skills/kaizen/README.md +1 -0
- package/skills/kaizen/SKILL.md +6 -2
- package/skills/kaizen/config.default.yml +8 -0
- package/skills/kaizen/discover.md +50 -0
- package/skills/kaizen/gather.md +22 -7
- package/skills/kaizen/settings.md +1 -1
- package/skills/kaizen/spec-build.md +4 -1
- package/skills/kaizen/spec-review.md +3 -1
- package/skills/kaizen/spec.md +2 -1
- package/skills/kaizen-help/SKILL.md +2 -1
- package/web/board.html +415 -100
- package/web/notebook.js +1 -1
- package/web/notes.html +77 -15
- package/web/term.css +285 -0
- package/web/term.js +39 -0
- package/web/term.ts +46 -0
package/web/board.html
CHANGED
|
@@ -10,20 +10,48 @@
|
|
|
10
10
|
:root[data-theme=dark]{--bg:#0c162f;--bg-2:#121e3c;--line:#22305a;--ink:#eef2fa;--ink-hi:#deeeff;--dim:#96aacd;--accent:#486ed9;--run:#4fd6e6;--ok:#5fd28a;--warn:#e6b450;--danger:#e85050;color-scheme:dark}
|
|
11
11
|
*{box-sizing:border-box}
|
|
12
12
|
/* Shape, type and depth. --shade is the one shadow colour, themed like the rest. */
|
|
13
|
-
:root{--r:
|
|
14
|
-
@media(prefers-color-scheme:dark){:root:not([data-theme=light]){--shade:rgb(0 0 0/.45)}}
|
|
15
|
-
:root[data-theme=dark]{--shade:rgb(0 0 0/.45)}
|
|
16
|
-
body{margin:0;background:var(--
|
|
13
|
+
:root{--r:12px;--r-s:8px;--r-pill:999px;--font:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;--mono:ui-monospace,"JetBrains Mono","Cascadia Code","SF Mono",Menlo,Consolas,monospace;--on-accent:#fff;--hover:color-mix(in srgb,var(--dim) 12%,transparent);--shade:rgb(12 22 47/.08);--shadow-1:0 1px 2px var(--shade);--shadow-2:0 0 0 1px var(--shade),0 8px 24px -6px var(--shade);--canvas:color-mix(in srgb,var(--bg) 95.5%,var(--ink))}
|
|
14
|
+
@media(prefers-color-scheme:dark){:root:not([data-theme=light]){--shade:rgb(0 0 0/.45);--canvas:color-mix(in srgb,var(--bg) 70%,black)}}
|
|
15
|
+
:root[data-theme=dark]{--shade:rgb(0 0 0/.45);--canvas:color-mix(in srgb,var(--bg) 70%,black)}
|
|
16
|
+
body{margin:0;background:var(--canvas);color:var(--ink);font:13px/1.5 var(--font);-webkit-font-smoothing:antialiased;height:100vh;height:100dvh;overflow:hidden;display:flex}
|
|
17
17
|
button,input,select{font:inherit;color:inherit;background:none;border:0;padding:0}
|
|
18
18
|
button{cursor:pointer}
|
|
19
19
|
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
20
20
|
svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;display:block}
|
|
21
21
|
|
|
22
|
+
/* side menu: the same lines in board.html and notes.html (a test holds them equal) */
|
|
23
|
+
.rail{width:220px;flex:none;display:flex;flex-direction:column;gap:2px;padding:18px 12px 14px;overflow:hidden auto;transition:width .18s}
|
|
24
|
+
.rail .top{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 0 18px 10px}
|
|
25
|
+
.rail .home{display:block;color:inherit;text-decoration:none}
|
|
26
|
+
.rail .top .mascot{display:none;width:36px;height:34px}
|
|
27
|
+
.rail a.it{display:flex;align-items:center;gap:10px;height:34px;padding:0 10px;border-radius:var(--r-s);font-size:13px;font-weight:500;color:var(--dim);text-decoration:none;white-space:nowrap;transition:background .12s,color .12s}
|
|
28
|
+
.rail a.it:hover{background:var(--hover);color:var(--ink)}
|
|
29
|
+
.rail a.it[aria-current=page]{background:color-mix(in srgb,var(--accent) 14%,transparent);color:var(--ink)}
|
|
30
|
+
.rail a.it[aria-current=page] svg{color:var(--accent)}
|
|
31
|
+
.rail .end{flex:1}
|
|
32
|
+
.theme{display:flex;gap:2px;padding:3px;border-radius:var(--r-s);background:var(--hover)}
|
|
33
|
+
.theme button{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;height:28px;border-radius:4px;font-size:12px;font-weight:500;color:var(--dim);transition:background .12s,color .12s}
|
|
34
|
+
.theme button svg{width:15px;height:15px}
|
|
35
|
+
.theme button:hover{color:var(--ink)}
|
|
36
|
+
.theme button[aria-pressed=true]{background:var(--bg-2);color:var(--ink);box-shadow:var(--shadow-1)}
|
|
37
|
+
[data-rail=min] .rail{width:60px;padding-inline:10px;align-items:center}
|
|
38
|
+
[data-rail=min] .rail .top{flex-direction:column;gap:12px;padding:0 0 14px}
|
|
39
|
+
[data-rail=min] .rail .top pre.mark{display:none}
|
|
40
|
+
[data-rail=min] .rail .top .mascot{display:block}
|
|
41
|
+
[data-rail=min] .rail a.it{width:40px;justify-content:center;padding:0}
|
|
42
|
+
[data-rail=min] .rail .lbl{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
|
|
43
|
+
[data-rail=min] .theme{flex-direction:column}
|
|
44
|
+
[data-rail=min] .theme button{width:34px;flex:none}
|
|
45
|
+
@media(max-width:640px){#fold{display:none}}
|
|
46
|
+
/* end side menu */
|
|
47
|
+
/* the work area: an inset rounded panel on the canvas the side menu sits on */
|
|
48
|
+
.app{flex:1;min-width:0;display:flex;flex-direction:column;margin:8px 8px 8px 0;background:var(--bg);border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
|
|
49
|
+
@media(max-width:640px){.app{margin:0;border:0;border-radius:0}}
|
|
22
50
|
/* header */
|
|
23
|
-
.hdr{display:flex;align-items:center;gap:
|
|
51
|
+
.hdr{display:flex;align-items:center;gap:12px;padding:10px 20px 10px 28px;border-bottom:1px solid color-mix(in srgb,var(--line) 60%,transparent);flex-wrap:wrap}
|
|
24
52
|
pre.mark{margin:0;font:700 8px/1 var(--mono)}
|
|
25
53
|
pre.mark span:nth-child(1){color:var(--ink-hi)}pre.mark span:nth-child(2){color:color-mix(in srgb,var(--ink-hi),var(--accent))}pre.mark span:nth-child(3){color:var(--accent)}
|
|
26
|
-
.sel{border:1px solid var(--line);border-radius:var(--r-s);padding:
|
|
54
|
+
.sel{border:1px solid var(--line);border-radius:var(--r-s);padding:4px 10px;background-color:var(--bg-2);color:var(--ink);font-size:13px}
|
|
27
55
|
select.sel,button.sel{cursor:pointer;appearance:none;padding-right:30px;max-width:100%;background-image:linear-gradient(45deg,transparent 50%,var(--dim) 50%),linear-gradient(135deg,var(--dim) 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px;background-repeat:no-repeat}
|
|
28
56
|
.sel:hover{border-color:var(--dim)}
|
|
29
57
|
.agents{display:flex;flex-wrap:wrap;gap:6px}
|
|
@@ -31,6 +59,8 @@ select.sel,button.sel{cursor:pointer;appearance:none;padding-right:30px;max-widt
|
|
|
31
59
|
.agent svg{width:14px;height:14px}.agent:hover{border-color:var(--dim)}
|
|
32
60
|
.yolo{display:inline-flex;align-items:center;gap:8px;margin-right:auto;font-size:13px;color:var(--ink);cursor:pointer;white-space:nowrap}
|
|
33
61
|
.agent[aria-checked=true]{border-color:var(--accent);color:var(--ink);background:color-mix(in srgb,var(--accent) 14%,var(--bg-2))}
|
|
62
|
+
.hdr h1{margin:0;font-size:14px;font-weight:600}
|
|
63
|
+
.hdr>.combo>.sel:not(:hover):not(:focus){border-color:transparent;background-color:transparent}
|
|
34
64
|
.hdr .sel{max-width:min(320px,100%);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}
|
|
35
65
|
/* project picker: a select-shaped button over a searchable list */
|
|
36
66
|
.combo{position:relative;max-width:100%;display:flex}
|
|
@@ -54,39 +84,39 @@ select.sel,button.sel{cursor:pointer;appearance:none;padding-right:30px;max-widt
|
|
|
54
84
|
/* the mic button turns into a level meter while listening: five bars, each weighted so the set reads as a wave */
|
|
55
85
|
.vu{display:inline-flex;gap:2px;align-items:center;height:100%}.vu i{width:2px;border-radius:1px;background:currentColor;height:calc(3px + 12px*min(1,var(--lvl,0)*var(--w)));transition:height .08s}
|
|
56
86
|
.ib.mic.on{color:var(--accent)}.ib.mic.busy{color:var(--accent)}.ib.mic i{font-style:normal;font-size:10px;font-variant-numeric:tabular-nums}.row .mic{margin-right:auto;align-self:center}
|
|
57
|
-
.btn{border:1px solid var(--line);border-radius:var(--r-s);padding:
|
|
87
|
+
.btn{border:1px solid var(--line);border-radius:var(--r-s);padding:4px 12px;color:var(--ink);background:var(--bg-2);min-height:30px;font-size:13px;font-weight:500;box-shadow:var(--shadow-1);transition:background .12s,border-color .12s,color .12s}
|
|
58
88
|
.btn:hover{border-color:var(--dim)}
|
|
59
|
-
.btn.pri{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}.btn.pri:hover{
|
|
89
|
+
.btn.pri{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}.btn.pri:hover{background:color-mix(in srgb,var(--accent) 88%,var(--ink))}
|
|
60
90
|
.btn.warn{border-color:var(--warn);color:var(--warn)}.btn.warn:hover{background:var(--warn);color:var(--bg)}
|
|
61
91
|
.btn.danger{color:var(--danger)}.btn.danger:hover{border-color:var(--danger)}
|
|
62
92
|
.btn.ghost{background:none;border-color:transparent;color:var(--dim);box-shadow:none}.btn.ghost:hover{color:var(--ink);background:var(--hover)}
|
|
63
93
|
|
|
64
94
|
/* summary strip: each count is a pill that filters the board */
|
|
65
95
|
.strip{display:inline-flex;gap:8px;flex-wrap:wrap;align-items:center;font-size:13px;color:var(--dim)}
|
|
66
|
-
.strip button{display:inline-flex;align-items:center;gap:6px;color:var(--dim);padding:
|
|
96
|
+
.strip button{display:inline-flex;align-items:center;gap:6px;color:var(--dim);padding:2px 10px;border-radius:var(--r-pill);box-shadow:inset 0 0 0 1px var(--line);transition:background .12s,color .12s}
|
|
67
97
|
.strip button:hover,.strip button.on{color:var(--ink);background:var(--hover)}
|
|
68
98
|
.strip button.attn{color:var(--warn);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--warn) 45%,transparent)}
|
|
69
99
|
.strip svg{width:14px;height:14px;stroke-width:1.8}
|
|
70
100
|
|
|
71
101
|
/* board */
|
|
72
|
-
main{flex:1;min-height:0;display:grid;grid-template-columns:repeat(5,minmax(200px,1fr));gap:
|
|
102
|
+
main{flex:1;min-height:0;display:grid;grid-template-columns:repeat(5,minmax(200px,1fr));gap:12px;padding:12px 28px 20px;overflow:auto;align-content:start}
|
|
73
103
|
section h2::before{content:"";display:block;height:16px;margin-top:-16px}
|
|
74
|
-
section{padding-top:
|
|
75
|
-
section h2{margin:0;font-size:13px;color:var(--ink);font-weight:
|
|
76
|
-
section h2 i{font-style:normal;font-weight:
|
|
104
|
+
section{padding-top:8px}
|
|
105
|
+
section h2{margin:0;font-size:13px;color:var(--ink);font-weight:500;display:flex;align-items:center;gap:8px;height:32px;padding-inline:2px;position:sticky;top:0;background:var(--bg);z-index:1}
|
|
106
|
+
section h2 i{font-style:normal;font-weight:400;font-size:12px;color:var(--dim)}
|
|
77
107
|
section h2 i:empty{display:none}
|
|
78
108
|
section h2 .ib{margin-left:auto}
|
|
79
|
-
ul{list-style:none;margin:0;padding:
|
|
109
|
+
ul{list-style:none;margin:0;padding:4px 0 0;display:flex;flex-direction:column;gap:6px}
|
|
80
110
|
|
|
81
111
|
/* card, Linear grammar in our palette
|
|
82
112
|
line 1 status icon · id (dim) · ⋯ menu on hover
|
|
83
113
|
line 2 title, medium weight, wraps to 2 lines
|
|
84
114
|
line 3 chips, only when set
|
|
85
115
|
Card ground is --bg-2 on a --bg column, lifted by --shadow-1. Actions never sit on the card. */
|
|
86
|
-
.card{background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r);padding:12px 14px;cursor:pointer;display:flex;flex-direction:column;gap:6px;position:relative;box-shadow:var(--shadow-1);transition:
|
|
87
|
-
.card:hover{
|
|
116
|
+
.card{background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r-s);padding:12px 14px;cursor:pointer;display:flex;flex-direction:column;gap:6px;position:relative;box-shadow:var(--shadow-1);transition:border-color .12s,box-shadow .12s}
|
|
117
|
+
.card:hover{border-color:color-mix(in srgb,var(--dim) 45%,var(--line));box-shadow:var(--shadow-2)}
|
|
88
118
|
.card.menu{z-index:2}
|
|
89
|
-
.card.open{border-color:var(--accent);box-shadow:0 0 0
|
|
119
|
+
.card.open{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}
|
|
90
120
|
.card.archived .c-title,.card.archived .c-id{opacity:.7}
|
|
91
121
|
section h2 .ord{min-height:22px;padding:0 8px;font-weight:500}
|
|
92
122
|
section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
@@ -98,7 +128,7 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
98
128
|
.card .pick{position:absolute;right:14px;top:14px;margin:0;accent-color:var(--accent)}
|
|
99
129
|
.card.picked{border-color:var(--accent)}
|
|
100
130
|
.card:has(.pick) .age,.card:has(.pick) .more{display:none}
|
|
101
|
-
.c-id{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dim);min-height:
|
|
131
|
+
.c-id{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dim);min-height:18px}
|
|
102
132
|
.c-id .st{width:14px;height:14px;display:grid;place-items:center;flex-shrink:0}
|
|
103
133
|
.c-id .st svg{width:14px;height:14px;stroke-width:1.8}
|
|
104
134
|
.c-id .idt{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-variant-numeric:tabular-nums}
|
|
@@ -106,16 +136,16 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
106
136
|
.c-id .more{display:none;margin:-4px -6px -4px 0}
|
|
107
137
|
.card:hover .more,.card:focus-within .more,.card.menu .more{display:grid}
|
|
108
138
|
.card:hover .age,.card.menu .age{display:none}
|
|
109
|
-
.c-title{font-size:
|
|
110
|
-
.c-chips{display:flex;gap:
|
|
139
|
+
.c-title{font-size:13px;font-weight:500;line-height:1.45;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;color:var(--ink)}
|
|
140
|
+
.c-chips{display:flex;gap:4px;flex-wrap:wrap;margin-top:2px}
|
|
111
141
|
.c-chips:empty{display:none}
|
|
112
|
-
.chip{display:inline-flex;align-items:center;gap:5px;min-height:
|
|
142
|
+
.chip{display:inline-flex;align-items:center;gap:5px;min-height:20px;padding:1px 8px;border-radius:11px;overflow-wrap:anywhere;font-size:11px;font-weight:500;color:var(--dim);box-shadow:inset 0 0 0 1px var(--line)}
|
|
113
143
|
.chip i{width:6px;height:6px;border-radius:50%;background:var(--dim)}
|
|
114
144
|
.chip svg{width:11px;height:11px;stroke-width:1.7}
|
|
115
|
-
.chip.warn{color:var(--warn
|
|
116
|
-
.chip.accent{
|
|
117
|
-
.chip.ok{color:
|
|
118
|
-
.chip.run{color:var(--
|
|
145
|
+
.chip.warn{color:var(--ink)}.chip.warn i{background:var(--warn)}.chip.warn svg{color:var(--warn)}
|
|
146
|
+
.chip.accent i{background:var(--accent)}.chip.accent svg{color:var(--accent)}
|
|
147
|
+
.chip.ok svg{color:var(--ok)}
|
|
148
|
+
.chip.run{color:var(--ink)}.chip.run i{background:var(--run)}
|
|
119
149
|
.st.d-idea{color:var(--dim)}.st.d-start{color:var(--run)}.st.d-run{color:var(--run)}.st.d-stall{color:var(--dim)}.st.d-wait{color:var(--warn)}.st.d-done{color:var(--ok)}.st.d-aband{color:var(--dim)}
|
|
120
150
|
.d-run{color:var(--run)}.d-wait{color:var(--warn)}.d-done{color:var(--ok)}
|
|
121
151
|
.pulse{display:inline-block;width:7px;height:7px;border-radius:50%;background:currentColor;vertical-align:middle}
|
|
@@ -128,11 +158,11 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
128
158
|
.menu-pop button.danger{color:var(--danger)}
|
|
129
159
|
.menu-pop svg{width:15px;height:15px;flex-shrink:0}
|
|
130
160
|
.menu-pop hr{border:0;border-top:1px solid var(--line);margin:4px 0}
|
|
131
|
-
.more-btn{width:100%;padding:
|
|
132
|
-
.more-btn:hover{color:var(--ink)
|
|
161
|
+
.more-btn{width:100%;padding:6px;color:var(--dim);font-size:12px;border-radius:var(--r-pill)}.more-btn:hover{background:var(--hover)}
|
|
162
|
+
.more-btn:hover{color:var(--ink)}
|
|
133
163
|
|
|
134
164
|
/* inline forms replace a card while editing */
|
|
135
|
-
.inl{display:flex;flex-direction:column;gap:
|
|
165
|
+
.inl{display:flex;flex-direction:column;gap:8px;padding:12px;background:var(--bg-2);border:1px solid var(--accent);border-radius:var(--r-s);box-shadow:var(--shadow-2)}
|
|
136
166
|
.inl input,.inl textarea,.notes textarea{width:100%;border:1px solid var(--line);border-radius:var(--r-s);padding:8px 12px;background:var(--bg-2);font:inherit;color:var(--ink)}
|
|
137
167
|
.inl input:focus,.inl textarea:focus,.notes textarea:focus,.sel:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}
|
|
138
168
|
.inl textarea,.notes textarea{min-height:72px;resize:vertical}
|
|
@@ -141,6 +171,8 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
141
171
|
.inl .row,.notes .row{display:flex;gap:6px;justify-content:flex-end}
|
|
142
172
|
.notes{display:flex;flex-direction:column;gap:8px}
|
|
143
173
|
#pbody img{max-width:100%;border-radius:var(--r-s)}
|
|
174
|
+
/* the in-card agent: a wider panel, and the terminal filling its body */
|
|
175
|
+
aside.wide{width:min(960px,100%)}#pbody .term{height:100%;min-height:320px;background:var(--bg);border-radius:var(--r-s)}#pbody .term .xterm{height:100%;padding:4px}#pbody .term .xterm-viewport{background:transparent!important}
|
|
144
176
|
.desc{border-bottom:1px solid var(--line);padding-bottom:6px;margin-bottom:14px}.desc h3{margin-top:14px}
|
|
145
177
|
.chat{display:flex;flex-direction:column;gap:10px;margin:0 0 14px}.msg{border:1px solid var(--line);border-radius:var(--r);padding:10px 12px;background:var(--bg)}.msg time{display:block;font-size:11px;color:var(--dim);margin-bottom:4px}.msg p:last-child{margin-bottom:0}.msg img{max-width:100%;max-height:260px;border-radius:var(--r-s);display:block;margin:4px 0}
|
|
146
178
|
.notes .tools{display:flex;gap:6px;align-items:center}.notes .tools span{flex:1;font-size:11px;color:var(--dim)}.notes .att{font-size:11px;color:var(--dim)}
|
|
@@ -154,17 +186,24 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
154
186
|
@keyframes fi{from{opacity:0}}
|
|
155
187
|
|
|
156
188
|
/* footer */
|
|
157
|
-
.ftr{display:flex;justify-content:space-between;gap:16px;padding:
|
|
189
|
+
.ftr{display:flex;justify-content:space-between;gap:16px;padding:6px 28px;border-top:1px solid var(--line);color:var(--dim);font-size:12px;flex-wrap:wrap;align-items:center}
|
|
158
190
|
.ftr span+span,.ftr a+a{margin-left:14px}.ftr .strip{margin-right:14px;vertical-align:middle;font-size:12px}
|
|
159
191
|
/* The footer is out of the eye's path, so the waiting alert is filled, not just outlined. */
|
|
160
192
|
.ftr .strip .attn{color:var(--ink);background:color-mix(in srgb,var(--warn) 18%,transparent);box-shadow:inset 0 0 0 1px var(--warn)}.ftr .strip .attn:hover,.ftr .strip .attn.on{background:color-mix(in srgb,var(--warn) 30%,transparent)}.ftr .strip .attn svg{color:var(--warn)}.ftr .st svg{width:12px;height:12px}
|
|
161
|
-
.ftr a{color:var(--dim);cursor:pointer;white-space:nowrap;text-decoration:
|
|
193
|
+
.ftr a{color:var(--dim);cursor:pointer;white-space:nowrap;text-decoration:none}.ftr a:hover{color:var(--ink)}
|
|
162
194
|
|
|
163
195
|
/* side panel */
|
|
164
196
|
.scrim{position:fixed;inset:0;z-index:10;background:color-mix(in srgb,var(--bg) 60%,transparent);backdrop-filter:blur(3px);display:none}
|
|
165
197
|
.scrim.on{display:block}
|
|
166
198
|
aside{position:fixed;top:0;right:0;bottom:0;z-index:11;width:520px;max-width:100%;background:var(--bg-2);border-left:1px solid var(--line);box-shadow:var(--shadow-2);display:flex;flex-direction:column;transform:translateX(100%);visibility:hidden;transition:transform .18s,visibility .18s}
|
|
167
199
|
aside.open{transform:none;visibility:visible}
|
|
200
|
+
/* Settings and the Icebox are pages: the same panel, docked into the app column, its content in a centred column */
|
|
201
|
+
body.paged #board,body.paged .ftr,body.paged .hdr,body.paged .scrim{display:none}
|
|
202
|
+
aside.docked{position:static;flex:1;min-height:0;width:auto;transform:none;visibility:visible;transition:none;box-shadow:none;border:0;background:none}
|
|
203
|
+
aside.docked>header,aside.docked>.body{padding-inline:max(28px,calc((100% - 760px)/2))}
|
|
204
|
+
aside.docked>header{border-bottom:0;padding-top:28px;padding-bottom:12px}
|
|
205
|
+
aside.docked>header b{font-size:20px}
|
|
206
|
+
aside.docked #pclose{display:none}
|
|
168
207
|
aside header{display:flex;align-items:flex-start;gap:12px;padding:20px 24px 16px;border-bottom:1px solid var(--line)}
|
|
169
208
|
aside header .t{flex:1;min-width:0}
|
|
170
209
|
aside header b{display:block;font-size:17px;font-weight:600;line-height:1.35}
|
|
@@ -241,14 +280,45 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
241
280
|
.help p{margin:0 0 16px;color:var(--dim)}.help dl{display:grid;grid-template-columns:auto 1fr;gap:10px 20px;align-items:center;margin:0}.help dt{display:flex;gap:6px;justify-content:flex-end}.help dd{margin:0}
|
|
242
281
|
.help kbd{font:12px/1 ui-monospace,monospace;padding:4px 7px;border:1px solid var(--line);border-bottom-width:2px;border-radius:var(--r-s);background:var(--bg);color:var(--ink-hi)}
|
|
243
282
|
/* request sources: cards, status badges, top bar, and add-box */
|
|
244
|
-
|
|
245
|
-
.
|
|
246
|
-
.
|
|
247
|
-
.
|
|
248
|
-
.
|
|
249
|
-
.
|
|
250
|
-
.
|
|
251
|
-
.
|
|
283
|
+
/* the icebox as an inbox: toolbar, filter chips, one row per request, bulk bar */
|
|
284
|
+
.ice-bar{display:flex;align-items:center;gap:8px;margin:8px 0 8px}
|
|
285
|
+
.ice-bar .count{font-size:13px;font-weight:600;color:var(--ink)}
|
|
286
|
+
.ice-bar .end{flex:1}
|
|
287
|
+
.ice-bar .btn,.ice-bulk .btn,.ice-ra .btn{display:inline-flex;align-items:center;gap:6px}
|
|
288
|
+
.ice-bar .btn svg,.ice-bulk .btn svg,.ice-ra .btn svg{width:14px;height:14px}
|
|
289
|
+
.ice-bar input,.ice-row input,.ice-bulk input{accent-color:var(--accent);width:15px;height:15px;margin:0;flex-shrink:0}
|
|
290
|
+
.ice-bar .ice-sel{width:auto;max-width:170px;height:28px;font-size:12px;padding:2px 26px 2px 8px}
|
|
291
|
+
@media(max-width:640px){.ice-bar{flex-wrap:wrap}.ice-bar .ice-sel{max-width:none;min-width:0;width:0;flex:1 1 40%}}
|
|
292
|
+
.ice-bulk{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:8px;padding:6px 8px 6px 14px;margin:0 0 8px;border-radius:var(--r-pill);background:color-mix(in srgb,var(--dim) 12%,var(--bg-2));font-size:13px;color:var(--ink)}
|
|
293
|
+
.ice-bulk .end{flex:1}
|
|
294
|
+
.ice-list{display:flex;flex-direction:column;gap:1px}
|
|
295
|
+
.ice-list .ice-cat{margin:0;padding:14px 12px 4px;font-size:12px;font-weight:500}.ice-cat i{font-style:normal;font-weight:400;margin-left:6px}.ice-list .ice-cat:first-child{padding-top:4px}
|
|
296
|
+
.ice-row{border-radius:var(--r-s)}.ice-open{border-radius:0 0 var(--r-s) var(--r-s);margin-top:-1px;background:var(--hover)}.ice-row:has(+.ice-open){border-radius:var(--r-s) var(--r-s) 0 0;background:var(--hover)}
|
|
297
|
+
.ice-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;align-items:center;gap:10px;min-height:36px;padding:2px 8px 2px 12px;cursor:pointer}
|
|
298
|
+
.ice-row:hover,.ice-row.cur{background:var(--hover)}
|
|
299
|
+
.ice-row.cur{box-shadow:inset 0 0 0 1px var(--line)}
|
|
300
|
+
.ice-row.sel{background:color-mix(in srgb,var(--accent) 8%,transparent)}
|
|
301
|
+
.ice-t{font:inherit;font-size:13px;color:var(--ink);background:none;border:0;padding:6px 0;text-align:left;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
302
|
+
.ice-t[aria-expanded=true]{font-weight:500}
|
|
303
|
+
.ice-src{font-size:11px;color:var(--dim);max-width:150px;display:inline-flex;align-items:center;gap:6px;min-width:0}.ice-src .nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
304
|
+
.ice-age{font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums;white-space:nowrap;min-width:64px;text-align:right}
|
|
305
|
+
.ice-ra{display:none;gap:2px;min-width:64px;justify-content:flex-end}
|
|
306
|
+
.ice-ra .btn{padding:0 6px}
|
|
307
|
+
.ice-row:hover .ice-age,.ice-row.cur .ice-age,.ice-row:focus-within .ice-age{display:none}
|
|
308
|
+
.ice-row:hover .ice-ra,.ice-row.cur .ice-ra,.ice-row:focus-within .ice-ra{display:flex}
|
|
309
|
+
.ice-open{display:grid;gap:8px;padding:6px 12px 12px 37px}
|
|
310
|
+
.ice-zero{padding:40px 16px;text-align:center;border:1px solid var(--line);border-radius:var(--r);background:var(--bg-2)}
|
|
311
|
+
.ice-zero h4{margin:0 0 6px;font-size:14px;font-weight:600;color:var(--ink)}
|
|
312
|
+
.ice-zero p{margin:0;font-size:12px;color:var(--dim);line-height:1.5}
|
|
313
|
+
.ice-open .hint{font-size:12px;color:var(--dim)}
|
|
314
|
+
/* the sources: the run panel's drawer (aside rules), sliding over the icebox page */
|
|
315
|
+
body:not(.paged) .src-sec,body:not(.paged) .src-scrim{display:none}
|
|
316
|
+
.src-scrim{position:fixed;inset:0;z-index:10;background:color-mix(in srgb,var(--bg) 60%,transparent);backdrop-filter:blur(3px);opacity:0;visibility:hidden;transition:opacity .18s,visibility .18s}
|
|
317
|
+
.src-scrim.on{opacity:1;visibility:visible}
|
|
318
|
+
.src-sec .sub a{color:var(--accent);text-decoration:none;cursor:pointer}
|
|
319
|
+
.src-sec .sub a:hover{text-decoration:underline}
|
|
320
|
+
@media(hover:none){.ice-ra{display:flex}.ice-age{display:none}}
|
|
321
|
+
@media(max-width:640px){.ice-src .nm{display:none}.ice-open{padding-left:12px}}
|
|
252
322
|
.src-list{display:flex;flex-direction:column;gap:10px}
|
|
253
323
|
.src-card{background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r);padding:14px;box-shadow:var(--shadow-1);display:flex;flex-direction:column;gap:9px;transition:border-color .15s}
|
|
254
324
|
.src-card:hover{border-color:color-mix(in srgb,var(--line) 70%,var(--ink))}
|
|
@@ -256,6 +326,9 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
256
326
|
.src-info{display:flex;align-items:center;gap:8px;min-width:0}
|
|
257
327
|
.src-ico{width:20px;height:20px;flex-shrink:0;color:var(--accent);display:inline-flex;align-items:center;justify-content:center}
|
|
258
328
|
.src-ico svg{width:18px;height:18px}
|
|
329
|
+
/* a site's own favicon; the generic icon after it shows only once the image fails and removes itself */
|
|
330
|
+
.fav{display:inline-flex;align-items:center;flex-shrink:0}.fav img{width:16px;height:16px;border-radius:4px;padding:1px;background:#fff;box-sizing:border-box}.fav img+svg{display:none}
|
|
331
|
+
.ice-src .fav svg{width:14px;height:14px}
|
|
259
332
|
.src-title{font-size:14px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
260
333
|
.src-acts{display:flex;align-items:center;gap:4px;flex-shrink:0}
|
|
261
334
|
.src-acts .btn{display:inline-flex;align-items:center;gap:5px}
|
|
@@ -272,8 +345,18 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
272
345
|
.src-badge.dim{background:color-mix(in srgb,var(--dim) 14%,transparent);color:var(--dim)}
|
|
273
346
|
.src-badge svg{width:12px;height:12px;stroke-width:2;flex-shrink:0}
|
|
274
347
|
.src-dest{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dim);padding-top:8px;border-top:1px solid var(--line)}
|
|
275
|
-
.src-dest .sel{flex:1;max-width:240px;height:28px;font-size:12px;padding:2px 8px}
|
|
276
|
-
.src-dest .btn{height:28px;padding:0 10px;font-size:12px}
|
|
348
|
+
.src-dest .sel{flex:1;min-width:0;max-width:240px;height:28px;font-size:12px;padding:2px 8px}
|
|
349
|
+
.src-dest .btn{height:28px;padding:0 10px;font-size:12px;display:inline-flex;align-items:center;gap:5px}
|
|
350
|
+
.src-dest .btn svg{width:14px;height:14px;flex-shrink:0}
|
|
351
|
+
.ice-hd{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin:22px 0 4px}
|
|
352
|
+
.ice-hd h3{margin:0;font-size:14px;font-weight:600;color:var(--ink)}
|
|
353
|
+
.ice-cat{font-size:12px;font-weight:600;color:var(--dim);margin:14px 0 6px}
|
|
354
|
+
.ice-form{display:grid;gap:6px;padding-top:8px;border-top:1px solid var(--line)}
|
|
355
|
+
.ice-form .sel{width:100%;max-width:none}
|
|
356
|
+
.ice-form textarea.sel{height:auto;min-height:64px;font:inherit;font-size:13px;resize:vertical}
|
|
357
|
+
.ice-form-acts{display:flex;align-items:center;gap:8px}
|
|
358
|
+
.ice-form-acts .hint{flex:1;margin:0;font-size:11px}
|
|
359
|
+
.ice-notes{font-size:12px;color:var(--dim);line-height:1.45;overflow-wrap:anywhere;white-space:pre-wrap}
|
|
277
360
|
.src-empty{padding:26px 16px;text-align:center;background:var(--bg);border:1px dashed var(--line);border-radius:var(--r);margin:12px 0 16px}
|
|
278
361
|
.src-empty-ico{width:36px;height:36px;margin:0 auto 10px;color:var(--dim);display:flex;align-items:center;justify-content:center}
|
|
279
362
|
.src-empty-ico svg{width:32px;height:32px}
|
|
@@ -282,10 +365,23 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
282
365
|
.src-empty-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;margin-top:12px;text-align:left}
|
|
283
366
|
.src-empty-item{background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r-s);padding:8px 10px;font-size:11px;color:var(--dim);line-height:1.4}
|
|
284
367
|
.src-empty-item strong{display:block;color:var(--ink);font-weight:500;margin-bottom:2px}
|
|
285
|
-
.src-add-
|
|
286
|
-
.src-add
|
|
287
|
-
.src-add
|
|
288
|
-
|
|
368
|
+
.src-add{display:grid;gap:12px;margin-top:20px;padding:16px;background:var(--bg);border:1px solid var(--line);border-radius:var(--r)}
|
|
369
|
+
.src-add h4{margin:0;font-size:13px;font-weight:600;color:var(--ink)}
|
|
370
|
+
.src-add .two{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
371
|
+
.src-add .two.plat{grid-template-columns:140px 1fr}
|
|
372
|
+
.src-add [hidden]{display:none}
|
|
373
|
+
.src-kind{display:inline-flex;gap:2px;padding:2px;border-radius:var(--r-pill);background:color-mix(in srgb,var(--dim) 12%,var(--bg-2));justify-self:start}
|
|
374
|
+
.src-kind button{font:inherit;font-size:12px;padding:3px 12px;border:0;border-radius:var(--r-pill);background:transparent;color:var(--dim);cursor:pointer}
|
|
375
|
+
.src-kind button[aria-pressed=true]{background:var(--bg-2);color:var(--ink);font-weight:500;box-shadow:var(--shadow-1)}
|
|
376
|
+
.src-add .hd{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
377
|
+
.src-add label{display:grid;gap:4px;font-size:12px;font-weight:500;color:var(--ink)}
|
|
378
|
+
.src-add label i{font-style:normal;font-weight:400;color:var(--dim)}
|
|
379
|
+
.src-add .sel{width:100%;max-width:none}
|
|
380
|
+
.src-add .acts{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
381
|
+
.src-add .acts .hint{margin:0;font-size:11px;color:var(--dim);line-height:1.4}
|
|
382
|
+
.src-add .acts .btn{flex-shrink:0}
|
|
383
|
+
@media(max-width:480px){.src-add .two,.src-add .two.plat{grid-template-columns:1fr}}
|
|
384
|
+
@media(max-width:900px){main{grid-template-columns:1fr;padding-inline:16px;gap:8px}.hdr,.ftr,aside.docked>header,aside.docked>.body{padding-inline:16px}.more{display:grid}.card:hover .age{display:inline}.toast{left:16px;max-width:calc(100% - 32px)}}
|
|
289
385
|
@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}
|
|
290
386
|
</style>
|
|
291
387
|
</head>
|
|
@@ -294,6 +390,7 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
294
390
|
<svg style="display:none"><defs>
|
|
295
391
|
<symbol id="i-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
|
|
296
392
|
<symbol id="i-gear" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></symbol>
|
|
393
|
+
<symbol id="i-inbox" viewBox="0 0 24 24"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.5 5.1L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.5-6.9A2 2 0 0 0 16.8 4H7.2a2 2 0 0 0-1.7 1.1z"/></symbol>
|
|
297
394
|
<symbol id="i-link" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.1.4l3-3a5 5 0 0 0-7.1-7.1l-1.7 1.7M14 11a5 5 0 0 0-7.1-.4l-3 3a5 5 0 0 0 7.1 7.1l1.7-1.7"/></symbol>
|
|
298
395
|
<symbol id="i-play" viewBox="0 0 24 24"><path d="M6 4l14 8-14 8z"/></symbol>
|
|
299
396
|
<symbol id="i-mic" viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3M9 21h6"/></symbol>
|
|
@@ -306,10 +403,17 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
306
403
|
<symbol id="i-open" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M15 3v18"/></symbol>
|
|
307
404
|
<symbol id="i-archive" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="5" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8M10 12h4"/></symbol>
|
|
308
405
|
<symbol id="i-restore" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></symbol>
|
|
406
|
+
<!-- the social platforms' marks, from simple-icons (CC0): their own favicons refuse to load on another site -->
|
|
407
|
+
<symbol id="i-b-x" viewBox="0 0 24 24"><path fill="currentColor" stroke="none" d="M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"/></symbol><symbol id="i-b-tiktok" viewBox="0 0 24 24"><path fill="currentColor" stroke="none" d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/></symbol><symbol id="i-b-instagram" viewBox="0 0 24 24"><path fill="currentColor" stroke="none" d="M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077"/></symbol><symbol id="i-b-threads" viewBox="0 0 24 24"><path fill="currentColor" stroke="none" d="M18.263 11.097c-.03-3.486-1.92-5.586-5.111-5.586-2.13 0-3.922.963-4.863 2.499l2.062 1.438c.535-.843 1.272-1.543 2.628-1.543 1.528 0 2.318.85 2.544 2.431a15 15 0 0 0-2.236-.173c-4.125 0-6.068 1.867-6.068 4.336s1.943 3.99 4.804 3.99c3.139 0 5.013-2.115 5.781-4.735.798.361 1.348 1.204 1.348 2.47 0 3.387-3.907 5.232-7.22 5.232-4.885 0-8.077-3.207-8.077-8.424 0-6.392 4.223-10.487 9.9-10.487 3.808 0 5.69 1.671 6.97 3.914l2.108-1.475C21.44 2.078 18.331 0 13.663 0 6.227 0 1.168 5.277 1.168 12.934c0 7 4.953 11.066 10.856 11.066 4.878 0 9.809-2.846 9.809-7.716 0-2.545-1.46-4.231-3.569-5.187m-6.33 4.855c-1.077 0-2.026-.512-2.026-1.453 0-1.483 1.822-1.934 3.606-1.934.678 0 1.34.045 1.927.173-.422 1.927-1.671 3.215-3.508 3.214Z"/></symbol>
|
|
408
|
+
<symbol id="i-at" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.9 7.9"/></symbol>
|
|
309
409
|
<symbol id="i-sheet" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18"/></symbol>
|
|
310
410
|
<symbol id="i-doc" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/></symbol>
|
|
311
411
|
<symbol id="i-refresh" viewBox="0 0 24 24"><path d="M21 3v5h-5M3 21v-5h5M20.5 13A9 9 0 0 1 5.7 18.3L3 16M3.5 11A9 9 0 0 1 18.3 5.7L21 8"/></symbol>
|
|
312
412
|
<symbol id="i-ext" viewBox="0 0 24 24"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"/></symbol>
|
|
413
|
+
<symbol id="i-board" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16M15 4v16"/></symbol>
|
|
414
|
+
<symbol id="i-side" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/></symbol>
|
|
415
|
+
<symbol id="i-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></symbol>
|
|
416
|
+
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z"/></symbol>
|
|
313
417
|
<symbol id="s-idea" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" stroke-dasharray="2 2.5"/></symbol>
|
|
314
418
|
<symbol id="s-start" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" stroke-dasharray="2 2.5"/><path d="M6.5 5.5v5l4-2.5z" fill="currentColor" stroke="none"/></symbol>
|
|
315
419
|
<symbol id="s-run" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6"/><path d="M8 2a6 6 0 0 1 0 12z" fill="currentColor" stroke="none"/></symbol>
|
|
@@ -329,17 +433,40 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
329
433
|
<symbol id="c-git" viewBox="0 0 16 16"><circle cx="4.5" cy="3.5" r="1.5"/><circle cx="4.5" cy="12.5" r="1.5"/><circle cx="11.5" cy="5.5" r="1.5"/><path d="M4.5 5v6M11.5 7c0 2.5-7 1.5-7 4"/></symbol>
|
|
330
434
|
</defs></svg>
|
|
331
435
|
|
|
332
|
-
<
|
|
333
|
-
<
|
|
436
|
+
<nav class="rail" aria-label="Main">
|
|
437
|
+
<div class="top"><a class="home" href="/" title="Board" aria-label="kaizen board"><pre class="mark"><span>█ █ ▄▀█ █ ▀▀█ █▀▀ █▄ █</span>
|
|
334
438
|
<span>█▀▄ █▀█ █ ▄▀ ██▄ █ ▀█</span>
|
|
335
|
-
<span>█ █ █ █ █ █▄▄ █▄▄ █ █</span></pre>
|
|
439
|
+
<span>█ █ █ █ █ █▄▄ █▄▄ █ █</span></pre><span class="mascot"></span></a><button class="ib" id="fold" aria-label="Collapse menu" title="Collapse menu"><svg><use href="#i-side"/></svg></button></div>
|
|
440
|
+
<a class="it" href="/" data-view="board" title="Board"><svg><use href="#i-board"/></svg><span class="lbl">Board</span></a>
|
|
441
|
+
<a class="it" href="/notebook" data-view="notes" title="Notes"><svg><use href="#i-note"/></svg><span class="lbl">Notes</span></a>
|
|
442
|
+
<a class="it" href="/?view=icebox" data-view="sources" title="Icebox: requests gathered from your sources"><svg><use href="#i-inbox"/></svg><span class="lbl">Icebox</span></a>
|
|
443
|
+
<a class="it" href="/?view=settings" data-view="settings" title="Settings"><svg><use href="#i-gear"/></svg><span class="lbl">Settings</span></a>
|
|
444
|
+
<span class="end"></span>
|
|
445
|
+
<div class="theme" role="group" aria-label="Theme"><button data-t="light" title="Light theme"><svg><use href="#i-sun"/></svg><span class="lbl">Light</span></button><button data-t="dark" title="Dark theme"><svg><use href="#i-moon"/></svg><span class="lbl">Dark</span></button></div>
|
|
446
|
+
</nav>
|
|
447
|
+
<script>
|
|
448
|
+
// Side menu: theme and collapse, the same lines in board.html and notes.html. Runs before
|
|
449
|
+
// the page draws, so a stored theme or a collapsed menu never flashes the default first.
|
|
450
|
+
(()=>{const r=document.documentElement,wide=matchMedia("(max-width:1100px)"),phone=matchMedia("(max-width:640px)"),os=matchMedia("(prefers-color-scheme: dark)");
|
|
451
|
+
// The toggle's choice is kept per browser; with none, the width decides. A phone always gets icons.
|
|
452
|
+
let fold=null;try{fold=localStorage.getItem("kz-rail")}catch(e){}
|
|
453
|
+
const rail=()=>{const m=phone.matches||(fold?fold==="min":wide.matches),b=document.getElementById("fold");r.dataset.rail=m?"min":"full";b.title=m?"Expand menu":"Collapse menu";b.setAttribute("aria-label",b.title);b.setAttribute("aria-expanded",!m)};
|
|
454
|
+
wide.addEventListener("change",rail);phone.addEventListener("change",rail);rail();
|
|
455
|
+
document.getElementById("fold").onclick=()=>{fold=r.dataset.rail==="min"?"full":"min";try{localStorage.setItem("kz-rail",fold)}catch(e){}rail()};
|
|
456
|
+
// Theme: light, dark, or unset for system, in kz-theme. The switch shows what is on screen.
|
|
457
|
+
const paint=()=>{const d=r.dataset.theme?r.dataset.theme==="dark":os.matches;document.querySelectorAll(".theme button").forEach(b=>b.setAttribute("aria-pressed",(b.dataset.t==="dark")===d))};
|
|
458
|
+
window.setTheme=t=>{if(t==="system")delete r.dataset.theme;else r.dataset.theme=t;try{t==="system"?localStorage.removeItem("kz-theme"):localStorage.setItem("kz-theme",t)}catch(e){}paint();dispatchEvent(new Event("kz-theme"))};
|
|
459
|
+
try{const t=localStorage.getItem("kz-theme");if(t)r.dataset.theme=t}catch(e){}
|
|
460
|
+
os.addEventListener("change",paint);paint();
|
|
461
|
+
document.querySelectorAll(".theme button").forEach(b=>b.onclick=()=>setTheme(b.dataset.t))})();
|
|
462
|
+
</script>
|
|
463
|
+
<div class="app">
|
|
464
|
+
<div class="hdr">
|
|
465
|
+
<h1>Board</h1>
|
|
336
466
|
<div class="combo" id="combo"><button class="sel" id="proj" aria-label="project" aria-haspopup="listbox" aria-expanded="false"></button><div class="menu-pop" id="projpop" hidden><input class="sel" id="projq" aria-label="Search projects" placeholder="Search projects…" spellcheck="false" autocomplete="off"><div id="projs" role="listbox"></div></div></div>
|
|
337
467
|
<div class="combo" id="showc" hidden><button class="sel" id="show" aria-label="Projects shown" aria-haspopup="true" aria-expanded="false"></button><div class="menu-pop" id="showpop" hidden><input class="sel" id="showq" aria-label="Search projects" placeholder="Search projects…" spellcheck="false" autocomplete="off"><div id="shows"></div><button id="showall">Show all</button></div></div>
|
|
338
468
|
<span class="sp"></span>
|
|
339
469
|
<button class="btn pri" id="newtop">New idea</button>
|
|
340
|
-
<a class="ib" href="/notebook" title="Notebook" aria-label="Notebook"><svg><use href="#i-note"/></svg></a>
|
|
341
|
-
<button class="ib" id="sources" title="Request sources" aria-label="Request sources"><svg><use href="#i-link"/></svg></button>
|
|
342
|
-
<button class="ib" id="settings" title="Settings" aria-label="Settings"><svg><use href="#i-gear"/></svg></button>
|
|
343
470
|
</div>
|
|
344
471
|
<main id="board" aria-live="polite"></main>
|
|
345
472
|
|
|
@@ -352,6 +479,7 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
352
479
|
<div class="body md" id="pbody"></div>
|
|
353
480
|
<div class="foot" id="pfoot"></div>
|
|
354
481
|
</aside>
|
|
482
|
+
</div>
|
|
355
483
|
<div class="toast" id="toast" role="status"></div>
|
|
356
484
|
<div class="help" id="help"><div>
|
|
357
485
|
<p>Optional. Everything here is also a button.</p>
|
|
@@ -478,8 +606,10 @@ async function load(quiet){
|
|
|
478
606
|
for(const c of S.cards){if(c.kind!=="run"||c.archived)continue;if(due(key(c),c.notice,c.status==="done",S.now))notifyUser(c.notice)}
|
|
479
607
|
// A redraw under a form being typed in would reset its fields and its focus.
|
|
480
608
|
if(!(quiet&&mode&&$("board").querySelector(".inl input")))render();
|
|
609
|
+
// Run on an idea closed the panel: open the card on its agent once the board knows the session.
|
|
610
|
+
if(wantTerm&&!openKey&&!mode){const n=S.cards.find(x=>key(x)===wantTerm&&x.pty);if(n){wantTerm=null;open(n);return}}
|
|
481
611
|
if(openKey){const c=S.cards.find(x=>key(x)===openKey),ta=$("pbody").querySelector(".notes textarea");if(ta&&document.activeElement===ta)return;
|
|
482
|
-
if(!c){if(was&&was.kind==="idea")follow(was);return}if(runDoc)openRun(c,true,true);else if(c.kind==="idea")
|
|
612
|
+
if(!c){if(was&&was.kind==="idea")follow(was);return}if(runDoc)openRun(c,true,true);else if(c.kind==="idea")ideaBody(c)}}
|
|
483
613
|
// An idea's card goes when its run starts. Follow it to that run rather than keep
|
|
484
614
|
// offering "Run again": the newest run in its project whose request holds the idea's
|
|
485
615
|
// text, the rule boardCards() retires it by. No such run: close the panel.
|
|
@@ -501,10 +631,12 @@ function card(c){
|
|
|
501
631
|
const chips=[];
|
|
502
632
|
if(c.status==="waiting")chips.push(`<span class="chip warn"><i></i>${esc(need(c.awaiting)||"needs you")}</span>`);
|
|
503
633
|
else if(c.status==="running")chips.push(`<span class="chip run"><i></i>running</span>`);
|
|
634
|
+
if(c.pty&&c.pty.alive)chips.push(`<span class="chip run" title="The agent is running in this card: open it, Terminal tab">in card</span>`);
|
|
504
635
|
if(all)chips.push(`<span class="chip">${esc(c.where)}</span>`);
|
|
505
636
|
chips.push(...(c.tags||[]).map(t=>{const mark=MARKS[t]?`<svg><use href="#c-${t}"/></svg>`:"";return `<span class="chip${t==="full"||t==="lite"?" accent":t==="yolo"?" warn":""}"${t==="yolo"?' title="Started in Yolo: the agent runs commands without asking"':""}>${mark}${esc(NAMES[t]||t)}</span>`}));
|
|
506
|
-
// A gathered idea's notes open with the stamp
|
|
507
|
-
|
|
637
|
+
// A gathered idea's notes open with the stamp promote() writes (ingest() wrote it before
|
|
638
|
+
// the icebox); its chip names the source.
|
|
639
|
+
const g=c.notes&&/^\[[^\]]*\] (?:Gathered from|Promoted from the icebox:) (.+?)\. Outside text/.exec(c.notes);if(g)chips.push(`<span class="chip" title="From ${esc(g[1])}">gathered</span>`);else if(c.notes)chips.push(`<span class="chip">notes</span>`);
|
|
508
640
|
// Information only: the panel's button is where it is committed and pushed.
|
|
509
641
|
if(c.git){const t=c.git==="pushed"?"Committed & pushed":"Not committed & pushed yet";chips.push(`<span class="chip ${c.git==="pushed"?"ok":"warn"}" role="img" title="${t}" aria-label="${t}"><svg><use href="#c-git"/></svg></span>`)}
|
|
510
642
|
const pick=picking&&!archive&&c.column===4;
|
|
@@ -579,7 +711,7 @@ async function startRun(c,kind){
|
|
|
579
711
|
const p=runPick&&runPick.k===key(c)?runPick:null;runPick=null;
|
|
580
712
|
const yolo=!!(p&&p.yolo&&!$("run-yolo")?.hidden);
|
|
581
713
|
const r=await post("/run",{dir:c.state,text:c.text,kind,agent:p?p.agent:undefined,yolo});
|
|
582
|
-
if(r.ok)toast((kind==="full"?"Full":"Lite")+(yolo?" yolo":"")+" run
|
|
714
|
+
if(r.ok){toast((kind==="full"?"Full":"Lite")+(yolo?" yolo":"")+" run "+where(r)+": "+via(r.agent));if(r.pty)wantTerm=key(c)}
|
|
583
715
|
load()}
|
|
584
716
|
|
|
585
717
|
function render(){
|
|
@@ -660,12 +792,15 @@ const TABS=[["Request","request"],["Notes","notes"],["Plan","plan"],["Backlog","
|
|
|
660
792
|
const tabOf=k=>TABS.findIndex(t=>t[1]===k);
|
|
661
793
|
// keepTab: a link already chose the tab.
|
|
662
794
|
function open(c,keepTab){
|
|
795
|
+
if(view!=="board")go("board",true);
|
|
663
796
|
stopDictation(true); // the panel body is about to be replaced
|
|
664
797
|
fixOff.clear();openKey=key(c);$("panel").classList.add("open");$("scrim").classList.add("on");$("ptitle").textContent=c.kind==="idea"?brief(c.text).head:c.text;render();
|
|
665
|
-
if(c.kind==="idea"){runDoc=null;$("psub").textContent="Idea · "+c.where;$("ptabs").innerHTML="";
|
|
666
|
-
const foot=$("pfoot");foot.innerHTML=c.archived?`<button class="btn pri" data-a="restore">Restore</button>`:c.status==="idea"?`<button class="btn ghost danger" data-a="delete">Delete</button><button class="btn ghost" data-a="archive">Archive</button><span class="dec"><button class="btn" data-a="reject">Reject</button><button class="btn" data-a="edit">Edit</button><button class="btn pri" data-a="run">Run</button></span>`:`<span class="note"
|
|
798
|
+
if(c.kind==="idea"){runDoc=null;$("psub").textContent="Idea · "+c.where;$("ptabs").innerHTML="";ideaBody(c);
|
|
799
|
+
const foot=$("pfoot");foot.innerHTML=c.archived?`<button class="btn pri" data-a="restore">Restore</button>`:c.status==="idea"?`<button class="btn ghost danger" data-a="delete">Delete</button><button class="btn ghost" data-a="archive">Archive</button><span class="dec"><button class="btn" data-a="reject">Reject</button><button class="btn" data-a="edit">Edit</button><button class="btn pri" data-a="run">Run</button></span>`:`<span class="note">${c.pty?"Starting: the agent runs below.":"Starting: a run was launched for this idea. No terminal with your agent? Run it again."}</span><span class="dec"><button class="btn" data-a="run">Run again</button></span>`;
|
|
667
800
|
foot.querySelectorAll("[data-a]").forEach(b=>b.onclick=()=>{const a=b.dataset.a;if(a==="archive"||a==="restore"){toggleArchive(c,a==="archive");return}const k=key(c);close();mode=a;modeKey=k;render()});return}
|
|
668
801
|
if(!keepTab)tab=tabOf(c.status==="abandoned"?"plan":({1:"plan",2:"backlog",3:"review",4:"preview"})[c.column]||"request");
|
|
802
|
+
// A run its agent is working on, in this board, opens on that agent.
|
|
803
|
+
if(!keepTab&&c.pty&&c.pty.alive&&c.status==="running")tab=TABS.length;
|
|
669
804
|
openRun(c,false)}
|
|
670
805
|
// Notes are a chat: one entry per message, files attached by paste or picker.
|
|
671
806
|
function parseNotes(src){const out=[];for(const raw of (src||"").split("\n")){const m=/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2})\]\s?(.*)$/.exec(raw);if(m){out.push({at:m[1],text:m[2]});continue}const last=out[out.length-1];if(last)last.text+="\n"+raw.replace(/^\s{2}/,"");else if(raw.trim())out.push({at:"",text:raw})}return out.map(n=>({...n,text:n.text.trim()})).filter(n=>n.text)}
|
|
@@ -724,19 +859,42 @@ function showProgress(c){
|
|
|
724
859
|
// again: something changed on the server, so the doc is read afresh: a note sent, a plan
|
|
725
860
|
// written or a commit made elsewhere shows in nothing the page already holds. One /run
|
|
726
861
|
// fetch per change while a run panel is open; a tab switch reuses the doc.
|
|
862
|
+
// ---- the in-card agent (agent.launch: card). xterm.js is loaded the first time a
|
|
863
|
+
// terminal is shown. One terminal is mounted at a time, in #pbody; a refresh that
|
|
864
|
+
// finds it already showing this session leaves it alone, so it never reconnects.
|
|
865
|
+
let wantTerm=null,termKey=null,termOff=null,termLib=null;
|
|
866
|
+
const where=r=>r.pty?"running in its card":"opened in your terminal";
|
|
867
|
+
const termLoad=()=>termLib||=new Promise((ok,no)=>{const l=document.createElement("link");l.rel="stylesheet";l.href="/term.css";document.head.append(l);const sc=document.createElement("script");sc.src="/term.js";sc.onload=()=>ok(window.kzTerm);sc.onerror=()=>{termLib=null;no()};document.head.append(sc)});
|
|
868
|
+
function termStop(){if(termOff)termOff();termOff=null;termKey=null;$("panel").classList.remove("wide")}
|
|
869
|
+
async function showTerm(c){const k=c.pty.key;if(termKey===k&&$("pbody").querySelector(".term"))return;
|
|
870
|
+
termStop();termKey=k;$("panel").classList.add("wide");$("pbody").innerHTML=`<div class="term" aria-label="Agent terminal"></div>`;
|
|
871
|
+
let mount;try{mount=await termLoad()}catch{$("pbody").innerHTML=`<p style="color:var(--dim)">The terminal could not load.</p>`;return}
|
|
872
|
+
if(termKey!==k)return;termOff=mount($("pbody").querySelector(".term"),k,()=>load(true))}
|
|
873
|
+
// An idea's panel: its terminal while the run it started is being born there, else its notes.
|
|
874
|
+
function ideaBody(c){if(c.pty)showTerm(c);else{termStop();chat(c,c.notes,c.archived||c.status!=="idea")}}
|
|
727
875
|
async function openRun(c,refresh,again){
|
|
728
876
|
if(!refresh||!runDoc||runDoc.id!==c.id||again){const d=await fetch(`/run/${encodeURIComponent(c.id)}?dir=${encodeURIComponent(c.state)}`).then(r=>r.json());
|
|
729
877
|
// A file list being confirmed, or a push under way, keeps the doc it was drawn from.
|
|
730
878
|
if(refresh&&$("pfoot").querySelector(".inl,[data-busy],.more[open]"))return;runDoc=d}
|
|
731
879
|
if(openKey!==key(c))return;
|
|
880
|
+
// The fetch above may have finished after a newer /state: draw the card as it is now
|
|
881
|
+
// (a resumed agent's terminal, not the one that ended).
|
|
882
|
+
c=S.cards.find(x=>key(x)===openKey)||c;
|
|
732
883
|
// A refresh while notes are being typed would wipe the textarea.
|
|
733
|
-
const ta=$("pbody").querySelector("textarea");if(refresh&&ta&&document.activeElement===ta)return;
|
|
884
|
+
const ta=$("pbody").querySelector(".notes textarea");if(refresh&&ta&&document.activeElement===ta)return;
|
|
734
885
|
$("psub").textContent=c.id+" · "+(WORD[c.status]||c.status)+(c.awaiting?" · "+need(c.awaiting):"");
|
|
735
|
-
|
|
886
|
+
// A card whose agent runs in the board gets its terminal as one more tab, chosen once
|
|
887
|
+
// right after a launch from here.
|
|
888
|
+
const tabs=c.pty?[...TABS,["Terminal","term"]]:TABS;
|
|
889
|
+
if(c.pty&&wantTerm===key(c)){wantTerm=null;tab=TABS.length}
|
|
890
|
+
if(tab>=tabs.length)tab=0;
|
|
891
|
+
$("panel").classList.toggle("wide",tabs[tab][1]==="term");
|
|
736
892
|
$("ptabs").innerHTML=tabs.map((t,i)=>`<button class="${i===tab?"on":""}" data-i="${i}">${t[0]}</button>`).join("");
|
|
737
893
|
[...$("ptabs").children].forEach(b=>b.onclick=()=>{tab=+b.dataset.i;openRun(c,true)});
|
|
738
894
|
const k=tabs[tab][1],doc=k==="preview"?(preview(runDoc.impl)&&attach(preview(runDoc.impl),c)):runDoc[k];
|
|
739
|
-
if(k
|
|
895
|
+
if(k!=="term")termStop();
|
|
896
|
+
if(k==="term")showTerm(c);
|
|
897
|
+
else if(k==="notes"){const ro=c.archived||c.status==="done"||c.status==="abandoned";
|
|
740
898
|
chat(c,doc,ro);}
|
|
741
899
|
else $("pbody").innerHTML=doc?md(doc,k==="backlog"):`<p style="color:var(--dim)">No ${({request:"request",plan:"plan",impl:"work report",preview:"preview",review:"review",backlog:"backlog"})[k]} yet.</p>`;
|
|
742
900
|
if(k==="backlog")showProgress(c);
|
|
@@ -757,21 +915,29 @@ async function openRun(c,refresh,again){
|
|
|
757
915
|
// gitBtn's buttons restyled as menu rows.
|
|
758
916
|
const more=gitBtn(c).replace(/class="btn( pri)?"/g,"")+`<button data-a="archive">Archive</button><hr><button data-a="ab" class="danger">Abandon</button>`;
|
|
759
917
|
foot.innerHTML=`<span class="note">${sent?"Sent to your agent.":APPROVAL[c.awaiting]+asks+" Answering here opens a new agent session."}</span><details class="more"><summary class="btn ghost">More</summary><div class="menu-pop">${more}</div></details><span class="dec"><button class="btn" data-a="revise"${sent?" disabled":""}>Revise</button><button class="btn pri" data-a="approve"${sent?" disabled":""}>${c.awaiting==="findings"?"Approve as-is":"Approve"}</button></span>`}
|
|
760
|
-
else if(c.status==="waiting")foot.innerHTML=`<span class="note">Waiting on you: ${esc(need(c.awaiting))}. Answer in the terminal running this run.</span>${arch}<button class="btn ghost" data-a="ab">Abandon</button>`;
|
|
918
|
+
else if(c.status==="waiting")foot.innerHTML=`<span class="note">Waiting on you: ${esc(need(c.awaiting))}. Answer in the ${c.pty&&c.pty.alive?"Terminal tab":"terminal running this run"}.</span>${arch}<button class="btn ghost" data-a="ab">Abandon</button>`;
|
|
761
919
|
else if(c.status==="done"||c.status==="abandoned")foot.innerHTML=arch;
|
|
762
|
-
else foot.innerHTML=`<span class="note">${c.status==="stalled"?"No progress for a while. Resume it
|
|
763
|
-
foot.querySelectorAll("[data-a]").forEach(b=>b.onclick=()=>{const a=b.dataset.a;if(a==="archive"||a==="restore"){toggleArchive(c,a==="archive");return}if(a==="push"){commitPush(c,"",b);return}if(a==="commit"){confirmCommit(c);return}if(a==="approve"){if(qs.length)confirmAnswers(c,qs);else decide(c,b);return}if(a==="revise"){confirmRevise(c);return}close();mode="reject";modeKey=key(c);render()})}
|
|
920
|
+
else foot.innerHTML=`<span class="note">${c.pty&&!c.pty.alive?"Its agent ended before the stage finished. Resume continues it here.":c.status==="stalled"?"No progress for a while. Resume it, or abandon it.":c.pty?"Running here: Terminal tab.":"Running in your agent."}</span>${arch}<button class="btn ghost" data-a="ab">Abandon run</button>${resumable(c)?`<button class="btn pri" data-a="resume">Resume</button>`:""}`;
|
|
921
|
+
foot.querySelectorAll("[data-a]").forEach(b=>b.onclick=()=>{const a=b.dataset.a;if(a==="archive"||a==="restore"){toggleArchive(c,a==="archive");return}if(a==="push"){commitPush(c,"",b);return}if(a==="commit"){confirmCommit(c);return}if(a==="approve"){if(qs.length)confirmAnswers(c,qs);else decide(c,b);return}if(a==="revise"){confirmRevise(c);return}if(a==="resume"){resume(c,b);return}close();mode="reject";modeKey=key(c);render()})}
|
|
764
922
|
// Answering an approval from the panel opens your agent on the run, told the decision and,
|
|
765
923
|
// for a revise, the comment. Answered approvals are remembered outside the footer, which
|
|
766
924
|
// every refresh rebuilds, so a second press cannot open a second agent. Keyed by when the
|
|
767
925
|
// run last moved: the same approval reached again after a revise is a new question.
|
|
768
926
|
// The footer's More menu is a <details>: close it on a click anywhere else.
|
|
769
927
|
document.addEventListener("click",e=>{for(const d of document.querySelectorAll(".more[open]"))if(!d.contains(e.target))d.open=false});
|
|
928
|
+
// Resume: a run whose in-card agent ended, or one stalled with no progress, gets its agent
|
|
929
|
+
// back on the run (its conversation, where the tool can resume one), in its card.
|
|
930
|
+
const resumable=c=>c.status==="stalled"||!!(c.pty&&!c.pty.alive);
|
|
931
|
+
async function resume(c,b){b.disabled=true;askNotify();const r=await post("/resume",{dir:c.state,id:c.id});
|
|
932
|
+
if(!r.ok){b.disabled=false;return}
|
|
933
|
+
toast("Resumed, "+where(r)+": "+via(r.agent));if(r.pty)wantTerm=key(c);
|
|
934
|
+
if(openKey===key(c)){$("pfoot").innerHTML="";openRun(c,true)}}
|
|
770
935
|
const APPROVAL={"approvals.plan":"The plan is ready. Read it in the Plan tab, then approve or send it back.","approvals.review":"The work is ready to check. Approve to finish it, or revise with what to change.","findings":"The review has open findings. Approve as-is to accept them and finish, or fix selected findings first."},decided=new Set(),answer=c=>key(c)+":"+c.awaiting+":"+c.moved;
|
|
771
936
|
async function decide(c,b,why,answers){b.disabled=true;askNotify();const r=await post("/decide",{dir:c.state,id:c.id,awaiting:c.awaiting,...(why===undefined?{}:{why}),...(answers?{answers}:{})});
|
|
772
937
|
// A failed send keeps the footer, and with it a typed comment.
|
|
773
938
|
if(!r.ok){b.disabled=false;return}
|
|
774
|
-
decided.add(answer(c));toast((why===undefined?"Approved":"Sent back")+"
|
|
939
|
+
decided.add(answer(c));toast((why===undefined?"Approved":"Sent back")+", "+where(r)+": "+via(r.agent));
|
|
940
|
+
if(r.pty)wantTerm=key(c);
|
|
775
941
|
if(openKey===key(c)){$("pfoot").innerHTML="";openRun(c,true)}}
|
|
776
942
|
function confirmRevise(c){const foot=$("pfoot"),plan=c.awaiting==="approvals.plan";
|
|
777
943
|
foot.innerHTML=`<div class="inl" style="flex:1"><div class="q">${plan?"What should the plan do differently?":"What should change in the work?"}</div><div class="q dim">Opens your agent on this run with your comment.${plan?" It revises the plan and comes back here for approval.":" It changes the work, rechecks it and comes back here."}</div><textarea aria-label="Revise comment"></textarea><div class="row"><button class="btn ghost" data-a="c">Cancel</button><button class="btn pri" data-a="s">Send back</button></div></div>`;
|
|
@@ -820,7 +986,7 @@ function fixForm(c){const k=n=>c.id+":"+n,sent=f=>fixSent.has(k(f.n)),fs=runDoc.
|
|
|
820
986
|
allb.onclick=()=>{const none=picks().length===open.length;open.forEach(f=>none?fixOff.add(k(f.n)):fixOff.delete(k(f.n)));sync()};
|
|
821
987
|
go.onclick=async()=>{const nums=picks();go.disabled=true;askNotify();const r=await post("/fix",{dir:c.state,id:c.id,nums});
|
|
822
988
|
// Sent findings lock as sent, so a second press cannot open a second agent on them.
|
|
823
|
-
if(r.ok){toast("Fixing "+nums.length+" finding"+(nums.length===1?"":"s")+"
|
|
989
|
+
if(r.ok){toast("Fixing "+nums.length+" finding"+(nums.length===1?"":"s")+", "+where(r)+": "+via(r.agent));if(r.pty)wantTerm=key(c);nums.forEach(n=>fixSent.add(k(n)));try{sessionStorage.setItem(SK,JSON.stringify([...fixSent]))}catch(e){}openRun(c,true)}else sync()};
|
|
824
990
|
sync();$("pbody").appendChild(el)}
|
|
825
991
|
// Commit and push, for a finished run in a project with a git remote. What the board
|
|
826
992
|
// did is recorded per run, so the settled label outlives later changes to the tree.
|
|
@@ -847,7 +1013,7 @@ async function commitPush(c,message,b){b.disabled=true;b.dataset.busy=1;b.textCo
|
|
|
847
1013
|
// A refused message comes back for another try, beside the current file list.
|
|
848
1014
|
if(!r.ok&&message&&openKey===key(c)&&runDoc.git.files&&runDoc.git.files.length)confirmCommit(c,message)}
|
|
849
1015
|
async function toggleArchive(c,archived){const r=await post("/archive",c.kind==="idea"?{dir:c.state,text:c.text,archived}:{dir:c.state,id:c.id,archived});if(r.ok)toast(archived?"Archived":"Restored");close();load()}
|
|
850
|
-
function close(){const k=openKey;stopDictation(true);openKey=null;runDoc=null;$("panel").classList.remove("open");$("scrim").classList.remove("on");render();if(k)cardEl(k)?.focus({preventScroll:true})}
|
|
1016
|
+
function close(){if(view!=="board")return;const k=openKey;stopDictation(true);termStop();openKey=null;runDoc=null;$("panel").classList.remove("open");$("scrim").classList.remove("on");render();if(k)cardEl(k)?.focus({preventScroll:true})}
|
|
851
1017
|
$("pclose").onclick=close;$("scrim").onclick=close;
|
|
852
1018
|
// An idea is one inbox line, so a structured request arrives as one wall:
|
|
853
1019
|
// "Fix x Problem: … Scope: … Acceptance criteria: (1) … (2) …". Split it, for display
|
|
@@ -897,7 +1063,7 @@ const groups=rows=>{const m=new Map();for(const r of rows){if(!m.has(r.group))m.
|
|
|
897
1063
|
let setQ="";
|
|
898
1064
|
async function openSettings(){
|
|
899
1065
|
stopDictation(true);
|
|
900
|
-
openKey=null;runDoc=null;$("panel").classList.add("open");$("scrim").classList.add("on");$("ptitle").textContent="Settings";$("ptabs").innerHTML="";$("pfoot").innerHTML="";
|
|
1066
|
+
termStop();openKey=null;runDoc=null;$("panel").classList.add("open");$("scrim").classList.add("on");$("ptitle").textContent="Settings";$("ptabs").innerHTML="";$("pfoot").innerHTML="";
|
|
901
1067
|
const cur=document.documentElement.dataset.theme||"system";
|
|
902
1068
|
const cfg=await fetch("/settings").then(r=>r.json()).catch(()=>null);
|
|
903
1069
|
$("psub").textContent="Theme applies to this browser. Everything else is saved to your kaizen settings.";$("psub").title=cfg?cfg.file:"";
|
|
@@ -932,70 +1098,217 @@ async function openSettings(){
|
|
|
932
1098
|
// Esc clears a search; with nothing to clear it closes the panel, as it does outside the field.
|
|
933
1099
|
$("sq").onkeydown=e=>{if(e.key!=="Escape")return;e.preventDefault();if(setQ){setQ="";e.target.value="";filter()}else close()};
|
|
934
1100
|
filter();if(hadFocus){const f=$("sq");f.focus();f.setSelectionRange(f.value.length,f.value.length)}
|
|
935
|
-
const setTheme=t=>{if(t==="system")delete document.documentElement.dataset.theme;else document.documentElement.dataset.theme=t;try{t==="system"?localStorage.removeItem("kz-theme"):localStorage.setItem("kz-theme",t)}catch(e){}};
|
|
936
1101
|
$("th").onchange=e=>setTheme(e.target.value);
|
|
937
1102
|
$("nt").onclick=()=>{askNotify();setTimeout(openSettings,300)};
|
|
938
1103
|
if(cfg)cfg.rows.forEach(r=>{const el=$("s-"+r.key.replace(/\./g,"-"));const save=async v=>{const f=document.activeElement?.id;const j=await post("/settings",{key:r.key,value:v});if(j.ok)toast("Saved");await openSettings();if(f?.startsWith("s-preset-"))$(f)?.focus()};
|
|
939
1104
|
if(el.tagName==="SELECT"||el.tagName==="INPUT")el.onchange=async()=>{await save(el.value.trim());if(el.tagName==="INPUT")load()};else el.querySelectorAll("[data-v]").forEach(b=>b.onclick=()=>save(b.dataset.v));if(el.matches(".chk"))el.onclick=()=>save(el.dataset.v);
|
|
940
1105
|
// The system's own folder dialog, opened by the board: a page cannot read a real path.
|
|
941
1106
|
if(r.folder)$(el.id+"-pick").onclick=async()=>{const j=await post("/pick-folder",{});if(j.path){await save(j.path);load()}}})}
|
|
942
|
-
// ----
|
|
943
|
-
|
|
1107
|
+
// ---- views: the board, or Settings and the Icebox as pages in the docked panel, each with its own address
|
|
1108
|
+
let view="board";
|
|
1109
|
+
const markView=v=>document.querySelectorAll(".rail a.it").forEach(a=>a.dataset.view===v?a.setAttribute("aria-current","page"):a.removeAttribute("aria-current"));
|
|
1110
|
+
function go(v,push){if(v==="icebox")v="sources";if(!["settings","sources"].includes(v))v="board";const was=view;view=v;markView(v);document.body.classList.toggle("paged",v!=="board");
|
|
1111
|
+
if(push&&v!==was)history.pushState(null,"",v==="board"?location.pathname:"?view="+(v==="sources"?"icebox":v));
|
|
1112
|
+
const p=$("panel");
|
|
1113
|
+
if(v==="board"){if(was==="board")return;
|
|
1114
|
+
// Undocked without the slide: it was a page, not a drawer leaving.
|
|
1115
|
+
p.style.transition="none";p.classList.remove("open","docked");p.offsetWidth;p.style.transition="";close();return}
|
|
1116
|
+
p.classList.add("docked");if(v==="settings")openSettings();else openSources()}
|
|
1117
|
+
document.querySelectorAll(".rail a[data-view]").forEach(a=>{if(a.dataset.view==="notes")return;a.onclick=e=>{if(e.ctrlKey||e.metaKey||e.shiftKey||e.button)return;e.preventDefault();if(a.dataset.view==="settings")setQ="";go(a.dataset.view,true)}});
|
|
1118
|
+
document.querySelector(".rail .home").onclick=e=>{if(e.ctrlKey||e.metaKey||e.shiftKey||e.button)return;e.preventDefault();go("board",true)};
|
|
1119
|
+
addEventListener("popstate",()=>go(new URLSearchParams(location.search).get("view")));
|
|
1120
|
+
// The menu's theme switch while Settings shows: its Theme row follows.
|
|
1121
|
+
addEventListener("kz-theme",()=>{if(view==="settings")openSettings()});
|
|
1122
|
+
// ---- the icebox: requests gathered from request sources, read like an inbox, until
|
|
1123
|
+
// someone promotes them to ideas or dismisses them. The sources themselves sit in a
|
|
1124
|
+
// section below, folded. Same panel as settings.
|
|
1125
|
+
const SRC="Icebox";
|
|
1126
|
+
// What the page keeps across the rebuild every server change brings: the item whose
|
|
1127
|
+
// promote form is open and the one the cursor is on (both "<dir> <id>"), the checked
|
|
1128
|
+
// items, the source filter ("<dir> <url>"), whether the sources section is open, and
|
|
1129
|
+
// whether a bulk action is still posting.
|
|
1130
|
+
// The category filter, and whether the add form takes a link or a social account/tag. The
|
|
1131
|
+
// project filter is the board's own `shown`, so the board and the icebox narrow alike.
|
|
1132
|
+
let iceForm=null,iceCur=null,iceSrc="",iceCat="",srcKind="link",srcShow=null,iceBusy=false,iceNav=null;const iceSel=new Set();
|
|
1133
|
+
const PLAT={x:"X",tiktok:"TikTok",instagram:"Instagram",threads:"Threads"};
|
|
944
1134
|
const srcOpen=()=>$("panel").classList.contains("open")&&$("ptitle").textContent===SRC;
|
|
945
1135
|
const ago=at=>{const m=Math.round((Date.now()-Date.parse(at))/60000);return m<1?"just now":m<60?m+" min ago":m<1440?Math.round(m/60)+" h ago":Math.round(m/1440)+" d ago"};
|
|
946
1136
|
async function openSources(){
|
|
947
1137
|
stopDictation(true);
|
|
948
1138
|
// What is half typed survives a refresh from the server.
|
|
949
|
-
const typed=srcOpen()?[$("src-url")?.value||"",$("src-label")?.value||"",document.activeElement?.id,$("src-dir")?.value]:["",""];
|
|
1139
|
+
const typed=srcOpen()?[$("src-url")?.value||"",$("src-label")?.value||"",document.activeElement?.id,$("src-dir")?.value,$("src-cat")?.value||"",$("src-plat")?.value,$("src-q")?.value||""]:["",""];
|
|
950
1140
|
// So does a project picked on a row but not yet moved.
|
|
951
1141
|
const picked=new Map(srcOpen()?[...$("pbody").querySelectorAll("[data-mv]")].map(m=>[m.dataset.k,m.value]):[]);
|
|
952
|
-
|
|
953
|
-
|
|
1142
|
+
// And a promote form half filled, for the item it was typed on.
|
|
1143
|
+
// And which control of which inbox row had focus, by item key: row indices shift when a row goes.
|
|
1144
|
+
const fRow=srcOpen()&&document.activeElement?.closest?.(".ice-row"),fPick=fRow?[fRow.dataset.k,document.activeElement.id.replace(/-\d+$/,"")]:null;
|
|
1145
|
+
const iceDraft=srcOpen()&&$("ice-title")?[$("ice-title").dataset.k,$("ice-title").value,$("ice-details").value]:null;
|
|
1146
|
+
// And a category typed on a row but not yet saved.
|
|
1147
|
+
const cats=new Map(srcOpen()?[...$("pbody").querySelectorAll("[data-ct]")].map(m=>[m.dataset.k,m.value]):[]);
|
|
1148
|
+
termStop();openKey=null;runDoc=null;$("panel").classList.add("open");$("scrim").classList.add("on");$("ptitle").textContent=SRC;$("ptabs").innerHTML="";$("pfoot").innerHTML="";
|
|
1149
|
+
$("psub").textContent="Requests gathered from your sources wait here until you promote them to Ideas or dismiss them; nothing starts on its own.";$("psub").title="";
|
|
954
1150
|
const dir=all?null:S.dir,q=dir?"?dir="+encodeURIComponent(dir):"";
|
|
955
1151
|
const d=await fetch("/sources"+q).then(r=>r.json()).catch(()=>null);
|
|
956
1152
|
if(!srcOpen())return;
|
|
957
1153
|
const host=u=>{try{return new URL(u).hostname}catch{return u}};
|
|
958
|
-
|
|
959
|
-
|
|
1154
|
+
// The favicon of the site a link is on, asked of that site itself (Google's own static host for
|
|
1155
|
+
// Sheets and Docs, whose site icon is generic): nothing but the host reaches anyone. A social
|
|
1156
|
+
// platform's mark is drawn here instead: Meta's favicons are same-origin only.
|
|
1157
|
+
const fav=(u,fb,soc)=>{if(PLAT[soc])return `<span class="fav">${ico("b-"+soc)}</span>`;let h;try{const x=new URL(u);if(/^https?:$/.test(x.protocol))h=x.origin}catch{}
|
|
1158
|
+
const src=!h?"":u.includes("docs.google.com/spreadsheets")?"https://ssl.gstatic.com/docs/spreadsheets/favicon3.ico":u.includes("docs.google.com/document")?"https://ssl.gstatic.com/docs/documents/images/kix-favicon7.ico":h+"/favicon.ico";
|
|
1159
|
+
return `<span class="fav">${src?`<img src="${esc(src)}" alt="" loading="lazy" referrerpolicy="no-referrer" onerror="this.remove()">`:""}${ico(fb)}</span>`};
|
|
1160
|
+
const srcType=s=>{if(s.social)return "social";const u=s.url;if(u.includes("docs.google.com/spreadsheets")||u.endsWith(".csv")||u.endsWith(".tsv"))return "sheet";if(u.includes("docs.google.com/document"))return "doc";return "link"};
|
|
1161
|
+
const list=d&&d.sources||[],ice=d&&d.icebox||[];
|
|
960
1162
|
// A source belongs to one project, named on its row. Picking another only arms Move: a select
|
|
961
1163
|
// changes on every arrow key, and each change would have moved the source. The names are the picker's;
|
|
962
1164
|
// a row whose project the picker does not list still gets its own, selected, so Move never points elsewhere.
|
|
963
1165
|
const opts=(on,name)=>(on&&!S.projects.some(p=>p.dir===on)?`<option value="${esc(on)}" selected>${esc(name||on)}</option>`:"")+S.projects.map(p=>`<option value="${esc(p.dir)}"${p.dir===on?" selected":""}>${esc(p.label)}</option>`).join("");
|
|
964
|
-
const topBar=`<div class="src-topbar"><div class="meta"><span class="count">${list.length} source${list.length===1?"":"s"}${dir?` in ${esc(S.projects.find(p=>p.dir===dir)?.label||dir)}`:""}</span><span class="sched">${d&&d.every&&d.every!=="off"?`Auto-check: every ${esc(d.every)}`:"Auto-check: off"} · <a id="src-to-settings" role="button" tabindex="0">Settings</a></span></div><button class="btn sm" id="src-go"${list.length?"":" disabled"}>${ico("refresh")} Gather all</button></div>`;
|
|
965
1166
|
const cards=list.map((s,i)=>{
|
|
966
|
-
const st=srcType(s
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
}).
|
|
970
|
-
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1167
|
+
const st=srcType(s),title=s.label||host(s.url);
|
|
1168
|
+
// A social source is never read by kaizen, so its not-ok is the agent's turn, not a failure.
|
|
1169
|
+
const statusHtml=s.social&&!s.last?.ok?`<span class="src-badge dim">${ico("at")}Gathered by your agent</span><span class="hint" style="margin:0">Run /kaizen gather</span>`:!s.last?`<span class="src-badge dim">Not gathered yet</span>`:s.last.ok?`<span class="src-badge ok"><svg viewBox="0 0 16 16"><path d="M3.5 8.5l3 3 6-6" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>${esc(s.last.note)}</span><span class="hint" style="margin:0">${ago(s.last.at)}</span>`:`<span class="src-badge warn"><svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" stroke="currentColor" fill="none" stroke-width="1.8"/><path d="M8 5v3.5M8 11v.1" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round"/></svg>Failed: ${esc(s.last.note)}</span><span class="hint" style="margin:0">${ago(s.last.at)}</span>`;
|
|
1170
|
+
return `<div class="src-card" id="src-card-${i}"><div class="src-hd"><div class="src-info"><span class="src-ico" title="${st==="social"?esc(PLAT[s.social]||"social"):st}">${fav(s.url,st==="sheet"?"sheet":st==="doc"?"doc":st==="social"?"at":"link",s.social)}</span><span class="src-title" title="${esc(title)}">${esc(title)}</span></div><div class="src-acts">${s.social?"":`<button class="btn ghost sm" id="src-chk-${i}" data-chk="${i}" aria-label="Check ${esc(title)} now" title="Check this source now">${ico("refresh")} Check</button>`}<button class="btn ghost danger sm" id="src-rm-${i}" data-rm="${i}" aria-label="Remove ${esc(title)}" title="Remove source">${ico("trash")}</button></div></div><div class="src-url"><a href="${esc(s.url)}" target="_blank" rel="noopener noreferrer" title="${esc(s.url)}"><span>${esc(s.url)}</span>${ico("ext")}</a></div><div class="src-status">${statusHtml}</div><div class="src-dest"><span>Category</span><input class="sel" id="src-ct-${i}" data-ct="${i}" data-k="${esc(s.dir+" "+s.url)}" value="${esc(s.category||"")}" placeholder="e.g. Feedback" maxlength="40" aria-label="Category of ${esc(title)}"><button class="btn sm" id="src-cs-${i}" aria-label="Save the category of ${esc(title)}" disabled>Save</button></div><div class="src-dest"><span>Project</span><select class="sel" id="src-mv-${i}" data-mv="${i}" data-k="${esc(s.dir+" "+s.url)}" aria-label="Project of ${esc(title)}">${opts(s.dir,s.project)}</select><button class="btn sm" id="src-mb-${i}" aria-label="Move ${esc(title)} to the chosen project" disabled>Move</button></div></div>`;
|
|
1171
|
+
}).join("");
|
|
1172
|
+
const fails=(d&&d.failed||[]).map(f=>`<p style="color:var(--dim)">The sources of ${esc(f)} could not be read.</p>`).join("");
|
|
1173
|
+
const emptyState=`<div class="src-empty"><div class="src-empty-ico">${ico("link")}</div><h4>No request sources yet</h4><p>Connect shared links to gather requests into the icebox, where you choose which become ideas. Nothing runs automatically until you start it.</p><div class="src-empty-grid"><div class="src-empty-item"><strong>Google Sheets</strong>Public sheets with idea or task rows. Rows marked Done are skipped.</div><div class="src-empty-item"><strong>Google Docs</strong>Shared public docs with items or notes written line by line.</div><div class="src-empty-item"><strong>Public links</strong>TSV, CSV, or raw text links containing requests.</div><div class="src-empty-item"><strong>Social media</strong>A keyword, #hashtag or @account on X, TikTok, Instagram or Threads, read by your agent.</div></div></div>`;
|
|
1174
|
+
const social=srcKind==="social";
|
|
1175
|
+
const addBox=`<div class="src-add"><div class="hd"><h4>Add a source</h4><div class="src-kind" role="group" aria-label="Kind of source"><button id="src-k-link" data-kind="link" aria-pressed="${!social}">Link</button><button id="src-k-social" data-kind="social" aria-pressed="${social}">Social</button></div></div><label for="src-url"${social?" hidden":""}><span>Link</span><input class="sel" id="src-url" placeholder="https://docs.google.com/spreadsheets/d/… or any public link" spellcheck="false" autocomplete="off"></label><div class="two plat"${social?"":" hidden"}><label for="src-plat"><span>Platform</span><select class="sel" id="src-plat">${Object.entries(PLAT).map(([k,v])=>`<option value="${k}">${v}</option>`).join("")}</select></label><label for="src-q"><span>Follow</span><input class="sel" id="src-q" placeholder="keyword, #hashtag or @account" maxlength="100" spellcheck="false" autocomplete="off"></label></div><div class="two"><label for="src-label"><span>Label <i>optional</i></span><input class="sel" id="src-label" placeholder="${social?"e.g. Mentions on X":"e.g. Feedback Q4"}"></label><label for="src-cat"><span>Category <i>optional</i></span><input class="sel" id="src-cat" placeholder="${social?"e.g. Social":"e.g. Bugs"}" maxlength="40"></label></div><label for="src-dir"><span>Project</span><select class="sel" id="src-dir">${dir?"":`<option value="">Choose project…</option>`}${opts(dir)}</select></label><div class="acts"><span class="hint">${social?"Kaizen signs in nowhere. Your agent reads public posts when you run /kaizen gather.":"Public links only. For private docs, run /kaizen gather."}</span><button class="btn pri" id="src-add">Add source</button></div></div>`;
|
|
1176
|
+
// The inbox: one row per request, by project when showing all, then by category,
|
|
1177
|
+
// narrowed to one source by its chip. Checked, open and cursor items are kept by key.
|
|
1178
|
+
const srcName=it=>{const s=list.find(x=>x.dir===it.dir&&x.url===it.source);return s?s.label||host(s.url):host(it.source)};
|
|
1179
|
+
const iceKey=it=>it.dir+" "+it.id,srcKey=it=>it.dir+" "+it.source;
|
|
1180
|
+
// Filters narrow in turn: project (the board's own picker, all projects only), then
|
|
1181
|
+
// category, then source; each chip counts what the filters before it leave.
|
|
1182
|
+
const catOf=it=>it.category||"Uncategorised",count=(xs,f)=>{const m=new Map();xs.forEach(it=>{const k=f(it);m.set(k,{it,n:(m.get(k)?.n||0)+1})});return m};
|
|
1183
|
+
const byProj=dir?new Map():count(ice,it=>it.dir);if(!dir)for(const pd of shown)if(!byProj.has(pd)){const p=showable().find(x=>x.dir===pd);if(p)byProj.set(pd,{it:{dir:pd,project:p.label},n:0})}
|
|
1184
|
+
const inProj=it=>dir||!shown.size||shown.has(it.dir),pv=ice.filter(inProj);
|
|
1185
|
+
const byCat=count(pv,catOf);if(iceCat&&!byCat.has(iceCat))iceCat="";
|
|
1186
|
+
const cv=pv.filter(it=>!iceCat||catOf(it)===iceCat);
|
|
1187
|
+
const bySrc=count(cv,srcKey);if(iceSrc&&!bySrc.has(iceSrc))iceSrc="";
|
|
1188
|
+
const keys=new Set(ice.map(iceKey));
|
|
1189
|
+
if(iceForm&&!keys.has(iceForm))iceForm=null;
|
|
1190
|
+
const groups=new Map();ice.forEach((it,i)=>{if(!inProj(it)||iceCat&&catOf(it)!==iceCat||iceSrc&&srcKey(it)!==iceSrc)return;const k=catOf(it);groups.has(k)||groups.set(k,[]);groups.get(k).push(i)});
|
|
1191
|
+
// Rows in the order they are drawn, which is the order j and k walk.
|
|
1192
|
+
const order=[...groups.values()].flat();
|
|
1193
|
+
// A checked request a filter hides is unchecked: a bulk action only ever acts on what shows.
|
|
1194
|
+
{const vis=new Set(order.map(i=>iceKey(ice[i])));[...iceSel].forEach(k=>vis.has(k)||iceSel.delete(k))}
|
|
1195
|
+
if(!order.some(i=>iceKey(ice[i])===iceCur))iceCur=order.length?iceKey(ice[order[0]]):null;
|
|
1196
|
+
const nSel=order.filter(i=>iceSel.has(iceKey(ice[i]))).length;
|
|
1197
|
+
// Filters are plain dropdowns, each shown only when it has a choice to make. The project one
|
|
1198
|
+
// is the board's own picker: one project, or all; several picked on the board read as such.
|
|
1199
|
+
const many=byProj.size>1,opt=(v,on,t)=>`<option value="${esc(v)}"${on?" selected":""}>${esc(t)}</option>`;
|
|
1200
|
+
const pick=(id,label,xs)=>`<select class="sel ice-sel" id="${id}" aria-label="${label}">${xs.join("")}</select>`;
|
|
1201
|
+
const chips=(many?pick("ice-fp","Project",[opt("",!shown.size,"All projects"),...(shown.size>1?[opt("*",true,shown.size+" projects")]:[]),...[...byProj].map(([k,{it}])=>opt(k,shown.size===1&&shown.has(k),it.project||k))]):"")
|
|
1202
|
+
+(byCat.size>1?pick("ice-fc","Category",[opt("",!iceCat,"All categories"),...[...byCat.keys()].map(k=>opt(k,iceCat===k,k))]):"")
|
|
1203
|
+
+(bySrc.size>1?pick("ice-fs","Source",[opt("",!iceSrc,"All sources"),...[...bySrc].map(([k,{it}])=>opt(k,iceSrc===k,srcName(it)))]):"");
|
|
1204
|
+
const bar=`<div class="ice-bar">${order.length?`<input type="checkbox" id="ice-all" aria-label="Select every request shown"${nSel&&nSel===order.length?" checked":""}>`:""}<span class="count">${order.length<ice.length?`${order.length} of ${ice.length}`:ice.length} waiting</span>${chips}<span class="end"></span><button class="btn sm" id="src-go"${list.length?"":" disabled"}>${ico("refresh")} Gather all</button><button class="btn ghost sm" id="src-tog" aria-expanded="${srcShow??!list.length}" aria-controls="src-sec">${ico("link")} Sources · ${list.length}</button></div>`;
|
|
1205
|
+
const bulk=iceSel.size?`<div class="ice-bulk" role="toolbar" aria-label="Selected requests"><span>${iceSel.size} selected</span><span class="end"></span><button class="btn sm" id="ice-bp" title="Each becomes its own idea, titled as gathered (p)">${ico("plus")} Promote</button><button class="btn ghost sm" id="ice-bd" title="Dismiss (d)">${ico("x")} Dismiss</button><button class="btn ghost sm" id="ice-bc" title="Clear selection (Esc)">Clear</button></div>`:"";
|
|
1206
|
+
// The open item's form: a clean title for the idea's card and the person's own details;
|
|
1207
|
+
// what was gathered is added to the idea's notes by the server.
|
|
1208
|
+
const form=it=>`<div class="ice-form"><label for="ice-title" class="hint" style="margin:0">Idea title</label><input class="sel" id="ice-title" data-k="${esc(iceKey(it))}" maxlength="200" value="${esc(it.text)}" aria-label="Idea title"><label for="ice-details" class="hint" style="margin:0">Details (optional)</label><textarea class="sel" id="ice-details" rows="3" placeholder="In your own words: what is needed, for whom, why" aria-label="Details (optional)"></textarea><div class="ice-form-acts"><span class="hint">What was gathered is added to the idea's notes.</span><button class="btn ghost sm" id="ice-cancel">Cancel</button><button class="btn pri sm" id="ice-ok">Promote</button></div></div>`;
|
|
1209
|
+
const row=i=>{const it=ice[i],k=iceKey(it),open=iceForm===k;
|
|
1210
|
+
return `<div class="ice-row${iceCur===k?" cur":""}${iceSel.has(k)?" sel":""}" data-i="${i}" data-k="${esc(k)}"><input type="checkbox" id="ice-ck-${i}" data-ck="${i}"${iceSel.has(k)?" checked":""} aria-label="Select: ${esc(it.text)}"><button class="ice-t" id="ice-o-${i}" data-o="${i}" aria-expanded="${open}" title="${esc(it.text)}">${esc(it.text)}</button><span class="ice-src" title="${esc(srcName(it))}">${fav(it.source,"link",list.find(x=>x.dir===it.dir&&x.url===it.source)?.social)}<span class="nm">${esc(many?it.project||srcName(it):srcName(it))}</span></span><span class="ice-age">${ago(it.at)}</span><span class="ice-ra"><button class="btn ghost sm" id="ice-pr-${i}" data-pr="${i}" title="Promote to Ideas as gathered (p)" aria-label="Promote to Ideas: ${esc(it.text)}">${ico("plus")}</button><button class="btn ghost sm" id="ice-ds-${i}" data-ds="${i}" title="Dismiss (d)" aria-label="Dismiss: ${esc(it.text)}">${ico("x")}</button></span></div>`
|
|
1211
|
+
+(open?`<div class="ice-open">${it.notes?`<div class="ice-notes">${esc(it.notes)}</div>`:""}<span class="hint" style="margin:0">From ${esc(srcName(it))} · ${ago(it.at)}</span>${form(it)}</div>`:"")};
|
|
1212
|
+
const inbox=ice.length&&!order.length?`<div class="ice-zero"><h4>Nothing matches</h4><p>${ice.length} waiting outside these filters.</p><p style="margin-top:10px"><button class="btn sm" id="ice-unf">Clear filters</button></p></div>`:ice.length?`<div class="ice-list">${[...groups].map(([g,ix])=>`<div class="ice-cat">${esc(g)}<i>${ix.length}</i></div>${ix.map(row).join("")}`).join("")}</div>`
|
|
1213
|
+
:`<div class="ice-zero"><h4>Inbox zero</h4><p>Nothing waiting. Gather all checks your sources for new requests; /kaizen discover in your agent sorts what comes in.</p></div>`;
|
|
1214
|
+
// The sources, in the right-hand drawer: shut until asked for, open by itself while there
|
|
1215
|
+
// are none until closed. Slid in only when opened; a rebuild while open shows it in place.
|
|
1216
|
+
const drawer=srcShow??!list.length,slide=drawer&&!$("src-sec")?.classList.contains("open");
|
|
1217
|
+
const sec=`<div class="src-scrim${drawer&&!slide?" on":""}" id="src-scrim"></div><aside class="src-sec${drawer&&!slide?" open":""}" id="src-sec" aria-label="Sources"${drawer?"":" inert"}><header><div class="t"><b>Sources</b><div class="sub">${d&&d.every&&d.every!=="off"?`Auto-check every ${esc(d.every)}`:"Auto-check off"} · <a id="src-to-settings" role="button" tabindex="0">Settings</a></div></div><button class="ib" id="src-x" aria-label="Close sources" title="Close">${ico("close")}</button></header><div class="body">${list.length?`<div class="src-list">${cards}</div>`:emptyState}${addBox}</div></aside>`;
|
|
1218
|
+
$("pbody").innerHTML=d&&d.error?`<p style="color:var(--dim)">${esc(d.error)}</p>`:!d?`<p style="color:var(--dim)">The icebox could not be read.</p>`:bar+bulk+fails+inbox+sec;
|
|
1219
|
+
if(!d||d.error)return;
|
|
1220
|
+
if(iceDraft&&iceDraft[0]===iceForm&&$("ice-title")){$("ice-title").value=iceDraft[1];$("ice-details").value=iceDraft[2]}
|
|
1221
|
+
if($("src-add")){if(typed[5])$("src-plat").value=typed[5];$("src-q").value=typed[6]||"";$("src-url").value=typed[0];$("src-label").value=typed[1];$("src-cat").value=typed[4]||"";if(typed[3]!==undefined&&$("src-dir"))$("src-dir").value=typed[3]}
|
|
1222
|
+
if($("src-to-settings"))$("src-to-settings").onclick=()=>{setQ="sources";go("settings",true)};
|
|
1223
|
+
if(slide){$("src-sec").offsetWidth;$("src-sec").classList.add("open");$("src-scrim").classList.add("on")}
|
|
1224
|
+
const srcShut=()=>{srcShow=false;$("src-sec").classList.remove("open");$("src-scrim").classList.remove("on");$("src-sec").inert=true;$("src-tog").setAttribute("aria-expanded","false");$("src-tog").focus()};
|
|
1225
|
+
$("src-x").onclick=$("src-scrim").onclick=srcShut;$("src-sec").onkeydown=e=>{if(e.key==="Escape"){e.stopPropagation();srcShut()}};
|
|
1226
|
+
$("src-tog").onclick=()=>{if($("src-sec").classList.contains("open"))return srcShut();srcShow=true;openSources().then(()=>srcShow&&$("src-url")?.focus())};
|
|
1227
|
+
if($("ice-fs"))$("ice-fs").onchange=e=>{iceSrc=e.target.value;openSources()};
|
|
1228
|
+
if($("ice-fc"))$("ice-fc").onchange=e=>{iceCat=e.target.value;openSources()};
|
|
1229
|
+
if($("ice-fp"))$("ice-fp").onchange=e=>{const v=e.target.value;if(v==="*")return;shown=new Set(v?[v]:[]);setShown();openSources()};
|
|
1230
|
+
if($("ice-unf"))$("ice-unf").onclick=()=>{iceCat=iceSrc="";if(!dir)shown.clear();setShown();openSources()};
|
|
1231
|
+
$("pbody").querySelectorAll("[data-kind]").forEach(b=>b.onclick=()=>{if(srcKind===b.dataset.kind)return;srcKind=b.dataset.kind;openSources().then(()=>$(srcKind==="social"?"src-q":"src-url")?.focus())});
|
|
975
1232
|
const act=async(body,done)=>{const j=await post("/sources",body);if(j.ok){done(j);load()}if(srcOpen())openSources()};
|
|
976
1233
|
$("pbody").querySelectorAll("[data-mv]").forEach(m=>{const s=list[+m.dataset.mv],b=$("src-mb-"+m.dataset.mv),to=picked.get(m.dataset.k);
|
|
977
1234
|
if(to&&[...m.options].some(o=>o.value===to))m.value=to;
|
|
978
1235
|
(m.onchange=()=>{b.disabled=m.value===s.dir})();
|
|
979
|
-
b.onclick=()=>act({op:"move",dir:s.dir,to:m.value,url:s.url},()=>toast(`Source moved.
|
|
1236
|
+
b.onclick=()=>act({op:"move",dir:s.dir,to:m.value,url:s.url},()=>toast(`Source moved. Requests already gathered stay in ${s.project||"the old project"}.`))});
|
|
1237
|
+
// The category saves on its button or Enter, never per keystroke.
|
|
1238
|
+
$("pbody").querySelectorAll("[data-ct]").forEach(m=>{const s=list[+m.dataset.ct],b=$("src-cs-"+m.dataset.ct),was=cats.get(m.dataset.k);
|
|
1239
|
+
if(was!==undefined)m.value=was;
|
|
1240
|
+
(m.oninput=()=>{b.disabled=m.value.trim()===(s.category||"")})();
|
|
1241
|
+
m.onkeydown=e=>{if(e.key==="Enter"&&!b.disabled)b.click()};
|
|
1242
|
+
b.onclick=()=>{b.disabled=true;act({op:"category",dir:s.dir,url:s.url,category:m.value.trim()},()=>toast(m.value.trim()?`New requests from ${s.label||host(s.url)} go under ${m.value.trim()}`:"Category cleared"))}});
|
|
1243
|
+
// The acting button is disabled as it is pressed, which drops focus to the page; it goes back to the cursor row.
|
|
1244
|
+
const focusCur=()=>{if(!document.activeElement||document.activeElement===document.body)$("pbody").querySelector(".ice-row.cur .ice-t")?.focus()};
|
|
1245
|
+
// Before rows go, the cursor steps to the next row that stays, else the one before.
|
|
1246
|
+
const stepPast=gone=>{const at=order.findIndex(i=>iceKey(ice[i])===iceCur);if(at<0||!gone.has(iceCur))return;
|
|
1247
|
+
const stay=i=>!gone.has(iceKey(ice[i])),n=order.slice(at+1).find(stay)??order.slice(0,at).reverse().find(stay);iceCur=n===undefined?null:iceKey(ice[n])};
|
|
1248
|
+
const iceAct=(op,it,msg,extra={})=>(async()=>{stepPast(new Set([iceKey(it)]));const j=await post("/icebox",{op,dir:it.dir,ids:[it.id],...extra});if(j.ok){if(op==="promote"&&iceForm===iceKey(it))iceForm=null;toast(msg);load()}if(srcOpen())openSources().then(focusCur)})();
|
|
1249
|
+
// Bulk: one dismiss per project; promote one request at a time, each its own idea
|
|
1250
|
+
// titled as gathered (one promote call with several ids would merge them into one).
|
|
1251
|
+
const bulkDo=async op=>{const its=ice.filter(it=>iceSel.has(iceKey(it)));if(!its.length)return;stepPast(new Set(its.map(iceKey)));iceBusy=true;$("ice-bp").disabled=$("ice-bd").disabled=true;let ok=0;
|
|
1252
|
+
try{if(op==="dismiss")for(const dd of new Set(its.map(it=>it.dir))){const ids=its.filter(it=>it.dir===dd).map(it=>it.id);if((await post("/icebox",{op,dir:dd,ids})).ok)ok+=ids.length}
|
|
1253
|
+
else for(const it of its)if((await post("/icebox",{op,dir:it.dir,ids:[it.id]})).ok)ok++}
|
|
1254
|
+
finally{iceBusy=false}
|
|
1255
|
+
iceSel.clear();if(ok)toast(`${ok} ${op==="promote"?"promoted to Ideas":"dismissed"}${ok<its.length?`, ${its.length-ok} not`:""}`);load();if(srcOpen())openSources().then(focusCur)};
|
|
1256
|
+
if(bulk){$("ice-bp").onclick=()=>bulkDo("promote");$("ice-bd").onclick=()=>bulkDo("dismiss");$("ice-bc").onclick=()=>{iceSel.clear();openSources()}}
|
|
1257
|
+
if($("ice-all"))$("ice-all").onchange=()=>{const k=order.map(i=>iceKey(ice[i]));if(nSel===order.length)k.forEach(x=>iceSel.delete(x));else k.forEach(x=>iceSel.add(x));openSources()};
|
|
1258
|
+
if($("ice-all"))$("ice-all").indeterminate=nSel>0&&nSel<order.length;
|
|
1259
|
+
$("pbody").querySelectorAll("[data-ck]").forEach(c=>c.onchange=()=>{const k=iceKey(ice[+c.dataset.ck]);iceCur=k;c.checked?iceSel.add(k):iceSel.delete(k);openSources()});
|
|
1260
|
+
// A row opens on its title or anywhere not a control; open shows what was gathered and the promote form.
|
|
1261
|
+
const toggle=i=>{const k=iceKey(ice[i]);iceCur=k;iceForm=iceForm===k?null:k;openSources().then(()=>{const t=$("ice-title");if(t&&iceForm===k){t.focus();t.select()}else $("ice-o-"+i)?.focus()})};
|
|
1262
|
+
$("pbody").querySelectorAll(".ice-row").forEach(r=>r.onclick=e=>{if(!e.target.closest("input,button"))toggle(+r.dataset.i)});
|
|
1263
|
+
$("pbody").querySelectorAll("[data-o]").forEach(b=>b.onclick=()=>toggle(+b.dataset.o));
|
|
1264
|
+
$("pbody").querySelectorAll("[data-pr]").forEach(b=>b.onclick=()=>{b.disabled=true;iceAct("promote",ice[+b.dataset.pr],"Promoted to Ideas")});
|
|
1265
|
+
$("pbody").querySelectorAll("[data-ds]").forEach(b=>b.onclick=()=>{b.disabled=true;iceAct("dismiss",ice[+b.dataset.ds],"Dismissed")});
|
|
1266
|
+
const open1=ice.find(it=>iceKey(it)===iceForm);
|
|
1267
|
+
if($("ice-ok")){
|
|
1268
|
+
const ok=()=>{const title=$("ice-title").value.trim();if(!title){$("ice-title").focus();return}$("ice-ok").disabled=true;iceAct("promote",open1,"Promoted to Ideas",{text:title,details:$("ice-details").value})};
|
|
1269
|
+
$("ice-ok").onclick=ok;
|
|
1270
|
+
$("ice-cancel").onclick=()=>{iceForm=null;openSources()};
|
|
1271
|
+
$("ice-title").onkeydown=e=>{if(e.key==="Enter")ok();if(e.key==="Escape")$("ice-cancel").click()};
|
|
1272
|
+
$("ice-details").onkeydown=e=>{if(e.key==="Enter"&&(e.ctrlKey||e.metaKey))ok();if(e.key==="Escape")$("ice-cancel").click()};
|
|
1273
|
+
}
|
|
1274
|
+
// Keys, read by the page's keydown while the icebox shows. True when the key was used.
|
|
1275
|
+
iceNav=e=>{if(e.altKey||e.ctrlKey||e.metaKey)return false;
|
|
1276
|
+
const k=e.key,at=order.findIndex(i=>iceKey(ice[i])===iceCur),i=order[at];
|
|
1277
|
+
if(k==="Escape"){if($("src-sec")?.classList.contains("open")){srcShut();return true}if(iceForm){iceForm=null;openSources();return true}if(iceSel.size){iceSel.clear();openSources();return true}return false}
|
|
1278
|
+
if(at<0)return false;
|
|
1279
|
+
if(k==="j"||k==="k"){e.preventDefault();const n=order[Math.max(0,Math.min(order.length-1,at+(k==="j"?1:-1)))];iceCur=iceKey(ice[n]);
|
|
1280
|
+
$("pbody").querySelectorAll(".ice-row").forEach(r=>r.classList.toggle("cur",+r.dataset.i===n));$("pbody").querySelector(`.ice-row[data-i="${n}"]`)?.scrollIntoView({block:"nearest"});return true}
|
|
1281
|
+
const b=k==="o"?$("ice-o-"+i):k==="x"?$("ice-ck-"+i):k==="p"?(iceSel.size?$("ice-bp"):$("ice-pr-"+i)):k==="d"?(iceSel.size?$("ice-bd"):$("ice-ds-"+i)):null;
|
|
1282
|
+
if(!b||b.disabled)return false;e.preventDefault();b.click();return true};
|
|
980
1283
|
$("pbody").querySelectorAll("[data-chk]").forEach(b=>{const s=list[+b.dataset.chk];
|
|
981
1284
|
b.onclick=()=>{b.disabled=true;b.innerHTML=ico("refresh")+" Checking…";
|
|
982
1285
|
act({op:"gather",dir:s.dir,url:s.url},j=>{const r=j.results&&j.results[0];
|
|
983
|
-
toast(r?(r.ok?`${s.label||host(s.url)}: ${r.note}`:`Failed: ${r.note}`):"Checked")})}});
|
|
1286
|
+
toast(r?(r.ok?`${s.label||host(s.url)}: ${r.note}${r.added?" in the icebox":""}`:`Failed: ${r.note}`):"Checked")})}});
|
|
984
1287
|
$("pbody").querySelectorAll("[data-rm]").forEach(b=>{const s=list[+b.dataset.rm];
|
|
985
|
-
b.onclick=()=>{if(!confirm(`Remove ${s.label||host(s.url)}?
|
|
1288
|
+
b.onclick=()=>{if(!confirm(`Remove ${s.label||host(s.url)}? What it gathered stays; added again later, it may bring back ones you deleted or dismissed.`))return;
|
|
986
1289
|
act({op:"remove",dir:list[+b.dataset.rm].dir,url:list[+b.dataset.rm].url},()=>toast("Source removed"))}});
|
|
987
|
-
|
|
988
|
-
|
|
1290
|
+
// A row that went takes focus to the cursor row's title, never to whatever now holds its index.
|
|
1291
|
+
const ci=ice.findIndex(it=>iceKey(it)===iceCur);
|
|
1292
|
+
if(fPick){const at=ice.findIndex(it=>iceKey(it)===fPick[0]);(at>=0?$(fPick[1]+"-"+at):ci>=0?$("ice-o-"+ci):null)?.focus()}
|
|
1293
|
+
const was=!fPick&&typed[2]&&$(typed[2]);if(was)(was.disabled?$(was.id.replace("src-mb-","src-mv-").replace("src-cs-","src-ct-")):was)?.focus();
|
|
1294
|
+
if($("src-add")){
|
|
1295
|
+
// A social source is a platform and what to follow; the server builds its link, and
|
|
1296
|
+
// nothing is gathered here: the agent reads it.
|
|
1297
|
+
$("src-add").onclick=()=>{const social=srcKind==="social",url=$("src-url").value.trim(),q=$("src-q").value.trim(),plat=$("src-plat").value;
|
|
1298
|
+
if(social?!q:!url){$(social?"src-q":"src-url").focus();return}const to=$("src-dir").value;if(!to){$("src-dir").focus();return}
|
|
989
1299
|
$("src-add").disabled=true;$("src-add").textContent="Adding…";
|
|
990
|
-
(async()=>{const
|
|
991
|
-
|
|
992
|
-
|
|
1300
|
+
(async()=>{const label=$("src-label").value.trim()||(social?`${PLAT[plat]}: ${q}`:""),category=$("src-cat").value.trim();
|
|
1301
|
+
const j=await post("/sources",social?{op:"add",dir:to,platform:plat,query:q,label,category}:{op:"add",dir:to,url,label,category});
|
|
1302
|
+
if(j.ok&&social){$("src-q").value=$("src-label").value=$("src-cat").value="";toast("Source added. Run /kaizen gather in your agent to read its posts.");load()}
|
|
1303
|
+
else if(j.ok){$("src-url").value=$("src-label").value=$("src-cat").value="";const g=await post("/sources",{op:"gather",dir:to,url}),r=g.results&&g.results[0];
|
|
1304
|
+
toast(r?(r.ok?`Source added: ${r.note} in the icebox`:`Source added, but: ${r.note}`):"Source added");load()}
|
|
993
1305
|
else toast(j.error||"Could not add source");
|
|
994
1306
|
if(srcOpen())openSources()})()};
|
|
995
|
-
$("src-url").onkeydown=$("src-label").onkeydown=e=>{if(e.key==="Enter")$("src-add").click()};
|
|
1307
|
+
$("src-url").onkeydown=$("src-q").onkeydown=$("src-label").onkeydown=$("src-cat").onkeydown=e=>{if(e.key==="Enter")$("src-add").click()};
|
|
1308
|
+
}
|
|
996
1309
|
if($("src-go"))$("src-go").onclick=()=>{$("src-go").disabled=true;$("src-go").innerHTML=ico("refresh")+" Gathering…";
|
|
997
|
-
act({op:"gather",dir:dir||undefined},j=>{const
|
|
998
|
-
toast((n?`${n} new
|
|
1310
|
+
act({op:"gather",dir:dir||undefined},j=>{const rs=j.results||[],soc=r=>list.some(s=>s.url===r.url&&s.social),n=rs.reduce((a,r)=>a+r.added,0),bad=rs.filter(r=>!r.ok&&!soc(r)).length,ag=rs.filter(soc).length;
|
|
1311
|
+
toast((n?`${n} new in the icebox`:"Nothing new")+(bad?`, ${bad} source${bad===1?"":"s"} could not be read`:"")+(ag?`; ${ag} social for /kaizen gather`:""))})}}
|
|
999
1312
|
// With `report`, the toast carries a link to a prefilled GitHub bug form (/issue) and
|
|
1000
1313
|
// stays long enough to click it.
|
|
1001
1314
|
let tt;function toast(s,report){const t=$("toast");t.textContent=s;t.classList.toggle("rep",!!report);if(report){const a=document.createElement("a");a.href="/issue?"+new URLSearchParams(report);a.target="_blank";a.rel="noopener";a.textContent="Report";t.append(" ",a)}t.classList.add("on");clearTimeout(tt);tt=setTimeout(()=>t.classList.remove("on"),report?8000:4000)}
|
|
@@ -1048,16 +1361,16 @@ $("shows").onchange=e=>{const i=e.target.dataset.i,d=SP[i]?.dir;if(!d)return;e.t
|
|
|
1048
1361
|
$("showall").onclick=()=>{shown.clear();setShown()};
|
|
1049
1362
|
document.addEventListener("click",e=>{if(!e.target.closest("#showc")){$("showpop").hidden=true;$("show").setAttribute("aria-expanded",false)}});
|
|
1050
1363
|
$("newtop").onclick=()=>{mode="new";render();$("board").scrollTo(0,0)};
|
|
1051
|
-
$("settings").onclick=()=>{setQ="";openSettings()};$("sources").onclick=openSources;
|
|
1052
|
-
try{const t=localStorage.getItem("kz-theme");if(t)document.documentElement.dataset.theme=t}catch(e){}
|
|
1053
1364
|
$("kbd").onclick=()=>$("help").classList.add("on");$("help").onclick=()=>$("help").classList.remove("on");
|
|
1054
1365
|
document.addEventListener("click",()=>{if(menuKey||clearMenu){menuKey=null;clearMenu=false;render()}});
|
|
1055
1366
|
document.onkeydown=e=>{
|
|
1367
|
+
// The icebox reads its own keys first; a checkbox there keeps them, for x then j.
|
|
1368
|
+
if(view==="sources"&&iceNav&&srcOpen()&&!$("help").classList.contains("on")&&!e.target.matches("input:not([type=checkbox]),select,textarea,[contenteditable]")&&!e.target.closest("#src-sec")&&iceNav(e))return;
|
|
1056
1369
|
if(e.target.matches("input,select,textarea,[contenteditable]"))return;
|
|
1057
1370
|
if(e.key==="?"){$("help").classList.toggle("on");return}
|
|
1058
1371
|
if(e.key==="/"&&$("sq")&&$("panel").classList.contains("open")){e.preventDefault();$("sq").focus();return}
|
|
1059
1372
|
if(e.key==="Escape"){$("help").classList.remove("on");if(mode){mode=null;render()}else close();return}
|
|
1060
|
-
if(e.key==="n"){mode="new";render()}
|
|
1373
|
+
if(e.key==="n"){if(view!=="board")go("board",true);mode="new";render()}
|
|
1061
1374
|
if(e.key==="a"){all=!all;keep(all?null:S.dir);load()}
|
|
1062
1375
|
const mv={ArrowUp:[0,-1],k:[0,-1],ArrowDown:[0,1],j:[0,1],ArrowLeft:[-1,0],h:[-1,0],ArrowRight:[1,0],l:[1,0]}[e.key];
|
|
1063
1376
|
// In a run's panel: left/right between its tabs, up/down scroll it. Other panels keep the browser's arrows.
|
|
@@ -1074,13 +1387,15 @@ document.onkeydown=e=>{
|
|
|
1074
1387
|
let boot=null;
|
|
1075
1388
|
// Back from a terminal: what was committed or pushed there touched nothing the server watches.
|
|
1076
1389
|
addEventListener("focus",()=>load(true));
|
|
1077
|
-
function listen(){const es=new EventSource("/events");es.onmessage=ev=>{if(ev.data==="changed"){load(true);if(srcOpen()&&!$("src-go")?.disabled&&!$("src-add")?.disabled)openSources()}else if(ev.data==="reload")location.reload();else if(ev.data.startsWith("hello ")){const b=ev.data.slice(6);if(boot&&b!==boot)location.reload();boot=b}};es.onerror=()=>{es.close();setTimeout(listen,3000)}}
|
|
1390
|
+
function listen(){const es=new EventSource("/events");es.onmessage=ev=>{if(ev.data==="changed"){load(true);if(srcOpen()&&!iceBusy&&!$("src-go")?.disabled&&!$("src-add")?.disabled)openSources()}else if(ev.data==="reload")location.reload();else if(ev.data.startsWith("hello ")){const b=ev.data.slice(6);if(boot&&b!==boot)location.reload();boot=b}};es.onerror=()=>{es.close();setTimeout(listen,3000)}}
|
|
1078
1391
|
// Back to the project picked last time, unless the URL says which view it wants. A
|
|
1079
1392
|
// plain fetch: a project that is gone should fall back to all, not raise a toast.
|
|
1393
|
+
// A page named in the address shows from the first paint; it is filled once the board has loaded.
|
|
1394
|
+
const v0=(v=>v==="icebox"?"sources":v)(new URLSearchParams(location.search).get("view"));markView(["settings","sources"].includes(v0)?v0:"board");document.body.classList.toggle("paged",["settings","sources"].includes(v0));
|
|
1080
1395
|
(async()=>{let d;try{d=localStorage.getItem("kz-proj")}catch(e){}
|
|
1081
1396
|
if(d&&!urlAll)try{if((await fetch("/cd",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({dir:d})})).ok)all=false;else keep()}catch(e){}
|
|
1082
1397
|
try{const f=JSON.parse(sessionStorage.getItem("kz-form"));if(f)[mode,modeKey]=f}catch(e){}
|
|
1083
|
-
load().then(deep);listen()})();
|
|
1398
|
+
load().then(()=>{go(v0);deep()});listen()})();
|
|
1084
1399
|
</script>
|
|
1085
1400
|
</body>
|
|
1086
1401
|
</html>
|