Compare commits

..

16 Commits

Author SHA1 Message Date
local
697e9dce23 fix(01-02): auto-match API URL protocol to client load protocol
Client now detects whether it was loaded over HTTP or HTTPS and
selects the corresponding API base URL, so both protocols work
without manual config changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:00:43 +00:00
local
1fde98ca79 feat(01-02): implement health check round-trip with CORS and tutorial comments
- Add shared HealthResponse DTO in ChatAgent.Shared
- Add HealthController API endpoint with CORS policy for localhost:5200
- Add ChatApiClient typed HttpClient wrapper in WASM client
- Update Home.razor to display health check result on load
- Simplify MainLayout to minimal centered layout
- Add global imports for Services and Shared.Models
- Replace app.css with clean Phase 1 light theme styles
- Remove unused OpenAPI package from API project
- All files include tutorial-style inline comments (CODE-01)
2026-03-27 22:58:19 +00:00
local
4ef27598a0 docs(01-01): complete solution scaffold plan
- SUMMARY.md with execution results and metrics
- STATE.md advanced to plan 2 of 2 (50%)
- ROADMAP.md updated with plan progress
- CODE-02 requirement marked complete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:53:47 +00:00
local
c6f1225810 feat(01-01): configure predictable dev ports and API base URL
- Client: https://localhost:5200, http://localhost:5100
- API: https://localhost:7100, http://localhost:7000
- Client wwwroot/appsettings.json with ApiBaseUrl pointing to API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:52:27 +00:00
local
eeaa9de3c5 feat(01-01): create three-project .NET 9 solution scaffold
- ChatAgent.sln at repo root with Client, Api, Shared projects
- Blazor WASM client, ASP.NET Core Web API, shared class library
- Both Client and Api reference Shared project
- Removed template boilerplate (WeatherForecast, Class1)
- Added .NET ignores to .gitignore
- Pinned .NET 9 SDK via global.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:51:55 +00:00
local
b958230216 docs(01): add validation strategy for phase 1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:50:43 +00:00
local
4f1176be19 docs(01-architecture-foundation): create phase plan 2026-03-27 01:48:23 +00:00
local
410150eff9 docs(01): research phase domain 2026-03-27 01:38:56 +00:00
local
ccc9b0da91 docs(state): record phase 1 context session 2026-03-27 01:30:56 +00:00
local
98055dfb04 docs(01): capture phase context 2026-03-27 01:30:44 +00:00
local
7962df1094 docs: create roadmap (5 phases) 2026-03-27 01:21:35 +00:00
local
81bcc49bae docs: define v1 requirements 2026-03-27 01:07:09 +00:00
local
d9878dea73 docs: complete project research 2026-03-27 00:59:24 +00:00
local
b45ae0400e chore: add project config 2026-03-27 00:49:08 +00:00
local
6dbb1085b0 docs: initialize project 2026-03-27 00:43:18 +00:00
local
d963902a28 docs: map existing codebase 2026-03-27 00:21:00 +00:00
56 changed files with 5587 additions and 0 deletions

11
.gitignore vendored
View File

@@ -216,3 +216,14 @@ pyvenv.cfg
.venv .venv
pip-selfcheck.json pip-selfcheck.json
# ---> .NET / C#
bin/
obj/
*.user
*.suo
.vs/
.idea/
*.swp
**/wwwroot/_framework/
*.DotSettings.user

84
.planning/PROJECT.md Normal file
View File

@@ -0,0 +1,84 @@
# Chat Agent WebApp
## What This Is
A personal AI chat web application built with Blazor WebAssembly and the OpenAI GPT API. Users send messages, receive streaming AI responses rendered as markdown, and manage multiple persistent conversations. The project doubles as an incremental learning journey — each phase introduces one concept with well-documented, explained code, making it suitable as a Blazor tutorial for a developer experienced in C# but new to the framework.
## Core Value
A working, well-understood AI chat interface — every line of code is intentional and explained, so the builder learns Blazor patterns while shipping a real product.
## Requirements
### Validated
(None yet — ship to validate)
### Active
- [ ] Send messages to OpenAI GPT and display responses
- [ ] Stream AI responses token-by-token in real time
- [ ] Render AI responses as formatted markdown
- [ ] Create, switch between, and delete multiple conversations
- [ ] Persist conversation history to JSON files across sessions
- [ ] Clean, responsive chat UI with message bubbles and input area
- [ ] Well-commented code with inline explanations of Blazor concepts
- [ ] Incremental build structure — each phase introduces one concept
### Out of Scope
- Authentication/login — single user, no auth needed
- Database (SQL Server, PostgreSQL) — JSON file storage is sufficient for personal use
- Deployment/hosting — local development only for v1
- Multi-user support — personal tool
- OAuth or third-party login — not needed
- LangChain / agentic workflows — deferred to v2 milestone
- RAG (retrieval-augmented generation) — deferred to v2 milestone
- MCP servers — deferred to v2 milestone
## Context
- Builder is experienced in C# but new to Blazor — Blazor-specific patterns (components, lifecycle, dependency injection, SignalR for streaming) need clear inline documentation
- Project serves as both a real tool and a structured learning path
- v2 milestone will layer agentic capabilities: LangChain for workflow orchestration, RAG for document retrieval, and MCP servers for tool integration
- Blazor WebAssembly chosen — runs client-side in the browser, needs a separate backend API for OpenAI calls (API key must not be exposed to client)
- OpenAI GPT is the LLM backend (GPT-4o or latest available)
- JSON file storage on disk — simplest persistence, no database setup
## Constraints
- **Tech stack**: .NET / C# / Blazor WebAssembly — non-negotiable
- **LLM provider**: OpenAI GPT API
- **Storage**: JSON files on local disk
- **Architecture**: WASM client + backend API (API key stays server-side)
- **Code style**: Every Blazor concept introduced must have inline comments explaining what it does and why
## Key Decisions
| Decision | Rationale | Outcome |
|----------|-----------|---------|
| Blazor WebAssembly over Server | User preference — client-side execution | — Pending |
| JSON file storage over SQLite/SQL | Simplest option for single-user personal tool | — Pending |
| OpenAI GPT as sole LLM provider | User preference for v1; multi-provider deferred | — Pending |
| Tutorial-style incremental phases | Project doubles as Blazor learning path | — Pending |
| v1 = chat, v2 = agentic features | Ship working chat first, layer LangChain/RAG/MCP later | — Pending |
## Evolution
This document evolves at phase transitions and milestone boundaries.
**After each phase transition** (via `/gsd:transition`):
1. Requirements invalidated? → Move to Out of Scope with reason
2. Requirements validated? → Move to Validated with phase reference
3. New requirements emerged? → Add to Active
4. Decisions to log? → Add to Key Decisions
5. "What This Is" still accurate? → Update if drifted
**After each milestone** (via `/gsd:complete-milestone`):
1. Full review of all sections
2. Core Value check — still the right priority?
3. Audit Out of Scope — reasons still valid?
4. Update Context with current state
---
*Last updated: 2026-03-27 after initialization*

102
.planning/REQUIREMENTS.md Normal file
View File

@@ -0,0 +1,102 @@
# Requirements: Chat Agent WebApp
**Defined:** 2026-03-27
**Core Value:** A working, well-understood AI chat interface — every line of code is intentional and explained
## v1 Requirements
### Chat Core
- [ ] **CHAT-01**: User can type a message and send it to the OpenAI GPT API
- [ ] **CHAT-02**: User receives the AI response displayed in the chat area
### Conversation Management
- [ ] **CONV-01**: User can create a new conversation
- [ ] **CONV-02**: User can switch between existing conversations
- [ ] **CONV-03**: User can delete a conversation
- [ ] **CONV-04**: Conversations persist to JSON files and survive app restarts
### Message Display
- [ ] **DISP-01**: AI responses render as formatted markdown (headings, lists, bold, links)
- [ ] **DISP-02**: Code blocks in AI responses display with syntax highlighting
- [ ] **DISP-03**: User can copy a message to clipboard
### UI/Layout
- [ ] **UI-01**: App has sidebar with conversation list and main chat area
- [ ] **UI-02**: Chat area has text input with send button
- [ ] **UI-03**: App layout is responsive across screen sizes
### Code Quality
- [ ] **CODE-01**: Every Blazor concept introduced has inline comments explaining what and why
- [x] **CODE-02**: Each phase introduces one concept incrementally (tutorial-style progression)
## v2 Requirements
### Streaming
- **STRM-01**: AI responses stream token-by-token in real time
- **STRM-02**: User can cancel an in-progress AI response
- **STRM-03**: Loading/typing indicator shows during AI generation
### Agentic Workflows
- **AGNT-01**: LangChain integration for workflow orchestration
- **AGNT-02**: RAG pipeline for document retrieval
- **AGNT-03**: MCP server integration for tool use
### Enhanced Features
- **ENH-01**: Auto-generated conversation titles from first message
- **ENH-02**: System prompt configuration per conversation
- **ENH-03**: Model selector (GPT-4o, GPT-4o-mini, etc.)
- **ENH-04**: Dark/light theme toggle
- **ENH-05**: Auto-scroll during message display
- **ENH-06**: Visual distinction between user and AI messages (styled bubbles)
## Out of Scope
| Feature | Reason |
|---------|--------|
| Authentication/login | Single user, no auth needed |
| Database storage | JSON files sufficient for personal use |
| Deployment/hosting | Local development only for v1 |
| Multi-user support | Personal tool |
| OAuth/third-party login | Not needed |
| Voice input/output | Complexity, not core to chat value |
| Image/multimodal input | Defer to v2+ |
| Real-time collaboration | Single user |
| Mobile app | Web-first |
## Traceability
Which phases cover which requirements. Updated during roadmap creation.
| Requirement | Phase | Status |
|-------------|-------|--------|
| CHAT-01 | Phase 3 | Pending |
| CHAT-02 | Phase 3 | Pending |
| CONV-01 | Phase 2 | Pending |
| CONV-02 | Phase 2 | Pending |
| CONV-03 | Phase 2 | Pending |
| CONV-04 | Phase 2 | Pending |
| DISP-01 | Phase 4 | Pending |
| DISP-02 | Phase 4 | Pending |
| DISP-03 | Phase 4 | Pending |
| UI-01 | Phase 5 | Pending |
| UI-02 | Phase 5 | Pending |
| UI-03 | Phase 5 | Pending |
| CODE-01 | Phase 1 | Pending |
| CODE-02 | Phase 1 | Complete |
**Coverage:**
- v1 requirements: 14 total
- Mapped to phases: 14
- Unmapped: 0 ✓
---
*Requirements defined: 2026-03-27*
*Last updated: 2026-03-27 after roadmap creation — all 14 requirements mapped*

94
.planning/ROADMAP.md Normal file
View File

@@ -0,0 +1,94 @@
# Roadmap: Chat Agent WebApp
## Overview
Five phases take this project from an empty solution to a complete personal AI chat application. Each phase introduces one Blazor concept and delivers something verifiable. The dependency chain is strict: the WASM/API architecture must exist before storage, storage before conversations, conversations before AI, AI before display polish, and display before UI completeness. Streaming is deferred to v2. The result is a working, well-understood chat interface where every line of code is intentional and explained.
## Phases
**Phase Numbering:**
- Integer phases (1, 2, 3): Planned milestone work
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)
Decimal phases appear between their surrounding integers in numeric order.
- [ ] **Phase 1: Architecture Foundation** - Two-project solution scaffold with WASM/API split and shared models; establishes the tutorial commenting convention
- [ ] **Phase 2: Conversation Storage** - JSON file persistence and full conversation CRUD via repository pattern and Minimal API endpoints
- [ ] **Phase 3: Basic AI Chat** - End-to-end chat loop (send message → OpenAI → display response) without streaming
- [ ] **Phase 4: Message Display** - Markdown rendering, syntax-highlighted code blocks, and copy-to-clipboard
- [ ] **Phase 5: UI Polish** - Sidebar layout, chat input area, and responsive design across screen sizes
## Phase Details
### Phase 1: Architecture Foundation
**Goal**: The solution structure is locked in and the critical boundaries (no API key in WASM, no file I/O in WASM, no direct OpenAI calls from WASM) are architecturally enforced before any feature code is written
**Depends on**: Nothing (first phase)
**Requirements**: CODE-01, CODE-02
**Success Criteria** (what must be TRUE):
1. Running `dotnet run` on both projects starts WASM client and API server without errors
2. A test HTTP request from the WASM client reaches the API server and returns a response (CORS working)
3. The shared models library is referenced by both projects with no duplication of DTOs
4. Running `dotnet publish` on the WASM project completes with no IL trim warnings
5. Every file introduced contains inline comments explaining the Blazor concept it demonstrates
**Plans**: 2 plans
Plans:
- [x] 01-01-PLAN.md — Solution scaffold: create three projects, wire references, configure ports
- [ ] 01-02-PLAN.md — Health check round-trip: shared DTO, API controller with CORS, typed client, home page
**UI hint**: yes
### Phase 2: Conversation Storage
**Goal**: Users can create, switch between, and delete conversations that persist to disk and survive app restarts — with no AI integration yet
**Depends on**: Phase 1
**Requirements**: CONV-01, CONV-02, CONV-03, CONV-04
**Success Criteria** (what must be TRUE):
1. User can create a new conversation and see it appear in the conversation list
2. User can click a conversation in the list and switch to it (conversation content loads)
3. User can delete a conversation and see it removed from the list
4. After closing and reopening the browser, all conversations and their messages are still present
**Plans**: TBD
### Phase 3: Basic AI Chat
**Goal**: Users can send a message and receive an AI response in the active conversation, with the full request/response cycle working end-to-end
**Depends on**: Phase 2
**Requirements**: CHAT-01, CHAT-02
**Success Criteria** (what must be TRUE):
1. User can type a message, press send, and see their message appear in the chat area
2. User receives an AI response from OpenAI GPT displayed in the chat area
3. The conversation history is preserved across the full exchange and saved to disk
4. The OpenAI API key is never present in any WASM project file or browser network request
**Plans**: TBD
### Phase 4: Message Display
**Goal**: AI responses render as readable, formatted content — not raw markdown strings — with syntax-highlighted code and one-click copy
**Depends on**: Phase 3
**Requirements**: DISP-01, DISP-02, DISP-03
**Success Criteria** (what must be TRUE):
1. An AI response containing markdown (headings, lists, bold, links) renders as formatted HTML in the chat area
2. Code blocks in AI responses display with syntax highlighting appropriate to the language
3. User can click a copy button on any message and the text is copied to the clipboard
**Plans**: TBD
**UI hint**: yes
### Phase 5: UI Polish
**Goal**: The app has a complete, usable layout — sidebar with conversation list, chat input area, and a responsive design that works across screen sizes
**Depends on**: Phase 4
**Requirements**: UI-01, UI-02, UI-03
**Success Criteria** (what must be TRUE):
1. App displays a sidebar on the left with the conversation list and a main chat area on the right
2. The chat area has a text input field and a send button visible at the bottom of the screen
3. The layout reflows gracefully on a narrow viewport (mobile-width browser window) without horizontal scrolling or clipped content
**Plans**: TBD
**UI hint**: yes
## Progress
**Execution Order:**
Phases execute in numeric order: 1 → 2 → 3 → 4 → 5
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Architecture Foundation | 0/2 | Planning complete | - |
| 2. Conversation Storage | 0/TBD | Not started | - |
| 3. Basic AI Chat | 0/TBD | Not started | - |
| 4. Message Display | 0/TBD | Not started | - |
| 5. UI Polish | 0/TBD | Not started | - |

82
.planning/STATE.md Normal file
View File

@@ -0,0 +1,82 @@
---
gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
status: executing
stopped_at: Completed 01-01-PLAN.md
last_updated: "2026-03-27T22:53:36.052Z"
last_activity: 2026-03-27
progress:
total_phases: 5
completed_phases: 0
total_plans: 2
completed_plans: 1
percent: 0
---
# Project State
## Project Reference
See: .planning/PROJECT.md (updated 2026-03-27)
**Core value:** A working, well-understood AI chat interface — every line of code is intentional and explained
**Current focus:** Phase 01 — architecture-foundation
## Current Position
Phase: 01 (architecture-foundation) — EXECUTING
Plan: 2 of 2
Status: Ready to execute
Last activity: 2026-03-27
Progress: [░░░░░░░░░░] 0%
## Performance Metrics
**Velocity:**
- Total plans completed: 0
- Average duration: —
- Total execution time: 0 hours
**By Phase:**
| Phase | Plans | Total | Avg/Plan |
|-------|-------|-------|----------|
| - | - | - | - |
**Recent Trend:**
- Last 5 plans: —
- Trend: —
*Updated after each plan completion*
| Phase 01 P01 | 2min | 2 tasks | 26 files |
## Accumulated Context
### Decisions
Decisions are logged in PROJECT.md Key Decisions table.
Recent decisions affecting current work:
- [Init]: Streaming (STRM-01/02/03) deferred to v2 — user chose non-streaming for v1
- [Init]: Tutorial convention established in Phase 1 — CODE-01/02 anchor there, applied cross-phase
- [Init]: Phase order follows strict dependency chain: scaffold → storage → AI → display → UI polish
- [Phase 01]: Pinned .NET 9 via global.json since machine default is .NET 10
### Pending Todos
None yet.
### Blockers/Concerns
- [Research flag] Phase 4: Verify `Markdown.ColorCode` WASM compatibility before implementing (community-reported, not officially confirmed)
- [Research flag] Phase 4: Confirm Markdig `DisableHtml()` decision — accepted XSS risk for single-user tool; document explicitly in code
## Session Continuity
Last session: 2026-03-27T22:53:36.049Z
Stopped at: Completed 01-01-PLAN.md
Resume file: None

View File

@@ -0,0 +1,170 @@
# Architecture
**Analysis Date:** 2026-03-27
## Pattern Overview
**Overall:** Agent-orchestrated agentic code framework (GSD - Get Shit Done)
**Key Characteristics:**
- Multi-agent cooperative system: 18 specialized agents working in coordinated workflows
- Document-driven state management: All decisions and progress stored as Markdown files
- Workflow-based orchestration: 56 workflows guide agent interactions and user engagement
- Agentic code execution: Plans are executable prompts for Claude model execution
- Extensible per-runtime: Same architecture replicated across 7 IDE/agent providers (.claude, .agent, .gemini, .codex, .cursor, .windsurf, .opencode)
## Layers
**Orchestration Layer:**
- Purpose: Coordinate user commands, route to agents, manage phase/milestone progression
- Location: `.claude/get-shit-done/workflows/`
- Contains: 56 workflow definitions (markdown files), each defining a multi-step process
- Depends on: gsd-tools.cjs CLI for state management, git operations, config parsing
- Used by: All commands (user entry points)
**Agent Layer:**
- Purpose: Specialized reasoning for different task types (planning, execution, research, verification, mapping)
- Location: `.claude/agents/` (18 agents)
- Contains: Agent role definitions, instructions, tool access lists
- Agents: gsd-planner, gsd-executor, gsd-phase-researcher, gsd-verifier, gsd-debugger, gsd-codebase-mapper, gsd-integration-checker, gsd-ui-auditor, gsd-plan-checker, and 9 others
- Depends on: Workflow coordination, tools (Read, Write, Bash, Grep, Glob, WebFetch)
- Used by: Orchestration workflows, invoked via `@agent-name` or Task() calls
**State Management Layer:**
- Purpose: Persistent tracking of project progress, decisions, and configuration
- Location: `.planning/` (global state), phase directories (phase-local state)
- Contains: STATE.md (global), CONTEXT.md (decisions), ROADMAP.md (scope), PLAN.md (task breakdown), SUMMARY.md (execution results), VERIFICATION.md (quality gates)
- Depends on: gsd-tools.cjs for parsing, git for history
- Used by: All agents and orchestration layer
**Tool Layer:**
- Purpose: Provide execution capabilities for agents
- Location: Various (bash, file I/O, git, web)
- Tools: Read, Write, Edit, Bash, Grep, Glob, WebFetch, mcp__context7__*
- Depends on: Agent invocations
- Used by: Agents during execution
**CLI Tool Layer:**
- Purpose: Centralized state/config operations shared across 50+ workflow/agent files
- Location: `.claude/get-shit-done/bin/gsd-tools.cjs` (~1000 lines)
- Contains: 100+ commands for state management, phase operations, validation, progress tracking
- Depends on: Node.js built-ins, git, file system
- Used by: Every workflow and agent (called via bash subprocess)
## Data Flow
**Phase Planning Flow:**
1. User invokes `/gsd:plan-phase PHASE_NUMBER`
2. Orchestrator (plan-phase workflow) loads PHASE state via gsd-tools.cjs
3. Planner agent (gsd-planner) reads CONTEXT.md (user decisions), codebase structure, creates PLAN.md
4. Plan Checker agent validates PLAN.md structure and quality
5. PLAN.md written to phase directory
6. User reviews, optionally discusses via `/gsd:discuss-phase` → updates CONTEXT.md
7. Flow loops if plan changes needed
**Phase Execution Flow:**
1. User invokes `/gsd:execute-phase PHASE_NUMBER`
2. Executor orchestrator loads phase plans (multiple per phase)
3. Plans grouped by wave (dependency-aware parallelization)
4. For each wave:
- Executor agent loads PLAN.md
- Executes tasks sequentially (each task → code change → test verification)
- Commits per-task (atomic commits for rollback safety)
- Writes SUMMARY.md with results
5. Orchestrator collects all SUMMARY.md files
6. Verifier agent checks phase completion against VERIFICATION.md
7. STATE.md updated with completion status
8. Planning docs committed to git (if commit_docs: true)
**Verification Flow:**
1. Phase execution completes, SUMMARY.md produced
2. Verifier agent compares output against VERIFICATION.md (user-defined success criteria)
3. If gaps detected: Trigger `/gsd:plan-phase --gaps` for remedial planning
4. Remedial PLAN.md created with gap-closure tasks
5. Execute → Verify loop continues until no gaps
**State Management Flow:**
1. STATE.md maintains global position: current_phase, current_milestone, workstream, blockers
2. Phase directories contain CONTEXT.md (decisions), PLAN.md (tasks), SUMMARY.md (results), VERIFICATION.md (success gates)
3. ROADMAP.md holds master scope: all phases with descriptions
4. .planning/config.json: branching strategy, model profiles, search behavior
5. Each operation updates relevant state file, committed atomically
## Key Abstractions
**Phase:**
- Purpose: Logical unit of work scoped by user, contains multiple Plans
- Examples: `1`, `1.1`, `1.2` (decimal numbering for sub-phases)
- Pattern: Each phase has directory `.planning/phases/N/` with CONTEXT.md, PLAN.md, SUMMARY.md, VERIFICATION.md
**Plan:**
- Purpose: Executable task breakdown derived from phase scope (typically 2-3 tasks per plan)
- Examples: `PLAN-01.md`, `PLAN-02.md` within a phase directory
- Pattern: PLAN.md contains frontmatter (metadata), objective, context references, tasks array, success criteria
**Task:**
- Purpose: Atomic unit of work within a plan (code change, test, commit)
- Types: `type="auto"` (fully autonomous), `type="checkpoint"` (pause before), `type="review"` (requires human sign-off)
- Pattern: Task has action (what to do), verification criteria, expected artifacts
**Workflow:**
- Purpose: Multi-step process coordinating user input and agent actions
- Examples: `plan-phase.md`, `execute-phase.md`, `discuss-phase.md`
- Pattern: Workflows define steps with conditional branching, agent spawning, state updates
**Agent Profiles:**
- Purpose: Model selection strategy for agent execution (quality vs cost)
- Profiles: `quality` (Opus for all), `balanced` (Opus planning, Sonnet execution), `budget` (minimal Opus)
- Pattern: Resolved via gsd-tools.cjs based on `.planning/config.json` model_profile setting
## Entry Points
**Command Entry Point:**
- Location: `.claude/get-shit-done/workflows/` (any `.md` file)
- Triggers: User invokes `/gsd:WORKFLOW_NAME [args]`
- Responsibilities: Parse arguments, call gsd-tools.cjs init command, route to agents or inline execution
**Agent Entry Point:**
- Location: `.claude/agents/AGENT_NAME.md`
- Triggers: Spawned by orchestrator via `@AGENT_NAME` mention or Task() call
- Responsibilities: Load context, read mandatory files, execute role-specific logic, return results
**Tool Entry Point:**
- Location: `.claude/get-shit-done/bin/gsd-tools.cjs`
- Triggers: `node gsd-tools.cjs <command> [args]`
- Responsibilities: Parse subcommands, perform atomic state/git operations, output JSON
## Error Handling
**Strategy:** Graceful degradation with checkpoint pauses
**Patterns:**
- **Auth Errors:** Executor pauses at checkpoint, waits for user to provide credentials
- **Verification Failures:** Verifier identifies gaps, planner creates remedial PLAN.md, executor runs gap-closure phase
- **Invalid State:** gsd-tools.cjs validate commands detect inconsistencies, suggest repairs
- **Parse Failures:** Frontmatter validation catches schema violations early
- **Git Conflicts:** Executor respects branching strategy, creates feature branches per config
## Cross-Cutting Concerns
**Logging:** Shell output captured from gsd-tools.cjs, bash task execution. Errors logged to console, summary tracked in SUMMARY.md.
**Validation:** gsd-tools.cjs provides `validate` commands for:
- Phase numbering consistency
- Plan structure (required fields, task arrays)
- References (@ paths resolve to existing files)
- Artifacts (must_haves tracked in PLAN.md)
**Authentication:** Config stored in `~/.gsd/` (outside repo), environment variable overrides for CI. Secrets never committed to git.
**Atomicity:** gsd-tools.cjs commit command handles commit_docs check and .gitignore detection. Each task commits separately for rollback safety.
**Determinism:** Model profiles ensure same agent type always gets same model (unless inherit mode). Frontmatter schema validation prevents parsing ambiguity.
---
*Architecture analysis: 2026-03-27*

View File

