Merge branch agent_teams_features into main
This commit is contained in:
commit
41717c5c7e
17 changed files with 609 additions and 281 deletions
376
package.json
376
package.json
|
|
@ -1,197 +1,195 @@
|
||||||
{
|
{
|
||||||
"name": "claude-agent-teams-ui",
|
"name": "claude-agent-teams-ui",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Desktop app that visualizes Claude Code session execution — explore conversations, track context usage, and analyze tool calls",
|
"description": "Desktop app that visualizes Claude Code session execution — explore conversations, track context usage, and analyze tool calls",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Илия (777genius)",
|
"name": "Илия (777genius)",
|
||||||
"email": "quantjumppro@gmail.com"
|
"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"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"homepage": "https://github.com/777genius/claude_agent_teams_ui",
|
||||||
"out/renderer/**",
|
"repository": {
|
||||||
"dist-electron/**",
|
"type": "git",
|
||||||
"package.json"
|
"url": "https://github.com/777genius/claude_agent_teams_ui.git"
|
||||||
],
|
|
||||||
"asar": true,
|
|
||||||
"asarUnpack": [
|
|
||||||
"out/renderer/**"
|
|
||||||
],
|
|
||||||
"npmRebuild": false,
|
|
||||||
"extraMetadata": {
|
|
||||||
"main": "dist-electron/main/index.cjs"
|
|
||||||
},
|
},
|
||||||
"mac": {
|
"bugs": {
|
||||||
"category": "public.app-category.developer-tools",
|
"url": "https://github.com/777genius/claude_agent_teams_ui/issues"
|
||||||
"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": {
|
"main": "dist-electron/main/index.cjs",
|
||||||
"sign": false
|
"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": {
|
"dependencies": {
|
||||||
"target": [
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"nsis"
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
],
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"icon": "resources/icons/win/icon.ico"
|
"@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": {
|
"devDependencies": {
|
||||||
"target": [
|
"@eslint-community/eslint-plugin-eslint-comments": "^4.6.0",
|
||||||
"AppImage",
|
"@eslint/js": "^9.39.2",
|
||||||
"deb",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"rpm",
|
"@types/hast": "^3.0.4",
|
||||||
"pacman"
|
"@types/mdast": "^4.0.4",
|
||||||
],
|
"@types/node": "^25.0.7",
|
||||||
"icon": "resources/icons/png",
|
"@types/react": "^18.3.3",
|
||||||
"category": "Development"
|
"@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": {
|
"build": {
|
||||||
"afterInstall": "resources/afterInstall.sh"
|
"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": {
|
"packageManager": "pnpm@10.25.0+sha512.5e82639027af37cf832061bcc6d639c219634488e0f2baebe785028a793de7b525ffcd3f7ff574f5e9860654e098fe852ba8ac5dd5cefe1767d23a020a92f501"
|
||||||
"oneClick": false,
|
}
|
||||||
"perMachine": false,
|
|
||||||
"allowToChangeInstallationDirectory": true
|
|
||||||
},
|
|
||||||
"publish": [
|
|
||||||
{
|
|
||||||
"provider": "github",
|
|
||||||
"releaseType": "draft"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"packageManager": "pnpm@10.25.0+sha512.5e82639027af37cf832061bcc6d639c219634488e0f2baebe785028a793de7b525ffcd3f7ff574f5e9860654e098fe852ba8ac5dd5cefe1767d23a020a92f501"
|
|
||||||
}
|
|
||||||
|
|
@ -769,7 +769,8 @@ async function handleGetMemberLogs(
|
||||||
async function handleGetLogsForTask(
|
async function handleGetLogsForTask(
|
||||||
_event: IpcMainInvokeEvent,
|
_event: IpcMainInvokeEvent,
|
||||||
teamName: unknown,
|
teamName: unknown,
|
||||||
taskId: unknown
|
taskId: unknown,
|
||||||
|
options?: { owner?: string; status?: string }
|
||||||
): Promise<IpcResult<MemberLogSummary[]>> {
|
): Promise<IpcResult<MemberLogSummary[]>> {
|
||||||
const vTeam = validateTeamName(teamName);
|
const vTeam = validateTeamName(teamName);
|
||||||
if (!vTeam.valid) {
|
if (!vTeam.valid) {
|
||||||
|
|
@ -779,8 +780,15 @@ async function handleGetLogsForTask(
|
||||||
if (!vTask.valid) {
|
if (!vTask.valid) {
|
||||||
return { success: false, error: vTask.error ?? 'Invalid taskId' };
|
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', () =>
|
return wrapTeamHandler('getLogsForTask', () =>
|
||||||
getTeamMemberLogsFinder().findLogsForTask(vTeam.value!, vTask.value!)
|
getTeamMemberLogsFinder().findLogsForTask(vTeam.value!, vTask.value!, opts)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,14 @@ export class TeamMemberLogsFinder {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns session logs that reference the given task (TaskCreate, TaskUpdate, comments, etc.).
|
* 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<MemberLogSummary[]> {
|
async findLogsForTask(
|
||||||
|
teamName: string,
|
||||||
|
taskId: string,
|
||||||
|
options?: { owner?: string; status?: string }
|
||||||
|
): Promise<MemberLogSummary[]> {
|
||||||
const discovery = await this.discoverProjectSessions(teamName);
|
const discovery = await this.discoverProjectSessions(teamName);
|
||||||
if (!discovery) return [];
|
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<string>();
|
||||||
|
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(
|
return results.sort(
|
||||||
(a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
|
(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<boolean> {
|
private async fileMentionsTaskId(filePath: string, taskId: string): Promise<boolean> {
|
||||||
const escaped = taskId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
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(`"task_id"\\s*:\\s*"${escaped}"`, 'i'),
|
||||||
new RegExp(`"taskId"\\s*:\\s*"${escaped}"`, 'i'),
|
new RegExp(`"taskId"\\s*:\\s*"${escaped}"`, 'i'),
|
||||||
new RegExp(`#${escaped}\\b`),
|
new RegExp(`#${escaped}\\b`),
|
||||||
];
|
];
|
||||||
|
if (numericTaskId) {
|
||||||
|
patterns.push(
|
||||||
|
new RegExp(`"task_id"\\s*:\\s*${numericTaskId}\\b`),
|
||||||
|
new RegExp(`"taskId"\\s*:\\s*${numericTaskId}\\b`)
|
||||||
|
);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
||||||
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
||||||
|
|
|
||||||
|
|
@ -573,8 +573,17 @@ const electronAPI: ElectronAPI = {
|
||||||
getMemberLogs: async (teamName: string, memberName: string) => {
|
getMemberLogs: async (teamName: string, memberName: string) => {
|
||||||
return invokeIpcWithResult<MemberLogSummary[]>(TEAM_GET_MEMBER_LOGS, teamName, memberName);
|
return invokeIpcWithResult<MemberLogSummary[]>(TEAM_GET_MEMBER_LOGS, teamName, memberName);
|
||||||
},
|
},
|
||||||
getLogsForTask: async (teamName: string, taskId: string) => {
|
getLogsForTask: async (
|
||||||
return invokeIpcWithResult<MemberLogSummary[]>(TEAM_GET_LOGS_FOR_TASK, teamName, taskId);
|
teamName: string,
|
||||||
|
taskId: string,
|
||||||
|
options?: { owner?: string; status?: string }
|
||||||
|
) => {
|
||||||
|
return invokeIpcWithResult<MemberLogSummary[]>(
|
||||||
|
TEAM_GET_LOGS_FOR_TASK,
|
||||||
|
teamName,
|
||||||
|
taskId,
|
||||||
|
options
|
||||||
|
);
|
||||||
},
|
},
|
||||||
getMemberStats: async (teamName: string, memberName: string) => {
|
getMemberStats: async (teamName: string, memberName: string) => {
|
||||||
return invokeIpcWithResult<MemberFullStats>(TEAM_GET_MEMBER_STATS, teamName, memberName);
|
return invokeIpcWithResult<MemberFullStats>(TEAM_GET_MEMBER_STATS, teamName, memberName);
|
||||||
|
|
|
||||||
88
src/renderer/components/team/ProvisioningProgressBlock.tsx
Normal file
88
src/renderer/components/team/ProvisioningProgressBlock.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-md border border-[var(--color-border)] bg-[var(--color-surface-raised)] px-3 py-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
|
{loading ? (
|
||||||
|
<Loader2 className="size-3.5 shrink-0 animate-spin text-[var(--color-text-muted)]" />
|
||||||
|
) : null}
|
||||||
|
<p className="text-xs font-medium text-[var(--color-text)]">{title}</p>
|
||||||
|
</div>
|
||||||
|
{onCancel ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 shrink-0 px-2 text-xs"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{message ? <p className="mt-1.5 text-xs text-[var(--color-text-muted)]">{message}</p> : null}
|
||||||
|
<div className="mt-2 flex items-center gap-1 overflow-x-auto pb-0.5">
|
||||||
|
{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 (
|
||||||
|
<div key={step} className="flex items-center gap-1">
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className={cn(
|
||||||
|
'whitespace-nowrap px-2 py-0.5 text-[11px] font-normal',
|
||||||
|
isDone && 'border-emerald-400/60 bg-emerald-500/10 text-emerald-200',
|
||||||
|
isCurrent &&
|
||||||
|
'border-[var(--color-accent)]/70 bg-[var(--color-accent)]/15 text-[var(--color-text)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="mr-1 inline-flex size-4 items-center justify-center rounded-full border border-current text-[10px]">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
{STEP_LABELS[step]}
|
||||||
|
</Badge>
|
||||||
|
{index < STEP_ORDER.filter((s) => s !== 'ready').length - 1 ? (
|
||||||
|
<span className="text-[var(--color-text-muted)]">→</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -9,6 +9,7 @@ import { buildTaskCountsByOwner } from '@renderer/utils/pathNormalize';
|
||||||
import { MessageSquare, Pencil, Play, Plus, Search, Trash2, X } from 'lucide-react';
|
import { MessageSquare, Pencil, Play, Plus, Search, Trash2, X } from 'lucide-react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
|
import { ActiveTasksBlock } from './activity/ActiveTasksBlock';
|
||||||
import { ActivityTimeline } from './activity/ActivityTimeline';
|
import { ActivityTimeline } from './activity/ActivityTimeline';
|
||||||
import { CreateTaskDialog } from './dialogs/CreateTaskDialog';
|
import { CreateTaskDialog } from './dialogs/CreateTaskDialog';
|
||||||
import { EditTeamDialog } from './dialogs/EditTeamDialog';
|
import { EditTeamDialog } from './dialogs/EditTeamDialog';
|
||||||
|
|
@ -661,6 +662,12 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<ActiveTasksBlock
|
||||||
|
members={data.members}
|
||||||
|
tasks={data.tasks}
|
||||||
|
onMemberClick={setSelectedMember}
|
||||||
|
onTaskClick={setSelectedTask}
|
||||||
|
/>
|
||||||
<ActivityTimeline
|
<ActivityTimeline
|
||||||
messages={filteredMessages}
|
messages={filteredMessages}
|
||||||
members={data.members}
|
members={data.members}
|
||||||
|
|
@ -678,6 +685,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
|
|
||||||
<ReviewDialog
|
<ReviewDialog
|
||||||
open={requestChangesTaskId !== null}
|
open={requestChangesTaskId !== null}
|
||||||
|
teamName={teamName}
|
||||||
taskId={requestChangesTaskId}
|
taskId={requestChangesTaskId}
|
||||||
onCancel={() => setRequestChangesTaskId(null)}
|
onCancel={() => setRequestChangesTaskId(null)}
|
||||||
onSubmit={(comment) => {
|
onSubmit={(comment) => {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Badge } from '@renderer/components/ui/badge';
|
|
||||||
import { Button } from '@renderer/components/ui/button';
|
import { Button } from '@renderer/components/ui/button';
|
||||||
import { cn } from '@renderer/lib/utils';
|
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import { CheckCircle2, X } from 'lucide-react';
|
import { CheckCircle2, X } from 'lucide-react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
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 { ProvisioningStep } from './provisioningSteps';
|
||||||
import type { TeamProvisioningProgress } from '@shared/types';
|
import type { TeamProvisioningProgress } from '@shared/types';
|
||||||
|
|
@ -137,51 +136,20 @@ export const TeamProvisioningBanner = ({
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 rounded-md border border-[var(--color-border)] bg-[var(--color-surface-raised)] px-3 py-2">
|
<div className="mb-3">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<ProvisioningProgressBlock
|
||||||
<p className="text-xs text-[var(--color-text-muted)]">{progress.message}</p>
|
title="Launching team"
|
||||||
{canCancel ? (
|
message={progress.message}
|
||||||
<Button
|
currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1}
|
||||||
variant="outline"
|
loading
|
||||||
size="sm"
|
onCancel={
|
||||||
className="h-6 shrink-0 px-2 text-xs"
|
canCancel
|
||||||
onClick={() => {
|
? () => {
|
||||||
void cancelProvisioning(progress.runId);
|
void cancelProvisioning(progress.runId);
|
||||||
}}
|
}
|
||||||
>
|
: null
|
||||||
Cancel
|
}
|
||||||
</Button>
|
/>
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 flex items-center gap-1 overflow-x-auto pb-0.5">
|
|
||||||
{STEP_ORDER.map((step, index) => {
|
|
||||||
const isDone = progressStepIndex >= 0 && index < progressStepIndex;
|
|
||||||
const isCurrent = progressStepIndex >= 0 && index === progressStepIndex;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={step} className="flex items-center gap-1">
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className={cn(
|
|
||||||
'whitespace-nowrap px-2 py-0.5 text-[11px] font-normal',
|
|
||||||
isDone && 'border-emerald-400/60 bg-emerald-500/10 text-emerald-200',
|
|
||||||
|
|
||||||
isCurrent &&
|
|
||||||
'border-[var(--color-accent)]/70 bg-[var(--color-accent)]/15 text-[var(--color-text)]'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="mr-1 inline-flex size-4 items-center justify-center rounded-full border border-current text-[10px]">
|
|
||||||
{index + 1}
|
|
||||||
</span>
|
|
||||||
{STEP_LABELS[step]}
|
|
||||||
</Badge>
|
|
||||||
{index < STEP_ORDER.length - 1 ? (
|
|
||||||
<span className="text-[var(--color-text-muted)]">→</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
121
src/renderer/components/team/activity/ActiveTasksBlock.tsx
Normal file
121
src/renderer/components/team/activity/ActiveTasksBlock.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className="mb-3 space-y-1.5">
|
||||||
|
<p className="text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
||||||
|
Сейчас в работе
|
||||||
|
</p>
|
||||||
|
{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 (
|
||||||
|
<article
|
||||||
|
key={`${member.name}-${taskId}`}
|
||||||
|
className="overflow-hidden rounded-md"
|
||||||
|
style={{
|
||||||
|
backgroundColor: CARD_BG,
|
||||||
|
border: CARD_BORDER_STYLE,
|
||||||
|
borderLeft: `3px solid ${colors.border}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2">
|
||||||
|
<span className="relative flex size-2 shrink-0">
|
||||||
|
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-70" />
|
||||||
|
<span className="relative inline-flex size-2 rounded-full bg-emerald-500" />
|
||||||
|
</span>
|
||||||
|
<Loader2
|
||||||
|
className="size-3.5 shrink-0 animate-spin"
|
||||||
|
style={{ color: colors.border }}
|
||||||
|
/>
|
||||||
|
{onMemberClick ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide transition-opacity hover:opacity-90 focus:outline-none focus:ring-1 focus:ring-[var(--color-border)]"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.badge,
|
||||||
|
color: colors.text,
|
||||||
|
border: `1px solid ${colors.border}40`,
|
||||||
|
}}
|
||||||
|
onClick={() => onMemberClick(member)}
|
||||||
|
>
|
||||||
|
{member.name}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.badge,
|
||||||
|
color: colors.text,
|
||||||
|
border: `1px solid ${colors.border}40`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{member.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{roleLabel ? (
|
||||||
|
<span className="text-[10px]" style={{ color: CARD_ICON_MUTED }}>
|
||||||
|
{roleLabel}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<span
|
||||||
|
className="min-w-0 flex-1 truncate text-[10px]"
|
||||||
|
style={{ color: CARD_ICON_MUTED }}
|
||||||
|
>
|
||||||
|
выполняет
|
||||||
|
</span>
|
||||||
|
{task &&
|
||||||
|
(onTaskClick ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="truncate rounded px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-text)] transition-opacity hover:opacity-90 focus:outline-none focus:ring-1 focus:ring-[var(--color-border)]"
|
||||||
|
style={{ border: `1px solid ${colors.border}40` }}
|
||||||
|
onClick={() => onTaskClick(task)}
|
||||||
|
title={task.subject}
|
||||||
|
>
|
||||||
|
#{task.id} {task.subject.slice(0, 40)}
|
||||||
|
{task.subject.length > 40 ? '…' : ''}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="truncate px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-text)]"
|
||||||
|
style={{ border: `1px solid ${colors.border}40` }}
|
||||||
|
title={task.subject}
|
||||||
|
>
|
||||||
|
#{task.id} {task.subject.slice(0, 40)}
|
||||||
|
{task.subject.length > 40 ? '…' : ''}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -4,9 +4,14 @@ import type { ParsedMessageReply } from '@renderer/utils/agentMessageFormatting'
|
||||||
|
|
||||||
interface ReplyQuoteBlockProps {
|
interface ReplyQuoteBlockProps {
|
||||||
reply: ParsedMessageReply;
|
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 => (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div
|
<div
|
||||||
className="rounded-md border-l-2 border-[var(--color-border-emphasis)] bg-[var(--color-surface)] px-3 py-2"
|
className="rounded-md border-l-2 border-[var(--color-border-emphasis)] bg-[var(--color-surface)] px-3 py-2"
|
||||||
|
|
@ -17,6 +22,6 @@ export const ReplyQuoteBlock = ({ reply }: ReplyQuoteBlockProps): React.JSX.Elem
|
||||||
</span>
|
</span>
|
||||||
<p className="line-clamp-3 text-xs text-[var(--color-text-muted)]">{reply.originalText}</p>
|
<p className="line-clamp-3 text-xs text-[var(--color-text-muted)]">{reply.originalText}</p>
|
||||||
</div>
|
</div>
|
||||||
<MarkdownViewer content={reply.replyText} maxHeight="max-h-56" copyable />
|
<MarkdownViewer content={reply.replyText} maxHeight={bodyMaxHeight} copyable />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { api } from '@renderer/api';
|
import { api } from '@renderer/api';
|
||||||
import { Button } from '@renderer/components/ui/button';
|
import { Button } from '@renderer/components/ui/button';
|
||||||
|
|
@ -13,10 +13,12 @@ import {
|
||||||
} from '@renderer/components/ui/dialog';
|
} from '@renderer/components/ui/dialog';
|
||||||
import { Input } from '@renderer/components/ui/input';
|
import { Input } from '@renderer/components/ui/input';
|
||||||
import { Label } from '@renderer/components/ui/label';
|
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 { cn } from '@renderer/lib/utils';
|
||||||
import { Check, CheckCircle2, Loader2 } from 'lucide-react';
|
import { Check, CheckCircle2, Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { MentionSuggestion } from '@renderer/types/mention';
|
||||||
import type { Project, TeamLaunchRequest, TeamProvisioningPrepareResult } from '@shared/types';
|
import type { Project, TeamLaunchRequest, TeamProvisioningPrepareResult } from '@shared/types';
|
||||||
|
|
||||||
interface LaunchTeamDialogProps {
|
interface LaunchTeamDialogProps {
|
||||||
|
|
@ -72,7 +74,7 @@ export const LaunchTeamDialog = ({
|
||||||
const [cwdMode, setCwdMode] = useState<'project' | 'custom'>('project');
|
const [cwdMode, setCwdMode] = useState<'project' | 'custom'>('project');
|
||||||
const [selectedProjectPath, setSelectedProjectPath] = useState('');
|
const [selectedProjectPath, setSelectedProjectPath] = useState('');
|
||||||
const [customCwd, setCustomCwd] = useState('');
|
const [customCwd, setCustomCwd] = useState('');
|
||||||
const [prompt, setPrompt] = useState('');
|
const promptDraft = useDraftPersistence({ key: `launchTeam:${teamName}:prompt` });
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [projectsLoading, setProjectsLoading] = useState(false);
|
const [projectsLoading, setProjectsLoading] = useState(false);
|
||||||
const [projectsError, setProjectsError] = useState<string | null>(null);
|
const [projectsError, setProjectsError] = useState<string | null>(null);
|
||||||
|
|
@ -88,7 +90,6 @@ export const LaunchTeamDialog = ({
|
||||||
setPrepareState('idle');
|
setPrepareState('idle');
|
||||||
setPrepareMessage(null);
|
setPrepareMessage(null);
|
||||||
setPrepareWarnings([]);
|
setPrepareWarnings([]);
|
||||||
setPrompt('');
|
|
||||||
setCwdMode('project');
|
setCwdMode('project');
|
||||||
setSelectedProjectPath('');
|
setSelectedProjectPath('');
|
||||||
setCustomCwd('');
|
setCustomCwd('');
|
||||||
|
|
@ -195,6 +196,16 @@ export const LaunchTeamDialog = ({
|
||||||
|
|
||||||
const effectiveCwd = cwdMode === 'project' ? selectedProjectPath.trim() : customCwd.trim();
|
const effectiveCwd = cwdMode === 'project' ? selectedProjectPath.trim() : customCwd.trim();
|
||||||
|
|
||||||
|
const mentionSuggestions = useMemo<MentionSuggestion[]>(
|
||||||
|
() =>
|
||||||
|
projects.map((p) => ({
|
||||||
|
id: p.path,
|
||||||
|
name: p.name,
|
||||||
|
subtitle: p.path,
|
||||||
|
})),
|
||||||
|
[projects]
|
||||||
|
);
|
||||||
|
|
||||||
const activeError = localError ?? provisioningError;
|
const activeError = localError ?? provisioningError;
|
||||||
|
|
||||||
const handleSubmit = (): void => {
|
const handleSubmit = (): void => {
|
||||||
|
|
@ -210,7 +221,7 @@ export const LaunchTeamDialog = ({
|
||||||
await onLaunch({
|
await onLaunch({
|
||||||
teamName,
|
teamName,
|
||||||
cwd: effectiveCwd,
|
cwd: effectiveCwd,
|
||||||
prompt: prompt.trim() || undefined,
|
prompt: promptDraft.value.trim() || undefined,
|
||||||
});
|
});
|
||||||
resetFormState();
|
resetFormState();
|
||||||
onClose();
|
onClose();
|
||||||
|
|
@ -362,12 +373,20 @@ export const LaunchTeamDialog = ({
|
||||||
<Label htmlFor="launch-prompt" className="text-xs text-[var(--color-text-muted)]">
|
<Label htmlFor="launch-prompt" className="text-xs text-[var(--color-text-muted)]">
|
||||||
Prompt (optional)
|
Prompt (optional)
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<MentionableTextarea
|
||||||
id="launch-prompt"
|
id="launch-prompt"
|
||||||
className="min-h-[100px] resize-y text-xs"
|
className="min-h-[100px] text-xs"
|
||||||
value={prompt}
|
minRows={4}
|
||||||
onChange={(event) => setPrompt(event.target.value)}
|
maxRows={12}
|
||||||
placeholder="Instructions for team lead..."
|
value={promptDraft.value}
|
||||||
|
onValueChange={promptDraft.setValue}
|
||||||
|
suggestions={mentionSuggestions}
|
||||||
|
placeholder="Instructions for team lead... Use @ to mention projects."
|
||||||
|
footerRight={
|
||||||
|
promptDraft.isSaved ? (
|
||||||
|
<span className="text-[10px] text-[var(--color-text-muted)]">Draft saved</span>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { Button } from '@renderer/components/ui/button';
|
import { Button } from '@renderer/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -11,9 +9,11 @@ import {
|
||||||
} from '@renderer/components/ui/dialog';
|
} from '@renderer/components/ui/dialog';
|
||||||
import { Label } from '@renderer/components/ui/label';
|
import { Label } from '@renderer/components/ui/label';
|
||||||
import { Textarea } from '@renderer/components/ui/textarea';
|
import { Textarea } from '@renderer/components/ui/textarea';
|
||||||
|
import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence';
|
||||||
|
|
||||||
interface ReviewDialogProps {
|
interface ReviewDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
teamName: string;
|
||||||
taskId: string | null;
|
taskId: string | null;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSubmit: (comment?: string) => void;
|
onSubmit: (comment?: string) => void;
|
||||||
|
|
@ -21,20 +21,23 @@ interface ReviewDialogProps {
|
||||||
|
|
||||||
export const ReviewDialog = ({
|
export const ReviewDialog = ({
|
||||||
open,
|
open,
|
||||||
|
teamName,
|
||||||
taskId,
|
taskId,
|
||||||
onCancel,
|
onCancel,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: ReviewDialogProps): React.JSX.Element => {
|
}: ReviewDialogProps): React.JSX.Element => {
|
||||||
const [comment, setComment] = useState('');
|
const draft = useDraftPersistence({
|
||||||
|
key: `requestChanges:${teamName}:${taskId ?? ''}`,
|
||||||
|
enabled: Boolean(teamName && taskId),
|
||||||
|
});
|
||||||
|
|
||||||
const handleCancel = (): void => {
|
const handleCancel = (): void => {
|
||||||
setComment('');
|
|
||||||
onCancel();
|
onCancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (): void => {
|
const handleSubmit = (): void => {
|
||||||
const trimmed = comment.trim() || undefined;
|
const trimmed = draft.value.trim() || undefined;
|
||||||
setComment('');
|
draft.clearDraft();
|
||||||
onSubmit(trimmed);
|
onSubmit(trimmed);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -58,10 +61,13 @@ export const ReviewDialog = ({
|
||||||
<Textarea
|
<Textarea
|
||||||
id="review-comment"
|
id="review-comment"
|
||||||
className="min-h-[110px] text-xs"
|
className="min-h-[110px] text-xs"
|
||||||
value={comment}
|
value={draft.value}
|
||||||
placeholder="Describe what needs to change..."
|
placeholder="Describe what needs to change..."
|
||||||
onChange={(event) => setComment(event.target.value)}
|
onChange={(event) => draft.setValue(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
{draft.isSaved ? (
|
||||||
|
<span className="text-[10px] text-[var(--color-text-muted)]">Draft saved</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { buildReplyBlock, parseMessageReply } from '@renderer/utils/agentMessage
|
||||||
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
|
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
|
||||||
import { getModifierKeyName } from '@renderer/utils/keyboardUtils';
|
import { getModifierKeyName } from '@renderer/utils/keyboardUtils';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { MessageSquare, Reply, Send, X } from 'lucide-react';
|
import { ChevronDown, ChevronUp, MessageSquare, Reply, Send, X } from 'lucide-react';
|
||||||
|
|
||||||
import type { MentionSuggestion } from '@renderer/types/mention';
|
import type { MentionSuggestion } from '@renderer/types/mention';
|
||||||
import type { ResolvedTeamMember, TaskComment } from '@shared/types';
|
import type { ResolvedTeamMember, TaskComment } from '@shared/types';
|
||||||
|
|
@ -36,6 +36,16 @@ export const TaskCommentsSection = ({
|
||||||
const commentsRef = useMarkCommentsRead(teamName, taskId, comments);
|
const commentsRef = useMarkCommentsRead(teamName, taskId, comments);
|
||||||
|
|
||||||
const [replyTo, setReplyTo] = useState<{ author: string; text: string } | null>(null);
|
const [replyTo, setReplyTo] = useState<{ author: string; text: string } | null>(null);
|
||||||
|
const [expandedCommentIds, setExpandedCommentIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const toggleCommentExpanded = useCallback((commentId: string) => {
|
||||||
|
setExpandedCommentIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(commentId)) next.delete(commentId);
|
||||||
|
else next.add(commentId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const draft = useDraftPersistence({ key: `taskComment:${teamName}:${taskId}` });
|
const draft = useDraftPersistence({ key: `taskComment:${teamName}:${taskId}` });
|
||||||
|
|
||||||
|
|
@ -106,6 +116,18 @@ export const TaskCommentsSection = ({
|
||||||
: formatDistanceToNow(date, { addSuffix: true });
|
: formatDistanceToNow(date, { addSuffix: true });
|
||||||
})()}
|
})()}
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-0.5 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:text-[var(--color-text-secondary)] group-hover:opacity-100"
|
||||||
|
onClick={() => toggleCommentExpanded(comment.id)}
|
||||||
|
title={expandedCommentIds.has(comment.id) ? 'Свернуть' : 'Развернуть'}
|
||||||
|
>
|
||||||
|
{expandedCommentIds.has(comment.id) ? (
|
||||||
|
<ChevronUp size={12} />
|
||||||
|
) : (
|
||||||
|
<ChevronDown size={12} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ml-auto flex items-center gap-0.5 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:text-[var(--color-text-secondary)] group-hover:opacity-100"
|
className="ml-auto flex items-center gap-0.5 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:text-[var(--color-text-secondary)] group-hover:opacity-100"
|
||||||
|
|
@ -123,10 +145,17 @@ export const TaskCommentsSection = ({
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
{(() => {
|
{(() => {
|
||||||
const reply = parseMessageReply(comment.text);
|
const reply = parseMessageReply(comment.text);
|
||||||
|
const expanded = expandedCommentIds.has(comment.id);
|
||||||
return reply ? (
|
return reply ? (
|
||||||
<ReplyQuoteBlock reply={reply} />
|
<ReplyQuoteBlock
|
||||||
|
reply={reply}
|
||||||
|
bodyMaxHeight={expanded ? undefined : 'max-h-56'}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<MarkdownViewer content={comment.text} maxHeight="max-h-[120px]" />
|
<MarkdownViewer
|
||||||
|
content={comment.text}
|
||||||
|
maxHeight={expanded ? undefined : 'max-h-[120px]'}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
||||||
import { MemberLogsTab } from '@renderer/components/team/members/MemberLogsTab';
|
import { MemberLogsTab } from '@renderer/components/team/members/MemberLogsTab';
|
||||||
import { Badge } from '@renderer/components/ui/badge';
|
import { Badge } from '@renderer/components/ui/badge';
|
||||||
|
|
@ -10,6 +12,7 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@renderer/components/ui/dialog';
|
} from '@renderer/components/ui/dialog';
|
||||||
|
import { markAsRead } from '@renderer/services/commentReadStorage';
|
||||||
import { TASK_STATUS_LABELS, TASK_STATUS_STYLES } from '@renderer/utils/memberHelpers';
|
import { TASK_STATUS_LABELS, TASK_STATUS_STYLES } from '@renderer/utils/memberHelpers';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import {
|
import {
|
||||||
|
|
@ -48,6 +51,19 @@ export const TaskDetailDialog = ({
|
||||||
}: TaskDetailDialogProps): React.JSX.Element => {
|
}: TaskDetailDialogProps): React.JSX.Element => {
|
||||||
const currentTask = task ? (taskMap.get(task.id) ?? task) : null;
|
const currentTask = task ? (taskMap.get(task.id) ?? task) : null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !currentTask) return;
|
||||||
|
const comments = currentTask.comments ?? [];
|
||||||
|
if (comments.length === 0) return;
|
||||||
|
const latest = Math.max(...comments.map((c) => new Date(c.createdAt).getTime()));
|
||||||
|
if (latest > 0) markAsRead(teamName, currentTask.id, latest);
|
||||||
|
}, [open, teamName, currentTask?.id, currentTask?.comments]);
|
||||||
|
|
||||||
|
const handleDependencyClick = (taskId: string): void => {
|
||||||
|
onClose();
|
||||||
|
onScrollToTask?.(taskId);
|
||||||
|
};
|
||||||
|
|
||||||
if (!currentTask) {
|
if (!currentTask) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||||
|
|
@ -66,11 +82,6 @@ export const TaskDetailDialog = ({
|
||||||
const blockedByIds = currentTask.blockedBy?.filter((id) => id.length > 0) ?? [];
|
const blockedByIds = currentTask.blockedBy?.filter((id) => id.length > 0) ?? [];
|
||||||
const blocksIds = currentTask.blocks?.filter((id) => id.length > 0) ?? [];
|
const blocksIds = currentTask.blocks?.filter((id) => id.length > 0) ?? [];
|
||||||
|
|
||||||
const handleDependencyClick = (taskId: string): void => {
|
|
||||||
onClose();
|
|
||||||
onScrollToTask?.(taskId);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||||
<DialogContent className="max-h-[85vh] min-w-0 overflow-y-auto overflow-x-hidden sm:max-w-4xl">
|
<DialogContent className="max-h-[85vh] min-w-0 overflow-y-auto overflow-x-hidden sm:max-w-4xl">
|
||||||
|
|
@ -222,7 +233,12 @@ export const TaskDetailDialog = ({
|
||||||
<h4 className="mb-2 text-xs font-medium text-[var(--color-text-muted)]">
|
<h4 className="mb-2 text-xs font-medium text-[var(--color-text-muted)]">
|
||||||
Execution Logs
|
Execution Logs
|
||||||
</h4>
|
</h4>
|
||||||
<MemberLogsTab teamName={teamName} taskId={currentTask.id} />
|
<MemberLogsTab
|
||||||
|
teamName={teamName}
|
||||||
|
taskId={currentTask.id}
|
||||||
|
taskOwner={currentTask.owner}
|
||||||
|
taskStatus={currentTask.status}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,17 @@ interface MemberLogsTabProps {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
memberName?: string;
|
memberName?: string;
|
||||||
taskId?: string;
|
taskId?: string;
|
||||||
|
/** When viewing task logs: include owner's sessions when task is in_progress */
|
||||||
|
taskOwner?: string;
|
||||||
|
taskStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MemberLogsTab = ({
|
export const MemberLogsTab = ({
|
||||||
teamName,
|
teamName,
|
||||||
memberName,
|
memberName,
|
||||||
taskId,
|
taskId,
|
||||||
|
taskOwner,
|
||||||
|
taskStatus,
|
||||||
}: MemberLogsTabProps): React.JSX.Element => {
|
}: MemberLogsTabProps): React.JSX.Element => {
|
||||||
const [logs, setLogs] = useState<MemberLogSummary[]>([]);
|
const [logs, setLogs] = useState<MemberLogSummary[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -47,7 +52,10 @@ export const MemberLogsTab = ({
|
||||||
}
|
}
|
||||||
const result =
|
const result =
|
||||||
taskId != null
|
taskId != null
|
||||||
? await api.teams.getLogsForTask(teamName, taskId)
|
? await api.teams.getLogsForTask(teamName, taskId, {
|
||||||
|
owner: taskOwner,
|
||||||
|
status: taskStatus,
|
||||||
|
})
|
||||||
: await api.teams.getMemberLogs(teamName, memberName!);
|
: await api.teams.getMemberLogs(teamName, memberName!);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setLogs(result);
|
setLogs(result);
|
||||||
|
|
@ -66,7 +74,7 @@ export const MemberLogsTab = ({
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [teamName, memberName, taskId]);
|
}, [teamName, memberName, taskId, taskOwner, taskStatus]);
|
||||||
|
|
||||||
const handleExpand = useCallback(
|
const handleExpand = useCallback(
|
||||||
async (log: MemberLogSummary) => {
|
async (log: MemberLogSummary) => {
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,10 @@ const badgeVariants = cva(
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
const Badge = ({ className, variant, ...props }: BadgeProps): React.JSX.Element => {
|
const Badge = ({ className, variant, ...props }: BadgeProps): React.JSX.Element => {
|
||||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
return <span className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components -- Standard shadcn export pattern
|
// eslint-disable-next-line react-refresh/only-export-components -- Standard shadcn export pattern
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,13 @@ const DialogContent = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
<div className="sticky top-0 z-10 -mr-6 -mt-6 h-0 w-full overflow-visible">
|
||||||
|
<DialogPrimitive.Close className="absolute right-0 top-0 rounded-sm p-1.5 opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-1 focus:ring-[var(--color-border-emphasis)] disabled:pointer-events-none">
|
||||||
|
<X className="size-4 text-[var(--color-text-muted)]" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</div>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-1 focus:ring-[var(--color-border-emphasis)] disabled:pointer-events-none">
|
|
||||||
<X className="size-4 text-[var(--color-text-muted)]" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -361,7 +361,11 @@ export interface TeamsAPI {
|
||||||
aliveList: () => Promise<string[]>;
|
aliveList: () => Promise<string[]>;
|
||||||
createConfig: (request: TeamCreateConfigRequest) => Promise<void>;
|
createConfig: (request: TeamCreateConfigRequest) => Promise<void>;
|
||||||
getMemberLogs: (teamName: string, memberName: string) => Promise<MemberLogSummary[]>;
|
getMemberLogs: (teamName: string, memberName: string) => Promise<MemberLogSummary[]>;
|
||||||
getLogsForTask: (teamName: string, taskId: string) => Promise<MemberLogSummary[]>;
|
getLogsForTask: (
|
||||||
|
teamName: string,
|
||||||
|
taskId: string,
|
||||||
|
options?: { owner?: string; status?: string }
|
||||||
|
) => Promise<MemberLogSummary[]>;
|
||||||
getMemberStats: (teamName: string, memberName: string) => Promise<MemberFullStats>;
|
getMemberStats: (teamName: string, memberName: string) => Promise<MemberFullStats>;
|
||||||
launchTeam: (request: TeamLaunchRequest) => Promise<TeamLaunchResponse>;
|
launchTeam: (request: TeamLaunchRequest) => Promise<TeamLaunchResponse>;
|
||||||
getAllTasks: () => Promise<GlobalTask[]>;
|
getAllTasks: () => Promise<GlobalTask[]>;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue