Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type {
AgentPromptFileContext,
AgentPromptFileSide,
SendToTerminalAgentInput,
} from "./useSendToTerminalAgent";
export {
formatAgentPromptWithFileContext,
useSendToTerminalAgent,
} from "./useSendToTerminalAgent";
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { toast } from "@superset/ui/sonner";
import { workspaceTrpc } from "@superset/workspace-client";
import { useCallback } from "react";
import { normalizeTerminalCommand } from "renderer/lib/terminal/launch-command";

export type AgentPromptFileSide = "additions" | "deletions" | "mixed";

export interface AgentPromptFileContext {
path: string;
startLine: number;
endLine: number;
/** Which side of the diff the selection covers. When omitted, the prompt
* is rendered without a side annotation (single-file viewer case).
* `deletions` and `mixed` are tagged explicitly because their line
* numbers refer to the pre-diff file and would otherwise be ambiguous. */
side?: AgentPromptFileSide;
}

interface FormatPromptInput {
comment: string;
file: AgentPromptFileContext;
}

/**
* Build the prompt body for sending a comment to a CLI agent that should
* be anchored to a specific file/line range (e.g. inline diff comments,
* file-viewer selections). Keep this stable so consumers can format the
* same way without sharing logic.
*/
export function formatAgentPromptWithFileContext({
comment,
file,
}: FormatPromptInput): string {
const range =
file.startLine === file.endLine
? `L${file.startLine}`
: `L${file.startLine}-L${file.endLine}`;
const sideSuffix =
file.side === "deletions"
? " (deleted lines)"
: file.side === "mixed"
? " (across deletions and additions)"
: "";
return `In ${file.path}:${range}${sideSuffix}: ${comment}`;
}

export interface SendToTerminalAgentInput {
workspaceId: string;
terminalId: string;
/** Already-formatted prompt body. Trailing newline is added by the hook. */
text: string;
}

interface UseSendToTerminalAgentResult {
send: (input: SendToTerminalAgentInput) => Promise<void>;
isPending: boolean;
}