@@ -0,0 +1,250 @@
# Codebase Concerns
**Analysis Date:** 2026-03-27
## Silent Error Handling (Critical)
**Widespread use of empty catch blocks:**
- Files: `/.claude/hooks/gsd-prompt-guard.js`, `/.claude/hooks/gsd-context-monitor.js`, `/.claude/hooks/gsd-workflow-guard.js`, `/.claude/hooks/gsd-statusline.js`, `/.claude/hooks/gsd-check-update.js`
- Issue: 14+ catch blocks across hooks swallow errors silently with `catch (e) {}` or `catch (e) { // comment }` without logging
- Impact: Failures in hook execution go undetected, making debugging difficult. When files are corrupted, config fails to parse, or file system errors occur, no indication is provided to users or developers
- Current mitigation: Comments explain intent (e.g., "Silent fail -- bridge is best-effort", "don't break statusline on parse errors"), but no logging mechanism exists
- Recommendation: Implement optional debug logging to stderr (only when hooks are invoked in debug mode) without breaking normal operation. Use environment variable like `GSD_HOOK_DEBUG=1` to enable error logging
---
## Fragile Hook Timeout Architecture
**Context-Monitor Hook (`.claude/hooks/gsd-context-monitor.js`)**:
- Issue: 10-second stdin timeout (line 35) is tight for slow systems. On Windows/Git Bash, pipe delays can exceed this
- Files: `/.claude/hooks/gsd-context-monitor.js` (line 35: `setTimeout(() => process.exit(0), 10000)`)
- Current mitigation: Comments reference issues #775, #1162 indicating this was a known problem
- Risk: On systems with slow I/O, the hook exits prematurely without reading input, potentially leaving metrics unprocessed
- Safe modification: Consider detecting platform and adjusting timeout accordingly, or make timeout configurable via environment variable
---
## Hardcoded Buffer Percentage
**Status Line Hook Context Normalization**:
- Issue: 16.5% buffer hardcoded for Claude Code autocompact (line 29)
- Files: `/.claude/hooks/gsd-statusline.js` (line 29: `const AUTO_COMPACT_BUFFER_PCT = 16.5`)
- Impact: If Claude Code changes autocompact behavior or other models have different buffers, context calculations become inaccurate
- Recommendation: Make this configurable via `.planning/config.json` with sensible defaults per model (Claude, Gemini, OpenCode)
---
## Stale Hook Version Tracking
**Hook Version Header Validation**:
- Issue: Version detection depends on comment headers in JavaScript files (line 78 in `gsd-check-update.js`)
- Files: `/.claude/hooks/gsd-check-update.js` (lines 73-92)
- Risk: If hook files are minified, reformatted, or copied incorrectly, version headers can be lost, making stale hook detection fail silently
- Current state: Detects "unknown" version if header is missing, but this doesn't prevent the hook from running with wrong version
- Recommendation: Add a checksum or content hash validation in addition to version headers to detect unintended hook modifications
---
## Process Detachment on Windows
**Check-Update Hook Background Process**:
- Issue: Uses `spawn()` with `detached: true` (line 111 in `gsd-check-update.js`)
- Files: `/.claude/hooks/gsd-check-update.js` (lines 108-114)
- Impact: Background npm process can hang on Windows if not properly detached. Blocks are prevented by `unref()` (line 114), but edge cases remain if process fails
- Current state: `windowsHide: true` flag added, `stdio: 'ignore'` set
- Recommendation: Add timeout to background spawn, return immediately after unref, and consider writing to log file instead of silently ignoring errors
---
## Config File Parsing Without Validation
**Multiple Hooks Read `.planning/config.json`**:
- Issue: Config files are parsed with `JSON.parse()` but no schema validation exists
- Files: `/.claude/hooks/gsd-context-monitor.js` (line 53), `/.claude/hooks/gsd-workflow-guard.js` (line 65)
- Risk: Malformed JSON or missing expected fields cause silent failures. If config structure changes, old configs won't error but silently ignore new fields
- Recommendation: Implement a simple schema validation utility (even lightweight: check for required keys and types) and log warnings when config doesn't match expected shape
---
## Race Conditions in Metrics Bridge
**Context Metrics Between Statusline and Monitor Hooks**:
- Issue: Two separate hooks read/write to `/tmp/claude-ctx-{sessionId}.json` without file locking
- Files: `/.claude/hooks/gsd-statusline.js` (line 40-47 writes), `/.claude/hooks/gsd-context-monitor.js` (line 63-70 reads)
- Risk: On high-frequency tool use, write and read can race, causing monitor hook to read stale metrics or corrupted JSON
- Current mitigation: Both have try-catch around JSON.parse(), stale metrics are detected by timestamp (line 74)
- Recommendation: Use atomic writes (write-to-temp-then-rename pattern) for metrics file, or switch to a simpler shared state mechanism
---
## Missing Logging Infrastructure
**No Structured Logging**:
- Issue: Hooks only log to stdout via process.stdout.write() for hook outputs. No persistent logs for troubleshooting
- Files: All hook files (`/.claude/hooks/*.js`)
- Impact: When hooks fail silently, no audit trail exists to debug issues. Users cannot see what hooks detected or why they exited
- Recommendation: Create optional debug logs in `.planning/hooks-debug.log` (only when env var `GSD_DEBUG=1`), append structured JSON records with timestamp, hook name, and outcome
---
## Unbounded Memory in Warning State File
**Context Monitor Warning Debounce**:
- Issue: Warning state tracked in `/tmp/claude-ctx-{sessionId}-warned.json` accumulates indefinitely
- Files: `/.claude/hooks/gsd-context-monitor.js` (lines 87-117)
- Risk: Temporary directory might not clean this up, causing stale state from previous sessions to affect new sessions
- Current mitigation: State is per-session (uses sessionId), so scope is limited
- Recommendation: Add timestamp to warning state and treat state older than 1 hour as stale (auto-reset)
---
## Regex Pattern Complexity in Prompt Guard
**Prompt Injection Detection Patterns**:
- Issue: 12 regex patterns for injection detection (lines 18-32 in `gsd-prompt-guard.js`) are not anchored and may be fragile
- Files: `/.claude/hooks/gsd-prompt-guard.js` (lines 18-32)
- Risk: Some patterns (e.g., `/you\s+are\s+now\s+/i`) could match benign documentation text about "You are now ready to..." and trigger false warnings
- Impact: While warnings are advisory-only, frequent false positives degrade user experience
- Recommendation: Review patterns for false positive risk, consider context (e.g., only in code blocks, not in comments), or add allowlist for common false positives
---
## Inconsistent Hook Version Across Environments
**Multi-Environment Hook Duplication**:
- Issue: Identical hooks duplicated across `.claude/`, `.agent/`, `.gemini/`, `.opencode/` directories
- Files: 4x each of `gsd-prompt-guard.js`, `gsd-context-monitor.js`, `gsd-workflow-guard.js`, `gsd-statusline.js`, `gsd-check-update.js`
- Risk: If one environment's hooks are updated, others can drift out of sync. Version check in `gsd-check-update.js` detects this (line 81-82) but doesn't auto-fix
- Impact: Users with multiple agent environments may see inconsistent behavior
- Recommendation: Consolidate hooks to a shared location or implement auto-sync mechanism in installer
---
## Resource Leaks in Timeouts
**Stdin Timeout Cleanup**:
- Issue: `setTimeout()` created for stdin timeout in all hooks, relies on `clearTimeout()` to clean up
- Files: `/.claude/hooks/gsd-prompt-guard.js`, `/.claude/hooks/gsd-context-monitor.js`, `/.claude/hooks/gsd-workflow-guard.js`, `/.claude/hooks/gsd-statusline.js`
- Risk: If process exits before reaching `process.stdin.on('end')`, timeout continues running until expiry (3-10 seconds)
- Current state: Process exits on timeout with code 0, but spawned processes inherit this timeout
- Recommendation: Add `unref()` call on timeout timers so they don't keep process alive
---
## Platform-Specific Path Separator
**Windows Path Handling in Guards**:
- Issue: Guards check for both `/` and `\\` in file paths (e.g., line 52 in `gsd-prompt-guard.js`)
- Files: `/.claude/hooks/gsd-prompt-guard.js` (line 52), `/.claude/hooks/gsd-workflow-guard.js` (line 43)
- Risk: Mixing separators when checking `.planning/` paths may not catch all variations on Windows (e.g., mixed slashes)
- Recommendation: Normalize paths using `path.normalize()` before comparison, or use `path.sep` for platform detection
---
## Missing Config Directory Override Documentation
**CLAUDE_CONFIG_DIR Environment Variable**:
- Issue: Support for `CLAUDE_CONFIG_DIR` environment variable is implemented (line 18 in `gsd-check-update.js`, line 73 in `gsd-statusline.js`) but not documented
- Files: `/.claude/hooks/gsd-check-update.js` (line 18), `/.claude/hooks/gsd-statusline.js` (line 73)
- Impact: Users with custom config directories won't know this variable exists, potentially breaking hook functionality
- Recommendation: Document in copilot-instructions.md or add reference in hook comments
---
## Temporary File Cleanup Strategy
**Metric Files Never Cleaned**:
- Issue: `/tmp/claude-ctx-{sessionId}.json` and `/tmp/claude-ctx-{sessionId}-warned.json` are never deleted
- Files: `/.claude/hooks/gsd-statusline.js` (writes), `/.claude/hooks/gsd-context-monitor.js` (reads)
- Risk: On long-running systems, /tmp fills up with stale session files
- Recommendation: Implement cleanup logic to delete temp files older than 7 days, or hook into session cleanup to delete files on logout
---
## Package.json Missing Dependencies
**Minimal Configuration**:
- Issue: `.claude/package.json` only contains `{"type":"commonjs"}`, no dependencies listed
- Files: `/.claude/package.json`
- Risk: Hooks use built-in Node modules (fs, path, os, child_process), so this is correct, but appears incomplete
- Current state: This is intentional (all hooks use only Node builtins), but unclear to maintainers
- Recommendation: Add comment in package.json explaining that hooks intentionally use no external dependencies for portability
---
## Potential Memory Bloat in Large Outputs
**No Stream Processing in Hooks**:
- Issue: All hooks read entire stdin into memory before processing (`let input = ''; process.stdin.on('data', chunk => input += chunk)`)
- Files: All hook files
- Risk: If a tool call writes gigabytes of content (e.g., massive file read), the hook process could run out of memory
- Current mitigation: Only tool writes/edits are inspected, and these are typically small
- Recommendation: For hooks processing large content (especially prompt injection guard), implement streaming regex matching or line-by-line processing
---
## Test Coverage Gaps
**No Unit Tests for Hooks**:
- What's not tested: None of the 5 hook files have automated tests
- Files: `/.claude/hooks/*.js` (all of them)
- Risk: Changes to hook logic have no regression protection. Timeout tweaks, path logic, or regex patterns could break without detection
- Impact: Hook failures are silent (by design), so broken hooks go unnoticed until users report issues
- Priority: High — hooks are critical path for agent context management
---
## Missing Error Recovery in Executor
**Deviation Rule Priority Conflicts**:
- Issue: Executor auto-applies Rules 1-3 without clear priority when multiple rules could apply
- Files: `/.github/agents/gsd-executor.agent.md` (line 150 mentions "RULE PRIORITY:")
- Risk: When a task has multiple issues (e.g., bug AND missing critical functionality), executor might fix bug first, missing that missing-function blocking task completion
- Recommendation: Clarify priority order: blocking issues (Rule 3) > missing critical functionality (Rule 2) > bugs (Rule 1)
---
## Subprocess Error Propagation in Background Checks
**npm view Command Failure Silent**:
- Issue: `execSync('npm view get-shit-done-cc version')` wrapped in try-catch, timeout 10s (line 96 in `gsd-check-update.js`)
- Files: `/.claude/hooks/gsd-check-update.js` (lines 95-97)
- Risk: If npm is not installed, offline, or slow, fails silently. Users won't know update check failed
- Impact: Latest version is set to "unknown", update check becomes unreliable
- Recommendation: Log to file when update check fails (optional debug log), or retry with exponential backoff
---
## Missing Instrumentation for Decision Points
**No Audit Trail for Hook Decisions**:
- Issue: When context monitor injects critical warning or workflow guard injects advisory, no record of decision
- Files: `/.claude/hooks/gsd-context-monitor.js` (lines 125-142), `/.claude/hooks/gsd-workflow-guard.js` (lines 78-87)
- Risk: If user behavior doesn't match warning, no way to audit what warning was shown or when
- Recommendation: Write decision logs to `.planning/hooks-audit.log` with timestamp, hook name, decision, and reason (only in GSD active mode)
---
## Scalability of Hook Frequency
**Hooks Run on Every Tool Use**:
- Issue: Context monitor (PostToolUse) runs after EVERY tool call, reads file, parses JSON, checks thresholds
- Files: `/.claude/hooks/gsd-context-monitor.js`
- Risk: On high-frequency tool sessions (100+ tools), accumulated overhead could impact performance
- Current mitigation: Debounce warnings (line 108), stale metric detection (line 74)
- Recommendation: Profile hook execution time under load, consider batching metrics or reducing check frequency
---
## Unclear Behavior on Missing Files
**Graceful Degradation Assumptions**:
- Issue: When `.planning/STATE.md`, `.planning/config.json`, or todo files don't exist, hooks silently skip functionality
- Files: All hooks that check for optional files
- Risk: Unclear to users whether missing files are "normal" or indicate misconfiguration
- Example: Statusline shows "no task" if todos not found, but user might think todos are lost
- Recommendation: Distinguish between "feature not enabled" (e.g., no .planning/ = not a GSD project) vs. "feature broken" (e.g., config.json exists but unparseable)
---
*Concerns audit: 2026-03-27*

View File

@@ -0,0 +1,179 @@
# Coding Conventions
**Analysis Date:** 2026-03-27
## Language & Runtime
**Primary Language:** JavaScript (CommonJS)
- Node.js environment (no TypeScript transpilation)
- All hooks use `.js` extension with shebang: `#!/usr/bin/env node`
**Module System:**
- CommonJS (`require()` only, no ES6 imports)
- Node.js built-in modules: `fs`, `path`, `os`, `child_process`
- No external npm dependencies in hook files
## Naming Patterns
**Files:**
- Kebab-case: `gsd-hook-name.js`
- Pattern: `gsd-` prefix + feature name + `.js` extension
- Examples: `gsd-prompt-guard.js`, `gsd-context-monitor.js`, `gsd-statusline.js`
**Functions:**
- camelCase: `detectConfigDir()`, `clearTimeout()`, `writeFileSync()`
- Single-letter shorthand acceptable for simple operations: `f` for file in loops
**Constants:**
- UPPER_SNAKE_CASE for immutable values: `WARNING_THRESHOLD`, `CRITICAL_THRESHOLD`, `STALE_SECONDS`, `DEBOUNCE_CALLS`
- Inline comments after constants for clarity: `const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%`
**Variables:**
- camelCase: `input`, `stdinTimeout`, `data`, `filePath`, `findings`, `configPath`
- Descriptive names: `remaining_percentage`, `callsSinceWarn`, `hookEventName`
- Single-letter loop variables: `f`, `e` (for errors), `p` (for patterns)
**Types/Objects:**
- Object keys use snake_case: `tool_name`, `tool_input`, `file_path`, `hook_version`, `installed_version`
- Array items plural: `staleHooks`, `findings`, `allowedPatterns`, `files`, `hookFiles`
## Code Style
**Formatting:**
- No linter configured (no eslintrc, prettier, or biome config files found)
- Line length: typically 80-120 characters, no strict enforced limit observed
- Indentation: 2 spaces consistently across all files
**Comments:**
- Single-line comments: `// comment`
- Multi-line blocks: Multiple `//` lines stacked (no `/* */` blocks found)
- Header comments with metadata: Version, purpose, triggers, behavior
- Pattern: `// gsd-hook-version: X.Y.Z` as first comment after shebang
- Purpose explanation follows immediately
**String Formatting:**
- Single quotes for regular strings: `'utf8'`, `'end'`
- Template literals for interpolation: `` `${variable}` ``
- Backticks for paths and code examples in comments: `` `${filePath}` ``
## Error Handling
**Try-Catch Pattern:**
- All file I/O and JSON parsing wrapped in try-catch
- Silent failures preferred: catch blocks often empty or call `process.exit(0)`
- Examples from codebase:
- `try { const data = JSON.parse(input); } catch { process.exit(0); }`
- `try { ... } catch (e) { // Silent fail on parse errors }`
**Graceful Degradation:**
- Never block operations with errors
- Timeout guards prevent hanging: `const stdinTimeout = setTimeout(() => process.exit(0), 3000);`
- File existence checks before operations: `if (fs.existsSync(configPath)) { ... }`
- Return early on missing critical data: `if (!sessionId) { process.exit(0); }`
**Error Recovery:**
- Corrupted files reset to defaults: `catch (e) { warnData = { callsSinceWarn: 0, lastLevel: null }; }`
- Optional chain operators for safe access: `data.tool_input?.file_path || ''`
## Type Coercion & Checks
**Null/Undefined Handling:**
- Null coalescing default values: `data.session_id || ''`, `data.cwd || process.cwd()`
- Explicit null checks: `if (remaining != null)` (distinguishes null from undefined)
- Undefined/null fallbacks: `|| 'unknown'`, `|| []`, `|| {}`
**Truthiness Checks:**
- Explicit boolean comparisons: `if (config.hooks?.workflow_guard) { ... }`
- Array length checks: `if (files.length > 0) { ... }`, `if (findings.length === 0) { ... }`
**Number Conversions:**
- Explicit parsing: `Math.floor(Date.now() / 1000)`, `Math.round(100 - usableRemaining)`
- Clamping with Math: `Math.max(0, value)`, `Math.min(100, value)`
## Imports & Requires
**Order (when present):**
1. Node.js built-in modules: `fs`, `path`, `os`, `child_process`
2. Destructured imports grouped: `const { spawn } = require('child_process');`
3. No external package requires in hooks
**Require Pattern:**
```javascript
const fs = require('fs');
const path = require('path');
const os = require('os');
const { spawn } = require('child_process');
```
## Object & Array Patterns
**Object Creation:**
- Literal syntax: `{ hookEventName: 'PreToolUse', additionalContext: message }`
- Computed properties from template literals: `` { [key]: value } `` (not observed, uses direct literals)
- Spread operators: Not used in codebase
**Array Operations:**
- `.filter()`: `files.filter(f => f.startsWith(session))`
- `.map()`: `files.map(f => ({ name: f, mtime: fs.statSync(...) }))`
- `.find()`: `todos.find(t => t.status === 'in_progress')`
- `.some()`: `allowedPatterns.some(p => p.test(filePath))`
- Arrow functions for callbacks: `chunk => input += chunk`
**Sorting & Ordering:**
- Reverse chronological: `files.sort((a, b) => b.mtime - a.mtime)`
- File system operations ordered first, then filtering
## Output Patterns
**JSON Output:**
- All hook output is JSON: `process.stdout.write(JSON.stringify(output));`
- Output structure includes `hookSpecificOutput` wrapper:
```javascript
const output = {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
additionalContext: message
}
};
```
**Process Exit:**
- Success: `process.exit(0)` or implicit exit after writing output
- Error: `process.exit(0)` (never `process.exit(1)` to avoid breaking workflows)
- Cleanup: `clearTimeout()` called before operations
## File I/O Patterns
**Synchronous Operations:**
- `fs.readFileSync(path, 'utf8')` for reads
- `fs.writeFileSync(path, JSON.stringify(data))` for writes
- `fs.existsSync(path)` for checks
- `fs.readdirSync(path)` for directory listing
- `fs.statSync(path).mtime` for file metadata
**Path Operations:**
- `path.join()` for path concatenation: `path.join(cwd, '.planning', 'config.json')`
- `path.basename(dir)` for extracting final component
- `path.dirname()` for parent directory
## Regular Expressions
**Pattern Definition:**
- Captured in constants array at file top: `const INJECTION_PATTERNS = [ /pattern1/i, /pattern2/i ]`
- Case-insensitive flag commonly used: `/pattern/i`
- Multiline patterns use raw strings: `/[\u200B-\u200F]/` for Unicode detection
**Pattern Testing:**
- `.test()` method for boolean check: `if (pattern.test(content))`
- `.match()` for capture groups: `const versionMatch = content.match(/\\/\\/ gsd-hook-version:\\s*(.+)/)`
- `.source` property for pattern string: `findings.push(pattern.source)`
## No Additional Frameworks
**Testing:** Not detected - no test files exist
**Build Tools:** Not detected - runs directly as Node.js scripts
**Linting/Formatting:** Not detected - no .eslintrc, .prettierrc, or similar configs
---
*Convention analysis: 2026-03-27*

View File

@@ -0,0 +1,114 @@
# External Integrations
**Analysis Date:** 2026-03-27
## APIs & External Services
**Web Search:**
- Brave Search - Web search integration for research and discovery phases
- SDK/Client: Native `fetch` API (Node.js built-in)
- Auth: `BRAVE_API_KEY` environment variable or `~/.gsd/brave_api_key` file
- Endpoint: `https://api.search.brave.com/res/v1/web/search`
- Implementation: `cmdWebSearch()` in `/home/ys/family-repo/AgenticCode/.claude/get-shit-done/bin/lib/commands.cjs`
**Web Scraping (Optional):**
- Firecrawl - Web content extraction
- Auth: `FIRECRAWL_API_KEY` environment variable or `~/.gsd/firecrawl_api_key` file
- Status: Configuration detected, not actively used in codebase
**Search Alternative (Optional):**
- Exa Search - Alternative search API
- Auth: `~/.gsd/exa_api_key` file
- Status: Configuration detected, not actively used in codebase
## Data Storage
**Repositories:**
- Git - Primary version control system for all planning documents
- Client: Git CLI via `child_process.execSync()` and `execFileSync()`
- Operations: Commit, status, log, diff, tag management
- Integration points: `execGit()` in `core.cjs`, phase operations in `phase.cjs`
**File Storage:**
- Local filesystem only
- `.planning/` directory tree - All state, roadmaps, phases, requirements
- `~/.gsd/` directory - User-level API key storage
- Multi-workspace support via `.planning/config.json` sub_repos configuration
**Caching:**
- None detected
## Authentication & Identity
**Auth Provider:**
- Custom/API Key based
- No centralized identity provider
- Individual API keys per external service stored in files or environment variables
- Git authentication via system SSH/credentials configured externally
## Monitoring & Observability
**Error Tracking:**
- None detected - errors handled via console output
**Logs:**
- Console output via `output()` and `error()` functions in `/home/ys/family-repo/AgenticCode/.claude/get-shit-done/bin/lib/core.cjs`
- Support for structured JSON output via `--raw` flag
- Context monitoring hooks in `.claude/hooks/gsd-context-monitor.js`
## CI/CD & Deployment
**Hosting:**
- Self-hosted Node.js CLI
- IDE integration via Claude, Gemini, Agent, Cursor, OpenCode, Windsurf editors
- Editor-specific hooks and commands in `./.[editor]/.claude/hooks/`
**CI Pipeline:**
- Git hooks integration via Claude settings
- Post-tool-use monitoring: `gsd-context-monitor.js`
- Pre-write guards: `gsd-prompt-guard.js`
- Update checking: `gsd-check-update.js`
## Environment Configuration
**Required env vars:**
- `HOME` - User home directory (for API key storage and config)
- `BRAVE_API_KEY` - (optional) Brave Search API key for web search functionality
**Optional env vars:**
- `FIRECRAWL_API_KEY` - Web scraping capability
- `EXA_API_KEY` - Alternative search provider
**Secrets location:**
- Environment variables: `.env` or shell environment
- File-based: `~/.gsd/brave_api_key`, `~/.gsd/firecrawl_api_key`, `~/.gsd/exa_api_key`
- Git-ignored configuration: Not applicable (no local .env files in codebase)
## Webhooks & Callbacks
**Incoming:**
- None detected
**Outgoing:**
- Git commits via `execGit()` - Triggers CI/CD if configured
- Status updates via stdio - Display progress to Claude/editor interface
## Git Integration
**Operations Supported:**
- `git status` - Check working tree state
- `git add` - Stage files
- `git commit` - Create planning commits with auto-generated messages
- `git log` - Query commit history
- `git diff` - Compare changes
- `git tag` - Version marking for phases and milestones
- Commit routing for multi-repo workspaces
**Key Implementation:**
- Location: `execGit()` in `/home/ys/family-repo/AgenticCode/.claude/get-shit-done/bin/lib/core.cjs`
- Supports sub-repo commit routing via `commit-to-subrepo` command
- Automatic message generation with structured frontmatter
---
*Integration audit: 2026-03-27*

View File

@@ -0,0 +1,76 @@
# Technology Stack
**Analysis Date:** 2026-03-27
## Languages
**Primary:**
- JavaScript (Node.js) - Core framework and tooling
- CommonJS (`.cjs`) - Module format for all library files
- Markdown - Configuration, documentation, and state files
## Runtime
**Environment:**
- Node.js (version not explicitly specified, uses `#!/usr/bin/env node` shebang)
**Package Manager:**
- npm (inferred from package.json presence)
- Lockfile: Not detected in codebase
## Frameworks
**Core:**
- Get Shit Done (GSD) Framework v1.29.0 - Agentic code orchestration and planning
- Native Node.js APIs - `child_process`, `fs`, `path`
**Search & Web:**
- Brave Search API - Web search integration via HTTP fetch
**Build/Dev:**
- Node.js built-in utilities - No external build tool detected
## Key Dependencies
**Critical:**
- `child_process` - Subprocess execution for git commands and CLI operations
- `fs` - File system operations for planning document management
- `path` - Cross-platform path handling
- `os` - Environment detection (home directory, temp directories)
**Infrastructure:**
- `fetch` API (native Node.js) - HTTP requests for Brave Search API
## Configuration
**Environment:**
- Stored in `~/.gsd/` directory with separate API key files
- Environment variable overrides: `BRAVE_API_KEY`, `FIRECRAWL_API_KEY`, `EXA_API_KEY`
**Key Configuration Files:**
- `.planning/config.json` - Project-wide configuration and feature flags
- `settings.json` - Agent-specific settings (per `./.agent/`, `./.claude/`, `./.gemini/`, etc.)
- `.gsd-file-manifest.json` - File integrity tracking with SHA256 hashes
**Configuration Features:**
- Multi-repository workspace support
- Model profile resolution
- Milestone and workstream management
- Phase numbering strategies (decimal or custom)
## Platform Requirements
**Development:**
- Node.js with built-in ES2020+ support
- Git (for repository operations and commits)
- Bash/Shell environment for command execution
- File system with support for symlinks and deep directory structures
**Production:**
- Self-contained Node.js CLI tool
- No external runtime required beyond Node.js
- Execution via `node gsd-tools.cjs` or direct command invocation
---
*Stack analysis: 2026-03-27*

View File

@@ -0,0 +1,217 @@
# Codebase Structure
**Analysis Date:** 2026-03-27
## Directory Layout
```
/home/ys/family-repo/AgenticCode/
├── .agent/ # Anthropic Agent-specific GSD setup
│ ├── agents/ # Agent definitions (symlinks or copies)
│ ├── get-shit-done/ # Workflows, templates, references
│ ├── hooks/ # Post-tool integration hooks
│ ├── skills/ # Project skills (agent instructions)
│ └── package.json # CommonJS marker
├── .claude/ # Claude Code / Claude Web setup
│ ├── agents/ # 18 agent definition files (master)
│ ├── get-shit-done/ # Master workflows, references, commands
│ │ ├── bin/ # gsd-tools.cjs CLI (~1000 lines)
│ │ ├── commands/gsd/ # Command metadata (workstreams.md)
│ │ ├── references/ # Documentation and config specs
│ │ ├── templates/ # Scaffold templates for PLAN.md, SUMMARY.md
│ │ └── workflows/ # 56 orchestration workflows
│ ├── commands/gsd/ # Claude Code command overrides
│ ├── hooks/ # Post-tool hooks for linting/formatting
│ ├── package.json # CommonJS marker
│ └── settings.json # Per-runtime settings
├── .codex/ # Codeium Codex setup (mirrors .claude/)
├── .cursor/ # Cursor IDE setup (mirrors .claude/)
├── .gemini/ # Google Gemini setup (mirrors .claude/)
├── .opencode/ # OpenCode setup (mirrors .claude/)
├── .windsurf/ # Codeium Windsurf setup (mirrors .claude/)
├── .github/ # GitHub integration
│ ├── gsd-file-manifest.json # File integrity checksums
│ └── get-shit-done/ # GitHub action templates
├── .planning/ # Project state and planning documents
│ ├── codebase/ # Analysis documents (STACK.md, ARCHITECTURE.md, etc.)
│ ├── config.json # Project configuration (branching, model profiles)
│ ├── STATE.md # Global project state (current phase, milestone, blockers)
│ ├── ROADMAP.md # Full scope and phase definitions
│ ├── REQUIREMENTS.md # Traceability for feature requirements
│ └── phases/ # Phase execution directories
│ ├── 1/ # Phase 1
│ │ ├── CONTEXT.md # User decisions for this phase
│ │ ├── PLAN-01.md # Task breakdown (plan 1 of N)
│ │ ├── PLAN-02.md # Task breakdown (plan 2 of N)
│ │ ├── SUMMARY-01.md # Execution results (for PLAN-01)
│ │ ├── VERIFICATION.md # Success criteria and quality gates
│ │ └── WAITING.json # Optional: pause signal for checkpoints
│ ├── 1.1/ # Sub-phase 1.1
│ └── 2/ # Phase 2
├── .git/ # Git repository
├── .gitignore # Standard: excludes node_modules, .env, .planning (optional)
├── README.md # Project descriptor
└── .gsd-file-manifest.json # Root-level file integrity tracking
```
## Directory Purposes
**`.claude/` (Master Directory):**
- Purpose: Contains the canonical GSD system — agents, workflows, CLI tool, references
- Contains: Agent role definitions (18 files), 56 workflows, gsd-tools.cjs, documentation
- Key files: `agents/gsd-planner.md`, `agents/gsd-executor.md`, `agents/gsd-codebase-mapper.md`, `get-shit-done/bin/gsd-tools.cjs`
**`.claude/agents/`:**
- Purpose: Agent role definitions — instructions, tool access, execution patterns
- Contains: 18 agent files (one per agent type)
- File pattern: `gsd-{role-name}.md` (e.g., `gsd-planner.md`)
- Size: Large files (10KB-45KB), front-loaded with role definition, process steps
**`.claude/get-shit-done/workflows/`:**
- Purpose: Orchestration logic — multi-step processes coordinating agents and user input
- Contains: 56 markdown workflow files
- Naming: workflow names map to commands (e.g., `execute-phase.md``/gsd:execute-phase`)
- Pattern: Each workflow defines steps with conditional branching, gsd-tools.cjs calls, agent spawning
**`.claude/get-shit-done/bin/`:**
- Purpose: CLI tool providing centralized state/git operations
- Contains: `gsd-tools.cjs` (main tool, ~1000 lines), `lib/` (supporting utilities)
- Commands: 100+ subcommands for state, phases, roadmaps, validation, commits
- Called from: Every workflow and agent via `node gsd-tools.cjs <command>`
**`.claude/get-shit-done/references/`:**
- Purpose: Documentation and configuration specifications
- Contains: 15 reference files (model-profiles.md, planning-config.md, checkpoints.md, etc.)
- Usage: Read by agents to understand patterns, by implementers to understand system
**`.claude/get-shit-done/templates/`:**
- Purpose: Scaffold templates for plan and summary generation
- Contains: PLAN.md template, SUMMARY.md template, CONTEXT.md template
- Usage: Developers fill templates when creating new phases
**`.agent/`, `.gemini/`, `.codex/`, `.cursor/`, `.windsurf/`, `.opencode/`:**
- Purpose: Per-IDE setup directories (mirrors of `.claude/`)
- Contains: Symlinks or copies of agents/, workflows/, hooks/, settings.json
- Why separate: Each IDE has own credential storage, hook integration points, settings
**`.planning/`:**
- Purpose: Global project state and phase-local planning documents
- Contains: STATE.md (global), ROADMAP.md (full scope), config.json, phases/ directory tree
- Key files: `STATE.md` (current position), `ROADMAP.md` (phase definitions), `config.json` (branching strategy, model profiles)
**`.planning/phases/N/`:**
- Purpose: Per-phase execution directory — holds decisions, plans, summaries
- Contains: CONTEXT.md (user decisions), PLAN-*.md (task breakdowns), SUMMARY-*.md (results), VERIFICATION.md (success gates)
- Naming: Phase directories use number (1, 1.1, 1.2, 2) corresponding to ROADMAP.md phases
## Key File Locations
**Entry Points:**
- `.claude/get-shit-done/workflows/*.md` - User command entry points
- `.claude/agents/*.md` - Agent entry points (invoked by workflows)
- `.claude/get-shit-done/bin/gsd-tools.cjs` - CLI tool entry point
**Configuration:**
- `.planning/config.json` - Project-wide configuration (branching, model profiles, search behavior)
- `.claude/settings.json` - Per-runtime agent settings (model overrides, parallelization)
- `~/.gsd/defaults.json` - User-global GSD defaults (read-only in repo)
**Core Logic:**
- `.claude/agents/gsd-planner.md` - Phase planning logic (45KB, ~700 lines)
- `.claude/agents/gsd-executor.md` - Plan execution logic (21KB, ~450 lines)
- `.claude/agents/gsd-codebase-mapper.md` - Project analysis for architecture/tech docs
- `.claude/get-shit-done/bin/gsd-tools.cjs` - Centralized state/git operations
**State & Decisions:**
- `.planning/STATE.md` - Global state (current phase, milestone, workstream, blockers)
- `.planning/ROADMAP.md` - Master scope (phase numbers, descriptions, order)
- `.planning/phases/N/CONTEXT.md` - User decisions for phase N
- `.planning/phases/N/PLAN-*.md` - Task breakdowns (executable prompts)
**Execution & Verification:**
- `.planning/phases/N/SUMMARY-*.md` - Execution results, commit hashes, artifacts
- `.planning/phases/N/VERIFICATION.md` - Success criteria, quality gates, test requirements
- `.planning/REQUIREMENTS.md` - Requirement IDs (REQ-01, etc.) for traceability
**Documentation & References:**
- `.claude/get-shit-done/references/model-profiles.md` - Agent model selection strategies
- `.claude/get-shit-done/references/planning-config.md` - Configuration options spec
- `.claude/get-shit-done/references/checkpoints.md` - Pause point definition and usage
## Naming Conventions
**Files:**
- Workflows: kebab-case (e.g., `execute-phase.md`, `plan-phase.md`)
- Agents: kebab-case with gsd- prefix (e.g., `gsd-planner.md`, `gsd-executor.md`)
- Phase documents: UPPERCASE with phase number (e.g., `PLAN-01.md`, `SUMMARY-02.md`)
- Config files: lowercase (e.g., `config.json`, `settings.json`)
- State files: UPPERCASE (e.g., `STATE.md`, `ROADMAP.md`)
**Directories:**
- Hidden IDE-specific: dot-prefixed (`.claude`, `.agent`, `.gemini`)
- Planning: `.planning` with phase structure (phases/1/, phases/1.1/, etc.)
- Tools/resources: lowercase (agents, workflows, bin, lib, references, templates)
**Phase Numbering:**
- Integer phases: 1, 2, 3 (major phases)
- Decimal sub-phases: 1.1, 1.2, 1.3 (sub-divisions of phase 1)
- Directory structure mirrors numbering: `.planning/phases/1/`, `.planning/phases/1.1/`
## Where to Add New Code
**New Agent:**
1. Create ``.claude/agents/gsd-{agent-name}.md`` with role definition, process steps, tools
2. Size: Keep under 50KB (agents are consumed whole as context)
3. Register: List in agent registry within workflow files and orchestrator
4. Mirror: Copy to `.agent/agents/`, `.gemini/agents/`, etc. after validation
**New Workflow:**
1. Create `.claude/get-shit-done/workflows/{workflow-name}.md`
2. Define: `<purpose>`, `<process>` with numbered steps, subprocess calls to gsd-tools.cjs
3. Commands: If user-facing, add metadata entry in `.claude/commands/gsd/`
4. Routing: Document how workflow invokes agents (inline vs spawned via Task())
**New Reference Documentation:**
1. Create `.claude/get-shit-done/references/{topic}.md`
2. Purpose: Explain system patterns, configuration options, validation rules
3. Read by: Agents when implementing features, developers understanding system
4. Update: Link from agent files or workflow documentation
**New Phase (during project execution):**
1. Create phase directory: `.planning/phases/{N}/` (where N is next phase number)
2. Create CONTEXT.md (template in references/)
3. Planner creates PLAN-*.md files
4. Add phase to ROADMAP.md with description
5. Update STATE.md current_phase field
**Phase Documents (during planning):**
- PLAN.md: Created by gsd-planner agent (follows template in templates/)
- VERIFICATION.md: User creates to define success criteria (template available)
- CONTEXT.md: Created by gsd-discuss-phase agent from user decisions
## Special Directories
**`.planning/codebase/`:**
- Purpose: Analysis documents for executor reference (STACK.md, ARCHITECTURE.md, CONVENTIONS.md, TESTING.md, CONCERNS.md)
- Generated: By gsd-codebase-mapper agent via `/gsd:map-codebase`
- Committed: Yes, documents tracked in git for future reference
**`.planning/phases/N/`:**
- Purpose: Execution workspace for phase N (isolated state)
- Generated: Directories created by gsd-tools.cjs during phase setup
- Committed: Yes, all planning docs committed (unless .planning/ in .gitignore)
**`.claude/get-shit-done/bin/lib/`:**
- Purpose: Supporting utilities for gsd-tools.cjs
- Contents: Shared functions for JSON parsing, git operations, file I/O
- Generated: No, part of GSD system
- Committed: Yes, part of codebase
**`node_modules/`:**
- Purpose: NPM dependencies (if any)
- Generated: Yes, by `npm install`
- Committed: No, excluded via .gitignore
---
*Structure analysis: 2026-03-27*

