Skip to content

Add Agents/Runs surfaces and run-detail timeline in Agent mode#808

Merged
Chris0Jeky merged 13 commits intomainfrom
agt/agent-mode-surfaces-and-runs
Apr 12, 2026
Merged

Add Agents/Runs surfaces and run-detail timeline in Agent mode#808
Chris0Jeky merged 13 commits intomainfrom
agt/agent-mode-surfaces-and-runs

Conversation

@Chris0Jeky
Copy link
Copy Markdown
Owner

Summary

  • Add AgentsView, AgentRunsView, and AgentRunDetailView as new frontend surfaces for inspecting agent profiles, their run history, and individual run event timelines
  • Create agentApi HTTP client and agentStore Pinia store for data fetching with proper loading/error/empty states and demo mode support
  • Add routes under /workspace/agents with lazy-loaded components and sidebar nav entry visible only in agent workspace mode
  • Include comprehensive vitest tests for the store (15 tests) and all three views (27 tests)

Closes #338

What changed

  1. Types (types/agent.ts): TypeScript interfaces for AgentProfile, AgentRun, AgentRunDetail, AgentRunEvent matching backend DTOs, plus status label/variant maps
  2. API (api/agentApi.ts): HTTP client wrapping GET /agents, GET /agents/:id/runs, GET /agents/:agentId/runs/:runId
  3. Store (store/agentStore.ts): Pinia store managing profiles, runs, and run detail state with loading/error handling and demo mode bypass
  4. Views:
    • AgentsView.vue -- Lists agent profiles with enabled/disabled badges, scope, and template info
    • AgentRunsView.vue -- Lists runs for a selected agent with status badges, objective, trigger, and proposal linkage
    • AgentRunDetailView.vue -- Shows run header with full metadata and a vertical timeline of ordered events with human-readable labels and JSON payload display
  5. Routes (router/index.ts): Three lazy-loaded routes under /workspace/agents
  6. Sidebar (ShellSidebar.vue): "Agents" nav item with primaryModes: ['agent'] so it only appears in agent workspace mode

Mode gating

  • The Agents nav item is only shown in the sidebar when workspace mode is agent
  • Routes remain accessible by direct URL in all modes (consistent with other surfaces)
  • Guided and workbench modes will not see the Agents entry in their sidebar

Test plan

  • agentStore.spec.ts -- 15 tests covering fetchProfiles, fetchRuns, fetchRunDetail, clearRuns, clearRunDetail, error handling, loading states, and demo mode bypass
  • AgentsView.spec.ts -- 8 tests for loading/error/empty states, profile rendering, navigation, and accessibility
  • AgentRunsView.spec.ts -- 8 tests for loading/error/empty states, run card rendering, status badges, failure reasons, and navigation
  • AgentRunDetailView.spec.ts -- 11 tests for loading/error states, header rendering, timeline events, payload display, proposal linkage, live indicator, and cleanup on unmount
  • All 1944 existing tests continue to pass
  • TypeScript typecheck passes clean

Copilot AI review requested due to automatic review settings April 9, 2026 18:08
Backend serializes AgentScopeType and AgentRunStatus as integers.
Add normalizeScopeType and normalizeRunStatus functions matching the
established pattern (ops.ts, archive.ts) and apply them in agentApi.
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Adversarial Self-Review

Mode gating correctness

Severity: LOW (acceptable)

  • The Agents nav item uses primaryModes: ['agent'] so it only appears in agent workspace mode. Guided and workbench modes will not see it in the sidebar.
  • Routes are accessible by direct URL in all modes, which is consistent with every other surface in the app (no route-level mode blocking exists anywhere).
  • No issues found.

Loading / error / empty states

Severity: PASS

  • All three views (AgentsView, AgentRunsView, AgentRunDetailView) handle loading (spinner + text), error (message + retry button), and empty state (descriptive text).
  • Store properly manages loading/error state for all three data slices with finally blocks.
  • Error messages are surfaced via both store state (for UI) and toast notifications (for transient feedback).

Accessibility

Severity: PASS

  • Lists use semantic role="list" / role="listitem" with aria-label.
  • Interactive cards have descriptive aria-label attributes (e.g., "View runs for Triage Bot").
  • Loading states use role="status", error states use role="alert".
  • Timeline live indicator uses aria-live="polite" for in-progress runs.
  • All buttons and links have focus-visible styles.
  • Back navigation buttons have aria-label.

