kaizen-agent 1.24.0 → 1.26.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/chatgpt.test.ts +44 -0
- package/cli/chatgpt.ts +175 -0
- 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 +5 -1
- package/cli/sources.test.ts +170 -25
- package/cli/sources.ts +153 -29
- package/cli/state.ts +33 -6
- package/cli/web.ts +175 -21
- 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 +17 -0
- package/skills/kaizen/discover.md +50 -0
- package/skills/kaizen/gather.md +22 -7
- package/skills/kaizen/settings.md +4 -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 +358 -103
- package/web/notebook.js +1 -1
- package/web/notes.html +16 -14
- 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,25 @@ 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
|
|
196
|
-
body.paged #board,body.paged .ftr,body.paged .
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
aside.docked
|
|
200
|
-
aside.docked
|
|
200
|
+
/* Settings and the Icebox are pages: the same panel, docked into the app column, its content in a centred column */
|
|
201
|
+
body.paged #board,body.paged .ftr,body.paged .scrim{display:none}
|
|
202
|
+
/* The Icebox keeps the project bar, without the board's title and New idea; Settings has none */
|
|
203
|
+
body.paged:not(.ice) .hdr,body.ice .hdr h1{display:none}body.ice #newtop{visibility:hidden}
|
|
204
|
+
aside.docked{position:static;flex:1;min-height:0;width:auto;transform:none;visibility:visible;transition:none;box-shadow:none;border:0;background:none}
|
|
205
|
+
aside.docked>header,aside.docked>.body{padding-inline:max(28px,calc((100% - 760px)/2))}
|
|
206
|
+
aside.docked>header{border-bottom:0;padding-top:28px;padding-bottom:12px}
|
|
207
|
+
aside.docked>header b{font-size:20px}
|
|
201
208
|
aside.docked #pclose{display:none}
|
|
202
209
|
aside header{display:flex;align-items:flex-start;gap:12px;padding:20px 24px 16px;border-bottom:1px solid var(--line)}
|
|
203
210
|
aside header .t{flex:1;min-width:0}
|
|
@@ -275,14 +282,48 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
275
282
|
.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
283
|
.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
284
|
/* request sources: cards, status badges, top bar, and add-box */
|
|
278
|
-
|
|
279
|
-
.
|
|
280
|
-
.
|
|
281
|
-
.
|
|
282
|
-
.
|
|
283
|
-
.
|
|
284
|
-
.
|
|
285
|
-
.
|
|
285
|
+
/* the icebox as an inbox: toolbar, filter chips, one row per request, bulk bar */
|
|
286
|
+
.ice-bar{display:flex;align-items:center;gap:8px;margin:8px 0 8px}
|
|
287
|
+
.cg-onb{border:1px solid color-mix(in srgb,var(--accent) 45%,var(--line));background:color-mix(in srgb,var(--accent) 8%,var(--bg-2));border-radius:var(--r);padding:14px 16px;margin:4px 0 12px}
|
|
288
|
+
.cg-onb b{font-size:14px;color:var(--ink-hi)}.cg-onb p{margin:6px 0 12px;font-size:13px;color:var(--dim);line-height:1.5}
|
|
289
|
+
.cg-onb .acts{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap}
|
|
290
|
+
.ice-bar .count{font-size:13px;font-weight:600;color:var(--ink)}
|
|
291
|
+
.ice-bar .end{flex:1}
|
|
292
|
+
.ice-bar .btn,.ice-bulk .btn,.ice-ra .btn{display:inline-flex;align-items:center;gap:6px}
|
|
293
|
+
.ice-bar .btn svg,.ice-bulk .btn svg,.ice-ra .btn svg{width:14px;height:14px}
|
|
294
|
+
.ice-bar input,.ice-row input,.ice-bulk input{accent-color:var(--accent);width:15px;height:15px;margin:0;flex-shrink:0}
|
|
295
|
+
.ice-bar .ice-sel{width:auto;max-width:170px;height:28px;font-size:12px;padding:2px 26px 2px 8px}
|
|
296
|
+
@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%}}
|
|
297
|
+
.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)}
|
|
298
|
+
.ice-bulk .end{flex:1}
|
|
299
|
+
.ice-list{display:flex;flex-direction:column;gap:1px}
|
|
300
|
+
.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}
|
|
301
|
+
.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)}
|
|
302
|
+
.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}
|
|
303
|
+
.ice-row:hover,.ice-row.cur{background:var(--hover)}
|
|
304
|
+
.ice-row.cur{box-shadow:inset 0 0 0 1px var(--line)}
|
|
305
|
+
.ice-row.sel{background:color-mix(in srgb,var(--accent) 8%,transparent)}
|
|
306
|
+
.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}
|
|
307
|
+
.ice-t[aria-expanded=true]{font-weight:500}
|
|
308
|
+
.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}
|
|
309
|
+
.ice-age{font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums;white-space:nowrap;min-width:64px;text-align:right}
|
|
310
|
+
.ice-ra{display:none;gap:2px;min-width:64px;justify-content:flex-end}
|
|
311
|
+
.ice-ra .btn{padding:0 6px}
|
|
312
|
+
.ice-row:hover .ice-age,.ice-row.cur .ice-age,.ice-row:focus-within .ice-age{display:none}
|
|
313
|
+
.ice-row:hover .ice-ra,.ice-row.cur .ice-ra,.ice-row:focus-within .ice-ra{display:flex}
|
|
314
|
+
.ice-open{display:grid;gap:8px;padding:6px 12px 12px 37px}
|
|
315
|
+
.ice-zero{padding:40px 16px;text-align:center;border:1px solid var(--line);border-radius:var(--r);background:var(--bg-2)}
|
|
316
|
+
.ice-zero h4{margin:0 0 6px;font-size:14px;font-weight:600;color:var(--ink)}
|
|
317
|
+
.ice-zero p{margin:0;font-size:12px;color:var(--dim);line-height:1.5}
|
|
318
|
+
.ice-open .hint{font-size:12px;color:var(--dim)}
|
|
319
|
+
/* the sources: the run panel's drawer (aside rules), sliding over the icebox page */
|
|
320
|
+
body:not(.paged) .src-sec,body:not(.paged) .src-scrim{display:none}
|
|
321
|
+
.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}
|
|
322
|
+
.src-scrim.on{opacity:1;visibility:visible}
|
|
323
|
+
.src-sec .sub a{color:var(--accent);text-decoration:none;cursor:pointer}
|
|
324
|
+
.src-sec .sub a:hover{text-decoration:underline}
|
|
325
|
+
@media(hover:none){.ice-ra{display:flex}.ice-age{display:none}}
|
|
326
|
+
@media(max-width:640px){.ice-src .nm{display:none}.ice-open{padding-left:12px}}
|
|
286
327
|
.src-list{display:flex;flex-direction:column;gap:10px}
|
|
287
328
|
.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
329
|
.src-card:hover{border-color:color-mix(in srgb,var(--line) 70%,var(--ink))}
|
|
@@ -290,6 +331,9 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
290
331
|
.src-info{display:flex;align-items:center;gap:8px;min-width:0}
|
|
291
332
|
.src-ico{width:20px;height:20px;flex-shrink:0;color:var(--accent);display:inline-flex;align-items:center;justify-content:center}
|
|
292
333
|
.src-ico svg{width:18px;height:18px}
|
|
334
|
+
/* a site's own favicon; the generic icon after it shows only once the image fails and removes itself */
|
|
335
|
+
.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}
|
|
336
|
+
.ice-src .fav svg{width:14px;height:14px}
|
|
293
337
|
.src-title{font-size:14px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
294
338
|
.src-acts{display:flex;align-items:center;gap:4px;flex-shrink:0}
|
|
295
339
|
.src-acts .btn{display:inline-flex;align-items:center;gap:5px}
|
|
@@ -306,8 +350,18 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
306
350
|
.src-badge.dim{background:color-mix(in srgb,var(--dim) 14%,transparent);color:var(--dim)}
|
|
307
351
|
.src-badge svg{width:12px;height:12px;stroke-width:2;flex-shrink:0}
|
|
308
352
|
.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}
|
|
353
|
+
.src-dest .sel{flex:1;min-width:0;max-width:240px;height:28px;font-size:12px;padding:2px 8px}
|
|
354
|
+
.src-dest .btn{height:28px;padding:0 10px;font-size:12px;display:inline-flex;align-items:center;gap:5px}
|
|
355
|
+
.src-dest .btn svg{width:14px;height:14px;flex-shrink:0}
|
|
356
|
+
.ice-hd{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin:22px 0 4px}
|
|
357
|
+
.ice-hd h3{margin:0;font-size:14px;font-weight:600;color:var(--ink)}
|
|
358
|
+
.ice-cat{font-size:12px;font-weight:600;color:var(--dim);margin:14px 0 6px}
|
|
359
|
+
.ice-form{display:grid;gap:6px;padding-top:8px;border-top:1px solid var(--line)}
|
|
360
|
+
.ice-form .sel{width:100%;max-width:none}
|
|
361
|
+
.ice-form textarea.sel{height:auto;min-height:64px;font:inherit;font-size:13px;resize:vertical}
|
|
362
|
+
.ice-form-acts{display:flex;align-items:center;gap:8px}
|
|
363
|
+
.ice-form-acts .hint{flex:1;margin:0;font-size:11px}
|
|
364
|
+
.ice-notes{font-size:12px;color:var(--dim);line-height:1.45;overflow-wrap:anywhere;white-space:pre-wrap}
|
|
311
365
|
.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
366
|
.src-empty-ico{width:36px;height:36px;margin:0 auto 10px;color:var(--dim);display:flex;align-items:center;justify-content:center}
|
|
313
367
|
.src-empty-ico svg{width:32px;height:32px}
|
|
@@ -316,10 +370,23 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
316
370
|
.src-empty-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;margin-top:12px;text-align:left}
|
|
317
371
|
.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
372
|
.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
|
-
|
|
373
|
+
.src-add{display:grid;gap:12px;margin-top:20px;padding:16px;background:var(--bg);border:1px solid var(--line);border-radius:var(--r)}
|
|
374
|
+
.src-add h4{margin:0;font-size:13px;font-weight:600;color:var(--ink)}
|
|
375
|
+
.src-add .two{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
376
|
+
.src-add .two.plat{grid-template-columns:140px 1fr}
|
|
377
|
+
.src-add [hidden]{display:none}
|
|
378
|
+
.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}
|
|
379
|
+
.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}
|
|
380
|
+
.src-kind button[aria-pressed=true]{background:var(--bg-2);color:var(--ink);font-weight:500;box-shadow:var(--shadow-1)}
|
|
381
|
+
.src-add .hd{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
382
|
+
.src-add label{display:grid;gap:4px;font-size:12px;font-weight:500;color:var(--ink)}
|
|
383
|
+
.src-add label i{font-style:normal;font-weight:400;color:var(--dim)}
|
|
384
|
+
.src-add .sel{width:100%;max-width:none}
|
|
385
|
+
.src-add .acts{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
386
|
+
.src-add .acts .hint{margin:0;font-size:11px;color:var(--dim);line-height:1.4}
|
|
387
|
+
.src-add .acts .btn{flex-shrink:0}
|
|
388
|
+
@media(max-width:480px){.src-add .two,.src-add .two.plat{grid-template-columns:1fr}}
|
|
389
|
+
@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
390
|
@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}
|
|
324
391
|
</style>
|
|
325
392
|
</head>
|
|
@@ -328,6 +395,7 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
328
395
|
<svg style="display:none"><defs>
|
|
329
396
|
<symbol id="i-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
|
|
330
397
|
<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>
|
|
398
|
+
<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
399
|
<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
400
|
<symbol id="i-play" viewBox="0 0 24 24"><path d="M6 4l14 8-14 8z"/></symbol>
|
|
333
401
|
<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 +408,9 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
340
408
|
<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
409
|
<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
410
|
<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>
|
|
411
|
+
<!-- the social platforms' marks, from simple-icons (CC0): their own favicons refuse to load on another site -->
|
|
412
|
+
<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>
|
|
413
|
+
<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
414
|
<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
415
|
<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
416
|
<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 +444,7 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
373
444
|
<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
445
|
<a class="it" href="/" data-view="board" title="Board"><svg><use href="#i-board"/></svg><span class="lbl">Board</span></a>
|
|
375
446
|
<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=
|
|
447
|
+
<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
448
|
<a class="it" href="/?view=settings" data-view="settings" title="Settings"><svg><use href="#i-gear"/></svg><span class="lbl">Settings</span></a>
|
|
378
449
|
<span class="end"></span>
|
|
379
450
|
<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 +611,10 @@ async function load(quiet){
|
|
|
540
611
|
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
612
|
// A redraw under a form being typed in would reset its fields and its focus.
|
|
542
613
|
if(!(quiet&&mode&&$("board").querySelector(".inl input")))render();
|
|
614
|
+
// Run on an idea closed the panel: open the card on its agent once the board knows the session.
|
|
615
|
+
if(wantTerm&&!openKey&&!mode){const n=S.cards.find(x=>key(x)===wantTerm&&x.pty);if(n){wantTerm=null;open(n);return}}
|
|
543
616
|
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")
|
|
617
|
+
if(!c){if(was&&was.kind==="idea")follow(was);return}if(runDoc)openRun(c,true,true);else if(c.kind==="idea")ideaBody(c)}}
|
|
545
618
|
// An idea's card goes when its run starts. Follow it to that run rather than keep
|
|
546
619
|
// offering "Run again": the newest run in its project whose request holds the idea's
|
|
547
620
|
// text, the rule boardCards() retires it by. No such run: close the panel.
|
|
@@ -563,10 +636,12 @@ function card(c){
|
|
|
563
636
|
const chips=[];
|
|
564
637
|
if(c.status==="waiting")chips.push(`<span class="chip warn"><i></i>${esc(need(c.awaiting)||"needs you")}</span>`);
|
|
565
638
|
else if(c.status==="running")chips.push(`<span class="chip run"><i></i>running</span>`);
|
|
639
|
+
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
640
|
if(all)chips.push(`<span class="chip">${esc(c.where)}</span>`);
|
|
567
641
|
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
|
-
|
|
642
|
+
// A gathered idea's notes open with the stamp promote() writes (ingest() wrote it before
|
|
643
|
+
// the icebox); its chip names the source.
|
|
644
|
+
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
645
|
// Information only: the panel's button is where it is committed and pushed.
|
|
571
646
|
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
647
|
const pick=picking&&!archive&&c.column===4;
|
|
@@ -641,7 +716,7 @@ async function startRun(c,kind){
|
|
|
641
716
|
const p=runPick&&runPick.k===key(c)?runPick:null;runPick=null;
|
|
642
717
|
const yolo=!!(p&&p.yolo&&!$("run-yolo")?.hidden);
|
|
643
718
|
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
|
|
719
|
+
if(r.ok){toast((kind==="full"?"Full":"Lite")+(yolo?" yolo":"")+" run "+where(r)+": "+via(r.agent));if(r.pty)wantTerm=key(c)}
|
|
645
720
|
load()}
|
|
646
721
|
|
|
647
722
|
function render(){
|
|
@@ -725,10 +800,12 @@ function open(c,keepTab){
|
|
|
725
800
|
if(view!=="board")go("board",true);
|
|
726
801
|
stopDictation(true); // the panel body is about to be replaced
|
|
727
802
|
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"
|
|
803
|
+
if(c.kind==="idea"){runDoc=null;$("psub").textContent="Idea · "+c.where;$("ptabs").innerHTML="";ideaBody(c);
|
|
804
|
+
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
805
|
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
806
|
if(!keepTab)tab=tabOf(c.status==="abandoned"?"plan":({1:"plan",2:"backlog",3:"review",4:"preview"})[c.column]||"request");
|
|
807
|
+
// A run its agent is working on, in this board, opens on that agent.
|
|
808
|
+
if(!keepTab&&c.pty&&c.pty.alive&&c.status==="running")tab=TABS.length;
|
|
732
809
|
openRun(c,false)}
|
|
733
810
|
// Notes are a chat: one entry per message, files attached by paste or picker.
|
|
734
811
|
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 +864,42 @@ function showProgress(c){
|
|
|
787
864
|
// again: something changed on the server, so the doc is read afresh: a note sent, a plan
|
|
788
865
|
// written or a commit made elsewhere shows in nothing the page already holds. One /run
|
|
789
866
|
// fetch per change while a run panel is open; a tab switch reuses the doc.
|
|
867
|
+
// ---- the in-card agent (agent.launch: card). xterm.js is loaded the first time a
|
|
868
|
+
// terminal is shown. One terminal is mounted at a time, in #pbody; a refresh that
|
|
869
|
+
// finds it already showing this session leaves it alone, so it never reconnects.
|
|
870
|
+
let wantTerm=null,termKey=null,termOff=null,termLib=null;
|
|
871
|
+
const where=r=>r.pty?"running in its card":"opened in your terminal";
|
|
872
|
+
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)});
|
|
873
|
+
function termStop(){if(termOff)termOff();termOff=null;termKey=null;$("panel").classList.remove("wide")}
|
|
874
|
+
async function showTerm(c){const k=c.pty.key;if(termKey===k&&$("pbody").querySelector(".term"))return;
|
|
875
|
+
termStop();termKey=k;$("panel").classList.add("wide");$("pbody").innerHTML=`<div class="term" aria-label="Agent terminal"></div>`;
|
|
876
|
+
let mount;try{mount=await termLoad()}catch{$("pbody").innerHTML=`<p style="color:var(--dim)">The terminal could not load.</p>`;return}
|
|
877
|
+
if(termKey!==k)return;termOff=mount($("pbody").querySelector(".term"),k,()=>load(true))}
|
|
878
|
+
// An idea's panel: its terminal while the run it started is being born there, else its notes.
|
|
879
|
+
function ideaBody(c){if(c.pty)showTerm(c);else{termStop();chat(c,c.notes,c.archived||c.status!=="idea")}}
|
|
790
880
|
async function openRun(c,refresh,again){
|
|
791
881
|
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
882
|
// A file list being confirmed, or a push under way, keeps the doc it was drawn from.
|
|
793
883
|
if(refresh&&$("pfoot").querySelector(".inl,[data-busy],.more[open]"))return;runDoc=d}
|
|
794
884
|
if(openKey!==key(c))return;
|
|
885
|
+
// The fetch above may have finished after a newer /state: draw the card as it is now
|
|
886
|
+
// (a resumed agent's terminal, not the one that ended).
|
|
887
|
+
c=S.cards.find(x=>key(x)===openKey)||c;
|
|
795
888
|
// A refresh while notes are being typed would wipe the textarea.
|
|
796
|
-
const ta=$("pbody").querySelector("textarea");if(refresh&&ta&&document.activeElement===ta)return;
|
|
889
|
+
const ta=$("pbody").querySelector(".notes textarea");if(refresh&&ta&&document.activeElement===ta)return;
|
|
797
890
|
$("psub").textContent=c.id+" · "+(WORD[c.status]||c.status)+(c.awaiting?" · "+need(c.awaiting):"");
|
|
798
|
-
|
|
891
|
+
// A card whose agent runs in the board gets its terminal as one more tab, chosen once
|
|
892
|
+
// right after a launch from here.
|
|
893
|
+
const tabs=c.pty?[...TABS,["Terminal","term"]]:TABS;
|
|
894
|
+
if(c.pty&&wantTerm===key(c)){wantTerm=null;tab=TABS.length}
|
|
895
|
+
if(tab>=tabs.length)tab=0;
|
|
896
|
+
$("panel").classList.toggle("wide",tabs[tab][1]==="term");
|
|
799
897
|
$("ptabs").innerHTML=tabs.map((t,i)=>`<button class="${i===tab?"on":""}" data-i="${i}">${t[0]}</button>`).join("");
|
|
800
898
|
[...$("ptabs").children].forEach(b=>b.onclick=()=>{tab=+b.dataset.i;openRun(c,true)});
|
|
801
899
|
const k=tabs[tab][1],doc=k==="preview"?(preview(runDoc.impl)&&attach(preview(runDoc.impl),c)):runDoc[k];
|
|
802
|
-
if(k
|
|
900
|
+
if(k!=="term")termStop();
|
|
901
|
+
if(k==="term")showTerm(c);
|
|
902
|
+
else if(k==="notes"){const ro=c.archived||c.status==="done"||c.status==="abandoned";
|
|
803
903
|
chat(c,doc,ro);}
|
|
804
904
|
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
905
|
if(k==="backlog")showProgress(c);
|
|
@@ -820,21 +920,29 @@ async function openRun(c,refresh,again){
|
|
|
820
920
|
// gitBtn's buttons restyled as menu rows.
|
|
821
921
|
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
922
|
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>`;
|
|
923
|
+
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
924
|
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()})}
|
|
925
|
+
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>`:""}`;
|
|
926
|
+
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
927
|
// Answering an approval from the panel opens your agent on the run, told the decision and,
|
|
828
928
|
// for a revise, the comment. Answered approvals are remembered outside the footer, which
|
|
829
929
|
// every refresh rebuilds, so a second press cannot open a second agent. Keyed by when the
|
|
830
930
|
// run last moved: the same approval reached again after a revise is a new question.
|
|
831
931
|
// The footer's More menu is a <details>: close it on a click anywhere else.
|
|
832
932
|
document.addEventListener("click",e=>{for(const d of document.querySelectorAll(".more[open]"))if(!d.contains(e.target))d.open=false});
|
|
933
|
+
// Resume: a run whose in-card agent ended, or one stalled with no progress, gets its agent
|
|
934
|
+
// back on the run (its conversation, where the tool can resume one), in its card.
|
|
935
|
+
const resumable=c=>c.status==="stalled"||!!(c.pty&&!c.pty.alive);
|
|
936
|
+
async function resume(c,b){b.disabled=true;askNotify();const r=await post("/resume",{dir:c.state,id:c.id});
|
|
937
|
+
if(!r.ok){b.disabled=false;return}
|
|
938
|
+
toast("Resumed, "+where(r)+": "+via(r.agent));if(r.pty)wantTerm=key(c);
|
|
939
|
+
if(openKey===key(c)){$("pfoot").innerHTML="";openRun(c,true)}}
|
|
833
940
|
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
941
|
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
942
|
// A failed send keeps the footer, and with it a typed comment.
|
|
836
943
|
if(!r.ok){b.disabled=false;return}
|
|
837
|
-
decided.add(answer(c));toast((why===undefined?"Approved":"Sent back")+"
|
|
944
|
+
decided.add(answer(c));toast((why===undefined?"Approved":"Sent back")+", "+where(r)+": "+via(r.agent));
|
|
945
|
+
if(r.pty)wantTerm=key(c);
|
|
838
946
|
if(openKey===key(c)){$("pfoot").innerHTML="";openRun(c,true)}}
|
|
839
947
|
function confirmRevise(c){const foot=$("pfoot"),plan=c.awaiting==="approvals.plan";
|
|
840
948
|
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 +991,7 @@ function fixForm(c){const k=n=>c.id+":"+n,sent=f=>fixSent.has(k(f.n)),fs=runDoc.
|
|
|
883
991
|
allb.onclick=()=>{const none=picks().length===open.length;open.forEach(f=>none?fixOff.add(k(f.n)):fixOff.delete(k(f.n)));sync()};
|
|
884
992
|
go.onclick=async()=>{const nums=picks();go.disabled=true;askNotify();const r=await post("/fix",{dir:c.state,id:c.id,nums});
|
|
885
993
|
// 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")+"
|
|
994
|
+
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
995
|
sync();$("pbody").appendChild(el)}
|
|
888
996
|
// Commit and push, for a finished run in a project with a git remote. What the board
|
|
889
997
|
// did is recorded per run, so the settled label outlives later changes to the tree.
|
|
@@ -910,7 +1018,7 @@ async function commitPush(c,message,b){b.disabled=true;b.dataset.busy=1;b.textCo
|
|
|
910
1018
|
// A refused message comes back for another try, beside the current file list.
|
|
911
1019
|
if(!r.ok&&message&&openKey===key(c)&&runDoc.git.files&&runDoc.git.files.length)confirmCommit(c,message)}
|
|
912
1020
|
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})}
|
|
1021
|
+
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
1022
|
$("pclose").onclick=close;$("scrim").onclick=close;
|
|
915
1023
|
// An idea is one inbox line, so a structured request arrives as one wall:
|
|
916
1024
|
// "Fix x Problem: … Scope: … Acceptance criteria: (1) … (2) …". Split it, for display
|
|
@@ -960,7 +1068,7 @@ const groups=rows=>{const m=new Map();for(const r of rows){if(!m.has(r.group))m.
|
|
|
960
1068
|
let setQ="";
|
|
961
1069
|
async function openSettings(){
|
|
962
1070
|
stopDictation(true);
|
|
963
|
-
openKey=null;runDoc=null;$("panel").classList.add("open");$("scrim").classList.add("on");$("ptitle").textContent="Settings";$("ptabs").innerHTML="";$("pfoot").innerHTML="";
|
|
1071
|
+
termStop();openKey=null;runDoc=null;$("panel").classList.add("open");$("scrim").classList.add("on");$("ptitle").textContent="Settings";$("ptabs").innerHTML="";$("pfoot").innerHTML="";
|
|
964
1072
|
const cur=document.documentElement.dataset.theme||"system";
|
|
965
1073
|
const cfg=await fetch("/settings").then(r=>r.json()).catch(()=>null);
|
|
966
1074
|
$("psub").textContent="Theme applies to this browser. Everything else is saved to your kaizen settings.";$("psub").title=cfg?cfg.file:"";
|
|
@@ -1001,11 +1109,11 @@ async function openSettings(){
|
|
|
1001
1109
|
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
1110
|
// The system's own folder dialog, opened by the board: a page cannot read a real path.
|
|
1003
1111
|
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
|
|
1112
|
+
// ---- views: the board, or Settings and the Icebox as pages in the docked panel, each with its own address
|
|
1005
1113
|
let view="board";
|
|
1006
1114
|
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);
|
|
1115
|
+
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");document.body.classList.toggle("ice",v==="sources");
|
|
1116
|
+
if(push&&v!==was)history.pushState(null,"",v==="board"?location.pathname:"?view="+(v==="sources"?"icebox":v));
|
|
1009
1117
|
const p=$("panel");
|
|
1010
1118
|
if(v==="board"){if(was==="board")return;
|
|
1011
1119
|
// Undocked without the slide: it was a page, not a drawer leaving.
|
|
@@ -1016,63 +1124,208 @@ document.querySelector(".rail .home").onclick=e=>{if(e.ctrlKey||e.metaKey||e.shi
|
|
|
1016
1124
|
addEventListener("popstate",()=>go(new URLSearchParams(location.search).get("view")));
|
|
1017
1125
|
// The menu's theme switch while Settings shows: its Theme row follows.
|
|
1018
1126
|
addEventListener("kz-theme",()=>{if(view==="settings")openSettings()});
|
|
1019
|
-
// ----
|
|
1020
|
-
|
|
1127
|
+
// ---- the icebox: requests gathered from request sources, read like an inbox, until
|
|
1128
|
+
// someone promotes them to ideas or dismisses them. The sources themselves sit in a
|
|
1129
|
+
// section below, folded. Same panel as settings.
|
|
1130
|
+
const SRC="Icebox";
|
|
1131
|
+
// What the page keeps across the rebuild every server change brings: the item whose
|
|
1132
|
+
// promote form is open and the one the cursor is on (both "<dir> <id>"), the checked
|
|
1133
|
+
// items, the source filter ("<dir> <url>"), whether the sources section is open, and
|
|
1134
|
+
// whether a bulk action is still posting.
|
|
1135
|
+
// The category filter, and whether the add form takes a link or a social account/tag. The
|
|
1136
|
+
// project filter is the board's own `shown`, so the board and the icebox narrow alike.
|
|
1137
|
+
let iceForm=null,iceCur=null,iceSrc="",iceCat="",srcKind="link",srcShow=null,iceBusy=false,iceNav=null;const iceSel=new Set();
|
|
1138
|
+
const PLAT={x:"X",tiktok:"TikTok",instagram:"Instagram",threads:"Threads"};
|
|
1021
1139
|
const srcOpen=()=>$("panel").classList.contains("open")&&$("ptitle").textContent===SRC;
|
|
1022
1140
|
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
1141
|
async function openSources(){
|
|
1024
1142
|
stopDictation(true);
|
|
1025
1143
|
// 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]:["",""];
|
|
1144
|
+
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
1145
|
// So does a project picked on a row but not yet moved.
|
|
1028
1146
|
const picked=new Map(srcOpen()?[...$("pbody").querySelectorAll("[data-mv]")].map(m=>[m.dataset.k,m.value]):[]);
|
|
1029
|
-
|
|
1030
|
-
|
|
1147
|
+
// And a promote form half filled, for the item it was typed on.
|
|
1148
|
+
// And which control of which inbox row had focus, by item key: row indices shift when a row goes.
|
|
1149
|
+
const fRow=srcOpen()&&document.activeElement?.closest?.(".ice-row"),fPick=fRow?[fRow.dataset.k,document.activeElement.id.replace(/-\d+$/,"")]:null;
|
|
1150
|
+
const iceDraft=srcOpen()&&$("ice-title")?[$("ice-title").dataset.k,$("ice-title").value,$("ice-details").value]:null;
|
|
1151
|
+
// And a category typed on a row but not yet saved.
|
|
1152
|
+
const cats=new Map(srcOpen()?[...$("pbody").querySelectorAll("[data-ct]")].map(m=>[m.dataset.k,m.value]):[]);
|
|
1153
|
+
termStop();openKey=null;runDoc=null;$("panel").classList.add("open");$("scrim").classList.add("on");$("ptitle").textContent=SRC;$("ptabs").innerHTML="";$("pfoot").innerHTML="";
|
|
1154
|
+
$("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
1155
|
const dir=all?null:S.dir,q=dir?"?dir="+encodeURIComponent(dir):"";
|
|
1032
1156
|
const d=await fetch("/sources"+q).then(r=>r.json()).catch(()=>null);
|
|
1033
1157
|
if(!srcOpen())return;
|
|
1034
1158
|
const host=u=>{try{return new URL(u).hostname}catch{return u}};
|
|
1035
|
-
|
|
1036
|
-
|
|
1159
|
+
// The favicon of the site a link is on, asked of that site itself (Google's own static host for
|
|
1160
|
+
// Sheets and Docs, whose site icon is generic): nothing but the host reaches anyone. A social
|
|
1161
|
+
// platform's mark is drawn here instead: Meta's favicons are same-origin only.
|
|
1162
|
+
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{}
|
|
1163
|
+
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";
|
|
1164
|
+
return `<span class="fav">${src?`<img src="${esc(src)}" alt="" loading="lazy" referrerpolicy="no-referrer" onerror="this.remove()">`:""}${ico(fb)}</span>`};
|
|
1165
|
+
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"};
|
|
1166
|
+
const list=d&&d.sources||[],ice=d&&d.icebox||[],G=d&&d.gatherer||{},chatOn=G.use==="chatgpt"&&G.connected;
|
|
1037
1167
|
// A source belongs to one project, named on its row. Picking another only arms Move: a select
|
|
1038
1168
|
// changes on every arrow key, and each change would have moved the source. The names are the picker's;
|
|
1039
1169
|
// a row whose project the picker does not list still gets its own, selected, so Move never points elsewhere.
|
|
1040
1170
|
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
1171
|
const cards=list.map((s,i)=>{
|
|
1043
|
-
const st=srcType(s
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}).
|
|
1047
|
-
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1172
|
+
const st=srcType(s),title=s.label||host(s.url);
|
|
1173
|
+
// A social source is never read by kaizen, so its not-ok is the agent's turn, not a failure.
|
|
1174
|
+
const statusHtml=s.social&&(!s.last||!s.last.ok&&/gathered by your agent/i.test(s.last.note))?`<span class="src-badge dim">${ico("at")}${chatOn?"Gathered with ChatGPT":"Gathered by your agent"}</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>`;
|
|
1175
|
+
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-ag-${i}" data-ag="${i}" aria-label="Gather ${esc(title)} with your agent" title="${chatOn?"Read this source with your ChatGPT plan":"Open your agent to run /kaizen gather for this project"}">${ico("refresh")} Gather</button>`:`<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>`;
|
|
1176
|
+
}).join("");
|
|
1177
|
+
const fails=(d&&d.failed||[]).map(f=>`<p style="color:var(--dim)">The sources of ${esc(f)} could not be read.</p>`).join("");
|
|
1178
|
+
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>`;
|
|
1179
|
+
const social=srcKind==="social";
|
|
1180
|
+
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?(chatOn?"Your ChatGPT plan reads public posts when you press Gather on the source.":"Kaizen signs in nowhere. Your agent reads public posts when you press Gather on the source."):"Public links only. For private docs, run /kaizen gather."}</span><button class="btn pri" id="src-add">Add source</button></div></div>`;
|
|
1181
|
+
// The inbox: one row per request, by project when showing all, then by category,
|
|
1182
|
+
// narrowed to one source by its chip. Checked, open and cursor items are kept by key.
|
|
1183
|
+
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)};
|
|
1184
|
+
const iceKey=it=>it.dir+" "+it.id,srcKey=it=>it.dir+" "+it.source;
|
|
1185
|
+
// Filters narrow in turn: project (the board's own picker, all projects only), then
|
|
1186
|
+
// category, then source; each chip counts what the filters before it leave.
|
|
1187
|
+
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};
|
|
1188
|
+
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})}
|
|
1189
|
+
const inProj=it=>dir||!shown.size||shown.has(it.dir),pv=ice.filter(inProj);
|
|
1190
|
+
const byCat=count(pv,catOf);if(iceCat&&!byCat.has(iceCat))iceCat="";
|
|
1191
|
+
const cv=pv.filter(it=>!iceCat||catOf(it)===iceCat);
|
|
1192
|
+
const bySrc=count(cv,srcKey);if(iceSrc&&!bySrc.has(iceSrc))iceSrc="";
|
|
1193
|
+
const keys=new Set(ice.map(iceKey));
|
|
1194
|
+
if(iceForm&&!keys.has(iceForm))iceForm=null;
|
|
1195
|
+
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)});
|
|
1196
|
+
// Rows in the order they are drawn, which is the order j and k walk.
|
|
1197
|
+
const order=[...groups.values()].flat();
|
|
1198
|
+
// A checked request a filter hides is unchecked: a bulk action only ever acts on what shows.
|
|
1199
|
+
{const vis=new Set(order.map(i=>iceKey(ice[i])));[...iceSel].forEach(k=>vis.has(k)||iceSel.delete(k))}
|
|
1200
|
+
if(!order.some(i=>iceKey(ice[i])===iceCur))iceCur=order.length?iceKey(ice[order[0]]):null;
|
|
1201
|
+
const nSel=order.filter(i=>iceSel.has(iceKey(ice[i]))).length;
|
|
1202
|
+
// Filters are plain dropdowns, each shown only when it has a choice to make. The project one
|
|
1203
|
+
// is the board's own picker: one project, or all; several picked on the board read as such.
|
|
1204
|
+
const many=byProj.size>1,opt=(v,on,t)=>`<option value="${esc(v)}"${on?" selected":""}>${esc(t)}</option>`;
|
|
1205
|
+
const pick=(id,label,xs)=>`<select class="sel ice-sel" id="${id}" aria-label="${label}">${xs.join("")}</select>`;
|
|
1206
|
+
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))]):"")
|
|
1207
|
+
+(byCat.size>1?pick("ice-fc","Category",[opt("",!iceCat,"All categories"),...[...byCat.keys()].map(k=>opt(k,iceCat===k,k))]):"")
|
|
1208
|
+
+(bySrc.size>1?pick("ice-fs","Source",[opt("",!iceSrc,"All sources"),...[...bySrc].map(([k,{it}])=>opt(k,iceSrc===k,srcName(it)))]):"");
|
|
1209
|
+
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>`;
|
|
1210
|
+
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>`:"";
|
|
1211
|
+
// The open item's form: a clean title for the idea's card and the person's own details;
|
|
1212
|
+
// what was gathered is added to the idea's notes by the server.
|
|
1213
|
+
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>`;
|
|
1214
|
+
const row=i=>{const it=ice[i],k=iceKey(it),open=iceForm===k;
|
|
1215
|
+
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>`
|
|
1216
|
+
+(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>`:"")};
|
|
1217
|
+
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>`
|
|
1218
|
+
:`<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>`;
|
|
1219
|
+
// The sources, in the right-hand drawer: shut until asked for, open by itself while there
|
|
1220
|
+
// are none until closed. Slid in only when opened; a rebuild while open shows it in place.
|
|
1221
|
+
const drawer=srcShow??!list.length,slide=drawer&&!$("src-sec")?.classList.contains("open");
|
|
1222
|
+
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> · ${G.connected?`ChatGPT${G.email?" "+esc(G.email):""} <a id="cg-out" role="button" tabindex="0">Disconnect</a>`:`<a id="cg-in" role="button" tabindex="0">Connect ChatGPT</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>`;
|
|
1223
|
+
// First visit: offer the ChatGPT plan for social sources, or keep the agent.
|
|
1224
|
+
const onb=G.use==="ask"&&!G.connected?`<div class="cg-onb"><b>Gather social posts with your ChatGPT plan</b><p>Connect ChatGPT Plus or Pro and Gather reads X, TikTok, Instagram and Threads sources right here, counted against your plan, with no agent to open. Skip it and Gather keeps using your agent. You can change this in Settings.</p><div class="acts"><button class="btn ghost" id="cg-skip">Skip for now</button><button class="btn pri" id="cg-go">Continue with ChatGPT</button></div></div>`:"";
|
|
1225
|
+
$("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>`:onb+bar+bulk+fails+inbox+sec;
|
|
1226
|
+
if(!d||d.error)return;
|
|
1227
|
+
if(iceDraft&&iceDraft[0]===iceForm&&$("ice-title")){$("ice-title").value=iceDraft[1];$("ice-details").value=iceDraft[2]}
|
|
1228
|
+
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
1229
|
if($("src-to-settings"))$("src-to-settings").onclick=()=>{setQ="sources";go("settings",true)};
|
|
1230
|
+
// The tab is opened inside the click, before the sign-in URL comes back: a popup blocker
|
|
1231
|
+
// lets only that through. The board hears of the result through /events.
|
|
1232
|
+
const connect=async()=>{const w=window.open("","_blank");const j=await post("/chatgpt",{op:"connect"});if(j.ok&&w)w.location=j.url;else{w?.close();if(j.ok)location.href=j.url}};
|
|
1233
|
+
for(const id of ["cg-go","cg-in"])if($(id))$(id).onclick=connect;
|
|
1234
|
+
if($("cg-skip"))$("cg-skip").onclick=async()=>{if((await post("/chatgpt",{op:"skip"})).ok){toast("Gather keeps using your agent");openSources()}};
|
|
1235
|
+
if($("cg-out"))$("cg-out").onclick=async()=>{if(!confirm("Disconnect ChatGPT? Gather goes back to your agent."))return;if((await post("/chatgpt",{op:"disconnect"})).ok){toast("ChatGPT disconnected");openSources()}};
|
|
1236
|
+
if(slide){$("src-sec").offsetWidth;$("src-sec").classList.add("open");$("src-scrim").classList.add("on")}
|
|
1237
|
+
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()};
|
|
1238
|
+
$("src-x").onclick=$("src-scrim").onclick=srcShut;$("src-sec").onkeydown=e=>{if(e.key==="Escape"){e.stopPropagation();srcShut()}};
|
|
1239
|
+
$("src-tog").onclick=()=>{if($("src-sec").classList.contains("open"))return srcShut();srcShow=true;openSources().then(()=>srcShow&&$("src-url")?.focus())};
|
|
1240
|
+
if($("ice-fs"))$("ice-fs").onchange=e=>{iceSrc=e.target.value;openSources()};
|
|
1241
|
+
if($("ice-fc"))$("ice-fc").onchange=e=>{iceCat=e.target.value;openSources()};
|
|
1242
|
+
if($("ice-fp"))$("ice-fp").onchange=e=>{const v=e.target.value;if(v==="*")return;shown=new Set(v?[v]:[]);setShown();openSources()};
|
|
1243
|
+
if($("ice-unf"))$("ice-unf").onclick=()=>{iceCat=iceSrc="";if(!dir)shown.clear();setShown();openSources()};
|
|
1244
|
+
$("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
1245
|
const act=async(body,done)=>{const j=await post("/sources",body);if(j.ok){done(j);load()}if(srcOpen())openSources()};
|
|
1053
1246
|
$("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
1247
|
if(to&&[...m.options].some(o=>o.value===to))m.value=to;
|
|
1055
1248
|
(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.
|
|
1249
|
+
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"}.`))});
|
|
1250
|
+
// The category saves on its button or Enter, never per keystroke.
|
|
1251
|
+
$("pbody").querySelectorAll("[data-ct]").forEach(m=>{const s=list[+m.dataset.ct],b=$("src-cs-"+m.dataset.ct),was=cats.get(m.dataset.k);
|
|
1252
|
+
if(was!==undefined)m.value=was;
|
|
1253
|
+
(m.oninput=()=>{b.disabled=m.value.trim()===(s.category||"")})();
|
|
1254
|
+
m.onkeydown=e=>{if(e.key==="Enter"&&!b.disabled)b.click()};
|
|
1255
|
+
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"))}});
|
|
1256
|
+
// The acting button is disabled as it is pressed, which drops focus to the page; it goes back to the cursor row.
|
|
1257
|
+
const focusCur=()=>{if(!document.activeElement||document.activeElement===document.body)$("pbody").querySelector(".ice-row.cur .ice-t")?.focus()};
|
|
1258
|
+
// Before rows go, the cursor steps to the next row that stays, else the one before.
|
|
1259
|
+
const stepPast=gone=>{const at=order.findIndex(i=>iceKey(ice[i])===iceCur);if(at<0||!gone.has(iceCur))return;
|
|
1260
|
+
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])};
|
|
1261
|
+
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)})();
|
|
1262
|
+
// Bulk: one dismiss per project; promote one request at a time, each its own idea
|
|
1263
|
+
// titled as gathered (one promote call with several ids would merge them into one).
|
|
1264
|
+
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;
|
|
1265
|
+
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}
|
|
1266
|
+
else for(const it of its)if((await post("/icebox",{op,dir:it.dir,ids:[it.id]})).ok)ok++}
|
|
1267
|
+
finally{iceBusy=false}
|
|
1268
|
+
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)};
|
|
1269
|
+
if(bulk){$("ice-bp").onclick=()=>bulkDo("promote");$("ice-bd").onclick=()=>bulkDo("dismiss");$("ice-bc").onclick=()=>{iceSel.clear();openSources()}}
|
|
1270
|
+
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()};
|
|
1271
|
+
if($("ice-all"))$("ice-all").indeterminate=nSel>0&&nSel<order.length;
|
|
1272
|
+
$("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()});
|
|
1273
|
+
// A row opens on its title or anywhere not a control; open shows what was gathered and the promote form.
|
|
1274
|
+
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()})};
|
|
1275
|
+
$("pbody").querySelectorAll(".ice-row").forEach(r=>r.onclick=e=>{if(!e.target.closest("input,button"))toggle(+r.dataset.i)});
|
|
1276
|
+
$("pbody").querySelectorAll("[data-o]").forEach(b=>b.onclick=()=>toggle(+b.dataset.o));
|
|
1277
|
+
$("pbody").querySelectorAll("[data-pr]").forEach(b=>b.onclick=()=>{b.disabled=true;iceAct("promote",ice[+b.dataset.pr],"Promoted to Ideas")});
|
|
1278
|
+
$("pbody").querySelectorAll("[data-ds]").forEach(b=>b.onclick=()=>{b.disabled=true;iceAct("dismiss",ice[+b.dataset.ds],"Dismissed")});
|
|
1279
|
+
const open1=ice.find(it=>iceKey(it)===iceForm);
|
|
1280
|
+
if($("ice-ok")){
|
|
1281
|
+
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})};
|
|
1282
|
+
$("ice-ok").onclick=ok;
|
|
1283
|
+
$("ice-cancel").onclick=()=>{iceForm=null;openSources()};
|
|
1284
|
+
$("ice-title").onkeydown=e=>{if(e.key==="Enter")ok();if(e.key==="Escape")$("ice-cancel").click()};
|
|
1285
|
+
$("ice-details").onkeydown=e=>{if(e.key==="Enter"&&(e.ctrlKey||e.metaKey))ok();if(e.key==="Escape")$("ice-cancel").click()};
|
|
1286
|
+
}
|
|
1287
|
+
// Keys, read by the page's keydown while the icebox shows. True when the key was used.
|
|
1288
|
+
iceNav=e=>{if(e.altKey||e.ctrlKey||e.metaKey)return false;
|
|
1289
|
+
const k=e.key,at=order.findIndex(i=>iceKey(ice[i])===iceCur),i=order[at];
|
|
1290
|
+
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}
|
|
1291
|
+
if(at<0)return false;
|
|
1292
|
+
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]);
|
|
1293
|
+
$("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}
|
|
1294
|
+
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;
|
|
1295
|
+
if(!b||b.disabled)return false;e.preventDefault();b.click();return true};
|
|
1057
1296
|
$("pbody").querySelectorAll("[data-chk]").forEach(b=>{const s=list[+b.dataset.chk];
|
|
1058
1297
|
b.onclick=()=>{b.disabled=true;b.innerHTML=ico("refresh")+" Checking…";
|
|
1059
1298
|
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")})}});
|
|
1299
|
+
toast(r?(r.ok?`${s.label||host(s.url)}: ${r.note}${r.added?" in the icebox":""}`:`Failed: ${r.note}`):"Checked")})}});
|
|
1300
|
+
// A social source is read by the agent, so Gather opens one on /kaizen gather.
|
|
1301
|
+
$("pbody").querySelectorAll("[data-ag]").forEach(b=>{const s=list[+b.dataset.ag];
|
|
1302
|
+
b.onclick=()=>{b.disabled=true;if(chatOn)b.innerHTML=ico("refresh")+" Gathering…";
|
|
1303
|
+
act({op:"agent",dir:s.dir,url:s.url},j=>{const r=j.results&&j.results[0];toast(j.via==="chatgpt"?(r&&r.ok?`${s.label||host(s.url)}: ${r.note}${r.added?" in the icebox":""}`:`Failed: ${r?r.note:"no answer"}`):"/kaizen gather opened in your terminal: "+via(j.agent))})}});
|
|
1061
1304
|
$("pbody").querySelectorAll("[data-rm]").forEach(b=>{const s=list[+b.dataset.rm];
|
|
1062
|
-
b.onclick=()=>{if(!confirm(`Remove ${s.label||host(s.url)}?
|
|
1305
|
+
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
1306
|
act({op:"remove",dir:list[+b.dataset.rm].dir,url:list[+b.dataset.rm].url},()=>toast("Source removed"))}});
|
|
1064
|
-
|
|
1065
|
-
|
|
1307
|
+
// A row that went takes focus to the cursor row's title, never to whatever now holds its index.
|
|
1308
|
+
const ci=ice.findIndex(it=>iceKey(it)===iceCur);
|
|
1309
|
+
if(fPick){const at=ice.findIndex(it=>iceKey(it)===fPick[0]);(at>=0?$(fPick[1]+"-"+at):ci>=0?$("ice-o-"+ci):null)?.focus()}
|
|
1310
|
+
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();
|
|
1311
|
+
if($("src-add")){
|
|
1312
|
+
// A social source is a platform and what to follow; the server builds its link, and
|
|
1313
|
+
// nothing is gathered here: the agent reads it.
|
|
1314
|
+
$("src-add").onclick=()=>{const social=srcKind==="social",url=$("src-url").value.trim(),q=$("src-q").value.trim(),plat=$("src-plat").value;
|
|
1315
|
+
if(social?!q:!url){$(social?"src-q":"src-url").focus();return}const to=$("src-dir").value;if(!to){$("src-dir").focus();return}
|
|
1066
1316
|
$("src-add").disabled=true;$("src-add").textContent="Adding…";
|
|
1067
|
-
(async()=>{const
|
|
1068
|
-
|
|
1069
|
-
|
|
1317
|
+
(async()=>{const label=$("src-label").value.trim()||(social?`${PLAT[plat]}: ${q}`:""),category=$("src-cat").value.trim();
|
|
1318
|
+
const j=await post("/sources",social?{op:"add",dir:to,platform:plat,query:q,label,category}:{op:"add",dir:to,url,label,category});
|
|
1319
|
+
if(j.ok&&social){$("src-q").value=$("src-label").value=$("src-cat").value="";toast("Source added. Click Gather on it to have your agent read its posts.");load()}
|
|
1320
|
+
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];
|
|
1321
|
+
toast(r?(r.ok?`Source added: ${r.note} in the icebox`:`Source added, but: ${r.note}`):"Source added");load()}
|
|
1070
1322
|
else toast(j.error||"Could not add source");
|
|
1071
1323
|
if(srcOpen())openSources()})()};
|
|
1072
|
-
$("src-url").onkeydown=$("src-label").onkeydown=e=>{if(e.key==="Enter")$("src-add").click()};
|
|
1324
|
+
$("src-url").onkeydown=$("src-q").onkeydown=$("src-label").onkeydown=$("src-cat").onkeydown=e=>{if(e.key==="Enter")$("src-add").click()};
|
|
1325
|
+
}
|
|
1073
1326
|
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
|
|
1327
|
+
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;
|
|
1328
|
+
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
1329
|
// With `report`, the toast carries a link to a prefilled GitHub bug form (/issue) and
|
|
1077
1330
|
// stays long enough to click it.
|
|
1078
1331
|
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)}
|
|
@@ -1096,7 +1349,7 @@ async function pick(v){
|
|
|
1096
1349
|
if(v==="__find"){toast("Searching…");const j=await post("/find",{});if(j.ok)toast(j.added?`Found ${j.added} new project${j.added===1?"":"s"}`:"No new projects found")}
|
|
1097
1350
|
else if(v==="all"){all=true;keep()}
|
|
1098
1351
|
else{const j=await post("/cd",{dir:v});if(j.ok){all=false;keep(v)}}
|
|
1099
|
-
load()}
|
|
1352
|
+
await load();if(view==="sources")openSources()}
|
|
1100
1353
|
// The all-projects filter: tick the projects to show; none ticked shows every one. Kept per browser.
|
|
1101
1354
|
let shown=new Set((()=>{try{return JSON.parse(localStorage.getItem("kz-show"))||[]}catch(e){return[]}})());
|
|
1102
1355
|
// Known projects, plus any folder a card comes from that is not one (the global inbox).
|
|
@@ -1128,6 +1381,8 @@ $("newtop").onclick=()=>{mode="new";render();$("board").scrollTo(0,0)};
|
|
|
1128
1381
|
$("kbd").onclick=()=>$("help").classList.add("on");$("help").onclick=()=>$("help").classList.remove("on");
|
|
1129
1382
|
document.addEventListener("click",()=>{if(menuKey||clearMenu){menuKey=null;clearMenu=false;render()}});
|
|
1130
1383
|
document.onkeydown=e=>{
|
|
1384
|
+
// The icebox reads its own keys first; a checkbox there keeps them, for x then j.
|
|
1385
|
+
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
1386
|
if(e.target.matches("input,select,textarea,[contenteditable]"))return;
|
|
1132
1387
|
if(e.key==="?"){$("help").classList.toggle("on");return}
|
|
1133
1388
|
if(e.key==="/"&&$("sq")&&$("panel").classList.contains("open")){e.preventDefault();$("sq").focus();return}
|
|
@@ -1149,11 +1404,11 @@ document.onkeydown=e=>{
|
|
|
1149
1404
|
let boot=null;
|
|
1150
1405
|
// Back from a terminal: what was committed or pushed there touched nothing the server watches.
|
|
1151
1406
|
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)}}
|
|
1407
|
+
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
1408
|
// Back to the project picked last time, unless the URL says which view it wants. A
|
|
1154
1409
|
// plain fetch: a project that is gone should fall back to all, not raise a toast.
|
|
1155
1410
|
// 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));
|
|
1411
|
+
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));document.body.classList.toggle("ice",v0==="sources");
|
|
1157
1412
|
(async()=>{let d;try{d=localStorage.getItem("kz-proj")}catch(e){}
|
|
1158
1413
|
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
1414
|
try{const f=JSON.parse(sessionStorage.getItem("kz-form"));if(f)[mode,modeKey]=f}catch(e){}
|