feat(readme): update project title with link and remove unused media files

- Updated the project title in README.md to include a hyperlink for easier access to the documentation.
- Removed unused media files (compact.mp4, context.png, demo.mp4, noti.mp4) from the public directory to clean up the project.
- Enhanced the preload and renderer components to include a new method for retrieving file paths, improving file handling capabilities.
This commit is contained in:
iliya 2026-03-23 20:18:14 +02:00
parent 0876f192fa
commit c326f8f96e
15 changed files with 168 additions and 116 deletions

View file

@ -10,7 +10,7 @@
<a href="docs/screenshots/6.png"><img src="docs/screenshots/6.png" width="65" alt="Settings" /></a> <a href="docs/screenshots/6.png"><img src="docs/screenshots/6.png" width="65" alt="Settings" /></a>
</p> </p>
<h1 align="center">Claude Agent Teams UI</h1> <h1 align="center"><a href="https://777genius.github.io/claude_agent_teams_ui/">Claude Agent Teams UI</a></h1>
<p align="center"> <p align="center">
<strong><code>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.</code></strong> <strong><code>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.</code></strong>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 722 KiB

Binary file not shown.

Binary file not shown.

View file

@ -1,5 +1,5 @@
import { WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL } from '@shared/constants'; import { WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL } from '@shared/constants';
import { contextBridge, ipcRenderer } from 'electron'; import { contextBridge, ipcRenderer, webUtils } from 'electron';
import { import {
API_KEYS_DELETE, API_KEYS_DELETE,
@ -1490,6 +1490,8 @@ const electronAPI: ElectronAPI = {
invokeIpcWithResult<ApiKeyLookupResult[]>(API_KEYS_LOOKUP, envVarNames), invokeIpcWithResult<ApiKeyLookupResult[]>(API_KEYS_LOOKUP, envVarNames),
getStorageStatus: () => invokeIpcWithResult<ApiKeyStorageStatus>(API_KEYS_STORAGE_STATUS), getStorageStatus: () => invokeIpcWithResult<ApiKeyStorageStatus>(API_KEYS_STORAGE_STATUS),
}, },
getPathForFile: (file: File) => webUtils.getPathForFile(file),
}; };
// Use contextBridge to securely expose the API to the renderer process // Use contextBridge to securely expose the API to the renderer process

View file

@ -1175,4 +1175,6 @@ export class HttpAPIClient implements ElectronAPI {
return () => {}; return () => {};
}, },
}; };
getPathForFile = (_file: File): string => '';
} }

View file

