Tenant-scoped cross-project activity feed displayed on the Studio page. Shows the 10 most recent activities across all of the current user’s projects, with real-time updates pushed via SSE.
- The feed is tenant-scoped — the user sees activities only from projects that belong to their tenant.
- Initial snapshot is fetched via REST on Studio page mount; real-time updates arrive over SSE without polling.
- The Pinia store (
studioActivity) caps the in-memory list at 50 entries — the oldest items are dropped as new SSE events arrive. - Each activity row is clickable and navigates the user to the Workspace of the corresponding project.
- This UC is the tenant-level counterpart to the per-project Workspace Activity Feed (UC-05001).
Relationship to UC-05001
| Dimension | UC-05001 — Workspace Activity | UC-06001 — Studio Recent Activity |
|---|---|---|
| Scope | Single project | All projects of current tenant |
| Endpoint | /api/v1/projects/{projectId}/activities | /api/v1/studio/recent-activity |
| SSE endpoint | /api/v1/projects/{projectId}/activities/stream | /api/v1/studio/recent-activity/stream |
| DTO | ActivityDto | StudioActivityDto (adds projectId, projectName) |
| Navigation on row click | stays in workspace | navigates to workspace of that project |
| Store cap | no cap documented | 50 entries |
Data Entity — StudioActivityDto
StudioActivityDto is an enriched version of ActivityDto that adds tenant-level context fields.
Location: talkide-be/src/main/kotlin/com/mddsummer/talkide/features/activity/api/dto/StudioActivityDto.kt
| Field | Type | Nullable | Description |
|---|---|---|---|
id | Long | NOT NULL | Activity primary key |
projectId | Long | NOT NULL | FK → projects(id); identifies which project this activity belongs to |
projectName | String | NOT NULL | Human-readable project name; resolved from Caffeine cache (projectId → tenantId + projectName) at publish time |
conversationId | Long | NOT NULL | FK → conversations(id) |
agentRole | String | NOT NULL | Technical agent key — same values as ActivityDto.agentRole (e.g. talkide-frontend-dev) |
eventType | String | NOT NULL | TASK_STARTED | TASK_COMPLETED | TOOL_USE | AGENT_MESSAGE |
parentActivityId | Long | NULL | FK → activities(id) self-reference; same semantics as ActivityDto |
parentActivityDescription | String | NULL | Description parent activity (pokud parentActivityId je set a parent existuje); null pokud activity nemá parent nebo parent neexistuje |
description | String | NULL | User-facing label; set for TASK_STARTED and TASK_COMPLETED |
toolName | String | NULL | Raw tool name (e.g. Read, Write, Bash); set only for TOOL_USE |
toolCategory | String (enum) | NULL | Business category: READING | EDITING | EXECUTING | DELEGATING | BROWSING | OTHER; set only for TOOL_USE; null for TASK_STARTED, TASK_COMPLETED, AGENT_MESSAGE |
toolSummary | String | NULL | Extracted tool summary or first 200 chars of agent message |
createdAt | String (ISO 8601) | NOT NULL | Timestamp when the activity was created |
payloadJson is intentionally excluded from the response (internal detail, same policy as UC-05001).
Tenant Isolation — Caffeine Cache
ActivityEventBus maintains a Caffeine cache keyed by projectId mapping to (tenantId, projectName).
On dual-publish (project-level + tenant-level), the publisher looks up the cache to enrich the event with
projectName. Requests to /api/v1/studio/recent-activity filter by the tenantId extracted from the JWT.
REST — Initial Snapshot
sequenceDiagram
actor User
User->>+FE: opens Studio page
FE->>+BE: GET /api/v1/studio/recent-activity?limit=10 <br> Authorization: Bearer {accessToken}
BE->>BE: validate access token
alt token missing or invalid
BE-->>FE: 401 Unauthorized <br> ErrorResponse
end
BE->>DB: query activities WHERE tenant_id = ? ORDER BY created_at DESC LIMIT ?
BE->>-FE: 200 OK <br> StudioActivityDto[]
FE->>-User: render initial activity list (newest first)
GET /api/v1/studio/recent-activity?limit=10
Query parameters:
| Parameter | Required | Default | Min | Max | Description |
|---|---|---|---|---|---|
limit | no | 10 | 1 | 100 | Number of activities to return |
200 OK StudioActivityDto[]:
[
{
"id": 12346,
"projectId": 7,
"projectName": "My SaaS App",
"conversationId": 42,
"agentRole": "talkide-frontend-dev",
"eventType": "TASK_COMPLETED",
"parentActivityId": null,
"parentActivityDescription": null,
"description": "Pozadí aplikace",
"toolName": null,
"toolCategory": null,
"toolSummary": null,
"createdAt": "2026-04-30T11:24:30Z"
},
{
"id": 12345,
"projectId": 7,
"projectName": "My SaaS App",
"conversationId": 42,
"agentRole": "talkide-frontend-dev",
"eventType": "TASK_STARTED",
"parentActivityId": null,
"parentActivityDescription": null,
"description": "Pozadí aplikace",
"toolName": null,
"toolCategory": null,
"toolSummary": null,
"createdAt": "2026-04-30T11:24:00Z"
}
]
401 Unauthorized (missing or invalid access token) ErrorResponse:
{
"status": 401,
"code": "AUTHENTICATION_FAILED",
"message": "Access token is missing or invalid"
}
SSE — Live Stream
sequenceDiagram
actor User
User->>+FE: Studio page mounted
FE->>+BE: GET /api/v1/studio/recent-activity/stream <br> Authorization: Bearer {accessToken}
BE->>BE: validate access token
alt token missing or invalid
BE-->>FE: 401 Unauthorized <br> ErrorResponse
end
BE-->>FE: SSE event: connected
loop any project in tenant emits a new activity
BE->>BE: ActivityEventBus publishes to tenant-level channel
BE-->>FE: SSE event: activity <br> StudioActivityDto
end
loop every 30 seconds
BE-->>FE: SSE event: heartbeat
end
FE->>-User: activity list updated in real time (prepend, cap 50)
GET /api/v1/studio/recent-activity/stream
Authorization: Bearer token in Authorization header (native EventSource does not support
custom headers — FE uses a fetch-based SSE parser). Response content type: text/event-stream.
For the full SSE event-name convention and reconnect strategy, see architecture.md — SSE Convention.
SSE event: connected — stream established:
{}
SSE event: activity — a new activity was published to the tenant channel:
{
"id": 12347,
"projectId": 3,
"projectName": "E-commerce Backend",
"conversationId": 17,
"agentRole": "talkide-backend-dev",
"eventType": "TASK_STARTED",
"parentActivityId": null,
"parentActivityDescription": null,
"description": "Implementace platební brány",
"toolName": null,
"toolCategory": null,
"toolSummary": null,
"createdAt": "2026-04-30T12:00:00Z"
}
SSE event: heartbeat — keep-alive pulse every 30 s:
{}
401 Unauthorized (missing or invalid access token) ErrorResponse:
{
"status": 401,
"code": "AUTHENTICATION_FAILED",
"message": "Access token is missing or invalid"
}
UX Notes
- Row click navigates to the Workspace of the activity’s project (uses
projectId). - Project name (
projectName) is displayed alongside the agent role and description so the user can distinguish which project the activity belongs to. - Server-side deduplication: the REST snapshot (
GET /api/v1/studio/recent-activity) returns a BE pre-deduped list — consecutive activities sharing the same(tool_category, agent_role, parent_activity_id)key are coalesced by the repository query (window functionLAG()/ “gaps and islands” pattern). FE does not perform any own dedup logic; it is a pure renderer. - Tool call rendering: pro tool call activity (
TOOL_USEeventType sparentActivityId) FE zobrazuje text formátu"{toolCategory} pro {parentActivityDescription}", kdetoolCategoryje lokalizovaný název kategorie nástroje zToolCategoryenumu (např. “Reading files” proREADING). PokudparentActivityDescriptionje null, fallback natoolSummary. Pokud i to chybí, zobrazí se jen kategorie bez ” pro …”. - Store cap: the
studioActivityPinia store prepends incoming SSEactivityevents and trims the list to 50 entries. The REST snapshot replaces the list on mount. - No polling: the SSE stream keeps the feed live. Reconnect uses exponential backoff (see architecture.md).
- The Studio page connects the SSE stream on mount and disconnects on unmount.
Time-of-day Greeting
The Studio page displays a personalized greeting in the hero/header area. The greeting is computed entirely on the frontend — no backend call is needed.
Logic
The greeting phrase is selected based on new Date().getHours() at the time the Studio page renders:
| Hour range | CS phrase | EN phrase |
|---|---|---|
| 5:00 – 10:59 | Dobré ráno, {salutation} | Good morning, {salutation} |
| 11:00 – 12:59 | Dobrý den, {salutation} | Hi, {salutation} |
| 13:00 – 17:59 | Dobré odpoledne, {salutation} | Good afternoon, {salutation} |
| 18:00 – 4:59 | Dobrý večer, {salutation} | Good evening, {salutation} |
Name substitution
{salutation}is sourced fromuserProfile.salutation(loaded viaGET /api/v1/users/me).- If
salutationisnullor empty string, the greeting is rendered without a name (fallback variant — see i18n keys below).
i18n keys
Namespace: studio.greeting — 4 time-of-day keys × 2 variants (with name / without name) = 8 keys:
| Key | Value (CS) | Value (EN) |
|---|---|---|
studio.greeting.morning | Dobré ráno, {name}! | Good morning, {name}! |
studio.greeting.morning_no_name | Dobré ráno! | Good morning! |
studio.greeting.midday | Dobrý den, {name}! | Hi, {name}! |
studio.greeting.midday_no_name | Dobrý den! | Hi! |
studio.greeting.afternoon | Dobré odpoledne, {name}! | Good afternoon, {name}! |
studio.greeting.afternoon_no_name | Dobré odpoledne! | Good afternoon! |
studio.greeting.evening | Dobrý večer, {name}! | Good evening, {name}! |
studio.greeting.evening_no_name | Dobrý večer! | Good evening! |
Frontend rendering notes
- The greeting is a pure computed property / composable — no watcher, no API call.
- Re-evaluates when
userProfileis loaded (or reloaded after profile update). - The greeting does not automatically update while the user is on the Studio page (no timer); it reflects the hour at the time of page load/navigation.
Frontend
Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
limit | (optional) | 1 – 100 | — | Defaults to 10; clamped to 100 max |
Backend
Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
limit | min=1, max=100 | — | — | Default 10; extracted from tenantId in JWT — no path variable needed |
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
| Authenticated user, tenant has 2 projects each with 15 activities | GET /api/v1/studio/recent-activity?limit=10 is called | 200 OK with 10 most recent activities across both projects, ordered by created_at DESC |
| Authenticated user, tenant has no activities | GET /api/v1/studio/recent-activity is called | 200 OK with empty array |
| limit=5 provided | GET /api/v1/studio/recent-activity?limit=5 is called | 200 OK with at most 5 activities |
| limit=200 (exceeds max) | GET /api/v1/studio/recent-activity?limit=200 is called | 400 Bad Request with validation error |
| No Authorization header | GET /api/v1/studio/recent-activity is called | 401 AUTHENTICATION_FAILED error response |
| Authenticated user, SSE stream open, any project in tenant emits a new activity | ActivityEventBus publishes to tenant channel | SSE event activity with StudioActivityDto (including projectName) is pushed to connected client |
| No Authorization header | GET /api/v1/studio/recent-activity/stream is called | 401 AUTHENTICATION_FAILED error response |
| Activities from another tenant exist in DB | GET /api/v1/studio/recent-activity is called | Cross-tenant activities are NOT included in the response |
Thanks for the feedback.