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
102 changes: 54 additions & 48 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ import {
usePreviewMiniPlayerStore,
} from "../previewMiniPlayerStore";
import { IssueDetailPanel } from "./issue/IssueDetailPanel";
import { IssuesPanel } from "./issue/IssuesPanel";
import { IssuesUnavailableState } from "./issue/IssuesUnavailableState";
import { pullRequestPanelContext } from "./pullRequest/pullRequestDetail.logic";
import { PullRequestDetailPanel } from "./pullRequest/PullRequestDetailPanel";
Expand Down Expand Up @@ -2036,8 +2035,10 @@ export default function ChatView(props: ChatViewProps) {
const activeIssueSurfaceId =
activeRightPanelSurface?.kind === "issue"
? activeRightPanelSurface.id
: activeRightPanelSurface?.kind === "issues" && activeRightPanelSurface.selected
? issueSurfaceId(activeRightPanelSurface.selected)
: activeRightPanelSurface?.kind === "repository" &&
activeRightPanelSurface.view === "issues" &&
activeRightPanelSurface.selectedIssue
? issueSurfaceId(activeRightPanelSurface.selectedIssue)
: undefined;
const handleIssueTabStatusChange = useCallback(
(status: IssueTabStatus) => {
Expand Down Expand Up @@ -4611,15 +4612,30 @@ export default function ChatView(props: ChatViewProps) {
useRightPanelStore.getState().openRepository(activeThreadRef, "pull-requests");
}, [activeThreadRef, pullRequestsSurfaceAvailable]);
const addRepositorySurface = useCallback(() => {
if (!activeThreadRef || (!(isGitRepo && supportsGitHistory) && !pullRequestsSurfaceAvailable))
const issuesAvailable = supportsIssues && activeProject !== null;
if (
!activeThreadRef ||
(!(isGitRepo && supportsGitHistory) && !pullRequestsSurfaceAvailable && !issuesAvailable)
)
return;
useRightPanelStore
.getState()
.openRepository(
activeThreadRef,
isGitRepo && supportsGitHistory ? "history" : "pull-requests",
isGitRepo && supportsGitHistory
? "history"
: pullRequestsSurfaceAvailable
? "pull-requests"
: "issues",
);
}, [activeThreadRef, isGitRepo, pullRequestsSurfaceAvailable, supportsGitHistory]);
}, [
activeProject,
activeThreadRef,
isGitRepo,
pullRequestsSurfaceAvailable,
supportsGitHistory,
supportsIssues,
]);
const { state: deviceState, loaded: deviceStateLoaded } = useDeviceState(
activeThreadRef?.environmentId ?? null,
);
Expand Down Expand Up @@ -4869,7 +4885,7 @@ export default function ChatView(props: ChatViewProps) {
) {
panels.openProactive(
activeThreadRef,
{ id: "repository", kind: "repository", view: "pull-requests" },
{ id: "repository", kind: "repository", view: "pull-requests", selectedIssue: null },
userActionRevision,
);
} else if (
Expand Down Expand Up @@ -5998,22 +6014,6 @@ export default function ChatView(props: ChatViewProps) {
},
[composerOverlayElement],
);
// Which issue is not something the chooser can know, so it opens the browser and the reader
// picks inside it — in the same tab, rather than as one more of them.
const addIssueSurface = useCallback(() => {
if (!activeThreadRef) return;
useRightPanelStore.getState().openIssues(activeThreadRef);
}, [activeThreadRef]);
const selectIssueInPanel = useCallback(
(
target: { projectId: string; provider?: string; repository: string; number: number } | null,
) => {
if (!activeThreadRef) return;
useRightPanelStore.getState().selectIssueInPanel(activeThreadRef, target);
},
[activeThreadRef],
);
const issueSurfaceAvailable = supportsIssues && activeProject !== null;
const publishComposerOverlayHeight = useCallback(
(height: number) => {
const nextHeight = Math.ceil(height);
Expand Down Expand Up @@ -9761,10 +9761,29 @@ export default function ChatView(props: ChatViewProps) {
environmentId={environmentId}
cwd={isGitRepo ? gitCwd : null}
threadRef={activeThreadRef}
issueContext={
activeProject && activeProjectRef
? {
projectId: activeProject.id,
handoffTarget: {
kind: "existing-thread",
projectRef: activeProjectRef,
draftId: composerDraftTarget,
},
}
: null
}
selectedIssue={renderedRightPanelSurface.selectedIssue ?? null}
view={renderedRightPanelSurface.view}
active={rightPanelOpen}
gitHistoryAvailable={supportsGitHistory && isGitRepo}
gitHistoryPanelStore={gitHistoryPanelStore}
issuesAvailable={supportsIssues && activeProject !== null && activeProjectRef !== null}
onSelectIssue={(selected) =>
useRightPanelStore.getState().selectRepositoryIssue(activeThreadRef, selected)
}
onIssueStateChange={handleIssueTabStatusChange}
onOpenLinkedPullRequest={openLinkedPullRequest}
onViewChange={(view) =>
useRightPanelStore.getState().selectRepositoryView(activeThreadRef, view)
}
Expand Down Expand Up @@ -9823,12 +9842,10 @@ export default function ChatView(props: ChatViewProps) {
}
onOpenLinkedIssue={openLinkedIssue}
/>
) : (renderedRightPanelSurface?.kind === "issue" ||
renderedRightPanelSurface?.kind === "issues") &&
) : renderedRightPanelSurface?.kind === "issue" &&
issuesSurfaceCapabilityState === "loading" ? (
<DetailGhost label="Loading issues" />
) : (renderedRightPanelSurface?.kind === "issue" ||
renderedRightPanelSurface?.kind === "issues") &&
) : renderedRightPanelSurface?.kind === "issue" &&
issuesSurfaceCapabilityState === "unavailable" ? (
<IssuesUnavailableState
title="Issues unavailable"
Expand Down Expand Up @@ -9859,19 +9876,6 @@ export default function ChatView(props: ChatViewProps) {
onStateChange={handleIssueTabStatusChange}
onOpenLinkedPullRequest={openLinkedPullRequest}
/>
) : renderedRightPanelSurface?.kind === "issues" && activeProject && activeProjectRef ? (
<IssuesPanel
environmentId={activeThread.environmentId}
projectId={activeProject.id}
selected={renderedRightPanelSurface.selected}
onSelect={selectIssueInPanel}
handoffTarget={{
kind: "existing-thread",
projectRef: activeProjectRef,
draftId: composerDraftTarget,
}}
onStateChange={handleIssueTabStatusChange}
/>
) : renderedRightPanelSurface?.kind === "agents" ? (
<AgentsPanel
model={agentPanelModel}
Expand Down Expand Up @@ -10536,17 +10540,18 @@ export default function ChatView(props: ChatViewProps) {
onAddRepository={addRepositorySurface}
onAddFiles={addFilesSurface}
onAddPullRequest={addPullRequestSurface}
onAddIssue={addIssueSurface}
onAddAgents={addAgentsSurface}
onAddDevice={addDeviceSurface}
browserAvailable={isPreviewSupportedInRuntime()}
terminalAvailable={activeProject !== null}
diffAvailable={isServerThread && isGitRepo}
repositoryAvailable={(isGitRepo && supportsGitHistory) || pullRequestsSurfaceAvailable}
repositoryAvailable={
(isGitRepo && supportsGitHistory) ||
pullRequestsSurfaceAvailable ||
(supportsIssues && activeProject !== null)
}
filesAvailable={activeProject !== null}
pullRequestAvailable={pullRequestSurfaceAvailable}
issueAvailable={issueSurfaceAvailable}
pullRequestsAvailable={pullRequestsSurfaceAvailable}
agentsAvailable
issueStatuses={issueTabStatuses}
deviceAvailable={activeThreadRef !== null}
Expand Down Expand Up @@ -10598,17 +10603,18 @@ export default function ChatView(props: ChatViewProps) {
onAddRepository={addRepositorySurface}
onAddFiles={addFilesSurface}
onAddPullRequest={addPullRequestSurface}
onAddIssue={addIssueSurface}
onAddAgents={addAgentsSurface}
onAddDevice={addDeviceSurface}
browserAvailable={isPreviewSupportedInRuntime()}
terminalAvailable={activeProject !== null}
diffAvailable={isServerThread && isGitRepo}
repositoryAvailable={(isGitRepo && supportsGitHistory) || pullRequestsSurfaceAvailable}
repositoryAvailable={
(isGitRepo && supportsGitHistory) ||
pullRequestsSurfaceAvailable ||
(supportsIssues && activeProject !== null)
}
filesAvailable={activeProject !== null}
pullRequestAvailable={pullRequestSurfaceAvailable}
issueAvailable={issueSurfaceAvailable}
pullRequestsAvailable={pullRequestsSurfaceAvailable}
agentsAvailable
issueStatuses={issueTabStatuses}
deviceAvailable={activeThreadRef !== null}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/RepositoryPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { repositoryViewFromKey } from "./RepositoryPanel";

describe("RepositoryPanel", () => {
it("moves through repository views with the tablist keys", () => {
expect(repositoryViewFromKey("history", "ArrowRight")).toBe("pull-requests");
expect(repositoryViewFromKey("pull-requests", "ArrowLeft")).toBe("history");
expect(repositoryViewFromKey("history", "ArrowRight")).toBe("issues");
expect(repositoryViewFromKey("pull-requests", "ArrowLeft")).toBe("issues");
expect(repositoryViewFromKey("pull-requests", "Home")).toBe("history");
expect(repositoryViewFromKey("history", "End")).toBe("pull-requests");
});
Expand Down
75 changes: 62 additions & 13 deletions apps/web/src/components/RepositoryPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import type { EnvironmentId, ScopedThreadRef } from "@t3tools/contracts";
import type { EnvironmentId, ProjectId, ScopedThreadRef } from "@t3tools/contracts";
import { Activity, lazy, Suspense, useRef, useState, type KeyboardEvent } from "react";

import type { RepositoryView } from "~/rightPanelStore";
import type { IssueSelection, RepositoryView } from "~/rightPanelStore";
import type { GitHistoryPanelStore } from "./git-history/GitHistoryPanelState";

import type { IssueHandoffTarget } from "./issue/IssueDetailPanel";
import { ThreadPullRequestsPanel } from "./pullRequest/ThreadPullRequestsPanel";
import type { IssueTabStatus } from "./RightPanelTabs";
import { Button } from "./ui/button";

const GitHistoryPanel = lazy(() => import("./GitHistoryPanel"));
const IssuesPanel = lazy(() =>
import("./issue/IssuesPanel").then(({ IssuesPanel }) => ({ default: IssuesPanel })),
);

const views = ["history", "pull-requests"] as const;
const views = ["history", "issues", "pull-requests"] as const;

export function repositoryViewFromKey(view: RepositoryView, key: string): RepositoryView | null {
const index = views.indexOf(view);
Expand All @@ -20,25 +25,39 @@ export function repositoryViewFromKey(view: RepositoryView, key: string): Reposi
return null;
}

export default function RepositoryPanel(props: {
interface RepositoryPanelProps {
readonly environmentId: EnvironmentId;
readonly cwd: string | null;
readonly threadRef: ScopedThreadRef;
readonly issueContext: { projectId: ProjectId; handoffTarget: IssueHandoffTarget } | null;
readonly selectedIssue: IssueSelection | null;
readonly view: RepositoryView;
readonly active: boolean;
readonly gitHistoryAvailable: boolean;
readonly gitHistoryPanelStore: GitHistoryPanelStore;
readonly issuesAvailable: boolean;
readonly onViewChange: (view: RepositoryView) => void;
}) {
readonly onSelectIssue: (selected: IssueSelection | null) => void;
readonly onIssueStateChange: (status: IssueTabStatus) => void;
readonly onOpenLinkedPullRequest: (link: {
repository: string;
number: number;
url: string;
}) => void;
}

export default function RepositoryPanel(props: RepositoryPanelProps) {
const tabs = useRef<Array<HTMLButtonElement | null>>([]);
const [historyActivated, setHistoryActivated] = useState(props.view === "history");
const [issuesActivated, setIssuesActivated] = useState(props.view === "issues");
if (props.view === "history" && !historyActivated) setHistoryActivated(true);
const select = (view: RepositoryView) => props.onViewChange(view);
const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>, view: RepositoryView) => {
if (props.view === "issues" && !issuesActivated) setIssuesActivated(true);

const move = (event: KeyboardEvent<HTMLButtonElement>, view: RepositoryView) => {
const next = repositoryViewFromKey(view, event.key);
if (next === null) return;
if (!next) return;
event.preventDefault();
select(next);
props.onViewChange(next);
tabs.current[views.indexOf(next)]?.focus();
};

Expand All @@ -65,10 +84,10 @@ export default function RepositoryPanel(props: {
aria-selected={props.view === view}
aria-controls={`repository-panel-${view}`}
tabIndex={props.view === view ? 0 : -1}
onClick={() => select(view)}
onKeyDown={(event) => onKeyDown(event, view)}
onClick={() => props.onViewChange(view)}
onKeyDown={(event) => move(event, view)}
>
{view === "history" ? "History" : "Pull Requests"}
{view === "history" ? "History" : view === "issues" ? "Issues" : "Pull Requests"}
</Button>
))}
</div>
Expand All @@ -80,7 +99,7 @@ export default function RepositoryPanel(props: {
aria-labelledby="repository-tab-history"
className="min-h-0 flex-1"
>
{props.gitHistoryAvailable && props.cwd !== null ? (
{props.gitHistoryAvailable && props.cwd ? (
<Suspense fallback={null}>
<GitHistoryPanel
environmentId={props.environmentId}
Expand All @@ -98,6 +117,36 @@ export default function RepositoryPanel(props: {
</div>
</Activity>
) : null}
{issuesActivated ? (
<Activity mode={props.view === "issues" ? "visible" : "hidden"}>
<div
id="repository-panel-issues"
role="tabpanel"
aria-labelledby="repository-tab-issues"
className="min-h-0 flex-1"
>
{props.issuesAvailable && props.issueContext ? (
<Suspense fallback={null}>
<IssuesPanel
environmentId={props.environmentId}
projectId={props.issueContext.projectId}
selected={props.selectedIssue}
onSelect={props.onSelectIssue}
handoffTarget={props.issueContext.handoffTarget}
onStateChange={props.onIssueStateChange}
onOpenLinkedPullRequest={props.onOpenLinkedPullRequest}
/>
</Suspense>
) : (
<div className="flex size-full items-center justify-center p-6 text-center text-sm text-muted-foreground">
{props.issuesAvailable
? "Open a project thread to browse issues."
: "Update the environment server to browse issues."}
</div>
)}
</div>
</Activity>
) : null}
<Activity mode={props.view === "pull-requests" ? "visible" : "hidden"}>
<div
id="repository-panel-pull-requests"
Expand Down
3 changes: 0 additions & 3 deletions apps/web/src/components/RightPanelTabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ function renderTabs(
onAddPullRequest={() => undefined}
onAddRepository={() => undefined}
repositoryAvailable={false}
onAddIssue={() => undefined}
onAddDiff={() => undefined}
onAddFiles={() => undefined}
onAddAgents={() => undefined}
Expand All @@ -132,8 +131,6 @@ function renderTabs(
diffAvailable={false}
filesAvailable={false}
pullRequestAvailable={false}
issueAvailable={false}
pullRequestsAvailable={false}
agentsAvailable={false}
deviceAvailable={false}
>
Expand Down
Loading
Loading