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
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ bun run site:build # Astro marketing site build

**npm CLI is also required** (alongside bun) for one specific case: `scripts/build-grammars.sh` and `scripts/build-sql-grammar.sh` use `npm pack` to fetch grammar tarballs from the npm registry. Bun has no equivalent registry-fetch command yet (`bun pm pack` only packs the local package).

## Fresh worktrees: CRLF checkout vs biome's LF requirement

The repo has `core.autocrlf=true` set locally, so a **fresh `git worktree add` checks every file out with CRLF line endings** — while biome (which the lefthook pre-commit runs on staged files) requires LF. The main checkout's files are LF on disk only because they predate the autocrlf setting; a new worktree is CRLF everywhere, so the very first commit touching any file fails the format check with whole-file `␍`-removal diffs. Fix: after editing in a fresh worktree, run `bunx biome check --write <files you touched>` before committing (committed blobs are LF either way — only the working-tree bytes are wrong). Don't mass-format untouched files: that surfaces unrelated pre-existing lint fixes as spurious diffs.

Two other fresh-worktree facts: run `bun install` at the worktree root before anything else (node_modules is per-worktree), and Playwright e2e (`bun run test:e2e`) reuses any server already listening on :1420 (`reuseExistingServer`) — if the main checkout's dev server is running, e2e silently tests the *main checkout's* code, not the worktree's. Stop it first or the results are meaningless.

## React Compiler escape hatch

The React Compiler (`babel-plugin-react-compiler`, wired into `@vitejs/plugin-react` in `vite.config.ts`) auto-memoizes function components and hooks at build time. If a specific component breaks under compilation — usually because it relied on referential identity for a side effect — opt it out file-locally with the `"use no memo"` directive at the very top of the file:
Expand Down
92 changes: 75 additions & 17 deletions src/BorgDock.Tauri/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
// .storybook/main.ts

import type { StorybookConfig } from '@storybook/react-vite';
import { createRequire } from 'node:module';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import type { StorybookConfig } from '@storybook/react-vite';
import tailwindcss from '@tailwindcss/vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';

const here = dirname(fileURLToPath(import.meta.url));

const require = createRequire(import.meta.url);
// Resolve web-tree-sitter from wherever the package manager actually
// installed it (npm: nested in src/BorgDock.Tauri/node_modules, bun
// workspace: hoisted to repo root) — same trick as vite.config.ts.
// Forward slashes only: vite-plugin-static-copy hands `src` to fast-glob,
// which treats Windows backslashes as escape characters.
const webTreeSitterWasm = join(
dirname(require.resolve('web-tree-sitter')),
'web-tree-sitter.wasm',
).replace(/\\/g, '/');

