Project-scoped activity feed exposing real-time subagent events parsed from the Claude Code CLI NDJSON output. Only authenticated users with access to the project can subscribe. Events are persisted in the activities table and pushed via SSE.
- The activity feed is project-scoped, not conversation-scoped — the SSE stream persists across conversation switches in the workspace UI.
- NDJSON lines emitted by the CLI are mapped to activity rows according to four event types:
TASK_STARTED,TOOL_USE,AGENT_MESSAGE,TASK_COMPLETED. - An in-memory map
Map<String toolUseId, Long activityId>is maintained per conversation for the lifetime of the CLI process. Events whoseparent_tool_use_idis not found in the map are logged as WARNING and skipped. - Top-level PM text messages (
type=assistant,parent_tool_use_id=null) are not written toactivities— they belong to the conversation messages stream (UC-04003). - Historical activities can be retrieved via REST pagination using the
GET /activitiesendpoint with cursor-basedafterIdparameter. - The SSE stream pushes only new events as they arrive; it does not replay history.
- System SSE events
connectedandheartbeat(every 30 s) keep the connection alive.
Agent Role Mapping
agent_role (technical key) | Name | Label |
|---|---|---|
talkide-pm | Mara | Product Manager |
talkide-analyst | Iris | Analyst |
talkide-backend-dev | Theo | Backend Engineer |
talkide-frontend-dev | Eli | Frontend Engineer |
talkide-reviewer | Nia | Reviewer |
talkide-devops | Kai | DevOps |
Database — activities Table
<!-- changes/0008-create-activities-table.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="0008-create-activities-table" author="webapp-creator">
<createTable tableName="activities">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="project_id" type="bigint">
<constraints nullable="false" foreignKeyName="fk_activities_project" references="projects(id)"/>
</column>
<column name="conversation_id" type="bigint">
<constraints nullable="false" foreignKeyName="fk_activities_conversation" references="conversations(id)"/>
</column>
<column name="agent_role" type="varchar(64)">
<constraints nullable="false"/>
</column>
<column name="event_type" type="varchar(32)">
<constraints nullable="false"/>
</column>
<column name="parent_activity_id" type="bigint">
<constraints nullable="true" foreignKeyName="fk_activities_parent" references="activities(id)"/>
</column>
<column name="description" type="varchar(255)">
<constraints nullable="true"/>
</column>
<column name="tool_name" type="varchar(64)">
<constraints nullable="true"/>
</column>
<column name="tool_summary" type="text">
<constraints nullable="true"/>
</column>
<column name="payload_json" type="jsonb">
<constraints nullable="true"/>
</column>
<column name="created_at" type="timestamptz" defaultValueComputed="now()">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex tableName="activities" indexName="idx_activities_project_created_at">
<column name="project_id"/>
<column name="created_at" descending="true"/>
</createIndex>
<createIndex tableName="activities" indexName="idx_activities_project_conversation_created_at">
<column name="project_id"/>
<column name="conversation_id"/>
<column name="created_at" descending="true"/>
</createIndex>
</changeSet>
</databaseChangeLog>
Columns:
| Column | Type | Nullable | Description |
|---|---|---|---|
id | BIGINT PK | NOT NULL | Auto-increment primary key |
project_id | BIGINT | NOT NULL | FK → projects(id) |
conversation_id | BIGINT | NOT NULL | FK → conversations(id) |
agent_role | VARCHAR(64) | NOT NULL | talkide-pm | talkide-analyst | talkide-backend-dev | talkide-frontend-dev | talkide-reviewer | talkide-devops |
event_type | VARCHAR(32) | NOT NULL | TASK_STARTED | TASK_COMPLETED | TOOL_USE | AGENT_MESSAGE |
parent_activity_id | BIGINT | NULL | FK → activities(id) self-reference; nests TOOL_USE and AGENT_MESSAGE under a TASK_STARTED |
description | VARCHAR(255) | NULL | User-facing 3–5 word label; set only for TASK_STARTED (and inherited by TASK_COMPLETED from the parent TASK_STARTED) |
tool_category | VARCHAR(32) | NULL | Business category enum: READING | EDITING | EXECUTING | DELEGATING | BROWSING | OTHER — set only for TOOL_USE; NULL for TASK_STARTED, TASK_COMPLETED, AGENT_MESSAGE |
tool_name | VARCHAR(64) | NULL | Read, Write, Edit, Bash, … — set only for TOOL_USE |
tool_summary | TEXT | NULL | Extracted summary of tool input (Read/Write/Edit → file_path; Bash → command; AGENT_MESSAGE → first 200 chars of text) |
payload_json | JSONB | NULL | Raw tool input JSON or full AGENT_MESSAGE text; for detail/expand in future UC |
created_at | TIMESTAMPTZ | NOT NULL | Defaults to now() |
NDJSON → Activity Parsing Rules
Rules applied by ClaudeCliService when processing each NDJSON line:
| NDJSON event | Detection | Action |
|---|---|---|
Top-level tool_use with name == "Agent" (or "Task" as fallback) | type == "assistant" && content contains tool_use && tool.name == "Agent" (primary) or "Task" (fallback) && parent_tool_use_id == null | INSERT activity: event_type=TASK_STARTED, agent_role=tool.input.subagent_type (after stripping plugin: prefix — see note below), description=tool.input.description, payload_json=tool.input. Store mapping tool_use_id → activity.id in in-memory map. |
Nested tool_use (any name) | type == "assistant" && parent_tool_use_id != null && content contains tool_use | Lookup parent_tool_use_id in in-memory map → get parent activity and its agent_role. INSERT activity: event_type=TOOL_USE, agent_role=parent.agent_role, parent_activity_id=parent.id, tool_name=tool.name, tool_summary extracted from tool.input (Read/Write/Edit → file_path; Bash → command; otherwise first 100 chars of JSON), payload_json=tool.input. |
Nested assistant text | type == "assistant" && parent_tool_use_id != null && content contains text block | Lookup parent in map. INSERT activity: event_type=AGENT_MESSAGE, agent_role=parent.agent_role, parent_activity_id=parent.id, tool_summary=text.substring(0, 200), payload_json={"text": full_text}. |
Top-level tool_result matching a Task — sync | type == "user" && contains tool_result whose tool_use_id is found in in-memory map && tool_use_result.isAsync != true | Lookup tool_use_id → find parent TASK_STARTED activity. INSERT activity: event_type=TASK_COMPLETED, agent_role=parent.agent_role, description=parent.description (inherited), payload_json={"result": tool_result.content}. |
Top-level tool_result matching a Task — async (run_in_background: true) | type == "user" && contains tool_result whose tool_use_id is found in in-memory map && tool_use_result.isAsync == true | Detected as async launch — do not emit TASK_COMPLETED yet. Store the tool_use_id as pending-async in the in-memory map. Wait for the matching system event. |
system event with subtype: task_notification | type == "system" && subtype == "task_notification" && status == "completed" && tool_use_id found in pending-async map | Lookup tool_use_id → find parent TASK_STARTED activity. INSERT activity: event_type=TASK_COMPLETED, agent_role=parent.agent_role, description=parent.description (inherited), payload_json contains whitelisted fields only: task_id, status, summary, output_file, tool_use_id. |
system event with subtype: task_progress | type == "system" && subtype == "task_progress" && tool_use_id found in in-memory map | NDJSON shape: {"type":"system","subtype":"task_progress","tool_use_id":"toolu_…","last_tool_name":"Read","description":"…","status":"running"}. Lookup tool_use_id → get parent TASK_STARTED activity. INSERT activity: event_type=TOOL_USE, agent_role=parent.agent_role, parent_activity_id=parent.id, tool_name=last_tool_name, description=description. Dedup: a new TOOL_USE record is created only when last_tool_name changes from the last seen value for the same tool_use_id. Repeated Read events do not produce duplicates. |
Top-level assistant text (PM message) | type == "assistant" && parent_tool_use_id == null && text content | Do not write to activities — handled exclusively by the messages stream (UC-04003). |
Note — real Claude CLI format: In stream-json output from Claude CLI, the delegation tool name is
"Agent"(not"Task"). The"Task"variant is kept as a fallback for older builds. Additionally,subagent_typecarries a"plugin:"prefix (e.g."plugin:talkide-analyst"). The parser strips this prefix withremovePrefix("plugin:")before resolvingAgentRole, so the storedagent_rolevalue is always the clean key (e.g."talkide-analyst").tool_use IDs are NOT UUIDs. Claude CLI emits Anthropic-format strings such as
toolu_015oxKeDDmwYFYuTPoRVL8FG— never UUID-formatted IDs. The parser accepts any non-blank string as a validtool_use_idand is intentionally prefix-agnostic (if Anthropic changes fromtoolu_to another prefix, the parser keeps working without changes).Async task completion: When an Agent tool is invoked with
run_in_background: true, the immediatetool_resultcarriestool_use_result.isAsync == true. The parser detects this flag and defers TASK_COMPLETED — no activity row is written at that point. The actual completion is read from the subsequentsystemevent withsubtype: task_notificationandstatus: completed, matched back to the original delegation bytool_use_id. Thepayload_jsonstored for thetask_notificationevent is narrowed to a whitelist (task_id,status,summary,output_file,tool_use_id) to avoid persisting Anthropic-added fields that may appear in future CLI versions.
In-memory map lifecycle: Map<String toolUseId, Long activityId> is held in memory per conversation for the lifetime of the CLI process. It is lost on BE restart. Nested events whose parent_tool_use_id is not found in the map are logged as WARNING and discarded.
REST — Paginated History
sequenceDiagram
actor User
User->>+FE: opens workspace activity panel
FE->>+BE: GET /api/v1/projects/{projectId}/activities?limit=100 <br> Authorization: Bearer {accessToken}
BE->>BE: validate access token
alt token missing or invalid
BE-->>FE: 401 Unauthorized <br> ErrorResponse
end
BE->>DB: load project by projectId
alt project not found
BE-->>FE: 404 Not Found <br> ErrorResponse
end
BE->>BE: check project belongs to user's tenant
alt project does not belong to tenant
BE-->>FE: 403 Forbidden <br> ErrorResponse
end
BE->>DB: query activities WHERE project_id = ? ORDER BY id DESC LIMIT ?
BE->>-FE: 200 OK <br> ActivityResponse[]
FE->>-User: render activity feed history
GET /api/v1/projects/{projectId}/activities?limit=100&afterId=12345
Query parameters:
limit(optional, default 100, min 1, max 500) — number of rows to returnafterId(optional) — cursor pagination; returns only activities withid < afterId, ordered byid DESC
200 OK ActivityResponse[]:
[
{
"id": 12346,
"projectId": 7,
"conversationId": 42,
"agentRole": "talkide-frontend-dev",
"eventType": "TASK_COMPLETED",
"parentActivityId": null,
"description": "Pozadí aplikace",
"toolName": null,
"toolCategory": null,
"toolSummary": null,
"createdAt": "2026-04-30T11:24:30Z"
},
{
"id": 12345,
"projectId": 7,
"conversationId": 42,
"agentRole": "talkide-frontend-dev",
"eventType": "TASK_STARTED",
"parentActivityId": null,
"description": "Pozadí aplikace",
"toolName": null,
"toolCategory": null,
"toolSummary": null,
"createdAt": "2026-04-30T11:24:00Z"
}
]
payloadJsonis intentionally excluded from the REST/SSE response — it is an internal detail. A future UC may expose an activity detail endpoint.
SSE — Live Stream
sequenceDiagram
actor User
User->>+FE: workspace view opened (project loaded)
FE->>+BE: GET /api/v1/projects/{projectId}/activities/stream <br> Authorization: Bearer {accessToken}
BE->>BE: validate access token
alt token missing or invalid
BE-->>FE: 401 Unauthorized <br> ErrorResponse
end
BE->>DB: load project by projectId
alt project not found
BE-->>FE: 404 Not Found <br> ErrorResponse
end
BE->>BE: check project belongs to user's tenant
alt project does not belong to tenant
BE-->>FE: 403 Forbidden <br> ErrorResponse
end
BE-->>FE: SSE event: connected
loop CLI outputs NDJSON lines (any conversation in project)
BE->>BE: parse NDJSON line per parsing rules above
alt TASK_STARTED detected
BE->>DB: insert activity (event_type=TASK_STARTED)
BE-->>FE: SSE event: activity <br> ActivityResponse
else TOOL_USE detected
BE->>DB: insert activity (event_type=TOOL_USE)
BE-->>FE: SSE event: activity <br> ActivityResponse
else AGENT_MESSAGE detected
BE->>DB: insert activity (event_type=AGENT_MESSAGE)
BE-->>FE: SSE event: activity <br> ActivityResponse
else TASK_COMPLETED detected
BE->>DB: insert activity (event_type=TASK_COMPLETED)
BE-->>FE: SSE event: activity <br> ActivityResponse
end
end
loop every 30 seconds
BE-->>FE: SSE event: heartbeat
end
FE->>-User: activity feed updated in real time
GET /api/v1/projects/{projectId}/activities/stream
Authorization: Bearer token. Response is text/event-stream (SSE). The stream is project-scoped and survives conversation switches.
SSE event: connected — stream established:
{}
SSE event: activity — a new activity row was inserted:
{
"id": 12345,
"projectId": 7,
"conversationId": 42,
"agentRole": "talkide-frontend-dev",
"eventType": "TASK_STARTED",
"parentActivityId": null,
"description": "Pozadí aplikace",
"toolName": null,
"toolCategory": null,
"toolSummary": null,
"createdAt": "2026-04-30T11:24:00Z"
}
SSE event: heartbeat — keep-alive pulse every 30 s:
{}
For the full SSE event-name convention (connected, activity, heartbeat), auth requirements,
and reconnect strategy, see architecture.md — SSE Convention.
401 Unauthorized (missing or invalid access token) ErrorResponse:
{
"status": 401,
"code": "AUTHENTICATION_FAILED",
"message": "Access token is missing or invalid"
}
403 Forbidden (project does not belong to user’s tenant) ErrorResponse:
{
"status": 403,
"code": "FORBIDDEN",
"message": "You do not have access to this project"
}
404 Not Found (project not found) ErrorResponse:
{
"status": 404,
"code": "NOT_FOUND_PROJECT",
"message": "Project not found"
}
UX Guidelines
Context
Activity Panel je pravý sloupec workspace — tab “Activity” v přepínači Preview / Activity / Files (viz WorkspaceScreen.vue). Feed je project-scoped a přežívá přepnutí konverzace. Panel je po refactoru ActivityPanel.vue řízen reálnými daty z REST + SSE; momentálně obsahuje 7 hardcoded záznamů, které budou nahrazeny.
User Flow
- Uživatel otevře workspace — FE načte historii (
GET /activities?limit=100) a zobrazí skeleton. - FE současně otevře SSE stream (
GET /activities/stream). - Panel zobrazí historické záznamy (nejnovější nahoře).
- Nové SSE eventy se připínají nahoru s animací.
- Uživatel vidí
TASK_STARTEDaTASK_COMPLETED—TOOL_USEaAGENT_MESSAGEjsou skryté. - Uživatel klikne na ”▶” vedle
TASK_STARTED— řádek se rozbalí a zobrazí vnořené eventy. - Uživatel klikne znovu (nebo na ”▼”) — sbalí se zpět.
- (Volitelně) Uživatel klikne na chip agenta v team stripu → filtr feedu na daného agenta.
Layout
┌─────────────────────────────────────────────────────┐
│ LIVE ACTIVITY · PROJECT-SCOPED [Vše ▾] │ ← header + (future: agent filter)
├─────────────────────────────────────────────────────┤
│ ● [Eli] začala · Pozadí aplikace 2 min ▶ │ ← TASK_STARTED, running dot
│ ┌── [Read] /src/App.vue 2 min │ │
│ ├── [Write] /src/main.ts 2 min │ │ expanded
│ └── [msg] "Upravuji soubor..." 2 min │ ↓
│ ✓ [Eli] dokončila · Pozadí aplikace 5 min │ ← TASK_COMPLETED, ztlumené
│ ● [Mara] začala · Plánuje aplikaci 10 min ▶ │ ← TASK_STARTED (Mara = amber dot)
├─────────────────────────────────────────────────────┤
│ (empty state nebo skeleton) │
└─────────────────────────────────────────────────────┘
- Screen type: live feed list (pravý panel workspace)
- Responsive: panel má fixní šířku danou layoutem workspace; vnitřně fluid
- Overflow:
overflow-y: auto— scrolluje uvnitř panelu; panel nezkresluje vnější layout - Padding:
p-6(konzistentní s aktuálnímActivityPanel.vue)
Feed Item — Default Layout (TASK_STARTED a TASK_COMPLETED)
Každý viditelný řádek (jeden div s flex items-center gap-3 px-3 py-2.5 rounded-lg):
[ status dot ] [ avatar 20px ] [ agent name ] [ akce ] [ description ] [ čas ] [ expand ▶ ]
| Slot | Popis |
|---|---|
| Status dot | 8px kruh vlevo od avataru; pulsující = TASK_STARTED (barva agenta), statický šedý = TASK_COMPLETED |
| Avatar | TAvatar 20px — barevná inicializace z agentColor (oklch hodnoty z team.ts → po refactoru z API agentRole) |
| Agent name | text-[13px] text-[var(--fg-2)] min-w-[80px] (konzistentní s aktuálním panelem) |
| Akce | text-[13px] text-[var(--fg-3)] — i18n klíč workspace.activityPanel.action.started / .action.completed |
| Description | font-mono text-xs text-[var(--fg-1)] flex-1 — description z API; truncate s ellipsis pokud > 40 znaků |
| Čas | font-mono text-[11px] text-[var(--fg-4)] min-w-[70px] text-right — relativní (2 min, před 1 h) |
| Expand toggle | Pouze u TASK_STARTED: ikona ▶ (ChevronRight 12px, text-[var(--fg-3)]); otočí se na ▼ při rozbalení |
Vizuální odlišení event types:
| Event type | Status dot | Opacity řádku | Ikona |
|---|---|---|---|
| TASK_STARTED | pulsující ● v barvě agenta (amber=Mara, indigo=Eli…) s box-shadow: 0 0 6px <agentColor> | 100 % | ▶ expand |
| TASK_COMPLETED | statický ● var(--fg-4) bez glow | 60 % (opacity-60) | ✓ (Check 12px, var(--green)) |
Agent colors (mapování agentRole → barva pro dot a avatar):
| agentRole | Barva |
|---|---|
talkide-pm (Mara) | amber oklch(0.78 0.15 70) |
talkide-analyst (Iris) | pink oklch(0.75 0.13 320) |
talkide-backend-dev (Theo) | indigo oklch(0.72 0.13 270) |
talkide-frontend-dev (Eli) | indigo oklch(0.72 0.13 270) |
talkide-reviewer (Nia) | green oklch(0.78 0.13 150) |
talkide-devops (Kai) | teal oklch(0.78 0.12 200) |
Expanded State (TOOL_USE a AGENT_MESSAGE)
Po kliknutí na ▶ u TASK_STARTED se zobrazí všechny záznamy se stejným parentActivityId. Vnořené položky jsou vizuálně odsazeny:
● [Eli] začala · Pozadí aplikace 2 min ▼
├── 📄 Read /src/App.vue 2 min
├── ✏️ Edit /src/main.ts 2 min
├── 📝 Write /src/style.css 2 min
└── 💬 "Upravuji soubory..." 2 min
Layout vnořeného řádku (ml-8 flex items-center gap-2 px-3 py-1.5):
| Slot | Popis |
|---|---|
| Ikon tool | 14px ikona: Read = FileText, Write = FilePlus, Edit = FilePen, Bash = Terminal; AGENT_MESSAGE = MessageSquare — vše var(--fg-3) |
| Tool name | font-mono text-[11px] text-[var(--fg-3)] w-[40px] — Read / Write / Edit / Bash |
| Tool summary | font-mono text-xs text-[var(--fg-2)] flex-1 truncate — toolSummary z API (file_path nebo command nebo první 200 znaků textu) |
| Čas | font-mono text-[11px] text-[var(--fg-4)] |
AGENT_MESSAGE: tool_name je null, zobrazí se MessageSquare ikona + toolSummary truncated na 1 řádek. Klik na řádek rozbalí plný text (tooltip nebo inline expand — detail pro devs).
Animace rozbalení: max-height transition 200 ms ease-out (nebo Vue <Transition name="expand">).
Server-side deduplication (first page only)
The BE pre-deduplicates the activity feed only on the first page (i.e. requests without an afterId cursor). Consecutive activities sharing the same (tool_category, agent_role, parent_activity_id) key are coalesced into a single representative row using a window-function LAG()-based “gaps and islands” query. This prevents the UI from being flooded by, e.g., 20 consecutive Read tool calls from the same agent.
| Dedup rule | Detail |
|---|---|
| Coalesce key | (tool_category, agent_role, parent_activity_id) |
| Applied when | afterId is absent (first-page request) |
| “Load more” (afterId present) | Raw, undeduped events — full fidelity for historical drill-down |
| FE role | Pure renderer — no own dedup logic; renders whatever BE returns |
toolCategory values and their meaning:
toolCategory | Included tool names | Description |
|---|---|---|
READING | Read, Glob, Grep | Reading files or searching |
EDITING | Write, Edit, MultiEdit | Writing or modifying files |
EXECUTING | Bash | Running shell commands |
DELEGATING | Agent, Task | Delegating work to a sub-agent |
BROWSING | WebFetch, WebSearch | Browsing the web |
OTHER | anything else | Any tool not in the above categories |
null | — | Non-tool events: TASK_STARTED, TASK_COMPLETED, AGENT_MESSAGE |
toolCategory is set by RecordActivityUseCase.recordToolUse at insert time (mapping from tool_name). Raw tool_name is preserved in the database for forensics.
Empty State
Zobrazí se pokud activities pole je prázdné (žádná data z REST ani SSE):
┌─────────────────────────────────────────────────────┐
│ │
│ [Activity ikona 32px] │
│ Tým zatím nic neudělal │
│ Pošli zprávu Maře a tým se pustí do práce │
│ │
└─────────────────────────────────────────────────────┘
- Kontejner:
flex-1 flex flex-col items-center justify-center gap-3 - Ikona:
Activity(lucide) 32px,var(--fg-4) - Nadpis:
text-sm font-medium text-[var(--fg-2)] - Podnápis:
text-sm text-[var(--fg-3)]
Loading State (Skeleton)
Zobrazí se při prvním načítání (REST volání běží, žádná data ještě):
- 5 skeleton řádků:
animate-pulse, výška 38px,rounded-lg bg-[var(--bg-2)] - Šířky střídavě 70 % / 85 % / 60 % / 90 % / 75 % pro přirozený výgled
- Header “LIVE ACTIVITY” je viditelný i během skeleton stavu
Live Update Animation
Nový SSE event activity → prepend na začátek seznamu:
- Nový řádek se vloží na pozici 0 s
opacity-0 translate-y-[-8px] - V dalším frame přejde na
opacity-100 translate-y-0— transition 250 ms ease-out - Ostatní řádky se posunou plynule dolů (layout shift, ne transform)
Pokud je uživatel odscrollován dolů (historické záznamy) — nový event se přidá nahoru bez přeskrolování. Badge s počtem nových (”↑ 3 nové”) se zobrazí nahoře — klik přeskroluje na top. (Toto je doporučení pro MVP, implementace na uvážení vývojáře.)
Filtrace — Future Enhancement
Filtrace per agent a per conversation je odložena na post-MVP. Důvody: v MVP bude feed relativně krátký, project-scope pokrývá všechny konverzace, přidání filtrů zvyšuje komplexitu bez okamžité hodnoty.
Navrhovaná budoucí implementace:
- Řada tlačítek pod headerem:
Vše | Mara | Iris | Theo | Eli | Nia | Kai - Klik na chip v team stripu (UC-05002) = shortcut pro filtr agenta
- Filtr per conversation: dropdown
Aktuální konverzace | Všechny konverzace
Accessibility
- Každý feed item má
role="listitem", obalující<ul>márole="list" aria-label="Activity feed" - Expand button:
aria-expanded="true|false"+aria-controls="activity-children-{id}" - Vnořená sekce:
id="activity-children-{id}"(pro aria-controls) - Pulsující dot:
aria-hidden="true"(dekorativní) - Relativní čas:
<time :datetime="createdAt">2 min</time>s ISO atributem - Fokus na expand button se nemění po sbalení/rozbalení
Error States
| Stav | Chování |
|---|---|
| REST 401 | Workspace přesměruje na login (řeší parent WorkspaceScreen) |
| REST 403 / 404 | Workspace zobrazí error banner (řeší parent) |
| REST síťová chyba | Panel zobrazí malý inline error: text-xs text-[var(--red)] s “Nepodařilo se načíst aktivitu. Zkusit znovu.” |
| SSE odpojení | Tiché reconnect (exponential backoff); uživatel nevidí error pokud reconnect proběhne do 5 s; po 5 s žlutý banner var(--amber) “Živé aktualizace pozastaveny” |
i18n Keys
| Klíč | EN | CS |
|---|---|---|
workspace.activityPanel.header | Live activity | Živá aktivita |
workspace.activityPanel.empty.title | The team hasn't done anything yet | Tým zatím nic neudělal |
workspace.activityPanel.empty.subtitle | Send a message to Mara and the team will get to work | Pošli zprávu Maře a tým se pustí do práce |
workspace.activityPanel.action.started | started | začala |
workspace.activityPanel.action.completed | completed | dokončila |
workspace.activityPanel.toolUse.read | Read | Read |
workspace.activityPanel.toolUse.write | Write | Write |
workspace.activityPanel.toolUse.edit | Edit | Edit |
workspace.activityPanel.toolUse.bash | Bash | Bash |
workspace.activityPanel.expand | Show details | Zobrazit detaily |
workspace.activityPanel.collapse | Hide details | Skrýt detaily |
workspace.activityPanel.newItems | {n} new | {n} nové |
workspace.activityPanel.error.load | Failed to load activity. Try again. | Nepodařilo se načíst aktivitu. Zkusit znovu. |
workspace.activityPanel.error.stream | Live updates paused | Živé aktualizace pozastaveny |
Poznámka k překladu
action.started/action.completed: v češtině závisí tvar slovesa na pohlaví. Zatím fixní tvar dletalkide-frontend-dev= Eli (ženský rod). Pokud bude API vracetgenderfield nebo budou všichni agenti ženského rodu, překlady zůstanou správné. Jinak řešit pluralizací nebo gender-neutral formulací jako “spustila” → “spuštěno” — rozhodnutí pro frontend developera.
Frontend
Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| projectId | (path) | — | — | Required path segment |
| limit | (optional) | 1 – 500 | — | Defaults to 100 |
| afterId | (optional) | — | — | Cursor for pagination; omit to get latest |
Backend
Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| projectId | not_null, positive | — | — | Path variable; must reference an existing project belonging to user’s tenant |
| limit | min=1, max=500 | — | — | Default 100 |
| afterId | positive | — | — | Optional; if provided must be a valid positive long |
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
| Mara delegates task to Eli with description “Pozadí aplikace” | CLI emits top-level tool_use Agent with subagent_type=talkide-frontend-dev | TASK_STARTED activity is inserted; agent_role=talkide-frontend-dev, description=“Pozadí aplikace”; tool_use_id stored in in-memory map |
| Mara delegates task to Eli, CLI emits real format with plugin: prefix | CLI emits top-level tool_use Agent with subagent_type=plugin:talkide-frontend-dev | TASK_STARTED activity is inserted; agent_role=talkide-frontend-dev (plugin: prefix stripped); description and payload saved correctly |
| Eli (talkide-frontend-dev) TASK_STARTED exists in map, CLI emits nested tool_use Read with file_path=/src/App.vue | CLI NDJSON parsed | TOOL_USE activity inserted with parent_activity_id=parent.id, tool_name=Read, tool_summary=/src/App.vue |
| Eli TASK_STARTED in map, CLI emits nested assistant text message | CLI NDJSON parsed | AGENT_MESSAGE activity inserted with parent_activity_id=parent.id, tool_summary=first 200 chars, payload_json={“text”: full_text} |
| Eli TASK_STARTED in map, CLI emits top-level tool_result matching task tool_use_id | CLI NDJSON parsed | TASK_COMPLETED activity inserted; agent_role and description inherited from TASK_STARTED |
| authenticated user, project has 150 activities | GET /api/v1/projects/{projectId}/activities?limit=100 is called | 200 OK with 100 most recent activities ordered by id DESC |
| authenticated user, afterId=12345 provided | GET /api/v1/projects/{projectId}/activities?afterId=12345 is called | 200 OK with activities where id < 12345 ordered by id DESC |
| authenticated user, project belongs to user’s tenant | GET /api/v1/projects/{projectId}/activities/stream is called | SSE stream opens; connected event sent; subsequent CLI activity events are pushed in real time |
| authenticated user, SSE stream open, conversation is switched in workspace UI | stream remains open | stream continues receiving events from the new active conversation (project-scoped) |
| CLI emits NDJSON event with parent_tool_use_id not found in in-memory map | activity parsing runs | WARNING is logged; event is discarded; no activity row inserted; SSE stream unaffected |
| authenticated user, project does not exist | GET /api/v1/projects/{projectId}/activities is called | 404 NOT_FOUND_PROJECT error response is returned |
| authenticated user, project belongs to a different tenant | GET /api/v1/projects/{projectId}/activities is called | 403 FORBIDDEN error response is returned |
| no Authorization header | GET /api/v1/projects/{projectId}/activities/stream is called | 401 AUTHENTICATION_FAILED error response is returned |
Thanks for the feedback.