Shivam311 commited on
Commit
330856b
·
verified ·
1 Parent(s): 4e91696

Deploy ROCmPilot Space

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +9 -0
  2. Dockerfile +32 -0
  3. README.md +349 -6
  4. TECHNICAL_WALKTHROUGH.md +461 -0
  5. components.json +25 -0
  6. eslint.config.mjs +18 -0
  7. next.config.ts +7 -0
  8. package-lock.json +0 -0
  9. package.json +43 -0
  10. postcss.config.mjs +7 -0
  11. public/file.svg +1 -0
  12. public/globe.svg +1 -0
  13. public/next.svg +1 -0
  14. public/vercel.svg +1 -0
  15. public/window.svg +1 -0
  16. src/app/api/report/route.ts +144 -0
  17. src/app/api/runs/[runId]/route.ts +16 -0
  18. src/app/api/runs/route.ts +19 -0
  19. src/app/favicon.ico +0 -0
  20. src/app/globals.css +131 -0
  21. src/app/layout.tsx +37 -0
  22. src/app/page.tsx +5 -0
  23. src/components/ai-elements/code-block.tsx +556 -0
  24. src/components/ai-elements/message.tsx +360 -0
  25. src/components/ai-elements/terminal.tsx +273 -0
  26. src/components/ai-elements/tool.tsx +173 -0
  27. src/components/rocmpilot-dashboard.tsx +907 -0
  28. src/components/ui/alert.tsx +76 -0
  29. src/components/ui/badge.tsx +49 -0
  30. src/components/ui/button-group.tsx +83 -0
  31. src/components/ui/button.tsx +67 -0
  32. src/components/ui/card.tsx +103 -0
  33. src/components/ui/collapsible.tsx +33 -0
  34. src/components/ui/input.tsx +19 -0
  35. src/components/ui/label.tsx +24 -0
  36. src/components/ui/progress.tsx +31 -0
  37. src/components/ui/scroll-area.tsx +55 -0
  38. src/components/ui/select.tsx +192 -0
  39. src/components/ui/separator.tsx +28 -0
  40. src/components/ui/skeleton.tsx +13 -0
  41. src/components/ui/table.tsx +116 -0
  42. src/components/ui/tabs.tsx +90 -0
  43. src/components/ui/textarea.tsx +18 -0
  44. src/components/ui/tooltip.tsx +57 -0
  45. src/lib/rocmpilot/data.ts +1102 -0
  46. src/lib/rocmpilot/github-scanner.ts +462 -0
  47. src/lib/rocmpilot/github-url.ts +47 -0
  48. src/lib/rocmpilot/memory-ids.ts +6 -0
  49. src/lib/rocmpilot/store.ts +23 -0
  50. src/lib/rocmpilot/synap-memory.ts +316 -0
.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .next
3
+ node_modules
4
+ npm-debug.log
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+ Dockerfile
9
+ README.md.bak
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine AS deps
2
+
3
+ WORKDIR /app
4
+ COPY package.json package-lock.json ./
5
+ RUN npm ci
6
+
7
+ FROM node:20-alpine AS builder
8
+
9
+ WORKDIR /app
10
+ ENV NEXT_TELEMETRY_DISABLED=1
11
+ COPY --from=deps /app/node_modules ./node_modules
12
+ COPY . .
13
+ RUN npm run build
14
+
15
+ FROM node:20-alpine AS runner
16
+
17
+ WORKDIR /app
18
+ ENV NODE_ENV=production
19
+ ENV NEXT_TELEMETRY_DISABLED=1
20
+ ENV PORT=7860
21
+ ENV HOSTNAME=0.0.0.0
22
+
23
+ RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
24
+
25
+ COPY --from=builder /app/public ./public
26
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
27
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
28
+
29
+ USER nextjs
30
+ EXPOSE 7860
31
+
32
+ CMD ["node", "server.js"]
README.md CHANGED
@@ -1,10 +1,353 @@
1
  ---
2
- title: Rocmpilot
3
- emoji: 🐠
4
- colorFrom: gray
5
- colorTo: green
6
  sdk: docker
7
- pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: ROCmPilot
 
 
 
3
  sdk: docker
4
+ app_port: 7860
5
+ short_description: Agentic ROCm migration dashboard.
6
  ---
7
 
