Skip to content
Closed
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
15 changes: 9 additions & 6 deletions apps/web/src/AppRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ElectronBrowserHost } from "./browser/ElectronBrowserHost";
import { PreviewAutomationHosts } from "./components/preview/PreviewAutomationHosts";
import { QuitHoldOverlay } from "./components/QuitHoldOverlay";
import { AppAtomRegistryProvider } from "./rpc/atomRegistry";
import { I18nProvider } from "./i18n/I18nProvider";
import type { AppRouter } from "./router";

/**
Expand All @@ -13,11 +14,13 @@ import type { AppRouter } from "./router";
*/
export function AppRoot({ router }: { readonly router: AppRouter }) {
return (
<AppAtomRegistryProvider>
<RouterProvider router={router} />
<PreviewAutomationHosts />
<ElectronBrowserHost />
<QuitHoldOverlay />
</AppAtomRegistryProvider>
<I18nProvider>
<AppAtomRegistryProvider>
<RouterProvider router={router} />
<PreviewAutomationHosts />
<ElectronBrowserHost />
<QuitHoldOverlay />
</AppAtomRegistryProvider>
</I18nProvider>
);
}
12 changes: 12 additions & 0 deletions apps/web/src/clientPersistenceStorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ describe("clientPersistenceStorage", () => {
expect(settings).not.toHaveProperty("diffWordWrap");
});

it("keeps the language across reloads and defaults it to the system", async () => {
const testWindow = getTestWindow();
const { readBrowserClientSettings, writeBrowserClientSettings } =
await import("./clientPersistenceStorage");

testWindow.localStorage.setItem("t3code:client-settings:v1", JSON.stringify({}));
expect(readBrowserClientSettings()?.language).toBe("system");

writeBrowserClientSettings({ ...DEFAULT_CLIENT_SETTINGS, language: "zh-CN" });
expect(readBrowserClientSettings()?.language).toBe("zh-CN");
});

