diff --git a/package.json b/package.json index 8a25762f..7eaa6de2 100644 --- a/package.json +++ b/package.json @@ -1,197 +1,195 @@ { - "name": "claude-agent-teams-ui", - "type": "module", - "version": "0.1.0", - "description": "Desktop app that visualizes Claude Code session execution — explore conversations, track context usage, and analyze tool calls", - "license": "AGPL-3.0", - "author": { - "name": "Илия (777genius)", - "email": "quantjumppro@gmail.com" - }, - "homepage": "https://github.com/777genius/claude_agent_teams_ui", - "repository": { - "type": "git", - "url": "https://github.com/777genius/claude_agent_teams_ui.git" - }, - "bugs": { - "url": "https://github.com/777genius/claude_agent_teams_ui/issues" - }, - "main": "dist-electron/main/index.cjs", - "scripts": { - "dev": "trap 'kill 0' INT; electron-vite dev", - "dev:kill": "pkill -f 'electron-vite|electron \\.' 2>/dev/null; echo 'Done'", - "build": "electron-vite build", - "dist": "electron-builder --mac --win --linux", - "dist:mac": "electron-builder --mac --publish always", - "dist:mac:arm64": "electron-builder --mac --arm64 --publish always", - "dist:mac:x64": "electron-builder --mac --x64 --publish always", - "dist:win": "electron-builder --win --publish always", - "dist:linux": "electron-builder --linux --publish always", - "preview": "electron-vite preview", - "typecheck": "tsc --noEmit", - "lint": "eslint src/", - "lint:fix": "eslint src/ --fix", - "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"", - "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css}\"", - "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm build", - "fix": "pnpm lint:fix && pnpm format", - "quality": "pnpm check && pnpm format:check && npx knip", - "test:chunks": "tsx test/test-chunk-building.ts", - "test:semantic": "tsx test/test-semantic-steps.ts", - "test:noise": "tsx test/test-noise-filtering.ts", - "test:task-filtering": "tsx test/test-task-filtering.ts", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", - "test:coverage:critical": "vitest run --coverage --config vitest.critical.config.ts", - "standalone": "tsx src/main/standalone.ts", - "standalone:build": "electron-vite build && vite build --config vite.standalone.config.ts", - "standalone:start": "node dist-standalone/index.cjs" - }, - "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@fastify/cors": "^11.2.0", - "@fastify/static": "^9.0.0", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-tooltip": "^1.2.8", - "@tanstack/react-virtual": "^3.10.8", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "1.0.4", - "date-fns": "^3.6.0", - "electron-updater": "^6.7.3", - "fastify": "^5.7.4", - "idb-keyval": "^6.2.2", - "lucide-react": "^0.562.0", - "mdast-util-to-hast": "^13.2.1", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-markdown": "^10.1.0", - "remark-gfm": "^4.0.1", - "remark-parse": "^11.0.0", - "ssh-config": "^5.0.4", - "ssh2": "^1.17.0", - "tailwind-merge": "^3.5.0", - "tailwindcss-animate": "^1.0.7", - "unified": "^11.0.5", - "zustand": "^4.5.0" - }, - "devDependencies": { - "@eslint-community/eslint-plugin-eslint-comments": "^4.6.0", - "@eslint/js": "^9.39.2", - "@tailwindcss/typography": "^0.5.19", - "@types/hast": "^3.0.4", - "@types/mdast": "^4.0.4", - "@types/node": "^25.0.7", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@types/ssh2": "^1.15.5", - "@vitejs/plugin-react": "^4.3.1", - "@vitest/coverage-v8": "^3.1.4", - "autoprefixer": "^10.4.17", - "electron": "^40.3.0", - "electron-builder": "^25.1.8", - "electron-vite": "^2.3.0", - "eslint": "^9.39.2", - "eslint-config-prettier": "^10.1.8", - "eslint-import-resolver-typescript": "^4.4.4", - "eslint-plugin-boundaries": "^5.3.1", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.26", - "eslint-plugin-security": "^3.0.1", - "eslint-plugin-simple-import-sort": "^12.1.1", - "eslint-plugin-sonarjs": "^3.0.6", - "eslint-plugin-tailwindcss": "^3.18.2", - "globals": "^17.2.0", - "happy-dom": "^20.0.2", - "knip": "^5.82.1", - "postcss": "^8.4.35", - "prettier": "^3.8.1", - "prettier-plugin-tailwindcss": "^0.7.2", - "tailwindcss": "^3.4.1", - "tsx": "^4.21.0", - "typescript": "^5.9.3", - "typescript-eslint": "^8.54.0", - "vite": "^5.4.2", - "vitest": "^3.1.4" - }, - "build": { - "appId": "com.claudecode.context", - "productName": "Claude Agent Teams UI", - "directories": { - "output": "release" + "name": "claude-agent-teams-ui", + "type": "module", + "version": "0.1.0", + "description": "Desktop app that visualizes Claude Code session execution — explore conversations, track context usage, and analyze tool calls", + "license": "AGPL-3.0", + "author": { + "name": "Илия (777genius)", + "email": "quantjumppro@gmail.com" }, - "files": [ - "out/renderer/**", - "dist-electron/**", - "package.json" - ], - "asar": true, - "asarUnpack": [ - "out/renderer/**" - ], - "npmRebuild": false, - "extraMetadata": { - "main": "dist-electron/main/index.cjs" + "homepage": "https://github.com/777genius/claude_agent_teams_ui", + "repository": { + "type": "git", + "url": "https://github.com/777genius/claude_agent_teams_ui.git" }, - "mac": { - "category": "public.app-category.developer-tools", - "target": [ - "dmg", - "zip" - ], - "hardenedRuntime": true, - "gatekeeperAssess": false, - "notarize": true, - "entitlements": "resources/entitlements.mac.plist", - "entitlementsInherit": "resources/entitlements.mac.inherit.plist", - "icon": "resources/icons/mac/icon.icns" + "bugs": { + "url": "https://github.com/777genius/claude_agent_teams_ui/issues" }, - "dmg": { - "sign": false + "main": "dist-electron/main/index.cjs", + "scripts": { + "dev": "electron-vite dev", + "dev:kill": "pkill -f 'electron-vite|electron \\.' 2>/dev/null; echo 'Done'", + "build": "electron-vite build", + "dist": "electron-builder --mac --win --linux", + "dist:mac": "electron-builder --mac --publish always", + "dist:mac:arm64": "electron-builder --mac --arm64 --publish always", + "dist:mac:x64": "electron-builder --mac --x64 --publish always", + "dist:win": "electron-builder --win --publish always", + "dist:linux": "electron-builder --linux --publish always", + "preview": "electron-vite preview", + "typecheck": "tsc --noEmit", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css}\"", + "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm build", + "fix": "pnpm lint:fix && pnpm format", + "quality": "pnpm check && pnpm format:check && npx knip", + "test:chunks": "tsx test/test-chunk-building.ts", + "test:semantic": "tsx test/test-semantic-steps.ts", + "test:noise": "tsx test/test-noise-filtering.ts", + "test:task-filtering": "tsx test/test-task-filtering.ts", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:coverage:critical": "vitest run --coverage --config vitest.critical.config.ts", + "standalone": "tsx src/main/standalone.ts", + "standalone:build": "electron-vite build && vite build --config vite.standalone.config.ts", + "standalone:start": "node dist-standalone/index.cjs" }, - "win": { - "target": [ - "nsis" - ], - "icon": "resources/icons/win/icon.ico" + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@fastify/cors": "^11.2.0", + "@fastify/static": "^9.0.0", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-virtual": "^3.10.8", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "1.0.4", + "date-fns": "^3.6.0", + "electron-updater": "^6.7.3", + "fastify": "^5.7.4", + "idb-keyval": "^6.2.2", + "lucide-react": "^0.562.0", + "mdast-util-to-hast": "^13.2.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "ssh-config": "^5.0.4", + "ssh2": "^1.17.0", + "tailwind-merge": "^3.5.0", + "tailwindcss-animate": "^1.0.7", + "unified": "^11.0.5", + "zustand": "^4.5.0" }, - "linux": { - "target": [ - "AppImage", - "deb", - "rpm", - "pacman" - ], - "icon": "resources/icons/png", - "category": "Development" + "devDependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.6.0", + "@eslint/js": "^9.39.2", + "@tailwindcss/typography": "^0.5.19", + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "@types/node": "^25.0.7", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/ssh2": "^1.15.5", + "@vitejs/plugin-react": "^4.3.1", + "@vitest/coverage-v8": "^3.1.4", + "autoprefixer": "^10.4.17", + "electron": "^40.3.0", + "electron-builder": "^25.1.8", + "electron-vite": "^2.3.0", + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-boundaries": "^5.3.1", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.26", + "eslint-plugin-security": "^3.0.1", + "eslint-plugin-simple-import-sort": "^12.1.1", + "eslint-plugin-sonarjs": "^3.0.6", + "eslint-plugin-tailwindcss": "^3.18.2", + "globals": "^17.2.0", + "happy-dom": "^20.0.2", + "knip": "^5.82.1", + "postcss": "^8.4.35", + "prettier": "^3.8.1", + "prettier-plugin-tailwindcss": "^0.7.2", + "tailwindcss": "^3.4.1", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.54.0", + "vite": "^5.4.2", + "vitest": "^3.1.4" }, - "deb": { - "afterInstall": "resources/afterInstall.sh" + "build": { + "appId": "com.claudecode.context", + "productName": "Claude Agent Teams UI", + "directories": { + "output": "release" + }, + "files": [ + "out/renderer/**", + "dist-electron/**", + "package.json" + ], + "asar": true, + "asarUnpack": [ + "out/renderer/**" + ], + "npmRebuild": false, + "extraMetadata": { + "main": "dist-electron/main/index.cjs" + }, + "mac": { + "category": "public.app-category.developer-tools", + "target": [ + "dmg", + "zip" + ], + "hardenedRuntime": true, + "gatekeeperAssess": false, + "notarize": true, + "entitlements": "resources/entitlements.mac.plist", + "entitlementsInherit": "resources/entitlements.mac.inherit.plist", + "icon": "resources/icons/mac/icon.icns" + }, + "dmg": { + "sign": false + }, + "win": { + "target": [ + "nsis" + ], + "icon": "resources/icons/win/icon.ico" + }, + "linux": { + "target": [ + "AppImage", + "deb", + "rpm", + "pacman" + ], + "icon": "resources/icons/png", + "category": "Development" + }, + "deb": { + "afterInstall": "resources/afterInstall.sh" + }, + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true + }, + "publish": [{ + "provider": "github", + "releaseType": "draft" + }] }, - "nsis": { - "oneClick": false, - "perMachine": false, - "allowToChangeInstallationDirectory": true - }, - "publish": [ - { - "provider": "github", - "releaseType": "draft" - } - ] - }, - "packageManager": "pnpm@10.25.0+sha512.5e82639027af37cf832061bcc6d639c219634488e0f2baebe785028a793de7b525ffcd3f7ff574f5e9860654e098fe852ba8ac5dd5cefe1767d23a020a92f501" -} + "packageManager": "pnpm@10.25.0+sha512.5e82639027af37cf832061bcc6d639c219634488e0f2baebe785028a793de7b525ffcd3f7ff574f5e9860654e098fe852ba8ac5dd5cefe1767d23a020a92f501" +} \ No newline at end of file diff --git a/src/main/ipc/teams.ts b/src/main/ipc/teams.ts index 0d6212c4..b309b77f 100644 --- a/src/main/ipc/teams.ts +++ b/src/main/ipc/teams.ts @@ -769,7 +769,8 @@ async function handleGetMemberLogs( async function handleGetLogsForTask( _event: IpcMainInvokeEvent, teamName: unknown, - taskId: unknown + taskId: unknown, + options?: { owner?: string; status?: string } ): Promise> { const vTeam = validateTeamName(teamName); if (!vTeam.valid) { @@ -779,8 +780,15 @@ async function handleGetLogsForTask( if (!vTask.valid) { return { success: false, error: vTask.error ?? 'Invalid taskId' }; } + const opts = + options && typeof options === 'object' + ? { + owner: typeof options.owner === 'string' ? options.owner : undefined, + status: typeof options.status === 'string' ? options.status : undefined, + } + : undefined; return wrapTeamHandler('getLogsForTask', () => - getTeamMemberLogsFinder().findLogsForTask(vTeam.value!, vTask.value!) + getTeamMemberLogsFinder().findLogsForTask(vTeam.value!, vTask.value!, opts) ); } diff --git a/src/main/services/team/TeamMemberLogsFinder.ts b/src/main/services/team/TeamMemberLogsFinder.ts index bb5ecf15..d6c1251e 100644 --- a/src/main/services/team/TeamMemberLogsFinder.ts +++ b/src/main/services/team/TeamMemberLogsFinder.ts @@ -104,8 +104,14 @@ export class TeamMemberLogsFinder { /** * Returns session logs that reference the given task (TaskCreate, TaskUpdate, comments, etc.). + * When the task is in_progress and has an owner, also includes that owner's session logs so + * the executor's current activity is visible even before the JSONL mentions the task id. */ - async findLogsForTask(teamName: string, taskId: string): Promise { + async findLogsForTask( + teamName: string, + taskId: string, + options?: { owner?: string; status?: string } + ): Promise { const discovery = await this.discoverProjectSessions(teamName); if (!discovery) return []; @@ -159,6 +165,32 @@ export class TeamMemberLogsFinder { } } + const includeOwnerSessions = + options?.status === 'in_progress' && + typeof options?.owner === 'string' && + options.owner.trim().length > 0; + if (includeOwnerSessions) { + const ownerLogs = await this.findMemberLogs(teamName, options.owner!.trim()); + const seen = new Set(); + for (const log of results) { + const key = + log.kind === 'subagent' + ? `subagent:${log.sessionId}:${log.subagentId}` + : `lead:${log.sessionId}`; + seen.add(key); + } + for (const log of ownerLogs) { + const key = + log.kind === 'subagent' + ? `subagent:${log.sessionId}:${log.subagentId}` + : `lead:${log.sessionId}`; + if (!seen.has(key)) { + seen.add(key); + results.push(log); + } + } + } + return results.sort( (a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime() ); @@ -307,11 +339,18 @@ export class TeamMemberLogsFinder { private async fileMentionsTaskId(filePath: string, taskId: string): Promise { const escaped = taskId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const patterns = [ + const numericTaskId = /^\d+$/.test(taskId) ? taskId : null; + const patterns: RegExp[] = [ new RegExp(`"task_id"\\s*:\\s*"${escaped}"`, 'i'), new RegExp(`"taskId"\\s*:\\s*"${escaped}"`, 'i'), new RegExp(`#${escaped}\\b`), ]; + if (numericTaskId) { + patterns.push( + new RegExp(`"task_id"\\s*:\\s*${numericTaskId}\\b`), + new RegExp(`"taskId"\\s*:\\s*${numericTaskId}\\b`) + ); + } try { const stream = createReadStream(filePath, { encoding: 'utf8' }); const rl = readline.createInterface({ input: stream, crlfDelay: Infinity }); diff --git a/src/preload/index.ts b/src/preload/index.ts index ae217b67..60f89524 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -573,8 +573,17 @@ const electronAPI: ElectronAPI = { getMemberLogs: async (teamName: string, memberName: string) => { return invokeIpcWithResult(TEAM_GET_MEMBER_LOGS, teamName, memberName); }, - getLogsForTask: async (teamName: string, taskId: string) => { - return invokeIpcWithResult(TEAM_GET_LOGS_FOR_TASK, teamName, taskId); + getLogsForTask: async ( + teamName: string, + taskId: string, + options?: { owner?: string; status?: string } + ) => { + return invokeIpcWithResult( + TEAM_GET_LOGS_FOR_TASK, + teamName, + taskId, + options + ); }, getMemberStats: async (teamName: string, memberName: string) => { return invokeIpcWithResult(TEAM_GET_MEMBER_STATS, teamName, memberName); diff --git a/src/renderer/components/team/ProvisioningProgressBlock.tsx b/src/renderer/components/team/ProvisioningProgressBlock.tsx new file mode 100644 index 00000000..91f656b5 --- /dev/null +++ b/src/renderer/components/team/ProvisioningProgressBlock.tsx @@ -0,0 +1,88 @@ +import { Badge } from '@renderer/components/ui/badge'; +import { Button } from '@renderer/components/ui/button'; +import { cn } from '@renderer/lib/utils'; +import { Loader2 } from 'lucide-react'; + +import { STEP_LABELS, STEP_ORDER } from './provisioningSteps'; + +import type { ProvisioningStep } from './provisioningSteps'; + +export interface ProvisioningProgressBlockProps { + /** Title above the steps, e.g. "Launching team" */ + title: string; + /** Optional status message */ + message?: string | null; + /** Index of the current step in STEP_ORDER (0-based), or -1 if unknown */ + currentStepIndex: number; + /** Show spinner next to title */ + loading?: boolean; + /** Cancel button label and handler */ + onCancel?: (() => void) | null; + className?: string; +} + +export const ProvisioningProgressBlock = ({ + title, + message, + currentStepIndex, + loading = false, + onCancel, + className, +}: ProvisioningProgressBlockProps): React.JSX.Element => { + return ( +
+
+
+ {loading ? ( + + ) : null} +

{title}

+
+ {onCancel ? ( + + ) : null} +
+ {message ?

{message}

: null} +
+ {STEP_ORDER.filter((s): s is ProvisioningStep => s !== 'ready').map((step, index) => { + const isDone = currentStepIndex >= 0 && index < currentStepIndex; + const isCurrent = currentStepIndex >= 0 && index === currentStepIndex; + + return ( +
+ + + {index + 1} + + {STEP_LABELS[step]} + + {index < STEP_ORDER.filter((s) => s !== 'ready').length - 1 ? ( + + ) : null} +
+ ); + })} +
+
+ ); +}; diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index acb2da32..7044239f 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -9,6 +9,7 @@ import { buildTaskCountsByOwner } from '@renderer/utils/pathNormalize'; import { MessageSquare, Pencil, Play, Plus, Search, Trash2, X } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; +import { ActiveTasksBlock } from './activity/ActiveTasksBlock'; import { ActivityTimeline } from './activity/ActivityTimeline'; import { CreateTaskDialog } from './dialogs/CreateTaskDialog'; import { EditTeamDialog } from './dialogs/EditTeamDialog'; @@ -661,6 +662,12 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele } > + setRequestChangesTaskId(null)} onSubmit={(comment) => { diff --git a/src/renderer/components/team/TeamProvisioningBanner.tsx b/src/renderer/components/team/TeamProvisioningBanner.tsx index 092c8725..8aff6cea 100644 --- a/src/renderer/components/team/TeamProvisioningBanner.tsx +++ b/src/renderer/components/team/TeamProvisioningBanner.tsx @@ -1,13 +1,12 @@ import { useEffect, useRef, useState } from 'react'; -import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; -import { cn } from '@renderer/lib/utils'; import { useStore } from '@renderer/store'; import { CheckCircle2, X } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; -import { STEP_LABELS, STEP_ORDER } from './provisioningSteps'; +import { ProvisioningProgressBlock } from './ProvisioningProgressBlock'; +import { STEP_ORDER } from './provisioningSteps'; import type { ProvisioningStep } from './provisioningSteps'; import type { TeamProvisioningProgress } from '@shared/types'; @@ -137,51 +136,20 @@ export const TeamProvisioningBanner = ({ if (isActive) { return ( -
-
-

{progress.message}

- {canCancel ? ( - - ) : null} -
-
- {STEP_ORDER.map((step, index) => { - const isDone = progressStepIndex >= 0 && index < progressStepIndex; - const isCurrent = progressStepIndex >= 0 && index === progressStepIndex; - - return ( -
- - - {index + 1} - - {STEP_LABELS[step]} - - {index < STEP_ORDER.length - 1 ? ( - - ) : null} -
- ); - })} -
+
+ = 0 ? progressStepIndex : -1} + loading + onCancel={ + canCancel + ? () => { + void cancelProvisioning(progress.runId); + } + : null + } + />
); } diff --git a/src/renderer/components/team/activity/ActiveTasksBlock.tsx b/src/renderer/components/team/activity/ActiveTasksBlock.tsx new file mode 100644 index 00000000..7a375d73 --- /dev/null +++ b/src/renderer/components/team/activity/ActiveTasksBlock.tsx @@ -0,0 +1,121 @@ +import { CARD_BG, CARD_BORDER_STYLE, CARD_ICON_MUTED } from '@renderer/constants/cssVariables'; +import { getTeamColorSet } from '@renderer/constants/teamColors'; +import { formatAgentRole } from '@renderer/utils/formatAgentRole'; +import { Loader2 } from 'lucide-react'; + +import type { ResolvedTeamMember, TeamTask } from '@shared/types'; + +interface ActiveTasksBlockProps { + members: ResolvedTeamMember[]; + tasks: TeamTask[]; + onMemberClick?: (member: ResolvedTeamMember) => void; + onTaskClick?: (task: TeamTask) => void; +} + +export const ActiveTasksBlock = ({ + members, + tasks, + onMemberClick, + onTaskClick, +}: ActiveTasksBlockProps): React.JSX.Element | null => { + const taskMap = new Map(tasks.map((t) => [t.id, t])); + const working = members.filter((m) => m.currentTaskId != null); + if (working.length === 0) return null; + + return ( +
+

+ Сейчас в работе +

+ {working.map((member) => { + const taskId = member.currentTaskId!; + const task = taskMap.get(taskId); + const colors = getTeamColorSet(member.color ?? ''); + const roleLabel = formatAgentRole( + member.role ?? (member.agentType !== 'general-purpose' ? member.agentType : undefined) + ); + + return ( +
+
+ + + + + + {onMemberClick ? ( + + ) : ( + + {member.name} + + )} + {roleLabel ? ( + + {roleLabel} + + ) : null} + + выполняет + + {task && + (onTaskClick ? ( + + ) : ( + + #{task.id} {task.subject.slice(0, 40)} + {task.subject.length > 40 ? '…' : ''} + + ))} +
+
+ ); + })} +
+ ); +}; diff --git a/src/renderer/components/team/activity/ReplyQuoteBlock.tsx b/src/renderer/components/team/activity/ReplyQuoteBlock.tsx index 46b450f2..e8206e4a 100644 --- a/src/renderer/components/team/activity/ReplyQuoteBlock.tsx +++ b/src/renderer/components/team/activity/ReplyQuoteBlock.tsx @@ -4,9 +4,14 @@ import type { ParsedMessageReply } from '@renderer/utils/agentMessageFormatting' interface ReplyQuoteBlockProps { reply: ParsedMessageReply; + /** When set, limits height of the reply body (e.g. "max-h-56"). Omit to show full content. */ + bodyMaxHeight?: string; } -export const ReplyQuoteBlock = ({ reply }: ReplyQuoteBlockProps): React.JSX.Element => ( +export const ReplyQuoteBlock = ({ + reply, + bodyMaxHeight = 'max-h-56', +}: ReplyQuoteBlockProps): React.JSX.Element => (

{reply.originalText}

- +
); diff --git a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx index 466b6174..aa89d793 100644 --- a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +++ b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { api } from '@renderer/api'; import { Button } from '@renderer/components/ui/button'; @@ -13,10 +13,12 @@ import { } from '@renderer/components/ui/dialog'; import { Input } from '@renderer/components/ui/input'; import { Label } from '@renderer/components/ui/label'; -import { Textarea } from '@renderer/components/ui/textarea'; +import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea'; +import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence'; import { cn } from '@renderer/lib/utils'; import { Check, CheckCircle2, Loader2 } from 'lucide-react'; +import type { MentionSuggestion } from '@renderer/types/mention'; import type { Project, TeamLaunchRequest, TeamProvisioningPrepareResult } from '@shared/types'; interface LaunchTeamDialogProps { @@ -72,7 +74,7 @@ export const LaunchTeamDialog = ({ const [cwdMode, setCwdMode] = useState<'project' | 'custom'>('project'); const [selectedProjectPath, setSelectedProjectPath] = useState(''); const [customCwd, setCustomCwd] = useState(''); - const [prompt, setPrompt] = useState(''); + const promptDraft = useDraftPersistence({ key: `launchTeam:${teamName}:prompt` }); const [projects, setProjects] = useState([]); const [projectsLoading, setProjectsLoading] = useState(false); const [projectsError, setProjectsError] = useState(null); @@ -88,7 +90,6 @@ export const LaunchTeamDialog = ({ setPrepareState('idle'); setPrepareMessage(null); setPrepareWarnings([]); - setPrompt(''); setCwdMode('project'); setSelectedProjectPath(''); setCustomCwd(''); @@ -195,6 +196,16 @@ export const LaunchTeamDialog = ({ const effectiveCwd = cwdMode === 'project' ? selectedProjectPath.trim() : customCwd.trim(); + const mentionSuggestions = useMemo( + () => + projects.map((p) => ({ + id: p.path, + name: p.name, + subtitle: p.path, + })), + [projects] + ); + const activeError = localError ?? provisioningError; const handleSubmit = (): void => { @@ -210,7 +221,7 @@ export const LaunchTeamDialog = ({ await onLaunch({ teamName, cwd: effectiveCwd, - prompt: prompt.trim() || undefined, + prompt: promptDraft.value.trim() || undefined, }); resetFormState(); onClose(); @@ -362,12 +373,20 @@ export const LaunchTeamDialog = ({ -