This TechSpec defines the implementation plan for the Arandu Session Browser & Plan Manager POC, a React-based web
application at apps/web that lets developers browse coding agent sessions, review plans and task lists, and add
developer annotations. The POC uses mock data with a SessionRepository adapter pattern (
src/features/sessions/data/) so the data layer can be swapped for real filesystem access later. TanStack Query v5
manages session/task data caching while Zustand handles UI state (active tab, sidebar selection). The existing VS
Code-like layout shell (ActivityBar, Sidebar, TabBar) is reused and restyled with a new visual identity. Session
detail views use an accumulating tab model (max ~5 tabs, VS Code-style). Plan markdown is rendered with the existing
react-markdown + remark-gfm pipeline, and tasks are extracted from [ ]/[x] checkboxes in the repository layer
as structured Task[] objects cached by TanStack Query. Developer annotations are in-memory only (not persisted),
using the existing ReviewPanel block-based comment system with line-range selection.
Key risks: mock data fidelity vs. real Copilot session structure, and ensuring the adapter interface is broad enough for future real implementations without over-engineering the POC.
apps/web/src/
├── features/
│ └── sessions/ # NEW — Session browser feature module
│ ├── data/
│ │ ├── session.repository.ts # SessionRepository interface
│ │ ├── mock-session.repository.ts # MockSessionRepository implementation
│ │ ├── mock-data.ts # Hardcoded mock session data
│ │ └── plan-parser.ts # Markdown → Task[] parser
│ ├── hooks/
│ │ ├── use-sessions.ts # TanStack Query hook: session list
│ │ ├── use-session-detail.ts # TanStack Query hook: single session + plan + tasks
│ │ └── use-session-comments.ts # In-memory comment state hook
│ ├── components/
│ │ ├── SessionList.tsx # Sidebar session list component
│ │ ├── SessionDetail.tsx # Tab content: plan viewer + task list
│ │ ├── TaskList.tsx # Extracted task list display
│ │ └── SessionTab.tsx # Individual tab header component
│ ├── types/
│ │ └── session.types.ts # Session, Plan, Task, Comment types
│ └── index.ts # Public exports
├── components/
│ └── layout/ # EXISTING — Reuse & restyle
│ ├── ActivityBar.tsx # Vertical icon bar (restyle)
│ ├── Sidebar.tsx # Collapsible panel (extend with SessionList)
│ ├── TabBar.tsx # Horizontal tabs (extend with session tabs)
│ └── MainContent.tsx # NEW — Tab content area orchestrator
│ └── markdown/ # EXISTING — Reuse as-is
│ └── MarkdownViewer.tsx # Block-based markdown with line numbers
│ └── review/ # EXISTING — Reuse as-is
│ └── ReviewPanel.tsx # Comment system with line-range selection
├── store/
│ └── useAranduStore.ts # MODIFY — Add session UI state (tabs, active session)
├── lib/
│ └── query-client.ts # NEW — TanStack Query client setup
└── App.tsx # MODIFY — Wire QueryClientProvider + session feature
Boundaries:
features/sessions/owns all session-specific logic (data, hooks, components, types)components/layout/is shared infrastructure — extended but not owned by the session featurecomponents/markdown/andcomponents/review/are reused as-is from the existing codebasestore/useAranduStore.tsis extended with session-specific UI slices (tab state, active session)lib/query-client.tsprovides the shared TanStack Query client instance
| Component | Responsibility | Key Dependencies |
|---|---|---|
SessionRepository |
Abstract interface for session data access | None (pure interface) |
MockSessionRepository |
Hardcoded mock implementation with plan.md parsing | plan-parser.ts, mock data |
plan-parser.ts |
Extracts Task[] from markdown checkbox syntax |
None (pure function) |
use-sessions |
TanStack Query hook for session list | SessionRepository, @tanstack/react-query |
use-session-detail |
TanStack Query hook for session plan + tasks | SessionRepository, @tanstack/react-query |
use-session-comments |
In-memory annotation state per session | React useState/useRef |
SessionList |
Renders session entries in the Sidebar | use-sessions, Zustand store |
SessionDetail |
Orchestrates plan viewer + task list + review panel | use-session-detail, MarkdownViewer, ReviewPanel |
TaskList |
Displays extracted tasks with checkbox status | Task[] from parent |
TabBar (extended) |
Manages session tab headers with close buttons | Zustand store (tab state) |
MainContent |
Routes tab content area to correct SessionDetail | Zustand store (active tab) |
Data flow: User clicks session in SessionList → Zustand adds tab → MainContent renders SessionDetail for
active tab → use-session-detail hook fetches plan+tasks from SessionRepository via TanStack Query → MarkdownViewer
renders plan, TaskList renders extracted tasks, ReviewPanel enables annotations.
// apps/web/src/features/sessions/data/session.repository.ts
interface SessionRepository {
/** List all available sessions, sorted by modification date descending */
listSessions (): Promise<SessionSummary[]>;
/** Get full session detail including raw plan markdown and parsed tasks */
getSessionDetail (sessionId: string): Promise<SessionDetail>;
}
// apps/web/src/features/sessions/data/plan-parser.ts
/** Pure function: extracts Task[] from plan.md markdown content */
function parsePlanTasks (markdown: string): Task[];// apps/web/src/features/sessions/data/mock-session.repository.ts
class MockSessionRepository implements SessionRepository {
async listSessions (): Promise<SessionSummary[]> {
// Returns hardcoded session summaries with realistic Copilot-like data
}
async getSessionDetail (sessionId: string): Promise<SessionDetail> {
// Returns mock plan markdown + runs parsePlanTasks() to extract Task[]
}
}// apps/web/src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity, // Mock data never goes stale
gcTime: 1000 * 60 * 30, // 30 min garbage collection
retry: false, // No retries for mock data
},
},
});// apps/web/src/features/sessions/types/session.types.ts
/** Shown in the sidebar session list */
interface SessionSummary {
id: string; // UUID matching Copilot session ID
name: string; // Derived from workspace.yaml or first line of plan
summary: string; // Brief description or plan excerpt
createdAt: string; // ISO 8601
modifiedAt: string; // ISO 8601
agentType: 'copilot' | 'generic'; // Agent identifier for UI badges
workspacePath: string; // Original workspace directory path
taskStats: {
total: number;
completed: number;
};
}
/** Full session detail for the tab content view */
interface SessionDetail {
id: string;
name: string;
summary: string;
createdAt: string;
modifiedAt: string;
agentType: 'copilot' | 'generic';
workspacePath: string;
plan: {
raw: string; // Original plan.md markdown content
tasks: Task[]; // Extracted from [ ]/[x] checkboxes
};
}
/** Individual task extracted from plan.md checkboxes */
interface Task {
id: string; // Generated: `task-{lineNumber}`
title: string; // Text content after [ ] or [x]
completed: boolean; // true if [x], false if [ ]
lineNumber: number; // Source line in plan.md (for comment anchoring)
indent: number; // Nesting level (0-based, from leading whitespace)
}
/** Developer annotation on plan content */
interface SessionComment {
id: string; // UUID
sessionId: string;
lineRange: {
start: number; // Start line in plan.md
end: number; // End line in plan.md
};
content: string; // Comment text (plain text)
createdAt: string; // ISO 8601
}
/** Tab state in Zustand store */
interface SessionTab {
sessionId: string;
title: string; // Session name for tab header
}Validation rules:
SessionSummary.idandSessionDetail.idmust be valid UUIDsTask.lineNumbermust be > 0 and correspond to actual plan.md linesSessionComment.lineRange.startmust be <=lineRange.endSessionTablist max length: 5 (enforced in Zustand action)
SessionRepository Interface Contract:
| Method | Input | Output | Error Behavior |
|---|---|---|---|
listSessions() |
none | Promise<SessionSummary[]> |
Returns empty array on failure |
getSessionDetail(sessionId) |
string (UUID) |
Promise<SessionDetail> |
Throws SessionNotFoundError if ID unknown |
class SessionNotFoundError extends Error {
constructor (public sessionId: string) {
super(`Session not found: ${sessionId}`);
this.name = 'SessionNotFoundError';
}
}TanStack Query Key Conventions:
- Session list:
['sessions'] - Session detail:
['sessions', sessionId]
Zustand Store Extension (session UI slice):
// Added to existing useAranduStore.ts
interface SessionUIState {
tabs: SessionTab[]; // Open session tabs (max 5)
activeTabId: string | null; // Currently visible session tab
sidebarSessionsExpanded: boolean;
// Actions
openSession: (session: SessionSummary) => void;
closeTab: (sessionId: string) => void;
setActiveTab: (sessionId: string) => void;
}openSession behavior:
- If session already has a tab → activate that tab (no duplicate)
- If tabs.length >= 5 → close oldest tab, then add new
- Otherwise → add new tab at end, activate it
closeTab behavior:
- Remove tab from array
- If closed tab was active → activate the tab to the left (or first tab, or null if empty)
Comments are stored per-session in a Map<string, SessionComment[]> managed by the use-session-comments hook. No
persistence, no API. The hook exposes:
interface UseSessionCommentsReturn {
comments: SessionComment[];
addComment: (lineRange: { start: number; end: number }, content: string) => void;
removeComment: (commentId: string) => void;
}Not applicable — this POC has no backend API. All data flows through the in-memory SessionRepository interface.
| Existing Component | Integration Strategy | Modifications Required |
|---|---|---|
ActivityBar |
Add "Sessions" icon (e.g., FolderGit2 from lucide-react) |
Add new nav item to icon list |
Sidebar |
Render SessionList when Sessions activity is selected |
Add conditional content rendering |
TabBar |
Extend to show SessionTab headers with close buttons |
Add session tab type, close button handler |
MarkdownViewer |
Reuse as-is inside SessionDetail for plan.md rendering |
No changes — receives markdown string prop |
ReviewPanel |
Reuse as-is inside SessionDetail for annotations |
Wire to use-session-comments hook instead of store |
| Dependency | Version | Usage | Status |
|---|---|---|---|
@tanstack/react-query |
^5.x | Session data caching | Installed, currently unused — activate |
react-markdown |
^9.x | Plan.md rendering | Already in use by MarkdownViewer |
remark-gfm |
^4.x | GFM support (checkboxes, tables) | Already in use |
zustand |
^5.x | UI state management | Already in use |
lucide-react |
^0.x | Icons | Already in use |
shadcn/ui components |
latest | UI primitives (ScrollArea, Badge, Button) | Already configured |
No new dependencies required. All needed packages are already installed.
- App Initialization (
App.tsx)
- Wrap app in
QueryClientProviderwith sharedqueryClient - Instantiate
MockSessionRepositoryand provide via React context or module-level singleton - Render existing layout shell (ActivityBar + Sidebar + TabBar + MainContent)
- Session List Loading (on app mount)
SessionListcomponent mounts inside SidebaruseSessions()hook triggersqueryClient.fetchQuery(['sessions'], () => repository.listSessions())- TanStack Query caches
SessionSummary[]— staleTime: Infinity (mock data) - Sidebar renders list sorted by
modifiedAtdescending
- Session Selection (user click in sidebar)
- User clicks a session entry in
SessionList openSession(session)Zustand action fires → adds/activates tabTabBarre-renders with new tab headerMainContentrendersSessionDetailfor activesessionIduseSessionDetail(sessionId)hook triggersqueryClient.fetchQuery(['sessions', sessionId], () => repository.getSessionDetail(sessionId))- TanStack Query caches
SessionDetail(includes raw markdown + parsedTask[])
- Plan Rendering (inside SessionDetail)
MarkdownViewerreceivesplan.rawmarkdown string- Renders with block-based line numbers (existing functionality)
TaskListreceivesplan.tasksand renders checkbox-style task items
- Annotation (user interaction)
- User selects line range in
MarkdownViewer(existing block selection) ReviewPanelopens with selected range- User types annotation text and submits
addComment()fromuse-session-commentsstores in-memory- Comment indicators appear on
MarkdownViewergutter (existing functionality)
- Tab Switching
- User clicks different tab in
TabBar setActiveTab(sessionId)fires in ZustandMainContentswaps to correspondingSessionDetail- TanStack Query serves cached data (no re-fetch)
| Failure Mode | Expected Behavior | User-Visible Outcome |
|---|---|---|
| Session list fetch fails | TanStack Query error state |
Sidebar shows "Failed to load sessions" message with retry button |
| Session detail fetch fails | TanStack Query error state |
Tab content shows "Session not found" error message |
| Invalid session ID in openSession | SessionNotFoundError caught by TanStack Query |
Error boundary in tab content area |
| Max tabs exceeded (>5) | Oldest tab auto-closed by openSession action |
Oldest tab disappears, new tab opens |
| Plan.md parsing yields no tasks | parsePlanTasks returns empty Task[] |
TaskList renders "No tasks found" placeholder |
| Comment on invalid line range | addComment validates range, silently ignores invalid |
No comment created, no error shown |
Since this is a mock-data POC, most failure modes are edge cases during development rather than production concerns.
| Affected Component | Type of Impact | Description & Risk Level | Required Action |
|---|---|---|---|
App.tsx |
Structural Change | Add QueryClientProvider wrapper, session repository provider. Low risk — additive only. |
Wire providers around existing app tree |
useAranduStore.ts |
State Extension | Add session UI slice (tabs, activeTab). Low risk — new slice, no changes to existing state. | Add SessionUIState slice using Zustand's slice pattern |
ActivityBar |
UI Extension | Add "Sessions" navigation icon. Low risk — one new item in existing icon array. | Add icon entry |
Sidebar |
Content Extension | Conditionally render SessionList vs. existing content based on active activity. Low risk. |
Add conditional rendering branch |
TabBar |
Feature Extension | Support session-type tabs with close buttons. Medium risk — existing tab logic may need generalization. | Extend tab model to support closeable session tabs |
MarkdownViewer |
No Change | Reused as-is for plan.md rendering. No risk. | None |
ReviewPanel |
Integration Change | Wire to use-session-comments hook instead of store for session-specific comments. Low risk. |
Pass comments/handlers as props instead of reading from store |
| CSS/Theme | Visual Overhaul | Restyle all layout components with new visual identity. Medium risk — broad surface area. | Update CSS variables, component styles in Tailwind config |
Test framework: Vitest (already configured in vitest.config.ts)
| Component | Test Focus | Mock Requirements |
|---|---|---|
parsePlanTasks() |
Checkbox extraction accuracy, edge cases (nested, empty, mixed) | None — pure function |
MockSessionRepository |
Returns expected data shape, SessionNotFoundError for unknown IDs |
None |
openSession action |
Tab accumulation, duplicate prevention, max-5 eviction, activation | None — Zustand store test |
closeTab action |
Tab removal, active tab reassignment | None |
use-session-comments |
Add/remove comments, line range validation | None |
Critical test cases for parsePlanTasks:
- [ ] unchecked task → { completed: false, title: "unchecked task" }
- [x] completed task → { completed: true, title: "completed task" }
- [ ] nested task → { completed: false, indent: 1 }
- [ ] task with **bold** → { title: "task with **bold**" }
Empty markdown → []
Markdown with no checkboxes → []
Mixed content with checkboxes → only checkbox items extracted
| Test Scenario | Components Involved | Validation |
|---|---|---|
| Session list renders on mount | SessionList + useSessions + MockSessionRepository |
List shows all mock sessions with correct metadata |
| Click session opens tab | SessionList + TabBar + SessionDetail |
New tab appears, detail content loads |
| Plan markdown renders with tasks | SessionDetail + MarkdownViewer + TaskList |
Markdown rendered, tasks extracted and shown |
| Annotation workflow | SessionDetail + ReviewPanel + use-session-comments |
Select lines → add comment → comment indicator appears |
| Tab switching preserves state | Multiple SessionDetail instances |
Switch tabs, previous tab retains scroll position |
| Max tabs behavior | 6 sequential session opens | 5th tab opens normally, 6th evicts oldest |
| Verification | Method |
|---|---|
SessionRepository contract |
TypeScript compiler enforces interface implementation |
| Mock data shape matches real Copilot format | Manual review of mock data against real ~/.copilot/session-state/ structure |
| TanStack Query cache keys | Unit tests verify cache hits on repeated fetches |
| Zustand tab state invariants | Unit tests verify max-5, no duplicates, active-tab consistency |
-
Types & Interfaces (
session.types.ts,session.repository.ts) — Foundation with no dependencies. Enables parallel work on all other components. -
Plan Parser (
plan-parser.ts) — Pure function, independently testable. Unblocks mock repository implementation. -
Mock Data & Repository (
mock-data.ts,mock-session.repository.ts) — Depends on types + parser. Creates realistic session data matching Copilot format. Provides the data layer for all UI work. -
TanStack Query Setup (
query-client.ts,use-sessions.ts,use-session-detail.ts) — Depends on repository. Wire TanStack Query hooks with mock repository as query functions. -
Zustand Store Extension (
useAranduStore.tssession UI slice) — Add tab state management (openSession, closeTab, setActiveTab). Independent of data hooks. -
Session List Component (
SessionList.tsx) — Depends onuseSessionshook. First visible UI — renders sessions in sidebar. -
Session Detail Components (
SessionDetail.tsx,TaskList.tsx,SessionTab.tsx) — Depends onuseSessionDetailhook + existingMarkdownViewer. Core content view. -
Layout Integration (
ActivityBar,Sidebar,TabBar,MainContent,App.tsx) — Wire session feature into existing layout shell. Add QueryClientProvider. -
Comment Integration (
use-session-comments.ts+ wire toReviewPanel) — In-memory annotation support using existing ReviewPanel. -
Visual Restyling — Update CSS variables, Tailwind config, and component styles for new visual identity across all layout components.
| Dependency | Status | Blocking? |
|---|---|---|
| TanStack Query v5 | Installed, unused | No — just need to import and configure |
| Zustand v5 | Active | No — extend existing store |
| react-markdown + remark-gfm | Active | No — already rendering markdown |
| shadcn/ui components | Configured | No — already available |
| Vitest | Configured | No — test infrastructure ready |
No blocking external dependencies. All required libraries are already installed.
Since this is a client-side POC with mock data, traditional server-side monitoring does not apply. Instead:
Development-time observability:
| Tool | Purpose | Implementation |
|---|---|---|
| React Query DevTools | Inspect cache state, query status, stale/fresh timing | Add <ReactQueryDevtools> in App.tsx (dev only) |
| Zustand DevTools | Inspect tab state, active session, sidebar state | Already available via devtools middleware in existing store |
| Browser Console | Error logging for repository failures, parse errors | console.error in TanStack Query onError callbacks |
| Vite HMR | Hot reload during development | Already configured |
Key metrics to log (console):
- Session list fetch duration (even for mock — validates hook wiring)
- Plan parse time and task count per session
- Tab state transitions (open/close/switch)
Not applicable for a client-side POC. Acceptance criteria:
- App loads without errors in Chrome/Edge (latest)
- All mock sessions appear in sidebar
- Clicking a session opens a tab with plan content
- Tasks extracted and displayed from plan markdown
- Annotations can be added/removed via ReviewPanel
- Max 5 tabs enforced, oldest evicted
This is a greenfield POC in apps/web — no production traffic, no existing users to migrate.
- Development branch: All work on feature branch off
main - Incremental PRs: Follow build order above — types/data first, UI second, integration last
- Local validation:
npm run devserves the POC atlocalhost:5173 - No deployment pipeline needed: POC is local-only for now
- Git revert: Since all changes are in
apps/web/src/features/sessions/and minimal modifications to existing files, revert is straightforward - Existing functionality preserved: Layout shell modifications are additive (new activity item, new tab type) — removing them restores original behavior
- No data migration: In-memory mock data, nothing to roll back
| Decision | Rationale | Alternative Rejected |
|---|---|---|
Feature Module Architecture (src/features/sessions/) |
Clean domain boundary, easy to find/modify session code, follows React community conventions | Flat structure (all in src/components/) — harder to navigate as feature grows |
| SessionRepository adapter pattern | Enables future swap to real filesystem access without changing hooks/components | Inline mock data in store — requires refactoring when adding real data |
| TanStack Query for data + Zustand for UI | TanStack Query provides caching, loading/error states, deduplication for free. Zustand handles synchronous UI state (tabs). Clean separation. | Zustand for everything — loses caching/query semantics; TanStack Query for everything — poor fit for synchronous UI state |
| Plan.md parsing in repository layer | Parsed Task[] cached by TanStack Query alongside raw markdown. Single fetch returns both. |
Parsing in component — mixes data transformation with rendering, re-parses on every render |
| In-memory comments (not persisted) | Simplest implementation, validates the annotation UX without persistence complexity | localStorage — adds serialization complexity for a POC that may pivot |
| No router (Zustand-driven tabs) | Single-screen app with tabs doesn't need URL routing. Simpler architecture. | React Router — adds dependency and complexity for a single-screen POC |
| Max 5 tabs with oldest eviction | Prevents memory bloat, familiar VS Code pattern | Unlimited tabs — memory concerns, cluttered UI |
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Mock data diverges from real Copilot format | Medium | Medium — future adapter may need rework | Base mock data on actual ~/.copilot/session-state/ files examined during discovery |
| SessionRepository interface too narrow | Low | Medium — adding methods later is straightforward | Start minimal (list + detail), extend when real data requirements emerge |
| New visual identity delays POC | Medium | Low — functionality works regardless of styling | Build functionality first, restyle as final step |
| Existing layout components tightly coupled to current store | Low | Medium — may require prop refactoring | Audit each layout component's store dependencies before integration |
| TanStack Query staleTime: Infinity masks reactivity issues | Low | Low — only matters when moving to real data | Document that real implementation must configure appropriate staleTime |
apps/webat/Users/william/Work/Projects/Devitools/arandu/apps/webis the correct working directory for this POC- All existing
apps/webdependencies (@tanstack/react-query,react-markdown,remark-gfm,zustand,shadcn/ui) are at compatible versions and working - The existing layout components (ActivityBar, Sidebar, TabBar, MarkdownViewer, ReviewPanel) are functional and can be extended without major refactoring
- Mock data matching Copilot's
workspace.yaml+plan.mdformat is sufficient to validate the session browser concept - In-memory comment storage is acceptable for POC validation — persistence will be added in a future iteration
- Chrome/Edge (latest) is the target browser — no IE/Safari compatibility needed for POC
- The
plan-parser.tsonly needs to handle GFM-style- [ ]and- [x]checkboxes (not arbitrary task formats)
- Visual identity specifics: The "new visual identity" needs a design reference or style guide. The TechSpec assumes this will be provided during the restyling phase — functionality can proceed without it.
- Agent-agnostic session format: While the UI is designed to be agent-agnostic (via
agentTypefield), the specific session format for non-Copilot agents (Claude Code, Gemini CLI) is not yet defined. The mock data can includeagentType: 'generic'entries for validation.
| Standard | Compliance |
|---|---|
| TypeScript strict mode | Yes — all new code uses strict TypeScript with explicit types |
| React 18 patterns | Yes — functional components, hooks, no class components |
| shadcn/ui conventions | Yes — uses shadcn/ui primitives (ScrollArea, Badge, Button, etc.) |
| Tailwind CSS | Yes — all styling via Tailwind utility classes + CSS variables |
| Vitest testing | Yes — unit + integration tests using existing Vitest config |
| ESLint | Yes — follows existing eslint.config.js rules |
| Feature module pattern | Yes — self-contained src/features/sessions/ with clear public API |
| TanStack Query conventions | Yes — query keys as arrays, custom hooks wrapping useQuery |
| Zustand patterns | Yes — slice pattern for store extension, devtools middleware |