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>
|
<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 |
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 { 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
|
||||||
|
|
|
||||||
|
|
@ -1175,4 +1175,6 @@ export class HttpAPIClient implements ElectronAPI {
|
||||||
return () => {};
|
return () => {};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getPathForFile = (_file: File): string => '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ? (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue