Skip to content

Commit 661e0e6

Browse files
authored
Fix theme hydration and terminal/file navigation (#333)
* Fix theme hydration and sidebar file tree behavior - add a stable server snapshot for `useTheme` - make chat header preview toggle safe when missing - update tests for the sidebar file tree shortcut and Git rebase flow * Remove dead UI plumbing and fix githubRef projection - Stop destructuring unused viewer and issue-thread props - Attach `githubRef` after building projection snapshots - Simplify validation and query helpers * Fix thread snapshot typing for optional GitHub ref - Build thread snapshots with an optional `githubRef` - Keep the projection query aligned with `OrchestrationThread`
1 parent 451669c commit 661e0e6

15 files changed

Lines changed: 58 additions & 324 deletions

File tree

apps/server/src/git/Layers/GitManager.test.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -787,19 +787,12 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
787787
Effect.gen(function* () {
788788
const repoDir = yield* makeTempDir("okcode-git-manager-");
789789
yield* initRepo(repoDir);
790-
const remoteDir = yield* createBareRemote();
791-
yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
792-
yield* runGit(repoDir, ["push", "-u", "origin", "main"]);
793790
yield* runGit(repoDir, ["checkout", "-b", "feature/rebase-before-commit"]);
794-
795-
const updaterDir = yield* makeTempDir("okcode-git-manager-updater-");
796-
yield* runGit(updaterDir, ["clone", "--branch", "main", remoteDir, "."]);
797-
yield* runGit(updaterDir, ["config", "user.email", "test@example.com"]);
798-
yield* runGit(updaterDir, ["config", "user.name", "Test User"]);
799-
fs.writeFileSync(path.join(updaterDir, "base.txt"), "remote main update\n");
800-
yield* runGit(updaterDir, ["add", "base.txt"]);
801-
yield* runGit(updaterDir, ["commit", "-m", "Remote main update"]);
802-
yield* runGit(updaterDir, ["push", "origin", "main"]);
791+
yield* runGit(repoDir, ["checkout", "main"]);
792+
fs.writeFileSync(path.join(repoDir, "base.txt"), "local main update\n");
793+
yield* runGit(repoDir, ["add", "base.txt"]);
794+
yield* runGit(repoDir, ["commit", "-m", "Local main update"]);
795+
yield* runGit(repoDir, ["checkout", "feature/rebase-before-commit"]);
803796

804797
fs.writeFileSync(path.join(repoDir, "README.md"), "hello\nrebased feature work\n");
805798

@@ -812,15 +805,14 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
812805
});
813806

814807
expect(result.commit.status).toBe("created");
815-
816-
const remoteMainSha = yield* runGit(repoDir, ["rev-parse", "origin/main"]).pipe(
808+
const mainSha = yield* runGit(repoDir, ["rev-parse", "main"]).pipe(
817809
Effect.map((gitResult) => gitResult.stdout.trim()),
818810
);
819-
const mergeBase = yield* runGit(repoDir, ["merge-base", "HEAD", "origin/main"]).pipe(
811+
const mergeBase = yield* runGit(repoDir, ["merge-base", "HEAD", "main"]).pipe(
820812
Effect.map((gitResult) => gitResult.stdout.trim()),
821813
);
822814

823-
expect(mergeBase).toBe(remoteMainSha);
815+
expect(mergeBase).toBe(mainSha);
824816
}),
825817
30_000,
826818
);

apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -552,26 +552,29 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
552552
} catch {
553553
// Ignore invalid JSON — treat as no ref
554554
}
555-
return {
556-
id: row.threadId,
557-
projectId: row.projectId,
558-
title: row.title,
559-
model: row.model,
560-
runtimeMode: row.runtimeMode,
561-
interactionMode: row.interactionMode,
562-
branch: row.branch,
563-
worktreePath: row.worktreePath,
564-
...(githubRef ? { githubRef } : {}),
565-
latestTurn: latestTurnByThread.get(row.threadId) ?? null,
566-
createdAt: row.createdAt,
567-
updatedAt: row.updatedAt,
568-
deletedAt: row.deletedAt,
569-
messages: messagesByThread.get(row.threadId) ?? [],
570-
proposedPlans: proposedPlansByThread.get(row.threadId) ?? [],
571-
activities: activitiesByThread.get(row.threadId) ?? [],
572-
checkpoints: checkpointsByThread.get(row.threadId) ?? [],
573-
session: sessionsByThread.get(row.threadId) ?? null,
574-
};
555+
const thread = Object.assign(
556+
{
557+
id: row.threadId,
558+
projectId: row.projectId,
559+
title: row.title,
560+
model: row.model,
561+
runtimeMode: row.runtimeMode,
562+
interactionMode: row.interactionMode,
563+
branch: row.branch,
564+
worktreePath: row.worktreePath,
565+
latestTurn: latestTurnByThread.get(row.threadId) ?? null,
566+
createdAt: row.createdAt,
567+
updatedAt: row.updatedAt,
568+
deletedAt: row.deletedAt,
569+
messages: messagesByThread.get(row.threadId) ?? [],
570+
proposedPlans: proposedPlansByThread.get(row.threadId) ?? [],
571+
activities: activitiesByThread.get(row.threadId) ?? [],
572+
checkpoints: checkpointsByThread.get(row.threadId) ?? [],
573+
session: sessionsByThread.get(row.threadId) ?? null,
574+
},
575+
githubRef ? { githubRef } : {},
576+
) as OrchestrationThread;
577+
return thread;
575578
});
576579

577580
const snapshot = {

apps/server/src/persistence/Services/ProjectionThreads.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
ThreadId,
1515
TurnId,
1616
} from "@okcode/contracts";
17-
import type { GitHubRef } from "@okcode/contracts";
1817
import { Option, Schema, ServiceMap } from "effect";
1918
import type { Effect } from "effect";
2019

apps/web/src/components/ChatView.tsx

