@jabworks/condux 0.13.3 → 0.14.1

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.13.3",
3
+ "version": "0.14.1",
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": {
@@ -372,16 +372,21 @@
372
372
  /* A keyboard layer nobody can discover is a keyboard layer nobody uses. kit.js
373
373
  places this next to every theme group, so the ? overlay has a visible way in
374
374
  for anyone who was never told the shortcut exists. */
375
- /* kit.js wraps the theme group and the help button in this. The theme group
376
- keeps growing where its host stretched it (the guide's rail), and stays
377
- content-sized where the host did not (the board's action row). */
375
+ /* kit.js wraps the theme group and the help button in this. The pairing is
376
+ content-sized, so ? sits against the group no matter what the host is: in a
377
+ flex row the wrapper shrink-wraps anyway, and in block flow (a sidebar) the
378
+ wrapper fills the width but the group no longer stretches across it. Growing
379
+ the group is opt-in — a host that wants it to fill the row says so itself,
380
+ the way the style guide's rail does with `.kit-controls > .themebar`. This
381
+ was the default until 2026-08-25, and the one surface hosting the group in
382
+ block flow (plan-review's nav) got a ? at the far edge (docket #52). */
378
383
  .kit-controls {
379
384
  display: flex;
380
385
  align-items: center;
381
386
  }
382
387
 
383
388
  .kit-controls > .kit-theme {
384
- flex: 1 1 auto;
389
+ flex: none;
385
390
  }
386
391
 
387
392
  .kit-helpbtn {
@@ -476,12 +481,21 @@
476
481
  the smaller 0.25rem step now say so explicitly via --radius, which
477
482
  already carries that value and is already this file's convention
478
483
  (.nav .tabs, .popover, .chip, code, pre, …). */
484
+ /* Docket #48 / Q16 — three theme states, not two. These tokens are
485
+ dark-first like the core, so their light values are an override. Written
486
+ as a bare media query, clicking Dark on a light system flipped every core
487
+ token and left the highlight cream, because the media query still
488
+ matched. Written the way scripts/tokens/core.css:84-85,130 writes it:
489
+ the system-light block excludes an explicit dark stamp, and an explicit
490
+ light block beats a dark OS. */
479
491
  :root {
480
492
  --hl: rgba(255, 202, 22, 0.30); --hl-active: rgba(255, 202, 22, 0.55);
481
493
  }
482
494
  @media (prefers-color-scheme: light) {
483
- :root { --hl: rgba(234, 179, 8, 0.42); --hl-active: rgba(202, 138, 4, 0.62); }
495
+ :root:not([data-theme="dark"]) { --hl: rgba(234, 179, 8, 0.42); --hl-active: rgba(202, 138, 4, 0.62); }
484
496
  }
497
+ :root[data-theme="light"] { --hl: rgba(234, 179, 8, 0.42); --hl-active: rgba(202, 138, 4, 0.62); }
498
+ :root[data-theme="dark"] { --hl: rgba(255, 202, 22, 0.30); --hl-active: rgba(255, 202, 22, 0.55); }
485
499
  * { box-sizing: border-box; }
486
500
  html, body { height: 100%; }
487
501
  /* .kit-error sets display:flex unconditionally; an author rule beats the
@@ -493,9 +507,16 @@
493
507
  one legitimately 2-D layout in this file; everything else here is a
494
508
  one-dimensional row or column and stays flex. */
495
509
  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; }
496
- .app { display: grid; grid-template-columns: 248px minmax(0, 1fr) 372px; min-height: 0; }
510
+ /* D10 — the document takes the width the two lists were spending. Every
511
+ column rule is scoped ABOVE the stacking breakpoint on purpose: an
512
+ unscoped `grid-template-columns` outranks the 860px and 1100px rules by
513
+ source order, and the three-column grid survives into a phone viewport.
514
+ Measured at 640px while building D10: `.main` collapsed to 88px and the
515
+ 340px popover ran off the right edge of the screen. */
516
+ .app { display: grid; grid-template-columns: 236px minmax(0, 1fr) 330px; min-height: 0; }
517
+ .app > * { min-width: 0; } /* Q14 — a bare 1fr column floors at min-content */
497
518
  @media (max-width: 1100px) {
498
- .app { grid-template-columns: 208px minmax(0, 1fr) 320px; }
519
+ .app { grid-template-columns: 208px minmax(0, 1fr) 300px; }
499
520
  }
