From 2f603e1e8cc720fcf40a300051d04dadb0417cde Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Tue, 31 Mar 2026 23:52:31 -0500 Subject: [PATCH 1/3] Polish skill detail dialog layout - Add icon, tag chips, and improved command/path presentation - Make dialog content and skills page scrollable within available height --- apps/web/src/components/skills/SkillsPage.tsx | 55 +++++++++++++------ apps/web/src/components/ui/dialog.tsx | 8 ++- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/apps/web/src/components/skills/SkillsPage.tsx b/apps/web/src/components/skills/SkillsPage.tsx index a7f3f2a2d..88ec65f9c 100644 --- a/apps/web/src/components/skills/SkillsPage.tsx +++ b/apps/web/src/components/skills/SkillsPage.tsx @@ -138,30 +138,53 @@ function SkillDetailDialog(props: { const mutable = isCatalog ? !skill.immutable && skill.installed : skill.mutable; const pathValue = skill.path; const slashName = isCatalog ? skill.name.toLowerCase().replace(/\s+/g, "-") : skill.name; + const tags = "tags" in skill ? skill.tags : []; + const Icon = skillIcon("icon" in skill ? skill.icon : "file"); return ( - {skill.name} - {skill.description} - - -
- {("tags" in skill ? skill.tags : []).map((tag) => ( - - {tag} - - ))} +
+
+ +
+
+ {skill.name} + {skill.description} +
+ {tags.length > 0 && ( +
+ {tags.map((tag) => ( + + {tag} + + ))} +
+ )} + +
-

Slash commands

-

/{slashName}

-

/skill read {slashName}

+

+ Slash commands +

+
+

+ /{slashName} +

+

+ /skill read {slashName} +

+
{pathValue ? (
-

Path

-

{pathValue}

+

+ Installed path +

+

+ {pathValue} +

) : null}
@@ -422,7 +445,7 @@ export function SkillsPage(props: {
-
+
diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx index 774047fee..73ae52e11 100644 --- a/apps/web/src/components/ui/dialog.tsx +++ b/apps/web/src/components/ui/dialog.tsx @@ -90,7 +90,13 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { } function DialogPanel({ className, ...props }: React.ComponentProps<"div">) { - return
; + return ( +
+ ); } function DialogFooter({ From 0e76bd074fd8fb4a089cf7515da6aa4e3ce45884 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 1 Apr 2026 00:02:58 -0500 Subject: [PATCH 2/3] Handle merge conflicts without visible markers - Fall back to ours/theirs content from the git index when conflict markers cannot be parsed - Apply full-file fallback candidates directly during resolution --- .../prReview/Layers/MergeConflictResolver.ts | 96 +++++++++++++++++-- 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/apps/server/src/prReview/Layers/MergeConflictResolver.ts b/apps/server/src/prReview/Layers/MergeConflictResolver.ts index 5fccb6be5..2b800f45e 100644 --- a/apps/server/src/prReview/Layers/MergeConflictResolver.ts +++ b/apps/server/src/prReview/Layers/MergeConflictResolver.ts @@ -3,7 +3,7 @@ import { promises as fsPromises } from "node:fs"; import { Effect, Layer } from "effect"; import type { PrConflictCandidateResolution, PrReviewSummary } from "@okcode/contracts"; -import { GitCore } from "../../git/Services/GitCore.ts"; +import { GitCore, type GitCoreShape } from "../../git/Services/GitCore.ts"; import { MergeConflictResolver, type MergeConflictResolverShape, @@ -114,15 +114,91 @@ function buildCandidatesForFile(input: { return candidates; } -async function readCandidatesForConflicts(cwd: string, conflictedFiles: readonly string[]) { +/** + * Read the "ours" (stage 2) and "theirs" (stage 3) versions of a conflicted + * file directly from the git index. This works even when the working-tree + * copy has no parseable conflict markers (e.g. binary files, diff3 style, + * already-partially-resolved markers, or multiple conflict blocks). + */ +async function buildFallbackCandidatesFromIndex( + gitCore: GitCoreShape, + cwd: string, + relativePath: string, +): Promise { + const candidates: PrConflictCandidateResolution[] = []; + const tryStage = async ( + stage: "2" | "3", + label: "ours" | "theirs", + title: string, + description: string, + ) => { + try { + const result = await Effect.runPromise( + gitCore.execute({ + operation: "showConflictStage", + cwd, + args: ["show", `:${stage}:${relativePath}`], + allowNonZeroExit: true, + }), + ); + if (result.code === 0) { + candidates.push( + buildCandidate({ + id: `${relativePath}:${label}`, + path: relativePath, + title, + description, + confidence: "review", + replacement: result.stdout, + }), + ); + } + } catch { + // Stage does not exist in the index; skip this side. + } + }; + + await tryStage( + "2", + "ours", + "Prefer current side (full file)", + "Review-required candidate using the full current-branch version from the git index.", + ); + await tryStage( + "3", + "theirs", + "Prefer incoming side (full file)", + "Review-required candidate using the full incoming-branch version from the git index.", + ); + + return candidates; +} + +async function readCandidatesForConflicts( + cwd: string, + conflictedFiles: readonly string[], + gitCore: GitCoreShape, +) { const candidates: PrConflictCandidateResolution[] = []; for (const relativePath of conflictedFiles) { try { const absolutePath = path.join(cwd, relativePath); const contents = await fsPromises.readFile(absolutePath, "utf8"); - candidates.push(...buildCandidatesForFile({ relativePath, contents })); + const fileCandidates = buildCandidatesForFile({ relativePath, contents }); + if (fileCandidates.length > 0) { + candidates.push(...fileCandidates); + } else { + // Marker parsing failed (diff3 style, multiple blocks, etc.) – fall + // back to full-file ours/theirs from the git index. + candidates.push( + ...(await buildFallbackCandidatesFromIndex(gitCore, cwd, relativePath)), + ); + } } catch { - // Ignore unreadable files; they remain unresolved and will be surfaced in summary text. + // File unreadable from disk – still try index-based fallback. + candidates.push( + ...(await buildFallbackCandidatesFromIndex(gitCore, cwd, relativePath)), + ); } } return candidates; @@ -136,7 +212,7 @@ const makeMergeConflictResolver = Effect.gen(function* () { try: async () => { const status = await Effect.runPromise(gitCore.statusDetails(cwd)); if (status.hasConflicts) { - const candidates = await readCandidatesForConflicts(cwd, status.conflictedFiles); + const candidates = await readCandidatesForConflicts(cwd, status.conflictedFiles, gitCore); return { status: "conflicted" as const, mergeableState: pullRequest.mergeable, @@ -193,10 +269,12 @@ const makeMergeConflictResolver = Effect.gen(function* () { const absolutePath = path.join(cwd, candidate.path); const contents = await fsPromises.readFile(absolutePath, "utf8"); const parsed = parseFirstConflictBlock(contents); - if (!parsed) { - throw new Error("Conflict markers were not found in the target file."); - } - const nextContents = `${parsed.before}${candidate.previewPatch}${parsed.after}`; + // When markers are parseable, splice the candidate into the + // surrounding context. Otherwise the candidate contains the + // full file content (index-based fallback) – write it directly. + const nextContents = parsed + ? `${parsed.before}${candidate.previewPatch}${parsed.after}` + : candidate.previewPatch; await fsPromises.writeFile(absolutePath, nextContents, "utf8"); return { candidateId, From aae7c1db43b3084025a760ce4d9df0e57993b3ff Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 1 Apr 2026 00:18:34 -0500 Subject: [PATCH 3/3] Pin @pierre/diffs to 1.1.8 - Replace the beta range with the stable 1.1.8 release - Update the lockfile to match the pinned dependency --- apps/web/package.json | 2 +- bun.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 768635a73..d4ddb4932 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -30,7 +30,7 @@ "@lexical/react": "^0.41.0", "@okcode/contracts": "workspace:*", "@okcode/shared": "workspace:*", - "@pierre/diffs": "^1.1.0-beta.16", + "@pierre/diffs": "1.1.8", "@tanstack/react-pacer": "^0.19.4", "@tanstack/react-query": "^5.90.0", "@tanstack/react-router": "^1.160.2", diff --git a/bun.lock b/bun.lock index 3d643b017..1b35b3e86 100644 --- a/bun.lock +++ b/bun.lock @@ -114,7 +114,7 @@ "@lexical/react": "^0.41.0", "@okcode/contracts": "workspace:*", "@okcode/shared": "workspace:*", - "@pierre/diffs": "^1.1.0-beta.16", + "@pierre/diffs": "1.1.8", "@tanstack/react-pacer": "^0.19.4", "@tanstack/react-query": "^5.90.0", "@tanstack/react-router": "^1.160.2",