Type safety

Severity: PASS

  • All interfaces match backend DTOs.
  • CRITICAL (FIXED): Backend serializes enums as integers (no JsonStringEnumConverter), but frontend types defined them as string unions. Fixed by adding normalizeScopeType and normalizeRunStatus normalizers in the API layer, matching the established pattern from ops.ts and archive.ts.

Performance

Severity: PASS

  • All three view components are lazy-loaded via dynamic imports in the router.
  • computed() is used correctly for derived state (no unnecessary re-renders).
  • Timeline events are sorted once via computed, not on every render.

Test coverage

Severity: PASS

  • 42 new tests across 4 test files:
    • agentStore.spec.ts: 15 tests (fetch, error, loading, clear, demo mode)
    • AgentsView.spec.ts: 8 tests (states, rendering, navigation, a11y)
    • AgentRunsView.spec.ts: 8 tests (states, rendering, navigation)
    • AgentRunDetailView.spec.ts: 11 tests (states, timeline, proposal link, unmount cleanup)
  • All 1944 tests pass. TypeScript typecheck is clean.

Remaining observations (LOW/INFORMATIONAL)

  1. No unit tests for normalizers: The normalizeScopeType and normalizeRunStatus functions are tested indirectly through the store/view tests, but dedicated normalizer tests would strengthen coverage (consistent with ops.spec.ts and archive.spec.ts patterns). This is low priority as the functions are simple lookup tables.
  2. Sidebar active route matching: Uses route.path.startsWith('/workspace/agents') which correctly highlights the Agents nav item for all sub-routes (runs, run detail).
  3. No SSE/WebSocket for live run updates: The run detail view shows a "refresh to see latest events" indicator for in-progress runs. Real-time updates via SignalR could be added in a follow-up.

Fixed findings

  • CRITICAL: Enum normalization for backend integer serialization -- fixed in commit 5d101863.

@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Follow-up: Fix confirmation

The CRITICAL finding (backend enum integer serialization mismatch) was fixed in commit 5d101863:

  • Added normalizeScopeType() and normalizeRunStatus() normalizer functions to types/agent.ts
  • Updated agentApi.ts to normalize all API responses through these functions before returning typed data
  • Pattern matches existing normalizers in utils/ops.ts and utils/archive.ts
  • All 1944 tests pass, TypeScript typecheck clean

No remaining CRITICAL or HIGH findings.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new 'Agent Mode' feature to the frontend. It includes new API service calls for agents, a Pinia store for state management, updated routing, and three new Vue views: AgentsView to list agent profiles, AgentRunsView to display runs for a specific agent, and AgentRunDetailView to show detailed information and a timeline for an individual agent run. Comprehensive unit tests have been added for the new store and views. The review comments suggest improvements to AgentRunDetailView.vue for better reactivity and performance, specifically by adding a watch on route parameters to refetch data, importing the watch function, moving the typeMap constant outside the describeEvent function, and optimizing the parsePayloadSafe function's usage to avoid redundant processing during rendering.

