codehere 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/use-cases/generate-final-report.js +86 -191
- package/dist/application/use-cases/narrator-prompt-template.js +1 -1
- package/dist/index.js +10 -6
- package/dist/infrastructure/cost/budget-config.js +1 -1
- package/dist/infrastructure/cost/spend-aggregator.js +1 -1
- package/dist/infrastructure/hooks/claude-sdk-hook.js +7 -1
- package/dist/infrastructure/orchestration/agents-config-seed.js +1 -1
- package/dist/infrastructure/orchestration/aider-adapter.js +4 -1
- package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
- package/dist/infrastructure/orchestration/claude-code-adapter.js +5 -1
- package/dist/infrastructure/orchestration/codex-adapter.js +4 -9
- package/dist/infrastructure/orchestration/conductor-adapter.js +9 -3
- package/dist/infrastructure/orchestration/failover-config.js +1 -1
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +2 -2
- package/dist/infrastructure/orchestration/graphify-context.js +18 -13
- package/dist/infrastructure/orchestration/memory-injector.js +50 -2
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +25 -25
- package/dist/infrastructure/orchestration/progress-aggregator.js +41 -6
- package/dist/infrastructure/orchestration/session-manager.js +18 -0
- package/dist/infrastructure/orchestration/session-persistence.js +2 -2
- package/dist/infrastructure/protocols/cloud-sync.js +2 -2
- package/dist/infrastructure/protocols/mcp-server-impl.js +66 -2
- package/dist/infrastructure/protocols/ui-page-v2.js +260 -40
- package/dist/infrastructure/protocols/verify-page.js +42 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +1 -1
- package/dist/infrastructure/storage/project-thread-reader.js +1 -1
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- package/dist/presentation/cli/commands/init-command.js +66 -55
- package/package.json +2 -1
|
@@ -662,14 +662,31 @@ const SHARED_STYLES = `
|
|
|
662
662
|
.audit-line b { color: var(--fg-90); font-weight: 600; }
|
|
663
663
|
.audit-line code { background: hsl(0 0% 9%); padding: 1px 5px; border-radius: 3px; color: var(--fg-70); }
|
|
664
664
|
|
|
665
|
+
/* project-memory nudge · same quiet family as .audit-line (trust present-
|
|
666
|
+
but-quiet); .on = the post-click confirmation state */
|
|
667
|
+
.mem-nudge {
|
|
668
|
+
font-family: var(--mono); font-size: 10.5px; color: var(--fg-40);
|
|
669
|
+
padding: 6px 12px; margin: 8px 0;
|
|
670
|
+
background: rgba(0,0,0,0.2);
|
|
671
|
+
border-left: 2px solid var(--border); border-radius: 3px;
|
|
672
|
+
line-height: 1.55;
|
|
673
|
+
}
|
|
674
|
+
.mem-nudge .mem-btn {
|
|
675
|
+
background: none; border: 1px solid var(--border); border-radius: 4px;
|
|
676
|
+
color: var(--fg-70); font-family: var(--mono); font-size: 10.5px;
|
|
677
|
+
padding: 2px 8px; cursor: pointer; margin-left: 4px;
|
|
678
|
+
}
|
|
679
|
+
.mem-nudge .mem-btn:hover { border-color: var(--accent); color: var(--fg); }
|
|
680
|
+
.mem-nudge.on { border-left-color: rgba(40,200,64,0.4); color: var(--fg-70); }
|
|
681
|
+
|
|
665
682
|
/* auditor persona · audit-chain + verifier + hook-badges amplified,
|
|
666
683
|
summary muted (per demo v12 line 2462: "amplify audit-lines +
|
|
667
684
|
hook-badges + trust-grades") */
|
|
668
|
-
body
|
|
669
|
-
body
|
|
670
|
-
body
|
|
671
|
-
body
|
|
672
|
-
body
|
|
685
|
+
body[data-persona="auditor"] .audit-line { background: rgba(74,144,226,0.06); border-left-color: #4a90e2; color: var(--fg-70); font-size: 11.5px; padding: 8px 14px; }
|
|
686
|
+
body[data-persona="auditor"] .verifier-panel { box-shadow: 0 0 0 2px rgba(40,200,64,0.18); }
|
|
687
|
+
body[data-persona="auditor"] .hook-badge { transform: scale(1.05); }
|
|
688
|
+
body[data-persona="auditor"] .file-chip .trust { font-weight: 700; padding: 2px 7px; }
|
|
689
|
+
body[data-persona="auditor"] .panel:has(p) { opacity: 0.7; }
|
|
673
690
|
|
|
674
691
|
/* SHAPE: velocity · hide tool-call cards, summary prose, tokens, verifier · keep outcome + files */
|
|
675
692
|
body.shape-velocity .tc-card,
|
|
@@ -683,6 +700,13 @@ const SHARED_STYLES = `
|
|
|
683
700
|
body.shape-security .verifier-panel { box-shadow: 0 0 0 2px rgba(255,95,87,0.18); padding: 14px; }
|
|
684
701
|
body.shape-security .hook-block-card { box-shadow: 0 0 0 2px rgba(255,95,87,0.3); }
|
|
685
702
|
|
|
703
|
+
/* Reshape shapes recompose the goal room too (slice 4): "just what shipped"
|
|
704
|
+
* / velocity hide per-turn meta; the audit/security shape amplifies the
|
|
705
|
+
* room's audit line. Reuses the existing body.shape-* + .p-eng/.audit-line. */
|
|
706
|
+
body.shape-ship-only #task-list .card-row2 .p-eng,
|
|
707
|
+
body.shape-velocity #task-list .card-row2 .p-eng { display: none !important; }
|
|
708
|
+
body.shape-security #task-list .audit-line { background: rgba(74,144,226,0.06); border-left-color: #4a90e2; font-size: 11.5px; padding: 8px 14px; }
|
|
709
|
+
|
|
686
710
|
/* v0.8 S2.5 · Settings drawer · ported from demo v12 lines 178-192 + A-09
|
|
687
711
|
* in the backlog. Right-side off-canvas panel · holds Context Sources +
|
|
688
712
|
* future section-visibility toggles. */
|
|
@@ -936,6 +960,29 @@ const SHARED_STYLES = `
|
|
|
936
960
|
box-shadow: 0 0 0 3px rgba(255,154,118,0.18);
|
|
937
961
|
}
|
|
938
962
|
|
|
963
|
+
/* Goal-first home · each project (goal) is a group; its tasks live inside.
|
|
964
|
+
* The home is organized by what you're working toward, not by status. */
|
|
965
|
+
.goal-group { margin: 0 0 22px; }
|
|
966
|
+
.goal-head {
|
|
967
|
+
display: flex; align-items: baseline; gap: 10px;
|
|
968
|
+
padding: 0 0 8px; margin: 20px 0 10px;
|
|
969
|
+
border-bottom: 1px solid var(--border);
|
|
970
|
+
}
|
|
971
|
+
.goal-group:first-child .goal-head { margin-top: 0; }
|
|
972
|
+
.goal-title { font-size: 15px; font-weight: 600; color: var(--fg); letter-spacing: -0.01em; }
|
|
973
|
+
.goal-meta { font-family: var(--mono); font-size: 10.5px; color: var(--fg-40); margin-left: auto; }
|
|
974
|
+
.goal-head { cursor: pointer; }
|
|
975
|
+
.goal-head:hover .goal-title { color: var(--accent); }
|
|
976
|
+
.goal-head:hover .goal-meta { color: var(--fg-70); }
|
|
977
|
+
|
|
978
|
+
/* The goal room · a single goal opened to its multi-agent turns, in order. */
|
|
979
|
+
.room-head { margin: 4px 0 14px; }
|
|
980
|
+
.room-back { background: transparent; border: 0; color: var(--fg-50); font-family: var(--mono); font-size: 11px; cursor: pointer; padding: 0; margin-bottom: 12px; }
|
|
981
|
+
.room-back:hover { color: var(--fg); }
|
|
982
|
+
.room-title { font-size: 20px; font-weight: 600; color: var(--fg); letter-spacing: -0.01em; }
|
|
983
|
+
.room-sub { font-family: var(--mono); font-size: 11px; color: var(--fg-40); margin-top: 5px; }
|
|
984
|
+
.turns-label { font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; color: var(--fg-40); text-transform: uppercase; margin: 18px 0 12px; }
|
|
985
|
+
|
|
939
986
|
/* Task card · glass surface · matches demo v12 .glass-card pattern.
|
|
940
987
|
* Faint white-on-dark fill + 1px low-contrast border · sits on top of
|
|
941
988
|
* the ambient dot-grid. Hover lifts 2px with a bigger shadow + inner
|
|
@@ -1515,8 +1562,9 @@ const SHARED_STYLES = `
|
|
|
1515
1562
|
.input-ta::placeholder { color: var(--fg-40); }
|
|
1516
1563
|
.input-ta:disabled { color: var(--fg-30); }
|
|
1517
1564
|
.input-meta { font-family: var(--mono); font-size: 10px; color: var(--fg-40); white-space: nowrap; display: inline-flex; gap: 8px; align-items: center; }
|
|
1518
|
-
.input-meta .pick-agent { color: var(--fg-70); cursor: pointer; padding: 2px 6px; border-radius: 3px; transition: background 0.15s; }
|
|
1565
|
+
.input-meta .pick-agent { color: var(--fg-70); cursor: pointer; padding: 2px 6px; border-radius: 3px; transition: background 0.15s; background: transparent; border: none; font: inherit; -webkit-appearance: none; appearance: none; }
|
|
1519
1566
|
.input-meta .pick-agent:hover { background: rgba(255,255,255,0.05); color: var(--fg); }
|
|
1567
|
+
.input-meta select.pick-agent option { background: var(--bg-2, #1a1a1f); color: var(--fg); }
|
|
1520
1568
|
.input-meta .cost-est { color: var(--allow); padding: 2px 6px; border-radius: 3px; background: rgba(40,200,64,0.06); }
|
|
1521
1569
|
.send-btn {
|
|
1522
1570
|
background: var(--accent); color: #000; border: none; border-radius: 6px;
|
|
@@ -1536,7 +1584,11 @@ const SHARED_STYLES = `
|
|
|
1536
1584
|
`;
|
|
1537
1585
|
const APP_SCRIPT = `
|
|
1538
1586
|
const _cached = {};
|
|
1539
|
-
let _filter = 'all', _detail = null, _offline = false;
|
|
1587
|
+
let _filter = 'all', _detail = null, _room = null, _offline = false, _autoOpened = false;
|
|
1588
|
+
// Project-memory state for the open room · null = unknown (fetch in flight),
|
|
1589
|
+
// false = agents forget this project (show the nudge), true = memory already
|
|
1590
|
+
// on (stay quiet), 'just-on' = turned on from the nudge this visit (confirm).
|
|
1591
|
+
let _roomMemory = null;
|
|
1540
1592
|
const ICONS = { queued:'\\u23F3', running:'\\u25CF', paused:'\\u23F8', blocked:'\\uD83D\\uDD12', done:'\\u2713', failed:'\\u2715', crashed:'!', killed:'\\u2298' };
|
|
1541
1593
|
const STAT_ORDER = ['running','queued','paused','blocked','done','failed','crashed','killed'];
|
|
1542
1594
|
const FILTERS = { all: s=>true, running: s=>['running','queued','paused','blocked'].includes(s.status), done: s=>['done','completed'].includes(s.status), failed: s=>['failed','crashed','killed','error'].includes(s.status) };
|
|
@@ -1668,12 +1720,14 @@ function codeCard(path, snippet, lang) {
|
|
|
1668
1720
|
}
|
|
1669
1721
|
function allS(d) { const sp=d.spawned||d.sessions||[], pe=d.persisted||[], seen=new Set(sp.map(s=>s.sessionId)); return [...sp,...pe.filter(p=>!seen.has(p.sessionId))]; }
|
|
1670
1722
|
function cardActions(s) {
|
|
1723
|
+
// Only 'kill' is wired (POST /api/task/:id/kill). Pause/Resume/Restart had NO
|
|
1724
|
+
// backend endpoint — they rendered as live buttons that silently no-op'd (R11
|
|
1725
|
+
// honest-state lie, 2026-06-03). Removed until real process control lands; Kill
|
|
1726
|
+
// and Cancel both map to the wired kill, so every rendered control here works.
|
|
1671
1727
|
const m={
|
|
1672
|
-
running:'<button class="card-btn
|
|
1673
|
-
paused:'<button class="card-btn
|
|
1728
|
+
running:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Kill</button>',
|
|
1729
|
+
paused:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Kill</button>',
|
|
1674
1730
|
queued:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Cancel</button>',
|
|
1675
|
-
crashed:'<button class="card-btn restart" onclick="event.stopPropagation();a(\\'restart\\',\\''+s+'\\')">Restart</button>',
|
|
1676
|
-
failed:'<button class="card-btn restart" onclick="event.stopPropagation();a(\\'restart\\',\\''+s+'\\')">Restart</button>',
|
|
1677
1731
|
blocked:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Cancel</button>',
|
|
1678
1732
|
};
|
|
1679
1733
|
return m[mapped(s)]||'';
|
|
@@ -1691,11 +1745,101 @@ function mapped(s) { return s==='queued'?'running':s==='blocked'?'paused':aStat(
|
|
|
1691
1745
|
function findS(id) { const s=allS(_cached.tasks||{}); return s.find(x=>x.sessionId===id)||null; }
|
|
1692
1746
|
|
|
1693
1747
|
function render() {
|
|
1694
|
-
if(_detail){ const t=findS(_detail); if(t)renderD(t); else{ _detail=null; renderL(); } }
|
|
1748
|
+
if(_detail){ const t=findS(_detail); if(t)renderD(t); else{ _detail=null; if(_room)renderRoom(); else renderL(); } }
|
|
1749
|
+
else if(_room) renderRoom();
|
|
1695
1750
|
else renderL();
|
|
1696
1751
|
upCnt(); upBud(); upSq(); upWk(); upIdx();
|
|
1697
1752
|
}
|
|
1698
1753
|
|
|
1754
|
+
// The goal room — one goal opened to its tasks as a chronological thread of
|
|
1755
|
+
// agent turns. Renders into #task-list (same surface as the goals list).
|
|
1756
|
+
function openRoom(enc){
|
|
1757
|
+
_room=decodeURIComponent(enc||''); _detail=null;
|
|
1758
|
+
// Project-memory status for the nudge — fetched once per entry from the
|
|
1759
|
+
// daemon (the SAME readiness the injector acts on, not a guess).
|
|
1760
|
+
_roomMemory=null;
|
|
1761
|
+
(function(forCwd){
|
|
1762
|
+
fetch('/api/memory/status?cwd='+encodeURIComponent(forCwd)).then(x=>x.json()).then(j=>{
|
|
1763
|
+
if(_room===forCwd){ _roomMemory=!!j.ready; render(); }
|
|
1764
|
+
}).catch(()=>{});
|
|
1765
|
+
})(_room);
|
|
1766
|
+
document.getElementById('task-list-view').style.display='';
|
|
1767
|
+
document.getElementById('task-detail-view').style.display='none';
|
|
1768
|
+
document.body.classList.remove('detail-open');
|
|
1769
|
+
// Hand-off affordance: inside a room the composer targets THIS goal (cwd is
|
|
1770
|
+
// pinned in submit) and the picker defaults to whoever worked it last —
|
|
1771
|
+
// continuing is zero clicks, handing to a different agent is one visible
|
|
1772
|
+
// click. Set once on entry (not in renderRoom, which re-runs every poll and
|
|
1773
|
+
// would stomp the user's choice).
|
|
1774
|
+
const ta=document.getElementById('main-ta');
|
|
1775
|
+
if(ta) ta.placeholder='Add the next task in this goal\\u2026 any agent picks up where the last one left off';
|
|
1776
|
+
const ap=document.getElementById('agent-pick');
|
|
1777
|
+
if(ap){
|
|
1778
|
+
const turns=allS(_cached.tasks||{}).filter(x=>!/^demo-/.test(x.sessionId||'')&&(x.cwd||'')===_room&&x.agent).sort((a,b)=>(a.startedAt||0)-(b.startedAt||0));
|
|
1779
|
+
const raw=turns.length?String(turns[turns.length-1].agent):'';
|
|
1780
|
+
ap.value=['claude-code','codex','aider','gemini','opencode'].indexOf(raw)>=0?raw:'auto';
|
|
1781
|
+
}
|
|
1782
|
+
render();
|
|
1783
|
+
}
|
|
1784
|
+
function closeRoom(){
|
|
1785
|
+
_room=null; _roomMemory=null;
|
|
1786
|
+
const ta=document.getElementById('main-ta');
|
|
1787
|
+
if(ta) ta.placeholder='Describe a task or ask a question...';
|
|
1788
|
+
const ap=document.getElementById('agent-pick'); if(ap) ap.value='auto';
|
|
1789
|
+
render();
|
|
1790
|
+
}
|
|
1791
|
+
async function initRoomMemory(btn){
|
|
1792
|
+
if(!_room) return;
|
|
1793
|
+
if(btn){ btn.disabled=true; btn.textContent='turning on\\u2026'; }
|
|
1794
|
+
try{
|
|
1795
|
+
const r=await fetch('/api/memory/init',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cwd:_room})});
|
|
1796
|
+
if(r.ok){ _roomMemory='just-on'; render(); }
|
|
1797
|
+
else { const e=await r.json().catch(()=>({})); al('Could not turn on memory: '+(e.error||e.reason||'unknown')); if(btn){btn.disabled=false;btn.textContent='turn on project memory';} }
|
|
1798
|
+
}catch(e){ al('Network error: '+e.message); if(btn){btn.disabled=false;btn.textContent='turn on project memory';} }
|
|
1799
|
+
}
|
|
1800
|
+
function renderRoom(){
|
|
1801
|
+
const c=document.getElementById('task-list');
|
|
1802
|
+
const _hs=document.getElementById('host-sessions-strip'); if(_hs){ _hs.style.display='none'; }
|
|
1803
|
+
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,ch=>({'&':'&','<':'<','>':'>','"':'"'}[ch]));
|
|
1804
|
+
const cwd=_room;
|
|
1805
|
+
const items=allS(_cached.tasks||{}).filter(x=>!/^demo-/.test(x.sessionId||'') && (x.cwd||'')===cwd);
|
|
1806
|
+
items.sort((a,b)=>(a.startedAt||0)-(b.startedAt||0)); // turns in the order they happened
|
|
1807
|
+
const projects=_cached.projects||[];
|
|
1808
|
+
const p=projects.find(x=>x.cwd===cwd);
|
|
1809
|
+
const title=(p&&p.title)||(cwd?(cwd.split('/').filter(Boolean).pop()||'Goal'):'Goal');
|
|
1810
|
+
const agents=[...new Set(items.map(x=>x.agent).filter(Boolean))];
|
|
1811
|
+
let html='<div class="room-head"><button class="room-back" onclick="closeRoom()">\\u2190 all goals</button>';
|
|
1812
|
+
html+='<div class="room-title">'+esc(title)+'</div>';
|
|
1813
|
+
// Persona-adaptive (direction B): everyone sees the plain-English outcome;
|
|
1814
|
+
// the technical path is engineer-only (.p-eng · hidden for PM/designer via
|
|
1815
|
+
// the existing body[data-persona] CSS — recomposes live, no re-render).
|
|
1816
|
+
const _done=items.filter(x=>x.status==='completed'||x.status==='done').length;
|
|
1817
|
+
const _outcome=items.length?(_done===items.length?('\\u2713 all '+items.length+' shipped'):(_done+' of '+items.length+' shipped')):'no tasks yet';
|
|
1818
|
+
html+='<div class="room-sub">'+esc(_outcome)+(agents.length?' \\u00b7 '+agents.map(esc).join(', '):'')+'<span class="p-eng"> \\u00b7 '+esc(cwd||'')+'</span></div></div>';
|
|
1819
|
+
if(items.length){
|
|
1820
|
+
html+='<div class="turns-label">How your agents worked this goal, in order</div>';
|
|
1821
|
+
for(const t of items) html+=cardH(t);
|
|
1822
|
+
// Auditor signal · subtle for everyone, amplified for the Auditor persona
|
|
1823
|
+
// via body[data-persona="auditor"] .audit-line. Honest: real step count,
|
|
1824
|
+
// and every step IS in the local hash-chained audit log.
|
|
1825
|
+
const _stepsTotal=items.reduce((n,t)=>n+((t.steps||[]).length),0);
|
|
1826
|
+
html+='<div class="audit-line allow" style="margin-top:14px">🔒 every step in this goal is in the local audit chain \\u00b7 <b>'+_stepsTotal+' steps</b> \\u00b7 verify \\u21bb</div>';
|
|
1827
|
+
} else {
|
|
1828
|
+
html+='<div class="empty-state"><h2>No tasks in this goal yet.</h2></div>';
|
|
1829
|
+
}
|
|
1830
|
+
// Project-memory nudge (stranger-walk finding 2026-06-06): web-only users
|
|
1831
|
+
// never run codehere-init, so agents silently get NO memory of the
|
|
1832
|
+
// project. Honest + quiet: rendered ONLY when the daemon says memory is
|
|
1833
|
+
// off; the confirmation says "from the next task" because injection
|
|
1834
|
+
// happens at spawn — no fake instant effect.
|
|
1835
|
+
if(_roomMemory===false){
|
|
1836
|
+
html+='<div class="mem-nudge">🧠 agents forget this project between runs \\u00b7 <button class="mem-btn" onclick="initRoomMemory(this)">turn on project memory</button></div>';
|
|
1837
|
+
} else if(_roomMemory==='just-on'){
|
|
1838
|
+
html+='<div class="mem-nudge on">\\u2713 project memory is on \\u2014 agents will remember this project from the next task</div>';
|
|
1839
|
+
}
|
|
1840
|
+
c.innerHTML=html;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1699
1843
|
// C-01 · first-run indexing banner · auto-show when session count < 3 +
|
|
1700
1844
|
// only once per browser session (sessionStorage tracks the dismissal).
|
|
1701
1845
|
function upIdx(){
|
|
@@ -1773,39 +1917,66 @@ function fmtGap(ms){
|
|
|
1773
1917
|
|
|
1774
1918
|
function renderL() {
|
|
1775
1919
|
const c=document.getElementById('task-list'), all=allS(_cached.tasks||{});
|
|
1920
|
+
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,ch=>({'&':'&','<':'<','>':'>','"':'"'}[ch]));
|
|
1776
1921
|
const fl=all.filter(FILTERS[_filter]||FILTERS.all);
|
|
1777
1922
|
let html='';
|
|
1778
|
-
//
|
|
1779
|
-
//
|
|
1923
|
+
// Demo-shape sessions (id prefix 'demo-') render AFTER the goals — they
|
|
1924
|
+
// demonstrate the product, but the home leads with YOUR goals (spec:
|
|
1925
|
+
// "Home is a list of your goals"; demos were burying it).
|
|
1780
1926
|
const demo=fl.filter(x=>/^demo-/.test(x.sessionId||''));
|
|
1781
1927
|
const rest=fl.filter(x=>!/^demo-/.test(x.sessionId||''));
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
const
|
|
1788
|
-
if(
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1928
|
+
// Goal-first home: group the rest by project (the goal/room), not by status.
|
|
1929
|
+
// cwd → project title (from /api/projects); fall back to the folder name.
|
|
1930
|
+
const projects=_cached.projects||[];
|
|
1931
|
+
const titleFor=cwd=>{
|
|
1932
|
+
if(!cwd) return 'Ungrouped';
|
|
1933
|
+
const p=projects.find(x=>x.cwd===cwd);
|
|
1934
|
+
if(p&&p.title) return p.title;
|
|
1935
|
+
const parts=String(cwd).split('/').filter(Boolean);
|
|
1936
|
+
return parts.length?parts[parts.length-1]:'Ungrouped';
|
|
1937
|
+
};
|
|
1938
|
+
const groups=new Map();
|
|
1939
|
+
for(const t of rest){
|
|
1940
|
+
const key=t.cwd||'ungrouped';
|
|
1941
|
+
if(!groups.has(key)) groups.set(key,{cwd:t.cwd,items:[]});
|
|
1942
|
+
groups.get(key).items.push(t);
|
|
1943
|
+
}
|
|
1944
|
+
const lastTs=g=>g.items.reduce((m,x)=>Math.max(m,x.startedAt||0),0);
|
|
1945
|
+
const ordered=[...groups.values()].sort((a,b)=>lastTs(b)-lastTs(a));
|
|
1946
|
+
let prevGroupTs=null;
|
|
1947
|
+
for(const g of ordered){
|
|
1948
|
+
// Time-gap separator BETWEEN goals (ordered by recency) — a big gap means
|
|
1949
|
+
// these goals are from a different stretch of work ("last week").
|
|
1950
|
+
const gts=lastTs(g);
|
|
1951
|
+
if(prevGroupTs!==null && gts>0 && prevGroupTs>0){
|
|
1952
|
+
const gg=Math.abs(prevGroupTs-gts);
|
|
1953
|
+
if(gg>2*3600*1000){ html+='<div class="time-gap"><span class="line"></span><span class="label">'+fmtGap(gg)+'</span><span class="line"></span></div>'; }
|
|
1954
|
+
}
|
|
1955
|
+
prevGroupTs=gts;
|
|
1956
|
+
g.items.sort((a,b)=>(b.startedAt||0)-(a.startedAt||0));
|
|
1957
|
+
const running=g.items.filter(x=>['running','queued','paused','blocked'].includes(x.status)).length;
|
|
1958
|
+
const meta=g.items.length+' task'+(g.items.length===1?'':'s')+(running?' · '+running+' running':'');
|
|
1959
|
+
html+='<div class="goal-group"><div class="goal-head" data-goal-cwd="'+encodeURIComponent(g.cwd||'')+'" onclick="openRoom(\\''+encodeURIComponent(g.cwd||'')+'\\')"><span class="goal-title">'+esc(titleFor(g.cwd))+'</span><span class="goal-meta">'+esc(meta)+' \\u00b7 open \\u2192</span></div>';
|
|
1960
|
+
// Time-gap separator within a goal · a big gap between consecutive tasks
|
|
1961
|
+
// in the same goal is meaningful ("picked this back up 3 days later").
|
|
1791
1962
|
let prevTs=null;
|
|
1792
|
-
for(const t of items){
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
// 'running' (still alive, no gap) and the first card (no predecessor).
|
|
1796
|
-
const ts = t.startedAt || 0;
|
|
1797
|
-
if(g!=='running' && prevTs!==null && ts>0 && prevTs>0){
|
|
1963
|
+
for(const t of g.items){
|
|
1964
|
+
const ts=t.startedAt||0;
|
|
1965
|
+
if(prevTs!==null && ts>0 && prevTs>0){
|
|
1798
1966
|
const gap=Math.abs(prevTs-ts);
|
|
1799
|
-
if(gap
|
|
1800
|
-
html+='<div class="time-gap"><span class="line"></span><span class="label">'+fmtGap(gap)+'</span><span class="line"></span></div>';
|
|
1801
|
-
}
|
|
1967
|
+
if(gap>2*3600*1000){ html+='<div class="time-gap"><span class="line"></span><span class="label">'+fmtGap(gap)+'</span><span class="line"></span></div>'; }
|
|
1802
1968
|
}
|
|
1803
1969
|
html+=cardH(t);
|
|
1804
1970
|
prevTs=ts;
|
|
1805
1971
|
}
|
|
1972
|
+
html+='</div>';
|
|
1973
|
+
}
|
|
1974
|
+
if(demo.length){
|
|
1975
|
+
html+='<div class="section-label featured">Featured</div>';
|
|
1976
|
+
for(const t of demo) html+=cardH(t);
|
|
1806
1977
|
}
|
|
1807
1978
|
if(!html){
|
|
1808
|
-
const h={all:'No
|
|
1979
|
+
const h={all:'No goals yet. Type a task below to start one.',running:'No running tasks.',done:'No completed tasks.',failed:'No failed tasks.'};
|
|
1809
1980
|
html='<div class="empty-state"><h2>'+(h[_filter]||'No tasks found.')+'</h2></div>';
|
|
1810
1981
|
}
|
|
1811
1982
|
c.innerHTML=html;
|
|
@@ -1814,6 +1985,8 @@ function renderL() {
|
|
|
1814
1985
|
function renderHostSessions(data){
|
|
1815
1986
|
const strip=document.getElementById('host-sessions-strip');
|
|
1816
1987
|
if(!strip)return;
|
|
1988
|
+
// The observed-sessions strip belongs to the goals home, not inside a room.
|
|
1989
|
+
if(_room){ strip.style.display='none'; strip.innerHTML=''; return; }
|
|
1817
1990
|
const sessions=(data&&data.sessions)||[];
|
|
1818
1991
|
if(!sessions.length){ strip.style.display='none'; strip.innerHTML=''; return; }
|
|
1819
1992
|
function he(s){ return String(s).replace(/[&<>"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
@@ -1880,7 +2053,7 @@ function cardH(t) {
|
|
|
1880
2053
|
const stalledHtml = t.stalledReason ? '<div class="card-stalled" title="the watchdog stopped this task because the agent went silent">'+he(t.stalledReason)+'</div>' : '';
|
|
1881
2054
|
return '<div class="task-card '+agentClass+pinClass+'" onclick="showD(\\''+t.sessionId+'\\')">'
|
|
1882
2055
|
+'<div class="card-row1">'+statIcon(s)+'<span class="card-title" title="'+displayPrompt(t.prompt).replace(/"/g,'"').slice(0,400)+'">'+title(t.prompt)+'</span>'+tagHtml(t.tags||[])+agentChip+'</div>'
|
|
1883
|
-
+'<div class="card-row2">'+metaParts.join('')+verB(t.verification)+outcomeHtml+'</div>'
|
|
2056
|
+
+'<div class="card-row2"><span class="p-eng">'+metaParts.join('')+'</span>'+verB(t.verification)+outcomeHtml+'</div>'
|
|
1884
2057
|
+stalledHtml
|
|
1885
2058
|
+(cardActions(t.sessionId)?'<div class="card-row3">'+cardActions(t.sessionId)+'</div>':'')+'</div>';
|
|
1886
2059
|
}
|
|
@@ -2401,6 +2574,12 @@ function renderD(t) {
|
|
|
2401
2574
|
// Empty finalReport object or null/empty summary => honest "unavailable" outcome,
|
|
2402
2575
|
// not a green check. Composer must offer a re-spawn affordance for these cases.
|
|
2403
2576
|
const hasReportText = !!(t.finalReport && typeof t.finalReport.summary === 'string' && t.finalReport.summary.trim());
|
|
2577
|
+
// §G R8 — green ✓ "Final Report" ONLY for a verified live-agent summary
|
|
2578
|
+
// (source === 'agent-summary'). Every other source — deterministic fallback,
|
|
2579
|
+
// error-recovery, mock synthesis — renders an honest ⚠, never a fake ✓.
|
|
2580
|
+
// POSITIVE allowlist (not deny-one) so a new/unknown source can't sneak a ✓.
|
|
2581
|
+
const reportSource = (t.finalReport && t.finalReport.source) || '';
|
|
2582
|
+
const isVerifiedAgentReport = reportSource === 'agent-summary';
|
|
2404
2583
|
const isTerminal = ['completed','done','killed','failed','error','crashed'].includes(t.status);
|
|
2405
2584
|
// The headline "N files changed" number. Normally derived from the actual
|
|
2406
2585
|
// artifact list, so claim === rendered count, always. The optional
|
|
@@ -2415,8 +2594,13 @@ function renderD(t) {
|
|
|
2415
2594
|
} else {
|
|
2416
2595
|
oHtml+=(claimedFiles?'<div class="outcome-item"><span class="check">'+ms(true)+'</span> '+claimedFiles+' file'+(claimedFiles===1?'':'s')+' changed</div>':'');
|
|
2417
2596
|
oHtml+='<div class="outcome-item"><span class="'+(nStep>0?'check':'pending')+'">'+(nStep>0?ms(true):'\\u2022')+'</span> '+nStep+' step'+(nStep===1?'':'s')+'</div>';
|
|
2418
|
-
if(hasReportText) {
|
|
2597
|
+
if(hasReportText && isVerifiedAgentReport) {
|
|
2419
2598
|
oHtml+='<div class="outcome-item"><span class="check">'+ms(true)+'</span> Final Report</div>';
|
|
2599
|
+
} else if(hasReportText) {
|
|
2600
|
+
const _rl = reportSource==='deterministic' ? 'No written report \\u2014 recorded facts only'
|
|
2601
|
+
: reportSource==='error-recovery' ? 'Final Report \\u2014 task did not complete'
|
|
2602
|
+
: 'Final Report \\u2014 unverified (not a live-agent summary)';
|
|
2603
|
+
oHtml+='<div class="outcome-item"><span class="pending">\\u26A0</span> '+_rl+'</div>';
|
|
2420
2604
|
} else if(isTerminal) {
|
|
2421
2605
|
oHtml+='<div class="outcome-item"><span class="pending">\\u26A0</span> Final Report unavailable</div>';
|
|
2422
2606
|
}
|
|
@@ -3190,9 +3374,28 @@ function al(m){const e=document.getElementById('toast');if(e){e.textContent=m;e.
|
|
|
3190
3374
|
async function fetchAll() {
|
|
3191
3375
|
const sk=document.getElementById('skeleton');if(sk){const p=sk.parentNode;if(p)p.removeChild(sk);}
|
|
3192
3376
|
try {
|
|
3193
|
-
const [t,r]=await Promise.all([fetch('/api/tasks').then(x=>x.json()),fetch('/api/readiness/agents').then(x=>x.json())]);
|
|
3194
|
-
_cached.tasks=t;_cached.readiness=r;
|
|
3377
|
+
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:[]}))]);
|
|
3378
|
+
_cached.tasks=t;_cached.readiness=r;_cached.projects=(pj&&pj.projects)||[];
|
|
3195
3379
|
if(t.pricing)_cached.pricing=t.pricing;
|
|
3380
|
+
// Auto-open the most relevant thread on first load (running > most-recent) —
|
|
3381
|
+
// landing on a bare list "did nothing" feel (Ega 2026-06-02). Once only, and
|
|
3382
|
+
// only if the user hasn't already navigated; Back still returns to the list.
|
|
3383
|
+
if(!_autoOpened){
|
|
3384
|
+
const _all=allS(t||{});
|
|
3385
|
+
if(_all.length){
|
|
3386
|
+
_autoOpened=true;
|
|
3387
|
+
if(_detail===null){
|
|
3388
|
+
// Demos never hijack first load — auto-open is for YOUR work
|
|
3389
|
+
// (a seeded demo stuck on "running" was stealing it · 2026-06-06 walk).
|
|
3390
|
+
const _real=_all.filter(x=>!/^demo-/.test(x.sessionId||''));
|
|
3391
|
+
const _act=_real.filter(x=>['running','queued','paused','blocked'].includes(x.status));
|
|
3392
|
+
const _pool=_act.length?_act:_real.slice();
|
|
3393
|
+
_pool.sort((a,b)=>(b.startedAt||0)-(a.startedAt||0));
|
|
3394
|
+
const _pick=_pool[0];
|
|
3395
|
+
if(_pick&&_pick.sessionId){ showD(_pick.sessionId); }
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3196
3399
|
render();
|
|
3197
3400
|
}catch(e){console.error('fetch error',e);}
|
|
3198
3401
|
try {
|
|
@@ -3221,7 +3424,20 @@ async function submit(ta,btn,taskId){
|
|
|
3221
3424
|
// which is the disabled-composer bug's mirror image: composer enabled but
|
|
3222
3425
|
// routed wrong.
|
|
3223
3426
|
const url = taskId ? '/api/task/'+taskId+'/message' : '/api/task';
|
|
3224
|
-
|
|
3427
|
+
let body;
|
|
3428
|
+
if(taskId){ body=JSON.stringify({text:p}); }
|
|
3429
|
+
else {
|
|
3430
|
+
// Room-aware spawn: typing while a goal room is open adds the task TO
|
|
3431
|
+
// that goal (cwd pinned to the room) — before this, the room composer
|
|
3432
|
+
// silently spawned OUTSIDE the room, the same routing lie as the
|
|
3433
|
+
// follow-up bug above. Agent comes from the visible picker; 'auto'
|
|
3434
|
+
// omits it so the server's conductor default applies unchanged.
|
|
3435
|
+
const payload={prompt:p};
|
|
3436
|
+
if(_room) payload.cwd=_room;
|
|
3437
|
+
const ap=document.getElementById('agent-pick');
|
|
3438
|
+
if(ap&&ap.value&&ap.value!=='auto') payload.agent=ap.value;
|
|
3439
|
+
body=JSON.stringify(payload);
|
|
3440
|
+
}
|
|
3225
3441
|
const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body});
|
|
3226
3442
|
if(!r.ok){const e=await r.json();al('Failed: '+(e.error||'unknown'));btn.disabled=false;ta.disabled=false;_submitting=false;}else{
|
|
3227
3443
|
ta.value='';btn.textContent='\\u2713';setTimeout(()=>{btn.textContent='\\u2191';btn.disabled=false;ta.disabled=false;_submitting=false;ta.focus();},1200);
|
|
@@ -3946,6 +4162,7 @@ export const TASK_HTML_V2 = `<!doctype html>
|
|
|
3946
4162
|
<option value="pm">PM view</option>
|
|
3947
4163
|
<option value="designer">Designer view</option>
|
|
3948
4164
|
<option value="eng">Engineer view</option>
|
|
4165
|
+
<option value="auditor">Auditor view</option>
|
|
3949
4166
|
</select>
|
|
3950
4167
|
<button class="density-toggle" id="density-toggle" title="Toggle compact / standard density">⊞ Compact</button>
|
|
3951
4168
|
<button class="reshape-btn" id="reshape-open" title="Describe the view you want · agent composes from primitives">⌘ Reshape view</button>
|
|
@@ -3997,7 +4214,6 @@ export const TASK_HTML_V2 = `<!doctype html>
|
|
|
3997
4214
|
|
|
3998
4215
|
<div class="content">
|
|
3999
4216
|
<div id="task-list-view" class="content-inner">
|
|
4000
|
-
<div id="host-sessions-strip" style="display:none"></div>
|
|
4001
4217
|
<div id="task-list">
|
|
4002
4218
|
<div class="skeleton" id="skeleton">
|
|
4003
4219
|
<div class="skeleton-card"></div>
|
|
@@ -4005,6 +4221,10 @@ export const TASK_HTML_V2 = `<!doctype html>
|
|
|
4005
4221
|
<div class="skeleton-card"></div>
|
|
4006
4222
|
</div>
|
|
4007
4223
|
</div>
|
|
4224
|
+
<!-- Observed host sessions live BELOW the goals: the spec's home is
|
|
4225
|
+
"a list of your goals" — ambient observation must not bury the
|
|
4226
|
+
thesis surface (found burying it during the 2026-06-06 walk). -->
|
|
4227
|
+
<div id="host-sessions-strip" style="display:none"></div>
|
|
4008
4228
|
</div>
|
|
4009
4229
|
<div id="task-detail-view" style="display:none" class="content-inner">
|
|
4010
4230
|
<div id="detail-view"></div>
|
|
@@ -4021,7 +4241,7 @@ export const TASK_HTML_V2 = `<!doctype html>
|
|
|
4021
4241
|
<div class="input-area">
|
|
4022
4242
|
<div class="input-bar">
|
|
4023
4243
|
<textarea class="input-ta composer-bar" id="main-ta" placeholder="Describe a task or ask a question..." rows="1"></textarea>
|
|
4024
|
-
<div class="input-meta composer-meta"><
|
|
4244
|
+
<div class="input-meta composer-meta"><select class="pick-agent" id="agent-pick" title="Who does this task — Auto picks the best fit" aria-label="Pick the agent for this task"><option value="auto">↳ Auto</option><option value="claude-code">claude-code</option><option value="codex">codex</option><option value="aider">aider</option><option value="gemini">gemini</option><option value="opencode">opencode</option></select><span class="cost-est" title="Estimated cost · based on prompt length + recent agent pricing">~$0.02</span></div>
|
|
4025
4245
|
<button class="send-btn composer-send" id="main-btn">↑</button>
|
|
4026
4246
|
</div>
|
|
4027
4247
|
<div class="kbd-hint">
|
|
@@ -298,6 +298,23 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
298
298
|
{ id:"F-TASK-CARD", name:"Task card", surface:"list", sel:".task-card", status:"shipped", tier:"v0.7" },
|
|
299
299
|
{ id:"F-TIME-GAP-PILL", name:"Brand pill + relative-time sub", surface:"list", sel:".time-gap", status:"shipped", tier:"v0.7" },
|
|
300
300
|
{ id:"F-WORKTREE-ALERT", name:"Worktree alert", surface:"list", sel:".worktree-alert", status:"partial", tier:"v0.7" },
|
|
301
|
+
/* goal-first home + goal room (v1 spec · shared-room hand-off). Goal groups
|
|
302
|
+
render from REAL non-demo tasks (same data dependency as F-TIME-GAP-PILL);
|
|
303
|
+
room features are observed by driving the REAL openRoom() on the first
|
|
304
|
+
goal-head's data-goal-cwd, then closeRoom() — same pattern as showD(). */
|
|
305
|
+
{ id:"F-GOAL-GROUP", name:"Goal-first home · tasks grouped by goal", surface:"list", sel:".goal-group", status:"shipped", tier:"v1" },
|
|
306
|
+
{ id:"F-GOAL-HEAD-OPEN", name:"Goal head opens its room (carries cwd)", surface:"list", sel:".goal-head[data-goal-cwd]", status:"shipped", tier:"v1" },
|
|
307
|
+
{ id:"F-AGENT-PICK", name:"Composer agent picker (Auto + 5 agents)", surface:"list", sel:"select#agent-pick", status:"shipped", tier:"v1" },
|
|
308
|
+
/* room selectors are scoped to #task-list: the hidden detail view's DOM
|
|
309
|
+
persists (display:none still matches querySelectorAll) and also has an
|
|
310
|
+
.audit-line — unscoped, a dead room would false-OBSERVE off it. */
|
|
311
|
+
{ id:"F-ROOM-HEAD", name:"Goal room header (title + outcome sub)", surface:"room", sel:"#task-list .room-head .room-title", status:"shipped", tier:"v1" },
|
|
312
|
+
{ id:"F-ROOM-BACK", name:"Room back-to-all-goals affordance", surface:"room", sel:"#task-list .room-back", status:"shipped", tier:"v1" },
|
|
313
|
+
{ id:"F-ROOM-TURNS", name:"Turns in order (how agents worked the goal)", surface:"room", sel:"#task-list .turns-label", status:"partial", tier:"v1" },
|
|
314
|
+
{ id:"F-ROOM-AUDIT", name:"Room audit line (steps in local chain)", surface:"room", sel:"#task-list .audit-line", status:"partial", tier:"v1" },
|
|
315
|
+
/* partial by nature: renders ONLY when the daemon reports project memory
|
|
316
|
+
OFF for the opened room (initialized projects correctly show nothing). */
|
|
317
|
+
{ id:"F-ROOM-MEM-NUDGE", name:"Project-memory init nudge (turn on memory)", surface:"room", sel:"#task-list .mem-nudge", status:"partial", tier:"v1" },
|
|
301
318
|
{ id:"M-OVERLAY-LINEAR", name:"Overlay-renderer Linear", surface:"list", sel:".or-overlay", status:"shipped", tier:"v0.9" },
|
|
302
319
|
{ id:"M-PRIMITIVES-NOTE", name:"Primitives note", surface:"list", sel:".primitives-note .prims .p", status:"shipped", tier:"v0.8" },
|
|
303
320
|
{ id:"M-RESHAPE-AGENT-COMPOSE", name:"Agent-compose animation", surface:"list", sel:".agent-compose .line", status:"shipped", tier:"v0.8" },
|
|
@@ -651,6 +668,31 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
651
668
|
});
|
|
652
669
|
});
|
|
653
670
|
}, Promise.resolve());
|
|
671
|
+
}).then(function(){
|
|
672
|
+
// room-surface features · drive the REAL openRoom() (same pattern as
|
|
673
|
+
// showD above) on the first goal-head's data-goal-cwd, observe, close.
|
|
674
|
+
// No goal-head on the home = no real tasks; the room genuinely cannot
|
|
675
|
+
// be opened, so room features go MISSING/PARTIAL-GAP per their status —
|
|
676
|
+
// honest, and F-GOAL-GROUP fails alongside so the cause is visible.
|
|
677
|
+
var room = FEATURES.filter(function(f){ return f.surface==="room" && f.status!=="roadmap" && f.status!=="future"; });
|
|
678
|
+
if (!room.length) return;
|
|
679
|
+
var w0 = frameWin();
|
|
680
|
+
var gh = frameDoc().querySelector(".goal-head[data-goal-cwd]");
|
|
681
|
+
var check = function(){
|
|
682
|
+
var d = frameDoc();
|
|
683
|
+
room.forEach(function(f){
|
|
684
|
+
var present = d.querySelectorAll(f.sel).length > 0;
|
|
685
|
+
var r = { id:f.id, present:present, verdict:covVerdict(f, present) };
|
|
686
|
+
covResults.push(r); paintCov(r);
|
|
687
|
+
});
|
|
688
|
+
};
|
|
689
|
+
if (!gh){ check(); return; }
|
|
690
|
+
try { w0.openRoom(gh.getAttribute("data-goal-cwd")); } catch(e){}
|
|
691
|
+
return sleep(200).then(function(){
|
|
692
|
+
check();
|
|
693
|
+
try { if (w0.closeRoom) w0.closeRoom(); } catch(e){}
|
|
694
|
+
return sleep(120);
|
|
695
|
+
});
|
|
654
696
|
}).then(function(){
|
|
655
697
|
// roadmap / future: SKIP (correctly absent), no observation
|
|
656
698
|
FEATURES.filter(function(f){ return f.status==="roadmap" || f.status==="future"; }).forEach(function(f){
|
|
@@ -31,7 +31,7 @@ import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'fs';
|
|
|
31
31
|
import { homedir } from 'os';
|
|
32
32
|
import { join } from 'path';
|
|
33
33
|
import { createHash } from 'crypto';
|
|
34
|
-
const PROJECTS_DIR = join(homedir(), '.codehere', 'projects');
|
|
34
|
+
const PROJECTS_DIR = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'projects');
|
|
35
35
|
/** Match migration module's defaults. Same regex set kept in sync. */
|
|
36
36
|
const TEMP_DIR_PATTERNS = [/^\/tmp\//, /^\/var\/folders\//, /^\/private\/var\/folders\//, /^\/private\/tmp\//];
|
|
37
37
|
function isTempDir(cwd) {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
22
22
|
import { join } from 'path';
|
|
23
23
|
import { homedir } from 'os';
|
|
24
|
-
const DEFAULT_PROJECTS_DIR = join(homedir(), '.codehere', 'projects');
|
|
24
|
+
const DEFAULT_PROJECTS_DIR = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'projects');
|
|
25
25
|
/** List all known projects with summary stats. Cheap; no full task content. */
|
|
26
26
|
export function listProjects(options) {
|
|
27
27
|
const root = options?.projectsDir ?? DEFAULT_PROJECTS_DIR;
|
|
@@ -24,7 +24,7 @@ import { existsSync, readdirSync, readFileSync, appendFileSync, statSync } from
|
|
|
24
24
|
import { join } from 'path';
|
|
25
25
|
import { homedir } from 'os';
|
|
26
26
|
import { generateThreadTitle } from '../../application/use-cases/generate-thread-title.js';
|
|
27
|
-
const DEFAULT_PROJECTS_DIR = join(homedir(), '.codehere', 'projects');
|
|
27
|
+
const DEFAULT_PROJECTS_DIR = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'projects');
|
|
28
28
|
function scanThread(slug, file) {
|
|
29
29
|
let title = null;
|
|
30
30
|
let firstPrompt = null;
|