create-walle 0.1.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.
Files changed (136) hide show
  1. package/bin/create-walle.js +134 -0
  2. package/package.json +18 -0
  3. package/template/.env.example +40 -0
  4. package/template/CLAUDE.md +12 -0
  5. package/template/LICENSE +21 -0
  6. package/template/README.md +167 -0
  7. package/template/bin/setup.js +100 -0
  8. package/template/claude-code-skill.md +60 -0
  9. package/template/claude-task-manager/api-prompts.js +1841 -0
  10. package/template/claude-task-manager/api-reviews.js +275 -0
  11. package/template/claude-task-manager/approval-agent.js +454 -0
  12. package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
  13. package/template/claude-task-manager/db.js +1721 -0
  14. package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
  15. package/template/claude-task-manager/git-utils.js +214 -0
  16. package/template/claude-task-manager/package-lock.json +1607 -0
  17. package/template/claude-task-manager/package.json +31 -0
  18. package/template/claude-task-manager/prompt-harvest.js +1148 -0
  19. package/template/claude-task-manager/public/css/prompts.css +880 -0
  20. package/template/claude-task-manager/public/css/reviews.css +430 -0
  21. package/template/claude-task-manager/public/css/walle.css +732 -0
  22. package/template/claude-task-manager/public/favicon.ico +0 -0
  23. package/template/claude-task-manager/public/icon.svg +37 -0
  24. package/template/claude-task-manager/public/index.html +8346 -0
  25. package/template/claude-task-manager/public/js/prompts.js +3159 -0
  26. package/template/claude-task-manager/public/js/reviews.js +1292 -0
  27. package/template/claude-task-manager/public/js/walle.js +3081 -0
  28. package/template/claude-task-manager/public/manifest.json +13 -0
  29. package/template/claude-task-manager/public/prompts.html +4353 -0
  30. package/template/claude-task-manager/public/setup.html +216 -0
  31. package/template/claude-task-manager/queue-engine.js +404 -0
  32. package/template/claude-task-manager/server-state.js +5 -0
  33. package/template/claude-task-manager/server.js +2254 -0
  34. package/template/claude-task-manager/session-utils.js +124 -0
  35. package/template/claude-task-manager/start.sh +17 -0
  36. package/template/claude-task-manager/tests/test-ai-search.js +61 -0
  37. package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
  38. package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
  39. package/template/claude-task-manager/tests/test-features-v2.js +127 -0
  40. package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
  41. package/template/claude-task-manager/tests/test-insights.js +124 -0
  42. package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
  43. package/template/claude-task-manager/tests/test-permissions.js +122 -0
  44. package/template/claude-task-manager/tests/test-pin.js +51 -0
  45. package/template/claude-task-manager/tests/test-prompts.js +164 -0
  46. package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
  47. package/template/claude-task-manager/tests/test-review.js +104 -0
  48. package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
  49. package/template/claude-task-manager/tests/test-send-final.js +30 -0
  50. package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
  51. package/template/claude-task-manager/tests/test-send-integration.js +107 -0
  52. package/template/claude-task-manager/tests/test-send-visual.js +34 -0
  53. package/template/claude-task-manager/tests/test-session-create.js +147 -0
  54. package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
  55. package/template/claude-task-manager/tests/test-url-hash.js +68 -0
  56. package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
  57. package/template/claude-task-manager/tests/test-ux-review.js +130 -0
  58. package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
  59. package/template/claude-task-manager/tests/test-zoom.js +92 -0
  60. package/template/claude-task-manager/tests/test-zoom2.js +67 -0
  61. package/template/docs/site/api/README.md +187 -0
  62. package/template/docs/site/guides/claude-code.md +58 -0
  63. package/template/docs/site/guides/configuration.md +96 -0
  64. package/template/docs/site/guides/quickstart.md +158 -0
  65. package/template/docs/site/index.md +14 -0
  66. package/template/docs/site/skills/README.md +135 -0
  67. package/template/wall-e/.dockerignore +11 -0
  68. package/template/wall-e/Dockerfile +25 -0
  69. package/template/wall-e/adapters/adapter-base.js +37 -0
  70. package/template/wall-e/adapters/ctm.js +193 -0
  71. package/template/wall-e/adapters/slack.js +56 -0
  72. package/template/wall-e/agent.js +319 -0
  73. package/template/wall-e/api-walle.js +1073 -0
  74. package/template/wall-e/brain.js +1235 -0
  75. package/template/wall-e/channels/agent-api.js +172 -0
  76. package/template/wall-e/channels/channel-base.js +14 -0
  77. package/template/wall-e/channels/imessage-channel.js +113 -0
  78. package/template/wall-e/channels/slack-channel.js +118 -0
  79. package/template/wall-e/chat.js +778 -0
  80. package/template/wall-e/decision/confidence.js +93 -0
  81. package/template/wall-e/deploy.sh +35 -0
  82. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
  83. package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
  84. package/template/wall-e/extraction/contradiction.js +168 -0
  85. package/template/wall-e/extraction/knowledge-extractor.js +190 -0
  86. package/template/wall-e/fly.toml +24 -0
  87. package/template/wall-e/loops/ingest.js +34 -0
  88. package/template/wall-e/loops/reflect.js +63 -0
  89. package/template/wall-e/loops/tasks.js +487 -0
  90. package/template/wall-e/loops/think.js +125 -0
  91. package/template/wall-e/package-lock.json +533 -0
  92. package/template/wall-e/package.json +18 -0
  93. package/template/wall-e/scripts/ingest-slack-search.js +85 -0
  94. package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
  95. package/template/wall-e/scripts/slack-backfill.js +295 -0
  96. package/template/wall-e/scripts/slack-channel-history.js +454 -0
  97. package/template/wall-e/server.js +93 -0
  98. package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
  99. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
  100. package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
  101. package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
  102. package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
  103. package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
  104. package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
  105. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
  106. package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
  107. package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
  108. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
  109. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
  110. package/template/wall-e/skills/claude-code-reader.js +144 -0
  111. package/template/wall-e/skills/mcp-client.js +407 -0
  112. package/template/wall-e/skills/skill-executor.js +163 -0
  113. package/template/wall-e/skills/skill-loader.js +410 -0
  114. package/template/wall-e/skills/skill-planner.js +88 -0
  115. package/template/wall-e/skills/slack-ingest.js +329 -0
  116. package/template/wall-e/skills/slack-pull-live.js +270 -0
  117. package/template/wall-e/skills/tool-executor.js +188 -0
  118. package/template/wall-e/tests/adapter-base.test.js +20 -0
  119. package/template/wall-e/tests/adapter-ctm.test.js +122 -0
  120. package/template/wall-e/tests/adapter-slack.test.js +98 -0
  121. package/template/wall-e/tests/agent-api.test.js +256 -0
  122. package/template/wall-e/tests/api-walle.test.js +222 -0
  123. package/template/wall-e/tests/brain.test.js +602 -0
  124. package/template/wall-e/tests/channels.test.js +104 -0
  125. package/template/wall-e/tests/chat.test.js +103 -0
  126. package/template/wall-e/tests/confidence.test.js +134 -0
  127. package/template/wall-e/tests/contradiction.test.js +217 -0
  128. package/template/wall-e/tests/ingest.test.js +113 -0
  129. package/template/wall-e/tests/mcp-client.test.js +71 -0
  130. package/template/wall-e/tests/reflect.test.js +103 -0
  131. package/template/wall-e/tests/server.test.js +111 -0
  132. package/template/wall-e/tests/skills.test.js +198 -0
  133. package/template/wall-e/tests/slack-ingest.test.js +103 -0
  134. package/template/wall-e/tests/think.test.js +435 -0
  135. package/template/wall-e/tools/local-tools.js +697 -0
  136. package/template/wall-e/tools/slack-mcp.js +290 -0
