@ajvikram/relay 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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +538 -0
  3. package/dist/cli/commands/branch.d.ts +9 -0
  4. package/dist/cli/commands/branch.d.ts.map +1 -0
  5. package/dist/cli/commands/branch.js +69 -0
  6. package/dist/cli/commands/branch.js.map +1 -0
  7. package/dist/cli/commands/diff.d.ts +8 -0
  8. package/dist/cli/commands/diff.d.ts.map +1 -0
  9. package/dist/cli/commands/diff.js +61 -0
  10. package/dist/cli/commands/diff.js.map +1 -0
  11. package/dist/cli/commands/history.d.ts +8 -0
  12. package/dist/cli/commands/history.d.ts.map +1 -0
  13. package/dist/cli/commands/history.js +32 -0
  14. package/dist/cli/commands/history.js.map +1 -0
  15. package/dist/cli/commands/hook.d.ts +6 -0
  16. package/dist/cli/commands/hook.d.ts.map +1 -0
  17. package/dist/cli/commands/hook.js +118 -0
  18. package/dist/cli/commands/hook.js.map +1 -0
  19. package/dist/cli/commands/init.d.ts +4 -0
  20. package/dist/cli/commands/init.d.ts.map +1 -0
  21. package/dist/cli/commands/init.js +41 -0
  22. package/dist/cli/commands/init.js.map +1 -0
  23. package/dist/cli/commands/install.d.ts +18 -0
  24. package/dist/cli/commands/install.d.ts.map +1 -0
  25. package/dist/cli/commands/install.js +203 -0
  26. package/dist/cli/commands/install.js.map +1 -0
  27. package/dist/cli/commands/load.d.ts +9 -0
  28. package/dist/cli/commands/load.d.ts.map +1 -0
  29. package/dist/cli/commands/load.js +69 -0
  30. package/dist/cli/commands/load.js.map +1 -0
  31. package/dist/cli/commands/repos.d.ts +7 -0
  32. package/dist/cli/commands/repos.d.ts.map +1 -0
  33. package/dist/cli/commands/repos.js +53 -0
  34. package/dist/cli/commands/repos.js.map +1 -0
  35. package/dist/cli/commands/save.d.ts +9 -0
  36. package/dist/cli/commands/save.d.ts.map +1 -0
  37. package/dist/cli/commands/save.js +267 -0
  38. package/dist/cli/commands/save.js.map +1 -0
  39. package/dist/cli/commands/status.d.ts +23 -0
  40. package/dist/cli/commands/status.d.ts.map +1 -0
  41. package/dist/cli/commands/status.js +134 -0
  42. package/dist/cli/commands/status.js.map +1 -0
  43. package/dist/cli/commands/ui.d.ts +9 -0
  44. package/dist/cli/commands/ui.d.ts.map +1 -0
  45. package/dist/cli/commands/ui.js +54 -0
  46. package/dist/cli/commands/ui.js.map +1 -0
  47. package/dist/cli/commands/vscode.d.ts +6 -0
  48. package/dist/cli/commands/vscode.d.ts.map +1 -0
  49. package/dist/cli/commands/vscode.js +86 -0
  50. package/dist/cli/commands/vscode.js.map +1 -0
  51. package/dist/cli/index.d.ts +3 -0
  52. package/dist/cli/index.d.ts.map +1 -0
  53. package/dist/cli/index.js +143 -0
  54. package/dist/cli/index.js.map +1 -0
  55. package/dist/format/renderer.d.ts +5 -0
  56. package/dist/format/renderer.d.ts.map +1 -0
  57. package/dist/format/renderer.js +207 -0
  58. package/dist/format/renderer.js.map +1 -0
  59. package/dist/format/schema.d.ts +516 -0
  60. package/dist/format/schema.d.ts.map +1 -0
  61. package/dist/format/schema.js +90 -0
  62. package/dist/format/schema.js.map +1 -0
  63. package/dist/format/template.d.ts +44 -0
  64. package/dist/format/template.d.ts.map +1 -0
  65. package/dist/format/template.js +131 -0
  66. package/dist/format/template.js.map +1 -0
  67. package/dist/git/context.d.ts +23 -0
  68. package/dist/git/context.d.ts.map +1 -0
  69. package/dist/git/context.js +121 -0
  70. package/dist/git/context.js.map +1 -0
  71. package/dist/index.d.ts +8 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +8 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/mcp/server.d.ts +2 -0
  76. package/dist/mcp/server.d.ts.map +1 -0
  77. package/dist/mcp/server.js +164 -0
  78. package/dist/mcp/server.js.map +1 -0
  79. package/dist/server/index.d.ts +12 -0
  80. package/dist/server/index.d.ts.map +1 -0
  81. package/dist/server/index.js +143 -0
  82. package/dist/server/index.js.map +1 -0
  83. package/dist/server/ui.d.ts +2 -0
  84. package/dist/server/ui.d.ts.map +1 -0
  85. package/dist/server/ui.js +797 -0
  86. package/dist/server/ui.js.map +1 -0
  87. package/dist/storage/global.d.ts +40 -0
  88. package/dist/storage/global.d.ts.map +1 -0
  89. package/dist/storage/global.js +126 -0
  90. package/dist/storage/global.js.map +1 -0
  91. package/dist/storage/index.d.ts +59 -0
  92. package/dist/storage/index.d.ts.map +1 -0
  93. package/dist/storage/index.js +253 -0
  94. package/dist/storage/index.js.map +1 -0
  95. package/package.json +54 -0
