From 438e071d2ad5cc9793cac4066953a8d591afeecf Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 23 Jul 2026 17:57:08 -0500 Subject: [PATCH] =?UTF-8?q?feat(code):=20default-on=20Code=20surface,=20gi?= =?UTF-8?q?thub=E2=86=92code=20tab=20alias=20(cave-m6ys)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of the Code surface (follow-up to cave-k0ua): the flag is retired rather than defaulted — caveCodeSurface() is deleted and every flag-off fallback with it — and the standalone GitHub surface is absorbed as Code's GitHub tab. "github" moves from the canonical mode union to MODE_ALIASES (kept-in-state tab-selector class, like calendar): deep links, persisted last-surface, palette payloads, and openGitHubTarget() all keep working, landing on Code with the GitHub tab active via key={mode} remount + initialTopTab. The sidebar's conditional spread becomes a static Code row carrying the assigned-work badge; GitHubView's lazy-surfaces wrapper is deleted (loadGitHubView stays — warmup + Code's tab load the same chunk). Deferred (follow-up bead): slimming Chat's code rail and retargeting cave:open-file / pendingCodeRailOpen at the Code surface — the existing rail pin in code-surface-mode.test.ts still documents that boundary. Verified: typecheck; targeted pin suites; full test:api + test:app. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- playwright.config.ts | 5 -- .../chat-rail-modern-redesign.test.ts | 4 +- src/components/code-surface-mode.test.ts | 60 ++++++++++--------- src/components/code-view.tsx | 11 +++- src/components/github-native-open.test.ts | 6 +- src/components/lazy-surfaces.tsx | 8 +-- src/components/sidebar-minimal.test.ts | 4 +- src/components/sidebar-minimal.tsx | 15 ++--- src/components/workspace-alias-modes.test.ts | 7 +++ src/components/workspace.tsx | 51 ++++------------ src/lib/feature-flags.ts | 12 ---- src/lib/sidebar-nav-state.test.ts | 1 + src/lib/workspace-mode.ts | 14 +++-- tests/code-surface.spec.ts | 4 +- 14 files changed, 86 insertions(+), 116 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index e3050184c..74de9aa12 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -121,11 +121,6 @@ export default defineConfig({ reuseExistingServer: false, env: { COVEN_CAVE_E2E: "1", - // The dedicated Code surface (cave-k0ua) is validated flag-ON here so - // tests/code-surface.spec.ts and the mobile drill-in spec exercise the - // real landing; existing specs never asserted the GitHub sidebar row, - // and ?mode=github stays canonical either way. - NEXT_PUBLIC_CAVE_CODE_SURFACE: "1", // Keep app-owned preferences and backdrop bytes out of the developer's // real ~/.coven directory. A per-config UUID prevents concurrent runs or // later PID reuse from sharing stale state while remaining stable for diff --git a/src/components/chat-rail-modern-redesign.test.ts b/src/components/chat-rail-modern-redesign.test.ts index bb9735fef..09305357c 100644 --- a/src/components/chat-rail-modern-redesign.test.ts +++ b/src/components/chat-rail-modern-redesign.test.ts @@ -32,8 +32,8 @@ assert.match( ); assert.match( workspace, - /const targetMode = \(e as CustomEvent<\{ mode\?: string \}>\)\.detail\?\.mode;[\s\S]*?if \(targetMode === "code" && !caveCodeSurface\(\)\)[\s\S]*?setMode\(targetMode as WorkspaceMode\)/, - "The navigate listener redirects flag-off code links, then calls setMode with other requested modes", + /const targetMode = \(e as CustomEvent<\{ mode\?: string \}>\)\.detail\?\.mode;[\s\S]*?setMode\(targetMode as WorkspaceMode\)/, + "The navigate listener funnels every requested mode through setMode (aliases resolve there)", ); // ── Uppercase counted section headers (RESULTS) + compact Projects header ──── diff --git a/src/components/code-surface-mode.test.ts b/src/components/code-surface-mode.test.ts index 42d255c0b..2ba78c3e6 100644 --- a/src/components/code-surface-mode.test.ts +++ b/src/components/code-surface-mode.test.ts @@ -8,10 +8,11 @@ import { readFile } from "node:fs/promises"; // predecessor (code-view.test.ts) was the retirement guard keeping it deleted. // The owner requested a Codex-style multi-session coding surface — diffs, // files, terminal, per-session PR context, worktrees, branches, with GitHub -// absorbed as a tab — so the mode returned, flag-gated by caveCodeSurface() -// (NEXT_PUBLIC_CAVE_CODE_SURFACE). These pins document the sanctioned shape: -// the surface exists, the flag gates entry, and Chat's own code rail stays -// untouched until the flagged follow-up that slims it. +// absorbed as a tab — so the mode returned behind caveCodeSurface(). Phase 2 +// (cave-m6ys) made it default-on: the flag is retired, the standalone GitHub +// surface/row is absorbed ("github" is a tab alias in MODE_ALIASES), and +// Chat's own code rail stays untouched until the flagged follow-up that +// slims it. These pins document that sanctioned shape. const workspace = await readFile(new URL("./workspace.tsx", import.meta.url), "utf8"); const sidebar = await readFile(new URL("./sidebar-minimal.tsx", import.meta.url), "utf8"); @@ -35,8 +36,8 @@ assert.match( ); assert.match( workspace, - /mode === "code" \? \(\s* s\.project_root\)[\s\S]*?openFamiliarSession\(repoSession\.id, repoSession\.familiarId\)[\s\S]*?setMode\("chat"\)/, - "flag-off code deep-links redirect to the newest repo chat or Chat fallback", + /caveCodeSurface/, + "no flag-off fallbacks survive in the Workspace wiring", ); -// The setMode funnel is the choke point for every other entry (?mode= deep -// link, persisted last-surface restore): flag off, "code" lands on Chat -// instead of rendering a gated surface. assert.match( - workspace, - /if \(next === "code" && !caveCodeSurface\(\)\) \{[\s\S]{0,600}?setModeRaw\("chat"\);\s*return;/, - "setMode funnels flag-off code requests to Chat", + modeType, + /github: "code"/, + "MODE_ALIASES routes the absorbed GitHub surface onto Code (cave-m6ys)", ); // File/diff links from inbox cards etc. still target Chat's code rail this @@ -77,21 +81,21 @@ assert.match( "keyboard surface order keeps the primary cluster without Code", ); -// ── Sidebar row swap ───────────────────────────────────────────────────────── +// ── Sidebar row ────────────────────────────────────────────────────────────── -// One quiet slot, two vocabularies: flag on → Code row (GitHub becomes a tab -// inside the surface); flag off → the standalone GitHub row, byte-identical to -// the pre-flag sidebar. The conditional spread keeps FOLDER_MODES a single -// literal so palette/mobile/canonical-name extraction regexes stay valid. +// One quiet slot, one vocabulary (cave-m6ys): the Code row owns it and keeps +// carrying the assigned-work badge; the standalone GitHub row is gone for +// good — its surface lives under Code's GitHub tab and mode "github" aliases +// there, so old muscle memory and deep links still land on the content. assert.match( sidebar, - /\.\.\.\(caveCodeSurface\(\)\s*\?\s*\[\{ id: "code", label: "Code", iconName: "ph:code"/, - "flag on: the Code quiet row takes the GitHub slot", + /\{ id: "code", label: "Code", iconName: "ph:code"[\s\S]{0,200}?badge: \(p\) => badgeText\(p\.githubAssignedCount\)/, + "the Code quiet row owns the slot and carries the assigned-work badge", ); -assert.match( +assert.doesNotMatch( sidebar, - /\{ id: "github", label: "GitHub", iconName: "ph:github-logo"/, - "flag off: the GitHub row literal survives in FOLDER_MODES", + /id: "github"/, + "no standalone GitHub row survives in FOLDER_MODES", ); assert.match( codeView, diff --git a/src/components/code-view.tsx b/src/components/code-view.tsx index 0fa3863df..01cb4d059 100644 --- a/src/components/code-view.tsx +++ b/src/components/code-view.tsx @@ -3,7 +3,7 @@ /** * CodeView — the dedicated Code surface (cave-k0ua): a Codex-style * multi-session coding tab. Reverses the earlier Code-mode retirement on the - * owner's request; gated by caveCodeSurface() (NEXT_PUBLIC_CAVE_CODE_SURFACE). + * owner's request; default-on since phase 2 (cave-m6ys). * * Phase 3+ (this shape): top-level Sessions/GitHub tabs, the session rail * (grouped by project, git-attribution badges, + New session) and the @@ -11,7 +11,8 @@ * composer (code-composer.tsx). New sessions start via code-new-session.tsx — * project + familiar + optional fresh worktree. The inspector and mobile * layout land in follow-up PRs. GitHub mounts whole under the GitHub tab - * (its sidebar row hides when the flag is on). + * (the standalone GitHub surface and its sidebar row were absorbed; the + * "github" workspace mode is now a tab alias landing here). */ import React, { useEffect, useMemo, useRef, useState } from "react"; @@ -38,6 +39,9 @@ const LazyGitHubView = dynamic( export type CodeViewProps = { sessions: SessionRow[]; + /** Landing tab override — the "github" mode alias mounts CodeView on its + * GitHub tab (deep-link continuity for the absorbed standalone surface). */ + initialTopTab?: CodeTopTab; onJumpToSession: (sessionId: string, familiarId?: string | null) => void; onFocusCard: (cardId: string) => void; githubTarget?: GitHubItemTarget | null; @@ -46,6 +50,7 @@ export type CodeViewProps = { export function CodeView({ sessions, + initialTopTab, onJumpToSession, onFocusCard, githubTarget, @@ -71,7 +76,7 @@ export function CodeView({ window.history.replaceState(null, "", window.location.pathname + (query ? `?${query}` : "") + window.location.hash); }, []); const [topTab, setTopTab] = useState( - githubTarget ? "github" : deepLink?.topTab ?? "sessions", + githubTarget ? "github" : deepLink?.topTab ?? initialTopTab ?? "sessions", ); // Selection is tri-state for the mobile drill-in: `undefined` = nothing // chosen yet (auto-pick allowed), `null` = the user explicitly went Back to diff --git a/src/components/github-native-open.test.ts b/src/components/github-native-open.test.ts index af4554bed..265c01e53 100644 --- a/src/components/github-native-open.test.ts +++ b/src/components/github-native-open.test.ts @@ -36,12 +36,12 @@ assert.match( ); assert.match( workspace, - / = [ // Submissions (OpenCoven runtime/harness submit) is hidden from the nav; the // mode + page remain reachable programmatically but aren't surfaced here. // - // Code ⇄ GitHub row swap (cave-k0ua): behind caveCodeSurface(), the Codex- - // style Code surface takes over this quiet slot and GitHub becomes a tab - // inside it (the row keeps carrying the assigned-work badge). Flag off keeps - // the standalone GitHub row exactly as before. A conditional spread keeps - // FOLDER_MODES one literal, so the palette, mobile tabs, and the - // canonical-name pins all derive from whichever vocabulary is active. - ...(caveCodeSurface() - ? [{ id: "code", label: "Code", iconName: "ph:code", description: "Multi-session coding — diffs, files, branches, worktrees, and GitHub", badge: (p) => badgeText(p.githubAssignedCount), quiet: true } satisfies FolderModeRow] - : [{ id: "github", label: "GitHub", iconName: "ph:github-logo", description: "Issues and PRs assigned to you", badge: (p) => badgeText(p.githubAssignedCount), quiet: true } satisfies FolderModeRow]), + // Code (cave-k0ua, default-on since cave-m6ys): the Codex-style coding + // surface owns this quiet slot; GitHub is a tab inside it (the row keeps + // carrying the assigned-work badge). The standalone GitHub row is gone — + // mode "github" is a tab alias that lands on Code's GitHub tab. + { id: "code", label: "Code", iconName: "ph:code", description: "Multi-session coding — diffs, files, branches, worktrees, and GitHub", badge: (p) => badgeText(p.githubAssignedCount), quiet: true }, ]; // Rows actually rendered in the sidebar — everything except on-demand surfaces diff --git a/src/components/workspace-alias-modes.test.ts b/src/components/workspace-alias-modes.test.ts index f8e544da7..c176f1b21 100644 --- a/src/components/workspace-alias-modes.test.ts +++ b/src/components/workspace-alias-modes.test.ts @@ -62,6 +62,13 @@ assert.match( "the roles/capabilities aliases render the Marketplace hub on their sections (keyed remount)", ); +assert.equal(MODE_ALIASES.github, "code"); +assert.match( + workspace, + /mode === "code" \|\| mode === "github"[\s\S]{0,500}?({ fireAt: "", title: "", whenText: "" }); const [editingReminder, setEditingReminder] = useState(null); // Deep-link target for the native GitHub surface (a GitHub-event inbox - // notification's PR/issue). Cleared on leaving the surface so a later manual - // visit doesn't re-open a stale item. + // notification's PR/issue). GitHub lives on the Code surface's GitHub tab + // (cave-m6ys), so the target survives within the whole surface — mode + // "github" (the tab alias) or "code" — and clears on leaving it so a later + // manual visit doesn't re-open a stale item. const [githubTarget, setGithubTarget] = useState(null); useEffect(() => { - if (mode !== "github" && githubTarget) setGithubTarget(null); + if (mode !== "github" && mode !== "code" && githubTarget) setGithubTarget(null); }, [mode, githubTarget]); const [toasts, setToasts] = useState([]); const [glyphPickerFor, setGlyphPickerFor] = useState(null); @@ -1779,22 +1770,8 @@ export function Workspace() { const onNavigate = (e: Event) => { const targetMode = (e as CustomEvent<{ mode?: string }>).detail?.mode; if (!targetMode) return; - // Legacy fallback while the Code surface flag is off: redirect to the - // most-recent repo session in chat (the retirement-era behavior). With - // the flag on, "code" falls through to setMode and lands on the surface. - if (targetMode === "code" && !caveCodeSurface()) { - const repoSession = [...sessionsRef.current] - .filter((s) => s.project_root) - .sort((a, b) => (b.updated_at || b.created_at).localeCompare(a.updated_at || a.created_at))[0]; - if (repoSession) { - openFamiliarSession(repoSession.id, repoSession.familiarId); - } else { - setMode("chat"); - } - return; - } - // Alias modes (flow, journal, groupchat, …) need no special-casing: - // setMode's alias funnel routes them via MODE_ALIASES. + // Alias modes (flow, journal, groupchat, github, …) need no + // special-casing: setMode's alias funnel routes them via MODE_ALIASES. setMode(targetMode as WorkspaceMode); }; window.addEventListener("cave:navigate-mode", onNavigate as EventListener); @@ -2855,16 +2832,14 @@ export function Workspace() { navigationRequest={browserNavigationQueue[0] ?? null} onNavigationConsumed={acknowledgeBrowserNavigation} /> - ) : mode === "github" ? ( - onPaletteIntent({ kind: "focus-card", cardId })} - initialTarget={githubTarget} - onTasksRefresh={() => void loadGitHubTasks(true)} - /> - ) : mode === "code" ? ( + ) : mode === "code" || mode === "github" ? ( + // "github" is a tab alias (cave-m6ys): the standalone surface was + // absorbed as Code's GitHub tab. Keyed remount so ?mode=github deep + // links land on the tab even when Code is already the active surface. onPaletteIntent({ kind: "focus-card", cardId })} githubTarget={githubTarget} diff --git a/src/lib/feature-flags.ts b/src/lib/feature-flags.ts index 700711d98..435c73d63 100644 --- a/src/lib/feature-flags.ts +++ b/src/lib/feature-flags.ts @@ -6,15 +6,3 @@ function envFlag(value: string | undefined): boolean { export function caveChatoutCodex(): boolean { return envFlag(process.env.NEXT_PUBLIC_CAVE_CHATOUT_CODEX); } - -/** - * Dedicated Code surface (cave-k0ua): the Codex-style multi-session coding - * tab. While flagged off, the sidebar keeps the GitHub row and `?mode=code` - * deep links fall back to the legacy redirect (most-recent repo chat). When - * on, the Code row replaces the GitHub row (GitHub mounts as a tab inside - * Code). NEXT_PUBLIC_ env vars are inlined at build time, so both branches - * of the swap stay in the bundle but the choice is fixed per build. - */ -export function caveCodeSurface(): boolean { - return envFlag(process.env.NEXT_PUBLIC_CAVE_CODE_SURFACE); -} diff --git a/src/lib/sidebar-nav-state.test.ts b/src/lib/sidebar-nav-state.test.ts index f5098d39b..10228c260 100644 --- a/src/lib/sidebar-nav-state.test.ts +++ b/src/lib/sidebar-nav-state.test.ts @@ -40,6 +40,7 @@ test("deep-linkable modes hosted by another surface light the host row (cave-s9p assert.equal(sidebarRowState("inbox", "calendar"), "active", "calendar renders on Schedules"); assert.equal(sidebarRowState("board", "familiar-work-queue"), "active", "the Queue is a Tasks-hub tab"); assert.equal(sidebarRowState("inbox", "flow"), "active", "retired flow remaps to Schedules"); + assert.equal(sidebarRowState("code", "github"), "active", "github is Code's GitHub tab (cave-m6ys)"); }); test("Journal is a Memories tab — the Memories (grimoire) row hosts it", () => { diff --git a/src/lib/workspace-mode.ts b/src/lib/workspace-mode.ts index 8ad7cf524..7c761fc2b 100644 --- a/src/lib/workspace-mode.ts +++ b/src/lib/workspace-mode.ts @@ -17,7 +17,6 @@ export type CanonicalWorkspaceMode = | "board" | "inbox" | "browser" - | "github" | "code" | "marketplace" | "submissions" @@ -31,7 +30,8 @@ export type AliasWorkspaceMode = | "calendar" | "familiar-work-queue" | "roles" - | "capabilities"; + | "capabilities" + | "github"; export type WorkspaceMode = CanonicalWorkspaceMode | AliasWorkspaceMode; @@ -42,10 +42,9 @@ export const CANONICAL_WORKSPACE_MODES: readonly CanonicalWorkspaceMode[] = [ "board", "inbox", "browser", - "github", // Code — the Codex-style multi-session coding surface (cave-k0ua). Reverses - // the earlier Code-mode retirement: gated by caveCodeSurface(); while the - // flag is off, setMode redirects "code" to the legacy chat fallback. + // the earlier Code-mode retirement; default-on since phase 2 (cave-m6ys), + // with GitHub absorbed as its GitHub tab ("github" is now an alias below). "code", "marketplace", "submissions", @@ -66,7 +65,9 @@ export const CANONICAL_WORKSPACE_MODES: readonly CanonicalWorkspaceMode[] = [ * the canonical surface on the matching tab, keyed by the alias so deep * links remount onto it — `calendar` (Rituals' Calendar tab), * `familiar-work-queue` (Tasks' Queue tab), `roles` / `capabilities` - * (Marketplace hub sections). + * (Marketplace hub sections), `github` (Code's GitHub tab — the standalone + * surface was absorbed in cave-m6ys; old deep links and persisted + * last-surface strings keep landing on the same content). * * workspace-alias-modes.test.ts pins Workspace's branches to this table; * sidebar-nav-state derives row highlighting from it. @@ -79,6 +80,7 @@ export const MODE_ALIASES = { "familiar-work-queue": "board", roles: "marketplace", capabilities: "marketplace", + github: "code", } as const satisfies Record; export function isAliasWorkspaceMode(mode: string): mode is AliasWorkspaceMode { diff --git a/tests/code-surface.spec.ts b/tests/code-surface.spec.ts index 131e2af27..c3e618501 100644 --- a/tests/code-surface.spec.ts +++ b/tests/code-surface.spec.ts @@ -3,9 +3,7 @@ import { expect, test, type Page } from "@playwright/test"; // The dedicated Code surface (cave-k0ua): a Codex-style multi-session coding // tab — session rail grouped by project, per-session workbench // (Diff | Files | Terminal | PR), inspector column, and a GitHub top tab. -// Runs flag-ON via NEXT_PUBLIC_CAVE_CODE_SURFACE=1 in playwright.config.ts's -// webServer env (NEXT_PUBLIC_ flags are inlined at dev-server start, so a -// per-test toggle is impossible by construction). +// Default-on since phase 2 (cave-m6ys) — no flag env needed. // // Daemon-less — onboarding dismissed, every endpoint mocked via page.route.