View File

@@ -0,0 +1,209 @@
# Testing Patterns
**Analysis Date:** 2026-03-27
## Test Framework
**Status:** Not detected
- No test files (*.test.js, *.spec.js) found in codebase
- No test runner configured (no jest.config.js, vitest.config.js, or mocha configuration)
- No test dependencies listed in package.json files
**Code Context:**
- The codebase consists of 5 Node.js hook scripts (579 total lines across `.claude/hooks/`)
- Each hook is a standalone CLI tool that reads JSON from stdin and outputs JSON to stdout
- Hooks are event-driven (SessionStart, PreToolUse, PostToolUse, AfterTool lifecycle events)
- No application code beyond these hooks exists in the repository
## Script Type & Testing Approach
**Current Architecture:**
Each hook file (`gsd-*.js` in `/home/ys/family-repo/AgenticCode/.claude/hooks/`) follows the same structural pattern:
- Shebang: `#!/usr/bin/env node`
- Node.js built-in modules only (fs, path, os, child_process)
- JSON stdin → processing → JSON stdout
- Silent failure on errors (timeout guards, try-catch with exit(0))
**Hook Files:**
- `gsd-prompt-guard.js` (96 lines) - Detects prompt injection patterns in written files
- `gsd-statusline.js` (119 lines) - Renders context usage and task status
- `gsd-context-monitor.js` (156 lines) - Warns when context window is low
- `gsd-workflow-guard.js` (94 lines) - Advises on workflow compliance
- `gsd-check-update.js` (114 lines) - Checks for GSD updates in background
## Manual Testing Indicators
**Integration Points (evidence of real-world testing):**
- Comments referencing specific issues: `See #775`, `See #1162`, `See #870`, `See #884`
- Platform-specific workarounds: `windowsHide: true` for child_process to prevent console flash on Windows
- Timeout guards: `const stdinTimeout = setTimeout(() => process.exit(0), 3000);` prevents hanging on pipe issues
- Git Bash compatibility: explicit handling of stdin timeout on Windows Git Bash
**Behavioral Validation Patterns:**
- Config file validation: reads `.planning/config.json`, catches parse errors gracefully
- File existence checks before operations: `if (fs.existsSync(filePath))`
- Stale data detection: `if ((now - metrics.timestamp) > STALE_SECONDS)`
- Severity escalation tracking: debounce counter resets on warning level change
## Input Validation
**JSON Parsing with Error Handling:**
All hooks follow this pattern (example from `gsd-prompt-guard.js`):
```javascript
let input = '';
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
clearTimeout(stdinTimeout);
try {
const data = JSON.parse(input);
// Process data
} catch {
// Silent fail — never block tool execution
process.exit(0);
}
});
```
**Defensive Checks:**
- Field existence: `const toolName = data.tool_name;` then `if (toolName !== 'Write' && toolName !== 'Edit')`
- Optional chaining: `data.tool_input?.file_path || ''`
- Null checks: `if (!sessionId) { process.exit(0); }`
- Default values: `data.cwd || process.cwd()`, `data.model?.display_name || 'Claude'`
## File I/O Testing
**Patterns for Reliability:**
- Synchronous I/O ensures order: `fs.readFileSync()` → process → `fs.writeFileSync()`
- Directory existence checked before writing: `if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir, { recursive: true }); }`
- Try-catch wraps all file operations that could fail:
```javascript
try {
const bridgeData = JSON.stringify({ ... });
fs.writeFileSync(bridgePath, bridgeData);
} catch (e) {
// Silent fail -- bridge is best-effort, don't break statusline
}
```
## State Machine / Behavior Testing
**Context Monitor Debounce Logic** (`gsd-context-monitor.js`):
- Tracks warning state in file: `/tmp/claude-ctx-{session_id}-warned.json`
- Debounce counter incremented: `warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1`
- Severity escalation bypasses debounce: `if (severityEscalated) { // emit immediately }`
- State reset on warn: `warnData.callsSinceWarn = 0`
This pattern validates behavior without formal tests:
```javascript
let warnData = { callsSinceWarn: 0, lastLevel: null };
const isCritical = remaining <= CRITICAL_THRESHOLD;
const currentLevel = isCritical ? 'critical' : 'warning';
const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
process.exit(0); // Suppress warning
}
```
## Regex Pattern Testing
**Prompt Injection Detection** (`gsd-prompt-guard.js`):
Patterns tested against content without formal unit tests:
```javascript
const INJECTION_PATTERNS = [
/ignore\s+(all\s+)?previous\s+instructions/i,
/override\s+(system|previous)\s+(prompt|instructions)/i,
/you\s+are\s+now\s+(?:a|an|the)\s+/i,
/(?:print|output|reveal|show|display|repeat)\s+(?:your\s+)?(?:system\s+)?(?:prompt|instructions)/i,
/\[SYSTEM\]/i,
/<<\s*SYS\s*>>/i,
];
for (const pattern of INJECTION_PATTERNS) {
if (pattern.test(content)) {
findings.push(pattern.source);
}
}
```
Unicode detection without regex: `if (/[\u200B-\u200F\u2028-\u202F\uFEFF\u00AD]/.test(content))`
## Environment & Configuration Testing
**Configuration Loading Safety** (all hooks):
- Try-catch with silent fail:
```javascript
const configPath = path.join(cwd, '.planning', 'config.json');
if (fs.existsSync(configPath)) {
try {
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
if (config.hooks?.context_warnings === false) {
process.exit(0); // Feature disabled
}
} catch (e) {
// Ignore config parse errors
}
}
```
- Optional chaining for nested config: `config.hooks?.workflow_guard`, `config.hooks?.context_warnings`
**Environment Variable Access:**
```javascript
const envDir = process.env.CLAUDE_CONFIG_DIR;
if (envDir && fs.existsSync(path.join(envDir, 'get-shit-done', 'VERSION'))) {
return envDir; // Custom config dir detected
}
```
## Performance & Resource Management
**Timeout Guards (prevent resource leaks):**
- All hooks implement stdin timeout: `const stdinTimeout = setTimeout(() => process.exit(0), 3000);`
- Longer timeout for high-volume operations: `const stdinTimeout = setTimeout(() => process.exit(0), 10000);` in context-monitor
- Always cleared before processing: `clearTimeout(stdinTimeout);`
**Background Process Management** (`gsd-check-update.js`):
- Child process spawned with `stdio: 'ignore'`: doesn't inherit parent's stdio
- Process detached on Windows: `detached: true` (required for proper cleanup)
- Parent calls `child.unref()`: parent doesn't wait for child to exit
```javascript
const child = spawn(process.execPath, ['-e', `...inline script...`], {
stdio: 'ignore',
windowsHide: true,
detached: true
});
child.unref();
```
## Test Coverage Gaps
**Areas Without Formal Testing:**
1. **Regex Pattern Accuracy** - Injection patterns untested against false positives/negatives
2. **Debounce Counter Edge Cases** - Corruption recovery, counter reset logic
3. **Platform-Specific Behavior** - Windows vs Linux path handling, process detachment
4. **Concurrent Access** - Multiple hooks writing to same state files simultaneously
5. **Large Input Handling** - No tests for multi-megabyte JSON on stdin
6. **Stale File Cleanup** - No validation that temp files are properly removed
7. **Spawn Child Behavior** - Background update check success/failure not validated
## Recommendation for Testing
**Given the architecture (CLI hooks, not library code):**
- Integration testing more valuable than unit tests
- Manual testing via real Claude Code sessions is current primary validation
- Would recommend:
1. Snapshot tests for JSON output structure
2. Mock file system for configuration loading paths
3. Integration tests simulating tool use hook flow
4. Platform-specific testing (Windows, macOS, Linux) for path handling
**Lack of tests is acceptable for:**
- Simple stdin/stdout data transformation scripts
- Hooks deployed once per installation (not on every tool call)
- Silent-fail-safe design (errors don't break workflows)
- Real-world testing via 50+ GitHub issues and fixes
---
*Testing analysis: 2026-03-27*

38
.planning/config.json Normal file
View File

@@ -0,0 +1,38 @@
{
"model_profile": "balanced",
"commit_docs": true,
"parallelization": true,
"search_gitignored": false,
"brave_search": false,
"firecrawl": false,
"exa_search": false,
"git": {
"branching_strategy": "none",
"phase_branch_template": "gsd/phase-{phase}-{slug}",
"milestone_branch_template": "gsd/{milestone}-{slug}",
"quick_branch_template": null
},
"workflow": {
"research": true,
"plan_check": true,
"verifier": true,
"nyquist_validation": true,
"auto_advance": false,
"node_repair": true,
"node_repair_budget": 2,
"ui_phase": true,
"ui_safety_gate": true,
"text_mode": false,
"research_before_questions": false,
"discuss_mode": "discuss",
"skip_discuss": false,
"_auto_chain_active": false
},
"hooks": {
"context_warnings": true
},
"agent_skills": {},
"resolve_model_ids": "omit",
"mode": "interactive",
"granularity": "standard"
}

View File

@@ -0,0 +1,256 @@
---
phase: 01-architecture-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- ChatAgent.sln
- src/ChatAgent.Client/ChatAgent.Client.csproj
- src/ChatAgent.Client/Program.cs
- src/ChatAgent.Client/App.razor
- src/ChatAgent.Client/_Imports.razor
- src/ChatAgent.Client/wwwroot/index.html
- src/ChatAgent.Client/Properties/launchSettings.json
- src/ChatAgent.Api/ChatAgent.Api.csproj
- src/ChatAgent.Api/Program.cs
- src/ChatAgent.Api/Properties/launchSettings.json
- src/ChatAgent.Api/appsettings.json
- src/ChatAgent.Api/appsettings.Development.json
- src/ChatAgent.Shared/ChatAgent.Shared.csproj
- .gitignore
autonomous: true
requirements:
- CODE-02
must_haves:
truths:
- "Running `dotnet build ChatAgent.sln` from repo root succeeds with zero errors"
- "Solution contains exactly three projects: ChatAgent.Client, ChatAgent.Api, ChatAgent.Shared"
- "Both Client and Api projects reference ChatAgent.Shared"
- "Client runs on https://localhost:5200, API runs on https://localhost:7100"
artifacts:
- path: "ChatAgent.sln"
provides: "Solution file at repo root"
contains: "ChatAgent.Client"
- path: "src/ChatAgent.Client/ChatAgent.Client.csproj"
provides: "Blazor WASM client project"
contains: "ChatAgent.Shared"
- path: "src/ChatAgent.Api/ChatAgent.Api.csproj"
provides: "ASP.NET Core Web API project"
contains: "ChatAgent.Shared"
- path: "src/ChatAgent.Shared/ChatAgent.Shared.csproj"
provides: "Shared class library"
contains: "net9.0"
key_links:
- from: "src/ChatAgent.Client/ChatAgent.Client.csproj"
to: "src/ChatAgent.Shared/ChatAgent.Shared.csproj"
via: "ProjectReference"
pattern: "ProjectReference.*ChatAgent\\.Shared"
- from: "src/ChatAgent.Api/ChatAgent.Api.csproj"
to: "src/ChatAgent.Shared/ChatAgent.Shared.csproj"
via: "ProjectReference"
pattern: "ProjectReference.*ChatAgent\\.Shared"
---
<objective>
Create the three-project .NET 9 solution scaffold with Blazor WASM client, ASP.NET Core Web API, and shared class library. Configure predictable development ports and project references.
Purpose: Establish the architectural skeleton that all subsequent phases build on. Phase 1's concept is "solution structure and project boundaries" (per CODE-02). No feature code -- just the verified scaffold.
Output: A buildable solution with three projects, shared references, and aligned port configuration.
</objective>
<execution_context>
@/home/ys/family-repo/AgenticCode/.claude/get-shit-done/workflows/execute-plan.md
@/home/ys/family-repo/AgenticCode/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-architecture-foundation/01-CONTEXT.md
@.planning/phases/01-architecture-foundation/01-RESEARCH.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Create solution and three projects with references</name>
<files>
ChatAgent.sln,
src/ChatAgent.Client/ChatAgent.Client.csproj,
src/ChatAgent.Api/ChatAgent.Api.csproj,
src/ChatAgent.Shared/ChatAgent.Shared.csproj
</files>
<read_first>
.planning/phases/01-architecture-foundation/01-RESEARCH.md (Standard Stack and Installation sections),
.planning/phases/01-architecture-foundation/01-CONTEXT.md (D-01 through D-03 decisions),
CLAUDE.md
</read_first>
<action>
Run the following commands from the repo root (`/home/ys/family-repo/AgenticCode`). Per D-01, D-02, D-03:
1. Create the solution file at repo root:
```
dotnet new sln -n ChatAgent
```
2. Create the three projects targeting net9.0:
```
dotnet new blazorwasm -n ChatAgent.Client --framework net9.0 -o src/ChatAgent.Client
dotnet new webapi -n ChatAgent.Api --framework net9.0 --use-controllers -o src/ChatAgent.Api
dotnet new classlib -n ChatAgent.Shared --framework net9.0 -o src/ChatAgent.Shared
```
3. Add all three projects to the solution:
```
dotnet sln ChatAgent.sln add src/ChatAgent.Client/ChatAgent.Client.csproj
dotnet sln ChatAgent.sln add src/ChatAgent.Api/ChatAgent.Api.csproj
dotnet sln ChatAgent.sln add src/ChatAgent.Shared/ChatAgent.Shared.csproj
```
4. Add Shared reference to both Client and Api:
```
dotnet add src/ChatAgent.Client/ChatAgent.Client.csproj reference src/ChatAgent.Shared/ChatAgent.Shared.csproj
dotnet add src/ChatAgent.Api/ChatAgent.Api.csproj reference src/ChatAgent.Shared/ChatAgent.Shared.csproj
```
5. Delete the template-generated placeholder files that are not needed:
- `src/ChatAgent.Shared/Class1.cs` (will be replaced by Models/ directory in Plan 02)
- `src/ChatAgent.Api/Controllers/WeatherForecastController.cs` (template default, replaced by HealthController in Plan 02)
- `src/ChatAgent.Api/WeatherForecast.cs` (template default model)
6. Create a `.gitignore` at repo root if one does not already exist. Include standard .NET ignores:
```
bin/
obj/
*.user
*.suo
.vs/
.idea/
*.swp
**/wwwroot/_framework/
```
7. Run `dotnet build ChatAgent.sln` to verify the scaffold compiles.
</action>
<verify>
<automated>cd /home/ys/family-repo/AgenticCode && dotnet build ChatAgent.sln --nologo 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- `ChatAgent.sln` exists at `/home/ys/family-repo/AgenticCode/ChatAgent.sln`
- `ChatAgent.sln` contains the string `ChatAgent.Client`
- `ChatAgent.sln` contains the string `ChatAgent.Api`
- `ChatAgent.sln` contains the string `ChatAgent.Shared`
- `src/ChatAgent.Client/ChatAgent.Client.csproj` contains `ProjectReference` with `ChatAgent.Shared`
- `src/ChatAgent.Api/ChatAgent.Api.csproj` contains `ProjectReference` with `ChatAgent.Shared`
- All three `.csproj` files contain `net9.0` as TargetFramework
- `dotnet build ChatAgent.sln` exits with code 0
- `src/ChatAgent.Shared/Class1.cs` does NOT exist
- `src/ChatAgent.Api/Controllers/WeatherForecastController.cs` does NOT exist
- `.gitignore` exists at repo root and contains `bin/` and `obj/`
</acceptance_criteria>
<done>Three-project solution builds successfully from repo root with shared references wired in both directions.</done>
</task>
<task type="auto">
<name>Task 2: Configure predictable dev ports and clean up template defaults</name>
<files>
src/ChatAgent.Client/Properties/launchSettings.json,
src/ChatAgent.Api/Properties/launchSettings.json,
src/ChatAgent.Api/appsettings.json,
src/ChatAgent.Api/appsettings.Development.json,
src/ChatAgent.Client/wwwroot/appsettings.json
</files>
<read_first>
src/ChatAgent.Client/Properties/launchSettings.json,
src/ChatAgent.Api/Properties/launchSettings.json,
src/ChatAgent.Api/appsettings.json,
.planning/phases/01-architecture-foundation/01-RESEARCH.md (Pitfall 1: port alignment, Pattern 4: base URL config)
</read_first>
<action>
Per the research (Pitfall 1), template-generated ports are random and cause CORS mismatches. Set predictable ports:
1. Edit `src/ChatAgent.Client/Properties/launchSettings.json`:
- Set the HTTPS URL to `https://localhost:5200`
- Set the HTTP URL to `http://localhost:5100`
- Apply to both the profile used by `dotnet run` and any IIS Express profile
- Keep the `"inspectUri"` setting if present (needed for Blazor WASM debugging)
2. Edit `src/ChatAgent.Api/Properties/launchSettings.json`:
- Set the HTTPS URL to `https://localhost:7100`
- Set the HTTP URL to `http://localhost:7000`
- Remove the `"launchBrowser": true` setting if present (API has no browser UI)
3. Create `src/ChatAgent.Client/wwwroot/appsettings.json` with the API base URL:
```json
{
"ApiBaseUrl": "https://localhost:7100"
}
```
This file is PUBLIC (served to the browser). Never put secrets here.
4. Verify `src/ChatAgent.Api/appsettings.json` exists (template-generated). Remove any Swagger/OpenAPI configuration if present (not needed for this project). Ensure it has a clean structure:
```json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
```
5. Ensure `src/ChatAgent.Api/appsettings.Development.json` exists with dev logging:
```json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
```
6. Run `dotnet build ChatAgent.sln` again to confirm nothing broke.
</action>
<verify>
<automated>cd /home/ys/family-repo/AgenticCode && grep -c "5200" src/ChatAgent.Client/Properties/launchSettings.json && grep -c "7100" src/ChatAgent.Api/Properties/launchSettings.json && grep -c "ApiBaseUrl" src/ChatAgent.Client/wwwroot/appsettings.json && dotnet build ChatAgent.sln --nologo 2>&1 | tail -3</automated>
</verify>
<acceptance_criteria>
- `src/ChatAgent.Client/Properties/launchSettings.json` contains `5200`
- `src/ChatAgent.Api/Properties/launchSettings.json` contains `7100`
- `src/ChatAgent.Client/wwwroot/appsettings.json` contains `"ApiBaseUrl": "https://localhost:7100"`
- `src/ChatAgent.Api/appsettings.json` does NOT contain `swagger` or `Swagger` (cleaned up)
- `dotnet build ChatAgent.sln` exits with code 0
</acceptance_criteria>
<done>Both projects configured with predictable ports (Client: 5200, API: 7100), client has API base URL in public config, build still passes.</done>
</task>
</tasks>
<verification>
From repo root:
1. `dotnet build ChatAgent.sln` completes with 0 errors
2. Solution has exactly 3 projects (grep for `.csproj` in sln file)
3. Both Client and Api reference Shared (grep for ProjectReference in both .csproj files)
4. Port 5200 appears in Client launchSettings, port 7100 in API launchSettings
5. Client wwwroot/appsettings.json has ApiBaseUrl pointing to API port
</verification>
<success_criteria>
- Three-project solution builds from `dotnet build ChatAgent.sln` with zero errors
- Projects are in `src/` subdirectories per D-02
- Solution file is at repo root per D-03
- Shared library is referenced by both Client and Api
- Ports are predictable and aligned (Client: 5200, API: 7100)
- Template boilerplate (WeatherForecast) is removed
</success_criteria>
<output>
After completion, create `.planning/phases/01-architecture-foundation/01-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,121 @@
---
phase: 01-architecture-foundation
plan: 01
subsystem: infra
tags: [dotnet, blazor-wasm, webapi, solution-scaffold, net9]
# Dependency graph
requires: []
provides:
- Three-project .NET 9 solution (Client, Api, Shared)
- Predictable dev ports (Client: 5200, API: 7100)
- Shared project reference wiring
- API base URL configuration in client
affects: [01-02, 02, 03, 04, 05]
# Tech tracking
tech-stack:
added: [".NET 9 SDK 9.0.312", "Blazor WebAssembly Standalone", "ASP.NET Core Web API", "Class Library"]
patterns: ["Three-project solution with shared library", "Predictable port assignment", "global.json SDK pinning"]
key-files:
created:
- ChatAgent.sln
- global.json
- src/ChatAgent.Client/ChatAgent.Client.csproj
- src/ChatAgent.Api/ChatAgent.Api.csproj
- src/ChatAgent.Shared/ChatAgent.Shared.csproj
- src/ChatAgent.Client/wwwroot/appsettings.json
modified:
- .gitignore
- src/ChatAgent.Client/Properties/launchSettings.json
- src/ChatAgent.Api/Properties/launchSettings.json
key-decisions:
- "Pinned .NET 9 via global.json since .NET 10 is installed as default"
- "Kept inspectUri in Client launchSettings for Blazor WASM debugging support"
patterns-established:
- "SDK pinning: global.json at repo root locks .NET version"
- "Port convention: Client HTTPS=5200, API HTTPS=7100"
- "Client config: wwwroot/appsettings.json for public settings (ApiBaseUrl)"
requirements-completed: [CODE-02]
# Metrics
duration: 2min
completed: 2026-03-27
---
# Phase 01 Plan 01: Solution Scaffold Summary
**Three-project .NET 9 solution with Blazor WASM client, ASP.NET Core Web API, and shared class library at predictable dev ports**
## Performance
- **Duration:** 2 min
- **Started:** 2026-03-27T22:50:21Z
- **Completed:** 2026-03-27T22:52:42Z
- **Tasks:** 2
- **Files modified:** 26
## Accomplishments
- Created ChatAgent.sln with three projects targeting net9.0
- Wired ChatAgent.Shared as ProjectReference in both Client and Api
- Configured predictable ports (Client: 5200, API: 7100) and API base URL
- Removed template boilerplate (WeatherForecast, Class1)
- Pinned .NET 9 SDK via global.json
## Task Commits
Each task was committed atomically:
1. **Task 1: Create solution and three projects with references** - `eeaa9de` (feat)
2. **Task 2: Configure predictable dev ports and clean up template defaults** - `c6f1225` (feat)
## Files Created/Modified
- `ChatAgent.sln` - Solution file at repo root with 3 projects
- `global.json` - Pins .NET SDK to 9.0.312
- `src/ChatAgent.Client/ChatAgent.Client.csproj` - Blazor WASM client with Shared reference
- `src/ChatAgent.Api/ChatAgent.Api.csproj` - Web API with Shared reference
- `src/ChatAgent.Shared/ChatAgent.Shared.csproj` - Shared class library
- `src/ChatAgent.Client/Properties/launchSettings.json` - Client ports 5200/5100
- `src/ChatAgent.Api/Properties/launchSettings.json` - API ports 7100/7000
- `src/ChatAgent.Client/wwwroot/appsettings.json` - ApiBaseUrl pointing to API
- `.gitignore` - Added .NET-specific ignore patterns
## Decisions Made
- Pinned .NET 9 via global.json since machine default is .NET 10 (10.0.201) -- required to match CLAUDE.md constraint
- Kept inspectUri in Client launchSettings profiles for Blazor WASM debugging support
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Added global.json to pin .NET 9 SDK**
- **Found during:** Task 1 (before project creation)
- **Issue:** Machine has .NET 10 as default SDK; `dotnet new` would target net10.0 without intervention
- **Fix:** Created global.json with version 9.0.312 and rollForward: latestPatch
- **Files modified:** global.json
- **Verification:** `dotnet --version` returns 9.0.312
- **Committed in:** eeaa9de (Task 1 commit)
---
**Total deviations:** 1 auto-fixed (1 blocking)
**Impact on plan:** Essential for correctness -- without SDK pinning, all projects would target wrong framework version.
## Issues Encountered
None
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- Solution scaffold is complete and builds with zero errors
- Ready for Plan 02: CORS configuration, health endpoint, HttpClient setup, and tutorial comments
- All three projects have proper references and port alignment
---
*Phase: 01-architecture-foundation*
*Completed: 2026-03-27*

View File

@@ -0,0 +1,366 @@
---
phase: 01-architecture-foundation
plan: 02
type: execute
wave: 2
depends_on:
- 01-01
files_modified:
- src/ChatAgent.Shared/Models/HealthResponse.cs
- src/ChatAgent.Api/Program.cs
- src/ChatAgent.Api/Controllers/HealthController.cs
- src/ChatAgent.Client/Program.cs
- src/ChatAgent.Client/Services/ChatApiClient.cs
- src/ChatAgent.Client/Pages/Home.razor
- src/ChatAgent.Client/Layout/MainLayout.razor
- src/ChatAgent.Client/_Imports.razor
- src/ChatAgent.Client/wwwroot/css/app.css
autonomous: false
requirements:
- CODE-01
- CODE-02
must_haves:
truths:
- "A GET request from the WASM client to /api/health on the API server returns a HealthResponse with status 'healthy'"
- "CORS does not block the cross-origin request from localhost:5200 to localhost:7100"
- "The Home page displays the health check result (status and timestamp) fetched from the API"
- "Every .cs and .razor file contains inline comments explaining the Blazor/ASP.NET Core concept it demonstrates"
- "dotnet publish on the WASM project completes with no IL trimmer warnings"
artifacts:
- path: "src/ChatAgent.Shared/Models/HealthResponse.cs"
provides: "Shared DTO for health check"
contains: "public class HealthResponse"
- path: "src/ChatAgent.Api/Controllers/HealthController.cs"
provides: "Health check API endpoint"
contains: "[HttpGet]"
- path: "src/ChatAgent.Api/Program.cs"
provides: "API entry point with CORS and controller mapping"
contains: "AllowBlazorClient"
- path: "src/ChatAgent.Client/Services/ChatApiClient.cs"
provides: "Typed HttpClient wrapper"
contains: "GetHealthAsync"
- path: "src/ChatAgent.Client/Program.cs"
provides: "WASM entry point with DI registration"
contains: "AddHttpClient<ChatApiClient>"
- path: "src/ChatAgent.Client/Pages/Home.razor"
provides: "Landing page showing health check result"
contains: "@inject"
key_links:
- from: "src/ChatAgent.Client/Pages/Home.razor"
to: "src/ChatAgent.Client/Services/ChatApiClient.cs"
via: "@inject ChatApiClient"
pattern: "@inject.*ChatApiClient"
- from: "src/ChatAgent.Client/Services/ChatApiClient.cs"
to: "src/ChatAgent.Api/Controllers/HealthController.cs"
via: "HTTP GET to api/health"
pattern: "api/health"
- from: "src/ChatAgent.Api/Controllers/HealthController.cs"
to: "src/ChatAgent.Shared/Models/HealthResponse.cs"
via: "returns HealthResponse object"
pattern: "HealthResponse"
- from: "src/ChatAgent.Api/Program.cs"
to: "CORS middleware"
via: "UseCors before MapControllers"
pattern: "UseCors.*AllowBlazorClient"
---
<objective>
Implement the health check round-trip: shared DTO, API controller with CORS, typed HttpClient in the WASM client, and a home page that displays the result. Every file gets full tutorial-style inline comments per CODE-01.
Purpose: Prove the WASM-to-API communication path works end-to-end with CORS, establishing the pattern all future API calls will follow. This is Phase 1's single concept: "solution structure and project boundaries" (CODE-02).
Output: A running app where the WASM client calls the API health endpoint and displays the response.
</objective>
<execution_context>
@/home/ys/family-repo/AgenticCode/.claude/get-shit-done/workflows/execute-plan.md
@/home/ys/family-repo/AgenticCode/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-architecture-foundation/01-CONTEXT.md
@.planning/phases/01-architecture-foundation/01-RESEARCH.md
@.planning/phases/01-architecture-foundation/01-01-SUMMARY.md
<interfaces>
<!-- From Plan 01 outputs -- executor needs these project paths and port config -->
Projects created by Plan 01:
- src/ChatAgent.Client/ (Blazor WASM, port 5200)
- src/ChatAgent.Api/ (ASP.NET Core Web API, port 7100)
- src/ChatAgent.Shared/ (class library, referenced by both)
Client config (src/ChatAgent.Client/wwwroot/appsettings.json):
```json
{
"ApiBaseUrl": "https://localhost:7100"
}
```
All projects target net9.0. Solution file at repo root: ChatAgent.sln
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Implement shared DTO, API health endpoint with CORS, and client services</name>
<files>
src/ChatAgent.Shared/Models/HealthResponse.cs,
src/ChatAgent.Api/Program.cs,
src/ChatAgent.Api/Controllers/HealthController.cs,
src/ChatAgent.Client/Program.cs,
src/ChatAgent.Client/Services/ChatApiClient.cs,
src/ChatAgent.Client/Pages/Home.razor,
src/ChatAgent.Client/Layout/MainLayout.razor,
src/ChatAgent.Client/_Imports.razor,
src/ChatAgent.Client/wwwroot/css/app.css
</files>
<read_first>
src/ChatAgent.Shared/ChatAgent.Shared.csproj,
src/ChatAgent.Api/Program.cs,
src/ChatAgent.Api/ChatAgent.Api.csproj,
src/ChatAgent.Client/Program.cs,
src/ChatAgent.Client/ChatAgent.Client.csproj,
src/ChatAgent.Client/_Imports.razor,
src/ChatAgent.Client/Layout/MainLayout.razor,
src/ChatAgent.Client/Pages/Home.razor,
src/ChatAgent.Client/wwwroot/css/app.css,
.planning/phases/01-architecture-foundation/01-RESEARCH.md,
.planning/phases/01-architecture-foundation/01-CONTEXT.md
</read_first>
<action>
Create/modify the following files. Per D-07/D-08/D-09 (CODE-01), EVERY file must have tutorial-style inline comments explaining WHAT each Blazor/ASP.NET Core concept is and WHY it is used. Comments go inline as XML doc comments and `//` comments right next to the code.
**A. Shared DTO** -- Create `src/ChatAgent.Shared/Models/HealthResponse.cs`:
Create the `Models/` directory under `src/ChatAgent.Shared/`. Write the HealthResponse class in namespace `ChatAgent.Shared.Models` with two properties:
- `public string Status { get; set; } = string.Empty;` -- server health status (e.g., "healthy")
- `public DateTime Timestamp { get; set; }` -- server timestamp proving live response
Include XML doc comments on the class explaining it is a shared DTO (Data Transfer Object) that lives in the Shared project so both Client and Api use the same type without duplication. When the API returns this object, the Client deserializes it into the same class.
**B. API Program.cs** -- Rewrite `src/ChatAgent.Api/Program.cs`:
The file must contain, in order:
1. Top comment block explaining this is the ASP.NET Core Web API entry point, that it will eventually proxy OpenAI calls and manage JSON storage, but in Phase 1 only serves a health check
2. `builder.Services.AddControllers();` with comment: "We use Controllers (not Minimal API) for explicit structure (D-05)"
3. CORS configuration using `builder.Services.AddCors()` with a named policy `"AllowBlazorClient"` that allows origin `"https://localhost:5200"`, `AllowAnyHeader()`, `AllowAnyMethod()`. Include comments explaining: CORS is required because the WASM client runs on a different origin (different port), browsers block cross-origin requests by default for security
4. `var app = builder.Build();`
5. Middleware in correct order with comment "Middleware order matters in ASP.NET Core -- CORS must be applied before routing and authorization":
- `app.UseCors("AllowBlazorClient");`
- `app.UseAuthorization();`
- `app.MapControllers();` with comment explaining it discovers all [ApiController] classes
6. `app.Run();`
Do NOT include Swagger/OpenAPI middleware. Do NOT include `app.UseHttpsRedirection()` (it causes issues with local dev).
**C. Health Controller** -- Create `src/ChatAgent.Api/Controllers/HealthController.cs`:
Create the `Controllers/` directory if it does not exist (template may have one already). Write:
- Namespace: `ChatAgent.Api.Controllers`
- Class: `HealthController : ControllerBase`
- Attributes: `[ApiController]` and `[Route("api/[controller]")]`
- Method: `[HttpGet] public IActionResult Get()` returning `Ok(new HealthResponse { Status = "healthy", Timestamp = DateTime.UtcNow })`
- Using: `ChatAgent.Shared.Models`
- Comments explaining: [ApiController] enables automatic model validation, [Route] maps the class to a URL path, [HttpGet] maps this method to GET requests. This endpoint proves CORS works between WASM and API.
**D. Client Program.cs** -- Rewrite `src/ChatAgent.Client/Program.cs`:
The file must contain:
1. Top comment block explaining this is the Blazor WASM application entry point, that WebAssemblyHostBuilder configures root components, services (DI), and configuration
2. Using statements: `Microsoft.AspNetCore.Components.Web`, `Microsoft.AspNetCore.Components.WebAssembly.Hosting`, `ChatAgent.Client`, `ChatAgent.Client.Services`
3. `var builder = WebAssemblyHostBuilder.CreateDefault(args);`
4. `builder.RootComponents.Add<App>("#app");` with comment explaining it renders the App component inside the `<div id="app">` element in index.html
5. `builder.RootComponents.Add<HeadOutlet>("head::after");` with comment explaining HeadOutlet manages head elements from Razor components
6. Read API base URL from config: `var apiBaseUrl = builder.Configuration["ApiBaseUrl"] ?? "https://localhost:7100";` with comment explaining wwwroot/appsettings.json is PUBLIC, never put secrets there
7. Register typed HttpClient: `builder.Services.AddHttpClient<ChatApiClient>(client => { client.BaseAddress = new Uri(apiBaseUrl); });` with comments explaining AddHttpClient uses IHttpClientFactory internally for proper socket management, per-client configuration, and constructor DI
8. `await builder.Build().RunAsync();`
**E. Typed HttpClient** -- Create `src/ChatAgent.Client/Services/ChatApiClient.cs`:
Create the `Services/` directory under `src/ChatAgent.Client/`. Write:
- Namespace: `ChatAgent.Client.Services`
- Class: `ChatApiClient` with constructor injection of `HttpClient`
- Method: `public async Task<HealthResponse?> GetHealthAsync()` calling `_httpClient.GetFromJsonAsync<HealthResponse>("api/health")`
- Using: `ChatAgent.Shared.Models`, `System.Net.Http.Json`
- Comments explaining: In Blazor WASM, HttpClient is backed by the browser's Fetch API. We wrap it in a typed client so components don't depend on HttpClient directly (D-04). This makes testing easier and centralizes API URL management.
**F. Home Page** -- Rewrite `src/ChatAgent.Client/Pages/Home.razor`:
The file must contain:
1. `@page "/"` directive with comment explaining this maps the component to the root URL
2. `@using ChatAgent.Client.Services`
3. `@using ChatAgent.Shared.Models`
4. `@inject ChatApiClient ApiClient` with comment explaining @inject requests a service from the DI container
5. `<PageTitle>Chat Agent</PageTitle>`
6. An `<h1>Chat Agent</h1>` heading
7. A section that displays health check results:
- If `_healthResponse` is null and `_error` is null, show "Checking API connection..." text with class="loading"
- If `_healthResponse` is not null, show "API Status: {Status}" and "Server Time: {Timestamp}" in a div with class="health-status"
- If `_error` is not null, show the error message in a div with class="error-message"
8. An `@code` block with:
- `private HealthResponse? _healthResponse;`
- `private string? _error;`
- `protected override async Task OnInitializedAsync()` that calls `ApiClient.GetHealthAsync()` in a try/catch, setting `_error` on failure
- Comment explaining `OnInitializedAsync` is a Blazor lifecycle method called once when the component is first rendered, and that `StateHasChanged()` is called automatically after it completes
Use plain HTML/CSS per D-10 (no MudBlazor). Light theme per D-11.
**G. MainLayout** -- Update `src/ChatAgent.Client/Layout/MainLayout.razor`:
Simplify to a clean layout with:
- `@inherits LayoutComponentBase` with comment explaining LayoutComponentBase is the base class for layout components
- A `<main>` element wrapping `@Body` with comment explaining @Body is where the routed page content renders
- Remove any template navigation or sidebar (not needed in Phase 1)
- Basic styling: centered content with padding
**H. _Imports.razor** -- Update `src/ChatAgent.Client/_Imports.razor`:
Ensure these using directives are present (keep existing ones, add missing):
- `@using System.Net.Http`
- `@using System.Net.Http.Json`
- `@using Microsoft.AspNetCore.Components.Forms`
- `@using Microsoft.AspNetCore.Components.Routing`
- `@using Microsoft.AspNetCore.Components.Web`
- `@using Microsoft.AspNetCore.Components.Web.Virtualization`
- `@using Microsoft.AspNetCore.Components.WebAssembly.Http`
- `@using Microsoft.JSInterop`
- `@using ChatAgent.Client`
- `@using ChatAgent.Client.Layout`
- `@using ChatAgent.Client.Services`
- `@using ChatAgent.Shared.Models`
Add a comment at the top explaining _Imports.razor provides global using directives for all .razor files in the project.
**I. App CSS** -- Update `src/ChatAgent.Client/wwwroot/css/app.css`:
Replace with minimal clean CSS for Phase 1 (D-10 plain HTML/CSS, D-11 light theme):
```css
html, body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 0;
background-color: #ffffff;
color: #333333;
}
main {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
h1 {
color: #1a1a1a;
margin-bottom: 1.5rem;
}
.health-status {
padding: 1rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: #f9f9f9;
}
.health-status p {
margin: 0.5rem 0;
}
.error-message {
color: #d32f2f;
padding: 1rem;
border: 1px solid #d32f2f;
border-radius: 8px;
background-color: #fce4ec;
}
.loading {
color: #666666;
font-style: italic;
}
```
After all files are created, run:
1. `dotnet build ChatAgent.sln` -- must succeed
2. `dotnet publish src/ChatAgent.Client/ChatAgent.Client.csproj -c Release --nologo 2>&1 | grep -i "warning IL"` -- must produce no output (no IL trimmer warnings)
</action>
<verify>
<automated>cd /home/ys/family-repo/AgenticCode && dotnet build ChatAgent.sln --nologo 2>&1 | tail -5 && echo "---PUBLISH---" && dotnet publish src/ChatAgent.Client/ChatAgent.Client.csproj -c Release --nologo 2>&1 | grep -ci "warning IL" || echo "0 IL warnings"</automated>
</verify>
<acceptance_criteria>
- `src/ChatAgent.Shared/Models/HealthResponse.cs` contains `public class HealthResponse`
- `src/ChatAgent.Shared/Models/HealthResponse.cs` contains `public string Status`
- `src/ChatAgent.Shared/Models/HealthResponse.cs` contains `public DateTime Timestamp`
- `src/ChatAgent.Api/Program.cs` contains `AddCors`
- `src/ChatAgent.Api/Program.cs` contains `AllowBlazorClient`
- `src/ChatAgent.Api/Program.cs` contains `WithOrigins("https://localhost:5200")`
- `src/ChatAgent.Api/Program.cs` contains `UseCors` BEFORE `MapControllers` (line number of UseCors < line number of MapControllers)
- `src/ChatAgent.Api/Program.cs` contains `AddControllers`
- `src/ChatAgent.Api/Program.cs` does NOT contain `swagger` or `Swagger` or `UseHttpsRedirection`
- `src/ChatAgent.Api/Controllers/HealthController.cs` contains `[ApiController]`
- `src/ChatAgent.Api/Controllers/HealthController.cs` contains `[HttpGet]`
- `src/ChatAgent.Api/Controllers/HealthController.cs` contains `HealthResponse`
- `src/ChatAgent.Client/Program.cs` contains `AddHttpClient<ChatApiClient>`
- `src/ChatAgent.Client/Program.cs` contains `ApiBaseUrl`
- `src/ChatAgent.Client/Services/ChatApiClient.cs` contains `GetHealthAsync`
- `src/ChatAgent.Client/Services/ChatApiClient.cs` contains `api/health`
- `src/ChatAgent.Client/Pages/Home.razor` contains `@inject ChatApiClient`
- `src/ChatAgent.Client/Pages/Home.razor` contains `OnInitializedAsync`
- `src/ChatAgent.Client/_Imports.razor` contains `ChatAgent.Shared.Models`
- `dotnet build ChatAgent.sln` exits with code 0
- `dotnet publish` produces 0 IL trimmer warnings
- Every `.cs` file created contains at least 3 comment lines (`//` or `///`)
- `src/ChatAgent.Api/Program.cs` contains at least 5 comment lines
- `src/ChatAgent.Client/Program.cs` contains at least 5 comment lines
</acceptance_criteria>
<done>All source files created with full tutorial comments. Solution builds. WASM publishes with no IL trim warnings. Health check round-trip code is in place.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: Verify CORS health check works end-to-end</name>
<files>
src/ChatAgent.Client/Pages/Home.razor,
src/ChatAgent.Api/Controllers/HealthController.cs
</files>
<action>
Start both projects and verify the WASM client successfully calls the API health endpoint across origins.
1. In terminal 1: `cd /home/ys/family-repo/AgenticCode && dotnet run --project src/ChatAgent.Api`
2. In terminal 2: `cd /home/ys/family-repo/AgenticCode && dotnet run --project src/ChatAgent.Client`
3. Open browser to https://localhost:5200
4. Verify the page shows "Chat Agent" heading with "API Status: healthy" and a server timestamp
5. Check browser console (F12) for absence of CORS errors
6. Optionally test API directly: `curl -k https://localhost:7100/api/health`
</action>
<verify>
<automated>cd /home/ys/family-repo/AgenticCode && dotnet build ChatAgent.sln --nologo 2>&1 | tail -3</automated>
</verify>
<done>User confirms health check displays correctly in browser with no CORS errors.</done>
</task>
</tasks>
<verification>
Phase 1 success criteria from ROADMAP.md:
1. `dotnet run` on both projects starts without errors -- verified in checkpoint
2. WASM client reaches API server and gets response (CORS working) -- verified in checkpoint
3. Shared models library referenced by both projects -- verified by build success + ProjectReference in .csproj
4. `dotnet publish` on WASM completes with no IL trim warnings -- verified in Task 1 automated check
5. Every file contains inline comments explaining the Blazor concept -- verified by acceptance criteria comment line counts
</verification>
<success_criteria>
- Health check response visible in browser at https://localhost:5200 showing "healthy" status and timestamp
- No CORS errors in browser console
- All .cs and .razor files have tutorial-style inline comments (CODE-01)
- Phase introduces exactly one concept: solution structure and project boundaries (CODE-02)
- Build and publish both succeed cleanly
</success_criteria>
<output>
After completion, create `.planning/phases/01-architecture-foundation/01-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,97 @@
# Phase 1: Architecture Foundation - Context
**Gathered:** 2026-03-27
**Status:** Ready for planning
<domain>
## Phase Boundary
Two-project solution scaffold with WASM/API split and shared models. Establishes the tutorial commenting convention. The critical boundaries (no API key in WASM, no file I/O in WASM, no direct OpenAI calls from WASM) are architecturally enforced before any feature code is written.
</domain>
<decisions>
## Implementation Decisions
### Solution Structure
- **D-01:** Solution named `ChatAgent.sln` at repo root with three projects: `ChatAgent.Client` (Blazor WASM), `ChatAgent.Api` (ASP.NET Core backend), `ChatAgent.Shared` (shared models/DTOs)
- **D-02:** Projects live in `src/` subfolders: `src/ChatAgent.Client/`, `src/ChatAgent.Api/`, `src/ChatAgent.Shared/`
- **D-03:** Solution file at repo root for easy `dotnet build` from project root
### API Communication
- **D-04:** Typed HttpClient pattern — a `ChatApiClient` class in the Client project wraps all backend API calls, registered via DI
- **D-05:** Backend uses traditional MVC Controllers (not Minimal API) — more structure, familiar pattern for tutorial
- **D-06:** CORS configured on the API to allow the WASM client origin during development
### Tutorial Comments
- **D-07:** Full tutorial-style inline comments — explain everything including basic patterns, treat every file as a teaching moment
- **D-08:** Comments go inline (XML doc comments and `//` comments right next to the code), no separate companion docs
- **D-09:** Every Blazor concept introduced must have a comment explaining WHAT it is and WHY it's used
### UI Framework
- **D-10:** Start with plain HTML/CSS in Phase 1 — no MudBlazor yet. Learn raw Blazor rendering first, add component library later
- **D-11:** Light theme as default look and feel
### Claude's Discretion
- CORS configuration details (origins, headers, methods)
- Base URL configuration approach (appsettings.json vs environment variables)
- Project file (.csproj) configuration details
- Test project structure (if any placeholder tests needed)
- .NET version targeting (9 vs 10 based on current stability)
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Project context
- `.planning/PROJECT.md` — Project vision, constraints, core value (tutorial-style build)
- `.planning/REQUIREMENTS.md` — CODE-01 and CODE-02 requirements for this phase
- `.planning/ROADMAP.md` — Phase 1 success criteria and dependencies
### Research
- `.planning/research/STACK.md` — Recommended .NET 9 stack, OpenAI SDK, Markdig, version compatibility
- `.planning/research/ARCHITECTURE.md` — WASM/API split architecture, component boundaries, data flow
- `.planning/research/PITFALLS.md` — Critical pitfalls: streaming transport, API key exposure, DI lifetimes, IL trimming
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- No existing application code — greenfield project
### Established Patterns
- No patterns yet — this phase establishes the foundational patterns all subsequent phases will follow
### Integration Points
- `.planning/` directory exists with research and project docs
- `.claude/` directory has GSD workflow tooling (do not modify)
- Git repo already initialized with planning commits
</code_context>
<specifics>
## Specific Ideas
- ChatAgent.* namespace convention for all projects
- Controllers over Minimal API for more structured, tutorial-friendly code
- Plain HTML/CSS first to understand raw Blazor before adding component libraries
- Full verbose comments — this is a learning project, not a production codebase
</specifics>
<deferred>
## Deferred Ideas
None — discussion stayed within phase scope
</deferred>
---
*Phase: 01-architecture-foundation*
*Context gathered: 2026-03-27*

View File

@@ -0,0 +1,111 @@
# Phase 1: Architecture Foundation - Discussion Log
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
**Date:** 2026-03-27
**Phase:** 01-Architecture Foundation
**Areas discussed:** Solution structure, API communication, Tutorial comments, UI framework
---
## Solution Structure
### Naming
| Option | Description | Selected |
|--------|-------------|----------|
| ChatAgent.* | ChatAgent.Client, ChatAgent.Api, ChatAgent.Shared — clean namespace | ✓ |
| Custom name | You have a specific name in mind | |
| You decide | Claude picks a sensible naming convention | |
**User's choice:** ChatAgent.* namespace
### Location
| Option | Description | Selected |
|--------|-------------|----------|
| Repo root | Solution file at repo root, projects in src/ subfolders | ✓ |
| Nested src/ folder | Everything under src/ — keeps planning/config separate from code | |
| You decide | Claude picks based on .NET conventions | |
**User's choice:** Repo root with src/ subfolders
---
## API Communication
### HTTP Client Pattern
| Option | Description | Selected |
|--------|-------------|----------|
| Typed HttpClient | Named/typed HttpClient via DI — ChatApiClient class wraps all API calls | ✓ |
| Raw HttpClient | Inject HttpClient directly into components — simpler but less organized | |
| You decide | Claude picks the best pattern for a tutorial project | |
**User's choice:** Typed HttpClient (ChatApiClient)
### API Style
| Option | Description | Selected |
|--------|-------------|----------|
| Minimal API | app.MapGet/MapPost — modern .NET, less boilerplate | |
| Controllers | Traditional MVC controllers — more structure, more files | ✓ |
| You decide | Claude picks based on .NET 9 best practices | |
**User's choice:** Controllers
---
## Tutorial Comments
### Comment Depth
| Option | Description | Selected |
|--------|-------------|----------|
| Explain Blazor concepts | Comment on Blazor-specific things, skip basic C# | |
| Full tutorial | Explain everything including basic patterns — treat every file as a teaching moment | ✓ |
| Minimal + README | Light inline comments, but a per-phase README explaining what was built and why | |
**User's choice:** Full tutorial — explain everything
### Location
| Option | Description | Selected |
|--------|-------------|----------|
| Inline comments | XML doc comments and // comments right next to the code | ✓ |
| Companion docs | Separate .md file per phase explaining the concepts introduced | |
| Both | Inline comments + companion docs for deeper explanation | |
**User's choice:** Inline comments only
---
## UI Framework
### Framework Choice
| Option | Description | Selected |
|--------|-------------|----------|
| MudBlazor now | Install MudBlazor 9.2.0 in Phase 1 — ready for later phases | |
| Plain HTML/CSS first | Start minimal, add MudBlazor later — learn raw Blazor rendering first | ✓ |
| You decide | Claude picks what makes sense for a tutorial progression | |
**User's choice:** Plain HTML/CSS first
### Look & Feel
| Option | Description | Selected |
|--------|-------------|----------|
| Dark theme | Dark background, light text — like ChatGPT/Claude | |
| Light theme | Light background, dark text — clean and bright | ✓ |
| System default | Follow OS preference | |
| You decide | Claude picks a sensible default | |
**User's choice:** Light theme
---
## Claude's Discretion
- CORS configuration details
- Base URL configuration approach
- .csproj configuration details
- .NET version targeting
- Test project structure
## Deferred Ideas
None — discussion stayed within phase scope

View File

@@ -0,0 +1,500 @@
# Phase 1: Architecture Foundation - Research
**Researched:** 2026-03-27
**Domain:** .NET 9 Blazor WASM + ASP.NET Core Web API solution scaffolding
**Confidence:** HIGH
## Summary
Phase 1 creates the three-project solution structure (`ChatAgent.Client`, `ChatAgent.Api`, `ChatAgent.Shared`) with working CORS communication between WASM client and API server, and establishes the tutorial commenting convention that applies to all subsequent phases. No feature code is written -- this phase locks in the architectural boundaries (no API key in WASM, no file I/O in WASM, no direct OpenAI calls from WASM) so that later phases build on a verified foundation.
The .NET 9 SDK is available on this machine (9.0.312), alongside .NET 10 (10.0.201). Per CLAUDE.md the project targets .NET 9. The `blazorwasm`, `webapi`, and `classlib` templates are all available. The `webapi` template supports `--use-controllers` for the MVC Controller pattern the user chose (D-05). The hosted Blazor WASM template was removed in .NET 8, so the three projects must be created separately and added to a solution manually.
**Primary recommendation:** Create the solution using `dotnet new` templates targeting `net9.0`, add a shared class library for DTOs, wire up a minimal health-check endpoint, confirm CORS works from WASM to API, and verify `dotnet publish` completes cleanly.
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
- **D-01:** Solution named `ChatAgent.sln` at repo root with three projects: `ChatAgent.Client` (Blazor WASM), `ChatAgent.Api` (ASP.NET Core backend), `ChatAgent.Shared` (shared models/DTOs)
- **D-02:** Projects live in `src/` subfolders: `src/ChatAgent.Client/`, `src/ChatAgent.Api/`, `src/ChatAgent.Shared/`
- **D-03:** Solution file at repo root for easy `dotnet build` from project root
- **D-04:** Typed HttpClient pattern -- a `ChatApiClient` class in the Client project wraps all backend API calls, registered via DI
- **D-05:** Backend uses traditional MVC Controllers (not Minimal API) -- more structure, familiar pattern for tutorial
- **D-06:** CORS configured on the API to allow the WASM client origin during development
- **D-07:** Full tutorial-style inline comments -- explain everything including basic patterns, treat every file as a teaching moment
- **D-08:** Comments go inline (XML doc comments and `//` comments right next to the code), no separate companion docs
- **D-09:** Every Blazor concept introduced must have a comment explaining WHAT it is and WHY it's used
- **D-10:** Start with plain HTML/CSS in Phase 1 -- no MudBlazor yet. Learn raw Blazor rendering first, add component library later
- **D-11:** Light theme as default look and feel
### Claude's Discretion
- CORS configuration details (origins, headers, methods)
- Base URL configuration approach (appsettings.json vs environment variables)
- Project file (.csproj) configuration details
- Test project structure (if any placeholder tests needed)
- .NET version targeting (9 vs 10 based on current stability)
### Deferred Ideas (OUT OF SCOPE)
None -- discussion stayed within phase scope
</user_constraints>
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| CODE-01 | Every Blazor concept introduced has inline comments explaining what and why | Tutorial commenting convention (D-07/D-08/D-09); every `.cs`, `.razor`, and `.csproj` file gets comments explaining the Blazor/ASP.NET Core concept it demonstrates |
| CODE-02 | Each phase introduces one concept incrementally (tutorial-style progression) | Phase 1's concept is "solution structure and project boundaries" -- no feature code, no OpenAI calls, no persistence, no streaming. Just the scaffold + one health-check round-trip to prove communication works |
</phase_requirements>
## Standard Stack
### Core (Phase 1 Only)
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| .NET 9 SDK | 9.0.312 | Runtime and tooling | Installed on machine; stable; CLAUDE.md specifies .NET 9 |
| Blazor WebAssembly Standalone | net9.0 | Client SPA | Non-negotiable per constraints |
| ASP.NET Core Web API | net9.0 | Backend API server | Required for API key isolation |
| Class Library | net9.0 | Shared models/DTOs | Referenced by both Client and Api projects |
### Not Needed in Phase 1
| Library | Why Deferred |
|---------|-------------|
| `OpenAI` NuGet | No AI calls in Phase 1 -- introduced in Phase 3 |
| `Markdig` | No markdown rendering in Phase 1 -- introduced in Phase 4 |
| `MudBlazor` | D-10 explicitly defers this; plain HTML/CSS first |
| Any test framework | No business logic to test yet; placeholder tests are optional (Claude's discretion -- recommend skipping to keep Phase 1 focused) |
**Installation:**
```bash
# From repo root
dotnet new sln -n ChatAgent
# Create projects targeting .NET 9
dotnet new blazorwasm -n ChatAgent.Client --framework net9.0 -o src/ChatAgent.Client
dotnet new webapi -n ChatAgent.Api --framework net9.0 --use-controllers -o src/ChatAgent.Api
dotnet new classlib -n ChatAgent.Shared --framework net9.0 -o src/ChatAgent.Shared
# Add projects to solution
dotnet sln ChatAgent.sln add src/ChatAgent.Client/ChatAgent.Client.csproj
dotnet sln ChatAgent.sln add src/ChatAgent.Api/ChatAgent.Api.csproj
dotnet sln ChatAgent.sln add src/ChatAgent.Shared/ChatAgent.Shared.csproj
# Add Shared reference to both projects
dotnet add src/ChatAgent.Client/ChatAgent.Client.csproj reference src/ChatAgent.Shared/ChatAgent.Shared.csproj
dotnet add src/ChatAgent.Api/ChatAgent.Api.csproj reference src/ChatAgent.Shared/ChatAgent.Shared.csproj
```
## Architecture Patterns
### Recommended Project Structure (Phase 1)
```
ChatAgent.sln # Solution file at repo root (D-03)
src/
ChatAgent.Client/ # Blazor WASM standalone app
ChatAgent.Client.csproj
Program.cs # DI registration, HttpClient base URL config
App.razor # Root component
_Imports.razor # Global using directives
Layout/
MainLayout.razor # App shell layout
Pages/
Home.razor # Landing page (placeholder in Phase 1)
Services/
ChatApiClient.cs # Typed HttpClient wrapper (D-04)
wwwroot/
index.html # HTML host page
css/app.css # Basic styling (plain CSS, D-10)
ChatAgent.Api/ # ASP.NET Core Web API
ChatAgent.Api.csproj
Program.cs # DI, CORS, controller mapping
Controllers/
HealthController.cs # GET /api/health -- proves CORS works
appsettings.json # API config (base URLs, future API key ref)
appsettings.Development.json # Dev-specific config
ChatAgent.Shared/ # Shared class library
ChatAgent.Shared.csproj
Models/
HealthResponse.cs # Simple DTO for health-check response
```
### Pattern 1: Typed HttpClient in Blazor WASM (D-04)
**What:** A `ChatApiClient` class wraps `HttpClient` for all API calls. Registered in DI as a typed client.
**When to use:** Always -- components never use `HttpClient` directly.
**Example:**
```csharp
// Source: ASP.NET Core docs + CONTEXT.md D-04
// ChatApiClient.cs -- Typed HttpClient wrapper
// In Blazor WASM, HttpClient is backed by the browser's Fetch API.
// We wrap it in a typed client so components don't depend on HttpClient directly.
// This makes testing easier and centralizes API URL management.
public class ChatApiClient
{
private readonly HttpClient _httpClient;
public ChatApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
/// <summary>
/// Calls the health endpoint to verify the API server is reachable.
/// This is the first HTTP call from WASM to the backend -- proves CORS works.
/// </summary>
public async Task<HealthResponse?> GetHealthAsync()
{
return await _httpClient.GetFromJsonAsync<HealthResponse>("api/health");
}
}
// Registration in Program.cs:
// AddHttpClient<T> registers a typed HttpClient with its own configuration.
// The base address points to the API server (different port during local dev).
builder.Services.AddHttpClient<ChatApiClient>(client =>
{
client.BaseAddress = new Uri("https://localhost:7100");
});
```
### Pattern 2: MVC Controller on API (D-05)
**What:** Traditional `[ApiController]` with `[Route("api/[controller]")]` attributes instead of Minimal API `app.MapGet()`.
**When to use:** All API endpoints in this project.
**Example:**
```csharp
// HealthController.cs -- ASP.NET Core MVC Controller
// We use Controllers (not Minimal API) for more explicit structure (D-05).
// Each controller is a class with methods for each HTTP verb.
// [ApiController] enables automatic model validation and 400 responses.
[ApiController]
[Route("api/[controller]")]
public class HealthController : ControllerBase
{
/// <summary>
/// Health check endpoint. Returns server status and timestamp.
/// Used by the WASM client to verify the API is reachable and CORS is working.
/// </summary>
[HttpGet]
public IActionResult Get()
{
return Ok(new HealthResponse
{
Status = "healthy",
Timestamp = DateTime.UtcNow
});
}
}
```
### Pattern 3: CORS Configuration (D-06)
**What:** Explicit CORS policy on the API allowing the WASM client origin.
**When to use:** Required for any cross-origin HTTP call from WASM to API during development.
**Example:**
```csharp
// Program.cs (API project) -- CORS setup
// CORS (Cross-Origin Resource Sharing) is required because the Blazor WASM client
// runs on a different port (e.g., localhost:5200) than the API (e.g., localhost:7100).
// Browsers block cross-origin requests by default for security.
// We define a named policy that allows only our client origin.
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowBlazorClient", policy =>
{
policy.WithOrigins("https://localhost:5200") // WASM client origin
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// ... after builder.Build()
app.UseCors("AllowBlazorClient");
```
### Pattern 4: Base URL Configuration (Claude's Discretion)
**Recommendation:** Use `appsettings.json` in the WASM client for the API base URL. This is standard for Blazor WASM and the file is in `wwwroot/` (public, no secrets). Environment variables are harder to configure for WASM since it runs in the browser.
```csharp
// wwwroot/appsettings.json (Client project) -- PUBLIC config only
// In Blazor WASM, appsettings.json is a static file served to the browser.
// NEVER put secrets here. Only public configuration like API URLs.
{
"ApiBaseUrl": "https://localhost:7100"
}
// Program.cs -- reading the config
var apiBaseUrl = builder.Configuration["ApiBaseUrl"]
?? throw new InvalidOperationException("ApiBaseUrl not configured");
builder.Services.AddHttpClient<ChatApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
```
### Anti-Patterns to Avoid
- **Any `System.IO.File` usage in the Client project:** WASM runs in a browser sandbox; file I/O does not persist. All persistence is server-side.
- **Any API key or secret in `wwwroot/appsettings.json`:** This file is publicly accessible. Secrets belong in the API project only.
- **OpenAI SDK registration in the Client `Program.cs`:** All OpenAI calls go through the backend API. The Client never talks to OpenAI directly.
- **CORS wildcard (`AllowAnyOrigin`):** Even for local dev, scope to the actual client origin. Prevents accidental exposure.
- **Logic in `.razor` code blocks:** Components call services; services own logic (D-04 enforces this via typed client pattern).
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| HTTP client management | Manual `HttpClient` instantiation in components | `IHttpClientFactory` via `AddHttpClient<T>` | Proper socket management, DI lifecycle, testability |
| JSON serialization | Manual string building | `System.Text.Json` (built-in) with `GetFromJsonAsync`/`PostAsJsonAsync` | Type-safe, trim-compatible, zero dependencies |
| CORS handling | Custom middleware or response headers | `builder.Services.AddCors()` + `app.UseCors()` | Handles preflight OPTIONS requests, header management automatically |
| Project references | Copy-paste DTOs between projects | `dotnet add reference` to Shared project | Single source of truth; compile-time verification |
## Common Pitfalls
### Pitfall 1: Wrong Port in CORS or Client Config
**What goes wrong:** WASM client gets CORS errors because the API CORS policy specifies a different port than the client actually runs on.
**Why it happens:** `dotnet new blazorwasm` and `dotnet new webapi` assign random ports in `launchSettings.json`. Developer hardcodes one set of ports but the templates use different ones.
**How to avoid:** After project creation, check `Properties/launchSettings.json` in BOTH projects. Align the CORS origin in the API with the actual client URL. Or set explicit ports in `launchSettings.json`.
**Warning signs:** Browser console shows `Access-Control-Allow-Origin` errors.
### Pitfall 2: Missing `UseCors()` Middleware Order
**What goes wrong:** CORS policy is registered in DI but `app.UseCors()` is called after `app.MapControllers()`, causing CORS headers to not be applied.
**Why it happens:** ASP.NET Core middleware order matters. CORS must run before routing/endpoints.
**How to avoid:** Call `app.UseCors("PolicyName")` before `app.MapControllers()` and before `app.UseAuthorization()`.
**Warning signs:** CORS errors despite correct policy configuration.
### Pitfall 3: IL Trimming Warnings on Publish
**What goes wrong:** `dotnet publish` for the WASM project produces IL trimmer warnings about types that may be removed.
**Why it happens:** Blazor WASM uses IL trimming in Release mode. Types used only via reflection (JSON serialization) can be trimmed away.
**How to avoid:** For Phase 1, the surface area is tiny (one DTO). Verify `dotnet publish` completes without warnings. In later phases, use `[JsonSerializable]` source generators.
**Warning signs:** `dotnet publish` output contains lines with `IL2xxx` warning codes.
### Pitfall 4: Solution File Path Issues
**What goes wrong:** `dotnet build` from repo root fails because `ChatAgent.sln` references projects with wrong relative paths.
**Why it happens:** Solution was created in a subdirectory, or projects were moved after being added.
**How to avoid:** Create the solution at repo root, add projects using their paths relative to the sln file location.
**Warning signs:** `The project file could not be found` errors.
### Pitfall 5: WASM Project Serves on HTTPS But Certificate Not Trusted
**What goes wrong:** Browser shows security warning when accessing WASM app, blocking API calls.
**Why it happens:** Dev HTTPS certificate not installed or not trusted.
**How to avoid:** Run `dotnet dev-certs https --trust` once before starting development. Alternatively, use `--no-https` flag during project creation for Phase 1 simplicity and add HTTPS later.
**Warning signs:** Browser shows "Your connection is not private" page.
## Code Examples
### Shared DTO (ChatAgent.Shared)
```csharp
// Models/HealthResponse.cs
// This is a shared model (DTO - Data Transfer Object).
// It lives in the Shared project so both the Client and Api can use it
// without duplicating the class definition. When the API returns this object,
// the Client can deserialize it into the same type.
namespace ChatAgent.Shared.Models;
public class HealthResponse
{
/// <summary>
/// Server health status (e.g., "healthy").
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// Server timestamp -- proves we're getting a live response, not a cached one.
/// </summary>
public DateTime Timestamp { get; set; }
}
```
### WASM Program.cs (Client)
```csharp
// Program.cs -- Blazor WASM application entry point
// This is where the WebAssembly application starts.
// WebAssemblyHostBuilder configures:
// 1. Root components (what Razor components to render)
// 2. Services (dependency injection container)
// 3. Configuration (reads from wwwroot/appsettings.json)
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using ChatAgent.Client;
using ChatAgent.Client.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// Add<App>("#app") tells Blazor to render the App component
// inside the <div id="app"> element in wwwroot/index.html.
builder.RootComponents.Add<App>("#app");
// HeadOutlet manages <head> elements (title, meta tags) from Razor components.
builder.RootComponents.Add<HeadOutlet>("head::after");
// Read the API base URL from configuration (wwwroot/appsettings.json).
// This file is PUBLIC -- never put secrets here.
var apiBaseUrl = builder.Configuration["ApiBaseUrl"]
?? "https://localhost:7100";
// Register ChatApiClient as a typed HttpClient.
// AddHttpClient<T> uses IHttpClientFactory internally, which:
// - Manages HttpClient lifetimes properly (avoids socket exhaustion)
// - Allows per-client configuration (base address, headers)
// - Makes the service injectable via constructor DI
builder.Services.AddHttpClient<ChatApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
await builder.Build().RunAsync();
```
### API Program.cs (Server)
```csharp
// Program.cs -- ASP.NET Core Web API entry point
// This is the backend server that the Blazor WASM client talks to.
// It will eventually proxy OpenAI calls and manage JSON file storage.
// In Phase 1, it only serves a health check endpoint.
var builder = WebApplication.CreateBuilder(args);
// AddControllers() registers MVC controller services.
// We use Controllers (not Minimal API) for explicit structure (D-05).
builder.Services.AddControllers();
// CORS (Cross-Origin Resource Sharing) configuration.
// The WASM client runs on a different origin (different port),
// so the browser blocks requests unless the server explicitly allows them.
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowBlazorClient", policy =>
{
// TODO: Read from configuration instead of hardcoding
policy.WithOrigins("https://localhost:5200")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
var app = builder.Build();
// Middleware order matters in ASP.NET Core.
// CORS must be applied before routing and authorization.
app.UseCors("AllowBlazorClient");
app.UseAuthorization();
// MapControllers() discovers all [ApiController] classes
// and maps their [HttpGet], [HttpPost], etc. attributes to routes.
app.MapControllers();
app.Run();
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Hosted Blazor WASM template (Client+Server+Shared in one template) | Separate projects added to a manual solution | .NET 8 (Nov 2023) | Must create three projects manually; no `--hosted` flag |
| `Newtonsoft.Json` for serialization | `System.Text.Json` built into .NET | .NET Core 3.0+ | No extra dependency; source generators for trim safety |
| `HttpClient` registered directly as Singleton/Scoped | `IHttpClientFactory` via `AddHttpClient<T>` | .NET Core 2.1+ | Proper socket management; typed clients for clean architecture |
## .NET Version Decision (Claude's Discretion)
**Recommendation: Target .NET 9 (`net9.0`)**
Rationale:
- CLAUDE.md explicitly specifies .NET 9 as the target
- .NET 9.0.312 SDK is installed on this machine
- .NET 10 SDK (10.0.201) is also available but is not yet at LTS; using it in a tutorial project risks encountering preview-stage breaking changes
- All recommended packages (OpenAI 2.9.1, Markdig 1.1.1, MudBlazor 9.2.0) are confirmed compatible with .NET 9
- The `blazorwasm` template with `--framework net9.0` is verified working
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | None yet (greenfield) |
| Config file | None -- see Wave 0 |
| Quick run command | `dotnet build ChatAgent.sln` |
| Full suite command | `dotnet build ChatAgent.sln && dotnet publish src/ChatAgent.Client/ChatAgent.Client.csproj -c Release --nologo` |
### Phase Requirements -> Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| CODE-01 | Every file has inline comments explaining Blazor concepts | manual | Manual review of all `.cs` and `.razor` files | N/A |
| CODE-02 | Phase introduces one concept incrementally | manual | Manual review -- Phase 1 concept = "solution structure and project boundaries" | N/A |
| (SC-1) | `dotnet run` starts both projects without errors | smoke | `dotnet build ChatAgent.sln` (build verification) | N/A -- Wave 0 |
| (SC-2) | WASM client reaches API server (CORS working) | integration | Manual -- start both projects, verify health endpoint from client UI | N/A |
| (SC-3) | Shared models referenced by both projects | build | `dotnet build ChatAgent.sln` (compile-time check) | N/A -- Wave 0 |
| (SC-4) | `dotnet publish` completes with no IL trim warnings | smoke | `dotnet publish src/ChatAgent.Client/ChatAgent.Client.csproj -c Release --nologo 2>&1 \| grep -c "warning IL"` | N/A -- Wave 0 |
| (SC-5) | Every file contains inline comments | manual | Manual review | N/A |
### Sampling Rate
- **Per task commit:** `dotnet build ChatAgent.sln`
- **Per wave merge:** `dotnet build ChatAgent.sln && dotnet publish src/ChatAgent.Client/ChatAgent.Client.csproj -c Release --nologo`
- **Phase gate:** Full build + publish clean + manual CORS verification
### Wave 0 Gaps
- No formal test project needed in Phase 1 -- the verification is build success + publish success + manual CORS check
- The `dotnet build` and `dotnet publish` commands serve as the automated validation
- CODE-01 and CODE-02 are inherently manual-review requirements
## Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| .NET 9 SDK | All projects | Yes | 9.0.312 | -- |
| .NET 10 SDK | Not required | Yes | 10.0.201 | -- |
| `dotnet new blazorwasm` template | Client project | Yes | Included in SDK | -- |
| `dotnet new webapi` template | API project | Yes | Included in SDK | -- |
| `dotnet new classlib` template | Shared project | Yes | Included in SDK | -- |
| HTTPS dev certificate | Local HTTPS | Unknown | -- | Use `--no-https` flag or run `dotnet dev-certs https --trust` |
**Missing dependencies with no fallback:** None.
**Missing dependencies with fallback:**
- HTTPS dev certificate status is unknown -- if untrusted, can use HTTP for Phase 1 local dev or run the trust command.
## Open Questions
1. **Port numbers for local development**
- What we know: Templates assign ports in `launchSettings.json` which vary per creation
- What's unclear: Exact ports until projects are actually created
- Recommendation: After project creation, inspect both `launchSettings.json` files and align CORS config with actual client URL. Consider setting explicit predictable ports (e.g., Client: 5200, API: 7100).
2. **HTTPS vs HTTP for Phase 1 local dev**
- What we know: HTTPS requires a trusted dev certificate
- What's unclear: Whether the dev cert is already trusted on this machine
- Recommendation: Attempt HTTPS first. If certificate issues arise, fall back to HTTP for Phase 1 (add `--no-https` and use `http://` URLs). HTTPS can be re-enabled in later phases.
## Project Constraints (from CLAUDE.md)
- **Tech stack**: .NET / C# / Blazor WebAssembly -- non-negotiable
- **LLM provider**: OpenAI GPT API (not used in Phase 1, but architecture must support it)
- **Storage**: JSON files on local disk (not used in Phase 1, but architecture must support it)
- **Architecture**: WASM client + backend API (API key stays server-side)
- **Code style**: Every Blazor concept introduced must have inline comments explaining what it does and why
- **GSD Workflow**: Use GSD entry points for all file changes
## Sources
### Primary (HIGH confidence)
- .NET 9 SDK verified installed: `dotnet --list-sdks` shows 9.0.312
- `blazorwasm` template verified: `dotnet new list blazorwasm` confirms availability
- `webapi --use-controllers` flag verified: `dotnet new webapi --help` confirms option exists
- Template output verified: Created temporary projects to confirm `Program.cs` structure, `.csproj` contents, and default file layout
### Secondary (MEDIUM confidence)
- `.planning/research/ARCHITECTURE.md` -- pre-existing project research on WASM/API split patterns
- `.planning/research/STACK.md` -- pre-existing stack research with NuGet-verified versions
- `.planning/research/PITFALLS.md` -- pre-existing pitfalls research from official GitHub issues
### Tertiary (LOW confidence)
- None -- all findings verified against installed SDK and official templates
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH -- verified against installed SDK and templates
- Architecture: HIGH -- three-project structure is well-documented Microsoft pattern; template output confirmed
- Pitfalls: HIGH -- CORS ordering, port mismatch, and IL trimming are well-known ASP.NET Core issues documented in official sources
**Research date:** 2026-03-27
**Valid until:** 2026-04-27 (stable -- .NET 9 is mature, templates are not changing)

View File

@@ -0,0 +1,77 @@
---
phase: 1
slug: architecture-foundation
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-03-27
---
# Phase 1 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | dotnet test (built-in) |
| **Config file** | none — Wave 0 installs |
| **Quick run command** | `dotnet build` |
| **Full suite command** | `dotnet build && dotnet publish src/ChatAgent.Client/ChatAgent.Client.csproj` |
| **Estimated runtime** | ~15 seconds |
---
## Sampling Rate
- **After every task commit:** Run `dotnet build`
- **After every plan wave:** Run `dotnet build && dotnet publish src/ChatAgent.Client/ChatAgent.Client.csproj`
- **Before `/gsd:verify-work`:** Full suite must be green
- **Max feedback latency:** 15 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| 01-01-01 | 01 | 1 | CODE-01 | build | `dotnet build` | ❌ W0 | ⬜ pending |
| 01-01-02 | 01 | 1 | CODE-01 | build | `dotnet build` | ❌ W0 | ⬜ pending |
| 01-02-01 | 02 | 1 | CODE-02 | build+run | `dotnet build && dotnet run --project src/ChatAgent.Api` | ❌ W0 | ⬜ pending |
| 01-02-02 | 02 | 2 | CODE-02 | publish | `dotnet publish src/ChatAgent.Client/ChatAgent.Client.csproj` | ❌ W0 | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
- [ ] Solution and projects created via `dotnet new` commands
- [ ] All three projects buildable with `dotnet build`
*If none: "Existing infrastructure covers all phase requirements."*
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| CORS working end-to-end | CODE-01 | Requires both projects running simultaneously | Start API, start Client, verify client can reach API endpoint |
| Tutorial comments present | CODE-02 | Content quality check | Review each file for inline Blazor concept explanations |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 15s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending

Binary file not shown.

View File

@@ -0,0 +1,389 @@
# Architecture Research
**Domain:** Blazor WebAssembly AI Chat Application
**Researched:** 2026-03-27
**Confidence:** HIGH (Microsoft official docs + verified community patterns)
## Standard Architecture
### System Overview
```
┌──────────────────────────────────────────────────────────────────────┐
│ BROWSER (Blazor WASM) │
├──────────────────────────────────────────────────────────────────────┤
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────────┐ │
│ │ ChatPage │ │ ConvList │ │ MessageBubble │ │
│ │ (container) │ │ (sidebar) │ │ (leaf component) │ │
│ └───────┬────────┘ └───────┬────────┘ └────────────────────────┘ │
│ │ │ │
│ ┌───────▼──────────────────▼─────────────────────────────────────┐ │
│ │ ConversationStateService (Singleton DI) │ │
│ │ Holds: active conversation, message list, loading flag │ │
│ └───────┬────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────▼────────────────────────────────────────────────────────┐ │
│ │ ChatApiClient (HttpClient wrapper) │ │
│ │ POST /api/conversations, GET /api/stream?…, DELETE … │ │
│ └───────┬────────────────────────────────────────────────────────┘ │
└──────────┼─────────────────────────────────────────────────────────┘
│ HTTP / SSE (text/event-stream)
┌──────────▼─────────────────────────────────────────────────────────┐
│ ASP.NET Core Minimal API (Server) │
├─────────────────────────────────────────────────────────────────────┤
│ ┌──────────────────────┐ ┌──────────────────────────────────┐ │
│ │ ChatEndpoints │ │ ConversationEndpoints │ │
│ │ POST /api/chat │ │ GET/POST/DELETE /api/… │ │
│ │ GET /api/chat/… │ │ │ │
│ └──────────┬───────────┘ └───────────────┬──────────────────┘ │
│ │ │ │
│ ┌──────────▼───────────┐ ┌───────────────▼──────────────────┐ │
│ │ OpenAiService │ │ ConversationRepository │ │
│ │ (streams tokens via │ │ (reads/writes JSON files) │ │
│ │ openai-dotnet SDK) │ │ │ │
│ └──────────┬───────────┘ └───────────────┬──────────────────┘ │
│ │ │ │
├─────────────┼───────────────────────────────┼────────────────────────┤
│ │ HTTPS │ local disk │
│ ┌─────▼──────┐ ┌─────────▼──────────────────┐ │
│ │ OpenAI API │ │ ~/chat-data/ │ │
│ │ (GPT-4o) │ │ conversations/{id}.json │ │
│ └────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
### Component Responsibilities
| Component | Responsibility | Typical Implementation |
|-----------|----------------|------------------------|
| ChatPage | Top-level container — composes sidebar + chat panel, owns route | Blazor page component (`@page "/chat/{id?}"`) |
| ConversationList | Lists saved conversations, triggers create/delete/switch | Child component with EventCallback to parent |
| MessageList | Renders all messages in active conversation | Child component, iterates message model |
| MessageBubble | Renders single message — user vs AI, markdown for AI | Leaf component, uses Markdig or similar |
| ChatInput | Text area + send button, raises OnSend event | Child component with EventCallback |
| ConversationStateService | Singleton in-memory state — active conversation, messages, streaming flag | C# service registered `AddSingleton`, raises `OnChange` events |
| ChatApiClient | Wraps HttpClient, handles streaming plumbing | Scoped service, uses `SetBrowserResponseStreamingEnabled(true)` |
| ChatEndpoints | Minimal API: accepts message, streams SSE response from OpenAI | Static endpoint methods wired in `Program.cs` |
| ConversationEndpoints | Minimal API: CRUD for conversations | Static endpoint methods |
| OpenAiService | Calls OpenAI SDK, returns `IAsyncEnumerable<string>` of tokens | Scoped service on server |
| ConversationRepository | Read/write JSON files on disk | Singleton or Scoped service on server |
## Recommended Project Structure
```
ChatAgentWebApp/
├── ChatAgentWebApp.Client/ # Blazor WASM project
│ ├── Components/
│ │ ├── Chat/
│ │ │ ├── ChatPage.razor # Page — route entry point
│ │ │ ├── MessageList.razor # Renders message history
│ │ │ ├── MessageBubble.razor # Single message (user/AI)
│ │ │ └── ChatInput.razor # Text input + send
│ │ └── Conversations/
│ │ └── ConversationList.razor # Sidebar conversation switcher
│ ├── Services/
│ │ ├── ConversationStateService.cs # In-memory singleton state
│ │ └── ChatApiClient.cs # HttpClient wrapper + SSE reading
│ └── Program.cs # DI registration, HttpClient base URL
├── ChatAgentWebApp.Server/ # ASP.NET Core Minimal API project
│ ├── Endpoints/
│ │ ├── ChatEndpoints.cs # POST /api/chat/stream (SSE)
│ │ └── ConversationEndpoints.cs # GET/POST/DELETE /api/conversations
│ ├── Services/
│ │ ├── OpenAiService.cs # Wraps openai-dotnet SDK, yields tokens
│ │ └── ConversationRepository.cs # JSON file read/write
│ ├── Models/ # Server-only models (request/response)
│ └── Program.cs # Minimal API wiring, CORS, DI
└── ChatAgentWebApp.Shared/ # Shared library (both projects reference)
└── Models/
├── Conversation.cs # Shared model — id, title, createdAt
└── ChatMessage.cs # Shared model — role, content, timestamp
```
### Structure Rationale
- **Client/Services/:** All HttpClient wiring and streaming logic lives here, not in components. Components stay dumb (data in via parameters, actions out via EventCallback).
- **Shared/Models/:** Models used by both Client (display) and Server (serialization) live here. Eliminates duplicate DTOs.
- **Server/Endpoints/:** Minimal API endpoint registration separated by concern (chat streaming vs conversation CRUD). Keeps Program.cs clean.
- **Server/Services/:** OpenAI SDK calls and file I/O isolated from HTTP concerns. Enables testing without HTTP context.
## Architectural Patterns
### Pattern 1: SSE Streaming from Minimal API to WASM Client
**What:** The server endpoint writes `text/event-stream` frames to the response as OpenAI tokens arrive. The WASM client reads the response as a stream using `SetBrowserResponseStreamingEnabled(true)` and processes tokens without waiting for the full response.
**When to use:** Any time you need token-by-token streaming from an LLM. The alternative (wait for full response, then display) is noticeably worse UX for long answers.
**Trade-offs:** Slightly more plumbing than a simple JSON response. SSE is one-directional (server to client), which is fine here — the client sends the initial message as a POST, and the stream returns the reply.
**Server endpoint pattern:**
```csharp
// Server: ChatEndpoints.cs
app.MapPost("/api/chat/stream", async (ChatRequest request, OpenAiService ai,
ConversationRepository repo, HttpContext http) =>
{
http.Response.Headers.ContentType = "text/event-stream";
http.Response.Headers.CacheControl = "no-cache";
await foreach (var token in ai.StreamResponseAsync(request))
{
await http.Response.WriteAsync($"data: {token}\n\n");
await http.Response.Body.FlushAsync();
}
await http.Response.WriteAsync("event: done\ndata: end\n\n");
await repo.AppendMessageAsync(request.ConversationId, assistantMessage);
});
```
**Client consumption pattern:**
```csharp
// Client: ChatApiClient.cs
var req = new HttpRequestMessage(HttpMethod.Post, "/api/chat/stream");
req.SetBrowserResponseStreamingEnabled(true); // Critical for WASM
req.Content = JsonContent.Create(chatRequest);
var response = await _httpClient.SendAsync(req,
HttpCompletionOption.ResponseHeadersRead); // Don't buffer full body
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (line?.StartsWith("data: ") == true)
{
var token = line[6..];
_stateService.AppendToken(token);
await InvokeAsync(StateHasChanged); // Update UI per token
}
}
```
### Pattern 2: Singleton State Service as Shared State
**What:** A `ConversationStateService` registered as a singleton (in WASM, scoped = singleton anyway) holds the active conversation, message list, and streaming state. Components subscribe to an `OnChange` event to re-render when state updates.
**When to use:** When multiple components need to reflect the same data — the sidebar list, the message pane, and the input box all depend on the same active conversation.
**Trade-offs:** Simple and explicit. Not as formal as Redux/Flux but appropriate for single-user personal tool. Avoids prop-drilling through component hierarchies.
**Example:**
```csharp
// ConversationStateService.cs
public class ConversationStateService
{
public Conversation? ActiveConversation { get; private set; }
public List<ChatMessage> Messages { get; } = new();
public bool IsStreaming { get; private set; }
public event Action? OnChange; // Components subscribe to this
public void SetActive(Conversation conv)
{
ActiveConversation = conv;
Messages.Clear();
NotifyStateChanged();
}
public void AppendToken(string token)
{
// Append to last message (AI response being streamed)
Messages.Last().Content += token;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
```
### Pattern 3: Repository for JSON File Persistence
**What:** `ConversationRepository` encapsulates all file I/O. Each conversation is stored as `{id}.json` in a configured data directory. The repository loads/saves these files and maintains an in-memory index for listing.
**When to use:** Always — never write `File.ReadAll...` directly in endpoint handlers. Even for JSON files, the repository pattern keeps concerns separate and makes the storage medium swappable.
**Trade-offs:** Slight overhead vs inline file I/O. But it cleanly separates persistence from HTTP handling and makes unit testing possible without touching the filesystem.
```csharp
// ConversationRepository.cs
public class ConversationRepository
{
private readonly string _dataDir;
public async Task<List<Conversation>> GetAllAsync() { ... }
public async Task<Conversation?> GetByIdAsync(string id) { ... }
public async Task SaveAsync(Conversation conv) { ... }
public async Task DeleteAsync(string id) { ... }
public async Task AppendMessageAsync(string id, ChatMessage msg) { ... }
}
```
## Data Flow
### Sending a Message and Receiving a Streaming Response
```
[User types message + clicks Send]
[ChatInput.razor] → OnSend EventCallback
[ChatPage.razor] calls ConversationStateService.BeginStreaming()
[ChatApiClient.PostMessageStreamAsync()] — POST /api/chat/stream
[Server: ChatEndpoints] receives request
[OpenAiService.StreamResponseAsync()] → calls OpenAI GPT API
[OpenAI returns streaming response] — tokens arrive incrementally
[Server writes SSE frames] → "data: Hello\n\n", "data: world\n\n", ...
[Client StreamReader] reads lines as they arrive (ResponseHeadersRead)
[ConversationStateService.AppendToken()] mutates last message
[OnChange event fires] → all subscribed components call StateHasChanged()
[MessageList re-renders] — user sees tokens appearing in real time
[Server sends "event: done"] → client marks IsStreaming = false
[Server persists full response] → ConversationRepository.AppendMessageAsync()
```
### Conversation Management Flow
```
[User clicks "New Conversation"]
[ConversationList.razor] → EventCallback to ChatPage
[ChatApiClient.CreateConversationAsync()] → POST /api/conversations
[Server: ConversationEndpoints] → ConversationRepository.SaveAsync()
[Returns new Conversation object]
[ConversationStateService.SetActive(newConv)] → clears message list
[All components re-render] — empty chat ready for input
```
### State Management Summary
```
ConversationStateService (Singleton in WASM)
OnChange event
↓ (subscribed in OnInitialized, unsubscribed in Dispose)
[ChatPage] [MessageList] [ConversationList] [ChatInput]
Mutations via method calls (SetActive, AppendToken, SetStreaming)
Triggered by ChatApiClient responses
```
## Build Order Implications
Components have clear dependency layers. Build from the bottom up:
1. **Shared Models**`Conversation`, `ChatMessage` (no deps, both projects need these)
2. **ConversationRepository** — file I/O, no HTTP (testable in isolation)
3. **OpenAiService** — OpenAI SDK calls, yields `IAsyncEnumerable<string>`
4. **Server Endpoints** — wires services to HTTP (depends on 2 and 3)
5. **ChatApiClient** — WASM HTTP client + SSE consumer (depends on server being up)
6. **ConversationStateService** — in-memory state (depends on models, no HTTP)
7. **Leaf UI components**`MessageBubble`, `ChatInput`, `ConversationList` (pure display)
8. **Container components**`MessageList`, `ChatPage` (compose leaves, use state service)
This order maps naturally to build phases: backend first (phases 1-3), then state layer, then UI.
## Scaling Considerations
This is a single-user personal tool. Scaling is not a concern for v1.
| Scale | Architecture Adjustment |
|-------|--------------------------|
| 1 user (current) | Monolith fine, JSON files fine, no auth needed |
| Multi-user | Add auth, move to SQLite or Postgres, scope state service per user |
| Cloud deploy | Externalize API key via Azure Key Vault, containerize server |
### Scaling Priorities (if needed in v2+)
1. **First bottleneck:** JSON files don't support concurrent writes — add SQLite (EF Core migration is straightforward)
2. **Second bottleneck:** Single server process — Blazor WASM + separate API already decoupled; scale API independently
## Anti-Patterns
### Anti-Pattern 1: Calling OpenAI from the WASM Client
**What people do:** Register `HttpClient` in the Blazor WASM project and call `api.openai.com` directly.
**Why it's wrong:** The OpenAI API key is visible to anyone who opens browser DevTools. The key is in the `Authorization` header of every request.
**Do this instead:** All OpenAI calls go through the backend API. The server reads the key from `appsettings.json` or environment variables (server-side, never shipped to browser).
### Anti-Pattern 2: Buffering the Streaming Response
**What people do:** Call `await response.Content.ReadAsStringAsync()` after `SendAsync`, then parse the complete response.
**Why it's wrong:** In Blazor WASM, without `SetBrowserResponseStreamingEnabled(true)` and `ResponseHeadersRead`, the browser buffers the entire response before making any of it available. The UI shows nothing until the full AI response is complete — defeating the entire point of streaming.
**Do this instead:** Set `SetBrowserResponseStreamingEnabled(true)` on the `HttpRequestMessage` and use `HttpCompletionOption.ResponseHeadersRead` in `SendAsync`. Read the response as a stream line by line.
### Anti-Pattern 3: Calling StateHasChanged from a Background Thread
**What people do:** Mutate state and call `StateHasChanged()` directly inside async token-reading loops.
**Why it's wrong:** In Blazor WASM, this is mostly harmless today because WASM is single-threaded — but in .NET 10+, multi-threaded WASM is becoming a reality. The correct pattern also reads more clearly.
**Do this instead:** Use `await InvokeAsync(StateHasChanged)` when updating UI from within async callbacks or loops. This schedules re-render on the correct synchronization context and is safe across all hosting models.
### Anti-Pattern 4: Fat Components (Logic in Razor Files)
**What people do:** Put API call logic, JSON deserialization, and state mutation directly in `.razor` component code blocks.
**Why it's wrong:** The tutorial nature of this project means code must be readable. Logic buried in components is hard to explain, hard to test, and violates single responsibility. The builder is also learning Blazor patterns — fat components teach bad habits.
**Do this instead:** Components only call services. Services own all logic. This also demonstrates the Blazor DI pattern explicitly, which is a key learning objective.
## Integration Points
### External Services
| Service | Integration Pattern | Notes |
|---------|---------------------|-------|
| OpenAI GPT API | `openai-dotnet` SDK on server, `IAsyncEnumerable<string>` returned | Never from WASM client. Key in `appsettings.json` on server. |
| Browser FileSystem | None — all persistence is server-side JSON files | WASM cannot write to local disk; server has full file access |
### Internal Boundaries
| Boundary | Communication | Notes |
|----------|---------------|-------|
| WASM Client ↔ API Server | HTTP / SSE via `HttpClient` | Configure base URL + CORS. During development, server serves client static files (hosted model). |
| ChatPage ↔ Child Components | Blazor parameters + EventCallback | Downward via `[Parameter]`, upward via `EventCallback<T>` |
| Components ↔ State Service | Injected singleton, `OnChange` event subscription | Components subscribe in `OnInitialized`, unsubscribe in `IDisposable.Dispose` |
| Endpoints ↔ Services | Constructor DI | Both `OpenAiService` and `ConversationRepository` injected into endpoint handlers |
| OpenAiService ↔ ConversationRepository | No direct coupling | Endpoint coordinates both — calls AI service, then persists to repo |
## Sources
- [Microsoft Docs: Call a web API from Blazor (aspnetcore-10.0)](https://learn.microsoft.com/en-us/aspnet/core/blazor/call-web-api?view=aspnetcore-10.0)
- [Meziantou: Streaming an HTTP response in Blazor WebAssembly](https://www.meziantou.net/streaming-an-http-response-in-blazor-webassembly.htm)
- [Strathweb: Built-in support for Server Sent Events in .NET 9](https://www.strathweb.com/2024/07/built-in-support-for-server-sent-events-in-net-9/)
- [Petkir: Stream chat to your frontend with SSE in ASP.NET Core (.NET 10)](https://www.petkir.at/blog/semantic-kernel/01_chat_03_sse)
- [openai/openai-dotnet issue #65: Streaming doesn't work properly in Blazor WASM](https://github.com/openai/openai-dotnet/issues/65)
- [Microsoft Docs: Blazor project structure](https://learn.microsoft.com/en-us/aspnet/core/blazor/project-structure?view=aspnetcore-10.0)
- [Microsoft Docs: Blazor state management](https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management/?view=aspnetcore-10.0)
- [Syncfusion: MVVM Pattern in Blazor For State Management](https://www.syncfusion.com/blogs/post/mvvm-pattern-blazor-state-management)
- [PalmHill.BlazorChat — reference implementation (WASM + WebAPI + real-time LLM)](https://github.com/edgett/PalmHill.BlazorChat)
---
*Architecture research for: Blazor WebAssembly AI Chat Application*
*Researched: 2026-03-27*

View File

@@ -0,0 +1,202 @@
# Feature Research
**Domain:** Personal AI chat web application (single-user, OpenAI GPT backend, Blazor WebAssembly)
**Researched:** 2026-03-27
**Confidence:** HIGH (core features verified against live ChatGPT/Claude, OpenAI API docs, and Blazor ecosystem)
## Feature Landscape
### Table Stakes (Users Expect These)
Features users assume exist. Missing these = product feels incomplete.
| Feature | Why Expected | Complexity | Notes |
|---------|--------------|------------|-------|
| Send message and receive response | Core function of any AI chat app | LOW | POST to backend API; backend calls OpenAI chat completions endpoint |
| Streaming token-by-token responses | ChatGPT normalized this; blocking responses feel broken | MEDIUM | Server-Sent Events (SSE) from backend; HttpClient streaming or SignalR on WASM client; creates "typewriter" effect |
| Markdown rendering in AI responses | GPT always responds with markdown; raw markdown is unreadable | MEDIUM | Markdig library on backend or client; MarkupString in Blazor to render HTML; Markdown.ColorCode for syntax highlighting |
| Syntax-highlighted code blocks | Code responses are a primary GPT use-case; unformatted code is unusable | MEDIUM | Markdown.ColorCode NuGet package; note: CsharpToColouredHTML has WASM compatibility issues — use base ColorCode package |
| Copy-to-clipboard on code blocks | Standard expectation from ChatGPT/Claude; users paste code constantly | LOW | JavaScript interop in Blazor (`navigator.clipboard.writeText`); small JS interop call |
| Multiple named conversations | Users need to separate topics; single-thread apps feel like a toy | MEDIUM | Conversation list in sidebar; each conversation has ID, title, message list; JSON file per conversation |
| Create and switch between conversations | Navigation between conversations is core workflow | LOW | Once multi-conversation storage exists, switching is just loading by ID |
| Delete conversations | Users need to clean up; no delete = clutter accumulates | LOW | Remove JSON file; update conversation list |
| Persist conversation history across sessions | Without persistence, app is useless after refresh | MEDIUM | JSON file storage on disk; load on startup; save on every message |
| Auto-scroll to latest message | Standard chat behavior; missing it feels broken | LOW | JavaScript interop to scroll div; or CSS scroll-behavior |
| Loading/thinking indicator | Users need feedback that a request is in-flight | LOW | Show spinner or "..." while awaiting first token; hide once streaming starts |
| Input disabled during response | Prevent double-submit while response is streaming | LOW | Boolean state flag; disable textarea and button while `isStreaming = true` |
| Send on Enter key | Standard text input convention for chat | LOW | `@onkeydown` handler; Shift+Enter for newline |
| Responsive layout | Mobile-friendly is expected even for personal tools | LOW | CSS flexbox/grid; sidebar collapses on small screens |
### Differentiators (Competitive Advantage)
Features that set the product apart. Not required, but valuable.
| Feature | Value Proposition | Complexity | Notes |
|---------|-------------------|------------|-------|
| Auto-generated conversation titles | Reduces naming friction; GPT can summarize first message as title | LOW | Call GPT with "Summarize this in 5 words" after first exchange; update conversation title |
| System prompt / persona configuration | Power users want to customize GPT behavior per conversation | MEDIUM | Add `systemPrompt` field to conversation model; include as first message in API payload |
| Message edit and regenerate | Fix typos without starting over; common in ChatGPT | MEDIUM | Truncate conversation at edited message; resend; requires re-streaming |
| Token usage display | Helps users understand context window consumption; teaches GPT behavior | LOW | OpenAI API returns usage in response; display in footer or message metadata |
| Conversation search | Find a past conversation by keyword | MEDIUM | Client-side search over loaded conversation titles; full-text needs indexing |
| Export conversation | Save as markdown or text file | LOW | Serialize messages to markdown string; trigger browser download via JS interop |
| Model selector (GPT-4o vs GPT-4o-mini) | Cost vs quality tradeoff is real; power users want control | LOW | Dropdown stored in app settings or per-conversation; passed as `model` parameter in API call |
| Well-commented tutorial-style code | The project doubles as a Blazor learning resource | LOW (implementation cost) | Inline `// Blazor:` comments on lifecycle hooks, DI, component patterns — this is a core differentiator for this specific project's purpose |
### Anti-Features (Commonly Requested, Often Problematic)
Features that seem good but create problems.
| Feature | Why Requested | Why Problematic | Alternative |
|---------|---------------|-----------------|-------------|
| Authentication / login | "What if someone else uses it?" | Single-user personal tool; adds OAuth complexity with zero value | Leave open; document that it's intentionally single-user |
| Database (SQL/SQLite) | "JSON doesn't scale" | Premature optimization for a personal tool; adds EF Core migration complexity | JSON files are fast, human-readable, and zero-setup — perfect for this scope |
| Real-time sync across tabs | "What if I have two windows open?" | SignalR state sync complexity; no real use case for single user | Reload on focus; acceptable for personal tool |
| Plugin / tool calling system | "GPT can call functions!" | That's LangChain/MCP territory; v2 scope — building it now adds architecture complexity before core chat works | Defer to v2 milestone with LangChain and MCP servers |
| Voice input / output | ChatGPT has it | OpenAI Realtime API is being deprecated May 2026; adds Web Speech API complexity; out of scope | Text-only for v1 |
| Image uploads / multimodal | GPT-4o supports it | WASM file upload + base64 encoding + vision API adds significant complexity | Text chat first; defer multimodal to v2 |
| Conversation branching | "What if I want to explore different answers?" | Complex tree data structure; confusing UX; rare real-world use | Regenerate last response is sufficient for 95% of cases |
| Infinite scroll / lazy loading | "What about long conversations?" | Adds virtual scrolling complexity; JSON load is fine at personal scale | Load full conversation on select; revisit if performance suffers |
| PWA / offline support | "Make it installable" | Service worker complexity; AI chat requires internet anyway | Responsive web design is sufficient |
## Feature Dependencies
```
[JSON File Storage]
└──required by──> [Multiple Conversations]
└──required by──> [Create / Switch / Delete Conversations]
└──required by──> [Persist History Across Sessions]
[OpenAI API Call (blocking)]
└──required by──> [Streaming Responses]
└──required by──> [Loading Indicator]
└──required by──> [Input Disabled During Streaming]
[Markdown Rendering]
└──required by──> [Syntax-Highlighted Code Blocks]
└──required by──> [Copy-to-Clipboard on Code Blocks]
[Streaming Responses] ──enhances──> [Auto-Scroll to Latest Message]
[Multiple Conversations] ──enables──> [Auto-Generated Titles]
[Multiple Conversations] ──enables──> [Conversation Search]
[Multiple Conversations] ──enables──> [Export Conversation]
[System Prompt Config] ──enhances──> [Multiple Conversations]
(each conversation can have its own persona)
[Token Usage Display] ──conflicts with [Streaming]
(usage metadata only available when stream=false or in the final chunk)
```
### Dependency Notes
- **JSON File Storage required by Multiple Conversations:** Conversations need somewhere to live before the list/switch UI can be built. Storage phase must precede conversation management phase.
- **Blocking API call required by Streaming:** Must implement the non-streaming call first to understand the request/response shape, then layer SSE streaming on top.
- **Markdown Rendering required by Syntax Highlighting:** ColorCode is a Markdig pipeline extension — Markdig must be wired in before syntax highlighting can be added.
- **Token Usage Display conflicts with Streaming:** OpenAI streams individual content chunks; the `usage` field only appears in the final chunk (`finish_reason: stop`). Implementation must capture the last chunk separately.
## MVP Definition
### Launch With (v1)
Minimum viable product — what's needed to validate the concept.
- [ ] Send message, receive non-streaming GPT response — validates API connectivity and basic loop
- [ ] Streaming responses — core UX differentiator; blocking responses feel unacceptable
- [ ] Markdown rendering with syntax highlighting — GPT responses are markdown; unrendered output is unusable
- [ ] Create / switch / delete multiple conversations — without this, the app is a single disposable thread
- [ ] JSON file persistence — conversations must survive page refresh to be useful
- [ ] Auto-scroll and loading indicator — baseline polish that makes the app feel complete
- [ ] Copy-to-clipboard on code blocks — high-frequency action for developer-focused use
- [ ] Tutorial-style inline code comments — this project's defining purpose as a learning resource
### Add After Validation (v1.x)
Features to add once core is working.
- [ ] Auto-generated conversation titles — reduces friction once the core loop is validated
- [ ] System prompt / persona configuration — natural extension once multi-conversation is stable
- [ ] Model selector — easy add once API layer is clean; real value for cost control
- [ ] Export conversation — low complexity, high occasional value
### Future Consideration (v2+)
Features to defer until product-market fit is established.
- [ ] Message edit and regenerate — medium complexity; wait until core loop is solid
- [ ] Token usage display — useful but not blocking; needs streaming completion handling
- [ ] Conversation search — only valuable when there are many conversations to search
- [ ] LangChain / agentic workflows — explicitly v2 scope per PROJECT.md
- [ ] RAG document retrieval — v2 scope
- [ ] MCP server integration — v2 scope
## Feature Prioritization Matrix
| Feature | User Value | Implementation Cost | Priority |
|---------|------------|---------------------|----------|
| Send message / receive response | HIGH | LOW | P1 |
| Streaming responses | HIGH | MEDIUM | P1 |
| Markdown + syntax highlighting | HIGH | MEDIUM | P1 |
| Multiple conversations + persistence | HIGH | MEDIUM | P1 |
| Auto-scroll + loading indicator | HIGH | LOW | P1 |
| Copy code to clipboard | HIGH | LOW | P1 |
| Tutorial-style code comments | HIGH (for this project) | LOW | P1 |
| Auto-generated conversation titles | MEDIUM | LOW | P2 |
| System prompt configuration | MEDIUM | MEDIUM | P2 |
| Model selector | MEDIUM | LOW | P2 |
| Export conversation | LOW | LOW | P2 |
| Token usage display | LOW | LOW | P2 |
| Message edit and regenerate | MEDIUM | MEDIUM | P3 |
| Conversation search | LOW | MEDIUM | P3 |
**Priority key:**
- P1: Must have for launch
- P2: Should have, add when possible
- P3: Nice to have, future consideration
## Competitor Feature Analysis
| Feature | ChatGPT (OpenAI) | Claude (Anthropic) | This Project |
|---------|------------------|--------------------|--------------|
| Streaming responses | Yes, token-by-token | Yes, token-by-token | Yes — SSE via backend API |
| Markdown rendering | Yes | Yes | Yes — Markdig + MarkupString |
| Syntax highlighted code | Yes + copy button | Yes + copy button | Yes — Markdown.ColorCode |
| Multiple conversations (sidebar) | Yes | Yes (Projects) | Yes — JSON file per conversation |
| Conversation persistence | Yes (cloud) | Yes (cloud) | Yes — local JSON files |
| Auto-generated titles | Yes | Yes | v1.x — GPT summarization call |
| System prompt | Via custom instructions | Via system prompt | v1.x — per-conversation field |
| Model selector | Yes (GPT-5.4 variants) | Yes (Opus/Sonnet/Haiku) | v1.x — GPT-4o vs GPT-4o-mini |
| Voice input/output | Yes (Advanced Voice Mode) | No | Deliberately excluded from v1 |
| Image uploads | Yes (multimodal) | Yes (multimodal) | Deliberately excluded from v1 |
| Plugin / tool calling | Yes (via GPT Actions) | Yes (via tool use) | v2 — MCP servers |
| RAG / document search | Yes (file attachments) | Yes (Projects + files) | v2 — RAG milestone |
| Auth / multi-user | Yes | Yes | Deliberately excluded (single user) |
## Blazor-Specific Implementation Notes
These are not features per se, but implementation constraints that affect feature complexity in Blazor WebAssembly:
- **JavaScript Interop is required for:** clipboard access, scroll-to-bottom, syntax highlighting via client-side JS libraries (Highlight.js alternative to server-side ColorCode)
- **API key must never reach WASM client:** all OpenAI calls must go through the ASP.NET Core backend API — the WASM client calls the backend, the backend calls OpenAI
- **Streaming from backend to WASM client:** options are SSE (Server-Sent Events via `HttpClient` streaming) or SignalR HubConnection; SSE is simpler for one-way server-to-client streaming; SignalR is better if bidirectional messaging is needed later
- **MarkupString in Blazor:** required to render HTML from Markdig; must be used intentionally as it bypasses Blazor's XSS protections — only render trusted content (GPT output is untrusted; sanitize or accept risk as single-user personal tool)
- **Markdown.ColorCode WASM note:** base `Markdown.ColorCode` package works in WASM; `Markdown.ColorCode.CSharpToColoredHtml` does NOT — avoid the latter
## Sources
- [ChatGPT vs Claude feature comparison 2026 — LogicWeb](https://www.logicweb.com/chatgpt-vs-claude-ultimate-ai-comparison-in-2026/)
- [OpenAI Streaming API documentation](https://platform.openai.com/docs/api-reference/chat/streaming)
- [OpenAI Streaming Responses Guide](https://developers.openai.com/api/docs/guides/streaming-responses)
- [OpenAI Conversation State Guide](https://platform.openai.com/docs/guides/conversation-state)
- [Best practices for OpenAI Chat streaming UI — Pamela Fox](http://blog.pamelafox.org/2023/09/best-practices-for-openai-chat-apps_16.html)
- [PalmHill.BlazorChat — Blazor WASM + LLM reference implementation](https://github.com/edgett/PalmHill.BlazorChat)
- [Blazor Live Preview Markdown with Markdig — Syncfusion](https://www.syncfusion.com/blogs/post/blazor-live-preview-markdown-editors-content-using-markdig-library)
- [Markdown.ColorCode NuGet package](https://www.nuget.org/packages/Markdown.ColorCode)
- [16 Chat UI Design Patterns 2025](https://bricxlabs.com/articles/message-screen-ui-deisgn)
- [AI Chat Interface UX Patterns — UXPatterns.dev](https://uxpatterns.dev/patterns/ai-intelligence/ai-chat)
- [Conversational AI UI Comparison 2025 — IntuitionLabs](https://intuitionlabs.ai/articles/conversational-ai-ui-comparison-2025)
- [Token management best practices — OpenAI Community](https://community.openai.com/t/best-practices-for-cost-efficient-high-quality-context-management-in-long-ai-chats/1373996)
---
*Feature research for: Personal AI Chat WebApp (Blazor WebAssembly + OpenAI GPT)*
*Researched: 2026-03-27*

View File

@@ -0,0 +1,302 @@
# Pitfalls Research
**Domain:** Blazor WebAssembly AI Chat Application (OpenAI GPT, JSON storage, streaming)
**Researched:** 2026-03-27
**Confidence:** HIGH (multiple authoritative sources: official GitHub issues, Microsoft docs, verified community findings)
---
## Critical Pitfalls
### Pitfall 1: Streaming Silently Broken in Blazor WASM Without Custom Transport
**What goes wrong:**
OpenAI's .NET SDK streaming (`CompleteChatStreamingAsync`, returning `IAsyncEnumerable`) does not stream token-by-token in Blazor WASM. The entire response arrives at once after generation completes, making it appear like a non-streaming call. There is no error thrown — it just does not stream. This is confirmed in the official `openai-dotnet` GitHub issue tracker (#65).
**Why it happens:**
Blazor WASM uses a browser-based `HttpClient` backed by the Fetch API via JS interop. Response streaming requires explicitly calling `SetBrowserResponseStreamingEnabled(true)` on the underlying `HttpRequestMessage`. The OpenAI .NET SDK does not set this flag by default. Without it, the browser buffers the entire response body before exposing it to the .NET layer.
**How to avoid:**
Create a custom `HttpClientPipelineTransport` that overrides `OnSendingRequest` to enable browser streaming:
```csharp
public class BlazorHttpClientTransport : HttpClientPipelineTransport
{
protected override void OnSendingRequest(
PipelineMessage message,
HttpRequestMessage httpRequest)
{
httpRequest.SetBrowserResponseStreamingEnabled(true);
}
}
// Wire up at client construction:
var options = new OpenAIClientOptions();
options.Transport = new BlazorHttpClientTransport();
var chatClient = new ChatClient(model: "gpt-4o", apiKey, options);
```
This must be done server-side (in the backend API that proxies to OpenAI), not in the WASM client directly.
**Warning signs:**
- Tokens appear all at once after a delay instead of progressively
- No console errors — it looks like it is working but is not streaming
- Local dev works "fine" because the full response still arrives, just not incrementally
**Phase to address:**
Phase that introduces streaming (SSE/token-by-token rendering). Must be addressed before any streaming demo is built.
---
### Pitfall 2: OpenAI API Key Exposed in Blazor WASM Client
**What goes wrong:**
A developer puts the OpenAI API key in `wwwroot/appsettings.json` or reads it from `IConfiguration` in a WASM component. The key is then downloadable by any browser visitor by requesting `/_framework/blazor.boot.json` or simply navigating to `wwwroot/appsettings.json` directly. The key is burned.
**Why it happens:**
Experienced C# developers coming from ASP.NET Core or console apps expect `appsettings.json` and `IConfiguration` to be server-side. In Blazor WASM, `wwwroot/appsettings.json` is a static file served to the browser — it is not protected in any way. User Secrets also do not help: they are embedded into the published bundle in plaintext.
**How to avoid:**
The OpenAI API key must live exclusively in the backend API (ASP.NET Core Minimal API or Web API project). The WASM client calls a backend endpoint (e.g., `/api/chat`) that holds the key and proxies requests to OpenAI. The key never appears in any client-side file or JS bundle. Use `dotnet user-secrets` on the server project only.
**Warning signs:**
- Any `IConfiguration["OpenAI:ApiKey"]` usage in a `.razor` file or service registered in the WASM `Program.cs`
- `appsettings.json` in `wwwroot/` containing any token, key, or secret
- The word `sk-` visible in browser DevTools → Network → response body for any `.json` file
**Phase to address:**
Phase 1 (project setup / architecture foundation). This must be locked in before any OpenAI code is written.
---
### Pitfall 3: Scoped DI Services Act as Singletons in WASM — State Leaks Across Conversations
**What goes wrong:**
A developer registers a `ConversationService` or `ChatStateService` as `Scoped`, expecting it to reset between logical "sessions" like it would in ASP.NET Core (per-request). In Blazor WASM there is exactly one DI scope for the lifetime of the browser tab. The service never resets. All conversations accumulate state in a single object, producing corrupted cross-conversation history.
**Why it happens:**
In ASP.NET Core, Scoped = per HTTP request. In Blazor WASM, Scoped = per application lifetime (equivalent to Singleton). There is no shorter-lived scope unless you use `OwningComponentBase`. Developers familiar with server-side DI expect different behavior.
**How to avoid:**
- Understand that in WASM, `Scoped` and `Singleton` are functionally identical
- For services that manage per-conversation state, design them to hold a collection keyed by conversation ID rather than holding mutable "current conversation" state
- If a service must be component-scoped, inherit from `OwningComponentBase` which creates a DI scope tied to the component's lifetime
- Never store mutable "active session" state in a scoped/singleton service; store a dictionary of `ConversationId → ConversationState`
**Warning signs:**
- Switching conversations causes the wrong history to appear
- Deleting a conversation does not fully clear its state from memory
- Services have fields like `CurrentConversation` or `ActiveMessages` rather than `Dictionary<Guid, Conversation>`
**Phase to address:**
Phase introducing conversation state management and multi-conversation switching.
---
### Pitfall 4: `StateHasChanged` Not Called During Token Streaming — UI Freezes Until Completion
**What goes wrong:**
The developer wires up streaming correctly (transport fixed, backend proxying works) but the UI does not update token-by-token. The message bubble stays empty until all tokens arrive, then the entire response appears at once. This is indistinguishable from the streaming transport bug (Pitfall 1) if not diagnosed carefully.
**Why it happens:**
Blazor does not automatically re-render after every `await` inside an `async` event handler or lifecycle method. When consuming an `IAsyncEnumerable<string>` (streaming tokens), the component must explicitly call `StateHasChanged()` after appending each token. Without this call, Blazor batches rendering and only repaints when the entire method completes.
**How to avoid:**
```csharp
await foreach (var token in streamingResponse)
{
currentMessage += token;
StateHasChanged(); // required — Blazor will not re-render otherwise
await Task.Yield(); // prevents UI thread starvation on rapid token delivery
}
```
Additionally, consider throttling `StateHasChanged` calls (e.g., every 50ms or every N tokens) to avoid excessive rendering if token delivery is very fast.
**Warning signs:**
- Streaming transport is confirmed working (via backend logs) but UI still shows nothing until complete
- Token-by-token updates visible in server logs but not in the browser
- Removing `await Task.Yield()` causes the browser tab to become unresponsive during streaming
**Phase to address:**
Streaming UI rendering phase. Document this explicitly inline in the component code.
---
### Pitfall 5: JSON File Storage Architecture Assumes Server Filesystem — Not Viable Pure Client-Side
**What goes wrong:**
A developer writes file I/O code (`File.ReadAllText`, `File.WriteAllText`) directly in the WASM project. The code compiles without error but throws at runtime because Blazor WASM runs in a browser sandbox with no access to the host filesystem. The virtual WASM filesystem resets on every page refresh.
**Why it happens:**
C# file APIs exist in the WASM .NET runtime but map to an in-memory virtual filesystem, not the OS disk. Developers coming from console or desktop C# assume `File.WriteAllText("conversations.json", json)` writes to disk. It does not — the data vanishes on refresh.
**How to avoid:**
JSON file storage must live in the backend API (server-side), not in the WASM client. The correct architecture:
- WASM client calls `POST /api/conversations` → backend writes JSON to disk
- WASM client calls `GET /api/conversations` → backend reads JSON from disk and returns it
- Backend stores files in a configurable local path (e.g., `~/chat-data/`)
This reinforces the same architectural boundary required for API key protection (Pitfall 2).
**Warning signs:**
- Any `System.IO.File` or `System.IO.Directory` usage inside the WASM project (`Client/`)
- Conversations persist during a session but disappear on browser refresh
- Data is present in the WASM virtual FS (`MemoryFileSystem`) but absent from the OS
**Phase to address:**
Phase 1 (architecture setup). The WASM/backend split must be established before any persistence code is written.
---
### Pitfall 6: IL Trimming Silently Breaks Code in Release Builds
**What goes wrong:**
The app works perfectly in `dotnet run` (Debug) but breaks in `dotnet publish` (Release). JSON serialization loses properties, services cannot be resolved, or features silently stop working. No exceptions in development, cryptic failures in production.
**Why it happens:**
Blazor WASM uses aggressive IL trimming (ILLink) during publish to reduce bundle size. The trimmer performs static analysis and removes types/methods that appear unreachable — including types used only via reflection (JSON serialization, DI, JSInterop callbacks). Debug builds do not trim.
**How to avoid:**
- Use `[JsonSerializable]` with `System.Text.Json` source generation for all DTO types
- Apply `[DynamicDependency]` to methods called via reflection
- Apply `[JSInvokable]` to all methods callable from JavaScript
- Run `dotnet publish` early in the project (Phase 1 or 2) to detect trim warnings while the surface is small
- Treat `<TrimmerRootDescriptor>` as a last resort, not a first step
**Warning signs:**
- App works in `dotnet run` but throws `NullReferenceException` or loses data after `dotnet publish`
- JSON responses missing properties that were present in debug
- IL trimmer warnings during publish that were ignored
**Phase to address:**
Phase 1 (publish pipeline verification). Also Phase covering JSON data models for conversations.
---
## Technical Debt Patterns
| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable |
|----------|-------------------|----------------|-----------------|
| Put API key in WASM `appsettings.json` | Faster to get OpenAI call working | Key is permanently burned; must rotate | Never |
| Call OpenAI directly from WASM HttpClient | Eliminates backend project | API key exposed; no server-side rate limiting; blocks v2 RAG/LangChain which needs server | Never |
| Write file I/O in WASM project | Familiar C# patterns | Silent data loss on refresh; hard to migrate later | Never |
| All logic in `.razor` files | Faster iteration in early phases | Untestable; components become unmaintainable; hard to add v2 agent layer | Phase 1 only, refactor before Phase 3 |
| Single `ChatService` singleton holding all state | Simple to start | State leaks across conversations; breaks multi-conversation feature | Never in this project — multi-conversation is a core requirement |
| Skip `StateHasChanged` calls during streaming | Code is simpler | UI appears broken; streaming appears non-functional | Never |
| Skip IL trim testing until "done" | Saves time during early phases | Trim bugs compound as codebase grows | Acceptable in Phase 1-2 if `dotnet publish` test is added to Phase 3 |
---
## Integration Gotchas
| Integration | Common Mistake | Correct Approach |
|-------------|----------------|-----------------|
| OpenAI .NET SDK + Blazor WASM | Using SDK directly in WASM project without custom transport — streaming silently broken | Custom `BlazorHttpClientTransport` with `SetBrowserResponseStreamingEnabled(true)` on backend; or server-side only call |
| OpenAI streaming via backend proxy | Backend returns full `StreamingChatCompletionUpdate` objects — client gets batched response | Backend uses `IAsyncEnumerable` with `[EnumeratorCancellation]`; streams SSE or NDJSON to client |
| CORS between WASM client and backend API | Forgetting `AddCors` + `UseCors` on backend; 403/CORS errors during local dev | Configure CORS policy explicitly in backend `Program.cs`; scope to localhost dev origins |
| JSON serialization of conversation models | Properties stripped by trimmer in Release; conversation history loses fields | Use `System.Text.Json` source generators with `[JsonSerializable]` for all model types |
| Markdown rendering (AI responses) | Using `MarkupString` with raw AI output — XSS risk if AI returns script tags | Use a library like `Markdig` server-side, or a client-side library with HTML sanitization enabled |
---
## Performance Traps
| Trap | Symptoms | Prevention | When It Breaks |
|------|----------|------------|----------------|
| Calling `StateHasChanged` on every token without throttling | Browser tab becomes unresponsive during fast streaming; CPU spikes | Throttle to every 50ms or every N tokens using a timer or counter | At ~20+ tokens/second (typical GPT-4o speed) |
| Re-rendering entire conversation list on every message append | Visible flicker; full list DOM re-created on each token | Use `@key` directive on conversation list items; isolate streaming component from sidebar | At 5+ conversations in the list |
| Loading all conversation history on app start | Slow initial load for users with many old conversations | Lazy-load conversation content; sidebar shows metadata only; load full messages on selection | At 50+ conversations stored in JSON |
| Excessive component nesting for chat messages | Sluggish scroll performance with many messages | Keep message list in a single component with virtualization (`Virtualize`) for long histories | At 200+ messages in a single conversation |
---
## Security Mistakes
| Mistake | Risk | Prevention |
|---------|------|------------|
| OpenAI API key in `wwwroot/appsettings.json` | Key visible to any browser user; unlimited API charges | Key lives only in backend server project; accessed via `dotnet user-secrets` or environment variable |
| Calling OpenAI API directly from WASM (no backend) | Same as above; also bypasses rate limiting and logging | Mandatory backend proxy — this is enforced by the architecture from Phase 1 |
| Rendering AI response HTML without sanitization | AI model could produce `<script>` tags; XSS attack on self | Use Markdown-to-HTML library with HTML sanitization; never use raw `MarkupString` on LLM output |
| Storing secrets in WASM `IConfiguration` | Even runtime config values in WASM are readable from browser | All secrets in server-side `IConfiguration` only; WASM config contains only public values (API endpoint URLs) |
| CORS wildcard (`AllowAnyOrigin`) in backend | Allows any site to call the local chat backend | Restrict CORS to `localhost` origins during development; lock down on deploy |
---
## UX Pitfalls
| Pitfall | User Impact | Better Approach |
|---------|-------------|-----------------|
| No loading indicator while waiting for first streaming token | App appears frozen; user thinks it broke | Show typing indicator / spinner immediately on message send; hide when first token arrives |
| No way to cancel an in-progress stream | User must wait for full response even if they sent the wrong message | Pass `CancellationToken` through to streaming call; expose cancel button that calls `cts.Cancel()` |
| Auto-scroll that fights user scroll position | User scrolls up to read history; app violently scrolls back to bottom on each token | Only auto-scroll if the user is already at the bottom; detect scroll position before each `StateHasChanged` |
| No error message when OpenAI API call fails | Empty response bubble; user does not know what happened | `try/catch` around all API calls; display inline error in message bubble with retry option |
| Conversation list has no visual indication of active conversation | User loses track of which conversation is displayed | Highlight active conversation item; update `document.title` with conversation name |
| Markdown rendered as raw text | AI responses with code blocks and lists look like symbol-laden garbage | Wire up Markdown renderer before any AI content is displayed — do not defer this |
---
## "Looks Done But Isn't" Checklist
- [ ] **Streaming:** Tokens appear progressively in the UI — verify this is actual token-by-token delivery, not a batched response that arrives quickly. Check with a slow prompt.
- [ ] **API key security:** Open DevTools → Network → find any `.json` request → confirm no `sk-` prefixed values appear in any response body.
- [ ] **Conversation persistence:** Close and reopen the browser tab (not just refresh). Confirm conversations are still present.
- [ ] **Multi-conversation isolation:** Open conversation A, send a message. Switch to conversation B. Verify conversation A's messages do not bleed into B.
- [ ] **Stream cancellation:** Start a long generation. Click cancel or navigate away. Confirm the backend stops consuming tokens (check backend logs).
- [ ] **Release build:** Run `dotnet publish` at least once before calling any phase "done." Confirm the published app loads and all features work.
- [ ] **Error handling:** Temporarily set an invalid API key. Confirm the UI shows a user-friendly error rather than a blank component or silent failure.
- [ ] **Markdown rendering:** Ask the AI for a code snippet. Confirm it renders in a code block, not as raw backtick-surrounded text.
---
## Recovery Strategies
| Pitfall | Recovery Cost | Recovery Steps |
|---------|---------------|----------------|
| API key exposed in WASM | HIGH | Immediately rotate key in OpenAI dashboard; add `Client/` project scan to CI to block any future secret patterns |
| All logic in `.razor` files (discovered late) | MEDIUM | Extract services incrementally — one component per PR; do not refactor all at once |
| File I/O written in WASM project | MEDIUM | Move all persistence calls to backend API; update WASM to use `HttpClient` calls to new endpoints |
| Streaming not working (transport not set) | LOW | Add `BlazorHttpClientTransport` wrapper — 10-line fix once identified; the hard part is diagnosing it |
| Scoped service holding mutable conversation state | MEDIUM | Redesign service to hold `Dictionary<Guid, ConversationState>`; update all call sites |
| IL trim breaks release build | MEDIUMHIGH (depends on when discovered) | Add source generators for all model types; treat every trim warning as a compile error |
---
## Pitfall-to-Phase Mapping
| Pitfall | Prevention Phase | Verification |
|---------|------------------|--------------|
| API key in WASM client | Phase 1: Project setup and architecture | Grep WASM project for `sk-`; confirm key only in backend `user-secrets` |
| File I/O in WASM project | Phase 1: Project setup and architecture | Grep WASM project for `System.IO.File`; confirm persistence is backend-only |
| Direct OpenAI call from WASM | Phase 1: Project setup and architecture | Confirm no OpenAI SDK registration in WASM `Program.cs` |
| CORS misconfiguration | Phase 1: First HTTP call from WASM to backend | Verify browser console shows no CORS errors during local dev |
| Scoped DI lifetime confusion | Phase covering conversation state management | Test: create two conversations; switch between them; verify history isolation |
| Streaming transport not set | Phase introducing token-by-token streaming | Observe: tokens must appear incrementally; verify with slow prompt or network throttle |
| StateHasChanged missing in stream loop | Phase introducing token-by-token streaming | Same as above — visible as UI not updating mid-stream |
| UI freeze without streaming throttle | Phase polishing streaming UI | CPU profiler during active stream; verify no jank |
| IL trimming breaks release | Phase 1 (publish test) + any phase adding new model types | Run `dotnet publish` as part of each phase completion check |
| Markdown XSS via raw MarkupString | Phase introducing Markdown rendering | Code review: confirm no `new MarkupString(aiResponse)` without prior sanitization |
| Auto-scroll fighting user scroll | Phase building chat message UI | Manual test: scroll up mid-stream; verify auto-scroll does not override |
| No cancel button | Phase building streaming UI | Test: start stream, navigate away; check backend logs confirm stream terminated |
---
## Sources
- [openai/openai-dotnet Issue #65 — Streaming doesn't work properly in Blazor WASM](https://github.com/openai/openai-dotnet/issues/65) — confirmed fix with `BlazorHttpClientTransport`
- [Microsoft Q&A — Streaming Issue with Blazor WebAssembly and Semantic Kernel and OpenAI](https://learn.microsoft.com/en-sg/answers/questions/2242618/streaming-issue-with-blazor-webassembly-and-semati)
- [DEV Community — Real Blazor WebAssembly Production Pitfalls](https://dev.to/janhjordie/real-blazor-webassembly-production-pitfalls-3hmf) — IL trimming, JS interop, release-only failures
- [Chandradev Blog — 10 Blazor Coding Mistakes](https://chandradev819.wordpress.com/2025/12/17/10-blazor-coding-mistakes-i-see-in-real-projects-and-how-to-avoid-them/) — logic in components, DI misuse, naming
- [Thinktecture — Dependency Injection Scopes in Blazor](https://www.thinktecture.com/en/blazor/dependency-injection-scopes-in-blazor/) — Scoped = Singleton in WASM
- [ASP.NET Core Blazor DI docs](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection) — official lifetime guidance
- [ASP.NET Core Blazor rendering performance best practices](https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/rendering) — StateHasChanged, re-render control
- [dotnet/aspnetcore Issue #43098 — StateHasChanged not firing with IAsyncEnumerable](https://github.com/dotnet/aspnetcore/issues/43098)
- [Microsoft — Secure ASP.NET Core Blazor WebAssembly](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/) — API key security, no secrets in WASM
- [DEV Community — The Missing Third Config Layer: User Secrets in Blazor WASM](https://dev.to/j_sakamoto/the-missing-third-config-layer-adding-user-secrets-to-blazor-webassembly-2a5a) — confirms user secrets are NOT secret in WASM
- [Microsoft — Blazor WebAssembly file access Q&A](https://learn.microsoft.com/en-us/answers/questions/1290337/blazor-webassembly-get-file-access) — browser sandbox, no local disk
- [tpeczek.com — ASP.NET Core 9 and IAsyncEnumerable — Async Streaming from Blazor WASM](https://www.tpeczek.com/2024/09/aspnet-core-9-and-iasyncenumerable.html) — correct streaming patterns
- [dotnet/aspnetcore Issue #55982 — network error with IAsyncEnumerable streaming in WASM](https://github.com/dotnet/aspnetcore/issues/55982)
---
*Pitfalls research for: Blazor WebAssembly AI Chat Application*
*Researched: 2026-03-27*

180
.planning/research/STACK.md Normal file
View File

@@ -0,0 +1,180 @@
# Stack Research
**Domain:** Blazor WebAssembly AI Chat Application (.NET / C#)
**Researched:** 2026-03-27
**Confidence:** HIGH (core stack verified via NuGet and official Microsoft docs; version numbers confirmed via nuget.org)
---
## Recommended Stack
### Core Technologies
| Technology | Version | Purpose | Why Recommended |
|------------|---------|---------|-----------------|
| .NET 9 SDK | 9.x (latest patch) | Runtime, tooling, SDK | LTS-adjacent, stable, .NET 10 is in preview — stay on 9 for a tutorial project targeting a stable foundation |
| Blazor WebAssembly Standalone | .NET 9 | Client SPA running in-browser | Non-negotiable per project constraints; client-side execution with no server round-trip for UI |
| ASP.NET Core Web API | .NET 9 | Backend proxy for OpenAI calls | Required to keep the OpenAI API key server-side; WASM cannot access secrets directly |
| C# 13 | Included with .NET 9 | Application language | Included in .NET 9 SDK; no separate install needed |
**Critical architecture note:** The "hosted Blazor WebAssembly" template (single `.sln` with Client + Server + Shared projects) was removed in .NET 8. In .NET 9, you create two separate projects manually: a `dotnet new blazorwasm` standalone client and a `dotnet new webapi` backend, then add them to a solution. This is the correct approach for this project.
### OpenAI Integration
| Library | Version | Purpose | Why Recommended |
|---------|---------|---------|-----------------|
| `OpenAI` (official) | 2.9.1 | OpenAI API client with streaming | The official OpenAI-published .NET library; supports `CompleteChatStreamingAsync()` returning `AsyncCollectionResult<StreamingChatCompletionUpdate>` via `await foreach`; stable release as of 2026-03-02 |
**Do not use** `OpenAI-DotNet` (version 8.8.8) — this is an unofficial community package with a different API surface. The official `OpenAI` package is published directly by OpenAI and is the correct choice.
**Streaming mechanism:** The backend Web API endpoint calls `CompleteChatStreamingAsync()` and proxies chunks to the client. The WASM client uses `HttpCompletionOption.ResponseHeadersRead` with `SetBrowserResponseStreamingEnabled(true)` on the `HttpRequestMessage` to consume the streamed response. In .NET 10 streaming is enabled by default; in .NET 9 it must be explicitly opted in per-request.
### Markdown Rendering
| Library | Version | Purpose | Why Recommended |
|---------|---------|---------|-----------------|
| `Markdig` | 1.1.1 | Parse markdown text to HTML | The de facto standard markdown processor for .NET; CommonMark-compliant, fast, extensible, targets .NET Standard 2.0 so works in WASM; used by Microsoft and Syncfusion as the underlying engine |
**How it integrates in Blazor:** Call `Markdig.Markdown.ToHtml(content)` on the client, render the result with `@((MarkupString)htmlContent)` in a Razor component. No JS interop needed.
### UI Component Library
| Library | Version | Purpose | Why Recommended |
|---------|---------|---------|-----------------|
| `MudBlazor` | 9.2.0 | Material Design component library | Full .NET 9 support confirmed; pure C# with minimal JavaScript; comprehensive chat-friendly components (MudTextField, MudPaper, MudScrollToBottom, MudList); large community; no per-seat licensing |
**Alternative considered:** Radzen Blazor (free, good) and Telerik UI for Blazor (licensed). MudBlazor wins for a tutorial/personal project because it is free, has zero JS dependencies, and has excellent documentation for learners.
### JSON Storage (Server-side)
| Technology | Version | Purpose | Why Recommended |
|------------|---------|---------|-----------------|
| `System.Text.Json` | Built into .NET 9 | Serialize/deserialize conversation history | Built-in, no extra dependency; `JsonSerializerOptions` with `WriteIndented = true` for human-readable files; async file I/O via `File.ReadAllTextAsync` / `File.WriteAllTextAsync` |
Storage lives entirely on the **backend** (Web API project). The WASM client cannot access the local filesystem — only the server can. API endpoints expose CRUD operations over conversations, with JSON files persisted in a configurable directory on the server host.
---
## Supporting Libraries
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| `Microsoft.Extensions.AI` (abstractions) | 9.x preview | Optional AI abstraction layer | Skip for v1 — adds indirection before the core chat pattern is understood. Relevant for v2 when adding multi-provider support |
| `Blazored.LocalStorage` | latest | Browser local storage | Not needed for this project — persistence is on the server via JSON files, not the browser |
| `System.Net.ServerSentEvents` | Built into .NET 9 | SSE parser for streaming | Used automatically by the `OpenAI` library on the server; no direct usage needed |
---
## Development Tools
| Tool | Purpose | Notes |
|------|---------|-------|
| Visual Studio 2022 (v17.12+) | IDE with Blazor hot reload | Recommended for tutorial builder; full Blazor debugging, component preview, and hot reload support |
| VS Code + C# Dev Kit | Lighter-weight alternative | Works well; use `dotnet watch` for hot reload |
| `dotnet watch run` | Hot reload during development | Run in both Client and Server project directories simultaneously |
| `dotnet-dev-certs` | HTTPS dev certificate | Required for local HTTPS; run `dotnet dev-certs https --trust` once |
---
## Installation
```bash
# Create solution
mkdir ChatAgentApp && cd ChatAgentApp
dotnet new sln -n ChatAgentApp
# Create Blazor WASM client (standalone)
dotnet new blazorwasm -n ChatAgentApp.Client --framework net9.0
dotnet sln add ChatAgentApp.Client/ChatAgentApp.Client.csproj
# Create ASP.NET Core Web API backend
dotnet new webapi -n ChatAgentApp.Api --framework net9.0
dotnet sln add ChatAgentApp.Api/ChatAgentApp.Api.csproj
# Install OpenAI SDK in the API project
cd ChatAgentApp.Api
dotnet add package OpenAI --version 2.9.1
# Install Markdig in the Client project
cd ../ChatAgentApp.Client
dotnet add package Markdig --version 1.1.1
dotnet add package MudBlazor --version 9.2.0
```
---
## Alternatives Considered
| Recommended | Alternative | When to Use Alternative |
|-------------|-------------|-------------------------|
| `OpenAI` 2.9.1 (official) | `OpenAI-DotNet` 8.8.8 (unofficial) | Never — the official package is now stable and maintained by OpenAI directly |
| `OpenAI` 2.9.1 (official) | `Azure.AI.OpenAI` 2.1.0 | When targeting Azure OpenAI Service specifically (e.g., enterprise, EU data residency, private endpoints) — overkill for this project |
| `Markdig` | `CommonMark.NET` | Only if strict CommonMark compliance matters more than extensions; Markdig is a superset and the ecosystem standard |
| `MudBlazor` | Radzen Blazor | Radzen is fine; choose it if you already know it; MudBlazor has more learning resources |
| `MudBlazor` | Telerik UI for Blazor | Telerik requires a paid license; not appropriate for a personal tool |
| Standalone WASM + separate Web API | Blazor Web App template (unified) | Use the unified Blazor Web App template when you want mixed Server+WASM render modes on a single project; overkill for this project and obscures the WASM-specific patterns the tutorial aims to teach |
| JSON flat files (server-side) | SQLite via EF Core | SQLite is a better choice at scale; JSON is simpler for single-user personal tools and avoids introducing a migration workflow |
---
## What NOT to Use
| Avoid | Why | Use Instead |
|-------|-----|-------------|
| `OpenAI-DotNet` (unofficial) | Different API surface, not maintained by OpenAI, version numbers create confusion | Official `OpenAI` NuGet package |
| `Microsoft.SemanticKernel` | Adds significant abstraction and dependency weight for a tutorial; streaming works but is complex to explain | Direct `OpenAI` SDK calls; add SK in v2 when orchestration is needed |
| JavaScript `EventSource` API via JSInterop for streaming | Blazor WASM has `SetBrowserResponseStreamingEnabled` which avoids JS interop; adding JSInterop for streaming increases complexity significantly | `HttpCompletionOption.ResponseHeadersRead` + `SetBrowserResponseStreamingEnabled(true)` in the HTTP handler |
| `Newtonsoft.Json` | Unnecessary dependency; `System.Text.Json` is built into .NET 9 and is faster; Newtonsoft was the pre-.NET Core standard | `System.Text.Json` (built-in) |
| `Blazored.LocalStorage` for persistence | Browser storage is limited (~5MB), cleared by users, and not suitable for chat history of any meaningful length; also exposes all data client-side | Server-side JSON file storage via the Web API |
| AOT compilation during learning phase | Dramatically increases build times; not needed until production optimization is a concern; confusing to introduce in a tutorial | Default IL interpretation; add AOT opt-in note in the final phase |
---
## Stack Patterns by Variant
**For streaming responses from the API backend to the WASM client:**
- Backend streams OpenAI tokens as `text/event-stream` (SSE) or `application/x-ndjson`
- Client uses `SetBrowserResponseStreamingEnabled(true)` on `HttpRequestMessage`
- Client reads with `HttpCompletionOption.ResponseHeadersRead` and iterates the stream
- Trigger `StateHasChanged()` in the component after each token to update the UI
**For local JSON file storage on the server:**
- Define a `ConversationRepository` service on the API that reads/writes from a configurable base path
- Register as `Singleton` (not `Scoped`) since there is only one user and file access must be serialized
- Use `SemaphoreSlim(1,1)` to prevent concurrent write conflicts even in single-user mode
**For markdown rendering in the client:**
- Use `Markdig.Markdown.ToHtml(text, pipeline)` where `pipeline` is built with `MarkdownPipelineBuilder` enabling extensions (e.g., `UseAutoLinks()`, `UseEmojiAndSmiley()`)
- Render the HTML string using `@((MarkupString)html)` inside a `<div class="markdown-body">` element
- Apply CSS (GitHub Markdown CSS or custom) scoped to `.markdown-body` for code blocks and tables
---
## Version Compatibility
| Package | Compatible With | Notes |
|---------|-----------------|-------|
| `OpenAI` 2.9.1 | .NET Standard 2.0+ (.NET 9 confirmed) | Published 2026-03-02; requires `System.Net.ServerSentEvents` (built into .NET 9) |
| `Markdig` 1.1.1 | .NET 8.0, .NET Standard 2.0, .NET Framework 4.6.2 | .NET 9 compatible via .NET 8 TFM; published 2026-03-04 |
| `MudBlazor` 9.2.0 | .NET 8.0, .NET 9.0, .NET 10.0 | Published 2026-03-18; version 9.x = full support for .NET 9 |
| .NET 9 SDK | Blazor WASM + Web API in same solution | Both project types target `net9.0`; no cross-framework issues |
---
## Sources
- https://www.nuget.org/packages/OpenAI — Official OpenAI NuGet package; version 2.9.1 confirmed (2026-03-02)
- https://github.com/openai/openai-dotnet — Official OpenAI .NET SDK; streaming API verified (`CompleteChatStreamingAsync`, `await foreach`)
- https://www.nuget.org/packages/Markdig — Markdig version 1.1.1 confirmed (2026-03-04)
- https://www.nuget.org/packages/MudBlazor — MudBlazor 9.2.0 confirmed; .NET 8/9/10 full support (2026-03-18)
- https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-9.0 — Official Blazor hosting model docs; standalone WASM vs Blazor Web App distinction verified
- https://learn.microsoft.com/en-us/dotnet/core/compatibility/networking/10.0/default-http-streaming — Breaking change: WASM streaming opt-in (.NET 9) vs default (.NET 10)
- https://www.strathweb.com/2024/07/built-in-support-for-server-sent-events-in-net-9/ — SSE native support in .NET 9 via `System.Net.ServerSentEvents`; used internally by OpenAI SDK (MEDIUM confidence, single source)
- https://github.com/openai/openai-dotnet/issues/65 — Confirmed streaming issue in Blazor WASM requires `SetBrowserResponseStreamingEnabled(true)` (MEDIUM confidence, GitHub issue thread)
- https://devblogs.microsoft.com/dotnet/openai-dotnet-library/ — Official .NET Blog announcement of the OpenAI library
- https://dev.to/kazinix/blazor-web-app-webassembly-hosted-in-net8-and-net9-1k6g — Hosted template removal in .NET 8+, manual solution structure (MEDIUM confidence)
---
*Stack research for: Blazor WebAssembly AI Chat Application*
*Researched: 2026-03-27*

View File

@@ -0,0 +1,195 @@
# Project Research Summary
**Project:** Blazor WebAssembly AI Chat Application
**Domain:** Single-user personal AI chat web app (.NET / C# / OpenAI GPT)
**Researched:** 2026-03-27
**Confidence:** HIGH
## Executive Summary
This is a single-user personal AI chat application built on Blazor WebAssembly with an ASP.NET Core backend. The project has a dual purpose: functioning as a useful personal tool and serving as a tutorial-quality reference implementation for Blazor WASM patterns. The recommended architecture is a strict two-project split — a standalone Blazor WASM client and a separate ASP.NET Core Minimal API server — reflecting a breaking change in .NET 8+ that removed the hosted Blazor WASM template. The client runs entirely in the browser; the server holds secrets, calls OpenAI, and manages disk persistence. This boundary is non-negotiable and must be established before any feature code is written.
The core technical challenge is streaming. OpenAI's token-by-token streaming requires explicit opt-in in Blazor WASM (`SetBrowserResponseStreamingEnabled(true)`) that the SDK does not set by default — the stream silently falls back to buffered delivery with no error. Combined with the need to call `StateHasChanged()` on every token to update the UI, streaming is the highest-risk implementation step and must be validated early. All other features — conversation management, markdown rendering, copy-to-clipboard — are well-understood patterns with clear .NET implementations.
The key risk profile is concentrated in Phase 1 (architecture foundation) and the streaming phase. Three "never" mistakes — putting the API key in WASM, writing file I/O in the WASM project, and calling OpenAI directly from WASM — must be locked out architecturally before feature development begins. Once those boundaries are established, the remainder of the v1 feature set follows a clear dependency chain from storage to conversations to streaming to UI polish.
## Key Findings
### Recommended Stack
The stack is .NET 9 throughout: a `blazorwasm` standalone client and a `webapi` backend in a single solution, connected by HTTP and SSE. There are no exotic dependencies — the official `OpenAI` NuGet package (2.9.1, published by OpenAI directly) handles AI calls, `Markdig` (1.1.1) handles markdown-to-HTML conversion, `MudBlazor` (9.2.0) provides Material Design UI components with zero JavaScript dependencies, and `System.Text.Json` (built-in) handles JSON serialization and file storage. All versions are confirmed compatible with .NET 9.
The most important stack decision is what to exclude: do not use `OpenAI-DotNet` (unofficial community package), `Microsoft.SemanticKernel` (excessive abstraction for v1), `Newtonsoft.Json` (superseded by System.Text.Json), `Blazored.LocalStorage` (wrong persistence layer for this architecture), or JSInterop for streaming (WASM has a native streaming opt-in that avoids it).
**Core technologies:**
- `.NET 9 SDK + C# 13`: Runtime and language — stable, LTS-adjacent, both project types target `net9.0`
- `Blazor WebAssembly (standalone)`: Client SPA — non-negotiable per project constraints; runs in-browser with no server round-trip for UI
- `ASP.NET Core Minimal API`: Backend proxy — required to keep the OpenAI key server-side and to handle disk I/O that WASM cannot perform
- `OpenAI` 2.9.1 (official): AI calls — `CompleteChatStreamingAsync()` with `await foreach`; the only correct .NET SDK choice
- `Markdig` 1.1.1: Markdown rendering — de facto .NET standard; CommonMark-compliant; renders via `@((MarkupString)html)` with no JS interop
- `MudBlazor` 9.2.0: UI components — pure C#, zero JS dependencies, comprehensive chat-friendly components, free license
- `System.Text.Json` (built-in): Persistence — serialize conversations to JSON files on the server; no extra dependency
### Expected Features
The feature set is well-defined by comparison to ChatGPT and Claude as reference products. The v1 scope is intentionally constrained to what makes the app genuinely usable, with explicit anti-features documented to prevent scope creep during implementation.
**Must have (table stakes):**
- Send message / receive streaming response — core loop; blocking responses are unacceptable by 2026 standards
- Markdown rendering with syntax-highlighted code blocks — GPT always responds with markdown; raw output is unusable
- Multiple named conversations with create / switch / delete — without this, the app is a single disposable thread
- JSON file persistence across sessions — conversations must survive page refresh to be useful
- Auto-scroll to latest message and loading indicator — baseline polish that makes the app feel complete
- Copy-to-clipboard on code blocks — high-frequency action for the developer-focused target user
- Input disabled during streaming and send-on-Enter — prevents double-submit and matches chat conventions
- Tutorial-style inline code comments — the project's defining purpose as a learning resource
**Should have (competitive, v1.x):**
- Auto-generated conversation titles — reduces naming friction; single GPT summarization call
- System prompt / persona configuration — power-user feature; natural extension once multi-conversation works
- Model selector (GPT-4o vs GPT-4o-mini) — cost/quality tradeoff; low implementation cost
- Export conversation to markdown/text — low complexity, occasional high value
**Defer (v2+):**
- Message edit and regenerate — medium complexity; wait until core loop is solid
- Token usage display — streaming completion handling required; not blocking
- LangChain / agentic workflows, RAG, MCP server integration — explicitly v2 per project intent
- Voice input/output, image uploads, multi-user auth, PWA — all documented anti-features with clear rationale
Feature dependencies are explicit: JSON storage must precede conversation management, which must precede conversation switching. A basic blocking API call must precede streaming. Markdown must precede syntax highlighting, which must precede copy-to-clipboard.
### Architecture Approach
The architecture is a strict two-tier system: a Blazor WASM SPA in the browser communicating with an ASP.NET Core Minimal API via HTTP and SSE. State on the client is managed by a singleton `ConversationStateService` that raises `OnChange` events — components subscribe in `OnInitialized` and unsubscribe in `Dispose`. There is a `Shared` library project that holds `Conversation` and `ChatMessage` models used by both tiers, eliminating duplicate DTOs.
Components are kept intentionally thin (data in via `[Parameter]`, actions out via `EventCallback<T>`). All logic lives in services. This is explicitly stated as a tutorial goal — fat components teach bad habits and are hard to explain.
**Major components:**
1. `ConversationStateService` (WASM singleton) — active conversation, message list, streaming flag; raises `OnChange` for all subscribed components
2. `ChatApiClient` (WASM scoped service) — wraps `HttpClient`, handles SSE stream reading with `SetBrowserResponseStreamingEnabled(true)` and `ResponseHeadersRead`
3. `OpenAiService` (server scoped) — wraps official OpenAI SDK, returns `IAsyncEnumerable<string>` of tokens to endpoint handlers
4. `ConversationRepository` (server singleton) — reads/writes JSON files under a configurable data directory; uses `SemaphoreSlim(1,1)` for write serialization
5. `ChatEndpoints` + `ConversationEndpoints` (server Minimal API) — thin HTTP layer wiring services to routes; SSE streaming endpoint proxies tokens to client
6. Leaf UI components: `MessageBubble`, `ChatInput`, `ConversationList`, `MessageList` — pure display, no service calls
7. Container component: `ChatPage` — composes all child components, owns the route (`@page "/chat/{id?}"`)
**Build order:** Shared models → `ConversationRepository``OpenAiService` → server endpoints → `ChatApiClient``ConversationStateService` → leaf UI → container UI. This maps directly to implementation phases.
### Critical Pitfalls
1. **Streaming silently broken in WASM (Pitfall 1 + 4)** — Two distinct failure modes that appear identical: (a) the OpenAI SDK does not set `SetBrowserResponseStreamingEnabled(true)` so the browser buffers the entire response; (b) `StateHasChanged()` is not called per-token so Blazor batches all renders until the stream completes. Both produce the same symptom — tokens appear all at once. Fix: custom `BlazorHttpClientTransport` on the backend, and explicit `StateHasChanged()` + `await Task.Yield()` inside the `await foreach` token loop. Throttle to ~50ms intervals to prevent UI thread starvation at GPT-4o token speeds.
2. **API key exposure in WASM (Pitfall 2)**`wwwroot/appsettings.json` is a static file served to any browser visitor. `dotnet user-secrets` in WASM projects are embedded in the published bundle in plaintext. The key must live exclusively in the server project, accessed via server-side `user-secrets` or environment variables. This boundary must be established in Phase 1 and never crossed.
3. **File I/O in WASM project (Pitfall 5)**`System.IO.File` compiles in WASM but writes to an in-memory virtual filesystem that resets on every page refresh. All persistence must go through backend API endpoints. Reinforce the same architectural boundary as the API key rule.
4. **Scoped DI = Singleton in WASM (Pitfall 3)** — In Blazor WASM there is exactly one DI scope for the tab lifetime. A service registered as `Scoped` never resets. Design `ConversationStateService` to hold a collection keyed by conversation ID, not mutable "current conversation" fields.
5. **IL trimming breaks Release builds (Pitfall 6)** — Debug builds do not trim; published builds do. JSON serialization properties, DI-resolved types, and JSInterop callbacks can be silently stripped. Use `[JsonSerializable]` source generators on all model types and run `dotnet publish` once in Phase 1 to catch trim warnings while the surface is small.
## Implications for Roadmap
Based on combined research, the architecture dependency chain and pitfall prevention requirements suggest five phases:
### Phase 1: Architecture Foundation
**Rationale:** Three critical "never" mistakes (API key in WASM, file I/O in WASM, direct OpenAI call from WASM) must be architecturally locked before any feature code is written. The WASM/backend split is the load-bearing constraint everything else depends on. This phase also establishes the `Shared` models library which both tiers need immediately.
**Delivers:** Working solution structure with two projects + shared library; CORS configured; basic HTTP connectivity verified WASM-to-server; `dotnet publish` tested once to catch IL trim warnings early; placeholder endpoints in place; no OpenAI calls yet.
**Addresses:** Project scaffolding, solution structure (FEATURES.md scaffolding prerequisite)
**Avoids:** API key exposure (Pitfall 2), file I/O in WASM (Pitfall 5), direct OpenAI calls from WASM (Architecture anti-pattern 1), IL trimming surprises (Pitfall 6)
### Phase 2: Conversation Storage and Management
**Rationale:** JSON file storage is the prerequisite for every conversation-related feature. Per the feature dependency graph: `[JSON File Storage] → [Multiple Conversations] → [Create/Switch/Delete/Persist]`. This phase must come before any AI integration because the persistence layer needs to exist before we can store AI responses.
**Delivers:** `ConversationRepository` with full CRUD, `ConversationEndpoints` wired to HTTP routes, `ConversationList` sidebar component, create/switch/delete conversations working, conversation history persisted to disk and loaded on startup. The app has no AI yet but has a working conversation management UI.
**Uses:** `System.Text.Json` built-in, `SemaphoreSlim(1,1)` for write serialization, `MudBlazor` for sidebar components
**Implements:** `ConversationRepository`, `ConversationEndpoints`, `ConversationStateService` (initial version), `ConversationList.razor`
**Avoids:** Scoped DI state leaks (Pitfall 3) — design `ConversationStateService` with `Dictionary<Guid, ConversationState>` from the start
### Phase 3: Basic AI Chat (Non-Streaming)
**Rationale:** Per the feature dependency chain, a working blocking API call must be established before streaming is layered on top. Building non-streaming first validates the full request/response shape, CORS, error handling, and conversation history construction without the added complexity of SSE. This is the correct learning sequence for a tutorial project.
**Delivers:** Full chat loop working end-to-end: user sends message → backend calls OpenAI → response appended to conversation → conversation saved to disk. All without streaming. Markdown rendering added here because GPT responses with raw markdown are effectively unusable and would make all testing painful.
**Uses:** `OpenAI` 2.9.1 SDK, `Markdig` 1.1.1, `MudBlazor` chat components
**Implements:** `OpenAiService`, `ChatEndpoints` (non-streaming POST), `ChatApiClient` (basic POST), `MessageBubble.razor` with `@((MarkupString)html)` rendering, `ChatInput.razor`
**Avoids:** Markdown XSS via raw `MarkupString` (PITFALLS integration gotchas) — sanitize or accept risk explicitly in code comments
### Phase 4: Streaming Responses
**Rationale:** Streaming is the highest-risk implementation step. Research identified two independent failure modes (transport not set, `StateHasChanged` not called) that produce identical symptoms. Addressing this in its own phase means streaming can be diagnosed and debugged in isolation, without other variables. All streaming-specific patterns — `BlazorHttpClientTransport`, SSE endpoint, `ResponseHeadersRead`, per-token `StateHasChanged` with throttling — are introduced and documented here.
**Delivers:** Token-by-token streaming from OpenAI through the backend SSE endpoint to the WASM UI. Loading indicator shown immediately on send, hidden on first token. Auto-scroll to latest message. Input disabled during streaming. Cancel button wired to `CancellationToken`. Stream throttling (~50ms) to prevent UI thread starvation.
**Uses:** `SetBrowserResponseStreamingEnabled(true)`, `HttpCompletionOption.ResponseHeadersRead`, `text/event-stream` SSE frames, `await Task.Yield()` in token loop
**Implements:** Streaming `ChatEndpoints`, updated `ChatApiClient` with stream reader, updated `MessageList` and `ChatPage` with streaming state
**Avoids:** Streaming silently broken (Pitfall 1), UI freeze without `StateHasChanged` (Pitfall 4), UI thread starvation from unthrottled renders (PITFALLS performance traps)
### Phase 5: Polish and v1.x Features
**Rationale:** Once the core loop (storage + AI + streaming) is solid, the remaining v1.x features are all low-to-medium complexity additions that build on the established foundation. Grouping them together allows the tutorial narrative to focus on "extending a working app" rather than "getting the basics right."
**Delivers:** Auto-generated conversation titles (GPT summarization call after first exchange), syntax-highlighted code blocks (`Markdown.ColorCode` Markdig pipeline extension), copy-to-clipboard on code blocks (JS interop via `navigator.clipboard.writeText`), responsive layout for mobile, error handling with user-visible messages, model selector dropdown (GPT-4o vs GPT-4o-mini). Optional v1.x additions: system prompt configuration, export conversation.
**Uses:** `Markdown.ColorCode` NuGet package (base package, NOT `CSharpToColoredHtml` which breaks WASM), `navigator.clipboard` JS interop
**Implements:** Updated `MarkdownPipeline` with ColorCode extension, `ClipboardService.cs` JS interop wrapper, settings model for model selection
### Phase Ordering Rationale
- **Architecture before features** prevents the three hardest-to-recover-from mistakes (API key exposure, WASM file I/O, wrong project boundaries) from being baked in.
- **Storage before AI** follows the feature dependency graph exactly: conversations need a home before AI responses can be stored in them.
- **Non-streaming before streaming** validates the full request/response shape with simpler code, making streaming easier to debug when it is introduced.
- **Streaming as its own phase** isolates the highest-risk technical challenge. Combined with the tutorial purpose, this also makes for a clear "here is how streaming actually works in Blazor WASM" chapter.
- **Polish last** respects the single-responsibility of each phase and avoids complexity interleaving.
### Research Flags
Phases likely needing deeper research during planning (i.e., run `/gsd:research-phase`):
- **Phase 4 (Streaming):** The `BlazorHttpClientTransport` workaround and SSE frame format have multiple interacting constraints. Phase planning should re-verify the current state of `openai-dotnet` issue #65 and confirm whether .NET 9.x patch releases have changed the default behavior. Token throttling strategy (timer vs counter) also warrants a concrete recommendation.
- **Phase 5 (Markdown.ColorCode + JS Interop):** The WASM compatibility note (base `Markdown.ColorCode` works; `CSharpToColoredHtml` does not) was sourced from community reports. Verify against the current NuGet package version before implementing.
Phases with standard patterns (skip research-phase):
- **Phase 1 (Architecture Foundation):** The two-project solution structure and CORS setup are fully documented in official Microsoft docs. No novel patterns.
- **Phase 2 (Conversation Storage):** Repository pattern with JSON file I/O is a standard .NET pattern. `SemaphoreSlim` for single-writer serialization is well-documented.
- **Phase 3 (Basic AI Chat):** OpenAI SDK usage for non-streaming chat completions is documented in the official SDK repo with examples. Markdig integration in Blazor has multiple tutorial references.
## Confidence Assessment
| Area | Confidence | Notes |
|------|------------|-------|
| Stack | HIGH | All package versions verified on nuget.org; official SDK confirmed by OpenAI .NET Blog post; version compatibility table verified against published TFM support |
| Features | HIGH | Feature set cross-referenced against live ChatGPT and Claude UX; OpenAI streaming API docs consulted; Blazor-specific constraints verified |
| Architecture | HIGH | Microsoft official Blazor docs + verified community implementations (PalmHill.BlazorChat reference); all patterns confirmed with working code samples |
| Pitfalls | HIGH | Critical pitfalls sourced from official GitHub issue tracker (`openai-dotnet` #65, `aspnetcore` #43098), Microsoft Q&A, and documented production experience |
**Overall confidence:** HIGH
### Gaps to Address
- **Streaming transport behavior in .NET 9 patch releases:** The `SetBrowserResponseStreamingEnabled(true)` workaround is confirmed required in .NET 9 and becomes default in .NET 10. There is a possibility a .NET 9.x patch release may have changed this behavior. Verify at the start of Phase 4 by checking the official .NET 9 breaking change notes.
- **StateHasChanged throttling threshold:** Research recommends ~50ms or every N tokens, but the optimal value depends on GPT-4o's actual token delivery rate and the target device's rendering performance. Treat as a tunable constant in code rather than a magic number.
- **XSS risk of rendering GPT output as MarkupString:** This is a known accepted risk for a single-user personal tool. Document the decision explicitly in the code (tutorial purpose) rather than leaving it as a silent assumption. Consider adding `Markdig`'s `DisableHtml()` pipeline option as a low-friction mitigation.
- **CORS configuration for deployment:** Research covered localhost development CORS. If the app is ever deployed (even to a home server), the CORS origin list needs updating. Document this as a deployment note in Phase 1.
## Sources
### Primary (HIGH confidence)
- https://www.nuget.org/packages/OpenAI — OpenAI 2.9.1 version and publish date confirmed
- https://github.com/openai/openai-dotnet — Streaming API (`CompleteChatStreamingAsync`, `await foreach`) verified
- https://www.nuget.org/packages/Markdig — Markdig 1.1.1 confirmed; .NET 8 TFM confirmed .NET 9 compatible
- https://www.nuget.org/packages/MudBlazor — MudBlazor 9.2.0 confirmed; .NET 8/9/10 support listed
- https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-9.0 — Standalone WASM vs Blazor Web App distinction; hosted template removal confirmed
- https://learn.microsoft.com/en-us/dotnet/core/compatibility/networking/10.0/default-http-streaming — WASM streaming opt-in (.NET 9) vs default (.NET 10) breaking change
- https://learn.microsoft.com/en-us/aspnet/core/blazor/call-web-api?view=aspnetcore-10.0 — HttpClient streaming patterns for Blazor
- https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection — Official DI lifetime guidance; Scoped = Singleton in WASM
- https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/ — API key security; no secrets in WASM bundle
- https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/rendering — StateHasChanged and re-render control
- https://devblogs.microsoft.com/dotnet/openai-dotnet-library/ — Official .NET Blog announcement of the OpenAI SDK
### Secondary (MEDIUM confidence)
- https://github.com/openai/openai-dotnet/issues/65 — Confirmed `SetBrowserResponseStreamingEnabled(true)` fix with `BlazorHttpClientTransport`; community-verified workaround
- https://www.meziantou.net/streaming-an-http-response-in-blazor-webassembly.htm — WASM streaming HttpClient patterns
- https://www.thinktecture.com/en/blazor/dependency-injection-scopes-in-blazor/ — Scoped = Singleton in WASM; verified against official docs
- https://www.strathweb.com/2024/07/built-in-support-for-server-sent-events-in-net-9/ — SSE native support in .NET 9
- https://github.com/edgett/PalmHill.BlazorChat — Reference implementation; WASM + WebAPI + real-time LLM
- https://dev.to/janhjordie/real-blazor-webassembly-production-pitfalls-3hmf — IL trimming, JS interop, release-only failures
- https://github.com/dotnet/aspnetcore/issues/43098 — StateHasChanged not firing with IAsyncEnumerable
### Tertiary (MEDIUM-LOW confidence, validate before use)
- https://dev.to/kazinix/blazor-web-app-webassembly-hosted-in-net8-and-net9-1k6g — Hosted template removal in .NET 8+ (single community source; cross-checked against official docs)
- https://chandradev819.wordpress.com/2025/12/17/10-blazor-coding-mistakes-i-see-in-real-projects-and-how-to-avoid-them/ — Fat component patterns, DI misuse
- https://www.nuget.org/packages/Markdown.ColorCode — WASM base package compatibility note (community-reported; verify during Phase 5 implementation)
---
*Research completed: 2026-03-27*
*Ready for roadmap: yes*

147
CLAUDE.md Normal file
View File

@@ -0,0 +1,147 @@
<!-- GSD:project-start source:PROJECT.md -->
## Project
**Chat Agent WebApp**
A personal AI chat web application built with Blazor WebAssembly and the OpenAI GPT API. Users send messages, receive streaming AI responses rendered as markdown, and manage multiple persistent conversations. The project doubles as an incremental learning journey — each phase introduces one concept with well-documented, explained code, making it suitable as a Blazor tutorial for a developer experienced in C# but new to the framework.
**Core Value:** A working, well-understood AI chat interface — every line of code is intentional and explained, so the builder learns Blazor patterns while shipping a real product.
### Constraints
- **Tech stack**: .NET / C# / Blazor WebAssembly — non-negotiable
- **LLM provider**: OpenAI GPT API
- **Storage**: JSON files on local disk
- **Architecture**: WASM client + backend API (API key stays server-side)
- **Code style**: Every Blazor concept introduced must have inline comments explaining what it does and why
<!-- GSD:project-end -->
<!-- GSD:stack-start source:research/STACK.md -->
## Technology Stack
## Recommended Stack
### Core Technologies
| Technology | Version | Purpose | Why Recommended |
|------------|---------|---------|-----------------|
| .NET 9 SDK | 9.x (latest patch) | Runtime, tooling, SDK | LTS-adjacent, stable, .NET 10 is in preview — stay on 9 for a tutorial project targeting a stable foundation |
| Blazor WebAssembly Standalone | .NET 9 | Client SPA running in-browser | Non-negotiable per project constraints; client-side execution with no server round-trip for UI |
| ASP.NET Core Web API | .NET 9 | Backend proxy for OpenAI calls | Required to keep the OpenAI API key server-side; WASM cannot access secrets directly |
| C# 13 | Included with .NET 9 | Application language | Included in .NET 9 SDK; no separate install needed |
### OpenAI Integration
| Library | Version | Purpose | Why Recommended |
|---------|---------|---------|-----------------|
| `OpenAI` (official) | 2.9.1 | OpenAI API client with streaming | The official OpenAI-published .NET library; supports `CompleteChatStreamingAsync()` returning `AsyncCollectionResult<StreamingChatCompletionUpdate>` via `await foreach`; stable release as of 2026-03-02 |
### Markdown Rendering
| Library | Version | Purpose | Why Recommended |
|---------|---------|---------|-----------------|
| `Markdig` | 1.1.1 | Parse markdown text to HTML | The de facto standard markdown processor for .NET; CommonMark-compliant, fast, extensible, targets .NET Standard 2.0 so works in WASM; used by Microsoft and Syncfusion as the underlying engine |
### UI Component Library
| Library | Version | Purpose | Why Recommended |
|---------|---------|---------|-----------------|
| `MudBlazor` | 9.2.0 | Material Design component library | Full .NET 9 support confirmed; pure C# with minimal JavaScript; comprehensive chat-friendly components (MudTextField, MudPaper, MudScrollToBottom, MudList); large community; no per-seat licensing |
### JSON Storage (Server-side)
| Technology | Version | Purpose | Why Recommended |
|------------|---------|---------|-----------------|
| `System.Text.Json` | Built into .NET 9 | Serialize/deserialize conversation history | Built-in, no extra dependency; `JsonSerializerOptions` with `WriteIndented = true` for human-readable files; async file I/O via `File.ReadAllTextAsync` / `File.WriteAllTextAsync` |
## Supporting Libraries
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| `Microsoft.Extensions.AI` (abstractions) | 9.x preview | Optional AI abstraction layer | Skip for v1 — adds indirection before the core chat pattern is understood. Relevant for v2 when adding multi-provider support |
| `Blazored.LocalStorage` | latest | Browser local storage | Not needed for this project — persistence is on the server via JSON files, not the browser |
| `System.Net.ServerSentEvents` | Built into .NET 9 | SSE parser for streaming | Used automatically by the `OpenAI` library on the server; no direct usage needed |
## Development Tools
| Tool | Purpose | Notes |
|------|---------|-------|
| Visual Studio 2022 (v17.12+) | IDE with Blazor hot reload | Recommended for tutorial builder; full Blazor debugging, component preview, and hot reload support |
| VS Code + C# Dev Kit | Lighter-weight alternative | Works well; use `dotnet watch` for hot reload |
| `dotnet watch run` | Hot reload during development | Run in both Client and Server project directories simultaneously |
| `dotnet-dev-certs` | HTTPS dev certificate | Required for local HTTPS; run `dotnet dev-certs https --trust` once |
## Installation
# Create solution
# Create Blazor WASM client (standalone)
# Create ASP.NET Core Web API backend
# Install OpenAI SDK in the API project
# Install Markdig in the Client project
## Alternatives Considered
| Recommended | Alternative | When to Use Alternative |
|-------------|-------------|-------------------------|
| `OpenAI` 2.9.1 (official) | `OpenAI-DotNet` 8.8.8 (unofficial) | Never — the official package is now stable and maintained by OpenAI directly |
| `OpenAI` 2.9.1 (official) | `Azure.AI.OpenAI` 2.1.0 | When targeting Azure OpenAI Service specifically (e.g., enterprise, EU data residency, private endpoints) — overkill for this project |
| `Markdig` | `CommonMark.NET` | Only if strict CommonMark compliance matters more than extensions; Markdig is a superset and the ecosystem standard |
| `MudBlazor` | Radzen Blazor | Radzen is fine; choose it if you already know it; MudBlazor has more learning resources |
| `MudBlazor` | Telerik UI for Blazor | Telerik requires a paid license; not appropriate for a personal tool |
| Standalone WASM + separate Web API | Blazor Web App template (unified) | Use the unified Blazor Web App template when you want mixed Server+WASM render modes on a single project; overkill for this project and obscures the WASM-specific patterns the tutorial aims to teach |
| JSON flat files (server-side) | SQLite via EF Core | SQLite is a better choice at scale; JSON is simpler for single-user personal tools and avoids introducing a migration workflow |
## What NOT to Use
| Avoid | Why | Use Instead |
|-------|-----|-------------|
| `OpenAI-DotNet` (unofficial) | Different API surface, not maintained by OpenAI, version numbers create confusion | Official `OpenAI` NuGet package |
| `Microsoft.SemanticKernel` | Adds significant abstraction and dependency weight for a tutorial; streaming works but is complex to explain | Direct `OpenAI` SDK calls; add SK in v2 when orchestration is needed |
| JavaScript `EventSource` API via JSInterop for streaming | Blazor WASM has `SetBrowserResponseStreamingEnabled` which avoids JS interop; adding JSInterop for streaming increases complexity significantly | `HttpCompletionOption.ResponseHeadersRead` + `SetBrowserResponseStreamingEnabled(true)` in the HTTP handler |
| `Newtonsoft.Json` | Unnecessary dependency; `System.Text.Json` is built into .NET 9 and is faster; Newtonsoft was the pre-.NET Core standard | `System.Text.Json` (built-in) |
| `Blazored.LocalStorage` for persistence | Browser storage is limited (~5MB), cleared by users, and not suitable for chat history of any meaningful length; also exposes all data client-side | Server-side JSON file storage via the Web API |
| AOT compilation during learning phase | Dramatically increases build times; not needed until production optimization is a concern; confusing to introduce in a tutorial | Default IL interpretation; add AOT opt-in note in the final phase |
## Stack Patterns by Variant
- Backend streams OpenAI tokens as `text/event-stream` (SSE) or `application/x-ndjson`
- Client uses `SetBrowserResponseStreamingEnabled(true)` on `HttpRequestMessage`
- Client reads with `HttpCompletionOption.ResponseHeadersRead` and iterates the stream
- Trigger `StateHasChanged()` in the component after each token to update the UI
- Define a `ConversationRepository` service on the API that reads/writes from a configurable base path
- Register as `Singleton` (not `Scoped`) since there is only one user and file access must be serialized
- Use `SemaphoreSlim(1,1)` to prevent concurrent write conflicts even in single-user mode
- Use `Markdig.Markdown.ToHtml(text, pipeline)` where `pipeline` is built with `MarkdownPipelineBuilder` enabling extensions (e.g., `UseAutoLinks()`, `UseEmojiAndSmiley()`)
- Render the HTML string using `@((MarkupString)html)` inside a `<div class="markdown-body">` element
- Apply CSS (GitHub Markdown CSS or custom) scoped to `.markdown-body` for code blocks and tables
## Version Compatibility
| Package | Compatible With | Notes |
|---------|-----------------|-------|
| `OpenAI` 2.9.1 | .NET Standard 2.0+ (.NET 9 confirmed) | Published 2026-03-02; requires `System.Net.ServerSentEvents` (built into .NET 9) |
| `Markdig` 1.1.1 | .NET 8.0, .NET Standard 2.0, .NET Framework 4.6.2 | .NET 9 compatible via .NET 8 TFM; published 2026-03-04 |
| `MudBlazor` 9.2.0 | .NET 8.0, .NET 9.0, .NET 10.0 | Published 2026-03-18; version 9.x = full support for .NET 9 |
| .NET 9 SDK | Blazor WASM + Web API in same solution | Both project types target `net9.0`; no cross-framework issues |
## Sources
- https://www.nuget.org/packages/OpenAI — Official OpenAI NuGet package; version 2.9.1 confirmed (2026-03-02)
- https://github.com/openai/openai-dotnet — Official OpenAI .NET SDK; streaming API verified (`CompleteChatStreamingAsync`, `await foreach`)
- https://www.nuget.org/packages/Markdig — Markdig version 1.1.1 confirmed (2026-03-04)
- https://www.nuget.org/packages/MudBlazor — MudBlazor 9.2.0 confirmed; .NET 8/9/10 full support (2026-03-18)
- https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-9.0 — Official Blazor hosting model docs; standalone WASM vs Blazor Web App distinction verified
- https://learn.microsoft.com/en-us/dotnet/core/compatibility/networking/10.0/default-http-streaming — Breaking change: WASM streaming opt-in (.NET 9) vs default (.NET 10)
- https://www.strathweb.com/2024/07/built-in-support-for-server-sent-events-in-net-9/ — SSE native support in .NET 9 via `System.Net.ServerSentEvents`; used internally by OpenAI SDK (MEDIUM confidence, single source)
- https://github.com/openai/openai-dotnet/issues/65 — Confirmed streaming issue in Blazor WASM requires `SetBrowserResponseStreamingEnabled(true)` (MEDIUM confidence, GitHub issue thread)
- https://devblogs.microsoft.com/dotnet/openai-dotnet-library/ — Official .NET Blog announcement of the OpenAI library
- https://dev.to/kazinix/blazor-web-app-webassembly-hosted-in-net8-and-net9-1k6g — Hosted template removal in .NET 8+, manual solution structure (MEDIUM confidence)
<!-- GSD:stack-end -->
<!-- GSD:conventions-start source:CONVENTIONS.md -->
## Conventions
Conventions not yet established. Will populate as patterns emerge during development.
<!-- GSD:conventions-end -->
<!-- GSD:architecture-start source:ARCHITECTURE.md -->
## Architecture
Architecture not yet mapped. Follow existing patterns found in the codebase.
<!-- GSD:architecture-end -->
<!-- GSD:workflow-start source:GSD defaults -->
## GSD Workflow Enforcement
Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.
Use these entry points:
- `/gsd:quick` for small fixes, doc updates, and ad-hoc tasks
- `/gsd:debug` for investigation and bug fixing
- `/gsd:execute-phase` for planned phase work
Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.
<!-- GSD:workflow-end -->
<!-- GSD:profile-start -->
## Developer Profile
> Profile not yet configured. Run `/gsd:profile-user` to generate your developer profile.
> This section is managed by `generate-claude-profile` -- do not edit manually.
<!-- GSD:profile-end -->

69
ChatAgent.sln Normal file
View File

@@ -0,0 +1,69 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Client", "src\ChatAgent.Client\ChatAgent.Client.csproj", "{600EA0C4-7CDE-4807-BE3C-30A6D2242392}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Api", "src\ChatAgent.Api\ChatAgent.Api.csproj", "{467D4550-6F9A-456E-B99C-0ABE94070ECF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Shared", "src\ChatAgent.Shared\ChatAgent.Shared.csproj", "{06182E3F-BC78-449B-ADF6-D9EE49E48945}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Debug|Any CPU.Build.0 = Debug|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Debug|x64.ActiveCfg = Debug|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Debug|x64.Build.0 = Debug|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Debug|x86.ActiveCfg = Debug|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Debug|x86.Build.0 = Debug|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Release|Any CPU.ActiveCfg = Release|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Release|Any CPU.Build.0 = Release|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Release|x64.ActiveCfg = Release|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Release|x64.Build.0 = Release|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Release|x86.ActiveCfg = Release|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Release|x86.Build.0 = Release|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Debug|x64.ActiveCfg = Debug|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Debug|x64.Build.0 = Debug|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Debug|x86.ActiveCfg = Debug|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Debug|x86.Build.0 = Debug|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Release|Any CPU.Build.0 = Release|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Release|x64.ActiveCfg = Release|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Release|x64.Build.0 = Release|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Release|x86.ActiveCfg = Release|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Release|x86.Build.0 = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Debug|Any CPU.Build.0 = Debug|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Debug|x64.ActiveCfg = Debug|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Debug|x64.Build.0 = Debug|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Debug|x86.ActiveCfg = Debug|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Debug|x86.Build.0 = Debug|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|Any CPU.ActiveCfg = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|Any CPU.Build.0 = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x64.ActiveCfg = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x64.Build.0 = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x86.ActiveCfg = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{600EA0C4-7CDE-4807-BE3C-30A6D2242392} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{467D4550-6F9A-456E-B99C-0ABE94070ECF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{06182E3F-BC78-449B-ADF6-D9EE49E48945} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal

6
global.json Normal file
View File

@@ -0,0 +1,6 @@
{
"sdk": {
"version": "9.0.312",
"rollForward": "latestPatch"
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ChatAgent.Shared\ChatAgent.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,47 @@
// HealthController.cs -- API endpoint that proves the WASM-to-API connection works.
//
// This is the first controller in the project. It exists to verify that:
// 1. The API server starts and responds to HTTP requests
// 2. CORS allows the Blazor WASM client to call across origins
// 3. The shared HealthResponse DTO serializes/deserializes correctly
//
// In ASP.NET Core, a "controller" is a class that handles HTTP requests.
// The framework routes requests to controller methods based on attributes.
using ChatAgent.Shared.Models;
using Microsoft.AspNetCore.Mvc;
namespace ChatAgent.Api.Controllers
{
// [ApiController] enables several conveniences for API development:
// - Automatic HTTP 400 responses for invalid model state
// - Automatic inference of [FromBody] for complex types
// - Problem details responses for error status codes
// It signals "this class handles API requests, not HTML pages."
[ApiController]
// [Route("api/[controller]")] maps this class to the URL path /api/health.
// The [controller] token is replaced with the class name minus "Controller",
// so HealthController becomes "health" -> /api/health.
[Route("api/[controller]")]
public class HealthController : ControllerBase
{
// [HttpGet] maps this method to HTTP GET requests at the controller's route.
// When a client sends GET /api/health, ASP.NET Core calls this method.
// It returns an IActionResult, which gives us control over the HTTP status code.
[HttpGet]
public IActionResult Get()
{
// Return HTTP 200 OK with a HealthResponse body.
// The framework serializes the object to JSON automatically
// because this is an [ApiController].
// DateTime.UtcNow provides a live timestamp so the client can verify
// the response is fresh (not cached).
return Ok(new HealthResponse
{
Status = "healthy",
Timestamp = DateTime.UtcNow
});
}
}
}

View File

@@ -0,0 +1,59 @@
// Program.cs -- ASP.NET Core Web API entry point for ChatAgent.
//
// This is the backend server. In Phase 1, it only serves a health check endpoint.
// In later phases, it will proxy OpenAI API calls (keeping the API key server-side)
// and manage JSON file storage for conversation persistence.
//
// ASP.NET Core uses a "builder pattern": first configure services (DI container),
// then build the app, configure middleware pipeline, and run.
var builder = WebApplication.CreateBuilder(args);
// --- Service Registration (Dependency Injection container) ---
// AddControllers() registers MVC controller services so ASP.NET Core discovers
// classes decorated with [ApiController]. We use Controllers (not Minimal API)
// for explicit structure -- each controller is a separate file with clear routing (D-05).
builder.Services.AddControllers();
// AddCors() registers Cross-Origin Resource Sharing services.
// CORS is REQUIRED because the Blazor WASM client runs on a different origin
// (https://localhost:5200) than this API (https://localhost:7100).
// Browsers block cross-origin HTTP requests by default as a security measure.
// Without this policy, the client's fetch() calls to the API would be rejected.
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowBlazorClient", policy =>
{
policy
// Only allow requests from the Blazor WASM client origin
.WithOrigins("https://localhost:5200")
// Allow any HTTP header (Content-Type, Accept, etc.)
.AllowAnyHeader()
// Allow any HTTP method (GET, POST, PUT, DELETE, etc.)
.AllowAnyMethod();
});
});
var app = builder.Build();
// --- Middleware Pipeline ---
// Middleware order matters in ASP.NET Core -- each middleware runs in the order
// it is registered. CORS must be applied before routing and authorization
// so that preflight (OPTIONS) requests are handled correctly.
// Apply the CORS policy globally -- every response includes the correct
// Access-Control-Allow-Origin header for the Blazor client's origin.
app.UseCors("AllowBlazorClient");
// UseAuthorization() enables the authorization middleware. Even though we have
// no auth in Phase 1, it is included because ASP.NET Core expects it in the
// pipeline when controllers are used. It is a no-op without [Authorize] attributes.
app.UseAuthorization();
// MapControllers() scans the assembly for all classes with [ApiController]
// and maps their routes. This is what connects HealthController's
// [Route("api/[controller]")] to the URL path /api/health.
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:7000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7100;http://localhost:7000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,12 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.14" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.14" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ChatAgent.Shared\ChatAgent.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,23 @@
@* MainLayout.razor -- The root layout component for the application.
In Blazor, layout components wrap page content. Every routed page (@page)
is rendered inside the layout's @Body placeholder. This is similar to
_Layout.cshtml in MVC or master pages in Web Forms.
Phase 1 uses a minimal layout -- just centered content with padding.
Later phases will add a sidebar for conversation management.
*@
@* @inherits LayoutComponentBase makes this a layout component.
LayoutComponentBase provides the Body property, which is a RenderFragment
containing the routed page's content. Without this base class, @Body
would not be available. *@
@inherits LayoutComponentBase
<main>
@* @Body is where the routed page content renders.
When the user navigates to "/", the Home.razor component's markup
appears here. When they navigate to another @page, that component
renders here instead. The layout stays the same -- only @Body changes. *@
@Body
</main>

View File

@@ -0,0 +1,77 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}

View File

@@ -0,0 +1,39 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">ChatAgent.Client</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="nav flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>
@code {
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@@ -0,0 +1,83 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -0,0 +1,18 @@
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -0,0 +1,86 @@
@* Home.razor -- The landing page for ChatAgent.
@page "/" maps this component to the root URL. When a user navigates to "/",
the Blazor router renders this component inside MainLayout's @Body placeholder.
This page demonstrates the health check round-trip:
1. On load, it calls the API's /api/health endpoint via ChatApiClient
2. Displays the server's health status and timestamp
3. Proves CORS is working (WASM on :5200 calling API on :7100)
*@
@* @page directive maps this component to a URL route.
"/" means this is the default/home page. *@
@page "/"
@* Import the service and model namespaces for this component.
These could also be in _Imports.razor for global access. *@
@using ChatAgent.Client.Services
@using ChatAgent.Shared.Models
@* @inject requests a service from the Dependency Injection (DI) container.
ChatApiClient was registered in Program.cs via AddHttpClient<ChatApiClient>.
Blazor creates one instance per component and injects it here. *@
@inject ChatApiClient ApiClient
<PageTitle>Chat Agent</PageTitle>
<h1>Chat Agent</h1>
@* Conditional rendering: Blazor re-renders the component after OnInitializedAsync completes.
We show different content based on the state of our data fields. *@
@if (_healthResponse is null && _error is null)
{
@* Loading state: OnInitializedAsync has not completed yet *@
<p class="loading">Checking API connection...</p>
}
else if (_healthResponse is not null)
{
@* Success state: API responded with a HealthResponse *@
<div class="health-status">
<p><strong>API Status:</strong> @_healthResponse.Status</p>
<p><strong>Server Time:</strong> @_healthResponse.Timestamp.ToString("yyyy-MM-dd HH:mm:ss UTC")</p>
</div>
}
else if (_error is not null)
{
@* Error state: the API call failed (network error, CORS blocked, server down, etc.) *@
<div class="error-message">
<p><strong>Connection Error:</strong> @_error</p>
</div>
}
@code {
// Private fields to hold the health check result or error message.
// Blazor components use private fields for state that drives the UI.
private HealthResponse? _healthResponse;
private string? _error;
/// <summary>
/// OnInitializedAsync is a Blazor lifecycle method called once when the component
/// is first rendered. It runs after the component receives its initial parameters.
///
/// Because it returns Task, Blazor awaits it and automatically calls
/// StateHasChanged() when it completes, triggering a re-render.
/// This means we do NOT need to call StateHasChanged() manually here.
///
/// The component renders twice: once immediately (showing "Checking..."),
/// and again after this method completes (showing the result or error).
/// </summary>
protected override async Task OnInitializedAsync()
{
try
{
// Call the API health endpoint via our typed HttpClient wrapper.
// This makes an HTTP GET to https://localhost:7100/api/health.
_healthResponse = await ApiClient.GetHealthAsync();
}
catch (Exception ex)
{
// Catch any exception (network error, CORS block, timeout, etc.)
// and store the message for display in the error UI.
_error = $"Failed to reach API: {ex.Message}";
}
}
}

View File

@@ -0,0 +1,57 @@
@page "/weather"
@inject HttpClient Http
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Farenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
}
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -0,0 +1,52 @@
// Program.cs -- Blazor WebAssembly application entry point for ChatAgent.
//
// This is where the WASM client is configured and launched. Unlike a server-side
// ASP.NET Core app, a Blazor WASM app runs entirely in the browser. The
// WebAssemblyHostBuilder configures:
// - Root components (what gets rendered into the HTML page)
// - Services (dependency injection container, similar to server-side DI)
// - Configuration (reads from wwwroot/appsettings.json)
//
// This file will grow as we add more services in later phases (e.g., chat state management).
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using ChatAgent.Client;
using ChatAgent.Client.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// Add<App>("#app") tells Blazor to render the App component inside the
// <div id="app"> element in wwwroot/index.html. This is the "mount point"
// for the entire Blazor component tree.
builder.RootComponents.Add<App>("#app");
// HeadOutlet allows Razor components to modify <head> elements (e.g., <title>)
// using the <PageTitle> and <HeadContent> components. "head::after" means
// content is appended after existing head elements.
builder.RootComponents.Add<HeadOutlet>("head::after");
// Determine the API base URL by matching the protocol the app was loaded with.
// In Blazor WASM, builder.HostEnvironment.BaseAddress is the URL the browser
// used to load the app (e.g., "http://localhost:5100/" or "https://localhost:5200/").
// We check if the app was loaded over HTTPS and pick the corresponding API URL.
// IMPORTANT: wwwroot/ files are PUBLIC -- they are downloaded to the browser.
// Never put secrets (API keys, passwords) in appsettings.json for a WASM app.
// The API key lives server-side in the ChatAgent.Api project.
var isHttps = builder.HostEnvironment.BaseAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
var apiBaseUrl = isHttps
? builder.Configuration["ApiBaseUrl_Https"] ?? "https://localhost:7100"
: builder.Configuration["ApiBaseUrl_Http"] ?? "http://localhost:7000";
// AddHttpClient<ChatApiClient> registers a typed HttpClient using IHttpClientFactory.
// IHttpClientFactory manages the underlying HttpMessageHandler lifetime to prevent
// socket exhaustion (a common problem with raw HttpClient in long-running apps).
// The lambda configures the client with the API base URL so ChatApiClient
// does not need to know the URL -- it is injected with a pre-configured HttpClient.
// In Blazor WASM, HttpClient uses the browser's Fetch API under the hood.
builder.Services.AddHttpClient<ChatApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
await builder.Build().RunAsync();

View File

@@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5100",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:5200;http://localhost:5100",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,53 @@
// ChatApiClient.cs -- Typed HttpClient wrapper for communicating with the ChatAgent API.
//
// WHY A TYPED CLIENT? Instead of injecting HttpClient directly into components,
// we wrap it in a dedicated service class. This follows the "typed client" pattern (D-04):
// - Components depend on ChatApiClient, not HttpClient (loose coupling)
// - API URL paths are centralized here, not scattered across components
// - Easy to mock for testing -- swap ChatApiClient, not HttpClient
// - IHttpClientFactory manages the underlying HttpClient lifetime
//
// In Blazor WASM, HttpClient is backed by the browser's Fetch API.
// The base URL is configured in Program.cs via AddHttpClient<ChatApiClient>.
using System.Net.Http.Json;
using ChatAgent.Shared.Models;
namespace ChatAgent.Client.Services
{
/// <summary>
/// Typed HttpClient for the ChatAgent API. Each public method maps to one API endpoint.
/// Constructor injection of HttpClient is provided by IHttpClientFactory,
/// which was configured in Program.cs with the API base URL.
/// </summary>
public class ChatApiClient
{
// The HttpClient instance is injected by the DI container (IHttpClientFactory).
// It is pre-configured with BaseAddress pointing to the API server.
private readonly HttpClient _httpClient;
/// <summary>
/// Constructor receives an HttpClient from IHttpClientFactory DI registration.
/// The client is already configured with the API base URL.
/// </summary>
public ChatApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
/// <summary>
/// Calls GET /api/health on the API server and deserializes the JSON response
/// into a HealthResponse object. Returns null if deserialization fails.
///
/// GetFromJsonAsync is an extension method from System.Net.Http.Json that
/// combines the HTTP GET call and JSON deserialization into a single step.
/// It uses System.Text.Json internally (the built-in .NET JSON serializer).
/// </summary>
public async Task<HealthResponse?> GetHealthAsync()
{
// "api/health" is a relative URL -- it is appended to the BaseAddress
// configured in Program.cs (e.g., https://localhost:7100/api/health).
return await _httpClient.GetFromJsonAsync<HealthResponse>("api/health");
}
}
}

View File

@@ -0,0 +1,19 @@
@* _Imports.razor -- Global using directives for all .razor files in this project.
Any @using directive placed here is automatically available in every .razor file
in the ChatAgent.Client project. This avoids repeating common imports in each component.
It works like a "global usings" file but specifically for Razor components.
*@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using ChatAgent.Client
@using ChatAgent.Client.Layout
@using ChatAgent.Client.Services
@using ChatAgent.Shared.Models

View File

@@ -0,0 +1,4 @@
{
"ApiBaseUrl_Http": "http://localhost:7000",
"ApiBaseUrl_Https": "https://localhost:7100"
}

View File

@@ -0,0 +1,117 @@
/* app.css -- Application styles for ChatAgent (Phase 1).
*
* Phase 1 uses plain HTML/CSS (D-10) with a light theme (D-11).
* MudBlazor will be introduced in Phase 5 for UI polish.
* These styles provide a clean, minimal appearance for the health check page.
*/
html, body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 0;
background-color: #ffffff;
color: #333333;
}
main {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
h1 {
color: #1a1a1a;
margin-bottom: 1.5rem;
}
.health-status {
padding: 1rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: #f9f9f9;
}
.health-status p {
margin: 0.5rem 0;
}
.error-message {
color: #d32f2f;
padding: 1rem;
border: 1px solid #d32f2f;
border-radius: 8px;
background-color: #fce4ec;
}
.loading {
color: #666666;
font-style: italic;
}
/* Blazor error UI -- shown when an unhandled exception occurs.
* This is built into the Blazor template's index.html and should be kept. */
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
/* Loading progress indicator -- shown while the WASM runtime downloads.
* This SVG-based progress circle is defined in index.html. */
.loading-progress {
position: relative;
display: block;
width: 8rem;
height: 8rem;
margin: 20vh auto 1rem auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ChatAgent.Client</title>
<base href="/" />
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="ChatAgent.Client.styles.css" rel="stylesheet" />
</head>
<body>
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
[
{
"date": "2022-01-06",
"temperatureC": 1,
"summary": "Freezing"
},
{
"date": "2022-01-07",
"temperatureC": 14,
"summary": "Bracing"
},
{
"date": "2022-01-08",
"temperatureC": -13,
"summary": "Freezing"
},
{
"date": "2022-01-09",
"temperatureC": -16,
"summary": "Balmy"
},
{
"date": "2022-01-10",
"temperatureC": -2,
"summary": "Chilly"
}
]

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,32 @@
// HealthResponse.cs -- Shared Data Transfer Object (DTO) for the health check endpoint.
//
// WHY SHARED? This class lives in the ChatAgent.Shared project, which is referenced by
// both the API (ChatAgent.Api) and the client (ChatAgent.Client). By sharing the type:
// - The API serializes a HealthResponse to JSON when responding to GET /api/health
// - The Client deserializes that same JSON back into a HealthResponse object
// - Both sides agree on the shape of the data -- no mismatched property names or types
//
// This is the "shared contract" pattern: one type definition, two consumers.
namespace ChatAgent.Shared.Models
{
/// <summary>
/// Data Transfer Object (DTO) returned by the API health check endpoint.
/// Contains the server's health status and current timestamp to prove
/// the response is live (not cached or stale).
/// </summary>
public class HealthResponse
{
/// <summary>
/// Server health status string (e.g., "healthy").
/// Initialized to empty string to avoid null warnings with nullable enabled.
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// Server-side UTC timestamp at the moment the health check ran.
/// The client displays this to confirm the response is fresh.
/// </summary>
public DateTime Timestamp { get; set; }
}
}