500
521
  @media (max-width: 860px) {
501
522
  .app { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; max-height: none; }
@@ -537,9 +558,17 @@
537
558
  .badge-warning { background: var(--warning-muted); color: var(--warning); border-color: var(--warning-border); }
538
559
  .badge-soft { background: var(--primary-muted); color: var(--primary-text); }
539
560
 
540
- /* Left: navigation — Contents (docs + headings) | Files (paths touched) */
541
- .nav { border-right: 1px solid var(--border); background: var(--card); overflow-y: auto; padding: 18px 14px 40px; }
542
- .nav .kit-theme { margin: 0 0 12px; }
561
+ /* Left: navigation — Contents (docs + headings) | Files (paths touched).
562
+ D10 — a table of contents rather than a menu: no card ground, no active
563
+ pill, depth as padding. Note `.nav a` is display:flex, so a negative
564
+ text-indent does not produce a hanging indent here — it shifts the flex
565
+ container's first line and clips the label against the row. */
566
+ .nav { border-right: 1px solid var(--border); background: var(--background); overflow-y: auto; padding: 22px 16px 40px; }
567
+ /* Space the whole theme+? pairing, not the group inside it. .kit-controls
568
+ centres its children on the cross axis and flex centres the MARGIN box,
569
+ so a bottom margin here would ride the group up ~6px while ? stayed put
570
+ (docket #52). */
571
+ .nav .kit-controls { margin: 0 0 12px; }
543
572
  .nav .tabs { display: flex; margin: 0 0 12px; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
544
573
  .nav .tab { flex: 1; font: 500 var(--text-2xs) var(--sans); letter-spacing: .08em; text-transform: uppercase; padding: 6px 4px;
545
574
  background: transparent; border: none; border-right: 1px solid var(--border); color: var(--subtle); cursor: pointer; }
@@ -547,16 +576,16 @@
547
576
  .nav .tab:hover { color: var(--foreground); }
548
577
  .nav .tab.active { background: var(--accent); color: var(--foreground); }
549
578
  .nav a.doc { font-weight: 600; color: var(--foreground); }
550
- .nav a.doc.active { border-left-color: var(--primary); background: var(--accent); }
579
+ .nav a.doc.active { color: var(--foreground); background: transparent; }
551
580
  .nav a.file .t { font-family: var(--mono); font-size: var(--text-xs); }
552
581
  .nav .none { color: var(--subtle); font-size: var(--text-xs); padding: 4px 6px; }
553
582
  code.mention-flash { outline: 2px solid var(--warning); outline-offset: 1px; }
554
583
  .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; }
555
- .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; }
584
+ .nav a { display: flex; align-items: center; gap: 6px; color: var(--muted-foreground); text-decoration: none; padding: 3px 2px; border-radius: 0; border-left: none; }
556
585
  .nav a span.t { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
557
- .nav a:hover { background: var(--accent); color: var(--foreground); }
558
- .nav a.active { color: var(--foreground); border-left-color: var(--primary); background: var(--accent); }
559
- .nav a.lvl2 { padding-left: 16px; } .nav a.lvl3 { padding-left: 28px; font-size: var(--text-xs); color: var(--subtle); }
586
+ .nav a:hover { color: var(--foreground); }
587
+ .nav a.active { color: var(--foreground); font-weight: 600; background: transparent; }
588
+ .nav a.lvl2 { padding-left: 14px; } .nav a.lvl3 { padding-left: 26px; font-size: var(--text-xs); color: var(--subtle); }
560
589
 
561
590
  /* Directory mode: doc-site tree — folder rows fold, filter narrows, the
562
591
  active doc alone carries its headings. All of it DIRMODE-gated. */
@@ -566,14 +595,14 @@
566
595
  .nav .tree-filter input:focus { border-color: var(--ring); }
567
596
  .nav a.folder { font-weight: 600; color: var(--muted-foreground); }
568
597
  .nav a.folder .arrow { flex: none; width: 12px; color: var(--subtle); font-size: var(--text-2xs); }
569
- .crumbs { display: none; max-width: 760px; margin: 0 auto 18px; font: var(--text-xs) var(--mono); color: var(--subtle);
598
+ .crumbs { display: none; max-width: 720px; margin: 0 auto 18px; font: var(--text-xs) var(--mono); color: var(--subtle);
570
599
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
571
600
  .crumbs .seg { color: var(--muted-foreground); }
572
601
  .crumbs a.seg { color: var(--muted-foreground); text-decoration: none; cursor: pointer; }
573
602
  .crumbs a.seg:hover { color: var(--foreground); text-decoration: underline; }
574
603
  .crumbs .sep { margin: 0 6px; color: var(--subtle); }
575
604
  .crumbs .cur { color: var(--foreground); }
576
- .pnav { display: none; max-width: 760px; margin: 32px auto 0; padding-top: 14px; border-top: 1px solid var(--border);
605
+ .pnav { display: none; max-width: 720px; margin: 32px auto 0; padding-top: 14px; border-top: 1px solid var(--border);
577
606
  justify-content: space-between; gap: 12px; }
578
607
  .pnav a { display: inline-flex; align-items: center; gap: 6px; max-width: 48%; color: var(--muted-foreground);
579
608
  text-decoration: none; font-size: var(--text-sm); padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--radius);
@@ -581,23 +610,23 @@
581
610
  .pnav a:hover { background: var(--accent); color: var(--foreground); }
582
611
  .pnav .spacer { flex: 1; }
583
612
 
584
- /* Center: plan */
585
- .main { overflow-y: auto; padding: 28px 44px 120px; position: relative;
586
- background-image:
587
- linear-gradient(to right, color-mix(in oklab, var(--foreground) 3%, transparent) 1px, transparent 1px),
588
- linear-gradient(to bottom, color-mix(in oklab, var(--foreground) 3%, transparent) 1px, transparent 1px);
589
- background-size: 32px 32px; }
590
- .main .hint { color: var(--subtle); margin: 0 auto 24px; font-size: var(--text-xs); max-width: 760px; }
591
- .doc { max-width: 760px; margin: 0 auto; line-height: var(--leading-relaxed); }
613
+ /* Center: plan. `position: relative` and `overflow-y: auto` are load-bearing
614
+ and must both stay: #popover is absolutely positioned against this box,
615
+ and selDoc stores coordinates relative to it plus its scrollTop. Moving
616
+ the scroll to body, or wrapping #plan in its own scroller, silently
617
+ breaks every annotation anchor. */
618
+ .main { overflow-y: auto; padding: 40px 44px 140px; position: relative; }
619
+ .main .hint { color: var(--subtle); margin: 0 auto 24px; font-size: var(--text-xs); max-width: 720px; }
620
+ .doc { max-width: 720px; margin: 0 auto; line-height: var(--leading-relaxed); counter-reset: hl; }
592
621
 
593
622
  /* Revision diff — on a revised plan, show what changed instead of a silent swap. */
594
- .revbar { display: none; align-items: center; gap: 10px; max-width: 760px; margin: 0 auto 20px;
623
+ .revbar { display: none; align-items: center; gap: 10px; max-width: 720px; margin: 0 auto 20px;
595
624
  padding: 9px 12px; border: 1px solid var(--warning-border); background: var(--warning-muted); border-radius: var(--radius); }
596
625
  .revbar.show { display: flex; }
597
626
  .revbar .rb-text { color: var(--warning); font: 600 var(--text-xs) var(--sans); }
598
627
  .revbar .rb-stat { color: var(--muted-foreground); font-size: var(--text-xs); }
599
628
  .revbar .rb-actions { margin-left: auto; display: flex; gap: 6px; }
600
- .diffwrap { display: none; max-width: 760px; margin: 0 auto; }
629
+ .diffwrap { display: none; max-width: 720px; margin: 0 auto; }
601
630
  .diffwrap.show { display: block; }
602
631
  .diffview { background: var(--muted); border: 1px solid var(--border); border-radius: var(--radius);
603
632
  padding: 8px 0; overflow-x: auto; font: var(--text-xs)/var(--leading-normal) var(--mono); }
@@ -609,11 +638,24 @@
609
638
  .diffline.del { background: var(--destructive-muted); color: var(--destructive); }
610
639
  .diffline.del .g { color: var(--destructive); }
611
640
  .diffline.fold { justify-content: center; color: var(--subtle); font-style: italic; padding: 3px 12px; }
641
+ /* D10 — a typographic spine. Hierarchy is space and rule, not boxes: h1
642
+ closes with a rule, h2 opens a section against a hairline, h3 hangs off
643
+ a short dash. This is what makes a long plan skimmable without folding
644
+ it — folds are ruled out on this surface (live Ranges and mark.hl nodes
645
+ have no client rects inside a collapsed <details>). */
612
646
  h1, h2, h3, h4, h5, h6 { line-height: var(--leading-snug); margin: 1.5em 0 0.6em; scroll-margin-top: 24px; }
613
- h1 { font-size: var(--text-xl); } h2 { font-size: var(--text-md); } h3 { font-size: var(--text-base); color: var(--muted-foreground); }
647
+ h1 { font-size: var(--text-2xl); letter-spacing: -0.015em; margin: 0 0 0.2em; padding-bottom: 0.5em; border-bottom: 1px solid var(--border); }
648
+ h2 { font-size: var(--text-lg); letter-spacing: -0.01em; margin: 2.4em 0 0.7em; padding-top: 0.9em; border-top: 1px solid var(--border); }
649
+ h3 { font-size: var(--text-base); color: var(--foreground); margin: 1.9em 0 0.5em; }
650
+ h3::before { content: ''; display: inline-block; width: 14px; height: 1px; vertical-align: middle; margin-right: 10px; background: var(--subtle); }
614
651
  h4 { font-size: var(--text-sm); } h5, h6 { font-size: var(--text-xs); color: var(--muted-foreground); }
652
+ .doc > *:first-child, .docpane > *:first-child { margin-top: 0; }
615
653
  p { margin: 0.6em 0; }
616
654
  ul, ol { margin: 0.6em 0; padding-left: 22px; } li { margin: 0.25em 0; }
655
+ /* Q15 — capping measure on <p> alone misses the worst line, and a plan is
656
+ mostly list items. Table cells keep their column. */
657
+ .doc p, .doc li { max-width: 68ch; }
658
+ .doc td p, .doc td li, .doc th p { max-width: none; }
617
659
  li.task { list-style: none; margin-left: -18px; }
618
660
  del { color: var(--subtle); }
619
661
  a { color: var(--info); }
@@ -622,12 +664,32 @@
622
664
  pre code { background: none; border: none; padding: 0; }
623
665
  blockquote { margin: 0.6em 0; padding: 2px 14px; color: var(--muted-foreground); border-left: 3px solid var(--primary); }
624
666
  hr { border: none; border-top: 1px solid var(--border); margin: 1.6em 0; }
667
+ /* D10 — h2 now opens a section against its own rule, which makes a
668
+ preceding `---` a second divider saying the same thing. draft-plan's
669
+ canonical template puts `---` immediately before every task card, so
670
+ this is the shape of nearly every plan this surface renders: measured
671
+ 9 doubled rules in one 9-task plan. Each block is wrapped in its own
672
+ .blk, so `hr + h2` cannot match — the relative selector looks forward
673
+ to the next block instead. The h2's rule is the spine and stays; the
674
+ redundant separator goes. */
675
+ .blk:has(> hr):has(+ .blk h2) { display: none; }
625
676
  table { border-collapse: collapse; margin: 0.9em 0; width: 100%; display: block; overflow-x: auto; font-size: var(--text-sm); }
626
677
  th, td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; vertical-align: top; }
627
678
  th { background: var(--muted); font-weight: 600; }
628
679
  tbody tr:nth-child(even) { background: color-mix(in oklch, var(--muted) 40%, transparent); }
629
680
  .blk { border-radius: 4px; padding: 1px 8px; margin: 0 -8px; }
630
- mark.hl { background: var(--hl); color: inherit; border-radius: 2px; cursor: pointer; padding: 0 1px; }
681
+ /* D10 — numbered marginalia. The counter increments per highlight in
682
+ document order and the review column repeats the same ordinal in its
683
+ gutter, so the two columns read as one artifact. paint() orders the
684
+ thread by document position to keep the two numberings in agreement —
685
+ without that they diverge the moment a note is added above an earlier
686
+ one. The counter is also the only trace of an annotation on paper, since
687
+ the review column is print-suppressed. */
688
+ mark.hl { background: var(--hl); color: inherit; border-radius: 2px; cursor: pointer; padding: 0.1em 1px;
689
+ box-decoration-break: clone; -webkit-box-decoration-break: clone; counter-increment: hl; }
690
+ mark.hl::after { content: counter(hl); font: 600 var(--text-2xs) var(--mono); color: var(--muted-foreground);
691
+ vertical-align: super; margin-left: 2px; line-height: 0; }
692
+ mark.hl:hover { background: var(--hl-active); }
631
693
  mark.hl.active { background: var(--hl-active); box-shadow: 0 0 0 1px var(--warning-border); }
632
694
 
633
695
  /* Floating "comment" button (Button surface) + comment Popover */
@@ -664,20 +726,51 @@
664
726
  .popover .ta { min-height: 132px; border: none; border-radius: 0; padding: 8px 10px; } .popover .ta:focus-visible { outline: none; box-shadow: none; }
665
727
  .composer .ta { min-height: 52px; }
666
728
 
667
- /* Right: annotations (Cards) + composer + decisions */
668
- .chat { border-left: 1px solid var(--border); background: var(--card); display: flex; flex-direction: column; }
669
- .chat .label { padding: 16px 16px 0; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
670
- .thread { flex: 1; overflow-y: auto; padding: 6px 14px; display: flex; flex-direction: column; gap: 8px; }
729
+ /* Right: annotations as marginalia + composer + decisions.
730
+ D10 — the review column recedes so the document can lead. Notes lose
731
+ their card chrome and are separated by rule alone, each keyed by the
732
+ same ordinal its highlight carries in the document. */
733
+ .chat { border-left: 1px solid var(--border); background: var(--background); display: flex; flex-direction: column; }
734
+ .chat .label { padding: 22px 18px 0; margin-bottom: 4px; display: flex; align-items: center; gap: 8px;
735
+ font: 600 var(--text-2xs) var(--sans); letter-spacing: .09em; text-transform: uppercase; color: var(--subtle); }
736
+ .thread { flex: 1; overflow-y: auto; padding: 10px 18px; display: flex; flex-direction: column; gap: 0; counter-reset: note; }
671
737
  .thread .empty { color: var(--subtle); padding: 8px 4px; }
672
- /* Terminus: Card */
673
- .msg { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; background: var(--card); box-shadow: var(--shadow-sm); cursor: default; }
674
- .msg.note { border-color: var(--warning-border); cursor: pointer; }
675
- .msg.note.active { box-shadow: 0 0 0 2px var(--warning); }
738
+ .msg { position: relative; background: transparent; border: none; border-top: 1px solid var(--border);
739
+ border-left: 2px solid transparent; border-radius: 0; padding: 14px 0 14px 26px; box-shadow: none; cursor: default; }
740
+ .msg:first-child { border-top: none; }
741
+ .msg::before { counter-increment: note; content: counter(note); position: absolute; left: 8px; top: 15px;
742
+ font: 600 var(--text-2xs) var(--mono); color: var(--subtle); }
743
+ /* A note on another document takes no ordinal — see paint(). The marker
744
+ says "annotated, elsewhere" without claiming a position in a sequence
745
+ the reader cannot see. */
746
+ .msg[data-other-doc]::before { counter-increment: none; content: '·'; }
747
+ .msg.note { cursor: pointer; }
748
+ .msg.note.active { background: var(--accent); }
676
749
  .msg .meta { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 4px; align-items: center; }
677
- .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); }
750
+ .msg .who { color: var(--muted-foreground); font: 600 var(--text-2xs) var(--sans); text-transform: uppercase; letter-spacing: .07em; }
678
751
  .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); }
679
- .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; }
680
- .msg .body { white-space: pre-wrap; word-break: break-word; color: var(--card-foreground); }
752
+ .msg .quote { color: var(--muted-foreground); font-size: var(--text-2xs); font-style: italic; border-left: 2px solid var(--hl-active); padding: 1px 9px; margin-bottom: 6px; max-height: 42px; overflow: hidden; }
753
+ .msg .body { white-space: pre-wrap; word-break: break-word; color: var(--foreground); }
754
+
755
+ /* Category as colour, carried by the two marks already in the gutter — the
756
+ ordinal and the label — rather than by a card edge. Lets you count the
757
+ issues in a full thread without reading it. `data-cat` is set in paint();
758
+ the category is otherwise only written as TEXT into .who, and CSS cannot
759
+ select on text content. */
760
+ /* The five categories are exactly the popover's chips — Comment, Issue,
761
+ Question, Suggestion, Nitpick — plus "Note", which is what a note gets
762
+ when no chip was clicked (openToolbar clears pendingCat on every open,
763
+ so the markup's initial `active` chip is not a default). Comment and
764
+ Note are deliberately quiet: they are the common case, and colouring
765
+ them would leave nothing for the categories that carry a decision. */
766
+ .msg[data-cat="Issue"] { border-left-color: var(--destructive); }
767
+ .msg[data-cat="Issue"]::before, .msg[data-cat="Issue"] .who { color: var(--destructive); }
768
+ .msg[data-cat="Question"] { border-left-color: var(--warning); }
769
+ .msg[data-cat="Question"]::before, .msg[data-cat="Question"] .who { color: var(--warning); }
770
+ .msg[data-cat="Suggestion"] { border-left-color: var(--primary); }
771
+ .msg[data-cat="Suggestion"]::before, .msg[data-cat="Suggestion"] .who { color: var(--primary-text); }
772
+ .msg[data-cat="Nitpick"] { border-left-color: var(--subtle); }
773
+ .msg[data-cat="Nitpick"]::before, .msg[data-cat="Nitpick"] .who { color: var(--subtle); }
681
774
 
