Skip to content
Merged
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
12 changes: 0 additions & 12 deletions src/components/chat-surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { useWorkspaceRailController } from "@/lib/use-workspace-rail-controller"
import { useResolvedFamiliars } from "@/lib/familiar-resolve";
import type { Familiar, SessionOrigin, SessionRow } from "@/lib/types";
import type { PendingChatAction } from "@/lib/pending-chat-action";
import type { PendingCodeRailOpen } from "@/lib/pending-code-rail-open";
import type { InitialCommandControls } from "@/lib/command-controls";
import { requestSummonFamiliar } from "@/lib/summon-events";

Expand Down Expand Up @@ -82,12 +81,10 @@ type Props = {
onRetryFamiliars?: () => void;
pendingProjectRoot: string | null;
pendingChatAction?: PendingChatAction;
pendingCodeRailOpen?: PendingCodeRailOpen | null;
onSetActiveFamiliar: (id: string | null) => void;
onFamiliarScopeChange: (id: string | null, opts?: { multi?: boolean; preserveSurface?: boolean }) => void;
onClearPendingProjectRoot: () => void;
onPendingChatActionHandled: () => void;
onPendingCodeRailOpenHandled: () => void;
onSessionStarted: () => void;
onSlashFromChat: (command: string, args: string) => boolean;
onOpenOnboarding: () => void;
Expand Down Expand Up @@ -123,12 +120,10 @@ export function ChatSurface({
onRetryFamiliars,
pendingProjectRoot,
pendingChatAction,
pendingCodeRailOpen,
onSetActiveFamiliar,
onFamiliarScopeChange,
onClearPendingProjectRoot,
onPendingChatActionHandled,
onPendingCodeRailOpenHandled,
onSessionStarted,
onSlashFromChat,
onOpenOnboarding,
Expand Down Expand Up @@ -185,7 +180,6 @@ export function ChatSurface({
mobileAvailable: mobileRail,
mobileOpen: mobileRailOpen,
setMobileOpen: setMobileRailOpen,
openTarget: openCodeRailTarget,
collapse: collapseCodeRail,
} = railController;

Expand Down Expand Up @@ -299,12 +293,6 @@ export function ChatSurface({
onPendingChatActionHandled();
}, [onPendingChatActionHandled, onSetActiveFamiliar, pendingChatAction, routerRef]);

useEffect(() => {
if (!pendingCodeRailOpen) return;
openCodeRailTarget(pendingCodeRailOpen);
onPendingCodeRailOpenHandled();
}, [onPendingCodeRailOpenHandled, openCodeRailTarget, pendingCodeRailOpen]);

function startProjectChat(projectRoot: string) {
setScope("conversation");
window.setTimeout(() => routerRef.current?.newChat(projectRoot), 0);
Expand Down
11 changes: 6 additions & 5 deletions src/components/code-surface-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,13 @@ assert.match(
"MODE_ALIASES routes the absorbed GitHub surface onto Code (cave-m6ys)",
);

// File/diff links from inbox cards etc. still target Chat's code rail this
// phase — retargeting them to the Code surface is an explicit follow-up.
// File/diff links from chat transcripts, inbox cards, the Projects hub —
// everywhere — land on the Code surface (cave-ohcj): the workspace bridges
// the events into code mode with the raising chat session attached.
assert.match(
workspace,
/File\/diff links target ChatSurface's code rail[\s\S]*?setPendingCodeRailOpen\([\s\S]*?setMode\("chat"\)/,
"file-open events keep targeting Chat's code rail until the flagged follow-up",
/File\/diff links land on the Code surface[\s\S]*?setPendingCodeOpen\([\s\S]*?setMode\("code"\)/,
"file-open events route to the Code surface, not Chat's code rail",
);

// The primary keyboard cluster is unchanged: Code is a quiet destination, not
Expand Down Expand Up @@ -118,7 +119,7 @@ assert.match(
);
assert.match(
workbench,
/<SessionChangesInner key=\{workRoot\} projectRoot=\{workRoot\} running=\{running\} \/>/,
/<SessionChangesInner\s+key=\{workRoot\}\s+projectRoot=\{workRoot\}\s+running=\{running\}/,
"Diff tab mounts the proven changes panel keyed+scoped to the work root",
);
assert.match(
Expand Down
51 changes: 50 additions & 1 deletion src/components/code-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
import dynamic from "next/dynamic";
import { Icon } from "@/lib/icon";
import {
codeSessionWorkRoot,
groupCodeRailSessions,
parseCodeDeepLink,
type CodeTopTab,
Expand All @@ -27,6 +28,7 @@ import { CodeSessionRail } from "@/components/code-session-rail";
import { CodeWorkbench } from "@/components/code-workbench";
import { CodeNewSession } from "@/components/code-new-session";
import type { GitHubItemTarget } from "@/lib/github-item-url";
import type { PendingCodeOpen } from "@/lib/pending-code-open";
import type { SessionRow } from "@/lib/types";

// GitHubView keeps its own chunk: CodeView opens far more often than its
Expand All @@ -45,6 +47,11 @@ export type CodeViewProps = {
onJumpToSession: (sessionId: string, familiarId?: string | null) => void;
onFocusCard: (cardId: string) => void;
githubTarget?: GitHubItemTarget | null;
/** A file/diff open raised anywhere in the app (cave-ohcj): the workspace
* routes cave:open-project-file / cave:open-file-diff /
* cave:browse-project-files here instead of Chat's code rail. */
pendingOpen?: PendingCodeOpen | null;
onPendingOpenHandled?: () => void;
onTasksRefresh: () => void;
};

Expand All @@ -54,6 +61,8 @@ export function CodeView({
onJumpToSession,
onFocusCard,
githubTarget,
pendingOpen,
onPendingOpenHandled,
onTasksRefresh,
}: CodeViewProps) {
// `?mode=code&session=<id>&ctab=<sessions|github>&wtab=<diff|files|terminal|pr>`
Expand Down Expand Up @@ -96,6 +105,36 @@ export function CodeView({
);

const groups = useMemo(() => groupCodeRailSessions(sessions), [sessions]);

// Consume a routed file/diff open (cave-ohcj): select the raising chat
// session's workbench — or, for a Projects-hub root browse, the newest
// session working in that root — and hand the target down for tab focus.
// Held with the session it resolved to so a later manual session switch
// doesn't replay a stale file focus into an unrelated workbench.
const [workbenchTarget, setWorkbenchTarget] = useState<{
open: PendingCodeOpen;
sessionId: string | null;
} | null>(null);
useEffect(() => {
if (!pendingOpen) return;
const byId = pendingOpen.sessionId
? groups.flatMap((g) => g.sessions).find((row) => row.id === pendingOpen.sessionId)
: undefined;
const root = pendingOpen.kind === "files" ? pendingOpen.root : undefined;
const trim = (p: string) => p.replace(/\/+$/, "");
const byRoot =
!byId && root
? groups.flatMap((g) => g.sessions).find((row) => trim(codeSessionWorkRoot(row)) === trim(root))
: undefined;
const target = byId ?? byRoot;
setTopTab("sessions");
if (target) setSelectedId(target.id);
// Root browse with no matching session: there is no workbench to focus —
// land on the surface and leave the rail/selection as-is.
setWorkbenchTarget(root && !target ? null : { open: pendingOpen, sessionId: target?.id ?? null });
onPendingOpenHandled?.();
}, [groups, onPendingOpenHandled, pendingOpen]);
Comment on lines +118 to +136

const selected = useMemo(() => {
if (!selectedId) return null;
for (const group of groups) {
Expand Down Expand Up @@ -175,7 +214,12 @@ export function CodeView({
<CodeSessionRail
sessions={sessions}
selectedId={selectedId ?? null}
onSelect={setSelectedId}
onSelect={(id) => {
// A manual switch is a context change — drop any pending file
// focus so it can't replay into the newly picked workbench.
setWorkbenchTarget(null);
setSelectedId(id);
}}
onNewSession={() => setNewSessionOpen(true)}
/>
</div>
Expand All @@ -198,6 +242,11 @@ export function CodeView({
key={selected.id}
row={selected}
initialTab={deepLink?.sessionId === selected.id ? deepLink?.workbenchTab : undefined}
openTarget={
workbenchTarget && (workbenchTarget.sessionId ?? selected.id) === selected.id
? workbenchTarget.open
: undefined
}
onJumpToSession={onJumpToSession}
onRefresh={onTasksRefresh}
/>
Expand Down
13 changes: 12 additions & 1 deletion src/components/code-workbench-files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,22 @@
* CodeMirror stays out of the surface's initial chunk.
*/

import React, { useCallback, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { ProjectTree } from "@/components/project-tree";
import { RailFilePreview } from "@/components/rail-file-preview";

export function CodeWorkbenchFiles({
projectRoot,
familiarId,
focusPath,
focusNonce,
}: {
projectRoot: string;
familiarId?: string | null;
/** A routed file open (cave-ohcj): selects this path in the tree/preview.
* `focusNonce` re-applies the jump when the same path repeats. */
focusPath?: string | null;
focusNonce?: number;
}) {
const [selectedPath, setSelectedPath] = useState<string | null>(null);

Expand All @@ -34,6 +40,11 @@ export function CodeWorkbenchFiles({
[projectRoot],
);

useEffect(() => {
if (!focusPath) return;
openPath(focusPath);
}, [focusNonce, focusPath, openPath]);

return (
<div className="flex h-full min-h-0">
<div className="w-64 shrink-0 overflow-y-auto border-r border-[var(--border-hairline)]">
Expand Down
27 changes: 25 additions & 2 deletions src/components/code-workbench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
codeSessionWorkRoot,
type CodeWorkbenchTab,
} from "@/lib/code-surface";
import type { PendingCodeOpen } from "@/lib/pending-code-open";
import type { SessionRow } from "@/lib/types";

const LazyFilesTab = dynamic(
Expand Down Expand Up @@ -67,17 +68,27 @@ const TAB_LABELS: Array<{ id: CodeWorkbenchTab; label: string; icon: Parameters<
export function CodeWorkbench({
row,
initialTab,
openTarget,
onJumpToSession,
onRefresh,
}: {
row: SessionRow;
/** Deep-linked workbench tab (?wtab=). */
initialTab?: CodeWorkbenchTab;
/** A routed file/diff open (cave-ohcj): lands on the Files or Diff tab with
* that path focused. `nonce` re-triggers the jump for a repeated path. */
openTarget?: PendingCodeOpen;
onJumpToSession: (sessionId: string, familiarId?: string | null) => void;
/** Re-poll the enriched session list (branch/worktree chips) after inspector mutations. */
onRefresh?: () => void;
}) {
const [tab, setTab] = useState<CodeWorkbenchTab>(initialTab ?? "diff");
// A routed open outranks the resting/deep-linked tab — a diff jump shows
// the Diff tab, a file open the Files tab (also re-applied per nonce).
useEffect(() => {
if (!openTarget) return;
setTab(openTarget.kind === "changes" ? "diff" : "files");
}, [openTarget]);
// Inspector (branches/worktrees/env) is an opt-in right column; md+ only —
// the narrow-screen treatment lands with the mobile layout pass.
const [inspectorOpen, setInspectorOpen] = useState(false);
Expand Down Expand Up @@ -173,10 +184,22 @@ export function CodeWorkbench({
{tab === "diff" ? (
// Keyed by work root: the panel's file/diff/checkpoint state is
// per-repo, and switching sessions must never show stale rows.
<SessionChangesInner key={workRoot} projectRoot={workRoot} running={running} />
<SessionChangesInner
key={workRoot}
projectRoot={workRoot}
running={running}
focusPath={openTarget?.kind === "changes" ? openTarget.path : undefined}
focusNonce={openTarget?.kind === "changes" ? openTarget.nonce : undefined}
/>
) : null}
{tab === "files" ? (
<LazyFilesTab key={workRoot} projectRoot={workRoot} familiarId={row.familiarId} />
<LazyFilesTab
key={workRoot}
projectRoot={workRoot}
familiarId={row.familiarId}
focusPath={openTarget?.kind === "files" ? openTarget.path : undefined}
focusNonce={openTarget?.kind === "files" ? openTarget.nonce : undefined}
/>
) : null}
{terminalOpened ? (
<div className={tab === "terminal" ? "h-full min-h-0" : "hidden"}>
Expand Down
Loading
Loading