it("keeps the default diff file state across reloads and defaults it to expanded", async () => {
const testWindow = getTestWindow();
const { readBrowserClientSettings, writeBrowserClientSettings } =
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import { useHandleNewThread } from "../hooks/useHandleNewThread";
import { useOpenPanelPullRequestUrl } from "../hooks/useOpenPanelPullRequestUrl";
import { writeTextToClipboard } from "../hooks/useCopyToClipboard";
import { useClientSettings } from "../hooks/useSettings";
import { useConnectionStatusCopy } from "../i18n/I18nProvider";
import { useTheme } from "../hooks/useTheme";
import { readLocalApi } from "../localApi";
import { desktopLocalBackendId } from "../connection/desktopLocal";
Expand Down Expand Up @@ -619,6 +620,7 @@ function OpenCommandPaletteDialog(props: {
readonly openOverlayMode: (mode: SearchOverlayMode) => void;
readonly clearOpenIntent: () => void;
}) {
const connectionStatusCopy = useConnectionStatusCopy();
const navigate = useNavigate();
const pathname = useLocation({ select: (location) => location.pathname });
const { clearOpenIntent, openIntent, openOverlayMode, setOpen } = props;
Expand Down Expand Up @@ -874,7 +876,7 @@ function OpenCommandPaletteDialog(props: {
isPrimary,
machine: resolveEnvironmentMachineKind(environment.serverConfig),
isConnected: canCreateProjectInEnvironment(environment.connection.phase),
status: connectionStatusText(environment.connection),
status: connectionStatusText(environment.connection, connectionStatusCopy),
};
});

Expand All @@ -886,7 +888,7 @@ function OpenCommandPaletteDialog(props: {
});

return options;
}, [environments]);
}, [connectionStatusCopy, environments]);
const defaultAddProjectEnvironmentId =
addProjectEnvironmentOptions.find((option) => option.isConnected)?.environmentId ?? null;
const wslAddProjectEnvironmentOption = useMemo(
Expand Down
46 changes: 24 additions & 22 deletions apps/web/src/components/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { useWorkspaceMutationRefresh } from "../hooks/useWorkspaceMutationRefres
import { useProject, useThread } from "../state/entities";
import { resolveThreadRouteRef } from "../threadRoutes";
import { useClientSettings, useUpdateClientSettings } from "../hooks/useSettings";
import { useI18n } from "../i18n/I18nProvider";
import { formatShortTimestamp } from "../timestampFormat";
import { DiffFilePathCopyButton } from "./DiffFilePathCopyButton";
import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell";
Expand Down Expand Up @@ -110,6 +111,7 @@ export default function DiffPanel({
initialGitScope: initialGitScopeProp,
workspaceMutationId,
}: DiffPanelProps) {
const { language, t } = useI18n();
const { resolvedTheme } = useTheme();
const settings = useClientSettings();
const [initialGitScope] = useState(initialGitScopeProp);
Expand Down Expand Up @@ -216,21 +218,21 @@ export default function DiffPanel({
const selectedScopeLabel =
selectedTurnId === null
? selectedGitScope === "unstaged"
? "Working tree"
: "Branch changes"
? t("diff.scope.workingTree")
: t("diff.scope.branchChanges")
: selectedTurn?.turnId === latestTurn?.turnId
? "Latest turn"
: `Turn ${selectedCheckpointTurnCount ?? "?"}`;
? t("diff.scope.latestTurn")
: t("diff.scope.turn", { count: selectedCheckpointTurnCount ?? "?" });
const reviewSectionId = selectedTurn ? `turn:${selectedTurn.turnId}` : selectedGitScope;
const collapseScopeKey = routeThreadRef
? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}:${reviewSectionId}`
: null;
const codeViewMountKey = `${collapseScopeKey ?? reviewSectionId}:${codeViewRevision}`;
const reviewSectionTitle = selectedTurn
? `Turn ${selectedCheckpointTurnCount ?? "?"}`
? t("diff.scope.turn", { count: selectedCheckpointTurnCount ?? "?" })
: selectedGitScope === "unstaged"
? "Working tree"
: "Branch changes";
? t("diff.scope.workingTree")
: t("diff.scope.branchChanges");
const selectedCheckpointRange = useMemo(
() =>
typeof selectedCheckpointTurnCount === "number"
Expand Down Expand Up @@ -558,7 +560,7 @@ export default function DiffPanel({
<DropdownMenu>
<DropdownMenuTrigger
className="inline-flex h-6 max-w-full items-center gap-1 rounded-md bg-accent px-2 text-xs font-medium text-accent-foreground outline-none transition-colors hover:bg-accent/80 focus-visible:ring-2 focus-visible:ring-ring"
aria-label={`Diff scope: ${selectedScopeLabel}`}
aria-label={t("diff.scope.label", { scope: selectedScopeLabel })}
>
<span className="truncate">{selectedScopeLabel}</span>
<ChevronDownIcon className="size-3.5 shrink-0 opacity-70" />
Expand All @@ -572,7 +574,7 @@ export default function DiffPanel({
}
onClick={() => selectGitScope("unstaged")}
>
<span>Working tree</span>
<span>{t("diff.scope.workingTree")}</span>
</DropdownMenuItem>
<DropdownMenuItem
className={
Expand All @@ -582,7 +584,7 @@ export default function DiffPanel({
}
onClick={() => selectGitScope("branch")}
>
<span>Branch changes</span>
<span>{t("diff.scope.branchChanges")}</span>
</DropdownMenuItem>
<DropdownMenuItem
className={
Expand All @@ -594,10 +596,10 @@ export default function DiffPanel({
if (latestTurn) selectTurn(latestTurn.turnId);
}}
>
<span>Latest turn</span>
<span>{t("diff.scope.latestTurn")}</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Turn</DropdownMenuSubTrigger>
<DropdownMenuSubTrigger>{t("diff.scope.turns")}</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-64">
{orderedTurnDiffSummaries.map((summary) => {
const turnCount =
Expand All @@ -612,9 +614,13 @@ export default function DiffPanel({
}
onClick={() => selectTurn(summary.turnId)}
>
<span>Turn {turnCount}</span>
<span>{t("diff.scope.turn", { count: turnCount })}</span>
<span className="ml-auto text-xs tabular-nums text-muted-foreground">
{formatShortTimestamp(summary.completedAt, settings.timestampFormat)}
{formatShortTimestamp(
summary.completedAt,
settings.timestampFormat,
language,
)}
</span>
</DropdownMenuItem>
);
Expand Down Expand Up @@ -895,15 +901,15 @@ export default function DiffPanel({
<DiffPanelShell mode={mode} header={headerRow}>
{!activeThread ? (
<div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70">
Select a thread to inspect turn diffs.
{t("diff.empty.selectThread")}
</div>
) : !isGitRepo ? (
<div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70">
Turn diffs are unavailable because this project is not a git repository.
{t("diff.empty.notRepository")}
</div>
) : selectedTurnId !== null && orderedTurnDiffSummaries.length === 0 ? (
<div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70">
No completed turns yet.
{t("diff.empty.noCompletedTurns")}
</div>
) : (
<>
Expand Down Expand Up @@ -932,11 +938,7 @@ export default function DiffPanel({
/>
) : (
<div className="flex h-full items-center justify-center px-3 py-2 text-xs text-muted-foreground/70">
<p>
{hasNoNetChanges
? "No net changes in this selection."
: "No patch available for this selection."}
</p>
<p>{hasNoNetChanges ? t("diff.empty.noNetChanges") : t("diff.empty.noPatch")}</p>
</div>
)
) : renderablePatch.kind === "files" ? (
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import {
replaceTextRange,
} from "../../composer-logic";
import { DISCONNECTED_COMPOSER_PLACEHOLDER } from "../../composerPlaceholder";
import { useI18n } from "../../i18n/I18nProvider";
import {
deriveComposerSendState,
getAntigravitySendBlockReason,
Expand Down Expand Up @@ -1430,6 +1431,7 @@ export interface ChatComposerProps {
// --------------------------------------------------------------------------

export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) {
const { t } = useI18n();
const {
composerDraftTarget,
environmentId,
Expand Down Expand Up @@ -6672,12 +6674,12 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
: showPlanFollowUpPrompt && activeProposedPlan
? "Add feedback to refine the plan, or leave this blank to implement it"
: projectSelectionRequired
? "Choose a project above to start a thread"
? t("composer.placeholder.chooseProject")
: showProviderUnavailable
? "Enable a provider in Settings to send a message"
? t("composer.placeholder.enableProvider")
: phase === "disconnected"
? DISCONNECTED_COMPOSER_PLACEHOLDER
: "Ask anything, @tag files/folders, $use skills, or / for commands"
: t("composer.placeholder.default")
}
disabled={
isConnecting ||
Expand Down
14 changes: 8 additions & 6 deletions apps/web/src/components/chat/ComposerPrimaryActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Button } from "../ui/button";
import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu";
import { Spinner } from "../ui/spinner";
import { composerFloatingLayerProps } from "./composerEventScope";
import { useI18n } from "../../i18n/I18nProvider";

interface PendingActionState {
questionIndex: number;
Expand Down Expand Up @@ -81,6 +82,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
? { onPointerDown: preventPointerFocus }
: undefined;
const environmentIdentificationMode = useEnvironmentIdentificationMode();
const { t } = useI18n();
const isSendDisabled = sendDisabledReason !== null;
const stageBackdropVariant = useSidebarStageBackdropVariant(
environmentIdentificationMode === "artwork",
Expand All @@ -99,7 +101,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
)}
{...pointerFocusProps}
onClick={onInterrupt}
aria-label="Stop generation"
aria-label={t("composer.stopGeneration")}
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true">
<rect x="2" y="2" width="8" height="8" rx="1.5" />
Expand Down Expand Up @@ -238,16 +240,16 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
}
aria-label={
isEnvironmentUnavailable
? "Environment disconnected"
? t("composer.environmentDisconnected")
: sendDisabledReason
? sendDisabledReason
: isConnecting
? "Connecting"
? t("composer.connecting")
: isPreparingWorktree
? "Preparing worktree"
? t("composer.preparingWorktree")
: isSendBusy
? "Sending"
: "Send message"
? t("composer.sending")
: t("composer.sendMessage")
}
>
{stageBackdropVariant ? (
Expand Down
24 changes: 20 additions & 4 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ import { cn } from "~/lib/utils";
import { useUiStateStore } from "~/uiStateStore";
import { type TimestampFormat } from "@t3tools/contracts/settings";
import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat";
import type { AppLanguage } from "../../i18n/locale";
import { useI18n } from "../../i18n/I18nProvider";

import { SkillInlineText } from "./SkillInlineText";
import { deriveAgentSpawnSummary } from "./agentSpawnSummary";
Expand All @@ -255,6 +257,7 @@ interface TimelineRowSharedState {
citationRequest: AssistantCitationTarget | null;
listRef: React.RefObject<LegendListRef | null>;
timestampFormat: TimestampFormat;
language: AppLanguage;
routeThreadKey: string;
threadRef: ScopedThreadRef | null;
markdownCwd: string | undefined;
Expand Down Expand Up @@ -485,6 +488,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
topFadeEnabled = false,
loadEarlier = null,
}: MessagesTimelineProps) {
const { language } = useI18n();
const [expandedTurnIds, setExpandedTurnIds] = useState<ReadonlySet<TurnId>>(new Set());
const [expandedWorkGroupIds, setExpandedWorkGroupIds] = useState<ReadonlySet<string>>(new Set());
// Preserve member disclosure state across virtualization.
Expand Down Expand Up @@ -898,6 +902,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
citationRequest: readyCitationRequest,
listRef,
timestampFormat,
language,
routeThreadKey,
// Keep Markdown callbacks memoized during unrelated activity updates.
threadRef: citationThreadRef,
Expand Down Expand Up @@ -929,6 +934,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
readyCitationRequest,
listRef,
timestampFormat,
language,
routeThreadKey,
citationThreadRef,
markdownCwd,
Expand Down Expand Up @@ -1816,10 +1822,15 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
<div className="flex shrink-0 items-center gap-2">
<Tooltip>
<TooltipTrigger render={<p className="text-muted-foreground text-xs tabular-nums" />}>
{formatDayAwareTimestamp(row.message.createdAt, ctx.timestampFormat)}
{formatDayAwareTimestamp(
row.message.createdAt,
ctx.timestampFormat,
undefined,
ctx.language,
)}
</TooltipTrigger>
<TooltipPopup>
{formatChatTimestampTooltip(row.message.createdAt, ctx.timestampFormat)}
{formatChatTimestampTooltip(row.message.createdAt, ctx.timestampFormat, ctx.language)}
</TooltipPopup>
</Tooltip>
<div className="flex items-center gap-0.5">
Expand Down Expand Up @@ -2022,10 +2033,15 @@ function AssistantMessageMeta({
{!message.streaming && (
<Tooltip>
<TooltipTrigger render={<p className="text-muted-foreground text-xs tabular-nums" />}>
{formatDayAwareTimestamp(message.updatedAt, ctx.timestampFormat)}
{formatDayAwareTimestamp(
message.updatedAt,
ctx.timestampFormat,
undefined,
ctx.language,
)}
</TooltipTrigger>
<TooltipPopup>
{formatChatTimestampTooltip(message.updatedAt, ctx.timestampFormat)}
{formatChatTimestampTooltip(message.updatedAt, ctx.timestampFormat, ctx.language)}
</TooltipPopup>
</Tooltip>
)}
Expand Down
Loading
Loading