Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type EditorContent,
extractFilePaths,
xmlToContent,
xmlToPlainText,
} from "./content";

describe("xmlToContent", () => {
Expand Down Expand Up @@ -196,6 +197,30 @@ describe("xmlToContent", () => {
expect(extractFilePaths(content)).toEqual(["src/sub", "src/a.ts"]);
});

it("xmlToPlainText renders folder mentions as @mentions", () => {
expect(
xmlToPlainText('look at <folder path="products/agentic_tests" /> please'),
).toBe("look at @products/agentic_tests please");
});

it("xmlToPlainText renders file mentions as @mentions", () => {
expect(
xmlToPlainText('see <file path="src/foo/bar.ts" /> for details'),
).toBe("see @foo/bar.ts for details");
});

it("xmlToPlainText renders structured chip types as @label", () => {
expect(
xmlToPlainText(
'investigate <error id="err-1" /> and <feature_flag id="flag-2" />',
),
).toBe("investigate @err-1 and @flag-2");
});

it("xmlToPlainText leaves plain text untouched", () => {
expect(xmlToPlainText("ship the fix")).toBe("ship the fix");
});

it("round-trips contentToXml for a mix of text and chips", () => {
const content: EditorContent = {
segments: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ function chipFromTag(tag: string, rawAttrs: string): MentionChip | null {
}
}

export function xmlToPlainText(xml: string): string {
return contentToPlainText(xmlToContent(xml));
}

export function xmlToContent(xml: string): EditorContent {
const segments: EditorContent["segments"] = [];
let lastIndex = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CHAT_CONTENT_MAX_WIDTH } from "@features/sessions/constants";
import { useContextUsage } from "@features/sessions/hooks/useContextUsage";
import { useConversationSearch } from "@features/sessions/hooks/useConversationSearch";
import { SessionTaskIdProvider } from "@features/sessions/hooks/useSessionTaskId";
import {
sessionStoreSetters,
useOptimisticItemsForTask,
Expand Down Expand Up @@ -267,37 +268,39 @@ export function ConversationView({
/>
)}

<VirtualizedList
ref={listRef}
items={items}
getItemKey={getItemKey}
renderItem={renderItem}
onScrollStateChange={handleScrollStateChange}
keepMounted={mcpAppIndices}
className="absolute inset-0 bg-background"
itemClassName="mx-auto px-2 py-1.5"
itemStyle={{ maxWidth: CHAT_CONTENT_MAX_WIDTH }}
footer={
<div className={compact ? "pb-1" : "pb-16"}>
<SessionFooter
task={task}
isPromptPending={isPromptPending}
promptStartedAt={promptStartedAt}
lastGenerationDuration={
lastTurnInfo?.isComplete
? Math.max(0, lastTurnInfo.durationMs - pausedDurationMs)
: null
}
lastStopReason={lastTurnInfo?.stopReason}
queuedCount={queuedMessages.length}
hasPendingPermission={pendingPermissionsCount > 0}
pausedDurationMs={pausedDurationMs}
isCompacting={isCompacting}
usage={contextUsage}
/>
</div>
}
/>
<SessionTaskIdProvider taskId={taskId}>
<VirtualizedList
ref={listRef}
items={items}
getItemKey={getItemKey}
renderItem={renderItem}
onScrollStateChange={handleScrollStateChange}
keepMounted={mcpAppIndices}
className="absolute inset-0 bg-background"
itemClassName="mx-auto px-2 py-1.5"
itemStyle={{ maxWidth: CHAT_CONTENT_MAX_WIDTH }}
footer={
<div className={compact ? "pb-1" : "pb-16"}>
<SessionFooter
task={task}
isPromptPending={isPromptPending}
promptStartedAt={promptStartedAt}
lastGenerationDuration={
lastTurnInfo?.isComplete
? Math.max(0, lastTurnInfo.durationMs - pausedDurationMs)
: null
}
lastStopReason={lastTurnInfo?.stopReason}
queuedCount={queuedMessages.length}
hasPendingPermission={pendingPermissionsCount > 0}
pausedDurationMs={pausedDurationMs}
isCompacting={isCompacting}
usage={contextUsage}
/>
</div>
}
/>
</SessionTaskIdProvider>
{showScrollButton && (
<Box className="absolute right-4 bottom-4 z-10">
<Button size="1" variant="solid" onClick={scrollToBottom}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Tooltip } from "@components/ui/Tooltip";
import { usePendingScrollStore } from "@features/code-editor/stores/pendingScrollStore";
import { MarkdownRenderer } from "@features/editor/components/MarkdownRenderer";
import { usePanelLayoutStore } from "@features/panels";
import { useSessionTaskId } from "@features/sessions/hooks/useSessionTaskId";
import { useCwd } from "@features/sidebar/hooks/useCwd";
import { useTaskStore } from "@features/tasks/stores/taskStore";
import type { FileItem } from "@hooks/useRepoFiles";
import { useRepoFiles } from "@hooks/useRepoFiles";
import { Check, Copy } from "@phosphor-icons/react";
Expand Down Expand Up @@ -46,7 +46,7 @@ function InlineFileLink({
const { filePath: rawPath, lineSuffix } = parseFilePath(text);
const filePath = resolvedPath ?? rawPath;
const filename = rawPath.split("/").pop() ?? rawPath;
const taskId = useTaskStore((s) => s.selectedTaskId);
const taskId = useSessionTaskId();
const repoPath = useCwd(taskId ?? "");
const openFileInSplit = usePanelLayoutStore((s) => s.openFileInSplit);
const requestScroll = usePendingScrollStore((s) => s.requestScroll);
Expand Down Expand Up @@ -86,7 +86,7 @@ function InlineFileLink({

function BareFileLink({ text }: { text: string }) {
const { filePath: bareFilename } = parseFilePath(text);
const taskId = useTaskStore((s) => s.selectedTaskId);
const taskId = useSessionTaskId();
const repoPath = useCwd(taskId ?? "");
const { files } = useRepoFiles(repoPath ?? undefined);
const resolved = useMemo(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FileIcon } from "@components/ui/FileIcon";
import { usePanelLayoutStore } from "@features/panels";
import { useSessionTaskId } from "@features/sessions/hooks/useSessionTaskId";
import { useCwd } from "@features/sidebar/hooks/useCwd";
import { useTaskStore } from "@features/tasks/stores/taskStore";
import { useWorkspace } from "@features/workspace/hooks/useWorkspace";
import { Flex, Text } from "@radix-ui/themes";
import { trpcClient } from "@renderer/trpc/client";
Expand Down Expand Up @@ -32,7 +32,7 @@ function toRelativePath(absolutePath: string, repoPath: string | null): string {
export const FileMentionChip = memo(function FileMentionChip({
filePath,
}: FileMentionChipProps) {
const taskId = useTaskStore((s) => s.selectedTaskId);
const taskId = useSessionTaskId();
const repoPath = useCwd(taskId ?? "");
const workspace = useWorkspace(taskId ?? undefined);
const openFileInSplit = usePanelLayoutStore((s) => s.openFileInSplit);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createContext, type ReactNode, useContext } from "react";

const SessionTaskIdContext = createContext<string | null>(null);

export function SessionTaskIdProvider({
taskId,
children,
}: {
taskId: string | null | undefined;
children: ReactNode;
}) {
return (
<SessionTaskIdContext.Provider value={taskId ?? null}>
{children}
</SessionTaskIdContext.Provider>
);
}

export function useSessionTaskId(): string | null {
return useContext(SessionTaskIdContext);
}
42 changes: 36 additions & 6 deletions apps/code/src/renderer/sagas/task/task-creation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ vi.mock("@features/sessions/service/service", () => ({
}),
}));

vi.mock("@renderer/utils/generateTitle", () => ({
createFileTagRegex: () => /<file\s+path="([^"]+)"\s*\/>/g,
}));

vi.mock("@utils/logger", () => ({
logger: {
scope: () => ({
Expand Down Expand Up @@ -409,7 +405,7 @@ describe("TaskCreationSaga", () => {
);
});

it("sets fallback title when description is attachment-only", async () => {
it("renders attachment-only description as @mention title", async () => {
const createdTask = createTask();
const startedTask = createTask({ latest_run: createRun() });
const createTaskMock = vi.fn().mockResolvedValue(createdTask);
Expand Down Expand Up @@ -438,12 +434,46 @@ describe("TaskCreationSaga", () => {

expect(createTaskMock).toHaveBeenCalledWith(
expect.objectContaining({
title: "Reading attachment\u2026",
title: "@tmp/code.ts",
description: '<file path="/tmp/code.ts" />',
}),
);
});

it("renders folder mentions as readable @mention in title", async () => {
const createdTask = createTask();
const startedTask = createTask({ latest_run: createRun() });
const createTaskMock = vi.fn().mockResolvedValue(createdTask);
const createTaskRunMock = vi.fn().mockResolvedValue(createRun());
const startTaskRunMock = vi.fn().mockResolvedValue(startedTask);

const saga = new TaskCreationSaga({
posthogClient: {
createTask: createTaskMock,
deleteTask: vi.fn(),
getTask: vi.fn(),
createTaskRun: createTaskRunMock,
startTaskRun: startTaskRunMock,
sendRunCommand: vi.fn(),
updateTask: vi.fn(),
} as never,
});

await saga.run({
content:
'look at <folder path="products/agentic_tests" /> and tell me what you see',
repository: "posthog/posthog",
workspaceMode: "cloud",
branch: "main",
});

expect(createTaskMock).toHaveBeenCalledWith(
expect.objectContaining({
title: "look at @products/agentic_tests and tell me what you see",
}),
);
});

it("truncates title to 255 chars", async () => {
const longText = "x".repeat(300);
const createdTask = createTask();
Expand Down
4 changes: 2 additions & 2 deletions apps/code/src/renderer/sagas/task/task-creation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { buildPromptBlocks } from "@features/editor/utils/prompt-builder";
import { xmlToPlainText } from "@features/message-editor/utils/content";
import { DEFAULT_PANEL_IDS } from "@features/panels/constants/panelConstants";
import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore";
import { useProvisioningStore } from "@features/provisioning/stores/provisioningStore";
Expand All @@ -18,7 +19,6 @@ import type {
import { Saga, type SagaLogger } from "@posthog/shared";
import type { PostHogAPIClient } from "@renderer/api/posthogClient";
import { trpcClient } from "@renderer/trpc";
import { createFileTagRegex } from "@renderer/utils/generateTitle";
import { getTaskRepository } from "@renderer/utils/repository";
import {
type ExecutionMode,
Expand Down Expand Up @@ -401,7 +401,7 @@ export class TaskCreationSaga extends Saga<
name: "task_creation",
execute: async () => {
const description = input.taskDescription ?? input.content ?? "";
const plainText = description.replace(createFileTagRegex(), "").trim();
const plainText = xmlToPlainText(description).trim();
const result = await this.deps.posthogClient.createTask({
title: (plainText || "Reading attachment\u2026").slice(0, 255),
description,
Expand Down
Loading