## 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>
219 lines
6.8 KiB
TypeScript
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,
|
|
};
|
|
};
|