mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Merge branch 'slave' of https://github.com/rennf93/roboco into security/api-8000-loopback
This commit is contained in:
@@ -8646,11 +8646,12 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| `panel/src/app/(dashboard)/tasks/page.tsx` + `tasks/[taskId]/page.tsx` | Task list + task detail (tabbed) |
|
| `panel/src/app/(dashboard)/tasks/page.tsx` + `tasks/[taskId]/page.tsx` | Task list + task detail (tabbed) |
|
||||||
| `panel/src/app/(dashboard)/kanban/page.tsx` | Operator kanban (dev/qa/pm/pr-review views) |
|
| `panel/src/app/(dashboard)/kanban/page.tsx` | Operator kanban (dev/qa/pm/pr-review views) |
|
||||||
| `panel/src/app/(dashboard)/prompter/page.tsx` | Intake chat (single + MegaTask batch scope) |
|
| `panel/src/app/(dashboard)/prompter/page.tsx` | Intake chat (single + MegaTask batch scope) |
|
||||||
| `panel/src/app/(dashboard)/agents/page.tsx` | Agents hub: Fleet (roster/spawn/status, default) + Conversations tabs via `?tab=fleet\|conversations` — `AgentsFleetView` (fleet roster/spawn/activity) and `A2AView` (org-wide switchboard/list + transcript + CEO reply composer + "New DM" dialog, live via `/ws/system` `a2a.message` frames — a pure lift of the old standalone `/a2a` page body) |
|
| `panel/src/app/(dashboard)/agents/page.tsx` | Agents hub: Fleet (roster/spawn/status, default) + Conversations + Journals tabs via `?tab=fleet\|conversations\|journals` — `AgentsFleetView` (fleet roster/spawn/activity), `A2AView` (org-wide switchboard/list + transcript + CEO reply composer + "New DM" dialog, live via `/ws/system` `a2a.message` frames — a pure lift of the old standalone `/a2a` page body), and `JournalsView` (per-agent reflection log — a pure lift of the old standalone `/journals` page body, wave 11) |
|
||||||
| `panel/src/app/(dashboard)/a2a/page.tsx` | Redirect shim → `/agents?tab=conversations` (A2A folded into the Agents hub's Conversations tab, wave 9) — kept so old bookmarks/links still resolve |
|
| `panel/src/app/(dashboard)/a2a/page.tsx` | Redirect shim → `/agents?tab=conversations` (A2A folded into the Agents hub's Conversations tab, wave 9) — kept so old bookmarks/links still resolve |
|
||||||
|
| `panel/src/app/(dashboard)/journals/page.tsx` | Redirect shim → `/agents?tab=journals` (Journals folded into the Agents hub's Journals tab, wave 11) — kept so old bookmarks/links still resolve; `journals/[entryId]/page.tsx` (entry detail) is unchanged at its own route, only its back-links now point at `/agents?tab=journals` |
|
||||||
| `panel/src/app/(dashboard)/settings/page.tsx` + `settings/ai-providers/page.tsx` | Settings: feature flags, AI routing, transcript retention, self-hosted |
|
| `panel/src/app/(dashboard)/settings/page.tsx` + `settings/ai-providers/page.tsx` | Settings: feature flags, AI routing, transcript retention, self-hosted |
|
||||||
| `panel/src/app/(dashboard)/workstation/page.tsx` | Workstation: Products + Projects merged as URL-param tabs (`?tab=products\|projects`, Products first); `products/page.tsx` and `projects/page.tsx` are now server-component redirects to it |
|
| `panel/src/app/(dashboard)/workstation/page.tsx` | Workstation: Products + Projects merged as URL-param tabs (`?tab=products\|projects`, Products first); `products/page.tsx` and `projects/page.tsx` are now server-component redirects to it |
|
||||||
| `panel/src/app/(dashboard)/{business,journals,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx` | Per-domain pages |
|
| `panel/src/app/(dashboard)/{business,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx` | Per-domain pages |
|
||||||
| `panel/src/app/(auth)/login/page.tsx` | Cloud-auth login form (email/password → `useLogin` → `/auth/login`); only reachable/relevant once `proxy.ts` starts gating the `(dashboard)` group |
|
| `panel/src/app/(auth)/login/page.tsx` | Cloud-auth login form (email/password → `useLogin` → `/auth/login`); only reachable/relevant once `proxy.ts` starts gating the `(dashboard)` group |
|
||||||
| `panel/src/proxy.ts` | Next 16's rename of `middleware.ts`: probes `/auth/status` (docker-internal orchestrator URL, fails open to "off" on any error/timeout) and redirects to `/login` when cloud auth is on and no session cookie is present |
|
| `panel/src/proxy.ts` | Next 16's rename of `middleware.ts`: probes `/auth/status` (docker-internal orchestrator URL, fails open to "off" on any error/timeout) and redirects to `/login` when cloud auth is on and no session cookie is present |
|
||||||
| `panel/src/components/dashboard/` | Overview cards: command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions-card, quick-actions-registry, recent-activity, `x-post-queue.tsx`, `roadmap-review-queue.tsx` |
|
| `panel/src/components/dashboard/` | Overview cards: command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions-card, quick-actions-registry, recent-activity, `x-post-queue.tsx`, `roadmap-review-queue.tsx` |
|
||||||
@@ -8661,7 +8662,7 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| `panel/src/components/tasks/` + `tasks/task-detail/` | task-table, create/edit-task-dialog, task-filters, acceptance-criteria-editor, dependency-selector, task-detail tabs (overview/plan/progress/commits/sessions/notes/dependencies/**findings**) |
|
| `panel/src/components/tasks/` + `tasks/task-detail/` | task-table, create/edit-task-dialog, task-filters, acceptance-criteria-editor, dependency-selector, task-detail tabs (overview/plan/progress/commits/sessions/notes/dependencies/**findings**) |
|
||||||
| `panel/src/components/settings/` | feature-flags-card, ai-routing-card, transcript-retention-card, self-hosted-section, `x-credentials-card.tsx` (write-only OAuth 1.0a secrets, mounted in `settings/page.tsx`) |
|
| `panel/src/components/settings/` | feature-flags-card, ai-routing-card, transcript-retention-card, self-hosted-section, `x-credentials-card.tsx` (write-only OAuth 1.0a secrets, mounted in `settings/page.tsx`) |
|
||||||
| `panel/src/components/conventions/conventions-tab.tsx` | Per-project architecture map + health (in edit-project dialog) |
|
| `panel/src/components/conventions/conventions-tab.tsx` | Per-project architecture map + health (in edit-project dialog) |
|
||||||
| `panel/src/components/projects/`, `products/`, `agents/`, `business/`, `auditor/`, `knowledge-base/`, `git/`, `journals/`, `work-sessions/`, `notifications/`, `rate-limit/`, `layout/`, `ui/` | Per-domain component groups (`projects/` and `products/` each export a `*-view.tsx` consumed by `workstation/page.tsx`, plus a `*-card-grid.tsx` reusing the sibling table's exported badge renderers; `agents/` similarly exports `agents-fleet-view.tsx` (`AgentsFleetView`) consumed by `agents/page.tsx`'s Fleet tab, plus `agent-card.tsx`'s DM quick-action button); `ui/` = Radix-based primitives (dialog, table, tabs, select, switch, required-notes-dialog, sonner toaster, markdown) |
|
| `panel/src/components/projects/`, `products/`, `agents/`, `business/`, `auditor/`, `knowledge-base/`, `git/`, `journals/`, `work-sessions/`, `notifications/`, `rate-limit/`, `layout/`, `ui/` | Per-domain component groups (`projects/` and `products/` each export a `*-view.tsx` consumed by `workstation/page.tsx`, plus a `*-card-grid.tsx` reusing the sibling table's exported badge renderers; `agents/` similarly exports `agents-fleet-view.tsx` (`AgentsFleetView`) consumed by `agents/page.tsx`'s Fleet tab, plus `agent-card.tsx`'s DM quick-action button; `journals/` similarly exports `journals-view.tsx` (`JournalsView`) consumed by `agents/page.tsx`'s Journals tab); `ui/` = Radix-based primitives (dialog, table, tabs, select, switch, required-notes-dialog, sonner toaster, markdown) |
|
||||||
| `panel/src/hooks/use-websocket.ts` | Shared `useWebSocket<T>(path, handlers?, isSystem?)` hook (auto-reconnect, heartbeat) |
|
| `panel/src/hooks/use-websocket.ts` | Shared `useWebSocket<T>(path, handlers?, isSystem?)` hook (auto-reconnect, heartbeat) |
|
||||||
| `panel/src/hooks/use-{tasks,agents,projects,products,usage,prompter,secretary,dashboard,git,journals,notifications,knowledge-base,observability,work-sessions,providers,rate-limit-{sync,websocket}}.ts` | TanStack Query + zustand data hooks |
|
| `panel/src/hooks/use-{tasks,agents,projects,products,usage,prompter,secretary,dashboard,git,journals,notifications,knowledge-base,observability,work-sessions,providers,rate-limit-{sync,websocket}}.ts` | TanStack Query + zustand data hooks |
|
||||||
| `panel/src/hooks/use-a2a-live.ts` | `useA2AConversations` / `useA2AAdminPairs` / `useA2AMessages` (TanStack Query over `a2aApi`) + `useReplyAsCeo` / `useCreateCeoConversation` / `useSendCeoMessage` mutations; `a2aLiveKeys` query-key namespace |
|
| `panel/src/hooks/use-a2a-live.ts` | `useA2AConversations` / `useA2AAdminPairs` / `useA2AMessages` (TanStack Query over `a2aApi`) + `useReplyAsCeo` / `useCreateCeoConversation` / `useSendCeoMessage` mutations; `a2aLiveKeys` query-key namespace |
|
||||||
@@ -8690,6 +8691,7 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| Intake / MegaTask | `app/(dashboard)/prompter/page.tsx` + `components/prompter/*` | Live SSE chat with spawned Claude/Grok intake agent; single-project, product, or multi-project (`project_ids`) MegaTask → `propose_batch` → `confirm-batch` |
|
| Intake / MegaTask | `app/(dashboard)/prompter/page.tsx` + `components/prompter/*` | Live SSE chat with spawned Claude/Grok intake agent; single-project, product, or multi-project (`project_ids`) MegaTask → `propose_batch` → `confirm-batch` |
|
||||||
| Agents — Fleet | `app/(dashboard)/agents/page.tsx` (`?tab=fleet`, default) + `components/agents/agents-fleet-view.tsx` + `components/agents/*` | Full agent roster grouped into Leadership/Backend/Frontend/UX-UI/Support grids: live state, spawn/stop controls, activity-stream link, token usage. Each card's DM button (hidden for `EXCLUDE_NON_DM_ROLES`) deep-links `/agents?tab=conversations&dm=<agent id>` into the Conversations tab |
|
| Agents — Fleet | `app/(dashboard)/agents/page.tsx` (`?tab=fleet`, default) + `components/agents/agents-fleet-view.tsx` + `components/agents/*` | Full agent roster grouped into Leadership/Backend/Frontend/UX-UI/Support grids: live state, spawn/stop controls, activity-stream link, token usage. Each card's DM button (hidden for `EXCLUDE_NON_DM_ROLES`) deep-links `/agents?tab=conversations&dm=<agent id>` into the Conversations tab |
|
||||||
| Agents — Conversations (switchboard + reply + New DM) | `app/(dashboard)/agents/page.tsx` (`?tab=conversations`) + `components/a2a/a2a-view.tsx` + `components/a2a/*` | CEO watches every agent-to-agent conversation live: default org-chart switchboard (pair cards grouped by cell/PM-chain/board, pulsing on fresh `a2a.message` frames) or the classic conversation list; drill-in shows the transcript + a reply composer that lets the CEO chime into a watched thread as itself (task-linked conversations only). "New DM" opens a fresh CEO-owned 1:1 with any DM-capable agent (no task link needed) — either from the dialog's own picker or preselected from a validated `?dm=` deep link off the Fleet tab; the recipient is woken via the a2a_request dispatch path if offline, and the CEO's own threads render with `A2ADirectComposer` instead of the reply composer. `/a2a` redirects here |
|
| Agents — Conversations (switchboard + reply + New DM) | `app/(dashboard)/agents/page.tsx` (`?tab=conversations`) + `components/a2a/a2a-view.tsx` + `components/a2a/*` | CEO watches every agent-to-agent conversation live: default org-chart switchboard (pair cards grouped by cell/PM-chain/board, pulsing on fresh `a2a.message` frames) or the classic conversation list; drill-in shows the transcript + a reply composer that lets the CEO chime into a watched thread as itself (task-linked conversations only). "New DM" opens a fresh CEO-owned 1:1 with any DM-capable agent (no task link needed) — either from the dialog's own picker or preselected from a validated `?dm=` deep link off the Fleet tab; the recipient is woken via the a2a_request dispatch path if offline, and the CEO's own threads render with `A2ADirectComposer` instead of the reply composer. `/a2a` redirects here |
|
||||||
|
| Agents — Journals | `app/(dashboard)/agents/page.tsx` (`?tab=journals`) + `components/journals/journals-view.tsx` + `components/journals/*` | Per-agent reflection log lifted into the Agents hub's third tab: agent picker (left, local search state seeded from the last saved query) + a type/task-filterable entry list (right, `JournalView`); each task-linked entry carries a Task badge + copy-UUID button. Entry detail still lives at `/journals/{entryId}`, its back-links now pointing at `/agents?tab=journals`. `/journals` redirects here |
|
||||||
| Workstation | `app/(dashboard)/workstation/page.tsx` + `components/{products,projects}/*-view.tsx` | Products + Projects as one sidebar entry, tab-switched via `?tab=`; each surface has a Cards\|Table view toggle (default Cards, persisted per-surface via `ui-store`'s `productsView`/`projectsView`) with client-side name/cell(-count) sorting in card view; Projects' q/cell/inactive filters are local `useState`, not URL params (scroll-bounce prevention) |
|
| Workstation | `app/(dashboard)/workstation/page.tsx` + `components/{products,projects}/*-view.tsx` | Products + Projects as one sidebar entry, tab-switched via `?tab=`; each surface has a Cards\|Table view toggle (default Cards, persisted per-surface via `ui-store`'s `productsView`/`projectsView`) with client-side name/cell(-count) sorting in card view; Projects' q/cell/inactive filters are local `useState`, not URL params (scroll-bounce prevention) |
|
||||||
| Project Settings / Conventions | `components/projects/edit-project-dialog.tsx` + `components/conventions/conventions-tab.tsx` | Per-project `.roboco/conventions.yml` map + health; Save / Restore via PR |
|
| Project Settings / Conventions | `components/projects/edit-project-dialog.tsx` + `components/conventions/conventions-tab.tsx` | Per-project `.roboco/conventions.yml` map + health; Save / Restore via PR |
|
||||||
| Usage Dashboard | `components/dashboard/usage-overview-panel.tsx` + `hooks/use-usage.ts` | Token/cost totals; live WS snapshot with HTTP-polling fallback |
|
| Usage Dashboard | `components/dashboard/usage-overview-panel.tsx` + `hooks/use-usage.ts` | Token/cost totals; live WS snapshot with HTTP-polling fallback |
|
||||||
@@ -8718,6 +8720,7 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| `useReplyAsCeo` | hook | `hooks/use-a2a-live.ts` | Mutation wrapping `a2aApi.replyAsCeo`; invalidates the conversation list + the watched transcript's messages on success |
|
| `useReplyAsCeo` | hook | `hooks/use-a2a-live.ts` | Mutation wrapping `a2aApi.replyAsCeo`; invalidates the conversation list + the watched transcript's messages on success |
|
||||||
| `A2AView` | comp | `components/a2a/a2a-view.tsx` | Conversations tab body — pure lift of the old standalone `/a2a` page (switchboard/list, transcript, reply/direct composer, New DM); owns the `?dm=` quick-action latch (`dmParam`/`prevDmParam` render-phase state, re-arms once `dm` is stripped) and the `?conversation=` selection, both now targeting `/agents` instead of `/a2a` |
|
| `A2AView` | comp | `components/a2a/a2a-view.tsx` | Conversations tab body — pure lift of the old standalone `/a2a` page (switchboard/list, transcript, reply/direct composer, New DM); owns the `?dm=` quick-action latch (`dmParam`/`prevDmParam` render-phase state, re-arms once `dm` is stripped) and the `?conversation=` selection, both now targeting `/agents` instead of `/a2a` |
|
||||||
| `AgentsFleetView` | comp | `components/agents/agents-fleet-view.tsx` | Fleet tab body — pure lift of the old standalone `/agents` page (roster grids, orchestrator status, spawn/stop controls); `AgentCard`'s DM button pushes the `?dm=` deep link the Conversations tab's latch consumes |
|
| `AgentsFleetView` | comp | `components/agents/agents-fleet-view.tsx` | Fleet tab body — pure lift of the old standalone `/agents` page (roster grids, orchestrator status, spawn/stop controls); `AgentCard`'s DM button pushes the `?dm=` deep link the Conversations tab's latch consumes |
|
||||||
|
| `JournalsView` | comp | `components/journals/journals-view.tsx` | Journals tab body — pure lift of the old standalone `/journals` page (agent picker + filterable entry list); its `agent`/`type`/`task` URL params keep working, every writer now targets `/agents` (not `/journals`) preserving the rest of the query string (e.g. `tab=journals`), mirroring the `A2AView` idiom; the agent search box stays local `useState`, not URL-synced, so a per-keystroke filter doesn't bounce scroll position |
|
||||||
| DM quick-action (`?dm=` deep link) | logic | `components/agents/agent-card.tsx` + `components/a2a/a2a-view.tsx` + `components/a2a/a2a-new-dm-dialog.tsx` | `AgentCard`'s DM button (hidden for `EXCLUDE_NON_DM_ROLES`) pushes `/agents?tab=conversations&dm=<agent id>`; `A2AViewContent` latches it open once per distinct value then strips `dm` from the URL, re-arming (`prevDmParam` reset to `null`) so a repeated identical deep link still fires instead of being silently swallowed by a stale latch; `A2ANewDmDialog` only preselects the target once it resolves against the live roster and passes `EXCLUDE_NON_DM_ROLES` — the deep link is untrusted URL input, never trusted blindly |
|
| DM quick-action (`?dm=` deep link) | logic | `components/agents/agent-card.tsx` + `components/a2a/a2a-view.tsx` + `components/a2a/a2a-new-dm-dialog.tsx` | `AgentCard`'s DM button (hidden for `EXCLUDE_NON_DM_ROLES`) pushes `/agents?tab=conversations&dm=<agent id>`; `A2AViewContent` latches it open once per distinct value then strips `dm` from the URL, re-arming (`prevDmParam` reset to `null`) so a repeated identical deep link still fires instead of being silently swallowed by a stale latch; `A2ANewDmDialog` only preselects the target once it resolves against the live roster and passes `EXCLUDE_NON_DM_ROLES` — the deep link is untrusted URL input, never trusted blindly |
|
||||||
| `A2ASwitchboard` / `A2APairCard` | comp | `components/a2a/a2a-switchboard.tsx` + `a2a-pair-card.tsx` | Org-chart pair cards grouped into sections (cell/PM-chain/board/cross-team) via `groupPairsBySection`; each card pulses for `PAIR_PULSE_FADE_MS` (45s) after a matching live frame |
|
| `A2ASwitchboard` / `A2APairCard` | comp | `components/a2a/a2a-switchboard.tsx` + `a2a-pair-card.tsx` | Org-chart pair cards grouped into sections (cell/PM-chain/board/cross-team) via `groupPairsBySection`; each card pulses for `PAIR_PULSE_FADE_MS` (45s) after a matching live frame |
|
||||||
| `A2AReplyComposer` | comp | `components/a2a/a2a-reply-composer.tsx` | CEO chime-in box on a selected WATCHED conversation; disabled when it has no linked task (A2A sends require one) |
|
| `A2AReplyComposer` | comp | `components/a2a/a2a-reply-composer.tsx` | CEO chime-in box on a selected WATCHED conversation; disabled when it has no linked task (A2A sends require one) |
|
||||||
@@ -8734,7 +8737,7 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| `XCredentialsCard` | comp | `components/settings/x-credentials-card.tsx` | Write-only entry of the 4 OAuth 1.0a secrets; set-all-4 or clear-all-4 |
|
| `XCredentialsCard` | comp | `components/settings/x-credentials-card.tsx` | Write-only entry of the 4 OAuth 1.0a secrets; set-all-4 or clear-all-4 |
|
||||||
| `RequiredNotesDialog` | comp | `components/ui/required-notes-dialog.tsx` | Reusable notes-gated confirm; submit disabled on empty/whitespace |
|
| `RequiredNotesDialog` | comp | `components/ui/required-notes-dialog.tsx` | Reusable notes-gated confirm; submit disabled on empty/whitespace |
|
||||||
| `CommandCenter` | comp | `components/dashboard/command-center.tsx` | Overview page body; composes all dashboard cards |
|
| `CommandCenter` | comp | `components/dashboard/command-center.tsx` | Overview page body; composes all dashboard cards |
|
||||||
| `QUICK_ACTIONS_REGISTRY` | const | `components/dashboard/quick-actions-registry.ts` | 23-action catalog (id/label/icon/href/tip) derived from the sidebar route surface + tab-parameterized deep links; `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy `QuickActionsBar` destination; `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids |
|
| `QUICK_ACTIONS_REGISTRY` | const | `components/dashboard/quick-actions-registry.ts` | 23-action catalog (id/label/icon/href/tip) derived from the sidebar route surface + tab-parameterized deep links; `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy `QuickActionsBar` destination; `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids; the `journals` and `a2a` entries' `href`s retarget to `/agents?tab=journals` / `?tab=conversations` (waves 11 and 9) now that their standalone pages are redirect shims |
|
||||||
| `QuickActionsCard` / `QuickActionsCustomizeDialog` | comp | `components/dashboard/quick-actions-card.tsx` | Chosen-order icon+label grid off `useUIStore.quickActionIds` (empty-state message when none selected); pencil-icon dialog toggles/reorders via checkbox + up/down arrows and resets to defaults, persisted through `setQuickActionIds`/`resetQuickActionIds` |
|
| `QuickActionsCard` / `QuickActionsCustomizeDialog` | comp | `components/dashboard/quick-actions-card.tsx` | Chosen-order icon+label grid off `useUIStore.quickActionIds` (empty-state message when none selected); pencil-icon dialog toggles/reorders via checkbox + up/down arrows and resets to defaults, persisted through `setQuickActionIds`/`resetQuickActionIds` |
|
||||||
| `DeliveryTabContent` | comp | `components/metrics/delivery-tab.tsx` | Cycle-time/bottleneck/rework/scorecard panels |
|
| `DeliveryTabContent` | comp | `components/metrics/delivery-tab.tsx` | Cycle-time/bottleneck/rework/scorecard panels |
|
||||||
| `GitActionsPanel` | comp | `components/git/git-actions-panel.tsx` | Commit/push/PR/rebase actions + destructive-confirm "Clean Up Stale Branches" (`AlertDialog`) |
|
| `GitActionsPanel` | comp | `components/git/git-actions-panel.tsx` | Commit/push/PR/rebase actions + destructive-confirm "Clean Up Stale Branches" (`AlertDialog`) |
|
||||||
@@ -8760,10 +8763,10 @@ graph TD
|
|||||||
Routes --> Tasks["tasks + tasks/[id]"]
|
Routes --> Tasks["tasks + tasks/[id]"]
|
||||||
Routes --> Kanban["kanban (dev/qa/pm/pr-review)"]
|
Routes --> Kanban["kanban (dev/qa/pm/pr-review)"]
|
||||||
Routes --> Prompter["prompter (single + MegaTask)"]
|
Routes --> Prompter["prompter (single + MegaTask)"]
|
||||||
Routes --> AgentsHub["agents (Fleet + Conversations tabs; a2a redirects here)"]
|
Routes --> AgentsHub["agents (Fleet + Conversations + Journals tabs; a2a + journals redirect here)"]
|
||||||
Routes --> Settings["settings + ai-providers"]
|
Routes --> Settings["settings + ai-providers"]
|
||||||
Routes --> Workstation["workstation (Products + Projects tabs)"]
|
Routes --> Workstation["workstation (Products + Projects tabs)"]
|
||||||
Routes --> Domain["business/journals/git/kb/auditor/work-sessions/notifications"]
|
Routes --> Domain["business/git/kb/auditor/work-sessions/notifications"]
|
||||||
Overview --> Dash["components/dashboard/*"]
|
Overview --> Dash["components/dashboard/*"]
|
||||||
Dash --> Release["ReleaseProposalCard"]
|
Dash --> Release["ReleaseProposalCard"]
|
||||||
Dash --> Playbook["PlaybookReviewQueue"]
|
Dash --> Playbook["PlaybookReviewQueue"]
|
||||||
@@ -8794,11 +8797,12 @@ panel/ (Next.js 16, package roboco-panel v0.14.0)
|
|||||||
│ ├── tasks/page.tsx + tasks/[taskId]/page.tsx
|
│ ├── tasks/page.tsx + tasks/[taskId]/page.tsx
|
||||||
│ ├── kanban/page.tsx (dev/qa/pm/pr-review views)
|
│ ├── kanban/page.tsx (dev/qa/pm/pr-review views)
|
||||||
│ ├── prompter/page.tsx (intake chat: single + MegaTask batch)
|
│ ├── prompter/page.tsx (intake chat: single + MegaTask batch)
|
||||||
│ ├── agents/page.tsx (Agents hub: Fleet + Conversations tabs; a2a/page.tsx now redirects here)
|
│ ├── agents/page.tsx (Agents hub: Fleet + Conversations + Journals tabs; a2a/page.tsx + journals/page.tsx now redirect here)
|
||||||
│ ├── a2a/page.tsx (redirect → /agents?tab=conversations)
|
│ ├── a2a/page.tsx (redirect → /agents?tab=conversations)
|
||||||
|
│ ├── journals/page.tsx (redirect → /agents?tab=journals; journals/[entryId]/page.tsx unchanged)
|
||||||
│ ├── settings/page.tsx + settings/ai-providers/page.tsx
|
│ ├── settings/page.tsx + settings/ai-providers/page.tsx
|
||||||
│ ├── workstation/page.tsx (Products + Projects tabs; projects/, products/ page.tsx now redirect here)
|
│ ├── workstation/page.tsx (Products + Projects tabs; projects/, products/ page.tsx now redirect here)
|
||||||
│ └── {business,journals,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx
|
│ └── {business,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx
|
||||||
├── src/components/
|
├── src/components/
|
||||||
│ ├── dashboard/ (command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, x-post-queue, roadmap-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions-card, quick-actions-registry, recent-activity)
|
│ ├── dashboard/ (command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, x-post-queue, roadmap-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions-card, quick-actions-registry, recent-activity)
|
||||||
│ ├── metrics/ (delivery-tab, usage-time-series-chart, agent/team-usage-chart, model-usage-donut, sessions-table)
|
│ ├── metrics/ (delivery-tab, usage-time-series-chart, agent/team-usage-chart, model-usage-donut, sessions-table)
|
||||||
@@ -8813,6 +8817,7 @@ panel/ (Next.js 16, package roboco-panel v0.14.0)
|
|||||||
│ ├── conventions/conventions-tab.tsx (per-project architecture map + health)
|
│ ├── conventions/conventions-tab.tsx (per-project architecture map + health)
|
||||||
│ ├── projects/projects-view.tsx + project-card-grid.tsx, products/products-view.tsx + product-card-grid.tsx (Workstation tab panes; card grids reuse the sibling table's exported badge renderers)
|
│ ├── projects/projects-view.tsx + project-card-grid.tsx, products/products-view.tsx + product-card-grid.tsx (Workstation tab panes; card grids reuse the sibling table's exported badge renderers)
|
||||||
│ ├── agents/agents-fleet-view.tsx (Fleet tab body; agent-card.tsx owns the DM quick-action button)
|
│ ├── agents/agents-fleet-view.tsx (Fleet tab body; agent-card.tsx owns the DM quick-action button)
|
||||||
|
│ ├── journals/journals-view.tsx (Journals tab body — pure lift of the old /journals page)
|
||||||
│ ├── projects/ products/ agents/ business/ auditor/ knowledge-base/ git/ journals/ work-sessions/ notifications/ rate-limit/ layout/
|
│ ├── projects/ products/ agents/ business/ auditor/ knowledge-base/ git/ journals/ work-sessions/ notifications/ rate-limit/ layout/
|
||||||
│ └── ui/ (Radix-based primitives: dialog, table, tabs, select, switch, required-notes-dialog, sonner toaster, markdown)
|
│ └── ui/ (Radix-based primitives: dialog, table, tabs, select, switch, required-notes-dialog, sonner toaster, markdown)
|
||||||
├── src/hooks/
|
├── src/hooks/
|
||||||
@@ -8925,6 +8930,7 @@ Deliberately **not** on this card (compose/env-coupled, unsafe for a runtime tog
|
|||||||
> - `49a87c2b`+`9a33efba` (2026-07-18, PR #556, Workstation card grids) — new `product-card-grid.tsx`/`project-card-grid.tsx` (same intrinsic `grid-cols-[repeat(auto-fill,minmax(17rem,1fr))]` sizing as `agent-grid.tsx`), reusing `CellsList`/`ProgressCell` (newly exported from `product-table.tsx`) and `getExternalUrl`/badge renderers (newly exported from `project-table.tsx`); each Workstation surface gains a Cards\|Table toggle (default Cards, persisted per-surface via `ui-store`'s new `productsView`/`projectsView`) plus a name/cell(-count) sort control that only applies in card view — the table keeps its own unsorted render. `sortProducts`/`sortProjects` (both exported for direct unit tests) use a direction multiplier rather than sort-then-reverse, since reversing also flips the relative order of ties, and `sortProjects`' cell-key comparator falls back to the raw `assigned_cell` value for a backend `Team` outside the panel's `teamLabels` map instead of crashing; the fix commit tightened both to this shape after adversarial review caught the initial sort-then-reverse cut.
|
> - `49a87c2b`+`9a33efba` (2026-07-18, PR #556, Workstation card grids) — new `product-card-grid.tsx`/`project-card-grid.tsx` (same intrinsic `grid-cols-[repeat(auto-fill,minmax(17rem,1fr))]` sizing as `agent-grid.tsx`), reusing `CellsList`/`ProgressCell` (newly exported from `product-table.tsx`) and `getExternalUrl`/badge renderers (newly exported from `project-table.tsx`); each Workstation surface gains a Cards\|Table toggle (default Cards, persisted per-surface via `ui-store`'s new `productsView`/`projectsView`) plus a name/cell(-count) sort control that only applies in card view — the table keeps its own unsorted render. `sortProducts`/`sortProjects` (both exported for direct unit tests) use a direction multiplier rather than sort-then-reverse, since reversing also flips the relative order of ties, and `sortProjects`' cell-key comparator falls back to the raw `assigned_cell` value for a backend `Team` outside the panel's `teamLabels` map instead of crashing; the fix commit tightened both to this shape after adversarial review caught the initial sort-then-reverse cut.
|
||||||
> - `97306fe7`+`151d6e0f` (2026-07-18, PR #557, customizable Quick Actions) — the hardcoded `QuickActionsBar` (deleted) is replaced by `QuickActionsCard` (`components/dashboard/quick-actions-card.tsx`), backed by a new 23-action `QUICK_ACTIONS_REGISTRY` (`components/dashboard/quick-actions-registry.ts`, id/label/icon/href/tip per entry, tab-parameterized deep links e.g. `/git?tab=sessions`, `/metrics?tab=delivery`); `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy bar destination (secretary/journals/auditor) so the swap drops nothing from a fresh install's default view. Selection persists as `quickActionIds` on `useUIStore` (new key in `partialize`, contract-tested); `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids instead of crashing. A pencil-icon `QuickActionsCustomizeDialog` (inline in `quick-actions-card.tsx`) lets the CEO show/hide + reorder (up/down arrows) + reset to defaults; the card shows an empty-state message when every action is hidden. `command-center.tsx` and its test swap `QuickActionsBar`/`QuickActionsBarStub` references for `QuickActionsCard`/`QuickActionsCardStub`.
|
> - `97306fe7`+`151d6e0f` (2026-07-18, PR #557, customizable Quick Actions) — the hardcoded `QuickActionsBar` (deleted) is replaced by `QuickActionsCard` (`components/dashboard/quick-actions-card.tsx`), backed by a new 23-action `QUICK_ACTIONS_REGISTRY` (`components/dashboard/quick-actions-registry.ts`, id/label/icon/href/tip per entry, tab-parameterized deep links e.g. `/git?tab=sessions`, `/metrics?tab=delivery`); `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy bar destination (secretary/journals/auditor) so the swap drops nothing from a fresh install's default view. Selection persists as `quickActionIds` on `useUIStore` (new key in `partialize`, contract-tested); `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids instead of crashing. A pencil-icon `QuickActionsCustomizeDialog` (inline in `quick-actions-card.tsx`) lets the CEO show/hide + reorder (up/down arrows) + reset to defaults; the card shows an empty-state message when every action is hidden. `command-center.tsx` and its test swap `QuickActionsBar`/`QuickActionsBarStub` references for `QuickActionsCard`/`QuickActionsCardStub`.
|
||||||
> - `431cb2ae`+`70e53bc1` (2026-07-18, branch `feature/wave-9-agents-hub`, PR #558) — Agents hub: `/agents` gains Fleet + Conversations tabs (`?tab=`, Fleet default). The entire former `/a2a` page body moves into `components/a2a/a2a-view.tsx` (`A2AView`, a pure lift — its own `?conversation=` param keeps working, every writer now targets `/agents` preserving the rest of the query string) and a new `components/agents/agents-fleet-view.tsx` (`AgentsFleetView`) holds the old `/agents` roster body; `a2a/page.tsx` becomes a redirect shim → `/agents?tab=conversations`; the sidebar's standalone A2A nav entry is removed (folded into "Agents"). `AgentCard` gains a DM quick-action button (hidden for `EXCLUDE_NON_DM_ROLES`) pushing `/agents?tab=conversations&dm=<agent id>`; `A2AViewContent` latches the `?dm=` param open once per distinct value and strips it from the URL. Follow-up fix (`70e53bc1`): the deep-linked target was being trusted blindly — `A2ANewDmDialog` now only preselects it once validated against the live roster + `EXCLUDE_NON_DM_ROLES`, and the latch re-arms once `dm` clears so a repeated identical deep link still fires.
|
> - `431cb2ae`+`70e53bc1` (2026-07-18, branch `feature/wave-9-agents-hub`, PR #558) — Agents hub: `/agents` gains Fleet + Conversations tabs (`?tab=`, Fleet default). The entire former `/a2a` page body moves into `components/a2a/a2a-view.tsx` (`A2AView`, a pure lift — its own `?conversation=` param keeps working, every writer now targets `/agents` preserving the rest of the query string) and a new `components/agents/agents-fleet-view.tsx` (`AgentsFleetView`) holds the old `/agents` roster body; `a2a/page.tsx` becomes a redirect shim → `/agents?tab=conversations`; the sidebar's standalone A2A nav entry is removed (folded into "Agents"). `AgentCard` gains a DM quick-action button (hidden for `EXCLUDE_NON_DM_ROLES`) pushing `/agents?tab=conversations&dm=<agent id>`; `A2AViewContent` latches the `?dm=` param open once per distinct value and strips it from the URL. Follow-up fix (`70e53bc1`): the deep-linked target was being trusted blindly — `A2ANewDmDialog` now only preselects it once validated against the live roster + `EXCLUDE_NON_DM_ROLES`, and the latch re-arms once `dm` clears so a repeated identical deep link still fires.
|
||||||
|
> - `cc57b9f1` (2026-07-18, branch `feature/wave-11-journals-tab`) — Journals joins the Agents hub as its third tab: `agents/page.tsx`'s `TabDef.value` gains `"journals"`, rendering `<JournalsView/>` in a third `TabsContent`. The entire former `/journals` page body moves into `components/journals/journals-view.tsx` (`JournalsView`, mirroring the `A2AView` lift — `agent`/`type`/`task` params keep working, every writer now targets `/agents` preserving the rest of the query string); `journals/page.tsx` becomes a redirect shim → `/agents?tab=journals`; `journals/[entryId]/page.tsx`'s three back-links retarget to `/agents?tab=journals` (the detail route itself is unchanged). `sidebar.tsx`'s standalone Journals nav entry is removed (its `BookOpen` icon import dropped) and the "Agents" tip text now reads "...A2A conversations, and journals"; `quick-actions-registry.ts`'s `journals` entry's `href` retargets `/journals` → `/agents?tab=journals` (mirroring the `a2a` entry's wave-9 retarget).
|
||||||
|
|
||||||
## Regression Risks
|
## Regression Risks
|
||||||
|
|
||||||
|
|||||||
+14
-8
@@ -15,11 +15,12 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| `panel/src/app/(dashboard)/tasks/page.tsx` + `tasks/[taskId]/page.tsx` | Task list + task detail (tabbed) |
|
| `panel/src/app/(dashboard)/tasks/page.tsx` + `tasks/[taskId]/page.tsx` | Task list + task detail (tabbed) |
|
||||||
| `panel/src/app/(dashboard)/kanban/page.tsx` | Operator kanban (dev/qa/pm/pr-review views) |
|
| `panel/src/app/(dashboard)/kanban/page.tsx` | Operator kanban (dev/qa/pm/pr-review views) |
|
||||||
| `panel/src/app/(dashboard)/prompter/page.tsx` | Intake chat (single + MegaTask batch scope) |
|
| `panel/src/app/(dashboard)/prompter/page.tsx` | Intake chat (single + MegaTask batch scope) |
|
||||||
| `panel/src/app/(dashboard)/agents/page.tsx` | Agents hub: Fleet (roster/spawn/status, default) + Conversations tabs via `?tab=fleet\|conversations` — `AgentsFleetView` (fleet roster/spawn/activity) and `A2AView` (org-wide switchboard/list + transcript + CEO reply composer + "New DM" dialog, live via `/ws/system` `a2a.message` frames — a pure lift of the old standalone `/a2a` page body) |
|
| `panel/src/app/(dashboard)/agents/page.tsx` | Agents hub: Fleet (roster/spawn/status, default) + Conversations + Journals tabs via `?tab=fleet\|conversations\|journals` — `AgentsFleetView` (fleet roster/spawn/activity), `A2AView` (org-wide switchboard/list + transcript + CEO reply composer + "New DM" dialog, live via `/ws/system` `a2a.message` frames — a pure lift of the old standalone `/a2a` page body), and `JournalsView` (per-agent reflection log — a pure lift of the old standalone `/journals` page body, wave 11) |
|
||||||
| `panel/src/app/(dashboard)/a2a/page.tsx` | Redirect shim → `/agents?tab=conversations` (A2A folded into the Agents hub's Conversations tab, wave 9) — kept so old bookmarks/links still resolve |
|
| `panel/src/app/(dashboard)/a2a/page.tsx` | Redirect shim → `/agents?tab=conversations` (A2A folded into the Agents hub's Conversations tab, wave 9) — kept so old bookmarks/links still resolve |
|
||||||
|
| `panel/src/app/(dashboard)/journals/page.tsx` | Redirect shim → `/agents?tab=journals` (Journals folded into the Agents hub's Journals tab, wave 11) — kept so old bookmarks/links still resolve; `journals/[entryId]/page.tsx` (entry detail) is unchanged at its own route, only its back-links now point at `/agents?tab=journals` |
|
||||||
| `panel/src/app/(dashboard)/settings/page.tsx` + `settings/ai-providers/page.tsx` | Settings: feature flags, AI routing, transcript retention, self-hosted |
|
| `panel/src/app/(dashboard)/settings/page.tsx` + `settings/ai-providers/page.tsx` | Settings: feature flags, AI routing, transcript retention, self-hosted |
|
||||||
| `panel/src/app/(dashboard)/workstation/page.tsx` | Workstation: Products + Projects merged as URL-param tabs (`?tab=products\|projects`, Products first); `products/page.tsx` and `projects/page.tsx` are now server-component redirects to it |
|
| `panel/src/app/(dashboard)/workstation/page.tsx` | Workstation: Products + Projects merged as URL-param tabs (`?tab=products\|projects`, Products first); `products/page.tsx` and `projects/page.tsx` are now server-component redirects to it |
|
||||||
| `panel/src/app/(dashboard)/{business,journals,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx` | Per-domain pages |
|
| `panel/src/app/(dashboard)/{business,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx` | Per-domain pages |
|
||||||
| `panel/src/app/(auth)/login/page.tsx` | Cloud-auth login form (email/password → `useLogin` → `/auth/login`); only reachable/relevant once `proxy.ts` starts gating the `(dashboard)` group |
|
| `panel/src/app/(auth)/login/page.tsx` | Cloud-auth login form (email/password → `useLogin` → `/auth/login`); only reachable/relevant once `proxy.ts` starts gating the `(dashboard)` group |
|
||||||
| `panel/src/app/(tg)/layout.tsx` + `(tg)/tg/page.tsx` | Telegram Mini App cockpit at `/tg`: slim shell (no sidebar/header, `next/script` loads the Telegram WebApp bridge `afterInteractive`) + bootstrap page that resolves `window.Telegram.WebApp`, POSTs its `initData` to `/telegram/webapp-auth` unconditionally, then renders the tabbed cockpit (or an "Open from Telegram" / error state) |
|
| `panel/src/app/(tg)/layout.tsx` + `(tg)/tg/page.tsx` | Telegram Mini App cockpit at `/tg`: slim shell (no sidebar/header, `next/script` loads the Telegram WebApp bridge `afterInteractive`) + bootstrap page that resolves `window.Telegram.WebApp`, POSTs its `initData` to `/telegram/webapp-auth` unconditionally, then renders the tabbed cockpit (or an "Open from Telegram" / error state) |
|
||||||
| `panel/src/proxy.ts` | Next 16's rename of `middleware.ts`: probes `/auth/status` (docker-internal orchestrator URL, fails open to "off" on any error/timeout) and redirects to `/login` when cloud auth is on and no session cookie is present; matcher excludes `tg(?:/|$)` — the Mini App authenticates via Telegram `initData`, not the password cookie, so it must never be redirected to `/login` |
|
| `panel/src/proxy.ts` | Next 16's rename of `middleware.ts`: probes `/auth/status` (docker-internal orchestrator URL, fails open to "off" on any error/timeout) and redirects to `/login` when cloud auth is on and no session cookie is present; matcher excludes `tg(?:/|$)` — the Mini App authenticates via Telegram `initData`, not the password cookie, so it must never be redirected to `/login` |
|
||||||
@@ -33,7 +34,7 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| `panel/src/lib/telegram/webapp.ts` | Thin typed wrapper over the global `window.Telegram.WebApp` (`ready`/`expand`/`initData`); `waitForTelegramWebApp` polls (100ms, 1.5s timeout) for the CDN script since it loads `afterInteractive` |
|
| `panel/src/lib/telegram/webapp.ts` | Thin typed wrapper over the global `window.Telegram.WebApp` (`ready`/`expand`/`initData`); `waitForTelegramWebApp` polls (100ms, 1.5s timeout) for the CDN script since it loads `afterInteractive` |
|
||||||
| `panel/src/components/settings/` | feature-flags-card, ai-routing-card, transcript-retention-card, self-hosted-section, `x-credentials-card.tsx` (write-only OAuth 1.0a secrets, mounted in `settings/page.tsx`) |
|
| `panel/src/components/settings/` | feature-flags-card, ai-routing-card, transcript-retention-card, self-hosted-section, `x-credentials-card.tsx` (write-only OAuth 1.0a secrets, mounted in `settings/page.tsx`) |
|
||||||
| `panel/src/components/conventions/conventions-tab.tsx` | Per-project architecture map + health (in edit-project dialog) |
|
| `panel/src/components/conventions/conventions-tab.tsx` | Per-project architecture map + health (in edit-project dialog) |
|
||||||
| `panel/src/components/projects/`, `products/`, `agents/`, `business/`, `auditor/`, `knowledge-base/`, `git/`, `journals/`, `work-sessions/`, `notifications/`, `rate-limit/`, `layout/`, `ui/` | Per-domain component groups (`projects/` and `products/` each export a `*-view.tsx` consumed by `workstation/page.tsx`, plus a `*-card-grid.tsx` reusing the sibling table's exported badge renderers; `agents/` similarly exports `agents-fleet-view.tsx` (`AgentsFleetView`) consumed by `agents/page.tsx`'s Fleet tab, plus `agent-card.tsx`'s DM quick-action button); `ui/` = Radix-based primitives (dialog, table, tabs, select, switch, required-notes-dialog, sonner toaster, markdown) |
|
| `panel/src/components/projects/`, `products/`, `agents/`, `business/`, `auditor/`, `knowledge-base/`, `git/`, `journals/`, `work-sessions/`, `notifications/`, `rate-limit/`, `layout/`, `ui/` | Per-domain component groups (`projects/` and `products/` each export a `*-view.tsx` consumed by `workstation/page.tsx`, plus a `*-card-grid.tsx` reusing the sibling table's exported badge renderers; `agents/` similarly exports `agents-fleet-view.tsx` (`AgentsFleetView`) consumed by `agents/page.tsx`'s Fleet tab, plus `agent-card.tsx`'s DM quick-action button; `journals/` similarly exports `journals-view.tsx` (`JournalsView`) consumed by `agents/page.tsx`'s Journals tab); `ui/` = Radix-based primitives (dialog, table, tabs, select, switch, required-notes-dialog, sonner toaster, markdown) |
|
||||||
| `panel/src/hooks/use-websocket.ts` | Shared `useWebSocket<T>(path, handlers?, isSystem?)` hook (auto-reconnect, heartbeat) |
|
| `panel/src/hooks/use-websocket.ts` | Shared `useWebSocket<T>(path, handlers?, isSystem?)` hook (auto-reconnect, heartbeat) |
|
||||||
| `panel/src/hooks/use-{tasks,agents,projects,products,usage,prompter,secretary,dashboard,git,journals,notifications,knowledge-base,observability,work-sessions,providers,rate-limit-{sync,websocket}}.ts` | TanStack Query + zustand data hooks |
|
| `panel/src/hooks/use-{tasks,agents,projects,products,usage,prompter,secretary,dashboard,git,journals,notifications,knowledge-base,observability,work-sessions,providers,rate-limit-{sync,websocket}}.ts` | TanStack Query + zustand data hooks |
|
||||||
| `panel/src/hooks/use-a2a-live.ts` | `useA2AConversations` / `useA2AAdminPairs` / `useA2AMessages` (TanStack Query over `a2aApi`) + `useReplyAsCeo` / `useCreateCeoConversation` / `useSendCeoMessage` mutations; `a2aLiveKeys` query-key namespace |
|
| `panel/src/hooks/use-a2a-live.ts` | `useA2AConversations` / `useA2AAdminPairs` / `useA2AMessages` (TanStack Query over `a2aApi`) + `useReplyAsCeo` / `useCreateCeoConversation` / `useSendCeoMessage` mutations; `a2aLiveKeys` query-key namespace |
|
||||||
@@ -62,6 +63,7 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| Intake / MegaTask | `app/(dashboard)/prompter/page.tsx` + `components/prompter/*` | Live SSE chat with spawned Claude/Grok intake agent; single-project, product, or multi-project (`project_ids`) MegaTask → `propose_batch` → `confirm-batch` |
|
| Intake / MegaTask | `app/(dashboard)/prompter/page.tsx` + `components/prompter/*` | Live SSE chat with spawned Claude/Grok intake agent; single-project, product, or multi-project (`project_ids`) MegaTask → `propose_batch` → `confirm-batch` |
|
||||||
| Agents — Fleet | `app/(dashboard)/agents/page.tsx` (`?tab=fleet`, default) + `components/agents/agents-fleet-view.tsx` + `components/agents/*` | Full agent roster grouped into Leadership/Backend/Frontend/UX-UI/Support grids: live state, spawn/stop controls, activity-stream link, token usage. Each card's DM button (hidden for `EXCLUDE_NON_DM_ROLES`) deep-links `/agents?tab=conversations&dm=<agent id>` into the Conversations tab |
|
| Agents — Fleet | `app/(dashboard)/agents/page.tsx` (`?tab=fleet`, default) + `components/agents/agents-fleet-view.tsx` + `components/agents/*` | Full agent roster grouped into Leadership/Backend/Frontend/UX-UI/Support grids: live state, spawn/stop controls, activity-stream link, token usage. Each card's DM button (hidden for `EXCLUDE_NON_DM_ROLES`) deep-links `/agents?tab=conversations&dm=<agent id>` into the Conversations tab |
|
||||||
| Agents — Conversations (switchboard + reply + New DM) | `app/(dashboard)/agents/page.tsx` (`?tab=conversations`) + `components/a2a/a2a-view.tsx` + `components/a2a/*` | CEO watches every agent-to-agent conversation live: default org-chart switchboard (pair cards grouped by cell/PM-chain/board, pulsing on fresh `a2a.message` frames) or the classic conversation list; drill-in shows the transcript + a reply composer that lets the CEO chime into a watched thread as itself (task-linked conversations only). "New DM" opens a fresh CEO-owned 1:1 with any DM-capable agent (no task link needed) — either from the dialog's own picker or preselected from a validated `?dm=` deep link off the Fleet tab; the recipient is woken via the a2a_request dispatch path if offline, and the CEO's own threads render with `A2ADirectComposer` instead of the reply composer. `/a2a` redirects here |
|
| Agents — Conversations (switchboard + reply + New DM) | `app/(dashboard)/agents/page.tsx` (`?tab=conversations`) + `components/a2a/a2a-view.tsx` + `components/a2a/*` | CEO watches every agent-to-agent conversation live: default org-chart switchboard (pair cards grouped by cell/PM-chain/board, pulsing on fresh `a2a.message` frames) or the classic conversation list; drill-in shows the transcript + a reply composer that lets the CEO chime into a watched thread as itself (task-linked conversations only). "New DM" opens a fresh CEO-owned 1:1 with any DM-capable agent (no task link needed) — either from the dialog's own picker or preselected from a validated `?dm=` deep link off the Fleet tab; the recipient is woken via the a2a_request dispatch path if offline, and the CEO's own threads render with `A2ADirectComposer` instead of the reply composer. `/a2a` redirects here |
|
||||||
|
| Agents — Journals | `app/(dashboard)/agents/page.tsx` (`?tab=journals`) + `components/journals/journals-view.tsx` + `components/journals/*` | Per-agent reflection log lifted into the Agents hub's third tab: agent picker (left, local search state seeded from the last saved query) + a type/task-filterable entry list (right, `JournalView`); each task-linked entry carries a Task badge + copy-UUID button. Entry detail still lives at `/journals/{entryId}`, its back-links now pointing at `/agents?tab=journals`. `/journals` redirects here |
|
||||||
| Workstation | `app/(dashboard)/workstation/page.tsx` + `components/{products,projects}/*-view.tsx` | Products + Projects as one sidebar entry, tab-switched via `?tab=`; each surface has a Cards\|Table view toggle (default Cards, persisted per-surface via `ui-store`'s `productsView`/`projectsView`) with client-side name/cell(-count) sorting in card view; Projects' q/cell/inactive filters are local `useState`, not URL params (scroll-bounce prevention) |
|
| Workstation | `app/(dashboard)/workstation/page.tsx` + `components/{products,projects}/*-view.tsx` | Products + Projects as one sidebar entry, tab-switched via `?tab=`; each surface has a Cards\|Table view toggle (default Cards, persisted per-surface via `ui-store`'s `productsView`/`projectsView`) with client-side name/cell(-count) sorting in card view; Projects' q/cell/inactive filters are local `useState`, not URL params (scroll-bounce prevention) |
|
||||||
| Project Settings / Conventions | `components/projects/edit-project-dialog.tsx` + `components/conventions/conventions-tab.tsx` | Per-project `.roboco/conventions.yml` map + health; Save / Restore via PR |
|
| Project Settings / Conventions | `components/projects/edit-project-dialog.tsx` + `components/conventions/conventions-tab.tsx` | Per-project `.roboco/conventions.yml` map + health; Save / Restore via PR |
|
||||||
| Usage Dashboard | `components/dashboard/usage-overview-panel.tsx` + `hooks/use-usage.ts` | Token/cost totals; live WS snapshot with HTTP-polling fallback |
|
| Usage Dashboard | `components/dashboard/usage-overview-panel.tsx` + `hooks/use-usage.ts` | Token/cost totals; live WS snapshot with HTTP-polling fallback |
|
||||||
@@ -91,6 +93,7 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| `useReplyAsCeo` | hook | `hooks/use-a2a-live.ts` | Mutation wrapping `a2aApi.replyAsCeo`; invalidates the conversation list + the watched transcript's messages on success |
|
| `useReplyAsCeo` | hook | `hooks/use-a2a-live.ts` | Mutation wrapping `a2aApi.replyAsCeo`; invalidates the conversation list + the watched transcript's messages on success |
|
||||||
| `A2AView` | comp | `components/a2a/a2a-view.tsx` | Conversations tab body — pure lift of the old standalone `/a2a` page (switchboard/list, transcript, reply/direct composer, New DM); owns the `?dm=` quick-action latch (`dmParam`/`prevDmParam` render-phase state, re-arms once `dm` is stripped) and the `?conversation=` selection, both now targeting `/agents` instead of `/a2a` |
|
| `A2AView` | comp | `components/a2a/a2a-view.tsx` | Conversations tab body — pure lift of the old standalone `/a2a` page (switchboard/list, transcript, reply/direct composer, New DM); owns the `?dm=` quick-action latch (`dmParam`/`prevDmParam` render-phase state, re-arms once `dm` is stripped) and the `?conversation=` selection, both now targeting `/agents` instead of `/a2a` |
|
||||||
| `AgentsFleetView` | comp | `components/agents/agents-fleet-view.tsx` | Fleet tab body — pure lift of the old standalone `/agents` page (roster grids, orchestrator status, spawn/stop controls); `AgentCard`'s DM button pushes the `?dm=` deep link the Conversations tab's latch consumes |
|
| `AgentsFleetView` | comp | `components/agents/agents-fleet-view.tsx` | Fleet tab body — pure lift of the old standalone `/agents` page (roster grids, orchestrator status, spawn/stop controls); `AgentCard`'s DM button pushes the `?dm=` deep link the Conversations tab's latch consumes |
|
||||||
|
| `JournalsView` | comp | `components/journals/journals-view.tsx` | Journals tab body — pure lift of the old standalone `/journals` page (agent picker + filterable entry list); its `agent`/`type`/`task` URL params keep working, every writer now targets `/agents` (not `/journals`) preserving the rest of the query string (e.g. `tab=journals`), mirroring the `A2AView` idiom; the agent search box stays local `useState`, not URL-synced, so a per-keystroke filter doesn't bounce scroll position |
|
||||||
| DM quick-action (`?dm=` deep link) | logic | `components/agents/agent-card.tsx` + `components/a2a/a2a-view.tsx` + `components/a2a/a2a-new-dm-dialog.tsx` | `AgentCard`'s DM button (hidden for `EXCLUDE_NON_DM_ROLES`) pushes `/agents?tab=conversations&dm=<agent id>`; `A2AViewContent` latches it open once per distinct value then strips `dm` from the URL, re-arming (`prevDmParam` reset to `null`) so a repeated identical deep link still fires instead of being silently swallowed by a stale latch; `A2ANewDmDialog` only preselects the target once it resolves against the live roster and passes `EXCLUDE_NON_DM_ROLES` — the deep link is untrusted URL input, never trusted blindly |
|
| DM quick-action (`?dm=` deep link) | logic | `components/agents/agent-card.tsx` + `components/a2a/a2a-view.tsx` + `components/a2a/a2a-new-dm-dialog.tsx` | `AgentCard`'s DM button (hidden for `EXCLUDE_NON_DM_ROLES`) pushes `/agents?tab=conversations&dm=<agent id>`; `A2AViewContent` latches it open once per distinct value then strips `dm` from the URL, re-arming (`prevDmParam` reset to `null`) so a repeated identical deep link still fires instead of being silently swallowed by a stale latch; `A2ANewDmDialog` only preselects the target once it resolves against the live roster and passes `EXCLUDE_NON_DM_ROLES` — the deep link is untrusted URL input, never trusted blindly |
|
||||||
| `A2ASwitchboard` / `A2APairCard` | comp | `components/a2a/a2a-switchboard.tsx` + `a2a-pair-card.tsx` | Org-chart pair cards grouped into sections (cell/PM-chain/board/cross-team) via `groupPairsBySection`; each card pulses for `PAIR_PULSE_FADE_MS` (45s) after a matching live frame |
|
| `A2ASwitchboard` / `A2APairCard` | comp | `components/a2a/a2a-switchboard.tsx` + `a2a-pair-card.tsx` | Org-chart pair cards grouped into sections (cell/PM-chain/board/cross-team) via `groupPairsBySection`; each card pulses for `PAIR_PULSE_FADE_MS` (45s) after a matching live frame |
|
||||||
| `A2AReplyComposer` | comp | `components/a2a/a2a-reply-composer.tsx` | CEO chime-in box on a selected WATCHED conversation; disabled when it has no linked task (A2A sends require one) |
|
| `A2AReplyComposer` | comp | `components/a2a/a2a-reply-composer.tsx` | CEO chime-in box on a selected WATCHED conversation; disabled when it has no linked task (A2A sends require one) |
|
||||||
@@ -107,7 +110,7 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| `XCredentialsCard` | comp | `components/settings/x-credentials-card.tsx` | Write-only entry of the 4 OAuth 1.0a secrets; set-all-4 or clear-all-4 |
|
| `XCredentialsCard` | comp | `components/settings/x-credentials-card.tsx` | Write-only entry of the 4 OAuth 1.0a secrets; set-all-4 or clear-all-4 |
|
||||||
| `RequiredNotesDialog` | comp | `components/ui/required-notes-dialog.tsx` | Reusable notes-gated confirm; submit disabled on empty/whitespace |
|
| `RequiredNotesDialog` | comp | `components/ui/required-notes-dialog.tsx` | Reusable notes-gated confirm; submit disabled on empty/whitespace |
|
||||||
| `CommandCenter` | comp | `components/dashboard/command-center.tsx` | Overview page body; composes all dashboard cards |
|
| `CommandCenter` | comp | `components/dashboard/command-center.tsx` | Overview page body; composes all dashboard cards |
|
||||||
| `QUICK_ACTIONS_REGISTRY` | const | `components/dashboard/quick-actions-registry.ts` | 23-action catalog (id/label/icon/href/tip) derived from the sidebar route surface + tab-parameterized deep links; `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy `QuickActionsBar` destination; `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids |
|
| `QUICK_ACTIONS_REGISTRY` | const | `components/dashboard/quick-actions-registry.ts` | 23-action catalog (id/label/icon/href/tip) derived from the sidebar route surface + tab-parameterized deep links; `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy `QuickActionsBar` destination; `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids; the `journals` and `a2a` entries' `href`s retarget to `/agents?tab=journals` / `?tab=conversations` (waves 11 and 9) now that their standalone pages are redirect shims |
|
||||||
| `QuickActionsCard` / `QuickActionsCustomizeDialog` | comp | `components/dashboard/quick-actions-card.tsx` | Chosen-order icon+label grid off `useUIStore.quickActionIds` (empty-state message when none selected); pencil-icon dialog toggles/reorders via checkbox + up/down arrows and resets to defaults, persisted through `setQuickActionIds`/`resetQuickActionIds` |
|
| `QuickActionsCard` / `QuickActionsCustomizeDialog` | comp | `components/dashboard/quick-actions-card.tsx` | Chosen-order icon+label grid off `useUIStore.quickActionIds` (empty-state message when none selected); pencil-icon dialog toggles/reorders via checkbox + up/down arrows and resets to defaults, persisted through `setQuickActionIds`/`resetQuickActionIds` |
|
||||||
| `DeliveryTabContent` | comp | `components/metrics/delivery-tab.tsx` | Cycle-time/bottleneck/rework/scorecard panels |
|
| `DeliveryTabContent` | comp | `components/metrics/delivery-tab.tsx` | Cycle-time/bottleneck/rework/scorecard panels |
|
||||||
| `GitActionsPanel` | comp | `components/git/git-actions-panel.tsx` | Commit/push/PR/rebase actions + destructive-confirm "Clean Up Stale Branches" (`AlertDialog`) |
|
| `GitActionsPanel` | comp | `components/git/git-actions-panel.tsx` | Commit/push/PR/rebase actions + destructive-confirm "Clean Up Stale Branches" (`AlertDialog`) |
|
||||||
@@ -133,10 +136,10 @@ graph TD
|
|||||||
Routes --> Tasks["tasks + tasks/[id]"]
|
Routes --> Tasks["tasks + tasks/[id]"]
|
||||||
Routes --> Kanban["kanban (dev/qa/pm/pr-review)"]
|
Routes --> Kanban["kanban (dev/qa/pm/pr-review)"]
|
||||||
Routes --> Prompter["prompter (single + MegaTask)"]
|
Routes --> Prompter["prompter (single + MegaTask)"]
|
||||||
Routes --> AgentsHub["agents (Fleet + Conversations tabs; a2a redirects here)"]
|
Routes --> AgentsHub["agents (Fleet + Conversations + Journals tabs; a2a + journals redirect here)"]
|
||||||
Routes --> Settings["settings + ai-providers"]
|
Routes --> Settings["settings + ai-providers"]
|
||||||
Routes --> Workstation["workstation (Products + Projects tabs)"]
|
Routes --> Workstation["workstation (Products + Projects tabs)"]
|
||||||
Routes --> Domain["business/journals/git/kb/auditor/work-sessions/notifications"]
|
Routes --> Domain["business/git/kb/auditor/work-sessions/notifications"]
|
||||||
Overview --> Dash["components/dashboard/*"]
|
Overview --> Dash["components/dashboard/*"]
|
||||||
Dash --> Release["ReleaseProposalCard"]
|
Dash --> Release["ReleaseProposalCard"]
|
||||||
Dash --> Playbook["PlaybookReviewQueue"]
|
Dash --> Playbook["PlaybookReviewQueue"]
|
||||||
@@ -170,11 +173,12 @@ panel/ (Next.js 16, package roboco-panel v0.14.0)
|
|||||||
│ ├── tasks/page.tsx + tasks/[taskId]/page.tsx
|
│ ├── tasks/page.tsx + tasks/[taskId]/page.tsx
|
||||||
│ ├── kanban/page.tsx (dev/qa/pm/pr-review views)
|
│ ├── kanban/page.tsx (dev/qa/pm/pr-review views)
|
||||||
│ ├── prompter/page.tsx (intake chat: single + MegaTask batch)
|
│ ├── prompter/page.tsx (intake chat: single + MegaTask batch)
|
||||||
│ ├── agents/page.tsx (Agents hub: Fleet + Conversations tabs; a2a/page.tsx now redirects here)
|
│ ├── agents/page.tsx (Agents hub: Fleet + Conversations + Journals tabs; a2a/page.tsx + journals/page.tsx now redirect here)
|
||||||
│ ├── a2a/page.tsx (redirect → /agents?tab=conversations)
|
│ ├── a2a/page.tsx (redirect → /agents?tab=conversations)
|
||||||
|
│ ├── journals/page.tsx (redirect → /agents?tab=journals; journals/[entryId]/page.tsx unchanged)
|
||||||
│ ├── settings/page.tsx + settings/ai-providers/page.tsx
|
│ ├── settings/page.tsx + settings/ai-providers/page.tsx
|
||||||
│ ├── workstation/page.tsx (Products + Projects tabs; projects/, products/ page.tsx now redirect here)
|
│ ├── workstation/page.tsx (Products + Projects tabs; projects/, products/ page.tsx now redirect here)
|
||||||
│ └── {business,journals,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx
|
│ └── {business,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx
|
||||||
├── src/components/
|
├── src/components/
|
||||||
│ ├── dashboard/ (command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, x-post-queue, roadmap-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions-card, quick-actions-registry, recent-activity)
|
│ ├── dashboard/ (command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, x-post-queue, roadmap-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions-card, quick-actions-registry, recent-activity)
|
||||||
│ ├── metrics/ (delivery-tab, usage-time-series-chart, agent/team-usage-chart, model-usage-donut, sessions-table)
|
│ ├── metrics/ (delivery-tab, usage-time-series-chart, agent/team-usage-chart, model-usage-donut, sessions-table)
|
||||||
@@ -190,6 +194,7 @@ panel/ (Next.js 16, package roboco-panel v0.14.0)
|
|||||||
│ ├── conventions/conventions-tab.tsx (per-project architecture map + health)
|
│ ├── conventions/conventions-tab.tsx (per-project architecture map + health)
|
||||||
│ ├── projects/projects-view.tsx + project-card-grid.tsx, products/products-view.tsx + product-card-grid.tsx (Workstation tab panes; card grids reuse the sibling table's exported badge renderers)
|
│ ├── projects/projects-view.tsx + project-card-grid.tsx, products/products-view.tsx + product-card-grid.tsx (Workstation tab panes; card grids reuse the sibling table's exported badge renderers)
|
||||||
│ ├── agents/agents-fleet-view.tsx (Fleet tab body; agent-card.tsx owns the DM quick-action button)
|
│ ├── agents/agents-fleet-view.tsx (Fleet tab body; agent-card.tsx owns the DM quick-action button)
|
||||||
|
│ ├── journals/journals-view.tsx (Journals tab body — pure lift of the old /journals page)
|
||||||
│ ├── projects/ products/ agents/ business/ auditor/ knowledge-base/ git/ journals/ work-sessions/ notifications/ rate-limit/ layout/
|
│ ├── projects/ products/ agents/ business/ auditor/ knowledge-base/ git/ journals/ work-sessions/ notifications/ rate-limit/ layout/
|
||||||
│ └── ui/ (Radix-based primitives: dialog, table, tabs, select, switch, required-notes-dialog, sonner toaster, markdown)
|
│ └── ui/ (Radix-based primitives: dialog, table, tabs, select, switch, required-notes-dialog, sonner toaster, markdown)
|
||||||
├── src/hooks/
|
├── src/hooks/
|
||||||
@@ -306,6 +311,7 @@ Deliberately **not** on this card (compose/env-coupled, unsafe for a runtime tog
|
|||||||
> - `49a87c2b`+`9a33efba` (2026-07-18, PR #556, Workstation card grids) — new `product-card-grid.tsx`/`project-card-grid.tsx` (same intrinsic `grid-cols-[repeat(auto-fill,minmax(17rem,1fr))]` sizing as `agent-grid.tsx`), reusing `CellsList`/`ProgressCell` (newly exported from `product-table.tsx`) and `getExternalUrl`/badge renderers (newly exported from `project-table.tsx`); each Workstation surface gains a Cards\|Table toggle (default Cards, persisted per-surface via `ui-store`'s new `productsView`/`projectsView`) plus a name/cell(-count) sort control that only applies in card view — the table keeps its own unsorted render. `sortProducts`/`sortProjects` (both exported for direct unit tests) use a direction multiplier rather than sort-then-reverse, since reversing also flips the relative order of ties, and `sortProjects`' cell-key comparator falls back to the raw `assigned_cell` value for a backend `Team` outside the panel's `teamLabels` map instead of crashing; the fix commit tightened both to this shape after adversarial review caught the initial sort-then-reverse cut.
|
> - `49a87c2b`+`9a33efba` (2026-07-18, PR #556, Workstation card grids) — new `product-card-grid.tsx`/`project-card-grid.tsx` (same intrinsic `grid-cols-[repeat(auto-fill,minmax(17rem,1fr))]` sizing as `agent-grid.tsx`), reusing `CellsList`/`ProgressCell` (newly exported from `product-table.tsx`) and `getExternalUrl`/badge renderers (newly exported from `project-table.tsx`); each Workstation surface gains a Cards\|Table toggle (default Cards, persisted per-surface via `ui-store`'s new `productsView`/`projectsView`) plus a name/cell(-count) sort control that only applies in card view — the table keeps its own unsorted render. `sortProducts`/`sortProjects` (both exported for direct unit tests) use a direction multiplier rather than sort-then-reverse, since reversing also flips the relative order of ties, and `sortProjects`' cell-key comparator falls back to the raw `assigned_cell` value for a backend `Team` outside the panel's `teamLabels` map instead of crashing; the fix commit tightened both to this shape after adversarial review caught the initial sort-then-reverse cut.
|
||||||
> - `97306fe7`+`151d6e0f` (2026-07-18, PR #557, customizable Quick Actions) — the hardcoded `QuickActionsBar` (deleted) is replaced by `QuickActionsCard` (`components/dashboard/quick-actions-card.tsx`), backed by a new 23-action `QUICK_ACTIONS_REGISTRY` (`components/dashboard/quick-actions-registry.ts`, id/label/icon/href/tip per entry, tab-parameterized deep links e.g. `/git?tab=sessions`, `/metrics?tab=delivery`); `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy bar destination (secretary/journals/auditor) so the swap drops nothing from a fresh install's default view. Selection persists as `quickActionIds` on `useUIStore` (new key in `partialize`, contract-tested); `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids instead of crashing. A pencil-icon `QuickActionsCustomizeDialog` (inline in `quick-actions-card.tsx`) lets the CEO show/hide + reorder (up/down arrows) + reset to defaults; the card shows an empty-state message when every action is hidden. `command-center.tsx` and its test swap `QuickActionsBar`/`QuickActionsBarStub` references for `QuickActionsCard`/`QuickActionsCardStub`.
|
> - `97306fe7`+`151d6e0f` (2026-07-18, PR #557, customizable Quick Actions) — the hardcoded `QuickActionsBar` (deleted) is replaced by `QuickActionsCard` (`components/dashboard/quick-actions-card.tsx`), backed by a new 23-action `QUICK_ACTIONS_REGISTRY` (`components/dashboard/quick-actions-registry.ts`, id/label/icon/href/tip per entry, tab-parameterized deep links e.g. `/git?tab=sessions`, `/metrics?tab=delivery`); `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy bar destination (secretary/journals/auditor) so the swap drops nothing from a fresh install's default view. Selection persists as `quickActionIds` on `useUIStore` (new key in `partialize`, contract-tested); `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids instead of crashing. A pencil-icon `QuickActionsCustomizeDialog` (inline in `quick-actions-card.tsx`) lets the CEO show/hide + reorder (up/down arrows) + reset to defaults; the card shows an empty-state message when every action is hidden. `command-center.tsx` and its test swap `QuickActionsBar`/`QuickActionsBarStub` references for `QuickActionsCard`/`QuickActionsCardStub`.
|
||||||
> - `431cb2ae`+`70e53bc1` (2026-07-18, branch `feature/wave-9-agents-hub`, PR #558) — Agents hub: `/agents` gains Fleet + Conversations tabs (`?tab=`, Fleet default). The entire former `/a2a` page body moves into `components/a2a/a2a-view.tsx` (`A2AView`, a pure lift — its own `?conversation=` param keeps working, every writer now targets `/agents` preserving the rest of the query string) and a new `components/agents/agents-fleet-view.tsx` (`AgentsFleetView`) holds the old `/agents` roster body; `a2a/page.tsx` becomes a redirect shim → `/agents?tab=conversations`; the sidebar's standalone A2A nav entry is removed (folded into "Agents"). `AgentCard` gains a DM quick-action button (hidden for `EXCLUDE_NON_DM_ROLES`) pushing `/agents?tab=conversations&dm=<agent id>`; `A2AViewContent` latches the `?dm=` param open once per distinct value and strips it from the URL. Follow-up fix (`70e53bc1`): the deep-linked target was being trusted blindly — `A2ANewDmDialog` now only preselects it once validated against the live roster + `EXCLUDE_NON_DM_ROLES`, and the latch re-arms once `dm` clears so a repeated identical deep link still fires.
|
> - `431cb2ae`+`70e53bc1` (2026-07-18, branch `feature/wave-9-agents-hub`, PR #558) — Agents hub: `/agents` gains Fleet + Conversations tabs (`?tab=`, Fleet default). The entire former `/a2a` page body moves into `components/a2a/a2a-view.tsx` (`A2AView`, a pure lift — its own `?conversation=` param keeps working, every writer now targets `/agents` preserving the rest of the query string) and a new `components/agents/agents-fleet-view.tsx` (`AgentsFleetView`) holds the old `/agents` roster body; `a2a/page.tsx` becomes a redirect shim → `/agents?tab=conversations`; the sidebar's standalone A2A nav entry is removed (folded into "Agents"). `AgentCard` gains a DM quick-action button (hidden for `EXCLUDE_NON_DM_ROLES`) pushing `/agents?tab=conversations&dm=<agent id>`; `A2AViewContent` latches the `?dm=` param open once per distinct value and strips it from the URL. Follow-up fix (`70e53bc1`): the deep-linked target was being trusted blindly — `A2ANewDmDialog` now only preselects it once validated against the live roster + `EXCLUDE_NON_DM_ROLES`, and the latch re-arms once `dm` clears so a repeated identical deep link still fires.
|
||||||
|
> - `cc57b9f1` (2026-07-18, branch `feature/wave-11-journals-tab`) — Journals joins the Agents hub as its third tab: `agents/page.tsx`'s `TabDef.value` gains `"journals"`, rendering `<JournalsView/>` in a third `TabsContent`. The entire former `/journals` page body moves into `components/journals/journals-view.tsx` (`JournalsView`, mirroring the `A2AView` lift — `agent`/`type`/`task` params keep working, every writer now targets `/agents` preserving the rest of the query string); `journals/page.tsx` becomes a redirect shim → `/agents?tab=journals`; `journals/[entryId]/page.tsx`'s three back-links retarget to `/agents?tab=journals` (the detail route itself is unchanged). `sidebar.tsx`'s standalone Journals nav entry is removed (its `BookOpen` icon import dropped) and the "Agents" tip text now reads "...A2A conversations, and journals"; `quick-actions-registry.ts`'s `journals` entry's `href` retargets `/journals` → `/agents?tab=journals` (mirroring the `a2a` entry's wave-9 retarget).
|
||||||
|
|
||||||
## Regression Risks
|
## Regression Risks
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
|
|
||||||
// The two tab panes have their own dedicated tests — stub them here so this
|
// The three tab panes have their own dedicated tests — stub them here so
|
||||||
// page test only checks tab composition + the URL-driven default, mirroring
|
// this page test only checks tab composition + the URL-driven default,
|
||||||
// workstation/__tests__/page.test.tsx.
|
// mirroring workstation/__tests__/page.test.tsx.
|
||||||
vi.mock("@/components/agents/agents-fleet-view", () => ({
|
vi.mock("@/components/agents/agents-fleet-view", () => ({
|
||||||
AgentsFleetView: () => <div>AgentsFleetViewStub</div>,
|
AgentsFleetView: () => <div>AgentsFleetViewStub</div>,
|
||||||
}));
|
}));
|
||||||
vi.mock("@/components/a2a/a2a-view", () => ({
|
vi.mock("@/components/a2a/a2a-view", () => ({
|
||||||
A2AView: () => <div>A2AViewStub</div>,
|
A2AView: () => <div>A2AViewStub</div>,
|
||||||
}));
|
}));
|
||||||
|
vi.mock("@/components/journals/journals-view", () => ({
|
||||||
|
JournalsView: () => <div>JournalsViewStub</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
const mockReplace = vi.fn();
|
const mockReplace = vi.fn();
|
||||||
let searchParams = new URLSearchParams();
|
let searchParams = new URLSearchParams();
|
||||||
@@ -54,4 +57,19 @@ describe("AgentsPage", () => {
|
|||||||
);
|
);
|
||||||
expect(screen.getByText("A2AViewStub")).toBeInTheDocument();
|
expect(screen.getByText("A2AViewStub")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("activates the Journals tab from ?tab=journals", () => {
|
||||||
|
searchParams = new URLSearchParams("tab=journals");
|
||||||
|
render(<AgentsPage />);
|
||||||
|
|
||||||
|
expect(screen.getByRole("tab", { name: "Journals" })).toHaveAttribute(
|
||||||
|
"data-state",
|
||||||
|
"active",
|
||||||
|
);
|
||||||
|
expect(screen.getByRole("tab", { name: "Fleet" })).toHaveAttribute(
|
||||||
|
"data-state",
|
||||||
|
"inactive",
|
||||||
|
);
|
||||||
|
expect(screen.getByText("JournalsViewStub")).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ import {
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { AgentsFleetView } from "@/components/agents/agents-fleet-view";
|
import { AgentsFleetView } from "@/components/agents/agents-fleet-view";
|
||||||
import { A2AView } from "@/components/a2a/a2a-view";
|
import { A2AView } from "@/components/a2a/a2a-view";
|
||||||
|
import { JournalsView } from "@/components/journals/journals-view";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Valid tab values
|
// Valid tab values
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
interface TabDef {
|
interface TabDef {
|
||||||
value: "fleet" | "conversations";
|
value: "fleet" | "conversations" | "journals";
|
||||||
label: string;
|
label: string;
|
||||||
hint: string;
|
hint: string;
|
||||||
}
|
}
|
||||||
@@ -33,6 +34,11 @@ const TAB_DEFS: TabDef[] = [
|
|||||||
label: "Conversations",
|
label: "Conversations",
|
||||||
hint: "Live agent-to-agent message switchboard and history",
|
hint: "Live agent-to-agent message switchboard and history",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "journals",
|
||||||
|
label: "Journals",
|
||||||
|
hint: "Per-agent reflections, learnings, and decisions",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const TAB_VALUES = TAB_DEFS.map((t) => t.value);
|
const TAB_VALUES = TAB_DEFS.map((t) => t.value);
|
||||||
@@ -88,6 +94,10 @@ function AgentsPageContent() {
|
|||||||
<TabsContent value="conversations" className="mt-4">
|
<TabsContent value="conversations" className="mt-4">
|
||||||
<A2AView />
|
<A2AView />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="journals" className="mt-4">
|
||||||
|
<JournalsView />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default function JournalEntryPage({ params }: JournalEntryPageProps) {
|
|||||||
if (error || !entry) {
|
if (error || !entry) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Link href="/journals" prefetch={false}>
|
<Link href="/agents?tab=journals" prefetch={false}>
|
||||||
<Button variant="ghost" size="sm">
|
<Button variant="ghost" size="sm">
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
Back to Journals
|
Back to Journals
|
||||||
@@ -94,7 +94,7 @@ export default function JournalEntryPage({ params }: JournalEntryPageProps) {
|
|||||||
"The journal entry you're looking for doesn't exist or has been deleted."}
|
"The journal entry you're looking for doesn't exist or has been deleted."}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center gap-4">
|
<div className="flex justify-center gap-4">
|
||||||
<Link href="/journals" prefetch={false}>
|
<Link href="/agents?tab=journals" prefetch={false}>
|
||||||
<Button>View All Journals</Button>
|
<Button>View All Journals</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,7 +112,7 @@ export default function JournalEntryPage({ params }: JournalEntryPageProps) {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Link href="/journals" prefetch={false}>
|
<Link href="/agents?tab=journals" prefetch={false}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
|
||||||
|
const { redirect } = vi.hoisted(() => ({ redirect: vi.fn() }));
|
||||||
|
vi.mock("next/navigation", () => ({ redirect }));
|
||||||
|
|
||||||
|
import JournalsPage from "../page";
|
||||||
|
|
||||||
|
describe("JournalsPage", () => {
|
||||||
|
it("redirects to the Agents hub's Journals tab", () => {
|
||||||
|
JournalsPage();
|
||||||
|
expect(redirect).toHaveBeenCalledWith("/agents?tab=journals");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,272 +1,7 @@
|
|||||||
"use client";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { Suspense, useCallback, useEffect } from "react";
|
// Journals merged into the Agents hub as its Journals tab (CEO decision,
|
||||||
import { useSearchParams, useRouter } from "next/navigation";
|
// wave 11) — this route now just forwards old links/bookmarks.
|
||||||
import { useAgents } from "@/hooks/use-agents";
|
|
||||||
import { JournalEntryType } from "@/types";
|
|
||||||
import { AgentList } from "@/components/journals/agent-list";
|
|
||||||
import { JournalView } from "@/components/journals/journal-view";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import { HelpTip } from "@/components/ui/help-tip";
|
|
||||||
import { usePageRefresh } from "@/hooks";
|
|
||||||
import { BookOpen, Search } from "lucide-react";
|
|
||||||
|
|
||||||
const JOURNALS_STATE_KEY = "roboco-journals-state";
|
|
||||||
|
|
||||||
interface JournalsState {
|
|
||||||
agent: string | null;
|
|
||||||
q: string | null;
|
|
||||||
type: string | null;
|
|
||||||
task: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveJournalsState(state: JournalsState) {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(JOURNALS_STATE_KEY, JSON.stringify(state));
|
|
||||||
} catch {
|
|
||||||
// Ignore localStorage errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadJournalsState(): JournalsState | null {
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem(JOURNALS_STATE_KEY);
|
|
||||||
return stored ? JSON.parse(stored) : null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function JournalsPageContent() {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
// Read state from URL params
|
|
||||||
const urlAgentId = searchParams.get("agent");
|
|
||||||
const urlSearch = searchParams.get("q") || "";
|
|
||||||
const urlType = searchParams.get("type");
|
|
||||||
const urlTask = searchParams.get("task");
|
|
||||||
|
|
||||||
// Restore from localStorage if URL has no params (fresh navigation)
|
|
||||||
useEffect(() => {
|
|
||||||
const hasUrlParams = urlAgentId || urlSearch || urlType || urlTask;
|
|
||||||
if (!hasUrlParams) {
|
|
||||||
const saved = loadJournalsState();
|
|
||||||
if (saved?.agent) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (saved.agent) params.set("agent", saved.agent);
|
|
||||||
if (saved.q) params.set("q", saved.q);
|
|
||||||
if (saved.type) params.set("type", saved.type);
|
|
||||||
if (saved.task) params.set("task", saved.task);
|
|
||||||
const query = params.toString();
|
|
||||||
if (query) {
|
|
||||||
router.replace(`/journals?${query}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []); // Intentionally only run on mount
|
|
||||||
|
|
||||||
// Derive state from URL
|
|
||||||
const selectedAgentId = urlAgentId;
|
|
||||||
const agentSearch = urlSearch;
|
|
||||||
const typeFilter = (urlType as JournalEntryType) || "all";
|
|
||||||
const taskFilter = urlTask;
|
|
||||||
|
|
||||||
const { data: agents, isLoading: loadingAgents, refetch } = useAgents();
|
|
||||||
|
|
||||||
const { register, unregister } = usePageRefresh();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const cb = () => {
|
|
||||||
void refetch();
|
|
||||||
};
|
|
||||||
register(cb);
|
|
||||||
return () => unregister(cb);
|
|
||||||
}, [register, unregister, refetch]);
|
|
||||||
|
|
||||||
// Save state to localStorage whenever URL params change
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedAgentId) {
|
|
||||||
saveJournalsState({
|
|
||||||
agent: selectedAgentId,
|
|
||||||
q: agentSearch || null,
|
|
||||||
type: urlType, // Use raw URL param (null if "all")
|
|
||||||
task: taskFilter,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [selectedAgentId, agentSearch, urlType, taskFilter]);
|
|
||||||
|
|
||||||
// Update URL params
|
|
||||||
const updateParams = useCallback(
|
|
||||||
(updates: Record<string, string | null>) => {
|
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
|
||||||
Object.entries(updates).forEach(([key, value]) => {
|
|
||||||
if (value) {
|
|
||||||
params.set(key, value);
|
|
||||||
} else {
|
|
||||||
params.delete(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const query = params.toString();
|
|
||||||
router.push(query ? `/journals?${query}` : "/journals");
|
|
||||||
},
|
|
||||||
[router, searchParams],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelectAgent = useCallback(
|
|
||||||
(agentId: string | null) => {
|
|
||||||
// Only reset filters when changing to a different agent
|
|
||||||
if (agentId !== selectedAgentId) {
|
|
||||||
updateParams({ agent: agentId, type: null, task: null });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[updateParams, selectedAgentId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAgentSearch = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
updateParams({ q: value || null });
|
|
||||||
},
|
|
||||||
[updateParams],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTypeChange = useCallback(
|
|
||||||
(value: JournalEntryType | "all") => {
|
|
||||||
updateParams({ type: value === "all" ? null : value });
|
|
||||||
},
|
|
||||||
[updateParams],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTaskChange = useCallback(
|
|
||||||
(value: string | null) => {
|
|
||||||
updateParams({ task: value });
|
|
||||||
},
|
|
||||||
[updateParams],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Filter agents by search
|
|
||||||
const filteredAgents = (agents ?? []).filter((agent) => {
|
|
||||||
if (!agentSearch) return true;
|
|
||||||
const query = agentSearch.toLowerCase();
|
|
||||||
return (
|
|
||||||
agent.agent_id.toLowerCase().includes(query) ||
|
|
||||||
agent.role.toLowerCase().includes(query) ||
|
|
||||||
agent.team?.toLowerCase().includes(query)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get selected agent
|
|
||||||
const selectedAgent = agents?.find((a) => a.agent_id === selectedAgentId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-col gap-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between shrink-0">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Agent Journals</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
View agent reflections, learnings, and decisions
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content — one screen; the agent list and the detail each scroll inside */}
|
|
||||||
<div className="grid grid-cols-12 gap-6 flex-1 min-h-0">
|
|
||||||
{/* Sidebar */}
|
|
||||||
<div className="col-span-12 lg:col-span-3 min-h-0">
|
|
||||||
<Card className="h-full flex flex-col">
|
|
||||||
<CardContent className="p-3 flex flex-1 flex-col min-h-0">
|
|
||||||
{/* Agent Search */}
|
|
||||||
<HelpTip label="Filters the agent list below by ID, role, or team">
|
|
||||||
<div className="relative mb-3">
|
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
value={agentSearch}
|
|
||||||
onChange={(e) => handleAgentSearch(e.target.value)}
|
|
||||||
placeholder="Search agents..."
|
|
||||||
className="pl-9"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</HelpTip>
|
|
||||||
|
|
||||||
{/* Agent List */}
|
|
||||||
<AgentList
|
|
||||||
agents={filteredAgents}
|
|
||||||
isLoading={loadingAgents}
|
|
||||||
selectedAgentId={selectedAgentId}
|
|
||||||
onSelectAgent={handleSelectAgent}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Journal Content */}
|
|
||||||
<div className="col-span-12 lg:col-span-9 min-h-0">
|
|
||||||
<Card className="h-full flex flex-col">
|
|
||||||
<CardContent className="p-6 flex-1 min-h-0 overflow-hidden">
|
|
||||||
{selectedAgent ? (
|
|
||||||
<JournalView
|
|
||||||
agent={selectedAgent}
|
|
||||||
typeFilter={typeFilter as JournalEntryType | "all"}
|
|
||||||
onTypeChange={handleTypeChange}
|
|
||||||
taskFilter={taskFilter}
|
|
||||||
onTaskChange={handleTaskChange}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-16 text-muted-foreground">
|
|
||||||
<BookOpen className="h-16 w-16 mx-auto mb-4 opacity-50" />
|
|
||||||
<h3 className="text-lg font-medium mb-2">Select an Agent</h3>
|
|
||||||
<p className="text-sm">
|
|
||||||
Choose an agent from the list to view their journal entries
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap in Suspense for useSearchParams
|
|
||||||
export default function JournalsPage() {
|
export default function JournalsPage() {
|
||||||
return (
|
redirect("/agents?tab=journals");
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<Skeleton className="h-9 w-48 mb-2" />
|
|
||||||
<Skeleton className="h-5 w-64" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-12 gap-6">
|
|
||||||
<div className="col-span-12 lg:col-span-3">
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-3 space-y-2">
|
|
||||||
<Skeleton className="h-10 w-full" />
|
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
|
||||||
<Skeleton key={i} className="h-12 w-full" />
|
|
||||||
))}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 lg:col-span-9">
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<Skeleton className="h-64 w-full" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<JournalsPageContent />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export const QUICK_ACTIONS_REGISTRY: QuickAction[] = [
|
|||||||
id: "a2a",
|
id: "a2a",
|
||||||
label: "A2A",
|
label: "A2A",
|
||||||
icon: Radio,
|
icon: Radio,
|
||||||
href: "/a2a",
|
href: "/agents?tab=conversations",
|
||||||
tip: "Live agent-to-agent message switchboard and history",
|
tip: "Live agent-to-agent message switchboard and history",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -115,7 +115,7 @@ export const QUICK_ACTIONS_REGISTRY: QuickAction[] = [
|
|||||||
id: "journals",
|
id: "journals",
|
||||||
label: "Journals",
|
label: "Journals",
|
||||||
icon: BookOpen,
|
icon: BookOpen,
|
||||||
href: "/journals",
|
href: "/agents?tab=journals",
|
||||||
tip: "Per-agent reflections and learnings",
|
tip: "Per-agent reflections and learnings",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Suspense, useCallback, useEffect, useState } from "react";
|
||||||
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
|
import { useAgents } from "@/hooks/use-agents";
|
||||||
|
import { JournalEntryType } from "@/types";
|
||||||
|
import { AgentList } from "@/components/journals/agent-list";
|
||||||
|
import { JournalView } from "@/components/journals/journal-view";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
import { usePageRefresh } from "@/hooks";
|
||||||
|
import { BookOpen, Search } from "lucide-react";
|
||||||
|
|
||||||
|
const JOURNALS_STATE_KEY = "roboco-journals-state";
|
||||||
|
|
||||||
|
interface JournalsState {
|
||||||
|
agent: string | null;
|
||||||
|
q: string | null;
|
||||||
|
type: string | null;
|
||||||
|
task: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveJournalsState(state: JournalsState) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(JOURNALS_STATE_KEY, JSON.stringify(state));
|
||||||
|
} catch {
|
||||||
|
// Ignore localStorage errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadJournalsState(): JournalsState | null {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(JOURNALS_STATE_KEY);
|
||||||
|
return stored ? JSON.parse(stored) : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Journals tab content — extracted from the standalone /journals page so it
|
||||||
|
* can live inside the Agents hub tab shell (see agents/page.tsx). Its
|
||||||
|
* `agent`/`type`/`task` params keep working on the /agents route; every
|
||||||
|
* writer below targets /agents (not /journals) preserving the rest of the
|
||||||
|
* query string (e.g. `tab=journals`), copying the A2AView idiom. The agent
|
||||||
|
* search box is local state, not a URL param — a per-keystroke `router.push`
|
||||||
|
* would bounce the scroll position on every character. */
|
||||||
|
function JournalsViewContent() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// Read state from URL params
|
||||||
|
const urlAgentId = searchParams.get("agent");
|
||||||
|
const urlType = searchParams.get("type");
|
||||||
|
const urlTask = searchParams.get("task");
|
||||||
|
|
||||||
|
// Local-only: not URL-synced (see file header), lazily seeded from the last
|
||||||
|
// saved search so a fresh /agents?tab=journals visit isn't blank.
|
||||||
|
const [agentSearch, setAgentSearch] = useState(
|
||||||
|
() => loadJournalsState()?.q ?? "",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore from localStorage if URL has no params (fresh navigation)
|
||||||
|
useEffect(() => {
|
||||||
|
const hasUrlParams = urlAgentId || urlType || urlTask;
|
||||||
|
if (!hasUrlParams) {
|
||||||
|
const saved = loadJournalsState();
|
||||||
|
if (saved?.agent) {
|
||||||
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
|
params.set("agent", saved.agent);
|
||||||
|
if (saved.type) params.set("type", saved.type);
|
||||||
|
if (saved.task) params.set("task", saved.task);
|
||||||
|
router.replace(`/agents?${params.toString()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []); // Intentionally only run on mount
|
||||||
|
|
||||||
|
// Derive state from URL
|
||||||
|
const selectedAgentId = urlAgentId;
|
||||||
|
const typeFilter = (urlType as JournalEntryType) || "all";
|
||||||
|
const taskFilter = urlTask;
|
||||||
|
|
||||||
|
const { data: agents, isLoading: loadingAgents, refetch } = useAgents();
|
||||||
|
|
||||||
|
const { register, unregister } = usePageRefresh();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cb = () => {
|
||||||
|
void refetch();
|
||||||
|
};
|
||||||
|
register(cb);
|
||||||
|
return () => unregister(cb);
|
||||||
|
}, [register, unregister, refetch]);
|
||||||
|
|
||||||
|
// Save state to localStorage whenever the URL params or search change
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedAgentId) {
|
||||||
|
saveJournalsState({
|
||||||
|
agent: selectedAgentId,
|
||||||
|
q: agentSearch || null,
|
||||||
|
type: urlType, // Use raw URL param (null if "all")
|
||||||
|
task: taskFilter,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [selectedAgentId, agentSearch, urlType, taskFilter]);
|
||||||
|
|
||||||
|
// Update URL params — copies the current query string first so `tab` (and
|
||||||
|
// any other param) survives the round trip to /agents.
|
||||||
|
const updateParams = useCallback(
|
||||||
|
(updates: Record<string, string | null>) => {
|
||||||
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
|
Object.entries(updates).forEach(([key, value]) => {
|
||||||
|
if (value) {
|
||||||
|
params.set(key, value);
|
||||||
|
} else {
|
||||||
|
params.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const query = params.toString();
|
||||||
|
router.push(query ? `/agents?${query}` : "/agents");
|
||||||
|
},
|
||||||
|
[router, searchParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectAgent = useCallback(
|
||||||
|
(agentId: string | null) => {
|
||||||
|
// Only reset filters when changing to a different agent
|
||||||
|
if (agentId !== selectedAgentId) {
|
||||||
|
updateParams({ agent: agentId, type: null, task: null });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[updateParams, selectedAgentId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTypeChange = useCallback(
|
||||||
|
(value: JournalEntryType | "all") => {
|
||||||
|
updateParams({ type: value === "all" ? null : value });
|
||||||
|
},
|
||||||
|
[updateParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTaskChange = useCallback(
|
||||||
|
(value: string | null) => {
|
||||||
|
updateParams({ task: value });
|
||||||
|
},
|
||||||
|
[updateParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter agents by search
|
||||||
|
const filteredAgents = (agents ?? []).filter((agent) => {
|
||||||
|
if (!agentSearch) return true;
|
||||||
|
const query = agentSearch.toLowerCase();
|
||||||
|
return (
|
||||||
|
agent.agent_id.toLowerCase().includes(query) ||
|
||||||
|
agent.role.toLowerCase().includes(query) ||
|
||||||
|
agent.team?.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get selected agent
|
||||||
|
const selectedAgent = agents?.find((a) => a.agent_id === selectedAgentId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// h-[calc(100dvh-7rem)]: the tab shell's TabsList sits above this content
|
||||||
|
// (unlike the old standalone /journals page), so a plain h-full has no
|
||||||
|
// definite height to fill — same fixed-height idiom as A2AView.
|
||||||
|
<div className="flex h-[calc(100dvh-7rem)] flex-col gap-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between shrink-0">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Agent Journals</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
View agent reflections, learnings, and decisions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content — one screen; the agent list and the detail each scroll inside */}
|
||||||
|
<div className="grid grid-cols-12 gap-6 flex-1 min-h-0">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div className="col-span-12 lg:col-span-3 min-h-0">
|
||||||
|
<Card className="h-full flex flex-col">
|
||||||
|
<CardContent className="p-3 flex flex-1 flex-col min-h-0">
|
||||||
|
{/* Agent Search */}
|
||||||
|
<HelpTip label="Filters the agent list below by ID, role, or team">
|
||||||
|
<div className="relative mb-3">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
value={agentSearch}
|
||||||
|
onChange={(e) => setAgentSearch(e.target.value)}
|
||||||
|
placeholder="Search agents..."
|
||||||
|
className="pl-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</HelpTip>
|
||||||
|
|
||||||
|
{/* Agent List */}
|
||||||
|
<AgentList
|
||||||
|
agents={filteredAgents}
|
||||||
|
isLoading={loadingAgents}
|
||||||
|
selectedAgentId={selectedAgentId}
|
||||||
|
onSelectAgent={handleSelectAgent}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Journal Content */}
|
||||||
|
<div className="col-span-12 lg:col-span-9 min-h-0">
|
||||||
|
<Card className="h-full flex flex-col">
|
||||||
|
<CardContent className="p-6 flex-1 min-h-0 overflow-hidden">
|
||||||
|
{selectedAgent ? (
|
||||||
|
<JournalView
|
||||||
|
agent={selectedAgent}
|
||||||
|
typeFilter={typeFilter as JournalEntryType | "all"}
|
||||||
|
onTypeChange={handleTypeChange}
|
||||||
|
taskFilter={taskFilter}
|
||||||
|
onTaskChange={handleTaskChange}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-16 text-muted-foreground">
|
||||||
|
<BookOpen className="h-16 w-16 mx-auto mb-4 opacity-50" />
|
||||||
|
<h3 className="text-lg font-medium mb-2">Select an Agent</h3>
|
||||||
|
<p className="text-sm">
|
||||||
|
Choose an agent from the list to view their journal entries
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap in Suspense for useSearchParams
|
||||||
|
export function JournalsView() {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Skeleton className="h-9 w-48 mb-2" />
|
||||||
|
<Skeleton className="h-5 w-64" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-12 gap-6">
|
||||||
|
<div className="col-span-12 lg:col-span-3">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-3 space-y-2">
|
||||||
|
<Skeleton className="h-10 w-full" />
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-12 w-full" />
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-12 lg:col-span-9">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<Skeleton className="h-64 w-full" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<JournalsViewContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
Bot,
|
Bot,
|
||||||
Shield,
|
Shield,
|
||||||
BookOpen,
|
|
||||||
Briefcase,
|
Briefcase,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Database,
|
Database,
|
||||||
@@ -89,13 +88,7 @@ export const navItems = [
|
|||||||
title: "Agents",
|
title: "Agents",
|
||||||
href: "/agents",
|
href: "/agents",
|
||||||
icon: Bot,
|
icon: Bot,
|
||||||
tip: "Every agent's live state, spawn controls, and A2A conversations",
|
tip: "Every agent's live state, spawn controls, A2A conversations, and journals",
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Journals",
|
|
||||||
href: "/journals",
|
|
||||||
icon: BookOpen,
|
|
||||||
tip: "Per-agent reflections and learnings",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Auditor",
|
title: "Auditor",
|
||||||
|
|||||||
Reference in New Issue
Block a user