@jabworks/condux 0.12.1 → 0.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jabworks/condux",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "OpenCode plugin for the condux workflow toolkit — bundles the condux skills and injects the specialist agents (coder, explorer, planner, researcher) plus an opt-in plan-review listener.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -9,7 +9,7 @@
9
9
  Card, Textarea, Popover, Separator) — adapted as vanilla CSS, no React. -->
10
10
  <style>
11
11
  /* tokens:core:start */
12
- /* Shared colour core — do not hand-edit; edit scripts/tokens/core.css. */
12
+ /* Shared design core — do not hand-edit; edit scripts/tokens/core.css. */
13
13
  :root {
14
14
  --background: #111110;
15
15
  --card: #191918;
@@ -41,11 +41,49 @@
41
41
  --info-border: #104d87;
42
42
  --outline: rgba(237, 237, 236, 0.08);
43
43
  --subtle: #686560;
44
- --radius: 0.25rem;
44
+ --shadow-sm: 0 1px 2px rgba(6, 6, 5, 0.5);
45
+ --shadow-md: 0 4px 14px rgba(6, 6, 5, 0.55);
46
+ --shadow-lg: 0 20px 60px rgba(6, 6, 5, 0.62);
45
47
  --mono: "Geist Mono", "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Code", monospace;
48
+ --sans: "Geist", "Inter Tight", ui-sans-serif, system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
49
+ --text-2xs: 0.6875rem;
50
+ --text-xs: 0.75rem;
51
+ --text-sm: 0.8125rem;
52
+ --text-base: 0.875rem;
53
+ --text-md: 1rem;
54
+ --text-lg: 1.125rem;
55
+ --text-xl: 1.375rem;
56
+ --text-2xl: 1.75rem;
57
+ --text-3xl: 2.5rem;
58
+ --text-4xl: 3.5rem;
59
+ --leading-tight: 1.15;
60
+ --leading-snug: 1.35;
61
+ --leading-normal: 1.55;
62
+ --leading-relaxed: 1.7;
63
+ --tracking-tight: -0.022em;
64
+ --tracking-normal: 0em;
65
+ --tracking-wide: 0.06em;
66
+ --space-1: 0.25rem;
67
+ --space-2: 0.5rem;
68
+ --space-3: 0.75rem;
69
+ --space-4: 1rem;
70
+ --space-5: 1.5rem;
71
+ --space-6: 2rem;
72
+ --space-8: 3rem;
73
+ --space-12: 4rem;
74
+ --radius-sm: 0.1875rem;
75
+ --radius: 0.25rem;
76
+ --radius-md: 0.5rem;
77
+ --radius-lg: 0.75rem;
78
+ --radius-full: 9999px;
79
+ --dur-fast: 120ms;
80
+ --dur: 200ms;
81
+ --dur-slow: 320ms;
82
+ --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
83
+ --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
46
84
  }
