Skip to content
Open
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
158 changes: 111 additions & 47 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import { FilePicker } from "./chrome/FilePicker";
import { UsageFooter } from "./chrome/UsageFooter";
import { useProjectBranches } from "./hooks/useProjectBranches";
import {
loadAllProjectsView,
loadProjectRailOpen,
loadSidebarTabOrder,
saveAllProjectsView,
saveProjectRailOpen,
type SidebarTabId,
} from "./lib/appearance";
Expand Down Expand Up @@ -209,6 +211,7 @@ import {
import { removeProjectData } from "./lib/projectData";
import {
archiveProject,
collectRailProjects,
forgetProject,
lastProjectPath,
loadRecents,
Expand Down Expand Up @@ -366,6 +369,7 @@ import {
mergeHistorySummary,
mergeProjectHistorySummary,
replaceProjectHistory,
historyAcrossProjects,
historyWithLiveSessions,
summaryFromSession,
} from "./lib/sessionHistory";
Expand Down Expand Up @@ -627,7 +631,8 @@ export default function App({
[],
);
const [projectRailOpen, setProjectRailOpen] = useState(loadProjectRailOpen);
const tabCloseScope = "project" as const;
const [allProjectsView, setAllProjectsView] = useState(loadAllProjectsView);
const tabCloseScope = allProjectsView ? "workspace" : "project";
const currentProjectDock = findProjectTerminal(projectTerminals, projectCwd);
const dockVisible = !!currentProjectDock?.open;
const [sidebarTab, setSidebarTab] = useState<SidebarTabId>(
Expand Down Expand Up @@ -942,6 +947,10 @@ export default function App({
projectCwd;
const sidebarCwdRef = useRef(sidebarCwd);
sidebarCwdRef.current = sidebarCwd;
/** A project not listed yet stays out, or its one row would pass for a full list. */
const isCachedHistoryCwd = (cwd: string) =>
cwd === sidebarCwdRef.current ||
loadedProjectsRef.current.has(normalizeProjectPath(cwd));
const sidebarCwdKey =
sidebarCwd && sidebarCwd !== "~" ? normalizeProjectPath(sidebarCwd) : null;
const historyFailed =
Expand Down Expand Up @@ -1169,6 +1178,30 @@ export default function App({
void refreshHistory(sidebarCwd);
}, [sidebarCwd, refreshHistory]);

const railProjectPaths = useMemo(
() =>
[...collectRailProjects(recents, sidebarCwd).values()].map((p) => p.path),
[recents, sidebarCwd],
);

// The all-projects list needs every rail project's rows, not only the ones
// visited so far this run.
useEffect(() => {
if (!allProjectsView) return;
for (const path of railProjectPaths) {
const key = normalizeProjectPath(path);
if (loadedProjectsRef.current.has(key)) continue;
void listSessionsByProject(path)
.then((rows) => {
setHistory((current) => replaceProjectHistory(current, path, rows));
setLoadedProjects((prev) =>
prev.has(key) ? prev : new Set(prev).add(key),
);
})
.catch(() => undefined);
}
}, [allProjectsView, railProjectPaths]);

useEffect(() => {
if (!inboxViewOpen) return;
let cancelled = false;
Expand Down Expand Up @@ -1200,7 +1233,7 @@ export default function App({
.then((summary) => {
if (!summary) return;
lastPersisted.current.set(session.id, fingerprint);
if (summary.cwd === sidebarCwdRef.current) {
if (isCachedHistoryCwd(summary.cwd)) {
setHistory((current) => mergeProjectHistorySummary(current, summary));
}
})
Expand Down Expand Up @@ -1260,7 +1293,7 @@ export default function App({
const summary = await upsertSession(session).catch(() => null);
if (!summary) return;
lastPersisted.current.set(session.id, fingerprint);
if (summary.cwd === sidebarCwdRef.current) {
if (isCachedHistoryCwd(summary.cwd)) {
setHistory((current) =>
mergeProjectHistorySummary(current, summary),
);
Expand Down Expand Up @@ -2316,30 +2349,28 @@ export default function App({
[onClosePane, onCloseTab, tabCloseScope],
);

const deckProjectTabs = useMemo(() => {
const stripTabs = useMemo(() => {
if (allProjectsView) return tabs;
// A projectless session belongs to no project, so it stands on its own
// rather than trailing the last project's tabs.
const active = tabs.find((tab) => tab.id === activeTabId);
if (active && !workspaceTabCwd(active, sessions)) return [active];
return filterTabsForProject(tabs, sessions, projectCwd);
}, [activeTabId, tabs, sessions, projectCwd]);
}, [activeTabId, allProjectsView, tabs, sessions, projectCwd]);

const onNext = useCallback(() => {
const index = deckProjectTabs.findIndex((t) => t.id === activeTabId);
if (index >= 0)
activateTab(deckProjectTabs[(index + 1) % deckProjectTabs.length].id);
}, [activateTab, activeTabId, deckProjectTabs]);
const index = stripTabs.findIndex((t) => t.id === activeTabId);
if (index >= 0) activateTab(stripTabs[(index + 1) % stripTabs.length].id);
}, [activateTab, activeTabId, stripTabs]);

const onPrev = useCallback(() => {
const index = deckProjectTabs.findIndex((t) => t.id === activeTabId);
const index = stripTabs.findIndex((t) => t.id === activeTabId);
if (index >= 0) {
activateTab(
deckProjectTabs[
(index - 1 + deckProjectTabs.length) % deckProjectTabs.length
].id,
stripTabs[(index - 1 + stripTabs.length) % stripTabs.length].id,
);
}
}, [activateTab, activeTabId, deckProjectTabs]);
}, [activateTab, activeTabId, stripTabs]);

const onVisitBack = useCallback(() => {
const openIds = new Set(tabsRef.current.map((tab) => tab.id));
Expand Down Expand Up @@ -2371,13 +2402,10 @@ export default function App({

const onActivate = useCallback(
(slot: number) => {
const tab =
slot < 0
? deckProjectTabs[deckProjectTabs.length - 1]
: deckProjectTabs[slot];
const tab = slot < 0 ? stripTabs[stripTabs.length - 1] : stripTabs[slot];
if (tab) activateTab(tab.id);
},
[activateTab, deckProjectTabs],
[activateTab, stripTabs],
);

const onFocusPane = useCallback(
Expand Down Expand Up @@ -2549,15 +2577,11 @@ export default function App({
leafIds(entry.layout).includes(sessionId),
);
if (!tab) return false;
setActiveTabId(tab.id);
setTabs((prev) =>
prev.map((entry) =>
entry.id === tab.id ? { ...entry, focusedId: sessionId } : entry,
),
);
setComposerFocused(true);
// Goes through activateTab so the current project follows a chat picked
// from another project (all-projects list, working agents).
activateTab(tab.id, sessionId);
return true;
}, []);
}, [activateTab]);

const replaceBlankPaneWithSession = useCallback((session: Session) => {
const tab =
Expand Down Expand Up @@ -2746,6 +2770,14 @@ export default function App({
if (focusOpenSession(sessionId)) return;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const session = await ensureOpenSession(sessionId);
if (!session || session.inboxAsk) return;
// The all-projects list offers other projects' chats too.
if (
looksLikeProject(session.cwd) &&
!sameProjectPath(session.cwd, projectCwdRef.current)
) {
setProjectCwd(normalizeProjectPath(session.cwd));
setRecents(rememberProject(session.cwd));
}
if (replaceBlankPaneWithSession(session)) return;
const tab = newTab(session.id);
appendTab(tab, session.cwd);
Expand Down Expand Up @@ -3343,6 +3375,16 @@ export default function App({
[activateTab, appendTab, onCwdChange, readProjectReturnMemory],
);

const onAllProjectsViewChange = useCallback((all: boolean) => {
if (all) {
setSearchViewOpen(false);
setInboxViewOpen(false);
setNotesViewOpen(false);
}
setAllProjectsView(all);
saveAllProjectsView(all);
}, []);

const pickProject = useCallback(async () => {
const path = await pickFolder();
if (path) onSelectProject(path);
Expand Down Expand Up @@ -4760,7 +4802,7 @@ export default function App({
[onOpenApprovalSession],
);

const nextTitleTabs: TitleTab[] = deckProjectTabs.map((tab) =>
const nextTitleTabs: TitleTab[] = stripTabs.map((tab) =>
toTitleTab(tab, sessions, dirtyFiles),
);
tabProjectsRef.current = new Map(
Expand All @@ -4779,17 +4821,35 @@ export default function App({
[history, sidebarCwd],
);

const sidebarGitHint = useMemo(
() => ({
...(projectBranches?.current ? { branch: projectBranches.current } : {}),
...(sidebarCwd && sidebarCwd !== "~"
? { repo: projectName(sidebarCwd) }
: {}),
}),
[projectBranches, sidebarCwd],
);
const sidebarHistory = useMemo(
() =>
historyWithLiveSessions(history, sessions, sidebarCwd, {
...(projectBranches?.current
? { branch: projectBranches.current }
: {}),
...(sidebarCwd && sidebarCwd !== "~"
? { repo: projectName(sidebarCwd) }
: {}),
}),
[history, projectBranches, sessions, sidebarCwd],
allProjectsView
? historyAcrossProjects(history, sessions, railProjectPaths, (cwd) =>
sameProjectPath(cwd, sidebarCwd) ? sidebarGitHint : undefined,
)
: historyWithLiveSessions(
history,
sessions,
sidebarCwd,
sidebarGitHint,
),
[
allProjectsView,
history,
railProjectPaths,
sessions,
sidebarCwd,
sidebarGitHint,
],
);
const inboxRelatedSessions = useMemo(() => {
const byId = new Map<string, SessionSummary>();
Expand Down Expand Up @@ -4825,19 +4885,20 @@ export default function App({
sessions
.filter(
(session) =>
!session.inboxAsk && sameProjectPath(session.cwd, sidebarCwd),
!session.inboxAsk &&
(allProjectsView || sameProjectPath(session.cwd, sidebarCwd)),
)
.map((session) =>
summaryFromSession(session, {
...(projectBranches?.current
? { branch: projectBranches.current }
: {}),
...(sidebarCwd && sidebarCwd !== "~"
? { repo: projectName(sidebarCwd) }
: {}),
}),
summaryFromSession(
session,
sameProjectPath(session.cwd, sidebarCwd)
? sidebarGitHint
: looksLikeProject(session.cwd)
? { repo: projectName(session.cwd) }
: undefined,
),
),
[projectBranches, sessions, sidebarCwd],
[allProjectsView, sessions, sidebarCwd, sidebarGitHint],
);

const onToggleSidebar = useCallback(() => {
Expand Down Expand Up @@ -5518,6 +5579,8 @@ export default function App({
onSelectProject={onSelectProject}
onOpenProject={pickProject}
onRemoveProject={onRemoveProject}
allProjectsView={allProjectsView}
onAllProjectsViewChange={onAllProjectsViewChange}
onNew={onNew}
openSessions={openProjectSessions}
onNewTerminal={onNewTerminal}
Expand Down Expand Up @@ -5614,6 +5677,7 @@ export default function App({
onGoToFile={onGoToFile}
recents={recents}
onSelectProject={onSelectProject}
showProject={allProjectsView}
/>

<main className="relative min-h-0 min-w-0 flex-1">
Expand Down
37 changes: 24 additions & 13 deletions src/chrome/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import {
type MentionIndex,
type MentionToken,
} from "../lib/fileMentions";
import type { ProjectFile } from "../lib/fs";
import { basename, type ProjectFile } from "../lib/fs";
import {
composeInboxMessage,
type InboxComposerCard,
Expand Down Expand Up @@ -90,6 +90,7 @@ import { ContextMeter } from "./ContextMeter";
import { AttachmentChip } from "./AttachmentChip";
import { BranchPicker } from "./BranchPicker";
import { CwdPicker } from "./CwdPicker";
import { ProjectLogoIcon } from "./ProjectLogoIcon";
import { FileMentionPicker } from "./FileMentionPicker";
import { FileTypeIcon } from "./FileTypeIcon";
import { InboxMiniCard } from "./InboxMiniCard";
Expand Down Expand Up @@ -1277,17 +1278,6 @@ export function Composer({
) : null}
{hideTopBar ? null : (
<div className="flex min-w-0 items-center gap-2.5 px-3 pt-2.5">
{hideProjectPicker ? null : (
<CwdPicker
cwd={cwd}
recents={recents}
projectLogoPath={projectLogoPath}
enabled={enabled}
onCwdChange={onCwdChange}
onNewTerminal={onNewTerminal}
onClose={() => ref.current?.focus()}
/>
)}
{hideBranchPicker ? null : (
<BranchPicker
cwd={cwd}
Expand Down Expand Up @@ -1480,7 +1470,7 @@ export function Composer({
if (
e.target instanceof Element &&
e.target.closest(
"[data-model-picker], [data-access-picker], [data-model-settings]",
"[data-model-picker], [data-access-picker], [data-model-settings], [data-cwd-picker], [data-cwd-submenu]",
)
) {
return;
Expand All @@ -1491,6 +1481,27 @@ export function Composer({
}}
>
<div className="flex shrink-0 items-center gap-1">
{hideProjectPicker ? null : (
<CwdPicker
cwd={cwd}
recents={recents}
projectLogoPath={projectLogoPath}
enabled={enabled}
pill
chevron
onCwdChange={onCwdChange}
onNewTerminal={onNewTerminal}
onClose={() => ref.current?.focus()}
>
<ProjectLogoIcon
path={projectLogoPath}
className="size-3.5 shrink-0"
/>
<span className="min-w-0 truncate text-[11px]">
{looksLikeProject(cwd) ? basename(cwd) : "Choose project"}
</span>
</CwdPicker>
)}
<ModelPicker
harness={harness}
model={model}
Expand Down
Loading
Loading