@@ -0,0 +1,797 @@
1
+ export function getDashboardHtml() {
2
+ return `<!DOCTYPE html>
3
+ <html lang="en" class="dark">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Relay — AI Context Handoff Dashboard</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ darkMode: 'class',
12
+ theme: {
13
+ extend: {
14
+ colors: {
15
+ brand: {
16
+ 50: '#f0fdf4',
17
+ 500: '#22c55e',
18
+ 600: '#16a34a',
19
+ 900: '#14532d',
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+ </script>
26
+ <style>
27
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; background: #020617; color: #f8fafc; }
28
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
29
+ ::-webkit-scrollbar-track { background: #0f172a; }
30
+ ::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
31
+ ::-webkit-scrollbar-thumb:hover { background: #475569; }
32
+ .glass { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(12px); }
33
+ </style>
34
+ </head>
35
+ <body class="bg-slate-950 text-slate-100 min-h-screen flex flex-col font-sans selection:bg-emerald-500 selection:text-slate-950">
36
+
37
+ <!-- Top Navbar -->
38
+ <header class="border-b border-slate-800 bg-slate-900/90 backdrop-blur sticky top-0 z-40">
39
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
40
+ <div class="flex items-center space-x-3">
41
+ <span class="text-2xl">🔄</span>
42
+ <div>
43
+ <span class="text-lg font-bold tracking-tight bg-gradient-to-r from-emerald-400 to-cyan-400 bg-clip-text text-transparent">Relay</span>
44
+ <span class="text-xs text-slate-400 ml-2 px-2 py-0.5 rounded bg-slate-800 border border-slate-700">v0.1.0</span>
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Global Repo Switcher & Navigation Tools -->
49
+ <div class="flex items-center space-x-3" id="repo-meta">
50
+ <div id="repo-select-container" class="hidden items-center gap-1.5">
51
+ <span class="text-xs text-slate-400">📁</span>
52
+ <select id="repo-select" onchange="onRepoChange(this.value)" class="bg-slate-800 border border-slate-700 text-xs font-mono rounded-lg px-2.5 py-1.5 text-slate-200 focus:outline-none focus:border-emerald-500 cursor-pointer">
53
+ <option value="">Loading repos...</option>
54
+ </select>
55
+ </div>
56
+
57
+ <span id="badge-branch" class="text-xs font-mono px-2.5 py-1.5 rounded-full bg-slate-800 border border-slate-700 text-slate-300 flex items-center gap-1.5">
58
+ <span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
59
+ <span id="branch-name">main</span>
60
+ </span>
61
+
62
+ <!-- User Guide Button -->
63
+ <button onclick="openGuideModal()" class="bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 font-medium px-3.5 py-1.5 rounded-lg text-xs transition flex items-center gap-1.5">
64
+ <span>📖</span>
65
+ <span>User Guide</span>
66
+ </button>
67
+
68
+ <button onclick="copyOptimizedPrompt()" class="bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-semibold px-4 py-1.5 rounded-lg text-xs transition shadow-lg shadow-emerald-500/20 flex items-center gap-1.5">
69
+ <span>📋</span>
70
+ <span>Copy Handoff Prompt</span>
71
+ </button>
72
+ </div>
73
+ </div>
74
+ </header>
75
+
76
+ <!-- Main Container -->
77
+ <main class="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
78
+
79
+ <!-- Left / Center Content (2 Columns) -->
80
+ <div class="lg:col-span-2 space-y-6">
81
+
82
+ <!-- Active Goal Banner -->
83
+ <section class="p-6 rounded-xl bg-gradient-to-br from-slate-900 to-slate-900/80 border border-slate-800 shadow-xl relative overflow-hidden">
84
+ <div class="absolute top-0 right-0 p-4 flex gap-2">
85
+ <span id="badge-status" class="px-2.5 py-1 text-xs font-medium rounded-md uppercase tracking-wider bg-amber-500/10 text-amber-400 border border-amber-500/20">In Progress</span>
86
+ <span id="badge-token" class="hidden px-2.5 py-1 text-xs font-medium rounded-md uppercase tracking-wider bg-rose-500/10 text-rose-400 border border-rose-500/20">Token Exhausted</span>
87
+ </div>
88
+ <h2 class="text-xs font-semibold tracking-wider text-slate-400 uppercase mb-2">🎯 Active Session Goal</h2>
89
+ <p id="session-goal" class="text-xl font-medium text-slate-100 leading-snug">Loading handoff...</p>
90
+
91
+ <div class="mt-4 pt-4 border-t border-slate-800/80 flex flex-wrap gap-4 text-xs text-slate-400">
92
+ <div>Origin: <span id="source-platform" class="text-slate-200 font-semibold capitalize">—</span></div>
93
+ <div>Saved: <span id="saved-time" class="text-slate-200">—</span></div>
94
+ <div>HEAD Commit: <span id="head-commit" class="font-mono text-slate-200">—</span></div>
95
+ </div>
96
+ </section>
97
+
98
+ <!-- Next Action Checklist with Pagination -->
99
+ <section class="p-6 rounded-xl bg-slate-900 border border-slate-800 shadow-md">
100
+ <div class="flex items-center justify-between mb-4">
101
+ <h3 class="text-base font-semibold text-slate-200 flex items-center gap-2">
102
+ <span>✅</span> Next Steps for Incoming Agent
103
+ </h3>
104
+ <div class="flex items-center gap-2">
105
+ <span id="next-step-count" class="text-xs px-2 py-0.5 rounded bg-slate-800 text-slate-400 font-mono">0 pending</span>
106
+ <div id="steps-pagination" class="hidden items-center gap-1 text-xs">
107
+ <button onclick="changePage('steps', -1)" id="btn-steps-prev" class="px-2 py-0.5 rounded bg-slate-800 text-slate-300 hover:bg-slate-700 disabled:opacity-30 disabled:cursor-not-allowed">«</button>
108
+ <span id="page-steps-text" class="text-slate-400 font-mono px-1">1/1</span>
109
+ <button onclick="changePage('steps', 1)" id="btn-steps-next" class="px-2 py-0.5 rounded bg-slate-800 text-slate-300 hover:bg-slate-700 disabled:opacity-30 disabled:cursor-not-allowed">»</button>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ <div id="next-steps-list" class="space-y-2.5">
114
+ <div class="text-slate-500 text-sm italic">No next steps defined.</div>
115
+ </div>
116
+ </section>
117
+
118
+ <!-- Architectural Decisions & Constraints -->
119
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
120
+ <!-- Decisions with Pagination -->
121
+ <section class="p-6 rounded-xl bg-slate-900 border border-slate-800 shadow-md flex flex-col">
122
+ <div class="flex items-center justify-between mb-3">
123
+ <h3 class="text-base font-semibold text-slate-200 flex items-center gap-2">
124
+ <span>🏛️</span> Decisions Made
125
+ </h3>
126
+ <div id="decisions-pagination" class="hidden items-center gap-1 text-xs">
127
+ <button onclick="changePage('decisions', -1)" id="btn-decisions-prev" class="px-2 py-0.5 rounded bg-slate-800 text-slate-300 hover:bg-slate-700 disabled:opacity-30 disabled:cursor-not-allowed">«</button>
128
+ <span id="page-decisions-text" class="text-slate-400 font-mono px-1">1/1</span>
129
+ <button onclick="changePage('decisions', 1)" id="btn-decisions-next" class="px-2 py-0.5 rounded bg-slate-800 text-slate-300 hover:bg-slate-700 disabled:opacity-30 disabled:cursor-not-allowed">»</button>
130
+ </div>
131
+ </div>
132
+ <div id="decisions-list" class="space-y-3 flex-1">
133
+ <div class="text-slate-500 text-sm italic">No decisions recorded.</div>
134
+ </div>
135
+ </section>
136
+
137
+ <!-- Hard Constraints -->
138
+ <section class="p-6 rounded-xl bg-slate-900 border border-slate-800 shadow-md flex flex-col">
139
+ <h3 class="text-base font-semibold text-rose-400 flex items-center gap-2 mb-3">
140
+ <span>🚫</span> Non-Negotiable Constraints
141
+ </h3>
142
+ <div id="constraints-list" class="space-y-2 flex-1">
143
+ <div class="text-slate-500 text-sm italic">No constraints specified.</div>
144
+ </div>
145
+ </section>
146
+ </div>
147
+
148
+ <!-- Failed Approaches Graveyard with Pagination -->
149
+ <section class="p-6 rounded-xl bg-slate-900 border border-slate-800 shadow-md">
150
+ <div class="flex items-center justify-between mb-3">
151
+ <h3 class="text-base font-semibold text-amber-400 flex items-center gap-2">
152
+ <span>🪦</span> Failed Approaches (Do Not Repeat)
153
+ </h3>
154
+ <div id="failed-pagination" class="hidden items-center gap-1 text-xs">
155
+ <button onclick="changePage('failed', -1)" id="btn-failed-prev" class="px-2 py-0.5 rounded bg-slate-800 text-slate-300 hover:bg-slate-700 disabled:opacity-30 disabled:cursor-not-allowed">«</button>
156
+ <span id="page-failed-text" class="text-slate-400 font-mono px-1">1/1</span>
157
+ <button onclick="changePage('failed', 1)" id="btn-failed-next" class="px-2 py-0.5 rounded bg-slate-800 text-slate-300 hover:bg-slate-700 disabled:opacity-30 disabled:cursor-not-allowed">»</button>
158
+ </div>
159
+ </div>
160
+ <div id="failed-approaches-list" class="space-y-3">
161
+ <div class="text-slate-500 text-sm italic">No failed approaches documented.</div>
162
+ </div>
163
+ </section>
164
+
165
+ <!-- Live Git Diff Viewer -->
166
+ <section class="p-6 rounded-xl bg-slate-900 border border-slate-800 shadow-md">
167
+ <div class="flex items-center justify-between mb-3">
168
+ <h3 class="text-base font-semibold text-slate-200 flex items-center gap-2">
169
+ <span>📝</span> Workspace Diff Snapshot
170
+ </h3>
171
+ <span id="diff-meta" class="text-xs text-slate-400 font-mono">0 files modified</span>
172
+ </div>
173
+ <pre id="diff-content" class="bg-slate-950 p-4 rounded-lg text-xs font-mono text-slate-300 overflow-x-auto max-h-60 border border-slate-800">No active uncommitted diff.</pre>
174
+ </section>
175
+ </div>
176
+
177
+ <!-- Right Column (Prompt & History Sidebar) -->
178
+ <div class="space-y-6">
179
+
180
+ <!-- Live Prompt Preview & Customizer -->
181
+ <section class="p-6 rounded-xl bg-slate-900 border border-slate-800 shadow-md">
182
+ <h3 class="text-base font-semibold text-slate-200 flex items-center gap-2 mb-2">
183
+ <span>⚡</span> Handoff Prompt Preview
184
+ </h3>
185
+ <p class="text-xs text-slate-400 mb-4">Paste this into Claude, Cursor, Copilot, or Antigravity to resume instantaneously.</p>
186
+
187
+ <div class="space-y-2 mb-4 text-xs text-slate-300">
188
+ <label class="flex items-center gap-2 cursor-pointer">
189
+ <input type="checkbox" id="opt-failed" checked onchange="updatePromptPreview()" class="rounded bg-slate-800 border-slate-700 text-emerald-500 focus:ring-0">
190
+ <span>Include Failed Approaches</span>
191
+ </label>
192
+ <label class="flex items-center gap-2 cursor-pointer">
193
+ <input type="checkbox" id="opt-constraints" checked onchange="updatePromptPreview()" class="rounded bg-slate-800 border-slate-700 text-emerald-500 focus:ring-0">
194
+ <span>Include Constraints</span>
195
+ </label>
196
+ <label class="flex items-center gap-2 cursor-pointer">
197
+ <input type="checkbox" id="opt-files" checked onchange="updatePromptPreview()" class="rounded bg-slate-800 border-slate-700 text-emerald-500 focus:ring-0">
198
+ <span>Include Modified Files</span>
199
+ </label>
200
+ </div>
201
+
202
+ <textarea id="prompt-preview" readonly class="w-full h-56 bg-slate-950 text-emerald-400 font-mono text-xs p-3 rounded-lg border border-slate-800 resize-none focus:outline-none select-all"></textarea>
203
+
204
+ <button onclick="copyOptimizedPrompt()" class="mt-3 w-full bg-slate-800 hover:bg-slate-700 text-slate-200 font-medium py-2 rounded-lg text-xs transition border border-slate-700 flex items-center justify-center gap-2">
205
+ <span>📋 Copy to Clipboard</span>
206
+ </button>
207
+ </section>
208
+
209
+ <!-- Session History Timeline with Pagination -->
210
+ <section class="p-6 rounded-xl bg-slate-900 border border-slate-800 shadow-md">
211
+ <div class="flex items-center justify-between mb-3">
212
+ <h3 class="text-base font-semibold text-slate-200 flex items-center gap-2">
213
+ <span>⏱️</span> Session History
214
+ </h3>
215
+ <div id="history-pagination" class="hidden items-center gap-1 text-xs">
216
+ <button onclick="changePage('history', -1)" id="btn-history-prev" class="px-2 py-0.5 rounded bg-slate-800 text-slate-300 hover:bg-slate-700 disabled:opacity-30 disabled:cursor-not-allowed">«</button>
217
+ <span id="page-history-text" class="text-slate-400 font-mono px-1">1/1</span>
218
+ <button onclick="changePage('history', 1)" id="btn-history-next" class="px-2 py-0.5 rounded bg-slate-800 text-slate-300 hover:bg-slate-700 disabled:opacity-30 disabled:cursor-not-allowed">»</button>
219
+ </div>
220
+ </div>
221
+ <div id="history-timeline" class="space-y-3">
222
+ <div class="text-slate-500 text-sm italic">Loading history...</div>
223
+ </div>
224
+ </section>
225
+
226
+ </div>
227
+ </main>
228
+
229
+ <!-- Interactive User Guide Modal -->
230
+ <div id="guide-modal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-sm hidden opacity-0 transition-opacity duration-200">
231
+ <div class="bg-slate-900 border border-slate-800 rounded-2xl max-w-3xl w-full max-h-[85vh] flex flex-col shadow-2xl overflow-hidden">
232
+ <!-- Modal Header -->
233
+ <div class="p-5 border-b border-slate-800 flex items-center justify-between bg-slate-950/50">
234
+ <div class="flex items-center gap-3">
235
+ <span class="text-2xl">📖</span>
236
+ <div>
237
+ <h2 class="text-lg font-bold text-slate-100">Relay User Guide & Quickstart</h2>
238
+ <p class="text-xs text-slate-400">Everything you need to know about seamless cross-platform AI context handoff</p>
239
+ </div>
240
+ </div>
241
+ <button onclick="closeGuideModal()" class="text-slate-400 hover:text-slate-200 text-xl font-bold p-1 rounded-lg hover:bg-slate-800 transition">✕</button>
242
+ </div>
243
+
244
+ <!-- Tab Navigation -->
245
+ <div class="flex border-b border-slate-800 px-5 pt-2 bg-slate-900 gap-2 text-xs font-medium">
246
+ <button onclick="switchTab('tab-quickstart')" id="btn-tab-quickstart" class="px-3.5 py-2.5 rounded-t-lg border-b-2 border-emerald-500 text-emerald-400 bg-slate-800/60 font-semibold transition">🚀 Quickstart</button>
247
+ <button onclick="switchTab('tab-platforms')" id="btn-tab-platforms" class="px-3.5 py-2.5 rounded-t-lg border-b-2 border-transparent text-slate-400 hover:text-slate-200 hover:bg-slate-800/40 transition">🤖 AI Platforms</button>
248
+ <button onclick="switchTab('tab-cli')" id="btn-tab-cli" class="px-3.5 py-2.5 rounded-t-lg border-b-2 border-transparent text-slate-400 hover:text-slate-200 hover:bg-slate-800/40 transition">💻 CLI Cheat Sheet</button>
249
+ <button onclick="switchTab('tab-mcp')" id="btn-tab-mcp" class="px-3.5 py-2.5 rounded-t-lg border-b-2 border-transparent text-slate-400 hover:text-slate-200 hover:bg-slate-800/40 transition">🔌 MCP Setup</button>
250
+ </div>
251
+
252
+ <!-- Modal Body (Scrollable) -->
253
+ <div class="p-6 overflow-y-auto space-y-6 text-xs text-slate-300 leading-relaxed flex-1">
254
+
255
+ <!-- Tab 1: Quickstart -->
256
+ <div id="tab-quickstart" class="space-y-4">
257
+ <div class="p-4 rounded-xl bg-emerald-950/20 border border-emerald-800/40 text-emerald-300">
258
+ <div class="font-bold text-sm text-emerald-200 mb-1">What is Relay?</div>
259
+ Relay solves AI token exhaustion and platform lock-in. It captures your goal, decisions, constraints, failed approaches, diffs, and next steps into a lightweight, Git-native handoff so another agent on any platform can resume immediately without repeating mistakes.
260
+ </div>
261
+
262
+ <h4 class="font-semibold text-slate-100 text-sm">The 4-Step Relay Workflow:</h4>
263
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
264
+ <div class="p-3.5 rounded-xl bg-slate-950 border border-slate-800">
265
+ <div class="font-bold text-slate-200 mb-1 text-xs flex items-center gap-1.5"><span class="w-4 h-4 rounded-full bg-slate-800 text-center leading-4 font-mono">1</span> Initialize</div>
266
+ <code class="text-emerald-400 font-mono text-[11px] block mt-1">relay init</code>
267
+ <p class="text-slate-400 text-[11px] mt-1">Creates <code class="text-slate-300">.relay/</code> and configures git tracking.</p>
268
+ </div>
269
+ <div class="p-3.5 rounded-xl bg-slate-950 border border-slate-800">
270
+ <div class="font-bold text-slate-200 mb-1 text-xs flex items-center gap-1.5"><span class="w-4 h-4 rounded-full bg-slate-800 text-center leading-4 font-mono">2</span> Install Global Rules</div>
271
+ <code class="text-emerald-400 font-mono text-[11px] block mt-1">relay install all -g</code>
272
+ <p class="text-slate-400 text-[11px] mt-1">AI models in Claude, Cursor, Copilot auto-read handoffs system-wide.</p>
273
+ </div>
274
+ <div class="p-3.5 rounded-xl bg-slate-950 border border-slate-800">
275
+ <div class="font-bold text-slate-200 mb-1 text-xs flex items-center gap-1.5"><span class="w-4 h-4 rounded-full bg-slate-800 text-center leading-4 font-mono">3</span> Save Context</div>
276
+ <code class="text-emerald-400 font-mono text-[11px] block mt-1">relay save</code>
277
+ <p class="text-slate-400 text-[11px] mt-1">When tokens run low, save decisions, constraints, and next steps.</p>
278
+ </div>
279
+ <div class="p-3.5 rounded-xl bg-slate-950 border border-slate-800">
280
+ <div class="font-bold text-slate-200 mb-1 text-xs flex items-center gap-1.5"><span class="w-4 h-4 rounded-full bg-slate-800 text-center leading-4 font-mono">4</span> Resume on New Platform</div>
281
+ <code class="text-emerald-400 font-mono text-[11px] block mt-1">relay load --prompt --clipboard</code>
282
+ <p class="text-slate-400 text-[11px] mt-1">Paste into the new AI chat or let MCP auto-load the context.</p>
283
+ </div>
284
+ </div>
285
+ </div>
286
+
287
+ <!-- Tab 2: AI Platforms -->
288
+ <div id="tab-platforms" class="space-y-4 hidden">
289
+ <div class="space-y-3">
290
+ <div class="p-3.5 rounded-xl bg-slate-950 border border-slate-800">
291
+ <div class="font-bold text-slate-200 text-xs mb-1">🟣 Claude & Claude Code</div>
292
+ <p class="text-slate-400 text-[11px]">Uses <code class="text-slate-300">CLAUDE.md</code> or MCP server. The model automatically checks for <code class="text-slate-300">.relay/handoff.md</code> on start.</p>
293
+ </div>
294
+ <div class="p-3.5 rounded-xl bg-slate-950 border border-slate-800">
295
+ <div class="font-bold text-slate-200 text-xs mb-1">🟦 Cursor & Cursor Composer</div>
296
+ <p class="text-slate-400 text-[11px]">Uses <code class="text-slate-300">.cursor/rules/relay.mdc</code> or VS Code Tasks. Press <kbd class="px-1 py-0.5 rounded bg-slate-800 border border-slate-700">Cmd+Shift+B</kbd> to launch tasks.</p>
297
+ </div>
298
+ <div class="p-3.5 rounded-xl bg-slate-950 border border-slate-800">
299
+ <div class="font-bold text-slate-200 text-xs mb-1">🐙 GitHub Copilot</div>
300
+ <p class="text-slate-400 text-[11px]">Uses <code class="text-slate-300">.github/copilot-instructions.md</code> to guide suggestions and chat responses based on active handoff.</p>
301
+ </div>
302
+ <div class="p-3.5 rounded-xl bg-slate-950 border border-slate-800">
303
+ <div class="font-bold text-slate-200 text-xs mb-1">⚡ Google Antigravity</div>
304
+ <p class="text-slate-400 text-[11px]">Uses <code class="text-slate-300">AGENTS.md</code> or stdio MCP server for agentic pair programming handoffs.</p>
305
+ </div>
306
+ </div>
307
+ </div>
308
+
309
+ <!-- Tab 3: CLI Cheat Sheet -->
310
+ <div id="tab-cli" class="space-y-3 hidden">
311
+ <div class="p-3 bg-slate-950 rounded-xl border border-slate-800 font-mono text-[11px] space-y-2">
312
+ <div><span class="text-emerald-400 font-bold">relay save</span> <span class="text-slate-500"># Interactive guided handoff save</span></div>
313
+ <div><span class="text-emerald-400 font-bold">relay save --agent</span> <span class="text-slate-500"># Ask agent to fill in JSON</span></div>
314
+ <div><span class="text-emerald-400 font-bold">relay load --prompt --clipboard</span> <span class="text-slate-500"># Copy compact prompt to clipboard</span></div>
315
+ <div><span class="text-emerald-400 font-bold">relay status</span> <span class="text-slate-500"># Inspect drift & commit alignment</span></div>
316
+ <div><span class="text-emerald-400 font-bold">relay repos</span> <span class="text-slate-500"># List all registered projects on computer</span></div>
317
+ <div><span class="text-emerald-400 font-bold">relay branch list</span> <span class="text-slate-500"># Manage branch-scoped context</span></div>
318
+ <div><span class="text-emerald-400 font-bold">relay hook install</span> <span class="text-slate-500"># Auto-draft handoffs on git commit</span></div>
319
+ <div><span class="text-emerald-400 font-bold">relay ui --global</span> <span class="text-slate-500"># Launch multi-repo web dashboard</span></div>
320
+ </div>
321
+ </div>
322
+
323
+ <!-- Tab 4: MCP Setup -->
324
+ <div id="tab-mcp" class="space-y-3 hidden">
325
+ <p class="text-slate-400 text-xs">Add Relay to your platform's MCP configuration for 5 programmatic tools (<code class="text-slate-300">read_handoff</code>, <code class="text-slate-300">save_handoff</code>, <code class="text-slate-300">get_git_context</code>, <code class="text-slate-300">check_drift</code>, <code class="text-slate-300">list_history</code>):</p>
326
+ <pre class="bg-slate-950 p-4 rounded-xl border border-slate-800 font-mono text-[11px] text-emerald-300 overflow-x-auto">{
327
+ "mcpServers": {
328
+ "relay": {
329
+ "command": "relay",
330
+ "args": ["serve"]
331
+ }
332
+ }
333
+ }</pre>
334
+ </div>
335
+
336
+ </div>
337
+
338
+ <!-- Modal Footer -->
339
+ <div class="p-4 border-t border-slate-800 bg-slate-950/50 flex justify-end">
340
+ <button onclick="closeGuideModal()" class="bg-slate-800 hover:bg-slate-700 text-slate-200 font-semibold px-4 py-2 rounded-lg text-xs transition">Close Guide</button>
341
+ </div>
342
+ </div>
343
+ </div>
344
+
345
+ <!-- Toast Notification -->
346
+ <div id="toast" class="fixed bottom-6 right-6 bg-emerald-500 text-slate-950 font-semibold px-4 py-2.5 rounded-lg shadow-xl text-sm transition-all duration-300 opacity-0 pointer-events-none transform translate-y-2 flex items-center gap-2 z-50">
347
+ <span>✓</span>
348
+ <span id="toast-msg">Copied to clipboard!</span>
349
+ </div>
350
+
351
+ <script>
352
+ let activeHandoff = null;
353
+ let activeHistory = [];
354
+ let selectedRepo = '';
355
+
356
+ // Pagination states (page index 0-based, pageSize = 4)
357
+ const PAGE_SIZE = 4;
358
+ const pagination = {
359
+ steps: 0,
360
+ decisions: 0,
361
+ failed: 0,
362
+ history: 0,
363
+ };
364
+
365
+ async function loadData() {
366
+ await loadRepos();
367
+ await fetchHandoff();
368
+ await fetchHistory();
369
+ await fetchStatus();
370
+ await fetchDiff();
371
+ }
372
+
373
+ async function loadRepos() {
374
+ try {
375
+ const res = await fetch('/api/repos');
376
+ if (res.ok) {
377
+ const data = await res.json();
378
+ if (data.repos && data.repos.length > 0) {
379
+ const container = document.getElementById('repo-select-container');
380
+ const select = document.getElementById('repo-select');
381
+ select.innerHTML = '';
382
+ data.repos.forEach(r => {
383
+ const opt = document.createElement('option');
384
+ opt.value = r.path;
385
+ opt.innerText = r.name + (r.lastGoal ? ' (' + (r.lastStatus || 'active') + ')' : '');
386
+ if (selectedRepo === r.path || (!selectedRepo && r.path === data.current)) {
387
+ opt.selected = true;
388
+ }
389
+ select.appendChild(opt);
390
+ });
391
+ container.classList.remove('hidden');
392
+ container.classList.add('flex');
393
+ }
394
+ }
395
+ } catch (err) {
396
+ console.error('Failed to load global repos:', err);
397
+ }
398
+ }
399
+
400
+ function onRepoChange(path) {
401
+ selectedRepo = path;
402
+ pagination.steps = 0;
403
+ pagination.decisions = 0;
404
+ pagination.failed = 0;
405
+ pagination.history = 0;
406
+ fetchHandoff();
407
+ fetchHistory();
408
+ fetchStatus();
409
+ fetchDiff();
410
+ }
411
+
412
+ function getRepoParam() {
413
+ return selectedRepo ? '?repo=' + encodeURIComponent(selectedRepo) : '';
414
+ }
415
+
416
+ async function fetchHandoff() {
417
+ try {
418
+ const res = await fetch('/api/handoff' + getRepoParam());
419
+ const data = await res.json();
420
+ activeHandoff = data.handoff;
421
+ renderHandoff(data.handoff);
422
+ updatePromptPreview();
423
+ } catch (err) {
424
+ console.error('Failed to load handoff:', err);
425
+ }
426
+ }
427
+
428
+ async function fetchHistory() {
429
+ try {
430
+ const res = await fetch('/api/history' + getRepoParam());
431
+ const data = await res.json();
432
+ activeHistory = data.entries || [];
433
+ renderHistory();
434
+ } catch (err) {
435
+ console.error('Failed to load history:', err);
436
+ }
437
+ }
438
+
439
+ async function fetchStatus() {
440
+ try {
441
+ const res = await fetch('/api/status' + getRepoParam());
442
+ const data = await res.json();
443
+ renderStatus(data);
444
+ } catch (err) {
445
+ console.error('Failed to load status:', err);
446
+ }
447
+ }
448
+
449
+ async function fetchDiff() {
450
+ try {
451
+ const res = await fetch('/api/diff' + getRepoParam());
452
+ const data = await res.json();
453
+ const diffEl = document.getElementById('diff-content');
454
+ if (data.raw_diff && data.raw_diff.trim().length > 0) {
455
+ diffEl.innerText = data.raw_diff;
456
+ } else {
457
+ diffEl.innerText = 'No uncommitted changes in current workspace.';
458
+ }
459
+ } catch (err) {
460
+ console.error('Failed to load diff:', err);
461
+ }
462
+ }
463
+
464
+ // ─── Pagination Engine ──────────────────────────────────────────────────
465
+
466
+ function changePage(target, delta) {
467
+ let itemsLength = 0;
468
+ if (target === 'steps') itemsLength = activeHandoff?.next_steps?.length || 0;
469
+ if (target === 'decisions') itemsLength = activeHandoff?.decisions?.length || 0;
470
+ if (target === 'failed') itemsLength = activeHandoff?.failed_approaches?.length || 0;
471
+ if (target === 'history') itemsLength = activeHistory.length;
472
+
473
+ const totalPages = Math.ceil(itemsLength / PAGE_SIZE) || 1;
474
+ pagination[target] = Math.max(0, Math.min(pagination[target] + delta, totalPages - 1));
475
+
476
+ if (target === 'steps') renderSteps();
477
+ if (target === 'decisions') renderDecisions();
478
+ if (target === 'failed') renderFailed();
479
+ if (target === 'history') renderHistory();
480
+ }
481
+
482
+ function renderSteps() {
483
+ const steps = activeHandoff?.next_steps || [];
484
+ const totalPages = Math.ceil(steps.length / PAGE_SIZE) || 1;
485
+ const currentPage = Math.min(pagination.steps, totalPages - 1);
486
+
487
+ const container = document.getElementById('next-steps-list');
488
+ const pagEl = document.getElementById('steps-pagination');
489
+ document.getElementById('next-step-count').innerText = steps.length + ' pending';
490
+
491
+ if (steps.length > PAGE_SIZE) {
492
+ pagEl.classList.remove('hidden');
493
+ pagEl.classList.add('flex');
494
+ document.getElementById('page-steps-text').innerText = (currentPage + 1) + '/' + totalPages;
495
+ document.getElementById('btn-steps-prev').disabled = currentPage === 0;
496
+ document.getElementById('btn-steps-next').disabled = currentPage >= totalPages - 1;
497
+ } else {
498
+ pagEl.classList.add('hidden');
499
+ }
500
+
501
+ if (steps.length > 0) {
502
+ const start = currentPage * PAGE_SIZE;
503
+ const pageItems = steps.slice(start, start + PAGE_SIZE);
504
+ container.innerHTML = pageItems.map((s, idx) => \`
505
+ <div class="flex items-start gap-3 p-3 rounded-lg bg-slate-950 border border-slate-800">
506
+ <span class="font-mono text-xs text-emerald-400 bg-emerald-950/60 px-2 py-0.5 rounded border border-emerald-800/50">\${start + idx + 1}</span>
507
+ <div class="flex-1">
508
+ <div class="text-sm font-medium text-slate-200">\${s.task}</div>
509
+ \${s.context ? \`<div class="text-xs text-slate-400 mt-0.5">\${s.context}</div>\` : ''}
510
+ </div>
511
+ <span class="text-xs uppercase font-mono px-2 py-0.5 rounded \${s.priority === 'immediate' ? 'bg-rose-950 text-rose-300 border border-rose-800' : 'bg-slate-800 text-slate-400'}">\${s.priority || 'next'}</span>
512
+ </div>
513
+ \`).join('');
514
+ } else {
515
+ container.innerHTML = '<div class="text-slate-500 text-sm italic">No next steps defined.</div>';
516
+ }
517
+ }
518
+
519
+ function renderDecisions() {
520
+ const decisions = activeHandoff?.decisions || [];
521
+ const totalPages = Math.ceil(decisions.length / PAGE_SIZE) || 1;
522
+ const currentPage = Math.min(pagination.decisions, totalPages - 1);
523
+
524
+ const container = document.getElementById('decisions-list');
525
+ const pagEl = document.getElementById('decisions-pagination');
526
+
527
+ if (decisions.length > PAGE_SIZE) {
528
+ pagEl.classList.remove('hidden');
529
+ pagEl.classList.add('flex');
530
+ document.getElementById('page-decisions-text').innerText = (currentPage + 1) + '/' + totalPages;
531
+ document.getElementById('btn-decisions-prev').disabled = currentPage === 0;
532
+ document.getElementById('btn-decisions-next').disabled = currentPage >= totalPages - 1;
533
+ } else {
534
+ pagEl.classList.add('hidden');
535
+ }
536
+
537
+ if (decisions.length > 0) {
538
+ const start = currentPage * PAGE_SIZE;
539
+ const pageItems = decisions.slice(start, start + PAGE_SIZE);
540
+ container.innerHTML = pageItems.map(d => \`
541
+ <div class="p-3 rounded-lg bg-slate-950 border border-slate-800 text-xs space-y-1">
542
+ <div class="font-semibold text-slate-200">\${d.what}</div>
543
+ <div class="text-slate-400"><span class="text-slate-500">Why:</span> \${d.why}</div>
544
+ \${d.evidence ? \`<div class="text-slate-500 font-mono">\${d.evidence}</div>\` : ''}
545
+ </div>
546
+ \`).join('');
547
+ } else {
548
+ container.innerHTML = '<div class="text-slate-500 text-sm italic">No decisions recorded.</div>';
549
+ }
550
+ }
551
+
552
+ function renderFailed() {
553
+ const failed = activeHandoff?.failed_approaches || [];
554
+ const totalPages = Math.ceil(failed.length / PAGE_SIZE) || 1;
555
+ const currentPage = Math.min(pagination.failed, totalPages - 1);
556
+
557
+ const container = document.getElementById('failed-approaches-list');
558
+ const pagEl = document.getElementById('failed-pagination');
559
+
560
+ if (failed.length > PAGE_SIZE) {
561
+ pagEl.classList.remove('hidden');
562
+ pagEl.classList.add('flex');
563
+ document.getElementById('page-failed-text').innerText = (currentPage + 1) + '/' + totalPages;
564
+ document.getElementById('btn-failed-prev').disabled = currentPage === 0;
565
+ document.getElementById('btn-failed-next').disabled = currentPage >= totalPages - 1;
566
+ } else {
567
+ pagEl.classList.add('hidden');
568
+ }
569
+
570
+ if (failed.length > 0) {
571
+ const start = currentPage * PAGE_SIZE;
572
+ const pageItems = failed.slice(start, start + PAGE_SIZE);
573
+ container.innerHTML = pageItems.map(f => \`
574
+ <div class="p-3 rounded-lg bg-amber-950/20 border border-amber-900/30 text-xs space-y-1">
575
+ <div class="font-semibold text-amber-300">\${f.approach}</div>
576
+ <div class="text-slate-400"><span class="text-amber-500/80">Reason:</span> \${f.why_failed}</div>
577
+ \${f.alternative_suggested ? \`<div class="text-emerald-400"><span class="text-slate-500">Alternative:</span> \${f.alternative_suggested}</div>\` : ''}
578
+ </div>
579
+ \`).join('');
580
+ } else {
581
+ container.innerHTML = '<div class="text-slate-500 text-sm italic">No failed approaches documented.</div>';
582
+ }
583
+ }
584
+
585
+ function renderHandoff(h) {
586
+ if (!h) {
587
+ document.getElementById('session-goal').innerText = 'No active handoff document found in this repository.';
588
+ document.getElementById('source-platform').innerText = '—';
589
+ document.getElementById('saved-time').innerText = '—';
590
+ document.getElementById('head-commit').innerText = '—';
591
+ document.getElementById('branch-name').innerText = 'main';
592
+ document.getElementById('next-steps-list').innerHTML = '<div class="text-slate-500 text-sm italic">No active handoff. Run relay save to record context.</div>';
593
+ document.getElementById('decisions-list').innerHTML = '<div class="text-slate-500 text-sm italic">No decisions recorded.</div>';
594
+ document.getElementById('constraints-list').innerHTML = '<div class="text-slate-500 text-sm italic">No constraints specified.</div>';
595
+ document.getElementById('failed-approaches-list').innerHTML = '<div class="text-slate-500 text-sm italic">No failed approaches documented.</div>';
596
+ document.getElementById('diff-meta').innerText = '0 files modified';
597
+ document.getElementById('prompt-preview').value = 'No handoff available.';
598
+ return;
599
+ }
600
+
601
+ document.getElementById('session-goal').innerText = h.session?.goal || 'Active task';
602
+ document.getElementById('source-platform').innerText = h.source_platform || 'other';
603
+ document.getElementById('saved-time').innerText = h.timestamp ? new Date(h.timestamp).toLocaleTimeString() : '—';
604
+ document.getElementById('head-commit').innerText = (h.repository?.head_commit || 'unknown').slice(0, 7);
605
+ document.getElementById('branch-name').innerText = h.repository?.branch || 'main';
606
+
607
+ // Status badge
608
+ const badgeStatus = document.getElementById('badge-status');
609
+ const statusText = (h.session?.status || 'in_progress').replace(/_/g, ' ');
610
+ badgeStatus.innerText = statusText;
611
+ if (h.session?.status === 'blocked') {
612
+ badgeStatus.className = 'px-2.5 py-1 text-xs font-medium rounded-md uppercase tracking-wider bg-rose-500/10 text-rose-400 border border-rose-500/20';
613
+ } else if (h.session?.status === 'done') {
614
+ badgeStatus.className = 'px-2.5 py-1 text-xs font-medium rounded-md uppercase tracking-wider bg-emerald-500/10 text-emerald-400 border border-emerald-500/20';
615
+ } else {
616
+ badgeStatus.className = 'px-2.5 py-1 text-xs font-medium rounded-md uppercase tracking-wider bg-amber-500/10 text-amber-400 border border-amber-500/20';
617
+ }
618
+
619
+ // Token exhausted banner
620
+ const badgeToken = document.getElementById('badge-token');
621
+ if (h.session?.token_exhausted) {
622
+ badgeToken.classList.remove('hidden');
623
+ } else {
624
+ badgeToken.classList.add('hidden');
625
+ }
626
+
627
+ // Render paginated lists
628
+ renderSteps();
629
+ renderDecisions();
630
+ renderFailed();
631
+
632
+ // Constraints
633
+ const constraintsContainer = document.getElementById('constraints-list');
634
+ if (h.constraints && h.constraints.length > 0) {
635
+ constraintsContainer.innerHTML = h.constraints.map(c => \`
636
+ <div class="p-2.5 rounded-lg bg-rose-950/20 border border-rose-900/30 text-xs text-rose-300 flex items-start gap-2">
637
+ <span>⚠</span>
638
+ <span>\${c}</span>
639
+ </div>
640
+ \`).join('');
641
+ } else {
642
+ constraintsContainer.innerHTML = '<div class="text-slate-500 text-sm italic">No constraints specified.</div>';
643
+ }
644
+
645
+ // Files modified meta
646
+ document.getElementById('diff-meta').innerText = (h.files_modified?.length || 0) + ' files modified';
647
+ }
648
+
649
+ function renderHistory() {
650
+ const entries = activeHistory;
651
+ const totalPages = Math.ceil(entries.length / PAGE_SIZE) || 1;
652
+ const currentPage = Math.min(pagination.history, totalPages - 1);
653
+
654
+ const container = document.getElementById('history-timeline');
655
+ const pagEl = document.getElementById('history-pagination');
656
+
657
+ if (entries.length > PAGE_SIZE) {
658
+ pagEl.classList.remove('hidden');
659
+ pagEl.classList.add('flex');
660
+ document.getElementById('page-history-text').innerText = (currentPage + 1) + '/' + totalPages;
661
+ document.getElementById('btn-history-prev').disabled = currentPage === 0;
662
+ document.getElementById('btn-history-next').disabled = currentPage >= totalPages - 1;
663
+ } else {
664
+ pagEl.classList.add('hidden');
665
+ }
666
+
667
+ if (entries.length > 0) {
668
+ const start = currentPage * PAGE_SIZE;
669
+ const pageItems = entries.slice(start, start + PAGE_SIZE);
670
+ container.innerHTML = pageItems.map(e => \`
671
+ <div class="p-3 rounded-lg bg-slate-950 border border-slate-800 text-xs space-y-1">
672
+ <div class="flex items-center justify-between">
673
+ <span class="font-mono text-emerald-400">\${e.id}</span>
674
+ <span class="text-slate-500">\${e.timestamp ? new Date(e.timestamp).toLocaleTimeString() : ''}</span>
675
+ </div>
676
+ <div class="text-slate-300 font-medium truncate">\${e.goal}</div>
677
+ <div class="text-slate-500 text-[11px] capitalize">Origin: \${e.source_platform} • \${e.status}</div>
678
+ </div>
679
+ \`).join('');
680
+ } else {
681
+ container.innerHTML = '<div class="text-slate-500 text-sm italic">No past sessions recorded.</div>';
682
+ }
683
+ }
684
+
685
+ function renderStatus(status) {
686
+ if (!status || !status.hasHandoff) return;
687
+ const branchNameEl = document.getElementById('branch-name');
688
+ const baseBranch = (activeHandoff && activeHandoff.repository?.branch) || 'main';
689
+ if (!status.branchMatch) {
690
+ branchNameEl.innerText = baseBranch + ' (mismatch!)';
691
+ document.getElementById('badge-branch').classList.add('border-amber-500/50', 'text-amber-300');
692
+ } else {
693
+ branchNameEl.innerText = baseBranch;
694
+ document.getElementById('badge-branch').classList.remove('border-amber-500/50', 'text-amber-300');
695
+ }
696
+ }
697
+
698
+ function updatePromptPreview() {
699
+ if (!activeHandoff) {
700
+ document.getElementById('prompt-preview').value = 'No handoff available.';
701
+ return;
702
+ }
703
+ const incFailed = document.getElementById('opt-failed').checked;
704
+ const incConstraints = document.getElementById('opt-constraints').checked;
705
+ const incFiles = document.getElementById('opt-files').checked;
706
+
707
+ const lines = [];
708
+ lines.push('## RELAY CONTEXT HANDOFF');
709
+ lines.push(\`You are continuing work from \${activeHandoff.source_platform || 'previous session'}.\`);
710
+ lines.push(\`**GOAL:** \${activeHandoff.session?.goal || 'Ongoing work'}\`);
711
+ lines.push(\`**STATUS:** \${activeHandoff.session?.status || 'in_progress'}\`);
712
+ lines.push(\`**BRANCH:** \${activeHandoff.repository?.branch || 'main'} @ \${(activeHandoff.repository?.head_commit || '').slice(0, 7)}\`);
713
+ lines.push('');
714
+
715
+ if (incFiles && activeHandoff.files_modified?.length) {
716
+ lines.push('**FILES CHANGED:**');
717
+ activeHandoff.files_modified.forEach(f => lines.push(\`- \${f.path} (\${f.status}): \${f.summary}\`));
718
+ lines.push('');
719
+ }
720
+
721
+ if (incConstraints && activeHandoff.constraints?.length) {
722
+ lines.push('**HARD CONSTRAINTS:**');
723
+ activeHandoff.constraints.forEach(c => lines.push(\`- \${c}\`));
724
+ lines.push('');
725
+ }
726
+
727
+ if (incFailed && activeHandoff.failed_approaches?.length) {
728
+ lines.push('**FAILED APPROACHES (DO NOT REPEAT):**');
729
+ activeHandoff.failed_approaches.forEach(f => lines.push(\`- \${f.approach}: \${f.why_failed}\`));
730
+ lines.push('');
731
+ }
732
+
733
+ if (activeHandoff.next_steps?.length) {
734
+ lines.push('**IMMEDIATE NEXT STEPS:**');
735
+ activeHandoff.next_steps.forEach((s, i) => lines.push(\`\${i + 1}. \${s.task}\`));
736
+ lines.push('');
737
+ }
738
+
739
+ lines.push('**BEGIN** — Acknowledge this context and execute the first next step.');
740
+ document.getElementById('prompt-preview').value = lines.join('\\n');
741
+ }
742
+
743
+ async function copyOptimizedPrompt() {
744
+ const text = document.getElementById('prompt-preview').value;
745
+ if (!text) return;
746
+ try {
747
+ await navigator.clipboard.writeText(text);
748
+ showToast('Handoff prompt copied to clipboard!');
749
+ } catch {
750
+ showToast('Copied to clipboard');
751
+ }
752
+ }
753
+
754
+ function showToast(msg) {
755
+ const toast = document.getElementById('toast');
756
+ document.getElementById('toast-msg').innerText = msg;
757
+ toast.classList.remove('opacity-0', 'pointer-events-none');
758
+ setTimeout(() => {
759
+ toast.classList.add('opacity-0', 'pointer-events-none');
760
+ }, 2500);
761
+ }
762
+
763
+ // ─── Modal Management ───────────────────────────────────────────────────
764
+
765
+ function openGuideModal() {
766
+ const modal = document.getElementById('guide-modal');
767
+ modal.classList.remove('hidden');
768
+ setTimeout(() => modal.classList.remove('opacity-0'), 10);
769
+ }
770
+
771
+ function closeGuideModal() {
772
+ const modal = document.getElementById('guide-modal');
773
+ modal.classList.add('opacity-0');
774
+ setTimeout(() => modal.classList.add('hidden'), 200);
775
+ }
776
+
777
+ function switchTab(tabId) {
778
+ const tabs = ['tab-quickstart', 'tab-platforms', 'tab-cli', 'tab-mcp'];
779
+ tabs.forEach(t => {
780
+ const el = document.getElementById(t);
781
+ const btn = document.getElementById('btn-' + t);
782
+ if (t === tabId) {
783
+ el.classList.remove('hidden');
784
+ btn.className = 'px-3.5 py-2.5 rounded-t-lg border-b-2 border-emerald-500 text-emerald-400 bg-slate-800/60 font-semibold transition';
785
+ } else {
786
+ el.classList.add('hidden');
787
+ btn.className = 'px-3.5 py-2.5 rounded-t-lg border-b-2 border-transparent text-slate-400 hover:text-slate-200 hover:bg-slate-800/40 transition';
788
+ }
789
+ });
790
+ }
791
+
792
+ window.addEventListener('DOMContentLoaded', loadData);
793
+ </script>
794
+ </body>
795
+ </html>`;
796
+ }
797
+ //# sourceMappingURL=ui.js.map