diff --git a/apps/desktop/layout-spec.json b/apps/desktop/layout-spec.json index 011e1d53..dbca65cd 100644 --- a/apps/desktop/layout-spec.json +++ b/apps/desktop/layout-spec.json @@ -92,7 +92,25 @@ "collapseAt": 704, "listContentLine": 32, "listControlsOuterInset": 16, - "listContentBehavior": "Split-list titles, first selection labels, and search icons share the pageSection content line; collapsed-sidebar recovery actions remain shell chrome." + "listContentBehavior": "Split-list titles, first selection labels, and search icons share the pageSection content line; collapsed-sidebar recovery actions remain shell chrome.", + "pullRequests": { + "inspectorMinWidth": 192, + "inspectorPreferredWidth": "23cqw", + "inspectorMaxWidth": 256, + "inspectorCollapseAt": 960, + "inspectorInset": 12, + "inspectorRadius": "modal", + "inspectorElevation": "raised", + "behavior": "Show list, primary PR detail, and a reserved floating Inspector at standard widths; collapse the Inspector before the existing list/detail compact transition." + } + }, + "uiLab": { + "navigationWidth": 196, + "catalogMaxWidth": 1160, + "scenarioToolbarHeight": 48, + "cardMinWidth": 280, + "compactAt": 760, + "behavior": "Keep a persistent catalog rail at standard widths, move it above content at compact widths, and let each production scenario retain its own container-query breakpoints." } }, "verticalRhythm": { diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 74d2ef13..b9cfd301 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -398,7 +398,7 @@ import { type DockTab, } from "./dock/Dock"; import { BrowserPanel } from "./browser/Browser"; -import { GitDockContent } from "./git/GitDockContent"; +import { GitDockContent, PullRequestDockContent } from "./git/GitDockContent"; import { TerminalDockContent } from "./terminal/TerminalDockContent"; import { TrajectoryView } from "./session/TrajectoryView"; import { SessionRail } from "./sidebar/SessionRail"; @@ -5383,6 +5383,7 @@ export default function App() { "side-chat", ...(componentEnabled("files.surface") ? ["files" as const] : []), ...(componentEnabled("git.surface") ? ["git" as const] : []), + ...(componentEnabled("git.surface") ? ["pull-request" as const] : []), ], [componentEnabled], ); @@ -5863,6 +5864,7 @@ export default function App() { terminal: "terminal.dock", files: "files.surface", git: "git.surface", + "pull-request": "git.surface", }; const componentId = component[t]; if (componentId && !componentEnabled(componentId)) { @@ -8641,10 +8643,15 @@ export default function App() { ), git: ( + ), + "pull-request": ( + ), }} diff --git a/apps/desktop/src/design/DesignSystemPreview.tsx b/apps/desktop/src/design/DesignSystemPreview.tsx index 612e4d4e..30991c9c 100644 --- a/apps/desktop/src/design/DesignSystemPreview.tsx +++ b/apps/desktop/src/design/DesignSystemPreview.tsx @@ -180,11 +180,17 @@ function SectionHeading({ eyebrow, title }: { eyebrow: string; title: string }) ); } -export function DesignSystemPreview() { +export function DesignSystemPreview({ + catalogHref = "?ui-lab=home", + initialThemeMode = "system", +}: { + catalogHref?: string; + initialThemeMode?: ThemeMode; +}) { const toast = useToast(); const systemDark = useSystemDark(); const appearance = useAppearanceSettings(); - const [themeMode, setThemeMode] = useState("system"); + const [themeMode, setThemeMode] = useState(initialThemeMode); const [boldText, setBoldText] = useState(false); const [selectedProvider, setSelectedProvider] = useState("codex"); const [selectedChoice, setSelectedChoice] = useState("automatic"); @@ -252,6 +258,7 @@ export function DesignSystemPreview() { data-ds-theme={resolvedTheme} > + ) : null} diff --git a/apps/desktop/src/github/pull-requests.css b/apps/desktop/src/github/pull-requests.css index 6637a883..c32fbc1c 100644 --- a/apps/desktop/src/github/pull-requests.css +++ b/apps/desktop/src/github/pull-requests.css @@ -6,6 +6,22 @@ width: clamp(20rem, 38cqw, 31rem); } +.pull-request-detail-workspace { + display: grid; + grid-template-columns: minmax(0, 1fr) clamp(12rem, 23cqw, 16rem); +} + +.pull-request-inspector { + min-width: 0; + margin: var(--ds-space-surface-inset); + margin-inline-start: 0; + overflow: hidden; + background: var(--ds-color-surface); + border: 1px solid var(--border); + border-radius: var(--ds-radius-modal); + box-shadow: var(--ds-elevation-raised); +} + .pull-request-body { font-size: var(--ds-type-body-size); line-height: var(--ds-type-prose-leading); @@ -59,6 +75,21 @@ display: none; } +@container (max-width: 60rem) { + .pull-request-detail-workspace { + grid-template-columns: minmax(0, 1fr); + } + + .pull-request-inspector { + display: none; + } + + .pull-request-secondary-action-label, + .pull-request-primary-action-label { + display: none; + } +} + @container (max-width: 44rem) { .pull-requests-list-pane { width: 100%; @@ -75,4 +106,8 @@ .pull-request-back { display: inline-flex; } + + .pull-requests-page[data-compact-detail="true"] .pull-request-primary-action-label { + display: inline; + } } diff --git a/apps/desktop/src/github/pullRequests.ts b/apps/desktop/src/github/pullRequests.ts index f4183f43..e200b7c7 100644 --- a/apps/desktop/src/github/pullRequests.ts +++ b/apps/desktop/src/github/pullRequests.ts @@ -3,6 +3,17 @@ import type { GitHubPullRequestReference } from "../taskboard/taskBoard"; export type PullRequestView = "all" | "reviewing" | "authored"; export type PullRequestReadiness = "all" | "draft" | "ready"; +export type PullRequestMergeReadiness = + | "closed" + | "draft" + | "conflicting" + | "checks_failed" + | "checks_pending" + | "changes_requested" + | "review_required" + | "ready" + | "pending"; +export type PullRequestCheckResult = "passed" | "failed" | "pending"; export interface PullRequestGroup { id: "review-requested" | "reviewed" | "authored"; @@ -83,13 +94,45 @@ export function pullRequestCheckState(detail: GitHubPullRequestDetail): | "failed" | "passed" { if (detail.checks.length === 0) return "none"; - if (detail.checks.some((check) => - ["FAILURE", "ERROR", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED"].includes( - check.conclusion.toLocaleUpperCase() || check.status.toLocaleUpperCase(), - ))) return "failed"; - if (detail.checks.some((check) => - !check.conclusion || ["QUEUED", "IN_PROGRESS", "PENDING", "EXPECTED"].includes( - check.status.toLocaleUpperCase(), - ))) return "pending"; + if (detail.checks.some((check) => pullRequestCheckResult(check) === "failed")) return "failed"; + if (detail.checks.some((check) => pullRequestCheckResult(check) === "pending")) return "pending"; return "passed"; } + +export function pullRequestCheckResult( + check: GitHubPullRequestDetail["checks"][number], +): PullRequestCheckResult { + const conclusion = check.conclusion.toLocaleUpperCase(); + if (["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) return "passed"; + if ([ + "FAILURE", + "ERROR", + "CANCELLED", + "TIMED_OUT", + "ACTION_REQUIRED", + "STALE", + "STARTUP_FAILURE", + ].includes(conclusion)) return "failed"; + return "pending"; +} + +export function pullRequestMergeReadiness( + detail: GitHubPullRequestDetail, +): PullRequestMergeReadiness { + if (detail.state.toLocaleUpperCase() !== "OPEN") return "closed"; + if (detail.isDraft) return "draft"; + if ( + detail.mergeable.toLocaleUpperCase() === "CONFLICTING" + || detail.mergeStateStatus.toLocaleUpperCase() === "DIRTY" + ) return "conflicting"; + + const checks = pullRequestCheckState(detail); + if (checks === "failed") return "checks_failed"; + + const review = detail.reviewDecision.toLocaleUpperCase(); + if (review === "CHANGES_REQUESTED") return "changes_requested"; + if (checks === "pending") return "checks_pending"; + if (review === "REVIEW_REQUIRED") return "review_required"; + if (detail.mergeable.toLocaleUpperCase() === "MERGEABLE") return "ready"; + return "pending"; +} diff --git a/apps/desktop/src/i18n/index.tsx b/apps/desktop/src/i18n/index.tsx index d95aee0f..d78b0c59 100644 --- a/apps/desktop/src/i18n/index.tsx +++ b/apps/desktop/src/i18n/index.tsx @@ -1,4 +1,4 @@ -import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react"; +import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; import { LOCALES, type Locale, type StringKey } from "./strings"; @@ -50,27 +50,42 @@ const I18nContext = createContext({ t: (k) => k, }); -export function I18nProvider({ children }: { children: ReactNode }) { - const [preference, setPreferenceState] = useState(storedPreference); +export function I18nProvider({ + children, + preferenceOverride, +}: { + children: ReactNode; + /** A non-persistent preview value. UI Lab uses this without changing the user's app setting. */ + preferenceOverride?: LanguagePreference; +}) { + const [storedPreferenceState, setPreferenceState] = useState(storedPreference); + const preference = preferenceOverride ?? storedPreferenceState; const locale: Locale = preference === "system" ? resolveSystemLocale() : preference; + useEffect(() => { + const root = document.documentElement; + const previous = root.lang; + root.lang = locale; + return () => { + root.lang = previous; + }; + }, [locale]); + const t = useMemo(() => { const table = LOCALES[locale].strings; return (key, vars) => interpolate(table[key] ?? key, vars); }, [locale]); const setPreference = useCallback((p: LanguagePreference) => { + if (preferenceOverride !== undefined) return; setPreferenceState(p); try { localStorage.setItem(STORAGE_KEY, p); } catch { /* private mode — the choice just won't survive a restart */ } - // `lang` drives font fallback and hyphenation; leaving it as "en" makes CJK text render with - // the wrong face on some systems. - document.documentElement.lang = p === "system" ? resolveSystemLocale() : p; - }, []); + }, [preferenceOverride]); return ( diff --git a/apps/desktop/src/i18n/strings.ts b/apps/desktop/src/i18n/strings.ts index 52e40395..d86a877e 100644 --- a/apps/desktop/src/i18n/strings.ts +++ b/apps/desktop/src/i18n/strings.ts @@ -1693,6 +1693,7 @@ export const en = { "dock.terminal": "Terminal", "dock.browser": "Browser", "dock.git": "Git", + "dock.pullRequest": "PR", "dock.close": "Close panel", "dock.resize": "Drag to resize the panel", "dock.newTerminal": "New terminal", @@ -1706,6 +1707,7 @@ export const en = { "dock.browserDesc": "Open a local app or URL.", "dock.filesDesc": "Browse and edit workspace files.", "dock.gitDesc": "Review changes in the working tree.", + "dock.pullRequestDesc": "Inspect and review the pull request for this branch.", "dock.trajectoryDesc": "Inspect the session timeline, events, and execution details.", "dock.maximize": "Widen the panel", "dock.restore": "Restore the panel width", @@ -2426,7 +2428,8 @@ export const en = { "pullRequests.view.authored": "Authored", "pullRequests.detailViews": "Pull request detail views", "pullRequests.detail.summary": "Summary", - "pullRequests.detail.code": "Code", + "pullRequests.detail.changes": "Changes", + "pullRequests.detail.checks": "Checks", "pullRequests.group.review-requested": "Review requested", "pullRequests.group.reviewed": "Previously reviewed", "pullRequests.group.authored": "Authored", @@ -2453,10 +2456,16 @@ export const en = { "pullRequests.taskLinked": "Linked this pull request to \"{title}\".", "pullRequests.taskUnlinked": "Unlinked this pull request from \"{title}\".", "pullRequests.taskLinkChanged": "The task link changed. Refresh and try again.", - "pullRequests.chat": "Chat", + "pullRequests.chat": "Join conversation", + "pullRequests.reviewChanges": "Review changes", "pullRequests.branch": "Branch", "pullRequests.reviewers": "Reviewers", "pullRequests.noReviewers": "No reviewers", + "pullRequests.reviewState.approved": "approved", + "pullRequests.reviewState.changes_requested": "changes requested", + "pullRequests.reviewState.commented": "commented", + "pullRequests.reviewState.dismissed": "dismissed", + "pullRequests.reviewState.pending": "pending", "pullRequests.comments": "Comments", "pullRequests.noComments": "No comments", "pullRequests.commentCount": "{count} comments", @@ -2467,6 +2476,28 @@ export const en = { "pullRequests.checksPending": "{count} checks running", "pullRequests.status": "Status", "pullRequests.draft": "Draft", + "pullRequests.state.open": "Open", + "pullRequests.inspector": "Pull request status", + "pullRequests.mergeReadiness": "Merge readiness", + "pullRequests.readiness.closed": "Closed", + "pullRequests.readiness.draft": "Draft", + "pullRequests.readiness.conflicting": "Resolve conflicts", + "pullRequests.readiness.checks_failed": "Checks failed", + "pullRequests.readiness.checks_pending": "Checks running", + "pullRequests.readiness.changes_requested": "Changes requested", + "pullRequests.readiness.review_required": "Review required", + "pullRequests.readiness.ready": "Ready to merge", + "pullRequests.readiness.pending": "Merge status pending", + "pullRequests.linkedTask": "Linked task", + "pullRequests.noLinkedTask": "No linked task", + "pullRequests.labels": "Labels", + "pullRequests.noLabels": "No labels", + "pullRequests.activity": "Activity", + "pullRequests.updated": "Updated {age}", + "pullRequests.checkStatus.success": "Passed", + "pullRequests.checkStatus.failure": "Failed", + "pullRequests.checkStatus.pending": "Running", + "pullRequests.openCheck": "Open {name}", "pullRequests.description": "Description", "pullRequests.noDescription": "No description provided.", "pullRequests.changedFiles": "{count} changed files", @@ -4192,6 +4223,7 @@ export const zhCN: Record = { "dock.terminal": "终端", "dock.browser": "浏览器", "dock.git": "Git", + "dock.pullRequest": "PR", "dock.close": "关闭面板", "dock.resize": "拖动调整面板宽度", "dock.newTerminal": "新建终端", @@ -4205,6 +4237,7 @@ export const zhCN: Record = { "dock.browserDesc": "打开本地应用或网址。", "dock.filesDesc": "浏览、编辑工作区的文件。", "dock.gitDesc": "查看工作区的改动。", + "dock.pullRequestDesc": "查看并审阅当前分支的 pull request。", "dock.trajectoryDesc": "查看当前会话的时间线、事件和执行详情。", "dock.maximize": "加宽面板", "dock.restore": "恢复面板宽度", @@ -4888,7 +4921,8 @@ export const zhCN: Record = { "pullRequests.view.authored": "我创建的", "pullRequests.detailViews": "Pull request 详情视图", "pullRequests.detail.summary": "摘要", - "pullRequests.detail.code": "代码", + "pullRequests.detail.changes": "变更", + "pullRequests.detail.checks": "检查", "pullRequests.group.review-requested": "待我审阅", "pullRequests.group.reviewed": "已审阅", "pullRequests.group.authored": "我创建的", @@ -4915,10 +4949,16 @@ export const zhCN: Record = { "pullRequests.taskLinked": "已把这个 pull request 关联到“{title}”。", "pullRequests.taskUnlinked": "已解除这个 pull request 与“{title}”的关联。", "pullRequests.taskLinkChanged": "任务关联已发生变化,请刷新后重试。", - "pullRequests.chat": "对话", + "pullRequests.chat": "加入对话", + "pullRequests.reviewChanges": "审阅变更", "pullRequests.branch": "分支", "pullRequests.reviewers": "审阅者", "pullRequests.noReviewers": "没有审阅者", + "pullRequests.reviewState.approved": "已批准", + "pullRequests.reviewState.changes_requested": "要求修改", + "pullRequests.reviewState.commented": "已评论", + "pullRequests.reviewState.dismissed": "已撤销", + "pullRequests.reviewState.pending": "待处理", "pullRequests.comments": "评论", "pullRequests.noComments": "没有评论", "pullRequests.commentCount": "{count} 条评论", @@ -4929,6 +4969,28 @@ export const zhCN: Record = { "pullRequests.checksPending": "{count} 项检查运行中", "pullRequests.status": "状态", "pullRequests.draft": "草稿", + "pullRequests.state.open": "打开", + "pullRequests.inspector": "Pull request 状态", + "pullRequests.mergeReadiness": "合并准备度", + "pullRequests.readiness.closed": "已关闭", + "pullRequests.readiness.draft": "草稿", + "pullRequests.readiness.conflicting": "需要解决冲突", + "pullRequests.readiness.checks_failed": "检查失败", + "pullRequests.readiness.checks_pending": "检查运行中", + "pullRequests.readiness.changes_requested": "需要修改", + "pullRequests.readiness.review_required": "需要审阅", + "pullRequests.readiness.ready": "可以合并", + "pullRequests.readiness.pending": "正在确认合并状态", + "pullRequests.linkedTask": "关联任务", + "pullRequests.noLinkedTask": "未关联任务", + "pullRequests.labels": "标签", + "pullRequests.noLabels": "没有标签", + "pullRequests.activity": "活动", + "pullRequests.updated": "更新于 {age}", + "pullRequests.checkStatus.success": "已通过", + "pullRequests.checkStatus.failure": "失败", + "pullRequests.checkStatus.pending": "运行中", + "pullRequests.openCheck": "打开 {name}", "pullRequests.description": "描述", "pullRequests.noDescription": "未提供描述。", "pullRequests.changedFiles": "{count} 个变更文件", diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx index f02ba34a..ce529a27 100644 --- a/apps/desktop/src/main.tsx +++ b/apps/desktop/src/main.tsx @@ -23,6 +23,16 @@ const showPetPreview = import.meta.env.DEV && searchParams.has("pet-preview"); const showRichTranscript = import.meta.env.DEV && searchParams.has("rich-transcript"); +const uiLabRoute = import.meta.env.DEV ? searchParams.get("ui-lab") : null; +const showUiLab = uiLabRoute !== null; +const uiLabThemeOverride = showUiLab + ? searchParams.get("theme") === "light" || searchParams.get("theme") === "dark" + ? searchParams.get("theme") as "light" | "dark" + : "system" + : undefined; +const uiLabLanguageOverride = showUiLab + ? searchParams.get("lang") === "zh" ? "zh-CN" : "en" + : undefined; if (showDesktopPet) document.documentElement.classList.add("desktop-pet-window-root"); // The webview's own menu (Reload / Inspect Element) is a browser artefact, not something a desktop @@ -63,7 +73,9 @@ if (!showDesktopPet && currentDesktopPlatform() === "macos") { async function render() { const Root = showDesktopPet ? DesktopPetWindow - : showPetPreview + : showUiLab + ? (await import("./design/ui-lab/UiLab")).UiLab + : showPetPreview ? (await import("./pet/PetPreview")).PetPreview : showRichTranscript ? (await import("./session/RichTranscriptPreview")).RichTranscriptPreview @@ -73,8 +85,8 @@ async function render() { ReactDOM.createRoot(document.getElementById("root")!).render( - - + + diff --git a/apps/desktop/src/theme.tsx b/apps/desktop/src/theme.tsx index a07c963e..dc7ea00b 100644 --- a/apps/desktop/src/theme.tsx +++ b/apps/desktop/src/theme.tsx @@ -36,9 +36,16 @@ const ThemeContext = createContext({ * at sunset while the app is open. An explicit light/dark must *stop* listening, or the user's * choice would be silently overridden the next time the OS changed its mind. */ -export function ThemeProvider({ children }: { children: ReactNode }) { +export function ThemeProvider({ + children, + preferenceOverride, +}: { + children: ReactNode; + /** A non-persistent preview value. UI Lab uses this without changing the user's app setting. */ + preferenceOverride?: ThemePreference; +}) { const appearance = useAppearanceSettings(); - const preference = appearance.preference; + const preference = preferenceOverride ?? appearance.preference; const [system, setSystem] = useState(systemScheme); useEffect(() => { @@ -72,8 +79,9 @@ export function ThemeProvider({ children }: { children: ReactNode }) { }, [appearance, scheme]); const setPreference = useCallback((p: ThemePreference) => { + if (preferenceOverride !== undefined) return; setAppearanceSettings({ preference: p }); - }, []); + }, [preferenceOverride]); return ( {children} diff --git a/apps/desktop/tests/dockArchitecture.test.ts b/apps/desktop/tests/dockArchitecture.test.ts index 8537ad4c..452e817f 100644 --- a/apps/desktop/tests/dockArchitecture.test.ts +++ b/apps/desktop/tests/dockArchitecture.test.ts @@ -30,11 +30,15 @@ describe("Dock container and content seam", () => { expect(app).toContain(" { activateDom(); const opened = []; const view = renderDock( - ["trajectory", "browser", "terminal", "side-chat", "files", "git"], + ["trajectory", "browser", "terminal", "side-chat", "files", "git", "pull-request"], "home", true, (surface) => opened.push(surface), @@ -83,6 +83,7 @@ describe("Dock plugin component gate", () => { const cards = Array.from(view.container.querySelectorAll(".dock-surface-grid > button")); expect(cards[2]?.textContent).toContain("Terminal"); expect(cards[3]?.getAttribute("aria-label")).toBe("Side chat"); + expect(cards[6]?.getAttribute("aria-label")).toBe("PR"); expect(cards.every((card) => card.classList.contains("dock-surface-card"))).toBe(true); expect(cards.every((card) => card.classList.contains("bg-card"))).toBe(true); expect(cards.every((card) => card.classList.contains("p-3"))).toBe(true); diff --git a/apps/desktop/tests/githubPullRequests.test.ts b/apps/desktop/tests/githubPullRequests.test.ts index 86a05db8..0c034617 100644 --- a/apps/desktop/tests/githubPullRequests.test.ts +++ b/apps/desktop/tests/githubPullRequests.test.ts @@ -4,7 +4,9 @@ import { filterPullRequests, githubPullRequestReference, groupPullRequests, + pullRequestCheckResult, pullRequestCheckState, + pullRequestMergeReadiness, shortPullRequestAge, } from "../src/github/pullRequests"; import type { GitHubPullRequestDetail, GitHubPullRequestSummary } from "../src/bridge"; @@ -85,6 +87,17 @@ describe("GitHub pull request projections", () => { expect(pullRequestCheckState({ ...base, checks: [] })).toBe("none"); expect(pullRequestCheckState({ ...base, checks: [{ name: "test", status: "IN_PROGRESS", conclusion: "", detailsUrl: null }] })).toBe("pending"); expect(pullRequestCheckState({ ...base, checks: [{ name: "test", status: "COMPLETED", conclusion: "FAILURE", detailsUrl: null }] })).toBe("failed"); + expect(pullRequestCheckResult({ name: "neutral", status: "COMPLETED", conclusion: "NEUTRAL", detailsUrl: null })).toBe("passed"); + expect(pullRequestMergeReadiness({ ...base, checks: [] })).toBe("ready"); + expect(pullRequestMergeReadiness({ ...base, isDraft: true, checks: [] })).toBe("draft"); + expect(pullRequestMergeReadiness({ ...base, mergeable: "CONFLICTING", checks: [] })).toBe("conflicting"); + expect(pullRequestMergeReadiness({ ...base, state: "CLOSED", checks: [] })).toBe("closed"); + expect(pullRequestMergeReadiness({ ...base, reviewDecision: "REVIEW_REQUIRED", checks: [] })).toBe("review_required"); + expect(pullRequestMergeReadiness({ + ...base, + reviewDecision: "CHANGES_REQUESTED", + checks: [], + })).toBe("changes_requested"); }); test("projects the stable GitHub identity stored by task links", () => { diff --git a/apps/desktop/tests/githubPullRequestsRendered.test.tsx b/apps/desktop/tests/githubPullRequestsRendered.test.tsx index 718a66d2..4d592c54 100644 --- a/apps/desktop/tests/githubPullRequestsRendered.test.tsx +++ b/apps/desktop/tests/githubPullRequestsRendered.test.tsx @@ -15,6 +15,10 @@ const { githubPullRequestReference } = await import("../src/github/pullRequests" const layoutSpec = JSON.parse( readFileSync(new URL("../layout-spec.json", import.meta.url), "utf8"), ); +const pullRequestCss = readFileSync( + new URL("../src/github/pull-requests.css", import.meta.url), + "utf8", +); const mounted = []; let restoreCanvasContext = null; @@ -45,7 +49,7 @@ const summary = { isDraft: false, updatedAt: "2026-08-24T10:00:00Z", createdAt: "2026-08-23T10:00:00Z", - labels: [], + labels: [{ name: "enhancement", color: "2f81f7" }], commentsCount: 2, authored: true, reviewRequested: false, @@ -63,8 +67,8 @@ const detail = { state: "OPEN", mergeStateStatus: "CLEAN", mergeable: "MERGEABLE", - reviewDecision: "", - reviewers: [], + reviewDecision: "APPROVED", + reviewers: [{ login: "reviewer", state: "APPROVED" }], checks: [{ name: "test", status: "COMPLETED", conclusion: "SUCCESS", detailsUrl: null }], files: [{ path: "src/github.ts", additions: 120, deletions: 8, changeType: "MODIFIED" }], }; @@ -86,7 +90,7 @@ const reviewingDetail = { }; describe("PullRequestsPage", () => { - test("loads real data projections, switches code view, and starts chat", async () => { + test("renders the PR workspace, reviews changes and checks, and starts chat", async () => { activateDom(); disableCanvasDrawing(); dom.window.localStorage.setItem("codetwo.language", "en"); @@ -112,6 +116,22 @@ describe("PullRequestsPage", () => { listContentLine: 32, listControlsOuterInset: 16, }); + expect(layoutSpec.content.workbench.pullRequests).toMatchObject({ + inspectorMinWidth: 192, + inspectorPreferredWidth: "23cqw", + inspectorMaxWidth: 256, + inspectorCollapseAt: 960, + inspectorInset: 12, + inspectorRadius: "modal", + inspectorElevation: "raised", + }); + expect(pullRequestCss).toContain("@container (max-width: 60rem)"); + expect(pullRequestCss).toContain(".pull-request-inspector"); + expect(pullRequestCss).toContain("margin: var(--ds-space-surface-inset)"); + expect(pullRequestCss).toContain("margin-inline-start: 0"); + expect(pullRequestCss).toContain("border-radius: var(--ds-radius-modal)"); + expect(pullRequestCss).toContain("box-shadow: var(--ds-elevation-raised)"); + expect(pullRequestCss).toContain(".pull-request-secondary-action-label"); expect(listHeader?.className).toContain("pl-page-section"); expect(listHeader?.contains(views)).toBeFalse(); expect(listControls?.contains(views)).toBeTrue(); @@ -125,12 +145,29 @@ describe("PullRequestsPage", () => { }); expect(dom.document.body.textContent).toContain("1 checks passed"); expect(dom.document.body.textContent).toContain("Added real pull request data"); + const inspector = view.container.querySelector("[data-pull-request-inspector]"); + expect(inspector?.getAttribute("aria-label")).toBe("Pull request status"); + expect(inspector?.className).not.toContain("border-l"); + expect(inspector?.className).not.toContain("bg-sidebar"); + expect(inspector?.textContent).toContain("Ready to merge"); + expect(inspector?.textContent).toContain("reviewer"); + expect(inspector?.textContent).toContain("enhancement"); + + click(button(dom.document.body, "Changes")); + await flush(); + expect(dom.document.body.textContent).toContain("src/github.ts"); + + click(button(dom.document.body, "Checks")); + await flush(); + expect(dom.document.body.textContent).toContain("test"); + expect(dom.document.body.textContent).toContain("Passed"); - click(button(dom.document.body, "Code")); + click(button(dom.document.body, "Summary")); + click(button(dom.document.body, "Review changes")); await flush(); expect(dom.document.body.textContent).toContain("src/github.ts"); - click(button(dom.document.body, "Chat")); + click(button(dom.document.body, "Join conversation")); expect(chatted).toEqual(detail); }); diff --git a/apps/desktop/tests/uiLabRendered.test.tsx b/apps/desktop/tests/uiLabRendered.test.tsx new file mode 100644 index 00000000..b9225960 --- /dev/null +++ b/apps/desktop/tests/uiLabRendered.test.tsx @@ -0,0 +1,136 @@ +// @ts-nocheck +import { afterEach, describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { act as reactAct } from "react"; + +import { + activateDom, + dom, + flush, + mount, +} from "./domTestHarness"; + +activateDom(); + +const { resetAppearanceSettings, setAppearanceSettings } = await import("../src/appearance"); +const { I18nProvider } = await import("../src/i18n"); +const { ThemeProvider } = await import("../src/theme"); +const { UiLab } = await import("../src/design/ui-lab/UiLab"); +const { + loadPullRequest, + loadPullRequests, + pullRequestPanelApi, + pullRequestTasks, +} = await import("../src/design/ui-lab/fixtures"); + +const layoutSpec = JSON.parse( + readFileSync(new URL("../layout-spec.json", import.meta.url), "utf8"), +); +const mainSource = readFileSync(new URL("../src/main.tsx", import.meta.url), "utf8"); + +const mounted = []; +let restoreCanvasContext = null; + +function renderUiLab({ route = "home", theme = "dark", language = "en" } = {}) { + activateDom(); + if (!restoreCanvasContext) { + const getContext = dom.HTMLCanvasElement.prototype.getContext; + dom.HTMLCanvasElement.prototype.getContext = () => null; + restoreCanvasContext = () => { + dom.HTMLCanvasElement.prototype.getContext = getContext; + }; + } + const view = mount( + + + + + , + ); + mounted.push(view); + return view; +} + +afterEach(async () => { + for (const view of mounted.splice(0)) await reactAct(async () => view.unmount()); + restoreCanvasContext?.(); + restoreCanvasContext = null; + resetAppearanceSettings(); + dom.window.localStorage.removeItem("codetwo.language"); + dom.document.documentElement.classList.remove("dark", "scheme-mismatch"); + dom.document.documentElement.lang = "en"; + dom.document.body.replaceChildren(); + await flush(); +}); + +describe("UI Lab", () => { + test("publishes a stable catalog contract backed by the layout spec", () => { + const view = renderUiLab(); + + expect(view.container.querySelector("main h1")?.textContent).toBe("UI Lab"); + expect(view.container.querySelector('nav[aria-label="UI Lab sections"] a[aria-current="page"]')?.textContent).toBe("Catalog"); + expect(layoutSpec.content.uiLab).toEqual({ + navigationWidth: 196, + catalogMaxWidth: 1160, + scenarioToolbarHeight: 48, + cardMinWidth: 280, + compactAt: 760, + behavior: "Keep a persistent catalog rail at standard widths, move it above content at compact widths, and let each production scenario retain its own container-query breakpoints.", + }); + + const links = Array.from(view.container.querySelectorAll("a")).map((link) => link.getAttribute("href")); + expect(links).toContain("?ui-lab=design-system&theme=dark&lang=en"); + expect(links).toContain("?ui-lab=pull-requests&theme=dark&lang=en"); + expect(links).toContain("?ui-lab=pr-dock&theme=dark&lang=en"); + expect(links).toContain("?rich-transcript=1"); + expect(links).toContain("?pet-preview=1"); + expect(view.container.textContent).toContain("never call a bridge or remote service"); + }); + + test("composes the production PR workspace with deterministic fixtures", async () => { + const view = renderUiLab({ route: "pull-requests" }); + + expect(view.container.querySelector(".ui-lab-scenario-toolbar")?.textContent).toContain("Deterministic fixture"); + expect(view.container.querySelector(".pull-requests-list-pane h1")?.textContent).toBe("Pull requests"); + expect(view.container.querySelector(".pull-requests-page")).not.toBeNull(); + + const summaries = await loadPullRequests(); + const detail = await loadPullRequest(summaries[0]); + expect(summaries.map((item) => item.number)).toEqual([279, 276]); + expect(detail.files[0].path).toBe("apps/desktop/src/App.tsx"); + expect(pullRequestTasks[0].pullRequest?.number).toBe(279); + }); + + test("composes the production PR review panel beside the conversation", async () => { + const view = renderUiLab({ route: "pr-dock" }); + + expect(view.container.querySelector("main[aria-label='Conversation fixture']")?.textContent).toContain("Usage sidebar review"); + expect(view.container.querySelector("[data-dock-placement='right']")).not.toBeNull(); + expect(view.container.querySelector('section[aria-label="GitHub pull request"]')).not.toBeNull(); + + const pullRequest = await pullRequestPanelApi.currentPullRequest("/ui-lab/acme/code-two"); + const diff = await pullRequestPanelApi.pullRequestDiff("/ui-lab/acme/code-two", 279); + expect(pullRequest?.number).toBe(279); + expect(diff.text).toContain("const showUsage = account !== null;"); + }); + + test("uses URL theme and language without overwriting normal preferences", async () => { + setAppearanceSettings({ preference: "light" }); + dom.window.localStorage.setItem("codetwo.language", "en"); + const appearanceBefore = dom.window.localStorage.getItem("codetwo.appearance.v1"); + + renderUiLab({ theme: "dark", language: "zh-CN" }); + await flush(); + + expect(dom.document.documentElement.classList.contains("dark")).toBe(true); + expect(dom.document.documentElement.lang).toBe("zh-CN"); + expect(dom.window.localStorage.getItem("codetwo.language")).toBe("en"); + expect(dom.window.localStorage.getItem("codetwo.appearance.v1")).toBe(appearanceBefore); + }); + + test("keeps the UI Lab route development-only and preserves the legacy design-system route", () => { + expect(mainSource).toContain('import.meta.env.DEV ? searchParams.get("ui-lab") : null'); + expect(mainSource).toContain('import.meta.env.DEV && searchParams.has("design-system")'); + expect(mainSource).toContain('import("./design/ui-lab/UiLab")'); + }); +}); diff --git a/docs/sdlc/changes/2026-09-01-floating-pr-inspector-prototype/change.md b/docs/sdlc/changes/2026-09-01-floating-pr-inspector-prototype/change.md new file mode 100644 index 00000000..651dc791 --- /dev/null +++ b/docs/sdlc/changes/2026-09-01-floating-pr-inspector-prototype/change.md @@ -0,0 +1,142 @@ +--- +id: change-2026-09-01-floating-pr-inspector-prototype +kind: change +schema: 2 +status: verified +risk: low +owner: codex +approvers: user via the 2026-09-01 request to try the PR workspace's trailing Inspector as a floating panel +approved_at: 2026-09-01 +created: 2026-09-01 +updated: 2026-09-01 +source: direct user visual-design feedback on the verified PR workspace and permanent UI Lab +inputs: verified PR workspace, deterministic UI Lab PR fixture, existing Inspector breakpoints, and CodeTwo design tokens +outputs: a development-only three-variant Inspector comparison with the floating card selected by default +scope: apps/desktop/src/design/ui-lab, apps/desktop/layout-spec.json, apps/desktop/tests/uiLabRendered.test.tsx, docs/sdlc/changes/2026-09-01-floating-pr-inspector-prototype +next_trigger: production promotion is tracked by change-2026-09-01-floating-pr-inspector +verification_mode: owner +verified_by: codex +verified_at: 2026-09-01 +--- + +# Prototype a floating PR Inspector + +## Intent + +The verified PR workspace currently attaches its contextual Inspector directly to the primary +detail column. The user asked to try the trailing panel as a floating surface. The question is +visual and spatial: whether detaching the Inspector improves hierarchy without making PR content +feel obstructed or shrinking the primary review region too far. + +The desired outcome is a reversible UI Lab comparison using the real `PullRequestsPage` and its +deterministic fixture. This change does not alter the production page, GitHub behavior, persisted +state, or remote data. It should make the requested floating direction the default comparison +while preserving the existing attached layout and a more aggressive overlay as references. + +## Spec + +The existing `?ui-lab=pull-requests` route accepts `variant=attached|floating|overlay` and defaults +to `floating`. Attached retains the production rail. Floating reserves its existing bounded width +but detaches it with a 12px inset, rounded surface, border, and elevation. Overlay gives the +primary region its full width and positions the Inspector above it for comparison. The production +960px Inspector collapse remains authoritative for all variants. + +A development-only bottom switcher shows the current variant, cycles with buttons or left/right +arrow keys, writes selection into the URL, restores browser history, and does not intercept arrow +keys while a text field is focused. Theme and locale links preserve the selected variant. + +### Acceptance criteria + +- [x] AC-1: PR Workspace defaults to a visibly detached floating Inspector while attached and + overlay variants remain reachable through stable URL state. +- [x] AC-2: The switcher exposes accessible named controls, click and keyboard cycling, browser + history restoration, and preserves variant state across theme and locale links. +- [x] AC-3: Desktop dark/light and narrow rendered checks show no unintended clipping, overflow, + framework overlay, or relevant console errors; the existing compact Inspector collapse is + unchanged. +- [x] AC-4: Focused tests, renderer checks, documentation checks, SDLC checks, and whitespace + checks pass without adding the prototype to production application behavior. + +## Decision and gates + +The user's direct request accepts this low-risk, development-only visual prototype for execution. +Codex owns implementation and owner verification. Selecting and promoting a production variant is +a later human design Gate. The prototype request alone did not authorize GitHub mutation; the +user's later `pr` request authorizes including this historical decision record in the verified +Draft PR scope, but does not authorize merge, release, deployment, or production mutation. + +## Plan + +1. Record the three comparison geometries and keep the current production collapse breakpoint. +2. Add URL-backed variant state and a development-only keyboard-accessible switcher to the + existing PR Workspace fixture. +3. Style attached, floating, and overlay variants with current CodeTwo tokens and no production + component fork. +4. Run focused checks and inspect desktop dark/light plus narrow behavior in the in-app browser. + +Rollback removes this change bundle and the UI Lab-only variant code. Production components, +stored data, and remote services require no cleanup. + +## Build + +Added three URL-addressed Inspector layouts to the permanent development-only PR Workspace fixture. +The requested floating card is the default and reserves the production Inspector column while +adding a 12px inset, 16px radius, border, tokenized surface, and raised elevation. Attached remains +the production baseline; overlay deliberately gives the primary region the full width for a more +aggressive comparison. + +Added a bottom comparison switcher using the shared Button component. Clicks and left/right arrow +keys update the visible variant and URL, browser history restores prior selection, text inputs keep +their arrow-key behavior, and theme/locale navigation preserves the variant. The production +component, GitHub bridge, and normal app routes are unchanged. + +## Verification + +Verdict: verified. + +### Acceptance evidence + +- AC-1: PASS — the in-app browser rendered `variant=attached`, `variant=floating`, and + `variant=overlay` at 1440x900. Floating was the no-param default and measured a 244px-wide card + inset 12px from the top, right, and bottom with a 16px radius and raised shadow. +- AC-2: PASS — browser flow `Next -> Back -> ArrowLeft` cycled C to B to A + and restored B with the matching URL; theme navigation retained `variant=floating`. The focused + `bun test tests/uiLabRendered.test.tsx` contract also passed six tests and 32 expectations. +- AC-3: PASS — browser viewport checks `1440x900 dark`, `1440x900 light`, and `680x860 light` + rendered floating in both themes and the compact state. The narrow state hid the Inspector under + the existing 960px rule. Every state kept body + width equal to viewport width, contained meaningful DOM, showed no framework overlay, and had no + console warnings or errors. +- AC-4: PASS — full `cd apps/desktop && bun test` passed 800 tests across 138 files with 3834 + expectations. `bun run build:renderer` passed ESLint, Stylelint, TypeScript, and Vite; exact + prototype strings were absent from production output. Final docs, SDLC, worktree, and whitespace + Gates passed. + +The first verification attempt failed because the switcher used raw buttons, one shadow token did +not exist, and a full-suite assertion read a shared test window's URL. The correction adopted the +shared Button, the existing menu elevation token, and component-owned link state while retaining +real-browser URL proof; the complete rerun then passed. + +Residual risk: this verifies a renderer-only deterministic fixture, not native WebView chrome or a +real authenticated GitHub session. The production PR Inspector remains attached until the user +selects a prototype variant. + +## Review and release + +Review handoff: [Draft PR #212](https://github.com/IchenDEV/codeTwo/pull/212). +Approval: comparison implementation, subsequent production selection, and Draft PR delivery were +authorized by the user's 2026-09-01 requests. +Release target: none. +Release identity: not applicable until released. +Smoke evidence: not applicable until released. +Rollback: remove the UI Lab-only prototype wrapper, styles, switcher, layout contract, tests, and this Artifact. +No release: the selected comparison is retained as historical decision evidence in the authorized +Draft PR; merge, deployment, and release remain unauthorized. + +Preparing this section does not authorize merge, deployment, release, or production mutation. + +## Feedback + +The user selected the reserved floating-card variant and requested production implementation on +2026-09-01. Promotion and prototype cleanup are tracked in +[`change-2026-09-01-floating-pr-inspector`](../2026-09-01-floating-pr-inspector/change.md). diff --git a/docs/sdlc/changes/2026-09-01-floating-pr-inspector/change.md b/docs/sdlc/changes/2026-09-01-floating-pr-inspector/change.md new file mode 100644 index 00000000..b4954642 --- /dev/null +++ b/docs/sdlc/changes/2026-09-01-floating-pr-inspector/change.md @@ -0,0 +1,131 @@ +--- +id: change-2026-09-01-floating-pr-inspector +kind: change +schema: 2 +status: verified +risk: low +owner: codex +approvers: user via the 2026-09-01 request to start implementing the approved floating Inspector +approved_at: 2026-09-01 +created: 2026-09-01 +updated: 2026-09-01 +source: direct implementation request after reviewing change-2026-09-01-floating-pr-inspector-prototype +inputs: selected reserved floating-card prototype, production PullRequestsPage, existing Inspector layout contract, and permanent UI Lab fixture +outputs: the production PR Inspector as a reserved floating card with obsolete prototype variants removed +scope: apps/desktop/src/github/PullRequestsPage.tsx, apps/desktop/src/github/pull-requests.css, apps/desktop/src/design/ui-lab, apps/desktop/layout-spec.json, apps/desktop/tests/githubPullRequestsRendered.test.tsx, apps/desktop/tests/uiLabRendered.test.tsx, docs/sdlc/changes/2026-09-01-floating-pr-inspector, docs/sdlc/changes/2026-09-01-floating-pr-inspector-prototype +next_trigger: human review of the authorized Draft PR +verification_mode: owner +verified_by: codex owner verification +verified_at: 2026-09-01 +--- + +# Promote the floating PR Inspector + +## Intent + +The UI Lab comparison established that a reserved floating card gives the PR Inspector clearer +hierarchy without obscuring review content. The user selected that direction and asked to begin +implementation. The production PR Workspace should now use the chosen floating treatment, while +the permanent UI Lab should return to rendering a single truthful production state instead of +retaining obsolete prototype controls. + +This change is visual only. It must preserve Inspector content, semantics, width bounds, scrolling, +the 960px collapse rule, list/detail compact behavior, GitHub operations, and persisted state. It +must not keep A/C variants, add a presentation setting, change the conversation-side PR Dock, or +introduce another component abstraction. + +## Spec + +At widths above 960px, the trailing Inspector remains in its existing reserved grid column and is +inset 12px from the top, right, and bottom. It uses the established surface, border, modal radius, +and raised-elevation tokens. The leading edge remains aligned with its reserved column so primary +content width and existing hierarchy match the approved prototype. At or below 960px the Inspector +continues to hide before the list/detail compact transition. + +The UI Lab PR Workspace renders this production behavior directly. Prototype query-state parsing, +the A/B/C switcher, overlay styles, and prototype-specific tests/layout metadata are removed. + +### Acceptance criteria + +- [x] AC-1: The production Pull requests page renders its trailing Inspector as the selected + reserved floating card without changing its content, semantics, width bounds, or actions. +- [x] AC-2: At or below 960px the Inspector still hides before the existing 704px list/detail + transition, with no horizontal overflow in desktop or compact UI Lab rendering. +- [x] AC-3: The permanent UI Lab renders the production PR Workspace without variant query state, + prototype switcher UI, or prototype-only CSS and layout contracts. +- [x] AC-4: Focused tests, full desktop tests, renderer build, documentation and SDLC Gates, and + rendered dark/light/narrow inspection pass with no relevant console errors. + +## Decision and gates + +The user's direct request after reviewing the prototype accepts Intent, Spec, and the visual design +Gate for production implementation. Codex owns implementation and owner verification. The user's +later `pr` request authorizes creating a branch, pushing this verified scope, and opening a Draft +PR. Merge, release, deployment, and production-environment mutation remain unauthorized. + +## Plan + +1. Move the selected inset, surface, border, radius, and elevation to the production Inspector CSS. +2. Record those stable constraints under the production Pull requests layout contract and protect + them with the existing rendered test. +3. Delete the A/C variants, URL state, switcher, and prototype-only styling/test metadata from UI + Lab while retaining the verified prototype Artifact as the decision record. +4. Run focused checks, inspect production-component rendering in dark/light and compact widths, + then run the repository handoff Gates. + +Rollback restores the attached Inspector classes/CSS and the earlier layout contract. No data, +backend, GitHub, migration, or remote cleanup is required. + +## Build + +The production `PullRequestsPage` Inspector keeps its existing grid column and semantics, while +`pull-requests.css` now supplies the selected 12px inset, surface border, modal radius, and raised +elevation. UI Lab continues to render `PullRequestsPage` itself, but its temporary variant query +state, switcher, A/C presentation styles, and prototype-only layout/test contracts are deleted. + +The production layout contract records the stable floating-card geometry and preserves the 960px +Inspector collapse ahead of the existing 704px list/detail compact transition. No GitHub data, +operation, persistence, Dock, or conversation-side behavior changed. + +## Verification + +Verdict: verified. + +### Acceptance evidence + +- AC-1: PASS — `cd apps/desktop && bun test tests/githubPullRequestsRendered.test.tsx tests/uiLabRendered.test.tsx` completed with 10 passing tests and 85 expectations; live dark and light UI Lab inspection measured the production Inspector at a 12px top/right/bottom inset, 16px radius, token-derived shadow, intact `complementary` semantics, and working Review changes navigation. +- AC-2: PASS — `cd apps/desktop && bun test tests/githubPullRequestsRendered.test.tsx tests/uiLabRendered.test.tsx` protected the 960px/704px layout contract; live checks at 1280px, 920px, and 680px found no horizontal overflow, hid only the Inspector at 920px, and preserved the existing list-only compact state at 680px. +- AC-3: PASS — `rg -n "ui-lab-prototype-switcher|data-inspector-variant|PullRequestInspectorVariant|variant=overlay|variant=attached" apps/desktop/src apps/desktop/tests apps/desktop/layout-spec.json` returned no matches, while dark/light rendering exposed no prototype controls or variant query state. +- AC-4: PASS — `cd apps/desktop && bun test` completed with 799 passing tests, 0 failures, and 3,847 expectations; `bun run lint` and `bun run build:renderer` passed; `bun script/verify/docs.ts`, `bun script/verify/sdlc.ts`, `bun script/verify/sdlc.ts --worktree`, and `git diff --check` passed. Dark, light, medium, and narrow rendered inspection produced no relevant console warning or error. + +Lock-screen-compatible native acceptance additionally launched the current packaged `C2-dev.app` +against a fresh isolated data directory. The native host stayed live, bound its expected local port, +and registered a 1176x784 `C2 Dev` main window with the macOS window server. Because macOS blocks +window capture while the desktop is locked, a same-host native `WKWebView` harness loaded the +permanent production-component fixture and used WebKit's own snapshot path. The harness advanced +only the decorative entrance animation to its normal completed state because hidden documents do +not tick that animation. At 1280x720 it verified and captured dark and light rendering with the +12px right inset, 16px radius, token-derived shadow, no overflow, and no error state; clicking +Review changes selected Changes and rendered the 30-file view. At 920x800 it verified and captured +the expected hidden Inspector with both list and detail panes remaining visible and no overflow. + +Residual risk: the locked-machine pass separates native-shell startup from native-WebKit visual +and interaction rendering; it does not provide a pixel capture of the actual Electrobun window. +A signed-in native-shell GitHub session was also not exercised, so provider/auth integration remains +covered by existing automated behavior rather than this visual pass. + +## Review and release + +Review handoff: [Draft PR #212](https://github.com/IchenDEV/codeTwo/pull/212). +Approval: implementation plus Draft PR delivery from the user's 2026-09-01 requests. +Release target: none. +Release identity: not applicable until released. +Smoke evidence: not applicable until released. +Rollback: revert this production floating-card change; no migration or remote cleanup is required. +No release: Draft PR delivery is authorized; merge and release are not authorized. + +Preparing this section does not authorize merge, deployment, release, or production mutation. + +## Feedback + +No post-implementation feedback exists yet. diff --git a/docs/sdlc/changes/2026-09-01-pr-workspace-and-dock/change.md b/docs/sdlc/changes/2026-09-01-pr-workspace-and-dock/change.md new file mode 100644 index 00000000..7ca55a73 --- /dev/null +++ b/docs/sdlc/changes/2026-09-01-pr-workspace-and-dock/change.md @@ -0,0 +1,180 @@ +--- +id: change-2026-09-01-pr-workspace-and-dock +kind: change +schema: 2 +status: verified +risk: medium +owner: codex +approvers: user via the 2026-09-01 PR workspace implementation approval +approved_at: 2026-09-01 +created: 2026-09-01 +updated: 2026-09-01 +source: direct user request with an attached PR-workspace reference, followed by approval of the rendered CodeTwo prototype and an explicit request to implement it +inputs: existing Pull requests page, current-branch GitHub pull-request panel, right work Dock, CodeTwo layout specification, and the approved rendered prototype +outputs: a three-region Pull requests workbench plus a dedicated current-branch PR surface beside the conversation +scope: apps/desktop/src/github, apps/desktop/src/git, apps/desktop/src/dock/Dock.tsx, apps/desktop/src/App.tsx, apps/desktop/src/i18n/strings.ts, apps/desktop/layout-spec.json, apps/desktop/tests, docs/sdlc/changes/2026-09-01-pr-workspace-and-dock +next_trigger: human review of the authorized Draft PR +verification_mode: owner +verified_by: codex +verified_at: 2026-09-01 +--- + +# Improve the PR workspace and add a conversation-side PR surface + +## Intent + +The user wants CodeTwo's GitHub pull-request experience to carry the information hierarchy of the +supplied reference without copying its application-wide navigation. The full Pull requests page +currently has a list and detail view, but branch, review, checks, status, and task metadata compete +inside one central column. The right work Dock already contains a capable current-branch PR panel, +but it is hidden inside the broader Git surface and therefore is not a direct conversation-side PR +destination. + +The desired outcome is a macOS-oriented split workspace: global PR selection on the leading side, +the selected PR's title and content in the primary region, and contextual merge/review/check/task +state in a trailing Inspector. Beside a coding conversation, PR must be a first-class Dock surface +that follows the focused session's checkout and branch. This change must reuse the existing GitHub +bridge, review/merge behavior, design tokens, workbench breakpoints, and Dock ownership. It must not +add another GitHub protocol, change merge authorization, create or mutate pull requests, redesign +the application rail, or introduce a mobile application layout. + +## Spec + +At standard usable widths, Pull requests displays three regions: the existing filtered PR list, a +primary detail region, and a trailing Inspector. The primary header keeps title, author, state, +source and base branches, changed-file count, and additions/deletions together. Summary, Changes, +and Checks are explicit detail tabs. Review changes opens Changes; Join conversation uses the +existing chat handoff. The Inspector prioritizes merge readiness, review state, reviewers, task +association, checks, comments, and last activity without duplicating a second PR description. + +The workbench keeps the existing `704px` list/detail collapse contract. The new Inspector is +`clamp(192px, 23cqw, 256px)` and collapses first when the Pull requests container is at or below +`960px`, preserving a usable primary region. Status meaning always includes text or an icon, not +color alone. Existing loading, empty, error, filtering, task-linking, GitHub-open, and compact-back +behavior remains available. + +The right work Dock exposes a dedicated PR tab beside Files and Git. It renders the existing +current-branch `GitHubPullRequestPanel`, including overview, diff, checks, review submission, merge +confirmation, and GitHub opening. Git remains the working-tree summary and no longer embeds a +second copy of the PR panel. PR availability follows the existing `git.surface` component policy; +missing GitHub remotes, `gh`, authentication, repository state, or branch PR data continue to use +the existing safe empty and error states. + +### Acceptance criteria + +- [x] AC-1: At standard width the full Pull requests page renders the selected PR as list, primary + detail, and contextual Inspector; at or below the recorded breakpoints it removes the + Inspector first and preserves the existing list/detail compact navigation. +- [x] AC-2: The primary detail exposes Summary, Changes, and Checks with title/state/branch/diff + context, while Review changes opens Changes and Join conversation keeps the selected PR. +- [x] AC-3: The conversation-side Dock has a direct PR surface for the focused checkout and branch; + Git shows only working-tree state, and the existing review, merge, diff, loading, empty, and + failure behavior remains unchanged. +- [x] AC-4: English and Chinese labels, keyboard-accessible controls, semantic landmarks, + non-color-only states, and narrow reflow are covered by focused rendered tests. +- [x] AC-5: Focused tests, full desktop tests, renderer type/build/lint checks, lifecycle Gates, and + real rendered dark, light, and narrow inspection pass with no relevant clipping or console + errors. + +## Decision and gates + +The user approved the rendered direction and explicitly requested implementation on 2026-09-01, +which accepts Intent, Spec, and the visual design Gate for execution. Codex owns implementation and +owner verification. No security, data migration, provider protocol, merge, release, deployment, or +production Gate is opened. The user's separate `pr` request on 2026-09-01 authorizes creating a +branch, pushing this verified scope, and opening a Draft PR; it does not authorize merging, +releasing, or deploying CodeTwo. + +## Plan + +1. Record the new Pull requests Inspector geometry in the existing desktop layout specification. +2. Promote the existing current-branch PR panel into a dedicated Dock surface and leave Git as the + working-tree surface without adding backend commands or data models. +3. Recompose the full Pull requests detail into a primary region plus Inspector, add Checks as an + explicit view, and preserve compact selection, task linkage, and chat handoff. +4. Update English and Chinese copy and protect the new Dock ownership, state projection, detail + views, action behavior, semantics, and responsive contract with focused tests. +5. Run the applicable full Gates and inspect real renderer output in dark, light, standard, and + narrow layouts before changing the Artifact to `verified`. + +Rollback reverts this change. GitHub commands and persisted data are unchanged, so rollback does +not require migration or remote cleanup. + +## Build + +- Added `pull-request` as a first-class Dock surface under the existing `git.surface` component + policy. It renders the existing current-branch `GitHubPullRequestPanel`; the Git surface now + owns only the working-tree summary. +- Reworked the full Pull requests detail into a primary content region plus semantic Inspector, + with Summary, Changes, and Checks views, a primary Review changes action, and the existing chat, + GitHub-open, task-link, filtering, loading, error, and compact-back behavior preserved. +- Added a deterministic merge-readiness projection and shared check-result projection so text, + icons, tones, and counts agree across Summary, Checks, and Inspector states. +- Added English and Chinese labels, layout-spec geometry, responsive action-label behavior, and + focused projection, interaction, Dock-ownership, semantic, and reflow coverage. +- No GitHub command, persistence, review/merge authorization, dependency, or remote data contract + changed. The temporary development-only visual fixture used for browser screenshots was removed + before final build and verification. + +## Verification + +Verdict: verified. + +Real renderer inspection used the in-app browser against the Vite development renderer at +`http://127.0.0.1:1420/`. A temporary local fixture mounted the production components and was +removed before the final build. The standard 1440x900 dark workspace rendered the PR list, +primary Summary, and 256px Inspector without clipping. Review changes selected Changes and showed +the file list; Checks showed three named passed checks. The 1440x900 light Chinese rendering +showed translated navigation, actions, and Inspector labels. At 680x820 the Inspector and list +collapsed, the back action and primary Review changes label remained visible, and the page +reported `clientWidth == scrollWidth == 680`. The conversation preview rendered the real Dock and +current-branch PR panel at 440px; PR was directly selectable, all checks were visible, and Changes +rendered the diff. All inspected states had no relevant browser warnings or errors. + +### Acceptance evidence + +- AC-1: PASS — `layout-spec.json` records `clamp(192px, 23cqw, 256px)` and the 960px-first / + 704px-second collapse sequence; rendered 1440x900 and 680x820 inspection confirmed the three + regions and compact detail with no horizontal overflow. +- AC-2: PASS — `cd apps/desktop && bun test tests/githubPullRequests.test.ts + tests/githubPullRequestsRendered.test.tsx tests/githubPullRequestPanelRendered.test.tsx` exercised + Summary, Changes, Checks, Review changes, and Join conversation; real browser interaction + confirmed the changed-file and check rows. +- AC-3: PASS — focused Dock and existing `GitHubPullRequestPanel` tests passed, including overview, + diff, empty, draft, review, merge-confirmation, and Git-only ownership checks; the rendered Dock + showed the active PR surface beside a conversation. +- AC-4: PASS — `cd apps/desktop && bun test tests/githubPullRequests.test.ts + tests/githubPullRequestsRendered.test.tsx tests/githubPullRequestPanelRendered.test.tsx + tests/dockArchitecture.test.ts tests/dockPluginGateRendered.test.tsx` passed 22 tests and 161 + expectations; English and Chinese rendered labels, semantic Inspector, accessible action names, + text-plus-icon status, and responsive CSS contracts are covered. Final `bun test` passed 794 + tests across 137 files, 3814 expectations, and zero failures. +- AC-5: PASS — `bun run build:renderer` completed ESLint, Stylelint, TypeScript, and Vite build; + `bun script/verify/docs.ts`, `bun script/verify/sdlc.ts`, + `bun script/verify/sdlc.ts --worktree`, and `git diff --check` all passed. Browser screenshots + were captured to `/tmp/codetwo-pr-workspace-dark.png`, + `/tmp/codetwo-pr-workspace-light-zh.png`, `/tmp/codetwo-pr-workspace-narrow.png`, and + `/tmp/codetwo-pr-dock-dark.png`. + +Residual risk: live authenticated GitHub review, comment, and merge mutations were not performed +because this request did not authorize remote mutations. Their existing panel workflow and mocked +regression coverage pass unchanged. Merge readiness is an intentional local projection of the +detail fields already returned by GitHub; it does not claim to replace GitHub's authoritative +merge button decision. + +## Review and release + +Review handoff: [Draft PR #212](https://github.com/IchenDEV/codeTwo/pull/212). +Approval: implementation plus Draft PR delivery, from the user's 2026-09-01 requests. +Release target: none. +Release identity: not applicable until released. +Smoke evidence: not applicable until released. +Rollback: revert this change; there is no data or remote migration. +No release: implementation, push, and Draft PR creation are authorized; merge, deployment, and +release are not authorized. + +Preparing this section does not authorize merge, deployment, release, or production mutation. + +## Feedback + +No post-implementation feedback exists yet. diff --git a/docs/sdlc/changes/2026-09-01-ui-lab/change.md b/docs/sdlc/changes/2026-09-01-ui-lab/change.md new file mode 100644 index 00000000..707ea9be --- /dev/null +++ b/docs/sdlc/changes/2026-09-01-ui-lab/change.md @@ -0,0 +1,167 @@ +--- +id: change-2026-09-01-ui-lab +kind: change +schema: 2 +status: verified +risk: low +owner: codex +approvers: user via the 2026-09-01 request to make UI test and design-system demo pages permanent +approved_at: 2026-09-01 +created: 2026-09-01 +updated: 2026-09-01 +source: direct user request after clarifying that the earlier PR screenshots came from a temporary fixture page +inputs: existing DesignSystemPreview, development preview query routes, production UI components, and deterministic PR test fixtures +outputs: a permanent development-only UI Lab catalog with stable design-system and product-scenario URLs +scope: apps/desktop/src/main.tsx, apps/desktop/src/theme.tsx, apps/desktop/src/i18n/index.tsx, apps/desktop/src/design, apps/desktop/layout-spec.json, apps/desktop/tests, docs/sdlc/changes/2026-09-01-ui-lab +next_trigger: human review of the authorized Draft PR, then future UI Lab scenario additions +verification_mode: owner +verified_by: codex +verified_at: 2026-09-01 +--- + +# Add a permanent UI Lab and design-system demo catalog + +## Intent + +The repository has a substantial `?design-system` preview and several one-off development query +routes, but no single discoverable catalog for stable UI fixtures. During PR-workspace validation a +temporary page was useful for rendering real components with deterministic data, yet the resulting +screenshot could be mistaken for the actual desktop application. The user asked to make a fixed +set of UI test and design-system demo pages available for future design and regression work. + +The desired outcome is a development-only UI Lab with canonical URLs, an explicit fixture identity, +and real production components driven by deterministic local data. It must reuse the existing +design system and previews, avoid a second visual language, avoid remote GitHub actions, and stay +out of normal production application behavior. It is a developer surface, not evidence that an +authenticated real-app workflow passed. + +## Spec + +`?ui-lab=home` is the catalog. It links to `?ui-lab=design-system`, +`?ui-lab=pull-requests`, and `?ui-lab=pr-dock`, and also exposes the existing rich-transcript and +pet previews. The existing `?design-system` URL remains a backwards-compatible alias. Product +scenario pages use a shared lab toolbar with a back link, route identity, a visible +`Deterministic fixture` marker, theme links, and locale links. URL query state is sufficient for +direct reload and browser back/forward behavior. + +The PR workspace scenario renders the real `PullRequestsPage` with stable list, detail, file, +check, reviewer, label, and task data. The PR Dock scenario renders the real Dock and +`GitHubPullRequestPanel` beside a restrained conversation fixture, including Overview and Changes. +Neither scenario calls the desktop bridge or mutates GitHub. Light/dark and English/Chinese +overrides are controlled by URL and do not persist into the user's normal application settings. + +The catalog and scenario shell use the established 196px design-preview navigation width, 48px +toolbar, 1160px content bound, design tokens, and 760px compact breakpoint. At compact widths the +catalog navigation becomes a top bar and scenario content retains its own product breakpoints. +All navigation is semantic, keyboard reachable, visibly focused, and exposes current-page state. + +### Acceptance criteria + +- [x] AC-1: The development renderer exposes a discoverable UI Lab catalog with stable links to + Design System, PR Workspace, PR Dock, rich transcript, and pet preview; the legacy + `?design-system` route remains functional. +- [x] AC-2: PR Workspace and PR Dock render production components with deterministic local data, + make fixture/dev-only identity unmistakable, and perform no bridge or remote mutation. +- [x] AC-3: `theme=system|light|dark` and `lang=en|zh` are URL-addressable for UI Lab routes and do + not overwrite persisted normal-app preferences. +- [x] AC-4: Semantic navigation, current-page state, keyboard names/focus, light/dark rendering, + and desktop/narrow reflow are protected by focused tests and real browser evidence without + horizontal overflow or relevant console errors. +- [x] AC-5: Full desktop tests, renderer lint/type/build, docs and SDLC Gates, and production-build + checks pass; normal application startup remains the fallback when no development preview + query is present. + +## Decision and gates + +The user's direct implementation request on 2026-09-01 accepts Intent and this narrowly scoped +developer-tool design. Codex owns implementation and owner verification. The user's later `pr` +request authorizes creating a branch, pushing this verified scope, and opening a Draft PR. Merge, +release, deployment, and production mutation remain unauthorized. + +## Plan + +1. Record the fixed UI Lab shell and breakpoint contract in the existing layout specification. +2. Add a development-only query router and shared catalog/scenario shell under `src/design`. +3. Move deterministic PR workspace and Dock fixtures into permanent, explicitly labelled + scenarios that render the existing production components. +4. Add non-persistent theme and locale provider overrides for URL-driven preview states while + preserving existing provider behavior everywhere else. +5. Add route and rendered coverage, run full Gates, and inspect standard and narrow views in the + in-app browser before marking this Artifact verified. + +Rollback removes the UI Lab route/files and optional provider overrides. There is no stored data, +backend command, dependency, or remote cleanup. + +## Build + +- Added a development-only `?ui-lab=` router and catalog with canonical Design System, PR + Workspace, and conversation-side PR Dock routes plus links to the existing rich-transcript and + pet previews. The normal application remains the fallback, and the legacy `?design-system` + alias remains available. +- Added a shared scenario toolbar, explicit `Dev only` and `Deterministic fixture` labels, stable + layout-spec geometry, semantic catalog navigation, and responsive catalog/scenario shells using + the existing C2 design tokens. +- Added deterministic PR data and API adapters that render the production `PullRequestsPage`, + `Dock`, and `GitHubPullRequestPanel` without invoking the desktop bridge, network, or GitHub + mutations. +- Added non-persistent theme and locale provider overrides. UI Lab URLs can select system, light, + dark, English, or Chinese without rewriting the user's saved normal-app settings. +- Made the Design System preview inherit the selected URL theme and keep a visible return to UI + Lab at both desktop and compact widths. + +## Verification + +Verdict: verified. + +The in-app browser inspected the development renderer at `http://127.0.0.1:1420/`. At 1440x900, +the dark catalog, light PR Workspace, and dark conversation-side PR Dock rendered without clipping +or horizontal overflow. Theme navigation changed the URL and removed the dark root state. Review +changes selected the production Changes tab and revealed the fixture file list; Dock Changes 30 +rendered the expected added and removed diff lines. At 680x860, PR Workspace switched from list to +detail, PR Dock became a full-width panel, and Design System kept a visible UI Lab return link. +Every inspected state reported `body.scrollWidth == window.innerWidth` and no relevant browser +warnings or errors. + +### Acceptance evidence + +- AC-1: PASS — `tests/uiLabRendered.test.tsx` protects the catalog's five stable destinations, + current-page semantics, layout contract, development gate, and legacy Design System alias; + browser navigation loaded each canonical page with title `C2` and meaningful DOM content. +- AC-2: PASS — `cd apps/desktop && bun test tests/uiLabRendered.test.tsx` protects the production + workspace/Dock composition and deterministic PR, task, current-branch, and diff fixtures. + Browser interaction confirmed the resolved fixture data and production Changes views; the + production Vite output contains none of the UI Lab or fixture strings. +- AC-3: PASS — `cd apps/desktop && bun test tests/uiLabRendered.test.tsx` confirmed dark/Chinese + preview state while the saved light/English preferences remained byte-for-byte unchanged. + Browser theme navigation confirmed URL-addressed light and dark states; Design System inherited + the URL theme. +- AC-4: PASS — `cd apps/desktop && bun test tests/uiLabRendered.test.tsx + tests/githubPullRequestsRendered.test.tsx tests/githubPullRequestPanelRendered.test.tsx` + protects the semantic and interaction contracts. Browser DOM snapshots confirmed semantic + landmarks and controls; screenshots covered dark/light at 1440x900 and compact PR, Dock, and + Design System at 680x860 with no horizontal overflow or console errors. +- AC-5: PASS — full `bun test` passed 799 tests across 138 files with 3829 expectations and zero + failures. `bun run build:renderer` completed ESLint, Stylelint, TypeScript, and Vite build. Final + docs, SDLC, worktree, and whitespace Gates passed. + +Residual risk: UI Lab is intentionally a renderer-only developer surface, so it does not validate +native WebView chrome or authenticated GitHub mutations. Those remote actions remain disabled by +the fixture API and covered by the existing production panel tests rather than executed against a +real repository. + +## Review and release + +Review handoff: [Draft PR #212](https://github.com/IchenDEV/codeTwo/pull/212). +Approval: implementation plus Draft PR delivery from the user's 2026-09-01 requests. +Release target: none. +Release identity: not applicable until released. +Smoke evidence: not applicable until released. +Rollback: revert this change; no migration or remote cleanup is required. +No release: this remains a development surface; Draft PR creation is authorized, while merge, +deployment, and release are not authorized. + +Preparing this section does not authorize merge, deployment, release, or production mutation. + +## Feedback + +No post-implementation feedback exists yet.