47
85
  @media (prefers-color-scheme: light) {
48
- :root {
86
+ :root:not([data-theme="dark"]) {
49
87
  --background: #f6f5ef;
50
88
  --card: #fcfbf6;
51
89
  --card-foreground: #36302a;
@@ -76,34 +114,341 @@
76
114
  --info-border: #bed7ef;
77
115
  --outline: rgba(54, 48, 42, 0.08);
78
116
  --subtle: #a8a294;
117
+ --shadow-sm: 0 1px 2px rgba(54, 48, 42, 0.1);
118
+ --shadow-md: 0 4px 14px rgba(54, 48, 42, 0.13);
119
+ --shadow-lg: 0 20px 60px rgba(54, 48, 42, 0.18);
79
120
  }
80
121
  }
122
+ :root[data-theme="light"] {
123
+ --background: #f6f5ef;
124
+ --card: #fcfbf6;
125
+ --card-foreground: #36302a;
126
+ --popover: #fcfbf6;
127
+ --foreground: #36302a;
128
+ --muted: #f0efe3;
129
+ --muted-foreground: #726c5e;
130
+ --border: #ddd6c6;
131
+ --input: #ddd6c6;
132
+ --primary: #967e60;
133
+ --primary-foreground: #ffffff;
134
+ --primary-hover: #897254;
135
+ --primary-muted: #efece0;
136
+ --primary-text: #6f5e44;
137
+ --ring: #967e60;
138
+ --accent: #eae7da;
139
+ --success: #3f7a34;
140
+ --success-muted: #e6f5e2;
141
+ --success-border: #c2e1b3;
142
+ --warning: #93761d;
143
+ --warning-muted: #fbf4c4;
144
+ --warning-border: #ebd87f;
145
+ --destructive: #bf4f3b;
146
+ --destructive-muted: #fbe8e2;
147
+ --destructive-border: #f0c9ba;
148
+ --info: #3f6fa6;
149
+ --info-muted: #e6f1fc;
150
+ --info-border: #bed7ef;
151
+ --outline: rgba(54, 48, 42, 0.08);
152
+ --subtle: #a8a294;
153
+ --shadow-sm: 0 1px 2px rgba(54, 48, 42, 0.1);
154
+ --shadow-md: 0 4px 14px rgba(54, 48, 42, 0.13);
155
+ --shadow-lg: 0 20px 60px rgba(54, 48, 42, 0.18);
156
+ }
81
157
  /* tokens:core:end */
82
158
 
83
- /* plan-review extensions — the annotation highlight and a second radius
84
- step. Outside the markers on purpose: anything inside is replaced. */
159
+ /* kit:css:start */
160
+ /* Shared state layer — do not hand-edit; edit scripts/tokens/kit.css. */
161
+
162
+ /* :where() keeps these at zero specificity, so a surface overrides them by
163
+ writing a plain rule rather than escalating into an !important contest. */
164
+ :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
165
+ outline: 2px solid var(--ring);
166
+ outline-offset: 2px;
167
+ border-radius: var(--radius-sm);
168
+ }
169
+
170
+ /* Mouse users should not see the ring; keyboard users must. */
171
+ :where(a, button, input, select, textarea, summary, [tabindex]):focus:not(:focus-visible) {
172
+ outline: none;
173
+ }
174
+
175
+ .kit-skip {
176
+ position: absolute;
177
+ left: var(--space-2);
178
+ top: -4rem;
179
+ z-index: 999;
180
+ padding: var(--space-2) var(--space-3);
181
+ background: var(--primary);
182
+ color: var(--primary-foreground);
183
+ border-radius: var(--radius);
184
+ text-decoration: none;
185
+ transition: top var(--dur) var(--ease-out);
186
+ }
187
+
188
+ .kit-skip:focus {
189
+ top: var(--space-2);
190
+ }
191
+
192
+ /* Empty, loading and error states. Namespaced kit- because three of the four
193
+ surfaces already own an .empty or .error class of their own. */
194
+ .kit-empty {
195
+ border: 1px dashed var(--border);
196
+ border-radius: var(--radius-md);
197
+ padding: var(--space-8) var(--space-4);
198
+ text-align: center;
199
+ color: var(--muted-foreground);
200
+ }
201
+
202
+ .kit-empty__title {
203
+ margin: 0 0 var(--space-1);
204
+ font-size: var(--text-md);
205
+ color: var(--foreground);
206
+ }
207
+
208
+ .kit-empty__body {
209
+ margin: 0 auto;
210
+ max-width: 44ch;
211
+ font-size: var(--text-sm);
212
+ text-wrap: pretty;
213
+ }
214
+
215
+ .kit-error {
216
+ display: flex;
217
+ gap: var(--space-2);
218
+ align-items: baseline;
219
+ padding: var(--space-2) var(--space-3);
220
+ background: var(--destructive-muted);
221
+ border: 1px solid var(--destructive-border);
222
+ border-radius: var(--radius);
223
+ color: var(--destructive);
224
+ font-size: var(--text-sm);
225
+ }
226
+
227
+ /* Shaped like the content it stands in for — never a spinner. Width is the
228
+ caller's job, so one rule serves a title, a row and a paragraph. */
229
+ .kit-skel {
230
+ height: 0.75rem;
231
+ border-radius: var(--radius-sm);
232
+ background: linear-gradient(90deg, var(--muted) 0%, var(--accent) 50%, var(--muted) 100%);
233
+ background-size: 200% 100%;
234
+ animation: kit-shimmer 1.4s var(--ease-out) infinite;
235
+ }
236
+
237
+ @keyframes kit-shimmer {
238
+ 100% {
239
+ background-position: -200% 0;
240
+ }
241
+ }
242
+
243
+ /* Keyboard help overlay, populated by kit.js from the live binding registry. */
244
+ .kit-help {
245
+ position: fixed;
246
+ inset: 0;
247
+ z-index: 998;
248
+ display: grid;
249
+ place-items: center;
250
+ padding: var(--space-4);
251
+ background: color-mix(in oklch, var(--background) 78%, transparent);
252
+ }
253
+
254
+ .kit-help[hidden] {
255
+ display: none;
256
+ }
257
+
258
+ .kit-help__panel {
259
+ width: min(34rem, 100%);
260
+ max-height: 80dvh;
261
+ overflow-y: auto;
262
+ padding: var(--space-5);
263
+ background: var(--card);
264
+ color: var(--card-foreground);
265
+ border: 1px solid var(--border);
266
+ border-radius: var(--radius-md);
267
+ box-shadow: var(--shadow-lg);
268
+ }
269
+
270
+ .kit-help__title {
271
+ margin: 0 0 var(--space-4);
272
+ font-family: var(--mono);
273
+ font-size: var(--text-2xs);
274
+ letter-spacing: var(--tracking-wide);
275
+ text-transform: uppercase;
276
+ color: var(--subtle);
277
+ }
278
+
279
+ .kit-help__row {
280
+ display: flex;
281
+ justify-content: space-between;
282
+ gap: var(--space-4);
283
+ padding: var(--space-1) 0;
284
+ font-size: var(--text-sm);
285
+ }
286
+
287
+ .kit-help__keys {
288
+ flex: none;
289
+ display: flex;
290
+ gap: var(--space-1);
291
+ }
292
+
293
+ .kit-kbd {
294
+ min-width: 1.4rem;
295
+ padding: 0.1rem 0.4rem;
296
+ text-align: center;
297
+ font-family: var(--mono);
298
+ font-size: var(--text-2xs);
299
+ color: var(--foreground);
300
+ background: var(--popover);
301
+ border: 1px solid var(--border);
302
+ border-bottom-width: 2px;
303
+ border-radius: var(--radius-sm);
304
+ }
305
+
306
+ .kit-theme {
307
+ display: inline-flex;
308
+ gap: 1px;
309
+ }
310
+
311
+ .kit-theme button {
312
+ font: inherit;
313
+ font-size: var(--text-2xs);
314
+ padding: var(--space-1) var(--space-2);
315
+ color: var(--muted-foreground);
316
+ background: var(--card);
317
+ border: 1px solid var(--border);
318
+ cursor: pointer;
319
+ transition:
320
+ color var(--dur-fast) var(--ease-out),
321
+ background-color var(--dur-fast) var(--ease-out),
322
+ border-color var(--dur-fast) var(--ease-out);
323
+ }
324
+
325
+ .kit-theme button:first-child {
326
+ border-radius: var(--radius) 0 0 var(--radius);
327
+ }
328
+
329
+ .kit-theme button:last-child {
330
+ border-radius: 0 var(--radius) var(--radius) 0;
331
+ }
332
+
333
+ .kit-theme button:hover {
334
+ color: var(--foreground);
335
+ border-color: var(--primary);
336
+ }
337
+
338
+ .kit-theme button[aria-pressed="true"] {
339
+ color: var(--primary-foreground);
340
+ background: var(--primary);
341
+ border-color: var(--primary);
342
+ }
343
+
344
+ .kit-copied {
345
+ color: var(--success);
346
+ }
347
+
348
+ @media (prefers-reduced-motion: reduce) {
349
+ *,
350
+ *::before,
351
+ *::after {
352
+ animation-duration: 0.01ms !important;
353
+ animation-iteration-count: 1 !important;
354
+ transition-duration: 0.01ms !important;
355
+ scroll-behavior: auto !important;
356
+ }
357
+ }
358
+
359
+ /* Browsers do not print background colours by default, so a dark-themed page
360
+ prints light text onto white and reads as blank. Repointing the tokens is
361
+ what makes Ctrl-P work at all; hiding chrome is the surface's opt-in. */
362
+ @media print {
363
+ :root {
364
+ --background: #ffffff;
365
+ --card: #ffffff;
366
+ --popover: #ffffff;
367
+ --muted: #f4f4f2;
368
+ --accent: #f4f4f2;
369
+ --foreground: #000000;
370
+ --card-foreground: #000000;
371
+ --muted-foreground: #444444;
372
+ --subtle: #666666;
373
+ --border: #999999;
374
+ --input: #999999;
375
+ --shadow-sm: none;
376
+ --shadow-md: none;
377
+ --shadow-lg: none;
378
+ }
379
+
380
+ body {
381
+ background: #ffffff;
382
+ color: #000000;
383
+ }
384
+
385
+ .kit-skip,
386
+ .kit-help,
387
+ .kit-theme,
388
+ [data-kit-chrome] {
389
+ display: none !important;
390
+ }
391
+
392
+ a[href^="http"]::after {
393
+ content: " (" attr(href) ")";
394
+ font-size: 90%;
395
+ color: #555555;
396
+ }
397
+
398
+ .kit-skel {
399
+ animation: none;
400
+ }
401
+ }
402
+ /* kit:css:end */
403
+
404
+ /* plan-review extensions — the annotation highlight. Outside the markers
405
+ on purpose: anything inside is replaced.
406
+ --radius-md was defined here at 0.25rem, shadowing the core's 0.5rem
407
+ by source order. The core value is adopted as-is; spots that wanted
408
+ the smaller 0.25rem step now say so explicitly via --radius, which
409
+ already carries that value and is already this file's convention
410
+ (.nav .tabs, .popover, .chip, code, pre, …). */
85
411
  :root {
86
412
  --hl: rgba(255, 202, 22, 0.30); --hl-active: rgba(255, 202, 22, 0.55);
87
- --radius-md: 0.25rem;
88
413
  }
89
414
  @media (prefers-color-scheme: light) {
90
415
  :root { --hl: rgba(234, 179, 8, 0.42); --hl-active: rgba(202, 138, 4, 0.62); }
91
416
  }
92
417
  * { box-sizing: border-box; }
93
418
  html, body { height: 100%; }
94
- body { margin: 0; background: var(--background); color: var(--foreground); font: 13px/1.6 var(--mono); font-variant-numeric: tabular-nums; display: flex; flex-direction: column; height: 100vh; }
95
- .app { display: grid; grid-template-columns: 248px minmax(0, 1fr) 372px; flex: 1 1 auto; min-height: 0; }
419
+ /* .kit-error sets display:flex unconditionally; an author rule beats the
420
+ UA [hidden] default regardless of specificity, so the hidden attribute
421
+ needs its own author rule to still win. */
422
+ .kit-error[hidden] { display: none; }
423
+ /* Page shell: content row fills, the decision bar pins to the bottom.
424
+ .app below is the second axis (3 columns) — together they are the
425
+ one legitimately 2-D layout in this file; everything else here is a
426
+ one-dimensional row or column and stays flex. */
427
+ body { margin: 0; background: var(--background); color: var(--foreground); font-family: var(--sans); font-size: var(--text-sm); line-height: var(--leading-normal); font-variant-numeric: tabular-nums; display: grid; grid-template-rows: 1fr auto; height: 100dvh; }
428
+ .app { display: grid; grid-template-columns: 248px minmax(0, 1fr) 372px; min-height: 0; }
429
+ @media (max-width: 1100px) {
430
+ .app { grid-template-columns: 208px minmax(0, 1fr) 320px; }
431
+ }
432
+ @media (max-width: 860px) {
433
+ .app { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; max-height: none; }
434
+ .nav { max-height: 38vh; border-right: none; border-bottom: 1px solid var(--border); }
435
+ .chat { max-height: 34vh; border-left: none; border-top: 1px solid var(--border); }
436
+ }
437
+ @media (max-width: 640px) {
438
+ .main { padding: var(--space-5) var(--space-4) 100px; }
439
+ .popover { width: calc(100vw - 24px); }
440
+ }
96
441
  ::-webkit-scrollbar { width: 9px; height: 9px; }
