@delete101020/aidlc 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTOMATION.md +148 -0
- package/README.md +684 -0
- package/dist/assets/annotate-artifact.skill.md +121 -0
- package/dist/assets/epic-context.skill.md +55 -0
- package/dist/bundle.js +49470 -0
- package/dist/templates/ainative/agents/native-engineer.md +66 -0
- package/dist/templates/ainative/agents/native-operator.md +71 -0
- package/dist/templates/ainative/agents/native-originator.md +56 -0
- package/dist/templates/ainative/agents/native-product-owner.md +68 -0
- package/dist/templates/ainative/agents/native-reviewer.md +65 -0
- package/dist/templates/ainative/agents/native-verifier.md +58 -0
- package/dist/templates/ainative/artifacts/build-plan.md +56 -0
- package/dist/templates/ainative/artifacts/implement.md +55 -0
- package/dist/templates/ainative/artifacts/intent.md +50 -0
- package/dist/templates/ainative/artifacts/maintain.md +85 -0
- package/dist/templates/ainative/artifacts/review.md +68 -0
- package/dist/templates/ainative/artifacts/spec.md +69 -0
- package/dist/templates/ainative/artifacts/verify.md +55 -0
- package/dist/templates/ainative/skills/native-build-plan.md +74 -0
- package/dist/templates/ainative/skills/native-implement.md +62 -0
- package/dist/templates/ainative/skills/native-intent.md +50 -0
- package/dist/templates/ainative/skills/native-maintain.md +94 -0
- package/dist/templates/ainative/skills/native-review.md +82 -0
- package/dist/templates/ainative/skills/native-spec.md +83 -0
- package/dist/templates/ainative/skills/native-verify.md +60 -0
- package/dist/templates/sdlc/agents/designer.md +87 -0
- package/dist/templates/sdlc/agents/developer.md +178 -0
- package/dist/templates/sdlc/agents/example-with-skills.md +59 -0
- package/dist/templates/sdlc/agents/orchestrator.md +186 -0
- package/dist/templates/sdlc/agents/po.md +115 -0
- package/dist/templates/sdlc/agents/qa.md +141 -0
- package/dist/templates/sdlc/agents/tech-lead.md +124 -0
- package/dist/templates/sdlc/artifacts/design.md +130 -0
- package/dist/templates/sdlc/artifacts/execute-test.md +64 -0
- package/dist/templates/sdlc/artifacts/implement.backend.md +96 -0
- package/dist/templates/sdlc/artifacts/implement.md +70 -0
- package/dist/templates/sdlc/artifacts/implement.web-react.md +109 -0
- package/dist/templates/sdlc/artifacts/implement.web.md +99 -0
- package/dist/templates/sdlc/artifacts/plan.md +97 -0
- package/dist/templates/sdlc/artifacts/prototype.md +141 -0
- package/dist/templates/sdlc/artifacts/test-cases.md +47 -0
- package/dist/templates/sdlc/artifacts/test-plan.md +100 -0
- package/dist/templates/sdlc/artifacts/unit-test.md +32 -0
- package/dist/templates/sdlc/profiles/agile-lite.yaml +29 -0
- package/dist/templates/sdlc/profiles/hybrid.yaml +43 -0
- package/dist/templates/sdlc/profiles/iso-ieee.yaml +67 -0
- package/dist/templates/sdlc/profiles/none.yaml +21 -0
- package/dist/templates/sdlc/skills/advance-epic.md +77 -0
- package/dist/templates/sdlc/skills/analyze-requirements.md +279 -0
- package/dist/templates/sdlc/skills/discovery-gate.md +118 -0
- package/dist/templates/sdlc/skills/execute-test.md +74 -0
- package/dist/templates/sdlc/skills/generate-test-cases.md +77 -0
- package/dist/templates/sdlc/skills/implement.md +74 -0
- package/dist/templates/sdlc/skills/prd.md +114 -0
- package/dist/templates/sdlc/skills/prototype.md +128 -0
- package/dist/templates/sdlc/skills/tech-design.md +134 -0
- package/dist/templates/sdlc/skills/test-plan.md +115 -0
- package/dist/templates/sdlc/skills/test-report.md +38 -0
- package/dist/templates/sdlc/skills/unit-test.md +43 -0
- package/dist/templates/sdlc/validators/ci.mjs +49 -0
- package/dist/templates/sdlc/validators/traceability.mjs +244 -0
- package/dist/templates/speckit/agents/analyst.md +42 -0
- package/dist/templates/speckit/agents/speckit-developer.md +36 -0
- package/dist/templates/speckit/agents/speckit-qa.md +38 -0
- package/dist/templates/speckit/agents/speckit-tech-lead.md +40 -0
- package/dist/templates/speckit/artifacts/analyze.md +48 -0
- package/dist/templates/speckit/artifacts/clarify.md +64 -0
- package/dist/templates/speckit/artifacts/plan.md +57 -0
- package/dist/templates/speckit/artifacts/specify.md +64 -0
- package/dist/templates/speckit/artifacts/tasks.md +33 -0
- package/dist/templates/speckit/skills/analyze.md +50 -0
- package/dist/templates/speckit/skills/clarify.md +31 -0
- package/dist/templates/speckit/skills/plan.md +56 -0
- package/dist/templates/speckit/skills/specify.md +52 -0
- package/dist/templates/speckit/skills/speckit-implement.md +33 -0
- package/dist/templates/speckit/skills/tasks.md +38 -0
- package/dist/tools/epic-memory-hook.mjs +78 -0
- package/dist/tools/epic-memory.mjs +130 -0
- package/dist/tools/md-to-html.mjs +478 -0
- package/dist/tools/vendor/marked.esm.mjs +2432 -0
- package/dist/vendor/annotron/LICENSE +21 -0
- package/dist/vendor/annotron/README.md +213 -0
- package/dist/vendor/annotron/bin/annotron +467 -0
- package/dist/vendor/annotron/commands/annotron.md +11 -0
- package/dist/vendor/annotron/hooks/annotron-hook.sh +38 -0
- package/dist/vendor/annotron/hooks/hooks.json +50 -0
- package/dist/vendor/annotron/package.json +72 -0
- package/dist/vendor/annotron/skills/annotron-review/SKILL.md +86 -0
- package/dist/vendor/annotron/src/chrome.html +2029 -0
- package/dist/vendor/annotron/src/mdRender.js +208 -0
- package/dist/vendor/annotron/src/sdk.js +1042 -0
- package/dist/vendor/annotron/src/server.js +780 -0
- package/package.json +60 -0
|
@@ -0,0 +1,2029 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>annotron</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Petrona:ital,wght@0,300;0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
10
|
+
<style>
|
|
11
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
12
|
+
|
|
13
|
+
:root {
|
|
14
|
+
--bar-h: 60px;
|
|
15
|
+
--sidebar-w: 388px;
|
|
16
|
+
--radius: 9px;
|
|
17
|
+
|
|
18
|
+
--bg: #F3F3F8;
|
|
19
|
+
--panel: #fff;
|
|
20
|
+
--ink: #1B1A3D;
|
|
21
|
+
--accent: #2741F1;
|
|
22
|
+
--accent-hover: #1f34c9;
|
|
23
|
+
--accent-soft: rgba(39,65,241,.1);
|
|
24
|
+
--purple: #4E11B2;
|
|
25
|
+
--purple-soft: #8C78F3;
|
|
26
|
+
--coral: #D88689;
|
|
27
|
+
--green: #2fbf71;
|
|
28
|
+
--green-ink: #1c8a4e;
|
|
29
|
+
|
|
30
|
+
--line-08: rgba(27,26,61,.08);
|
|
31
|
+
--line-10: rgba(27,26,61,.10);
|
|
32
|
+
--line-14: rgba(27,26,61,.14);
|
|
33
|
+
--dim-45: rgba(27,26,61,.45);
|
|
34
|
+
--dim-5: rgba(27,26,61,.5);
|
|
35
|
+
--dim-6: rgba(27,26,61,.6);
|
|
36
|
+
--dim-7: rgba(27,26,61,.7);
|
|
37
|
+
--ink-82: rgba(27,26,61,.82);
|
|
38
|
+
|
|
39
|
+
--ui: 'Inter', system-ui, -apple-system, sans-serif;
|
|
40
|
+
--serif: 'Petrona', Georgia, serif;
|
|
41
|
+
--mono: ui-monospace, Menlo, monospace;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
html, body { height: 100%; }
|
|
45
|
+
body {
|
|
46
|
+
font-family: var(--ui); font-size: 14px;
|
|
47
|
+
background: var(--bg); color: var(--ink);
|
|
48
|
+
-webkit-font-smoothing: antialiased;
|
|
49
|
+
}
|
|
50
|
+
::selection { background: rgba(39,65,241,.18); }
|
|
51
|
+
::-webkit-scrollbar { width: 11px; height: 11px; }
|
|
52
|
+
::-webkit-scrollbar-thumb { background: rgba(27,26,61,.16); border-radius: 99px; border: 3px solid transparent; background-clip: content-box; }
|
|
53
|
+
::-webkit-scrollbar-thumb:hover { background: rgba(27,26,61,.28); background-clip: content-box; }
|
|
54
|
+
|
|
55
|
+
@keyframes cfFade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
|
56
|
+
@keyframes cfPulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
|
|
57
|
+
@keyframes thinking-bounce { 0%,80%,100% { transform: scale(.6); opacity: .35; } 40% { transform: scale(1); opacity: 1; } }
|
|
58
|
+
|
|
59
|
+
button { font-family: var(--ui); border: none; cursor: pointer; }
|
|
60
|
+
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
61
|
+
button:disabled { opacity: .45; cursor: default; }
|
|
62
|
+
kbd { padding: 1px 5px; border-radius: 5px; background: var(--bg); border: 1px solid var(--line-10); font: 600 10px var(--mono); color: var(--dim-6); }
|
|
63
|
+
|
|
64
|
+
/* ── Topbar ─────────────────────────────────────────────────────────── */
|
|
65
|
+
#topbar {
|
|
66
|
+
display: flex; align-items: center; gap: 18px;
|
|
67
|
+
height: var(--bar-h); padding: 0 22px;
|
|
68
|
+
background: var(--panel); border-bottom: 1px solid var(--line-08);
|
|
69
|
+
flex: none; z-index: 30;
|
|
70
|
+
}
|
|
71
|
+
#topbar .left { display: flex; align-items: center; gap: 13px; min-width: 0; flex: 1; }
|
|
72
|
+
#topbar .logo { font: 700 17px var(--ui); letter-spacing: -.03em; color: var(--accent); flex: none; }
|
|
73
|
+
#topbar .divider { width: 1px; height: 20px; background: var(--line-14); flex: none; }
|
|
74
|
+
#file-crumb { display: inline-flex; align-items: center; gap: 7px; min-width: 0; font: 400 12.5px/1 var(--mono); color: var(--dim-5); }
|
|
75
|
+
#file-crumb svg { flex: none; opacity: .6; }
|
|
76
|
+
#file-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left; }
|
|
77
|
+
#topbar .right { display: flex; align-items: center; gap: 8px; flex: none; }
|
|
78
|
+
|
|
79
|
+
.live-pill { display: inline-flex; align-items: center; gap: 7px; padding: 0 11px; height: 32px; border-radius: 999px; background: var(--bg); font: 500 12px var(--ui); color: var(--dim-6); }
|
|
80
|
+
.live-pill .dot { width: 7px; height: 7px; border-radius: 50%; background: rgba(27,26,61,.3); }
|
|
81
|
+
.live-pill.live .dot { background: var(--green); }
|
|
82
|
+
|
|
83
|
+
.btn-ghost { display: inline-flex; align-items: center; gap: 7px; height: 36px; padding: 0 14px; border-radius: var(--radius); border: 1px solid var(--line-14); background: var(--panel); color: var(--ink); font: 600 13px var(--ui); }
|
|
84
|
+
.btn-ghost:hover:not(:disabled) { background: var(--bg); }
|
|
85
|
+
#btn-annotate.active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
|
|
86
|
+
.btn-primary { display: inline-flex; align-items: center; gap: 7px; height: 36px; padding: 0 16px; border-radius: var(--radius); background: var(--accent); color: #fff; font: 600 13px var(--ui); box-shadow: 0 1px 2px rgba(27,26,61,.18); }
|
|
87
|
+
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); }
|
|
88
|
+
.btn-primary:active:not(:disabled) { transform: scale(.98); }
|
|
89
|
+
#btn-help { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: var(--radius); border: 1px solid var(--line-14); background: var(--panel); color: var(--dim-6); font: 600 14px var(--ui); }
|
|
90
|
+
#btn-help:hover { background: var(--bg); color: var(--ink); }
|
|
91
|
+
|
|
92
|
+
/* ── Layout ─────────────────────────────────────────────────────────── */
|
|
93
|
+
#main { display: flex; height: calc(100vh - var(--bar-h)); }
|
|
94
|
+
|
|
95
|
+
#outline-sidebar { width: 280px; flex: none; display: flex; flex-direction: column; background: var(--panel); border-right: 1px solid var(--line-08); min-height: 0; transition: width .2s ease; }
|
|
96
|
+
#outline-sidebar.hidden { display: none; }
|
|
97
|
+
#outline-sidebar.collapsed { width: 48px; }
|
|
98
|
+
#outline-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid var(--line-08); flex: none; }
|
|
99
|
+
#outline-title { font: 600 12px var(--ui); color: var(--dim-6); text-transform: uppercase; letter-spacing: .06em; flex: 1; overflow: hidden; white-space: nowrap; }
|
|
100
|
+
#outline-title.hidden { display: none; }
|
|
101
|
+
#btn-outline-toggle { width: 32px; height: 32px; padding: 0; border-radius: 6px; border: none; background: none; cursor: pointer; color: var(--dim-5); font-size: 16px; display: flex; align-items: center; justify-content: center; transition: background .15s; }
|
|
102
|
+
#btn-outline-toggle:hover { background: rgba(39,65,241,.08); color: var(--accent); }
|
|
103
|
+
#outline-list { flex: 1; min-height: 0; overflow: auto; padding: 12px 0; }
|
|
104
|
+
#outline-sidebar.collapsed #outline-list { padding: 4px 0; }
|
|
105
|
+
.outline-item { padding: 8px 16px; font: 13px var(--ui); color: var(--dim-5); cursor: pointer; border-left: 3px solid transparent; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: background .15s, border-color .15s, color .15s; }
|
|
106
|
+
#outline-sidebar.collapsed .outline-item { padding: 8px 6px; font-size: 0; }
|
|
107
|
+
#outline-sidebar.collapsed .outline-item::before { content: '•'; font-size: 12px; }
|
|
108
|
+
.outline-item:hover { background: rgba(39,65,241,.06); color: var(--ink); }
|
|
109
|
+
.outline-item.active { border-left-color: var(--accent); color: var(--accent); background: rgba(39,65,241,.08); }
|
|
110
|
+
#outline-sidebar.collapsed .outline-item.active { border-left-color: transparent; }
|
|
111
|
+
.outline-item.level-1 { padding-left: 16px; font-weight: 600; }
|
|
112
|
+
.outline-item.level-2 { padding-left: 32px; }
|
|
113
|
+
.outline-item.level-3 { padding-left: 48px; font-size: 12px; }
|
|
114
|
+
.outline-item.level-4 { padding-left: 64px; font-size: 12px; color: var(--dim-6); }
|
|
115
|
+
#outline-sidebar.collapsed .outline-item.level-1,
|
|
116
|
+
#outline-sidebar.collapsed .outline-item.level-2,
|
|
117
|
+
#outline-sidebar.collapsed .outline-item.level-3,
|
|
118
|
+
#outline-sidebar.collapsed .outline-item.level-4 { padding-left: 6px; }
|
|
119
|
+
|
|
120
|
+
#iframe-wrap { flex: 1; min-width: 0; position: relative; background: var(--bg); }
|
|
121
|
+
|
|
122
|
+
/* Markdown source editor (Markdown mode only) */
|
|
123
|
+
#md-editor { width: 44%; min-width: 320px; max-width: 720px; flex: none; display: flex; flex-direction: column; background: var(--panel); border-right: 1px solid var(--line-08); min-height: 0; }
|
|
124
|
+
#md-editor[hidden] { display: none; }
|
|
125
|
+
.mde-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid var(--line-08); font: 600 12px var(--ui); color: var(--dim-6); text-transform: uppercase; letter-spacing: .06em; flex: none; }
|
|
126
|
+
.mde-status { font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--dim-45); }
|
|
127
|
+
.mde-status.dirty { color: var(--orange, #e07d00); }
|
|
128
|
+
.mde-status.saved { color: var(--green-ink); }
|
|
129
|
+
#md-textarea { flex: 1; min-height: 0; width: 100%; border: none; outline: none; resize: none; padding: 16px 18px; background: var(--panel); color: var(--ink); font: 13px/1.65 var(--mono); tab-size: 2; white-space: pre; overflow: auto; }
|
|
130
|
+
.mde-foot { flex: none; padding: 7px 14px; border-top: 1px solid var(--line-08); background: #FAFAFC; font: 400 11px var(--ui); color: var(--dim-45); display: flex; align-items: center; gap: 6px; }
|
|
131
|
+
iframe { width: 100%; height: 100%; border: none; display: block; background: #fff; }
|
|
132
|
+
|
|
133
|
+
#sidebar { width: var(--sidebar-w); flex: none; display: flex; flex-direction: column; background: var(--panel); border-left: 1px solid var(--line-08); min-height: 0; position: relative; }
|
|
134
|
+
|
|
135
|
+
.resize-handle {
|
|
136
|
+
position: absolute;
|
|
137
|
+
cursor: row-resize;
|
|
138
|
+
background: transparent;
|
|
139
|
+
transition: background 0.2s;
|
|
140
|
+
z-index: 100;
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
}
|
|
145
|
+
.resize-handle.h-resize {
|
|
146
|
+
cursor: col-resize;
|
|
147
|
+
width: 6px;
|
|
148
|
+
top: 0;
|
|
149
|
+
bottom: 0;
|
|
150
|
+
}
|
|
151
|
+
.resize-handle.h-resize::before {
|
|
152
|
+
content: '';
|
|
153
|
+
width: 3px;
|
|
154
|
+
height: 40px;
|
|
155
|
+
background: var(--line-14);
|
|
156
|
+
border-radius: 2px;
|
|
157
|
+
transition: background 0.2s;
|
|
158
|
+
}
|
|
159
|
+
.resize-handle.h-resize:hover::before {
|
|
160
|
+
background: var(--accent);
|
|
161
|
+
box-shadow: 0 0 8px rgba(39, 65, 241, 0.3);
|
|
162
|
+
}
|
|
163
|
+
.resize-handle.v-resize {
|
|
164
|
+
height: 6px;
|
|
165
|
+
left: 0;
|
|
166
|
+
right: 0;
|
|
167
|
+
}
|
|
168
|
+
.resize-handle.v-resize::before {
|
|
169
|
+
content: '';
|
|
170
|
+
width: 40px;
|
|
171
|
+
height: 3px;
|
|
172
|
+
background: var(--line-14);
|
|
173
|
+
border-radius: 2px;
|
|
174
|
+
transition: background 0.2s;
|
|
175
|
+
}
|
|
176
|
+
.resize-handle.v-resize:hover::before {
|
|
177
|
+
background: var(--accent);
|
|
178
|
+
box-shadow: 0 0 8px rgba(39, 65, 241, 0.3);
|
|
179
|
+
}
|
|
180
|
+
.resize-handle.v-top {
|
|
181
|
+
top: -3px;
|
|
182
|
+
margin-top: 0;
|
|
183
|
+
}
|
|
184
|
+
.resize-handle.v-bottom {
|
|
185
|
+
bottom: -3px;
|
|
186
|
+
margin-bottom: 0;
|
|
187
|
+
}
|
|
188
|
+
.resize-handle.h-left {
|
|
189
|
+
left: -3px;
|
|
190
|
+
margin-left: 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Tabs */
|
|
194
|
+
#sidebar-tabs { display: flex; gap: 4px; padding: 8px 12px 0; border-bottom: 1px solid var(--line-08); flex: none; }
|
|
195
|
+
.stab { position: relative; height: 42px; padding: 0 14px; background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; font: 600 13.5px var(--ui); color: var(--dim-5); display: inline-flex; align-items: center; }
|
|
196
|
+
.stab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
197
|
+
.stab:hover:not(.active) { color: var(--ink); }
|
|
198
|
+
.badge { margin-left: 7px; padding: 1px 7px; border-radius: 999px; background: var(--accent-soft); color: var(--accent); font: 600 11px var(--ui); }
|
|
199
|
+
|
|
200
|
+
/* Tab panels */
|
|
201
|
+
.tab-panel { display: none; flex: 1; min-height: 0; overflow: auto; }
|
|
202
|
+
.tab-panel.active { display: flex; flex-direction: column; }
|
|
203
|
+
|
|
204
|
+
#annotations-list, #history-list, #history-resolved { padding: 14px; display: flex; flex-direction: column; gap: 11px; flex: 1; }
|
|
205
|
+
/* Resolved comments archived at the top of the History tab */
|
|
206
|
+
#history-resolved:empty { display: none; }
|
|
207
|
+
#history-resolved { padding-bottom: 4px; }
|
|
208
|
+
.hist-section-label { font: 600 11px var(--ui); letter-spacing: .07em; text-transform: uppercase; color: var(--dim-45); padding: 2px 2px 0; }
|
|
209
|
+
|
|
210
|
+
/* Composer card (new comment) */
|
|
211
|
+
.composer-card { flex-shrink: 0; animation: cfFade .18s ease-out; border: 1px solid rgba(39,65,241,.3); border-radius: 14px; background: var(--panel); box-shadow: 0 6px 20px rgba(27,26,61,.1); padding: 13px; }
|
|
212
|
+
.composer-card .cc-head { display: flex; align-items: center; gap: 6px; font: 600 11px var(--ui); letter-spacing: .09em; text-transform: uppercase; color: var(--accent); margin-bottom: 9px; }
|
|
213
|
+
.composer-card .cc-quote { border-left: 3px solid var(--coral); padding: 2px 0 2px 10px; font: italic 400 12.5px/1.5 var(--ui); color: var(--dim-6); margin-bottom: 11px; max-height: 66px; overflow: hidden; }
|
|
214
|
+
.composer-card textarea { width: 100%; min-height: 66px; resize: vertical; border: 1px solid var(--line-14); border-radius: 10px; padding: 10px 11px; font: 400 13.5px/1.5 var(--ui); color: var(--ink); outline: none; }
|
|
215
|
+
.composer-card textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(39,65,241,.12); }
|
|
216
|
+
.cc-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; }
|
|
217
|
+
.cc-cancel { height: 32px; padding: 0 13px; border-radius: 8px; border: 1px solid var(--line-14); background: var(--panel); color: var(--dim-7); font: 600 12.5px var(--ui); }
|
|
218
|
+
.cc-cancel:hover { background: var(--bg); }
|
|
219
|
+
.cc-submit { height: 32px; padding: 0 15px; border-radius: 8px; border: none; background: var(--accent); color: #fff; font: 600 12.5px var(--ui); }
|
|
220
|
+
.cc-submit:hover { background: var(--accent-hover); }
|
|
221
|
+
|
|
222
|
+
/* Comment cards */
|
|
223
|
+
.ann-card { flex-shrink: 0; cursor: pointer; border-radius: 14px; padding: 13px 14px; background: var(--panel); border: 1px solid var(--line-10); box-shadow: 0 1px 2px rgba(27,26,61,.04); transition: border-color .15s, box-shadow .15s; animation: cfFade .15s ease-out; }
|
|
224
|
+
.ann-card:hover { border-color: rgba(39,65,241,.4); }
|
|
225
|
+
.ann-card.selected { border-color: var(--accent); box-shadow: 0 4px 16px rgba(39,65,241,.14); }
|
|
226
|
+
.ann-card.pending { border-left: 3px solid var(--coral); }
|
|
227
|
+
.ann-card.resolved { background: #FAFBFA; opacity: .85; }
|
|
228
|
+
.ann-card.failed { border-color: #E5484D; box-shadow: 0 0 0 1px rgba(229,72,77,.35); }
|
|
229
|
+
.ann-card.failed:hover { border-color: #E5484D; }
|
|
230
|
+
|
|
231
|
+
.ann-head { display: flex; align-items: center; gap: 9px; margin-bottom: 9px; }
|
|
232
|
+
.ann-avatar { width: 28px; height: 28px; border-radius: 50%; flex: none; display: flex; align-items: center; justify-content: center; color: #fff; font: 600 11px var(--ui); }
|
|
233
|
+
.ann-who { flex: 1; min-width: 0; }
|
|
234
|
+
.ann-author { font: 600 12.5px var(--ui); color: var(--ink); }
|
|
235
|
+
.ann-time { font: 400 11px var(--ui); color: var(--dim-45); }
|
|
236
|
+
.ann-badge-resolved { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; background: rgba(47,191,113,.12); color: var(--green-ink); font: 600 10.5px var(--ui); }
|
|
237
|
+
.ann-badge-failed { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; background: rgba(229,72,77,.12); color: #C1272D; font: 600 10.5px var(--ui); }
|
|
238
|
+
/* Retry-all banner (shown above the list when comments failed to apply) */
|
|
239
|
+
.retry-banner { flex-shrink: 0; display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 12px; border: 1px solid rgba(229,72,77,.4); background: rgba(229,72,77,.07); animation: cfFade .15s ease-out; }
|
|
240
|
+
.retry-banner .rb-text { flex: 1; font: 500 12.5px var(--ui); color: #C1272D; }
|
|
241
|
+
.retry-banner .rb-btn { flex: none; display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px; border-radius: 8px; background: #E5484D; color: #fff; font: 600 12px var(--ui); border: none; }
|
|
242
|
+
.retry-banner .rb-btn:hover { background: #C1272D; }
|
|
243
|
+
.retry-banner .rb-btn:disabled { opacity: .5; cursor: default; }
|
|
244
|
+
.ann-action.retry { color: #C1272D; }
|
|
245
|
+
.ann-action.retry:hover { color: #E5484D; }
|
|
246
|
+
.ann-kind-tag { padding: 2px 8px; border-radius: 999px; background: var(--bg); color: var(--dim-6); font: 600 10px var(--ui); text-transform: uppercase; letter-spacing: .04em; }
|
|
247
|
+
.ann-remove { margin-left: 4px; width: 22px; height: 22px; border-radius: 6px; background: none; color: var(--dim-45); font-size: 15px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; flex: none; }
|
|
248
|
+
.ann-remove:hover { background: var(--bg); color: var(--coral); }
|
|
249
|
+
|
|
250
|
+
.ann-quote { border-left: 3px solid rgba(216,134,137,.7); padding: 1px 0 1px 9px; font: italic 400 12px/1.45 var(--ui); color: var(--dim-5); margin-bottom: 8px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
|
|
251
|
+
|
|
252
|
+
.ann-thread { display: flex; flex-direction: column; gap: 8px; }
|
|
253
|
+
.tmsg { font: 400 13.5px/1.55 var(--ui); color: var(--ink-82); }
|
|
254
|
+
.tmsg.agent { background: rgba(140,120,243,.09); border-radius: 10px; padding: 8px 10px; }
|
|
255
|
+
.tmsg .tmsg-who { font: 600 10.5px var(--ui); letter-spacing: .04em; text-transform: uppercase; margin-bottom: 3px; }
|
|
256
|
+
.tmsg.agent .tmsg-who { color: var(--purple); }
|
|
257
|
+
|
|
258
|
+
.ann-actions { display: flex; gap: 14px; align-items: center; margin-top: 11px; padding-top: 10px; border-top: 1px solid rgba(27,26,61,.07); }
|
|
259
|
+
.ann-action { display: inline-flex; align-items: center; gap: 5px; background: none; border: none; padding: 0; font: 600 12px var(--ui); color: var(--dim-5); }
|
|
260
|
+
.ann-action:hover { color: var(--accent); }
|
|
261
|
+
.tmsg-copy, .msg-copy { display: inline-flex; align-items: center; background: none; border: none; padding: 0; font: 600 11px var(--ui); color: var(--dim-45); }
|
|
262
|
+
.tmsg-copy:hover, .msg-copy:hover { color: var(--accent); }
|
|
263
|
+
|
|
264
|
+
/* Inline reply / note editors */
|
|
265
|
+
.ann-editor { margin-top: 10px; }
|
|
266
|
+
.ann-editor textarea { width: 100%; min-height: 54px; resize: vertical; border: 1px solid var(--line-14); border-radius: 10px; padding: 9px 10px; font: 400 13px/1.5 var(--ui); color: var(--ink); outline: none; }
|
|
267
|
+
.ann-editor textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(39,65,241,.1); }
|
|
268
|
+
.ann-editor-row { display: flex; gap: 8px; justify-content: flex-end; align-items: center; margin-top: 8px; }
|
|
269
|
+
.mini-attach { display: inline-flex; align-items: center; gap: 5px; height: 30px; padding: 0 10px; border-radius: 8px; border: 1px solid var(--line-14); background: var(--panel); color: var(--dim-7); font: 600 12px var(--ui); margin-right: auto; }
|
|
270
|
+
.mini-attach:hover { background: var(--bg); }
|
|
271
|
+
|
|
272
|
+
/* Empty states */
|
|
273
|
+
.empty-state { text-align: center; padding: 20px 16px; color: var(--dim-45); }
|
|
274
|
+
.empty-state .es-icon { width: 40px; height: 40px; margin: 0 auto 10px; border-radius: 12px; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #B4A8FC, var(--purple-soft)); box-shadow: 0 4px 12px rgba(78,17,178,.15); }
|
|
275
|
+
.empty-state .es-title { font: 600 12px var(--ui); color: var(--ink); margin-bottom: 4px; }
|
|
276
|
+
.empty-state .es-sub { font: 400 11px/1.5 var(--ui); max-width: 240px; margin: 0 auto; }
|
|
277
|
+
.empty-state b { color: var(--accent); font-weight: 600; }
|
|
278
|
+
|
|
279
|
+
/* History timeline */
|
|
280
|
+
#history-list { padding-left: 22px; }
|
|
281
|
+
.hist-row { display: flex; gap: 13px; padding-bottom: 20px; position: relative; }
|
|
282
|
+
.hist-rail { flex: none; display: flex; flex-direction: column; align-items: center; }
|
|
283
|
+
.hist-dot { width: 11px; height: 11px; border-radius: 50%; flex: none; margin-top: 3px; }
|
|
284
|
+
.hist-line { flex: 1; width: 2px; background: var(--line-08); margin-top: 2px; }
|
|
285
|
+
.hist-row:last-child .hist-line { display: none; }
|
|
286
|
+
.hist-text { font: 400 13px/1.5 var(--ui); color: var(--ink-82); }
|
|
287
|
+
.hist-time { font: 400 11px var(--ui); color: var(--dim-45); margin-top: 2px; }
|
|
288
|
+
|
|
289
|
+
/* Attach previews */
|
|
290
|
+
.attach-previews { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
291
|
+
.attach-previews:empty { display: none; }
|
|
292
|
+
.attach-thumb { position: relative; width: 52px; height: 52px; border-radius: 8px; border: 1px solid var(--line-14); overflow: hidden; background: var(--bg); flex: none; }
|
|
293
|
+
.attach-previews.mini .attach-thumb { width: 40px; height: 40px; }
|
|
294
|
+
.attach-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
295
|
+
.attach-thumb .attach-remove { position: absolute; top: 1px; right: 1px; width: 18px; height: 18px; border-radius: 50%; background: rgba(0,0,0,.6); color: #fff; font-size: 13px; line-height: 16px; padding: 0; }
|
|
296
|
+
.attach-thumb .attach-remove:hover { background: var(--coral); }
|
|
297
|
+
|
|
298
|
+
/* Agent activity / conversation strip */
|
|
299
|
+
#conversation-wrap { flex: none; border-top: 1px solid var(--line-08); background: #FAFAFC; max-height: 210px; overflow: auto; }
|
|
300
|
+
#conversation-wrap:empty { display: none; }
|
|
301
|
+
#conversation { padding: 12px 14px; display: flex; flex-direction: column; gap: 10px; }
|
|
302
|
+
.msg { font: 400 13px/1.55 var(--ui); }
|
|
303
|
+
.msg .msg-label { font: 600 10.5px var(--ui); letter-spacing: .04em; text-transform: uppercase; color: var(--purple); margin-bottom: 4px; }
|
|
304
|
+
.msg.agent { background: rgba(140,120,243,.09); border-radius: 10px; padding: 9px 11px; color: var(--ink-82); }
|
|
305
|
+
.msg.human .msg-label { color: var(--accent); }
|
|
306
|
+
.msg.cancelled { opacity: .7; }
|
|
307
|
+
.msg.cancelled .msg-label { color: var(--coral); }
|
|
308
|
+
.thinking-dots { display: flex; gap: 5px; align-items: center; padding: 2px 0; }
|
|
309
|
+
.thinking-dots span { width: 7px; height: 7px; border-radius: 50%; background: var(--purple-soft); animation: thinking-bounce 1.2s ease-in-out infinite; }
|
|
310
|
+
.thinking-dots span:nth-child(2) { animation-delay: .2s; }
|
|
311
|
+
.thinking-dots span:nth-child(3) { animation-delay: .4s; }
|
|
312
|
+
.work-steps { display: flex; flex-direction: column; gap: 3px; margin: 2px 0; }
|
|
313
|
+
.work-step { font: 400 12px/1.4 var(--ui); color: var(--ink-82); display: flex; gap: 6px; align-items: baseline; }
|
|
314
|
+
.work-step::before { content: '›'; color: var(--purple-soft); flex: none; }
|
|
315
|
+
.work-step.done { color: var(--dim-45); }
|
|
316
|
+
.work-step.done::before { content: '✓'; color: var(--green); }
|
|
317
|
+
|
|
318
|
+
/* Permission cards */
|
|
319
|
+
#perm-requests { display: flex; flex-direction: column; gap: 8px; padding: 0 14px; flex: none; }
|
|
320
|
+
#perm-requests:empty { display: none; }
|
|
321
|
+
#perm-requests:not(:empty) { padding-top: 12px; }
|
|
322
|
+
.perm-card { border: 1px solid #E7A23B; border-left: 3px solid #E7A23B; border-radius: 12px; background: #FFF9F0; padding: 11px 12px; animation: cfFade .15s ease-out; }
|
|
323
|
+
.perm-head { font: 600 10.5px var(--ui); letter-spacing: .06em; text-transform: uppercase; color: #B5730C; margin-bottom: 5px; }
|
|
324
|
+
.perm-tool { font: 500 12.5px var(--mono); color: var(--ink); word-break: break-word; margin-bottom: 9px; }
|
|
325
|
+
.perm-actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
326
|
+
.perm-actions button { height: 30px; padding: 0 12px; border-radius: 8px; font: 600 12px var(--ui); }
|
|
327
|
+
.perm-allow { background: var(--green); color: #fff; }
|
|
328
|
+
.perm-always { background: var(--panel); border: 1px solid var(--line-14); color: var(--ink); }
|
|
329
|
+
.perm-always:hover { background: var(--bg); }
|
|
330
|
+
.perm-deny { background: var(--coral); color: #fff; }
|
|
331
|
+
|
|
332
|
+
/* Bottom composer */
|
|
333
|
+
#composer { flex: none; border-top: 1px solid var(--line-08); background: var(--panel); padding: 12px 14px 14px; }
|
|
334
|
+
#agent-info-bar {
|
|
335
|
+
padding: 8px 12px;
|
|
336
|
+
background: var(--bg);
|
|
337
|
+
border-radius: 6px;
|
|
338
|
+
font: 400 11px var(--mono);
|
|
339
|
+
color: var(--dim-5);
|
|
340
|
+
margin-bottom: 10px;
|
|
341
|
+
display: flex;
|
|
342
|
+
align-items: center;
|
|
343
|
+
gap: 16px;
|
|
344
|
+
flex-wrap: wrap;
|
|
345
|
+
border: 1px solid var(--line-08);
|
|
346
|
+
}
|
|
347
|
+
.info-item { display: inline-flex; align-items: center; gap: 6px; }
|
|
348
|
+
.info-label { color: var(--dim-45); font-weight: 500; }
|
|
349
|
+
.info-value { color: var(--ink); font-weight: 600; }
|
|
350
|
+
|
|
351
|
+
#composer-status { display: flex; align-items: center; justify-content: space-between; margin-bottom: 9px; }
|
|
352
|
+
#agent-status { display: inline-flex; align-items: center; gap: 7px; font: 500 12px var(--ui); color: var(--dim-6); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
353
|
+
#agent-status .status-dot-sm { width: 8px; height: 8px; border-radius: 50%; background: rgba(27,26,61,.35); flex: none; }
|
|
354
|
+
#agent-status.working .status-dot-sm { background: var(--accent); animation: cfPulse 1s ease-in-out infinite; }
|
|
355
|
+
#agent-status.idle .status-dot-sm { background: var(--green); }
|
|
356
|
+
#agent-status.needs-perm { color: #B5730C; font-weight: 600; }
|
|
357
|
+
#agent-status.needs-perm .status-dot-sm { background: #E7A23B; }
|
|
358
|
+
#remote-approve-wrap { display: inline-flex; align-items: center; gap: 6px; font: 500 12px var(--ui); color: var(--dim-5); cursor: pointer; user-select: none; }
|
|
359
|
+
#remote-approve { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; }
|
|
360
|
+
|
|
361
|
+
/* Agent metrics bar */
|
|
362
|
+
#agent-metrics { display: flex; align-items: center; gap: 12px; padding: 8px 0; font: 400 11px var(--ui); color: var(--dim-45); flex-wrap: wrap; }
|
|
363
|
+
#agent-metrics.hidden { display: none; }
|
|
364
|
+
.metric-group { display: inline-flex; align-items: center; gap: 6px; }
|
|
365
|
+
.metric-label { color: var(--dim-6); font-weight: 500; }
|
|
366
|
+
.metric-value { color: var(--ink); font-family: var(--mono); font-weight: 600; }
|
|
367
|
+
.metric-bar { width: 100px; height: 4px; border-radius: 2px; background: var(--line-10); overflow: hidden; }
|
|
368
|
+
.metric-bar-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--purple-soft)); border-radius: 2px; transition: width .3s ease; }
|
|
369
|
+
|
|
370
|
+
/* Model selector */
|
|
371
|
+
#model-selector-wrap { display: inline-flex; align-items: center; gap: 6px; }
|
|
372
|
+
#model-selector { padding: 5px 10px; border: 1px solid var(--line-10); border-radius: 6px; background: var(--panel); color: var(--ink); font: 500 11px var(--ui); cursor: pointer; }
|
|
373
|
+
#model-selector:hover { border-color: var(--line-14); background: var(--bg); }
|
|
374
|
+
#model-selector:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
|
|
375
|
+
|
|
376
|
+
#composer-box { border: 1px solid var(--line-14); border-radius: 12px; background: var(--panel); overflow: hidden; }
|
|
377
|
+
#composer-box:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(39,65,241,.1); }
|
|
378
|
+
#free-text { width: 100%; min-height: 62px; max-height: 150px; resize: none; border: none; outline: none; padding: 11px 12px; font: 400 13.5px/1.5 var(--ui); color: var(--ink); display: block; font-family: var(--ui); }
|
|
379
|
+
#composer-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 10px; border-top: 1px solid rgba(27,26,61,.07); background: #FAFAFC; }
|
|
380
|
+
#btn-attach { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; border: 1px solid var(--line-14); background: var(--panel); color: var(--dim-7); font-size: 16px; padding: 0; cursor: pointer; }
|
|
381
|
+
#btn-attach:hover { background: var(--bg); color: var(--ink); }
|
|
382
|
+
#composer-send-group { display: flex; align-items: center; gap: 6px; }
|
|
383
|
+
#btn-send { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; border: none; background: var(--accent); color: #fff; font-size: 16px; padding: 0; cursor: pointer; }
|
|
384
|
+
#btn-send:hover:not(:disabled) { background: var(--accent-hover); }
|
|
385
|
+
#btn-cancel-agent { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; border: none; background: var(--coral); color: #fff; font-size: 16px; padding: 0; cursor: pointer; }
|
|
386
|
+
#btn-cancel-agent:hover:not(:disabled) { background: #c96e71; }
|
|
387
|
+
#composer .attach-previews { margin-top: 9px; }
|
|
388
|
+
#shortcut-hints { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 9px; font: 400 11px var(--ui); color: var(--dim-45); }
|
|
389
|
+
#shortcut-hints .hint { display: inline-flex; align-items: center; gap: 4px; }
|
|
390
|
+
|
|
391
|
+
/* Toast */
|
|
392
|
+
#toast { position: fixed; bottom: 26px; left: 50%; transform: translateX(-50%) translateY(10px); z-index: 90; display: inline-flex; align-items: center; gap: 9px; padding: 11px 18px; border-radius: 12px; background: var(--ink); color: #fff; font: 500 13px var(--ui); box-shadow: 0 12px 34px rgba(27,26,61,.35); opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; }
|
|
393
|
+
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
394
|
+
#toast svg { color: var(--purple-soft); flex: none; }
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
/* Done overlay (shown after finishing the review) */
|
|
398
|
+
#done-overlay { position: fixed; inset: 0; z-index: 200; display: none; align-items: center; justify-content: center; background: rgba(243,243,248,.82); backdrop-filter: blur(6px); }
|
|
399
|
+
#done-overlay.show { display: flex; animation: cfFade .2s ease-out; }
|
|
400
|
+
.done-card { text-align: center; background: var(--panel); border: 1px solid var(--line-10); border-radius: 20px; padding: 40px 44px; box-shadow: 0 20px 60px rgba(27,26,61,.2); max-width: 420px; }
|
|
401
|
+
.done-icon { width: 64px; height: 64px; margin: 0 auto 18px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: rgba(47,191,113,.14); color: var(--green-ink); }
|
|
402
|
+
.done-title { font: 600 20px var(--serif); color: var(--ink); margin-bottom: 8px; }
|
|
403
|
+
.done-sub { font: 400 13.5px/1.6 var(--ui); color: var(--dim-6); }
|
|
404
|
+
|
|
405
|
+
/* Undo toast */
|
|
406
|
+
#undo-toast { position: fixed; bottom: 26px; left: 50%; transform: translateX(-50%) translateY(10px); z-index: 90; display: flex; align-items: center; gap: 12px; padding: 10px 16px; border-radius: 12px; background: var(--ink); color: #fff; font: 500 13px var(--ui); box-shadow: 0 12px 34px rgba(27,26,61,.35); opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; }
|
|
407
|
+
#undo-toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); pointer-events: auto; }
|
|
408
|
+
#undo-toast button { background: var(--purple-soft); color: #fff; padding: 4px 12px; border-radius: 8px; font: 600 12px var(--ui); }
|
|
409
|
+
|
|
410
|
+
/* Help overlay */
|
|
411
|
+
#kbd-overlay { display: none; position: fixed; inset: 0; z-index: 100; background: rgba(27,26,61,.4); align-items: center; justify-content: center; }
|
|
412
|
+
#kbd-overlay.open { display: flex; }
|
|
413
|
+
#kbd-panel { background: var(--panel); border-radius: 16px; padding: 24px 28px; min-width: 320px; max-width: 90vw; box-shadow: 0 20px 60px rgba(27,26,61,.3); }
|
|
414
|
+
#kbd-panel h2 { font: 500 20px var(--serif); margin-bottom: 16px; }
|
|
415
|
+
.kbd-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid var(--line-08); gap: 16px; }
|
|
416
|
+
.kbd-row:last-of-type { border-bottom: none; }
|
|
417
|
+
.kbd-desc { font: 400 13px var(--ui); color: var(--ink-82); }
|
|
418
|
+
.kbd-keys { display: flex; gap: 4px; flex: none; }
|
|
419
|
+
#kbd-close { margin-top: 18px; width: 100%; height: 38px; border-radius: 9px; background: var(--bg); color: var(--ink); font: 600 13px var(--ui); }
|
|
420
|
+
#kbd-close:hover { background: var(--line-08); }
|
|
421
|
+
|
|
422
|
+
@media (prefers-reduced-motion: reduce) { *, .ann-card, .composer-card { animation: none !important; transition: none !important; } }
|
|
423
|
+
@media (max-width: 640px) {
|
|
424
|
+
#sidebar { width: 100%; position: fixed; bottom: 0; left: 0; right: 0; height: 56vh; border-left: none; border-top: 1px solid var(--line-08); }
|
|
425
|
+
#iframe-wrap { height: 44vh; }
|
|
426
|
+
}
|
|
427
|
+
</style>
|
|
428
|
+
</head>
|
|
429
|
+
<body>
|
|
430
|
+
|
|
431
|
+
<header id="topbar">
|
|
432
|
+
<div class="left">
|
|
433
|
+
<span class="logo">annotron</span>
|
|
434
|
+
<span class="divider"></span>
|
|
435
|
+
<span id="file-crumb">
|
|
436
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h16"/><path d="M9 4h6l5 5v9a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z"/><path d="M14 4v5h5"/></svg>
|
|
437
|
+
<span id="file-label" title="">—</span>
|
|
438
|
+
</span>
|
|
439
|
+
</div>
|
|
440
|
+
<div class="right">
|
|
441
|
+
<span class="live-pill" id="live-pill" title="Connecting…"><span class="dot"></span>Live</span>
|
|
442
|
+
<button id="btn-annotate" class="btn-ghost" aria-pressed="false" title="Toggle annotate mode (A)">
|
|
443
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>
|
|
444
|
+
<span id="annotate-label">Annotate</span>
|
|
445
|
+
</button>
|
|
446
|
+
<button id="btn-md-source" class="btn-ghost" title="Toggle Markdown source editor" style="display:none">
|
|
447
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v14H4z"/><path d="M7 15v-4l2 2 2-2v4"/><path d="M16 11v4"/><path d="M14 13l2 2 2-2"/></svg>Markdown
|
|
448
|
+
</button>
|
|
449
|
+
<button id="btn-download" class="btn-ghost" title="Download clean HTML (D)">
|
|
450
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="M8 11l4 4 4-4"/><path d="M4 21h16"/></svg>Download
|
|
451
|
+
</button>
|
|
452
|
+
<button id="btn-finalize" class="btn-primary" title="Write result to disk (Shift+F)">
|
|
453
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>Finalize
|
|
454
|
+
</button>
|
|
455
|
+
<button id="btn-save-md" class="btn-primary" title="Save changes to the .md source (Cmd/Ctrl+S)" style="display:none">
|
|
456
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><path d="M17 21v-8H7v8"/><path d="M7 3v5h8"/></svg>Save
|
|
457
|
+
</button>
|
|
458
|
+
<button id="btn-done" class="btn-ghost" title="Finish review & stop the annotron server">
|
|
459
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M18.36 6.64A9 9 0 1 1 5.64 6.64"/><path d="M12 2v10"/></svg>Done
|
|
460
|
+
</button>
|
|
461
|
+
<button id="btn-help" title="Keyboard shortcuts (?)" aria-label="Show keyboard shortcuts">?</button>
|
|
462
|
+
</div>
|
|
463
|
+
</header>
|
|
464
|
+
|
|
465
|
+
<div id="main">
|
|
466
|
+
<aside id="outline-sidebar" class="hidden">
|
|
467
|
+
<div id="outline-header">
|
|
468
|
+
<span id="outline-title">Outline</span>
|
|
469
|
+
<button id="btn-outline-toggle" title="Toggle sidebar width (Ctrl+L)">‹</button>
|
|
470
|
+
</div>
|
|
471
|
+
<div id="outline-list"></div>
|
|
472
|
+
</aside>
|
|
473
|
+
|
|
474
|
+
<div id="md-editor" hidden>
|
|
475
|
+
<div class="mde-head">
|
|
476
|
+
<span>Markdown source</span>
|
|
477
|
+
<span class="mde-status" id="mde-status"></span>
|
|
478
|
+
</div>
|
|
479
|
+
<textarea id="md-textarea" spellcheck="false" placeholder="# Loading…" wrap="off"></textarea>
|
|
480
|
+
<div class="mde-foot"><kbd>Cmd/Ctrl</kbd><kbd>S</kbd> Save · edits render on Save</div>
|
|
481
|
+
</div>
|
|
482
|
+
<div id="iframe-wrap">
|
|
483
|
+
<iframe id="artifact-frame" title="Artifact preview" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
|
|
484
|
+
</div>
|
|
485
|
+
|
|
486
|
+
<aside id="sidebar">
|
|
487
|
+
<div class="resize-handle h-resize h-left"></div>
|
|
488
|
+
<div id="sidebar-tabs">
|
|
489
|
+
<button class="stab active" data-tab="annotations">Annotations <span class="badge" id="ann-badge" style="display:none">0</span></button>
|
|
490
|
+
<button class="stab" data-tab="history">History <span class="badge" id="hist-badge" style="display:none">0</span></button>
|
|
491
|
+
</div>
|
|
492
|
+
|
|
493
|
+
<div id="tab-annotations" class="tab-panel active">
|
|
494
|
+
<div id="annotations-list"></div>
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
<div id="tab-history" class="tab-panel">
|
|
498
|
+
<div id="history-resolved"></div>
|
|
499
|
+
<div id="history-list"></div>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
<div class="resize-handle v-top"></div>
|
|
503
|
+
<div id="conversation-wrap"><div id="conversation"></div></div>
|
|
504
|
+
<div class="resize-handle v-bottom"></div>
|
|
505
|
+
|
|
506
|
+
<div id="perm-requests"></div>
|
|
507
|
+
|
|
508
|
+
<div id="composer">
|
|
509
|
+
<div id="agent-info-bar">
|
|
510
|
+
<div class="info-item">
|
|
511
|
+
<span class="info-label">🤖</span>
|
|
512
|
+
<span class="info-value" id="info-model">Default</span>
|
|
513
|
+
</div>
|
|
514
|
+
<div class="info-item">
|
|
515
|
+
<span class="info-label">🧠</span>
|
|
516
|
+
<span class="info-value" id="info-context">—</span>
|
|
517
|
+
</div>
|
|
518
|
+
<div class="info-item">
|
|
519
|
+
<span class="info-label">📊</span>
|
|
520
|
+
<span class="info-value" id="info-tokens">—</span>
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
<div id="composer-status">
|
|
524
|
+
<div style="display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0;">
|
|
525
|
+
<span id="agent-status" class="idle"><span class="status-dot-sm"></span><span id="agent-status-text">Idle</span></span>
|
|
526
|
+
<div id="model-selector-wrap">
|
|
527
|
+
<select id="model-selector" title="Select model for agent">
|
|
528
|
+
<option value="">Default</option>
|
|
529
|
+
<option value="claude-opus-4-8">Opus 4.8</option>
|
|
530
|
+
<option value="claude-sonnet-5">Sonnet 5</option>
|
|
531
|
+
<option value="claude-haiku-4-5-20251001">Haiku 4.5</option>
|
|
532
|
+
</select>
|
|
533
|
+
</div>
|
|
534
|
+
<label id="ask-mode-wrap" title="Force Ask Mode even with comments (for Q&A over edits)" style="display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--dim-6);">
|
|
535
|
+
<input type="checkbox" id="ask-mode-toggle" style="cursor: pointer;">
|
|
536
|
+
<span>Ask Mode</span>
|
|
537
|
+
</label>
|
|
538
|
+
</div>
|
|
539
|
+
<label id="remote-approve-wrap" title="Approve tool permissions from this browser instead of the terminal">
|
|
540
|
+
<input type="checkbox" id="remote-approve"> Remote approve
|
|
541
|
+
</label>
|
|
542
|
+
</div>
|
|
543
|
+
<div id="agent-metrics" class="hidden">
|
|
544
|
+
<div class="metric-group">
|
|
545
|
+
<span class="metric-label">Model:</span>
|
|
546
|
+
<span class="metric-value" id="metric-model">—</span>
|
|
547
|
+
</div>
|
|
548
|
+
<div class="metric-group">
|
|
549
|
+
<span class="metric-label">Tokens:</span>
|
|
550
|
+
<span class="metric-value" id="metric-tokens">—</span>
|
|
551
|
+
<div class="metric-bar" style="margin-left: 4px;">
|
|
552
|
+
<div class="metric-bar-fill" id="metric-tokens-bar" style="width: 0%;"></div>
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
<div class="metric-group">
|
|
556
|
+
<span class="metric-label">Context:</span>
|
|
557
|
+
<span class="metric-value" id="metric-context">—</span>
|
|
558
|
+
<div class="metric-bar" style="margin-left: 4px;">
|
|
559
|
+
<div class="metric-bar-fill" id="metric-context-bar" style="width: 0%;"></div>
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
<div id="composer-box">
|
|
564
|
+
<textarea id="free-text" placeholder="Message to agent… (Enter to send, Shift+Enter for newline, paste an image to attach)"></textarea>
|
|
565
|
+
<div id="composer-bar">
|
|
566
|
+
<button id="btn-attach" title="Attach image(s)">
|
|
567
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1.6"/><path d="M21 15l-5-5L5 21"/></svg>
|
|
568
|
+
</button>
|
|
569
|
+
<div id="composer-send-group">
|
|
570
|
+
<button id="btn-cancel-agent" title="Cancel" style="display:none">
|
|
571
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
572
|
+
</button>
|
|
573
|
+
<button id="btn-send" title="Send (Enter)">
|
|
574
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4z"/></svg>
|
|
575
|
+
</button>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
<div id="attach-previews" class="attach-previews"></div>
|
|
580
|
+
<input type="file" id="file-input" accept="image/*" multiple hidden>
|
|
581
|
+
<div id="shortcut-hints">
|
|
582
|
+
<span class="hint">Comment only → apply change</span>
|
|
583
|
+
<span class="hint">+ Message → ask question</span>
|
|
584
|
+
<span class="hint"><kbd>↵</kbd> Send</span>
|
|
585
|
+
<span class="hint"><kbd>Ctrl</kbd><kbd>Z</kbd> Undo</span>
|
|
586
|
+
<span class="hint"><kbd>Esc</kbd> Off</span>
|
|
587
|
+
<span class="hint" style="margin-left:auto"><kbd>?</kbd> Help</span>
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
</aside>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<div id="done-overlay">
|
|
594
|
+
<div class="done-card">
|
|
595
|
+
<div class="done-icon"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg></div>
|
|
596
|
+
<div class="done-title">Review complete</div>
|
|
597
|
+
<div class="done-sub">The annotron server has been stopped. You can safely close this tab.</div>
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
|
|
601
|
+
<div id="toast" role="status" aria-live="polite"></div>
|
|
602
|
+
|
|
603
|
+
<div id="undo-toast" role="status" aria-live="polite">
|
|
604
|
+
<span id="undo-msg">Removed</span>
|
|
605
|
+
<button id="btn-undo" title="Undo (Ctrl+Z)">Undo</button>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<div id="kbd-overlay" role="dialog" aria-modal="true" aria-label="Keyboard shortcuts">
|
|
609
|
+
<div id="kbd-panel">
|
|
610
|
+
<h2>Keyboard shortcuts & modes</h2>
|
|
611
|
+
<div class="kbd-row"><span class="kbd-desc">Apply edit (Edit Mode)</span><span class="kbd-keys">Comment only, no message</span></div>
|
|
612
|
+
<div class="kbd-row"><span class="kbd-desc">Ask question (Ask Mode)</span><span class="kbd-keys">Comment + Message, or Message only</span></div>
|
|
613
|
+
<div class="kbd-row"><span class="kbd-desc">Toggle annotate mode</span><span class="kbd-keys"><kbd>A</kbd></span></div>
|
|
614
|
+
<div class="kbd-row"><span class="kbd-desc">Send feedback</span><span class="kbd-keys"><kbd>Enter</kbd></span></div>
|
|
615
|
+
<div class="kbd-row"><span class="kbd-desc">Undo last removal</span><span class="kbd-keys"><kbd>Ctrl</kbd><kbd>Z</kbd></span></div>
|
|
616
|
+
<div class="kbd-row"><span class="kbd-desc">Download clean HTML</span><span class="kbd-keys"><kbd>D</kbd></span></div>
|
|
617
|
+
<div class="kbd-row"><span class="kbd-desc">Exit annotate / close</span><span class="kbd-keys"><kbd>Esc</kbd></span></div>
|
|
618
|
+
<button id="kbd-close">Close</button>
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
|
|
622
|
+
<script>
|
|
623
|
+
console.log('[annotron] chrome.html script loaded');
|
|
624
|
+
const TAG = 'annotron-sdk';
|
|
625
|
+
const params = new URLSearchParams(location.search);
|
|
626
|
+
const file = params.get('file') || '';
|
|
627
|
+
|
|
628
|
+
document.getElementById('file-label').textContent = file || '(no file)';
|
|
629
|
+
document.getElementById('file-label').title = file;
|
|
630
|
+
|
|
631
|
+
const frame = document.getElementById('artifact-frame');
|
|
632
|
+
const btnAnnotate = document.getElementById('btn-annotate');
|
|
633
|
+
const annotateLabel = document.getElementById('annotate-label');
|
|
634
|
+
const btnDownload = document.getElementById('btn-download');
|
|
635
|
+
const btnFinalize = document.getElementById('btn-finalize');
|
|
636
|
+
const btnDone = document.getElementById('btn-done');
|
|
637
|
+
const doneOverlay = document.getElementById('done-overlay');
|
|
638
|
+
const btnSend = document.getElementById('btn-send');
|
|
639
|
+
const btnAttach = document.getElementById('btn-attach');
|
|
640
|
+
const btnCancel = document.getElementById('btn-cancel-agent');
|
|
641
|
+
const fileInput = document.getElementById('file-input');
|
|
642
|
+
const attachPreviews = document.getElementById('attach-previews');
|
|
643
|
+
const permRequests = document.getElementById('perm-requests');
|
|
644
|
+
const agentStatusEl = document.getElementById('agent-status');
|
|
645
|
+
const agentStatusText= document.getElementById('agent-status-text');
|
|
646
|
+
const remoteApprove = document.getElementById('remote-approve');
|
|
647
|
+
const modelSelector = document.getElementById('model-selector');
|
|
648
|
+
const askModeToggle = document.getElementById('ask-mode-toggle');
|
|
649
|
+
const agentMetrics = document.getElementById('agent-metrics');
|
|
650
|
+
const metricModel = document.getElementById('metric-model');
|
|
651
|
+
const metricTokens = document.getElementById('metric-tokens');
|
|
652
|
+
const metricContext = document.getElementById('metric-context');
|
|
653
|
+
const metricTokensBar= document.getElementById('metric-tokens-bar');
|
|
654
|
+
const metricContextBar=document.getElementById('metric-context-bar');
|
|
655
|
+
const btnHelp = document.getElementById('btn-help');
|
|
656
|
+
const livePill = document.getElementById('live-pill');
|
|
657
|
+
const freeText = document.getElementById('free-text');
|
|
658
|
+
const toastEl = document.getElementById('toast');
|
|
659
|
+
const undoToast = document.getElementById('undo-toast');
|
|
660
|
+
const undoMsg = document.getElementById('undo-msg');
|
|
661
|
+
const btnUndo = document.getElementById('btn-undo');
|
|
662
|
+
const kbdOverlay = document.getElementById('kbd-overlay');
|
|
663
|
+
const kbdClose = document.getElementById('kbd-close');
|
|
664
|
+
const annList = document.getElementById('annotations-list');
|
|
665
|
+
const histList = document.getElementById('history-list');
|
|
666
|
+
const histResolved= document.getElementById('history-resolved');
|
|
667
|
+
const convoWrap = document.getElementById('conversation-wrap');
|
|
668
|
+
const convoEl = document.getElementById('conversation');
|
|
669
|
+
const annBadge = document.getElementById('ann-badge');
|
|
670
|
+
const histBadge = document.getElementById('hist-badge');
|
|
671
|
+
const outlineSidebar = document.getElementById('outline-sidebar');
|
|
672
|
+
const outlineList = document.getElementById('outline-list');
|
|
673
|
+
const btnOutlineToggle = document.getElementById('btn-outline-toggle');
|
|
674
|
+
const outlineTitle = document.getElementById('outline-title');
|
|
675
|
+
let outlineCollapsed = false;
|
|
676
|
+
const tabPanel = document.querySelector('.tab-panel.active');
|
|
677
|
+
const convoWrapEl = document.getElementById('conversation-wrap');
|
|
678
|
+
const composerEl = document.getElementById('composer');
|
|
679
|
+
const resizeHandles = document.querySelectorAll('.resize-handle');
|
|
680
|
+
|
|
681
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
682
|
+
let annotating = false;
|
|
683
|
+
let sdkReady = false;
|
|
684
|
+
let savedScroll = 0;
|
|
685
|
+
let serializeResolvers = {};
|
|
686
|
+
let serializeCounter = 0;
|
|
687
|
+
let undoStack = [];
|
|
688
|
+
let undoTimer = null;
|
|
689
|
+
let toastTimer = null;
|
|
690
|
+
let agentWorking = false;
|
|
691
|
+
let selectedAnnotationId = null;
|
|
692
|
+
let openReplyId = null; // annotation id whose reply editor is open
|
|
693
|
+
// Resolved annotations live in the History tab and are NOT highlighted in the
|
|
694
|
+
// artifact — except the one the reviewer is currently focusing from History,
|
|
695
|
+
// which we temporarily add back to the synced set so its location flashes.
|
|
696
|
+
let focusedResolvedId = null;
|
|
697
|
+
let lastRounds = []; // last feedback rounds from the server (History timeline)
|
|
698
|
+
let me = { name: 'You', detail: '', initials: 'YO', color: '#2741F1' }; // reviewer identity
|
|
699
|
+
|
|
700
|
+
// Image staging (runtime only — never persisted to the sidecar).
|
|
701
|
+
const composerImages = []; // main composer
|
|
702
|
+
const replyImgStore = {}; // saved-annotation id -> images staged in its reply box
|
|
703
|
+
const noteImgStore = {}; // pending-new id -> images staged in its note box
|
|
704
|
+
|
|
705
|
+
// annotations: saved annotation objects (with threads). Each:
|
|
706
|
+
// { id, kind, selector, label, text, thread:[{role,message,timestamp}], createdAt, status }
|
|
707
|
+
let annotations = [];
|
|
708
|
+
// pendingNew: new comments committed locally but not yet sent to the agent.
|
|
709
|
+
let pendingNew = [];
|
|
710
|
+
|
|
711
|
+
// Agent model & metrics
|
|
712
|
+
let selectedModel = localStorage.getItem('annotron-model') || '';
|
|
713
|
+
let forceAskMode = localStorage.getItem('annotron-ask-mode') === 'true';
|
|
714
|
+
let agentMetricsData = { model: '', tokensUsed: 0, tokensMax: 200000, contextUsed: 0, contextMax: 65536 };
|
|
715
|
+
|
|
716
|
+
// Resizable panels state
|
|
717
|
+
let maxTabPanelHeight = null;
|
|
718
|
+
|
|
719
|
+
// ── SVG helpers ──────────────────────────────────────────────────────────
|
|
720
|
+
const ICON = {
|
|
721
|
+
check: '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>',
|
|
722
|
+
resolve: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>',
|
|
723
|
+
reply: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21l3-.7a3 3 0 0 0 1.4-.8L18.5 8.4a2 2 0 0 0 0-2.8l-.1-.1a2 2 0 0 0-2.8 0L4.5 16.6a3 3 0 0 0-.8 1.4z"/></svg>',
|
|
724
|
+
retry: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>',
|
|
725
|
+
msg: '<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
|
|
726
|
+
toast: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>',
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
// ── Model selector ─────────────────────────────────────────────────────────
|
|
730
|
+
const infoModel = document.getElementById('info-model');
|
|
731
|
+
const infoContext = document.getElementById('info-context');
|
|
732
|
+
const infoTokens = document.getElementById('info-tokens');
|
|
733
|
+
|
|
734
|
+
function updateInfoBar() {
|
|
735
|
+
// Update model display
|
|
736
|
+
const modelDisplay = selectedModel ?
|
|
737
|
+
(selectedModel.includes('opus') ? 'Opus 4.8' :
|
|
738
|
+
selectedModel.includes('sonnet') ? 'Sonnet 5' :
|
|
739
|
+
selectedModel.includes('haiku') ? 'Haiku 4.5' :
|
|
740
|
+
selectedModel) : 'Default';
|
|
741
|
+
infoModel.textContent = modelDisplay;
|
|
742
|
+
|
|
743
|
+
// Update context display
|
|
744
|
+
if (agentMetricsData.contextUsed > 0) {
|
|
745
|
+
const fmt = (n) => n > 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
|
|
746
|
+
infoContext.textContent = fmt(agentMetricsData.contextUsed) + '/' + fmt(agentMetricsData.contextMax);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Update tokens display
|
|
750
|
+
if (agentMetricsData.tokensUsed > 0) {
|
|
751
|
+
const fmt = (n) => n > 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
|
|
752
|
+
infoTokens.textContent = fmt(agentMetricsData.tokensUsed) + '/' + fmt(agentMetricsData.tokensMax);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
modelSelector.value = selectedModel;
|
|
757
|
+
modelSelector.addEventListener('change', (e) => {
|
|
758
|
+
selectedModel = e.target.value;
|
|
759
|
+
localStorage.setItem('annotron-model', selectedModel);
|
|
760
|
+
updateInfoBar();
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
updateInfoBar();
|
|
764
|
+
|
|
765
|
+
// ── Ask Mode toggle ────────────────────────────────────────────────────────
|
|
766
|
+
askModeToggle.checked = forceAskMode;
|
|
767
|
+
askModeToggle.addEventListener('change', (e) => {
|
|
768
|
+
forceAskMode = e.target.checked;
|
|
769
|
+
localStorage.setItem('annotron-ask-mode', String(forceAskMode));
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// ── Metrics display ────────────────────────────────────────────────────────
|
|
773
|
+
function updateMetricsDisplay(metrics) {
|
|
774
|
+
agentMetricsData = { ...agentMetricsData, ...metrics };
|
|
775
|
+
const { model, tokensUsed, tokensMax, contextUsed, contextMax } = agentMetricsData;
|
|
776
|
+
if (model) metricModel.textContent = model;
|
|
777
|
+
if (tokensUsed > 0) {
|
|
778
|
+
const fmt = (n) => n > 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
|
|
779
|
+
metricTokens.textContent = fmt(tokensUsed) + ' / ' + fmt(tokensMax);
|
|
780
|
+
metricTokensBar.style.width = Math.min(100, (tokensUsed / tokensMax) * 100) + '%';
|
|
781
|
+
}
|
|
782
|
+
if (contextUsed > 0) {
|
|
783
|
+
const fmt = (n) => n > 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
|
|
784
|
+
metricContext.textContent = fmt(contextUsed) + ' / ' + fmt(contextMax);
|
|
785
|
+
metricContextBar.style.width = Math.min(100, (contextUsed / contextMax) * 100) + '%';
|
|
786
|
+
}
|
|
787
|
+
agentMetrics.classList.remove('hidden');
|
|
788
|
+
updateInfoBar();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// ── Sidebar tabs ───────────────────────────────────────────────────────────
|
|
792
|
+
document.querySelectorAll('.stab').forEach(btn => {
|
|
793
|
+
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
|
|
794
|
+
});
|
|
795
|
+
function switchTab(tab) {
|
|
796
|
+
document.querySelectorAll('.stab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
|
|
797
|
+
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
|
798
|
+
const newPanel = document.getElementById('tab-' + tab);
|
|
799
|
+
if (newPanel) newPanel.classList.add('active');
|
|
800
|
+
// Leaving History drops the temporary highlight on any focused resolved card.
|
|
801
|
+
if (tab !== 'history' && focusedResolvedId) {
|
|
802
|
+
focusedResolvedId = null;
|
|
803
|
+
syncAnnotationsToArtifact();
|
|
804
|
+
}
|
|
805
|
+
// Trigger auto-resize for the newly visible panel
|
|
806
|
+
setTimeout(() => {
|
|
807
|
+
const activePanel = document.querySelector('.tab-panel.active');
|
|
808
|
+
if (activePanel && maxTabPanelHeight) {
|
|
809
|
+
const scrollHeight = activePanel.scrollHeight;
|
|
810
|
+
const newHeight = Math.min(scrollHeight + 20, maxTabPanelHeight);
|
|
811
|
+
activePanel.style.maxHeight = newHeight + 'px';
|
|
812
|
+
}
|
|
813
|
+
}, 50);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ── Help overlay ───────────────────────────────────────────────────────────
|
|
817
|
+
function openHelp() { kbdOverlay.classList.add('open'); kbdClose.focus(); }
|
|
818
|
+
function closeHelp() { kbdOverlay.classList.remove('open'); btnHelp.focus(); }
|
|
819
|
+
btnHelp.addEventListener('click', openHelp);
|
|
820
|
+
kbdClose.addEventListener('click', closeHelp);
|
|
821
|
+
kbdOverlay.addEventListener('click', (e) => { if (e.target === kbdOverlay) closeHelp(); });
|
|
822
|
+
|
|
823
|
+
// ── Toast ──────────────────────────────────────────────────────────────────
|
|
824
|
+
function flashToast(msg) {
|
|
825
|
+
toastEl.innerHTML = ICON.toast + '<span></span>';
|
|
826
|
+
toastEl.lastElementChild.textContent = msg;
|
|
827
|
+
toastEl.classList.add('show');
|
|
828
|
+
clearTimeout(toastTimer);
|
|
829
|
+
toastTimer = setTimeout(() => toastEl.classList.remove('show'), 1900);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Native Cmd+C / Ctrl+C for copy - no floating button needed
|
|
833
|
+
|
|
834
|
+
// ── Time formatting ──────────────────────────────────────────────────────
|
|
835
|
+
function timeAgo(iso) {
|
|
836
|
+
if (!iso) return '';
|
|
837
|
+
const t = new Date(iso).getTime();
|
|
838
|
+
if (isNaN(t)) return '';
|
|
839
|
+
const s = Math.max(0, Math.round((Date.now() - t) / 1000));
|
|
840
|
+
if (s < 45) return 'just now';
|
|
841
|
+
const m = Math.round(s / 60);
|
|
842
|
+
if (m < 60) return m + 'm ago';
|
|
843
|
+
const h = Math.round(m / 60);
|
|
844
|
+
if (h < 24) return h + 'h ago';
|
|
845
|
+
const d = Math.round(h / 24);
|
|
846
|
+
if (d < 7) return d + 'd ago';
|
|
847
|
+
return new Date(iso).toLocaleDateString();
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// ── Artifact load ──────────────────────────────────────────────────────────
|
|
851
|
+
function loadArtifact() {
|
|
852
|
+
if (!file) return;
|
|
853
|
+
// Reset readiness here (before navigating) rather than on the iframe 'load'
|
|
854
|
+
// event: 'load' can fire *after* the SDK's sdk-ready message is processed,
|
|
855
|
+
// which would race sdkReady back to false and silently disable Finalize/Download.
|
|
856
|
+
sdkReady = false;
|
|
857
|
+
savedScroll = frame.contentWindow?.scrollY || 0;
|
|
858
|
+
frame.src = '/artifact?file=' + encodeURIComponent(file);
|
|
859
|
+
}
|
|
860
|
+
loadArtifact();
|
|
861
|
+
|
|
862
|
+
// ── Outline sidebar ────────────────────────────────────────────────────────
|
|
863
|
+
function renderOutlineWithHeadings(headings) {
|
|
864
|
+
if (!headings || headings.length === 0) {
|
|
865
|
+
outlineSidebar.classList.add('hidden');
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
outlineSidebar.classList.remove('hidden');
|
|
869
|
+
outlineList.innerHTML = '';
|
|
870
|
+
headings.forEach(h => {
|
|
871
|
+
const item = document.createElement('div');
|
|
872
|
+
item.className = `outline-item level-${h.level}`;
|
|
873
|
+
item.textContent = h.text;
|
|
874
|
+
item.addEventListener('click', () => {
|
|
875
|
+
const el = frame.contentDocument?.getElementById(h.id);
|
|
876
|
+
if (el) {
|
|
877
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
878
|
+
updateActiveOutlineItem(h.id);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
item.dataset.id = h.id;
|
|
882
|
+
outlineList.appendChild(item);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// Track scroll to highlight current outline item
|
|
886
|
+
frame.contentWindow?.addEventListener('scroll', () => {
|
|
887
|
+
if (!headings.length) return;
|
|
888
|
+
let current = headings[0].id;
|
|
889
|
+
for (const h of headings) {
|
|
890
|
+
const el = frame.contentDocument?.getElementById(h.id);
|
|
891
|
+
if (el && el.getBoundingClientRect().top < 100) current = h.id;
|
|
892
|
+
else break;
|
|
893
|
+
}
|
|
894
|
+
updateActiveOutlineItem(current);
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function updateActiveOutlineItem(activeId) {
|
|
899
|
+
document.querySelectorAll('.outline-item').forEach(item => {
|
|
900
|
+
item.classList.toggle('active', item.dataset.id === activeId);
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function toggleOutlineSidebar() {
|
|
905
|
+
outlineCollapsed = !outlineCollapsed;
|
|
906
|
+
outlineSidebar.classList.toggle('collapsed', outlineCollapsed);
|
|
907
|
+
btnOutlineToggle.textContent = outlineCollapsed ? '›' : '‹';
|
|
908
|
+
outlineTitle.classList.toggle('hidden', outlineCollapsed);
|
|
909
|
+
localStorage.setItem('outline-collapsed', outlineCollapsed);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Restore collapsed state from localStorage
|
|
913
|
+
if (localStorage.getItem('outline-collapsed') === 'true') {
|
|
914
|
+
outlineCollapsed = true;
|
|
915
|
+
outlineSidebar.classList.add('collapsed');
|
|
916
|
+
btnOutlineToggle.textContent = '›';
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
btnOutlineToggle.addEventListener('click', toggleOutlineSidebar);
|
|
920
|
+
|
|
921
|
+
// Keyboard shortcut: Ctrl+L to toggle outline
|
|
922
|
+
document.addEventListener('keydown', (e) => {
|
|
923
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'l' && !annotating) {
|
|
924
|
+
e.preventDefault();
|
|
925
|
+
toggleOutlineSidebar();
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// ── Resizable panels ───────────────────────────────────────────────────────
|
|
930
|
+
function setupResizeHandles() {
|
|
931
|
+
const sidebar = document.getElementById('sidebar');
|
|
932
|
+
|
|
933
|
+
resizeHandles.forEach((handle) => {
|
|
934
|
+
let isResizing = false;
|
|
935
|
+
let startPos = 0;
|
|
936
|
+
|
|
937
|
+
handle.addEventListener('mousedown', (e) => {
|
|
938
|
+
isResizing = true;
|
|
939
|
+
startPos = handle.classList.contains('h-resize') ? e.clientX : e.clientY;
|
|
940
|
+
document.body.style.userSelect = 'none';
|
|
941
|
+
document.body.style.cursor = handle.classList.contains('h-resize') ? 'col-resize' : 'row-resize';
|
|
942
|
+
|
|
943
|
+
// Lock max height on first drag
|
|
944
|
+
if (handle.classList.contains('v-top') && !maxTabPanelHeight) {
|
|
945
|
+
const activePanel = sidebar.querySelector('.tab-panel.active');
|
|
946
|
+
if (activePanel) {
|
|
947
|
+
maxTabPanelHeight = activePanel.offsetHeight;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
document.addEventListener('mousemove', (e) => {
|
|
953
|
+
if (!isResizing) return;
|
|
954
|
+
const sidebar = document.getElementById('sidebar');
|
|
955
|
+
|
|
956
|
+
if (handle.classList.contains('h-resize')) {
|
|
957
|
+
// Horizontal resize: change sidebar width
|
|
958
|
+
const delta = e.clientX - startPos;
|
|
959
|
+
const currentWidth = sidebar.offsetWidth;
|
|
960
|
+
const newWidth = Math.max(280, Math.min(600, currentWidth - delta));
|
|
961
|
+
sidebar.style.width = newWidth + 'px';
|
|
962
|
+
startPos = e.clientX;
|
|
963
|
+
} else if (handle.classList.contains('v-top')) {
|
|
964
|
+
const delta = e.clientY - startPos;
|
|
965
|
+
// Resize tab-panel: auto-fit content but max = locked height
|
|
966
|
+
const activePanel = sidebar.querySelector('.tab-panel.active');
|
|
967
|
+
if (activePanel && maxTabPanelHeight) {
|
|
968
|
+
const newHeight = Math.max(100, maxTabPanelHeight + delta);
|
|
969
|
+
activePanel.style.maxHeight = newHeight + 'px';
|
|
970
|
+
activePanel.style.minHeight = 'auto'; // Auto-fit to content, but respect max
|
|
971
|
+
startPos = e.clientY;
|
|
972
|
+
}
|
|
973
|
+
} else if (handle.classList.contains('v-bottom')) {
|
|
974
|
+
const delta = e.clientY - startPos;
|
|
975
|
+
// Resize conversation up/down
|
|
976
|
+
const currentHeight = convoWrapEl.offsetHeight;
|
|
977
|
+
const newHeight = Math.max(50, currentHeight + delta);
|
|
978
|
+
convoWrapEl.style.maxHeight = newHeight + 'px';
|
|
979
|
+
startPos = e.clientY;
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
document.addEventListener('mouseup', () => {
|
|
984
|
+
isResizing = false;
|
|
985
|
+
document.body.style.userSelect = '';
|
|
986
|
+
document.body.style.cursor = '';
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
// Auto-resize tab-panel when comments added (within max)
|
|
991
|
+
const observer = new MutationObserver(() => {
|
|
992
|
+
const activePanel = sidebar.querySelector('.tab-panel.active');
|
|
993
|
+
if (activePanel && maxTabPanelHeight) {
|
|
994
|
+
const scrollHeight = activePanel.scrollHeight;
|
|
995
|
+
const newHeight = Math.min(scrollHeight + 20, maxTabPanelHeight);
|
|
996
|
+
activePanel.style.maxHeight = newHeight + 'px';
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
observer.observe(sidebar, { childList: true, subtree: true });
|
|
1001
|
+
}
|
|
1002
|
+
setupResizeHandles();
|
|
1003
|
+
|
|
1004
|
+
// ── postMessage bridge ─────────────────────────────────────────────────────
|
|
1005
|
+
window.addEventListener('message', (e) => {
|
|
1006
|
+
const d = e.data;
|
|
1007
|
+
if (!d || !d[TAG]) return;
|
|
1008
|
+
if (d.type === 'sdk-ready') {
|
|
1009
|
+
sdkReady = true;
|
|
1010
|
+
frame.contentWindow.postMessage({ [TAG]: true, type: 'set-annotate', value: annotating }, '*');
|
|
1011
|
+
syncAnnotationsToArtifact();
|
|
1012
|
+
if (d.headings && d.headings.length > 0) {
|
|
1013
|
+
renderOutlineWithHeadings(d.headings);
|
|
1014
|
+
}
|
|
1015
|
+
if (savedScroll) frame.contentWindow.scrollTo(0, savedScroll);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (d.type === 'element-selected') {
|
|
1019
|
+
addPending({ kind: 'element', selector: d.selector, label: d.label, text: '', note: d.note || '' });
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (d.type === 'text-selected') {
|
|
1023
|
+
addPending({
|
|
1024
|
+
kind: 'text', selector: d.selector, label: d.label, text: d.text, note: d.note || '',
|
|
1025
|
+
textStart: d.textStart, textEnd: d.textEnd,
|
|
1026
|
+
textPrefix: d.textPrefix || '', textSuffix: d.textSuffix || '',
|
|
1027
|
+
});
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (d.type === 'text-edit') {
|
|
1031
|
+
// Direct inline edit of the selected run in the .md (no agent). The server
|
|
1032
|
+
// re-verifies the text before writing; on success the file watcher reloads
|
|
1033
|
+
// the iframe and re-renders automatically.
|
|
1034
|
+
(async () => {
|
|
1035
|
+
try {
|
|
1036
|
+
const res = await fetch('/edit-text', {
|
|
1037
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1038
|
+
body: JSON.stringify({ file: d.file || file, oldText: d.oldText, newText: d.newText, index: d.index }),
|
|
1039
|
+
});
|
|
1040
|
+
if (res.ok) { flashToast(d.newText === '' ? 'Deleted' : 'Saved to .md'); }
|
|
1041
|
+
else { flashToast('Could not apply edit — try the Markdown pane'); }
|
|
1042
|
+
} catch { flashToast('Edit failed — server not reachable'); }
|
|
1043
|
+
})();
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (d.type === 'annotation-selected') {
|
|
1047
|
+
switchTab('annotations');
|
|
1048
|
+
selectAnnotation(d.id);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (d.type === 'serialized') {
|
|
1052
|
+
const res = serializeResolvers[d.reqId];
|
|
1053
|
+
if (res) { res(d.html); delete serializeResolvers[d.reqId]; }
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
function requestSerialize() {
|
|
1059
|
+
return new Promise((resolve) => {
|
|
1060
|
+
const reqId = ++serializeCounter;
|
|
1061
|
+
serializeResolvers[reqId] = resolve;
|
|
1062
|
+
frame.contentWindow.postMessage({ [TAG]: true, type: 'serialize', reqId }, '*');
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// ── Annotate toggle ────────────────────────────────────────────────────────
|
|
1067
|
+
function setAnnotate(val) {
|
|
1068
|
+
annotating = val;
|
|
1069
|
+
btnAnnotate.classList.toggle('active', annotating);
|
|
1070
|
+
btnAnnotate.setAttribute('aria-pressed', String(annotating));
|
|
1071
|
+
annotateLabel.textContent = annotating ? 'Annotating' : 'Annotate';
|
|
1072
|
+
frame.contentWindow?.postMessage({ [TAG]: true, type: 'set-annotate', value: annotating }, '*');
|
|
1073
|
+
}
|
|
1074
|
+
btnAnnotate.addEventListener('click', () => setAnnotate(!annotating));
|
|
1075
|
+
|
|
1076
|
+
function jumpToElement(selector) {
|
|
1077
|
+
if (!selector || !sdkReady) return;
|
|
1078
|
+
frame.contentWindow?.postMessage({ [TAG]: true, type: 'jump-to-element', selector }, '*');
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// All annotations known to the client, for lookups by id.
|
|
1082
|
+
function visibleAnnotations() { return annotations.concat(pendingNew); }
|
|
1083
|
+
// Annotations that get highlighted in the artifact: open ones + pending, plus
|
|
1084
|
+
// the single resolved annotation currently focused from the History tab.
|
|
1085
|
+
function syncedAnnotations() {
|
|
1086
|
+
const open = annotations.filter(a => a.status !== 'resolved');
|
|
1087
|
+
const focused = focusedResolvedId
|
|
1088
|
+
? annotations.filter(a => a.id === focusedResolvedId && a.status === 'resolved')
|
|
1089
|
+
: [];
|
|
1090
|
+
return open.concat(focused, pendingNew);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function syncAnnotationsToArtifact() {
|
|
1094
|
+
if (!sdkReady) return;
|
|
1095
|
+
frame.contentWindow?.postMessage({
|
|
1096
|
+
[TAG]: true, type: 'set-annotations',
|
|
1097
|
+
annotations: syncedAnnotations(), selectedId: selectedAnnotationId,
|
|
1098
|
+
}, '*');
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function selectAnnotation(id, shouldFocusArtifact = false) {
|
|
1102
|
+
selectedAnnotationId = id;
|
|
1103
|
+
const annotation = visibleAnnotations().find(item => item.id === id);
|
|
1104
|
+
// Resolved cards are only clickable from History; focusing one flashes its
|
|
1105
|
+
// location by temporarily adding it back to the highlighted set.
|
|
1106
|
+
focusedResolvedId = (annotation && annotation.status === 'resolved') ? id : null;
|
|
1107
|
+
renderAnnotations();
|
|
1108
|
+
renderHistoryTab();
|
|
1109
|
+
const card = document.querySelector(`.ann-card[data-id='${CSS.escape(id)}']`);
|
|
1110
|
+
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
1111
|
+
if (shouldFocusArtifact && annotation) {
|
|
1112
|
+
syncAnnotationsToArtifact();
|
|
1113
|
+
frame.contentWindow?.postMessage({ [TAG]: true, type: 'focus-annotation', annotation }, '*');
|
|
1114
|
+
} else {
|
|
1115
|
+
syncAnnotationsToArtifact();
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// ── Reviewer identity ───────────────────────────────────────────────────────
|
|
1120
|
+
const AVATAR_COLORS = ['#2741F1', '#4E11B2', '#8C78F3', '#D88689', '#1c8a4e', '#2fbf71'];
|
|
1121
|
+
function initialsOf(name) {
|
|
1122
|
+
const parts = String(name || '').trim().split(/[\s@._-]+/).filter(Boolean);
|
|
1123
|
+
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
1124
|
+
return (parts[0] || 'YO').slice(0, 2).toUpperCase();
|
|
1125
|
+
}
|
|
1126
|
+
function colorOf(name) {
|
|
1127
|
+
let h = 0;
|
|
1128
|
+
for (const c of String(name || '')) h = (h * 31 + c.charCodeAt(0)) >>> 0;
|
|
1129
|
+
return AVATAR_COLORS[h % AVATAR_COLORS.length];
|
|
1130
|
+
}
|
|
1131
|
+
async function loadWhoami() {
|
|
1132
|
+
if (!file) return;
|
|
1133
|
+
try {
|
|
1134
|
+
const res = await fetch('/whoami?file=' + encodeURIComponent(file));
|
|
1135
|
+
if (!res.ok) return;
|
|
1136
|
+
const d = await res.json();
|
|
1137
|
+
if (d && d.name) {
|
|
1138
|
+
me = { name: d.name, detail: d.detail || '', initials: initialsOf(d.name), color: colorOf(d.name) };
|
|
1139
|
+
renderAnnotations();
|
|
1140
|
+
}
|
|
1141
|
+
} catch {}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// ── Load / save annotations ────────────────────────────────────────────────
|
|
1145
|
+
async function loadAnnotations() {
|
|
1146
|
+
if (!file) return;
|
|
1147
|
+
try {
|
|
1148
|
+
const res = await fetch('/annotations?file=' + encodeURIComponent(file));
|
|
1149
|
+
if (!res.ok) return;
|
|
1150
|
+
const data = await res.json();
|
|
1151
|
+
annotations = data.annotations || [];
|
|
1152
|
+
lastRounds = data.rounds || [];
|
|
1153
|
+
renderAnnotations();
|
|
1154
|
+
renderHistoryTab();
|
|
1155
|
+
} catch {}
|
|
1156
|
+
}
|
|
1157
|
+
async function saveAnnotations() {
|
|
1158
|
+
if (!file) return;
|
|
1159
|
+
try {
|
|
1160
|
+
await fetch('/annotations', {
|
|
1161
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1162
|
+
body: JSON.stringify({ file, annotations }),
|
|
1163
|
+
});
|
|
1164
|
+
} catch {}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// ── New pending comment (note captured by the inline composer in the artifact) ─
|
|
1168
|
+
function addPending(item) {
|
|
1169
|
+
item.id = 'pending_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
|
|
1170
|
+
pendingNew.push(item);
|
|
1171
|
+
selectedAnnotationId = item.id;
|
|
1172
|
+
switchTab('annotations');
|
|
1173
|
+
renderAnnotations();
|
|
1174
|
+
flashToast('Comment added — Send feedback to notify the agent');
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// ── Render annotations list ────────────────────────────────────────────────
|
|
1178
|
+
function renderAnnotations() {
|
|
1179
|
+
annList.innerHTML = '';
|
|
1180
|
+
|
|
1181
|
+
// Resolved comments are archived to the History tab; only open comments and
|
|
1182
|
+
// not-yet-sent pending ones belong in the Annotations list.
|
|
1183
|
+
const open = annotations.filter(a => a.status !== 'resolved');
|
|
1184
|
+
const total = open.length + pendingNew.length;
|
|
1185
|
+
annBadge.textContent = total;
|
|
1186
|
+
annBadge.style.display = total > 0 ? '' : 'none';
|
|
1187
|
+
|
|
1188
|
+
if (total === 0) {
|
|
1189
|
+
annList.appendChild(buildEmptyState(
|
|
1190
|
+
'No open comments',
|
|
1191
|
+
'Select any text in the document, then choose <b>Comment</b> from the popover or right-click menu. Resolved comments move to <b>History</b>.'
|
|
1192
|
+
));
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Banner to retry every comment the agent couldn't apply.
|
|
1196
|
+
const failed = open.filter(a => a.applyFailed);
|
|
1197
|
+
if (failed.length) annList.appendChild(buildRetryBanner(failed));
|
|
1198
|
+
|
|
1199
|
+
pendingNew.forEach(item => annList.appendChild(buildAnnotationCard(item, true)));
|
|
1200
|
+
open.forEach(ann => annList.appendChild(buildAnnotationCard(ann, false)));
|
|
1201
|
+
|
|
1202
|
+
syncAnnotationsToArtifact();
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function buildRetryBanner(failed) {
|
|
1206
|
+
const bar = document.createElement('div');
|
|
1207
|
+
bar.className = 'retry-banner';
|
|
1208
|
+
const txt = document.createElement('div');
|
|
1209
|
+
txt.className = 'rb-text';
|
|
1210
|
+
txt.textContent = `${failed.length} comment${failed.length !== 1 ? 's' : ''} couldn't be applied`;
|
|
1211
|
+
const btn = document.createElement('button');
|
|
1212
|
+
btn.className = 'rb-btn';
|
|
1213
|
+
btn.innerHTML = ICON.retry + 'Retry all';
|
|
1214
|
+
btn.disabled = agentWorking;
|
|
1215
|
+
btn.addEventListener('click', () => retryAnnotations(failed));
|
|
1216
|
+
bar.append(txt, btn);
|
|
1217
|
+
return bar;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// The reviewer's instruction lives as the last human message in the thread.
|
|
1221
|
+
function lastHumanNote(ann) {
|
|
1222
|
+
const humans = (ann.thread || []).filter(m => m.role === 'human');
|
|
1223
|
+
return humans.length ? humans[humans.length - 1].message : (ann.note || ann.label || '');
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// Re-send failed comments to the agent for another apply attempt.
|
|
1227
|
+
async function retryAnnotations(anns) {
|
|
1228
|
+
const list = (anns || []).filter(a => a && a.applyFailed);
|
|
1229
|
+
if (!list.length || agentWorking) return;
|
|
1230
|
+
const items = list.map(a => ({
|
|
1231
|
+
id: a.id, kind: a.kind, selector: a.selector || null, label: a.label || null, text: a.text || null,
|
|
1232
|
+
textStart: a.textStart ?? null, textEnd: a.textEnd ?? null,
|
|
1233
|
+
textPrefix: a.textPrefix || null, textSuffix: a.textSuffix || null,
|
|
1234
|
+
note: lastHumanNote(a), retry: true, images: [],
|
|
1235
|
+
}));
|
|
1236
|
+
// Optimistically clear the failed flag so the UI reflects the in-flight retry.
|
|
1237
|
+
list.forEach(a => { a.applyFailed = false; });
|
|
1238
|
+
renderAnnotations();
|
|
1239
|
+
try {
|
|
1240
|
+
const res = await fetch('/feedback', {
|
|
1241
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1242
|
+
body: JSON.stringify({ file, items, message: '', images: [] }),
|
|
1243
|
+
});
|
|
1244
|
+
if (res.ok) { flashToast(`Retrying ${items.length} comment${items.length !== 1 ? 's' : ''}…`); setAgentWorking(true); }
|
|
1245
|
+
else list.forEach(a => { a.applyFailed = true; });
|
|
1246
|
+
} catch { list.forEach(a => { a.applyFailed = true; }); renderAnnotations(); }
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function buildEmptyState(title, subHtml) {
|
|
1250
|
+
const wrap = document.createElement('div');
|
|
1251
|
+
wrap.className = 'empty-state';
|
|
1252
|
+
wrap.innerHTML = `<div class="es-icon">${ICON.msg}</div><div class="es-title">${title}</div><div class="es-sub">${subHtml}</div>`;
|
|
1253
|
+
return wrap;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function buildAnnotationCard(ann, isPending) {
|
|
1257
|
+
const card = document.createElement('div');
|
|
1258
|
+
const failed = !isPending && ann.applyFailed && ann.status !== 'resolved';
|
|
1259
|
+
card.className = 'ann-card' + (isPending ? ' pending' : '') +
|
|
1260
|
+
(ann.status === 'resolved' ? ' resolved' : '') +
|
|
1261
|
+
(failed ? ' failed' : '') +
|
|
1262
|
+
(ann.id === selectedAnnotationId ? ' selected' : '');
|
|
1263
|
+
card.dataset.id = ann.id;
|
|
1264
|
+
|
|
1265
|
+
// Header
|
|
1266
|
+
const authorName = isPending ? me.name : (ann.author || me.name);
|
|
1267
|
+
const authorDetail = isPending ? me.detail : (ann.authorDetail || me.detail);
|
|
1268
|
+
const head = document.createElement('div');
|
|
1269
|
+
head.className = 'ann-head';
|
|
1270
|
+
const avatar = document.createElement('span');
|
|
1271
|
+
avatar.className = 'ann-avatar';
|
|
1272
|
+
avatar.style.background = colorOf(authorName);
|
|
1273
|
+
avatar.textContent = initialsOf(authorName);
|
|
1274
|
+
if (authorDetail) avatar.title = authorDetail;
|
|
1275
|
+
const who = document.createElement('div');
|
|
1276
|
+
who.className = 'ann-who';
|
|
1277
|
+
const authorEl = document.createElement('div');
|
|
1278
|
+
authorEl.className = 'ann-author';
|
|
1279
|
+
authorEl.textContent = authorName;
|
|
1280
|
+
if (authorDetail) authorEl.title = authorDetail;
|
|
1281
|
+
const timeEl = document.createElement('div');
|
|
1282
|
+
timeEl.className = 'ann-time';
|
|
1283
|
+
timeEl.textContent = isPending
|
|
1284
|
+
? 'pending send'
|
|
1285
|
+
: (ann.status === 'resolved' && ann.resolvedAt ? 'Resolved ' + timeAgo(ann.resolvedAt) : timeAgo(ann.createdAt));
|
|
1286
|
+
who.append(authorEl, timeEl);
|
|
1287
|
+
head.append(avatar, who);
|
|
1288
|
+
|
|
1289
|
+
if (!isPending && ann.status === 'resolved') {
|
|
1290
|
+
const rb = document.createElement('span');
|
|
1291
|
+
rb.className = 'ann-badge-resolved';
|
|
1292
|
+
rb.innerHTML = ICON.check + 'Resolved';
|
|
1293
|
+
head.appendChild(rb);
|
|
1294
|
+
} else if (failed) {
|
|
1295
|
+
const fb = document.createElement('span');
|
|
1296
|
+
fb.className = 'ann-badge-failed';
|
|
1297
|
+
fb.innerHTML = ICON.retry + 'Not applied';
|
|
1298
|
+
head.appendChild(fb);
|
|
1299
|
+
} else {
|
|
1300
|
+
const kt = document.createElement('span');
|
|
1301
|
+
kt.className = 'ann-kind-tag';
|
|
1302
|
+
kt.textContent = ann.kind || 'note';
|
|
1303
|
+
head.appendChild(kt);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const rm = document.createElement('button');
|
|
1307
|
+
rm.className = 'ann-remove'; rm.textContent = '×'; rm.title = 'Remove';
|
|
1308
|
+
rm.addEventListener('click', (e) => { e.stopPropagation(); isPending ? removePending(ann.id) : removeAnnotation(ann.id); });
|
|
1309
|
+
head.appendChild(rm);
|
|
1310
|
+
card.appendChild(head);
|
|
1311
|
+
|
|
1312
|
+
// Quote
|
|
1313
|
+
const quoteText = ann.text || ann.label || '';
|
|
1314
|
+
if (quoteText) {
|
|
1315
|
+
const q = document.createElement('div');
|
|
1316
|
+
q.className = 'ann-quote';
|
|
1317
|
+
q.textContent = '"' + quoteText + '"';
|
|
1318
|
+
card.appendChild(q);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Body / thread
|
|
1322
|
+
const thread = document.createElement('div');
|
|
1323
|
+
thread.className = 'ann-thread';
|
|
1324
|
+
if (isPending) {
|
|
1325
|
+
const body = document.createElement('div');
|
|
1326
|
+
body.className = 'tmsg';
|
|
1327
|
+
body.textContent = ann.note || '';
|
|
1328
|
+
thread.appendChild(body);
|
|
1329
|
+
// Optional image attachments that ride along when Send feedback fires.
|
|
1330
|
+
const previews = document.createElement('div');
|
|
1331
|
+
previews.className = 'attach-previews mini';
|
|
1332
|
+
previews.style.marginTop = '8px';
|
|
1333
|
+
const store = (noteImgStore[ann.id] = noteImgStore[ann.id] || []);
|
|
1334
|
+
const attach = createImageAttach(previews, store);
|
|
1335
|
+
const imgBtn = document.createElement('button');
|
|
1336
|
+
imgBtn.className = 'mini-attach'; imgBtn.style.marginTop = '8px';
|
|
1337
|
+
imgBtn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1.6"/><path d="M21 15l-5-5L5 21"/></svg>Image';
|
|
1338
|
+
const imgInput = document.createElement('input');
|
|
1339
|
+
imgInput.type = 'file'; imgInput.accept = 'image/*'; imgInput.multiple = true; imgInput.hidden = true;
|
|
1340
|
+
imgBtn.addEventListener('click', (e) => { e.stopPropagation(); imgInput.click(); });
|
|
1341
|
+
imgInput.addEventListener('change', () => { for (const f of imgInput.files) attach.add(f); imgInput.value = ''; });
|
|
1342
|
+
thread.append(previews, imgBtn, imgInput);
|
|
1343
|
+
} else if (ann.thread && ann.thread.length) {
|
|
1344
|
+
ann.thread.forEach(msg => {
|
|
1345
|
+
const m = document.createElement('div');
|
|
1346
|
+
m.className = 'tmsg ' + (msg.role === 'agent' ? 'agent' : 'human');
|
|
1347
|
+
if (msg.role === 'agent') {
|
|
1348
|
+
const who2 = document.createElement('div'); who2.className = 'tmsg-who'; who2.textContent = 'Agent';
|
|
1349
|
+
const b = document.createElement('div'); b.textContent = msg.message;
|
|
1350
|
+
m.append(who2, b, makeCopyBtn(msg.message, 'tmsg-copy'));
|
|
1351
|
+
} else {
|
|
1352
|
+
m.textContent = msg.message;
|
|
1353
|
+
}
|
|
1354
|
+
thread.appendChild(m);
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
card.appendChild(thread);
|
|
1358
|
+
|
|
1359
|
+
// Actions (saved annotations only)
|
|
1360
|
+
if (!isPending) {
|
|
1361
|
+
const actions = document.createElement('div');
|
|
1362
|
+
actions.className = 'ann-actions';
|
|
1363
|
+
const resolve = document.createElement('button');
|
|
1364
|
+
resolve.className = 'ann-action';
|
|
1365
|
+
resolve.innerHTML = ICON.resolve + (ann.status === 'resolved' ? 'Reopen' : 'Resolve');
|
|
1366
|
+
resolve.addEventListener('click', (e) => { e.stopPropagation(); toggleResolve(ann.id); });
|
|
1367
|
+
const reply = document.createElement('button');
|
|
1368
|
+
reply.className = 'ann-action';
|
|
1369
|
+
reply.innerHTML = ICON.reply + 'Reply';
|
|
1370
|
+
reply.addEventListener('click', (e) => { e.stopPropagation(); openReplyId = (openReplyId === ann.id ? null : ann.id); renderAnnotations(); });
|
|
1371
|
+
actions.append(resolve, reply);
|
|
1372
|
+
if (failed) {
|
|
1373
|
+
const retry = document.createElement('button');
|
|
1374
|
+
retry.className = 'ann-action retry';
|
|
1375
|
+
retry.innerHTML = ICON.retry + 'Retry';
|
|
1376
|
+
retry.disabled = agentWorking;
|
|
1377
|
+
retry.addEventListener('click', (e) => { e.stopPropagation(); retryAnnotations([ann]); });
|
|
1378
|
+
actions.append(retry);
|
|
1379
|
+
}
|
|
1380
|
+
card.appendChild(actions);
|
|
1381
|
+
|
|
1382
|
+
if (openReplyId === ann.id) card.appendChild(buildReplyEditor(ann));
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Jump to element in the artifact when the card is clicked
|
|
1386
|
+
if (ann.selector) {
|
|
1387
|
+
card.addEventListener('click', () => selectAnnotation(ann.id, true));
|
|
1388
|
+
}
|
|
1389
|
+
return card;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function buildReplyEditor(ann) {
|
|
1393
|
+
const wrap = document.createElement('div');
|
|
1394
|
+
wrap.className = 'ann-editor';
|
|
1395
|
+
const previews = document.createElement('div');
|
|
1396
|
+
previews.className = 'attach-previews mini';
|
|
1397
|
+
const store = (replyImgStore[ann.id] = replyImgStore[ann.id] || []);
|
|
1398
|
+
const attach = createImageAttach(previews, store);
|
|
1399
|
+
const ta = document.createElement('textarea');
|
|
1400
|
+
ta.placeholder = 'Reply to the agent…';
|
|
1401
|
+
ta.addEventListener('paste', (e) => attach.handlePaste(e));
|
|
1402
|
+
ta.addEventListener('click', (e) => e.stopPropagation());
|
|
1403
|
+
ta.addEventListener('keydown', (e) => {
|
|
1404
|
+
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); sendAnnotationReply(ann, ta); }
|
|
1405
|
+
});
|
|
1406
|
+
const row = document.createElement('div');
|
|
1407
|
+
row.className = 'ann-editor-row';
|
|
1408
|
+
const imgBtn = document.createElement('button');
|
|
1409
|
+
imgBtn.className = 'mini-attach';
|
|
1410
|
+
imgBtn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1.6"/><path d="M21 15l-5-5L5 21"/></svg>Image';
|
|
1411
|
+
const imgInput = document.createElement('input');
|
|
1412
|
+
imgInput.type = 'file'; imgInput.accept = 'image/*'; imgInput.multiple = true; imgInput.hidden = true;
|
|
1413
|
+
imgBtn.addEventListener('click', (e) => { e.stopPropagation(); imgInput.click(); });
|
|
1414
|
+
imgInput.addEventListener('change', () => { for (const f of imgInput.files) attach.add(f); imgInput.value = ''; });
|
|
1415
|
+
const send = document.createElement('button');
|
|
1416
|
+
send.className = 'cc-submit'; send.textContent = 'Send reply';
|
|
1417
|
+
send.addEventListener('click', (e) => { e.stopPropagation(); sendAnnotationReply(ann, ta); });
|
|
1418
|
+
row.append(imgBtn, imgInput, send);
|
|
1419
|
+
wrap.append(previews, ta, row);
|
|
1420
|
+
wrap.addEventListener('click', (e) => e.stopPropagation());
|
|
1421
|
+
setTimeout(() => ta.focus(), 20);
|
|
1422
|
+
return wrap;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Send a reply to a specific saved annotation → wakes the agent
|
|
1426
|
+
async function sendAnnotationReply(ann, textarea) {
|
|
1427
|
+
const note = textarea.value.trim();
|
|
1428
|
+
const staged = replyImgStore[ann.id] || [];
|
|
1429
|
+
if (!note && !staged.length) return;
|
|
1430
|
+
textarea.disabled = true;
|
|
1431
|
+
try {
|
|
1432
|
+
const images = await uploadImages(staged);
|
|
1433
|
+
ann.thread = ann.thread || [];
|
|
1434
|
+
const shown = note || `(${images.length} image${images.length !== 1 ? 's' : ''} attached)`;
|
|
1435
|
+
ann.thread.push({ role: 'human', message: shown, timestamp: new Date().toISOString() });
|
|
1436
|
+
delete replyImgStore[ann.id];
|
|
1437
|
+
openReplyId = null;
|
|
1438
|
+
renderAnnotations();
|
|
1439
|
+
await fetch('/feedback', {
|
|
1440
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1441
|
+
body: JSON.stringify({
|
|
1442
|
+
file,
|
|
1443
|
+
items: [{ id: ann.id, kind: ann.kind, selector: ann.selector, label: ann.label, text: ann.text, note }],
|
|
1444
|
+
message: '', images,
|
|
1445
|
+
}),
|
|
1446
|
+
});
|
|
1447
|
+
setAgentWorking(true);
|
|
1448
|
+
} finally { textarea.disabled = false; }
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function toggleResolve(id) {
|
|
1452
|
+
const ann = annotations.find(a => a.id === id);
|
|
1453
|
+
if (!ann) return;
|
|
1454
|
+
ann.status = ann.status === 'resolved' ? 'open' : 'resolved';
|
|
1455
|
+
if (ann.status === 'resolved') ann.resolvedAt = new Date().toISOString();
|
|
1456
|
+
else delete ann.resolvedAt;
|
|
1457
|
+
// A resolved comment leaves the artifact highlights and the Annotations list;
|
|
1458
|
+
// reopening it (or resolving) clears any lingering temporary focus.
|
|
1459
|
+
if (focusedResolvedId === id) focusedResolvedId = null;
|
|
1460
|
+
if (ann.status === 'open' && selectedAnnotationId === id) switchTab('annotations');
|
|
1461
|
+
renderAnnotations();
|
|
1462
|
+
renderHistoryTab();
|
|
1463
|
+
saveAnnotations();
|
|
1464
|
+
flashToast(ann.status === 'resolved' ? 'Comment resolved — moved to History' : 'Comment reopened');
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// ── Render history ─────────────────────────────────────────────────────────
|
|
1468
|
+
const HIST_DOT = { open:'#8C78F3', comment:'#2741F1', agent:'#4E11B2', resolve:'#2fbf71', download:'#D88689' };
|
|
1469
|
+
|
|
1470
|
+
// The History tab holds two things: resolved comments (as cards, clickable to
|
|
1471
|
+
// flash their location) and the feedback-round timeline.
|
|
1472
|
+
function renderHistoryTab() {
|
|
1473
|
+
const resolved = annotations.filter(a => a.status === 'resolved');
|
|
1474
|
+
const rounds = lastRounds || [];
|
|
1475
|
+
|
|
1476
|
+
const total = resolved.length + rounds.length;
|
|
1477
|
+
histBadge.textContent = total;
|
|
1478
|
+
histBadge.style.display = total > 0 ? '' : 'none';
|
|
1479
|
+
|
|
1480
|
+
// Resolved comments section
|
|
1481
|
+
histResolved.innerHTML = '';
|
|
1482
|
+
if (resolved.length) {
|
|
1483
|
+
const label = document.createElement('div');
|
|
1484
|
+
label.className = 'hist-section-label';
|
|
1485
|
+
label.textContent = `Resolved · ${resolved.length}`;
|
|
1486
|
+
histResolved.appendChild(label);
|
|
1487
|
+
resolved.forEach(ann => histResolved.appendChild(buildAnnotationCard(ann, false)));
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
renderRounds(resolved.length, rounds);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function renderRounds(resolvedCount, rounds) {
|
|
1494
|
+
histList.innerHTML = '';
|
|
1495
|
+
|
|
1496
|
+
if (!rounds.length) {
|
|
1497
|
+
if (!resolvedCount) {
|
|
1498
|
+
histList.appendChild(buildEmptyState('No history yet', 'Resolved comments, edits, and downloads will show up here as you work.'));
|
|
1499
|
+
}
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
rounds.slice().reverse().forEach(round => {
|
|
1503
|
+
const row = document.createElement('div');
|
|
1504
|
+
row.className = 'hist-row';
|
|
1505
|
+
const rail = document.createElement('div');
|
|
1506
|
+
rail.className = 'hist-rail';
|
|
1507
|
+
const dot = document.createElement('span');
|
|
1508
|
+
dot.className = 'hist-dot';
|
|
1509
|
+
const color = HIST_DOT[round.kind] || '#8C78F3';
|
|
1510
|
+
dot.style.background = color;
|
|
1511
|
+
dot.style.boxShadow = '0 0 0 3px ' + color + '22';
|
|
1512
|
+
const line = document.createElement('span'); line.className = 'hist-line';
|
|
1513
|
+
rail.append(dot, line);
|
|
1514
|
+
const body = document.createElement('div');
|
|
1515
|
+
const cnt = round.annotationCount != null ? `${round.annotationCount} annotation${round.annotationCount !== 1 ? 's' : ''}` : '';
|
|
1516
|
+
const text = round.agentSummary || round.text || [round.id, cnt].filter(Boolean).join(' · ') || 'Round';
|
|
1517
|
+
body.innerHTML = `<div class="hist-text"></div><div class="hist-time">${round.timestamp ? new Date(round.timestamp).toLocaleString() : ''}</div>`;
|
|
1518
|
+
body.querySelector('.hist-text').textContent = text;
|
|
1519
|
+
row.append(rail, body);
|
|
1520
|
+
histList.appendChild(row);
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// ── Pending items ──────────────────────────────────────────────────────────
|
|
1525
|
+
function removePending(id) {
|
|
1526
|
+
const idx = pendingNew.findIndex(c => c.id === id);
|
|
1527
|
+
if (idx === -1) return;
|
|
1528
|
+
const [item] = pendingNew.splice(idx, 1);
|
|
1529
|
+
if (selectedAnnotationId === id) selectedAnnotationId = null;
|
|
1530
|
+
undoStack.push({ type: 'pending', item, index: idx });
|
|
1531
|
+
if (undoStack.length > 10) undoStack.shift();
|
|
1532
|
+
renderAnnotations();
|
|
1533
|
+
showUndoToast(item.label || item.kind);
|
|
1534
|
+
}
|
|
1535
|
+
function removeAnnotation(id) {
|
|
1536
|
+
const idx = annotations.findIndex(a => a.id === id);
|
|
1537
|
+
if (idx === -1) return;
|
|
1538
|
+
const [ann] = annotations.splice(idx, 1);
|
|
1539
|
+
if (selectedAnnotationId === id) selectedAnnotationId = null;
|
|
1540
|
+
if (focusedResolvedId === id) focusedResolvedId = null;
|
|
1541
|
+
undoStack.push({ type: 'saved', item: ann, index: idx });
|
|
1542
|
+
if (undoStack.length > 10) undoStack.shift();
|
|
1543
|
+
renderAnnotations();
|
|
1544
|
+
renderHistoryTab();
|
|
1545
|
+
saveAnnotations();
|
|
1546
|
+
showUndoToast(ann.label || ann.kind);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// ── Undo ───────────────────────────────────────────────────────────────────
|
|
1550
|
+
function showUndoToast(label) {
|
|
1551
|
+
undoMsg.textContent = `Removed: ${label}`;
|
|
1552
|
+
undoToast.classList.add('visible');
|
|
1553
|
+
clearTimeout(undoTimer);
|
|
1554
|
+
undoTimer = setTimeout(() => undoToast.classList.remove('visible'), 4000);
|
|
1555
|
+
}
|
|
1556
|
+
function hideUndoToast() { clearTimeout(undoTimer); undoToast.classList.remove('visible'); }
|
|
1557
|
+
function undoRemove() {
|
|
1558
|
+
if (!undoStack.length) return;
|
|
1559
|
+
const { type, item, index } = undoStack.pop();
|
|
1560
|
+
if (type === 'pending') pendingNew.splice(index, 0, item);
|
|
1561
|
+
else { annotations.splice(index, 0, item); saveAnnotations(); }
|
|
1562
|
+
selectedAnnotationId = item.id;
|
|
1563
|
+
renderAnnotations();
|
|
1564
|
+
renderHistoryTab();
|
|
1565
|
+
hideUndoToast();
|
|
1566
|
+
}
|
|
1567
|
+
btnUndo.addEventListener('click', undoRemove);
|
|
1568
|
+
|
|
1569
|
+
// ── Image attachments (reusable) ────────────────────────────────────────────
|
|
1570
|
+
function fileToDataUrl(blob) {
|
|
1571
|
+
return new Promise((resolve, reject) => {
|
|
1572
|
+
const r = new FileReader();
|
|
1573
|
+
r.onload = () => resolve(r.result);
|
|
1574
|
+
r.onerror = reject;
|
|
1575
|
+
r.readAsDataURL(blob);
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
async function uploadImages(imgs) {
|
|
1579
|
+
const out = [];
|
|
1580
|
+
for (const img of imgs) {
|
|
1581
|
+
try {
|
|
1582
|
+
const res = await fetch('/upload', {
|
|
1583
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1584
|
+
body: JSON.stringify({ file, name: img.name, dataUrl: img.dataUrl }),
|
|
1585
|
+
});
|
|
1586
|
+
if (res.ok) { const d = await res.json(); out.push({ name: d.name, path: d.path }); }
|
|
1587
|
+
} catch {}
|
|
1588
|
+
}
|
|
1589
|
+
return out;
|
|
1590
|
+
}
|
|
1591
|
+
function createImageAttach(previewEl, imgs) {
|
|
1592
|
+
function render() {
|
|
1593
|
+
previewEl.innerHTML = '';
|
|
1594
|
+
imgs.forEach(img => {
|
|
1595
|
+
const thumb = document.createElement('div');
|
|
1596
|
+
thumb.className = 'attach-thumb'; thumb.title = img.name;
|
|
1597
|
+
const im = document.createElement('img'); im.src = img.dataUrl; im.alt = img.name;
|
|
1598
|
+
const rm = document.createElement('button');
|
|
1599
|
+
rm.className = 'attach-remove'; rm.textContent = '×'; rm.title = 'Remove image';
|
|
1600
|
+
rm.addEventListener('click', (e) => { e.stopPropagation(); const i = imgs.indexOf(img); if (i >= 0) imgs.splice(i, 1); render(); });
|
|
1601
|
+
thumb.append(im, rm); previewEl.appendChild(thumb);
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
async function add(blob) {
|
|
1605
|
+
if (!blob || !blob.type || !blob.type.startsWith('image/')) return;
|
|
1606
|
+
const dataUrl = await fileToDataUrl(blob);
|
|
1607
|
+
imgs.push({ id: 'img_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6), name: blob.name || ('pasted.' + (blob.type.split('/')[1] || 'png')), dataUrl });
|
|
1608
|
+
render();
|
|
1609
|
+
}
|
|
1610
|
+
function handlePaste(e) {
|
|
1611
|
+
const items = e.clipboardData?.items || [];
|
|
1612
|
+
let handled = false;
|
|
1613
|
+
for (const it of items) {
|
|
1614
|
+
if (it.type && it.type.startsWith('image/')) { const blob = it.getAsFile(); if (blob) { add(blob); handled = true; } }
|
|
1615
|
+
}
|
|
1616
|
+
if (handled) e.preventDefault();
|
|
1617
|
+
}
|
|
1618
|
+
render();
|
|
1619
|
+
return { add, handlePaste, render, upload: () => uploadImages(imgs), clear() { imgs.length = 0; render(); }, count: () => imgs.length };
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
const composerAttach = createImageAttach(attachPreviews, composerImages);
|
|
1623
|
+
btnAttach.addEventListener('click', () => fileInput.click());
|
|
1624
|
+
fileInput.addEventListener('change', () => { for (const f of fileInput.files) composerAttach.add(f); fileInput.value = ''; });
|
|
1625
|
+
freeText.addEventListener('paste', (e) => composerAttach.handlePaste(e));
|
|
1626
|
+
|
|
1627
|
+
// ── Agent working state ─────────────────────────────────────────────────────
|
|
1628
|
+
let workingStartMs = 0, workingTimer = null;
|
|
1629
|
+
function fmtElapsed(ms) {
|
|
1630
|
+
const s = Math.max(0, Math.floor(ms / 1000));
|
|
1631
|
+
return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
|
|
1632
|
+
}
|
|
1633
|
+
function setAgentWorking(on) {
|
|
1634
|
+
agentWorking = on;
|
|
1635
|
+
btnCancel.style.display = on ? '' : 'none';
|
|
1636
|
+
btnCancel.disabled = false;
|
|
1637
|
+
btnSend.disabled = on;
|
|
1638
|
+
clearInterval(workingTimer); workingTimer = null;
|
|
1639
|
+
if (on) {
|
|
1640
|
+
// Keep counting from the first "working" tick of a round, not every re-entry.
|
|
1641
|
+
if (!workingStartMs) workingStartMs = Date.now();
|
|
1642
|
+
setStatus('working', 'Working… ' + fmtElapsed(Date.now() - workingStartMs));
|
|
1643
|
+
workingTimer = setInterval(() => {
|
|
1644
|
+
// Don't clobber a "Needs permission" message; resume once it's cleared.
|
|
1645
|
+
if (agentWorking && agentStatusEl.classList.contains('working')) {
|
|
1646
|
+
agentStatusText.textContent = 'Working… ' + fmtElapsed(Date.now() - workingStartMs);
|
|
1647
|
+
}
|
|
1648
|
+
}, 1000);
|
|
1649
|
+
} else {
|
|
1650
|
+
workingStartMs = 0;
|
|
1651
|
+
setStatus('idle', 'Idle');
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
function setStatus(state, text) {
|
|
1655
|
+
agentStatusEl.classList.remove('working', 'idle', 'needs-perm');
|
|
1656
|
+
agentStatusEl.classList.add(state);
|
|
1657
|
+
agentStatusText.textContent = text;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// ── Remote permission approval ──────────────────────────────────────────────
|
|
1661
|
+
remoteApprove.addEventListener('change', () => {
|
|
1662
|
+
fetch('/permission/mode', {
|
|
1663
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1664
|
+
body: JSON.stringify({ file, enabled: remoteApprove.checked }),
|
|
1665
|
+
}).catch(() => {});
|
|
1666
|
+
});
|
|
1667
|
+
function renderPermCard({ requestId, tool, summary }) {
|
|
1668
|
+
if (permRequests.querySelector(`[data-req="${requestId}"]`)) return;
|
|
1669
|
+
const card = document.createElement('div');
|
|
1670
|
+
card.className = 'perm-card'; card.dataset.req = requestId;
|
|
1671
|
+
const head = document.createElement('div'); head.className = 'perm-head'; head.textContent = '⚠ Permission needed';
|
|
1672
|
+
const toolEl = document.createElement('div'); toolEl.className = 'perm-tool'; toolEl.textContent = summary || tool;
|
|
1673
|
+
const actions = document.createElement('div'); actions.className = 'perm-actions';
|
|
1674
|
+
const allow = document.createElement('button'); allow.className = 'perm-allow'; allow.textContent = 'Allow';
|
|
1675
|
+
const always = document.createElement('button'); always.className = 'perm-always'; always.textContent = 'Allow always';
|
|
1676
|
+
always.title = `Allow "${tool}" for the rest of this session without asking`;
|
|
1677
|
+
const deny = document.createElement('button'); deny.className = 'perm-deny'; deny.textContent = 'Deny';
|
|
1678
|
+
allow.addEventListener('click', () => decidePermission(requestId, 'allow', false));
|
|
1679
|
+
always.addEventListener('click', () => decidePermission(requestId, 'allow', true));
|
|
1680
|
+
deny.addEventListener('click', () => decidePermission(requestId, 'deny', false));
|
|
1681
|
+
actions.append(allow, always, deny);
|
|
1682
|
+
card.append(head, toolEl, actions);
|
|
1683
|
+
permRequests.appendChild(card);
|
|
1684
|
+
setStatus('needs-perm', 'Needs your permission');
|
|
1685
|
+
}
|
|
1686
|
+
function removePermCard(requestId) {
|
|
1687
|
+
const card = permRequests.querySelector(`[data-req="${requestId}"]`);
|
|
1688
|
+
if (card) card.remove();
|
|
1689
|
+
if (!permRequests.children.length) setStatus(agentWorking ? 'working' : 'idle', agentWorking ? 'Working…' : 'Idle');
|
|
1690
|
+
}
|
|
1691
|
+
function decidePermission(requestId, decision, always) {
|
|
1692
|
+
const card = permRequests.querySelector(`[data-req="${requestId}"]`);
|
|
1693
|
+
if (card) card.querySelectorAll('button').forEach(b => b.disabled = true);
|
|
1694
|
+
fetch('/permission/decision', {
|
|
1695
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1696
|
+
body: JSON.stringify({ file, requestId, decision, always }),
|
|
1697
|
+
}).catch(() => {});
|
|
1698
|
+
removePermCard(requestId);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// ── Agent activity / conversation ──────────────────────────────────────────
|
|
1702
|
+
function showWorking() {
|
|
1703
|
+
let el = convoEl.querySelector('[data-working]');
|
|
1704
|
+
if (el) return el;
|
|
1705
|
+
el = document.createElement('div');
|
|
1706
|
+
el.className = 'msg agent'; el.setAttribute('data-working', '1');
|
|
1707
|
+
const lbl = document.createElement('div'); lbl.className = 'msg-label'; lbl.textContent = 'Agent';
|
|
1708
|
+
const steps = document.createElement('div'); steps.className = 'work-steps';
|
|
1709
|
+
const dots = document.createElement('div'); dots.className = 'thinking-dots'; dots.innerHTML = '<span></span><span></span><span></span>';
|
|
1710
|
+
el.append(lbl, steps, dots);
|
|
1711
|
+
convoEl.appendChild(el);
|
|
1712
|
+
convoWrap.scrollTop = convoWrap.scrollHeight;
|
|
1713
|
+
return el;
|
|
1714
|
+
}
|
|
1715
|
+
function addWorkStep(step, done) {
|
|
1716
|
+
if (!step) return;
|
|
1717
|
+
const el = showWorking();
|
|
1718
|
+
const steps = el.querySelector('.work-steps');
|
|
1719
|
+
const prev = steps.querySelector('.work-step:not(.done)');
|
|
1720
|
+
if (prev) prev.classList.add('done');
|
|
1721
|
+
const line = document.createElement('div');
|
|
1722
|
+
line.className = 'work-step' + (done ? ' done' : '');
|
|
1723
|
+
line.textContent = step;
|
|
1724
|
+
steps.appendChild(line);
|
|
1725
|
+
convoWrap.scrollTop = convoWrap.scrollHeight;
|
|
1726
|
+
}
|
|
1727
|
+
function hideWorking() { convoEl.querySelectorAll('[data-working]').forEach(el => el.remove()); }
|
|
1728
|
+
|
|
1729
|
+
async function cancelAgent() {
|
|
1730
|
+
if (!agentWorking) return;
|
|
1731
|
+
btnCancel.disabled = true;
|
|
1732
|
+
addWorkStep('Cancelling…', false);
|
|
1733
|
+
try {
|
|
1734
|
+
await fetch('/cancel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file }) });
|
|
1735
|
+
} catch {}
|
|
1736
|
+
}
|
|
1737
|
+
btnCancel.addEventListener('click', cancelAgent);
|
|
1738
|
+
|
|
1739
|
+
// ── Send feedback ──────────────────────────────────────────────────────────
|
|
1740
|
+
async function sendFeedback() {
|
|
1741
|
+
const message = freeText.value.trim();
|
|
1742
|
+
const composerImgCount = composerAttach.count();
|
|
1743
|
+
const noteImgCount = pendingNew.reduce((n, c) => n + (noteImgStore[c.id]?.length || 0), 0);
|
|
1744
|
+
if (!pendingNew.length && !message && !composerImgCount && !noteImgCount) return;
|
|
1745
|
+
|
|
1746
|
+
btnSend.disabled = true;
|
|
1747
|
+
try {
|
|
1748
|
+
const items = await Promise.all(pendingNew.map(async c => ({
|
|
1749
|
+
id: null, kind: c.kind, selector: c.selector || null, label: c.label || null, text: c.text || null,
|
|
1750
|
+
textStart: c.textStart ?? null, textEnd: c.textEnd ?? null,
|
|
1751
|
+
textPrefix: c.textPrefix || null, textSuffix: c.textSuffix || null,
|
|
1752
|
+
note: c.note || null,
|
|
1753
|
+
images: noteImgStore[c.id] ? await uploadImages(noteImgStore[c.id]) : [],
|
|
1754
|
+
})));
|
|
1755
|
+
const images = await composerAttach.upload();
|
|
1756
|
+
const totalImgs = images.length + items.reduce((n, it) => n + it.images.length, 0);
|
|
1757
|
+
const res = await fetch('/feedback', {
|
|
1758
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1759
|
+
body: JSON.stringify({ file, items, message, images, model: selectedModel || null, askMode: forceAskMode }),
|
|
1760
|
+
});
|
|
1761
|
+
if (res.ok) {
|
|
1762
|
+
pendingNew.forEach(c => { delete noteImgStore[c.id]; });
|
|
1763
|
+
pendingNew = [];
|
|
1764
|
+
composerAttach.clear();
|
|
1765
|
+
selectedAnnotationId = null;
|
|
1766
|
+
freeText.value = '';
|
|
1767
|
+
undoStack = [];
|
|
1768
|
+
hideUndoToast();
|
|
1769
|
+
await loadAnnotations();
|
|
1770
|
+
if (message || totalImgs) {
|
|
1771
|
+
const suffix = totalImgs ? ` 📎 ${totalImgs} image${totalImgs !== 1 ? 's' : ''}` : '';
|
|
1772
|
+
addConvoMsg('human', (message || '(images attached)') + suffix);
|
|
1773
|
+
}
|
|
1774
|
+
flashToast('Feedback sent to agent');
|
|
1775
|
+
setAgentWorking(true);
|
|
1776
|
+
}
|
|
1777
|
+
} finally {
|
|
1778
|
+
if (!agentWorking) btnSend.disabled = false;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
btnSend.addEventListener('click', sendFeedback);
|
|
1782
|
+
freeText.addEventListener('keydown', (e) => {
|
|
1783
|
+
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); sendFeedback(); }
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
// ── Copy helpers ────────────────────────────────────────────────────────────
|
|
1787
|
+
function fallbackCopy(text, done) {
|
|
1788
|
+
const ta = document.createElement('textarea');
|
|
1789
|
+
ta.value = text; ta.style.cssText = 'position:fixed;top:-9999px;opacity:0;';
|
|
1790
|
+
document.body.appendChild(ta); ta.select();
|
|
1791
|
+
try { document.execCommand('copy'); if (done) done(); } catch {}
|
|
1792
|
+
ta.remove();
|
|
1793
|
+
}
|
|
1794
|
+
function copyText(text, btn) {
|
|
1795
|
+
const flash = () => { if (!btn) return; const orig = btn.textContent; btn.textContent = 'Copied'; setTimeout(() => { btn.textContent = orig; }, 1200); };
|
|
1796
|
+
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(flash).catch(() => fallbackCopy(text, flash));
|
|
1797
|
+
else fallbackCopy(text, flash);
|
|
1798
|
+
}
|
|
1799
|
+
function makeCopyBtn(text, cls) {
|
|
1800
|
+
const b = document.createElement('button');
|
|
1801
|
+
b.className = cls; b.textContent = 'Copy'; b.title = 'Copy message';
|
|
1802
|
+
b.addEventListener('click', (e) => { e.stopPropagation(); copyText(text, b); });
|
|
1803
|
+
return b;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// ── Conversation (general agent messages) ──────────────────────────────────
|
|
1807
|
+
function addConvoMsg(role, text) {
|
|
1808
|
+
const el = document.createElement('div');
|
|
1809
|
+
el.className = 'msg ' + role;
|
|
1810
|
+
const label = document.createElement('div'); label.className = 'msg-label'; label.textContent = role === 'human' ? me.name : 'Agent';
|
|
1811
|
+
const body = document.createElement('div'); body.textContent = text;
|
|
1812
|
+
el.append(label, body);
|
|
1813
|
+
if (role === 'agent') el.append(makeCopyBtn(text, 'msg-copy'));
|
|
1814
|
+
convoEl.appendChild(el);
|
|
1815
|
+
convoWrap.scrollTop = convoWrap.scrollHeight;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// ── Download ───────────────────────────────────────────────────────────────
|
|
1819
|
+
async function doDownload() {
|
|
1820
|
+
if (!sdkReady) { flashToast('Preview is still loading — try again in a moment.'); return; }
|
|
1821
|
+
btnDownload.disabled = true;
|
|
1822
|
+
try {
|
|
1823
|
+
const html = await requestSerialize();
|
|
1824
|
+
const blob = new Blob([html], { type: 'text/html' });
|
|
1825
|
+
const a = document.createElement('a');
|
|
1826
|
+
a.href = URL.createObjectURL(blob);
|
|
1827
|
+
a.download = file.split('/').pop() || 'artifact.html';
|
|
1828
|
+
a.click();
|
|
1829
|
+
URL.revokeObjectURL(a.href);
|
|
1830
|
+
flashToast('Downloaded a clean copy');
|
|
1831
|
+
} finally { btnDownload.disabled = false; }
|
|
1832
|
+
}
|
|
1833
|
+
btnDownload.addEventListener('click', doDownload);
|
|
1834
|
+
|
|
1835
|
+
// ── Finalize ───────────────────────────────────────────────────────────────
|
|
1836
|
+
btnFinalize.addEventListener('click', async () => {
|
|
1837
|
+
if (!sdkReady) { flashToast('Preview is still loading — try again in a moment.'); return; }
|
|
1838
|
+
if (!confirm('Write the current rendered artifact to disk? This overwrites the file.')) return;
|
|
1839
|
+
btnFinalize.disabled = true;
|
|
1840
|
+
try {
|
|
1841
|
+
const html = await requestSerialize();
|
|
1842
|
+
const res = await fetch('/finalize', {
|
|
1843
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1844
|
+
body: JSON.stringify({ file, html }),
|
|
1845
|
+
});
|
|
1846
|
+
if (res.ok) { addConvoMsg('human', 'Finalized — result written to disk.'); flashToast('Finalized to disk'); }
|
|
1847
|
+
else alert('Finalize failed: ' + res.statusText);
|
|
1848
|
+
} finally { btnFinalize.disabled = false; }
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
// ── Done: finish the review and shut the server down ────────────────────────
|
|
1852
|
+
let sessionDone = false;
|
|
1853
|
+
btnDone.addEventListener('click', async () => {
|
|
1854
|
+
const warn = isMd && mdDirty
|
|
1855
|
+
? 'You have unsaved Markdown changes. Save them, finish the review, and shut down the server?'
|
|
1856
|
+
: 'Finish the review and shut down the annotron server?';
|
|
1857
|
+
if (!confirm(warn)) return;
|
|
1858
|
+
btnDone.disabled = true;
|
|
1859
|
+
try {
|
|
1860
|
+
if (isMd && mdDirty) await saveMd();
|
|
1861
|
+
// End the agent's poll loop cleanly, give it a moment to receive the signal
|
|
1862
|
+
// and exit, THEN stop the server (else /stop could kill the server before
|
|
1863
|
+
// the agent's long-poll response flushes and it never logs "Done").
|
|
1864
|
+
await fetch('/done', {
|
|
1865
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file }),
|
|
1866
|
+
}).catch(() => {});
|
|
1867
|
+
await new Promise(r => setTimeout(r, 600));
|
|
1868
|
+
await fetch('/stop', {
|
|
1869
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),
|
|
1870
|
+
}).catch(() => {});
|
|
1871
|
+
} finally {
|
|
1872
|
+
finishSession();
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
function finishSession() {
|
|
1876
|
+
sessionDone = true; // stop SSE from trying to reconnect
|
|
1877
|
+
doneOverlay.classList.add('show');
|
|
1878
|
+
livePill.classList.remove('live');
|
|
1879
|
+
livePill.textContent = 'Stopped';
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// ── SSE ────────────────────────────────────────────────────────────────────
|
|
1883
|
+
function connectSSE() {
|
|
1884
|
+
if (!file) return;
|
|
1885
|
+
const es = new EventSource('/events?file=' + encodeURIComponent(file));
|
|
1886
|
+
es.addEventListener('reload', () => { savedScroll = frame.contentWindow?.scrollY || 0; loadArtifact(); loadSource(); });
|
|
1887
|
+
es.addEventListener('agent-thinking', () => { setAgentWorking(true); showWorking(); });
|
|
1888
|
+
es.addEventListener('agent-progress', (e) => { try { const d = JSON.parse(e.data); addWorkStep(d.step, d.done); } catch {} });
|
|
1889
|
+
es.addEventListener('agent-progress-done', () => {
|
|
1890
|
+
const steps = convoEl.querySelector('[data-working] .work-steps');
|
|
1891
|
+
const last = steps && steps.querySelector('.work-step:not(.done)');
|
|
1892
|
+
if (last) last.classList.add('done');
|
|
1893
|
+
});
|
|
1894
|
+
es.addEventListener('agent-status', (e) => {
|
|
1895
|
+
try {
|
|
1896
|
+
const t = JSON.parse(e.data).type || '';
|
|
1897
|
+
if (t === 'permission_prompt') setStatus('needs-perm', 'Needs your permission');
|
|
1898
|
+
else if (t === 'idle' || t === 'idle_prompt') { if (!agentWorking) setStatus('idle', 'Idle'); }
|
|
1899
|
+
} catch {}
|
|
1900
|
+
});
|
|
1901
|
+
es.addEventListener('agent-metrics', (e) => {
|
|
1902
|
+
try { updateMetricsDisplay(JSON.parse(e.data)); } catch {}
|
|
1903
|
+
});
|
|
1904
|
+
es.addEventListener('permission-request', (e) => { try { renderPermCard(JSON.parse(e.data)); } catch {} });
|
|
1905
|
+
es.addEventListener('permission-resolved', (e) => { try { removePermCard(JSON.parse(e.data).requestId); } catch {} });
|
|
1906
|
+
es.addEventListener('permission-mode', (e) => { try { remoteApprove.checked = !!JSON.parse(e.data).enabled; } catch {} });
|
|
1907
|
+
es.addEventListener('agent-cancelled', () => {
|
|
1908
|
+
hideWorking(); setAgentWorking(false);
|
|
1909
|
+
addConvoMsg('agent', '⛔ Cancellation requested — the agent will stop at its next checkpoint.');
|
|
1910
|
+
const last = convoEl.lastElementChild; if (last) last.classList.add('cancelled');
|
|
1911
|
+
});
|
|
1912
|
+
es.addEventListener('agent-reply', async (e) => {
|
|
1913
|
+
hideWorking(); setAgentWorking(false);
|
|
1914
|
+
try {
|
|
1915
|
+
const d = JSON.parse(e.data);
|
|
1916
|
+
const msg = d.message || e.data;
|
|
1917
|
+
if (d.annotationId) {
|
|
1918
|
+
// The agent acted on this comment → server marked it resolved; reload so
|
|
1919
|
+
// it moves to the History tab, then reveal it wherever it now lives.
|
|
1920
|
+
await loadAnnotations();
|
|
1921
|
+
const card = document.querySelector(`.ann-card[data-id="${CSS.escape(d.annotationId)}"]`);
|
|
1922
|
+
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
1923
|
+
flashToast(d.failed ? "Couldn't apply — Retry available" : 'Agent replied — comment resolved to History');
|
|
1924
|
+
} else {
|
|
1925
|
+
addConvoMsg('agent', msg);
|
|
1926
|
+
flashToast('Agent replied');
|
|
1927
|
+
}
|
|
1928
|
+
} catch { addConvoMsg('agent', e.data); }
|
|
1929
|
+
});
|
|
1930
|
+
es.addEventListener('session-done', () => { es.close(); finishSession(); });
|
|
1931
|
+
es.onopen = () => { livePill.classList.add('live'); livePill.title = 'Live'; };
|
|
1932
|
+
es.onerror = () => {
|
|
1933
|
+
if (sessionDone) { es.close(); return; } // review finished — don't reconnect
|
|
1934
|
+
livePill.classList.remove('live'); livePill.title = 'Disconnected — reconnecting…';
|
|
1935
|
+
es.close(); setTimeout(() => { if (!sessionDone) connectSSE(); }, 3000);
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
connectSSE();
|
|
1939
|
+
|
|
1940
|
+
// ── Keyboard shortcuts ─────────────────────────────────────────────────────
|
|
1941
|
+
document.addEventListener('keydown', (e) => {
|
|
1942
|
+
const inInput = ['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName);
|
|
1943
|
+
const ctrl = e.ctrlKey || e.metaKey;
|
|
1944
|
+
const helpOpen = kbdOverlay.classList.contains('open');
|
|
1945
|
+
if (e.key === 'Escape') {
|
|
1946
|
+
if (helpOpen) { closeHelp(); return; }
|
|
1947
|
+
if (annotating) { setAnnotate(false); return; }
|
|
1948
|
+
}
|
|
1949
|
+
if (ctrl && (e.key === 's' || e.key === 'S')) { if (isMd) { e.preventDefault(); saveMd(); } return; }
|
|
1950
|
+
if (ctrl && e.key === 'z' && !e.shiftKey) { if (undoStack.length) { e.preventDefault(); undoRemove(); } return; }
|
|
1951
|
+
if (ctrl && e.key === 'Enter') { e.preventDefault(); sendFeedback(); return; }
|
|
1952
|
+
if (!inInput && !ctrl) {
|
|
1953
|
+
if (e.key === 'a' || e.key === 'A') { e.preventDefault(); setAnnotate(!annotating); return; }
|
|
1954
|
+
if (e.key === 'd' || e.key === 'D') { e.preventDefault(); doDownload(); return; }
|
|
1955
|
+
if (e.key === '?') { e.preventDefault(); helpOpen ? closeHelp() : openHelp(); return; }
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
// ── Markdown mode ────────────────────────────────────────────────────────────
|
|
1960
|
+
// When the artifact is a .md source, annotron renders it to HTML (with merslim
|
|
1961
|
+
// diagrams) for review, and exposes an editable Markdown pane + Save button so
|
|
1962
|
+
// the user can sync edits back to the .md (the source of truth).
|
|
1963
|
+
const isMd = /\.md$/i.test(file);
|
|
1964
|
+
const mdEditor = document.getElementById('md-editor');
|
|
1965
|
+
const mdTextarea = document.getElementById('md-textarea');
|
|
1966
|
+
const mdStatus = document.getElementById('mde-status');
|
|
1967
|
+
const btnMdSource = document.getElementById('btn-md-source');
|
|
1968
|
+
const btnSaveMd = document.getElementById('btn-save-md');
|
|
1969
|
+
let mdDirty = false, mdLoaded = false;
|
|
1970
|
+
|
|
1971
|
+
function setMdStatus(kind) {
|
|
1972
|
+
mdStatus.className = 'mde-status' + (kind ? ' ' + kind : '');
|
|
1973
|
+
mdStatus.textContent = kind === 'dirty' ? 'Unsaved changes' : (kind === 'saved' ? 'Saved' : '');
|
|
1974
|
+
}
|
|
1975
|
+
async function loadSource(force) {
|
|
1976
|
+
if (!isMd) return;
|
|
1977
|
+
if (mdDirty && !force) return; // never clobber unsaved edits
|
|
1978
|
+
try {
|
|
1979
|
+
const res = await fetch('/source?file=' + encodeURIComponent(file));
|
|
1980
|
+
if (!res.ok) return;
|
|
1981
|
+
const d = await res.json();
|
|
1982
|
+
mdTextarea.value = d.markdown || '';
|
|
1983
|
+
mdLoaded = true; mdDirty = false; setMdStatus('');
|
|
1984
|
+
} catch {}
|
|
1985
|
+
}
|
|
1986
|
+
function toggleMdEditor() {
|
|
1987
|
+
const show = mdEditor.hasAttribute('hidden');
|
|
1988
|
+
if (show) {
|
|
1989
|
+
mdEditor.removeAttribute('hidden');
|
|
1990
|
+
btnMdSource.classList.add('active');
|
|
1991
|
+
if (!mdLoaded) loadSource();
|
|
1992
|
+
setTimeout(() => mdTextarea.focus(), 30);
|
|
1993
|
+
} else {
|
|
1994
|
+
mdEditor.setAttribute('hidden', '');
|
|
1995
|
+
btnMdSource.classList.remove('active');
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
async function saveMd() {
|
|
1999
|
+
if (!isMd) return;
|
|
2000
|
+
btnSaveMd.disabled = true;
|
|
2001
|
+
try {
|
|
2002
|
+
const res = await fetch('/save-md', {
|
|
2003
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2004
|
+
body: JSON.stringify({ file, markdown: mdTextarea.value }),
|
|
2005
|
+
});
|
|
2006
|
+
if (res.ok) { mdDirty = false; setMdStatus('saved'); flashToast('Saved to .md — re-rendering preview'); }
|
|
2007
|
+
else { const e = await res.json().catch(() => ({})); flashToast('Save failed: ' + (e.error || res.statusText)); }
|
|
2008
|
+
} catch { flashToast('Save failed'); }
|
|
2009
|
+
finally { btnSaveMd.disabled = false; }
|
|
2010
|
+
}
|
|
2011
|
+
if (isMd) {
|
|
2012
|
+
btnFinalize.style.display = 'none'; // Finalize would overwrite .md with HTML
|
|
2013
|
+
btnMdSource.style.display = '';
|
|
2014
|
+
btnSaveMd.style.display = '';
|
|
2015
|
+
btnMdSource.addEventListener('click', toggleMdEditor);
|
|
2016
|
+
btnSaveMd.addEventListener('click', saveMd);
|
|
2017
|
+
mdTextarea.addEventListener('input', () => { mdDirty = true; setMdStatus('dirty'); });
|
|
2018
|
+
mdTextarea.addEventListener('keydown', (e) => {
|
|
2019
|
+
if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) { e.preventDefault(); saveMd(); }
|
|
2020
|
+
});
|
|
2021
|
+
loadSource(); // keep the textarea in sync even while hidden, so Save is safe
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// ── Init ───────────────────────────────────────────────────────────────────
|
|
2025
|
+
loadWhoami();
|
|
2026
|
+
loadAnnotations();
|
|
2027
|
+
</script>
|
|
2028
|
+
</body>
|
|
2029
|
+
</html>
|