kaizen-agent 1.24.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 +4 -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 +339 -101
- package/web/notebook.js +1 -1
- package/web/notes.html +14 -11
- package/web/term.css +285 -0
- package/web/term.js +39 -0
- package/web/term.ts +46 -0
package/web/board.html
CHANGED
|
@@ -10,17 +10,17 @@
|
|
|
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
22
|
/* side menu: the same lines in board.html and notes.html (a test holds them equal) */
|
|
23
|
-
.rail{width:220px;flex:none;
|
|
23
|
+
.rail{width:220px;flex:none;display:flex;flex-direction:column;gap:2px;padding:18px 12px 14px;overflow:hidden auto;transition:width .18s}
|
|
24
24
|
.rail .top{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 0 18px 10px}
|
|
25
25
|
.rail .home{display:block;color:inherit;text-decoration:none}
|
|
26
26
|
.rail .top .mascot{display:none;width:36px;height:34px}
|
|
@@ -44,12 +44,14 @@ svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:1.6;stroke
|
|
|
44
44
|
[data-rail=min] .theme button{width:34px;flex:none}
|
|
45
45
|
@media(max-width:640px){#fold{display:none}}
|
|
46
46
|
/* end side menu */
|
|
47
|
-
|
|
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}}
|
|
48
50
|
/* header */
|
|
49
|
-
.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}
|
|
50
52
|
pre.mark{margin:0;font:700 8px/1 var(--mono)}
|
|
51
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)}
|
|
52
|
-
.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}
|
|
53
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}
|
|
54
56
|
.sel:hover{border-color:var(--dim)}
|
|
55
57
|
.agents{display:flex;flex-wrap:wrap;gap:6px}
|
|
@@ -57,7 +59,8 @@ select.sel,button.sel{cursor:pointer;appearance:none;padding-right:30px;max-widt
|
|
|
57
59
|
.agent svg{width:14px;height:14px}.agent:hover{border-color:var(--dim)}
|
|
58
60
|
.yolo{display:inline-flex;align-items:center;gap:8px;margin-right:auto;font-size:13px;color:var(--ink);cursor:pointer;white-space:nowrap}
|
|
59
61
|
.agent[aria-checked=true]{border-color:var(--accent);color:var(--ink);background:color-mix(in srgb,var(--accent) 14%,var(--bg-2))}
|
|
60
|
-
.hdr h1{margin:0;font-size:
|
|
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}
|
|
61
64
|
.hdr .sel{max-width:min(320px,100%);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}
|
|
62
65
|
/* project picker: a select-shaped button over a searchable list */
|
|
63
66
|
.combo{position:relative;max-width:100%;display:flex}
|
|
@@ -81,39 +84,39 @@ select.sel,button.sel{cursor:pointer;appearance:none;padding-right:30px;max-widt
|
|
|
81
84
|
/* the mic button turns into a level meter while listening: five bars, each weighted so the set reads as a wave */
|
|
82
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}
|
|
83
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}
|
|
84
|
-
.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}
|
|
85
88
|
.btn:hover{border-color:var(--dim)}
|
|
86
|
-
.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))}
|
|
87
90
|
.btn.warn{border-color:var(--warn);color:var(--warn)}.btn.warn:hover{background:var(--warn);color:var(--bg)}
|
|
88
91
|
.btn.danger{color:var(--danger)}.btn.danger:hover{border-color:var(--danger)}
|
|
89
92
|
.btn.ghost{background:none;border-color:transparent;color:var(--dim);box-shadow:none}.btn.ghost:hover{color:var(--ink);background:var(--hover)}
|
|
90
93
|
|
|
91
94
|
/* summary strip: each count is a pill that filters the board */
|
|
92
95
|
.strip{display:inline-flex;gap:8px;flex-wrap:wrap;align-items:center;font-size:13px;color:var(--dim)}
|
|
93
|
-
.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}
|
|
94
97
|
.strip button:hover,.strip button.on{color:var(--ink);background:var(--hover)}
|
|
95
98
|
.strip button.attn{color:var(--warn);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--warn) 45%,transparent)}
|
|
96
99
|
.strip svg{width:14px;height:14px;stroke-width:1.8}
|
|
97
100
|
|
|
98
101
|
/* board */
|
|
99
|
-
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}
|
|
100
103
|
section h2::before{content:"";display:block;height:16px;margin-top:-16px}
|
|
101
|
-
section{padding-top:
|
|
102
|
-
section h2{margin:0;font-size:13px;color:var(--ink);font-weight:
|
|
103
|
-
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)}
|
|
104
107
|
section h2 i:empty{display:none}
|
|
105
108
|
section h2 .ib{margin-left:auto}
|
|
106
|
-
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}
|
|
107
110
|
|
|
108
111
|
/* card, Linear grammar in our palette
|
|
109
112
|
line 1 status icon · id (dim) · ⋯ menu on hover
|
|
110
113
|
line 2 title, medium weight, wraps to 2 lines
|
|
111
114
|
line 3 chips, only when set
|
|
112
115
|
Card ground is --bg-2 on a --bg column, lifted by --shadow-1. Actions never sit on the card. */
|
|
113
|
-
.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:
|
|
114
|
-
.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)}
|
|
115
118
|
.card.menu{z-index:2}
|
|
116
|
-
.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)}
|
|
117
120
|
.card.archived .c-title,.card.archived .c-id{opacity:.7}
|
|
118
121
|
section h2 .ord{min-height:22px;padding:0 8px;font-weight:500}
|
|
119
122
|
section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
@@ -125,7 +128,7 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
125
128
|
.card .pick{position:absolute;right:14px;top:14px;margin:0;accent-color:var(--accent)}
|
|
126
129
|
.card.picked{border-color:var(--accent)}
|
|
127
130
|
.card:has(.pick) .age,.card:has(.pick) .more{display:none}
|
|
128
|
-
.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}
|
|
129
132
|
.c-id .st{width:14px;height:14px;display:grid;place-items:center;flex-shrink:0}
|
|
130
133
|
.c-id .st svg{width:14px;height:14px;stroke-width:1.8}
|
|
131
134
|
.c-id .idt{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-variant-numeric:tabular-nums}
|
|
@@ -133,16 +136,16 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
133
136
|
.c-id .more{display:none;margin:-4px -6px -4px 0}
|
|
134
137
|
.card:hover .more,.card:focus-within .more,.card.menu .more{display:grid}
|
|
135
138
|
.card:hover .age,.card.menu .age{display:none}
|
|
136
|
-
.c-title{font-size:
|
|
137
|
-
.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}
|
|
138
141
|
.c-chips:empty{display:none}
|
|
139
|
-
.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)}
|
|
140
143
|
.chip i{width:6px;height:6px;border-radius:50%;background:var(--dim)}
|
|
141
144
|
.chip svg{width:11px;height:11px;stroke-width:1.7}
|
|
142
|
-
.chip.warn{color:var(--warn
|
|
143
|
-
.chip.accent{
|
|
144
|
-
.chip.ok{color:
|
|
145
|
-
.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)}
|
|
146
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)}
|
|
147
150
|
.d-run{color:var(--run)}.d-wait{color:var(--warn)}.d-done{color:var(--ok)}
|
|
148
151
|
.pulse{display:inline-block;width:7px;height:7px;border-radius:50%;background:currentColor;vertical-align:middle}
|
|
@@ -155,11 +158,11 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
155
158
|
.menu-pop button.danger{color:var(--danger)}
|
|
156
159
|
.menu-pop svg{width:15px;height:15px;flex-shrink:0}
|
|
157
160
|
.menu-pop hr{border:0;border-top:1px solid var(--line);margin:4px 0}
|
|
158
|
-
.more-btn{width:100%;padding:
|
|
159
|
-
.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)}
|
|
160
163
|
|
|
161
164
|
/* inline forms replace a card while editing */
|
|
162
|
-
.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)}
|
|
163
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)}
|
|
164
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)}
|
|
165
168
|
.inl textarea,.notes textarea{min-height:72px;resize:vertical}
|
|
@@ -168,6 +171,8 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
168
171
|
.inl .row,.notes .row{display:flex;gap:6px;justify-content:flex-end}
|
|
169
172
|
.notes{display:flex;flex-direction:column;gap:8px}
|
|
170
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}
|
|
171
176
|
.desc{border-bottom:1px solid var(--line);padding-bottom:6px;margin-bottom:14px}.desc h3{margin-top:14px}
|
|
172
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}
|
|
173
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)}
|
|
@@ -181,23 +186,23 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
181
186
|
@keyframes fi{from{opacity:0}}
|
|
182
187
|
|
|
183
188
|
/* footer */
|
|
184
|
-
.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}
|
|
185
190
|
.ftr span+span,.ftr a+a{margin-left:14px}.ftr .strip{margin-right:14px;vertical-align:middle;font-size:12px}
|
|
186
191
|
/* The footer is out of the eye's path, so the waiting alert is filled, not just outlined. */
|
|
187
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}
|
|
188
|
-
.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)}
|
|
189
194
|
|
|
190
195
|
/* side panel */
|
|
191
196
|
.scrim{position:fixed;inset:0;z-index:10;background:color-mix(in srgb,var(--bg) 60%,transparent);backdrop-filter:blur(3px);display:none}
|
|
192
197
|
.scrim.on{display:block}
|
|
193
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}
|
|
194
199
|
aside.open{transform:none;visibility:visible}
|
|
195
|
-
/* Settings and
|
|
200
|
+
/* Settings and the Icebox are pages: the same panel, docked into the app column, its content in a centred column */
|
|
196
201
|
body.paged #board,body.paged .ftr,body.paged .hdr,body.paged .scrim{display:none}
|
|
197
|
-
aside.docked{position:static;flex:1;min-height:0;width:auto;transform:none;visibility:visible;transition:none;box-shadow:none;border:0}
|
|
198
|
-
aside.docked
|
|
199
|
-
aside.docked
|
|
200
|
-
aside.docked
|
|
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}
|
|
201
206
|
aside.docked #pclose{display:none}
|
|
202
207
|
aside header{display:flex;align-items:flex-start;gap:12px;padding:20px 24px 16px;border-bottom:1px solid var(--line)}
|
|
203
208
|
aside header .t{flex:1;min-width:0}
|
|
@@ -275,14 +280,45 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
275
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}
|
|
276
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)}
|
|
277
282
|
/* request sources: cards, status badges, top bar, and add-box */
|
|
278
|
-
|
|
279
|
-
.
|
|
280
|
-
.
|
|
281
|
-
.
|
|
282
|
-
.
|
|
283
|
-
.
|
|
284
|
-
.
|
|
285
|
-
.
|
|
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}}
|
|
286
322
|
.src-list{display:flex;flex-direction:column;gap:10px}
|
|
287
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}
|
|
288
324
|
.src-card:hover{border-color:color-mix(in srgb,var(--line) 70%,var(--ink))}
|
|
@@ -290,6 +326,9 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
290
326
|
.src-info{display:flex;align-items:center;gap:8px;min-width:0}
|
|
291
327
|
.src-ico{width:20px;height:20px;flex-shrink:0;color:var(--accent);display:inline-flex;align-items:center;justify-content:center}
|
|
292
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}
|
|
293
332
|
.src-title{font-size:14px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
294
333
|
.src-acts{display:flex;align-items:center;gap:4px;flex-shrink:0}
|
|
295
334
|
.src-acts .btn{display:inline-flex;align-items:center;gap:5px}
|
|
@@ -306,8 +345,18 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
306
345
|
.src-badge.dim{background:color-mix(in srgb,var(--dim) 14%,transparent);color:var(--dim)}
|
|
307
346
|
.src-badge svg{width:12px;height:12px;stroke-width:2;flex-shrink:0}
|
|
308
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)}
|
|
309
|
-
.src-dest .sel{flex:1;max-width:240px;height:28px;font-size:12px;padding:2px 8px}
|
|
310
|
-
.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}
|
|
311
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}
|
|
312
361
|
.src-empty-ico{width:36px;height:36px;margin:0 auto 10px;color:var(--dim);display:flex;align-items:center;justify-content:center}
|
|
313
362
|
.src-empty-ico svg{width:32px;height:32px}
|
|
@@ -316,10 +365,23 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
316
365
|
.src-empty-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;margin-top:12px;text-align:left}
|
|
317
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}
|
|
318
367
|
.src-empty-item strong{display:block;color:var(--ink);font-weight:500;margin-bottom:2px}
|
|
319
|
-
.src-add-
|
|
320
|
-
.src-add
|
|
321
|
-
.src-add
|
|
322
|
-
|
|
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)}}
|
|
323
385
|
@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}
|
|
324
386
|
</style>
|
|
325
387
|
</head>
|
|
@@ -328,6 +390,7 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
328
390
|
<svg style="display:none"><defs>
|
|
329
391
|
<symbol id="i-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
|
|
330
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>
|
|
331
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>
|
|
332
395
|
<symbol id="i-play" viewBox="0 0 24 24"><path d="M6 4l14 8-14 8z"/></symbol>
|
|
333
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>
|
|
@@ -340,6 +403,9 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
340
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>
|
|
341
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>
|
|
342
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>
|
|
343
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>
|
|
344
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>
|
|
345
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>
|
|
@@ -373,7 +439,7 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
373
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>
|
|
374
440
|
<a class="it" href="/" data-view="board" title="Board"><svg><use href="#i-board"/></svg><span class="lbl">Board</span></a>
|
|
375
441
|
<a class="it" href="/notebook" data-view="notes" title="Notes"><svg><use href="#i-note"/></svg><span class="lbl">Notes</span></a>
|
|
376
|
-
<a class="it" href="/?view=
|
|
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>
|
|
377
443
|
<a class="it" href="/?view=settings" data-view="settings" title="Settings"><svg><use href="#i-gear"/></svg><span class="lbl">Settings</span></a>
|
|
378
444
|
<span class="end"></span>
|
|
379
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>
|
|
@@ -540,8 +606,10 @@ async function load(quiet){
|
|
|
540
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)}
|
|
541
607
|
// A redraw under a form being typed in would reset its fields and its focus.
|
|
542
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}}
|
|
543
611
|
if(openKey){const c=S.cards.find(x=>key(x)===openKey),ta=$("pbody").querySelector(".notes textarea");if(ta&&document.activeElement===ta)return;
|
|
544
|
-
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)}}
|
|
545
613
|
// An idea's card goes when its run starts. Follow it to that run rather than keep
|
|
546
614
|
// offering "Run again": the newest run in its project whose request holds the idea's
|
|
547
615
|
// text, the rule boardCards() retires it by. No such run: close the panel.
|
|
@@ -563,10 +631,12 @@ function card(c){
|
|
|
563
631
|
const chips=[];
|
|
564
632
|
if(c.status==="waiting")chips.push(`<span class="chip warn"><i></i>${esc(need(c.awaiting)||"needs you")}</span>`);
|
|
565
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>`);
|
|
566
635
|
if(all)chips.push(`<span class="chip">${esc(c.where)}</span>`);
|
|
567
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>`}));
|
|
568
|
-
// A gathered idea's notes open with the stamp
|
|
569
|
-
|
|
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>`);
|
|
570
640
|
// Information only: the panel's button is where it is committed and pushed.
|
|
571
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>`)}
|
|
572
642
|
const pick=picking&&!archive&&c.column===4;
|
|
@@ -641,7 +711,7 @@ async function startRun(c,kind){
|
|
|
641
711
|
const p=runPick&&runPick.k===key(c)?runPick:null;runPick=null;
|
|
642
712
|
const yolo=!!(p&&p.yolo&&!$("run-yolo")?.hidden);
|
|
643
713
|
const r=await post("/run",{dir:c.state,text:c.text,kind,agent:p?p.agent:undefined,yolo});
|
|
644
|
-
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)}
|
|
645
715
|
load()}
|
|
646
716
|
|
|
647
717
|
function render(){
|
|
@@ -725,10 +795,12 @@ function open(c,keepTab){
|
|
|
725
795
|
if(view!=="board")go("board",true);
|
|
726
796
|
stopDictation(true); // the panel body is about to be replaced
|
|
727
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();
|
|
728
|
-
if(c.kind==="idea"){runDoc=null;$("psub").textContent="Idea · "+c.where;$("ptabs").innerHTML="";
|
|
729
|
-
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>`;
|
|
730
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}
|
|
731
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;
|
|
732
804
|
openRun(c,false)}
|
|
733
805
|
// Notes are a chat: one entry per message, files attached by paste or picker.
|
|
734
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)}
|
|
@@ -787,19 +859,42 @@ function showProgress(c){
|
|
|
787
859
|
// again: something changed on the server, so the doc is read afresh: a note sent, a plan
|
|
788
860
|
// written or a commit made elsewhere shows in nothing the page already holds. One /run
|
|
789
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")}}
|
|
790
875
|
async function openRun(c,refresh,again){
|
|
791
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());
|
|
792
877
|
// A file list being confirmed, or a push under way, keeps the doc it was drawn from.
|
|
793
878
|
if(refresh&&$("pfoot").querySelector(".inl,[data-busy],.more[open]"))return;runDoc=d}
|
|
794
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;
|
|
795
883
|
// A refresh while notes are being typed would wipe the textarea.
|
|
796
|
-
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;
|
|
797
885
|
$("psub").textContent=c.id+" · "+(WORD[c.status]||c.status)+(c.awaiting?" · "+need(c.awaiting):"");
|
|
798
|
-
|
|
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");
|
|
799
892
|
$("ptabs").innerHTML=tabs.map((t,i)=>`<button class="${i===tab?"on":""}" data-i="${i}">${t[0]}</button>`).join("");
|
|
800
893
|
[...$("ptabs").children].forEach(b=>b.onclick=()=>{tab=+b.dataset.i;openRun(c,true)});
|
|
801
894
|
const k=tabs[tab][1],doc=k==="preview"?(preview(runDoc.impl)&&attach(preview(runDoc.impl),c)):runDoc[k];
|
|
802
|
-
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";
|
|
803
898
|
chat(c,doc,ro);}
|
|
804
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>`;
|
|
805
900
|
if(k==="backlog")showProgress(c);
|
|
@@ -820,21 +915,29 @@ async function openRun(c,refresh,again){
|
|
|
820
915
|
// gitBtn's buttons restyled as menu rows.
|
|
821
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>`;
|
|
822
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>`}
|
|
823
|
-
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>`;
|
|
824
919
|
else if(c.status==="done"||c.status==="abandoned")foot.innerHTML=arch;
|
|
825
|
-
else foot.innerHTML=`<span class="note">${c.status==="stalled"?"No progress for a while. Resume it
|
|
826
|
-
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()})}
|
|
827
922
|
// Answering an approval from the panel opens your agent on the run, told the decision and,
|
|
828
923
|
// for a revise, the comment. Answered approvals are remembered outside the footer, which
|
|
829
924
|
// every refresh rebuilds, so a second press cannot open a second agent. Keyed by when the
|
|
830
925
|
// run last moved: the same approval reached again after a revise is a new question.
|
|
831
926
|
// The footer's More menu is a <details>: close it on a click anywhere else.
|
|
832
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)}}
|
|
833
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;
|
|
834
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}:{})});
|
|
835
937
|
// A failed send keeps the footer, and with it a typed comment.
|
|
836
938
|
if(!r.ok){b.disabled=false;return}
|
|
837
|
-
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);
|
|
838
941
|
if(openKey===key(c)){$("pfoot").innerHTML="";openRun(c,true)}}
|
|
839
942
|
function confirmRevise(c){const foot=$("pfoot"),plan=c.awaiting==="approvals.plan";
|
|
840
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>`;
|
|
@@ -883,7 +986,7 @@ function fixForm(c){const k=n=>c.id+":"+n,sent=f=>fixSent.has(k(f.n)),fs=runDoc.
|
|
|
883
986
|
allb.onclick=()=>{const none=picks().length===open.length;open.forEach(f=>none?fixOff.add(k(f.n)):fixOff.delete(k(f.n)));sync()};
|
|
884
987
|
go.onclick=async()=>{const nums=picks();go.disabled=true;askNotify();const r=await post("/fix",{dir:c.state,id:c.id,nums});
|
|
885
988
|
// Sent findings lock as sent, so a second press cannot open a second agent on them.
|
|
886
|
-
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()};
|
|
887
990
|
sync();$("pbody").appendChild(el)}
|
|
888
991
|
// Commit and push, for a finished run in a project with a git remote. What the board
|
|
889
992
|
// did is recorded per run, so the settled label outlives later changes to the tree.
|
|
@@ -910,7 +1013,7 @@ async function commitPush(c,message,b){b.disabled=true;b.dataset.busy=1;b.textCo
|
|
|
910
1013
|
// A refused message comes back for another try, beside the current file list.
|
|
911
1014
|
if(!r.ok&&message&&openKey===key(c)&&runDoc.git.files&&runDoc.git.files.length)confirmCommit(c,message)}
|
|
912
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()}
|
|
913
|
-
function close(){if(view!=="board")return;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})}
|
|
914
1017
|
$("pclose").onclick=close;$("scrim").onclick=close;
|
|
915
1018
|
// An idea is one inbox line, so a structured request arrives as one wall:
|
|
916
1019
|
// "Fix x Problem: … Scope: … Acceptance criteria: (1) … (2) …". Split it, for display
|
|
@@ -960,7 +1063,7 @@ const groups=rows=>{const m=new Map();for(const r of rows){if(!m.has(r.group))m.
|
|
|
960
1063
|
let setQ="";
|
|
961
1064
|
async function openSettings(){
|
|
962
1065
|
stopDictation(true);
|
|
963
|
-
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="";
|
|
964
1067
|
const cur=document.documentElement.dataset.theme||"system";
|
|
965
1068
|
const cfg=await fetch("/settings").then(r=>r.json()).catch(()=>null);
|
|
966
1069
|
$("psub").textContent="Theme applies to this browser. Everything else is saved to your kaizen settings.";$("psub").title=cfg?cfg.file:"";
|
|
@@ -1001,11 +1104,11 @@ async function openSettings(){
|
|
|
1001
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);
|
|
1002
1105
|
// The system's own folder dialog, opened by the board: a page cannot read a real path.
|
|
1003
1106
|
if(r.folder)$(el.id+"-pick").onclick=async()=>{const j=await post("/pick-folder",{});if(j.path){await save(j.path);load()}}})}
|
|
1004
|
-
// ---- views: the board, or Settings and
|
|
1107
|
+
// ---- views: the board, or Settings and the Icebox as pages in the docked panel, each with its own address
|
|
1005
1108
|
let view="board";
|
|
1006
1109
|
const markView=v=>document.querySelectorAll(".rail a.it").forEach(a=>a.dataset.view===v?a.setAttribute("aria-current","page"):a.removeAttribute("aria-current"));
|
|
1007
|
-
function go(v,push){if(!["settings","sources"].includes(v))v="board";const was=view;view=v;markView(v);document.body.classList.toggle("paged",v!=="board");
|
|
1008
|
-
if(push&&v!==was)history.pushState(null,"",v==="board"?location.pathname:"?view="+v);
|
|
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));
|
|
1009
1112
|
const p=$("panel");
|
|
1010
1113
|
if(v==="board"){if(was==="board")return;
|
|
1011
1114
|
// Undocked without the slide: it was a page, not a drawer leaving.
|
|
@@ -1016,63 +1119,196 @@ document.querySelector(".rail .home").onclick=e=>{if(e.ctrlKey||e.metaKey||e.shi
|
|
|
1016
1119
|
addEventListener("popstate",()=>go(new URLSearchParams(location.search).get("view")));
|
|
1017
1120
|
// The menu's theme switch while Settings shows: its Theme row follows.
|
|
1018
1121
|
addEventListener("kz-theme",()=>{if(view==="settings")openSettings()});
|
|
1019
|
-
// ----
|
|
1020
|
-
|
|
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"};
|
|
1021
1134
|
const srcOpen=()=>$("panel").classList.contains("open")&&$("ptitle").textContent===SRC;
|
|
1022
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"};
|
|
1023
1136
|
async function openSources(){
|
|
1024
1137
|
stopDictation(true);
|
|
1025
1138
|
// What is half typed survives a refresh from the server.
|
|
1026
|
-
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||""]:["",""];
|
|
1027
1140
|
// So does a project picked on a row but not yet moved.
|
|
1028
1141
|
const picked=new Map(srcOpen()?[...$("pbody").querySelectorAll("[data-mv]")].map(m=>[m.dataset.k,m.value]):[]);
|
|
1029
|
-
|
|
1030
|
-
|
|
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="";
|
|
1031
1150
|
const dir=all?null:S.dir,q=dir?"?dir="+encodeURIComponent(dir):"";
|
|
1032
1151
|
const d=await fetch("/sources"+q).then(r=>r.json()).catch(()=>null);
|
|
1033
1152
|
if(!srcOpen())return;
|
|
1034
1153
|
const host=u=>{try{return new URL(u).hostname}catch{return u}};
|
|
1035
|
-
|
|
1036
|
-
|
|
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||[];
|
|
1037
1162
|
// A source belongs to one project, named on its row. Picking another only arms Move: a select
|
|
1038
1163
|
// changes on every arrow key, and each change would have moved the source. The names are the picker's;
|
|
1039
1164
|
// a row whose project the picker does not list still gets its own, selected, so Move never points elsewhere.
|
|
1040
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("");
|
|
1041
|
-
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>`;
|
|
1042
1166
|
const cards=list.map((s,i)=>{
|
|
1043
|
-
const st=srcType(s
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}).
|
|
1047
|
-
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
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]}
|
|
1051
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())});
|
|
1052
1232
|
const act=async(body,done)=>{const j=await post("/sources",body);if(j.ok){done(j);load()}if(srcOpen())openSources()};
|
|
1053
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);
|
|
1054
1234
|
if(to&&[...m.options].some(o=>o.value===to))m.value=to;
|
|
1055
1235
|
(m.onchange=()=>{b.disabled=m.value===s.dir})();
|
|
1056
|
-
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};
|
|
1057
1283
|
$("pbody").querySelectorAll("[data-chk]").forEach(b=>{const s=list[+b.dataset.chk];
|
|
1058
1284
|
b.onclick=()=>{b.disabled=true;b.innerHTML=ico("refresh")+" Checking…";
|
|
1059
1285
|
act({op:"gather",dir:s.dir,url:s.url},j=>{const r=j.results&&j.results[0];
|
|
1060
|
-
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")})}});
|
|
1061
1287
|
$("pbody").querySelectorAll("[data-rm]").forEach(b=>{const s=list[+b.dataset.rm];
|
|
1062
|
-
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;
|
|
1063
1289
|
act({op:"remove",dir:list[+b.dataset.rm].dir,url:list[+b.dataset.rm].url},()=>toast("Source removed"))}});
|
|
1064
|
-
|
|
1065
|
-
|
|
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}
|
|
1066
1299
|
$("src-add").disabled=true;$("src-add").textContent="Adding…";
|
|
1067
|
-
(async()=>{const
|
|
1068
|
-
|
|
1069
|
-
|
|
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()}
|
|
1070
1305
|
else toast(j.error||"Could not add source");
|
|
1071
1306
|
if(srcOpen())openSources()})()};
|
|
1072
|
-
$("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
|
+
}
|
|
1073
1309
|
if($("src-go"))$("src-go").onclick=()=>{$("src-go").disabled=true;$("src-go").innerHTML=ico("refresh")+" Gathering…";
|
|
1074
|
-
act({op:"gather",dir:dir||undefined},j=>{const
|
|
1075
|
-
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`:""))})}}
|
|
1076
1312
|
// With `report`, the toast carries a link to a prefilled GitHub bug form (/issue) and
|
|
1077
1313
|
// stays long enough to click it.
|
|
1078
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)}
|
|
@@ -1128,6 +1364,8 @@ $("newtop").onclick=()=>{mode="new";render();$("board").scrollTo(0,0)};
|
|
|
1128
1364
|
$("kbd").onclick=()=>$("help").classList.add("on");$("help").onclick=()=>$("help").classList.remove("on");
|
|
1129
1365
|
document.addEventListener("click",()=>{if(menuKey||clearMenu){menuKey=null;clearMenu=false;render()}});
|
|
1130
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;
|
|
1131
1369
|
if(e.target.matches("input,select,textarea,[contenteditable]"))return;
|
|
1132
1370
|
if(e.key==="?"){$("help").classList.toggle("on");return}
|
|
1133
1371
|
if(e.key==="/"&&$("sq")&&$("panel").classList.contains("open")){e.preventDefault();$("sq").focus();return}
|
|
@@ -1149,11 +1387,11 @@ document.onkeydown=e=>{
|
|
|
1149
1387
|
let boot=null;
|
|
1150
1388
|
// Back from a terminal: what was committed or pushed there touched nothing the server watches.
|
|
1151
1389
|
addEventListener("focus",()=>load(true));
|
|
1152
|
-
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)}}
|
|
1153
1391
|
// Back to the project picked last time, unless the URL says which view it wants. A
|
|
1154
1392
|
// plain fetch: a project that is gone should fall back to all, not raise a toast.
|
|
1155
1393
|
// A page named in the address shows from the first paint; it is filled once the board has loaded.
|
|
1156
|
-
const v0=new URLSearchParams(location.search).get("view");markView(["settings","sources"].includes(v0)?v0:"board");document.body.classList.toggle("paged",["settings","sources"].includes(v0));
|
|
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));
|
|
1157
1395
|
(async()=>{let d;try{d=localStorage.getItem("kz-proj")}catch(e){}
|
|
1158
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){}
|
|
1159
1397
|
try{const f=JSON.parse(sessionStorage.getItem("kz-form"));if(f)[mode,modeKey]=f}catch(e){}
|