codehere 1.0.1 → 1.0.2
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/README.md +36 -192
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +110 -7
- package/dist/index.js +38 -1212
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
- package/dist/presentation/cli/commands/check-command.js +178 -23
- package/dist/presentation/cli/commands/help-command.js +21 -285
- package/dist/ui.js +5 -236
- package/dist/utils/version.js +0 -6
- package/package.json +9 -38
- package/dist/application/services/verdict-service.js +0 -154
- package/dist/application/use-cases/assess-cross-vendor.js +0 -126
- package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/export-aat.js +0 -107
- package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
- package/dist/application/use-cases/generate-final-report.js +0 -259
- package/dist/application/use-cases/generate-narration.js +0 -130
- package/dist/application/use-cases/generate-thread-title.js +0 -181
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/plan-auto-review.js +0 -57
- package/dist/application/use-cases/reconcile-audit.js +0 -100
- package/dist/application/use-cases/search-sessions.js +0 -70
- package/dist/application/use-cases/should-auto-review.js +0 -71
- package/dist/application/use-cases/thread-export.js +0 -245
- package/dist/application/use-cases/verify-claim.js +0 -456
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/audit.js +0 -11
- package/dist/domain/services/codebase-detector.js +0 -85
- package/dist/domain/task.js +0 -91
- package/dist/error-handler.js +0 -139
- package/dist/infrastructure/completion/bash-completion.js +0 -29
- package/dist/infrastructure/completion/fish-completion.js +0 -17
- package/dist/infrastructure/completion/zsh-completion.js +0 -25
- package/dist/infrastructure/config/config-manager.js +0 -174
- package/dist/infrastructure/config/product-context.js +0 -49
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -198
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/github-api.js +0 -110
- package/dist/infrastructure/context/github-device-flow.js +0 -65
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -122
- package/dist/infrastructure/context/registry-bootstrap.js +0 -95
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -114
- package/dist/infrastructure/cost/cost-router.js +0 -97
- package/dist/infrastructure/cost/spend-aggregator.js +0 -222
- package/dist/infrastructure/cost/team-ledger.js +0 -355
- package/dist/infrastructure/discovery/discovery-log.js +0 -85
- package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
- package/dist/infrastructure/hooks/hook-mode.js +0 -87
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
- package/dist/infrastructure/orchestration/context-handoff.js +0 -162
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -67
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
- package/dist/infrastructure/orchestration/graphify-context.js +0 -209
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1590
- package/dist/infrastructure/orchestration/session-persistence.js +0 -394
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
- package/dist/infrastructure/protocols/mcp-tools.js +0 -401
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
- package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
- package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
- package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
- package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
- package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
- package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
- package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
- package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
- package/dist/infrastructure/protocols/ui/styles.js +0 -2147
- package/dist/infrastructure/protocols/ui/tokens.js +0 -96
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
- package/dist/infrastructure/protocols/verify-page.js +0 -1677
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/fix-code.js +0 -144
- package/dist/infrastructure/security/fix-secret.js +0 -89
- package/dist/infrastructure/security/permissions-config.js +0 -235
- package/dist/infrastructure/security/scan-types.js +0 -10
- package/dist/infrastructure/security/ship-scan.js +0 -195
- package/dist/infrastructure/security/trust-scorer.js +0 -724
- package/dist/infrastructure/security/vibe-checks.js +0 -99
- package/dist/infrastructure/storage/atomic-write.js +0 -86
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
- package/dist/infrastructure/storage/project-thread-reader.js +0 -409
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -201
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -217
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -590
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -105
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -403
- package/dist/presentation/cli/commands/scan-command.js +0 -52
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
- package/dist/presentation/cli/commands/search-command.js +0 -51
- package/dist/presentation/cli/commands/setup-command.js +0 -330
- package/dist/presentation/cli/commands/status-command.js +0 -614
- package/dist/presentation/cli/commands/team-command.js +0 -256
- package/dist/presentation/cli/commands/thread-command.js +0 -370
- package/dist/presentation/cli/commands/trust-command.js +0 -328
- package/dist/presentation/cli/commands/verify-command.js +0 -298
- package/dist/presentation/cli/error-display.js +0 -328
- package/dist/presentation/cli/repl.js +0 -805
- package/dist/presentation/cli/task-deliverable.js +0 -153
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -147
|
@@ -1,2998 +0,0 @@
|
|
|
1
|
-
import { PHASE_CLASSIFIER_SRC } from '../thread-phase.js';
|
|
2
|
-
import { RENDER_HELPERS } from './app-script-render.js';
|
|
3
|
-
import { CONVERSATION_TURN } from './app-script-conversation.js';
|
|
4
|
-
import { SIDEBAR } from './app-script-sidebar.js';
|
|
5
|
-
import { WPASS_MODAL } from './app-script-wpass.js';
|
|
6
|
-
import { OVERLAY_RENDERER } from './app-script-overlay.js';
|
|
7
|
-
import { CONTEXT_SOURCES } from './app-script-context-sources.js';
|
|
8
|
-
import { RECOVERY_ACTIONS } from './app-script-recovery.js';
|
|
9
|
-
import { SETTINGS_DRAWER } from './app-script-drawer.js';
|
|
10
|
-
import { AUDIT_CHAIN_VERIFY } from './app-script-audit-verify.js';
|
|
11
|
-
import { COMPOSER_COST } from './app-script-composer-cost.js';
|
|
12
|
-
import { SHARE_MODAL } from './app-script-share.js';
|
|
13
|
-
import { AUDIT_FOOTER } from './app-script-audit-footer.js';
|
|
14
|
-
import { FAILOVER_BANNER } from './app-script-failover.js';
|
|
15
|
-
import { GITHUB_INTEGRATION } from './app-script-github.js';
|
|
16
|
-
import { ONBOARDING } from './app-script-onboarding.js';
|
|
17
|
-
import { TOUR } from './app-script-tour.js';
|
|
18
|
-
import { OBSERVER_RAIL } from './app-script-observer.js';
|
|
19
|
-
import { ARTIFACTS } from './app-script-artifacts.js';
|
|
20
|
-
export const APP_SCRIPT = `
|
|
21
|
-
const _cached = {};
|
|
22
|
-
// Expose the live cache so /verify coverage can drive the REAL state-dependent
|
|
23
|
-
// updaters (upBud/upIdx/upWk) over a known state and observe the actual banner
|
|
24
|
-
// render — a const is not auto-attached to window, so the harness can't reach it
|
|
25
|
-
// otherwise. Read/written only by the coverage drives, which restore after.
|
|
26
|
-
try{ window._cached = _cached; }catch(e){}
|
|
27
|
-
let _filter = 'all', _detail = null, _room = null, _offline = false, _autoOpened = false;
|
|
28
|
-
// Client-side memo of answered agent-questions (sessionId|toolUseId -> labels)
|
|
29
|
-
// so the question card stays "answered" across the post-answer re-render (the
|
|
30
|
-
// step itself isn't marked server-side in v1).
|
|
31
|
-
var _answeredAsks = {};
|
|
32
|
-
var _convOpenByTask = {};
|
|
33
|
-
// The artifacts drawer is rendered INLINE in the detail HTML, so a poll-driven
|
|
34
|
-
// render() rebuilds it closed — clicking "N artifacts" opened it for under one
|
|
35
|
-
// poll interval, then it snapped shut (Ega: "i cant click", 2026-07-02). Persist
|
|
36
|
-
// the open state across re-render, same pattern as _convOpenByTask; reset when a
|
|
37
|
-
// different task detail opens (showD).
|
|
38
|
-
var _artOpen = false;
|
|
39
|
-
// Same render-wipe class for individual expanded step rows: expand a step to
|
|
40
|
-
// read its output, the poll re-renders and collapses it (3rd instance found by
|
|
41
|
-
// the systemic sweep). Keyed by task + step signature (startedAt+label), stable
|
|
42
|
-
// across renders. Toggling updates it via __rememberStepOpen.
|
|
43
|
-
var _stepOpen = {};
|
|
44
|
-
// DESIGN.md §4.3 room→receipts: the step wall (activityBlock) collapses into a
|
|
45
|
-
// one-line receipt inside the open conversation — evidence one click away, never
|
|
46
|
-
// always-on. Open state persisted per session (render-wipe-safe), reset on task
|
|
47
|
-
// switch. Toggling updates it via __rememberReceiptOpen.
|
|
48
|
-
var _receiptOpen = {};
|
|
49
|
-
var _glMoreOpen = {}; // glance turn's full-report <details> · same render-wipe guard as _receiptOpen
|
|
50
|
-
// Cross-vendor verdict cache, per session id. undefined = not fetched (renderD
|
|
51
|
-
// kicks the fetch); null = fetched, not a cross-vendor review; object = the
|
|
52
|
-
// receipt. Cached so the panel lives in renderD's output and survives
|
|
53
|
-
// writeDetail's morph-diff (a DOM-injected node would be culled).
|
|
54
|
-
var _crossVendorByTask = {};
|
|
55
|
-
// Project-memory state for the open room · null = unknown (fetch in flight),
|
|
56
|
-
// false = agents forget this project (show the nudge), true = memory already
|
|
57
|
-
// on (stay quiet), 'just-on' = turned on from the nudge this visit (confirm).
|
|
58
|
-
let _roomMemory = null;
|
|
59
|
-
// The FULL project thread (all clones, all-time) fetched from /api/projects/:slug when
|
|
60
|
-
// a room opens — so the conversation shows every turn, not just the recent /api/tasks subset.
|
|
61
|
-
let _roomThread = null;
|
|
62
|
-
// S19 glyph language: never emoji-as-icon — blocked/queued/paused carry their
|
|
63
|
-
// word on the surface; the glyph only accents it (see the token-block contract).
|
|
64
|
-
const ICONS = { queued:'\\u25CB', running:'\\u25CF', paused:'\\u00B7', blocked:'\\u2298', done:'\\u2713', failed:'\\u2715', crashed:'!', killed:'\\u2298' };
|
|
65
|
-
const STAT_ORDER = ['running','queued','paused','blocked','done','failed','crashed','killed'];
|
|
66
|
-
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) };
|
|
67
|
-
|
|
68
|
-
function ms(t) { return t?'\\u2713':'\\u2715'; }
|
|
69
|
-
function dur(ms) { if(!ms&&ms!==0)return''; const s=Math.floor(ms/1000); if(s<60)return s+'s'; return Math.floor(s/60)+'m '+(s%60)+'s'; }
|
|
70
|
-
// S4 · a finished-or-dead task with no recorded end must not wear a live
|
|
71
|
-
// wall-clock (a crashed orphan with durationMs=0 AND completedAt=0 read
|
|
72
|
-
// "23,860m" — now−started). Live states still tick; unknown says so.
|
|
73
|
-
function taskDur(t){
|
|
74
|
-
if(t.durationMs) return dur(t.durationMs);
|
|
75
|
-
if(t.completedAt && t.startedAt) return dur(t.completedAt - t.startedAt);
|
|
76
|
-
if(['running','queued','paused','blocked'].includes(t.status) && t.startedAt) return dur(Date.now()-t.startedAt);
|
|
77
|
-
return '\u2014';
|
|
78
|
-
}
|
|
79
|
-
// Strip internal context-injection prefixes from a prompt before showing it to
|
|
80
|
-
// the user. Real spawns prepend "[graphify context: ...]\\n\\n" (and failover
|
|
81
|
-
// adds "[Previous session context — ...]") — functional for the agent, noise
|
|
82
|
-
// for the human. Found via real-execution dogfood #45: the prefix was becoming
|
|
83
|
-
// the thread title. Display-only; the actual spawn prompt keeps the prefix.
|
|
84
|
-
function displayPrompt(p) {
|
|
85
|
-
return String(p == null ? '' : p)
|
|
86
|
-
.replace(/^\\[graphify context:[\\s\\S]*?\\]\\s*\\n+/, '')
|
|
87
|
-
.replace(/^\\[Previous session context[\\s\\S]*?\\]\\s*\\n+/, '')
|
|
88
|
-
.replace(/^\\s+/, '');
|
|
89
|
-
}
|
|
90
|
-
function title(p) {
|
|
91
|
-
// Build a headline from whole CLAUSES (split on · — and sentence-enders),
|
|
92
|
-
// accumulating up to ~72 chars — so we never chop mid-phrase like the old
|
|
93
|
-
// "...200 rps · Lua INCR in". Trailing punctuation stripped per clause.
|
|
94
|
-
var s=(displayPrompt(p)||'Untitled').split('\\n')[0].trim();
|
|
95
|
-
var parts=s.split(/\\s*[\\u00B7\\u2014]\\s*|(?<=[.!?])\\s+/)
|
|
96
|
-
.map(function(x){return x.replace(/[.!?,;:\\s]+$/,'').trim();}).filter(Boolean);
|
|
97
|
-
var out='';
|
|
98
|
-
for(var i=0;i<parts.length;i++){
|
|
99
|
-
var nx=out?out+' \\u00B7 '+parts[i]:parts[i];
|
|
100
|
-
if(out && nx.length>72) break;
|
|
101
|
-
out=nx; if(out.length>=72) break;
|
|
102
|
-
}
|
|
103
|
-
out=(out||s).trim();
|
|
104
|
-
if(out.length>80) out=out.slice(0,80).replace(/\\s+\\S*$/,'')+'\\u2026';
|
|
105
|
-
return out||'Untitled';
|
|
106
|
-
}
|
|
107
|
-
// HTML-escaped title — use at any site that inserts a title into innerHTML, so a
|
|
108
|
-
// prompt like "<img onerror=...>" can never execute (module-level so it's in
|
|
109
|
-
// scope everywhere; the function-local escapers don't reach every render site).
|
|
110
|
-
function titleH(p){ return String(title(p)).replace(/[&<>"]/g,function(ch){return ({'&':'&','<':'<','>':'>','"':'"'})[ch];}); }
|
|
111
|
-
|
|
112
|
-
// Per-agent identity badge — primitive #1 from AGENTIC_UX_FRAMEWORK.
|
|
113
|
-
// Returns a squircle with the agent's initials in its identity color.
|
|
114
|
-
// Recognized: claude-code, codex, aider, gemini, opencode, conductor, auto.
|
|
115
|
-
function agentKey(id) {
|
|
116
|
-
const s = String(id||'').toLowerCase();
|
|
117
|
-
if (s === 'claude-code' || s.startsWith('claude')) return 'claude';
|
|
118
|
-
if (s === 'codex' || s.startsWith('codex')) return 'codex';
|
|
119
|
-
if (s === 'aider' || s.startsWith('aider')) return 'aider';
|
|
120
|
-
if (s === 'gemini' || s.startsWith('gemini')) return 'gemini';
|
|
121
|
-
if (s === 'opencode' || s.startsWith('opencode')) return 'opencode';
|
|
122
|
-
if (s === 'cursor' || s.startsWith('cursor')) return 'cursor';
|
|
123
|
-
if (s === 'conductor') return 'conductor';
|
|
124
|
-
if (s === 'auto') return 'auto';
|
|
125
|
-
return 'auto';
|
|
126
|
-
}
|
|
127
|
-
function agentInitials(id) {
|
|
128
|
-
const k = agentKey(id);
|
|
129
|
-
if (k === 'claude') return 'CC';
|
|
130
|
-
if (k === 'codex') return 'CD';
|
|
131
|
-
if (k === 'aider') return 'AI';
|
|
132
|
-
if (k === 'gemini') return 'GM';
|
|
133
|
-
if (k === 'opencode') return 'OC';
|
|
134
|
-
if (k === 'cursor') return 'CU';
|
|
135
|
-
if (k === 'conductor') return 'CH';
|
|
136
|
-
return '?';
|
|
137
|
-
}
|
|
138
|
-
function ab(id, size) {
|
|
139
|
-
const k = agentKey(id);
|
|
140
|
-
const sz = size === 'sm' ? ' sm' : (size === 'lg' ? ' lg' : '');
|
|
141
|
-
return '<span class="ab ' + k + sz + '">' + agentInitials(id) + '</span>';
|
|
142
|
-
}
|
|
143
|
-
// Phase 3 · who's-involved. Overlapping avatars for the agents (and, when
|
|
144
|
-
// collaboration lands, people) involved in a room/task. Dedup by agent key,
|
|
145
|
-
// show up to 4, "+N" overflow, names in the tooltip. String-only (no regex:
|
|
146
|
-
// this runs inside a template literal where backslashes get mangled).
|
|
147
|
-
function partStack(agents) {
|
|
148
|
-
var list = (agents || []).filter(Boolean);
|
|
149
|
-
if (!list.length) return '';
|
|
150
|
-
var seen = {}, uniq = [];
|
|
151
|
-
for (var i = 0; i < list.length; i++) {
|
|
152
|
-
if (agentInitials(list[i]) === '?') continue; // skip unrecognized agents — no broken "?" chip
|
|
153
|
-
var k = agentKey(list[i]); if (!seen[k]) { seen[k] = 1; uniq.push(list[i]); }
|
|
154
|
-
}
|
|
155
|
-
if (!uniq.length) return '';
|
|
156
|
-
var show = uniq.slice(0, 4), more = uniq.length - show.length;
|
|
157
|
-
var names = show.map(function (a) { return agentLabel(a); }).join(', ') + (more > 0 ? ' +' + more + ' more' : '');
|
|
158
|
-
var h = '<span class="pstack" role="img" aria-label="involved: ' + names + '" title="' + names + '">';
|
|
159
|
-
// Distinct 2-char vendor codes (CC/CD/CH…) — single letters collapsed Claude,
|
|
160
|
-
// Codex and Conductor all to "C". Vendor-colored + lightly spaced (not crowded).
|
|
161
|
-
for (var j = 0; j < show.length; j++) { var _k = agentKey(show[j]); h += '<span class="ab stk ' + _k + '" title="' + agentLabel(show[j]) + '">' + agentInitials(show[j]) + '</span>'; }
|
|
162
|
-
if (more > 0) h += '<span class="ab stk more">+' + more + '</span>';
|
|
163
|
-
return h + '</span>';
|
|
164
|
-
}
|
|
165
|
-
function agentLabel(id) {
|
|
166
|
-
const k = agentKey(id);
|
|
167
|
-
if (k === 'claude') return 'Claude Code';
|
|
168
|
-
if (k === 'codex') return 'Codex';
|
|
169
|
-
if (k === 'aider') return 'Aider';
|
|
170
|
-
if (k === 'gemini') return 'Gemini';
|
|
171
|
-
if (k === 'opencode') return 'OpenCode';
|
|
172
|
-
if (k === 'cursor') return 'Cursor';
|
|
173
|
-
if (k === 'conductor') return 'Conductor';
|
|
174
|
-
if (k === 'auto') return 'Auto';
|
|
175
|
-
return id || '?';
|
|
176
|
-
}
|
|
177
|
-
// Cost signal pill — primitive #2.
|
|
178
|
-
${RENDER_HELPERS}
|
|
179
|
-
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))]; }
|
|
180
|
-
function cardActions(s) {
|
|
181
|
-
// Only 'kill' is wired (POST /api/task/:id/kill). Pause/Resume/Restart had NO
|
|
182
|
-
// backend endpoint — they rendered as live buttons that silently no-op'd (R11
|
|
183
|
-
// honest-state lie, 2026-06-03). Removed until real process control lands; Kill
|
|
184
|
-
// and Cancel both map to the wired kill, so every rendered control here works.
|
|
185
|
-
const m={
|
|
186
|
-
running:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Kill</button>',
|
|
187
|
-
paused:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Kill</button>',
|
|
188
|
-
queued:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Cancel</button>',
|
|
189
|
-
blocked:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Cancel</button>',
|
|
190
|
-
};
|
|
191
|
-
return m[mapped(s)]||'';
|
|
192
|
-
}
|
|
193
|
-
function statIcon(s) { return '<span class="status-icon '+s+'"></span>'; }
|
|
194
|
-
function verB(v) { return v&&v!=='unverified'?'<span class="verify-badge '+v+'">'+v+'</span>':''; }
|
|
195
|
-
function tagHtml(t) { return t.map(x=>'<span style="font-size:10.5px;color:var(--fg-40);font-family:var(--mono)">'+x+'</span>').join(''); }
|
|
196
|
-
|
|
197
|
-
function st(s) {
|
|
198
|
-
if(s==='completed')return'done'; if(s==='error'||s==='killed')return'failed'; if(s==='paused')return'paused'; return s;
|
|
199
|
-
}
|
|
200
|
-
function aStat(s) { return s==='blocked'?'paused':s==='killed'?'failed':s==='crashed'?'failed':st(s); }
|
|
201
|
-
function mapped(s) { return s==='queued'?'running':s==='blocked'?'paused':aStat(s); }
|
|
202
|
-
|
|
203
|
-
function findS(id) {
|
|
204
|
-
const s=allS(_cached.tasks||{});
|
|
205
|
-
let hit=s.find(x=>x.sessionId===id); if(hit) return hit;
|
|
206
|
-
if(_roomThread&&_roomThread.tasks){ hit=_roomThread.tasks.find(x=>x.sessionId===id); if(hit) return hit; }
|
|
207
|
-
for(var k in _sbThreads){ const t=(_sbThreads[k]||[]).find(x=>x.sessionId===id); if(t) return t; }
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Crafted agent picker (composer). Replaces the native <select>; the chosen
|
|
212
|
-
// value lives in the hidden #agent-pick (unchanged read contract). Rows show the
|
|
213
|
-
// vendor badge + REAL readiness (agentS over _cached.readiness) — honest, never faked.
|
|
214
|
-
var AGENTS=['claude-code','codex','aider','gemini','opencode'];
|
|
215
|
-
var _AP_SPARK='<svg class="ap-spark" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l1.7 6.3L20 10l-6.3 1.7L12 18l-1.7-6.3L4 10l6.3-1.7z"/></svg>';
|
|
216
|
-
var _apInit=false;
|
|
217
|
-
function agentReadyState(id){ var ags=(_cached.readiness&&_cached.readiness.agents)||[]; var a=ags.find(function(x){return x&&(x.id===id||x.agent===id);}); return a?agentS(a.state):null; }
|
|
218
|
-
function _apTrigHtml(val){
|
|
219
|
-
if(!val||val==='auto'){ return _AP_SPARK+'<span class="ap-name">Auto</span>'; }
|
|
220
|
-
return '<span class="ab stk '+agentKey(val)+'">'+(String(agentInitials(val)).charAt(0)||'?')+'</span><span class="ap-name">'+String(agentLabel(val)).replace(/[<>&]/g,'')+'</span>';
|
|
221
|
-
}
|
|
222
|
-
function setAgentPick(val){
|
|
223
|
-
var inp=document.getElementById('agent-pick');
|
|
224
|
-
// Set the value AND fire 'change' so the existing updateCostEst listener (which
|
|
225
|
-
// the old <select> drove natively) recomputes the cost estimate on agent change.
|
|
226
|
-
if(inp){ inp.value=val; try{ inp.dispatchEvent(new Event('change',{bubbles:true})); }catch(e){} }
|
|
227
|
-
var cur=document.getElementById('agentpick-cur'); if(cur) cur.innerHTML=_apTrigHtml(val);
|
|
228
|
-
closeAgentMenu();
|
|
229
|
-
}
|
|
230
|
-
function renderAgentMenu(){
|
|
231
|
-
var m=document.getElementById('agentpick-menu'); if(!m) return;
|
|
232
|
-
var cur=(document.getElementById('agent-pick')||{}).value||'auto';
|
|
233
|
-
var html='<div class="ap-item'+(cur==='auto'?' sel':'')+'" role="option" tabindex="0" onclick="setAgentPick(\\'auto\\')" onkeydown="if(event.key===\\'Enter\\'){setAgentPick(\\'auto\\')}">'+_AP_SPARK+'<span class="ap-il"><span class="ap-in">Auto</span><span class="ap-isub">best agent for the task</span></span>'+(cur==='auto'?'<span class="ap-chk">\\u2713</span>':'')+'</div>';
|
|
234
|
-
AGENTS.forEach(function(id){
|
|
235
|
-
var st=agentReadyState(id);
|
|
236
|
-
var stw=st?'<span class="ap-st">'+st+'</span>':'';
|
|
237
|
-
var dot=st?'<span class="ap-dot '+st+'"></span>':'';
|
|
238
|
-
html+='<div class="ap-item'+(cur===id?' sel':'')+'" role="option" tabindex="0" onclick="setAgentPick(\\''+id+'\\')" onkeydown="if(event.key===\\'Enter\\'){setAgentPick(\\''+id+'\\')}"><span class="ab stk '+agentKey(id)+'">'+(String(agentInitials(id)).charAt(0)||'?')+'</span><span class="ap-il"><span class="ap-in">'+agentLabel(id)+'</span></span>'+stw+dot+(cur===id?'<span class="ap-chk">\\u2713</span>':'')+'</div>';
|
|
239
|
-
});
|
|
240
|
-
m.innerHTML=html;
|
|
241
|
-
}
|
|
242
|
-
function toggleAgentMenu(e){ if(e){e.stopPropagation();} var m=document.getElementById('agentpick-menu'),b=document.getElementById('agentpick-btn'); if(!m) return; if(m.hidden){ renderAgentMenu(); m.hidden=false; if(b)b.setAttribute('aria-expanded','true'); } else { closeAgentMenu(); } }
|
|
243
|
-
function closeAgentMenu(){ var m=document.getElementById('agentpick-menu'),b=document.getElementById('agentpick-btn'); if(m&&!m.hidden){m.hidden=true; if(b)b.setAttribute('aria-expanded','false');} }
|
|
244
|
-
function initAgentPick(){
|
|
245
|
-
if(_apInit) return; var cur=document.getElementById('agentpick-cur'); if(!cur) return; _apInit=true;
|
|
246
|
-
setAgentPick((document.getElementById('agent-pick')||{}).value||'auto');
|
|
247
|
-
document.addEventListener('click',function(e){ if(!(e.target&&e.target.closest&&e.target.closest('.agentpick-wrap'))) closeAgentMenu(); });
|
|
248
|
-
document.addEventListener('keydown',function(e){ if(e.key==='Escape') closeAgentMenu(); });
|
|
249
|
-
}
|
|
250
|
-
function render() {
|
|
251
|
-
if(_detail){ const t=findS(_detail); if(t){ const _sy=window.scrollY; renderD(t); if(_sy>0)window.scrollTo(0,_sy); } else{ _detail=null; if(_room)renderRoom(); else renderL(); } }
|
|
252
|
-
else if(_room) renderRoom();
|
|
253
|
-
else renderL();
|
|
254
|
-
upBud(); upWk(); upIdx(); upHero(); renderDecisions();
|
|
255
|
-
renderSidebar(); initAgentPick(); maybeStartOnboarding(); if(typeof initGhImport==='function') initGhImport();
|
|
256
|
-
}
|
|
257
|
-
// Cockpit shell · the persistent Projects sidebar, as a REAL tree.
|
|
258
|
-
// The caret toggles expand/collapse (animated via CSS, state in _sbExpanded,
|
|
259
|
-
// lazy-fetches a project's tasks on first expand); the name opens the project in
|
|
260
|
-
// the main pane. The active project auto-expands. Leaves open the run in the pane.
|
|
261
|
-
${SIDEBAR}
|
|
262
|
-
// The goal room — one goal opened to its tasks as a chronological thread of
|
|
263
|
-
// agent turns. Renders into #task-list (same surface as the goals list).
|
|
264
|
-
function openRoom(enc){
|
|
265
|
-
_room=decodeURIComponent(enc||''); _detail=null;
|
|
266
|
-
try{ var _rh=_roomHash(); if(_rh && location.hash!==_rh) location.hash=_rh; }catch(e){} // room state → URL (not the reverse)
|
|
267
|
-
// Entering a room closes the Spend page (loop-6 verifier catch: without
|
|
268
|
-
// this the FULL spend hero stayed rendered below the room feed under a
|
|
269
|
-
// room hash — screen/hash drift, the exact thing the spine exists to kill).
|
|
270
|
-
var _sv0=document.getElementById('spend-view'); if(_sv0) _sv0.style.display='none';
|
|
271
|
-
document.getElementById('task-list-view').style.display='';
|
|
272
|
-
// Project-memory status for the nudge — fetched once per entry from the
|
|
273
|
-
// daemon (the SAME readiness the injector acts on, not a guess).
|
|
274
|
-
_roomMemory=null;
|
|
275
|
-
(function(forCwd){
|
|
276
|
-
fetch('/api/memory/status?cwd='+encodeURIComponent(forCwd)).then(x=>x.json()).then(j=>{
|
|
277
|
-
if(_room===forCwd){ _roomMemory=!!j.ready; render(); }
|
|
278
|
-
}).catch(()=>{});
|
|
279
|
-
})(_room);
|
|
280
|
-
// Pull the FULL project thread (all clones, all-time) so the room renders the complete
|
|
281
|
-
// conversation, not the recent /api/tasks subset (e.g. 405 turns, not 15).
|
|
282
|
-
_roomThread=null;
|
|
283
|
-
(function(forCwd){
|
|
284
|
-
var proj=(_cached.projects||[]).find(function(x){return projPaths(x).indexOf(normC(forCwd))>=0;});
|
|
285
|
-
var slug=proj&&proj.slug;
|
|
286
|
-
if(!slug) return;
|
|
287
|
-
fetch('/api/projects/'+encodeURIComponent(slug)).then(x=>x.json()).then(j=>{
|
|
288
|
-
if(_room===forCwd && j && j.thread){ _roomThread={cwd:forCwd, tasks:(j.thread.tasks||[])}; render(); }
|
|
289
|
-
}).catch(function(){});
|
|
290
|
-
})(_room);
|
|
291
|
-
document.getElementById('task-list-view').style.display='';
|
|
292
|
-
document.getElementById('task-detail-view').style.display='none';
|
|
293
|
-
document.body.classList.remove('detail-open');
|
|
294
|
-
// Hand-off affordance: inside a room the composer targets THIS goal (cwd is
|
|
295
|
-
// pinned in submit) and the picker defaults to whoever worked it last —
|
|
296
|
-
// continuing is zero clicks, handing to a different agent is one visible
|
|
297
|
-
// click. Set once on entry (not in renderRoom, which re-runs every poll and
|
|
298
|
-
// would stomp the user's choice).
|
|
299
|
-
const ta=document.getElementById('main-ta');
|
|
300
|
-
if(ta) ta.placeholder='Add the next task in this goal\\u2026 any agent picks up where the last one left off';
|
|
301
|
-
const ap=document.getElementById('agent-pick');
|
|
302
|
-
if(ap){
|
|
303
|
-
const turns=allS(_cached.tasks||{}).filter(x=>!/^demo-/.test(x.sessionId||'')&&!x.mockRun&&normC(x.cwd)===normC(_room)&&x.agent).sort((a,b)=>(a.startedAt||0)-(b.startedAt||0));
|
|
304
|
-
const raw=turns.length?String(turns[turns.length-1].agent):'';
|
|
305
|
-
ap.value=['claude-code','codex','aider','gemini','opencode'].indexOf(raw)>=0?raw:'auto';
|
|
306
|
-
}
|
|
307
|
-
render();
|
|
308
|
-
}
|
|
309
|
-
function closeRoom(){
|
|
310
|
-
_room=null; _roomMemory=null; _roomThread=null;
|
|
311
|
-
try{ if((location.hash||'').indexOf('#/room/')===0) history.replaceState(null,'',location.pathname+location.search); }catch(e){}
|
|
312
|
-
const ta=document.getElementById('main-ta');
|
|
313
|
-
if(ta) ta.placeholder='Describe a task or ask a question...';
|
|
314
|
-
const ap=document.getElementById('agent-pick'); if(ap){ if(typeof setAgentPick==='function') setAgentPick('auto'); else ap.value='auto'; }
|
|
315
|
-
render();
|
|
316
|
-
}
|
|
317
|
-
async function initRoomMemory(btn){
|
|
318
|
-
if(!_room) return;
|
|
319
|
-
if(btn){ btn.disabled=true; btn.textContent='turning on\\u2026'; }
|
|
320
|
-
try{
|
|
321
|
-
const r=await fetch('/api/memory/init',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cwd:_room})});
|
|
322
|
-
if(r.ok){ _roomMemory='just-on'; render(); }
|
|
323
|
-
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';} }
|
|
324
|
-
}catch(e){ al('Network error: '+e.message); if(btn){btn.disabled=false;btn.textContent='turn on project memory';} }
|
|
325
|
-
}
|
|
326
|
-
// Group turns into threads (continued conversations) within a room. Union-find
|
|
327
|
-
// over the persisted/live lineage edge (continuedFrom — set on reply turns +
|
|
328
|
-
// in-session hand-offs). A turn with no edge is its own single-turn thread.
|
|
329
|
-
// Returns an array of turn-arrays, each sorted oldest-first, groups ordered by
|
|
330
|
-
// their earliest turn. Sparse today (most threads = 1 turn); richer once
|
|
331
|
-
// cross-agent hand-off (Phase 3) persists more edges.
|
|
332
|
-
function threadGroups(items){
|
|
333
|
-
const byId={}; items.forEach(t=>{ byId[t.sessionId]=t; });
|
|
334
|
-
const parent={};
|
|
335
|
-
const find=x=>{ while(parent[x]!==undefined && parent[x]!==x){ parent[x]=parent[parent[x]]!==undefined?parent[parent[x]]:parent[x]; x=parent[x]; } return x; };
|
|
336
|
-
items.forEach(t=>{ if(parent[t.sessionId]===undefined) parent[t.sessionId]=t.sessionId; });
|
|
337
|
-
items.forEach(t=>{ const f=t.continuedFrom||t.parentSessionId; if(f&&byId[f]){ const ra=find(t.sessionId), rb=find(f); if(ra!==rb) parent[ra]=rb; } });
|
|
338
|
-
// Re-runs of the same task are one thread even without a lineage edge — the
|
|
339
|
-
// same rule the home card uses (title(prompt) equality, see the runs strip).
|
|
340
|
-
// Scoped to the same cwd so cross-project coincidences never merge.
|
|
341
|
-
const byTitle={};
|
|
342
|
-
items.forEach(t=>{
|
|
343
|
-
const k=title(t.prompt);
|
|
344
|
-
if(!k||k==='Untitled') return;
|
|
345
|
-
const key=String(t.cwd||'')+'\\u0000'+k;
|
|
346
|
-
if(byTitle[key]){ const ra=find(t.sessionId), rb=find(byTitle[key]); if(ra!==rb) parent[ra]=rb; }
|
|
347
|
-
else byTitle[key]=t.sessionId;
|
|
348
|
-
});
|
|
349
|
-
const groups={};
|
|
350
|
-
items.forEach(t=>{ const r=find(t.sessionId); (groups[r]=groups[r]||[]).push(t); });
|
|
351
|
-
const arr=Object.keys(groups).map(k=>groups[k].slice().sort((a,b)=>(a.startedAt||0)-(b.startedAt||0)));
|
|
352
|
-
arr.sort((a,b)=>(a[0].startedAt||0)-(b[0].startedAt||0));
|
|
353
|
-
return arr;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Day Log (Direction A, Ega-picked 2026-07-06) — one line per task: rail hue =
|
|
357
|
-
// agent, title, glance fragment, receipt right. Same-title re-runs collapse to
|
|
358
|
-
// one line with a run count + total cost. Conversations/hand-offs (the wedge)
|
|
359
|
-
// keep the full turn treatment.
|
|
360
|
-
function dayLogGlance(t){
|
|
361
|
-
if(t.finalReport && typeof t.finalReport.summary==='string' && t.finalReport.summary.trim()){
|
|
362
|
-
var s=String(t.finalReport.summary).replace(/[#*>_~\\[\\]\\u0060]/g,'').replace(/\\s+/g,' ').trim();
|
|
363
|
-
s=s.replace(/^Aider v[\\d.]+\\s*/i,'');
|
|
364
|
-
var m=s.match(/^[\\s\\S]*?(?:[!?]|\\.(?!\\.))(?:\\s|$)/);
|
|
365
|
-
s=(m?m[0]:s).trim();
|
|
366
|
-
if(s.length>110) s=s.slice(0,110)+'\\u2026';
|
|
367
|
-
return s;
|
|
368
|
-
}
|
|
369
|
-
if(['running','queued','paused','blocked'].indexOf(t.status)>=0 && (t.steps||[]).length){
|
|
370
|
-
return 'working\\u2026 '+compactLabel((t.steps[t.steps.length-1]||{}).label||'');
|
|
371
|
-
}
|
|
372
|
-
var n=(t.steps||[]).length;
|
|
373
|
-
return n? n+' step'+(n===1?'':'s'):'';
|
|
374
|
-
}
|
|
375
|
-
// Day-divider label · the Day Log's calendar seams: 'today' / 'yesterday' /
|
|
376
|
-
// '30 Jun' (+ the year when it isn't this year). Machine fact -> mono (it
|
|
377
|
-
// reuses the .turns-label treatment; the static "Tasks in this project"
|
|
378
|
-
// header died for it — the first divider anchors the list).
|
|
379
|
-
function dayLabel(ts){
|
|
380
|
-
var d=new Date(ts), now=new Date();
|
|
381
|
-
var key=function(x){ return x.getFullYear()+'-'+x.getMonth()+'-'+x.getDate(); };
|
|
382
|
-
if(key(d)===key(now)) return 'today';
|
|
383
|
-
if(key(d)===key(new Date(now.getTime()-86400000))) return 'yesterday';
|
|
384
|
-
return d.getDate()+' '+['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][d.getMonth()]
|
|
385
|
-
+(d.getFullYear()!==now.getFullYear()?' '+d.getFullYear():'');
|
|
386
|
-
}
|
|
387
|
-
function dayLogRow(t, runCount, totalCents){
|
|
388
|
-
var esc=function(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];});};
|
|
389
|
-
var _verified=!t.mockRun && !(t.artifacts&&t.artifacts.noGitTracking) && !!(t.verdict&&(t.verdict.grade?t.verdict.grade.green:(t.verdict.claim&&t.verdict.claim.status==='verified')));
|
|
390
|
-
var live=['running','queued','paused','blocked'].indexOf(t.status)>=0;
|
|
391
|
-
var bad=['failed','error','crashed','killed'].indexOf(t.status)>=0;
|
|
392
|
-
var cents=(typeof totalCents==='number')?totalCents:((t.cost&&typeof t.cost.spent==='number')?t.cost.spent:null);
|
|
393
|
-
var co=(cents!==null)?'$'+(cents/100).toFixed(cents<10?4:2):'';
|
|
394
|
-
var meta=[(runCount>1?'':taskDur(t)),co].filter(Boolean).join(' \\u00b7 ')+(runCount>1&&co?' total':'');
|
|
395
|
-
var g=dayLogGlance(t);
|
|
396
|
-
return '<div class="dlog'+(live?' live':'')+'" tabindex="0" role="button" style="--rail:'+railHue(t.agent,t.agent,false)+'" onclick="showD(\\''+t.sessionId+'\\')" onkeydown="if(event.key===\\'Enter\\')showD(\\''+t.sessionId+'\\')">'
|
|
397
|
-
+'<span class="dlog-t" data-user-content>'+esc(title(t.prompt))+'</span>'
|
|
398
|
-
+(runCount>1?'<span class="dlog-x">\\u00d7'+runCount+'</span>':'')
|
|
399
|
-
+(_verified?'<span class="dlog-v">verified</span>':(bad?'<span class="dlog-b">stopped</span>':(live?'<span class="dlog-l">'+esc(String(t.status))+'</span>':'')))
|
|
400
|
-
+(g?'<span class="dlog-g" data-user-content>'+esc(g)+'</span>':'')
|
|
401
|
-
+(meta?'<span class="dlog-r">'+esc(meta)+'</span>':'')
|
|
402
|
-
+'</div>';
|
|
403
|
-
}
|
|
404
|
-
function renderRoom(){
|
|
405
|
-
const c=document.getElementById('task-list');
|
|
406
|
-
const _or=document.getElementById('observer-rail'); if(_or){ _or.style.display='none'; }
|
|
407
|
-
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,ch=>({'&':'&','<':'<','>':'>','"':'"'}[ch]));
|
|
408
|
-
const cwd=_room;
|
|
409
|
-
const projects=_cached.projects||[];
|
|
410
|
-
// A room is a PROJECT (a repo), which may span several local clones. Find the
|
|
411
|
-
// project that owns this cwd (membership over its localPaths), then scope the
|
|
412
|
-
// room to every clone's turns. Falls back to the single cwd when no project
|
|
413
|
-
// record exists (e.g. mock daemon with no /api/projects).
|
|
414
|
-
const p=projects.find(x=>projPaths(x).indexOf(normC(cwd))>=0)||projects.find(x=>normC(x.cwd)===normC(cwd));
|
|
415
|
-
const paths=p?projPaths(p):[normC(cwd)];
|
|
416
|
-
// S18 · practice runs never sit among the room's real turns either — they
|
|
417
|
-
// collapse behind the same quiet practice row (still one click to detail).
|
|
418
|
-
// Base the room on the FULL project thread when loaded (all turns, all-time), then let
|
|
419
|
-
// live/recent tasks from the cache override by sessionId (fresher status/verdict/cost,
|
|
420
|
-
// plus just-spawned turns not yet mirrored). Falls back to the cache subset (mock daemon
|
|
421
|
-
// / pre-fetch) so the room is never empty.
|
|
422
|
-
let roomAll;
|
|
423
|
-
if(_roomThread && _roomThread.cwd===_room && _roomThread.tasks){
|
|
424
|
-
const byId=new Map();
|
|
425
|
-
for(const t of _roomThread.tasks){ if(!/^demo-/.test(t.sessionId||'')) byId.set(t.sessionId, t); }
|
|
426
|
-
for(const t of allS(_cached.tasks||{})){ if(!/^demo-/.test(t.sessionId||'') && paths.indexOf(normC(t.cwd))>=0){ var _prev=byId.get(t.sessionId); byId.set(t.sessionId, (_prev&&_prev.archived&&!('archived' in t))?Object.assign({},t,{archived:_prev.archived}):t); } }
|
|
427
|
-
roomAll=[...byId.values()];
|
|
428
|
-
} else {
|
|
429
|
-
roomAll=allS(_cached.tasks||{}).filter(x=>!/^demo-/.test(x.sessionId||'') && paths.indexOf(normC(x.cwd))>=0);
|
|
430
|
-
}
|
|
431
|
-
const practice=roomAll.filter(x=>x.mockRun);
|
|
432
|
-
const items=roomAll.filter(x=>!x.mockRun);
|
|
433
|
-
items.sort((a,b)=>(a.startedAt||0)-(b.startedAt||0)); // turns in the order they happened
|
|
434
|
-
// Fold zero-signal runs (terminal, no report, no cost, <2 steps) so abandoned
|
|
435
|
-
// junk doesn't sit among real work. An honest failure (with a report, cost, or
|
|
436
|
-
// steps) is signal and stays visible.
|
|
437
|
-
const _zeroSig=function(t){
|
|
438
|
-
if(t.batchId) return false; // batch fan-outs are real multi-agent work
|
|
439
|
-
if(['running','queued','paused','blocked','completed','done'].indexOf(t.status)>=0) return false;
|
|
440
|
-
const hasReport=!!(t.finalReport&&t.finalReport.summary&&String(t.finalReport.summary).trim());
|
|
441
|
-
const hasCost=!!(t.cost&&typeof t.cost.spent==='number'&&t.cost.spent>0);
|
|
442
|
-
return !hasReport && !hasCost && (t.steps||[]).length<2;
|
|
443
|
-
};
|
|
444
|
-
const archivedItems=items.filter(function(x){return x.archived;});
|
|
445
|
-
const folded=items.filter(function(x){return _zeroSig(x)&&!x.archived;});
|
|
446
|
-
const shown=items.filter(function(x){return !_zeroSig(x)&&!x.archived;});
|
|
447
|
-
window._roomSlug=(p&&p.slug)||''; // the archive handler needs the project slug
|
|
448
|
-
const repoName=p?projName(p):(cwd?(cwd.split('/').filter(Boolean).pop()||'Project'):'Project');
|
|
449
|
-
// Title = the project name (same as the breadcrumb + the home list). Using
|
|
450
|
-
// p.title directly diverged ("Agent" vs "muhammadegaa/codehere"); projName
|
|
451
|
-
// already prefers the remote name, so repoName is the one consistent identity.
|
|
452
|
-
const title=repoName;
|
|
453
|
-
const agents=[...new Set(items.map(x=>x.agent).filter(Boolean))];
|
|
454
|
-
// The "← Projects" back button died for the rail's Today row (nav option B,
|
|
455
|
-
// 2026-07-07) — the crumb keeps the place-label, the rail does the moving.
|
|
456
|
-
let html='<div class="room-head">';
|
|
457
|
-
html+='<div class="room-crumb" style="font-family:var(--mono);font-size:10.5px;color:var(--fg-40);margin:2px 0 4px">Projects / '+esc(repoName)+'</div>';
|
|
458
|
-
// Rename is offered ONLY for local projects (no remote): there projName returns
|
|
459
|
-
// p.title, so a rename shows immediately. Remote projects keep their canonical
|
|
460
|
-
// repo name (renaming the slug would be a lie), so no pencil.
|
|
461
|
-
var _canRename=!!(p&&p.slug&&!p.remote);
|
|
462
|
-
html+='<div class="room-title-row"><span class="room-title" id="room-title" data-user-content>'+esc(title)+'</span>'
|
|
463
|
-
+(_canRename?'<button class="room-rename" title="Rename project" aria-label="Rename project" onclick="renameRoom(\\''+esc(p.slug)+'\\')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg></button>':'')
|
|
464
|
-
+'</div>';
|
|
465
|
-
// Everyone sees the plain-English outcome; the cwd path rides along
|
|
466
|
-
// (S20: the persona CSS-hiding died — lenses recompose via templates).
|
|
467
|
-
const _done=shown.filter(x=>x.status==='completed'||x.status==='done').length;
|
|
468
|
-
const _outcome=shown.length?(_done===shown.length?('all '+shown.length+' done'):(_done+' of '+shown.length+' done')):'no tasks yet';
|
|
469
|
-
// Short trailing path, not the dev-y full /Users/... (full path on hover).
|
|
470
|
-
var _cwParts=String(cwd||'').split('/').filter(Boolean);
|
|
471
|
-
var _cwShort=_cwParts.length>3?('\\u2026/'+_cwParts.slice(-3).join('/')):(cwd||'');
|
|
472
|
-
html+='<div class="room-sub">'+esc(_outcome)+(agents.length?' \\u00b7 '+partStack(agents):'')+'<span class="room-cwd" data-user-content title="'+esc(cwd||'')+'"> \\u00b7 '+esc(_cwShort)+'</span></div></div>';
|
|
473
|
-
// Live-agents strip · the room shows who is working right now, at the top,
|
|
474
|
-
// before the task history (L2 mockup). Real state only (running/queued/paused/blocked).
|
|
475
|
-
const _live=items.filter(x=>['running','queued','paused','blocked'].indexOf(x.status)>=0);
|
|
476
|
-
if(_live.length){
|
|
477
|
-
html+='<div class="live-strip">'+_live.map(function(x){
|
|
478
|
-
return '<span class="live-a"><span class="live-dot"></span>'+esc(agentLabel(x.agent)||'agent')+' <span class="live-task" data-user-content>'+esc(String(x.prompt||'').slice(0,48))+'</span></span>';
|
|
479
|
-
}).join('')+'</div>';
|
|
480
|
-
}
|
|
481
|
-
if(shown.length){
|
|
482
|
-
// It-4 V1-MULTITASK + V1-FANOUT-ALL · batch grouping. Turns spawned by one
|
|
483
|
-
// /api/task/batch call share a batchId. When >=2 turns share one, render a
|
|
484
|
-
// single .project-rollup header (counts DERIVED LIVE every render — never a
|
|
485
|
-
// stored aggregate, which would lie after a crash) + the children as a
|
|
486
|
-
// side-by-side .fanout-all grid. Lone turns render as one-line rows.
|
|
487
|
-
const seenBatch={};
|
|
488
|
-
const renderTurn=(t,full)=>{
|
|
489
|
-
const bid=t.batchId;
|
|
490
|
-
if(bid){
|
|
491
|
-
if(seenBatch[bid]) return ''; // batch already rendered at its first turn
|
|
492
|
-
const sibs=items.filter(x=>x.batchId===bid);
|
|
493
|
-
if(sibs.length>=2){ seenBatch[bid]=1; return batchH(sibs); }
|
|
494
|
-
}
|
|
495
|
-
// Day Log (Direction A, Ega-picked 2026-07-06) — ONE row treatment for
|
|
496
|
-
// the whole feed. Depth lives in the detail view, not the feed. The
|
|
497
|
-
// cleanup pass (same day) removed the coexisting full-turn treatment:
|
|
498
|
-
// groups keep only their border + label as context.
|
|
499
|
-
return dayLogRow(t,1);
|
|
500
|
-
};
|
|
501
|
-
// Threads-as-groups · turns continued from one another (a hand-off or a
|
|
502
|
-
// follow-up) wrap into one conversation. Most threads are a single turn —
|
|
503
|
-
// those render exactly as before, no wrapper. Richer once Phase 3 persists
|
|
504
|
-
// cross-agent hand-off edges.
|
|
505
|
-
const groups=threadGroups(shown);
|
|
506
|
-
// Day dividers · the Day Log's calendar seams. Groups arrive sorted by
|
|
507
|
-
// first-turn startedAt, so one running lastDay marker is enough.
|
|
508
|
-
let _lastDay='';
|
|
509
|
-
for(const g of groups){
|
|
510
|
-
const _gts=g[0].startedAt||0;
|
|
511
|
-
if(_gts){ const _dk=new Date(_gts).toDateString(); if(_dk!==_lastDay){ _lastDay=_dk; html+='<div class="turns-label day-div">'+esc(dayLabel(_gts))+'</div>'; } }
|
|
512
|
-
if(g.length>1){
|
|
513
|
-
// A thread that crosses AGENTS is the wedge — make it pop. A same-agent
|
|
514
|
-
// follow-up stays quiet (the plain border); a cross-agent hand-off gets
|
|
515
|
-
// the accent border + an agent-chain header + a "handed to X" marker
|
|
516
|
-
// between the turns where the baton changes hands.
|
|
517
|
-
const ga=[...new Set(g.map(x=>x.agent).filter(Boolean))];
|
|
518
|
-
const crossAgent=ga.length>1;
|
|
519
|
-
const chain=ga.map(agentLabel);
|
|
520
|
-
const border=crossAgent?'2px solid var(--accent)':'2px solid var(--border)';
|
|
521
|
-
// A group can form two ways: lineage edges (a real conversation) or
|
|
522
|
-
// same-title re-runs (the home runs-strip rule). Label what it IS.
|
|
523
|
-
const ids={}; g.forEach(x=>{ ids[x.sessionId]=1; });
|
|
524
|
-
const hasEdge=g.some(x=>{ const f=x.continuedFrom||x.parentSessionId; return f&&ids[f]; });
|
|
525
|
-
// Same-title re-runs (no lineage, one agent) collapse to ONE Day-Log
|
|
526
|
-
// line: newest run's glance + a run count + the family's total cost.
|
|
527
|
-
// Six identical failed runs used to eat 30 lines; now they eat one.
|
|
528
|
-
if(!crossAgent && !hasEdge){
|
|
529
|
-
let cents=0, any=false;
|
|
530
|
-
for(const x of g){ if(x.cost&&typeof x.cost.spent==='number'){ cents+=x.cost.spent; any=true; } }
|
|
531
|
-
html+=dayLogRow(g[g.length-1], g.length, any?cents:undefined);
|
|
532
|
-
continue;
|
|
533
|
-
}
|
|
534
|
-
// A failover is not a chosen hand-off — the label and the baton mark
|
|
535
|
-
// say which one happened (quiet: same treatment, honest words + reason).
|
|
536
|
-
const anyFailover=g.some(x=>x.failoverFrom);
|
|
537
|
-
html+='<div class="thread-group'+(crossAgent?' handoff':'')+'" style="border-left:'+border+';padding-left:10px;margin:6px 0">'
|
|
538
|
-
+'<div class="turns-label" style="margin-top:0">'
|
|
539
|
-
+(crossAgent?(anyFailover?'failover':'hand-off')+' \\u00b7 '+esc(chain.join(' \\u203a ')):(hasEdge?'conversation \\u00b7 '+g.length+' turns':g.length+' runs of this task'))
|
|
540
|
-
+'</div>';
|
|
541
|
-
let prevAgent=null;
|
|
542
|
-
for(const t of g){
|
|
543
|
-
if(crossAgent&&prevAgent&&t.agent&&t.agent!==prevAgent){
|
|
544
|
-
const _foR=(t.failoverFrom&&t.failoverReason)?String(t.failoverReason).replace(/_/g,' '):'';
|
|
545
|
-
html+='<div class="handoff-mark" style="color:var(--accent);font-size:12px;margin:2px 0 2px 2px">\\u21b3 '+(t.failoverFrom?esc(agentLabel(t.agent))+' took over'+(_foR?' \\u00b7 '+esc(_foR):''):'handed to '+esc(agentLabel(t.agent)))+'</div>';
|
|
546
|
-
}
|
|
547
|
-
html+=renderTurn(t);
|
|
548
|
-
if(t.agent) prevAgent=t.agent;
|
|
549
|
-
}
|
|
550
|
-
html+='</div>';
|
|
551
|
-
} else {
|
|
552
|
-
html+=renderTurn(g[0]);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
// One quiet door to the proof — for the moment someone actually doubts, not
|
|
556
|
-
// a standing billboard about our own honesty. The audit log is still there;
|
|
557
|
-
// it just doesn't narrate itself in the conversation.
|
|
558
|
-
} else if(!practice.length && !folded.length){
|
|
559
|
-
html+='<div class="empty-state"><h2>No tasks here yet.</h2><p style="color:var(--fg-50);font-size:13px;margin-top:6px">Describe a task below \\u00b7 any agent picks it up, and codehere records what it did and checks it.</p></div>';
|
|
560
|
-
}
|
|
561
|
-
if(folded.length){
|
|
562
|
-
html+='<details class="folded-runs"><summary>'+folded.length+' run'+(folded.length===1?'':'s')+' that changed nothing</summary><div class="reveal-wrap"><div class="reveal-body">'+folded.map(function(t){return dayLogRow(t,1);}).join('')+'</div></div></details>';
|
|
563
|
-
}
|
|
564
|
-
if(archivedItems.length){
|
|
565
|
-
html+='<details class="folded-runs archived-runs"><summary>'+archivedItems.length+' archived</summary><div class="reveal-wrap"><div class="reveal-body">'+archivedItems.map(function(t){return dayLogRow(t,1);}).join('')+'</div></div></details>';
|
|
566
|
-
}
|
|
567
|
-
practice.sort((a,b)=>(a.startedAt||0)-(b.startedAt||0));
|
|
568
|
-
html+=pracGroupHtml(practice);
|
|
569
|
-
// Project-memory nudge (stranger-walk finding 2026-06-06): web-only users
|
|
570
|
-
// never run codehere-init, so agents silently get NO memory of the project.
|
|
571
|
-
// First-run only (Ega 2026-06-17) — shown on a fresh project (<=1 turn), not
|
|
572
|
-
// re-nagged on every visit. The confirmation says "from the next task" because
|
|
573
|
-
// injection happens at spawn — no fake instant effect.
|
|
574
|
-
if(_roomMemory===false && items.length<=1){
|
|
575
|
-
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>';
|
|
576
|
-
} else if(_roomMemory==='just-on'){
|
|
577
|
-
html+='<div class="mem-nudge on">\\u2713 project memory is on \\u00b7 agents will remember this project from the next task</div>';
|
|
578
|
-
}
|
|
579
|
-
// S12 · same non-destructive morph as renderL (hover/scroll survive polls).
|
|
580
|
-
morphInto(c, html);
|
|
581
|
-
}
|
|
582
|
-
// Inline-rename a (local) project. Swaps the title to an input; Enter or blur
|
|
583
|
-
// saves via the existing POST /api/projects/:slug/rename, Esc restores. Same-origin
|
|
584
|
-
// POST (no token needed). On success, refetch + re-render so the new name shows
|
|
585
|
-
// everywhere (room title + crumb + home), consistently.
|
|
586
|
-
window.renameRoom = function(slug){
|
|
587
|
-
var el=document.getElementById('room-title'); if(!el) return;
|
|
588
|
-
var cur=el.textContent||'';
|
|
589
|
-
var inp=document.createElement('input');
|
|
590
|
-
inp.className='room-title-edit'; inp.value=cur; inp.setAttribute('aria-label','Project name');
|
|
591
|
-
el.replaceWith(inp); inp.focus(); inp.select();
|
|
592
|
-
var settled=false;
|
|
593
|
-
var finish=function(save){
|
|
594
|
-
if(settled) return; settled=true;
|
|
595
|
-
var v=(inp.value||'').trim();
|
|
596
|
-
if(save && v && v!==cur){
|
|
597
|
-
fetch('/api/projects/'+encodeURIComponent(slug)+'/rename',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:v})})
|
|
598
|
-
.then(function(r){return r.json().catch(function(){return {};});})
|
|
599
|
-
.then(function(){ return (typeof fetchAll==='function'?fetchAll():Promise.resolve()); })
|
|
600
|
-
.then(function(){ if(window._room) renderRoom(); })
|
|
601
|
-
.catch(function(){ render(); });
|
|
602
|
-
} else { render(); }
|
|
603
|
-
};
|
|
604
|
-
inp.onkeydown=function(e){ if(e.key==='Enter'){e.preventDefault();finish(true);} else if(e.key==='Escape'){e.preventDefault();finish(false);} };
|
|
605
|
-
inp.onblur=function(){ finish(true); };
|
|
606
|
-
};
|
|
607
|
-
// Archive (soft-delete) or restore a task. POSTs the audit-safe archive event,
|
|
608
|
-
// then refetches the project thread so the archived flag is recomputed, and
|
|
609
|
-
// re-renders. The task is never deleted — it moves to the "archived" reveal.
|
|
610
|
-
window.archiveTask = function(sessionId, archived){
|
|
611
|
-
var slug=window._roomSlug; if(!slug) return;
|
|
612
|
-
fetch('/api/projects/'+encodeURIComponent(slug)+'/archive',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:sessionId, archived:!!archived})})
|
|
613
|
-
.then(function(r){return r.json().catch(function(){return {};});})
|
|
614
|
-
.then(function(){
|
|
615
|
-
return fetch('/api/projects/'+encodeURIComponent(slug)).then(function(x){return x.json();}).then(function(j){
|
|
616
|
-
if(j&&j.thread&&window._room){ _roomThread={cwd:_room, tasks:(j.thread.tasks||[])}; }
|
|
617
|
-
}).catch(function(){});
|
|
618
|
-
})
|
|
619
|
-
.then(function(){ if(window._room) renderRoom(); })
|
|
620
|
-
.catch(function(){});
|
|
621
|
-
};
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
// C-01 · first-run indexing banner · auto-show when session count < 3 +
|
|
625
|
-
// only once per browser session (sessionStorage tracks the dismissal).
|
|
626
|
-
function upIdx(){
|
|
627
|
-
const banner=document.getElementById('indexing-banner');
|
|
628
|
-
if(!banner)return;
|
|
629
|
-
try{
|
|
630
|
-
if(sessionStorage.getItem('codehere_indexing_dismissed')==='1'){banner.style.display='none'; return;}
|
|
631
|
-
}catch{}
|
|
632
|
-
const all=allS(_cached.tasks||{});
|
|
633
|
-
banner.style.display = all.length<3 ? 'block' : 'none';
|
|
634
|
-
if(all.length>=3){
|
|
635
|
-
try{sessionStorage.setItem('codehere_indexing_dismissed','1');}catch{}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// F-02 · cross-worktree alert · scan for any running session whose cwd
|
|
640
|
-
// differs from the most-common cwd AND has been running > 5 minutes. Pulses
|
|
641
|
-
// in the squad row · click to jump to that session's detail.
|
|
642
|
-
function upWk(){
|
|
643
|
-
const all=allS(_cached.tasks||{});
|
|
644
|
-
const running=all.filter(x=>['running','queued','paused','blocked'].includes(x.status) && x.cwd);
|
|
645
|
-
const ae=document.getElementById('worktree-alert');
|
|
646
|
-
const at=document.getElementById('worktree-alert-text');
|
|
647
|
-
if(!ae||!at){return;}
|
|
648
|
-
if(running.length<2){ae.style.display='none'; window._wkTarget=null; return;}
|
|
649
|
-
// Pick the cwd that's most-common · everything else is "other"
|
|
650
|
-
const cwdCounts={};
|
|
651
|
-
for(const r of running) cwdCounts[normC(r.cwd)]=(cwdCounts[normC(r.cwd)]||0)+1;
|
|
652
|
-
let topCwd='', topN=0;
|
|
653
|
-
for(const k in cwdCounts) if(cwdCounts[k]>topN){topN=cwdCounts[k]; topCwd=k;}
|
|
654
|
-
const now=Date.now();
|
|
655
|
-
const oddOnes=running.filter(r=>normC(r.cwd)!==topCwd && (now-r.startedAt) > 5*60*1000);
|
|
656
|
-
if(!oddOnes.length){ae.style.display='none'; window._wkTarget=null; return;}
|
|
657
|
-
const t=oddOnes[0];
|
|
658
|
-
window._wkTarget=t.sessionId;
|
|
659
|
-
// Shorten cwd to its last path component for display
|
|
660
|
-
const short=String(t.cwd||'').split('/').filter(Boolean).slice(-1)[0] || t.cwd;
|
|
661
|
-
// S12 · taskDur honesty rule for the chip: a live minute counter ONLY for a
|
|
662
|
-
// session that is verifiably live (a heartbeat — its latest step or its
|
|
663
|
-
// spawn — within the last 30 minutes). A stuck/stalled record (the demo
|
|
664
|
-
// fixture frozen 16 days on "running") shows its stalled state, never a
|
|
665
|
-
// "running 23362m" lie.
|
|
666
|
-
const _steps=t.steps||[];
|
|
667
|
-
const _lastStep=_steps.length?_steps[_steps.length-1]:null;
|
|
668
|
-
const _beat=Math.max(t.startedAt||0, _lastStep&&(_lastStep.completedAt||_lastStep.startedAt)||0);
|
|
669
|
-
ae.style.display='inline-flex';
|
|
670
|
-
if(t.stalledReason || (now-_beat) > 30*60*1000){
|
|
671
|
-
at.textContent='stalled in '+short;
|
|
672
|
-
} else {
|
|
673
|
-
const mins=Math.floor((now-t.startedAt)/60000);
|
|
674
|
-
at.textContent='running '+mins+'m in '+short;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// ── S12 · A-home hero — the story of your day ──────────────────────────────
|
|
679
|
-
// Every number is COMPUTED from _cached.tasks, never hardcoded: today =
|
|
680
|
-
// startedAt within the LOCAL day · agents = distinct agents among today's
|
|
681
|
-
// non-demo tasks (demo/practice sessions never inflate the story). Zero tasks
|
|
682
|
-
// today -> the honest first-run hero, no fake numbers. data-verify-hero-*
|
|
683
|
-
// stamps carry the claim so the H13 probe can re-derive and catch a lie.
|
|
684
|
-
let _heroPrev={agents:0,verified:0,caught:0,stopped:0,running:0,spend:0};
|
|
685
|
-
let _heroSnap=null;
|
|
686
|
-
function heroCounts(){
|
|
687
|
-
// S18 · PAIRED with verify-page H13's expected-derivation (hReal filter):
|
|
688
|
-
// practice runs (mockRun) never feed the day-line — a practice agent never
|
|
689
|
-
// counts as "worked for you today". Change BOTH sides identically (the
|
|
690
|
-
// honest-rule update), never one side alone.
|
|
691
|
-
const all=allS(_cached.tasks||{}).filter(t=>!/^demo-/.test(t.sessionId||'')&&!t.mockRun);
|
|
692
|
-
const sod=new Date(); sod.setHours(0,0,0,0);
|
|
693
|
-
const today=all.filter(t=>(t.startedAt||0)>=sod.getTime());
|
|
694
|
-
const liveSt=['running','queued','paused','blocked'];
|
|
695
|
-
// Refuter C4 · a task still running that started BEFORE midnight is part of
|
|
696
|
-
// "now" — live counts ignore the day boundary; day-scoped counts don't.
|
|
697
|
-
const live=all.filter(t=>liveSt.includes(t.status));
|
|
698
|
-
// Refuter C1 · "verified" derives from the VERDICT (claims match recorded
|
|
699
|
-
// reality), never the dead t.verification field — and never a mock run, a
|
|
700
|
-
// failed run, or an untracked repo (the same gates the green block obeys).
|
|
701
|
-
// 2.2 · "verified" derives from the COMPOSITE GRADE (green only with an
|
|
702
|
-
// INDEPENDENT signal — observed on disk or adversarial cross-vendor), never
|
|
703
|
-
// self-report alone. Fallback to the legacy claim check for any verdict with
|
|
704
|
-
// no grade attached (real tasks always carry one via composeVerdict).
|
|
705
|
-
const isVerified=t=>!!(t.verdict&&(t.verdict.grade?t.verdict.grade.green:(t.verdict.claim&&t.verdict.claim.status==='verified')))
|
|
706
|
-
&& !t.mockRun
|
|
707
|
-
&& ['completed','done'].includes(t.status)
|
|
708
|
-
&& !(t.artifacts&&t.artifacts.noGitTracking);
|
|
709
|
-
return {
|
|
710
|
-
today: today.length,
|
|
711
|
-
agents: [...new Set(today.concat(live).map(t=>String(t.agent||'')).filter(Boolean))].length,
|
|
712
|
-
verified: today.filter(isVerified).length,
|
|
713
|
-
// Refuter C3 · "caught" means the VERIFIER caught a contradiction —
|
|
714
|
-
// user-killed / crashed runs are honest "didn't finish", a separate word.
|
|
715
|
-
caught: today.filter(t=>{var v=t.verdict;return !!(v&&(v.grade?v.grade.headline==='broken':(v.claim&&v.claim.status==='broken')));}).length,
|
|
716
|
-
stopped: today.filter(t=>['failed','error','crashed','killed'].includes(t.status)).length,
|
|
717
|
-
running: live.length,
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
// S13 · with tasks today the hero element IS the TODAY day-line — same
|
|
721
|
-
// element, same data-verify-hero-* stamps, same heroCounts derivations
|
|
722
|
-
// (H13 unchanged). The big two-line hero renders ONLY first-run, where it
|
|
723
|
-
// keeps its teach copy. The S12 proof strip merged into the OSS footer
|
|
724
|
-
// (one audit truth line, not two).
|
|
725
|
-
function upHero(){
|
|
726
|
-
const el=document.getElementById('home-hero');
|
|
727
|
-
if(!el) return;
|
|
728
|
-
if(_room){ el.style.display='none'; document.body.classList.remove('first-run'); return; }
|
|
729
|
-
el.style.display='block';
|
|
730
|
-
// A scan/fix lives INSIDE this hero's innerHTML. While one is mid-flight, never rebuild the hero —
|
|
731
|
-
// it would wipe the result (and break the running fix timer's DOM refs) out from under the user.
|
|
732
|
-
if(window._scanBusy) return;
|
|
733
|
-
const c=heroCounts();
|
|
734
|
-
const p=_cached.pricing;
|
|
735
|
-
const spendC=p?(p.spentTodayCents||0):null;
|
|
736
|
-
const capC=p&&p.dailyBudgetCents?p.dailyBudgetCents:null;
|
|
737
|
-
// The claim, stamped for the H13 probe — re-derived independently there.
|
|
738
|
-
el.setAttribute('data-verify-hero-mode', c.today?'counts':'first-run');
|
|
739
|
-
el.setAttribute('data-verify-hero-agents', String(c.agents));
|
|
740
|
-
el.setAttribute('data-verify-hero-verified', String(c.verified));
|
|
741
|
-
el.setAttribute('data-verify-hero-caught', String(c.caught));
|
|
742
|
-
el.setAttribute('data-verify-hero-stopped', String(c.stopped));
|
|
743
|
-
el.setAttribute('data-verify-hero-running', String(c.running));
|
|
744
|
-
// The product home is never a landing page — always the compact day-line, never
|
|
745
|
-
// the big marketing hero (that pitch lives on codehere.uk + the onboarding tour).
|
|
746
|
-
document.body.classList.remove('first-run');
|
|
747
|
-
el.classList.add('day-line-mode');
|
|
748
|
-
const snap=JSON.stringify([c.today,c.agents,c.verified,c.caught,c.stopped,c.running,spendC,capC]);
|
|
749
|
-
if(snap===_heroSnap) return; // unchanged — leave the DOM (and any focus) alone
|
|
750
|
-
_heroSnap=snap;
|
|
751
|
-
if(!c.today){
|
|
752
|
-
// No activity today: a slim, product-y day-line. When there's earlier work
|
|
753
|
-
// (surfaced as "Recent" below), drop the "no tasks" note — it contradicts it.
|
|
754
|
-
const dt=new Date();
|
|
755
|
-
const DAYS=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
|
|
756
|
-
const MONTHS=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
757
|
-
var _anyHist=allS(_cached.tasks||{}).some(function(x){return !/^demo-/.test(x.sessionId||'')&&!x.mockRun;});
|
|
758
|
-
el.innerHTML='<div class="day-line"><span class="dl-today">'+DAYS[dt.getDay()]+' \\u00b7 '+MONTHS[dt.getMonth()]+' '+dt.getDate()+'</span>'+(_anyHist?'':'<span>\\u00b7</span><span class="dl-quiet">no tasks yet today</span>')+'</div>';
|
|
759
|
-
_heroPrev={agents:0,verified:0,caught:0,stopped:0,running:0,spend:0};
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
// Day-line · plain language, calm casing. "checked" splits into verified
|
|
763
|
-
// (the claim held) and caught (the claim broke) — a broken claim is never
|
|
764
|
-
// folded into a "verified" count. Every number still comes from heroCounts;
|
|
765
|
-
// the data-verify-hero-* stamps (set in upHero) are untouched.
|
|
766
|
-
el.innerHTML='<div class="day-line"><span class="dl-today">Today</span>'
|
|
767
|
-
+'<span>\\u00b7</span><span><b>'+c.agents+'</b> agent'+(c.agents===1?'':'s')+'</span>'
|
|
768
|
-
+(c.verified>0?'<span>\\u00b7</span><span><b>'+c.verified+'</b> verified</span>':'')
|
|
769
|
-
+(c.caught>0?'<span>\\u00b7</span><span><b>'+c.caught+'</b> caught</span>':'')
|
|
770
|
-
+(c.running>0?'<span>\\u00b7</span><span><b>'+c.running+'</b> running</span>':'')
|
|
771
|
-
+(spendC!=null?'<span>\\u00b7</span><span><b>$'+(spendC/100).toFixed(2)+'</b>'+(capC?' / $'+(capC/100).toFixed(2):'')+'</span>':'')
|
|
772
|
-
+'</div>';
|
|
773
|
-
_heroPrev={agents:c.agents,verified:c.verified,caught:c.caught,stopped:c.stopped,running:c.running,spend:spendC!=null?spendC/100:0};
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function fmtGap(ms){
|
|
777
|
-
// Two-line label · big "4h 38m later" + small relative-time sub
|
|
778
|
-
// ("≈ 5 hours" · "≈ 2 days"). Demo v12 lines 425-426 ·
|
|
779
|
-
// .time-gap .label b / .time-gap .label .sub. R10 retro 2026-05-24:
|
|
780
|
-
// earlier adjective sub ("between sessions") was banked as a
|
|
781
|
-
// "numbers over adjectives" violation · replaced with a real
|
|
782
|
-
// approximate delta so the gap reads as a measurement, not a feeling.
|
|
783
|
-
let main;
|
|
784
|
-
const m=Math.floor(ms/60000);
|
|
785
|
-
if(m<60) main = m+'m later';
|
|
786
|
-
else {
|
|
787
|
-
const h=Math.floor(m/60); const rm=m%60;
|
|
788
|
-
if(h<24) main = rm ? h+'h '+rm+'m later' : h+'h later';
|
|
789
|
-
else main = Math.floor(h/24)+'d later';
|
|
790
|
-
}
|
|
791
|
-
let sub;
|
|
792
|
-
const hours = ms / 3600000;
|
|
793
|
-
if(hours < 1) {
|
|
794
|
-
const mins = Math.round(ms/60000);
|
|
795
|
-
sub = '\\u2248 ' + mins + ' minute' + (mins===1?'':'s');
|
|
796
|
-
} else if(hours < 24) {
|
|
797
|
-
const h = Math.round(hours);
|
|
798
|
-
sub = '\\u2248 ' + h + ' hour' + (h===1?'':'s');
|
|
799
|
-
} else if(hours < 24*30) {
|
|
800
|
-
const days = Math.round(hours/24);
|
|
801
|
-
sub = '\\u2248 ' + days + ' day' + (days===1?'':'s');
|
|
802
|
-
} else {
|
|
803
|
-
const wks = Math.round(hours/(24*7));
|
|
804
|
-
sub = '\\u2248 ' + wks + ' week' + (wks===1?'':'s');
|
|
805
|
-
}
|
|
806
|
-
return '<b>'+main+'</b><span class="sub">'+sub+'</span>';
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// One goal = one room regardless of path SPELLING. New spawns canonicalize
|
|
810
|
-
// cwd server-side (realpath at the spawn boundary), but persisted records
|
|
811
|
-
// predate that: on macOS /tmp/x and /private/tmp/x are the same directory
|
|
812
|
-
// (/tmp -> /private/tmp symlink) and rendered as TWO goals for one repo
|
|
813
|
-
// (browser dogfood 2026-06-10). Merge the known-equivalent legacy spellings
|
|
814
|
-
// at every cwd comparison; display keeps the canonical (/private) form.
|
|
815
|
-
function normC(s){ var c=String(s==null?'':s); return c.replace(/^\\/(tmp|var)(\\/|$)/,'/private/$1$2'); }
|
|
816
|
-
|
|
817
|
-
// S12 · non-destructive list render. Build the new HTML into a detached
|
|
818
|
-
// container and morph children by index (the SAME reconcile writeDetail
|
|
819
|
-
// uses), so a poll never wipes hover / focus / scroll — every unchanged card
|
|
820
|
-
// keeps its exact DOM node. This is the renderL/renderRoom counterpart of
|
|
821
|
-
// writeDetail's morph; morphNode is hoisted (function declaration).
|
|
822
|
-
function morphInto(c, html){
|
|
823
|
-
if(!c.firstElementChild && !(c.textContent||'').trim()){ c.innerHTML=html; return; }
|
|
824
|
-
var tmp=document.createElement('div'); tmp.innerHTML=html;
|
|
825
|
-
var nw=Array.prototype.slice.call(tmp.childNodes);
|
|
826
|
-
for(var i=0;i<nw.length;i++){
|
|
827
|
-
var o=c.childNodes[i];
|
|
828
|
-
if(!o){ c.appendChild(nw[i]); continue; }
|
|
829
|
-
morphNode(o, nw[i]);
|
|
830
|
-
}
|
|
831
|
-
while(c.childNodes.length>nw.length){ c.removeChild(c.lastChild); }
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// S12 · EARLIER chapter progressive disclosure — render the latest ~20, the
|
|
835
|
-
// rest behind an explicit expander (no unbounded DOM, no scroll hijack).
|
|
836
|
-
let _earlierShown = 20;
|
|
837
|
-
function showEarlier(){
|
|
838
|
-
_earlierShown = Number.MAX_SAFE_INTEGER;
|
|
839
|
-
const sc=document.querySelector('.content');
|
|
840
|
-
const y=sc?sc.scrollTop:0;
|
|
841
|
-
renderL();
|
|
842
|
-
if(sc) sc.scrollTop=y; // scroll preserved across expansion
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// S13 · the history row — EARLIER + demo fixtures collapse behind ONE quiet
|
|
846
|
-
// row; expanding reuses the S12 scroll-preserving mechanics. The harness
|
|
847
|
-
// expands/collapses through this REAL render path (never DOM injection).
|
|
848
|
-
let _histOpen = false;
|
|
849
|
-
function toggleHist(){
|
|
850
|
-
const sc=document.querySelector('.content');
|
|
851
|
-
const y=sc?sc.scrollTop:0;
|
|
852
|
-
_histOpen=!_histOpen;
|
|
853
|
-
if(!_histOpen) _earlierShown=20;
|
|
854
|
-
renderL();
|
|
855
|
-
if(sc) sc.scrollTop=y; // scroll preserved across expansion
|
|
856
|
-
}
|
|
857
|
-
window.__histExpand=function(){ if(!_histOpen){ _histOpen=true; renderL(); } };
|
|
858
|
-
window.__histCollapse=function(){ if(_histOpen){ _histOpen=false; _earlierShown=20; renderL(); } };
|
|
859
|
-
|
|
860
|
-
// S18 · the practice expander — practice (CODEHERE_MOCK) runs collapse behind
|
|
861
|
-
// their own quiet row beside the history row. Same scroll-preserving
|
|
862
|
-
// mechanics as toggleHist; shared by the home and the room render.
|
|
863
|
-
let _pracOpen = false;
|
|
864
|
-
function togglePrac(){
|
|
865
|
-
const sc=document.querySelector('.content');
|
|
866
|
-
const y=sc?sc.scrollTop:0;
|
|
867
|
-
_pracOpen=!_pracOpen;
|
|
868
|
-
if(_room) renderRoom(); else renderL();
|
|
869
|
-
if(sc) sc.scrollTop=y;
|
|
870
|
-
}
|
|
871
|
-
window.__pracExpand=function(){ if(!_pracOpen){ _pracOpen=true; if(_room) renderRoom(); else renderL(); } };
|
|
872
|
-
window.__pracCollapse=function(){ if(_pracOpen){ _pracOpen=false; if(_room) renderRoom(); else renderL(); } };
|
|
873
|
-
// One quiet row + (when open) the one-line practice rows. Used by renderL
|
|
874
|
-
// and renderRoom so practice never sits among real work on either surface.
|
|
875
|
-
function pracGroupHtml(practice){
|
|
876
|
-
if(!practice.length) return '';
|
|
877
|
-
let html='<button class="hist-row prac-row" onclick="togglePrac()" aria-expanded="'+(_pracOpen?'true':'false')+'" data-verify-unit="practice" data-verify-practice-n="'+practice.length+'">practice runs \\u00b7 '+practice.length+(_pracOpen?' \\u00b7 collapse':' \\u00b7 expand \\u203A')+'</button>';
|
|
878
|
-
if(_pracOpen) html+='<div class="practice-rows">'+practice.map(rowH).join('')+'</div>';
|
|
879
|
-
return html;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// One goal group (head + its cards, with the >2h time-gap separators kept).
|
|
883
|
-
function goalGroupsHtml(items, titleFor, esc){
|
|
884
|
-
const groups=new Map();
|
|
885
|
-
for(const t of items){
|
|
886
|
-
const key=normC(t.cwd)||'ungrouped';
|
|
887
|
-
if(!groups.has(key)) groups.set(key,{cwd:t.cwd,items:[]});
|
|
888
|
-
groups.get(key).items.push(t);
|
|
889
|
-
}
|
|
890
|
-
const lastTs=g=>g.items.reduce((m,x)=>Math.max(m,x.startedAt||0),0);
|
|
891
|
-
const ordered=[...groups.values()].sort((a,b)=>lastTs(b)-lastTs(a));
|
|
892
|
-
let html='';
|
|
893
|
-
for(const g of ordered){
|
|
894
|
-
g.items.sort((a,b)=>(b.startedAt||0)-(a.startedAt||0));
|
|
895
|
-
const running=g.items.filter(x=>['running','queued','paused','blocked'].includes(x.status)).length;
|
|
896
|
-
const meta=g.items.length+' task'+(g.items.length===1?'':'s')+(running?' · '+running+' running':'');
|
|
897
|
-
html+='<div class="goal-group"><div class="goal-head" tabindex="0" role="button" data-goal-cwd="'+encodeURIComponent(g.cwd||'')+'" onclick="event.stopPropagation();openRoom(\\''+encodeURIComponent(g.cwd||'')+'\\')" onkeydown="if(event.key===\\'Enter\\'||event.key===\\' \\'){event.preventDefault();openRoom(\\''+encodeURIComponent(g.cwd||'')+'\\')}"><span class="goal-title" data-user-content>'+esc(titleFor(g.cwd))+'</span><span class="goal-meta">'+esc(meta)+' \\u00b7 open \\u203A</span></div>';
|
|
898
|
-
// Time-gap separator within a goal · a big gap between consecutive tasks
|
|
899
|
-
// in the same goal is meaningful ("picked this back up 3 days later").
|
|
900
|
-
let prevTs=null;
|
|
901
|
-
for(const runs of groupByTitle(g.items)){
|
|
902
|
-
const t=runs[0];
|
|
903
|
-
const ts=t.startedAt||0;
|
|
904
|
-
if(prevTs!==null && ts>0 && prevTs>0){
|
|
905
|
-
const gap=Math.abs(prevTs-ts);
|
|
906
|
-
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>'; }
|
|
907
|
-
}
|
|
908
|
-
html+=groupCardH(runs);
|
|
909
|
-
prevTs=ts;
|
|
910
|
-
}
|
|
911
|
-
html+='</div>';
|
|
912
|
-
}
|
|
913
|
-
return html;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// Project = a repository (git remote = identity; clones collapse server-side).
|
|
917
|
-
// Display name: org/repo from the remote when present, else the project title
|
|
918
|
-
// or the folder basename.
|
|
919
|
-
function projName(p){
|
|
920
|
-
if(p&&p.remote){ const parts=String(p.remote).split('/').filter(Boolean); return parts.length>=2?parts.slice(1).join('/'):p.remote; }
|
|
921
|
-
if(p&&p.title) return p.title;
|
|
922
|
-
const cwd=(p&&(p.cwd||(p.localPaths&&p.localPaths[0])))||'';
|
|
923
|
-
const seg=String(cwd).split('/').filter(Boolean); return seg.length?seg[seg.length-1]:'Project';
|
|
924
|
-
}
|
|
925
|
-
// Compact relative-age label for a project's last activity.
|
|
926
|
-
function projAgo(ts){
|
|
927
|
-
if(!ts) return '';
|
|
928
|
-
const s=Math.max(0,Math.floor((Date.now()-ts)/1000));
|
|
929
|
-
if(s<60) return 'just now';
|
|
930
|
-
const m=Math.floor(s/60); if(m<60) return m+'m ago';
|
|
931
|
-
const h=Math.floor(m/60); if(h<24) return h+'h ago';
|
|
932
|
-
const d=Math.floor(h/24); return d+'d ago';
|
|
933
|
-
}
|
|
934
|
-
// The set of normalized cwds that belong to a project (its clones).
|
|
935
|
-
function projPaths(p){ return ((p&&p.localPaths&&p.localPaths.length)?p.localPaths:[(p&&p.cwd)||'']).map(normC); }
|
|
936
|
-
// The project-first home body: one row per repository, newest-first. Live
|
|
937
|
-
// running counts + thread counts overlay from the live /api/tasks records
|
|
938
|
-
// (which carry the freshest status), keyed to the project via its localPaths.
|
|
939
|
-
function projectListHtml(esc){
|
|
940
|
-
const projects=_cached.projects||[];
|
|
941
|
-
if(!projects.length) return '';
|
|
942
|
-
const real=allS(_cached.tasks||{}).filter(x=>!/^demo-/.test(x.sessionId||'')&&!x.mockRun);
|
|
943
|
-
// normalized-cwd -> project.id
|
|
944
|
-
const cwdToId={};
|
|
945
|
-
projects.forEach(p=>{ projPaths(p).forEach(np=>{ if(np) cwdToId[np]=p.id; }); });
|
|
946
|
-
const tasksById={};
|
|
947
|
-
real.forEach(t=>{ const pid=cwdToId[normC(t.cwd)]; if(pid){ (tasksById[pid]=tasksById[pid]||[]).push(t); } });
|
|
948
|
-
const _live=t=>['running','queued','paused','blocked'].includes(t.status);
|
|
949
|
-
let html='<div class="section-label section-label-row">Projects<button class="add-gh" onclick="event.stopPropagation();openClone()" title="Clone a GitHub repo and open it as a project">+ from GitHub</button></div>';
|
|
950
|
-
for(const p of projects){
|
|
951
|
-
const mine=tasksById[p.id]||[];
|
|
952
|
-
const running=mine.filter(_live).length;
|
|
953
|
-
// turns = all-time count from the project thread.jsonl (p.taskCount). A "threads"
|
|
954
|
-
// count is intentionally NOT shown here: it was computed from _cached.tasks (a recent
|
|
955
|
-
// subset) while turns came from thread.jsonl (all-time) — incoherent. Threads-as-groups
|
|
956
|
-
// is shown IN the room where it's consistent; bring a count back (server-side, same
|
|
957
|
-
// source) once cross-agent hand-offs make multi-turn threads meaningful (Phase 3).
|
|
958
|
-
const turns=p.taskCount||mine.length||0;
|
|
959
|
-
const bits=[];
|
|
960
|
-
bits.push(turns+' turn'+(turns===1?'':'s'));
|
|
961
|
-
if(running) bits.push(running+' running');
|
|
962
|
-
if(p.totalCostCents>0) bits.push('$'+(p.totalCostCents/100).toFixed(2));
|
|
963
|
-
const ago=projAgo(p.lastActivityAt); if(ago) bits.push(ago);
|
|
964
|
-
const cwd=(p.cwd||(p.localPaths&&p.localPaths[0]))||'';
|
|
965
|
-
html+='<div class="goal-group'+(running?' running':'')+'"><div class="goal-head" tabindex="0" role="button" data-verify-unit="project" data-project-id="'+esc(p.id)+'" data-goal-cwd="'+encodeURIComponent(cwd)+'" onclick="event.stopPropagation();openRoom(\\''+encodeURIComponent(cwd)+'\\')" onkeydown="if(event.key===\\'Enter\\'||event.key===\\' \\'){event.preventDefault();openRoom(\\''+encodeURIComponent(cwd)+'\\')}">'
|
|
966
|
-
+'<span class="goal-title" data-user-content>'+esc(projName(p))+'</span>'
|
|
967
|
-
+'<span class="goal-meta">'+(partStack(p.recentAgents)?partStack(p.recentAgents)+' \\u00b7 ':'')+esc(bits.join(' \\u00b7 '))+' \\u00b7 open \\u203A</span></div></div>';
|
|
968
|
-
}
|
|
969
|
-
return html;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
function renderL() {
|
|
973
|
-
const c=document.getElementById('task-list'), all=allS(_cached.tasks||{});
|
|
974
|
-
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,ch=>({'&':'&','<':'<','>':'>','"':'"'}[ch]));
|
|
975
|
-
const fl=all.filter(FILTERS[_filter]||FILTERS.all);
|
|
976
|
-
let html='';
|
|
977
|
-
// Demo-shape sessions (id prefix 'demo-') render AFTER the real work —
|
|
978
|
-
// they demonstrate the product, but the home leads with YOUR story.
|
|
979
|
-
const demo=fl.filter(x=>/^demo-/.test(x.sessionId||''));
|
|
980
|
-
// S18 · practice runs (mockRun-stamped by the daemon under CODEHERE_MOCK)
|
|
981
|
-
// never sit among real work — they collapse behind their own quiet row
|
|
982
|
-
// beside the history expander. Same records, same showD detail: grouped,
|
|
983
|
-
// never deleted, never unreachable. Demo fixtures stay the FEATURED group.
|
|
984
|
-
const practice=fl.filter(x=>!/^demo-/.test(x.sessionId||'')&&x.mockRun);
|
|
985
|
-
const rest=fl.filter(x=>!/^demo-/.test(x.sessionId||'')&&!x.mockRun);
|
|
986
|
-
// cwd -> project title (from /api/projects); fall back to the folder name.
|
|
987
|
-
const projects=_cached.projects||[];
|
|
988
|
-
const titleFor=cwd=>{
|
|
989
|
-
if(!cwd) return 'Ungrouped';
|
|
990
|
-
const p=projects.find(x=>normC(x.cwd)===normC(cwd));
|
|
991
|
-
if(p&&p.title) return p.title;
|
|
992
|
-
const parts=String(cwd).split('/').filter(Boolean);
|
|
993
|
-
return parts.length?parts[parts.length-1]:'Ungrouped';
|
|
994
|
-
};
|
|
995
|
-
// S13 · TODAY = live tasks (any start day) + today's finished work, as
|
|
996
|
-
// ONE-LINE rows (direction E), live first then newest. EARLIER + the demo
|
|
997
|
-
// fixtures collapse behind the single history row; expanding keeps the S12
|
|
998
|
-
// goal groups (goal heads still open rooms, time-gaps + demo cards intact).
|
|
999
|
-
const _live=t=>['running','queued','paused','blocked'].includes(t.status);
|
|
1000
|
-
const _sod=new Date(); _sod.setHours(0,0,0,0);
|
|
1001
|
-
const _dayStart=_sod.getTime();
|
|
1002
|
-
const chToday=rest.filter(t=>_live(t)||(t.startedAt||0)>=_dayStart);
|
|
1003
|
-
chToday.sort((a,b)=>{ const la=_live(a)?1:0, lb=_live(b)?1:0; if(la!==lb) return lb-la; return (b.startedAt||0)-(a.startedAt||0); });
|
|
1004
|
-
let chEarlier=rest.filter(t=>!_live(t)&&(t.startedAt||0)<_dayStart);
|
|
1005
|
-
chEarlier.sort((a,b)=>(b.startedAt||0)-(a.startedAt||0));
|
|
1006
|
-
const histN=chEarlier.length+demo.length;
|
|
1007
|
-
if(chToday.length){
|
|
1008
|
-
html+='<div class="section-label">Recent</div><div class="today-rows">'+groupByTitle(chToday).map(groupRowH).join('')+'</div>';
|
|
1009
|
-
}
|
|
1010
|
-
// Projects live in the persistent sidebar now; on the home pane the list is a
|
|
1011
|
-
// duplicate (the confusion Ega flagged), so .home-projects is hidden on desktop
|
|
1012
|
-
// and kept only for mobile (sidebar collapses) + the verify probe (display:none
|
|
1013
|
-
// still matches querySelectorAll).
|
|
1014
|
-
var _projHtml=projectListHtml(esc); // '' when there are no projects (keeps the first-run empty-state trigger alive)
|
|
1015
|
-
if(_projHtml) html+='<div class="home-projects">'+_projHtml+'</div>';
|
|
1016
|
-
// A returning user whose last work was before today would otherwise land on an
|
|
1017
|
-
// EMPTY home — everything hidden behind this collapsed "earlier" row. So when
|
|
1018
|
-
// there's nothing today, surface the recent work DIRECTLY (grouped by project),
|
|
1019
|
-
// never a blank page. When there IS work today, keep "earlier" collapsible.
|
|
1020
|
-
const _todayEmpty=!chToday.length;
|
|
1021
|
-
if(histN){
|
|
1022
|
-
if(_todayEmpty){
|
|
1023
|
-
html+='<div class="section-label">Recent</div>';
|
|
1024
|
-
} else {
|
|
1025
|
-
html+='<button class="hist-row" onclick="toggleHist()" aria-expanded="'+(_histOpen?'true':'false')+'">earlier \\u00b7 '+histN+' run'+(histN===1?'':'s')+(_histOpen?' \\u00b7 collapse':' \\u00b7 expand \\u203A')+'</button>';
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
if(_histOpen||_todayEmpty){
|
|
1029
|
-
const earlierTotal=chEarlier.length;
|
|
1030
|
-
const earlierHidden=Math.max(0, earlierTotal-_earlierShown);
|
|
1031
|
-
if(earlierHidden>0) chEarlier=chEarlier.slice(0,_earlierShown);
|
|
1032
|
-
if(chEarlier.length){
|
|
1033
|
-
html+=goalGroupsHtml(chEarlier, titleFor, esc);
|
|
1034
|
-
if(earlierHidden>0){
|
|
1035
|
-
html+='<button class="show-earlier" onclick="showEarlier()">show earlier work ('+earlierHidden+' more)</button>';
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
if(demo.length){
|
|
1039
|
-
html+='<div class="section-label featured">Featured \\u00b7 demo</div>';
|
|
1040
|
-
for(const t of demo) html+=cardH(t);
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
practice.sort((a,b)=>(b.startedAt||0)-(a.startedAt||0));
|
|
1044
|
-
html+=pracGroupHtml(practice);
|
|
1045
|
-
if(!html){
|
|
1046
|
-
if(_filter==='all'){
|
|
1047
|
-
// First-run onboarding: what codehere is, the coding agents detected on THIS
|
|
1048
|
-
// machine (the cross-vendor lineup is the whole point), and a one-click first
|
|
1049
|
-
// task. Genuine first run only (no tasks, no projects).
|
|
1050
|
-
html='<div class="empty-state onboarding">'
|
|
1051
|
-
+'<h2>Welcome to codehere</h2>'
|
|
1052
|
-
+'<p class="ob-lede">Running locally. No account, nothing leaves your machine. Two steps to your first verified task:</p>'
|
|
1053
|
-
+'<div class="ob-cta"><button class="ob-tourbtn" onclick="startTour()">Take the 30-second tour \\u2192</button></div>'
|
|
1054
|
-
+'<div class="ob-step"><span class="ob-num">1</span><div class="ob-b"><div class="ob-h">Your agents</div>'+obAgentsHtml()+'</div></div>'
|
|
1055
|
-
+'<div class="ob-step"><span class="ob-num">2</span><div class="ob-b"><div class="ob-h">Start a task</div>'
|
|
1056
|
-
+'<div class="ob-ex">'
|
|
1057
|
-
+'<button class="ex-prompt" data-ex="Review the code in this project for bugs and security issues, and tell me what to fix" onclick="exFill(this.dataset.ex)">Review my code for bugs</button>'
|
|
1058
|
-
+'<button class="ex-prompt" data-ex="Explain how this project is structured and what the main parts do" onclick="exFill(this.dataset.ex)">Explain this codebase</button>'
|
|
1059
|
-
+'<button class="ex-prompt" data-ex="Add a test for the most important function in this project and make it pass" onclick="exFill(this.dataset.ex)">Write a test</button>'
|
|
1060
|
-
+'</div>'
|
|
1061
|
-
+'<div class="ex-gh">Working on a repo on GitHub? <button class="add-gh" onclick="openClone()">+ from GitHub</button></div>'
|
|
1062
|
-
+'</div></div>'
|
|
1063
|
-
+'</div>';
|
|
1064
|
-
} else {
|
|
1065
|
-
const h={running:'No running tasks.',done:'No completed tasks.',failed:'No failed tasks.'};
|
|
1066
|
-
html='<div class="empty-state"><h2>'+(h[_filter]||'No tasks found.')+'</h2></div>';
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
// S12 · morph, never wipe — zero hover/scroll/focus loss on the poll.
|
|
1070
|
-
morphInto(c, html);
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
${TOUR}
|
|
1074
|
-
${ONBOARDING}
|
|
1075
|
-
${GITHUB_INTEGRATION}
|
|
1076
|
-
|
|
1077
|
-
${OBSERVER_RAIL}
|
|
1078
|
-
|
|
1079
|
-
// It-4 V1-MULTITASK + V1-FANOUT-ALL · render one batch (siblings sharing a
|
|
1080
|
-
// batchId) as a derived-live rollup header + a side-by-side fanout grid.
|
|
1081
|
-
// Counts are computed from the child records on EVERY render — never stored —
|
|
1082
|
-
// so a daemon crash mid-run reads "running", not a stale "done" (the plan's
|
|
1083
|
-
// honesty constraint). Each grid column reuses cardH so a column is a real
|
|
1084
|
-
// turn card labelled with the real adapter chip; a not-ready adapter is simply
|
|
1085
|
-
// absent (it never got a child), never a faked column.
|
|
1086
|
-
function batchH(sibs){
|
|
1087
|
-
const isDone=x=>x.status==='completed'||x.status==='done';
|
|
1088
|
-
const isRun=x=>['running','queued','paused','blocked'].includes(x.status);
|
|
1089
|
-
const isFail=x=>['error','killed','crashed','failed'].includes(x.status);
|
|
1090
|
-
const n=sibs.length, done=sibs.filter(isDone).length, run=sibs.filter(isRun).length, fail=sibs.filter(isFail).length;
|
|
1091
|
-
const parts=[done+' done'];
|
|
1092
|
-
if(run>0) parts.push(run+' running');
|
|
1093
|
-
if(fail>0) parts.push(fail+' failed');
|
|
1094
|
-
let html='<div class="project-rollup"><span class="rollup-icon">≡</span>'
|
|
1095
|
-
+'<span class="rollup-count">'+n+' sub-task'+(n===1?'':'s')+'</span>'
|
|
1096
|
-
+'<span class="rollup-sep">\\u00b7</span><span class="rollup-stat">'+parts.join(' \\u00b7 ')+'</span>'
|
|
1097
|
-
+'<span class="rollup-note">sent to every ready agent</span></div>';
|
|
1098
|
-
// Fanout grid · one column per child, sorted by adapter for a stable order.
|
|
1099
|
-
const ordered=[...sibs].sort((a,b)=>String(a.agent||'').localeCompare(String(b.agent||'')));
|
|
1100
|
-
html+='<div class="fanout-all">';
|
|
1101
|
-
for(const t of ordered){
|
|
1102
|
-
html+='<div class="fanout-col"><div class="fanout-col-head">'+ab(t.agent,'sm')+agentLabel(t.agent)+'</div>'+cardH(t)+'</div>';
|
|
1103
|
-
}
|
|
1104
|
-
html+='</div>';
|
|
1105
|
-
return html;
|
|
1106
|
-
}
|
|
1107
|
-
// S12 · ONE story-arc line per card, derived from REAL data only — steps
|
|
1108
|
-
// count, verdict status, failover lineage, a caught contradiction when
|
|
1109
|
-
// tests.claimed disagrees with the observed counts. No real arc data -> ''
|
|
1110
|
-
// (the existing meta stays); we never invent narrative.
|
|
1111
|
-
function cardArc(t){
|
|
1112
|
-
const he=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
1113
|
-
const steps=t.steps||[];
|
|
1114
|
-
if(['running','queued','paused','blocked'].includes(t.status)){
|
|
1115
|
-
if(!steps.length) return '';
|
|
1116
|
-
const last=steps[steps.length-1];
|
|
1117
|
-
return '<div class="card-arc">Now: <span data-user-content>'+compactLabel(last.label||last.tool||'')+'</span> \\u00b7 step '+steps.length+'</div>';
|
|
1118
|
-
}
|
|
1119
|
-
const parts=[];
|
|
1120
|
-
if(t.failoverFrom&&t.failoverTo) parts.push('<span class="wr">recovered from '+he(agentLabel(t.failoverFrom))+'</span>');
|
|
1121
|
-
const tests=t.artifacts&&t.artifacts.tests;
|
|
1122
|
-
const vr=t.verdict&&t.verdict.claim;
|
|
1123
|
-
const passed=tests?(tests.passed||0):0, failed=tests?(tests.failed||0):0;
|
|
1124
|
-
const cl=tests&&tests.claimed;
|
|
1125
|
-
const caught=!!(cl&&((cl.passed!==passed)||(cl.failed!==failed)));
|
|
1126
|
-
const isFail=['failed','error','crashed','killed'].includes(t.status);
|
|
1127
|
-
if(caught){
|
|
1128
|
-
parts.push('<span class="wr">verifier caught a claimed '+cl.passed+'/'+(cl.passed+cl.failed)+'</span> \\u203A re-ran \\u203A '+passed+'/'+(passed+failed));
|
|
1129
|
-
} else if(isFail && failed>0){
|
|
1130
|
-
parts.push('<span class="wr">verifier found '+failed+' red test'+(failed===1?'':'s')+' \\u00b7 left unmerged</span>');
|
|
1131
|
-
} else if(vr&&vr.status==='broken'){
|
|
1132
|
-
parts.push('<span class="wr">claim contradicts reality \\u00b7 caught by the verifier</span>');
|
|
1133
|
-
} else if(vr&&vr.status==='verified'&&!isFail&&tests&&tests.observed){
|
|
1134
|
-
parts.push('<span class="ok">verifier re-ran the tests \\u203A '+passed+'/'+(passed+failed)+' \\u2713</span>');
|
|
1135
|
-
}
|
|
1136
|
-
if(!parts.length) return '';
|
|
1137
|
-
const lead=steps.length?steps.length+' step'+(steps.length===1?'':'s')+' \\u203A ':'';
|
|
1138
|
-
return '<div class="card-arc">'+lead+parts.join(' \\u00b7 ')+'</div>';
|
|
1139
|
-
}
|
|
1140
|
-
function cardH(t) {
|
|
1141
|
-
// Unified card (2026-06-20): every card — today, history, demo, fanout — uses
|
|
1142
|
-
// the SAME .tcard render as the home rows, so the product never shows two card
|
|
1143
|
-
// styles. Preserve the demo pin marker (the F-PIN-DEMO coverage selector
|
|
1144
|
-
// .task-card.tc-pinned).
|
|
1145
|
-
var h = rowH(t);
|
|
1146
|
-
if(/^demo-/.test(t.sessionId||'')) h = h.replace('class="task-card tcard', 'class="task-card tcard tc-pinned');
|
|
1147
|
-
return h;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
// S13 · ONE-line row (direction E): status dot+word · title · agent (mono,
|
|
1151
|
-
// no rainbow) · checks · cost. Keeps the task-card class + tabindex/Enter +
|
|
1152
|
-
// the stop control + the agent-* class — harness selectors + the morph rely
|
|
1153
|
-
// on them. Verified green ONLY through the same gate heroCounts uses (S12).
|
|
1154
|
-
// S20 fix · ONE verdict precedence for every row-level derivation. This is
|
|
1155
|
-
// the canonical detail verdictLine order (practice first, then the gated
|
|
1156
|
-
// verified branches, then broken) extended with the row-only states the
|
|
1157
|
-
// detail never renders (stalled, live, terminal-unclean, done). rowStatus,
|
|
1158
|
-
// rowClaims and outcomeSentence all read THIS state, never their own branch
|
|
1159
|
-
// order — before this, each hand-rolled its own precedence and the same
|
|
1160
|
-
// mock+broken record wore a different verdict word per lens.
|
|
1161
|
-
function verdictState(t){
|
|
1162
|
-
const claim=(t.verdict&&t.verdict.claim&&t.verdict.claim.status)||'';
|
|
1163
|
-
const live=['running','queued','paused','blocked'].includes(t.status);
|
|
1164
|
-
const unclean=['failed','error','crashed','killed'].includes(t.status);
|
|
1165
|
-
const doneSt=['completed','done'].includes(t.status);
|
|
1166
|
-
const noGit=!!(t.artifacts&&t.artifacts.noGitTracking);
|
|
1167
|
-
if(t.mockRun&&!live) return 'practice';
|
|
1168
|
-
if(claim==='verified'&&doneSt&&!noGit) return 'verified';
|
|
1169
|
-
if(claim==='verified'&&doneSt&&noGit) return 'partial';
|
|
1170
|
-
if(claim==='broken') return 'caught';
|
|
1171
|
-
if(t.stalledReason) return 'stalled';
|
|
1172
|
-
if(live) return 'live';
|
|
1173
|
-
if(unclean) return 'stopped';
|
|
1174
|
-
if(doneSt) return 'done';
|
|
1175
|
-
return 'unknown';
|
|
1176
|
-
}
|
|
1177
|
-
function rowStatus(t){
|
|
1178
|
-
switch(verdictState(t)){
|
|
1179
|
-
case 'verified': return {cls:'verified',word:'Verified'};
|
|
1180
|
-
case 'caught': return {cls:'caught',word:'Caught'};
|
|
1181
|
-
case 'stalled': return {cls:'stalled',word:'Stalled'};
|
|
1182
|
-
case 'live':
|
|
1183
|
-
if(t.status==='running') return {cls:'running',word:'Running'};
|
|
1184
|
-
if(t.status==='queued') return {cls:'running',word:'Queued'};
|
|
1185
|
-
return {cls:'running',word:'Paused'};
|
|
1186
|
-
case 'stopped': return {cls:'stopped',word:'Stopped'};
|
|
1187
|
-
case 'practice':
|
|
1188
|
-
// Default vocabulary stays run-status (the practice group already names
|
|
1189
|
-
// the mode; the detail's verdict word is Practice) — but a practice
|
|
1190
|
-
// verdict NEVER wears Caught/Verified: a mock run fabricated its own
|
|
1191
|
-
// artifacts, so a verdict word here would contradict the detail.
|
|
1192
|
-
if(t.stalledReason) return {cls:'stalled',word:'Stalled'};
|
|
1193
|
-
if(['failed','error','crashed','killed'].includes(t.status)) return {cls:'stopped',word:'Stopped'};
|
|
1194
|
-
return {cls:'done',word:'Done'};
|
|
1195
|
-
case 'partial': return {cls:'done',word:'Done'};
|
|
1196
|
-
case 'done': return {cls:'done',word:'Done'};
|
|
1197
|
-
default: return {cls:'done',word:String(t.status||'?')};
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
function rowH(t){
|
|
1201
|
-
// S20 · Status lens: the row IS the outcome sentence (template branch).
|
|
1202
|
-
if(curLens()==='status') return statusRowH(t);
|
|
1203
|
-
const rs=rowStatus(t);
|
|
1204
|
-
const agentClass='agent-'+String(t.agent||'').toLowerCase().replace(/[^a-z0-9]/g,'-');
|
|
1205
|
-
const live=['running','queued','paused','blocked'].includes(t.status);
|
|
1206
|
-
const tests=t.artifacts&&t.artifacts.tests;
|
|
1207
|
-
const passed=tests?(tests.passed||0):0, failed=tests?(tests.failed||0):0, total=passed+failed;
|
|
1208
|
-
const co=(t.cost&&typeof t.cost.spent==='number')?'$'+(t.cost.spent/100).toFixed(2):'';
|
|
1209
|
-
const nf=(t.artifacts&&t.artifacts.files&&t.artifacts.files.length)||0;
|
|
1210
|
-
// the status stat-pill carries the HONEST verdict word (rowClaims) — green ✓
|
|
1211
|
-
// ONLY when verified, never a fake ✓ for a stopped/caught/partial run. Live
|
|
1212
|
-
// gets the run pulse. This keeps the H15 honesty contract across both lenses.
|
|
1213
|
-
const vs=verdictState(t);
|
|
1214
|
-
// 2.2 honesty gate — IDENTICAL to the detail headline + summary tile: a green
|
|
1215
|
-
// ✓ verified ONLY when the composite grade confirms an INDEPENDENT signal.
|
|
1216
|
-
// A self-reported run (grade present but not green) reads '○ reported', never
|
|
1217
|
-
// a green check. Legacy/seeded verdicts with no grade keep the claim check.
|
|
1218
|
-
const _graded=t.verdict&&t.verdict.grade;
|
|
1219
|
-
const _green=vs==='verified'&&(_graded?!!t.verdict.grade.green:true);
|
|
1220
|
-
var pill;
|
|
1221
|
-
if(live){ pill='<span class="tc-stat run"><span class="tc-pulse"></span>'+rs.word+'</span>'; }
|
|
1222
|
-
else if(_green){ pill='<span class="tc-stat ok">\\u2713 verified'+(total>0?' '+passed+'/'+total:'')+'</span>'; }
|
|
1223
|
-
else if(vs==='verified'){ pill='<span class="tc-stat neutral">\\u25CB reported</span>'; }
|
|
1224
|
-
else { var cl=rowClaims(t); var pc=(vs==='caught'?'fail':'neutral'); pill='<span class="tc-stat '+pc+'">'+cl.word+'</span>'; }
|
|
1225
|
-
// meta line: the vendor + what it touched (mockup: "Claude Code · 3 files")
|
|
1226
|
-
var meta='<span class="tc-vendor">'+agentLabel(t.agent)+'</span>'+(nf?' \\u00b7 '+nf+' file'+(nf===1?'':'s'):'');
|
|
1227
|
-
const stop=live?'<button class="card-stop" data-verify-action="stop" title="Stop this agent now" aria-label="Stop this agent now" onclick="stopTask(event,\\''+t.sessionId+'\\')">\\u25A0 stop</button>':'';
|
|
1228
|
-
return '<div class="task-card tcard '+agentClass+(live?' active':'')+'" tabindex="0" role="button" onclick="showD(\\''+t.sessionId+'\\')">'
|
|
1229
|
-
+'<span class="tc-av">'+ab(t.agent)+'</span>'
|
|
1230
|
-
+'<div class="tc-body"><div class="tc-title" data-user-content title="'+convHe(displayPrompt(t.prompt)).slice(0,400)+'">'+convHe(title(t.prompt))+'</div>'
|
|
1231
|
-
+'<div class="tc-meta">'+meta+'</div></div>'
|
|
1232
|
-
+'<div class="tc-glyphs">'+pill+(co?'<span class="tc-cost">'+co+'</span>':'')+stop+'<span class="tc-arrow">\\u203A</span></div>'
|
|
1233
|
-
+'</div>';
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// S21 · thread grouping — runs of the SAME task (same title) collapse into one
|
|
1237
|
-
// thread: the newest run is the head card, older runs nest below as compact rows.
|
|
1238
|
-
// Every run keeps its own showD(sessionId) target (one-click record, and the e2e
|
|
1239
|
-
// sid selectors still resolve). A single-run task renders exactly as before.
|
|
1240
|
-
function groupByTitle(items){
|
|
1241
|
-
const map=new Map();
|
|
1242
|
-
// Same guards as threadGroups (cold-review P2): Untitled tasks never merge
|
|
1243
|
-
// (each keys on its own sessionId) and the key is cwd-scoped so same-title
|
|
1244
|
-
// tasks from different projects stay separate threads.
|
|
1245
|
-
for(const t of items){ const tt=title(t.prompt); const k=(tt&&tt!=='Untitled')?(String(t.cwd||'')+'\\u0000'+tt):('sid:'+String(t.sessionId||'')); if(!map.has(k)) map.set(k,[]); map.get(k).push(t); }
|
|
1246
|
-
const groups=[];
|
|
1247
|
-
for(const pair of map){ const runs=pair[1]; runs.sort((a,b)=>(b.startedAt||0)-(a.startedAt||0)); groups.push(runs); }
|
|
1248
|
-
groups.sort((a,b)=>(b[0].startedAt||0)-(a[0].startedAt||0));
|
|
1249
|
-
return groups;
|
|
1250
|
-
}
|
|
1251
|
-
function _runRowH(r){ const rc=rowClaims(r); const co=(r.cost&&typeof r.cost.spent==='number')?'$'+(r.cost.spent/100).toFixed(2):'\\u2014';
|
|
1252
|
-
const sid=String(r.sessionId||'').replace(/[^A-Za-z0-9._:-]/g,'');
|
|
1253
|
-
return '<div class="task-run" tabindex="0" role="button" onclick="showD(\\''+sid+'\\')"><span class="tr-who">'+convHe(agentLabel(r.agent))+'</span><span class="tr-st st-'+rc.cls+'">'+convHe(rc.word)+'</span><span class="tr-c">'+co+'</span></div>'; }
|
|
1254
|
-
function _threadWrap(headHtml, runs){
|
|
1255
|
-
let cents=0; for(const r of runs){ if(r.cost&&typeof r.cost.spent==='number') cents+=r.cost.spent; }
|
|
1256
|
-
const tot=cents?'$'+(cents/100).toFixed(2):'';
|
|
1257
|
-
return '<div class="task-thread">'+headHtml
|
|
1258
|
-
+'<div class="task-runs"><div class="tr-more">'+runs.length+' runs \\u00b7 '+tot+'</div>'+runs.slice(1).map(_runRowH).join('')+'</div>'
|
|
1259
|
-
+'</div>';
|
|
1260
|
-
}
|
|
1261
|
-
function groupRowH(runs){ return runs.length===1?rowH(runs[0]):_threadWrap(rowH(runs[0]), runs); }
|
|
1262
|
-
function groupCardH(runs){ return runs.length===1?cardH(runs[0]):_threadWrap(cardH(runs[0]), runs); }
|
|
1263
|
-
|
|
1264
|
-
// ── S20 · lenses: claims-state word + honest outcome sentence ──────────────
|
|
1265
|
-
// rowClaims = the Review lead. outcomeSentence = the Status line, built ONLY
|
|
1266
|
-
// from recorded verdict/status/stall facts (the S13 story-arc rule — no real
|
|
1267
|
-
// data, no narrative). BOTH derive from verdictState above — one precedence,
|
|
1268
|
-
// one truth per record, only the vocabulary differs per lens. A stopped or
|
|
1269
|
-
// stalled run wears the same word family as the default lens (stopped, never
|
|
1270
|
-
// a failure word the default lens does not claim).
|
|
1271
|
-
function rowClaims(t){
|
|
1272
|
-
switch(verdictState(t)){
|
|
1273
|
-
case 'practice': return {cls:'done',word:'\\u25CB practice'};
|
|
1274
|
-
case 'verified': return {cls:'verified',word:'\\u2713 verified'};
|
|
1275
|
-
case 'partial': return {cls:'done',word:'\\u25CB partial'};
|
|
1276
|
-
case 'caught': return {cls:'caught',word:'\\u2715 caught'};
|
|
1277
|
-
case 'stalled': return {cls:'stopped',word:'\\u25A0 stalled'};
|
|
1278
|
-
case 'live': return {cls:'running',word:'\\u25CF unchecked'};
|
|
1279
|
-
case 'stopped': return {cls:'stopped',word:'\\u25A0 stopped'};
|
|
1280
|
-
default: return {cls:'done',word:'\\u25CB unverified'};
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
function outcomeSentence(t){
|
|
1284
|
-
switch(verdictState(t)){
|
|
1285
|
-
case 'practice': return 'practice run \\u00b7 wiring only';
|
|
1286
|
-
case 'verified': return 'verified \\u00b7 claims match reality';
|
|
1287
|
-
case 'partial': return 'done \\u00b7 partly verified \\u00b7 not a git repo';
|
|
1288
|
-
case 'caught': return 'caught \\u00b7 the claim contradicted the record';
|
|
1289
|
-
case 'stalled': return 'did not finish \\u00b7 stopped for going silent';
|
|
1290
|
-
case 'live': return t.status==='running'?'in progress':String(t.status);
|
|
1291
|
-
case 'stopped': return 'did not finish \\u00b7 '+String(t.status);
|
|
1292
|
-
case 'done': return 'done \\u00b7 unverified';
|
|
1293
|
-
default: return String(t.status||'unknown');
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
// Status-lens row: title + the outcome sentence + spend. Keeps the
|
|
1297
|
-
// .task-card/.trow contract (tabindex, Enter, agent-* class, stop, pin).
|
|
1298
|
-
function statusRowH(t){
|
|
1299
|
-
const agentClass='agent-'+String(t.agent||'').toLowerCase().replace(/[^a-z0-9]/g,'-');
|
|
1300
|
-
const pinClass=/^demo-/.test(t.sessionId||'')?' tc-pinned':'';
|
|
1301
|
-
const live=['running','queued','paused','blocked'].includes(t.status);
|
|
1302
|
-
const stop=live?'<button class="card-stop" data-verify-action="stop" title="Stop this agent now" aria-label="Stop this agent now" onclick="stopTask(event,\\''+t.sessionId+'\\')">\\u25A0 stop</button>':'';
|
|
1303
|
-
const co=(t.cost&&typeof t.cost.spent==='number')?'<span class="row-meta">$'+(t.cost.spent/100).toFixed(2)+'</span>':'';
|
|
1304
|
-
return '<div class="task-card trow srow '+agentClass+pinClass+'" tabindex="0" role="button" onclick="showD(\\''+t.sessionId+'\\')">'
|
|
1305
|
-
+'<span class="row-title" data-user-content title="'+displayPrompt(t.prompt).replace(/"/g,'"').slice(0,400)+'">'+titleH(t.prompt)+'</span>'
|
|
1306
|
-
+'<span class="row-outcome">'+outcomeSentence(t)+'</span>'
|
|
1307
|
-
+co
|
|
1308
|
-
+stop
|
|
1309
|
-
+'</div>';
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// ── S13 · NEEDS YOUR DECISION — the only orange on the page ────────────────
|
|
1313
|
-
// Derived from REAL state on every render: (a) pending W-PASS intents (the
|
|
1314
|
-
// same /api/writepass/pending list the modal polls — window._wpPendingAll),
|
|
1315
|
-
// (b) tasks whose claim the verifier BROKE today, (c) today's failed/killed
|
|
1316
|
-
// runs with the original prompt recorded (Retry can respawn them). Empty ->
|
|
1317
|
-
// the section does not render at all; H14 pins absent <=> N===0.
|
|
1318
|
-
function decisionItems(){
|
|
1319
|
-
// S18 · PAIRED with verify-page H14's expected-derivation (dAll filter):
|
|
1320
|
-
// a practice (mockRun) failure never demands a real decision. Change BOTH
|
|
1321
|
-
// sides identically (the honest-rule update), never one side alone.
|
|
1322
|
-
const all=allS(_cached.tasks||{}).filter(t=>!/^demo-/.test(t.sessionId||'')&&!t.mockRun);
|
|
1323
|
-
const sod=new Date(); sod.setHours(0,0,0,0);
|
|
1324
|
-
const ds=sod.getTime();
|
|
1325
|
-
const today=t=>(t.startedAt||0)>=ds;
|
|
1326
|
-
const passes=window._wpPendingAll||[];
|
|
1327
|
-
const broken=all.filter(t=>today(t)&&t.verdict&&t.verdict.claim&&t.verdict.claim.status==='broken');
|
|
1328
|
-
const failed=all.filter(t=>today(t)&&['failed','error','crashed','killed'].includes(t.status)
|
|
1329
|
-
&&t.prompt&&!(t.verdict&&t.verdict.claim&&t.verdict.claim.status==='broken'));
|
|
1330
|
-
return {passes,broken,failed,n:passes.length+broken.length+failed.length};
|
|
1331
|
-
}
|
|
1332
|
-
function renderDecisions(){
|
|
1333
|
-
const c=document.getElementById('decision-queue');
|
|
1334
|
-
if(!c) return;
|
|
1335
|
-
const he=s=>String(s==null?'':s).replace(/[&<>"]/g,ch=>({'&':'&','<':'<','>':'>','"':'"'}[ch]));
|
|
1336
|
-
const d=decisionItems();
|
|
1337
|
-
if(_room||d.n===0){
|
|
1338
|
-
c.style.display='none'; c.innerHTML='';
|
|
1339
|
-
c.removeAttribute('data-verify-unit'); c.removeAttribute('data-verify-decisions-n');
|
|
1340
|
-
return;
|
|
1341
|
-
}
|
|
1342
|
-
c.style.display='block';
|
|
1343
|
-
c.setAttribute('data-verify-unit','decisions');
|
|
1344
|
-
c.setAttribute('data-verify-decisions-n', String(d.n));
|
|
1345
|
-
// Exactly ONE filled button on the screen: the first row's primary.
|
|
1346
|
-
let first=true;
|
|
1347
|
-
const btn=(act,label,kbd,onclick,fill)=>'<button class="dq-btn'+(fill?' fill':(act==='deny'||act==='retry')?' quiet':'')+'" data-act="'+act+'" onclick="'+onclick+'">'+label+' <span class="kbd">'+kbd+'</span></button>';
|
|
1348
|
-
let rows='';
|
|
1349
|
-
for(const p of d.passes){
|
|
1350
|
-
const it=p.intent||{};
|
|
1351
|
-
rows+='<div class="dq-row" tabindex="0" data-kind="pass">'
|
|
1352
|
-
+'<div class="dq-what"><div class="dq-t"><span class="dq-id">W-PASS</span>'+he(it.emittedBy||'an agent')+' wants to write: <span data-user-content>'+he((it.method||'')+' '+(it.target||''))+'</span></div>'
|
|
1353
|
-
+'<div class="dq-why">external write \\u00b7 gated by codehere'+(it.reason?' \\u00b7 <span data-user-content>'+he(String(it.reason).slice(0,140))+'</span>':'')+'</div>'
|
|
1354
|
-
+'<div class="dq-result" style="display:none"></div></div>'
|
|
1355
|
-
+'<div class="dq-acts">'
|
|
1356
|
-
+btn('approve','Approve','A',"wpQueueDecide('"+he(it.id)+"','approve',this)",first)
|
|
1357
|
-
+btn('deny','Deny','X',"wpQueueDecide('"+he(it.id)+"','cancel',this)",false)
|
|
1358
|
-
+'</div></div>';
|
|
1359
|
-
first=false;
|
|
1360
|
-
}
|
|
1361
|
-
for(const t of d.broken){
|
|
1362
|
-
rows+='<div class="dq-row" tabindex="0" data-kind="broken">'
|
|
1363
|
-
+'<div class="dq-what"><div class="dq-t">'+agentLabel(t.agent)+' says it\\u2019s done. The verifier disagrees.</div>'
|
|
1364
|
-
+'<div class="dq-why"><span data-user-content>'+titleH(t.prompt)+'</span> \\u00b7 the claim contradicts recorded reality \\u00b7 left for your call</div></div>'
|
|
1365
|
-
+'<div class="dq-acts">'+btn('review','Review','\\u23CE',"showD('"+he(t.sessionId)+"')",first)+'</div></div>';
|
|
1366
|
-
first=false;
|
|
1367
|
-
}
|
|
1368
|
-
// Collapse identical failed attempts (same prompt+cwd+agent) into ONE row — a
|
|
1369
|
-
// Retry that re-fails (e.g. a rate-limited agent) shouldn't stack duplicate
|
|
1370
|
-
// decision rows. One row per distinct failure, with the attempt count; the
|
|
1371
|
-
// latest attempt drives Review/Retry.
|
|
1372
|
-
const _fg=[], _fk={};
|
|
1373
|
-
for(const t of d.failed){
|
|
1374
|
-
const k=(t.prompt||'')+'|'+(t.cwd||'');
|
|
1375
|
-
if(_fk[k]!==undefined){ const g=_fg[_fk[k]]; g.count++; if(g.agents.indexOf(t.agent)<0) g.agents.push(t.agent); if((t.startedAt||0)>(g.t.startedAt||0)) g.t=t; }
|
|
1376
|
-
else { _fk[k]=_fg.length; _fg.push({t:t,count:1,agents:[t.agent]}); }
|
|
1377
|
-
}
|
|
1378
|
-
for(const g of _fg){ const t=g.t;
|
|
1379
|
-
const who=g.agents.map(a=>agentLabel(a)).join(', ');
|
|
1380
|
-
rows+='<div class="dq-row" tabindex="0" data-kind="failed">'
|
|
1381
|
-
+'<div class="dq-what"><div class="dq-t"><span data-user-content>'+titleH(t.prompt)+'</span> didn\\u2019t finish.</div>'
|
|
1382
|
-
+'<div class="dq-why">'+who+' \\u00b7 '+he(t.stalledReason||t.status)+(g.count>1?' \\u00b7 '+g.count+' attempts':'')+' \\u00b7 Retry respawns the same prompt fresh</div></div>'
|
|
1383
|
-
+'<div class="dq-acts">'+btn('review','Review','\\u23CE',"showD('"+he(t.sessionId)+"')",first)
|
|
1384
|
-
+btn('retry','Retry','R',"recoverRetry('"+he(t.sessionId)+"')",false)+'</div></div>';
|
|
1385
|
-
first=false;
|
|
1386
|
-
}
|
|
1387
|
-
// Kicker counts the actionable ROWS shown (failures collapsed by attempt), not
|
|
1388
|
-
// raw attempts — so "· N" always matches what's on screen. (The verify
|
|
1389
|
-
// attribute above keeps d.n, the raw count, for the H14 contract.)
|
|
1390
|
-
c.innerHTML='<div class="dq-kicker">Needs your decision <span class="cnt">\\u00b7 '+(d.passes.length+d.broken.length+_fg.length)+'</span></div>'+rows;
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
function upBud() {
|
|
1394
|
-
const p=_cached.pricing||_cached.tasks?.pricing;
|
|
1395
|
-
const e=document.getElementById('budget');
|
|
1396
|
-
const banner=document.getElementById('budget-banner');
|
|
1397
|
-
const bwhat=document.getElementById('budget-banner-what');
|
|
1398
|
-
const bmsg=document.getElementById('budget-banner-msg');
|
|
1399
|
-
if(!p||!p.dailyBudgetCents){
|
|
1400
|
-
e.textContent='';e.className='budget';
|
|
1401
|
-
if(banner) banner.style.display='none';
|
|
1402
|
-
return;
|
|
1403
|
-
}
|
|
1404
|
-
const d=p.dailyBudgetCents,s=p.spentTodayCents||0;
|
|
1405
|
-
e.textContent=' \\u00b7 $'+(s/100).toFixed(2)+' / $'+(d/100).toFixed(2);
|
|
1406
|
-
e.className='budget'+(s>=d?' block':s>=d*0.8?' warn':' safe');
|
|
1407
|
-
const ib=document.querySelector('.input-bar');
|
|
1408
|
-
if(ib){ib.className='input-bar'+(s>=d?' block':s>=d*0.8?' warn':'');}
|
|
1409
|
-
// F-01 · budget threshold banner · only shows when crossing 80%
|
|
1410
|
-
if(banner && bwhat && bmsg){
|
|
1411
|
-
if(s>=d){
|
|
1412
|
-
banner.style.display='block';
|
|
1413
|
-
banner.className='budget-banner block';
|
|
1414
|
-
bwhat.textContent='budget exhausted';
|
|
1415
|
-
bmsg.textContent='daily cap $'+(d/100).toFixed(2)+' spent · paid spawns will block until tomorrow.';
|
|
1416
|
-
} else if(s>=d*0.8){
|
|
1417
|
-
banner.style.display='block';
|
|
1418
|
-
banner.className='budget-banner';
|
|
1419
|
-
bwhat.textContent='budget threshold';
|
|
1420
|
-
bmsg.textContent='$'+(s/100).toFixed(2)+' of $'+(d/100).toFixed(2)+' spent today · paid spawns require confirm.';
|
|
1421
|
-
} else {
|
|
1422
|
-
banner.style.display='none';
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
// Map readiness state -> dot class. 'setup' is distinct from 'busy': busy means
|
|
1428
|
-
// actively working a task, setup means installed/known but can't run until the
|
|
1429
|
-
// user does something (auth, key, install). Conflating them was a screen lie —
|
|
1430
|
-
// a dead-without-setup agent looked like it was working (dogfood 2026-05-27 #4).
|
|
1431
|
-
// S13: the squad strip died; this readiness map feeds the ⌘K Agents command.
|
|
1432
|
-
function agentS(state){if(!state||state==='error')return'offline';if(state==='installed'||state==='ready')return'ready';if(state==='needs_install'||state==='needs_auth'||state==='billed_key_required')return'setup';return'busy';}
|
|
1433
|
-
|
|
1434
|
-
// S13 · filters live in ⌘K; the active filter shows as ONE removable token
|
|
1435
|
-
// above the list (never a tab bar).
|
|
1436
|
-
function setFilter(f){
|
|
1437
|
-
_filter=f;
|
|
1438
|
-
const tk=document.getElementById('filter-token');
|
|
1439
|
-
if(tk){
|
|
1440
|
-
if(f==='all'){ tk.style.display='none'; tk.innerHTML=''; }
|
|
1441
|
-
else { tk.style.display='block'; tk.innerHTML='<span class="ftoken">filter: '+f+' <span class="x" role="button" tabindex="0" aria-label="Clear filter" title="clear \\u00b7 back to everything" onclick="setFilter(\\'all\\')">\\u00d7</span></span>'; }
|
|
1442
|
-
}
|
|
1443
|
-
if(_cached.tasks)render();
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
// URL-sync (eng-review 2026-07-03, D3) — hash deep-link so /#/task/:id is unique,
|
|
1447
|
-
// bookmarkable, and the back-button works. SINGLE-MACHINE by design: session ids
|
|
1448
|
-
// aren't portable across daemons (the record travels by signed bundle, not this
|
|
1449
|
-
// link), so this is DX + a foundation, not team-share. The URL is a PROJECTION:
|
|
1450
|
-
// written only in the mutators (showD/hideD), hydrated on load + hashchange.
|
|
1451
|
-
// render() NEVER reads it — reading it per-frame would re-introduce the
|
|
1452
|
-
// poll-render-wipe class the sweep fixed. String-only (no regex: template literal).
|
|
1453
|
-
function _hashForDetail(){ return _detail ? ('#/task/'+encodeURIComponent(_detail)) : ''; }
|
|
1454
|
-
// The URL spine (nav option A, Ega-approved 2026-07-06): rooms and the Spend
|
|
1455
|
-
// page get hashes too, so refresh / back / forward always land where you were.
|
|
1456
|
-
// Same projection rule as tasks: mutators write the hash, hydration reads it,
|
|
1457
|
-
// render() never touches it. Room ids prefer the project SLUG (short, stable);
|
|
1458
|
-
// a raw cwd is accepted on read as the fallback.
|
|
1459
|
-
function _roomHash(){
|
|
1460
|
-
if(!_room) return '';
|
|
1461
|
-
var p=(_cached.projects||[]).find(function(x){ return projPaths(x).indexOf(normC(_room))>=0; });
|
|
1462
|
-
return '#/room/'+encodeURIComponent(p&&p.slug?p.slug:_room);
|
|
1463
|
-
}
|
|
1464
|
-
function _spendVisible(){ var sv=document.getElementById('spend-view'); return !!(sv&&sv.style.display==='block'); }
|
|
1465
|
-
function _routeFromHash(){
|
|
1466
|
-
var h=location.hash||'', pfx='#/task/', rfx='#/room/';
|
|
1467
|
-
if(h.indexOf(pfx)===0){
|
|
1468
|
-
var id=decodeURIComponent(h.slice(pfx.length));
|
|
1469
|
-
if(!id || id===_detail) return;
|
|
1470
|
-
if(typeof findS==='function' && findS(id)) showD(id);
|
|
1471
|
-
// Unresolvable id → say so. Silently keeping the previous screen under a
|
|
1472
|
-
// hash that names a different task showed the WRONG task to anyone opening
|
|
1473
|
-
// a shared link (verify-distill loop-2 find, confirmed on both builds).
|
|
1474
|
-
else showTaskNotFound(id);
|
|
1475
|
-
} else if(h.indexOf(rfx)===0){
|
|
1476
|
-
var key=decodeURIComponent(h.slice(rfx.length));
|
|
1477
|
-
if(!key) return;
|
|
1478
|
-
var p=(_cached.projects||[]).find(function(x){ return x.slug===key; })
|
|
1479
|
-
||(_cached.projects||[]).find(function(x){ return projPaths(x).indexOf(normC(key))>=0; });
|
|
1480
|
-
var cwd=p?(p.cwd||projPaths(p)[0]):(key.indexOf('/')===0?key:null);
|
|
1481
|
-
if(!cwd){
|
|
1482
|
-
// Unknown project id → say so and land home (leaving whatever screen was
|
|
1483
|
-
// up under an empty hash is the same state-drift the spine kills).
|
|
1484
|
-
al('No project with this id here.');
|
|
1485
|
-
try{ history.replaceState(null,'',location.pathname+location.search); }catch(e){}
|
|
1486
|
-
if(_detail||document.body.classList.contains('detail-open')) hideD();
|
|
1487
|
-
if(_spendVisible()){ document.getElementById('spend-view').style.display='none'; document.getElementById('task-list-view').style.display=''; }
|
|
1488
|
-
if(_room) closeRoom();
|
|
1489
|
-
return;
|
|
1490
|
-
}
|
|
1491
|
-
// Widened gates here + below (ship-review catch): the not-found screen
|
|
1492
|
-
// nulls _detail with the container visible — Back/Today must clear it too.
|
|
1493
|
-
if(_detail||document.body.classList.contains('detail-open')) hideD();
|
|
1494
|
-
if(normC(_room)!==normC(cwd)) openRoom(encodeURIComponent(cwd));
|
|
1495
|
-
} else if(h==='#/spend'){
|
|
1496
|
-
if(!_spendVisible()) showSpend();
|
|
1497
|
-
} else {
|
|
1498
|
-
if(_detail||document.body.classList.contains('detail-open')) hideD();
|
|
1499
|
-
if(_spendVisible()){ document.getElementById('spend-view').style.display='none'; document.getElementById('task-list-view').style.display=''; renderSidebar(); }
|
|
1500
|
-
if(_room) closeRoom();
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
// Honest dead-link state (reuses the .empty-state treatment — no new class).
|
|
1504
|
-
// Plain about WHY the id can miss: this view loads recent + opened-room tasks
|
|
1505
|
-
// only, and session ids aren't portable across machines (the URL-sync design
|
|
1506
|
-
// note above). The one real affordance is the way back.
|
|
1507
|
-
function showTaskNotFound(id){
|
|
1508
|
-
_detail=null;
|
|
1509
|
-
if(_ctlPoll){ clearInterval(_ctlPoll); _ctlPoll=null; } _ctlId=null; // stop polling the task we navigated away from (ship-review catch)
|
|
1510
|
-
document.getElementById('task-list-view').style.display='none';
|
|
1511
|
-
document.getElementById('task-detail-view').style.display='block';
|
|
1512
|
-
var _sv=document.getElementById('spend-view'); if(_sv) _sv.style.display='none';
|
|
1513
|
-
document.body.classList.add('detail-open');
|
|
1514
|
-
var dv=document.getElementById('detail-view');
|
|
1515
|
-
dv.setAttribute('data-sid','');
|
|
1516
|
-
dv.innerHTML='<div class="detail-header"><span class="back-btn" onclick="hideD()">\\u2190 back \\u00b7 <span class="kbd">Esc</span></span></div>'
|
|
1517
|
-
+'<div class="empty-state" data-verify-unit="task-not-found"><h2>No task with this id here.</h2>'
|
|
1518
|
-
+'<p style="color:var(--fg-50);font-size:13px;margin-top:6px" data-user-content>'+String(id).slice(0,80).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];})
|
|
1519
|
-
+'</p><p style="color:var(--fg-40);font-size:12px;margin-top:10px">It may live in a project\\u2019s older history (open that project first), or on another machine \\u2014 task links don\\u2019t travel between daemons; the signed record does.</p></div>';
|
|
1520
|
-
}
|
|
1521
|
-
if(typeof window!=='undefined') window.addEventListener('hashchange', _routeFromHash);
|
|
1522
|
-
function showD(id){
|
|
1523
|
-
if(id!==_detail){ _artOpen=false; _stepOpen={}; _receiptOpen={}; _glMoreOpen={}; } // a different task starts with drawer closed + collapsed receipt + no step expansions carried over
|
|
1524
|
-
_detail=id;
|
|
1525
|
-
try{ var _h=_hashForDetail(); if(location.hash!==_h) location.hash=_h; }catch(e){} // project state → URL (not the reverse)
|
|
1526
|
-
const t=findS(id);
|
|
1527
|
-
if(!t){_detail=null;return;}
|
|
1528
|
-
document.getElementById('task-list-view').style.display='none';
|
|
1529
|
-
document.getElementById('task-detail-view').style.display='block';
|
|
1530
|
-
var _sv2=document.getElementById('spend-view'); if(_sv2) _sv2.style.display='none';
|
|
1531
|
-
// R10 fix · when detail-view opens, hide the global main composer so the
|
|
1532
|
-
// detail-view follow-up composer ("Ask a follow-up about this task...")
|
|
1533
|
-
// is the ONLY input visible. Pre-fix · BOTH composers rendered stacked,
|
|
1534
|
-
// both with "-> Auto · ~$0.02" chips · user couldn't tell which one
|
|
1535
|
-
// typed to which target. Surfaced 2026-05-24 by Ega.
|
|
1536
|
-
document.body.classList.add('detail-open');
|
|
1537
|
-
renderD(t);
|
|
1538
|
-
try{ updateCostEst(); }catch(e){}
|
|
1539
|
-
}
|
|
1540
|
-
function hideD(){
|
|
1541
|
-
_detail=null;
|
|
1542
|
-
// Leaving a task lands in the room (when one is open) — the URL says so too.
|
|
1543
|
-
try{
|
|
1544
|
-
var _h0=location.hash||'';
|
|
1545
|
-
if(_h0.indexOf('#/task/')===0||_h0==='#/spend'){
|
|
1546
|
-
var _rh=_roomHash();
|
|
1547
|
-
if(_rh){ if(_h0!==_rh) location.hash=_rh; }
|
|
1548
|
-
else history.replaceState(null,'',location.pathname+location.search);
|
|
1549
|
-
}
|
|
1550
|
-
}catch(e){}
|
|
1551
|
-
if(_ctlPoll){ clearInterval(_ctlPoll); _ctlPoll=null; } _ctlId=null; // stop control polling
|
|
1552
|
-
document.getElementById('task-list-view').style.display='';
|
|
1553
|
-
document.getElementById('task-detail-view').style.display='none';
|
|
1554
|
-
var _sv=document.getElementById('spend-view'); if(_sv) _sv.style.display='none';
|
|
1555
|
-
document.body.classList.remove('detail-open');
|
|
1556
|
-
render();
|
|
1557
|
-
}
|
|
1558
|
-
// Spend & Audit page: the team cost+audit ledger as its own destination (nav tab),
|
|
1559
|
-
// not a banner on the workspace. Loaded on demand from /api/team.
|
|
1560
|
-
function showSpend(){
|
|
1561
|
-
// Spend is a top-level place — entering it ENDS the room/detail context
|
|
1562
|
-
// (loop-7 verifier catch: without this the project row stayed .active
|
|
1563
|
-
// beside Spend's .here — two you-are-heres — and _detail went stale).
|
|
1564
|
-
if(_detail) hideD();
|
|
1565
|
-
if(_room) closeRoom();
|
|
1566
|
-
document.getElementById('task-list-view').style.display='none';
|
|
1567
|
-
document.getElementById('task-detail-view').style.display='none';
|
|
1568
|
-
document.body.classList.remove('detail-open');
|
|
1569
|
-
var sv=document.getElementById('spend-view'); if(sv) sv.style.display='block';
|
|
1570
|
-
try{ if(location.hash!=='#/spend') location.hash='#/spend'; }catch(e){}
|
|
1571
|
-
try{ renderSidebar(); }catch(e){} // the rail's you-are-here follows
|
|
1572
|
-
try{ renderTeamHero(); }catch(e){}
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
// Open an observed host session as a THREAD · the meta-dogfood payoff: the
|
|
1576
|
-
// coding-agent session running in your own terminal, with every tool call
|
|
1577
|
-
// codehere scanned, in order, hash-chained.
|
|
1578
|
-
async function showHostSession(shortId){
|
|
1579
|
-
_detail='host:'+shortId;
|
|
1580
|
-
document.getElementById('task-list-view').style.display='none';
|
|
1581
|
-
document.getElementById('task-detail-view').style.display='block';
|
|
1582
|
-
document.body.classList.add('detail-open');
|
|
1583
|
-
const dv=document.getElementById('detail-view');
|
|
1584
|
-
dv.innerHTML='<div class="detail-header"><span class="back-btn" onclick="hideD()">\\u2190 Back</span></div><div style="color:var(--fg-40);font-size:12px;padding:20px 0">loading observed session...</div>';
|
|
1585
|
-
let d;
|
|
1586
|
-
try{ d=await fetch('/api/host-sessions/'+encodeURIComponent(shortId)).then(x=>x.json()); }
|
|
1587
|
-
catch(e){ dv.innerHTML='<div class="detail-header"><span class="back-btn" onclick="hideD()">\\u2190 Back</span></div><div style="color:var(--caught);padding:20px 0">Could not load session.</div>'; return; }
|
|
1588
|
-
if(!d||d.error){ dv.innerHTML='<div class="detail-header"><span class="back-btn" onclick="hideD()">\\u2190 Back</span></div><div style="color:var(--fg-40);padding:20px 0">'+he(d&&d.error||'No such session')+'</div>'; return; }
|
|
1589
|
-
function he(s){ return String(s).replace(/[&<>"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
1590
|
-
function ago(ts){ const m=Math.floor((Date.now()-new Date(ts).getTime())/60000); if(m<1)return'just now'; if(m<60)return m+'m ago'; const h=Math.floor(m/60); if(h<24)return h+'h ago'; return Math.floor(h/24)+'d ago'; }
|
|
1591
|
-
function clock(ts){ try{ return new Date(ts).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}); }catch{return'';} }
|
|
1592
|
-
const calls=d.calls||[];
|
|
1593
|
-
// Tool-call timeline · one row per observed call · time · tool · preview · state
|
|
1594
|
-
// No phase narrative here · raw terminal tool calls don't carry lifecycle
|
|
1595
|
-
// semantics (a dev session building codehere mentions "audit"/"build"/"test"
|
|
1596
|
-
// as domain words, so the classifier lies). Phases are a spawned-task concept.
|
|
1597
|
-
// Instead, group calls by tool so the timeline reads as activity, not noise.
|
|
1598
|
-
let timeline='';
|
|
1599
|
-
for(const c of calls){
|
|
1600
|
-
const tag = c.criticalFindings>0 ? '<span class="obs-call-tag crit">critical</span>'
|
|
1601
|
-
: c.blocked ? '<span class="obs-call-tag block">blocked</span>'
|
|
1602
|
-
: c.findings>0 ? '<span class="obs-call-tag warn">'+c.findings+' finding'+(c.findings===1?'':'s')+'</span>'
|
|
1603
|
-
: '<span class="obs-call-tag ok">scanned</span>';
|
|
1604
|
-
timeline+='<div class="obs-call'+(c.blocked?' is-block':'')+'">'
|
|
1605
|
-
+'<span class="obs-call-time">'+he(clock(c.timestamp))+'</span>'
|
|
1606
|
-
+'<span class="obs-call-tool">'+he(c.toolName||'')+'</span>'
|
|
1607
|
-
+'<span class="obs-call-prev">'+(c.preview?he(c.preview):'<span style="color:var(--fg-30)">\\u2014</span>')+'</span>'
|
|
1608
|
-
+tag
|
|
1609
|
-
+'</div>';
|
|
1610
|
-
}
|
|
1611
|
-
// Tool mix summary · gives the observed thread a real "outcome"-style header
|
|
1612
|
-
// (what KIND of work happened) without inventing phases that aren't there.
|
|
1613
|
-
const _mix={};
|
|
1614
|
-
for(const c of calls){ const k=c.toolName||'?'; _mix[k]=(_mix[k]||0)+1; }
|
|
1615
|
-
const _mixStr=Object.entries(_mix).sort((a,b)=>b[1]-a[1]).map(([k,v])=>he(k)+' '+v).join(' · ');
|
|
1616
|
-
// Three states, not two. A chain with no entries walks clean, so treating validity as a
|
|
1617
|
-
// boolean painted this green over a log that did not exist. It is not broken either — there
|
|
1618
|
-
// is simply nothing recorded, and DESIGN.md §1 reserves green for a measurement, which an
|
|
1619
|
-
// empty walk is not.
|
|
1620
|
-
const chainKnown=d.chainVerifiable!==false;
|
|
1621
|
-
const chainOk=chainKnown&&d.chainValid;
|
|
1622
|
-
// Chain-flow · the real audit chain as connected nodes (demo v12 .chain-flow,
|
|
1623
|
-
// gap 4.8). Condensed to KEY events: the first/last few + every blocked or
|
|
1624
|
-
// critical call (never hide those), with a gap marker for the elided middle.
|
|
1625
|
-
// Each node is a real chain entry — seq + tool + state — linked left->right.
|
|
1626
|
-
function chainFlow(cs){
|
|
1627
|
-
if(!cs.length) return '';
|
|
1628
|
-
function he(s){ return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
1629
|
-
function init(tool){ return (String(tool||'?').replace(/[^A-Za-z]/g,'')[0]||'?').toUpperCase(); }
|
|
1630
|
-
var keep=new Set();
|
|
1631
|
-
for(var i=0;i<cs.length;i++){ if(i<2||i>=cs.length-2||cs[i].blocked||cs[i].criticalFindings>0) keep.add(i); }
|
|
1632
|
-
var nodes='';
|
|
1633
|
-
var prevKept=-1;
|
|
1634
|
-
for(var j=0;j<cs.length;j++){
|
|
1635
|
-
if(!keep.has(j)) continue;
|
|
1636
|
-
if(prevKept>=0 && j-prevKept>1) nodes+='<span class="cf-gap" title="'+(j-prevKept-1)+' more scanned">···</span>';
|
|
1637
|
-
else if(prevKept>=0) nodes+='<span class="cf-link"></span>';
|
|
1638
|
-
var c=cs[j];
|
|
1639
|
-
var cls=c.criticalFindings>0?'crit':c.blocked?'block':'ok';
|
|
1640
|
-
var t=c.preview?he(c.preview).slice(0,60):he(c.toolName||'');
|
|
1641
|
-
nodes+='<span class="cf-node '+cls+'" title="'+(c.seq!=null?'#'+c.seq+' · ':'')+he(c.toolName||'')+' · '+t+'">'
|
|
1642
|
-
+'<span class="cf-seq">'+(c.seq!=null?'#'+c.seq:'')+'</span><span class="cf-tool">'+init(c.toolName)+'</span></span>';
|
|
1643
|
-
prevKept=j;
|
|
1644
|
-
}
|
|
1645
|
-
return '<div class="chain-flow-wrap"><div class="chain-flow">'+nodes+'</div>'
|
|
1646
|
-
+'<div class="cf-legend">spawn \\u203A latest · '+cs.length+' calls · each node is a hash-linked entry</div></div>';
|
|
1647
|
-
}
|
|
1648
|
-
const chainHtml='<div class="panel"><div class="panel-title">Audit chain</div>'
|
|
1649
|
-
+chainFlow(calls)
|
|
1650
|
-
+'<div class="verified-block" style="padding:14px 0">'
|
|
1651
|
-
+'<div class="badge" style="background:'+(!chainKnown?'var(--fg-30)':(chainOk?'var(--verified)':'var(--caught)'))+'">'+(!chainKnown?'\\u00b7':(chainOk?'\\u2713':'\\u2715'))+'</div>'
|
|
1652
|
-
+'<div class="k">'+(!chainKnown?'nothing recorded yet':(chainOk?'hash chain verified':'chain broken'))+'</div>'
|
|
1653
|
-
+'<div class="what">'+(!chainKnown
|
|
1654
|
-
?'No audit entries on this machine. Install the hook to start recording: codehere hook install'
|
|
1655
|
-
:d.chainEntries+' entries · SHA-256 hash-chained · integrity-checked (catches edits after writing)')+'</div>'
|
|
1656
|
-
+'</div></div>';
|
|
1657
|
-
const oHtml='<div class="panel"><div class="panel-title">Observed</div>'
|
|
1658
|
-
+'<div class="outcome-item"><span class="check">'+ms(true)+'</span> '+d.totalCalls+' tool calls scanned by the hook</div>'
|
|
1659
|
-
+(d.blockedCalls>0?'<div class="outcome-item"><span class="cross">'+ms(false)+'</span> '+d.blockedCalls+' blocked before running</div>':'<div class="outcome-item"><span class="check">'+ms(true)+'</span> nothing blocked</div>')
|
|
1660
|
-
+(d.criticalCalls>0?'<div class="outcome-item"><span class="cross">'+ms(false)+'</span> '+d.criticalCalls+' critical finding'+(d.criticalCalls===1?'':'s')+'</div>':'')
|
|
1661
|
-
+(_mixStr?'<div class="outcome-item" style="color:var(--fg-50);font-family:var(--mono);font-size:12px"><span class="pending">\\u2022</span> '+_mixStr+'</div>':'')
|
|
1662
|
-
+'</div>';
|
|
1663
|
-
dv.innerHTML='<div class="detail-header">'
|
|
1664
|
-
+'<span class="back-btn" onclick="hideD()">\\u2190 Back</span>'
|
|
1665
|
-
+'<div class="detail-title-wrap">'
|
|
1666
|
-
+'<div class="detail-kicker">observed · runs in your terminal · read-only</div>'
|
|
1667
|
-
+'<h1 class="detail-title">'+he(d.sessionId)+'</h1>'
|
|
1668
|
-
+'</div></div>'
|
|
1669
|
-
+'<div class="detail-meta"><span class="status-icon done"></span> last activity '+he(ago(calls.length?calls[calls.length-1].timestamp:Date.now()))+' · '+d.totalCalls+' calls</div>'
|
|
1670
|
-
+oHtml+chainHtml
|
|
1671
|
-
+'<div class="panel"><div class="panel-title">Tool calls ('+calls.length+')</div><div class="obs-timeline">'+timeline+'</div></div>'
|
|
1672
|
-
// Honest interaction affordance · an observed session runs in the user's own
|
|
1673
|
-
// terminal; codehere watches it read-only via the hook and cannot inject a
|
|
1674
|
-
// prompt into it. So NO fake composer (that would be a screen-lie). Instead,
|
|
1675
|
-
// an honest note + a real action: fork the context into a managed agent the
|
|
1676
|
-
// user CAN drive from here.
|
|
1677
|
-
+'<div class="obs-readonly-note">'
|
|
1678
|
-
+'<div class="t">This session runs in your terminal. codehere observes it read-only · it can\\'t send a prompt back into your shell. Continue the work in a managed agent codehere CAN drive:</div>'
|
|
1679
|
-
+'<button class="obs-fork-btn" id="ctl-open-btn" onclick="controlManagedAgent(\\''+he(d.sessionId)+'\\')">Continue in a managed agent \\u203A</button>'
|
|
1680
|
-
+'<div id="control-panel"></div>'
|
|
1681
|
-
+'</div>';
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
// Remote-control · open a codehere-managed agent (codex) and DRIVE it from here:
|
|
1685
|
-
// open a control session, then inject() messages into the running turn via the
|
|
1686
|
-
// daemon control endpoints. The observed terminal session stays read-only; this
|
|
1687
|
-
// is a real agent codehere owns end-to-end (turn/steer).
|
|
1688
|
-
let _ctlId=null, _ctlPoll=null;
|
|
1689
|
-
async function controlManagedAgent(sessionId){
|
|
1690
|
-
const btn=document.getElementById('ctl-open-btn');
|
|
1691
|
-
const panel=document.getElementById('control-panel');
|
|
1692
|
-
if(!panel)return;
|
|
1693
|
-
if(btn)btn.disabled=true, btn.textContent='opening managed agent…';
|
|
1694
|
-
try{
|
|
1695
|
-
const r=await fetch('/api/control/open',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent:'codex',prompt:'Continue the work from terminal session '+sessionId})}).then(x=>x.json());
|
|
1696
|
-
if(!r.controlId){ if(btn)btn.disabled=false,btn.textContent='Continue in a managed agent \\u203A'; al('Control open failed: '+(r.error||'unknown')); return; }
|
|
1697
|
-
_ctlId=r.controlId;
|
|
1698
|
-
if(btn)btn.style.display='none';
|
|
1699
|
-
// Honest "Remote Control · live" affordance — rendered ONLY here, after a
|
|
1700
|
-
// real channel opened. data-verify-channel="live" lets /verify assert the
|
|
1701
|
-
// badge can never exist without a live channel (anti-screen-lie invariant).
|
|
1702
|
-
panel.innerHTML='<div class="ctl-badge" data-verify-unit="remote-control" data-verify-channel="live"><span class="lv"></span> Remote Control \\u00b7 live</div>'
|
|
1703
|
-
+'<div class="ctl-head">managed agent \\u00b7 <span class="ag">codex</span> \\u00b7 thread <code>'+r.threadId+'</code> \\u00b7 steer it below</div>'
|
|
1704
|
-
+'<div class="ctl-steer">'
|
|
1705
|
-
+ '<span class="chip" data-steer="continue">\\u203A continue</span>'
|
|
1706
|
-
+ '<span class="chip" data-steer="run the tests">run the tests</span>'
|
|
1707
|
-
+ '<span class="chip" data-steer="show me the diff">show me the diff</span>'
|
|
1708
|
-
+ '<span class="chip stop" data-steer="__stop">\\u2715 stop steering</span>'
|
|
1709
|
-
+'</div>'
|
|
1710
|
-
+'<div class="ctl-feed" id="ctl-feed"></div>'
|
|
1711
|
-
+'<div class="ctl-composer"><input id="ctl-input" placeholder="Steer this live session\\u2026 (goes to the real running agent)"/><button id="ctl-send">Send \\u2191</button></div>'
|
|
1712
|
-
+'<div class="ctl-channel"><span class="lv"></span> <b>live channel attached</b> \\u00b7 steers recorded in audit chain \\u00b7 reachable from any device you\\'re signed in on</div>';
|
|
1713
|
-
document.getElementById('ctl-send').onclick=ctlInject;
|
|
1714
|
-
document.getElementById('ctl-input').addEventListener('keydown',e=>{ if(e.key==='Enter')ctlInject(); });
|
|
1715
|
-
panel.querySelectorAll('.ctl-steer .chip').forEach(function(c){ c.onclick=function(){ var s=c.getAttribute('data-steer'); if(s==='__stop'){ctlStop();return;} var inp=document.getElementById('ctl-input'); if(inp){ inp.value=s; ctlInject(); } }; });
|
|
1716
|
-
ctlPollEvents();
|
|
1717
|
-
_ctlPoll=setInterval(ctlPollEvents,1500);
|
|
1718
|
-
}catch(e){ if(btn)btn.disabled=false,btn.textContent='Continue in a managed agent \\u203A'; al('Network error: '+e.message); }
|
|
1719
|
-
}
|
|
1720
|
-
async function ctlInject(){
|
|
1721
|
-
const inp=document.getElementById('ctl-input'); if(!inp||!_ctlId)return;
|
|
1722
|
-
const msg=inp.value.trim(); if(!msg)return;
|
|
1723
|
-
inp.value='';
|
|
1724
|
-
try{ const r=await fetch('/api/control/'+_ctlId+'/inject',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg})}).then(x=>x.json());
|
|
1725
|
-
if(!r.accepted)al('Inject not accepted');
|
|
1726
|
-
setTimeout(ctlPollEvents,300);
|
|
1727
|
-
}catch(e){ al('Inject failed: '+e.message); }
|
|
1728
|
-
}
|
|
1729
|
-
// Stop steering from here. Honest scope: detaches the control channel (stops
|
|
1730
|
-
// polling + closes the panel). It does NOT claim to kill the remote process —
|
|
1731
|
-
// that would be an unverifiable overclaim. The badge disappears with the panel.
|
|
1732
|
-
function ctlStop(){
|
|
1733
|
-
if(_ctlPoll){ clearInterval(_ctlPoll); _ctlPoll=null; }
|
|
1734
|
-
_ctlId=null;
|
|
1735
|
-
const panel=document.getElementById('control-panel');
|
|
1736
|
-
if(panel)panel.innerHTML='<div class="ctl-evt dim">stopped steering \\u00b7 channel detached \\u00b7 the agent keeps running in its own session</div>';
|
|
1737
|
-
}
|
|
1738
|
-
async function ctlPollEvents(){
|
|
1739
|
-
if(!_ctlId)return;
|
|
1740
|
-
try{ const r=await fetch('/api/control/'+_ctlId+'/events').then(x=>x.json());
|
|
1741
|
-
const feed=document.getElementById('ctl-feed'); if(!feed)return;
|
|
1742
|
-
function he(s){ return String(s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
1743
|
-
feed.innerHTML=(r.events||[]).map(e=>{ const it=(e.params||{}).item||{}; const t=it.text||it.command||it.type||e.method; return '<div class="ctl-evt">'+he(t)+'</div>'; }).join('')||'<div class="ctl-evt dim">agent working…</div>';
|
|
1744
|
-
}catch{}
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
// ── Phase model · the demo v12 lifecycle spine ────────────────────────────
|
|
1748
|
-
// A thread reads as a phased narrative (research -> build -> ship -> audit -> live),
|
|
1749
|
-
// not a flat step list. Steps don't carry phase labels, so classify by the
|
|
1750
|
-
// dominant signal in the label/output. Order matters: test later phases first
|
|
1751
|
-
// so a "run tests" step lands in ship, not research.
|
|
1752
|
-
// Phase classifier · PHASE_ORDER / PHASE_META / stepPhase / classifySteps are
|
|
1753
|
-
// injected from the unit-tested module (thread-phase.ts) — one source of truth.
|
|
1754
|
-
// The \\bcode\\b bug that mislabeled read steps as BUILD is why this is no longer
|
|
1755
|
-
// inline-untested.
|
|
1756
|
-
${PHASE_CLASSIFIER_SRC}
|
|
1757
|
-
// Chapter-card divider between phases · demo v12 .chapter-card pattern.
|
|
1758
|
-
function chapterCard(prevPhase,nextPhase){
|
|
1759
|
-
var prev=PHASE_META[prevPhase], next=PHASE_META[nextPhase];
|
|
1760
|
-
return '<div class="chapter-divider">'
|
|
1761
|
-
+(prev?'<span class="ch-prev">\\u2713 '+prev.done+'</span>':'<span class="ch-prev"></span>')
|
|
1762
|
-
+'<span class="ch-line"></span>'
|
|
1763
|
-
+'<span class="ch-next">Phase '+next.n+' of 5 \\u00B7 <b>'+next.label+'</b></span>'
|
|
1764
|
-
+'</div>';
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
${ARTIFACTS}
|
|
1768
|
-
|
|
1769
|
-
${CONVERSATION_TURN}
|
|
1770
|
-
function __rememberConvOpen(el){
|
|
1771
|
-
try{
|
|
1772
|
-
var sid=el&&el.getAttribute('data-sid');
|
|
1773
|
-
if(sid) _convOpenByTask[sid]=!!el.open;
|
|
1774
|
-
}catch(e){}
|
|
1775
|
-
}
|
|
1776
|
-
function __rememberStepOpen(el){
|
|
1777
|
-
try{
|
|
1778
|
-
var k=el&&el.getAttribute('data-stepk');
|
|
1779
|
-
if(k) _stepOpen[decodeURIComponent(k)]=!!el.open;
|
|
1780
|
-
}catch(e){}
|
|
1781
|
-
}
|
|
1782
|
-
function __rememberReceiptOpen(el){
|
|
1783
|
-
try{
|
|
1784
|
-
var k=el&&el.getAttribute('data-rsid');
|
|
1785
|
-
if(k) _receiptOpen[decodeURIComponent(k)]=!!el.open;
|
|
1786
|
-
}catch(e){}
|
|
1787
|
-
}
|
|
1788
|
-
// Same render-wipe guard for the glance turn's collapsed full report — without
|
|
1789
|
-
// it the poll morph strips the open attribute and the report snaps shut mid-read.
|
|
1790
|
-
function __rememberGlOpen(el){
|
|
1791
|
-
try{
|
|
1792
|
-
var k=el&&el.getAttribute('data-glsid');
|
|
1793
|
-
if(k) _glMoreOpen[decodeURIComponent(k)]=!!el.open;
|
|
1794
|
-
}catch(e){}
|
|
1795
|
-
}
|
|
1796
|
-
// Unified timeline (task #51) · the conversation IS the spine, and the tool
|
|
1797
|
-
// activity is woven INTO it as inline, collapsed-by-default events (native
|
|
1798
|
-
// <details>, no JS), grouped by EMERGENT sections (segmentSteps — dynamic, not
|
|
1799
|
-
// a fixed pipeline). Turn timestamps are display strings and step times are
|
|
1800
|
-
// epoch (not clock-comparable), so we place the activity block right where
|
|
1801
|
-
// execution starts: after the first non-user turn (routing / first agent),
|
|
1802
|
-
// before that agent reports back. Reads correct for seeded dialogues and for
|
|
1803
|
-
// real single-agent runs (where turns are sparse and the steps dominate).
|
|
1804
|
-
// Internal tool ids leak into step labels ("mcp__plugin_context-mode_context-
|
|
1805
|
-
// mode__ctx_batch_execute", "Claude requested permissions to use mcp__...").
|
|
1806
|
-
// Humanize the SUMMARY line only — the expanded output keeps the raw evidence,
|
|
1807
|
-
// so the record stays honest while the conversation reads like a person did it.
|
|
1808
|
-
function humanizeStep(s){
|
|
1809
|
-
var x=String(s||'');
|
|
1810
|
-
x=x.replace(/mcp__([A-Za-z0-9_-]+)__([A-Za-z0-9_-]+)/g,function(_,srv,tool){
|
|
1811
|
-
srv=srv.replace(/^plugin[_-]/,'');
|
|
1812
|
-
var m=srv.match(/^(.+?)[_-]\\1$/); if(m) srv=m[1];
|
|
1813
|
-
return tool.replace(/_/g,' ')+' \\u00b7 '+srv;
|
|
1814
|
-
});
|
|
1815
|
-
x=x.replace(/Claude requested permissions? to use\\s+[^,.]+[^\\u00b7]*/i,'needed a permission that was not granted');
|
|
1816
|
-
// Bare internal tool names ("ToolSearch") read like code — split the camelCase.
|
|
1817
|
-
if(/^[A-Z][a-z]+(?:[A-Z][a-z]+)+$/.test(x)) x=x.replace(/([a-z])([A-Z])/g,'$1 $2').toLowerCase();
|
|
1818
|
-
return x;
|
|
1819
|
-
}
|
|
1820
|
-
function renderTimeline(t, wrapCtxUrls){
|
|
1821
|
-
var turns=Array.isArray(t.turns)?t.turns:[];
|
|
1822
|
-
var steps=(t.toolTrace||t.steps||[]);
|
|
1823
|
-
if(!turns.length && !steps.length) return '';
|
|
1824
|
-
function evtRow(z){
|
|
1825
|
-
var raw=String(z.label||'');
|
|
1826
|
-
// Write-pass · the one moment the human MUST act. Keep the rich primitive
|
|
1827
|
-
// (Approve+send opens the write-pass modal) rather than collapsing it to a
|
|
1828
|
-
// one-liner — collapsing would orphan the approval trigger.
|
|
1829
|
-
if(/^\\s*w-?pass\\b|^\\s*write-pass\\b/i.test(raw)){
|
|
1830
|
-
var parts=raw.split(/\\s*·\\s*/);
|
|
1831
|
-
var wtitle=(parts[1]||raw||'write awaits').replace(/</g,'<');
|
|
1832
|
-
var wtail=(parts.slice(2).join(' · ')||(z.state==='active'?'awaiting approval':'')).replace(/</g,'<');
|
|
1833
|
-
var pHtml=(z.output?String(z.output):'').replace(/</g,'<')
|
|
1834
|
-
.replace(/(in-progress|pending|todo|backlog)/g,'<span class="from">$1</span>')
|
|
1835
|
-
.replace(/(\\u2192|->)/g,'<span class="arrow">$1</span>')
|
|
1836
|
-
.replace(/(done|approved|merged|shipped|sent)/g,'<span class="val">$1</span>');
|
|
1837
|
-
return '<div class="write-pass" style="margin-left:25px">'
|
|
1838
|
-
+'<div class="head"><span>write-pass</span><span class="tail" data-user-content style="color:var(--fg-50);letter-spacing:normal;text-transform:none">'+wtail+'</span></div>'
|
|
1839
|
-
+'<h4 data-user-content>'+wtitle+'</h4>'
|
|
1840
|
-
+(pHtml?'<div class="payload" data-user-content>'+pHtml+'</div>':'')
|
|
1841
|
-
+'<div class="actions"><button class="primary" onclick="if(window.openWpModal)openWpModal()">Approve + send</button></div>'
|
|
1842
|
-
+'</div>';
|
|
1843
|
-
}
|
|
1844
|
-
var lbl=wrapCtxUrls(humanizeStep(raw).replace(/</g,'<'));
|
|
1845
|
-
var cls='allow', txt='L0';
|
|
1846
|
-
if(z.state==='failed'){ cls='block'; txt=/block|denied|refused|permission/i.test(String(z.output||z.errorText||z.label||''))?'BLOCKED':'FAILED'; }
|
|
1847
|
-
else if(z.state==='active'){ cls='clean'; txt='RUN'; }
|
|
1848
|
-
// Active step · the agent is working on this RIGHT NOW. Render the live
|
|
1849
|
-
// .thinking dots + streaming .repl-beat instead of a collapsed details row,
|
|
1850
|
-
// so the reasoning primitive renders on a real active step (not only in the
|
|
1851
|
-
// dead toolCardsHtml path). SSE refetch swaps it for a completed row when
|
|
1852
|
-
// the step finishes.
|
|
1853
|
-
if(z.state==='active'){
|
|
1854
|
-
var beat=z.output?ansiToHtml(String(z.output).slice(0,300)):lbl;
|
|
1855
|
-
return '<div class="tl-evt '+cls+' tl-active">'
|
|
1856
|
-
+'<div class="tl-lbl" data-user-content>'+lbl+'</div>'
|
|
1857
|
-
+'<div class="thinking"><span class="dots"><span></span><span></span><span></span></span>working</div>'
|
|
1858
|
-
+'<div class="repl-beat" data-user-content>'+beat+'<span class="cursor"></span></div>'
|
|
1859
|
-
+'</div>';
|
|
1860
|
-
}
|
|
1861
|
-
var dur=(z.completedAt&&z.startedAt)?'<span class="tl-dur">'+(Math.round((z.completedAt-z.startedAt)/100)/10)+'s</span>':'';
|
|
1862
|
-
// Lift the step's OUTCOME into the summary line so the conversation reads as
|
|
1863
|
-
// what HAPPENED ("Edit x · +59 lines · change applied"), not a hollow label.
|
|
1864
|
-
// The meaningless "L0" badge is dropped for normal completed steps; the badge
|
|
1865
|
-
// stays only for failed/blocked states, which are worth flagging.
|
|
1866
|
-
var oc=String(z.output==null?'':z.output).trim();
|
|
1867
|
-
var rawFirst=oc.split(/\\r?\\n/)[0]||'';
|
|
1868
|
-
var ocFirst=humanizeStep(rawFirst);
|
|
1869
|
-
var ocHtml=ocFirst?'<span class="tl-outcome" data-user-content>'+wrapCtxUrls(ocFirst.slice(0,72).replace(/</g,'<'))+'</span>':'';
|
|
1870
|
-
// Compare raw-to-raw: humanizeStep shortens the first line, and comparing
|
|
1871
|
-
// the raw total against the shortened line faked a "more to expand" state.
|
|
1872
|
-
var more=(oc.length>rawFirst.length+2)||(rawFirst.length>72);
|
|
1873
|
-
var badge=(cls!=='allow')?'<span class="tl-badge '+cls+'">'+txt+'</span>':'';
|
|
1874
|
-
var out=more?renderStepOutput(z):'';
|
|
1875
|
-
// The raw label stays one hover away (title attr) — humanize rewrites the
|
|
1876
|
-
// summary, it must not erase the original evidence from the DOM.
|
|
1877
|
-
var rawTitle=(humanizeStep(raw)!==raw)?' title="'+raw.replace(/"/g,'"').replace(/</g,'<')+'"':'';
|
|
1878
|
-
// Persist expansion across the poll re-render (systemic render-wipe sweep):
|
|
1879
|
-
// key on task + step signature so an expanded step stays open. Only
|
|
1880
|
-
// expandable rows (more) get the key + ontoggle; flat rows have nothing.
|
|
1881
|
-
var _sk=(t.sessionId||'')+'|'+(z.startedAt||'')+'|'+raw;
|
|
1882
|
-
var _stepIsOpen=more && !!_stepOpen[_sk];
|
|
1883
|
-
var _stepAttrs=more?(' ontoggle="__rememberStepOpen(this)" data-stepk="'+encodeURIComponent(_sk)+'"'+(_stepIsOpen?' open':'')):'';
|
|
1884
|
-
return '<details class="tl-evt '+cls+(more?'':' tl-flat')+'"'+_stepAttrs+'><summary><span class="tl-caret">\\u25B8</span><span class="tl-lbl" data-user-content'+rawTitle+'>'+lbl+'</span>'+ocHtml+dur+badge+'</summary>'+out+'</details>';
|
|
1885
|
-
}
|
|
1886
|
-
// The agent's own words (kind:'say') — rendered as the agent speaking, with
|
|
1887
|
-
// its avatar, distinct from the collapsed tool events.
|
|
1888
|
-
function thinkRow(st){
|
|
1889
|
-
// The agent's interleaved reasoning — muted, with a "thinking" marker, full
|
|
1890
|
-
// markdown (so code in reasoning renders properly), no trim.
|
|
1891
|
-
return '<div class="tl-think" data-verify-unit="think"><span class="tl-think-tag">\\u29B8 thinking</span><div class="tl-think-text report-md" data-user-content>'+renderReportMd(st.label)+'</div></div>';
|
|
1892
|
-
}
|
|
1893
|
-
function askRow(st){
|
|
1894
|
-
// The agent is asking the user. Renders the question + options as a real
|
|
1895
|
-
// card; clicking an option (single-select) sends the answer, which continues
|
|
1896
|
-
// the SAME session via /api/task/:id/message (--resume). Already answered ->
|
|
1897
|
-
// shows the chosen answer, locked.
|
|
1898
|
-
var q=st.question||{questions:[]};
|
|
1899
|
-
var answered=st.answered||_answeredAsks[(t.sessionId||'')+'|'+(q.toolUseId||'')];
|
|
1900
|
-
var anyMulti=(q.questions||[]).some(function(x){return x.multiSelect;});
|
|
1901
|
-
var qs=(q.questions||[]).map(function(qq){
|
|
1902
|
-
var opts=(qq.options||[]).map(function(o){
|
|
1903
|
-
var picked=answered && (answered.indexOf?answered.indexOf(o.label)>=0:false);
|
|
1904
|
-
return '<div class="ask-opt'+(picked?' picked':'')+'" data-ask-label="'+convHe(o.label)+'"'+(answered?'':' onclick="chAnswerOpt(this)"')+'><span class="ask-opt-label" data-user-content>'+convHe(o.label)+'</span>'+(o.description?'<span class="ask-opt-desc" data-user-content>'+convHe(o.description)+'</span>':'')+'</div>';
|
|
1905
|
-
}).join('');
|
|
1906
|
-
return '<div class="ask-q">'+(qq.header?'<div class="ask-q-head" data-user-content>'+convHe(qq.header)+'</div>':'')+'<div class="ask-q-text" data-user-content>'+convHe(qq.question)+'</div><div class="ask-opts">'+opts+'</div></div>';
|
|
1907
|
-
}).join('');
|
|
1908
|
-
var foot = answered ? '<div class="ask-answered">\\u2713 answered: <span data-user-content>'+convHe((answered.join?answered.join(', '):String(answered)))+'</span></div>'
|
|
1909
|
-
: (anyMulti ? '<div class="ask-foot"><button class="ask-send" onclick="chAnswerSend(this)">Send answer</button></div>' : '<div class="ask-hint">click an option to answer \\u00b7 the agent continues from here</div>');
|
|
1910
|
-
return '<div class="tl-ask'+(answered?' answered':'')+'" data-verify-unit="ask"'+(anyMulti?' data-ask-multi="1"':'')+(q.toolUseId?' data-ask-tuid="'+convHe(q.toolUseId)+'"':'')+'><div class="tl-ask-head">'+ab(t.agent,'sm')+'<span class="tl-ask-title">The agent is asking you</span></div>'+qs+foot+'</div>';
|
|
1911
|
-
}
|
|
1912
|
-
function sayRow(st){
|
|
1913
|
-
return '<div class="tl-say" data-verify-unit="say" data-verify-textlen="'+String((st.label||'').trim().length)+'">'+ab(t.agent,'sm')+'<div class="tl-say-text report-md" data-user-content>'+renderReportMd(st.label)+'</div></div>';
|
|
1914
|
-
}
|
|
1915
|
-
// Single in-order pass so the agent's words + its tool actions interleave the
|
|
1916
|
-
// way they actually happened ("explains -> acts"). Dynamic ⧁ section dividers
|
|
1917
|
-
// appear when a RUN of tool actions shifts activity (segmentSteps logic,
|
|
1918
|
-
// inline); a 'say' breaks the run so the next tool reopens a section.
|
|
1919
|
-
function activityBlock(){
|
|
1920
|
-
if(!steps.length) return '';
|
|
1921
|
-
// Inline audit-line · the first beat of the timeline makes the local audit
|
|
1922
|
-
// chain VISIBLE in the flow (not just in the footer). ONLY state-derived
|
|
1923
|
-
// claims: the pre-prompt scan genuinely runs on every spawn
|
|
1924
|
-
// (session-manager scanPromptBeforeSpawn, all adapters), but its VERDICT
|
|
1925
|
-
// isn't plumbed to the task object — so say it ran, never that it was
|
|
1926
|
-
// "clean". "L0 hooks loaded" / "writes require approval" were cut by the
|
|
1927
|
-
// 2026-06-07 review: claude-only / opt-in respectively — false as blanket
|
|
1928
|
-
// claims for the other vendors and the default posture.
|
|
1929
|
-
var _aAgent=(agentLabel(t.agent)||'agent').toLowerCase();
|
|
1930
|
-
var _aFiles=((t.artifacts&&t.artifacts.files)||[]).length;
|
|
1931
|
-
var html='<div class="audit-line allow"><span class="seq">#0001</span>'
|
|
1932
|
-
+'pre-prompt scan ran \\u00b7 routed to <b>'+_aAgent+'</b>'
|
|
1933
|
-
+(_aFiles?' \\u00b7 context: <b>'+_aFiles+' file'+(_aFiles===1?'':'s')+'</b>':'')
|
|
1934
|
-
+'</div>';
|
|
1935
|
-
var curPhase=null, openActs=false;
|
|
1936
|
-
var close=function(){ if(openActs){ html+='</div>'; openActs=false; } };
|
|
1937
|
-
for(var i=0;i<steps.length;i++){
|
|
1938
|
-
var st=steps[i];
|
|
1939
|
-
if(st.kind==='say'){ close(); curPhase=null;
|
|
1940
|
-
// Don't repeat the distilled report here — it's already the top summary
|
|
1941
|
-
// bubble (renderD summHtml). Showing the exact same words twice is the
|
|
1942
|
-
// duplication the summary-bubble comment warns against.
|
|
1943
|
-
var _rep=(t.finalReport&&t.finalReport.summary)||'';
|
|
1944
|
-
if(_rep&&String(st.label||'').trim()===String(_rep).trim()){ continue; }
|
|
1945
|
-
html+=sayRow(st); continue; }
|
|
1946
|
-
if(st.kind==='think'){ close(); curPhase=null; html+=thinkRow(st); continue; }
|
|
1947
|
-
if(st.kind==='ask'){ close(); curPhase=null; html+=askRow(st); continue; }
|
|
1948
|
-
var ph=(typeof stepPhase==='function'?stepPhase(st):null)||curPhase||'research';
|
|
1949
|
-
// S12 · the existing phase dividers upgraded to the NUMBERED chapter
|
|
1950
|
-
// style (01 RESEARCH · 02 BUILD · …) — same classifier, same .tl-div
|
|
1951
|
-
// class (the F-THREAD-PHASED probe keeps matching), richer staging.
|
|
1952
|
-
if(ph!==curPhase){
|
|
1953
|
-
close();
|
|
1954
|
-
var _pm=(typeof PHASE_META!=='undefined'&&PHASE_META[ph])||null;
|
|
1955
|
-
var _plabel=(_pm&&_pm.label)||ph;
|
|
1956
|
-
// No canonical phase number prefix: steps render chronologically, so a
|
|
1957
|
-
// phase index (research=01…ship=03) reads out of order (03 · 01 · 02 · 03)
|
|
1958
|
-
// and looks broken. The phase word alone marks what kind of work it is.
|
|
1959
|
-
html+='<div class="tl-div"><span class="tl-verb">'+convHe(_plabel)+'</span><span class="tl-ln"></span></div>';
|
|
1960
|
-
curPhase=ph;
|
|
1961
|
-
}
|
|
1962
|
-
if(!openActs){ html+='<div class="tl-acts">'; openActs=true; }
|
|
1963
|
-
html+=evtRow(st);
|
|
1964
|
-
}
|
|
1965
|
-
close();
|
|
1966
|
-
return html;
|
|
1967
|
-
}
|
|
1968
|
-
// split point: just after the first non-user (routing / agent) turn
|
|
1969
|
-
var splitAt=0;
|
|
1970
|
-
for(var i=0;i<turns.length;i++){ var sp=turns[i].speaker||turns[i].role; if(isAgentSpeaker(sp)){ splitAt=i+1; break; } }
|
|
1971
|
-
if(!turns.length) splitAt=0;
|
|
1972
|
-
var head=turns.slice(0,splitAt).map(renderTurn).join('');
|
|
1973
|
-
var tail=turns.slice(splitAt).map(renderTurn).join('');
|
|
1974
|
-
// DESIGN.md §4.3 — collapse the step wall into a one-line receipt. Evidence
|
|
1975
|
-
// one click away, never always-on. The room meta line already carries the
|
|
1976
|
-
// room-level summary (duration · cost · steps · artifacts), so the receipt
|
|
1977
|
-
// summary labels what expands (the trace) without re-stating cost/files.
|
|
1978
|
-
function receiptWrap(inner){
|
|
1979
|
-
if(!steps.length) return inner; // nothing to collapse (routing-only turn)
|
|
1980
|
-
var _rsid=(t.sessionId||'');
|
|
1981
|
-
var _open=!!_receiptOpen[_rsid];
|
|
1982
|
-
var _tests=(t.artifacts&&t.artifacts.tests)||null;
|
|
1983
|
-
var _tp=_tests?(_tests.passed||0):0, _tf=_tests?(_tests.failed||0):0;
|
|
1984
|
-
var parts=[steps.length+' step'+(steps.length===1?'':'s')];
|
|
1985
|
-
if(_tests&&(_tp+_tf)>0) parts.push(_tp+'/'+(_tp+_tf)+' tests');
|
|
1986
|
-
var summary=parts.join(' \\u00b7 ');
|
|
1987
|
-
return '<details class="tl-receipt" data-rsid="'+convHe(_rsid)+'" ontoggle="__rememberReceiptOpen(this)"'+(_open?' open':'')
|
|
1988
|
-
+'><summary class="rcpt-row"><span class="tl-caret">\\u25B8</span><span class="rcpt-sum">'+summary+'</span><span class="rcpt-view">view receipt \\u203A</span></summary>'
|
|
1989
|
-
+'<div class="rcpt-body">'+inner+'</div></details>';
|
|
1990
|
-
}
|
|
1991
|
-
return '<div class="panel tl-panel"><div class="panel-title">Conversation</div>'+head+receiptWrap(activityBlock())+tail+'</div>';
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
// Recursive DOM morph: reconcile an old node toward a new one, descending so
|
|
1995
|
-
// only the leaves that actually changed get touched — a new timeline step
|
|
1996
|
-
// appends its row, every existing row keeps its exact DOM node (no flicker, no
|
|
1997
|
-
// restarted animation). isEqualNode short-circuits any unchanged subtree.
|
|
1998
|
-
function morphNode(o, n){
|
|
1999
|
-
if(o.nodeType!==n.nodeType){ o.replaceWith(n); return; }
|
|
2000
|
-
if(o.nodeType===3){ if(o.nodeValue!==n.nodeValue) o.nodeValue=n.nodeValue; return; }
|
|
2001
|
-
if(o.nodeType!==1){ if(!o.isEqualNode(n)) o.replaceWith(n); return; }
|
|
2002
|
-
if(o.tagName!==n.tagName){ o.replaceWith(n); return; }
|
|
2003
|
-
if(o.isEqualNode(n)) return;
|
|
2004
|
-
// sync attributes
|
|
2005
|
-
var oa=o.attributes, i;
|
|
2006
|
-
for(i=oa.length-1;i>=0;i--){ if(!n.hasAttribute(oa[i].name)) o.removeAttribute(oa[i].name); }
|
|
2007
|
-
var na=n.attributes;
|
|
2008
|
-
for(i=0;i<na.length;i++){ if(o.getAttribute(na[i].name)!==na[i].value) o.setAttribute(na[i].name, na[i].value); }
|
|
2009
|
-
// reconcile child nodes (text + elements) by index — content here is
|
|
2010
|
-
// append-mostly (streaming steps), so positional matching holds
|
|
2011
|
-
var nNodes=Array.prototype.slice.call(n.childNodes);
|
|
2012
|
-
for(i=0;i<nNodes.length;i++){
|
|
2013
|
-
var oNode=o.childNodes[i];
|
|
2014
|
-
if(!oNode){ o.appendChild(nNodes[i]); continue; }
|
|
2015
|
-
morphNode(oNode, nNodes[i]);
|
|
2016
|
-
}
|
|
2017
|
-
while(o.childNodes.length>nNodes.length){ o.removeChild(o.lastChild); }
|
|
2018
|
-
}
|
|
2019
|
-
// Layer 3 · answer the agent's question. Clicking an option (single-select)
|
|
2020
|
-
// sends immediately; multi-select toggles + a Send button. The answer POSTs to
|
|
2021
|
-
// the existing /api/task/:id/message route -> appendMessage continues the SAME
|
|
2022
|
-
// claude session via --resume, and its steps merge back into the timeline.
|
|
2023
|
-
function chAnswerOpt(el){
|
|
2024
|
-
var card=el.closest('.tl-ask'); if(!card||card.classList.contains('answered'))return;
|
|
2025
|
-
if(card.getAttribute('data-ask-multi')==='1'){ el.classList.toggle('picked'); return; }
|
|
2026
|
-
chSendAnswer(card,[el.getAttribute('data-ask-label')]);
|
|
2027
|
-
}
|
|
2028
|
-
function chAnswerSend(btn){
|
|
2029
|
-
var card=btn.closest('.tl-ask'); if(!card)return;
|
|
2030
|
-
var picked=Array.prototype.slice.call(card.querySelectorAll('.ask-opt.picked')).map(function(o){return o.getAttribute('data-ask-label');});
|
|
2031
|
-
if(!picked.length)return;
|
|
2032
|
-
chSendAnswer(card,picked);
|
|
2033
|
-
}
|
|
2034
|
-
function chSendAnswer(card,labels){
|
|
2035
|
-
var sid=_detail; if(!sid)return;
|
|
2036
|
-
_answeredAsks[sid+'|'+(card.getAttribute('data-ask-tuid')||'')]=labels;
|
|
2037
|
-
var qtext=(card.querySelector('.ask-q-text')||{}).textContent||'';
|
|
2038
|
-
var answer='You asked: "'+qtext+'"\\nMy answer: '+labels.join(', ');
|
|
2039
|
-
card.classList.add('answered');
|
|
2040
|
-
card.querySelectorAll('.ask-opt').forEach(function(o){ o.removeAttribute('onclick'); if(labels.indexOf(o.getAttribute('data-ask-label'))>=0)o.classList.add('picked'); });
|
|
2041
|
-
var foot=card.querySelector('.ask-foot, .ask-hint');
|
|
2042
|
-
if(foot)foot.outerHTML='<div class="ask-answered">\\u2713 sending your answer\\u2026</div>';
|
|
2043
|
-
fetch('/api/task/'+encodeURIComponent(sid)+'/message',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:answer})})
|
|
2044
|
-
.then(function(r){return r.json();})
|
|
2045
|
-
.then(function(){ if(typeof fetchAll==='function'){ fetchAll(); } else if(_detail===sid){ showD(sid); } })
|
|
2046
|
-
.catch(function(){ var a=card.querySelector('.ask-answered'); if(a)a.textContent='could not send \\u00b7 try again'; });
|
|
2047
|
-
}
|
|
2048
|
-
// SAME-session live re-render -> morph in place (fine-grained, incl. inside the
|
|
2049
|
-
// timeline). First render / a different session -> full innerHTML write.
|
|
2050
|
-
// (Ega: append-only render, reconcile inside the timeline)
|
|
2051
|
-
function writeDetail(t, html){
|
|
2052
|
-
var dv=document.getElementById('detail-view');
|
|
2053
|
-
var sid=String((t&&t.sessionId)||'');
|
|
2054
|
-
var same=dv.getAttribute('data-sid')===sid && dv.childNodes.length>0;
|
|
2055
|
-
dv.setAttribute('data-sid', sid);
|
|
2056
|
-
if(!same){ dv.innerHTML=html; return; }
|
|
2057
|
-
var tmp=document.createElement('div'); tmp.innerHTML=html;
|
|
2058
|
-
var nw=Array.prototype.slice.call(tmp.childNodes);
|
|
2059
|
-
for(var i=0;i<nw.length;i++){
|
|
2060
|
-
var o=dv.childNodes[i];
|
|
2061
|
-
if(!o){ dv.appendChild(nw[i]); continue; }
|
|
2062
|
-
morphNode(o, nw[i]);
|
|
2063
|
-
}
|
|
2064
|
-
while(dv.childNodes.length>nw.length){ dv.removeChild(dv.lastChild); }
|
|
2065
|
-
}
|
|
2066
|
-
// The cross-vendor receipt panel — the proof a single-vendor tool can't make:
|
|
2067
|
-
// a DIFFERENT vendor reviewed an earlier writer in this room, the chain proves
|
|
2068
|
-
// both, here's the cost. Green ✓ only when the server marked it proven (a clean
|
|
2069
|
-
// read-only cross-vendor review whose chain verified); otherwise a neutral dot
|
|
2070
|
-
// + the honest caveats. codehere does NOT judge the findings here.
|
|
2071
|
-
function crossVendorPanel(cv){
|
|
2072
|
-
function esc(x){ return String(x==null?'':x).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
2073
|
-
var proven=!!cv.proven;
|
|
2074
|
-
var icon=proven?'\\u2713':'\\u00B7';
|
|
2075
|
-
var bg=proven?'var(--verified)':'var(--fg-30)';
|
|
2076
|
-
var k=proven?(esc(cv.reviewer.agent)+' reviewed '+esc(cv.writer.agent)+'\\u2019s work \\u00b7 a different vendor')
|
|
2077
|
-
:'cross-vendor review \\u00b7 not a clean proof yet';
|
|
2078
|
-
var cav='';
|
|
2079
|
-
if(cv.caveats&&cv.caveats.length){ for(var i=0;i<cv.caveats.length;i++){ cav+='<div class="outcome-item"><span class="pending">\\u2022</span> '+esc(cv.caveats[i])+'</div>'; } }
|
|
2080
|
-
// Corroboration by a different vendor reads as the brand-gradient SEAL (v2 mockup):
|
|
2081
|
-
// the one place the gradient earns its keep, the signature trust object.
|
|
2082
|
-
if(proven){
|
|
2083
|
-
return '<div class="seal-wrap" data-cross-vendor="proven"><div class="seal"><div class="seal-inner">'
|
|
2084
|
-
+'<div class="seal-check">\\u2713</div>'
|
|
2085
|
-
+'<div class="seal-txt">'
|
|
2086
|
-
+'<div class="seal-l1">Corroborated by <b>'+esc(cv.reviewer.agent)+'</b> \\u00b7 a different vendor</div>'
|
|
2087
|
-
+'<div class="seal-l2" data-user-content>'+esc(cv.summary)+'</div>'
|
|
2088
|
-
+'</div></div>'+cav+'</div></div>';
|
|
2089
|
-
}
|
|
2090
|
-
return '<div class="panel" data-cross-vendor="caveated">'
|
|
2091
|
-
+'<div class="panel-title">Cross-vendor review</div>'
|
|
2092
|
-
+'<div class="verified-block" style="padding:14px 0">'
|
|
2093
|
-
+'<div class="badge" style="background:'+bg+'">'+icon+'</div>'
|
|
2094
|
-
+'<div class="k">'+k+'</div>'
|
|
2095
|
-
+'<div class="what">'+esc(cv.summary)+'</div>'
|
|
2096
|
-
+'</div>'+cav+'</div>';
|
|
2097
|
-
}
|
|
2098
|
-
// Lazy-load the receipt for one session, cache it, and re-render the open detail
|
|
2099
|
-
// so the panel joins renderD's output (morph-stable). Sets null first so renderD
|
|
2100
|
-
// won't re-fetch; overwrites with the verdict only if the server returns one.
|
|
2101
|
-
async function loadCrossVendor(id){
|
|
2102
|
-
if(!id || Object.prototype.hasOwnProperty.call(_crossVendorByTask,id)) return;
|
|
2103
|
-
_crossVendorByTask[id]=null;
|
|
2104
|
-
try{
|
|
2105
|
-
const r=await fetch('/api/task/'+encodeURIComponent(id)+'/cross-vendor');
|
|
2106
|
-
if(!r.ok) return;
|
|
2107
|
-
const j=await r.json();
|
|
2108
|
-
const cv=j&&j.crossVendor;
|
|
2109
|
-
if(!cv) return; // stays null — not a cross-vendor review
|
|
2110
|
-
_crossVendorByTask[id]=cv;
|
|
2111
|
-
if(_detail===id){ const tt=findS(id); if(tt) renderD(tt); }
|
|
2112
|
-
}catch(e){}
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
// REQUEST_METHOD §2 — the acceptance contract, rendered at the top of the room:
|
|
2116
|
-
// what "done" means, agreed up front, each criterion met (✓) / unmet (✕) /
|
|
2117
|
-
// pending (○). This is the keystone that turns the verdict from post-hoc vibe
|
|
2118
|
-
// into "did it meet the criteria we set before it ran". Empty => nothing renders.
|
|
2119
|
-
function contractBlockHtml(t){
|
|
2120
|
-
var cs=(t&&t.acceptanceCriteria)||[];
|
|
2121
|
-
if(!Array.isArray(cs)||!cs.length) return '';
|
|
2122
|
-
var rows=cs.map(function(c){
|
|
2123
|
-
var met=(c&&c.met);
|
|
2124
|
-
var g=met===true?'\\u2713':(met===false?'\\u2715':'\\u25CB');
|
|
2125
|
-
var cls=met===true?'met':(met===false?'unmet':'pending');
|
|
2126
|
-
return '<div class="ac-row '+cls+'"><span class="ac-g">'+g+'</span><span class="ac-t" data-user-content>'+convHe(String((c&&c.text)||''))+'</span></div>';
|
|
2127
|
-
}).join('');
|
|
2128
|
-
var n=cs.length, done=cs.filter(function(c){return c&&c.met===true;}).length;
|
|
2129
|
-
return '<div class="contract"><div class="ac-head">Done means<span class="ac-count">'+done+'/'+n+'</span></div>'+rows+'</div>';
|
|
2130
|
-
}
|
|
2131
|
-
function renderD(t) {
|
|
2132
|
-
const s=st(t.status), steps=(t.steps||[]), files=t.artifacts?.files||[], d=taskDur(t);
|
|
2133
|
-
const co=t.cost?t.cost.spent:null;
|
|
2134
|
-
const nStep=steps.length;
|
|
2135
|
-
|
|
2136
|
-
// R8/R9/R10/R11 — Final Report outcome only ✓ when prose summary actually exists.
|
|
2137
|
-
// Empty finalReport object or null/empty summary => honest "unavailable" outcome,
|
|
2138
|
-
// not a green check. Composer must offer a re-spawn affordance for these cases.
|
|
2139
|
-
const hasReportText = !!(t.finalReport && typeof t.finalReport.summary === 'string' && t.finalReport.summary.trim());
|
|
2140
|
-
// Outer-scope mirror of the conversation's default-open state (the real
|
|
2141
|
-
// _convOpen is block-scoped later). When the conversation shows the steps by
|
|
2142
|
-
// default, the separate collapsed "N steps" trace is redundant — hide it.
|
|
2143
|
-
const _convShown = steps.length>0 || !hasReportText;
|
|
2144
|
-
// §G R8 — green ✓ "Final Report" ONLY for a verified live-agent summary
|
|
2145
|
-
// (source === 'agent-summary'). Every other source — deterministic fallback,
|
|
2146
|
-
// error-recovery, mock synthesis — renders an honest ⚠, never a fake ✓.
|
|
2147
|
-
// POSITIVE allowlist (not deny-one) so a new/unknown source can't sneak a ✓.
|
|
2148
|
-
const reportSource = (t.finalReport && t.finalReport.source) || '';
|
|
2149
|
-
const isVerifiedAgentReport = reportSource === 'agent-summary';
|
|
2150
|
-
const isTerminal = ['completed','done','killed','failed','error','crashed'].includes(t.status);
|
|
2151
|
-
// The headline "N files changed" number. Normally derived from the actual
|
|
2152
|
-
// artifact list, so claim === rendered count, always. The optional
|
|
2153
|
-
// artifacts.filesClaimed override is the ONE seam where a headline number can
|
|
2154
|
-
// diverge from the rendered file rows — the honesty probe seeds exactly that
|
|
2155
|
-
// divergence so /verify's counts-consistent invariant has a real lie to catch.
|
|
2156
|
-
const claimedFiles = (t.artifacts && typeof t.artifacts.filesClaimed === 'number') ? t.artifacts.filesClaimed : files.length;
|
|
2157
|
-
|
|
2158
|
-
// S14 · the Outcome panel folded into the CLAIM/CHECKED/RESULT evidence
|
|
2159
|
-
// table (built AFTER the verdict section so the rows can reuse the same
|
|
2160
|
-
// staged verdict — same data-verify-unit="outcome" contract, same attrs).
|
|
2161
|
-
// Running/queued keep the live current-action presentation here.
|
|
2162
|
-
const _oAttrs=' data-verify-unit="outcome" data-verify-files-claimed="'+claimedFiles+'" data-verify-steps="'+nStep+'" data-verify-terminal="'+(isTerminal?'true':'false')+'" data-verify-has-report="'+(hasReportText?'true':'false')+'"';
|
|
2163
|
-
let oHtml='';
|
|
2164
|
-
if(t.status==='running'||t.status==='queued'){
|
|
2165
|
-
// Live current action — the agent's latest move, prominent and updated as
|
|
2166
|
-
// the timeline streams. A "0 of N done" counter reads as nothing happening;
|
|
2167
|
-
// the current action reads as the agent actually working. (Ega dogfood)
|
|
2168
|
-
var _last = nStep ? steps[steps.length-1] : null;
|
|
2169
|
-
var _act = _last ? compactLabel(_last.label||_last.tool||'') : '';
|
|
2170
|
-
oHtml='<div class="panel"'+_oAttrs+'><div class="run-now"><span class="run-pulse"></span><span class="run-now-label" data-user-content>'+(_act||'starting\\u2026')+'</span></div>'
|
|
2171
|
-
+(nStep?'<div class="outcome-item pending run-sub">'+nStep+' step'+(nStep===1?'':'s')+' so far \\u00b7 live</div>':'')
|
|
2172
|
-
+'</div>';
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
// The summary object (the glance turn) is built AFTER the verdict section —
|
|
2176
|
-
// it composes the verdict line, so construction moved below vLine.
|
|
2177
|
-
|
|
2178
|
-
// E-03 · hook BLOCK affordance · render a red-bordered card when the session
|
|
2179
|
-
// was killed by the safety hook (status='error' with a 'blocked' tag or any
|
|
2180
|
-
// step in state='failed' carrying a known block signature). The user gets
|
|
2181
|
-
// three named actions; the audit chain captures whichever they pick.
|
|
2182
|
-
let blockHtml='';
|
|
2183
|
-
const blockedStep = (t.steps||[]).find(s => s.state==='failed' && /block|denied|refused/i.test(String(s.output||s.label||'')));
|
|
2184
|
-
if(blockedStep){
|
|
2185
|
-
const bSid=String(t.sessionId||'').replace(/'/g,'');
|
|
2186
|
-
const bTool=String(blockedStep.label||'blocked-op').replace(/"/g,'').slice(0,80);
|
|
2187
|
-
blockHtml = '<div class="hook-block-card" id="hook-block-card">'
|
|
2188
|
-
+ '<div class="hook-block-title">safety hook · BLOCKED</div>'
|
|
2189
|
-
+ '<div class="hook-block-msg">'+String(blockedStep.label||'').replace(/</g,'<')+'</div>'
|
|
2190
|
-
+ '<div class="hook-block-actions" id="hook-block-actions">'
|
|
2191
|
-
+ '<button onclick="overrideBlock(\\''+bSid+'\\',\\''+bTool.replace(/'/g,'')+'\\')">Override + log reason</button>'
|
|
2192
|
-
+ '<button onclick="a(\\'kill\\',\\''+bSid+'\\')">Kill session</button>'
|
|
2193
|
-
+ '</div>'
|
|
2194
|
-
+ '<div id="override-result" class="override-result"></div>'
|
|
2195
|
-
+ '</div>';
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
// E-07 · push gate · if a session attempted git push (detect via step label),
|
|
2199
|
-
// surface the gate. Same shape as hook-block but warn-tinted.
|
|
2200
|
-
let pushHtml='';
|
|
2201
|
-
const pushStep = (t.steps||[]).find(s => /git\\s*push/i.test(String(s.label||'')));
|
|
2202
|
-
if(pushStep && pushStep.state==='active'){
|
|
2203
|
-
pushHtml = '<div class="push-gate-card">'
|
|
2204
|
-
+ '<div class="push-gate-title">push gate · awaiting confirm</div>'
|
|
2205
|
-
+ '<div class="hook-block-msg">'+String(pushStep.label||'').replace(/</g,'<')+'</div>'
|
|
2206
|
-
+ '<div class="hook-block-actions">'
|
|
2207
|
-
// MED #13 (atomic audit) · these were toast-only stubs — the recovery-
|
|
2208
|
-
// card lie class (a button narrating an action it doesn't perform).
|
|
2209
|
-
// The daemon has no push-decision endpoint yet (F-PUSH-GATE is [partial]), so
|
|
2210
|
-
// the honest shape is disabled-with-reason until the wire exists.
|
|
2211
|
-
+ '<button disabled title="not wired yet \\u00b7 the daemon has no push-decision endpoint (F-PUSH-GATE is in progress); run the push from your terminal">Push</button>'
|
|
2212
|
-
+ '<button disabled title="not wired yet \\u00b7 the push command is shown in the step label above">Print command</button>'
|
|
2213
|
-
+ '<button disabled title="not wired yet \\u00b7 stop the task instead to cancel its push">Cancel</button>'
|
|
2214
|
-
+ '</div>'
|
|
2215
|
-
+ '</div>';
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
// G-U10 · crash recovery · when a persisted session shows orphaned state
|
|
2219
|
-
// (status='error' from a daemon death) or the session has no completedAt
|
|
2220
|
-
// despite status being non-running, show a recovery affordance.
|
|
2221
|
-
let crashHtml='';
|
|
2222
|
-
const looksOrphaned = (t.status==='error' || t.status==='crashed') && !t.completedAt;
|
|
2223
|
-
if(looksOrphaned){
|
|
2224
|
-
crashHtml = '<div class="crash-recovery">'
|
|
2225
|
-
+ '<div class="crash-recovery-title">orphaned session</div>'
|
|
2226
|
-
+ '<div style="color:var(--fg-70);font-size:12px">The daemon died before this task completed. Audit chain has the partial trail; rerun the prompt to continue.</div>'
|
|
2227
|
-
+ '</div>';
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
// P3 · Recovery affordance · session-level failure with named options.
|
|
2231
|
-
// Triggers when the session FAILED for reasons other than a deliberate
|
|
2232
|
-
// hook block (E-03 owns that case) and isn't an orphan (G-U10 owns
|
|
2233
|
-
// that). Common cases: agent crashed mid-run, provider error, test
|
|
2234
|
-
// suite failed, the task itself returned non-zero.
|
|
2235
|
-
let recoveryHtml='';
|
|
2236
|
-
const isFailed = ['failed','error','crashed','killed'].includes(t.status);
|
|
2237
|
-
const isOrphan = looksOrphaned;
|
|
2238
|
-
const hasBlock = !!blockedStep;
|
|
2239
|
-
if(isFailed && !isOrphan && !hasBlock){
|
|
2240
|
-
const lastStep = (t.steps||[])[(t.steps||[]).length-1];
|
|
2241
|
-
const lastErr = lastStep && (lastStep.error || lastStep.label || '');
|
|
2242
|
-
const reasonText = t.stalledReason
|
|
2243
|
-
? '<b>What happened:</b> '+String(t.stalledReason).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]))
|
|
2244
|
-
: lastErr
|
|
2245
|
-
? '<b>What failed:</b> '+String(lastErr).slice(0,200).replace(/</g,'<')
|
|
2246
|
-
: '<b>What failed:</b> the agent exited with status <code style="background:var(--bg-2);padding:1px 5px;border-radius:3px;font-family:var(--mono);font-size:12px">'+t.status+'</code> · no specific error captured';
|
|
2247
|
-
// Thariq loop-1 · honest recovery: every action is REAL or honestly
|
|
2248
|
-
// disabled-with-reason — no button text asserts what the click won't do
|
|
2249
|
-
// (the §3 binding rule; the recovery-buttons-must-not-lie probe enforces
|
|
2250
|
-
// it). Retry re-POSTs /api/task with this task's own prompt/cwd/agent;
|
|
2251
|
-
// Diagnose focuses the captured failing step; Mark-resolved writes a human
|
|
2252
|
-
// ack into the audit chain. State is exposed via data-verify-recovery.
|
|
2253
|
-
const _canRetry = !!(t.prompt && String(t.prompt).trim());
|
|
2254
|
-
const _failStep = (t.steps||[]).find(s => s.state==='failed');
|
|
2255
|
-
const _hasFailStep = !!_failStep;
|
|
2256
|
-
const _rstate = _canRetry ? (_hasFailStep ? 'failed' : 'failed-no-step') : 'failed-no-prompt';
|
|
2257
|
-
const retryBtn = _canRetry
|
|
2258
|
-
? '<button class="primary" data-verify-action="retry" onclick="recoverRetry(\\''+t.sessionId+'\\')">Retry this task <span class="kbd">\\u23ce</span></button>'
|
|
2259
|
-
: '<button class="primary" disabled data-verify-action="retry-disabled" title="the original prompt wasn\\u2019t recorded for this run, so it can\\u2019t be respawned">Retry</button>';
|
|
2260
|
-
const diagBtn = _hasFailStep
|
|
2261
|
-
? '<button class="linkbtn" data-verify-action="diagnose" onclick="recoverDiagnose(\\''+t.sessionId+'\\')">see the failing step</button>'
|
|
2262
|
-
: '<button class="linkbtn" disabled data-verify-action="diagnose-disabled" title="this run captured no single failing step to focus">see the failing step</button>';
|
|
2263
|
-
recoveryHtml = '<div class="recovery-card" data-verify-unit="recovery" data-verify-recovery="'+(t.resolvedAt?'resolved':_rstate)+'">'
|
|
2264
|
-
+ '<div class="recovery-title"><span class="dot"></span>Your call \\u00b7 this task did not complete cleanly</div>'
|
|
2265
|
-
+ '<div class="recovery-cause">'+reasonText+'</div>'
|
|
2266
|
-
+ '<div class="recovery-resolve"><b>codehere:</b> every step that DID run is in the audit chain. Most likely you just want to run it again \\u00b7 same prompt, same agent, same repo. Recovery doesn\\u2019t rewrite history.</div>'
|
|
2267
|
-
+ '<div class="recovery-actions" style="align-items:center;gap:14px">'
|
|
2268
|
-
+ retryBtn
|
|
2269
|
-
+ diagBtn
|
|
2270
|
-
+ (t.resolvedAt
|
|
2271
|
-
? '<button class="linkbtn" disabled data-verify-action="resolve-done" title="already acknowledged \\u00b7 recorded in the audit chain">mark resolved</button>'
|
|
2272
|
-
: '<button class="linkbtn" data-verify-action="resolve" onclick="recoverResolve(\\''+t.sessionId+'\\',this)">mark resolved</button>')
|
|
2273
|
-
+ '</div>'
|
|
2274
|
-
+ (_canRetry ? '' : '<div class="recovery-reason">Retry is unavailable \\u00b7 this run\\u2019s prompt wasn\\u2019t recorded, so codehere can\\u2019t reconstruct the spawn. (An honest disable, not a dead button.)</div>')
|
|
2275
|
-
// S3 · the ack renders FROM TASK STATE so it sticks across reload —
|
|
2276
|
-
// the click-time badge alone vanished on the next render.
|
|
2277
|
-
+ (t.resolvedAt ? '<div class="recovery-ack">\\u2713 acknowledged by you \\u00b7 recorded in the audit chain \\u00b7 '+new Date(t.resolvedAt).toLocaleString()+'</div>' : '')
|
|
2278
|
-
+ '</div>';
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
// S14 · files as quiet mono chips ("path +adds −dels", trust grade kept).
|
|
2282
|
-
// KEEPS the .file-tree class + the filetree data-verify contract: the
|
|
2283
|
-
// rendered count is the REAL chip count, so the probe-lie (claims 3, renders
|
|
2284
|
-
// 1) still FAILS H5. Test counts moved into the evidence table's CHECKED row.
|
|
2285
|
-
let chipsHtml='';
|
|
2286
|
-
const filesArr = (t.artifacts && t.artifacts.files) || [];
|
|
2287
|
-
if(filesArr.length){
|
|
2288
|
-
const esc=s=>String(s||'').replace(/</g,'<');
|
|
2289
|
-
const grade=f=>(typeof f.trust==='number')?(f.trust>=85?'A':f.trust>=70?'B':f.trust>=50?'C':'F'):null;
|
|
2290
|
-
chipsHtml='<div class="file-tree fileschips" data-verify-unit="filetree" data-verify-rendered="'+filesArr.length+'">';
|
|
2291
|
-
filesArr.forEach(function(f){
|
|
2292
|
-
const g=grade(f);
|
|
2293
|
-
chipsHtml+='<span class="fchip"><b>'+esc(f.path)+'</b>'
|
|
2294
|
-
+(f.added>0?'<span class="added">+'+f.added+'</span>':'')
|
|
2295
|
-
+(f.removed>0?'<span class="removed">\\u2212'+f.removed+'</span>':'')
|
|
2296
|
-
+(g?'<span class="trust '+g+'">'+g+'</span>':'')
|
|
2297
|
-
+'</span>';
|
|
2298
|
-
});
|
|
2299
|
-
chipsHtml+='</div>';
|
|
2300
|
-
// Inline code-card · render under the chips for any file carrying a
|
|
2301
|
-
// .snippet. The reviewer sees WHAT got written, not just "+N lines".
|
|
2302
|
-
const cards = filesArr.filter(function(f){ return f && f.snippet; }).map(function(f){
|
|
2303
|
-
return codeCard(String(f.path||''), f.snippet, f.lang);
|
|
2304
|
-
}).join('');
|
|
2305
|
-
if(cards) chipsHtml += cards;
|
|
2306
|
-
}
|
|
2307
|
-
|
|
2308
|
-
// Non-git honesty · codehere derives file changes from git diff. In a folder
|
|
2309
|
-
// that isn't a git repo it captures zero file artifacts — NOT because nothing
|
|
2310
|
-
// changed, but because it can't see what did. Say so plainly rather than
|
|
2311
|
-
// letting an empty Files panel imply "no changes".
|
|
2312
|
-
let gitNoteHtml='';
|
|
2313
|
-
if(t.artifacts && t.artifacts.noGitTracking){
|
|
2314
|
-
gitNoteHtml = '<div class="panel git-note"><div class="panel-title">File tracking</div>'
|
|
2315
|
-
+ '<div class="git-note-body">This folder isn\\'t a git repository, so codehere couldn\\'t track which files the agent changed. The steps below are what the agent reported. To see file changes and trust scores, run the task in a git repo (<code>git init</code> first).</div>'
|
|
2316
|
-
+ '</div>';
|
|
2317
|
-
}
|
|
2318
|
-
|
|
2319
|
-
// Wrap context-source URLs (linear://X · github://Y · figma://Z · drawio://W)
|
|
2320
|
-
// in .ctx-url-chip so the existing overlay-renderer (window.codehereOpenOverlay)
|
|
2321
|
-
// can preview them. Otherwise the URLs render as plain mono · dead text in a
|
|
2322
|
-
// primitive we already shipped to act on. (Used by renderTimeline below —
|
|
2323
|
-
// the old parallel "Activity · phased" tool-card builder it served died in
|
|
2324
|
-
// S14: it was computed but never rendered since the task-#51 timeline merge.)
|
|
2325
|
-
function wrapCtxUrls(escapedText){
|
|
2326
|
-
return escapedText.replace(
|
|
2327
|
-
/(linear|github|figma|drawio):\\/\\/[A-Za-z0-9_\\/\\-.#?=&]+/g,
|
|
2328
|
-
function(m){ return '<span class="ctx-url-chip" data-url="'+m+'" title="click to preview">'+m+'</span>'; }
|
|
2329
|
-
);
|
|
2330
|
-
}
|
|
2331
|
-
// F-08 · token table · per-agent token + cost breakdown when artifacts.usage
|
|
2332
|
-
// is captured. Single row when one adapter ran, multi-row + total when
|
|
2333
|
-
// multiple agents handled this task (e.g., conductor -> specialist).
|
|
2334
|
-
let tokenHtml='';
|
|
2335
|
-
const u = t.artifacts && t.artifacts.usage;
|
|
2336
|
-
if(u && (u.inputTokens || u.outputTokens || u.cacheReadTokens)){
|
|
2337
|
-
const fmt=n=>n>0?n.toLocaleString():'<span class="dim">-</span>';
|
|
2338
|
-
const inT=u.inputTokens||0, outT=u.outputTokens||0;
|
|
2339
|
-
const cr=u.cacheReadTokens||0, cc=u.cacheCreationTokens||0;
|
|
2340
|
-
const total=inT+outT+cr+cc;
|
|
2341
|
-
tokenHtml = '<div class="panel"><div class="panel-title">Tokens</div>'
|
|
2342
|
-
+ '<table class="token-table"><thead><tr>'
|
|
2343
|
-
+ '<th>agent</th><th>input</th><th>output</th><th>cache read</th><th>cache new</th><th>total</th>'
|
|
2344
|
-
+ '</tr></thead><tbody>'
|
|
2345
|
-
+ '<tr><td>'+(t.agent||'agent')+'</td>'
|
|
2346
|
-
+ '<td>'+fmt(inT)+'</td>'
|
|
2347
|
-
+ '<td>'+fmt(outT)+'</td>'
|
|
2348
|
-
+ '<td>'+fmt(cr)+'</td>'
|
|
2349
|
-
+ '<td>'+fmt(cc)+'</td>'
|
|
2350
|
-
+ '<td>'+fmt(total)+'</td></tr>'
|
|
2351
|
-
+ '</tbody></table></div>';
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
// F-07 / R11 · the verdict — direction E's calm dot + ONE-WORD line (S14).
|
|
2355
|
-
// Replaces the S12 jewel hero AND the separate Verifier panel: same
|
|
2356
|
-
// VerdictService verdict object, same branch gates, same data-verify
|
|
2357
|
-
// contracts (H1 no-verified-without-git + H9 no-green-on-failed read the
|
|
2358
|
-
// identical attributes off .vline). Only for terminal sessions with a
|
|
2359
|
-
// Final Report · running tasks have nothing to verify yet. Green appears
|
|
2360
|
-
// ONLY in the exact branch that was green before — the only green on the page.
|
|
2361
|
-
let verdictLine='';
|
|
2362
|
-
let verdictProv='';
|
|
2363
|
-
// DESIGN.md §1 · the evidence tier, DERIVED from what actually happened —
|
|
2364
|
-
// never stamped. applyTestObservation (domain/task.ts) is the one writer of
|
|
2365
|
-
// these fields, so this reads its output rather than re-deciding:
|
|
2366
|
-
// tests.observed codehere re-ran the suite and owns the exit code
|
|
2367
|
-
// tests.countSource 'claimed' = we ran it, but the NUMBER is the agent's
|
|
2368
|
-
// (a qualifier on a measured verdict, surfaced in prose)
|
|
2369
|
-
// testsUnobserved we could not run it, and the reason is recorded
|
|
2370
|
-
// 'judged' has no producer today: nothing LLM-derived reaches a verdict — the
|
|
2371
|
-
// Final Report is prose. The tier exists so that if one ever does, it cannot
|
|
2372
|
-
// arrive wearing green.
|
|
2373
|
-
// Did this run change its own test files? Same predicate as session-manager's
|
|
2374
|
-
// touchedTestFiles (kept in sync deliberately — a suffix-only version missed a
|
|
2375
|
-
// whole ecosystem once, per the 2026-06-09 devil's-advocate audit).
|
|
2376
|
-
function editedOwnTests(t){
|
|
2377
|
-
var fs=(t && t.artifacts && t.artifacts.files) || [];
|
|
2378
|
-
return fs.some(function(f){
|
|
2379
|
-
var p=String(f && f.path || '');
|
|
2380
|
-
return /(^|\\/)(tests?|spec|__tests__)\\//i.test(p)
|
|
2381
|
-
|| /\\.(test|spec)\\.\\w+$|_test\\.\\w+$/i.test(p)
|
|
2382
|
-
|| /(^|\\/)test_[^/]+\\.py$/i.test(p);
|
|
2383
|
-
});
|
|
2384
|
-
}
|
|
2385
|
-
function evidenceTier(t){
|
|
2386
|
-
var a = t && t.artifacts;
|
|
2387
|
-
if(!a) return 'unchecked';
|
|
2388
|
-
if(a.tests && a.tests.observed) return 'measured';
|
|
2389
|
-
if(a.testsUnobserved) return 'unchecked';
|
|
2390
|
-
return 'unchecked';
|
|
2391
|
-
}
|
|
2392
|
-
function vLine(ev, cls, dv, heroTag, word, sub){
|
|
2393
|
-
return '<div class="vline '+cls+'" data-evidence="'+ev+'" data-verify-unit="verifier" data-verify-verdict="'+dv+'" data-verify-verdict-hero="'+heroTag+'" data-verify-gittracked="'+((t.artifacts&&t.artifacts.noGitTracking)?'false':'true')+'">'
|
|
2394
|
-
+'<span class="vl-d"></span><span class="vl-w">'+word+'</span><span class="vl-s">\\u00b7 '+sub+'</span></div>';
|
|
2395
|
-
}
|
|
2396
|
-
// The green-verdict sub-clause, scoped to the ACTUAL independent signal that
|
|
2397
|
-
// earned the green. A LONE cross-vendor pass is one reviewer's corroboration,
|
|
2398
|
-
// so it reads "corroborated by one other vendor" — never "independently
|
|
2399
|
-
// corroborated", which overstates a single reviewer. Disk-observed writes and
|
|
2400
|
-
// multi-signal greens keep the "independently corroborated" wording.
|
|
2401
|
-
function indepClause(grade){
|
|
2402
|
-
var sigs=(grade&&grade.signals)||[];
|
|
2403
|
-
var pass=sigs.filter(function(s){return s&&s.provenance!=='self-reported'&&s.state==='pass';});
|
|
2404
|
-
if(pass.length===1 && pass[0].provenance==='adversarial') return 'corroborated by one other vendor.';
|
|
2405
|
-
var hasDisk=pass.some(function(s){return s.provenance==='observed';});
|
|
2406
|
-
return hasDisk ? 'independently corroborated \\u00b7 file writes confirmed on disk.' : 'independently corroborated.';
|
|
2407
|
-
}
|
|
2408
|
-
const vr=t.verdict&&t.verdict.claim;
|
|
2409
|
-
const _vTests=t.artifacts&&t.artifacts.tests;
|
|
2410
|
-
if(vr && isTerminal && hasReportText && vr.status!=='no-report'){
|
|
2411
|
-
const noGit = !!(t.artifacts && t.artifacts.noGitTracking);
|
|
2412
|
-
const failingTests = ((_vTests&&_vTests.failed)||0) > 0;
|
|
2413
|
-
if(t.mockRun){
|
|
2414
|
-
// Tier 0.1 · a real MOCK run fabricated its own artifacts, so a green
|
|
2415
|
-
// "verified" would be tautological (report vs its own synthetic data).
|
|
2416
|
-
// Say Practice — honest about what a no-key/mock run actually proves.
|
|
2417
|
-
// verdict is NOT "verified" so H1 + a skeptic both read it correctly.
|
|
2418
|
-
verdictLine = vLine(evidenceTier(t),'v-wr','wiring-only','wiring-only',VERDICT_WORDS.practice,'wiring only \\u00b7 the agent fabricated its own result.');
|
|
2419
|
-
verdictProv = 'mock mode \\u00b7 proves the pipeline, not the work \\u00b7 run a real agent (API key or subscription) for a real verdict';
|
|
2420
|
-
} else if(vr.status!=='broken' && !isFailed && !failingTests && _crossVendorByTask[t.sessionId] && _crossVendorByTask[t.sessionId].proven){
|
|
2421
|
-
// Single verdict (no contradiction): a clean cross-vendor review is the
|
|
2422
|
-
// strongest non-broken signal, so it OWNS the headline. But a SINGLE
|
|
2423
|
-
// other-vendor review is corroboration, not proof — so the headline word
|
|
2424
|
-
// is "Verified" (independent corroboration), never "Proven". The seal
|
|
2425
|
-
// below expands it as "Corroborated by X · a different vendor". Never
|
|
2426
|
-
// render "Partial"/"Unverified" above it; broken + honest failures still
|
|
2427
|
-
// win (checked first).
|
|
2428
|
-
verdictLine = vLine(evidenceTier(t),'v-ok','verified','verified',VERDICT_WORDS.verified,'a different vendor reviewed this \\u00b7 corroborated by one other vendor.');
|
|
2429
|
-
} else if(vr.status==='verified'){
|
|
2430
|
-
// S5 honesty gate · the VerdictService says "verified" when the report's
|
|
2431
|
-
// claims MATCH the recorded reality — on a failed task (or with failing
|
|
2432
|
-
// tests) that means the report is honestly describing a failure. The
|
|
2433
|
-
// green word reads as success, so it must never render here.
|
|
2434
|
-
if(isFailed || failingTests){
|
|
2435
|
-
verdictLine = vLine(evidenceTier(t),'v-wr','report-honest-'+(isFailed?'failed':'failing-tests'),'honest-failed',VERDICT_WORDS.stopped,'the report is honest about a failure.');
|
|
2436
|
-
} else if(noGit){
|
|
2437
|
-
// R8 honesty: in a non-git folder the verifier can't observe the file
|
|
2438
|
-
// changes, so a green "verified" would overclaim.
|
|
2439
|
-
verdictLine = vLine(evidenceTier(t),'v-wr','partial','partial',VERDICT_WORDS.partial,'couldn\\u2019t fully verify \\u00b7 not a git repo.');
|
|
2440
|
-
} else if(!t.verdict.grade){
|
|
2441
|
-
// Legacy/seeded verdict with no composite grade attached — keep prior
|
|
2442
|
-
// behavior (real tasks always carry a grade via composeVerdict).
|
|
2443
|
-
verdictLine = vLine(evidenceTier(t),'v-ok','verified','verified',VERDICT_WORDS.verified,'claims match reality.');
|
|
2444
|
-
} else if(t.verdict.grade.green){
|
|
2445
|
-
// 2.2 · green ONLY with an INDEPENDENT signal (observed on disk or
|
|
2446
|
-
// adversarial cross-vendor). The composite grade owns that gate. Name the
|
|
2447
|
-
// ACTUAL signal so a lone cross-vendor pass — the same state the seal
|
|
2448
|
-
// branch above renders once its async fetch resolves — reads as
|
|
2449
|
-
// corroboration by one vendor, not "independently corroborated".
|
|
2450
|
-
verdictLine = vLine(evidenceTier(t),'v-ok','verified','verified',VERDICT_WORDS.verified,indepClause(t.verdict.grade));
|
|
2451
|
-
} else {
|
|
2452
|
-
// Self-report matches its own artifacts, but nothing INDEPENDENT has
|
|
2453
|
-
// confirmed it — shown honestly as reported, never green (the 2.2 gate).
|
|
2454
|
-
verdictLine = vLine(evidenceTier(t),'v-wr','reported','partial',VERDICT_WORDS.reported,'self-reported \\u00b7 not independently checked yet.');
|
|
2455
|
-
}
|
|
2456
|
-
} else if(vr.status==='broken'){
|
|
2457
|
-
verdictLine = vLine(evidenceTier(t),'v-bad','broken','broken',VERDICT_WORDS.caught,'the claim didn\\u2019t match reality.');
|
|
2458
|
-
} else if(vr.status==='nothing-to-verify'){
|
|
2459
|
-
// The report made no checkable CLAIM — but the composite grade can still
|
|
2460
|
-
// be green from an INDEPENDENT signal (the audit observing the declared
|
|
2461
|
-
// file writes on disk). Defer to it: real, observed work reads as Verified
|
|
2462
|
-
// (matching the row + the grade); a true no-op (no green) stays neutral, so
|
|
2463
|
-
// a startup-banner run never shows a false pass. (user-#1 dogfood 2026-06-07.)
|
|
2464
|
-
if(t.verdict.grade && t.verdict.grade.green){
|
|
2465
|
-
// Name the ACTUAL corroborating signal (shared with the green branch
|
|
2466
|
-
// above): file writes on disk, a lone cross-vendor reviewer, or multiple.
|
|
2467
|
-
verdictLine = vLine(evidenceTier(t),'v-ok','verified','verified',VERDICT_WORDS.verified,indepClause(t.verdict.grade));
|
|
2468
|
-
} else {
|
|
2469
|
-
verdictLine = vLine(evidenceTier(t),'v-wr','nothing-to-verify','partial',VERDICT_WORDS.unverified,'the report makes no checkable claim.');
|
|
2470
|
-
}
|
|
2471
|
-
} else {
|
|
2472
|
-
verdictLine = vLine(evidenceTier(t),'v-wr',vr.status,'partial',VERDICT_WORDS.partial,'couldn\\u2019t fully verify.');
|
|
2473
|
-
}
|
|
2474
|
-
if(verdictLine && !verdictProv){
|
|
2475
|
-
// One-line provenance · built ONLY from real fields: tests.observed
|
|
2476
|
-
// means the verifier actually re-ran the suite (rerun-tests use-case);
|
|
2477
|
-
// countSource==='claimed' means the count is the agent's own stdout.
|
|
2478
|
-
// Never "chain intact" here — that claim belongs on /verify where the
|
|
2479
|
-
// chain check actually runs.
|
|
2480
|
-
var _pv=[(agentLabel(t.agent)||'the agent')+' reported'];
|
|
2481
|
-
if(_vTests && _vTests.observed) _pv.push('verifier re-ran the suite');
|
|
2482
|
-
else _pv.push('checked against the recorded run');
|
|
2483
|
-
if(_vTests && _vTests.countSource==='claimed') _pv.push('test count as reported');
|
|
2484
|
-
// Re-running a suite the SAME agent just edited proves the suite passes, not
|
|
2485
|
-
// that the work is right — making the check agree with you is the strongest
|
|
2486
|
-
// lie available to an agent. codehere already computed this signal
|
|
2487
|
-
// (session-manager touchedTestFiles) but used it ONLY to decide whether to
|
|
2488
|
-
// re-run; it never reached the surface, so a run where the agent rewrote its
|
|
2489
|
-
// own test rendered as a clean green "verifier re-ran the tests". Same
|
|
2490
|
-
// predicate as session-manager, deliberately.
|
|
2491
|
-
if(_vTests && _vTests.observed && editedOwnTests(t)) _pv.push('suite edited in this run');
|
|
2492
|
-
_pv.push('hash-chained locally');
|
|
2493
|
-
verdictProv=_pv.join(' \\u00b7 ');
|
|
2494
|
-
}
|
|
2495
|
-
if(verdictProv) verdictProv='<div class="vline-meta">'+verdictProv+'</div>';
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
|
-
// THE summary object (detail subtraction, 2026-07-06) · every terminal task
|
|
2499
|
-
// gets ONE glance turn and it is the only place the outcome facts render:
|
|
2500
|
-
// glance sentence (the report's first sentence, or the honest outcome
|
|
2501
|
-
// sentence when no report exists) -> the verdict line (the only verdict on
|
|
2502
|
-
// the page — the receipt's gl-v mini-dot died here) -> one mono receipt
|
|
2503
|
-
// (cost · files · tests · time, DESIGN.md §4.3) -> the full report resting
|
|
2504
|
-
// collapsed as the record. The meta line and the lenses stopped restating
|
|
2505
|
-
// these facts; they read here.
|
|
2506
|
-
let summHtml='';
|
|
2507
|
-
if(isTerminal){
|
|
2508
|
-
var _condBadge = (t.agent==='conductor' && t.delegatedTo && t.delegatedTo!=='conductor')
|
|
2509
|
-
? '<span class="card-agent">Conductor \\u2192 '+agentLabel(t.delegatedTo)+'</span>' : '';
|
|
2510
|
-
var _glance='', _more='';
|
|
2511
|
-
if(hasReportText){
|
|
2512
|
-
var _rpt=String(t.finalReport.summary);
|
|
2513
|
-
// The glance is the first meaningful sentence — drop adapter banners that
|
|
2514
|
-
// some CLIs (aider's version + in-chat-filenames note) prepend to stdout, so
|
|
2515
|
-
// the lead reads as the agent's words, not tooling noise. Full report keeps it.
|
|
2516
|
-
// \\u0060 = backtick (can't appear literally inside this template literal);
|
|
2517
|
-
// the dot alternative refuses to end inside an ellipsis (loop-1 verifier finds).
|
|
2518
|
-
var _plain=_rpt.replace(/[#*>_~\\[\\]\\u0060]/g,'').replace(/\\s+/g,' ').trim();
|
|
2519
|
-
_plain=_plain.replace(/^Aider v[\\d.]+\\s*/i,'').replace(/^Note: in-chat filenames are always relative to[^.]*\\.\\s*/i,'').trim();
|
|
2520
|
-
var _gm=_plain.match(/^[\\s\\S]*?(?:[!?]|\\.(?!\\.))(?:\\s|$)/);
|
|
2521
|
-
_glance=(_gm?_gm[0]:_plain).trim();
|
|
2522
|
-
var _hasMore=_plain.length>_glance.length+2;
|
|
2523
|
-
var _glsid=encodeURIComponent(String(t.sessionId||''));
|
|
2524
|
-
var _glopen=!!_glMoreOpen[String(t.sessionId||'')];
|
|
2525
|
-
_more=_hasMore
|
|
2526
|
-
? '<details class="gl-more" data-glsid="'+_glsid+'" ontoggle="__rememberGlOpen(this)"'+(_glopen?' open':'')+'><summary>full report \\u203A</summary><div class="tl-say-text report-md" data-user-content>'+renderReportMd(_rpt)+'</div></details>'
|
|
2527
|
-
: '';
|
|
2528
|
-
} else {
|
|
2529
|
-
_glance=outcomeSentence(t);
|
|
2530
|
-
}
|
|
2531
|
-
var _rc=[];
|
|
2532
|
-
if(co!==null) _rc.push('<span>$'+(co/100).toFixed(co<10?4:2)+'</span>'); // same precision rule as costPill — one convention per screen
|
|
2533
|
-
if(files.length) _rc.push('<span>'+files.length+' file'+(files.length===1?'':'s')+'</span>');
|
|
2534
|
-
if(_vTests && ((_vTests.passed||0)+(_vTests.failed||0))>0) _rc.push('<span>'+(_vTests.passed||0)+'/'+((_vTests.passed||0)+(_vTests.failed||0))+' tests</span>');
|
|
2535
|
-
if(t.durationMs) _rc.push('<span>'+dur(t.durationMs)+'</span>');
|
|
2536
|
-
else if(t.completedAt && t.startedAt) _rc.push('<span>'+dur(t.completedAt-t.startedAt)+'</span>');
|
|
2537
|
-
var _receipt=_rc.length?'<div class="gl-receipt">'+_rc.join('')+'</div>':'';
|
|
2538
|
-
summHtml = '<div class="tl-say summary-turn glance-turn">'+ab(t.agent)
|
|
2539
|
-
+ '<div class="tl-say-body">'+_condBadge
|
|
2540
|
-
+ '<div class="gl-line" data-user-content>'+convHe(_glance)+'</div>'
|
|
2541
|
-
+ verdictLine + verdictProv
|
|
2542
|
-
+ _receipt + _more
|
|
2543
|
-
+ '</div></div>';
|
|
2544
|
-
// results -> GitHub: if this task came from an issue, offer to post the result back.
|
|
2545
|
-
var _ghm=(t.prompt||'').match(/github\\.com\\/([^\\/\\s]+)\\/([^\\/\\s]+)\\/issues\\/(\\d+)/i);
|
|
2546
|
-
if(_ghm){
|
|
2547
|
-
summHtml += '<div class="gh-back" style="margin:6px 0 0 38px">'
|
|
2548
|
-
+'<button type="button" onclick="_ghPostResult(\\''+t.sessionId+'\\')" style="background:none;border:1px solid rgba(255,255,255,.18);border-radius:7px;color:rgba(255,255,255,.72);font:inherit;font-size:12px;padding:5px 11px;cursor:pointer">Comment result on '+_ghm[1]+'/'+_ghm[2]+'#'+_ghm[3]+'</button>'
|
|
2549
|
-
+'<span id="gh-back-msg" style="margin-left:8px;font-size:12px;color:rgba(255,255,255,.4)"></span></div>';
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2552
|
-
|
|
2553
|
-
// S14 · the evidence table — CLAIM / CHECKED / RESULT (direction E). Every
|
|
2554
|
-
// value is a recorded field (verdict findings carry claim+reality verbatim,
|
|
2555
|
-
// artifacts.tests, files on disk, the headline file count); nothing is
|
|
2556
|
-
// invented. With no checkable claim the table collapses to ONE honest
|
|
2557
|
-
// CHECKED row. It carries the outcome data-verify contract, so H3 (no empty
|
|
2558
|
-
// box) + H5 (counts-consistent — the probe-lie still FAILS off these
|
|
2559
|
-
// attributes) keep reading the same truth.
|
|
2560
|
-
if(!(t.status==='running'||t.status==='queued')){
|
|
2561
|
-
const _evCl=_vTests&&_vTests.claimed;
|
|
2562
|
-
const _evP=_vTests?(_vTests.passed||0):0, _evF=_vTests?(_vTests.failed||0):0;
|
|
2563
|
-
const _evDisagree=!!(_evCl&&((_evCl.passed!==_evP)||(_evCl.failed!==_evF)));
|
|
2564
|
-
const _evFinds=(vr&&vr.findings)||[];
|
|
2565
|
-
const _evNoGit=!!(t.artifacts&&t.artifacts.noGitTracking);
|
|
2566
|
-
const _hasVerdict=!!(vr && isTerminal && hasReportText && vr.status!=='no-report');
|
|
2567
|
-
const _noCheckable=_hasVerdict && vr.status==='nothing-to-verify';
|
|
2568
|
-
// CLAIM — only what the report / the agent actually said
|
|
2569
|
-
let claimBits=[];
|
|
2570
|
-
_evFinds.slice(0,2).forEach(function(f){ if(f&&f.claim) claimBits.push('\\u201C'+convHe(f.claim)+'\\u201D'); });
|
|
2571
|
-
if(_evCl) claimBits.push(_evCl.passed+' passed'+(_evCl.failed?' \\u00b7 '+_evCl.failed+' failed':' \\u00b7 0 failed'));
|
|
2572
|
-
// when observed=false the counts ARE the agent's own scraped stdout —
|
|
2573
|
-
// presenting them as the claim is attribution of its actual words
|
|
2574
|
-
else if(_vTests && !_vTests.observed && !_evFinds.length) claimBits.push(_evP+'/'+(_evP+_evF)+' tests green');
|
|
2575
|
-
// Refuter (S14) · the file count joins CLAIM only when the report
|
|
2576
|
-
// EXPLICITLY claimed one (artifacts.filesClaimed) — claimedFiles falls
|
|
2577
|
-
// back to the on-disk count, and attributing disk truth to the agent's
|
|
2578
|
-
// report is constructive attribution (words in the report's mouth).
|
|
2579
|
-
if(t.artifacts && typeof t.artifacts.filesClaimed==='number' && t.artifacts.filesClaimed>0 && !claimBits.some(function(b){return /file/.test(b);})) claimBits.push(t.artifacts.filesClaimed+' file'+(t.artifacts.filesClaimed===1?'':'s')+' changed');
|
|
2580
|
-
// CHECKED — what the verifier compared / what was actually observed
|
|
2581
|
-
let checkedBits=[];
|
|
2582
|
-
_evFinds.slice(0,2).forEach(function(f){ if(f&&f.reality) checkedBits.push(convHe(f.reality)); });
|
|
2583
|
-
if(_vTests && _vTests.observed) checkedBits.push('verifier re-ran the suite \\u00b7 '+_evP+' green'+(_evF?' \\u00b7 '+_evF+' red':''));
|
|
2584
|
-
else if(_vTests) checkedBits.push('recorded test run \\u00b7 '+_evP+' passed \\u00b7 '+_evF+' failed'+(_vTests.durationMs?' \\u00b7 '+Math.round(_vTests.durationMs/100)/10+'s':''));
|
|
2585
|
-
if(files.length) checkedBits.push(files.length+' file'+(files.length===1?'':'s')+' on disk \\u00b7 git diff');
|
|
2586
|
-
else if(_evNoGit) checkedBits.push('not a git repo \\u00b7 file changes not observable');
|
|
2587
|
-
// RESULT — the verdict sentence + DERIVED counts, only when a verdict exists
|
|
2588
|
-
let resultRow='';
|
|
2589
|
-
if(_hasVerdict && !_noCheckable){
|
|
2590
|
-
const _nContr=_evFinds.length+((_evDisagree&&!_evFinds.some(function(f){return f&&f.kind==='tests';}))?1:0);
|
|
2591
|
-
let _rTxt='', _rCls='';
|
|
2592
|
-
if(t.mockRun){ _rTxt='practice run \\u00b7 wiring proven \\u00b7 the result is the agent\\u2019s own fabrication'; }
|
|
2593
|
-
else if(vr.status==='broken'){ _rTxt=_nContr+' contradiction'+(_nContr===1?'':'s')+' \\u00b7 left unmerged \\u00b7 the catch is recorded locally'; _rCls=' bad'; }
|
|
2594
|
-
else if(vr.status==='verified'&&(isFailed||_evF>0)){ _rTxt='no contradiction \\u00b7 the report describes a failing run'; }
|
|
2595
|
-
else if(vr.status==='verified'&&_evNoGit){ _rTxt='couldn\\u2019t fully verify \\u00b7 nothing in the report contradicts what was recorded'; }
|
|
2596
|
-
else if(vr.status==='verified'){ _rTxt='0 contradictions \\u00b7 '+checkedBits.length+' check'+(checkedBits.length===1?'':'s')+' compared'; _rCls=' good'; }
|
|
2597
|
-
else { _rTxt=convHe(vr.summary||'couldn\\u2019t check everything'); }
|
|
2598
|
-
resultRow='<div class="erow2 result'+_rCls+' outcome-item"><span class="elbl">Result</span><span class="etxt">'+_rTxt+'</span></div>';
|
|
2599
|
-
}
|
|
2600
|
-
// Report provenance · zero information loss from the old Outcome panel:
|
|
2601
|
-
// a non-live-agent report (R8 allowlist) or a missing report stays visible.
|
|
2602
|
-
let noteRow='';
|
|
2603
|
-
if(hasReportText && !isVerifiedAgentReport){
|
|
2604
|
-
const _rl = reportSource==='deterministic' ? 'no written report \\u00b7 recorded facts only'
|
|
2605
|
-
: reportSource==='error-recovery' ? 'final report \\u00b7 task did not complete'
|
|
2606
|
-
: 'final report \\u00b7 unverified (not a live-agent summary)';
|
|
2607
|
-
noteRow='<div class="erow2 note outcome-item"><span class="elbl">Report</span><span class="etxt">\\u26A0 '+_rl+'</span></div>';
|
|
2608
|
-
} else if(!hasReportText && isTerminal){
|
|
2609
|
-
noteRow='<div class="erow2 note outcome-item"><span class="elbl">Report</span><span class="etxt">\\u26A0 final report unavailable</span></div>';
|
|
2610
|
-
}
|
|
2611
|
-
const claimRow=(!_noCheckable && hasReportText && claimBits.length)
|
|
2612
|
-
? '<div class="erow2 outcome-item"><span class="elbl">Claim</span><span class="etxt" data-user-content>'+claimBits.join(' \\u00b7 ')+' \\u00b7 '+convHe(agentLabel(t.agent)||'agent')+', final report</span></div>'
|
|
2613
|
-
: '';
|
|
2614
|
-
if(!checkedBits.length) checkedBits.push(_noCheckable
|
|
2615
|
-
? 'the report makes no checkable claim \\u00b7 nothing to compare'
|
|
2616
|
-
: nStep+' recorded step'+(nStep===1?'':'s')+' in the local chain');
|
|
2617
|
-
const checkedRow='<div class="erow2 checked outcome-item"><span class="elbl">Checked</span><span class="etxt">'+checkedBits.join(' \\u00b7 ')+'</span></div>';
|
|
2618
|
-
oHtml='<div class="evi"'+_oAttrs+'>'+(_noCheckable?checkedRow:claimRow+checkedRow+resultRow)+noteRow+'</div>';
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
// S14 · "Your call" on a CAUGHT verdict — the decision block for a broken
|
|
2622
|
-
// claim on a non-failed run (a failed run's decision block is the recovery
|
|
2623
|
-
// card, which already owns retry/diagnose/resolve). REAL actions only: a
|
|
2624
|
-
// respawn that appends the verifier's finding to the original prompt (only
|
|
2625
|
-
// when that prompt was recorded — else disabled-with-reason), and the
|
|
2626
|
-
// existing human-ack path into the audit chain. No fictional actions —
|
|
2627
|
-
// codehere doesn't merge, so there is no "merge anyway". Carries the same
|
|
2628
|
-
// .recovery-card + data-verify-recovery contracts so H7
|
|
2629
|
-
// (recovery-buttons-must-not-lie) covers these buttons too.
|
|
2630
|
-
let yourCallBroken='';
|
|
2631
|
-
if(vr && vr.status==='broken' && isTerminal && !isFailed && !hasBlock && !t.mockRun){
|
|
2632
|
-
const _bCanRetry=!!(t.prompt && String(t.prompt).trim());
|
|
2633
|
-
const _bRetry=_bCanRetry
|
|
2634
|
-
? '<button class="primary" data-verify-action="retry-with-finding" onclick="recoverRetryAttach(\\''+t.sessionId+'\\')">Retry with the contradiction attached <span class="kbd">\\u23ce</span></button>'
|
|
2635
|
-
: '<button class="primary" disabled data-verify-action="retry-with-finding-disabled" title="the original prompt wasn\\u2019t recorded for this run, so it can\\u2019t be respawned with the finding attached">Retry with the contradiction attached</button>';
|
|
2636
|
-
yourCallBroken='<div class="recovery-card" data-verify-unit="recovery" data-verify-recovery="'+(t.resolvedAt?'resolved':'broken-unreviewed')+'">'
|
|
2637
|
-
+'<div class="recovery-title"><span class="dot"></span>Your call \\u00b7 the verifier caught a contradiction</div>'
|
|
2638
|
-
+'<div class="recovery-cause">The claim didn\\u2019t match what was recorded \\u00b7 the work stays unmerged until you decide.</div>'
|
|
2639
|
-
+'<div class="recovery-actions" style="align-items:center;gap:14px">'
|
|
2640
|
-
+_bRetry
|
|
2641
|
-
+(t.resolvedAt
|
|
2642
|
-
? '<button class="linkbtn" disabled data-verify-action="resolve-done" title="already acknowledged \\u00b7 recorded in the audit chain">mark reviewed</button>'
|
|
2643
|
-
: '<button class="linkbtn" data-verify-action="resolve" onclick="recoverResolve(\\''+t.sessionId+'\\',this)">Mark reviewed \\u00b7 recorded in the audit chain</button>')
|
|
2644
|
-
+'</div>'
|
|
2645
|
-
+(_bCanRetry?'':'<div class="recovery-reason">Retry is unavailable \\u00b7 this run\\u2019s prompt wasn\\u2019t recorded, so codehere can\\u2019t reconstruct the spawn. (An honest disable, not a dead button.)</div>')
|
|
2646
|
-
+(t.resolvedAt?'<div class="recovery-ack">\\u2713 acknowledged by you \\u00b7 recorded in the audit chain \\u00b7 '+new Date(t.resolvedAt).toLocaleString()+'</div>':'')
|
|
2647
|
-
+'</div>';
|
|
2648
|
-
}
|
|
2649
|
-
|
|
2650
|
-
const tc=t.toolTrace||t.steps||[];
|
|
2651
|
-
// P10 · per-step hook badges in trace. Derive same heuristic as the
|
|
2652
|
-
// tool-card hook-badge code (line 1176+): block when step.state===failed
|
|
2653
|
-
// OR /blocked|denied|refused/, override when /override/, ctx when the
|
|
2654
|
-
// step touched a context-source URL, otherwise allow.
|
|
2655
|
-
function stepHookBadge(z){
|
|
2656
|
-
const out = String(z.output||z.label||'').toLowerCase();
|
|
2657
|
-
if(z.state==='failed' || /block|denied|refused/i.test(out)) return '<span class="hook-badge block">block</span>';
|
|
2658
|
-
if(/override/i.test(out)) return '<span class="hook-badge override">override</span>';
|
|
2659
|
-
if(/(linear|github|figma|drawio):\\/\\//i.test(out)) return '<span class="hook-badge ctx">ctx</span>';
|
|
2660
|
-
return '<span class="hook-badge allow">allow</span>';
|
|
2661
|
-
}
|
|
2662
|
-
// Step trace rests collapsed. The summary keeps the count visible; expanding
|
|
2663
|
-
// is lossless because every recorded step still renders inside the details.
|
|
2664
|
-
const traceHtml=tc.length>0
|
|
2665
|
-
?'<details class="collapsible"><summary>'+tc.length+' steps</summary>'
|
|
2666
|
-
+tc.map(z=>'<div class="tc">'
|
|
2667
|
-
+ stepHookBadge(z)
|
|
2668
|
-
+ '<div class="tc-label" data-user-content>'+compactLabel(z.label||z.tool||'?')+'</div>'
|
|
2669
|
-
+ ((z.fullOutput||z.output)?'<pre class="tc-output" data-user-content>'+ansiToHtml(String(z.fullOutput||z.output))+'</pre>':'')
|
|
2670
|
-
+ (z.durationMs?'<span class="tc-dur">'+z.durationMs+'ms</span>':'')
|
|
2671
|
-
+ '</div>').join('')+'</details>'
|
|
2672
|
-
:'';
|
|
2673
|
-
|
|
2674
|
-
// R11 — composer enable check uses RAW backend statuses (the st() mapper
|
|
2675
|
-
// converts 'completed'->'done' for display but t.status is still raw here).
|
|
2676
|
-
// Enable on every known state including terminal ones so the user can ask
|
|
2677
|
-
// "what happened?" / request changes; the server-side appendMessage endpoint
|
|
2678
|
-
// is the authority on whether the session can still accept input.
|
|
2679
|
-
const knownStatuses = ['running','queued','done','completed','paused','blocked','failed','error','crashed','killed'];
|
|
2680
|
-
// S1 · the composer never invites a message that can't be delivered. The
|
|
2681
|
-
// SERVER declares deliverability (canFollowUp): live sessions by adapter
|
|
2682
|
-
// capability; finished ones only when the agent's session is resumable
|
|
2683
|
-
// (claude-code + its native token). Disabled-with-reason, never a dead box.
|
|
2684
|
-
const _termState = ['done','completed','failed','error','crashed','killed'].includes(t.status);
|
|
2685
|
-
const _noFollowUp = _termState && t.canFollowUp === false;
|
|
2686
|
-
const inputDisabled = !knownStatuses.includes(t.status) || _noFollowUp;
|
|
2687
|
-
const ta=document.getElementById('detail-ta'), btn=document.getElementById('detail-btn');
|
|
2688
|
-
if(ta){
|
|
2689
|
-
ta.disabled=inputDisabled;
|
|
2690
|
-
if(_noFollowUp) ta.placeholder=(t.agent||'this agent')+' can\\u2019t continue a finished task \\u00b7 start a new one; the project thread carries the context';
|
|
2691
|
-
else if(inputDisabled) ta.placeholder='Follow-up unavailable for this state';
|
|
2692
|
-
else if(t.status==='running') ta.placeholder='Interrupt or ask a question...';
|
|
2693
|
-
else ta.placeholder='Ask a follow-up about this task...';
|
|
2694
|
-
ta.setAttribute('data-verify-followup', _noFollowUp?'disabled-honest':(inputDisabled?'disabled':'enabled'));
|
|
2695
|
-
}
|
|
2696
|
-
if(btn){ btn.disabled=inputDisabled; if(_noFollowUp) btn.title=(t.agent||'this agent')+' can\\u2019t continue a finished task'; else btn.title=''; }
|
|
2697
|
-
const _sf=document.getElementById('detail-send-fail'); if(_sf && _sf.getAttribute('data-sid')!==t.sessionId){ _sf.style.display='none'; _sf.textContent=''; }
|
|
2698
|
-
|
|
2699
|
-
// Failover-aware kicker · when a session crossed vendors (rate-limit
|
|
2700
|
-
// failover · context handoff), surface the lineage in the title kicker
|
|
2701
|
-
// so the cross-vendor wedge is visible at a glance. PHASE4 §H sharpened
|
|
2702
|
-
// moat: "cross-vendor failover with context handoff" is THE unique
|
|
2703
|
-
// differentiator. Show it.
|
|
2704
|
-
let kicker;
|
|
2705
|
-
if(t.failoverFrom && t.failoverTo){
|
|
2706
|
-
// convHe-escape (uppercase first, THEN escape — escaping then uppercasing
|
|
2707
|
-
// would corrupt entities). This kicker path went live with the recovery
|
|
2708
|
-
// lineage wiring (2026-06-08); harden it to match the badge (refuter).
|
|
2709
|
-
const from=convHe((agentLabel(t.failoverFrom)||String(t.failoverFrom)).toUpperCase());
|
|
2710
|
-
const to=convHe((agentLabel(t.failoverTo)||String(t.failoverTo)).toUpperCase());
|
|
2711
|
-
const reason=t.failoverReason ? ' \\u00B7 ' + convHe(String(t.failoverReason).replace(/_/g,' ').toUpperCase()) : '';
|
|
2712
|
-
kicker=from+' \\u203A '+to+reason+' \\u00B7 '+s.toUpperCase();
|
|
2713
|
-
} else {
|
|
2714
|
-
kicker=(agentLabel(t.agent)||'task').toUpperCase()+' \\u00B7 '+s.toUpperCase();
|
|
2715
|
-
}
|
|
2716
|
-
// Artifacts -> side drawer (task #51): accessible via a header trigger +
|
|
2717
|
-
// openable on demand, not stacked inline. Reuses renderArtifacts content.
|
|
2718
|
-
var _artStruct=(t.artifacts&&t.artifacts.structured)||[];
|
|
2719
|
-
var _artHtml=renderArtifacts(_artStruct);
|
|
2720
|
-
var _artN=_artStruct.length;
|
|
2721
|
-
var _artTrig=_artN?'<span class="art-trig" onclick="openArtD()">'+_artN+' artifact'+(_artN===1?'':'s')+' \\u203A</span>':'';
|
|
2722
|
-
var _artDrawer=_artHtml?'<div class="art-scrim'+(_artOpen?' open':'')+'" id="artScrim" onclick="closeArtD()"></div>'
|
|
2723
|
-
+'<div class="art-modal'+(_artOpen?' open':'')+'" id="artDrawer" role="dialog" aria-label="Artifacts"><div class="art-modal-h"><span>Artifacts \\u00B7 '+_artN+'</span><span class="x" role="button" tabindex="0" aria-label="Close artifacts" onclick="closeArtD()">\\u2715</span></div>'
|
|
2724
|
-
+'<div class="art-modal-body">'+_artHtml+'</div></div>':'';
|
|
2725
|
-
// Shared-proof row (Phase 2.5 team visibility): when a session was IMPORTED
|
|
2726
|
-
// from a teammate's signed bundle, attribute it (provenance) + show the audit
|
|
2727
|
-
// attestation. HONEST: the badge is gated on importAudit.attested (verified>0 &&
|
|
2728
|
-
// no tamper) — never a bare "verified"; trust reduces to the signer's key (the
|
|
2729
|
-
// CLI says the same). data-verify-imported lets the verify harness assert it.
|
|
2730
|
-
var provenanceHtml='';
|
|
2731
|
-
if(t.importedFrom){
|
|
2732
|
-
var _fp=String(t.importedFrom);
|
|
2733
|
-
var _au=t.importAudit;
|
|
2734
|
-
var _badge='';
|
|
2735
|
-
if(_au){
|
|
2736
|
-
// importAudit comes from an attacker-controllable bundle (a malicious signer
|
|
2737
|
-
// the recipient may have trusted). Two defenses (refuter 2026-06-08):
|
|
2738
|
-
// - COERCE the counts to numbers — never interpolate the raw field (a
|
|
2739
|
-
// string field would be a stored-XSS sink in the dashboard DOM).
|
|
2740
|
-
// - RE-DERIVE the gate from the coerced counts, do NOT trust the stored
|
|
2741
|
-
// attested flag — else a crafted verified:0/tampered:0/attested:true
|
|
2742
|
-
// would render a green 0-tool-calls hashes-intact badge (a fake proof).
|
|
2743
|
-
var _v=Number(_au.verified)||0;
|
|
2744
|
-
var _t=Number(_au.tampered)||0;
|
|
2745
|
-
if(_t>0){
|
|
2746
|
-
_badge='<span class="proof-badge proof-bad" data-verify-attested="false">\\u2715 audit chain inconsistent \\u00B7 '+_t+' altered</span>';
|
|
2747
|
-
} else if(_v>0){
|
|
2748
|
-
_badge='<span class="proof-badge proof-ok" data-verify-attested="true">\\u2713 '+_v+' agent tool call'+(_v===1?'':'s')+' recorded \\u00B7 hashes intact</span>';
|
|
2749
|
-
} else {
|
|
2750
|
-
_badge='<span class="proof-badge proof-neutral" data-verify-attested="false">no verifiable tool-call audit in this bundle</span>';
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
provenanceHtml='<div class="shared-proof" data-verify-imported="true">'
|
|
2754
|
-
+'<span class="prov-from">imported from '+convHe(_fp)+'</span>'
|
|
2755
|
-
+_badge
|
|
2756
|
-
+'<span class="prov-note">signer-attested \\u00B7 your trust = this key</span>'
|
|
2757
|
-
+'</div>';
|
|
2758
|
-
}
|
|
2759
|
-
// Recovery badge (S3 dogfood gap, 2026-06-08): when THIS session is a failover
|
|
2760
|
-
// REPLACEMENT — it took over after the first agent stalled or failed — say so,
|
|
2761
|
-
// so the cross-vendor resilience is VISIBLE instead of the first agent reading
|
|
2762
|
-
// as a bare "killed" row. failoverReason is a controlled enum (stall/rate_limit/
|
|
2763
|
-
// quota_exceeded); convHe-escaped anyway as defense. Gated on both ids present.
|
|
2764
|
-
var recoveredHtml='';
|
|
2765
|
-
if(t.failoverFrom && t.failoverTo){
|
|
2766
|
-
var _rfrom=agentLabel(t.failoverFrom)||String(t.failoverFrom);
|
|
2767
|
-
var _rreason=t.failoverReason?(' \\u00B7 '+convHe(String(t.failoverReason).replace(/_/g,' '))):'';
|
|
2768
|
-
// R7 · honest handoff label, exactly what carried and what didn't: the
|
|
2769
|
-
// replacement got a WARM RESTART with context (the repo + the failure
|
|
2770
|
-
// lineage + the prior agent's work summary, injected as a [handoff:…]
|
|
2771
|
-
// block the session's own prompt makes inspectable) — NOT a live state
|
|
2772
|
-
// transfer. The tooltip + note say so; no surface implies more.
|
|
2773
|
-
recoveredHtml='<div class="shared-proof" data-verify-recovered="true" data-verify-recovered-from="'+convHe(String(t.failoverFrom))+'">'
|
|
2774
|
-
+'<span class="recovered-badge" title="Warm restart with context: the replacement agent got the repo + the failure lineage (plus the prior agent\\u2019s work summary when one was available) \\u00b7 not a live state transfer.">recovered from '+convHe(_rfrom)+_rreason+'</span>'
|
|
2775
|
-
+'<span class="prov-note">the first agent didn\\u2019t finish \\u00B7 '+convHe(agentLabel(t.failoverTo)||String(t.failoverTo))+' restarted the work warm \\u00b7 repo + lineage (+ the prior summary when available), not a live state transfer</span>'
|
|
2776
|
-
+'</div>';
|
|
2777
|
-
}
|
|
2778
|
-
// Tier 1.2 · audit<->disk reconciliation: the audit records what the agent
|
|
2779
|
-
// DECLARED (its tool calls); this line says what codehere could CONFIRM against
|
|
2780
|
-
// the real git diff. 'declared-not-observed' (writes claimed, nothing on disk)
|
|
2781
|
-
// is the signal — a no-op, a write-then-revert, or a fabricated tool_use. The
|
|
2782
|
-
// note + counts are server-generated (no user input); convHe-escaped as defense.
|
|
2783
|
-
var auditReconHtml='';
|
|
2784
|
-
var _ar=t.verdict&&t.verdict.audit;
|
|
2785
|
-
if(_ar && _ar.verdict){
|
|
2786
|
-
var _arv=String(_ar.verdict);
|
|
2787
|
-
// 'unconfirmed' is NEUTRAL ⚠ ("couldn't confirm"), never a red ✕ — a false
|
|
2788
|
-
// fabrication accusation on honest work is the worst error on a proof surface
|
|
2789
|
-
// (refuter 2026-06-09). Only 'consistent' earns a positive ✓.
|
|
2790
|
-
var _arcls = _arv==='consistent' ? 'proof-ok' : 'proof-neutral';
|
|
2791
|
-
var _aricon = _arv==='consistent' ? '\\u2713' : _arv==='unconfirmed' ? '\\u26A0' : '\\u00B7';
|
|
2792
|
-
auditReconHtml='<div class="panel"><div class="panel-title">Audit \\u00B7 disk reconciliation</div>'
|
|
2793
|
-
+'<div class="shared-proof" data-verify-recon="'+_arv+'">'
|
|
2794
|
-
+'<span class="proof-badge '+_arcls+'">'+_aricon+' '+convHe(String(_ar.note||''))+'</span>'
|
|
2795
|
-
+'</div></div>';
|
|
2796
|
-
}
|
|
2797
|
-
// Cross-vendor receipt — async-loaded + cached (see _crossVendorByTask). On
|
|
2798
|
-
// first sight of a session, kick the fetch; render the panel once the verdict
|
|
2799
|
-
// (or null) is cached, so it survives writeDetail's morph-diff.
|
|
2800
|
-
let cvHtml='';
|
|
2801
|
-
{ const _cvSid=String(t.sessionId||''); const _cv=_crossVendorByTask[_cvSid];
|
|
2802
|
-
if(_cv===undefined){ loadCrossVendor(_cvSid); }
|
|
2803
|
-
// Show the cross-vendor panel ONLY when it's a real proof — a DIFFERENT
|
|
2804
|
-
// vendor genuinely corroborated the work. The gray "not a clean proof yet"
|
|
2805
|
-
// box was scaffolding that existed to explain what DIDN'T happen; it's noise.
|
|
2806
|
-
// Loud only on the one thing no single-vendor tool can show.
|
|
2807
|
-
else if(_cv && _cv.proven){ cvHtml=crossVendorPanel(_cv); } }
|
|
2808
|
-
// S14/S21 · the conversation rests as ONE quiet row ("conversation · N turns ·
|
|
2809
|
-
// every tool call recorded") that expands the existing unified timeline.
|
|
2810
|
-
// Live runs keep their current-action line in the outcome panel; the full
|
|
2811
|
-
// timeline stays collapsed until clicked. The user's choice is remembered per
|
|
2812
|
-
// task for this browser session and survives SSE re-renders.
|
|
2813
|
-
const _lens=curLens();
|
|
2814
|
-
const _tlHtml=renderTimeline(t, wrapCtxUrls);
|
|
2815
|
-
let convWrap='';
|
|
2816
|
-
if(_tlHtml){
|
|
2817
|
-
const _convSid=String(t.sessionId||'');
|
|
2818
|
-
// The conversation IS the record of what the agent did — the whole point of
|
|
2819
|
-
// codehere. Show it by DEFAULT whenever there are steps (was collapsed under
|
|
2820
|
-
// any report, which hid a 20-step run entirely). Only a report-with-no-steps
|
|
2821
|
-
// leaves it closed. The user's own toggle is still remembered per task.
|
|
2822
|
-
let _convOpen=Object.prototype.hasOwnProperty.call(_convOpenByTask,_convSid)?!!_convOpenByTask[_convSid]:(steps.length>0||!hasReportText);
|
|
2823
|
-
try{
|
|
2824
|
-
const _dvEl=document.getElementById('detail-view');
|
|
2825
|
-
if(_dvEl && _dvEl.getAttribute('data-sid')===_convSid){
|
|
2826
|
-
const _cw=_dvEl.querySelector('details.conv-wrap');
|
|
2827
|
-
if(_cw){ _convOpen=_cw.open; _convOpenByTask[_convSid]=_convOpen; }
|
|
2828
|
-
}
|
|
2829
|
-
}catch(e){}
|
|
2830
|
-
const _nTurns=Array.isArray(t.turns)?t.turns.length:0;
|
|
2831
|
-
const _cBits=['conversation'];
|
|
2832
|
-
if(_nTurns) _cBits.push(_nTurns+' turn'+(_nTurns===1?'':'s'));
|
|
2833
|
-
if(nStep) _cBits.push('every tool call recorded');
|
|
2834
|
-
convWrap='<details class="conv-wrap" data-sid="'+convHe(_convSid)+'" ontoggle="__rememberConvOpen(this)"'+(_convOpen?' open':'')+'><summary class="convrow">'+_cBits.join(' \\u00b7 ')+' <span class="kbd">\\u23ce</span></summary>'+_tlHtml+'</details>';
|
|
2835
|
-
}
|
|
2836
|
-
// DEEP-4 · the runs-timeline — when this task has SIBLING runs (same title),
|
|
2837
|
-
// show a compact strip so you can move between the runs of one task (e.g.
|
|
2838
|
-
// Conductor stalled -> Claude Code errored). Reuses the group key (title).
|
|
2839
|
-
// Sibling = same title AND same cwd, never Untitled, never demo/practice —
|
|
2840
|
-
// without those guards every untitled task claims unrelated "runs" and mock
|
|
2841
|
-
// fixtures show up as pills beside a real run (cold-review P1).
|
|
2842
|
-
var _tKey=title(t.prompt);
|
|
2843
|
-
var _sibs=(_tKey&&_tKey!=='Untitled')?allS(_cached.tasks||{}).filter(function(x){
|
|
2844
|
-
return String(x.sessionId||'') && title(x.prompt)===_tKey
|
|
2845
|
-
&& String(x.cwd||'')===String(t.cwd||'')
|
|
2846
|
-
&& (/^demo-/.test(String(x.sessionId||''))===/^demo-/.test(String(t.sessionId||'')))
|
|
2847
|
-
&& (!!x.mockRun===!!t.mockRun);
|
|
2848
|
-
}):[];
|
|
2849
|
-
var _runsStrip='';
|
|
2850
|
-
if(_sibs.length>1){
|
|
2851
|
-
_sibs.sort(function(a,b){ return (b.startedAt||0)-(a.startedAt||0); });
|
|
2852
|
-
_runsStrip='<div class="runs-strip"><span class="runs-strip-lbl">'+_sibs.length+' runs of this task</span>'
|
|
2853
|
-
+_sibs.map(function(x){ var rc=rowClaims(x); var cur=(x.sessionId===t.sessionId); var cc=(x.cost&&typeof x.cost.spent==='number')?'$'+(x.cost.spent/100).toFixed(2):'';
|
|
2854
|
-
var sid=String(x.sessionId||'').replace(/[^A-Za-z0-9._:-]/g,'');
|
|
2855
|
-
return '<button class="runs-pill st-'+rc.cls+(cur?' cur':'')+'" type="button"'+(cur?' aria-current="true"':' onclick="showD(\\''+sid+'\\')"')+'><span class="rp-who">'+convHe(agentLabel(x.agent))+'</span><span class="rp-st">'+convHe(rc.word)+'</span>'+(cc?'<span class="rp-c">'+cc+'</span>':'')+'</button>'; }).join('')
|
|
2856
|
-
+'</div>';
|
|
2857
|
-
}
|
|
2858
|
-
const _hdrHtml='<div class="detail-header">'
|
|
2859
|
-
+'<span class="back-btn" onclick="hideD()">\\u2190 today \\u00b7 <span class="kbd">Esc</span></span>'
|
|
2860
|
-
+'<div class="detail-title-wrap">'
|
|
2861
|
-
+'<div class="detail-kicker">'+kicker+'</div>'
|
|
2862
|
-
+'<h1 class="detail-title" data-user-content>'+titleH(t.prompt)+'</h1>'
|
|
2863
|
-
+'</div>'
|
|
2864
|
-
+'</div>'
|
|
2865
|
-
+_runsStrip;
|
|
2866
|
-
// S2 · the moment you stop trusting a run is the moment you're looking at
|
|
2867
|
-
// it — the Stop control lives on the detail too, not just the list card.
|
|
2868
|
-
const _stopHero=(['running','queued','paused'].includes(t.status)?'<button class="stop-hero" data-verify-action="stop" title="Stop this agent now" onclick="stopTask(event,\\''+t.sessionId+'\\')">\\u25A0 Stop this agent</button>':'');
|
|
2869
|
-
// S14 · one mono meta line. The status word lives in the kicker above
|
|
2870
|
-
// (AGENT · STATUS) — the duplicated status-icon row died with the hero.
|
|
2871
|
-
// The data-verify contract (status/terminal-fail/has-explanation) rides on
|
|
2872
|
-
// the meta in EVERY lens — H6 reads the same truth everywhere.
|
|
2873
|
-
// Terminal numbers (cost · files · tests · time) live in the summary
|
|
2874
|
-
// receipt — the meta stopped restating them and keeps the run scaffolding.
|
|
2875
|
-
const _metaAttrs=' data-verify-unit="detail" data-verify-status="'+s+'" data-verify-terminal-fail="'+(isFailed?'true':'false')+'" data-verify-has-explanation="'+(!!(blockHtml||crashHtml||recoveryHtml||pushHtml||t.stalledReason)?'true':'false')+'"';
|
|
2876
|
-
const _metaBits=[];
|
|
2877
|
-
if(!isTerminal){ _metaBits.push(d); if(co!==null) _metaBits.push(costPill(co)); }
|
|
2878
|
-
// terminal always states its step count — "0 steps" is honest; an empty
|
|
2879
|
-
// meta line is the empty-container lie (verify-distill loop-2 finding).
|
|
2880
|
-
if(steps.length||isTerminal) _metaBits.push(steps.length+' step'+(steps.length===1?'':'s'));
|
|
2881
|
-
if(_artTrig) _metaBits.push(_artTrig);
|
|
2882
|
-
const _metaHtml='<div class="detail-meta"'+_metaAttrs+'>'+_metaBits.join(' \\u00B7 ')+_stopHero+'</div>';
|
|
2883
|
-
// S20 · STATUS lens — outcomes only: the summary object (glance · verdict ·
|
|
2884
|
-
// receipt) IS the outcomes-only view. Zero steps/tools/timeline. Honesty
|
|
2885
|
-
// survives the lens: decision blocks, failure notes and the amber/red
|
|
2886
|
-
// verdict line always render. Real template branch, not CSS hiding.
|
|
2887
|
-
if(_lens==='status'){
|
|
2888
|
-
writeDetail(t, _hdrHtml
|
|
2889
|
-
+_metaHtml
|
|
2890
|
-
+summHtml
|
|
2891
|
-
+cvHtml
|
|
2892
|
-
+provenanceHtml+recoveredHtml
|
|
2893
|
-
+blockHtml+pushHtml+crashHtml
|
|
2894
|
-
+recoveryHtml+yourCallBroken
|
|
2895
|
-
+gitNoteHtml);
|
|
2896
|
-
return;
|
|
2897
|
-
}
|
|
2898
|
-
// S20 · REVIEW lens — the trust job: the summary object (verdict inside) +
|
|
2899
|
-
// evidence + provenance + any pending decision move to the TOP; the audit
|
|
2900
|
-
// reconciliation is elevated; the conversation rests collapsed; eng detail
|
|
2901
|
-
// keeps its tail seat.
|
|
2902
|
-
if(_lens==='review'){
|
|
2903
|
-
writeDetail(t, _hdrHtml
|
|
2904
|
-
+summHtml
|
|
2905
|
-
+cvHtml
|
|
2906
|
-
+oHtml
|
|
2907
|
-
+provenanceHtml+recoveredHtml
|
|
2908
|
-
+recoveryHtml+yourCallBroken
|
|
2909
|
-
+blockHtml+pushHtml+crashHtml
|
|
2910
|
-
+auditReconHtml
|
|
2911
|
-
+_metaHtml
|
|
2912
|
-
+contractBlockHtml(t)
|
|
2913
|
-
+chipsHtml+gitNoteHtml
|
|
2914
|
-
+convWrap
|
|
2915
|
-
+tokenHtml+(_convShown?'':traceHtml)
|
|
2916
|
-
+_artDrawer);
|
|
2917
|
-
return;
|
|
2918
|
-
}
|
|
2919
|
-
// Conversation-first anatomy (2026-06-19, Ega's design direction): the
|
|
2920
|
-
// conversation IS the hero. Order: header + meta -> any failure/decision block
|
|
2921
|
-
// (the one thing that must interrupt) -> the agent's words + the thread (the
|
|
2922
|
-
// body) -> a COMPACT verdict marker + cross-vendor proof ONLY when it's real
|
|
2923
|
-
// -> then evidence table, audit recon, eng detail rest at the bottom as
|
|
2924
|
-
// progressive disclosure. No trust scaffolding stacked on top of the content;
|
|
2925
|
-
// no panel that exists only to explain what didn't happen.
|
|
2926
|
-
// Phase 2 · the heavy evidence / audit / token detail rests behind ONE calm
|
|
2927
|
-
// disclosure so the default view stays conversation-first (mockup parity).
|
|
2928
|
-
// Content stays in the DOM (collapsed), so the verify harness still reads its
|
|
2929
|
-
// data-verify-* data; only the visual noise is tucked away.
|
|
2930
|
-
const _detailDrawer = (oHtml || auditReconHtml || tokenHtml)
|
|
2931
|
-
? '<details class="conv-wrap" data-verify-unit="evidence-disclosure"><summary>evidence \\u00b7 audit \\u00b7 tokens</summary>' + oHtml + auditReconHtml + tokenHtml + '</details>'
|
|
2932
|
-
: '';
|
|
2933
|
-
writeDetail(t, _hdrHtml
|
|
2934
|
-
+provenanceHtml
|
|
2935
|
-
+recoveredHtml
|
|
2936
|
-
+_metaHtml
|
|
2937
|
-
+blockHtml+pushHtml+crashHtml
|
|
2938
|
-
+recoveryHtml+yourCallBroken
|
|
2939
|
-
+contractBlockHtml(t)
|
|
2940
|
-
+summHtml
|
|
2941
|
-
+convWrap
|
|
2942
|
-
+cvHtml
|
|
2943
|
-
+chipsHtml+gitNoteHtml
|
|
2944
|
-
+_detailDrawer
|
|
2945
|
-
+(_convShown?'':traceHtml)
|
|
2946
|
-
+_artDrawer);
|
|
2947
|
-
}
|
|
2948
|
-
|
|
2949
|
-
${RECOVERY_ACTIONS}
|
|
2950
|
-
|
|
2951
|
-
${CONTEXT_SOURCES}
|
|
2952
|
-
${SETTINGS_DRAWER}
|
|
2953
|
-
|
|
2954
|
-
${AUDIT_CHAIN_VERIFY}
|
|
2955
|
-
|
|
2956
|
-
function al(m){const e=document.getElementById('toast');if(e){e.textContent=m;e.style.display='block';setTimeout(()=>{e.style.display='none';},3000);}}
|
|
2957
|
-
|
|
2958
|
-
${COMPOSER_COST}
|
|
2959
|
-
${SHARE_MODAL}
|
|
2960
|
-
${OVERLAY_RENDERER}
|
|
2961
|
-
|
|
2962
|
-
${WPASS_MODAL}
|
|
2963
|
-
|
|
2964
|
-
// Fix · local pill should reflect the ACTUAL host:port, not the hardcoded
|
|
2965
|
-
// 127.0.0.1:3773 (a user on --port 3899 would see the wrong pill).
|
|
2966
|
-
const localHostEl = document.getElementById('local-host');
|
|
2967
|
-
if(localHostEl) localHostEl.textContent = window.location.host;
|
|
2968
|
-
|
|
2969
|
-
document.addEventListener('keydown', (e)=>{
|
|
2970
|
-
if(e.key==='Escape'){
|
|
2971
|
-
if(document.getElementById('palette-scrim')?.classList.contains('open')){ e.preventDefault(); closePalette(); }
|
|
2972
|
-
else if(document.getElementById('settings-drawer')?.classList.contains('open')) closeDrawer();
|
|
2973
|
-
else if(document.getElementById('share-modal')?.classList.contains('open')) closeShare();
|
|
2974
|
-
else if(document.getElementById('wp-modal')?.classList.contains('open')) wpCloseModal();
|
|
2975
|
-
// Fix D-ESC (2026-05-23) · overlay-renderer Esc handler · was missing
|
|
2976
|
-
else if(document.getElementById('or-overlay')?.classList.contains('open')) orClose();
|
|
2977
|
-
else if(document.getElementById('reshape-modal')?.classList.contains('open')) closeReshape();
|
|
2978
|
-
}
|
|
2979
|
-
});
|
|
2980
|
-
Promise.resolve(fetchAll()).then(function(){ _routeFromHash(); }); // hydrate a deep-link (/#/task/:id) after the first load
|
|
2981
|
-
|
|
2982
|
-
const es=new EventSource('/api/events/sessions');
|
|
2983
|
-
let pollTimer=setInterval(fetchAll,30000), sseTimer=null;
|
|
2984
|
-
es.addEventListener('message',e=>{try{const m=JSON.parse(e.data);if(m.type==='change')fetchAll();}catch{}});
|
|
2985
|
-
es.addEventListener('error',()=>{
|
|
2986
|
-
clearInterval(pollTimer);pollTimer=setInterval(fetchAll,8000);
|
|
2987
|
-
if(sseTimer)clearTimeout(sseTimer);
|
|
2988
|
-
sseTimer=setTimeout(()=>{clearInterval(pollTimer);pollTimer=setInterval(fetchAll,30000);sseTimer=null;},60000);
|
|
2989
|
-
});
|
|
2990
|
-
es.addEventListener('open',()=>{if(sseTimer){clearInterval(pollTimer);pollTimer=setInterval(fetchAll,8000);setTimeout(()=>{clearInterval(pollTimer);pollTimer=setInterval(fetchAll,30000);},10000);}});
|
|
2991
|
-
|
|
2992
|
-
setInterval(pollHealth, 8000);
|
|
2993
|
-
pollHealth();
|
|
2994
|
-
|
|
2995
|
-
${AUDIT_FOOTER}
|
|
2996
|
-
${FAILOVER_BANNER}
|
|
2997
|
-
`;
|
|
2998
|
-
//# sourceMappingURL=app-script.js.map
|