diff --git a/README.md b/README.md index 4fe5f5f0..9838c690 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Settings

-

Claude Agent Teams UI

+

Claude Agent Teams UI

You're the CTO, agents are your team. They handle tasks themselves, message each other, review each other. You just look at the kanban board and drink coffee. diff --git a/public/compact.mp4 b/public/compact.mp4 deleted file mode 100644 index bfe810a7..00000000 Binary files a/public/compact.mp4 and /dev/null differ diff --git a/public/context.png b/public/context.png deleted file mode 100644 index be428201..00000000 Binary files a/public/context.png and /dev/null differ diff --git a/public/demo.mp4 b/public/demo.mp4 deleted file mode 100644 index 8de39842..00000000 Binary files a/public/demo.mp4 and /dev/null differ diff --git a/public/noti.mp4 b/public/noti.mp4 deleted file mode 100644 index 9c0ef58a..00000000 Binary files a/public/noti.mp4 and /dev/null differ diff --git a/src/preload/index.ts b/src/preload/index.ts index 9b360223..3aa647db 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,5 +1,5 @@ import { WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL } from '@shared/constants'; -import { contextBridge, ipcRenderer } from 'electron'; +import { contextBridge, ipcRenderer, webUtils } from 'electron'; import { API_KEYS_DELETE, @@ -1490,6 +1490,8 @@ const electronAPI: ElectronAPI = { invokeIpcWithResult(API_KEYS_LOOKUP, envVarNames), getStorageStatus: () => invokeIpcWithResult(API_KEYS_STORAGE_STATUS), }, + + getPathForFile: (file: File) => webUtils.getPathForFile(file), }; // Use contextBridge to securely expose the API to the renderer process diff --git a/src/renderer/api/httpClient.ts b/src/renderer/api/httpClient.ts index b515456d..a73fd8cb 100644 --- a/src/renderer/api/httpClient.ts +++ b/src/renderer/api/httpClient.ts @@ -1175,4 +1175,6 @@ export class HttpAPIClient implements ElectronAPI { return () => {}; }, }; + + getPathForFile = (_file: File): string => ''; } diff --git a/src/renderer/components/team/ProvisioningProgressBlock.tsx b/src/renderer/components/team/ProvisioningProgressBlock.tsx index 732598f4..56a10e07 100644 --- a/src/renderer/components/team/ProvisioningProgressBlock.tsx +++ b/src/renderer/components/team/ProvisioningProgressBlock.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { Button } from '@renderer/components/ui/button'; import { cn } from '@renderer/lib/utils'; -import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react'; +import { CheckCircle2, ChevronDown, ChevronRight, Loader2, X } from 'lucide-react'; import { MarkdownViewer } from '../chat/viewers/MarkdownViewer'; @@ -37,6 +37,10 @@ export interface ProvisioningProgressBlockProps { loading?: boolean; /** Cancel button label and handler */ onCancel?: (() => void) | null; + /** Success message shown inside the block header (e.g. "Team launched — all N teammates online") */ + successMessage?: string | null; + /** Dismiss handler — renders an X button in the block header top-right */ + onDismiss?: (() => void) | null; /** ISO timestamp when provisioning started */ startedAt?: string; /** PID of the CLI process */ @@ -127,6 +131,8 @@ export const ProvisioningProgressBlock = ({ errorStepIndex, loading = false, onCancel, + successMessage, + onDismiss, startedAt, pid, cliLogsTail, @@ -191,6 +197,33 @@ export const ProvisioningProgressBlock = ({ className )} > + {successMessage ? ( +

+ +

{successMessage}

+ {onDismiss ? ( + + ) : null} +
+ ) : onDismiss ? ( +
+ +
+ ) : null}
{loading ? ( diff --git a/src/renderer/components/team/TeamProvisioningBanner.tsx b/src/renderer/components/team/TeamProvisioningBanner.tsx index 31d560b5..90ef0a5a 100644 --- a/src/renderer/components/team/TeamProvisioningBanner.tsx +++ b/src/renderer/components/team/TeamProvisioningBanner.tsx @@ -3,7 +3,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { Button } from '@renderer/components/ui/button'; import { useStore } from '@renderer/store'; import { getCurrentProvisioningProgressForTeam } from '@renderer/store/slices/teamSlice'; -import { CheckCircle2, X } from 'lucide-react'; +import { X } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { ProvisioningProgressBlock } from './ProvisioningProgressBlock'; @@ -114,18 +114,6 @@ export const TeamProvisioningBanner = ({ return (
-
- -

{readyMessage}

- -
setDismissed(true)} />
); diff --git a/src/renderer/components/team/dialogs/ReviewDialog.tsx b/src/renderer/components/team/dialogs/ReviewDialog.tsx index ad9c6d4e..9e843ef0 100644 --- a/src/renderer/components/team/dialogs/ReviewDialog.tsx +++ b/src/renderer/components/team/dialogs/ReviewDialog.tsx @@ -79,7 +79,7 @@ export const ReviewDialog = ({ } }} > - + Request Changes Task #{taskId ? deriveTaskDisplayId(taskId) : ''} diff --git a/src/renderer/components/team/dialogs/TaskCommentInput.tsx b/src/renderer/components/team/dialogs/TaskCommentInput.tsx index bbf69ae8..6991e4e3 100644 --- a/src/renderer/components/team/dialogs/TaskCommentInput.tsx +++ b/src/renderer/components/team/dialogs/TaskCommentInput.tsx @@ -96,7 +96,12 @@ export const TaskCommentInput = ({ const supported: File[] = []; for (const file of fileArray) { if (categorizeFile(file) === 'unsupported') { - const filePath = (file as { path?: string }).path; + let filePath = ''; + try { + filePath = window.electronAPI.getPathForFile(file); + } catch { + // Clipboard files: no path available + } if (filePath) { const current = draft.value; draft.setValue(current ? filePath + '\n' + current : filePath + '\n'); diff --git a/src/renderer/components/team/members/MemberLogsTab.tsx b/src/renderer/components/team/members/MemberLogsTab.tsx index 1ed8f1a1..03a5359b 100644 --- a/src/renderer/components/team/members/MemberLogsTab.tsx +++ b/src/renderer/components/team/members/MemberLogsTab.tsx @@ -362,10 +362,18 @@ export const MemberLogsTab = ({ const previewHasMore = allPreviewMessages.length > previewVisibleCount; const previewOnline = useMemo((): boolean => { + if (!previewLog) return false; + // Primary signal: the session file is still being written to + if (previewLog.isOngoing) return true; + // Secondary: check message freshness with generous windows const newest = previewMessages[0]; if (!newest) return false; - return Date.now() - newest.timestamp.getTime() <= 10_000; - }, [previewMessages]); + const ageMs = Date.now() - newest.timestamp.getTime(); + // Task actively in progress — agent may pause between visible outputs + if (taskStatus === 'in_progress') return ageMs <= 60_000; + // Completed/other tasks — shorter window + return ageMs <= 15_000; + }, [previewLog, previewMessages, taskStatus]); const expandedLogSummary = useMemo(() => { if (!expandedId) return null; diff --git a/src/renderer/components/team/messages/MessageComposer.tsx b/src/renderer/components/team/messages/MessageComposer.tsx index 2af3535d..1d3a610a 100644 --- a/src/renderer/components/team/messages/MessageComposer.tsx +++ b/src/renderer/components/team/messages/MessageComposer.tsx @@ -412,8 +412,6 @@ export const MessageComposer = ({ onDrop={handleDropWrapper} onPaste={handlePasteWrapper} > - -
{isLeadRecipient ? ( @@ -842,103 +840,106 @@ export const MessageComposer = ({ ) : null}
- - } - cornerAction={ -
- {/* NOTE: ContextRing disabled — usage formula is inaccurate */} - - - - - Voice to text - - - - +
+ + + } + cornerAction={ +
+ {/* NOTE: ContextRing disabled — usage formula is inaccurate */} + + + + Voice to text + + + + + + + + {isProvisioning && !sending ? ( + + Sending unavailable while team is launching + + ) : null} + +
+ } + footerRight={ +
+ {sendError ? ( + + + {sendError} + + ) : lastResult?.deduplicated ? ( + + + Reused recent cross-team request - - {isProvisioning && !sending ? ( - - Sending unavailable while team is launching - ) : null} - -
- } - footerRight={ -
- {sendError ? ( - - - {sendError} - - ) : lastResult?.deduplicated ? ( - - - Reused recent cross-team request - - ) : null} - {remaining < 200 ? ( - - {remaining} chars left - - ) : null} - {draft.isSaved ? ( - Saved - ) : null} -
- } - /> + {remaining < 200 ? ( + + {remaining} chars left + + ) : null} + {draft.isSaved ? ( + Saved + ) : null} +
+ } + /> +
); }; diff --git a/src/renderer/hooks/useComposerDraft.ts b/src/renderer/hooks/useComposerDraft.ts index bfdc2484..9b2eff53 100644 --- a/src/renderer/hooks/useComposerDraft.ts +++ b/src/renderer/hooks/useComposerDraft.ts @@ -331,9 +331,17 @@ export function useComposerDraft(teamName: string): UseComposerDraftResult { const unsupportedPaths: string[] = []; for (const f of fileArray) { if (categorizeFile(f) === 'unsupported') { - const p = (f as { path?: string }).path; - if (p) unsupportedPaths.push(p); - else setAttachmentError(`Unsupported file: ${f.name}`); + let filePath = ''; + try { + filePath = window.electronAPI.getPathForFile(f); + } catch { + // Clipboard files or non-Electron: no path available + } + if (filePath) { + unsupportedPaths.push(filePath); + } else { + setAttachmentError(`Unsupported file: ${f.name}`); + } } else { supported.push(f); } diff --git a/src/shared/types/api.ts b/src/shared/types/api.ts index 14674d56..8451ab8b 100644 --- a/src/shared/types/api.ts +++ b/src/shared/types/api.ts @@ -832,6 +832,9 @@ export interface ElectronAPI { // Extension Store — API Keys Management (Electron-only, optional) apiKeys?: ApiKeysAPI; + + /** Get absolute file path for a File object (works in sandboxed renderers). */ + getPathForFile: (file: File) => string; } // =============================================================================