mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-25 20:25:51 +08:00
fix: stabilize desktop environment auth navigation
This commit is contained in:
@@ -195,8 +195,9 @@ export class DesktopWindowManager {
|
|||||||
this.actions.setActiveTarget(target);
|
this.actions.setActiveTarget(target);
|
||||||
this.buildAppMenu();
|
this.buildAppMenu();
|
||||||
this.mainWindow.setTitle(`${this.appName} - ${target.name}`);
|
this.mainWindow.setTitle(`${this.appName} - ${target.name}`);
|
||||||
await this.showContentTarget(target);
|
const finalUrl = await this.showContentTarget(target);
|
||||||
this.emitDesktopState();
|
this.emitDesktopState();
|
||||||
|
return finalUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
async showLauncher() {
|
async showLauncher() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { app, BrowserWindow, clipboard, dialog, ipcMain, shell, systemPreferences } from 'electron';
|
import { app, BrowserWindow, clipboard, dialog, ipcMain, session, shell, systemPreferences } from 'electron';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -259,6 +259,28 @@ async function getEnvironmentLaunchTarget(environment) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hasCloudWebSession() {
|
||||||
|
const cookies = await session.defaultSession.cookies.get({});
|
||||||
|
return cookies.some((cookie) => {
|
||||||
|
const cookieDomain = String(cookie.domain || '');
|
||||||
|
return cookieDomain.includes('cloudcli.ai')
|
||||||
|
&& /-auth-token(?:\.\d+)?$/.test(cookie.name)
|
||||||
|
&& Boolean(cookie.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCloudAuthRedirect(url) {
|
||||||
|
if (!url) return false;
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const controlPlane = new URL(CLOUDCLI_CONTROL_PLANE_URL);
|
||||||
|
return parsed.origin === controlPlane.origin
|
||||||
|
&& (parsed.pathname === '/login' || parsed.pathname.startsWith('/auth/'));
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getDiagnosticsText() {
|
function getDiagnosticsText() {
|
||||||
const cloudAccount = cloud.getAccount();
|
const cloudAccount = cloud.getAccount();
|
||||||
const localState = getLocalState();
|
const localState = getLocalState();
|
||||||
@@ -676,8 +698,18 @@ async function openEnvironmentInDesktop(environment) {
|
|||||||
nextEnvironment = await cloud.startEnvironmentAndWait(environment, REMOTE_START_TIMEOUT_MS);
|
nextEnvironment = await cloud.startEnvironmentAndWait(environment, REMOTE_START_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = await getEnvironmentLaunchTarget(nextEnvironment);
|
let target = getEnvironmentTarget(nextEnvironment);
|
||||||
await desktopWindow.showTarget(target);
|
if (!(await hasCloudWebSession())) {
|
||||||
|
target = await getEnvironmentLaunchTarget(nextEnvironment);
|
||||||
|
}
|
||||||
|
|
||||||
|
const usedBootstrap = Boolean(target.loadUrl);
|
||||||
|
const finalUrl = await desktopWindow.showTarget(target);
|
||||||
|
if (!usedBootstrap && isCloudAuthRedirect(finalUrl)) {
|
||||||
|
const bootstrapTarget = await getEnvironmentLaunchTarget(nextEnvironment);
|
||||||
|
bootstrapTarget.forceLoad = true;
|
||||||
|
await desktopWindow.showTarget(bootstrapTarget);
|
||||||
|
}
|
||||||
return getDesktopState();
|
return getDesktopState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -233,19 +233,21 @@ export class ViewHost {
|
|||||||
}
|
}
|
||||||
const view = this.getOrCreateTabView(tabId);
|
const view = this.getOrCreateTabView(tabId);
|
||||||
this.attach(view);
|
this.attach(view);
|
||||||
if (view.__cloudcliLoadedUrl !== target.url) {
|
if (target.forceLoad || view.__cloudcliLoadedUrl !== target.url) {
|
||||||
view.__cloudcliLoadingUrl = loadUrl;
|
view.__cloudcliLoadingUrl = loadUrl;
|
||||||
try {
|
try {
|
||||||
await loadUrlWithTimeout(view.webContents, loadUrl);
|
await loadUrlWithTimeout(view.webContents, loadUrl);
|
||||||
view.__cloudcliLoadedUrl = target.url;
|
view.__cloudcliLoadedUrl = target.url;
|
||||||
view.__cloudcliStartupHtml = null;
|
view.__cloudcliStartupHtml = null;
|
||||||
delete target.loadUrl;
|
delete target.loadUrl;
|
||||||
|
delete target.forceLoad;
|
||||||
} finally {
|
} finally {
|
||||||
if (view.__cloudcliLoadingUrl === loadUrl) {
|
if (view.__cloudcliLoadingUrl === loadUrl) {
|
||||||
view.__cloudcliLoadingUrl = null;
|
view.__cloudcliLoadingUrl = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return view.webContents.getURL();
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadTab(tabId) {
|
reloadTab(tabId) {
|
||||||
|
|||||||
62
src/components/chat/utils/toolGrouping.ts
Normal file
62
src/components/chat/utils/toolGrouping.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import type { ChatMessage } from '../types/types';
|
||||||
|
|
||||||
|
export const TOOL_GROUP_THRESHOLD = 3;
|
||||||
|
|
||||||
|
export interface ToolGroupItem {
|
||||||
|
_isGroup: true;
|
||||||
|
toolName: string;
|
||||||
|
messages: ChatMessage[];
|
||||||
|
timestamp: ChatMessage['timestamp'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageListItem = ChatMessage | ToolGroupItem;
|
||||||
|
|
||||||
|
export function isToolGroupItem(item: MessageListItem): item is ToolGroupItem {
|
||||||
|
return '_isGroup' in item && (item as ToolGroupItem)._isGroup === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGroupableToolMessage(message: ChatMessage): message is ChatMessage & { toolName: string } {
|
||||||
|
return Boolean(message.isToolUse && message.toolName && !message.isSubagentContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupConsecutiveTools(messages: ChatMessage[]): MessageListItem[] {
|
||||||
|
const items: MessageListItem[] = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
while (index < messages.length) {
|
||||||
|
const message = messages[index];
|
||||||
|
|
||||||
|
if (!isGroupableToolMessage(message)) {
|
||||||
|
items.push(message);
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const run: ChatMessage[] = [message];
|
||||||
|
let nextIndex = index + 1;
|
||||||
|
|
||||||
|
while (
|
||||||
|
nextIndex < messages.length &&
|
||||||
|
isGroupableToolMessage(messages[nextIndex]) &&
|
||||||
|
messages[nextIndex].toolName === message.toolName
|
||||||
|
) {
|
||||||
|
run.push(messages[nextIndex]);
|
||||||
|
nextIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (run.length >= TOOL_GROUP_THRESHOLD) {
|
||||||
|
items.push({
|
||||||
|
_isGroup: true,
|
||||||
|
toolName: message.toolName,
|
||||||
|
messages: run,
|
||||||
|
timestamp: message.timestamp,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
items.push(...run);
|
||||||
|
}
|
||||||
|
|
||||||
|
index = nextIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
import type { Dispatch, RefObject, SetStateAction } from 'react';
|
import type { Dispatch, RefObject, SetStateAction } from 'react';
|
||||||
|
|
||||||
import type { ChatMessage } from '../../types/types';
|
import type { ChatMessage } from '../../types/types';
|
||||||
@@ -10,9 +10,11 @@ import type {
|
|||||||
ProviderModelsDefinition,
|
ProviderModelsDefinition,
|
||||||
} from '../../../../types/app';
|
} from '../../../../types/app';
|
||||||
import { getIntrinsicMessageKey } from '../../utils/messageKeys';
|
import { getIntrinsicMessageKey } from '../../utils/messageKeys';
|
||||||
|
import { groupConsecutiveTools, isToolGroupItem } from '../../utils/toolGrouping';
|
||||||
|
|
||||||
import MessageComponent from './MessageComponent';
|
import MessageComponent from './MessageComponent';
|
||||||
import ProviderSelectionEmptyState from './ProviderSelectionEmptyState';
|
import ProviderSelectionEmptyState from './ProviderSelectionEmptyState';
|
||||||
|
import ToolGroupContainer from './ToolGroupContainer';
|
||||||
|
|
||||||
interface ChatMessagesPaneProps {
|
interface ChatMessagesPaneProps {
|
||||||
scrollContainerRef: RefObject<HTMLDivElement>;
|
scrollContainerRef: RefObject<HTMLDivElement>;
|
||||||
@@ -118,6 +120,7 @@ export default function ChatMessagesPane({
|
|||||||
const messageKeyMapRef = useRef<WeakMap<ChatMessage, string>>(new WeakMap());
|
const messageKeyMapRef = useRef<WeakMap<ChatMessage, string>>(new WeakMap());
|
||||||
const allocatedKeysRef = useRef<Set<string>>(new Set());
|
const allocatedKeysRef = useRef<Set<string>>(new Set());
|
||||||
const generatedMessageKeyCounterRef = useRef(0);
|
const generatedMessageKeyCounterRef = useRef(0);
|
||||||
|
const groupedVisibleMessages = useMemo(() => groupConsecutiveTools(visibleMessages), [visibleMessages]);
|
||||||
|
|
||||||
// Keep keys stable across prepends so existing MessageComponent instances retain local state.
|
// Keep keys stable across prepends so existing MessageComponent instances retain local state.
|
||||||
const getMessageKey = useCallback((message: ChatMessage) => {
|
const getMessageKey = useCallback((message: ChatMessage) => {
|
||||||
@@ -252,28 +255,56 @@ export default function ChatMessagesPane({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{visibleMessages.map((message, index) => {
|
{(() => {
|
||||||
const prevMessage = index > 0 ? visibleMessages[index - 1] : null;
|
let prevMessage: ChatMessage | null = null;
|
||||||
return (
|
|
||||||
<MessageComponent
|
return groupedVisibleMessages.map((item) => {
|
||||||
key={getMessageKey(message)}
|
if (isToolGroupItem(item)) {
|
||||||
message={message}
|
const groupPrevMessage = prevMessage;
|
||||||
prevMessage={prevMessage}
|
prevMessage = item.messages[item.messages.length - 1] || prevMessage;
|
||||||
createDiff={createDiff}
|
|
||||||
onFileOpen={onFileOpen}
|
return (
|
||||||
onShowSettings={onShowSettings}
|
<ToolGroupContainer
|
||||||
onGrantToolPermission={onGrantToolPermission}
|
key={`tool-group-${getMessageKey(item.messages[0])}`}
|
||||||
autoExpandTools={autoExpandTools}
|
group={item}
|
||||||
showRawParameters={showRawParameters}
|
prevMessage={groupPrevMessage}
|
||||||
showThinking={showThinking}
|
createDiff={createDiff}
|
||||||
selectedProject={selectedProject}
|
getMessageKey={getMessageKey}
|
||||||
provider={provider}
|
onFileOpen={onFileOpen}
|
||||||
/>
|
onShowSettings={onShowSettings}
|
||||||
);
|
onGrantToolPermission={onGrantToolPermission}
|
||||||
})}
|
autoExpandTools={autoExpandTools}
|
||||||
|
showRawParameters={showRawParameters}
|
||||||
|
showThinking={showThinking}
|
||||||
|
selectedProject={selectedProject}
|
||||||
|
provider={provider}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagePrevMessage = prevMessage;
|
||||||
|
prevMessage = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MessageComponent
|
||||||
|
key={getMessageKey(item)}
|
||||||
|
message={item}
|
||||||
|
prevMessage={messagePrevMessage}
|
||||||
|
createDiff={createDiff}
|
||||||
|
onFileOpen={onFileOpen}
|
||||||
|
onShowSettings={onShowSettings}
|
||||||
|
onGrantToolPermission={onGrantToolPermission}
|
||||||
|
autoExpandTools={autoExpandTools}
|
||||||
|
showRawParameters={showRawParameters}
|
||||||
|
showThinking={showThinking}
|
||||||
|
selectedProject={selectedProject}
|
||||||
|
provider={provider}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
147
src/components/chat/view/subcomponents/ToolGroupContainer.tsx
Normal file
147
src/components/chat/view/subcomponents/ToolGroupContainer.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { ChatMessage, ClaudePermissionSuggestion, PermissionGrantResult, Provider } from '../../types/types';
|
||||||
|
import type { Project } from '../../../../types/app';
|
||||||
|
import type { ToolGroupItem } from '../../utils/toolGrouping';
|
||||||
|
import { getToolConfig } from '../../tools';
|
||||||
|
|
||||||
|
import MessageComponent from './MessageComponent';
|
||||||
|
|
||||||
|
type DiffLine = {
|
||||||
|
type: string;
|
||||||
|
content: string;
|
||||||
|
lineNum: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ToolGroupContainerProps {
|
||||||
|
group: ToolGroupItem;
|
||||||
|
prevMessage: ChatMessage | null;
|
||||||
|
createDiff: (oldStr: string, newStr: string) => DiffLine[];
|
||||||
|
getMessageKey: (message: ChatMessage) => string;
|
||||||
|
onFileOpen?: (filePath: string, diffInfo?: unknown) => void;
|
||||||
|
onShowSettings?: () => void;
|
||||||
|
onGrantToolPermission?: (suggestion: ClaudePermissionSuggestion) => PermissionGrantResult | null | undefined;
|
||||||
|
autoExpandTools?: boolean;
|
||||||
|
showRawParameters?: boolean;
|
||||||
|
showThinking?: boolean;
|
||||||
|
selectedProject?: Project | null;
|
||||||
|
provider: Provider | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseToolInput(toolInput: unknown): unknown {
|
||||||
|
if (typeof toolInput !== 'string') {
|
||||||
|
return toolInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(toolInput);
|
||||||
|
} catch {
|
||||||
|
return toolInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToolInputPreview(message: ChatMessage): string {
|
||||||
|
const config = getToolConfig(message.toolName || 'UnknownTool').input;
|
||||||
|
const parsedInput = parseToolInput(message.toolInput);
|
||||||
|
const title = typeof config.title === 'function' ? config.title(parsedInput) : config.title;
|
||||||
|
const value = config.getValue?.(parsedInput);
|
||||||
|
|
||||||
|
return String(value || title || message.displayText || message.content || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToolGroupIcon(icon: string | undefined, toolName: string): string {
|
||||||
|
if (icon === 'terminal') {
|
||||||
|
return '$';
|
||||||
|
}
|
||||||
|
|
||||||
|
return icon || toolName.slice(0, 1).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ToolGroupContainer({
|
||||||
|
group,
|
||||||
|
prevMessage,
|
||||||
|
createDiff,
|
||||||
|
getMessageKey,
|
||||||
|
onFileOpen,
|
||||||
|
onShowSettings,
|
||||||
|
onGrantToolPermission,
|
||||||
|
autoExpandTools,
|
||||||
|
showRawParameters,
|
||||||
|
showThinking,
|
||||||
|
selectedProject,
|
||||||
|
provider,
|
||||||
|
}: ToolGroupContainerProps) {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const config = getToolConfig(group.toolName).input;
|
||||||
|
const label = config.label || group.toolName;
|
||||||
|
const borderClass = config.colorScheme?.border || 'border-border';
|
||||||
|
const iconClass = config.colorScheme?.icon || 'text-muted-foreground';
|
||||||
|
const icon = getToolGroupIcon(config.icon, group.toolName);
|
||||||
|
|
||||||
|
const preview = useMemo(() => {
|
||||||
|
const visiblePreviews = group.messages
|
||||||
|
.slice(0, 2)
|
||||||
|
.map(getToolInputPreview)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const extraCount = group.messages.length - visiblePreviews.length;
|
||||||
|
const previewText = visiblePreviews.join(', ');
|
||||||
|
|
||||||
|
if (!previewText) {
|
||||||
|
return extraCount > 0 ? `+${extraCount} more` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return extraCount > 0 ? `${previewText}, +${extraCount} more` : previewText;
|
||||||
|
}, [group.messages]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chat-message tool px-3 sm:px-0" data-message-timestamp={group.timestamp || undefined}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`group flex w-full items-center gap-2 border-l-2 ${borderClass} rounded-r-md bg-muted/25 px-3 py-2 text-left transition-colors hover:bg-muted/40 dark:bg-muted/10 dark:hover:bg-muted/20`}
|
||||||
|
onClick={() => setIsExpanded((current) => !current)}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
>
|
||||||
|
<ChevronRight
|
||||||
|
className={`h-3.5 w-3.5 flex-shrink-0 text-muted-foreground transition-transform ${isExpanded ? 'rotate-90' : ''}`}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<span className={`${iconClass} flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-background/80 text-xs font-medium`}>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 flex-shrink-0 text-xs font-medium text-foreground">{label}</span>
|
||||||
|
<span className="flex-shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
|
||||||
|
x{group.messages.length}
|
||||||
|
</span>
|
||||||
|
{preview && (
|
||||||
|
<>
|
||||||
|
<span className="text-[10px] text-muted-foreground/40">/</span>
|
||||||
|
<span className="min-w-0 truncate font-mono text-xs text-muted-foreground">{preview}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="mt-2 space-y-3 sm:space-y-4">
|
||||||
|
{group.messages.map((message, index) => (
|
||||||
|
<MessageComponent
|
||||||
|
key={getMessageKey(message)}
|
||||||
|
message={message}
|
||||||
|
prevMessage={index > 0 ? group.messages[index - 1] : prevMessage}
|
||||||
|
createDiff={createDiff}
|
||||||
|
onFileOpen={onFileOpen}
|
||||||
|
onShowSettings={onShowSettings}
|
||||||
|
onGrantToolPermission={onGrantToolPermission}
|
||||||
|
autoExpandTools={autoExpandTools}
|
||||||
|
showRawParameters={showRawParameters}
|
||||||
|
showThinking={showThinking}
|
||||||
|
selectedProject={selectedProject}
|
||||||
|
provider={provider}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user