97
442
  ::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 9999px; border: 2px solid transparent; background-clip: padding-box; }
98
443
  ::-webkit-scrollbar-track { background: transparent; }
99
444
 
100
445
  /* ---- Terminus: Button ---- */
101
446
  .btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap;
102
- font: 500 13px var(--mono); border-radius: var(--radius-md); padding: 8px 14px; border: 1px solid transparent;
447
+ font: 500 var(--text-sm) var(--sans); border-radius: var(--radius); padding: 8px 14px; border: 1px solid transparent;
103
448
  background: transparent; color: var(--foreground); cursor: pointer; transition: background-color .12s, border-color .12s, color .12s; }
104
449
  .btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--background), 0 0 0 4px color-mix(in oklch, var(--ring) 45%, transparent); }
105
450
  .btn:disabled { pointer-events: none; opacity: .4; }
106
- .btn-sm { padding: 6px 10px; font-size: 12px; }
451
+ .btn-sm { padding: 6px 10px; font-size: var(--text-xs); }
107
452
  .btn-block { width: 100%; }
108
453
  .btn-solid { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); }
109
454
  .btn-solid:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
@@ -119,40 +464,41 @@
119
464
  .btn-danger:hover { background: color-mix(in oklch, var(--destructive-muted) 70%, var(--destructive)); }
120
465
 
121
466
  /* ---- Terminus: Badge ---- */
122
- .badge { display: inline-flex; align-items: center; gap: 4px; border-radius: var(--radius-md); border: 1px solid transparent;
123
- font: 500 11px var(--mono); padding: 1px 6px; min-width: 18px; justify-content: center; }
467
+ .badge { display: inline-flex; align-items: center; gap: 4px; border-radius: var(--radius); border: 1px solid transparent;
468
+ font: 500 var(--text-2xs) var(--mono); padding: 1px 6px; min-width: 18px; justify-content: center; }
124
469
  .badge-warning { background: var(--warning-muted); color: var(--warning); border-color: var(--warning-border); }
125
470
  .badge-soft { background: var(--primary-muted); color: var(--primary-text); }
126
471
 
127
472
  /* Left: navigation — Contents (docs + headings) | Files (paths touched) */
128
473
  .nav { border-right: 1px solid var(--border); background: var(--card); overflow-y: auto; padding: 18px 14px 40px; }
