- Removed broken LaunchSwarmDialog (formula-based) from TopBar/LeftPanel - All Rocket buttons (TopBar, LeftPanel, DAG nodes, social cards) now open AssignmentPanel (archetype-based) which actually works - Every Rocket clears taskId first so assignMode && !taskId condition passes - Conversation button priority: taskId always shows conversation, not assign panel - Added TelemetryStrip: minimized right sidebar with status dots when non-telemetry panel (conversation/assignment) is active - Live feed has minimize button → restores last taskId or assignMode - DAG nodes: Signal icon → restores telemetry feed - Social button on DAG nodes: single router.push to avoid race (setView + setTaskId) - Fixed social card message button: opens right panel with drawer:closed (no popup) Co-Authored-By: Oz <oz-agent@warp.dev>
3384 lines
138 KiB
Text
3384 lines
138 KiB
Text
diff --git a/.beads/.gitignore b/.beads/.gitignore
|
|
index 0acd8c6..dba6914 100644
|
|
--- a/.beads/.gitignore
|
|
+++ b/.beads/.gitignore
|
|
@@ -1,37 +1,20 @@
|
|
-# SQLite databases
|
|
-*.db
|
|
-*.db?*
|
|
-*.db-journal
|
|
-*.db-wal
|
|
-*.db-shm
|
|
+# Dolt database (managed by Dolt, not git)
|
|
+dolt/
|
|
+dolt-access.lock
|
|
|
|
-# Daemon runtime files
|
|
-daemon.lock
|
|
-daemon.log
|
|
-daemon.pid
|
|
+# Runtime files
|
|
bd.sock
|
|
+bd.sock.startlock
|
|
sync-state.json
|
|
last-touched
|
|
|
|
# Local version tracking (prevents upgrade notification spam after git ops)
|
|
.local_version
|
|
|
|
-# Legacy database files
|
|
-db.sqlite
|
|
-bd.db
|
|
-
|
|
# Worktree redirect file (contains relative path to main repo's .beads/)
|
|
# Must not be committed as paths would be wrong in other clones
|
|
redirect
|
|
|
|
-# Merge artifacts (temporary files from 3-way merge)
|
|
-beads.base.jsonl
|
|
-beads.base.meta.json
|
|
-beads.left.jsonl
|
|
-beads.left.meta.json
|
|
-beads.right.jsonl
|
|
-beads.right.meta.json
|
|
-
|
|
# Sync state (local-only, per-machine)
|
|
# These files are machine-specific and should not be shared across clones
|
|
.sync.lock
|
|
@@ -39,6 +22,31 @@ beads.right.meta.json
|
|
sync_base.jsonl
|
|
export-state/
|
|
|
|
+# Ephemeral store (SQLite - wisps/molecules, intentionally not versioned)
|
|
+ephemeral.sqlite3
|
|
+ephemeral.sqlite3-journal
|
|
+ephemeral.sqlite3-wal
|
|
+ephemeral.sqlite3-shm
|
|
+
|
|
+# Legacy files (from pre-Dolt versions)
|
|
+*.db
|
|
+*.db?*
|
|
+*.db-journal
|
|
+*.db-wal
|
|
+*.db-shm
|
|
+db.sqlite
|
|
+bd.db
|
|
+daemon.lock
|
|
+daemon.log
|
|
+daemon-*.log.gz
|
|
+daemon.pid
|
|
+beads.base.jsonl
|
|
+beads.base.meta.json
|
|
+beads.left.jsonl
|
|
+beads.left.meta.json
|
|
+beads.right.jsonl
|
|
+beads.right.meta.json
|
|
+
|
|
# NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here.
|
|
# They would override fork protection in .git/info/exclude, allowing
|
|
# contributors to accidentally commit upstream issue databases.
|
|
diff --git a/.beads/config.yaml b/.beads/config.yaml
|
|
index ff8bc92..68ce17a 100644
|
|
--- a/.beads/config.yaml
|
|
+++ b/.beads/config.yaml
|
|
@@ -1,67 +1,2 @@
|
|
-# Beads Configuration File
|
|
-# This file configures default behavior for all bd commands in this repository
|
|
-# All settings can also be set via environment variables (BD_* prefix)
|
|
-# or overridden with command-line flags
|
|
-
|
|
-# Issue prefix for this repository (used by bd init)
|
|
-# If not set, bd init will auto-detect from directory name
|
|
-# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
|
|
-# issue-prefix: ""
|
|
-
|
|
-# Use no-db mode: load from JSONL, no SQLite, write back after each command
|
|
-# When true, bd will use .beads/issues.jsonl as the source of truth
|
|
-# instead of SQLite database
|
|
-# no-db: false
|
|
-
|
|
-# Disable daemon for RPC communication (forces direct database access)
|
|
-# no-daemon: false
|
|
-
|
|
-# Disable auto-flush of database to JSONL after mutations
|
|
-# no-auto-flush: false
|
|
-
|
|
-# Disable auto-import from JSONL when it's newer than database
|
|
-# no-auto-import: false
|
|
-
|
|
-# Enable JSON output by default
|
|
-# json: false
|
|
-
|
|
-# Default actor for audit trails (overridden by BD_ACTOR or --actor)
|
|
-# actor: ""
|
|
-
|
|
-# Path to database (overridden by BEADS_DB or --db)
|
|
-# db: ""
|
|
-
|
|
-# Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON)
|
|
-# auto-start-daemon: true
|
|
-
|
|
-# Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE)
|
|
-# flush-debounce: "5s"
|
|
-
|
|
-# Export events (audit trail) to .beads/events.jsonl on each flush/sync
|
|
-# When enabled, new events are appended incrementally using a high-water mark.
|
|
-# Use 'bd export --events' to trigger manually regardless of this setting.
|
|
-# events-export: false
|
|
-
|
|
-# Git branch for beads commits (bd sync will commit to this branch)
|
|
-# IMPORTANT: Set this for team projects so all clones use the same sync branch.
|
|
-# This setting persists across clones (unlike database config which is gitignored).
|
|
-# Can also use BEADS_SYNC_BRANCH env var for local override.
|
|
-# If not set, bd sync will require you to run 'bd config set sync.branch <branch>'.
|
|
-# sync-branch: "beads-sync"
|
|
-
|
|
-# Multi-repo configuration (experimental - bd-307)
|
|
-# Allows hydrating from multiple repositories and routing writes to the correct JSONL
|
|
-# repos:
|
|
-# primary: "." # Primary repo (where this database lives)
|
|
-# additional: # Additional repos to hydrate from (read-only)
|
|
-# - ~/beads-planning # Personal planning repo
|
|
-# - ~/work-planning # Work planning repo
|
|
-
|
|
-# Integration settings (access with 'bd config get/set')
|
|
-# These are stored in the database, not in this file:
|
|
-# - jira.url
|
|
-# - jira.project
|
|
-# - linear.url
|
|
-# - linear.api-key
|
|
-# - github.org
|
|
-# - github.repo
|
|
+sync:
|
|
+ mode: dolt-native
|
|
diff --git a/.beads/metadata.json b/.beads/metadata.json
|
|
index c787975..9de33ec 100644
|
|
--- a/.beads/metadata.json
|
|
+++ b/.beads/metadata.json
|
|
@@ -1,4 +1,8 @@
|
|
{
|
|
- "database": "beads.db",
|
|
- "jsonl_export": "issues.jsonl"
|
|
+ "database": "dolt",
|
|
+ "jsonl_export": "issues.jsonl",
|
|
+ "backend": "dolt",
|
|
+ "dolt_mode": "server",
|
|
+ "dolt_server_port": 3307,
|
|
+ "dolt_database": "beadboard"
|
|
}
|
|
\ No newline at end of file
|
|
diff --git a/AGENTS.md b/AGENTS.md
|
|
index 87860bf..8e21900 100644
|
|
--- a/AGENTS.md
|
|
+++ b/AGENTS.md
|
|
@@ -10,6 +10,8 @@ This repo is execution-first, evidence-first, and beads-driven.
|
|
4. Evidence before assertions: do not claim fixed/passing/done without fresh command output.
|
|
5. Keep language simple in user-facing labels and UI copy.
|
|
6. Reuse shared code paths/components; avoid one-off logic drift across pages.
|
|
+7. Treat BeadBoard as a multi-agent coordination + communication system first; optimize feature decisions for swarm execution clarity before cosmetic/layout preferences.
|
|
+8. Runtime UI route surface is query-driven from `/` (`view=social|graph|activity`); do not reintroduce direct App Router page sprawl without explicit approval.
|
|
|
|
## Quick Beads Workflow
|
|
|
|
@@ -63,15 +65,21 @@ npm run test
|
|
|
|
If UI changed, refresh screenshots and record artifact paths.
|
|
|
|
+## Runtime Surface Guardrails
|
|
+
|
|
+1. Keep the active runtime page surface minimal under `src/app`.
|
|
+2. Preserve deprecated/legacy page implementations in `reference/routes/**` when useful for reuse.
|
|
+3. Maintain backward-compatible redirects in `next.config.ts` when route contracts change.
|
|
+
|
|
## Realtime / Refresh Bug Triage Pattern
|
|
|
|
When status updates are stale or require refresh:
|
|
|
|
1. Verify source-of-truth parity (`bd show` vs app output).
|
|
2. Confirm read path prefers live BD data when needed.
|
|
-3. Confirm watcher inputs include DB + WAL + touch markers.
|
|
-4. Confirm SSE fallback compares mtime/timestamps, not only static file content.
|
|
-5. Add regression tests for watcher/events behavior.
|
|
+3. Confirm watcher coverage for active project scope roots and relevant agent/message files.
|
|
+4. Confirm SSE event flow and client subscription behavior across all active views.
|
|
+5. Add regression tests for watcher/events behavior and scope switching.
|
|
|
|
## Parallel Agent Pattern
|
|
|
|
@@ -106,6 +114,8 @@ Use parallel agents for independent beads.
|
|
- Confirm DB + WAL + touch markers are watched and SSE fallback uses mtime/timestamps.
|
|
7. Missing test registration:
|
|
- New test files must be included in `npm run test` script if the suite is explicitly enumerated.
|
|
+8. Documentation drift:
|
|
+ - Do not claim features in `README.md` that are not currently shipped, unless clearly labeled as roadmap.
|
|
|
|
## Session Completion (Landing the Plane)
|
|
|
|
@@ -136,3 +146,90 @@ Never claim:
|
|
- "closed"
|
|
|
|
unless you have run the proving command(s) in the current session and can cite results.
|
|
+
|
|
+<!-- BEGIN BEADS INTEGRATION -->
|
|
+## Issue Tracking with bd (beads)
|
|
+
|
|
+**IMPORTANT**: This project uses **bd (beads)** for ALL issue tracking. Do NOT use markdown TODOs, task lists, or other tracking methods.
|
|
+
|
|
+### Why bd?
|
|
+
|
|
+- Dependency-aware: Track blockers and relationships between issues
|
|
+- Git-friendly: Auto-syncs to JSONL for version control
|
|
+- Agent-optimized: JSON output, ready work detection, discovered-from links
|
|
+- Prevents duplicate tracking systems and confusion
|
|
+
|
|
+### Quick Start
|
|
+
|
|
+**Check for ready work:**
|
|
+
|
|
+```bash
|
|
+bd ready --json
|
|
+```
|
|
+
|
|
+**Create new issues:**
|
|
+
|
|
+```bash
|
|
+bd create "Issue title" --description="Detailed context" -t bug|feature|task -p 0-4 --json
|
|
+bd create "Issue title" --description="What this issue is about" -p 1 --deps discovered-from:bd-123 --json
|
|
+```
|
|
+
|
|
+**Claim and update:**
|
|
+
|
|
+```bash
|
|
+bd update bd-42 --status in_progress --json
|
|
+bd update bd-42 --priority 1 --json
|
|
+```
|
|
+
|
|
+**Complete work:**
|
|
+
|
|
+```bash
|
|
+bd close bd-42 --reason "Completed" --json
|
|
+```
|
|
+
|
|
+### Issue Types
|
|
+
|
|
+- `bug` - Something broken
|
|
+- `feature` - New functionality
|
|
+- `task` - Work item (tests, docs, refactoring)
|
|
+- `epic` - Large feature with subtasks
|
|
+- `chore` - Maintenance (dependencies, tooling)
|
|
+
|
|
+### Priorities
|
|
+
|
|
+- `0` - Critical (security, data loss, broken builds)
|
|
+- `1` - High (major features, important bugs)
|
|
+- `2` - Medium (default, nice-to-have)
|
|
+- `3` - Low (polish, optimization)
|
|
+- `4` - Backlog (future ideas)
|
|
+
|
|
+### Workflow for AI Agents
|
|
+
|
|
+1. **Check ready work**: `bd ready` shows unblocked issues
|
|
+2. **Claim your task**: `bd update <id> --status in_progress`
|
|
+3. **Work on it**: Implement, test, document
|
|
+4. **Discover new work?** Create linked issue:
|
|
+ - `bd create "Found bug" --description="Details about what was found" -p 1 --deps discovered-from:<parent-id>`
|
|
+5. **Complete**: `bd close <id> --reason "Done"`
|
|
+
|
|
+### Auto-Sync
|
|
+
|
|
+bd automatically syncs with git:
|
|
+
|
|
+- Exports to `.beads/issues.jsonl` after changes (5s debounce)
|
|
+- Imports from JSONL when newer (e.g., after `git pull`)
|
|
+- No manual export/import needed!
|
|
+
|
|
+### Important Rules
|
|
+
|
|
+- ✅ Use bd for ALL task tracking
|
|
+- ✅ Always use `--json` flag for programmatic use
|
|
+- ✅ Link discovered work with `discovered-from` dependencies
|
|
+- ✅ Check `bd ready` before asking "what should I work on?"
|
|
+- ❌ Do NOT create markdown TODO lists
|
|
+- ❌ Do NOT use external issue trackers
|
|
+- ❌ Do NOT duplicate tracking systems
|
|
+
|
|
+For more details, see README.md and docs/QUICKSTART.md.
|
|
+
|
|
+<!-- END BEADS INTEGRATION -->
|
|
diff --git a/README.md b/README.md
|
|
index afe7406..76805ea 100644
|
|
--- a/README.md
|
|
+++ b/README.md
|
|
@@ -1,67 +1,205 @@
|
|
# BeadBoard
|
|
+**Work in Progress, please contribute!**
|
|
+**BeadBoard is a multi-agent swarm coordination system built on [Beads](https://github.com/steveyegge/beads) inspired by [Gastown](https://github.com/steveyegge/gastown).** Thanks [Steve Yegge](https://github.com/steveyegge)!
|
|
|
|
-**The Windows-native Control Center for [Beads](https://github.com/steveyegge/beads).**
|
|
-
|
|
-BeadBoard is a high-performance local dashboard for managing your software development tasks. Built on the Beads protocol, it provides a unified, visualization-rich interface over your distributed project landscape.
|
|
-
|
|
-## 🚀 Why BeadBoard?
|
|
-Most task managers are siloes. BeadBoard is a lens over your source code.
|
|
-- **Source of Truth**: Reads directly from `.beads/issues.jsonl` in your repo. No database sync skew.
|
|
-- **Windows Optimized**: Built from the ground up to handle Windows paths, drive letters, and filesystem performance.
|
|
-- **Zero Latency**: Optimistic UI updates make interactions feel instant.
|
|
-
|
|
-## ✨ Core Features
|
|
-
|
|
-### 1. Multi-Project Registry & Scanner
|
|
-Stop context switching between repos.
|
|
-- **Project Registry**: Persist your favorite project roots for one-click access.
|
|
-- **Auto-Discovery**: Built-in filesystem scanner finds Bead-enabled projects across your drives.
|
|
-- **Aggregate Mode**: View tasks from *all* registered projects in a single unified board.
|
|
-
|
|
-### 2. Interactive Kanban Dashboard (`/`)
|
|
-Manage your flow state.
|
|
-
|
|
-- **Live Updates**: Boards refresh automatically when the underlying JSONL files change (e.g., via CLI).
|
|
-- **Progressive Disclosure**: Task details, metadata, and relations are tucked away until you need them.
|
|
-- **Smart Filtering**: Filter by priority, assignee, status, or full-text search across thousands of beads.
|
|
-
|
|
-### 3. Dependency Graph Explorer (`/graph`)
|
|
-Understand the "Why" and "What's Next".
|
|
-
|
|
-- **Epic-Centric Layout**: Automatically groups tasks by Epic for logical clustering.
|
|
-- **True DAG Visualization**: Uses Dagre layout engine to enforce a strict Left-to-Right dependency flow.
|
|
- - *Left*: Incoming Blockers
|
|
- - *Center*: Focus Task
|
|
- - *Right*: Unlocks / Downstream
|
|
-- **Focus Mode**: Minimizable dependency strip and deep-linking support for sharing exact views.
|
|
-- **Smart Metadata**: See bead counts, priorities, and status health at a glance.
|
|
-
|
|
-### 4. Agent Sessions Hub (`/sessions`)
|
|
-Coordinate multi-agent workflows with social-dense visibility.
|
|
-- **Epic-Grouped Task Feed**: Tasks organized by parent Epic with session state indicators (active, reviewing, needs_input, stale).
|
|
-- **Cross-Agent Communication**: Built-in messaging with HANDOFF, BLOCKED, and INFO categories.
|
|
-- **Agent Productivity Metrics**: Real-time stats showing active tasks, completions, and recent wins.
|
|
-- **Derived Activity Engine**: O(N) snapshot diffing computes project history on-demand without separate event storage.
|
|
-- **`bb agent` CLI Integration**: Visualizes data from agent registry, reservations, and mailboxes.
|
|
-
|
|
-### 5. Chronological Timeline (`/timeline`)
|
|
-Real-time activity feed for all project events.
|
|
-- **Live Updates**: Server-Sent Events stream changes instantly.
|
|
-- **Date Grouping**: Events organized by day (Today, Yesterday, etc.).
|
|
-- **Polymorphic Cards**: Distinct visual styles for Status, Lifecycle, and Diff events.
|
|
-- **History Buffer**: Recent events preserved across server restarts.
|
|
-
|
|
-## 🛠️ Stack
|
|
-- **Framework**: Next.js 15 (App Router)
|
|
-- **UI Engine**: React 19 + Framer Motion
|
|
-- **Styling**: Tailwind CSS + Custom Design System
|
|
-- **Type Safety**: Strict TypeScript
|
|
-
|
|
-## ⚡ Quick Start
|
|
-1. **Install**: `npm install`
|
|
-2. **Run**: `npm run dev`
|
|
-3. **Explore**: Open `http://localhost:3000`
|
|
-
|
|
-## 🤝 Contribution
|
|
-- **Typecheck**: `npm run typecheck`
|
|
-- **Test**: `npm run test`
|
|
+BB is a visual operations layer for running agent teams against real dependency-constrained work.
|
|
+
|
|
+---
|
|
+
|
|
+## What This App Is
|
|
+BeadBoard is not just a task board. It is an execution system for coordinating agents around shared Beads workflows:
|
|
+
|
|
+- Agent-to-agent communication with explicit categories (`HANDOFF`, `BLOCKED`, `DECISION`, `INFO`)
|
|
+- Conversation threads merged from activity events, agent messages, and local interactions
|
|
+- Graph/topology context for deciding what should move next
|
|
+- Global project scope switching across single and aggregate workspaces
|
|
+- Swarm orchestration with archetypes/templates and assignment controls
|
|
+
|
|
+---
|
|
+
|
|
+
|
|
+
|
|
+---
|
|
+
|
|
+## Core Features
|
|
+
|
|
+### 1. Agent Communication System
|
|
+- Structured message lifecycle: `send`, `inbox`, `read`, `ack`
|
|
+- Message states: `unread`, `read`, `acked`
|
|
+- Per-task conversation threads combining:
|
|
+ - activity events
|
|
+ - agent mail
|
|
+ - local bd interactions
|
|
+- Required acknowledgment semantics for high-signal categories (`HANDOFF`, `BLOCKED`)
|
|
+
|
|
+### 2. Swarm Coordination Surface
|
|
+- Agent Pool Monitor with:
|
|
+ - Archetypes
|
|
+ - Templates
|
|
+ - Needs Agent queue
|
|
+ - Pre-assigned queue
|
|
+ - Squad roster
|
|
+- Assignment workflow through the graph workspace and right panel
|
|
+
|
|
+
|
|
+### 3. Graph + Dependency Topology
|
|
+- DAG-oriented graph workspace for execution decisions
|
|
+- Task/dependency tab modes for different planning lenses
|
|
+- Blocker/unblock context surfaced directly in task cards
|
|
+- Graph analysis support (cycle and blocked-chain context)
|
|
+
|
|
+
|
|
+### 4. Global Project Scope + Scanner
|
|
+- Project registry and scanner-backed discovery
|
|
+- Single-project and aggregate modes
|
|
+- Runtime scope switching without leaving the primary workspace
|
|
+
|
|
+### 5. Realtime Operations Layer
|
|
+- Live updates via watchers + SSE
|
|
+- Activity stream integration with session/task context
|
|
+- Mutation/writeback feedback integrated into the same operational surface
|
|
+
|
|
+---
|
|
+
|
|
+## Runtime Surface
|
|
+
|
|
+### Active Route
|
|
+- `/`
|
|
+
|
|
+### View Modes
|
|
+- `/?view=social`
|
|
+- `/?view=graph`
|
|
+- `/?view=activity`
|
|
+
|
|
+### Compatibility Redirects
|
|
+- `/graph` -> `/?view=graph`
|
|
+- `/sessions` -> `/?view=social`
|
|
+- `/timeline` -> `/?view=activity`
|
|
+- `/mockup` -> `/`
|
|
+
|
|
+### Archived Route Vault
|
|
+Legacy route implementations are preserved in `reference/routes/**` and excluded from active runtime validation scope.
|
|
+
|
|
+---
|
|
+
|
|
+## Install
|
|
+
|
|
+### Prerequisites
|
|
+- Node.js `18.18+` (Node `20 LTS` recommended)
|
|
+- npm
|
|
+
|
|
+### Clone + Install
|
|
+```bash
|
|
+git clone https://github.com/zenchantlive/beadboard.git
|
|
+cd beadboard
|
|
+npm install
|
|
+```
|
|
+
|
|
+---
|
|
+
|
|
+## Quick Start
|
|
+
|
|
+```bash
|
|
+npm run dev
|
|
+```
|
|
+
|
|
+Open:
|
|
+
|
|
+```text
|
|
+http://localhost:3000
|
|
+```
|
|
+
|
|
+---
|
|
+
|
|
+## Configuration
|
|
+No external service is required for core local usage.
|
|
+
|
|
+Runtime behavior is driven by:
|
|
+- Local Beads project data
|
|
+- Registered/scanned project roots
|
|
+- URL query state (`view`, `task`, `swarm`, `agent`, `epic`, `graphTab`, panel state)
|
|
+
|
|
+---
|
|
+
|
|
+## Operating Flow
|
|
+
|
|
+### 1. Coordinate through Graph + Pool
|
|
+Open `/?view=graph`, inspect dependency topology, and drive assignment from the pool panel.
|
|
+
|
|
+### 2. Communicate in Context
|
|
+Open a task thread to read merged conversation context and process message acknowledgments.
|
|
+
|
|
+### 3. Switch Scope as Work Expands
|
|
+Use registry/scanner controls to move between local and aggregate project scope.
|
|
+
|
|
+### 4. Track Live Signal
|
|
+Use social/activity views to monitor execution movement and operational events.
|
|
+
|
|
+---
|
|
+
|
|
+## Roadmap Notes
|
|
+- Cross-view assign controls in all major views.
|
|
+- Social naming/UX evolution (including possible shift toward “swim” terminology).
|
|
+- Continued expansion of global project config/scanner workflows.
|
|
+
|
|
+---
|
|
+
|
|
+## Scripts
|
|
+
|
|
+```bash
|
|
+npm run dev
|
|
+npm run build
|
|
+npm run start
|
|
+npm run typecheck
|
|
+npm run lint
|
|
+npm run test
|
|
+npm run video
|
|
+npm run video:render
|
|
+npm run video:thumbnail
|
|
+```
|
|
+
|
|
+---
|
|
+
|
|
+## Architecture
|
|
+- **Frontend**: Next.js App Router + React 19 + Tailwind + Framer Motion + Radix
|
|
+- **Graph stack**: XYFlow + Dagre
|
|
+- **Core domain**: Beads issue model, graph/kanban/session/social builders
|
|
+- **Coordination layer**: agent mail + session communication + swarm orchestration state
|
|
+- **Realtime**: watchers + SSE + snapshot differ + activity persistence
|
|
+- **Validation/typing**: strict TypeScript + Zod contracts where applicable
|
|
+
|
|
+---
|
|
+
|
|
+## Project Structure
|
|
+
|
|
+```text
|
|
+src/
|
|
+ app/
|
|
+ page.tsx # active runtime route
|
|
+ api/ # runtime API routes
|
|
+ components/
|
|
+ shared/ graph/ social/ activity/ sessions/ swarm/ kanban/
|
|
+ hooks/
|
|
+ lib/
|
|
+reference/
|
|
+ routes/ # archived route implementations
|
|
+```
|
|
+
|
|
+---
|
|
+
|
|
+## Contributing
|
|
+1. Keep active runtime pages in `src/app` minimal.
|
|
+2. Promote reusable logic into `src/lib`, `src/components`, `src/hooks`.
|
|
+3. Archive non-runtime route experiments in `reference/routes`.
|
|
+4. Run quality gates before merge:
|
|
+
|
|
+```bash
|
|
+npm run typecheck
|
|
+npm run lint
|
|
+npm run test
|
|
+```
|
|
+
|
|
+---
|
|
+
|
|
+## License
|
|
+MIT
|
|
diff --git a/docs/protocols/operative-protocol-v1.md b/docs/protocols/operative-protocol-v1.md
|
|
index 73b0c23..a8522e9 100644
|
|
--- a/docs/protocols/operative-protocol-v1.md
|
|
+++ b/docs/protocols/operative-protocol-v1.md
|
|
@@ -1,7 +1,7 @@
|
|
# Operative Protocol v1 (Session Constitution)
|
|
|
|
Date: 2026-02-14
|
|
-Status: Approved for implementation
|
|
+Status: Approved for implementation (superseded for migration planning by `docs/protocols/2026-02-28-bd-audit-coordination-schema.md`)
|
|
Scope: `bb-u6f.6.1`
|
|
Applies to: `bb-u6f.6.2`, `bb-u6f.6.3`, `bb-u6f.6.4`, `bb-u6f.6.5`
|
|
|
|
@@ -15,6 +15,10 @@ Boundaries:
|
|
3. No direct writes to `.beads/issues.jsonl`.
|
|
4. User-facing labels must stay plain language.
|
|
|
|
+Migration note:
|
|
+1. New work should target the `coord.v1` `bd audit` event model documented in `docs/protocols/2026-02-28-bd-audit-coordination-schema.md`.
|
|
+2. `bb` coordination semantics are legacy compatibility behavior pending removal after migration sign-off.
|
|
+
|
|
## 2. Normative Language
|
|
|
|
1. MUST: required behavior.
|
|
diff --git a/eslint.config.mjs b/eslint.config.mjs
|
|
index 29a92b5..5ae70d1 100644
|
|
--- a/eslint.config.mjs
|
|
+++ b/eslint.config.mjs
|
|
@@ -14,6 +14,7 @@ const eslintConfig = [
|
|
'.next/**',
|
|
'.agents/**',
|
|
'skills/**',
|
|
+ 'reference/**',
|
|
'next-env.d.ts',
|
|
],
|
|
},
|
|
diff --git a/next.config.ts b/next.config.ts
|
|
index 0076578..05d1bdb 100644
|
|
--- a/next.config.ts
|
|
+++ b/next.config.ts
|
|
@@ -4,6 +4,30 @@ import path from 'node:path';
|
|
const nextConfig: NextConfig = {
|
|
reactStrictMode: true,
|
|
outputFileTracingRoot: path.join(process.cwd()),
|
|
+ async redirects() {
|
|
+ return [
|
|
+ {
|
|
+ source: '/graph',
|
|
+ destination: '/?view=graph',
|
|
+ permanent: false,
|
|
+ },
|
|
+ {
|
|
+ source: '/sessions',
|
|
+ destination: '/?view=social',
|
|
+ permanent: false,
|
|
+ },
|
|
+ {
|
|
+ source: '/timeline',
|
|
+ destination: '/?view=activity',
|
|
+ permanent: false,
|
|
+ },
|
|
+ {
|
|
+ source: '/mockup',
|
|
+ destination: '/',
|
|
+ permanent: false,
|
|
+ },
|
|
+ ];
|
|
+ },
|
|
webpack(config, { dev }) {
|
|
if (dev) {
|
|
// Avoid intermittent Windows ENOENT errors from webpack filesystem pack cache.
|
|
diff --git a/package.json b/package.json
|
|
index 972b5e6..3f49a2c 100644
|
|
--- a/package.json
|
|
+++ b/package.json
|
|
@@ -9,7 +9,7 @@
|
|
"start": "next start",
|
|
"lint": "eslint .",
|
|
"typecheck": "tsc --noEmit",
|
|
- "test": "node --test tests/bootstrap.test.mjs && node --import tsx --test tests/components/shared/base-card.test.tsx && node --import tsx --test tests/components/shared/agent-avatar.test.tsx && node --import tsx --test tests/components/sessions/sessions-header.test.ts && node --import tsx --test tests/components/sessions/agent-station-logic.test.ts && node --import tsx --test tests/lib/parser.test.ts && node --import tsx --test tests/lib/pathing.test.ts && node --import tsx --test tests/components/shared/left-panel.test.tsx && node --import tsx --test tests/components/shared/top-bar.test.tsx && node --import tsx --test tests/components/shared/mobile-nav.test.tsx && node --import tsx --test tests/components/swarm/swarm-card.test.tsx && node --import tsx --test tests/hooks/url-state-integration.test.ts && node --import tsx --test tests/hooks/use-graph-analysis.test.ts && node --import tsx --test tests/components/graph/smart-dag.test.tsx && node --import tsx --test tests/components/unified-shell.test.tsx && node --import tsx --test tests/components/graph/graph-node-labels.test.tsx && node --import tsx --test tests/components/graph/graph-node-assign.test.tsx",
|
|
+ "test": "node --test tests/bootstrap.test.mjs && node --import tsx --test tests/components/shared/base-card.test.tsx && node --import tsx --test tests/components/shared/agent-avatar.test.tsx && node --import tsx --test tests/components/sessions/sessions-header.test.ts && node --import tsx --test tests/components/sessions/agent-station-logic.test.ts && node --import tsx --test tests/lib/parser.test.ts && node --import tsx --test tests/lib/pathing.test.ts && node --import tsx --test tests/components/shared/left-panel.test.tsx && node --import tsx --test tests/components/shared/top-bar.test.tsx && node --import tsx --test tests/components/shared/mobile-nav.test.tsx && node --import tsx --test tests/components/swarm/swarm-card.test.tsx && node --import tsx --test tests/hooks/url-state-integration.test.ts && node --import tsx --test tests/hooks/use-graph-analysis.test.ts && node --import tsx --test tests/components/graph/smart-dag.test.tsx && node --import tsx --test tests/components/unified-shell.test.tsx && node --import tsx --test tests/components/graph/graph-node-labels.test.tsx && node --import tsx --test tests/components/graph/graph-node-assign.test.tsx && node --import tsx --test tests/lib/coord-schema.test.ts && node --import tsx --test tests/lib/coord-events.test.ts && node --import tsx --test tests/api/coord-events-route.test.ts && node --import tsx --test tests/lib/coord-projections-inbox.test.ts && node --import tsx --test tests/lib/coord-projections-reservations.test.ts && node --import tsx --test tests/components/sessions/conversation-drawer-coord.test.tsx",
|
|
"video": "remotion preview src/video/index.ts",
|
|
"video:render": "remotion render src/video/index.ts Main out/video.mp4",
|
|
"video:thumbnail": "remotion still src/video/index.ts Main out/thumbnail.png --frame=60"
|
|
diff --git a/src/app/api/beads/_shared.ts b/src/app/api/beads/_shared.ts
|
|
index e5f5c50..b1399fa 100644
|
|
--- a/src/app/api/beads/_shared.ts
|
|
+++ b/src/app/api/beads/_shared.ts
|
|
@@ -29,7 +29,7 @@ export async function handleMutationRequest(request: Request, operation: Mutatio
|
|
const payload = validateMutationPayload(operation, body);
|
|
const result = await executeMutation(operation, payload);
|
|
|
|
- const status = result.ok ? 200 : result.error?.classification === 'not_found' ? 404 : 400;
|
|
+ const status = result.ok ? 200 : result.error?.classification === 'not_found' ? 503 : 400;
|
|
return NextResponse.json(result, { status });
|
|
} catch (error) {
|
|
if (error instanceof MutationValidationError) {
|
|
diff --git a/src/app/api/sessions/[beadId]/conversation/route.ts b/src/app/api/sessions/[beadId]/conversation/route.ts
|
|
index bac7b34..6548b47 100644
|
|
--- a/src/app/api/sessions/[beadId]/conversation/route.ts
|
|
+++ b/src/app/api/sessions/[beadId]/conversation/route.ts
|
|
@@ -22,7 +22,7 @@ export async function GET(
|
|
const activity = history.filter((e: ActivityEvent) => e.beadId === beadId);
|
|
|
|
// 2. Get communication for this bead
|
|
- const summary = await getCommunicationSummary();
|
|
+ const summary = await getCommunicationSummary(projectRoot);
|
|
const messages = summary.messages.filter((m: AgentMessage) => m.bead_id === beadId);
|
|
|
|
// 3. Get local bd interactions via CLI
|
|
@@ -55,4 +55,4 @@ export async function GET(
|
|
console.error('[API/Sessions/Conversation] Failed:', error);
|
|
return NextResponse.json({ ok: false, error: String(error) }, { status: 500 });
|
|
}
|
|
-}
|
|
\ No newline at end of file
|
|
+}
|
|
diff --git a/src/app/api/sessions/route.ts b/src/app/api/sessions/route.ts
|
|
index e9a3395..b018d92 100644
|
|
--- a/src/app/api/sessions/route.ts
|
|
+++ b/src/app/api/sessions/route.ts
|
|
@@ -41,9 +41,9 @@ export async function GET(request: Request): Promise<Response> {
|
|
try {
|
|
const issues = await readIssuesFromDisk({ projectRoot, preferBd: true });
|
|
const activity = activityEventBus.getHistory(projectRoot);
|
|
- const communication = await getCommunicationSummary();
|
|
+ const communication = await getCommunicationSummary(projectRoot);
|
|
const livenessMap = await getAgentLivenessMap(projectRoot, activity);
|
|
- const incursions = await calculateIncursions();
|
|
+ const incursions = await calculateIncursions(projectRoot, livenessMap);
|
|
const agentsResult = await listAgents({}, { projectRoot });
|
|
|
|
const feed = buildSessionTaskFeed(issues, activity, communication, livenessMap);
|
|
diff --git a/src/app/api/swarm/list/route.ts b/src/app/api/swarm/list/route.ts
|
|
index 37aec33..a26ab08 100644
|
|
--- a/src/app/api/swarm/list/route.ts
|
|
+++ b/src/app/api/swarm/list/route.ts
|
|
@@ -21,9 +21,10 @@ export async function GET(request: Request): Promise<Response> {
|
|
});
|
|
|
|
if (!result.success) {
|
|
+ const status = result.classification === 'not_found' ? 503 : 400;
|
|
return NextResponse.json(
|
|
{ ok: false, error: { classification: result.classification ?? 'unknown', message: result.error ?? result.stderr } },
|
|
- { status: 400 },
|
|
+ { status },
|
|
);
|
|
}
|
|
|
|
diff --git a/src/app/api/swarm/status/route.ts b/src/app/api/swarm/status/route.ts
|
|
index 4d7e3b8..6e9a138 100644
|
|
--- a/src/app/api/swarm/status/route.ts
|
|
+++ b/src/app/api/swarm/status/route.ts
|
|
@@ -29,9 +29,10 @@ export async function GET(request: Request): Promise<Response> {
|
|
});
|
|
|
|
if (!result.success) {
|
|
+ const status = result.classification === 'not_found' ? 503 : 400;
|
|
return NextResponse.json(
|
|
{ ok: false, error: { classification: result.classification ?? 'unknown', message: result.error ?? result.stderr } },
|
|
- { status: 400 },
|
|
+ { status },
|
|
);
|
|
}
|
|
|
|
diff --git a/src/app/globals.css b/src/app/globals.css
|
|
index a84e8b4..c4d448f 100644
|
|
--- a/src/app/globals.css
|
|
+++ b/src/app/globals.css
|
|
@@ -6,27 +6,27 @@
|
|
|
|
:root {
|
|
/* ========== VISUAL-TRUTH UI TOKEN CONTRACT (bb-vt.1.1) ========== */
|
|
- /* Warm charcoal aurora palette - DISTINCT LAYERS */
|
|
- --ui-bg-app: #181716; /* Darkest - page background */
|
|
- --ui-bg-header: #131211; /* Header - darker than sidebar */
|
|
- --ui-bg-shell: #1f1e1d; /* Sidebar - distinct from main */
|
|
- --ui-bg-panel: #282725; /* Panels/cards within sidebar */
|
|
- --ui-bg-main: #242322; /* Main content area - distinct */
|
|
- --ui-bg-card: #302e2c; /* Cards - lightest layer */
|
|
- --ui-bg-elevated: #3a3836; /* Elevated/selected elements */
|
|
-
|
|
- --ui-border-soft: rgba(180, 175, 165, 0.2);
|
|
- --ui-border-strong: rgba(180, 175, 165, 0.35);
|
|
-
|
|
- --ui-text-primary: #f0eeea;
|
|
- --ui-text-muted: #a8a49a;
|
|
-
|
|
- --ui-accent-ready: #35d98f;
|
|
- --ui-accent-blocked: #ff4c72;
|
|
- --ui-accent-warning: #ffb24a;
|
|
- --ui-accent-info: #35c9ff;
|
|
- --ui-accent-action-green: #35d98f;
|
|
- --ui-accent-action-red: #ff4c72;
|
|
+ /* Map legacy --ui-* tokens to theme tokens so all themes work consistently */
|
|
+ --ui-bg-app: var(--surface-backdrop);
|
|
+ --ui-bg-header: var(--surface-primary);
|
|
+ --ui-bg-shell: var(--surface-primary);
|
|
+ --ui-bg-panel: var(--surface-secondary);
|
|
+ --ui-bg-main: var(--surface-secondary);
|
|
+ --ui-bg-card: var(--surface-elevated);
|
|
+ --ui-bg-elevated: var(--surface-elevated);
|
|
+
|
|
+ --ui-border-soft: var(--border-subtle);
|
|
+ --ui-border-strong: var(--border-default);
|
|
+
|
|
+ --ui-text-primary: var(--text-primary);
|
|
+ --ui-text-muted: var(--text-tertiary);
|
|
+
|
|
+ --ui-accent-ready: var(--accent-success);
|
|
+ --ui-accent-blocked: var(--accent-danger);
|
|
+ --ui-accent-warning: var(--accent-warning);
|
|
+ --ui-accent-info: var(--accent-info);
|
|
+ --ui-accent-action-green: var(--accent-success);
|
|
+ --ui-accent-action-red: var(--accent-danger);
|
|
|
|
--ui-font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
--ui-font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
|
|
@@ -43,30 +43,25 @@
|
|
--color-accent-teal: var(--ui-accent-info);
|
|
|
|
--color-text-primary: var(--ui-text-primary);
|
|
- --color-text-secondary: #c4cfdb;
|
|
+ --color-text-secondary: var(--text-secondary);
|
|
--color-text-muted: var(--ui-text-muted);
|
|
- --color-text-on-primary: #10161d;
|
|
+ --color-text-on-primary: var(--text-inverse);
|
|
|
|
--color-border-default: var(--ui-border-strong);
|
|
--color-border-subtle: var(--ui-border-soft);
|
|
|
|
/* Status colors */
|
|
- --status-open: var(--ui-accent-info);
|
|
- --status-ready: var(--ui-accent-ready);
|
|
- --status-in-progress: var(--ui-accent-warning);
|
|
- --status-progress: var(--ui-accent-warning);
|
|
- --status-blocked: var(--ui-accent-blocked);
|
|
- --status-blocked-earthy: var(--ui-accent-blocked);
|
|
- --status-closed: #7f8b98;
|
|
- --status-closed-earthy: #7f8b98;
|
|
- --status-deferred: #7f8b98;
|
|
+ --status-open: var(--accent-info);
|
|
+ --status-progress: var(--status-in-progress);
|
|
+ --status-blocked-earthy: var(--status-blocked);
|
|
+ --status-closed-earthy: var(--status-closed);
|
|
|
|
/* Liveness colors */
|
|
--liveness-active: var(--ui-accent-ready);
|
|
--liveness-stale: var(--ui-accent-warning);
|
|
--liveness-stuck: var(--ui-accent-action-red);
|
|
--liveness-dead: var(--ui-accent-action-red);
|
|
- --liveness-idle: #7f8b98;
|
|
+ --liveness-idle: var(--text-tertiary);
|
|
|
|
/* Agent Role Colors */
|
|
--agent-role-ui: #6B9BD2;
|
|
@@ -83,10 +78,10 @@
|
|
--priority-p4: #64748b;
|
|
|
|
/* Blocks/Unlocks Section Colors */
|
|
- --color-blocks-bg: rgba(212, 165, 116, 0.1);
|
|
- --color-unlocks-bg: rgba(229, 115, 115, 0.1);
|
|
- --color-blocks-border: rgba(212, 165, 116, 0.2);
|
|
- --color-unlocks-border: rgba(229, 115, 115, 0.2);
|
|
+ --color-blocks-bg: var(--status-in-progress);
|
|
+ --color-unlocks-bg: var(--status-blocked);
|
|
+ --color-blocks-border: color-mix(in srgb, var(--accent-warning) 35%, transparent);
|
|
+ --color-unlocks-border: color-mix(in srgb, var(--accent-danger) 35%, transparent);
|
|
|
|
/* ========== RADI ========== */
|
|
--radius-sm: 0.375rem;
|
|
@@ -98,10 +93,8 @@
|
|
--radius-pill: 9999px;
|
|
|
|
/* ========== SHADOWS ========== */
|
|
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
- --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
- --shadow-soft-lg: 0 10px 30px -10px rgba(0, 0, 0, 0.3);
|
|
- --shadow-soft-xl: 0 20px 40px -10px rgba(0, 0, 0, 0.4);
|
|
+ --shadow-soft-lg: var(--shadow-md);
|
|
+ --shadow-soft-xl: var(--shadow-lg);
|
|
|
|
/* ========== TYPOGRAPHY ========== */
|
|
--font-ui-stack: var(--ui-font-sans);
|
|
@@ -139,13 +132,12 @@
|
|
--sidebar-right-width: 17.5rem;
|
|
--topbar-height: 3.75rem;
|
|
|
|
- --glass-base: linear-gradient(180deg,
|
|
- color-mix(in srgb, var(--ui-bg-card) 72%, black),
|
|
- color-mix(in srgb, var(--ui-bg-panel) 78%, black));
|
|
- --edge-top: color-mix(in srgb, var(--ui-border-strong) 80%, white 20%);
|
|
- --edge-bottom: color-mix(in srgb, var(--ui-border-soft) 75%, black 25%);
|
|
- --elevation-ambient: 0 20px 40px -16px rgba(0, 0, 0, 0.78);
|
|
- --elevation-tight: 0 10px 24px -12px rgba(0, 0, 0, 0.7);
|
|
+ --glass-base: linear-gradient(180deg, var(--surface-elevated), var(--surface-secondary));
|
|
+ --edge-side: var(--ui-border-soft);
|
|
+ --edge-top: color-mix(in srgb, var(--ui-border-strong) 88%, white 12%);
|
|
+ --edge-bottom: color-mix(in srgb, var(--ui-border-soft) 88%, black 12%);
|
|
+ --elevation-ambient: var(--shadow-lg);
|
|
+ --elevation-tight: var(--shadow-md);
|
|
|
|
/* ========== LEGACY COMPATIBILITY TOKENS ========== */
|
|
/* For existing components that reference these */
|
|
@@ -181,7 +173,7 @@ body {
|
|
|
|
* {
|
|
scrollbar-width: thin;
|
|
- scrollbar-color: rgba(148, 163, 184, 0.35) rgba(255, 255, 255, 0.02);
|
|
+ scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
|
}
|
|
|
|
*::-webkit-scrollbar {
|
|
@@ -190,18 +182,18 @@ body {
|
|
}
|
|
|
|
*::-webkit-scrollbar-track {
|
|
- background: rgba(255, 255, 255, 0.02);
|
|
+ background: var(--scrollbar-track);
|
|
border-radius: 9999px;
|
|
}
|
|
|
|
*::-webkit-scrollbar-thumb {
|
|
- background: linear-gradient(180deg, rgba(156, 163, 175, 0.55), rgba(107, 114, 128, 0.45));
|
|
- border: 1px solid rgba(255, 255, 255, 0.1);
|
|
+ background: var(--scrollbar-thumb);
|
|
+ border: 1px solid color-mix(in srgb, var(--scrollbar-thumb) 70%, transparent);
|
|
border-radius: 9999px;
|
|
}
|
|
|
|
*::-webkit-scrollbar-thumb:hover {
|
|
- background: linear-gradient(180deg, rgba(186, 194, 209, 0.72), rgba(124, 136, 156, 0.62));
|
|
+ background: var(--scrollbar-thumb-hover);
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
@@ -210,12 +202,12 @@ body {
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
- background: rgba(148, 163, 184, 0.15);
|
|
+ background: var(--scrollbar-thumb);
|
|
border-radius: 20px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
- background: rgba(148, 163, 184, 0.3);
|
|
+ background: var(--scrollbar-thumb-hover);
|
|
}
|
|
|
|
.no-scrollbar::-webkit-scrollbar {
|
|
@@ -229,14 +221,14 @@ body {
|
|
|
|
|
|
.workflow-card {
|
|
- border: 1px solid rgba(255, 255, 255, 0.08);
|
|
- border-top-color: rgba(255, 255, 255, 0.24);
|
|
- border-bottom-color: rgba(0, 0, 0, 0.9);
|
|
- background: linear-gradient(180deg, rgba(42, 44, 52, 0.6) 0%, rgba(22, 23, 28, 0.6) 100%);
|
|
+ border: 1px solid var(--border-default);
|
|
+ border-top-color: color-mix(in srgb, var(--border-strong) 88%, white 12%);
|
|
+ border-bottom-color: color-mix(in srgb, var(--border-subtle) 88%, black 12%);
|
|
+ background: linear-gradient(180deg, var(--surface-quaternary) 0%, var(--surface-secondary) 100%);
|
|
box-shadow:
|
|
var(--elevation-ambient),
|
|
var(--elevation-tight),
|
|
- inset 0 1px 1px rgba(255, 255, 255, 0.15);
|
|
+ inset 0 1px 1px color-mix(in srgb, var(--alpha-white-high) 40%, transparent);
|
|
backdrop-filter: blur(24px) saturate(120%);
|
|
-webkit-backdrop-filter: blur(24px) saturate(120%);
|
|
transform: translateZ(0);
|
|
@@ -244,14 +236,14 @@ body {
|
|
}
|
|
|
|
.workflow-card-selected {
|
|
- border-color: rgba(96, 165, 250, 0.42);
|
|
- border-top-color: rgba(96, 165, 250, 0.58);
|
|
- background: linear-gradient(180deg, rgba(60, 68, 88, 0.7) 0%, rgba(35, 40, 52, 0.7) 100%);
|
|
+ border-color: color-mix(in srgb, var(--accent-info) 42%, var(--border-default));
|
|
+ border-top-color: color-mix(in srgb, var(--accent-info) 58%, var(--border-strong));
|
|
+ background: linear-gradient(180deg, color-mix(in srgb, var(--surface-active) 55%, var(--surface-elevated)), var(--surface-secondary));
|
|
box-shadow:
|
|
- 0 20px 48px -8px rgba(0, 0, 0, 0.9),
|
|
- 0 0 0 1px rgba(96, 165, 250, 0.25),
|
|
- 0 0 40px rgba(96, 165, 250, 0.15),
|
|
- inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
|
+ var(--shadow-lg),
|
|
+ 0 0 0 1px color-mix(in srgb, var(--accent-info) 25%, transparent),
|
|
+ var(--glow-info),
|
|
+ inset 0 1px 1px color-mix(in srgb, var(--alpha-white-high) 50%, transparent);
|
|
}
|
|
|
|
.glass-panel {
|
|
@@ -265,19 +257,19 @@ body {
|
|
}
|
|
|
|
.bg-earthy-gradient {
|
|
- background: linear-gradient(to bottom right, #2D2D2D, #363636);
|
|
+ background: linear-gradient(to bottom right, var(--surface-secondary), var(--surface-tertiary));
|
|
}
|
|
|
|
-/* Shared dark form controls to avoid white-on-white browser defaults */
|
|
+/* Shared themed form controls */
|
|
.ui-field {
|
|
- border: 1px solid rgba(255, 255, 255, 0.1);
|
|
+ border: 1px solid var(--border-default);
|
|
border-top-color: var(--edge-top);
|
|
border-bottom-color: var(--edge-bottom);
|
|
- background: linear-gradient(180deg, rgba(32, 34, 42, 0.72), rgba(17, 19, 26, 0.72));
|
|
+ background: linear-gradient(180deg, var(--surface-input), var(--surface-secondary));
|
|
color: var(--color-text-strong);
|
|
box-shadow:
|
|
- 0 8px 20px -12px rgba(0, 0, 0, 0.85),
|
|
- inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
+ var(--shadow-sm),
|
|
+ inset 0 1px 0 color-mix(in srgb, var(--alpha-white-high) 22%, transparent);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
}
|
|
@@ -288,10 +280,10 @@ body {
|
|
|
|
.ui-field:focus-visible {
|
|
outline: none;
|
|
- border-color: rgba(96, 165, 250, 0.48);
|
|
+ border-color: color-mix(in srgb, var(--accent-info) 48%, var(--border-default));
|
|
box-shadow:
|
|
- 0 0 0 2px rgba(96, 165, 250, 0.2),
|
|
- inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
+ 0 0 0 2px color-mix(in srgb, var(--accent-info) 20%, transparent),
|
|
+ inset 0 1px 0 color-mix(in srgb, var(--alpha-white-high) 22%, transparent);
|
|
}
|
|
|
|
.ui-select {
|
|
@@ -326,19 +318,19 @@ body {
|
|
}
|
|
|
|
.ui-shell-topbar {
|
|
- background: linear-gradient(180deg, color-mix(in srgb, var(--ui-bg-panel) 92%, black), var(--ui-bg-shell));
|
|
+ background: linear-gradient(180deg, var(--ui-bg-panel), var(--ui-bg-shell));
|
|
border-bottom: 1px solid color-mix(in srgb, var(--ui-accent-info) 22%, var(--ui-border-soft));
|
|
- box-shadow: 0 10px 24px -20px rgba(0, 0, 0, 0.9);
|
|
+ box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.ui-shell-middle {
|
|
- background: linear-gradient(180deg, color-mix(in srgb, var(--ui-bg-app) 74%, black), color-mix(in srgb, var(--ui-bg-app) 90%, black));
|
|
+ background: linear-gradient(180deg, var(--ui-bg-app), var(--ui-bg-main));
|
|
border-left: 1px solid color-mix(in srgb, var(--ui-accent-info) 20%, var(--ui-border-soft));
|
|
border-right: 1px solid color-mix(in srgb, var(--ui-accent-info) 20%, var(--ui-border-soft));
|
|
}
|
|
|
|
.ui-shell-panel {
|
|
- background: linear-gradient(180deg, color-mix(in srgb, var(--ui-bg-shell) 86%, black), color-mix(in srgb, var(--ui-bg-panel) 84%, black));
|
|
+ background: linear-gradient(180deg, var(--ui-bg-shell), var(--ui-bg-panel));
|
|
border-left: 1px solid color-mix(in srgb, var(--ui-accent-info) 30%, var(--ui-border-soft));
|
|
}
|
|
|
|
@@ -349,8 +341,8 @@ body {
|
|
|
|
.workflow-graph-legend {
|
|
backdrop-filter: blur(12px);
|
|
- background: rgba(20, 23, 31, 0.72);
|
|
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
+ background: color-mix(in srgb, var(--surface-overlay) 78%, transparent);
|
|
+ box-shadow: inset 0 1px 0 color-mix(in srgb, var(--alpha-white-high) 22%, transparent), var(--shadow-md);
|
|
}
|
|
|
|
.workflow-graph-flow .react-flow__viewport {
|
|
diff --git a/src/app/graph/page.tsx b/src/app/graph/page.tsx
|
|
deleted file mode 100644
|
|
index dbc23ce..0000000
|
|
--- a/src/app/graph/page.tsx
|
|
+++ /dev/null
|
|
@@ -1,39 +0,0 @@
|
|
-import { DependencyGraphPage } from '../../components/graph/dependency-graph-page';
|
|
-
|
|
-export const dynamic = 'force-dynamic';
|
|
-import { readIssuesForScope } from '../../lib/aggregate-read';
|
|
-import { resolveProjectScope } from '../../lib/project-scope';
|
|
-import { listProjects } from '../../lib/registry';
|
|
-
|
|
-interface GraphPageProps {
|
|
- searchParams?: Promise<Record<string, string | string[] | undefined>>;
|
|
-}
|
|
-
|
|
-export default async function GraphPage({ searchParams }: GraphPageProps) {
|
|
- const params = (await searchParams) ?? {};
|
|
- const requestedProjectKey = typeof params.project === 'string' ? params.project : null;
|
|
- const requestedMode = typeof params.mode === 'string' ? params.mode : null;
|
|
- const registryProjects = await listProjects();
|
|
- const scope = resolveProjectScope({
|
|
- currentProjectRoot: process.cwd(),
|
|
- registryProjects,
|
|
- requestedProjectKey,
|
|
- requestedMode,
|
|
- });
|
|
-
|
|
- const issues = await readIssuesForScope({
|
|
- mode: scope.mode,
|
|
- selected: scope.selected,
|
|
- scopeOptions: scope.options,
|
|
- preferBd: true,
|
|
- });
|
|
- return (
|
|
- <DependencyGraphPage
|
|
- issues={issues}
|
|
- projectRoot={scope.selected.root}
|
|
- projectScopeKey={scope.selected.key}
|
|
- projectScopeOptions={scope.options}
|
|
- projectScopeMode={scope.mode}
|
|
- />
|
|
- );
|
|
-}
|
|
diff --git a/src/app/mockup/page.tsx b/src/app/mockup/page.tsx
|
|
deleted file mode 100644
|
|
index b1ff3a7..0000000
|
|
--- a/src/app/mockup/page.tsx
|
|
+++ /dev/null
|
|
@@ -1,556 +0,0 @@
|
|
-"use client"
|
|
-
|
|
-import { useCallback, useEffect, useMemo, useState } from "react"
|
|
-import { useRouter, useSearchParams } from "next/navigation"
|
|
-import { ArrowLeft, ArrowUpRight, Clock3, Link2, MessageCircle, TriangleAlert, X } from "lucide-react"
|
|
-
|
|
-import { Badge } from "@/components/ui/badge"
|
|
-import { Button } from "@/components/ui/button"
|
|
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
-import { Input } from "@/components/ui/input"
|
|
-import { ScrollArea } from "@/components/ui/scroll-area"
|
|
-import { Separator } from "@/components/ui/separator"
|
|
-
|
|
-type TaskStatus = "open" | "in_progress" | "blocked" | "deferred" | "closed"
|
|
-
|
|
-type Task = {
|
|
- id: string
|
|
- title: string
|
|
- description: string
|
|
- status: TaskStatus
|
|
- priority: 0 | 1 | 2 | 3 | 4
|
|
- issueType: string
|
|
- assignee: string
|
|
- owner: string
|
|
- labels: string[]
|
|
- blockedReason: string
|
|
- updatedAgo: string
|
|
- dependencyCount: number
|
|
- blockedByCount: number
|
|
- commentCount: number
|
|
- unread: boolean
|
|
-}
|
|
-
|
|
-type Epic = {
|
|
- id: string
|
|
- name: string
|
|
- progress: number
|
|
- openCount: number
|
|
- tasks: Task[]
|
|
-}
|
|
-
|
|
-const palette = {
|
|
- primary: "#F2A62F",
|
|
- secondary: "#00D1A8",
|
|
- accent: "#0FC5AE",
|
|
- eggplant: "#4A2F63",
|
|
- bg: "#2D2E3C",
|
|
- surface: "#333341",
|
|
- border: "#4A4D5C",
|
|
- text: "#EDEBE5",
|
|
- textSecondary: "#B8B7B1",
|
|
- mutedBg: "#2F2F3E",
|
|
- success: "#0FC5AE",
|
|
- warning: "#D28A2C",
|
|
- error: "#D64545",
|
|
- info: "#00D1A8",
|
|
- atmosphereWarm: "#5A4632",
|
|
- atmosphereCool: "#23484D",
|
|
-}
|
|
-
|
|
-const initialEpics: Epic[] = [
|
|
- {
|
|
- id: "bb-ui2",
|
|
- name: "Unified UX - Earthy Dark Shell",
|
|
- progress: 69,
|
|
- openCount: 11,
|
|
- tasks: [
|
|
- { id: "bb-atf", title: "Agent swarm-view-integrator", description: "Integrate swarm view into social workroom shell.", status: "open", priority: 1, issueType: "task", assignee: "sarah.lee", owner: "swarm-team", labels: ["social", "swarm"], blockedReason: "", updatedAgo: "8m", dependencyCount: 1, blockedByCount: 0, commentCount: 3, unread: true },
|
|
- { id: "bb-z6s", title: "Agent social-view-integrator", description: "Wire social stream cards and panel routing.", status: "in_progress", priority: 0, issueType: "feature", assignee: "alex.chen", owner: "social-team", labels: ["social", "ui"], blockedReason: "", updatedAgo: "14m", dependencyCount: 2, blockedByCount: 0, commentCount: 7, unread: true },
|
|
- { id: "bb-nuy", title: "Agent swarm-card-builder", description: "Build consistent swarm card visuals and metadata.", status: "blocked", priority: 0, issueType: "bug", assignee: "alex.chen", owner: "swarm-team", labels: ["swarm", "cards"], blockedReason: "Waiting on dependency bb-ui2.0", updatedAgo: "35m", dependencyCount: 3, blockedByCount: 1, commentCount: 5, unread: true },
|
|
- { id: "bb-3ha", title: "Agent sessions-integrator", description: "Session metrics panel integrated and verified.", status: "closed", priority: 2, issueType: "chore", assignee: "alex.chen", owner: "sessions-team", labels: ["sessions"], blockedReason: "", updatedAgo: "2h", dependencyCount: 0, blockedByCount: 0, commentCount: 4, unread: false },
|
|
- ],
|
|
- },
|
|
- {
|
|
- id: "bb-xhm",
|
|
- name: "Timeline and Activity Feed",
|
|
- progress: 80,
|
|
- openCount: 5,
|
|
- tasks: [
|
|
- { id: "bb-3dv", title: "Agent rightpanel-builder", description: "Implement right rail card stack and compact activity.", status: "open", priority: 2, issueType: "task", assignee: "alex.chen", owner: "layout-team", labels: ["layout", "right-panel"], blockedReason: "", updatedAgo: "11m", dependencyCount: 1, blockedByCount: 0, commentCount: 1, unread: true },
|
|
- { id: "bb-dwz", title: "Agent leftpanel-builder", description: "Epic->task navigation with search and metadata icons.", status: "in_progress", priority: 1, issueType: "feature", assignee: "sarah.lee", owner: "layout-team", labels: ["layout", "left-panel"], blockedReason: "", updatedAgo: "19m", dependencyCount: 0, blockedByCount: 0, commentCount: 6, unread: true },
|
|
- { id: "bb-5am", title: "Agent topbar-builder", description: "Topbar controls and filter sync.", status: "blocked", priority: 1, issueType: "bug", assignee: "agent-007", owner: "layout-team", labels: ["topbar"], blockedReason: "Navigation contract mismatch", updatedAgo: "41m", dependencyCount: 2, blockedByCount: 1, commentCount: 2, unread: false },
|
|
- { id: "bb-z2l", title: "Agent mobile-nav-builder", description: "Mobile drawer flow for three-pane shell.", status: "deferred", priority: 1, issueType: "task", assignee: "sarah.lee", owner: "mobile-team", labels: ["mobile", "navigation"], blockedReason: "", updatedAgo: "52m", dependencyCount: 0, blockedByCount: 0, commentCount: 2, unread: false },
|
|
- ],
|
|
- },
|
|
-]
|
|
-
|
|
-function statusClasses(status: TaskStatus) {
|
|
- if (status === "in_progress") return "border-l-[3px] border-l-[#0FC5AE] bg-[linear-gradient(145deg,#333341,#2F2F3E)]"
|
|
- if (status === "blocked") return "border-l-[3px] border-l-[#D64545] bg-[linear-gradient(145deg,#333341,#302B31)]"
|
|
- if (status === "deferred") return "border-l-[3px] border-l-[#D28A2C] bg-[linear-gradient(145deg,#333341,#342F29)]"
|
|
- if (status === "closed") return "border-l-[3px] border-l-[#6D6F7B] bg-[linear-gradient(145deg,#333341,#2F3039)]"
|
|
- return "border-l-[3px] border-l-[#00D1A8] bg-[linear-gradient(145deg,#333341,#2D313D)]"
|
|
-}
|
|
-
|
|
-function statusBadge(status: TaskStatus) {
|
|
- if (status === "in_progress") return "bg-[#0FC5AE] text-[#0E2220]"
|
|
- if (status === "blocked") return "bg-[#D64545] text-white"
|
|
- if (status === "deferred") return "bg-[#D28A2C] text-[#24190C]"
|
|
- if (status === "closed") return "bg-[#5A5D6A] text-[#D4D6DE]"
|
|
- return "bg-[#00D1A8] text-[#07221C]"
|
|
-}
|
|
-
|
|
-const panelClass = "rounded-2xl border shadow-[0_16px_40px_rgba(0,0,0,0.28)] backdrop-blur-[2px]"
|
|
-const subPanelClass = "rounded-xl border"
|
|
-
|
|
-function updateQuery(searchParams: URLSearchParams, updates: Record<string, string | null>) {
|
|
- const next = new URLSearchParams(searchParams.toString())
|
|
- for (const [key, value] of Object.entries(updates)) {
|
|
- if (!value) next.delete(key)
|
|
- else next.set(key, value)
|
|
- }
|
|
- const qs = next.toString()
|
|
- return qs ? `?${qs}` : "?"
|
|
-}
|
|
-
|
|
-export default function MockupPage() {
|
|
- const searchParams = useSearchParams()
|
|
- const router = useRouter()
|
|
-
|
|
- const [epics, setEpics] = useState(initialEpics)
|
|
- const [query, setQuery] = useState("")
|
|
- const [leftMode, setLeftMode] = useState<"epics" | "tasks">("epics")
|
|
-
|
|
- const urlEpic = searchParams.get("epic")
|
|
- const urlTask = searchParams.get("task")
|
|
- const urlThread = searchParams.get("thread") === "open"
|
|
-
|
|
- const initialEpic = epics.find((epic) => epic.id === urlEpic) ?? epics[0]
|
|
- const [selectedEpicId, setSelectedEpicId] = useState(initialEpic.id)
|
|
- const [selectedTaskId, setSelectedTaskId] = useState(urlTask ?? initialEpic.tasks[0].id)
|
|
- const [threadOpen, setThreadOpen] = useState(urlThread)
|
|
- const [threadEditMode, setThreadEditMode] = useState(false)
|
|
-
|
|
- const [draftTitle, setDraftTitle] = useState("")
|
|
- const [draftDescription, setDraftDescription] = useState("")
|
|
- const [draftStatus, setDraftStatus] = useState<TaskStatus>("open")
|
|
- const [draftPriority, setDraftPriority] = useState<0 | 1 | 2 | 3 | 4>(2)
|
|
- const [draftIssueType, setDraftIssueType] = useState("")
|
|
- const [draftAssignee, setDraftAssignee] = useState("")
|
|
- const [draftOwner, setDraftOwner] = useState("")
|
|
- const [draftLabels, setDraftLabels] = useState("")
|
|
- const [draftBlockedReason, setDraftBlockedReason] = useState("")
|
|
- const [savePulse, setSavePulse] = useState(false)
|
|
-
|
|
- const closeThread = useCallback(() => {
|
|
- setThreadOpen(false)
|
|
- setThreadEditMode(false)
|
|
- }, [])
|
|
-
|
|
- useEffect(() => {
|
|
- const next = updateQuery(searchParams, {
|
|
- epic: selectedEpicId,
|
|
- task: selectedTaskId,
|
|
- thread: threadOpen ? "open" : null,
|
|
- })
|
|
- router.replace(next, { scroll: false })
|
|
- }, [router, searchParams, selectedEpicId, selectedTaskId, threadOpen])
|
|
-
|
|
- useEffect(() => {
|
|
- if (!threadOpen) {
|
|
- return
|
|
- }
|
|
- const onKeyDown = (event: KeyboardEvent) => {
|
|
- if (event.key === "Escape") {
|
|
- closeThread()
|
|
- }
|
|
- }
|
|
- window.addEventListener("keydown", onKeyDown)
|
|
- return () => window.removeEventListener("keydown", onKeyDown)
|
|
- }, [threadOpen, closeThread])
|
|
-
|
|
- const selectedEpic = epics.find((epic) => epic.id === selectedEpicId) ?? epics[0]
|
|
- const filteredTasks = useMemo(() => {
|
|
- const q = query.trim().toLowerCase()
|
|
- return selectedEpic.tasks.filter((task) => `${task.id} ${task.title}`.toLowerCase().includes(q))
|
|
- }, [query, selectedEpic.tasks])
|
|
- const selectedTask = filteredTasks.find((task) => task.id === selectedTaskId) ?? filteredTasks[0]
|
|
-
|
|
- useEffect(() => {
|
|
- if (!selectedTask) return
|
|
- setDraftTitle(selectedTask.title)
|
|
- setDraftDescription(selectedTask.description)
|
|
- setDraftStatus(selectedTask.status)
|
|
- setDraftPriority(selectedTask.priority)
|
|
- setDraftIssueType(selectedTask.issueType)
|
|
- setDraftAssignee(selectedTask.assignee)
|
|
- setDraftOwner(selectedTask.owner)
|
|
- setDraftLabels(selectedTask.labels.join(", "))
|
|
- setDraftBlockedReason(selectedTask.blockedReason)
|
|
- setThreadEditMode(false)
|
|
- }, [selectedTask, selectedTask?.id])
|
|
-
|
|
- const saveTaskChanges = () => {
|
|
- if (!selectedTask) return
|
|
- const nextLabels = draftLabels
|
|
- .split(",")
|
|
- .map((part) => part.trim())
|
|
- .filter(Boolean)
|
|
- setEpics((current) =>
|
|
- current.map((epic) =>
|
|
- epic.id !== selectedEpicId
|
|
- ? epic
|
|
- : {
|
|
- ...epic,
|
|
- tasks: epic.tasks.map((task) =>
|
|
- task.id !== selectedTask.id
|
|
- ? task
|
|
- : {
|
|
- ...task,
|
|
- title: draftTitle,
|
|
- description: draftDescription,
|
|
- status: draftStatus,
|
|
- priority: draftPriority,
|
|
- issueType: draftIssueType,
|
|
- assignee: draftAssignee,
|
|
- owner: draftOwner,
|
|
- labels: nextLabels,
|
|
- blockedReason: draftBlockedReason,
|
|
- updatedAgo: "now",
|
|
- blockedByCount: draftStatus === "blocked" ? Math.max(task.blockedByCount, 1) : 0,
|
|
- }
|
|
- ),
|
|
- }
|
|
- )
|
|
- )
|
|
- setSavePulse(true)
|
|
- setTimeout(() => setSavePulse(false), 900)
|
|
- }
|
|
-
|
|
- return (
|
|
- <main className="min-h-screen" style={{ backgroundColor: palette.bg, color: palette.text }}>
|
|
- <div
|
|
- className="min-h-screen"
|
|
- style={{
|
|
- backgroundImage:
|
|
- `radial-gradient(circle at 12% 16%, rgba(90,70,50,0.55), transparent 34%), radial-gradient(circle at 88% 82%, rgba(35,72,77,0.50), transparent 32%)`,
|
|
- }}
|
|
- >
|
|
- <div className="mx-auto max-w-[1500px] px-4 py-6 md:px-8 md:py-8">
|
|
- <header className="mb-6 flex flex-wrap items-end justify-between gap-4">
|
|
- <div>
|
|
- <h1 className="text-4xl font-semibold tracking-tight leading-[1.02] md:text-5xl">Social Workroom</h1>
|
|
- <p className="mt-2 text-sm" style={{ color: palette.textSecondary }}>Task-first center. Epic drill-in. Live awareness rail.</p>
|
|
- </div>
|
|
- <Badge className="rounded-full px-3 py-1 text-xs text-white" style={{ backgroundColor: palette.eggplant }}>mockup route</Badge>
|
|
- </header>
|
|
-
|
|
- <section className="grid gap-4 lg:grid-cols-[24%_52%_24%]">
|
|
- <Card className={panelClass} style={{ backgroundColor: palette.surface, borderColor: palette.border }}>
|
|
- <CardHeader className="pb-3">
|
|
- <div className="flex items-center justify-between">
|
|
- {leftMode === "tasks" ? (
|
|
- <Button variant="ghost" className="h-8 px-2" onClick={() => setLeftMode("epics")}>
|
|
- <ArrowLeft className="mr-2 h-4 w-4" /> Back to epics
|
|
- </Button>
|
|
- ) : (
|
|
- <CardTitle className="text-lg">Epics</CardTitle>
|
|
- )}
|
|
- <Badge className="rounded-full" style={{ backgroundColor: palette.mutedBg, color: palette.textSecondary }}>{selectedEpic.openCount} open</Badge>
|
|
- </div>
|
|
- <CardDescription style={{ color: palette.textSecondary }}>Select an epic, then choose a task.</CardDescription>
|
|
- </CardHeader>
|
|
- <CardContent className="pt-0">
|
|
- <Input
|
|
- value={query}
|
|
- onChange={(event) => setQuery(event.target.value)}
|
|
- placeholder={leftMode === "epics" ? "Search epics" : "Search tasks"}
|
|
- className="mb-3"
|
|
- style={{ backgroundColor: palette.mutedBg, borderColor: palette.border }}
|
|
- />
|
|
- <ScrollArea className="h-[520px] pr-2">
|
|
- <div className="space-y-2">
|
|
- {leftMode === "epics"
|
|
- ? epics
|
|
- .filter((epic) => epic.name.toLowerCase().includes(query.toLowerCase()))
|
|
- .map((epic) => (
|
|
- <button
|
|
- key={epic.id}
|
|
- type="button"
|
|
- className={`${subPanelClass} w-full p-3 text-left transition duration-200 hover:-translate-y-[1px] hover:shadow-[0_10px_24px_rgba(0,0,0,0.35)]`}
|
|
- style={{ backgroundColor: palette.surface, borderColor: palette.border }}
|
|
- onClick={() => {
|
|
- setSelectedEpicId(epic.id)
|
|
- setSelectedTaskId(epic.tasks[0]?.id ?? "")
|
|
- setLeftMode("tasks")
|
|
- closeThread()
|
|
- }}
|
|
- >
|
|
- <p className="text-sm font-semibold">{epic.name}</p>
|
|
- <p className="mt-1 text-xs" style={{ color: palette.textSecondary }}>{epic.id}</p>
|
|
- </button>
|
|
- ))
|
|
- : filteredTasks.map((task) => (
|
|
- <button
|
|
- key={task.id}
|
|
- type="button"
|
|
- onClick={() => {
|
|
- setSelectedTaskId(task.id)
|
|
- closeThread()
|
|
- }}
|
|
- className={`${subPanelClass} w-full p-3 text-left transition duration-200 ${selectedTask?.id === task.id
|
|
- ? "shadow-[0_12px_26px_rgba(0,0,0,0.4)]"
|
|
- : "hover:-translate-y-[1px] hover:shadow-[0_10px_22px_rgba(0,0,0,0.33)]"
|
|
- }`}
|
|
- style={{
|
|
- backgroundColor: selectedTask?.id === task.id ? palette.mutedBg : palette.surface,
|
|
- borderColor: selectedTask?.id === task.id ? palette.primary : palette.border,
|
|
- }}
|
|
- >
|
|
- <p className="text-sm font-semibold">{task.id}</p>
|
|
- <p className="mt-1 line-clamp-1 text-xs" style={{ color: palette.textSecondary }}>{task.title}</p>
|
|
- <div className="mt-2 flex items-center gap-3 text-[11px]" style={{ color: palette.textSecondary }}>
|
|
- <span className="inline-flex items-center gap-1"><Clock3 className="h-3 w-3" />{task.updatedAgo}</span>
|
|
- <span className="inline-flex items-center gap-1"><Link2 className="h-3 w-3" />{task.dependencyCount}</span>
|
|
- <span className="inline-flex items-center gap-1"><MessageCircle className="h-3 w-3" />{task.commentCount}</span>
|
|
- </div>
|
|
- </button>
|
|
- ))}
|
|
- </div>
|
|
- </ScrollArea>
|
|
- </CardContent>
|
|
- </Card>
|
|
-
|
|
- <Card className={panelClass} style={{ backgroundColor: palette.surface, borderColor: palette.border }}>
|
|
- <CardHeader className="pb-3">
|
|
- <div className="flex items-center justify-between">
|
|
- <div>
|
|
- <CardTitle className="text-lg">{selectedEpic.name}</CardTitle>
|
|
- <CardDescription style={{ color: palette.textSecondary }}>Task cards + thread context</CardDescription>
|
|
- </div>
|
|
- <Button className="h-8 rounded-full px-4 text-white" style={{ backgroundColor: palette.primary }}>New update</Button>
|
|
- </div>
|
|
- </CardHeader>
|
|
- <CardContent className="pt-0">
|
|
- <ScrollArea className="h-[430px] pr-2">
|
|
- <div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
|
|
- {filteredTasks.map((task) => (
|
|
- <button
|
|
- key={task.id}
|
|
- type="button"
|
|
- onClick={() => setSelectedTaskId(task.id)}
|
|
- className={`rounded-xl border p-4 text-left transition duration-200 hover:-translate-y-[1px] hover:shadow-[0_14px_28px_rgba(0,0,0,0.35)] ${statusClasses(task.status)}`}
|
|
- style={{ borderColor: selectedTask?.id === task.id ? palette.primary : palette.border }}
|
|
- >
|
|
- <div className="flex items-center justify-between">
|
|
- <span className="text-sm font-semibold" style={{ color: palette.eggplant }}>{task.id}</span>
|
|
- <Badge className={`rounded-full px-2 py-0.5 text-[11px] ${statusBadge(task.status)}`}>{task.status.replace("_", " ")}</Badge>
|
|
- </div>
|
|
- <p className="mt-3 text-[1.7rem] font-semibold leading-[1.15]">{task.title}</p>
|
|
- <p className="mt-2 line-clamp-2 text-sm" style={{ color: palette.textSecondary }}>{task.description}</p>
|
|
- <div className="mt-4 flex items-center gap-3 text-xs" style={{ color: palette.textSecondary }}>
|
|
- <span className="inline-flex items-center gap-1"><Clock3 className="h-3.5 w-3.5" />{task.updatedAgo}</span>
|
|
- <span className="inline-flex items-center gap-1"><Link2 className="h-3.5 w-3.5" />{task.dependencyCount}</span>
|
|
- <span className="inline-flex items-center gap-1"><MessageCircle className="h-3.5 w-3.5" />{task.commentCount}</span>
|
|
- </div>
|
|
- </button>
|
|
- ))}
|
|
- </div>
|
|
- </ScrollArea>
|
|
- <Separator className="my-4" />
|
|
- <div className={`${subPanelClass} p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]`} style={{ borderColor: palette.border, backgroundColor: palette.mutedBg }}>
|
|
- <div className="mb-2 flex items-center justify-between">
|
|
- <p className="text-sm font-semibold">Conversation: {selectedTask?.id}</p>
|
|
- <Button variant="ghost" className="h-7 px-2" style={{ color: palette.secondary }} onClick={() => setThreadOpen(true)}>
|
|
- Open thread <ArrowUpRight className="ml-1 h-3.5 w-3.5" />
|
|
- </Button>
|
|
- </div>
|
|
- <div className="space-y-2">
|
|
- <div className="rounded-lg border px-3 py-2 text-sm" style={{ borderColor: "#5A5D6A", backgroundColor: "#2A2B37" }}>
|
|
- <span className="font-semibold" style={{ color: palette.success }}>alex.chen</span>
|
|
- <span className="mx-1 text-xs" style={{ color: "#8F92A3" }}>2m</span>
|
|
- <span style={{ color: palette.textSecondary }}>Need confirmation that detail strip stays sticky while card grid scrolls.</span>
|
|
- </div>
|
|
- <div className="rounded-lg border px-3 py-2 text-sm" style={{ borderColor: "#5A5D6A", backgroundColor: "#2A2B37" }}>
|
|
- <span className="font-semibold" style={{ color: palette.secondary }}>sarah.lee</span>
|
|
- <span className="mx-1 text-xs" style={{ color: "#8F92A3" }}>1m</span>
|
|
- <span style={{ color: palette.textSecondary }}>Approved if right rail remains visible at 1280px breakpoint.</span>
|
|
- </div>
|
|
- </div>
|
|
- </div>
|
|
- </CardContent>
|
|
- </Card>
|
|
-
|
|
- <Card className={panelClass} style={{ backgroundColor: palette.surface, borderColor: palette.border }}>
|
|
- <CardHeader className="pb-3">
|
|
- <CardTitle className="text-lg">Live Context</CardTitle>
|
|
- <CardDescription style={{ color: palette.textSecondary }}>Persistent awareness while working tasks.</CardDescription>
|
|
- </CardHeader>
|
|
- <CardContent className="space-y-4 pt-0">
|
|
- <div className={`${subPanelClass} p-3 shadow-[0_8px_20px_rgba(0,0,0,0.25)]`} style={{ borderColor: palette.border, backgroundColor: palette.mutedBg }}>
|
|
- <p className="mb-2 text-sm font-semibold">Live Agents</p>
|
|
- <div className="space-y-1 text-sm">
|
|
- <p className="flex items-center justify-between"><span>swarm-view-integrator</span><span style={{ color: palette.success }}>online</span></p>
|
|
- <p className="flex items-center justify-between"><span>social-view-integrator</span><span style={{ color: palette.warning }}>away</span></p>
|
|
- <p className="flex items-center justify-between"><span>graph-integrator</span><span style={{ color: palette.info }}>busy</span></p>
|
|
- </div>
|
|
- </div>
|
|
- <div className={`${subPanelClass} p-3 shadow-[0_8px_20px_rgba(0,0,0,0.25)]`} style={{ borderColor: palette.border, backgroundColor: palette.mutedBg }}>
|
|
- <p className="mb-2 text-sm font-semibold">Recent Activity</p>
|
|
- <div className="space-y-1 text-xs" style={{ color: palette.textSecondary }}>
|
|
- <p>5m · bb-z6s moved to in progress</p>
|
|
- <p>11m · bb-atf received 2 comments</p>
|
|
- <p>18m · bb-3ha marked closed</p>
|
|
- <p>33m · bb-nuy dependency changed</p>
|
|
- </div>
|
|
- </div>
|
|
- <div className={`${subPanelClass} p-3 shadow-[0_8px_20px_rgba(0,0,0,0.22)]`} style={{ borderColor: "#6A4E2F", backgroundColor: "#3A332B" }}>
|
|
- <p className="mb-2 text-sm font-semibold">Attention</p>
|
|
- <p className="flex items-center gap-2 text-sm" style={{ color: "#F2C684" }}><TriangleAlert className="h-4 w-4" /> 2 blocked tasks in selected epic</p>
|
|
- </div>
|
|
- </CardContent>
|
|
- </Card>
|
|
- </section>
|
|
- </div>
|
|
- </div>
|
|
-
|
|
- {threadOpen ? (
|
|
- <div className="fixed inset-0 z-50 flex items-end justify-center bg-black/35 p-2 md:items-center md:p-4" onClick={closeThread}>
|
|
- <div
|
|
- className={`${panelClass} w-full max-w-[980px] p-4 md:p-5`}
|
|
- style={{
|
|
- borderColor: "#5B5E71",
|
|
- background: "linear-gradient(180deg,#323342,#2A2B38)",
|
|
- color: palette.text,
|
|
- }}
|
|
- onClick={(event) => event.stopPropagation()}
|
|
- >
|
|
- <div className="mb-3 flex items-center justify-between">
|
|
- <div>
|
|
- <p className="text-lg font-semibold">Thread · {selectedTask?.id}</p>
|
|
- <p className="text-xs" style={{ color: palette.textSecondary }}>Bead summary and inline edit mode</p>
|
|
- </div>
|
|
- <Button variant="ghost" className="h-8 w-8 p-0 hover:bg-white/10" aria-label="Close thread" onClick={closeThread}><X className="h-4 w-4" /></Button>
|
|
- </div>
|
|
- <div className="mt-3 rounded-xl border p-4" style={{ borderColor: "#55586A", backgroundColor: "#2A2B37" }}>
|
|
- <div className="mb-3 flex items-center justify-between">
|
|
- <p className="text-sm font-semibold">{threadEditMode ? "Edit task" : "Task summary"}</p>
|
|
- <Badge className="rounded-full px-2 py-0.5 text-[11px]" style={{ backgroundColor: savePulse ? palette.success : "#3C3E4E", color: savePulse ? "#0E2220" : "#B8B7B1" }}>
|
|
- {savePulse ? "saved" : "ready"}
|
|
- </Badge>
|
|
- </div>
|
|
- {!threadEditMode ? (
|
|
- <div className="space-y-3 text-sm">
|
|
- <div className="rounded-lg border p-3" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>
|
|
- <p className="text-xs mb-1" style={{ color: "#A4A7B7" }}>{selectedTask?.id}</p>
|
|
- <p className="font-semibold text-base text-[#ECEBE5]">{selectedTask?.title}</p>
|
|
- <p className="mt-1" style={{ color: palette.textSecondary }}>{selectedTask?.description}</p>
|
|
- </div>
|
|
- <div className="grid grid-cols-2 gap-2">
|
|
- <div className="rounded-lg border p-2" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>Status: {selectedTask?.status}</div>
|
|
- <div className="rounded-lg border p-2" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>Priority: P{selectedTask?.priority}</div>
|
|
- <div className="rounded-lg border p-2" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>Assignee: {selectedTask?.assignee || "-"}</div>
|
|
- <div className="rounded-lg border p-2" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>Owner: {selectedTask?.owner || "-"}</div>
|
|
- <div className="rounded-lg border p-2 col-span-2" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>
|
|
- Labels: {selectedTask?.labels.join(", ") || "-"}
|
|
- </div>
|
|
- <div className="rounded-lg border p-2 col-span-2" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>
|
|
- Blocked reason: {selectedTask?.blockedReason || "None"}
|
|
- </div>
|
|
- </div>
|
|
- <div className="flex justify-end">
|
|
- <Button className="rounded-full px-4 text-[#24190C]" style={{ backgroundColor: palette.primary }} onClick={() => setThreadEditMode(true)}>
|
|
- Edit
|
|
- </Button>
|
|
- </div>
|
|
- </div>
|
|
- ) : (
|
|
- <>
|
|
- <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
- <div>
|
|
- <p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Title</p>
|
|
- <Input value={draftTitle} onChange={(event) => setDraftTitle(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} />
|
|
- </div>
|
|
- <div>
|
|
- <p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Assignee</p>
|
|
- <Input value={draftAssignee} onChange={(event) => setDraftAssignee(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} />
|
|
- </div>
|
|
- <div className="md:col-span-2">
|
|
- <p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Description</p>
|
|
- <Input value={draftDescription} onChange={(event) => setDraftDescription(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} />
|
|
- </div>
|
|
- <div>
|
|
- <p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Issue type</p>
|
|
- <Input value={draftIssueType} onChange={(event) => setDraftIssueType(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} />
|
|
- </div>
|
|
- <div>
|
|
- <p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Owner</p>
|
|
- <Input value={draftOwner} disabled style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text, opacity: 0.7 }} />
|
|
- </div>
|
|
- <div className="md:col-span-2">
|
|
- <p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Labels (comma separated)</p>
|
|
- <Input value={draftLabels} onChange={(event) => setDraftLabels(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} />
|
|
- </div>
|
|
- </div>
|
|
- <div className="mt-3">
|
|
- <p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Status</p>
|
|
- <div className="flex flex-wrap gap-2">
|
|
- {(["open", "in_progress", "blocked", "deferred", "closed"] as TaskStatus[]).map((status) => (
|
|
- <button
|
|
- key={status}
|
|
- type="button"
|
|
- className={`rounded-full border px-2 py-1 text-xs ${draftStatus === status ? statusBadge(status) : ""}`}
|
|
- style={{
|
|
- borderColor: draftStatus === status ? "transparent" : palette.border,
|
|
- backgroundColor: draftStatus === status ? undefined : "#323342",
|
|
- color: draftStatus === status ? undefined : palette.textSecondary,
|
|
- }}
|
|
- onClick={() => setDraftStatus(status)}
|
|
- >
|
|
- {status.replace("_", " ")}
|
|
- </button>
|
|
- ))}
|
|
- </div>
|
|
- </div>
|
|
- <div className="mt-3">
|
|
- <p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Priority</p>
|
|
- <div className="flex flex-wrap gap-2">
|
|
- {([0, 1, 2, 3, 4] as const).map((priority) => (
|
|
- <button
|
|
- key={priority}
|
|
- type="button"
|
|
- onClick={() => setDraftPriority(priority)}
|
|
- className="rounded-full border px-2 py-1 text-xs"
|
|
- style={{
|
|
- borderColor: draftPriority === priority ? palette.eggplant : palette.border,
|
|
- backgroundColor: draftPriority === priority ? "#F4EAFE" : palette.surface,
|
|
- color: draftPriority === priority ? palette.eggplant : palette.textSecondary,
|
|
- }}
|
|
- >
|
|
- P{priority}
|
|
- </button>
|
|
- ))}
|
|
- </div>
|
|
- </div>
|
|
- <div className="mt-3">
|
|
- <p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Blocked reason</p>
|
|
- <Input value={draftBlockedReason} onChange={(event) => setDraftBlockedReason(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} />
|
|
- </div>
|
|
- <div className="mt-4 flex justify-end gap-2">
|
|
- <Button variant="outline" className="rounded-full px-4 border-[#585B6D] bg-[#323342] text-[#B8B7B1] hover:bg-[#3A3B49]" onClick={() => setThreadEditMode(false)}>
|
|
- Cancel
|
|
- </Button>
|
|
- <Button className="rounded-full px-4 text-[#24190C]" style={{ backgroundColor: palette.primary }} onClick={() => { saveTaskChanges(); setThreadEditMode(false) }}>
|
|
- Save changes
|
|
- </Button>
|
|
- </div>
|
|
- </>
|
|
- )}
|
|
- </div>
|
|
- </div>
|
|
- </div>
|
|
- ) : null}
|
|
- </main>
|
|
- )
|
|
-}
|
|
diff --git a/src/app/page-old.tsx b/src/app/page-old.tsx
|
|
deleted file mode 100644
|
|
index dbbce2a..0000000
|
|
--- a/src/app/page-old.tsx
|
|
+++ /dev/null
|
|
@@ -1,37 +0,0 @@
|
|
-import { KanbanPage } from '../components/kanban/kanban-page';
|
|
-import { readIssuesForScope } from '../lib/aggregate-read';
|
|
-import { resolveProjectScope } from '../lib/project-scope';
|
|
-import { listProjects } from '../lib/registry';
|
|
-
|
|
-interface PageProps {
|
|
- searchParams?: Promise<Record<string, string | string[] | undefined>>;
|
|
-}
|
|
-
|
|
-export default async function Page({ searchParams }: PageProps) {
|
|
- const params = (await searchParams) ?? {};
|
|
- const requestedProjectKey = typeof params.project === 'string' ? params.project : null;
|
|
- const requestedMode = typeof params.mode === 'string' ? params.mode : null;
|
|
- const registryProjects = await listProjects();
|
|
- const scope = resolveProjectScope({
|
|
- currentProjectRoot: process.cwd(),
|
|
- registryProjects,
|
|
- requestedProjectKey,
|
|
- requestedMode,
|
|
- });
|
|
-
|
|
- const issues = await readIssuesForScope({
|
|
- mode: scope.mode,
|
|
- selected: scope.selected,
|
|
- scopeOptions: scope.options,
|
|
- preferBd: true,
|
|
- });
|
|
- return (
|
|
- <KanbanPage
|
|
- issues={issues}
|
|
- projectRoot={scope.selected.root}
|
|
- projectScopeKey={scope.selected.key}
|
|
- projectScopeOptions={scope.options}
|
|
- projectScopeMode={scope.mode}
|
|
- />
|
|
- );
|
|
-}
|
|
diff --git a/src/app/sessions/page.tsx b/src/app/sessions/page.tsx
|
|
deleted file mode 100644
|
|
index 83a3837..0000000
|
|
--- a/src/app/sessions/page.tsx
|
|
+++ /dev/null
|
|
@@ -1,5 +0,0 @@
|
|
-import { redirect } from 'next/navigation';
|
|
-
|
|
-export default function SessionsRedirectPage() {
|
|
- redirect('/?view=social');
|
|
-}
|
|
diff --git a/src/app/timeline/page.tsx b/src/app/timeline/page.tsx
|
|
deleted file mode 100644
|
|
index da94814..0000000
|
|
--- a/src/app/timeline/page.tsx
|
|
+++ /dev/null
|
|
@@ -1,60 +0,0 @@
|
|
-'use client';
|
|
-
|
|
-import { useEffect } from 'react';
|
|
-import { TimelineFeed } from '../../components/timeline/timeline-feed';
|
|
-import { useTimelineStore } from '../../components/timeline/timeline-store';
|
|
-
|
|
-export default function TimelinePage() {
|
|
- return (
|
|
- <div className="mx-auto max-w-3xl px-4 py-8">
|
|
- <header className="mb-8">
|
|
- <h1 className="text-2xl font-bold text-text-strong">Activity Timeline</h1>
|
|
- <p className="text-text-muted">Real-time stream of project mutations.</p>
|
|
- </header>
|
|
-
|
|
- <TimelineControls />
|
|
- <TimelineSubscription />
|
|
- <TimelineFeed />
|
|
- </div>
|
|
- );
|
|
-}
|
|
-
|
|
-function TimelineControls() {
|
|
- return (
|
|
- <div className="mb-6 flex gap-2">
|
|
- {/* Placeholder for future filters */}
|
|
- <div className="text-sm text-text-muted">Showing all activity</div>
|
|
- </div>
|
|
- );
|
|
-}
|
|
-
|
|
-function TimelineSubscription() {
|
|
- const { addEvent, setHistory } = useTimelineStore();
|
|
-
|
|
- useEffect(() => {
|
|
- // 1. Fetch history
|
|
- fetch('/api/activity')
|
|
- .then(res => {
|
|
- if (!res.ok) throw new Error('History fetch failed');
|
|
- return res.json();
|
|
- })
|
|
- .then(data => setHistory(data))
|
|
- .catch(err => console.error('Failed to load history', err));
|
|
-
|
|
- // 2. Subscribe to SSE
|
|
- const es = new EventSource('/api/events');
|
|
-
|
|
- es.addEventListener('activity', (e) => {
|
|
- try {
|
|
- const event = JSON.parse(e.data);
|
|
- addEvent(event);
|
|
- } catch (err) {
|
|
- console.error('Failed to parse activity event', err);
|
|
- }
|
|
- });
|
|
-
|
|
- return () => es.close();
|
|
- }, [setHistory, addEvent]);
|
|
-
|
|
- return null;
|
|
-}
|
|
\ No newline at end of file
|
|
diff --git a/src/components/activity/activity-panel.tsx b/src/components/activity/activity-panel.tsx
|
|
index 8df8023..89744b9 100644
|
|
--- a/src/components/activity/activity-panel.tsx
|
|
+++ b/src/components/activity/activity-panel.tsx
|
|
@@ -111,25 +111,25 @@ export function formatRelativeTime(timestamp: string): string {
|
|
function getAgentTone(status: AgentStatus): AgentTone {
|
|
const tones: Record<AgentStatus, AgentTone> = {
|
|
active: {
|
|
- cardClass: 'bg-[#173126]',
|
|
+ cardClass: 'bg-[var(--status-ready)]',
|
|
labelClass: 'text-[#7CB97A]',
|
|
ringClass: 'ring-[#7CB97A]/45',
|
|
glowClass: 'bg-[#7CB97A]/30',
|
|
},
|
|
stale: {
|
|
- cardClass: 'bg-[#322817]',
|
|
+ cardClass: 'bg-[var(--status-in-progress)]',
|
|
labelClass: 'text-[#D4A574]',
|
|
ringClass: 'ring-[#D4A574]/45',
|
|
glowClass: 'bg-[#D4A574]/30',
|
|
},
|
|
stuck: {
|
|
- cardClass: 'bg-[#341a1f]',
|
|
+ cardClass: 'bg-[var(--status-blocked)]',
|
|
labelClass: 'text-[#C97A7A]',
|
|
ringClass: 'ring-[#C97A7A]/45',
|
|
glowClass: 'bg-[#C97A7A]/30',
|
|
},
|
|
dead: {
|
|
- cardClass: 'bg-[#2b232b]',
|
|
+ cardClass: 'bg-[var(--surface-primary)]',
|
|
labelClass: 'text-[#A78A94]',
|
|
ringClass: 'ring-[#A78A94]/40',
|
|
glowClass: 'bg-[#A78A94]/25',
|
|
@@ -147,84 +147,84 @@ export function getEventTone(kind: string): EventTone {
|
|
label: 'Created',
|
|
labelClass: 'text-[#7CB97A]',
|
|
dotClass: 'bg-[#7CB97A]',
|
|
- cardClass: 'bg-[#182f25]',
|
|
+ cardClass: 'bg-[var(--status-ready)]',
|
|
idClass: 'text-[#9ACB98]',
|
|
},
|
|
opened: {
|
|
label: 'Opened',
|
|
labelClass: 'text-[#7CB97A]',
|
|
dotClass: 'bg-[#7CB97A]',
|
|
- cardClass: 'bg-[#182f25]',
|
|
+ cardClass: 'bg-[var(--status-ready)]',
|
|
idClass: 'text-[#9ACB98]',
|
|
},
|
|
closed: {
|
|
label: 'Closed',
|
|
labelClass: 'text-[#D4A574]',
|
|
dotClass: 'bg-[#D4A574]',
|
|
- cardClass: 'bg-[#332716]',
|
|
+ cardClass: 'bg-[var(--status-in-progress)]',
|
|
idClass: 'text-[#DAB891]',
|
|
},
|
|
reopened: {
|
|
label: 'Reopened',
|
|
labelClass: 'text-[#5B95E8]',
|
|
dotClass: 'bg-[#5B95E8]',
|
|
- cardClass: 'bg-[#1b2b43]',
|
|
+ cardClass: 'bg-[var(--surface-primary)]',
|
|
idClass: 'text-[#8DB4EF]',
|
|
},
|
|
status_changed: {
|
|
label: 'Status changed',
|
|
labelClass: 'text-[#D4A574]',
|
|
dotClass: 'bg-[#D4A574]',
|
|
- cardClass: 'bg-[#2f2518]',
|
|
+ cardClass: 'bg-[var(--status-in-progress)]',
|
|
idClass: 'text-[#DAB891]',
|
|
},
|
|
priority_changed: {
|
|
label: 'Priority changed',
|
|
labelClass: 'text-[#D4A574]',
|
|
dotClass: 'bg-[#D4A574]',
|
|
- cardClass: 'bg-[#2f2518]',
|
|
+ cardClass: 'bg-[var(--status-in-progress)]',
|
|
idClass: 'text-[#DAB891]',
|
|
},
|
|
assignee_changed: {
|
|
label: 'Assigned',
|
|
labelClass: 'text-[#D4A574]',
|
|
dotClass: 'bg-[#D4A574]',
|
|
- cardClass: 'bg-[#2f2518]',
|
|
+ cardClass: 'bg-[var(--status-in-progress)]',
|
|
idClass: 'text-[#DAB891]',
|
|
},
|
|
dependency_added: {
|
|
label: 'Dependency added',
|
|
labelClass: 'text-[#D4A574]',
|
|
dotClass: 'bg-[#D4A574]',
|
|
- cardClass: 'bg-[#2f2518]',
|
|
+ cardClass: 'bg-[var(--status-in-progress)]',
|
|
idClass: 'text-[#DAB891]',
|
|
},
|
|
dependency_removed: {
|
|
label: 'Dependency removed',
|
|
labelClass: 'text-[#C97A7A]',
|
|
dotClass: 'bg-[#C97A7A]',
|
|
- cardClass: 'bg-[#321b21]',
|
|
+ cardClass: 'bg-[var(--status-blocked)]',
|
|
idClass: 'text-[#D9A9A9]',
|
|
},
|
|
heartbeat: {
|
|
label: 'Heartbeat',
|
|
labelClass: 'text-[#5BA8A0]',
|
|
dotClass: 'bg-[#5BA8A0]',
|
|
- cardClass: 'bg-[#173034]',
|
|
+ cardClass: 'bg-[var(--surface-primary)]',
|
|
idClass: 'text-[#8BC9C1]',
|
|
},
|
|
commented: {
|
|
label: 'Commented',
|
|
labelClass: 'text-[#5BA8A0]',
|
|
dotClass: 'bg-[#5BA8A0]',
|
|
- cardClass: 'bg-[#173034]',
|
|
+ cardClass: 'bg-[var(--surface-primary)]',
|
|
idClass: 'text-[#8BC9C1]',
|
|
},
|
|
comment_added: {
|
|
label: 'Commented',
|
|
labelClass: 'text-[#5BA8A0]',
|
|
dotClass: 'bg-[#5BA8A0]',
|
|
- cardClass: 'bg-[#173034]',
|
|
+ cardClass: 'bg-[var(--surface-primary)]',
|
|
idClass: 'text-[#8BC9C1]',
|
|
},
|
|
};
|
|
@@ -234,7 +234,7 @@ export function getEventTone(kind: string): EventTone {
|
|
label: normalized.replace(/_/g, ' '),
|
|
labelClass: 'text-[#5BA8A0]',
|
|
dotClass: 'bg-[#5BA8A0]',
|
|
- cardClass: 'bg-[#173034]',
|
|
+ cardClass: 'bg-[var(--surface-primary)]',
|
|
idClass: 'text-[#8BC9C1]',
|
|
}
|
|
);
|
|
@@ -299,7 +299,7 @@ export function ActivityPanel({ issues, collapsed = false, projectRoot }: Activi
|
|
const activeAgents = agentRoster.filter(a => a.status === 'active').length;
|
|
if (collapsed) {
|
|
return (
|
|
- <div className="flex flex-col items-center gap-6 py-6 h-full bg-[linear-gradient(180deg,rgba(0,0,0,0.2),rgba(0,0,0,0.36))] shadow-[inset_10px_0_22px_-20px_rgba(0,0,0,0.9)]">
|
|
+ <div className="flex flex-col items-center gap-6 py-6 h-full bg-[var(--surface-secondary)]">
|
|
{/* Collapsed Agent Icons with ZFC Rings */}
|
|
<div className="flex flex-col gap-4">
|
|
{agentRoster.slice(0, 6).map(agent => (
|
|
@@ -316,7 +316,7 @@ export function ActivityPanel({ issues, collapsed = false, projectRoot }: Activi
|
|
agent.status === 'stale' ? 'ring-[#D4A574]/45' :
|
|
agent.status === 'stuck' ? 'ring-[#C97A7A]/45' : 'ring-[#A78A94]/40'
|
|
)}>
|
|
- <AvatarFallback className="text-[10px] font-bold bg-[#1a1a1a] text-text-muted">
|
|
+ <AvatarFallback className="text-[10px] font-bold bg-[var(--surface-primary)] text-text-muted">
|
|
{getInitials(agent.name)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
@@ -340,9 +340,9 @@ export function ActivityPanel({ issues, collapsed = false, projectRoot }: Activi
|
|
}
|
|
|
|
return (
|
|
- <div className="flex flex-col h-full bg-[#070f19] backdrop-blur-xl">
|
|
+ <div className="flex flex-col h-full bg-[var(--surface-secondary)]">
|
|
{/* AGENT ROSTER SECTION */}
|
|
- <div className="flex-shrink-0 p-4 bg-[#0b1625] shadow-[0_16px_24px_-24px_rgba(0,0,0,0.9)]">
|
|
+ <div className="flex-shrink-0 p-4 bg-[var(--surface-primary)] border-b border-[var(--border-subtle)]">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse shadow-[0_0_8px_#10b981]" />
|
|
@@ -368,7 +368,7 @@ export function ActivityPanel({ issues, collapsed = false, projectRoot }: Activi
|
|
getAgentTone(agent.status).glowClass
|
|
)} />
|
|
<Avatar className={cn("h-8 w-8 relative z-10 ring-1", getAgentTone(agent.status).ringClass)}>
|
|
- <AvatarFallback className="text-[10px] font-bold bg-[#252525]">
|
|
+ <AvatarFallback className="text-[10px] font-bold bg-[var(--surface-primary)]">
|
|
{getInitials(agent.name)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
diff --git a/src/components/activity/swarm-command-feed.tsx b/src/components/activity/swarm-command-feed.tsx
|
|
index 5976f14..b54aedc 100644
|
|
--- a/src/components/activity/swarm-command-feed.tsx
|
|
+++ b/src/components/activity/swarm-command-feed.tsx
|
|
@@ -76,9 +76,9 @@ export function SwarmCommandFeed({ epicId, issues, projectRoot }: SwarmCommandFe
|
|
}, [projectRoot, contextBeadIds]);
|
|
|
|
return (
|
|
- <div className="flex flex-col h-full bg-[#050a10] border-l border-[var(--ui-border-soft)]">
|
|
+ <div className="flex flex-col h-full bg-[var(--surface-secondary)] border-l border-[var(--ui-border-soft)]">
|
|
{/* SQUAD ROSTER SECTION */}
|
|
- <div className="flex-shrink-0 p-4 bg-[#0a111a] shadow-[0_16px_24px_-24px_rgba(0,0,0,0.9)] z-10">
|
|
+ <div className="flex-shrink-0 p-4 bg-[var(--surface-primary)] shadow-[0_16px_24px_-24px_rgba(0,0,0,0.9)] z-10">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse shadow-[0_0_8px_#10b981]" />
|
|
@@ -96,7 +96,7 @@ export function SwarmCommandFeed({ epicId, issues, projectRoot }: SwarmCommandFe
|
|
) : (
|
|
<div className="grid grid-cols-1 gap-2">
|
|
{rosterEntries.map((agent, i) => (
|
|
- <div key={i} className="flex gap-3 p-2.5 bg-[#0f1824] border border-[var(--ui-border-soft)] rounded-xl items-center shadow-lg transition-all hover:border-[var(--ui-accent-info)]/30">
|
|
+ <div key={i} className="flex gap-3 p-2.5 bg-[var(--surface-elevated)] border border-[var(--ui-border-soft)] rounded-xl items-center shadow-lg transition-all hover:border-[var(--ui-accent-info)]/30">
|
|
<div className="relative">
|
|
<div className="absolute -inset-0.5 rounded-full blur-[2px] opacity-70 bg-emerald-500/20" />
|
|
<Avatar className="h-9 w-9 relative z-10 ring-2 ring-emerald-500/40">
|
|
diff --git a/src/components/graph/assignment-panel.tsx b/src/components/graph/assignment-panel.tsx
|
|
index 055aa2b..c481d68 100644
|
|
--- a/src/components/graph/assignment-panel.tsx
|
|
+++ b/src/components/graph/assignment-panel.tsx
|
|
@@ -18,6 +18,7 @@ export interface AssignmentPanelProps {
|
|
projectRoot: string;
|
|
issues: BeadIssue[];
|
|
epicId?: string;
|
|
+ onIssueUpdated?: () => void;
|
|
}
|
|
|
|
function hasAgentLabel(labels: string[]): boolean {
|
|
@@ -38,13 +39,17 @@ function truncateTitle(title: string, maxLength: number = 30): string {
|
|
}
|
|
|
|
function getTemplateId(issue: BeadIssue): string | null {
|
|
+ const templateLabel = issue.labels?.find(l => l.startsWith('template:'));
|
|
+ if (templateLabel) {
|
|
+ return templateLabel.replace('template:', '');
|
|
+ }
|
|
if (issue.metadata?.templateId && typeof issue.metadata.templateId === 'string') {
|
|
return issue.metadata.templateId;
|
|
}
|
|
return issue.templateId;
|
|
}
|
|
|
|
-export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }: AssignmentPanelProps) {
|
|
+export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId, onIssueUpdated }: AssignmentPanelProps) {
|
|
const [inspectingArchetypeId, setInspectingArchetypeId] = useState<string | null>(null);
|
|
const [inspectingTemplateId, setInspectingTemplateId] = useState<string | null>(null);
|
|
const [showArchetypeList, setShowArchetypeList] = useState(false);
|
|
@@ -127,21 +132,28 @@ export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }:
|
|
|
|
const handleApplyTemplateToEpic = async (templateId: string, targetEpicId: string) => {
|
|
try {
|
|
+ const epic = issues.find(issue => issue.id === targetEpicId);
|
|
+ const currentLabels = epic?.labels || [];
|
|
+ const newLabels = [...currentLabels.filter(l => !l.startsWith('template:')), `template:${templateId}`];
|
|
+
|
|
const res = await fetch('/api/beads/update', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
projectRoot,
|
|
id: targetEpicId,
|
|
- metadata: { templateId }
|
|
+ labels: newLabels
|
|
})
|
|
});
|
|
|
|
if (!res.ok) {
|
|
- throw new Error('Failed to apply template');
|
|
+ const errorData = await res.json();
|
|
+ console.error('Template API error:', errorData);
|
|
+ throw new Error(errorData?.error?.message || 'Failed to apply template');
|
|
}
|
|
|
|
console.log('Template applied successfully:', { templateId, epicId: targetEpicId });
|
|
+ onIssueUpdated?.();
|
|
} catch (error) {
|
|
console.error('Failed to apply template:', error);
|
|
}
|
|
@@ -149,21 +161,28 @@ export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }:
|
|
|
|
const handleRemoveTemplateFromEpic = async (targetEpicId: string) => {
|
|
try {
|
|
+ const epic = issues.find(issue => issue.id === targetEpicId);
|
|
+ const currentLabels = epic?.labels || [];
|
|
+ const newLabels = currentLabels.filter(l => !l.startsWith('template:'));
|
|
+
|
|
const res = await fetch('/api/beads/update', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
projectRoot,
|
|
id: targetEpicId,
|
|
- metadata: { templateId: null }
|
|
+ labels: newLabels
|
|
})
|
|
});
|
|
|
|
if (!res.ok) {
|
|
- throw new Error('Failed to remove template');
|
|
+ const errorData = await res.json();
|
|
+ console.error('Template API error:', errorData);
|
|
+ throw new Error(errorData?.error?.message || 'Failed to remove template');
|
|
}
|
|
|
|
console.log('Template removed successfully');
|
|
+ onIssueUpdated?.();
|
|
} catch (error) {
|
|
console.error('Failed to remove template:', error);
|
|
}
|
|
diff --git a/src/components/kanban/kanban-page.tsx b/src/components/kanban/kanban-page.tsx
|
|
index 8923ace..7e3540d 100644
|
|
--- a/src/components/kanban/kanban-page.tsx
|
|
+++ b/src/components/kanban/kanban-page.tsx
|
|
@@ -107,6 +107,7 @@ export function KanbanPage({
|
|
);
|
|
const graphHref = useMemo(() => {
|
|
const params = new URLSearchParams();
|
|
+ params.set('view', 'graph');
|
|
if (projectScopeMode !== 'single') {
|
|
params.set('mode', projectScopeMode);
|
|
}
|
|
@@ -114,7 +115,7 @@ export function KanbanPage({
|
|
params.set('project', projectScopeKey);
|
|
}
|
|
const query = params.toString();
|
|
- return query ? `/graph?${query}` : '/graph';
|
|
+ return query ? `/?${query}` : '/?view=graph';
|
|
}, [projectScopeKey, projectScopeMode]);
|
|
const allowMutations = projectScopeMode === 'single';
|
|
const blockedTree = useMemo(
|
|
diff --git a/src/components/sessions/conversation-drawer.tsx b/src/components/sessions/conversation-drawer.tsx
|
|
index ca002fd..ef013fe 100644
|
|
--- a/src/components/sessions/conversation-drawer.tsx
|
|
+++ b/src/components/sessions/conversation-drawer.tsx
|
|
@@ -14,6 +14,47 @@ interface ThreadItem {
|
|
data: any;
|
|
}
|
|
|
|
+export type CoordMessageAction = 'read' | 'ack';
|
|
+
|
|
+export function buildCoordMessageActionEvent(params: {
|
|
+ action: CoordMessageAction;
|
|
+ message: AgentMessage;
|
|
+ beadId: string;
|
|
+ projectRoot: string;
|
|
+ nowIso?: string;
|
|
+}): Record<string, unknown> {
|
|
+ const now = params.nowIso ?? new Date().toISOString();
|
|
+ const eventType = params.action === 'read' ? 'READ' : 'ACK';
|
|
+ const compactNow = now.replace(/[-:.TZ]/g, '');
|
|
+ return {
|
|
+ version: 'coord.v1',
|
|
+ kind: 'coord_event',
|
|
+ issue_id: params.beadId,
|
|
+ actor: params.message.to_agent,
|
|
+ timestamp: now,
|
|
+ data: {
|
|
+ event_type: eventType,
|
|
+ event_id: `evt_${eventType.toLowerCase()}_${compactNow}_${params.message.message_id}`,
|
|
+ event_ref: params.message.message_id,
|
|
+ project_root: params.projectRoot,
|
|
+ payload: {},
|
|
+ },
|
|
+ };
|
|
+}
|
|
+
|
|
+export function buildCommentMutationBody(params: {
|
|
+ projectRoot: string;
|
|
+ text: string;
|
|
+ actor?: string;
|
|
+}): Record<string, unknown> {
|
|
+ const actor = params.actor?.trim();
|
|
+ return {
|
|
+ projectRoot: params.projectRoot,
|
|
+ text: params.text,
|
|
+ ...(actor ? { actor } : {}),
|
|
+ };
|
|
+}
|
|
+
|
|
interface ConversationDrawerProps {
|
|
beadId: string | null;
|
|
bead: BeadIssue | null;
|
|
@@ -44,6 +85,7 @@ export function ConversationDrawer({
|
|
const [thread, setThread] = useState<ThreadItem[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [commentText, setCommentText] = useState('');
|
|
+ const [commentActor, setCommentActor] = useState('');
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [metrics, setMetrics] = useState<AgentMetrics | null>(null);
|
|
const [showSummary, setShowSummary] = useState(false);
|
|
@@ -77,6 +119,16 @@ export function ConversationDrawer({
|
|
}
|
|
}, [agentId, projectRoot]);
|
|
|
|
+ useEffect(() => {
|
|
+ if (typeof window === 'undefined') return;
|
|
+ try {
|
|
+ const stored = window.localStorage.getItem('bb.humanActor');
|
|
+ if (stored) setCommentActor(stored);
|
|
+ } catch {
|
|
+ // ignore storage failures
|
|
+ }
|
|
+ }, []);
|
|
+
|
|
useEffect(() => {
|
|
if (open) {
|
|
if (beadId) fetchConversation({ silent: refreshTrigger > 0 });
|
|
@@ -107,9 +159,16 @@ export function ConversationDrawer({
|
|
const res = await fetch(`/api/sessions/${beadId}/comment`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
- body: JSON.stringify({ projectRoot, text: commentText })
|
|
+ body: JSON.stringify(buildCommentMutationBody({ projectRoot, text: commentText, actor: commentActor })),
|
|
});
|
|
if (res.ok) {
|
|
+ try {
|
|
+ if (commentActor.trim()) {
|
|
+ window.localStorage.setItem('bb.humanActor', commentActor.trim());
|
|
+ }
|
|
+ } catch {
|
|
+ // ignore storage failures
|
|
+ }
|
|
setCommentText('');
|
|
await fetchConversation();
|
|
onActivity?.();
|
|
@@ -127,8 +186,16 @@ export function ConversationDrawer({
|
|
if (!message) return;
|
|
|
|
try {
|
|
- const res = await fetch(`/api/sessions/${beadId}/messages/${messageId}/${action}?agent=${encodeURIComponent(message.to_agent)}`, {
|
|
- method: 'POST'
|
|
+ const event = buildCoordMessageActionEvent({
|
|
+ action,
|
|
+ message,
|
|
+ beadId,
|
|
+ projectRoot,
|
|
+ });
|
|
+ const res = await fetch('/api/coord/events', {
|
|
+ method: 'POST',
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
+ body: JSON.stringify({ projectRoot, event }),
|
|
});
|
|
if (res.ok) {
|
|
await fetchConversation();
|
|
@@ -259,6 +326,12 @@ export function ConversationDrawer({
|
|
{beadId && !showSummary && (
|
|
<footer className="border-t border-white/5 bg-white/[0.01] p-6 flex-none shadow-[0_-12px_32px_rgba(0,0,0,0.2)]">
|
|
<form onSubmit={handleAddComment} className="space-y-4">
|
|
+ <input
|
|
+ value={commentActor}
|
|
+ onChange={(e) => setCommentActor(e.target.value)}
|
|
+ placeholder="Comment as (username)"
|
|
+ className="w-full rounded-xl border border-white/10 bg-black/30 px-3 py-2 text-xs text-text-body outline-none transition-all focus:border-sky-500/50 focus:ring-1 focus:ring-sky-500/20 placeholder:text-text-muted/30"
|
|
+ />
|
|
<textarea
|
|
value={commentText}
|
|
onChange={(e) => setCommentText(e.target.value)}
|
|
@@ -420,4 +493,4 @@ function ThreadRow({ item, onRead, onAck }: {
|
|
)}
|
|
</div>
|
|
);
|
|
-}
|
|
\ No newline at end of file
|
|
+}
|
|
diff --git a/src/components/shared/left-panel.tsx b/src/components/shared/left-panel.tsx
|
|
index b6503c7..adf5470 100644
|
|
--- a/src/components/shared/left-panel.tsx
|
|
+++ b/src/components/shared/left-panel.tsx
|
|
@@ -1,7 +1,7 @@
|
|
'use client';
|
|
|
|
import { useMemo, useState } from 'react';
|
|
-import { ChevronDown, ChevronRight, Folder, FolderOpen, Star } from 'lucide-react';
|
|
+import { ChevronDown, ChevronRight, Folder, FolderOpen, Pencil, Star } from 'lucide-react';
|
|
|
|
import type { BeadIssue } from '../../lib/types';
|
|
import { cn } from '../../lib/utils';
|
|
@@ -23,6 +23,7 @@ export interface LeftPanelProps {
|
|
issues: BeadIssue[];
|
|
selectedEpicId?: string | null;
|
|
onEpicSelect?: (epicId: string | null) => void;
|
|
+ onEpicEdit?: (epicId: string) => void;
|
|
filters: LeftPanelFilters;
|
|
onFiltersChange: (filters: LeftPanelFilters) => void;
|
|
}
|
|
@@ -172,7 +173,7 @@ function isTaskMatch(task: BeadIssue, filters: LeftPanelFilters): boolean {
|
|
return true;
|
|
}
|
|
|
|
-export function LeftPanel({ issues, selectedEpicId, onEpicSelect, filters, onFiltersChange }: LeftPanelProps) {
|
|
+export function LeftPanel({ issues, selectedEpicId, onEpicSelect, onEpicEdit, filters, onFiltersChange }: LeftPanelProps) {
|
|
const { view, setView } = useUrlState();
|
|
const entries = useMemo(() => buildEntries(issues), [issues]);
|
|
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
|
@@ -369,6 +370,16 @@ export function LeftPanel({ issues, selectedEpicId, onEpicSelect, filters, onFil
|
|
>
|
|
<Star className="h-3 w-3" aria-hidden="true" />
|
|
</button>
|
|
+ {onEpicEdit && (
|
|
+ <button
|
|
+ type="button"
|
|
+ onClick={() => onEpicEdit(epic.id)}
|
|
+ className="inline-flex h-5 w-5 items-center justify-center rounded text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]"
|
|
+ aria-label={`Edit ${epic.title}`}
|
|
+ >
|
|
+ <Pencil className="h-3 w-3" aria-hidden="true" />
|
|
+ </button>
|
|
+ )}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 text-[11px]">
|
|
diff --git a/src/components/shared/thread-drawer.tsx b/src/components/shared/thread-drawer.tsx
|
|
index 79e8978..e58380d 100644
|
|
--- a/src/components/shared/thread-drawer.tsx
|
|
+++ b/src/components/shared/thread-drawer.tsx
|
|
@@ -198,23 +198,24 @@ export function ThreadDrawer({
|
|
};
|
|
|
|
const handleCommentSubmit = async () => {
|
|
- if (!projectRoot || !id || !comment.trim()) {
|
|
+ const targetIssueId = issue?.id ?? '';
|
|
+ if (!projectRoot || !targetIssueId || !comment.trim()) {
|
|
return;
|
|
}
|
|
|
|
setCommentState('sending');
|
|
|
|
try {
|
|
- await postComment(projectRoot, id, comment.trim());
|
|
+ await postComment(projectRoot, targetIssueId, comment.trim());
|
|
setComment('');
|
|
setCommentState('sent');
|
|
// Refresh comments
|
|
- const response = await fetch(`/api/beads/${id}/comments?projectRoot=${encodeURIComponent(projectRoot)}`);
|
|
+ const response = await fetch(`/api/beads/${targetIssueId}/comments?projectRoot=${encodeURIComponent(projectRoot)}`);
|
|
const payload = (await response.json()) as { ok: boolean; comments?: CommentFromApi[] };
|
|
if (payload.ok && payload.comments) {
|
|
setComments(payload.comments);
|
|
}
|
|
- await onIssueUpdated?.(id);
|
|
+ await onIssueUpdated?.(targetIssueId);
|
|
setTimeout(() => setCommentState('ready'), 900);
|
|
} catch (error) {
|
|
console.error('Comment failed:', error);
|
|
@@ -473,13 +474,13 @@ export function ThreadDrawer({
|
|
placeholder="Type a message to neighbors..."
|
|
className="border-0 bg-transparent text-[var(--ui-text-primary)] placeholder:text-[var(--ui-text-muted)]"
|
|
autoComplete="off"
|
|
- disabled={commentState === 'sending'}
|
|
+ disabled={commentState === 'sending' || !issue || !projectRoot}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
className="h-8 rounded-full bg-[var(--ui-accent-action-green)] px-3 text-[#082012] hover:bg-[color-mix(in_srgb,var(--ui-accent-action-green)_86%,white)] disabled:opacity-50"
|
|
onClick={() => void handleCommentSubmit()}
|
|
- disabled={!comment.trim() || commentState === 'sending'}
|
|
+ disabled={!comment.trim() || commentState === 'sending' || !issue || !projectRoot}
|
|
>
|
|
<Send className="h-3.5 w-3.5" />
|
|
</Button>
|
|
diff --git a/src/components/shared/unified-shell.tsx b/src/components/shared/unified-shell.tsx
|
|
index 9db756b..3dbbb13 100644
|
|
--- a/src/components/shared/unified-shell.tsx
|
|
+++ b/src/components/shared/unified-shell.tsx
|
|
@@ -19,6 +19,7 @@ import { ContextualRightPanel } from '../activity/contextual-right-panel';
|
|
import { AssignmentPanel } from '../graph/assignment-panel';
|
|
import { useSwarmList } from '../../hooks/use-swarm-list';
|
|
import { useBeadsSubscription } from '../../hooks/use-beads-subscription';
|
|
+import { useBdHealth } from '../../hooks/use-bd-health';
|
|
|
|
export interface UnifiedShellProps {
|
|
issues: BeadIssue[];
|
|
@@ -55,6 +56,7 @@ export function UnifiedShell({
|
|
|
|
const socialCards = useMemo(() => buildSocialCards(issues), [issues]);
|
|
const { swarms: swarmCards } = useSwarmList(projectRoot);
|
|
+ const bdHealth = useBdHealth(projectRoot);
|
|
|
|
const selectedSocialCard = taskId ? socialCards.find(c => c.id === taskId) : null;
|
|
const selectedSwarmCard = swarmId ? swarmCards.find(c => c.swarmId === swarmId) : null;
|
|
@@ -89,9 +91,11 @@ export function UnifiedShell({
|
|
}, []);
|
|
|
|
// Chat Mode Logic: If a card is selected (drawer='open'), we show Chat popup
|
|
- const isChatOpen = drawer === 'open' && (!!taskId || !!swarmId);
|
|
- const drawerTitle = selectedSocialCard?.title || selectedSwarmCard?.title || '';
|
|
- const drawerId = taskId || swarmId || '';
|
|
+ const isChatOpen = drawer === 'open' && (!!taskId || !!swarmId || !!epicId);
|
|
+ const selectedEpic = epicId ? issues.find((issue) => issue.id === epicId && issue.issue_type === 'epic') ?? null : null;
|
|
+ const drawerTitle = selectedSocialCard?.title || selectedSwarmCard?.title || selectedEpic?.title || '';
|
|
+ const drawerId = taskId || swarmId || epicId || '';
|
|
+ const selectedItem = selectedEpic ?? selectedIssue;
|
|
|
|
// Panel resize hook
|
|
const { leftWidth, rightWidth, handleLeftResize, handleRightResize } = usePanelResize();
|
|
@@ -149,6 +153,7 @@ export function UnifiedShell({
|
|
projectRoot={projectRoot}
|
|
issues={issues}
|
|
epicId={epicId ?? undefined}
|
|
+ onIssueUpdated={async () => { router.refresh(); }}
|
|
/>
|
|
);
|
|
}
|
|
@@ -161,6 +166,11 @@ export function UnifiedShell({
|
|
<div className="flex flex-col h-screen bg-[var(--surface-backdrop)]" data-testid="unified-shell">
|
|
{/* TOP BAR: 3rem fixed */}
|
|
<TopBar />
|
|
+ {!bdHealth.loading && !bdHealth.healthy ? (
|
|
+ <div className="border-b border-amber-500/35 bg-amber-500/12 px-4 py-2 text-xs text-amber-100">
|
|
+ <span className="font-semibold">BD setup issue:</span> {bdHealth.message}
|
|
+ </div>
|
|
+ ) : null}
|
|
|
|
{/* MAIN AREA: Flex layout for resizable panels */}
|
|
<div
|
|
@@ -173,6 +183,7 @@ export function UnifiedShell({
|
|
issues={issues}
|
|
selectedEpicId={epicId}
|
|
onEpicSelect={setEpicId}
|
|
+ onEpicEdit={(id) => { setEpicId(id); setDrawer('open'); }}
|
|
filters={filters}
|
|
onFiltersChange={setFilters}
|
|
/>
|
|
@@ -209,7 +220,7 @@ export function UnifiedShell({
|
|
title={drawerTitle}
|
|
id={drawerId}
|
|
embedded={true}
|
|
- issue={selectedIssue}
|
|
+ issue={selectedItem}
|
|
projectRoot={projectRoot}
|
|
onIssueUpdated={async () => {
|
|
router.refresh();
|
|
diff --git a/src/hooks/use-panel-resize.ts b/src/hooks/use-panel-resize.ts
|
|
index 1c74386..d3f2b80 100644
|
|
--- a/src/hooks/use-panel-resize.ts
|
|
+++ b/src/hooks/use-panel-resize.ts
|
|
@@ -10,25 +10,35 @@ export const MIN_LEFT_WIDTH = 192;
|
|
export const MIN_RIGHT_WIDTH = 256;
|
|
|
|
export function usePanelResize() {
|
|
- const [leftWidth, setLeftWidth] = useState(() => {
|
|
- if (typeof window === 'undefined') return DEFAULT_LEFT_WIDTH;
|
|
- const saved = localStorage.getItem(LEFT_PANEL_KEY);
|
|
- return saved ? parseInt(saved, 10) : DEFAULT_LEFT_WIDTH;
|
|
- });
|
|
-
|
|
- const [rightWidth, setRightWidth] = useState(() => {
|
|
- if (typeof window === 'undefined') return DEFAULT_RIGHT_WIDTH;
|
|
- const saved = localStorage.getItem(RIGHT_PANEL_KEY);
|
|
- return saved ? parseInt(saved, 10) : DEFAULT_RIGHT_WIDTH;
|
|
- });
|
|
+ const [leftWidth, setLeftWidth] = useState(DEFAULT_LEFT_WIDTH);
|
|
+ const [rightWidth, setRightWidth] = useState(DEFAULT_RIGHT_WIDTH);
|
|
+ const [mounted, setMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
- localStorage.setItem(LEFT_PANEL_KEY, String(leftWidth));
|
|
- }, [leftWidth]);
|
|
+ const savedLeft = localStorage.getItem(LEFT_PANEL_KEY);
|
|
+ const savedRight = localStorage.getItem(RIGHT_PANEL_KEY);
|
|
+
|
|
+ if (savedLeft) {
|
|
+ setLeftWidth(parseInt(savedLeft, 10));
|
|
+ }
|
|
+ if (savedRight) {
|
|
+ setRightWidth(parseInt(savedRight, 10));
|
|
+ }
|
|
+
|
|
+ setMounted(true);
|
|
+ }, []);
|
|
+
|
|
+ useEffect(() => {
|
|
+ if (mounted) {
|
|
+ localStorage.setItem(LEFT_PANEL_KEY, String(leftWidth));
|
|
+ }
|
|
+ }, [leftWidth, mounted]);
|
|
|
|
useEffect(() => {
|
|
- localStorage.setItem(RIGHT_PANEL_KEY, String(rightWidth));
|
|
- }, [rightWidth]);
|
|
+ if (mounted) {
|
|
+ localStorage.setItem(RIGHT_PANEL_KEY, String(rightWidth));
|
|
+ }
|
|
+ }, [rightWidth, mounted]);
|
|
|
|
const clampLeftWidth = useCallback((width: number) => {
|
|
const maxWidth = Math.floor(window.innerWidth * 0.30);
|
|
diff --git a/src/lib/agent-sessions.ts b/src/lib/agent-sessions.ts
|
|
index d46001b..6cc47e8 100644
|
|
--- a/src/lib/agent-sessions.ts
|
|
+++ b/src/lib/agent-sessions.ts
|
|
@@ -1,8 +1,13 @@
|
|
import type { ActivityEvent } from './activity';
|
|
import type { BeadIssue } from './types';
|
|
import { listAgents, deriveLiveness } from './agent-registry';
|
|
-import { inboxAgentMessages, type AgentMessage } from './agent-mail';
|
|
-import { statusAgentReservations, classifyOverlap } from './agent-reservations';
|
|
+import type { AgentMessage } from './agent-mail';
|
|
+import {
|
|
+ calculateReservationIncursions,
|
|
+ projectInboxFromDisk,
|
|
+ projectReservations,
|
|
+ readCoordEventsFromDisk,
|
|
+} from './coord-projections';
|
|
|
|
export type AgentSessionState = 'active' | 'reviewing' | 'deciding' | 'needs_input' | 'completed' | 'stale' | 'evicted' | 'idle' | 'stuck' | 'dead';
|
|
|
|
@@ -148,56 +153,21 @@ export interface Incursion {
|
|
/**
|
|
* Calculates global incursions by comparing all active reservations.
|
|
*/
|
|
-export async function calculateIncursions(): Promise<Incursion[]> {
|
|
- const statusResult = await statusAgentReservations({});
|
|
- if (!statusResult.ok || !statusResult.data) return [];
|
|
-
|
|
- const reservations = statusResult.data.reservations;
|
|
- const incursions: Incursion[] = [];
|
|
- const processedPairs = new Set<string>();
|
|
-
|
|
- for (let i = 0; i < reservations.length; i++) {
|
|
- for (let j = i + 1; j < reservations.length; j++) {
|
|
- const resA = reservations[i];
|
|
- const resB = reservations[j];
|
|
-
|
|
- // Don't compare an agent against themselves
|
|
- if (resA.agent_id === resB.agent_id) continue;
|
|
-
|
|
- const overlap = classifyOverlap(resA.scope, resB.scope);
|
|
- if (overlap !== 'disjoint') {
|
|
- const key = [resA.agent_id, resB.agent_id].sort().join(':') + ':' + [resA.scope, resB.scope].sort().join('|');
|
|
- if (processedPairs.has(key)) continue;
|
|
- processedPairs.add(key);
|
|
-
|
|
- incursions.push({
|
|
- scope: overlap === 'exact' ? resA.scope : `${resA.scope} ↔ ${resB.scope}`,
|
|
- agents: [resA.agent_id, resB.agent_id],
|
|
- severity: overlap
|
|
- });
|
|
- }
|
|
- }
|
|
- }
|
|
-
|
|
- return incursions;
|
|
+export async function calculateIncursions(
|
|
+ projectRoot: string = process.cwd(),
|
|
+ agentLivenessMap: Record<string, string> = {},
|
|
+): Promise<Incursion[]> {
|
|
+ const events = await readCoordEventsFromDisk(projectRoot);
|
|
+ const reservations = projectReservations(events, agentLivenessMap);
|
|
+ return calculateReservationIncursions(reservations);
|
|
}
|
|
|
|
/**
|
|
* Gathers all relevant communication for all agents to build a summary for aggregation.
|
|
*/
|
|
-export async function getCommunicationSummary(): Promise<CommunicationSummary> {
|
|
- const agentsResult = await listAgents({});
|
|
- const agents = agentsResult.data ?? [];
|
|
- const allMessages: AgentMessage[] = [];
|
|
-
|
|
- for (const agent of agents) {
|
|
- const inbox = await inboxAgentMessages({ agent: agent.agent_id });
|
|
- if (inbox.data) {
|
|
- allMessages.push(...inbox.data);
|
|
- }
|
|
- }
|
|
-
|
|
- return { messages: allMessages };
|
|
+export async function getCommunicationSummary(projectRoot: string = process.cwd()): Promise<CommunicationSummary> {
|
|
+ const coordMessages = await projectInboxFromDisk(projectRoot);
|
|
+ return { messages: coordMessages };
|
|
}
|
|
|
|
export interface AgentMetrics {
|
|
diff --git a/src/lib/bd-path.ts b/src/lib/bd-path.ts
|
|
deleted file mode 100644
|
|
index 6ab3be3..0000000
|
|
--- a/src/lib/bd-path.ts
|
|
+++ /dev/null
|
|
@@ -1,78 +0,0 @@
|
|
-import fs from 'node:fs/promises';
|
|
-import path from 'node:path';
|
|
-
|
|
-export interface ResolveBdExecutableOptions {
|
|
- explicitPath?: string | null;
|
|
- env?: NodeJS.ProcessEnv;
|
|
-}
|
|
-
|
|
-export interface BdExecutableResolution {
|
|
- executable: string;
|
|
- source: 'config' | 'path';
|
|
-}
|
|
-
|
|
-export class BdExecutableNotFoundError extends Error {
|
|
- readonly code = 'BD_NOT_FOUND';
|
|
-
|
|
- constructor(message: string) {
|
|
- super(message);
|
|
- this.name = 'BdExecutableNotFoundError';
|
|
- }
|
|
-}
|
|
-
|
|
-async function fileExists(filePath: string): Promise<boolean> {
|
|
- try {
|
|
- await fs.access(filePath);
|
|
- return true;
|
|
- } catch {
|
|
- return false;
|
|
- }
|
|
-}
|
|
-
|
|
-function splitEnvPath(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
- const value = env.Path ?? env.PATH ?? '';
|
|
- if (!value.trim()) {
|
|
- return [];
|
|
- }
|
|
-
|
|
- return value.split(';').map((segment) => segment.trim()).filter(Boolean);
|
|
-}
|
|
-
|
|
-function executableCandidates(directory: string): string[] {
|
|
- return ['bd.exe', 'bd.cmd', 'bd.bat', 'bd'].map((name) => path.join(directory, name));
|
|
-}
|
|
-
|
|
-function buildNotFoundMessage(explicitPath?: string | null): string {
|
|
- const lines = [
|
|
- 'bd.exe was not found.',
|
|
- 'Install it with: npm install -g @beads/bd',
|
|
- 'Or configure an explicit executable path in request payload/config.',
|
|
- ];
|
|
-
|
|
- if (explicitPath) {
|
|
- lines.push(`Configured path was not found: ${explicitPath}`);
|
|
- }
|
|
-
|
|
- return lines.join(' ');
|
|
-}
|
|
-
|
|
-export async function resolveBdExecutable(options: ResolveBdExecutableOptions = {}): Promise<BdExecutableResolution> {
|
|
- if (options.explicitPath && options.explicitPath.trim()) {
|
|
- const explicit = path.resolve(options.explicitPath);
|
|
- if (await fileExists(explicit)) {
|
|
- return { executable: explicit, source: 'config' };
|
|
- }
|
|
-
|
|
- throw new BdExecutableNotFoundError(buildNotFoundMessage(options.explicitPath));
|
|
- }
|
|
-
|
|
- for (const dir of splitEnvPath(options.env)) {
|
|
- for (const candidate of executableCandidates(dir)) {
|
|
- if (await fileExists(candidate)) {
|
|
- return { executable: candidate, source: 'path' };
|
|
- }
|
|
- }
|
|
- }
|
|
-
|
|
- throw new BdExecutableNotFoundError(buildNotFoundMessage());
|
|
-}
|
|
diff --git a/src/lib/bridge.ts b/src/lib/bridge.ts
|
|
index 4895e3a..9b0da60 100644
|
|
--- a/src/lib/bridge.ts
|
|
+++ b/src/lib/bridge.ts
|
|
@@ -1,10 +1,7 @@
|
|
-import { exec as nodeExec } from 'node:child_process';
|
|
-import { promisify } from 'node:util';
|
|
+import { spawn } from 'node:child_process';
|
|
import path from 'node:path';
|
|
|
|
-import { BdExecutableNotFoundError, resolveBdExecutable } from './bd-path';
|
|
-
|
|
-const execAsync = promisify(nodeExec);
|
|
+import { normalizeProjectRootForRuntime } from './project-root';
|
|
|
|
export type BdFailureClassification = 'not_found' | 'timeout' | 'non_zero_exit' | 'bad_args' | 'unknown';
|
|
|
|
@@ -12,7 +9,9 @@ export interface RunBdCommandOptions {
|
|
projectRoot: string;
|
|
args: string[];
|
|
timeoutMs?: number;
|
|
+ // Deprecated: accepted for payload compatibility, ignored by runner.
|
|
explicitBdPath?: string | null;
|
|
+ stdinText?: string;
|
|
}
|
|
|
|
export interface RunBdCommandResult {
|
|
@@ -29,8 +28,10 @@ export interface RunBdCommandResult {
|
|
}
|
|
|
|
interface RunBdCommandDeps {
|
|
- resolveBdExecutable: typeof resolveBdExecutable;
|
|
- exec: (command: string, options: { cwd: string; timeout: number; env: NodeJS.ProcessEnv }) => Promise<{ stdout: string; stderr: string }>;
|
|
+ exec: (
|
|
+ command: string,
|
|
+ options: { cwd: string; timeout: number; env: NodeJS.ProcessEnv; stdinText?: string },
|
|
+ ) => Promise<{ stdout: string; stderr: string }>;
|
|
env: NodeJS.ProcessEnv;
|
|
}
|
|
|
|
@@ -39,29 +40,51 @@ function normalizeOutput(text: unknown): string {
|
|
return text.replaceAll('\r\n', '\n').trim();
|
|
}
|
|
|
|
+function getExitCode(error: unknown): number | null {
|
|
+ if (!error || typeof error !== 'object') return null;
|
|
+ const value = (error as { exitCode?: unknown }).exitCode;
|
|
+ return typeof value === 'number' ? value : null;
|
|
+}
|
|
+
|
|
function toErrorMessage(value: unknown): string {
|
|
if (value instanceof Error) return value.message;
|
|
return String(value ?? 'Unknown error');
|
|
}
|
|
|
|
function classifyFailure(error: NodeJS.ErrnoException & { stderr?: string; killed?: boolean; signal?: string }): BdFailureClassification {
|
|
+ const exitCode = getExitCode(error);
|
|
if (error.code === 'ENOENT') return 'not_found';
|
|
if (error.code === 'ETIMEDOUT' || error.killed || error.signal === 'SIGTERM') return 'timeout';
|
|
const stderr = normalizeOutput(error.stderr);
|
|
- if (typeof error.code === 'number') {
|
|
+ if (
|
|
+ /not recognized as an internal or external command/i.test(stderr) ||
|
|
+ /command not found/i.test(stderr) ||
|
|
+ /["']bd["'] is not recognized/i.test(stderr) ||
|
|
+ /bd: not found/i.test(stderr)
|
|
+ ) {
|
|
+ return 'not_found';
|
|
+ }
|
|
+ if (typeof error.code === 'number' || exitCode !== null) {
|
|
if (/(unknown|invalid|required|usage)/i.test(stderr)) return 'bad_args';
|
|
return 'non_zero_exit';
|
|
}
|
|
return 'unknown';
|
|
}
|
|
|
|
+function buildBdNotFoundMessage(): string {
|
|
+ return 'bd command not found in PATH. Install with: npm install -g @beads/bd';
|
|
+}
|
|
+
|
|
function buildShellCommand(executable: string, args: string[]): string {
|
|
+ const sanitizedExecutable = executable.replace(/^['"]+|['"]+$/g, '');
|
|
// Normalize to forward slashes for Windows shell compatibility
|
|
- const normalizedExe = executable.split(path.sep).join('/');
|
|
+ const normalizedExe = sanitizedExecutable.split(path.sep).join('/');
|
|
|
|
if (process.platform === 'win32') {
|
|
- // Windows: quote the executable path, leave simple args unquoted
|
|
- const quotedExe = `"${normalizedExe}"`;
|
|
+ // Windows: do not quote plain command tokens like `bd`; quote only when needed.
|
|
+ const quotedExe = /[\s&|<>()^"]/.test(normalizedExe)
|
|
+ ? `"${normalizedExe.replace(/"/g, '""')}"`
|
|
+ : normalizedExe;
|
|
const quotedArgs = args.map(a => {
|
|
if (/[\s&|<>()^"]/.test(a)) return `"${a.replace(/"/g, '""')}"`;
|
|
return a;
|
|
@@ -73,45 +96,109 @@ function buildShellCommand(executable: string, args: string[]): string {
|
|
}
|
|
}
|
|
|
|
+async function execShellCommand(
|
|
+ command: string,
|
|
+ options: { cwd: string; timeout: number; env: NodeJS.ProcessEnv; stdinText?: string },
|
|
+): Promise<{ stdout: string; stderr: string }> {
|
|
+ return new Promise((resolve, reject) => {
|
|
+ const shell = process.platform === 'win32' ? 'cmd.exe' : '/bin/sh';
|
|
+ const shellArgs = process.platform === 'win32' ? ['/d', '/s', '/c', command] : ['-lc', command];
|
|
+
|
|
+ const child = spawn(shell, shellArgs, {
|
|
+ cwd: options.cwd,
|
|
+ env: options.env,
|
|
+ stdio: 'pipe',
|
|
+ });
|
|
+
|
|
+ let stdout = '';
|
|
+ let stderr = '';
|
|
+ let timedOut = false;
|
|
+
|
|
+ const timer = setTimeout(() => {
|
|
+ timedOut = true;
|
|
+ child.kill('SIGTERM');
|
|
+ }, options.timeout);
|
|
+
|
|
+ child.stdout.on('data', (chunk: Buffer | string) => {
|
|
+ stdout += chunk.toString();
|
|
+ });
|
|
+ child.stderr.on('data', (chunk: Buffer | string) => {
|
|
+ stderr += chunk.toString();
|
|
+ });
|
|
+
|
|
+ child.on('error', (error) => {
|
|
+ clearTimeout(timer);
|
|
+ const wrapped = error as NodeJS.ErrnoException & { stdout?: string; stderr?: string };
|
|
+ wrapped.stdout = stdout;
|
|
+ wrapped.stderr = stderr;
|
|
+ reject(wrapped);
|
|
+ });
|
|
+
|
|
+ child.on('close', (code, signal) => {
|
|
+ clearTimeout(timer);
|
|
+ if (code === 0 && !timedOut) {
|
|
+ resolve({ stdout, stderr });
|
|
+ return;
|
|
+ }
|
|
+ const error = new Error(`Command failed with code ${code ?? 'null'}`) as NodeJS.ErrnoException & {
|
|
+ stdout?: string;
|
|
+ stderr?: string;
|
|
+ killed?: boolean;
|
|
+ signal?: string;
|
|
+ };
|
|
+ error.code = timedOut ? 'ETIMEDOUT' : 'BD_EXIT';
|
|
+ error.stdout = stdout;
|
|
+ error.stderr = stderr;
|
|
+ error.killed = timedOut;
|
|
+ error.signal = signal ?? undefined;
|
|
+ (error as { exitCode?: number }).exitCode = code ?? 1;
|
|
+ reject(error);
|
|
+ });
|
|
+
|
|
+ if (options.stdinText !== undefined) {
|
|
+ child.stdin.write(options.stdinText);
|
|
+ }
|
|
+ child.stdin.end();
|
|
+ });
|
|
+}
|
|
+
|
|
export async function runBdCommand(
|
|
options: RunBdCommandOptions,
|
|
injectedDeps?: Partial<RunBdCommandDeps>,
|
|
): Promise<RunBdCommandResult> {
|
|
const startedAt = Date.now();
|
|
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
- const cwd = options.projectRoot;
|
|
+ const cwd = normalizeProjectRootForRuntime(options.projectRoot);
|
|
const args = [...options.args];
|
|
if (process.env.BD_NO_DAEMON === 'true') {
|
|
args.unshift('--no-daemon');
|
|
}
|
|
|
|
const deps: RunBdCommandDeps = {
|
|
- resolveBdExecutable: injectedDeps?.resolveBdExecutable ?? resolveBdExecutable,
|
|
- exec: injectedDeps?.exec ?? execAsync,
|
|
+ exec: injectedDeps?.exec ?? execShellCommand,
|
|
env: injectedDeps?.env ?? process.env,
|
|
};
|
|
|
|
- let command = options.explicitBdPath ?? 'bd';
|
|
+ const command = 'bd';
|
|
|
|
try {
|
|
- const resolved = await deps.resolveBdExecutable({
|
|
- explicitPath: options.explicitBdPath,
|
|
- env: deps.env,
|
|
- });
|
|
- command = resolved.executable;
|
|
-
|
|
const shellCommand = buildShellCommand(command, args);
|
|
|
|
- const mingwBin = 'C:\\msys64\\mingw64\\bin';
|
|
- const existingPath = deps.env.Path ?? deps.env.PATH ?? '';
|
|
- const enhancedPath = existingPath.includes('mingw64')
|
|
- ? existingPath
|
|
- : `${mingwBin};${existingPath}`;
|
|
+ let env = deps.env;
|
|
+ if (process.platform === 'win32') {
|
|
+ const mingwBin = 'C:\\msys64\\mingw64\\bin';
|
|
+ const existingPath = deps.env.Path ?? deps.env.PATH ?? '';
|
|
+ const enhancedPath = existingPath.includes('mingw64')
|
|
+ ? existingPath
|
|
+ : `${mingwBin};${existingPath}`;
|
|
+ env = { ...deps.env, Path: enhancedPath, PATH: enhancedPath };
|
|
+ }
|
|
|
|
const { stdout, stderr } = await deps.exec(shellCommand, {
|
|
cwd,
|
|
timeout: timeoutMs,
|
|
- env: { ...deps.env, Path: enhancedPath, PATH: enhancedPath },
|
|
+ env,
|
|
+ stdinText: options.stdinText,
|
|
});
|
|
|
|
return {
|
|
@@ -127,39 +214,25 @@ export async function runBdCommand(
|
|
error: null,
|
|
};
|
|
} catch (rawError) {
|
|
- if (rawError instanceof BdExecutableNotFoundError) {
|
|
- return {
|
|
- success: false,
|
|
- classification: 'not_found',
|
|
- command,
|
|
- args,
|
|
- cwd,
|
|
- stdout: '',
|
|
- stderr: '',
|
|
- code: null,
|
|
- durationMs: Date.now() - startedAt,
|
|
- error: rawError.message,
|
|
- };
|
|
- }
|
|
-
|
|
const error = rawError as NodeJS.ErrnoException & {
|
|
stderr?: string;
|
|
stdout?: string;
|
|
killed?: boolean;
|
|
signal?: string;
|
|
};
|
|
+ const classification = classifyFailure(error);
|
|
|
|
return {
|
|
success: false,
|
|
- classification: classifyFailure(error),
|
|
+ classification,
|
|
command,
|
|
args,
|
|
cwd,
|
|
stdout: normalizeOutput(error.stdout),
|
|
stderr: normalizeOutput(error.stderr),
|
|
- code: typeof error.code === 'number' ? error.code : null,
|
|
+ code: typeof error.code === 'number' ? error.code : getExitCode(error),
|
|
durationMs: Date.now() - startedAt,
|
|
- error: toErrorMessage(error),
|
|
+ error: classification === 'not_found' ? buildBdNotFoundMessage() : toErrorMessage(error),
|
|
};
|
|
}
|
|
}
|
|
diff --git a/src/lib/mutations.ts b/src/lib/mutations.ts
|
|
index a7fc5a0..03d08bc 100644
|
|
--- a/src/lib/mutations.ts
|
|
+++ b/src/lib/mutations.ts
|
|
@@ -7,6 +7,7 @@ export type MutationStatus = 'open' | 'in_progress' | 'blocked' | 'deferred' | '
|
|
interface MutationBasePayload {
|
|
projectRoot: string;
|
|
bdPath?: string;
|
|
+ actor?: string;
|
|
}
|
|
|
|
export interface CreateMutationPayload extends MutationBasePayload {
|
|
@@ -155,6 +156,7 @@ function parseBasePayload(raw: unknown): MutationBasePayload {
|
|
return {
|
|
projectRoot: asNonEmptyString(data.projectRoot, 'projectRoot'),
|
|
bdPath: asOptionalString(data.bdPath),
|
|
+ actor: asOptionalString(data.actor),
|
|
};
|
|
}
|
|
|
|
@@ -235,7 +237,7 @@ function pushOptionalArg(args: string[], flag: string, value: string | undefined
|
|
|
|
function pushOptionalLabels(args: string[], labels: string[] | undefined): void {
|
|
if (labels && labels.length > 0) {
|
|
- args.push('-l', labels.join(','));
|
|
+ args.push('--set-labels', labels.join(','));
|
|
}
|
|
}
|
|
|
|
@@ -267,7 +269,7 @@ export function buildBdMutationArgs(operation: MutationOperation, payload: Mutat
|
|
pushOptionalArg(args, '-a', data.assignee);
|
|
pushOptionalLabels(args, data.labels);
|
|
if (data.metadata) {
|
|
- args.push('--metadata', JSON.stringify(data.metadata));
|
|
+ args.push(`--metadata=${JSON.stringify(data.metadata)}`);
|
|
}
|
|
args.push('--json');
|
|
return args;
|
|
@@ -303,11 +305,12 @@ export async function executeMutation(
|
|
deps: Partial<ExecuteMutationDeps> = {},
|
|
): Promise<MutationResponse> {
|
|
const runner = deps.runBdCommand ?? runBdCommand;
|
|
- const args = buildBdMutationArgs(operation, payload);
|
|
+ const args = payload.actor
|
|
+ ? ['--actor', payload.actor, ...buildBdMutationArgs(operation, payload)]
|
|
+ : buildBdMutationArgs(operation, payload);
|
|
const command = await runner({
|
|
projectRoot: payload.projectRoot,
|
|
args,
|
|
- explicitBdPath: payload.bdPath,
|
|
});
|
|
|
|
if (!command.success) {
|
|
@@ -317,7 +320,7 @@ export async function executeMutation(
|
|
command,
|
|
error: {
|
|
classification: command.classification ?? 'unknown',
|
|
- message: command.error ?? (command.stderr || 'Mutation command failed.'),
|
|
+ message: command.stderr || command.error || 'Mutation command failed.',
|
|
},
|
|
};
|
|
}
|
|
diff --git a/src/styles/themes/light.css b/src/styles/themes/light.css
|
|
index 8d8254a..6dca439 100644
|
|
--- a/src/styles/themes/light.css
|
|
+++ b/src/styles/themes/light.css
|
|
@@ -8,104 +8,135 @@
|
|
/* ==========================================================================
|
|
1. SURFACE LAYERS - Softer greys, not pure white
|
|
========================================================================== */
|
|
- --surface-backdrop: #e2e8f0;
|
|
- --surface-elevated: #f1f5f9;
|
|
- --surface-primary: #e8edf5;
|
|
- --surface-secondary: #f8fafc;
|
|
- --surface-tertiary: #cbd5e1;
|
|
- --surface-quaternary: #ffffff;
|
|
- --surface-overlay: #f1f5f9;
|
|
- --surface-input: #ffffff;
|
|
- --surface-hover: rgba(15, 23, 42, 0.06);
|
|
- --surface-active: rgba(59, 130, 246, 0.15);
|
|
+ --surface-backdrop: #e8edf3;
|
|
+ --surface-elevated: #f7f9fc;
|
|
+ --surface-primary: #dce4ee;
|
|
+ --surface-secondary: #f1f5f9;
|
|
+ --surface-tertiary: #e5ebf3;
|
|
+ --surface-quaternary: #f8fafc;
|
|
+ --surface-overlay: #eef3f8;
|
|
+ --surface-input: #fcfdff;
|
|
+ --surface-hover: rgba(15, 23, 42, 0.08);
|
|
+ --surface-active: rgba(37, 99, 235, 0.22);
|
|
--surface-tooltip: #1e293b;
|
|
|
|
/* ==========================================================================
|
|
2. BORDERS - Visible grey
|
|
========================================================================== */
|
|
- --border-subtle: rgba(71, 85, 105, 0.2);
|
|
- --border-default: rgba(71, 85, 105, 0.35);
|
|
- --border-strong: rgba(51, 65, 85, 0.5);
|
|
- --border-accent: rgba(37, 99, 235, 0.6);
|
|
+ --border-subtle: rgba(51, 65, 85, 0.28);
|
|
+ --border-default: rgba(51, 65, 85, 0.44);
|
|
+ --border-strong: rgba(30, 41, 59, 0.62);
|
|
+ --border-accent: rgba(29, 78, 216, 0.72);
|
|
|
|
/* ==========================================================================
|
|
3. TEXT - Dark slate (NOT white!)
|
|
========================================================================== */
|
|
- --text-primary: #0f172a;
|
|
- --text-secondary: #334155;
|
|
- --text-tertiary: #64748b;
|
|
- --text-disabled: #94a3b8;
|
|
+ --text-primary: #0b1324;
|
|
+ --text-secondary: #1f344e;
|
|
+ --text-tertiary: #4b6078;
|
|
+ --text-disabled: #7d92a9;
|
|
--text-inverse: #f8fafc;
|
|
|
|
/* ==========================================================================
|
|
4. ACCENTS - Vibrant but not neon
|
|
========================================================================== */
|
|
- --accent-info: #2563eb;
|
|
- --accent-success: #16a34a;
|
|
- --accent-warning: #d97706;
|
|
- --accent-danger: #dc2626;
|
|
+ --accent-info: #1d4ed8;
|
|
+ --accent-success: #15803d;
|
|
+ --accent-warning: #b45309;
|
|
+ --accent-danger: #b91c1c;
|
|
|
|
/* ==========================================================================
|
|
5. ACCENT GLOWS - Subtle on light
|
|
========================================================================== */
|
|
- --glow-info: 0 0 16px rgba(37, 99, 235, 0.2);
|
|
- --glow-success: 0 0 16px rgba(22, 163, 74, 0.2);
|
|
- --glow-warning: 0 0 16px rgba(217, 119, 6, 0.2);
|
|
- --glow-danger: 0 0 16px rgba(220, 38, 38, 0.2);
|
|
+ --glow-info: 0 0 18px rgba(29, 78, 216, 0.24);
|
|
+ --glow-success: 0 0 18px rgba(21, 128, 61, 0.22);
|
|
+ --glow-warning: 0 0 18px rgba(180, 83, 9, 0.24);
|
|
+ --glow-danger: 0 0 18px rgba(185, 28, 28, 0.22);
|
|
|
|
/* ==========================================================================
|
|
6. GRAPH COLORS
|
|
========================================================================== */
|
|
- --graph-node-default: rgba(255, 255, 255, 0.95);
|
|
- --graph-node-epic: rgba(37, 99, 235, 0.15);
|
|
- --graph-edge-default: rgba(71, 85, 105, 0.4);
|
|
- --graph-edge-selected: #2563eb;
|
|
- --graph-edge-cycle: #d97706;
|
|
+ --graph-node-default: rgba(248, 250, 252, 0.96);
|
|
+ --graph-node-epic: rgba(29, 78, 216, 0.18);
|
|
+ --graph-edge-default: rgba(51, 65, 85, 0.48);
|
|
+ --graph-edge-selected: #1d4ed8;
|
|
+ --graph-edge-cycle: #b45309;
|
|
|
|
/* ==========================================================================
|
|
7. SEMANTIC ALPHAS - Dark overlays for light theme
|
|
========================================================================== */
|
|
- --alpha-white-low: rgba(255, 255, 255, 0.6);
|
|
- --alpha-white-medium: rgba(255, 255, 255, 0.8);
|
|
- --alpha-white-high: rgba(255, 255, 255, 0.95);
|
|
- --alpha-black-low: rgba(15, 23, 42, 0.06);
|
|
- --alpha-black-medium: rgba(15, 23, 42, 0.12);
|
|
- --alpha-black-high: rgba(15, 23, 42, 0.2);
|
|
+ --alpha-white-low: rgba(255, 255, 255, 0.55);
|
|
+ --alpha-white-medium: rgba(255, 255, 255, 0.75);
|
|
+ --alpha-white-high: rgba(255, 255, 255, 0.9);
|
|
+ --alpha-black-low: rgba(15, 23, 42, 0.08);
|
|
+ --alpha-black-medium: rgba(15, 23, 42, 0.15);
|
|
+ --alpha-black-high: rgba(15, 23, 42, 0.24);
|
|
|
|
/* ==========================================================================
|
|
8. STATUS COLORS - More opaque for visibility
|
|
========================================================================== */
|
|
- --status-ready: rgba(22, 163, 74, 0.2);
|
|
- --status-in-progress: rgba(217, 119, 6, 0.2);
|
|
- --status-blocked: rgba(220, 38, 38, 0.2);
|
|
- --status-closed: rgba(100, 116, 139, 0.15);
|
|
- --status-deferred: rgba(100, 116, 139, 0.1);
|
|
+ --status-ready: rgba(21, 128, 61, 0.22);
|
|
+ --status-in-progress: rgba(180, 83, 9, 0.22);
|
|
+ --status-blocked: rgba(185, 28, 28, 0.22);
|
|
+ --status-closed: rgba(71, 85, 105, 0.18);
|
|
+ --status-deferred: rgba(71, 85, 105, 0.13);
|
|
|
|
/* ==========================================================================
|
|
9. SHADOWS - Softer on light
|
|
========================================================================== */
|
|
- --shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.1);
|
|
- --shadow-md: 0 4px 6px rgba(15, 23, 42, 0.12);
|
|
- --shadow-lg: 0 10px 15px rgba(15, 23, 42, 0.15);
|
|
+ --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.14);
|
|
+ --shadow-md: 0 6px 14px rgba(15, 23, 42, 0.16);
|
|
+ --shadow-lg: 0 14px 26px rgba(15, 23, 42, 0.2);
|
|
|
|
/* ==========================================================================
|
|
10. AGENT ROLE COLORS
|
|
========================================================================== */
|
|
- --agent-role-ui: #2563eb;
|
|
- --agent-role-graph: #16a34a;
|
|
+ --agent-role-ui: #1d4ed8;
|
|
+ --agent-role-graph: #15803d;
|
|
--agent-role-orchestrator: #7c3aed;
|
|
--agent-role-researcher: #ea580c;
|
|
|
|
/* ==========================================================================
|
|
11. SCROLLBARS
|
|
========================================================================== */
|
|
- --scrollbar-track: rgba(15, 23, 42, 0.05);
|
|
- --scrollbar-thumb: rgba(100, 116, 139, 0.3);
|
|
- --scrollbar-thumb-hover: rgba(71, 85, 105, 0.45);
|
|
+ --scrollbar-track: rgba(15, 23, 42, 0.08);
|
|
+ --scrollbar-thumb: rgba(71, 85, 105, 0.38);
|
|
+ --scrollbar-thumb-hover: rgba(51, 65, 85, 0.52);
|
|
|
|
/* ==========================================================================
|
|
12. CODE/SYNTAX
|
|
========================================================================== */
|
|
- --code-background: #e2e8f0;
|
|
- --code-text: #334155;
|
|
+ --code-background: #dbe5f1;
|
|
+ --code-text: #1f344e;
|
|
+
|
|
+ /* ==========================================================================
|
|
+ 14. LEGACY UI MAPPINGS - For components using --ui-* variables
|
|
+ ========================================================================== */
|
|
+ --ui-bg-app: var(--surface-backdrop);
|
|
+ --ui-bg-header: var(--surface-primary);
|
|
+ --ui-bg-shell: var(--surface-primary);
|
|
+ --ui-bg-panel: var(--surface-tertiary);
|
|
+ --ui-bg-main: var(--surface-backdrop);
|
|
+ --ui-bg-card: var(--surface-quaternary);
|
|
+ --ui-bg-elevated: var(--surface-elevated);
|
|
+
|
|
+ --ui-border-soft: var(--border-subtle);
|
|
+ --ui-border-strong: var(--border-default);
|
|
+
|
|
+ --ui-text-primary: var(--text-primary);
|
|
+ --ui-text-muted: var(--text-tertiary);
|
|
+
|
|
+ --ui-accent-ready: var(--accent-success);
|
|
+ --ui-accent-blocked: var(--accent-danger);
|
|
+ --ui-accent-warning: var(--accent-warning);
|
|
+ --ui-accent-info: var(--accent-info);
|
|
+}
|
|
+
|
|
+[data-theme="light"] body {
|
|
+ background-color: var(--ui-bg-app);
|
|
+ background-image:
|
|
+ radial-gradient(1200px 520px at 24% -12%, rgba(29, 78, 216, 0.09), transparent 62%),
|
|
+ radial-gradient(980px 420px at 82% 18%, rgba(21, 128, 61, 0.07), transparent 58%),
|
|
+ linear-gradient(180deg, color-mix(in srgb, var(--ui-bg-app) 92%, white), var(--ui-bg-main));
|
|
+ background-attachment: fixed;
|
|
}
|
|
diff --git a/tests/components/unified-shell.test.tsx b/tests/components/unified-shell.test.tsx
|
|
index d2661a6..0c2d538 100644
|
|
--- a/tests/components/unified-shell.test.tsx
|
|
+++ b/tests/components/unified-shell.test.tsx
|
|
@@ -40,6 +40,12 @@ test('UnifiedShell - imports AssignmentPanel', async () => {
|
|
assert.ok(fileContent.includes('AssignmentPanel'), 'Should import AssignmentPanel');
|
|
});
|
|
|
|
+test('UnifiedShell - checks bd health and renders setup warning', async () => {
|
|
+ const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/unified-shell.tsx'), 'utf-8');
|
|
+ assert.ok(fileContent.includes('useBdHealth'), 'Should use bd health hook');
|
|
+ assert.ok(fileContent.includes('BD setup issue:'), 'Should show bd setup warning text');
|
|
+});
|
|
+
|
|
// Test that AssignmentPanel is rendered conditionally based on view and assignMode
|
|
test('UnifiedShell - renders AssignmentPanel conditionally', async () => {
|
|
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/unified-shell.tsx'), 'utf-8');
|
|
diff --git a/tests/lib/bd-path.test.ts b/tests/lib/bd-path.test.ts
|
|
deleted file mode 100644
|
|
index bc7d5ed..0000000
|
|
--- a/tests/lib/bd-path.test.ts
|
|
+++ /dev/null
|
|
@@ -1,43 +0,0 @@
|
|
-import test from 'node:test';
|
|
-import assert from 'node:assert/strict';
|
|
-import fs from 'node:fs/promises';
|
|
-import os from 'node:os';
|
|
-import path from 'node:path';
|
|
-
|
|
-import { BdExecutableNotFoundError, resolveBdExecutable } from '../../src/lib/bd-path';
|
|
-
|
|
-test('resolveBdExecutable prefers explicit configured path when provided', async () => {
|
|
- const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-bd-path-'));
|
|
- const explicit = path.join(temp, 'tools', 'bd.exe');
|
|
- await fs.mkdir(path.dirname(explicit), { recursive: true });
|
|
- await fs.writeFile(explicit, '');
|
|
-
|
|
- const resolved = await resolveBdExecutable({ explicitPath: explicit, env: { Path: '', NODE_ENV: 'test' } });
|
|
-
|
|
- assert.equal(resolved.executable, explicit);
|
|
- assert.equal(resolved.source, 'config');
|
|
-});
|
|
-
|
|
-test('resolveBdExecutable finds bd.exe on PATH when explicit path is not set', async () => {
|
|
- const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-bd-path-env-'));
|
|
- const candidate = path.join(temp, 'bd.exe');
|
|
- await fs.writeFile(candidate, '');
|
|
-
|
|
- const resolved = await resolveBdExecutable({ env: { Path: temp, NODE_ENV: 'test' } });
|
|
-
|
|
- assert.equal(resolved.executable, candidate);
|
|
- assert.equal(resolved.source, 'path');
|
|
-});
|
|
-
|
|
-test('resolveBdExecutable throws actionable setup guidance when executable is missing', async () => {
|
|
- await assert.rejects(
|
|
- () => resolveBdExecutable({ env: { Path: '', NODE_ENV: 'test' } }),
|
|
- (error: unknown) => {
|
|
- assert.equal(error instanceof BdExecutableNotFoundError, true);
|
|
- const message = String((error as Error).message).toLowerCase();
|
|
- assert.equal(message.includes('npm install -g @beads/bd'), true);
|
|
- assert.equal(message.includes('bd.exe'), true);
|
|
- return true;
|
|
- },
|
|
- );
|
|
-});
|
|
diff --git a/tests/lib/bridge.test.ts b/tests/lib/bridge.test.ts
|
|
index d4a2259..91077c7 100644
|
|
--- a/tests/lib/bridge.test.ts
|
|
+++ b/tests/lib/bridge.test.ts
|
|
@@ -2,6 +2,7 @@ import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { runBdCommand } from '../../src/lib/bridge';
|
|
+import { normalizeProjectRootForRuntime } from '../../src/lib/project-root';
|
|
|
|
test('runBdCommand returns structured success payload from exec output', async () => {
|
|
const result = await runBdCommand(
|
|
@@ -12,12 +13,11 @@ test('runBdCommand returns structured success payload from exec output', async (
|
|
explicitBdPath: 'C:/tools/bd.exe',
|
|
},
|
|
{
|
|
- resolveBdExecutable: async () => ({ executable: 'C:/tools/bd.exe', source: 'config' }),
|
|
exec: async (command: string, options: any) => {
|
|
- assert.ok(command.includes('bd'));
|
|
+ assert.ok(command.startsWith('bd '));
|
|
assert.ok(command.includes('list'));
|
|
assert.ok(command.includes('--json'));
|
|
- assert.equal(options.cwd, 'C:/repo/project');
|
|
+ assert.equal(options.cwd, normalizeProjectRootForRuntime('C:/repo/project'));
|
|
return { stdout: '[{"id":"bb-1"}]\r\n', stderr: '' };
|
|
},
|
|
},
|
|
@@ -32,7 +32,6 @@ test('runBdCommand classifies missing executable as not_found', async () => {
|
|
const result = await runBdCommand(
|
|
{ projectRoot: 'C:/repo/project', args: ['list'] },
|
|
{
|
|
- resolveBdExecutable: async () => ({ executable: 'C:/tools/bd.exe', source: 'config' }),
|
|
exec: async () => {
|
|
const error = new Error('spawn ENOENT') as NodeJS.ErrnoException;
|
|
error.code = 'ENOENT';
|
|
@@ -49,7 +48,6 @@ test('runBdCommand classifies timeout failures', async () => {
|
|
const result = await runBdCommand(
|
|
{ projectRoot: 'C:/repo/project', args: ['list'], timeoutMs: 5 },
|
|
{
|
|
- resolveBdExecutable: async () => ({ executable: 'C:/tools/bd.exe', source: 'config' }),
|
|
exec: async () => {
|
|
const error = new Error('timed out') as NodeJS.ErrnoException & { killed?: boolean; signal?: string };
|
|
error.code = 'ETIMEDOUT';
|
|
@@ -68,7 +66,6 @@ test('runBdCommand classifies non-zero bad-argument exits', async () => {
|
|
const result = await runBdCommand(
|
|
{ projectRoot: 'C:/repo/project', args: ['update', '--bad-flag'] },
|
|
{
|
|
- resolveBdExecutable: async () => ({ executable: 'C:/tools/bd.exe', source: 'config' }),
|
|
exec: async () => {
|
|
const error = new Error('exit code 1') as NodeJS.ErrnoException & {
|
|
stdout?: string;
|
|
@@ -85,3 +82,26 @@ test('runBdCommand classifies non-zero bad-argument exits', async () => {
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.classification, 'bad_args');
|
|
});
|
|
+
|
|
+test('runBdCommand treats shell "not recognized" stderr as not_found', async () => {
|
|
+ const result = await runBdCommand(
|
|
+ { projectRoot: 'C:/repo/project', args: ['list'] },
|
|
+ {
|
|
+ exec: async () => {
|
|
+ const error = new Error('exit code 1') as NodeJS.ErrnoException & {
|
|
+ stdout?: string;
|
|
+ stderr?: string;
|
|
+ exitCode?: number;
|
|
+ };
|
|
+ error.code = 'BD_EXIT';
|
|
+ error.stderr = `'bd' is not recognized as an internal or external command`;
|
|
+ error.exitCode = 1;
|
|
+ throw error;
|
|
+ },
|
|
+ },
|
|
+ );
|
|
+
|
|
+ assert.equal(result.success, false);
|
|
+ assert.equal(result.classification, 'not_found');
|
|
+ assert.equal(result.error?.includes('bd command not found in PATH'), true);
|
|
+});
|
|
diff --git a/tests/lib/mutations.test.ts b/tests/lib/mutations.test.ts
|
|
index dbf0aed..06bd1b8 100644
|
|
--- a/tests/lib/mutations.test.ts
|
|
+++ b/tests/lib/mutations.test.ts
|
|
@@ -63,7 +63,7 @@ test('executeMutation surfaces bridge failures in normalized response', async ()
|
|
return {
|
|
success: false,
|
|
classification: 'non_zero_exit',
|
|
- command: 'bd.exe',
|
|
+ command: 'bd',
|
|
args,
|
|
cwd: root,
|
|
stdout: '',
|
|
@@ -93,7 +93,7 @@ test('executeMutation returns successful normalized response', async () => {
|
|
return {
|
|
success: true,
|
|
classification: null,
|
|
- command: 'bd.exe',
|
|
+ command: 'bd',
|
|
args,
|
|
cwd: root,
|
|
stdout: '{"id":"bb-123"}',
|
|
@@ -109,3 +109,62 @@ test('executeMutation returns successful normalized response', async () => {
|
|
assert.equal(result.operation, 'update');
|
|
assert.equal(result.command.success, true);
|
|
});
|
|
+
|
|
+test('executeMutation includes --actor when provided in payload', async () => {
|
|
+ const payload = validateMutationPayload('comment', {
|
|
+ projectRoot: root,
|
|
+ id: 'bb-123',
|
|
+ text: 'Operator note',
|
|
+ actor: 'zenchant',
|
|
+ });
|
|
+
|
|
+ const result = await executeMutation('comment', payload, {
|
|
+ runBdCommand: async ({ args }) => {
|
|
+ assert.deepEqual(args, ['--actor', 'zenchant', 'comments', 'add', 'bb-123', 'Operator note', '--json']);
|
|
+ return {
|
|
+ success: true,
|
|
+ classification: null,
|
|
+ command: 'bd',
|
|
+ args,
|
|
+ cwd: root,
|
|
+ stdout: '{"ok":true}',
|
|
+ stderr: '',
|
|
+ code: 0,
|
|
+ durationMs: 2,
|
|
+ error: null,
|
|
+ };
|
|
+ },
|
|
+ });
|
|
+
|
|
+ assert.equal(result.ok, true);
|
|
+});
|
|
+
|
|
+test('executeMutation ignores bdPath and uses default runner contract', async () => {
|
|
+ const payload = validateMutationPayload('update', {
|
|
+ projectRoot: root,
|
|
+ id: 'bb-123',
|
|
+ status: 'in_progress',
|
|
+ bdPath: 'C:/Tools/beads/bd.exe',
|
|
+ });
|
|
+
|
|
+ const result = await executeMutation('update', payload, {
|
|
+ runBdCommand: async (options) => {
|
|
+ assert.equal(options.explicitBdPath, undefined);
|
|
+ assert.deepEqual(options.args, ['update', 'bb-123', '-s', 'in_progress', '--json']);
|
|
+ return {
|
|
+ success: true,
|
|
+ classification: null,
|
|
+ command: 'bd',
|
|
+ args: options.args,
|
|
+ cwd: root,
|
|
+ stdout: '{"ok":true}',
|
|
+ stderr: '',
|
|
+ code: 0,
|
|
+ durationMs: 2,
|
|
+ error: null,
|
|
+ };
|
|
+ },
|
|
+ });
|
|
+
|
|
+ assert.equal(result.ok, true);
|
|
+});
|
|
diff --git a/tsconfig.json b/tsconfig.json
|
|
index e0d07bd..23ebab9 100644
|
|
--- a/tsconfig.json
|
|
+++ b/tsconfig.json
|
|
@@ -20,5 +20,5 @@
|
|
}
|
|
},
|
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
- "exclude": ["node_modules"]
|
|
+ "exclude": ["node_modules", "reference/**"]
|
|
}
|