@jabworks/condux 0.8.2 → 0.9.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.8.2",
3
+ "version": "0.9.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": {
@@ -15,7 +15,7 @@ import { spawnSync } from 'node:child_process';
15
15
  import { fileURLToPath } from 'node:url';
16
16
 
17
17
  const SKILL_BASE = path.dirname(fileURLToPath(import.meta.url));
18
- const PLUGIN_ROOT = path.resolve(SKILL_BASE, '..', '..', '..');
18
+ const PLUGIN_ROOT = path.resolve(SKILL_BASE, '..', '..');
19
19
  const HOME = os.homedir();
20
20
  const EXEC_TIMEOUT = 5000;
21
21
  const PACKAGE = '@jabworks/condux';
@@ -45,9 +45,9 @@ the test suite; also right for headless/CI runs). Use this for ad-hoc review of
45
45
  are not in plan mode. After submitting, read `<file>.feedback.md` to action it.
46
46
 
47
47
  **Directory mode** reviews a whole spec folder (e.g. `specs/wan-config/` from
48
- the `technical-spec` skill): every `*.md` in the tree is listed in the sidebar
49
- (grouped by folder, `index.md` first, dotdirs skipped),
50
- notes are tagged with their source file, edits to any file live-reload (with a
48
+ the `technical-spec` skill): every `*.md` in the tree appears in a doc-site
49
+ sidebar — collapsible folders (`index.md` first, dotdirs skipped), a filter
50
+ over doc names and headings, breadcrumb and prev/next — notes are tagged with their source file, edits to any file live-reload (with a
51
51
  per-file revision diff), and the decision lands in `<dir>/review.feedback.md`
52
52
  grouped by file. Works in manual and `--steer` modes; this is also the live
53
53
  preview surface for `technical-spec` (its old preview server is retired).
@@ -132,8 +132,7 @@
132
132
  .nav .tab:last-child { border-right: none; }
133
133
  .nav .tab:hover { color: var(--foreground); }
134
134
  .nav .tab.active { background: var(--accent); color: var(--foreground); }
135
- .nav .group { font: 600 10px var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--subtle); margin: 14px 6px 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
136
- .nav a.doc { font-weight: 600; color: var(--foreground); margin-top: 10px; }
135
+ .nav a.doc { font-weight: 600; color: var(--foreground); }
137
136
  .nav a.doc.active { border-left-color: var(--primary); background: var(--accent); }
138
137
  .nav a.file .t { font-size: 12px; }
139
138
  .nav .none { color: var(--subtle); font-size: 12px; padding: 4px 6px; }
@@ -145,6 +144,29 @@
145
144
  .nav a.active { color: var(--foreground); border-left-color: var(--primary); background: var(--accent); }
146
145
  .nav a.lvl2 { padding-left: 16px; } .nav a.lvl3 { padding-left: 28px; font-size: 12px; color: var(--subtle); }
147
146
 
147
+ /* Directory mode: doc-site tree — folder rows fold, filter narrows, the
148
+ active doc alone carries its headings. All of it DIRMODE-gated. */
149
+ .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);
151
+ border: 1px solid var(--border); border-radius: var(--radius); padding: 6px 8px; outline: none; }
152
+ .nav .tree-filter input:focus { border-color: var(--ring); }
153
+ .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);
156
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
157
+ .crumbs .seg { color: var(--muted-foreground); }
158
+ .crumbs a.seg { color: var(--muted-foreground); text-decoration: none; cursor: pointer; }
159
+ .crumbs a.seg:hover { color: var(--foreground); text-decoration: underline; }
160
+ .crumbs .sep { margin: 0 6px; color: var(--subtle); }
161
+ .crumbs .cur { color: var(--foreground); }
162
+ .pnav { display: none; max-width: 760px; margin: 32px auto 0; padding-top: 14px; border-top: 1px solid var(--border);
163
+ justify-content: space-between; gap: 12px; }
164
+ .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);
166
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
167
+ .pnav a:hover { background: var(--accent); color: var(--foreground); }
168
+ .pnav .spacer { flex: 1; }
169
+
148
170
  /* Center: plan */