682
775
  /* Terminus: Separator + composer */
683
776
  .composer { border-top: 1px solid var(--border); padding: 12px 14px; }
@@ -709,6 +802,22 @@
709
802
  .sent .again { margin-top: 10px; }
710
803
  .dot { width: 7px; height: 7px; border-radius: 9999px; background: currentColor; animation: pulse 1.3s ease-in-out infinite; }
711
804
  @keyframes pulse { 0%, 100% { opacity: .3; } 50% { opacity: 1; } }
805
+
806
+ /* Docket #50 — the shell has to COLLAPSE for print, not just hide its
807
+ chrome. The kit's print block sets [data-kit-chrome] { display: none },
808
+ which hides .nav and .chat but leaves their grid TRACKS on .app, so
809
+ .main auto-placed into the first (248px) track and the plan printed in a
810
+ ~151px column. .main is also the scroll container, so printed output was
811
+ clipped to the one screenful scrolled into view — the rest of the plan
812
+ did not print at all.
813
+ The measure cap stays: prose set to the full page width is no more
814
+ readable on paper than on screen. Only the centring margin goes, so the
815
+ column sits against the page's own margin. */
816
+ @media print {
817
+ .app { display: block; }
818
+ .main { overflow: visible; padding: 0; height: auto; }
819
+ .doc, .crumbs { margin: 0; }
820
+ }
712
821
  </style>