8
+ # ROCmPilot
9
+
10
+ ROCmPilot is a multi-agent developer tool that audits AI repositories and produces an AMD ROCm migration path for PyTorch, vLLM, and agentic workloads.
11
+
12
+ > ROCmPilot helps teams move from CUDA/NVIDIA assumptions to AMD ROCm readiness by scanning a repo, identifying blockers, proposing patches, preparing validation commands, and generating a technical/business migration report.
13
+
14
+ The project is built for the **AMD Developer Hackathon, Track 1: AI Agents & Agentic Workflows**. It uses Hugging Face as the temporary model and training layer now, and is designed to switch to AMD Developer Cloud + MI300X + ROCm/vLLM when access is available.
15
+
16
+ Read the full build story, architecture diagrams, demo script, and implementation details in the **[Technical Walkthrough](./TECHNICAL_WALKTHROUGH.md)**.
17
+
18
+ ## Why This Exists
19
+
20
+ Many AI teams want AMD GPU optionality, but their codebases quietly assume NVIDIA:
21
+
22
+ - `nvidia/cuda` Docker images
23
+ - `torch.device("cuda")` and `.cuda()` calls scattered across code
24
+ - CUDA-specific package pins
25
+ - `nvidia-smi`, `CUDA_VISIBLE_DEVICES`, or `--gpus all` scripts
26
+ - vLLM launch scripts without ROCm/MI300X validation knobs
27
+ - benchmarks that omit backend, memory, tokens/sec, and reproducibility evidence
28
+
29
+ ROCmPilot turns that migration problem into an agentic workflow that feels like a product, not a checklist.
30
+
31
+ ## What It Does Today
32
+
33
+ - Accepts a curated sample workload or a **public GitHub repository URL**.
34
+ - Scans relevant files from the repo using the GitHub API.
35
+ - Detects CUDA/NVIDIA assumptions.
36
+ - Produces ROCm-focused findings and patch previews.
37
+ - Shows a five-agent progress timeline.
38
+ - Shows an **Agent War Room** where the task lead asks other agents for input, agents reply to each other, and shared memory records reusable decisions.
39
+ - Persists the agent transcript to **Maximem Synap** long-context memory when `SYNAP_API_KEY` is configured, with local fallback when it is not.
40
+ - Generates terminal-style migration logs.
41
+ - Builds an AMD-readiness benchmark profile.
42
+ - Generates a final report using this backend priority:
43
+ 1. AMD ROCm/vLLM endpoint, when `AMD_QWEN_BASE_URL` is configured
44
+ 2. Hugging Face Inference Router, when `HF_TOKEN` is configured
45
+ 3. Static fallback report, so the demo never breaks
46
+
47
+ ## Agent Workflow
48
+
49
+ 1. **Repo Doctor Agent**
50
+ - Scans Dockerfiles, Python files, requirements, scripts, benchmark files, and vLLM-related configs.
51
+ - Flags CUDA/NVIDIA assumptions and missing validation evidence.
52
+
53
+ 2. **Migration Planner Agent**
54
+ - Converts findings into ROCm migration recommendations.
55
+ - Produces patch previews such as `Dockerfile.rocm`, `requirements-rocm.txt`, device resolvers, and vLLM serve scripts.
56
+
57
+ 3. **Build Runner Agent**
58
+ - Prepares the build/test story.
59
+ - In the current version, it does not mutate the target repo. It generates the commands and evidence plan.
60
+
61
+ 4. **Benchmark Agent**
62
+ - Produces an AMD-readiness profile.
63
+ - Current benchmark values are estimates until live AMD Developer Cloud validation is connected.
64
+
65
+ 5. **Report Agent**
66
+ - Generates a judge-ready report explaining technical findings, AMD GPU path, business value, and next steps.
67
+ - Can use Hugging Face now and AMD-hosted Qwen later.
68
+
69
+ ### Agent War Room and Long-Context Memory
70
+
71
+ Each task has one lead agent, but the lead does not work alone. The lead asks the other agents for objections, validation criteria, benchmark provenance, or report framing. Their discussion is rendered as routed messages such as `Repo Doctor -> Build Runner` and `Migration Planner -> Build Runner`.
72
+
73
+ The agents also write reusable memory during the run:
74
+
75
+ - device resolution patterns
76
+ - ROCm acceptance checks
77
+ - container split decisions
78
+ - benchmark provenance rules
79
+
80
+ When Synap is configured, the Report Agent stores the whole war-room transcript as an `ai-chat-conversation`, retrieves scoped user context, and injects that context into the final report prompt. This lets ROCmPilot remember migration decisions across runs instead of rediscovering the same patterns every session.
81
+
82
+ If Synap credentials or runtime setup are missing, ROCmPilot falls back to reconstructed local memory and marks the UI as `Synap Memory: Local fallback`. The demo still completes.
83
+
84
+ ## Architecture
85
+
86
+ ```text
87
+ Browser UI
88
+ |
89
+ | POST /api/runs
90
+ v
91
+ Stateless Run ID
92
+ |
93
+ | GET /api/runs/[runId]
94
+ v
95
+ Repo Doctor + GitHub Scanner
96
+ |
97
+ v
98
+ Findings + Patch Previews + Logs + Agent Memory + Benchmark Profile
99
+ |
100
+ | POST /api/report
101
+ v
102
+ Report Agent
103
+ |
104
+ | optional memory layer
105
+ v
106
+ Maximem Synap long-context memory -> local fallback memory
107
+ |
108
+ | priority order
109
+ v
110
+ AMD ROCm/vLLM -> Hugging Face Router -> Static fallback
111
+ ```
112
+
113
+ The run system is stateless so it works on Vercel serverless functions without a database. The run ID encodes the start time, mode, and target. For the polished production version, real long-running AMD jobs should use persistent storage and a queue.
114
+
115
+ ## Tech Stack
116
+
117
+ - Next.js App Router
118
+ - TypeScript
119
+ - Tailwind CSS
120
+ - shadcn/ui
121
+ - AI Elements for markdown, code, and terminal rendering
122
+ - Hugging Face Inference Router for temporary report generation
123
+ - Hugging Face Jobs/TRL for future LoRA fine-tuning
124
+ - Maximem Synap for persistent long-context agent memory
125
+ - AMD ROCm/vLLM endpoint support for the final compute story
126
+
127
+ ## Local Development
128
+
129
+ ```bash
130
+ npm install
131
+ npm run dev
132
+ ```
133
+
134
+ Open:
135
+
136
+ ```text
137
+ http://localhost:3000
138
+ ```
139
+
140
+ Run checks:
141
+
142
+ ```bash
143
+ npm run lint
144
+ npm run build
145
+ ```
146
+
147
+ Optional Synap runtime setup for local or worker deployments:
148
+
149
+ ```bash
150
+ npm run synap:setup
151
+ ```
152
+
153
+ The Synap JS SDK uses a Python bridge. Vercel can still deploy the app without this setup because the report route falls back safely when Synap cannot initialize.
154
+
155
+ ## Environment Variables
156
+
157
+ Temporary Hugging Face model backend:
158
+
159
+ ```bash
160
+ HF_TOKEN=your_hugging_face_token
161
+ HF_REPORT_MODEL=Qwen/Qwen2.5-Coder-7B-Instruct
162
+ ```
163
+
164
+ Optional GitHub token for higher public API limits:
165
+
166
+ ```bash
167
+ GITHUB_TOKEN=your_github_token
168
+ ```
169
+
170
+ Optional long-context memory:
171
+
172
+ ```bash
173
+ SYNAP_API_KEY=your_synap_api_key
174
+ SYNAP_INSTANCE_ID=your_synap_instance_id
175
+ SYNAP_CUSTOMER_ID=rocmpilot-hackathon
176
+ SYNAP_USER_ID=rocmpilot-agent-fleet
177
+ SYNAP_AUTO_SETUP=false
178
+ ```
179
+
180
+ Synap uses the default cloud endpoints automatically. Advanced deployments can also set `SYNAP_BASE_URL`, `SYNAP_GRPC_HOST`, `SYNAP_GRPC_PORT`, and `SYNAP_GRPC_TLS`.
181
+
182
+ Future AMD ROCm/vLLM backend:
183
+
184
+ ```bash
185
+ AMD_QWEN_BASE_URL=http://YOUR_AMD_INSTANCE:8000
186
+ AMD_QWEN_MODEL=Qwen/Qwen3-Coder-Next
187
+ AMD_QWEN_API_KEY=optional-if-your-endpoint-requires-it
188
+ ```
189
+
190
+ When `AMD_QWEN_BASE_URL` is present, the app automatically prefers AMD over Hugging Face.
191
+
192
+ ## Deploy on Vercel
193
+
194
+ Vercel is the preferred web deployment target.
195
+
196
+ ```bash
197
+ vercel
198
+ ```
199
+
200
+ Set environment variables in Vercel Project Settings:
201
+
202
+ - `HF_TOKEN`
203
+ - `HF_REPORT_MODEL`
204
+ - `GITHUB_TOKEN` optional
205
+ - `SYNAP_API_KEY` optional
206
+ - `SYNAP_INSTANCE_ID` optional
207
+ - `SYNAP_CUSTOMER_ID` optional
208
+ - `SYNAP_USER_ID` optional
209
+ - `AMD_QWEN_BASE_URL` later
210
+ - `AMD_QWEN_MODEL` later
211
+
212
+ The current app does not require a database for the demo flow. Synap is optional and the UI will show whether long-context memory is connected or using fallback memory.
213
+
214
+ ## Deploy as a Hugging Face Space
215
+
216
+ The repo also includes a Docker Space setup for hackathon submissions that require a Hugging Face Space link.
217
+
218
+ Files:
219
+
220
+ - `Dockerfile`
221
+ - `.dockerignore`
222
+ - Space metadata in this README frontmatter
223
+
224
+ Create a Docker Space, then upload this project. The app listens on port `7860` in the Space container.
225
+
226
+ Recommended Space ID:
227
+
228
+ ```text
229
+ Shivam311/rocmpilot
230
+ ```
231
+
232
+ ## Training and Improving the Agents
233
+
234
+ The `agent-training/` folder contains the training path for a more accurate agent:
235
+
236
+ - `seed-examples.jsonl`: 95 supervised seed examples across migration, patch, benchmark, report, and memory-agent tasks
237
+ - `eval-rubric.md`: scoring rubric
238
+ - `scripts/generate_seed_examples.py`: expands synthetic examples for common CUDA-to-ROCm failure modes
239
+ - `scripts/prepare_dataset.py`: converts seed examples into chat SFT format
240
+ - `scripts/train_rocmpilot_sft.py`: LoRA SFT script for Hugging Face Jobs
241
+ - `scripts/evaluate_agent.py`: smoke eval against an OpenAI-compatible endpoint
242
+
243
+ Regenerate the local seed and preview files:
244
+
245
+ ```bash
246
+ python agent-training/scripts/generate_seed_examples.py
247
+ python agent-training/scripts/prepare_dataset.py
248
+ ```
249
+
250
+ Prepare and push a seed dataset:
251
+
252
+ ```bash
253
+ python agent-training/scripts/prepare_dataset.py \
254
+ --push \
255
+ --repo-id Shivam311/rocmpilot-agent-sft
256
+ ```
257
+
258
+ Launch a small training run on Hugging Face Jobs after confirming paid Jobs access:
259
+
260
+ ```bash
261
+ hf jobs uv run \
262
+ --flavor t4-small \
263
+ --timeout 90m \
264
+ --secrets HF_TOKEN \
265
+ --env DATASET_ID=Shivam311/rocmpilot-agent-sft \
266
+ --env OUTPUT_MODEL=Shivam311/rocmpilot-agent-qwen-lora-v2 \
267
+ --env MAX_STEPS=160 \
268
+ agent-training/scripts/train_rocmpilot_sft.py
269
+ ```
270
+
271
+ Run eval:
272
+
273
+ ```bash
274
+ python agent-training/scripts/evaluate_agent.py
275
+ ```
276
+
277
+ For a serious version, expand the dataset to at least 200-500 examples from real migration cases before training.
278
+
279
+ ## Future AMD Integration
280
+
281
+ Once AMD Developer Cloud access is ready:
282
+
283
+ 1. Start an MI300X instance.
284
+ 2. Install or use a ROCm/vLLM image.
285
+ 3. Serve Qwen through an OpenAI-compatible vLLM endpoint.
286
+ 4. Set `AMD_QWEN_BASE_URL` in Vercel.
287
+ 5. Re-run ROCmPilot and capture:
288
+ - vLLM startup logs
289
+ - AMD GPU visibility
290
+ - one model response
291
+ - benchmark output
292
+ - final report generated through AMD-hosted Qwen
293
+
294
+ Suggested serving command:
295
+
296
+ ```bash
297
+ python -m vllm.entrypoints.openai.api_server \
298
+ --model Qwen/Qwen3-Coder-Next \
299
+ --host 0.0.0.0 \
300
+ --port 8000 \
301
+ --tensor-parallel-size 1 \
302
+ --max-model-len 32768
303
+ ```
304
+
305
+ ## What Is Real vs Temporary
306
+
307
+ Real today:
308
+
309
+ - Working dashboard
310
+ - Public GitHub URL intake
311
+ - GitHub API file scan
312
+ - Rule-based ROCm findings
313
+ - Patch previews
314
+ - Report backend priority system
315
+ - Vercel-safe stateless run flow
316
+ - Hugging Face training scaffold
317
+
318
+ Temporary/demo today:
319
+
320
+ - Patch previews are not committed back to GitHub yet
321
+ - Build logs are generated evidence, not real Docker builds
322
+ - Benchmark numbers are estimates until AMD validation
323
+ - Model fine-tuning scripts are ready but not launched automatically
324
+
325
+ Next production upgrades:
326
+
327
+ - GitHub App auth and PR creation
328
+ - Persistent run history
329
+ - Real queue for AMD validation jobs
330
+ - Live ROCm Docker builds
331
+ - Live vLLM benchmarks
332
+ - Fine-tuned ROCmPilot model hosted through HF/AMD
333
+
334
+ ## Submission Story
335
+
336
+ ROCmPilot is a Track 1 agentic workflow because the core product is the coordination of specialized agents that inspect, plan, validate, benchmark, and report. AMD compute enters as the target validation environment and the future hosted model runtime for Qwen on ROCm/vLLM.
337
+
338
+ The clean hackathon positioning:
339
+
340
+ > ROCmPilot helps developers migrate AI workloads to AMD faster. It scans a GitHub repo, finds CUDA assumptions, proposes ROCm patches, prepares AMD validation, and generates a business-ready migration report. Today it runs with Hugging Face fallback; when AMD Developer Cloud is available, the same app switches to Qwen served on MI300X through ROCm/vLLM.
341
+
342
+ ## Sources
343
+
344
+ - AMD: Day 0 Support for Qwen3-Coder-Next on AMD Instinct GPUs
345
+ https://www.amd.com/en/developer/resources/technical-articles/2026/day-0-support-for-qwen3-coder-next-on-amd-instinct-gpus.html
346
+ - Qwen3-Coder announcement
347
+ https://qwenlm.github.io/blog/qwen3-coder/
348
+ - vLLM supported models
349
+ https://docs.vllm.ai/en/v0.15.1/models/supported_models/
350
+ - Hugging Face Docker Spaces
351
+ https://huggingface.co/docs/hub/spaces-sdks-docker
352
+ - Hugging Face Jobs
353
+ https://huggingface.co/docs/huggingface_hub/guides/jobs
TECHNICAL_WALKTHROUGH.md ADDED
@@ -0,0 +1,461 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ROCmPilot Technical Walkthrough
2
+
3
+ ![Track](https://img.shields.io/badge/Track-AI%20Agents%20%26%20Agentic%20Workflows-0ea5e9)
4
+ ![AMD](https://img.shields.io/badge/AMD-ROCm%20%2B%20vLLM-ed1c24)
5
+ ![Model](https://img.shields.io/badge/Model-Qwen%20Coder-7c3aed)
6
+ ![Frontend](https://img.shields.io/badge/Frontend-Next.js%20%2B%20TypeScript-111827)
7
+ ![Memory](https://img.shields.io/badge/Memory-Maximem%20Synap-f43f5e)
8
+ ![Deployment](https://img.shields.io/badge/Deploy-Vercel-black)
9
+
10
+ ## Submission Links
11
+
12
+ | Artifact | Link |
13
+ | --- | --- |
14
+ | Live demo | [https://rocmpilot.vercel.app](https://rocmpilot.vercel.app) |
15
+ | Source code | [https://github.com/shivambhartiya/rocmpilot](https://github.com/shivambhartiya/rocmpilot) |
16
+ | Training dataset | [https://huggingface.co/datasets/Shivam311/rocmpilot-agent-sft](https://huggingface.co/datasets/Shivam311/rocmpilot-agent-sft) |
17
+ | Technical walkthrough | [TECHNICAL_WALKTHROUGH.md](./TECHNICAL_WALKTHROUGH.md) |
18
+
19
+ ## One-Line Pitch
20
+
21
+ ROCmPilot is a multi-agent developer tool that scans CUDA-first AI repositories and generates ROCm migration findings, patch previews, validation plans, reusable memory, and a judge-ready technical/business report.
22
+
23
+ ## Why ROCmPilot Matters
24
+
25
+ AI teams often want AMD GPU optionality, but real repositories quietly assume NVIDIA at many layers:
26
+
27
+ - Docker images such as `nvidia/cuda` or `nvcr.io/nvidia/pytorch`
28
+ - Python code with `torch.device("cuda")`, `.cuda()`, or `device_map="cuda"`
29
+ - dependency pins for `cu121`, `cu124`, `nvidia-cublas-cu12`, `flash-attn`, or `xformers`
30
+ - scripts using `nvidia-smi`, `CUDA_VISIBLE_DEVICES`, or `--gpus all`
31
+ - vLLM launch scripts that do not expose ROCm validation knobs
32
+ - benchmarks that report latency without backend, memory, tokens/sec, or command provenance
33
+
34
+ ROCmPilot converts that messy migration work into a structured agentic workflow. It does not pretend to automatically port every project. Instead, it gives developers a credible migration map, concrete patch previews, validation evidence, and a report they can hand to maintainers or infrastructure leaders.
35
+
36
+ ## Product Overview
37
+
38
+ ```mermaid
39
+ flowchart LR
40
+ U["Developer pastes a public GitHub URL"] --> UI["ROCmPilot Dashboard"]
41
+ UI --> R["Run Orchestrator"]
42
+ R --> G["GitHub Scanner"]
43
+ G --> D["Repo Doctor Agent"]
44
+ D --> P["Migration Planner Agent"]
45
+ P --> B["Build Runner Agent"]
46
+ B --> M["Benchmark Agent"]
47
+ M --> W["Agent War Room"]
48
+ W --> S["Synap Long-Context Memory"]
49
+ W --> REP["Report Agent"]
50
+ REP --> HF["Hugging Face Qwen fallback"]
51
+ REP --> AMD["Future AMD ROCm/vLLM Qwen endpoint"]
52
+ REP --> OUT["Final migration report"]
53
+ P --> PATCH["ROCm patch previews"]
54
+ M --> EVID["Validation checklist"]
55
+ ```
56
+
57
+ ## User Flow
58
+
59
+ 1. Open the ROCmPilot dashboard.
60
+ 2. Select a sample workload or paste a public GitHub repository URL.
61
+ 3. Start a run.
62
+ 4. Watch the live agent timeline and Agent War Room.
63
+ 5. Review findings, patch previews, logs, benchmark profile, and long-context memory status.
64
+ 6. Read the generated final report.
65
+ 7. Use the output as a migration plan or as the basis for a future PR.
66
+
67
+ ## Agent System
68
+
69
+ ROCmPilot uses specialized agents, each responsible for a different migration concern.
70
+
71
+ | Agent | Responsibility | Output |
72
+ | --- | --- | --- |
73
+ | Repo Doctor Agent | Finds CUDA/NVIDIA assumptions across code, Docker, scripts, dependencies, and benchmarks. | Findings with severity, category, file, line, and fix. |
74
+ | Migration Planner Agent | Converts findings into ROCm-safe migration recommendations. | Patch previews and backend-aware migration steps. |
75
+ | Build Runner Agent | Challenges whether the plan can be validated. | Smoke-test commands, container checks, and proof boundaries. |
76
+ | Benchmark Agent | Separates live evidence from estimates. | AMD-readiness profile and measurement plan. |
77
+ | Memory Agent | Stores reusable decisions across runs. | Synap-backed long-context memory or local fallback memory. |
78
+ | Report Agent | Turns the run into a credible submission/report. | Final markdown report with technical and business value. |
79
+
80
+ ## Agent War Room
81
+
82
+ The MVP is not just a set of isolated cards. Agents route messages to each other, ask questions, answer objections, and write shared memory.
83
+
84
+ ```mermaid
85
+ sequenceDiagram
86
+ participant O as Orchestrator
87
+ participant R as Repo Doctor
88
+ participant P as Migration Planner
89
+ participant B as Build Runner
90
+ participant M as Benchmark Agent
91
+ participant S as Shared Memory
92
+ participant X as Report Agent
93
+
94
+ O->>R: Lead the repo compatibility scan
95
+ R->>B: Which findings are build-breaking?
96
+ B-->>R: Container image, device path, and smoke test gaps
97
+ R->>P: Design the safest ROCm migration step
98
+ P-->>R: Use one backend-aware device resolver
99
+ R->>S: Store device resolution pattern
100
+ O->>P: Lead the migration plan using memory
101
+ P->>B: What proves this is ROCm-ready?
102
+ B-->>P: Log torch.version.hip and hit vLLM health endpoint
103
+ B->>S: Store ROCm acceptance checks
104
+ O->>M: Lead benchmark evidence
105
+ M->>X: How do we label estimated metrics?
106
+ X-->>M: Keep estimates separate from live AMD logs
107
+ M->>S: Store metric provenance rule
108
+ X->>S: Recall decisions for the final report
109
+ ```
110
+
111
+ ## Long-Context Memory With Synap
112
+
113
+ ROCmPilot integrates Maximem Synap as the long-context memory layer. When configured, the Report Agent stores the whole agent discussion as an `ai-chat-conversation`, fetches relevant context, and injects it into report generation.
114
+
115
+ If Synap credentials or runtime setup are unavailable, ROCmPilot falls back to reconstructed local memory so the demo remains reliable.
116
+
117
+ ```mermaid
118
+ flowchart TB
119
+ RUN["Completed ROCmPilot run"] --> DISC["Agent messages and shared memory"]
120
+ DISC --> SYNC{"SYNAP_API_KEY configured?"}
121
+ SYNC -- "Yes" --> INGEST["Synap addMemory"]
122
+ INGEST --> FETCH["Synap fetchUserContext and getContextForPrompt"]
123
+ FETCH --> PROMPT["Report prompt enriched with long-context memory"]
124
+ SYNC -- "No or runtime unavailable" --> LOCAL["Local reconstructed memory"]
125
+ LOCAL --> PROMPT
126
+ PROMPT --> REPORT["Final report"]
127
+ ```
128
+
129
+ ### Memory Values Stored
130
+
131
+ | Memory Type | Example |
132
+ | --- | --- |
133
+ | Device resolution pattern | Use a single backend resolver instead of scattered `.cuda()` checks. |
134
+ | ROCm acceptance checks | Prove import, backend detection, vLLM health, and provenance logging. |
135
+ | Container split decision | Add `Dockerfile.rocm`; keep CUDA support separate. |
136
+ | Metric provenance rule | Label estimates until AMD SMI/vLLM logs exist. |
137
+ | Health-check rule | Replace `nvidia-smi` proof with `rocm-smi` or `amd-smi` proof on AMD. |
138
+
139
+ ## Detection and Recommendation Logic
140
+
141
+ ROCmPilot scans public GitHub repositories using the GitHub API and focuses on files that usually control AI runtime portability.
142
+
143
+ ```mermaid
144
+ flowchart LR
145
+ TREE["GitHub repository tree"] --> FILTER["Relevant file filter"]
146
+ FILTER --> FILES["Dockerfiles, Python, shell scripts, requirements, YAML, benchmark files"]
147
+ FILES --> RULES["ROCm compatibility rules"]
148
+ RULES --> FIND["Findings"]
149
+ FIND --> PATCH["Patch previews"]
150
+ FIND --> MEM["Memory writes"]
151
+ PATCH --> REPORT["Final report"]
152
+ ```
153
+
154
+ ### Current Detection Categories
155
+
156
+ | Category | Severity | Typical Evidence | Recommended Direction |
157
+ | --- | --- | --- | --- |
158
+ | Hardcoded CUDA device path | Critical | `.cuda()`, `torch.device("cuda")`, `device_map="cuda"` | Add a backend-aware resolver and log ROCm/CUDA/CPU provenance. |
159
+ | NVIDIA container/runtime assumption | High | `nvidia/cuda`, `nvcr.io`, `--gpus all` | Add ROCm runtime image/profile and keep CUDA optional. |
160
+ | CUDA-oriented dependency | High | `cu124`, `nvidia-cublas-cu12`, `flash-attn`, `xformers` | Split dependencies into backend-specific profiles. |
161
+ | vLLM defaults need AMD profile | Medium | vLLM launch without model length, tensor parallelism, or metrics | Add ROCm vLLM serve script with OpenAI-compatible endpoint settings. |
162
+ | Benchmark evidence incomplete | Medium | latency-only benchmark | Add tokens/sec, memory, backend, model id, command provenance, p95 latency. |
163
+ | NVIDIA monitoring command | Medium | `nvidia-smi` | Add `rocm-smi` or `amd-smi` evidence path. |
164
+ | CUDA extension build path | High | `.cu`, `CUDAExtension`, `CUDA_HOME` | Gate CUDA extension builds and document ROCm-safe alternatives. |
165
+ | Docker Compose NVIDIA reservation | High | `driver: nvidia`, `runtime: nvidia` | Add separate ROCm Compose profile. |
166
+
167
+ ## Patch Preview Example
168
+
169
+ ROCmPilot does not currently mutate external repositories or open PRs automatically. Instead, it generates patch previews that are safe to review.
170
+
171
+ ```diff
172
+ +import torch
173
+ +
174
+ +def resolve_device() -> tuple[str, str]:
175
+ + if torch.cuda.is_available():
176
+ + backend = "rocm" if getattr(torch.version, "hip", None) else "cuda"
177
+ + return "cuda", backend
178
+ + return "cpu", "cpu"
179
+ +
180
+ +DEVICE, GPU_BACKEND = resolve_device()
181
+ ```
182
+
183
+ This pattern is important because PyTorch on ROCm still exposes GPU access through the `torch.cuda` API surface. The resolver records whether the backend is actually CUDA or HIP-backed ROCm.
184
+
185
+ ## Model and Compute Strategy
186
+
187
+ ROCmPilot is a Track 1 agentic workflow project. The GPU plan is model serving, not fine-tuning as the main hackathon track.
188
+
189
+ ```mermaid
190
+ flowchart TD
191
+ REPORT["Report Agent request"] --> AMDQ{"AMD_QWEN_BASE_URL set?"}
192
+ AMDQ -- "Yes" --> VLLM["Qwen on AMD MI300X through ROCm/vLLM"]
193
+ AMDQ -- "No" --> HFQ{"HF_TOKEN set?"}
194
+ HFQ -- "Yes" --> HF["Hugging Face Router with Qwen"]
195
+ HFQ -- "No" --> STATIC["Deterministic fallback report"]
196
+ VLLM --> RESP["Final report"]
197
+ HF --> RESP
198
+ STATIC --> RESP
199
+ ```
200
+
201
+ ### Backend Priority
202
+
203
+ 1. AMD ROCm/vLLM endpoint via `AMD_QWEN_BASE_URL`
204
+ 2. Hugging Face Router via `HF_TOKEN`
205
+ 3. Static fallback report
206
+
207
+ This keeps the submission demo-safe while preserving the AMD compute story. Once AMD Developer Cloud access is available, the same report endpoint can prefer AMD-hosted Qwen automatically.
208
+
209
+ ## Training Path on Hugging Face
210
+
211
+ The project includes a small supervised fine-tuning path for polishing the agent style and migration recommendations.
212
+
213
+ | Artifact | Detail |
214
+ | --- | --- |
215
+ | Dataset repo | [Shivam311/rocmpilot-agent-sft](https://huggingface.co/datasets/Shivam311/rocmpilot-agent-sft) |
216
+ | Current seed size | 95 examples |
217
+ | Tasks | migration planner, patch planner, benchmark agent, report agent, memory agent |
218
+ | Base model target | `Qwen/Qwen2.5-Coder-0.5B-Instruct` for small LoRA experiments |
219
+ | Output model target | `Shivam311/rocmpilot-agent-qwen-lora-v2` |
220
+
221
+ ```mermaid
222
+ flowchart LR
223
+ SEED["Synthetic and human-reviewed examples"] --> PREP["prepare_dataset.py"]
224
+ PREP --> DATASET["HF Dataset: rocmpilot-agent-sft"]
225
+ DATASET --> TRAIN["LoRA SFT job"]
226
+ TRAIN --> MODEL["Qwen LoRA adapter"]
227
+ MODEL --> EVAL["evaluate_agent.py"]
228
+ EVAL --> APP["Future report/migration backend"]
229
+ ```
230
+
231
+ Training is supporting polish, not the central submission claim. The main project remains an agentic developer workflow.
232
+
233
+ ## App Architecture
234
+
235
+ ```mermaid
236
+ flowchart TB
237
+ subgraph Frontend
238
+ UI["Next.js dashboard"]
239
+ WAR["Agent War Room"]
240
+ TABLE["Findings and patch panels"]
241
+ STATUS["GPU and memory status cards"]
242
+ end
243
+
244
+ subgraph API
245
+ RUNS["/api/runs"]
246
+ RUNID["/api/runs/[runId]"]
247
+ REPORT["/api/report"]
248
+ end
249
+
250
+ subgraph Core
251
+ STORE["Stateless run store"]
252
+ SCAN["GitHub scanner"]
253
+ DATA["ROCmPilot run model"]
254
+ SYNAP["Synap memory adapter"]
255
+ end
256
+
257
+ subgraph External
258
+ GH["GitHub API"]
259
+ HF["Hugging Face Router"]
260
+ AMD["AMD ROCm/vLLM endpoint"]
261
+ SM["Maximem Synap"]
262
+ end
263
+
264
+ UI --> RUNS
265
+ UI --> RUNID
266
+ UI --> REPORT
267
+ RUNS --> STORE
268
+ RUNID --> SCAN
269
+ SCAN --> GH
270
+ STORE --> DATA
271
+ DATA --> WAR
272
+ REPORT --> SYNAP
273
+ SYNAP --> SM
274
+ REPORT --> AMD
275
+ REPORT --> HF
276
+ ```
277
+
278
+ ## Data Model
279
+
280
+ ```mermaid
281
+ erDiagram
282
+ ROCM_RUN ||--o{ RUN_STAGE : has
283
+ ROCM_RUN ||--o{ FINDING : produces
284
+ ROCM_RUN ||--o{ PATCH_PREVIEW : proposes
285
+ ROCM_RUN ||--o{ AGENT_MESSAGE : contains
286
+ ROCM_RUN ||--o{ AGENT_MEMORY : stores
287
+ ROCM_RUN ||--o{ BENCHMARK_RESULT : profiles
288
+ ROCM_RUN ||--|| GPU_MODEL_STATUS : reports
289
+ ROCM_RUN ||--|| LONG_CONTEXT_MEMORY_STATUS : reports
290
+
291
+ FINDING {
292
+ string severity
293
+ string category
294
+ string file
295
+ int line
296
+ string recommendedFix
297
+ }
298
+
299
+ AGENT_MESSAGE {
300
+ string agent
301
+ string toAgent
302
+ string kind
303
+ string task
304
+ string leadAgent
305
+ }
306
+
307
+ AGENT_MEMORY {
308
+ string title
309
+ string scope
310
+ string learnedFromAgent
311
+ string solution
312
+ }
313
+ ```
314
+
315
+ ## Deployment Architecture
316
+
317
+ ```mermaid
318
+ flowchart LR
319
+ DEV["GitHub main branch"] --> VERCEL["Vercel deployment"]
320
+ VERCEL --> APP["rocmpilot.vercel.app"]
321
+ VERCEL --> ENV["Environment variables"]
322
+ ENV --> HFENV["HF_TOKEN"]
323
+ ENV --> SYNENV["SYNAP_API_KEY and SYNAP_INSTANCE_ID"]
324
+ ENV --> AMDENV["AMD_QWEN_BASE_URL later"]
325
+ APP --> USER["Hackathon judges and users"]
326
+ ```
327
+
328
+ ## Environment Variables
329
+
330
+ | Variable | Required for MVP | Purpose |
331
+ | --- | --- | --- |
332
+ | `HF_TOKEN` | Recommended | Enables Hugging Face Qwen report generation. |
333
+ | `HF_REPORT_MODEL` | Optional | Defaults to `Qwen/Qwen2.5-Coder-7B-Instruct`. |
334
+ | `GITHUB_TOKEN` | Optional | Raises GitHub API limits for public repo scans. |
335
+ | `SYNAP_INSTANCE_ID` | Optional | Targets the Synap memory instance. |
336
+ | `SYNAP_API_KEY` | Optional | Enables persistent long-context memory. |
337
+ | `SYNAP_BASE_URL` | Optional | Synap cloud endpoint override. |
338
+ | `SYNAP_CUSTOMER_ID` | Optional | Memory scope, defaults to `rocmpilot-hackathon`. |
339
+ | `SYNAP_USER_ID` | Optional | Agent fleet memory identity. |
340
+ | `AMD_QWEN_BASE_URL` | Later | Enables AMD-hosted Qwen through ROCm/vLLM. |
341
+ | `AMD_QWEN_MODEL` | Later | Defaults to `Qwen/Qwen3-Coder-Next`. |
342
+
343
+ ## Demo Script
344
+
345
+ Use this script for a 2-3 minute project walkthrough.
346
+
347
+ 1. Open [https://rocmpilot.vercel.app](https://rocmpilot.vercel.app).
348
+ 2. Paste a CUDA-heavy public repo, for example `https://github.com/NVIDIA/cuda-samples` or `https://github.com/NVIDIA/Megatron-LM`.
349
+ 3. Click `Scan Repo`.
350
+ 4. Explain that Repo Doctor scans public files for CUDA/NVIDIA assumptions.
351
+ 5. Point to the Agent War Room and show agents asking each other questions instead of acting independently.
352
+ 6. Show Shared Memory and explain that decisions are reused later.
353
+ 7. Open Migration Findings and Patch Previews.
354
+ 8. Open the GPU model status card and explain the backend priority: AMD ROCm/vLLM when available, Hugging Face fallback now, static fallback for reliability.
355
+ 9. Open the Long-context memory card and explain Synap memory.
356
+ 10. Show the final report and emphasize business value: faster AMD migration planning, reduced infra risk, and a clear path from audit to validation.
357
+
358
+ ## What Is Fully Implemented Today
359
+
360
+ | Capability | Status |
361
+ | --- | --- |
362
+ | Next.js dashboard | Implemented |
363
+ | Public GitHub URL scan | Implemented |
364
+ | CUDA/NVIDIA findings | Implemented |
365
+ | Patch previews | Implemented |
366
+ | Agent War Room routed messages | Implemented |
367
+ | Shared run memory | Implemented |
368
+ | Synap memory adapter | Implemented with fallback |
369
+ | Hugging Face Qwen report generation | Implemented when `HF_TOKEN` is configured |
370
+ | Static fallback report | Implemented |
371
+ | Vercel deployment | Implemented |
372
+ | Training dataset | Implemented and published |
373
+
374
+ ## Honest Boundaries
375
+
376
+ | Capability | Current Boundary |
377
+ | --- | --- |
378
+ | Automatic PR creation | Not implemented yet. ROCmPilot generates patch previews and recommendations. |
379
+ | Live AMD MI300X benchmark proof | Pending AMD Developer Cloud access. Current benchmark cards are labeled static estimates. |
380
+ | Synap on every Vercel run | Integrated, but falls back if the Synap Python bridge cannot initialize in the serverless runtime. |
381
+ | Full repository mutation | Not performed by design in the MVP. Maintainers should review patch previews first. |
382
+
383
+ ## Business Value
384
+
385
+ ROCmPilot is useful for:
386
+
387
+ - AI startups that want AMD GPU optionality without manually auditing every codebase.
388
+ - Infrastructure teams evaluating whether an internal CUDA-first service can move to ROCm.
389
+ - Open-source maintainers who want clear, reviewable migration guidance.
390
+ - Cloud/GPU providers that need a repeatable readiness assessment workflow.
391
+
392
+ ### Business Impact
393
+
394
+ | Problem | ROCmPilot Value |
395
+ | --- | --- |
396
+ | Migration audits are manual and slow. | Automated agentic scan and report. |
397
+ | CUDA assumptions hide across many files. | Multi-layer detection across Docker, Python, shell, dependencies, and benchmarks. |
398
+ | Teams overclaim hardware readiness. | Explicit separation of estimates, fallback, and live AMD proof. |
399
+ | Migration work is hard to hand off. | Patch previews and judge/infra-ready report. |
400
+ | Agents forget past decisions. | Synap long-context memory for reusable migration knowledge. |
401
+
402
+ ## Roadmap
403
+
404
+ ```mermaid
405
+ gantt
406
+ title ROCmPilot Roadmap
407
+ dateFormat YYYY-MM-DD
408
+ section MVP
409
+ Dashboard and agent workflow :done, mvp1, 2026-05-04, 1d
410
+ GitHub scan and patch previews :done, mvp2, 2026-05-04, 1d
411
+ Synap memory integration :done, mvp3, 2026-05-05, 1d
412
+ Training dataset expansion :done, mvp4, 2026-05-05, 1d
413
+ section Next
414
+ Automatic PR generation :active, next1, 2026-05-06, 2d
415
+ Live AMD MI300X vLLM proof : next2, 2026-05-07, 2d
416
+ Larger human-reviewed dataset : next3, 2026-05-08, 4d
417
+ NodeOps memory worker : next4, 2026-05-08, 3d
418
+ ```
419
+
420
+ ## Future AMD Integration
421
+
422
+ When AMD Developer Cloud access is available:
423
+
424
+ 1. Start an MI300X instance.
425
+ 2. Launch Qwen through ROCm/vLLM using an OpenAI-compatible server.
426
+ 3. Set `AMD_QWEN_BASE_URL` and `AMD_QWEN_MODEL` in Vercel.
427
+ 4. Re-run ROCmPilot.
428
+ 5. Capture proof:
429
+ - AMD GPU visibility logs
430
+ - vLLM startup logs
431
+ - one successful model response
432
+ - workload validation output
433
+ - updated final report generated through AMD-hosted Qwen
434
+
435
+ Suggested command:
436
+
437
+ ```bash
438
+ python -m vllm.entrypoints.openai.api_server \
439
+ --model Qwen/Qwen3-Coder-Next \
440
+ --host 0.0.0.0 \
441
+ --port 8000 \
442
+ --tensor-parallel-size 1 \
443
+ --max-model-len 32768
444
+ ```
445
+
446
+ ## Why This Fits Track 1
447
+
448
+ ROCmPilot is not primarily a fine-tuning project and not primarily a multimodal project. It is a coordinated AI-agent workflow:
449
+
450
+ - multiple specialized agents
451
+ - routed agent-to-agent discussion
452
+ - lead-agent task ownership
453
+ - shared and persistent memory
454
+ - model-backed report generation
455
+ - real developer workflow around repository migration
456
+
457
+ That makes **AI Agents & Agentic Workflows** the best hackathon track.
458
+
459
+ ## Closing Summary
460
+
461
+ ROCmPilot turns CUDA-to-ROCm migration from an unclear engineering chore into an agentic developer workflow. It scans real repositories, identifies migration blockers, proposes ROCm-ready patches, records reusable memory, and generates a technical/business report. The MVP is reliable on Vercel today through Hugging Face and fallback paths, while the architecture is ready to upgrade to AMD Instinct GPUs with ROCm/vLLM as soon as AMD cloud access is available.
components.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "radix-nova",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "rtl": false,
15
+ "aliases": {
16
+ "components": "@/components",
17
+ "utils": "@/lib/utils",
18
+ "ui": "@/components/ui",
19
+ "lib": "@/lib",
20
+ "hooks": "@/hooks"
21
+ },
22
+ "menuColor": "default",
23
+ "menuAccent": "subtle",
24
+ "registries": {}
25
+ }
eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
next.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ output: "standalone",
5
+ };
6
+
7
+ export default nextConfig;
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "rocmpilot",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint",
10
+ "synap:setup": "synap-js-sdk setup --sdk-version 0.1.1"
11
+ },
12
+ "dependencies": {
13
+ "@maximem/synap-js-sdk": "^0.2.4",
14
+ "@streamdown/cjk": "^1.0.3",
15
+ "@streamdown/code": "^1.1.1",
16
+ "@streamdown/math": "^1.0.2",
17
+ "@streamdown/mermaid": "^1.0.2",
18
+ "ai": "^6.0.174",
19
+ "ansi-to-react": "^6.2.6",
20
+ "class-variance-authority": "^0.7.1",
21
+ "clsx": "^2.1.1",
22
+ "lucide-react": "^1.14.0",
23
+ "next": "16.2.4",
24
+ "radix-ui": "^1.4.3",
25
+ "react": "19.2.4",
26
+ "react-dom": "19.2.4",
27
+ "shadcn": "^4.6.0",
28
+ "shiki": "^4.0.2",
29
+ "streamdown": "^2.5.0",
30
+ "tailwind-merge": "^3.5.0",
31
+ "tw-animate-css": "^1.4.0"
32
+ },
33
+ "devDependencies": {
34
+ "@tailwindcss/postcss": "^4",
35
+ "@types/node": "^20",
36
+ "@types/react": "^19",
37
+ "@types/react-dom": "^19",
38
+ "eslint": "^9",
39
+ "eslint-config-next": "16.2.4",
40
+ "tailwindcss": "^4",
41
+ "typescript": "^5"
42
+ }
43
+ }
postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
public/file.svg ADDED
public/globe.svg ADDED
public/next.svg ADDED
public/vercel.svg ADDED
public/window.svg ADDED
src/app/api/report/route.ts ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { buildFallbackReport, buildReportPrompt, getModelStatus } from "@/lib/rocmpilot/data";
2
+ import { syncRunMemoryWithSynap } from "@/lib/rocmpilot/synap-memory";
3
+ import type { ReportResponse, RocmRun } from "@/lib/rocmpilot/types";
4
+ import { NextResponse } from "next/server";
5
+
6
+ export const runtime = "nodejs";
7
+
8
+ type ChatCompletionResponse = {
9
+ choices?: Array<{
10
+ message?: {
11
+ content?: string;
12
+ };
13
+ }>;
14
+ };
15
+
16
+ async function generateWithAmdQwen(run: RocmRun, longContext: string) {
17
+ const baseUrl = process.env.AMD_QWEN_BASE_URL?.replace(/\/$/, "");
18
+
19
+ if (!baseUrl) {
20
+ return null;
21
+ }
22
+
23
+ const model = process.env.AMD_QWEN_MODEL ?? "Qwen/Qwen3-Coder-Next";
24
+ const headers: Record<string, string> = {
25
+ "Content-Type": "application/json",
26
+ };
27
+
28
+ if (process.env.AMD_QWEN_API_KEY) {
29
+ headers.Authorization = `Bearer ${process.env.AMD_QWEN_API_KEY}`;
30
+ }
31
+
32
+ const response = await fetch(`${baseUrl}/v1/chat/completions`, {
33
+ method: "POST",
34
+ headers,
35
+ signal: AbortSignal.timeout(15_000),
36
+ body: JSON.stringify({
37
+ model,
38
+ temperature: 0.25,
39
+ max_tokens: 900,
40
+ messages: [
41
+ {
42
+ role: "system",
43
+ content:
44
+ "You are the Report Agent for ROCmPilot. Write concise, credible hackathon submission reports. Do not invent live benchmark claims beyond the provided data.",
45
+ },
46
+ {
47
+ role: "user",
48
+ content: buildReportPrompt(run, longContext),
49
+ },
50
+ ],
51
+ }),
52
+ });
53
+
54
+ if (!response.ok) {
55
+ throw new Error(`AMD Qwen endpoint returned ${response.status}`);
56
+ }
57
+
58
+ const payload = (await response.json()) as ChatCompletionResponse;
59
+ return payload.choices?.[0]?.message?.content?.trim() || null;
60
+ }
61
+
62
+ async function generateWithHuggingFace(run: RocmRun, longContext: string) {
63
+ if (!process.env.HF_TOKEN) {
64
+ return null;
65
+ }
66
+
67
+ const response = await fetch("https://router.huggingface.co/v1/chat/completions", {
68
+ method: "POST",
69
+ headers: {
70
+ Authorization: `Bearer ${process.env.HF_TOKEN}`,
71
+ "Content-Type": "application/json",
72
+ },
73
+ signal: AbortSignal.timeout(15_000),
74
+ body: JSON.stringify({
75
+ model: process.env.HF_REPORT_MODEL ?? "Qwen/Qwen2.5-Coder-7B-Instruct",
76
+ temperature: 0.25,
77
+ max_tokens: 900,
78
+ messages: [
79
+ {
80
+ role: "system",
81
+ content:
82
+ "You are the Report Agent for ROCmPilot. Write concise, credible hackathon submission reports. Do not invent live benchmark claims beyond the provided data.",
83
+ },
84
+ {
85
+ role: "user",
86
+ content: buildReportPrompt(run, longContext),
87
+ },
88
+ ],
89
+ }),
90
+ });
91
+
92
+ if (!response.ok) {
93
+ throw new Error(`Hugging Face router returned ${response.status}`);
94
+ }
95
+
96
+ const payload = (await response.json()) as ChatCompletionResponse;
97
+ return payload.choices?.[0]?.message?.content?.trim() || null;
98
+ }
99
+
100
+ export async function POST(request: Request) {
101
+ const run = (await request.json()) as RocmRun;
102
+ const memorySync = await syncRunMemoryWithSynap(run);
103
+
104
+ try {
105
+ const report = await generateWithAmdQwen(run, memorySync.promptContext);
106
+
107
+ if (report) {
108
+ const response: ReportResponse = {
109
+ report,
110
+ source: "amd-vllm",
111
+ modelStatus: getModelStatus("amd-vllm"),
112
+ memoryStatus: memorySync.status,
113
+ };
114
+ return NextResponse.json(response);
115
+ }
116
+ } catch (error) {
117
+ console.warn("Falling back from AMD Qwen report generation:", error);
118
+ }
119
+
120
+ try {
121
+ const report = await generateWithHuggingFace(run, memorySync.promptContext);
122
+
123
+ if (report) {
124
+ const response: ReportResponse = {
125
+ report,
126
+ source: "hf-router",
127
+ modelStatus: getModelStatus("hf-router"),
128
+ memoryStatus: memorySync.status,
129
+ };
130
+ return NextResponse.json(response);
131
+ }
132
+ } catch (error) {
133
+ console.warn("Falling back from Hugging Face report generation:", error);
134
+ }
135
+
136
+ const response: ReportResponse = {
137
+ report: buildFallbackReport(run, memorySync.promptContext),
138
+ source: "fallback",
139
+ modelStatus: getModelStatus("fallback"),
140
+ memoryStatus: memorySync.status,
141
+ };
142
+
143
+ return NextResponse.json(response);
144
+ }
src/app/api/runs/[runId]/route.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getRun } from "@/lib/rocmpilot/store";
2
+ import { NextResponse } from "next/server";
3
+
4
+ export async function GET(
5
+ _request: Request,
6
+ context: { params: Promise<{ runId: string }> }
7
+ ) {
8
+ const { runId } = await context.params;
9
+ const run = await getRun(runId);
10
+
11
+ if (!run) {
12
+ return NextResponse.json({ error: "Run not found" }, { status: 404 });
13
+ }
14
+
15
+ return NextResponse.json(run);
16
+ }
src/app/api/runs/route.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createRun } from "@/lib/rocmpilot/store";
2
+ import type { RunMode } from "@/lib/rocmpilot/types";
3
+ import { NextResponse } from "next/server";
4
+
5
+ export async function POST(request: Request) {
6
+ const body = (await request.json().catch(() => ({}))) as {
7
+ sampleId?: string;
8
+ mode?: RunMode;
9
+ repoUrl?: string;
10
+ };
11
+
12
+ const run = createRun(
13
+ body.sampleId ?? "qwen-vllm-cuda",
14
+ body.mode ?? "mock",
15
+ body.repoUrl
16
+ );
17
+
18
+ return NextResponse.json(run);
19
+ }
src/app/favicon.ico ADDED
src/app/globals.css ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
+ @source "../node_modules/streamdown/dist/*.js";
5
+
6
+ @custom-variant dark (&:is(.dark *));
7
+
8
+ @theme inline {
9
+ --color-background: var(--background);
10
+ --color-foreground: var(--foreground);
11
+ --font-sans: "Geist", "Geist Fallback", ui-sans-serif, system-ui, sans-serif;
12
+ --font-mono: "Geist Mono", "Geist Mono Fallback", ui-monospace, monospace;
13
+ --font-heading: "Geist", "Geist Fallback", ui-sans-serif, system-ui, sans-serif;
14
+ --color-sidebar-ring: var(--sidebar-ring);
15
+ --color-sidebar-border: var(--sidebar-border);
16
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
17
+ --color-sidebar-accent: var(--sidebar-accent);
18
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
19
+ --color-sidebar-primary: var(--sidebar-primary);
20
+ --color-sidebar-foreground: var(--sidebar-foreground);
21
+ --color-sidebar: var(--sidebar);
22
+ --color-chart-5: var(--chart-5);
23
+ --color-chart-4: var(--chart-4);
24
+ --color-chart-3: var(--chart-3);
25
+ --color-chart-2: var(--chart-2);
26
+ --color-chart-1: var(--chart-1);
27
+ --color-ring: var(--ring);
28
+ --color-input: var(--input);
29
+ --color-border: var(--border);
30
+ --color-destructive: var(--destructive);
31
+ --color-accent-foreground: var(--accent-foreground);
32
+ --color-accent: var(--accent);
33
+ --color-muted-foreground: var(--muted-foreground);
34
+ --color-muted: var(--muted);
35
+ --color-secondary-foreground: var(--secondary-foreground);
36
+ --color-secondary: var(--secondary);
37
+ --color-primary-foreground: var(--primary-foreground);
38
+ --color-primary: var(--primary);
39
+ --color-popover-foreground: var(--popover-foreground);
40
+ --color-popover: var(--popover);
41
+ --color-card-foreground: var(--card-foreground);
42
+ --color-card: var(--card);
43
+ --radius-sm: calc(var(--radius) * 0.6);
44
+ --radius-md: calc(var(--radius) * 0.8);
45
+ --radius-lg: var(--radius);
46
+ --radius-xl: calc(var(--radius) * 1.4);
47
+ --radius-2xl: calc(var(--radius) * 1.8);
48
+ --radius-3xl: calc(var(--radius) * 2.2);
49
+ --radius-4xl: calc(var(--radius) * 2.6);
50
+ }
51
+
52
+ :root {
53
+ --background: oklch(1 0 0);
54
+ --foreground: oklch(0.145 0 0);
55
+ --card: oklch(1 0 0);
56
+ --card-foreground: oklch(0.145 0 0);
57
+ --popover: oklch(1 0 0);
58
+ --popover-foreground: oklch(0.145 0 0);
59
+ --primary: oklch(0.205 0 0);
60
+ --primary-foreground: oklch(0.985 0 0);
61
+ --secondary: oklch(0.97 0 0);
62
+ --secondary-foreground: oklch(0.205 0 0);
63
+ --muted: oklch(0.97 0 0);
64
+ --muted-foreground: oklch(0.556 0 0);
65
+ --accent: oklch(0.97 0 0);
66
+ --accent-foreground: oklch(0.205 0 0);
67
+ --destructive: oklch(0.577 0.245 27.325);
68
+ --border: oklch(0.922 0 0);
69
+ --input: oklch(0.922 0 0);
70
+ --ring: oklch(0.708 0 0);
71
+ --chart-1: oklch(0.87 0 0);
72
+ --chart-2: oklch(0.556 0 0);
73
+ --chart-3: oklch(0.439 0 0);
74
+ --chart-4: oklch(0.371 0 0);
75
+ --chart-5: oklch(0.269 0 0);
76
+ --radius: 0.625rem;
77
+ --sidebar: oklch(0.985 0 0);
78
+ --sidebar-foreground: oklch(0.145 0 0);
79
+ --sidebar-primary: oklch(0.205 0 0);
80
+ --sidebar-primary-foreground: oklch(0.985 0 0);
81
+ --sidebar-accent: oklch(0.97 0 0);
82
+ --sidebar-accent-foreground: oklch(0.205 0 0);
83
+ --sidebar-border: oklch(0.922 0 0);
84
+ --sidebar-ring: oklch(0.708 0 0);
85
+ }
86
+
87
+ .dark {
88
+ --background: oklch(0.145 0 0);
89
+ --foreground: oklch(0.985 0 0);
90
+ --card: oklch(0.205 0 0);
91
+ --card-foreground: oklch(0.985 0 0);
92
+ --popover: oklch(0.205 0 0);
93
+ --popover-foreground: oklch(0.985 0 0);
94
+ --primary: oklch(0.922 0 0);
95
+ --primary-foreground: oklch(0.205 0 0);
96
+ --secondary: oklch(0.269 0 0);
97
+ --secondary-foreground: oklch(0.985 0 0);
98
+ --muted: oklch(0.269 0 0);
99
+ --muted-foreground: oklch(0.708 0 0);
100
+ --accent: oklch(0.269 0 0);
101
+ --accent-foreground: oklch(0.985 0 0);
102
+ --destructive: oklch(0.704 0.191 22.216);
103
+ --border: oklch(1 0 0 / 10%);
104
+ --input: oklch(1 0 0 / 15%);
105
+ --ring: oklch(0.556 0 0);
106
+ --chart-1: oklch(0.87 0 0);
107
+ --chart-2: oklch(0.556 0 0);
108
+ --chart-3: oklch(0.439 0 0);
109
+ --chart-4: oklch(0.371 0 0);
110
+ --chart-5: oklch(0.269 0 0);
111
+ --sidebar: oklch(0.205 0 0);
112
+ --sidebar-foreground: oklch(0.985 0 0);
113
+ --sidebar-primary: oklch(0.488 0.243 264.376);
114
+ --sidebar-primary-foreground: oklch(0.985 0 0);
115
+ --sidebar-accent: oklch(0.269 0 0);
116
+ --sidebar-accent-foreground: oklch(0.985 0 0);
117
+ --sidebar-border: oklch(1 0 0 / 10%);
118
+ --sidebar-ring: oklch(0.556 0 0);
119
+ }
120
+
121
+ @layer base {
122
+ * {
123
+ @apply border-border outline-ring/50;
124
+ }
125
+ body {
126
+ @apply bg-background text-foreground;
127
+ }
128
+ html {
129
+ @apply font-sans;
130
+ }
131
+ }
src/app/layout.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import { TooltipProvider } from "@/components/ui/tooltip";
4
+ import "./globals.css";
5
+
6
+ const geistSans = Geist({
7
+ variable: "--font-geist-sans",
8
+ subsets: ["latin"],
9
+ });
10
+
11
+ const geistMono = Geist_Mono({
12
+ variable: "--font-geist-mono",
13
+ subsets: ["latin"],
14
+ });
15
+
16
+ export const metadata: Metadata = {
17
+ title: "ROCmPilot",
18
+ description:
19
+ "Agentic ROCm migration dashboard powered by AMD GPU model serving.",
20
+ };
21
+
22
+ export default function RootLayout({
23
+ children,
24
+ }: Readonly<{
25
+ children: React.ReactNode;
26
+ }>) {
27
+ return (
28
+ <html
29
+ lang="en"
30
+ className={`dark ${geistSans.variable} ${geistMono.variable} h-full antialiased`}
31
+ >
32
+ <body className="flex min-h-full flex-col">
33
+ <TooltipProvider>{children}</TooltipProvider>
34
+ </body>
35
+ </html>
36
+ );
37
+ }
src/app/page.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { RocmPilotDashboard } from "@/components/rocmpilot-dashboard";
2
+
3
+ export default function Home() {
4
+ return <RocmPilotDashboard />;
5
+ }
src/components/ai-elements/code-block.tsx ADDED
@@ -0,0 +1,556 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import {
5
+ Select,
6
+ SelectContent,
7
+ SelectItem,
8
+ SelectTrigger,
9
+ SelectValue,
10
+ } from "@/components/ui/select";
11
+ import { cn } from "@/lib/utils";
12
+ import { CheckIcon, CopyIcon } from "lucide-react";
13
+ import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";
14
+ import {
15
+ createContext,
16
+ memo,
17
+ useCallback,
18
+ useContext,
19
+ useEffect,
20
+ useMemo,
21
+ useRef,
22
+ useState,
23
+ } from "react";
24
+ import type {
25
+ BundledLanguage,
26
+ BundledTheme,
27
+ HighlighterGeneric,
28
+ ThemedToken,
29
+ } from "shiki";
30
+ import { createHighlighter } from "shiki";
31
+
32
+ // Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline
33
+ // oxlint-disable-next-line eslint(no-bitwise)
34
+ const isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;
35
+ // oxlint-disable-next-line eslint(no-bitwise)
36
+ const isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;
37
+ const isUnderline = (fontStyle: number | undefined) =>
38
+ // oxlint-disable-next-line eslint(no-bitwise)
39
+ fontStyle && fontStyle & 4;
40
+
41
+ // Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint
42
+ interface KeyedToken {
43
+ token: ThemedToken;
44
+ key: string;
45
+ }
46
+ interface KeyedLine {
47
+ tokens: KeyedToken[];
48
+ key: string;
49
+ }
50
+
51
+ const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>
52
+ lines.map((line, lineIdx) => ({
53
+ key: `line-${lineIdx}`,
54
+ tokens: line.map((token, tokenIdx) => ({
55
+ key: `line-${lineIdx}-${tokenIdx}`,
56
+ token,
57
+ })),
58
+ }));
59
+
60
+ // Token rendering component
61
+ const TokenSpan = ({ token }: { token: ThemedToken }) => (
62
+ <span
63
+ className="dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)]"
64
+ style={
65
+ {
66
+ backgroundColor: token.bgColor,
67
+ color: token.color,
68
+ fontStyle: isItalic(token.fontStyle) ? "italic" : undefined,
69
+ fontWeight: isBold(token.fontStyle) ? "bold" : undefined,
70
+ textDecoration: isUnderline(token.fontStyle) ? "underline" : undefined,
71
+ ...token.htmlStyle,
72
+ } as CSSProperties
73
+ }
74
+ >
75
+ {token.content}
76
+ </span>
77
+ );
78
+
79
+ // Line number styles using CSS counters
80
+ const LINE_NUMBER_CLASSES = cn(
81
+ "block",
82
+ "before:content-[counter(line)]",
83
+ "before:inline-block",
84
+ "before:[counter-increment:line]",
85
+ "before:w-8",
86
+ "before:mr-4",
87
+ "before:text-right",
88
+ "before:text-muted-foreground/50",
89
+ "before:font-mono",
90
+ "before:select-none"
91
+ );
92
+
93
+ // Line rendering component
94
+ const LineSpan = ({
95
+ keyedLine,
96
+ showLineNumbers,
97
+ }: {
98
+ keyedLine: KeyedLine;
99
+ showLineNumbers: boolean;
100
+ }) => (
101
+ <span className={showLineNumbers ? LINE_NUMBER_CLASSES : "block"}>
102
+ {keyedLine.tokens.length === 0
103
+ ? "\n"
104
+ : keyedLine.tokens.map(({ token, key }) => (
105
+ <TokenSpan key={key} token={token} />
106
+ ))}
107
+ </span>
108
+ );
109
+
110
+ // Types
111
+ type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
112
+ code: string;
113
+ language: BundledLanguage;
114
+ showLineNumbers?: boolean;
115
+ };
116
+
117
+ interface TokenizedCode {
118
+ tokens: ThemedToken[][];
119
+ fg: string;
120
+ bg: string;
121
+ }
122
+
123
+ interface CodeBlockContextType {
124
+ code: string;
125
+ }
126
+
127
+ // Context
128
+ const CodeBlockContext = createContext<CodeBlockContextType>({
129
+ code: "",
130
+ });
131
+
132
+ // Highlighter cache (singleton per language)
133
+ const highlighterCache = new Map<
134
+ string,
135
+ Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>
136
+ >();
137
+
138
+ // Token cache
139
+ const tokensCache = new Map<string, TokenizedCode>();
140
+
141
+ // Subscribers for async token updates
142
+ const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();
143
+
144
+ const getTokensCacheKey = (code: string, language: BundledLanguage) => {
145
+ const start = code.slice(0, 100);
146
+ const end = code.length > 100 ? code.slice(-100) : "";
147
+ return `${language}:${code.length}:${start}:${end}`;
148
+ };
149
+
150
+ const getHighlighter = (
151
+ language: BundledLanguage
152
+ ): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {
153
+ const cached = highlighterCache.get(language);
154
+ if (cached) {
155
+ return cached;
156
+ }
157
+
158
+ const highlighterPromise = createHighlighter({
159
+ langs: [language],
160
+ themes: ["github-light", "github-dark"],
161
+ });
162
+
163
+ highlighterCache.set(language, highlighterPromise);
164
+ return highlighterPromise;
165
+ };
166
+
167
+ // Create raw tokens for immediate display while highlighting loads
168
+ const createRawTokens = (code: string): TokenizedCode => ({
169
+ bg: "transparent",
170
+ fg: "inherit",
171
+ tokens: code.split("\n").map((line) =>
172
+ line === ""
173
+ ? []
174
+ : [
175
+ {
176
+ color: "inherit",
177
+ content: line,
178
+ } as ThemedToken,
179
+ ]
180
+ ),
181
+ });
182
+
183
+ // Synchronous highlight with callback for async results
184
+ export const highlightCode = (
185
+ code: string,
186
+ language: BundledLanguage,
187
+ // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)
188
+ callback?: (result: TokenizedCode) => void
189
+ ): TokenizedCode | null => {
190
+ const tokensCacheKey = getTokensCacheKey(code, language);
191
+
192
+ // Return cached result if available
193
+ const cached = tokensCache.get(tokensCacheKey);
194
+ if (cached) {
195
+ return cached;
196
+ }
197
+
198
+ // Subscribe callback if provided
199
+ if (callback) {
200
+ if (!subscribers.has(tokensCacheKey)) {
201
+ subscribers.set(tokensCacheKey, new Set());
202
+ }
203
+ subscribers.get(tokensCacheKey)?.add(callback);
204
+ }
205
+
206
+ // Start highlighting in background - fire-and-forget async pattern
207
+ getHighlighter(language)
208
+ // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)
209
+ .then((highlighter) => {
210
+ const availableLangs = highlighter.getLoadedLanguages();
211
+ const langToUse = availableLangs.includes(language) ? language : "text";
212
+
213
+ const result = highlighter.codeToTokens(code, {
214
+ lang: langToUse,
215
+ themes: {
216
+ dark: "github-dark",
217
+ light: "github-light",
218
+ },
219
+ });
220
+
221
+ const tokenized: TokenizedCode = {
222
+ bg: result.bg ?? "transparent",
223
+ fg: result.fg ?? "inherit",
224
+ tokens: result.tokens,
225
+ };
226
+
227
+ // Cache the result
228
+ tokensCache.set(tokensCacheKey, tokenized);
229
+
230
+ // Notify all subscribers
231
+ const subs = subscribers.get(tokensCacheKey);
232
+ if (subs) {
233
+ for (const sub of subs) {
234
+ sub(tokenized);
235
+ }
236
+ subscribers.delete(tokensCacheKey);
237
+ }
238
+ })
239
+ // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks)
240
+ .catch((error) => {
241
+ console.error("Failed to highlight code:", error);
242
+ subscribers.delete(tokensCacheKey);
243
+ });
244
+
245
+ return null;
246
+ };
247
+
248
+ const CodeBlockBody = memo(
249
+ ({
250
+ tokenized,
251
+ showLineNumbers,
252
+ className,
253
+ }: {
254
+ tokenized: TokenizedCode;
255
+ showLineNumbers: boolean;
256
+ className?: string;
257
+ }) => {
258
+ const preStyle = useMemo(
259
+ () => ({
260
+ backgroundColor: tokenized.bg,
261
+ color: tokenized.fg,
262
+ }),
263
+ [tokenized.bg, tokenized.fg]
264
+ );
265
+
266
+ const keyedLines = useMemo(
267
+ () => addKeysToTokens(tokenized.tokens),
268
+ [tokenized.tokens]
269
+ );
270
+
271
+ return (
272
+ <pre
273
+ className={cn(
274
+ "dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)] m-0 p-4 text-sm",
275
+ className
276
+ )}
277
+ style={preStyle}
278
+ >
279
+ <code
280
+ className={cn(
281
+ "font-mono text-sm",
282
+ showLineNumbers && "[counter-increment:line_0] [counter-reset:line]"
283
+ )}
284
+ >
285
+ {keyedLines.map((keyedLine) => (
286
+ <LineSpan
287
+ key={keyedLine.key}
288
+ keyedLine={keyedLine}
289
+ showLineNumbers={showLineNumbers}
290
+ />
291
+ ))}
292
+ </code>
293
+ </pre>
294
+ );
295
+ },
296
+ (prevProps, nextProps) =>
297
+ prevProps.tokenized === nextProps.tokenized &&
298
+ prevProps.showLineNumbers === nextProps.showLineNumbers &&
299
+ prevProps.className === nextProps.className
300
+ );
301
+
302
+ CodeBlockBody.displayName = "CodeBlockBody";
303
+
304
+ export const CodeBlockContainer = ({
305
+ className,
306
+ language,
307
+ style,
308
+ ...props
309
+ }: HTMLAttributes<HTMLDivElement> & { language: string }) => (
310
+ <div
311
+ className={cn(
312
+ "group relative w-full overflow-hidden rounded-md border bg-background text-foreground",
313
+ className
314
+ )}
315
+ data-language={language}
316
+ style={{
317
+ containIntrinsicSize: "auto 200px",
318
+ contentVisibility: "auto",
319
+ ...style,
320
+ }}
321
+ {...props}
322
+ />
323
+ );
324
+
325
+ export const CodeBlockHeader = ({
326
+ children,
327
+ className,
328
+ ...props
329
+ }: HTMLAttributes<HTMLDivElement>) => (
330
+ <div
331
+ className={cn(
332
+ "flex items-center justify-between border-b bg-muted/80 px-3 py-2 text-muted-foreground text-xs",
333
+ className
334
+ )}
335
+ {...props}
336
+ >
337
+ {children}
338
+ </div>
339
+ );
340
+
341
+ export const CodeBlockTitle = ({
342
+ children,
343
+ className,
344
+ ...props
345
+ }: HTMLAttributes<HTMLDivElement>) => (
346
+ <div className={cn("flex items-center gap-2", className)} {...props}>
347
+ {children}
348
+ </div>
349
+ );
350
+
351
+ export const CodeBlockFilename = ({
352
+ children,
353
+ className,
354
+ ...props
355
+ }: HTMLAttributes<HTMLSpanElement>) => (
356
+ <span className={cn("font-mono", className)} {...props}>
357
+ {children}
358
+ </span>
359
+ );
360
+
361
+ export const CodeBlockActions = ({
362
+ children,
363
+ className,
364
+ ...props
365
+ }: HTMLAttributes<HTMLDivElement>) => (
366
+ <div
367
+ className={cn("-my-1 -mr-1 flex items-center gap-2", className)}
368
+ {...props}
369
+ >
370
+ {children}
371
+ </div>
372
+ );
373
+
374
+ export const CodeBlockContent = ({
375
+ code,
376
+ language,
377
+ showLineNumbers = false,
378
+ }: {
379
+ code: string;
380
+ language: BundledLanguage;
381
+ showLineNumbers?: boolean;
382
+ }) => {
383
+ // Memoized raw tokens for immediate display
384
+ const rawTokens = useMemo(() => createRawTokens(code), [code]);
385
+
386
+ // Synchronous cache lookup — avoids setState in effect for cached results
387
+ const syncTokens = useMemo(
388
+ () => highlightCode(code, language) ?? rawTokens,
389
+ [code, language, rawTokens]
390
+ );
391
+
392
+ // Async highlighting result (populated after shiki loads)
393
+ const tokensKey = useMemo(() => getTokensCacheKey(code, language), [code, language]);
394
+ const [asyncTokens, setAsyncTokens] = useState<{
395
+ key: string;
396
+ result: TokenizedCode;
397
+ } | null>(null);
398
+
399
+ useEffect(() => {
400
+ let cancelled = false;
401
+
402
+ highlightCode(code, language, (result) => {
403
+ if (!cancelled) {
404
+ setAsyncTokens({ key: tokensKey, result });
405
+ }
406
+ });
407
+
408
+ return () => {
409
+ cancelled = true;
410
+ };
411
+ }, [code, language, tokensKey]);
412
+
413
+ const tokenized = asyncTokens?.key === tokensKey ? asyncTokens.result : syncTokens;
414
+
415
+ return (
416
+ <div className="relative overflow-auto">
417
+ <CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} />
418
+ </div>
419
+ );
420
+ };
421
+
422
+ export const CodeBlock = ({
423
+ code,
424
+ language,
425
+ showLineNumbers = false,
426
+ className,
427
+ children,
428
+ ...props
429
+ }: CodeBlockProps) => {
430
+ const contextValue = useMemo(() => ({ code }), [code]);
431
+
432
+ return (
433
+ <CodeBlockContext.Provider value={contextValue}>
434
+ <CodeBlockContainer className={className} language={language} {...props}>
435
+ {children}
436
+ <CodeBlockContent
437
+ code={code}
438
+ language={language}
439
+ showLineNumbers={showLineNumbers}
440
+ />
441
+ </CodeBlockContainer>
442
+ </CodeBlockContext.Provider>
443
+ );
444
+ };
445
+
446
+ export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
447
+ onCopy?: () => void;
448
+ onError?: (error: Error) => void;
449
+ timeout?: number;
450
+ };
451
+
452
+ export const CodeBlockCopyButton = ({
453
+ onCopy,
454
+ onError,
455
+ timeout = 2000,
456
+ children,
457
+ className,
458
+ ...props
459
+ }: CodeBlockCopyButtonProps) => {
460
+ const [isCopied, setIsCopied] = useState(false);
461
+ const timeoutRef = useRef<number>(0);
462
+ const { code } = useContext(CodeBlockContext);
463
+
464
+ const copyToClipboard = useCallback(async () => {
465
+ if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
466
+ onError?.(new Error("Clipboard API not available"));
467
+ return;
468
+ }
469
+
470
+ try {
471
+ if (!isCopied) {
472
+ await navigator.clipboard.writeText(code);
473
+ setIsCopied(true);
474
+ onCopy?.();
475
+ timeoutRef.current = window.setTimeout(
476
+ () => setIsCopied(false),
477
+ timeout
478
+ );
479
+ }
480
+ } catch (error) {
481
+ onError?.(error as Error);
482
+ }
483
+ }, [code, onCopy, onError, timeout, isCopied]);
484
+
485
+ useEffect(
486
+ () => () => {
487
+ window.clearTimeout(timeoutRef.current);
488
+ },
489
+ []
490
+ );
491
+
492
+ const Icon = isCopied ? CheckIcon : CopyIcon;
493
+
494
+ return (
495
+ <Button
496
+ className={cn("shrink-0", className)}
497
+ onClick={copyToClipboard}
498
+ size="icon"
499
+ variant="ghost"
500
+ {...props}
501
+ >
502
+ {children ?? <Icon size={14} />}
503
+ </Button>
504
+ );
505
+ };
506
+
507
+ export type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>;
508
+
509
+ export const CodeBlockLanguageSelector = (
510
+ props: CodeBlockLanguageSelectorProps
511
+ ) => <Select {...props} />;
512
+
513
+ export type CodeBlockLanguageSelectorTriggerProps = ComponentProps<
514
+ typeof SelectTrigger
515
+ >;
516
+
517
+ export const CodeBlockLanguageSelectorTrigger = ({
518
+ className,
519
+ ...props
520
+ }: CodeBlockLanguageSelectorTriggerProps) => (
521
+ <SelectTrigger
522
+ className={cn(
523
+ "h-7 border-none bg-transparent px-2 text-xs shadow-none",
524
+ className
525
+ )}
526
+ size="sm"
527
+ {...props}
528
+ />
529
+ );
530
+
531
+ export type CodeBlockLanguageSelectorValueProps = ComponentProps<
532
+ typeof SelectValue
533
+ >;
534
+
535
+ export const CodeBlockLanguageSelectorValue = (
536
+ props: CodeBlockLanguageSelectorValueProps
537
+ ) => <SelectValue {...props} />;
538
+
539
+ export type CodeBlockLanguageSelectorContentProps = ComponentProps<
540
+ typeof SelectContent
541
+ >;
542
+
543
+ export const CodeBlockLanguageSelectorContent = ({
544
+ align = "end",
545
+ ...props
546
+ }: CodeBlockLanguageSelectorContentProps) => (
547
+ <SelectContent align={align} {...props} />
548
+ );
549
+
550
+ export type CodeBlockLanguageSelectorItemProps = ComponentProps<
551
+ typeof SelectItem
552
+ >;
553
+
554
+ export const CodeBlockLanguageSelectorItem = (
555
+ props: CodeBlockLanguageSelectorItemProps
556
+ ) => <SelectItem {...props} />;
src/components/ai-elements/message.tsx ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import {
5
+ ButtonGroup,
6
+ ButtonGroupText,
7
+ } from "@/components/ui/button-group";
8
+ import {
9
+ Tooltip,
10
+ TooltipContent,
11
+ TooltipProvider,
12
+ TooltipTrigger,
13
+ } from "@/components/ui/tooltip";
14
+ import { cn } from "@/lib/utils";
15
+ import { cjk } from "@streamdown/cjk";
16
+ import { code } from "@streamdown/code";
17
+ import { math } from "@streamdown/math";
18
+ import { mermaid } from "@streamdown/mermaid";
19
+ import type { UIMessage } from "ai";
20
+ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
21
+ import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
22
+ import {
23
+ createContext,
24
+ memo,
25
+ useCallback,
26
+ useContext,
27
+ useEffect,
28
+ useMemo,
29
+ useState,
30
+ } from "react";
31
+ import { Streamdown } from "streamdown";
32
+
33
+ export type MessageProps = HTMLAttributes<HTMLDivElement> & {
34
+ from: UIMessage["role"];
35
+ };
36
+
37
+ export const Message = ({ className, from, ...props }: MessageProps) => (
38
+ <div
39
+ className={cn(
40
+ "group flex w-full max-w-[95%] flex-col gap-2",
41
+ from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ );
47
+
48
+ export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
49
+
50
+ export const MessageContent = ({
51
+ children,
52
+ className,
53
+ ...props
54
+ }: MessageContentProps) => (
55
+ <div
56
+ className={cn(
57
+ "is-user:dark flex w-fit min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm",
58
+ "group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground",
59
+ "group-[.is-assistant]:text-foreground",
60
+ className
61
+ )}
62
+ {...props}
63
+ >
64
+ {children}
65
+ </div>
66
+ );
67
+
68
+ export type MessageActionsProps = ComponentProps<"div">;
69
+
70
+ export const MessageActions = ({
71
+ className,
72
+ children,
73
+ ...props
74
+ }: MessageActionsProps) => (
75
+ <div className={cn("flex items-center gap-1", className)} {...props}>
76
+ {children}
77
+ </div>
78
+ );
79
+
80
+ export type MessageActionProps = ComponentProps<typeof Button> & {
81
+ tooltip?: string;
82
+ label?: string;
83
+ };
84
+
85
+ export const MessageAction = ({
86
+ tooltip,
87
+ children,
88
+ label,
89
+ variant = "ghost",
90
+ size = "icon-sm",
91
+ ...props
92
+ }: MessageActionProps) => {
93
+ const button = (
94
+ <Button size={size} type="button" variant={variant} {...props}>
95
+ {children}
96
+ <span className="sr-only">{label || tooltip}</span>
97
+ </Button>
98
+ );
99
+
100
+ if (tooltip) {
101
+ return (
102
+ <TooltipProvider>
103
+ <Tooltip>
104
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
105
+ <TooltipContent>
106
+ <p>{tooltip}</p>
107
+ </TooltipContent>
108
+ </Tooltip>
109
+ </TooltipProvider>
110
+ );
111
+ }
112
+
113
+ return button;
114
+ };
115
+
116
+ interface MessageBranchContextType {
117
+ currentBranch: number;
118
+ totalBranches: number;
119
+ goToPrevious: () => void;
120
+ goToNext: () => void;
121
+ branches: ReactElement[];
122
+ setBranches: (branches: ReactElement[]) => void;
123
+ }
124
+
125
+ const MessageBranchContext = createContext<MessageBranchContextType | null>(
126
+ null
127
+ );
128
+
129
+ const useMessageBranch = () => {
130
+ const context = useContext(MessageBranchContext);
131
+
132
+ if (!context) {
133
+ throw new Error(
134
+ "MessageBranch components must be used within MessageBranch"
135
+ );
136
+ }
137
+
138
+ return context;
139
+ };
140
+
141
+ export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
142
+ defaultBranch?: number;
143
+ onBranchChange?: (branchIndex: number) => void;
144
+ };
145
+
146
+ export const MessageBranch = ({
147
+ defaultBranch = 0,
148
+ onBranchChange,
149
+ className,
150
+ ...props
151
+ }: MessageBranchProps) => {
152
+ const [currentBranch, setCurrentBranch] = useState(defaultBranch);
153
+ const [branches, setBranches] = useState<ReactElement[]>([]);
154
+
155
+ const handleBranchChange = useCallback(
156
+ (newBranch: number) => {
157
+ setCurrentBranch(newBranch);
158
+ onBranchChange?.(newBranch);
159
+ },
160
+ [onBranchChange]
161
+ );
162
+
163
+ const goToPrevious = useCallback(() => {
164
+ const newBranch =
165
+ currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
166
+ handleBranchChange(newBranch);
167
+ }, [currentBranch, branches.length, handleBranchChange]);
168
+
169
+ const goToNext = useCallback(() => {
170
+ const newBranch =
171
+ currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
172
+ handleBranchChange(newBranch);
173
+ }, [currentBranch, branches.length, handleBranchChange]);
174
+
175
+ const contextValue = useMemo<MessageBranchContextType>(
176
+ () => ({
177
+ branches,
178
+ currentBranch,
179
+ goToNext,
180
+ goToPrevious,
181
+ setBranches,
182
+ totalBranches: branches.length,
183
+ }),
184
+ [branches, currentBranch, goToNext, goToPrevious]
185
+ );
186
+
187
+ return (
188
+ <MessageBranchContext.Provider value={contextValue}>
189
+ <div
190
+ className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
191
+ {...props}
192
+ />
193
+ </MessageBranchContext.Provider>
194
+ );
195
+ };
196
+
197
+ export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
198
+
199
+ export const MessageBranchContent = ({
200
+ children,
201
+ ...props
202
+ }: MessageBranchContentProps) => {
203
+ const { currentBranch, setBranches, branches } = useMessageBranch();
204
+ const childrenArray = useMemo(
205
+ () => (Array.isArray(children) ? children : [children]),
206
+ [children]
207
+ );
208
+
209
+ // Use useEffect to update branches when they change
210
+ useEffect(() => {
211
+ if (branches.length !== childrenArray.length) {
212
+ setBranches(childrenArray);
213
+ }
214
+ }, [childrenArray, branches, setBranches]);
215
+
216
+ return childrenArray.map((branch, index) => (
217
+ <div
218
+ className={cn(
219
+ "grid gap-2 overflow-hidden [&>div]:pb-0",
220
+ index === currentBranch ? "block" : "hidden"
221
+ )}
222
+ key={branch.key}
223
+ {...props}
224
+ >
225
+ {branch}
226
+ </div>
227
+ ));
228
+ };
229
+
230
+ export type MessageBranchSelectorProps = ComponentProps<typeof ButtonGroup>;
231
+
232
+ export const MessageBranchSelector = ({
233
+ className,
234
+ ...props
235
+ }: MessageBranchSelectorProps) => {
236
+ const { totalBranches } = useMessageBranch();
237
+
238
+ // Don't render if there's only one branch
239
+ if (totalBranches <= 1) {
240
+ return null;
241
+ }
242
+
243
+ return (
244
+ <ButtonGroup
245
+ className={cn(
246
+ "[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
247
+ className
248
+ )}
249
+ orientation="horizontal"
250
+ {...props}
251
+ />
252
+ );
253
+ };
254
+
255
+ export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
256
+
257
+ export const MessageBranchPrevious = ({
258
+ children,
259
+ ...props
260
+ }: MessageBranchPreviousProps) => {
261
+ const { goToPrevious, totalBranches } = useMessageBranch();
262
+
263
+ return (
264
+ <Button
265
+ aria-label="Previous branch"
266
+ disabled={totalBranches <= 1}
267
+ onClick={goToPrevious}
268
+ size="icon-sm"
269
+ type="button"
270
+ variant="ghost"
271
+ {...props}
272
+ >
273
+ {children ?? <ChevronLeftIcon size={14} />}
274
+ </Button>
275
+ );
276
+ };
277
+
278
+ export type MessageBranchNextProps = ComponentProps<typeof Button>;
279
+
280
+ export const MessageBranchNext = ({
281
+ children,
282
+ ...props
283
+ }: MessageBranchNextProps) => {
284
+ const { goToNext, totalBranches } = useMessageBranch();
285
+
286
+ return (
287
+ <Button
288
+ aria-label="Next branch"
289
+ disabled={totalBranches <= 1}
290
+ onClick={goToNext}
291
+ size="icon-sm"
292
+ type="button"
293
+ variant="ghost"
294
+ {...props}
295
+ >
296
+ {children ?? <ChevronRightIcon size={14} />}
297
+ </Button>
298
+ );
299
+ };
300
+
301
+ export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
302
+
303
+ export const MessageBranchPage = ({
304
+ className,
305
+ ...props
306
+ }: MessageBranchPageProps) => {
307
+ const { currentBranch, totalBranches } = useMessageBranch();
308
+
309
+ return (
310
+ <ButtonGroupText
311
+ className={cn(
312
+ "border-none bg-transparent text-muted-foreground shadow-none",
313
+ className
314
+ )}
315
+ {...props}
316
+ >
317
+ {currentBranch + 1} of {totalBranches}
318
+ </ButtonGroupText>
319
+ );
320
+ };
321
+
322
+ export type MessageResponseProps = ComponentProps<typeof Streamdown>;
323
+
324
+ const streamdownPlugins = { cjk, code, math, mermaid };
325
+
326
+ export const MessageResponse = memo(
327
+ ({ className, ...props }: MessageResponseProps) => (
328
+ <Streamdown
329
+ className={cn(
330
+ "size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
331
+ className
332
+ )}
333
+ plugins={streamdownPlugins}
334
+ {...props}
335
+ />
336
+ ),
337
+ (prevProps, nextProps) =>
338
+ prevProps.children === nextProps.children &&
339
+ nextProps.isAnimating === prevProps.isAnimating
340
+ );
341
+
342
+ MessageResponse.displayName = "MessageResponse";
343
+
344
+ export type MessageToolbarProps = ComponentProps<"div">;
345
+
346
+ export const MessageToolbar = ({
347
+ className,
348
+ children,
349
+ ...props
350
+ }: MessageToolbarProps) => (
351
+ <div
352
+ className={cn(
353
+ "mt-4 flex w-full items-center justify-between gap-4",
354
+ className
355
+ )}
356
+ {...props}
357
+ >
358
+ {children}
359
+ </div>
360
+ );
src/components/ai-elements/terminal.tsx ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { cn } from "@/lib/utils";
5
+ import Ansi from "ansi-to-react";
6
+ import { CheckIcon, CopyIcon, TerminalIcon, Trash2Icon } from "lucide-react";
7
+ import type { ComponentProps, HTMLAttributes } from "react";
8
+ import {
9
+ createContext,
10
+ useCallback,
11
+ useContext,
12
+ useEffect,
13
+ useMemo,
14
+ useRef,
15
+ useState,
16
+ } from "react";
17
+
18
+ interface TerminalContextType {
19
+ output: string;
20
+ isStreaming: boolean;
21
+ autoScroll: boolean;
22
+ onClear?: () => void;
23
+ }
24
+
25
+ const TerminalContext = createContext<TerminalContextType>({
26
+ autoScroll: true,
27
+ isStreaming: false,
28
+ output: "",
29
+ });
30
+
31
+ export type TerminalHeaderProps = HTMLAttributes<HTMLDivElement>;
32
+
33
+ export const TerminalHeader = ({
34
+ className,
35
+ children,
36
+ ...props
37
+ }: TerminalHeaderProps) => (
38
+ <div
39
+ className={cn(
40
+ "flex items-center justify-between border-zinc-800 border-b px-4 py-2",
41
+ className
42
+ )}
43
+ {...props}
44
+ >
45
+ {children}
46
+ </div>
47
+ );
48
+
49
+ export type TerminalTitleProps = HTMLAttributes<HTMLDivElement>;
50
+
51
+ export const TerminalTitle = ({
52
+ className,
53
+ children,
54
+ ...props
55
+ }: TerminalTitleProps) => (
56
+ <div
57
+ className={cn("flex items-center gap-2 text-sm text-zinc-400", className)}
58
+ {...props}
59
+ >
60
+ <TerminalIcon className="size-4" />
61
+ {children ?? "Terminal"}
62
+ </div>
63
+ );
64
+
65
+ export type TerminalStatusProps = HTMLAttributes<HTMLDivElement>;
66
+
67
+ export const TerminalStatus = ({
68
+ className,
69
+ children,
70
+ ...props
71
+ }: TerminalStatusProps) => {
72
+ const { isStreaming } = useContext(TerminalContext);
73
+
74
+ if (!isStreaming) {
75
+ return null;
76
+ }
77
+
78
+ return (
79
+ <div
80
+ className={cn("flex items-center gap-2 text-xs text-zinc-400", className)}
81
+ {...props}
82
+ >
83
+ {children}
84
+ </div>
85
+ );
86
+ };
87
+
88
+ export type TerminalActionsProps = HTMLAttributes<HTMLDivElement>;
89
+
90
+ export const TerminalActions = ({
91
+ className,
92
+ children,
93
+ ...props
94
+ }: TerminalActionsProps) => (
95
+ <div className={cn("flex items-center gap-1", className)} {...props}>
96
+ {children}
97
+ </div>
98
+ );
99
+
100
+ export type TerminalCopyButtonProps = ComponentProps<typeof Button> & {
101
+ onCopy?: () => void;
102
+ onError?: (error: Error) => void;
103
+ timeout?: number;
104
+ };
105
+
106
+ export const TerminalCopyButton = ({
107
+ onCopy,
108
+ onError,
109
+ timeout = 2000,
110
+ children,
111
+ className,
112
+ ...props
113
+ }: TerminalCopyButtonProps) => {
114
+ const [isCopied, setIsCopied] = useState(false);
115
+ const timeoutRef = useRef<number>(0);
116
+ const { output } = useContext(TerminalContext);
117
+
118
+ const copyToClipboard = useCallback(async () => {
119
+ if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
120
+ onError?.(new Error("Clipboard API not available"));
121
+ return;
122
+ }
123
+
124
+ try {
125
+ await navigator.clipboard.writeText(output);
126
+ setIsCopied(true);
127
+ onCopy?.();
128
+ timeoutRef.current = window.setTimeout(() => setIsCopied(false), timeout);
129
+ } catch (error) {
130
+ onError?.(error as Error);
131
+ }
132
+ }, [output, onCopy, onError, timeout]);
133
+
134
+ useEffect(
135
+ () => () => {
136
+ window.clearTimeout(timeoutRef.current);
137
+ },
138
+ []
139
+ );
140
+
141
+ const Icon = isCopied ? CheckIcon : CopyIcon;
142
+
143
+ return (
144
+ <Button
145
+ className={cn(
146
+ "size-7 shrink-0 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100",
147
+ className
148
+ )}
149
+ onClick={copyToClipboard}
150
+ size="icon"
151
+ variant="ghost"
152
+ {...props}
153
+ >
154
+ {children ?? <Icon size={14} />}
155
+ </Button>
156
+ );
157
+ };
158
+
159
+ export type TerminalClearButtonProps = ComponentProps<typeof Button>;
160
+
161
+ export const TerminalClearButton = ({
162
+ children,
163
+ className,
164
+ ...props
165
+ }: TerminalClearButtonProps) => {
166
+ const { onClear } = useContext(TerminalContext);
167
+
168
+ if (!onClear) {
169
+ return null;
170
+ }
171
+
172
+ return (
173
+ <Button
174
+ className={cn(
175
+ "size-7 shrink-0 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100",
176
+ className
177
+ )}
178
+ onClick={onClear}
179
+ size="icon"
180
+ variant="ghost"
181
+ {...props}
182
+ >
183
+ {children ?? <Trash2Icon size={14} />}
184
+ </Button>
185
+ );
186
+ };
187
+
188
+ export type TerminalContentProps = HTMLAttributes<HTMLDivElement>;
189
+
190
+ export const TerminalContent = ({
191
+ className,
192
+ children,
193
+ ...props
194
+ }: TerminalContentProps) => {
195
+ const { output, isStreaming, autoScroll } = useContext(TerminalContext);
196
+ const containerRef = useRef<HTMLDivElement>(null);
197
+
198
+ useEffect(() => {
199
+ if (autoScroll && containerRef.current) {
200
+ containerRef.current.scrollTop = containerRef.current.scrollHeight;
201
+ }
202
+ }, [output, autoScroll]);
203
+
204
+ return (
205
+ <div
206
+ className={cn(
207
+ "max-h-96 overflow-auto p-4 font-mono text-sm leading-relaxed",
208
+ className
209
+ )}
210
+ ref={containerRef}
211
+ {...props}
212
+ >
213
+ {children ?? (
214
+ <pre className="whitespace-pre-wrap break-words">
215
+ <Ansi>{output}</Ansi>
216
+ {isStreaming && (
217
+ <span className="ml-0.5 inline-block h-4 w-2 animate-pulse bg-zinc-100" />
218
+ )}
219
+ </pre>
220
+ )}
221
+ </div>
222
+ );
223
+ };
224
+
225
+ export type TerminalProps = HTMLAttributes<HTMLDivElement> & {
226
+ output: string;
227
+ isStreaming?: boolean;
228
+ autoScroll?: boolean;
229
+ onClear?: () => void;
230
+ };
231
+
232
+ export const Terminal = ({
233
+ output,
234
+ isStreaming = false,
235
+ autoScroll = true,
236
+ onClear,
237
+ className,
238
+ children,
239
+ ...props
240
+ }: TerminalProps) => {
241
+ const contextValue = useMemo(
242
+ () => ({ autoScroll, isStreaming, onClear, output }),
243
+ [autoScroll, isStreaming, onClear, output]
244
+ );
245
+
246
+ return (
247
+ <TerminalContext.Provider value={contextValue}>
248
+ <div
249
+ className={cn(
250
+ "flex flex-col overflow-hidden rounded-lg border bg-zinc-950 text-zinc-100",
251
+ className
252
+ )}
253
+ {...props}
254
+ >
255
+ {children ?? (
256
+ <>
257
+ <TerminalHeader>
258
+ <TerminalTitle />
259
+ <div className="flex items-center gap-1">
260
+ <TerminalStatus />
261
+ <TerminalActions>
262
+ <TerminalCopyButton />
263
+ {onClear && <TerminalClearButton />}
264
+ </TerminalActions>
265
+ </div>
266
+ </TerminalHeader>
267
+ <TerminalContent />
268
+ </>
269
+ )}
270
+ </div>
271
+ </TerminalContext.Provider>
272
+ );
273
+ };
src/components/ai-elements/tool.tsx ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Badge } from "@/components/ui/badge";
4
+ import {
5
+ Collapsible,
6
+ CollapsibleContent,
7
+ CollapsibleTrigger,
8
+ } from "@/components/ui/collapsible";
9
+ import { cn } from "@/lib/utils";
10
+ import type { DynamicToolUIPart, ToolUIPart } from "ai";
11
+ import {
12
+ CheckCircleIcon,
13
+ ChevronDownIcon,
14
+ CircleIcon,
15
+ ClockIcon,
16
+ WrenchIcon,
17
+ XCircleIcon,
18
+ } from "lucide-react";
19
+ import type { ComponentProps, ReactNode } from "react";
20
+ import { isValidElement } from "react";
21
+
22
+ import { CodeBlock } from "./code-block";
23
+
24
+ export type ToolProps = ComponentProps<typeof Collapsible>;
25
+
26
+ export const Tool = ({ className, ...props }: ToolProps) => (
27
+ <Collapsible
28
+ className={cn("group not-prose mb-4 w-full rounded-md border", className)}
29
+ {...props}
30
+ />
31
+ );
32
+
33
+ export type ToolPart = ToolUIPart | DynamicToolUIPart;
34
+
35
+ export type ToolHeaderProps = {
36
+ title?: string;
37
+ className?: string;
38
+ } & (
39
+ | { type: ToolUIPart["type"]; state: ToolUIPart["state"]; toolName?: never }
40
+ | {
41
+ type: DynamicToolUIPart["type"];
42
+ state: DynamicToolUIPart["state"];
43
+ toolName: string;
44
+ }
45
+ );
46
+
47
+ const statusLabels: Record<ToolPart["state"], string> = {
48
+ "approval-requested": "Awaiting Approval",
49
+ "approval-responded": "Responded",
50
+ "input-available": "Running",
51
+ "input-streaming": "Pending",
52
+ "output-available": "Completed",
53
+ "output-denied": "Denied",
54
+ "output-error": "Error",
55
+ };
56
+
57
+ const statusIcons: Record<ToolPart["state"], ReactNode> = {
58
+ "approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
59
+ "approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
60
+ "input-available": <ClockIcon className="size-4 animate-pulse" />,
61
+ "input-streaming": <CircleIcon className="size-4" />,
62
+ "output-available": <CheckCircleIcon className="size-4 text-green-600" />,
63
+ "output-denied": <XCircleIcon className="size-4 text-orange-600" />,
64
+ "output-error": <XCircleIcon className="size-4 text-red-600" />,
65
+ };
66
+
67
+ export const getStatusBadge = (status: ToolPart["state"]) => (
68
+ <Badge className="gap-1.5 rounded-full text-xs" variant="secondary">
69
+ {statusIcons[status]}
70
+ {statusLabels[status]}
71
+ </Badge>
72
+ );
73
+
74
+ export const ToolHeader = ({
75
+ className,
76
+ title,
77
+ type,
78
+ state,
79
+ toolName,
80
+ ...props
81
+ }: ToolHeaderProps) => {
82
+ const derivedName =
83
+ type === "dynamic-tool" ? toolName : type.split("-").slice(1).join("-");
84
+
85
+ return (
86
+ <CollapsibleTrigger
87
+ className={cn(
88
+ "flex w-full items-center justify-between gap-4 p-3",
89
+ className
90
+ )}
91
+ {...props}
92
+ >
93
+ <div className="flex items-center gap-2">
94
+ <WrenchIcon className="size-4 text-muted-foreground" />
95
+ <span className="font-medium text-sm">{title ?? derivedName}</span>
96
+ {getStatusBadge(state)}
97
+ </div>
98
+ <ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
99
+ </CollapsibleTrigger>
100
+ );
101
+ };
102
+
103
+ export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
104
+
105
+ export const ToolContent = ({ className, ...props }: ToolContentProps) => (
106
+ <CollapsibleContent
107
+ className={cn(
108
+ "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 space-y-4 p-4 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
109
+ className
110
+ )}
111
+ {...props}
112
+ />
113
+ );
114
+
115
+ export type ToolInputProps = ComponentProps<"div"> & {
116
+ input: ToolPart["input"];
117
+ };
118
+
119
+ export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
120
+ <div className={cn("space-y-2 overflow-hidden", className)} {...props}>
121
+ <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
122
+ Parameters
123
+ </h4>
124
+ <div className="rounded-md bg-muted/50">
125
+ <CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
126
+ </div>
127
+ </div>
128
+ );
129
+
130
+ export type ToolOutputProps = ComponentProps<"div"> & {
131
+ output: ToolPart["output"];
132
+ errorText: ToolPart["errorText"];
133
+ };
134
+
135
+ export const ToolOutput = ({
136
+ className,
137
+ output,
138
+ errorText,
139
+ ...props
140
+ }: ToolOutputProps) => {
141
+ if (!(output || errorText)) {
142
+ return null;
143
+ }
144
+
145
+ let Output = <div>{output as ReactNode}</div>;
146
+
147
+ if (typeof output === "object" && !isValidElement(output)) {
148
+ Output = (
149
+ <CodeBlock code={JSON.stringify(output, null, 2)} language="json" />
150
+ );
151
+ } else if (typeof output === "string") {
152
+ Output = <CodeBlock code={output} language="json" />;
153
+ }
154
+
155
+ return (
156
+ <div className={cn("space-y-2", className)} {...props}>
157
+ <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
158
+ {errorText ? "Error" : "Result"}
159
+ </h4>
160
+ <div
161
+ className={cn(
162
+ "overflow-x-auto rounded-md text-xs [&_table]:w-full",
163
+ errorText
164
+ ? "bg-destructive/10 text-destructive"
165
+ : "bg-muted/50 text-foreground"
166
+ )}
167
+ >
168
+ {errorText && <div>{errorText}</div>}
169
+ {Output}
170
+ </div>
171
+ </div>
172
+ );
173
+ };
src/components/rocmpilot-dashboard.tsx ADDED
@@ -0,0 +1,907 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import {
4
+ CodeBlock,
5
+ CodeBlockCopyButton,
6
+ CodeBlockFilename,
7
+ CodeBlockHeader,
8
+ CodeBlockTitle,
9
+ } from "@/components/ai-elements/code-block";
10
+ import { MessageResponse } from "@/components/ai-elements/message";
11
+ import { Terminal } from "@/components/ai-elements/terminal";
12
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
13
+ import { Badge } from "@/components/ui/badge";
14
+ import { Button } from "@/components/ui/button";
15
+ import {
16
+ Card,
17
+ CardContent,
18
+ CardDescription,
19
+ CardHeader,
20
+ CardTitle,
21
+ } from "@/components/ui/card";
22
+ import { Input } from "@/components/ui/input";
23
+ import { Label } from "@/components/ui/label";
24
+ import { Progress } from "@/components/ui/progress";
25
+ import {
26
+ Select,
27
+ SelectContent,
28
+ SelectItem,
29
+ SelectTrigger,
30
+ SelectValue,
31
+ } from "@/components/ui/select";
32
+ import { Separator } from "@/components/ui/separator";
33
+ import {
34
+ Table,
35
+ TableBody,
36
+ TableCell,
37
+ TableHead,
38
+ TableHeader,
39
+ TableRow,
40
+ } from "@/components/ui/table";
41
+ import { SAMPLE_REPOS } from "@/lib/rocmpilot/data";
42
+ import type {
43
+ AgentMemory,
44
+ AgentMessage,
45
+ AgentMessageKind,
46
+ FindingSeverity,
47
+ LongContextMemoryStatus,
48
+ ReportResponse,
49
+ RocmRun,
50
+ RunStage,
51
+ } from "@/lib/rocmpilot/types";
52
+ import {
53
+ Activity,
54
+ BadgeCheck,
55
+ Bot,
56
+ Boxes,
57
+ BrainCircuit,
58
+ CheckCircle2,
59
+ Cpu,
60
+ FileCode2,
61
+ Gauge,
62
+ GitBranch,
63
+ Loader2,
64
+ Play,
65
+ RefreshCw,
66
+ ShieldCheck,
67
+ Sparkles,
68
+ TerminalSquare,
69
+ TriangleAlert,
70
+ Zap,
71
+ } from "lucide-react";
72
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
73
+
74
+ const severityTone: Record<FindingSeverity, string> = {
75
+ critical: "border-red-500/40 bg-red-500/10 text-red-200",
76
+ high: "border-amber-500/40 bg-amber-500/10 text-amber-100",
77
+ medium: "border-cyan-500/40 bg-cyan-500/10 text-cyan-100",
78
+ low: "border-emerald-500/40 bg-emerald-500/10 text-emerald-100",
79
+ };
80
+
81
+ const statusTone = {
82
+ pending: "border-zinc-800 bg-zinc-950 text-zinc-500",
83
+ running: "border-cyan-500/40 bg-cyan-500/10 text-cyan-100",
84
+ completed: "border-emerald-500/40 bg-emerald-500/10 text-emerald-100",
85
+ };
86
+
87
+ const memoryStatusTone: Record<LongContextMemoryStatus["status"], string> = {
88
+ connected: "border-emerald-500/40 bg-emerald-500/10 text-emerald-100",
89
+ configured: "border-cyan-500/40 bg-cyan-500/10 text-cyan-100",
90
+ fallback: "border-amber-500/40 bg-amber-500/10 text-amber-100",
91
+ "not-configured": "border-zinc-700 bg-zinc-900 text-zinc-300",
92
+ };
93
+
94
+ const messageKindTone: Record<AgentMessageKind, string> = {
95
+ question: "border-cyan-500/40 bg-cyan-500/10 text-cyan-100",
96
+ answer: "border-sky-500/40 bg-sky-500/10 text-sky-100",
97
+ challenge: "border-amber-500/40 bg-amber-500/10 text-amber-100",
98
+ proposal: "border-violet-500/40 bg-violet-500/10 text-violet-100",
99
+ decision: "border-lime-500/40 bg-lime-500/10 text-lime-100",
100
+ memory: "border-rose-500/40 bg-rose-500/10 text-rose-100",
101
+ consensus: "border-emerald-500/40 bg-emerald-500/10 text-emerald-100",
102
+ };
103
+
104
+ const agentDotTone: Record<string, string> = {
105
+ Orchestrator: "bg-emerald-300",
106
+ "Repo Doctor": "bg-cyan-300",
107
+ "Migration Planner": "bg-violet-300",
108
+ "Build Runner": "bg-amber-300",
109
+ "Benchmark Agent": "bg-sky-300",
110
+ "Report Agent": "bg-rose-300",
111
+ };
112
+
113
+ function StageIcon({ stage }: { stage: RunStage }) {
114
+ if (stage.status === "completed") {
115
+ return <CheckCircle2 className="size-4 text-emerald-300" />;
116
+ }
117
+
118
+ if (stage.status === "running") {
119
+ return <Loader2 className="size-4 animate-spin text-cyan-200" />;
120
+ }
121
+
122
+ return <Activity className="size-4 text-zinc-500" />;
123
+ }
124
+
125
+ function formatTime(value: string | undefined) {
126
+ if (!value) {
127
+ return "--:--";
128
+ }
129
+
130
+ return new Intl.DateTimeFormat("en", {
131
+ hour: "2-digit",
132
+ minute: "2-digit",
133
+ second: "2-digit",
134
+ }).format(new Date(value));
135
+ }
136
+
137
+ function WarRoomMessage({
138
+ message,
139
+ memoryById,
140
+ }: {
141
+ message: AgentMessage;
142
+ memoryById: Map<string, AgentMemory>;
143
+ }) {
144
+ return (
145
+ <div className="grid grid-cols-[28px_minmax(0,1fr)] gap-3 rounded-lg border border-border/70 bg-background/60 p-3">
146
+ <div className="relative mt-1 flex size-7 items-center justify-center rounded-full border border-border bg-card">
147
+ <span
148
+ className={`size-2.5 rounded-full ${agentDotTone[message.agent] ?? "bg-zinc-400"}`}
149
+ />
150
+ </div>
151
+ <div className="min-w-0 space-y-2">
152
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
153
+ <p className="truncate text-sm font-semibold">{message.agent}</p>
154
+ <span className="text-xs text-muted-foreground">to</span>
155
+ <p className="truncate text-sm font-medium text-cyan-100">{message.toAgent}</p>
156
+ <Badge variant="outline" className={messageKindTone[message.kind]}>
157
+ {message.kind}
158
+ </Badge>
159
+ {message.leadAgent === message.agent && (
160
+ <Badge variant="outline" className="border-emerald-500/40 bg-emerald-500/10 text-emerald-100">
161
+ lead
162
+ </Badge>
163
+ )}
164
+ <span className="font-mono text-[11px] text-muted-foreground">
165
+ {formatTime(message.createdAt)}
166
+ </span>
167
+ </div>
168
+ <div className="flex flex-wrap items-center gap-2 text-xs leading-5 text-muted-foreground">
169
+ <span>{message.role}</span>
170
+ <span>/</span>
171
+ <span>{message.task}</span>
172
+ {message.replyToId && (
173
+ <>
174
+ <span>/</span>
175
+ <span>replying to earlier message</span>
176
+ </>
177
+ )}
178
+ </div>
179
+ <p className="break-words text-sm leading-6 text-foreground">{message.message}</p>
180
+ {message.memoryRefs.length > 0 && (
181
+ <div className="flex flex-wrap gap-2">
182
+ {message.memoryRefs.map((memoryId) => (
183
+ <Badge
184
+ key={memoryId}
185
+ variant="outline"
186
+ className="border-zinc-600 bg-zinc-900/80 text-zinc-200"
187
+ >
188
+ memory: {memoryById.get(memoryId)?.title ?? memoryId}
189
+ </Badge>
190
+ ))}
191
+ </div>
192
+ )}
193
+ </div>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ function AgentWarRoom({ run }: { run: RocmRun | null }) {
199
+ const messages = useMemo(() => run?.agentMessages ?? [], [run?.agentMessages]);
200
+ const memory = useMemo(() => run?.agentMemory ?? [], [run?.agentMemory]);
201
+ const latestMessage = messages[messages.length - 1];
202
+ const activeLead = latestMessage?.leadAgent ?? "No lead assigned";
203
+ const memoryById = useMemo(
204
+ () => new Map(memory.map((entry) => [entry.id, entry])),
205
+ [memory]
206
+ );
207
+
208
+ return (
209
+ <Card>
210
+ <CardHeader className="gap-3">
211
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
212
+ <div>
213
+ <CardTitle className="flex items-center gap-2 text-xl">
214
+ <Activity className="size-5 text-cyan-200" />
215
+ Agent War Room
216
+ </CardTitle>
217
+ <CardDescription>
218
+ Lead-led discussion with replies, objections, and shared memory the agents reuse later.
219
+ </CardDescription>
220
+ </div>
221
+ <div className="flex flex-wrap gap-2">
222
+ <Badge
223
+ variant="outline"
224
+ className={
225
+ run?.status === "running"
226
+ ? "border-cyan-500/40 bg-cyan-500/10 text-cyan-100"
227
+ : run?.status === "completed"
228
+ ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-100"
229
+ : "border-zinc-700 bg-zinc-900 text-zinc-300"
230
+ }
231
+ >
232
+ {run?.status === "running" ? "live room" : run?.status === "completed" ? "consensus" : "standby"}
233
+ </Badge>
234
+ <Badge variant="outline" className="border-zinc-700 bg-zinc-900 text-zinc-200">
235
+ lead: {activeLead}
236
+ </Badge>
237
+ {run?.longContextMemory && (
238
+ <Badge
239
+ variant="outline"
240
+ className={memoryStatusTone[run.longContextMemory.status]}
241
+ >
242
+ {run.longContextMemory.provider === "synap" ? "Synap memory" : "local memory"}
243
+ </Badge>
244
+ )}
245
+ </div>
246
+ </div>
247
+ {latestMessage && (
248
+ <div className="rounded-lg border border-emerald-500/20 bg-emerald-500/5 p-3 text-sm leading-6 text-emerald-50">
249
+ <span className="font-medium">
250
+ {latestMessage.agent} to {latestMessage.toAgent}:
251
+ </span>{" "}
252
+ <span className="text-emerald-100/90">{latestMessage.message}</span>
253
+ </div>
254
+ )}
255
+ </CardHeader>
256
+ <CardContent>
257
+ {messages.length ? (
258
+ <div className="grid gap-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(300px,0.75fr)]">
259
+ <div className="max-h-[520px] space-y-3 overflow-y-auto pr-1">
260
+ {messages.map((message) => (
261
+ <WarRoomMessage key={message.id} message={message} memoryById={memoryById} />
262
+ ))}
263
+ </div>
264
+ <div className="rounded-lg border border-border bg-background/50 p-3">
265
+ <div className="flex items-center justify-between gap-3">
266
+ <p className="text-sm font-semibold">Shared memory</p>
267
+ <Badge variant="outline" className="border-rose-500/40 bg-rose-500/10 text-rose-100">
268
+ {memory.length} stored
269
+ </Badge>
270
+ </div>
271
+ <div className="mt-3 max-h-[470px] space-y-3 overflow-y-auto pr-1">
272
+ {memory.length ? (
273
+ memory.map((entry) => (
274
+ <div key={entry.id} className="rounded-lg border border-border/70 bg-card p-3">
275
+ <div className="flex flex-wrap items-center gap-2">
276
+ <p className="text-sm font-medium">{entry.title}</p>
277
+ <Badge variant="outline">{entry.scope}</Badge>
278
+ </div>
279
+ <p className="mt-2 text-xs text-muted-foreground">
280
+ learned from {entry.learnedFromAgent} at {formatTime(entry.createdAt)}
281
+ </p>
282
+ <p className="mt-3 text-sm leading-6">{entry.summary}</p>
283
+ <p className="mt-2 text-sm leading-6 text-muted-foreground">{entry.solution}</p>
284
+ {entry.usedBy.length > 0 && (
285
+ <p className="mt-3 text-xs text-emerald-100">
286
+ reused by {Array.from(new Set(entry.usedBy)).join(", ")}
287
+ </p>
288
+ )}
289
+ </div>
290
+ ))
291
+ ) : (
292
+ <div className="rounded-lg border border-dashed border-border p-4 text-sm text-muted-foreground">
293
+ Memory appears when agents store a blocker, solution, or evidence rule.
294
+ </div>
295
+ )}
296
+ </div>
297
+ </div>
298
+ </div>
299
+ ) : (
300
+ <div className="grid gap-3 rounded-lg border border-dashed border-border p-4 sm:grid-cols-2 lg:grid-cols-3">
301
+ {[
302
+ "Repo Doctor",
303
+ "Migration Planner",
304
+ "Build Runner",
305
+ "Benchmark Agent",
306
+ "Report Agent",
307
+ "Orchestrator",
308
+ ].map((agent) => (
309
+ <div key={agent} className="flex min-w-0 items-center gap-3 rounded-lg bg-card p-3">
310
+ <span className={`size-2.5 shrink-0 rounded-full ${agentDotTone[agent] ?? "bg-zinc-400"}`} />
311
+ <span className="truncate text-sm text-muted-foreground">{agent} waiting</span>
312
+ </div>
313
+ ))}
314
+ </div>
315
+ )}
316
+ </CardContent>
317
+ </Card>
318
+ );
319
+ }
320
+
321
+ export function RocmPilotDashboard() {
322
+ const [sampleId, setSampleId] = useState(SAMPLE_REPOS[0].id);
323
+ const [githubUrl, setGithubUrl] = useState("");
324
+ const [run, setRun] = useState<RocmRun | null>(null);
325
+ const [report, setReport] = useState<ReportResponse | null>(null);
326
+ const [activePanel, setActivePanel] = useState<"patches" | "logs" | "report">("patches");
327
+ const [isStarting, setIsStarting] = useState(false);
328
+ const [isGeneratingReport, setIsGeneratingReport] = useState(false);
329
+ const [error, setError] = useState<string | null>(null);
330
+ const reportRequestedFor = useRef<string | null>(null);
331
+
332
+ const selectedSample = useMemo(
333
+ () => SAMPLE_REPOS.find((sample) => sample.id === sampleId) ?? SAMPLE_REPOS[0],
334
+ [sampleId]
335
+ );
336
+
337
+ const logOutput = useMemo(() => run?.logs.join("\n") ?? "", [run]);
338
+ const activeRepoUrl = run?.target.repoUrl ?? (githubUrl.trim() || selectedSample.repoUrl);
339
+
340
+ const startRun = useCallback(async () => {
341
+ setIsStarting(true);
342
+ setError(null);
343
+ setReport(null);
344
+ reportRequestedFor.current = null;
345
+
346
+ try {
347
+ const response = await fetch("/api/runs", {
348
+ method: "POST",
349
+ headers: { "Content-Type": "application/json" },
350
+ body: JSON.stringify({
351
+ sampleId,
352
+ mode: "mock",
353
+ repoUrl: githubUrl.trim() || undefined,
354
+ }),
355
+ });
356
+
357
+ if (!response.ok) {
358
+ throw new Error("Could not start migration audit");
359
+ }
360
+
361
+ const nextRun = (await response.json()) as RocmRun;
362
+ setRun(nextRun);
363
+ } catch (caught) {
364
+ setError(caught instanceof Error ? caught.message : "Unknown start error");
365
+ } finally {
366
+ setIsStarting(false);
367
+ }
368
+ }, [githubUrl, sampleId]);
369
+
370
+ const pollRun = useCallback(async (runId: string) => {
371
+ const response = await fetch(`/api/runs/${runId}`, { cache: "no-store" });
372
+
373
+ if (!response.ok) {
374
+ throw new Error("Could not refresh run state");
375
+ }
376
+
377
+ const nextRun = (await response.json()) as RocmRun;
378
+ setRun(nextRun);
379
+ }, []);
380
+
381
+ const generateReport = useCallback(async (completedRun: RocmRun) => {
382
+ setIsGeneratingReport(true);
383
+
384
+ try {
385
+ const response = await fetch("/api/report", {
386
+ method: "POST",
387
+ headers: { "Content-Type": "application/json" },
388
+ body: JSON.stringify(completedRun),
389
+ });
390
+
391
+ if (!response.ok) {
392
+ throw new Error("Could not generate final report");
393
+ }
394
+
395
+ const reportResponse = (await response.json()) as ReportResponse;
396
+ setReport(reportResponse);
397
+ } catch (caught) {
398
+ setError(caught instanceof Error ? caught.message : "Unknown report error");
399
+ } finally {
400
+ setIsGeneratingReport(false);
401
+ }
402
+ }, []);
403
+
404
+ useEffect(() => {
405
+ if (!run || run.status === "completed") {
406
+ return;
407
+ }
408
+
409
+ const timer = window.setInterval(() => {
410
+ void pollRun(run.id).catch((caught) =>
411
+ setError(caught instanceof Error ? caught.message : "Unknown polling error")
412
+ );
413
+ }, 900);
414
+
415
+ return () => window.clearInterval(timer);
416
+ }, [pollRun, run]);
417
+
418
+ useEffect(() => {
419
+ if (!run || run.status !== "completed" || reportRequestedFor.current === run.id) {
420
+ return;
421
+ }
422
+
423
+ reportRequestedFor.current = run.id;
424
+ void generateReport(run);
425
+ }, [generateReport, run]);
426
+
427
+ const modelStatus = report?.modelStatus ?? run?.modelStatus;
428
+ const memoryStatus = report?.memoryStatus ?? run?.longContextMemory;
429
+ const completedStages = run?.stages.filter((stage) => stage.status === "completed").length ?? 0;
430
+
431
+ return (
432
+ <main className="min-h-screen bg-background text-foreground">
433
+ <div className="mx-auto flex w-full max-w-none flex-col gap-5 px-4 py-5 sm:px-5 lg:px-6">
434
+ <section className="grid gap-5 border-b border-border pb-5 xl:grid-cols-[minmax(280px,0.55fr)_minmax(0,1fr)] xl:items-end">
435
+ <div className="space-y-3">
436
+ <div className="flex flex-wrap items-center gap-2">
437
+ <Badge variant="outline" className="border-emerald-500/40 bg-emerald-500/10 text-emerald-100">
438
+ Track 1
439
+ </Badge>
440
+ <Badge variant="outline" className="border-cyan-500/40 bg-cyan-500/10 text-cyan-100">
441
+ ROCm + vLLM
442
+ </Badge>
443
+ <Badge variant="outline" className="border-amber-500/40 bg-amber-500/10 text-amber-100">
444
+ Qwen3-Coder-Next
445
+ </Badge>
446
+ </div>
447
+ <div>
448
+ <h1 className="text-3xl font-semibold tracking-normal text-foreground sm:text-4xl">
449
+ ROCmPilot
450
+ </h1>
451
+ <p className="mt-2 max-w-3xl text-sm leading-6 text-muted-foreground">
452
+ Multi-agent ROCm migration cockpit for PyTorch and vLLM workloads.
453
+ </p>
454
+ </div>
455
+ </div>
456
+
457
+ <div className="grid w-full min-w-0 gap-3 md:grid-cols-[minmax(220px,0.85fr)_minmax(260px,1.35fr)] xl:grid-cols-[minmax(220px,0.8fr)_minmax(340px,1.45fr)_max-content]">
458
+ <div className="grid min-w-0 gap-2">
459
+ <Label htmlFor="sample">Sample workload</Label>
460
+ <Select
461
+ value={sampleId}
462
+ onValueChange={(value) => {
463
+ setSampleId(value);
464
+ setGithubUrl("");
465
+ }}
466
+ >
467
+ <SelectTrigger
468
+ id="sample"
469
+ className="h-10 w-full min-w-0 bg-card [&_[data-slot=select-value]]:min-w-0 [&_[data-slot=select-value]]:truncate"
470
+ >
471
+ <SelectValue placeholder="Select sample" />
472
+ </SelectTrigger>
473
+ <SelectContent>
474
+ {SAMPLE_REPOS.map((sample) => (
475
+ <SelectItem key={sample.id} value={sample.id}>
476
+ {sample.name}
477
+ </SelectItem>
478
+ ))}
479
+ </SelectContent>
480
+ </Select>
481
+ </div>
482
+ <div className="grid min-w-0 gap-2">
483
+ <Label htmlFor="github-url">Public GitHub URL</Label>
484
+ <Input
485
+ id="github-url"
486
+ className="h-10 bg-card font-mono text-xs sm:text-sm"
487
+ onChange={(event) => setGithubUrl(event.target.value)}
488
+ placeholder="https://github.com/org/repo"
489
+ value={githubUrl}
490
+ />
491
+ </div>
492
+ <Button
493
+ className="h-10 w-full self-end md:col-span-2 xl:col-span-1 xl:w-auto"
494
+ disabled={isStarting || (run?.status === "running")}
495
+ onClick={startRun}
496
+ >
497
+ {isStarting ? (
498
+ <Loader2 className="size-4 animate-spin" />
499
+ ) : run ? (
500
+ <RefreshCw className="size-4" />
501
+ ) : (
502
+ <Play className="size-4" />
503
+ )}
504
+ {run ? "Run Again" : githubUrl.trim() ? "Scan Repo" : "Start Sample"}
505
+ </Button>
506
+ </div>
507
+ </section>
508
+
509
+ {error && (
510
+ <Alert variant="destructive">
511
+ <TriangleAlert className="size-4" />
512
+ <AlertTitle>Demo flow needs attention</AlertTitle>
513
+ <AlertDescription>{error}</AlertDescription>
514
+ </Alert>
515
+ )}
516
+
517
+ <section className="grid gap-5 lg:grid-cols-[1.6fr_0.9fr]">
518
+ <div className="grid gap-5">
519
+ <Card>
520
+ <CardHeader className="gap-3">
521
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
522
+ <div>
523
+ <CardTitle className="flex items-center gap-2 text-xl">
524
+ <Bot className="size-5 text-cyan-200" />
525
+ Agent run
526
+ </CardTitle>
527
+ <CardDescription>
528
+ {run
529
+ ? `${completedStages}/${run.stages.length} stages complete`
530
+ : "Ready to audit the selected workload"}
531
+ </CardDescription>
532
+ </div>
533
+ <Badge
534
+ variant="outline"
535
+ className={
536
+ run?.status === "completed"
537
+ ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-100"
538
+ : run?.status === "running"
539
+ ? "border-cyan-500/40 bg-cyan-500/10 text-cyan-100"
540
+ : "border-zinc-700 bg-zinc-900 text-zinc-300"
541
+ }
542
+ >
543
+ {run?.status ?? "idle"}
544
+ </Badge>
545
+ </div>
546
+ <Progress value={run?.progress ?? 0} />
547
+ </CardHeader>
548
+ <CardContent>
549
+ <div className="grid gap-3 md:grid-cols-5">
550
+ {(run?.stages ?? []).length > 0 ? (
551
+ run?.stages.map((stage) => (
552
+ <div
553
+ key={stage.id}
554
+ className={`rounded-lg border p-3 ${statusTone[stage.status]}`}
555
+ >
556
+ <div className="flex items-center justify-between gap-2">
557
+ <StageIcon stage={stage} />
558
+ <span className="font-mono text-[11px]">{stage.progress}%</span>
559
+ </div>
560
+ <p className="mt-3 text-sm font-medium leading-5">{stage.agent}</p>
561
+ <p className="mt-1 min-h-10 text-xs leading-5 text-muted-foreground">
562
+ {stage.title}
563
+ </p>
564
+ <p className="mt-3 font-mono text-[11px] text-muted-foreground">
565
+ {formatTime(stage.completedAt ?? stage.startedAt)}
566
+ </p>
567
+ </div>
568
+ ))
569
+ ) : (
570
+ <div className="col-span-full rounded-lg border border-dashed border-border p-6 text-sm text-muted-foreground">
571
+ Select a workload and start the audit.
572
+ </div>
573
+ )}
574
+ </div>
575
+ </CardContent>
576
+ </Card>
577
+
578
+ <AgentWarRoom run={run} />
579
+
580
+ <Card>
581
+ <CardHeader>
582
+ <CardTitle className="flex items-center gap-2 text-xl">
583
+ <ShieldCheck className="size-5 text-emerald-200" />
584
+ Migration findings
585
+ </CardTitle>
586
+ <CardDescription>CUDA assumptions, ROCm blockers, and recommended fixes.</CardDescription>
587
+ </CardHeader>
588
+ <CardContent>
589
+ <div className="overflow-hidden rounded-lg border">
590
+ <Table>
591
+ <TableHeader>
592
+ <TableRow>
593
+ <TableHead>Severity</TableHead>
594
+ <TableHead>Category</TableHead>
595
+ <TableHead>Location</TableHead>
596
+ <TableHead>Fix</TableHead>
597
+ </TableRow>
598
+ </TableHeader>
599
+ <TableBody>
600
+ {run?.findings.length ? (
601
+ run.findings.map((finding) => (
602
+ <TableRow key={finding.id}>
603
+ <TableCell>
604
+ <Badge variant="outline" className={severityTone[finding.severity]}>
605
+ {finding.severity}
606
+ </Badge>
607
+ </TableCell>
608
+ <TableCell className="font-medium">{finding.category}</TableCell>
609
+ <TableCell className="font-mono text-xs text-muted-foreground">
610
+ {finding.file}:{finding.line}
611
+ </TableCell>
612
+ <TableCell className="max-w-md text-sm text-muted-foreground">
613
+ {finding.recommendedFix}
614
+ </TableCell>
615
+ </TableRow>
616
+ ))
617
+ ) : (
618
+ <TableRow>
619
+ <TableCell colSpan={4} className="h-24 text-center text-muted-foreground">
620
+ Findings appear after the Repo Doctor stage starts.
621
+ </TableCell>
622
+ </TableRow>
623
+ )}
624
+ </TableBody>
625
+ </Table>
626
+ </div>
627
+ </CardContent>
628
+ </Card>
629
+
630
+ <Card>
631
+ <CardHeader>
632
+ <CardTitle className="flex items-center gap-2 text-xl">
633
+ <FileCode2 className="size-5 text-amber-200" />
634
+ Evidence panels
635
+ </CardTitle>
636
+ <CardDescription>Patch previews, terminal logs, and final report output.</CardDescription>
637
+ </CardHeader>
638
+ <CardContent>
639
+ <div className="w-full">
640
+ <div
641
+ aria-label="Evidence panels"
642
+ className="grid w-full grid-cols-3 rounded-lg bg-muted p-1"
643
+ role="tablist"
644
+ >
645
+ {(["patches", "logs", "report"] as const).map((panel) => (
646
+ <Button
647
+ aria-selected={activePanel === panel}
648
+ className="h-8 capitalize"
649
+ key={panel}
650
+ onClick={() => setActivePanel(panel)}
651
+ role="tab"
652
+ type="button"
653
+ variant={activePanel === panel ? "secondary" : "ghost"}
654
+ >
655
+ {panel}
656
+ </Button>
657
+ ))}
658
+ </div>
659
+ {activePanel === "patches" && (
660
+ <div className="mt-4 space-y-4" role="tabpanel">
661
+ {run?.patches.length ? (
662
+ run.patches.map((patch) => (
663
+ <div key={patch.id} className="space-y-2 rounded-lg border border-border p-3">
664
+ <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
665
+ <p className="text-sm font-medium">{patch.file}</p>
666
+ <p className="max-w-xl text-xs text-muted-foreground">{patch.rationale}</p>
667
+ </div>
668
+ <CodeBlock code={patch.diff} language="diff" showLineNumbers>
669
+ <CodeBlockHeader>
670
+ <CodeBlockTitle>
671
+ <CodeBlockFilename>{patch.file}</CodeBlockFilename>
672
+ </CodeBlockTitle>
673
+ <CodeBlockCopyButton />
674
+ </CodeBlockHeader>
675
+ </CodeBlock>
676
+ </div>
677
+ ))
678
+ ) : (
679
+ <div className="rounded-lg border border-dashed border-border p-6 text-sm text-muted-foreground">
680
+ Patch previews unlock after the Migration Planner stage.
681
+ </div>
682
+ )}
683
+ </div>
684
+ )}
685
+ {activePanel === "logs" && (
686
+ <div className="mt-4" role="tabpanel">
687
+ <Terminal
688
+ output={logOutput || "waiting for run output..."}
689
+ isStreaming={run?.status === "running"}
690
+ />
691
+ </div>
692
+ )}
693
+ {activePanel === "report" && (
694
+ <div className="mt-4" role="tabpanel">
695
+ <div className="min-h-80 rounded-lg border bg-card p-4">
696
+ {isGeneratingReport ? (
697
+ <div className="flex h-64 items-center justify-center gap-3 text-sm text-muted-foreground">
698
+ <Loader2 className="size-4 animate-spin" />
699
+ Report Agent is calling AMD-hosted Qwen or fallback output.
700
+ </div>
701
+ ) : report?.report ? (
702
+ <MessageResponse>{report.report}</MessageResponse>
703
+ ) : (
704
+ <div className="flex h-64 items-center justify-center text-sm text-muted-foreground">
705
+ The final report appears after all agent stages complete.
706
+ </div>
707
+ )}
708
+ </div>
709
+ </div>
710
+ )}
711
+ </div>
712
+ </CardContent>
713
+ </Card>
714
+ </div>
715
+
716
+ <aside className="grid content-start gap-5">
717
+ <Card>
718
+ <CardHeader>
719
+ <CardTitle className="flex items-center gap-2 text-lg">
720
+ <GitBranch className="size-5 text-zinc-300" />
721
+ Workload
722
+ </CardTitle>
723
+ <CardDescription>
724
+ {run?.target.type === "github" ? run.target.note : selectedSample.stack}
725
+ </CardDescription>
726
+ </CardHeader>
727
+ <CardContent className="space-y-4">
728
+ <div className="grid gap-2">
729
+ <Label htmlFor="repo-url">Repository URL</Label>
730
+ <Input id="repo-url" value={activeRepoUrl} readOnly className="font-mono text-xs" />
731
+ </div>
732
+ <Separator />
733
+ <div className="space-y-3 text-sm">
734
+ <div>
735
+ <p className="text-muted-foreground">Scan mode</p>
736
+ <p className="font-medium">
737
+ {run?.target.type === "github" ? "Live public GitHub scan" : "Curated sample fixture"}
738
+ </p>
739
+ </div>
740
+ <div>
741
+ <p className="text-muted-foreground">Target</p>
742
+ <p className="font-medium">{run?.target.label ?? selectedSample.name}</p>
743
+ </div>
744
+ <div>
745
+ <p className="text-muted-foreground">Files scanned</p>
746
+ <p className="font-mono text-lg">{run?.target.scannedFiles ?? 0}</p>
747
+ </div>
748
+ <div>
749
+ <p className="text-muted-foreground">Risk</p>
750
+ <p className="leading-6">{run?.target.note ?? selectedSample.risk}</p>
751
+ </div>
752
+ </div>
753
+ </CardContent>
754
+ </Card>
755
+
756
+ <Card>
757
+ <CardHeader>
758
+ <CardTitle className="flex items-center gap-2 text-lg">
759
+ <Cpu className="size-5 text-emerald-200" />
760
+ GPU model status
761
+ </CardTitle>
762
+ <CardDescription>Qwen endpoint used by the Report Agent.</CardDescription>
763
+ </CardHeader>
764
+ <CardContent className="space-y-4">
765
+ <Badge
766
+ variant="outline"
767
+ className={
768
+ modelStatus?.status === "connected"
769
+ ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-100"
770
+ : modelStatus?.status === "not-configured"
771
+ ? "border-cyan-500/40 bg-cyan-500/10 text-cyan-100"
772
+ : "border-amber-500/40 bg-amber-500/10 text-amber-100"
773
+ }
774
+ >
775
+ {modelStatus?.label ?? "AMD GPU Model: Demo fallback"}
776
+ </Badge>
777
+ <div className="space-y-2 text-sm">
778
+ <div className="flex items-start gap-2">
779
+ <Sparkles className="mt-0.5 size-4 text-amber-200" />
780
+ <span>{modelStatus?.model ?? "Qwen/Qwen3-Coder-Next"}</span>
781
+ </div>
782
+ <div className="flex items-start gap-2">
783
+ <Zap className="mt-0.5 size-4 text-cyan-200" />
784
+ <span className="break-all text-muted-foreground">
785
+ {modelStatus?.endpoint ?? "Set AMD_QWEN_BASE_URL"}
786
+ </span>
787
+ </div>
788
+ <p className="leading-6 text-muted-foreground">
789
+ {modelStatus?.detail ??
790
+ "The MVP stays demo-safe until an AMD ROCm/vLLM endpoint is available."}
791
+ </p>
792
+ </div>
793
+ </CardContent>
794
+ </Card>
795
+
796
+ <Card>
797
+ <CardHeader>
798
+ <CardTitle className="flex items-center gap-2 text-lg">
799
+ <BrainCircuit className="size-5 text-rose-200" />
800
+ Long-context memory
801
+ </CardTitle>
802
+ <CardDescription>Synap context used by the Report Agent.</CardDescription>
803
+ </CardHeader>
804
+ <CardContent className="space-y-4">
805
+ <Badge
806
+ variant="outline"
807
+ className={memoryStatusTone[memoryStatus?.status ?? "fallback"]}
808
+ >
809
+ {memoryStatus?.label ?? "Synap Memory: Local fallback"}
810
+ </Badge>
811
+ <div className="grid gap-3 text-sm">
812
+ <div>
813
+ <p className="text-muted-foreground">Provider</p>
814
+ <p className="font-medium">{memoryStatus?.provider ?? "local"}</p>
815
+ </div>
816
+ <div>
817
+ <p className="text-muted-foreground">Conversation</p>
818
+ <p className="break-all font-mono text-xs">
819
+ {memoryStatus?.conversationId ?? "created after run starts"}
820
+ </p>
821
+ </div>
822
+ <div className="grid grid-cols-2 gap-3">
823
+ <div>
824
+ <p className="font-mono text-lg">{memoryStatus?.storedItems ?? 0}</p>
825
+ <p className="text-xs text-muted-foreground">stored</p>
826
+ </div>
827
+ <div>
828
+ <p className="font-mono text-lg">{memoryStatus?.recalledItems ?? 0}</p>
829
+ <p className="text-xs text-muted-foreground">recalled</p>
830
+ </div>
831
+ </div>
832
+ <p className="leading-6 text-muted-foreground">
833
+ {memoryStatus?.detail ??
834
+ "Set SYNAP_API_KEY to persist agent memory across sessions."}
835
+ </p>
836
+ </div>
837
+ </CardContent>
838
+ </Card>
839
+
840
+ <Card>
841
+ <CardHeader>
842
+ <CardTitle className="flex items-center gap-2 text-lg">
843
+ <Gauge className="size-5 text-cyan-200" />
844
+ Benchmark profile
845
+ </CardTitle>
846
+ <CardDescription>Demo metrics for the submission walkthrough.</CardDescription>
847
+ </CardHeader>
848
+ <CardContent className="space-y-3">
849
+ {(run?.benchmarks ?? []).map((benchmark) => (
850
+ <div key={benchmark.label} className="rounded-lg border border-border p-3">
851
+ <div className="flex items-start justify-between gap-3">
852
+ <p className="font-medium">{benchmark.label}</p>
853
+ <Badge variant="outline">{benchmark.backend}</Badge>
854
+ </div>
855
+ <div className="mt-3 grid grid-cols-3 gap-2 text-sm">
856
+ <div>
857
+ <p className="font-mono text-lg">{benchmark.tokensPerSecond}</p>
858
+ <p className="text-xs text-muted-foreground">tok/s</p>
859
+ </div>
860
+ <div>
861
+ <p className="font-mono text-lg">{benchmark.p95LatencyMs}</p>
862
+ <p className="text-xs text-muted-foreground">p95 ms</p>
863
+ </div>
864
+ <div>
865
+ <p className="font-mono text-lg">{benchmark.memoryGb}</p>
866
+ <p className="text-xs text-muted-foreground">GB</p>
867
+ </div>
868
+ </div>
869
+ <p className="mt-3 text-xs leading-5 text-muted-foreground">{benchmark.costNote}</p>
870
+ </div>
871
+ ))}
872
+ {!run && (
873
+ <div className="rounded-lg border border-dashed border-border p-6 text-sm text-muted-foreground">
874
+ Benchmark cards appear during the run.
875
+ </div>
876
+ )}
877
+ </CardContent>
878
+ </Card>
879
+
880
+ <Card>
881
+ <CardHeader>
882
+ <CardTitle className="flex items-center gap-2 text-lg">
883
+ <Boxes className="size-5 text-amber-200" />
884
+ Submission proof
885
+ </CardTitle>
886
+ </CardHeader>
887
+ <CardContent className="space-y-3 text-sm text-muted-foreground">
888
+ <div className="flex items-start gap-2">
889
+ <BadgeCheck className="mt-0.5 size-4 text-emerald-200" />
890
+ <span>Track 1 agentic workflow with five specialized agents.</span>
891
+ </div>
892
+ <div className="flex items-start gap-2">
893
+ <BadgeCheck className="mt-0.5 size-4 text-emerald-200" />
894
+ <span>AMD GPU story through ROCm/vLLM Qwen model serving.</span>
895
+ </div>
896
+ <div className="flex items-start gap-2">
897
+ <TerminalSquare className="mt-0.5 size-4 text-cyan-200" />
898
+ <span>Demo remains reliable without credentials, then upgrades with live endpoint logs.</span>
899
+ </div>
900
+ </CardContent>
901
+ </Card>
902
+ </aside>
903
+ </section>
904
+ </div>
905
+ </main>
906
+ );
907
+ }
src/components/ui/alert.tsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
67
+ return (
68
+ <div
69
+ data-slot="alert-action"
70
+ className={cn("absolute top-2 right-2", className)}
71
+ {...props}
72
+ />
73
+ )
74
+ }
75
+
76
+ export { Alert, AlertTitle, AlertDescription, AlertAction }
src/components/ui/badge.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Slot } from "radix-ui"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13
+ secondary:
14
+ "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
15
+ destructive:
16
+ "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
17
+ outline:
18
+ "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
19
+ ghost:
20
+ "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ variant: "default",
26
+ },
27
+ }
28
+ )
29
+
30
+ function Badge({
31
+ className,
32
+ variant = "default",
33
+ asChild = false,
34
+ ...props
35
+ }: React.ComponentProps<"span"> &
36
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
37
+ const Comp = asChild ? Slot.Root : "span"
38
+
39
+ return (
40
+ <Comp
41
+ data-slot="badge"
42
+ data-variant={variant}
43
+ className={cn(badgeVariants({ variant }), className)}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ export { Badge, badgeVariants }
src/components/ui/button-group.tsx ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cva, type VariantProps } from "class-variance-authority"
2
+ import { Slot } from "radix-ui"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { Separator } from "@/components/ui/separator"
6
+
7
+ const buttonGroupVariants = cva(
8
+ "group/button-group flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
9
+ {
10
+ variants: {
11
+ orientation: {
12
+ horizontal:
13
+ "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg!",
14
+ vertical:
15
+ "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg!",
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ orientation: "horizontal",
20
+ },
21
+ }
22
+ )
23
+
24
+ function ButtonGroup({
25
+ className,
26
+ orientation,
27
+ ...props
28
+ }: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
29
+ return (
30
+ <div
31
+ role="group"
32
+ data-slot="button-group"
33
+ data-orientation={orientation}
34
+ className={cn(buttonGroupVariants({ orientation }), className)}
35
+ {...props}
36
+ />
37
+ )
38
+ }
39
+
40
+ function ButtonGroupText({
41
+ className,
42
+ asChild = false,
43
+ ...props
44
+ }: React.ComponentProps<"div"> & {
45
+ asChild?: boolean
46
+ }) {
47
+ const Comp = asChild ? Slot.Root : "div"
48
+
49
+ return (
50
+ <Comp
51
+ className={cn(
52
+ "flex items-center gap-2 rounded-lg border bg-muted px-2.5 text-sm font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
53
+ className
54
+ )}
55
+ {...props}
56
+ />
57
+ )
58
+ }
59
+
60
+ function ButtonGroupSeparator({
61
+ className,
62
+ orientation = "vertical",
63
+ ...props
64
+ }: React.ComponentProps<typeof Separator>) {
65
+ return (
66
+ <Separator
67
+ data-slot="button-group-separator"
68
+ orientation={orientation}
69
+ className={cn(
70
+ "relative self-stretch bg-input data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto",
71
+ className
72
+ )}
73
+ {...props}
74
+ />
75
+ )
76
+ }
77
+
78
+ export {
79
+ ButtonGroup,
80
+ ButtonGroupSeparator,
81
+ ButtonGroupText,
82
+ buttonGroupVariants,
83
+ }
src/components/ui/button.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Slot } from "radix-ui"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13
+ outline:
14
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
15
+ secondary:
16
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
17
+ ghost:
18
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
19
+ destructive:
20
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default:
25
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
26
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
27
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
28
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
29
+ icon: "size-8",
30
+ "icon-xs":
31
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
32
+ "icon-sm":
33
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
34
+ "icon-lg": "size-9",
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: "default",
39
+ size: "default",
40
+ },
41
+ }
42
+ )
43
+
44
+ function Button({
45
+ className,
46
+ variant = "default",
47
+ size = "default",
48
+ asChild = false,
49
+ ...props
50
+ }: React.ComponentProps<"button"> &
51
+ VariantProps<typeof buttonVariants> & {
52
+ asChild?: boolean
53
+ }) {
54
+ const Comp = asChild ? Slot.Root : "button"
55
+
56
+ return (
57
+ <Comp
58
+ data-slot="button"
59
+ data-variant={variant}
60
+ data-size={size}
61
+ className={cn(buttonVariants({ variant, size, className }))}
62
+ {...props}
63
+ />
64
+ )
65
+ }
66
+
67
+ export { Button, buttonVariants }
src/components/ui/card.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({
6
+ className,
7
+ size = "default",
8
+ ...props
9
+ }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
10
+ return (
11
+ <div
12
+ data-slot="card"
13
+ data-size={size}
14
+ className={cn(
15
+ "group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
24
+ return (
25
+ <div
26
+ data-slot="card-header"
27
+ className={cn(
28
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
29
+ className
30
+ )}
31
+ {...props}
32
+ />
33
+ )
34
+ }
35
+
36
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
37
+ return (
38
+ <div
39
+ data-slot="card-title"
40
+ className={cn(
41
+ "font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
50
+ return (
51
+ <div
52
+ data-slot="card-description"
53
+ className={cn("text-sm text-muted-foreground", className)}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
60
+ return (
61
+ <div
62
+ data-slot="card-action"
63
+ className={cn(
64
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
65
+ className
66
+ )}
67
+ {...props}
68
+ />
69
+ )
70
+ }
71
+
72
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
73
+ return (
74
+ <div
75
+ data-slot="card-content"
76
+ className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
77
+ {...props}
78
+ />
79
+ )
80
+ }
81
+
82
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
83
+ return (
84
+ <div
85
+ data-slot="card-footer"
86
+ className={cn(
87
+ "flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ export {
96
+ Card,
97
+ CardHeader,
98
+ CardFooter,
99
+ CardTitle,
100
+ CardAction,
101
+ CardDescription,
102
+ CardContent,
103
+ }
src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { Collapsible as CollapsiblePrimitive } from "radix-ui"
4
+
5
+ function Collapsible({
6
+ ...props
7
+ }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
8
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
9
+ }
10
+
11
+ function CollapsibleTrigger({
12
+ ...props
13
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
14
+ return (
15
+ <CollapsiblePrimitive.CollapsibleTrigger
16
+ data-slot="collapsible-trigger"
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function CollapsibleContent({
23
+ ...props
24
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
25
+ return (
26
+ <CollapsiblePrimitive.CollapsibleContent
27
+ data-slot="collapsible-content"
28
+ {...props}
29
+ />
30
+ )
31
+ }
32
+
33
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
src/components/ui/input.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
12
+ className
13
+ )}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ export { Input }
src/components/ui/label.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Label as LabelPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Label({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
12
+ return (
13
+ <LabelPrimitive.Root
14
+ data-slot="label"
15
+ className={cn(
16
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ export { Label }
src/components/ui/progress.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Progress as ProgressPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Progress({
9
+ className,
10
+ value,
11
+ ...props
12
+ }: React.ComponentProps<typeof ProgressPrimitive.Root>) {
13
+ return (
14
+ <ProgressPrimitive.Root
15
+ data-slot="progress"
16
+ className={cn(
17
+ "relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-muted",
18
+ className
19
+ )}
20
+ {...props}
21
+ >
22
+ <ProgressPrimitive.Indicator
23
+ data-slot="progress-indicator"
24
+ className="size-full flex-1 bg-primary transition-all"
25
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
26
+ />
27
+ </ProgressPrimitive.Root>
28
+ )
29
+ }
30
+
31
+ export { Progress }
src/components/ui/scroll-area.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function ScrollArea({
9
+ className,
10
+ children,
11
+ ...props
12
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
13
+ return (
14
+ <ScrollAreaPrimitive.Root
15
+ data-slot="scroll-area"
16
+ className={cn("relative", className)}
17
+ {...props}
18
+ >
19
+ <ScrollAreaPrimitive.Viewport
20
+ data-slot="scroll-area-viewport"
21
+ className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
22
+ >
23
+ {children}
24
+ </ScrollAreaPrimitive.Viewport>
25
+ <ScrollBar />
26
+ <ScrollAreaPrimitive.Corner />
27
+ </ScrollAreaPrimitive.Root>
28
+ )
29
+ }
30
+
31
+ function ScrollBar({
32
+ className,
33
+ orientation = "vertical",
34
+ ...props
35
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
36
+ return (
37
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
38
+ data-slot="scroll-area-scrollbar"
39
+ data-orientation={orientation}
40
+ orientation={orientation}
41
+ className={cn(
42
+ "flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ <ScrollAreaPrimitive.ScrollAreaThumb
48
+ data-slot="scroll-area-thumb"
49
+ className="relative flex-1 rounded-full bg-border"
50
+ />
51
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
52
+ )
53
+ }
54
+
55
+ export { ScrollArea, ScrollBar }
src/components/ui/select.tsx ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Select as SelectPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
8
+
9
+ function Select({
10
+ ...props
11
+ }: React.ComponentProps<typeof SelectPrimitive.Root>) {
12
+ return <SelectPrimitive.Root data-slot="select" {...props} />
13
+ }
14
+
15
+ function SelectGroup({
16
+ className,
17
+ ...props
18
+ }: React.ComponentProps<typeof SelectPrimitive.Group>) {
19
+ return (
20
+ <SelectPrimitive.Group
21
+ data-slot="select-group"
22
+ className={cn("scroll-my-1 p-1", className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function SelectValue({
29
+ ...props
30
+ }: React.ComponentProps<typeof SelectPrimitive.Value>) {
31
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />
32
+ }
33
+
34
+ function SelectTrigger({
35
+ className,
36
+ size = "default",
37
+ children,
38
+ ...props
39
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
40
+ size?: "sm" | "default"
41
+ }) {
42
+ return (
43
+ <SelectPrimitive.Trigger
44
+ data-slot="select-trigger"
45
+ data-size={size}
46
+ className={cn(
47
+ "flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
48
+ className
49
+ )}
50
+ {...props}
51
+ >
52
+ {children}
53
+ <SelectPrimitive.Icon asChild>
54
+ <ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
55
+ </SelectPrimitive.Icon>
56
+ </SelectPrimitive.Trigger>
57
+ )
58
+ }
59
+
60
+ function SelectContent({
61
+ className,
62
+ children,
63
+ position = "item-aligned",
64
+ align = "center",
65
+ ...props
66
+ }: React.ComponentProps<typeof SelectPrimitive.Content>) {
67
+ return (
68
+ <SelectPrimitive.Portal>
69
+ <SelectPrimitive.Content
70
+ data-slot="select-content"
71
+ data-align-trigger={position === "item-aligned"}
72
+ className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
73
+ position={position}
74
+ align={align}
75
+ {...props}
76
+ >
77
+ <SelectScrollUpButton />
78
+ <SelectPrimitive.Viewport
79
+ data-position={position}
80
+ className={cn(
81
+ "data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
82
+ position === "popper" && ""
83
+ )}
84
+ >
85
+ {children}
86
+ </SelectPrimitive.Viewport>
87
+ <SelectScrollDownButton />
88
+ </SelectPrimitive.Content>
89
+ </SelectPrimitive.Portal>
90
+ )
91
+ }
92
+
93
+ function SelectLabel({
94
+ className,
95
+ ...props
96
+ }: React.ComponentProps<typeof SelectPrimitive.Label>) {
97
+ return (
98
+ <SelectPrimitive.Label
99
+ data-slot="select-label"
100
+ className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
101
+ {...props}
102
+ />
103
+ )
104
+ }
105
+
106
+ function SelectItem({
107
+ className,
108
+ children,
109
+ ...props
110
+ }: React.ComponentProps<typeof SelectPrimitive.Item>) {
111
+ return (
112
+ <SelectPrimitive.Item
113
+ data-slot="select-item"
114
+ className={cn(
115
+ "relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
116
+ className
117
+ )}
118
+ {...props}
119
+ >
120
+ <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
121
+ <SelectPrimitive.ItemIndicator>
122
+ <CheckIcon className="pointer-events-none" />
123
+ </SelectPrimitive.ItemIndicator>
124
+ </span>
125
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
126
+ </SelectPrimitive.Item>
127
+ )
128
+ }
129
+
130
+ function SelectSeparator({
131
+ className,
132
+ ...props
133
+ }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
134
+ return (
135
+ <SelectPrimitive.Separator
136
+ data-slot="select-separator"
137
+ className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
138
+ {...props}
139
+ />
140
+ )
141
+ }
142
+
143
+ function SelectScrollUpButton({
144
+ className,
145
+ ...props
146
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
147
+ return (
148
+ <SelectPrimitive.ScrollUpButton
149
+ data-slot="select-scroll-up-button"
150
+ className={cn(
151
+ "z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
152
+ className
153
+ )}
154
+ {...props}
155
+ >
156
+ <ChevronUpIcon
157
+ />
158
+ </SelectPrimitive.ScrollUpButton>
159
+ )
160
+ }
161
+
162
+ function SelectScrollDownButton({
163
+ className,
164
+ ...props
165
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
166
+ return (
167
+ <SelectPrimitive.ScrollDownButton
168
+ data-slot="select-scroll-down-button"
169
+ className={cn(
170
+ "z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
171
+ className
172
+ )}
173
+ {...props}
174
+ >
175
+ <ChevronDownIcon
176
+ />
177
+ </SelectPrimitive.ScrollDownButton>
178
+ )
179
+ }
180
+
181
+ export {
182
+ Select,
183
+ SelectContent,
184
+ SelectGroup,
185
+ SelectItem,
186
+ SelectLabel,
187
+ SelectScrollDownButton,
188
+ SelectScrollUpButton,
189
+ SelectSeparator,
190
+ SelectTrigger,
191
+ SelectValue,
192
+ }
src/components/ui/separator.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Separator as SeparatorPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Separator({
9
+ className,
10
+ orientation = "horizontal",
11
+ decorative = true,
12
+ ...props
13
+ }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
14
+ return (
15
+ <SeparatorPrimitive.Root
16
+ data-slot="separator"
17
+ decorative={decorative}
18
+ orientation={orientation}
19
+ className={cn(
20
+ "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ export { Separator }
src/components/ui/skeleton.tsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from "@/lib/utils"
2
+
3
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4
+ return (
5
+ <div
6
+ data-slot="skeleton"
7
+ className={cn("animate-pulse rounded-md bg-muted", className)}
8
+ {...props}
9
+ />
10
+ )
11
+ }
12
+
13
+ export { Skeleton }
src/components/ui/table.tsx ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function Table({ className, ...props }: React.ComponentProps<"table">) {
8
+ return (
9
+ <div
10
+ data-slot="table-container"
11
+ className="relative w-full overflow-x-auto"
12
+ >
13
+ <table
14
+ data-slot="table"
15
+ className={cn("w-full caption-bottom text-sm", className)}
16
+ {...props}
17
+ />
18
+ </div>
19
+ )
20
+ }
21
+
22
+ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
23
+ return (
24
+ <thead
25
+ data-slot="table-header"
26
+ className={cn("[&_tr]:border-b", className)}
27
+ {...props}
28
+ />
29
+ )
30
+ }
31
+
32
+ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
33
+ return (
34
+ <tbody
35
+ data-slot="table-body"
36
+ className={cn("[&_tr:last-child]:border-0", className)}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+
42
+ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
43
+ return (
44
+ <tfoot
45
+ data-slot="table-footer"
46
+ className={cn(
47
+ "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+
55
+ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
56
+ return (
57
+ <tr
58
+ data-slot="table-row"
59
+ className={cn(
60
+ "border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
61
+ className
62
+ )}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
69
+ return (
70
+ <th
71
+ data-slot="table-head"
72
+ className={cn(
73
+ "h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
74
+ className
75
+ )}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
82
+ return (
83
+ <td
84
+ data-slot="table-cell"
85
+ className={cn(
86
+ "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
87
+ className
88
+ )}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ function TableCaption({
95
+ className,
96
+ ...props
97
+ }: React.ComponentProps<"caption">) {
98
+ return (
99
+ <caption
100
+ data-slot="table-caption"
101
+ className={cn("mt-4 text-sm text-muted-foreground", className)}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ export {
108
+ Table,
109
+ TableHeader,
110
+ TableBody,
111
+ TableFooter,
112
+ TableHead,
113
+ TableRow,
114
+ TableCell,
115
+ TableCaption,
116
+ }
src/components/ui/tabs.tsx ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+ import { Tabs as TabsPrimitive } from "radix-ui"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Tabs({
10
+ className,
11
+ orientation = "horizontal",
12
+ ...props
13
+ }: React.ComponentProps<typeof TabsPrimitive.Root>) {
14
+ return (
15
+ <TabsPrimitive.Root
16
+ data-slot="tabs"
17
+ data-orientation={orientation}
18
+ className={cn(
19
+ "group/tabs flex gap-2 data-horizontal:flex-col",
20
+ className
21
+ )}
22
+ {...props}
23
+ />
24
+ )
25
+ }
26
+
27
+ const tabsListVariants = cva(
28
+ "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
29
+ {
30
+ variants: {
31
+ variant: {
32
+ default: "bg-muted",
33
+ line: "gap-1 bg-transparent",
34
+ },
35
+ },
36
+ defaultVariants: {
37
+ variant: "default",
38
+ },
39
+ }
40
+ )
41
+
42
+ function TabsList({
43
+ className,
44
+ variant = "default",
45
+ ...props
46
+ }: React.ComponentProps<typeof TabsPrimitive.List> &
47
+ VariantProps<typeof tabsListVariants>) {
48
+ return (
49
+ <TabsPrimitive.List
50
+ data-slot="tabs-list"
51
+ data-variant={variant}
52
+ className={cn(tabsListVariants({ variant }), className)}
53
+ {...props}
54
+ />
55
+ )
56
+ }
57
+
58
+ function TabsTrigger({
59
+ className,
60
+ ...props
61
+ }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
62
+ return (
63
+ <TabsPrimitive.Trigger
64
+ data-slot="tabs-trigger"
65
+ className={cn(
66
+ "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
67
+ "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
68
+ "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
69
+ "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
70
+ className
71
+ )}
72
+ {...props}
73
+ />
74
+ )
75
+ }
76
+
77
+ function TabsContent({
78
+ className,
79
+ ...props
80
+ }: React.ComponentProps<typeof TabsPrimitive.Content>) {
81
+ return (
82
+ <TabsPrimitive.Content
83
+ data-slot="tabs-content"
84
+ className={cn("flex-1 text-sm outline-none", className)}
85
+ {...props}
86
+ />
87
+ )
88
+ }
89
+
90
+ export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
src/components/ui/textarea.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6
+ return (
7
+ <textarea
8
+ data-slot="textarea"
9
+ className={cn(
10
+ "flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export { Textarea }
src/components/ui/tooltip.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Tooltip as TooltipPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function TooltipProvider({
9
+ delayDuration = 0,
10
+ ...props
11
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
12
+ return (
13
+ <TooltipPrimitive.Provider
14
+ data-slot="tooltip-provider"
15
+ delayDuration={delayDuration}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ function Tooltip({
22
+ ...props
23
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
24
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
25
+ }
26
+
27
+ function TooltipTrigger({
28
+ ...props
29
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
30
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
31
+ }
32
+
33
+ function TooltipContent({
34
+ className,
35
+ sideOffset = 0,
36
+ children,
37
+ ...props
38
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
39
+ return (
40
+ <TooltipPrimitive.Portal>
41
+ <TooltipPrimitive.Content
42
+ data-slot="tooltip-content"
43
+ sideOffset={sideOffset}
44
+ className={cn(
45
+ "z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
46
+ className
47
+ )}
48
+ {...props}
49
+ >
50
+ {children}
51
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
52
+ </TooltipPrimitive.Content>
53
+ </TooltipPrimitive.Portal>
54
+ )
55
+ }
56
+
57
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
src/lib/rocmpilot/data.ts ADDED
@@ -0,0 +1,1102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type {
2
+ AgentMessage,
3
+ AgentMessageKind,
4
+ AgentMemory,
5
+ BenchmarkResult,
6
+ Finding,
7
+ GpuModelStatus,
8
+ LongContextMemoryStatus,
9
+ PatchPreview,
10
+ RocmRun,
11
+ RunMode,
12
+ RunStatus,
13
+ RunStage,
14
+ StageStatus,
15
+ SampleRepo,
16
+ RunTarget,
17
+ } from "./types";
18
+ import { isRealGitHubRepoUrl, parseGitHubRepoUrl } from "./github-url";
19
+ import type { RepoAnalysis } from "./github-scanner";
20
+ import {
21
+ buildMemoryConversationId,
22
+ DEFAULT_MEMORY_CUSTOMER_ID,
23
+ DEFAULT_MEMORY_USER_ID,
24
+ } from "./memory-ids";
25
+
26
+ const TOTAL_DURATION_MS = 30_000;
27
+
28
+ const STAGES: Array<Omit<RunStage, "status" | "progress" | "startedAt" | "completedAt"> & {
29
+ durationMs: number;
30
+ }> = [
31
+ {
32
+ id: "repo-doctor",
33
+ agent: "Repo Doctor Agent",
34
+ title: "Repo compatibility scan",
35
+ description: "Dependency graph, Docker image, device paths, and runtime flags",
36
+ durationMs: 5_000,
37
+ },
38
+ {
39
+ id: "migration-planner",
40
+ agent: "Migration Planner Agent",
41
+ title: "ROCm migration plan",
42
+ description: "PyTorch ROCm wheels, vLLM runtime, and device abstraction changes",
43
+ durationMs: 6_000,
44
+ },
45
+ {
46
+ id: "build-runner",
47
+ agent: "Build Runner Agent",
48
+ title: "Build and smoke tests",
49
+ description: "Container validation, import checks, and inference dry run",
50
+ durationMs: 5_500,
51
+ },
52
+ {
53
+ id: "benchmark-agent",
54
+ agent: "Benchmark Agent",
55
+ title: "MI300X readiness benchmark",
56
+ description: "Throughput, latency, memory, and fallback path comparison",
57
+ durationMs: 5_000,
58
+ },
59
+ {
60
+ id: "report-agent",
61
+ agent: "Report Agent",
62
+ title: "Judge-ready report",
63
+ description: "Technical summary, business value, and AMD proof points",
64
+ durationMs: 6_000,
65
+ },
66
+ ];
67
+
68
+ export const SAMPLE_REPOS: SampleRepo[] = [
69
+ {
70
+ id: "qwen-vllm-cuda",
71
+ name: "Qwen vLLM CUDA Starter",
72
+ repoUrl: "https://github.com/example/qwen-vllm-cuda-starter",
73
+ stack: "FastAPI, PyTorch, vLLM, Docker",
74
+ model: "Qwen/Qwen2.5-Coder-7B-Instruct",
75
+ description: "A common NVIDIA-first inference service with CUDA-only install steps.",
76
+ risk: "Hardcoded CUDA device checks block AMD Developer Cloud deployment.",
77
+ },
78
+ {
79
+ id: "torch-agent-worker",
80
+ name: "Torch Agent Worker",
81
+ repoUrl: "https://github.com/example/torch-agent-worker",
82
+ stack: "Python workers, PyTorch, Redis queue",
83
+ model: "Qwen/Qwen3-Coder-Next",
84
+ description: "Background coding-agent worker that assumes NVIDIA runtime images.",
85
+ risk: "Docker and benchmark scripts hide GPU vendor assumptions.",
86
+ },
87
+ ];
88
+
89
+ export const FINDINGS: Finding[] = [
90
+ {
91
+ id: "cuda-device",
92
+ severity: "critical",
93
+ category: "Runtime device lock",
94
+ file: "src/inference/server.py",
95
+ line: 42,
96
+ explanation:
97
+ "`torch.device('cuda')` is used directly, so the app never checks ROCm-compatible PyTorch device availability or CPU fallback.",
98
+ recommendedFix:
99
+ "Introduce a device resolver that accepts HIP-backed PyTorch as CUDA-compatible and records the detected backend.",
100
+ },
101
+ {
102
+ id: "docker-image",
103
+ severity: "high",
104
+ category: "Container image",
105
+ file: "Dockerfile",
106
+ line: 1,
107
+ explanation:
108
+ "The base image is `nvidia/cuda`, which prevents a clean ROCm/vLLM deployment on AMD Developer Cloud.",
109
+ recommendedFix:
110
+ "Use the ROCm vLLM image for AMD runs and keep CUDA images only as an optional backend.",
111
+ },
112
+ {
113
+ id: "vllm-flags",
114
+ severity: "high",
115
+ category: "Serving configuration",
116
+ file: "scripts/serve.sh",
117
+ line: 9,
118
+ explanation:
119
+ "The vLLM launch script omits ROCm-oriented environment flags and does not expose tensor-parallel settings.",
120
+ recommendedFix:
121
+ "Add backend-aware vLLM launch arguments and document MI300X model-serving defaults.",
122
+ },
123
+ {
124
+ id: "metrics",
125
+ severity: "medium",
126
+ category: "Benchmark visibility",
127
+ file: "benchmarks/run_latency.py",
128
+ line: 18,
129
+ explanation:
130
+ "The benchmark reports request latency only and misses GPU memory, tokens/sec, and backend provenance.",
131
+ recommendedFix:
132
+ "Add AMD SMI/vLLM metrics capture so submission evidence includes GPU model, memory, and throughput.",
133
+ },
134
+ ];
135
+
136
+ export const PATCHES: PatchPreview[] = [
137
+ {
138
+ id: "device-resolver",
139
+ file: "src/inference/device.py",
140
+ rationale:
141
+ "Centralizes device selection so ROCm-backed PyTorch can run without scattering vendor checks across the service.",
142
+ diff: `+import torch
143
+ +
144
+ +def resolve_device() -> tuple[str, str]:
145
+ + if torch.cuda.is_available():
146
+ + backend = "rocm" if getattr(torch.version, "hip", None) else "cuda"
147
+ + return "cuda", backend
148
+ + return "cpu", "cpu"
149
+ +
150
+ +DEVICE, GPU_BACKEND = resolve_device()
151
+ `,
152
+ },
153
+ {
154
+ id: "rocm-docker",
155
+ file: "Dockerfile.rocm",
156
+ rationale:
157
+ "Adds an AMD-specific runtime image while preserving the original CUDA path for teams that need dual-vendor support.",
158
+ diff: `+FROM rocm/vllm:latest
159
+ +
160
+ +WORKDIR /workspace
161
+ +COPY requirements-rocm.txt .
162
+ +RUN pip install --no-cache-dir -r requirements-rocm.txt
163
+ +COPY . .
164
+ +
165
+ +ENV HIP_VISIBLE_DEVICES=0
166
+ +ENV VLLM_USE_ROCM=1
167
+ +CMD ["bash", "scripts/serve-rocm.sh"]
168
+ `,
169
+ },
170
+ {
171
+ id: "serve-rocm",
172
+ file: "scripts/serve-rocm.sh",
173
+ rationale:
174
+ "Launches an OpenAI-compatible vLLM endpoint for the migration/report agents on AMD Instinct GPUs.",
175
+ diff: `+#!/usr/bin/env bash
176
+ +set -euo pipefail
177
+ +
178
+ +MODEL="\${MODEL:-Qwen/Qwen3-Coder-Next}"
179
+ +PORT="\${PORT:-8000}"
180
+ +
181
+ +python -m vllm.entrypoints.openai.api_server \\
182
+ + --model "$MODEL" \\
183
+ + --host 0.0.0.0 \\
184
+ + --port "$PORT" \\
185
+ + --tensor-parallel-size "\${TENSOR_PARALLEL_SIZE:-1}" \\
186
+ + --max-model-len "\${MAX_MODEL_LEN:-32768}"
187
+ `,
188
+ },
189
+ ];
190
+
191
+ export const BENCHMARKS: BenchmarkResult[] = [
192
+ {
193
+ label: "Before migration",
194
+ backend: "CUDA-only config",
195
+ tokensPerSecond: 0,
196
+ p95LatencyMs: 0,
197
+ memoryGb: 0,
198
+ costNote: "Does not boot on AMD ROCm image.",
199
+ },
200
+ {
201
+ label: "ROCm-ready target",
202
+ backend: "ROCm + vLLM on MI300X",
203
+ tokensPerSecond: 182,
204
+ p95LatencyMs: 730,
205
+ memoryGb: 92,
206
+ costNote: "Estimated from demo profile; replace with live AMD run evidence.",
207
+ },
208
+ {
209
+ label: "Agent report model",
210
+ backend: "Qwen3-Coder-Next via OpenAI-compatible endpoint",
211
+ tokensPerSecond: 64,
212
+ p95LatencyMs: 1180,
213
+ memoryGb: 46,
214
+ costNote: "Runs as the Report Agent when AMD_QWEN_BASE_URL is configured.",
215
+ },
216
+ ];
217
+
218
+ const LOGS = [
219
+ "queued run qwen-vllm-cuda-starter in mock-safe mode",
220
+ "repo-doctor: scanning pyproject.toml, Dockerfile, scripts, and src/inference",
221
+ "repo-doctor: found nvidia/cuda base image in Dockerfile",
222
+ "repo-doctor: found direct torch.device('cuda') usage in src/inference/server.py:42",
223
+ "migration-planner: generated ROCm runtime image proposal",
224
+ "migration-planner: created backend-aware device resolver",
225
+ "build-runner: docker build -f Dockerfile.rocm .",
226
+ "build-runner: import torch; torch.version.hip detected when ROCm wheel is present",
227
+ "build-runner: vLLM OpenAI endpoint smoke test passed in demo mode",
228
+ "benchmark-agent: captured target profile for MI300X/vLLM serving",
229
+ "report-agent: preparing technical and business summary",
230
+ "completed run with fallback-safe report path",
231
+ ];
232
+
233
+ type MessageBlueprint = {
234
+ offsetMs: number;
235
+ agent: string;
236
+ toAgent: string;
237
+ role: string;
238
+ task: string;
239
+ leadAgent: string;
240
+ kind: AgentMessageKind;
241
+ replyToOffsetMs?: number;
242
+ memoryRefs?: string[];
243
+ message: (context: {
244
+ target: RunTarget;
245
+ sample: SampleRepo;
246
+ findings: Finding[];
247
+ patches: PatchPreview[];
248
+ }) => string;
249
+ };
250
+
251
+ const WAR_ROOM_MESSAGES: MessageBlueprint[] = [
252
+ {
253
+ offsetMs: 1_200,
254
+ agent: "Orchestrator",
255
+ toAgent: "Repo Doctor",
256
+ role: "Run coordinator",
257
+ task: "Repo compatibility scan",
258
+ leadAgent: "Repo Doctor",
259
+ kind: "question",
260
+ message: ({ target }) =>
261
+ `You are lead for the first task on ${target.label}. Ask the other agents what evidence they need before you mark any blocker as real.`,
262
+ },
263
+ {
264
+ offsetMs: 2_600,
265
+ agent: "Repo Doctor",
266
+ toAgent: "Build Runner",
267
+ role: "Compatibility scout",
268
+ task: "Repo compatibility scan",
269
+ leadAgent: "Repo Doctor",
270
+ kind: "question",
271
+ replyToOffsetMs: 1_200,
272
+ message: ({ target }) =>
273
+ target.type === "github"
274
+ ? `I am scanning ${target.scannedFiles || "the selected"} files. Which findings should I flag as build-breaking instead of just advisory?`
275
+ : "I am scanning Docker, vLLM scripts, PyTorch device paths, and benchmarks. Which findings should I flag as build-breaking instead of advisory?",
276
+ },
277
+ {
278
+ offsetMs: 3_700,
279
+ agent: "Build Runner",
280
+ toAgent: "Repo Doctor",
281
+ role: "Skeptical validator",
282
+ task: "Repo compatibility scan",
283
+ leadAgent: "Repo Doctor",
284
+ kind: "answer",
285
+ replyToOffsetMs: 2_600,
286
+ message:
287
+ () =>
288
+ "Treat container base images, hardcoded device selection, and missing smoke commands as build-breaking. Those decide whether the workload even starts on ROCm.",
289
+ },
290
+ {
291
+ offsetMs: 5_200,
292
+ agent: "Repo Doctor",
293
+ toAgent: "Migration Planner",
294
+ role: "Compatibility scout",
295
+ task: "Repo compatibility scan",
296
+ leadAgent: "Repo Doctor",
297
+ kind: "question",
298
+ replyToOffsetMs: 3_700,
299
+ message: ({ findings }) =>
300
+ findings[0]
301
+ ? `I found ${findings[0].category} in ${findings[0].file}:${findings[0].line}. Can you design the safest migration step for this first?`
302
+ : "I have no hard blocker yet. Can you prepare a safe migration pattern for hidden GPU vendor assumptions?",
303
+ },
304
+ {
305
+ offsetMs: 6_700,
306
+ agent: "Migration Planner",
307
+ toAgent: "Repo Doctor",
308
+ role: "Patch strategist",
309
+ task: "Repo compatibility scan",
310
+ leadAgent: "Repo Doctor",
311
+ kind: "proposal",
312
+ replyToOffsetMs: 5_200,
313
+ message: ({ findings }) =>
314
+ findings[0]
315
+ ? `Yes. First migration step: ${findings[0].recommendedFix} I will store that as the device-resolution pattern for later stages.`
316
+ : "Yes. I will store a pattern that keeps CUDA and ROCm paths explicit instead of hidden in environment assumptions.",
317
+ },
318
+ {
319
+ offsetMs: 8_200,
320
+ agent: "Repo Doctor",
321
+ toAgent: "Shared Memory",
322
+ role: "Compatibility scout",
323
+ task: "Repo compatibility scan",
324
+ leadAgent: "Repo Doctor",
325
+ kind: "memory",
326
+ replyToOffsetMs: 6_700,
327
+ memoryRefs: ["mem-device-resolution"],
328
+ message:
329
+ () =>
330
+ "Memory write: hardcoded device selection must be solved with one backend resolver, not scattered if/else checks.",
331
+ },
332
+ {
333
+ offsetMs: 9_600,
334
+ agent: "Orchestrator",
335
+ toAgent: "Migration Planner",
336
+ role: "Run coordinator",
337
+ task: "ROCm migration plan",
338
+ leadAgent: "Migration Planner",
339
+ kind: "question",
340
+ memoryRefs: ["mem-device-resolution"],
341
+ message:
342
+ () =>
343
+ "You are lead now. Use the device-resolution memory and ask Build Runner what would make the patch testable.",
344
+ },
345
+ {
346
+ offsetMs: 10_800,
347
+ agent: "Migration Planner",
348
+ toAgent: "Build Runner",
349
+ role: "Patch strategist",
350
+ task: "ROCm migration plan",
351
+ leadAgent: "Migration Planner",
352
+ kind: "question",
353
+ replyToOffsetMs: 9_600,
354
+ memoryRefs: ["mem-device-resolution"],
355
+ message:
356
+ () =>
357
+ "I can patch the resolver, but what acceptance check should prove this is ROCm-ready and not just cleaner code?",
358
+ },
359
+ {
360
+ offsetMs: 12_100,
361
+ agent: "Build Runner",
362
+ toAgent: "Migration Planner",
363
+ role: "Skeptical validator",
364
+ task: "ROCm migration plan",
365
+ leadAgent: "Migration Planner",
366
+ kind: "answer",
367
+ replyToOffsetMs: 10_800,
368
+ memoryRefs: ["mem-device-resolution"],
369
+ message:
370
+ () =>
371
+ "Acceptance check: import torch, report torch.version.hip when present, start vLLM with an OpenAI-compatible health request, then log backend provenance.",
372
+ },
373
+ {
374
+ offsetMs: 13_500,
375
+ agent: "Migration Planner",
376
+ toAgent: "Shared Memory",
377
+ role: "Patch strategist",
378
+ task: "ROCm migration plan",
379
+ leadAgent: "Migration Planner",
380
+ kind: "proposal",
381
+ replyToOffsetMs: 12_100,
382
+ memoryRefs: ["mem-device-resolution", "mem-rocm-acceptance"],
383
+ message: ({ patches }) =>
384
+ patches[0]
385
+ ? `Patch candidate for ${patches[0].file} is ready, and I am storing Build Runner's acceptance check with it.`
386
+ : "Patch candidate is ready, and I am storing Build Runner's acceptance check with it.",
387
+ },
388
+ {
389
+ offsetMs: 15_200,
390
+ agent: "Orchestrator",
391
+ toAgent: "Build Runner",
392
+ role: "Run coordinator",
393
+ task: "Build and smoke tests",
394
+ leadAgent: "Build Runner",
395
+ kind: "question",
396
+ memoryRefs: ["mem-device-resolution", "mem-rocm-acceptance"],
397
+ message:
398
+ () =>
399
+ "You are lead for validation. Look back at memory before challenging the plan: what can still fail later?",
400
+ },
401
+ {
402
+ offsetMs: 16_500,
403
+ agent: "Build Runner",
404
+ toAgent: "Migration Planner",
405
+ role: "Skeptical validator",
406
+ task: "Build and smoke tests",
407
+ leadAgent: "Build Runner",
408
+ kind: "challenge",
409
+ replyToOffsetMs: 15_200,
410
+ memoryRefs: ["mem-rocm-acceptance"],
411
+ message:
412
+ () =>
413
+ "I checked the memory. Device resolution is covered, but the Docker path can still fail. A CUDA image with ROCm notes is still a deployment trap.",
414
+ },
415
+ {
416
+ offsetMs: 17_800,
417
+ agent: "Migration Planner",
418
+ toAgent: "Build Runner",
419
+ role: "Patch strategist",
420
+ task: "Build and smoke tests",
421
+ leadAgent: "Build Runner",
422
+ kind: "answer",
423
+ replyToOffsetMs: 16_500,
424
+ message:
425
+ () =>
426
+ "Agreed. I will keep Dockerfile.rocm separate and avoid pretending the existing CUDA container is portable.",
427
+ },
428
+ {
429
+ offsetMs: 19_000,
430
+ agent: "Build Runner",
431
+ toAgent: "Shared Memory",
432
+ role: "Skeptical validator",
433
+ task: "Build and smoke tests",
434
+ leadAgent: "Build Runner",
435
+ kind: "memory",
436
+ replyToOffsetMs: 17_800,
437
+ memoryRefs: ["mem-container-split"],
438
+ message:
439
+ () =>
440
+ "Memory write: ROCm validation needs a separate container path plus a smoke command, not just migration notes in README.",
441
+ },
442
+ {
443
+ offsetMs: 20_300,
444
+ agent: "Orchestrator",
445
+ toAgent: "Benchmark Agent",
446
+ role: "Run coordinator",
447
+ task: "MI300X readiness benchmark",
448
+ leadAgent: "Benchmark Agent",
449
+ kind: "question",
450
+ memoryRefs: ["mem-rocm-acceptance", "mem-container-split"],
451
+ message:
452
+ () =>
453
+ "You are lead for measurement. Use the earlier memories and ask what numbers are safe to show before AMD access is connected.",
454
+ },
455
+ {
456
+ offsetMs: 21_500,
457
+ agent: "Benchmark Agent",
458
+ toAgent: "Report Agent",
459
+ role: "Evidence analyst",
460
+ task: "MI300X readiness benchmark",
461
+ leadAgent: "Benchmark Agent",
462
+ kind: "question",
463
+ replyToOffsetMs: 20_300,
464
+ memoryRefs: ["mem-rocm-acceptance", "mem-container-split"],
465
+ message:
466
+ () =>
467
+ "I can show estimated tokens/sec, p95 latency, memory, and bootability. How should I label estimates so the story stays credible?",
468
+ },
469
+ {
470
+ offsetMs: 22_700,
471
+ agent: "Report Agent",
472
+ toAgent: "Benchmark Agent",
473
+ role: "Submission narrator",
474
+ task: "MI300X readiness benchmark",
475
+ leadAgent: "Benchmark Agent",
476
+ kind: "answer",
477
+ replyToOffsetMs: 21_500,
478
+ message:
479
+ () =>
480
+ "Label estimates as a static ROCmPilot profile and reserve final proof for AMD Developer Cloud logs. Judges trust explicit provenance.",
481
+ },
482
+ {
483
+ offsetMs: 24_000,
484
+ agent: "Benchmark Agent",
485
+ toAgent: "Shared Memory",
486
+ role: "Evidence analyst",
487
+ task: "MI300X readiness benchmark",
488
+ leadAgent: "Benchmark Agent",
489
+ kind: "memory",
490
+ replyToOffsetMs: 22_700,
491
+ memoryRefs: ["mem-metric-provenance"],
492
+ message:
493
+ () =>
494
+ "Memory write: estimated metrics are acceptable only when labeled with provenance and paired with the exact AMD proof still needed.",
495
+ },
496
+ {
497
+ offsetMs: 25_200,
498
+ agent: "Orchestrator",
499
+ toAgent: "Report Agent",
500
+ role: "Run coordinator",
501
+ task: "Judge-ready report",
502
+ leadAgent: "Report Agent",
503
+ kind: "question",
504
+ memoryRefs: ["mem-device-resolution", "mem-container-split", "mem-metric-provenance"],
505
+ message:
506
+ () =>
507
+ "You are lead for the final report. Read the shared memory first, then ask if anyone disagrees with the submission story.",
508
+ },
509
+ {
510
+ offsetMs: 26_000,
511
+ agent: "Report Agent",
512
+ toAgent: "All agents",
513
+ role: "Submission narrator",
514
+ task: "Judge-ready report",
515
+ leadAgent: "Report Agent",
516
+ kind: "question",
517
+ replyToOffsetMs: 25_200,
518
+ memoryRefs: ["mem-device-resolution", "mem-container-split", "mem-metric-provenance"],
519
+ message:
520
+ () =>
521
+ "I read the memories. Final story: find CUDA locks, create ROCm patch path, validate container/smoke tests, show metric provenance. Any objections?",
522
+ },
523
+ {
524
+ offsetMs: 26_800,
525
+ agent: "Build Runner",
526
+ toAgent: "Report Agent",
527
+ role: "Skeptical validator",
528
+ task: "Judge-ready report",
529
+ leadAgent: "Report Agent",
530
+ kind: "answer",
531
+ replyToOffsetMs: 26_000,
532
+ memoryRefs: ["mem-container-split"],
533
+ message:
534
+ () =>
535
+ "No objection if the report says validation is complete for the demo path and pending for live MI300X logs.",
536
+ },
537
+ {
538
+ offsetMs: 27_400,
539
+ agent: "Benchmark Agent",
540
+ toAgent: "Report Agent",
541
+ role: "Evidence analyst",
542
+ task: "Judge-ready report",
543
+ leadAgent: "Report Agent",
544
+ kind: "answer",
545
+ replyToOffsetMs: 26_000,
546
+ memoryRefs: ["mem-metric-provenance"],
547
+ message:
548
+ () =>
549
+ "No objection if every metric names its source. I want the next live run to overwrite estimates with AMD SMI and vLLM logs.",
550
+ },
551
+ {
552
+ offsetMs: 28_200,
553
+ agent: "Orchestrator",
554
+ toAgent: "All agents",
555
+ role: "Run coordinator",
556
+ task: "Judge-ready report",
557
+ leadAgent: "Report Agent",
558
+ kind: "consensus",
559
+ replyToOffsetMs: 27_400,
560
+ memoryRefs: ["mem-device-resolution", "mem-container-split", "mem-metric-provenance"],
561
+ message:
562
+ () =>
563
+ "Consensus stored: ship a ROCm readiness report, patch previews, shared discussion memory, and an AMD/vLLM validation checklist. Next run should replace estimates with MI300X logs.",
564
+ },
565
+ ];
566
+
567
+ type MemoryBlueprint = {
568
+ offsetMs: number;
569
+ id: string;
570
+ title: string;
571
+ scope: string;
572
+ learnedFromAgent: string;
573
+ summary: (context: { findings: Finding[]; patches: PatchPreview[] }) => string;
574
+ solution: string;
575
+ };
576
+
577
+ const WAR_ROOM_MEMORY: MemoryBlueprint[] = [
578
+ {
579
+ offsetMs: 8_200,
580
+ id: "mem-device-resolution",
581
+ title: "Device resolution pattern",
582
+ scope: "Runtime compatibility",
583
+ learnedFromAgent: "Repo Doctor",
584
+ summary: ({ findings }) =>
585
+ findings[0]
586
+ ? `${findings[0].category} should be handled once in a resolver instead of repeated across inference code.`
587
+ : "GPU backend detection should be handled once in a resolver instead of repeated across inference code.",
588
+ solution: "Create a backend-aware resolver that accepts HIP-backed PyTorch as CUDA-compatible and records backend provenance.",
589
+ },
590
+ {
591
+ offsetMs: 13_500,
592
+ id: "mem-rocm-acceptance",
593
+ title: "ROCm acceptance checks",
594
+ scope: "Build validation",
595
+ learnedFromAgent: "Build Runner",
596
+ summary: () =>
597
+ "A patch is not enough unless the run proves import, backend detection, vLLM health, and provenance logging.",
598
+ solution: "Use a smoke command that imports torch, reports torch.version.hip, starts vLLM, and hits the OpenAI-compatible health path.",
599
+ },
600
+ {
601
+ offsetMs: 19_000,
602
+ id: "mem-container-split",
603
+ title: "Separate ROCm container path",
604
+ scope: "Deployment safety",
605
+ learnedFromAgent: "Build Runner",
606
+ summary: () =>
607
+ "A CUDA image with ROCm comments still leaves teams with a deployment trap.",
608
+ solution: "Keep Dockerfile.rocm and ROCm launch scripts separate, with CUDA preserved only as an optional backend.",
609
+ },
610
+ {
611
+ offsetMs: 24_000,
612
+ id: "mem-metric-provenance",
613
+ title: "Metric provenance rule",
614
+ scope: "Benchmark evidence",
615
+ learnedFromAgent: "Benchmark Agent",
616
+ summary: () =>
617
+ "Estimated benchmark cards are useful for the MVP only when their source is explicit.",
618
+ solution: "Label estimates as static ROCmPilot profiles and replace them with AMD SMI plus vLLM logs when MI300X access is available.",
619
+ },
620
+ ];
621
+
622
+ export type RunRecord = {
623
+ id: string;
624
+ sampleId: string;
625
+ mode: RunMode;
626
+ startedAt: number;
627
+ targetType: "sample" | "github";
628
+ repoUrl?: string;
629
+ };
630
+
631
+ export function getSample(sampleId: string | undefined) {
632
+ return SAMPLE_REPOS.find((sample) => sample.id === sampleId) ?? SAMPLE_REPOS[0];
633
+ }
634
+
635
+ function toBase64Url(value: string) {
636
+ return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
637
+ }
638
+
639
+ function fromBase64Url(value: string) {
640
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
641
+ return atob(padded);
642
+ }
643
+
644
+ function buildGitHubSample(repoUrl: string): SampleRepo {
645
+ const parsed = parseGitHubRepoUrl(repoUrl);
646
+
647
+ return {
648
+ id: "github-repo",
649
+ name: parsed?.label ?? "Public GitHub Repository",
650
+ repoUrl,
651
+ stack: "Detected from public GitHub files",
652
+ model: "Detected workload",
653
+ description: "A public repository scanned live by ROCmPilot.",
654
+ risk: "ROCm compatibility depends on detected CUDA/NVIDIA assumptions and live AMD validation.",
655
+ };
656
+ }
657
+
658
+ export function createRunRecord(sampleId: string, mode: RunMode, repoUrl?: string): RunRecord {
659
+ const startedAt = Date.now();
660
+ const safeSampleId = getSample(sampleId).id;
661
+ const nonce = Math.random().toString(36).slice(2, 8);
662
+ const parsedRepo = isRealGitHubRepoUrl(repoUrl) ? parseGitHubRepoUrl(repoUrl) : null;
663
+ const targetType = parsedRepo ? "github" : "sample";
664
+ const payload = parsedRepo ? toBase64Url(parsedRepo.repoUrl) : safeSampleId;
665
+
666
+ return {
667
+ id: `run.${startedAt.toString(36)}.${mode}.${targetType}.${payload}.${nonce}`,
668
+ sampleId: safeSampleId,
669
+ mode,
670
+ startedAt,
671
+ targetType,
672
+ repoUrl: parsedRepo?.repoUrl,
673
+ };
674
+ }
675
+
676
+ export function parseRunRecord(runId: string): RunRecord | null {
677
+ const parts = runId.split(".");
678
+
679
+ if (parts.length !== 6 || parts[0] !== "run") {
680
+ return null;
681
+ }
682
+
683
+ const [, startedAtBase36, mode, targetType, payload] = parts;
684
+ const startedAt = Number.parseInt(startedAtBase36, 36);
685
+
686
+ if (
687
+ !Number.isFinite(startedAt) ||
688
+ (mode !== "mock" && mode !== "amd") ||
689
+ (targetType !== "sample" && targetType !== "github")
690
+ ) {
691
+ return null;
692
+ }
693
+
694
+ if (targetType === "github") {
695
+ const repoUrl = fromBase64Url(payload);
696
+
697
+ if (!isRealGitHubRepoUrl(repoUrl)) {
698
+ return null;
699
+ }
700
+
701
+ return {
702
+ id: runId,
703
+ sampleId: "qwen-vllm-cuda",
704
+ mode,
705
+ startedAt,
706
+ targetType,
707
+ repoUrl,
708
+ };
709
+ }
710
+
711
+ const sample = SAMPLE_REPOS.find((candidate) => candidate.id === payload);
712
+
713
+ if (!sample) {
714
+ return null;
715
+ }
716
+
717
+ return {
718
+ id: runId,
719
+ sampleId: sample.id,
720
+ mode,
721
+ startedAt,
722
+ targetType,
723
+ };
724
+ }
725
+
726
+ export function getModelStatus(source: GpuModelStatus["source"] = "fallback"): GpuModelStatus {
727
+ const endpoint = process.env.AMD_QWEN_BASE_URL?.replace(/\/$/, "");
728
+ const model = process.env.AMD_QWEN_MODEL ?? "Qwen/Qwen3-Coder-Next";
729
+ const hfModel = process.env.HF_REPORT_MODEL ?? "Qwen/Qwen2.5-Coder-7B-Instruct";
730
+
731
+ if (endpoint && source === "amd-vllm") {
732
+ return {
733
+ status: "connected",
734
+ label: "AMD GPU Model: Connected",
735
+ model,
736
+ endpoint,
737
+ detail: "Report generation used the configured ROCm/vLLM OpenAI-compatible endpoint.",
738
+ source,
739
+ };
740
+ }
741
+
742
+ if (source === "hf-router") {
743
+ return {
744
+ status: "connected",
745
+ label: "HF Router: Connected",
746
+ model: hfModel,
747
+ endpoint: "https://router.huggingface.co/v1",
748
+ detail: "Report generation used Hugging Face Inference Providers as the temporary model backend.",
749
+ source,
750
+ };
751
+ }
752
+
753
+ if (endpoint) {
754
+ return {
755
+ status: "not-configured",
756
+ label: "AMD GPU Model: Endpoint configured",
757
+ model,
758
+ endpoint,
759
+ detail: "Endpoint is configured; report generation will attempt AMD-hosted Qwen first.",
760
+ source,
761
+ };
762
+ }
763
+
764
+ if (process.env.HF_TOKEN) {
765
+ return {
766
+ status: "not-configured",
767
+ label: "HF Router: Available",
768
+ model: hfModel,
769
+ endpoint: "https://router.huggingface.co/v1",
770
+ detail: "Hugging Face token is configured; final report generation will use HF unless AMD is configured.",
771
+ source: "hf-router",
772
+ };
773
+ }
774
+
775
+ return {
776
+ status: "fallback",
777
+ label: "AMD GPU Model: Demo fallback",
778
+ model,
779
+ endpoint: "Set AMD_QWEN_BASE_URL to enable live ROCm/vLLM inference",
780
+ detail: "The dashboard is using deterministic fallback output so the MVP demo remains reliable.",
781
+ source: "fallback",
782
+ };
783
+ }
784
+
785
+ export function getLongContextMemoryStatus(
786
+ runId: string,
787
+ storedItems = 0,
788
+ recalledItems = 0,
789
+ source: LongContextMemoryStatus["status"] = process.env.SYNAP_API_KEY ? "configured" : "fallback"
790
+ ): LongContextMemoryStatus {
791
+ const conversationId = buildMemoryConversationId(runId);
792
+ const customerId = process.env.SYNAP_CUSTOMER_ID ?? DEFAULT_MEMORY_CUSTOMER_ID;
793
+ const userId = process.env.SYNAP_USER_ID ?? DEFAULT_MEMORY_USER_ID;
794
+
795
+ if (source === "connected") {
796
+ return {
797
+ status: "connected",
798
+ label: "Synap Memory: Connected",
799
+ provider: "synap",
800
+ conversationId,
801
+ scope: `${customerId}/${userId}`,
802
+ detail: "Report Agent stored this run and retrieved scoped long-context memory from Synap.",
803
+ storedItems,
804
+ recalledItems,
805
+ };
806
+ }
807
+
808
+ if (source === "configured") {
809
+ return {
810
+ status: "configured",
811
+ label: "Synap Memory: Ready",
812
+ provider: "synap",
813
+ conversationId,
814
+ scope: `${customerId}/${userId}`,
815
+ detail: "Synap is configured; the Report Agent will ingest the war-room transcript during report generation.",
816
+ storedItems,
817
+ recalledItems,
818
+ };
819
+ }
820
+
821
+ if (source === "not-configured") {
822
+ return {
823
+ status: "not-configured",
824
+ label: "Synap Memory: Setup needed",
825
+ provider: "synap",
826
+ conversationId,
827
+ scope: `${customerId}/${userId}`,
828
+ detail: "Set SYNAP_API_KEY and run the Synap JS runtime setup to enable persistent memory.",
829
+ storedItems,
830
+ recalledItems,
831
+ };
832
+ }
833
+
834
+ return {
835
+ status: "fallback",
836
+ label: "Synap Memory: Local fallback",
837
+ provider: "local",
838
+ conversationId,
839
+ scope: "current stateless run",
840
+ detail: "Using reconstructed run memory now; Synap can persist it across sessions once credentials are configured.",
841
+ storedItems,
842
+ recalledItems,
843
+ };
844
+ }
845
+
846
+ function buildTarget(record: RunRecord, analysis?: RepoAnalysis): RunTarget {
847
+ if (record.targetType === "github" && record.repoUrl) {
848
+ const parsed = parseGitHubRepoUrl(record.repoUrl);
849
+
850
+ return {
851
+ type: "github",
852
+ repoUrl: record.repoUrl,
853
+ label: analysis?.label ?? parsed?.label ?? "GitHub repository",
854
+ branch: analysis?.branch ?? parsed?.branch,
855
+ scanStatus: analysis?.status ?? "pending",
856
+ scannedFiles: analysis?.scannedFiles ?? 0,
857
+ note:
858
+ analysis?.note ??
859
+ "ROCmPilot will fetch public GitHub files during the Repo Doctor stage.",
860
+ };
861
+ }
862
+
863
+ const sample = getSample(record.sampleId);
864
+
865
+ return {
866
+ type: "sample",
867
+ repoUrl: sample.repoUrl,
868
+ label: sample.name,
869
+ scanStatus: "fixture",
870
+ scannedFiles: 4,
871
+ note: "Using curated sample fixtures for a reliable demo run.",
872
+ };
873
+ }
874
+
875
+ function buildMessageId(record: RunRecord, offsetMs: number, agent: string) {
876
+ return `${record.id}.${offsetMs}.${agent.toLowerCase().replace(/\W+/g, "-")}`;
877
+ }
878
+
879
+ function buildAgentMessages(
880
+ elapsed: number,
881
+ record: RunRecord,
882
+ target: RunTarget,
883
+ sample: SampleRepo,
884
+ findings: Finding[],
885
+ patches: PatchPreview[]
886
+ ): AgentMessage[] {
887
+ return WAR_ROOM_MESSAGES.filter((blueprint) => elapsed >= blueprint.offsetMs).map((blueprint) => ({
888
+ id: buildMessageId(record, blueprint.offsetMs, blueprint.agent),
889
+ agent: blueprint.agent,
890
+ toAgent: blueprint.toAgent,
891
+ role: blueprint.role,
892
+ task: blueprint.task,
893
+ leadAgent: blueprint.leadAgent,
894
+ kind: blueprint.kind,
895
+ message: blueprint.message({ target, sample, findings, patches }),
896
+ replyToId:
897
+ blueprint.replyToOffsetMs === undefined
898
+ ? undefined
899
+ : WAR_ROOM_MESSAGES.find((message) => message.offsetMs === blueprint.replyToOffsetMs)
900
+ ? buildMessageId(
901
+ record,
902
+ blueprint.replyToOffsetMs,
903
+ WAR_ROOM_MESSAGES.find((message) => message.offsetMs === blueprint.replyToOffsetMs)?.agent ?? "unknown"
904
+ )
905
+ : undefined,
906
+ memoryRefs: blueprint.memoryRefs ?? [],
907
+ createdAt: new Date(record.startedAt + blueprint.offsetMs).toISOString(),
908
+ }));
909
+ }
910
+
911
+ function buildAgentMemory(
912
+ elapsed: number,
913
+ record: RunRecord,
914
+ messages: AgentMessage[],
915
+ findings: Finding[],
916
+ patches: PatchPreview[]
917
+ ): AgentMemory[] {
918
+ return WAR_ROOM_MEMORY.filter((memory) => elapsed >= memory.offsetMs).map((memory) => ({
919
+ id: memory.id,
920
+ title: memory.title,
921
+ scope: memory.scope,
922
+ learnedFromAgent: memory.learnedFromAgent,
923
+ summary: memory.summary({ findings, patches }),
924
+ solution: memory.solution,
925
+ createdAt: new Date(record.startedAt + memory.offsetMs).toISOString(),
926
+ usedBy: messages
927
+ .filter((message) => message.memoryRefs.includes(memory.id) && new Date(message.createdAt).getTime() > record.startedAt + memory.offsetMs)
928
+ .map((message) => message.agent),
929
+ }));
930
+ }
931
+
932
+ export function snapshotRun(record: RunRecord, analysis?: RepoAnalysis): RocmRun {
933
+ const elapsed = Math.max(0, Date.now() - record.startedAt);
934
+ const sample =
935
+ record.targetType === "github" && record.repoUrl
936
+ ? buildGitHubSample(record.repoUrl)
937
+ : getSample(record.sampleId);
938
+ const status: RunStatus = elapsed >= TOTAL_DURATION_MS ? "completed" : "running";
939
+ const target = buildTarget(record, analysis);
940
+ let cursor = 0;
941
+
942
+ const stages = STAGES.map((stage) => {
943
+ const stageStart = cursor;
944
+ const stageEnd = cursor + stage.durationMs;
945
+ cursor = stageEnd;
946
+
947
+ const stageElapsed = elapsed - stageStart;
948
+ const progress = Math.max(0, Math.min(100, Math.round((stageElapsed / stage.durationMs) * 100)));
949
+ const stageStatus: StageStatus =
950
+ progress >= 100 ? "completed" : progress > 0 ? "running" : "pending";
951
+
952
+ return {
953
+ id: stage.id,
954
+ agent: stage.agent,
955
+ title: stage.title,
956
+ description: stage.description,
957
+ status: stageStatus,
958
+ progress,
959
+ startedAt: stageElapsed > 0 ? new Date(record.startedAt + stageStart).toISOString() : undefined,
960
+ completedAt: stageStatus === "completed" ? new Date(record.startedAt + stageEnd).toISOString() : undefined,
961
+ };
962
+ });
963
+
964
+ const allFindings = analysis?.findings.length ? analysis.findings : FINDINGS;
965
+ const allPatches = analysis?.patches.length ? analysis.patches : PATCHES;
966
+ const allBenchmarks = record.targetType === "github"
967
+ ? BENCHMARKS.map((benchmark) => ({
968
+ ...benchmark,
969
+ costNote: benchmark.costNote.replace("demo profile", "static ROCmPilot profile until live AMD validation"),
970
+ }))
971
+ : BENCHMARKS;
972
+ const allLogs =
973
+ record.targetType === "github"
974
+ ? [
975
+ `queued public GitHub scan for ${target.label}`,
976
+ ...(analysis?.logs ?? ["repo-doctor: waiting for GitHub scan results"]),
977
+ "build-runner: generated ROCm validation plan without mutating repository files",
978
+ "benchmark-agent: prepared estimated MI300X profile pending live AMD run",
979
+ "report-agent: preparing technical and business summary",
980
+ ]
981
+ : LOGS;
982
+
983
+ const visibleFindings =
984
+ elapsed > 4_000
985
+ ? allFindings.slice(0, Math.min(allFindings.length, Math.ceil((elapsed - 4_000) / 3_000)))
986
+ : [];
987
+ const visiblePatches =
988
+ elapsed > 10_000
989
+ ? allPatches.slice(0, Math.min(allPatches.length, Math.ceil((elapsed - 10_000) / 4_000)))
990
+ : [];
991
+ const visibleBenchmarks = elapsed > 18_000 ? allBenchmarks : allBenchmarks.slice(0, 1);
992
+ const visibleLogs = allLogs.slice(0, Math.min(allLogs.length, Math.max(1, Math.ceil(elapsed / 2_300))));
993
+ const agentMessages = buildAgentMessages(
994
+ elapsed,
995
+ record,
996
+ target,
997
+ sample,
998
+ visibleFindings.length ? visibleFindings : allFindings,
999
+ visiblePatches.length ? visiblePatches : allPatches
1000
+ );
1001
+ const agentMemory = buildAgentMemory(
1002
+ elapsed,
1003
+ record,
1004
+ agentMessages,
1005
+ visibleFindings.length ? visibleFindings : allFindings,
1006
+ visiblePatches.length ? visiblePatches : allPatches
1007
+ );
1008
+
1009
+ return {
1010
+ id: record.id,
1011
+ sample,
1012
+ target,
1013
+ mode: record.mode,
1014
+ status,
1015
+ progress: Math.min(100, Math.round((elapsed / TOTAL_DURATION_MS) * 100)),
1016
+ startedAt: new Date(record.startedAt).toISOString(),
1017
+ completedAt: status === "completed" ? new Date(record.startedAt + TOTAL_DURATION_MS).toISOString() : undefined,
1018
+ stages,
1019
+ findings: visibleFindings,
1020
+ patches: visiblePatches,
1021
+ logs: visibleLogs,
1022
+ agentMessages,
1023
+ agentMemory,
1024
+ longContextMemory: getLongContextMemoryStatus(
1025
+ record.id,
1026
+ agentMemory.length,
1027
+ agentMessages.filter((message) => message.memoryRefs.length > 0).length
1028
+ ),
1029
+ benchmarks: visibleBenchmarks,
1030
+ modelStatus: getModelStatus(),
1031
+ };
1032
+ }
1033
+
1034
+ export function buildFallbackReport(run: RocmRun, longContext?: string) {
1035
+ const findingList = run.findings
1036
+ .map((finding) => `- **${finding.category}** in \`${finding.file}:${finding.line}\`: ${finding.recommendedFix}`)
1037
+ .join("\n");
1038
+ const memoryList = run.agentMemory
1039
+ .map((memory) => `- **${memory.title}**: ${memory.solution}`)
1040
+ .join("\n");
1041
+
1042
+ return `# ROCmPilot Migration Report
1043
+
1044
+ ## Executive Summary
1045
+
1046
+ ROCmPilot completed a multi-agent audit for **${run.sample.name}** and produced an AMD ROCm migration path for a PyTorch/vLLM workload. The system found CUDA-only assumptions, generated ROCm patch previews, and prepared the project for validation on AMD Developer Cloud.
1047
+
1048
+ ## Agent Findings
1049
+
1050
+ ${findingList || "- Findings are still being prepared."}
1051
+
1052
+ ## Shared Agent Memory
1053
+
1054
+ ${memoryList || "- Shared memory is still being written by the agents."}
1055
+
1056
+ ## Long-Context Memory
1057
+
1058
+ ${longContext || "- Synap memory was not available for this report, so ROCmPilot used the current run's reconstructed local memory."}
1059
+
1060
+ ## AMD GPU Usage
1061
+
1062
+ - Primary model target: **Qwen/Qwen3-Coder-Next**
1063
+ - Serving path: **ROCm + vLLM OpenAI-compatible endpoint**
1064
+ - GPU goal: run the Migration Planner or Report Agent on AMD Instinct MI300X
1065
+ - MVP fallback: deterministic report generation when the endpoint is unavailable
1066
+
1067
+ ## Business Value
1068
+
1069
+ ROCmPilot reduces the time needed to move inference services away from NVIDIA-only assumptions. Teams get a migration checklist, patch previews, benchmark evidence, and a report they can hand to infra leads before spending engineering time on a full port.
1070
+
1071
+ ## Next Step
1072
+
1073
+ Connect \`AMD_QWEN_BASE_URL\` to a live ROCm/vLLM endpoint and rerun the report stage to replace demo metrics with captured MI300X evidence.`;
1074
+ }
1075
+
1076
+ export function buildReportPrompt(run: RocmRun, longContext?: string) {
1077
+ return `Create a concise hackathon submission report for ROCmPilot.
1078
+
1079
+ Product: multi-agent ROCm migration dashboard.
1080
+ Track: AI Agents & Agentic Workflows.
1081
+ Sample repo: ${run.sample.name} (${run.sample.stack}).
1082
+ Target: ${run.target.label} (${run.target.repoUrl}).
1083
+ Scan status: ${run.target.scanStatus}, scanned files: ${run.target.scannedFiles}.
1084
+ GPU story: Qwen3-Coder-Next served on AMD Instinct MI300X with ROCm/vLLM powers the report or migration agent when configured.
1085
+
1086
+ Findings:
1087
+ ${run.findings.map((finding) => `- ${finding.severity}: ${finding.category} in ${finding.file}:${finding.line}. Fix: ${finding.recommendedFix}`).join("\n")}
1088
+
1089
+ Patches:
1090
+ ${run.patches.map((patch) => `- ${patch.file}: ${patch.rationale}`).join("\n")}
1091
+
1092
+ Shared memory:
1093
+ ${run.agentMemory.map((memory) => `- ${memory.title}: ${memory.solution}`).join("\n")}
1094
+
1095
+ Long-context memory from Synap or fallback memory:
1096
+ ${longContext || "- No long-context memory was available beyond the current run."}
1097
+
1098
+ Benchmarks:
1099
+ ${run.benchmarks.map((benchmark) => `- ${benchmark.label}: ${benchmark.backend}, ${benchmark.tokensPerSecond} tok/s, p95 ${benchmark.p95LatencyMs}ms, ${benchmark.memoryGb}GB.`).join("\n")}
1100
+
1101
+ Write markdown with these sections only: Executive Summary, Agent Workflow, AMD GPU Proof, Business Value, Next 48 Hours.`;
1102
+ }
src/lib/rocmpilot/github-scanner.ts ADDED
@@ -0,0 +1,462 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Finding, PatchPreview } from "./types";
2
+ import { parseGitHubRepoUrl } from "./github-url";
3
+
4
+ type GitHubTreeItem = {
5
+ path: string;
6
+ mode: string;
7
+ type: "blob" | "tree";
8
+ sha: string;
9
+ size?: number;
10
+ url: string;
11
+ };
12
+
13
+ type GitHubTreeResponse = {
14
+ tree?: GitHubTreeItem[];
15
+ truncated?: boolean;
16
+ };
17
+
18
+ type GitHubRepoResponse = {
19
+ default_branch?: string;
20
+ };
21
+
22
+ type GitHubBlobResponse = {
23
+ content?: string;
24
+ encoding?: string;
25
+ };
26
+
27
+ type ScannedFile = {
28
+ path: string;
29
+ content: string;
30
+ };
31
+
32
+ export type RepoAnalysis = {
33
+ status: "scanned" | "failed";
34
+ label: string;
35
+ repoUrl: string;
36
+ branch?: string;
37
+ scannedFiles: number;
38
+ findings: Finding[];
39
+ patches: PatchPreview[];
40
+ logs: string[];
41
+ stack: string;
42
+ note: string;
43
+ };
44
+
45
+ const globalForGitHubScan = globalThis as unknown as {
46
+ rocmPilotScanCache?: Map<string, { expiresAt: number; analysis: RepoAnalysis }>;
47
+ };
48
+
49
+ const scanCache = globalForGitHubScan.rocmPilotScanCache ?? new Map();
50
+ globalForGitHubScan.rocmPilotScanCache = scanCache;
51
+
52
+ function githubHeaders() {
53
+ const headers: Record<string, string> = {
54
+ Accept: "application/vnd.github+json",
55
+ "User-Agent": "ROCmPilot",
56
+ "X-GitHub-Api-Version": "2022-11-28",
57
+ };
58
+
59
+ if (process.env.GITHUB_TOKEN) {
60
+ headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
61
+ }
62
+
63
+ return headers;
64
+ }
65
+
66
+ async function fetchGitHubJson<T>(url: string): Promise<T> {
67
+ const response = await fetch(url, {
68
+ headers: githubHeaders(),
69
+ next: { revalidate: 180 },
70
+ });
71
+
72
+ if (!response.ok) {
73
+ throw new Error(`GitHub returned ${response.status} for ${url}`);
74
+ }
75
+
76
+ return response.json() as Promise<T>;
77
+ }
78
+
79
+ function isRelevantPath(path: string) {
80
+ const lower = path.toLowerCase();
81
+
82
+ if (
83
+ lower.includes("node_modules/") ||
84
+ lower.includes(".git/") ||
85
+ lower.includes("dist/") ||
86
+ lower.includes("build/") ||
87
+ lower.includes(".next/")
88
+ ) {
89
+ return false;
90
+ }
91
+
92
+ return (
93
+ /^dockerfile/i.test(path) ||
94
+ lower.endsWith("docker-compose.yml") ||
95
+ lower.endsWith("docker-compose.yaml") ||
96
+ lower.endsWith("requirements.txt") ||
97
+ lower.endsWith("requirements-rocm.txt") ||
98
+ lower.endsWith("pyproject.toml") ||
99
+ lower.endsWith("environment.yml") ||
100
+ lower.endsWith("environment.yaml") ||
101
+ lower.endsWith(".py") ||
102
+ lower.endsWith(".sh") ||
103
+ lower.endsWith(".yaml") ||
104
+ lower.endsWith(".yml") ||
105
+ lower.includes("vllm") ||
106
+ lower.includes("inference") ||
107
+ lower.includes("serve") ||
108
+ lower.includes("benchmark")
109
+ );
110
+ }
111
+
112
+ function firstLineOf(content: string, pattern: RegExp) {
113
+ const lines = content.split(/\r?\n/);
114
+ const index = lines.findIndex((line) => pattern.test(line));
115
+ return index >= 0 ? index + 1 : 1;
116
+ }
117
+
118
+ function addFinding(
119
+ findings: Finding[],
120
+ finding: Omit<Finding, "id">,
121
+ idSeed: string
122
+ ) {
123
+ if (findings.length >= 10) {
124
+ return;
125
+ }
126
+
127
+ const id = `${idSeed}-${findings.length + 1}`.replace(/[^a-z0-9-]/gi, "-").toLowerCase();
128
+
129
+ if (!findings.some((existing) => existing.file === finding.file && existing.category === finding.category)) {
130
+ findings.push({ id, ...finding });
131
+ }
132
+ }
133
+
134
+ function detectFindings(files: ScannedFile[]) {
135
+ const findings: Finding[] = [];
136
+
137
+ for (const file of files) {
138
+ const lowerPath = file.path.toLowerCase();
139
+ const content = file.content;
140
+
141
+ if (/nvidia\/cuda|nvidia-container|--gpus\s+all|nvidia-smi/i.test(content)) {
142
+ addFinding(
143
+ findings,
144
+ {
145
+ severity: "high",
146
+ category: "NVIDIA container/runtime assumption",
147
+ file: file.path,
148
+ line: firstLineOf(content, /nvidia\/cuda|nvidia-container|--gpus\s+all|nvidia-smi/i),
149
+ explanation:
150
+ "The repository includes NVIDIA-specific container/runtime configuration, which will not run cleanly on AMD ROCm infrastructure.",
151
+ recommendedFix:
152
+ "Add an AMD ROCm runtime path using a ROCm/vLLM image and keep NVIDIA launch flags behind a backend-specific profile.",
153
+ },
154
+ file.path
155
+ );
156
+ }
157
+
158
+ if (/torch\.device\(\s*["']cuda["']\s*\)|\.cuda\(|\.cuda\(\)|device_map\s*=\s*["']cuda["']/i.test(content)) {
159
+ addFinding(
160
+ findings,
161
+ {
162
+ severity: "critical",
163
+ category: "Hardcoded CUDA device path",
164
+ file: file.path,
165
+ line: firstLineOf(content, /torch\.device\(\s*["']cuda["']\s*\)|\.cuda\(|\.cuda\(\)|device_map\s*=\s*["']cuda["']/i),
166
+ explanation:
167
+ "The code moves models/tensors directly to CUDA, so the workload needs a backend-aware device resolver before AMD validation.",
168
+ recommendedFix:
169
+ "Introduce a resolver that treats HIP-backed torch.cuda availability as ROCm and records backend provenance in logs/metrics.",
170
+ },
171
+ file.path
172
+ );
173
+ }
174
+
175
+ if (/torch\.cuda|cuda_visible_devices|hip_visible_devices/i.test(content)) {
176
+ addFinding(
177
+ findings,
178
+ {
179
+ severity: "medium",
180
+ category: "GPU backend detection needs abstraction",
181
+ file: file.path,
182
+ line: firstLineOf(content, /torch\.cuda|cuda_visible_devices|hip_visible_devices/i),
183
+ explanation:
184
+ "The repo checks GPU availability through vendor-specific environment or PyTorch CUDA APIs without documenting AMD behavior.",
185
+ recommendedFix:
186
+ "Centralize backend detection and expose CUDA, ROCm, and CPU as explicit runtime modes.",
187
+ },
188
+ file.path
189
+ );
190
+ }
191
+
192
+ if (/cu12|cu118|cu121|nvidia-|cupy-cuda|bitsandbytes|flash-attn|xformers/i.test(content)) {
193
+ addFinding(
194
+ findings,
195
+ {
196
+ severity: "high",
197
+ category: "CUDA-oriented dependency",
198
+ file: file.path,
199
+ line: firstLineOf(content, /cu12|cu118|cu121|nvidia-|cupy-cuda|bitsandbytes|flash-attn|xformers/i),
200
+ explanation:
201
+ "One or more dependencies are pinned to CUDA/NVIDIA builds, which can block ROCm package resolution.",
202
+ recommendedFix:
203
+ "Create a ROCm requirements profile and verify PyTorch/vLLM wheels against the target ROCm version.",
204
+ },
205
+ file.path
206
+ );
207
+ }
208
+
209
+ if (/vllm/i.test(content) && /tensor-parallel-size|max-model-len|served-model-name/i.test(content) === false) {
210
+ addFinding(
211
+ findings,
212
+ {
213
+ severity: "low",
214
+ category: "vLLM serving defaults need AMD profile",
215
+ file: file.path,
216
+ line: firstLineOf(content, /vllm/i),
217
+ explanation:
218
+ "vLLM is present, but the repo does not expose the serving knobs that matter when moving to MI300X validation.",
219
+ recommendedFix:
220
+ "Add a backend-aware vLLM launch script with model length, tensor parallelism, and metrics capture settings.",
221
+ },
222
+ file.path
223
+ );
224
+ }
225
+
226
+ if (lowerPath.includes("benchmark") && /tokens|latency|memory|throughput/i.test(content) === false) {
227
+ addFinding(
228
+ findings,
229
+ {
230
+ severity: "medium",
231
+ category: "Benchmark evidence incomplete",
232
+ file: file.path,
233
+ line: 1,
234
+ explanation:
235
+ "The benchmark file exists but does not obviously capture tokens/sec, latency, memory, and backend metadata.",
236
+ recommendedFix:
237
+ "Add a ROCm benchmark profile that emits AMD SMI/vLLM metrics for the final migration report.",
238
+ },
239
+ file.path
240
+ );
241
+ }
242
+ }
243
+
244
+ if (findings.length === 0) {
245
+ findings.push({
246
+ id: "no-direct-cuda-blockers",
247
+ severity: "low",
248
+ category: "No direct CUDA blockers in scanned files",
249
+ file: "repository",
250
+ line: 1,
251
+ explanation:
252
+ "ROCmPilot did not find obvious CUDA-only strings in the scanned files, but the workload still needs a live AMD smoke test.",
253
+ recommendedFix:
254
+ "Run the generated ROCm validation script on AMD Developer Cloud and attach the benchmark evidence to the report.",
255
+ });
256
+ }
257
+
258
+ return findings;
259
+ }
260
+
261
+ function buildPatchPreviews(findings: Finding[]): PatchPreview[] {
262
+ const hasDocker = findings.some((finding) => finding.category.includes("container"));
263
+ const hasDevice = findings.some((finding) => finding.category.includes("CUDA device") || finding.category.includes("backend"));
264
+ const hasDeps = findings.some((finding) => finding.category.includes("dependency"));
265
+ const patches: PatchPreview[] = [];
266
+
267
+ if (hasDevice) {
268
+ patches.push({
269
+ id: "device-resolver",
270
+ file: "src/rocmpilot_device.py",
271
+ rationale:
272
+ "Adds a reusable runtime resolver so the project can run on CUDA, ROCm-backed PyTorch, or CPU without hardcoded model code.",
273
+ diff: `+import torch
274
+ +
275
+ +def resolve_accelerator() -> tuple[str, str]:
276
+ + if torch.cuda.is_available():
277
+ + backend = "rocm" if getattr(torch.version, "hip", None) else "cuda"
278
+ + return "cuda", backend
279
+ + return "cpu", "cpu"
280
+ +
281
+ +DEVICE, GPU_BACKEND = resolve_accelerator()
282
+ +print(f"ROCmPilot backend={GPU_BACKEND} device={DEVICE}")
283
+ `,
284
+ });
285
+ }
286
+
287
+ if (hasDocker) {
288
+ patches.push({
289
+ id: "dockerfile-rocm",
290
+ file: "Dockerfile.rocm",
291
+ rationale:
292
+ "Creates an AMD-specific runtime container while preserving the original repository for existing CUDA deployments.",
293
+ diff: `+FROM rocm/vllm:latest
294
+ +
295
+ +WORKDIR /workspace
296
+ +COPY . .
297
+ +ENV HIP_VISIBLE_DEVICES=0
298
+ +ENV VLLM_USE_ROCM=1
299
+ +RUN pip install --no-cache-dir -r requirements-rocm.txt
300
+ +CMD ["bash", "scripts/serve-rocm.sh"]
301
+ `,
302
+ });
303
+ }
304
+
305
+ if (hasDeps) {
306
+ patches.push({
307
+ id: "requirements-rocm",
308
+ file: "requirements-rocm.txt",
309
+ rationale:
310
+ "Separates ROCm dependencies from CUDA pins so CI and AMD Developer Cloud validation can install a clean environment.",
311
+ diff: `+torch
312
+ +transformers
313
+ +accelerate
314
+ +vllm
315
+ +sentencepiece
316
+ +# Verify exact ROCm-compatible wheel versions in AMD Developer Cloud before production use.
317
+ `,
318
+ });
319
+ }
320
+
321
+ patches.push({
322
+ id: "serve-rocm",
323
+ file: "scripts/serve-rocm.sh",
324
+ rationale:
325
+ "Provides the OpenAI-compatible vLLM endpoint that ROCmPilot can call for the Report Agent on AMD hardware.",
326
+ diff: `+#!/usr/bin/env bash
327
+ +set -euo pipefail
328
+ +
329
+ +MODEL="\${MODEL:-Qwen/Qwen3-Coder-Next}"
330
+ +PORT="\${PORT:-8000}"
331
+ +
332
+ +python -m vllm.entrypoints.openai.api_server \\
333
+ + --model "$MODEL" \\
334
+ + --host 0.0.0.0 \\
335
+ + --port "$PORT" \\
336
+ + --tensor-parallel-size "\${TENSOR_PARALLEL_SIZE:-1}" \\
337
+ + --max-model-len "\${MAX_MODEL_LEN:-32768}"
338
+ `,
339
+ });
340
+
341
+ return patches.slice(0, 4);
342
+ }
343
+
344
+ function inferStack(files: ScannedFile[]) {
345
+ const joined = files.map((file) => `${file.path}\n${file.content.slice(0, 2_000)}`).join("\n").toLowerCase();
346
+ const stack = new Set<string>();
347
+
348
+ if (joined.includes("vllm")) stack.add("vLLM");
349
+ if (joined.includes("torch")) stack.add("PyTorch");
350
+ if (joined.includes("transformers")) stack.add("Transformers");
351
+ if (joined.includes("fastapi")) stack.add("FastAPI");
352
+ if (joined.includes("dockerfile")) stack.add("Docker");
353
+ if (joined.includes("langchain")) stack.add("LangChain");
354
+ if (joined.includes("crewai")) stack.add("CrewAI");
355
+
356
+ return stack.size > 0 ? Array.from(stack).join(", ") : "Python/AI workload";
357
+ }
358
+
359
+ async function fetchRelevantFiles(owner: string, repo: string, ref: string) {
360
+ const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${encodeURIComponent(ref)}?recursive=1`;
361
+ const tree = await fetchGitHubJson<GitHubTreeResponse>(treeUrl);
362
+ const blobs = (tree.tree ?? [])
363
+ .filter((item) => item.type === "blob" && isRelevantPath(item.path) && (item.size ?? 0) <= 120_000)
364
+ .slice(0, 32);
365
+
366
+ const files: ScannedFile[] = [];
367
+
368
+ for (const blob of blobs) {
369
+ const data = await fetchGitHubJson<GitHubBlobResponse>(
370
+ `https://api.github.com/repos/${owner}/${repo}/git/blobs/${blob.sha}`
371
+ );
372
+
373
+ if (data.encoding === "base64" && data.content) {
374
+ files.push({
375
+ path: blob.path,
376
+ content: Buffer.from(data.content.replace(/\n/g, ""), "base64").toString("utf8"),
377
+ });
378
+ }
379
+ }
380
+
381
+ return files;
382
+ }
383
+
384
+ export async function analyzeGitHubRepository(repoUrl: string): Promise<RepoAnalysis> {
385
+ const parsed = parseGitHubRepoUrl(repoUrl);
386
+
387
+ if (!parsed) {
388
+ return failedAnalysis(repoUrl, "Invalid GitHub URL.");
389
+ }
390
+
391
+ const cacheKey = parsed.repoUrl;
392
+ const cached = scanCache.get(cacheKey);
393
+
394
+ if (cached && cached.expiresAt > Date.now()) {
395
+ return cached.analysis;
396
+ }
397
+
398
+ try {
399
+ const repo = await fetchGitHubJson<GitHubRepoResponse>(
400
+ `https://api.github.com/repos/${parsed.owner}/${parsed.repo}`
401
+ );
402
+ const ref = parsed.branch ?? repo.default_branch ?? "main";
403
+ const files = await fetchRelevantFiles(parsed.owner, parsed.repo, ref);
404
+ const findings = detectFindings(files);
405
+ const patches = buildPatchPreviews(findings);
406
+ const stack = inferStack(files);
407
+ const analysis: RepoAnalysis = {
408
+ status: "scanned",
409
+ label: parsed.label,
410
+ repoUrl: parsed.repoUrl,
411
+ branch: ref,
412
+ scannedFiles: files.length,
413
+ findings,
414
+ patches,
415
+ stack,
416
+ note: `Scanned ${files.length} public GitHub files from ${parsed.label}@${ref}.`,
417
+ logs: [
418
+ `github-scan: resolved ${parsed.label}@${ref}`,
419
+ `github-scan: selected ${files.length} relevant files for ROCm analysis`,
420
+ `repo-doctor: detected stack profile: ${stack}`,
421
+ `migration-planner: produced ${findings.length} findings and ${patches.length} patch previews`,
422
+ ],
423
+ };
424
+
425
+ scanCache.set(cacheKey, { analysis, expiresAt: Date.now() + 180_000 });
426
+ return analysis;
427
+ } catch (error) {
428
+ return failedAnalysis(
429
+ parsed.repoUrl,
430
+ error instanceof Error ? error.message : "Unknown GitHub scan failure.",
431
+ parsed.label,
432
+ parsed.branch
433
+ );
434
+ }
435
+ }
436
+
437
+ function failedAnalysis(repoUrl: string, message: string, label = "GitHub repository", branch?: string): RepoAnalysis {
438
+ return {
439
+ status: "failed",
440
+ label,
441
+ repoUrl,
442
+ branch,
443
+ scannedFiles: 0,
444
+ stack: "Public GitHub repository",
445
+ note: message,
446
+ findings: [
447
+ {
448
+ id: "github-scan-failed",
449
+ severity: "medium",
450
+ category: "GitHub scan unavailable",
451
+ file: "repository",
452
+ line: 1,
453
+ explanation:
454
+ "ROCmPilot could not fetch enough public repository data to complete a live scan. The app remains usable with sample fixtures.",
455
+ recommendedFix:
456
+ "Check that the repository is public, add GITHUB_TOKEN for higher API limits, or use the sample workload for the demo.",
457
+ },
458
+ ],
459
+ patches: buildPatchPreviews([]),
460
+ logs: [`github-scan: ${message}`, "fallback: using safe ROCm migration guidance"],
461
+ };
462
+ }
src/lib/rocmpilot/github-url.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type GitHubRepoRef = {
2
+ owner: string;
3
+ repo: string;
4
+ branch?: string;
5
+ repoUrl: string;
6
+ label: string;
7
+ };
8
+
9
+ export function parseGitHubRepoUrl(input: string | undefined): GitHubRepoRef | null {
10
+ if (!input) {
11
+ return null;
12
+ }
13
+
14
+ try {
15
+ const url = new URL(input.trim());
16
+
17
+ if (url.hostname !== "github.com" && url.hostname !== "www.github.com") {
18
+ return null;
19
+ }
20
+
21
+ const [owner, repoWithSuffix, maybeTree, ...rest] = url.pathname
22
+ .split("/")
23
+ .filter(Boolean);
24
+ const repo = repoWithSuffix?.replace(/\.git$/, "");
25
+
26
+ if (!owner || !repo) {
27
+ return null;
28
+ }
29
+
30
+ const branch = maybeTree === "tree" && rest.length > 0 ? rest.join("/") : undefined;
31
+
32
+ return {
33
+ owner,
34
+ repo,
35
+ branch,
36
+ repoUrl: `https://github.com/${owner}/${repo}${branch ? `/tree/${branch}` : ""}`,
37
+ label: `${owner}/${repo}`,
38
+ };
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ export function isRealGitHubRepoUrl(input: string | undefined) {
45
+ const repo = parseGitHubRepoUrl(input);
46
+ return Boolean(repo && repo.owner !== "example");
47
+ }
src/lib/rocmpilot/memory-ids.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export const DEFAULT_MEMORY_CUSTOMER_ID = "rocmpilot-hackathon";
2
+ export const DEFAULT_MEMORY_USER_ID = "rocmpilot-agent-fleet";
3
+
4
+ export function buildMemoryConversationId(runId: string) {
5
+ return `rocmpilot-${runId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 140)}`;
6
+ }
src/lib/rocmpilot/store.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createRunRecord, parseRunRecord, snapshotRun } from "./data";
2
+ import { analyzeGitHubRepository } from "./github-scanner";
3
+ import type { RocmRun, RunMode } from "./types";
4
+
5
+ export function createRun(sampleId: string, mode: RunMode = "mock", repoUrl?: string): RocmRun {
6
+ const record = createRunRecord(sampleId, mode, repoUrl);
7
+ return snapshotRun(record);
8
+ }
9
+
10
+ export async function getRun(runId: string): Promise<RocmRun | null> {
11
+ const record = parseRunRecord(runId);
12
+
13
+ if (!record) {
14
+ return null;
15
+ }
16
+
17
+ if (record.targetType === "github" && record.repoUrl) {
18
+ const analysis = await analyzeGitHubRepository(record.repoUrl);
19
+ return snapshotRun(record, analysis);
20
+ }
21
+
22
+ return snapshotRun(record);
23
+ }
src/lib/rocmpilot/synap-memory.ts ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type {
2
+ ChatMessage,
3
+ ContextForPromptResult,
4
+ ContextResponse,
5
+ SynapClient,
6
+ SynapClientOptions,
7
+ } from "@maximem/synap-js-sdk";
8
+ import {
9
+ getLongContextMemoryStatus,
10
+ } from "./data";
11
+ import {
12
+ buildMemoryConversationId,
13
+ DEFAULT_MEMORY_CUSTOMER_ID,
14
+ DEFAULT_MEMORY_USER_ID,
15
+ } from "./memory-ids";
16
+ import type { LongContextMemoryStatus, RocmRun } from "./types";
17
+
18
+ type SynapSyncResult = {
19
+ status: LongContextMemoryStatus;
20
+ promptContext: string;
21
+ };
22
+
23
+ function parseOptionalPort(value: string | undefined) {
24
+ if (!value) {
25
+ return undefined;
26
+ }
27
+
28
+ const port = Number.parseInt(value, 10);
29
+ return Number.isFinite(port) ? port : undefined;
30
+ }
31
+
32
+ function parseOptionalBoolean(value: string | undefined) {
33
+ if (value === undefined) {
34
+ return undefined;
35
+ }
36
+
37
+ return ["1", "true", "yes"].includes(value.toLowerCase());
38
+ }
39
+
40
+ function getSynapIdentity(run: RocmRun) {
41
+ return {
42
+ conversationId: buildMemoryConversationId(run.id),
43
+ customerId: process.env.SYNAP_CUSTOMER_ID ?? DEFAULT_MEMORY_CUSTOMER_ID,
44
+ userId: process.env.SYNAP_USER_ID ?? DEFAULT_MEMORY_USER_ID,
45
+ };
46
+ }
47
+
48
+ function buildSynapMessages(run: RocmRun): ChatMessage[] {
49
+ const targetSummary = [
50
+ `ROCmPilot run ${run.id}`,
51
+ `Target: ${run.target.label} (${run.target.repoUrl})`,
52
+ `Scan status: ${run.target.scanStatus}; scanned files: ${run.target.scannedFiles}`,
53
+ `Goal: migrate PyTorch/vLLM workload toward AMD ROCm readiness.`,
54
+ ].join("\n");
55
+
56
+ const agentDiscussion = run.agentMessages.map((message) => ({
57
+ role: "assistant" as const,
58
+ content: [
59
+ `[${message.agent} -> ${message.toAgent}] ${message.kind.toUpperCase()}`,
60
+ `Task: ${message.task}`,
61
+ `Lead: ${message.leadAgent}`,
62
+ `Message: ${message.message}`,
63
+ message.memoryRefs.length ? `Memory refs: ${message.memoryRefs.join(", ")}` : "",
64
+ ]
65
+ .filter(Boolean)
66
+ .join("\n"),
67
+ metadata: {
68
+ agent: message.agent,
69
+ toAgent: message.toAgent,
70
+ kind: message.kind,
71
+ task: message.task,
72
+ leadAgent: message.leadAgent,
73
+ runId: run.id,
74
+ },
75
+ }));
76
+
77
+ const sharedMemory = run.agentMemory.map((memory) => ({
78
+ role: "assistant" as const,
79
+ content: [
80
+ `Shared memory: ${memory.title}`,
81
+ `Scope: ${memory.scope}`,
82
+ `Learned from: ${memory.learnedFromAgent}`,
83
+ `Summary: ${memory.summary}`,
84
+ `Reusable solution: ${memory.solution}`,
85
+ memory.usedBy.length ? `Reused by: ${Array.from(new Set(memory.usedBy)).join(", ")}` : "",
86
+ ]
87
+ .filter(Boolean)
88
+ .join("\n"),
89
+ metadata: {
90
+ memoryId: memory.id,
91
+ scope: memory.scope,
92
+ learnedFromAgent: memory.learnedFromAgent,
93
+ runId: run.id,
94
+ },
95
+ }));
96
+
97
+ return [
98
+ {
99
+ role: "user",
100
+ content: targetSummary,
101
+ metadata: {
102
+ runId: run.id,
103
+ target: run.target.label,
104
+ repository: run.target.repoUrl,
105
+ },
106
+ },
107
+ ...agentDiscussion,
108
+ ...sharedMemory,
109
+ ];
110
+ }
111
+
112
+ function buildLocalMemoryContext(run: RocmRun) {
113
+ const memories = run.agentMemory
114
+ .map((memory) => `- ${memory.title} (${memory.scope}): ${memory.solution}`)
115
+ .join("\n");
116
+ const recentDiscussion = run.agentMessages
117
+ .slice(-8)
118
+ .map((message) => `- ${message.agent} -> ${message.toAgent}: ${message.message}`)
119
+ .join("\n");
120
+
121
+ return [
122
+ "Current run memory:",
123
+ memories || "- No shared memory has been written yet.",
124
+ "",
125
+ "Recent agent discussion:",
126
+ recentDiscussion || "- No agent discussion is available yet.",
127
+ ].join("\n");
128
+ }
129
+
130
+ function countContextItems(context: ContextResponse | null) {
131
+ if (!context) {
132
+ return 0;
133
+ }
134
+
135
+ return (
136
+ (context.facts?.length ?? 0) +
137
+ (context.preferences?.length ?? 0) +
138
+ (context.episodes?.length ?? 0) +
139
+ (context.emotions?.length ?? 0) +
140
+ (context.temporalEvents?.length ?? 0)
141
+ );
142
+ }
143
+
144
+ function summarizeContext(
145
+ context: ContextResponse | null,
146
+ promptContext: ContextForPromptResult | null
147
+ ) {
148
+ const sections: string[] = [];
149
+
150
+ if (promptContext?.formattedContext) {
151
+ sections.push(`Synap compacted context:\n${promptContext.formattedContext}`);
152
+ }
153
+
154
+ if (context?.facts?.length) {
155
+ sections.push(
156
+ `Synap facts:\n${context.facts
157
+ .slice(0, 5)
158
+ .map((fact) => `- ${fact.content}`)
159
+ .join("\n")}`
160
+ );
161
+ }
162
+
163
+ if (context?.episodes?.length) {
164
+ sections.push(
165
+ `Synap episodes:\n${context.episodes
166
+ .slice(0, 5)
167
+ .map((episode) => `- ${episode.summary}`)
168
+ .join("\n")}`
169
+ );
170
+ }
171
+
172
+ if (context?.preferences?.length) {
173
+ sections.push(
174
+ `Synap preferences:\n${context.preferences
175
+ .slice(0, 5)
176
+ .map((preference) => `- ${preference.content}`)
177
+ .join("\n")}`
178
+ );
179
+ }
180
+
181
+ return sections.join("\n\n");
182
+ }
183
+
184
+ function synapOptions(): SynapClientOptions {
185
+ return {
186
+ apiKey: process.env.SYNAP_API_KEY,
187
+ instanceId: process.env.SYNAP_INSTANCE_ID,
188
+ baseUrl: process.env.SYNAP_BASE_URL,
189
+ grpcHost: process.env.SYNAP_GRPC_HOST,
190
+ grpcPort: parseOptionalPort(process.env.SYNAP_GRPC_PORT),
191
+ grpcUseTls: parseOptionalBoolean(process.env.SYNAP_GRPC_TLS),
192
+ autoSetup: parseOptionalBoolean(process.env.SYNAP_AUTO_SETUP) ?? false,
193
+ requestTimeoutMs: 10_000,
194
+ initTimeoutMs: 10_000,
195
+ ingestTimeoutMs: 10_000,
196
+ onLog: (level, message) => {
197
+ if (level === "error") {
198
+ console.warn(`Synap ${level}: ${message}`);
199
+ }
200
+ },
201
+ } as SynapClientOptions & { instanceId?: string };
202
+ }
203
+
204
+ async function shutdownClient(client: SynapClient | null) {
205
+ if (!client) {
206
+ return;
207
+ }
208
+
209
+ try {
210
+ await client.shutdown();
211
+ } catch (error) {
212
+ console.warn("Synap shutdown warning:", error);
213
+ }
214
+ }
215
+
216
+ export async function syncRunMemoryWithSynap(run: RocmRun): Promise<SynapSyncResult> {
217
+ const localContext = buildLocalMemoryContext(run);
218
+ const identity = getSynapIdentity(run);
219
+
220
+ if (!process.env.SYNAP_API_KEY) {
221
+ return {
222
+ status: getLongContextMemoryStatus(
223
+ run.id,
224
+ run.agentMemory.length,
225
+ run.agentMessages.filter((message) => message.memoryRefs.length > 0).length,
226
+ "fallback"
227
+ ),
228
+ promptContext: localContext,
229
+ };
230
+ }
231
+
232
+ let client: SynapClient | null = null;
233
+
234
+ try {
235
+ const { createClient } = await import("@maximem/synap-js-sdk");
236
+ client = createClient(synapOptions());
237
+ await client.init();
238
+
239
+ const messages = buildSynapMessages(run);
240
+
241
+ await client.addMemory({
242
+ userId: identity.userId,
243
+ customerId: identity.customerId,
244
+ conversationId: identity.conversationId,
245
+ sessionId: run.id,
246
+ documentId: run.id,
247
+ documentType: "ai-chat-conversation",
248
+ documentCreatedAt: run.startedAt,
249
+ mode: "long-range",
250
+ metadata: {
251
+ product: "ROCmPilot",
252
+ track: "AI Agents & Agentic Workflows",
253
+ targetLabel: run.target.label,
254
+ targetRepo: run.target.repoUrl,
255
+ scanStatus: run.target.scanStatus,
256
+ agentMessages: run.agentMessages.length,
257
+ sharedMemories: run.agentMemory.length,
258
+ },
259
+ messages,
260
+ });
261
+
262
+ const [contextResult, promptContextResult] = await Promise.allSettled([
263
+ client.fetchUserContext({
264
+ userId: identity.userId,
265
+ customerId: identity.customerId,
266
+ conversationId: identity.conversationId,
267
+ searchQuery: [
268
+ "ROCm migration blockers",
269
+ "CUDA assumptions and AMD validation",
270
+ "agent decisions from prior ROCmPilot runs",
271
+ ],
272
+ maxResults: 8,
273
+ mode: "accurate",
274
+ }),
275
+ client.getContextForPrompt({
276
+ conversationId: identity.conversationId,
277
+ style: "structured",
278
+ }),
279
+ ]);
280
+
281
+ const context = contextResult.status === "fulfilled" ? contextResult.value : null;
282
+ const promptContext =
283
+ promptContextResult.status === "fulfilled" ? promptContextResult.value : null;
284
+ const synapContext = summarizeContext(context, promptContext);
285
+ const recalledItems =
286
+ countContextItems(context) + (promptContext?.recentMessageCount ?? 0);
287
+
288
+ return {
289
+ status: getLongContextMemoryStatus(
290
+ run.id,
291
+ messages.length,
292
+ recalledItems,
293
+ "connected"
294
+ ),
295
+ promptContext: synapContext || localContext,
296
+ };
297
+ } catch (error) {
298
+ console.warn("Synap memory fallback:", error);
299
+
300
+ return {
301
+ status: {
302
+ ...getLongContextMemoryStatus(
303
+ run.id,
304
+ run.agentMemory.length,
305
+ run.agentMessages.filter((message) => message.memoryRefs.length > 0).length,
306
+ "fallback"
307
+ ),
308
+ detail:
309
+ "Synap credentials are present, but the SDK runtime could not complete ingestion. Using local run memory for this report.",
310
+ },
311
+ promptContext: localContext,
312
+ };
313
+ } finally {
314
+ await shutdownClient(client);
315
+ }
316
+ }