Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
128 commits
Select commit Hold shift + click to select a range
98fd368
feat(conversations): add conversation list view
senamakel Oct 6, 2026
5ca1af3
feat(conversations): add thread list to conversations view
senamakel Oct 6, 2026
418eabc
feat(thread): add optional actionDir field to Thread
senamakel Oct 6, 2026
a2c2cb0
feat(threads): generate thread titles from first message
senamakel Oct 6, 2026
d9f576d
feat(threads): generate thread titles from first message
senamakel Oct 6, 2026
7c00bb1
feat(conversations): add thread grouping helper with tests
senamakel Oct 6, 2026
7f5562d
feat(config): add local AI inference settings
senamakel Oct 6, 2026
17d210e
feat(conversations): track unread threads in thread list
senamakel Oct 6, 2026
e202076
feat(conversations): add thread list component
senamakel Oct 6, 2026
64bcb64
feat(conversations): add thread list component
senamakel Oct 6, 2026
a26662e
feat(conversations): add thread list component
senamakel Oct 6, 2026
2cadbe3
feat(conversations): add thread pinning and unread indicators
senamakel Oct 6, 2026
2e94b6d
feat(i18n): add missing translation keys across locales
senamakel Oct 6, 2026
1fbbc86
feat(threads): add working_dir field to conversation threads
senamakel Oct 6, 2026
bed7f57
feat(conversations): add thread list component
senamakel Oct 6, 2026
9b5a389
chore(vendor): add tinyagents dependency
senamakel Oct 6, 2026
e32a1b9
feat(conversations): add thread list component
senamakel Oct 6, 2026
e25ddd3
test(conversations): add ThreadList component tests
senamakel Oct 6, 2026
e1d22b2
feat(threads): add thread pinning support
senamakel Oct 6, 2026
82b5742
refactor(threads): simplify working dir resolution
senamakel Oct 6, 2026
ca28b81
feat(threads): add schema registry for thread operations
senamakel Oct 6, 2026
f6def3c
style(threads): reorder imports and re-exports
senamakel Oct 6, 2026
b633c7d
feat(thread): add thread API service and slice
senamakel Oct 6, 2026
5cd10db
feat(conversations): add workspace picker component
senamakel Oct 6, 2026
31f3fc5
chore(config): remove unused tauri config helpers
senamakel Oct 6, 2026
2489230
feat(conversations): add reasoning effort picker
senamakel Oct 6, 2026
2adf0ad
feat(conversations): add conversation list view
senamakel Oct 6, 2026
97547d5
feat(conversations): add conversation list view
senamakel Oct 6, 2026
ceea3e0
feat(conversations): add thread workspace chip
senamakel Oct 6, 2026
f4d68d6
feat(conversations): add empty state for conversations list
senamakel Oct 6, 2026
28452a3
feat(conversations): add workspace and reasoning effort pickers
senamakel Oct 6, 2026
2e8f067
feat(conversations): add thread workspace chip
senamakel Oct 6, 2026
1900fd5
test(conversations): add WorkspacePicker tests
senamakel Oct 6, 2026
78d4d95
test(conversations): cover rendering of the conversations page
senamakel Oct 6, 2026
2c4d9a5
test(conversations): cover thread workspace chip rendering
senamakel Oct 6, 2026
607681b
test(json-rpc): cover working dir binding before first message
senamakel Oct 6, 2026
7cfe341
feat(about_app): add conversation intelligence catalog entry
senamakel Oct 6, 2026
84d3e79
feat(assistant-ui): add todo list element
senamakel Oct 6, 2026
326747f
feat(conversations): show agent running status in aui
senamakel Oct 6, 2026
8e3e347
feat(conversations): add pinned todo card
senamakel Oct 6, 2026
798c3e0
feat(conversations): add conversation list view
senamakel Oct 6, 2026
8330330
feat(conversations): add thread draft hook
senamakel Oct 6, 2026
095dfd0
feat(conversations): add conversation list view
senamakel Oct 6, 2026
e828d83
feat(conversations): add conversation list view
senamakel Oct 6, 2026
898c2c7
feat(conversations): add thread draft hook
senamakel Oct 6, 2026
e060fcc
feat(conversations): add todo list part component
senamakel Oct 6, 2026
ec0eef7
test(conversations): cover thread draft hook behaviour
senamakel Oct 6, 2026
34492fb
test(conversations): cover todo list part rendering
senamakel Oct 6, 2026
9b55d64
feat(conversations): add conversation list view
senamakel Oct 6, 2026
a1cfdc8
feat(conversations): add conversation list view
senamakel Oct 6, 2026
c19bed5
feat(conversations): add live elapsed time hook
senamakel Oct 6, 2026
136054c
feat(conversations): add subagent task card component
senamakel Oct 6, 2026
bfeeadf
refactor(conversations): split live elapsed timer into since and runn…
senamakel Oct 6, 2026
90ef3cc
feat(conversations): add conversation list view
senamakel Oct 6, 2026
06977df
feat(conversations): show subagent and todo progress in parallel agen…
senamakel Oct 6, 2026
27bdb70
feat(conversations): render assistant ui tool calls
senamakel Oct 6, 2026
e621fad
feat(conversations): add conversation list view
senamakel Oct 6, 2026
5cc78ee
feat(conversations): add conversation list view
senamakel Oct 6, 2026
d4283a5
feat(conversations): add conversation list view
senamakel Oct 6, 2026
48b958b
feat(conversations): show live elapsed time on subagent task cards
senamakel Oct 6, 2026
58bad39
chore: I don't see a diff in your message — the Files, Stat, and Diff…
senamakel Oct 6, 2026
e3499cf
feat(conversations): add assistant ui chat component
senamakel Oct 6, 2026
5b947f5
chore: I don't see a diff in your message — the Files, Stat, and Diff…
senamakel Oct 6, 2026
c6fd629
feat(assistant-ui): group consecutive tool calls into activity groups
senamakel Oct 6, 2026
2459a40
feat(i18n): add interrupted turn notice strings
senamakel Oct 6, 2026
81a7189
feat(i18n): add bengali translations
senamakel Oct 6, 2026
49c7654
docs(i18n): add German translations
senamakel Oct 6, 2026
6af0085
docs(i18n): add Spanish translations
senamakel Oct 6, 2026
19b9025
feat(assistant-ui): add tool group element
senamakel Oct 6, 2026
60d7e0f
feat(i18n): add Hindi translations
senamakel Oct 6, 2026
7c24164
feat(assistant-ui): add Indonesian translations for thread component
senamakel Oct 6, 2026
ab718b6
feat(i18n): add italian translations for chat todos and subagents
senamakel Oct 6, 2026
56264b3
feat(conversations): add Korean translations for chat tool parts
senamakel Oct 6, 2026
ee3e99b
chore: I don't see a diff in your message — the Files, Stat, and Diff…
senamakel Oct 6, 2026
f7d8185
feat(i18n): add Polish translations
senamakel Oct 6, 2026
282df99
feat(i18n): add Portuguese, Russian, and Simplified Chinese locales
senamakel Oct 6, 2026
b61392f
chore: I don't see a diff in your message — the "Diff:" section is em…
senamakel Oct 6, 2026
aa510a6
feat(assistant-ui): add thread component
senamakel Oct 6, 2026
c670216
feat(i18n): add Arabic and German translations
senamakel Oct 6, 2026
372a83e
docs(i18n): add French translations for new UI strings
senamakel Oct 6, 2026
b9b4b36
feat(assistant-ui): add thread component
senamakel Oct 6, 2026
497e2f0
feat(i18n): add spanish and italian translations
senamakel Oct 6, 2026
1435e70
feat(i18n): add korean translations
senamakel Oct 6, 2026
1545afd
feat(i18n): add Indonesian translations
senamakel Oct 6, 2026
33e5438
feat(i18n): add bengali relative time strings
senamakel Oct 6, 2026
521a9e5
feat(i18n): add Portuguese translations
senamakel Oct 6, 2026
22a3ae7
docs(i18n): add Hindi translations
senamakel Oct 6, 2026
0a64f57
feat(assistant-ui): add Polish translations for thread component
senamakel Oct 6, 2026
6015a24
chore: I don't see an actual diff in your message — the "Diff:" secti…
senamakel Oct 6, 2026
eb42f3b
chore: I don't see a diff in your message — the "Diff:" section is em…
senamakel Oct 6, 2026
f4478db
feat(assistant-ui): add activity and tool group components
senamakel Oct 6, 2026
d7d7bce
docs(i18n): add missing English translation strings
senamakel Oct 6, 2026
5f99c09
chore: I don't see a diff in your message — the Files, Stat, and Diff…
senamakel Oct 6, 2026
3b77da3
test(conversations): cover parallel agents card rendering
senamakel Oct 6, 2026
f2f5fb2
test(conversations): add coverage for agent status and stall warning
senamakel Oct 6, 2026
f316fa1
docs(i18n): add German translations
senamakel Oct 6, 2026
ca4ef89
docs(i18n): add Spanish translations
senamakel Oct 6, 2026
21db952
docs(i18n): add French translations for new UI strings
senamakel Oct 6, 2026
a08e414
feat(i18n): add hindi translations for conversation ui
senamakel Oct 6, 2026
9b699f7
feat(i18n): add Indonesian translations
senamakel Oct 6, 2026
28da8f6
docs(i18n): add Italian translations
senamakel Oct 6, 2026
c2d7f29
feat(i18n): add korean translations
senamakel Oct 6, 2026
48f9800
feat(i18n): add Polish translations
senamakel Oct 6, 2026
796bdb7
feat(i18n): add Portuguese translations
senamakel Oct 6, 2026
91d268a
chore: I don't see an actual diff in your message — the "Diff:" secti…
senamakel Oct 6, 2026
a3cc2da
feat(i18n): add Russian and Chinese translations
senamakel Oct 6, 2026
ef84b13
feat(i18n): add Simplified Chinese translations
senamakel Oct 6, 2026
154da0d
feat(conversations): persist thread drafts across navigation
senamakel Oct 6, 2026
0e7fe54
feat(conversations): add conversation list view
senamakel Oct 6, 2026
ad96ea9
feat(assistant-ui): add todo list element
senamakel Oct 6, 2026
b8eb19a
feat(conversations): add conversation list view
senamakel Oct 6, 2026
e097f2b
feat(conversations): add stall warning for assistant responses
senamakel Oct 6, 2026
ab13f90
test(conversations): cover assistant ui tool call rendering
senamakel Oct 6, 2026
ff4a6fc
test(conversations): cover thread workspace chip rendering
senamakel Oct 6, 2026
bf52222
test(assistant-ui): cover composer cue behaviour in thread
senamakel Oct 6, 2026
13e68bc
feat(utils): add relative time formatting helper
senamakel Oct 6, 2026
fa14540
test(conversations): cover interrupted turn notice rendering
senamakel Oct 6, 2026
c480a30
test(conversations): cover rendering of the conversations page
senamakel Oct 6, 2026
fe10388
feat(i18n): add missing translation keys across all locales
senamakel Oct 6, 2026
2d13a34
feat(conversations): add pinned todo card to conversation view
senamakel Oct 6, 2026
8caf14e
feat(todo-list): add assistant todo list element
senamakel Oct 6, 2026
f789eb5
test(assistant-ui): add todo list element tests
senamakel Oct 6, 2026
4b0f350
chore(vendor): pin tinyagents to the per-thread working dir branch (t…
senamakel Oct 7, 2026
9f5a690
fix: resolve PR 7048 merge conflict and review findings
senamakel Oct 7, 2026
bd5c50d
style: format working folder validation changes
senamakel Oct 7, 2026
49946a3
fix: make failed conversation sends thread-safe
senamakel Oct 7, 2026
04cbc2d
Merge remote-tracking branch 'upstream/main' into pr/7048
senamakel Oct 7, 2026
4bdd98c
fix: restore conversation send defaults
senamakel Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 47 additions & 6 deletions app/src/components/assistant-ui/activity-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,47 @@ import {
ToolGroupTrigger,
} from '@/components/assistant-ui/elements/tool-group';
import { OpenHumanReasoningGroup } from '@/components/assistant-ui/reasoning-group';
import { formatElapsed } from '@/components/assistant-ui/utils/task';
import { useT } from '@/lib/i18n/I18nContext';
import { type MessagePrimitive, useAuiState } from '@assistant-ui/react';
import { type FC, type PropsWithChildren, useState } from 'react';

export type ActivityGroupPart = MessagePrimitive.GroupedParts.GroupPart;

type Translate = (key: string) => string;

/**
* Trigger text for a run of reasoning and tool calls.
*
* Tool calls are what the reader counts; reasoning is either there or not, so
* it is named rather than counted. The group only exists when it holds at
* least one of the two, so the empty fallback is never shown in practice.
*
* `workedMs`, when known, turns a settled group into OpenClaw's turn receipt:
* "Worked for 42s · 3 tool calls".
*/
export function activityGroupLabel(reasoningCount: number, toolCount: number): string {
const tools = toolCount > 0 ? `${toolCount} tool ${toolCount === 1 ? 'call' : 'calls'}` : null;
if (reasoningCount > 0 && tools) return `Reasoning · ${tools}`;
if (reasoningCount > 0) return 'Reasoning';
return tools ?? 'Activity';
export function activityGroupLabel(
reasoningCount: number,
toolCount: number,
t: Translate,
workedMs?: number
): string {
const tools =
toolCount > 0
? t(toolCount === 1 ? 'chat.tools.callOne' : 'chat.tools.callOther').replace(
'{count}',
String(toolCount)
)
: null;
if (workedMs !== undefined && tools) {
return t('chat.tools.workedFor')
.replace('{duration}', formatElapsed(workedMs))
.replace('{calls}', tools);
}
if (reasoningCount > 0 && tools)
return t('chat.tools.reasoningWithCalls').replace('{calls}', tools);
if (reasoningCount > 0) return t('chat.tools.reasoning');
return tools ?? t('chat.tools.activity');
}

/**
Expand Down Expand Up @@ -73,6 +97,18 @@ export const ActivityGroup: FC<PropsWithChildren<{ group: ActivityGroupPart }>>
const requiresAction = useAuiState(s =>
indices.some(i => s.message.parts[i]?.status?.type === 'requires-action')
);
// The turn's wall time (`chat_done.timing.total_ms`), credited to this group
// only when it holds every tool call in the message: a message with several
// activity groups cannot say which share of the time each one took.
const messageToolCount = useAuiState(
s => s.message.parts.filter(part => part?.type === 'tool-call').length
);
const totalStreamTime = useAuiState(
s =>
(s.message.metadata as { timing?: { totalStreamTime?: number } } | undefined)?.timing
?.totalStreamTime
);
const { t } = useT();
const [userOpen, setUserOpen] = useState<boolean | null>(null);

const running = group.status.type === 'running' || isTail;
Expand All @@ -88,7 +124,12 @@ export const ActivityGroup: FC<PropsWithChildren<{ group: ActivityGroupPart }>>
<ToolGroupRoot variant="ghost" open={userOpen ?? live} onOpenChange={setUserOpen}>
<ToolGroupTrigger
count={toolCount}
label={activityGroupLabel(reasoningCount, toolCount)}
label={activityGroupLabel(
reasoningCount,
toolCount,
t,
!live && toolCount === messageToolCount ? totalStreamTime : undefined
)}
active={running}
/>
<ToolGroupContent>{children}</ToolGroupContent>
Expand Down
7 changes: 6 additions & 1 deletion app/src/components/assistant-ui/elements/subagent-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
* Changes from upstream:
* - `cn` import path (`@/components/assistant-ui/lib/utils`).
* - `../utils/range` -> `@/components/assistant-ui/utils/range`.
* - `SubagentItem.done`: an agent's own finished flag. Upstream marks the
* first `completedCount` rows done, which is only right when workers finish
* in list order; parallel workers do not.
* No hard-coded user-facing copy — `agent.name`/`agent.model` are
* caller-supplied props, so there is nothing to route through `useT()` here.
*/
Expand All @@ -19,6 +22,8 @@ import { mono, paper } from './surfaces';
export interface SubagentItem {
name: string;
model: string;
/** This agent has finished; falls back to `index < completedCount` when absent. */
done?: boolean;
}

export function SubagentList({
Expand All @@ -45,7 +50,7 @@ export function SubagentList({
className={cn('flex min-h-[14.5rem] w-full max-w-xs flex-col gap-2', className)}
{...props}>
{agents.map((agent, index) => {
const done = index < completedCount;
const done = agent.done ?? index < completedCount;
const width = progress[index] ?? 0;
const percentage = pct(width, 100);

Expand Down
86 changes: 86 additions & 0 deletions app/src/components/assistant-ui/elements/todo-list.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

import { type TodoItem, todoProgress, TodoProgressCard, TodoReceipt } from './todo-list';

const items: TodoItem[] = [
{ id: '1', text: 'Read the spec', status: 'done' },
{ id: '2', text: 'Write the code', status: 'active' },
{ id: '3', text: 'Ship it', status: 'pending' },
];

describe('todoProgress', () => {
it('names the first active step and its position', () => {
const progress = todoProgress(items);
expect(progress).toMatchObject({ done: 1, total: 3, position: 2, anyActive: true });
expect(progress.current?.text).toBe('Write the code');
});

it('falls back to the first pending step, then the last done one', () => {
const pending = todoProgress([
{ id: '1', text: 'A', status: 'done' },
{ id: '2', text: 'B', status: 'pending' },
]);
expect(pending.current?.text).toBe('B');
const finished = todoProgress([
{ id: '1', text: 'A', status: 'done' },
{ id: '2', text: 'B', status: 'done' },
]);
expect(finished).toMatchObject({ allDone: true, position: 2 });
expect(finished.current?.text).toBe('B');
});

it('handles an empty list', () => {
expect(todoProgress([])).toMatchObject({ total: 0, position: 0, allDone: false });
});
});

describe('TodoProgressCard', () => {
const labels = { title: 'Todos', completedLabel: 'Completed', countLabel: '1 of 3' };

it('collapsed shows the current step and pos/total, not the list', () => {
render(<TodoProgressCard items={items} open={false} onOpenChange={vi.fn()} {...labels} />);
const toggle = screen.getByRole('button', { name: 'Todos' });
expect(toggle).toHaveAttribute('aria-expanded', 'false');
expect(toggle).toHaveTextContent('Write the code');
expect(screen.getByTestId('todo-progress-count')).toHaveTextContent('2/3');
// The steps stay mounted but hidden, so they take no space and are not visible.
expect(screen.getByText('Ship it')).not.toBeVisible();
});

it('expanded shows the count label and every step', () => {
const onOpenChange = vi.fn();
render(<TodoProgressCard items={items} open onOpenChange={onOpenChange} {...labels} />);
expect(screen.getByTestId('todo-progress-count')).toHaveTextContent('1 of 3');
expect(screen.getByText('Ship it')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Todos' }));
expect(onOpenChange).toHaveBeenCalledWith(false);
});

it('says completed once every step is done', () => {
render(
<TodoProgressCard
items={items.map(item => ({ ...item, status: 'done' as const }))}
open={false}
onOpenChange={vi.fn()}
{...labels}
/>
);
expect(screen.getByTestId('todo-progress-count')).toHaveTextContent('Completed');
});
});

describe('TodoReceipt', () => {
it('is one line that expands to the snapshot', () => {
const onOpenChange = vi.fn();
const { rerender } = render(
<TodoReceipt items={items} label="Progress 1/3" open={false} onOpenChange={onOpenChange} />
);
expect(screen.getByRole('button')).toHaveTextContent('Progress 1/3 · Write the code');
expect(screen.queryByText('Ship it')).toBeNull();
fireEvent.click(screen.getByRole('button'));
expect(onOpenChange).toHaveBeenCalledWith(true);
rerender(<TodoReceipt items={items} label="Progress 1/3" open onOpenChange={onOpenChange} />);
expect(screen.getByText('Ship it')).toBeInTheDocument();
});
});
Loading
Loading