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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- **ADDED**: optional regular-expression search over file paths, with invalid-pattern feedback
- **ADDED**: `regex:` URL hide rules alongside existing wildcards, with validation before saving
- **ADDED**: in-page file tabs with branch-specific scroll positions, separate PR/MR/commit workspaces and trusted session-only metadata
- **ADDED**: Unified / Side-by-side full-file diffs in file tabs and View full, with synchronized panes and preserved source-line positions
- **CHANGED**: switching branches from a file page preserves its resolved file path instead of opening the branch root
- **CHANGED**: enable GitHub device-flow and GitLab PKCE sign-in with Codetree's registered public client IDs
- **CHANGED**: minimum Chrome version to 140 for trusted-only local and Sync storage access
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ The edge tab opens the sidebar. Use **Files**, **Pull requests / Merge requests*

In **Files**, toggle `.*` to search full paths with a case-insensitive regular expression, for example `\.(dart|swift)$` or `^src/`. Enter the pattern without `/` delimiters or flags. Invalid patterns show an error. Search includes loaded entries and combines with **Only unviewed**; the mode resets when navigating or switching tabs.

Click a tree file to open it in Codetree's in-page workspace; clicking it again activates the existing tab. Changed files show full-file diffs. Close tabs with their close buttons, or use Left/Right and Home/End while a file-tab button has focus. Ctrl/Cmd-click still opens the file or native diff in a new browser tab; submodules retain their native navigation. Native comments and review controls remain available below the workspace. **View full** in a native header or the tree's full-diff action still opens the separate dialog, whose previous/next controls cover all changed files independently of sidebar search.
Click a tree file to open it in Codetree's in-page workspace; clicking it again activates the existing tab. Changed files show full-file diffs with a **Unified / Side-by-side** switch, also available in **View full**. Side-by-side aligns old/new lines and synchronizes scrolling; switching keeps your source position. The layout is shared within the page and resets to Unified on reload. Close tabs with their close buttons, or use Left/Right and Home/End while a file-tab button has focus. Ctrl/Cmd-click still opens the file or native diff in a new browser tab; submodules retain their native navigation. Native comments and review controls remain available below the workspace. **View full** in a native header or the tree's full-diff action still opens the separate dialog, whose previous/next controls cover all changed files independently of sidebar search.

Switching branches keeps Code's open paths and active file while loading the selected branch; each branch has separate scroll positions. A missing file stays open with Retry and, when available, an action to open it on the default branch. Each PR, MR and commit has a separate workspace; returning restores its tabs and positions. Selecting a branch from a review exits to Code and restores Code's workspace. Reloading the same native route preserves the selected inline tab; navigating to another native file selects that file.

Expand Down
4 changes: 4 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ Every asynchronous reply is applied only if the generations captured at its star

In-page file tabs use `FILE_TABS` with `scope: 'code'|'review'` and an optional complete metadata `value`. Reads return paths, active path, positions, opaque page identity and a broker-derived `accountID`; saves echo that account ID and reject mismatches, including account changes while queued. Storage keys derive the sender's browser tab ID and selected account, provider, origin, owner/repository and scope. Code shares paths across branches; review adds its validated PR/MR number or commit SHA. Positions are keyed by branch or exact review revisions. `src/shared/file-tabs.js` shares the 32-file/256-position limits; the broker also bounds storage to 50 workspaces and 64 KiB per entry, validates saved metadata on read and never persists text. Session operations do not touch other keys. New workspace behavior still requires installed-browser verification.

`file-content.js` projects validated unified diff rows into paired old/new rows without re-fetching revisions or re-running highlighting. Both split panes virtualize the same row range and synchronize scrolling. The page-local `diffLayout` signal updates tabs and dialogs together; it is not a stored preference. Viewport adapters translate saved tab positions to/from unified row coordinates, keeping the existing `FILE_TABS` schema.

View fields in `state.js` are signal-backed accessors, so factories keep the `state.x = value` syntax. `expanded` is replaced with a new Set and compared by contents. Generations, tree entries, the tree model and keyboard focus stay plain fields. `updateTree()` invalidates one tree derivation after structural changes; expansion and the debounced search invalidate it directly. Multi-field updates use `batch`. Files status is stored once; the visible `loading`/`error` accessors choose between Files and list status without copying on tab switches.

The app root owns sidebar bindings and global listeners. Each navigation disposes its page root, closes its viewer and branch popover, removes native buttons and clears pending folder requests. Page liveness checks complement request generations. Each viewer shell owns its scroll listener and frames, and releases them on close or replacement. `catchError` on the app root reports computation errors as a toast; feature actions run untracked. Settings uses the same primitives with separate preference, account, OAuth and Sync bindings and listens for stored preference changes.
Expand All @@ -107,6 +109,8 @@ See [Checks and releases](RELEASING.md) for CI and publication.
`npm test` runs Node.js regressions without a browser:

