fix: dock workspace history and detect Git on project switches
Keep commit history at the sidebar bottom, expand it to half the Git content area, and preserve independently scrolling panes when resizing. Use the shared refresh action and remove the redundant changed-files border. Load Git status on project changes regardless of restored sidebar selection, and keep status and error fallback scoped to the selected project. Release note: 提交历史默认贴底,展开占用一半内容区,支持拖拽调整并统一使用顶部刷新。切换项目后自动识别 Git 工作区,无需手动刷新。
This commit is contained in:
@@ -5843,6 +5843,115 @@ describe("App", () => {
|
||||
expect(await screen.findByText("tree-only.txt")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("Git project switch detection", () => {
|
||||
const secondProject = {
|
||||
...project,
|
||||
id: "00000000-0000-4000-8000-000000000102",
|
||||
name: "Second project",
|
||||
rootPath: "C:\\Second",
|
||||
executionSpace: { kind: "local" as const, rootPath: "C:\\Second" },
|
||||
};
|
||||
const repositoryChanges = {
|
||||
rootPath: project.rootPath,
|
||||
available: true,
|
||||
status: "?? first-only.md",
|
||||
patch: "",
|
||||
files: [{ path: "first-only.md", status: "??" }],
|
||||
truncated: false,
|
||||
};
|
||||
const nonRepositoryChanges = {
|
||||
rootPath: secondProject.rootPath,
|
||||
available: false,
|
||||
status: "",
|
||||
patch: "",
|
||||
files: [],
|
||||
truncated: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.setItem("goodbuddy.active-project.v1", projectId);
|
||||
localStorage.setItem("goodbuddy.workbar-layout.v1", JSON.stringify({
|
||||
instances: [
|
||||
{ id: "10000000-0000-4000-8000-000000000001", appId: "tasks", title: "任务中心" },
|
||||
{ id: "10000000-0000-4000-8000-000000000002", appId: "workspace", title: "工作区" },
|
||||
],
|
||||
activeInstanceId: "10000000-0000-4000-8000-000000000002",
|
||||
expanded: true,
|
||||
dock: "right",
|
||||
widthRatio: 0.3,
|
||||
taskScope: "current-project",
|
||||
}));
|
||||
vi.spyOn(api.projects, "list").mockResolvedValue([project, secondProject]);
|
||||
vi.spyOn(api.workspace, "listDirectory").mockResolvedValue({
|
||||
path: "", entries: [], truncated: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("detects a Git project with restored workspace selection without refresh", async () => {
|
||||
vi.spyOn(api.workspace, "getChanges")
|
||||
.mockResolvedValueOnce({ ...nonRepositoryChanges, rootPath: project.rootPath })
|
||||
.mockResolvedValueOnce({ ...repositoryChanges, rootPath: secondProject.rootPath });
|
||||
render(<App />);
|
||||
fireEvent.click(screen.getByLabelText("切换助手工作栏"));
|
||||
expect(await screen.findByRole("tab", { name: "工作区" })).toHaveAttribute("aria-selected", "true");
|
||||
await waitFor(() => expect(api.workspace.getChanges).toHaveBeenCalledWith(projectId));
|
||||
expect(screen.queryByRole("button", { name: "Git 工作区" })).not.toBeInTheDocument();
|
||||
|
||||
selectProjectOption(secondProject.name);
|
||||
|
||||
expect(await screen.findByRole("button", { name: "Git 工作区" })).toBeVisible();
|
||||
expect(api.workspace.getChanges).toHaveBeenLastCalledWith(secondProject.id);
|
||||
expect(api.workspace.getChanges).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it.each(["non-repository", "error result", "rejection"] as const)(
|
||||
"clears previous Git flags and files when switching to a %s",
|
||||
async (outcome) => {
|
||||
const next = deferred<Awaited<ReturnType<DesktopApi["workspace"]["getChanges"]>>>();
|
||||
vi.spyOn(api.workspace, "getChanges")
|
||||
.mockResolvedValueOnce(repositoryChanges)
|
||||
.mockReturnValueOnce(next.promise);
|
||||
render(<App />);
|
||||
fireEvent.click(screen.getByLabelText("切换助手工作栏"));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Git 工作区" }));
|
||||
expect(await screen.findByText("first-only.md")).toBeVisible();
|
||||
|
||||
selectProjectOption(secondProject.name);
|
||||
await waitFor(() => expect(api.workspace.getChanges).toHaveBeenLastCalledWith(secondProject.id));
|
||||
expect(screen.queryByRole("button", { name: "Git 工作区" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("first-only.md")).not.toBeInTheDocument();
|
||||
await act(async () => {
|
||||
if (outcome === "rejection") next.reject(new Error("Git offline"));
|
||||
else next.resolve({ ...nonRepositoryChanges, ...(outcome === "error result" ? { error: "Git offline" } : {}) });
|
||||
});
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Git 工作区" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("first-only.md")).not.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["error result", "rejection"] as const)(
|
||||
"preserves same-project Git flags and files after a refresh %s",
|
||||
async (outcome) => {
|
||||
const getChanges = vi.spyOn(api.workspace, "getChanges").mockResolvedValueOnce(repositoryChanges);
|
||||
render(<App />);
|
||||
fireEvent.click(screen.getByLabelText("切换助手工作栏"));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Git 工作区" }));
|
||||
expect(await screen.findByText("first-only.md")).toBeVisible();
|
||||
if (outcome === "rejection") getChanges.mockRejectedValueOnce(new Error("Git offline"));
|
||||
else getChanges.mockResolvedValueOnce({ ...nonRepositoryChanges, rootPath: project.rootPath, error: "Git offline" });
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "刷新工作区文件" }));
|
||||
});
|
||||
|
||||
await waitFor(() => expect(getChanges).toHaveBeenCalledTimes(2));
|
||||
expect(screen.getByRole("button", { name: "Git 工作区" })).toBeVisible();
|
||||
expect(screen.getByText("first-only.md")).toBeVisible();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores stale Git changes after switching projects", async () => {
|
||||
const secondProject = {
|
||||
...project,
|
||||
@@ -5904,13 +6013,15 @@ describe("App", () => {
|
||||
});
|
||||
fireEvent.click(screen.getByLabelText("切换助手工作栏"));
|
||||
expect(await screen.findByText("second.md")).toBeInTheDocument();
|
||||
resolveFirst?.({
|
||||
rootPath: project.rootPath,
|
||||
available: true,
|
||||
status: "?? stale.md",
|
||||
patch: "",
|
||||
files: [{ path: "stale.md", status: "??" }],
|
||||
truncated: false,
|
||||
await act(async () => {
|
||||
resolveFirst?.({
|
||||
rootPath: project.rootPath,
|
||||
available: true,
|
||||
status: "?? stale.md",
|
||||
patch: "",
|
||||
files: [{ path: "stale.md", status: "??" }],
|
||||
truncated: false,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
|
||||
@@ -2033,7 +2033,10 @@ function App(): React.JSX.Element {
|
||||
const assistantTasksRef = useRef(assistantTasks);
|
||||
const [tokenUsage, setTokenUsage] =
|
||||
useState<TokenUsageSummary>(emptyTokenUsage);
|
||||
const [workspaceChanges, setWorkspaceChanges] = useState<WorkspaceChanges>();
|
||||
const [workspaceChanges, setWorkspaceChanges] = useState<{
|
||||
projectId: string;
|
||||
changes: WorkspaceChanges;
|
||||
}>();
|
||||
const [assistantArtifacts, setAssistantArtifacts] = useState<
|
||||
AssistantArtifact[]
|
||||
>([]);
|
||||
@@ -4060,9 +4063,15 @@ function App(): React.JSX.Element {
|
||||
workspaceChangesRequestRef.current === requestId &&
|
||||
activeProjectIdRef.current === projectId
|
||||
) {
|
||||
setWorkspaceChanges((current) => changes.error && current?.available
|
||||
? { ...current, error: changes.error }
|
||||
: changes);
|
||||
setWorkspaceChanges((current) => ({
|
||||
projectId,
|
||||
changes:
|
||||
changes.error &&
|
||||
current?.projectId === projectId &&
|
||||
current.changes.available
|
||||
? { ...current.changes, error: changes.error }
|
||||
: changes,
|
||||
}));
|
||||
}
|
||||
},
|
||||
[],
|
||||
@@ -5626,9 +5635,6 @@ function App(): React.JSX.Element {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (assistantSidebarTab !== "workspace") {
|
||||
return;
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
void refreshWorkspaceChanges().catch(() => {
|
||||
notify({
|
||||
@@ -5638,7 +5644,7 @@ function App(): React.JSX.Element {
|
||||
});
|
||||
}, 0);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [assistantSidebarTab, refreshWorkspaceChanges]);
|
||||
}, [refreshWorkspaceChanges]);
|
||||
|
||||
useEffect(() => {
|
||||
void window.goodbuddy.experts
|
||||
@@ -10857,7 +10863,11 @@ function App(): React.JSX.Element {
|
||||
open={assistantSidebarOpen}
|
||||
restoreFocusRef={assistantSidebarToggleRef}
|
||||
tab={assistantSidebarTab}
|
||||
workspaceChanges={workspaceChanges}
|
||||
workspaceChanges={
|
||||
workspaceChanges?.projectId === activeProjectId
|
||||
? workspaceChanges.changes
|
||||
: undefined
|
||||
}
|
||||
workspaceProjectId={activeProjectId || undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -209,6 +209,24 @@ describe('WorkspaceFilesPanel', () => {
|
||||
expect(css).toMatch(/\.workspace-git__create-branch\s*\{[^}]*justify-self: start/)
|
||||
})
|
||||
|
||||
it('docks history in both states with independently scrolling changes and commits', () => {
|
||||
const css = readFileSync(join(process.cwd(), 'src/renderer/src/styles.css'), 'utf8')
|
||||
const navigation = css.match(/\.workspace-files__navigation:has\(\.workspace-files__list:not\(\[hidden\]\) .*?\)\s*\{([^}]*)\}/)?.[1]
|
||||
expect(navigation).toContain('height: 100%')
|
||||
expect(navigation).toContain('padding-bottom: 0')
|
||||
const history = css.match(/^\.workspace-git__history\s*\{([^}]*)\}/m)?.[1]
|
||||
expect(history).toContain('flex: 1')
|
||||
expect(history).toContain('min-height: 0')
|
||||
expect(history).not.toContain('max-height')
|
||||
expect(css).toMatch(/\.workspace-git__history-scroll\s*\{[^}]*flex: 1;[^}]*overflow: auto;[^}]*min-height: 0/)
|
||||
expect(css).toMatch(/\.workspace-git__changes\s*\{[^}]*flex: 1;[^}]*min-height: 0;[^}]*overflow: auto/)
|
||||
expect(css).toMatch(/\.workspace-git__history-dock\s*\{[^}]*flex: 0 0 auto;[^}]*min-height: 0/)
|
||||
expect(css).toContain('min-height: min(80px, 50%)')
|
||||
expect(css).toContain('max-height: max(50%, calc(100% - 80px))')
|
||||
expect(css.match(/^\.workspace-files__changed\s*\{([^}]*)\}/m)?.[1]).not.toContain('border-bottom')
|
||||
expect(css).toMatch(/\.workspace-git__history-toggle\s*\{[^}]*flex-shrink: 0/)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['D:\\project', 'd:\\project\\target', 'target/notes.txt'],
|
||||
['/project', '/project', 'notes.txt'],
|
||||
@@ -299,10 +317,37 @@ describe('WorkspaceFilesPanel', () => {
|
||||
fireEvent.click(history)
|
||||
const commit = (await screen.findByText('Initial commit')).closest('button')!
|
||||
expect(commit).toHaveClass('workspace-files__changed-row', 'workspace-git__commit')
|
||||
expect(commit.closest('.workspace-files__git-view')).not.toHaveAttribute('hidden')
|
||||
const separator = screen.getByRole('separator')
|
||||
expect(separator.closest('.workspace-git__history-dock')).toHaveStyle({ flexBasis: '50%' })
|
||||
fireEvent.keyDown(separator, { key: 'ArrowUp' })
|
||||
expect(separator.closest('.workspace-git__history-dock')).toHaveStyle({ flexBasis: '55%' })
|
||||
fireEvent.click(history)
|
||||
expect(screen.queryByRole('separator')).not.toBeInTheDocument()
|
||||
fireEvent.click(history)
|
||||
expect(screen.getByRole('separator').closest('.workspace-git__history-dock')).toHaveStyle({ flexBasis: '55%' })
|
||||
fireEvent.click(commit)
|
||||
expect(await screen.findByRole('button', { name: /notes\.txt/ })).toHaveClass('workspace-files__changed-row')
|
||||
})
|
||||
|
||||
it('refreshes expanded history from the shared workspace refresh without a duplicate button', async () => {
|
||||
let version = 0
|
||||
const manage = vi.fn(async (_project: string, action: { kind: string }) => action.kind === 'history'
|
||||
? { kind: 'history', commits: [{ oid: 'a'.repeat(40), subject: `Commit ${version}`, author: 'Author', time: '2026-09-09T10:00:00Z', refs: '' }], hasMore: false }
|
||||
: { kind: 'branches', current: 'main', branches: [] })
|
||||
vi.stubGlobal('goodbuddy', { workspace: { manage } })
|
||||
const props = { projectId: 'project', isRepository: true, changedFiles: [], onListDirectory: vi.fn(async () => ({ path: '', entries: [], truncated: false })), onLoadDiff: vi.fn(), onOpenFile: vi.fn() }
|
||||
const { rerender } = render(<WorkspaceFilesPanel {...props} refreshToken={version} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Git 工作区' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交历史' }))
|
||||
const first = await screen.findByText('Commit 0')
|
||||
expect(within(first.closest('section')!).queryByRole('button', { name: '刷新' })).not.toBeInTheDocument()
|
||||
version++
|
||||
rerender(<WorkspaceFilesPanel {...props} refreshToken={version} />)
|
||||
await screen.findByText('Commit 1')
|
||||
expect(screen.queryByText('Commit 0')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('cancels an in-flight remote directory browse without moving or reopening the dialog', async () => {
|
||||
let resolveBrowse!: (value: unknown) => void
|
||||
const browseDirectories = vi.fn(() => new Promise(resolve => { resolveBrowse = resolve }))
|
||||
|
||||
@@ -507,7 +507,7 @@ export function WorkspaceFilesPanel({
|
||||
{browsedPath.split('/').filter(Boolean).map((part, index, parts) => <button type="button" key={index} title={parts.slice(0, index + 1).join('/')} aria-current={index === parts.length - 1 ? 'location' : undefined} onClick={() => browse(parts.slice(0, index + 1).join('/'))}>{part}</button>)}
|
||||
</div>
|
||||
</nav>}
|
||||
<div hidden={activeView !== 'changes'}>
|
||||
<div className="workspace-files__git-view" hidden={activeView !== 'changes'}>
|
||||
{showGit && <WorkspaceGitTools key={projectId} projectId={projectId} refreshToken={refreshToken} onRefresh={refresh} viewControl={
|
||||
<SegmentedControl ariaLabel={t('management.changedView')} value={changeView} onChange={setChangeView} options={[{ value: 'list', label: t('management.list') }, { value: 'tree', label: t('management.tree') }]} />
|
||||
}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useEffectEvent, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ChevronDown, ChevronLeft, ChevronRight, Download, FileText, GitBranch } from 'lucide-react'
|
||||
import type { WorkspaceManagementAction, WorkspaceManagementResult } from '../../shared/workspace-management-contracts'
|
||||
@@ -16,7 +16,8 @@ export function WorkspaceGitTools({ projectId, refreshToken, onRefresh, viewCont
|
||||
const [error, setError] = useState('')
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const [history, setHistory] = useState<Result<'history'>>()
|
||||
const [height, setHeight] = useState(240)
|
||||
const [historySize, setHistorySize] = useState(50)
|
||||
const workingRef = useRef<HTMLDivElement>(null)
|
||||
const [commit, setCommit] = useState<string>()
|
||||
const [files, setFiles] = useState<Result<'commitFiles'>>()
|
||||
const [patch, setPatch] = useState<Result<'commitDiff'>>()
|
||||
@@ -65,6 +66,13 @@ export function WorkspaceGitTools({ projectId, refreshToken, onRefresh, viewCont
|
||||
} catch (reason) { if (generation === request.current) setError(String(reason)) }
|
||||
finally { if (generation === request.current) setBusy(false) }
|
||||
}
|
||||
const refreshHistory = useEffectEvent(() => {
|
||||
if (historyOpen && !commit && !busy) void loadHistory()
|
||||
})
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => refreshHistory(), 0)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [projectId, refreshToken])
|
||||
const loadCommit = async (oid: string, selectedPath?: string): Promise<void> => {
|
||||
const generation = ++request.current
|
||||
setBusy(true); setError('')
|
||||
@@ -79,7 +87,9 @@ export function WorkspaceGitTools({ projectId, refreshToken, onRefresh, viewCont
|
||||
}
|
||||
return <div className="workspace-git">
|
||||
{error && <p role="alert">{error}<button type="button" disabled={busy} onClick={() => { if (commit) void loadCommit(commit, path); else void loadHistory() }}>{t('files.retry')}</button></p>}
|
||||
<div hidden={Boolean(commit)} className="workspace-git__working">
|
||||
{busy && <p role="status">{t('files.reading')}</p>}
|
||||
<div ref={workingRef} hidden={Boolean(commit)} className="workspace-git__working">
|
||||
<div className="workspace-git__current">
|
||||
<div className="workspace-git__toolbar">
|
||||
<div className="workspace-git__repository-actions">
|
||||
<button ref={branchTrigger} className="model-button workspace-git__branch-trigger" type="button" aria-expanded={branchOpen} onClick={() => setBranchOpen(!branchOpen)}><GitBranch size={14} aria-hidden="true" /><span className="model-button__label" title={branches?.current}>{branches?.current || t('management.branch')}</span><ChevronDown size={14} aria-hidden="true" /></button>
|
||||
@@ -92,16 +102,18 @@ export function WorkspaceGitTools({ projectId, refreshToken, onRefresh, viewCont
|
||||
<div className="workspace-git__branch-list">{branches?.branches.filter((branch) => branch.name.toLowerCase().includes(query.toLowerCase())).map((branch) => <button className="workspace-files__changed-row" type="button" key={`${branch.remote}:${branch.name}`} disabled={busy} aria-current={!branch.remote && branches.current === branch.name ? 'true' : undefined} onClick={() => void run({ kind: 'switchBranch', branch: branch.name, remote: branch.remote })}><GitBranch size={14} aria-hidden="true" /><span title={branch.name}>{branch.name}</span><small>{t(branch.remote ? 'management.remote' : 'management.local')}</small></button>)}</div>
|
||||
<button className="secondary-button workspace-git__create-branch" type="button" disabled={busy || !query.trim()} onClick={() => void run({ kind: 'createBranch', branch: query })}>{t('management.createBranch')}</button>
|
||||
</div>}
|
||||
{children}
|
||||
<div className="workspace-git__changes">{children}</div>
|
||||
</div>
|
||||
<div className="workspace-git__history-dock" style={{ flexBasis: historyOpen ? `${historySize}%` : undefined }}>
|
||||
{historyOpen && <div role="separator" tabIndex={0} aria-label={t('management.resizeHistory')} aria-orientation="horizontal" aria-valuemin={10} aria-valuemax={90} aria-valuenow={historySize} onKeyDown={(event) => {
|
||||
if (['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) { event.preventDefault(); setHistorySize((current) => event.key === 'Home' ? 10 : event.key === 'End' ? 90 : Math.max(10, Math.min(90, current + (event.key === 'ArrowUp' ? 5 : -5)))) }
|
||||
}} onPointerDown={(event) => { event.currentTarget.setPointerCapture(event.pointerId); event.currentTarget.dataset.startY = String(event.clientY); event.currentTarget.dataset.startSize = String(historySize) }} onPointerMove={(event) => {
|
||||
const availableHeight = workingRef.current?.getBoundingClientRect().height ?? 0
|
||||
if (availableHeight > 0 && event.currentTarget.hasPointerCapture(event.pointerId)) setHistorySize(Math.max(10, Math.min(90, Number(event.currentTarget.dataset.startSize) + (Number(event.currentTarget.dataset.startY) - event.clientY) / availableHeight * 100)))
|
||||
}} onPointerUp={(event) => event.currentTarget.releasePointerCapture(event.pointerId)} />}
|
||||
<button className="workspace-files__changed-row workspace-git__history-toggle" type="button" aria-expanded={historyOpen} onClick={() => { setHistoryOpen(!historyOpen); if (!historyOpen && !history) void loadHistory() }}>{historyOpen ? <ChevronDown size={14} aria-hidden="true" /> : <ChevronRight size={14} aria-hidden="true" />}<span>{t('management.history')}</span></button>
|
||||
<section hidden={!historyOpen} className="workspace-git__history" style={{ height }}>
|
||||
<div role="separator" tabIndex={0} aria-label={t('management.resizeHistory')} aria-orientation="horizontal" aria-valuemin={120} aria-valuemax={600} aria-valuenow={height} onKeyDown={(event) => {
|
||||
if (['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) { event.preventDefault(); setHeight((current) => event.key === 'Home' ? 120 : event.key === 'End' ? 600 : Math.max(120, Math.min(600, current + (event.key === 'ArrowUp' ? 20 : -20)))) }
|
||||
}} onPointerDown={(event) => { event.currentTarget.setPointerCapture(event.pointerId); event.currentTarget.dataset.startY = String(event.clientY); event.currentTarget.dataset.startHeight = String(height) }} onPointerMove={(event) => {
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) setHeight(Math.max(120, Math.min(600, Number(event.currentTarget.dataset.startHeight) + Number(event.currentTarget.dataset.startY) - event.clientY)))
|
||||
}} onPointerUp={(event) => event.currentTarget.releasePointerCapture(event.pointerId)} />
|
||||
<section hidden={!historyOpen} className="workspace-git__history">
|
||||
<div className="workspace-git__history-scroll">
|
||||
<button className="secondary-button" type="button" disabled={busy} onClick={() => void loadHistory()}>{t('sidebar.workspace.refresh')}</button>
|
||||
{history?.commits.map((item) => <button type="button" className="workspace-files__changed-row workspace-git__commit" key={item.oid} disabled={busy} onClick={(event) => { returnTrigger.current = event.currentTarget; void loadCommit(item.oid) }}>
|
||||
<strong title={item.subject}>{item.subject}</strong><span title={`${item.author} · ${new Date(item.time).toLocaleString()}`}>{item.author} · {new Date(item.time).toLocaleString()}</span><small title={`${item.oid} ${item.refs}`}>{item.oid.slice(0, 8)} {item.refs}</small>
|
||||
</button>)}
|
||||
@@ -109,6 +121,7 @@ export function WorkspaceGitTools({ projectId, refreshToken, onRefresh, viewCont
|
||||
{history?.hasMore && <button className="secondary-button" type="button" disabled={busy} onClick={() => void loadHistory(true)}>{t('management.loadMore')}</button>}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{commit && <section>
|
||||
<div hidden={Boolean(path)}>
|
||||
@@ -121,6 +134,5 @@ export function WorkspaceGitTools({ projectId, refreshToken, onRefresh, viewCont
|
||||
{patch?.truncated && <p>{t('files.diffTruncated')}</p>}
|
||||
</div>}
|
||||
</section>}
|
||||
{busy && <p role="status">{t('files.reading')}</p>}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -4509,7 +4509,6 @@ button > svg {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
padding-bottom: var(--space-2);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
@@ -17525,11 +17524,44 @@ details.settings-section > :not(summary) + :not(summary) {
|
||||
}
|
||||
.workspace-git__branch-list:empty { display: none; }
|
||||
.workspace-git__create-branch { justify-self: start; max-width: 100%; }
|
||||
.workspace-git__history-toggle { margin-top: var(--space-2); font-weight: 650; }
|
||||
.workspace-git__history { display: flex; flex-direction: column; flex: 0 0 auto; min-height: 120px; max-height: 600px; border-top: 1px solid var(--border-default); }
|
||||
.workspace-git__history-toggle { font-weight: 650; }
|
||||
.workspace-git__history-dock { display: flex; flex-direction: column; flex: 0 0 auto; min-height: 0; border-top: 1px solid var(--border-default); }
|
||||
.workspace-git__history-dock:has(.workspace-git__history:not([hidden])) {
|
||||
min-height: min(80px, 50%);
|
||||
max-height: max(50%, calc(100% - 80px));
|
||||
}
|
||||
.workspace-git__history { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
||||
.workspace-git__history[hidden] { display: none; }
|
||||
.workspace-git__history > [role="separator"] { height: var(--space-2); flex: 0 0 auto; cursor: ns-resize; touch-action: none; }
|
||||
.workspace-git__history-scroll { overflow: auto; min-height: 0; }
|
||||
.workspace-git__history-dock > [role="separator"] { height: var(--space-2); flex: 0 0 auto; cursor: ns-resize; touch-action: none; }
|
||||
.workspace-git__history-scroll { flex: 1; overflow: auto; min-height: 0; }
|
||||
/* Keep the history header docked even when its content is collapsed. */
|
||||
.workspace-files__navigation:has(.workspace-files__list:not([hidden]) .workspace-files__git-view:not([hidden]) .workspace-git__working:not([hidden])) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.workspace-files__navigation:has(.workspace-git) .workspace-files,
|
||||
.workspace-files__navigation:has(.workspace-git) .workspace-files__list:not([hidden]),
|
||||
.workspace-files__git-view:not([hidden]),
|
||||
.workspace-git,
|
||||
.workspace-git__current {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
.workspace-git__working { flex: 1; }
|
||||
.workspace-git__current { gap: var(--space-2); overflow: auto; }
|
||||
.workspace-git > [role="status"] { margin: 0; flex-shrink: 0; }
|
||||
.workspace-files__header,
|
||||
.workspace-git__toolbar,
|
||||
.workspace-git__history-toggle { flex-shrink: 0; }
|
||||
.workspace-git__changes {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.workspace-git__commit { display: grid; width: 100%; padding: var(--space-2); grid-template-columns: minmax(0, 1fr); gap: var(--space-1); border-bottom: 1px solid var(--border-subtle); text-align: left; }
|
||||
.workspace-git__commit strong, .workspace-git__commit small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.workspace-git__commit span, .workspace-git__commit small { color: var(--text-secondary); font-size: var(--font-caption); }
|
||||
|
||||
Reference in New Issue
Block a user