From 3f60f7f665393c55b4f35622592702693f8cab2b Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Wed, 12 Aug 2026 08:58:04 +0100 Subject: [PATCH] Link learning trajectories to their conversation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trajectory detail panel showed the conversation id as plain text, so reviewing an example meant copying the room id and hunting for it in the inbox — with the evidence panel open on one screen and the conversation on another. Render it as a link instead. The id has to travel as an owned id (accountId~conversationId): once account runtimes exist, findConversation rejects a bare room id outright, so a plain link would 404 for every multi-account install. Co-Authored-By: Claude Opus 5 --- .../src/features/learning/learning-page.tsx | 16 +++++++++++++++- .../src/features/learning/learning.css | 1 + tests/dashboard/learning.test.tsx | 18 +++++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/src/features/learning/learning-page.tsx b/apps/dashboard/src/features/learning/learning-page.tsx index 4c4f35c..f6e5b47 100644 --- a/apps/dashboard/src/features/learning/learning-page.tsx +++ b/apps/dashboard/src/features/learning/learning-page.tsx @@ -1,5 +1,7 @@ import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { Link } from 'react-router-dom'; +import { createOwnedConversationId } from '../conversations/conversation-identity.js'; import { learningApi } from './learning-api.js'; import './learning.css'; @@ -13,7 +15,19 @@ function TrajectoryDetail({ id }: { id: string }) { return (
-
Conversation
{trajectory.conversationId}
+
+
Conversation
+
+ {/* The conversations route resolves owned ids only, so the account + that produced the trajectory has to travel with the room id. */} + + {trajectory.conversationId} + +
+
Policy mode
{trajectory.policyMode ?? 'Unknown'}
Outcome
{trajectory.outcomeType.replaceAll('-', ' ')}
Matures
{new Date(trajectory.maturityAt).toLocaleString()}
diff --git a/apps/dashboard/src/features/learning/learning.css b/apps/dashboard/src/features/learning/learning.css index b6f4e07..d44ba1d 100644 --- a/apps/dashboard/src/features/learning/learning.css +++ b/apps/dashboard/src/features/learning/learning.css @@ -14,6 +14,7 @@ .trajectory-facts { display: flex; flex-wrap: wrap; gap: .35rem 1.4rem; margin: 0; } .trajectory-facts dt { color: var(--muted); font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; } .trajectory-facts dd { margin: 0; font-size: .85rem; word-break: break-all; } +.trajectory-conversation-link { color: var(--accent); text-decoration: underline; text-underline-offset: .15em; } .decision-list { display: grid; gap: .75rem; list-style: none; margin: 0; padding: 0; } .decision-card { border: 1px solid var(--border); border-radius: .6rem; display: grid; gap: .5rem; padding: .7rem .8rem; } .decision-heading { align-items: center; display: flex; flex-wrap: wrap; gap: .5rem; } diff --git a/tests/dashboard/learning.test.tsx b/tests/dashboard/learning.test.tsx index f587cb7..8cf063a 100644 --- a/tests/dashboard/learning.test.tsx +++ b/tests/dashboard/learning.test.tsx @@ -6,6 +6,7 @@ import { cleanup, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { HttpResponse, http } from 'msw'; import { setupServer } from 'msw/node'; +import { MemoryRouter } from 'react-router-dom'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { LearningPage } from '../../apps/dashboard/src/features/learning/learning-page.js'; @@ -62,7 +63,9 @@ function renderLearning() { }); return render( - + + + , ); } @@ -96,6 +99,19 @@ describe('learning page trajectory detail', () => { await waitFor(() => expect(detailCalls).toEqual([TRAJECTORY_ID])); }); + it('links the evidence to the owning account\'s conversation', async () => { + renderLearning(); + await screen.findByText(/human created success/i); + await userEvent.click(screen.getByRole('button', { name: /show conversation evidence/i })); + + const link = await screen.findByRole('link', { name: '!room:reddit.com' }); + // The account id has to survive in the owned id or the route 404s. + expect(link).toHaveAttribute( + 'href', + `/conversations/${encodeURIComponent('%40t2_learner%3Areddit.com~!room%3Areddit.com')}`, + ); + }); + it('collapses the evidence again on a second click', async () => { renderLearning(); await screen.findByText(/human created success/i);