713
822
  </head>
714
823
  <body>
@@ -1473,12 +1582,47 @@
1473
1582
  function sendChat(){ const ta=document.getElementById('input'); const text=ta.value.trim(); if(!text)return;
1474
1583
  annotations.push({id:nextId++,kind:'chat',quote:'',text:text,section:''}); ta.value=''; paint(); }
1475
1584
 
1585
+ // The thread renders in DOCUMENT order, not insertion order, because each
1586
+ // highlight carries a CSS counter numbered by document position and the
1587
+ // gutter repeats that ordinal. Left in insertion order the two numberings
1588
+ // disagree the moment a note is added above an earlier one.
1589
+ //
1590
+ // Sort a COPY: `annotations` is the identity store that saveNote pushes to
1591
+ // and the delete handler indexes into with indexOf, so reordering it in
1592
+ // place breaks both. And never reorder the rendered DOM instead — chat
1593
+ // messages have no document position and would be shuffled to the top.
1594
+ function threadOrder(){
1595
+ // Scope the mark lookup to the ACTIVE pane. In DIRMODE every doc's pane
1596
+ // stays in the DOM hidden, so a document-wide query happily matches a
1597
+ // mark in another document's pane and sorts against it.
1598
+ const pane=docState[activeDoc]&&docState[activeDoc].pane;
1599
+ const markOf=function(a){ return pane?pane.querySelector('mark.hl[data-aid="'+a.id+'"]'):null; };
1600
+ // Array.prototype.sort is stable, so a 0 comparison keeps chat messages
1601
+ // — and any note whose mark is missing — in their existing order.
1602
+ return annotations.slice().sort(function(x,y){
1603
+ if(x.kind!=='note'||y.kind!=='note') return 0;
1604
+ const mx=markOf(x), my=markOf(y);
1605
+ if(!mx||!my) return 0;
1606
+ return (mx.compareDocumentPosition(my)&Node.DOCUMENT_POSITION_FOLLOWING)?-1:1;
1607
+ });
1608
+ }
1476
1609
  function paint(){
1477
1610
  const box=document.getElementById('thread'); box.innerHTML='';
1478
1611
  document.getElementById('count').textContent=annotations.length?'('+annotations.length+')':'';
1479
1612
  if(!annotations.length){ box.innerHTML='<div class="empty">No notes yet. Select text to comment, or send a message.</div>'; updateBadges(); updateSummary(); return; }
1480
- annotations.forEach(function(a){
1613
+ threadOrder().forEach(function(a){
1481
1614
  const m=document.createElement('div'); m.className='msg'+(a.kind==='note'?' note':''); if(a.kind==='note')m.dataset.aid=String(a.id);
1615
+ // The category is otherwise written only as TEXT into .who, and CSS
1616
+ // cannot select on text content — this is what the colour rules key on.
1617
+ m.dataset.cat=(a.kind==='note'?(a.cat||'Note'):'message');
1618
+ // DIRMODE: the thread lists notes from EVERY document, but the in-document
1619
+ // ordinal counts only the visible pane — marks inside a display:none pane
1620
+ // do not increment a CSS counter. Numbering every row would therefore
1621
+ // label a note "3" in the gutter whose highlight reads "1" in the
1622
+ // document, which is exactly the divergence the numbering exists to
1623
+ // remove. Rows belonging to another document are flagged so they take no
1624
+ // ordinal, leaving both sequences running 1..m over the same set.
1625
+ if(a.kind==='note'&&a.doc&&a.doc!==activeDoc) m.dataset.otherDoc='1';
1482
1626
  const meta=document.createElement('div'); meta.className='meta';
1483
1627
  const who=document.createElement('span'); who.className='who'; who.textContent=a.kind==='note'?(a.cat||'Note'):'message';
1484
1628
  const del=document.createElement('button'); del.className='del'; del.textContent='×';
@@ -1552,7 +1696,12 @@
1552
1696
  if(submitting) return; submitting=true;
1553
1697
  document.querySelectorAll('.dbar button').forEach(function(b){b.disabled=true;});
1554
1698
  try{
1555
- const r=await fetch('/api/feedback',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({decision:decision,thread:annotations})});
1699
+ // Send the thread in the SAME order the reviewer saw it. The rendered
1700
+ // thread is ordered by document position and each note is numbered in
1701
+ // the gutter; posting the raw insertion-order array would hand the
1702
+ // agent a different numbering than the one on screen, so "note 1" in a
1703
+ // conversation would mean two different notes.
1704
+ const r=await fetch('/api/feedback',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({decision:decision,thread:threadOrder()})});
1556
1705
  if(!r.ok) throw new Error('status '+r.status);
1557
1706
  hideApiError();
1558
1707
  decisionSent=true; // a terminal decision makes the server exit —