- `core.test.mjs`: full-file diff validation.
- `split-diff.test.mjs`: View full layout controls, aligned old/new rows, synchronized/clamped scrolling, source-line positions, font/viewport changes and disposal through the production viewer with synthetic geometry.
- `file-tabs.test.mjs`: file navigation and session restoration, including Split diff positions across file/review switches, Refresh and page reload, plus independent inline/dialog scrolling with a shared layout.
- `hosts.test.mjs`: request contexts built like the content script's, and rejected branch/path segments.
- `hide-rules.test.mjs`: regex and wildcard URL exclusions, validation, persisted rules, URL query/fragment boundaries and code-only scope.
- `viewed.test.mjs`: Viewed marks through the bundled service worker in a VM with mocked APIs.
Expand Down
2 changes: 2 additions & 0 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Both providers require a token for review-state filters. GitLab reviewer/approva

The native button is inserted into a changed file's header. A rerendered header gets one button; a route change clears the old bindings. The sidebar can stay closed. Headers with a native file path receive the button even when API access fails; clicking offers account connection/retry guidance. Website sign-in and extension API authentication are separate. Text previews include all unchanged context, additions and removals, old/new line numbers and previous/next file navigation.

The **Unified / Side-by-side** switch is available in file tabs and **View full**. Unified is the default; the selected layout is shared by both viewers until the page reloads. Side-by-side shows the old revision on the left and the new revision on the right, with aligned change blocks, empty gaps for unmatched additions/removals and synchronized scrolling. Switching reuses the loaded revisions and preserves the top source line. Saved tab positions use unified coordinates so reopening a file in either layout restores the same source location. Ordinary repository text and comments do not show this switch.

An unchanged rename loads the previous path at the base revision and the new path at the head revision. Added/deleted files use an empty opposite revision. Known binary extensions have disabled native buttons; encoding, binary content and size checks also run in the broker.

Changed text must have a complete patch that matches both source revisions. Missing/truncated or incompatible patches show an error with a native diff link. There is no fallback that presents an incomplete file diff as complete.
Expand Down
78 changes: 74 additions & 4 deletions src/content/sidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -909,11 +909,12 @@ button:disabled {
padding: 13px 16px;
border-bottom: 1px solid var(--border);
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.viewer-heading {
flex: 1;
flex: 1 1 160px;
min-width: 0;
}
.viewer-heading strong {
Expand All @@ -931,13 +932,43 @@ button:disabled {
}
.viewer-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
max-width: 100%;
}
.viewer-actions a {
font-size: 11px;
margin-right: 9px;
}
.diff-layout {
display: inline-flex;
flex: 0 0 auto;
gap: 2px;
padding: 2px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--muted-bg);
}
.diff-layout button {
min-height: 24px;
padding: 4px 8px;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--muted);
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.diff-layout button:hover {
background: var(--hover);
color: var(--text);
}
.diff-layout button[aria-pressed='true'] {
background: var(--selected);
color: var(--accent);
}
.viewer[aria-label='Full-file diff'] .viewer-body {
background: var(--code-bg);
color: var(--code-text);
Expand All @@ -949,6 +980,45 @@ button:disabled {
position: relative;
scrollbar-width: thin;
}
.viewer-body.split-diff {
display: flex;
flex-direction: column;
overflow: hidden;
}
.split-diff-labels {
display: grid;
grid-template-columns: 1fr 1fr;
flex-shrink: 0;
background: var(--muted-bg);
color: var(--muted);
border-bottom: 1px solid var(--border);
font-size: 11px;
}
.split-diff-labels span {
padding: 5px 10px;
}
.split-diff-labels span + span,
.split-diff-pane + .split-diff-pane {
border-left: 1px solid var(--border);
}
.split-diff-panes {
display: flex;
flex: 1;
min-height: 0;
}
.split-diff-pane {
flex: 1;
min-width: 0;
overflow: auto;
position: relative;
background: var(--code-bg);
color: var(--code-text);
scrollbar-width: thin;
}
.split-diff-pane:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
// Loading and error states fill the dialog and sit in its middle.
.viewer-body > .empty:only-child {
min-height: 100%;
Expand Down Expand Up @@ -984,6 +1054,9 @@ button:disabled {
.code-row.removed {
background: var(--diff-delete-bg);
}
.code-row.diff-gap {
background: var(--muted-bg);
}
.code-row .line-number {
display: inline-block;
text-align: right;
Expand Down Expand Up @@ -1119,9 +1192,6 @@ button:disabled {
.file-workspace {
margin: 8px;
}
.file-workspace .viewer-header {
flex-wrap: wrap;
}
}
@media (prefers-reduced-motion: reduce) {
* {
Expand Down
2 changes: 2 additions & 0 deletions src/content/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const viewFields = [
'viewLoading',
'viewError',
'diff',
'diffLayout',
'pulls',
'loadingAll',
'selected',
Expand Down Expand Up @@ -83,6 +84,7 @@ export function createState() {
viewLoading: false,
viewError: '',
diff: null,
diffLayout: 'unified',
branches: null,
pulls: [],
loadingAll: false,
Expand Down
Loading
Loading