@@ -0,0 +1,4353 @@
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>Claude Prompt Editor</title>
7
+ <link rel="icon" type="image/png" sizes="32x32" href="/icon-32.png">
8
+ <link rel="icon" type="image/png" sizes="16x16" href="/icon-16.png">
9
+ <link rel="apple-touch-icon" sizes="180x180" href="/icon-180.png">
10
+ <link rel="manifest" href="/manifest.json">
11
+ <meta name="theme-color" content="#7aa2f7">
12
+ <style>
13
+ :root {
14
+ --bg: #1a1b26;
15
+ --bg-light: #24283b;
16
+ --bg-lighter: #2f3349;
17
+ --fg: #c0caf5;
18
+ --fg-dim: #565f89;
19
+ --accent: #7aa2f7;
20
+ --accent-hover: #89b4fa;
21
+ --green: #9ece6a;
22
+ --red: #f7768e;
23
+ --yellow: #e0af68;
24
+ --purple: #bb9af7;
25
+ --border: #3b4261;
26
+ }
27
+ * { margin: 0; padding: 0; box-sizing: border-box; }
28
+ body {
29
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif;
30
+ background: var(--bg);
31
+ color: var(--fg);
32
+ height: 100vh;
33
+ overflow: hidden;
34
+ display: flex;
35
+ flex-direction: column;
36
+ }
37
+
38
+ /* Top bar */
39
+ #topbar {
40
+ display: flex;
41
+ align-items: center;
42
+ height: 44px;
43
+ background: var(--bg-light);
44
+ border-bottom: 1px solid var(--border);
45
+ padding: 0 12px;
46
+ gap: 12px;
47
+ flex-shrink: 0;
48
+ }
49
+ .logo { font-weight: 700; font-size: 14px; color: var(--accent); white-space: nowrap; cursor: pointer; }
50
+ .topbar-actions { display: flex; gap: 6px; margin-left: auto; }
51
+ .btn {
52
+ background: var(--bg-lighter);
53
+ border: 1px solid var(--border);
54
+ color: var(--fg);
55
+ padding: 5px 12px;
56
+ border-radius: 6px;
57
+ font-size: 12px;
58
+ cursor: pointer;
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 5px;
62
+ transition: all 0.15s;
63
+ }
64
+ .btn:hover { background: var(--border); }
65
+ .btn.active { background: var(--accent); color: #1a1b26 !important; border-color: var(--accent); }
66
+ .btn.active:hover { background: var(--accent-hover); }
67
+ .btn.primary { background: var(--accent); color: #1a1b26; border-color: var(--accent); }
68
+ .btn.primary:hover { background: var(--accent-hover); }
69
+ .btn.danger { color: var(--red); }
70
+ .btn.danger:hover { background: rgba(247,118,142,0.15); }
71
+ .btn.small { padding: 3px 8px; font-size: 11px; }
72
+ .btn.icon-btn { padding: 4px 8px; }
73
+
74
+ /* Main layout: sidebar + editor */
75
+ #main {
76
+ display: flex;
77
+ flex: 1;
78
+ overflow: hidden;
79
+ }
80
+
81
+ /* Prompt sidebar */
82
+ #prompt-sidebar {
83
+ width: 280px;
84
+ background: var(--bg-light);
85
+ border-right: 1px solid var(--border);
86
+ display: flex;
87
+ flex-direction: column;
88
+ flex-shrink: 0;
89
+ }
90
+ .sidebar-header {
91
+ padding: 10px;
92
+ border-bottom: 1px solid var(--border);
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap: 6px;
96
+ }
97
+ .sidebar-header .search-row { display: flex; gap: 4px; }
98
+ .sidebar-header input {
99
+ flex: 1;
100
+ background: var(--bg);
101
+ color: var(--fg);
102
+ border: 1px solid var(--border);
103
+ padding: 5px 8px;
104
+ border-radius: 4px;
105
+ font-size: 11px;
106
+ outline: none;
107
+ }
108
+ .sidebar-header input:focus { border-color: var(--accent); }
109
+ .sidebar-header input.ai-active {
110
+ border-color: var(--purple);
111
+ background: rgba(187, 154, 247, 0.08);
112
+ }
113
+ .ai-search-toggle {
114
+ background: var(--bg-lighter);
115
+ border: 1px solid var(--border);
116
+ color: var(--fg-dim);
117
+ padding: 4px 7px;
118
+ border-radius: 4px;
119
+ cursor: pointer;
120
+ font-size: 10px;
121
+ font-weight: 600;
122
+ white-space: nowrap;
123
+ transition: all 0.15s;
124
+ }
125
+ .ai-search-toggle:hover { border-color: var(--purple); color: var(--purple); }
126
+ .ai-search-toggle.active {
127
+ background: var(--purple);
128
+ border-color: var(--purple);
129
+ color: #1a1b26;
130
+ }
131
+ .ai-result-reason {
132
+ font-size: 10px;
133
+ color: var(--purple);
134
+ margin-top: 2px;
135
+ line-height: 1.3;
136
+ font-style: italic;
137
+ }
138
+ .ai-result-score {
139
+ font-size: 9px;
140
+ background: var(--purple);
141
+ color: #1a1b26;
142
+ padding: 1px 5px;
143
+ border-radius: 8px;
144
+ font-weight: 600;
145
+ flex-shrink: 0;
146
+ }
147
+ .ai-searching {
148
+ padding: 16px;
149
+ text-align: center;
150
+ color: var(--fg-dim);
151
+ font-size: 12px;
152
+ }
153
+ .ai-searching .spinner {
154
+ display: inline-block;
155
+ width: 14px;
156
+ height: 14px;
157
+ border: 2px solid var(--border);
158
+ border-top-color: var(--purple);
159
+ border-radius: 50%;
160
+ animation: spin 0.6s linear infinite;
161
+ margin-right: 6px;
162
+ vertical-align: middle;
163
+ }
164
+ @keyframes spin { to { transform: rotate(360deg); } }
165
+ .folder-tree {
166
+ flex: 0 0 auto;
167
+ max-height: 200px;
168
+ overflow-y: auto;
169
+ padding: 6px;
170
+ }
171
+ .folder-item {
172
+ display: flex;
173
+ align-items: center;
174
+ gap: 6px;
175
+ padding: 5px 8px;
176
+ border-radius: 5px;
177
+ cursor: pointer;
178
+ font-size: 12px;
179
+ margin-bottom: 2px;
180
+ transition: background 0.1s;
181
+ }
182
+ .folder-item:hover { background: var(--bg-lighter); }
183
+ .folder-item.active { background: var(--accent); color: #1a1b26; }
184
+ .folder-item .folder-icon { font-size: 14px; }
185
+ .folder-item .folder-count {
186
+ margin-left: auto;
187
+ font-size: 10px;
188
+ background: var(--bg);
189
+ padding: 1px 6px;
190
+ border-radius: 8px;
191
+ color: var(--fg-dim);
192
+ }
193
+ .prompt-list {
194
+ border-top: 1px solid var(--border);
195
+ flex: 1;
196
+ overflow-y: auto;
197
+ padding: 6px;
198
+ }
199
+ .prompt-item {
200
+ padding: 8px 8px 8px 6px;
201
+ border-radius: 5px;
202
+ cursor: pointer;
203
+ font-size: 12px;
204
+ margin-bottom: 3px;
205
+ border-left: 3px solid transparent;
206
+ transition: background 0.1s;
207
+ position: relative;
208
+ display: flex;
209
+ align-items: flex-start;
210
+ gap: 6px;
211
+ }
212
+ .prompt-item:hover { background: var(--bg-lighter); }
213
+ .prompt-item.active { background: var(--bg-lighter); border-left-color: var(--accent); }
214
+ .prompt-item.pinned { border-left-color: var(--yellow); }
215
+ .prompt-item.active.pinned { border-left-color: var(--yellow); }
216
+ .prompt-item.drag-over { border-top: 2px solid var(--accent); }
217
+ .prompt-item .drag-handle {
218
+ cursor: grab;
219
+ color: var(--fg-dim);
220
+ opacity: 0;
221
+ font-size: 10px;
222
+ padding: 2px 0;
223
+ flex-shrink: 0;
224
+ user-select: none;
225
+ transition: opacity 0.1s;
226
+ }
227
+ .prompt-item:hover .drag-handle { opacity: 0.5; }
228
+ .prompt-item .drag-handle:hover { opacity: 1; }
229
+ .prompt-item .prompt-content { flex: 1; min-width: 0; }
230
+ .prompt-item .prompt-title {
231
+ font-weight: 500;
232
+ margin-bottom: 2px;
233
+ overflow: hidden;
234
+ text-overflow: ellipsis;
235
+ white-space: nowrap;
236
+ }
237
+ .prompt-item .prompt-meta {
238
+ display: flex;
239
+ gap: 6px;
240
+ font-size: 10px;
241
+ color: var(--fg-dim);
242
+ }
243
+ .prompt-item .prompt-meta .tag {
244
+ background: var(--bg);
245
+ padding: 0 5px;
246
+ border-radius: 3px;
247
+ }
248
+ .prompt-item .prompt-actions {
249
+ display: none;
250
+ gap: 2px;
251
+ flex-shrink: 0;
252
+ }
253
+ .prompt-item:hover .prompt-actions { display: flex; }
254
+ .prompt-item .prompt-actions .act-btn {
255
+ background: none;
256
+ border: none;
257
+ color: var(--fg-dim);
258
+ cursor: pointer;
259
+ padding: 2px 4px;
260
+ border-radius: 3px;
261
+ font-size: 11px;
262
+ line-height: 1;
263
+ transition: all 0.1s;
264
+ }
265
+ .prompt-item .prompt-actions .act-btn:hover { background: var(--bg); color: var(--fg); }
266
+ .prompt-item .prompt-actions .act-btn.pin-active { color: var(--yellow); }
267
+ .prompt-item.starred .prompt-title::before { content: '\2605 '; color: var(--yellow); }
268
+ .prompt-item .pin-badge { color: var(--yellow); font-size: 10px; margin-right: 2px; }
269
+
270
+ /* Folder item actions */
271
+ .folder-item { position: relative; }
272
+ .folder-item .folder-actions {
273
+ display: none;
274
+ gap: 2px;
275
+ margin-left: auto;
276
+ flex-shrink: 0;
277
+ }
278
+ .folder-item:hover .folder-actions { display: flex; }
279
+ .folder-item:hover .folder-count { display: none; }
280
+ .folder-item .folder-actions .act-btn {
281
+ background: none;
282
+ border: none;
283
+ color: var(--fg-dim);
284
+ cursor: pointer;
285
+ padding: 1px 4px;
286
+ border-radius: 3px;
287
+ font-size: 11px;
288
+ transition: all 0.1s;
289
+ }
290
+ .folder-item .folder-actions .act-btn:hover { background: var(--bg); color: var(--fg); }
291
+ .folder-item.drag-over { border-top: 2px solid var(--accent); }
292
+ .folder-item .drag-handle {
293
+ cursor: grab;
294
+ color: var(--fg-dim);
295
+ opacity: 0;
296
+ font-size: 10px;
297
+ flex-shrink: 0;
298
+ user-select: none;
299
+ }
300
+ .folder-item:hover .drag-handle { opacity: 0.5; }
301
+ .folder-item .drag-handle:hover { opacity: 1; }
302
+ .folder-rename-input {
303
+ background: var(--bg);
304
+ color: var(--fg);
305
+ border: 1px solid var(--accent);
306
+ padding: 2px 6px;
307
+ border-radius: 3px;
308
+ font-size: 12px;
309
+ outline: none;
310
+ width: 100%;
311
+ }
312
+
313
+ /* Editor area */
314
+ #editor-area {
315
+ flex: 1;
316
+ display: flex;
317
+ flex-direction: column;
318
+ overflow: hidden;
319
+ }
320
+
321
+ /* Editor toolbar */
322
+ .editor-toolbar {
323
+ display: flex;
324
+ align-items: center;
325
+ gap: 4px;
326
+ padding: 7px 18px;
327
+ border-bottom: 1px solid var(--border);
328
+ background: var(--bg-light);
329
+ flex-wrap: wrap;
330
+ flex-shrink: 0;
331
+ }
332
+ .toolbar-group {
333
+ display: flex;
334
+ gap: 2px;
335
+ padding-right: 10px;
336
+ border-right: 1px solid var(--border);
337
+ margin-right: 6px;
338
+ }
339
+ .toolbar-group:last-child { border-right: none; }
340
+ .toolbar-btn {
341
+ background: none;
342
+ border: 1px solid transparent;
343
+ color: var(--fg-dim);
344
+ padding: 5px 9px;
345
+ border-radius: 5px;
346
+ cursor: pointer;
347
+ font-size: 13px;
348
+ font-weight: 600;
349
+ transition: all 0.12s;
350
+ }
351
+ .toolbar-btn:hover { background: var(--bg-lighter); color: var(--fg); }
352
+ .toolbar-btn.active { background: var(--accent); color: #1a1b26; }
353
+
354
+ /* Meta bar (title, tags, context type) */
355
+ .meta-bar {
356
+ display: flex;
357
+ align-items: center;
358
+ gap: 10px;
359
+ padding: 10px 18px;
360
+ border-bottom: 1px solid var(--border);
361
+ background: var(--bg-light);
362
+ flex-shrink: 0;
363
+ }
364
+ .meta-bar input {
365
+ background: transparent;
366
+ border: none;
367
+ color: var(--fg);
368
+ font-size: 17px;
369
+ font-weight: 600;
370
+ outline: none;
371
+ flex: 1;
372
+ min-width: 0;
373
+ letter-spacing: -0.01em;
374
+ }
375
+ .meta-bar input::placeholder { color: var(--fg-dim); }
376
+ .meta-bar select {
377
+ background: var(--bg);
378
+ color: var(--fg);
379
+ border: 1px solid var(--border);
380
+ padding: 4px 8px;
381
+ border-radius: 4px;
382
+ font-size: 11px;
383
+ outline: none;
384
+ }
385
+ .tags-input {
386
+ display: flex;
387
+ align-items: center;
388
+ gap: 4px;
389
+ flex-wrap: wrap;
390
+ }
391
+ .tag-chip {
392
+ background: var(--bg-lighter);
393
+ border: 1px solid var(--border);
394
+ padding: 1px 8px;
395
+ border-radius: 10px;
396
+ font-size: 10px;
397
+ display: flex;
398
+ align-items: center;
399
+ gap: 4px;
400
+ cursor: default;
401
+ }
402
+ .tag-chip .tag-remove {
403
+ cursor: pointer;
404
+ opacity: 0.6;
405
+ font-size: 12px;
406
+ }
407
+ .tag-chip .tag-remove:hover { opacity: 1; }
408
+ .tags-input input {
409
+ background: transparent;
410
+ border: none;
411
+ color: var(--fg);
412
+ font-size: 11px;
413
+ outline: none;
414
+ width: 80px;
415
+ padding: 2px 0;
416
+ }
417
+
418
+ /* Send dropdown */
419
+ .send-dropdown-wrap {
420
+ position: relative;
421
+ display: inline-flex;
422
+ }
423
+ .send-dropdown-wrap .btn-main {
424
+ border-radius: 6px 0 0 6px;
425
+ border-right: none;
426
+ }
427
+ .send-dropdown-wrap .btn-caret {
428
+ border-radius: 0 6px 6px 0;
429
+ padding: 5px 6px;
430
+ border-left: 1px solid rgba(26,27,38,0.3);
431
+ min-width: 0;
432
+ }
433
+ .send-dropdown {
434
+ display: none;
435
+ position: absolute;
436
+ top: 100%;
437
+ right: 0;
438
+ margin-top: 4px;
439
+ background: var(--bg-light);
440
+ border: 1px solid var(--border);
441
+ border-radius: 8px;
442
+ min-width: 280px;
443
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
444
+ z-index: 100;
445
+ overflow: hidden;
446
+ }
447
+ .send-dropdown.open { display: block; }
448
+ .send-dropdown-section {
449
+ padding: 4px 0;
450
+ border-bottom: 1px solid var(--border);
451
+ }
452
+ .send-dropdown-section:last-child { border-bottom: none; }
453
+ .send-dropdown-section .section-label {
454
+ padding: 6px 12px 2px;
455
+ font-size: 10px;
456
+ text-transform: uppercase;
457
+ color: var(--fg-dim);
458
+ letter-spacing: 0.5px;
459
+ }
460
+ .send-dropdown-item {
461
+ display: flex;
462
+ align-items: center;
463
+ gap: 8px;
464
+ padding: 7px 12px;
465
+ font-size: 12px;
466
+ color: var(--fg);
467
+ cursor: pointer;
468
+ transition: background 0.1s;
469
+ }
470
+ .send-dropdown-item:hover { background: var(--bg-lighter); }
471
+ .send-dropdown-item .item-icon { font-size: 14px; flex-shrink: 0; width: 20px; text-align: center; }
472
+ .send-dropdown-item .item-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
473
+ .send-dropdown-item .item-meta { font-size: 10px; color: var(--fg-dim); flex-shrink: 0; }
474
+ .send-dropdown-item.disabled { opacity: 0.4; cursor: not-allowed; }
475
+ .send-dropdown-item.disabled:hover { background: transparent; }
476
+
477
+ /* Usage badges in meta bar */
478
+ .usage-badge {
479
+ display: inline-flex;
480
+ align-items: center;
481
+ gap: 4px;
482
+ font-size: 10px;
483
+ color: var(--fg-dim);
484
+ background: var(--bg);
485
+ padding: 2px 8px;
486
+ border-radius: 10px;
487
+ border: 1px solid var(--border);
488
+ cursor: pointer;
489
+ }
490
+ .usage-badge:hover { color: var(--fg); border-color: var(--accent); }
491
+
492
+ /* Rich text editor */
493
+ .editor-wrapper {
494
+ flex: 1;
495
+ overflow: auto;
496
+ padding: 40px 48px;
497
+ background: #171822;
498
+ }
499
+ .ProseMirror {
500
+ width: 740px;
501
+ margin: 0 auto;
502
+ min-height: 400px;
503
+ outline: none;
504
+ color: #d1d5e8;
505
+ font-family: 'Courier New', Courier, monospace;
506
+ font-size: 15px;
507
+ line-height: 1.8;
508
+ padding: 36px 44px;
509
+ background: var(--bg);
510
+ border: 1px solid var(--border);
511
+ border-radius: 10px;
512
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
513
+ }
514
+ .ProseMirror p { margin-bottom: 12px; }
515
+ /* First bold-only paragraph looks like a document title */
516
+ .ProseMirror > p:first-child > strong:only-child,
517
+ .ProseMirror > p:first-child > b:only-child {
518
+ font-size: 1.6em;
519
+ letter-spacing: -0.02em;
520
+ line-height: 1.35;
521
+ color: var(--fg);
522
+ }
523
+ /* Empty paragraphs act as section spacers */
524
+ .ProseMirror > p:empty { margin-bottom: 8px; }
525
+ .ProseMirror h1 {
526
+ font-size: 1.75em;
527
+ font-weight: 700;
528
+ margin: 32px 0 14px;
529
+ color: var(--fg);
530
+ letter-spacing: -0.02em;
531
+ line-height: 1.3;
532
+ }
533
+ .ProseMirror h2 {
534
+ font-size: 1.4em;
535
+ font-weight: 600;
536
+ margin: 28px 0 12px;
537
+ color: var(--fg);
538
+ letter-spacing: -0.01em;
539
+ line-height: 1.35;
540
+ }
541
+ .ProseMirror h3 {
542
+ font-size: 1.15em;
543
+ font-weight: 600;
544
+ margin: 22px 0 8px;
545
+ color: var(--fg);
546
+ line-height: 1.4;
547
+ }
548
+ .ProseMirror ul, .ProseMirror ol { padding-left: 28px; margin-bottom: 12px; }
549
+ .ProseMirror li { margin-bottom: 5px; }
550
+ .ProseMirror li p { margin-bottom: 5px; }
551
+ .ProseMirror blockquote {
552
+ border-left: 3px solid var(--accent);
553
+ padding: 4px 0 4px 18px;
554
+ color: var(--fg-dim);
555
+ margin: 14px 0;
556
+ background: rgba(122, 162, 247, 0.04);
557
+ border-radius: 0 6px 6px 0;
558
+ }
559
+ .ProseMirror a {
560
+ color: var(--accent);
561
+ text-decoration: underline;
562
+ text-decoration-color: rgba(122, 162, 247, 0.35);
563
+ text-underline-offset: 2px;
564
+ transition: text-decoration-color 0.15s;
565
+ }
566
+ .ProseMirror a:hover {
567
+ text-decoration-color: var(--accent);
568
+ }
569
+ .ProseMirror code {
570
+ background: var(--bg-lighter);
571
+ padding: 2px 7px;
572
+ border-radius: 4px;
573
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
574
+ font-size: 0.87em;
575
+ border: 1px solid rgba(59, 66, 97, 0.5);
576
+ }
577
+ .ProseMirror pre {
578
+ background: var(--bg-light);
579
+ border: 1px solid var(--border);
580
+ border-radius: 8px;
581
+ padding: 16px 18px;
582
+ margin: 16px 0;
583
+ overflow-x: auto;
584
+ }
585
+ .ProseMirror pre code {
586
+ background: none;
587
+ padding: 0;
588
+ border-radius: 0;
589
+ border: none;
590
+ font-size: 0.87em;
591
+ display: block;
592
+ white-space: pre;
593
+ }
594
+ .ProseMirror hr {
595
+ border: none;
596
+ border-top: 1px solid var(--border);
597
+ margin: 28px 0;
598
+ }
599
+ .ProseMirror strong { font-weight: 700; color: var(--fg); }
600
+ .ProseMirror em { font-style: italic; }
601
+ .ProseMirror img {
602
+ max-width: 100%;
603
+ border-radius: 8px;
604
+ margin: 14px 0;
605
+ cursor: pointer;
606
+ border: 2px solid transparent;
607
+ transition: border-color 0.15s, box-shadow 0.15s;
608
+ }
609
+ .ProseMirror img.img-selected {
610
+ border-color: var(--accent);
611
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 30%, transparent);
612
+ }
613
+ .ProseMirror .is-editor-empty:first-child::before {
614
+ content: attr(data-placeholder);
615
+ color: var(--fg-dim);
616
+ pointer-events: none;
617
+ float: left;
618
+ height: 0;
619
+ }
620
+
621
+ /* Version panel */
622
+ #version-panel {
623
+ display: none;
624
+ width: 300px;
625
+ background: var(--bg-light);
626
+ border-left: 1px solid var(--border);
627
+ flex-direction: column;
628
+ overflow: hidden;
629
+ }
630
+ #version-panel.active { display: flex; }
631
+ .version-header {
632
+ padding: 10px;
633
+ border-bottom: 1px solid var(--border);
634
+ font-size: 13px;
635
+ font-weight: 600;
636
+ display: flex;
637
+ align-items: center;
638
+ justify-content: space-between;
639
+ }
640
+ .version-list {
641
+ flex: 1;
642
+ overflow-y: auto;
643
+ padding: 6px;
644
+ }
645
+ .version-item {
646
+ padding: 8px;
647
+ border-radius: 5px;
648
+ margin-bottom: 4px;
649
+ cursor: pointer;
650
+ border-left: 3px solid transparent;
651
+ transition: background 0.1s;
652
+ }
653
+ .version-item:hover { background: var(--bg-lighter); }
654
+ .version-item.active { border-left-color: var(--accent); background: var(--bg-lighter); }
655
+ .version-item .v-num { font-size: 12px; font-weight: 600; color: var(--accent); }
656
+ .version-item .v-date { font-size: 10px; color: var(--fg-dim); }
657
+ .version-item .v-msg { font-size: 11px; color: var(--fg-dim); margin-top: 2px; }
658
+
659
+ /* Queue Builder panel */
660
+ #queue-builder {
661
+ display: none;
662
+ width: 300px;
663
+ background: var(--bg-light);
664
+ border-left: 1px solid var(--border);
665
+ flex-direction: column;
666
+ overflow: hidden;
667
+ flex-shrink: 0;
668
+ }
669
+ #queue-builder.active { display: flex; }
670
+ .queue-builder-item {
671
+ display: flex;
672
+ align-items: center;
673
+ gap: 6px;
674
+ padding: 6px 8px;
675
+ border-radius: 5px;
676
+ margin-bottom: 3px;
677
+ font-size: 12px;
678
+ background: var(--bg);
679
+ border: 1px solid var(--border);
680
+ cursor: grab;
681
+ }
682
+ .queue-builder-item:hover { border-color: var(--accent); }
683
+ .queue-builder-item.drag-over { border-top: 2px solid var(--accent); }
684
+ .queue-builder-item .qb-num {
685
+ font-size: 10px;
686
+ font-weight: 600;
687
+ color: var(--accent);
688
+ width: 18px;
689
+ text-align: center;
690
+ flex-shrink: 0;
691
+ }
692
+ .queue-builder-item .qb-title {
693
+ flex: 1;
694
+ overflow: hidden;
695
+ text-overflow: ellipsis;
696
+ white-space: nowrap;
697
+ }
698
+ .queue-builder-item .qb-type {
699
+ font-size: 9px;
700
+ color: var(--fg-dim);
701
+ background: var(--bg-lighter);
702
+ padding: 1px 5px;
703
+ border-radius: 8px;
704
+ }
705
+ .queue-builder-item .qb-remove {
706
+ background: none;
707
+ border: none;
708
+ color: var(--fg-dim);
709
+ cursor: pointer;
710
+ font-size: 14px;
711
+ padding: 0 2px;
712
+ line-height: 1;
713
+ }
714
+ .queue-builder-item .qb-remove:hover { color: var(--red); }
715
+ .prompt-item .queue-add-btn {
716
+ display: none;
717
+ background: none;
718
+ border: none;
719
+ color: var(--fg-dim);
720
+ cursor: pointer;
721
+ padding: 2px 4px;
722
+ border-radius: 3px;
723
+ font-size: 11px;
724
+ line-height: 1;
725
+ transition: all 0.1s;
726
+ }
727
+ .prompt-item:hover .queue-add-btn { display: inline-block; }
728
+ .prompt-item .queue-add-btn:hover { background: var(--bg); color: var(--accent); }
729
+
730
+ /* Image annotation overlay */
731
+ .annotation-overlay {
732
+ position: fixed;
733
+ inset: 0;
734
+ background: rgba(0,0,0,0.85);
735
+ z-index: 200;
736
+ display: none;
737
+ flex-direction: column;
738
+ align-items: center;
739
+ justify-content: center;
740
+ }
741
+ .annotation-overlay.active { display: flex; }
742
+ .annotation-toolbar {
743
+ display: flex;
744
+ gap: 6px;
745
+ padding: 8px;
746
+ background: var(--bg-light);
747
+ border-radius: 8px;
748
+ margin-bottom: 8px;
749
+ }
750
+ .annotation-toolbar .tool-btn {
751
+ background: var(--bg-lighter);
752
+ border: 1px solid var(--border);
753
+ color: var(--fg);
754
+ padding: 6px 12px;
755
+ border-radius: 5px;
756
+ cursor: pointer;
757
+ font-size: 12px;
758
+ transition: all 0.1s;
759
+ }
760
+ .annotation-toolbar .tool-btn:hover { border-color: var(--accent); }
761
+ .annotation-toolbar .tool-btn.active { background: var(--accent); color: #1a1b26; border-color: var(--accent); }
762
+ .annotation-canvas-container {
763
+ position: relative;
764
+ max-width: 90vw;
765
+ max-height: calc(100vh - 120px);
766
+ overflow: auto;
767
+ }
768
+ .annotation-canvas-container img {
769
+ max-width: 100%;
770
+ display: block;
771
+ }
772
+ .annotation-canvas-container canvas {
773
+ position: absolute;
774
+ top: 0;
775
+ left: 0;
776
+ cursor: crosshair;
777
+ }
778
+
779
+ /* Right panel area for chains, permissions, etc */
780
+ .panel-tabs {
781
+ display: flex;
782
+ border-bottom: 1px solid var(--border);
783
+ background: var(--bg);
784
+ }
785
+ .panel-tab {
786
+ padding: 6px 14px;
787
+ font-size: 12px;
788
+ cursor: pointer;
789
+ color: var(--fg-dim);
790
+ border-bottom: 2px solid transparent;
791
+ transition: all 0.1s;
792
+ }
793
+ .panel-tab:hover { color: var(--fg); }
794
+ .panel-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
795
+
796
+ /* Chains visual editor */
797
+ .chain-canvas {
798
+ flex: 1;
799
+ overflow: auto;
800
+ position: relative;
801
+ background: var(--bg);
802
+ background-image: radial-gradient(var(--border) 1px, transparent 1px);
803
+ background-size: 20px 20px;
804
+ }
805
+ .chain-node {
806
+ position: absolute;
807
+ background: var(--bg-light);
808
+ border: 1px solid var(--border);
809
+ border-radius: 8px;
810
+ padding: 10px;
811
+ min-width: 150px;
812
+ cursor: move;
813
+ font-size: 12px;
814
+ }
815
+ .chain-node:hover { border-color: var(--accent); }
816
+ .chain-node .node-title { font-weight: 600; margin-bottom: 4px; }
817
+ .chain-node .node-type { font-size: 10px; color: var(--fg-dim); }
818
+
819
+ /* Empty state */
820
+ .empty-state {
821
+ display: flex;
822
+ flex-direction: column;
823
+ align-items: center;
824
+ justify-content: center;
825
+ flex: 1;
826
+ gap: 12px;
827
+ color: var(--fg-dim);
828
+ text-align: center;
829
+ padding: 40px;
830
+ }
831
+ .empty-state h3 { color: var(--fg); font-size: 16px; }
832
+ .empty-state p { font-size: 13px; line-height: 1.6; max-width: 400px; }
833
+
834
+ /* Modal */
835
+ .modal-overlay {
836
+ position: fixed;
837
+ inset: 0;
838
+ background: rgba(0,0,0,0.6);
839
+ display: flex;
840
+ align-items: center;
841
+ justify-content: center;
842
+ z-index: 100;
843
+ }
844
+ .modal-overlay.hidden { display: none; }
845
+ .modal {
846
+ background: var(--bg-light);
847
+ border: 1px solid var(--border);
848
+ border-radius: 10px;
849
+ padding: 20px;
850
+ min-width: 420px;
851
+ max-width: 90vw;
852
+ max-height: 90vh;
853
+ overflow-y: auto;
854
+ }
855
+ .modal h3 { margin-bottom: 14px; font-size: 15px; }
856
+ .modal label { display: block; font-size: 12px; color: var(--fg-dim); margin-bottom: 4px; margin-top: 10px; }
857
+ .modal input, .modal select, .modal textarea {
858
+ width: 100%;
859
+ background: var(--bg);
860
+ color: var(--fg);
861
+ border: 1px solid var(--border);
862
+ padding: 8px 10px;
863
+ border-radius: 5px;
864
+ font-size: 13px;
865
+ outline: none;
866
+ }
867
+ .modal input:focus, .modal select:focus, .modal textarea:focus { border-color: var(--accent); }
868
+ .modal textarea { min-height: 80px; resize: vertical; font-family: 'SF Mono', monospace; }
869
+ .modal .btn-row { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
870
+
871
+ /* Scrollbars */
872
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
873
+ ::-webkit-scrollbar-track { background: transparent; }
874
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
875
+ ::-webkit-scrollbar-thumb:hover { background: var(--fg-dim); }
876
+
877
+ /* Bottom status bar */
878
+ #statusbar {
879
+ display: flex;
880
+ align-items: center;
881
+ height: 24px;
882
+ background: var(--bg-light);
883
+ border-top: 1px solid var(--border);
884
+ padding: 0 12px;
885
+ font-size: 10px;
886
+ color: var(--fg-dim);
887
+ gap: 12px;
888
+ flex-shrink: 0;
889
+ }
890
+ #statusbar .status-item { display: flex; align-items: center; gap: 4px; }
891
+
892
+ /* Toast notification system */
893
+ #toast-container {
894
+ position: fixed; bottom: 20px; right: 20px; z-index: 10000;
895
+ display: flex; flex-direction: column-reverse; gap: 8px;
896
+ pointer-events: none; max-width: 420px;
897
+ }
898
+ .toast {
899
+ display: flex; align-items: flex-start; gap: 10px;
900
+ background: var(--bg-light); border: 1px solid var(--border);
901
+ border-radius: 12px; padding: 12px 16px;
902
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4), 0 2px 8px rgba(0,0,0,0.2);
903
+ pointer-events: auto; cursor: default;
904
+ transform: translateX(120%); opacity: 0;
905
+ transition: transform 0.35s cubic-bezier(0.21,1.02,0.73,1), opacity 0.35s ease;
906
+ max-width: 420px; min-width: 280px;
907
+ }
908
+ .toast.show { transform: translateX(0); opacity: 1; }
909
+ .toast.hide { transform: translateX(120%); opacity: 0; }
910
+ .toast-icon {
911
+ width: 28px; height: 28px; border-radius: 8px;
912
+ display: flex; align-items: center; justify-content: center;
913
+ flex-shrink: 0; font-size: 14px;
914
+ }
915
+ .toast-icon.success { background: rgba(158,206,106,0.15); color: var(--green); }
916
+ .toast-icon.error { background: rgba(247,118,142,0.15); color: var(--red); }
917
+ .toast-icon.warning { background: rgba(224,175,104,0.15); color: var(--yellow); }
918
+ .toast-icon.info { background: rgba(122,162,247,0.15); color: var(--accent); }
919
+ .toast-body { flex: 1; min-width: 0; }
920
+ .toast-title { font-size: 12px; font-weight: 600; color: var(--fg); margin-bottom: 2px; }
921
+ .toast-msg { font-size: 11px; color: var(--fg-dim); line-height: 1.4; word-break: break-word; }
922
+ .toast-close {
923
+ background: none; border: none; color: var(--fg-dim); cursor: pointer;
924
+ font-size: 14px; padding: 0 0 0 8px; line-height: 1; flex-shrink: 0;
925
+ opacity: 0; transition: opacity 0.2s;
926
+ }
927
+ .toast:hover .toast-close { opacity: 1; }
928
+ .toast-progress {
929
+ position: absolute; bottom: 0; left: 0; height: 2px;
930
+ border-radius: 0 0 12px 12px; transition: width linear;
931
+ }
932
+ .toast-progress.success { background: var(--green); }
933
+ .toast-progress.error { background: var(--red); }
934
+ .toast-progress.warning { background: var(--yellow); }
935
+ .toast-progress.info { background: var(--accent); }
936
+
937
+ /* --- Autocomplete Dropdown --- */
938
+ .autocomplete-wrap { position: relative; }
939
+ .autocomplete-dropdown {
940
+ display: none;
941
+ position: absolute;
942
+ top: 100%;
943
+ left: 0;
944
+ right: 0;
945
+ background: var(--bg-light);
946
+ border: 1px solid var(--border);
947
+ border-radius: 6px;
948
+ max-height: 260px;
949
+ overflow-y: auto;
950
+ z-index: 200;
951
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
952
+ margin-top: 2px;
953
+ }
954
+ .autocomplete-dropdown.visible { display: block; }
955
+ .ac-item {
956
+ display: flex;
957
+ align-items: flex-start;
958
+ gap: 8px;
959
+ padding: 6px 10px;
960
+ cursor: pointer;
961
+ font-size: 11px;
962
+ border-bottom: 1px solid var(--border);
963
+ transition: background 0.1s;
964
+ }
965
+ .ac-item:last-child { border-bottom: none; }
966
+ .ac-item:hover, .ac-item.selected { background: var(--bg-lighter); }
967
+ .ac-type {
968
+ font-size: 9px;
969
+ padding: 1px 5px;
970
+ border-radius: 4px;
971
+ font-weight: 600;
972
+ flex-shrink: 0;
973
+ margin-top: 1px;
974
+ }
975
+ .ac-type.history { background: rgba(122,162,247,0.15); color: var(--accent); }
976
+ .ac-type.library { background: rgba(158,206,106,0.15); color: var(--green); }
977
+ .ac-type.tool_context { background: rgba(224,175,104,0.15); color: var(--yellow); }
978
+ .ac-text { flex: 1; line-height: 1.4; color: var(--fg); }
979
+ .ac-text .full { display: block; font-size: 10px; color: var(--fg-dim); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
980
+
981
+ /* --- Lifecycle Badge --- */
982
+ .lifecycle-badge {
983
+ font-size: 9px;
984
+ padding: 1px 5px;
985
+ border-radius: 8px;
986
+ font-weight: 600;
987
+ text-transform: uppercase;
988
+ letter-spacing: 0.3px;
989
+ }
990
+ .lifecycle-badge.draft { background: rgba(86,95,137,0.3); color: var(--fg-dim); }
991
+ .lifecycle-badge.used { background: rgba(122,162,247,0.2); color: var(--accent); }
992
+ .lifecycle-badge.frequent { background: rgba(158,206,106,0.2); color: var(--green); }
993
+ .lifecycle-badge.template { background: rgba(187,154,247,0.2); color: var(--purple); }
994
+ .lifecycle-badge.archived { background: rgba(247,118,142,0.15); color: var(--red); }
995
+
996
+ /* --- Harvest Panel (modal overlay) --- */
997
+ .harvest-modal {
998
+ position: fixed;
999
+ top: 0; left: 0; right: 0; bottom: 0;
1000
+ background: rgba(0,0,0,0.6);
1001
+ z-index: 500;
1002
+ display: none;
1003
+ align-items: center;
1004
+ justify-content: center;
1005
+ }
1006
+ .harvest-modal.visible { display: flex; }
1007
+ .harvest-content {
1008
+ background: var(--bg-light);
1009
+ border: 1px solid var(--border);
1010
+ border-radius: 10px;
1011
+ padding: 20px;
1012
+ max-width: 560px;
1013
+ width: 90%;
1014
+ max-height: 80vh;
1015
+ overflow-y: auto;
1016
+ }
1017
+ .harvest-stat {
1018
+ display: flex;
1019
+ justify-content: space-between;
1020
+ padding: 4px 0;
1021
+ font-size: 12px;
1022
+ border-bottom: 1px solid var(--border);
1023
+ }
1024
+ .harvest-stat:last-child { border-bottom: none; }
1025
+ .harvest-stat .label { color: var(--fg-dim); }
1026
+ .harvest-stat .value { font-weight: 600; color: var(--fg); }
1027
+ .harvest-projects {
1028
+ max-height: 120px;
1029
+ overflow-y: auto;
1030
+ margin-top: 6px;
1031
+ font-size: 11px;
1032
+ }
1033
+ .harvest-projects div {
1034
+ display: flex;
1035
+ justify-content: space-between;
1036
+ padding: 2px 0;
1037
+ color: var(--fg-dim);
1038
+ }
1039
+ .harvest-projects div span:last-child { color: var(--fg); }
1040
+
1041
+ /* --- Copilot & Similar Prompts Panel --- */
1042
+ #copilot-panel {
1043
+ width: 340px;
1044
+ background: var(--bg-light);
1045
+ border-left: 1px solid var(--border);
1046
+ display: none;
1047
+ flex-direction: column;
1048
+ flex-shrink: 0;
1049
+ overflow: hidden;
1050
+ }
1051
+ #copilot-panel.active { display: flex; }
1052
+ .copilot-tabs {
1053
+ display: flex;
1054
+ border-bottom: 1px solid var(--border);
1055
+ background: var(--bg);
1056
+ }
1057
+ .copilot-tab {
1058
+ flex: 1;
1059
+ padding: 8px 4px;
1060
+ font-size: 11px;
1061
+ text-align: center;
1062
+ cursor: pointer;
1063
+ color: var(--fg-dim);
1064
+ border-bottom: 2px solid transparent;
1065
+ transition: all 0.15s;
1066
+ }
1067
+ .copilot-tab:hover { color: var(--fg); }
1068
+ .copilot-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
1069
+ .copilot-body { flex: 1; overflow-y: auto; padding: 10px; }
1070
+
1071
+ /* Similar prompts */
1072
+ .similar-item {
1073
+ padding: 8px;
1074
+ background: var(--bg);
1075
+ border: 1px solid var(--border);
1076
+ border-radius: 6px;
1077
+ margin-bottom: 6px;
1078
+ cursor: pointer;
1079
+ font-size: 11px;
1080
+ transition: border-color 0.15s;
1081
+ }
1082
+ .similar-item:hover { border-color: var(--accent); }
1083
+ .similar-item .si-title { font-weight: 500; margin-bottom: 3px; }
1084
+ .similar-item .si-meta { color: var(--fg-dim); font-size: 10px; }
1085
+ .similar-item .si-match { float: right; font-size: 9px; color: var(--accent); }
1086
+
1087
+ /* Copilot chat */
1088
+ .copilot-chat { display: flex; flex-direction: column; height: 100%; }
1089
+ .copilot-messages { flex: 1; overflow-y: auto; padding: 8px; }
1090
+ .copilot-msg {
1091
+ padding: 8px 10px;
1092
+ border-radius: 8px;
1093
+ margin-bottom: 6px;
1094
+ font-size: 12px;
1095
+ line-height: 1.5;
1096
+ max-width: 90%;
1097
+ }
1098
+ .copilot-msg.user { background: var(--accent); color: #1a1b26; margin-left: auto; }
1099
+ .copilot-msg.assistant { background: var(--bg); border: 1px solid var(--border); }
1100
+ .copilot-input-row {
1101
+ display: flex;
1102
+ gap: 4px;
1103
+ padding: 8px;
1104
+ border-top: 1px solid var(--border);
1105
+ }
1106
+ .copilot-input-row input {
1107
+ flex: 1;
1108
+ background: var(--bg);
1109
+ color: var(--fg);
1110
+ border: 1px solid var(--border);
1111
+ padding: 6px 10px;
1112
+ border-radius: 6px;
1113
+ font-size: 12px;
1114
+ outline: none;
1115
+ }
1116
+ .copilot-input-row input:focus { border-color: var(--accent); }
1117
+
1118
+ /* Patterns view */
1119
+ .pattern-item {
1120
+ padding: 8px;
1121
+ background: var(--bg);
1122
+ border: 1px solid var(--border);
1123
+ border-radius: 6px;
1124
+ margin-bottom: 6px;
1125
+ }
1126
+ .pattern-item .pi-freq { float: right; font-size: 10px; background: var(--accent); color: #1a1b26; padding: 1px 6px; border-radius: 8px; font-weight: 600; }
1127
+ .pattern-item .pi-text { font-size: 12px; margin-bottom: 3px; }
1128
+ .pattern-item .pi-meta { font-size: 10px; color: var(--fg-dim); }
1129
+ </style>
1130
+ </head>
1131
+ <body>
1132
+ <div id="toast-container"></div>
1133
+ <div id="topbar">
1134
+ <span class="logo" onclick="navigateToMain()">Claude Task Manager</span>
1135
+ <span style="color:var(--fg-dim);font-size:12px">/</span>
1136
+ <span style="font-size:13px;font-weight:500">Prompt Editor</span>
1137
+ <div class="topbar-actions">
1138
+ <button class="btn" data-view="chains" onclick="showView('chains')">Chains</button>
1139
+ <button class="btn" data-view="conversations" onclick="showView('conversations')">Sessions DB</button>
1140
+ <button class="btn" data-view="templates" onclick="showView('templates')">Templates</button>
1141
+ <button class="btn" data-view="backups" onclick="showView('backups')" style="color:var(--green)">Backups</button>
1142
+ <button class="btn" data-view="patterns" onclick="showView('patterns')">Patterns</button>
1143
+ <button class="btn" onclick="openHarvestModal()" style="color:var(--yellow)">Harvest</button>
1144
+ <button class="btn" onclick="toggleCopilotPanel()" id="copilot-toggle">Copilot</button>
1145
+ <button class="btn" onclick="toggleQueueBuilder()" id="queue-builder-toggle">Queue</button>
1146
+ <button class="btn primary" onclick="createNewPrompt()">+ New Prompt</button>
1147
+ </div>
1148
+ </div>
1149
+
1150
+ <div id="main">
1151
+ <!-- Sidebar: folders + prompt list -->
1152
+ <div id="prompt-sidebar">
1153
+ <div class="sidebar-header">
1154
+ <div class="search-row autocomplete-wrap">
1155
+ <input id="prompt-search" type="text" placeholder="Search prompts..." oninput="onSearchInput()" onkeydown="onSearchKeydown(event)" autocomplete="off">
1156
+ <div class="autocomplete-dropdown" id="ac-dropdown"></div>
1157
+ <button id="ai-search-btn" class="ai-search-toggle" onclick="toggleAiSearch()" title="AI-powered semantic search (Enter to search)">AI</button>
1158
+ <button class="btn small" onclick="createNewFolder()">+ Folder</button>
1159
+ </div>
1160
+ <div style="display:flex;gap:4px;">
1161
+ <select id="context-filter" onchange="filterPrompts()" style="flex:1;background:var(--bg);color:var(--fg);border:1px solid var(--border);padding:3px 6px;border-radius:4px;font-size:10px;">
1162
+ <option value="">All Types</option>
1163
+ <option value="general">General</option>
1164
+ <option value="coding">Coding</option>
1165
+ <option value="debugging">Debugging</option>
1166
+ <option value="design">Design</option>
1167
+ <option value="research">Research</option>
1168
+ <option value="devops">DevOps</option>
1169
+ </select>
1170
+ <select id="lifecycle-filter" onchange="filterPrompts()" style="flex:1;background:var(--bg);color:var(--fg);border:1px solid var(--border);padding:3px 6px;border-radius:4px;font-size:10px;">
1171
+ <option value="">All Status</option>
1172
+ <option value="draft">Draft</option>
1173
+ <option value="used">Used</option>
1174
+ <option value="frequent">Frequent</option>
1175
+ <option value="template">Template</option>
1176
+ <option value="archived">Archived</option>
1177
+ </select>
1178
+ <button class="btn small" id="pe-star-filter-btn" onclick="filterPrompts('starred')">&#9733;</button>
1179
+ </div>
1180
+ </div>
1181
+ <div class="folder-tree" id="folder-tree"></div>
1182
+ <div class="prompt-list" id="prompt-list"></div>
1183
+ </div>
1184
+
1185
+ <!-- Editor / Panel area -->
1186
+ <div id="editor-area">
1187
+ <!-- View: Editor -->
1188
+ <div id="view-editor" style="display:flex;flex-direction:column;flex:1;overflow:hidden;">
1189
+ <div class="meta-bar" id="meta-bar" style="display:none;">
1190
+ <input id="prompt-title-input" type="text" placeholder="Prompt title..." oninput="onTitleChange()">
1191
+ <select id="prompt-context-type" onchange="onContextTypeChange()" style="background:var(--bg);color:var(--fg);border:1px solid var(--border);padding:4px 8px;border-radius:4px;font-size:11px;">
1192
+ <option value="general">General</option>
1193
+ <option value="coding">Coding</option>
1194
+ <option value="debugging">Debugging</option>
1195
+ <option value="design">Design</option>
1196
+ <option value="research">Research</option>
1197
+ <option value="devops">DevOps</option>
1198
+ </select>
1199
+ <div class="tags-input" id="tags-input">
1200
+ <input id="tag-input" type="text" placeholder="Add tag..." onkeydown="onTagKeydown(event)">
1201
+ </div>
1202
+ <select id="lifecycle-status" onchange="onLifecycleChange()" style="background:var(--bg);color:var(--fg);border:1px solid var(--border);padding:3px 6px;border-radius:4px;font-size:10px;">
1203
+ <option value="draft">Draft</option>
1204
+ <option value="used">Used</option>
1205
+ <option value="frequent">Frequent</option>
1206
+ <option value="template">Template</option>
1207
+ <option value="archived">Archived</option>
1208
+ </select>
1209
+ <button class="btn small" onclick="toggleStar()" id="star-btn" title="Star/unstar">&#9734;</button>
1210
+ <button class="btn small" onclick="toggleVersions()" title="Version History">History</button>
1211
+ <span id="usage-badge-wrap"></span>
1212
+ <div class="send-dropdown-wrap" id="send-dropdown-wrap">
1213
+ <button class="btn small primary btn-main" onclick="sendToNewSession()">Send</button>
1214
+ <button class="btn small primary btn-caret" onclick="toggleSendDropdown(event)">&#9660;</button>
1215
+ <div class="send-dropdown" id="send-dropdown">
1216
+ <div class="send-dropdown-section">
1217
+ <div class="send-dropdown-item" onclick="sendToNewSession()">
1218
+ <span class="item-icon">&#128640;</span>
1219
+ <span class="item-label">Send to New Session</span>
1220
+ </div>
1221
+ </div>
1222
+ <div class="send-dropdown-section" id="send-active-sessions">
1223
+ <div class="section-label">Active Sessions</div>
1224
+ <div class="send-dropdown-item disabled">
1225
+ <span class="item-icon">&#128268;</span>
1226
+ <span class="item-label">No active sessions</span>
1227
+ </div>
1228
+ </div>
1229
+ <div class="send-dropdown-section">
1230
+ <div class="send-dropdown-item" onclick="copyPromptText()">
1231
+ <span class="item-icon">&#128203;</span>
1232
+ <span class="item-label">Copy as Text</span>
1233
+ </div>
1234
+ <div class="send-dropdown-item" onclick="copyPromptMarkdown()">
1235
+ <span class="item-icon">&#128221;</span>
1236
+ <span class="item-label">Copy as Markdown</span>
1237
+ </div>
1238
+ </div>
1239
+ </div>
1240
+ </div>
1241
+ </div>
1242
+ <div class="editor-toolbar" id="editor-toolbar" style="display:none;">
1243
+ <div class="toolbar-group">
1244
+ <button class="toolbar-btn" onclick="execCmd('toggleBold')" title="Bold"><b>B</b></button>
1245
+ <button class="toolbar-btn" onclick="execCmd('toggleItalic')" title="Italic"><i>I</i></button>
1246
+ <button class="toolbar-btn" onclick="execCmd('toggleStrike')" title="Strikethrough"><s>S</s></button>
1247
+ <button class="toolbar-btn" onclick="execCmd('toggleCode')" title="Code">&lt;/&gt;</button>
1248
+ </div>
1249
+ <div class="toolbar-group">
1250
+ <button class="toolbar-btn" onclick="execCmd('toggleHeading', {level:1})" title="H1">H1</button>
1251
+ <button class="toolbar-btn" onclick="execCmd('toggleHeading', {level:2})" title="H2">H2</button>
1252
+ <button class="toolbar-btn" onclick="execCmd('toggleHeading', {level:3})" title="H3">H3</button>
1253
+ </div>
1254
+ <div class="toolbar-group">
1255
+ <button class="toolbar-btn" onclick="execCmd('toggleBulletList')" title="Bullet List">&#8226;</button>
1256
+ <button class="toolbar-btn" onclick="execCmd('toggleOrderedList')" title="Ordered List">1.</button>
1257
+ <button class="toolbar-btn" onclick="execCmd('toggleBlockquote')" title="Quote">&ldquo;</button>
1258
+ <button class="toolbar-btn" onclick="execCmd('toggleCodeBlock')" title="Code Block">{ }</button>
1259
+ </div>
1260
+ <div class="toolbar-group">
1261
+ <button class="toolbar-btn" onclick="execCmd('setHorizontalRule')" title="Horizontal Rule">&mdash;</button>
1262
+ <button class="toolbar-btn" onclick="insertImage()" title="Insert Image">&#128247;</button>
1263
+ <button class="toolbar-btn" onclick="takeScreenshot()" title="Screenshot">&#128248;</button>
1264
+ </div>
1265
+ <div class="toolbar-group">
1266
+ <button class="toolbar-btn" onclick="execCmd('undo')" title="Undo">&#8617;</button>
1267
+ <button class="toolbar-btn" onclick="execCmd('redo')" title="Redo">&#8618;</button>
1268
+ </div>
1269
+ <div class="toolbar-group">
1270
+ <select id="font-select" onchange="setEditorFont(this.value)" title="Editor Font" style="background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:4px;font-size:11px;padding:2px 4px;outline:none;cursor:pointer;min-width:110px;">
1271
+ <option value="'Courier New', Courier, monospace">Courier New</option>
1272
+ <option value="'Menlo', 'Monaco', 'Consolas', monospace">Menlo / Consolas</option>
1273
+ <option value="'SF Mono', 'Fira Code', 'Cascadia Code', monospace">SF Mono / Fira Code</option>
1274
+ <option value="-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif">System Sans</option>
1275
+ <option value="Georgia, 'Times New Roman', serif">Georgia / Serif</option>
1276
+ <option value="'Helvetica Neue', Arial, sans-serif">Helvetica / Arial</option>
1277
+ </select>
1278
+ </div>
1279
+ <div class="toolbar-group" style="border-right:none;">
1280
+ <button class="toolbar-btn" onclick="zoomEditor(-1)" title="Zoom Out">&#8722;</button>
1281
+ <select id="zoom-select" onchange="setZoom(parseInt(this.value))" style="background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:4px;font-size:11px;padding:2px 4px;outline:none;cursor:pointer;min-width:60px;text-align:center;-webkit-appearance:none;appearance:none;background-image:url('data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%228%22 height=%225%22><path d=%22M0 0l4 5 4-5z%22 fill=%22%23565f89%22/></svg>');background-repeat:no-repeat;background-position:right 4px center;padding-right:14px;">
1282
+ <option value="50">50%</option>
1283
+ <option value="67">67%</option>
1284
+ <option value="75">75%</option>
1285
+ <option value="80">80%</option>
1286
+ <option value="90">90%</option>
1287
+ <option value="100" selected>100%</option>
1288
+ <option value="110">110%</option>
1289
+ <option value="125">125%</option>
1290
+ <option value="150">150%</option>
1291
+ <option value="175">175%</option>
1292
+ <option value="200">200%</option>
1293
+ <option value="250">250%</option>
1294
+ <option value="300">300%</option>
1295
+ </select>
1296
+ <button class="toolbar-btn" onclick="zoomEditor(1)" title="Zoom In">+</button>
1297
+ </div>
1298
+ </div>
1299
+ <div id="copilot-suggestion-bar" style="display:none;padding:6px 12px;background:var(--bg-lighter);border-bottom:1px solid var(--border);font-size:11px;color:var(--fg-dim);line-height:1.4;">
1300
+ <div style="display:flex;align-items:flex-start;gap:8px;">
1301
+ <span style="font-size:10px;padding:1px 5px;border-radius:4px;background:rgba(187,154,247,0.2);color:var(--purple);font-weight:600;flex-shrink:0;">Copilot</span>
1302
+ <div id="copilot-suggestion-text" style="flex:1;"></div>
1303
+ <button class="btn small" onclick="toggleCopilotPreview()" id="copilot-preview-btn" style="font-size:10px;">Preview</button>
1304
+ <button class="btn small" onclick="applyCopilotSuggestion()" id="copilot-apply-btn" style="display:none;font-size:10px;">Apply</button>
1305
+ <button class="btn small" onclick="dismissCopilotSuggestion()" style="font-size:10px;padding:2px 6px;">X</button>
1306
+ </div>
1307
+ <div id="copilot-preview" style="display:none;margin-top:8px;max-height:300px;overflow-y:auto;background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:8px;font-family:monospace;font-size:11px;white-space:pre-wrap;color:var(--fg);line-height:1.5;"></div>
1308
+ </div>
1309
+ <div class="editor-wrapper" id="editor-wrapper" style="display:none;">
1310
+ <div id="editor"></div>
1311
+ </div>
1312
+ <div class="empty-state" id="editor-empty">
1313
+ <h3>Prompt Editor</h3>
1314
+ <p>Create, edit, and manage your prompts with rich text formatting, image annotations, versioning, and prompt chains.</p>
1315
+ <p style="font-size:12px;margin-top:8px">
1316
+ <strong>Quick start:</strong> Click <strong>+ New Prompt</strong> or select an existing prompt from the sidebar.
1317
+ </p>
1318
+ <div style="display:grid;grid-template-columns:auto auto;gap:4px 16px;font-size:11px;margin-top:12px;">
1319
+ <span>Paste images</span><span style="color:var(--accent)">Ctrl+V in editor</span>
1320
+ <span>Add tags</span><span style="color:var(--accent)">Type + Enter in tag field</span>
1321
+ <span>Version history</span><span style="color:var(--accent)">History button</span>
1322
+ <span>Send to session</span><span style="color:var(--accent)">Send button</span>
1323
+ </div>
1324
+ </div>
1325
+ </div>
1326
+
1327
+ <!-- View: Chains -->
1328
+ <div id="view-chains" style="display:none;flex:1;flex-direction:column;overflow:hidden;">
1329
+ <div style="padding:10px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg);">
1330
+ <h3 style="font-size:14px;flex:1">Prompt Chains</h3>
1331
+ <button class="btn primary small" onclick="createNewChain()">+ New Chain</button>
1332
+ </div>
1333
+ <div id="chain-list" style="padding:10px;overflow-y:auto;flex:1;"></div>
1334
+ </div>
1335
+
1336
+
1337
+ <!-- View: Session Conversations DB -->
1338
+ <div id="view-conversations" style="display:none;flex:1;flex-direction:column;overflow:hidden;">
1339
+ <div style="padding:10px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg);">
1340
+ <h3 style="font-size:14px;flex:1">Session Conversations</h3>
1341
+ <input id="conv-search" type="text" placeholder="Search conversations..." style="width:200px;background:var(--bg-lighter);color:var(--fg);border:1px solid var(--border);padding:4px 8px;border-radius:4px;font-size:11px;outline:none;" oninput="loadConversations()">
1342
+ <button class="btn primary small" onclick="importConversations()">Import All Sessions</button>
1343
+ </div>
1344
+ <div id="conv-list" style="flex:1;overflow-y:auto;padding:10px;"></div>
1345
+ </div>
1346
+
1347
+ <!-- View: Templates -->
1348
+ <div id="view-templates" style="display:none;flex:1;flex-direction:column;overflow:hidden;">
1349
+ <div style="padding:10px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg);">
1350
+ <h3 style="font-size:14px;flex:1">Prompt Templates</h3>
1351
+ <button class="btn primary small" onclick="createNewTemplate()">+ New Template</button>
1352
+ </div>
1353
+ <div id="template-list" style="flex:1;overflow-y:auto;padding:10px;"></div>
1354
+ </div>
1355
+
1356
+ <!-- View: Backups -->
1357
+ <div id="view-backups" style="display:none;flex:1;flex-direction:column;overflow:hidden;">
1358
+ <div style="padding:10px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg);">
1359
+ <h3 style="font-size:14px;flex:1">Backup Manager</h3>
1360
+ <span id="backup-status" style="font-size:11px;color:var(--fg-dim)"></span>
1361
+ <button class="btn primary small" onclick="createManualBackup()">Backup Now</button>
1362
+ </div>
1363
+ <div style="padding:12px;font-size:12px;color:var(--fg-dim);border-bottom:1px solid var(--border);background:var(--bg-light);">
1364
+ Daily auto-backups are created automatically. Backups older than 30 days are cleaned up (minimum 5 kept). Restoring a backup creates a safety snapshot of the current state first.
1365
+ </div>
1366
+ <div id="backup-list" style="flex:1;overflow-y:auto;padding:10px;"></div>
1367
+ </div>
1368
+
1369
+ <!-- View: Patterns -->
1370
+ <div id="view-patterns" style="display:none;flex:1;flex-direction:column;overflow:hidden;">
1371
+ <div style="padding:10px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg);">
1372
+ <h3 style="font-size:14px;flex:1">Prompt Patterns</h3>
1373
+ <span id="pattern-count" style="font-size:11px;color:var(--fg-dim)"></span>
1374
+ <button class="btn primary small" onclick="detectPatterns()">Detect Patterns</button>
1375
+ </div>
1376
+ <div style="padding:8px;font-size:11px;color:var(--fg-dim);border-bottom:1px solid var(--border);background:var(--bg-light);">
1377
+ Patterns are prompts you've used repeatedly across sessions. Detecting patterns helps you identify reusable workflows.
1378
+ </div>
1379
+ <div id="pattern-list" style="flex:1;overflow-y:auto;padding:10px;"></div>
1380
+ </div>
1381
+ </div>
1382
+
1383
+ <!-- Copilot / Similar Prompts Panel (right side) -->
1384
+ <div id="copilot-panel">
1385
+ <div class="copilot-tabs">
1386
+ <div class="copilot-tab active" onclick="switchCopilotTab('similar')">Similar</div>
1387
+ <div class="copilot-tab" onclick="switchCopilotTab('copilot')">Copilot</div>
1388
+ <div class="copilot-tab" onclick="switchCopilotTab('sessions')">Sessions</div>
1389
+ <div class="copilot-tab" onclick="switchCopilotTab('frequent')">Frequent</div>
1390
+ <div style="margin-left:auto;padding:4px;">
1391
+ <button class="btn small" onclick="toggleCopilotPanel()" style="padding:2px 6px;">X</button>
1392
+ </div>
1393
+ </div>
1394
+ <div class="copilot-body" id="copilot-body">
1395
+ <div id="copilot-similar" class="copilot-tab-content">
1396
+ <div style="padding:12px;text-align:center;color:var(--fg-dim);font-size:12px;">Open a prompt to see similar prompts from your history.</div>
1397
+ </div>
1398
+ <div id="copilot-chat" class="copilot-tab-content" style="display:none;">
1399
+ <div class="copilot-chat">
1400
+ <div class="copilot-messages" id="copilot-messages">
1401
+ <div class="copilot-msg assistant">Hi! I'm Prompt Copilot. Ask me about your prompting history, patterns, or how to improve a prompt.</div>
1402
+ </div>
1403
+ <div class="copilot-input-row">
1404
+ <input id="copilot-input" type="text" placeholder="Ask about your prompts..." onkeydown="if(event.key==='Enter')sendCopilotMsg()">
1405
+ <button class="btn small primary" onclick="sendCopilotMsg()">Send</button>
1406
+ </div>
1407
+ </div>
1408
+ </div>
1409
+ <div id="copilot-sessions" class="copilot-tab-content" style="display:none;">
1410
+ <div style="padding:12px;text-align:center;color:var(--fg-dim);font-size:12px;">Open a prompt to see which sessions it was sent to.</div>
1411
+ </div>
1412
+ <div id="copilot-frequent" class="copilot-tab-content" style="display:none;">
1413
+ <div style="padding:12px;text-align:center;color:var(--fg-dim);font-size:12px;">Loading frequent questions...</div>
1414
+ </div>
1415
+ </div>
1416
+ </div>
1417
+
1418
+ <!-- Version history panel (right side) -->
1419
+ <div id="version-panel">
1420
+ <div class="version-header">
1421
+ <span>Version History</span>
1422
+ <button class="btn small" onclick="toggleVersions()">Close</button>
1423
+ </div>
1424
+ <div class="version-list" id="version-list"></div>
1425
+ </div>
1426
+
1427
+ <!-- Queue Builder panel (right side) -->
1428
+ <div id="queue-builder">
1429
+ <div class="version-header" style="display:flex;align-items:center;gap:8px;">
1430
+ <span style="flex:1;font-weight:600;">Queue Builder</span>
1431
+ <button class="btn small" onclick="toggleQueueBuilder()">Close</button>
1432
+ </div>
1433
+ <div style="padding:8px;border-bottom:1px solid var(--border);">
1434
+ <div style="display:flex;gap:6px;align-items:center;margin-bottom:6px;">
1435
+ <label style="font-size:11px;color:var(--fg-dim);white-space:nowrap;">Session:</label>
1436
+ <select id="queue-session-select" onchange="onQueueSessionChange()" style="flex:1;background:var(--bg);color:var(--fg);border:1px solid var(--border);padding:3px 6px;border-radius:4px;font-size:11px;"></select>
1437
+ </div>
1438
+ <div style="display:flex;gap:6px;align-items:center;">
1439
+ <label style="font-size:11px;color:var(--fg-dim);white-space:nowrap;">Mode:</label>
1440
+ <button class="btn small" id="queue-mode-btn" onclick="toggleQueueBuilderMode()" style="font-size:10px;">Manual</button>
1441
+ <label style="font-size:11px;color:var(--fg-dim);white-space:nowrap;margin-left:auto;">Timeout:</label>
1442
+ <input id="queue-idle-timeout" type="number" value="10" min="1" max="120" style="width:48px;background:var(--bg);color:var(--fg);border:1px solid var(--border);padding:2px 4px;border-radius:4px;font-size:11px;">
1443
+ <span style="font-size:10px;color:var(--fg-dim)">s</span>
1444
+ </div>
1445
+ </div>
1446
+ <div style="display:flex;align-items:center;padding:4px 8px;border-bottom:1px solid var(--border);">
1447
+ <span style="font-size:11px;color:var(--fg-dim);flex:1;" id="queue-builder-count">0 items</span>
1448
+ <button class="btn small" style="color:var(--red);font-size:10px;" onclick="clearQueueItems()">Clear All</button>
1449
+ </div>
1450
+ <div id="queue-builder-list" style="flex:1;overflow-y:auto;padding:6px;min-height:60px;">
1451
+ <div style="padding:16px;text-align:center;color:var(--fg-dim);font-size:11px;">
1452
+ Click +Q on prompts to add them here, or type inline prompts below.
1453
+ </div>
1454
+ </div>
1455
+ <div style="padding:8px;border-top:1px solid var(--border);">
1456
+ <div style="display:flex;gap:4px;margin-bottom:6px;position:relative;" class="autocomplete-wrap">
1457
+ <input id="queue-inline-input" type="text" placeholder="Add inline prompt..." style="flex:1;background:var(--bg);color:var(--fg);border:1px solid var(--border);padding:5px 8px;border-radius:4px;font-size:11px;outline:none;" onkeydown="onQueueInputKeydown(event)" oninput="onQueueInputChange()" autocomplete="off">
1458
+ <div class="autocomplete-dropdown" id="queue-ac-dropdown"></div>
1459
+ <button class="btn small" onclick="addInlineToQueue()">+ Add</button>
1460
+ </div>
1461
+ <button class="btn small primary" style="width:100%;" onclick="sendQueue()" id="queue-send-btn">Send Queue</button>
1462
+ </div>
1463
+ </div>
1464
+ </div>
1465
+
1466
+ <!-- Image Annotation Overlay -->
1467
+ <div class="annotation-overlay" id="annotation-overlay">
1468
+ <div class="annotation-toolbar" id="annotation-toolbar">
1469
+ <button class="tool-btn active" data-tool="arrow" onclick="setAnnotationTool('arrow')">Arrow</button>
1470
+ <button class="tool-btn" data-tool="rect" onclick="setAnnotationTool('rect')">Rectangle</button>
1471
+ <button class="tool-btn" data-tool="circle" onclick="setAnnotationTool('circle')">Circle</button>
1472
+ <button class="tool-btn" data-tool="text" onclick="setAnnotationTool('text')">Text</button>
1473
+ <button class="tool-btn" data-tool="freehand" onclick="setAnnotationTool('freehand')">Freehand</button>
1474
+ <button class="tool-btn" data-tool="blur" onclick="setAnnotationTool('blur')">Blur</button>
1475
+ <button class="tool-btn" data-tool="callout" onclick="setAnnotationTool('callout')">Callout</button>
1476
+ <input type="color" id="annotation-color" value="#f7768e" style="width:30px;height:28px;border:none;cursor:pointer;background:none;">
1477
+ <input type="range" id="annotation-size" min="1" max="10" value="3" style="width:80px;">
1478
+ <button class="btn small" onclick="undoAnnotation()">Undo</button>
1479
+ <button class="btn small primary" onclick="saveAnnotations()">Save</button>
1480
+ <button class="btn small danger" onclick="closeAnnotation()">Cancel</button>
1481
+ </div>
1482
+ <div class="annotation-canvas-container" id="annotation-container">
1483
+ <img id="annotation-image">
1484
+ <canvas id="annotation-canvas"></canvas>
1485
+ </div>
1486
+ </div>
1487
+
1488
+ <!-- Modals -->
1489
+ <!-- Harvest Modal -->
1490
+ <div class="harvest-modal" id="harvest-modal">
1491
+ <div class="harvest-content">
1492
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
1493
+ <h3 style="font-size:16px;flex:1;">Session Harvester</h3>
1494
+ <button class="btn small" onclick="closeHarvestModal()">Close</button>
1495
+ </div>
1496
+ <div id="harvest-stats" style="margin-bottom:12px;">
1497
+ <div style="text-align:center;color:var(--fg-dim);font-size:12px;">Loading harvest preview...</div>
1498
+ </div>
1499
+ <div style="display:flex;gap:8px;margin-bottom:12px;">
1500
+ <button class="btn primary" onclick="runHarvest('incremental')" id="harvest-inc-btn">Harvest New</button>
1501
+ <button class="btn" onclick="runHarvest('all')" id="harvest-all-btn">Full Re-harvest</button>
1502
+ <select id="harvest-days" style="background:var(--bg);color:var(--fg);border:1px solid var(--border);padding:4px 6px;border-radius:4px;font-size:11px;">
1503
+ <option value="">All time</option>
1504
+ <option value="7">Last 7 days</option>
1505
+ <option value="30" selected>Last 30 days</option>
1506
+ <option value="90">Last 90 days</option>
1507
+ </select>
1508
+ </div>
1509
+ <div id="harvest-result" style="display:none;padding:10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;font-size:12px;"></div>
1510
+ </div>
1511
+ </div>
1512
+
1513
+ <div class="modal-overlay hidden" id="folder-modal">
1514
+ <div class="modal" style="min-width:320px">
1515
+ <h3>New Folder</h3>
1516
+ <label>Name</label>
1517
+ <input id="folder-name" type="text" placeholder="Folder name...">
1518
+ <label>Color</label>
1519
+ <input id="folder-color" type="color" value="#7aa2f7" style="height:36px;">
1520
+ <div class="btn-row">
1521
+ <button class="btn" onclick="closeModal('folder-modal')">Cancel</button>
1522
+ <button class="btn primary" onclick="saveFolderModal()">Create</button>
1523
+ </div>
1524
+ </div>
1525
+ </div>
1526
+
1527
+ <div class="modal-overlay hidden" id="chain-modal">
1528
+ <div class="modal">
1529
+ <h3 id="chain-modal-title">New Chain</h3>
1530
+ <label>Name</label>
1531
+ <input id="chain-name" type="text" placeholder="Chain name...">
1532
+ <label>Description</label>
1533
+ <textarea id="chain-desc" placeholder="What does this chain do?"></textarea>
1534
+ <div class="btn-row">
1535
+ <button class="btn" onclick="closeModal('chain-modal')">Cancel</button>
1536
+ <button class="btn primary" onclick="saveChainModal()">Save</button>
1537
+ </div>
1538
+ </div>
1539
+ </div>
1540
+
1541
+
1542
+ <div class="modal-overlay hidden" id="template-modal">
1543
+ <div class="modal">
1544
+ <h3>New Template</h3>
1545
+ <label>Name</label>
1546
+ <input id="tpl-name" type="text" placeholder="Template name...">
1547
+ <label>Description</label>
1548
+ <input id="tpl-desc" type="text" placeholder="What is this template for?">
1549
+ <label>Category</label>
1550
+ <select id="tpl-category">
1551
+ <option value="general">General</option>
1552
+ <option value="coding">Coding</option>
1553
+ <option value="debugging">Debugging</option>
1554
+ <option value="design">Design</option>
1555
+ <option value="research">Research</option>
1556
+ </select>
1557
+ <label>Content (use {{variable}} for placeholders)</label>
1558
+ <textarea id="tpl-content" rows="8" placeholder="Write your template here...&#10;Use {{project_name}}, {{description}}, etc."></textarea>
1559
+ <label>Variables (comma-separated)</label>
1560
+ <input id="tpl-vars" type="text" placeholder="project_name, description, language">
1561
+ <div class="btn-row">
1562
+ <button class="btn" onclick="closeModal('template-modal')">Cancel</button>
1563
+ <button class="btn primary" onclick="saveTemplateModal()">Create</button>
1564
+ </div>
1565
+ </div>
1566
+ </div>
1567
+
1568
+ <div id="statusbar">
1569
+ <span class="status-item" id="status-prompt">No prompt selected</span>
1570
+ <span class="status-item" id="status-version"></span>
1571
+ <span class="status-item" id="status-words"></span>
1572
+ <span class="status-item" style="margin-left:auto" id="status-saved"></span>
1573
+ </div>
1574
+
1575
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"></script>
1576
+ <script>
1577
+ // --- Toast Notification System ---
1578
+ const TOAST_ICONS = {
1579
+ success: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M3 8.5L6.5 12L13 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1580
+ error: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
1581
+ warning: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 3V9M8 12V12.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
1582
+ info: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 5V5.5M8 8V12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
1583
+ };
1584
+
1585
+ function toast(message, opts = {}) {
1586
+ const type = opts.type || 'info';
1587
+ const title = opts.title || '';
1588
+ const duration = opts.duration || (type === 'error' ? 6000 : 3500);
1589
+ const container = document.getElementById('toast-container');
1590
+ if (!container) return;
1591
+
1592
+ const el = document.createElement('div');
1593
+ el.className = 'toast';
1594
+ el.style.position = 'relative';
1595
+ el.innerHTML = `
1596
+ <div class="toast-icon ${type}">${TOAST_ICONS[type]}</div>
1597
+ <div class="toast-body">
1598
+ ${title ? `<div class="toast-title">${title}</div>` : ''}
1599
+ <div class="toast-msg">${message}</div>
1600
+ </div>
1601
+ <button class="toast-close" onclick="this.closest('.toast').remove()">&times;</button>
1602
+ <div class="toast-progress ${type}" style="width:100%"></div>
1603
+ `;
1604
+ container.appendChild(el);
1605
+ requestAnimationFrame(() => {
1606
+ el.classList.add('show');
1607
+ const bar = el.querySelector('.toast-progress');
1608
+ if (bar) {
1609
+ bar.style.transitionDuration = duration + 'ms';
1610
+ requestAnimationFrame(() => { bar.style.width = '0%'; });
1611
+ }
1612
+ });
1613
+ setTimeout(() => {
1614
+ el.classList.remove('show');
1615
+ el.classList.add('hide');
1616
+ setTimeout(() => el.remove(), 400);
1617
+ }, duration);
1618
+ return el;
1619
+ }
1620
+
1621
+ // ============================================================
1622
+ // State
1623
+ // ============================================================
1624
+ function getCookie(name) {
1625
+ const m = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]+)'));
1626
+ return m ? m[1] : '';
1627
+ }
1628
+ const TOKEN = getCookie('ctm_token') || new URLSearchParams(location.search).get('token') || '';
1629
+ const API = (path) => `/api${path}${path.includes('?') ? '&' : '?'}token=${TOKEN}`;
1630
+
1631
+ const state = {
1632
+ currentPromptId: null,
1633
+ currentFolderId: null,
1634
+ prompts: [],
1635
+ folders: [],
1636
+ editor: null,
1637
+ tags: [],
1638
+ starred: false,
1639
+ starredFilter: false,
1640
+ saveTimeout: null,
1641
+ currentView: 'editor',
1642
+ versionPanelOpen: false,
1643
+ annotations: [],
1644
+ annotationTool: 'arrow',
1645
+ annotationImageId: null,
1646
+ // Session management
1647
+ ws: null,
1648
+ activeSessions: [], // [{id, label, cmd, cwd, pid, createdAt}]
1649
+ pendingPromptSend: null, // {sessionId, text} — waiting for session ready
1650
+ };
1651
+
1652
+ // ============================================================
1653
+ // Editor Setup (using contenteditable since Tiptap CDN is complex)
1654
+ // We use a simple contenteditable div with execCommand for rich text
1655
+ // ============================================================
1656
+ function cleanPastedHtml(html) {
1657
+ const doc = new DOMParser().parseFromString(html, 'text/html');
1658
+
1659
+ // Remove Google Docs wrapper elements, style tags, and internal guid spans
1660
+ doc.querySelectorAll('style, meta, script, link, title, google-sheets-html-origin').forEach(el => el.remove());
1661
+ // Unwrap Google Docs internal guid wrapper spans (keep their children)
1662
+ doc.querySelectorAll('span[id^="docs-internal-guid"]').forEach(el => el.replaceWith(...el.childNodes));
1663
+
1664
+ // Allowed tags — everything else gets unwrapped (keep content, drop tag)
1665
+ const ALLOWED = new Set([
1666
+ 'p', 'br', 'div',
1667
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
1668
+ 'strong', 'b', 'em', 'i', 'u', 's', 'strike', 'del',
1669
+ 'ul', 'ol', 'li',
1670
+ 'blockquote', 'pre', 'code',
1671
+ 'a', 'img', 'hr',
1672
+ 'table', 'thead', 'tbody', 'tr', 'th', 'td',
1673
+ 'sub', 'sup',
1674
+ ]);
1675
+
1676
+ // Allowed attributes per tag
1677
+ const ALLOWED_ATTRS = {
1678
+ 'a': ['href'],
1679
+ 'img': ['src', 'alt', 'width', 'height'],
1680
+ 'td': ['colspan', 'rowspan'],
1681
+ 'th': ['colspan', 'rowspan'],
1682
+ };
1683
+
1684
+ function cleanNode(node) {
1685
+ if (node.nodeType === 3) return; // text node, keep as-is
1686
+ if (node.nodeType !== 1) { node.remove(); return; } // non-element, non-text: remove
1687
+
1688
+ const tag = node.tagName.toLowerCase();
1689
+
1690
+ // Convert Google Docs styled spans to semantic tags
1691
+ if (tag === 'span') {
1692
+ const style = node.getAttribute('style') || '';
1693
+ let wrapper = null;
1694
+ if (/font-weight:\s*(bold|[7-9]\d\d)/.test(style)) {
1695
+ wrapper = doc.createElement('strong');
1696
+ }
1697
+ if (/font-style:\s*italic/.test(style)) {
1698
+ const em = doc.createElement('em');
1699
+ if (wrapper) { wrapper.appendChild(em); em.append(...node.childNodes); }
1700
+ else { wrapper = em; em.append(...node.childNodes); }
1701
+ }
1702
+ if (/text-decoration[^:]*:\s*[^;]*line-through/.test(style)) {
1703
+ const s = doc.createElement('s');
1704
+ if (wrapper) { const deepest = wrapper.querySelector('em') || wrapper; deepest.appendChild(s); s.append(...node.childNodes); }
1705
+ else { wrapper = s; s.append(...node.childNodes); }
1706
+ }
1707
+ if (wrapper) {
1708
+ if (!wrapper.querySelector('em') && !wrapper.querySelector('s')) wrapper.append(...node.childNodes);
1709
+ node.replaceWith(wrapper);
1710
+ Array.from(wrapper.querySelectorAll('*')).forEach(cleanNode);
1711
+ return;
1712
+ }
1713
+ // Plain span — unwrap
1714
+ node.replaceWith(...node.childNodes);
1715
+ return;
1716
+ }
1717
+
1718
+ if (!ALLOWED.has(tag)) {
1719
+ // Unwrap: keep children, remove the tag itself
1720
+ node.replaceWith(...node.childNodes);
1721
+ return;
1722
+ }
1723
+
1724
+ // Strip all attributes except allowed ones
1725
+ const allowedAttrs = ALLOWED_ATTRS[tag] || [];
1726
+ for (const attr of Array.from(node.attributes)) {
1727
+ if (!allowedAttrs.includes(attr.name)) {
1728
+ node.removeAttribute(attr.name);
1729
+ }
1730
+ }
1731
+
1732
+ // Recurse into children (snapshot the list since we may modify it)
1733
+ Array.from(node.childNodes).forEach(cleanNode);
1734
+ }
1735
+
1736
+ Array.from(doc.body.childNodes).forEach(cleanNode);
1737
+
1738
+ // Clean up Google Docs artifacts and excessive empty paragraphs
1739
+ let result = doc.body.innerHTML;
1740
+ // Remove paragraphs that only contain <b><br></b> (Google Docs spacer)
1741
+ result = result.replace(/<p[^>]*>\s*<b>\s*<br\s*\/?>\s*<\/b>\s*<\/p>/gi, '<p></p>');
1742
+ // Remove paragraphs that only contain <br> wrapped in any inline tag
1743
+ result = result.replace(/<p[^>]*>\s*<(?:b|strong|em|i|span)>\s*<br\s*\/?>\s*<\/(?:b|strong|em|i|span)>\s*<\/p>/gi, '<p></p>');
1744
+ result = result.replace(/<p>\s*<br\s*\/?>\s*<\/p>/gi, '<p></p>'); // <p><br></p> → <p></p>
1745
+ result = result.replace(/(<p>\s*<\/p>\s*){3,}/gi, '<p></p>'); // collapse 3+ empty paragraphs to 1
1746
+
1747
+ return result;
1748
+ }
1749
+
1750
+ function initEditor() {
1751
+ const editorDiv = document.getElementById('editor');
1752
+ editorDiv.contentEditable = true;
1753
+ editorDiv.setAttribute('data-placeholder', 'Start writing your prompt...');
1754
+ editorDiv.classList.add('ProseMirror');
1755
+ editorDiv.style.minHeight = '300px';
1756
+ editorDiv.style.outline = 'none';
1757
+
1758
+ editorDiv.addEventListener('input', () => {
1759
+ scheduleAutoSave();
1760
+ updateWordCount();
1761
+ triggerCopilotSuggestion();
1762
+ if (!editorDiv.textContent.trim()) {
1763
+ editorDiv.classList.add('is-editor-empty');
1764
+ } else {
1765
+ editorDiv.classList.remove('is-editor-empty');
1766
+ }
1767
+ });
1768
+
1769
+ // Paste handler: clean up rich HTML from Google Docs, Word, etc + image support
1770
+ editorDiv.addEventListener('paste', async (e) => {
1771
+ const items = e.clipboardData?.items;
1772
+ if (!items) return;
1773
+
1774
+ // Check for images first
1775
+ for (const item of items) {
1776
+ if (item.type.startsWith('image/')) {
1777
+ e.preventDefault();
1778
+ const blob = item.getAsFile();
1779
+ const filename = `paste-${Date.now()}.png`;
1780
+ await uploadAndInsertImage(blob, filename);
1781
+ return;
1782
+ }
1783
+ }
1784
+
1785
+ // Clean up rich HTML paste (Google Docs, Word, web pages)
1786
+ const html = e.clipboardData.getData('text/html');
1787
+ if (html) {
1788
+ e.preventDefault();
1789
+ const clean = cleanPastedHtml(html);
1790
+ document.execCommand('insertHTML', false, clean);
1791
+ scheduleAutoSave();
1792
+ }
1793
+ });
1794
+
1795
+ // Drop image support
1796
+ editorDiv.addEventListener('drop', async (e) => {
1797
+ const files = e.dataTransfer?.files;
1798
+ if (!files) return;
1799
+
1800
+ for (const file of files) {
1801
+ if (file.type.startsWith('image/')) {
1802
+ e.preventDefault();
1803
+ await uploadAndInsertImage(file, file.name);
1804
+ return;
1805
+ }
1806
+ }
1807
+ });
1808
+
1809
+ state.editor = editorDiv;
1810
+ }
1811
+
1812
+ function execCmd(cmd, value) {
1813
+ const editorDiv = document.getElementById('editor');
1814
+ editorDiv.focus();
1815
+
1816
+ if (cmd === 'undo') { document.execCommand('undo'); return; }
1817
+ if (cmd === 'redo') { document.execCommand('redo'); return; }
1818
+
1819
+ switch (cmd) {
1820
+ case 'toggleBold': document.execCommand('bold'); break;
1821
+ case 'toggleItalic': document.execCommand('italic'); break;
1822
+ case 'toggleStrike': document.execCommand('strikeThrough'); break;
1823
+ case 'toggleCode':
1824
+ const sel = window.getSelection();
1825
+ if (sel.rangeCount) {
1826
+ const range = sel.getRangeAt(0);
1827
+ const code = document.createElement('code');
1828
+ code.textContent = range.toString();
1829
+ range.deleteContents();
1830
+ range.insertNode(code);
1831
+ }
1832
+ break;
1833
+ case 'toggleHeading':
1834
+ document.execCommand('formatBlock', false, `h${value.level}`);
1835
+ break;
1836
+ case 'toggleBulletList':
1837
+ document.execCommand('insertUnorderedList');
1838
+ break;
1839
+ case 'toggleOrderedList':
1840
+ document.execCommand('insertOrderedList');
1841
+ break;
1842
+ case 'toggleBlockquote':
1843
+ document.execCommand('formatBlock', false, 'blockquote');
1844
+ break;
1845
+ case 'toggleCodeBlock':
1846
+ document.execCommand('formatBlock', false, 'pre');
1847
+ break;
1848
+ case 'setHorizontalRule':
1849
+ document.execCommand('insertHorizontalRule');
1850
+ break;
1851
+ }
1852
+ scheduleAutoSave();
1853
+ }
1854
+
1855
+ async function uploadAndInsertImage(blob, filename) {
1856
+ const promptId = state.currentPromptId || 0;
1857
+ try {
1858
+ const res = await fetch(API(`/images/upload?prompt_id=${promptId}&filename=${encodeURIComponent(filename)}`), {
1859
+ method: 'POST',
1860
+ headers: { 'Content-Type': blob.type },
1861
+ body: blob,
1862
+ });
1863
+ const data = await res.json();
1864
+ if (data.filename) {
1865
+ if (!state.imagePathCache) state.imagePathCache = {};
1866
+ state.imagePathCache[data.id] = { file_path: data.path, filename: data.filename };
1867
+ const img = document.createElement('img');
1868
+ img.src = API(`/images/file/${data.filename}`);
1869
+ img.dataset.imageId = data.id;
1870
+ setupImageHandlers(img);
1871
+ const sel = window.getSelection();
1872
+ if (sel.rangeCount) {
1873
+ const range = sel.getRangeAt(0);
1874
+ range.insertNode(img);
1875
+ range.collapse(false);
1876
+ } else {
1877
+ document.getElementById('editor').appendChild(img);
1878
+ }
1879
+ scheduleAutoSave();
1880
+ }
1881
+ } catch (e) {
1882
+ console.error('Upload failed:', e);
1883
+ }
1884
+ }
1885
+
1886
+ function insertImage() {
1887
+ const input = document.createElement('input');
1888
+ input.type = 'file';
1889
+ input.accept = 'image/*';
1890
+ input.onchange = async () => {
1891
+ if (input.files[0]) await uploadAndInsertImage(input.files[0], input.files[0].name);
1892
+ };
1893
+ input.click();
1894
+ }
1895
+
1896
+ async function takeScreenshot() {
1897
+ try {
1898
+ const res = await fetch(API('/screenshot'), { method: 'POST' });
1899
+ const data = await res.json();
1900
+ if (data.filename) {
1901
+ if (!state.imagePathCache) state.imagePathCache = {};
1902
+ state.imagePathCache[data.id] = { file_path: data.path, filename: data.filename };
1903
+ const img = document.createElement('img');
1904
+ img.src = API(`/images/file/${data.filename}`);
1905
+ img.dataset.imageId = data.id;
1906
+ setupImageHandlers(img);
1907
+ document.getElementById('editor').appendChild(img);
1908
+ scheduleAutoSave();
1909
+ }
1910
+ } catch (e) {
1911
+ toast('Screenshot failed: ' + e.message, { type: 'error' });
1912
+ }
1913
+ }
1914
+
1915
+ // ============================================================
1916
+ // Image Selection (single click = select, double click = edit)
1917
+ // ============================================================
1918
+ function selectEditorImage(img) {
1919
+ // Deselect previous
1920
+ document.querySelectorAll('#editor img.img-selected').forEach(el => el.classList.remove('img-selected'));
1921
+ img.classList.add('img-selected');
1922
+ }
1923
+
1924
+ function deselectAllImages() {
1925
+ document.querySelectorAll('#editor img.img-selected').forEach(el => el.classList.remove('img-selected'));
1926
+ }
1927
+
1928
+ function setupImageHandlers(img) {
1929
+ img.onclick = (e) => {
1930
+ e.preventDefault();
1931
+ e.stopPropagation();
1932
+ selectEditorImage(img);
1933
+ };
1934
+ img.ondblclick = (e) => {
1935
+ e.preventDefault();
1936
+ e.stopPropagation();
1937
+ const imageId = parseInt(img.dataset.imageId);
1938
+ if (imageId) openAnnotationEditor(imageId, img.src);
1939
+ };
1940
+ }
1941
+
1942
+ // Deselect images when clicking elsewhere in editor
1943
+ document.addEventListener('click', (e) => {
1944
+ if (!e.target.closest('#editor img') && !e.target.closest('.annotation-overlay')) {
1945
+ deselectAllImages();
1946
+ }
1947
+ });
1948
+
1949
+ // Delete selected image with Backspace/Delete
1950
+ document.addEventListener('keydown', (e) => {
1951
+ if (e.target.closest('#editor') && (e.key === 'Backspace' || e.key === 'Delete')) {
1952
+ const selected = document.querySelector('#editor img.img-selected');
1953
+ if (selected) {
1954
+ e.preventDefault();
1955
+ selected.remove();
1956
+ scheduleAutoSave();
1957
+ }
1958
+ }
1959
+ });
1960
+
1961
+ // ============================================================
1962
+ // Image Annotation
1963
+ // ============================================================
1964
+ let annotationHistory = [];
1965
+ let currentAnnotation = null;
1966
+
1967
+ function openAnnotationEditor(imageId, imageSrc) {
1968
+ state.annotationImageId = imageId;
1969
+ annotationHistory = [];
1970
+
1971
+ const overlay = document.getElementById('annotation-overlay');
1972
+ const img = document.getElementById('annotation-image');
1973
+ const canvas = document.getElementById('annotation-canvas');
1974
+
1975
+ img.onload = () => {
1976
+ canvas.width = img.naturalWidth;
1977
+ canvas.height = img.naturalHeight;
1978
+ canvas.style.width = img.width + 'px';
1979
+ canvas.style.height = img.height + 'px';
1980
+ redrawAnnotations();
1981
+ };
1982
+ img.src = imageSrc;
1983
+ overlay.classList.add('active');
1984
+ setupAnnotationCanvas();
1985
+ }
1986
+
1987
+ function closeAnnotation() {
1988
+ document.getElementById('annotation-overlay').classList.remove('active');
1989
+ }
1990
+
1991
+ function setAnnotationTool(tool) {
1992
+ state.annotationTool = tool;
1993
+ document.querySelectorAll('#annotation-toolbar .tool-btn').forEach(b => {
1994
+ b.classList.toggle('active', b.dataset.tool === tool);
1995
+ });
1996
+ }
1997
+
1998
+ function setupAnnotationCanvas() {
1999
+ const canvas = document.getElementById('annotation-canvas');
2000
+ const ctx = canvas.getContext('2d');
2001
+ let drawing = false;
2002
+ let startX, startY;
2003
+ let freehandPoints = [];
2004
+
2005
+ canvas.onmousedown = (e) => {
2006
+ const rect = canvas.getBoundingClientRect();
2007
+ const scaleX = canvas.width / rect.width;
2008
+ const scaleY = canvas.height / rect.height;
2009
+ startX = (e.clientX - rect.left) * scaleX;
2010
+ startY = (e.clientY - rect.top) * scaleY;
2011
+ drawing = true;
2012
+ freehandPoints = [{ x: startX, y: startY }];
2013
+ };
2014
+
2015
+ canvas.onmousemove = (e) => {
2016
+ if (!drawing) return;
2017
+ const rect = canvas.getBoundingClientRect();
2018
+ const scaleX = canvas.width / rect.width;
2019
+ const scaleY = canvas.height / rect.height;
2020
+ const x = (e.clientX - rect.left) * scaleX;
2021
+ const y = (e.clientY - rect.top) * scaleY;
2022
+
2023
+ if (state.annotationTool === 'freehand') {
2024
+ freehandPoints.push({ x, y });
2025
+ redrawAnnotations();
2026
+ drawFreehand(ctx, freehandPoints, getAnnotationColor(), getAnnotationSize());
2027
+ } else {
2028
+ // Preview
2029
+ redrawAnnotations();
2030
+ drawShape(ctx, state.annotationTool, startX, startY, x, y, getAnnotationColor(), getAnnotationSize());
2031
+ }
2032
+ };
2033
+
2034
+ canvas.onmouseup = (e) => {
2035
+ if (!drawing) return;
2036
+ drawing = false;
2037
+ const rect = canvas.getBoundingClientRect();
2038
+ const scaleX = canvas.width / rect.width;
2039
+ const scaleY = canvas.height / rect.height;
2040
+ const endX = (e.clientX - rect.left) * scaleX;
2041
+ const endY = (e.clientY - rect.top) * scaleY;
2042
+
2043
+ let annotation;
2044
+ if (state.annotationTool === 'freehand') {
2045
+ annotation = { type: 'freehand', points: freehandPoints, color: getAnnotationColor(), size: getAnnotationSize() };
2046
+ } else if (state.annotationTool === 'text') {
2047
+ const text = prompt('Enter text:');
2048
+ if (!text) return;
2049
+ annotation = { type: 'text', x: startX, y: startY, text, color: getAnnotationColor(), size: getAnnotationSize() * 5 };
2050
+ } else if (state.annotationTool === 'callout') {
2051
+ const num = annotationHistory.filter(a => a.type === 'callout').length + 1;
2052
+ annotation = { type: 'callout', x: startX, y: startY, num, color: getAnnotationColor(), size: 20 };
2053
+ } else {
2054
+ annotation = { type: state.annotationTool, x1: startX, y1: startY, x2: endX, y2: endY, color: getAnnotationColor(), size: getAnnotationSize() };
2055
+ }
2056
+
2057
+ annotationHistory.push(annotation);
2058
+ redrawAnnotations();
2059
+ };
2060
+ }
2061
+
2062
+ function getAnnotationColor() { return document.getElementById('annotation-color').value; }
2063
+ function getAnnotationSize() { return parseInt(document.getElementById('annotation-size').value); }
2064
+
2065
+ function redrawAnnotations() {
2066
+ const canvas = document.getElementById('annotation-canvas');
2067
+ const ctx = canvas.getContext('2d');
2068
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2069
+
2070
+ for (const ann of annotationHistory) {
2071
+ if (ann.type === 'freehand') {
2072
+ drawFreehand(ctx, ann.points, ann.color, ann.size);
2073
+ } else if (ann.type === 'text') {
2074
+ ctx.font = `${ann.size}px sans-serif`;
2075
+ ctx.fillStyle = ann.color;
2076
+ ctx.fillText(ann.text, ann.x, ann.y);
2077
+ } else if (ann.type === 'callout') {
2078
+ ctx.beginPath();
2079
+ ctx.arc(ann.x, ann.y, ann.size, 0, Math.PI * 2);
2080
+ ctx.fillStyle = ann.color;
2081
+ ctx.fill();
2082
+ ctx.fillStyle = '#fff';
2083
+ ctx.font = `bold ${ann.size}px sans-serif`;
2084
+ ctx.textAlign = 'center';
2085
+ ctx.textBaseline = 'middle';
2086
+ ctx.fillText(ann.num.toString(), ann.x, ann.y);
2087
+ ctx.textAlign = 'start';
2088
+ ctx.textBaseline = 'alphabetic';
2089
+ } else {
2090
+ drawShape(ctx, ann.type, ann.x1, ann.y1, ann.x2, ann.y2, ann.color, ann.size);
2091
+ }
2092
+ }
2093
+ }
2094
+
2095
+ function drawShape(ctx, type, x1, y1, x2, y2, color, size) {
2096
+ ctx.strokeStyle = color;
2097
+ ctx.lineWidth = size;
2098
+ ctx.fillStyle = color;
2099
+
2100
+ if (type === 'arrow') {
2101
+ ctx.beginPath();
2102
+ ctx.moveTo(x1, y1);
2103
+ ctx.lineTo(x2, y2);
2104
+ ctx.stroke();
2105
+ // Arrowhead
2106
+ const angle = Math.atan2(y2 - y1, x2 - x1);
2107
+ const headLen = size * 5;
2108
+ ctx.beginPath();
2109
+ ctx.moveTo(x2, y2);
2110
+ ctx.lineTo(x2 - headLen * Math.cos(angle - 0.4), y2 - headLen * Math.sin(angle - 0.4));
2111
+ ctx.lineTo(x2 - headLen * Math.cos(angle + 0.4), y2 - headLen * Math.sin(angle + 0.4));
2112
+ ctx.closePath();
2113
+ ctx.fill();
2114
+ } else if (type === 'rect') {
2115
+ ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
2116
+ } else if (type === 'circle') {
2117
+ const rx = Math.abs(x2 - x1) / 2;
2118
+ const ry = Math.abs(y2 - y1) / 2;
2119
+ const cx = (x1 + x2) / 2;
2120
+ const cy = (y1 + y2) / 2;
2121
+ ctx.beginPath();
2122
+ ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
2123
+ ctx.stroke();
2124
+ } else if (type === 'blur') {
2125
+ // Pixelate region
2126
+ const img = document.getElementById('annotation-image');
2127
+ const sx = Math.min(x1, x2), sy = Math.min(y1, y2);
2128
+ const sw = Math.abs(x2 - x1), sh = Math.abs(y2 - y1);
2129
+ if (sw > 0 && sh > 0) {
2130
+ ctx.save();
2131
+ ctx.filter = 'blur(8px)';
2132
+ ctx.drawImage(img, sx, sy, sw, sh, sx, sy, sw, sh);
2133
+ ctx.restore();
2134
+ }
2135
+ }
2136
+ }
2137
+
2138
+ function drawFreehand(ctx, points, color, size) {
2139
+ if (points.length < 2) return;
2140
+ ctx.strokeStyle = color;
2141
+ ctx.lineWidth = size;
2142
+ ctx.lineCap = 'round';
2143
+ ctx.lineJoin = 'round';
2144
+ ctx.beginPath();
2145
+ ctx.moveTo(points[0].x, points[0].y);
2146
+ for (let i = 1; i < points.length; i++) {
2147
+ ctx.lineTo(points[i].x, points[i].y);
2148
+ }
2149
+ ctx.stroke();
2150
+ }
2151
+
2152
+ function undoAnnotation() {
2153
+ annotationHistory.pop();
2154
+ redrawAnnotations();
2155
+ }
2156
+
2157
+ async function saveAnnotations() {
2158
+ if (!state.annotationImageId) return;
2159
+ try {
2160
+ await fetch(API(`/images/${state.annotationImageId}/annotations`), {
2161
+ method: 'PUT',
2162
+ headers: { 'Content-Type': 'application/json' },
2163
+ body: JSON.stringify({ annotations: annotationHistory }),
2164
+ });
2165
+ // Save canvas as new image with annotations burned in
2166
+ const canvas = document.getElementById('annotation-canvas');
2167
+ const img = document.getElementById('annotation-image');
2168
+ const mergedCanvas = document.createElement('canvas');
2169
+ mergedCanvas.width = canvas.width;
2170
+ mergedCanvas.height = canvas.height;
2171
+ const mctx = mergedCanvas.getContext('2d');
2172
+ mctx.drawImage(img, 0, 0);
2173
+ mctx.drawImage(canvas, 0, 0);
2174
+
2175
+ mergedCanvas.toBlob(async (blob) => {
2176
+ const res = await fetch(API(`/images/upload?prompt_id=${state.currentPromptId || 0}&filename=annotated-${Date.now()}.png`), {
2177
+ method: 'POST',
2178
+ headers: { 'Content-Type': 'image/png' },
2179
+ body: blob,
2180
+ });
2181
+ const data = await res.json();
2182
+ // Update the image in the editor
2183
+ const editorImgs = document.querySelectorAll('#editor img');
2184
+ for (const eimg of editorImgs) {
2185
+ if (eimg.dataset.imageId == state.annotationImageId) {
2186
+ eimg.src = API(`/images/file/${data.filename}`);
2187
+ eimg.dataset.imageId = data.id;
2188
+ break;
2189
+ }
2190
+ }
2191
+ scheduleAutoSave();
2192
+ closeAnnotation();
2193
+ }, 'image/png');
2194
+ } catch (e) {
2195
+ toast('Save annotations failed: ' + e.message, { type: 'error' });
2196
+ }
2197
+ }
2198
+
2199
+ // ============================================================
2200
+ // CRUD & Data
2201
+ // ============================================================
2202
+ async function loadFolders() {
2203
+ const res = await fetch(API('/folders'));
2204
+ state.folders = await res.json();
2205
+ renderFolderTree();
2206
+ }
2207
+
2208
+ async function loadPrompts() {
2209
+ const opts = [];
2210
+ if (state.currentFolderId) opts.push(`folder_id=${state.currentFolderId}`);
2211
+ const search = document.getElementById('prompt-search')?.value;
2212
+ if (search) opts.push(`search=${encodeURIComponent(search)}`);
2213
+ const ctxFilter = document.getElementById('context-filter')?.value;
2214
+ if (ctxFilter) opts.push(`context_type=${ctxFilter}`);
2215
+ const lcFilter = document.getElementById('lifecycle-filter')?.value;
2216
+ if (lcFilter) opts.push(`lifecycle_status=${lcFilter}`);
2217
+
2218
+ const res = await fetch(API(`/prompts${opts.length ? '?' + opts.join('&') : ''}`));
2219
+ state.prompts = await res.json();
2220
+ renderPromptList();
2221
+ }
2222
+
2223
+ function filterPrompts(special) {
2224
+ if (special === 'starred') {
2225
+ state.starredFilter = !state.starredFilter;
2226
+ const btn = document.getElementById('pe-star-filter-btn');
2227
+ if (btn) {
2228
+ btn.classList.toggle('active', state.starredFilter);
2229
+ btn.style.color = state.starredFilter ? 'var(--yellow)' : '';
2230
+ }
2231
+ if (state.starredFilter) {
2232
+ state.currentFolderId = null;
2233
+ fetch(API('/prompts?starred=1')).then(r => r.json()).then(prompts => {
2234
+ state.prompts = prompts;
2235
+ renderFolderTree();
2236
+ renderPromptList();
2237
+ });
2238
+ } else {
2239
+ loadPrompts();
2240
+ }
2241
+ return;
2242
+ }
2243
+ loadPrompts();
2244
+ }
2245
+
2246
+ // ============================================================
2247
+ // AI Search
2248
+ // ============================================================
2249
+ let aiSearchMode = false;
2250
+ let aiSearchTimeout = null;
2251
+
2252
+ function toggleAiSearch() {
2253
+ aiSearchMode = !aiSearchMode;
2254
+ const btn = document.getElementById('ai-search-btn');
2255
+ const input = document.getElementById('prompt-search');
2256
+ btn.classList.toggle('active', aiSearchMode);
2257
+ input.classList.toggle('ai-active', aiSearchMode);
2258
+ input.placeholder = aiSearchMode ? 'AI search... (press Enter)' : 'Search prompts...';
2259
+ if (!aiSearchMode) {
2260
+ // Switch back to normal search
2261
+ loadPrompts();
2262
+ } else {
2263
+ input.focus();
2264
+ }
2265
+ }
2266
+
2267
+ async function runAiSearch() {
2268
+ const query = document.getElementById('prompt-search').value.trim();
2269
+ if (!query) {
2270
+ loadPrompts();
2271
+ return;
2272
+ }
2273
+
2274
+ const list = document.getElementById('prompt-list');
2275
+ list.innerHTML = '<div class="ai-searching"><span class="spinner"></span>Searching with AI...</div>';
2276
+
2277
+ try {
2278
+ const res = await fetch(API('/prompts/ai-search'), {
2279
+ method: 'POST',
2280
+ headers: { 'Content-Type': 'application/json' },
2281
+ body: JSON.stringify({ query }),
2282
+ });
2283
+ const data = await res.json();
2284
+
2285
+ if (data.error) {
2286
+ list.innerHTML = `<div style="padding:12px;font-size:12px;color:var(--red);text-align:center">${escHtml(data.error)}</div>`;
2287
+ return;
2288
+ }
2289
+
2290
+ if (!data.results || !data.results.length) {
2291
+ list.innerHTML = '<div style="padding:12px;font-size:12px;color:var(--fg-dim);text-align:center">No matching prompts found.</div>';
2292
+ return;
2293
+ }
2294
+
2295
+ renderAiSearchResults(data.results);
2296
+ } catch (e) {
2297
+ list.innerHTML = `<div style="padding:12px;font-size:12px;color:var(--red);text-align:center">Search failed: ${escHtml(e.message)}</div>`;
2298
+ }
2299
+ }
2300
+
2301
+ function renderAiSearchResults(results) {
2302
+ const list = document.getElementById('prompt-list');
2303
+ list.innerHTML = results.map(r => {
2304
+ let tags = [];
2305
+ try { const parsed = JSON.parse(r.tags || '[]'); tags = Array.isArray(parsed) ? parsed : String(parsed).split(',').filter(Boolean); } catch { tags = (r.tags || '').split(',').filter(Boolean); }
2306
+ const isActive = state.currentPromptId === r.id;
2307
+ const isPinned = !!r.pinned;
2308
+ return `<div class="prompt-item ${isActive ? 'active' : ''} ${r.starred ? 'starred' : ''} ${isPinned ? 'pinned' : ''}"
2309
+ data-id="${r.id}" onclick="openPrompt(${r.id})">
2310
+ <div class="prompt-content">
2311
+ <div class="prompt-title">${isPinned ? '<span class="pin-badge">&#x1F4CC;</span>' : ''}${escHtml(r.title || 'Untitled')}</div>
2312
+ <div class="prompt-meta">
2313
+ <span>${r.context_type}</span>
2314
+ ${tags.slice(0, 2).map(t => `<span class="tag">${escHtml(t)}</span>`).join('')}
2315
+ </div>
2316
+ <div class="ai-result-reason">${escHtml(r.reason || '')}</div>
2317
+ </div>
2318
+ <span class="ai-result-score">${r.relevance}%</span>
2319
+ </div>`;
2320
+ }).join('');
2321
+ }
2322
+
2323
+ function renderFolderTree() {
2324
+ const tree = document.getElementById('folder-tree');
2325
+ const folders = state.folders;
2326
+
2327
+ let html = `<div class="folder-item ${!state.currentFolderId ? 'active' : ''}" onclick="selectFolder(null)">
2328
+ <span class="folder-icon">&#128193;</span>
2329
+ <span>All Prompts</span>
2330
+ </div>`;
2331
+
2332
+ for (const f of folders) {
2333
+ html += `<div class="folder-item ${state.currentFolderId === f.id ? 'active' : ''}"
2334
+ data-folder-id="${f.id}" draggable="true"
2335
+ ondragstart="onFolderDragStart(event, ${f.id})"
2336
+ ondragover="onFolderDragOver(event)"
2337
+ ondragleave="onFolderDragLeave(event)"
2338
+ ondrop="onFolderDrop(event, ${f.id})"
2339
+ onclick="selectFolder(${f.id})">
2340
+ <span class="drag-handle" title="Drag to reorder">&#8942;</span>
2341
+ <span class="folder-icon" style="color:${f.color || '#7aa2f7'}">&#128193;</span>
2342
+ <span class="folder-name">${escHtml(f.name)}</span>
2343
+ <div class="folder-actions">
2344
+ <button class="act-btn" onclick="event.stopPropagation();startRenameFolder(${f.id})" title="Rename">&#9998;</button>
2345
+ <button class="act-btn" onclick="event.stopPropagation();deleteFolderItem(${f.id})" title="Delete" style="color:var(--red)">&#x2715;</button>
2346
+ </div>
2347
+ </div>`;
2348
+ }
2349
+
2350
+ tree.innerHTML = html;
2351
+ }
2352
+
2353
+ // --- Folder actions ---
2354
+ function startRenameFolder(id) {
2355
+ const el = document.querySelector(`.folder-item[data-folder-id="${id}"] .folder-name`);
2356
+ if (!el) return;
2357
+ const currentName = el.textContent;
2358
+ const input = document.createElement('input');
2359
+ input.type = 'text';
2360
+ input.className = 'folder-rename-input';
2361
+ input.value = currentName;
2362
+ el.replaceWith(input);
2363
+ input.focus();
2364
+ input.select();
2365
+
2366
+ const save = async () => {
2367
+ const name = input.value.trim();
2368
+ if (name && name !== currentName) {
2369
+ await fetch(API(`/folders/${id}`), {
2370
+ method: 'PUT',
2371
+ headers: { 'Content-Type': 'application/json' },
2372
+ body: JSON.stringify({ name }),
2373
+ });
2374
+ }
2375
+ await loadFolders();
2376
+ };
2377
+
2378
+ input.addEventListener('blur', save);
2379
+ input.addEventListener('keydown', (e) => {
2380
+ if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
2381
+ if (e.key === 'Escape') { input.value = currentName; input.blur(); }
2382
+ });
2383
+ }
2384
+
2385
+ async function deleteFolderItem(id) {
2386
+ if (!confirm('Delete this folder? Prompts inside will be moved to All Prompts.')) return;
2387
+ await fetch(API(`/folders/${id}`), { method: 'DELETE' });
2388
+ if (state.currentFolderId === id) state.currentFolderId = null;
2389
+ await loadFolders();
2390
+ await loadPrompts();
2391
+ }
2392
+
2393
+ // --- Folder drag-and-drop reorder ---
2394
+ let dragFolderId = null;
2395
+
2396
+ function onFolderDragStart(e, id) {
2397
+ dragFolderId = id;
2398
+ e.dataTransfer.effectAllowed = 'move';
2399
+ e.dataTransfer.setData('text/plain', id);
2400
+ }
2401
+
2402
+ function onFolderDragOver(e) {
2403
+ e.preventDefault();
2404
+ const item = e.target.closest('.folder-item[data-folder-id]');
2405
+ if (item) item.classList.add('drag-over');
2406
+ }
2407
+
2408
+ function onFolderDragLeave(e) {
2409
+ const item = e.target.closest('.folder-item[data-folder-id]');
2410
+ if (item) item.classList.remove('drag-over');
2411
+ }
2412
+
2413
+ async function onFolderDrop(e, targetId) {
2414
+ e.preventDefault();
2415
+ const item = e.target.closest('.folder-item[data-folder-id]');
2416
+ if (item) item.classList.remove('drag-over');
2417
+ if (dragFolderId === null || dragFolderId === targetId) return;
2418
+
2419
+ const ids = state.folders.map(f => f.id);
2420
+ const fromIdx = ids.indexOf(dragFolderId);
2421
+ const toIdx = ids.indexOf(targetId);
2422
+ if (fromIdx === -1 || toIdx === -1) return;
2423
+
2424
+ ids.splice(fromIdx, 1);
2425
+ ids.splice(toIdx, 0, dragFolderId);
2426
+
2427
+ await fetch(API('/folders/reorder'), {
2428
+ method: 'POST',
2429
+ headers: { 'Content-Type': 'application/json' },
2430
+ body: JSON.stringify({ ids }),
2431
+ });
2432
+ await loadFolders();
2433
+ dragFolderId = null;
2434
+ }
2435
+
2436
+ function renderPromptList() {
2437
+ const list = document.getElementById('prompt-list');
2438
+ if (state.prompts.length === 0) {
2439
+ list.innerHTML = '<div style="padding:12px;font-size:12px;color:var(--fg-dim);text-align:center">No prompts yet. Click + New Prompt to get started.</div>';
2440
+ return;
2441
+ }
2442
+
2443
+ list.innerHTML = state.prompts.map(p => {
2444
+ let tags = [];
2445
+ try { const parsed = JSON.parse(p.tags || '[]'); tags = Array.isArray(parsed) ? parsed : String(parsed).split(',').filter(Boolean); } catch { tags = (p.tags || '').split(',').filter(Boolean); }
2446
+ const isActive = state.currentPromptId === p.id;
2447
+ const isPinned = !!p.pinned;
2448
+ return `<div class="prompt-item ${isActive ? 'active' : ''} ${p.starred ? 'starred' : ''} ${isPinned ? 'pinned' : ''}"
2449
+ data-id="${p.id}" draggable="true"
2450
+ ondragstart="onPromptDragStart(event, ${p.id})"
2451
+ ondragover="onPromptDragOver(event)"
2452
+ ondragleave="onPromptDragLeave(event)"
2453
+ ondrop="onPromptDrop(event, ${p.id})"
2454
+ onclick="openPrompt(${p.id})">
2455
+ <span class="drag-handle" title="Drag to reorder">&#8942;&#8942;</span>
2456
+ <div class="prompt-content">
2457
+ <div class="prompt-title">${isPinned ? '<span class="pin-badge" title="Pinned">&#x1F4CC;</span>' : ''}${escHtml(p.title || 'Untitled')}</div>
2458
+ <div class="prompt-meta">
2459
+ <span>${p.context_type}</span>
2460
+ ${p.lifecycle_status && p.lifecycle_status !== 'draft' ? `<span class="lifecycle-badge ${p.lifecycle_status}">${p.lifecycle_status}</span>` : ''}
2461
+ ${tags.slice(0, 2).map(t => `<span class="tag">${escHtml(t)}</span>`).join('')}
2462
+ <span>${timeAgo(p.updated_at)}</span>
2463
+ </div>
2464
+ </div>
2465
+ <div class="prompt-actions">
2466
+ <button class="act-btn" onclick="event.stopPropagation();addPromptToQueue(${p.id})" title="Add to Queue">+Q</button>
2467
+ <button class="act-btn ${isPinned ? 'pin-active' : ''}" onclick="event.stopPropagation();togglePin(${p.id},${isPinned?0:1})" title="${isPinned ? 'Unpin' : 'Pin to top'}">&#x1F4CC;</button>
2468
+ <button class="act-btn" onclick="event.stopPropagation();duplicatePrompt(${p.id})" title="Duplicate">&#x2398;</button>
2469
+ <button class="act-btn" onclick="event.stopPropagation();deletePromptItem(${p.id})" title="Delete" style="color:var(--red)">&#x2715;</button>
2470
+ </div>
2471
+ </div>`;
2472
+ }).join('');
2473
+ }
2474
+
2475
+ // --- Prompt list actions ---
2476
+ async function togglePin(id, pinValue) {
2477
+ await fetch(API(`/prompts/${id}`), {
2478
+ method: 'PUT',
2479
+ headers: { 'Content-Type': 'application/json' },
2480
+ body: JSON.stringify({ pinned: pinValue }),
2481
+ });
2482
+ await loadPrompts();
2483
+ }
2484
+
2485
+ async function duplicatePrompt(id) {
2486
+ const res = await fetch(API(`/prompts/${id}/duplicate`), { method: 'POST' });
2487
+ const data = await res.json();
2488
+ if (data.id) {
2489
+ await loadPrompts();
2490
+ openPrompt(data.id);
2491
+ }
2492
+ }
2493
+
2494
+ async function deletePromptItem(id) {
2495
+ if (!confirm('Delete this prompt? This cannot be undone.')) return;
2496
+ await fetch(API(`/prompts/${id}`), { method: 'DELETE' });
2497
+ if (state.currentPromptId === id) {
2498
+ state.currentPromptId = null;
2499
+ document.getElementById('meta-bar').style.display = 'none';
2500
+ document.getElementById('editor-toolbar').style.display = 'none';
2501
+ document.getElementById('editor-wrapper').style.display = 'none';
2502
+ document.getElementById('editor-empty').style.display = '';
2503
+ }
2504
+ await loadPrompts();
2505
+ }
2506
+
2507
+ // --- Prompt drag-and-drop reorder ---
2508
+ let dragPromptId = null;
2509
+
2510
+ function onPromptDragStart(e, id) {
2511
+ dragPromptId = id;
2512
+ e.dataTransfer.effectAllowed = 'move';
2513
+ e.dataTransfer.setData('text/plain', id);
2514
+ e.target.style.opacity = '0.5';
2515
+ setTimeout(() => { e.target.style.opacity = ''; }, 0);
2516
+ }
2517
+
2518
+ function onPromptDragOver(e) {
2519
+ e.preventDefault();
2520
+ e.dataTransfer.dropEffect = 'move';
2521
+ const item = e.target.closest('.prompt-item');
2522
+ if (item) item.classList.add('drag-over');
2523
+ }
2524
+
2525
+ function onPromptDragLeave(e) {
2526
+ const item = e.target.closest('.prompt-item');
2527
+ if (item) item.classList.remove('drag-over');
2528
+ }
2529
+
2530
+ async function onPromptDrop(e, targetId) {
2531
+ e.preventDefault();
2532
+ const item = e.target.closest('.prompt-item');
2533
+ if (item) item.classList.remove('drag-over');
2534
+ if (dragPromptId === null || dragPromptId === targetId) return;
2535
+
2536
+ // Reorder in local state
2537
+ const ids = state.prompts.map(p => p.id);
2538
+ const fromIdx = ids.indexOf(dragPromptId);
2539
+ const toIdx = ids.indexOf(targetId);
2540
+ if (fromIdx === -1 || toIdx === -1) return;
2541
+
2542
+ ids.splice(fromIdx, 1);
2543
+ ids.splice(toIdx, 0, dragPromptId);
2544
+
2545
+ // Save to server
2546
+ await fetch(API('/prompts/reorder'), {
2547
+ method: 'POST',
2548
+ headers: { 'Content-Type': 'application/json' },
2549
+ body: JSON.stringify({ ids }),
2550
+ });
2551
+ await loadPrompts();
2552
+ dragPromptId = null;
2553
+ }
2554
+
2555
+ function selectFolder(id) {
2556
+ state.currentFolderId = id;
2557
+ renderFolderTree();
2558
+ loadPrompts();
2559
+ updateUrlHash({ folder: id });
2560
+ }
2561
+
2562
+ async function openPrompt(id) {
2563
+ // Save any pending changes to the current prompt before switching
2564
+ if (state.currentPromptId && state.saveTimeout) {
2565
+ clearTimeout(state.saveTimeout);
2566
+ state.saveTimeout = null;
2567
+ await saveCurrentPrompt();
2568
+ }
2569
+
2570
+ const res = await fetch(API(`/prompts/${id}`));
2571
+ const prompt = await res.json();
2572
+ state.currentPromptId = id;
2573
+ try { const parsed = JSON.parse(prompt.tags || '[]'); state.tags = Array.isArray(parsed) ? parsed : String(parsed).split(',').filter(Boolean); } catch { state.tags = (prompt.tags || '').split(',').filter(Boolean); }
2574
+ state.starred = !!prompt.starred;
2575
+
2576
+ // Show editor UI
2577
+ document.getElementById('meta-bar').style.display = 'flex';
2578
+ document.getElementById('editor-toolbar').style.display = 'flex';
2579
+ document.getElementById('editor-wrapper').style.display = 'block';
2580
+ document.getElementById('editor-empty').style.display = 'none';
2581
+ showView('editor', true); // skip hash — we set it ourselves
2582
+
2583
+ // Update URL
2584
+ updateUrlHash({ prompt: id, folder: state.currentFolderId });
2585
+
2586
+ // Populate
2587
+ document.getElementById('prompt-title-input').value = prompt.title || '';
2588
+ document.getElementById('prompt-context-type').value = prompt.context_type || 'general';
2589
+ let html = prompt.content_html || prompt.content || '';
2590
+ // Clean legacy Google Docs artifacts from previously saved content
2591
+ if (html.includes('docs-internal-guid') || html.includes('margin-top:0pt') || /<p[^>]*dir="ltr"/.test(html)) {
2592
+ html = cleanPastedHtml(html);
2593
+ }
2594
+ if (typeof DOMPurify !== 'undefined') html = DOMPurify.sanitize(html);
2595
+ document.getElementById('editor').innerHTML = html; // sanitized by DOMPurify above
2596
+ document.getElementById('star-btn').innerHTML = state.starred ? '&#9733;' : '&#9734;';
2597
+ document.getElementById('star-btn').style.color = state.starred ? 'var(--yellow)' : '';
2598
+
2599
+ // Set lifecycle status
2600
+ const lcSel = document.getElementById('lifecycle-status');
2601
+ if (lcSel) lcSel.value = prompt.lifecycle_status || 'draft';
2602
+
2603
+ renderTags();
2604
+ updateWordCount();
2605
+ updateStatusBar();
2606
+ updateUsageBadge();
2607
+ renderPromptList();
2608
+
2609
+ // Update copilot panel if open
2610
+ if (document.getElementById('copilot-panel').classList.contains('active')) {
2611
+ loadSimilarPrompts();
2612
+ loadPromptSessions();
2613
+ }
2614
+
2615
+ // Cache image file paths for markdown export
2616
+ state.imagePathCache = {};
2617
+ if (prompt.images) {
2618
+ prompt.images.forEach(img => {
2619
+ state.imagePathCache[img.id] = { file_path: img.file_path, filename: img.filename };
2620
+ });
2621
+ }
2622
+
2623
+ // Setup image selection/annotation handlers
2624
+ document.querySelectorAll('#editor img').forEach(img => {
2625
+ if (img.dataset.imageId) setupImageHandlers(img);
2626
+ });
2627
+ }
2628
+
2629
+ async function createNewPrompt() {
2630
+ const res = await fetch(API('/prompts'), {
2631
+ method: 'POST',
2632
+ headers: { 'Content-Type': 'application/json' },
2633
+ body: JSON.stringify({
2634
+ title: 'New Prompt',
2635
+ content: '',
2636
+ content_html: '',
2637
+ folder_id: state.currentFolderId,
2638
+ context_type: 'general',
2639
+ }),
2640
+ });
2641
+ const data = await res.json();
2642
+ if (data.id) {
2643
+ await loadPrompts();
2644
+ openPrompt(data.id);
2645
+ }
2646
+ }
2647
+
2648
+ async function saveCurrentPrompt() {
2649
+ if (!state.currentPromptId) return;
2650
+ const editorDiv = document.getElementById('editor');
2651
+ const data = {
2652
+ title: document.getElementById('prompt-title-input').value,
2653
+ content: editorDiv.innerText,
2654
+ content_html: editorDiv.innerHTML,
2655
+ context_type: document.getElementById('prompt-context-type').value,
2656
+ tags: state.tags,
2657
+ starred: state.starred,
2658
+ };
2659
+
2660
+ await fetch(API(`/prompts/${state.currentPromptId}`), {
2661
+ method: 'PUT',
2662
+ headers: { 'Content-Type': 'application/json' },
2663
+ body: JSON.stringify(data),
2664
+ });
2665
+
2666
+ document.getElementById('status-saved').textContent = `Saved ${new Date().toLocaleTimeString()}`;
2667
+ }
2668
+
2669
+ function scheduleAutoSave() {
2670
+ if (state.saveTimeout) clearTimeout(state.saveTimeout);
2671
+ state.saveTimeout = setTimeout(() => saveCurrentPrompt(), 1500);
2672
+ document.getElementById('status-saved').textContent = 'Unsaved...';
2673
+ }
2674
+
2675
+ function onTitleChange() {
2676
+ // Sync title to sidebar in real-time
2677
+ const title = document.getElementById('prompt-title-input').value;
2678
+ if (state.currentPromptId) {
2679
+ const p = state.prompts.find(p => p.id === state.currentPromptId);
2680
+ if (p) p.title = title;
2681
+ const el = document.querySelector(`.prompt-item[data-id="${state.currentPromptId}"] .prompt-title`);
2682
+ if (el) el.textContent = title || 'Untitled';
2683
+ }
2684
+ scheduleAutoSave();
2685
+ }
2686
+ function onContextTypeChange() { scheduleAutoSave(); }
2687
+
2688
+ function renderTags() {
2689
+ const container = document.getElementById('tags-input');
2690
+ const input = document.getElementById('tag-input');
2691
+ container.innerHTML = '';
2692
+ for (const tag of state.tags) {
2693
+ const chip = document.createElement('span');
2694
+ chip.className = 'tag-chip';
2695
+ chip.innerHTML = `${escHtml(tag)} <span class="tag-remove" onclick="removeTag('${escHtml(tag)}')">&times;</span>`;
2696
+ container.appendChild(chip);
2697
+ }
2698
+ container.appendChild(input);
2699
+ }
2700
+
2701
+ function onTagKeydown(e) {
2702
+ if (e.key === 'Enter') {
2703
+ const val = e.target.value.trim();
2704
+ if (val && !state.tags.includes(val)) {
2705
+ state.tags.push(val);
2706
+ e.target.value = '';
2707
+ renderTags();
2708
+ scheduleAutoSave();
2709
+ }
2710
+ }
2711
+ }
2712
+
2713
+ function removeTag(tag) {
2714
+ state.tags = state.tags.filter(t => t !== tag);
2715
+ renderTags();
2716
+ scheduleAutoSave();
2717
+ }
2718
+
2719
+ function toggleStar() {
2720
+ state.starred = !state.starred;
2721
+ document.getElementById('star-btn').innerHTML = state.starred ? '&#9733;' : '&#9734;';
2722
+ document.getElementById('star-btn').style.color = state.starred ? 'var(--yellow)' : '';
2723
+ scheduleAutoSave();
2724
+ }
2725
+
2726
+ // ============================================================
2727
+ // Version History
2728
+ // ============================================================
2729
+ async function toggleVersions() {
2730
+ state.versionPanelOpen = !state.versionPanelOpen;
2731
+ document.getElementById('version-panel').classList.toggle('active', state.versionPanelOpen);
2732
+ if (state.versionPanelOpen && state.currentPromptId) {
2733
+ const res = await fetch(API(`/prompts/${state.currentPromptId}/versions`));
2734
+ const versions = await res.json();
2735
+ renderVersions(versions);
2736
+ }
2737
+ }
2738
+
2739
+ function renderVersions(versions) {
2740
+ const list = document.getElementById('version-list');
2741
+ if (!versions.length) {
2742
+ list.innerHTML = '<div style="padding:12px;font-size:12px;color:var(--fg-dim)">No versions yet.</div>';
2743
+ return;
2744
+ }
2745
+ list.innerHTML = versions.map(v => `<div class="version-item" onclick="previewVersion(${v.id})">
2746
+ <span class="v-num">v${v.version}</span>
2747
+ <span class="v-date">${new Date(v.created_at).toLocaleString()}</span>
2748
+ <div class="v-msg">${escHtml(v.message || '')}</div>
2749
+ <button class="btn small" style="margin-top:4px" onclick="event.stopPropagation();restoreVersion(${v.id})">Restore</button>
2750
+ </div>`).join('');
2751
+ }
2752
+
2753
+ async function restoreVersion(versionId) {
2754
+ if (!confirm('Restore this version? Current changes will be saved as a new version.')) return;
2755
+ await fetch(API(`/prompts/${state.currentPromptId}/restore`), {
2756
+ method: 'POST',
2757
+ headers: { 'Content-Type': 'application/json' },
2758
+ body: JSON.stringify({ version_id: versionId }),
2759
+ });
2760
+ openPrompt(state.currentPromptId);
2761
+ toggleVersions(); // refresh version list
2762
+ toggleVersions();
2763
+ }
2764
+
2765
+ // ============================================================
2766
+ // Send to Session / Copy
2767
+ // ============================================================
2768
+ // ============================================================
2769
+ // Send Dropdown & Session Management
2770
+ // ============================================================
2771
+ function getPromptPlainText() {
2772
+ const editor = document.getElementById('editor');
2773
+ if (!editor) return '';
2774
+ // Clone the editor and replace images with markdown image references
2775
+ const clone = editor.cloneNode(true);
2776
+ clone.querySelectorAll('img').forEach(img => {
2777
+ const imageId = img.dataset.imageId;
2778
+ const src = img.getAttribute('src') || '';
2779
+ // Use cached file_path from state if available
2780
+ const cached = state.imagePathCache?.[imageId];
2781
+ if (cached) {
2782
+ const placeholder = document.createTextNode(`![${cached.filename || 'image'}](${cached.file_path})`);
2783
+ img.parentNode.replaceChild(placeholder, img);
2784
+ } else {
2785
+ const match = src.match(/\/api\/images\/file\/(.+?)(?:\?|$)/);
2786
+ const filename = match ? match[1] : src;
2787
+ const placeholder = document.createTextNode(`![image](${filename})`);
2788
+ img.parentNode.replaceChild(placeholder, img);
2789
+ }
2790
+ });
2791
+ return clone.innerText;
2792
+ }
2793
+
2794
+ function toggleSendDropdown(e) {
2795
+ e && e.stopPropagation();
2796
+ const dd = document.getElementById('send-dropdown');
2797
+ dd.classList.toggle('open');
2798
+ if (dd.classList.contains('open')) {
2799
+ refreshActiveSessionsList();
2800
+ document.addEventListener('click', closeSendDropdownOutside, true);
2801
+ } else {
2802
+ document.removeEventListener('click', closeSendDropdownOutside, true);
2803
+ }
2804
+ }
2805
+
2806
+ function closeSendDropdownOutside(e) {
2807
+ const wrap = document.getElementById('send-dropdown-wrap');
2808
+ if (!wrap.contains(e.target)) {
2809
+ document.getElementById('send-dropdown').classList.remove('open');
2810
+ document.removeEventListener('click', closeSendDropdownOutside, true);
2811
+ }
2812
+ }
2813
+
2814
+ function refreshActiveSessionsList() {
2815
+ const container = document.getElementById('send-active-sessions');
2816
+ if (state.activeSessions.length === 0) {
2817
+ container.innerHTML = `
2818
+ <div class="section-label">Active Sessions</div>
2819
+ <div class="send-dropdown-item disabled">
2820
+ <span class="item-icon">&#128268;</span>
2821
+ <span class="item-label">No active sessions</span>
2822
+ </div>`;
2823
+ return;
2824
+ }
2825
+ container.innerHTML = `<div class="section-label">Send to Session</div>` +
2826
+ state.activeSessions.map(s => {
2827
+ const icon = s.cmd === 'claude' ? '&#129302;' : '&#128187;';
2828
+ const label = s.label || s.id.slice(0, 8);
2829
+ return `<div class="send-dropdown-item" onclick="sendToExistingSession('${s.id}')">
2830
+ <span class="item-icon">${icon}</span>
2831
+ <span class="item-label">${escHtml(label)}</span>
2832
+ <span class="item-meta">PID ${s.pid}</span>
2833
+ </div>`;
2834
+ }).join('');
2835
+ }
2836
+
2837
+ function connectWebSocket() {
2838
+ if (state.ws && state.ws.readyState <= 1) return;
2839
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
2840
+ const ws = new WebSocket(`${proto}//${location.host}?token=${TOKEN}`);
2841
+ state.ws = ws;
2842
+
2843
+ ws.onopen = () => {
2844
+ ws.send(JSON.stringify({ type: 'list' }));
2845
+ };
2846
+
2847
+ ws.onmessage = (e) => {
2848
+ const msg = JSON.parse(e.data);
2849
+ switch (msg.type) {
2850
+ case 'sessions':
2851
+ state.activeSessions = msg.sessions || [];
2852
+ refreshQueueSessionList();
2853
+ break;
2854
+ case 'created':
2855
+ onSessionCreated(msg);
2856
+ break;
2857
+ case 'data-changed':
2858
+ onDataChanged(msg);
2859
+ break;
2860
+ }
2861
+ };
2862
+
2863
+ ws.onclose = () => {
2864
+ state.ws = null;
2865
+ // Reconnect after 3s
2866
+ setTimeout(connectWebSocket, 3000);
2867
+ };
2868
+ }
2869
+
2870
+ function wsSend(msg) {
2871
+ if (state.ws?.readyState === 1) {
2872
+ state.ws.send(JSON.stringify(msg));
2873
+ }
2874
+ }
2875
+
2876
+ function resetSendButton() {
2877
+ const sendBtn = document.querySelector('.send-dropdown-wrap .btn-main');
2878
+ if (sendBtn) { sendBtn.disabled = false; sendBtn.textContent = 'Send'; }
2879
+ }
2880
+
2881
+ function onDataChanged(msg) {
2882
+ const r = msg.resource;
2883
+ if (r === 'prompts' || r === 'folders') {
2884
+ loadPrompts();
2885
+ }
2886
+ if (r === 'queue-draft') {
2887
+ const sid = getQueueSessionId();
2888
+ if (sid) loadQueueDraftForSession(sid);
2889
+ }
2890
+ }
2891
+
2892
+ function onSessionCreated(msg) {
2893
+ // Update sessions list
2894
+ if (!state.activeSessions.find(s => s.id === msg.id)) {
2895
+ state.activeSessions.push({ id: msg.id, label: msg.label, pid: msg.pid, cmd: 'claude', cwd: '' });
2896
+ }
2897
+
2898
+ // If we have a pending prompt send for this session, send it after a delay
2899
+ if (state.pendingPromptSend && state.pendingPromptSend.sessionId === msg.id) {
2900
+ const text = state.pendingPromptSend.text;
2901
+ state.pendingPromptSend = null;
2902
+
2903
+ // Wait for Claude to boot up (~2s for interactive mode)
2904
+ setTimeout(() => {
2905
+ wsSend({ type: 'input', id: msg.id, data: text });
2906
+ setTimeout(() => wsSend({ type: 'input', id: msg.id, data: '\r' }), 100);
2907
+ trackPromptUsage(msg.id);
2908
+ resetSendButton();
2909
+ // Navigate to the main page with this session
2910
+ const mainUrl = `/#session=${msg.id}`;
2911
+ window.open(mainUrl, '_blank');
2912
+ showStatusMessage('Prompt sent to new session');
2913
+ }, 2500);
2914
+ } else {
2915
+ // Session created but not from our send — still reset button if stuck
2916
+ resetSendButton();
2917
+ }
2918
+ }
2919
+
2920
+ async function sendToNewSession() {
2921
+ closeSendDropdown();
2922
+
2923
+ // Guard: prevent multiple clicks while a session is being created
2924
+ if (state.pendingPromptSend) {
2925
+ showStatusMessage('Session already being created...');
2926
+ return;
2927
+ }
2928
+
2929
+ const text = htmlToMarkdown(document.getElementById('editor')?.innerHTML || '').trim();
2930
+ if (!text) { toast('No prompt content to send.', { type: 'warning' }); return; }
2931
+
2932
+ const title = document.getElementById('prompt-title-input')?.value || 'Untitled';
2933
+ const sessionId = crypto.randomUUID();
2934
+
2935
+ // Set pending so we send the prompt when the session is ready
2936
+ state.pendingPromptSend = { sessionId, text };
2937
+
2938
+ // Disable the send button visually
2939
+ const sendBtn = document.querySelector('.send-dropdown-wrap .btn-main');
2940
+ if (sendBtn) { sendBtn.disabled = true; sendBtn.textContent = 'Creating...'; }
2941
+
2942
+ showStatusMessage('Creating new Claude session...');
2943
+
2944
+ wsSend({
2945
+ type: 'create',
2946
+ id: sessionId,
2947
+ cmd: 'claude',
2948
+ args: [],
2949
+ label: title.slice(0, 60),
2950
+ });
2951
+
2952
+ // Safety timeout: re-enable button after 15s if no response
2953
+ setTimeout(() => {
2954
+ if (state.pendingPromptSend?.sessionId === sessionId) {
2955
+ state.pendingPromptSend = null;
2956
+ resetSendButton();
2957
+ showStatusMessage('Session creation timed out');
2958
+ }
2959
+ }, 15000);
2960
+ }
2961
+
2962
+ async function sendToExistingSession(sessionId) {
2963
+ closeSendDropdown();
2964
+ const text = htmlToMarkdown(document.getElementById('editor')?.innerHTML || '').trim();
2965
+ if (!text) { toast('No prompt content to send.', { type: 'warning' }); return; }
2966
+
2967
+ wsSend({ type: 'input', id: sessionId, data: text });
2968
+ setTimeout(() => wsSend({ type: 'input', id: sessionId, data: '\r' }), 100);
2969
+ trackPromptUsage(sessionId);
2970
+
2971
+ showStatusMessage('Prompt sent to session');
2972
+
2973
+ // Open the session in main page
2974
+ const mainUrl = `/#session=${sessionId}`;
2975
+ window.open(mainUrl, '_blank');
2976
+ }
2977
+
2978
+ async function trackPromptUsage(sessionId) {
2979
+ if (!state.currentPromptId) return;
2980
+ try {
2981
+ await fetch(API(`/prompts/${state.currentPromptId}/usage`), {
2982
+ method: 'POST',
2983
+ headers: { 'Content-Type': 'application/json' },
2984
+ body: JSON.stringify({ session_id: sessionId }),
2985
+ });
2986
+ updateUsageBadge();
2987
+ } catch (e) {
2988
+ console.error('Failed to track usage:', e);
2989
+ }
2990
+ }
2991
+
2992
+ async function updateUsageBadge() {
2993
+ const wrap = document.getElementById('usage-badge-wrap');
2994
+ if (!state.currentPromptId) { wrap.innerHTML = ''; return; }
2995
+ try {
2996
+ const res = await fetch(API(`/prompts/${state.currentPromptId}/usage`));
2997
+ const stats = await res.json();
2998
+ if (stats.use_count > 0) {
2999
+ wrap.innerHTML = `<span class="usage-badge" onclick="showUsageHistory()" title="Click to see session history">
3000
+ &#128640; Used ${stats.use_count}x${stats.last_used ? ' · Last: ' + new Date(stats.last_used).toLocaleDateString() : ''}
3001
+ </span>`;
3002
+ } else {
3003
+ wrap.innerHTML = '';
3004
+ }
3005
+ } catch { wrap.innerHTML = ''; }
3006
+ }
3007
+
3008
+ async function showUsageHistory() {
3009
+ if (!state.currentPromptId) return;
3010
+ try {
3011
+ const res = await fetch(API(`/prompts/${state.currentPromptId}/usage/sessions`));
3012
+ const sessions = await res.json();
3013
+ if (sessions.length === 0) { toast('No usage history.', { type: 'info' }); return; }
3014
+ const lines = sessions.map(s => {
3015
+ const date = new Date(s.used_at).toLocaleString();
3016
+ return `${date} — Session: ${s.session_id.slice(0, 8)}...`;
3017
+ });
3018
+ toast(lines.join('<br>'), { type: 'info', title: 'Usage History', duration: 8000 });
3019
+ } catch (e) {
3020
+ toast('Failed to load usage history: ' + e.message, { type: 'error' });
3021
+ }
3022
+ }
3023
+
3024
+ function copyPromptText() {
3025
+ closeSendDropdown();
3026
+ const title = document.getElementById('prompt-title-input')?.value?.trim() || '';
3027
+ const body = getPromptPlainText();
3028
+ const text = title ? `${title}\n\n${body}` : body;
3029
+ navigator.clipboard.writeText(text).then(() => {
3030
+ showStatusMessage('Copied as text');
3031
+ });
3032
+ }
3033
+
3034
+ function copyPromptMarkdown() {
3035
+ closeSendDropdown();
3036
+ const title = document.getElementById('prompt-title-input')?.value?.trim() || '';
3037
+ const editor = document.getElementById('editor');
3038
+ const html = editor ? editor.innerHTML : '';
3039
+ const body = htmlToMarkdown(html);
3040
+ const md = title ? `# ${title}\n\n${body}` : body;
3041
+ navigator.clipboard.writeText(md).then(() => {
3042
+ showStatusMessage('Copied as markdown');
3043
+ });
3044
+ }
3045
+
3046
+ // DOM-based HTML→markdown (handles nested lists correctly)
3047
+ // Note: uses createElement+innerHTML on trusted editor content only (same-origin prompt HTML)
3048
+ function htmlToMarkdown(html) {
3049
+ const el = document.createElement('div');
3050
+ el.innerHTML = html; // trusted: only called with editor.innerHTML from our own contenteditable
3051
+ return _mdNode(el, 0, null).replace(/\n{3,}/g, '\n\n').trim();
3052
+ }
3053
+
3054
+ function _mdNode(node, depth, counter) {
3055
+ let out = '';
3056
+ for (const c of node.childNodes) {
3057
+ if (c.nodeType === 3) { out += c.textContent.replace(/&nbsp;/g, ' '); continue; }
3058
+ if (c.nodeType !== 1) continue;
3059
+ const tag = c.tagName.toLowerCase();
3060
+ const inner = (d, cnt) => _mdNode(c, d !== undefined ? d : depth, cnt !== undefined ? cnt : null);
3061
+ switch (tag) {
3062
+ case 'br': out += '\n'; break;
3063
+ case 'div': { const txt = inner(depth); out += txt + (txt.endsWith('\n') ? '' : '\n'); break; }
3064
+ case 'p': out += inner(depth) + '\n\n'; break;
3065
+ case 'h1': out += '# ' + inner() + '\n'; break;
3066
+ case 'h2': out += '## ' + inner() + '\n'; break;
3067
+ case 'h3': out += '### ' + inner() + '\n'; break;
3068
+ case 'strong': case 'b': out += '**' + inner() + '**'; break;
3069
+ case 'em': case 'i': out += '*' + inner() + '*'; break;
3070
+ case 'code':
3071
+ if (c.parentElement?.tagName === 'PRE') { out += c.textContent; break; }
3072
+ out += '`' + inner() + '`'; break;
3073
+ case 's': case 'strike': case 'del': out += '~~' + inner() + '~~'; break;
3074
+ case 'blockquote': { const t = inner(depth).trim(); out += t.split('\n').map(l => '> ' + l).join('\n') + '\n'; break; }
3075
+ case 'hr': out += '---\n'; break;
3076
+ case 'pre': out += '```\n' + c.textContent + '\n```\n'; break;
3077
+ case 'a': { const href = c.getAttribute('href') || ''; const text = inner(); out += (href && text !== href) ? `[${text}](${href})` : text; break; }
3078
+ case 'img': {
3079
+ const src = c.getAttribute('src') || '';
3080
+ const filePath = c.getAttribute('data-file-path') || '';
3081
+ const f = src.match(/\/api\/images\/file\/(.+?)(?:\?|$)/);
3082
+ const name = f ? f[1] : 'image';
3083
+ out += `![${name}](${filePath || src})\n`;
3084
+ break;
3085
+ }
3086
+ case 'ol': case 'ul': {
3087
+ const hasLi = Array.from(c.children).some(ch => ch.tagName === 'LI');
3088
+ if (hasLi) { out += inner(depth + 1, tag === 'ol' ? { n: 0 } : null); }
3089
+ else { out += _mdNode(c, depth, counter); }
3090
+ break;
3091
+ }
3092
+ case 'li': {
3093
+ const indent = ' '.repeat(Math.max(0, depth - 1));
3094
+ const prefix = counter ? `${++counter.n}. ` : '- ';
3095
+ out += indent + prefix + inner(depth).trim() + '\n';
3096
+ break;
3097
+ }
3098
+ default: out += _mdNode(c, depth, counter); break;
3099
+ }
3100
+ }
3101
+ return out;
3102
+ }
3103
+
3104
+ function closeSendDropdown() {
3105
+ document.getElementById('send-dropdown').classList.remove('open');
3106
+ document.removeEventListener('click', closeSendDropdownOutside, true);
3107
+ }
3108
+
3109
+ function showStatusMessage(msg) {
3110
+ document.getElementById('status-saved').textContent = msg;
3111
+ setTimeout(() => updateStatusBar(), 2500);
3112
+ }
3113
+
3114
+ // ============================================================
3115
+ // Folders
3116
+ // ============================================================
3117
+ function createNewFolder() {
3118
+ document.getElementById('folder-modal').classList.remove('hidden');
3119
+ document.getElementById('folder-name').value = '';
3120
+ document.getElementById('folder-name').focus();
3121
+ }
3122
+
3123
+ async function saveFolderModal() {
3124
+ const name = document.getElementById('folder-name').value.trim();
3125
+ if (!name) return;
3126
+ const color = document.getElementById('folder-color').value;
3127
+ await fetch(API('/folders'), {
3128
+ method: 'POST',
3129
+ headers: { 'Content-Type': 'application/json' },
3130
+ body: JSON.stringify({ name, color }),
3131
+ });
3132
+ closeModal('folder-modal');
3133
+ loadFolders();
3134
+ }
3135
+
3136
+ // ============================================================
3137
+ // Chains
3138
+ // ============================================================
3139
+ async function loadChains() {
3140
+ const res = await fetch(API('/chains'));
3141
+ const chains = await res.json();
3142
+ const list = document.getElementById('chain-list');
3143
+
3144
+ if (!chains.length) {
3145
+ list.innerHTML = `<div class="empty-state"><h3>No Prompt Chains</h3><p>Chains let you sequence multiple prompts with conditional branching.</p></div>`;
3146
+ return;
3147
+ }
3148
+
3149
+ list.innerHTML = chains.map(c => `<div class="prompt-item" onclick="openChain(${c.id})" style="border-left:3px solid var(--purple)">
3150
+ <div class="prompt-title">${escHtml(c.name)}</div>
3151
+ <div class="prompt-meta"><span>${escHtml(c.description || '')}</span><span>${timeAgo(c.updated_at)}</span></div>
3152
+ </div>`).join('');
3153
+ }
3154
+
3155
+ function createNewChain() {
3156
+ document.getElementById('chain-modal-title').textContent = 'New Chain';
3157
+ document.getElementById('chain-name').value = '';
3158
+ document.getElementById('chain-desc').value = '';
3159
+ document.getElementById('chain-modal').classList.remove('hidden');
3160
+ }
3161
+
3162
+ async function saveChainModal() {
3163
+ const name = document.getElementById('chain-name').value.trim();
3164
+ if (!name) return;
3165
+ const description = document.getElementById('chain-desc').value;
3166
+ await fetch(API('/chains'), {
3167
+ method: 'POST',
3168
+ headers: { 'Content-Type': 'application/json' },
3169
+ body: JSON.stringify({ name, description }),
3170
+ });
3171
+ closeModal('chain-modal');
3172
+ loadChains();
3173
+ }
3174
+
3175
+
3176
+ // ============================================================
3177
+ // Session Conversations
3178
+ // ============================================================
3179
+ async function loadConversations() {
3180
+ const search = document.getElementById('conv-search')?.value || '';
3181
+ const res = await fetch(API(`/conversations?limit=100${search ? '&search=' + encodeURIComponent(search) : ''}`));
3182
+ const convs = await res.json();
3183
+ const list = document.getElementById('conv-list');
3184
+
3185
+ if (!convs.length) {
3186
+ list.innerHTML = `<div class="empty-state"><h3>No Conversations Imported</h3><p>Click "Import All Sessions" to copy session conversations into the database for analysis and data mining.</p></div>`;
3187
+ return;
3188
+ }
3189
+
3190
+ list.innerHTML = convs.map(c => `<div class="prompt-item" style="border-left-color:var(--green)" onclick="viewConversation('${c.session_id}')">
3191
+ <div class="prompt-title">${escHtml(c.title || c.first_message?.slice(0, 60) || c.session_id.slice(0, 8))}</div>
3192
+ <div class="prompt-meta">
3193
+ <span style="color:var(--accent)">${escHtml(c.project_path?.replace(/^\/Users\/[^/]+\//, '~/') || '')}</span>
3194
+ <span>${c.user_msg_count} user / ${c.assistant_msg_count} assistant msgs</span>
3195
+ <span>${c.git_branch || ''}</span>
3196
+ <span>${timeAgo(c.imported_at)}</span>
3197
+ </div>
3198
+ </div>`).join('');
3199
+ }
3200
+
3201
+ async function importConversations() {
3202
+ document.getElementById('conv-list').innerHTML = '<div style="padding:20px;text-align:center;color:var(--accent)">Importing sessions... this may take a moment.</div>';
3203
+ try {
3204
+ const res = await fetch(API('/conversations/import'), { method: 'POST' });
3205
+ const data = await res.json();
3206
+ toast(`Imported ${data.imported} sessions.`, { type: 'success' });
3207
+ loadConversations();
3208
+ } catch (e) {
3209
+ toast('Import failed: ' + e.message, { type: 'error' });
3210
+ }
3211
+ }
3212
+
3213
+ async function viewConversation(sessionId) {
3214
+ const res = await fetch(API(`/conversations/${sessionId}`));
3215
+ const conv = await res.json();
3216
+ if (conv.error) { toast(conv.error, { type: 'error' }); return; }
3217
+
3218
+ const messages = JSON.parse(conv.messages || '[]');
3219
+ const modal = document.createElement('div');
3220
+ modal.className = 'modal-overlay';
3221
+ modal.innerHTML = `<div class="modal" style="max-width:800px;max-height:80vh;overflow-y:auto;">
3222
+ <h3 style="margin-bottom:4px">${escHtml(conv.title || conv.session_id.slice(0, 8))}</h3>
3223
+ <div style="font-size:11px;color:var(--fg-dim);margin-bottom:12px">${escHtml(conv.project_path || '')} | ${messages.length} messages</div>
3224
+ ${messages.map(m => `<div style="margin-bottom:12px;padding:8px;border-radius:6px;background:${m.role === 'user' ? 'var(--bg-lighter)' : 'var(--bg)'};border-left:3px solid ${m.role === 'user' ? 'var(--accent)' : 'var(--green)'}">
3225
+ <div style="font-size:10px;color:${m.role === 'user' ? 'var(--accent)' : 'var(--green)'};text-transform:uppercase;margin-bottom:4px">${m.role === 'user' ? 'You' : 'Claude'}</div>
3226
+ <div style="font-size:12px;line-height:1.6;white-space:pre-wrap;max-height:300px;overflow-y:auto">${escHtml(m.text || '')}</div>
3227
+ </div>`).join('')}
3228
+ <div class="btn-row"><button class="btn" onclick="this.closest('.modal-overlay').remove()">Close</button></div>
3229
+ </div>`;
3230
+ modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
3231
+ document.body.appendChild(modal);
3232
+ }
3233
+
3234
+ // ============================================================
3235
+ // Templates
3236
+ // ============================================================
3237
+ async function loadTemplates() {
3238
+ const res = await fetch(API('/templates'));
3239
+ const templates = await res.json();
3240
+ const list = document.getElementById('template-list');
3241
+
3242
+ if (!templates.length) {
3243
+ list.innerHTML = `<div class="empty-state"><h3>No Templates</h3><p>Templates let you create reusable prompts with {{variables}} that get filled in each time.</p></div>`;
3244
+ return;
3245
+ }
3246
+
3247
+ list.innerHTML = templates.map(t => {
3248
+ const vars = JSON.parse(t.variables || '[]');
3249
+ return `<div class="prompt-item" style="border-left-color:var(--yellow)">
3250
+ <div class="prompt-title">${escHtml(t.name)}</div>
3251
+ <div class="prompt-meta">
3252
+ <span>${escHtml(t.category)}</span>
3253
+ ${vars.map(v => `<span class="tag">{{${escHtml(v)}}}</span>`).join('')}
3254
+ </div>
3255
+ <div style="margin-top:6px;display:flex;gap:4px;">
3256
+ <button class="btn small" onclick="useTemplate(${t.id})">Use</button>
3257
+ <button class="btn small danger" onclick="deleteTemplate(${t.id})">Delete</button>
3258
+ </div>
3259
+ </div>`;
3260
+ }).join('');
3261
+ }
3262
+
3263
+ function createNewTemplate() {
3264
+ document.getElementById('template-modal').classList.remove('hidden');
3265
+ document.getElementById('tpl-name').value = '';
3266
+ document.getElementById('tpl-desc').value = '';
3267
+ document.getElementById('tpl-content').value = '';
3268
+ document.getElementById('tpl-vars').value = '';
3269
+ document.getElementById('tpl-name').focus();
3270
+ }
3271
+
3272
+ async function saveTemplateModal() {
3273
+ const data = {
3274
+ name: document.getElementById('tpl-name').value.trim(),
3275
+ description: document.getElementById('tpl-desc').value,
3276
+ content: document.getElementById('tpl-content').value,
3277
+ content_html: document.getElementById('tpl-content').value,
3278
+ category: document.getElementById('tpl-category').value,
3279
+ variables: document.getElementById('tpl-vars').value.split(',').map(s => s.trim()).filter(Boolean),
3280
+ };
3281
+ if (!data.name) return;
3282
+ await fetch(API('/templates'), {
3283
+ method: 'POST',
3284
+ headers: { 'Content-Type': 'application/json' },
3285
+ body: JSON.stringify(data),
3286
+ });
3287
+ closeModal('template-modal');
3288
+ loadTemplates();
3289
+ }
3290
+
3291
+ async function useTemplate(id) {
3292
+ const res = await fetch(API(`/templates/${id}`));
3293
+ const tpl = await res.json();
3294
+ const vars = JSON.parse(tpl.variables || '[]');
3295
+ let content = tpl.content;
3296
+
3297
+ // Prompt for each variable
3298
+ for (const v of vars) {
3299
+ const val = prompt(`Enter value for {{${v}}}:`);
3300
+ if (val === null) return;
3301
+ const escaped = v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3302
+ content = content.replace(new RegExp(`\\{\\{${escaped}\\}\\}`, 'g'), val);
3303
+ }
3304
+
3305
+ // Create a new prompt from the template
3306
+ const createRes = await fetch(API('/prompts'), {
3307
+ method: 'POST',
3308
+ headers: { 'Content-Type': 'application/json' },
3309
+ body: JSON.stringify({
3310
+ title: `${tpl.name} - ${new Date().toLocaleDateString()}`,
3311
+ content: content,
3312
+ content_html: `<pre>${escHtml(content)}</pre>`,
3313
+ context_type: tpl.category,
3314
+ folder_id: state.currentFolderId,
3315
+ }),
3316
+ });
3317
+ const data = await createRes.json();
3318
+ if (data.id) {
3319
+ showView('editor');
3320
+ await loadPrompts();
3321
+ openPrompt(data.id);
3322
+ }
3323
+ }
3324
+
3325
+ async function deleteTemplate(id) {
3326
+ if (!confirm('Delete this template?')) return;
3327
+ await fetch(API(`/templates/${id}`), { method: 'DELETE' });
3328
+ loadTemplates();
3329
+ }
3330
+
3331
+ // ============================================================
3332
+ // View Navigation
3333
+ // ============================================================
3334
+ function showView(view, skipHash) {
3335
+ state.currentView = view;
3336
+ const views = ['editor', 'chains', 'conversations', 'templates', 'backups', 'patterns'];
3337
+ for (const v of views) {
3338
+ const el = document.getElementById(`view-${v}`);
3339
+ if (el) el.style.display = v === view ? 'flex' : 'none';
3340
+ }
3341
+ // Highlight active view button
3342
+ document.querySelectorAll('.topbar-actions .btn[data-view]').forEach(btn => {
3343
+ btn.classList.toggle('active', btn.dataset.view === view);
3344
+ });
3345
+
3346
+ // Load data for the view
3347
+ if (view === 'chains') loadChains();
3348
+ if (view === 'conversations') loadConversations();
3349
+ if (view === 'templates') loadTemplates();
3350
+ if (view === 'backups') loadBackups();
3351
+ if (view === 'patterns') loadPatterns();
3352
+
3353
+ // Update URL hash (non-editor views)
3354
+ if (!skipHash && view !== 'editor') {
3355
+ updateUrlHash({ view });
3356
+ }
3357
+ }
3358
+
3359
+ function navigateToMain() {
3360
+ location.href = '/';
3361
+ }
3362
+
3363
+ // ============================================================
3364
+ // Utilities
3365
+ // ============================================================
3366
+ function closeModal(id) {
3367
+ document.getElementById(id).classList.add('hidden');
3368
+ }
3369
+
3370
+ function escHtml(s) {
3371
+ if (!s) return '';
3372
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
3373
+ }
3374
+
3375
+ function timeAgo(dateStr) {
3376
+ if (!dateStr) return '';
3377
+ const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
3378
+ if (seconds < 60) return 'just now';
3379
+ const minutes = Math.floor(seconds / 60);
3380
+ if (minutes < 60) return `${minutes}m ago`;
3381
+ const hours = Math.floor(minutes / 60);
3382
+ if (hours < 24) return `${hours}h ago`;
3383
+ const days = Math.floor(hours / 24);
3384
+ return `${days}d ago`;
3385
+ }
3386
+
3387
+ function updateWordCount() {
3388
+ const text = document.getElementById('editor')?.innerText || '';
3389
+ const words = text.trim() ? text.trim().split(/\s+/).length : 0;
3390
+ document.getElementById('status-words').textContent = `${words} words`;
3391
+ }
3392
+
3393
+ function updateStatusBar() {
3394
+ document.getElementById('status-prompt').textContent = state.currentPromptId
3395
+ ? `Prompt #${state.currentPromptId}`
3396
+ : 'No prompt selected';
3397
+ }
3398
+
3399
+ // ============================================================
3400
+ // Zoom
3401
+ // ============================================================
3402
+ const ZOOM_STEPS = [50, 67, 75, 80, 90, 100, 110, 125, 150, 175, 200, 250, 300];
3403
+ let currentZoom = 100;
3404
+
3405
+ function zoomEditor(dir) {
3406
+ const idx = ZOOM_STEPS.indexOf(currentZoom);
3407
+ if (idx === -1) {
3408
+ currentZoom = dir > 0 ? (ZOOM_STEPS.find(z => z > currentZoom) || 300) : ([...ZOOM_STEPS].reverse().find(z => z < currentZoom) || 50);
3409
+ } else {
3410
+ const next = idx + dir;
3411
+ if (next >= 0 && next < ZOOM_STEPS.length) currentZoom = ZOOM_STEPS[next];
3412
+ }
3413
+ applyZoom();
3414
+ }
3415
+
3416
+ function setZoom(value) {
3417
+ currentZoom = value;
3418
+ applyZoom();
3419
+ }
3420
+
3421
+ function applyZoom() {
3422
+ const editor = document.getElementById('editor');
3423
+ if (editor) editor.style.zoom = currentZoom / 100;
3424
+ const sel = document.getElementById('zoom-select');
3425
+ if (sel) sel.value = currentZoom;
3426
+ fetch(API('/settings'), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ui_editor_zoom: currentZoom }) }).catch(() => {});
3427
+ }
3428
+
3429
+ // Restore saved zoom from DB
3430
+ fetch(API('/settings?key=ui_editor_zoom')).then(r => r.json()).then(d => {
3431
+ if (d.value) { currentZoom = parseInt(d.value); applyZoom(); }
3432
+ }).catch(() => {});
3433
+
3434
+ // --- Font selection ---
3435
+ const DEFAULT_FONT = "'Courier New', Courier, monospace";
3436
+
3437
+ function setEditorFont(fontFamily, save) {
3438
+ const editor = document.getElementById('editor');
3439
+ if (editor) editor.style.fontFamily = fontFamily;
3440
+ const sel = document.getElementById('font-select');
3441
+ if (sel) sel.value = fontFamily;
3442
+ if (save !== false) fetch(API('/settings'), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ui_editor_font: fontFamily }) }).catch(() => {});
3443
+ }
3444
+
3445
+ // Restore saved font from DB (default: Courier New)
3446
+ fetch(API('/settings?key=ui_editor_font')).then(r => r.json()).then(d => {
3447
+ setEditorFont(d.value || DEFAULT_FONT, false);
3448
+ }).catch(() => { setEditorFont(DEFAULT_FONT, false); });
3449
+
3450
+ // Keyboard shortcuts
3451
+ document.addEventListener('keydown', (e) => {
3452
+ if (e.key === 'Escape') {
3453
+ document.querySelectorAll('.modal-overlay:not(.hidden)').forEach(m => m.classList.add('hidden'));
3454
+ if (document.getElementById('annotation-overlay').classList.contains('active')) closeAnnotation();
3455
+ }
3456
+ // Ctrl+S: save
3457
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
3458
+ e.preventDefault();
3459
+ saveCurrentPrompt();
3460
+ }
3461
+ });
3462
+
3463
+ // Listen for messages from main window (send prompt to session)
3464
+ window.addEventListener('message', (e) => {
3465
+ if (e.data?.type === 'session-data') {
3466
+ // Session data feedback
3467
+ console.log('Received session data:', e.data);
3468
+ }
3469
+ });
3470
+
3471
+ // ============================================================
3472
+ // Init
3473
+ // ============================================================
3474
+ // ============================================================
3475
+ // Backups
3476
+ // ============================================================
3477
+ async function loadBackups() {
3478
+ const res = await fetch(API('/backups'));
3479
+ const backups = await res.json();
3480
+ const list = document.getElementById('backup-list');
3481
+
3482
+ if (!backups.length) {
3483
+ list.innerHTML = `<div class="empty-state"><h3>No Backups Yet</h3><p>Click "Backup Now" to create your first manual backup. Daily auto-backups will also appear here.</p></div>`;
3484
+ return;
3485
+ }
3486
+
3487
+ list.innerHTML = `<table class="perm-table">
3488
+ <thead><tr><th>Backup</th><th>Size</th><th>Images</th><th>Created</th><th></th></tr></thead>
3489
+ <tbody>${backups.map(b => {
3490
+ const sizeKb = (b.size / 1024).toFixed(0);
3491
+ const sizeMb = (b.size / (1024 * 1024)).toFixed(1);
3492
+ const sizeStr = b.size > 1024 * 1024 ? `${sizeMb} MB` : `${sizeKb} KB`;
3493
+ const label = b.name.replace('task-manager-', '').replace('.db', '');
3494
+ const isDaily = b.name.includes('-daily');
3495
+ const isManual = b.name.includes('-manual');
3496
+ const isPreRestore = b.name.includes('-pre-restore');
3497
+ const tagColor = isDaily ? 'var(--accent)' : isManual ? 'var(--yellow)' : isPreRestore ? 'var(--red)' : 'var(--fg-dim)';
3498
+ const tagLabel = isDaily ? 'auto' : isManual ? 'manual' : isPreRestore ? 'pre-restore' : '';
3499
+ return `<tr>
3500
+ <td>
3501
+ <div style="font-weight:500">${escHtml(label)}</div>
3502
+ ${tagLabel ? `<span style="font-size:9px;padding:1px 6px;border-radius:3px;background:${tagColor}20;color:${tagColor}">${tagLabel}</span>` : ''}
3503
+ </td>
3504
+ <td>${sizeStr}</td>
3505
+ <td>${b.hasImages ? '<span style="color:var(--green)">Yes</span>' : '<span style="color:var(--fg-dim)">No</span>'}</td>
3506
+ <td>${new Date(b.createdAt).toLocaleString()}</td>
3507
+ <td style="white-space:nowrap;display:flex;gap:4px;">
3508
+ <button class="btn small primary" onclick="restoreFromBackup('${escHtml(b.name)}')">Restore</button>
3509
+ <button class="btn small danger" onclick="deleteBackupItem('${escHtml(b.name)}')">Delete</button>
3510
+ </td>
3511
+ </tr>`;
3512
+ }).join('')}</tbody>
3513
+ </table>`;
3514
+
3515
+ document.getElementById('backup-status').textContent = `${backups.length} backup(s)`;
3516
+ }
3517
+
3518
+ async function createManualBackup() {
3519
+ const label = prompt('Backup label (optional):', 'manual');
3520
+ if (label === null) return;
3521
+ document.getElementById('backup-status').textContent = 'Creating backup...';
3522
+ try {
3523
+ const res = await fetch(API('/backups'), {
3524
+ method: 'POST',
3525
+ headers: { 'Content-Type': 'application/json' },
3526
+ body: JSON.stringify({ label: label || 'manual' }),
3527
+ });
3528
+ const data = await res.json();
3529
+ if (data.error) {
3530
+ toast('Backup failed: ' + data.error, { type: 'error' });
3531
+ } else {
3532
+ document.getElementById('backup-status').textContent = `Backup created: ${data.backupName}`;
3533
+ }
3534
+ loadBackups();
3535
+ } catch (e) {
3536
+ toast('Backup failed: ' + e.message, { type: 'error' });
3537
+ }
3538
+ }
3539
+
3540
+ async function restoreFromBackup(name) {
3541
+ if (!confirm(`Restore from backup "${name}"?\n\nA safety snapshot of the current state will be created first. The page will reload after restore.`)) return;
3542
+ document.getElementById('backup-status').textContent = 'Restoring...';
3543
+ try {
3544
+ const res = await fetch(API('/backups/restore'), {
3545
+ method: 'POST',
3546
+ headers: { 'Content-Type': 'application/json' },
3547
+ body: JSON.stringify({ name }),
3548
+ });
3549
+ const data = await res.json();
3550
+ if (data.error) {
3551
+ toast('Restore failed: ' + data.error, { type: 'error' });
3552
+ } else {
3553
+ toast('Restore successful! Reloading...', { type: 'success' });
3554
+ setTimeout(() => location.reload(), 1500);
3555
+ }
3556
+ } catch (e) {
3557
+ toast('Restore failed: ' + e.message, { type: 'error' });
3558
+ }
3559
+ }
3560
+
3561
+ async function deleteBackupItem(name) {
3562
+ if (!confirm(`Delete backup "${name}"? This cannot be undone.`)) return;
3563
+ await fetch(API(`/backups/${encodeURIComponent(name)}`), { method: 'DELETE' });
3564
+ loadBackups();
3565
+ }
3566
+
3567
+ // ============================================================
3568
+ // URL Hash Routing
3569
+ // ============================================================
3570
+ function updateUrlHash(params) {
3571
+ const parts = [];
3572
+ if (params.view && params.view !== 'editor') {
3573
+ parts.push(`view=${params.view}`);
3574
+ } else {
3575
+ if (params.folder) parts.push(`folder=${params.folder}`);
3576
+ if (params.prompt) parts.push(`prompt=${params.prompt}`);
3577
+ }
3578
+ const hash = parts.length ? '#' + parts.join('&') : '';
3579
+ if (location.hash !== hash) {
3580
+ history.replaceState(null, '', location.pathname + location.search + hash);
3581
+ }
3582
+ }
3583
+
3584
+ function parseUrlHash() {
3585
+ const hash = location.hash.slice(1);
3586
+ if (!hash) return {};
3587
+ const params = {};
3588
+ for (const part of hash.split('&')) {
3589
+ const [k, v] = part.split('=');
3590
+ if (k && v !== undefined) params[k] = v;
3591
+ }
3592
+ return params;
3593
+ }
3594
+
3595
+ async function restoreFromHash() {
3596
+ const params = parseUrlHash();
3597
+
3598
+ // Restore view (chains, permissions, etc.)
3599
+ if (params.view) {
3600
+ showView(params.view, true);
3601
+ return;
3602
+ }
3603
+
3604
+ // Restore folder selection
3605
+ if (params.folder) {
3606
+ state.currentFolderId = parseInt(params.folder);
3607
+ renderFolderTree();
3608
+ await loadPrompts();
3609
+ }
3610
+
3611
+ // Restore open prompt
3612
+ if (params.prompt) {
3613
+ openPrompt(parseInt(params.prompt));
3614
+ }
3615
+ }
3616
+
3617
+ window.addEventListener('hashchange', () => {
3618
+ restoreFromHash();
3619
+ });
3620
+
3621
+ // ============================================================
3622
+ // Queue Builder
3623
+ // ============================================================
3624
+ let queueItems = [];
3625
+ let queueMode = 'manual';
3626
+ let queueCurrentSession = null;
3627
+
3628
+ function getQueueSessionId() {
3629
+ return document.getElementById('queue-session-select').value || null;
3630
+ }
3631
+
3632
+ function saveQueueToStorage() {
3633
+ const sessionId = queueCurrentSession;
3634
+ if (!sessionId) return;
3635
+ fetch(API(`/queue-draft/${encodeURIComponent(sessionId)}`), {
3636
+ method: 'PUT',
3637
+ headers: { 'Content-Type': 'application/json' },
3638
+ body: JSON.stringify({ items: queueItems, mode: queueMode }),
3639
+ }).catch(() => {});
3640
+ }
3641
+
3642
+ async function loadQueueDraftForSession(sessionId) {
3643
+ queueCurrentSession = sessionId;
3644
+ if (!sessionId) { queueItems = []; queueMode = 'manual'; renderQueueBuilderList(); updateQueueModeBtn(); return; }
3645
+ try {
3646
+ const res = await fetch(API(`/queue-draft/${encodeURIComponent(sessionId)}`));
3647
+ const data = await res.json();
3648
+ queueItems = data.items || [];
3649
+ queueMode = data.mode || 'manual';
3650
+ } catch { queueItems = []; queueMode = 'manual'; }
3651
+ renderQueueBuilderList();
3652
+ updateQueueModeBtn();
3653
+ }
3654
+
3655
+ function updateQueueModeBtn() {
3656
+ const modeBtn = document.getElementById('queue-mode-btn');
3657
+ if (modeBtn) modeBtn.textContent = queueMode === 'auto' ? 'Auto' : 'Manual';
3658
+ }
3659
+
3660
+ function onQueueSessionChange() {
3661
+ const sessionId = getQueueSessionId();
3662
+ if (sessionId !== queueCurrentSession) {
3663
+ loadQueueDraftForSession(sessionId);
3664
+ }
3665
+ }
3666
+
3667
+ function clearQueueItems() {
3668
+ queueItems = [];
3669
+ saveQueueToStorage();
3670
+ renderQueueBuilderList();
3671
+ }
3672
+
3673
+ function toggleQueueBuilder() {
3674
+ const panel = document.getElementById('queue-builder');
3675
+ const isActive = panel.classList.toggle('active');
3676
+ document.getElementById('queue-builder-toggle').classList.toggle('active', isActive);
3677
+ if (isActive) {
3678
+ refreshQueueSessionList();
3679
+ loadQueueDraftForSession(getQueueSessionId());
3680
+ }
3681
+ }
3682
+
3683
+ function toggleQueueBuilderMode() {
3684
+ queueMode = queueMode === 'auto' ? 'manual' : 'auto';
3685
+ updateQueueModeBtn();
3686
+ saveQueueToStorage();
3687
+ }
3688
+
3689
+ function refreshQueueSessionList() {
3690
+ const sel = document.getElementById('queue-session-select');
3691
+ const sessions = state.activeSessions || [];
3692
+ if (sessions.length === 0) {
3693
+ sel.innerHTML = '<option value="">No active sessions</option>';
3694
+ return;
3695
+ }
3696
+ const prevVal = sel.value;
3697
+ sel.innerHTML = sessions.map(s =>
3698
+ `<option value="${s.id}">${escHtml(s.label || s.id.slice(0, 8))} (PID ${s.pid || '?'})</option>`
3699
+ ).join('');
3700
+ // Keep previous selection if still valid
3701
+ if (prevVal && sessions.some(s => s.id === prevVal)) {
3702
+ sel.value = prevVal;
3703
+ }
3704
+ if (sel.value !== queueCurrentSession) {
3705
+ loadQueueDraftForSession(sel.value);
3706
+ }
3707
+ }
3708
+
3709
+ async function addPromptToQueue(promptId) {
3710
+ // Fetch the prompt text
3711
+ try {
3712
+ const res = await fetch(API(`/prompts/${promptId}`));
3713
+ const prompt = await res.json();
3714
+ queueItems.push({
3715
+ type: 'prompt',
3716
+ promptId,
3717
+ title: prompt.title || 'Untitled',
3718
+ text: prompt.content || '',
3719
+ });
3720
+ saveQueueToStorage();
3721
+ renderQueueBuilderList();
3722
+ // Open queue builder if not open
3723
+ const panel = document.getElementById('queue-builder');
3724
+ if (!panel.classList.contains('active')) {
3725
+ toggleQueueBuilder();
3726
+ }
3727
+ } catch (e) {
3728
+ console.error('Failed to fetch prompt:', e);
3729
+ }
3730
+ }
3731
+
3732
+ function addInlineToQueue() {
3733
+ const input = document.getElementById('queue-inline-input');
3734
+ const text = input.value.trim();
3735
+ if (!text) return;
3736
+ queueItems.push({
3737
+ type: 'inline',
3738
+ promptId: null,
3739
+ title: text.slice(0, 60),
3740
+ text,
3741
+ });
3742
+ input.value = '';
3743
+ saveQueueToStorage();
3744
+ renderQueueBuilderList();
3745
+ }
3746
+
3747
+ function removeFromQueue(idx) {
3748
+ queueItems.splice(idx, 1);
3749
+ saveQueueToStorage();
3750
+ renderQueueBuilderList();
3751
+ }
3752
+
3753
+ function moveQueueItem(fromIdx, toIdx) {
3754
+ if (toIdx < 0 || toIdx >= queueItems.length) return;
3755
+ const [item] = queueItems.splice(fromIdx, 1);
3756
+ queueItems.splice(toIdx, 0, item);
3757
+ saveQueueToStorage();
3758
+ renderQueueBuilderList();
3759
+ }
3760
+
3761
+ function renderQueueBuilderList() {
3762
+ const list = document.getElementById('queue-builder-list');
3763
+ const countEl = document.getElementById('queue-builder-count');
3764
+ if (countEl) countEl.textContent = `${queueItems.length} item${queueItems.length !== 1 ? 's' : ''}`;
3765
+ if (queueItems.length === 0) {
3766
+ list.innerHTML = '<div style="padding:16px;text-align:center;color:var(--fg-dim);font-size:11px;">Click +Q on prompts to add them here, or type inline prompts below.</div>';
3767
+ return;
3768
+ }
3769
+ list.innerHTML = queueItems.map((item, idx) => `
3770
+ <div class="queue-builder-item" draggable="true"
3771
+ ondragstart="onQueueDragStart(event,${idx})"
3772
+ ondragover="event.preventDefault();this.classList.add('drag-over')"
3773
+ ondragleave="this.classList.remove('drag-over')"
3774
+ ondrop="onQueueDrop(event,${idx})">
3775
+ <span class="qb-num">${idx + 1}</span>
3776
+ <span class="qb-title" title="${escHtml(item.text)}">${escHtml(item.title)}</span>
3777
+ <span class="qb-type">${item.type === 'prompt' ? '#' + item.promptId : 'inline'}</span>
3778
+ <button class="qb-remove" onclick="removeFromQueue(${idx})" title="Remove">&times;</button>
3779
+ </div>
3780
+ `).join('');
3781
+ }
3782
+
3783
+ let _queueDragIdx = -1;
3784
+ function onQueueDragStart(event, idx) {
3785
+ _queueDragIdx = idx;
3786
+ event.dataTransfer.effectAllowed = 'move';
3787
+ }
3788
+ function onQueueDrop(event, toIdx) {
3789
+ event.preventDefault();
3790
+ event.currentTarget.classList.remove('drag-over');
3791
+ if (_queueDragIdx >= 0 && _queueDragIdx !== toIdx) {
3792
+ moveQueueItem(_queueDragIdx, toIdx);
3793
+ }
3794
+ _queueDragIdx = -1;
3795
+ }
3796
+
3797
+ async function sendQueue() {
3798
+ const sessionId = getQueueSessionId();
3799
+ if (!sessionId) {
3800
+ toast('Select an active session first', { type: 'warning' });
3801
+ return;
3802
+ }
3803
+ if (queueItems.length === 0) {
3804
+ toast('Add at least one prompt to the queue', { type: 'warning' });
3805
+ return;
3806
+ }
3807
+
3808
+ const idleTimeoutMs = parseInt(document.getElementById('queue-idle-timeout').value || '10', 10) * 1000;
3809
+ const btn = document.getElementById('queue-send-btn');
3810
+ btn.disabled = true;
3811
+ btn.textContent = 'Sending...';
3812
+
3813
+ try {
3814
+ // Create and start queue on server
3815
+ const res = await fetch(API('/queues'), {
3816
+ method: 'POST',
3817
+ headers: { 'Content-Type': 'application/json' },
3818
+ body: JSON.stringify({
3819
+ sessionId,
3820
+ mode: queueMode,
3821
+ items: queueItems,
3822
+ idleTimeoutMs,
3823
+ autoStart: true,
3824
+ }),
3825
+ });
3826
+ const data = await res.json();
3827
+ if (!res.ok || data.error) throw new Error(data.error || `Server error ${res.status}`);
3828
+
3829
+ // Navigate to session in main page
3830
+ const mainUrl = `/#session=${sessionId}`;
3831
+ window.open(mainUrl, '_blank');
3832
+
3833
+ // Clear queue
3834
+ queueItems = [];
3835
+ saveQueueToStorage();
3836
+ renderQueueBuilderList();
3837
+ showStatusMessage('Queue sent and started');
3838
+ } catch (e) {
3839
+ toast('Failed to send queue: ' + e.message, { type: 'error' });
3840
+ } finally {
3841
+ btn.disabled = false;
3842
+ btn.textContent = 'Send Queue';
3843
+ }
3844
+ }
3845
+
3846
+ function showStatusMessage(msg) {
3847
+ const el = document.getElementById('status-saved');
3848
+ if (el) { el.textContent = msg; setTimeout(() => { el.textContent = ''; }, 3000); }
3849
+ }
3850
+
3851
+ // ============================================================
3852
+ // Autocomplete Engine
3853
+ // ============================================================
3854
+ let acTimeout = null;
3855
+ let acSelectedIdx = -1;
3856
+
3857
+ function onSearchInput() {
3858
+ const input = document.getElementById('prompt-search');
3859
+ const q = input.value.trim();
3860
+
3861
+ // Standard search with debounce
3862
+ clearTimeout(state.searchTimeout);
3863
+ state.searchTimeout = setTimeout(() => loadPrompts(), 300);
3864
+
3865
+ // Autocomplete
3866
+ clearTimeout(acTimeout);
3867
+ if (q.length < 3) { hideAutocomplete(); return; }
3868
+ acTimeout = setTimeout(() => fetchAutocomplete(q), 200);
3869
+ }
3870
+
3871
+ async function fetchAutocomplete(q) {
3872
+ try {
3873
+ const res = await fetch(API(`/autocomplete?q=${encodeURIComponent(q)}`));
3874
+ const data = await res.json();
3875
+ renderAutocomplete(data.results || []);
3876
+ } catch {}
3877
+ }
3878
+
3879
+ function renderAutocomplete(items) {
3880
+ const dd = document.getElementById('ac-dropdown');
3881
+ if (!items.length) { hideAutocomplete(); return; }
3882
+ acSelectedIdx = -1;
3883
+ dd.innerHTML = items.map((item, i) => `
3884
+ <div class="ac-item" data-idx="${i}" onmousedown="selectAutocomplete(${i})" onmouseover="acSelectedIdx=${i};highlightAc()">
3885
+ <span class="ac-type ${item.type}">${item.type === 'history' ? 'hist' : item.type === 'library' ? 'lib' : 'tool'}</span>
3886
+ <div class="ac-text">
3887
+ ${escHtml(item.text)}
3888
+ ${item.fullText && item.fullText !== item.text ? `<span class="full">${escHtml(item.fullText.slice(0, 120))}</span>` : ''}
3889
+ </div>
3890
+ </div>
3891
+ `).join('');
3892
+ dd.classList.add('visible');
3893
+ }
3894
+
3895
+ function selectAutocomplete(idx) {
3896
+ const dd = document.getElementById('ac-dropdown');
3897
+ const items = dd.querySelectorAll('.ac-item');
3898
+ if (idx < 0 || idx >= items.length) return;
3899
+ const text = items[idx].querySelector('.ac-text')?.textContent?.trim() || '';
3900
+ const firstLine = text.split('\n')[0].trim();
3901
+ document.getElementById('prompt-search').value = firstLine;
3902
+ hideAutocomplete();
3903
+ loadPrompts();
3904
+ }
3905
+
3906
+ function highlightAc() {
3907
+ const dd = document.getElementById('ac-dropdown');
3908
+ dd.querySelectorAll('.ac-item').forEach((el, i) => {
3909
+ el.classList.toggle('selected', i === acSelectedIdx);
3910
+ });
3911
+ }
3912
+
3913
+ function hideAutocomplete() {
3914
+ document.getElementById('ac-dropdown').classList.remove('visible');
3915
+ acSelectedIdx = -1;
3916
+ }
3917
+
3918
+ function onSearchKeydown(event) {
3919
+ const dd = document.getElementById('ac-dropdown');
3920
+ if (dd.classList.contains('visible')) {
3921
+ const items = dd.querySelectorAll('.ac-item');
3922
+ if (event.key === 'ArrowDown') { event.preventDefault(); acSelectedIdx = Math.min(acSelectedIdx + 1, items.length - 1); highlightAc(); return; }
3923
+ if (event.key === 'ArrowUp') { event.preventDefault(); acSelectedIdx = Math.max(acSelectedIdx - 1, 0); highlightAc(); return; }
3924
+ if (event.key === 'Enter' && acSelectedIdx >= 0) { event.preventDefault(); selectAutocomplete(acSelectedIdx); return; }
3925
+ if (event.key === 'Escape') { hideAutocomplete(); return; }
3926
+ }
3927
+ if (event.key === 'Enter' && aiSearchMode) { event.preventDefault(); runAiSearch(); }
3928
+ }
3929
+
3930
+ // Close autocomplete on click outside
3931
+ document.addEventListener('click', (e) => {
3932
+ if (!e.target.closest('.autocomplete-wrap')) hideAutocomplete();
3933
+ });
3934
+
3935
+ // ============================================================
3936
+ // Harvest Modal
3937
+ // ============================================================
3938
+ async function openHarvestModal() {
3939
+ document.getElementById('harvest-modal').classList.add('visible');
3940
+ document.getElementById('harvest-result').style.display = 'none';
3941
+ try {
3942
+ const res = await fetch(API('/harvest/preview'));
3943
+ const data = await res.json();
3944
+ const el = document.getElementById('harvest-stats');
3945
+ el.innerHTML = `
3946
+ <div class="harvest-stat"><span class="label">Total Sessions</span><span class="value">${data.totalSessions}</span></div>
3947
+ <div class="harvest-stat"><span class="label">Total Size</span><span class="value">${data.totalSizeMB} MB</span></div>
3948
+ <div class="harvest-stat"><span class="label">Modified Since Last Scan</span><span class="value">${data.modifiedSinceLastScan}</span></div>
3949
+ <div class="harvest-stat"><span class="label">Already Harvested</span><span class="value">${data.totalHarvested} messages</span></div>
3950
+ <div class="harvest-stat"><span class="label">Last Scan</span><span class="value">${data.lastScan ? timeAgo(data.lastScan) : 'Never'}</span></div>
3951
+ <div class="harvest-stat"><span class="label">Date Range</span><span class="value">${data.dateRange.oldest ? new Date(data.dateRange.oldest).toLocaleDateString() : '?'} - ${data.dateRange.newest ? new Date(data.dateRange.newest).toLocaleDateString() : '?'}</span></div>
3952
+ <div style="margin-top:8px;font-size:12px;font-weight:600;">Top Projects</div>
3953
+ <div class="harvest-projects">${(data.projects || []).slice(0, 8).map(p => `<div><span>${escHtml(p.name)}</span><span>${p.count} sessions</span></div>`).join('')}</div>
3954
+ `;
3955
+ } catch (e) {
3956
+ document.getElementById('harvest-stats').innerHTML = `<div style="color:var(--red);font-size:12px;">Error loading preview: ${escHtml(e.message)}</div>`;
3957
+ }
3958
+ }
3959
+
3960
+ function closeHarvestModal() {
3961
+ document.getElementById('harvest-modal').classList.remove('visible');
3962
+ }
3963
+
3964
+ async function runHarvest(scope) {
3965
+ const btnId = scope === 'incremental' ? 'harvest-inc-btn' : 'harvest-all-btn';
3966
+ const btn = document.getElementById(btnId);
3967
+ const origText = btn.textContent;
3968
+ btn.disabled = true;
3969
+ btn.textContent = 'Harvesting...';
3970
+ const resultEl = document.getElementById('harvest-result');
3971
+
3972
+ try {
3973
+ const maxDays = document.getElementById('harvest-days').value || undefined;
3974
+ const res = await fetch(API('/harvest'), {
3975
+ method: 'POST',
3976
+ headers: { 'Content-Type': 'application/json' },
3977
+ body: JSON.stringify({ scope, maxDays: maxDays ? parseInt(maxDays) : undefined }),
3978
+ });
3979
+ const data = await res.json();
3980
+ resultEl.style.display = 'block';
3981
+ resultEl.innerHTML = `
3982
+ <div style="color:var(--green);font-weight:600;margin-bottom:4px;">Harvest Complete</div>
3983
+ <div>Files scanned: ${data.filesScanned} / ${data.totalFiles}</div>
3984
+ <div>Messages harvested: ${data.messagesHarvested}</div>
3985
+ `;
3986
+ toast(`Harvested ${data.messagesHarvested} messages from ${data.filesScanned} sessions`, { type: 'success' });
3987
+ } catch (e) {
3988
+ resultEl.style.display = 'block';
3989
+ resultEl.innerHTML = `<div style="color:var(--red);">Error: ${escHtml(e.message)}</div>`;
3990
+ } finally {
3991
+ btn.disabled = false;
3992
+ btn.textContent = origText;
3993
+ }
3994
+ }
3995
+
3996
+ // ============================================================
3997
+ // Copilot Panel
3998
+ // ============================================================
3999
+ let copilotTab = 'similar';
4000
+ let copilotHistory = [];
4001
+
4002
+ function toggleCopilotPanel() {
4003
+ const panel = document.getElementById('copilot-panel');
4004
+ const isActive = panel.classList.toggle('active');
4005
+ document.getElementById('copilot-toggle').classList.toggle('active', isActive);
4006
+ if (isActive && state.currentPromptId) {
4007
+ loadSimilarPrompts();
4008
+ loadPromptSessions();
4009
+ }
4010
+ }
4011
+
4012
+ function switchCopilotTab(tab) {
4013
+ copilotTab = tab;
4014
+ document.querySelectorAll('.copilot-tab').forEach(t => t.classList.toggle('active', t.textContent.toLowerCase().includes(tab)));
4015
+ document.querySelectorAll('.copilot-tab-content').forEach(c => c.style.display = 'none');
4016
+ document.getElementById(`copilot-${tab}`).style.display = '';
4017
+ if (tab === 'frequent') loadFrequentQuestions();
4018
+ }
4019
+
4020
+ async function loadSimilarPrompts() {
4021
+ const el = document.getElementById('copilot-similar');
4022
+ if (!state.currentPromptId) { el.innerHTML = '<div style="padding:12px;text-align:center;color:var(--fg-dim);font-size:12px;">Open a prompt to see similar prompts.</div>'; return; }
4023
+
4024
+ const prompt = state.prompts.find(p => p.id === state.currentPromptId);
4025
+ const text = prompt?.content || prompt?.title || '';
4026
+ if (text.length < 20) { el.innerHTML = '<div style="padding:12px;text-align:center;color:var(--fg-dim);font-size:12px;">Prompt too short for similarity matching.</div>'; return; }
4027
+
4028
+ el.innerHTML = '<div style="padding:12px;text-align:center;color:var(--fg-dim);font-size:12px;">Searching...</div>';
4029
+ try {
4030
+ const res = await fetch(API(`/similar?text=${encodeURIComponent(text.slice(0, 500))}`));
4031
+ const data = await res.json();
4032
+ if (!data.results?.length) { el.innerHTML = '<div style="padding:12px;text-align:center;color:var(--fg-dim);font-size:12px;">No similar prompts found. Try harvesting more sessions.</div>'; return; }
4033
+ el.innerHTML = data.results.map(r => `
4034
+ <div class="similar-item" onclick="${r.promptId ? `openPrompt(${r.promptId})` : ''}">
4035
+ <span class="si-match">${Math.round((r.matchRatio || 0) * 100)}% match</span>
4036
+ <div class="si-title">${escHtml(r.text)}</div>
4037
+ <div class="si-meta">${r.type === 'library' ? 'Library prompt' : r.sessionId ? 'Session ' + r.sessionId.slice(0, 8) : ''} ${r.at ? '· ' + timeAgo(r.at) : ''}</div>
4038
+ </div>
4039
+ `).join('');
4040
+ } catch (e) {
4041
+ el.innerHTML = `<div style="padding:12px;color:var(--red);font-size:12px;">Error: ${escHtml(e.message)}</div>`;
4042
+ }
4043
+ }
4044
+
4045
+ async function loadPromptSessions() {
4046
+ const el = document.getElementById('copilot-sessions');
4047
+ if (!state.currentPromptId) { el.innerHTML = '<div style="padding:12px;text-align:center;color:var(--fg-dim);font-size:12px;">Open a prompt to see sessions.</div>'; return; }
4048
+
4049
+ try {
4050
+ const res = await fetch(API(`/prompts/${state.currentPromptId}/sessions`));
4051
+ const data = await res.json();
4052
+ if (!data.sessions?.length) { el.innerHTML = '<div style="padding:12px;text-align:center;color:var(--fg-dim);font-size:12px;">This prompt hasn\'t been sent to any sessions yet.</div>'; return; }
4053
+ el.innerHTML = data.sessions.map(s => `
4054
+ <div class="similar-item">
4055
+ <div class="si-title" style="cursor:pointer;" onclick="window.open('/#session=${s.session_id}','_blank')">Session ${s.session_id.slice(0, 8)}...</div>
4056
+ <div class="si-meta">${s.source} · ${timeAgo(s.at)}</div>
4057
+ <div style="display:flex;gap:4px;margin-top:4px;">
4058
+ <button class="btn small" style="font-size:9px;padding:1px 6px;${s.outcome === 'success' ? 'background:var(--green);color:#1a1b26;' : ''}" onclick="event.stopPropagation();setOutcomeForSession('${s.session_id}','success')">Success</button>
4059
+ <button class="btn small" style="font-size:9px;padding:1px 6px;${s.outcome === 'partial' ? 'background:var(--yellow);color:#1a1b26;' : ''}" onclick="event.stopPropagation();setOutcomeForSession('${s.session_id}','partial')">Partial</button>
4060
+ <button class="btn small" style="font-size:9px;padding:1px 6px;${s.outcome === 'failed' ? 'background:var(--red);color:#1a1b26;' : ''}" onclick="event.stopPropagation();setOutcomeForSession('${s.session_id}','failed')">Failed</button>
4061
+ </div>
4062
+ </div>
4063
+ `).join('');
4064
+ } catch (e) {
4065
+ el.innerHTML = `<div style="padding:12px;color:var(--red);font-size:12px;">Error: ${escHtml(e.message)}</div>`;
4066
+ }
4067
+ }
4068
+
4069
+ async function setOutcomeForSession(sessionId, outcome) {
4070
+ try {
4071
+ // Find the execution ID for this session's user messages
4072
+ const res = await fetch(API(`/prompt-executions/session/${sessionId}`));
4073
+ const data = await res.json();
4074
+ const userExecs = (data.executions || []).filter(e => e.role === 'user');
4075
+ if (userExecs.length > 0) {
4076
+ await fetch(API(`/prompt-executions/${userExecs[0].id}/outcome`), {
4077
+ method: 'POST',
4078
+ headers: { 'Content-Type': 'application/json' },
4079
+ body: JSON.stringify({ outcome }),
4080
+ });
4081
+ toast(`Outcome set to "${outcome}"`, { type: 'success' });
4082
+ loadPromptSessions(); // Refresh
4083
+ }
4084
+ } catch (e) {
4085
+ toast('Failed to set outcome: ' + e.message, { type: 'error' });
4086
+ }
4087
+ }
4088
+
4089
+ async function sendCopilotMsg() {
4090
+ const input = document.getElementById('copilot-input');
4091
+ const msg = input.value.trim();
4092
+ if (!msg) return;
4093
+ input.value = '';
4094
+
4095
+ const messagesEl = document.getElementById('copilot-messages');
4096
+ messagesEl.innerHTML += `<div class="copilot-msg user">${escHtml(msg)}</div>`;
4097
+ messagesEl.scrollTop = messagesEl.scrollHeight;
4098
+
4099
+ copilotHistory.push({ role: 'user', content: msg });
4100
+
4101
+ try {
4102
+ const res = await fetch(API('/copilot/chat'), {
4103
+ method: 'POST',
4104
+ headers: { 'Content-Type': 'application/json' },
4105
+ body: JSON.stringify({ message: msg, history: copilotHistory.slice(-10) }),
4106
+ });
4107
+ const data = await res.json();
4108
+ const reply = data.reply || 'No response';
4109
+ messagesEl.innerHTML += `<div class="copilot-msg assistant">${escHtml(reply)}</div>`;
4110
+ messagesEl.scrollTop = messagesEl.scrollHeight;
4111
+ copilotHistory.push({ role: 'assistant', content: reply });
4112
+ } catch (e) {
4113
+ messagesEl.innerHTML += `<div class="copilot-msg assistant" style="color:var(--red);">Error: ${escHtml(e.message)}</div>`;
4114
+ }
4115
+ }
4116
+
4117
+ // ============================================================
4118
+ // Frequent Questions (Multi-session surfacing)
4119
+ // ============================================================
4120
+ async function loadFrequentQuestions() {
4121
+ const el = document.getElementById('copilot-frequent');
4122
+ el.innerHTML = '<div style="padding:12px;text-align:center;color:var(--fg-dim);font-size:12px;">Loading...</div>';
4123
+ try {
4124
+ const res = await fetch(API('/frequent-questions?limit=15'));
4125
+ const data = await res.json();
4126
+ if (!data.questions?.length) {
4127
+ el.innerHTML = '<div style="padding:12px;text-align:center;color:var(--fg-dim);font-size:12px;">No frequently asked questions found. Try harvesting more sessions first.</div>';
4128
+ return;
4129
+ }
4130
+ el.innerHTML = data.questions.map(q => `
4131
+ <div class="similar-item" onclick="useFrequentQuestion(this)" data-text="${escAttr(q.text)}">
4132
+ <span class="si-match">${q.sessionCount} sessions</span>
4133
+ <div class="si-title">${escHtml(q.text)}</div>
4134
+ <div class="si-meta">${q.type === 'exact' ? 'Exact match' : 'Similar wording'} · Last: ${timeAgo(q.lastUsed)}</div>
4135
+ </div>
4136
+ `).join('');
4137
+ } catch (e) {
4138
+ el.innerHTML = `<div style="padding:12px;color:var(--red);font-size:12px;">Error: ${escHtml(e.message)}</div>`;
4139
+ }
4140
+ }
4141
+
4142
+ function useFrequentQuestion(el) {
4143
+ const text = el.dataset.text;
4144
+ if (text) {
4145
+ document.getElementById('queue-inline-input').value = text;
4146
+ toast('Question copied to queue input', { type: 'info' });
4147
+ }
4148
+ }
4149
+
4150
+ function escAttr(s) { return (s || '').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
4151
+
4152
+ // ============================================================
4153
+ // Copilot Auto-Suggestion (3s pause + 80 chars gate)
4154
+ // ============================================================
4155
+ let copilotSuggestTimeout = null;
4156
+ let copilotLastSuggestion = null;
4157
+
4158
+ function triggerCopilotSuggestion() {
4159
+ clearTimeout(copilotSuggestTimeout);
4160
+ const text = getPromptPlainText().trim();
4161
+ if (text.length < 80) return; // 80 char gate
4162
+ copilotSuggestTimeout = setTimeout(async () => {
4163
+ try {
4164
+ const res = await fetch(API('/copilot/suggest'), {
4165
+ method: 'POST',
4166
+ headers: { 'Content-Type': 'application/json' },
4167
+ body: JSON.stringify({ text: text.slice(0, 1000) }),
4168
+ });
4169
+ const data = await res.json();
4170
+ if (data && data.suggestion) {
4171
+ copilotLastSuggestion = data;
4172
+ showCopilotSuggestion(data);
4173
+ }
4174
+ } catch {}
4175
+ }, 3000); // 3 second pause
4176
+ }
4177
+
4178
+ function showCopilotSuggestion(data) {
4179
+ const bar = document.getElementById('copilot-suggestion-bar');
4180
+ const textEl = document.getElementById('copilot-suggestion-text');
4181
+ const applyBtn = document.getElementById('copilot-apply-btn');
4182
+ const qualityColors = { good: 'var(--green)', needs_work: 'var(--yellow)', vague: 'var(--red)' };
4183
+ const qColor = qualityColors[data.quality] || 'var(--fg-dim)';
4184
+ textEl.innerHTML = `<span style="color:${qColor};font-weight:600;">${escHtml(data.quality || '')}</span> ${escHtml(data.suggestion || '')}`;
4185
+ applyBtn.style.display = data.improved ? '' : 'none';
4186
+ bar.style.display = '';
4187
+ }
4188
+
4189
+ function toggleCopilotPreview() {
4190
+ const preview = document.getElementById('copilot-preview');
4191
+ const btn = document.getElementById('copilot-preview-btn');
4192
+ if (!preview) return;
4193
+ const visible = preview.style.display !== 'none';
4194
+ if (visible) {
4195
+ preview.style.display = 'none';
4196
+ if (btn) btn.textContent = 'Preview';
4197
+ } else {
4198
+ preview.textContent = copilotLastSuggestion?.improved || '(no improved text)';
4199
+ preview.style.display = 'block';
4200
+ if (btn) btn.textContent = 'Hide';
4201
+ }
4202
+ }
4203
+
4204
+ function applyCopilotSuggestion() {
4205
+ if (copilotLastSuggestion?.improved) {
4206
+ document.getElementById('editor').innerText = copilotLastSuggestion.improved;
4207
+ dismissCopilotSuggestion();
4208
+ toast('Applied improved version from Copilot', { type: 'success' });
4209
+ }
4210
+ }
4211
+
4212
+ function dismissCopilotSuggestion() {
4213
+ document.getElementById('copilot-suggestion-bar').style.display = 'none';
4214
+ const preview = document.getElementById('copilot-preview');
4215
+ if (preview) preview.style.display = 'none';
4216
+ copilotLastSuggestion = null;
4217
+ }
4218
+
4219
+ // ============================================================
4220
+ // Patterns View
4221
+ // ============================================================
4222
+ async function loadPatterns() {
4223
+ const list = document.getElementById('pattern-list');
4224
+ try {
4225
+ const res = await fetch(API('/patterns'));
4226
+ const data = await res.json();
4227
+ const patterns = data.patterns || [];
4228
+ document.getElementById('pattern-count').textContent = `${patterns.length} patterns`;
4229
+ if (!patterns.length) {
4230
+ list.innerHTML = '<div style="padding:16px;text-align:center;color:var(--fg-dim);font-size:12px;">No patterns detected yet. Click "Detect Patterns" to scan your harvested prompts.</div>';
4231
+ return;
4232
+ }
4233
+ list.innerHTML = patterns.map(p => `
4234
+ <div class="pattern-item">
4235
+ <span class="pi-freq">${p.frequency}x</span>
4236
+ <div class="pi-text">${escHtml(p.example_text || p.normalized_text)}</div>
4237
+ <div class="pi-meta">
4238
+ ${p.projects ? JSON.parse(p.projects).slice(0, 2).map(pr => escHtml(pr.replace(/^\/Users\/[^/]+\//, '~/'))).join(', ') : ''}
4239
+ ${p.last_used_at ? ' · Last: ' + timeAgo(p.last_used_at) : ''}
4240
+ </div>
4241
+ </div>
4242
+ `).join('');
4243
+ } catch (e) {
4244
+ list.innerHTML = `<div style="padding:12px;color:var(--red);font-size:12px;">Error: ${escHtml(e.message)}</div>`;
4245
+ }
4246
+ }
4247
+
4248
+ async function detectPatterns() {
4249
+ const btn = document.querySelector('#view-patterns .btn.primary');
4250
+ btn.disabled = true;
4251
+ btn.textContent = 'Detecting...';
4252
+ try {
4253
+ const res = await fetch(API('/patterns/detect'), { method: 'POST' });
4254
+ const data = await res.json();
4255
+ toast(`Detected ${data.count} patterns`, { type: 'success' });
4256
+ loadPatterns();
4257
+ } catch (e) {
4258
+ toast('Pattern detection failed: ' + e.message, { type: 'error' });
4259
+ } finally {
4260
+ btn.disabled = false;
4261
+ btn.textContent = 'Detect Patterns';
4262
+ }
4263
+ }
4264
+
4265
+ // ============================================================
4266
+ // Queue Inline Autocomplete
4267
+ // ============================================================
4268
+ let queueAcTimeout = null;
4269
+ let queueAcIdx = -1;
4270
+
4271
+ function onQueueInputChange() {
4272
+ const q = document.getElementById('queue-inline-input').value.trim();
4273
+ clearTimeout(queueAcTimeout);
4274
+ if (q.length < 3) { hideQueueAc(); return; }
4275
+ queueAcTimeout = setTimeout(async () => {
4276
+ try {
4277
+ const res = await fetch(API(`/autocomplete?q=${encodeURIComponent(q)}`));
4278
+ const data = await res.json();
4279
+ renderQueueAc(data.results || []);
4280
+ } catch {}
4281
+ }, 200);
4282
+ }
4283
+
4284
+ function renderQueueAc(items) {
4285
+ const dd = document.getElementById('queue-ac-dropdown');
4286
+ if (!items.length) { hideQueueAc(); return; }
4287
+ queueAcIdx = -1;
4288
+ dd.innerHTML = items.map((item, i) => `
4289
+ <div class="ac-item" onmousedown="selectQueueAc(${i})">
4290
+ <span class="ac-type ${item.type}">${item.type === 'history' ? 'hist' : item.type === 'library' ? 'lib' : 'tool'}</span>
4291
+ <div class="ac-text">${escHtml(item.text)}</div>
4292
+ </div>
4293
+ `).join('');
4294
+ dd.classList.add('visible');
4295
+ }
4296
+
4297
+ function selectQueueAc(idx) {
4298
+ const dd = document.getElementById('queue-ac-dropdown');
4299
+ const items = dd.querySelectorAll('.ac-item');
4300
+ if (idx < 0 || idx >= items.length) return;
4301
+ const text = items[idx].querySelector('.ac-text')?.textContent?.trim() || '';
4302
+ document.getElementById('queue-inline-input').value = text.split('\n')[0].trim();
4303
+ hideQueueAc();
4304
+ }
4305
+
4306
+ function hideQueueAc() { document.getElementById('queue-ac-dropdown').classList.remove('visible'); queueAcIdx = -1; }
4307
+
4308
+ function onQueueInputKeydown(event) {
4309
+ const dd = document.getElementById('queue-ac-dropdown');
4310
+ if (dd.classList.contains('visible')) {
4311
+ const items = dd.querySelectorAll('.ac-item');
4312
+ if (event.key === 'ArrowDown') { event.preventDefault(); queueAcIdx = Math.min(queueAcIdx + 1, items.length - 1); items.forEach((el, i) => el.classList.toggle('selected', i === queueAcIdx)); return; }
4313
+ if (event.key === 'ArrowUp') { event.preventDefault(); queueAcIdx = Math.max(queueAcIdx - 1, 0); items.forEach((el, i) => el.classList.toggle('selected', i === queueAcIdx)); return; }
4314
+ if (event.key === 'Enter' && queueAcIdx >= 0) { event.preventDefault(); selectQueueAc(queueAcIdx); return; }
4315
+ if (event.key === 'Escape') { hideQueueAc(); return; }
4316
+ }
4317
+ if (event.key === 'Enter') addInlineToQueue();
4318
+ }
4319
+
4320
+ // ============================================================
4321
+ // Lifecycle Status
4322
+ // ============================================================
4323
+ async function onLifecycleChange() {
4324
+ if (!state.currentPromptId) return;
4325
+ const status = document.getElementById('lifecycle-status').value;
4326
+ try {
4327
+ await fetch(API(`/prompts/${state.currentPromptId}`), {
4328
+ method: 'PUT',
4329
+ headers: { 'Content-Type': 'application/json' },
4330
+ body: JSON.stringify({ lifecycle_status: status }),
4331
+ });
4332
+ // Update local state
4333
+ const p = state.prompts.find(pp => pp.id === state.currentPromptId);
4334
+ if (p) p.lifecycle_status = status;
4335
+ renderPromptList();
4336
+ } catch (e) {
4337
+ toast('Failed to update lifecycle status: ' + e.message, { type: 'error' });
4338
+ }
4339
+ }
4340
+
4341
+ // ============================================================
4342
+ // Boot
4343
+ // ============================================================
4344
+ initEditor();
4345
+ connectWebSocket();
4346
+ (async () => {
4347
+ await loadFolders();
4348
+ await loadPrompts();
4349
+ restoreFromHash();
4350
+ })();
4351
+ </script>
4352
+ </body>
4353
+ </html>