Skip to content
Draft
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
12 changes: 7 additions & 5 deletions src/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SidebarSearch } from "./SidebarSearch";
import { SpaceBar } from "./SpaceBar";
import { UpdateBanner } from "./UpdateBanner";
import { ProjectSection } from "./sidebar/ProjectSection";
import { useLanguage } from "@/lib/i18n";

interface AppSidebarProps {
isOpen: boolean;
Expand Down Expand Up @@ -73,6 +74,7 @@ export const AppSidebar = memo(function AppSidebar({
onOpenSettings,
agents,
}: AppSidebarProps) {
const { t } = useLanguage();
// Load default chat limit from main-process settings
const [defaultChatLimit, setDefaultChatLimit] = useState(10);
useEffect(() => {
Expand Down Expand Up @@ -199,7 +201,7 @@ export const AppSidebar = memo(function AppSidebar({
className="no-drag flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[13px] font-medium text-sidebar-foreground/70 transition-all hover:bg-black/5 hover:text-sidebar-foreground dark:hover:bg-white/10"
>
<Plus className="h-3.5 w-3.5 shrink-0" />
<span>Add project</span>
<span>{t("Add project")}</span>
</button>
</div>

Expand Down Expand Up @@ -258,8 +260,8 @@ export const AppSidebar = memo(function AppSidebar({
{filteredProjects.length === 0 && (
<p className="px-2 py-8 text-center text-xs text-sidebar-foreground/50">
{projects.length === 0
? "Add a project to get started"
: "No projects in this space"}
? t("Add a project to get started")
: t("No projects in this space")}
</p>
)}
</div>
Expand All @@ -269,7 +271,7 @@ export const AppSidebar = memo(function AppSidebar({
<UpdateBanner />

<div className="flex items-center justify-center gap-1.5 px-3 py-1.5 text-[11px] text-sidebar-foreground/40">
<span>Harnss is in early beta</span>
<span>{t("Harnss is in early beta")}</span>
<span className="text-sidebar-foreground/20">·</span>
<a
href="https://github.com/OpenSource03/harnss/issues"
Expand All @@ -278,7 +280,7 @@ export const AppSidebar = memo(function AppSidebar({
className="inline-flex items-center gap-1 text-sidebar-foreground/50 transition-colors hover:text-sidebar-foreground/80"
>
<Bug className="h-3 w-3" />
<span>Report a bug</span>
<span>{t("Report a bug")}</span>
</a>
</div>

Expand Down
16 changes: 9 additions & 7 deletions src/components/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { isMac } from "@/lib/utils";
import type { AcpPermissionBehavior } from "@/types";
import { useLanguage } from "@/lib/i18n";

const PERMISSION_MODE_LABELS: Record<string, string> = {
plan: "Plan",
Expand Down Expand Up @@ -54,6 +55,7 @@ export const ChatHeader = memo(function ChatHeader({
onSeedDevExampleConversation,
onSeedDevExampleSpaceData,
}: ChatHeaderProps) {
const { t } = useLanguage();
const modeLabel = permissionMode ? PERMISSION_MODE_LABELS[permissionMode] : null;
const acpBehaviorLabel = acpPermissionBehavior
? ACP_PERMISSION_BEHAVIOR_LABELS[acpPermissionBehavior]
Expand All @@ -63,11 +65,11 @@ export const ChatHeader = memo(function ChatHeader({

// Collect all session detail rows for the unified tooltip
const detailRows: { label: string; value: string }[] = [];
if (model) detailRows.push({ label: "Model", value: model });
detailRows.push({ label: "Plan", value: planMode ? "On" : "Off" });
if (permissionDisplay) detailRows.push({ label: "Permissions", value: permissionDisplay });
if (totalCost > 0) detailRows.push({ label: "Cost", value: `$${totalCost.toFixed(4)}` });
if (sessionId) detailRows.push({ label: "Session", value: sessionId });
if (model) detailRows.push({ label: t("Model"), value: model });
detailRows.push({ label: t("Plan"), value: planMode ? "On" : "Off" });
if (permissionDisplay) detailRows.push({ label: t("Permissions"), value: permissionDisplay });
if (totalCost > 0) detailRows.push({ label: t("Cost"), value: `$${totalCost.toFixed(4)}` });
if (sessionId) detailRows.push({ label: t("Session"), value: sessionId });

const hasDetails = detailRows.length > 0;
const showDevSeedButton = import.meta.env.DEV && !!showDevFill && !!onSeedDevExampleConversation;
Expand Down Expand Up @@ -106,13 +108,13 @@ export const ChatHeader = memo(function ChatHeader({
<div className="space-y-0.5 text-xs">
{model && (
<div className="flex justify-between gap-4">
<span className="opacity-70">Model</span>
<span className="opacity-70">{t("Model")}</span>
<span className="font-mono">{model}</span>
</div>
)}
{permissionDisplay && (
<div className="flex justify-between gap-4">
<span className="opacity-70">Permissions</span>
<span className="opacity-70">{t("Permissions")}</span>
<span className="font-mono">{permissionDisplay}</span>
</div>
)}
Expand Down
7 changes: 4 additions & 3 deletions src/components/MessageBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export const MessageBubble = memo(function MessageBubble({
)}>
<div className="inline-flex items-center gap-1.5">
{isError ? <AlertCircle className="h-3 w-3" /> : <Info className="h-3 w-3" />}
{message.content}
<span data-no-translate>{message.content}</span>
</div>
</div>
);
Expand Down Expand Up @@ -244,7 +244,7 @@ export const MessageBubble = memo(function MessageBubble({
open={!!viewingImage}
onOpenChange={(isOpen) => { if (!isOpen) setViewingImage(null); }}
/>
{renderWithMentions(displayContent)}
<span data-no-translate>{renderWithMentions(displayContent)}</span>
{message.isQueued && (
<div className="mt-2 flex items-center gap-2 border-t border-foreground/[0.06] pt-2 text-[11px] text-muted-foreground">
<Clock className="h-3 w-3 shrink-0" />
Expand Down Expand Up @@ -334,7 +334,7 @@ export const MessageBubble = memo(function MessageBubble({
<div className={`flex justify-start px-4 ${isContinuation ? "py-0.5" : "py-1.5"}`}>
<Tooltip>
<TooltipTrigger asChild>
<div className="flow-root min-w-0 max-w-[85%] wrap-break-word">
<div className="flow-root min-w-0 max-w-[85%] wrap-break-word">
{showThinking && message.thinking && (
<div className={message.content ? "mb-2" : undefined}>
<ThinkingBlock
Expand All @@ -348,6 +348,7 @@ export const MessageBubble = memo(function MessageBubble({
<div
ref={proseRef}
className="flow-root prose dark:prose-invert prose-sm max-w-none text-foreground [&_li::marker]:text-foreground dark:[&_li::marker]:text-foreground/70"
data-no-translate
>
<IsStreamingMarkdownContext.Provider value={!!message.isStreaming}>
<ReactMarkdown
Expand Down
8 changes: 5 additions & 3 deletions src/components/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { AnalyticsSettings } from "@/components/settings/AnalyticsSettings";
import { isMac } from "@/lib/utils";
import type { InstalledAgent, ThemeOption } from "@/types";
import type { AppSettings } from "@/types/ui";
import { useLanguage } from "@/lib/i18n";

// ── Section definitions ──

Expand Down Expand Up @@ -112,6 +113,7 @@ export const SettingsView = memo(function SettingsView({
onToggleSidebar,
onReplayWelcome,
}: SettingsViewProps) {
const { t } = useLanguage();
const [activeSection, setActiveSection] = useState<SettingsSection>("general");
const macIslandTitlebarOffsetClass = "";

Expand Down Expand Up @@ -256,7 +258,7 @@ export const SettingsView = memo(function SettingsView({
<PanelLeft className="h-4 w-4" />
</Button>
)}
<span className={`leading-none text-sm font-semibold text-foreground ${macIslandTitlebarOffsetClass}`}>Settings</span>
<span className={`leading-none text-sm font-semibold text-foreground ${macIslandTitlebarOffsetClass}`}>{t("Settings")}</span>
</div>

<div className="flex min-h-0 flex-1 overflow-hidden">
Expand All @@ -278,10 +280,10 @@ export const SettingsView = memo(function SettingsView({
}`}
>
<Icon className="h-4 w-4 shrink-0" />
<span className="flex-1">{item.label}</span>
<span className="flex-1">{t(item.label)}</span>
{item.comingSoon && (
<span className="rounded bg-foreground/[0.06] px-1.5 py-px text-[10px] font-medium text-muted-foreground/70">
Soon
{t("Soon")}
</span>
)}
</button>
Expand Down
12 changes: 7 additions & 5 deletions src/components/SidebarSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useRef, useEffect, useCallback, memo } from "react";
import { Search, MessageSquare, Hash, X } from "lucide-react";
import type { SearchMessageResult, SearchSessionResult } from "@/types";
import { useLanguage } from "@/lib/i18n";

interface SidebarSearchProps {
projectIds: string[];
Expand All @@ -13,6 +14,7 @@ export const SidebarSearch = memo(function SidebarSearch({
onNavigateToMessage,
onSelectSession,
}: SidebarSearchProps) {
const { t } = useLanguage();
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [messageResults, setMessageResults] = useState<SearchMessageResult[]>([]);
Expand Down Expand Up @@ -108,7 +110,7 @@ export const SidebarSearch = memo(function SidebarSearch({
}}
onFocus={() => setIsOpen(true)}
onKeyDown={handleKeyDown}
placeholder="Search chats..."
placeholder={t("Search chats...")}
className="w-full bg-black/5 py-1.5 pe-8 ps-9 text-[13px] text-sidebar-foreground placeholder:text-sidebar-foreground/40 outline-none transition-colors focus:bg-black/10 dark:bg-white/5 dark:focus:bg-white/10"
/>
{query && (
Expand All @@ -127,18 +129,18 @@ export const SidebarSearch = memo(function SidebarSearch({
{showDropdown && (
<div className="absolute inset-x-3 top-full z-50 mt-1 max-h-80 overflow-y-auto rounded-xl border border-sidebar-border bg-popover p-1.5 shadow-xl glass-outline" style={{ "--island-fill": "var(--popover)" } as React.CSSProperties}>
{isSearching && (
<p className="px-2 py-3 text-center text-xs text-muted-foreground">Searching...</p>
<p className="px-2 py-3 text-center text-xs text-muted-foreground">{t("Searching")}...</p>
)}

{!isSearching && !hasResults && (
<p className="px-2 py-3 text-center text-xs text-muted-foreground">No results found</p>
<p className="px-2 py-3 text-center text-xs text-muted-foreground">{t("No results found")}</p>
)}

{/* Session results */}
{sessionResults.length > 0 && (
<div className="mb-1">
<p className="px-2 py-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60">
Chats
{t("Chats")}
</p>
{sessionResults.map((r) => (
<button
Expand All @@ -163,7 +165,7 @@ export const SidebarSearch = memo(function SidebarSearch({
{messageResults.length > 0 && (
<div>
<p className="px-2 py-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60">
Messages
{t("Messages")}
</p>
{messageResults.map((r, i) => (
<button
Expand Down
20 changes: 11 additions & 9 deletions src/components/SpaceBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Pencil, Trash2 } from "lucide-react";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { resolveLucideIcon } from "@/lib/icon-utils";
import type { Space } from "@/types";
import { useLanguage } from "@/lib/i18n";

interface SpaceBarProps {
spaces: Space[];
Expand Down Expand Up @@ -58,6 +59,7 @@ export const SpaceBar = memo(function SpaceBar({
onDropProject,
onOpenSettings,
}: SpaceBarProps) {
const { t } = useLanguage();
const sorted = [...spaces].sort((a, b) => a.order - b.order);
const [contextSpace, setContextSpace] = useState<Space | null>(null);
const [contextPos, setContextPos] = useState({ x: 0, y: 0 });
Expand Down Expand Up @@ -132,7 +134,7 @@ export const SpaceBar = memo(function SpaceBar({
</button>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
Settings
{t("Settings")}
</TooltipContent>
</Tooltip>
{/* Center group — flex row: [◂] [masked scroll area] [▸].
Expand Down Expand Up @@ -194,7 +196,7 @@ export const SpaceBar = memo(function SpaceBar({
</button>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{space.name}
<span data-no-translate>{space.name}</span>
</TooltipContent>
</Tooltip>
);
Expand Down Expand Up @@ -224,7 +226,7 @@ export const SpaceBar = memo(function SpaceBar({
</button>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
New space
{t("New space")}
</TooltipContent>
</Tooltip>

Expand All @@ -248,15 +250,15 @@ export const SpaceBar = memo(function SpaceBar({
>
<DropdownMenuItem onClick={() => { if (contextSpace) onEditSpace(contextSpace); closeContext(); }}>
<Pencil className="me-2 h-3.5 w-3.5" />
Edit
{t("Edit")}
</DropdownMenuItem>
{contextSpace?.id !== "default" && (
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => { if (contextSpace) setDeleteSpace(contextSpace); closeContext(); }}
>
<Trash2 className="me-2 h-3.5 w-3.5" />
Delete
{t("Delete")}
</DropdownMenuItem>
)}
</DropdownMenuContent>
Expand All @@ -267,15 +269,15 @@ export const SpaceBar = memo(function SpaceBar({
open={deleteSpace !== null}
onOpenChange={(open) => !open && setDeleteSpace(null)}
onConfirm={() => { if (deleteSpace) onDeleteSpace(deleteSpace.id); }}
title="Delete Space"
title={t("Delete Space")}
description={
<>
Are you sure you want to delete{" "}
{t("Are you sure you want to delete")}{" "}
<span className="font-medium text-foreground">{deleteSpace?.name}</span>?
Projects in this space will be moved to General.
{t("Projects in this space will be moved to General.")}
</>
}
confirmLabel="Delete"
confirmLabel={t("Delete")}
/>
</div>
);
Expand Down
7 changes: 5 additions & 2 deletions src/components/ThinkingBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
advanceThinkingAnimationState,
createThinkingAnimationState,
} from "@/lib/thinking-animation";
import { useLanguage } from "@/lib/i18n";

interface ThinkingBlockProps {
thinking: string;
Expand All @@ -18,6 +19,7 @@ interface ThinkingBlockProps {
}

export function ThinkingBlock({ thinking, isStreaming, thinkingComplete }: ThinkingBlockProps) {
const { t } = useLanguage();
const [open, setOpen] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
// Tracks whether user manually scrolled up in the inner thinking div
Expand Down Expand Up @@ -71,10 +73,10 @@ export function ThinkingBlock({ thinking, isStreaming, thinkingComplete }: Think
<Minus className={`h-3 w-3 ${isThinking ? "text-foreground/40" : "text-foreground/30"}`} />
{isThinking ? (
<TextShimmer as="span" className="italic opacity-60" duration={1.8} spread={1.5}>
Thinking...
{t("Thinking...")}
</TextShimmer>
) : (
<span className="italic text-foreground/40">Thought</span>
<span className="italic text-foreground/40">{t("Thought")}</span>
)}
</CollapsibleTrigger>
{/* Only render expandable content when there's actual thinking text */}
Expand All @@ -83,6 +85,7 @@ export function ThinkingBlock({ thinking, isStreaming, thinkingComplete }: Think
<div
ref={contentRef}
onScroll={handleScroll}
data-no-translate
className="mt-1 mb-2 max-h-60 overflow-auto border-s-2 border-foreground/10 ps-3 py-1 text-xs text-foreground/40 whitespace-pre-wrap"
>
{animationState.baseText}
Expand Down
Loading