149
171
  .main { overflow-y: auto; padding: 28px 44px 120px; position: relative;
150
172
  background-image:
@@ -282,7 +304,10 @@
282
304
  <button type="button" class="tab active" id="tab-contents">Contents</button>
283
305
  <button type="button" class="tab" id="tab-files">Files</button>
284
306
  </div>
285
- <div id="pane-contents"><nav id="toc"></nav></div>
307
+ <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>
310
+ </div>
286
311
  <div id="pane-files" style="display:none"><nav id="filelist"></nav></div>
287
312
  </aside>
288
313
 
@@ -296,7 +321,9 @@
296
321
  <button type="button" class="btn btn-ghost btn-sm" id="rb-dismiss">Dismiss</button>
297
322
  </span>
298
323
  </div>
324
+ <div class="crumbs" id="crumbs"></div>
299
325
  <div class="doc" id="plan">Loading plan…</div>
326
+ <div class="pnav" id="pnav"></div>
300
327
  <div class="diffwrap" id="diffwrap"><div class="diffview" id="diffview"></div></div>
301
328
  <div class="popover" id="popover">
302
329
  <div class="q" id="pop-quote"></div>
@@ -411,42 +438,123 @@
411
438
  });
412
439
  }
413
440
 
414
- function buildToc(){
415
- tocEl.innerHTML='';
416
- var lastGroup=null;
441
+ // ---- Directory-mode doc-site tree: collapsible folders, filter, chrome ----
442
+ // Collapse state lives in memory only (review tabs are short-lived); seeded
443
+ // so the active doc's ancestors are always open. The filter is a transient
444
+ // view — it never touches openFolders (snapshot restored on clear) and
445
+ // never affects what gets submitted.
446
+ let openFolders=new Set(), treeFilter='', preFilterOpen=null;
447
+ function parentOf(p){ const s=p.lastIndexOf('/'); return s>=0?p.slice(0,s):''; }
448
+ function seedOpen(doc){ let f=parentOf(doc); while(f){ openFolders.add(f); f=parentOf(f); } }
449
+ function headingAnchor(doc,h,lvl){
450
+ const a=document.createElement('a'); a.href='#'+h.id;
451
+ a.className='lvl'+lvl; a.dataset.target=h.id; a.dataset.doc=doc;
452
+ const t=document.createElement('span'); t.className='t'; t.textContent=h.text; a.appendChild(t);
453
+ const bd=document.createElement('span'); bd.className='badge badge-warning'; bd.style.display='none'; bd.dataset.for=h.id; a.appendChild(bd);
454
+ a.onclick=function(e){ e.preventDefault(); if(doc!==activeDoc)switchDoc(doc);
455
+ document.getElementById(h.id).scrollIntoView({behavior:'smooth',block:'start'}); };
456
+ return a;
457
+ }
458
+ function renderTree(){
459
+ const q=treeFilter.trim().toLowerCase(), mcache={};
460
+ // docs[] arrives folder-grouped (root first, index.md first per folder);
461
+ // rebuilding the tree and walking docs-then-sorted-dirs reproduces that
462
+ // order exactly — docs[] stays the one source of truth.
463
+ const root={dirs:{},docs:[]};
417
464
  docs.forEach(function(doc){
418
- const st=docState[doc];
419
- if(DIRMODE){
420
- const slash=doc.lastIndexOf('/');
421
- const group=slash>=0?doc.slice(0,slash):'';
422
- if(group && group!==lastGroup){ const g=document.createElement('div'); g.className='group'; g.title=group; g.textContent=group; tocEl.appendChild(g); }
423
- lastGroup=group;
424
- const d=document.createElement('a'); d.href='#'; d.className='doc'; d.dataset.doc=doc;
425
- const t=document.createElement('span'); t.className='t'; t.textContent=slash>=0?doc.slice(slash+1):doc; d.appendChild(t);
465
+ const parts=doc.split('/'); let node=root;
466
+ for(let i=0;i<parts.length-1;i++) node=node.dirs[parts[i]]||(node.dirs[parts[i]]={dirs:{},docs:[]});
467
+ node.docs.push(doc);
468
+ });
469
+ function matches(doc){
470
+ if(!q) return {show:true,heads:null};
471
+ if(mcache[doc]) return mcache[doc];
472
+ const heads=docState[doc].headings.filter(function(h){return h.text.toLowerCase().indexOf(q)>=0;});
473
+ const name=doc.toLowerCase().indexOf(q)>=0;
474
+ return (mcache[doc]={show:name||heads.length>0,heads:heads});
475
+ }
476
+ function indent(el,depth){ el.style.paddingLeft=(6+Math.min(depth,3)*12)+'px'; }
477
+ function hasMatch(node){
478
+ return node.docs.some(function(d){return matches(d).show;})
479
+ || Object.keys(node.dirs).some(function(k){return hasMatch(node.dirs[k]);});
480
+ }
481
+ (function walk(node,pathStr,depth){
482
+ node.docs.forEach(function(doc){
483
+ const m=matches(doc); if(!m.show) return;
484
+ const d=document.createElement('a'); d.href='#'; d.className='doc'+(doc===activeDoc?' active':''); d.dataset.doc=doc; d.title=doc;
485
+ indent(d,depth);
486
+ const t=document.createElement('span'); t.className='t'; t.textContent=doc.slice(pathStr?pathStr.length+1:0); d.appendChild(t);
426
487
  const bd=document.createElement('span'); bd.className='badge badge-warning'; bd.style.display='none'; bd.dataset.fordoc=doc; d.appendChild(bd);
427
488
  d.onclick=function(e){ e.preventDefault(); switchDoc(doc); main.scrollTop=0; };
428
489
  tocEl.appendChild(d);
429
- }
430
- st.headings.forEach(function(h){
431
- const a=document.createElement('a'); a.href='#'+h.id;
432
- a.className='lvl'+(DIRMODE?Math.min(h.level+1,3):h.level); a.dataset.target=h.id; a.dataset.doc=doc;
433
- const t=document.createElement('span'); t.className='t'; t.textContent=h.text; a.appendChild(t);
434
- const bd=document.createElement('span'); bd.className='badge badge-warning'; bd.style.display='none'; bd.dataset.for=h.id; a.appendChild(bd);
435
- a.onclick=function(e){ e.preventDefault(); if(doc!==activeDoc)switchDoc(doc);
436
- document.getElementById(h.id).scrollIntoView({behavior:'smooth',block:'start'}); };
437
- tocEl.appendChild(a);
490
+ // Headings: the active doc's under it normally; matched ones under any
491
+ // hit while filtering. A doc with no headings gets no stub.
492
+ const hs=q?m.heads:(doc===activeDoc?docState[doc].headings:[]);
493
+ hs.forEach(function(h){ const a=headingAnchor(doc,h,Math.min(h.level+1,3)); indent(a,depth+1); tocEl.appendChild(a); });
494
+ });
495
+ Object.keys(node.dirs).sort().forEach(function(name){
496
+ const sub=pathStr?pathStr+'/'+name:name;
497
+ if(q&&!hasMatch(node.dirs[name])) return;
498
+ const open=q?true:openFolders.has(sub);
499
+ const row=document.createElement('a'); row.href='#'; row.className='folder'; row.title=sub;
500
+ indent(row,depth);
501
+ const ar=document.createElement('span'); ar.className='arrow'; ar.textContent=open?'▾':'▸'; row.appendChild(ar);
502
+ const t=document.createElement('span'); t.className='t'; t.textContent=name+'/'; row.appendChild(t);
503
+ const bd=document.createElement('span'); bd.className='badge badge-warning'; bd.style.display='none'; bd.dataset.forfolder=sub; bd.dataset.open=open?'1':''; row.appendChild(bd);
504
+ row.onclick=function(e){ e.preventDefault(); if(q)return;
505
+ if(openFolders.has(sub))openFolders.delete(sub); else openFolders.add(sub); buildToc(); };
506
+ tocEl.appendChild(row);
507
+ if(open) walk(node.dirs[name],sub,depth+1);
438
508
  });
509
+ })(root,'',0);
510
+ if(q&&!tocEl.children.length){ const n=document.createElement('div'); n.className='none'; n.textContent='No matches.'; tocEl.appendChild(n); }
511
+ }
512
+ function buildToc(){
513
+ tocEl.innerHTML='';
514
+ if(DIRMODE){ renderTree(); }
515
+ else docs.forEach(function(doc){
516
+ docState[doc].headings.forEach(function(h){ tocEl.appendChild(headingAnchor(doc,h,h.level)); });
439
517
  });
440
518
  observeHeadings(); updateBadges();
441
519
  }
442
520
 
521
+ function updateChrome(){
522
+ if(!DIRMODE||!activeDoc) return;
523
+ const crumbs=document.getElementById('crumbs'); crumbs.innerHTML=''; crumbs.style.display='block';
524
+ const rootName=document.title.replace(/^review:\s*/,'');
525
+ const r=document.createElement('span'); r.className='seg'; r.textContent=rootName; crumbs.appendChild(r);
526
+ const parts=activeDoc.split('/');
527
+ parts.forEach(function(seg,i){
528
+ const sep=document.createElement('span'); sep.className='sep'; sep.textContent='›'; crumbs.appendChild(sep);
529
+ if(i<parts.length-1){
530
+ const f=parts.slice(0,i+1).join('/');
531
+ const a=document.createElement('a'); a.className='seg'; a.textContent=seg;
532
+ a.onclick=function(e){ e.preventDefault();
533
+ const qEl=document.getElementById('tree-q');
534
+ if(qEl&&qEl.value){ qEl.value=''; qEl.oninput(); } // a filtered tree ignores fold state — clear first
535
+ let g=f; while(g){ openFolders.add(g); g=parentOf(g); } buildToc(); };
536
+ crumbs.appendChild(a);
537
+ } else { const c=document.createElement('span'); c.className='seg cur'; c.textContent=seg; crumbs.appendChild(c); }
538
+ });
539
+ const pnav=document.getElementById('pnav'); pnav.innerHTML=''; pnav.style.display='flex';
540
+ const idx=docs.indexOf(activeDoc);
541
+ function jump(d,label){
542
+ const a=document.createElement('a'); a.href='#'; a.title=d;
543
+ a.textContent=label; a.onclick=function(e){ e.preventDefault(); switchDoc(d); main.scrollTop=0; };
544
+ return a;
545
+ }
546
+ if(idx>0) pnav.appendChild(jump(docs[idx-1],'← '+docs[idx-1].split('/').pop()));
547
+ const sp=document.createElement('span'); sp.className='spacer'; pnav.appendChild(sp);
548
+ if(idx>=0&&idx<docs.length-1) pnav.appendChild(jump(docs[idx+1],docs[idx+1].split('/').pop()+' →'));
549
+ }
550
+
443
551
  function switchDoc(doc){
444
552
  if(activeDoc&&docState[activeDoc]) docState[activeDoc].scroll=main.scrollTop;
445
553
  activeDoc=doc;
446
554
  docs.forEach(function(d){ docState[d].pane.style.display=d===doc?'':'none'; });
447
- tocEl.querySelectorAll('a.doc').forEach(function(a){ a.classList.toggle('active',a.dataset.doc===doc); });
448
555
  main.scrollTop=docState[doc].scroll||0;
449
- observeHeadings();
556
+ if(DIRMODE){ seedOpen(doc); buildToc(); updateChrome(); }
557
+ else observeHeadings();
450
558
  }
451
559
 
452
560
  // ---- Selection -> highlight ----
@@ -589,6 +697,13 @@
589
697
  if(a.doc)docCounts[a.doc]=(docCounts[a.doc]||0)+1; });
590
698
  document.querySelectorAll('.nav .badge[data-for]').forEach(function(b){ const c=counts[b.dataset.for]||0; b.textContent=c; b.style.display=c?'inline-flex':'none'; });
591
699
  document.querySelectorAll('.nav .badge[data-fordoc]').forEach(function(b){ const c=docCounts[b.dataset.fordoc]||0; b.textContent=c; b.style.display=c?'inline-flex':'none'; });
700
+ // Collapsed folders roll up their docs' counts (doc-level only — heading
701
+ // badges would double-count) so pending feedback is never invisible.
702
+ document.querySelectorAll('.nav .badge[data-forfolder]').forEach(function(b){
703
+ const f=b.dataset.forfolder+'/'; let c=0;
704
+ Object.keys(docCounts).forEach(function(d){ if(d.indexOf(f)===0)c+=docCounts[d]; });
705
+ b.textContent=c; b.style.display=(c&&!b.dataset.open)?'inline-flex':'none';
706
+ });
592
707
  }
