Adds a per-project forced provider (github|gitlab|gitea) on top of the
per-project API base URL overrides: stored under gitProviders.provider in
projects/<projectId>.json, sanitized server-side, and winning over remote-host
detection both in useGitProvider and in server repo resolution
(parseGitLabRemoteUrl/parseGiteaRemoteUrl accept any host when the provider
is forced). The Projects settings page replaces the three always-visible URL
fields with a provider selector (auto-detect + the three forges) and one URL
override for the active provider. Global provider override fields on the
GitHub/GitLab/Gitea settings tabs now render only once an account is
connected, and Settings search availability matches that gating.
Also fixes the useConfigStore/useDirectoryStore circular-import TDZ in the
bundled chunk via the window-registered store handle and defers the directory
subscription to a microtask; fixes the Gitea PR merge payload (Do carries the
merge-style string enum, not a boolean + MergeMethod); and adds a documented
Gitea client live-test harness (scripts/gitea-live-test.ts + client.d.ts).
Per provider (github|gitlab|gitea) a project override stored in
~/.config/openchamber/projects/<projectId>.json under gitProviders wins
over the global settings.json value (precedence: project override > global
> built-in default). Server forge routes resolve the override per request
directory (worktree-aware via git-common-dir + containment + path fallback,
60s TTL cache); the override host is also accepted for remote parsing and
client detection. New GET/PUT /api/projects/:projectId/git-providers route;
client openchamberConfig preserves the server-owned gitProviders key;
Projects page gains a Git provider API base URLs section; detection store
hydrates per-project overrides (memory-only, server-authoritative).
Configure a default API base URL per git provider (github/gitlab/gitea)
in settings.json gitProviders, with GitHub Enterprise support (Octokit
baseUrl + device-flow web origin derived from the API base), and replace
the client-side custom-domains list with server-persisted detection URL
chips (SSH/HTTPS forms normalized to hosts). The configured API base host
auto-counts as a detection host. Settings round-trip through the existing
/api/config/settings sanitizer; the UI store hydrates from server settings
with a one-time localStorage migration.
The assignee/label/milestone combobox and @-mention list were inline
absolute-positioned inside the ContextPanel rail (overflow-hidden) and the
PR-view scroll container, so the panels were clipped and invisible. Render
both via createPortal to document.body with fixed positioning computed from
the trigger's viewport rect (viewport-edge clamping, flip to the other side
when there is no room), close on outside pointerdown/scroll/resize while
still allowing interaction inside the panel, and keep the panel inside the
outside-click check.
Also show the loading spinner until the first lookup completes instead of a
brief 'No matches' flash (useForgeLookup now reports 'initialized').
The Gitea create-issue path (server route, client, wire api, facade, UI
button/dialog) landed with the forge user-lookup work; close the remaining
verification/documentation gaps:
- wire: gitea.test.ts covers issueCreate (POST /api/gitea/issues/create,
full/optional body, error throw)
- docs: DOCUMENTATION.md lists createIssue client method and the
POST /repos/{owner}/{repo}/issues endpoint (labels as names)
- ui: fix stale 'read-only by design' JSDoc in GiteaIssuesSection — creation
is offered here and the detail view provides edit/close/reopen
Graceful probe: an optional stat of a config/backup path outside the active
workspace now reports exists:false instead of 400, so boot-time probes don't
surface console errors for a check that was never mandatory.
Repo-scoped assignable-user search for GitHub, GitLab, and Gitea, surfaced as
an assignee combobox in the metadata editor and @-mention autocomplete in
forge comment/reply/review surfaces.
- server: GET /api/{provider}/users/search (assignees / project members),
query + directory/override repo resolution, 429 -> 503, connected:false
degradation; GitLab assignee writes resolve login -> ID server-side
- wire: searchUsers (+ searchLabels/milestones/branches/tags) on the three
API clients with tests
- facade: userSearch capability (all three), searchUsers adapters,
mapGithubAssignee/mapGitlabMember/mapGiteaAssignee -> ForgeUser
- ui: ForgeLookupCombobox (keyboard nav, debounced 30s-TTL cache,
connected-only caching), ForgeMentionTextarea (@ token parsing, caret
restore), free-text fallback when lookup is unavailable; i18n in 12 locales
- extras sharing the same infrastructure: GitLab create-issue dialog and
label/milestone/branch/tag lookups in the metadata editor
- linkedIssues: provider/repo/host fields (backward-compatible), gitea id generation fix, parseLinkedIssueRef/parseForgeEntityUrl
- work-status panel: live state cards (TTL-cached), link-by-URL dialog, per-row unlink
- forge entity view: 'Chats working on this' section derived from the session store
Add the Gitea teapot mark (Simple Icons, 24x24) to the icon sprite as a
custom icon, and swap the gitea fallback icons for it wherever the
provider brand is shown: context rail PR surface, context panel PR tab,
GitHeader PR chip, and the work-status Gitea PR row.
Extract a single shared parseGitHost (packages/ui/src/lib/gitHost.ts) used
by both the custom-domain registry (normalizeProviderDomain) and remote
detection (gitProvider). Previously both parsers required a user@ prefix,
rejecting valid bare scp remotes like codeberg.org:owner/repo.git, and
mangled IPv6 hosts. Now scp forms with or without a user, ssh:// URLs with
ports, git+ssh scheme, and bracketed/unbracketed IPv6 all normalize to a
bare hostname; Windows-path-like input is rejected.
Add an Issues tab alongside the merge-request / pull-request content in the
context panel's PR surface for both providers. The tab lists open issues
lazily and expands a selected issue into an inline detail view (body, labels,
assignees, comments) with a back action. Read-only: no issue create, update,
or close actions. Adds 15 i18n keys across all 11 locales.
Branch status resolves an open PR across the whole fork network first, so a
merged fork PR can never hide an open upstream PR for the same head. Only when
no target has an open PR does the branch's newest closed/merged PR come back,
as history.
The panel shows that history as a compact note and offers creating the next PR
below it, instead of either sticking on a terminal PR or going blank after a
merge. Terminal associations stay persisted for reload continuity but are never
treated as authority: they revalidate on the discovery cadence and on focus.
History is looked up only for the branch's own remote and name, and remembered
per repo+branch, so the extra lookup cannot exhaust the route's resolve budget.
The checks summary and merge-permission lookup are skipped for a closed or
merged PR, where neither is actionable.
Auto-derived project labels were title-cased, turning .ssh into .Ssh and
opencode-claude into Opencode Claude. Show the folder name verbatim in the
sidebar, window title, settings selector and notification templates, and
migrate persisted legacy labels back to the folder name (manual renames
are preserved).
Create no longer aborts when recoverStaleDraftDirectory rewrites the
implicit new-chat draft to the active project during the create probe.
Also rank the Unreleased Chat bullet below the changelog highlights.
Co-authored-by: serkraser <serkraser@gmail.com>
Keep both Unreleased changelog bullets: the lastDirectory new-chat
fallback and the Git PR panel stale-status fix from main.
Co-authored-by: serkraser <serkraser@gmail.com>