* feat(gitlab): add foundational glab runner, types, and issue operations
First slice of GitLab support, mirroring src/main/github/ structurally
without refactoring the working GitHub path.
- runner: add glabExecFileAsync parallel to ghExecFileAsync (same WSL
routing and retry policy; HTTP-status / network classification is
provider-agnostic so the existing helpers are reused).
- types: GitLabProjectRef carries host alongside path so self-hosted
instances and nested groups round-trip through the IPC layer. Mirror
shapes for MR/issue/work-item/comment/file/assignable-user.
- gitlab/gl-utils: concurrency limiter, error classification, project-ref
resolution honoring upstream/origin preference, and known-host
discovery via `glab auth status` so non-gitlab.com remotes are
recognized after the user authenticates.
- gitlab/mappers: pipeline-job → check-status mapping, MR state
resolution (including draft inferred from `Draft:`/`WIP:` title
prefix), and pipeline rollup.
- gitlab/issues: full issue CRUD via `glab api` against URL-encoded
project paths, with the same upstream/origin preference semantics as
the GitHub side.
63 unit tests passing across gl-utils / mappers / issues. Both
typecheck:node and typecheck:web clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): preflight glab auth check and URL parser
- preflight: probe `glab --version` + `glab auth status` alongside the
existing gh checks. PreflightStatus.glab is optional so renderer call
sites that only render git/gh keep typechecking; consumers gating on
GitLab affordances opt in via `glab?.authenticated`.
- gitlab-links: parse GitLab issue and merge-request URLs honoring (a)
arbitrary self-hosted hosts via the project-internal `/-/` separator
rather than locking to gitlab.com, (b) nested group paths, and (c)
GitLab's `!42` MR convention alongside `#42`.
26 unit tests added (5 new preflight cases, 21 URL-parser cases). Full
typecheck (node + cli + web) clean. Pre-existing runtime/orchestration
test failures unrelated to this branch — Node 25 vs the project's
pinned Node 24 engine.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): MR list/get + paginated `glab api -i` helper
Lean mirror of github/client.ts focused on the workspace-from-MR
keystone. Adopts GitLab-native filter semantics (Open / Merged /
Closed / All) instead of porting GitHub's search-DSL — that path is
covered by the upcoming My Todos surface.
- gl-utils: glabApiWithHeaders + parseGlabApiResponse for strict
pagination via X-Total / X-Total-Pages on `glab api -i` output.
CRLF / LF tolerant; status line never leaks into the headers map.
- types: MRListState, GitLabPagedResult<T>, ListMergeRequestsResult.
- mappers: mapMRToWorkItem + mapIssueToWorkItem produce the unified
GitLabWorkItem shape the picker consumes. isCrossRepository derived
from source_project_id !== target_project_id; deterministic id
fallback when the per-MR detail endpoint omits global id.
- client: getAuthenticatedViewer, getMergeRequest (with head pipeline
rolled up), getMergeRequestForBranch (mirrors github/getPRForBranch
semantics including refs/heads/ stripping and detached-HEAD guard),
listMergeRequests (paginated), getWorkItemByProjectRef (paste-URL
flow). Re-exports issues + projectRef helpers so callers don't have
to know the gl-utils module split.
35 new tests (98 total in src/main/gitlab/), full typecheck clean.
Tests split into client.test.ts + client-mr.test.ts to stay under the
oxlint max-lines budget — matches github/client*.test.ts pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): worktrees:resolveMrBase IPC + linkedGitLab* persistence
The workspace-from-MR keystone. Mirror of worktrees:resolvePrBase
shape and semantics — caller passes mrIid (with optional source_branch
/ isCrossRepository hints), handler returns either a remote/branch
ref (same-project MRs) or a SHA fetched from
refs/merge-requests/<iid>/head (fork MRs).
- types: linkedGitLabMR / linkedGitLabIssue on Worktree + WorktreeMeta.
Marked optional so existing test fixtures and persisted older
worktrees that pre-date these fields keep typechecking and loading
without a migration.
- persistence: getDefaultWorktreeMeta initializes both fields to null.
- worktree-logic: mergeWorktree carries them through from meta.
- worktrees IPC: resolveMrBase mirrors resolvePrBase. Resolves the
GitLab project via getProjectRef + known-host discovery, fetches the
MR work-item to derive source_branch + isCrossRepository when those
hints aren't provided, and uses GitLab's refs/merge-requests/<iid>/head
for fork MRs (parallel of GitHub's refs/pull/<N>/head).
- tests: 6 fixture updates for the new optional fields. Full
typecheck (node + cli + web) clean; 165 tests passing across
src/main/gitlab/, preflight, worktree-logic, and gitlab-links.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): IPC channels + preload bindings (gl.*)
Wire the GitLab backend to the renderer. Lean v1 surface — issues
CRUD, MR list/get/getForBranch, viewer, project slug, paste-URL
work-item lookup. Skips workItemDetails / listWorkItems-combined /
listTodos until the matching backend pieces land.
- main/ipc/gitlab.ts: thirteen handlers under the `gitlab:*` channel
prefix with the same assertRegisteredRepo guard the gh handlers use.
listIssues unwraps the structured result envelope to bare items[]
to match window.api.gh.listIssues' shape; consumers that need the
classified error can graduate to the envelope later.
- main/ipc/register-core-handlers.ts: register alongside gh.
- preload/api-types.ts: typed `gl: { ... }` block parallel to the
existing `gh: { ... }`. Imports the new GitLab types so renderer
code consuming the preload gets full inference.
- preload/index.ts: runtime `gl: { ... }` exposes wired to ipcRenderer.
Full typecheck (node + cli + web) clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): workspace-from-MR via paste-URL (keystone end-to-end)
The first user-visible GitLab moment. Pasting a GitLab issue or MR URL
into the workspace name field now resolves through the full pipeline
to a created workspace with the right base ref and linkedGitLab*
persisted. The dedicated GitLab tab + state-filter chips remain a
follow-up; everything below it is wired.
- shared/lib/new-workspace.ts: LinkedWorkItemSummary.type accepts
`'mr'` alongside `'issue' | 'pr'`. Renderer code that switches on
type explicitly handles each kind.
- ui store slice: NewWorkspaceDraft mirrors the new linked slots so
drafts persist GitLab selections across navigation. Optional fields
for backward compatibility with drafts saved before this branch.
- useComposerState:
- linkedGitLabIssue / linkedGitLabMR state, draft persistence,
repo-switch reset, applyWorktreeMeta wiring.
- applyLinkedGitLabWorkItem mirrors applyLinkedWorkItem; reuses
getLinkedWorkItemSuggestedName by structurally projecting the
GitLab item onto the helper's input shape.
- handleSmartGitLabItemSelect parallels handleSmartGitHubItemSelect:
for picked MRs, calls window.api.worktrees.resolveMrBase to
resolve the base ref (refs/merge-requests/<iid>/head for fork
MRs) and threads it through handleBaseBranchMrSelect.
- "was MR !N" reset hint when a repo switch wipes a GitLab
selection — `!N` matches gitlab.com's MR-reference convention.
- preload: window.api.worktrees.resolveMrBase + window.api.gl.* are
already in. ComposerCardProps grows onSmartGitLabItemSelect (+
optional onBaseBranchMrSelect).
- SmartWorkspaceNameField:
- Paste-URL detection: parseGitLabIssueOrMRLink (host-agnostic via
`/-/` separator) → window.api.gl.workItemByPath → row in the
dropdown → click → forwarded to onGitLabItemSelect.
- SmartWorkspaceNameSelection union, RowEntry union, RowIcon,
RowLabel, SelectionIcon all carry the gitlab-mr / gitlab-issue
kinds. MR rows show `!N` prefix; issue rows show `#N`.
- Tab UI not added in this commit — paste-URL works in 'smart'
mode, the dedicated tab + Open/Merged/Closed/All chips lands
in a follow-up.
- NewWorkspaceComposerCard: forwards onSmartGitLabItemSelect to the
picker.
Full typecheck (node + cli + web) clean. 165 unit tests passing in
affected files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): GitLab tab in SmartWorkspaceNameField with state filter
The discoverable demo path. The picker now has a "GitLab" tab — when
selected it lists the project's MRs filtered by state via
`gitlab:listMRs`, with an Open / Merged / Closed / All chip strip
that mirrors gitlab.com's MR-page tab strip. Paste-URL detection in
'smart' mode is unchanged; the new tab simply makes the surface
discoverable without requiring a URL.
- SmartNameMode gains 'gitlab'; Gitlab icon (lucide) added to the
MODES array between GitHub and Branch.
- MrStateFilter / MR_STATE_FILTERS centralizes the four chip values
so the labels stay GitLab-native (Open vs the GraphQL 'opened').
- listMRs effect: fires when mode === 'gitlab' and no GitLab URL is
in the input, with the current state filter and a page-1 fetch
bounded by RESULT_LIMIT.
- Paste-URL effect now coexists with the list effect: it owns
gitlabItems while a URL is in the input, the list effect owns it
otherwise. Switching tabs no longer clears the list.
- Chip strip rendered above the popover's CommandList only when
mode === 'gitlab'. Buttons use the same Button component the rest
of the picker uses for visual consistency.
Full typecheck (node + cli + web) clean. 165 unit tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): GitLab source on Tasks screen
The Tasks screen now offers GitLab as a third source alongside GitHub
and Linear. Selecting it surfaces MRs and issues for the primary
selected repo with a state filter (Open / Merged / Closed / All) that
mirrors gitlab.com's MR-page tab strip. Skips cross-repo aggregation,
search DSL, and Projects mode for v1 — those layers are GitHub-API-
shaped and would need a parallel store slice that is not worth porting
ahead of the actual demand for them.
- shared/types: GlobalSettings.defaultTaskSource accepts 'gitlab'.
- TaskPage:
- TaskSource union grows a 'gitlab' member; SOURCE_OPTIONS adds the
Gitlab icon between GitHub and Linear so the toolbar order matches
SmartWorkspaceNameField for cross-surface consistency.
- GITLAB_TASK_FILTERS centralizes the four chip values.
- Per-source state slim (matches Linear's pattern) — gitlabFilter,
gitlabItems, gitlabLoading, gitlabError, gitlabRefreshNonce.
- Data-fetch effect runs Promise.all over `window.api.gl.listMRs`
and `window.api.gl.listIssues` for the primary repo, merges and
sorts by updatedAt desc. 'merged' filter skips the issue fetch
(GitLab issues are 'opened' / 'closed' only).
- Filter bar block parallel to Linear's, with chips + a refresh
icon-button.
- List block: 5-column grid (ID / Title / Type+State / Updated /
Open-link). Row click opens the web URL — the GitLabItemDialog
is a follow-up commit, but the row affordance is enough for the
Tasks-screen demo.
- GitLab MRs render as `!N`; issues render as `#N` to match
gitlab.com's reference convention.
Full typecheck (node + cli + web) clean. 165 unit tests still
passing in affected files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): GitLabItemDialog (minimal) + Tasks screen wiring
Clicking a GitLab row on the Tasks screen now opens a side-sheet
preview with the item's title, state, author, and description body
rendered as markdown. "Open in browser" footer button stays as the
escape hatch; opening from the row is dialog-first now (matching the
GitHub side's row-click-to-dialog pattern). Files / comments /
pipeline tabs are deferred — they mirror substantial GitHub-side
surface area (work-item-details ~550 lines, GitHubItemDialog 2680
lines) and are not blocking the demo.
- types: MRInfo and GitLabIssueInfo gain optional description /
author / authorAvatarUrl. Optional because list endpoints strip
them; populated on detail-endpoint reads (`getMR` / `getIssue`).
- mappers: mapMRInfo and mapGitLabIssueInfo now pass description /
author / avatar through when present. Skipped (rather than
defaulted to '') so callers can distinguish "no body authored"
from "this came from a list".
- GitLabItemDialog: new ~200-line side sheet. Fetches the detail
payload via `window.api.gl.mr` / `gl.issue` on open; renders
CommentMarkdown for the description (reused from the GitHub
side); falls back to "No description." when the body is blank.
State badge tones picked locally — GitLab's MR state space is
wider than GitHub's so coupling them buys nothing.
- TaskPage: GitLab row now uses a div role=button with keyboard
handling so the inner Open-in-browser <button> nests cleanly
(HTML disallows nested <button>s, React would warn). Row click
sets gitlabDialogItem; the small ExternalLink icon stops
propagation so it still opens the URL.
Full typecheck (node + cli + web) clean. 165 unit tests passing in
affected files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): My Todos cross-project view on Tasks screen
The GitLab tab now has a Project | My Todos sub-toggle. "My Todos"
fetches gitlab.com/dashboard/todos via `glab api todos?state=pending`
and surfaces them in a separate table — action / title / project /
updated. This is the closest GitLab-native equivalent of GitHub's
notifications/inbox and lands in lieu of porting GitHub's search-DSL
which doesn't translate.
- shared/types: GitLabTodo type with action_name, target_type/iid,
target_url, project_path, author, updated_at. action_name kept as
open-ended string because new GitLab versions extend the verb set.
- gitlab/client.ts: listTodos uses `glab api --paginate todos?state=
pending&per_page=50`. User-scoped — cwd doesn't matter, but the
IPC path-validation guard still requires *some* registered repo
path so we keep the signature consistent with the rest of gl.*.
- IPC: `gitlab:todos` channel; preload `gl.todos`.
- TaskPage:
- gitlabView ('project' | 'todos') gates which list to render.
- Sub-toggle row above the chip strip; chips are hidden on the
Todos view since pending state has no Open/Merged/Closed axis.
- Refresh button serves both views (uses gitlabRefreshNonce).
- Todos table: 5-col grid, action verb (snake_case → spaces),
target title, project path (mono font for repo-likeness),
updated date, open-link icon. Row click opens target_url.
Full typecheck (node + cli + web) clean. 165 unit tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): gitlabProjects settings (recents auto-tracked) + tests
Settings persistence for GitLab project preferences plus tests for
the surface added since the last green run.
- shared/types: GitLabProjectSettings { pinned, recent } and an
optional GlobalSettings.gitlabProjects slot. Optional for
backward compat with profiles saved before this branch — the
persistence merge fills the empty default.
- shared/gitlab-projects: pure helper computeNextGitLabRecents that
prepends-and-dedupes by host+path, caps at GITLAB_RECENTS_MAX
(10). Pulled out of the IPC handler so it tests without mocking
Store.
- gitlab IPC: workItemByPath handler now pushes the resolved
project ref onto recents on success. 404 / auth-fail lookups
do not pollute the list — recents reflects projects the user
actually read.
Tests added (12 new, 177 total passing in affected files):
- gitlab-projects.test: prepend, dedupe, host-vs-host distinct,
cap at max, no input mutation.
- client.test: listTodos mapping, defensive state coercion,
empty-on-error fallback, missing-target field defaults.
- mappers.test: description / author / authorAvatarUrl pass-
through on both mapMRInfo and mapGitLabIssueInfo, plus the
"absent vs blank" distinguishing assertion.
- mappers-workitem.test (split): mapMRToWorkItem + mapIssueToWorkItem
cases moved out of mappers.test.ts to keep both files under the
oxlint max-lines budget.
Full typecheck (node + cli + web) clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): combined listWorkItems IPC + TaskPage refactor
Centralize the MR + issue merge logic that TaskPage was doing inline
into a single backend function and IPC channel. Future callers (the
picker's GitLab tab, any new widget) get the merge / sort / state-
mapping rule for free. The TaskPage effect drops from 60 lines of
inline orchestration to a single call.
- gitlab/issues: listIssues now accepts an IssueListState so the
combined caller can ask for closed / all instead of always opened.
CLI fallback path picks the right --opened / --closed / --all flag
per glab version. Existing callers keep the 'opened' default.
- gitlab/client: listWorkItems(state, page, perPage, preference) fans
out listMergeRequests + a raw issues fetch in parallel, merges by
updatedAt desc, returns a GitLabPagedResult<GitLabWorkItem>.
Bypasses listIssues for the issues side because IssueInfo strips
updated_at — the combined sort needs it.
state='merged' skips the issues fetch entirely (issues don't have
a merged lifecycle).
- IPC: new gitlab:listWorkItems handler.
- preload: gl.listWorkItems alongside gl.listMRs.
- TaskPage: GitLab fetch effect now calls gl.listWorkItems and stops
re-implementing the merge. Same UX, fewer moving parts.
Tests added (8 new in client-work-items.test.ts; +1 fix to
issues.test.ts for the new url-param order):
- merge ordering by updatedAt desc
- 'merged' state skips issues fetch
- closed / all state pass-through
- not_found envelope when project ref unresolved
- mr-error vs issue-side success interleaving
- combined error surfacing on either side failing
Full typecheck (node + cli + web) clean. 117 unit tests passing in
src/main/gitlab/ and src/shared/gitlab-projects.test.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): work-item-details + dialog Conversation/Pipeline tabs
Task 3 lean version. The minimal description-only dialog grows two
new tabs (Conversation / Pipeline) and four footer actions
(close / reopen / merge / comment). Files-tab and inline review-
comment positioning stay deferred — they mirror substantial GitHub-
side surface (GitHubItemDialog is 2680 lines, work-item-details.ts is
551) and the v1 demo doesn't need them.
- shared/types: GitLabPipelineJob (id, name, stage, status, webUrl,
duration), GitLabWorkItemDetails (item + body + comments[] +
pipelineJobs?[]). Mirrors GitHubWorkItemDetails layout.
- main/gitlab/work-item-details: getWorkItemDetails(repoPath, iid,
type) fans out parallel reads — issue: detail + discussions; MR:
detail + discussions, then pipeline jobs follow-up keyed off
head_pipeline.id. Discussion → MRComment flatten skips system
notes (auto-generated activity entries) so the conversation tab
shows only user content. Inline-review position carried through
as `path` + `line` for v1.5 to consume.
- main/gitlab/client: closeMR / reopenMR / mergeMR / addMRComment
mutations. mergeMR accepts the same 'merge' | 'squash' | 'rebase'
union as the GitHub side; close/reopen treat "already X" stderr
as success since the desired state is reached.
- IPC: gitlab:workItemDetails, closeMR, reopenMR, mergeMR,
addMRComment channels; preload `gl.*` bindings parallel.
- GitLabItemDialog rewrite: three Tabs (Description / Conversation /
Pipeline-MRs-only) + footer with comment composer + state-aware
Merge / Close / Reopen buttons. Cmd/Ctrl+Enter sends the comment
to match gitlab.com's textarea shortcut. Refresh icon in the
header re-fetches via a refreshNonce. eslint-disable max-lines on
the dialog matches the GitHub-side equivalent's reasoning.
Full typecheck (node + cli + web) clean. 184 unit tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): sidebar icon, Smart-mix MRs, Integrations card, "Project MRs" rename
Four follow-up fixes that surfaced from smoke-testing:
- SidebarNav: GitLab icon next to GitHub / Linear in the Tasks-row
shortcut strip; clicks open the Tasks page already filtered to the
GitLab source. ui.ts taskPageData.taskSource union grows to accept
'gitlab' so the openTaskPage call typechecks.
- SmartWorkspaceNameField: list-MRs effect now fires in 'smart' mode
too, not just on the dedicated GitLab tab. The mixed picker
surfaces the user's project MRs alongside GitHub items. Paste-URL
effect still wins when a GitLab URL is in the input — the list
effect bails on parsedGlLink !== null.
- TaskPage: GitLab toggle relabels "Project" → "Project MRs" so the
pairing with "My Todos" reads more clearly.
- IntegrationsPane: new GitLab card mirroring the GitHub card —
status badge (checking / connected / not-installed / not-
authenticated), install link to gitlab.com/gitlab-org/cli, copy-
ready `glab auth login` block, learn-more link to the auth/login
doc, re-check button. Search-entry registered so settings search
finds it. eslint-disable max-lines justified by the same pattern
that already lives there for GitHub + Linear.
- preload: PreflightStatus.glab is optional on the type so older
payloads typecheck; consumers gate on the optional chain.
Full typecheck (node + cli + web) clean. 184 unit tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gitlab): multi-repo aggregation on Tasks screen
Mirrors GitHub's cross-repo behavior. Previously the GitLab tab only
queried the first selected repo; now it fans out to every eligible
selected repo in parallel and merges results sorted by updatedAt
desc. The repo selector at the top of Tasks is the project picker —
it's the same one the GitHub tab uses, so the selection model is
consistent across providers.
- TaskPage gitlab fetch effect: Promise.allSettled across all
selectedRepos that aren't SSH-relay (folder-mode repos and remote
worktrees fall through). Each repo's project is resolved from its
own git remote by the main process; non-GitLab repos return
not_found which the renderer drops silently so a mixed selection
(GitHub + GitLab repos) doesn't surface false errors on the GitLab
tab.
- Per-row repoId tagging stays correct — items keep their source
repo's id through the merge, which matters for the dialog repoPath
resolution below.
- Banner display: only shown when EVERY eligible repo failed; partial
failure is signaled by the row count being lower, not a banner that
overshadows working repos.
- GitLabItemDialog repoPath: derived from the clicked item's
source repo (selectedRepos.find by repoId) instead of primaryRepo.
Without this, clicking an item from a non-primary repo would route
the detail fetch through the wrong repo's remote.
Full typecheck (node + cli + web) clean. 117 unit tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(gitlab): swap MR icon to GitMerge for visual distinction
GitPullRequest (curved-merge) reads similar to GitBranch (forking
line) at the small sizes we use in the picker — feedback was that
MR rows looked like branch rows. GitMerge (arrow-merge-into-line)
reads as its own thing and matches gitlab.com's MR iconography, so
users coming from the web UI find it familiar.
GitHub PRs keep GitPullRequest — that matches github.com and keeps
provider attribution distinct from GitLab MRs at a glance:
GitHub PR: GitPullRequest (curved merge)
GitLab MR: GitMerge (arrow merge)
Branch: GitBranch (fork)
Issue: CircleDot (provider-agnostic)
- SmartWorkspaceNameField RowIcon + SelectionIcon: gitlab-mr →
GitMerge. github-pr stays GitPullRequest.
- GitLabItemDialog header icon: GitMerge for MRs.
Full typecheck (node + cli + web) clean. 117 unit tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(gitlab): split shared types + preload into per-provider files
Pre-emptive merge-conflict reduction. The two recent main syncs
each surfaced ~5 conflicts, all in the same handful of central
files where every provider lands code. Moving the GitLab footprint
into provider-scoped files cuts the conflict surface roughly in half
without changing any runtime behavior.
- shared/gitlab-types.ts (new, 272 lines): every standalone GitLab
type that previously lived in shared/types.ts —
GitLabProjectRef / MRState / MRMergeableState / MRCheckDetail /
MRInfo / GitLabReaction / MRComment / GitLabCommentResult /
GitLabIssueInfo / GitLabViewer / GitLabAssignableUser /
GitLabWorkItem / GitLabMRFile / GitLabProjectSettings /
GitLabTodo[TargetType] / GitLabPipelineJob /
GitLabWorkItemDetails / GitLabIssueUpdate / MRListState /
GitLabPagedResult / ListMergeRequestsResult.
- shared/types.ts: re-exports the GitLab types so existing call
sites importing from '../shared/types' keep working unchanged.
GitLabProjectSettings additionally imported locally for the
GlobalSettings.gitlabProjects field. Worktree.linkedGitLabMR /
WorktreeMeta.linkedGitLabIssue / GlobalSettings.defaultTaskSource
union member stay here — they're entangled with non-GitLab
structs and moving them out would just shuffle the conflict
vector to a different file.
- preload/gitlab.ts (new, 106 lines): the entire gl.* runtime
binding block — viewer / projectSlug / mrForBranch / mr /
listMRs / listWorkItems / issue / listIssues / createIssue /
updateIssue / addIssueComment / listLabels /
listAssignableUsers / todos / workItemDetails / closeMR /
reopenMR / mergeMR / addMRComment / workItemByPath. Exported as
`glApi`.
- preload/index.ts: imports `glApi` and inlines as `gl: glApi`,
shrinking the file by ~95 lines.
Net: the two files most prone to conflict on upstream sync
(shared/types.ts, preload/index.ts) lose ~360 lines of
GitLab-specific code that now live in their own files where main's
non-GitLab edits can't touch them.
Full typecheck (node + cli + web) clean. 190 unit tests passing
in src/main/gitlab/, src/shared/gitlab-projects.test.ts,
src/main/ipc/{preflight,worktree-logic}.test.ts,
src/renderer/src/lib/gitlab-links.test.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(gitlab): satisfy pnpm pre-flight (lint + handler-registration test)
- TaskPage: lift the selected-repos identity key into a useMemo so the
GitLab fetch effect's dep array no longer holds a complex expression
(oxlint exhaustive-deps).
- register-core-handlers.test: mock ./gitlab alongside ./github / ./linear
so registerGitLabHandlers doesn't try to call ipcMain.handle in a unit
test that fakes only individual handler modules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(source-control): add Bitbucket hosted review support
* fix(source-control): align hosted review lookup with provider model
---------
Co-authored-by: Emilian Stoilkov <emilian.stoilkov@qaiware.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Improve mobile terminal streaming performance
Co-authored-by: Orca <help@stably.ai>
* Add mobile clear terminal action
Co-authored-by: Orca <help@stably.ai>
* Fix terminal connection test mock
Co-authored-by: Orca <help@stably.ai>
* WIP: mobile markdown tabs before rebase
Co-authored-by: Orca <help@stably.ai>
* Add mobile markdown editing
Co-authored-by: Orca <help@stably.ai>
* Harden mobile tab and markdown sync
Co-authored-by: Orca <help@stably.ai>
* Fix mobile terminal reconnect loading race
Co-authored-by: Orca <help@stably.ai>
* Polish mobile terminal keyboard behavior
Co-authored-by: Orca <help@stably.ai>
* Simplify mobile markdown editor chrome
Co-authored-by: Orca <help@stably.ai>
* Move mobile markdown actions to top
Co-authored-by: Orca <help@stably.ai>
* Use app modals for markdown discard
Co-authored-by: Orca <help@stably.ai>
* Dismiss keyboard before markdown confirmations
Co-authored-by: Orca <help@stably.ai>
* Add mobile file explorer
Co-authored-by: Orca <help@stably.ai>
* Fix mobile file explorer type narrowing
Co-authored-by: Orca <help@stably.ai>
* Fix mobile files navigation param
Co-authored-by: Orca <help@stably.ai>
* Show mobile files connection wait state
Co-authored-by: Orca <help@stably.ai>
* Preview text files on mobile
Co-authored-by: Orca <help@stably.ai>
* Simplify mobile file previews
Co-authored-by: Orca <help@stably.ai>
* Clarify unavailable mobile file types
Co-authored-by: Orca <help@stably.ai>
* Fix mobile subscription and preview review issues
Co-authored-by: Orca <help@stably.ai>
* Keep fallback terminals visible on mobile
Co-authored-by: Orca <help@stably.ai>
* Keep mobile terminal tap active
Co-authored-by: Orca <help@stably.ai>
* Preserve mobile terminal fallback order
Co-authored-by: Orca <help@stably.ai>
* Fix mobile session tab authority
Co-authored-by: Orca <help@stably.ai>
* Run mobile tests in mobile CI lane
Co-authored-by: Orca <help@stably.ai>
* Bump mobile app version to 0.0.7
Co-authored-by: Orca <help@stably.ai>
* Allow main window IPC wiring size
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(agent-dashboard): persist hook status across Orca restart
Hydrates the hook server's per-pane lastStatusByPaneKey from
userData/agent-hooks/last-status.json before binding the HTTP listener,
mirrors mutations to disk via a 250ms trailing debounce, and flushes
synchronously on stop(). Renderer dismissals fan out a new
agentStatus:drop IPC so the on-disk file evicts the entry and a
relaunch cannot resurrect it. Adds a bounded bootstrap queue in
useIpcEvents so events replayed by setListener() during window creation
are not dropped while App.tsx is still hydrating tabsByWorktree.
Gated on settings.experimentalAgentDashboard. Done, blocked, and quiet
working rows now all survive across restart.
Co-authored-by: Orca <help@stably.ai>
* fix(agent-dashboard): harden hook persistence IPC and gate-off deletion
Address review findings on the retention-restart branch:
- Wrap agentStatus:getSnapshot and agentStatus:drop IPC handlers in
try/catch so a throw cannot surface as an unhandled invoke rejection
(silent startup-hydration failure) or crash main from a fire-and-
forget listener.
- runStatusPersist no longer permanently suppresses gate-off deletion
retries on transient unlink errors (e.g. EPERM); deletedOnDisable
now flips only on success or ENOENT.
- Tighten tests: stale-version-hydrate now asserts the warn message
content; getSnapshot test uses toEqual; drop-handler test rejects
null/{}/[] in addition to the prior bad inputs.
Co-authored-by: Orca <help@stably.ai>
* fix(agent-dashboard): bound on-disk hydrate growth and reject tabId/paneKey drift
- Drop hydrate entries older than 7 days (HYDRATE_MAX_AGE_MS) so stale
rows from worktrees archived weeks ago do not pile up forever. PTY-
teardown eviction handles closed panes; the TTL covers daemon-restored
PTYs that never re-attach and crash-recovery paths.
- Reject hydrate entries whose `tabId` field diverges from the paneKey's
tab segment. Cheap defensive add against future renamer/shape drift.
Doc updated to move TTL out of the follow-ups list (now in scope).
Tests: new "drops hydrate entries older than the TTL cutoff" and "drops
a hydrate entry whose tabId disagrees with the paneKey prefix"; existing
hydrate fixtures now use a `recentTs()` helper instead of fixed 2023
timestamps.
Co-authored-by: Orca <help@stably.ai>
* fix(agent-dashboard): post-review polish on hook status persistence
Apply review-fix corrections on the agent-dashboard restart-persistence
work:
- Split dropStatusEntry from clearPaneState so renderer-driven dismiss
IPC no longer wipes lastPromptByPaneKey/lastToolByPaneKey for a
still-alive pane.
- Validate paneKey shape at the IPC boundary (isValidPaneKey).
- Let getSnapshot errors propagate instead of silently returning [] —
matches the renderer's existing .catch and avoids masking a broken
persistence path.
- Trust main's authoritative timing.stateStartedAt unconditionally on
same-state pings; fall back to existing only when timing is absent.
- Use strict < on the snapshot/live updatedAt guard so two events in
the same millisecond don't drop the second one (a <= guard regressed
two existing slice tests).
- Don't reset snapshotRequestedForReadyWindow in the catch handler;
combined with the per-store-update subscriber it would retry-storm
on persistent IPC failure.
- scheduleStatusPersist now resets the timer on each call (true
trailing-edge debounce) instead of leading-edge throttle.
- Fix doc references that named clearPaneState in dismiss/IPC context
where the implementation uses dropStatusEntry; add type-level JSDoc
on AgentStatusIpcPayload.
109/109 in-scope tests pass.
Co-authored-by: Orca <help@stably.ai>
* fix(agent-dashboard): clean stale on-disk entries during hydrate
- Defensive `lastStatusByPaneKey.clear()` at top of `hydrateLastStatusFromDisk` keeps repeat-start() calls from silently merging prior-session state.
- When sanitize drops entries (drift, TTL, schema), log a single `[agent-hooks] last-status hydrate dropped N entries (kept M)` warn and synchronously rewrite the file. Pre-fix, stale entries stayed on disk until a fresh hook event triggered a debounced write — users who hadn't run an agent in 8+ days would re-drop the same entries every cold boot.
- Prime `lastWrittenJson` from the raw on-disk bytes (instead of re-serializing) when hydration is lossless — robust against future shape drift in `serializeStatusFile`.
- `LAST_STATUS_FILE_VERSION = 2` comment now records why v1 was skipped (in-flight branch shape).
- IPC test mock uses `vi.importActual` for `isValidPaneKey` so it stays in sync with the real validator.
Co-authored-by: Orca <help@stably.ai>
* fix(agent-dashboard): persist acknowledgedAgentsByPaneKey across restart
Without this, agent rows the user already visited come back bold every relaunch now that the rows themselves survive restart (per docs/agent-dashboard-retention-restart.md). Hydrate sanitizes input field-by-field (rejects null/non-object/array, prototype-pollution keys, non-finite/non-positive values) and applies a 7-day TTL paralleling HYDRATE_MAX_AGE_MS in agent-hooks/server.ts so hard-quit/crash paths can't grow the persisted map forever.
Co-authored-by: Orca <help@stably.ai>
* docs(agent-dashboard): drop in-tree retention/restart design doc
Doc was a working artifact for this branch; the rationale lives in commit
history and the comments next to the persistence/hydrate code. Scrubs the
three call-site references that named it.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(sidebar): allow manual drag-and-drop reordering of repos
Users can now drag repo headers in the sidebar to reorder them. The
custom order is persisted to disk and survives restarts. Includes
design doc at docs/manual-repo-reorder.md.
Co-authored-by: Orca <help@stably.ai>
* fix: scope post-drag click swallow to dragged repo header
Avoid silently eating unrelated clicks if one races between pointerup and
the failsafe teardown.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(agent-hooks): introduce relay wire envelope + connectionId stamping
Adds the shared `agent-hook-relay.ts` module with the `agent.hook` JSON-RPC
notification envelope, the `agent_hook.requestReplay` /
`agent_hook.installPlugins` method names, and the
`ORCA_FEATURE_REMOTE_AGENT_HOOKS` flag helper. Promotes `AgentHookSource` to
`shared/` so the relay can import it without dragging Electron in.
Threads a `connectionId: string | null` field through `AgentHookEventPayload`,
the `agentStatus:set` IPC contract, and the renderer-bound preload listener.
Local hook posts stamp `null`; the relay-forwarded path will stamp from `mux`
identity in a later commit. Renderer uses the stamp for stale-event filtering
when an SSH connection tears down with notifications still in flight.
See docs/design/agent-status-over-ssh.md §1, §5, §8 (commit #1).
Co-authored-by: Orca <help@stably.ai>
* refactor(agent-hooks): extract shared listener; add relay-side adapter
Extracts the listener internals (request parsing, payload normalization,
endpoint-file writing, per-CLI extractors, warn-once Sets, slowloris timer
helper, request size cap, paneKey caches) from `src/main/agent-hooks/server.ts`
into a new transport-agnostic `src/shared/agent-hook-listener.ts`. The shared
module uses only Node builtins (no Electron) so it is safe to import from
`src/relay/`.
Adds `src/relay/agent-hook-server.ts` — a thin HTTP-loopback adapter that
wires the shared listener to a `forward(envelope)` callback so `relay.ts` can
re-emit each parsed payload as an `agent.hook` JSON-RPC notification on the
existing SshChannelMultiplexer. The adapter owns:
- 127.0.0.1:0 socket + bearer-token auth, identical shape to the local server
- per-paneKey last-payload cache + replayCachedPayloadsForPanes() for the
request-driven replay path used after `--connect` reattach (see §5 Path 3)
- clearPaneState(paneKey) for PTY-exit eviction (symmetric with local server)
- buildPtyEnv() / endpoint-file writing for relay-spawned PTYs
Orca's `AgentHookServer` is now a ~200-LoC adapter over the shared listener
that owns the IPC fanout, listener replay, and `ingestRemote(envelope, connId)`
entry point that bypasses the HTTP path for relay-forwarded events.
See docs/design/agent-status-over-ssh.md §3, §8 (commit #2).
Co-authored-by: Orca <help@stably.ai>
* fix(preload): expose connectionId on agentStatus.onSet type
src/preload/index.ts already passes through `connectionId?: string | null`
from main, but the PreloadApi declaration in api-types.ts was missing the
field. Align the type with the runtime contract so renderer call sites
can read connectionId without an `as` cast.
Co-authored-by: Orca <help@stably.ai>
* fix(agent-hooks): harden ingestRemote + relay replay; review-driven cleanup
- ingestRemote: re-run normalizeAgentStatusPayload at trust boundary;
trim+validate connectionId/paneKey/tabId/worktreeId
- relay: preserve source/env/version through replay via sidecar map;
drop sourceFromAgentType fallback that mis-tagged unknown agents
- shared listener: exhaustive switch+never on AgentHookSource dispatch
chains; extractPromptText returns trimmed values; export MAX_PANE_KEY_LEN
- preload: tighten connectionId from optional to required (always sent)
- main IPC: reorder spread so explicit envelope fields win on collision
Co-authored-by: Orca <help@stably.ai>
* chore(docs): drop agent-status-over-ssh design doc from PR
The design RFC was useful for authoring this PR series but doesn't belong
in-tree — keeping it here would freeze line-number references and design
prose against future churn. Folding it into the PR description instead.
Co-authored-by: Orca <help@stably.ai>
* chore(agent-hooks): widen ingestRemote type for env/version (PR2 prep)
Declares `env?: string` and `version?: string` on the `ingestRemote` envelope
parameter so PR2 only needs to add the `warnOnHookEnvOrVersionMismatch`
callsite, not also widen the type. The fields are forwarded verbatim from
the agent CLI POST body on the remote and let Orca's warn-once cross-build
/ dev-vs-prod diagnostics fire identically on remote-sourced events.
Type-only addition; no runtime consumer in this PR.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(telemetry): instrument on_path:false triage on onboarding_agent_picked
Adds path_source and path_failure_reason to onboarding_agent_picked so the
~30% on_path:false rate on dashboard 1562016 can be split between shell
hydration failures and genuinely-not-on-PATH cases before picking a fix.
See docs/agent-on-path-detection.md.
Co-authored-by: Orca <help@stably.ai>
* fix(telemetry): close PathSource compile-time-sync hole
Add `_PathSourceSync` guard mirroring `_PathFailureReasonSync` so adding
a new `PathSource` value to the alias without updating the schema (or
vice versa) fails the build. Without it, drift would silently drop
`onboarding_agent_picked` at the strict validator. Also replace stale
line-number references in docs/agent-on-path-detection.md with named
function/handler references that survive future edits.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Reopening a GitHub issue/PR drawer paid full IPC + `gh` startup latency on
every open. Two changes here:
1. Module-level SWR cache in GitHubItemDialog.tsx keyed by
(repoPath, issueSourcePreference, type, number). Reopening within 30s
paints cached data instantly; older entries paint stale-then-refresh.
Concurrent opens dedupe on a shared in-flight promise. Mutation
handlers invalidate by (repo, type, number); a cache-generation
counter prevents in-flight refetches from resurrecting stale data
after a mid-flight invalidation.
2. Collapsed GraphQL query for issue details replaces 3 serial `gh`
subprocesses (REST issue + REST comments + GraphQL participants) with
one round-trip. Falls back to the legacy fan-out on any GraphQL error
so historical contract is preserved.
Cross-window invalidation rides a new `gh:workItemMutated` IPC broadcast
that skips the originating sender (the source already updated its cache
optimistically — re-broadcasting would race the optimistic write).
`addIssueComment` now takes a `type` so the broadcast scopes correctly
when a PR shares its number with an issue.
Co-authored-by: Orca <help@stably.ai>
Added DeviceRegistry.rotatePendingDevice and threaded a 'rotate' option through the mobile:getPairingQR IPC + preload + MobilePane so explicit Regenerate clicks mint a fresh pending token instead of returning the same one.
Findings addressed:
- [medium] src/main/runtime/device-registry.ts:44-50 — 'Regenerate QR' no longer rotates the token
Rebased onto current main to resolve conflicts.
Co-authored-by: orca-bot <bot@stably.ai>
Fixed Windows titlebar bugs: WindowControls icon now seeded via new ipc isMaximized() getter on mount; CSS height dropped from 42px to 36px; spacer added to floating right-sidebar toggle and RightSidebar header so content isn't occluded.
Findings addressed:
- [medium] src/renderer/src/App.tsx:50-54 — WindowControls maximize icon wrong on startup if window starts maximized
- [medium] src/renderer/src/assets/main.css:434-460 — Window controls 42px tall but titlebar 36px — bottom 6px overlays content
- [low] src/renderer/src/App.tsx:776-803 — Spacer only rendered in workspace-active titlebar branch
Rebased onto current main to drop ~140 unrelated stale-main reverts; only the 6 Fixer-summary files are touched.
Co-authored-by: orca-bot <bot@stably.ai>
Surface worktree creation immediately and reconcile remote base state
asynchronously, emitting drift/conflict events as fetches complete.
Co-authored-by: Orca <help@stably.ai>
* wip
* WIP: Changes before auto-review fixes
Co-authored-by: Orca <help@stably.ai>
* WIP: Changes before auto-review fixes
Co-authored-by: Orca <help@stably.ai>
* WIP: Changes before auto-review fixes
Co-authored-by: Orca <help@stably.ai>
* fix: address auto-review findings (iteration 1)
Co-authored-by: Orca <help@stably.ai>
* fix: address auto-review findings (iteration 2)
Co-authored-by: Orca <help@stably.ai>
* fix: archive review context and improve agent detection on wizard mount
Co-authored-by: Orca <help@stably.ai>
* fix: address CI lint failures and split use-onboarding-flow.ts
Co-authored-by: Orca <help@stably.ai>
* fix: mock ./onboarding in register-core-handlers test
Co-authored-by: Orca <help@stably.ai>
* fix: also toggle light class on documentElement so onboarding e2e theme wait resolves
The onboarding e2e calls waitForFunction(() => classList.contains('dark') || classList.contains('light')) before snapshotting the starting theme. applyDocumentTheme only toggled 'dark', so on a host that resolves system to light the wait timed out (CI Linux headless). Toggle 'light' as the inverse class so consumers can observe the resolved theme symmetrically; Tailwind keys only on 'dark' so styling is unchanged.
Co-authored-by: Orca <help@stably.ai>
* fix: add braces to Landing menu close-on-outside-click handler
oxlint config requires braces for all if statements.
Co-authored-by: Orca <help@stably.ai>
* chore: trigger CI
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Replace the default Windows native title bar with a custom renderer-drawn
titlebar to match the macOS experience:
- Set titleBarStyle:'hidden' on win32 to remove the OS chrome
- Add min/max/close buttons (Fluent-style SVG) fixed to the top-right corner,
rendered last in DOM order so they're never blocked by -webkit-app-region:drag
- Route close through IPC (window:close-requested) so the terminal-running
confirmation guard stays active; minimize/maximize via window:minimize and
window:maximize IPC channels
- Add maximize state sync (window:maximize-changed) so the restore icon shows
correctly
- Add Orca logo + ··· application-menu button on the left in place of the bare
pl-2 spacer; ··· calls Menu.getApplicationMenu().popup() replicating Alt-key
reveal
- Add window-controls-titlebar-spacer to reserve 138px on the right of the
full-width titlebar so content isn't obscured by the overlay
Co-authored-by: Neil Parker <nwparker@anthropic.com>
* feat(feedback): let anonymous users opt in to PR tag + contact
When 'Submit anonymously' is checked the feedback dialog now smoothly
expands to reveal two optional fields:
- GitHub username \u2014 we'll @-mention them on the fix PR
- Email or x.com handle \u2014 we'll reach out when it's fixed
The animation uses a grid-rows 0fr\u21921fr transition so it expands
naturally without measuring DOM. New fields are forwarded through the
IPC + main-process proxy as 'anonymousGithubLogin' and 'anonymousContact'
so the backend can tell self-typed handles apart from verified gh
identity. Backend changes (slack message + route) ship in
orca-marketing-website.
Co-authored-by: Orca <help@stably.ai>
* feat(feedback): split anon contact into email + x.com, tighten copy
Address review:
- move 'all optional' wording up to the top header line
- drop the inline 'we'll tag you on the PR' helper (placeholder says it)
- split the single contact input into two separate inputs (email +
x.com handle), each with its own placeholder
IPC field rename: anonymousContact \u2192 anonymousEmail + anonymousX. Backend
PR is updated to match before either side ships.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(gh-project): diagnose env-shadowed gh tokens in auth errors
`gh auth refresh -s project` silently no-ops when GITHUB_TOKEN/GH_TOKEN
is exported in the user's shell — gh prefers env tokens and refuses to
modify them, exiting 0. Users follow the canned remediation, see no
error, retry, and stay stuck.
Add a one-shot `gh auth status` probe (gh:diagnoseAuth IPC) that:
- Detects env-shadowed credentials and rewrites the fix to `unset
GITHUB_TOKEN` plus a grep to find where it's exported.
- Detects missing gh install, plain missing-scope on a keyring login,
and SAML SSO authorization.
- Surfaces a tailored multi-button error UI in ProjectViewWrapper and
ProjectPicker instead of one canned 'Copy command'.
Co-authored-by: Orca <help@stably.ai>
* fix(gh-project): address review feedback
- Cross-platform shell guidance: PowerShell commands on Windows
(Get-ChildItem Env:, Remove-Item Env:, [Environment]::SetEnvironmentVariable)
via navigator.userAgent platform check.
- Use `window.api.shell.openUrl` for the docs button instead of
`window.open`, matching SidebarToolbar's external-URL pattern.
- Tighten gh auth status parser: accept single-label hostnames and
optional trailing colon; recover host from the inline 'Logged in to
<host>' line so a missed section header never silently drops accounts.
- Add tests for multi-host output and host-recovery fallback.
- Drop dead command/copy locals in ProjectViewWrapper.ErrorState by
short-circuiting the auth-error case before they're computed.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
When a worktree is created from a PR via the source picker, the new
local branch differs from the PR's head ref, so the branch-keyed PR
lookup misses and the worktree card shows no PR strip. Pass the
worktree's linkedPR number through the IPC call and fall back to a
number-based lookup in the main process. Also recover linkedPR from
a PR URL pasted into the workspace name when the user skips the
source picker. PR strip now wraps the whole row as the PR link.
Co-authored-by: Orca <help@stably.ai>
* Fix new workspace composer focus restore
* Unify new workspace source selection
* WIP: selected source pill in smart workspace name field
Co-authored-by: Orca <help@stably.ai>
* fix(new-workspace): truncate source pill so it doesn't expand the dialog
Co-authored-by: Orca <help@stably.ai>
* feat(new-workspace): add open-in-browser button to source pill, fix vertical alignment
Co-authored-by: Orca <help@stably.ai>
* refactor(new-workspace): drop redundant kind suffix, distinct PR/issue icons, tooltips on pill actions
Co-authored-by: Orca <help@stably.ai>
* fix(new-workspace): type linked URL into agent input without auto-submit
Co-authored-by: Orca <help@stably.ai>
* fix(new-workspace): use bracketed-paste for draft URL injection so it actually appears in the agent input
Co-authored-by: Orca <help@stably.ai>
* feat(agents): per-agent draft injection strategy (codex slow paste, pi/opencode type-chars)
Co-authored-by: Orca <help@stably.ai>
* fix(agents): smarter TUI-ready heuristic + bracketed paste for codex/pi/opencode
Replaces per-agent strategy guesswork with a measured readiness check:
title-idle / non-shell-foreground stable for 1.5s / 2.5s minimum floor.
Verified against codex, pi, opencode, claude in a node-pty + xterm-headless
test rig — bracketed paste lands in the input buffer for all four.
Co-authored-by: Orca <help@stably.ai>
* refactor(agents): drop unused per-agent draft strategy abstraction
The TUI-ready heuristic in agent-paste-draft.ts works for every tested
agent (claude/codex/pi/opencode), so the AgentDraftInjectionStrategy
field, type-chars + bracketed-paste-slow code paths, and per-agent
overrides are dead. Keep the `agent` arg on pasteDraftWhenAgentReady
for future per-agent escape hatches without touching every call site.
Co-authored-by: Orca <help@stably.ai>
* feat(agents): skip draft URL injection for copilot + cursor-agent
Both TUIs open with a 'Do you trust this folder?' menu on first launch
that consumes keystrokes as menu input — pasting a URL there either
selects an arbitrary option or quits the session. Mark them with
skipDraftUrlInjection so the workspace still opens cleanly; the user
types/pastes the URL themselves once past the trust menu.
Co-authored-by: Orca <help@stably.ai>
* feat(agents): native --prefill for claude, trust pre-write for cursor/copilot
Replaces the empirical TUI-ready waits with two deterministic mechanisms:
1) `claude --prefill <text>` flag — Claude launches with the URL already in
its input box, no submit. Eliminates the readiness/paste race entirely
for the most common agent.
2) DECSET 2004 (`\x1b[?2004h`) detection on the PTY data stream for every
other agent. That escape is the protocol-level "input layer ready,
accepting bracketed paste" handshake — emitted by claude/codex/pi/
opencode/gemini/cursor-agent/copilot the moment the input box mounts.
We tap it via a sidecar subscription on pty-dispatcher (no interference
with the primary xterm handler) and paste as soon as it lands. The
8s budget is now an upper bound, not a target.
Cursor-agent and Copilot's "Do you trust this folder?" menus are bypassed
by writing the same trust artifacts the CLIs themselves write after the
user accepts:
- Cursor: `~/.cursor/projects/<slug>/.workspace-trusted` (slug = abs path
with leading `/` stripped, remaining `/` → `-`).
- Copilot: append cwd to `trustedFolders` in `~/.copilot/config.json`
(the same array the bundled `addTrustedFolder` writes).
Verified against the cursor-agent CLI bundle (versions/2026.04.17-787b533/
index.js: `_=".workspace-trusted"`) and the @github/copilot 1.0.32 bundle
(`isFolderTrusted` / `addTrustedFolder` both read/write `trustedFolders`).
Both check via realpath() before string-comparing, so the trust preset
canonicalizes too.
skipDraftUrlInjection is dropped — both agents now get the draft URL
paste once the trust menu is pre-resolved.
Tests: 24 passing across tui-agent-startup, agent-trust-presets,
pty-dispatcher routing.
Co-authored-by: Orca <help@stably.ai>
* fix(agents): wait for post-?2004h render burst to settle before paste
OpenCode emits DECSET 2004 at ~500ms during alt-screen setup, then runs
a 1.3s splash render with NO bytes on the PTY, then paints the actual
input box at ~1.85s. Pasting on the bare ?2004h signal lands during the
silent gap and the bytes are dropped.
The fix: take ?2004h as the necessary precondition, then wait for the
TUI's render burst to finish — defined as 1500ms of stream silence
after the most recent post-?2004h byte. This captures both the fast
TUIs (claude/pi/codex emit setup escapes in one burst then go quiet)
and the slow ones (opencode emits, sleeps for the splash, emits again,
then goes quiet).
Verified against opencode/claude/pi in a node-pty rig: paste lands on
the first try with the new strategy. The hard 8s timeout still caps
the wait when an agent fails to launch.
Co-authored-by: Orca <help@stably.ai>
* fix(agents): guard agentTrust IPC so stale preload doesn't crash launch
If the preload bundle is older than the renderer (a real situation in
electron-vite dev because preload changes only apply on full restart,
not HMR), `window.api.agentTrust` is undefined and the launch crashes
with "Cannot read properties of undefined (reading 'markTrusted')"
before the worktree even opens.
Guard the call sites in launch-work-item-direct and useComposerState
to skip the trust pre-write when the IPC isn't exposed, and wrap the
invoke in try/catch so an IPC error never blocks the launch — the user
just sees the trust menu and accepts it manually, same as before this
feature shipped.
Co-authored-by: Orca <help@stably.ai>
* feat(tasks): route 'Use' through the New Workspace dialog instead of yolo-create
The Use CTA on the Tasks page used to create+activate a worktree
synchronously, which surprised users — the worktree appeared in the
sidebar before they had a chance to confirm name / agent / setup. The
unified New Workspace dialog landed in this branch already supports
opening with a linked work item pre-filled (see openComposerForItem /
openComposerForLinearItem), so just route Use through it.
The launchWorkItemDirect helper stays exported for ProjectViewWrapper,
which has its own UX where the immediate-create flow is the right call.
Co-authored-by: Orca <help@stably.ai>
* test(agents): include `agent` field in autohand startup-plan assertion
Merging main brought in the Autohand Code agent test (PR #1382), which
predated this branch's addition of `agent` to AgentStartupPlan.
Aligning the assertion fixes the lone CI test failure on this PR.
Co-authored-by: Orca <help@stably.ai>
* refactor(agents): drop unused expectedProcess arg + snapshot sidecar set
Two minor follow-ups from self-review:
1. `pasteDraftWhenAgentReady` no longer reads `expectedProcess` — readiness
is gated on DECSET 2004 alone now, not on PTY foreground process. Drop
it from the signature and from the two callers (launch-work-item-direct,
new-workspace).
2. The pty-dispatcher's sidecar fan-out iterates the live Set, which is
safe against deleting the current element but not against a watcher
that synchronously subscribes a sibling. Snapshot via Array.from
before the loop. Cheap (Set is tiny) and removes the latent footgun.
No behavior change.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): match the unified smart-name input's new placeholder
The CreateFromTab refactor in this branch replaced the separate "Workspace
name" Input with a single SmartWorkspaceNameField whose default-mode
placeholder is "Type a name, #1234, branch, GitHub or Linear URL". The
worktree-create e2e test was still anchoring on the old "Workspace name"
text and could not find the input.
Update the placeholder regex to match the new copy. Free-form text typed
into smart mode is treated as a workspace name by submitQuick — same
contract the test used before.
Verified locally: targeted e2e passes in 2.2s.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Add support for custom desktop notification sounds
* perf(notifications): cache custom sound + restart-on-play
Avoids re-reading the configured audio file (up to 10MB) from disk and
re-transferring it over IPC on every notification. Adds a path-only
resolver so repeated dispatches with an unchanged sound skip the heavy
load entirely.
For burst handling, follows the VS Code AccessibilitySignalService /
GNOME canberra pattern: one shared HTMLAudioElement per sound, restarted
from t=0 on each play, with an in-flight guard that drops new plays
while the sound is still ringing. This self-dedupes by the sound's own
duration without any magic time constant — distinct sounds remain free
to overlap. The Test button passes force: true so an explicit user
action always plays through.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* feat(cli): add browser tab profile controls
* feat(cli): add tab profile automation primitives
* refactor(cli): narrow tab profile automation scope
* chore: retrigger PR checks
* review: harden tab profile automation CLI
- Wait for tab re-registration after browser.tabSetProfile so a follow-up tab list --show-profile reads the new sessionProfileId from BrowserManager instead of the stale one from the previous webview
- Wait for tab registration after browser.tabProfileClone, matching browser.tabCreate, so the cloned browserPageId is operable when the CLI returns
- Short-circuit browser.tabSetProfile when the tab is already on the requested profile so we do not tear down and remount the webview for a no-op switch
- Switch TabShow.worktree from OptionalPlainString to OptionalString to match every other tab schema; empty --worktree should fall back to the active worktree, not pass through as the empty string
- Add max-lines disable to browser.test.ts (file grew past 300 lines after adding the new tab-profile and tab-show tests)
* review: fix useIpcEvents test setup for tab profile API
CI failure: useIpcEvents.test.ts threw at module load with TypeError: window.addEventListener is not a function. The chain: the rebased useIpcEvents.ts imports destroyPersistentWebview from webview-registry, which calls window.addEventListener at module load. The test stubs window via vi.stubGlobal as a plain object without addEventListener, so the typeof window check passes but the call throws.
- webview-registry.ts: tighten the module-load guard to also check that window.addEventListener is callable, so importing this module from a non-DOM-ish test env (vitest node env with stubbed window) does not throw at module load
- useIpcEvents.test.ts: add the new onRequestTabSetProfile and replyTabSetProfile stubs to all 8 window.api.ui mocks so the new IPC subscription registered by useIpcEvents resolves
* review: restore profile CRUD lost during rebase onto 1397-merged main
The rebase brought commit 3242aa27 (refactor: narrow tab profile automation scope) onto a main that already had the lifecycle CRUD from 1397. The refactor commit removes BrowserProfileList/Create/Delete types, runtime methods, RPC registrations and schemas, plus the help/specs entries, because those were the precursor versions in commit 1 of this branch. Post-rebase those removals land on the hardened versions inherited from main, breaking 1397.
Restore:
- runtime-types.ts: BrowserSessionProfile import; ProfileList/Create/Delete result types
- orca-runtime.ts: ProfileList/Create/Delete result type imports; browserProfileList/Create/Delete methods
- browser-core.ts: ProfileCreate, ProfileDelete schema imports; browser.profileList/profileCreate/profileDelete RPC registrations
- browser-schemas.ts: ProfileCreate, ProfileDelete zod schemas
- help.ts: list/create/delete subcommand lines under Browser Automation
- specs/browser-basic.ts: list/create/delete spec entries
---------
Co-authored-by: Nikolatesla-lj <Nikolatesla-lj@users.noreply.github.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>