593
708
 
594
709
  let spy;
@@ -761,6 +876,19 @@
761
876
  // decided — and single-file review when the server was started with
762
877
  // --no-reject (discovery's design stage).
763
878
  if(DIRMODE||meta.noReject){ const rj=document.getElementById('btn-reject'); if(rj)rj.style.display='none'; }
879
+ if(DIRMODE){
880
+ document.getElementById('tree-filter').style.display='block';
881
+ const qEl=document.getElementById('tree-q');
882
+ qEl.oninput=function(){
883
+ const v=qEl.value;
884
+ if(v&&preFilterOpen===null) preFilterOpen=new Set(openFolders); // snapshot on first keystroke
885
+ treeFilter=v;
886
+ if(!v&&preFilterOpen!==null){ openFolders=new Set(preFilterOpen); preFilterOpen=null;
887
+ if(activeDoc)seedOpen(activeDoc); } // doc switched while filtered — keep its ancestors open
888
+ buildToc();
889
+ };
890
+ qEl.onkeydown=function(e){ if(e.key==='Escape'){ qEl.value=''; qEl.oninput(); qEl.blur(); } };
891
+ }
764
892
  planEl.innerHTML='';
765
893
  docs.forEach(function(d,i){
766
894
  const pane=document.createElement('div'); pane.className='docpane'; pane.style.display='none';
@@ -768,7 +896,8 @@
768
896
  docState[d]={md:null,pane:pane,scroll:0,key:'d'+i,headings:[]};
769
897
  });
