Files
openchamber/packages/ui/src/components/chat/lib/turns/projectTurnActivity.ts
T
Bohdan TriapitsynandIuliia Ivashko 321cc7252a Major UI refresh: sidebar redesign, theme expansion, and chat performance optimizations (#706)
## Summary
Complete sidebar redesign and comprehensive UI polish pass with performance optimizations, theme system refinements, and desktop integration improvements.

## Key Changes

**Sidebar & Navigation Redesign**
- Redesigned sessions sidebar layout with unified button primitives
- Added activity sections with project grouping and improved session organization
- Refined sidebar corners, spacing, and visual hierarchy
- Removed NavRail component in favor of streamlined sidebar
- Stabilized sessions bar toggle position in fullscreen mode

**Performance Optimizations**
- Reduced chat streaming CPU usage and storage churn
- Optimized task tool polling and live timers with debouncing
- Prevented chat state races and reduced background request load
- Debounced draft writes and coalesced session reloads
- Optimized message store updates and turn tracking

**Theme & Visual System**
- Added theme-aware window corners (desktop) and border radius tokens
- Introduced glassmorphism effects on desktop sidebar
- Added backdrop blur to UI elements

**Chat Experience**
- Added session-based permission auto-accept toggle in chat input
- Polished permission shield UX with improved icon sizing and spacing
- Fixed chat scroll-to-bottom behavior and timeline tracking
- Enhanced tool output display with better path label detection
- Removed duplicate draft context details in chat header
- Added text selection menu to chat messages

**Git Improvements**
- Refreshed git history visual design with cleaner dividers
- Added remote removal action in sync selector
- Stabilized git polling to prevent excessive requests
- Improved tool output rendering for git operations

**Settings & Panels**
- Fixed mobile scrolling on settings pages
- Made outside-click settings close instantly
- Reduced settings load churn and CPU spikes
- Improved services dropdown layout and spacing
- Softened panel resize handles

**Desktop Integration**
- Synced macOS window theme with app theme
- Restored window dragging in sidebar header zones
- Fixed system window corners on macOS
- Improved header session metadata and action controls

**Button & Component Standardization**
- Unified button primitives across all components
- Standardized destructive action patterns
- Removed unused button variants (button-large, button-small)
- Aligned context tab close hit areas

---------

Co-authored-by: Iuliia Ivashko <yulia.ivashko@gmail.com>
2026-03-20 01:01:03 +02:00

219 lines
6.8 KiB
TypeScript

import { ACTIVITY_STANDALONE_TOOL_NAMES } from './constants';
import type {
ChatMessageEntry,
TurnActivityGroup,
TurnActivityRecord,
TurnPartRecord,
} from './types';
const isStandaloneTool = (toolName: unknown): boolean => {
return typeof toolName === 'string' && ACTIVITY_STANDALONE_TOOL_NAMES.has(toolName.toLowerCase());
};
const getPartEndTime = (part: unknown): number | undefined => {
const stateEnd = (part as { state?: { time?: { end?: unknown } } }).state?.time?.end;
if (typeof stateEnd === 'number') {
return stateEnd;
}
const timeEnd = (part as { time?: { end?: unknown } }).time?.end;
return typeof timeEnd === 'number' ? timeEnd : undefined;
};
const getPartText = (part: unknown): string | undefined => {
const text = (part as { text?: unknown }).text;
if (typeof text === 'string' && text.trim().length > 0) {
return text;
}
const content = (part as { content?: unknown }).content;
if (typeof content === 'string' && content.trim().length > 0) {
return content;
}
return undefined;
};
const getMessageFinish = (message: ChatMessageEntry): string | undefined => {
const finish = (message.info as { finish?: unknown }).finish;
return typeof finish === 'string' ? finish : undefined;
};
const isAssistantMessageCompleted = (message: ChatMessageEntry): boolean => {
const info = message.info as { time?: { completed?: unknown }; status?: unknown };
const completed = info.time?.completed;
const status = info.status;
if (typeof completed !== 'number' || completed <= 0) {
return false;
}
if (typeof status === 'string') {
return status === 'completed';
}
return true;
};
const buildTurnPartRecord = (
turnId: string,
messageId: string,
part: ChatMessageEntry['parts'][number],
partIndex: number,
): TurnPartRecord => {
return {
id: part.id ?? `${messageId}-part-${partIndex}-${part.type}`,
turnId,
messageId,
part,
partIndex,
endedAt: getPartEndTime(part),
};
};
interface ProjectActivityInput {
turnId: string;
assistantMessages: ChatMessageEntry[];
summarySourceMessageId?: string;
showTextJustificationActivity: boolean;
}
interface ProjectActivityResult {
activityParts: TurnActivityRecord[];
activitySegments: TurnActivityGroup[];
hasTools: boolean;
hasReasoning: boolean;
}
export const projectTurnActivity = (input: ProjectActivityInput): ProjectActivityResult => {
const activityParts: TurnActivityRecord[] = [];
let hasTools = false;
let hasReasoning = false;
const taskMessageById = new Map<string, string>();
const taskOrder: string[] = [];
const partsByAfterTool = new Map<string | null, TurnActivityRecord[]>();
let currentAfterToolPartId: string | null = null;
input.assistantMessages.forEach((message) => {
const messageCompleted = isAssistantMessageCompleted(message);
const finish = getMessageFinish(message);
message.parts.forEach((part, partIndex) => {
const isTool = part.type === 'tool';
if (isTool) {
hasTools = true;
}
const text = part.type === 'reasoning' || part.type === 'text'
? getPartText(part)
: undefined;
if (part.type === 'reasoning' && text) {
hasReasoning = true;
}
const toolName = isTool
? (part as { tool?: unknown }).tool
: undefined;
const standaloneTool = isTool && isStandaloneTool(toolName);
if (standaloneTool) {
const toolPartId = part.id ?? `${message.info.id}-part-${partIndex}-${part.type}`;
if (!taskMessageById.has(toolPartId)) {
taskMessageById.set(toolPartId, message.info.id);
taskOrder.push(toolPartId);
}
currentAfterToolPartId = toolPartId;
}
let kind: TurnActivityRecord['kind'] | null = null;
if (isTool) {
kind = 'tool';
} else if (part.type === 'reasoning') {
if (text) {
kind = 'reasoning';
}
} else if (
input.showTextJustificationActivity
&& part.type === 'text'
&& messageCompleted
&& typeof finish === 'string'
&& finish !== 'stop'
&& text
) {
kind = 'justification';
}
if (!kind) {
return;
}
const activity: TurnActivityRecord = {
...buildTurnPartRecord(input.turnId, message.info.id, part, partIndex),
kind,
};
activityParts.push(activity);
if (kind === 'tool' && standaloneTool) {
return;
}
const list = partsByAfterTool.get(currentAfterToolPartId) ?? [];
list.push(activity);
partsByAfterTool.set(currentAfterToolPartId, list);
});
});
const activitySegments: TurnActivityGroup[] = [];
const pickStartAnchor = (segmentParts: TurnActivityRecord[]): string | undefined => {
if (segmentParts.length === 0) {
return undefined;
}
const countByMessage = new Map<string, number>();
segmentParts.forEach((activity) => {
countByMessage.set(activity.messageId, (countByMessage.get(activity.messageId) ?? 0) + 1);
});
let firstWithAny: string | undefined;
let cumulative = 0;
for (const message of input.assistantMessages) {
const count = countByMessage.get(message.info.id) ?? 0;
if (count > 0 && !firstWithAny) {
firstWithAny = message.info.id;
}
cumulative += count;
if (cumulative >= 2) {
return message.info.id;
}
}
return firstWithAny;
};
const orderedKeys: Array<string | null> = [null, ...taskOrder];
orderedKeys.forEach((afterToolPartId) => {
const segmentParts = partsByAfterTool.get(afterToolPartId) ?? [];
if (segmentParts.length === 0) {
return;
}
const anchorMessageId = afterToolPartId === null
? pickStartAnchor(segmentParts)
: taskMessageById.get(afterToolPartId);
if (!anchorMessageId) {
return;
}
activitySegments.push({
id: `${input.turnId}:${anchorMessageId}:${afterToolPartId ?? 'start'}`,
anchorMessageId,
afterToolPartId,
parts: segmentParts,
});
});
return {
activityParts,
activitySegments,
hasTools,
hasReasoning,
};
};