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:
parent
0876f192fa
commit
c326f8f96e
15 changed files with 168 additions and 116 deletions
|
|
@ -10,7 +10,7 @@
|
|||
<a href="docs/screenshots/6.png"><img src="docs/screenshots/6.png" width="65" alt="Settings" /></a>
|
||||
</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">
|
||||
<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 |
BIN
public/demo.mp4
BIN
public/demo.mp4
Binary file not shown.
BIN
public/noti.mp4
BIN
public/noti.mp4
Binary file not shown.
|
|
@ -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<ApiKeyLookupResult[]>(API_KEYS_LOOKUP, envVarNames),
|
||||
getStorageStatus: () => invokeIpcWithResult<ApiKeyStorageStatus>(API_KEYS_STORAGE_STATUS),
|
||||
},
|
||||
|
||||
getPathForFile: (file: File) => webUtils.getPathForFile(file),
|
||||
};
|
||||
|
||||
// Use contextBridge to securely expose the API to the renderer process
|
||||
|
|
|
|||
|
|
@ -1175,4 +1175,6 @@ export class HttpAPIClient implements ElectronAPI {
|
|||
return () => {};
|
||||
},
|
||||
};
|
||||
|
||||
getPathForFile = (_file: File): string => '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<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 min-w-0 flex-1 items-center gap-2">
|
||||
{loading ? (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<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
|
||||
key={progress.runId}
|
||||
title="Launch details"
|
||||
|
|
@ -138,6 +126,8 @@ export const TeamProvisioningBanner = ({
|
|||
assistantOutput={progress.assistantOutput}
|
||||
defaultLiveOutputOpen={false}
|
||||
onCancel={null}
|
||||
successMessage={readyMessage}
|
||||
onDismiss={() => setDismissed(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export const ReviewDialog = ({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-4xl">
|
||||
<DialogContent className="w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Request Changes</DialogTitle>
|
||||
<DialogDescription>Task #{taskId ? deriveTaskDisplayId(taskId) : ''}</DialogDescription>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -412,8 +412,6 @@ export const MessageComposer = ({
|
|||
onDrop={handleDropWrapper}
|
||||
onPaste={handlePasteWrapper}
|
||||
>
|
||||
<DropZoneOverlay active={isDragOver} rejected={!supportsAttachments} />
|
||||
|
||||
<div className="mb-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{isLeadRecipient ? (
|
||||
|
|
@ -842,103 +840,106 @@ export const MessageComposer = ({
|
|||
) : null}
|
||||
</div>
|
||||
|
||||
<MentionableTextarea
|
||||
ref={textareaRef}
|
||||
id={`compose-${teamName}`}
|
||||
placeholder={
|
||||
isProvisioning
|
||||
? 'Team is launching... message will be queued for inbox delivery.'
|
||||
: isCrossTeam
|
||||
? `Cross-team message to ${targetDisplayName ?? 'team'}...`
|
||||
: 'Write a message... (Enter to send, Shift+Enter for new line)'
|
||||
}
|
||||
value={draft.text}
|
||||
onValueChange={draft.setText}
|
||||
suggestions={mentionSuggestions}
|
||||
teamSuggestions={teamMentionSuggestions}
|
||||
taskSuggestions={taskSuggestions}
|
||||
chips={draft.chips}
|
||||
onChipRemove={draft.removeChip}
|
||||
projectPath={projectPath}
|
||||
onFileChipInsert={draft.addChip}
|
||||
onModEnter={handleSend}
|
||||
onShiftTab={handleCycleActionMode}
|
||||
dismissMentionsRef={dismissMentionsRef}
|
||||
minRows={2}
|
||||
maxRows={6}
|
||||
maxLength={MAX_TEXT_LENGTH}
|
||||
disabled={sending}
|
||||
hintText={crossTeamHintText}
|
||||
cornerActionLeft={
|
||||
<ActionModeSelector
|
||||
value={actionMode}
|
||||
onChange={setActionMode}
|
||||
showDelegate={canDelegate}
|
||||
/>
|
||||
}
|
||||
cornerAction={
|
||||
<div className="flex items-center gap-2">
|
||||
{/* NOTE: ContextRing disabled — usage formula is inaccurate */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
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">
|
||||
<div className="relative">
|
||||
<DropZoneOverlay active={isDragOver} rejected={!supportsAttachments} />
|
||||
<MentionableTextarea
|
||||
ref={textareaRef}
|
||||
id={`compose-${teamName}`}
|
||||
placeholder={
|
||||
isProvisioning
|
||||
? 'Team is launching... message will be queued for inbox delivery.'
|
||||
: isCrossTeam
|
||||
? `Cross-team message to ${targetDisplayName ?? 'team'}...`
|
||||
: 'Write a message... (Enter to send, Shift+Enter for new line)'
|
||||
}
|
||||
value={draft.text}
|
||||
onValueChange={draft.setText}
|
||||
suggestions={mentionSuggestions}
|
||||
teamSuggestions={teamMentionSuggestions}
|
||||
taskSuggestions={taskSuggestions}
|
||||
chips={draft.chips}
|
||||
onChipRemove={draft.removeChip}
|
||||
projectPath={projectPath}
|
||||
onFileChipInsert={draft.addChip}
|
||||
onModEnter={handleSend}
|
||||
onShiftTab={handleCycleActionMode}
|
||||
dismissMentionsRef={dismissMentionsRef}
|
||||
minRows={2}
|
||||
maxRows={6}
|
||||
maxLength={MAX_TEXT_LENGTH}
|
||||
disabled={sending}
|
||||
hintText={crossTeamHintText}
|
||||
cornerActionLeft={
|
||||
<ActionModeSelector
|
||||
value={actionMode}
|
||||
onChange={setActionMode}
|
||||
showDelegate={canDelegate}
|
||||
/>
|
||||
}
|
||||
cornerAction={
|
||||
<div className="flex items-center gap-2">
|
||||
{/* NOTE: ContextRing disabled — usage formula is inaccurate */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<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}
|
||||
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')}
|
||||
>
|
||||
<Send size={12} />
|
||||
Send
|
||||
<Mic size={14} />
|
||||
</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>
|
||||
</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>
|
||||
) : 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>
|
||||
}
|
||||
/>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue