mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
* 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>
482 lines
15 KiB
TypeScript
482 lines
15 KiB
TypeScript
/* eslint-disable max-lines -- Why: all GitHub IPC handlers stay co-located so
|
|
the repo-path validation, preference-threading, and stats wiring patterns are
|
|
reviewable as one surface. Splitting by feature area would risk drifting
|
|
validation/gate conventions across handler files. */
|
|
import { ipcMain } from 'electron'
|
|
import { resolve } from 'path'
|
|
import type { Repo, GitHubIssueUpdate } from '../../shared/types'
|
|
import type { Store } from '../persistence'
|
|
import type { StatsCollector } from '../stats/collector'
|
|
import {
|
|
getPRForBranch,
|
|
getIssue,
|
|
getRepoSlug,
|
|
listIssues,
|
|
listWorkItems,
|
|
countWorkItems,
|
|
getWorkItem,
|
|
getWorkItemByOwnerRepo,
|
|
createIssue,
|
|
updateIssue,
|
|
addIssueComment,
|
|
listLabels,
|
|
listAssignableUsers,
|
|
getAuthenticatedViewer,
|
|
getPRChecks,
|
|
getPRComments,
|
|
resolveReviewThread,
|
|
addPRReviewComment,
|
|
addPRReviewCommentReply,
|
|
updatePRTitle,
|
|
mergePR,
|
|
checkOrcaStarred,
|
|
starOrca
|
|
} from '../github/client'
|
|
import { getWorkItemDetails, getPRFileContents } from '../github/work-item-details'
|
|
import { getRateLimit } from '../github/rate-limit'
|
|
import type { GitHubPRFile } from '../../shared/types'
|
|
import { dispatchWorkItem, type WorkItemArgs } from './github-work-item-args'
|
|
import {
|
|
getProjectViewTable,
|
|
listAccessibleProjects,
|
|
resolveProjectRef,
|
|
listProjectViews,
|
|
getWorkItemDetailsBySlug,
|
|
updateProjectItemFieldValue,
|
|
clearProjectItemFieldValue,
|
|
updateIssueBySlug,
|
|
updatePullRequestBySlug,
|
|
addIssueCommentBySlug,
|
|
updateIssueCommentBySlug,
|
|
deleteIssueCommentBySlug,
|
|
listLabelsBySlug,
|
|
listAssignableUsersBySlug,
|
|
listIssueTypesBySlug,
|
|
updateIssueTypeBySlug
|
|
} from '../github/project-view'
|
|
import type {
|
|
AddIssueCommentBySlugArgs,
|
|
ClearProjectItemFieldArgs,
|
|
DeleteIssueCommentBySlugArgs,
|
|
GetProjectViewTableArgs,
|
|
ListAssignableUsersBySlugArgs,
|
|
ListIssueTypesBySlugArgs,
|
|
ListLabelsBySlugArgs,
|
|
ListProjectViewsArgs,
|
|
ProjectWorkItemDetailsBySlugArgs,
|
|
ResolveProjectRefArgs,
|
|
UpdateIssueBySlugArgs,
|
|
UpdateIssueCommentBySlugArgs,
|
|
UpdateIssueTypeBySlugArgs,
|
|
UpdateProjectItemFieldArgs,
|
|
UpdatePullRequestBySlugArgs
|
|
} from '../../shared/github-project-types'
|
|
|
|
// Why: returns the full Repo object instead of just the path string so that
|
|
// callers have access to repo.id for stat tracking and other context.
|
|
function assertRegisteredRepo(repoPath: string, store: Store): Repo {
|
|
const resolvedRepoPath = resolve(repoPath)
|
|
const repo = store.getRepos().find((r) => resolve(r.path) === resolvedRepoPath)
|
|
if (!repo) {
|
|
throw new Error('Access denied: unknown repository path')
|
|
}
|
|
return repo
|
|
}
|
|
|
|
export function registerGitHubHandlers(store: Store, stats: StatsCollector): void {
|
|
ipcMain.handle('gh:prForBranch', async (_event, args: { repoPath: string; branch: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
const pr = await getPRForBranch(repo.path, args.branch)
|
|
// Emit pr_created when a PR is first detected for a branch.
|
|
// Why here: the renderer polls gh:prForBranch to check PR status per worktree.
|
|
// This captures PRs opened from any workflow (Orca UI, gh CLI, github.com).
|
|
if (pr && !stats.hasCountedPR(pr.url)) {
|
|
stats.record({
|
|
type: 'pr_created',
|
|
at: Date.now(),
|
|
repoId: repo.id,
|
|
meta: { prNumber: pr.number, prUrl: pr.url }
|
|
})
|
|
}
|
|
return pr
|
|
})
|
|
|
|
ipcMain.handle('gh:issue', (_event, args: { repoPath: string; number: number }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return getIssue(repo.path, args.number)
|
|
})
|
|
|
|
ipcMain.handle('gh:listIssues', (_event, args: { repoPath: string; limit?: number }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
// Why: listIssues now returns { items, error? }. The IPC handler unwraps to
|
|
// the items array for the existing contract; feature 1's UI consumes the
|
|
// richer envelope through `gh:listWorkItems` instead.
|
|
return listIssues(repo.path, args.limit, repo.issueSourcePreference).then((r) => r.items)
|
|
})
|
|
|
|
ipcMain.handle(
|
|
'gh:createIssue',
|
|
(_event, args: { repoPath: string; title: string; body: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return createIssue(repo.path, args.title, args.body, repo.issueSourcePreference)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:listWorkItems',
|
|
(_event, args: { repoPath: string; limit?: number; query?: string; before?: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return listWorkItems(
|
|
repo.path,
|
|
args.limit,
|
|
args.query,
|
|
args.before,
|
|
repo.issueSourcePreference
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle('gh:countWorkItems', (_event, args: { repoPath: string; query?: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return countWorkItems(repo.path, args.query, repo.issueSourcePreference)
|
|
})
|
|
|
|
ipcMain.handle('gh:workItem', (_event, args: WorkItemArgs) =>
|
|
dispatchWorkItem(args, assertRegisteredRepo(args.repoPath, store).path, getWorkItem)
|
|
)
|
|
ipcMain.handle(
|
|
'gh:workItemByOwnerRepo',
|
|
(
|
|
_event,
|
|
args: {
|
|
repoPath: string
|
|
owner: string
|
|
repo: string
|
|
number: number
|
|
type: 'issue' | 'pr'
|
|
}
|
|
) =>
|
|
getWorkItemByOwnerRepo(
|
|
assertRegisteredRepo(args.repoPath, store).path,
|
|
{ owner: args.owner, repo: args.repo },
|
|
args.number,
|
|
args.type
|
|
)
|
|
)
|
|
ipcMain.handle('gh:workItemDetails', (_event, args: WorkItemArgs) =>
|
|
dispatchWorkItem(args, assertRegisteredRepo(args.repoPath, store).path, getWorkItemDetails)
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:prFileContents',
|
|
(
|
|
_event,
|
|
args: {
|
|
repoPath: string
|
|
prNumber: number
|
|
path: string
|
|
oldPath?: string
|
|
status: GitHubPRFile['status']
|
|
headSha: string
|
|
baseSha: string
|
|
}
|
|
) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return getPRFileContents({
|
|
repoPath: repo.path,
|
|
prNumber: args.prNumber,
|
|
path: args.path,
|
|
oldPath: args.oldPath,
|
|
status: args.status,
|
|
headSha: args.headSha,
|
|
baseSha: args.baseSha
|
|
})
|
|
}
|
|
)
|
|
|
|
ipcMain.handle('gh:repoSlug', (_event, args: { repoPath: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return getRepoSlug(repo.path)
|
|
})
|
|
|
|
ipcMain.handle(
|
|
'gh:prChecks',
|
|
(
|
|
_event,
|
|
args: {
|
|
repoPath: string
|
|
prNumber: number
|
|
headSha?: string
|
|
noCache?: boolean
|
|
}
|
|
) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return getPRChecks(repo.path, args.prNumber, args.headSha, {
|
|
noCache: args.noCache
|
|
})
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:prComments',
|
|
(_event, args: { repoPath: string; prNumber: number; noCache?: boolean }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return getPRComments(repo.path, args.prNumber, { noCache: args.noCache })
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:resolveReviewThread',
|
|
(_event, args: { repoPath: string; threadId: string; resolve: boolean }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return resolveReviewThread(repo.path, args.threadId, args.resolve)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:addPRReviewCommentReply',
|
|
(
|
|
_event,
|
|
args: {
|
|
repoPath: string
|
|
prNumber: number
|
|
commentId: number
|
|
body: string
|
|
threadId?: string
|
|
path?: string
|
|
line?: number
|
|
}
|
|
) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
if (
|
|
typeof args.prNumber !== 'number' ||
|
|
!Number.isInteger(args.prNumber) ||
|
|
args.prNumber < 1
|
|
) {
|
|
return { ok: false, error: 'Invalid PR number' }
|
|
}
|
|
if (
|
|
typeof args.commentId !== 'number' ||
|
|
!Number.isInteger(args.commentId) ||
|
|
args.commentId < 1
|
|
) {
|
|
return { ok: false, error: 'Invalid comment ID' }
|
|
}
|
|
if (!args.body?.trim()) {
|
|
return { ok: false, error: 'Comment body required' }
|
|
}
|
|
return addPRReviewCommentReply(
|
|
repo.path,
|
|
args.prNumber,
|
|
args.commentId,
|
|
args.body.trim(),
|
|
args.threadId,
|
|
args.path,
|
|
args.line
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:addPRReviewComment',
|
|
(
|
|
_event,
|
|
args: {
|
|
repoPath: string
|
|
prNumber: number
|
|
commitId: string
|
|
path: string
|
|
line: number
|
|
startLine?: number
|
|
body: string
|
|
}
|
|
) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
if (
|
|
typeof args.prNumber !== 'number' ||
|
|
!Number.isInteger(args.prNumber) ||
|
|
args.prNumber < 1
|
|
) {
|
|
return { ok: false, error: 'Invalid PR number' }
|
|
}
|
|
if (typeof args.line !== 'number' || !Number.isInteger(args.line) || args.line < 1) {
|
|
return { ok: false, error: 'Invalid line number' }
|
|
}
|
|
if (
|
|
args.startLine !== undefined &&
|
|
(typeof args.startLine !== 'number' ||
|
|
!Number.isInteger(args.startLine) ||
|
|
args.startLine < 1 ||
|
|
args.startLine > args.line)
|
|
) {
|
|
return { ok: false, error: 'Invalid start line' }
|
|
}
|
|
if (!args.commitId?.trim()) {
|
|
return { ok: false, error: 'Missing PR head SHA' }
|
|
}
|
|
if (!args.path?.trim()) {
|
|
return { ok: false, error: 'File path required' }
|
|
}
|
|
if (!args.body?.trim()) {
|
|
return { ok: false, error: 'Comment body required' }
|
|
}
|
|
return addPRReviewComment({
|
|
repoPath: repo.path,
|
|
prNumber: args.prNumber,
|
|
commitId: args.commitId.trim(),
|
|
path: args.path,
|
|
line: args.line,
|
|
startLine: args.startLine,
|
|
body: args.body.trim()
|
|
})
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:updatePRTitle',
|
|
(_event, args: { repoPath: string; prNumber: number; title: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return updatePRTitle(repo.path, args.prNumber, args.title)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:mergePR',
|
|
(
|
|
_event,
|
|
args: { repoPath: string; prNumber: number; method?: 'merge' | 'squash' | 'rebase' }
|
|
) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return mergePR(repo.path, args.prNumber, args.method)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:updateIssue',
|
|
(_event, args: { repoPath: string; number: number; updates: GitHubIssueUpdate }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
if (typeof args.number !== 'number' || !Number.isInteger(args.number) || args.number < 1) {
|
|
return { ok: false, error: 'Invalid issue number' }
|
|
}
|
|
if (!args.updates || typeof args.updates !== 'object') {
|
|
return { ok: false, error: 'Updates object is required' }
|
|
}
|
|
return updateIssue(repo.path, args.number, args.updates)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:addIssueComment',
|
|
(_event, args: { repoPath: string; number: number; body: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
if (typeof args.number !== 'number' || !Number.isInteger(args.number) || args.number < 1) {
|
|
return { ok: false, error: 'Invalid issue number' }
|
|
}
|
|
if (!args.body?.trim()) {
|
|
return { ok: false, error: 'Comment body required' }
|
|
}
|
|
return addIssueComment(repo.path, args.number, args.body.trim())
|
|
}
|
|
)
|
|
|
|
ipcMain.handle('gh:listLabels', (_event, args: { repoPath: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return listLabels(repo.path, repo.issueSourcePreference)
|
|
})
|
|
|
|
ipcMain.handle('gh:listAssignableUsers', (_event, args: { repoPath: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return listAssignableUsers(repo.path, repo.issueSourcePreference)
|
|
})
|
|
|
|
// Star operations target the Orca repo itself — no repoPath validation needed
|
|
ipcMain.handle('gh:viewer', () => getAuthenticatedViewer())
|
|
ipcMain.handle('gh:checkOrcaStarred', () => checkOrcaStarred())
|
|
ipcMain.handle('gh:starOrca', () => starOrca())
|
|
|
|
// Why: `rate_limit` is exempt from GitHub's rate-limit accounting, so
|
|
// polling is cheap. A 30s in-process cache still avoids the gh subprocess
|
|
// cost on every render — see getRateLimit for the ttl rationale. Force
|
|
// parameter lets the renderer bust the cache after a known-expensive op
|
|
// (e.g. post-ProjectPicker discovery) without waiting out the ttl.
|
|
ipcMain.handle('gh:rateLimit', (_event, args?: { force?: boolean }) =>
|
|
getRateLimit(args?.force ? { force: true } : undefined)
|
|
)
|
|
|
|
// ── GitHub ProjectV2 view handlers ─────────────────────────────────
|
|
// Why: registered unconditionally so enabling the experimental flag at
|
|
// runtime takes effect without a restart. The renderer gates entry points.
|
|
// Handlers never throw across IPC — every failure mode resolves through the
|
|
// GitHubProjectViewError envelope.
|
|
|
|
ipcMain.handle('gh:listAccessibleProjects', () => listAccessibleProjects())
|
|
|
|
ipcMain.handle('gh:resolveProjectRef', (_event, args: ResolveProjectRefArgs) =>
|
|
resolveProjectRef(args)
|
|
)
|
|
|
|
ipcMain.handle('gh:listProjectViews', (_event, args: ListProjectViewsArgs) =>
|
|
listProjectViews(args)
|
|
)
|
|
|
|
ipcMain.handle('gh:getProjectViewTable', (_event, args: GetProjectViewTableArgs) =>
|
|
getProjectViewTable(args)
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:projectWorkItemDetailsBySlug',
|
|
(_event, args: ProjectWorkItemDetailsBySlugArgs) => getWorkItemDetailsBySlug(args)
|
|
)
|
|
|
|
ipcMain.handle('gh:updateProjectItemField', (_event, args: UpdateProjectItemFieldArgs) =>
|
|
updateProjectItemFieldValue(args)
|
|
)
|
|
|
|
ipcMain.handle('gh:clearProjectItemField', (_event, args: ClearProjectItemFieldArgs) =>
|
|
clearProjectItemFieldValue(args)
|
|
)
|
|
|
|
ipcMain.handle('gh:updateIssueBySlug', (_event, args: UpdateIssueBySlugArgs) =>
|
|
updateIssueBySlug(args)
|
|
)
|
|
|
|
ipcMain.handle('gh:updatePullRequestBySlug', (_event, args: UpdatePullRequestBySlugArgs) =>
|
|
updatePullRequestBySlug(args)
|
|
)
|
|
|
|
ipcMain.handle('gh:addIssueCommentBySlug', (_event, args: AddIssueCommentBySlugArgs) =>
|
|
addIssueCommentBySlug(args)
|
|
)
|
|
|
|
ipcMain.handle('gh:updateIssueCommentBySlug', (_event, args: UpdateIssueCommentBySlugArgs) =>
|
|
updateIssueCommentBySlug(args)
|
|
)
|
|
|
|
ipcMain.handle('gh:deleteIssueCommentBySlug', (_event, args: DeleteIssueCommentBySlugArgs) =>
|
|
deleteIssueCommentBySlug(args)
|
|
)
|
|
|
|
ipcMain.handle('gh:listLabelsBySlug', (_event, args: ListLabelsBySlugArgs) =>
|
|
listLabelsBySlug(args)
|
|
)
|
|
|
|
ipcMain.handle('gh:listAssignableUsersBySlug', (_event, args: ListAssignableUsersBySlugArgs) =>
|
|
listAssignableUsersBySlug(args)
|
|
)
|
|
|
|
ipcMain.handle('gh:listIssueTypesBySlug', (_event, args: ListIssueTypesBySlugArgs) =>
|
|
listIssueTypesBySlug(args)
|
|
)
|
|
|
|
ipcMain.handle('gh:updateIssueTypeBySlug', (_event, args: UpdateIssueTypeBySlugArgs) =>
|
|
updateIssueTypeBySlug(args)
|
|
)
|
|
|
|
// Why: issue-source preference writes go through the generic `repos:update`
|
|
// IPC (extended in this PR to accept `issueSourcePreference`). Routing
|
|
// through the same channel keeps a single write path, guarantees the
|
|
// `repos:changed` broadcast is emitted, and avoids two channels racing to
|
|
// persist the same field with different validation and eviction semantics.
|
|
// Reads piggyback on the `Repo` record already delivered by `repos:list`.
|
|
}
|