Issue Detail Page
Overview
The Issue Detail page is the hub for viewing and managing issue lifecycle in Symphony. It displays a complete timeline of activity (comments, findings, learnings, agent runs), enables inline editing of issue metadata, manages PR review workflows (approve/reject/merge), and visualizes dependencies. The page polls for real-time updates and integrates findings/learnings collection with the orchestrator's phase execution pipeline.
How It Works
Data Fetching & Polling
The page loads four independent data streams when a project slug and issue identifier are provided:
Each fetch is conditional — comments only load after the issue loads, PR data only loads after the issue loads. The polling composable refreshes all four endpoints simultaneously.
Timeline Construction
Findings, learnings, comments, and agent runs are unified into a chronologically sorted timeline via buildTimeline():
const timeline = computed(() => buildTimeline(
comments.value ?? [],
issue.value?.findings ?? [],
issue.value?.learnings ?? [],
issue.value?.agentRuns ?? [],
))Each type is mapped to a TimelineItem:
- Comment →
{ type: 'comment', content, author, createdAt } - Finding →
{ type: 'finding', content: summary + details, meta: category, createdAt } - Learning →
{ type: 'learning', content: pattern + context, createdAt } - Agent Run →
{ type: 'run', content: "Attempt N: status", meta: duration, createdAt }
Items are sorted lexicographically by timestamp.
Inline Editing Pipeline
The useIssueEditor composable manages state for six edit modes:
| Field | Initial State | Save Behavior |
|---|---|---|
| Title | text input (focused) | PUT /issues/:issueId with trimmed title |
| Description | textarea (auto-resizing) | PUT /issues/:issueId with trimmed content or null |
| Priority | select dropdown (1-5) | PUT /issues/:issueId with numeric priority |
| Size | select dropdown (small/medium/large) | PUT /issues/:issueId with size or null |
| Labels | tag list + input | PUT /issues/:issueId with updated array |
| Comments | add comment form | POST /issues/:issueId/comments with author='user' |
All writes trigger an updateIssue() call that:
- Makes the API request
- Calls
refreshIssue()to sync state - Sets a success flash (2s) or error flash (4s)
Saves are atomic — no partial updates.
PR Workflow
When a PR exists for an issue, three user actions are available:
Approve & Merge:
- User clicks "Approve & Merge" → POST
/issues/:issueId/merge - Handler calls
mergeAndComplete()service - Service executes git merge in the worktree
- Issue status is updated, PR status → 'merged',
mergedAttimestamp set - Page refreshes all data streams (issue, PR, diff, comments)
Reject:
- User enters rejection comment → POST
/issues/:issueId/pr/reject - Handler finds latest open/approved PR
- Sets PR status → 'rejected'
- Moves issue status to 'todo' (allows rework)
- Adds system comment with rejection reason
- Page refreshes
Status Changes:
- User changes status in detail panel
- PUT
/issues/:issueId/statuswith new status - Page syncs via refresh
Findings & Learnings
Agents and users add findings/learnings to document discovery:
Adding a Finding (via MCP tool or manual):
- POST
/issues/:issueId/findingswith category, summary, details, files - Handler appends to
issue.findingsJSON array (immutable append) - Issue
updatedAtis refreshed - Timeline updates automatically on next poll
Adding a Learning (via MCP tool or manual):
- POST
/issues/:issueId/learningswith pattern, context, applies_to - Handler appends to
issue.learningsJSON array - Issue
updatedAtrefreshed - Timeline updates automatically on next poll
Both endpoints validate input via schemas and generate UUIDs with prefixes (f-* for findings, l-* for learnings).
Dependencies & Subtasks
The page displays two issue relationship types:
Dependencies (prerequisites):
- Fetched from
issueDependenciestable - Enriched with project info for cross-project badges
- Display as read-only links (user clicks to navigate)
- Fetched from
Children (subtasks):
- Issues with
parentIssueId = issue.id - Enriched with project info
- Display as expandable list
- Issues with
Both are fetched in the issue detail endpoint and displayed as dependency graph.
Phase Awareness
The page determines current phase label and displays phase-specific UI:
const currentPhase = computed(() => {
const label = (issue.value?.labels ?? []).find(l => isPhaseLabel(l))
return label ? extractPhase(label) : null
})If a phase is active AND artifacts exist:
- Display phase metrics (entered_at, rejection_count, last_verdict)
- Show artifact revision history
- Display required sections from phase contract
Key Components
| File | Responsibility |
|---|---|
app/pages/p/[slug]/issues/[identifier].vue | Main page component, data fetching, polling setup, tab switching (activity/diff), event handlers |
app/utils/issueTypes.ts | TypeScript definitions for IssueDetail, PrData, IssueComment, PrFileStats, PrGateResult, PrRiskScore, TimelineItem |
app/utils/buildTimeline.ts | Unifies comments, findings, learnings, agent runs into chronological timeline |
app/composables/useIssueEditor.ts | State management for 6 edit modes (title, description, priority, size, labels, comments), save/error/success flashes |
app/utils/phaseLabels.ts | Phase label utilities (isPhaseLabel, extractPhase) |
app/utils/formatTime.ts | Timestamp formatting utilities |
app/components/DiffViewer.vue | PR diff rendering with syntax highlighting |
app/components/IssueDetailSidebar.vue | Sidebar metadata panel (priority, size, status, labels) |
app/components/IssueCommentForm.vue | Comment submission form |
app/components/CommentThread.vue | Threaded comment display |
app/components/PhaseArtifactTrail.vue | Phase artifact revision history display |
app/components/IssueMergeActions.vue | Approve/Reject/Merge action buttons |
server/api/projects/[projectId]/issues/by-identifier/[identifier].get.ts | Issue detail endpoint, enriches with dependencies, children, agent runs |
server/api/projects/[projectId]/issues/[issueId]/comments.get.ts | Fetch all comments for issue |
server/api/projects/[projectId]/issues/[issueId]/comments.post.ts | Create new comment, validates author and content |
server/api/projects/[projectId]/issues/[issueId]/pr/index.get.ts | Fetch latest PR for issue, sorted by createdAt desc |
server/api/projects/[projectId]/issues/[issueId]/findings.post.ts | Create new finding, appends to issue.findings array |
server/api/projects/[projectId]/issues/[issueId]/learnings.post.ts | Create new learning, appends to issue.learnings array |
server/api/projects/[projectId]/issues/[issueId]/merge.post.ts | Merge PR, delegates to mergeAndComplete service |
server/api/projects/[projectId]/issues/[issueId]/pr/reject.post.ts | Reject PR, moves issue to 'todo', adds comment |
Design Decisions
1. Polling for Real-Time Updates
Decision: Use 5-second polling instead of WebSockets.
Rationale: SQLite with WAL mode provides eventual consistency across processes. 5s is fast enough for human collaboration without server complexity. Simpler to debug and deploy than maintaining WebSocket connections.
2. Immutable Findings/Learnings Append
Decision: Append to JSON arrays rather than maintaining separate tables.
Rationale: Findings and learnings are append-only audit logs tied to issue lifetime. Storing as JSON arrays avoids join complexity and simplifies phase artifact validation (which scans findings for completion signals). Each append is atomic with issue.updatedAt refresh.
3. Composite Timeline from Multiple Sources
Decision: Unify comments, findings, learnings, runs into a single sorted timeline.
Rationale: Agents and users need to see complete context of work in chronological order. Separate feeds would fragment the narrative. The buildTimeline function is lightweight and sorts on the client.
4. Inline Editing with Optimistic UI
Decision: Update state locally, show flash messages on success/error rather than blocking UI.
Rationale: Reduces friction in workflow. Errors are displayed for 4s, enough to correct a title or re-submit. Full data refresh ensures consistency with server state.
5. PR Status Progression: open → approved → merged/rejected
Decision: Use explicit status field rather than relying on merge commit presence.
Rationale: Status is queryable and sortable. Merge handler explicitly sets status and mergedAt timestamp. Rejection is reversible (user can push again, creating a new PR record).
6. Dependencies Fetched in Issue Detail Endpoint
Decision: Enrich dependencies with project info in the GET endpoint.
Rationale: Avoids N+1 queries on the client. Dependencies are displayed read-only, so denormalization is safe. Users can click to navigate to a dependency issue.
Known Gaps
- Comment threading: Currently flat list; no parent/child comment relationships are stored in the schema.
- Duplicate findings/learnings: No deduplication logic exists. Agents may submit identical findings on retry. Consider adding a content hash check.
- Async validation of phase artifacts: Artifact existence is checked at phase transition time, but not validated on the detail page. A "Ready to Complete?" indicator would help agents avoid failed submissions.