474
+ .nav .kit-theme { margin: 0 0 12px; }
129
475
  .nav .tabs { display: flex; margin: 0 0 12px; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
130
- .nav .tab { flex: 1; font: 500 11px var(--mono); letter-spacing: .08em; text-transform: uppercase; padding: 6px 4px;
476
+ .nav .tab { flex: 1; font: 500 var(--text-2xs) var(--sans); letter-spacing: .08em; text-transform: uppercase; padding: 6px 4px;
131
477
  background: transparent; border: none; border-right: 1px solid var(--border); color: var(--subtle); cursor: pointer; }
132
478
  .nav .tab:last-child { border-right: none; }
133
479
  .nav .tab:hover { color: var(--foreground); }
134
480
  .nav .tab.active { background: var(--accent); color: var(--foreground); }
135
481
  .nav a.doc { font-weight: 600; color: var(--foreground); }
136
482
  .nav a.doc.active { border-left-color: var(--primary); background: var(--accent); }
137
- .nav a.file .t { font-size: 12px; }
138
- .nav .none { color: var(--subtle); font-size: 12px; padding: 4px 6px; }
483
+ .nav a.file .t { font-family: var(--mono); font-size: var(--text-xs); }
484
+ .nav .none { color: var(--subtle); font-size: var(--text-xs); padding: 4px 6px; }
139
485
  code.mention-flash { outline: 2px solid var(--warning); outline-offset: 1px; }
140
- .nav .label, .chat .label { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--subtle); margin: 0 0 12px; padding: 0 6px; }
486
+ .nav .label, .chat .label { font-size: var(--text-2xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--subtle); margin: 0 0 12px; padding: 0 6px; }
141
487
  .nav a { display: flex; align-items: center; gap: 6px; color: var(--muted-foreground); text-decoration: none; padding: 4px 6px; border-radius: 4px; border-left: 2px solid transparent; }
142
488
  .nav a span.t { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
143
489
  .nav a:hover { background: var(--accent); color: var(--foreground); }
144
490
  .nav a.active { color: var(--foreground); border-left-color: var(--primary); background: var(--accent); }
145
- .nav a.lvl2 { padding-left: 16px; } .nav a.lvl3 { padding-left: 28px; font-size: 12px; color: var(--subtle); }
491
+ .nav a.lvl2 { padding-left: 16px; } .nav a.lvl3 { padding-left: 28px; font-size: var(--text-xs); color: var(--subtle); }
146
492
 
147
493
  /* Directory mode: doc-site tree — folder rows fold, filter narrows, the
148
494
  active doc alone carries its headings. All of it DIRMODE-gated. */
149
495
  .nav .tree-filter { display: none; margin: 0 0 10px; }
150
- .nav .tree-filter input { width: 100%; font: inherit; font-size: 13px; color: var(--foreground); background: var(--background);
496
+ .nav .tree-filter input { width: 100%; font: inherit; font-size: var(--text-sm); color: var(--foreground); background: var(--background);
151
497
  border: 1px solid var(--border); border-radius: var(--radius); padding: 6px 8px; outline: none; }
152
498
  .nav .tree-filter input:focus { border-color: var(--ring); }
153
499
  .nav a.folder { font-weight: 600; color: var(--muted-foreground); }
154
- .nav a.folder .arrow { flex: none; width: 12px; color: var(--subtle); font-size: 10px; }
155
- .crumbs { display: none; max-width: 760px; margin: 0 auto 18px; font: 12px var(--mono); color: var(--subtle);
500
+ .nav a.folder .arrow { flex: none; width: 12px; color: var(--subtle); font-size: var(--text-2xs); }
501
+ .crumbs { display: none; max-width: 760px; margin: 0 auto 18px; font: var(--text-xs) var(--mono); color: var(--subtle);
156
502
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
157
503
  .crumbs .seg { color: var(--muted-foreground); }
158
504
  .crumbs a.seg { color: var(--muted-foreground); text-decoration: none; cursor: pointer; }
@@ -162,7 +508,7 @@
162
508
  .pnav { display: none; max-width: 760px; margin: 32px auto 0; padding-top: 14px; border-top: 1px solid var(--border);
163
509
  justify-content: space-between; gap: 12px; }
164
510
  .pnav a { display: inline-flex; align-items: center; gap: 6px; max-width: 48%; color: var(--muted-foreground);
165
- text-decoration: none; font-size: 13px; padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--radius);
511
+ text-decoration: none; font-size: var(--text-sm); padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--radius);
166
512
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
167
513
  .pnav a:hover { background: var(--accent); color: var(--foreground); }
168
514
  .pnav .spacer { flex: 1; }
@@ -173,20 +519,20 @@
173
519
  linear-gradient(to right, color-mix(in oklab, var(--foreground) 3%, transparent) 1px, transparent 1px),
174
520
  linear-gradient(to bottom, color-mix(in oklab, var(--foreground) 3%, transparent) 1px, transparent 1px);
175
521
  background-size: 32px 32px; }
176
- .main .hint { color: var(--subtle); margin: 0 auto 24px; font-size: 12px; max-width: 760px; }
177
- .doc { max-width: 760px; margin: 0 auto; }
522
+ .main .hint { color: var(--subtle); margin: 0 auto 24px; font-size: var(--text-xs); max-width: 760px; }
523
+ .doc { max-width: 760px; margin: 0 auto; line-height: var(--leading-relaxed); }
178
524
 
179
525
  /* Revision diff — on a revised plan, show what changed instead of a silent swap. */
180
526
  .revbar { display: none; align-items: center; gap: 10px; max-width: 760px; margin: 0 auto 20px;
181
527
  padding: 9px 12px; border: 1px solid var(--warning-border); background: var(--warning-muted); border-radius: var(--radius); }
182
528
  .revbar.show { display: flex; }
183
- .revbar .rb-text { color: var(--warning); font: 600 12px var(--mono); }
184
- .revbar .rb-stat { color: var(--muted-foreground); font-size: 12px; }
529
+ .revbar .rb-text { color: var(--warning); font: 600 var(--text-xs) var(--sans); }
530
+ .revbar .rb-stat { color: var(--muted-foreground); font-size: var(--text-xs); }
185
531
  .revbar .rb-actions { margin-left: auto; display: flex; gap: 6px; }
186
532
  .diffwrap { display: none; max-width: 760px; margin: 0 auto; }
187
533
  .diffwrap.show { display: block; }
188
534
  .diffview { background: var(--muted); border: 1px solid var(--border); border-radius: var(--radius);
189
- padding: 8px 0; overflow-x: auto; font: 12px/1.55 var(--mono); }
535
+ padding: 8px 0; overflow-x: auto; font: var(--text-xs)/var(--leading-normal) var(--mono); }
190
536
  .diffline { display: flex; white-space: pre; padding: 0 12px; }
191
537
  .diffline .g { width: 14px; flex: none; color: var(--subtle); user-select: none; }
192
538
  .diffline.ctx { color: var(--muted-foreground); }
@@ -195,20 +541,20 @@
195
541
  .diffline.del { background: var(--destructive-muted); color: var(--destructive); }
196
542
  .diffline.del .g { color: var(--destructive); }
197
543
  .diffline.fold { justify-content: center; color: var(--subtle); font-style: italic; padding: 3px 12px; }
198
- h1, h2, h3, h4, h5, h6 { line-height: 1.3; margin: 1.5em 0 0.6em; scroll-margin-top: 24px; }
199
- h1 { font-size: 21px; } h2 { font-size: 16px; } h3 { font-size: 14px; color: var(--muted-foreground); }
200
- h4 { font-size: 13px; } h5, h6 { font-size: 12px; color: var(--muted-foreground); }
544
+ h1, h2, h3, h4, h5, h6 { line-height: var(--leading-snug); margin: 1.5em 0 0.6em; scroll-margin-top: 24px; }
545
+ h1 { font-size: var(--text-xl); } h2 { font-size: var(--text-md); } h3 { font-size: var(--text-base); color: var(--muted-foreground); }
546
+ h4 { font-size: var(--text-sm); } h5, h6 { font-size: var(--text-xs); color: var(--muted-foreground); }
201
547
  p { margin: 0.6em 0; }
202
548
  ul, ol { margin: 0.6em 0; padding-left: 22px; } li { margin: 0.25em 0; }
203
549
  li.task { list-style: none; margin-left: -18px; }
204
550
  del { color: var(--subtle); }
205
551
  a { color: var(--info); }
206
- code { background: var(--muted); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; font-size: 12px; }
207
- pre { background: var(--muted); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; overflow-x: auto; }
552
+ code { background: var(--muted); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; font-family: var(--mono); font-size: var(--text-xs); }
553
+ pre { background: var(--muted); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; overflow-x: auto; font-family: var(--mono); }
208
554
  pre code { background: none; border: none; padding: 0; }
209
555
  blockquote { margin: 0.6em 0; padding: 2px 14px; color: var(--muted-foreground); border-left: 3px solid var(--primary); }
210
556
  hr { border: none; border-top: 1px solid var(--border); margin: 1.6em 0; }
211
- table { border-collapse: collapse; margin: 0.9em 0; width: 100%; display: block; overflow-x: auto; font-size: 13px; }
557
+ table { border-collapse: collapse; margin: 0.9em 0; width: 100%; display: block; overflow-x: auto; font-size: var(--text-sm); }
212
558
  th, td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; vertical-align: top; }
213
559
  th { background: var(--muted); font-weight: 600; }
214
560
  tbody tr:nth-child(even) { background: color-mix(in oklch, var(--muted) 40%, transparent); }
@@ -224,18 +570,18 @@
224
570
  .popover:not(.expanded) .chip:last-child { border-bottom-right-radius: var(--radius); }
225
571
  /* Floating annotation toolbar — a menubar of category items over an input. */
226
572
  .popover { position: absolute; z-index: 31; display: none; width: 340px; background: var(--popover); border: 1px solid var(--border);
227
- border-radius: var(--radius); box-shadow: 0 12px 32px rgba(0,0,0,0.45); padding: 0; }
228
- .popover .q { color: var(--muted-foreground); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
573
+ border-radius: var(--radius); box-shadow: var(--shadow-lg); padding: 0; }
574
+ .popover .q { color: var(--muted-foreground); font-size: var(--text-2xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
229
575
  padding: 6px 10px; margin: 0; background: var(--muted); border-bottom: 1px solid var(--border); border-radius: var(--radius) var(--radius) 0 0; }
230
576
  /* Menubar strip: category items, evenly split, divider-separated */
231
577
  .popover .cats { display: flex; margin: 0; border-bottom: 1px solid var(--border); }
232
- .chip { flex: 1; font: 500 11px var(--mono); padding: 6px 8px; text-align: center; border: none; border-right: 1px solid var(--border);
578
+ .chip { flex: 1; font: 500 var(--text-2xs) var(--sans); padding: 6px 8px; text-align: center; border: none; border-right: 1px solid var(--border);
233
579
  background: transparent; color: var(--muted-foreground); cursor: pointer; transition: background-color .12s, color .12s; }
234
580
  .chip:last-child { border-right: none; }
235
581
  .chip:hover { background: var(--accent); color: var(--foreground); }
236
582
  .chip.active { background: var(--primary-muted); color: var(--primary-text); }
237
583
  .popover .row { display: flex; justify-content: flex-end; gap: 8px; align-items: center; padding: 6px 8px; border-top: 1px solid var(--border); }
238
- .popover .hintk { color: var(--subtle); font-size: 11px; margin-right: auto; }
584
+ .popover .hintk { color: var(--subtle); font-size: var(--text-2xs); margin-right: auto; }
239
585
  /* Caret pointing at the selection (down when the toolbar sits above it) */
240
586
  .popover::after { content: ''; position: absolute; left: 18px; width: 9px; height: 9px; background: var(--popover);
241
587
  border: 1px solid var(--border); transform: rotate(45deg); display: none; }
@@ -243,8 +589,8 @@
243
589
  .popover.below::after { display: block; top: -5px; background: var(--muted); border-bottom: none; border-right: none; }
244
590
 
245
591
  /* ---- Terminus: Textarea ---- */
246
- .ta { width: 100%; resize: vertical; font: 13px/1.5 var(--mono); color: var(--foreground); background: transparent;
247
- border: 1px solid var(--input); border-radius: var(--radius-md); padding: 8px 10px; }
592
+ .ta { width: 100%; resize: vertical; font: var(--text-sm)/var(--leading-normal) var(--sans); color: var(--foreground); background: transparent;
593
+ border: 1px solid var(--input); border-radius: var(--radius); padding: 8px 10px; }
248
594
  .ta::placeholder { color: var(--subtle); }
249
595
  .ta:focus-visible { outline: none; border-color: var(--ring); box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring) 28%, transparent); }
250
596
  .popover .ta { min-height: 132px; border: none; border-radius: 0; padding: 8px 10px; } .popover .ta:focus-visible { outline: none; box-shadow: none; }
@@ -256,26 +602,26 @@
256
602
  .thread { flex: 1; overflow-y: auto; padding: 6px 14px; display: flex; flex-direction: column; gap: 8px; }
257
603
  .thread .empty { color: var(--subtle); padding: 8px 4px; }
258
604
  /* Terminus: Card */
259
- .msg { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; background: var(--card); box-shadow: 0 1px 2px rgba(0,0,0,0.16); cursor: default; }
605
+ .msg { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; background: var(--card); box-shadow: var(--shadow-sm); cursor: default; }
260
606
  .msg.note { border-color: var(--warning-border); cursor: pointer; }
261
607
  .msg.note.active { box-shadow: 0 0 0 2px var(--warning); }
262
608
  .msg .meta { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 4px; align-items: center; }
263
- .msg .who { color: var(--muted-foreground); font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; } .msg.note .who { color: var(--warning); }
264
- .msg .del { cursor: pointer; border: none; background: none; color: var(--subtle); font-size: 11px; padding: 0 2px; } .msg .del:hover { color: var(--destructive); }
265
- .msg .quote { color: var(--muted-foreground); font-size: 11px; border-left: 2px solid var(--warning); padding: 1px 8px; margin-bottom: 5px; max-height: 42px; overflow: hidden; }
609
+ .msg .who { color: var(--muted-foreground); font-size: var(--text-2xs); font-weight: 600; text-transform: uppercase; letter-spacing: .07em; } .msg.note .who { color: var(--warning); }
610
+ .msg .del { cursor: pointer; border: none; background: none; color: var(--subtle); font-size: var(--text-2xs); padding: 0 2px; } .msg .del:hover { color: var(--destructive); }
611
+ .msg .quote { color: var(--muted-foreground); font-size: var(--text-2xs); border-left: 2px solid var(--warning); padding: 1px 8px; margin-bottom: 5px; max-height: 42px; overflow: hidden; }
266
612
  .msg .body { white-space: pre-wrap; word-break: break-word; color: var(--card-foreground); }
267
613
 
268
614
  /* Terminus: Separator + composer */
269
615
  .composer { border-top: 1px solid var(--border); padding: 12px 14px; }
270
616
  .actions { padding: 0 14px 14px; }
271
- .count { color: var(--subtle); font-size: 11px; }
617
+ .count { color: var(--subtle); font-family: var(--mono); font-size: var(--text-2xs); }
272
618
 
273
619
  /* Bottom decision bar — decisions read as the end of the review. Reject sits
274
620
  isolated on the left; the two "continue" actions group on the right. The
275
621
  summary in the middle says what rides along with the decision. */
276
622
  .dbar { flex: none; display: flex; align-items: center; border-top: 1px solid var(--border); background: var(--card); padding: 10px 16px; min-height: 58px; }
277
623
  .dbar-live { display: flex; flex: 1; align-items: center; gap: 12px; }
278
- .dbar-summary { flex: 1; text-align: center; color: var(--subtle); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
624
+ .dbar-summary { flex: 1; text-align: center; color: var(--subtle); font-size: var(--text-xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
279
625
  .dbar-group { display: flex; gap: 8px; }
280
626
 
281
627
  /* Post-submit confirmation — replaces the decision bar content once a decision
@@ -285,12 +631,12 @@
285
631
  .sent.approve { border-color: var(--success-border); background: var(--success-muted); }
286
632
  .sent.revise { border-color: var(--warning-border); background: var(--warning-muted); }
287
633
  .sent.deny { border-color: var(--destructive-border); background: var(--destructive-muted); }
288
- .sent .h { display: flex; align-items: center; gap: 8px; font: 600 13px var(--mono); }
634
+ .sent .h { display: flex; align-items: center; gap: 8px; font: 600 var(--text-sm) var(--sans); }
289
635
  .sent.approve .h { color: var(--success); }
290
636
  .sent.revise .h { color: var(--warning); }
291
637
  .sent.deny .h { color: var(--destructive); }
292
638
  .sent .h { flex: none; }
293
- .sent .sub { color: var(--muted-foreground); font-size: 12px; line-height: 1.5; }
639
+ .sent .sub { color: var(--muted-foreground); font-size: var(--text-xs); line-height: var(--leading-normal); }
294
640
  .sent .ico { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; }
295
641
  .sent .again { margin-top: 10px; }
296
642
  .dot { width: 7px; height: 7px; border-radius: 9999px; background: currentColor; animation: pulse 1.3s ease-in-out infinite; }
@@ -299,21 +645,32 @@
299
645
  </head>
300
646
  <body>
301
647
  <div class="app">
302
- <aside class="nav">
648
+ <aside class="nav" data-kit-chrome>
649
+ <div class="kit-theme">
650
+ <button type="button" data-kit-theme="light">Light</button>
651
+ <button type="button" data-kit-theme="dark">Dark</button>
652
+ <button type="button" data-kit-theme="system">Auto</button>
653
+ </div>
303
654
  <div class="tabs">
304
655
  <button type="button" class="tab active" id="tab-contents">Contents</button>
305
656
  <button type="button" class="tab" id="tab-files">Files</button>
306
657
  </div>
307
658
  <div id="pane-contents">
308
- <div class="tree-filter" id="tree-filter"><input type="search" id="tree-q" placeholder="Filter docs…" aria-label="Filter documents"></div>
309
- <nav id="toc"></nav>
659
+ <div class="tree-filter" id="tree-filter"><input type="search" id="tree-q" data-kit-filter placeholder="Filter docs…" aria-label="Filter documents"></div>
660
+ <nav id="toc">
661
+ <div class="kit-skel" style="width:70%;height:0.85rem;margin:0 6px 10px"></div>
662
+ <div class="kit-skel" style="width:55%;height:0.85rem;margin:0 6px 10px"></div>
663
+ <div class="kit-skel" style="width:80%;height:0.85rem;margin:0 6px 10px"></div>
664
+ <div class="kit-skel" style="width:45%;height:0.85rem;margin:0 6px 10px"></div>
665
+ </nav>
310
666
  </div>
311
667
  <div id="pane-files" style="display:none"><nav id="filelist"></nav></div>
312
668
  </aside>
313
669
 
314
670
  <main class="main">
315
671
  <p class="hint">Select any text to comment on it. Or chat with the agent on the right. Then decide.</p>
316
- <div class="revbar" id="revbar">
672
+ <div class="kit-error" id="api-error" hidden><span class="msg-text"></span></div>
673
+ <div class="revbar" id="revbar" data-kit-chrome>
317
674
  <span class="rb-text">Plan revised</span>
318
675
  <span class="rb-stat" id="rb-stat"></span>
319
676
  <span class="rb-actions">
@@ -322,10 +679,15 @@
322
679
  </span>
323
680
  </div>
324
681
  <div class="crumbs" id="crumbs"></div>
325
- <div class="doc" id="plan">Loading plan…</div>
682
+ <div class="doc" id="plan">
683
+ <div class="kit-skel" style="width:60%;height:1.4rem;margin-bottom:1em"></div>
684
+ <div class="kit-skel" style="width:100%;margin-bottom:0.6em"></div>
685
+ <div class="kit-skel" style="width:96%;margin-bottom:0.6em"></div>
686
+ <div class="kit-skel" style="width:88%;margin-bottom:0.6em"></div>
687
+ </div>
326
688
  <div class="pnav" id="pnav"></div>
327
689
  <div class="diffwrap" id="diffwrap"><div class="diffview" id="diffview"></div></div>
328
- <div class="popover" id="popover">
690
+ <div class="popover" id="popover" data-kit-chrome>
329
691
  <div class="q" id="pop-quote"></div>
330
692
  <div class="cats" id="pop-cats">
331
693
  <button type="button" class="chip active" data-cat="Comment">Comment</button>
@@ -341,7 +703,7 @@
341
703
  </div>
342
704
  </main>
343
705
 
344
- <aside class="chat">
706
+ <aside class="chat" data-kit-chrome>
345
707
  <p class="label">Review <span class="count" id="count"></span></p>
346
708
  <div class="thread" id="thread"><div class="empty">No notes yet. Select text to comment, or send a message.</div></div>
347
709
  <div class="composer">
@@ -355,7 +717,7 @@
355
717
 
356
718
  <!-- Decisions live in a fixed bottom bar — the rail above is purely
357
719
  conversational (notes + messages). Reject is isolated on the left. -->
358
- <footer class="dbar">
720
+ <footer class="dbar" data-kit-chrome>
359
721
  <div class="dbar-live" id="dbar-live">
360
722
  <button class="btn btn-danger" id="btn-reject" data-decision="Reject">Reject</button>
361
723
  <span class="dbar-summary" id="dbar-summary"></span>
@@ -370,6 +732,342 @@
370
732
  </div>
371
733
  </footer>
372
734
 
735
+ <script>
736
+ /* kit:js:start */
737
+ /* Shared behaviour layer — do not hand-edit; edit scripts/tokens/kit.js. */
738
+
739
+ /* Deliberately free of backticks and interpolation openers. No target is a JS
740
+ template literal since the board-shell extraction, so this is insurance
741
+ rather than a requirement — but it is the difference between this file being
742
+ safe to inline anywhere and being safe only where someone remembered to
743
+ check. Regexes and escapes are avoided for the same reason. */
744
+
745
+ (function () {
746
+ 'use strict';
747
+
748
+ var THEME_KEY = 'jabworks-theme';
749
+ var THEME_MODES = ['light', 'dark', 'system'];
750
+ var root = document.documentElement;
751
+
752
+ /* localStorage throws on an opaque origin, which is what a file:// artifact
753
+ often gets. Degrade to session-only theming; never break the page. */
754
+ function readStored(key) {
755
+ try {
756
+ return window.localStorage.getItem(key);
757
+ } catch (e) {
758
+ return null;
759
+ }
760
+ }
761
+
762
+ function writeStored(key, value) {
763
+ try {
764
+ window.localStorage.setItem(key, value);
765
+ } catch (e) {
766
+ /* no-op */
767
+ }
768
+ }
769
+
770
+ function applyTheme(mode) {
771
+ if (mode === 'light' || mode === 'dark') root.setAttribute('data-theme', mode);
772
+ else root.removeAttribute('data-theme');
773
+
774
+ writeStored(THEME_KEY, mode);
775
+
776
+ var buttons = document.querySelectorAll('[data-kit-theme]');
777
+
778
+ for (var i = 0; i < buttons.length; i++) {
779
+ buttons[i].setAttribute('aria-pressed', String(buttons[i].getAttribute('data-kit-theme') === mode));
780
+ }
781
+ }
782
+
783
+ function currentTheme() {
784
+ return root.getAttribute('data-theme') || 'system';
785
+ }
786
+
787
+ function cycleTheme() {
788
+ applyTheme(THEME_MODES[(THEME_MODES.indexOf(currentTheme()) + 1) % THEME_MODES.length]);
789
+ }
790
+
791
+ function items() {
792
+ return Array.prototype.slice.call(document.querySelectorAll('[data-kit-item]'));
793
+ }
794
+
795
+ /* document.activeElement is null in a detached or not-yet-painted document,
796
+ and Element.closest is absent on the document node itself. */
797
+ function activeWithin(selector) {
798
+ var el = document.activeElement;
799
+
800
+ return el && el.closest ? el.closest(selector) : null;
801
+ }
802
+
803
+ function moveItem(step) {
804
+ var all = items();
805
+
806
+ if (all.length === 0) return;
807
+
808
+ var index = all.indexOf(activeWithin('[data-kit-item]'));
809
+ var next = all[Math.min(Math.max(index + step, 0), all.length - 1)] || all[0];
810
+
811
+ next.setAttribute('tabindex', '-1');
812
+ next.focus();
813
+ next.scrollIntoView({ block: 'nearest' });
814
+
815
+ /* Reflect the focused item in the URL so a board or a review can be linked
816
+ to a specific row. replaceState, not a hash assignment: assigning to
817
+ location.hash pushes a history entry per keypress. */
818
+ if (next.id && window.history && window.history.replaceState) {
819
+ window.history.replaceState(null, '', '#' + next.id);
820
+ }
821
+ }
822
+
823
+ function reveal(el) {
824
+ if (el && el.focus) {
825
+ el.focus();
826
+ if (el.select) el.select();
827
+
828
+ return true;
829
+ }
830
+
831
+ return false;
832
+ }
833
+
834
+ /* A surface whose filter field ships hidden cannot hang the hook on the field
835
+ itself — a hidden element does not take focus, so the binding would be a
836
+ silent no-op. Those surfaces put the hook on the control that REVEALS the
837
+ field instead; activate it, then focus what it revealed via aria-controls.
838
+ Without this the ? overlay would advertise "Focus filter" for a key that
839
+ only moves focus to a button, and a generated overlay that misdescribes its
840
+ own bindings is worse than no overlay. */
841
+ function focusFilter() {
842
+ var hook = document.querySelector('[data-kit-filter]');
843
+
844
+ if (!hook) return false;
845
+
846
+ if (hook.tagName === 'BUTTON') {
847
+ if (hook.getAttribute('aria-expanded') === 'false' && hook.click) hook.click();
848
+
849
+ if (reveal(document.getElementById(hook.getAttribute('aria-controls') || ''))) return true;
850
+ }
851
+
852
+ return reveal(hook);
853
+ }
854
+
855
+ function flashCopied(el) {
856
+ el.classList.add('kit-copied');
857
+ window.setTimeout(function () {
858
+ el.classList.remove('kit-copied');
859
+ }, 900);
860
+ }
861
+
862
+ function copyFrom(el) {
863
+ if (!el) return;
864
+
865
+ var text = el.getAttribute('data-kit-copy') || el.textContent || '';
866
+
867
+ if (!text || !navigator.clipboard || !navigator.clipboard.writeText) return;
868
+
869
+ navigator.clipboard.writeText(text.trim()).then(
870
+ function () {
871
+ flashCopied(el);
872
+ },
873
+ function () {
874
+ /* Clipboard is permission-gated; a refusal is not an error worth
875
+ interrupting the page for. */
876
+ },
877
+ );
878
+ }
879
+
880
+ function sections() {
881
+ return Array.prototype.slice.call(document.querySelectorAll('[data-kit-section]'));
882
+ }
883
+
884
+ function jumpToSection(n) {
885
+ var target = sections()[n - 1];
886
+
887
+ if (target) target.scrollIntoView({ block: 'start' });
888
+ }
889
+
890
+ /* One registry drives both the key handler and the help overlay, so the
891
+ documented shortcuts cannot drift from the implemented ones. */
892
+ var BINDINGS = [
893
+ { keys: ['j'], label: 'Next item', when: function () { return items().length > 0; }, run: function () { moveItem(1); } },
894
+ { keys: ['k'], label: 'Previous item', when: function () { return items().length > 0; }, run: function () { moveItem(-1); } },
895
+ {
896
+ keys: ['/'],
897
+ label: 'Focus filter',
898
+ when: function () { return Boolean(document.querySelector('[data-kit-filter]')); },
899
+ run: focusFilter,
900
+ },
901
+ {
902
+ keys: ['y'],
903
+ label: 'Copy id or path',
904
+ when: function () { return Boolean(document.querySelector('[data-kit-copy]')); },
905
+ run: function () {
906
+ copyFrom(activeWithin('[data-kit-copy]'));
907
+ },
908
+ },
909
+ { keys: ['t'], label: 'Cycle theme', when: function () { return true; }, run: cycleTheme },
910
+ {
911
+ keys: ['g', '1-9'],
912
+ label: 'Jump to section',
913
+ when: function () { return sections().length > 0; },
914
+ run: null,
915
+ },
916
+ { keys: ['?'], label: 'Show shortcuts', when: function () { return true; }, run: null },
917
+ { keys: ['Esc'], label: 'Dismiss', when: function () { return true; }, run: null },
918
+ ];
919
+
920
+ var overlay = null;
921
+
922
+ function buildOverlay() {
923
+ var host = document.createElement('div');
924
+
925
+ host.className = 'kit-help';
926
+ host.hidden = true;
927
+ host.setAttribute('role', 'dialog');
928
+ host.setAttribute('aria-modal', 'true');
929
+ host.setAttribute('aria-label', 'Keyboard shortcuts');
930
+
931
+ var panel = document.createElement('div');
932
+
933
+ panel.className = 'kit-help__panel';
934
+
935
+ var title = document.createElement('p');
936
+
937
+ title.className = 'kit-help__title';
938
+ title.textContent = 'Keyboard shortcuts';
939
+ panel.appendChild(title);
940
+
941
+ for (var i = 0; i < BINDINGS.length; i++) {
942
+ if (!BINDINGS[i].when()) continue;
943
+
944
+ var row = document.createElement('div');
945
+
946
+ row.className = 'kit-help__row';
947
+
948
+ var label = document.createElement('span');
949
+
950
+ label.textContent = BINDINGS[i].label;
951
+ row.appendChild(label);
952
+
953
+ var keys = document.createElement('span');
954
+
955
+ keys.className = 'kit-help__keys';
956
+
957
+ for (var k = 0; k < BINDINGS[i].keys.length; k++) {
958
+ var kbd = document.createElement('kbd');
959
+
960
+ kbd.className = 'kit-kbd';
961
+ kbd.textContent = BINDINGS[i].keys[k];
962
+ keys.appendChild(kbd);
963
+ }
964
+
965
+ row.appendChild(keys);
966
+ panel.appendChild(row);
967
+ }
968
+
969
+ host.appendChild(panel);
970
+ host.addEventListener('click', function (event) {
971
+ if (event.target === host) toggleOverlay(false);
972
+ });
973
+ document.body.appendChild(host);
974
+
975
+ return host;
976
+ }
977
+
978
+ function toggleOverlay(show) {
979
+ if (!overlay) overlay = buildOverlay();
980
+
981
+ overlay.hidden = !show;
982
+ }
983
+
984
+ function isTypingTarget(el) {
985
+ if (!el) return false;
986
+
987
+ var tag = el.tagName;
988
+
989
+ return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable === true;
990
+ }
991
+
992
+ var pendingJump = false;
993
+
994
+ function onKeydown(event) {
995
+ if (event.metaKey || event.ctrlKey || event.altKey) return;
996
+
997
+ if (event.key === 'Escape') {
998
+ toggleOverlay(false);
999
+ if (isTypingTarget(event.target) && event.target.blur) event.target.blur();
1000
+
1001
+ return;
1002
+ }
1003
+
1004
+ /* A filter box must be typeable: every other binding is inert while the
1005
+ caret is in a field. Escape above is the deliberate exception. */
1006
+ if (isTypingTarget(event.target)) return;
1007
+
1008
+ if (pendingJump) {
1009
+ pendingJump = false;
1010
+
1011
+ if (event.key >= '1' && event.key <= '9') {
1012
+ jumpToSection(Number(event.key));
1013
+ event.preventDefault();
1014
+
1015
+ return;
1016
+ }
1017
+ }
1018
+
1019
+ if (event.key === 'g' && sections().length > 0) {
1020
+ pendingJump = true;
1021
+ window.setTimeout(function () {
1022
+ pendingJump = false;
1023
+ }, 1200);
1024
+
1025
+ return;
1026
+ }
1027
+
1028
+ if (event.key === '?') {
1029
+ toggleOverlay(overlay ? overlay.hidden : true);
1030
+ event.preventDefault();
1031
+
1032
+ return;
1033
+ }
1034
+
1035
+ for (var i = 0; i < BINDINGS.length; i++) {
1036
+ if (BINDINGS[i].run && BINDINGS[i].keys.indexOf(event.key) !== -1 && BINDINGS[i].when()) {
1037
+ BINDINGS[i].run();
1038
+ event.preventDefault();
1039
+
1040
+ return;
1041
+ }
1042
+ }
1043
+ }
1044
+
1045
+ function init() {
1046
+ applyTheme(readStored(THEME_KEY) || 'system');
1047
+
1048
+ var buttons = document.querySelectorAll('[data-kit-theme]');
1049
+
1050
+ for (var i = 0; i < buttons.length; i++) {
1051
+ buttons[i].addEventListener('click', function (event) {
1052
+ applyTheme(event.currentTarget.getAttribute('data-kit-theme'));
1053
+ });
1054
+ }
1055
+
1056
+ document.addEventListener('click', function (event) {
1057
+ var target = event.target.closest ? event.target.closest('[data-kit-copy]') : null;
1058
+
1059
+ if (target) copyFrom(target);
1060
+ });
1061
+
1062
+ document.addEventListener('keydown', onKeydown);
1063
+ }
1064
+
1065
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1066
+ else init();
1067
+ })();
1068
+ /* kit:js:end */
1069
+ </script>
1070
+
373
1071
  <script>
374
1072
  // ---- Self-contained Markdown renderer (no external deps) ----
375
1073
  function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');}
@@ -395,13 +1093,26 @@
395
1093
  if(/^#{1,6}\s/.test(line)){const lvl=line.match(/^#+/)[0].length;const text=line.replace(/^#+\s/,'');out.push({html:'<h'+lvl+'>'+inline(text)+'</h'+lvl+'>',heading:{level:lvl,text:text}});i++;continue;}
396
1094
  if(/^---+\s*$/.test(line)){out.push({html:'<hr>',heading:null});i++;continue;}
397
1095
  if(/^\s*>/.test(line)){const buf=[];while(i<lines.length&&/^\s*>/.test(lines[i]))buf.push(lines[i++].replace(/^\s*>\s?/,''));out.push({html:'<blockquote>'+inline(buf.join(' '))+'</blockquote>',heading:null});continue;}
398
- if(/^\s*([-*+]|\d+\.)\s/.test(line)){const ordered=/^\s*\d+\./.test(line);const buf=[];while(i<lines.length&&/^\s*([-*+]|\d+\.)\s/.test(lines[i])){let it=lines[i].replace(/^\s*([-*+]|\d+\.)\s/,'');const cb=it.match(/^\[([ xX])\]\s+/);if(cb){it=it.slice(cb[0].length);buf.push('<li class="task">'+(/[xX]/.test(cb[1])?'☑':'☐')+' '+inline(it)+'</li>');}else{buf.push('<li>'+inline(it)+'</li>');}i++;}out.push({html:'<'+(ordered?'ol':'ul')+'>'+buf.join('')+'</'+(ordered?'ol':'ul')+'>',heading:null});continue;}
1096
+ // Lazy continuation: a soft-wrapped bullet leaves its follow-on lines with no
1097
+ // marker. Advancing only on marker lines ended the list there and rendered the
1098
+ // remainder as a left-margin paragraph — every wrapped bullet in every spec.
1099
+ // The exclusion set is deliberately the paragraph loop's below, not a new one.
1100
+ if(/^\s*([-*+]|\d+\.)\s/.test(line)){const ordered=/^\s*\d+\./.test(line);const buf=[];while(i<lines.length&&/^\s*([-*+]|\d+\.)\s/.test(lines[i])){let it=lines[i].replace(/^\s*([-*+]|\d+\.)\s/,'');i++;while(i<lines.length&&!/^\s*$/.test(lines[i])&&!/^(#{1,6}\s|```|\s*>|\s*([-*+]|\d+\.)\s|---+\s*$)/.test(lines[i])&&!(lines[i].indexOf('|')>=0&&i+1<lines.length&&isDelim(lines[i+1]))){it+=' '+lines[i].trim();i++;}const cb=it.match(/^\[([ xX])\]\s+/);if(cb){it=it.slice(cb[0].length);buf.push('<li class="task">'+(/[xX]/.test(cb[1])?'☑':'☐')+' '+inline(it)+'</li>');}else{buf.push('<li>'+inline(it)+'</li>');}}out.push({html:'<'+(ordered?'ol':'ul')+'>'+buf.join('')+'</'+(ordered?'ol':'ul')+'>',heading:null});continue;}
399
1101
  if(line.indexOf('|')>=0&&i+1<lines.length&&isDelim(lines[i+1])){const header=tcells(line);const aligns=tcells(lines[i+1]).map(function(c){const l=/^:/.test(c),r=/:$/.test(c);return l&&r?'center':r?'right':l?'left':'';});i+=2;const rows=[];while(i<lines.length&&lines[i].indexOf('|')>=0&&!/^\s*$/.test(lines[i])&&!/^(#{1,6}\s|```|---+\s*$)/.test(lines[i])){rows.push(tcells(lines[i]));i++;}let h='<table><thead><tr>';header.forEach(function(c,ci){h+='<th'+(aligns[ci]?' style="text-align:'+aligns[ci]+'"':'')+'>'+inline(c)+'</th>';});h+='</tr></thead><tbody>';rows.forEach(function(cs){h+='<tr>';header.forEach(function(_,ci){h+='<td'+(aligns[ci]?' style="text-align:'+aligns[ci]+'"':'')+'>'+inline(cs[ci]!==undefined?cs[ci]:'')+'</td>';});h+='</tr>';});h+='</tbody></table>';out.push({html:h,heading:null});continue;}
400
1102
  const buf=[];while(i<lines.length&&!/^\s*$/.test(lines[i])&&!/^(#{1,6}\s|```|\s*>|\s*([-*+]|\d+\.)\s|---+\s*$)/.test(lines[i])&&!(lines[i].indexOf('|')>=0&&i+1<lines.length&&isDelim(lines[i+1])))buf.push(lines[i++]);
401
1103
  out.push({html:'<p>'+inline(buf.join(' '))+'</p>',heading:null});}
402
1104
  return out;
403
1105
  }
404
1106
 
1107
+ // ---- API error banner (kit-error) ----
1108
+ function showApiError(msg){
1109
+ const el=document.getElementById('api-error'); if(!el) return;
1110
+ el.querySelector('.msg-text').textContent=msg; el.hidden=false;
1111
+ }
1112
+ function hideApiError(){
1113
+ const el=document.getElementById('api-error'); if(el) el.hidden=true;
1114
+ }
1115
+
405
1116
  // ---- State ----
406
1117
  const annotations = []; let nextId = 1;
407
1118
  const planEl=document.getElementById('plan');
@@ -482,6 +1193,7 @@
482
1193
  node.docs.forEach(function(doc){
483
1194
  const m=matches(doc); if(!m.show) return;
484
1195
  const d=document.createElement('a'); d.href='#'; d.className='doc'+(doc===activeDoc?' active':''); d.dataset.doc=doc; d.title=doc;
1196
+ d.setAttribute('data-kit-item','');
485
1197
  indent(d,depth);
486
1198
  const t=document.createElement('span'); t.className='t'; t.textContent=doc.slice(pathStr?pathStr.length+1:0); d.appendChild(t);
487
1199
  const bd=document.createElement('span'); bd.className='badge badge-warning'; bd.style.display='none'; bd.dataset.fordoc=doc; d.appendChild(bd);
@@ -718,7 +1430,7 @@
718
1430
  'Request Revisions':{cls:'revise', ico:'',wait:true, title:'Revisions requested',sub:'Your notes are on their way. The agent will revise the plan; the updated version reloads here when it’s ready.'},
719
1431
  'Reject': {cls:'deny', ico:'✕',wait:false,title:'Rejected',sub:'Sent to the agent — it will not implement this. It steps back to reconsider whether the feature should be built at all.'},
720
1432
  };
721
- let submitting=false;
1433
+ let submitting=false, decisionSent=false;
722
1434
  function clearAllNotes(){
723
1435
  annotations.slice().forEach(function(a){ if(a.kind==='note') unhighlight(a.id); });
724
1436
  annotations.length=0; nextId=1; paint();
@@ -740,10 +1452,18 @@
740
1452
  async function submit(decision){
741
1453
  if(submitting) return; submitting=true;
742
1454
  document.querySelectorAll('.dbar button').forEach(function(b){b.disabled=true;});
743
- try{ await fetch('/api/feedback',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({decision:decision,thread:annotations})}); }
744
- catch(e){}
745
- clearAllNotes(); // notes clear the moment the decision is sent
746
- showSent(decision);
1455
+ try{
1456
+ const r=await fetch('/api/feedback',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({decision:decision,thread:annotations})});
1457
+ if(!r.ok) throw new Error('status '+r.status);
1458
+ hideApiError();
1459
+ decisionSent=true; // a terminal decision makes the server exit —
1460
+ // the SSE drop that follows is expected, not a fault
1461
+ clearAllNotes(); // notes clear the moment the decision is sent
1462
+ showSent(decision);
1463
+ }catch(e){
1464
+ showApiError('Could not send your decision — the review server may be unreachable. Nothing was sent; try again.');
1465
+ document.querySelectorAll('.dbar button').forEach(function(b){b.disabled=false;});
1466
+ }
747
1467
  submitting=false;
748
1468
  }
749
1469
  document.getElementById('send').onclick=sendChat;
@@ -850,10 +1570,18 @@
850
1570
  tabC.onclick=function(){setTab(false);}; tabF.onclick=function(){setTab(true);};
851
1571
 
852
1572
  // ---- Load + live reload ----
853
- function fetchDoc(doc){ return fetch(DIRMODE?'/api/plan?doc='+encodeURIComponent(doc):'/api/plan').then(function(r){return r.text();}); }
1573
+ // null means "fetch failed" (error banner already shown) and is kept
1574
+ // distinct from '' so a transient failure can never masquerade as a
1575
+ // real empty document and trip the revised-doc diff below.
1576
+ function fetchDoc(doc){
1577
+ return fetch(DIRMODE?'/api/plan?doc='+encodeURIComponent(doc):'/api/plan')
1578
+ .then(function(r){ if(!r.ok) throw new Error('status '+r.status); hideApiError(); return r.text(); })
1579
+ .catch(function(e){ showApiError('Could not load "'+doc+'" — the review server may be unreachable.'); return null; });
1580
+ }
854
1581
  async function reloadDoc(doc){
855
1582
  if(!docState[doc]) return;
856
1583
  const md=await fetchDoc(doc);
1584
+ if(md===null) return; // load failed; state untouched, banner already shown
857
1585
  const st=docState[doc];
858
1586
  if(md===st.md) return; // spurious watch event, no content change
859
1587
  const prev=st.md, revised=prev!==null;
@@ -869,8 +1597,22 @@
869
1597
  }
870
1598
  async function boot(){
871
1599
  let meta={dir:false,docs:['plan']};
872
- try{ meta=await (await fetch('/api/docs')).json(); }catch(e){}
1600
+ try{
1601
+ const r=await fetch('/api/docs');
1602
+ if(!r.ok) throw new Error('status '+r.status);
1603
+ meta=await r.json();
1604
+ hideApiError();
1605
+ }catch(e){ showApiError('Could not reach the review server. Retry once it is back up.'); }
873
1606
  DIRMODE=!!meta.dir; docs=meta.docs||[];
1607
+ // A directory with no markdown at all — nothing to render, nothing to
1608
+ // load per-doc. Show the empty state and stop before the doc loop.
1609
+ if(DIRMODE&&!docs.length){
1610
+ tocEl.innerHTML='';
1611
+ planEl.innerHTML='<div class="kit-empty"><p class="kit-empty__title">No documents to review</p>'
1612
+ +'<p class="kit-empty__body">This directory has no markdown files.</p></div>';
1613
+ paint();
1614
+ return;
1615
+ }
874
1616
  // Accept-or-fix reviews hide the plan-only "Reject / don't build it"
875
1617
  // verdict: directory (spec) review always — a spec documents work already
876
1618
  // decided — and single-file review when the server was started with
@@ -889,22 +1631,31 @@
889
1631
  };
890
1632
  qEl.onkeydown=function(e){ if(e.key==='Escape'){ qEl.value=''; qEl.oninput(); qEl.blur(); } };
891
1633
  }
892
- planEl.innerHTML='';
1634
+ // Panes are built off-DOM and only swapped in once every doc has
1635
+ // resolved, so the .kit-skel placeholders already in #plan stay
1636
+ // visible for the whole fetch instead of being wiped to blank first.
1637
+ const frag=document.createDocumentFragment();
893
1638
  docs.forEach(function(d,i){
894
1639
  const pane=document.createElement('div'); pane.className='docpane'; pane.style.display='none';
895
- planEl.appendChild(pane);
1640
+ frag.appendChild(pane);
896
1641
  docState[d]={md:null,pane:pane,scroll:0,key:'d'+i,headings:[]};
897
1642
  });
898
- for(let i=0;i<docs.length;i++){ const d=docs[i]; renderDocPane(d,await fetchDoc(d)); }
1643
+ for(let i=0;i<docs.length;i++){ const d=docs[i]; const md=await fetchDoc(d); if(md!==null) renderDocPane(d,md); }
1644
+ planEl.innerHTML=''; planEl.appendChild(frag);
899
1645
  if(!DIRMODE||!docs.length) buildToc(); // DIRMODE: switchDoc below builds the tree once, seeded
900
1646
  paint();
901
1647
  if(docs.length) switchDoc(docs[0]);
902
1648
  buildFilesIndex();
903
1649
  }
904
- try{const es=new EventSource('/api/events');es.onmessage=function(ev){
905
- const m=String((ev&&ev.data)||''); const doc=m.indexOf('change:')===0?m.slice(7):docs[0];
906
- reloadDoc(doc);
907
- };}catch(e){}
1650
+ try{
1651
+ const es=new EventSource('/api/events');
1652
+ es.onmessage=function(ev){
1653
+ hideApiError();
1654
+ const m=String((ev&&ev.data)||''); const doc=m.indexOf('change:')===0?m.slice(7):docs[0];
1655
+ reloadDoc(doc);
1656
+ };
1657
+ es.onerror=function(){ if(!decisionSent) showApiError('Live connection to the review server was lost — revised docs will not auto-reload. Your notes are safe.'); };
1658
+ }catch(e){ showApiError('Live reload is unavailable in this browser.'); }
908
1659
  boot();
909
1660
  </script>
910
1661
  </body>