SSH workspaces previously rendered as Globe in some places and Wifi in
others. Globe implies web/internet and Wifi implies wireless network
strength — neither reads as "remote machine". Use Server/ServerOff
across every SSH context for a consistent mental model. Globe remains
in true browser/web surfaces (browser pane, address bar, tabs, clone
from URL, Browser settings pane).
Co-authored-by: Orca <help@stably.ai>
- Surface top 3 worktrees under a RECENT WORKTREES header on empty query
when there are ≥4 worktrees, with a WORKTREES header for the rest
- Support "repo/branch" composite queries in the palette search, with
highlight ranges on both segments
- Add placeholder hint for the new composite query
Co-authored-by: Orca <help@stably.ai>
Derive keyboard cycle order from an all-expanded layout so collapsed groups don't cause worktrees to be skipped, and uncollapse the All header on reveal when groupBy is 'none'.
Co-authored-by: Orca <help@stably.ai>
- fix(monaco): disable semantic validation in diff viewer
Monaco's sandboxed TS worker cannot resolve cross-file imports, cascading
into a long tail of false semantic diagnostics beyond the previously-ignored
codes. Replace the growing ignore list with noSemanticValidation; keep
syntax validation for genuine parse errors.
Co-authored-by: Orca <help@stably.ai>
* fix(pty): honor Windows shell selection on the daemon path + shell-specific icons
The "+" menu picker and the Settings → Default Shell preference both set a
shellOverride, but the daemon-backed PTY path never forwarded it. Every
Windows terminal ended up as PowerShell (or cmd.exe via COMSPEC) no matter
what the user picked.
Fix:
- Thread shellOverride through daemon-pty-adapter → createOrAttach RPC →
terminal-host → pty-subprocess, and actually resolve the correct launch
args (chcp for CMD, $PROFILE dot-sourcing for PowerShell, /mnt/<drive>
cwd translation for WSL) on the daemon path.
- Extract the Windows shell-args decision into a shared helper
(resolveWindowsShellLaunchArgs) so LocalPtyProvider and the daemon
spawner cannot drift again.
- In ipc/pty.ts, fall back to the persisted terminalWindowsShell setting
when no per-tab override is sent, so the daemon path honors the user's
Default Shell preference the same way LocalPtyProvider already did.
UI polish on the "+" dropdown and tab strip:
- Drop the "Default" tag next to the top entry (takes too much space).
- Rename "Command Prompt" → "CMD Prompt" to fit next to the Ctrl+T hint.
- Replace the generic terminal glyph with brand-style icons per shell
(ShellIcon). Both the "+" menu and the per-tab strip use the same
icon set so a WSL tab is visually distinct from a PowerShell tab.
Co-authored-by: Orca <help@stably.ai>
* fix(daemon): add oxlint-disable max-lines to daemon-server
CI counts 312 non-blank/non-comment lines for daemon-server.ts after the
shellOverride plumbing was added, exceeding the 300-line .ts override.
The file is a single RPC route table; splitting it would leak the host
reference across modules for no readability win, so add a scoped disable
with the rationale.
Co-authored-by: Orca <help@stably.ai>
* chore(lint): disable max-lines on pre-existing 312-line tabs-hydration test
Unrelated to the shell-selection fix, but surfaced on PR CI: the file is
right at oxlint's 300-line .ts ceiling; each case in the table is a
minimal fixture + assertion, so splitting it across files would scatter
closely related regression coverage for a single reducer.
Co-authored-by: Orca <help@stably.ai>
* chore(lint): disable max-lines on shared text-search module
The shared text-search module exceeds oxlint's 300-line .ts ceiling. It
is the single source of truth for rg arg construction, rg --json parsing,
git-grep submatch parsing, and relative-path normalization shared between
the local main process and the SSH relay. Re-splitting it would
re-introduce the maxBuffer divergence the design doc explicitly calls out.
Also reverts two speculative oxlint-disable directives on daemon-server
and tabs-hydration.test — those files were not the offender; CI's error
message elides the file path but running the lint locally against the
PR-merge commit pinpointed text-search.ts.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Extracts rg and git-grep search logic into src/shared/text-search.ts so
the local main and SSH relay paths stop reinventing arg construction,
JSON parsing, submatch regex, and accumulator/truncation semantics.
Fixes silent truncation in the relay: searchWithRg used execFile with
a 50MB maxBuffer cap that rg --json easily exceeds on large repos,
dropping matches with no error surfaced to the user. The relay now
streams via spawn, matching the local path.
See docs/design/share-text-search.md for full rationale.
Co-authored-by: Orca <help@stably.ai>
- Remove queryDetailsExpanded toggle; always show scope filters in the
Search tab since it is a secondary destination used specifically for
scoped search.
- Left-truncate pre-match text in MatchResultRow so the highlight stays
visible at narrow sidebar widths instead of being pushed off the right
edge. Mirrors VS Code's lcut behavior.
Co-authored-by: Orca <help@stably.ai>
Replace the flaky Radix submenu on the "+" New-tab dropdown with a flat
list of all available shells (PowerShell, Command Prompt, WSL when
present). The configured default shell is pinned to the top, labeled
"Default", and carries the Ctrl+T shortcut hint so users can see at a
glance which shell Ctrl+T will open.
Co-authored-by: Orca <help@stably.ai>
* chore: update .gitignore to include stackdump and .serena, enhance pre-commit script
* fix(win32): resolve EPERM on userData writes and batch-file spawn failures
Three Windows-specific issues prevented Orca from running correctly on
machines where Chromium resets the userData DACL during startup:
1. **EPERM on userData writes** — Chromium's BrowserWindow constructor calls
SetNamedSecurityInfo on the userData folder with a Protected DACL. When
propagated to child directories the ACEs carry the Inherit-Only flag,
meaning they apply to children-of-children but NOT to the directories
themselves. Any file write inside codex-runtime-home, agent-hooks, or
similar subdirectories fails with EPERM.
Fix: grant an explicit Full Control ACE (OI)(CI)(F) on userData and all
existing children before BrowserWindow is created (icacls /T /C).
Explicit ACEs survive future DACL propagation from the parent. Per-write
EPERM retries in fs-utils and installer-utils serve as the backstop for
directories created after startup.
2. **Batch-file spawn failures** — resolveCodexCommand() can return a .cmd
or .bat path (e.g. codex.cmd installed via npm). Node's spawn() cannot
execute batch scripts directly without shell:true, but shell:true with an
args array triggers DEP0190 because args are concatenated rather than
escaped. Both service.ts and codex-fetcher.ts were affected.
Fix: detect .cmd/.bat paths and route through cmd.exe /c explicitly,
which is equivalent to what shell:true does internally but avoids the
deprecation warning and arg-escaping hazard.
3. **Native dep rebuild failure** — electron-builder install-app-deps does
not expose the ignoreModules option. On Windows dev machines without the
full VC++ / Python toolchain, cpu-features (an optional dep of ssh2) fails
to build with node-gyp, aborting the entire postinstall step.
Fix: replace electron-builder install-app-deps with a thin wrapper script
(scripts/rebuild-native-deps.mjs) that calls @electron/rebuild's JS API
directly with ignoreModules: ['cpu-features'] on Windows. ssh2 detects
the missing native module and falls back to pure-JS automatically.
Refactoring: extract shared win32-utils.ts with getIcaclsExePath(),
getCmdExePath(), isWindowsBatchScript(), isPermissionError(), grantDirAcl(),
and getSpawnArgsForWindows() to eliminate five instances of duplicated
SystemRoot path construction and two near-identical EPERM retry blocks.
Reduce startup icacls calls from three sequential blocking /T invocations
to one, removing up to 20 s of potential startup delay.
* fix(win32): address review feedback on ACL and spawn helpers
- Fall back to SID via `whoami /user` when `USERNAME` is unset so
`grantDirAcl` works under services, CI, and hardened envs instead of
silently no-op'ing.
- Use a 60s timeout for recursive `icacls /T` walks; the 10s cap could
starve on large userData trees and silently fail the startup grant.
- Pass `windowsHide: true` to `icacls` and the cmd.exe-routed Codex
spawns so no console window flashes in the packaged GUI app.
- Add `/d` to `cmd.exe /c` invocations to disable AutoRun registry
commands — safer default for background spawns.
- Drop unused `createRequire`/`require` from rebuild-native-deps.mjs.
- Add `@electron/rebuild` as an explicit devDependency; relying on the
electron-builder transitive was brittle under pnpm.
- Fix two misleading "Re-enable inheritance" comments that describe
behavior opposite to what the code actually does (explicit ACL grant).
- Add unit tests for `isWindowsBatchScript`, `getSpawnArgsForWindows`,
and `isPermissionError` to lock in Windows batch detection + cmd.exe
routing.
Co-authored-by: Orca <help@stably.ai>
* fix(win32): unify PTY spawn through /d and document cmd.exe safety
- fetchViaPty now uses getCmdExePath() and /d /c, matching the rest of
the codebase instead of hand-rolling 'cmd.exe' + ['/c', ...].
- getSpawnArgsForWindows gains a SAFETY note: when the .cmd/.bat branch
is taken, cmd.exe re-parses the combined command line, so callers
must only pass trusted/literal args.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): stop terminal scroll from drifting upward during resize
xterm's terminal.resize() natively preserves viewportY across reflows (see
scroll-reflow.test.ts "reference: undisturbed"). The capture/restore dance
in the default safeFit path was fighting that — findLineByContent's 40-char
prefix match collides across duplicate scrollback lines (prompts, bullets,
borders) in long Claude sessions, so the viewport anchored to a slightly
wrong line each frame and the error compounded upward.
- Remove capture/restore from the default safeFit and fitAllPanesInternal
paths. Just fitAddon.fit(), matching how Superset and VSCode handle this.
- Keep pendingSplitScrollState and pendingDragScrollState branches intact —
split reparenting and divider drags still need those.
- Sidebar open/close is an instantaneous width change, so dispatch
SYNC_FIT_PANES_EVENT from a useLayoutEffect and let the terminal fit
synchronously pre-paint, eliminating the one-frame "old cols, new
container width" flash users saw when fits were deferred to a
ResizeObserver rAF.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): remove scroll-restore delay from in-pane divider drag
The divider drag was still using lockDragScroll/unlockDragScroll, which set
pendingDragScrollState on each affected pane so safeFit would
captureScrollState → fit → findLineByContent → scrollToLine on every frame.
findLineByContent scans the whole scrollback for a 40-char prefix match —
expensive on long Claude sessions and the cause of the drag feeling laggy.
Now that the previous commit established xterm's native viewportY
preservation is enough (see scroll-reflow.test.ts "reference: undisturbed"),
the divider doesn't need the snapshot either. Drop the lock calls from
onPointerDown/onPointerUp and the double-click equalize path; the default
safeFit path (plain fitAddon.fit()) handles it.
lockDragScroll / unlockDragScroll remain in the codebase for now — removing
them is a wider interface cleanup through pane-tree-ops and pane-drag-reorder
that can land separately.
Co-authored-by: Orca <help@stably.ai>
* refactor(pane-manager): drop unused drag-scroll lock machinery
The previous two commits showed xterm preserves viewportY natively across
resize, so safeFit's default path is just fit() and the divider no longer
sets pendingDragScrollState. That leaves a chain of dead plumbing:
- pane-drag-scroll.ts (lockDragScroll / unlockDragScroll) — nothing invokes
these anymore. Delete the file.
- ManagedPaneInternal.pendingDragScrollState — no writers, no readers. Drop
the field and the "null" initializer in createPaneDOM and test fixtures.
- pendingDragScrollState branches in safeFit and fitAllPanesInternal —
unreachable. Remove them. fitAllPanesInternal collapses to a simple loop
of safeFit calls.
- lockDragScroll / unlockDragScroll callback entries in TreeOpsCallbacks,
DragReorderCallbacks, DividerCallbacks, and PaneManager's callback
builders — drop.
- pane-lifecycle.ts WebGL context-loss handler: the three-branch
pendingSplit / pendingDrag / default capture-restore dance collapses to a
plain fit() + refresh(), relying on xterm's native viewportY preservation
and scheduleSplitScrollRestore for the split-reparent case.
- restoreScrollState import in pane-lifecycle.ts — now unused.
- Stale "Why: we previously locked..." comment block in pane-divider —
remove; the new code is self-explanatory and the motivation lives in the
pane-tree-ops safeFit comment.
pendingSplitScrollState stays: it's still load-bearing in
pane-split-scroll.ts where it gates the early double-rAF restore during
wrapInSplit DOM reparenting.
Net: -123 lines. No behavior change — verified by the full renderer test
suite (1116/1116).
Co-authored-by: Orca <help@stably.ai>
* docs: tighten sidebar-toggle comment in App.tsx
The comment referenced 'restoring its scroll' but after the drag-scroll
lock cleanup the fit no longer restores anything — xterm preserves
viewportY natively. Reword to match actual behavior.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): preserve scroll when exiting expanded pane mode
Cmd+Enter enter/exit expanded mode (toggleExpandPane) runs refreshPaneSizes,
which still had the same content-hash capture/restore dance that the rest
of the resize paths were cleared of: captureScrollState → fit →
findLineByContent → scrollToLine. On long Claude sessions the 40-char
prefix match collides with duplicate scrollback lines (prompts, bullets,
borders) and jumps to the wrong line, leaving the viewport scrolled up
after exiting expanded mode.
xterm preserves viewportY natively across resize (see scroll-reflow.test.ts
"reference: undisturbed"), so a bare fit() is enough — matching every other
resize path in this codebase.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(settings): add Ghostty config import
Add safe one-shot Ghostty import with preview and success summary,
and probe documented Ghostty config paths before applying changes.
Refs #958
* chore(git): ignore atl artifacts
* feat(settings): map font-weight, cursor-blink and focus-follows-mouse from Ghostty
Adds three safe direct-mapping Ghostty keys that have clear equivalents
in GlobalSettings: font-weight, cursor-style-blink, and focus-follows-mouse.
Refs #958
* feat(settings): expand Ghostty import to support colors, opacity and option-as-alt
- Add TerminalColorOverrides type grouping 21 optional xterm ITheme fields
- Add terminalBackgroundOpacity, terminalPanePaddingColor, terminalPaddingBalance
to GlobalSettings
- Extend parser to collect repeated keys as string[] (needed for palette lines)
- Map background-opacity, background, foreground, cursor-color,
selection-background/foreground, palette (0-15), window-padding-color,
window-padding-balance, macos-option-as-alt in mapper
- Merge terminalColorOverrides into xterm ITheme at theme resolution; apply
opacity as rgba() with allowTransparency enabled
- Accept hex colors with or without leading # (Ghostty omits it)
- Fix preview diff to use deep equality for object values so already-applied
color overrides no longer reappear on next import
* feat(settings): support background-blur-radius and window-padding-color extend in Ghostty import
- Map background-blur-radius > 0 to windowBackgroundBlur: true; apply
vibrancy on macOS and backgroundMaterial acrylic on Windows at window
creation (blur requires restart — no hot-reload IPC exists)
- Accept window-padding-color = extend/background as valid Ghostty values;
both map to default Orca padding behavior (undefined field) instead of
landing in unsupportedKeys
- Split mapper.test.ts into domain-scoped describes to stay under 300-line limit
* feat(settings): add Window section to Terminal settings panel
Expose terminalBackgroundOpacity, windowBackgroundBlur, terminalPaddingBalance,
terminalPanePaddingColor, and terminalColorOverrides in the settings UI so
imported Ghostty values can be viewed and changed manually.
- New TerminalWindowSection component (extracted from TerminalPane to stay
under the 400-line limit)
- Collapsible color overrides sub-section with ColorField for all 21 xterm
ITheme fields grouped as base, ANSI normal, and ANSI bright
- Reset button clears all color overrides at once
- Window blur toggle shows restart-required note (blur applies at window
creation, no hot-reload IPC exists)
- Search entries added for all new controls
* feat(settings): expand Ghostty import with scrollback, padding, divider, cursor and word-chars keys
Map 9 additional Ghostty keys to Orca settings:
- split-divider-color → terminalDividerColorDark + terminalDividerColorLight
(single value applies to both; Ghostty has no dark/light distinction)
- unfocused-split-opacity → terminalInactivePaneOpacity (direct float 0-1)
- scrollback-limit → terminalScrollbackLimit; applied to xterm scrollback option
- window-padding-x / window-padding-y → terminalPaddingX/Y; applied as CSS
vars --pane-padding-x / --pane-padding-y in terminal.css
- cursor-text → terminalColorOverrides.cursorAccent (xterm ITheme field)
- bold-color → terminalColorOverrides.bold (persisted; xterm ITheme has no
bold field yet — stored for future xterm upgrade)
- cursor-opacity → terminalCursorOpacity; blended into cursor rgba at theme
resolution time
- selection-word-chars → terminalWordSeparator; applied to xterm wordSeparator
- mouse-hide-while-typing → terminalMouseHideWhileTyping field added; renderer
application deferred (needs per-pane disposable + global mousemove listener)
* feat(settings): expose new Ghostty-imported settings in Terminal Settings UI
- Window section: scrollback limit, horizontal/vertical padding, hide mouse
while typing toggle, cursor text and bold color in Color Overrides
- Cursor section: cursor opacity NumberField
- Advanced section: word separators text input
- Search entries added for all new controls
- terminalDividerColorDark/Light and terminalInactivePaneOpacity skipped —
already present in Theme and Pane Styling sections respectively
* feat(terminal): implement mouse-hide-while-typing per pane
Register terminal.onData → cursor:none and mousemove → restore, scoped to
the pane container element. Uses the existing IDisposable per-pane pattern
(same as selectionDisposablesRef). Cleans up on pane close and effect teardown.
* refactor(settings): address ghostty import code review findings
- Centralize GhosttyImportPreview type in shared/types (remove duplicate from mapper)
- Fix parser to strip inline comments without breaking hex color values (#1a1a1a)
- Extract HEX_COLOR_RE to shared/color-validation to avoid duplication
- Remove redundant Number.isNaN checks after Number.isFinite (4 sites)
- Replace unsafe catch-all assignment with explicit font-family branch
- Migrate 280-line if-chain in mapGhosttyToOrca to FIELD_PARSERS registry
- Add human-readable setting labels in GhosttyImportModal via setting-labels map
- Add clarifying comment in index.ts re JSON.stringify undefined behavior
* fix(settings): harden ghostty import from judgment-day review
- Surface readFile errors in GhosttyImportPreview.error instead of
showing misleading 'No config found' on permission denied
- Guard handleApply against double-apply when already applied
- Strip surrounding quotes from parsed config values (font-family)
- Return null from palette handler when all entries fail validation
- Inform user when background-blur-radius radius is not preserved
- Add valuesEqual key-order stability via stableStringify
- Normalize hex colors to #-prefixed format across all color mappers
- Reject blank values before numeric parsing (Number('') === 0 trap)
- Remove selection-word-chars mapping (inverted xterm semantics)
- Guard window-padding-x/y against negative integers
- Reactive mouse-hide-while-typing on existing panes when setting toggles
- Merge terminalColorOverrides on import instead of replacing
* fix(ghostty): drop broken imports, tighten parsing, prompt restart for blur
Review found three high-impact issues in the Ghostty import: scrollback-limit
semantics are inverted/rescaled (Ghostty is bytes with 0=unlimited, xterm is
rows with 0=disabled), and window-padding-color + window-padding-balance set
CSS custom properties (--pane-padding-color, --pane-padding-balance) that
have no consuming rule anywhere in the tree — so users confirming "changes"
to those keys would see nothing happen.
Because none of the three keys have a safe mapping today, drop them from the
import and remove the dead UI controls + GlobalSettings fields + CSS var
plumbing. The mapper now lists them as unsupportedKeys alongside the
existing window-decoration / keybind / custom-shader entries.
Other fixes in the same review:
- allowTransparency now clears when background-opacity returns to 1 (prior
code only ever set it to true, leaving a stale flag with measurable render
cost).
- background-blur-radius = 0 no longer emits a misleading "radius value not
preserved" note (0 cleanly maps to blur=false with no radius to lose).
- Add a 1 MB size cap on the config read so a pathological or symlinked file
cannot OOM the main process.
- Make handleApply async and surface IPC errors inline in the modal instead
of flipping straight to "Import complete" on failure.
- Type settings.previewGhosttyImport as Promise<GhosttyImportPreview> in
preload so shape drift is caught at compile time.
- Make stableStringify recursive so future nested settings round-trip
cleanly through valuesEqual.
- Restrict window-padding-x/y and background-blur-radius to decimal ints;
prior code accepted exponent notation (1e10 sails through Number.isInteger)
and would have landed absurd values in the store.
- Window blur now shows a "Restart required" banner with a Restart now
button when the setting differs from the mount-time snapshot, mirroring
the ExperimentalPane daemon pattern. Blur only applies at BrowserWindow
creation on macOS/Windows.
Co-authored-by: Orca <help@stably.ai>
* feat(settings): move Ghostty import trigger to Terminal section header
Per review feedback: the "Import from Ghostty" row was taking its own slot
in the Terminal settings list alongside real configuration sections. Move
the trigger into the Terminal section's header (upper-right corner) as a
headerAction, next to the Terminal heading — it's a one-shot action, not a
setting.
- SettingsSection gains an optional `headerAction` slot rendered to the
right of the section title/description.
- The useGhosttyImport hook is lifted from TerminalPane into Settings.tsx
so the section header button (owned by Settings.tsx) and the modal
(still rendered inside TerminalPane) share one state instance.
- TerminalPane drops its own "Import" section + the TERMINAL_GHOSTTY_IMPORT
search entry group is no longer referenced there.
- Button carries the official Ghostty mark as a 16x16 icon so it reads
clearly as a cross-app import even before users parse the label.
useGhosttyImport now accepts `GlobalSettings | null` so the parent can call
it above the pre-load spinner guard without violating hook ordering; the
apply path no-ops until settings arrive. Related test file updated to
pass the new `ghostty` prop and to assert the trigger is *not* rendered
inside TerminalPane anymore.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix(tabs): repair mixed tab shortcut switching
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(typecheck): unblock tc:web project checks
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(tabs): split group/fallback id matching into separate branches
Why: matching both `tabId` and `id` in one findIndex predicate mixed two
identifier domains and risked a pathological collision between a tab's
backing entity id and another tab's unified id. Keep the group-path
(strict tabId match) and the fallback-path (backing-id match) in
separate branches.
Also clarify the comment on the dual `setActiveFile` + `activateTab`
write so future readers know why both calls are needed for split-group
disambiguation.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
Reorders tabs so Pull requests is first and becomes the default
selection (falling back to Branches for remote SSH repos where PRs
are disabled). Also auto-prefills the worktree note with
"PR #<n> — <title>" when a PR is picked, guarded by a ref so
user-typed notes are never clobbered.
Co-authored-by: Orca <help@stably.ai>
* feat(sidebar): improve PR checks and comments
Co-authored-by: Orca <help@stably.ai>
* fix(sidebar): classify bot comments via GitHub user.type instead of login heuristic
Third-party review bots like qodo-ai-reviewer, coderabbitai, and sonarcloud
don't follow the [bot] suffix or "bot"/"automation" substring convention, so
the regex-based detector misclassified them as human. Plumb REST
`user.type === 'Bot'` and GraphQL `author.__typename === 'Bot'` through to the
renderer as an authoritative isBot flag; fall back to the login heuristic only
when the data source can't report it.
Co-authored-by: Orca <help@stably.ai>
* fix(sidebar): allowlist AI review services that sign in as User accounts
qodo-ai-reviewer, coderabbitai, codium-ai and similar third-party review
services register as regular GitHub user accounts, so REST `user.type` is
"User" and their logins contain no "bot"/"automation" tokens. The previous
fix relied on the GitHub-reported type, which fails for these. Add an
explicit substring allowlist of known automation services so they still
land in the Bots tab.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>