diff --git a/README.md b/README.md
index 4fe5f5f0..9838c690 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
-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;
}
// =============================================================================