Compare commits
1 Commits
7a5c22593a
...
chatendpoi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
711df97ce9 |
@@ -1,242 +0,0 @@
|
||||
---
|
||||
name: "OPSX: Bulk Archive"
|
||||
description: Archive multiple completed changes at once
|
||||
category: Workflow
|
||||
tags: [workflow, archive, experimental, bulk]
|
||||
---
|
||||
|
||||
Archive multiple completed changes in a single operation.
|
||||
|
||||
This skill allows you to batch-archive changes, handling spec conflicts intelligently by checking the codebase to determine what's actually implemented.
|
||||
|
||||
**Input**: None required (prompts for selection)
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Get active changes**
|
||||
|
||||
Run `openspec list --json` to get all active changes.
|
||||
|
||||
If no active changes exist, inform user and stop.
|
||||
|
||||
2. **Prompt for change selection**
|
||||
|
||||
Use **AskUserQuestion tool** with multi-select to let user choose changes:
|
||||
- Show each change with its schema
|
||||
- Include an option for "All changes"
|
||||
- Allow any number of selections (1+ works, 2+ is the typical use case)
|
||||
|
||||
**IMPORTANT**: Do NOT auto-select. Always let the user choose.
|
||||
|
||||
3. **Batch validation - gather status for all selected changes**
|
||||
|
||||
For each selected change, collect:
|
||||
|
||||
a. **Artifact status** - Run `openspec status --change "<name>" --json`
|
||||
- Parse `schemaName` and `artifacts` list
|
||||
- Note which artifacts are `done` vs other states
|
||||
|
||||
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
|
||||
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
|
||||
- If no tasks file exists, note as "No tasks"
|
||||
|
||||
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
|
||||
- List which capability specs exist
|
||||
- For each, extract requirement names (lines matching `### Requirement: <name>`)
|
||||
|
||||
4. **Detect spec conflicts**
|
||||
|
||||
Build a map of `capability -> [changes that touch it]`:
|
||||
|
||||
```
|
||||
auth -> [change-a, change-b] <- CONFLICT (2+ changes)
|
||||
api -> [change-c] <- OK (only 1 change)
|
||||
```
|
||||
|
||||
A conflict exists when 2+ selected changes have delta specs for the same capability.
|
||||
|
||||
5. **Resolve conflicts agentically**
|
||||
|
||||
**For each conflict**, investigate the codebase:
|
||||
|
||||
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
|
||||
|
||||
b. **Search the codebase** for implementation evidence:
|
||||
- Look for code implementing requirements from each delta spec
|
||||
- Check for related files, functions, or tests
|
||||
|
||||
c. **Determine resolution**:
|
||||
- If only one change is actually implemented -> sync that one's specs
|
||||
- If both implemented -> apply in chronological order (older first, newer overwrites)
|
||||
- If neither implemented -> skip spec sync, warn user
|
||||
|
||||
d. **Record resolution** for each conflict:
|
||||
- Which change's specs to apply
|
||||
- In what order (if both)
|
||||
- Rationale (what was found in codebase)
|
||||
|
||||
6. **Show consolidated status table**
|
||||
|
||||
Display a table summarizing all changes:
|
||||
|
||||
```
|
||||
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
||||
|---------------------|-----------|-------|---------|-----------|--------|
|
||||
| schema-management | Done | 5/5 | 2 delta | None | Ready |
|
||||
| project-config | Done | 3/3 | 1 delta | None | Ready |
|
||||
| add-oauth | Done | 4/4 | 1 delta | auth (!) | Ready* |
|
||||
| add-verify-skill | 1 left | 2/5 | None | None | Warn |
|
||||
```
|
||||
|
||||
For conflicts, show the resolution:
|
||||
```
|
||||
* Conflict resolution:
|
||||
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
|
||||
```
|
||||
|
||||
For incomplete changes, show warnings:
|
||||
```
|
||||
Warnings:
|
||||
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
|
||||
```
|
||||
|
||||
7. **Confirm batch operation**
|
||||
|
||||
Use **AskUserQuestion tool** with a single confirmation:
|
||||
|
||||
- "Archive N changes?" with options based on status
|
||||
- Options might include:
|
||||
- "Archive all N changes"
|
||||
- "Archive only N ready changes (skip incomplete)"
|
||||
- "Cancel"
|
||||
|
||||
If there are incomplete changes, make clear they'll be archived with warnings.
|
||||
|
||||
8. **Execute archive for each confirmed change**
|
||||
|
||||
Process changes in the determined order (respecting conflict resolution):
|
||||
|
||||
a. **Sync specs** if delta specs exist:
|
||||
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
|
||||
- For conflicts, apply in resolved order
|
||||
- Track if sync was done
|
||||
|
||||
b. **Perform the archive**:
|
||||
```bash
|
||||
mkdir -p openspec/changes/archive
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
c. **Track outcome** for each change:
|
||||
- Success: archived successfully
|
||||
- Failed: error during archive (record error)
|
||||
- Skipped: user chose not to archive (if applicable)
|
||||
|
||||
9. **Display summary**
|
||||
|
||||
Show final results:
|
||||
|
||||
```
|
||||
## Bulk Archive Complete
|
||||
|
||||
Archived 3 changes:
|
||||
- schema-management-cli -> archive/2026-01-19-schema-management-cli/
|
||||
- project-config -> archive/2026-01-19-project-config/
|
||||
- add-oauth -> archive/2026-01-19-add-oauth/
|
||||
|
||||
Skipped 1 change:
|
||||
- add-verify-skill (user chose not to archive incomplete)
|
||||
|
||||
Spec sync summary:
|
||||
- 4 delta specs synced to main specs
|
||||
- 1 conflict resolved (auth: applied both in chronological order)
|
||||
```
|
||||
|
||||
If any failures:
|
||||
```
|
||||
Failed 1 change:
|
||||
- some-change: Archive directory already exists
|
||||
```
|
||||
|
||||
**Conflict Resolution Examples**
|
||||
|
||||
Example 1: Only one implemented
|
||||
```
|
||||
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
|
||||
|
||||
Checking add-oauth:
|
||||
- Delta adds "OAuth Provider Integration" requirement
|
||||
- Searching codebase... found src/auth/oauth.ts implementing OAuth flow
|
||||
|
||||
Checking add-jwt:
|
||||
- Delta adds "JWT Token Handling" requirement
|
||||
- Searching codebase... no JWT implementation found
|
||||
|
||||
Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
|
||||
```
|
||||
|
||||
Example 2: Both implemented
|
||||
```
|
||||
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
|
||||
|
||||
Checking add-rest-api (created 2026-01-10):
|
||||
- Delta adds "REST Endpoints" requirement
|
||||
- Searching codebase... found src/api/rest.ts
|
||||
|
||||
Checking add-graphql (created 2026-01-15):
|
||||
- Delta adds "GraphQL Schema" requirement
|
||||
- Searching codebase... found src/api/graphql.ts
|
||||
|
||||
Resolution: Both implemented. Will apply add-rest-api specs first,
|
||||
then add-graphql specs (chronological order, newer takes precedence).
|
||||
```
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Bulk Archive Complete
|
||||
|
||||
Archived N changes:
|
||||
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
|
||||
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
|
||||
|
||||
Spec sync summary:
|
||||
- N delta specs synced to main specs
|
||||
- No conflicts (or: M conflicts resolved)
|
||||
```
|
||||
|
||||
**Output On Partial Success**
|
||||
|
||||
```
|
||||
## Bulk Archive Complete (partial)
|
||||
|
||||
Archived N changes:
|
||||
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
|
||||
|
||||
Skipped M changes:
|
||||
- <change-2> (user chose not to archive incomplete)
|
||||
|
||||
Failed K changes:
|
||||
- <change-3>: Archive directory already exists
|
||||
```
|
||||
|
||||
**Output When No Changes**
|
||||
|
||||
```
|
||||
## No Changes to Archive
|
||||
|
||||
No active changes found. Create a new change to get started.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
|
||||
- Always prompt for selection, never auto-select
|
||||
- Detect spec conflicts early and resolve by checking codebase
|
||||
- When both changes are implemented, apply specs in chronological order
|
||||
- Skip spec sync only when implementation is missing (warn user)
|
||||
- Show clear per-change status before confirming
|
||||
- Use single confirmation for entire batch
|
||||
- Track and report all outcomes (success/skip/fail)
|
||||
- Preserve .openspec.yaml when moving to archive
|
||||
- Archive directory target uses current date: YYYY-MM-DD-<name>
|
||||
- If archive target exists, fail that change but continue with others
|
||||
@@ -1,114 +0,0 @@
|
||||
---
|
||||
name: "OPSX: Continue"
|
||||
description: Continue working on a change - create the next artifact (Experimental)
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Continue working on a change by creating the next artifact.
|
||||
|
||||
**Input**: Optionally specify a change name after `/opsx:continue` (e.g., `/opsx:continue add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to work on.
|
||||
|
||||
Present the top 3-4 most recently modified changes as options, showing:
|
||||
- Change name
|
||||
- Schema (from `schema` field if present, otherwise "spec-driven")
|
||||
- Status (e.g., "0/5 tasks", "complete", "no tasks")
|
||||
- How recently it was modified (from `lastModified` field)
|
||||
|
||||
Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to continue.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check current status**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand current state. The response includes:
|
||||
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
|
||||
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
|
||||
- `isComplete`: Boolean indicating if all artifacts are complete
|
||||
|
||||
3. **Act based on status**:
|
||||
|
||||
---
|
||||
|
||||
**If all artifacts are complete (`isComplete: true`)**:
|
||||
- Congratulate the user
|
||||
- Show final status including the schema used
|
||||
- Suggest: "All artifacts created! You can now implement this change with `/opsx:apply` or archive it with `/opsx:archive`."
|
||||
- STOP
|
||||
|
||||
---
|
||||
|
||||
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
|
||||
- Pick the FIRST artifact with `status: "ready"` from the status output
|
||||
- Get its instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- Parse the JSON. The key fields are:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- **Create the artifact file**:
|
||||
- Read any completed dependency files for context
|
||||
- Use `template` as the structure - fill in its sections
|
||||
- Apply `context` and `rules` as constraints when writing - but do NOT copy them into the file
|
||||
- Write to the output path specified in instructions
|
||||
- Show what was created and what's now unlocked
|
||||
- STOP after creating ONE artifact
|
||||
|
||||
---
|
||||
|
||||
**If no artifacts are ready (all blocked)**:
|
||||
- This shouldn't happen with a valid schema
|
||||
- Show status and suggest checking for issues
|
||||
|
||||
4. **After creating an artifact, show progress**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After each invocation, show:
|
||||
- Which artifact was created
|
||||
- Schema workflow being used
|
||||
- Current progress (N/M complete)
|
||||
- What artifacts are now unlocked
|
||||
- Prompt: "Run `/opsx:continue` to create the next artifact"
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
The artifact types and their purpose depend on the schema. Use the `instruction` field from the instructions output to understand what to create.
|
||||
|
||||
Common artifact patterns:
|
||||
|
||||
**spec-driven schema** (proposal → specs → design → tasks):
|
||||
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
||||
- The Capabilities section is critical - each capability listed will need a spec file.
|
||||
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
|
||||
- **design.md**: Document technical decisions, architecture, and implementation approach.
|
||||
- **tasks.md**: Break down implementation into checkboxed tasks.
|
||||
|
||||
For other schemas, follow the `instruction` field from the CLI output.
|
||||
|
||||
**Guardrails**
|
||||
- Create ONE artifact per invocation
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- Never skip artifacts or create out of order
|
||||
- If context is unclear, ask the user before creating
|
||||
- Verify the artifact file exists after writing before marking progress
|
||||
- Use the schema's artifact sequence, don't assume specific artifact names
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
@@ -1,131 +0,0 @@
|
||||
---
|
||||
name: "OPSX: Export Spec"
|
||||
description: Export a feature as a compact, portable spec for AI-assisted reimplementation on a sandboxed machine
|
||||
category: Workflow
|
||||
tags: [workflow, portability, experimental]
|
||||
---
|
||||
|
||||
Export a feature as a portable spec for AI-assisted reimplementation.
|
||||
|
||||
Instead of retyping code, you retype a compact spec. The AI on the sandbox generates the code.
|
||||
|
||||
---
|
||||
|
||||
**Input**: The argument after `/opsx:export-spec` is a change name (active or archived), or a description of the feature. If omitted, prompt for selection.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Identify the source feature**
|
||||
|
||||
Same selection logic as `/opsx:extract-feature`:
|
||||
- Check active changes and archive for the change name
|
||||
- If not found, prompt with **AskUserQuestion tool**
|
||||
- Read all artifacts: `proposal.md`, `design.md`, `tasks.md`, specs
|
||||
|
||||
2. **Analyze dependency chain (cumulative mode)**
|
||||
|
||||
Features often build on each other. Before generating the spec, determine
|
||||
what the target feature depends on.
|
||||
|
||||
a. Read all archived change proposals in `openspec/changes/archive/` (sorted by date).
|
||||
b. Build a dependency graph: what does the target require? What's superseded?
|
||||
c. Ask the user:
|
||||
> "This feature depends on earlier changes. How should I scope the spec?"
|
||||
> 1. **Cumulative** — include all foundation (recommended for fresh codebase)
|
||||
> 2. **Delta only** — just this change (target already has foundation)
|
||||
> 3. **Custom** — pick which dependencies to include
|
||||
|
||||
d. In cumulative mode: merge into one coherent spec, skip superseded components.
|
||||
e. In delta mode: add an "Assumes" section listing what must already exist.
|
||||
|
||||
3. **Read the actual implementation**
|
||||
|
||||
Read all source files created or modified by this feature (and dependencies if cumulative).
|
||||
The spec must reflect what was actually built, not just what was planned.
|
||||
|
||||
4. **Determine the target context**
|
||||
|
||||
Use **AskUserQuestion tool** to ask:
|
||||
> "Tell me about the target codebase:
|
||||
> 1. Project name / root namespace
|
||||
> 2. Existing stack (ASP.NET Core? Blazor? MudBlazor?)
|
||||
> 3. Does it already have any of these? (controllers, DI setup, chat endpoint)
|
||||
> 4. Does the target have OpenSpec? GitHub Copilot? Claude Code?"
|
||||
|
||||
This shapes what the spec assumes vs what it must specify.
|
||||
|
||||
5. **Generate the portable spec**
|
||||
|
||||
Create a single markdown document that is:
|
||||
- **Compact**: Target ~30-50 lines for a medium feature
|
||||
- **Precise**: Unambiguous enough for an AI to implement correctly
|
||||
- **Self-contained**: No references to external files or repos
|
||||
- **Stack-aware**: Uses the right terminology for the target stack
|
||||
|
||||
Structure:
|
||||
|
||||
```markdown
|
||||
# Feature: <Name>
|
||||
## Target: <project name> (<stack>)
|
||||
|
||||
## Packages
|
||||
<list with versions>
|
||||
|
||||
## Architecture
|
||||
<2-3 sentence overview>
|
||||
|
||||
## Components
|
||||
### <Component>: <path hint>
|
||||
- <What it does>
|
||||
- <Key behavior>
|
||||
- <Interface/contract>
|
||||
|
||||
## Contracts
|
||||
<API shapes, model definitions — things that MUST be exact>
|
||||
|
||||
## Wiring
|
||||
<DI registration, middleware order, config keys — dependency order>
|
||||
|
||||
## Behavior
|
||||
<Non-obvious requirements>
|
||||
```
|
||||
|
||||
**Compression strategies:**
|
||||
- Use bullet points, not prose
|
||||
- Specify contracts precisely (field names, types, API shapes)
|
||||
- Let the AI infer standard patterns
|
||||
- Only specify non-obvious behavior
|
||||
- Omit anything the AI would do by default
|
||||
|
||||
6. **Estimate typing effort**
|
||||
|
||||
Count characters in the spec. Compare to the code recipe equivalent.
|
||||
Show the compression ratio.
|
||||
|
||||
7. **Optionally generate an OpenSpec-compatible version**
|
||||
|
||||
If the target has OpenSpec, also generate:
|
||||
- A `proposal.md` (minimal — 5-10 lines)
|
||||
- A `tasks.md` (implementation steps)
|
||||
|
||||
Save as: `openspec/exports/<change-name>-openspec.md`
|
||||
|
||||
8. **Write the output**
|
||||
|
||||
Save to: `openspec/exports/<change-name>-spec.md`
|
||||
Display the full content for review.
|
||||
|
||||
**Guardrails**
|
||||
- Prioritize precision over brevity — ambiguity wastes more time than length
|
||||
- Always include exact field names, types, and API shapes
|
||||
- Include non-obvious gotchas (like /v1 base URL requirements)
|
||||
- Mental test: could an AI implement this correctly without seeing the original code?
|
||||
- If too complex for ~50 lines, split into multiple specs by component
|
||||
- Always show the compression ratio
|
||||
- Must be readable when printed in monospace — no wide tables or long lines
|
||||
- In cumulative mode, the spec must read as one coherent feature, not a list of changes
|
||||
- Skip superseded components — always describe the latest version
|
||||
- In delta mode, add an "Assumes" section so the target AI knows what must exist
|
||||
- In the output header, note which changes were included and which were skipped
|
||||
|
||||
ARGUMENTS: based on the above
|
||||
@@ -1,113 +0,0 @@
|
||||
---
|
||||
name: "OPSX: Extract Feature"
|
||||
description: Extract a feature into a minimal, printable code recipe for manual reimplementation
|
||||
category: Workflow
|
||||
tags: [workflow, portability, experimental]
|
||||
---
|
||||
|
||||
Extract a feature into a minimal, printable code recipe for manual reimplementation.
|
||||
|
||||
Generates a markdown document with:
|
||||
- Package dependencies
|
||||
- Ordered code blocks (no comments, no boilerplate)
|
||||
- Clear markers for generic vs domain-specific code
|
||||
|
||||
Print it, take it to the sandbox, type it in.
|
||||
|
||||
---
|
||||
|
||||
**Input**: The argument after `/opsx:extract-feature` is a change name (active or archived), a git commit range, or a list of files. If omitted, prompt for selection.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Identify the source feature**
|
||||
|
||||
If a change name is provided:
|
||||
- Check active changes: `openspec list --json`
|
||||
- Check archive: look in `openspec/changes/archive/` for directories ending with the name
|
||||
- If found, read its artifacts: `proposal.md`, `design.md`, `tasks.md`
|
||||
|
||||
If no name provided:
|
||||
- Run `openspec list --json` and list archived changes
|
||||
- Use **AskUserQuestion tool** to let the user select
|
||||
|
||||
If a file list or commit range is provided instead:
|
||||
- Read those files directly
|
||||
- Identify the feature from the code
|
||||
|
||||
2. **Analyze dependency chain (cumulative mode)**
|
||||
|
||||
Features often build on each other. Before generating the recipe, determine
|
||||
what the target feature depends on.
|
||||
|
||||
a. Read all archived change proposals in `openspec/changes/archive/` (sorted by date).
|
||||
b. Build a dependency graph: what does the target require? What's superseded?
|
||||
c. Ask the user:
|
||||
> "This feature depends on earlier changes. How should I scope the recipe?"
|
||||
> 1. **Cumulative** — include all foundation code (recommended for fresh codebase)
|
||||
> 2. **Delta only** — just this change's code (target already has foundation)
|
||||
> 3. **Custom** — pick which dependencies to include
|
||||
|
||||
d. In cumulative mode: merge the chain, skip superseded code, use final file versions.
|
||||
e. In delta mode: include only the selected change's code.
|
||||
|
||||
3. **Analyze the feature scope**
|
||||
|
||||
From the change artifacts and/or code (across full dependency chain if cumulative), determine:
|
||||
- Which files were created or modified
|
||||
- What NuGet/npm packages were added
|
||||
- What the dependency order is (e.g., models before controllers)
|
||||
- What is generic infrastructure vs domain-specific logic
|
||||
|
||||
Read all relevant source files. Always read the final current version of each file.
|
||||
|
||||
4. **Generate the code recipe**
|
||||
|
||||
Create a markdown document with these sections, in this order:
|
||||
|
||||
**Header**: Feature name, source change, date
|
||||
|
||||
**Prerequisites**: Package references with exact versions
|
||||
|
||||
**Steps**: Ordered by dependency. Each step contains:
|
||||
- Step number and action (e.g., "New file", "Add to existing file", "Modify")
|
||||
- Target path (relative, adaptable)
|
||||
- Namespace placeholder: `__YOUR_NAMESPACE__` where the target project namespace goes
|
||||
- **Code block**: The actual code, stripped of:
|
||||
- All comments (// and /* */ and /// XML docs)
|
||||
- Redundant blank lines
|
||||
- Verbose variable names that can be shortened
|
||||
- Any code not directly related to the feature
|
||||
- If the step modifies an existing file: show only the code to add, with a brief marker for insertion point (e.g., "Add after AddControllers()")
|
||||
|
||||
**Domain-Specific Sections**: Clearly marked with `// ADAPT:` prefix explaining what to change for the target domain
|
||||
|
||||
5. **Optimize for retyping**
|
||||
|
||||
Review the generated document and:
|
||||
- Merge small files if they can be combined
|
||||
- Remove any using statements that the IDE will auto-add
|
||||
- Shorten any unnecessarily verbose code
|
||||
- Ensure no step exceeds ~40 lines (split if needed)
|
||||
- Add line counts per step so the user can estimate effort
|
||||
- Total the overall line count at the top
|
||||
|
||||
6. **Write the output**
|
||||
|
||||
Save to: `openspec/exports/<change-name>-recipe.md`
|
||||
|
||||
Also display the full content so the user can review it immediately.
|
||||
|
||||
**Guardrails**
|
||||
- Never include comments in code blocks — the goal is minimum keystrokes
|
||||
- Always read the actual current source files, not just the change artifacts
|
||||
- Preserve compilation order: models -> services -> controllers -> DI registration
|
||||
- Mark domain-specific code clearly so the user knows what to adapt vs copy verbatim
|
||||
- Keep each step self-contained — the user may take breaks between steps
|
||||
- If a feature spans more than ~200 lines of stripped code, warn the user and suggest using `/opsx:export-spec` instead
|
||||
- Output must be valid markdown that renders well when printed
|
||||
- When in cumulative mode, skip superseded code — always use the latest version of each file
|
||||
- In the output header, show which changes were included and which were skipped
|
||||
- If a dependency chain is long (4+ changes), suggest `/opsx:export-spec` as more efficient
|
||||
|
||||
ARGUMENTS: based on the above
|
||||
@@ -1,97 +0,0 @@
|
||||
---
|
||||
name: "OPSX: Fast Forward"
|
||||
description: Create a change and generate all artifacts needed for implementation in one go
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Fast-forward through artifact creation - generate everything needed to start implementation.
|
||||
|
||||
**Input**: The argument after `/opsx:ff` is the change name (kebab-case), OR a description of what the user wants to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to get:
|
||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||
- `artifacts`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance for this artifact type
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "✓ Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` to start implementing."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
name: "OPSX: New"
|
||||
description: Start a new change using the experimental artifact workflow (OPSX)
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Start a new change using the experimental artifact-driven approach.
|
||||
|
||||
**Input**: The argument after `/opsx:new` is the change name (kebab-case), OR a description of what the user wants to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Determine the workflow schema**
|
||||
|
||||
Use the default schema (omit `--schema`) unless the user explicitly requests a different workflow.
|
||||
|
||||
**Use a different schema only if the user mentions:**
|
||||
- A specific schema name → use `--schema <name>`
|
||||
- "show workflows" or "what workflows" → run `openspec schemas --json` and let them choose
|
||||
|
||||
**Otherwise**: Omit `--schema` to use the default.
|
||||
|
||||
3. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
Add `--schema <name>` only if the user requested a specific workflow.
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
|
||||
|
||||
4. **Show the artifact status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
||||
|
||||
5. **Get instructions for the first artifact**
|
||||
The first artifact depends on the schema. Check the status output to find the first artifact with status "ready".
|
||||
```bash
|
||||
openspec instructions <first-artifact-id> --change "<name>"
|
||||
```
|
||||
This outputs the template and context for creating the first artifact.
|
||||
|
||||
6. **STOP and wait for user direction**
|
||||
|
||||
**Output**
|
||||
|
||||
After completing the steps, summarize:
|
||||
- Change name and location
|
||||
- Schema/workflow being used and its artifact sequence
|
||||
- Current status (0/N artifacts complete)
|
||||
- The template for the first artifact
|
||||
- Prompt: "Ready to create the first artifact? Run `/opsx:continue` or just describe what this change is about and I'll draft it."
|
||||
|
||||
**Guardrails**
|
||||
- Do NOT create any artifacts yet - just show the instructions
|
||||
- Do NOT advance beyond showing the first artifact template
|
||||
- If the name is invalid (not kebab-case), ask for a valid name
|
||||
- If a change with that name already exists, suggest using `/opsx:continue` instead
|
||||
- Pass --schema if using a non-default workflow
|
||||
@@ -1,550 +0,0 @@
|
||||
---
|
||||
name: "OPSX: Onboard"
|
||||
description: Guided onboarding - walk through a complete OpenSpec workflow cycle with narration
|
||||
category: Workflow
|
||||
tags: [workflow, onboarding, tutorial, learning]
|
||||
---
|
||||
|
||||
Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step.
|
||||
|
||||
---
|
||||
|
||||
## Preflight
|
||||
|
||||
Before starting, check if the OpenSpec CLI is installed:
|
||||
|
||||
```bash
|
||||
# Unix/macOS
|
||||
openspec --version 2>&1 || echo "CLI_NOT_INSTALLED"
|
||||
# Windows (PowerShell)
|
||||
# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" }
|
||||
```
|
||||
|
||||
**If CLI not installed:**
|
||||
> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`.
|
||||
|
||||
Stop here if not installed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Welcome
|
||||
|
||||
Display:
|
||||
|
||||
```
|
||||
## Welcome to OpenSpec!
|
||||
|
||||
I'll walk you through a complete change cycle—from idea to implementation—using a real task in your codebase. Along the way, you'll learn the workflow by doing it.
|
||||
|
||||
**What we'll do:**
|
||||
1. Pick a small, real task in your codebase
|
||||
2. Explore the problem briefly
|
||||
3. Create a change (the container for our work)
|
||||
4. Build the artifacts: proposal → specs → design → tasks
|
||||
5. Implement the tasks
|
||||
6. Archive the completed change
|
||||
|
||||
**Time:** ~15-20 minutes
|
||||
|
||||
Let's start by finding something to work on.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Task Selection
|
||||
|
||||
### Codebase Analysis
|
||||
|
||||
Scan the codebase for small improvement opportunities. Look for:
|
||||
|
||||
1. **TODO/FIXME comments** - Search for `TODO`, `FIXME`, `HACK`, `XXX` in code files
|
||||
2. **Missing error handling** - `catch` blocks that swallow errors, risky operations without try-catch
|
||||
3. **Functions without tests** - Cross-reference `src/` with test directories
|
||||
4. **Type issues** - `any` types in TypeScript files (`: any`, `as any`)
|
||||
5. **Debug artifacts** - `console.log`, `console.debug`, `debugger` statements in non-debug code
|
||||
6. **Missing validation** - User input handlers without validation
|
||||
|
||||
Also check recent git activity:
|
||||
```bash
|
||||
# Unix/macOS
|
||||
git log --oneline -10 2>/dev/null || echo "No git history"
|
||||
# Windows (PowerShell)
|
||||
# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" }
|
||||
```
|
||||
|
||||
### Present Suggestions
|
||||
|
||||
From your analysis, present 3-4 specific suggestions:
|
||||
|
||||
```
|
||||
## Task Suggestions
|
||||
|
||||
Based on scanning your codebase, here are some good starter tasks:
|
||||
|
||||
**1. [Most promising task]**
|
||||
Location: `src/path/to/file.ts:42`
|
||||
Scope: ~1-2 files, ~20-30 lines
|
||||
Why it's good: [brief reason]
|
||||
|
||||
**2. [Second task]**
|
||||
Location: `src/another/file.ts`
|
||||
Scope: ~1 file, ~15 lines
|
||||
Why it's good: [brief reason]
|
||||
|
||||
**3. [Third task]**
|
||||
Location: [location]
|
||||
Scope: [estimate]
|
||||
Why it's good: [brief reason]
|
||||
|
||||
**4. Something else?**
|
||||
Tell me what you'd like to work on.
|
||||
|
||||
Which task interests you? (Pick a number or describe your own)
|
||||
```
|
||||
|
||||
**If nothing found:** Fall back to asking what the user wants to build:
|
||||
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
|
||||
|
||||
### Scope Guardrail
|
||||
|
||||
If the user picks or describes something too large (major feature, multi-day work):
|
||||
|
||||
```
|
||||
That's a valuable task, but it's probably larger than ideal for your first OpenSpec run-through.
|
||||
|
||||
For learning the workflow, smaller is better—it lets you see the full cycle without getting stuck in implementation details.
|
||||
|
||||
**Options:**
|
||||
1. **Slice it smaller** - What's the smallest useful piece of [their task]? Maybe just [specific slice]?
|
||||
2. **Pick something else** - One of the other suggestions, or a different small task?
|
||||
3. **Do it anyway** - If you really want to tackle this, we can. Just know it'll take longer.
|
||||
|
||||
What would you prefer?
|
||||
```
|
||||
|
||||
Let the user override if they insist—this is a soft guardrail.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Explore Demo
|
||||
|
||||
Once a task is selected, briefly demonstrate explore mode:
|
||||
|
||||
```
|
||||
Before we create a change, let me quickly show you **explore mode**—it's how you think through problems before committing to a direction.
|
||||
```
|
||||
|
||||
Spend 1-2 minutes investigating the relevant code:
|
||||
- Read the file(s) involved
|
||||
- Draw a quick ASCII diagram if it helps
|
||||
- Note any considerations
|
||||
|
||||
```
|
||||
## Quick Exploration
|
||||
|
||||
[Your brief analysis—what you found, any considerations]
|
||||
|
||||
┌─────────────────────────────────────────┐
|
||||
│ [Optional: ASCII diagram if helpful] │
|
||||
└─────────────────────────────────────────┘
|
||||
|
||||
Explore mode (`/opsx:explore`) is for this kind of thinking—investigating before implementing. You can use it anytime you need to think through a problem.
|
||||
|
||||
Now let's create a change to hold our work.
|
||||
```
|
||||
|
||||
**PAUSE** - Wait for user acknowledgment before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Create the Change
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Creating a Change
|
||||
|
||||
A "change" in OpenSpec is a container for all the thinking and planning around a piece of work. It lives in `openspec/changes/<name>/` and holds your artifacts—proposal, specs, design, tasks.
|
||||
|
||||
Let me create one for our task.
|
||||
```
|
||||
|
||||
**DO:** Create the change with a derived kebab-case name:
|
||||
```bash
|
||||
openspec new change "<derived-name>"
|
||||
```
|
||||
|
||||
**SHOW:**
|
||||
```
|
||||
Created: `openspec/changes/<name>/`
|
||||
|
||||
The folder structure:
|
||||
```
|
||||
openspec/changes/<name>/
|
||||
├── proposal.md ← Why we're doing this (empty, we'll fill it)
|
||||
├── design.md ← How we'll build it (empty)
|
||||
├── specs/ ← Detailed requirements (empty)
|
||||
└── tasks.md ← Implementation checklist (empty)
|
||||
```
|
||||
|
||||
Now let's fill in the first artifact—the proposal.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Proposal
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## The Proposal
|
||||
|
||||
The proposal captures **why** we're making this change and **what** it involves at a high level. It's the "elevator pitch" for the work.
|
||||
|
||||
I'll draft one based on our task.
|
||||
```
|
||||
|
||||
**DO:** Draft the proposal content (don't save yet):
|
||||
|
||||
```
|
||||
Here's a draft proposal:
|
||||
|
||||
---
|
||||
|
||||
## Why
|
||||
|
||||
[1-2 sentences explaining the problem/opportunity]
|
||||
|
||||
## What Changes
|
||||
|
||||
[Bullet points of what will be different]
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `<capability-name>`: [brief description]
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- If modifying existing behavior -->
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/path/to/file.ts`: [what changes]
|
||||
- [other files if applicable]
|
||||
|
||||
---
|
||||
|
||||
Does this capture the intent? I can adjust before we save it.
|
||||
```
|
||||
|
||||
**PAUSE** - Wait for user approval/feedback.
|
||||
|
||||
After approval, save the proposal:
|
||||
```bash
|
||||
openspec instructions proposal --change "<name>" --json
|
||||
```
|
||||
Then write the content to `openspec/changes/<name>/proposal.md`.
|
||||
|
||||
```
|
||||
Proposal saved. This is your "why" document—you can always come back and refine it as understanding evolves.
|
||||
|
||||
Next up: specs.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Specs
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Specs
|
||||
|
||||
Specs define **what** we're building in precise, testable terms. They use a requirement/scenario format that makes expected behavior crystal clear.
|
||||
|
||||
For a small task like this, we might only need one spec file.
|
||||
```
|
||||
|
||||
**DO:** Create the spec file:
|
||||
```bash
|
||||
# Unix/macOS
|
||||
mkdir -p openspec/changes/<name>/specs/<capability-name>
|
||||
# Windows (PowerShell)
|
||||
# New-Item -ItemType Directory -Force -Path "openspec/changes/<name>/specs/<capability-name>"
|
||||
```
|
||||
|
||||
Draft the spec content:
|
||||
|
||||
```
|
||||
Here's the spec:
|
||||
|
||||
---
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: <Name>
|
||||
|
||||
<Description of what the system should do>
|
||||
|
||||
#### Scenario: <Scenario name>
|
||||
|
||||
- **WHEN** <trigger condition>
|
||||
- **THEN** <expected outcome>
|
||||
- **AND** <additional outcome if needed>
|
||||
|
||||
---
|
||||
|
||||
This format—WHEN/THEN/AND—makes requirements testable. You can literally read them as test cases.
|
||||
```
|
||||
|
||||
Save to `openspec/changes/<name>/specs/<capability>/spec.md`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Design
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Design
|
||||
|
||||
The design captures **how** we'll build it—technical decisions, tradeoffs, approach.
|
||||
|
||||
For small changes, this might be brief. That's fine—not every change needs deep design discussion.
|
||||
```
|
||||
|
||||
**DO:** Draft design.md:
|
||||
|
||||
```
|
||||
Here's the design:
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
[Brief context about the current state]
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- [What we're trying to achieve]
|
||||
|
||||
**Non-Goals:**
|
||||
- [What's explicitly out of scope]
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: [Key decision]
|
||||
|
||||
[Explanation of approach and rationale]
|
||||
|
||||
---
|
||||
|
||||
For a small task, this captures the key decisions without over-engineering.
|
||||
```
|
||||
|
||||
Save to `openspec/changes/<name>/design.md`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Tasks
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Tasks
|
||||
|
||||
Finally, we break the work into implementation tasks—checkboxes that drive the apply phase.
|
||||
|
||||
These should be small, clear, and in logical order.
|
||||
```
|
||||
|
||||
**DO:** Generate tasks based on specs and design:
|
||||
|
||||
```
|
||||
Here are the implementation tasks:
|
||||
|
||||
---
|
||||
|
||||
## 1. [Category or file]
|
||||
|
||||
- [ ] 1.1 [Specific task]
|
||||
- [ ] 1.2 [Specific task]
|
||||
|
||||
## 2. Verify
|
||||
|
||||
- [ ] 2.1 [Verification step]
|
||||
|
||||
---
|
||||
|
||||
Each checkbox becomes a unit of work in the apply phase. Ready to implement?
|
||||
```
|
||||
|
||||
**PAUSE** - Wait for user to confirm they're ready to implement.
|
||||
|
||||
Save to `openspec/changes/<name>/tasks.md`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: Apply (Implementation)
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Implementation
|
||||
|
||||
Now we implement each task, checking them off as we go. I'll announce each one and occasionally note how the specs/design informed the approach.
|
||||
```
|
||||
|
||||
**DO:** For each task:
|
||||
|
||||
1. Announce: "Working on task N: [description]"
|
||||
2. Implement the change in the codebase
|
||||
3. Reference specs/design naturally: "The spec says X, so I'm doing Y"
|
||||
4. Mark complete in tasks.md: `- [ ]` → `- [x]`
|
||||
5. Brief status: "✓ Task N complete"
|
||||
|
||||
Keep narration light—don't over-explain every line of code.
|
||||
|
||||
After all tasks:
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
All tasks done:
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
- [x] ...
|
||||
|
||||
The change is implemented! One more step—let's archive it.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 10: Archive
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Archiving
|
||||
|
||||
When a change is complete, we archive it. This moves it from `openspec/changes/` to `openspec/changes/archive/YYYY-MM-DD-<name>/`.
|
||||
|
||||
Archived changes become your project's decision history—you can always find them later to understand why something was built a certain way.
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```bash
|
||||
openspec archive "<name>"
|
||||
```
|
||||
|
||||
**SHOW:**
|
||||
```
|
||||
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
|
||||
|
||||
The change is now part of your project's history. The code is in your codebase, the decision record is preserved.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 11: Recap & Next Steps
|
||||
|
||||
```
|
||||
## Congratulations!
|
||||
|
||||
You just completed a full OpenSpec cycle:
|
||||
|
||||
1. **Explore** - Thought through the problem
|
||||
2. **New** - Created a change container
|
||||
3. **Proposal** - Captured WHY
|
||||
4. **Specs** - Defined WHAT in detail
|
||||
5. **Design** - Decided HOW
|
||||
6. **Tasks** - Broke it into steps
|
||||
7. **Apply** - Implemented the work
|
||||
8. **Archive** - Preserved the record
|
||||
|
||||
This same rhythm works for any size change—a small fix or a major feature.
|
||||
|
||||
---
|
||||
|
||||
## Command Reference
|
||||
|
||||
**Core workflow:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:propose` | Create a change and generate all artifacts |
|
||||
| `/opsx:explore` | Think through problems before/during work |
|
||||
| `/opsx:apply` | Implement tasks from a change |
|
||||
| `/opsx:archive` | Archive a completed change |
|
||||
|
||||
**Additional commands:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:new` | Start a new change, step through artifacts one at a time |
|
||||
| `/opsx:continue` | Continue working on an existing change |
|
||||
| `/opsx:ff` | Fast-forward: create all artifacts at once |
|
||||
| `/opsx:verify` | Verify implementation matches artifacts |
|
||||
|
||||
---
|
||||
|
||||
## What's Next?
|
||||
|
||||
Try `/opsx:propose` on something you actually want to build. You've got the rhythm now!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Graceful Exit Handling
|
||||
|
||||
### User wants to stop mid-way
|
||||
|
||||
If the user says they need to stop, want to pause, or seem disengaged:
|
||||
|
||||
```
|
||||
No problem! Your change is saved at `openspec/changes/<name>/`.
|
||||
|
||||
To pick up where we left off later:
|
||||
- `/opsx:continue <name>` - Resume artifact creation
|
||||
- `/opsx:apply <name>` - Jump to implementation (if tasks exist)
|
||||
|
||||
The work won't be lost. Come back whenever you're ready.
|
||||
```
|
||||
|
||||
Exit gracefully without pressure.
|
||||
|
||||
### User just wants command reference
|
||||
|
||||
If the user says they just want to see the commands or skip the tutorial:
|
||||
|
||||
```
|
||||
## OpenSpec Quick Reference
|
||||
|
||||
**Core workflow:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:propose <name>` | Create a change and generate all artifacts |
|
||||
| `/opsx:explore` | Think through problems (no code changes) |
|
||||
| `/opsx:apply <name>` | Implement tasks |
|
||||
| `/opsx:archive <name>` | Archive when done |
|
||||
|
||||
**Additional commands:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:new <name>` | Start a new change, step by step |
|
||||
| `/opsx:continue <name>` | Continue an existing change |
|
||||
| `/opsx:ff <name>` | Fast-forward: all artifacts at once |
|
||||
| `/opsx:verify <name>` | Verify implementation |
|
||||
|
||||
Try `/opsx:propose` to start your first change.
|
||||
```
|
||||
|
||||
Exit gracefully.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Follow the EXPLAIN → DO → SHOW → PAUSE pattern** at key transitions (after explore, after proposal draft, after tasks, after archive)
|
||||
- **Keep narration light** during implementation—teach without lecturing
|
||||
- **Don't skip phases** even if the change is small—the goal is teaching the workflow
|
||||
- **Pause for acknowledgment** at marked points, but don't over-pause
|
||||
- **Handle exits gracefully**—never pressure the user to continue
|
||||
- **Use real codebase tasks**—don't simulate or use fake examples
|
||||
- **Adjust scope gently**—guide toward smaller tasks but respect user choice
|
||||
@@ -1,134 +0,0 @@
|
||||
---
|
||||
name: "OPSX: Sync"
|
||||
description: Sync delta specs from a change to main specs
|
||||
category: Workflow
|
||||
tags: [workflow, specs, experimental]
|
||||
---
|
||||
|
||||
Sync delta specs from a change to main specs.
|
||||
|
||||
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
|
||||
|
||||
**Input**: Optionally specify a change name after `/opsx:sync` (e.g., `/opsx:sync add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show changes that have delta specs (under `specs/` directory).
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Find delta specs**
|
||||
|
||||
Look for delta spec files in `openspec/changes/<name>/specs/*/spec.md`.
|
||||
|
||||
Each delta spec file contains sections like:
|
||||
- `## ADDED Requirements` - New requirements to add
|
||||
- `## MODIFIED Requirements` - Changes to existing requirements
|
||||
- `## REMOVED Requirements` - Requirements to remove
|
||||
- `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
|
||||
|
||||
If no delta specs found, inform user and stop.
|
||||
|
||||
3. **For each delta spec, apply changes to main specs**
|
||||
|
||||
For each capability with a delta spec at `openspec/changes/<name>/specs/<capability>/spec.md`:
|
||||
|
||||
a. **Read the delta spec** to understand the intended changes
|
||||
|
||||
b. **Read the main spec** at `openspec/specs/<capability>/spec.md` (may not exist yet)
|
||||
|
||||
c. **Apply changes intelligently**:
|
||||
|
||||
**ADDED Requirements:**
|
||||
- If requirement doesn't exist in main spec → add it
|
||||
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
||||
|
||||
**MODIFIED Requirements:**
|
||||
- Find the requirement in main spec
|
||||
- Apply the changes - this can be:
|
||||
- Adding new scenarios (don't need to copy existing ones)
|
||||
- Modifying existing scenarios
|
||||
- Changing the requirement description
|
||||
- Preserve scenarios/content not mentioned in the delta
|
||||
|
||||
**REMOVED Requirements:**
|
||||
- Remove the entire requirement block from main spec
|
||||
|
||||
**RENAMED Requirements:**
|
||||
- Find the FROM requirement, rename to TO
|
||||
|
||||
d. **Create new main spec** if capability doesn't exist yet:
|
||||
- Create `openspec/specs/<capability>/spec.md`
|
||||
- Add Purpose section (can be brief, mark as TBD)
|
||||
- Add Requirements section with the ADDED requirements
|
||||
|
||||
4. **Show summary**
|
||||
|
||||
After applying all changes, summarize:
|
||||
- Which capabilities were updated
|
||||
- What changes were made (requirements added/modified/removed/renamed)
|
||||
|
||||
**Delta Spec Format Reference**
|
||||
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: New Feature
|
||||
The system SHALL do something new.
|
||||
|
||||
#### Scenario: Basic case
|
||||
- **WHEN** user does X
|
||||
- **THEN** system does Y
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Existing Feature
|
||||
#### Scenario: New scenario to add
|
||||
- **WHEN** user does A
|
||||
- **THEN** system does B
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Deprecated Feature
|
||||
|
||||
## RENAMED Requirements
|
||||
|
||||
- FROM: `### Requirement: Old Name`
|
||||
- TO: `### Requirement: New Name`
|
||||
```
|
||||
|
||||
**Key Principle: Intelligent Merging**
|
||||
|
||||
Unlike programmatic merging, you can apply **partial updates**:
|
||||
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
||||
- The delta represents *intent*, not a wholesale replacement
|
||||
- Use your judgment to merge changes sensibly
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Specs Synced: <change-name>
|
||||
|
||||
Updated main specs:
|
||||
|
||||
**<capability-1>**:
|
||||
- Added requirement: "New Feature"
|
||||
- Modified requirement: "Existing Feature" (added 1 scenario)
|
||||
|
||||
**<capability-2>**:
|
||||
- Created new spec file
|
||||
- Added requirement: "Another Feature"
|
||||
|
||||
Main specs are now updated. The change remains active - archive when implementation is complete.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Read both delta and main specs before making changes
|
||||
- Preserve existing content not mentioned in delta
|
||||
- If something is unclear, ask for clarification
|
||||
- Show what you're changing as you go
|
||||
- The operation should be idempotent - running twice should give same result
|
||||
@@ -1,164 +0,0 @@
|
||||
---
|
||||
name: "OPSX: Verify"
|
||||
description: Verify implementation matches change artifacts before archiving
|
||||
category: Workflow
|
||||
tags: [workflow, verify, experimental]
|
||||
---
|
||||
|
||||
Verify that an implementation matches the change artifacts (specs, tasks, design).
|
||||
|
||||
**Input**: Optionally specify a change name after `/opsx:verify` (e.g., `/opsx:verify add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show changes that have implementation tasks (tasks artifact exists).
|
||||
Include the schema used for each change if available.
|
||||
Mark changes with incomplete tasks as "(In Progress)".
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifacts exist for this change
|
||||
|
||||
3. **Get the change directory and load artifacts**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns the change directory and context files. Read all available artifacts from `contextFiles`.
|
||||
|
||||
4. **Initialize verification report structure**
|
||||
|
||||
Create a report structure with three dimensions:
|
||||
- **Completeness**: Track tasks and spec coverage
|
||||
- **Correctness**: Track requirement implementation and scenario coverage
|
||||
- **Coherence**: Track design adherence and pattern consistency
|
||||
|
||||
Each dimension can have CRITICAL, WARNING, or SUGGESTION issues.
|
||||
|
||||
5. **Verify Completeness**
|
||||
|
||||
**Task Completion**:
|
||||
- If tasks.md exists in contextFiles, read it
|
||||
- Parse checkboxes: `- [ ]` (incomplete) vs `- [x]` (complete)
|
||||
- Count complete vs total tasks
|
||||
- If incomplete tasks exist:
|
||||
- Add CRITICAL issue for each incomplete task
|
||||
- Recommendation: "Complete task: <description>" or "Mark as done if already implemented"
|
||||
|
||||
**Spec Coverage**:
|
||||
- If delta specs exist in `openspec/changes/<name>/specs/`:
|
||||
- Extract all requirements (marked with "### Requirement:")
|
||||
- For each requirement:
|
||||
- Search codebase for keywords related to the requirement
|
||||
- Assess if implementation likely exists
|
||||
- If requirements appear unimplemented:
|
||||
- Add CRITICAL issue: "Requirement not found: <requirement name>"
|
||||
- Recommendation: "Implement requirement X: <description>"
|
||||
|
||||
6. **Verify Correctness**
|
||||
|
||||
**Requirement Implementation Mapping**:
|
||||
- For each requirement from delta specs:
|
||||
- Search codebase for implementation evidence
|
||||
- If found, note file paths and line ranges
|
||||
- Assess if implementation matches requirement intent
|
||||
- If divergence detected:
|
||||
- Add WARNING: "Implementation may diverge from spec: <details>"
|
||||
- Recommendation: "Review <file>:<lines> against requirement X"
|
||||
|
||||
**Scenario Coverage**:
|
||||
- For each scenario in delta specs (marked with "#### Scenario:"):
|
||||
- Check if conditions are handled in code
|
||||
- Check if tests exist covering the scenario
|
||||
- If scenario appears uncovered:
|
||||
- Add WARNING: "Scenario not covered: <scenario name>"
|
||||
- Recommendation: "Add test or implementation for scenario: <description>"
|
||||
|
||||
7. **Verify Coherence**
|
||||
|
||||
**Design Adherence**:
|
||||
- If design.md exists in contextFiles:
|
||||
- Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
|
||||
- Verify implementation follows those decisions
|
||||
- If contradiction detected:
|
||||
- Add WARNING: "Design decision not followed: <decision>"
|
||||
- Recommendation: "Update implementation or revise design.md to match reality"
|
||||
- If no design.md: Skip design adherence check, note "No design.md to verify against"
|
||||
|
||||
**Code Pattern Consistency**:
|
||||
- Review new code for consistency with project patterns
|
||||
- Check file naming, directory structure, coding style
|
||||
- If significant deviations found:
|
||||
- Add SUGGESTION: "Code pattern deviation: <details>"
|
||||
- Recommendation: "Consider following project pattern: <example>"
|
||||
|
||||
8. **Generate Verification Report**
|
||||
|
||||
**Summary Scorecard**:
|
||||
```
|
||||
## Verification Report: <change-name>
|
||||
|
||||
### Summary
|
||||
| Dimension | Status |
|
||||
|--------------|------------------|
|
||||
| Completeness | X/Y tasks, N reqs|
|
||||
| Correctness | M/N reqs covered |
|
||||
| Coherence | Followed/Issues |
|
||||
```
|
||||
|
||||
**Issues by Priority**:
|
||||
|
||||
1. **CRITICAL** (Must fix before archive):
|
||||
- Incomplete tasks
|
||||
- Missing requirement implementations
|
||||
- Each with specific, actionable recommendation
|
||||
|
||||
2. **WARNING** (Should fix):
|
||||
- Spec/design divergences
|
||||
- Missing scenario coverage
|
||||
- Each with specific recommendation
|
||||
|
||||
3. **SUGGESTION** (Nice to fix):
|
||||
- Pattern inconsistencies
|
||||
- Minor improvements
|
||||
- Each with specific recommendation
|
||||
|
||||
**Final Assessment**:
|
||||
- If CRITICAL issues: "X critical issue(s) found. Fix before archiving."
|
||||
- If only warnings: "No critical issues. Y warning(s) to consider. Ready for archive (with noted improvements)."
|
||||
- If all clear: "All checks passed. Ready for archive."
|
||||
|
||||
**Verification Heuristics**
|
||||
|
||||
- **Completeness**: Focus on objective checklist items (checkboxes, requirements list)
|
||||
- **Correctness**: Use keyword search, file path analysis, reasonable inference - don't require perfect certainty
|
||||
- **Coherence**: Look for glaring inconsistencies, don't nitpick style
|
||||
- **False Positives**: When uncertain, prefer SUGGESTION over WARNING, WARNING over CRITICAL
|
||||
- **Actionability**: Every issue must have a specific recommendation with file/line references where applicable
|
||||
|
||||
**Graceful Degradation**
|
||||
|
||||
- If only tasks.md exists: verify task completion only, skip spec/design checks
|
||||
- If tasks + specs exist: verify completeness and correctness, skip design
|
||||
- If full artifacts: verify all three dimensions
|
||||
- Always note which checks were skipped and why
|
||||
|
||||
**Output Format**
|
||||
|
||||
Use clear markdown with:
|
||||
- Table for summary scorecard
|
||||
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
|
||||
- Code references in format: `file.ts:123`
|
||||
- Specific, actionable recommendations
|
||||
- No vague suggestions like "consider reviewing"
|
||||
@@ -1,246 +0,0 @@
|
||||
---
|
||||
name: openspec-bulk-archive-change
|
||||
description: Archive multiple completed changes at once. Use when archiving several parallel changes.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Archive multiple completed changes in a single operation.
|
||||
|
||||
This skill allows you to batch-archive changes, handling spec conflicts intelligently by checking the codebase to determine what's actually implemented.
|
||||
|
||||
**Input**: None required (prompts for selection)
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Get active changes**
|
||||
|
||||
Run `openspec list --json` to get all active changes.
|
||||
|
||||
If no active changes exist, inform user and stop.
|
||||
|
||||
2. **Prompt for change selection**
|
||||
|
||||
Use **AskUserQuestion tool** with multi-select to let user choose changes:
|
||||
- Show each change with its schema
|
||||
- Include an option for "All changes"
|
||||
- Allow any number of selections (1+ works, 2+ is the typical use case)
|
||||
|
||||
**IMPORTANT**: Do NOT auto-select. Always let the user choose.
|
||||
|
||||
3. **Batch validation - gather status for all selected changes**
|
||||
|
||||
For each selected change, collect:
|
||||
|
||||
a. **Artifact status** - Run `openspec status --change "<name>" --json`
|
||||
- Parse `schemaName` and `artifacts` list
|
||||
- Note which artifacts are `done` vs other states
|
||||
|
||||
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
|
||||
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
|
||||
- If no tasks file exists, note as "No tasks"
|
||||
|
||||
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
|
||||
- List which capability specs exist
|
||||
- For each, extract requirement names (lines matching `### Requirement: <name>`)
|
||||
|
||||
4. **Detect spec conflicts**
|
||||
|
||||
Build a map of `capability -> [changes that touch it]`:
|
||||
|
||||
```
|
||||
auth -> [change-a, change-b] <- CONFLICT (2+ changes)
|
||||
api -> [change-c] <- OK (only 1 change)
|
||||
```
|
||||
|
||||
A conflict exists when 2+ selected changes have delta specs for the same capability.
|
||||
|
||||
5. **Resolve conflicts agentically**
|
||||
|
||||
**For each conflict**, investigate the codebase:
|
||||
|
||||
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
|
||||
|
||||
b. **Search the codebase** for implementation evidence:
|
||||
- Look for code implementing requirements from each delta spec
|
||||
- Check for related files, functions, or tests
|
||||
|
||||
c. **Determine resolution**:
|
||||
- If only one change is actually implemented -> sync that one's specs
|
||||
- If both implemented -> apply in chronological order (older first, newer overwrites)
|
||||
- If neither implemented -> skip spec sync, warn user
|
||||
|
||||
d. **Record resolution** for each conflict:
|
||||
- Which change's specs to apply
|
||||
- In what order (if both)
|
||||
- Rationale (what was found in codebase)
|
||||
|
||||
6. **Show consolidated status table**
|
||||
|
||||
Display a table summarizing all changes:
|
||||
|
||||
```
|
||||
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
||||
|---------------------|-----------|-------|---------|-----------|--------|
|
||||
| schema-management | Done | 5/5 | 2 delta | None | Ready |
|
||||
| project-config | Done | 3/3 | 1 delta | None | Ready |
|
||||
| add-oauth | Done | 4/4 | 1 delta | auth (!) | Ready* |
|
||||
| add-verify-skill | 1 left | 2/5 | None | None | Warn |
|
||||
```
|
||||
|
||||
For conflicts, show the resolution:
|
||||
```
|
||||
* Conflict resolution:
|
||||
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
|
||||
```
|
||||
|
||||
For incomplete changes, show warnings:
|
||||
```
|
||||
Warnings:
|
||||
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
|
||||
```
|
||||
|
||||
7. **Confirm batch operation**
|
||||
|
||||
Use **AskUserQuestion tool** with a single confirmation:
|
||||
|
||||
- "Archive N changes?" with options based on status
|
||||
- Options might include:
|
||||
- "Archive all N changes"
|
||||
- "Archive only N ready changes (skip incomplete)"
|
||||
- "Cancel"
|
||||
|
||||
If there are incomplete changes, make clear they'll be archived with warnings.
|
||||
|
||||
8. **Execute archive for each confirmed change**
|
||||
|
||||
Process changes in the determined order (respecting conflict resolution):
|
||||
|
||||
a. **Sync specs** if delta specs exist:
|
||||
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
|
||||
- For conflicts, apply in resolved order
|
||||
- Track if sync was done
|
||||
|
||||
b. **Perform the archive**:
|
||||
```bash
|
||||
mkdir -p openspec/changes/archive
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
c. **Track outcome** for each change:
|
||||
- Success: archived successfully
|
||||
- Failed: error during archive (record error)
|
||||
- Skipped: user chose not to archive (if applicable)
|
||||
|
||||
9. **Display summary**
|
||||
|
||||
Show final results:
|
||||
|
||||
```
|
||||
## Bulk Archive Complete
|
||||
|
||||
Archived 3 changes:
|
||||
- schema-management-cli -> archive/2026-01-19-schema-management-cli/
|
||||
- project-config -> archive/2026-01-19-project-config/
|
||||
- add-oauth -> archive/2026-01-19-add-oauth/
|
||||
|
||||
Skipped 1 change:
|
||||
- add-verify-skill (user chose not to archive incomplete)
|
||||
|
||||
Spec sync summary:
|
||||
- 4 delta specs synced to main specs
|
||||
- 1 conflict resolved (auth: applied both in chronological order)
|
||||
```
|
||||
|
||||
If any failures:
|
||||
```
|
||||
Failed 1 change:
|
||||
- some-change: Archive directory already exists
|
||||
```
|
||||
|
||||
**Conflict Resolution Examples**
|
||||
|
||||
Example 1: Only one implemented
|
||||
```
|
||||
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
|
||||
|
||||
Checking add-oauth:
|
||||
- Delta adds "OAuth Provider Integration" requirement
|
||||
- Searching codebase... found src/auth/oauth.ts implementing OAuth flow
|
||||
|
||||
Checking add-jwt:
|
||||
- Delta adds "JWT Token Handling" requirement
|
||||
- Searching codebase... no JWT implementation found
|
||||
|
||||
Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
|
||||
```
|
||||
|
||||
Example 2: Both implemented
|
||||
```
|
||||
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
|
||||
|
||||
Checking add-rest-api (created 2026-01-10):
|
||||
- Delta adds "REST Endpoints" requirement
|
||||
- Searching codebase... found src/api/rest.ts
|
||||
|
||||
Checking add-graphql (created 2026-01-15):
|
||||
- Delta adds "GraphQL Schema" requirement
|
||||
- Searching codebase... found src/api/graphql.ts
|
||||
|
||||
Resolution: Both implemented. Will apply add-rest-api specs first,
|
||||
then add-graphql specs (chronological order, newer takes precedence).
|
||||
```
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Bulk Archive Complete
|
||||
|
||||
Archived N changes:
|
||||
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
|
||||
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
|
||||
|
||||
Spec sync summary:
|
||||
- N delta specs synced to main specs
|
||||
- No conflicts (or: M conflicts resolved)
|
||||
```
|
||||
|
||||
**Output On Partial Success**
|
||||
|
||||
```
|
||||
## Bulk Archive Complete (partial)
|
||||
|
||||
Archived N changes:
|
||||
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
|
||||
|
||||
Skipped M changes:
|
||||
- <change-2> (user chose not to archive incomplete)
|
||||
|
||||
Failed K changes:
|
||||
- <change-3>: Archive directory already exists
|
||||
```
|
||||
|
||||
**Output When No Changes**
|
||||
|
||||
```
|
||||
## No Changes to Archive
|
||||
|
||||
No active changes found. Create a new change to get started.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
|
||||
- Always prompt for selection, never auto-select
|
||||
- Detect spec conflicts early and resolve by checking codebase
|
||||
- When both changes are implemented, apply specs in chronological order
|
||||
- Skip spec sync only when implementation is missing (warn user)
|
||||
- Show clear per-change status before confirming
|
||||
- Use single confirmation for entire batch
|
||||
- Track and report all outcomes (success/skip/fail)
|
||||
- Preserve .openspec.yaml when moving to archive
|
||||
- Archive directory target uses current date: YYYY-MM-DD-<name>
|
||||
- If archive target exists, fail that change but continue with others
|
||||
@@ -1,118 +0,0 @@
|
||||
---
|
||||
name: openspec-continue-change
|
||||
description: Continue working on an OpenSpec change by creating the next artifact. Use when the user wants to progress their change, create the next artifact, or continue their workflow.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Continue working on a change by creating the next artifact.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to work on.
|
||||
|
||||
Present the top 3-4 most recently modified changes as options, showing:
|
||||
- Change name
|
||||
- Schema (from `schema` field if present, otherwise "spec-driven")
|
||||
- Status (e.g., "0/5 tasks", "complete", "no tasks")
|
||||
- How recently it was modified (from `lastModified` field)
|
||||
|
||||
Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to continue.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check current status**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand current state. The response includes:
|
||||
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
|
||||
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
|
||||
- `isComplete`: Boolean indicating if all artifacts are complete
|
||||
|
||||
3. **Act based on status**:
|
||||
|
||||
---
|
||||
|
||||
**If all artifacts are complete (`isComplete: true`)**:
|
||||
- Congratulate the user
|
||||
- Show final status including the schema used
|
||||
- Suggest: "All artifacts created! You can now implement this change or archive it."
|
||||
- STOP
|
||||
|
||||
---
|
||||
|
||||
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
|
||||
- Pick the FIRST artifact with `status: "ready"` from the status output
|
||||
- Get its instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- Parse the JSON. The key fields are:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- **Create the artifact file**:
|
||||
- Read any completed dependency files for context
|
||||
- Use `template` as the structure - fill in its sections
|
||||
- Apply `context` and `rules` as constraints when writing - but do NOT copy them into the file
|
||||
- Write to the output path specified in instructions
|
||||
- Show what was created and what's now unlocked
|
||||
- STOP after creating ONE artifact
|
||||
|
||||
---
|
||||
|
||||
**If no artifacts are ready (all blocked)**:
|
||||
- This shouldn't happen with a valid schema
|
||||
- Show status and suggest checking for issues
|
||||
|
||||
4. **After creating an artifact, show progress**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After each invocation, show:
|
||||
- Which artifact was created
|
||||
- Schema workflow being used
|
||||
- Current progress (N/M complete)
|
||||
- What artifacts are now unlocked
|
||||
- Prompt: "Want to continue? Just ask me to continue or tell me what to do next."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
The artifact types and their purpose depend on the schema. Use the `instruction` field from the instructions output to understand what to create.
|
||||
|
||||
Common artifact patterns:
|
||||
|
||||
**spec-driven schema** (proposal → specs → design → tasks):
|
||||
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
||||
- The Capabilities section is critical - each capability listed will need a spec file.
|
||||
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
|
||||
- **design.md**: Document technical decisions, architecture, and implementation approach.
|
||||
- **tasks.md**: Break down implementation into checkboxed tasks.
|
||||
|
||||
For other schemas, follow the `instruction` field from the CLI output.
|
||||
|
||||
**Guardrails**
|
||||
- Create ONE artifact per invocation
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- Never skip artifacts or create out of order
|
||||
- If context is unclear, ask the user before creating
|
||||
- Verify the artifact file exists after writing before marking progress
|
||||
- Use the schema's artifact sequence, don't assume specific artifact names
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
@@ -1,217 +0,0 @@
|
||||
---
|
||||
name: openspec-export-spec
|
||||
description: Export a feature as a compact, portable spec that an AI agent (Copilot, Claude) on a sandboxed machine can implement from. Optimized for minimal hand-typing.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Export a feature as a portable spec for AI-assisted reimplementation on a sandboxed machine.
|
||||
|
||||
Instead of retyping code, you retype a compact spec. The AI on the sandbox (Copilot, Claude, OpenSpec) generates the code from the spec.
|
||||
|
||||
---
|
||||
|
||||
**Input**: A change name (active or archived), or a description of the feature. If omitted, prompt for selection.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Identify the source feature**
|
||||
|
||||
Same as `openspec-extract-feature` step 1:
|
||||
- Check active changes and archive for the change name
|
||||
- If not found, prompt with **AskUserQuestion tool**
|
||||
- Read all artifacts: `proposal.md`, `design.md`, `tasks.md`, specs
|
||||
|
||||
2. **Analyze dependency chain (cumulative mode)**
|
||||
|
||||
Features often build on each other. Before generating the spec, determine
|
||||
what the target feature depends on.
|
||||
|
||||
a. **Read all archived change proposals** in `openspec/changes/archive/` (sorted by date).
|
||||
For each, read `proposal.md` to understand what it adds and what it depends on.
|
||||
|
||||
b. **Build a dependency graph**:
|
||||
- Which changes does the target feature require?
|
||||
- Which earlier changes are superseded? (e.g., if Change 6 replaces Change 3's
|
||||
implementation, include Change 6's version, not Change 3's)
|
||||
- Which changes are unrelated and can be skipped?
|
||||
|
||||
c. **Ask the user** using **AskUserQuestion tool**:
|
||||
> "This feature depends on earlier changes. How should I scope the spec?"
|
||||
>
|
||||
> Options:
|
||||
> 1. **Cumulative** — include all foundation this feature needs (recommended if target is a fresh codebase)
|
||||
> 2. **Delta only** — just what this specific change adds (use if target already has the foundation)
|
||||
> 3. **Custom** — let me pick which dependencies to include
|
||||
|
||||
Show the dependency chain so the user can decide.
|
||||
|
||||
d. **In cumulative mode**: The spec covers the entire stack from foundation to feature.
|
||||
- Merge packages, components, wiring from all included changes
|
||||
- Skip superseded components (use the latest version)
|
||||
- The spec should read as a single coherent feature, not a list of changes
|
||||
|
||||
e. **In delta mode**: The spec covers only the selected change.
|
||||
- Add a "Assumes" section listing what must already exist in the target
|
||||
|
||||
3. **Read the actual implementation**
|
||||
|
||||
Read all source files that were created or modified by this feature
|
||||
(and its dependencies if cumulative).
|
||||
The spec must reflect what was actually built, not just what was planned.
|
||||
|
||||
4. **Determine the target context**
|
||||
|
||||
Use **AskUserQuestion tool** to ask:
|
||||
> "Tell me about the target codebase:
|
||||
> 1. Project name / root namespace
|
||||
> 2. Existing stack (ASP.NET Core? Blazor? MudBlazor?)
|
||||
> 3. Does it already have any of these? (controllers, DI setup, chat endpoint)
|
||||
> 4. Does the target have OpenSpec? GitHub Copilot? Claude Code?"
|
||||
|
||||
This shapes what the spec assumes vs what it must specify.
|
||||
|
||||
5. **Generate the portable spec**
|
||||
|
||||
Create a single markdown document that is:
|
||||
- **Compact**: Target ~30-50 lines for a medium feature
|
||||
- **Precise**: Unambiguous enough for an AI to implement correctly
|
||||
- **Self-contained**: No references to external files or repos
|
||||
- **Stack-aware**: Uses the right terminology for the target stack
|
||||
|
||||
Structure:
|
||||
|
||||
```markdown
|
||||
# Feature: <Name>
|
||||
## Target: <project name> (<stack>)
|
||||
|
||||
## Packages
|
||||
<list with versions>
|
||||
|
||||
## Architecture
|
||||
<2-3 sentence overview of how the pieces fit together>
|
||||
|
||||
## Components
|
||||
|
||||
### <Component 1>: <path hint>
|
||||
- <What it does — 1 line>
|
||||
- <Key behavior — 1 line>
|
||||
- <Interface/contract — 1 line>
|
||||
|
||||
### <Component 2>: <path hint>
|
||||
...
|
||||
|
||||
## Contracts
|
||||
<API shapes, model definitions, SSE formats — the things that MUST be exact>
|
||||
|
||||
## Wiring
|
||||
<DI registration, middleware order, configuration keys — in dependency order>
|
||||
|
||||
## Behavior
|
||||
<Key behavioral requirements that aren't obvious from the structure>
|
||||
```
|
||||
|
||||
**Compression strategies:**
|
||||
- Use bullet points, not prose
|
||||
- Specify contracts precisely (field names, types, API shapes)
|
||||
- Let the AI infer standard patterns (error handling, null checks, etc.)
|
||||
- Only specify non-obvious behavior (the surprising parts)
|
||||
- Omit anything the AI would do by default for the given stack
|
||||
|
||||
6. **Estimate typing effort**
|
||||
|
||||
Count characters in the spec. Compare to the code recipe equivalent.
|
||||
Show the compression ratio:
|
||||
```
|
||||
Code recipe: ~120 lines to type
|
||||
This spec: ~35 lines to type
|
||||
Compression: 3.4x
|
||||
```
|
||||
|
||||
7. **Optionally generate an OpenSpec-compatible version**
|
||||
|
||||
If the target has OpenSpec, also generate a version structured as:
|
||||
- A `proposal.md` (minimal — 5-10 lines)
|
||||
- A `tasks.md` (implementation steps the target AI follows)
|
||||
|
||||
These can be even more compact because OpenSpec provides the scaffolding.
|
||||
|
||||
Save this variant alongside the main spec.
|
||||
|
||||
8. **Write the output**
|
||||
|
||||
Save to: `openspec/exports/<change-name>-spec.md`
|
||||
If OpenSpec variant: `openspec/exports/<change-name>-openspec.md`
|
||||
|
||||
Display the full content for review.
|
||||
|
||||
**Output Format**
|
||||
|
||||
```markdown
|
||||
# Feature: Semantic Kernel Chat with Tool Calling
|
||||
## Target: ApplicationX (ASP.NET Core + Blazor WASM + MudBlazor)
|
||||
|
||||
## Packages
|
||||
- Microsoft.SemanticKernel 1.74.0
|
||||
- Microsoft.SemanticKernel.Connectors.OpenAI 1.74.0
|
||||
|
||||
## Architecture
|
||||
POST /api/chat endpoint accepts messages, runs them through SK's chat completion
|
||||
with auto tool calling enabled, streams response as SSE. An ExtractionPlugin
|
||||
validates structured data extracted by the LLM.
|
||||
|
||||
## Components
|
||||
|
||||
### ChatController: Controllers/ChatController.cs
|
||||
- POST endpoint, injects Kernel via DI
|
||||
- Converts ChatMessage[] to SK ChatHistory
|
||||
- Streams via GetStreamingChatMessageContentsAsync
|
||||
- Outputs SSE: `data: {"text":"..."}\n\n` then `data: [DONE]\n\n`
|
||||
|
||||
### ExtractionPlugin: Plugins/ExtractionPlugin.cs
|
||||
- [KernelFunction("validate_extracted_fields")]
|
||||
- Accepts JSON string, deserializes to ExtractedFields
|
||||
- Returns {"isValid": bool, "errors": string[]}
|
||||
|
||||
### ExtractedFields: Models/ExtractedFields.cs
|
||||
- Required: Client(string), Project(string), Hours(decimal), Rate(decimal), Currency(string), Date(string)
|
||||
- Optional: Description(string), PoNumber(string)
|
||||
|
||||
### ValidationResult: Models/ValidationResult.cs
|
||||
- IsValid(bool), Errors(List<string>)
|
||||
|
||||
### ChatRequest/ChatMessage: Shared/Models/
|
||||
- ChatRequest: Messages(List<ChatMessage>)
|
||||
- ChatMessage: Role(string), Content(string)
|
||||
|
||||
## Wiring (Program.cs, add after AddControllers)
|
||||
- AddOpenAIChatCompletion(model, endpoint with /v1 suffix, apiKey)
|
||||
- AddKernel()
|
||||
- AddSingleton<ExtractionPlugin>()
|
||||
- UseCors for Blazor client origin
|
||||
|
||||
## Behavior
|
||||
- FunctionChoiceBehavior.Auto() enables autonomous tool calling
|
||||
- SK's built-in limit prevents runaway tool call loops
|
||||
- Plugin imported per-request via _kernel.ImportPluginFromObject
|
||||
- Base URL must include /v1 — OpenAI SDK appends chat/completions directly
|
||||
```
|
||||
|
||||
**Lines to type: ~35 | Code equivalent: ~150 lines | Compression: 4.3x**
|
||||
|
||||
**Guardrails**
|
||||
- Prioritize precision over brevity — an ambiguous spec wastes more time than a slightly longer one
|
||||
- Always include exact field names, types, and API shapes — these are the hardest to guess
|
||||
- Include non-obvious gotchas (like the /v1 base URL requirement)
|
||||
- Test the spec mentally: could an AI implement this correctly without seeing the original code?
|
||||
- If the feature is too complex for a single spec page (~50+ lines), split into multiple specs by component
|
||||
- Always show the compression ratio so the user can decide between spec and code recipe
|
||||
- The spec must be readable when printed in monospace — no wide tables or long lines
|
||||
- In cumulative mode, the spec must read as one coherent feature — not a list of sequential changes
|
||||
- Skip superseded components — always describe the latest version of each piece
|
||||
- In delta mode, add an "Assumes" section so the target AI knows what must already exist
|
||||
- In the output header, note which changes were included and which were skipped
|
||||
@@ -1,170 +0,0 @@
|
||||
---
|
||||
name: openspec-extract-feature
|
||||
description: Extract a feature from a change (archived or active) into a minimal, printable code recipe optimized for manual retyping into a sandboxed codebase.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Extract a feature into a minimal, printable code recipe for manual reimplementation.
|
||||
|
||||
Generates a markdown document with:
|
||||
- Package dependencies
|
||||
- Ordered code blocks (no comments, no boilerplate)
|
||||
- Clear markers for generic vs domain-specific code
|
||||
|
||||
Print it, take it to the sandbox, type it in.
|
||||
|
||||
---
|
||||
|
||||
**Input**: A change name (active or archived), a git commit range, or a list of files. If omitted, prompt for selection.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Identify the source feature**
|
||||
|
||||
If a change name is provided:
|
||||
- Check active changes: `openspec list --json`
|
||||
- Check archive: look in `openspec/changes/archive/` for directories ending with the name
|
||||
- If found, read its artifacts: `proposal.md`, `design.md`, `tasks.md`
|
||||
|
||||
If no name provided:
|
||||
- Run `openspec list --json` and list archived changes
|
||||
- Use **AskUserQuestion tool** to let the user select
|
||||
|
||||
If a file list or commit range is provided instead:
|
||||
- Read those files directly
|
||||
- Identify the feature from the code
|
||||
|
||||
2. **Analyze dependency chain (cumulative mode)**
|
||||
|
||||
Features are often not independent — they build on each other. Before generating
|
||||
the recipe, determine what the target feature depends on.
|
||||
|
||||
a. **Read all archived change proposals** in `openspec/changes/archive/` (sorted by date).
|
||||
For each, read `proposal.md` to understand what it adds and what it depends on.
|
||||
|
||||
b. **Build a dependency graph**:
|
||||
- Which changes does the target feature require?
|
||||
- Which earlier changes are superseded? (e.g., if Change 6 replaces Change 3's
|
||||
implementation, include Change 6's version, not Change 3's)
|
||||
- Which changes are unrelated and can be skipped?
|
||||
|
||||
c. **Ask the user** using **AskUserQuestion tool**:
|
||||
> "This feature depends on earlier changes. How should I scope the recipe?"
|
||||
>
|
||||
> Options:
|
||||
> 1. **Cumulative** — include all foundation code this feature needs (recommended if target is a fresh codebase)
|
||||
> 2. **Delta only** — just the code this specific change adds (use if target already has the foundation)
|
||||
> 3. **Custom** — let me pick which dependencies to include
|
||||
|
||||
Show the dependency chain so the user can decide, e.g.:
|
||||
```
|
||||
migrate-to-semantic-kernel depends on:
|
||||
← basic-chat-interface (UI + shared models)
|
||||
← wire-responses-api (superseded — SK replaces the proxy)
|
||||
← multi-turn-conversations (chat history)
|
||||
```
|
||||
|
||||
d. **In cumulative mode**: Merge the dependency chain into a single coherent recipe.
|
||||
- Walk changes in dependency order
|
||||
- Skip superseded code (use the latest version of each file)
|
||||
- Collapse packages from all changes into one prerequisites section
|
||||
- If a file was created in Change 2 and modified in Change 5, include only the final version
|
||||
|
||||
e. **In delta mode**: Include only the code from the selected change.
|
||||
|
||||
3. **Analyze the feature scope**
|
||||
|
||||
From the change artifacts and/or code, determine:
|
||||
- Which files were created or modified (across the full dependency chain if cumulative)
|
||||
- What NuGet/npm packages were added
|
||||
- What the dependency order is (e.g., models before controllers)
|
||||
- What is generic infrastructure vs domain-specific logic
|
||||
|
||||
Read all relevant source files in the current codebase to get the actual current code.
|
||||
**Always read the final current version of each file**, not intermediate versions.
|
||||
|
||||
3. **Generate the code recipe**
|
||||
|
||||
Create a markdown document with these sections, in this order:
|
||||
|
||||
**Header**: Feature name, source change, date
|
||||
|
||||
**Prerequisites**: Package references with exact versions
|
||||
|
||||
**Steps**: Ordered by dependency. Each step contains:
|
||||
- Step number and action (e.g., "New file", "Add to existing file", "Modify")
|
||||
- Target path (relative, adaptable)
|
||||
- Namespace placeholder: `__YOUR_NAMESPACE__` where the target project namespace goes
|
||||
- **Code block**: The actual code, stripped of:
|
||||
- All comments (// and /* */ and /// XML docs)
|
||||
- Redundant blank lines
|
||||
- Verbose variable names that can be shortened
|
||||
- Any code not directly related to the feature
|
||||
- If the step modifies an existing file: show only the code to add, with a brief marker for insertion point (e.g., "Add after AddControllers()")
|
||||
|
||||
**Domain-Specific Sections**: Clearly marked with `// ADAPT:` prefix explaining what to change for the target domain
|
||||
|
||||
4. **Optimize for retyping**
|
||||
|
||||
Review the generated document and:
|
||||
- Merge small files if they can be combined
|
||||
- Remove any using statements that the IDE will auto-add
|
||||
- Shorten any unnecessarily verbose code
|
||||
- Ensure no step exceeds ~40 lines (split if needed)
|
||||
- Add line counts per step so the user can estimate effort
|
||||
- Total the overall line count at the top
|
||||
|
||||
5. **Write the output**
|
||||
|
||||
Save to: `openspec/exports/<change-name>-recipe.md`
|
||||
|
||||
Also display the full content so the user can review it immediately.
|
||||
|
||||
**Output Format**
|
||||
|
||||
```markdown
|
||||
# Feature Recipe: <Feature Name>
|
||||
**Source**: <change-name> | **Lines to type**: ~<N>
|
||||
|
||||
## Prerequisites
|
||||
- `Microsoft.SemanticKernel` 1.74.0
|
||||
- `Microsoft.SemanticKernel.Connectors.OpenAI` 1.74.0
|
||||
|
||||
## Step 1: <Action> — <path> (~N lines)
|
||||
|
||||
```csharp
|
||||
<stripped code>
|
||||
```
|
||||
|
||||
## Step 2: <Action> — <path> (~N lines)
|
||||
|
||||
Insert after `<marker>`:
|
||||
```csharp
|
||||
<stripped code>
|
||||
```
|
||||
|
||||
## Domain-Specific (adapt these)
|
||||
|
||||
### ExtractionPlugin.cs
|
||||
```csharp
|
||||
// ADAPT: Replace field names and validation logic for your domain
|
||||
<code>
|
||||
```
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Never include comments in code blocks — the goal is minimum keystrokes
|
||||
- Always read the actual current source files, not just the change artifacts
|
||||
- Preserve compilation order: models → services → controllers → DI registration
|
||||
- Mark domain-specific code clearly so the user knows what to adapt vs copy verbatim
|
||||
- Keep each step self-contained — the user may take breaks between steps
|
||||
- If a feature spans more than ~200 lines of stripped code, warn the user and suggest using `/opsx:export-spec` instead
|
||||
- Output must be valid markdown that renders well when printed
|
||||
- When in cumulative mode, skip superseded code — always use the latest version of each file
|
||||
- In the output header, show which changes were included and which were skipped (with reason)
|
||||
- If a dependency chain is long (4+ changes), suggest `/opsx:export-spec` as a more efficient alternative
|
||||
@@ -1,101 +0,0 @@
|
||||
---
|
||||
name: openspec-ff-change
|
||||
description: Fast-forward through OpenSpec artifact creation. Use when the user wants to quickly create all artifacts needed for implementation without stepping through each one individually.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Fast-forward through artifact creation - generate everything needed to start implementation in one go.
|
||||
|
||||
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no clear input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to get:
|
||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||
- `artifacts`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance for this artifact type
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "✓ Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, suggest continuing that change instead
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
@@ -1,74 +0,0 @@
|
||||
---
|
||||
name: openspec-new-change
|
||||
description: Start a new OpenSpec change using the experimental artifact workflow. Use when the user wants to create a new feature, fix, or modification with a structured step-by-step approach.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Start a new change using the experimental artifact-driven approach.
|
||||
|
||||
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no clear input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Determine the workflow schema**
|
||||
|
||||
Use the default schema (omit `--schema`) unless the user explicitly requests a different workflow.
|
||||
|
||||
**Use a different schema only if the user mentions:**
|
||||
- A specific schema name → use `--schema <name>`
|
||||
- "show workflows" or "what workflows" → run `openspec schemas --json` and let them choose
|
||||
|
||||
**Otherwise**: Omit `--schema` to use the default.
|
||||
|
||||
3. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
Add `--schema <name>` only if the user requested a specific workflow.
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
|
||||
|
||||
4. **Show the artifact status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
||||
|
||||
5. **Get instructions for the first artifact**
|
||||
The first artifact depends on the schema (e.g., `proposal` for spec-driven).
|
||||
Check the status output to find the first artifact with status "ready".
|
||||
```bash
|
||||
openspec instructions <first-artifact-id> --change "<name>"
|
||||
```
|
||||
This outputs the template and context for creating the first artifact.
|
||||
|
||||
6. **STOP and wait for user direction**
|
||||
|
||||
**Output**
|
||||
|
||||
After completing the steps, summarize:
|
||||
- Change name and location
|
||||
- Schema/workflow being used and its artifact sequence
|
||||
- Current status (0/N artifacts complete)
|
||||
- The template for the first artifact
|
||||
- Prompt: "Ready to create the first artifact? Just describe what this change is about and I'll draft it, or ask me to continue."
|
||||
|
||||
**Guardrails**
|
||||
- Do NOT create any artifacts yet - just show the instructions
|
||||
- Do NOT advance beyond showing the first artifact template
|
||||
- If the name is invalid (not kebab-case), ask for a valid name
|
||||
- If a change with that name already exists, suggest continuing that change instead
|
||||
- Pass --schema if using a non-default workflow
|
||||
@@ -1,554 +0,0 @@
|
||||
---
|
||||
name: openspec-onboard
|
||||
description: Guided onboarding for OpenSpec - walk through a complete workflow cycle with narration and real codebase work.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step.
|
||||
|
||||
---
|
||||
|
||||
## Preflight
|
||||
|
||||
Before starting, check if the OpenSpec CLI is installed:
|
||||
|
||||
```bash
|
||||
# Unix/macOS
|
||||
openspec --version 2>&1 || echo "CLI_NOT_INSTALLED"
|
||||
# Windows (PowerShell)
|
||||
# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" }
|
||||
```
|
||||
|
||||
**If CLI not installed:**
|
||||
> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`.
|
||||
|
||||
Stop here if not installed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Welcome
|
||||
|
||||
Display:
|
||||
|
||||
```
|
||||
## Welcome to OpenSpec!
|
||||
|
||||
I'll walk you through a complete change cycle—from idea to implementation—using a real task in your codebase. Along the way, you'll learn the workflow by doing it.
|
||||
|
||||
**What we'll do:**
|
||||
1. Pick a small, real task in your codebase
|
||||
2. Explore the problem briefly
|
||||
3. Create a change (the container for our work)
|
||||
4. Build the artifacts: proposal → specs → design → tasks
|
||||
5. Implement the tasks
|
||||
6. Archive the completed change
|
||||
|
||||
**Time:** ~15-20 minutes
|
||||
|
||||
Let's start by finding something to work on.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Task Selection
|
||||
|
||||
### Codebase Analysis
|
||||
|
||||
Scan the codebase for small improvement opportunities. Look for:
|
||||
|
||||
1. **TODO/FIXME comments** - Search for `TODO`, `FIXME`, `HACK`, `XXX` in code files
|
||||
2. **Missing error handling** - `catch` blocks that swallow errors, risky operations without try-catch
|
||||
3. **Functions without tests** - Cross-reference `src/` with test directories
|
||||
4. **Type issues** - `any` types in TypeScript files (`: any`, `as any`)
|
||||
5. **Debug artifacts** - `console.log`, `console.debug`, `debugger` statements in non-debug code
|
||||
6. **Missing validation** - User input handlers without validation
|
||||
|
||||
Also check recent git activity:
|
||||
```bash
|
||||
# Unix/macOS
|
||||
git log --oneline -10 2>/dev/null || echo "No git history"
|
||||
# Windows (PowerShell)
|
||||
# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" }
|
||||
```
|
||||
|
||||
### Present Suggestions
|
||||
|
||||
From your analysis, present 3-4 specific suggestions:
|
||||
|
||||
```
|
||||
## Task Suggestions
|
||||
|
||||
Based on scanning your codebase, here are some good starter tasks:
|
||||
|
||||
**1. [Most promising task]**
|
||||
Location: `src/path/to/file.ts:42`
|
||||
Scope: ~1-2 files, ~20-30 lines
|
||||
Why it's good: [brief reason]
|
||||
|
||||
**2. [Second task]**
|
||||
Location: `src/another/file.ts`
|
||||
Scope: ~1 file, ~15 lines
|
||||
Why it's good: [brief reason]
|
||||
|
||||
**3. [Third task]**
|
||||
Location: [location]
|
||||
Scope: [estimate]
|
||||
Why it's good: [brief reason]
|
||||
|
||||
**4. Something else?**
|
||||
Tell me what you'd like to work on.
|
||||
|
||||
Which task interests you? (Pick a number or describe your own)
|
||||
```
|
||||
|
||||
**If nothing found:** Fall back to asking what the user wants to build:
|
||||
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
|
||||
|
||||
### Scope Guardrail
|
||||
|
||||
If the user picks or describes something too large (major feature, multi-day work):
|
||||
|
||||
```
|
||||
That's a valuable task, but it's probably larger than ideal for your first OpenSpec run-through.
|
||||
|
||||
For learning the workflow, smaller is better—it lets you see the full cycle without getting stuck in implementation details.
|
||||
|
||||
**Options:**
|
||||
1. **Slice it smaller** - What's the smallest useful piece of [their task]? Maybe just [specific slice]?
|
||||
2. **Pick something else** - One of the other suggestions, or a different small task?
|
||||
3. **Do it anyway** - If you really want to tackle this, we can. Just know it'll take longer.
|
||||
|
||||
What would you prefer?
|
||||
```
|
||||
|
||||
Let the user override if they insist—this is a soft guardrail.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Explore Demo
|
||||
|
||||
Once a task is selected, briefly demonstrate explore mode:
|
||||
|
||||
```
|
||||
Before we create a change, let me quickly show you **explore mode**—it's how you think through problems before committing to a direction.
|
||||
```
|
||||
|
||||
Spend 1-2 minutes investigating the relevant code:
|
||||
- Read the file(s) involved
|
||||
- Draw a quick ASCII diagram if it helps
|
||||
- Note any considerations
|
||||
|
||||
```
|
||||
## Quick Exploration
|
||||
|
||||
[Your brief analysis—what you found, any considerations]
|
||||
|
||||
┌─────────────────────────────────────────┐
|
||||
│ [Optional: ASCII diagram if helpful] │
|
||||
└─────────────────────────────────────────┘
|
||||
|
||||
Explore mode (`/opsx:explore`) is for this kind of thinking—investigating before implementing. You can use it anytime you need to think through a problem.
|
||||
|
||||
Now let's create a change to hold our work.
|
||||
```
|
||||
|
||||
**PAUSE** - Wait for user acknowledgment before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Create the Change
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Creating a Change
|
||||
|
||||
A "change" in OpenSpec is a container for all the thinking and planning around a piece of work. It lives in `openspec/changes/<name>/` and holds your artifacts—proposal, specs, design, tasks.
|
||||
|
||||
Let me create one for our task.
|
||||
```
|
||||
|
||||
**DO:** Create the change with a derived kebab-case name:
|
||||
```bash
|
||||
openspec new change "<derived-name>"
|
||||
```
|
||||
|
||||
**SHOW:**
|
||||
```
|
||||
Created: `openspec/changes/<name>/`
|
||||
|
||||
The folder structure:
|
||||
```
|
||||
openspec/changes/<name>/
|
||||
├── proposal.md ← Why we're doing this (empty, we'll fill it)
|
||||
├── design.md ← How we'll build it (empty)
|
||||
├── specs/ ← Detailed requirements (empty)
|
||||
└── tasks.md ← Implementation checklist (empty)
|
||||
```
|
||||
|
||||
Now let's fill in the first artifact—the proposal.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Proposal
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## The Proposal
|
||||
|
||||
The proposal captures **why** we're making this change and **what** it involves at a high level. It's the "elevator pitch" for the work.
|
||||
|
||||
I'll draft one based on our task.
|
||||
```
|
||||
|
||||
**DO:** Draft the proposal content (don't save yet):
|
||||
|
||||
```
|
||||
Here's a draft proposal:
|
||||
|
||||
---
|
||||
|
||||
## Why
|
||||
|
||||
[1-2 sentences explaining the problem/opportunity]
|
||||
|
||||
## What Changes
|
||||
|
||||
[Bullet points of what will be different]
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `<capability-name>`: [brief description]
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- If modifying existing behavior -->
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/path/to/file.ts`: [what changes]
|
||||
- [other files if applicable]
|
||||
|
||||
---
|
||||
|
||||
Does this capture the intent? I can adjust before we save it.
|
||||
```
|
||||
|
||||
**PAUSE** - Wait for user approval/feedback.
|
||||
|
||||
After approval, save the proposal:
|
||||
```bash
|
||||
openspec instructions proposal --change "<name>" --json
|
||||
```
|
||||
Then write the content to `openspec/changes/<name>/proposal.md`.
|
||||
|
||||
```
|
||||
Proposal saved. This is your "why" document—you can always come back and refine it as understanding evolves.
|
||||
|
||||
Next up: specs.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Specs
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Specs
|
||||
|
||||
Specs define **what** we're building in precise, testable terms. They use a requirement/scenario format that makes expected behavior crystal clear.
|
||||
|
||||
For a small task like this, we might only need one spec file.
|
||||
```
|
||||
|
||||
**DO:** Create the spec file:
|
||||
```bash
|
||||
# Unix/macOS
|
||||
mkdir -p openspec/changes/<name>/specs/<capability-name>
|
||||
# Windows (PowerShell)
|
||||
# New-Item -ItemType Directory -Force -Path "openspec/changes/<name>/specs/<capability-name>"
|
||||
```
|
||||
|
||||
Draft the spec content:
|
||||
|
||||
```
|
||||
Here's the spec:
|
||||
|
||||
---
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: <Name>
|
||||
|
||||
<Description of what the system should do>
|
||||
|
||||
#### Scenario: <Scenario name>
|
||||
|
||||
- **WHEN** <trigger condition>
|
||||
- **THEN** <expected outcome>
|
||||
- **AND** <additional outcome if needed>
|
||||
|
||||
---
|
||||
|
||||
This format—WHEN/THEN/AND—makes requirements testable. You can literally read them as test cases.
|
||||
```
|
||||
|
||||
Save to `openspec/changes/<name>/specs/<capability>/spec.md`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Design
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Design
|
||||
|
||||
The design captures **how** we'll build it—technical decisions, tradeoffs, approach.
|
||||
|
||||
For small changes, this might be brief. That's fine—not every change needs deep design discussion.
|
||||
```
|
||||
|
||||
**DO:** Draft design.md:
|
||||
|
||||
```
|
||||
Here's the design:
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
[Brief context about the current state]
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- [What we're trying to achieve]
|
||||
|
||||
**Non-Goals:**
|
||||
- [What's explicitly out of scope]
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: [Key decision]
|
||||
|
||||
[Explanation of approach and rationale]
|
||||
|
||||
---
|
||||
|
||||
For a small task, this captures the key decisions without over-engineering.
|
||||
```
|
||||
|
||||
Save to `openspec/changes/<name>/design.md`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Tasks
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Tasks
|
||||
|
||||
Finally, we break the work into implementation tasks—checkboxes that drive the apply phase.
|
||||
|
||||
These should be small, clear, and in logical order.
|
||||
```
|
||||
|
||||
**DO:** Generate tasks based on specs and design:
|
||||
|
||||
```
|
||||
Here are the implementation tasks:
|
||||
|
||||
---
|
||||
|
||||
## 1. [Category or file]
|
||||
|
||||
- [ ] 1.1 [Specific task]
|
||||
- [ ] 1.2 [Specific task]
|
||||
|
||||
## 2. Verify
|
||||
|
||||
- [ ] 2.1 [Verification step]
|
||||
|
||||
---
|
||||
|
||||
Each checkbox becomes a unit of work in the apply phase. Ready to implement?
|
||||
```
|
||||
|
||||
**PAUSE** - Wait for user to confirm they're ready to implement.
|
||||
|
||||
Save to `openspec/changes/<name>/tasks.md`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: Apply (Implementation)
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Implementation
|
||||
|
||||
Now we implement each task, checking them off as we go. I'll announce each one and occasionally note how the specs/design informed the approach.
|
||||
```
|
||||
|
||||
**DO:** For each task:
|
||||
|
||||
1. Announce: "Working on task N: [description]"
|
||||
2. Implement the change in the codebase
|
||||
3. Reference specs/design naturally: "The spec says X, so I'm doing Y"
|
||||
4. Mark complete in tasks.md: `- [ ]` → `- [x]`
|
||||
5. Brief status: "✓ Task N complete"
|
||||
|
||||
Keep narration light—don't over-explain every line of code.
|
||||
|
||||
After all tasks:
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
All tasks done:
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
- [x] ...
|
||||
|
||||
The change is implemented! One more step—let's archive it.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 10: Archive
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Archiving
|
||||
|
||||
When a change is complete, we archive it. This moves it from `openspec/changes/` to `openspec/changes/archive/YYYY-MM-DD-<name>/`.
|
||||
|
||||
Archived changes become your project's decision history—you can always find them later to understand why something was built a certain way.
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```bash
|
||||
openspec archive "<name>"
|
||||
```
|
||||
|
||||
**SHOW:**
|
||||
```
|
||||
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
|
||||
|
||||
The change is now part of your project's history. The code is in your codebase, the decision record is preserved.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 11: Recap & Next Steps
|
||||
|
||||
```
|
||||
## Congratulations!
|
||||
|
||||
You just completed a full OpenSpec cycle:
|
||||
|
||||
1. **Explore** - Thought through the problem
|
||||
2. **New** - Created a change container
|
||||
3. **Proposal** - Captured WHY
|
||||
4. **Specs** - Defined WHAT in detail
|
||||
5. **Design** - Decided HOW
|
||||
6. **Tasks** - Broke it into steps
|
||||
7. **Apply** - Implemented the work
|
||||
8. **Archive** - Preserved the record
|
||||
|
||||
This same rhythm works for any size change—a small fix or a major feature.
|
||||
|
||||
---
|
||||
|
||||
## Command Reference
|
||||
|
||||
**Core workflow:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:propose` | Create a change and generate all artifacts |
|
||||
| `/opsx:explore` | Think through problems before/during work |
|
||||
| `/opsx:apply` | Implement tasks from a change |
|
||||
| `/opsx:archive` | Archive a completed change |
|
||||
|
||||
**Additional commands:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:new` | Start a new change, step through artifacts one at a time |
|
||||
| `/opsx:continue` | Continue working on an existing change |
|
||||
| `/opsx:ff` | Fast-forward: create all artifacts at once |
|
||||
| `/opsx:verify` | Verify implementation matches artifacts |
|
||||
|
||||
---
|
||||
|
||||
## What's Next?
|
||||
|
||||
Try `/opsx:propose` on something you actually want to build. You've got the rhythm now!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Graceful Exit Handling
|
||||
|
||||
### User wants to stop mid-way
|
||||
|
||||
If the user says they need to stop, want to pause, or seem disengaged:
|
||||
|
||||
```
|
||||
No problem! Your change is saved at `openspec/changes/<name>/`.
|
||||
|
||||
To pick up where we left off later:
|
||||
- `/opsx:continue <name>` - Resume artifact creation
|
||||
- `/opsx:apply <name>` - Jump to implementation (if tasks exist)
|
||||
|
||||
The work won't be lost. Come back whenever you're ready.
|
||||
```
|
||||
|
||||
Exit gracefully without pressure.
|
||||
|
||||
### User just wants command reference
|
||||
|
||||
If the user says they just want to see the commands or skip the tutorial:
|
||||
|
||||
```
|
||||
## OpenSpec Quick Reference
|
||||
|
||||
**Core workflow:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:propose <name>` | Create a change and generate all artifacts |
|
||||
| `/opsx:explore` | Think through problems (no code changes) |
|
||||
| `/opsx:apply <name>` | Implement tasks |
|
||||
| `/opsx:archive <name>` | Archive when done |
|
||||
|
||||
**Additional commands:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:new <name>` | Start a new change, step by step |
|
||||
| `/opsx:continue <name>` | Continue an existing change |
|
||||
| `/opsx:ff <name>` | Fast-forward: all artifacts at once |
|
||||
| `/opsx:verify <name>` | Verify implementation |
|
||||
|
||||
Try `/opsx:propose` to start your first change.
|
||||
```
|
||||
|
||||
Exit gracefully.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Follow the EXPLAIN → DO → SHOW → PAUSE pattern** at key transitions (after explore, after proposal draft, after tasks, after archive)
|
||||
- **Keep narration light** during implementation—teach without lecturing
|
||||
- **Don't skip phases** even if the change is small—the goal is teaching the workflow
|
||||
- **Pause for acknowledgment** at marked points, but don't over-pause
|
||||
- **Handle exits gracefully**—never pressure the user to continue
|
||||
- **Use real codebase tasks**—don't simulate or use fake examples
|
||||
- **Adjust scope gently**—guide toward smaller tasks but respect user choice
|
||||
@@ -1,138 +0,0 @@
|
||||
---
|
||||
name: openspec-sync-specs
|
||||
description: Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Sync delta specs from a change to main specs.
|
||||
|
||||
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show changes that have delta specs (under `specs/` directory).
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Find delta specs**
|
||||
|
||||
Look for delta spec files in `openspec/changes/<name>/specs/*/spec.md`.
|
||||
|
||||
Each delta spec file contains sections like:
|
||||
- `## ADDED Requirements` - New requirements to add
|
||||
- `## MODIFIED Requirements` - Changes to existing requirements
|
||||
- `## REMOVED Requirements` - Requirements to remove
|
||||
- `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
|
||||
|
||||
If no delta specs found, inform user and stop.
|
||||
|
||||
3. **For each delta spec, apply changes to main specs**
|
||||
|
||||
For each capability with a delta spec at `openspec/changes/<name>/specs/<capability>/spec.md`:
|
||||
|
||||
a. **Read the delta spec** to understand the intended changes
|
||||
|
||||
b. **Read the main spec** at `openspec/specs/<capability>/spec.md` (may not exist yet)
|
||||
|
||||
c. **Apply changes intelligently**:
|
||||
|
||||
**ADDED Requirements:**
|
||||
- If requirement doesn't exist in main spec → add it
|
||||
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
||||
|
||||
**MODIFIED Requirements:**
|
||||
- Find the requirement in main spec
|
||||
- Apply the changes - this can be:
|
||||
- Adding new scenarios (don't need to copy existing ones)
|
||||
- Modifying existing scenarios
|
||||
- Changing the requirement description
|
||||
- Preserve scenarios/content not mentioned in the delta
|
||||
|
||||
**REMOVED Requirements:**
|
||||
- Remove the entire requirement block from main spec
|
||||
|
||||
**RENAMED Requirements:**
|
||||
- Find the FROM requirement, rename to TO
|
||||
|
||||
d. **Create new main spec** if capability doesn't exist yet:
|
||||
- Create `openspec/specs/<capability>/spec.md`
|
||||
- Add Purpose section (can be brief, mark as TBD)
|
||||
- Add Requirements section with the ADDED requirements
|
||||
|
||||
4. **Show summary**
|
||||
|
||||
After applying all changes, summarize:
|
||||
- Which capabilities were updated
|
||||
- What changes were made (requirements added/modified/removed/renamed)
|
||||
|
||||
**Delta Spec Format Reference**
|
||||
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: New Feature
|
||||
The system SHALL do something new.
|
||||
|
||||
#### Scenario: Basic case
|
||||
- **WHEN** user does X
|
||||
- **THEN** system does Y
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Existing Feature
|
||||
#### Scenario: New scenario to add
|
||||
- **WHEN** user does A
|
||||
- **THEN** system does B
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Deprecated Feature
|
||||
|
||||
## RENAMED Requirements
|
||||
|
||||
- FROM: `### Requirement: Old Name`
|
||||
- TO: `### Requirement: New Name`
|
||||
```
|
||||
|
||||
**Key Principle: Intelligent Merging**
|
||||
|
||||
Unlike programmatic merging, you can apply **partial updates**:
|
||||
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
||||
- The delta represents *intent*, not a wholesale replacement
|
||||
- Use your judgment to merge changes sensibly
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Specs Synced: <change-name>
|
||||
|
||||
Updated main specs:
|
||||
|
||||
**<capability-1>**:
|
||||
- Added requirement: "New Feature"
|
||||
- Modified requirement: "Existing Feature" (added 1 scenario)
|
||||
|
||||
**<capability-2>**:
|
||||
- Created new spec file
|
||||
- Added requirement: "Another Feature"
|
||||
|
||||
Main specs are now updated. The change remains active - archive when implementation is complete.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Read both delta and main specs before making changes
|
||||
- Preserve existing content not mentioned in delta
|
||||
- If something is unclear, ask for clarification
|
||||
- Show what you're changing as you go
|
||||
- The operation should be idempotent - running twice should give same result
|
||||
@@ -1,168 +0,0 @@
|
||||
---
|
||||
name: openspec-verify-change
|
||||
description: Verify implementation matches change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Verify that an implementation matches the change artifacts (specs, tasks, design).
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show changes that have implementation tasks (tasks artifact exists).
|
||||
Include the schema used for each change if available.
|
||||
Mark changes with incomplete tasks as "(In Progress)".
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifacts exist for this change
|
||||
|
||||
3. **Get the change directory and load artifacts**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns the change directory and context files. Read all available artifacts from `contextFiles`.
|
||||
|
||||
4. **Initialize verification report structure**
|
||||
|
||||
Create a report structure with three dimensions:
|
||||
- **Completeness**: Track tasks and spec coverage
|
||||
- **Correctness**: Track requirement implementation and scenario coverage
|
||||
- **Coherence**: Track design adherence and pattern consistency
|
||||
|
||||
Each dimension can have CRITICAL, WARNING, or SUGGESTION issues.
|
||||
|
||||
5. **Verify Completeness**
|
||||
|
||||
**Task Completion**:
|
||||
- If tasks.md exists in contextFiles, read it
|
||||
- Parse checkboxes: `- [ ]` (incomplete) vs `- [x]` (complete)
|
||||
- Count complete vs total tasks
|
||||
- If incomplete tasks exist:
|
||||
- Add CRITICAL issue for each incomplete task
|
||||
- Recommendation: "Complete task: <description>" or "Mark as done if already implemented"
|
||||
|
||||
**Spec Coverage**:
|
||||
- If delta specs exist in `openspec/changes/<name>/specs/`:
|
||||
- Extract all requirements (marked with "### Requirement:")
|
||||
- For each requirement:
|
||||
- Search codebase for keywords related to the requirement
|
||||
- Assess if implementation likely exists
|
||||
- If requirements appear unimplemented:
|
||||
- Add CRITICAL issue: "Requirement not found: <requirement name>"
|
||||
- Recommendation: "Implement requirement X: <description>"
|
||||
|
||||
6. **Verify Correctness**
|
||||
|
||||
**Requirement Implementation Mapping**:
|
||||
- For each requirement from delta specs:
|
||||
- Search codebase for implementation evidence
|
||||
- If found, note file paths and line ranges
|
||||
- Assess if implementation matches requirement intent
|
||||
- If divergence detected:
|
||||
- Add WARNING: "Implementation may diverge from spec: <details>"
|
||||
- Recommendation: "Review <file>:<lines> against requirement X"
|
||||
|
||||
**Scenario Coverage**:
|
||||
- For each scenario in delta specs (marked with "#### Scenario:"):
|
||||
- Check if conditions are handled in code
|
||||
- Check if tests exist covering the scenario
|
||||
- If scenario appears uncovered:
|
||||
- Add WARNING: "Scenario not covered: <scenario name>"
|
||||
- Recommendation: "Add test or implementation for scenario: <description>"
|
||||
|
||||
7. **Verify Coherence**
|
||||
|
||||
**Design Adherence**:
|
||||
- If design.md exists in contextFiles:
|
||||
- Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
|
||||
- Verify implementation follows those decisions
|
||||
- If contradiction detected:
|
||||
- Add WARNING: "Design decision not followed: <decision>"
|
||||
- Recommendation: "Update implementation or revise design.md to match reality"
|
||||
- If no design.md: Skip design adherence check, note "No design.md to verify against"
|
||||
|
||||
**Code Pattern Consistency**:
|
||||
- Review new code for consistency with project patterns
|
||||
- Check file naming, directory structure, coding style
|
||||
- If significant deviations found:
|
||||
- Add SUGGESTION: "Code pattern deviation: <details>"
|
||||
- Recommendation: "Consider following project pattern: <example>"
|
||||
|
||||
8. **Generate Verification Report**
|
||||
|
||||
**Summary Scorecard**:
|
||||
```
|
||||
## Verification Report: <change-name>
|
||||
|
||||
### Summary
|
||||
| Dimension | Status |
|
||||
|--------------|------------------|
|
||||
| Completeness | X/Y tasks, N reqs|
|
||||
| Correctness | M/N reqs covered |
|
||||
| Coherence | Followed/Issues |
|
||||
```
|
||||
|
||||
**Issues by Priority**:
|
||||
|
||||
1. **CRITICAL** (Must fix before archive):
|
||||
- Incomplete tasks
|
||||
- Missing requirement implementations
|
||||
- Each with specific, actionable recommendation
|
||||
|
||||
2. **WARNING** (Should fix):
|
||||
- Spec/design divergences
|
||||
- Missing scenario coverage
|
||||
- Each with specific recommendation
|
||||
|
||||
3. **SUGGESTION** (Nice to fix):
|
||||
- Pattern inconsistencies
|
||||
- Minor improvements
|
||||
- Each with specific recommendation
|
||||
|
||||
**Final Assessment**:
|
||||
- If CRITICAL issues: "X critical issue(s) found. Fix before archiving."
|
||||
- If only warnings: "No critical issues. Y warning(s) to consider. Ready for archive (with noted improvements)."
|
||||
- If all clear: "All checks passed. Ready for archive."
|
||||
|
||||
**Verification Heuristics**
|
||||
|
||||
- **Completeness**: Focus on objective checklist items (checkboxes, requirements list)
|
||||
- **Correctness**: Use keyword search, file path analysis, reasonable inference - don't require perfect certainty
|
||||
- **Coherence**: Look for glaring inconsistencies, don't nitpick style
|
||||
- **False Positives**: When uncertain, prefer SUGGESTION over WARNING, WARNING over CRITICAL
|
||||
- **Actionability**: Every issue must have a specific recommendation with file/line references where applicable
|
||||
|
||||
**Graceful Degradation**
|
||||
|
||||
- If only tasks.md exists: verify task completion only, skip spec/design checks
|
||||
- If tasks + specs exist: verify completeness and correctness, skip design
|
||||
- If full artifacts: verify all three dimensions
|
||||
- Always note which checks were skipped and why
|
||||
|
||||
**Output Format**
|
||||
|
||||
Use clear markdown with:
|
||||
- Table for summary scorecard
|
||||
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
|
||||
- Code references in format: `file.ts:123`
|
||||
- Specific, actionable recommendations
|
||||
- No vague suggestions like "consider reviewing"
|
||||
96
ARCHITECTURE.md
Normal file
96
ARCHITECTURE.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Architecture Decisions
|
||||
|
||||
## Blazor Hosting Model: Server vs WebAssembly
|
||||
|
||||
### How They Differ
|
||||
|
||||
| Aspect | Blazor Server | Blazor WASM |
|
||||
|--------|--------------|-------------|
|
||||
| Code runs | On the server | In the browser |
|
||||
| UI updates | Via SignalR WebSocket (server pushes diffs) | Locally in browser (no round-trip) |
|
||||
| Calling backend services | Direct — code is already server-side | Needs HTTP calls if accessing server resources |
|
||||
| Offline capable | No (requires persistent connection) | Yes |
|
||||
| Startup speed | Fast | Slower (downloads .NET runtime to browser) |
|
||||
|
||||
### Decision: Blazor WASM
|
||||
|
||||
This project uses **Blazor WebAssembly (standalone)** with a separate ASP.NET Core Web API backend.
|
||||
|
||||
## When Do You Need a Separate API Project?
|
||||
|
||||
There are two distinct concerns that are easy to conflate:
|
||||
|
||||
### 1. Consuming an External API (e.g. OpenAI)
|
||||
|
||||
Both Blazor Server and WASM can call external APIs directly from C# — no controller needed. The difference is **where that code executes**:
|
||||
|
||||
- **Blazor Server**: Code runs on the server. A service class calls OpenAI directly. API keys live safely in server memory. No controller, no exposed endpoint required.
|
||||
- **Blazor WASM**: Code runs in the browser. You *can* call OpenAI directly, but any **API key would be embedded in the browser download** — anyone could inspect it via browser dev tools. This is the primary reason to add a backend proxy.
|
||||
|
||||
### 2. Exposing an API Endpoint (e.g. ChatAgent.Api)
|
||||
|
||||
This is a separate concern. You expose an API endpoint when:
|
||||
|
||||
- Another application needs to communicate with your chat system
|
||||
- You want a REST API for mobile clients, scripts, or integrations
|
||||
- You need a server-side proxy to protect secrets from the browser (the WASM case above)
|
||||
|
||||
**Key insight:** You don't need an API endpoint to *use* an external service. You only need one in WASM to **keep secrets out of the browser**.
|
||||
|
||||
### 3. Component Updates Do Not Require an API
|
||||
|
||||
In both hosting models, Blazor components update themselves locally:
|
||||
|
||||
- Components hold state in fields/properties
|
||||
- Calling `StateHasChanged()` triggers a re-render
|
||||
- Components can call injected C# services directly
|
||||
- No HTTP round-trip is needed for UI updates
|
||||
|
||||
For example, a chat component that echoes "success msg!" back needs no API at all — a simple injected service handles it entirely within the client project.
|
||||
|
||||
## API Key Management Scenarios
|
||||
|
||||
The need for an API project in WASM depends on how secrets are managed:
|
||||
|
||||
| Scenario | WASM needs API project? | Why |
|
||||
|----------|------------------------|-----|
|
||||
| Raw API key in config | Yes | To keep the key out of browser-downloadable code |
|
||||
| Azure Key Vault | Yes | Browser sandbox cannot access Key Vault or managed identity |
|
||||
| API Management gateway (Azure APIM) with token auth | No | WASM calls the gateway directly; gateway handles auth via managed identity |
|
||||
| Blazor Server (any scenario) | No | Code is already server-side; secrets never leave the server |
|
||||
|
||||
### Enterprise Pattern: API Gateway
|
||||
|
||||
In enterprise environments, a common pattern avoids the need for a custom API proxy entirely:
|
||||
|
||||
1. An **API Management gateway** (e.g. Azure APIM) sits in front of the external service (e.g. OpenAI)
|
||||
2. The gateway authenticates via managed identity and handles secret retrieval
|
||||
3. The gateway exposes a public endpoint requiring only a subscription key or OAuth token
|
||||
4. WASM calls the gateway directly — no secrets in the browser, no custom API project needed
|
||||
|
||||
The "proxy" becomes infrastructure rather than application code.
|
||||
|
||||
## Current Project Structure
|
||||
|
||||
```
|
||||
ChatAgent.sln
|
||||
src/
|
||||
ChatAgent.Client/ -- Blazor WASM (standalone)
|
||||
Pages/ -- Routable page components
|
||||
Layout/ -- MainLayout, NavMenu
|
||||
Services/ -- Client-side services (e.g. ChatApiClient)
|
||||
Program.cs -- Client entry point, DI registration
|
||||
ChatAgent.Api/ -- ASP.NET Core Web API (backend proxy)
|
||||
Controllers/ -- API controllers (e.g. HealthController)
|
||||
Program.cs -- Server entry point, middleware config
|
||||
ChatAgent.Shared/ -- Models shared between Client and Api
|
||||
Models/ -- DTOs (e.g. HealthResponse)
|
||||
```
|
||||
|
||||
### Why Three Projects?
|
||||
|
||||
- **ChatAgent.Client**: The Blazor WASM app running in the browser
|
||||
- **ChatAgent.Api**: Exists to proxy requests that require server-side secrets (e.g. future OpenAI calls). Not needed for basic component interactions.
|
||||
- **ChatAgent.Shared**: Models referenced by both Client and Api, avoiding duplication
|
||||
|
||||
For the initial "echo success" phase, only ChatAgent.Client is actively used. The Api and Shared projects exist to support future integration with external services that require secret management.
|
||||
42
CLAUDE.md
42
CLAUDE.md
@@ -1,5 +1,43 @@
|
||||
## Project
|
||||
|
||||
Chat Agent WebApp — a personal AI chat app built with Blazor WebAssembly and the OpenAI GPT API that doubles as an incremental Blazor tutorial.
|
||||
**Chat Agent WebApp**
|
||||
|
||||
For full project details, constraints, and technology stack, see `openspec/specs/`.
|
||||
A personal AI chat web application built with Blazor WebAssembly and MudBlazor. Users send messages through a ChatGPT-style interface and receive responses from a backend service. The project is an incremental learning journey — each phase introduces one concept at a time, making it suitable for a C# developer experienced in backend work but new to web application frameworks.
|
||||
|
||||
**Core Value:** A working chat interface where every line of code is intentional and explained, so the builder learns Blazor patterns while shipping a real product.
|
||||
|
||||
**Current Phase:** Echo — the backend returns "success msg!" for every user message. No external API integration yet.
|
||||
|
||||
### Constraints
|
||||
|
||||
- **Tech stack**: C# / Blazor WebAssembly — non-negotiable
|
||||
- **Hosting model**: Blazor WASM (standalone) with separate ASP.NET Core Web API backend
|
||||
- **UI library**: MudBlazor
|
||||
- **Code style**: Simple, well-documented. Every Blazor concept introduced must have inline comments explaining what it does, why it's done that way, and what idiomatic alternatives exist
|
||||
|
||||
## Technology Stack
|
||||
|
||||
| Technology | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| .NET SDK | 9.0.x | Runtime and tooling |
|
||||
| Blazor WebAssembly Standalone | .NET 9 | Client SPA running in-browser |
|
||||
| ASP.NET Core Web API | .NET 9 | Backend proxy (for future external API calls) |
|
||||
| MudBlazor | latest | Material Design component library |
|
||||
| System.Text.Json | built-in | JSON serialization |
|
||||
|
||||
## Architecture
|
||||
|
||||
See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed hosting model discussion, API design decisions, and project structure rationale.
|
||||
|
||||
**Summary:**
|
||||
- Three-project solution: Client (WASM), Api (backend proxy), Shared (models)
|
||||
- Components update locally — no API needed for UI rendering
|
||||
- The Api project exists for future use when external services require server-side secrets
|
||||
- For the current echo phase, only the Client project is actively used
|
||||
|
||||
## Conventions
|
||||
|
||||
- Inline comments on every new Blazor concept: what it does, why, and idiomatic alternatives
|
||||
- Emphasize framework idiom and explain choices — written for a C# developer new to web/Blazor
|
||||
- Keep code simple; avoid abstractions until they are clearly needed
|
||||
- One concept per phase — do not introduce multiple new patterns at once
|
||||
|
||||
@@ -11,12 +11,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Api", "src\ChatAg
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Shared", "src\ChatAgent.Shared\ChatAgent.Shared.csproj", "{06182E3F-BC78-449B-ADF6-D9EE49E48945}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Api.Tests", "tests\ChatAgent.Api.Tests\ChatAgent.Api.Tests.csproj", "{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Client.Tests", "tests\ChatAgent.Client.Tests\ChatAgent.Client.Tests.csproj", "{DBB73B66-042A-4858-B813-23AEA84FC4C6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -63,30 +57,6 @@ Global
|
||||
{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
|
||||
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Release|x86.Build.0 = Release|Any CPU
|
||||
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -95,7 +65,5 @@ Global
|
||||
{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}
|
||||
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
{DBB73B66-042A-4858-B813-23AEA84FC4C6} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
58
README.md
58
README.md
@@ -1,2 +1,58 @@
|
||||
# AgenticCode
|
||||
# ChatAgent
|
||||
|
||||
A personal AI chat web application built with Blazor WebAssembly and MudBlazor. Currently in the **Echo phase** — the bot responds with "success msg!" to every message.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
ChatAgent.Client/ Blazor WASM app (runs in the browser)
|
||||
ChatAgent.Api/ ASP.NET Core Web API (backend proxy)
|
||||
ChatAgent.Shared/ Shared models between Client and Api
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
### Chat UI only (Echo phase)
|
||||
|
||||
The echo phase doesn't need the API backend — the client handles everything locally.
|
||||
|
||||
```bash
|
||||
cd src/ChatAgent.Client
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Open http://localhost:5100 in your browser.
|
||||
|
||||
### Full stack (Client + API)
|
||||
|
||||
Run both projects in separate terminals:
|
||||
|
||||
```bash
|
||||
# Terminal 1 — API backend
|
||||
cd src/ChatAgent.Api
|
||||
dotnet run
|
||||
|
||||
# Terminal 2 — Blazor WASM client
|
||||
cd src/ChatAgent.Client
|
||||
dotnet run
|
||||
```
|
||||
|
||||
| Service | HTTP | HTTPS |
|
||||
|---------|------|-------|
|
||||
| Client | http://localhost:5100 | https://localhost:5200 |
|
||||
| API | http://localhost:7000 | https://localhost:7100 |
|
||||
|
||||
The health check page is available at `/health` when the API is running.
|
||||
|
||||
## Build
|
||||
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
dotnet build
|
||||
```
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-03
|
||||
@@ -1,33 +0,0 @@
|
||||
## Context
|
||||
|
||||
CLAUDE.md is a monolithic file containing project identity, tech stack research, and stale GSD workflow sections. OpenSpec is now initialized and provides a structured home for this content as specs.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Move project description and constraints into `openspec/specs/project/spec.md`
|
||||
- Move technology stack research into `openspec/specs/stack/spec.md`
|
||||
- Populate `openspec/config.yaml` context so AI agents get project context when creating artifacts
|
||||
- Reduce CLAUDE.md to a slim file that points to OpenSpec for project knowledge
|
||||
|
||||
**Non-Goals:**
|
||||
- Rewriting or editing the migrated content (faithful move, not a rewrite)
|
||||
- Creating conventions or architecture specs (those are still empty placeholders)
|
||||
- Changing any application code
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Spec file format
|
||||
|
||||
The main specs in `openspec/specs/` will use a prose/reference format (not the WHEN/THEN delta format). The delta specs in the change use WHEN/THEN for requirements tracking, but the actual spec content is the migrated prose — tables, lists, and all.
|
||||
|
||||
### Decision 2: CLAUDE.md post-migration content
|
||||
|
||||
CLAUDE.md will retain only:
|
||||
- A one-line project summary
|
||||
- A pointer to `openspec/specs/` for project knowledge
|
||||
- Any workflow instructions specific to Claude Code (not project specs)
|
||||
|
||||
### Decision 3: config.yaml context
|
||||
|
||||
The `context` field in `openspec/config.yaml` will get a brief project summary and tech stack headline, so artifact generation has baseline context without reading full specs.
|
||||
@@ -1,27 +0,0 @@
|
||||
## Why
|
||||
|
||||
CLAUDE.md currently holds all project knowledge — description, constraints, and a large tech stack research block. With OpenSpec initialized, this content belongs in `openspec/specs/` where it can be managed as proper specs, referenced by changes, and won't conflict with CLAUDE.md's role as a slim workflow/instruction file.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Extract project description and constraints into a `project` spec
|
||||
- Extract full technology stack research into a `stack` spec
|
||||
- Populate `openspec/config.yaml` with project context
|
||||
- Slim CLAUDE.md down to workflow instructions with pointers to OpenSpec
|
||||
- Remove stale GSD placeholder sections (conventions, architecture, profile)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `project`: Project identity, core value statement, and non-negotiable constraints
|
||||
- `stack`: Technology stack decisions — packages, versions, alternatives, patterns, compatibility, and sources
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- None — no existing specs yet -->
|
||||
|
||||
## Impact
|
||||
|
||||
- `CLAUDE.md`: Reduced from ~147 lines to a slim pointer file
|
||||
- `openspec/specs/project/spec.md`: New file with project identity
|
||||
- `openspec/specs/stack/spec.md`: New file with stack research
|
||||
- `openspec/config.yaml`: Updated with project context
|
||||
@@ -1,24 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Project identity
|
||||
|
||||
The project spec SHALL contain the project name, a description paragraph, and a core value statement that communicates the project's dual purpose: working AI chat interface and Blazor learning journey.
|
||||
|
||||
#### Scenario: Project description present
|
||||
|
||||
- **WHEN** an AI agent or developer reads the project spec
|
||||
- **THEN** they find the project name ("Chat Agent WebApp"), a description of what the app does, and the core value statement
|
||||
|
||||
### Requirement: Project constraints
|
||||
|
||||
The project spec SHALL enumerate all non-negotiable constraints that govern technical decisions across the project.
|
||||
|
||||
#### Scenario: Constraints enumerated
|
||||
|
||||
- **WHEN** a decision is made about technology, architecture, or approach
|
||||
- **THEN** the project spec provides the authoritative list of constraints to check against:
|
||||
- Tech stack: .NET / C# / Blazor WebAssembly
|
||||
- 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
|
||||
@@ -1,51 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Core technology stack
|
||||
|
||||
The stack spec SHALL document the recommended core technologies with version, purpose, and rationale for each.
|
||||
|
||||
#### Scenario: Core stack documented
|
||||
|
||||
- **WHEN** a developer needs to add or update a dependency
|
||||
- **THEN** the stack spec provides the authoritative record of: .NET 9 SDK, Blazor WebAssembly Standalone, ASP.NET Core Web API, C# 13, OpenAI SDK 2.9.1, Markdig 1.1.1, MudBlazor 9.2.0, and System.Text.Json
|
||||
|
||||
### Requirement: Supporting libraries and tools
|
||||
|
||||
The stack spec SHALL document supporting libraries, development tools, and installation notes.
|
||||
|
||||
#### Scenario: Supporting libraries referenced
|
||||
|
||||
- **WHEN** a developer evaluates adding a new dependency
|
||||
- **THEN** the stack spec lists supporting libraries with guidance on when to use them (e.g., Microsoft.Extensions.AI — skip for v1)
|
||||
|
||||
### Requirement: Alternatives and exclusions
|
||||
|
||||
The stack spec SHALL document considered alternatives and explicitly excluded technologies with rationale.
|
||||
|
||||
#### Scenario: Alternative considered
|
||||
|
||||
- **WHEN** a developer proposes an alternative package or approach
|
||||
- **THEN** the stack spec provides a record of alternatives already evaluated and why the current choice was made
|
||||
|
||||
#### Scenario: Excluded technology referenced
|
||||
|
||||
- **WHEN** a developer considers using a technology on the exclusion list
|
||||
- **THEN** the stack spec explains why it was excluded and what to use instead
|
||||
|
||||
### Requirement: Stack patterns
|
||||
|
||||
The stack spec SHALL document implementation patterns that govern how stack technologies are used together (streaming, storage, markdown rendering).
|
||||
|
||||
#### Scenario: Pattern referenced during implementation
|
||||
|
||||
- **WHEN** a developer implements streaming, storage, or markdown rendering
|
||||
- **THEN** the stack spec provides the canonical pattern to follow
|
||||
|
||||
### Requirement: Version compatibility matrix
|
||||
|
||||
The stack spec SHALL maintain a compatibility matrix and list of authoritative sources for version decisions.
|
||||
|
||||
#### Scenario: Compatibility check
|
||||
|
||||
- **WHEN** a package version is being upgraded
|
||||
- **THEN** the stack spec provides the compatibility matrix to verify cross-package compatibility
|
||||
@@ -1,16 +0,0 @@
|
||||
## 1. Create main specs
|
||||
|
||||
- [x] 1.1 Create `openspec/specs/project/spec.md` with project description, core value, and constraints from CLAUDE.md
|
||||
- [x] 1.2 Create `openspec/specs/stack/spec.md` with full technology stack content from CLAUDE.md
|
||||
|
||||
## 2. Update config
|
||||
|
||||
- [x] 2.1 Populate `openspec/config.yaml` context field with project summary and tech stack headline
|
||||
|
||||
## 3. Slim down CLAUDE.md
|
||||
|
||||
- [x] 3.1 Replace CLAUDE.md contents with slim pointer file
|
||||
|
||||
## 4. Verify
|
||||
|
||||
- [x] 4.1 Confirm no content was lost — all substantive information is in specs
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-04
|
||||
@@ -1,45 +0,0 @@
|
||||
## Context
|
||||
|
||||
No test projects exist. The solution has 3 projects (Client, Api, Shared) under `src/`. Tests need to cover the API controllers and the client's ChatApiClient service, both of which involve HTTP and SSE streaming.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Establish test infrastructure (xUnit + Moq)
|
||||
- Test API controllers using WebApplicationFactory (integration-style)
|
||||
- Test ChatApiClient using a mock HttpMessageHandler (unit-style)
|
||||
- All tests runnable via `dotnet test` from solution root
|
||||
|
||||
**Non-Goals:**
|
||||
- Blazor component tests (bUnit) — Chat.razor is UI-heavy, defer to a future phase
|
||||
- End-to-end browser tests (Playwright/Selenium)
|
||||
- Testing the upstream Responses API itself
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Test framework — xUnit + Moq
|
||||
|
||||
xUnit is the .NET standard. Moq for mocking HttpMessageHandler so we can control HTTP responses in tests without hitting real servers.
|
||||
|
||||
### Decision 2: Test project layout
|
||||
|
||||
```
|
||||
tests/
|
||||
├── ChatAgent.Api.Tests/ → references Api project
|
||||
└── ChatAgent.Client.Tests/ → references Client project + Shared
|
||||
```
|
||||
|
||||
Both added to the `ChatAgent.sln` solution file under a `tests` solution folder.
|
||||
|
||||
### Decision 3: API tests use WebApplicationFactory
|
||||
|
||||
`Microsoft.AspNetCore.Mvc.Testing` provides `WebApplicationFactory<Program>` for integration-style tests. For ChatController, we inject a mock `IHttpClientFactory` that returns a handler with canned SSE responses — no real Responses API needed.
|
||||
|
||||
### Decision 4: Client tests mock HttpMessageHandler
|
||||
|
||||
ChatApiClient takes an HttpClient. Tests create an HttpClient with a custom `DelegatingHandler` that returns canned SSE response streams. This tests the SSE parsing logic in isolation.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [WebApplicationFactory requires Api's Program to be accessible] → Add `InternalsVisibleTo` or use the `public partial class Program {}` trick in Api's Program.cs
|
||||
- [SSE stream mocking is verbose] → Create a small helper method that builds SSE response content from a list of events
|
||||
@@ -1,26 +0,0 @@
|
||||
## Why
|
||||
|
||||
The project has zero test coverage. There are two controllers, a typed HttpClient service, and shared models — all untested. Adding tests now establishes the pattern before the codebase grows, and catches regressions as new phases are added.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Create an xUnit test project for the API (`ChatAgent.Api.Tests`)
|
||||
- Create an xUnit test project for the Client services (`ChatAgent.Client.Tests`)
|
||||
- Add tests for HealthController (GET /api/health)
|
||||
- Add tests for ChatController (POST /api/chat SSE streaming proxy)
|
||||
- Add tests for ChatApiClient (GetHealthAsync, SendChatStreamingAsync)
|
||||
- Add both test projects to the solution
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `test-infrastructure`: Test project setup, test framework choices, and shared test utilities
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- None — adding tests doesn't change existing specs -->
|
||||
|
||||
## Impact
|
||||
|
||||
- `tests/ChatAgent.Api.Tests/`: New xUnit test project
|
||||
- `tests/ChatAgent.Client.Tests/`: New xUnit test project
|
||||
- `ChatAgent.sln`: Add test projects
|
||||
@@ -1,56 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: API test project exists
|
||||
|
||||
An xUnit test project SHALL exist at `tests/ChatAgent.Api.Tests/` targeting the API project, with xUnit, Moq, and Microsoft.AspNetCore.Mvc.Testing as dependencies.
|
||||
|
||||
#### Scenario: API tests run
|
||||
|
||||
- **WHEN** `dotnet test` is run from the solution root
|
||||
- **THEN** API tests are discovered and executed
|
||||
|
||||
### Requirement: Client test project exists
|
||||
|
||||
An xUnit test project SHALL exist at `tests/ChatAgent.Client.Tests/` targeting the Client services, with xUnit and Moq as dependencies.
|
||||
|
||||
#### Scenario: Client tests run
|
||||
|
||||
- **WHEN** `dotnet test` is run from the solution root
|
||||
- **THEN** Client service tests are discovered and executed
|
||||
|
||||
### Requirement: HealthController test coverage
|
||||
|
||||
Tests SHALL verify that GET /api/health returns HTTP 200 with a valid HealthResponse containing a non-empty Status and a recent Timestamp.
|
||||
|
||||
#### Scenario: Health endpoint returns 200
|
||||
|
||||
- **WHEN** a GET request is sent to /api/health
|
||||
- **THEN** the response status is 200 and the body contains `status: "healthy"` and a UTC timestamp
|
||||
|
||||
### Requirement: ChatController test coverage
|
||||
|
||||
Tests SHALL verify that POST /api/chat returns a streaming SSE response containing text deltas and a [DONE] terminator. Tests SHALL mock the upstream Responses API.
|
||||
|
||||
#### Scenario: Chat streams text deltas
|
||||
|
||||
- **WHEN** a POST is sent to /api/chat with a valid ChatRequest
|
||||
- **THEN** the response is `text/event-stream` containing `data: {"text":"..."}` events followed by `data: [DONE]`
|
||||
|
||||
#### Scenario: Chat handles upstream error
|
||||
|
||||
- **WHEN** the Responses API is unreachable or returns an error
|
||||
- **THEN** the response contains a `data: {"error":"..."}` event followed by `data: [DONE]`
|
||||
|
||||
### Requirement: ChatApiClient test coverage
|
||||
|
||||
Tests SHALL verify that SendChatStreamingAsync correctly parses SSE events from the backend into text deltas, handles [DONE], and throws on error events.
|
||||
|
||||
#### Scenario: Client parses text deltas
|
||||
|
||||
- **WHEN** the backend returns SSE events with text deltas
|
||||
- **THEN** SendChatStreamingAsync yields each text fragment in order
|
||||
|
||||
#### Scenario: Client handles error event
|
||||
|
||||
- **WHEN** the backend returns an error SSE event
|
||||
- **THEN** SendChatStreamingAsync throws HttpRequestException with the error message
|
||||
@@ -1,23 +0,0 @@
|
||||
## 1. Test Project Setup
|
||||
|
||||
- [x] 1.1 Create xUnit test project at tests/ChatAgent.Api.Tests with xUnit, Moq, Microsoft.AspNetCore.Mvc.Testing
|
||||
- [x] 1.2 Create xUnit test project at tests/ChatAgent.Client.Tests with xUnit, Moq
|
||||
- [x] 1.3 Add both test projects to ChatAgent.sln under a tests solution folder
|
||||
- [x] 1.4 Make Api's Program class accessible to tests (public partial class Program)
|
||||
|
||||
## 2. API Tests
|
||||
|
||||
- [x] 2.1 Test HealthController: GET /api/health returns 200 with valid HealthResponse
|
||||
- [x] 2.2 Test ChatController: POST /api/chat streams SSE text deltas from mocked upstream
|
||||
- [x] 2.3 Test ChatController: POST /api/chat handles upstream error gracefully
|
||||
|
||||
## 3. Client Service Tests
|
||||
|
||||
- [x] 3.1 Create SSE response helper for building mock SSE streams
|
||||
- [x] 3.2 Test ChatApiClient.SendChatStreamingAsync: parses text deltas in order
|
||||
- [x] 3.3 Test ChatApiClient.SendChatStreamingAsync: throws on error event
|
||||
- [x] 3.4 Test ChatApiClient.GetHealthAsync: returns HealthResponse on success
|
||||
|
||||
## 4. Verify
|
||||
|
||||
- [x] 4.1 Run dotnet test from solution root — all tests pass
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-03
|
||||
@@ -1,48 +0,0 @@
|
||||
## Context
|
||||
|
||||
The project has a working Blazor WASM client and ASP.NET Core API with health check connectivity proven. The client currently uses Bootstrap for layout and has template pages (Counter, Weather). No UI component library is installed. This change introduces MudBlazor and builds the first real feature — a chat interface with hardcoded responses.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Install and configure MudBlazor as the UI component library
|
||||
- Build a ChatGPT/Gemini-inspired chat interface
|
||||
- Establish the message model and UI patterns that future phases will build on
|
||||
- Keep hardcoded responses so the UI is testable without API wiring
|
||||
|
||||
**Non-Goals:**
|
||||
- OpenAI API integration (future phase)
|
||||
- Markdown rendering of messages (future phase — Markdig)
|
||||
- Conversation persistence or history (future phase)
|
||||
- Multiple conversations / sidebar navigation (future phase)
|
||||
- Responsive mobile layout optimization
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: MudBlazor component choices
|
||||
|
||||
**Chat message list**: Use `MudPaper` cards inside a scrollable `div` (or `MudStack`). Each message gets a `MudPaper` with `Elevation="0"` and a background color to distinguish user vs assistant.
|
||||
|
||||
**Input area**: `MudTextField` with `Variant="Outlined"` and an `Adornment` send button (icon). This gives a single-line input with integrated send — similar to ChatGPT.
|
||||
|
||||
**Layout**: `MudLayout` + `MudAppBar` + `MudMainContent`. No drawer/sidebar yet — that comes when we add conversation management.
|
||||
|
||||
**Alternative considered**: Building with raw HTML/CSS. Rejected because MudBlazor is in the tech stack spec and provides the component patterns needed for later phases (dialogs, drawers, lists).
|
||||
|
||||
### Decision 2: ChatMessage model location
|
||||
|
||||
Place `ChatMessage.cs` in `ChatAgent.Shared` so it's available to both Client and API when API integration comes. Fields: `Role` (string: "user" or "assistant"), `Content` (string), `Timestamp` (DateTime).
|
||||
|
||||
### Decision 3: Chat page structure
|
||||
|
||||
The Chat.razor component owns the message list (`List<ChatMessage>`) and handles input. No separate service layer yet — the hardcoded response is inline in the component. When AI integration comes, a service will be extracted.
|
||||
|
||||
### Decision 4: Template page cleanup
|
||||
|
||||
Remove Counter.razor and Weather.razor. Move Home.razor from `/` to `/health` so the health check is still accessible but the chat page takes the root route.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [MudBlazor WASM bundle size] → Acceptable for a personal tool; AOT is deferred per stack spec
|
||||
- [No service abstraction for responses] → Intentional; extracting too early adds complexity before the pattern is clear. Will refactor when adding API integration.
|
||||
- [Removing template pages] → Low risk; they were scaffolding. Health check preserved at `/health`.
|
||||
@@ -1,35 +0,0 @@
|
||||
## Why
|
||||
|
||||
The project exists to be a working AI chat interface, but there is no chat UI yet — only a health check page. This change builds the foundational chat experience using MudBlazor components, with hardcoded responses so the UI can be developed and tested independently of the OpenAI API integration.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Install and configure MudBlazor in the Client project (NuGet, CSS/JS, providers, imports)
|
||||
- Replace the Bootstrap navbar/layout with a MudBlazor layout (MudLayout, MudAppBar, MudMainContent)
|
||||
- Create a Chat page with a message list and text input, styled after ChatGPT/Gemini
|
||||
- Add a shared `ChatMessage` model (role + content + timestamp)
|
||||
- Wire the input to append user messages and reply with a hardcoded bot response
|
||||
- Remove template pages (Counter, Weather) that are no longer needed
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `chat-ui`: The chat interface — message display, input handling, auto-scroll, and layout
|
||||
- `mudblazor-setup`: MudBlazor installation, theming, and provider configuration
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- None -->
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/ChatAgent.Client/ChatAgent.Client.csproj`: Add MudBlazor package
|
||||
- `src/ChatAgent.Client/wwwroot/index.html`: Add MudBlazor CSS/JS/font links
|
||||
- `src/ChatAgent.Client/_Imports.razor`: Add MudBlazor using
|
||||
- `src/ChatAgent.Client/Program.cs`: Add MudBlazor services
|
||||
- `src/ChatAgent.Client/Layout/MainLayout.razor`: Replace with MudBlazor layout
|
||||
- `src/ChatAgent.Client/Layout/NavMenu.razor`: Replace or remove Bootstrap nav
|
||||
- `src/ChatAgent.Client/Pages/Chat.razor`: New chat page (becomes default route)
|
||||
- `src/ChatAgent.Client/Pages/Home.razor`: Demote from `/` route or keep as `/health`
|
||||
- `src/ChatAgent.Shared/Models/ChatMessage.cs`: New shared message model
|
||||
- `src/ChatAgent.Client/Pages/Counter.razor`: Remove
|
||||
- `src/ChatAgent.Client/Pages/Weather.razor`: Remove
|
||||
@@ -1,66 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Message display
|
||||
|
||||
The chat page SHALL display messages in a vertically scrolling list, with each message showing the sender role (user or assistant), the message content, and a visual distinction between user and assistant messages (e.g., alignment, color, or avatar).
|
||||
|
||||
#### Scenario: User message displayed
|
||||
|
||||
- **WHEN** the user sends a message
|
||||
- **THEN** the message appears in the message list aligned or styled to indicate it is from the user
|
||||
|
||||
#### Scenario: Assistant message displayed
|
||||
|
||||
- **WHEN** the assistant responds
|
||||
- **THEN** the response appears in the message list with distinct styling from user messages (different alignment, color, or avatar)
|
||||
|
||||
#### Scenario: Message ordering
|
||||
|
||||
- **WHEN** multiple messages exist in the conversation
|
||||
- **THEN** messages are displayed in chronological order, oldest at top
|
||||
|
||||
### Requirement: Message input
|
||||
|
||||
The chat page SHALL provide a text input area at the bottom of the page where the user can type and submit messages.
|
||||
|
||||
#### Scenario: Submit via button
|
||||
|
||||
- **WHEN** the user types text and clicks the send button
|
||||
- **THEN** the message is added to the conversation and the input is cleared
|
||||
|
||||
#### Scenario: Submit via Enter key
|
||||
|
||||
- **WHEN** the user types text and presses Enter
|
||||
- **THEN** the message is submitted (same as clicking send)
|
||||
|
||||
#### Scenario: Empty input blocked
|
||||
|
||||
- **WHEN** the user attempts to send an empty or whitespace-only message
|
||||
- **THEN** nothing is sent and no message is added
|
||||
|
||||
### Requirement: Hardcoded response
|
||||
|
||||
In this phase, the assistant SHALL reply with a hardcoded message to every user input. This stubs the AI integration point for future phases.
|
||||
|
||||
#### Scenario: Bot replies to any input
|
||||
|
||||
- **WHEN** the user sends any message
|
||||
- **THEN** the assistant replies with a hardcoded response (e.g., "This is a placeholder response. AI integration coming soon!")
|
||||
|
||||
### Requirement: Auto-scroll
|
||||
|
||||
The message list SHALL automatically scroll to the newest message when a new message is added.
|
||||
|
||||
#### Scenario: New message scrolls into view
|
||||
|
||||
- **WHEN** a new message (user or assistant) is added to the conversation
|
||||
- **THEN** the message list scrolls to the bottom so the new message is visible
|
||||
|
||||
### Requirement: Chat page is default route
|
||||
|
||||
The chat page SHALL be the default route (`/`) of the application.
|
||||
|
||||
#### Scenario: App opens to chat
|
||||
|
||||
- **WHEN** the user navigates to the root URL
|
||||
- **THEN** the chat page is displayed
|
||||
@@ -1,46 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: MudBlazor package installed
|
||||
|
||||
The Client project SHALL have MudBlazor 9.2.0 installed as a NuGet dependency.
|
||||
|
||||
#### Scenario: Package reference present
|
||||
|
||||
- **WHEN** the Client project is built
|
||||
- **THEN** MudBlazor 9.2.0 is resolved as a dependency
|
||||
|
||||
### Requirement: MudBlazor services registered
|
||||
|
||||
MudBlazor services SHALL be registered in the Client's DI container via `AddMudServices()`.
|
||||
|
||||
#### Scenario: Services available
|
||||
|
||||
- **WHEN** the application starts
|
||||
- **THEN** MudBlazor services (snackbar, dialog, etc.) are available for injection
|
||||
|
||||
### Requirement: MudBlazor assets loaded
|
||||
|
||||
The Client's `index.html` SHALL include MudBlazor CSS, JS, and font references.
|
||||
|
||||
#### Scenario: Styles and scripts present
|
||||
|
||||
- **WHEN** the application loads in the browser
|
||||
- **THEN** MudBlazor CSS (`_content/MudBlazor/MudBlazor.min.css`), JS (`_content/MudBlazor/MudBlazor.min.js`), and Material Design Icons font are loaded
|
||||
|
||||
### Requirement: MudBlazor layout providers
|
||||
|
||||
The app root SHALL include `MudThemeProvider`, `MudPopoverProvider`, and `MudDialogProvider` so MudBlazor components function correctly.
|
||||
|
||||
#### Scenario: Providers present
|
||||
|
||||
- **WHEN** any MudBlazor component is rendered
|
||||
- **THEN** it functions correctly because the required providers are in the component tree
|
||||
|
||||
### Requirement: MudBlazor layout replaces Bootstrap
|
||||
|
||||
The application layout SHALL use MudBlazor layout components (`MudLayout`, `MudAppBar`, `MudMainContent`) instead of the current Bootstrap navbar.
|
||||
|
||||
#### Scenario: Layout renders with MudBlazor
|
||||
|
||||
- **WHEN** any page is displayed
|
||||
- **THEN** the page is wrapped in a MudBlazor layout with an app bar showing the application name
|
||||
@@ -1,38 +0,0 @@
|
||||
## 1. MudBlazor Setup
|
||||
|
||||
- [x] 1.1 Install MudBlazor 9.2.0 NuGet package in ChatAgent.Client
|
||||
- [x] 1.2 Add MudBlazor CSS, JS, and Material Design Icons font to index.html (remove Bootstrap CSS)
|
||||
- [x] 1.3 Add `@using MudBlazor` to _Imports.razor
|
||||
- [x] 1.4 Register MudBlazor services (`AddMudServices()`) in Program.cs
|
||||
- [x] 1.5 Add MudThemeProvider, MudPopoverProvider, MudDialogProvider to MainLayout.razor
|
||||
|
||||
## 2. Layout Migration
|
||||
|
||||
- [x] 2.1 Replace MainLayout.razor with MudBlazor layout (MudLayout, MudAppBar, MudMainContent)
|
||||
- [x] 2.2 Remove NavMenu.razor (Bootstrap navbar no longer needed)
|
||||
- [x] 2.3 Remove MainLayout.razor.css (MudBlazor handles styling)
|
||||
|
||||
## 3. Shared Model
|
||||
|
||||
- [x] 3.1 Create ChatMessage.cs in ChatAgent.Shared/Models with Role, Content, Timestamp
|
||||
|
||||
## 4. Chat Page
|
||||
|
||||
- [x] 4.1 Create Chat.razor at route `/` with message list and input area
|
||||
- [x] 4.2 Implement message display with MudPaper cards (distinct styling for user vs assistant)
|
||||
- [x] 4.3 Implement text input with MudTextField and send button adornment
|
||||
- [x] 4.4 Wire Enter key and send button to submit handler
|
||||
- [x] 4.5 Block empty/whitespace-only submissions
|
||||
- [x] 4.6 Add hardcoded assistant response after each user message
|
||||
- [x] 4.7 Implement auto-scroll to bottom on new messages
|
||||
|
||||
## 5. Cleanup
|
||||
|
||||
- [x] 5.1 Move Home.razor route from `/` to `/health`
|
||||
- [x] 5.2 Remove Counter.razor and Weather.razor
|
||||
- [x] 5.3 Update app.css — remove Bootstrap-specific styles, keep custom styles that still apply
|
||||
|
||||
## 6. Verify
|
||||
|
||||
- [x] 6.1 Run `dotnet build` on the solution to confirm no errors
|
||||
- [ ] 6.2 Manually verify: chat page loads at `/`, messages display correctly, hardcoded response works
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-04
|
||||
@@ -1,77 +0,0 @@
|
||||
## Context
|
||||
|
||||
The chat backend currently proxies requests to a local CLIProxyAPI instance (OpenAI-compatible API at `localhost:8317`) via manual `HttpClient` calls and SSE parsing in `ChatController`. The architecture works for simple chat completion but has no abstraction for tool calling, function invocation, or agentic loops. The goal is to adopt Semantic Kernel as the AI orchestration layer to enable structured extraction with autonomous validation.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Replace manual HTTP proxy logic with Semantic Kernel's chat completion service
|
||||
- Enable tool/function calling via SK plugins
|
||||
- Implement an agentic extraction loop: extract → validate → retry (up to 3 times) → escalate to user
|
||||
- Preserve the existing SSE contract so the Blazor client requires no changes
|
||||
- Maintain inline tutorial comments explaining SK concepts
|
||||
|
||||
**Non-Goals:**
|
||||
- Multi-agent orchestration (future — when Agent Framework reaches GA)
|
||||
- Changing the Blazor client or `ChatApiClient`
|
||||
- Adding new UI for structured output display (future change)
|
||||
- Replacing CLIProxyAPI — SK's OpenAI connector talks to it as-is
|
||||
- Authentication or multi-user support
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: Use SK's OpenAI chat completion connector pointed at CLIProxyAPI
|
||||
|
||||
**Choice:** `Microsoft.SemanticKernel.Connectors.OpenAI` with `OpenAIChatCompletionService` configured to use `localhost:8317` as the endpoint.
|
||||
|
||||
**Alternatives considered:**
|
||||
- SK Anthropic connector (talks to Anthropic API directly) — would bypass CLIProxyAPI and lose model-switching flexibility
|
||||
- Keep manual HttpClient alongside SK — defeats the purpose of the migration
|
||||
|
||||
**Rationale:** CLIProxyAPI already provides an OpenAI-compatible interface. SK's OpenAI connector works with any OpenAI-compatible endpoint. No infrastructure change required.
|
||||
|
||||
### D2: Register Kernel and plugins in DI via `Program.cs`
|
||||
|
||||
**Choice:** Configure `Kernel` in `Program.cs` using `builder.Services.AddKernel()` and register plugins via DI. Inject `Kernel` into `ChatController`.
|
||||
|
||||
**Rationale:** Follows ASP.NET Core conventions. The kernel is a singleton service with plugins registered at startup. Controller receives it via constructor injection, consistent with the existing pattern of injecting `IHttpClientFactory` and `IConfiguration`.
|
||||
|
||||
### D3: Validation as a native SK plugin function
|
||||
|
||||
**Choice:** Create an `ExtractionPlugin` class with `[KernelFunction]` methods: one for validation of extracted fields. The agent auto-invokes this via `ToolCallBehavior.AutoInvokeKernelFunctions`.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Manual tool call loop in controller code — loses SK's built-in retry/function-calling orchestration
|
||||
- Separate validation service outside SK — requires manual plumbing between LLM and validator
|
||||
|
||||
**Rationale:** SK's auto-invocation handles the loop naturally. The LLM sees the validation function as a tool, calls it, reads the result, and decides whether to retry or escalate. This is the core value proposition of adopting SK.
|
||||
|
||||
### D4: Iteration cap with human-in-the-loop escalation
|
||||
|
||||
**Choice:** Configure `ToolCallBehavior.AutoInvokeKernelFunctions` with `MaximumAutoInvokeAttempts = 3`. If the agent exhausts retries without valid output, it returns a clarification request as a regular chat message to the user.
|
||||
|
||||
**Rationale:** The iteration cap prevents runaway loops. The escalation path uses the existing chat UI — the agent simply asks for clarification in natural language, and the user responds in the next message. No special UI needed.
|
||||
|
||||
### D5: Preserve SSE contract via streaming kernel invocation
|
||||
|
||||
**Choice:** Use `kernel.InvokeStreamingAsync<StreamingChatMessageContent>()` (or `IChatCompletionService.GetStreamingChatMessageContentsAsync()`) and re-emit tokens as the same SSE format the client expects: `data: {"text":"..."}\n\n` and `data: [DONE]\n\n`.
|
||||
|
||||
**Rationale:** The Blazor client's `ChatApiClient` parses this exact format. By keeping the SSE contract identical, the entire client codebase remains untouched.
|
||||
|
||||
### D6: Predefined extraction schema as a strongly-typed C# class
|
||||
|
||||
**Choice:** Define an `ExtractedFields` record/class in `ChatAgent.Shared.Models` with the fixed set of known fields. Validation logic checks for required fields and type correctness.
|
||||
|
||||
**Rationale:** Single output type with fixed keys. A strongly-typed class gives compile-time safety, works with `System.Text.Json` serialization, and can carry data annotations for validation rules.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[SK OpenAI connector compatibility with CLIProxyAPI]** → CLIProxyAPI aims for OpenAI API parity but may have edge cases with tool calling responses. Mitigation: test tool calling end-to-end early; fall back to direct Anthropic connector if needed.
|
||||
- **[Streaming + tool calling interaction]** → When the agent calls a tool mid-stream, the streaming behavior may differ from pure chat completion. Mitigation: handle tool call chunks in the SSE bridge; may need to buffer during tool execution and resume streaming after.
|
||||
- **[SK version churn]** → Semantic Kernel is actively developed; APIs may evolve. Mitigation: pin to a specific stable version, document the version in stack spec.
|
||||
- **[Tutorial complexity increase]** → SK adds abstractions (kernel, plugins, functions) that need explaining. Mitigation: maintain inline comments for every SK concept, consistent with project convention.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- What are the exact field names and types for `ExtractedFields`? (Need user input for the real schema — can use a placeholder for initial implementation.)
|
||||
- Should tool call status ("Validating output...") be surfaced to the client as a distinct SSE event type, or just as regular text tokens? (Current design: regular text, revisit in a future change if needed.)
|
||||
@@ -1,28 +0,0 @@
|
||||
## Why
|
||||
|
||||
The chat backend currently proxies requests to an OpenAI-compatible API (CLIProxyAPI) via manual HttpClient calls and SSE parsing. As the agent evolves toward structured extraction with tool calling and autonomous validation loops, this manual plumbing becomes a liability. Semantic Kernel provides a production-ready abstraction for chat completion, tool/function calling, and auto-invocation — letting us focus on agent behavior rather than HTTP mechanics. Adopting it now establishes the foundation for the agentic workflow (natural language → structured extraction → tool-based validation → human-in-the-loop clarification).
|
||||
|
||||
## What Changes
|
||||
|
||||
- Replace manual HttpClient + SSE proxy in `ChatController` with Semantic Kernel's `OpenAIChatCompletionService` pointed at the existing CLIProxyAPI proxy
|
||||
- Add a validation plugin that the agent can call as a tool to validate extracted key-value output against a predefined schema
|
||||
- Introduce an agentic loop: the kernel autonomously retries extraction up to 2–3 times on validation failure, then escalates to the user for clarification
|
||||
- Keep the existing SSE contract to the Blazor client unchanged — `ChatApiClient` and `Chat.razor` are not modified
|
||||
- **BREAKING**: `ChatController` internals are rewritten; the manual Responses API proxy logic is removed entirely
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `agent-extraction`: Defines the structured field extraction behavior — predefined keys, validation rules, autonomous retry loop, and human-in-the-loop escalation
|
||||
- `semantic-kernel-integration`: Defines how Semantic Kernel is configured, registered, and wired into the API — kernel setup, OpenAI connector config, plugin registration
|
||||
|
||||
### Modified Capabilities
|
||||
- `chat-streaming`: The streaming requirement changes from "proxy SSE from upstream API" to "stream Semantic Kernel chat completion responses as SSE" — same client contract, different server implementation
|
||||
|
||||
## Impact
|
||||
|
||||
- **ChatAgent.Api**: New NuGet dependencies (`Microsoft.SemanticKernel`), `Program.cs` service registration changes, `ChatController` rewritten
|
||||
- **ChatAgent.Api.Tests**: Existing `ChatControllerTests` need updating to mock Semantic Kernel services instead of upstream HTTP calls
|
||||
- **Dependencies**: Adds `Microsoft.SemanticKernel` and `Microsoft.SemanticKernel.Connectors.OpenAI` packages
|
||||
- **Infrastructure**: No change — still talks to CLIProxyAPI at `localhost:8317`
|
||||
- **Client**: No change — SSE contract preserved
|
||||
@@ -1,66 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Structured field extraction from natural language
|
||||
|
||||
The agent SHALL extract a predefined set of key-value pairs from user-provided natural language text (e.g., email content) and return them as a structured JSON object.
|
||||
|
||||
#### Scenario: All fields extracted successfully
|
||||
|
||||
- **WHEN** the user sends a message containing natural language with all required information
|
||||
- **THEN** the agent returns a JSON object with all predefined fields populated from the text
|
||||
|
||||
#### Scenario: Partial extraction
|
||||
|
||||
- **WHEN** the user sends a message that contains some but not all required fields
|
||||
- **THEN** the agent extracts available fields and leaves missing fields as null
|
||||
|
||||
### Requirement: Predefined extraction schema
|
||||
|
||||
The system SHALL define a fixed set of known field names and types as a strongly-typed C# class. All extraction output MUST conform to this schema.
|
||||
|
||||
#### Scenario: Output conforms to schema
|
||||
|
||||
- **WHEN** the agent produces extracted fields
|
||||
- **THEN** every key in the output matches a field defined in the schema and values match expected types
|
||||
|
||||
### Requirement: Autonomous validation via tool calling
|
||||
|
||||
The agent SHALL validate extracted fields by calling a validation tool function. The validation tool checks that all required fields are present and correctly typed.
|
||||
|
||||
#### Scenario: Validation passes
|
||||
|
||||
- **WHEN** the agent calls the validation tool with a complete and correct extraction
|
||||
- **THEN** the tool returns a success result and the agent returns the final output to the user
|
||||
|
||||
#### Scenario: Validation fails with fixable errors
|
||||
|
||||
- **WHEN** the validation tool returns errors for missing or malformed fields
|
||||
- **THEN** the agent re-reads the source text and attempts to fix the extraction without user intervention
|
||||
|
||||
### Requirement: Autonomous retry with iteration cap
|
||||
|
||||
The agent SHALL retry extraction autonomously up to 3 times when validation fails. After exhausting retries, the agent MUST escalate to the user.
|
||||
|
||||
#### Scenario: Agent retries and succeeds
|
||||
|
||||
- **WHEN** validation fails on the first attempt but the error is recoverable
|
||||
- **THEN** the agent retries extraction and calls validation again, up to 3 total attempts
|
||||
|
||||
#### Scenario: Agent exhausts retries and escalates
|
||||
|
||||
- **WHEN** validation fails after 3 attempts
|
||||
- **THEN** the agent sends a natural language message to the user identifying the specific fields it could not resolve and asking for clarification
|
||||
|
||||
### Requirement: Human-in-the-loop clarification
|
||||
|
||||
When the agent escalates to the user, the user SHALL be able to provide the missing information in natural language, and the agent SHALL incorporate the clarification and re-attempt extraction.
|
||||
|
||||
#### Scenario: User provides clarification
|
||||
|
||||
- **WHEN** the agent asks for clarification about missing fields and the user responds
|
||||
- **THEN** the agent incorporates the user's response into the conversation context and produces an updated extraction
|
||||
|
||||
#### Scenario: Clarification via normal chat
|
||||
|
||||
- **WHEN** the agent escalates for clarification
|
||||
- **THEN** the clarification request appears as a regular assistant message in the chat UI, and the user responds via the normal chat input
|
||||
@@ -1,42 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Chat endpoint proxies to Responses API
|
||||
|
||||
The API backend SHALL expose `POST /api/chat` that accepts a list of messages and processes them using a Semantic Kernel chat completion service. The kernel is configured with an OpenAI connector pointed at the existing CLIProxyAPI proxy.
|
||||
|
||||
#### Scenario: Successful chat request
|
||||
|
||||
- **WHEN** the client sends a POST to `/api/chat` with a message list
|
||||
- **THEN** the API processes the messages through the Semantic Kernel and returns the response
|
||||
|
||||
### Requirement: Streaming response delivery
|
||||
|
||||
The API backend SHALL stream the Semantic Kernel's chat completion response back to the WASM client as `text/event-stream`, forwarding text content so the client can render tokens incrementally. The SSE event format MUST remain `data: {"text":"..."}\n\n` for text deltas and `data: [DONE]\n\n` for completion.
|
||||
|
||||
#### Scenario: Tokens stream to client
|
||||
|
||||
- **WHEN** the Semantic Kernel emits streaming chat message content
|
||||
- **THEN** the backend forwards each content chunk as an SSE event to the client containing the text fragment
|
||||
|
||||
#### Scenario: Stream completes
|
||||
|
||||
- **WHEN** the Semantic Kernel streaming response completes
|
||||
- **THEN** the backend signals stream completion to the client with `data: [DONE]\n\n`
|
||||
|
||||
### Requirement: Configurable proxy target
|
||||
|
||||
The CLIProxyAPI base URL and model name SHALL be configurable via `appsettings.json` in the API project, not hardcoded. These values are used to configure the Semantic Kernel OpenAI connector.
|
||||
|
||||
#### Scenario: Configuration read at startup
|
||||
|
||||
- **WHEN** the API starts
|
||||
- **THEN** it reads `ResponsesApi:BaseUrl` and `ResponsesApi:Model` from configuration to configure the Semantic Kernel
|
||||
|
||||
### Requirement: Error propagation
|
||||
|
||||
If the LLM service returns an error or is unreachable, the API backend SHALL return an error SSE event and the client SHALL display the error to the user.
|
||||
|
||||
#### Scenario: LLM service unreachable
|
||||
|
||||
- **WHEN** the CLIProxyAPI proxy is not running
|
||||
- **THEN** the client displays an error message instead of an assistant response
|
||||
@@ -1,42 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Semantic Kernel service registration
|
||||
|
||||
The API backend SHALL register a Semantic Kernel `Kernel` instance in the ASP.NET Core DI container at startup, configured with an OpenAI chat completion connector.
|
||||
|
||||
#### Scenario: Kernel registered at startup
|
||||
|
||||
- **WHEN** the API application starts
|
||||
- **THEN** a `Kernel` instance is available for injection into controllers
|
||||
|
||||
### Requirement: OpenAI connector targets CLIProxyAPI proxy
|
||||
|
||||
The Semantic Kernel OpenAI chat completion service SHALL be configured to use the existing CLIProxyAPI proxy endpoint as its base URL, reading the URL and model name from `appsettings.json`.
|
||||
|
||||
#### Scenario: Connector uses configured endpoint
|
||||
|
||||
- **WHEN** the kernel makes a chat completion request
|
||||
- **THEN** it sends the request to the URL specified in `ResponsesApi:BaseUrl` configuration
|
||||
|
||||
#### Scenario: Model from configuration
|
||||
|
||||
- **WHEN** the kernel makes a chat completion request
|
||||
- **THEN** it uses the model name specified in `ResponsesApi:Model` configuration
|
||||
|
||||
### Requirement: Plugin registration
|
||||
|
||||
The API backend SHALL register extraction and validation plugins with the Kernel so they are available as tools for the LLM to invoke.
|
||||
|
||||
#### Scenario: Plugins available as tools
|
||||
|
||||
- **WHEN** the kernel is constructed
|
||||
- **THEN** all registered plugin functions appear in the tool list sent to the LLM
|
||||
|
||||
### Requirement: Auto function calling
|
||||
|
||||
The Kernel SHALL be configured with automatic function calling enabled, allowing the LLM to invoke registered plugin functions without manual dispatch code.
|
||||
|
||||
#### Scenario: LLM invokes tool automatically
|
||||
|
||||
- **WHEN** the LLM decides to call a registered function during chat completion
|
||||
- **THEN** the kernel automatically executes the function and returns the result to the LLM
|
||||
@@ -1,40 +0,0 @@
|
||||
## 1. Add Semantic Kernel Dependencies
|
||||
|
||||
- [x] 1.1 Add `Microsoft.SemanticKernel` and `Microsoft.SemanticKernel.Connectors.OpenAI` NuGet packages to `ChatAgent.Api`
|
||||
- [x] 1.2 Remove the `OpenAI` SDK package if no longer needed after migration
|
||||
|
||||
## 2. Define Extraction Schema
|
||||
|
||||
- [x] 2.1 Create `ExtractedFields` class in `ChatAgent.Shared/Models/` with the predefined set of key-value fields (placeholder fields until real schema is provided)
|
||||
- [x] 2.2 Create `ValidationResult` class in `ChatAgent.Shared/Models/` with `IsValid`, `Errors` properties
|
||||
|
||||
## 3. Create Extraction Plugin
|
||||
|
||||
- [x] 3.1 Create `ExtractionPlugin` class in `ChatAgent.Api/Plugins/` with a `[KernelFunction]` validation method that checks `ExtractedFields` for required fields and type correctness
|
||||
- [x] 3.2 Add inline tutorial comments explaining SK plugin concepts (`[KernelFunction]`, `[Description]`, auto-invocation)
|
||||
|
||||
## 4. Wire Semantic Kernel in Program.cs
|
||||
|
||||
- [x] 4.1 Register `OpenAIChatCompletionService` in DI using `ResponsesApi:BaseUrl` and `ResponsesApi:Model` from config
|
||||
- [x] 4.2 Register `Kernel` with `AddKernel()` and import `ExtractionPlugin`
|
||||
- [x] 4.3 Add inline tutorial comments explaining kernel setup, connectors, and plugin registration
|
||||
|
||||
## 5. Rewrite ChatController
|
||||
|
||||
- [x] 5.1 Replace `IHttpClientFactory` and `IConfiguration` injection with `Kernel` injection
|
||||
- [x] 5.2 Replace manual HTTP proxy logic with `IChatCompletionService.GetStreamingChatMessageContentsAsync()` using the conversation history from the request
|
||||
- [x] 5.3 Configure `OpenAIPromptExecutionSettings` with `FunctionChoiceBehavior.Auto()` and `autoInvokeMaxCallCount = 3`
|
||||
- [x] 5.4 Re-emit streaming content as the existing SSE format (`data: {"text":"..."}\n\n` and `data: [DONE]\n\n`)
|
||||
- [x] 5.5 Add inline tutorial comments explaining streaming chat completion, execution settings, and tool call behavior
|
||||
|
||||
## 6. Update Tests
|
||||
|
||||
- [x] 6.1 Update `ChatControllerTests` to mock `IChatCompletionService` instead of upstream HTTP calls
|
||||
- [x] 6.2 Add tests for the validation plugin (`ExtractionPlugin` returns correct pass/fail results)
|
||||
- [x] 6.3 Add a test verifying the agent escalates to the user after max retries
|
||||
|
||||
## 7. Verify
|
||||
|
||||
- [x] 7.1 Run `dotnet build` to confirm no errors
|
||||
- [x] 7.2 Run `dotnet test` to confirm all tests pass
|
||||
- [ ] 7.3 Manual smoke test: send a chat message and verify streaming still works end-to-end through SK
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-04
|
||||
@@ -1,30 +0,0 @@
|
||||
## Context
|
||||
|
||||
Chat.razor currently constructs a `ChatRequest` with only the latest user message (line 161-164). The backend and Responses API already support multi-message input — no server changes needed. This is purely a client-side change.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Send full conversation history with each request for multi-turn context
|
||||
- Add a "New Chat" button to reset the session
|
||||
|
||||
**Non-Goals:**
|
||||
- Persistent conversation storage (explicitly deferred)
|
||||
- Multiple conversation tabs/sidebar
|
||||
- Token limit management or history truncation (small conversations for now)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Send full _messages list minus the empty placeholder
|
||||
|
||||
When building the `ChatRequest`, include all messages from `_messages` except the last one (which is the empty assistant placeholder waiting for streaming). This gives the AI the full conversation context.
|
||||
|
||||
### Decision 2: New Chat button in the AppBar
|
||||
|
||||
Place a "New Chat" icon button in the `MudAppBar` (in MainLayout.razor or Chat.razor). Clicking it clears `_messages` and resets to the empty state. Disabled during streaming.
|
||||
|
||||
Alternative considered: putting it in the input area. Rejected — the AppBar is more natural and matches ChatGPT/Gemini placement.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [No token limit management] → For long conversations, the full history could exceed the model's context window. Acceptable for now; truncation can be added later.
|
||||
@@ -1,21 +0,0 @@
|
||||
## Why
|
||||
|
||||
Each chat request currently sends only the latest user message, so the AI has no memory of previous exchanges within the same session. Users expect the assistant to remember context from earlier in the conversation, like ChatGPT/Gemini do.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Send the full conversation history (all prior user and assistant messages) with each API request instead of just the latest user message
|
||||
- The backend already forwards all messages in `ChatRequest.Messages` to the Responses API — no backend changes needed
|
||||
- Add a "New Chat" button to clear the conversation and start fresh
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
<!-- None -->
|
||||
|
||||
### Modified Capabilities
|
||||
- `chat-ui`: Send full message history with each request; add a "New Chat" button to reset the conversation
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/ChatAgent.Client/Pages/Chat.razor`: Change request construction to include full history; add new chat button
|
||||
@@ -1,31 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Streaming AI response
|
||||
|
||||
The assistant SHALL reply with a real AI response streamed from the backend API, using the full conversation history as context. Tokens appear incrementally as they arrive.
|
||||
|
||||
#### Scenario: Bot replies with streamed AI response
|
||||
|
||||
- **WHEN** the user sends any message
|
||||
- **THEN** the assistant message appears and grows token by token as the stream delivers text
|
||||
|
||||
#### Scenario: Full history sent with each request
|
||||
|
||||
- **WHEN** the user sends a message after prior exchanges
|
||||
- **THEN** all previous user and assistant messages are included in the API request so the AI has conversational context
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: New chat button
|
||||
|
||||
The chat page SHALL provide a button to clear the current conversation and start a new one.
|
||||
|
||||
#### Scenario: User starts a new chat
|
||||
|
||||
- **WHEN** the user clicks the "New Chat" button
|
||||
- **THEN** all messages are cleared and the empty state is shown
|
||||
|
||||
#### Scenario: New chat button disabled during streaming
|
||||
|
||||
- **WHEN** the assistant is currently streaming a response
|
||||
- **THEN** the "New Chat" button is disabled
|
||||
@@ -1,13 +0,0 @@
|
||||
## 1. Multi-turn History
|
||||
|
||||
- [x] 1.1 Change ChatRequest construction in Chat.razor to send all prior messages (excluding the empty assistant placeholder) instead of just the latest user message
|
||||
|
||||
## 2. New Chat Button
|
||||
|
||||
- [x] 2.1 Add a "New Chat" icon button to the AppBar or chat page that clears _messages
|
||||
- [x] 2.2 Disable the "New Chat" button while streaming is in progress
|
||||
|
||||
## 3. Verify
|
||||
|
||||
- [x] 3.1 Run dotnet build to confirm no errors
|
||||
- [x] 3.2 Run dotnet test to confirm existing tests still pass
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-04
|
||||
@@ -1,66 +0,0 @@
|
||||
## Context
|
||||
|
||||
The chat UI has a hardcoded response stub. A local OpenAI-compatible proxy at `localhost:8317` serves the Responses API (`POST /v1/responses`) with Claude models. The existing architecture has a WASM client calling the API backend — we add a new endpoint that proxies to the Responses API and streams tokens back.
|
||||
|
||||
The Responses API streaming format uses SSE with events like `response.output_text.delta` carrying a `delta` field with text fragments.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Wire real AI responses through the existing client → backend → proxy chain
|
||||
- Stream tokens to the UI for responsive feel
|
||||
- Keep the proxy URL and model configurable (server-side only)
|
||||
- Show a thinking indicator while waiting for first token
|
||||
|
||||
**Non-Goals:**
|
||||
- Conversation history / multi-turn context (future phase)
|
||||
- Model selection UI (future phase)
|
||||
- Retry logic or rate limiting
|
||||
- Markdown rendering of responses (future phase — Markdig)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Backend proxies the Responses API
|
||||
|
||||
The WASM client cannot call `localhost:8317` directly (different origin, and we keep external service URLs server-side). The API backend gets a new `ChatController` that:
|
||||
1. Receives messages from the client
|
||||
2. Forwards them to `POST /v1/responses` with `"stream": true`
|
||||
3. Reads the SSE stream, extracts `response.output_text.delta` events
|
||||
4. Re-emits the text deltas as simple SSE events to the client
|
||||
|
||||
**Format for client SSE**: `data: {"text": "<delta>"}\n\n` for each token, and `data: [DONE]\n\n` at the end. This is simpler than forwarding the full Responses API event structure.
|
||||
|
||||
**Alternative considered**: Having the client call `localhost:8317` directly via CORS. Rejected — breaks the architecture constraint of keeping external URLs server-side.
|
||||
|
||||
### Decision 2: Client-side streaming with SetBrowserResponseStreamingEnabled
|
||||
|
||||
Per the stack spec, the client uses:
|
||||
- `SetBrowserResponseStreamingEnabled(true)` on the `HttpRequestMessage`
|
||||
- `HttpCompletionOption.ResponseHeadersRead` to start reading before the full response arrives
|
||||
- Line-by-line iteration of the response stream
|
||||
|
||||
This avoids any JavaScript interop for streaming.
|
||||
|
||||
### Decision 3: Simple DTOs in Shared project
|
||||
|
||||
Add `ChatRequest` (list of messages) and keep the existing `ChatMessage` model. The SSE parsing happens in `ChatApiClient` — no DTO needed for individual stream events since they're parsed inline.
|
||||
|
||||
### Decision 4: Configuration via appsettings.json
|
||||
|
||||
The API's `appsettings.json` gets:
|
||||
```json
|
||||
{
|
||||
"ResponsesApi": {
|
||||
"BaseUrl": "http://localhost:8317",
|
||||
"Model": "claude-sonnet-4-6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is the API project's appsettings (server-side, not exposed to the browser).
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Proxy adds latency] → Minimal for localhost; acceptable tradeoff for keeping URLs server-side
|
||||
- [No conversation history] → Intentional; each request is single-turn for now. Multi-turn comes in a future phase.
|
||||
- [No retry on stream failure] → If the stream breaks mid-response, the partial text stays visible and an error is shown. Good enough for phase 1.
|
||||
@@ -1,30 +0,0 @@
|
||||
## Why
|
||||
|
||||
The chat UI currently returns hardcoded responses. A local OpenAI-compatible proxy is running at `localhost:8317` that exposes the Responses API (`POST /v1/responses`) backed by Anthropic Claude models. This change wires the chat to produce real AI responses via streaming, replacing the hardcoded stub.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a chat endpoint to the API backend that proxies requests to the local Responses API
|
||||
- Stream tokens from the Responses API back to the WASM client as SSE
|
||||
- Update ChatApiClient with a streaming chat method
|
||||
- Replace the hardcoded response in Chat.razor with live streaming from the API
|
||||
- Add a "thinking" indicator while the assistant is responding
|
||||
- Disable input during streaming to prevent overlapping requests
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `chat-streaming`: Streaming AI responses from the Responses API proxy through the backend to the WASM client
|
||||
|
||||
### Modified Capabilities
|
||||
- `chat-ui`: Replace hardcoded response with streaming AI response, add typing indicator, disable input during streaming
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/ChatAgent.Api/ChatAgent.Api.csproj`: Add no new packages (uses built-in HttpClient)
|
||||
- `src/ChatAgent.Api/Controllers/ChatController.cs`: New controller proxying to Responses API
|
||||
- `src/ChatAgent.Api/Program.cs`: Register HttpClient for the proxy, add configuration
|
||||
- `src/ChatAgent.Api/appsettings.json`: New — configure Responses API base URL and model
|
||||
- `src/ChatAgent.Client/Services/ChatApiClient.cs`: Add streaming chat method
|
||||
- `src/ChatAgent.Client/Pages/Chat.razor`: Replace hardcoded response with streaming call
|
||||
- `src/ChatAgent.Shared/Models/`: New request/response DTOs for the chat endpoint
|
||||
@@ -1,51 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Chat endpoint proxies to Responses API
|
||||
|
||||
The API backend SHALL expose `POST /api/chat` that accepts a list of messages and proxies the request to the local Responses API at a configurable base URL using the `POST /v1/responses` endpoint.
|
||||
|
||||
#### Scenario: Successful proxy request
|
||||
|
||||
- **WHEN** the client sends a POST to `/api/chat` with a message list
|
||||
- **THEN** the API forwards the messages to the Responses API with the configured model and returns the response
|
||||
|
||||
### Requirement: Streaming response delivery
|
||||
|
||||
The API backend SHALL stream the Responses API's SSE events back to the WASM client as `text/event-stream`, forwarding `response.output_text.delta` events so the client can render tokens incrementally.
|
||||
|
||||
#### Scenario: Tokens stream to client
|
||||
|
||||
- **WHEN** the Responses API emits `response.output_text.delta` events
|
||||
- **THEN** the backend forwards each delta as an SSE event to the client containing the text fragment
|
||||
|
||||
#### Scenario: Stream completes
|
||||
|
||||
- **WHEN** the Responses API emits `response.completed`
|
||||
- **THEN** the backend signals stream completion to the client
|
||||
|
||||
### Requirement: Configurable proxy target
|
||||
|
||||
The Responses API base URL and model name SHALL be configurable via `appsettings.json` in the API project, not hardcoded.
|
||||
|
||||
#### Scenario: Configuration read at startup
|
||||
|
||||
- **WHEN** the API starts
|
||||
- **THEN** it reads `ResponsesApi:BaseUrl` and `ResponsesApi:Model` from configuration
|
||||
|
||||
### Requirement: Client streams from backend
|
||||
|
||||
The WASM client SHALL call `POST /api/chat` with `SetBrowserResponseStreamingEnabled(true)` and `HttpCompletionOption.ResponseHeadersRead`, then iterate the SSE stream to update the UI token by token.
|
||||
|
||||
#### Scenario: Client reads streaming response
|
||||
|
||||
- **WHEN** the client sends a chat request
|
||||
- **THEN** it reads the response stream incrementally and appends each text delta to the assistant message in real time
|
||||
|
||||
### Requirement: Error propagation
|
||||
|
||||
If the Responses API returns an error or is unreachable, the API backend SHALL return an appropriate HTTP error status and the client SHALL display the error to the user.
|
||||
|
||||
#### Scenario: Proxy unreachable
|
||||
|
||||
- **WHEN** the Responses API is not running
|
||||
- **THEN** the client displays an error message instead of an assistant response
|
||||
@@ -1,50 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Hardcoded response
|
||||
|
||||
The assistant SHALL reply with a real AI response streamed from the backend API, replacing the previous hardcoded stub. Tokens appear incrementally as they arrive.
|
||||
|
||||
#### Scenario: Bot replies with streamed AI response
|
||||
|
||||
- **WHEN** the user sends any message
|
||||
- **THEN** the assistant message appears and grows token by token as the stream delivers text
|
||||
|
||||
### Requirement: Message input
|
||||
|
||||
The chat page SHALL provide a text input area at the bottom of the page where the user can type and submit messages.
|
||||
|
||||
#### Scenario: Submit via button
|
||||
|
||||
- **WHEN** the user types text and clicks the send button
|
||||
- **THEN** the message is added to the conversation and the input is cleared
|
||||
|
||||
#### Scenario: Submit via Enter key
|
||||
|
||||
- **WHEN** the user types text and presses Enter
|
||||
- **THEN** the message is submitted (same as clicking send)
|
||||
|
||||
#### Scenario: Empty input blocked
|
||||
|
||||
- **WHEN** the user attempts to send an empty or whitespace-only message
|
||||
- **THEN** nothing is sent and no message is added
|
||||
|
||||
#### Scenario: Input disabled during streaming
|
||||
|
||||
- **WHEN** the assistant is currently streaming a response
|
||||
- **THEN** the input field and send button are disabled until streaming completes
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Thinking indicator
|
||||
|
||||
The chat page SHALL show a visual indicator while waiting for the first token from the assistant.
|
||||
|
||||
#### Scenario: Indicator shown during wait
|
||||
|
||||
- **WHEN** the user sends a message and the assistant has not yet started streaming
|
||||
- **THEN** a thinking indicator (e.g., animated dots) is shown in the assistant message area
|
||||
|
||||
#### Scenario: Indicator replaced by content
|
||||
|
||||
- **WHEN** the first token arrives from the stream
|
||||
- **THEN** the thinking indicator is replaced by the streamed text
|
||||
@@ -1,29 +0,0 @@
|
||||
## 1. Shared Models
|
||||
|
||||
- [x] 1.1 Create ChatRequest.cs in ChatAgent.Shared/Models with a Messages list property
|
||||
|
||||
## 2. API Backend
|
||||
|
||||
- [x] 2.1 Add appsettings.json to ChatAgent.Api with ResponsesApi:BaseUrl and ResponsesApi:Model
|
||||
- [x] 2.2 Register an HttpClient for the Responses API proxy in Api Program.cs
|
||||
- [x] 2.3 Create ChatController with POST /api/chat that proxies to the Responses API with streaming
|
||||
- [x] 2.4 Parse Responses API SSE stream, extract response.output_text.delta events, re-emit as simplified SSE to client
|
||||
|
||||
## 3. Client Streaming
|
||||
|
||||
- [x] 3.1 Add a streaming SendChatAsync method to ChatApiClient that uses SetBrowserResponseStreamingEnabled and HttpCompletionOption.ResponseHeadersRead
|
||||
- [x] 3.2 Parse the simplified SSE stream line-by-line, yielding text deltas
|
||||
|
||||
## 4. Chat Page Updates
|
||||
|
||||
- [x] 4.1 Replace hardcoded response in Chat.razor with a call to ChatApiClient.SendChatAsync
|
||||
- [x] 4.2 Append tokens to the assistant message incrementally with StateHasChanged after each delta
|
||||
- [x] 4.3 Add a thinking indicator shown until the first token arrives
|
||||
- [x] 4.4 Disable input field and send button while streaming is in progress
|
||||
- [x] 4.5 Handle errors — display error message if API call fails
|
||||
- [x] 4.6 Auto-scroll during streaming (not just at the end)
|
||||
|
||||
## 5. Verify
|
||||
|
||||
- [x] 5.1 Run dotnet build to confirm no errors
|
||||
- [ ] 5.2 Manually verify: send a message, see streaming response from Claude
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-05
|
||||
@@ -1,57 +0,0 @@
|
||||
## Context
|
||||
|
||||
Assistant messages from the LLM contain markdown formatting (bold, code blocks, lists, headings) but are currently rendered as plain text via `<MudText>@message.Content</MudText>`. Markdig 1.1.1 is already in the stack spec but not yet wired up. The Blazor WASM client needs a rendering pipeline that converts markdown to HTML and displays it safely inside chat bubbles.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Render assistant message markdown as formatted HTML (headings, bold, italic, code, lists, tables, links)
|
||||
- Sanitize HTML output to prevent XSS from LLM-generated content
|
||||
- Style rendered elements to look good inside MudBlazor chat bubbles
|
||||
- Maintain streaming performance — re-render as tokens arrive without flicker
|
||||
|
||||
**Non-Goals:**
|
||||
- Rendering user messages as markdown (they stay plain text)
|
||||
- Syntax highlighting for code blocks (future enhancement)
|
||||
- LaTeX/math rendering
|
||||
- Image rendering from markdown
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: Use Markdig for markdown-to-HTML conversion
|
||||
|
||||
Markdig is already in the stack spec. It's the standard .NET markdown library, runs in WASM, and supports GFM (GitHub Flavored Markdown) extensions out of the box.
|
||||
|
||||
**Alternative**: Custom regex-based parsing — rejected, fragile and incomplete.
|
||||
|
||||
### D2: Render via `MarkupString` in Blazor
|
||||
|
||||
Blazor's `MarkupString` struct renders raw HTML in the component tree. We wrap the Markdig HTML output in `MarkupString` to display it. This replaces `@message.Content` with `@((MarkupString)renderedHtml)` for assistant messages only.
|
||||
|
||||
**Alternative**: Use a third-party Blazor markdown component — rejected, adds a dependency when Markdig + MarkupString achieves the same result with less coupling.
|
||||
|
||||
### D3: HTML sanitization via allowlist approach
|
||||
|
||||
LLM output is untrusted. After Markdig converts markdown to HTML, we strip any tags/attributes not on an allowlist. Allowed: `p, h1-h6, strong, em, code, pre, ul, ol, li, a (href only), table, thead, tbody, tr, th, td, br, blockquote`. This prevents script injection without a heavy sanitizer dependency.
|
||||
|
||||
**Alternative**: Use a NuGet sanitizer package (e.g., HtmlSanitizer) — viable but adds a dependency for a simple allowlist. If the allowlist proves insufficient, revisit.
|
||||
|
||||
### D4: Create a MarkdownService for the conversion pipeline
|
||||
|
||||
A `MarkdownService` class encapsulates the Markdig pipeline configuration, HTML sanitization, and caching. Registered as singleton in DI. This keeps the rendering logic out of the Razor component and makes it testable.
|
||||
|
||||
### D5: Incremental rendering during streaming
|
||||
|
||||
During streaming, the assistant message content grows token by token. Each time `StateHasChanged()` is called, the full content string is re-converted through Markdig. This is acceptable because:
|
||||
- Markdig is fast (sub-millisecond for typical message sizes)
|
||||
- Messages rarely exceed a few KB
|
||||
- No DOM diffing overhead — Blazor handles this efficiently
|
||||
|
||||
If performance becomes an issue, we can cache the last-known-good HTML prefix and only re-render the delta.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[XSS from LLM output]** → Mitigated by HTML sanitization allowlist. The allowlist is conservative — only structural tags, no script/style/event handlers.
|
||||
- **[Streaming flicker]** → Low risk. Blazor's diffing minimizes DOM updates. If observed, add debouncing to StateHasChanged calls.
|
||||
- **[Markdig WASM size]** → Markdig adds ~200KB to the WASM bundle. Already accepted in stack spec.
|
||||
- **[Incomplete markdown support]** → Markdig supports GFM by default. Edge cases (nested tables, complex HTML blocks) may render imperfectly but are rare in LLM output.
|
||||
@@ -1,24 +0,0 @@
|
||||
## Why
|
||||
|
||||
Assistant messages currently render as plain text — markdown formatting (bold, code blocks, lists, headings) from the LLM appears as raw characters. With Semantic Kernel and tool calling now in place, responses are increasingly structured and harder to read without proper rendering. Markdig 1.1.1 is already in the stack but not wired up.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Render assistant message content as HTML by converting markdown via Markdig
|
||||
- Sanitize rendered HTML to prevent XSS (the LLM output is untrusted content)
|
||||
- Style rendered markdown elements (code blocks, lists, tables) to fit the chat bubble aesthetic
|
||||
- Keep user messages as plain text (they are short inputs, not markdown)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `rich-text-display`: Markdown-to-HTML rendering pipeline for assistant messages, including sanitization and styling
|
||||
|
||||
### Modified Capabilities
|
||||
- `chat-ui`: Assistant message display changes from plain text to rendered markdown
|
||||
|
||||
## Impact
|
||||
|
||||
- ChatAgent.Client: Chat.razor message rendering, new markdown service, CSS additions
|
||||
- Dependencies: Markdig already in stack spec; may need an HTML sanitizer package
|
||||
- No backend changes — this is purely client-side rendering
|
||||
@@ -1,20 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Message display
|
||||
|
||||
The chat page SHALL display messages in a vertically scrolling list, with each message showing the sender role (user or assistant), the message content, and a visual distinction between user and assistant messages (e.g., alignment, color, or avatar). Assistant messages SHALL render content as formatted HTML converted from markdown; user messages SHALL display as plain text.
|
||||
|
||||
#### Scenario: User message displayed
|
||||
|
||||
- **WHEN** the user sends a message
|
||||
- **THEN** the message appears in the message list aligned or styled to indicate it is from the user, rendered as plain text
|
||||
|
||||
#### Scenario: Assistant message displayed
|
||||
|
||||
- **WHEN** the assistant responds
|
||||
- **THEN** the response appears in the message list with distinct styling from user messages, with markdown content rendered as formatted HTML
|
||||
|
||||
#### Scenario: Message ordering
|
||||
|
||||
- **WHEN** multiple messages exist in the conversation
|
||||
- **THEN** messages are displayed in chronological order, oldest at top
|
||||
@@ -1,91 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Markdown rendering for assistant messages
|
||||
|
||||
The system SHALL convert assistant message content from markdown to formatted HTML using Markdig, displaying headings, bold, italic, code blocks, lists, tables, links, and blockquotes with proper visual formatting.
|
||||
|
||||
#### Scenario: Markdown bold and italic rendered
|
||||
|
||||
- **WHEN** an assistant message contains `**bold**` or `*italic*` text
|
||||
- **THEN** the text is displayed with bold or italic formatting respectively
|
||||
|
||||
#### Scenario: Code block rendered
|
||||
|
||||
- **WHEN** an assistant message contains a fenced code block (triple backticks)
|
||||
- **THEN** the code is displayed in a monospace font within a visually distinct block
|
||||
|
||||
#### Scenario: Inline code rendered
|
||||
|
||||
- **WHEN** an assistant message contains inline code (single backticks)
|
||||
- **THEN** the code is displayed in a monospace font with a subtle background
|
||||
|
||||
#### Scenario: List rendered
|
||||
|
||||
- **WHEN** an assistant message contains a markdown list (ordered or unordered)
|
||||
- **THEN** the list is displayed with proper indentation and bullet/number markers
|
||||
|
||||
#### Scenario: Heading rendered
|
||||
|
||||
- **WHEN** an assistant message contains markdown headings (# through ######)
|
||||
- **THEN** the headings are displayed with appropriate size and weight
|
||||
|
||||
#### Scenario: Link rendered
|
||||
|
||||
- **WHEN** an assistant message contains a markdown link `[text](url)`
|
||||
- **THEN** the link is displayed as a clickable hyperlink opening in a new tab
|
||||
|
||||
#### Scenario: Table rendered
|
||||
|
||||
- **WHEN** an assistant message contains a markdown table
|
||||
- **THEN** the table is displayed with borders, header row styling, and proper alignment
|
||||
|
||||
### Requirement: HTML sanitization
|
||||
|
||||
The system SHALL sanitize all HTML output from the markdown renderer to prevent cross-site scripting (XSS) attacks from LLM-generated content.
|
||||
|
||||
#### Scenario: Script tags stripped
|
||||
|
||||
- **WHEN** assistant message content contains `<script>` tags
|
||||
- **THEN** the script tags and their content are removed from the rendered output
|
||||
|
||||
#### Scenario: Event handlers stripped
|
||||
|
||||
- **WHEN** assistant message content contains HTML attributes like `onclick` or `onerror`
|
||||
- **THEN** the attributes are removed from the rendered output
|
||||
|
||||
#### Scenario: Safe tags preserved
|
||||
|
||||
- **WHEN** assistant message content contains allowed structural HTML (p, strong, em, code, pre, ul, ol, li, a, table elements, blockquote, br, h1-h6)
|
||||
- **THEN** the tags are preserved in the rendered output
|
||||
|
||||
### Requirement: Markdown styling within chat bubbles
|
||||
|
||||
The system SHALL style rendered markdown elements to be visually consistent with the MudBlazor chat bubble theme.
|
||||
|
||||
#### Scenario: Code block styled in bubble
|
||||
|
||||
- **WHEN** a code block is rendered inside an assistant chat bubble
|
||||
- **THEN** it has a distinct background color, padding, border-radius, and does not overflow the bubble width (horizontal scroll if needed)
|
||||
|
||||
#### Scenario: Paragraph spacing in bubble
|
||||
|
||||
- **WHEN** multiple paragraphs are rendered inside an assistant chat bubble
|
||||
- **THEN** paragraphs have appropriate spacing without excessive gaps
|
||||
|
||||
### Requirement: Streaming compatibility
|
||||
|
||||
The system SHALL re-render markdown as new tokens arrive during streaming without visual glitches.
|
||||
|
||||
#### Scenario: Partial markdown rendered during streaming
|
||||
|
||||
- **WHEN** tokens are arriving and the current content contains incomplete markdown (e.g., a code block not yet closed)
|
||||
- **THEN** the content is rendered with best-effort formatting and updates smoothly as more tokens complete the markdown structure
|
||||
|
||||
### Requirement: User messages remain plain text
|
||||
|
||||
The system SHALL continue to render user messages as plain text without markdown processing.
|
||||
|
||||
#### Scenario: User message not processed as markdown
|
||||
|
||||
- **WHEN** a user message contains markdown syntax like `**bold**`
|
||||
- **THEN** the raw text `**bold**` is displayed, not formatted bold text
|
||||
@@ -1,26 +0,0 @@
|
||||
## 1. Markdown Service
|
||||
|
||||
- [x] 1.1 Add Markdig NuGet package to ChatAgent.Client project
|
||||
- [x] 1.2 Create MarkdownService class with a ConfigureMarkdigPipeline method using AdvancedExtensions (GFM tables, task lists, pipe tables)
|
||||
- [x] 1.3 Add ConvertToHtml method that takes a markdown string and returns sanitized HTML
|
||||
- [x] 1.4 Implement HTML sanitization via tag/attribute allowlist (p, h1-h6, strong, em, code, pre, ul, ol, li, a[href], table, thead, tbody, tr, th, td, br, blockquote)
|
||||
- [x] 1.5 Register MarkdownService as singleton in Program.cs
|
||||
|
||||
## 2. Chat Component Integration
|
||||
|
||||
- [x] 2.1 Inject MarkdownService into Chat.razor
|
||||
- [x] 2.2 For assistant messages, replace `@message.Content` with `@((MarkupString)MarkdownService.ConvertToHtml(message.Content))`
|
||||
- [x] 2.3 Keep user messages rendering as plain text via `@message.Content`
|
||||
- [x] 2.4 Verify streaming still works — markdown re-renders as tokens arrive
|
||||
|
||||
## 3. CSS Styling
|
||||
|
||||
- [x] 3.1 Add CSS for rendered markdown inside assistant bubbles: code blocks (background, padding, border-radius, overflow-x auto), inline code (background, padding), paragraphs (margin control), lists (indentation), tables (borders, header styling), blockquotes (left border, padding), links (color, underline)
|
||||
- [x] 3.2 Ensure code blocks do not overflow bubble width — add horizontal scroll
|
||||
- [x] 3.3 Ensure last paragraph/element in bubble has no bottom margin (tight fit)
|
||||
|
||||
## 4. Testing
|
||||
|
||||
- [x] 4.1 Add unit tests for MarkdownService: bold/italic, code blocks, lists, headings, tables, links
|
||||
- [x] 4.2 Add sanitization tests: script tags stripped, event handlers stripped, safe tags preserved
|
||||
- [x] 4.3 Manual test: send a message asking LLM to respond with markdown formatting and verify rendering
|
||||
@@ -1,9 +1,20 @@
|
||||
schema: spec-driven
|
||||
|
||||
context: |
|
||||
Chat Agent WebApp — personal AI chat app built with Blazor WebAssembly + OpenAI GPT API.
|
||||
Tech stack: .NET 9, C# 13, Blazor WASM (standalone client), ASP.NET Core Web API (backend proxy).
|
||||
Key libraries: OpenAI SDK 2.9.1, Markdig 1.1.1, MudBlazor 9.2.0, System.Text.Json.
|
||||
Storage: JSON files on local disk. Single-user, no auth.
|
||||
The project is also a Blazor tutorial — every concept must have inline comments explaining what and why.
|
||||
See openspec/specs/project/ and openspec/specs/stack/ for full details.
|
||||
# Project context (optional)
|
||||
# This is shown to AI when creating artifacts.
|
||||
# Add your tech stack, conventions, style guides, domain knowledge, etc.
|
||||
# Example:
|
||||
# context: |
|
||||
# Tech stack: TypeScript, React, Node.js
|
||||
# We use conventional commits
|
||||
# Domain: e-commerce platform
|
||||
|
||||
# Per-artifact rules (optional)
|
||||
# Add custom rules for specific artifacts.
|
||||
# Example:
|
||||
# rules:
|
||||
# proposal:
|
||||
# - Keep proposals under 500 words
|
||||
# - Always include a "Non-goals" section
|
||||
# tasks:
|
||||
# - Break tasks into chunks of max 2 hours
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
# OpenSpec Portable Variant
|
||||
## For hand-typing into target's OpenSpec on the sandbox
|
||||
|
||||
Type these two files into the target's `openspec/changes/add-sk-chat/` directory.
|
||||
Then run `/opsx:apply` and let the AI implement from the tasks.
|
||||
|
||||
**Lines to type**: ~25 | **vs code recipe**: ~120 lines | **Compression**: 4.8x
|
||||
|
||||
---
|
||||
|
||||
## File 1: proposal.md
|
||||
|
||||
```markdown
|
||||
# Add SK Chat Endpoint with Tool Calling
|
||||
|
||||
## Why
|
||||
Need an AI chat endpoint that streams responses and supports
|
||||
autonomous tool calling for structured data extraction/validation.
|
||||
|
||||
## What Changes
|
||||
- Add Semantic Kernel 1.74.0 with OpenAI connector
|
||||
- POST /api/chat endpoint streaming SSE via SK
|
||||
- ExtractionPlugin with [KernelFunction] for field validation
|
||||
- Shared models: ChatRequest, ChatMessage, ExtractedFields, ValidationResult
|
||||
|
||||
## Impact
|
||||
- API project: new controller, plugin, DI wiring
|
||||
- Shared project: new models
|
||||
- Config: ResponsesApi section in appsettings.json
|
||||
```
|
||||
|
||||
## File 2: tasks.md
|
||||
|
||||
```markdown
|
||||
## 1. Shared Models
|
||||
- [ ] 1.1 Create ChatMessage (Role string, Content string, Timestamp DateTime)
|
||||
- [ ] 1.2 Create ChatRequest (Messages List<ChatMessage>)
|
||||
- [ ] 1.3 Create ExtractedFields with required fields: Client, Project, Hours(decimal?), Rate(decimal?), Currency, Date; optional: Description, PoNumber
|
||||
- [ ] 1.4 Create ValidationResult (IsValid bool, Errors List<string>)
|
||||
|
||||
## 2. Extraction Plugin
|
||||
- [ ] 2.1 Create ExtractionPlugin class with [KernelFunction("validate_extracted_fields")]
|
||||
- [ ] 2.2 Accepts fieldsJson string, deserializes to ExtractedFields with PropertyNameCaseInsensitive
|
||||
- [ ] 2.3 Validates required fields non-null/non-empty, decimals > 0
|
||||
- [ ] 2.4 Returns JSON serialized ValidationResult
|
||||
|
||||
## 3. Chat Controller
|
||||
- [ ] 3.1 Create ChatController [ApiController] Route("api/[controller]") injecting Kernel
|
||||
- [ ] 3.2 POST endpoint: set response to text/event-stream, no-cache
|
||||
- [ ] 3.3 Convert request messages to SK ChatHistory (user/assistant roles)
|
||||
- [ ] 3.4 Import ExtractionPlugin per-request via _kernel.ImportPluginFromObject
|
||||
- [ ] 3.5 Use OpenAIPromptExecutionSettings with FunctionChoiceBehavior.Auto()
|
||||
- [ ] 3.6 Stream via GetStreamingChatMessageContentsAsync, emit SSE: data: {"text":"..."}\n\n
|
||||
- [ ] 3.7 Emit data: [DONE]\n\n on completion
|
||||
- [ ] 3.8 Handle HttpRequestException (emit error SSE), TaskCanceledException (silent)
|
||||
|
||||
## 4. DI Wiring (Program.cs)
|
||||
- [ ] 4.1 Add using Microsoft.SemanticKernel at top of Program.cs
|
||||
- [ ] 4.2 Read BaseUrl and Model from config "ResponsesApi" section
|
||||
- [ ] 4.3 AddOpenAIChatCompletion(modelId, endpoint with /v1 suffix, apiKey)
|
||||
- [ ] 4.4 AddKernel()
|
||||
- [ ] 4.5 AddSingleton<ExtractionPlugin>()
|
||||
|
||||
## 5. Configuration
|
||||
- [ ] 5.1 Add ResponsesApi section to appsettings.json: BaseUrl "http://localhost:8317/v1", Model "claude-sonnet-4-6"
|
||||
- [ ] 5.2 Add NuGet: Microsoft.SemanticKernel 1.74.0, Microsoft.SemanticKernel.Connectors.OpenAI 1.74.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage on sandbox
|
||||
|
||||
1. Create the change: `openspec new change "add-sk-chat"`
|
||||
2. Type `proposal.md` into `openspec/changes/add-sk-chat/proposal.md`
|
||||
3. Type `tasks.md` into `openspec/changes/add-sk-chat/tasks.md`
|
||||
4. Run `/opsx:apply add-sk-chat` — the AI implements all tasks
|
||||
@@ -1,251 +0,0 @@
|
||||
# Feature Recipe: Semantic Kernel Chat with Tool Calling
|
||||
|
||||
**Source**: migrate-to-semantic-kernel + wire-responses-api | **Lines to type**: ~120
|
||||
**Included**: wire-responses-api (SSE streaming), migrate-to-semantic-kernel (SK + plugins)
|
||||
**Skipped**: wire-responses-api's manual HttpClient proxy (superseded by SK)
|
||||
**Skipped**: basic-chat-interface (UI — not selected), multi-turn-conversations (not selected)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Add to your API `.csproj`:
|
||||
```xml
|
||||
<PackageReference Include="Microsoft.SemanticKernel" Version="1.74.0" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.74.0" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1: New file — Shared/Models/ChatMessage.cs (~6 lines)
|
||||
|
||||
```csharp
|
||||
namespace __YOUR_NAMESPACE__.Shared.Models
|
||||
{
|
||||
public class ChatMessage
|
||||
{
|
||||
public string Role { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2: New file — Shared/Models/ChatRequest.cs (~6 lines)
|
||||
|
||||
```csharp
|
||||
namespace __YOUR_NAMESPACE__.Shared.Models
|
||||
{
|
||||
public class ChatRequest
|
||||
{
|
||||
public List<ChatMessage> Messages { get; set; } = new();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: New file — Shared/Models/ExtractedFields.cs (~12 lines)
|
||||
|
||||
```csharp
|
||||
// ADAPT: Replace these fields with your domain's extraction schema
|
||||
namespace __YOUR_NAMESPACE__.Shared.Models
|
||||
{
|
||||
public class ExtractedFields
|
||||
{
|
||||
public string? Client { get; set; }
|
||||
public string? Project { get; set; }
|
||||
public decimal? Hours { get; set; }
|
||||
public decimal? Rate { get; set; }
|
||||
public string? Currency { get; set; }
|
||||
public string? Date { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? PoNumber { get; set; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: New file — Shared/Models/ValidationResult.cs (~5 lines)
|
||||
|
||||
```csharp
|
||||
namespace __YOUR_NAMESPACE__.Shared.Models
|
||||
{
|
||||
public class ValidationResult
|
||||
{
|
||||
public bool IsValid { get; set; }
|
||||
public List<string> Errors { get; set; } = new();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: New file — Plugins/ExtractionPlugin.cs (~35 lines)
|
||||
|
||||
```csharp
|
||||
// ADAPT: Change RequiredFields and validation logic to match your ExtractedFields
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using __YOUR_NAMESPACE__.Shared.Models;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace __YOUR_NAMESPACE__.Api.Plugins
|
||||
{
|
||||
public class ExtractionPlugin
|
||||
{
|
||||
private static readonly string[] RequiredFields =
|
||||
{ "Client", "Project", "Hours", "Rate", "Currency", "Date" };
|
||||
|
||||
[KernelFunction("validate_extracted_fields")]
|
||||
[Description("Validates extracted key-value fields against the required schema. " +
|
||||
"Call this after extracting fields from natural language text to check " +
|
||||
"that all required fields (Client, Project, Hours, Rate, Currency, Date) " +
|
||||
"are present and correctly typed. Returns validation result with any errors.")]
|
||||
public string ValidateExtractedFields(
|
||||
[Description("JSON string of extracted fields")] string fieldsJson)
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
ExtractedFields? fields;
|
||||
try
|
||||
{
|
||||
fields = JsonSerializer.Deserialize<ExtractedFields>(fieldsJson,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
result.IsValid = false;
|
||||
result.Errors.Add($"Invalid JSON: {ex.Message}");
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
|
||||
if (fields == null)
|
||||
{
|
||||
result.IsValid = false;
|
||||
result.Errors.Add("Deserialized fields object is null");
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fields.Client))
|
||||
result.Errors.Add("Missing required field: Client");
|
||||
if (string.IsNullOrWhiteSpace(fields.Project))
|
||||
result.Errors.Add("Missing required field: Project");
|
||||
if (fields.Hours == null || fields.Hours <= 0)
|
||||
result.Errors.Add("Missing or invalid required field: Hours (must be positive)");
|
||||
if (fields.Rate == null || fields.Rate <= 0)
|
||||
result.Errors.Add("Missing or invalid required field: Rate (must be positive)");
|
||||
if (string.IsNullOrWhiteSpace(fields.Currency))
|
||||
result.Errors.Add("Missing required field: Currency");
|
||||
if (string.IsNullOrWhiteSpace(fields.Date))
|
||||
result.Errors.Add("Missing required field: Date");
|
||||
|
||||
result.IsValid = result.Errors.Count == 0;
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 6: New file — Controllers/ChatController.cs (~40 lines)
|
||||
|
||||
```csharp
|
||||
using System.Text.Json;
|
||||
using __YOUR_NAMESPACE__.Api.Plugins;
|
||||
using __YOUR_NAMESPACE__.Shared.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace __YOUR_NAMESPACE__.Api.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ChatController : ControllerBase
|
||||
{
|
||||
private readonly Kernel _kernel;
|
||||
|
||||
public ChatController(Kernel kernel) { _kernel = kernel; }
|
||||
|
||||
[HttpPost]
|
||||
public async Task Post([FromBody] ChatRequest request)
|
||||
{
|
||||
Response.ContentType = "text/event-stream";
|
||||
Response.Headers["Cache-Control"] = "no-cache";
|
||||
try
|
||||
{
|
||||
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
|
||||
var chatHistory = new ChatHistory();
|
||||
foreach (var msg in request.Messages)
|
||||
{
|
||||
if (msg.Role == "user") chatHistory.AddUserMessage(msg.Content);
|
||||
else if (msg.Role == "assistant") chatHistory.AddAssistantMessage(msg.Content);
|
||||
}
|
||||
|
||||
var plugin = HttpContext.RequestServices.GetRequiredService<ExtractionPlugin>();
|
||||
_kernel.ImportPluginFromObject(plugin, "Extraction");
|
||||
|
||||
var settings = new OpenAIPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
|
||||
};
|
||||
|
||||
await foreach (var chunk in chatService.GetStreamingChatMessageContentsAsync(
|
||||
chatHistory, settings, _kernel, HttpContext.RequestAborted))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(chunk.Content))
|
||||
{
|
||||
await WriteSSEAsync($"{{\"text\":{JsonSerializer.Serialize(chunk.Content)}}}");
|
||||
await Response.Body.FlushAsync();
|
||||
}
|
||||
}
|
||||
await WriteSSEAsync("[DONE]");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
await WriteSSEAsync($"{{\"error\":{JsonSerializer.Serialize($"Failed to reach LLM service: {ex.Message}")}}}");
|
||||
await WriteSSEAsync("[DONE]");
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
}
|
||||
|
||||
private async Task WriteSSEAsync(string data)
|
||||
{
|
||||
await Response.WriteAsync($"data: {data}\n\n");
|
||||
await Response.Body.FlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 7: Add to Program.cs (~12 lines)
|
||||
|
||||
Add `using Microsoft.SemanticKernel;` at the top.
|
||||
|
||||
Insert after `builder.Services.AddControllers();`:
|
||||
```csharp
|
||||
// ADAPT: Change BaseUrl and Model for your proxy/LLM
|
||||
var baseUrl = builder.Configuration["ResponsesApi:BaseUrl"] ?? "http://localhost:8317/v1";
|
||||
var model = builder.Configuration["ResponsesApi:Model"] ?? "claude-sonnet-4-6";
|
||||
|
||||
builder.Services.AddOpenAIChatCompletion(
|
||||
modelId: model,
|
||||
endpoint: new Uri(baseUrl),
|
||||
apiKey: builder.Configuration["ResponsesApi:ApiKey"] ?? "not-needed");
|
||||
builder.Services.AddKernel();
|
||||
builder.Services.AddSingleton<__YOUR_NAMESPACE__.Api.Plugins.ExtractionPlugin>();
|
||||
```
|
||||
|
||||
## Step 8: Add to appsettings.json (~4 lines)
|
||||
|
||||
Add this section:
|
||||
```json
|
||||
"ResponsesApi": {
|
||||
"BaseUrl": "http://localhost:8317/v1",
|
||||
"Model": "claude-sonnet-4-6"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Base URL must include `/v1`** — the OpenAI SDK appends `chat/completions` directly, so without `/v1` you get a 404
|
||||
- **`using Microsoft.SemanticKernel;`** is required in Program.cs for the `AddOpenAIChatCompletion` extension method to resolve
|
||||
- **Plugin is imported per-request** via `_kernel.ImportPluginFromObject` — do not register it in the kernel at startup
|
||||
- **CORS**: If your Blazor client is on a different port, add CORS policy in Program.cs before `app.Run()`
|
||||
@@ -1,63 +0,0 @@
|
||||
# Feature: SK Chat Endpoint with Tool Calling
|
||||
## Target: ApplicationX (ASP.NET Core + Blazor WASM + MudBlazor)
|
||||
|
||||
**Included**: wire-responses-api (superseded — SK version used), migrate-to-semantic-kernel
|
||||
**Lines to type**: ~40 | **Code equivalent**: ~120 lines | **Compression**: 3x
|
||||
|
||||
## Packages
|
||||
- Microsoft.SemanticKernel 1.74.0
|
||||
- Microsoft.SemanticKernel.Connectors.OpenAI 1.74.0
|
||||
|
||||
## Architecture
|
||||
POST /api/chat accepts conversation messages, runs them through Semantic
|
||||
Kernel's chat completion with auto tool calling, streams response as SSE.
|
||||
An ExtractionPlugin lets the LLM validate structured data it extracts
|
||||
from natural language, retrying autonomously before escalating to the user.
|
||||
|
||||
## Components
|
||||
|
||||
### ChatController: Controllers/ChatController.cs
|
||||
- [ApiController] POST endpoint, injects Kernel via DI
|
||||
- Converts List<ChatMessage> to SK ChatHistory (user/assistant roles)
|
||||
- Imports ExtractionPlugin per-request via _kernel.ImportPluginFromObject
|
||||
- Uses OpenAIPromptExecutionSettings with FunctionChoiceBehavior.Auto()
|
||||
- Streams via GetStreamingChatMessageContentsAsync, skips empty chunks
|
||||
- SSE output: `data: {"text":"..."}\n\n` per chunk, `data: [DONE]\n\n` at end
|
||||
- Error: `data: {"error":"..."}\n\n` then [DONE]
|
||||
- Catches TaskCanceledException silently (client disconnect)
|
||||
|
||||
### ExtractionPlugin: Plugins/ExtractionPlugin.cs
|
||||
- [KernelFunction("validate_extracted_fields")]
|
||||
- [Description] tells LLM: validates extracted fields against required schema
|
||||
- Accepts string fieldsJson, deserializes to ExtractedFields
|
||||
- Checks required fields non-null/non-empty, decimals > 0
|
||||
- Returns JSON: {"IsValid": bool, "Errors": ["..."]}
|
||||
|
||||
### ExtractedFields: Shared/Models/ExtractedFields.cs
|
||||
- Required: Client(string?), Project(string?), Hours(decimal?), Rate(decimal?), Currency(string?), Date(string?)
|
||||
- Optional: Description(string?), PoNumber(string?)
|
||||
|
||||
### ValidationResult: Shared/Models/ValidationResult.cs
|
||||
- IsValid(bool), Errors(List<string>)
|
||||
|
||||
### ChatRequest + ChatMessage: Shared/Models/
|
||||
- ChatRequest: Messages(List<ChatMessage>)
|
||||
- ChatMessage: Role(string), Content(string), Timestamp(DateTime)
|
||||
|
||||
## Wiring (Program.cs, after AddControllers)
|
||||
1. `using Microsoft.SemanticKernel;` at top (required for extension methods)
|
||||
2. Read BaseUrl and Model from config section "ResponsesApi"
|
||||
3. AddOpenAIChatCompletion(modelId, endpoint: new Uri(baseUrl), apiKey)
|
||||
4. AddKernel()
|
||||
5. AddSingleton<ExtractionPlugin>()
|
||||
6. CORS policy if Blazor client on different port
|
||||
|
||||
## Config (appsettings.json)
|
||||
- ResponsesApi:BaseUrl = "http://localhost:8317/v1"
|
||||
- ResponsesApi:Model = "claude-sonnet-4-6"
|
||||
|
||||
## Gotchas
|
||||
- Base URL MUST include /v1 — OpenAI SDK appends chat/completions directly
|
||||
- Plugin imported per-request, not at startup (avoids kernel state leaks)
|
||||
- SK has built-in auto-invoke limit — no need to set max retries
|
||||
- JsonSerializerOptions needs PropertyNameCaseInsensitive = true for deserialization
|
||||
@@ -1,70 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
Define the autonomous agent-driven extraction pipeline — structured field extraction from natural language, schema-based validation via tool calling, autonomous retry logic, and human-in-the-loop clarification.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Structured field extraction from natural language
|
||||
|
||||
The agent SHALL extract a predefined set of key-value pairs from user-provided natural language text (e.g., email content) and return them as a structured JSON object.
|
||||
|
||||
#### Scenario: All fields extracted successfully
|
||||
|
||||
- **WHEN** the user sends a message containing natural language with all required information
|
||||
- **THEN** the agent returns a JSON object with all predefined fields populated from the text
|
||||
|
||||
#### Scenario: Partial extraction
|
||||
|
||||
- **WHEN** the user sends a message that contains some but not all required fields
|
||||
- **THEN** the agent extracts available fields and leaves missing fields as null
|
||||
|
||||
### Requirement: Predefined extraction schema
|
||||
|
||||
The system SHALL define a fixed set of known field names and types as a strongly-typed C# class. All extraction output MUST conform to this schema.
|
||||
|
||||
#### Scenario: Output conforms to schema
|
||||
|
||||
- **WHEN** the agent produces extracted fields
|
||||
- **THEN** every key in the output matches a field defined in the schema and values match expected types
|
||||
|
||||
### Requirement: Autonomous validation via tool calling
|
||||
|
||||
The agent SHALL validate extracted fields by calling a validation tool function. The validation tool checks that all required fields are present and correctly typed.
|
||||
|
||||
#### Scenario: Validation passes
|
||||
|
||||
- **WHEN** the agent calls the validation tool with a complete and correct extraction
|
||||
- **THEN** the tool returns a success result and the agent returns the final output to the user
|
||||
|
||||
#### Scenario: Validation fails with fixable errors
|
||||
|
||||
- **WHEN** the validation tool returns errors for missing or malformed fields
|
||||
- **THEN** the agent re-reads the source text and attempts to fix the extraction without user intervention
|
||||
|
||||
### Requirement: Autonomous retry with iteration cap
|
||||
|
||||
The agent SHALL retry extraction autonomously up to 3 times when validation fails. After exhausting retries, the agent MUST escalate to the user.
|
||||
|
||||
#### Scenario: Agent retries and succeeds
|
||||
|
||||
- **WHEN** validation fails on the first attempt but the error is recoverable
|
||||
- **THEN** the agent retries extraction and calls validation again, up to 3 total attempts
|
||||
|
||||
#### Scenario: Agent exhausts retries and escalates
|
||||
|
||||
- **WHEN** validation fails after 3 attempts
|
||||
- **THEN** the agent sends a natural language message to the user identifying the specific fields it could not resolve and asking for clarification
|
||||
|
||||
### Requirement: Human-in-the-loop clarification
|
||||
|
||||
When the agent escalates to the user, the user SHALL be able to provide the missing information in natural language, and the agent SHALL incorporate the clarification and re-attempt extraction.
|
||||
|
||||
#### Scenario: User provides clarification
|
||||
|
||||
- **WHEN** the agent asks for clarification about missing fields and the user responds
|
||||
- **THEN** the agent incorporates the user's response into the conversation context and produces an updated extraction
|
||||
|
||||
#### Scenario: Clarification via normal chat
|
||||
|
||||
- **WHEN** the agent escalates for clarification
|
||||
- **THEN** the clarification request appears as a regular assistant message in the chat UI, and the user responds via the normal chat input
|
||||
@@ -1,55 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
Define the streaming AI response pipeline — backend chat endpoint using Semantic Kernel, SSE delivery to the WASM client, configuration, and error handling.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Chat endpoint proxies to Responses API
|
||||
|
||||
The API backend SHALL expose `POST /api/chat` that accepts a list of messages and processes them using a Semantic Kernel chat completion service. The kernel is configured with an OpenAI connector pointed at the existing CLIProxyAPI proxy.
|
||||
|
||||
#### Scenario: Successful chat request
|
||||
|
||||
- **WHEN** the client sends a POST to `/api/chat` with a message list
|
||||
- **THEN** the API processes the messages through the Semantic Kernel and returns the response
|
||||
|
||||
### Requirement: Streaming response delivery
|
||||
|
||||
The API backend SHALL stream the Semantic Kernel's chat completion response back to the WASM client as `text/event-stream`, forwarding text content so the client can render tokens incrementally. The SSE event format MUST remain `data: {"text":"..."}\n\n` for text deltas and `data: [DONE]\n\n` for completion.
|
||||
|
||||
#### Scenario: Tokens stream to client
|
||||
|
||||
- **WHEN** the Semantic Kernel emits streaming chat message content
|
||||
- **THEN** the backend forwards each content chunk as an SSE event to the client containing the text fragment
|
||||
|
||||
#### Scenario: Stream completes
|
||||
|
||||
- **WHEN** the Semantic Kernel streaming response completes
|
||||
- **THEN** the backend signals stream completion to the client with `data: [DONE]\n\n`
|
||||
|
||||
### Requirement: Configurable proxy target
|
||||
|
||||
The CLIProxyAPI base URL and model name SHALL be configurable via `appsettings.json` in the API project, not hardcoded. These values are used to configure the Semantic Kernel OpenAI connector.
|
||||
|
||||
#### Scenario: Configuration read at startup
|
||||
|
||||
- **WHEN** the API starts
|
||||
- **THEN** it reads `ResponsesApi:BaseUrl` and `ResponsesApi:Model` from configuration to configure the Semantic Kernel
|
||||
|
||||
### Requirement: Client streams from backend
|
||||
|
||||
The WASM client SHALL call `POST /api/chat` with `SetBrowserResponseStreamingEnabled(true)` and `HttpCompletionOption.ResponseHeadersRead`, then iterate the SSE stream to update the UI token by token.
|
||||
|
||||
#### Scenario: Client reads streaming response
|
||||
|
||||
- **WHEN** the client sends a chat request
|
||||
- **THEN** it reads the response stream incrementally and appends each text delta to the assistant message in real time
|
||||
|
||||
### Requirement: Error propagation
|
||||
|
||||
If the LLM service returns an error or is unreachable, the API backend SHALL return an error SSE event and the client SHALL display the error to the user.
|
||||
|
||||
#### Scenario: LLM service unreachable
|
||||
|
||||
- **WHEN** the CLIProxyAPI proxy is not running
|
||||
- **THEN** the client displays an error message instead of an assistant response
|
||||
@@ -1,108 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
Define the chat interface — message display, input handling, auto-scroll, and routing.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Message display
|
||||
|
||||
The chat page SHALL display messages in a vertically scrolling list, with each message showing the sender role (user or assistant), the message content, and a visual distinction between user and assistant messages (e.g., alignment, color, or avatar). Assistant messages SHALL render content as formatted HTML converted from markdown; user messages SHALL display as plain text.
|
||||
|
||||
#### Scenario: User message displayed
|
||||
|
||||
- **WHEN** the user sends a message
|
||||
- **THEN** the message appears in the message list aligned or styled to indicate it is from the user, rendered as plain text
|
||||
|
||||
#### Scenario: Assistant message displayed
|
||||
|
||||
- **WHEN** the assistant responds
|
||||
- **THEN** the response appears in the message list with distinct styling from user messages, with markdown content rendered as formatted HTML
|
||||
|
||||
#### Scenario: Message ordering
|
||||
|
||||
- **WHEN** multiple messages exist in the conversation
|
||||
- **THEN** messages are displayed in chronological order, oldest at top
|
||||
|
||||
### Requirement: Message input
|
||||
|
||||
The chat page SHALL provide a text input area at the bottom of the page where the user can type and submit messages.
|
||||
|
||||
#### Scenario: Submit via button
|
||||
|
||||
- **WHEN** the user types text and clicks the send button
|
||||
- **THEN** the message is added to the conversation and the input is cleared
|
||||
|
||||
#### Scenario: Submit via Enter key
|
||||
|
||||
- **WHEN** the user types text and presses Enter
|
||||
- **THEN** the message is submitted (same as clicking send)
|
||||
|
||||
#### Scenario: Empty input blocked
|
||||
|
||||
- **WHEN** the user attempts to send an empty or whitespace-only message
|
||||
- **THEN** nothing is sent and no message is added
|
||||
|
||||
#### Scenario: Input disabled during streaming
|
||||
|
||||
- **WHEN** the assistant is currently streaming a response
|
||||
- **THEN** the input field and send button are disabled until streaming completes
|
||||
|
||||
### Requirement: Thinking indicator
|
||||
|
||||
The chat page SHALL show a visual indicator while waiting for the first token from the assistant.
|
||||
|
||||
#### Scenario: Indicator shown during wait
|
||||
|
||||
- **WHEN** the user sends a message and the assistant has not yet started streaming
|
||||
- **THEN** a thinking indicator (e.g., animated dots) is shown in the assistant message area
|
||||
|
||||
#### Scenario: Indicator replaced by content
|
||||
|
||||
- **WHEN** the first token arrives from the stream
|
||||
- **THEN** the thinking indicator is replaced by the streamed text
|
||||
|
||||
### Requirement: Streaming AI response
|
||||
|
||||
The assistant SHALL reply with a real AI response streamed from the backend API, using the full conversation history as context. Tokens appear incrementally as they arrive.
|
||||
|
||||
#### Scenario: Bot replies with streamed AI response
|
||||
|
||||
- **WHEN** the user sends any message
|
||||
- **THEN** the assistant message appears and grows token by token as the stream delivers text
|
||||
|
||||
#### Scenario: Full history sent with each request
|
||||
|
||||
- **WHEN** the user sends a message after prior exchanges
|
||||
- **THEN** all previous user and assistant messages are included in the API request so the AI has conversational context
|
||||
|
||||
### Requirement: New chat button
|
||||
|
||||
The chat page SHALL provide a button to clear the current conversation and start a new one.
|
||||
|
||||
#### Scenario: User starts a new chat
|
||||
|
||||
- **WHEN** the user clicks the "New Chat" button
|
||||
- **THEN** all messages are cleared and the empty state is shown
|
||||
|
||||
#### Scenario: New chat button disabled during streaming
|
||||
|
||||
- **WHEN** the assistant is currently streaming a response
|
||||
- **THEN** the "New Chat" button is disabled
|
||||
|
||||
### Requirement: Auto-scroll
|
||||
|
||||
The message list SHALL automatically scroll to the newest message when a new message is added.
|
||||
|
||||
#### Scenario: New message scrolls into view
|
||||
|
||||
- **WHEN** a new message (user or assistant) is added to the conversation
|
||||
- **THEN** the message list scrolls to the bottom so the new message is visible
|
||||
|
||||
### Requirement: Chat page is default route
|
||||
|
||||
The chat page SHALL be the default route (`/`) of the application.
|
||||
|
||||
#### Scenario: App opens to chat
|
||||
|
||||
- **WHEN** the user navigates to the root URL
|
||||
- **THEN** the chat page is displayed
|
||||
@@ -1,50 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
Define MudBlazor installation, theming, and provider configuration for the Client project.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: MudBlazor package installed
|
||||
|
||||
The Client project SHALL have MudBlazor 9.2.0 installed as a NuGet dependency.
|
||||
|
||||
#### Scenario: Package reference present
|
||||
|
||||
- **WHEN** the Client project is built
|
||||
- **THEN** MudBlazor 9.2.0 is resolved as a dependency
|
||||
|
||||
### Requirement: MudBlazor services registered
|
||||
|
||||
MudBlazor services SHALL be registered in the Client's DI container via `AddMudServices()`.
|
||||
|
||||
#### Scenario: Services available
|
||||
|
||||
- **WHEN** the application starts
|
||||
- **THEN** MudBlazor services (snackbar, dialog, etc.) are available for injection
|
||||
|
||||
### Requirement: MudBlazor assets loaded
|
||||
|
||||
The Client's `index.html` SHALL include MudBlazor CSS, JS, and font references.
|
||||
|
||||
#### Scenario: Styles and scripts present
|
||||
|
||||
- **WHEN** the application loads in the browser
|
||||
- **THEN** MudBlazor CSS (`_content/MudBlazor/MudBlazor.min.css`), JS (`_content/MudBlazor/MudBlazor.min.js`), and Material Design Icons font are loaded
|
||||
|
||||
### Requirement: MudBlazor layout providers
|
||||
|
||||
The app root SHALL include `MudThemeProvider`, `MudPopoverProvider`, and `MudDialogProvider` so MudBlazor components function correctly.
|
||||
|
||||
#### Scenario: Providers present
|
||||
|
||||
- **WHEN** any MudBlazor component is rendered
|
||||
- **THEN** it functions correctly because the required providers are in the component tree
|
||||
|
||||
### Requirement: MudBlazor layout replaces Bootstrap
|
||||
|
||||
The application layout SHALL use MudBlazor layout components (`MudLayout`, `MudAppBar`, `MudMainContent`) instead of the current Bootstrap navbar.
|
||||
|
||||
#### Scenario: Layout renders with MudBlazor
|
||||
|
||||
- **WHEN** any page is displayed
|
||||
- **THEN** the page is wrapped in a MudBlazor layout with an app bar showing the application name
|
||||
@@ -1,27 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
Define the project identity, core value, and non-negotiable constraints for Chat Agent WebApp.
|
||||
## Requirements
|
||||
### Requirement: Project identity
|
||||
|
||||
The project spec SHALL contain the project name, a description paragraph, and a core value statement that communicates the project's dual purpose: working AI chat interface and Blazor learning journey.
|
||||
|
||||
#### Scenario: Project description present
|
||||
|
||||
- **WHEN** an AI agent or developer reads the project spec
|
||||
- **THEN** they find the project name ("Chat Agent WebApp"), a description of what the app does, and the core value statement
|
||||
|
||||
### Requirement: Project constraints
|
||||
|
||||
The project spec SHALL enumerate all non-negotiable constraints that govern technical decisions across the project.
|
||||
|
||||
#### Scenario: Constraints enumerated
|
||||
|
||||
- **WHEN** a decision is made about technology, architecture, or approach
|
||||
- **THEN** the project spec provides the authoritative list of constraints to check against:
|
||||
- Tech stack: .NET / C# / Blazor WebAssembly
|
||||
- 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
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
Define rich text rendering for assistant messages — markdown-to-HTML conversion, sanitization, styling, and streaming compatibility.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Markdown rendering for assistant messages
|
||||
|
||||
The system SHALL convert assistant message content from markdown to formatted HTML using Markdig, displaying headings, bold, italic, code blocks, lists, tables, links, and blockquotes with proper visual formatting.
|
||||
|
||||
#### Scenario: Markdown bold and italic rendered
|
||||
|
||||
- **WHEN** an assistant message contains `**bold**` or `*italic*` text
|
||||
- **THEN** the text is displayed with bold or italic formatting respectively
|
||||
|
||||
#### Scenario: Code block rendered
|
||||
|
||||
- **WHEN** an assistant message contains a fenced code block (triple backticks)
|
||||
- **THEN** the code is displayed in a monospace font within a visually distinct block
|
||||
|
||||
#### Scenario: Inline code rendered
|
||||
|
||||
- **WHEN** an assistant message contains inline code (single backticks)
|
||||
- **THEN** the code is displayed in a monospace font with a subtle background
|
||||
|
||||
#### Scenario: List rendered
|
||||
|
||||
- **WHEN** an assistant message contains a markdown list (ordered or unordered)
|
||||
- **THEN** the list is displayed with proper indentation and bullet/number markers
|
||||
|
||||
#### Scenario: Heading rendered
|
||||
|
||||
- **WHEN** an assistant message contains markdown headings (# through ######)
|
||||
- **THEN** the headings are displayed with appropriate size and weight
|
||||
|
||||
#### Scenario: Link rendered
|
||||
|
||||
- **WHEN** an assistant message contains a markdown link `[text](url)`
|
||||
- **THEN** the link is displayed as a clickable hyperlink opening in a new tab
|
||||
|
||||
#### Scenario: Table rendered
|
||||
|
||||
- **WHEN** an assistant message contains a markdown table
|
||||
- **THEN** the table is displayed with borders, header row styling, and proper alignment
|
||||
|
||||
### Requirement: HTML sanitization
|
||||
|
||||
The system SHALL sanitize all HTML output from the markdown renderer to prevent cross-site scripting (XSS) attacks from LLM-generated content.
|
||||
|
||||
#### Scenario: Script tags stripped
|
||||
|
||||
- **WHEN** assistant message content contains `<script>` tags
|
||||
- **THEN** the script tags and their content are removed from the rendered output
|
||||
|
||||
#### Scenario: Event handlers stripped
|
||||
|
||||
- **WHEN** assistant message content contains HTML attributes like `onclick` or `onerror`
|
||||
- **THEN** the attributes are removed from the rendered output
|
||||
|
||||
#### Scenario: Safe tags preserved
|
||||
|
||||
- **WHEN** assistant message content contains allowed structural HTML (p, strong, em, code, pre, ul, ol, li, a, table elements, blockquote, br, h1-h6)
|
||||
- **THEN** the tags are preserved in the rendered output
|
||||
|
||||
### Requirement: Markdown styling within chat bubbles
|
||||
|
||||
The system SHALL style rendered markdown elements to be visually consistent with the MudBlazor chat bubble theme.
|
||||
|
||||
#### Scenario: Code block styled in bubble
|
||||
|
||||
- **WHEN** a code block is rendered inside an assistant chat bubble
|
||||
- **THEN** it has a distinct background color, padding, border-radius, and does not overflow the bubble width (horizontal scroll if needed)
|
||||
|
||||
#### Scenario: Paragraph spacing in bubble
|
||||
|
||||
- **WHEN** multiple paragraphs are rendered inside an assistant chat bubble
|
||||
- **THEN** paragraphs have appropriate spacing without excessive gaps
|
||||
|
||||
### Requirement: Streaming compatibility
|
||||
|
||||
The system SHALL re-render markdown as new tokens arrive during streaming without visual glitches.
|
||||
|
||||
#### Scenario: Partial markdown rendered during streaming
|
||||
|
||||
- **WHEN** tokens are arriving and the current content contains incomplete markdown (e.g., a code block not yet closed)
|
||||
- **THEN** the content is rendered with best-effort formatting and updates smoothly as more tokens complete the markdown structure
|
||||
|
||||
### Requirement: User messages remain plain text
|
||||
|
||||
The system SHALL continue to render user messages as plain text without markdown processing.
|
||||
|
||||
#### Scenario: User message not processed as markdown
|
||||
|
||||
- **WHEN** a user message contains markdown syntax like `**bold**`
|
||||
- **THEN** the raw text `**bold**` is displayed, not formatted bold text
|
||||
@@ -1,46 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
Define the Semantic Kernel integration layer — kernel registration, OpenAI connector configuration, plugin registration, and automatic function calling.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Semantic Kernel service registration
|
||||
|
||||
The API backend SHALL register a Semantic Kernel `Kernel` instance in the ASP.NET Core DI container at startup, configured with an OpenAI chat completion connector.
|
||||
|
||||
#### Scenario: Kernel registered at startup
|
||||
|
||||
- **WHEN** the API application starts
|
||||
- **THEN** a `Kernel` instance is available for injection into controllers
|
||||
|
||||
### Requirement: OpenAI connector targets CLIProxyAPI proxy
|
||||
|
||||
The Semantic Kernel OpenAI chat completion service SHALL be configured to use the existing CLIProxyAPI proxy endpoint as its base URL, reading the URL and model name from `appsettings.json`.
|
||||
|
||||
#### Scenario: Connector uses configured endpoint
|
||||
|
||||
- **WHEN** the kernel makes a chat completion request
|
||||
- **THEN** it sends the request to the URL specified in `ResponsesApi:BaseUrl` configuration
|
||||
|
||||
#### Scenario: Model from configuration
|
||||
|
||||
- **WHEN** the kernel makes a chat completion request
|
||||
- **THEN** it uses the model name specified in `ResponsesApi:Model` configuration
|
||||
|
||||
### Requirement: Plugin registration
|
||||
|
||||
The API backend SHALL register extraction and validation plugins with the Kernel so they are available as tools for the LLM to invoke.
|
||||
|
||||
#### Scenario: Plugins available as tools
|
||||
|
||||
- **WHEN** the kernel is constructed
|
||||
- **THEN** all registered plugin functions appear in the tool list sent to the LLM
|
||||
|
||||
### Requirement: Auto function calling
|
||||
|
||||
The Kernel SHALL be configured with automatic function calling enabled, allowing the LLM to invoke registered plugin functions without manual dispatch code.
|
||||
|
||||
#### Scenario: LLM invokes tool automatically
|
||||
|
||||
- **WHEN** the LLM decides to call a registered function during chat completion
|
||||
- **THEN** the kernel automatically executes the function and returns the result to the LLM
|
||||
@@ -1,54 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
Document the technology stack decisions for Chat Agent WebApp.
|
||||
## Requirements
|
||||
### Requirement: Core technology stack
|
||||
|
||||
The stack spec SHALL document the recommended core technologies with version, purpose, and rationale for each.
|
||||
|
||||
#### Scenario: Core stack documented
|
||||
|
||||
- **WHEN** a developer needs to add or update a dependency
|
||||
- **THEN** the stack spec provides the authoritative record of: .NET 9 SDK, Blazor WebAssembly Standalone, ASP.NET Core Web API, C# 13, OpenAI SDK 2.9.1, Markdig 1.1.1, MudBlazor 9.2.0, and System.Text.Json
|
||||
|
||||
### Requirement: Supporting libraries and tools
|
||||
|
||||
The stack spec SHALL document supporting libraries, development tools, and installation notes.
|
||||
|
||||
#### Scenario: Supporting libraries referenced
|
||||
|
||||
- **WHEN** a developer evaluates adding a new dependency
|
||||
- **THEN** the stack spec lists supporting libraries with guidance on when to use them (e.g., Microsoft.Extensions.AI — skip for v1)
|
||||
|
||||
### Requirement: Alternatives and exclusions
|
||||
|
||||
The stack spec SHALL document considered alternatives and explicitly excluded technologies with rationale.
|
||||
|
||||
#### Scenario: Alternative considered
|
||||
|
||||
- **WHEN** a developer proposes an alternative package or approach
|
||||
- **THEN** the stack spec provides a record of alternatives already evaluated and why the current choice was made
|
||||
|
||||
#### Scenario: Excluded technology referenced
|
||||
|
||||
- **WHEN** a developer considers using a technology on the exclusion list
|
||||
- **THEN** the stack spec explains why it was excluded and what to use instead
|
||||
|
||||
### Requirement: Stack patterns
|
||||
|
||||
The stack spec SHALL document implementation patterns that govern how stack technologies are used together (streaming, storage, markdown rendering).
|
||||
|
||||
#### Scenario: Pattern referenced during implementation
|
||||
|
||||
- **WHEN** a developer implements streaming, storage, or markdown rendering
|
||||
- **THEN** the stack spec provides the canonical pattern to follow
|
||||
|
||||
### Requirement: Version compatibility matrix
|
||||
|
||||
The stack spec SHALL maintain a compatibility matrix and list of authoritative sources for version decisions.
|
||||
|
||||
#### Scenario: Compatibility check
|
||||
|
||||
- **WHEN** a package version is being upgraded
|
||||
- **THEN** the stack spec provides the compatibility matrix to verify cross-package compatibility
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
Define test project setup, framework choices, and required test coverage for the ChatAgent solution.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: API test project exists
|
||||
|
||||
An xUnit test project SHALL exist at `tests/ChatAgent.Api.Tests/` targeting the API project, with xUnit, Moq, and Microsoft.AspNetCore.Mvc.Testing as dependencies.
|
||||
|
||||
#### Scenario: API tests run
|
||||
|
||||
- **WHEN** `dotnet test` is run from the solution root
|
||||
- **THEN** API tests are discovered and executed
|
||||
|
||||
### Requirement: Client test project exists
|
||||
|
||||
An xUnit test project SHALL exist at `tests/ChatAgent.Client.Tests/` targeting the Client services, with xUnit and Moq as dependencies.
|
||||
|
||||
#### Scenario: Client tests run
|
||||
|
||||
- **WHEN** `dotnet test` is run from the solution root
|
||||
- **THEN** Client service tests are discovered and executed
|
||||
|
||||
### Requirement: HealthController test coverage
|
||||
|
||||
Tests SHALL verify that GET /api/health returns HTTP 200 with a valid HealthResponse containing a non-empty Status and a recent Timestamp.
|
||||
|
||||
#### Scenario: Health endpoint returns 200
|
||||
|
||||
- **WHEN** a GET request is sent to /api/health
|
||||
- **THEN** the response status is 200 and the body contains `status: "healthy"` and a UTC timestamp
|
||||
|
||||
### Requirement: ChatController test coverage
|
||||
|
||||
Tests SHALL verify that POST /api/chat returns a streaming SSE response containing text deltas and a [DONE] terminator. Tests SHALL mock the upstream Responses API.
|
||||
|
||||
#### Scenario: Chat streams text deltas
|
||||
|
||||
- **WHEN** a POST is sent to /api/chat with a valid ChatRequest
|
||||
- **THEN** the response is `text/event-stream` containing `data: {"text":"..."}` events followed by `data: [DONE]`
|
||||
|
||||
#### Scenario: Chat handles upstream error
|
||||
|
||||
- **WHEN** the Responses API is unreachable or returns an error
|
||||
- **THEN** the response contains a `data: {"error":"..."}` event followed by `data: [DONE]`
|
||||
|
||||
### Requirement: ChatApiClient test coverage
|
||||
|
||||
Tests SHALL verify that SendChatStreamingAsync correctly parses SSE events from the backend into text deltas, handles [DONE], and throws on error events.
|
||||
|
||||
#### Scenario: Client parses text deltas
|
||||
|
||||
- **WHEN** the backend returns SSE events with text deltas
|
||||
- **THEN** SendChatStreamingAsync yields each text fragment in order
|
||||
|
||||
#### Scenario: Client handles error event
|
||||
|
||||
- **WHEN** the backend returns an error SSE event
|
||||
- **THEN** SendChatStreamingAsync throws HttpRequestException with the error message
|
||||
@@ -10,9 +10,4 @@
|
||||
<ProjectReference Include="..\ChatAgent.Shared\ChatAgent.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SemanticKernel" Version="1.74.0" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.74.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
// ChatController.cs -- Handles chat requests using Semantic Kernel for AI completion.
|
||||
//
|
||||
// This controller receives messages from the WASM client, processes them through
|
||||
// Semantic Kernel's chat completion service (pointed at a local CLIProxyAPI proxy),
|
||||
// and streams the response tokens back as Server-Sent Events (SSE).
|
||||
//
|
||||
// Key concepts demonstrated:
|
||||
// - Semantic Kernel injection and usage in an ASP.NET Core controller
|
||||
// - IChatCompletionService for streaming chat completions
|
||||
// - OpenAIPromptExecutionSettings for configuring tool calling behavior
|
||||
// - FunctionChoiceBehavior.Auto() for automatic tool invocation
|
||||
// - Streaming SK responses as SSE to maintain the existing client contract
|
||||
|
||||
using System.Text.Json;
|
||||
using ChatAgent.Api.Plugins;
|
||||
using ChatAgent.Shared.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace ChatAgent.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes chat requests through Semantic Kernel and streams tokens back to the client.
|
||||
/// The Kernel is configured in Program.cs with an OpenAI connector pointed at CLIProxyAPI.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ChatController : ControllerBase
|
||||
{
|
||||
// Kernel is the central Semantic Kernel object. It holds the AI service
|
||||
// (chat completion) and any registered plugins (tools). We inject it via DI
|
||||
// rather than creating it manually, following ASP.NET Core conventions.
|
||||
private readonly Kernel _kernel;
|
||||
|
||||
public ChatController(Kernel kernel)
|
||||
{
|
||||
_kernel = kernel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POST /api/chat -- Accepts a ChatRequest with messages, processes them through
|
||||
/// Semantic Kernel's chat completion with tool calling enabled, and streams
|
||||
/// text tokens back as SSE events.
|
||||
///
|
||||
/// Client SSE format (unchanged from before migration):
|
||||
/// data: {"text":"token here"}\n\n -- for each text delta
|
||||
/// data: [DONE]\n\n -- when streaming completes
|
||||
/// data: {"error":"message"}\n\n -- if an error occurs
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task Post([FromBody] ChatRequest request)
|
||||
{
|
||||
// Set the response content type to SSE so the client knows to read it as a stream.
|
||||
Response.ContentType = "text/event-stream";
|
||||
Response.Headers["Cache-Control"] = "no-cache";
|
||||
|
||||
try
|
||||
{
|
||||
// IChatCompletionService is the SK abstraction for chat-based AI models.
|
||||
// GetRequiredService<T>() retrieves it from the kernel's service collection.
|
||||
// This is the service registered via AddOpenAIChatCompletion() in Program.cs.
|
||||
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
|
||||
|
||||
// ChatHistory is SK's representation of a conversation. It maps directly
|
||||
// to the messages array in OpenAI's API format. We convert our ChatMessage
|
||||
// DTOs into SK's format.
|
||||
var chatHistory = new ChatHistory();
|
||||
foreach (var msg in request.Messages)
|
||||
{
|
||||
if (msg.Role == "user")
|
||||
chatHistory.AddUserMessage(msg.Content);
|
||||
else if (msg.Role == "assistant")
|
||||
chatHistory.AddAssistantMessage(msg.Content);
|
||||
}
|
||||
|
||||
// Import the ExtractionPlugin so its [KernelFunction] methods are available
|
||||
// as tools for this request. We import from the DI-registered instance.
|
||||
// This makes validate_extracted_fields() visible to the LLM.
|
||||
var extractionPlugin = HttpContext.RequestServices.GetRequiredService<ExtractionPlugin>();
|
||||
_kernel.ImportPluginFromObject(extractionPlugin, "Extraction");
|
||||
|
||||
// OpenAIPromptExecutionSettings controls how the LLM processes the request.
|
||||
//
|
||||
// FunctionChoiceBehavior.Auto() enables automatic function calling:
|
||||
// - The LLM sees all registered plugin functions as available tools
|
||||
// - When the LLM decides to call a tool, SK automatically executes it
|
||||
// - The tool result is fed back to the LLM so it can reason about it
|
||||
// - This creates the agentic loop: extract → validate → fix → retry
|
||||
//
|
||||
// The Auto() behavior allows the LLM to make tool call round-trips
|
||||
// autonomously. SK's built-in safeguard limits the number of auto-invoke
|
||||
// attempts to prevent runaway loops. If the agent exhausts retries,
|
||||
// it responds with a clarification request to the user.
|
||||
var executionSettings = new OpenAIPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
|
||||
};
|
||||
|
||||
// GetStreamingChatMessageContentsAsync returns an IAsyncEnumerable that yields
|
||||
// content chunks as they arrive from the LLM. Each chunk may contain:
|
||||
// - Text content (the actual response tokens)
|
||||
// - Tool call requests (which SK handles automatically via auto-invoke)
|
||||
//
|
||||
// We iterate the stream and forward text chunks as SSE events,
|
||||
// preserving the exact format the Blazor client expects.
|
||||
await foreach (var chunk in chatService.GetStreamingChatMessageContentsAsync(
|
||||
chatHistory,
|
||||
executionSettings,
|
||||
_kernel,
|
||||
HttpContext.RequestAborted))
|
||||
{
|
||||
// Only emit chunks that contain text content.
|
||||
// Tool call chunks are handled internally by SK and don't produce
|
||||
// visible output -- the LLM will emit text after processing tool results.
|
||||
if (!string.IsNullOrEmpty(chunk.Content))
|
||||
{
|
||||
await WriteSSEAsync($"{{\"text\":{JsonSerializer.Serialize(chunk.Content)}}}");
|
||||
await Response.Body.FlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// Signal stream completion to the client
|
||||
await WriteSSEAsync("[DONE]");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
await WriteSSEAsync($"{{\"error\":{JsonSerializer.Serialize($"Failed to reach LLM service: {ex.Message}")}}}");
|
||||
await WriteSSEAsync("[DONE]");
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Client disconnected — nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a single SSE event to the response stream.
|
||||
/// SSE format: "data: {payload}\n\n"
|
||||
/// </summary>
|
||||
private async Task WriteSSEAsync(string data)
|
||||
{
|
||||
await Response.WriteAsync($"data: {data}\n\n");
|
||||
await Response.Body.FlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// ExtractionPlugin.cs -- Semantic Kernel plugin for validating extracted fields.
|
||||
//
|
||||
// In Semantic Kernel, a "plugin" is a class whose methods are exposed to the LLM
|
||||
// as callable tools (functions). The LLM can decide to invoke these functions during
|
||||
// a conversation when it determines they are relevant to the task.
|
||||
//
|
||||
// Key SK concepts demonstrated here:
|
||||
//
|
||||
// [KernelFunction] -- Marks a method as a function the LLM can call. SK discovers
|
||||
// these at startup and includes them in the tool list sent with each LLM request.
|
||||
//
|
||||
// [Description] -- Tells the LLM what the function does. The LLM reads this text
|
||||
// to decide whether and when to call the function. Good descriptions are critical
|
||||
// for reliable tool use.
|
||||
//
|
||||
// Auto-invocation -- When configured with FunctionChoiceBehavior.Auto(), SK
|
||||
// automatically executes tool calls the LLM makes and feeds the results back,
|
||||
// allowing the LLM to reason about the output and decide next steps (retry, fix,
|
||||
// or respond to the user). This creates the agentic loop.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using ChatAgent.Shared.Models;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace ChatAgent.Api.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugin that validates extracted key-value fields against the predefined schema.
|
||||
/// The LLM calls this after extracting fields from natural language to check
|
||||
/// whether all required fields are present and correctly typed.
|
||||
/// </summary>
|
||||
public class ExtractionPlugin
|
||||
{
|
||||
// The required fields that must be non-null and non-empty for validation to pass.
|
||||
// These match the required properties on ExtractedFields.
|
||||
private static readonly string[] RequiredFields =
|
||||
{ "Client", "Project", "Hours", "Rate", "Currency", "Date" };
|
||||
|
||||
/// <summary>
|
||||
/// Validates extracted fields against the predefined schema.
|
||||
/// Returns a JSON object indicating whether the extraction is valid
|
||||
/// and listing any errors found.
|
||||
/// </summary>
|
||||
/// <param name="fieldsJson">
|
||||
/// JSON string representing the extracted fields. Expected shape:
|
||||
/// { "Client": "...", "Project": "...", "Hours": 3, ... }
|
||||
/// </param>
|
||||
/// <returns>JSON string with { "IsValid": bool, "Errors": [...] }</returns>
|
||||
[KernelFunction("validate_extracted_fields")]
|
||||
[Description("Validates extracted key-value fields against the required schema. " +
|
||||
"Call this after extracting fields from natural language text to check " +
|
||||
"that all required fields (Client, Project, Hours, Rate, Currency, Date) " +
|
||||
"are present and correctly typed. Returns validation result with any errors.")]
|
||||
public string ValidateExtractedFields(
|
||||
[Description("JSON string of extracted fields")] string fieldsJson)
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
|
||||
ExtractedFields? fields;
|
||||
try
|
||||
{
|
||||
fields = JsonSerializer.Deserialize<ExtractedFields>(fieldsJson,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
result.IsValid = false;
|
||||
result.Errors.Add($"Invalid JSON: {ex.Message}");
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
|
||||
if (fields == null)
|
||||
{
|
||||
result.IsValid = false;
|
||||
result.Errors.Add("Deserialized fields object is null");
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
|
||||
// Check each required field for presence and non-empty value
|
||||
if (string.IsNullOrWhiteSpace(fields.Client))
|
||||
result.Errors.Add("Missing required field: Client");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fields.Project))
|
||||
result.Errors.Add("Missing required field: Project");
|
||||
|
||||
if (fields.Hours == null || fields.Hours <= 0)
|
||||
result.Errors.Add("Missing or invalid required field: Hours (must be a positive number)");
|
||||
|
||||
if (fields.Rate == null || fields.Rate <= 0)
|
||||
result.Errors.Add("Missing or invalid required field: Rate (must be a positive number)");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fields.Currency))
|
||||
result.Errors.Add("Missing required field: Currency");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fields.Date))
|
||||
result.Errors.Add("Missing required field: Date");
|
||||
|
||||
result.IsValid = result.Errors.Count == 0;
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
// Program.cs -- ASP.NET Core Web API entry point for ChatAgent.
|
||||
//
|
||||
// These using directives bring in the Semantic Kernel extension methods for DI registration.
|
||||
// Without them, the AddOpenAIChatCompletion() and AddKernel() methods won't be found.
|
||||
using Microsoft.SemanticKernel;
|
||||
//
|
||||
// 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.
|
||||
@@ -20,44 +16,6 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
// for explicit structure -- each controller is a separate file with clear routing (D-05).
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// --- Semantic Kernel Setup ---
|
||||
//
|
||||
// Semantic Kernel (SK) is an AI orchestration framework from Microsoft. It provides:
|
||||
// - Chat completion connectors (OpenAI, Azure OpenAI, etc.)
|
||||
// - Plugin system for exposing C# methods as tools the LLM can call
|
||||
// - Automatic function calling (the LLM decides when to invoke tools)
|
||||
// - Streaming support for token-by-token delivery
|
||||
//
|
||||
// The "Kernel" is the central object: it holds the AI service, plugins, and configuration.
|
||||
// We register it in DI so controllers can inject it.
|
||||
|
||||
// Read the CLIProxyAPI proxy URL and model from appsettings.json.
|
||||
// The OpenAI connector works with any OpenAI-compatible API endpoint,
|
||||
// so we point it at our local CLIProxyAPI proxy rather than OpenAI directly.
|
||||
// IMPORTANT: The base URL must include "/v1" because the OpenAI SDK appends
|
||||
// "chat/completions" directly to the base URL. Without "/v1", requests would
|
||||
// hit "/chat/completions" instead of "/v1/chat/completions" and get a 404.
|
||||
var responsesApiBaseUrl = builder.Configuration["ResponsesApi:BaseUrl"] ?? "http://localhost:8317/v1";
|
||||
var model = builder.Configuration["ResponsesApi:Model"] ?? "claude-sonnet-4-6";
|
||||
|
||||
// AddOpenAIChatCompletion registers an IChatCompletionService in DI.
|
||||
// The "endpoint" parameter lets us target any OpenAI-compatible API (here: CLIProxyAPI).
|
||||
// The "apiKey" is required by the connector but CLIProxyAPI may not check it,
|
||||
// so we use a placeholder. In production, this would be a real API key.
|
||||
builder.Services.AddOpenAIChatCompletion(
|
||||
modelId: model,
|
||||
endpoint: new Uri(responsesApiBaseUrl),
|
||||
apiKey: builder.Configuration["ResponsesApi:ApiKey"] ?? "not-needed");
|
||||
|
||||
// AddKernel() registers the Kernel class itself in DI. It automatically picks up
|
||||
// any AI services (like the chat completion above) that are already registered.
|
||||
builder.Services.AddKernel();
|
||||
|
||||
// Register the ExtractionPlugin so the Kernel can expose its [KernelFunction] methods
|
||||
// as tools. When the LLM sees these tools, it can decide to call them during a conversation
|
||||
// to validate extracted data. The plugin is registered as a singleton via DI.
|
||||
builder.Services.AddSingleton<ChatAgent.Api.Plugins.ExtractionPlugin>();
|
||||
|
||||
// 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).
|
||||
@@ -99,8 +57,3 @@ app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
|
||||
// This partial class declaration makes the auto-generated Program class public,
|
||||
// which is required by WebApplicationFactory<Program> in integration tests.
|
||||
// Without this, the test project cannot reference the entry point type.
|
||||
public partial class Program { }
|
||||
|
||||
@@ -5,9 +5,5 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ResponsesApi": {
|
||||
"BaseUrl": "http://localhost:8317/v1",
|
||||
"Model": "claude-sonnet-4-6"
|
||||
}
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
@@ -10,12 +10,11 @@
|
||||
<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" />
|
||||
<PackageReference Include="Markdig" Version="1.1.1" />
|
||||
<PackageReference Include="MudBlazor" Version="9.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ChatAgent.Shared\ChatAgent.Shared.csproj" />
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ChatAgent.Shared\ChatAgent.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
91
src/ChatAgent.Client/Components/ChatInput.razor
Normal file
91
src/ChatAgent.Client/Components/ChatInput.razor
Normal file
@@ -0,0 +1,91 @@
|
||||
@*
|
||||
ChatInput.razor -- The message input bar at the bottom of the chat.
|
||||
|
||||
KEY BLAZOR CONCEPTS:
|
||||
- @bind-Value: Two-way data binding. When the user types, _messageText updates.
|
||||
When _messageText changes in code, the input reflects it. This is Blazor's
|
||||
equivalent of React's controlled components or Angular's [(ngModel)].
|
||||
- EventCallback: A typed delegate for child-to-parent communication. When the
|
||||
user clicks Send, this component invokes OnMessageSent, and the parent handles it.
|
||||
EventCallback is the Blazor pattern for "events flow up" (opposite of parameters
|
||||
which flow data down).
|
||||
- @onkeydown: Blazor's way to handle DOM events. The @ prefix binds a C# method
|
||||
to a JavaScript event. Blazor intercepts it via JS interop and calls your C# code.
|
||||
*@
|
||||
|
||||
@* max-width and margin:auto center the input bar to match the message list width. *@
|
||||
<div class="pa-3" style="border-top: 1px solid var(--mud-palette-lines-default); background: var(--mud-palette-background);">
|
||||
<div style="max-width: 768px; width: 100%; margin: 0 auto;">
|
||||
@* MudStack arranges children in a row. Spacing="2" adds gap between items.
|
||||
AlignItems centers them vertically within the row. *@
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
@* MudTextField is a Material Design text input.
|
||||
@bind-Value creates two-way binding to _messageText.
|
||||
Immediate="true" means the binding updates on every keystroke
|
||||
(default is on blur/focus-loss). We need this so Enter key handling
|
||||
always sees the latest typed text.
|
||||
Variant.Outlined draws a bordered input (vs Filled or Text). *@
|
||||
<MudTextField @bind-Value="_messageText"
|
||||
Placeholder="Type a message..."
|
||||
Variant="Variant.Outlined"
|
||||
Immediate="true"
|
||||
@onkeydown="HandleKeyDown"
|
||||
FullWidth="true"
|
||||
Disabled="@_isSending" />
|
||||
@* MudIconButton renders a Material icon as a clickable button.
|
||||
Icons.Material.Filled.Send is a built-in MudBlazor icon constant.
|
||||
Disabled prevents double-sends while processing. *@
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Send"
|
||||
Color="Color.Primary"
|
||||
OnClick="SendMessage"
|
||||
Disabled="@(string.IsNullOrWhiteSpace(_messageText) || _isSending)" />
|
||||
</MudStack>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
// Private field bound to the text input via @bind-Value.
|
||||
// The underscore prefix is a C# convention for private fields.
|
||||
private string _messageText = string.Empty;
|
||||
private bool _isSending;
|
||||
|
||||
/// <summary>
|
||||
/// EventCallback for notifying the parent when the user sends a message.
|
||||
/// The parent provides a handler method:
|
||||
/// <ChatInput OnMessageSent="@HandleNewMessage" />
|
||||
/// EventCallback<string> means the event carries a string payload (the message text).
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<string> OnMessageSent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Send button click (or Enter key). Invokes the parent's callback
|
||||
/// with the message text, then clears the input.
|
||||
/// </summary>
|
||||
private async Task SendMessage()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_messageText) || _isSending)
|
||||
return;
|
||||
|
||||
_isSending = true;
|
||||
|
||||
// InvokeAsync triggers the parent's event handler and passes the message text.
|
||||
// The parent (Chat.razor) will call ChatService.SendMessageAsync with this text.
|
||||
await OnMessageSent.InvokeAsync(_messageText);
|
||||
|
||||
_messageText = string.Empty;
|
||||
_isSending = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the message when the user presses Enter (without Shift).
|
||||
/// Shift+Enter can be used for multi-line input in a future phase.
|
||||
/// </summary>
|
||||
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter" && !e.ShiftKey)
|
||||
{
|
||||
await SendMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/ChatAgent.Client/Components/ChatMessageList.razor
Normal file
77
src/ChatAgent.Client/Components/ChatMessageList.razor
Normal file
@@ -0,0 +1,77 @@
|
||||
@*
|
||||
ChatMessageList.razor -- Renders the scrollable list of chat messages.
|
||||
|
||||
This is a "presentational" component -- it receives data via a [Parameter]
|
||||
and renders it. It does not manage state or call services directly.
|
||||
|
||||
KEY BLAZOR CONCEPTS:
|
||||
- [Parameter]: A property decorated with [Parameter] receives its value from
|
||||
the parent component's markup (like an HTML attribute). This is how data
|
||||
flows downward in Blazor's component tree (parent -> child).
|
||||
- @foreach: Razor syntax for looping. The @ prefix switches from HTML to C#.
|
||||
- Conditional CSS classes: We use a ternary expression to pick alignment
|
||||
based on IsUser, so user messages appear on the right and bot messages on the left.
|
||||
*@
|
||||
|
||||
@using ChatAgent.Client.Models
|
||||
|
||||
@* flex:1 fills available space; overflow-y:auto enables scrolling when messages exceed
|
||||
the visible area. flex-direction:column-reverse anchors content to the bottom,
|
||||
so the newest messages are always visible (like ChatGPT). *@
|
||||
<div style="flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column-reverse;">
|
||||
@* We wrap the messages in an inner div because column-reverse reverses the visual order
|
||||
of direct children. By putting all messages in a single child div, we keep their
|
||||
natural top-to-bottom order while the outer container anchors to the bottom. *@
|
||||
<div style="max-width: 768px; width: 100%; margin: 0 auto;">
|
||||
@if (Messages is null || Messages.Count == 0)
|
||||
{
|
||||
@* MudText is a MudBlazor typography component. Typo.Body1 sets the text style.
|
||||
Using MudBlazor components instead of raw HTML gives consistent Material Design styling. *@
|
||||
<MudText Typo="Typo.body1" Align="Align.Center" Class="mt-4" Style="color: var(--mud-palette-text-secondary);">
|
||||
Send a message to get started.
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
@* d-flex = display:flex (MudBlazor utility class, similar to Bootstrap).
|
||||
justify-end/justify-start control horizontal alignment of the chat bubble. *@
|
||||
<div class="d-flex @(message.IsUser ? "justify-end" : "justify-start") mb-3">
|
||||
@* MudPaper is a Material Design surface (a "card" with elevation/shadow).
|
||||
Elevation="1" adds a subtle shadow. Class applies padding and max-width. *@
|
||||
<MudPaper Elevation="1" Class="pa-3" Style="@GetBubbleStyle(message.IsUser)">
|
||||
<MudText Typo="Typo.body1">@message.Text</MudText>
|
||||
<MudText Typo="Typo.caption" Style="opacity: 0.6; margin-top: 4px;">
|
||||
@message.Timestamp.ToString("h:mm tt")
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The list of messages to display. Passed in from the parent Chat page.
|
||||
/// [Parameter] tells Blazor this value comes from the parent's markup:
|
||||
/// <ChatMessageList Messages="@someList" />
|
||||
/// Blazor re-renders this component when the parameter value changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IReadOnlyList<ChatMessage>? Messages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Builds the inline style for a chat bubble. User messages get the primary color;
|
||||
/// bot messages get the surface color. This is in a method because Blazor component
|
||||
/// attributes do not support mixed C# and markup inline (RZ9986).
|
||||
/// </summary>
|
||||
private static string GetBubbleStyle(bool isUser)
|
||||
{
|
||||
var bg = isUser
|
||||
? "background-color: var(--mud-palette-primary); color: var(--mud-palette-primary-text);"
|
||||
: "background-color: var(--mud-palette-surface);";
|
||||
return $"max-width: 75%; border-radius: 16px; {bg}";
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,43 @@
|
||||
@* MainLayout.razor -- The root layout component using MudBlazor.
|
||||
@* MainLayout.razor -- The root layout component for the application.
|
||||
|
||||
MudBlazor requires three providers in the component tree for its components to work:
|
||||
- MudThemeProvider: Supplies the Material Design theme (colors, typography, spacing)
|
||||
- MudPopoverProvider: Manages popover/dropdown positioning (used by MudSelect, MudMenu, etc.)
|
||||
- MudDialogProvider: Enables the dialog service to render modal dialogs
|
||||
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.
|
||||
|
||||
The layout uses MudLayout + MudAppBar + MudMainContent to create a standard
|
||||
Material Design app shell. MudMainContent automatically accounts for the AppBar
|
||||
height so page content doesn't render underneath it.
|
||||
MudBlazor requires certain provider components to be placed at the layout level:
|
||||
- MudThemeProvider: Applies the Material Design theme (colors, typography, spacing)
|
||||
- MudPopoverProvider: Renders popovers/tooltips outside the normal DOM flow
|
||||
so they are not clipped by parent overflow styles
|
||||
- MudDialogProvider: Renders dialogs (modal windows) at the root level
|
||||
- MudSnackbarProvider: Renders toast notifications at the root level
|
||||
*@
|
||||
|
||||
@* @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
|
||||
|
||||
@* MudBlazor providers -- must be in the layout so they wrap all page content *@
|
||||
<MudThemeProvider />
|
||||
<MudPopoverProvider />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
|
||||
<MudLayout>
|
||||
@* MudAppBar provides the top application bar. Dense reduces its height.
|
||||
The fixed position keeps it visible while scrolling. *@
|
||||
<MudAppBar Elevation="1" Dense="true">
|
||||
<MudText Typo="Typo.h6">Chat Agent</MudText>
|
||||
@* MudAppBar is a Material Design top bar. Elevation adds shadow depth.
|
||||
Fixed="true" keeps it pinned at the top when the page scrolls. *@
|
||||
<MudAppBar Elevation="1" Fixed="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Chat" Class="mr-2" />
|
||||
<MudText Typo="Typo.h6">ChatAgent</MudText>
|
||||
</MudAppBar>
|
||||
|
||||
@* MudMainContent renders the routed page content (same role as @Body in plain Blazor).
|
||||
It automatically adds top padding to clear the AppBar. *@
|
||||
<MudMainContent>
|
||||
@* MudMainContent automatically adds top padding to account for the fixed app bar,
|
||||
so page content doesn't render behind it. *@
|
||||
<MudMainContent Style="height: 100vh; display: flex; flex-direction: column;">
|
||||
@* @Body is where the routed page content renders.
|
||||
When the user navigates to "/", the Chat.razor component's markup
|
||||
appears here. The layout stays the same -- only @Body changes. *@
|
||||
@Body
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
|
||||
77
src/ChatAgent.Client/Layout/MainLayout.razor.css
Normal file
77
src/ChatAgent.Client/Layout/MainLayout.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
39
src/ChatAgent.Client/Layout/NavMenu.razor
Normal file
39
src/ChatAgent.Client/Layout/NavMenu.razor
Normal 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;
|
||||
}
|
||||
}
|
||||
83
src/ChatAgent.Client/Layout/NavMenu.razor.css
Normal file
83
src/ChatAgent.Client/Layout/NavMenu.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
29
src/ChatAgent.Client/Models/ChatMessage.cs
Normal file
29
src/ChatAgent.Client/Models/ChatMessage.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// ChatMessage.cs -- Represents a single message in the chat conversation.
|
||||
//
|
||||
// This is a plain C# model (sometimes called a DTO -- Data Transfer Object).
|
||||
// It holds the data for one chat bubble: who sent it, what they said, and when.
|
||||
// Both the UI components and the ChatService reference this model.
|
||||
|
||||
namespace ChatAgent.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A single message in the chat conversation.
|
||||
/// </summary>
|
||||
public class ChatMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The display text of the message.
|
||||
/// </summary>
|
||||
public string Text { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// True if the message was sent by the user; false if it came from the bot.
|
||||
/// The UI uses this to style and align the bubble differently.
|
||||
/// </summary>
|
||||
public bool IsUser { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the message was created. Used for display ordering.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; } = DateTime.Now;
|
||||
}
|
||||
@@ -1,279 +1,56 @@
|
||||
@* Chat.razor -- The main chat interface with streaming AI responses.
|
||||
@*
|
||||
Chat.razor -- The main chat page that composes ChatMessageList and ChatInput.
|
||||
|
||||
This is the primary page of the application, mapped to the root route "/".
|
||||
It displays a vertically scrolling message list and a text input at the bottom,
|
||||
styled after ChatGPT/Gemini.
|
||||
|
||||
Key Blazor concepts demonstrated:
|
||||
- @page routing (this component owns "/")
|
||||
- Two-way binding with @bind-Value on MudTextField
|
||||
- Event handling with @onclick and OnKeyDown
|
||||
- List rendering with @foreach over a List<T>
|
||||
- StateHasChanged() for manual re-render triggers during streaming
|
||||
- IJSRuntime for calling JavaScript (auto-scroll)
|
||||
- IAsyncEnumerable consumption for streaming API responses
|
||||
- Conditional rendering for thinking indicator and error states
|
||||
KEY BLAZOR CONCEPTS:
|
||||
- @page "/": This directive registers the component as a routable page.
|
||||
When the browser navigates to "/", Blazor's Router (in App.razor) renders
|
||||
this component inside MainLayout's @Body. The URL path is how Blazor picks
|
||||
which page component to show.
|
||||
- @inject: Requests a service from the DI container. This is the Razor syntax
|
||||
equivalent of constructor injection in regular C# classes. The service must be
|
||||
registered in Program.cs first.
|
||||
- StateHasChanged(): Tells Blazor "my state changed, please re-render."
|
||||
Blazor calls this automatically after event handlers (like button clicks),
|
||||
but since we update state inside an awaited service call, we call it explicitly
|
||||
to ensure the UI reflects the new messages immediately.
|
||||
*@
|
||||
|
||||
@page "/"
|
||||
@using ChatAgent.Client.Components
|
||||
@using ChatAgent.Client.Models
|
||||
|
||||
@* IJSRuntime lets us call JavaScript from C#. We use it to scroll the message
|
||||
container to the bottom after adding a new message, because Blazor has no
|
||||
built-in scroll API. *@
|
||||
@inject IJSRuntime JS
|
||||
@* @inject pulls ChatService from the DI container registered in Program.cs.
|
||||
"ChatService" is the type; "ChatService" after it is the property name we use in code. *@
|
||||
@inject ChatService ChatService
|
||||
|
||||
@* ChatApiClient is our typed HttpClient wrapper that handles API communication.
|
||||
It was registered in Program.cs via AddHttpClient<ChatApiClient>. *@
|
||||
@inject ChatApiClient ApiClient
|
||||
@* PageTitle sets the browser tab title. It works via the HeadOutlet
|
||||
registered in Program.cs (which manages <head> elements from components). *@
|
||||
<PageTitle>Chat</PageTitle>
|
||||
|
||||
@* MarkdownService converts markdown to sanitized HTML for rendering assistant messages.
|
||||
We use it to transform the raw LLM output into formatted HTML before display. *@
|
||||
@using ChatAgent.Client.Services
|
||||
@inject MarkdownService Markdown
|
||||
@* flex:1 makes this container fill all remaining space below the app bar.
|
||||
The inner flex-column stacks the message list (scrollable) above the input (fixed). *@
|
||||
<div style="flex: 1; display: flex; flex-direction: column; min-height: 0;">
|
||||
@* ChatMessageList is our presentational component. We pass the message list as a parameter.
|
||||
The = sign in Messages="@..." assigns the value; the @ prefix evaluates the C# expression. *@
|
||||
<ChatMessageList Messages="@ChatService.Messages" />
|
||||
|
||||
<PageTitle>Chat Agent</PageTitle>
|
||||
|
||||
@* Chat container: uses flexbox to fill available height.
|
||||
The message area grows to fill space; the input stays pinned at the bottom. *@
|
||||
<div class="chat-container">
|
||||
|
||||
@* Message list: scrollable area that grows to fill available space. *@
|
||||
<div class="message-list" @ref="_messageListRef">
|
||||
@if (_messages.Count == 0)
|
||||
{
|
||||
@* Empty state shown before any messages are sent *@
|
||||
<div class="empty-state">
|
||||
<MudText Typo="Typo.h5" Align="Align.Center" Class="mb-2"
|
||||
Style="color: var(--mud-palette-text-secondary);">
|
||||
Chat Agent
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Align="Align.Center"
|
||||
Style="color: var(--mud-palette-text-disabled);">
|
||||
Type a message to get started
|
||||
</MudText>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* Render each message as a MudPaper card.
|
||||
The CSS class changes based on Role to align user messages right, assistant left. *@
|
||||
@foreach (var message in _messages)
|
||||
{
|
||||
<div class="message-row @(message.Role == "user" ? "message-user" : "message-assistant")">
|
||||
<MudPaper Class="@($"message-bubble {(message.Role == "user" ? "bubble-user" : "bubble-assistant")}")"
|
||||
Elevation="0">
|
||||
@if (message.Role == "assistant" && string.IsNullOrEmpty(message.Content) && _isStreaming)
|
||||
{
|
||||
@* Thinking indicator: shown while waiting for the first token.
|
||||
MudProgressCircular gives an animated spinner that disappears
|
||||
once the first text delta arrives. *@
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
|
||||
}
|
||||
else if (message.Role == "assistant")
|
||||
{
|
||||
@* Assistant messages are rendered as HTML converted from markdown.
|
||||
MarkupString tells Blazor to treat the string as raw HTML rather
|
||||
than escaping it. The MarkdownService sanitizes the HTML to prevent XSS.
|
||||
We use GetRenderedHtml() to cache completed messages and avoid
|
||||
re-running Markdig on every StateHasChanged during streaming. *@
|
||||
<div class="markdown-body">
|
||||
@((MarkupString)GetRenderedHtml(message))
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* User messages stay as plain text — no markdown processing. *@
|
||||
<MudText Typo="Typo.body1">@message.Content</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@* Input area: pinned at the bottom of the chat container.
|
||||
Disabled attribute prevents interaction while the assistant is streaming.
|
||||
The "New Chat" button clears the conversation to start fresh. *@
|
||||
<div class="input-area">
|
||||
@if (_messages.Count > 0)
|
||||
{
|
||||
<div class="input-actions">
|
||||
<MudButton Variant="Variant.Text"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="NewChat"
|
||||
Disabled="_isStreaming">
|
||||
New Chat
|
||||
</MudButton>
|
||||
</div>
|
||||
}
|
||||
<MudTextField @bind-Value="_userInput"
|
||||
Placeholder="@(_isStreaming ? "Waiting for response..." : "Type a message...")"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@Icons.Material.Filled.Send"
|
||||
AdornmentColor="@(_isStreaming ? Color.Default : Color.Primary)"
|
||||
OnAdornmentClick="SendMessage"
|
||||
OnKeyDown="HandleKeyDown"
|
||||
Immediate="true"
|
||||
FullWidth="true"
|
||||
AutoFocus="true"
|
||||
Disabled="_isStreaming" />
|
||||
</div>
|
||||
@* ChatInput fires OnMessageSent when the user clicks Send or presses Enter.
|
||||
We bind that event to our HandleNewMessage method. *@
|
||||
<ChatInput OnMessageSent="@HandleNewMessage" />
|
||||
</div>
|
||||
|
||||
@code {
|
||||
// The conversation messages, displayed in the message list.
|
||||
private List<ChatMessage> _messages = new();
|
||||
|
||||
// The current text in the input field. Bound two-way via @bind-Value.
|
||||
private string _userInput = string.Empty;
|
||||
|
||||
// DOM reference to the message list div, used for auto-scrolling via JS interop.
|
||||
private ElementReference _messageListRef;
|
||||
|
||||
// Tracks whether we are currently streaming a response from the API.
|
||||
// Used to disable input and show the thinking indicator.
|
||||
private bool _isStreaming = false;
|
||||
|
||||
// Cache of rendered HTML for completed assistant messages. Without this cache,
|
||||
// every StateHasChanged() during streaming would re-run Markdig on ALL messages,
|
||||
// causing noticeable lag as the conversation grows. Only the actively streaming
|
||||
// message is re-rendered; completed messages use their cached HTML.
|
||||
private readonly Dictionary<ChatMessage, string> _renderedHtmlCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Returns cached rendered HTML for completed messages, or renders fresh
|
||||
/// for the actively streaming message. This avoids re-running Markdig on
|
||||
/// every message during each StateHasChanged call.
|
||||
/// Called when ChatInput fires its OnMessageSent event.
|
||||
/// Delegates to ChatService, then triggers a re-render so the new messages appear.
|
||||
/// </summary>
|
||||
private string GetRenderedHtml(ChatMessage message)
|
||||
private async Task HandleNewMessage(string messageText)
|
||||
{
|
||||
if (_renderedHtmlCache.TryGetValue(message, out var cached))
|
||||
return cached;
|
||||
|
||||
// Not cached — this is either the streaming message or first render.
|
||||
// Render it fresh. It will be cached once streaming completes.
|
||||
return Markdown.ConvertToHtml(message.Content);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all messages to start a new conversation.
|
||||
/// </summary>
|
||||
private void NewChat()
|
||||
{
|
||||
_messages.Clear();
|
||||
_renderedHtmlCache.Clear();
|
||||
_userInput = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Enter key press to submit the message.
|
||||
/// </summary>
|
||||
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter" && !e.ShiftKey && !_isStreaming)
|
||||
{
|
||||
await SendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the user's message and streams the AI response token by token.
|
||||
/// Each token delta updates the assistant message and triggers a re-render.
|
||||
/// </summary>
|
||||
private async Task SendMessage()
|
||||
{
|
||||
// Block empty or whitespace-only submissions, and prevent double-send during streaming
|
||||
if (string.IsNullOrWhiteSpace(_userInput) || _isStreaming)
|
||||
return;
|
||||
|
||||
// Add the user's message
|
||||
_messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = _userInput.Trim(),
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
|
||||
var userText = _userInput.Trim();
|
||||
_userInput = string.Empty;
|
||||
_isStreaming = true;
|
||||
|
||||
// Add an empty assistant message that will be filled token by token.
|
||||
// The thinking indicator shows while Content is empty.
|
||||
var assistantMessage = new ChatMessage
|
||||
{
|
||||
Role = "assistant",
|
||||
Content = string.Empty,
|
||||
Timestamp = DateTime.UtcNow
|
||||
};
|
||||
_messages.Add(assistantMessage);
|
||||
await ChatService.SendMessageAsync(messageText);
|
||||
|
||||
// StateHasChanged() tells Blazor to re-render this component.
|
||||
// After re-render, the updated ChatService.Messages list flows down
|
||||
// to ChatMessageList via its [Parameter], updating the UI.
|
||||
StateHasChanged();
|
||||
await ScrollToBottom();
|
||||
|
||||
try
|
||||
{
|
||||
// Build the request with the full conversation history for multi-turn context.
|
||||
// We include all messages except the last one (the empty assistant placeholder
|
||||
// that is waiting to be filled by the stream). This gives the AI the full
|
||||
// conversation so it can reference prior exchanges.
|
||||
var request = new ChatRequest
|
||||
{
|
||||
Messages = _messages
|
||||
.Where(m => !string.IsNullOrEmpty(m.Content))
|
||||
.Select(m => new ChatMessage { Role = m.Role, Content = m.Content, Timestamp = m.Timestamp })
|
||||
.ToList()
|
||||
};
|
||||
|
||||
// Stream tokens from the API. IAsyncEnumerable yields each text delta
|
||||
// as it arrives, allowing us to update the UI incrementally.
|
||||
await foreach (var delta in ApiClient.SendChatStreamingAsync(request))
|
||||
{
|
||||
// Append each token to the assistant message content.
|
||||
assistantMessage.Content += delta;
|
||||
|
||||
// StateHasChanged() triggers a re-render so the user sees each token appear.
|
||||
// This is the core of the streaming UX — without it, the full response
|
||||
// would only appear after the stream completes.
|
||||
StateHasChanged();
|
||||
|
||||
// Auto-scroll during streaming so new content stays visible
|
||||
await ScrollToBottom();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If the API call fails, show the error in the assistant message.
|
||||
assistantMessage.Content = $"Error: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isStreaming = false;
|
||||
assistantMessage.Timestamp = DateTime.UtcNow;
|
||||
|
||||
// Cache the final rendered HTML so future re-renders don't re-run Markdig
|
||||
_renderedHtmlCache[assistantMessage] = Markdown.ConvertToHtml(assistantMessage.Content);
|
||||
|
||||
StateHasChanged();
|
||||
await ScrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scrolls the message list to the bottom using JavaScript interop.
|
||||
/// </summary>
|
||||
private async Task ScrollToBottom()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("eval",
|
||||
"document.querySelector('.message-list').scrollTop = document.querySelector('.message-list').scrollHeight");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore scroll errors — non-critical UI enhancement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
/* Chat.razor.css -- Scoped styles for the chat interface.
|
||||
*
|
||||
* Blazor CSS isolation: this file is automatically scoped to Chat.razor.
|
||||
* Styles here only apply to elements rendered by this component, preventing
|
||||
* conflicts with other pages. The build system adds a unique attribute
|
||||
* (e.g., b-abc123) to both the CSS selectors and the rendered HTML.
|
||||
*
|
||||
* ::deep is needed for styles that target child component markup (like MudPaper)
|
||||
* because those elements are rendered by MudBlazor, not directly by this component.
|
||||
*/
|
||||
|
||||
/* Chat container: flexbox column that fills the viewport below the AppBar.
|
||||
* The message-list grows to fill available space; input-area stays at the bottom. */
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 48px); /* 48px = MudAppBar Dense height */
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Scrollable message area */
|
||||
.message-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem 1rem 0.5rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Empty state centered in the message area */
|
||||
.empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Message row: controls horizontal alignment */
|
||||
.message-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.message-user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message-assistant {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Message bubbles — ::deep is required because MudPaper renders its own elements */
|
||||
::deep .message-bubble {
|
||||
max-width: 75%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 1rem;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
::deep .bubble-user {
|
||||
background-color: var(--mud-palette-primary);
|
||||
color: white;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
}
|
||||
|
||||
::deep .bubble-assistant {
|
||||
background-color: var(--mud-palette-surface);
|
||||
border: 1px solid var(--mud-palette-lines-default);
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Input area pinned at the bottom */
|
||||
.input-area {
|
||||
padding: 0.75rem 1rem 1rem 1rem;
|
||||
border-top: 1px solid var(--mud-palette-lines-default);
|
||||
background-color: var(--mud-palette-background);
|
||||
}
|
||||
|
||||
/* --- Markdown rendering styles for assistant messages ---
|
||||
* These target HTML elements produced by Markdig inside the .markdown-body wrapper.
|
||||
* ::deep is required because the rendered HTML is injected via MarkupString,
|
||||
* not generated by a Blazor component, but it still lives inside this component's scope. */
|
||||
|
||||
/* Paragraphs: tighten spacing inside bubbles */
|
||||
::deep .markdown-body p {
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Remove bottom margin on the last element to keep bubble tight */
|
||||
::deep .markdown-body > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Code blocks: distinct background with horizontal scroll for wide content */
|
||||
::deep .markdown-body pre {
|
||||
background-color: var(--mud-palette-background-gray);
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
overflow-x: auto;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
::deep .markdown-body pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: 0.85rem;
|
||||
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
/* Inline code: subtle background to distinguish from surrounding text */
|
||||
::deep .markdown-body code {
|
||||
background-color: var(--mud-palette-background-gray);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875em;
|
||||
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
/* Lists: proper indentation inside bubbles */
|
||||
::deep .markdown-body ul,
|
||||
::deep .markdown-body ol {
|
||||
margin: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
::deep .markdown-body li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Tables: borders and header styling */
|
||||
::deep .markdown-body table {
|
||||
border-collapse: collapse;
|
||||
margin: 0.5rem 0;
|
||||
width: 100%;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
::deep .markdown-body th,
|
||||
::deep .markdown-body td {
|
||||
border: 1px solid var(--mud-palette-lines-default);
|
||||
padding: 0.375rem 0.625rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
::deep .markdown-body th {
|
||||
background-color: var(--mud-palette-background-gray);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Blockquotes: left border accent */
|
||||
::deep .markdown-body blockquote {
|
||||
border-left: 3px solid var(--mud-palette-primary);
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.25rem 0 0.25rem 0.75rem;
|
||||
color: var(--mud-palette-text-secondary);
|
||||
}
|
||||
|
||||
/* Links: themed color */
|
||||
::deep .markdown-body a {
|
||||
color: var(--mud-palette-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Headings: scale down for bubble context */
|
||||
::deep .markdown-body h1 { font-size: 1.25rem; font-weight: 600; margin: 0.5rem 0 0.25rem 0; }
|
||||
::deep .markdown-body h2 { font-size: 1.125rem; font-weight: 600; margin: 0.5rem 0 0.25rem 0; }
|
||||
::deep .markdown-body h3 { font-size: 1rem; font-weight: 600; margin: 0.5rem 0 0.25rem 0; }
|
||||
::deep .markdown-body h4,
|
||||
::deep .markdown-body h5,
|
||||
::deep .markdown-body h6 { font-size: 0.875rem; font-weight: 600; margin: 0.5rem 0 0.25rem 0; }
|
||||
|
||||
/* Action row above the text input (New Chat button) */
|
||||
.input-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
18
src/ChatAgent.Client/Pages/Counter.razor
Normal file
18
src/ChatAgent.Client/Pages/Counter.razor
Normal 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++;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@* Home.razor -- The health check page for ChatAgent.
|
||||
@* Home.razor -- The landing page for ChatAgent.
|
||||
|
||||
@page "/health" maps this component to the /health URL. Chat.razor now owns "/".
|
||||
The Blazor router renders this component inside MainLayout's @Body placeholder.
|
||||
@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
|
||||
@@ -10,7 +10,7 @@
|
||||
*@
|
||||
|
||||
@* @page directive maps this component to a URL route.
|
||||
"/health" provides access to the health check page (Chat.razor now owns "/"). *@
|
||||
"/health" keeps the health check accessible while "/" is now the chat page. *@
|
||||
@page "/health"
|
||||
|
||||
@* Import the service and model namespaces for this component.
|
||||
|
||||
57
src/ChatAgent.Client/Pages/Weather.razor
Normal file
57
src/ChatAgent.Client/Pages/Weather.razor
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -39,14 +39,6 @@ var apiBaseUrl = isHttps
|
||||
? builder.Configuration["ApiBaseUrl_Https"] ?? "https://localhost:7100"
|
||||
: builder.Configuration["ApiBaseUrl_Http"] ?? "http://localhost:7000";
|
||||
|
||||
// AddMudServices registers MudBlazor's internal services (snackbar, dialog, popover, etc.)
|
||||
// into the DI container. This is required before any MudBlazor component will work.
|
||||
builder.Services.AddMudServices();
|
||||
|
||||
// MarkdownService converts markdown to sanitized HTML for rendering assistant messages.
|
||||
// Registered as singleton because the Markdig pipeline is immutable and thread-safe.
|
||||
builder.Services.AddSingleton<ChatAgent.Client.Services.MarkdownService>();
|
||||
|
||||
// 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).
|
||||
@@ -58,4 +50,13 @@ builder.Services.AddHttpClient<ChatApiClient>(client =>
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
|
||||
// AddMudServices registers MudBlazor's internal services (theme, popover, scroll, etc.)
|
||||
// into the DI container. Required for MudBlazor components to function.
|
||||
builder.Services.AddMudServices();
|
||||
|
||||
// Register ChatService as a Singleton. In Blazor WASM, Singleton means "one instance per
|
||||
// browser tab" -- there is no server-side shared state. This keeps the message list
|
||||
// alive across page navigations within the same tab session.
|
||||
builder.Services.AddSingleton<ChatService>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
@@ -11,10 +11,7 @@
|
||||
// The base URL is configured in Program.cs via AddHttpClient<ChatApiClient>.
|
||||
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using ChatAgent.Shared.Models;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Http;
|
||||
|
||||
namespace ChatAgent.Client.Services
|
||||
{
|
||||
@@ -52,94 +49,5 @@ namespace ChatAgent.Client.Services
|
||||
// configured in Program.cs (e.g., https://localhost:7100/api/health).
|
||||
return await _httpClient.GetFromJsonAsync<HealthResponse>("api/health");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a chat request to POST /api/chat and streams the response as an
|
||||
/// async enumerable of text deltas. Each yielded string is a token fragment.
|
||||
///
|
||||
/// Key Blazor WASM streaming concepts:
|
||||
/// - SetBrowserResponseStreamingEnabled(true) tells the browser's Fetch API
|
||||
/// to make the response body readable as a stream (not buffered).
|
||||
/// - HttpCompletionOption.ResponseHeadersRead means we start reading the
|
||||
/// stream as soon as HTTP headers arrive, not after the full body downloads.
|
||||
/// - We parse the SSE format line by line, extracting "text" from each data event.
|
||||
/// </summary>
|
||||
public async IAsyncEnumerable<string> SendChatStreamingAsync(ChatRequest request)
|
||||
{
|
||||
// Build the HTTP request manually so we can set streaming options.
|
||||
var jsonContent = JsonSerializer.Serialize(request);
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "api/chat")
|
||||
{
|
||||
Content = new StringContent(jsonContent, Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
// SetBrowserResponseStreamingEnabled is a Blazor WASM extension that tells
|
||||
// the browser's Fetch API to expose the response as a ReadableStream.
|
||||
// Without this, the browser buffers the entire response before .NET can read it.
|
||||
httpRequest.SetBrowserResponseStreamingEnabled(true);
|
||||
|
||||
// ResponseHeadersRead: start processing as soon as headers arrive.
|
||||
using var response = await _httpClient.SendAsync(
|
||||
httpRequest,
|
||||
HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
// Read the SSE stream line by line.
|
||||
using var stream = await response.Content.ReadAsStreamAsync();
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
// Use ReadLineAsync and check for null instead of reader.EndOfStream,
|
||||
// because EndOfStream performs a synchronous read which is not supported
|
||||
// in Blazor WASM's async streaming pipeline.
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null)
|
||||
{
|
||||
// SSE lines starting with "data: " contain our payload.
|
||||
if (!line.StartsWith("data: "))
|
||||
continue;
|
||||
|
||||
var data = line.Substring(6);
|
||||
|
||||
// "[DONE]" signals the end of the stream.
|
||||
if (data == "[DONE]")
|
||||
yield break;
|
||||
|
||||
// Parse the simplified JSON event: {"text":"token"} or {"error":"message"}
|
||||
// Note: C# does not allow yield inside try-catch, so we parse first
|
||||
// and yield outside the try block.
|
||||
string? parsedText = null;
|
||||
string? parsedError = null;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(data);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("error", out var errorElement))
|
||||
{
|
||||
parsedError = errorElement.GetString();
|
||||
}
|
||||
else if (root.TryGetProperty("text", out var textElement))
|
||||
{
|
||||
parsedText = textElement.GetString();
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Skip malformed SSE data
|
||||
}
|
||||
|
||||
if (parsedError != null)
|
||||
{
|
||||
throw new HttpRequestException($"Chat API error: {parsedError}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(parsedText))
|
||||
{
|
||||
yield return parsedText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
65
src/ChatAgent.Client/Services/ChatService.cs
Normal file
65
src/ChatAgent.Client/Services/ChatService.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
// ChatService.cs -- Client-side service that manages chat state and produces responses.
|
||||
//
|
||||
// WHY A SERVICE? In Blazor, services registered in DI live for the lifetime of the app
|
||||
// (when registered as Singleton in WASM -- there is only one "user" per browser tab).
|
||||
// This keeps chat state (the message list) separate from UI components, following the
|
||||
// "service extracts logic from components" pattern. Components inject this service
|
||||
// and call its methods, rather than holding all state themselves.
|
||||
//
|
||||
// CURRENT PHASE (Echo): SendMessageAsync always returns "success msg!".
|
||||
// FUTURE: This service will call ChatApiClient to reach the backend, which will
|
||||
// forward the message to an external AI service.
|
||||
|
||||
using ChatAgent.Client.Models;
|
||||
|
||||
namespace ChatAgent.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the chat conversation state and produces responses.
|
||||
/// Registered as a Singleton in Program.cs so all components share the same message list.
|
||||
/// In Blazor WASM, Singleton means "one instance per browser tab" (there is no shared server state).
|
||||
/// </summary>
|
||||
public class ChatService
|
||||
{
|
||||
// The conversation history. Components read this list to render messages.
|
||||
// Using a List<T> (not IReadOnlyList) for simplicity in this phase.
|
||||
private readonly List<ChatMessage> _messages = new();
|
||||
|
||||
/// <summary>
|
||||
/// The full conversation history, oldest first.
|
||||
/// Components bind to this to render the chat bubbles.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ChatMessage> Messages => _messages;
|
||||
|
||||
/// <summary>
|
||||
/// Adds the user's message to the conversation, generates a response, and adds that too.
|
||||
/// Returns the bot's response message.
|
||||
///
|
||||
/// The method is async (returns Task) even though the current echo implementation is synchronous.
|
||||
/// This is intentional -- when we later replace the echo with an HTTP call to the API,
|
||||
/// the method signature won't need to change, and all callers already await it.
|
||||
/// </summary>
|
||||
public Task<ChatMessage> SendMessageAsync(string userText)
|
||||
{
|
||||
// Add the user's message to the conversation
|
||||
var userMessage = new ChatMessage
|
||||
{
|
||||
Text = userText,
|
||||
IsUser = true,
|
||||
Timestamp = DateTime.Now
|
||||
};
|
||||
_messages.Add(userMessage);
|
||||
|
||||
// Echo phase: always respond with "success msg!"
|
||||
// Future: replace this with an HTTP call via ChatApiClient
|
||||
var botMessage = new ChatMessage
|
||||
{
|
||||
Text = "success msg!",
|
||||
IsUser = false,
|
||||
Timestamp = DateTime.Now
|
||||
};
|
||||
_messages.Add(botMessage);
|
||||
|
||||
return Task.FromResult(botMessage);
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
// MarkdownService.cs -- Converts markdown text to sanitized HTML for display.
|
||||
//
|
||||
// This service wraps Markdig (a .NET markdown processor) and adds HTML sanitization
|
||||
// to safely render LLM-generated content in the browser. LLM output is untrusted —
|
||||
// it could contain <script> tags or event handlers that would execute in the browser
|
||||
// if rendered as raw HTML. The sanitization step strips everything not on the allowlist.
|
||||
//
|
||||
// Key concepts demonstrated:
|
||||
// - Markdig pipeline configuration with extension methods
|
||||
// - HTML sanitization via tag/attribute allowlist (defense-in-depth for XSS)
|
||||
// - MarkupString in Blazor for rendering raw HTML safely
|
||||
// - Singleton service registration (the pipeline is thread-safe and reusable)
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using Markdig;
|
||||
|
||||
namespace ChatAgent.Client.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts markdown to sanitized HTML. Registered as a singleton because
|
||||
/// the Markdig pipeline is immutable and thread-safe after construction.
|
||||
/// </summary>
|
||||
public class MarkdownService
|
||||
{
|
||||
// The Markdig pipeline is configured once and reused for all conversions.
|
||||
// UseAdvancedExtensions() enables GFM tables, pipe tables, task lists,
|
||||
// auto-links, and other commonly used markdown extensions.
|
||||
private readonly MarkdownPipeline _pipeline;
|
||||
|
||||
// Tags allowed in the sanitized output. Anything not on this list is stripped.
|
||||
// This is the core XSS defense — even if Markdig produces unexpected HTML,
|
||||
// only these structural tags survive.
|
||||
private static readonly HashSet<string> AllowedTags = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"p", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"strong", "em", "code", "pre",
|
||||
"ul", "ol", "li",
|
||||
"a",
|
||||
"table", "thead", "tbody", "tr", "th", "td",
|
||||
"br", "blockquote"
|
||||
};
|
||||
|
||||
// Attributes allowed on specific tags. Only href on <a> tags is permitted.
|
||||
// All other attributes (including event handlers like onclick) are stripped.
|
||||
private static readonly Dictionary<string, HashSet<string>> AllowedAttributes =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "a", new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "href" } }
|
||||
};
|
||||
|
||||
// Regex to match script and style blocks including their content.
|
||||
// These are stripped entirely — both tags and inner content — because
|
||||
// they can execute code in the browser.
|
||||
private static readonly Regex ScriptStyleRegex = new(
|
||||
@"<(script|style)\b[^>]*>[\s\S]*?</\1>",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
// Regex to match HTML tags (opening, closing, and self-closing).
|
||||
// Used by the sanitizer to find and filter tags in the Markdig output.
|
||||
private static readonly Regex TagRegex = new(
|
||||
@"<(/?)(\w+)(\s[^>]*)?>",
|
||||
RegexOptions.Compiled | RegexOptions.Singleline);
|
||||
|
||||
// Regex to match individual HTML attributes within a tag.
|
||||
// Captures the attribute name so we can check it against the allowlist.
|
||||
private static readonly Regex AttrRegex = new(
|
||||
@"(\w+)\s*=\s*(?:""[^""]*""|'[^']*'|\S+)",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
public MarkdownService()
|
||||
{
|
||||
_pipeline = new MarkdownPipelineBuilder()
|
||||
.UseAdvancedExtensions()
|
||||
.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a markdown string to sanitized HTML.
|
||||
/// Safe to use with MarkupString in Blazor for rendering.
|
||||
/// </summary>
|
||||
public string ConvertToHtml(string markdown)
|
||||
{
|
||||
if (string.IsNullOrEmpty(markdown))
|
||||
return string.Empty;
|
||||
|
||||
var rawHtml = Markdown.ToHtml(markdown, _pipeline);
|
||||
return SanitizeHtml(rawHtml);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strips HTML tags and attributes not on the allowlist.
|
||||
/// This is a conservative sanitizer — it only keeps structural tags
|
||||
/// needed for markdown rendering and removes everything else.
|
||||
/// </summary>
|
||||
internal string SanitizeHtml(string html)
|
||||
{
|
||||
// First pass: remove script and style blocks entirely (tags + content)
|
||||
html = ScriptStyleRegex.Replace(html, string.Empty);
|
||||
|
||||
// Second pass: filter remaining tags against the allowlist
|
||||
return TagRegex.Replace(html, match =>
|
||||
{
|
||||
var isClosing = match.Groups[1].Value == "/";
|
||||
var tagName = match.Groups[2].Value;
|
||||
var attrs = match.Groups[3].Value;
|
||||
|
||||
// Strip tags not on the allowlist entirely
|
||||
if (!AllowedTags.Contains(tagName))
|
||||
return string.Empty;
|
||||
|
||||
if (isClosing)
|
||||
return $"</{tagName}>";
|
||||
|
||||
// Filter attributes: only keep those on the allowlist for this tag
|
||||
var sanitizedAttrs = string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(attrs) &&
|
||||
AllowedAttributes.TryGetValue(tagName, out var allowed))
|
||||
{
|
||||
var attrMatches = AttrRegex.Matches(attrs);
|
||||
foreach (Match attrMatch in attrMatches)
|
||||
{
|
||||
var attrName = attrMatch.Groups[1].Value;
|
||||
if (allowed.Contains(attrName))
|
||||
{
|
||||
sanitizedAttrs += " " + attrMatch.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $"<{tagName}{sanitizedAttrs}>";
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using MudBlazor
|
||||
@using ChatAgent.Client
|
||||
@using ChatAgent.Client.Layout
|
||||
@using ChatAgent.Client.Services
|
||||
@using ChatAgent.Shared.Models
|
||||
@using MudBlazor
|
||||
|
||||
@@ -1,11 +1,53 @@
|
||||
/* app.css -- Application-wide styles for ChatAgent.
|
||||
/* app.css -- Application styles for ChatAgent (Phase 1).
|
||||
*
|
||||
* MudBlazor handles most styling via its component library.
|
||||
* This file contains only:
|
||||
* - Blazor framework styles (error UI, loading progress) that must stay
|
||||
* - Global overrides if needed
|
||||
* 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 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user