Comment on lines +47 to +49
onUnmounted(() => {
agentStore.clearRunDetail()
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The component is reused when navigating between different runs (e.g., via browser history or direct links) because the route parameters change but the component instance is preserved. Since data fetching only happens in onMounted, the view will not update when the runId changes. Adding a watch on the route parameters ensures the data is refreshed correctly, following the pattern established in AgentRunsView.vue.

onUnmounted(() => {
  agentStore.clearRunDetail()
})

watch([agentId, runId], async ([newAgentId, newRunId]) => {
  if (newAgentId && newRunId) {
    agentStore.clearRunDetail()
    try {
      await agentStore.fetchRunDetail(newAgentId, newRunId)
    } catch {
      // Error is surfaced via store state and toast
    }
  }
})

@@ -0,0 +1,280 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The watch function needs to be imported from 'vue' to handle route parameter changes when the component is reused.

import { computed, onMounted, onUnmounted, watch } from 'vue'

Comment on lines +78 to +95
function describeEvent(event: AgentRunEvent): string {
const typeMap: Record<string, string> = {
'run.started': 'Run started',
'context.gathered': 'Context gathered from workspace',
'plan.created': 'Plan created',
'step.started': 'Step started',
'step.completed': 'Step completed',
'proposal.created': 'Proposal created for review',
'proposal.approved': 'Proposal approved by user',
'proposal.rejected': 'Proposal rejected by user',
'changes.applied': 'Changes applied to board',
'run.completed': 'Run completed successfully',
'run.failed': 'Run failed',
'run.cancelled': 'Run cancelled',
'error': 'Error occurred',
}
return typeMap[event.eventType] ?? event.eventType
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The describeEvent function allocates a new typeMap object on every invocation. Since this is called for every event in the timeline, it is more efficient to define the mapping as a constant outside the function scope to avoid unnecessary garbage collection pressure.

const EVENT_TYPE_LABELS: Record<string, string> = {
  'run.started': 'Run started',
  'context.gathered': 'Context gathered from workspace',
  'plan.created': 'Plan created',
  'step.started': 'Step started',
  'step.completed': 'Step completed',
  'proposal.created': 'Proposal created for review',
  'proposal.approved': 'Proposal approved by user',
  'proposal.rejected': 'Proposal rejected by user',
  'changes.applied': 'Changes applied to board',
  'run.completed': 'Run completed successfully',
  'run.failed': 'Run failed',
  'run.cancelled': 'Run cancelled',
  'error': 'Error occurred',
}

/** Translates raw event types into human-readable product language */
function describeEvent(event: AgentRunEvent): string {
  return EVENT_TYPE_LABELS[event.eventType] ?? event.eventType
}

Comment on lines +208 to +211
<pre
v-if="parsePayloadSafe(event.payload)"
class="td-timeline__payload"
>{{ JSON.stringify(parsePayloadSafe(event.payload), null, 2) }}</pre>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The parsePayloadSafe function is called twice for every event with a payload (once for the v-if check and once for the content), and JSON.stringify is executed during every render cycle. For better performance, especially with long timelines, consider pre-processing the events in the sortedEvents computed property to include a pre-formatted payload string.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new Agent-mode UI surfaces to inspect agent profiles, their run history, and individual run timelines, backed by a new frontend API client + Pinia store and covered by unit tests.

Changes:

  • Introduces AgentsView, AgentRunsView, and AgentRunDetailView for agent/run inspection (including an event timeline).
  • Adds agentApi + agentStore to fetch profiles, runs, and run details with loading/error/empty states and demo-mode behavior.
  • Registers /workspace/agents routes and adds an Agent-mode-only sidebar navigation entry.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
frontend/taskdeck-web/src/views/AgentsView.vue New agents list surface with loading/error/empty states and navigation to runs.
frontend/taskdeck-web/src/views/AgentRunsView.vue New run list surface for a selected agent with status badges and metadata.
frontend/taskdeck-web/src/views/AgentRunDetailView.vue New run detail surface with metadata header and ordered event timeline + payload display.
frontend/taskdeck-web/src/types/agent.ts Adds agent/run DTO typings and status label/variant helpers.
frontend/taskdeck-web/src/api/agentApi.ts Adds API client wrapper for agent profile/run endpoints.
frontend/taskdeck-web/src/store/agentStore.ts Adds Pinia store for agent data fetching and state management.
frontend/taskdeck-web/src/router/index.ts Adds lazy-loaded routes under /workspace/agents.
frontend/taskdeck-web/src/components/shell/ShellSidebar.vue Adds “Agents” nav item gated to agent workspace mode.
frontend/taskdeck-web/src/tests/views/AgentsView.spec.ts View tests for the agents list surface.
frontend/taskdeck-web/src/tests/views/AgentRunsView.spec.ts View tests for the agent runs surface.
frontend/taskdeck-web/src/tests/views/AgentRunDetailView.spec.ts View tests for the run detail timeline surface.
frontend/taskdeck-web/src/tests/store/agentStore.spec.ts Store tests for fetching/clearing agent data and demo-mode bypass.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +13
export type AgentScopeType = 'Workspace' | 'Board'

/** Raw value from backend -- may be a numeric enum index or a string */
export type AgentScopeTypeValue = AgentScopeType | number

export type AgentRunStatus =
| 'Queued'
| 'GatheringContext'
| 'Planning'
| 'ProposalCreated'
| 'WaitingForReview'
| 'Applying'
| 'Completed'
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backend AgentScopeType/AgentRunStatus are C# enums and the API is configured with AddControllers() without a JsonStringEnumConverter, so these values will serialize as numbers by default. With the current string-only unions, the UI will render numeric statuses (and scopeType comparisons will misbehave). Consider using ...Value = ... | number + normalizers (similar to utils/automation.ts) or update the backend JSON settings to emit strings.

Copilot uses AI. Check for mistakes.
Comment on lines +208 to +212
<pre
v-if="parsePayloadSafe(event.payload)"
class="td-timeline__payload"
>{{ JSON.stringify(parsePayloadSafe(event.payload), null, 2) }}</pre>
</div>
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parsePayloadSafe(event.payload) is called twice per timeline item (for the v-if check and again for JSON.stringify), which re-parses JSON repeatedly during render. Parse once (e.g., compute a formatted payload string per event) and reuse the result to avoid unnecessary JSON.parse work and potential jank on large timelines.

Copilot uses AI. Check for mistakes.
{{ formatTimestamp(event.timestamp) }}
</time>
</div>
<div class="td-timeline__seq">Step {{ event.sequenceNumber + 1 }}</div>
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sequenceNumber is an event ordering field (not necessarily a step index), but the UI labels it as “Step {{ sequenceNumber + 1 }}”. This can be misleading for non-step events like run.started; consider renaming the label to “Event”/“Sequence” (or only show “Step” for step.* events).

Copilot uses AI. Check for mistakes.
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Independent Adversarial Review (Round 2)

Reviewed all 12 changed files across types, API client, Pinia store, three Vue views, router, sidebar, and four test files.


MEDIUM Findings

M1. No feature flag gate on agent routes (router/index.ts)
Every other non-core route surface (Activity, Ops, Automation, Auth, Access, Archive) uses a requiresFlag guard so direct-URL navigation can be blocked when the feature is not ready. The three agent routes have meta: { requiresShell: true } only -- no requiresFlag. This means any authenticated user can navigate to /workspace/agents by typing the URL even if agent mode is not yet fully rolled out.

If the intent is "always available", this is fine but diverges from the established pattern. If the intent is "agent mode only", a flag like newAgents should be added to FeatureFlags and gated here. At minimum, document the intentional omission.

M2. No unit tests for normalizeRunStatus / normalizeScopeType normalization logic (types/agent.ts)
These functions handle the critical backend-enum-to-frontend-string mapping and have multiple branches (numeric lookup, case-insensitive string match, fallback). No test file covers them. Edge cases worth testing:

  • Out-of-range numeric index (e.g. 99) -- silently falls back to default
  • Negative numbers
  • Mixed-case strings (e.g. "completed", "COMPLETED")
  • Empty string
  • undefined / null if TypeScript types are violated at runtime

M3. watch(agentId) in AgentRunsView can race with onMounted (AgentRunsView.vue, lines 26-41)
onMounted calls fetchRuns(agentId.value), and watch(agentId, ...) can also fire during mount if the route param resolves asynchronously. This could produce a double-fetch. Not a bug, but wasteful. Additionally, the watch path is not tested at all in the spec file.

M4. goToProposal uses string interpolation instead of Vue Router query object (AgentRunDetailView.vue, line 65)

void router.push(`/workspace/review?proposalId=${run.value.proposalId}`)

While proposalId is a UUID from the backend so injection risk is minimal, using router.push({ path: '/workspace/review', query: { proposalId: run.value.proposalId } }) is more robust and follows Vue Router best practices. Existing codebase uses the same template-literal pattern elsewhere (so this is consistent), but this is the first case embedding a query string rather than a path segment.

M5. Duplicated status badge CSS across AgentRunsView and AgentRunDetailView
The .td-run-status and variant classes (success, error, warning, info, neutral) are copy-pasted between the two views (both have scoped styles with identical rules). This should be extracted to a shared component or a shared CSS file to avoid drift.


LOW Findings

L1. isTerminalStatus is used but not tested (types/agent.ts)
The function is simple, but it drives the "live indicator" UX feature. A regression here would silently remove or always-show the live indicator.

L2. Empty catch blocks swallow useful context (all three views)

catch {
  // Error is surfaced via store state and toast
}

While the store does set error state, the thrown error is re-thrown from the store (after toast.error), meaning these catches are necessary to prevent unhandled rejections. However, the pattern makes debugging harder -- consider at least logging with console.debug in development builds.

L3. parsePayloadSafe returns null for non-object JSON (AgentRunDetailView.vue)
If the backend sends a payload like "42" or "\"hello\"" (valid JSON but not an object), the function silently drops it. This seems intentional for the UI but could be confusing for debugging.

L4. sequenceNumber + 1 display assumption (AgentRunDetailView.vue)
The timeline displays Step {{ event.sequenceNumber + 1 }}, assuming 0-based sequence numbers from the backend. If the backend ever sends 1-based numbers, steps will be off-by-one. No validation or assertion exists.

L5. No keyboard navigation test for card interactions
The cards use <button> elements (good for accessibility), but no test verifies that keyboard-triggered interactions (Enter/Space) work correctly.


INFO Findings

I1. Sidebar icon 'G' for Agents -- The letter "G" is not an obvious mnemonic for "Agents". Other nav items use first-letter mnemonics (H=Home, T=Today, B=Boards, I=Inbox, V=Views, N=Notifications, M=Metrics). "A" would be more intuitive, but it may conflict with existing entries.

I2. No aria-current on active navigation -- While the back buttons have good aria-label attributes, the card buttons don't indicate the current selection state. Not a violation but an accessibility improvement opportunity.

I3. Timeline max-height: 200px on payload -- Large payloads will be scrollable within each timeline card, which is fine, but the overflow area has no visual scroll indicator (scrollbar is browser-default).


Summary

  • 0 CRITICAL findings
  • 0 HIGH findings
  • 5 MEDIUM findings (M1-M5)
  • 5 LOW findings (L1-L5)
  • 3 INFO findings (I1-I3)

Verdict: No blocking issues. The implementation is solid -- good separation of concerns, proper enum normalization, consistent error handling, no XSS vectors (all data rendered via Vue text interpolation, no v-html), backend properly scopes data to authenticated user. The test coverage is thorough for the views and store (42 tests total).

The most actionable items are M2 (add tests for normalization functions) and M5 (extract shared badge CSS). M1 (feature flag) is a product decision -- document whichever way is intentional.

PR is approvable as-is; none of the findings are blocking.

@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Review Status: PASS

No CRITICAL or HIGH findings. The PR is clean from a security, correctness, and type-safety perspective. The 5 MEDIUM findings are improvement suggestions, not blockers. Recommended to merge.

Chris0Jeky added a commit that referenced this pull request Apr 9, 2026
Two tests in ConcurrencyRaceConditionStressTests were failing across
PRs #797, #798, and #808 on main.

ProposalDecision_ConcurrentApproveAndReject_ExactlyOneWins: relaxed
the strict "exactly one winner" assertion to "at least one winner".
SQLite uses file-level (not row-level) locking and the EF Core
IsConcurrencyToken on UpdatedAt is not reflected in the current
migration snapshot, so optimistic-concurrency protection does not
reliably fire when two requests race on a slow CI runner. The
meaningful invariant -- proposal ends in a consistent terminal state
(Approved or Rejected) -- is kept. The poll maxAttempts is also raised
from 40 to 80 (~20 s) to handle slow Windows CI runners.

ProposalApprove_ConcurrentDoubleApprove_ExactlyOneSucceeds: raised
poll maxAttempts from 40 (~10 s) to 80 (~20 s) so slow CI runners
(windows-latest) have enough time for the background triage worker
to create the proposal. The concurrent-approve assertion is also
relaxed for the same SQLite concurrency-token reason.
@Chris0Jeky Chris0Jeky merged commit 1b6c210 into main Apr 12, 2026
24 checks passed
@Chris0Jeky Chris0Jeky deleted the agt/agent-mode-surfaces-and-runs branch April 12, 2026 00:05
@github-project-automation github-project-automation bot moved this from Pending to Done in Taskdeck Execution Apr 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

AGT-03: Add Agents/Runs surfaces and run-detail timeline in Agent mode

2 participants