codehere 0.2.2 → 0.3.0-rc.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 +88 -68
- package/dist/domain/entities/task.js +4 -4
- package/dist/error-handler.js +4 -9
- package/dist/index.js +253 -1219
- package/dist/infrastructure/config/product-context.js +7 -6
- package/dist/infrastructure/hooks/claude-sdk-hook.js +348 -72
- package/dist/infrastructure/observability/execution-tracer.js +11 -6
- package/dist/infrastructure/orchestration/agent-adapter.js +13 -0
- package/dist/infrastructure/orchestration/agent-installer.js +101 -0
- package/dist/infrastructure/orchestration/agents-config-seed.js +56 -0
- package/dist/infrastructure/orchestration/aider-adapter.js +319 -0
- package/dist/infrastructure/orchestration/artifacts-extractor.js +175 -0
- package/dist/infrastructure/orchestration/claude-code-adapter.js +352 -0
- package/dist/infrastructure/orchestration/codex-adapter.js +310 -0
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +553 -0
- package/dist/infrastructure/orchestration/opencode-adapter.js +275 -0
- package/dist/infrastructure/orchestration/progress-aggregator.js +189 -0
- package/dist/infrastructure/orchestration/readiness.js +106 -0
- package/dist/infrastructure/orchestration/session-manager.js +272 -0
- package/dist/infrastructure/orchestration/session-persistence.js +109 -0
- package/dist/infrastructure/protocols/mcp-helpers.js +10 -30
- package/dist/infrastructure/protocols/mcp-server-impl.js +359 -21
- package/dist/infrastructure/protocols/ui-page.js +1827 -0
- package/dist/infrastructure/security/file-collection.js +115 -0
- package/dist/infrastructure/security/trust-scorer.js +301 -56
- package/dist/infrastructure/storage/task-repository.js +8 -7
- package/dist/monitoring.js +9 -38
- package/dist/presentation/cli/commands/help-command.js +96 -139
- package/dist/presentation/cli/commands/hook-command.js +17 -24
- package/dist/presentation/cli/commands/report-command.js +59 -72
- package/dist/presentation/cli/commands/setup-command.js +4 -4
- package/dist/presentation/cli/commands/status-command.js +321 -30
- package/dist/presentation/cli/commands/trust-command.js +84 -72
- package/dist/presentation/cli/error-display.js +17 -21
- package/dist/ui.js +11 -11
- package/package.json +24 -62
- package/dist/agents/base.js +0 -101
- package/dist/agents/orchestrator-model-driven.js +0 -141
- package/dist/agents/react-orchestrator-agent.js +0 -54
- package/dist/application/agents/execution-agent.js +0 -508
- package/dist/application/agents/multi-agent-orchestrator.js +0 -295
- package/dist/application/agents/planning-agent.js +0 -433
- package/dist/application/agents/validation-agent.js +0 -317
- package/dist/application/architectures/plan-and-act.js +0 -242
- package/dist/application/index.js +0 -12
- package/dist/application/services/agent-factory.js +0 -28
- package/dist/application/services/dependency-container.js +0 -596
- package/dist/application/services/git-context-service.js +0 -83
- package/dist/application/services/intelligent-router.js +0 -934
- package/dist/application/services/tool-executor-service.js +0 -94
- package/dist/application/use-cases/ask-question-use-case.js +0 -338
- package/dist/application/use-cases/edit-file-use-case.js +0 -830
- package/dist/application/use-cases/explain-file-use-case.js +0 -50
- package/dist/application/use-cases/index-codebase-use-case.js +0 -113
- package/dist/application/use-cases/planning-use-case.js +0 -276
- package/dist/application/use-cases/react-orchestration-use-case.js +0 -329
- package/dist/application/use-cases/reflexion-use-case.js +0 -30
- package/dist/application/workflows/deterministic-workflows.js +0 -189
- package/dist/ast-parser.js +0 -331
- package/dist/cache.js +0 -180
- package/dist/chat.js +0 -251
- package/dist/code-analysis.js +0 -327
- package/dist/context.js +0 -187
- package/dist/docs.js +0 -146
- package/dist/domain/entities/code-chunk.js +0 -6
- package/dist/domain/entities/decision.js +0 -9
- package/dist/domain/entities/edit.js +0 -6
- package/dist/domain/entities/knowledge-node.js +0 -9
- package/dist/domain/entities/knowledge-preservation.js +0 -10
- package/dist/domain/entities/memory.js +0 -6
- package/dist/domain/entities/pattern.js +0 -7
- package/dist/domain/entities/plan.js +0 -6
- package/dist/domain/entities/query.js +0 -6
- package/dist/domain/errors/embeddings-not-found-error.js +0 -13
- package/dist/domain/index.js +0 -19
- package/dist/domain/interfaces/ai-service.interface.js +0 -6
- package/dist/domain/interfaces/code-analyzer.interface.js +0 -6
- package/dist/domain/interfaces/decision-repository.interface.js +0 -9
- package/dist/domain/interfaces/embedding-repository.interface.js +0 -6
- package/dist/domain/interfaces/knowledge-graph-repository.interface.js +0 -9
- package/dist/domain/interfaces/knowledge-preservation-repository.interface.js +0 -8
- package/dist/domain/interfaces/memory-repository.interface.js +0 -6
- package/dist/domain/interfaces/pattern-repository.interface.js +0 -6
- package/dist/domain/interfaces/verification-service.interface.js +0 -6
- package/dist/domain/services/cross-team-pattern-sharing.js +0 -125
- package/dist/domain/services/decision-recorder.js +0 -223
- package/dist/domain/services/decision-retriever.js +0 -133
- package/dist/domain/services/environment-mode-manager.js +0 -22
- package/dist/domain/services/graded-memory-activation.js +0 -69
- package/dist/domain/services/knowledge-access.js +0 -132
- package/dist/domain/services/knowledge-extractor.js +0 -247
- package/dist/domain/services/knowledge-graph-builder.js +0 -268
- package/dist/domain/services/knowledge-graph-query.js +0 -187
- package/dist/domain/services/memory-retriever.js +0 -82
- package/dist/domain/services/memory-synthesizer.js +0 -91
- package/dist/domain/services/pattern-extractor.js +0 -442
- package/dist/domain/services/query-intent-classifier.js +0 -140
- package/dist/domain/services/react-loop.js +0 -186
- package/dist/domain/services/risk-tier-detector.js +0 -78
- package/dist/edit.js +0 -319
- package/dist/embed.js +0 -459
- package/dist/formatter.js +0 -168
- package/dist/git.js +0 -198
- package/dist/infrastructure/ai/cohere-ai-service.js +0 -607
- package/dist/infrastructure/ai/command-r-evaluation.js +0 -206
- package/dist/infrastructure/ai/model-config.js +0 -84
- package/dist/infrastructure/ai/provider-ai-service.js +0 -477
- package/dist/infrastructure/cache/embedding-cache.js +0 -175
- package/dist/infrastructure/cache/query-result-cache.js +0 -66
- package/dist/infrastructure/cache/response-cache.js +0 -48
- package/dist/infrastructure/cache/security-scan-cache.js +0 -47
- package/dist/infrastructure/chunking/ast-code-chunker.js +0 -19
- package/dist/infrastructure/chunking/code-aware-chunker.js +0 -397
- package/dist/infrastructure/context/context-compressor.js +0 -329
- package/dist/infrastructure/context/conversation-context.js +0 -128
- package/dist/infrastructure/context/session-manager.js +0 -94
- package/dist/infrastructure/cost/cost-tracker.js +0 -360
- package/dist/infrastructure/file-scanner/file-scanner.js +0 -106
- package/dist/infrastructure/governance/nist-ai-rmf.js +0 -292
- package/dist/infrastructure/governance/policy-as-code.js +0 -258
- package/dist/infrastructure/governance/prompt-to-code-lineage.js +0 -239
- package/dist/infrastructure/index.js +0 -7
- package/dist/infrastructure/observability/prompt-versioner.js +0 -145
- package/dist/infrastructure/protocols/a2a-metadata.js +0 -137
- package/dist/infrastructure/protocols/a2a-protocol.js +0 -314
- package/dist/infrastructure/protocols/mcp-oauth.js +0 -184
- package/dist/infrastructure/protocols/mcp-server.js +0 -333
- package/dist/infrastructure/protocols/streaming-handler.js +0 -103
- package/dist/infrastructure/providers/cohere-provider.js +0 -444
- package/dist/infrastructure/providers/fake-provider.js +0 -130
- package/dist/infrastructure/providers/local-llm-provider.js +0 -330
- package/dist/infrastructure/providers/model-config.types.js +0 -9
- package/dist/infrastructure/providers/model-provider.interface.js +0 -8
- package/dist/infrastructure/providers/openai-provider.js +0 -413
- package/dist/infrastructure/providers/openrouter-provider.js +0 -399
- package/dist/infrastructure/providers/operation-provider-resolver.js +0 -105
- package/dist/infrastructure/providers/provider-config-loader.js +0 -120
- package/dist/infrastructure/providers/provider-registry.js +0 -88
- package/dist/infrastructure/safety/pattern-detector.js +0 -133
- package/dist/infrastructure/security/ai-sast-scanner.js +0 -290
- package/dist/infrastructure/security/docker-sandbox.js +0 -178
- package/dist/infrastructure/security/enhanced-security-gate.js +0 -151
- package/dist/infrastructure/security/license-scanner.js +0 -167
- package/dist/infrastructure/security/provider-bias-detector.js +0 -164
- package/dist/infrastructure/security/sandbox-executor.js +0 -64
- package/dist/infrastructure/serialization/toon-serializer.js +0 -119
- package/dist/infrastructure/storage/memory-stream-service.js +0 -204
- package/dist/infrastructure/storage/plan-repository.js +0 -96
- package/dist/infrastructure/storage/sqlite-decision-repository.js +0 -232
- package/dist/infrastructure/storage/sqlite-embedding-repository.js +0 -311
- package/dist/infrastructure/storage/sqlite-knowledge-graph-repository.js +0 -417
- package/dist/infrastructure/storage/sqlite-knowledge-preservation-repository.js +0 -230
- package/dist/infrastructure/storage/sqlite-memory-repository.js +0 -229
- package/dist/infrastructure/storage/sqlite-pattern-repository.js +0 -249
- package/dist/infrastructure/storage/task-helpers.js +0 -197
- package/dist/infrastructure/telemetry/telemetry-service.js +0 -167
- package/dist/infrastructure/ux/appropriate-friction.js +0 -213
- package/dist/infrastructure/ux/bi-directional-learning.js +0 -337
- package/dist/infrastructure/ux/capability-boundaries.js +0 -42
- package/dist/infrastructure/ux/comprehensive-formatter.js +0 -117
- package/dist/infrastructure/ux/contextual-feature-discovery.js +0 -10
- package/dist/infrastructure/ux/expectation-management.js +0 -327
- package/dist/infrastructure/ux/feature-discovery.js +0 -14
- package/dist/infrastructure/ux/feedback-prompt.js +0 -45
- package/dist/infrastructure/ux/hitl-validation.js +0 -297
- package/dist/infrastructure/ux/progress-indicator.js +0 -116
- package/dist/infrastructure/ux/progressive-disclosure.js +0 -236
- package/dist/infrastructure/ux/review-checkpoint.js +0 -72
- package/dist/infrastructure/ux/staged-feature-intro.js +0 -24
- package/dist/infrastructure/ux/stream-markdown.js +0 -151
- package/dist/infrastructure/ux/syntax-highlighter.js +0 -91
- package/dist/infrastructure/ux/team-standards.js +0 -196
- package/dist/infrastructure/ux/transparency.js +0 -71
- package/dist/infrastructure/validation/agent-feedback.js +0 -55
- package/dist/infrastructure/validation/agent-validation-helper.js +0 -137
- package/dist/infrastructure/validation/review-handler.js +0 -40
- package/dist/infrastructure/validation/summary-aggregator.js +0 -70
- package/dist/infrastructure/validation/summary-extractor.js +0 -45
- package/dist/infrastructure/validation/trace-summary.js +0 -52
- package/dist/infrastructure/verification/verification-service.js +0 -47
- package/dist/infrastructure/xai/attention-visualizer.js +0 -172
- package/dist/infrastructure/xai/cot-visualizer.js +0 -283
- package/dist/infrastructure/xai/decision-tree-log.js +0 -212
- package/dist/infrastructure/xai/interactive-xai.js +0 -260
- package/dist/infrastructure/xai/uncertainty-quantifier.js +0 -198
- package/dist/intelligent-retrieval.js +0 -272
- package/dist/intent/classifier.js +0 -2
- package/dist/intent/context.js +0 -2
- package/dist/iterative-refinement.js +0 -189
- package/dist/learning.js +0 -232
- package/dist/log.js +0 -31
- package/dist/migrate.js +0 -133
- package/dist/parallel-processor.js +0 -534
- package/dist/planner.js +0 -198
- package/dist/policy.js +0 -26
- package/dist/presentation/cli/commands/ask-command.js +0 -314
- package/dist/presentation/cli/commands/cost-command.js +0 -191
- package/dist/presentation/cli/commands/decision-command.js +0 -207
- package/dist/presentation/cli/commands/docs-command.js +0 -25
- package/dist/presentation/cli/commands/fix-command.js +0 -53
- package/dist/presentation/cli/commands/knowledge-command.js +0 -129
- package/dist/presentation/cli/commands/learning-dashboard-command.js +0 -183
- package/dist/presentation/cli/commands/models-command.js +0 -194
- package/dist/presentation/cli/commands/orchestrate-command.js +0 -207
- package/dist/presentation/cli/commands/plan-command.js +0 -246
- package/dist/presentation/cli/commands/product-command.js +0 -199
- package/dist/presentation/cli/commands/react-command.js +0 -181
- package/dist/presentation/cli/commands/review-command.js +0 -154
- package/dist/presentation/cli/commands/run-command.js +0 -188
- package/dist/presentation/cli/commands/smart-command.js +0 -395
- package/dist/presentation/cli/commands/suggest-command.js +0 -138
- package/dist/presentation/cli/commands/trace-command.js +0 -129
- package/dist/presentation/cli/commands/undo-command.js +0 -150
- package/dist/query-optimizer.js +0 -205
- package/dist/refactor.js +0 -116
- package/dist/review.js +0 -206
- package/dist/search.js +0 -163
- package/dist/session.js +0 -704
- package/dist/task-verification.js +0 -336
- package/dist/testgen.js +0 -166
- package/dist/token-optimizer.js +0 -277
- package/dist/tools/bash.js +0 -213
- package/dist/tools/edit.js +0 -235
- package/dist/tools/executor.js +0 -153
- package/dist/tools/git-tool.js +0 -292
- package/dist/tools/parser.js +0 -158
- package/dist/tools/registry.js +0 -302
- package/dist/tools/search.js +0 -100
- package/dist/types.js +0 -2
- package/dist/utils/check-api-status.js +0 -100
- package/dist/utils/diff-parser.js +0 -162
- package/dist/verification/backup.js +0 -259
- package/dist/verification/compiler.js +0 -264
- package/dist/verification/index.js +0 -273
- package/dist/verification/test-runner.js +0 -379
|
@@ -0,0 +1,1827 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codehere web UI — two pages, one design system.
|
|
3
|
+
*
|
|
4
|
+
* / → TASK_HTML (v0.3: task intake + active session view)
|
|
5
|
+
* /history → HISTORY_HTML (audit feed across all agents — what I built Day 0)
|
|
6
|
+
*
|
|
7
|
+
* Plain JS, no framework. Both pages share the codehere.uk design system,
|
|
8
|
+
* centralized in SHARED_STYLES below — sourced from the actual
|
|
9
|
+
* ~/code/codehere-landing/app/globals.css (not guessed): HSL tokens matching
|
|
10
|
+
* the marketing site, keyframes (shimmer, badge-glow, beam-pulse, fade-up),
|
|
11
|
+
* and utility classes (.glass-card, .gradient-text-shimmer, .glow-line,
|
|
12
|
+
* .hero-beam, .card-lights).
|
|
13
|
+
*/
|
|
14
|
+
const SHARED_STYLES = `
|
|
15
|
+
:root {
|
|
16
|
+
/* HSL bg/fg matching landing globals.css dark palette */
|
|
17
|
+
--bg: hsl(0 0% 3.9%);
|
|
18
|
+
--bg-1: hsl(0 0% 6%);
|
|
19
|
+
--bg-2: hsl(0 0% 9%);
|
|
20
|
+
--fg: hsl(0 0% 98%);
|
|
21
|
+
--fg-70: rgba(255,255,255,0.70);
|
|
22
|
+
--fg-50: rgba(255,255,255,0.50);
|
|
23
|
+
--fg-40: rgba(255,255,255,0.40);
|
|
24
|
+
--fg-30: rgba(255,255,255,0.30);
|
|
25
|
+
--fg-20: rgba(255,255,255,0.20);
|
|
26
|
+
--border: rgba(255,255,255,0.10);
|
|
27
|
+
--border-soft: rgba(255,255,255,0.06);
|
|
28
|
+
--accent: #ff9a76;
|
|
29
|
+
--accent-blue: #4a90e2;
|
|
30
|
+
--accent-wash: rgba(255,154,118,0.10);
|
|
31
|
+
--allow: #28c840;
|
|
32
|
+
--block: #ff5f57;
|
|
33
|
+
--warn: #febc2e;
|
|
34
|
+
--radius: 8px;
|
|
35
|
+
--sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
36
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Keyframes */
|
|
40
|
+
@keyframes shimmer {
|
|
41
|
+
0% { background-position: -200% center; }
|
|
42
|
+
100% { background-position: 200% center; }
|
|
43
|
+
}
|
|
44
|
+
@keyframes badge-glow {
|
|
45
|
+
0%, 100% { box-shadow: 0 0 0 rgba(40,200,64,0); }
|
|
46
|
+
50% { box-shadow: 0 0 10px rgba(40,200,64,0.5); }
|
|
47
|
+
}
|
|
48
|
+
@keyframes fade-up {
|
|
49
|
+
from { opacity: 0; transform: translateY(12px); filter: blur(2px); }
|
|
50
|
+
to { opacity: 1; transform: translateY(0); filter: blur(0); }
|
|
51
|
+
}
|
|
52
|
+
@keyframes beam-pulse {
|
|
53
|
+
0%, 100% { opacity: 0.35; transform: translateX(-50%) scale(1); }
|
|
54
|
+
50% { opacity: 0.55; transform: translateX(-50%) scale(1.04); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Text gradient (landing's hero motif) */
|
|
58
|
+
.gradient-text-shimmer {
|
|
59
|
+
background: linear-gradient(
|
|
60
|
+
90deg,
|
|
61
|
+
#ff9a76 0%, #f5c6b0 20%, #ff9a76 35%,
|
|
62
|
+
#4a90e2 50%, #8ab8ee 65%, #4a90e2 80%, #ff9a76 100%
|
|
63
|
+
);
|
|
64
|
+
background-size: 200% auto;
|
|
65
|
+
-webkit-background-clip: text;
|
|
66
|
+
background-clip: text;
|
|
67
|
+
-webkit-text-fill-color: transparent;
|
|
68
|
+
animation: shimmer 6s linear infinite;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Glass card — landing's .glass-card with hover lift */
|
|
72
|
+
.glass-card {
|
|
73
|
+
background: rgba(255,255,255,0.02);
|
|
74
|
+
backdrop-filter: blur(16px);
|
|
75
|
+
-webkit-backdrop-filter: blur(16px);
|
|
76
|
+
border: 1px solid var(--border);
|
|
77
|
+
transition: border-color 0.3s ease, transform 0.3s cubic-bezier(0.16,1,0.3,1), box-shadow 0.3s ease;
|
|
78
|
+
}
|
|
79
|
+
.glass-card:hover {
|
|
80
|
+
border-color: rgba(255,255,255,0.15);
|
|
81
|
+
transform: translateY(-2px);
|
|
82
|
+
box-shadow: 0 12px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.04);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Horizontal divider — peach→transparent gradient */
|
|
86
|
+
.glow-line {
|
|
87
|
+
height: 1px;
|
|
88
|
+
background: linear-gradient(90deg, transparent 0%, rgba(255,154,118,0.25) 50%, transparent 100%);
|
|
89
|
+
margin: 18px 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Per-card traffic lights — mini terminal frame */
|
|
93
|
+
.card-lights {
|
|
94
|
+
display: inline-flex;
|
|
95
|
+
gap: 5px;
|
|
96
|
+
margin-right: 10px;
|
|
97
|
+
vertical-align: middle;
|
|
98
|
+
}
|
|
99
|
+
.card-lights span {
|
|
100
|
+
width: 9px; height: 9px; border-radius: 50%;
|
|
101
|
+
display: inline-block;
|
|
102
|
+
}
|
|
103
|
+
.card-lights .r { background: var(--block); }
|
|
104
|
+
.card-lights .y { background: var(--warn); }
|
|
105
|
+
.card-lights .g { background: var(--allow); }
|
|
106
|
+
|
|
107
|
+
/* Ambient hero beam (subtle) */
|
|
108
|
+
.hero-beam {
|
|
109
|
+
position: absolute;
|
|
110
|
+
inset: 0;
|
|
111
|
+
overflow: hidden;
|
|
112
|
+
pointer-events: none;
|
|
113
|
+
z-index: 0;
|
|
114
|
+
}
|
|
115
|
+
.hero-beam::before {
|
|
116
|
+
content: '';
|
|
117
|
+
position: absolute;
|
|
118
|
+
top: -20%;
|
|
119
|
+
left: 50%;
|
|
120
|
+
transform: translateX(-50%);
|
|
121
|
+
width: 75%;
|
|
122
|
+
height: 55%;
|
|
123
|
+
background: radial-gradient(ellipse at center,
|
|
124
|
+
rgba(255,154,118,0.08) 0%,
|
|
125
|
+
rgba(74,144,226,0.04) 40%,
|
|
126
|
+
transparent 70%);
|
|
127
|
+
filter: blur(50px);
|
|
128
|
+
animation: beam-pulse 12s ease-in-out infinite;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Running-state pill animation */
|
|
132
|
+
.badge-running-glow { animation: badge-glow 2.4s ease-in-out infinite; }
|
|
133
|
+
|
|
134
|
+
/* Entrance anim */
|
|
135
|
+
.fade-up { animation: fade-up 0.5s cubic-bezier(0.16,1,0.3,1) both; }
|
|
136
|
+
|
|
137
|
+
/* Pulsing dot — for "live" indicators on collapsed running sessions */
|
|
138
|
+
@keyframes dot-pulse {
|
|
139
|
+
0%, 100% { opacity: 0.45; transform: scale(1); }
|
|
140
|
+
50% { opacity: 1; transform: scale(1.18); }
|
|
141
|
+
}
|
|
142
|
+
.live-dot {
|
|
143
|
+
display: inline-block; width: 7px; height: 7px; border-radius: 50%;
|
|
144
|
+
background: var(--allow); box-shadow: 0 0 6px rgba(40,200,64,0.6);
|
|
145
|
+
animation: dot-pulse 1.4s ease-in-out infinite;
|
|
146
|
+
margin-right: 7px; vertical-align: middle;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ── Audit summary row (Gate B3 — moat visible on main page) ────────
|
|
150
|
+
Principle #3 status-at-a-glance + #7 agent-provenance + #6 marketing voice.
|
|
151
|
+
Dot-separated pills matching codehere.uk tone: "Today · 47 actions · 3 blocked · avg trust 91". */
|
|
152
|
+
.audit-summary {
|
|
153
|
+
display: flex; align-items: center; gap: 10px;
|
|
154
|
+
padding: 10px 14px;
|
|
155
|
+
margin: 0 0 10px;
|
|
156
|
+
border: 1px solid var(--border-soft);
|
|
157
|
+
background: rgba(255,154,118,0.03);
|
|
158
|
+
border-radius: 8px;
|
|
159
|
+
font-family: var(--mono); font-size: 11px;
|
|
160
|
+
color: var(--fg-70);
|
|
161
|
+
flex-wrap: wrap;
|
|
162
|
+
}
|
|
163
|
+
.audit-summary .eyebrow {
|
|
164
|
+
color: var(--accent); font-weight: 600; letter-spacing: 0.04em;
|
|
165
|
+
}
|
|
166
|
+
.audit-summary b { color: var(--fg); font-weight: 600; }
|
|
167
|
+
.audit-summary .blocked b { color: var(--block); }
|
|
168
|
+
.audit-summary .sep { color: var(--fg-20); user-select: none; }
|
|
169
|
+
.audit-summary.empty { display: none; }
|
|
170
|
+
|
|
171
|
+
/* ── Hook safety pill on running cards (Gate B1 — safety moat visible live) ──
|
|
172
|
+
Principle #7 agent-provenance. Surfaces the safety layer continuously, not only on block. */
|
|
173
|
+
.safety-pill {
|
|
174
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
175
|
+
margin-left: 10px;
|
|
176
|
+
padding: 2px 9px;
|
|
177
|
+
border-radius: 999px;
|
|
178
|
+
font-family: var(--mono); font-size: 10px;
|
|
179
|
+
color: var(--fg-50);
|
|
180
|
+
border: 1px solid var(--border-soft);
|
|
181
|
+
background: rgba(255,255,255,0.02);
|
|
182
|
+
}
|
|
183
|
+
.safety-pill .shield { color: var(--allow); }
|
|
184
|
+
.safety-pill .safe { color: var(--fg); font-weight: 600; }
|
|
185
|
+
.safety-pill .blocked { color: var(--block); font-weight: 600; }
|
|
186
|
+
.safety-pill.has-blocked { border-color: rgba(255,95,87,0.25); background: rgba(255,95,87,0.04); }
|
|
187
|
+
|
|
188
|
+
/* ── Status-at-a-glance banner (PM UX Layer 7) ──────────────────────
|
|
189
|
+
Principles #3 "status at a glance", #4 "multi-session default".
|
|
190
|
+
One row answers: is anything running/detected/blocked, when did code last change. */
|
|
191
|
+
.status-banner {
|
|
192
|
+
display: flex; align-items: center; gap: 16px;
|
|
193
|
+
padding: 10px 14px;
|
|
194
|
+
margin: 0 0 18px;
|
|
195
|
+
border: 1px solid var(--border);
|
|
196
|
+
background: rgba(255,255,255,0.02);
|
|
197
|
+
border-radius: 8px;
|
|
198
|
+
font-family: var(--mono); font-size: 11px;
|
|
199
|
+
color: var(--fg-50);
|
|
200
|
+
flex-wrap: wrap;
|
|
201
|
+
}
|
|
202
|
+
.status-banner .stat { display: inline-flex; align-items: center; gap: 6px; }
|
|
203
|
+
.status-banner .stat .label { color: var(--fg-40); text-transform: uppercase; letter-spacing: 0.1em; font-size: 10px; }
|
|
204
|
+
.status-banner .stat .value { color: var(--fg); font-weight: 600; }
|
|
205
|
+
.status-banner .stat .value.live { color: var(--allow); }
|
|
206
|
+
.status-banner .stat.blocked .value.nonzero { color: var(--block); }
|
|
207
|
+
.status-banner .sep { color: var(--fg-20); user-select: none; }
|
|
208
|
+
.status-banner .edge {
|
|
209
|
+
margin-left: auto;
|
|
210
|
+
color: var(--fg-50);
|
|
211
|
+
}
|
|
212
|
+
.status-banner .edge .value { color: var(--accent); font-weight: 400; }
|
|
213
|
+
|
|
214
|
+
/* ── Steps strip in collapsed card (PM UX Layer 2) ──────────────────
|
|
215
|
+
Principles #1 "web is not a terminal", #2 "info density".
|
|
216
|
+
Compact list of recent semantic steps instead of raw event chatter. */
|
|
217
|
+
.steps-mini {
|
|
218
|
+
margin: 6px 0 0;
|
|
219
|
+
display: flex; flex-direction: column; gap: 2px;
|
|
220
|
+
font-family: var(--mono); font-size: 11px;
|
|
221
|
+
}
|
|
222
|
+
.steps-mini .step {
|
|
223
|
+
display: flex; align-items: center; gap: 8px;
|
|
224
|
+
color: var(--fg-70);
|
|
225
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
226
|
+
}
|
|
227
|
+
.steps-mini .step .ico {
|
|
228
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
229
|
+
width: 13px; height: 13px; flex-shrink: 0;
|
|
230
|
+
font-size: 10px; font-weight: 700;
|
|
231
|
+
}
|
|
232
|
+
.steps-mini .step.done .ico { color: var(--allow); }
|
|
233
|
+
.steps-mini .step.active .ico { color: var(--accent); animation: dot-pulse 1.4s ease-in-out infinite; }
|
|
234
|
+
.steps-mini .step.failed .ico { color: var(--block); }
|
|
235
|
+
.steps-mini .step.pending .ico { color: var(--fg-40); }
|
|
236
|
+
.steps-mini .more {
|
|
237
|
+
color: var(--fg-40); font-size: 10px; letter-spacing: 0.06em;
|
|
238
|
+
padding-left: 21px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* ── Final report block (PM UX Layer 3) ─────────────────────────────
|
|
242
|
+
Principles #2 "info density", #7 "agent-provenance".
|
|
243
|
+
Shows up inside an expanded card when status='completed'. */
|
|
244
|
+
.final-report {
|
|
245
|
+
margin: 10px 0 0;
|
|
246
|
+
padding: 12px 14px;
|
|
247
|
+
border: 1px solid var(--border);
|
|
248
|
+
background: rgba(255,154,118,0.04);
|
|
249
|
+
border-radius: 8px;
|
|
250
|
+
font-family: var(--mono); font-size: 11px;
|
|
251
|
+
}
|
|
252
|
+
.final-report .fr-label {
|
|
253
|
+
color: var(--fg-40); text-transform: uppercase; letter-spacing: 0.12em; font-size: 10px;
|
|
254
|
+
margin-bottom: 8px;
|
|
255
|
+
}
|
|
256
|
+
.final-report .fr-files { display: flex; flex-direction: column; gap: 3px; }
|
|
257
|
+
.final-report .fr-file {
|
|
258
|
+
display: grid;
|
|
259
|
+
grid-template-columns: 1fr auto auto;
|
|
260
|
+
gap: 12px;
|
|
261
|
+
color: var(--fg-70);
|
|
262
|
+
}
|
|
263
|
+
.final-report .fr-file .path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
264
|
+
.final-report .fr-file .lines { color: var(--fg-50); white-space: nowrap; }
|
|
265
|
+
.final-report .fr-file .lines .add { color: var(--allow); }
|
|
266
|
+
.final-report .fr-file .lines .rem { color: var(--block); }
|
|
267
|
+
.final-report .fr-file .trust {
|
|
268
|
+
display: inline-block;
|
|
269
|
+
padding: 1px 7px; border-radius: 999px;
|
|
270
|
+
font-size: 10px; font-weight: 600;
|
|
271
|
+
border: 1px solid transparent;
|
|
272
|
+
min-width: 28px; text-align: center;
|
|
273
|
+
}
|
|
274
|
+
.final-report .fr-file .trust.good { color: var(--allow); background: rgba(40,200,64,0.08); border-color: rgba(40,200,64,0.22); }
|
|
275
|
+
.final-report .fr-file .trust.warn { color: var(--warn); background: rgba(254,188,46,0.08); border-color: rgba(254,188,46,0.25); }
|
|
276
|
+
.final-report .fr-file .trust.bad { color: var(--block); background: rgba(255,95,87,0.08); border-color: rgba(255,95,87,0.25); }
|
|
277
|
+
.final-report .fr-file .trust.none { color: var(--fg-40); background: transparent; border-color: var(--border-soft); }
|
|
278
|
+
.final-report .fr-tests, .final-report .fr-none {
|
|
279
|
+
margin-top: 8px; padding-top: 8px;
|
|
280
|
+
border-top: 1px solid var(--border-soft);
|
|
281
|
+
color: var(--fg-50);
|
|
282
|
+
}
|
|
283
|
+
.final-report .fr-tests .pass { color: var(--allow); }
|
|
284
|
+
.final-report .fr-tests .fail { color: var(--block); }
|
|
285
|
+
|
|
286
|
+
/* Floating pill nav — landing's FloatingNav visual */
|
|
287
|
+
.floating-nav {
|
|
288
|
+
position: fixed; top: 18px; left: 50%; transform: translateX(-50%);
|
|
289
|
+
z-index: 50;
|
|
290
|
+
display: flex; align-items: center; gap: 24px;
|
|
291
|
+
padding: 10px 20px;
|
|
292
|
+
border-radius: 999px;
|
|
293
|
+
border: 1px solid rgba(255,255,255,0.10);
|
|
294
|
+
background: rgba(255,255,255,0.03);
|
|
295
|
+
backdrop-filter: blur(20px);
|
|
296
|
+
-webkit-backdrop-filter: blur(20px);
|
|
297
|
+
transition: background 0.3s ease, box-shadow 0.3s ease;
|
|
298
|
+
}
|
|
299
|
+
.floating-nav:hover { background: rgba(0,0,0,0.6); box-shadow: 0 8px 32px rgba(0,0,0,0.5); }
|
|
300
|
+
.floating-nav .logo {
|
|
301
|
+
font-family: var(--sans); font-weight: 800; font-size: 14px;
|
|
302
|
+
background: linear-gradient(135deg, #ff9a76 0%, #4a90e2 100%);
|
|
303
|
+
-webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
|
|
304
|
+
}
|
|
305
|
+
.floating-nav a.nav-link {
|
|
306
|
+
font-family: var(--mono); font-size: 12px; color: var(--fg-50);
|
|
307
|
+
text-decoration: none; transition: color 0.2s ease;
|
|
308
|
+
}
|
|
309
|
+
.floating-nav a.nav-link:hover { color: var(--fg); }
|
|
310
|
+
.floating-nav a.nav-link.active { color: var(--fg); }
|
|
311
|
+
`;
|
|
312
|
+
// ── /history — audit feed (what we shipped earlier in this session) ────────────
|
|
313
|
+
export const HISTORY_HTML = `<!doctype html>
|
|
314
|
+
<html lang="en">
|
|
315
|
+
<head>
|
|
316
|
+
<meta charset="utf-8" />
|
|
317
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
318
|
+
<title>codehere · Mission Control</title>
|
|
319
|
+
<style>
|
|
320
|
+
${SHARED_STYLES}
|
|
321
|
+
* { box-sizing: border-box; }
|
|
322
|
+
html, body { height: 100%; }
|
|
323
|
+
body {
|
|
324
|
+
margin: 0;
|
|
325
|
+
background: var(--bg);
|
|
326
|
+
color: var(--fg);
|
|
327
|
+
font-family: var(--sans);
|
|
328
|
+
font-size: 13px;
|
|
329
|
+
line-height: 1.55;
|
|
330
|
+
-webkit-font-smoothing: antialiased;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* ── Header (marketing-hero motif: traffic-light dots) ─────────────── */
|
|
334
|
+
header {
|
|
335
|
+
position: sticky; top: 0; z-index: 10;
|
|
336
|
+
background: rgba(0,0,0,0.75);
|
|
337
|
+
backdrop-filter: blur(12px);
|
|
338
|
+
-webkit-backdrop-filter: blur(12px);
|
|
339
|
+
border-bottom: 1px solid var(--border);
|
|
340
|
+
padding: 14px 24px;
|
|
341
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
342
|
+
}
|
|
343
|
+
header .brand { display: flex; align-items: center; gap: 14px; }
|
|
344
|
+
header .lights { display: inline-flex; gap: 6px; }
|
|
345
|
+
header .lights span {
|
|
346
|
+
width: 11px; height: 11px; border-radius: 50%;
|
|
347
|
+
background: var(--bg-2);
|
|
348
|
+
border: 1px solid var(--border);
|
|
349
|
+
}
|
|
350
|
+
header .lights .r { background: var(--block); border-color: transparent; }
|
|
351
|
+
header .lights .y { background: var(--warn); border-color: transparent; }
|
|
352
|
+
header .lights .g { background: var(--allow); border-color: transparent; }
|
|
353
|
+
header .title {
|
|
354
|
+
font-family: var(--mono);
|
|
355
|
+
font-size: 13px; font-weight: 600;
|
|
356
|
+
letter-spacing: 0.01em;
|
|
357
|
+
}
|
|
358
|
+
header .title .muted { color: var(--fg-40); font-weight: 400; margin-left: 6px; }
|
|
359
|
+
header .meta {
|
|
360
|
+
display: flex; align-items: center; gap: 14px;
|
|
361
|
+
font-family: var(--mono);
|
|
362
|
+
font-size: 11px; color: var(--fg-50);
|
|
363
|
+
}
|
|
364
|
+
header .meta .dot {
|
|
365
|
+
display: inline-block; width: 6px; height: 6px; border-radius: 50%;
|
|
366
|
+
background: var(--fg-30); margin-right: 6px; vertical-align: middle;
|
|
367
|
+
}
|
|
368
|
+
header .meta .dot.live { background: var(--allow); box-shadow: 0 0 8px rgba(40,200,64,0.6); }
|
|
369
|
+
header .eyebrow {
|
|
370
|
+
display: inline-block;
|
|
371
|
+
font-family: var(--mono);
|
|
372
|
+
font-size: 10px; letter-spacing: 0.12em;
|
|
373
|
+
color: var(--fg-40);
|
|
374
|
+
text-transform: uppercase;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* ── Tagline under header ───────────────────────────────────────────── */
|
|
378
|
+
.tagline {
|
|
379
|
+
padding: 14px 24px 0;
|
|
380
|
+
font-family: var(--mono); font-size: 12px; color: var(--fg-50);
|
|
381
|
+
letter-spacing: 0.01em;
|
|
382
|
+
}
|
|
383
|
+
.tagline b { color: var(--fg-70); font-weight: 600; }
|
|
384
|
+
|
|
385
|
+
/* ── Status strip (answers A/B/C/D in one row) ──────────────────────── */
|
|
386
|
+
.status {
|
|
387
|
+
margin: 14px 24px 4px;
|
|
388
|
+
padding: 12px 16px;
|
|
389
|
+
border: 1px solid var(--border);
|
|
390
|
+
background: rgba(255,255,255,0.02);
|
|
391
|
+
border-radius: 8px;
|
|
392
|
+
display: flex; gap: 22px; flex-wrap: wrap;
|
|
393
|
+
font-family: var(--mono); font-size: 12px;
|
|
394
|
+
}
|
|
395
|
+
.status .stat { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
396
|
+
.status .stat .label { color: var(--fg-40); text-transform: uppercase; letter-spacing: 0.1em; font-size: 10px; }
|
|
397
|
+
.status .stat .value { color: var(--fg); font-weight: 600; }
|
|
398
|
+
.status .stat.blocked .value.nonzero { color: var(--block); }
|
|
399
|
+
.status .stat .value.live { color: var(--allow); }
|
|
400
|
+
.status .stat.latest { flex: 1 1 260px; min-width: 200px; }
|
|
401
|
+
.status .stat.latest .value { color: var(--accent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 400; }
|
|
402
|
+
.status .stat.latest .ago { color: var(--fg-40); font-weight: 400; }
|
|
403
|
+
.status .sep { color: var(--fg-20); user-select: none; }
|
|
404
|
+
|
|
405
|
+
/* ── Section label ─────────────────────────────────────────────────── */
|
|
406
|
+
.section-label {
|
|
407
|
+
padding: 18px 24px 6px;
|
|
408
|
+
font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em;
|
|
409
|
+
color: var(--fg-40); text-transform: uppercase;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* ── Feed ──────────────────────────────────────────────────────────── */
|
|
413
|
+
main { padding-bottom: 48px; }
|
|
414
|
+
#feed { list-style: none; margin: 0; padding: 0; }
|
|
415
|
+
#feed li {
|
|
416
|
+
padding: 11px 24px;
|
|
417
|
+
border-bottom: 1px solid var(--border-soft);
|
|
418
|
+
display: grid;
|
|
419
|
+
grid-template-columns: 88px 64px 80px 1fr 70px;
|
|
420
|
+
gap: 14px;
|
|
421
|
+
align-items: baseline;
|
|
422
|
+
transition: background 120ms ease;
|
|
423
|
+
}
|
|
424
|
+
#feed li:hover { background: rgba(255,255,255,0.02); }
|
|
425
|
+
#feed li .ts { color: var(--fg-40); font-family: var(--mono); font-size: 11px; }
|
|
426
|
+
#feed li .outcome {
|
|
427
|
+
font-family: var(--mono); font-size: 11px; font-weight: 600;
|
|
428
|
+
text-transform: uppercase; letter-spacing: 0.06em;
|
|
429
|
+
padding: 2px 8px; border-radius: 999px;
|
|
430
|
+
text-align: center;
|
|
431
|
+
border: 1px solid transparent;
|
|
432
|
+
}
|
|
433
|
+
#feed li .outcome.allow {
|
|
434
|
+
color: var(--allow);
|
|
435
|
+
background: rgba(40,200,64,0.08);
|
|
436
|
+
border-color: rgba(40,200,64,0.20);
|
|
437
|
+
}
|
|
438
|
+
#feed li .outcome.block {
|
|
439
|
+
color: var(--block);
|
|
440
|
+
background: rgba(255,95,87,0.10);
|
|
441
|
+
border-color: rgba(255,95,87,0.30);
|
|
442
|
+
}
|
|
443
|
+
#feed li .tool {
|
|
444
|
+
color: var(--accent);
|
|
445
|
+
font-family: var(--mono); font-size: 12px;
|
|
446
|
+
}
|
|
447
|
+
#feed li .summary {
|
|
448
|
+
color: var(--fg-70);
|
|
449
|
+
font-family: var(--mono); font-size: 12px;
|
|
450
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
451
|
+
}
|
|
452
|
+
#feed li .score {
|
|
453
|
+
text-align: right;
|
|
454
|
+
font-family: var(--mono); font-size: 11px;
|
|
455
|
+
color: var(--warn);
|
|
456
|
+
}
|
|
457
|
+
#feed li .score.zero { color: var(--fg-30); }
|
|
458
|
+
|
|
459
|
+
/* ── Empty state ───────────────────────────────────────────────────── */
|
|
460
|
+
#empty {
|
|
461
|
+
padding: 64px 24px;
|
|
462
|
+
text-align: center;
|
|
463
|
+
color: var(--fg-50);
|
|
464
|
+
}
|
|
465
|
+
#empty .pill {
|
|
466
|
+
display: inline-block;
|
|
467
|
+
padding: 6px 14px;
|
|
468
|
+
border: 1px solid var(--border);
|
|
469
|
+
border-radius: 999px;
|
|
470
|
+
background: rgba(255,255,255,0.03);
|
|
471
|
+
font-family: var(--mono); font-size: 11px;
|
|
472
|
+
color: var(--fg-70);
|
|
473
|
+
margin-bottom: 14px;
|
|
474
|
+
}
|
|
475
|
+
#empty .hint {
|
|
476
|
+
margin-top: 10px;
|
|
477
|
+
font-family: var(--mono); font-size: 11px; color: var(--fg-40);
|
|
478
|
+
}
|
|
479
|
+
code {
|
|
480
|
+
font-family: var(--mono);
|
|
481
|
+
background: rgba(255,255,255,0.05);
|
|
482
|
+
padding: 1px 6px; border-radius: 4px;
|
|
483
|
+
color: var(--fg);
|
|
484
|
+
}
|
|
485
|
+
</style>
|
|
486
|
+
</head>
|
|
487
|
+
<body>
|
|
488
|
+
<nav class="floating-nav">
|
|
489
|
+
<a href="/" class="logo">codehere</a>
|
|
490
|
+
<a href="/" class="nav-link">Tasks</a>
|
|
491
|
+
<a href="/history" class="nav-link active">History</a>
|
|
492
|
+
<span style="color:var(--fg-40);font-family:var(--mono);font-size:11px;">
|
|
493
|
+
<span id="live-dot" class="dot"></span><span id="conn">connecting</span>
|
|
494
|
+
</span>
|
|
495
|
+
</nav>
|
|
496
|
+
|
|
497
|
+
<div class="tagline" style="margin-top:90px">One feed. Every agent. Every action.</div>
|
|
498
|
+
|
|
499
|
+
<div class="status">
|
|
500
|
+
<div class="stat">
|
|
501
|
+
<span class="label">Sessions</span>
|
|
502
|
+
<span class="value live" id="stat-sessions">0</span>
|
|
503
|
+
</div>
|
|
504
|
+
<span class="sep">·</span>
|
|
505
|
+
<div class="stat blocked">
|
|
506
|
+
<span class="label">Blocked</span>
|
|
507
|
+
<span class="value" id="stat-blocked">0</span>
|
|
508
|
+
</div>
|
|
509
|
+
<span class="sep">·</span>
|
|
510
|
+
<div class="stat">
|
|
511
|
+
<span class="label">Events</span>
|
|
512
|
+
<span class="value" id="stat-events">0</span>
|
|
513
|
+
<span class="label">past hour</span>
|
|
514
|
+
</div>
|
|
515
|
+
<span class="sep">·</span>
|
|
516
|
+
<div class="stat latest">
|
|
517
|
+
<span class="label">Latest</span>
|
|
518
|
+
<span class="value" id="stat-latest">—</span>
|
|
519
|
+
<span class="ago" id="stat-latest-ago"></span>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<div class="section-label">Audit feed · Live</div>
|
|
524
|
+
<main>
|
|
525
|
+
<ul id="feed"></ul>
|
|
526
|
+
<div id="empty">
|
|
527
|
+
<div class="pill">No tool calls yet</div>
|
|
528
|
+
<div class="hint">
|
|
529
|
+
Install the hook → <code>ch hook install</code> — then any Claude Code, Cursor, or Codex tool call will appear here live.
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
</main>
|
|
533
|
+
|
|
534
|
+
<script>
|
|
535
|
+
(function () {
|
|
536
|
+
const feed = document.getElementById('feed');
|
|
537
|
+
const empty = document.getElementById('empty');
|
|
538
|
+
const countEl = document.getElementById('count');
|
|
539
|
+
const connEl = document.getElementById('conn');
|
|
540
|
+
const dot = document.getElementById('live-dot');
|
|
541
|
+
let count = 0;
|
|
542
|
+
|
|
543
|
+
function shortPath(p) {
|
|
544
|
+
const parts = String(p).split('/');
|
|
545
|
+
return parts.length <= 2 ? p : '…/' + parts.slice(-2).join('/');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function summarize(entry) {
|
|
549
|
+
if (entry.reason) return String(entry.reason);
|
|
550
|
+
if (entry.matched) return String(entry.matched);
|
|
551
|
+
|
|
552
|
+
const raw = entry.toolInput;
|
|
553
|
+
if (!raw) return String(entry.toolName || 'tool call');
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
const input = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
557
|
+
if (input.command) return String(input.command).replace(/\\n/g, ' ').slice(0, 140);
|
|
558
|
+
if (input.file_path) return shortPath(String(input.file_path));
|
|
559
|
+
if (Array.isArray(input.edits) && input.edits[0] && input.edits[0].file_path) {
|
|
560
|
+
return input.edits.length + ' edits → ' + shortPath(String(input.edits[0].file_path));
|
|
561
|
+
}
|
|
562
|
+
if (input.query) return String(input.query).slice(0, 140);
|
|
563
|
+
if (input.url) return String(input.url);
|
|
564
|
+
if (input.pattern) return 'grep ' + String(input.pattern).slice(0, 120);
|
|
565
|
+
if (input.path) return shortPath(String(input.path));
|
|
566
|
+
} catch { /* truncated JSON — regex fallback below */ }
|
|
567
|
+
|
|
568
|
+
const s = String(raw);
|
|
569
|
+
const fp = s.match(/"file_path"\\s*:\\s*"([^"]+)"/);
|
|
570
|
+
if (fp) return shortPath(fp[1]);
|
|
571
|
+
const cmd = s.match(/"command"\\s*:\\s*"([^"]{0,140})"/);
|
|
572
|
+
if (cmd) return cmd[1].replace(/\\\\n/g, ' ');
|
|
573
|
+
|
|
574
|
+
return String(entry.toolName || 'tool call');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function escape(s) {
|
|
578
|
+
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function render(entry) {
|
|
582
|
+
const ts = new Date(entry.timestamp || Date.now()).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
583
|
+
const blocked = Boolean(entry.blocked);
|
|
584
|
+
const outcome = blocked ? 'block' : 'allow';
|
|
585
|
+
const tool = String(entry.toolName || entry.tool_name || '—');
|
|
586
|
+
const score = entry.riskScore ?? entry.risk_score ?? 0;
|
|
587
|
+
const summary = summarize(entry);
|
|
588
|
+
|
|
589
|
+
const li = document.createElement('li');
|
|
590
|
+
li.innerHTML =
|
|
591
|
+
'<span class="ts">' + escape(ts) + '</span>' +
|
|
592
|
+
'<span class="outcome ' + outcome + '">' + outcome + '</span>' +
|
|
593
|
+
'<span class="tool">' + escape(tool) + '</span>' +
|
|
594
|
+
'<span class="summary" title="' + escape(summary) + '">' + escape(summary) + '</span>' +
|
|
595
|
+
'<span class="score ' + (score ? '' : 'zero') + '">' + (score ? score : '—') + '</span>';
|
|
596
|
+
feed.insertBefore(li, feed.firstChild);
|
|
597
|
+
count += 1;
|
|
598
|
+
countEl.textContent = String(count);
|
|
599
|
+
empty.style.display = 'none';
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ── Status strip (answers C/A/B/D) ───────────────────────────────────
|
|
603
|
+
const events = []; // accumulate for rolling-window stats
|
|
604
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
605
|
+
|
|
606
|
+
function parseTs(entry) {
|
|
607
|
+
const t = entry.timestamp;
|
|
608
|
+
if (typeof t === 'number') return t;
|
|
609
|
+
if (typeof t === 'string') { const n = Date.parse(t); return isNaN(n) ? Date.now() : n; }
|
|
610
|
+
return Date.now();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function agoString(ms) {
|
|
614
|
+
const s = Math.max(0, Math.floor((Date.now() - ms) / 1000));
|
|
615
|
+
if (s < 60) return s + 's ago';
|
|
616
|
+
if (s < 3600) return Math.floor(s / 60) + 'm ago';
|
|
617
|
+
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
|
|
618
|
+
return Math.floor(s / 86400) + 'd ago';
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function recomputeStats() {
|
|
622
|
+
const now = Date.now();
|
|
623
|
+
const cutoff = now - HOUR_MS;
|
|
624
|
+
const inWindow = events.filter(e => e._ts >= cutoff);
|
|
625
|
+
const sessions = new Set();
|
|
626
|
+
let blocked = 0;
|
|
627
|
+
for (const e of inWindow) {
|
|
628
|
+
if (e.sessionId) sessions.add(e.sessionId);
|
|
629
|
+
if (e.blocked) blocked += 1;
|
|
630
|
+
}
|
|
631
|
+
document.getElementById('stat-sessions').textContent = String(sessions.size);
|
|
632
|
+
const blockedEl = document.getElementById('stat-blocked');
|
|
633
|
+
blockedEl.textContent = String(blocked);
|
|
634
|
+
blockedEl.classList.toggle('nonzero', blocked > 0);
|
|
635
|
+
document.getElementById('stat-events').textContent = String(inWindow.length);
|
|
636
|
+
|
|
637
|
+
const latest = events[events.length - 1];
|
|
638
|
+
if (latest) {
|
|
639
|
+
const tool = String(latest.toolName || 'tool');
|
|
640
|
+
const sum = summarize(latest);
|
|
641
|
+
document.getElementById('stat-latest').textContent = tool + ' · ' + sum;
|
|
642
|
+
document.getElementById('stat-latest-ago').textContent = agoString(latest._ts);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const es = new EventSource('/events');
|
|
647
|
+
es.onopen = () => { dot.classList.add('live'); connEl.textContent = 'live'; };
|
|
648
|
+
es.onerror = () => { dot.classList.remove('live'); connEl.textContent = 'reconnecting'; };
|
|
649
|
+
es.onmessage = (ev) => {
|
|
650
|
+
try {
|
|
651
|
+
const entry = JSON.parse(ev.data);
|
|
652
|
+
entry._ts = parseTs(entry);
|
|
653
|
+
events.push(entry);
|
|
654
|
+
render(entry);
|
|
655
|
+
recomputeStats();
|
|
656
|
+
} catch { /* skip malformed */ }
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
// Keep "Xs/Xm ago" fresh even when no new events arrive
|
|
660
|
+
setInterval(() => { if (events.length) recomputeStats(); }, 1000);
|
|
661
|
+
})();
|
|
662
|
+
</script>
|
|
663
|
+
</body>
|
|
664
|
+
</html>`;
|
|
665
|
+
// ── / — task intake + active session view (v0.3 F1) ───────────────────────────
|
|
666
|
+
export const TASK_HTML = `<!doctype html>
|
|
667
|
+
<html lang="en">
|
|
668
|
+
<head>
|
|
669
|
+
<meta charset="utf-8" />
|
|
670
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
671
|
+
<title>codehere</title>
|
|
672
|
+
<style>
|
|
673
|
+
${SHARED_STYLES}
|
|
674
|
+
* { box-sizing: border-box; }
|
|
675
|
+
body { margin: 0; background: var(--bg); color: var(--fg); font-family: var(--sans); font-size: 13px; line-height: 1.55; -webkit-font-smoothing: antialiased; }
|
|
676
|
+
|
|
677
|
+
header {
|
|
678
|
+
position: sticky; top: 0; z-index: 10;
|
|
679
|
+
background: rgba(0,0,0,0.75); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
|
|
680
|
+
border-bottom: 1px solid var(--border);
|
|
681
|
+
padding: 14px 24px;
|
|
682
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
683
|
+
}
|
|
684
|
+
header .brand { display: flex; align-items: center; gap: 14px; }
|
|
685
|
+
header .lights { display: inline-flex; gap: 6px; }
|
|
686
|
+
header .lights span { width: 11px; height: 11px; border-radius: 50%; border: 1px solid transparent; }
|
|
687
|
+
header .lights .r { background: var(--block); }
|
|
688
|
+
header .lights .y { background: var(--warn); }
|
|
689
|
+
header .lights .g { background: var(--allow); }
|
|
690
|
+
header .title { font-family: var(--mono); font-size: 13px; font-weight: 600; }
|
|
691
|
+
header .title .muted { color: var(--fg-40); font-weight: 400; margin-left: 6px; }
|
|
692
|
+
header nav { display: flex; gap: 14px; font-family: var(--mono); font-size: 11px; }
|
|
693
|
+
header nav a { color: var(--fg-50); text-decoration: none; padding: 4px 8px; border-radius: 4px; }
|
|
694
|
+
header nav a:hover { color: var(--fg); background: rgba(255,255,255,0.04); }
|
|
695
|
+
header nav a.active { color: var(--fg); background: rgba(255,255,255,0.06); }
|
|
696
|
+
|
|
697
|
+
.shell { max-width: 960px; margin: 0 auto; padding: 90px 24px 24px; position: relative; }
|
|
698
|
+
|
|
699
|
+
/* ── Readiness banner (F1a) ─────────────────────────────────────────── */
|
|
700
|
+
.readiness {
|
|
701
|
+
margin: 12px 0 0;
|
|
702
|
+
padding: 14px 16px;
|
|
703
|
+
border-radius: 10px;
|
|
704
|
+
border: 1px solid var(--border);
|
|
705
|
+
background: var(--bg-1);
|
|
706
|
+
display: none;
|
|
707
|
+
}
|
|
708
|
+
.readiness[data-severity="warn"] {
|
|
709
|
+
border-color: rgba(254,188,46,0.35);
|
|
710
|
+
background: rgba(254,188,46,0.05);
|
|
711
|
+
}
|
|
712
|
+
.readiness[data-severity="blocker"] {
|
|
713
|
+
border-color: rgba(255,95,87,0.4);
|
|
714
|
+
background: rgba(255,95,87,0.06);
|
|
715
|
+
}
|
|
716
|
+
.readiness .r-title {
|
|
717
|
+
font-family: var(--mono); font-size: 12px; font-weight: 600; color: var(--fg);
|
|
718
|
+
margin-bottom: 4px;
|
|
719
|
+
}
|
|
720
|
+
.readiness[data-severity="warn"] .r-title { color: var(--warn); }
|
|
721
|
+
.readiness[data-severity="blocker"] .r-title { color: var(--block); }
|
|
722
|
+
.readiness .r-detail { font-family: var(--mono); font-size: 11px; color: var(--fg-70); line-height: 1.5; }
|
|
723
|
+
.readiness .r-action { margin-top: 8px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
724
|
+
.readiness .r-action code { background: rgba(255,255,255,0.06); padding: 3px 8px; border-radius: 4px; font-family: var(--mono); font-size: 11px; }
|
|
725
|
+
.readiness .r-action a { color: var(--accent); font-family: var(--mono); font-size: 11px; text-decoration: none; }
|
|
726
|
+
.readiness .r-action a:hover { text-decoration: underline; }
|
|
727
|
+
.readiness .r-action button {
|
|
728
|
+
background: transparent; color: var(--fg-70); font-family: var(--mono); font-size: 11px;
|
|
729
|
+
border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; cursor: pointer;
|
|
730
|
+
}
|
|
731
|
+
.readiness .r-action button:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
732
|
+
|
|
733
|
+
.intake { padding: 36px 0 24px; position: relative; z-index: 1; }
|
|
734
|
+
.intake h1 {
|
|
735
|
+
margin: 0 0 6px;
|
|
736
|
+
font-size: clamp(28px, 4vw, 44px);
|
|
737
|
+
font-weight: 900;
|
|
738
|
+
letter-spacing: -0.02em;
|
|
739
|
+
line-height: 1.08;
|
|
740
|
+
}
|
|
741
|
+
.intake p.sub { margin: 0 0 22px; color: var(--fg-50); font-size: 14px; max-width: 560px; }
|
|
742
|
+
.intake textarea {
|
|
743
|
+
width: 100%; min-height: 84px;
|
|
744
|
+
background: var(--bg-1); color: var(--fg);
|
|
745
|
+
border: 1px solid var(--border); border-radius: 10px;
|
|
746
|
+
padding: 14px 16px;
|
|
747
|
+
font-family: var(--mono); font-size: 13px; line-height: 1.55;
|
|
748
|
+
resize: vertical; outline: none;
|
|
749
|
+
}
|
|
750
|
+
.intake textarea:focus { border-color: rgba(255,154,118,0.4); }
|
|
751
|
+
.intake-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
|
|
752
|
+
.intake select, .intake button, .intake input.cwd {
|
|
753
|
+
font-family: var(--mono); font-size: 12px;
|
|
754
|
+
background: var(--bg-1); color: var(--fg);
|
|
755
|
+
border: 1px solid var(--border); border-radius: 8px;
|
|
756
|
+
padding: 8px 14px;
|
|
757
|
+
cursor: pointer;
|
|
758
|
+
}
|
|
759
|
+
.intake input.cwd {
|
|
760
|
+
cursor: text;
|
|
761
|
+
flex: 1 1 260px;
|
|
762
|
+
min-width: 200px;
|
|
763
|
+
}
|
|
764
|
+
.intake input.cwd:focus { outline: none; border-color: rgba(255,154,118,0.4); }
|
|
765
|
+
.intake input.cwd::placeholder { color: var(--fg-30); }
|
|
766
|
+
.intake button.spawn {
|
|
767
|
+
background: var(--accent); color: #000; font-weight: 600; border-color: var(--accent);
|
|
768
|
+
}
|
|
769
|
+
.intake button.spawn:hover { filter: brightness(1.05); }
|
|
770
|
+
.intake button.spawn[disabled] { opacity: 0.45; cursor: default; }
|
|
771
|
+
.intake .hint { margin-left: auto; color: var(--fg-40); font-family: var(--mono); font-size: 11px; }
|
|
772
|
+
|
|
773
|
+
/* ── Setup panel (onboarding — appears when a non-ready agent is picked) ───
|
|
774
|
+
Principle: codehere is the control plane from install → orchestrate → audit.
|
|
775
|
+
When the user picks an agent that isn't ready, we surface what to do
|
|
776
|
+
inline rather than fail at spawn time. */
|
|
777
|
+
.setup-panel {
|
|
778
|
+
margin: 10px 0 0;
|
|
779
|
+
padding: 12px 14px;
|
|
780
|
+
border: 1px solid var(--border);
|
|
781
|
+
border-radius: 8px;
|
|
782
|
+
background: rgba(254,188,46,0.04);
|
|
783
|
+
font-family: var(--mono); font-size: 11px;
|
|
784
|
+
color: var(--fg-70);
|
|
785
|
+
display: none;
|
|
786
|
+
}
|
|
787
|
+
.setup-panel[data-state="needs_install"] { border-color: rgba(74,144,226,0.3); background: rgba(74,144,226,0.05); }
|
|
788
|
+
.setup-panel[data-state="needs_auth"] { border-color: rgba(254,188,46,0.4); background: rgba(254,188,46,0.06); }
|
|
789
|
+
.setup-panel[data-state="billed_key_required"] { border-color: rgba(255,95,87,0.35); background: rgba(255,95,87,0.05); }
|
|
790
|
+
.setup-panel .sp-title {
|
|
791
|
+
font-weight: 600; color: var(--fg); margin-bottom: 6px;
|
|
792
|
+
}
|
|
793
|
+
.setup-panel[data-state="needs_install"] .sp-title { color: var(--accent-blue); }
|
|
794
|
+
.setup-panel[data-state="needs_auth"] .sp-title { color: var(--warn); }
|
|
795
|
+
.setup-panel[data-state="billed_key_required"] .sp-title { color: var(--block); }
|
|
796
|
+
.setup-panel code {
|
|
797
|
+
background: rgba(255,255,255,0.08); padding: 3px 8px; border-radius: 4px;
|
|
798
|
+
color: var(--fg); font-size: 11px;
|
|
799
|
+
}
|
|
800
|
+
.setup-panel a { color: var(--accent); text-decoration: none; }
|
|
801
|
+
.setup-panel a:hover { text-decoration: underline; }
|
|
802
|
+
.setup-panel .swap { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
|
803
|
+
.setup-panel .swap .swap-label { color: var(--fg-40); text-transform: uppercase; letter-spacing: 0.1em; font-size: 10px; }
|
|
804
|
+
.setup-panel .swap button {
|
|
805
|
+
font-family: var(--mono); font-size: 11px;
|
|
806
|
+
color: var(--accent); background: transparent;
|
|
807
|
+
border: 1px solid rgba(255,154,118,0.35); border-radius: 999px;
|
|
808
|
+
padding: 3px 10px; cursor: pointer;
|
|
809
|
+
}
|
|
810
|
+
.setup-panel .swap button:hover { color: var(--fg); background: rgba(255,154,118,0.1); border-color: var(--accent); }
|
|
811
|
+
|
|
812
|
+
/* F2 · Install button + streamed output (principle #10: codehere DOES the install) */
|
|
813
|
+
.setup-panel .install-row { margin-top: 10px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
814
|
+
.setup-panel .install-row button.install {
|
|
815
|
+
background: var(--accent); color: #000; font-weight: 600;
|
|
816
|
+
border: 1px solid var(--accent); border-radius: 8px;
|
|
817
|
+
padding: 8px 16px; cursor: pointer;
|
|
818
|
+
font-family: var(--sans); font-size: 12px;
|
|
819
|
+
}
|
|
820
|
+
.setup-panel .install-row button.install:hover { filter: brightness(1.05); }
|
|
821
|
+
.setup-panel .install-row button.install[disabled] { opacity: 0.5; cursor: default; }
|
|
822
|
+
.setup-panel .install-row button.install.running { background: transparent; color: var(--accent); }
|
|
823
|
+
.setup-panel .install-row button.install.done { background: rgba(40,200,64,0.1); color: var(--allow); border-color: rgba(40,200,64,0.35); }
|
|
824
|
+
.setup-panel .install-row button.install.failed { background: rgba(255,95,87,0.08); color: var(--block); border-color: rgba(255,95,87,0.35); }
|
|
825
|
+
.setup-panel .install-row .install-subtitle { color: var(--fg-50); font-size: 11px; font-family: var(--mono); }
|
|
826
|
+
.setup-panel .install-output {
|
|
827
|
+
margin-top: 10px; padding: 10px 12px;
|
|
828
|
+
background: #000; border: 1px solid var(--border-soft); border-radius: 6px;
|
|
829
|
+
font-family: var(--mono); font-size: 11px; color: var(--fg-70);
|
|
830
|
+
max-height: 220px; overflow-y: auto;
|
|
831
|
+
white-space: pre-wrap; word-break: break-word;
|
|
832
|
+
display: none;
|
|
833
|
+
}
|
|
834
|
+
.setup-panel .install-output.show { display: block; }
|
|
835
|
+
.setup-panel .install-output.has-err { border-color: rgba(255,95,87,0.3); }
|
|
836
|
+
.setup-panel .install-details-toggle {
|
|
837
|
+
font-family: var(--mono); font-size: 10px; color: var(--fg-40);
|
|
838
|
+
background: transparent; border: none; cursor: pointer;
|
|
839
|
+
padding: 2px 6px;
|
|
840
|
+
}
|
|
841
|
+
.setup-panel .install-details-toggle:hover { color: var(--fg-70); }
|
|
842
|
+
|
|
843
|
+
/* Per-option glyph in the select dropdown — rendered via the option text itself
|
|
844
|
+
since <option> can't be styled with pseudo-elements cross-browser. */
|
|
845
|
+
|
|
846
|
+
/* ── Starter prompts (Gate A1 — first-run path to success) ──────────
|
|
847
|
+
Principle #1 (web is not a terminal — a welcoming surface) + scope-discipline #9.
|
|
848
|
+
Three one-click prompts that let a stranger see codehere work in the project
|
|
849
|
+
they opened the daemon in, without needing to know what to ask. */
|
|
850
|
+
.starter-prompts {
|
|
851
|
+
display: flex; flex-wrap: wrap; gap: 6px;
|
|
852
|
+
margin: 10px 0 0;
|
|
853
|
+
}
|
|
854
|
+
.starter-prompts button {
|
|
855
|
+
font-family: var(--mono); font-size: 11px;
|
|
856
|
+
color: var(--fg-70);
|
|
857
|
+
border: 1px solid var(--border); border-radius: 999px;
|
|
858
|
+
background: rgba(255,255,255,0.02);
|
|
859
|
+
padding: 5px 12px; cursor: pointer;
|
|
860
|
+
transition: color .15s ease, border-color .15s ease, background .15s ease;
|
|
861
|
+
}
|
|
862
|
+
.starter-prompts button:hover {
|
|
863
|
+
color: var(--fg);
|
|
864
|
+
border-color: rgba(255,154,118,0.35);
|
|
865
|
+
background: rgba(255,154,118,0.06);
|
|
866
|
+
}
|
|
867
|
+
.starter-prompts .sp-label {
|
|
868
|
+
font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em;
|
|
869
|
+
color: var(--fg-40); text-transform: uppercase;
|
|
870
|
+
padding: 5px 2px 5px 0;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.section-label {
|
|
874
|
+
margin-top: 24px; margin-bottom: 8px;
|
|
875
|
+
font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em;
|
|
876
|
+
color: var(--fg-40); text-transform: uppercase;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.session {
|
|
880
|
+
border-radius: 12px;
|
|
881
|
+
margin-bottom: 12px;
|
|
882
|
+
overflow: hidden;
|
|
883
|
+
/* glass-card styling from shared tokens */
|
|
884
|
+
background: rgba(255,255,255,0.02);
|
|
885
|
+
backdrop-filter: blur(16px);
|
|
886
|
+
-webkit-backdrop-filter: blur(16px);
|
|
887
|
+
border: 1px solid var(--border);
|
|
888
|
+
transition: border-color 0.3s ease, transform 0.3s cubic-bezier(0.16,1,0.3,1), box-shadow 0.3s ease;
|
|
889
|
+
animation: fade-up 0.5s cubic-bezier(0.16,1,0.3,1) both;
|
|
890
|
+
}
|
|
891
|
+
.session:hover {
|
|
892
|
+
border-color: rgba(255,255,255,0.15);
|
|
893
|
+
transform: translateY(-2px);
|
|
894
|
+
box-shadow: 0 12px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.04);
|
|
895
|
+
}
|
|
896
|
+
.session-head {
|
|
897
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
898
|
+
padding: 12px 16px;
|
|
899
|
+
cursor: pointer;
|
|
900
|
+
user-select: none;
|
|
901
|
+
}
|
|
902
|
+
.session.expanded .session-head {
|
|
903
|
+
border-bottom: 1px solid var(--border-soft);
|
|
904
|
+
}
|
|
905
|
+
.session .chevron {
|
|
906
|
+
display: inline-block; margin-left: 8px; color: var(--fg-40);
|
|
907
|
+
font-family: var(--mono); font-size: 10px;
|
|
908
|
+
transition: transform 0.2s ease;
|
|
909
|
+
}
|
|
910
|
+
.session.expanded .chevron { transform: rotate(90deg); }
|
|
911
|
+
.session-body { display: none; }
|
|
912
|
+
.session.expanded .session-body { display: block; }
|
|
913
|
+
.session-head .meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
914
|
+
.session-head .prompt { font-family: var(--mono); font-size: 12px; color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 620px; }
|
|
915
|
+
.session-head .sub { font-family: var(--mono); font-size: 11px; color: var(--fg-40); }
|
|
916
|
+
.session-head .status-pill {
|
|
917
|
+
font-family: var(--mono); font-size: 10px; font-weight: 600;
|
|
918
|
+
text-transform: uppercase; letter-spacing: 0.08em;
|
|
919
|
+
padding: 3px 10px; border-radius: 999px;
|
|
920
|
+
border: 1px solid transparent;
|
|
921
|
+
}
|
|
922
|
+
.status-pill.running { color: var(--allow); background: rgba(40,200,64,0.08); border-color: rgba(40,200,64,0.25); }
|
|
923
|
+
.status-pill.completed { color: var(--fg-50); background: rgba(255,255,255,0.04); border-color: var(--border); }
|
|
924
|
+
.status-pill.killed { color: var(--warn); background: rgba(254,188,46,0.08); border-color: rgba(254,188,46,0.25); }
|
|
925
|
+
.status-pill.error { color: var(--block); background: rgba(255,95,87,0.10); border-color: rgba(255,95,87,0.30); }
|
|
926
|
+
.status-pill.observed-pill { color: var(--fg-50); background: rgba(255,255,255,0.03); border-color: var(--border); font-weight: 400; }
|
|
927
|
+
|
|
928
|
+
.session.observed { opacity: 0.88; }
|
|
929
|
+
.session.observed .session-head { border-bottom: none; }
|
|
930
|
+
|
|
931
|
+
.session-controls { display: flex; gap: 8px; }
|
|
932
|
+
.session-controls button {
|
|
933
|
+
font-family: var(--mono); font-size: 11px;
|
|
934
|
+
background: transparent; color: var(--fg-70);
|
|
935
|
+
border: 1px solid var(--border); border-radius: 6px;
|
|
936
|
+
padding: 4px 10px; cursor: pointer;
|
|
937
|
+
}
|
|
938
|
+
.session-controls button:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
939
|
+
.session-controls button.kill:hover { color: var(--block); border-color: rgba(255,95,87,0.4); }
|
|
940
|
+
|
|
941
|
+
.session-body { padding: 0 16px 14px; }
|
|
942
|
+
.event-log {
|
|
943
|
+
margin: 10px 0 0;
|
|
944
|
+
max-height: 360px; overflow-y: auto;
|
|
945
|
+
background: #000;
|
|
946
|
+
border: 1px solid var(--border-soft); border-radius: 8px;
|
|
947
|
+
padding: 10px 12px;
|
|
948
|
+
font-family: var(--mono); font-size: 11px; line-height: 1.6;
|
|
949
|
+
}
|
|
950
|
+
.event-log .event { color: var(--fg-70); white-space: pre-wrap; word-break: break-word; padding: 2px 0; }
|
|
951
|
+
.event-log .event .t { color: var(--fg-30); margin-right: 8px; }
|
|
952
|
+
.event-log .event .label { color: var(--fg-50); font-weight: 600; margin-right: 6px; }
|
|
953
|
+
.event-log .event.claude { color: var(--fg); }
|
|
954
|
+
.event-log .event.claude .label { color: var(--accent); }
|
|
955
|
+
.event-log .event.tool .label { color: var(--accent); }
|
|
956
|
+
.event-log .event.tool .tool-name { color: var(--accent); font-weight: 600; }
|
|
957
|
+
.event-log .event.result .label { color: var(--fg-50); }
|
|
958
|
+
.event-log .event.hook .label { color: var(--allow); }
|
|
959
|
+
.event-log .event.hook.blocked .label { color: var(--block); }
|
|
960
|
+
.event-log .event.done .label { color: var(--allow); }
|
|
961
|
+
.event-log .event.stderr { color: var(--warn); }
|
|
962
|
+
.event-log .event.stderr .label { color: var(--warn); }
|
|
963
|
+
.event-log .event.exit { color: var(--fg-50); font-style: italic; }
|
|
964
|
+
.event-log .event.error { color: var(--block); }
|
|
965
|
+
.event-log .event.error .label { color: var(--block); }
|
|
966
|
+
|
|
967
|
+
/* codehere:error events — structured recovery hints (F1b) */
|
|
968
|
+
.event-log .event.codehere-error {
|
|
969
|
+
margin: 6px -4px; padding: 10px 12px;
|
|
970
|
+
background: rgba(255,95,87,0.08); border: 1px solid rgba(255,95,87,0.3); border-radius: 6px;
|
|
971
|
+
color: var(--fg);
|
|
972
|
+
}
|
|
973
|
+
.event-log .event.codehere-error .label { color: var(--block); display: block; margin-bottom: 4px; }
|
|
974
|
+
.event-log .event.codehere-error .recovery { color: var(--fg-70); margin-top: 4px; display: block; }
|
|
975
|
+
|
|
976
|
+
#empty { padding: 48px 0; text-align: center; color: var(--fg-50); }
|
|
977
|
+
#empty .pill { display: inline-block; padding: 6px 14px; border: 1px solid var(--border); border-radius: 999px; background: rgba(255,255,255,0.03); font-family: var(--mono); font-size: 11px; color: var(--fg-70); }
|
|
978
|
+
</style>
|
|
979
|
+
</head>
|
|
980
|
+
<body>
|
|
981
|
+
<nav class="floating-nav">
|
|
982
|
+
<a href="/" class="logo">codehere</a>
|
|
983
|
+
<a href="/" class="nav-link active">Tasks</a>
|
|
984
|
+
<a href="/history" class="nav-link">History</a>
|
|
985
|
+
</nav>
|
|
986
|
+
|
|
987
|
+
<div class="shell">
|
|
988
|
+
|
|
989
|
+
<div class="hero-beam"></div>
|
|
990
|
+
|
|
991
|
+
<div class="readiness" id="readiness" style="position:relative;z-index:1"></div>
|
|
992
|
+
|
|
993
|
+
<div class="audit-summary empty" id="audit-summary" style="position:relative;z-index:1">
|
|
994
|
+
<span class="eyebrow">TODAY</span>
|
|
995
|
+
<span class="sep">·</span>
|
|
996
|
+
<span><b id="as-actions">0</b> actions</span>
|
|
997
|
+
<span class="sep">·</span>
|
|
998
|
+
<span class="blocked"><b id="as-blocked">0</b> blocked</span>
|
|
999
|
+
<span class="sep">·</span>
|
|
1000
|
+
<span>avg trust <b id="as-trust">—</b></span>
|
|
1001
|
+
<span class="sep">·</span>
|
|
1002
|
+
<span><b id="as-agents">0</b> agents</span>
|
|
1003
|
+
</div>
|
|
1004
|
+
|
|
1005
|
+
<div class="status-banner" id="status-banner" style="position:relative;z-index:1;display:none">
|
|
1006
|
+
<div class="stat"><span class="label">Running</span><span class="value live" id="sb-running">0</span></div>
|
|
1007
|
+
<span class="sep">·</span>
|
|
1008
|
+
<div class="stat"><span class="label">On machine</span><span class="value" id="sb-detected">0</span></div>
|
|
1009
|
+
<span class="sep">·</span>
|
|
1010
|
+
<div class="stat blocked"><span class="label">Blocked</span><span class="value" id="sb-blocked">0</span></div>
|
|
1011
|
+
<div class="edge"><span class="label">Last edit</span> <span class="value" id="sb-last">—</span></div>
|
|
1012
|
+
</div>
|
|
1013
|
+
|
|
1014
|
+
<div class="intake">
|
|
1015
|
+
<h1>What do you need <span class="gradient-text-shimmer">built or fixed</span>?</h1>
|
|
1016
|
+
<p class="sub">Describe a task. codehere delegates to Claude Code, wraps every action with the safety hook, streams it back here.</p>
|
|
1017
|
+
<textarea id="prompt" placeholder="e.g. add a --verbose flag to the trust command and document it in the README"></textarea>
|
|
1018
|
+
<div class="starter-prompts" id="starter-prompts">
|
|
1019
|
+
<span class="sp-label">TRY</span>
|
|
1020
|
+
<button type="button" data-prompt="Read the README and summarize what this project is in 3 short bullets.">Summarize the project</button>
|
|
1021
|
+
<button type="button" data-prompt="List the top-level files and give me a one-line note on what each is for.">Map the architecture</button>
|
|
1022
|
+
<button type="button" data-prompt="Run the tests and tell me what (if anything) is failing.">Run the tests</button>
|
|
1023
|
+
</div>
|
|
1024
|
+
<div class="intake-row">
|
|
1025
|
+
<select id="agent"><option value="claude-code">Claude Code</option></select>
|
|
1026
|
+
<input type="text" id="cwd" class="cwd" placeholder="working directory" spellcheck="false" />
|
|
1027
|
+
<button class="spawn" id="spawn">Assign task</button>
|
|
1028
|
+
</div>
|
|
1029
|
+
<div class="setup-panel" id="setup-panel" data-state=""></div>
|
|
1030
|
+
<div class="intake-row" style="margin-top:6px">
|
|
1031
|
+
<span class="hint" style="margin-left:auto">⌘ + Enter to assign</span>
|
|
1032
|
+
</div>
|
|
1033
|
+
</div>
|
|
1034
|
+
|
|
1035
|
+
<div class="glow-line" style="position:relative;z-index:1"></div>
|
|
1036
|
+
|
|
1037
|
+
<div class="section-label" style="position:relative;z-index:1">Sessions</div>
|
|
1038
|
+
<div id="sessions" style="position:relative;z-index:1"></div>
|
|
1039
|
+
<div id="empty">
|
|
1040
|
+
<div class="pill">No sessions yet — assign a task above</div>
|
|
1041
|
+
</div>
|
|
1042
|
+
|
|
1043
|
+
</div>
|
|
1044
|
+
|
|
1045
|
+
<script>
|
|
1046
|
+
(function () {
|
|
1047
|
+
const promptEl = document.getElementById('prompt');
|
|
1048
|
+
const spawnBtn = document.getElementById('spawn');
|
|
1049
|
+
const agentSelect = document.getElementById('agent');
|
|
1050
|
+
const cwdInput = document.getElementById('cwd');
|
|
1051
|
+
const sessionsEl = document.getElementById('sessions');
|
|
1052
|
+
const emptyEl = document.getElementById('empty');
|
|
1053
|
+
const sessions = new Map(); // sessionId → { card, logEl, unsubscribeSse }
|
|
1054
|
+
|
|
1055
|
+
// E3 · Populate agent dropdown from per-agent readiness.
|
|
1056
|
+
// Setup-aware: shows every registered agent with a state glyph (not just
|
|
1057
|
+
// installed ones). Selecting a non-ready agent unfolds the setup panel with
|
|
1058
|
+
// install/auth hints + swap suggestions.
|
|
1059
|
+
let agentReadiness = []; // cached copy of the readiness response
|
|
1060
|
+
const setupPanel = document.getElementById('setup-panel');
|
|
1061
|
+
|
|
1062
|
+
const STATE_GLYPH = {
|
|
1063
|
+
ready: '✓',
|
|
1064
|
+
billed_key_required: '⚠',
|
|
1065
|
+
needs_auth: '⚠',
|
|
1066
|
+
needs_install: '⤓',
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
function renderAgentOptions(agents) {
|
|
1070
|
+
agentReadiness = agents || [];
|
|
1071
|
+
if (agentReadiness.length === 0) return;
|
|
1072
|
+
// Sort: ready first, then needs_auth/billed, then needs_install
|
|
1073
|
+
const weight = { ready: 0, billed_key_required: 1, needs_auth: 2, needs_install: 3 };
|
|
1074
|
+
const sorted = [...agentReadiness].sort((a, b) => (weight[a.state] ?? 99) - (weight[b.state] ?? 99));
|
|
1075
|
+
agentSelect.innerHTML = sorted.map(a =>
|
|
1076
|
+
'<option value="' + escape(a.id) + '">' +
|
|
1077
|
+
(STATE_GLYPH[a.state] || '·') + ' ' + escape(a.label) +
|
|
1078
|
+
'</option>'
|
|
1079
|
+
).join('');
|
|
1080
|
+
// Trigger the panel render for whatever is selected first
|
|
1081
|
+
updateSetupPanel();
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function updateSetupPanel() {
|
|
1085
|
+
const id = agentSelect.value;
|
|
1086
|
+
const agent = agentReadiness.find(a => a.id === id);
|
|
1087
|
+
if (!agent || agent.state === 'ready') {
|
|
1088
|
+
setupPanel.style.display = 'none';
|
|
1089
|
+
setupPanel.removeAttribute('data-state');
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
setupPanel.dataset.state = agent.state;
|
|
1093
|
+
setupPanel.style.display = 'block';
|
|
1094
|
+
|
|
1095
|
+
let title = '', body = '';
|
|
1096
|
+
let installBtnHtml = '';
|
|
1097
|
+
if (agent.state === 'needs_install') {
|
|
1098
|
+
// Principle #10: codehere does the install, it doesn't print shell commands.
|
|
1099
|
+
// Show a button. Hide the raw command behind a "Details" toggle for users
|
|
1100
|
+
// who want to know what's being run.
|
|
1101
|
+
title = escape(agent.label) + ' isn\\'t installed yet';
|
|
1102
|
+
body = '<div>Click install and codehere will set it up for you — nothing to type.</div>';
|
|
1103
|
+
const hint = agent.installHint || {};
|
|
1104
|
+
if (hint.command) {
|
|
1105
|
+
installBtnHtml =
|
|
1106
|
+
'<div class="install-row">' +
|
|
1107
|
+
'<button type="button" class="install" data-agent="' + escape(agent.id) + '">Install ' + escape(agent.label) + '</button>' +
|
|
1108
|
+
'<button type="button" class="install-details-toggle" data-toggle="details">Details</button>' +
|
|
1109
|
+
'</div>' +
|
|
1110
|
+
'<div class="install-output" data-role="details" style="display:none">' +
|
|
1111
|
+
'<div style="color:var(--fg-50);margin-bottom:6px">We\\'ll run: <span style="color:var(--fg)">' + escape(hint.command) + '</span></div>' +
|
|
1112
|
+
'</div>';
|
|
1113
|
+
} else {
|
|
1114
|
+
body += '<div style="margin-top:6px;color:var(--fg-50)">This agent doesn\\'t ship with an automatic installer yet. You can still swap to a ready agent below.</div>';
|
|
1115
|
+
}
|
|
1116
|
+
} else if (agent.state === 'needs_auth') {
|
|
1117
|
+
title = escape(agent.label) + ' needs to be connected to an account';
|
|
1118
|
+
const hint = agent.authHint || {};
|
|
1119
|
+
body = (hint.detail ? '<div>' + escape(hint.detail) + '</div>' : '');
|
|
1120
|
+
if (hint.url) {
|
|
1121
|
+
body += '<div style="margin-top:6px"><a href="' + escape(hint.url) + '" target="_blank" rel="noreferrer">Open the sign-in page →</a></div>';
|
|
1122
|
+
}
|
|
1123
|
+
} else if (agent.state === 'billed_key_required') {
|
|
1124
|
+
title = escape(agent.label) + ' needs a paid API key';
|
|
1125
|
+
body = '<div>This agent doesn\\'t work with a Claude Code subscription — it needs a separate paid key from Anthropic or OpenAI. Using one of the ready agents below is usually the faster path.</div>';
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const readyAgents = agentReadiness.filter(a => a.state === 'ready');
|
|
1129
|
+
const swapHtml = readyAgents.length > 0
|
|
1130
|
+
? '<div class="swap">' +
|
|
1131
|
+
'<span class="swap-label">Use instead:</span>' +
|
|
1132
|
+
readyAgents.map(a => '<button type="button" data-swap="' + escape(a.id) + '">' + escape(a.label) + '</button>').join('') +
|
|
1133
|
+
'</div>'
|
|
1134
|
+
: '';
|
|
1135
|
+
|
|
1136
|
+
setupPanel.innerHTML =
|
|
1137
|
+
'<div class="sp-title">' + title + '</div>' +
|
|
1138
|
+
body + installBtnHtml + swapHtml;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// F2 · Install button handler — streams output in place, flips readiness on done.
|
|
1142
|
+
function runInstall(agentId, btnEl, outputEl) {
|
|
1143
|
+
btnEl.classList.add('running');
|
|
1144
|
+
btnEl.disabled = true;
|
|
1145
|
+
btnEl.textContent = 'Installing…';
|
|
1146
|
+
outputEl.classList.add('show');
|
|
1147
|
+
outputEl.textContent = '';
|
|
1148
|
+
let hadErr = false;
|
|
1149
|
+
|
|
1150
|
+
const res = fetch('/api/agents/' + encodeURIComponent(agentId) + '/install', { method: 'POST' });
|
|
1151
|
+
// The server responds with SSE. We read the body as a stream.
|
|
1152
|
+
res.then(r => {
|
|
1153
|
+
if (!r.ok) throw new Error('install request failed: ' + r.status);
|
|
1154
|
+
const reader = r.body.getReader();
|
|
1155
|
+
const decoder = new TextDecoder();
|
|
1156
|
+
let buf = '';
|
|
1157
|
+
const pump = () => reader.read().then(({ value, done }) => {
|
|
1158
|
+
if (done) return;
|
|
1159
|
+
buf += decoder.decode(value, { stream: true });
|
|
1160
|
+
let idx;
|
|
1161
|
+
while ((idx = buf.indexOf('\\n\\n')) >= 0) {
|
|
1162
|
+
const chunk = buf.slice(0, idx).trim();
|
|
1163
|
+
buf = buf.slice(idx + 2);
|
|
1164
|
+
if (!chunk.startsWith('data:')) continue;
|
|
1165
|
+
const json = chunk.slice(5).trim();
|
|
1166
|
+
if (!json) continue;
|
|
1167
|
+
try {
|
|
1168
|
+
const evt = JSON.parse(json);
|
|
1169
|
+
if (evt.type === 'out' || evt.type === 'err') {
|
|
1170
|
+
if (evt.type === 'err') hadErr = true;
|
|
1171
|
+
outputEl.textContent += evt.data || '';
|
|
1172
|
+
outputEl.scrollTop = outputEl.scrollHeight;
|
|
1173
|
+
} else if (evt.type === 'done') {
|
|
1174
|
+
btnEl.classList.remove('running');
|
|
1175
|
+
if (evt.code === 0) {
|
|
1176
|
+
btnEl.classList.add('done');
|
|
1177
|
+
btnEl.textContent = '✓ Installed';
|
|
1178
|
+
// Re-fetch readiness so the dropdown glyph updates
|
|
1179
|
+
fetch('/api/readiness/agents').then(r => r.json()).then(d => {
|
|
1180
|
+
renderAgentOptions(d.agents || []);
|
|
1181
|
+
// Select the now-installed agent so user can run the task immediately
|
|
1182
|
+
if ([...agentSelect.options].some(o => o.value === agentId)) {
|
|
1183
|
+
agentSelect.value = agentId;
|
|
1184
|
+
updateSetupPanel();
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
} else {
|
|
1188
|
+
btnEl.classList.add('failed');
|
|
1189
|
+
btnEl.textContent = '✗ Install failed';
|
|
1190
|
+
if (hadErr) outputEl.classList.add('has-err');
|
|
1191
|
+
}
|
|
1192
|
+
} else if (evt.type === 'error') {
|
|
1193
|
+
btnEl.classList.remove('running');
|
|
1194
|
+
btnEl.classList.add('failed');
|
|
1195
|
+
btnEl.textContent = '✗ Couldn\\'t run';
|
|
1196
|
+
outputEl.textContent += '\\n' + (evt.data || '');
|
|
1197
|
+
outputEl.classList.add('has-err');
|
|
1198
|
+
}
|
|
1199
|
+
} catch { /* skip malformed */ }
|
|
1200
|
+
}
|
|
1201
|
+
return pump();
|
|
1202
|
+
});
|
|
1203
|
+
return pump();
|
|
1204
|
+
}).catch(err => {
|
|
1205
|
+
btnEl.classList.remove('running');
|
|
1206
|
+
btnEl.classList.add('failed');
|
|
1207
|
+
btnEl.textContent = '✗ Couldn\\'t start';
|
|
1208
|
+
outputEl.textContent = String(err && err.message || err);
|
|
1209
|
+
outputEl.classList.add('has-err');
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
setupPanel.addEventListener('click', (ev) => {
|
|
1214
|
+
const t = ev.target;
|
|
1215
|
+
if (!t) return;
|
|
1216
|
+
const swapBtn = t.closest('button[data-swap]');
|
|
1217
|
+
if (swapBtn) {
|
|
1218
|
+
const id = swapBtn.getAttribute('data-swap');
|
|
1219
|
+
if (!id) return;
|
|
1220
|
+
agentSelect.value = id;
|
|
1221
|
+
updateSetupPanel();
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const installBtn = t.closest('button.install[data-agent]');
|
|
1225
|
+
if (installBtn && !installBtn.disabled) {
|
|
1226
|
+
const id = installBtn.getAttribute('data-agent');
|
|
1227
|
+
const output = setupPanel.querySelector('.install-output[data-role="details"]');
|
|
1228
|
+
if (id && output) runInstall(id, installBtn, output);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const toggle = t.closest('button[data-toggle="details"]');
|
|
1232
|
+
if (toggle) {
|
|
1233
|
+
const output = setupPanel.querySelector('.install-output[data-role="details"]');
|
|
1234
|
+
if (output) output.classList.toggle('show');
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
agentSelect.addEventListener('change', updateSetupPanel);
|
|
1239
|
+
|
|
1240
|
+
fetch('/api/readiness/agents').then(r => r.json()).then(d => renderAgentOptions(d.agents || [])).catch(() => { /* keep defaults */ });
|
|
1241
|
+
|
|
1242
|
+
// Working directory: persist user's pick, auto-fill with daemon cwd on first run.
|
|
1243
|
+
// Gate A1: strangers shouldn't have to type a path just to hit "Assign"; the
|
|
1244
|
+
// daemon already knows where it was launched and that's usually the right cwd.
|
|
1245
|
+
const savedCwd = localStorage.getItem('codehere:cwd');
|
|
1246
|
+
if (savedCwd) cwdInput.value = savedCwd;
|
|
1247
|
+
fetch('/api/readiness').then(r => r.json()).then(r => {
|
|
1248
|
+
const daemonCwd = r && r.detected && r.detected.daemonCwd;
|
|
1249
|
+
if (daemonCwd) {
|
|
1250
|
+
cwdInput.placeholder = daemonCwd;
|
|
1251
|
+
if (!cwdInput.value.trim()) cwdInput.value = daemonCwd; // auto-fill when nothing saved
|
|
1252
|
+
}
|
|
1253
|
+
}).catch(() => { /* placeholder unchanged */ });
|
|
1254
|
+
cwdInput.addEventListener('change', () => {
|
|
1255
|
+
localStorage.setItem('codehere:cwd', cwdInput.value.trim());
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
// Gate A1: starter-prompt chips prefill the textarea + focus it so the user
|
|
1259
|
+
// can edit or hit ⌘-Enter immediately. No scroll-jumping, no redirect.
|
|
1260
|
+
document.getElementById('starter-prompts').addEventListener('click', (ev) => {
|
|
1261
|
+
const btn = ev.target && ev.target.closest('button[data-prompt]');
|
|
1262
|
+
if (!btn) return;
|
|
1263
|
+
promptEl.value = btn.getAttribute('data-prompt') || '';
|
|
1264
|
+
promptEl.focus();
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
function escape(s) {
|
|
1268
|
+
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function now() {
|
|
1272
|
+
return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// Render a stream-json event from claude -p --output-format=stream-json.
|
|
1276
|
+
// We've seen the real shapes now (v0.3.0-alpha.1 dogfood). Key rules:
|
|
1277
|
+
// - stream_event = token-level deltas. Skip entirely — they aggregate into
|
|
1278
|
+
// 'assistant' / 'user' messages which we render cleanly.
|
|
1279
|
+
// - system subtypes: init/status are noise (skip); hook_started is noise
|
|
1280
|
+
// (skip — we render the paired hook_response for outcome). hook_response
|
|
1281
|
+
// shows name + allow/block so the user sees the safety layer.
|
|
1282
|
+
// - assistant message.content: mix of text + tool_use blocks. Render each.
|
|
1283
|
+
// - user message.content: tool_result blocks. Render condensed.
|
|
1284
|
+
// - result: final summary with duration + outcome.
|
|
1285
|
+
// - exit/error/stderr: meta from our own wrapper.
|
|
1286
|
+
function renderEvent(logEl, event) {
|
|
1287
|
+
const type = String(event.type || '');
|
|
1288
|
+
const data = event.data ?? {};
|
|
1289
|
+
|
|
1290
|
+
// ── Silent types (reduce noise) ──
|
|
1291
|
+
if (type === 'stream_event') return;
|
|
1292
|
+
if (type === 'system' && (data.subtype === 'init' || data.subtype === 'status' || data.subtype === 'hook_started')) return;
|
|
1293
|
+
if (type === 'stdout' && !String(data.line || '').trim()) return;
|
|
1294
|
+
// Codex internal lifecycle — not useful to show per-event
|
|
1295
|
+
if (type === 'thread.started' || type === 'turn.started') return;
|
|
1296
|
+
|
|
1297
|
+
// Rendered lines
|
|
1298
|
+
const lines = []; // { cls, label, body }
|
|
1299
|
+
|
|
1300
|
+
if (type === 'assistant' && data.message && Array.isArray(data.message.content)) {
|
|
1301
|
+
for (const block of data.message.content) {
|
|
1302
|
+
if (block.type === 'text') {
|
|
1303
|
+
const t = String(block.text || '').trim();
|
|
1304
|
+
if (t) lines.push({ cls: 'claude', label: 'claude', body: t });
|
|
1305
|
+
} else if (block.type === 'tool_use') {
|
|
1306
|
+
const name = String(block.name || 'tool');
|
|
1307
|
+
const input = block.input && typeof block.input === 'object' ? JSON.stringify(block.input) : '';
|
|
1308
|
+
const preview = input.length > 180 ? input.slice(0, 180) + '…' : input;
|
|
1309
|
+
lines.push({ cls: 'tool', label: name, body: preview });
|
|
1310
|
+
} else if (block.type === 'thinking') {
|
|
1311
|
+
// Skip thinking — too verbose for the log view
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
} else if (type === 'user' && data.message && Array.isArray(data.message.content)) {
|
|
1315
|
+
for (const block of data.message.content) {
|
|
1316
|
+
if (block.type === 'tool_result') {
|
|
1317
|
+
const raw = Array.isArray(block.content)
|
|
1318
|
+
? block.content.map(c => c.text || '').join('')
|
|
1319
|
+
: String(block.content || '');
|
|
1320
|
+
const t = raw.replace(/\\s+/g, ' ').trim();
|
|
1321
|
+
if (t) {
|
|
1322
|
+
const preview = t.length > 220 ? t.slice(0, 220) + '…' : t;
|
|
1323
|
+
lines.push({ cls: 'result', label: block.is_error ? 'error' : 'result', body: preview });
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
} else if (type === 'system' && data.subtype === 'hook_response') {
|
|
1328
|
+
const name = String(data.hook_name || 'hook');
|
|
1329
|
+
const exitCode = Number(data.exit_code ?? 0);
|
|
1330
|
+
const blocked = exitCode !== 0;
|
|
1331
|
+
const stderr = String(data.stderr || '').trim();
|
|
1332
|
+
const body = blocked ? (stderr || 'blocked') : 'ok';
|
|
1333
|
+
lines.push({ cls: 'hook' + (blocked ? ' blocked' : ''), label: name, body });
|
|
1334
|
+
} else if (type === 'result') {
|
|
1335
|
+
// Claude Code --output-format=stream-json final result
|
|
1336
|
+
const dur = Math.round(Number(data.duration_ms || 0) / 1000);
|
|
1337
|
+
const turns = Number(data.num_turns || 0);
|
|
1338
|
+
const summary = String(data.result || '').slice(0, 220);
|
|
1339
|
+
lines.push({ cls: 'done', label: data.subtype || 'done', body: turns + ' turns · ' + dur + 's · ' + summary });
|
|
1340
|
+
} else if (type === 'item.started') {
|
|
1341
|
+
// Codex fires item.started immediately; item.completed follows with results.
|
|
1342
|
+
// Silent to avoid doubling every tool call in the feed.
|
|
1343
|
+
return;
|
|
1344
|
+
} else if (type === 'item.completed') {
|
|
1345
|
+
// Codex event: { item: { type, ... } }. Observed item.types from live dogfood:
|
|
1346
|
+
// agent_message → text
|
|
1347
|
+
// command_execution → command (string), aggregated_output, exit_code, status
|
|
1348
|
+
// (+future: tool_call / local_shell_call / etc — defensive branches kept)
|
|
1349
|
+
const item = data.item && typeof data.item === 'object' ? data.item : {};
|
|
1350
|
+
const itemType = String(item.type || 'item');
|
|
1351
|
+
|
|
1352
|
+
if (itemType === 'agent_message' && item.text) {
|
|
1353
|
+
const t = String(item.text).trim();
|
|
1354
|
+
if (t) lines.push({ cls: 'claude', label: 'codex', body: t.slice(0, 280) });
|
|
1355
|
+
} else if (itemType === 'command_execution') {
|
|
1356
|
+
const cmd = String(item.command || '').replace(/\\n/g, ' ').trim();
|
|
1357
|
+
const cmdShort = cmd.length > 140 ? cmd.slice(0, 140) + '…' : cmd;
|
|
1358
|
+
lines.push({ cls: 'tool', label: 'shell', body: cmdShort });
|
|
1359
|
+
const output = String(item.aggregated_output || '').trim();
|
|
1360
|
+
if (output) {
|
|
1361
|
+
const outShort = output.length > 200 ? output.slice(0, 200) + '…' : output;
|
|
1362
|
+
lines.push({
|
|
1363
|
+
cls: item.exit_code === 0 || item.status === 'completed' ? 'result' : 'error',
|
|
1364
|
+
label: item.exit_code === 0 || item.status === 'completed' ? 'result' : 'exit ' + (item.exit_code ?? '?'),
|
|
1365
|
+
body: outShort,
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
} else if (itemType === 'tool_call' || itemType === 'local_shell_call') {
|
|
1369
|
+
// Forward-compat: other codex tool types we haven't seen yet
|
|
1370
|
+
const cmd = String(item.command || item.tool_name || item.name || 'tool');
|
|
1371
|
+
const args = item.arguments ? JSON.stringify(item.arguments).slice(0, 200) : '';
|
|
1372
|
+
lines.push({ cls: 'tool', label: cmd, body: args });
|
|
1373
|
+
} else if (itemType === 'reasoning' || itemType === 'thinking') {
|
|
1374
|
+
// skip — too verbose
|
|
1375
|
+
} else {
|
|
1376
|
+
// Unknown item type — render minimally for diagnosis
|
|
1377
|
+
lines.push({ cls: 'result', label: itemType, body: JSON.stringify(item).slice(0, 200) });
|
|
1378
|
+
}
|
|
1379
|
+
} else if (type === 'turn.completed') {
|
|
1380
|
+
// Codex turn-completed event — light summary
|
|
1381
|
+
const u = (data.usage && typeof data.usage === 'object') ? data.usage : {};
|
|
1382
|
+
const input = Number(u.input_tokens || 0);
|
|
1383
|
+
const output = Number(u.output_tokens || 0);
|
|
1384
|
+
if (input || output) {
|
|
1385
|
+
lines.push({ cls: 'done', label: 'turn', body: input + ' in · ' + output + ' out tokens' });
|
|
1386
|
+
}
|
|
1387
|
+
} else if (type === 'exit') {
|
|
1388
|
+
const status = String(data.status || '?');
|
|
1389
|
+
const dur = Math.round(Number(data.durationMs || 0) / 1000);
|
|
1390
|
+
lines.push({ cls: 'exit', label: 'exit', body: 'status=' + status + ' duration=' + dur + 's' });
|
|
1391
|
+
} else if (type === 'codehere:error') {
|
|
1392
|
+
// F1b structured error with recovery hint
|
|
1393
|
+
const code = String(data.code || 'error');
|
|
1394
|
+
const msg = String(data.message || '');
|
|
1395
|
+
const recovery = String(data.recovery || '');
|
|
1396
|
+
const div = document.createElement('div');
|
|
1397
|
+
div.className = 'event codehere-error';
|
|
1398
|
+
div.innerHTML =
|
|
1399
|
+
'<span class="label">' + escape(code) + '</span>' +
|
|
1400
|
+
escape(msg) +
|
|
1401
|
+
(recovery ? '<span class="recovery">→ ' + escape(recovery) + '</span>' : '');
|
|
1402
|
+
logEl.appendChild(div);
|
|
1403
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
1404
|
+
return;
|
|
1405
|
+
} else if (type === 'error') {
|
|
1406
|
+
lines.push({ cls: 'error', label: 'error', body: String(data.message || JSON.stringify(data)) });
|
|
1407
|
+
} else if (type === 'stderr') {
|
|
1408
|
+
const t = String(data.line || '').trim();
|
|
1409
|
+
if (t) lines.push({ cls: 'stderr', label: 'stderr', body: t });
|
|
1410
|
+
} else if (type === 'stdout') {
|
|
1411
|
+
lines.push({ cls: 'stdout', label: 'out', body: String(data.line || '') });
|
|
1412
|
+
}
|
|
1413
|
+
// Any other event type → silent (we don't dump raw JSON anymore)
|
|
1414
|
+
|
|
1415
|
+
if (lines.length === 0) return;
|
|
1416
|
+
|
|
1417
|
+
const ts = now();
|
|
1418
|
+
for (const ln of lines) {
|
|
1419
|
+
const div = document.createElement('div');
|
|
1420
|
+
div.className = 'event ' + ln.cls;
|
|
1421
|
+
div.innerHTML =
|
|
1422
|
+
'<span class="t">' + escape(ts) + '</span>' +
|
|
1423
|
+
'<span class="label">' + escape(ln.label) + '</span>' +
|
|
1424
|
+
escape(ln.body);
|
|
1425
|
+
logEl.appendChild(div);
|
|
1426
|
+
}
|
|
1427
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function updateStatus(pillEl, status) {
|
|
1431
|
+
pillEl.className = 'status-pill ' + status;
|
|
1432
|
+
pillEl.textContent = status;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function agentLabel(id) {
|
|
1436
|
+
const labels = { 'claude-code': 'Claude Code', 'codex': 'Codex', 'aider': 'Aider', 'kiro': 'Kiro' };
|
|
1437
|
+
return labels[id] || id || 'agent';
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function attachSession(session) {
|
|
1441
|
+
const card = document.createElement('div');
|
|
1442
|
+
card.className = 'session';
|
|
1443
|
+
card.dataset.sessionId = session.sessionId;
|
|
1444
|
+
// C3 · Persisted sessions are from a prior daemon run — no live stream, no
|
|
1445
|
+
// kill. Still useful: user sees their previous Final reports across restarts.
|
|
1446
|
+
const isPersisted = session.provenance === 'persisted';
|
|
1447
|
+
const runningClass = session.status === 'running' ? ' badge-running-glow' : '';
|
|
1448
|
+
const liveDot = session.status === 'running' ? '<span class="live-dot"></span>' : '';
|
|
1449
|
+
card.innerHTML =
|
|
1450
|
+
'<div class="session-head">' +
|
|
1451
|
+
'<div class="meta">' +
|
|
1452
|
+
'<div class="prompt" title="' + escape(session.prompt) + '">' +
|
|
1453
|
+
'<span class="card-lights"><span class="r"></span><span class="y"></span><span class="g"></span></span>' +
|
|
1454
|
+
escape(session.prompt) +
|
|
1455
|
+
'<span class="chevron">▸</span>' +
|
|
1456
|
+
'</div>' +
|
|
1457
|
+
'<div class="sub">' + liveDot + escape(agentLabel(session.agent)) + ' · ' + new Date(session.startedAt).toLocaleTimeString() +
|
|
1458
|
+
'<span class="safety-pill" style="display:none">' +
|
|
1459
|
+
'<span class="shield">🛡</span> ' +
|
|
1460
|
+
'<span class="safe">0</span> safe' +
|
|
1461
|
+
'<span class="blocked-portion" style="display:none"> · <span class="blocked">0</span> blocked</span>' +
|
|
1462
|
+
'</span>' +
|
|
1463
|
+
'</div>' +
|
|
1464
|
+
'<div class="steps-mini"></div>' +
|
|
1465
|
+
'</div>' +
|
|
1466
|
+
'<div class="session-controls">' +
|
|
1467
|
+
'<span class="status-pill ' + session.status + runningClass + '">' + session.status + '</span>' +
|
|
1468
|
+
'<button class="kill">Kill</button>' +
|
|
1469
|
+
'</div>' +
|
|
1470
|
+
'</div>' +
|
|
1471
|
+
'<div class="session-body">' +
|
|
1472
|
+
'<div class="final-report" style="display:none"></div>' +
|
|
1473
|
+
'<div class="event-log"></div>' +
|
|
1474
|
+
'</div>';
|
|
1475
|
+
|
|
1476
|
+
sessionsEl.prepend(card);
|
|
1477
|
+
emptyEl.style.display = 'none';
|
|
1478
|
+
|
|
1479
|
+
const logEl = card.querySelector('.event-log');
|
|
1480
|
+
const pillEl = card.querySelector('.status-pill');
|
|
1481
|
+
const killBtn = card.querySelector('.kill');
|
|
1482
|
+
const headEl = card.querySelector('.session-head');
|
|
1483
|
+
const stepsEl = card.querySelector('.steps-mini');
|
|
1484
|
+
const finalReportEl = card.querySelector('.final-report');
|
|
1485
|
+
const safetyPillEl = card.querySelector('.safety-pill');
|
|
1486
|
+
const safetySafeEl = safetyPillEl.querySelector('.safe');
|
|
1487
|
+
const safetyBlockedEl = safetyPillEl.querySelector('.blocked');
|
|
1488
|
+
const safetyBlockedWrap = safetyPillEl.querySelector('.blocked-portion');
|
|
1489
|
+
let safeCount = 0, blockedCount = 0;
|
|
1490
|
+
|
|
1491
|
+
// Gate B1 · increment safety counters on every Claude hook_response.
|
|
1492
|
+
// Principle #7 (agent-provenance): shows the hook firing continuously,
|
|
1493
|
+
// so the safety moat is visible mid-task, not only when something blocks.
|
|
1494
|
+
function bumpSafety(event) {
|
|
1495
|
+
if (event.type !== 'system') return;
|
|
1496
|
+
const d = event.data || {};
|
|
1497
|
+
if (d.subtype !== 'hook_response') return;
|
|
1498
|
+
if (Number(d.exit_code ?? 0) === 0) safeCount += 1; else blockedCount += 1;
|
|
1499
|
+
safetyPillEl.style.display = 'inline-flex';
|
|
1500
|
+
safetySafeEl.textContent = String(safeCount);
|
|
1501
|
+
if (blockedCount > 0) {
|
|
1502
|
+
safetyBlockedWrap.style.display = '';
|
|
1503
|
+
safetyBlockedEl.textContent = String(blockedCount);
|
|
1504
|
+
safetyPillEl.classList.add('has-blocked');
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Collapse/expand on header click (but not when clicking Kill)
|
|
1509
|
+
headEl.addEventListener('click', (ev) => {
|
|
1510
|
+
if (ev.target && ev.target.closest('.session-controls')) return;
|
|
1511
|
+
card.classList.toggle('expanded');
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
// Layer 2: render the semantic step list inside the collapsed card.
|
|
1515
|
+
// Principles #1 (web is not a terminal) + #2 (info density) — show last 3
|
|
1516
|
+
// steps with pass/active/fail glyphs; hide the rest behind "+ N more" so
|
|
1517
|
+
// the card stays compact even on a 20-step task.
|
|
1518
|
+
function renderSteps(steps) {
|
|
1519
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
1520
|
+
stepsEl.innerHTML = '';
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const tail = steps.slice(-3);
|
|
1524
|
+
const more = steps.length - tail.length;
|
|
1525
|
+
const glyph = { done: '✓', active: '⟳', failed: '✗', pending: '·' };
|
|
1526
|
+
stepsEl.innerHTML =
|
|
1527
|
+
tail.map(s => {
|
|
1528
|
+
const st = String(s.state || 'pending');
|
|
1529
|
+
const g = glyph[st] || '·';
|
|
1530
|
+
return '<div class="step ' + st + '"><span class="ico">' + g + '</span><span>' + escape(String(s.label || '')) + '</span></div>';
|
|
1531
|
+
}).join('') +
|
|
1532
|
+
(more > 0 ? '<div class="more">+ ' + more + ' earlier</div>' : '');
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Layer 3: render the final report block when the task completes.
|
|
1536
|
+
// Principles #2 + #7 — density (files/tests/trust in one view) and
|
|
1537
|
+
// agent-provenance (the report attributes to a specific agent).
|
|
1538
|
+
function renderArtifacts(artifacts) {
|
|
1539
|
+
if (!artifacts) { finalReportEl.style.display = 'none'; return; }
|
|
1540
|
+
finalReportEl.style.display = 'block';
|
|
1541
|
+
const files = Array.isArray(artifacts.files) ? artifacts.files : [];
|
|
1542
|
+
const tests = artifacts.tests;
|
|
1543
|
+
let html = '<div class="fr-label">Final report · ' + escape(agentLabel(session.agent)) + '</div>';
|
|
1544
|
+
if (files.length > 0) {
|
|
1545
|
+
html += '<div class="fr-files">' + files.map(f => {
|
|
1546
|
+
const lines = '<span class="add">+' + (f.added || 0) + '</span> <span class="rem">−' + (f.removed || 0) + '</span>';
|
|
1547
|
+
const trustCls = typeof f.trust === 'number'
|
|
1548
|
+
? (f.trust >= 80 ? 'good' : f.trust >= 60 ? 'warn' : 'bad')
|
|
1549
|
+
: 'none';
|
|
1550
|
+
const trustLabel = typeof f.trust === 'number' ? String(f.trust) : '—';
|
|
1551
|
+
return '<div class="fr-file">' +
|
|
1552
|
+
'<span class="path" title="' + escape(String(f.path)) + '">' + escape(String(f.path)) + '</span>' +
|
|
1553
|
+
'<span class="lines">' + lines + '</span>' +
|
|
1554
|
+
'<span class="trust ' + trustCls + '" title="Trust score">' + trustLabel + '</span>' +
|
|
1555
|
+
'</div>';
|
|
1556
|
+
}).join('') + '</div>';
|
|
1557
|
+
} else {
|
|
1558
|
+
html += '<div class="fr-none">No file changes detected (or not run inside a git repo).</div>';
|
|
1559
|
+
}
|
|
1560
|
+
if (tests && (tests.passed || tests.failed)) {
|
|
1561
|
+
html += '<div class="fr-tests">Tests · <span class="pass">' + (tests.passed || 0) + ' passed</span>' +
|
|
1562
|
+
(tests.failed ? ', <span class="fail">' + tests.failed + ' failed</span>' : '') +
|
|
1563
|
+
(tests.durationMs ? ' · ' + Math.round(tests.durationMs / 100) / 10 + 's' : '') + '</div>';
|
|
1564
|
+
}
|
|
1565
|
+
const u = artifacts.usage;
|
|
1566
|
+
if (u && (u.inputTokens || u.outputTokens)) {
|
|
1567
|
+
const fmt = (n) => n >= 10000 ? (Math.round(n / 100) / 10).toFixed(1) + 'k' : String(n);
|
|
1568
|
+
let tokensLine = 'Tokens · <b>' + fmt(u.inputTokens) + '</b> in · <b>' + fmt(u.outputTokens) + '</b> out';
|
|
1569
|
+
if (u.cacheReadTokens) tokensLine += ' · ' + fmt(u.cacheReadTokens) + ' cached';
|
|
1570
|
+
html += '<div class="fr-tests">' + tokensLine + '</div>';
|
|
1571
|
+
}
|
|
1572
|
+
finalReportEl.innerHTML = html;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Hydrate from initial snapshot (late-joiner support + persisted sessions)
|
|
1576
|
+
if (Array.isArray(session.steps) && session.steps.length) renderSteps(session.steps);
|
|
1577
|
+
if (session.artifacts) renderArtifacts(session.artifacts);
|
|
1578
|
+
|
|
1579
|
+
// Persisted sessions have no live event stream and no subprocess to kill.
|
|
1580
|
+
if (isPersisted) {
|
|
1581
|
+
killBtn.style.display = 'none';
|
|
1582
|
+
card.classList.add('observed'); // reuse the faded styling
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const es = new EventSource('/api/task/' + session.sessionId + '/events');
|
|
1587
|
+
es.onmessage = (ev) => {
|
|
1588
|
+
try {
|
|
1589
|
+
const event = JSON.parse(ev.data);
|
|
1590
|
+
|
|
1591
|
+
// Layer 2/3 synthetic events — handle before renderEvent (so they don't leak into the raw log)
|
|
1592
|
+
if (event.type === 'codehere:steps') { renderSteps(event.data?.steps); return; }
|
|
1593
|
+
if (event.type === 'codehere:artifacts') { renderArtifacts(event.data); return; }
|
|
1594
|
+
|
|
1595
|
+
// Any structured error flips the status pill immediately — don't wait for
|
|
1596
|
+
// the subprocess to exit. Many CLIs print a logical error to stdout and
|
|
1597
|
+
// then exit 0, which would otherwise paint the card green.
|
|
1598
|
+
if (event.type === 'codehere:error') {
|
|
1599
|
+
updateStatus(pillEl, 'error');
|
|
1600
|
+
// fall through — renderEvent renders the red banner in the log
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// Gate B1 · tally safety events continuously (non-exclusive with render)
|
|
1604
|
+
bumpSafety(event);
|
|
1605
|
+
|
|
1606
|
+
renderEvent(logEl, event);
|
|
1607
|
+
if (event.type === 'exit') {
|
|
1608
|
+
updateStatus(pillEl, event.data?.status || 'completed');
|
|
1609
|
+
killBtn.disabled = true; killBtn.style.opacity = '0.4';
|
|
1610
|
+
const dot = card.querySelector('.sub .live-dot');
|
|
1611
|
+
if (dot) dot.remove();
|
|
1612
|
+
es.close();
|
|
1613
|
+
}
|
|
1614
|
+
if (event.type === 'error') {
|
|
1615
|
+
updateStatus(pillEl, 'error');
|
|
1616
|
+
}
|
|
1617
|
+
} catch { /* skip */ }
|
|
1618
|
+
};
|
|
1619
|
+
es.onerror = () => { /* server may have closed after exit; fine */ };
|
|
1620
|
+
|
|
1621
|
+
killBtn.addEventListener('click', async (ev) => {
|
|
1622
|
+
ev.stopPropagation();
|
|
1623
|
+
killBtn.disabled = true; killBtn.textContent = '...';
|
|
1624
|
+
try {
|
|
1625
|
+
await fetch('/api/task/' + session.sessionId + '/kill', { method: 'POST' });
|
|
1626
|
+
} catch { /* ignore */ }
|
|
1627
|
+
killBtn.textContent = 'Kill';
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
sessions.set(session.sessionId, { card, logEl, es });
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
async function assignTask() {
|
|
1634
|
+
const prompt = promptEl.value.trim();
|
|
1635
|
+
if (!prompt) return;
|
|
1636
|
+
const agent = agentSelect.value || 'claude-code';
|
|
1637
|
+
const cwd = cwdInput.value.trim();
|
|
1638
|
+
spawnBtn.disabled = true; spawnBtn.textContent = '...';
|
|
1639
|
+
try {
|
|
1640
|
+
const res = await fetch('/api/task', {
|
|
1641
|
+
method: 'POST',
|
|
1642
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1643
|
+
body: JSON.stringify({ prompt, agent, cwd: cwd || undefined }),
|
|
1644
|
+
});
|
|
1645
|
+
const data = await res.json();
|
|
1646
|
+
if (!res.ok) { alert('Failed: ' + (data.error || res.status)); return; }
|
|
1647
|
+
attachSession({ sessionId: data.sessionId, prompt, startedAt: data.startedAt, status: data.status, agent: data.agent || agent });
|
|
1648
|
+
promptEl.value = '';
|
|
1649
|
+
} catch (e) {
|
|
1650
|
+
alert('Network error: ' + (e && e.message ? e.message : e));
|
|
1651
|
+
} finally {
|
|
1652
|
+
spawnBtn.disabled = false; spawnBtn.textContent = 'Assign task';
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
spawnBtn.addEventListener('click', assignTask);
|
|
1657
|
+
promptEl.addEventListener('keydown', (e) => {
|
|
1658
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); assignTask(); }
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
// ── Observed sessions (F1d) ──────────────────────────────────────────
|
|
1662
|
+
function shortId(id) { return String(id || '').slice(0, 8); }
|
|
1663
|
+
|
|
1664
|
+
function timeAgo(ms) {
|
|
1665
|
+
const s = Math.max(0, Math.floor((Date.now() - ms) / 1000));
|
|
1666
|
+
if (s < 60) return s + 's ago';
|
|
1667
|
+
if (s < 3600) return Math.floor(s / 60) + 'm ago';
|
|
1668
|
+
return Math.floor(s / 3600) + 'h ago';
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function renderActiveAgent(a) {
|
|
1672
|
+
const existingKey = 'agent:' + a.pid;
|
|
1673
|
+
const existing = document.querySelector('[data-session-id="' + CSS.escape(existingKey) + '"]');
|
|
1674
|
+
if (existing) return;
|
|
1675
|
+
const card = document.createElement('div');
|
|
1676
|
+
card.className = 'session observed';
|
|
1677
|
+
card.dataset.sessionId = existingKey;
|
|
1678
|
+
const uptime = a.uptimeSeconds < 60
|
|
1679
|
+
? a.uptimeSeconds + 's'
|
|
1680
|
+
: a.uptimeSeconds < 3600
|
|
1681
|
+
? Math.floor(a.uptimeSeconds / 60) + 'm'
|
|
1682
|
+
: Math.floor(a.uptimeSeconds / 3600) + 'h';
|
|
1683
|
+
card.innerHTML =
|
|
1684
|
+
'<div class="session-head">' +
|
|
1685
|
+
'<div class="meta">' +
|
|
1686
|
+
'<div class="prompt">' +
|
|
1687
|
+
'<span class="card-lights"><span class="r"></span><span class="y"></span><span class="g"></span></span>' +
|
|
1688
|
+
escape(a.label) + ' · ' + escape(a.tty) + ' · pid ' + a.pid +
|
|
1689
|
+
'</div>' +
|
|
1690
|
+
'<div class="sub">running ' + escape(uptime) + '</div>' +
|
|
1691
|
+
'</div>' +
|
|
1692
|
+
'<div class="session-controls">' +
|
|
1693
|
+
'<span class="status-pill observed-pill">detected</span>' +
|
|
1694
|
+
'</div>' +
|
|
1695
|
+
'</div>';
|
|
1696
|
+
sessionsEl.appendChild(card);
|
|
1697
|
+
emptyEl.style.display = 'none';
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function pruneStaleAgents(seenKeys) {
|
|
1701
|
+
// Remove cards for agents that have exited since the last fetch
|
|
1702
|
+
const cards = sessionsEl.querySelectorAll('.session.observed');
|
|
1703
|
+
cards.forEach(card => {
|
|
1704
|
+
const key = card.dataset.sessionId;
|
|
1705
|
+
if (key && key.startsWith('agent:') && !seenKeys.has(key)) card.remove();
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// Gate B3 · audit summary row — moat made visible on the main page.
|
|
1710
|
+
// Principles #3 (status at a glance), #7 (agent-provenance), #6 (marketing voice).
|
|
1711
|
+
const auditRow = document.getElementById('audit-summary');
|
|
1712
|
+
function updateAudit(d) {
|
|
1713
|
+
if (!d) { auditRow.classList.add('empty'); return; }
|
|
1714
|
+
const hasAny = (d.actions || 0) > 0;
|
|
1715
|
+
if (!hasAny) { auditRow.classList.add('empty'); return; }
|
|
1716
|
+
auditRow.classList.remove('empty');
|
|
1717
|
+
document.getElementById('as-actions').textContent = String(d.actions || 0);
|
|
1718
|
+
document.getElementById('as-blocked').textContent = String(d.blocked || 0);
|
|
1719
|
+
document.getElementById('as-trust').textContent = d.avgTrust == null ? '—' : String(d.avgTrust);
|
|
1720
|
+
document.getElementById('as-agents').textContent = String(d.agents || 0);
|
|
1721
|
+
}
|
|
1722
|
+
function refreshAudit() {
|
|
1723
|
+
fetch('/api/audit/summary').then(r => r.json()).then(updateAudit).catch(() => { /* fine */ });
|
|
1724
|
+
}
|
|
1725
|
+
refreshAudit();
|
|
1726
|
+
setInterval(refreshAudit, 30000); // audit log isn't hot; 30s is plenty
|
|
1727
|
+
|
|
1728
|
+
// Layer 7: status-at-a-glance banner — one row answers "what's happening".
|
|
1729
|
+
// Principles #3 (status at a glance) + #4 (multi-session default: one row scales from 1 to N sessions).
|
|
1730
|
+
const banner = document.getElementById('status-banner');
|
|
1731
|
+
let latestLastEditAt = null;
|
|
1732
|
+
function agoStr(ms) {
|
|
1733
|
+
if (!ms) return '—';
|
|
1734
|
+
const s = Math.max(0, Math.floor((Date.now() - ms) / 1000));
|
|
1735
|
+
if (s < 60) return s + 's ago';
|
|
1736
|
+
if (s < 3600) return Math.floor(s / 60) + 'm ago';
|
|
1737
|
+
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
|
|
1738
|
+
return Math.floor(s / 86400) + 'd ago';
|
|
1739
|
+
}
|
|
1740
|
+
function updateBanner(d) {
|
|
1741
|
+
const spawned = d.spawned || d.sessions || [];
|
|
1742
|
+
const running = spawned.filter(s => s.status === 'running').length;
|
|
1743
|
+
const blocked = spawned.filter(s => s.status === 'error').length;
|
|
1744
|
+
const detected = (d.activeAgents || []).length;
|
|
1745
|
+
document.getElementById('sb-running').textContent = String(running);
|
|
1746
|
+
document.getElementById('sb-detected').textContent = String(detected);
|
|
1747
|
+
const blockedEl = document.getElementById('sb-blocked');
|
|
1748
|
+
blockedEl.textContent = String(blocked);
|
|
1749
|
+
blockedEl.classList.toggle('nonzero', blocked > 0);
|
|
1750
|
+
latestLastEditAt = d.lastEditAt || null;
|
|
1751
|
+
document.getElementById('sb-last').textContent = agoStr(latestLastEditAt);
|
|
1752
|
+
// Show the banner once we have anything to show (sessions or detected agents)
|
|
1753
|
+
banner.style.display = (running + detected + blocked + spawned.length > 0) ? 'flex' : 'none';
|
|
1754
|
+
}
|
|
1755
|
+
// Tick the "last edit" relative label every second even without a refetch
|
|
1756
|
+
setInterval(() => {
|
|
1757
|
+
if (latestLastEditAt) document.getElementById('sb-last').textContent = agoStr(latestLastEditAt);
|
|
1758
|
+
}, 1000);
|
|
1759
|
+
|
|
1760
|
+
function loadSessions() {
|
|
1761
|
+
fetch('/api/tasks').then(r => r.json()).then(d => {
|
|
1762
|
+
for (const s of (d.spawned || d.sessions || [])) attachSession(s);
|
|
1763
|
+
// C3 · render completed sessions from prior daemon runs (read-only cards)
|
|
1764
|
+
for (const s of (d.persisted || [])) attachSession({ ...s, provenance: 'persisted' });
|
|
1765
|
+
const seen = new Set();
|
|
1766
|
+
for (const a of (d.activeAgents || [])) {
|
|
1767
|
+
seen.add('agent:' + a.pid);
|
|
1768
|
+
renderActiveAgent(a);
|
|
1769
|
+
}
|
|
1770
|
+
pruneStaleAgents(seen);
|
|
1771
|
+
updateBanner(d);
|
|
1772
|
+
}).catch(() => { /* fine */ });
|
|
1773
|
+
}
|
|
1774
|
+
loadSessions();
|
|
1775
|
+
// Refresh detected-agents list + banner every 5s
|
|
1776
|
+
setInterval(() => {
|
|
1777
|
+
fetch('/api/tasks').then(r => r.json()).then(d => {
|
|
1778
|
+
const seen = new Set();
|
|
1779
|
+
for (const a of (d.activeAgents || [])) {
|
|
1780
|
+
seen.add('agent:' + a.pid);
|
|
1781
|
+
renderActiveAgent(a);
|
|
1782
|
+
}
|
|
1783
|
+
pruneStaleAgents(seen);
|
|
1784
|
+
updateBanner(d);
|
|
1785
|
+
}).catch(() => { /* fine */ });
|
|
1786
|
+
}, 5000);
|
|
1787
|
+
|
|
1788
|
+
// ── F1a readiness banner ─────────────────────────────────────────────
|
|
1789
|
+
const readinessEl = document.getElementById('readiness');
|
|
1790
|
+
|
|
1791
|
+
function renderReadiness(report) {
|
|
1792
|
+
if (!report || report.state === 'ready') {
|
|
1793
|
+
readinessEl.style.display = 'none';
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
readinessEl.style.display = 'block';
|
|
1797
|
+
readinessEl.setAttribute('data-severity', report.severity);
|
|
1798
|
+
let actionHtml = '';
|
|
1799
|
+
if (report.action) {
|
|
1800
|
+
if (report.action.command) {
|
|
1801
|
+
actionHtml += '<code>' + escape(report.action.command) + '</code>';
|
|
1802
|
+
}
|
|
1803
|
+
if (report.action.url) {
|
|
1804
|
+
actionHtml += ' <a href="' + escape(report.action.url) + '" target="_blank" rel="noreferrer">' + escape(report.action.label || 'Open') + ' →</a>';
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
actionHtml += ' <button id="readiness-refresh">Check again</button>';
|
|
1808
|
+
readinessEl.innerHTML =
|
|
1809
|
+
'<div class="r-title">' + escape(report.title) + '</div>' +
|
|
1810
|
+
'<div class="r-detail">' + escape(report.detail) + '</div>' +
|
|
1811
|
+
'<div class="r-action">' + actionHtml + '</div>';
|
|
1812
|
+
const btn = document.getElementById('readiness-refresh');
|
|
1813
|
+
if (btn) btn.addEventListener('click', checkReadiness);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
function checkReadiness() {
|
|
1817
|
+
fetch('/api/readiness').then(r => r.json()).then(renderReadiness).catch(() => {
|
|
1818
|
+
readinessEl.style.display = 'none';
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
checkReadiness();
|
|
1823
|
+
})();
|
|
1824
|
+
</script>
|
|
1825
|
+
</body>
|
|
1826
|
+
</html>`;
|
|
1827
|
+
//# sourceMappingURL=ui-page.js.map
|