Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/memory/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ Channel DB modules: `db/slack_channels.py` (workspace connections, channel-agent

**Agent Detail Overview tab (#1107):** `components/OverviewPanel.vue` is the default landing tab — owns "trend over the last few days" while the persistent `AgentHeader` owns "now + cost" (no duplicate live gauges). Sections: About lead, needs-attention count + Operations link (hidden at zero), trend charts, health panel (uptime/latency clamped ≤7d by `agent_health_checks` retention), recent-activity drill-in, footprint chips. Charts: `StackedBarChart.vue` (CSS/flexbox) for executions-by-type; `TrendLineChart.vue` (uPlot) for line series. `InfoPanel.vue` leads with About + "What You Can Ask", `template.yaml` metadata behind a `<details>`.

**Dashboard Grid view (trinity-enterprise#47):** third dashboard mode (Grid / Graph / Timeline; Timeline stays default, choice in `localStorage['trinity-dashboard-view']`). Magnetic tile canvas: `components/FleetGrid.vue` (pan/zoom viewport + drag/swap/tidy/keyboard on an unbounded lattice — no Vue Flow) renders `components/AgentTile.vue` five-zone tiles (composing AgentAvatar/RuntimeBadge/Running+Autonomy toggles); `stores/fleetGrid.js` owns the per-user self-healing layout (localStorage v1) and lazy per-tile analytics hydration (viewport-gated, concurrency-capped, stale-while-revalidate over the `executions` store `${name}:${window}` cache); lattice math in `utils/gridLayout.js`. Chip data from batch endpoints (sync-health #389, operator-queue pending) on a visibility-aware poll active only while mounted. `network.js` additions: 3-state `viewMode`, `circuitBreakers` map, WS-driven `workingState`. **No new backend endpoints.** See [dashboard-grid-view.md](feature-flows/dashboard-grid-view.md).

**Collaboration Dashboard** (`views/AgentCollaboration.vue`, `components/AgentNode.vue`, `stores/collaborations.js`): Vue Flow node graph of agent-to-agent communication — draggable status-colored nodes, edges animated 3s on collaboration, real-time activity feed, replay with time-range filtering, localStorage node positions. Detection: the backend chat endpoint accepts `X-Source-Agent` and broadcasts `agent_collaboration` WS events; `activity_service` broadcasts `agent_activity` (`activity_type`: chat_start/chat_end/tool_call/schedule_start/schedule_end/agent_collaboration; `activity_state`: started/completed/failed/cancelled — a user-cancelled terminal is recorded as `cancelled`, distinct from `failed`, #1332).

### MCP Server (`src/mcp-server/`)
Expand Down
2 changes: 2 additions & 0 deletions docs/memory/feature-flows.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
|------|-----|--------|------|
| 2026-07-04 | #903 | fix(slack): thread-scoped session + per-speaker attribution + sender-filtered memory | [slack-channel-routing.md](feature-flows/slack-channel-routing.md) |
| 2026-07-04 | #1445 | fix(webhooks): gate schedule/webhook **creation** on a live owning agent (`is_agent_live` → 404; access-check-first so non-owners see a uniform 403) so a webhook token always resolves to a schedule of a live agent — closes the orphan-schedule class that made valid tokens 404 after #1423's soft-delete-aware token-lookup INNER JOIN | [webhook-triggers.md](feature-flows/webhook-triggers.md) |
| 2026-07-06 | trinity-enterprise#47 | feat(ui): Dashboard **Grid view** — third mode (Grid/Graph/Timeline, not default): magnetic tile canvas on an unbounded pan/zoom lattice; five-zone 384×216 tiles (adaptive chips, Activity·14d + Context·7d charts, Run/Auto toggles); drag with socket preview + swap, tidy/reset, keyboard reorder; skeleton-first render, viewport-gated cached analytics hydration, visibility-aware batch polling. No new backend endpoints | [dashboard-grid-view.md](feature-flows/dashboard-grid-view.md) |
| 2026-07-04 | #1018 | fix(nevermined): settlement-ordering — honest `success_unsettled` on settle-fail (was lying `"success"`); `Idempotency-Key` on the paid boundary keyed on `(payment-signature ∥ message)` with in-flight-409 / settled-replay / unsettled-re-drive-converge; `/retry-settlement` stub → honest 501. Tier 2 durable retry split to a follow-up | [nevermined-payments.md](feature-flows/nevermined-payments.md), [effect-idempotency.md](feature-flows/effect-idempotency.md) |
| 2026-07-04 | #186 | fix(security): close user & agent enumeration oracles — uniform 404 across the agent-access dependency family + router sweep + Tier-4 GETs; identical email-request body/status/timing; MCP `checkAgentAccess` uniform reason + owner-username removal (pentest 3.3.3) | [email-authentication.md](feature-flows/email-authentication.md), [agent-permissions.md](feature-flows/agent-permissions.md) |
| 2026-07-04 | #1444 | fix(chat): fail-loud + owner-gated `/task` chat-session persistence (no silent swallow, IDOR fix, SUCCESS-guarded) + fast unit regression guard | [authenticated-chat-tab.md](feature-flows/authenticated-chat-tab.md) |
Expand Down Expand Up @@ -84,6 +85,7 @@
|------|----------|-------------|
| Agent Network (Dashboard) | [agent-network.md](feature-flows/agent-network.md) | Real-time visual graph at `/` |
| Dashboard Timeline View | [dashboard-timeline-view.md](feature-flows/dashboard-timeline-view.md) | Graph/Timeline mode toggle with execution boxes |
| Dashboard Grid View | [dashboard-grid-view.md](feature-flows/dashboard-grid-view.md) | Magnetic tile canvas — third dashboard mode (trinity-enterprise#47) |
| Replay Timeline | [replay-timeline.md](feature-flows/replay-timeline.md) | Waterfall-style timeline visualization |
| Activity Stream | [activity-stream.md](feature-flows/activity-stream.md) | Centralized persistent activity tracking |
| Activity Monitoring | [activity-monitoring.md](feature-flows/activity-monitoring.md) | Real-time tool execution tracking |
Expand Down
129 changes: 129 additions & 0 deletions docs/memory/feature-flows/dashboard-grid-view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Feature Flow: Dashboard Grid View (magnetic tile canvas)

> **Last Updated**: 2026-07-06 (initial implementation)
> **Status**: Implemented — third dashboard mode, not default
> **Issue**: trinity-enterprise#47 (design of record embedded in the issue)
> **Requirements**: `docs/memory/requirements/core-agent.md` §9.8

## Overview

The Dashboard's third view mode alongside **Graph** (Vue Flow topology) and
**Timeline** (waterfall activity). Grid is a **magnetic tile canvas**: richer
384×216 landscape agent tiles that snap to a sparse, **unbounded** integer
lattice (negative coordinates included) the operator arranges freely —
islands, gaps, parked loners — with iPhone-style drag and live snap preview,
on the same pan/zoom dotted-canvas language as the graph view.

- Mode toggle: `Grid / Graph / Timeline` in the Dashboard header; selection
persists to `localStorage['trinity-dashboard-view']`. **Timeline stays the
default** for users with no saved preference.
- **No Vue Flow dependency** in this mode, and **no new backend endpoints**.

## Components & Data Flow

```
views/Dashboard.vue mode toggle, grid pane (v-if), Tidy up / Reset pills,
│ "N working now" header stat, empty/error/skeleton states
├─ components/FleetGrid.vue pan/zoom viewport, lattice, drag physics, sockets,
│ │ cell shading, keyboard reorder, zoom controls + legend,
│ │ viewport culling, shared 1s tick for tile timers
│ └─ components/AgentTile.vue five-zone tile (see below); composes
│ AgentAvatar / RuntimeBadge / RunningStateToggle / AutonomyToggle
├─ stores/fleetGrid.js per-user layout (localStorage v1, self-healing),
│ lazy analytics hydration queue (concurrency 4,
│ stale-while-revalidate over executions-store cache),
│ batch chip data (sync-health + operator-queue pending)
│ on a 60s visibility-aware poll, active only while mounted
├─ stores/network.js agents list, contextStats / executionStats / slotStats
│ (15s shared poll), NEW: viewMode ('grid'|'graph'|'timeline'),
│ circuitBreakers map, WS-driven workingState map
├─ stores/executions.js fetchAgentAnalytics(name, '14d') — existing
│ `${name}:${window}` cache (#1107)
└─ utils/gridLayout.js pure lattice math: cell geometry, spiral
nearest-free-cell, normalize (self-healing), tidy, bbox
```

### Data sources (all existing)

| Tile element | Source |
|---|---|
| Identity, status, runtime, repo, autonomy | `GET /api/agents` (network store) |
| Activity·14d chart + Context·7d chart | `GET /api/agents/{name}/analytics?window=14d` (#1107) — one fetch feeds both; last 7 timeline entries drive the context line |
| Live context %, Active/Idle/Offline | fleet `GET /api/agents/context-stats` (15s poll) |
| Success meter, tasks/cost/last-run, schedules chip | fleet `GET /api/agents/execution-stats` (15s poll) |
| ⚡ circuit open chip | `GET /api/agents/slots` → `circuit_breakers` map (#526) |
| ⟳ sync failing / git ✓ chips | `GET /api/agents/sync-health` (#389, batch) |
| ⚠ needs response / approval pending chip | `GET /api/operator-queue?status=pending` (batch, grouped per agent) |
| ▶ working + elapsed timer | WS `agent_activity` events → `workingState` map, reconciled by the context-stats poll; fallback `activityState === 'active'` |

### Trigger-bucket collapse (tile scale)

The backend's #1107 buckets collapse to three groups: **Scheduled** ←
Scheduled · **Manual·MCP** ← Chat/Tasks, MCP, Loops, Agent-to-agent, Other ·
**External** ← Channels, Public, Voice. A board-level legend (bottom-right)
explains the colors once — never repeated per tile.

## Layout model

- Layout = per-user map `agent → {c, r}`; localStorage key
`trinity-grid-layout-v1`; server-side per-user storage is a follow-up.
- **Self-healing** (`normalizeLayout`): new agents take the first free cell
near the origin (spiral search); deleted agents leave their gap; an invalid
or colliding saved position resolves to the nearest free cell.
- **Filters never destroy layout**: persisting merges the active layout over
the full saved map, so agents hidden by an owner/tag filter keep their
saved cells (filtering is indistinguishable from deletion client-side, so
absence is never treated as deletion).
- **Tidy up** compacts row-by-row (3 columns) preserving reading order,
anchored at the layout's own top-left, clamped to the coordinate bound.
**Reset** restores the deterministic default (system agent first,
reading-order grid) and drops stale saved entries.

## Interaction constants (design of record)

Cell 384×216, gaps 34/18 (column gap exceeds the 26px avatar overhang);
zoom 0.25–1.6× around the cursor; drag follows 1:1 (screen deltas ÷ zoom)
with ±3° velocity tilt; displace/reflow 280ms `cubic-bezier(.3,.7,.25,1)`;
overshoot snap 420ms `cubic-bezier(.22,1.35,.32,1)`; lock-ring pulse 450ms.
Drop on an occupied cell **swaps** (live preview while hovering; a swap never
disturbs a third tile). Keyboard: focused tile moves with arrow keys (through
a neighbor = swap). `prefers-reduced-motion` disables tilt/spring/pulse —
drops become instant placement. Multi-touch is discriminated by pointer id
(one drag at a time; a second touch cannot start a pan mid-drag).

## Performance contract (#47 acceptance criteria)

1. **Non-blocking first paint** — tiles render immediately from the agents
list the Dashboard already holds; per-section skeletons while analytics
stream in. Nothing awaits the full set.
2. **Lazy, capped hydration** — a tile asks the fleetGrid store to hydrate
only when near the viewport (culled tiles render a light placeholder and
fetch nothing); fetches run through a 4-slot queue into the executions
store's `(agent, window)` cache; stale entries (>5 min) serve instantly
and refresh in the background.
3. **Batch endpoints over per-agent loops** for chip data; the 60s poll is
visibility-aware (skips when `document.hidden`) and tears down when the
Grid unmounts (mode switch is `v-if`).
4. **A slow or failed per-agent fetch degrades that one tile only.**

## Failure modes & edge cases

- Corrupt/unavailable localStorage → default layout, session-local.
- Agent deleted/renamed mid-session → self-healing pass; a mid-drag removal
cancels the drag cleanly.
- Stopped agent → Offline state, context chart flattens to a dash.
- Analytics fetch error with no cache → charts degrade to flat/na quietly.
- WS `workingState` entries leaked by missed end events are reconciled by
the 15s context-stats poll (entries younger than one poll period are
spared to avoid a stale response evicting a fresh start).

## Testing

`src/frontend/e2e/dashboard-grid-view.spec.js` — mode toggle + tile render
(@smoke), mode persistence across reload, drag-to-cell with socket preview +
layout persistence, tidy/reset, and Graph/Timeline coexistence (@smoke).

## Out of scope (tracked follow-ups)

Fleet KPI strip; "Needs your attention" + live-activity right rail;
server-side per-user layout storage.
8 changes: 8 additions & 0 deletions docs/memory/requirements/core-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,14 @@
- **Status**: ❌ Removed (2025-12-23)
- **Reason**: Individual agent planning deferred to orchestrator-level. Claude Code handles task management internally.

### 9.8 Dashboard Grid View (trinity-enterprise#47)
- **Status**: ✅ Implemented (2026-07-06)
- **Description**: Third dashboard mode (Grid / Graph / Timeline) — a magnetic tile canvas: rich 384×216 landscape agent tiles snapping to a sparse, unbounded lattice the operator arranges freely, on the same pan/zoom dotted-canvas language as the graph view. Not the default (Timeline remains default for new users); selection persists to localStorage.
- **Key Features**: iPhone-style drag with live socket preview + swap-with-preview; Tidy up / Reset; keyboard arrow reorder; per-user layout (`agent → {col,row}`, localStorage v1, self-healing); five-zone tile (identity with half-out avatar, adaptive chip strip with live working timer, Activity·14d stacked-by-trigger + Context·7d trend charts, success micro-meter + stats, Run/Auto toggles); system agent keeps its purple treatment; `prefers-reduced-motion` honored.
- **Performance (first-class)**: skeleton-first render from `/api/agents`; per-tile analytics hydrate lazily (viewport-gated, concurrency-capped) into the existing `(agent, window)` cache with stale-while-revalidate; batch endpoints for chip data (sync-health, operator-queue) on a visibility-aware poll that tears down when the mode is inactive; viewport culling for 50+ fleets. **No new backend endpoints** — reads `/api/agents/{name}/analytics` (#1107), fleet context/execution/slot stats, `/api/agents/sync-health` (#389), operator-queue pending.
- **Out of scope (follow-ups)**: fleet KPI strip; "Needs your attention" + live-activity right rail.
- **Flow**: `docs/memory/feature-flows/dashboard-grid-view.md`

---

---
Expand Down
124 changes: 124 additions & 0 deletions src/frontend/e2e/dashboard-grid-view.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { test, expect } from '@playwright/test'

/**
* Dashboard Grid view e2e (trinity-enterprise#47).
*
* The Grid is a third dashboard mode alongside Graph and Timeline: a
* magnetic tile canvas on an unbounded pan/zoom lattice. These specs cover
* the mode toggle + persistence, tile rendering, drag-to-cell with swap,
* tidy/reset, and that the existing Graph/Timeline modes are untouched.
*
* Runs against a live stack — every install has at least `trinity-system`,
* so no fixtures are needed. Layout state is reset per test via
* localStorage.
*/

const LAYOUT_KEY = 'trinity-grid-layout-v1'
const MODE_KEY = 'trinity-dashboard-view'

async function gotoGrid(page) {
await page.goto('/')
await page.getByRole('button', { name: 'grid', exact: true }).click()
await expect(page.locator('.fleet-canvas')).toBeVisible()
}

test.describe('dashboard grid view (trinity-enterprise#47)', () => {
test.beforeEach(async ({ page }) => {
// Fresh layout + default mode for deterministic assertions.
await page.addInitScript(
([layoutKey]) => {
localStorage.removeItem(layoutKey)
},
[LAYOUT_KEY]
)
})

test('@smoke mode toggle shows Grid and renders agent tiles', async ({ page }) => {
await gotoGrid(page)

// At least the system agent tile renders, with tile chrome + zones.
const sysTile = page.locator('.gv-tile[data-agent="trinity-system"]')
await expect(sysTile).toBeVisible({ timeout: 15000 })
await expect(sysTile).toContainText('SYSTEM')
await expect(sysTile).toContainText('Activity · 14d')
await expect(sysTile).toContainText('Context · 7d')
await expect(sysTile).toContainText('System Dashboard')

// Grid-mode-only controls + board furniture.
await expect(page.getByRole('button', { name: 'Tidy up' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Reset', exact: true })).toBeVisible()
await expect(page.locator('.gv-legend')).toContainText('Scheduled')
await expect(page.locator('.gv-zoomlvl')).toContainText('%')
})

test('mode choice persists across reload', async ({ page }) => {
await gotoGrid(page)
expect(await page.evaluate((k) => localStorage.getItem(k), MODE_KEY)).toBe('grid')

await page.reload()
// Lands straight back in grid mode without touching the toggle.
await expect(page.locator('.fleet-canvas')).toBeVisible({ timeout: 15000 })
})

test('drag moves a tile to a free cell and persists the layout', async ({ page }) => {
await gotoGrid(page)
const tile = page.locator('.gv-tile[data-agent="trinity-system"]')
await expect(tile).toBeVisible({ timeout: 15000 })

const before = await page.evaluate((k) => localStorage.getItem(k), LAYOUT_KEY)

// Drag by two cell-heights straight down (mouse-level, exercises the
// pointer capture + socket path).
const box = await tile.boundingBox()
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2)
await page.mouse.down()
for (let i = 1; i <= 10; i++) {
await page.mouse.move(
box.x + box.width / 2,
box.y + box.height / 2 + (i / 10) * box.height * 2.2,
{ steps: 2 }
)
}
// Socket preview is visible mid-drag.
await expect(page.locator('.gv-socket.show')).toBeVisible()
await page.mouse.up()

await expect
.poll(async () => page.evaluate((k) => localStorage.getItem(k), LAYOUT_KEY))
.not.toBe(before)
})

test('tidy up compacts and reset restores the default layout', async ({ page }) => {
await gotoGrid(page)
await expect(page.locator('.gv-tile').first()).toBeVisible({ timeout: 15000 })

await page.getByRole('button', { name: 'Tidy up' }).click()
const tidied = await page.evaluate((k) => localStorage.getItem(k), LAYOUT_KEY)
expect(tidied).toBeTruthy()

await page.getByRole('button', { name: 'Reset', exact: true }).click()
const reset = await page.evaluate((k) => localStorage.getItem(k), LAYOUT_KEY)
expect(reset).toBeTruthy()
// Reset yields the deterministic default: every position is a small
// non-negative reading-order cell.
const positions = Object.values(JSON.parse(reset))
for (const p of positions) {
expect(p.c).toBeGreaterThanOrEqual(0)
expect(p.r).toBeGreaterThanOrEqual(0)
}
})

test('@smoke graph and timeline modes still work alongside grid', async ({ page }) => {
await gotoGrid(page)

// Graph: Vue Flow mounts, grid tears down.
await page.getByRole('button', { name: 'graph', exact: true }).click()
await expect(page.locator('.vue-flow')).toBeVisible({ timeout: 15000 })
await expect(page.locator('.fleet-canvas')).toHaveCount(0)

// Timeline: replay timeline mounts, graph tears down.
await page.getByRole('button', { name: 'timeline', exact: true }).click()
await expect(page.locator('.vue-flow')).toHaveCount(0)
await expect(page.locator('.fleet-canvas')).toHaveCount(0)
})
})
Loading
Loading