Lines changed: 0 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import {
33
DEFAULT_CHAT_FILE_MIME_TYPE,
44
DEFAULT_MODEL_BY_PROVIDER,
55
type ClaudeCodeEffort,
6-
type GitHubIssueDetail,
7-
type GitHubRef,
86
type MessageId,
97
type ProjectScript,
108
type ModelSlug,
@@ -184,7 +182,6 @@ import { deriveLatestContextWindowSnapshot } from "../lib/contextWindow";
184182
import { shouldUseCompactComposerFooter } from "./composerFooterLayout";
185183
import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
186184
import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor";
187-
import { IssueThreadDialog } from "./IssueThreadDialog";
188185
import { PullRequestThreadDialog } from "./PullRequestThreadDialog";
189186
import { MessagesTimeline } from "./chat/MessagesTimeline";
190187
import { ChatHeader } from "./chat/ChatHeader";
@@ -221,7 +218,6 @@ import {
221218
deriveComposerSendState,
222219
LAST_INVOKED_SCRIPT_BY_PROJECT_KEY,
223220
LastInvokedScriptByProjectSchema,
224-
IssueDialogState,
225221
PullRequestDialogState,
226222
QueuedMessage,
227223
readFileAsDataUrl,
@@ -563,7 +559,6 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
563559
const [composerHighlightedItemId, setComposerHighlightedItemId] = useState<string | null>(null);
564560
const [pullRequestDialogState, setPullRequestDialogState] =
565561
useState<PullRequestDialogState | null>(null);
566-
const [issueDialogState, setIssueDialogState] = useState<IssueDialogState | null>(null);
567562
const [pendingProjectScriptRun, setPendingProjectScriptRun] = useState<{
568563
script: ProjectScript;
569564
inputIds: string[];
@@ -746,24 +741,6 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
746741
setPullRequestDialogState(null);
747742
}, []);
748743

749-
const openIssueDialog = useCallback(
750-
(reference?: string) => {
751-
if (!isLocalDraftThread) {
752-
return;
753-
}
754-
setIssueDialogState({
755-
initialReference: reference ?? null,
756-
key: Date.now(),
757-
});
758-
setComposerHighlightedItemId(null);
759-
},
760-
[isLocalDraftThread],
761-
);
762-
763-
const closeIssueDialog = useCallback(() => {
764-
setIssueDialogState(null);
765-
}, []);
766-
767744
const openOrReuseProjectDraftThread = useCallback(
768745
async (input: { branch: string; worktreePath: string | null; envMode: DraftThreadEnvMode }) => {
769746
if (!activeProject) {
@@ -826,68 +803,6 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
826803
[openOrReuseProjectDraftThread],
827804
);
828805

829-
const handleStartIssueThread = useCallback(
830-
async (input: { issue: GitHubIssueDetail; mode: "local" | "worktree" }) => {
831-
if (!activeProject) {
832-
return;
833-
}
834-
// Extract owner/repo from the issue URL
835-
let owner = "";
836-
let repo = "";
837-
try {
838-
const url = new URL(input.issue.url);
839-
const parts = url.pathname.split("/").filter(Boolean);
840-
if (parts.length >= 2) {
841-
owner = parts[0]!;
842-
repo = parts[1]!;
843-
}
844-
} catch {
845-
// Fallback: cannot parse URL
846-
}
847-
848-
if (!owner || !repo) {
849-
return;
850-
}
851-
852-
const githubRef = {
853-
kind: "issue" as const,
854-
owner,
855-
repo,
856-
number: input.issue.number,
857-
} satisfies GitHubRef;
858-
859-
// Always create a fresh thread for an issue
860-
clearProjectDraftThreadId(activeProject.id);
861-
const nextId = newThreadId();
862-
setProjectDraftThreadId(activeProject.id, nextId, {
863-
createdAt: new Date().toISOString(),
864-
runtimeMode: DEFAULT_RUNTIME_MODE,
865-
envMode: input.mode,
866-
githubRef,
867-
});
868-
869-
// Pre-populate the composer with an issue context prompt
870-
const { setPrompt: storeSetPrompt } = useComposerDraftStore.getState();
871-
const labelsText =
872-
input.issue.labels.length > 0
873-
? `Labels: ${input.issue.labels.map((l) => l.name).join(", ")}\n`
874-
: "";
875-
const bodyPreview = input.issue.body
876-
? input.issue.body.slice(0, 2000) + (input.issue.body.length > 2000 ? "\n..." : "")
877-
: "";
878-
storeSetPrompt(
879-
nextId,
880-
`Resolve GitHub issue #${input.issue.number}: ${input.issue.title}\n\n${labelsText}${bodyPreview ? `${bodyPreview}\n\n` : ""}Please analyze this issue and implement a fix.`,
881-
);
882-
883-
await navigate({
884-
to: "/$threadId",
885-
params: { threadId: nextId },
886-
});
887-
},
888-
[activeProject, clearProjectDraftThreadId, navigate, setProjectDraftThreadId],
889-
);
890-
891806
useEffect(() => {
892807
if (!activeThread?.id) return;
893808
if (!latestTurnSettled) return;
@@ -1609,11 +1524,6 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
16091524

16101525
const pendingContext = useCodeViewerStore((state) => state.pendingContext);
16111526
const clearPendingContext = useCodeViewerStore((state) => state.clearPendingContext);
1612-
const codeViewerOpen = useCodeViewerStore((state) => state.isOpen);
1613-
const toggleCodeViewer = useCodeViewerStore((state) => state.toggle);
1614-
const diffViewerOpen = useDiffViewerStore((state) => state.isOpen);
1615-
const openDiffViewerConversation = useDiffViewerStore((state) => state.openConversation);
1616-
const closeDiffViewer = useDiffViewerStore((state) => state.close);
16171527
const openTurnDiffViewer = useDiffViewerStore((state) => state.openTurnDiff);
16181528
const handleOpenTurnDiff = useCallback(
16191529
(turnId: TurnId, filePath?: string) => {
@@ -1623,14 +1533,6 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
16231533
[activeThread, openTurnDiffViewer],
16241534
);
16251535

1626-
const handleToggleDiffViewer = useCallback(() => {
1627-
if (diffViewerOpen) {
1628-
closeDiffViewer();
1629-
} else if (activeThread) {
1630-
openDiffViewerConversation(activeThread.id);
1631-
}
1632-
}, [diffViewerOpen, closeDiffViewer, activeThread, openDiffViewerConversation]);
1633-
16341536
// When Cmd+L is pressed in the code viewer, insert the @file:lines mention into the composer
16351537
useEffect(() => {
16361538
if (!pendingContext) return;
@@ -4853,8 +4755,6 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
48534755
terminalAvailable={activeProject !== undefined}
48544756
terminalOpen={terminalState.terminalOpen}
48554757
terminalToggleShortcutLabel={terminalToggleShortcutLabel}
4856-
codeViewerOpen={codeViewerOpen}
4857-
diffViewerOpen={diffViewerOpen}
48584758
previewAvailable={isElectron && activeProject !== undefined}
48594759
previewOpen={previewOpen}
48604760
previewDock={previewDock}
@@ -4872,8 +4772,6 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
48724772
onImportProjectScripts={importProjectScripts}
48734773
onToggleTerminal={toggleTerminalVisibility}
48744774
onPrefetchTerminal={preloadThreadTerminalDrawer}
4875-
onToggleCodeViewer={toggleCodeViewer}
4876-
onToggleDiffViewer={handleToggleDiffViewer}
48774775
onTogglePreview={() => activeProjectId && togglePreviewOpen(activeProjectId)}
48784776
onTogglePreviewLayout={() => activeProjectId && togglePreviewLayout(activeProjectId)}
48794777
onMinimize={onMinimize}

apps/web/src/components/ConnectionIndicator.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { memo } from "react";
2-
import { WifiIcon, WifiOffIcon, RefreshCwIcon } from "lucide-react";
2+
import { RefreshCwIcon } from "lucide-react";
33

44
import { useConnectionHealth } from "../hooks/useConnectionHealth";
55
import { Tooltip, TooltipTrigger, TooltipPopup } from "./ui/tooltip";

apps/web/src/components/ScreenshotTool.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ async function captureRegion(rect: {
8484
function loadImage(src: string): Promise<HTMLImageElement> {
8585
return new Promise((resolve, reject) => {
8686
const img = new Image();
87-
img.onload = () => resolve(img);
88-
img.onerror = reject;
87+
img.addEventListener("load", () => resolve(img), { once: true });
88+
img.addEventListener("error", reject, { once: true });
8989
img.src = src;
9090
});
9191
}

apps/web/src/components/Sidebar.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { readFileSync } from "node:fs";
22
import { resolve } from "node:path";
33
import { describe, expect, it } from "vitest";
44

5-
describe("Sidebar file tree mounting", () => {
6-
it("keeps the workspace file tree mounted when the files section is collapsed", () => {
5+
describe("Sidebar file tree shortcut", () => {
6+
it("opens the right-panel file tree instead of mounting the tree inline", () => {
77
const src = readFileSync(resolve(import.meta.dirname, "./Sidebar.tsx"), "utf8");
88

9-
expect(src).toContain("<WorkspaceFileTree");
10-
expect(src).toContain('className={cn(filesCollapsedByProject.has(project.id) && "hidden")}');
11-
expect(src).not.toContain("!filesCollapsedByProject.has(project.id) && (");
9+
expect(src).toContain('aria-label="Open file tree"');
10+
expect(src).toContain('useRightPanelStore.getState().open("files")');
11+
expect(src).not.toContain("<WorkspaceFileTree");
1212
});
1313
});

0 commit comments

Comments
 (0)