Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/components/chat-rail-modern-redesign.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────
Expand Down
60 changes: 32 additions & 28 deletions src/components/code-surface-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -35,30 +36,33 @@ assert.match(
);
assert.match(
workspace,
/mode === "code" \? \(\s*<CodeView/,
"Workspace renders CodeView on the code mode",
/mode === "code" \|\| mode === "github" \? \([\s\S]{0,300}?<CodeView/,
"Workspace renders CodeView on the code mode and the absorbed github alias",
);
assert.match(
lazySurfaces,
/const loadCodeView = \(\) => import\("@\/components\/code-view"\)\.then\(\(m\) => m\.CodeView\)/,
"CodeView stays code-split behind lazy-surfaces — its chunk must not join the boot bundle",
);

// Flag gating: while caveCodeSurface() is off, "code" deep links keep the
// retirement-era fallback (newest repo chat); with it on they land on the
// surface. Both behaviors live in the same navigate-mode branch.
assert.match(
// Default-on (cave-m6ys): the build-time flag is retired. No workspace wiring
// may resurrect a gate in front of the surface, and the vocabulary records
// GitHub's absorption — "github" is an alias landing on Code's GitHub tab.
const featureFlags = await readFile(new URL("../lib/feature-flags.ts", import.meta.url), "utf8");
assert.doesNotMatch(
featureFlags,
/caveCodeSurface|CAVE_CODE_SURFACE/,
"the Code-surface feature flag is retired — default-on, no env gate",
);
assert.doesNotMatch(
workspace,
/if \(targetMode === "code" && !caveCodeSurface\(\)\) \{[\s\S]*?filter\(\(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
Expand All @@ -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,
Expand Down
11 changes: 8 additions & 3 deletions src/components/code-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
/**
* 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
* per-session workbench (Diff | Files | Terminal | PR) with the follow-up
* 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";
Expand All @@ -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;
Expand All @@ -46,6 +50,7 @@ export type CodeViewProps = {

export function CodeView({
sessions,
initialTopTab,
onJumpToSession,
onFocusCard,
githubTarget,
Expand All @@ -71,7 +76,7 @@ export function CodeView({
window.history.replaceState(null, "", window.location.pathname + (query ? `?${query}` : "") + window.location.hash);
}, []);
const [topTab, setTopTab] = useState<CodeTopTab>(
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
Expand Down
6 changes: 3 additions & 3 deletions src/components/github-native-open.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ assert.match(
);
assert.match(
workspace,
/<GitHubView[\s\S]{0,200}initialTarget=\{githubTarget\}/,
"the GitHub surface receives the deep-link target",
/<CodeView[\s\S]{0,300}githubTarget=\{githubTarget\}/,
"the Code surface receives the deep-link target (GitHub is its tab, cave-m6ys)",
);
assert.match(
workspace,
/if \(mode !== "github" && githubTarget\) setGithubTarget\(null\);/,
/if \(mode !== "github" && mode !== "code" && githubTarget\) setGithubTarget\(null\);/,
"leaving the surface clears the target so later visits don't re-open a stale item",
);

Expand Down
8 changes: 4 additions & 4 deletions src/components/lazy-surfaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ export type WarmableSidebarSurface =
| "grimoire"
| "agents";

export const GitHubView = dynamic(
timed("github", loadGitHubView),
{ ssr: false, loading: SurfaceFallback },
);
// The standalone GitHubView dynamic wrapper is gone (cave-m6ys): GitHub
// mounts only as Code's GitHub tab (code-view.tsx owns that lazy import).
// loadGitHubView stays for preloadSidebarSurface — warming the chunk still
// pays off when the user opens the tab.
Comment on lines +91 to +94

// Code surface (cave-k0ua): hosts diffs, file tree + editor, terminal and the
// GitHub tab — its chunk (CodeMirror et al.) must stay out of the boot bundle.
Expand Down
4 changes: 2 additions & 2 deletions src/components/sidebar-minimal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ assert.doesNotMatch(

assert.match(
source,
/\{ id: "github", label: "GitHub", iconName: "ph:github-logo"/,
"GitHub is visible by default",
/\{ id: "code", label: "Code", iconName: "ph:code"/,
"Code is visible by default (GitHub lives on its GitHub tab, cave-m6ys)",
);

// Recent Activity items must navigate: RecentActivityRollup's onClick calls
Expand Down
15 changes: 5 additions & 10 deletions src/components/sidebar-minimal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
import { sidebarRowState, type SidebarRowState } from "@/lib/sidebar-nav-state";
import { RecentActivityRollup } from "@/components/recent-activity-rollup";
import { SidebarFooter } from "@/components/sidebar-footer";
import { caveCodeSurface } from "@/lib/feature-flags";
import type { ResolvedFamiliar } from "@/lib/familiar-resolve";
import type { SessionRow } from "@/lib/types";
import type { InboxItem } from "@/lib/cave-inbox";
Expand Down Expand Up @@ -132,15 +131,11 @@ const FOLDER_MODES: Array<FolderModeRow> = [
// 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
Expand Down
7 changes: 7 additions & 0 deletions src/components/workspace-alias-modes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}?<CodeView\s+key=\{mode\}[\s\S]{0,200}?initialTopTab=\{mode === "github" \? "github" : undefined\}/,
"the github alias renders the Code surface on its GitHub tab (keyed remount, cave-m6ys)",
);

// ── Every mode-string entry point validates/routes through the shared
// vocabulary instead of ad-hoc special cases ──────────────────────────────

Expand Down
51 changes: 13 additions & 38 deletions src/components/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ import {
FamiliarsView,
FamiliarWorkQueueView,
FamiliarGlyphPicker,
GitHubView,
CodeView,
GrimoireView,
InboxEscalationsView,
Expand All @@ -87,7 +86,6 @@ import {
ShortcutsSheet,
} from "@/components/lazy-surfaces";
import { WorkspaceSidebar } from "@/components/workspace-sidebar";
import { caveCodeSurface } from "@/lib/feature-flags";
import { CHAT_OPEN_PROJECTS_EVENT, CHAT_FOCUS_PROJECT_EVENT, CHAT_OPEN_COVEN_EVENT, markCovenTabPending, markProjectsTabPending } from "@/lib/chat-tab-events";
import { HomeComposer } from "@/components/home-composer";
import { ChatSurface } from "@/components/chat-surface";
Expand Down Expand Up @@ -367,15 +365,6 @@ export function Workspace() {
setModeRaw("inbox");
return;
}
if (next === "code" && !caveCodeSurface()) {
// The Code surface is flag-gated (cave-k0ua). While off, "code" is a
// valid mode string (deep links, persisted last-surface) but must not
// render — land on Chat, the retirement-era fallback. The
// cave:navigate-mode handler upgrades this to "newest repo session"
// before reaching here.
setModeRaw("chat");
return;
}
setModeRaw(next);
}, []);
// Chat mode replaces the global nav with the project-grouped Chats sidebar.
Expand Down Expand Up @@ -514,11 +503,13 @@ export function Workspace() {
}>({ fireAt: "", title: "", whenText: "" });
const [editingReminder, setEditingReminder] = useState<InboxItem | null>(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<GitHubItemTarget | null>(null);
useEffect(() => {
if (mode !== "github" && githubTarget) setGithubTarget(null);
if (mode !== "github" && mode !== "code" && githubTarget) setGithubTarget(null);
}, [mode, githubTarget]);
const [toasts, setToasts] = useState<Toast[]>([]);
const [glyphPickerFor, setGlyphPickerFor] = useState<Familiar | null>(null);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -2855,16 +2832,14 @@ export function Workspace() {
navigationRequest={browserNavigationQueue[0] ?? null}
onNavigationConsumed={acknowledgeBrowserNavigation}
/>
) : mode === "github" ? (
<GitHubView
onJumpToSession={openFamiliarSession}
onFocusCard={(cardId) => 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.
<CodeView
key={mode}
sessions={sessions}
initialTopTab={mode === "github" ? "github" : undefined}
onJumpToSession={openFamiliarSession}
onFocusCard={(cardId) => onPaletteIntent({ kind: "focus-card", cardId })}
githubTarget={githubTarget}
Expand Down
12 changes: 0 additions & 12 deletions src/lib/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
1 change: 1 addition & 0 deletions src/lib/sidebar-nav-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading
Loading