{visibleTree.length === 0 ? (
query.trim() ? (
{t("projectTree.emptyNoMatch")}
+ ) : timeFilter !== "all" ? (
+
+
{t("projectTree.emptyNoTimeFilterMatch")}
+
+
) : (
{t("projectTree.emptyNoProjects")}
diff --git a/desktop/frontend/src/components/SettingsPanel.tsx b/desktop/frontend/src/components/SettingsPanel.tsx
index 32019f9ea..4f926bfbb 100644
--- a/desktop/frontend/src/components/SettingsPanel.tsx
+++ b/desktop/frontend/src/components/SettingsPanel.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState, type Dispatch, type ReactNode, type SetStateAction } from "react";
-import { Check, ChevronDown } from "lucide-react";
+import { Check, ChevronDown, X } from "lucide-react";
import { asArray } from "../lib/array";
import { useDeferredClose } from "../lib/useMountTransition";
import { app } from "../lib/bridge";
@@ -25,6 +25,8 @@ import { AnchoredPopover } from "./AnchoredPopover";
import { MCPServersSettingsPage, SkillsSettingsPage } from "./CapabilitiesPanel";
import { MemorySettingsPage } from "./MemoryPanel";
import { CopyButton } from "./CopyButton";
+import { getSuccessPreference, setSuccessPreference, getAttentionPreference, setAttentionPreference, playSuccessChime, playAttentionChime, type SoundWavPref } from "../lib/sound";
+import { getGenerativePreset, setGenerativePreset, generativeMusic, type GenerativePreset } from "../lib/generative-music";
import { ModalCloseButton } from "./ModalCloseButton";
const SETTINGS_TABS: SettingsTab[] = ["general", "models", "bots", "mcp", "skills", "memory", "permissions", "sandbox", "network", "appearance", "updates"];
@@ -133,6 +135,7 @@ export function SettingsPanel({ onClose, onChanged, initialTab }: { onClose: ()
{tab === "permissions" && s &&
}
{tab === "sandbox" && s &&
}
{tab === "network" && s &&
}
+
{tab === "appearance" && s && (
{
+ if (typeof window === "undefined") return false;
+ try { return window.localStorage.getItem("reasonix.defaultYolo") === "1"; } catch { return false; }
+ });
+ const [soundPref, setSoundPref] = useState(getSuccessPreference());
+ const [attentionPref, setAttentionPref] = useState(getAttentionPreference());
+ const [genMusicPreset, setGenMusicPreset] = useState(getGenerativePreset());
const setLanguage = (next: LangPref) => {
setPref(next);
void apply(() => app.SetDesktopLanguage(next));
@@ -634,6 +644,84 @@ function GeneralSection({ s, busy, apply }: SectionProps) {
))}
+
+
+
+
+
+
+
+ {t("settings.notificationSoundSuccess")}
+
+
+
+
+ {t("settings.notificationSoundAttention")}
+
+
+
+
+
+
+
+ {t("settings.generativeMusicPreset")}
+
+
+
+
);
}
@@ -2959,7 +3047,7 @@ function RuleList({
{r}
diff --git a/desktop/frontend/src/components/StatusBar.tsx b/desktop/frontend/src/components/StatusBar.tsx
index cf1e05811..43ef075b1 100644
--- a/desktop/frontend/src/components/StatusBar.tsx
+++ b/desktop/frontend/src/components/StatusBar.tsx
@@ -1,7 +1,12 @@
import { useEffect, useRef, useState } from "react";
import { Tooltip } from "./Tooltip";
-import { useI18n } from "../lib/i18n";
-import { type BalanceInfo, type CollaborationMode, type ContextInfo, type JobView, type ToolApprovalMode, type WireUsage } from "../lib/types";
+import { useI18n, SPINNER_WORDS } from "../lib/i18n";
+import type { BalanceInfo, CollaborationMode, ContextInfo, JobView, ToolApprovalMode, WireUsage } from "../lib/types";
+
+function fmtTokens(n: number): string {
+ if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k";
+ return String(n);
+}
// JobsChip is the status-bar background-jobs indicator: a count that opens an
// upward popover listing the running jobs (id · label · status), mirroring the
@@ -70,8 +75,6 @@ function nowRate(u?: WireUsage): string | null {
// avgRate is the SESSION-AGGREGATE cache-hit % — Σhit/Σ(hit+miss) across every
// turn — the steadier, cost-oriented number that matches the legacy dashboard.
-// On a non-compacting DeepSeek session it trails nowRate (early cold-start turns
-// drag the average down); it overtakes only when compaction craters single turns.
function avgRate(u?: WireUsage): string | null {
if (!u) return null;
const denom = u.sessionCacheHitTokens + u.sessionCacheMissTokens;
@@ -101,6 +104,8 @@ export function StatusBar({
toolApprovalMode,
cost,
currency,
+ turnTokens,
+ turnStartAt,
modelLabel,
currentTurnCount,
}: {
@@ -113,16 +118,36 @@ export function StatusBar({
toolApprovalMode: ToolApprovalMode;
cost?: number;
currency?: string;
+ turnTokens?: number;
+ turnStartAt?: number;
modelLabel?: string;
currentTurnCount?: number;
}) {
- const { t } = useI18n();
+ const { t, locale } = useI18n();
+ const [, setTick] = useState(0);
+ useEffect(() => {
+ if (!running) return;
+ const id = window.setInterval(() => setTick((n) => n + 1), 1000);
+ return () => window.clearInterval(id);
+ }, [running]);
+ const spinnerRunning = running;
+ const spinnerWord = spinnerRunning
+ ? SPINNER_WORDS[locale][Math.floor(Date.now() / 3000) % SPINNER_WORDS[locale].length]
+ : "";
const pct = context.window ? Math.min(100, Math.round((context.used / context.window) * 100)) : null;
const compactPct = context.compactRatio ? Math.round(context.compactRatio * 100) : null;
const nowPct = nowRate(usage);
const avgPct = avgRate(usage);
const jobsList = jobs ?? [];
const costLabel = formatMoney(cost, currency);
+ // 生成耗时
+ const elapsed = running && turnStartAt ? Date.now() - turnStartAt : 0;
+ const elapsedLabel = elapsed >= 1000
+ ? elapsed >= 60000
+ ? Math.floor(elapsed / 60000) + "m " + Math.floor((elapsed % 60000) / 1000) + "s"
+ : (elapsed / 1000).toFixed(1) + "s"
+ : "";
+
const balanceLabel = balance?.available && balance.display ? balance.display : "-";
const planMode = collaborationMode === "plan";
const goalMode = collaborationMode === "goal";
@@ -156,6 +181,12 @@ export function StatusBar({
{avgPct !== null ? `${avgPct}%` : "-"}
+ {spinnerRunning && (
+
{spinnerWord}…
+ )}
+ {running && (turnTokens ?? 0) > 0 && (
+
↓ {fmtTokens(turnTokens ?? 0)} {t("status.tokens")} · {elapsedLabel}
+ )}
diff --git a/desktop/frontend/src/components/TodoPanel.tsx b/desktop/frontend/src/components/TodoPanel.tsx
index 62fe14e89..124621452 100644
--- a/desktop/frontend/src/components/TodoPanel.tsx
+++ b/desktop/frontend/src/components/TodoPanel.tsx
@@ -11,9 +11,10 @@ import { Tooltip } from "./Tooltip";
// shows the current item so the footer stays compact during a long run. The ✕
// dismisses it (onDismiss) when the user abandons the task; a fresh todo_write
// brings it back.
-export function TodoPanel({ todos, onDismiss }: { todos: Todo[]; onDismiss: () => void }) {
+export function TodoPanel({ todos, onDismiss, stale }: { todos: Todo[]; onDismiss: () => void; stale?: boolean }) {
+ void stale;
const t = useT();
- const [open, setOpen] = useState(true);
+ const [open, setOpen] = useState(false);
const currentRef = useRef(null);
const done = todos.filter((t) => t.status === "completed").length;
diff --git a/desktop/frontend/src/components/ToolCard.tsx b/desktop/frontend/src/components/ToolCard.tsx
index 83776820b..47541c110 100644
--- a/desktop/frontend/src/components/ToolCard.tsx
+++ b/desktop/frontend/src/components/ToolCard.tsx
@@ -3,7 +3,7 @@ import { CodeViewer } from "./CodeViewer";
import { DiffView } from "./DiffView";
import { ProcessCard, ProcessStatusIcon, type ProcessState, type ProcessTone } from "./ProcessCard";
import { useT } from "../lib/i18n";
-import { diffsFor, subjectOf, summarize } from "../lib/tools";
+import { diffsFor, summarize } from "../lib/tools";
import { useShellExpand } from "../lib/shellExpand";
import type { Item } from "../lib/useController";
@@ -55,7 +55,6 @@ function splitPreview(text: string, n: number): { preview: string; total: number
export const ToolCard = memo(function ToolCard({ item, subcalls }: { item: ToolItem; subcalls?: ToolItem[] }) {
const t = useT();
const diffs = diffsFor(item.name, item.args);
- const subject = subjectOf(item.name, item.args);
const nested = subcalls ?? [];
const hasNested = nested.length > 0;
const profileText =
@@ -114,7 +113,6 @@ export const ToolCard = memo(function ToolCard({ item, subcalls }: { item: ToolI
name={
<>
{item.name}
- {subject && {subject}}
{profileText && {profileText}}
>
}
diff --git a/desktop/frontend/src/components/Transcript.tsx b/desktop/frontend/src/components/Transcript.tsx
index 92b3a2511..817c6da80 100644
--- a/desktop/frontend/src/components/Transcript.tsx
+++ b/desktop/frontend/src/components/Transcript.tsx
@@ -6,7 +6,7 @@ import { replaceAttachmentRefsForDisplay } from "../lib/attachmentDisplay";
import { AssistantMessage, TurnActions, UserMessage } from "./Message";
import { ProcessCard, ProcessCompactIcon, ProcessInfoIcon, ProcessPhaseIcon, ProcessStatusIcon } from "./ProcessCard";
import { ToolCard } from "./ToolCard";
-import { ChevronRight } from "lucide-react";
+import { ChevronDown, ChevronRight } from "lucide-react";
import { Welcome } from "./Welcome";
type ToolItem = Extract- ;
@@ -167,6 +167,9 @@ export function Transcript({
const resizeFrame = useRef(null);
const lastClientHeight = useRef(null);
const lastFooterHeight = useRef(null);
+ const showFABRef = useRef(false);
+ const [showFAB, setShowFAB] = useState(false);
+ const scrollingRef = useRef(false);
const questions = useMemo(() => {
const anchors: QuestionAnchor[] = [];
@@ -182,7 +185,15 @@ export function Transcript({
const onScroll = () => {
const el = scrollRef.current;
- if (el) stick.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
+ if (!el) return;
+ if (scrollingRef.current) return;
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
+ stick.current = atBottom;
+ const shouldShow = !atBottom;
+ if (shouldShow !== showFABRef.current) {
+ showFABRef.current = shouldShow;
+ setShowFAB(shouldShow);
+ }
};
// Track question count so we can detect when the user sends a new message.
@@ -330,6 +341,35 @@ export function Transcript({
jumpToQuestion(question);
}, [turnGroups.length]);
+ const scrollToBottom = useCallback(() => {
+ const el = scrollRef.current;
+ if (!el) return;
+ stick.current = true;
+ showFABRef.current = false;
+ setShowFAB(false);
+ scrollingRef.current = true;
+ el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
+ const clear = () => { scrollingRef.current = false; };
+ el.addEventListener("scrollend", clear, { once: true });
+ // scrollend is Safari 15+; fallback for older WebViews
+ setTimeout(clear, 400);
+ }, []);
+
+ // Keyboard shortcut: Cmd+↓ scrolls to bottom
+ useEffect(() => {
+ const onKey = (e: globalThis.KeyboardEvent) => {
+ if (e.key === "ArrowDown" && (e.metaKey || e.ctrlKey)) {
+ const target = e.target as HTMLElement | null;
+ const tag = target?.tagName;
+ if (tag === "INPUT" || tag === "TEXTAREA" || target?.isContentEditable) return;
+ e.preventDefault();
+ scrollToBottom();
+ }
+ };
+ document.addEventListener("keydown", onKey);
+ return () => document.removeEventListener("keydown", onKey);
+ }, [scrollToBottom]);
+
// ── Hot zone: fully rendered from hotStartIdx to end ─────────────────────
// Memoized separately from the assembly so streaming tokens don't rebuild
// the warm/cold zone JSX trees. Uses LiveStreamContext for streaming data
@@ -403,18 +443,15 @@ export function Transcript({
// don't rebuild it. The hot zone uses LiveAssistantMessage (reads live from
// LiveStreamContext) so streaming updates are captured immediately.
return (
-
- {empty &&
}
-
- {!empty && showQuestionNav && (
-
- )}
+
+
+ {empty && }
-
+
{turnGroups.length > HOT_TURNS && (
+
+
+ {!empty && showQuestionNav && (
+
+ )}
+
+ {!empty && showFAB && (
+
+ )}
);
}
diff --git a/desktop/frontend/src/components/WorkspacePanel.tsx b/desktop/frontend/src/components/WorkspacePanel.tsx
index 7da435203..f4795f411 100644
--- a/desktop/frontend/src/components/WorkspacePanel.tsx
+++ b/desktop/frontend/src/components/WorkspacePanel.tsx
@@ -24,9 +24,10 @@ import {
import { app } from "../lib/bridge";
import { useT } from "../lib/i18n";
import { loadLayoutSize, saveLayoutSize } from "../lib/layoutPreferences";
-import type { DirEntry, FilePreview, GitCommitView, GitCommitDetailView } from "../lib/types";
+
+const PREVIEW_DISMISSED_KEY = "reasonix.workspace.previewDismissed";
+import type { DirEntry, FilePreview, GitCommitDetailView, WorkspaceChangesView } from "../lib/types";
import { formatWorkspaceReference, WORKSPACE_REF_DRAG_TYPE } from "../lib/workspaceDrag";
-import { cleanGitDiff } from "../lib/diff";
import { CodeViewer } from "./CodeViewer";
import { ContextMenu, contextMenuPointFromEvent, type ContextMenuItem, type ContextMenuPoint } from "./ContextMenu";
import { FloatingMenu, FloatingMenuItems } from "./FloatingMenu";
@@ -45,6 +46,16 @@ const WORKSPACE_CONTEXT_MENU_FILE_HEIGHT = 92;
const WORKSPACE_CONTEXT_MENU_REF_HEIGHT = 48;
const WORKSPACE_MAX_PREVIEW_TABS = 5;
+function loadPreviewDismissed(): boolean {
+ if (typeof window === "undefined") return false;
+ try { return window.localStorage.getItem(PREVIEW_DISMISSED_KEY) === "1"; } catch { return false; }
+}
+
+function savePreviewDismissed(v: boolean): void {
+ if (typeof window === "undefined") return;
+ try { window.localStorage.setItem(PREVIEW_DISMISSED_KEY, v ? "1" : "0"); } catch {}
+}
+
function clampWorkspaceTreeWidth(width: number, panelWidth?: number): number {
const maxForPanel =
typeof panelWidth === "number" && Number.isFinite(panelWidth)
@@ -161,16 +172,20 @@ function formatBytes(n: number): string {
return `${n} B`;
}
-function formatCommitDate(dateStr: string): string {
- const d = new Date(dateStr);
- if (isNaN(d.getTime())) return dateStr;
- const day = String(d.getDate()).padStart(2, "0");
- const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
- const month = monthNames[d.getMonth()];
- const year = d.getFullYear();
- const hours = String(d.getHours()).padStart(2, "0");
- const minutes = String(d.getMinutes()).padStart(2, "0");
- return `${day} ${month} ${year} ${hours}:${minutes}`;
+function isDeletedChange(row: { gitStatus?: string }): boolean {
+ return !!row.gitStatus && row.gitStatus.includes("D");
+}
+
+function changeDetail(row: {
+ path: string;
+ oldPath?: string;
+ latestPrompt?: string;
+ turns?: number[];
+}): string {
+ if (row.latestPrompt) return row.latestPrompt;
+ if (row.oldPath) return `← ${row.oldPath}`;
+ if (row.turns && row.turns.length > 0) return `#${row.turns.join(", #")}`;
+ return row.path;
}
export function WorkspacePanel({
@@ -211,11 +226,13 @@ export function WorkspacePanel({
const [preview, setPreview] = useState
(null);
const [loadingPreview, setLoadingPreview] = useState(false);
const [viewMode, setViewMode] = useState<"files" | "changed">(initialViewMode);
- const [gitHistory, setGitHistory] = useState([]);
- const [loadingHistory, setLoadingHistory] = useState(false);
- const [expandedCommit, setExpandedCommit] = useState(null);
- const [commitDetail, setCommitDetail] = useState(null);
- const [loadingCommit, setLoadingCommit] = useState(false);
+ const [previewDismissed, setPreviewDismissedState] = useState(loadPreviewDismissed());
+ const setPreviewDismissed = useCallback((v: boolean) => {
+ setPreviewDismissedState(v);
+ savePreviewDismissed(v);
+ }, []);
+ const [changes, setChanges] = useState(null);
+ const [loadingChanges, setLoadingChanges] = useState(false);
const [selectionMenu, setSelectionMenu] = useState<{ x: number; y: number; text: string; path: string } | null>(null);
const [treeMenu, setTreeMenu] = useState<{ x: number; y: number; path: string; isDir: boolean } | null>(null);
const [treeBlankMenuPoint, setTreeBlankMenuPoint] = useState(null);
@@ -224,10 +241,16 @@ export function WorkspacePanel({
const [treeWidth, setTreeWidth] = useState(loadWorkspaceTreeWidth);
const [treeResizing, setTreeResizing] = useState(false);
const [recentOpen, setRecentOpen] = useState(false);
+ const [expandedCommit, setExpandedCommit] = useState(null);
+ const [commitDetail, setCommitDetail] = useState(null);
const lastPreviewModeActiveRef = useRef(null);
const recentAnchorRef = useRef(null);
+ const changesRequestRef = useRef(0);
const openDirsRef = useRef(openDirs);
+ // "changes" 模式只显示 git diff 文件(通过 loadChanges),不加载完整目录树
+ const effectiveMode = viewMode === "changed" ? "changes" : "files";
+
useEffect(() => {
openDirsRef.current = openDirs;
}, [openDirs]);
@@ -237,15 +260,19 @@ export function WorkspacePanel({
setEntriesByDir((prev) => ({ ...prev, [dir]: entries ?? [] }));
}, []);
- const loadGitHistory = useCallback(async () => {
- setLoadingHistory(true);
+ const loadChanges = useCallback(async () => {
+ const requestId = changesRequestRef.current + 1;
+ changesRequestRef.current = requestId;
+ setLoadingChanges(true);
try {
- const result = await app.WorkspaceGitHistory(selectedPath || "");
- setGitHistory(result || []);
+ const next = await app.WorkspaceChanges();
+ if (changesRequestRef.current === requestId) setChanges(next);
} catch (err) {
- setGitHistory([]);
+ if (changesRequestRef.current === requestId) {
+ setChanges({ files: [], gitAvailable: false, gitErr: String((err as Error)?.message ?? err) });
+ }
} finally {
- setLoadingHistory(false);
+ if (changesRequestRef.current === requestId) setLoadingChanges(false);
}
}, [selectedPath]);
@@ -257,30 +284,6 @@ export function WorkspacePanel({
});
}, [onRequestPanelWidth]);
- useEffect(() => {
- if (!open) return;
- if (expandedCommit) {
- let live = true;
- setLoadingCommit(true);
- app
- .WorkspaceGitCommitDetail(expandedCommit, selectedPath || "")
- .then((detail) => {
- if (live) setCommitDetail(detail);
- })
- .catch(() => {
- if (live) setCommitDetail(null);
- })
- .finally(() => {
- if (live) setLoadingCommit(false);
- });
- return () => {
- live = false;
- };
- } else {
- setCommitDetail(null);
- }
- }, [expandedCommit, selectedPath, open]);
-
const selectFile = useCallback(
(path: string) => {
onRequestPanelWidth?.(WORKSPACE_DUAL_PANEL_TARGET_WIDTH);
@@ -303,9 +306,6 @@ export function WorkspacePanel({
setSelectedPath(null);
setOpenTabs([]);
setPreview(null);
- setGitHistory([]);
- setExpandedCommit(null);
- setCommitDetail(null);
setSelectionMenu(null);
setTreeMenu(null);
setFilter("");
@@ -333,17 +333,17 @@ export function WorkspacePanel({
useEffect(() => {
if (!open) return;
if (viewMode === "changed") {
- void loadGitHistory();
+ void loadChanges();
}
- }, [selectedPath, viewMode, loadGitHistory, open]);
+ }, [open, viewMode, loadChanges]);
useEffect(() => {
if (!open || !refreshKey) return;
if (viewMode === "changed") {
- void loadGitHistory();
+ void loadChanges();
}
openDirsRef.current.forEach((dir) => void loadDir(dir));
- }, [loadGitHistory, loadDir, open, refreshKey, viewMode]);
+ }, [loadChanges, loadDir, open, refreshKey, viewMode]);
useEffect(() => {
if (!selectionMenu && !treeMenu) return;
@@ -368,14 +368,14 @@ export function WorkspacePanel({
setTreeBlankMenuPoint(null);
setSelectionMenu(null);
setTreeMenu(null);
- if (viewMode === "changed") {
- void loadGitHistory();
+ if (effectiveMode === "changes") {
+ void loadChanges();
return;
}
const dirs = Array.from(openDirsRef.current);
setEntriesByDir({});
dirs.forEach((dir) => void loadDir(dir));
- }, [loadGitHistory, loadDir, viewMode]);
+ }, [loadChanges, loadDir, effectiveMode, viewMode]);
const refreshSelected = useCallback(() => {
if (!selectedPath) return;
@@ -467,8 +467,13 @@ export function WorkspacePanel({
.sort((a, b) => a.path.localeCompare(b.path));
}, [entriesByDir, filter]);
- const searchPlaceholder = t("workspace.filter");
-
+ const changedRows = useMemo(() => {
+ const rows = changes?.files ?? [];
+ const q = filter.trim().toLowerCase();
+ if (!q) return rows;
+ return rows.filter((row) => `${row.path} ${row.oldPath ?? ""} ${row.gitStatus ?? ""}`.toLowerCase().includes(q));
+ }, [changes?.files, filter]);
+ const searchPlaceholder = effectiveMode === "changes" ? t("workspace.filterChanges") : t("workspace.filter");
const effectiveTreeWidth = useMemo(() => clampWorkspaceTreeWidth(treeWidth, panelWidth), [panelWidth, treeWidth]);
const filePreviewActive = openTabs.length > 0 || selectedPath !== null;
const changeDetailActive = changedMode && expandedCommit !== null;
@@ -502,12 +507,14 @@ export function WorkspacePanel({
}, [onClose, open, previewVisible, treeVisible]);
const hideTreeOrClosePanel = useCallback(() => {
- if (previewVisible) {
+ if (previewDismissed) {
+ setPreviewDismissed(false);
+ } else if (previewVisible) {
setTreeVisible(false);
} else {
onClose();
}
- }, [onClose, previewVisible]);
+ }, [onClose, previewDismissed, previewVisible]);
const setSavedTreeWidth = useCallback(
(width: number) => {
@@ -690,6 +697,40 @@ export function WorkspacePanel({
};
const isMarkdown = selectedPath?.toLowerCase().endsWith(".md") ?? false;
+
+ const renderChangedRows = () => {
+ if (loadingChanges) return {t("workspace.loading")}
;
+ if (!changes) return null;
+ if (changedRows.length === 0) return {t("workspace.noChanges")}
;
+ return changedRows.map((row) => {
+ const deleted = isDeletedChange(row);
+ return (
+
+ );
+ });
+ };
+
const treeBlankMenuItems: ContextMenuItem[] = [
{
key: "refresh-tree",
@@ -740,13 +781,19 @@ export function WorkspacePanel({
{maximized ? : }
- {selectedPath && (
+ {selectedPath ? (
- )}
+ ) : viewMode === "changed" ? (
+
+
+
+ ) : null}
- {viewMode === "changed" && !selectedPath ? (
-
- {loadingHistory ? (
-
{t("workspace.loading")}
- ) : gitHistory.length === 0 ? (
-
{t("workspace.noChanges")}
- ) : (
-
- {gitHistory.map((commit) => (
-
-
- {expandedCommit === commit.hash && (
-
- {loadingCommit ? (
-
{t("workspace.loading")}
- ) : commitDetail?.diff ? (
-
- ) : commitDetail?.files ? (
-
- {commitDetail.files.map((file) => (
-
- ))}
-
- ) : (
-
No details available
- )}
-
- )}
-
- ))}
-
- )}
-
- ) : viewMode === "changed" && selectedPath ? (
-
- {loadingHistory ? (
-
{t("workspace.loading")}
- ) : gitHistory.length === 0 ? (
-
{t("workspace.noChanges")}
- ) : (
-
- {gitHistory.map((commit) => (
-
-
- {expandedCommit === commit.hash && (
-
- {loadingCommit ? (
-
{t("workspace.loading")}
- ) : commitDetail?.diff ? (
-
- ) : (
-
No details available
- )}
-
- )}
-
- ))}
-
- )}
-
- ) : !selectedPath ? (
-
{t("workspace.pickFile")}
- ) : loadingPreview ? (
+ {!selectedPath ? (
{t("workspace.loading")}
) : preview?.err ? (
{preview.err}
@@ -1011,7 +962,6 @@ export function WorkspacePanel({
className={viewMode === "changed" ? "workspace-files__tab workspace-files__tab--active" : "workspace-files__tab"}
onClick={() => {
setViewMode("changed");
- void loadGitHistory();
}}
>
@@ -1019,13 +969,23 @@ export function WorkspacePanel({
)}
- {showViewTabs && (
-
-
-
- )}
+ {showViewTabs && null}
+
+ )}
+
+ {effectiveMode === "changes" && changes && changes.files.length > 0 && (
+
+ {changes.files.length} {changes.files.length === 1 ? "file" : "files"}
+ {(() => {
+ const onlySession = changes.files.filter((f) => f.sources.length === 1 && f.sources[0] === "session").length;
+ const onlyGit = changes.files.filter((f) => f.sources.length === 1 && f.sources[0] === "git").length;
+ const both = changes.files.length - onlySession - onlyGit;
+ const parts: string[] = [];
+ if (both > 0) parts.push(`${both} session+git`);
+ if (onlyGit > 0) parts.push(`${onlyGit} git`);
+ if (onlySession > 0) parts.push(`${onlySession} session`);
+ return parts.length > 0 ? · {parts.join(" · ")} : null;
+ })()}
)}
@@ -1033,6 +993,14 @@ export function WorkspacePanel({