const config: StorybookConfig = {
framework: {
name: '@storybook/react-vite',
Expand All @@ -31,7 +43,7 @@ const config: StorybookConfig = {
viteStaticCopy({
targets: [
{
src: 'node_modules/web-tree-sitter/web-tree-sitter.wasm',
src: webTreeSitterWasm,
dest: '.',
rename: { stripBase: true },
},
Expand Down Expand Up @@ -67,25 +79,71 @@ const config: StorybookConfig = {
{ find: '@tauri-apps/api/core', replacement: resolve(here, 'mocks/tauri-core.ts') },
{ find: '@tauri-apps/api/event', replacement: resolve(here, 'mocks/tauri-event.ts') },
{ find: '@tauri-apps/api/window', replacement: resolve(here, 'mocks/tauri-api-window.ts') },
{ find: '@tauri-apps/api/webviewWindow', replacement: resolve(here, 'mocks/tauri-api-webviewWindow.ts') },
{
find: '@tauri-apps/api/webviewWindow',
replacement: resolve(here, 'mocks/tauri-api-webviewWindow.ts'),
},
{ find: '@tauri-apps/api/app', replacement: resolve(here, 'mocks/tauri-api-app.ts') },
{ find: '@tauri-apps/api/dpi', replacement: resolve(here, 'mocks/tauri-api-dpi.ts') },
{ find: '@tauri-apps/plugin-opener', replacement: resolve(here, 'mocks/tauri-plugin-opener.ts') },
{ find: '@tauri-apps/plugin-store', replacement: resolve(here, 'mocks/tauri-plugin-store.ts') },
{ find: '@tauri-apps/plugin-clipboard-manager', replacement: resolve(here, 'mocks/tauri-plugin-clipboard-manager.ts') },
{ find: '@tauri-apps/plugin-dialog', replacement: resolve(here, 'mocks/tauri-plugin-dialog.ts') },
{
find: '@tauri-apps/plugin-opener',
replacement: resolve(here, 'mocks/tauri-plugin-opener.ts'),
},
{
find: '@tauri-apps/plugin-store',
replacement: resolve(here, 'mocks/tauri-plugin-store.ts'),
},
{
find: '@tauri-apps/plugin-clipboard-manager',
replacement: resolve(here, 'mocks/tauri-plugin-clipboard-manager.ts'),
},
{
find: '@tauri-apps/plugin-dialog',
replacement: resolve(here, 'mocks/tauri-plugin-dialog.ts'),
},
{ find: '@tauri-apps/plugin-fs', replacement: resolve(here, 'mocks/tauri-plugin-fs.ts') },
{ find: '@tauri-apps/plugin-autostart', replacement: resolve(here, 'mocks/tauri-plugin-autostart.ts') },
{
find: '@tauri-apps/plugin-autostart',
replacement: resolve(here, 'mocks/tauri-plugin-autostart.ts'),
},
{ find: '@/services/windows', replacement: resolve(here, 'mocks/services-windows.ts') },
{ find: '@/services/ado/workitems', replacement: resolve(here, 'mocks/services-ado-workitems.ts') },
{ find: '@/services/github/pulls', replacement: resolve(here, 'mocks/services-github-pulls.ts') },
{ find: '@/services/github/checks', replacement: resolve(here, 'mocks/services-github-checks.ts') },
{ find: '@/services/github/auth', replacement: resolve(here, 'mocks/services-github-auth.ts') },
{ find: '@/services/github/reviewThreads', replacement: resolve(here, 'mocks/services-github-reviewThreads.ts') },
{ find: '@/services/github/singleton', replacement: resolve(here, 'mocks/services-github-singleton.ts') },
{ find: '@/services/github/mutations', replacement: resolve(here, 'mocks/services-github-mutations.ts') },
{
find: '@/services/ado/workitems',
replacement: resolve(here, 'mocks/services-ado-workitems.ts'),
},
{
find: '@/services/github/pulls',
replacement: resolve(here, 'mocks/services-github-pulls.ts'),
},
{
find: '@/services/github/checks',
replacement: resolve(here, 'mocks/services-github-checks.ts'),
},
{
find: '@/services/github/polling',
replacement: resolve(here, 'mocks/services-github-polling.ts'),
},
{
find: '@/services/github/auth',
replacement: resolve(here, 'mocks/services-github-auth.ts'),
},
{
find: '@/services/github/reviewThreads',
replacement: resolve(here, 'mocks/services-github-reviewThreads.ts'),
},
{
find: '@/services/github/singleton',
replacement: resolve(here, 'mocks/services-github-singleton.ts'),
},
{
find: '@/services/github/mutations',
replacement: resolve(here, 'mocks/services-github-mutations.ts'),
},
// Anchored regex: matches ONLY the exact bare barrel import, not sub-modules.
{ find: /^@\/services\/github$/, replacement: resolve(here, 'mocks/services-github-barrel.ts') },
{
find: /^@\/services\/github$/,
replacement: resolve(here, 'mocks/services-github-barrel.ts'),
},
{ find: '@/services/pr-actions', replacement: resolve(here, 'mocks/services-pr-actions.ts') },
{ find: '@/generated/changelog', replacement: resolve(here, 'mocks/generated-changelog.ts') },
// Bare '@' catch-all goes last so specific mocks above take priority.
Expand Down
41 changes: 32 additions & 9 deletions src/BorgDock.Tauri/.storybook/mocks/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
// Singleton control surface used by the Tauri mocks and by story decorators.
// Lives on window so dynamic-imported mocks and the React tree can both reach it.

import type { Release } from '../../src/types/whats-new';
import type { WorkItem, WorkItemComment } from '../../src/types/work-item';
import type { PullRequest, PullRequestCommit, PullRequestFileChange } from '../../src/types/pull-request';
import type { CheckRun } from '../../src/types/check-run';
import type { ReviewThread } from '../../src/types/review-thread';
import type { ClaudeReviewComment } from '../../src/types/claude-review';
import type {
PullRequest,
PullRequestCommit,
PullRequestFileChange,
PullRequestWithChecks,
} from '../../src/types/pull-request';
import type { ReviewThread } from '../../src/types/review-thread';
import type { Release } from '../../src/types/whats-new';
import type { WorkItem, WorkItemComment } from '../../src/types/work-item';

export interface InvokeRecord {
command: string;
Expand Down Expand Up @@ -93,23 +98,41 @@ export interface RawReview {
export type GithubResponses = {
getOpenPRs?:
| PullRequest[]
| ((args: { owner: string; repo: string }) => PullRequest[] | Promise<PullRequest[]> | Promise<never>);
| ((args: {
owner: string;
repo: string;
}) => PullRequest[] | Promise<PullRequest[]> | Promise<never>);
getCheckRunsForRef?:
| CheckRun[]
| ((args: { ref: string }) => CheckRun[] | Promise<CheckRun[]> | Promise<never>);
pollOpenPrsAggregate?:
| PullRequestWithChecks[]
| ((args: {
owner: string;
repo: string;
}) => PullRequestWithChecks[] | Promise<PullRequestWithChecks[]> | Promise<never>);
tokenGetter?: () => string | Promise<string>;
// Phase 11 / FilesTab additions
getPRFiles?: PullRequestFileChange[] | (() => PullRequestFileChange[] | Promise<PullRequestFileChange[]>);
getCommitFiles?: PullRequestFileChange[] | (() => PullRequestFileChange[] | Promise<PullRequestFileChange[]>);
getPRFiles?:
| PullRequestFileChange[]
| (() => PullRequestFileChange[] | Promise<PullRequestFileChange[]>);
getCommitFiles?:
| PullRequestFileChange[]
| (() => PullRequestFileChange[] | Promise<PullRequestFileChange[]>);
getPRCommits?: PullRequestCommit[] | (() => PullRequestCommit[] | Promise<PullRequestCommit[]>);
getReviewThreads?: ReviewThread[] | (() => ReviewThread[] | Promise<ReviewThread[]>);
// DiscussionTab additions
getReviews?: RawReview[] | (() => RawReview[] | Promise<RawReview[]>);
getAllComments?: ClaudeReviewComment[] | (() => ClaudeReviewComment[] | Promise<ClaudeReviewComment[]>);
getAllComments?:
| ClaudeReviewComment[]
| (() => ClaudeReviewComment[] | Promise<ClaudeReviewComment[]>);
};

// Phase 11 — pr-actions overrides
export type PrActionResponses = Record<string, '__throw__' | '__fail__' | ((args: unknown) => unknown)>;
export type PrActionResponses = Record<
string,
'__throw__' | '__fail__' | ((args: unknown) => unknown)
>;

export interface StorybookTauriControl {
channels: Map<string, Set<ChannelListener>>;
Expand Down
7 changes: 4 additions & 3 deletions src/BorgDock.Tauri/.storybook/mocks/services-github-barrel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
// the production index.ts, which re-exports via relative paths — and
// those relative paths bypass our sub-path aliases.

export * from './services-github-pulls';
export * from './services-github-checks';
export * from './services-github-auth';
export * from './services-github-reviewThreads';
export * from './services-github-checks';
export * from './services-github-polling';
export * from './services-github-pulls';
export * from './services-github-reviews';
export * from './services-github-reviewThreads';
export * from './services-github-singleton';
22 changes: 22 additions & 0 deletions src/BorgDock.Tauri/.storybook/mocks/services-github-polling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// .storybook/mocks/services-github-polling.ts
//
// Drop-in replacement for @/services/github/polling. Stories that exercise
// the polling cycle stub responses via:
// getControl().githubResponses.pollOpenPrsAggregate = [prWithChecksFixture];
// or a function for pending/rejected promises:
// getControl().githubResponses.pollOpenPrsAggregate = () => new Promise(() => {});

import type { PullRequestWithChecks } from '../../src/types';
import { getControl } from './control';

export const POLL_OPEN_PRS_QUERY = '<<storybook mock>>';

export async function pollOpenPrsAggregate(
_client: unknown,
owner: string,
repo: string,
): Promise<PullRequestWithChecks[]> {
const r = getControl().githubResponses.pollOpenPrsAggregate;
if (typeof r === 'function') return r({ owner, repo });
return r ?? [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,13 @@ function makePr(): PullRequestWithChecks {
commitCount: 1,
requestedReviewers: [],
},
checks: [],
overallStatus: 'green',
failedCheckNames: [],
failedCheckSuiteIds: [],
pendingCheckNames: [],
passedCount: 0,
skippedCount: 0,
totalCheckCount: 0,
};
}

Expand Down Expand Up @@ -301,7 +302,10 @@ describe('Pop-out window title bars — drag region clickability', () => {
it('PrDetailPanel: WindowControls cluster carries bd-wc-group (no-drag escape hatch)', () => {
const { container } = render(<PrDetailPanel pr={makePr()} popOutWindow={true} />);
const cluster = container.querySelector('.bd-wc-group');
expect(cluster, 'PrDetailPanel: cluster missing — buttons would not be clickable').not.toBeNull();
expect(
cluster,
'PrDetailPanel: cluster missing — buttons would not be clickable',
).not.toBeNull();
});

it('WhatsNewApp: WindowControls cluster carries bd-wc-group', () => {
Expand Down
3 changes: 2 additions & 1 deletion src/BorgDock.Tauri/src/components/focus/__tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ export function makePr(
requestedReviewers: [],
...overrides,
},
checks: [],
overallStatus: 'green',
failedCheckNames: [],
failedCheckSuiteIds: [],
pendingCheckNames: [],
passedCount: 1,
skippedCount: 0,
totalCheckCount: 0,
};
}

Expand Down
Loading
Loading