Skip to content

Commit 54da45c

Browse files
CopilotBunsDev
andauthored
merge main into PR branch
Agent-Logs-Url: https://github.com/OpenKnots/okcode/sessions/c09e6a8c-001f-4ada-a5f7-e3b0018da8f4 Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com>
1 parent ccdc37e commit 54da45c

21 files changed

Lines changed: 787 additions & 193 deletions

apps/server/src/wsServer.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import {
6464
ProjectionSnapshotQuery,
6565
type ProjectionSnapshotQueryShape,
6666
} from "./orchestration/Services/ProjectionSnapshotQuery";
67+
import { createAttachmentId } from "./attachmentStore";
6768

6869
const asEventId = (value: string): EventId => EventId.makeUnsafe(value);
6970
const asProviderItemId = (value: string): ProviderItemId => ProviderItemId.makeUnsafe(value);
@@ -687,6 +688,29 @@ describe("WebSocket Server", () => {
687688
expect(bytes).toEqual(Buffer.from("hello-attachment"));
688689
});
689690

691+
it("serves persisted attachments by attachment id", async () => {
692+
const baseDir = makeTempDir("okcode-state-attachment-id-");
693+
const { attachmentsDir } = deriveServerPathsSync(baseDir, undefined);
694+
const attachmentId = createAttachmentId("thread-preview");
695+
if (!attachmentId) {
696+
throw new Error("Failed to create a safe test attachment id.");
697+
}
698+
const attachmentPath = path.join(attachmentsDir, `${attachmentId}.patch`);
699+
fs.mkdirSync(path.dirname(attachmentPath), { recursive: true });
700+
fs.writeFileSync(attachmentPath, Buffer.from("diff --git a/a.ts b/a.ts\n"));
701+
702+
const { cwd } = makeWorkspaceFixture("project");
703+
server = await createTestServer({ cwd, baseDir });
704+
const addr = server.address();
705+
const port = typeof addr === "object" && addr !== null ? addr.port : 0;
706+
expect(port).toBeGreaterThan(0);
707+
708+
const response = await fetch(`http://127.0.0.1:${port}/attachments/${attachmentId}`);
709+
expect(response.status).toBe(200);
710+
expect(response.headers.get("content-type")).toContain("text/x-diff");
711+
expect(await response.text()).toContain("diff --git");
712+
});
713+
690714
it("serves persisted attachments for URL-encoded paths", async () => {
691715
const baseDir = makeTempDir("okcode-state-attachments-encoded-");
692716
const { attachmentsDir } = deriveServerPathsSync(baseDir, undefined);

apps/server/src/wsServer.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,26 @@ function isNewerSemver(a: string, b: string): boolean {
137137
return false;
138138
}
139139

140+
function inferAttachmentContentType(filePath: string): string {
141+
const mimeType = Mime.getType(filePath);
142+
if (mimeType) {
143+
return mimeType;
144+
}
145+
146+
const normalizedPath = filePath.toLowerCase();
147+
if (normalizedPath.endsWith(".patch") || normalizedPath.endsWith(".diff")) {
148+
return "text/x-diff; charset=utf-8";
149+
}
150+
if (normalizedPath.endsWith(".md")) {
151+
return "text/markdown; charset=utf-8";
152+
}
153+
if (normalizedPath.endsWith(".txt")) {
154+
return "text/plain; charset=utf-8";
155+
}
156+
157+
return "application/octet-stream";
158+
}
159+
140160
/**
141161
* Remote address from the HTTP upgrade (`request.socket`). The `ws` library often does not
142162
* expose a reliable `socket.remoteAddress` when handling messages, so we capture it here.
@@ -717,7 +737,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
717737
return;
718738
}
719739

720-
const contentType = Mime.getType(filePath) ?? "application/octet-stream";
740+
const contentType = inferAttachmentContentType(filePath);
721741
res.writeHead(200, {
722742
"Content-Type": contentType,
723743
"Cache-Control": "public, max-age=31536000, immutable",

apps/web/src/components/ChatView.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -434,10 +434,10 @@ export default function ChatView({
434434
const navigate = useNavigate();
435435
const activeProjectId = threads.find((t) => t.id === threadId)?.projectId ?? null;
436436
const previewOpen = usePreviewStateStore((state) =>
437-
activeProjectId ? (state.openByProjectId[activeProjectId] ?? false) : false,
437+
activeThreadId ? (state.openByThreadId[activeThreadId] ?? false) : false,
438438
);
439-
const togglePreviewOpen = usePreviewStateStore((state) => state.toggleProjectOpen);
440-
const setPreviewOpen = usePreviewStateStore((state) => state.setProjectOpen);
439+
const togglePreviewOpen = usePreviewStateStore((state) => state.toggleThreadOpen);
440+
const setPreviewOpen = usePreviewStateStore((state) => state.setThreadOpen);
441441
const previewDock = usePreviewStateStore((state) =>
442442
activeProjectId ? (state.dockByProjectId[activeProjectId] ?? "top") : "top",
443443
);
@@ -1747,7 +1747,7 @@ export default function ChatView({
17471747
const handlePreviewUrl = useCallback(
17481748
(url: string) => {
17491749
if (!activeProject || !activeThread) return;
1750-
setPreviewOpen(activeProject.id, true);
1750+
setPreviewOpen(activeThread.id, true);
17511751
void previewBridgeRef?.createTab({ url });
17521752
},
17531753
[activeProject, activeThread, setPreviewOpen, previewBridgeRef],
@@ -4937,7 +4937,7 @@ export default function ChatView({
49374937
onImportProjectScripts={importProjectScripts}
49384938
onToggleTerminal={toggleTerminalVisibility}
49394939
onPrefetchTerminal={preloadThreadTerminalDrawer}
4940-
onTogglePreview={() => activeProjectId && togglePreviewOpen(activeProjectId)}
4940+
onTogglePreview={() => activeThreadId && togglePreviewOpen(activeThreadId)}
49414941
onTogglePreviewLayout={() => activeProjectId && togglePreviewLayout(activeProjectId)}
49424942
onMinimize={onMinimize}
49434943
/>
@@ -4980,7 +4980,7 @@ export default function ChatView({
49804980
key={previewPanelKey ?? undefined}
49814981
projectId={activeProject!.id}
49824982
threadId={threadId}
4983-
onClose={() => setPreviewOpen(activeProject!.id, false)}
4983+
onClose={() => setPreviewOpen(threadId, false)}
49844984
/>
49854985
</div>
49864986
<div
@@ -5003,7 +5003,7 @@ export default function ChatView({
50035003
key={previewPanelKey ?? undefined}
50045004
projectId={activeProject!.id}
50055005
threadId={threadId}
5006-
onClose={() => setPreviewOpen(activeProject!.id, false)}
5006+
onClose={() => setPreviewOpen(threadId, false)}
50075007
/>
50085008
</div>
50095009
) : null}
@@ -5884,7 +5884,7 @@ export default function ChatView({
58845884
key={previewPanelKey ?? undefined}
58855885
projectId={activeProject!.id}
58865886
threadId={threadId}
5887-
onClose={() => setPreviewOpen(activeProject!.id, false)}
5887+
onClose={() => setPreviewOpen(threadId, false)}
58885888
/>
58895889
</div>
58905890
</>

apps/web/src/components/DiffPanel.tsx

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { parsePatchFiles } from "@pierre/diffs";
21
import { FileDiff, type FileDiffMetadata, Virtualizer } from "@pierre/diffs/react";
32
import { useQuery } from "@tanstack/react-query";
43
import {
@@ -18,7 +17,8 @@ import { useTheme } from "../hooks/useTheme";
1817
import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";
1918
import { buildAcceptedDiffFileKey, filterAcceptedDiffFiles } from "../lib/diffPanelAcceptance";
2019
import { checkpointDiffQueryOptions } from "../lib/providerReactQuery";
21-
import { buildPatchCacheKey, resolveDiffThemeName } from "../lib/diffRendering";
20+
import { resolveDiffThemeName } from "../lib/diffRendering";
21+
import { parseRenderablePatch } from "../lib/renderablePatch";
2222
import { cn } from "../lib/utils";
2323
import { useStore } from "../store";
2424
import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell";
@@ -95,42 +95,6 @@ const DIFF_PANEL_UNSAFE_CSS = `
9595
}
9696
`;
9797

98-
type RenderablePatch =
99-
| { kind: "files"; files: FileDiffMetadata[] }
100-
| { kind: "raw"; text: string; reason: string };
101-
102-
function getRenderablePatch(
103-
patch: string | undefined,
104-
cacheScope = "diff-panel",
105-
): RenderablePatch | null {
106-
if (!patch) return null;
107-
const normalizedPatch = patch.trim();
108-
if (normalizedPatch.length === 0) return null;
109-
110-
try {
111-
const parsedPatches = parsePatchFiles(
112-
normalizedPatch,
113-
buildPatchCacheKey(normalizedPatch, cacheScope),
114-
);
115-
const files = parsedPatches.flatMap((parsedPatch) => parsedPatch.files);
116-
if (files.length > 0) {
117-
return { kind: "files", files };
118-
}
119-
120-
return {
121-
kind: "raw",
122-
text: normalizedPatch,
123-
reason: "Unsupported diff format. Showing raw patch.",
124-
};
125-
} catch {
126-
return {
127-
kind: "raw",
128-
text: normalizedPatch,
129-
reason: "Failed to parse patch. Showing raw patch.",
130-
};
131-
}
132-
}
133-
13498
type FileDiffCategory = "all" | "added" | "modified" | "deleted" | "renamed";
13599

136100
const CATEGORY_ORDER: FileDiffCategory[] = ["all", "added", "modified", "deleted", "renamed"];
@@ -256,7 +220,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
256220
? "Failed to load checkpoint diff."
257221
: null;
258222
const renderablePatch = useMemo(
259-
() => getRenderablePatch(selectedPatch, `diff-panel:${resolvedTheme}`),
223+
() => parseRenderablePatch(selectedPatch, `diff-panel:${resolvedTheme}`),
260224
[resolvedTheme, selectedPatch],
261225
);
262226
const renderableFiles = useMemo(() => {

apps/web/src/components/GitActionsControl.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ export default function GitActionsControl({
367367
activeProjectId,
368368
}: GitActionsControlProps) {
369369
const { settings } = useAppSettings();
370-
const setPreviewOpen = usePreviewStateStore((state) => state.setProjectOpen);
370+
const setPreviewOpen = usePreviewStateStore((state) => state.setThreadOpen);
371371
const openFileInViewer = useFileViewNavigation();
372372
const threadToastData = useMemo(
373373
() => (activeThreadId ? { threadId: activeThreadId } : undefined),

apps/web/src/components/PreviewPanel.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { PreviewTabsState, PreviewTabState, ProjectId } from "@okcode/contracts";
1+
import type { PreviewTabsState, PreviewTabState, ProjectId, ThreadId } from "@okcode/contracts";
22
import { type FormEvent, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
33
import {
44
ChevronLeftIcon,
@@ -111,7 +111,7 @@ function tabDisplayTitle(tab: PreviewTabState): string {
111111

112112
interface PreviewPanelProps {
113113
projectId: ProjectId;
114-
threadId: string;
114+
threadId: ThreadId;
115115
onClose: () => void;
116116
}
117117

@@ -149,7 +149,7 @@ function resolveViewportDimensions(
149149
export function PreviewPanel({ projectId, threadId, onClose }: PreviewPanelProps) {
150150
const { settings } = useAppSettings();
151151
const previewBridge = readDesktopPreviewBridge();
152-
const setProjectOpen = usePreviewStateStore((state) => state.setProjectOpen);
152+
const setThreadOpen = usePreviewStateStore((state) => state.setThreadOpen);
153153
const favoriteUrls = usePreviewStateStore((state) => state.favoriteUrls);
154154
const toggleFavoriteUrl = usePreviewStateStore((state) => state.toggleFavoriteUrl);
155155
const presetId = usePreviewStateStore((state) => state.presetByProjectId[projectId] ?? null);
@@ -422,7 +422,7 @@ export function PreviewPanel({ projectId, threadId, onClose }: PreviewPanelProps
422422
};
423423

424424
const onClosePreview = () => {
425-
setProjectOpen(projectId, false);
425+
setThreadOpen(threadId, false);
426426
void previewBridge?.closeAll();
427427
onClose();
428428
};

0 commit comments

Comments
 (0)