770
898
  for(let i=0;i<docs.length;i++){ const d=docs[i]; renderDocPane(d,await fetchDoc(d)); }
771
- buildToc(); paint();
899
+ if(!DIRMODE||!docs.length) buildToc(); // DIRMODE: switchDoc below builds the tree once, seeded
900
+ paint();
772
901
  if(docs.length) switchDoc(docs[0]);
773
902
  buildFilesIndex();
774
903
  }
@@ -3,9 +3,12 @@
3
3
  ## The review surface
4
4
 
5
5
  A three-pane layout over a **decision bar**: a left sidebar with two tabs —
6
- **Contents** (heading outline with per-section note counts; in directory mode,
7
- each document is a top-level node with its headings nested, click to switch
8
- docs) and **Files** (paths the plan touches, parsed from inline code and
6
+ **Contents** (heading outline with per-section note counts; in directory mode
7
+ it becomes a doc-site tree: collapsible folders — everything folded except the
8
+ active doc's ancestors, collapsed folders roll up their docs' note counts — a
9
+ filter box matching doc names and headings, and the active doc's headings
10
+ nested under it; a breadcrumb and prev/next footer ride the content pane) and
11
+ **Files** (paths the plan touches, parsed from inline code and
9
12
  fences, badged **exists**/**new** against the repo; click jumps to the first
10
13
  mention, repeat clicks cycle) — the centered rendered plan (middle), and a
11
14
  **Review** rail (right) holding your notes and messages. The decisions live in
@@ -16,7 +16,7 @@
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
19
- "command": "node \"${PLUGIN_ROOT}/skills/condux/plan-review/references/annotate-server.js\" --codex-stop",
19
+ "command": "node \"${PLUGIN_ROOT}/skills/plan-review/references/annotate-server.js\" --codex-stop",
20
20
  "timeout": 345600
21
21
  }
22
22
  ]