/**
* Shared writer for pushing a comment/prompt into an existing terminal
* agent's pty via the host-service `terminal.writeInput` mutation.
* Surfaces (DiffPane composer, file-viewer comments, etc.) should funnel
* through this so the payload normalization + error toast stay consistent.
*/
export function useSendToTerminalAgent(): UseSendToTerminalAgentResult {
const writeInput = workspaceTrpc.terminal.writeInput.useMutation();

const send = useCallback(
async ({ workspaceId, terminalId, text }: SendToTerminalAgentInput) => {
try {
await writeInput.mutateAsync({
workspaceId,
terminalId,
data: normalizeTerminalCommand(text),
});
} catch (error) {
const message =
error instanceof Error ? error.message : "Unknown error";
toast.error("Couldn't send to agent", { description: message });
throw error;
}
},
[writeInput],
);

return { send, isPending: writeInput.isPending };
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,52 @@ import { CodeView, type CodeViewHandle } from "@pierre/diffs/react";
import type { RendererContext } from "@superset/panes";
import { useCallback, useMemo, useRef } from "react";
import type { DiffPaneData, PaneViewerData } from "../../../../types";
import { useChangeset } from "../../../useChangeset";
import { type ChangesetFile, useChangeset } from "../../../useChangeset";
import { useOpenInExternalEditor } from "../../../useOpenInExternalEditor";
import { useSidebarDiffRef } from "../../../useSidebarDiffRef";
import { useViewedFiles } from "../../../useViewedFiles";
import { AgentCommentComposer } from "./components/AgentCommentComposer";
import { CommentThread } from "./components/CommentThread";
import { DiffHeaderMetadata } from "./components/DiffHeaderMetadata";
import { DiffHeaderPrefix } from "./components/DiffHeaderPrefix";
import {
type DiffCommentThread,
type DiffAnnotationMetadata,
useDiffAnnotationsByPath,
} from "./hooks/useDiffAnnotations";
import { useDiffCodeViewItems } from "./hooks/useDiffCodeViewItems";
import { useDiffCodeViewScroll } from "./hooks/useDiffCodeViewScroll";
import { useDiffCodeViewTheme } from "./hooks/useDiffCodeViewTheme";
import { useDiffCommentComposer } from "./hooks/useDiffCommentComposer";

interface CreateNewAgentSessionInput {
configId: string;
placement: "split-pane" | "new-tab";
prompt: string;
}

interface DiffPaneProps {
context: RendererContext<PaneViewerData>;
workspaceId: string;
onOpenFile: (path: string, openInNewTab?: boolean) => void;
onCreateNewAgentSession?: (
input: CreateNewAgentSessionInput,
) => Promise<{ terminalId: string } | null>;
}

export function DiffPane({ context, workspaceId, onOpenFile }: DiffPaneProps) {
export function DiffPane({
context,
workspaceId,
onOpenFile,
onCreateNewAgentSession,
}: DiffPaneProps) {
const data = context.pane.data as DiffPaneData;
const codeViewRef = useRef<CodeViewHandle<DiffCommentThread>>(null);
const codeViewRef = useRef<CodeViewHandle<DiffAnnotationMetadata>>(null);

const ref = useSidebarDiffRef(workspaceId);
const { files, isLoading } = useChangeset({ workspaceId, ref });
const { viewedSet, setViewed } = useViewedFiles(workspaceId);
const openInExternalEditor = useOpenInExternalEditor(workspaceId);
const annotationsByPath = useDiffAnnotationsByPath({ workspaceId });
const { options, style } = useDiffCodeViewTheme();
const threadAnnotationsByPath = useDiffAnnotationsByPath({ workspaceId });

const collapsedSet = useMemo(
() => new Set(data.collapsedFiles ?? []),
Expand All @@ -61,13 +76,38 @@ export function DiffPane({ context, workspaceId, onOpenFile }: DiffPaneProps) {
[updateData],
);

// fileByItemId is produced by useDiffCodeViewItems below, but the composer
// hook needs access to look files up at submit time. Funnel through a
// stable ref so the composer hook can be wired before items are computed
// and still read the latest map when its submit callback fires.
const fileByItemIdRef = useRef<ReadonlyMap<string, ChangesetFile>>(new Map());
const getFile = useCallback(
(itemId: string) => fileByItemIdRef.current.get(itemId),
[],
);

const {
composerAnnotationsByItemId,
onSelectedLinesChange,
onGutterUtilityClick,
clear: clearComposer,
submit: submitComposer,
} = useDiffCommentComposer({
workspaceId,
codeViewRef,
getFile,
onCreateNewAgentSession,
});

const { items, fileByItemId, pathToItemId, hasPendingDiff, hasDiffError } =
useDiffCodeViewItems({
workspaceId,
files,
collapsedSet,
annotationsByPath,
annotationsByPath: threadAnnotationsByPath,
extraAnnotationsByItemId: composerAnnotationsByItemId,
});
fileByItemIdRef.current = fileByItemId;

const { targetItemId } = useDiffCodeViewScroll({
codeViewRef,
Expand All @@ -79,8 +119,20 @@ export function DiffPane({ context, workspaceId, onOpenFile }: DiffPaneProps) {
setCollapsed,
});

const { options, style } = useDiffCodeViewTheme();

const codeViewOptions = useMemo(
() => ({
...options,
enableLineSelection: true,
enableGutterUtility: true,
onGutterUtilityClick,
}),
[options, onGutterUtilityClick],
);

const renderHeaderPrefix = useCallback(
(item: CodeViewItem<DiffCommentThread>) => {
(item: CodeViewItem<DiffAnnotationMetadata>) => {
const file = fileByItemId.get(item.id);
if (!file) return null;
return (
Expand All @@ -95,7 +147,7 @@ export function DiffPane({ context, workspaceId, onOpenFile }: DiffPaneProps) {
);

const renderHeaderMetadata = useCallback(
(item: CodeViewItem<DiffCommentThread>) => {
(item: CodeViewItem<DiffAnnotationMetadata>) => {
const file = fileByItemId.get(item.id);
if (!file) return null;
return (
Expand Down Expand Up @@ -124,11 +176,23 @@ export function DiffPane({ context, workspaceId, onOpenFile }: DiffPaneProps) {
const renderAnnotation = useCallback(
(
annotation:
| LineAnnotation<DiffCommentThread>
| DiffLineAnnotation<DiffCommentThread>,
item: CodeViewItem<DiffCommentThread>,
| LineAnnotation<DiffAnnotationMetadata>
| DiffLineAnnotation<DiffAnnotationMetadata>,
item: CodeViewItem<DiffAnnotationMetadata>,
) => {
if (item.type !== "diff") return null;
const m = annotation.metadata;
if (m.kind === "composer") {
return (
<AgentCommentComposer
workspaceId={workspaceId}
startLine={m.startLine}
endLine={m.endLine}
onCancel={clearComposer}
onSubmit={submitComposer}
/>
);
}
const annotationSide = "side" in annotation ? annotation.side : undefined;
const focused =
item.id === targetItemId &&
Expand All @@ -139,16 +203,24 @@ export function DiffPane({ context, workspaceId, onOpenFile }: DiffPaneProps) {
return (
<CommentThread
workspaceId={workspaceId}
threadId={annotation.metadata.threadId}
isResolved={annotation.metadata.isResolved}
isOutdated={annotation.metadata.isOutdated}
url={annotation.metadata.url}
comments={annotation.metadata.comments}
threadId={m.threadId}
isResolved={m.isResolved}
isOutdated={m.isOutdated}
url={m.url}
comments={m.comments}
focusTick={focused ? data.focusTick : undefined}
/>
);
},
[workspaceId, targetItemId, data.focusLine, data.focusSide, data.focusTick],
[
workspaceId,
targetItemId,
data.focusLine,
data.focusSide,
data.focusTick,
clearComposer,
submitComposer,
],
);

if (files.length === 0) {
Expand All @@ -172,12 +244,13 @@ export function DiffPane({ context, workspaceId, onOpenFile }: DiffPaneProps) {
}

return (
<CodeView<DiffCommentThread>
<CodeView<DiffAnnotationMetadata>
ref={codeViewRef}
className="h-full w-full overflow-y-auto overflow-x-clip overscroll-contain [overflow-anchor:none]"
style={style}
items={items}
options={options}
options={codeViewOptions}
onSelectedLinesChange={onSelectedLinesChange}
renderHeaderPrefix={renderHeaderPrefix}
renderHeaderMetadata={renderHeaderMetadata}
renderAnnotation={renderAnnotation}
Expand Down
Loading
Loading