From 6841fa17d44e089b4cef03645efd6419a06f7611 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Mar 2026 15:49:42 -0800 Subject: [PATCH] Scope changed-files tree PR to relevant files - revert unrelated server-side attachment/image changes from this branch\n- keep changed-files tree UX fixes (icons, directory behavior, whitespace-safe paths, stat visibility)\n\nCo-authored-by: codex --- apps/server/src/attachmentPaths.ts | 8 ----- apps/server/src/attachmentStore.ts | 1 - apps/server/src/config.ts | 5 +--- apps/server/src/git/Errors.ts | 2 -- apps/server/src/git/Services/GitManager.ts | 2 -- .../server/src/git/Services/TextGeneration.ts | 2 -- apps/server/src/imageMime.ts | 30 +++---------------- apps/server/src/index.ts | 1 - apps/server/src/main.ts | 18 +++++------ apps/server/src/os-jank.ts | 1 - apps/server/src/wsServer.ts | 8 ++--- apps/web/src/components/ChatView.tsx | 14 +++++---- apps/web/src/lib/turnDiffTree.test.ts | 14 +++++++++ apps/web/src/lib/turnDiffTree.ts | 1 - 14 files changed, 38 insertions(+), 69 deletions(-) diff --git a/apps/server/src/attachmentPaths.ts b/apps/server/src/attachmentPaths.ts index be404076bd6f7c0c8ec9096b2cd820afe918fc05..c1680f6c082f360371fb4cdd35ce93574511c6d3 100644 --- a/apps/server/src/attachmentPaths.ts +++ b/apps/server/src/attachmentPaths.ts @@ -2,14 +2,6 @@ import path from "node:path"; export const ATTACHMENTS_ROUTE_PREFIX = "/attachments"; -export function stripAttachmentRoutePrefix(pathname: string): string | null { - if (!pathname.startsWith(ATTACHMENTS_ROUTE_PREFIX)) { - return null; - } - const remainder = pathname.slice(ATTACHMENTS_ROUTE_PREFIX.length); - return remainder.startsWith("/") ? remainder.slice(1) : remainder; -} - export function normalizeAttachmentRelativePath(rawRelativePath: string): string | null { const normalized = path.normalize(rawRelativePath).replace(/^[/\\]+/, ""); if (normalized.length === 0 || normalized.startsWith("..") || normalized.includes("\0")) { diff --git a/apps/server/src/attachmentStore.ts b/apps/server/src/attachmentStore.ts index 5fcebd64570fc9ce0ba391807e068c1fc602b39e..48be2df8a6cfcc053656de8de6835053241587a4 100644 --- a/apps/server/src/attachmentStore.ts +++ b/apps/server/src/attachmentStore.ts @@ -14,7 +14,6 @@ const ATTACHMENT_ID_THREAD_SEGMENT_MAX_CHARS = 80; const ATTACHMENT_ID_THREAD_SEGMENT_PATTERN = "[a-z0-9_]+(?:-[a-z0-9_]+)*"; const ATTACHMENT_ID_UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; -// Keep this regex strict so attachment IDs cannot be used as path traversal vectors. const ATTACHMENT_ID_PATTERN = new RegExp( `^(${ATTACHMENT_ID_THREAD_SEGMENT_PATTERN})-(${ATTACHMENT_ID_UUID_PATTERN})$`, "i", diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index ce226dc7b01e4408975bdfc8473079c4238f8863..ccbcea469d6feaf6d63876f91175f9ab6d9b6874 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -9,12 +9,9 @@ import { Effect, FileSystem, Layer, Path, ServiceMap } from "effect"; export const DEFAULT_PORT = 3773; -export const KEYBINDINGS_CONFIG_FILENAME = "keybindings.json" as const; export type RuntimeMode = "web" | "desktop"; -export const isDesktopMode = (mode: RuntimeMode): mode is "desktop" => mode === "desktop"; - /** * ServerConfigShape - Process/runtime configuration required by the server. */ @@ -53,7 +50,7 @@ export class ServerConfig extends ServiceMap.Service()( /** * GitManagerServiceError - Errors emitted by stacked Git workflow orchestration. - * - * Keep this union centralized so callers can pattern-match once at API boundaries. */ export type GitManagerServiceError = | GitManagerError diff --git a/apps/server/src/git/Services/GitManager.ts b/apps/server/src/git/Services/GitManager.ts index d917d4c1e91855b303a7855d521b0c94bcc39a0b..e3f90202dd92a047e9b466fb0a32c7b48900e8e8 100644 --- a/apps/server/src/git/Services/GitManager.ts +++ b/apps/server/src/git/Services/GitManager.ts @@ -38,8 +38,6 @@ export interface GitManagerShape { /** * GitManager - Service tag for stacked Git workflow orchestration. - * - * Single tag keeps orchestration wiring swappable in Layer-based tests. */ export class GitManager extends ServiceMap.Service()( "t3/git/Services/GitManager", diff --git a/apps/server/src/git/Services/TextGeneration.ts b/apps/server/src/git/Services/TextGeneration.ts index 877d1904ece2c1510034972169ee7d0c6b2f5dac..daae27fe66d4e7b313c72a741a39ea2dbc14d3de 100644 --- a/apps/server/src/git/Services/TextGeneration.ts +++ b/apps/server/src/git/Services/TextGeneration.ts @@ -62,8 +62,6 @@ export interface TextGenerationService { /** * TextGenerationShape - Service API for commit/PR text generation. - * - * Interface isolates provider-specific model behavior behind deterministic method contracts. */ export interface TextGenerationShape { /** diff --git a/apps/server/src/imageMime.ts b/apps/server/src/imageMime.ts index 8131dcec3a2953db66e7b699569b604c4a9b2022..814abbb32c1c483b595a2a0c8e82e3d03718f0db 100644 --- a/apps/server/src/imageMime.ts +++ b/apps/server/src/imageMime.ts @@ -29,28 +29,6 @@ export const SAFE_IMAGE_FILE_EXTENSIONS = new Set([ ".webp", ]); -const normalizeImageExtension = (extension: string): string | null => { - const trimmed = extension.trim(); - if (trimmed.length === 0) { - return null; - } - - const lower = trimmed.toLowerCase(); - return lower.startsWith(".") ? lower : `.${lower}`; -}; - -const coerceSafeImageExtension = ( - extension: string | null | undefined, -): string | null => { - if (!extension) return null; - const normalized = normalizeImageExtension(extension); - if (!normalized) return null; - return SAFE_IMAGE_FILE_EXTENSIONS.has(normalized) ? normalized : null; -}; - -export const isSafeImageExtension = (extension: string): boolean => - coerceSafeImageExtension(extension) !== null; - export function parseBase64DataUrl( dataUrl: string, ): { readonly mimeType: string; readonly base64: string } | null { @@ -85,15 +63,15 @@ export function inferImageExtension(input: { mimeType: string; fileName?: string return fromMime; } - const fromMimeExtension = coerceSafeImageExtension(Mime.getExtension(input.mimeType)); - if (fromMimeExtension) { + const fromMimeExtension = Mime.getExtension(input.mimeType); + if (fromMimeExtension && SAFE_IMAGE_FILE_EXTENSIONS.has(fromMimeExtension)) { return fromMimeExtension; } const fileName = input.fileName?.trim() ?? ""; const extensionMatch = /\.([a-z0-9]{1,8})$/i.exec(fileName); - const fileNameExtension = extensionMatch ? coerceSafeImageExtension(extensionMatch[1]) : null; - if (fileNameExtension) { + const fileNameExtension = extensionMatch ? `.${extensionMatch[1]!.toLowerCase()}` : ""; + if (SAFE_IMAGE_FILE_EXTENSIONS.has(fileNameExtension)) { return fileNameExtension; } diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 8cdd090ec94e5683976847a294730f4813645449..6eb26602cab91bb1954d6f8b933ef4342877c0ca 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -10,7 +10,6 @@ import { version } from "../package.json" with { type: "json" }; import { ServerLive } from "./wsServer"; import { NetService } from "@t3tools/shared/Net"; -// Compose all required services once so every CLI invocation shares the same runtime. const RuntimeLayer = Layer.empty.pipe( Layer.provideMerge(CliConfig.layer), Layer.provideMerge(ServerLive), diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 2ea89cb609b1d64045007f723896a085ea62fc15..4f36e7ea1291bb7ca890a0ba507eb6b35a023fe3 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -12,10 +12,8 @@ import { NetService } from "@t3tools/shared/Net"; import { DEFAULT_PORT, - KEYBINDINGS_CONFIG_FILENAME, resolveStaticDir, ServerConfig, - isDesktopMode, type RuntimeMode, type ServerConfigShape, } from "./config"; @@ -135,8 +133,6 @@ const ServerConfigLive = (input: CliInput) => ); const mode = Option.getOrElse(input.mode, () => env.mode); - const isDesktop = isDesktopMode(mode); - const isWeb = !isDesktop; const port = yield* Option.match(input.port, { onSome: (value) => Effect.succeed(value), @@ -144,7 +140,7 @@ const ServerConfigLive = (input: CliInput) => if (env.port) { return Effect.succeed(env.port); } - if (isDesktop) { + if (mode === "desktop") { return Effect.succeed(DEFAULT_PORT); } return findAvailablePort(DEFAULT_PORT); @@ -157,15 +153,15 @@ const ServerConfigLive = (input: CliInput) => const noBrowser = Option.match(input.noBrowser, { // effect/cli boolean flags parse to `false` when absent; in that case // we still want env/mode fallbacks to apply. - onSome: (value) => (value ? true : (env.noBrowser ?? isDesktop)), - onNone: () => env.noBrowser ?? isDesktop, + onSome: (value) => (value ? true : (env.noBrowser ?? mode === "desktop")), + onNone: () => env.noBrowser ?? mode === "desktop", }); const authToken = Option.getOrUndefined(input.authToken) ?? env.authToken; const autoBootstrapProjectFromCwd = Option.match(input.autoBootstrapProjectFromCwd, { // effect/cli boolean flags parse to `false` when absent; in that case // we still want env/mode fallbacks to apply. - onSome: (value) => (value ? true : (env.autoBootstrapProjectFromCwd ?? isWeb)), - onNone: () => env.autoBootstrapProjectFromCwd ?? isWeb, + onSome: (value) => (value ? true : (env.autoBootstrapProjectFromCwd ?? mode === "web")), + onNone: () => env.autoBootstrapProjectFromCwd ?? mode === "web", }); const logWebSocketEvents = Option.match(input.logWebSocketEvents, { // effect/cli boolean flags parse to `false` when absent; in that case @@ -175,11 +171,11 @@ const ServerConfigLive = (input: CliInput) => }); const staticDir = devUrl ? undefined : yield* cliConfig.resolveStaticDir; const { join } = yield* Path.Path; - const keybindingsConfigPath = join(stateDir, KEYBINDINGS_CONFIG_FILENAME); + const keybindingsConfigPath = join(stateDir, "keybindings.json"); const host = Option.getOrUndefined(input.host) ?? env.host ?? - (isDesktop ? "127.0.0.1" : undefined); + (mode === "desktop" ? "127.0.0.1" : undefined); return { mode, diff --git a/apps/server/src/os-jank.ts b/apps/server/src/os-jank.ts index 978b2414e6ada7dce6313e0eecca8090d3c1c4e8..3f5e2129da06fae068f70415a18331c03dbc8312 100644 --- a/apps/server/src/os-jank.ts +++ b/apps/server/src/os-jank.ts @@ -3,7 +3,6 @@ import { Effect, Path } from "effect"; import { execFileSync } from "node:child_process"; export function fixPath(): void { - // Only macOS shells need PATH hydration from interactive startup files. if (process.platform !== "darwin") return; try { diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index 310bf756288df8fa34c254a33e788c9a6dcb8ae6..50af3b8b10ef5fd0b168047e61bd45269e7e4581 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -60,9 +60,9 @@ import { ServerConfig } from "./config"; import { GitCore } from "./git/Services/GitCore.ts"; import { tryHandleProjectFaviconRequest } from "./projectFaviconRoute"; import { + ATTACHMENTS_ROUTE_PREFIX, normalizeAttachmentRelativePath, resolveAttachmentRelativePath, - stripAttachmentRoutePrefix, } from "./attachmentPaths"; import { createAttachmentId, @@ -350,9 +350,9 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< return; } - const routeRelativePath = stripAttachmentRoutePrefix(url.pathname); - if (routeRelativePath !== null) { - const normalizedRelativePath = normalizeAttachmentRelativePath(routeRelativePath); + if (url.pathname.startsWith(ATTACHMENTS_ROUTE_PREFIX)) { + const rawRelativePath = url.pathname.slice(ATTACHMENTS_ROUTE_PREFIX.length); + const normalizedRelativePath = normalizeAttachmentRelativePath(rawRelativePath); if (!normalizedRelativePath) { respond(400, { "Content-Type": "text/plain" }, "Invalid attachment path"); return; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 1998ad84aed9798583fb61f13b7899c4187dfb6e..494e14be752db00e9166413f7a7b0dcc27542334 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -3202,10 +3202,10 @@ const ChangedFilesTree = memo(function ChangedFilesTree(props: { setExpandedDirectories(allDirectoryExpansionState); }, [allDirectoryExpansionState]); - const toggleDirectory = useCallback((pathValue: string) => { + const toggleDirectory = useCallback((pathValue: string, fallbackExpanded: boolean) => { setExpandedDirectories((current) => ({ ...current, - [pathValue]: !current[pathValue], + [pathValue]: !(current[pathValue] ?? fallbackExpanded), })); }, []); @@ -3219,7 +3219,7 @@ const ChangedFilesTree = memo(function ChangedFilesTree(props: { type="button" className="group flex w-full items-center gap-1.5 rounded-md py-1 pr-2 text-left hover:bg-background/80" style={{ paddingLeft: `${leftPadding}px` }} - onClick={() => toggleDirectory(node.path)} + onClick={() => toggleDirectory(node.path, depth === 0)} >