@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { cn } from '@renderer/lib/utils'; 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'; import { MarkdownViewer } from '../chat/viewers/MarkdownViewer';
@ -37,6 +37,10 @@ export interface ProvisioningProgressBlockProps {
loading?: boolean; loading?: boolean;
/** Cancel button label and handler */ /** Cancel button label and handler */
onCancel?: (() => void) | null; 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 */ /** ISO timestamp when provisioning started */
startedAt?: string; startedAt?: string;
/** PID of the CLI process */ /** PID of the CLI process */
@ -127,6 +131,8 @@ export const ProvisioningProgressBlock = ({
errorStepIndex, errorStepIndex,
loading = false, loading = false,
onCancel, onCancel,
successMessage,
onDismiss,
startedAt, startedAt,
pid, pid,
cliLogsTail, cliLogsTail,
@ -191,6 +197,33 @@ export const ProvisioningProgressBlock = ({
className className
)} )}
> >
{successMessage ? (
<div className="mb-1.5 flex items-center gap-2">
<CheckCircle2 size={14} className="shrink-0 text-[var(--step-done-text)]" />
<p className="flex-1 text-xs text-[var(--step-success-text)]">{successMessage}</p>
{onDismiss ? (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 shrink-0 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
onClick={onDismiss}
>
<X size={12} />
</Button>
) : null}
</div>
) : onDismiss ? (
<div className="flex justify-end">
<Button
variant="ghost"
size="sm"
className="h-6 w-6 shrink-0 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
onClick={onDismiss}
>
<X size={12} />
</Button>
</div>
) : null}
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2"> <div className="flex min-w-0 flex-1 items-center gap-2">
{loading ? ( {loading ? (

View file

@ -3,7 +3,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { getCurrentProvisioningProgressForTeam } from '@renderer/store/slices/teamSlice'; 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 { useShallow } from 'zustand/react/shallow';
import { ProvisioningProgressBlock } from './ProvisioningProgressBlock'; import { ProvisioningProgressBlock } from './ProvisioningProgressBlock';
@ -114,18 +114,6 @@ export const TeamProvisioningBanner = ({
return ( return (
<div className="mb-3"> <div className="mb-3">
<div className="flex items-center gap-2 rounded-md border border-[var(--step-done-border)] bg-[var(--step-done-bg)] px-3 py-2">
<CheckCircle2 size={14} className="shrink-0 text-[var(--step-done-text)]" />
<p className="flex-1 text-xs text-[var(--step-success-text)]">{readyMessage}</p>
<Button
variant="outline"
size="sm"
className="h-6 shrink-0 border-[var(--step-done-border)] px-2 text-xs text-[var(--step-done-text)] hover:bg-[var(--step-done-bg)]"
onClick={() => setDismissed(true)}
>
<X size={12} />
</Button>
</div>
<ProvisioningProgressBlock <ProvisioningProgressBlock
key={progress.runId} key={progress.runId}
title="Launch details" title="Launch details"
@ -138,6 +126,8 @@ export const TeamProvisioningBanner = ({
assistantOutput={progress.assistantOutput} assistantOutput={progress.assistantOutput}
defaultLiveOutputOpen={false} defaultLiveOutputOpen={false}
onCancel={null} onCancel={null}
successMessage={readyMessage}
onDismiss={() => setDismissed(true)}
/> />
</div> </div>
); );

View file

@ -79,7 +79,7 @@ export const ReviewDialog = ({
} }
}} }}
> >
<DialogContent className="sm:max-w-4xl"> <DialogContent className="w-[600px]">
<DialogHeader> <DialogHeader>
<DialogTitle>Request Changes</DialogTitle> <DialogTitle>Request Changes</DialogTitle>
<DialogDescription>Task #{taskId ? deriveTaskDisplayId(taskId) : ''}</DialogDescription> <DialogDescription>Task #{taskId ? deriveTaskDisplayId(taskId) : ''}</DialogDescription>

View file

@ -96,7 +96,12 @@ export const TaskCommentInput = ({
const supported: File[] = []; const supported: File[] = [];
for (const file of fileArray) { for (const file of fileArray) {
if (categorizeFile(file) === 'unsupported') { 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) { if (filePath) {
const current = draft.value; const current = draft.value;
draft.setValue(current ? filePath + '\n' + current : filePath + '\n'); draft.setValue(current ? filePath + '\n' + current : filePath + '\n');

View file

@ -362,10 +362,18 @@ export const MemberLogsTab = ({
const previewHasMore = allPreviewMessages.length > previewVisibleCount; const previewHasMore = allPreviewMessages.length > previewVisibleCount;
const previewOnline = useMemo((): boolean => { 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]; const newest = previewMessages[0];
if (!newest) return false; if (!newest) return false;
return Date.now() - newest.timestamp.getTime() <= 10_000; const ageMs = Date.now() - newest.timestamp.getTime();
}, [previewMessages]); // 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(() => { const expandedLogSummary = useMemo(() => {
if (!expandedId) return null; if (!expandedId) return null;

View file

@ -412,8 +412,6 @@ export const MessageComposer = ({
onDrop={handleDropWrapper} onDrop={handleDropWrapper}
onPaste={handlePasteWrapper} onPaste={handlePasteWrapper}
> >
<DropZoneOverlay active={isDragOver} rejected={!supportsAttachments} />
<div className="mb-1 space-y-2"> <div className="mb-1 space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isLeadRecipient ? ( {isLeadRecipient ? (
@ -842,103 +840,106 @@ export const MessageComposer = ({
) : null} ) : null}
</div> </div>
<MentionableTextarea <div className="relative">
ref={textareaRef} <DropZoneOverlay active={isDragOver} rejected={!supportsAttachments} />
id={`compose-${teamName}`} <MentionableTextarea
placeholder={ ref={textareaRef}
isProvisioning id={`compose-${teamName}`}
? 'Team is launching... message will be queued for inbox delivery.' placeholder={
: isCrossTeam isProvisioning
? `Cross-team message to ${targetDisplayName ?? 'team'}...` ? 'Team is launching... message will be queued for inbox delivery.'
: 'Write a message... (Enter to send, Shift+Enter for new line)' : isCrossTeam
} ? `Cross-team message to ${targetDisplayName ?? 'team'}...`
value={draft.text} : 'Write a message... (Enter to send, Shift+Enter for new line)'
onValueChange={draft.setText} }
suggestions={mentionSuggestions} value={draft.text}
teamSuggestions={teamMentionSuggestions} onValueChange={draft.setText}
taskSuggestions={taskSuggestions} suggestions={mentionSuggestions}
chips={draft.chips} teamSuggestions={teamMentionSuggestions}
onChipRemove={draft.removeChip} taskSuggestions={taskSuggestions}
projectPath={projectPath} chips={draft.chips}
onFileChipInsert={draft.addChip} onChipRemove={draft.removeChip}
onModEnter={handleSend} projectPath={projectPath}
onShiftTab={handleCycleActionMode} onFileChipInsert={draft.addChip}
dismissMentionsRef={dismissMentionsRef} onModEnter={handleSend}
minRows={2} onShiftTab={handleCycleActionMode}
maxRows={6} dismissMentionsRef={dismissMentionsRef}
maxLength={MAX_TEXT_LENGTH} minRows={2}
disabled={sending} maxRows={6}
hintText={crossTeamHintText} maxLength={MAX_TEXT_LENGTH}
cornerActionLeft={ disabled={sending}
<ActionModeSelector hintText={crossTeamHintText}
value={actionMode} cornerActionLeft={
onChange={setActionMode} <ActionModeSelector
showDelegate={canDelegate} value={actionMode}
/> onChange={setActionMode}
} showDelegate={canDelegate}
cornerAction={ />
<div className="flex items-center gap-2"> }
{/* NOTE: ContextRing disabled — usage formula is inaccurate */} cornerAction={
<Tooltip> <div className="flex items-center gap-2">
<TooltipTrigger asChild> {/* NOTE: ContextRing disabled — usage formula is inaccurate */}
<button <Tooltip>
type="button" <TooltipTrigger asChild>
className="inline-flex shrink-0 items-center rounded-full p-1.5 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text-secondary)]"
onClick={() => void window.electronAPI.openExternal('https://voicetext.site')}
>
<Mic size={14} />
</button>
</TooltipTrigger>
<TooltipContent side="top">Voice to text</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex">
<button <button
type="button" type="button"
className="inline-flex shrink-0 items-center gap-1 rounded-full bg-blue-600 px-3 py-1.5 text-[11px] font-medium text-white shadow-sm transition-colors hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-50" className="inline-flex shrink-0 items-center rounded-full p-1.5 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text-secondary)]"
disabled={!canSend} onClick={() => void window.electronAPI.openExternal('https://voicetext.site')}
onClick={handleSend}
> >
<Send size={12} /> <Mic size={14} />
Send
</button> </button>
</TooltipTrigger>
<TooltipContent side="top">Voice to text</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex">
<button
type="button"
className="inline-flex shrink-0 items-center gap-1 rounded-full bg-blue-600 px-3 py-1.5 text-[11px] font-medium text-white shadow-sm transition-colors hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-50"
disabled={!canSend}
onClick={handleSend}
>
<Send size={12} />
Send
</button>
</span>
</TooltipTrigger>
{isProvisioning && !sending ? (
<TooltipContent side="top">
Sending unavailable while team is launching
</TooltipContent>
) : null}
</Tooltip>
</div>
}
footerRight={
<div className="flex items-center gap-2">
{sendError ? (
<span className="inline-flex items-center gap-1 rounded bg-red-500/10 px-1.5 py-0.5 text-[10px] text-red-400">
<AlertCircle size={10} className="shrink-0" />
{sendError}
</span>
) : lastResult?.deduplicated ? (
<span className="inline-flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-[10px] text-amber-300">
<Check size={10} className="shrink-0" />
Reused recent cross-team request
</span> </span>
</TooltipTrigger>
{isProvisioning && !sending ? (
<TooltipContent side="top">
Sending unavailable while team is launching
</TooltipContent>
) : null} ) : null}
</Tooltip> {remaining < 200 ? (
</div> <span
} className={`text-[10px] ${remaining < 100 ? 'text-yellow-400' : 'text-[var(--color-text-muted)]'}`}
footerRight={ >
<div className="flex items-center gap-2"> {remaining} chars left
{sendError ? ( </span>
<span className="inline-flex items-center gap-1 rounded bg-red-500/10 px-1.5 py-0.5 text-[10px] text-red-400"> ) : null}
<AlertCircle size={10} className="shrink-0" /> {draft.isSaved ? (
{sendError} <span className="text-[10px] text-[var(--color-text-muted)]">Saved</span>
</span> ) : null}
) : lastResult?.deduplicated ? ( </div>
<span className="inline-flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-[10px] text-amber-300"> }
<Check size={10} className="shrink-0" /> />
Reused recent cross-team request </div>
</span>
) : null}
{remaining < 200 ? (
<span
className={`text-[10px] ${remaining < 100 ? 'text-yellow-400' : 'text-[var(--color-text-muted)]'}`}
>
{remaining} chars left
</span>
) : null}
{draft.isSaved ? (
<span className="text-[10px] text-[var(--color-text-muted)]">Saved</span>
) : null}
</div>
}
/>
</div> </div>
); );
}; };

View file

@ -331,9 +331,17 @@ export function useComposerDraft(teamName: string): UseComposerDraftResult {
const unsupportedPaths: string[] = []; const unsupportedPaths: string[] = [];
for (const f of fileArray) { for (const f of fileArray) {
if (categorizeFile(f) === 'unsupported') { if (categorizeFile(f) === 'unsupported') {
const p = (f as { path?: string }).path; let filePath = '';
if (p) unsupportedPaths.push(p); try {
else setAttachmentError(`Unsupported file: ${f.name}`); 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 { } else {
supported.push(f); supported.push(f);
} }

View file

@ -832,6 +832,9 @@ export interface ElectronAPI {
// Extension Store — API Keys Management (Electron-only, optional) // Extension Store — API Keys Management (Electron-only, optional)
apiKeys?: ApiKeysAPI; apiKeys?: ApiKeysAPI;
/** Get absolute file path for a File object (works in sandboxed renderers). */
getPathForFile: (file: File) => string;
} }
// ============================================================================= // =============================================================================