+ {diff}
+
+);
diff --git a/packages/ui/src/components/chat/message/parts/ToolPart.tsx b/packages/ui/src/components/chat/message/parts/ToolPart.tsx
index ee4983de..c5a2e4a3 100644
--- a/packages/ui/src/components/chat/message/parts/ToolPart.tsx
+++ b/packages/ui/src/components/chat/message/parts/ToolPart.tsx
@@ -2,7 +2,6 @@
import React from 'react';
import { useMobileAppActions } from '@/apps/mobileAppContext';
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
-import { PatchDiff } from '@pierre/diffs/react';
import { cn } from '@/lib/utils';
import { SimpleMarkdownRenderer } from '../../MarkdownRenderer';
import { MessageFilesDisplay } from '../../FileAttachment';
@@ -10,7 +9,6 @@ import { getToolMetadata } from '@/lib/toolHelpers';
import type { ToolPart as ToolPartType, ToolState as ToolStateUnion, FilePart } from '@opencode-ai/sdk/v2';
import { toolDisplayStyles } from '@/lib/typography';
import { WorkerHighlightedCode } from '@/components/code/WorkerHighlightedCode';
-import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSessionMessageRecords, useEnsureSessionMessages } from '@/sync/sync-context';
@@ -24,8 +22,8 @@ import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
import { copyTextToClipboard } from '@/lib/clipboard';
import type { ContentChangeReason } from '@/hooks/useChatAutoFollow';
import type { ToolPopupContent } from '../types';
-import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
-import { getDefaultTheme } from '@/lib/theme/themes';
+import { PlainDiffFallback } from './PlainDiffFallback';
+import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
import {
formatEditOutput,
@@ -42,7 +40,7 @@ import { DiffViewToggle, type DiffViewMode } from '../DiffViewToggle';
import { MinDurationShineText } from './MinDurationShineText';
import { ToolRevealOnMount } from './ToolRevealOnMount';
import { getToolIcon } from './toolPresentation';
-import { useDurationTickerNow } from './useDurationTicker';
+import { useDurationTickerNow } from '@/hooks/useDurationTicker';
import {
buildTaskSummaryEntriesFromSession,
normalizeTaskSummaryEntries,
@@ -385,40 +383,6 @@ const getToolDiagnosticSection = (
};
};
-const usePierreThemeConfig = () => {
- const themeSystem = useOptionalThemeSystem();
- const fallbackLightTheme = React.useMemo(() => getDefaultTheme(false), []);
- const fallbackDarkTheme = React.useMemo(() => getDefaultTheme(true), []);
-
- const availableThemes = React.useMemo(
- () => themeSystem?.availableThemes ?? [fallbackLightTheme, fallbackDarkTheme],
- [fallbackDarkTheme, fallbackLightTheme, themeSystem?.availableThemes],
- );
- const lightThemeId = themeSystem?.lightThemeId ?? fallbackLightTheme.metadata.id;
- const darkThemeId = themeSystem?.darkThemeId ?? fallbackDarkTheme.metadata.id;
-
- const lightTheme = React.useMemo(
- () => availableThemes.find((theme) => theme.metadata.id === lightThemeId) ?? fallbackLightTheme,
- [availableThemes, fallbackLightTheme, lightThemeId],
- );
- const darkTheme = React.useMemo(
- () => availableThemes.find((theme) => theme.metadata.id === darkThemeId) ?? fallbackDarkTheme,
- [availableThemes, darkThemeId, fallbackDarkTheme],
- );
-
- React.useEffect(() => {
- ensurePierreThemeRegistered(lightTheme);
- ensurePierreThemeRegistered(darkTheme);
- }, [darkTheme, lightTheme]);
-
- const currentVariant = themeSystem?.currentTheme.metadata.variant ?? 'light';
-
- return {
- pierreTheme: { light: lightTheme.metadata.id, dark: darkTheme.metadata.id },
- pierreThemeType: currentVariant === 'dark' ? ('dark' as const) : ('light' as const),
- };
-};
-
// Parse question tool output: "User has answered your questions: "Q1"="A1", "Q2"="A2". You can now..."
const parseQuestionOutput = (output: string): Array<{ question: string; answer: string }> | null => {
const match = output.match(/^User has answered your questions:\s*(.+?)\.\s*You can now/s);
@@ -1138,30 +1102,6 @@ const TaskToolSummary: React.FC<{
);
};
-interface DiffPreviewProps {
- diff: string;
- pierreTheme: { light: string; dark: string };
- pierreThemeType: 'light' | 'dark';
- diffViewMode: DiffViewMode;
-}
-
-const TOOL_DIFF_UNSAFE_CSS = `
- [data-diff-header],
- [data-diff] {
- [data-separator] {
- height: 24px !important;
- }
- }
-`;
-
-const TOOL_DIFF_METRICS = {
- hunkLineCount: 50,
- lineHeight: 24,
- diffHeaderHeight: 44,
- hunkSeparatorHeight: 24,
- spacing: 0,
-};
-
const TOOL_COLLAPSED_CUSTOM_STYLE: React.CSSProperties = {
...toolDisplayStyles.getCollapsedStyles(),
padding: 0,
@@ -1260,85 +1200,18 @@ const renderAnimatedPathWithIcon = (path: string, animate = true, grow = true, s
);
};
-const PlainDiffFallback: React.FC<{ diff: string }> = ({ diff }) => (
-
- {diff}
-
+// The rich diff preview is the only tool-card piece that needs the
+// @pierre/diffs + Shiki stack; lazy-loading it keeps that stack out of the
+// eager chat graph. While the chunk loads, the plain-text patch renders as the
+// Suspense fallback, mirroring the preview's own error fallback.
+const LazyToolPartDiffPreview = lazyWithChunkRecovery(() => import('./ToolPartDiffPreview'));
+
+const DiffPreview: React.FC<{ diff: string; diffViewMode: DiffViewMode }> = ({ diff, diffViewMode }) => (
+ {t('aboutDialog.openChamberVersionLabel', { version: currentVersion })}
{t('aboutDialog.openCodeVersionLabel', { version: openCodeVersion || t('settings.openchamber.about.state.unknown') })}
{stop.prose}
diff --git a/packages/ui/src/components/views/walkthrough/WalkthroughView.tsx b/packages/ui/src/components/views/walkthrough/WalkthroughView.tsx index c0403b75..51b20387 100644 --- a/packages/ui/src/components/views/walkthrough/WalkthroughView.tsx +++ b/packages/ui/src/components/views/walkthrough/WalkthroughView.tsx @@ -10,14 +10,16 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { useI18n, type Locale } from '@/lib/i18n'; +import { openExternalUrl } from '@/lib/url'; import { buildWalkthroughView } from '@/lib/walkthrough/model'; import type { WalkthroughSource, WalkthroughWorkingTreeScope } from '@/lib/walkthrough/types'; import { ModelSelector } from '@/components/sections/agents/ModelSelector'; -import { deriveBaseBranch } from '@/components/views/git/baseBranch'; +import { deriveBaseBranch, hasResolvableBaseBranch } from '@/components/views/git/baseBranch'; import { runtimeFetch } from '@/lib/runtime-fetch'; import { useConfigStore } from '@/stores/useConfigStore'; -import { useGitBranches, useGitStatus } from '@/stores/useGitStore'; +import { useGitBranches, useGitStatus, useGitStore } from '@/stores/useGitStore'; import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore'; import { getFreshestPrStatusForBranch, @@ -41,6 +43,12 @@ interface WalkthroughViewProps { const SCOPES: WalkthroughWorkingTreeScope[] = ['all', 'staged', 'working']; +// What a walkthrough is — and what it deliberately is not — cannot be read off +// the panel: the first question users asked about it was whether its marks were +// review findings. The guide answers that, so it is reachable from the surface +// itself rather than only from the release announcement. +const WALKTHROUGH_GUIDE_URL = 'https://docs.openchamber.dev/walkthrough/'; + // DropdownMenuLabel defaults to the same size and weight as its items, which // makes a heading read as another choice. This matches SelectLabel, the // treatment used by the worktree picker. @@ -152,6 +160,12 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => { const status = useGitStatus(directory || null); const branches = useGitBranches(directory || null); + const ensureAll = useGitStore((state) => state.ensureAll); + const { github, git } = useRuntimeAPIs(); + + useEffect(() => { + if (directory) void ensureAll(directory, git); + }, [directory, ensureAll, git]); // The branch source reviews everything on this branch that is not on its // base. Three-dot semantics server-side mean merges from the base are @@ -162,22 +176,33 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => { if (!headRef) return null; const all = branches?.all ?? []; const localBranches = all.filter((name) => !name.startsWith('remotes/')); + const remoteBranches = all + .filter((name) => name.startsWith('remotes/')) + .map((name) => name.slice('remotes/'.length)); const remoteNames = new Set( - all - .filter((name) => name.startsWith('remotes/')) - .map((name) => name.slice('remotes/'.length).split('/')[0]) + remoteBranches + .map((name) => name.split('/')[0]) .filter(Boolean) ); - const baseRef = deriveBaseBranch({ remoteNames, localBranches }); - if (!baseRef || baseRef === headRef) return null; + const trackingRemote = status?.tracking?.split('/')[0]; + const defaultBranch = (trackingRemote && branches?.defaultBranches?.[trackingRemote]) + ?? branches?.defaultBranches?.origin; + const baseRef = deriveBaseBranch({ + remoteNames, + localBranches, + defaultBranch, + headBranch: headRef, + }); + if (!baseRef || baseRef === headRef || !hasResolvableBaseBranch({ baseBranch: baseRef, localBranches, remoteBranches })) { + return null; + } return { kind: 'branch', baseRef, headRef }; - }, [branches, currentBranch]); + }, [branches, currentBranch, status?.tracking]); // The pull request for this branch used to appear only after visiting the PR // panel, because nothing else asked GitHub about it. Ask here too: the status // store already dedupes by signature and throttles by TTL, so several panels // wanting the same answer produce one request. - const { github } = useRuntimeAPIs(); const githubConnected = useGitHubAuthStore((state) => state.status?.connected ?? false); const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked); const ensurePrStatusEntry = useGitHubPrStatusStore((state) => state.ensureEntry); @@ -323,15 +348,36 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => { // Explicit pick first, then the model that actually produced what is on // screen, then whatever settings resolve to. The middle step is what makes // reopening a review show the model behind it rather than the default. - const activeModel = selectedModel - ?? (entry.result?.model ? `${entry.result.model.providerID}/${entry.result.model.modelID}` : undefined) - ?? (entry.readiness?.model ? `${entry.readiness.model.providerID}/${entry.readiness.model.modelID}` : undefined); - const [activeProviderId, ...activeModelParts] = (activeModel ?? '').split('/'); - const activeModelId = activeModelParts.join('/'); - + // Never present a provider without a usable login as the current selection — + // the picker already hides them from the menu; showing one as selected was + // the whole "why say so?" failure mode. const modelsMetadata = useConfigStore((state) => state.modelsMetadata); const [modelProviders, setModelProviders] = useState{t('walkthrough.help.guide')}
+