Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/browser/components/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ import { useReviews } from "@/browser/hooks/useReviews";
import { ReviewsBanner } from "./ReviewsBanner";
import type { ReviewNoteData } from "@/common/types/review";
import { PopoverError } from "./PopoverError";
import { ConnectionStatusIndicator } from "./ConnectionStatusIndicator";
import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext";

interface AIViewProps {
Expand Down Expand Up @@ -753,7 +752,6 @@ const AIViewInner: React.FC<AIViewProps> = ({
onTerminate={handleTerminateBackgroundBash}
/>
<ReviewsBanner workspaceId={workspaceId} />
<ConnectionStatusIndicator />
{isQueuedAgentTask && (
<div className="border-border-medium bg-background-secondary text-muted mb-2 rounded-md border px-3 py-2 text-xs">
This agent task is queued and will start automatically when a parallel slot is
Expand Down
27 changes: 17 additions & 10 deletions src/browser/components/ChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React, {
} from "react";
import { CommandSuggestions, COMMAND_SUGGESTION_KEYS } from "../CommandSuggestions";
import type { Toast } from "../ChatInputToast";
import { ConnectionStatusToast } from "../ConnectionStatusToast";
import { ChatInputToast } from "../ChatInputToast";
import { createCommandToast, createErrorToast } from "../ChatInputToasts";
import { parseCommand } from "@/browser/utils/slashCommands/parser";
Expand Down Expand Up @@ -1529,6 +1530,8 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
return `Type a message... (${hints.join(", ")})`;
})();

const activeToast = toast ?? (variant === "creation" ? creationState.toast : null);

// No wrapper needed - parent controls layout for both variants
const Wrapper = React.Fragment;
const wrapperProps = {};
Expand Down Expand Up @@ -1558,16 +1561,20 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
data-autofocus-state="done"
>
<div className={cn("w-full", variant !== "creation" && "mx-auto max-w-4xl")}>
{/* Toast - show shared toast (slash commands) or variant-specific toast */}
<ChatInputToast
toast={toast ?? (variant === "creation" ? creationState.toast : null)}
onDismiss={() => {
handleToastDismiss();
if (variant === "creation") {
creationState.setToast(null);
}
}}
/>
{/* Toasts (overlay) */}
<div className="pointer-events-none absolute right-[15px] bottom-full left-[15px] z-[1000] mb-2 flex flex-col gap-2 [&>*]:pointer-events-auto">
<ConnectionStatusToast wrap={false} />
<ChatInputToast
toast={activeToast}
wrap={false}
onDismiss={() => {
handleToastDismiss();
if (variant === "creation") {
creationState.setToast(null);
}
}}
/>
</div>

{/* Attached reviews preview - show styled blocks with remove/edit buttons */}
{/* Hide during send to avoid duplicate display with the sent message */}
Expand Down
123 changes: 65 additions & 58 deletions src/browser/components/ChatInputToast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,25 @@ export interface Toast {
interface ChatInputToastProps {
toast: Toast | null;
onDismiss: () => void;
/**
* When false, render only the toast content (no absolute-positioned wrapper).
* Useful for stacking multiple toasts under a single overlay container.
*/
wrap?: boolean;
}

export const SolutionLabel: React.FC<{ children: ReactNode }> = ({ children }) => (
<div className="text-muted-light mb-1 text-[10px] uppercase">{children}</div>
);

export const ChatInputToast: React.FC<ChatInputToastProps> = ({ toast, onDismiss }) => {
const wrapperClassName =
"pointer-events-none absolute right-[15px] bottom-full left-[15px] z-[1000] mb-2 [&>*]:pointer-events-auto";

export const ChatInputToast: React.FC<ChatInputToastProps> = ({
toast,
onDismiss,
wrap = true,
}) => {
const [isLeaving, setIsLeaving] = React.useState(false);

// Avoid carrying the fade-out animation state across toast changes.
Expand Down Expand Up @@ -72,67 +84,62 @@ export const ChatInputToast: React.FC<ChatInputToastProps> = ({ toast, onDismiss
// Use rich error style when there's a title or solution
const isRichError = toast.type === "error" && (toast.title ?? toast.solution);

if (isRichError) {
return (
<div className="pointer-events-none absolute right-[15px] bottom-full left-[15px] z-[1000] mb-2 [&>*]:pointer-events-auto">
<div
role="alert"
aria-live="assertive"
className="bg-toast-fatal-bg border-toast-fatal-border text-danger-soft animate-[toastSlideIn_0.2s_ease-out] rounded border px-3 py-2.5 text-xs shadow-[0_4px_12px_rgba(0,0,0,0.3)]"
>
<div className="flex items-start gap-1.5">
<span className="text-sm leading-none">⚠</span>
<div className="flex-1">
{toast.title && <div className="mb-1.5 font-semibold">{toast.title}</div>}
<div className="text-light mt-1.5 leading-[1.4]">{toast.message}</div>
{toast.solution && (
<div className="bg-dark font-monospace text-code-type mt-2 rounded px-2 py-1.5 text-[11px]">
{toast.solution}
</div>
)}
const content = isRichError ? (
<div
role="alert"
aria-live="assertive"
className="bg-toast-fatal-bg border-toast-fatal-border text-danger-soft animate-[toastSlideIn_0.2s_ease-out] rounded border px-3 py-2.5 text-xs shadow-[0_4px_12px_rgba(0,0,0,0.3)]"
>
<div className="flex items-start gap-1.5">
<span className="text-sm leading-none">⚠</span>
<div className="flex-1">
{toast.title && <div className="mb-1.5 font-semibold">{toast.title}</div>}
<div className="text-light mt-1.5 leading-[1.4]">{toast.message}</div>
{toast.solution && (
<div className="bg-dark font-monospace text-code-type mt-2 rounded px-2 py-1.5 text-[11px]">
{toast.solution}
</div>
<button
onClick={handleDismiss}
aria-label="Dismiss"
className="flex h-4 w-4 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-base leading-none text-inherit opacity-60 transition-opacity hover:opacity-100"
>
×
</button>
</div>
)}
</div>
<button
onClick={handleDismiss}
aria-label="Dismiss"
className="flex h-4 w-4 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-base leading-none text-inherit opacity-60 transition-opacity hover:opacity-100"
>
×
</button>
</div>
);
}

// Regular toast for simple messages and success
return (
<div className="pointer-events-none absolute right-[15px] bottom-full left-[15px] z-[1000] mb-2 [&>*]:pointer-events-auto">
<div
role={toast.type === "error" ? "alert" : "status"}
aria-live={toast.type === "error" ? "assertive" : "polite"}
className={cn(
"px-3 py-1.5 rounded text-xs flex items-center gap-1.5 shadow-[0_4px_12px_rgba(0,0,0,0.3)]",
isLeaving
? "animate-[toastFadeOut_0.2s_ease-out_forwards]"
: "animate-[toastSlideIn_0.2s_ease-out]",
toastTypeStyles[toast.type]
)}
>
<span className="text-sm leading-none">{toast.type === "success" ? "✓" : "⚠"}</span>
<div className="flex-1">
{toast.title && <div className="mb-px text-[11px] font-semibold">{toast.title}</div>}
<div className="opacity-90">{toast.message}</div>
</div>
{toast.type === "error" && (
<button
onClick={handleDismiss}
aria-label="Dismiss"
className="flex h-4 w-4 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-base leading-none text-inherit opacity-60 transition-opacity hover:opacity-100"
>
×
</button>
)}
</div>
) : (
<div
role={toast.type === "error" ? "alert" : "status"}
aria-live={toast.type === "error" ? "assertive" : "polite"}
className={cn(
"px-3 py-1.5 rounded text-xs flex items-center gap-1.5 shadow-[0_4px_12px_rgba(0,0,0,0.3)]",
isLeaving
? "animate-[toastFadeOut_0.2s_ease-out_forwards]"
: "animate-[toastSlideIn_0.2s_ease-out]",
toastTypeStyles[toast.type]
)}
>
<span className="text-sm leading-none">{toast.type === "success" ? "✓" : "⚠"}</span>
<div className="flex-1">
{toast.title && <div className="mb-px text-[11px] font-semibold">{toast.title}</div>}
<div className="opacity-90">{toast.message}</div>
</div>
{toast.type === "error" && (
<button
onClick={handleDismiss}
aria-label="Dismiss"
className="flex h-4 w-4 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-base leading-none text-inherit opacity-60 transition-opacity hover:opacity-100"
>
×
</button>
)}
</div>
);

if (!wrap) return content;

return <div className={wrapperClassName}>{content}</div>;
};
59 changes: 0 additions & 59 deletions src/browser/components/ConnectionStatusIndicator.tsx

This file was deleted.

82 changes: 82 additions & 0 deletions src/browser/components/ConnectionStatusToast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from "react";
import { useAPI } from "@/browser/contexts/API";

const wrapperClassName =
"pointer-events-none absolute right-[15px] bottom-full left-[15px] z-[1000] mb-2 [&>*]:pointer-events-auto";

/**
* Connection status banner that uses the same *overlay placement* as ChatInputToast.
*
* This avoids layout shifts in:
* - the creation screen (new chat)
* - the workspace chat window
*/
interface ConnectionStatusToastProps {
/**
* When false, render only the toast content (no absolute-positioned wrapper).
* Useful for stacking multiple toasts under a single overlay container.
*/
wrap?: boolean;
}

export const ConnectionStatusToast: React.FC<ConnectionStatusToastProps> = ({ wrap = true }) => {
const apiState = useAPI();

// Don't show anything when connected or during initial connection.
// Auth required is handled by a separate modal flow.
if (
apiState.status === "connected" ||
apiState.status === "connecting" ||
apiState.status === "auth_required"
) {
return null;
}

if (apiState.status === "degraded" || apiState.status === "reconnecting") {
const content = (
<div
role="status"
aria-live="polite"
className="bg-warning/10 border-warning/30 text-warning flex animate-[toastSlideIn_0.2s_ease-out] items-center gap-2 rounded border px-3 py-1.5 text-xs shadow-[0_4px_12px_rgba(0,0,0,0.3)]"
>
<span className="bg-warning inline-block h-2 w-2 animate-pulse rounded-full" />
<span>
{apiState.status === "degraded" ? (
"Connection unstable — messages may be delayed"
) : (
<>
Reconnecting to server
{apiState.attempt > 1 && ` (attempt ${apiState.attempt})`}…
</>
)}
</span>
</div>
);

if (!wrap) return content;

return <div className={wrapperClassName}>{content}</div>;
}

if (apiState.status === "error") {
const content = (
<div
role="alert"
aria-live="assertive"
className="bg-toast-error-bg border-toast-error-border text-toast-error-text flex animate-[toastSlideIn_0.2s_ease-out] items-center gap-2 rounded border px-3 py-1.5 text-xs shadow-[0_4px_12px_rgba(0,0,0,0.3)]"
>
<span className="bg-danger inline-block h-2 w-2 rounded-full" />
<span>Connection lost</span>
<button type="button" onClick={apiState.retry} className="underline hover:no-underline">
Retry
</button>
</div>
);

if (!wrap) return content;

return <div className={wrapperClassName}>{content}</div>;
}

return null;
};
2 changes: 0 additions & 2 deletions src/browser/components/ProjectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { FrontendWorkspaceMetadata } from "@/common/types/workspace";
import { ModeProvider } from "@/browser/contexts/ModeContext";
import { ProviderOptionsProvider } from "@/browser/contexts/ProviderOptionsContext";
import { ThinkingProvider } from "@/browser/contexts/ThinkingContext";
import { ConnectionStatusIndicator } from "./ConnectionStatusIndicator";
import { ChatInput } from "./ChatInput/index";
import type { ChatInputAPI } from "./ChatInput/types";
import { ArchivedWorkspaces } from "./ArchivedWorkspaces";
Expand Down Expand Up @@ -127,7 +126,6 @@ export const ProjectPage: React.FC<ProjectPageProps> = ({
<ModeProvider projectPath={projectPath}>
<ProviderOptionsProvider>
<ThinkingProvider projectPath={projectPath}>
<ConnectionStatusIndicator />
{/* Scrollable content area */}
<div className="min-h-0 flex-1 overflow-y-auto">
{/* Top section: centers ChatInput in top portion of viewport */}
Expand Down
Loading