codehere 0.7.1 → 1.0.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.
Files changed (139) hide show
  1. package/README.md +22 -13
  2. package/dist/application/services/verdict-service.js +154 -0
  3. package/dist/application/use-cases/assess-cross-vendor.js +126 -0
  4. package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
  5. package/dist/application/use-cases/export-aat.js +107 -0
  6. package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
  7. package/dist/application/use-cases/generate-final-report.js +140 -15
  8. package/dist/application/use-cases/generate-narration.js +5 -3
  9. package/dist/application/use-cases/generate-thread-title.js +5 -2
  10. package/dist/application/use-cases/narrator-prompt-template.js +1 -1
  11. package/dist/application/use-cases/plan-auto-review.js +57 -0
  12. package/dist/application/use-cases/reconcile-audit.js +100 -0
  13. package/dist/application/use-cases/rerun-tests.js +226 -0
  14. package/dist/application/use-cases/search-sessions.js +70 -0
  15. package/dist/application/use-cases/should-auto-review.js +71 -0
  16. package/dist/application/use-cases/thread-export.js +90 -0
  17. package/dist/application/use-cases/verify-claim.js +187 -5
  18. package/dist/domain/audit.js +11 -0
  19. package/dist/domain/services/codebase-detector.js +0 -6
  20. package/dist/domain/task.js +91 -0
  21. package/dist/error-handler.js +3 -115
  22. package/dist/index.js +279 -31
  23. package/dist/infrastructure/completion/bash-completion.js +7 -3
  24. package/dist/infrastructure/completion/fish-completion.js +7 -12
  25. package/dist/infrastructure/completion/zsh-completion.js +7 -12
  26. package/dist/infrastructure/config/config-manager.js +4 -3
  27. package/dist/infrastructure/config/product-context.js +1 -19
  28. package/dist/infrastructure/context/adapters/figma-source.js +37 -2
  29. package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
  30. package/dist/infrastructure/context/github-api.js +110 -0
  31. package/dist/infrastructure/context/github-device-flow.js +65 -0
  32. package/dist/infrastructure/context/oauth-store.js +5 -2
  33. package/dist/infrastructure/context/registry-bootstrap.js +0 -4
  34. package/dist/infrastructure/cost/budget-config.js +4 -3
  35. package/dist/infrastructure/cost/cost-router.js +16 -23
  36. package/dist/infrastructure/cost/spend-aggregator.js +91 -16
  37. package/dist/infrastructure/cost/team-ledger.js +355 -0
  38. package/dist/infrastructure/discovery/discovery-log.js +85 -0
  39. package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
  40. package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
  41. package/dist/infrastructure/hooks/hook-mode.js +20 -4
  42. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
  43. package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
  44. package/dist/infrastructure/observability/execution-tracer.js +1 -1
  45. package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
  46. package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
  47. package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
  48. package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
  49. package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
  50. package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
  51. package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
  52. package/dist/infrastructure/orchestration/context-handoff.js +32 -14
  53. package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
  54. package/dist/infrastructure/orchestration/failover-config.js +1 -11
  55. package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
  56. package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
  57. package/dist/infrastructure/orchestration/graphify-context.js +14 -59
  58. package/dist/infrastructure/orchestration/memory-injector.js +4 -1
  59. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
  60. package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
  61. package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
  62. package/dist/infrastructure/orchestration/session-manager.js +593 -94
  63. package/dist/infrastructure/orchestration/session-persistence.js +177 -41
  64. package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
  65. package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
  66. package/dist/infrastructure/protocols/cloud-sync.js +2 -2
  67. package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
  68. package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
  69. package/dist/infrastructure/protocols/mcp-tools.js +401 -0
  70. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
  71. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
  72. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
  73. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
  74. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
  75. package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
  76. package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
  77. package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
  78. package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
  79. package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
  80. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
  81. package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
  82. package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
  83. package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
  84. package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
  85. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
  86. package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
  87. package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
  88. package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
  89. package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
  90. package/dist/infrastructure/protocols/ui/styles.js +2147 -0
  91. package/dist/infrastructure/protocols/ui/tokens.js +96 -0
  92. package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
  93. package/dist/infrastructure/protocols/verify-page.js +884 -148
  94. package/dist/infrastructure/security/fix-code.js +144 -0
  95. package/dist/infrastructure/security/fix-secret.js +89 -0
  96. package/dist/infrastructure/security/permissions-config.js +3 -2
  97. package/dist/infrastructure/security/scan-types.js +10 -0
  98. package/dist/infrastructure/security/ship-scan.js +195 -0
  99. package/dist/infrastructure/security/trust-scorer.js +70 -3
  100. package/dist/infrastructure/security/vibe-checks.js +99 -0
  101. package/dist/infrastructure/storage/atomic-write.js +86 -0
  102. package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
  103. package/dist/infrastructure/storage/project-thread-reader.js +262 -25
  104. package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
  105. package/dist/monitoring.js +2 -2
  106. package/dist/presentation/cli/commands/audit-command.js +79 -4
  107. package/dist/presentation/cli/commands/budget-command.js +2 -2
  108. package/dist/presentation/cli/commands/check-command.js +96 -0
  109. package/dist/presentation/cli/commands/context-source-command.js +20 -1
  110. package/dist/presentation/cli/commands/help-command.js +5 -5
  111. package/dist/presentation/cli/commands/hook-command.js +5 -5
  112. package/dist/presentation/cli/commands/init-command.js +89 -66
  113. package/dist/presentation/cli/commands/memory-command.js +28 -28
  114. package/dist/presentation/cli/commands/permission-command.js +1 -12
  115. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  116. package/dist/presentation/cli/commands/protocols-command.js +4 -4
  117. package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
  118. package/dist/presentation/cli/commands/report-command.js +48 -32
  119. package/dist/presentation/cli/commands/scan-command.js +52 -0
  120. package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
  121. package/dist/presentation/cli/commands/search-command.js +51 -0
  122. package/dist/presentation/cli/commands/setup-command.js +31 -8
  123. package/dist/presentation/cli/commands/status-command.js +39 -86
  124. package/dist/presentation/cli/commands/team-command.js +256 -0
  125. package/dist/presentation/cli/commands/thread-command.js +87 -5
  126. package/dist/presentation/cli/commands/trust-command.js +53 -31
  127. package/dist/presentation/cli/commands/verify-command.js +74 -9
  128. package/dist/presentation/cli/error-display.js +0 -8
  129. package/dist/presentation/cli/repl.js +15 -7
  130. package/dist/presentation/cli/task-deliverable.js +36 -10
  131. package/dist/testing/replay-adapter.js +0 -19
  132. package/dist/ui.js +51 -239
  133. package/dist/utils/version.js +0 -6
  134. package/package.json +30 -10
  135. package/dist/domain/entities/task.js +0 -61
  136. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  137. package/dist/infrastructure/security/input-validator.js +0 -152
  138. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  139. package/dist/infrastructure/storage/task-repository.js +0 -206
