Skip to content
Open
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
11 changes: 9 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@
*.swp
*.swo
*~
docs/
/docs/
.collaborator
.superpowers
.claude/
notes/
Screenshots/
.worktrees
opencode.json
opencode.json

# Generated SCM QA repos (scripts in fixtures/git/ are tracked)
fixtures/git/merge-conflict/
fixtures/git/submodule/
fixtures/git/rebase-todo/
fixtures/git/dirty-worktree/
fixtures/git/_bare-dirty.git/
8 changes: 7 additions & 1 deletion collab-electron/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ data/

# Playwright MCP
.playwright-mcp/
scripts/scm-screenshots/.deps/node_modules/
scripts/scm-screenshots/harness/fixture-data.json

# Editor/IDE
*.swp
Expand All @@ -40,6 +42,10 @@ electron/out/
# Back up these files separately when migrating machines
.claude/
CLAUDE.md
docs/
# Private docs — allow versioned SCM QA docs (tracked specs live under docs/superpowers/)
docs/*
!docs/SCM_TEST_MATRIX.md
!docs/screenshots/
!docs/screenshots/**
dev-app-update.yml
build-debug.log
71 changes: 71 additions & 0 deletions collab-electron/docs/SCM_TEST_MATRIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# SCM test matrix

Manual QA checklist for Source Control (Phase 0–2).

## Foundation

- [ ] `git status` shows `repoState` (clean, merging, rebasing, interactive-rebase, cherry-picking, reverting)
- [ ] Selected remote in SyncBar matches Commit & Push / Commit & Sync
- [ ] Refresh updates counts and branch

## Phase A — Viewer diff

- [ ] Click staged/unstaged file opens Monaco diff in viewer tile
- [ ] Header shows path and ref labels; Esc closes diff
- [ ] Compare with Branch opens branch picker and diff

## Phase B — Clone & remotes

- [ ] Clone Repository from non-repo workspace adds workspace
- [ ] Manage remotes: add, remove (with confirm)

## Phase C — Conflicts

- [ ] Merge banner shows Continue / Abort during merge
- [ ] Accept Current / Incoming / Mark Resolved on conflict files
- [ ] Commit disabled while conflicts exist

## Phase D — Context menus

- [ ] Right-click file: stage, unstage, discard, open changes, reveal
- [ ] SCM header menu: refresh, pull, push, open .gitignore

## Phase E — History

- [ ] History section lists commits; context menu revert/cherry-pick/reset
- [ ] Open Graph starts replay

## Phase F — Interactive rebase

- [ ] During `git rebase -i`, todo list appears with reorder and action dropdowns
- [ ] Continue / Abort work

## Phase G — Submodules & worktrees

- [ ] Submodules listed when `.gitmodules` exists; Update runs
- [ ] Worktrees listed when multiple exist

## Phase H — Partial staging

- [ ] Stage hunk from diff viewer (when hunks available)

## Phase I — Signing & config

- [ ] Sign commit checkbox when `commit.gpgsign` configured
- [ ] Settings → Git shows read-only user.name, email, credential.helper

## Fixtures

From repo root, run `./fixtures/git/setup-all.sh` (see `fixtures/git/README.md`). Open a generated folder as a workspace:

| Path | Covers |
|------|--------|
| `fixtures/git/merge-conflict` | Phase C — merge banner, conflict markers in `base.txt` |
| `fixtures/git/submodule` | Phase G — `child` submodule |
| `fixtures/git/rebase-todo` | Phase F — interactive rebase paused at `break` |
| `fixtures/git/dirty-worktree` | Overview — staged/unstaged/untracked, tags, stashes, sync bar |

## Phase I — LFS badge

- [ ] Repo with `*.bin filter=lfs diff=lfs merge=lfs -text` in `.gitattributes` shows **LFS** badge on matching changed files
- [ ] Badge absent when `git-lfs` is not installed or path is not LFS-tracked
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added collab-electron/docs/screenshots/scm/08-stash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions collab-electron/docs/screenshots/scm/QA_RESULTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# SCM QA results

Run date: 2026-06-01
Branch: `dev` (PR #44)

## Automated (fixture + backend)

| Area | Result | Notes |
|------|--------|-------|
| Fixture setup (`setup-all.sh`) | **PASS** | merge-conflict, submodule, rebase-todo, dirty-worktree |
| `gitStatus` / `gitRepoState` | **PASS** | clean, merging, interactive-rebase states |
| Stage / unstage / commit | **PASS** | temp repo in `git-source-control.fixture.test.ts` |
| Branches / tags / remotes / stash | **PASS** | dirty-worktree fixture |
| Merge conflict detection | **PASS** | unmerged `base.txt` in merge-conflict |
| Rebase todo parse | **PASS** | Falls back to `git-rebase-todo.backup` when paused at `break` |
| Submodule / worktree list | **PASS** | submodule fixture |
| `gitLog` / `gitDiff` | **PASS** | dirty-worktree |
| `npm run build` (collab-electron) | **PASS** | electron-vite production build |
| Dark screenshots 01–10 | **PASS** | `bun scripts/capture-scm-screenshots.mjs` |

Test command:

```bash
./fixtures/git/setup-all.sh
cd collab-electron && bun test src/main/git-source-control.fixture.test.ts
```

## Manual only (Electron UI)

Per [SCM_TEST_MATRIX.md](../../SCM_TEST_MATRIX.md): context menus, real push/pull, clone flow, GPG sign commit, LFS badge, graph replay, partial hunk staging in live app. Screenshots use component harness + git bridge, not full Electron shell.

## Fix applied during QA

- `gitRebaseTodoList`: read `git-rebase-todo.backup` when primary todo is empty (interactive rebase paused at `break`).
30 changes: 30 additions & 0 deletions collab-electron/docs/screenshots/scm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SCM screenshots

PNG captures of the Source Control UI (Phase 0–2) for PR #44 review. Rendered with real `@collab/components` styles, dark theme, and live `git` data from `fixtures/git/`.

## Regenerating

From the repository root:

```bash
./fixtures/git/setup-all.sh
cd collab-electron
bun scripts/capture-scm-screenshots.mjs
```

Requires Bun, Chromium (installed automatically via Playwright on first run), and `git` on `PATH`.

## Files

| File | What it shows |
|------|----------------|
| `01-scm-overview.png` | Full SCM panel: branch header, sync bar, commit box, staged/unstaged/untracked changes (`fixtures/git/dirty-worktree`) |
| `02-commit-box.png` | Commit message area with sample text, Amend, Commit / Commit & Push / Commit & Sync |
| `03-branch-picker.png` | Branch picker dropdown with local branches and tags |
| `04-sync-remotes.png` | **Manage Remotes** modal (origin + backup from dirty-worktree fixture) |
| `05-merge-conflicts.png` | Merge-in-progress banner and conflicted `base.txt` (`fixtures/git/merge-conflict`) |
| `06-history.png` | History section expanded with commit list and selected commit file list |
| `07-interactive-rebase.png` | Interactive rebase todo list with actions (`fixtures/git/rebase-todo`) |
| `08-stash.png` | Stashes section expanded with saved stashes |
| `09-viewer-diff.png` | Viewer-style Monaco side-by-side diff for `base.txt` during merge |
| `10-settings-git.png` | Settings → Git read-only config (`user.name`, `user.email`, etc.) |
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.diff-editor-root {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}

.diff-editor-hunks {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 6px 8px;
border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}

.diff-editor-hunk-btn {
font-size: 11px;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: transparent;
color: var(--foreground);
cursor: pointer;
}

.diff-editor-hunk-btn:hover {
background: color-mix(in srgb, var(--foreground) 8%, transparent);
}

.diff-editor-container {
flex: 1;
min-height: 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useEffect, useRef } from "react";
import * as monaco from "monaco-editor";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import "./DiffEditorView.css";

self.MonacoEnvironment = {
getWorker() {
return new editorWorker();
},
};

function languageFromPath(filePath: string): string {
const ext = filePath.slice(filePath.lastIndexOf(".") + 1).toLowerCase();
const map: Record<string, string> = {
ts: "typescript",
tsx: "typescript",
js: "javascript",
jsx: "javascript",
json: "json",
py: "python",
md: "markdown",
css: "css",
html: "html",
yaml: "yaml",
yml: "yaml",
sh: "shell",
};
return map[ext] ?? "plaintext";
}

export interface DiffEditorViewProps {
filePath: string;
original: string;
modified: string;
readOnly?: boolean;
theme?: "light" | "dark";
onStageHunk?: (patch: string) => void;
hunks?: Array<{ index: number; header: string; patch: string }>;
}

export function DiffEditorView({
filePath,
original,
modified,
readOnly = true,
theme = "dark",
onStageHunk,
hunks,
}: DiffEditorViewProps) {
const containerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<monaco.editor.IStandaloneDiffEditor | null>(null);

useEffect(() => {
if (!containerRef.current) return;

const lang = languageFromPath(filePath);
const originalModel = monaco.editor.createModel(
original,
lang,
monaco.Uri.parse(`git-diff-original://${filePath}`),
);
const modifiedModel = monaco.editor.createModel(
modified,
lang,
monaco.Uri.parse(`git-diff-modified://${filePath}`),
);

const diffEditor = monaco.editor.createDiffEditor(containerRef.current, {
readOnly,
renderSideBySide: true,
automaticLayout: true,
theme: theme === "dark" ? "vs-dark" : "vs",
originalEditable: false,
});
diffEditor.setModel({ original: originalModel, modified: modifiedModel });
editorRef.current = diffEditor;

return () => {
diffEditor.dispose();
originalModel.dispose();
modifiedModel.dispose();
editorRef.current = null;
};
}, [filePath, original, modified, readOnly, theme]);

return (
<div className="diff-editor-root">
{hunks && hunks.length > 0 && onStageHunk && (
<div className="diff-editor-hunks">
{hunks.map((hunk) => (
<button
key={hunk.index}
type="button"
className="diff-editor-hunk-btn"
onClick={() => onStageHunk(hunk.patch)}
title="Stage this hunk"
>
Stage hunk {hunk.index + 1}
</button>
))}
</div>
)}
<div ref={containerRef} className="diff-editor-container" />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { DiffEditorView } from "./DiffEditorView";
export type { DiffEditorViewProps } from "./DiffEditorView";
Loading