From d6a0f4c3a117e2db3fe4d5085b534c532faffe08 Mon Sep 17 00:00:00 2001 From: iliya Date: Tue, 10 Mar 2026 21:54:53 +0200 Subject: [PATCH] feat: enhance task management features and improve messaging components - Updated README to include new 'Solo mode' feature for single-agent task management. - Refactored message handling in TeamDataService and TeamProvisioningService to improve deduplication of lead messages. - Enhanced linkification in chat components to support team mentions. - Introduced AnimatedHeightReveal for smoother task item animations in the sidebar. - Improved task comment input to support chip draft persistence and team suggestions. - Cleaned up CSS by removing unused animations related to task item entry. --- README.md | 4 +- src/main/ipc/teams.ts | 24 ++- src/main/services/team/TeamDataService.ts | 151 +++++++++++++----- .../services/team/TeamProvisioningService.ts | 44 ++++- .../components/chat/UserChatGroup.tsx | 13 +- .../chat/items/TeammateMessageItem.tsx | 13 +- .../chat/viewers/MarkdownViewer.tsx | 25 --- .../components/settings/SettingsTabs.tsx | 2 +- .../components/sidebar/GlobalTaskList.tsx | 83 +++++----- .../components/sidebar/SidebarTaskItem.tsx | 7 +- .../components/team/activity/ActivityItem.tsx | 17 +- .../team/activity/AnimatedHeightReveal.tsx | 14 +- .../team/activity/LeadThoughtsGroup.tsx | 32 +++- .../team/dialogs/SendMessageDialog.tsx | 22 ++- .../team/dialogs/TaskCommentInput.tsx | 16 +- .../team/dialogs/TaskCommentsSection.tsx | 35 +++- .../team/messages/MessageComposer.tsx | 24 +-- .../team/review/ChangeReviewDialog.tsx | 104 +++++++++++- .../components/ui/ChipInteractionLayer.tsx | 32 +++- src/renderer/components/ui/CodeChipBadge.tsx | 13 +- .../components/ui/MentionableTextarea.tsx | 55 +++++-- src/renderer/index.css | 18 --- src/renderer/store/slices/editorSlice.ts | 35 ++++ src/renderer/types/inlineChip.ts | 19 ++- src/renderer/utils/chipUtils.ts | 7 +- src/shared/types/editor.ts | 2 + 26 files changed, 600 insertions(+), 211 deletions(-) diff --git a/README.md b/README.md index 1213f49f..bdd793b2 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,14 @@ A new approach to task management with AI agent teams. - **Assemble your team** — create agent teams with different roles that work autonomously in parallel -- **Agents talk to each other** — they communicate, create and manage their own tasks, and leave comments +- **Agents talk to each other** — they communicate, create and manage their own tasks, review, leave comments - **Cross-team communication** — agents can fully communicate across different teams; you can configure or prompt them to collaborate and message each other between teams - **Sit back and watch** — tasks change status on the kanban board while agents handle everything on their own - **Review changes like in Cursor** — see what code each task changed, then approve, reject, or comment - **Full tool visibility** — inspect exactly which tools an agent used to complete each task - **Live process section** — see which agents are running processes and open URLs directly in the browser - **Stay in control** — send a direct message to any agent, drop a comment on a task, or pick a quick action right on the kanban card whenever you want to clarify something or add new work +- **Solo mode** — one-member team: a single agent that creates its own tasks and shows live progress. Saves tokens; can expand to a full team anytime
More features @@ -39,7 +40,6 @@ A new approach to task management with AI agent teams.
- **Deep session analysis** — detailed breakdown of what happened in each Claude session: bash commands, reasoning, subprocesses -- **Solo mode** — one-member team: a single agent that creates its own tasks and shows live progress. Saves tokens; can expand to a full team anytime - **Smart task-to-log/changes matching** — automatically links Claude session logs/changes to specific tasks - **Advanced context monitoring system** — comprehensive breakdown of what consumes tokens at every step: user messages, Claude.md instructions, tool outputs, thinking text, and team coordination. Token usage, percentage of context window, and session cost are displayed for each category, with detailed views by category or size. - **Recent tasks across projects** — browse the latest completed tasks from all your projects in one place diff --git a/src/main/ipc/teams.ts b/src/main/ipc/teams.ts index 67625ba9..0098cae6 100644 --- a/src/main/ipc/teams.ts +++ b/src/main/ipc/teams.ts @@ -429,13 +429,21 @@ async function handleGetData( } const normalizeText = (text: string): string => text.trim().replace(/\r\n/g, '\n'); + const isLeadThoughtLike = (msg: { source?: unknown; to?: string }): boolean => + !msg.to && (msg.source === 'lead_process' || msg.source === 'lead_session'); + const getLeadThoughtFingerprint = (msg: { + from: string; + text: string; + leadSessionId?: string; + }): string => `${msg.leadSessionId ?? ''}\0${msg.from}\0${normalizeText(msg.text)}`; - // Collect text fingerprints from ALL non-live messages (inbox, lead_session, sentMessages) - // so we can dedup lead_process live messages against them. + // Collect fingerprints only for thought-like lead messages. Include leadSessionId so a + // repeated thought in a new session does not get collapsed into an old session's history. const existingTextFingerprints = new Set(); for (const msg of data.messages) { if (typeof msg.from !== 'string' || typeof msg.text !== 'string') continue; - existingTextFingerprints.add(`${msg.from}\0${normalizeText(msg.text)}`); + if (!isLeadThoughtLike(msg)) continue; + existingTextFingerprints.add(getLeadThoughtFingerprint(msg)); } const keyFor = (m: { @@ -450,20 +458,20 @@ async function handleGetData( return `${m.timestamp}\0${m.from}\0${(m.text ?? '').slice(0, 80)}`; }; - // Text-based fingerprints for lead_process messages to catch duplicates - // with different messageIds (e.g. lead-turn-* vs lead-sendmsg-* with same text) + // Text-based fingerprints for live lead thoughts to catch duplicates with different + // messageIds inside the same session (e.g. lead-turn-* re-emits). const leadProcessTextFingerprints = new Set(); const merged: typeof data.messages = []; const seen = new Set(); for (const msg of [...data.messages, ...live]) { if ((msg as { source?: unknown }).source === 'lead_process' && !msg.to) { - const fp = `${msg.from}\0${normalizeText(msg.text ?? '')}`; - // Skip if same text already exists from any source (inbox, lead_session, etc.) + const fp = getLeadThoughtFingerprint(msg); + // Skip if the same thought already exists in persisted history for the same session. if (existingTextFingerprints.has(fp)) { continue; } - // Dedup lead_process messages with same text but different messageIds + // Dedup live lead_process thoughts with the same text in the same session. if (leadProcessTextFingerprints.has(fp)) { continue; } diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts index 2dc5284c..b25bd97d 100644 --- a/src/main/services/team/TeamDataService.ts +++ b/src/main/services/team/TeamDataService.ts @@ -319,18 +319,21 @@ export class TeamDataService { // Dedup: if a lead_process message text is also present in lead_session, prefer lead_session. // This avoids double-rendering when we persist lead process messages and later load the lead JSONL. // Exception: lead_process messages with `to` field are captured SendMessage — never dedup those. - if (leadTexts.length > 0 && sentMessages.length > 0) { + if (leadTexts.length > 0) { const normalizeText = (text: string): string => text.trim().replace(/\r\n/g, '\n'); + const getLeadThoughtFingerprint = ( + msg: Pick + ) => `${msg.leadSessionId ?? ''}\0${msg.from}\0${normalizeText(msg.text ?? '')}`; const leadSessionFingerprints = new Set(); for (const msg of leadTexts) { if (msg.source !== 'lead_session') continue; - leadSessionFingerprints.add(`${msg.from}\0${normalizeText(msg.text)}`); + leadSessionFingerprints.add(getLeadThoughtFingerprint(msg)); } messages = messages.filter((m) => { if (m.source !== 'lead_process') return true; // Captured SendMessage messages (with recipient) are real messages — never dedup if (m.to) return true; - const fp = `${m.from}\0${normalizeText(m.text ?? '')}`; + const fp = getLeadThoughtFingerprint(m); return !leadSessionFingerprints.has(fp); }); } @@ -1195,39 +1198,70 @@ export class TeamDataService { }); } - private async extractLeadSessionTexts(config: TeamConfig): Promise { - if (!config.leadSessionId || !config.projectPath) { - return []; - } - - const projectId = encodePath(config.projectPath); + private getLeadProjectDirCandidates(projectPath: string): string[] { + const projectId = encodePath(projectPath); const baseDir = extractBaseDir(projectId); - let jsonlPath = path.join(getProjectsBasePath(), baseDir, `${config.leadSessionId}.jsonl`); - - try { - await fs.promises.access(jsonlPath, fs.constants.F_OK); - } catch { + const candidateDirs = [ + path.join(getProjectsBasePath(), baseDir), // Claude Code encodes underscores as hyphens in project directory names; // our encodePath only handles slashes. Try the underscore-to-hyphen variant. - const altBaseDir = baseDir.replace(/_/g, '-'); - if (altBaseDir !== baseDir) { - const altPath = path.join( - getProjectsBasePath(), - altBaseDir, - `${config.leadSessionId}.jsonl` - ); - try { - await fs.promises.access(altPath, fs.constants.F_OK); - jsonlPath = altPath; - } catch { - return []; - } - } else { - return []; + ...(baseDir.includes('_') + ? [path.join(getProjectsBasePath(), baseDir.replace(/_/g, '-'))] + : []), + ]; + + return [...new Set(candidateDirs)]; + } + + private async getLeadSessionJsonlPaths(projectPath: string): Promise> { + const jsonlPaths = new Map(); + for (const dirPath of this.getLeadProjectDirCandidates(projectPath)) { + let entries: fs.Dirent[]; + try { + entries = await fs.promises.readdir(dirPath, { withFileTypes: true }); + } catch { + continue; + } + + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue; + const sessionId = entry.name.slice(0, -'.jsonl'.length).trim(); + if (!sessionId || jsonlPaths.has(sessionId)) continue; + jsonlPaths.set(sessionId, path.join(dirPath, entry.name)); } } - const leadName = config.members?.find((m) => m.agentType === 'team-lead')?.name ?? 'team-lead'; + return jsonlPaths; + } + + private getRecentLeadSessionIds(config: TeamConfig): string[] { + const sessionIds: string[] = []; + const seen = new Set(); + const pushSessionId = (value: unknown): void => { + if (typeof value !== 'string') return; + const sessionId = value.trim(); + if (!sessionId || seen.has(sessionId)) return; + seen.add(sessionId); + sessionIds.push(sessionId); + }; + + pushSessionId(config.leadSessionId); + if (Array.isArray(config.sessionHistory)) { + for (let i = config.sessionHistory.length - 1; i >= 0; i--) { + pushSessionId(config.sessionHistory[i]); + } + } + + return sessionIds; + } + + private async extractLeadSessionTextsFromJsonl( + jsonlPath: string, + leadName: string, + leadSessionId: string, + maxTexts: number + ): Promise { + if (maxTexts <= 0) return []; // Optimization: read from the end of the JSONL file (we only need the last N texts). // The full file can be huge; scanning from the start causes long stalls on Windows. @@ -1242,7 +1276,7 @@ export class TeamDataService { const fileSize = stat.size; let scanBytes = Math.min(INITIAL_SCAN_BYTES, fileSize); - while (textsReversed.length < MAX_LEAD_TEXTS && scanBytes <= MAX_SCAN_BYTES) { + while (textsReversed.length < maxTexts && scanBytes <= MAX_SCAN_BYTES) { const start = Math.max(0, fileSize - scanBytes); const buffer = Buffer.alloc(scanBytes); await handle.read(buffer, 0, scanBytes, start); @@ -1314,13 +1348,22 @@ export class TeamDataService { const toolCalls = toolCallsList.length > 0 ? toolCallsList : undefined; const toolSummary = toolCalls ? formatToolSummaryFromCalls(toolCalls) : undefined; - // Stable messageId: timestamp + text prefix (survives tail-scan range changes) + const entryUuid = typeof msg.uuid === 'string' ? msg.uuid.trim() : ''; + const assistantMessageId = typeof message.id === 'string' ? message.id.trim() : ''; + const stableMessageId = entryUuid + ? `lead-thought-${entryUuid}` + : assistantMessageId + ? `lead-thought-msg-${assistantMessageId}` + : null; + + // Fallback messageId: timestamp + text prefix (survives tail-scan range changes) const textPrefix = combined .slice(0, 50) .replace(/[^\p{L}\p{N}]/gu, '') .slice(0, 20); - const messageId = `lead-session-${timestamp}-${textPrefix}`; + const messageId = + stableMessageId ?? `lead-session-${leadSessionId}-${timestamp}-${textPrefix}`; if (seenMessageIds.has(messageId)) continue; seenMessageIds.add(messageId); @@ -1330,15 +1373,15 @@ export class TeamDataService { timestamp, read: true, source: 'lead_session', - leadSessionId: config.leadSessionId, + leadSessionId, messageId, toolSummary, toolCalls, }); - if (textsReversed.length >= MAX_LEAD_TEXTS) break; + if (textsReversed.length >= maxTexts) break; } - if (textsReversed.length >= MAX_LEAD_TEXTS) break; + if (textsReversed.length >= maxTexts) break; if (scanBytes === fileSize) break; scanBytes = Math.min(fileSize, scanBytes * 2); } @@ -1349,6 +1392,42 @@ export class TeamDataService { // Convert back to chronological order (old behavior) and keep the last N texts. textsReversed.reverse(); const texts = textsReversed; + return texts.length > maxTexts ? texts.slice(-maxTexts) : texts; + } + + private async extractLeadSessionTexts(config: TeamConfig): Promise { + if (!config.projectPath) { + return []; + } + + const leadName = config.members?.find((m) => m.agentType === 'team-lead')?.name ?? 'team-lead'; + const sessionIds = this.getRecentLeadSessionIds(config); + if (sessionIds.length === 0) { + return []; + } + const availableJsonlPaths = await this.getLeadSessionJsonlPaths(config.projectPath); + if (availableJsonlPaths.size === 0) { + return []; + } + + const texts: InboxMessage[] = []; + for (const sessionId of sessionIds) { + if (texts.length >= MAX_LEAD_TEXTS) break; + const jsonlPath = availableJsonlPaths.get(sessionId); + if (!jsonlPath) continue; + const remaining = MAX_LEAD_TEXTS - texts.length; + const sessionTexts = await this.extractLeadSessionTextsFromJsonl( + jsonlPath, + leadName, + sessionId, + remaining + ); + if (sessionTexts.length > 0) { + texts.push(...sessionTexts); + } + } + + texts.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)); return texts.length > MAX_LEAD_TEXTS ? texts.slice(-MAX_LEAD_TEXTS) : texts; } diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index d1b98ff7..316629f0 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -557,7 +557,12 @@ function buildPersistentLeadContext(opts: { `\n - ALLOWED: You may message "user" (the human operator) via SendMessage.` + `\n - ALLOWED: You may use the Task tool for regular subagents WITHOUT team_name — these are normal Claude Code helpers, not teammates.` + `\n - If teammates are added later (e.g. via UI), you may then spawn them using the Task tool with team_name + name.` + - `\n - Work on tasks directly yourself. Use subagents for research and parallel work as needed.` + + `\n - TASK BOARD FIRST (MANDATORY): Do NOT do substantial work silently or off-board.` + + `\n - Before you start meaningful implementation, debugging, research, review, or follow-up work, make sure there is a visible team-board task for it and that task is assigned to you.` + + `\n - If the user asks for new work, your first move is to create/update the relevant board task(s), then start work from those tasks.` + + `\n - If scope changes mid-task, update the existing task or create a follow-up task before continuing.` + + `\n - If you notice you already began meaningful work without a task, stop, put it on the board, then continue.` + + `\n - Work on tasks directly yourself. Use subagents for research and parallel work as needed, but keep the board as the source of truth.` + `\n - PROGRESS REPORTING (MANDATORY): Since you have no teammates, "user" is your only communication channel.` + `\n - SendMessage "user" at minimum: when you start a task (after marking it in_progress), when you complete a task, and when you hit a meaningful milestone/blocker/decision.` + `\n - Avoid long silent stretches. If something is taking longer than expected, send a brief update and the next step.` + @@ -741,6 +746,7 @@ function buildProvisioningPrompt(request: TeamCreateRequest): string { const step3Block = isSolo ? `3) If user instructions describe work to be done — create tasks on the team board and assign each task to yourself (“${leadName}”) as owner.\n` + ` - Prefer fewer, broader tasks over many micro-tasks.\n` + + ` - Every substantial item that may be worked later must exist on the board; do NOT keep implicit/off-board work.\n` + ` - CRITICAL: Do NOT start working on the tasks now. Provisioning is ONLY for setting up the team structure.\n` + ` - The tasks will be executed after the team is launched separately.` : `3) If user instructions explicitly ask to create tasks OR describe substantial/assigned work that should be tracked — create tasks on the team board. @@ -844,6 +850,7 @@ function buildLaunchPrompt( - Execute tasks sequentially and keep the board + user updated: - Identify the next READY task (pending, not blocked by incomplete dependencies). - If the task is unassigned, set yourself ("${leadName}") as owner. + - If the work you are about to do is not represented on the board yet, create/update the task first before continuing. - BEFORE doing any work on a task: mark it started (in_progress). - Immediately SendMessage "user" that you started task # (what you're doing + next step). - While working: after each meaningful milestone/decision/blocker, add a task comment on #. If the milestone is user-relevant, also SendMessage "user". @@ -3646,10 +3653,29 @@ export class TeamProvisioningService { * Used for both pre-ready (provisioning) and post-ready assistant text. * Emits a coalesced `lead-message` event for renderer refresh. */ - private pushLiveLeadTextMessage(run: ProvisioningRun, cleanText: string): void { + private getStableLeadThoughtMessageId(msg: Record): string | null { + const entryUuid = typeof msg.uuid === 'string' ? msg.uuid.trim() : ''; + if (entryUuid) { + return `lead-thought-${entryUuid}`; + } + + const message = (msg.message ?? msg) as Record; + const assistantMessageId = typeof message.id === 'string' ? message.id.trim() : ''; + if (assistantMessageId) { + return `lead-thought-msg-${assistantMessageId}`; + } + + return null; + } + + private pushLiveLeadTextMessage( + run: ProvisioningRun, + cleanText: string, + stableMessageId?: string + ): void { run.leadMsgSeq += 1; const leadName = this.getRunLeadName(run); - const messageId = `lead-turn-${run.runId}-${run.leadMsgSeq}`; + const messageId = stableMessageId || `lead-turn-${run.runId}-${run.leadMsgSeq}`; // Attach accumulated tool call details from preceding tool_use messages, then reset. const toolCalls = run.pendingToolCalls.length > 0 ? [...run.pendingToolCalls] : undefined; const toolSummary = toolCalls ? formatToolSummaryFromCalls(toolCalls) : undefined; @@ -3778,7 +3804,11 @@ export class TeamProvisioningService { ) { const cleanText = stripAgentBlocks(text).trim(); if (cleanText.length > 0) { - this.pushLiveLeadTextMessage(run, cleanText); + this.pushLiveLeadTextMessage( + run, + cleanText, + this.getStableLeadThoughtMessageId(msg) ?? undefined + ); } } } else { @@ -3787,7 +3817,11 @@ export class TeamProvisioningService { if (!run.silentUserDmForward && !hasCapturedSendMessage) { const cleanText = stripAgentBlocks(text).trim(); if (cleanText.length > 0) { - this.pushLiveLeadTextMessage(run, cleanText); + this.pushLiveLeadTextMessage( + run, + cleanText, + this.getStableLeadThoughtMessageId(msg) ?? undefined + ); } } } diff --git a/src/renderer/components/chat/UserChatGroup.tsx b/src/renderer/components/chat/UserChatGroup.tsx index 3d3b34eb..6cf59051 100644 --- a/src/renderer/components/chat/UserChatGroup.tsx +++ b/src/renderer/components/chat/UserChatGroup.tsx @@ -9,7 +9,7 @@ import { useTheme } from '@renderer/hooks/useTheme'; import { useStore } from '@renderer/store'; import { REHYPE_PLUGINS } from '@renderer/utils/markdownPlugins'; import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; -import { linkifyMentionsInMarkdown } from '@renderer/utils/mentionLinkify'; +import { linkifyAllMentionsInMarkdown } from '@renderer/utils/mentionLinkify'; import { stripAgentBlocks } from '@shared/constants/agentBlocks'; import { createLogger } from '@shared/utils/logger'; import { format } from 'date-fns'; @@ -394,6 +394,13 @@ const UserChatGroupInner = ({ userGroup }: Readonly): React. [members] ); + // Get team names for @team linkification + const teams = useStore((s) => s.teams); + const teamNames = useMemo( + () => teams.filter((t) => !t.deletedAt).map((t) => t.teamName), + [teams] + ); + // Get search state for highlighting const { searchQuery, searchMatches, currentSearchIndex } = useStore( useShallow((s) => ({ @@ -490,8 +497,8 @@ const UserChatGroupInner = ({ userGroup }: Readonly): React. // Pre-process: convert @memberName to mention:// markdown links const displayText = useMemo( - () => linkifyMentionsInMarkdown(baseDisplayText, memberColorMap), - [baseDisplayText, memberColorMap] + () => linkifyAllMentionsInMarkdown(baseDisplayText, memberColorMap, teamNames), + [baseDisplayText, memberColorMap, teamNames] ); return ( diff --git a/src/renderer/components/chat/items/TeammateMessageItem.tsx b/src/renderer/components/chat/items/TeammateMessageItem.tsx index da48665f..212de247 100644 --- a/src/renderer/components/chat/items/TeammateMessageItem.tsx +++ b/src/renderer/components/chat/items/TeammateMessageItem.tsx @@ -13,7 +13,7 @@ import { useStore } from '@renderer/store'; import { detectOperationalNoise } from '@renderer/utils/agentMessageFormatting'; import { formatTokensCompact } from '@renderer/utils/formatters'; import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; -import { linkifyMentionsInMarkdown } from '@renderer/utils/mentionLinkify'; +import { linkifyAllMentionsInMarkdown } from '@renderer/utils/mentionLinkify'; import { stripAgentBlocks } from '@shared/constants/agentBlocks'; import { extractMarkdownPlainText } from '@shared/utils/markdownTextSearch'; import { format } from 'date-fns'; @@ -91,6 +91,13 @@ export const TeammateMessageItem: React.FC = ({ [members] ); + // Get team names for @team linkification + const teams = useStore((s) => s.teams); + const teamNames = useMemo( + () => teams.filter((t) => !t.deletedAt).map((t) => t.teamName), + [teams] + ); + // Detect operational noise const noiseLabel = useMemo( () => detectOperationalNoise(teammateMessage.content, teammateMessage.teammateId), @@ -114,8 +121,8 @@ export const TeammateMessageItem: React.FC = ({ const displayContent = useMemo(() => { const stripped = stripAgentBlocks(teammateMessage.content); - return linkifyMentionsInMarkdown(stripped, memberColorMap); - }, [teammateMessage.content, memberColorMap]); + return linkifyAllMentionsInMarkdown(stripped, memberColorMap, teamNames); + }, [teammateMessage.content, memberColorMap, teamNames]); // Noise: minimal inline row (no card, no expand) if (noiseLabel) { diff --git a/src/renderer/components/chat/viewers/MarkdownViewer.tsx b/src/renderer/components/chat/viewers/MarkdownViewer.tsx index aec3ac8e..c8b85363 100644 --- a/src/renderer/components/chat/viewers/MarkdownViewer.tsx +++ b/src/renderer/components/chat/viewers/MarkdownViewer.tsx @@ -246,31 +246,6 @@ function createViewerMarkdownComponents( } return badge; } - if (href?.startsWith('team://')) { - let teamName = ''; - try { - teamName = decodeURIComponent(href.slice('team://'.length)); - } catch { - // malformed percent-encoding - } - return ( - - - {children} - - ); - } if (href?.startsWith('task://')) { const taskId = href.slice('task://'.length); return ( diff --git a/src/renderer/components/settings/SettingsTabs.tsx b/src/renderer/components/settings/SettingsTabs.tsx index ddffa206..4e4b4ed3 100644 --- a/src/renderer/components/settings/SettingsTabs.tsx +++ b/src/renderer/components/settings/SettingsTabs.tsx @@ -19,7 +19,7 @@ interface TabConfig { const tabs: TabConfig[] = [ { id: 'general', label: 'General', icon: Settings }, - { id: 'connection', label: 'Connection', icon: Server, electronOnly: true }, + // { id: 'connection', label: 'Connection', icon: Server, electronOnly: true }, { id: 'notifications', label: 'Notifications', icon: Bell }, { id: 'advanced', label: 'Advanced', icon: Wrench }, ]; diff --git a/src/renderer/components/sidebar/GlobalTaskList.tsx b/src/renderer/components/sidebar/GlobalTaskList.tsx index afa747da..ad7bb37f 100644 --- a/src/renderer/components/sidebar/GlobalTaskList.tsx +++ b/src/renderer/components/sidebar/GlobalTaskList.tsx @@ -28,6 +28,7 @@ import { } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; +import { AnimatedHeightReveal } from '../team/activity/AnimatedHeightReveal'; import { Combobox, type ComboboxOption } from '../ui/combobox'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; @@ -513,15 +514,16 @@ export const GlobalTaskList = ({ onRename={() => setRenamingTaskKey(`${task.teamName}:${task.id}`)} onDelete={() => handleDeleteTask(task.teamName, task.id)} > - taskLocalState.getRenamedSubject(t.teamName, t.id)} - /> + + taskLocalState.getRenamedSubject(t.teamName, t.id)} + /> + ))} @@ -611,15 +613,16 @@ export const GlobalTaskList = ({ onRename={() => setRenamingTaskKey(`${task.teamName}:${task.id}`)} onDelete={() => handleDeleteTask(task.teamName, task.id)} > - taskLocalState.getRenamedSubject(t.teamName, t.id)} - /> + + taskLocalState.getRenamedSubject(t.teamName, t.id)} + /> + ))} @@ -677,17 +680,18 @@ export const GlobalTaskList = ({ onRename={() => setRenamingTaskKey(`${task.teamName}:${task.id}`)} onDelete={() => handleDeleteTask(task.teamName, task.id)} > - - taskLocalState.getRenamedSubject(t.teamName, t.id) - } - /> + + + taskLocalState.getRenamedSubject(t.teamName, t.id) + } + /> + ); @@ -744,16 +748,17 @@ export const GlobalTaskList = ({ onRename={() => setRenamingTaskKey(`${task.teamName}:${task.id}`)} onDelete={() => handleDeleteTask(task.teamName, task.id)} > - - taskLocalState.getRenamedSubject(t.teamName, t.id) - } - /> + + + taskLocalState.getRenamedSubject(t.teamName, t.id) + } + /> + ); diff --git a/src/renderer/components/sidebar/SidebarTaskItem.tsx b/src/renderer/components/sidebar/SidebarTaskItem.tsx index 018172b6..dc017ec9 100644 --- a/src/renderer/components/sidebar/SidebarTaskItem.tsx +++ b/src/renderer/components/sidebar/SidebarTaskItem.tsx @@ -58,8 +58,6 @@ interface SidebarTaskItemProps { task: GlobalTask; hideTeamName?: boolean; showTeamName?: boolean; - /** When true, the item plays an enter animation */ - isNew?: boolean; /** The composite key "teamName:taskId" of the task being renamed, or null */ renamingKey?: string | null; /** Called when rename is completed with Enter or blur */ @@ -74,7 +72,6 @@ export const SidebarTaskItem = ({ task, hideTeamName, showTeamName, - isNew, renamingKey, onRenameComplete, onRenameCancel, @@ -147,12 +144,10 @@ export const SidebarTaskItem = ({ const showTeamRow = showTeamName && !hideTeamName; - const enterClass = isNew ? 'task-item-enter-animate' : ''; - return ( + ) : !isFolder && onOpenInEditor ? (