@@ -0,0 +1,588 @@
1
+ import { COST_PER_M_OUTPUT_TOKENS_USD } from '../../cost/spend-aggregator.js';
2
+ import { VERDICT_WORDS } from '../../../application/services/verdict-service.js';
3
+ /**
4
+ * Composer cost estimate — computed, never narrated. Three honest modes (free tier, a real
5
+ * per-token rate, unknown), each labelled as what it is rather than rounded into a claim.
6
+ *
7
+ * Split out of app-script.ts (strangler fig). Verbatim slice, interpolated back at the
8
+ * position it was cut from so it keeps sharing the surrounding IIFE scope — its functions
9
+ * are still called from the code left behind. Verified by hashing the composed APP_SCRIPT
10
+ * before and after: same bytes.
11
+ */
12
+ export const COMPOSER_COST = `/* S7 · the composer cost estimate is COMPUTED, never narrated. Three honest
13
+ modes, each labeled as what it is:
14
+ - free -> the picked agent has no per-token cost: "$0 · free tier"
15
+ - recent -> median of the user's recent REAL completed-task costs for that
16
+ agent + this prompt's input-token share, priced from COST_RATES
17
+ (the SAME table the daemon bills with, interpolated at build
18
+ so client and server can't drift)
19
+ - typical -> no history yet for a paid agent: a flat figure said plainly
20
+ ("typical") instead of dressed up as a computation.
21
+ The tooltip is set per mode so it never claims a computation that didn't
22
+ happen (the old chip hardcoded ~$0.02 under a "based on prompt length +
23
+ pricing" tooltip — the S7 audit CRIT). */
24
+ var COST_RATES=${JSON.stringify(COST_PER_M_OUTPUT_TOKENS_USD)};
25
+ var VERDICT_WORDS=${JSON.stringify(VERDICT_WORDS)};
26
+ function estCost(agent, promptChars){
27
+ var rate=COST_RATES[agent];
28
+ if(rate===undefined||rate===0) return { mode:'free', text:'$0 \\u00B7 free tier', title:'This agent runs on a free tier \\u00b7 no per-token cost.' };
29
+ var recent=[];
30
+ try{
31
+ var all=allS(_cached.tasks||{});
32
+ for(var i=0;i<all.length;i++){
33
+ var x=all[i];
34
+ if(x.agent===agent && x.cost && typeof x.cost.spent==='number' && x.cost.spent>0 && ['completed','done'].includes(x.status)) recent.push(x.cost.spent);
35
+ }
36
+ }catch(e){}
37
+ // prompt's own input share: ~4 chars/token at the input rate (1/5 of output)
38
+ var promptCents=((promptChars/4)/1000000)*(rate*0.2)*100;
39
+ if(recent.length){
40
+ recent.sort(function(a,b){return a-b;});
41
+ var med=recent[Math.floor(recent.length/2)];
42
+ var cents=med+promptCents;
43
+ return { mode:'recent', text:'~$'+(cents/100).toFixed(cents<10?4:2),
44
+ title:'Estimate: median of your last '+recent.length+' real '+agent+' task'+(recent.length===1?'':'s')+' + this prompt\\u2019s input share, at the same pricing table the daemon uses.' };
45
+ }
46
+ return { mode:'typical', text:'~$0.02 \\u00B7 typical',
47
+ title:'No completed '+agent+' tasks yet to average \\u00b7 $0.02 is a typical small-task cost, not a computation. Your budget cap bounds the real spend.' };
48
+ }
49
+ function updateCostEst(){
50
+ var pick=document.getElementById('agent-pick');
51
+ var mainAgent=(pick&&pick.value&&pick.value!=='auto')?pick.value:'conductor';
52
+ var mta=document.getElementById('main-ta');
53
+ var mEl=document.getElementById('cost-est-main');
54
+ if(mEl){ var m=estCost(mainAgent,(mta&&mta.value?mta.value.length:0)); mEl.textContent=m.text; mEl.title=m.title; mEl.setAttribute('data-verify-mode',m.mode); }
55
+ var dEl=document.getElementById('cost-est-detail');
56
+ if(dEl){
57
+ var dt=_detail?findS(_detail):null;
58
+ var dAgent=(dt&&dt.agent&&dt.agent!=='conductor')?dt.agent:'conductor';
59
+ var dta2=document.getElementById('detail-ta');
60
+ var d=estCost(dAgent,(dta2&&dta2.value?dta2.value.length:0));
61
+ dEl.textContent=d.text; dEl.title=d.title; dEl.setAttribute('data-verify-mode',d.mode);
62
+ }
63
+ }
64
+ // ── S13 · ⌘K palette — every relocated control as a command that NAMES its
65
+ // visible consequence and RUNS the existing function. Grouped Act / View /
66
+ // Trust / Go; type-to-filter; up/down + Enter executes; Esc closes. ─────────────
67
+ // S20 · three lenses replace the five personas. body[data-lens] + a REAL
68
+ // re-render: rowH/cardH/renderD branch their templates per lens — different
69
+ // section order and inclusion, never display:none. Persisted, announced.
70
+ function curLens(){
71
+ const v=document.body.getAttribute('data-lens');
72
+ return (v==='review'||v==='status')?v:'default';
73
+ }
74
+ function applyLens(v){
75
+ v=(v==='review'||v==='status')?v:'default';
76
+ document.body.setAttribute('data-lens',v);
77
+ try{ localStorage.setItem('codehere-lens',v); }catch(e){}
78
+ // Lossless switch: recompose via the normal render path, keep the scroll.
79
+ const _sy=window.scrollY;
80
+ window.__lensSwitch=true;
81
+ try{
82
+ if(_cached.tasks) render();
83
+ renderHostSessions(_cached.host||{sessions:[]});
84
+ }catch(e){}
85
+ window.__lensSwitch=false;
86
+ if(_sy>0)window.scrollTo(0,_sy);
87
+ return lensEffect(v);
88
+ }
89
+ function lensEffect(v){
90
+ // One line naming the visible change — never a bare "switched".
91
+ if(v==='review') return 'Review \\u00b7 verdict + evidence lead \\u00b7 conversation rests collapsed';
92
+ if(v==='status') return 'Status \\u00b7 outcomes only \\u00b7 one sentence per task + spend';
93
+ return 'Default \\u00b7 the full view is back';
94
+ }
95
+ function applyDensity(on){
96
+ // Same body.density-compact class the old header toggle drove.
97
+ document.body.classList.toggle('density-compact',!!on);
98
+ try{ localStorage.setItem('codehere-density-compact',on?'1':'0'); }catch(e){}
99
+ }
100
+ let _palItems=[], _palSel=0;
101
+ function paletteCommands(q){
102
+ const cmds=[];
103
+ const add=(group,cmd,label,cons,kbd,run)=>cmds.push({group:group,cmd:cmd,label:label,cons:cons,kbd:kbd||'',run:run});
104
+ // Act
105
+ add('Act','new-task','New task','focuses the composer \\u00b7 live cost estimate as you type','N',function(){ if(_detail)hideD(); if(_room)closeRoom(); const ta=document.getElementById('main-ta'); if(ta)ta.focus(); });
106
+ const running=allS(_cached.tasks||{}).filter(t=>['running','queued','paused','blocked'].includes(t.status));
107
+ if(running.length){
108
+ running.slice(0,6).forEach(function(t){
109
+ add('Act','stop-task','Stop: '+displayPrompt(t.prompt).slice(0,60),'stops '+agentLabel(t.agent)+' now \\u00b7 the run ends as stopped','',function(){ stopTask(null,t.sessionId); });
110
+ });
111
+ } else {
112
+ add('Act','stop-task','Stop a running task\\u2026','nothing is running right now \\u00b7 live tasks list here when there are any','',function(){ al('Nothing is running right now'); });
113
+ }
114
+ add('Act','share','Share this thread','signed read-only link \\u00b7 the bundle is your audit trail, not your code','',function(){ if(window.openShare)window.openShare(); else al('Share is unavailable'); });
115
+ add('Act','verify-chain','Verify the audit chain','walks every entry \\u00b7 the verdict lands on the audit footer','',function(){ const f=document.querySelector('.oss-footer'); if(f&&f.scrollIntoView)f.scrollIntoView({block:'end'}); verifyAuditChain(); al('Walking the chain \\u00b7 the verdict lands on the audit footer'); });
116
+ // View · ONE consistent experience. The lens (Default/Review/Status), Reshape
117
+ // and Density mode-switches were removed (2026-06-20) — they made the UI shift
118
+ // around between visits. The default conversation-first view is the only view.
119
+ // Filter (below) stays: it scopes the list, it doesn't reshape the design.
120
+ [['all','everything'],['running','only running tasks'],['done','only completed tasks'],['failed','only failed tasks']].forEach(function(f){
121
+ add('View','filter-'+f[0],'Filter: '+f[0]+(_filter===f[0]?' \\u2713':''),'shows '+f[1]+(f[0]==='all'?'':' \\u00b7 removable token above the list'),'',function(){ setFilter(f[0]); al('Filter: '+f[0]); });
122
+ });
123
+ // Trust
124
+ add('Trust','settings','Settings','configured integrations, docs','',function(){ openDrawer(); });
125
+ // Go
126
+ const ags=(_cached.readiness&&_cached.readiness.agents)||[];
127
+ const agSum=ags.length?ags.map(function(a){ return agentLabel(a.id)+' '+agentS(a.state); }).join(' \\u00b7 '):'readiness loads with the first poll';
128
+ add('Go','agents','Participants',agSum,'',function(){ openDrawer(); al('Who can work your rooms \\u00b7 '+agSum); });
129
+ add('Go','observer','Observed terminal sessions','the sessions codehere is watching \\u00b7 expands + scrolls to them','',function(){
130
+ window._obsOpen=true; renderHostSessions(_cached.host||{sessions:[]});
131
+ const o=document.getElementById('observer-rail'); if(o&&o.scrollIntoView)o.scrollIntoView({block:'center'});
132
+ });
133
+ add('Go','history','History','earlier work + demos \\u00b7 expands below today, scroll preserved','',function(){
134
+ window.__histExpand();
135
+ const h=document.querySelector('.hist-row'); if(h&&h.scrollIntoView)h.scrollIntoView({block:'start'});
136
+ });
137
+ add('Go','tour','Take the tour','a quick walkthrough \\u00b7 projects, the + entry point, the composer, the picker, send','',function(){ if(_detail)hideD(); if(_room)closeRoom(); startTour(); });
138
+ // jump-to-task by typing its title · top 8 REAL matches, Enter opens it
139
+ if(q){
140
+ const ql=q.toLowerCase();
141
+ allS(_cached.tasks||{}).filter(function(t){ return t.prompt&&displayPrompt(t.prompt).toLowerCase().indexOf(ql)>=0; })
142
+ .slice(0,8).forEach(function(t){
143
+ add('Go','jump-task','Open: '+displayPrompt(t.prompt).slice(0,60),'opens this task\\u2019s detail \\u00b7 '+agentLabel(t.agent),'',function(){ showD(t.sessionId); });
144
+ });
145
+ }
146
+ return cmds;
147
+ }
148
+ function paletteFilter(q){
149
+ const all=paletteCommands(q);
150
+ if(!q) return all.filter(c=>c.cmd!=='jump-task');
151
+ const ql=q.toLowerCase();
152
+ return all.filter(c=>c.cmd==='jump-task'||(c.label+' '+c.cons).toLowerCase().indexOf(ql)>=0);
153
+ }
154
+ // Phase 4 · one crafted icon per palette group (Raycast-grade feel), inline SVG
155
+ // inheriting currentColor. String-only, no regex (template-literal safe).
156
+ function palIcon(group){
157
+ var sv="<svg aria-hidden='true' focusable='false' width='15' height='15' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'>";
158
+ var p;
159
+ if(group==='Act') p="<rect x='3' y='4' width='18' height='16' rx='2'/><path d='m7 9 3 3-3 3'/><path d='M13 15h4'/>";
160
+ else if(group==='View') p="<path d='M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z'/><circle cx='12' cy='12' r='3'/>";
161
+ else if(group==='Trust') p="<path d='M12 3l7 3v5c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6Z'/><path d='m9 12 2 2 4-4'/>";
162
+ else p="<path d='M5 12h14'/><path d='m13 6 6 6-6 6'/>"; // Go
163
+ return '<span class="picon">'+sv+p+'</svg></span>';
164
+ }
165
+ function renderPalette(q){
166
+ const list=document.getElementById('palette-list');
167
+ if(!list) return;
168
+ _palItems=paletteFilter(q);
169
+ if(_palSel>=_palItems.length)_palSel=Math.max(0,_palItems.length-1);
170
+ const he=s=>String(s==null?'':s).replace(/[&<>"]/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[ch]));
171
+ let html='', grp='';
172
+ _palItems.forEach(function(it,i){
173
+ if(it.group!==grp){ grp=it.group; html+='<div class="pgroup">'+grp+'</div>'; }
174
+ html+='<div class="prow'+(i===_palSel?' sel':'')+'" data-pi="'+i+'" data-verify-cmd="'+he(it.cmd)+'">'+palIcon(it.group)+'<span class="n"'+((it.cmd==='stop-task'||it.cmd==='jump-task')?' data-user-content':'')+'>'+he(it.label)+'</span><span class="c">'+he(it.cons)+'</span>'+(it.kbd?'<span class="pk">'+he(it.kbd)+'</span>':'')+'</div>';
175
+ });
176
+ if(!_palItems.length) html='<div class="pempty">no command matches \\u201c'+he(q)+'\\u201d \\u00b7 esc closes</div>';
177
+ list.innerHTML=html;
178
+ const selEl=list.querySelector('.prow.sel');
179
+ if(selEl&&selEl.scrollIntoView)selEl.scrollIntoView({block:'nearest'});
180
+ }
181
+ function openPalette(){
182
+ const sc=document.getElementById('palette-scrim');
183
+ const inp=document.getElementById('palette-input');
184
+ if(!sc||!inp) return;
185
+ _palSel=0; inp.value='';
186
+ renderPalette('');
187
+ sc.classList.add('open');
188
+ setTimeout(function(){ inp.focus(); },30);
189
+ }
190
+ function closePalette(){
191
+ const sc=document.getElementById('palette-scrim');
192
+ if(sc)sc.classList.remove('open');
193
+ }
194
+ function runPaletteItem(i){
195
+ const it=_palItems[i];
196
+ if(!it) return;
197
+ closePalette();
198
+ try{ it.run(); }catch(e){ al('Command failed \\u00b7 '+((e&&e.message)||e)); }
199
+ }
200
+ // The harness drives the palette programmatically (coverage re-points the
201
+ // relocated header controls to these commands).
202
+ try{ window.openPalette=openPalette; window.closePalette=closePalette; }catch(e){}
203
+
204
+ async function fetchAll() {
205
+ const sk=document.getElementById('skeleton');if(sk){const p=sk.parentNode;if(p)p.removeChild(sk);}
206
+ try {
207
+ const [t,r,pj]=await Promise.all([fetch('/api/tasks').then(x=>x.json()),fetch('/api/readiness/agents').then(x=>x.json()),fetch('/api/projects').then(x=>x.json()).catch(()=>({projects:[]}))]);
208
+ _cached.tasks=t;_cached.readiness=r;_cached.projects=(pj&&pj.projects)||[];
209
+ if(t.pricing)_cached.pricing=t.pricing;
210
+ // S13 refuter · the 2026-06-02 auto-open ("a bare list felt like nothing
211
+ // happened") is RETIRED: the home now answers at rest — decision queue
212
+ // first, today's truth, one calm column. Auto-opening a detail made the
213
+ // first paint the one screen S13 didn't redesign, and broke the
214
+ // one-orange rule on landing. The user opens work; we don't hijack.
215
+ render();
216
+ updateCostEst();
217
+ }catch(e){console.error('fetch error',e);}
218
+ try {
219
+ const h=await fetch('/api/host-sessions').then(x=>x.json());
220
+ renderHostSessions(h);
221
+ }catch{}
222
+ try {
223
+ // Refuter C5 · the spend figure sits in the hero now — a page-load
224
+ // snapshot frozen for the tab's lifetime read as live. Refresh each poll.
225
+ const p=await fetch('/api/spend/today').then(x=>x.json());
226
+ if(p&&p.budget){_cached.pricing={dailyBudgetCents:Math.round((p.budget.dailyUsd||0)*100),spentTodayCents:Math.round((p.totalUsd||0)*100)}; upBud();}
227
+ }catch{}
228
+ try{ var _spv=document.getElementById('spend-view'); if(_spv&&_spv.style.display==='block') await renderTeamHero(); }catch(e){ /* never let the hero break the page */ }
229
+ }
230
+
231
+ // ── Cost + audit hero (the team ledger) ──────────────────────────────────────
232
+ // The first thing on the home page: what the team spent and an integrity-checked
233
+ // record of what each agent did. Cost for everyone (this machine), the full
234
+ // merged team view when codehere team has a dir. Self-contained + try/catch so
235
+ // it can never break the rest of the dashboard.
236
+ let _teamHero={team:null,spend:null,mode:'team'};
237
+ function _tchEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
238
+ function _tchUsd(n){n=Number(n)||0; return '$'+n.toFixed(2);}
239
+ function _tchRows(obj){
240
+ const e=Object.entries(obj||{}).sort((a,b)=>b[1]-a[1]); if(!e.length) return '';
241
+ const top=e[0][1]||1;
242
+ return e.map(([k,v])=>'<div class="tch-r"><span class="tch-nm">'+_tchEsc(k)+'</span><span class="tch-tk"><i style="width:'+Math.max(2,Math.round(100*v/top))+'%"></i></span><span class="tch-am">'+_tchUsd(v)+'</span></div>').join('');
243
+ }
244
+ async function renderTeamHero(){
245
+ const el=document.getElementById('team-cost-hero'); if(!el) return;
246
+ if(arguments[0]!==1){ // a real refresh (not a toggle re-render) refetches
247
+ try{ _teamHero.team=await fetch('/api/team').then(x=>x.json()); }catch{ _teamHero.team=null; }
248
+ try{ _teamHero.spend=await fetch('/api/spend/today').then(x=>x.json()); }catch{ _teamHero.spend=null; }
249
+ try{ _teamHero.audit=await fetch('/api/audit/summary').then(x=>x.json()); }catch{ _teamHero.audit=null; }
250
+ }
251
+ const team=_teamHero.team, spend=_teamHero.spend;
252
+ const configured = !!(team && team.configured);
253
+ const audit = _teamHero.audit;
254
+ el.style.display=''; // dedicated Spend & Audit page — always render, even at $0 / no team
255
+
256
+ const mineOnly = configured && _teamHero.mode==='mine';
257
+ const mine = configured ? (team.machines||[]).find(m=>m.machineId===team.myMachineId) : null;
258
+ // pick the active dataset
259
+ let total, perAgent, perProject, dailyUsd, machines, totalActions, broken, perAgentSessions={}, tampered=[], unsigned=[];
260
+ if(configured && !mineOnly){
261
+ total=team.teamTotal; perAgent=team.perAgent; perProject=team.perProject; dailyUsd=team.dailyUsd;
262
+ machines=team.machines||[]; totalActions=team.totalActions; broken=team.brokenChains||[];
263
+ perAgentSessions=team.perAgentSessions||{}; tampered=team.tamperedShares||[]; unsigned=team.unsignedShares||[];
264
+ } else if(configured && mineOnly){
265
+ total=mine?mine.totalUsd:0; perAgent=mine?mine.byAgent:{}; perProject=mine?(mine.byProject||{}):{}; dailyUsd=team.dailyUsd;
266
+ machines=mine?[mine]:[]; totalActions=mine&&mine.audit?mine.audit.actions:0; broken=(mine&&mine.audit&&!mine.audit.chainOk)?[mine.machineId]:[];
267
+ // per-agent/project for a machine come as {name:usd}
268
+ const a={}; for(const k in (mine&&mine.byAgent)||{})a[k]=mine.byAgent[k]; perAgent=a;
269
+ } else {
270
+ const sp=spend||{totalUsd:0,byAgent:{},byProject:{},budget:null};
271
+ total=sp.totalUsd||0; const a={},pj={};
272
+ for(const k in sp.byAgent||{})a[k]=sp.byAgent[k].usd; for(const k in sp.byProject||{})pj[k]=sp.byProject[k].usd;
273
+ perAgent=a; perProject=pj; dailyUsd=sp.budget?sp.budget.dailyUsd:null; machines=[]; totalActions=audit?audit.actions:0; broken=[];
274
+ }
275
+ const pct = dailyUsd? Math.min(100,Math.round(100*total/dailyUsd)) : null;
276
+ const left = dailyUsd!=null? Math.max(0,dailyUsd-total) : null;
277
+
278
+ const toggle = configured ? '<div class="tch-tog"><button class="'+(!mineOnly?'on':'')+'" onclick="_tchSet(\\'team\\')">Team</button><button class="'+(mineOnly?'on':'')+'" onclick="_tchSet(\\'mine\\')">This machine</button></div>' : '';
279
+ const machineRows = (configured && !mineOnly && machines.length)
280
+ ? '<div class="tch-grp">By machine</div>'+machines.map(m=>'<div class="tch-r"><span class="tch-nm">'+_tchEsc(m.machineId)+(m.machineId===team.myMachineId?' <span class="tch-me">you</span>':'')+'</span><span class="tch-tk"><i style="width:'+Math.max(2,Math.round(100*m.totalUsd/(machines[0].totalUsd||1)))+'%"></i></span><span class="tch-am">'+_tchUsd(m.totalUsd)+'</span></div>').join('')
281
+ : '';
282
+ const agentRows = Object.keys(perAgent||{}).length? '<div class="tch-grp">By agent</div>'+_tchRows(perAgent) : '';
283
+ const projRows = Object.keys(perProject||{}).length? '<div class="tch-grp">By project</div>'+_tchRows(perProject) : '';
284
+ // Practice runs live OUTSIDE the totals (fabricated cost, never billed) —
285
+ // one quiet line, only when any exist (Ega pick A, 2026-07-07).
286
+ let _prUsd=0,_prN=0;
287
+ if(configured && !mineOnly && team.practice){ _prUsd=team.practice.usd||0; _prN=team.practice.sessions||0; }
288
+ else if(configured && mineOnly && mine){ _prUsd=mine.practiceUsd||0; _prN=mine.practiceSessions||0; }
289
+ else if(!configured && spend && spend.practice){ _prUsd=spend.practice.usd||0; _prN=spend.practice.sessions||0; }
290
+ const practiceRow = _prN>0? '<div class="tch-sub" style="margin-top:8px">practice runs \\u00b7 '+_prN+' session'+(_prN===1?'':'s')+' \\u00b7 '+_tchUsd(_prUsd)+' fabricated \\u00b7 not in the totals</div>' : '';
291
+
292
+ let recordCol='';
293
+ if(configured){
294
+ const chainRows = machines.filter(m=>m.audit).map(m=>'<div class="tch-mach"><span>'+_tchEsc(m.machineId)+'</span><span class="'+(m.audit.chainOk?'ok':'bad')+'">'+(m.audit.chainOk?'\\u2713 ok':'\\u2715 BROKEN')+' \\u00b7 '+(m.audit.actions||0).toLocaleString()+'</span></div>').join('');
295
+ // The green claim renders ONLY when share data was actually loaded (team
296
+ // view — mine-mode never populates tampered/unsigned) AND nothing is
297
+ // unsigned: "all shares signed" next to an "unsigned: X" line is a lie.
298
+ const sigLine = tampered.length
299
+ ? '<div class="tch-broken">\\u2715 signature invalid on '+tampered.map(_tchEsc).join(', ')+' \\u2014 altered or re-signed with a different key. Do not trust that share.</div>'
300
+ : ((!mineOnly && machines.length && !unsigned.length)? '<div class="tch-sub" style="color:var(--verified,#84b594)">\\u2713 all shares signed \\u00b7 keys match the ones first seen</div>' : '');
301
+ const unsignedLine = unsigned.length? '<div class="tch-sub">unsigned, can\\'t verify: '+unsigned.map(_tchEsc).join(', ')+'</div>' : '';
302
+ const actKeys = Object.keys(perAgentSessions||{}).sort(function(a,b){return perAgentSessions[b]-perAgentSessions[a];});
303
+ const actByAgent = actKeys.length
304
+ ? '<div class="tch-grp">What each agent did</div>'+actKeys.map(function(a){return '<div class="tch-mach"><span>'+_tchEsc(a)+'</span><span>'+perAgentSessions[a]+' session'+(perAgentSessions[a]===1?'':'s')+'</span></div>';}).join('')
305
+ : '';
306
+ recordCol='<div class="tch-col"><div class="tch-lab">Record</div><div class="tch-sub">what each agent did, integrity-checked</div>'
307
+ +'<div class="tch-stat"><div class="k">Actions</div><div class="v">'+(totalActions||0).toLocaleString()+'</div></div>'
308
+ +(broken.length?'<div class="tch-broken">\\u2715 chain broken on '+broken.map(_tchEsc).join(', ')+'. Investigate before trusting that record.</div>':'')
309
+ +sigLine+unsignedLine
310
+ +actByAgent
311
+ +(chainRows?'<div class="tch-grp">Chain integrity, per machine</div>'+chainRows:'')
312
+ +'</div>';
313
+ } else if(audit){
314
+ // Solo: this machine's own integrity-checked record (no team merged yet).
315
+ recordCol='<div class="tch-col"><div class="tch-lab">Record</div><div class="tch-sub">what each agent did on this machine, integrity-checked</div>'
316
+ +'<div class="tch-stat"><div class="k">Actions (last 24h)</div><div class="v">'+(audit.actions||0).toLocaleString()+'</div></div>'
317
+ +(audit.agents?'<div class="tch-grp">'+audit.agents+' agent'+(audit.agents===1?'':'s')+' active \\u00b7 '+(audit.blocked||0)+' blocked by trust</div>':'')
318
+ +'</div>';
319
+ }
320
+
321
+ const budBtn='style="background:none;border:1px solid rgba(255,255,255,.18);border-radius:6px;color:rgba(255,255,255,.72);font:inherit;font-size:10.5px;padding:2px 8px;margin-left:5px;cursor:pointer"';
322
+ const budInput='style="width:80px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.18);border-radius:6px;color:inherit;font:inherit;font-size:10.5px;padding:2px 7px;margin-left:8px"';
323
+ const setBudgetUI='<span class="tch-setbud">set a daily cap:<button '+budBtn+' onclick="_tchSetBudget(5)">$5</button><button '+budBtn+' onclick="_tchSetBudget(20)">$20</button><button '+budBtn+' onclick="_tchSetBudget(50)">$50</button><input id="tch-bud-input" type="number" min="1" step="1" placeholder="$ custom" '+budInput+'><button '+budBtn+' onclick="_tchSetBudgetInput()">set</button></span>';
324
+ const changeBudget='<span class="tch-hint" style="cursor:pointer;text-decoration:underline" onclick="_tchSetBudget(null)">change</span>';
325
+ const barMeta = dailyUsd!=null
326
+ ? '<div class="tch-barm"><span>'+pct+'% used'+(configured&&!mineOnly?', '+machines.length+' machines merged':'')+'</span><span>'+_tchUsd(left)+' left'+(!configured?' \\u00b7 '+changeBudget:'')+'</span></div>'
327
+ : '<div class="tch-barm"><span>no budget set</span>'+(configured?'<span class="tch-hint">set one: codehere team budget &lt;amount&gt;</span>':setBudgetUI)+'</div>';
328
+ const ofTxt = dailyUsd!=null? ' <span class="tch-of">of '+_tchUsd(dailyUsd)+(configured?' team':'')+' budget today</span>' : '';
329
+ const setupHint = !configured? '<div class="tch-setup">Solo view. Run <code>codehere team set-dir &lt;a git repo&gt;</code> on each machine to merge the whole team, local-first.</div>' : '';
330
+ // In-product discovery recruiting hook (Torres). Opt-in: shows only when a team is
331
+ // configured AND a scheduler URL is set (CODEHERE_DISCOVERY_URL). Dismissible.
332
+ const recruit = (configured && team && team.recruitUrl && !_recruitDismissed())
333
+ ? '<div id="ch-recruit" style="display:flex;align-items:center;gap:8px;padding:9px 12px;margin:0 0 12px;border:1px solid rgba(255,255,255,.12);border-radius:8px;background:rgba(255,255,255,.03);font-size:12px;color:var(--fg-70,rgba(255,255,255,.7))"><span style="flex:1">Running 2+ agents on a team? We\\'re learning how teams handle agent cost and what each one did. <a href="'+_tchEsc(team.recruitUrl)+'" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:underline">20-min chat \\u2192</a></span><button onclick="_recruitDismiss()" title="dismiss" style="background:none;border:0;color:inherit;cursor:pointer;font-size:14.5px;opacity:.6;line-height:1">\\u00d7</button></div>'
334
+ : '';
335
+
336
+ el.innerHTML=recruit+'<style id="tch-css">'+_TCH_CSS+'</style>'
337
+ // data-reading · DESIGN.md §0. The largest type on a product surface is a
338
+ // MEASURED FACT, never a claim or a heading. This total is metered spend, so
339
+ // it earns --fs-reading; ui-token-gate refuses that size on anything else.
340
+ +'<div class="tch-band"><div class="tch-spend"><div class="tch-big" data-reading>'+_tchUsd(total)+ofTxt+'</div>'
341
+ +(dailyUsd!=null?'<div class="tch-bar"><i style="width:'+(pct||0)+'%"></i></div>':'')+barMeta
342
+ +'<div class="tch-est">estimate across all agents and machines, not a provider invoice</div></div>'+toggle+'</div>'
343
+ +'<div class="tch-cols"><div class="tch-col tch-cost"><div class="tch-lab">Cost</div><div class="tch-sub">where the bill went, today</div>'
344
+ +machineRows+agentRows+projRows+practiceRow
345
+ +((!machineRows&&!agentRows&&!projRows)?'<div class="tch-sub" style="margin-top:8px">No agent spend recorded today.</div>':'')
346
+ +'</div>'+recordCol+'</div>'+setupHint;
347
+ el.style.display='block';
348
+ }
349
+ function _recruitDismissed(){ try{ return localStorage.getItem('ch-recruit-dismissed')==='1'; }catch(e){ return false; } }
350
+ function _recruitDismiss(){ try{ localStorage.setItem('ch-recruit-dismissed','1'); }catch(e){} var n=document.getElementById('ch-recruit'); if(n)n.remove(); }
351
+ function _tchSet(mode){ _teamHero.mode=mode; try{ renderTeamHero(1); }catch{} }
352
+ // Set (or clear, usd=null) this machine's daily cap from the page — no CLI needed.
353
+ // Solo only; team budgets come from team config via the codehere team budget CLI.
354
+ async function _tchSetBudget(usd){
355
+ try{ await fetch('/api/budget',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dailyUsd:usd})}); }catch(e){}
356
+ try{ await renderTeamHero(); }catch(e){}
357
+ }
358
+ // Custom typed cap from the input next to the presets.
359
+ function _tchSetBudgetInput(){ var el=document.getElementById('tch-bud-input'); if(!el)return; var v=Number(el.value); if(!(v>0)){ el.focus(); return; } _tchSetBudget(v); }
360
+ const _TCH_CSS=[
361
+ '#team-cost-hero{margin:0 0 18px}',
362
+ '#team-cost-hero .tch-band{display:flex;align-items:flex-end;gap:18px;padding:14px 0 12px;border-bottom:1px solid var(--border-soft,rgba(255,255,255,.06))}',
363
+ '#team-cost-hero .tch-spend{flex:1}',
364
+ '#team-cost-hero .tch-big[data-reading]{font-size:40px;font-weight:700;letter-spacing:-.02em;line-height:1;color:var(--fg)}',
365
+ '#team-cost-hero .tch-of{font-size:13px;font-weight:500;color:var(--fg-50,rgba(255,255,255,.5))}',
366
+ '#team-cost-hero .tch-bar{height:6px;border-radius:4px;background:var(--bg-3,hsl(0 0% 12%));margin:9px 0 4px;max-width:420px;overflow:hidden}',
367
+ '#team-cost-hero .tch-bar>i{display:block;height:100%;background:var(--accent)}',
368
+ '#team-cost-hero .tch-barm{display:flex;justify-content:space-between;max-width:420px;font-size:10.5px;color:var(--fg-50,rgba(255,255,255,.5))}',
369
+ '#team-cost-hero .tch-hint{font-family:var(--mono);color:var(--fg-40,rgba(255,255,255,.4))}',
370
+ '#team-cost-hero .tch-est{font-size:10.5px;color:var(--fg-40,rgba(255,255,255,.4));margin-top:5px}',
371
+ '#team-cost-hero .tch-tog{display:flex;border:1px solid var(--border);border-radius:7px;overflow:hidden;font-size:12px}',
372
+ '#team-cost-hero .tch-tog button{background:none;border:none;color:var(--fg-50,rgba(255,255,255,.5));padding:6px 12px;cursor:pointer;font:inherit}',
373
+ '#team-cost-hero .tch-tog button.on{background:var(--accent-wash,rgba(255,154,118,.1));color:var(--accent);font-weight:600}',
374
+ '#team-cost-hero .tch-cols{display:grid;grid-template-columns:1fr 1fr;gap:0;margin-top:14px}',
375
+ '#team-cost-hero .tch-col{padding:2px 0}',
376
+ '#team-cost-hero .tch-cost{padding-right:26px;border-right:1px solid var(--border-soft,rgba(255,255,255,.06))}',
377
+ '#team-cost-hero .tch-col+.tch-col{padding-left:26px}',
378
+ '#team-cost-hero .tch-lab{font-size:10.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--fg-50,rgba(255,255,255,.5))}',
379
+ '#team-cost-hero .tch-sub{font-size:10.5px;color:var(--fg-40,rgba(255,255,255,.4));margin:2px 0 9px}',
380
+ '#team-cost-hero .tch-grp{font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--fg-40,rgba(255,255,255,.4));margin:12px 0 6px}',
381
+ '#team-cost-hero .tch-r{display:grid;grid-template-columns:fit-content(220px) 1fr 56px;gap:9px;align-items:center;margin-bottom:5px}',
382
+ '#team-cost-hero .tch-nm{font-size:12px;color:var(--fg-70,rgba(255,255,255,.7));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
383
+ '#team-cost-hero .tch-me{color:var(--fg-40,rgba(255,255,255,.4));font-size:10.5px}',
384
+ '#team-cost-hero .tch-tk{height:5px;background:var(--bg-3,hsl(0 0% 12%));border-radius:3px;overflow:hidden}',
385
+ '#team-cost-hero .tch-tk>i{display:block;height:100%;background:var(--accent)}',
386
+ '#team-cost-hero .tch-am{font-size:12px;text-align:right;color:var(--fg);font-family:var(--mono)}',
387
+ '#team-cost-hero .tch-stat .k{font-size:10.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--fg-40,rgba(255,255,255,.4))}',
388
+ '#team-cost-hero .tch-stat .v{font-size:20px;font-weight:600;color:var(--fg)}',
389
+ '#team-cost-hero .tch-mach{display:flex;justify-content:space-between;font-size:12px;padding:4px 0;border-bottom:1px solid var(--border-soft,rgba(255,255,255,.06))}',
390
+ '#team-cost-hero .tch-mach .ok{color:var(--verified,#84b594);font-weight:600}',
391
+ '#team-cost-hero .tch-mach .bad{color:var(--caught,#cf8b7e);font-weight:600}',
392
+ '#team-cost-hero .tch-broken{font-size:12px;color:var(--caught,#cf8b7e);margin:6px 0}',
393
+ '#team-cost-hero .tch-setup{font-size:12px;color:var(--fg-40,rgba(255,255,255,.4));margin-top:12px;padding-top:10px;border-top:1px solid var(--border-soft,rgba(255,255,255,.06))}',
394
+ '#team-cost-hero .tch-setup code{font-family:var(--mono);color:var(--fg-70,rgba(255,255,255,.7))}'
395
+ ].join('');
396
+
397
+ let _submitting=false;
398
+ // It-4 V1-FANOUT-ALL · honest @all fanout. A leading "@all " sends the SAME
399
+ // question to every READY adapter as a read-only batch (one column per vendor,
400
+ // each carrying that vendor's real words). Never fakes a column for a not-ready
401
+ // adapter; with <2 ready vendors there's nothing to compare, so we say so
402
+ // plainly instead of rendering a misleading one-column "fanout".
403
+ async function fanoutAll(ta,btn,prompt){
404
+ const d=_cached.readiness||{};
405
+ const ready=(d.agents||[]).filter(a=>agentS(a.state)==='ready');
406
+ if(ready.length<2){
407
+ al('@all needs at least 2 ready agents to compare \\u00b7 only '+ready.length+' ready right now. Open Settings to set more up, or send to one agent without @all.');
408
+ btn.disabled=false;ta.disabled=false;_submitting=false;
409
+ return;
410
+ }
411
+ const cwd=_room||undefined;
412
+ const tasks=ready.map(a=>({prompt,agent:a.id,...(cwd?{cwd}:{}),readOnly:true}));
413
+ try{
414
+ const r=await fetch('/api/task/batch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tasks})});
415
+ if(!r.ok){const e=await r.json().catch(()=>({}));al('@all failed: '+(e.error||'unknown'));btn.disabled=false;ta.disabled=false;_submitting=false;return;}
416
+ ta.value='';btn.textContent='\\u2713';setTimeout(()=>{btn.textContent='\\u2191';btn.disabled=false;ta.disabled=false;_submitting=false;ta.focus();},1200);
417
+ fetchAll();
418
+ }catch(e){al('Network error: '+e.message);btn.disabled=false;ta.disabled=false;_submitting=false;}
419
+ }
420
+ async function submit(ta,btn,taskId){
421
+ if(_submitting)return;
422
+ const p=ta.value.trim();
423
+ if(!p)return;
424
+ _submitting=true;
425
+ btn.disabled=true;ta.disabled=true;
426
+ // @all fanout — only on a fresh spawn (not a follow-up to one task).
427
+ if(!taskId && /^@all\\s+/i.test(p)){
428
+ return fanoutAll(ta,btn,p.replace(/^@all\\s+/i,'').trim());
429
+ }
430
+ try {
431
+ // When called from detail view (taskId set), route to follow-up endpoint
432
+ // so the message goes to the SAME session that ran the original work.
433
+ // Without this, every "ask follow-up" silently spawned a brand-new task,
434
+ // which is the disabled-composer bug's mirror image: composer enabled but
435
+ // routed wrong.
436
+ const url = taskId ? '/api/task/'+taskId+'/message' : '/api/task';
437
+ let body;
438
+ if(taskId){ body=JSON.stringify({text:p}); }
439
+ else {
440
+ // Room-aware spawn: typing while a goal room is open adds the task TO
441
+ // that goal (cwd pinned to the room) — before this, the room composer
442
+ // silently spawned OUTSIDE the room, the same routing lie as the
443
+ // follow-up bug above. Agent comes from the visible picker; 'auto'
444
+ // omits it so the server's conductor default applies unchanged.
445
+ const payload={prompt:p};
446
+ if(_room) payload.cwd=_room;
447
+ const ap=document.getElementById('agent-pick');
448
+ if(ap&&ap.value&&ap.value!=='auto') payload.agent=ap.value;
449
+ // Hand-off link: a new task typed into an open room CONTINUES the thread.
450
+ // Pin parentSessionId to the most recent turn in this room so the server
451
+ // mirrors a lineage edge — threadGroups then links the conversation even
452
+ // when the next turn is a DIFFERENT agent (the cross-agent hand-off, the
453
+ // wedge). Without this, each room task is an orphan and 0 threads cross agents.
454
+ if(_room){
455
+ // _cached.tasks is a GROUPED object (batch->turns), not an array — every
456
+ // other reader flattens it via allS(); doing the same here avoids the
457
+ // "not iterable" throw. _roomThread.tasks is already a flat array.
458
+ let last=null;
459
+ const pool=allS(_cached.tasks||{}).slice();
460
+ if(_roomThread&&_roomThread.cwd===_room&&Array.isArray(_roomThread.tasks)) for(const t of _roomThread.tasks) pool.push(t);
461
+ for(const t of pool){ if(!t||!t.sessionId||/^demo-/.test(t.sessionId)) continue; if(normC(t.cwd)!==normC(_room)) continue; if(!last||(t.startedAt||0)>(last.startedAt||0)) last=t; }
462
+ if(last&&last.sessionId) payload.parentSessionId=last.sessionId;
463
+ }
464
+ body=JSON.stringify(payload);
465
+ }
466
+ const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body});
467
+ if(!r.ok){
468
+ const e=await r.json().catch(function(){return {};});
469
+ // S1 · a failed send shows a PERSISTENT plain reason at the composer —
470
+ // never a vanishing toast, never a silent no-op, and never the \u2713.
471
+ // S1 honesty, now on BOTH composers: a failed spawn/send shows a
472
+ // PERSISTENT plain reason where the user is looking \u2014 detail uses
473
+ // #detail-send-fail, the home composer uses #main-send-fail. Never a
474
+ // vanishing toast for the product's most important action.
475
+ const sf=document.getElementById(taskId?'detail-send-fail':'main-send-fail');
476
+ if(sf){ sf.textContent='Not sent \u00b7 '+String(e.error||('HTTP '+r.status)); if(taskId)sf.setAttribute('data-sid', taskId); sf.style.display='block'; }
477
+ else al('Failed: '+(e.error||'unknown'));
478
+ btn.disabled=false;ta.disabled=false;_submitting=false;
479
+ }else{
480
+ const sf0=document.getElementById(taskId?'detail-send-fail':'main-send-fail');
481
+ if(sf0){ sf0.style.display='none'; sf0.textContent=''; }
482
+ const resp=await r.json().catch(function(){return {};});
483
+ ta.value='';btn.textContent='\\u2713';setTimeout(()=>{btn.textContent='\\u2191';btn.disabled=false;ta.disabled=false;_submitting=false;},1200);
484
+ await fetchAll();
485
+ // Don't leave the user on the same page — a fresh spawn opens the run it
486
+ // just created, so you immediately SEE the task you described start working.
487
+ // Follow-ups (taskId set) stay put: you're already looking at that task.
488
+ if(!taskId && resp && resp.sessionId){ showD(resp.sessionId); }
489
+ else { ta.focus(); }
490
+ }
491
+ }catch(e){al('Network error: '+e.message);btn.disabled=false;ta.disabled=false;_submitting=false;}
492
+ }
493
+
494
+ function offlineMode(v){
495
+ _offline=v;
496
+ const b=document.getElementById('offline-banner');
497
+ if(b)b.style.display=v?'block':'none';
498
+ }
499
+
500
+ let _healthFails=0;
501
+ async function pollHealth(){
502
+ try {
503
+ const r=await fetch('/health',{cache:'no-store'});
504
+ if(r.ok){if(_healthFails>0){offlineMode(false);fetchAll();}_healthFails=0;}
505
+ else {_healthFails++;if(_healthFails>=2)offlineMode(true);}
506
+ } catch {_healthFails++;if(_healthFails>=2)offlineMode(true);}
507
+ }
508
+
509
+ document.addEventListener('DOMContentLoaded',()=>{
510
+ // A-10 · populate the OSS footer · version from /api/version
511
+ fetch('/api/version').then(r=>r.json()).then(v=>{
512
+ const ve=document.getElementById('oss-version');
513
+ if(ve && v && v.version) ve.textContent='v'+v.version;
514
+ }).catch(()=>{});
515
+ // v0.8 S2.5 · settings drawer wire-up · opened from the ⌘K palette now
516
+ document.getElementById('settings-close')?.addEventListener('click', closeDrawer);
517
+ document.getElementById('drawer-overlay')?.addEventListener('click', closeDrawer);
518
+
519
+ // S20 · lens + density restore — both controls live in ⌘K; the saved lens
520
+ // survives reload through the same body attribute the templates branch on.
521
+ // (Set BEFORE the first fetch renders, so the first paint is already lensed.)
522
+ // ONE consistent experience (2026-06-20): always boot the default view. The
523
+ // persisted lens/density are ignored so the UI never reopens in a different
524
+ // mode than the user left another day. Clear them so they can't resurface.
525
+ document.body.setAttribute('data-lens','default');
526
+ document.body.classList.remove('density-compact');
527
+ try{ localStorage.removeItem('codehere-lens'); localStorage.removeItem('codehere-density-compact'); localStorage.removeItem('codehere_shape'); }catch{}
528
+
529
+ // S13 · ⌘K palette wiring — header hint, type-to-filter, up/down + Enter, Esc.
530
+ document.getElementById('cmdk-open')?.addEventListener('click', openPalette);
531
+ const _palInp=document.getElementById('palette-input');
532
+ if(_palInp){
533
+ _palInp.addEventListener('input', ()=>{ _palSel=0; renderPalette(_palInp.value.trim()); });
534
+ _palInp.addEventListener('keydown', (e)=>{
535
+ if(e.key==='ArrowDown'){ e.preventDefault(); _palSel=Math.min(Math.max(0,_palItems.length-1),_palSel+1); renderPalette(_palInp.value.trim()); }
536
+ else if(e.key==='ArrowUp'){ e.preventDefault(); _palSel=Math.max(0,_palSel-1); renderPalette(_palInp.value.trim()); }
537
+ else if(e.key==='Enter'){ e.preventDefault(); runPaletteItem(_palSel); }
538
+ });
539
+ }
540
+ document.getElementById('palette-list')?.addEventListener('click', (e)=>{
541
+ const row=e.target&&e.target.closest?e.target.closest('.prow'):null;
542
+ if(row) runPaletteItem(Number(row.getAttribute('data-pi')));
543
+ });
544
+ document.getElementById('palette-scrim')?.addEventListener('click', (e)=>{
545
+ if(e.target===document.getElementById('palette-scrim')) closePalette();
546
+ });
547
+
548
+ // S13 · decision-queue keyboard — the printed hints actually work when a
549
+ // row is focused: A approve · X deny · R retry · ⏎ review/primary.
550
+ document.getElementById('decision-queue')?.addEventListener('keydown', (e)=>{
551
+ const row=e.target&&e.target.closest?e.target.closest('.dq-row'):null;
552
+ if(!row) return;
553
+ const click=(sel)=>{ const b=row.querySelector(sel); if(b&&!b.disabled){ e.preventDefault(); b.click(); return true; } return false; };
554
+ const k=String(e.key||'').toLowerCase();
555
+ if(k==='a') click('[data-act="approve"]');
556
+ else if(k==='x') click('[data-act="deny"]');
557
+ else if(k==='r') click('[data-act="retry"]');
558
+ else if(e.key==='Enter'&&e.target===row) click('[data-act="review"],[data-act="approve"]');
559
+ });
560
+
561
+ // v0.8 S3 · Reshape modal wire-up · the palette applies shapes directly;
562
+ // the modal stays for free-text reshape (X / outside-click / Esc closes)
563
+ document.getElementById('reshape-close')?.addEventListener('click', closeReshape);
564
+ document.getElementById('reshape-modal')?.addEventListener('click', e=>{
565
+ if(e.target===document.getElementById('reshape-modal')) closeReshape();
566
+ });
567
+ document.querySelectorAll('.reshape-suggestions .pill').forEach(b=>{
568
+ b.addEventListener('click', ()=>{
569
+ const shape=b.dataset.shape;
570
+ const prompt=b.dataset.prompt;
571
+ const cfg=SHAPE_CONFIGS[shape] || 'density: standard';
572
+ applyShape(shape, prompt, cfg);
573
+ });
574
+ });
575
+ document.getElementById('reshape-submit')?.addEventListener('click', ()=>{
576
+ const inp=document.getElementById('reshape-input');
577
+ const txt=(inp?.value||'').trim();
578
+ if(!txt) return;
579
+ const shape=classifyShape(txt);
580
+ applyShape(shape, txt, 'density: standard · agent-composed from prompt');
581
+ });
582
+ document.getElementById('reshape-input')?.addEventListener('keydown', e=>{
583
+ if(e.key==='Enter') document.getElementById('reshape-submit')?.click();
584
+ });
585
+ // Restore last shape on load
586
+ restoreShape();
587
+ `;
588
+ //# sourceMappingURL=app-script-composer-cost.js.map