mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
fix(run-actions): a split pane in the run tab can't end the run; autoRunAfterSetup fires on first open when the host ran setup in an environment build
- Command-finished events carry the pane's leaf; a run learns its pane while its tab has one, ignores other panes, and Stop interrupts the run's own pane. - Workspaces whose setup ran in the environment build (hooks report environmentProvisioned) start the autoRunAfterSetup action the first time they are opened, once per workspace.
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
type WorktreeRunActions
|
||||
} from '@/lib/run-actions/run-action-config'
|
||||
import { startRunAction, stopRunAction } from '@/lib/run-actions/run-action-controller'
|
||||
import { autoRunProvisionedWorkspaceOnce } from '@/lib/run-actions/run-action-events'
|
||||
import { useWorktreeRunActionRuns, type RunActionRun } from '@/lib/run-actions/run-actions-store'
|
||||
import type { WorkspacePort } from '../../../../shared/workspace-ports'
|
||||
import { RunActionButtonView } from './RunActionButtonView'
|
||||
@@ -79,6 +80,11 @@ export function RunActionButton({ worktreeId }: { worktreeId: string }): React.J
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[config, worktreeId, runs]
|
||||
)
|
||||
useEffect(() => {
|
||||
if (config?.setupProvisioned) {
|
||||
void autoRunProvisionedWorkspaceOnce(worktreeId, config)
|
||||
}
|
||||
}, [config, worktreeId])
|
||||
if (!config || !primary) {
|
||||
return null
|
||||
}
|
||||
|
||||
+7
-1
@@ -1,4 +1,5 @@
|
||||
/** Keep store-level command status while parked; foreground confirmation and input-intent inference remain with mounted panes. */
|
||||
import { parsePaneKey } from '../../../../../../shared/workspace-layout/stable-pane-id'
|
||||
import type { AgentStatusEntry } from '../../../../../../shared/agent-status/contracts/agent-status-types'
|
||||
import { resolvePaneAgentOwner } from '../../../../../../shared/agent-status/identity/pane-agent-owner'
|
||||
import { parseAppSshPtyId } from '../../../../../../shared/ssh-pty-id'
|
||||
@@ -173,7 +174,12 @@ export function createParkedTerminalCommandStatusPolicy(options: {
|
||||
return
|
||||
}
|
||||
// Commands can change HEAD/index; nudge git UI immediately rather than wait for polling.
|
||||
dispatchTerminalCommandFinishedEvent(worktreeId, bestEffortExitCode, tabId)
|
||||
dispatchTerminalCommandFinishedEvent(
|
||||
worktreeId,
|
||||
bestEffortExitCode,
|
||||
tabId,
|
||||
parsePaneKey(paneKey)?.leafId
|
||||
)
|
||||
void retireForegroundAgentUnlessConfirmed()
|
||||
// Mirror SSH’s unprobed same-turn drop; local PTYs require mounted foreground confirmation against nested-shell 133;D.
|
||||
if (parseAppSshPtyId(ptyId) === null) {
|
||||
|
||||
+3
-1
@@ -1,3 +1,4 @@
|
||||
import { parsePaneKey } from '../../../../../../shared/workspace-layout/stable-pane-id'
|
||||
import { agentTypeToIconAgent } from '../../../../../../shared/agent-status/presentation/agent-type-icon-agent'
|
||||
import { dismissRetainedProgramResult } from '@/lib/program-status/result-retention'
|
||||
import { isRetainedProgramResult } from '@/lib/program-status/row'
|
||||
@@ -217,7 +218,8 @@ export function installPaneAgentIdentity(session: ConnectPanePtySession): void {
|
||||
dispatchTerminalCommandFinishedEvent(
|
||||
session.deps.worktreeId,
|
||||
bestEffortExitCode,
|
||||
session.deps.tabId
|
||||
session.deps.tabId,
|
||||
parsePaneKey(session.cacheKey)?.leafId
|
||||
)
|
||||
const state = useAppStore.getState()
|
||||
const entry = state.agentStatusByPaneKey[session.cacheKey]
|
||||
|
||||
@@ -6,13 +6,16 @@ export type TerminalCommandFinishedEventDetail = {
|
||||
exitCode: number | null
|
||||
/** The pane's tab, when the emitter knows it; consumers tracking one tab's command need it. */
|
||||
tabId?: string
|
||||
/** The pane's stable leaf: a split's commands are not the tab's first pane's. */
|
||||
leafId?: string
|
||||
}
|
||||
|
||||
// Window events let decoupled consumers observe per-pane OSC completion without accessing terminal internals.
|
||||
export function dispatchTerminalCommandFinishedEvent(
|
||||
worktreeId: string,
|
||||
exitCode: number | null,
|
||||
tabId?: string
|
||||
tabId?: string,
|
||||
leafId?: string
|
||||
): void {
|
||||
// Why: unit tests and non-DOM renderer shims may expose only the preload API.
|
||||
if (typeof window.dispatchEvent !== 'function') {
|
||||
@@ -21,7 +24,7 @@ export function dispatchTerminalCommandFinishedEvent(
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<TerminalCommandFinishedEventDetail>(ORCA_TERMINAL_COMMAND_FINISHED_EVENT, {
|
||||
detail: { worktreeId, exitCode, ...(tabId ? { tabId } : {}) }
|
||||
detail: { worktreeId, exitCode, ...(tabId ? { tabId } : {}), ...(leafId ? { leafId } : {}) }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ export type WorktreeRunActions = {
|
||||
scripts: OrcaRunScripts
|
||||
/** Ids defined by the shared orca.yaml (they need the user's approval before running). */
|
||||
sharedIds: ReadonlySet<string>
|
||||
/** The host prepared the checkout itself (setup ran in an environment build, not a Setup tab). */
|
||||
setupProvisioned?: true
|
||||
}
|
||||
|
||||
export type RunActionTarget = {
|
||||
@@ -96,13 +98,20 @@ export async function loadWorktreeRunActions(
|
||||
}
|
||||
const repo = state.repos.find((candidate) => candidate.id === target.repoId)
|
||||
let shared: OrcaHooks['scripts'] | undefined
|
||||
let setupProvisioned = false
|
||||
try {
|
||||
const result = await checkRuntimeHooks(target.hostId, target.repoId, options)
|
||||
shared = result.status === 'error' ? undefined : (result.hooks?.scripts ?? undefined)
|
||||
setupProvisioned = result.environmentProvisioned === true
|
||||
} catch {
|
||||
shared = undefined
|
||||
}
|
||||
return mergeRunActions(shared, repo?.hookSettings?.scripts, target.isLocal ? 'local' : 'remote')
|
||||
const merged = mergeRunActions(
|
||||
shared,
|
||||
repo?.hookSettings?.scripts,
|
||||
target.isLocal ? 'local' : 'remote'
|
||||
)
|
||||
return setupProvisioned ? { ...merged, setupProvisioned: true } : merged
|
||||
}
|
||||
|
||||
/** The action the Run button and shortcut act on: the last one still running here, else the default. */
|
||||
|
||||
@@ -10,6 +10,7 @@ type MockState = {
|
||||
{ id: string; entityId: string; contentType: string; groupId: string }[]
|
||||
>
|
||||
ptyIdsByTabId: Record<string, string[]>
|
||||
terminalLayoutsByTabId: Record<string, unknown>
|
||||
createTab: ReturnType<typeof vi.fn>
|
||||
closeTab: ReturnType<typeof vi.fn>
|
||||
setTabCustomTitle: ReturnType<typeof vi.fn>
|
||||
@@ -69,7 +70,8 @@ vi.stubGlobal('window', new EventTarget())
|
||||
const { mergeRunActions, loadWorktreeRunActions } = await import('./run-action-config')
|
||||
const { startRunAction, stopRunAction, relaunchInterruptedRunActions, armRunActionRelaunch } =
|
||||
await import('./run-action-controller')
|
||||
const { installRunActionEvents, armAutoRunAfterSetup } = await import('./run-action-events')
|
||||
const { installRunActionEvents, armAutoRunAfterSetup, autoRunProvisionedWorkspaceOnce } =
|
||||
await import('./run-action-events')
|
||||
const { getRunActionRun, resetRunActionRunsForTests } = await import('./run-actions-store')
|
||||
const { dispatchTerminalCommandFinishedEvent } =
|
||||
await import('@/hooks/terminal-command-finished-event')
|
||||
@@ -90,6 +92,7 @@ function makeState(): MockState {
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: {},
|
||||
ptyIdsByTabId: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
createTab: vi.fn((worktreeId: string) => {
|
||||
const tab = { id: `tab-${++nextTab}` }
|
||||
state.tabsByWorktree[worktreeId] = [...(state.tabsByWorktree[worktreeId] ?? []), tab]
|
||||
@@ -322,6 +325,73 @@ describe('run actions', () => {
|
||||
expect(await startRunAction(WT_B, web, config([web]))).toBe(false)
|
||||
})
|
||||
|
||||
it('a command finishing in a split of the run tab does not end the run', async () => {
|
||||
await startRunAction(WT_A, web, config([web]))
|
||||
const { tabId } = getRunActionRun(WT_A, 'web')!
|
||||
const L1 = '11111111-1111-4111-8111-111111111111'
|
||||
const L2 = '22222222-2222-4222-8222-222222222222'
|
||||
const notifyLayouts = (): void => {
|
||||
for (const listener of mocks.subscribers) {
|
||||
listener(state, { tabsByWorktree: state.tabsByWorktree, terminalLayoutsByTabId: {} })
|
||||
}
|
||||
}
|
||||
state.terminalLayoutsByTabId = { [tabId]: { root: { type: 'leaf', leafId: L1 } } }
|
||||
notifyLayouts()
|
||||
expect(getRunActionRun(WT_A, 'web')?.leafId).toBe(L1)
|
||||
state.terminalLayoutsByTabId = {
|
||||
[tabId]: {
|
||||
root: {
|
||||
type: 'split',
|
||||
direction: 'vertical',
|
||||
first: { type: 'leaf', leafId: L1 },
|
||||
second: { type: 'leaf', leafId: L2 }
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyLayouts()
|
||||
dispatchTerminalCommandFinishedEvent(WT_A, 0, tabId, L2)
|
||||
expect(getRunActionRun(WT_A, 'web')?.status).toBe('running')
|
||||
dispatchTerminalCommandFinishedEvent(WT_A, 1, tabId, L1)
|
||||
expect(getRunActionRun(WT_A, 'web')).toMatchObject({ status: 'failed', exitCode: 1 })
|
||||
})
|
||||
|
||||
it('ignores a pane it cannot attribute while the tab has several panes', async () => {
|
||||
await startRunAction(WT_A, web, config([web]))
|
||||
const { tabId } = getRunActionRun(WT_A, 'web')!
|
||||
state.terminalLayoutsByTabId = {
|
||||
[tabId]: {
|
||||
root: {
|
||||
type: 'split',
|
||||
direction: 'vertical',
|
||||
first: { type: 'leaf', leafId: '11111111-1111-4111-8111-111111111111' },
|
||||
second: { type: 'leaf', leafId: '22222222-2222-4222-8222-222222222222' }
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatchTerminalCommandFinishedEvent(WT_A, 0, tabId, '22222222-2222-4222-8222-222222222222')
|
||||
expect(getRunActionRun(WT_A, 'web')?.status).toBe('running')
|
||||
})
|
||||
|
||||
it('auto-runs once on first open when the host ran setup in the environment build', async () => {
|
||||
const storage = new Map<string, string>()
|
||||
vi.stubGlobal('localStorage', {
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => void storage.set(key, value)
|
||||
})
|
||||
try {
|
||||
const provisioned = { ...config([web]), setupProvisioned: true as const }
|
||||
provisioned.scripts = { ...provisioned.scripts, autoRunAfterSetup: true }
|
||||
expect(await autoRunProvisionedWorkspaceOnce(WT_A, provisioned)).toBe(true)
|
||||
expect(getRunActionRun(WT_A, 'web')?.status).toBe('running')
|
||||
dispatchTerminalCommandFinishedEvent(WT_A, 0, getRunActionRun(WT_A, 'web')!.tabId)
|
||||
expect(await autoRunProvisionedWorkspaceOnce(WT_A, provisioned)).toBe(false)
|
||||
const notProvisioned = { ...provisioned, setupProvisioned: undefined }
|
||||
expect(await autoRunProvisionedWorkspaceOnce(WT_B, notProvisioned)).toBe(false)
|
||||
} finally {
|
||||
vi.stubGlobal('localStorage', undefined)
|
||||
}
|
||||
})
|
||||
|
||||
it('auto-runs the default action once setup succeeds, not when it fails', async () => {
|
||||
mocks.checkRuntimeHooks.mockResolvedValue({
|
||||
status: 'ok',
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
type RunActionTarget,
|
||||
type WorktreeRunActions
|
||||
} from './run-action-config'
|
||||
import { findLiveRunTabId, sameRunTab } from './run-action-tabs'
|
||||
import { findLiveRunTabId, runPanePtyId, sameRunTab } from './run-action-tabs'
|
||||
|
||||
/** Conductor's grace period: an interrupted dev server gets this long before its shell is closed. */
|
||||
export const RUN_ACTION_STOP_GRACE_MS = 5_000
|
||||
@@ -101,8 +101,7 @@ async function stopRunActionOnce(worktreeId: string, actionId: string): Promise<
|
||||
return
|
||||
}
|
||||
const state = useAppStore.getState()
|
||||
const liveTabId = findLiveRunTabId(state, worktreeId, run.tabId)
|
||||
const ptyId = liveTabId ? state.ptyIdsByTabId[liveTabId]?.[0] : undefined
|
||||
const ptyId = runPanePtyId(state, worktreeId, run.tabId, run.leafId)
|
||||
if (!ptyId) {
|
||||
deleteRunActionRun(worktreeId, actionId)
|
||||
return
|
||||
|
||||
@@ -13,10 +13,15 @@ import {
|
||||
findRunActionRunByTab,
|
||||
getRunActionRun,
|
||||
listRunActionRuns,
|
||||
patchRunActionRun
|
||||
patchRunActionRun,
|
||||
type RunActionRun
|
||||
} from './run-actions-store'
|
||||
import { loadWorktreeRunActions, pickPrimaryRunAction } from './run-action-config'
|
||||
import { findLiveRunTabId, sameRunTab } from './run-action-tabs'
|
||||
import {
|
||||
loadWorktreeRunActions,
|
||||
pickPrimaryRunAction,
|
||||
type WorktreeRunActions
|
||||
} from './run-action-config'
|
||||
import { findLiveRunTabId, runPanePtyId, runTabLeafIds, sameRunTab } from './run-action-tabs'
|
||||
import {
|
||||
isRunActionRelaunchArmed,
|
||||
notifyRunActionStopped,
|
||||
@@ -59,6 +64,10 @@ function handleCommandFinished(detail: TerminalCommandFinishedEventDetail): void
|
||||
if (!run || (run.status !== 'running' && run.status !== 'stopping')) {
|
||||
return
|
||||
}
|
||||
if (!isRunPane(run, detail.leafId)) {
|
||||
// A command finished in a split of the run's tab, not the action itself.
|
||||
return
|
||||
}
|
||||
// OSC 133;D may omit the exit code; without one the run is only known to have ended.
|
||||
const status =
|
||||
run.status === 'stopping'
|
||||
@@ -70,6 +79,31 @@ function handleCommandFinished(detail: TerminalCommandFinishedEventDetail): void
|
||||
notifyRunActionStopped(tabId)
|
||||
}
|
||||
|
||||
function isRunPane(run: RunActionRun, leafId: string | undefined): boolean {
|
||||
if (!leafId) {
|
||||
return true
|
||||
}
|
||||
if (run.leafId) {
|
||||
return run.leafId === leafId
|
||||
}
|
||||
// The run's pane isn't known yet: only a tab with one pane proves which pane finished.
|
||||
const leaves = runTabLeafIds(useAppStore.getState(), run.worktreeId, run.tabId)
|
||||
return leaves.length <= 1
|
||||
}
|
||||
|
||||
/** A run learns its pane while its tab still has exactly one, before any split exists. */
|
||||
function learnRunPanes(state: AppState): void {
|
||||
for (const run of listRunActionRuns()) {
|
||||
if (run.leafId) {
|
||||
continue
|
||||
}
|
||||
const leaves = runTabLeafIds(state, run.worktreeId, run.tabId)
|
||||
if (leaves.length === 1) {
|
||||
patchRunActionRun(run.worktreeId, run.actionId, { leafId: leaves[0] })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pruneClosedRunTabs(state: AppState): void {
|
||||
for (const run of listRunActionRuns()) {
|
||||
// A restarting host rebuilds its tab list; the relaunch owns these runs until it settles.
|
||||
@@ -91,6 +125,42 @@ function pruneClosedRunTabs(state: AppState): void {
|
||||
}
|
||||
}
|
||||
|
||||
const AUTO_RAN_KEY = 'orca.runActions.autoRan.v1'
|
||||
|
||||
function readAutoRan(): string[] {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(globalThis.localStorage?.getItem(AUTO_RAN_KEY) ?? '[]')
|
||||
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string') : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The host ran setup while building the environment, so no Setup tab will ever finish here:
|
||||
* honor `autoRunAfterSetup` the first time the workspace is opened, once per workspace.
|
||||
*/
|
||||
export async function autoRunProvisionedWorkspaceOnce(
|
||||
worktreeId: string,
|
||||
config: WorktreeRunActions
|
||||
): Promise<boolean> {
|
||||
const action = config.setupProvisioned ? pickAutoRunAction(config.scripts) : null
|
||||
const autoRan = readAutoRan()
|
||||
if (!action || autoRan.includes(worktreeId) || getRunActionRun(worktreeId, action.id)) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
// Recorded before starting, so a second open (or window) can't start it twice.
|
||||
globalThis.localStorage?.setItem(
|
||||
AUTO_RAN_KEY,
|
||||
JSON.stringify([...autoRan, worktreeId].slice(-500))
|
||||
)
|
||||
} catch {
|
||||
// Best effort: without storage this still runs once per window session.
|
||||
}
|
||||
return startRunAction(worktreeId, action, config, { activate: false })
|
||||
}
|
||||
|
||||
/** Follows run terminals: their exits update status, and closing one forgets its run. */
|
||||
export function installRunActionEvents(): () => void {
|
||||
const onFinished = (event: Event): void => {
|
||||
@@ -104,6 +174,9 @@ export function installRunActionEvents(): () => void {
|
||||
let pruneTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const installedAt = Date.now()
|
||||
const unsubscribe = useAppStore.subscribe((state, previous) => {
|
||||
if (state.terminalLayoutsByTabId !== previous.terminalLayoutsByTabId) {
|
||||
learnRunPanes(state)
|
||||
}
|
||||
if (state.tabsByWorktree === previous.tabsByWorktree || pruneTimer) {
|
||||
return
|
||||
}
|
||||
@@ -136,8 +209,7 @@ const STARTUP_PRUNE_GRACE_MS = 15_000
|
||||
async function reconcileLocalRunsAfterStartup(): Promise<void> {
|
||||
const state = useAppStore.getState()
|
||||
for (const run of listRunActionRuns()) {
|
||||
const liveTabId = findLiveRunTabId(state, run.worktreeId, run.tabId)
|
||||
const ptyId = liveTabId ? state.ptyIdsByTabId[liveTabId]?.[0] : undefined
|
||||
const ptyId = runPanePtyId(state, run.worktreeId, run.tabId, run.leafId)
|
||||
if (run.status !== 'running' || !ptyId || getRemoteRuntimePtyOwner(ptyId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AppState } from '@/store/types'
|
||||
import type { TerminalPaneLayoutNode } from '../../../../shared/workspace-layout/terminal-tab-types'
|
||||
|
||||
/** A remote host adopts a tab the window created under `web-terminal-<id>`; both name one tab. */
|
||||
const MIRRORED_TAB_PREFIX = 'web-terminal-'
|
||||
@@ -21,3 +22,43 @@ export function findLiveRunTabId(
|
||||
(state.tabsByWorktree[worktreeId] ?? []).find((tab) => sameRunTab(tab.id, tabId))?.id ?? null
|
||||
)
|
||||
}
|
||||
|
||||
function collectLeafIds(node: TerminalPaneLayoutNode | null | undefined, out: string[]): string[] {
|
||||
if (!node) {
|
||||
return out
|
||||
}
|
||||
if (node.type === 'leaf') {
|
||||
out.push(node.leafId)
|
||||
} else {
|
||||
collectLeafIds(node.first, out)
|
||||
collectLeafIds(node.second, out)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Every pane of the run's tab, in layout order. */
|
||||
export function runTabLeafIds(
|
||||
state: Pick<AppState, 'tabsByWorktree' | 'terminalLayoutsByTabId'>,
|
||||
worktreeId: string,
|
||||
tabId: string
|
||||
): string[] {
|
||||
const liveTabId = findLiveRunTabId(state, worktreeId, tabId)
|
||||
return liveTabId ? collectLeafIds(state.terminalLayoutsByTabId?.[liveTabId]?.root, []) : []
|
||||
}
|
||||
|
||||
/** The PTY of the run's own pane, falling back to the tab's first PTY. */
|
||||
export function runPanePtyId(
|
||||
state: Pick<AppState, 'tabsByWorktree' | 'terminalLayoutsByTabId' | 'ptyIdsByTabId'>,
|
||||
worktreeId: string,
|
||||
tabId: string,
|
||||
leafId: string | undefined
|
||||
): string | undefined {
|
||||
const liveTabId = findLiveRunTabId(state, worktreeId, tabId)
|
||||
if (!liveTabId) {
|
||||
return undefined
|
||||
}
|
||||
const byLeaf = leafId
|
||||
? state.terminalLayoutsByTabId?.[liveTabId]?.ptyIdsByLeafId?.[leafId]
|
||||
: undefined
|
||||
return byLeaf ?? state.ptyIdsByTabId[liveTabId]?.[0]
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ export type RunActionRun = {
|
||||
actionName: string
|
||||
/** The terminal tab showing this run's output. */
|
||||
tabId: string
|
||||
/** The tab's pane running the action (learned once the pane exists); splits are other panes. */
|
||||
leafId?: string
|
||||
status: RunActionStatus
|
||||
exitCode: number | null
|
||||
startedAt: number
|
||||
@@ -40,7 +42,8 @@ function readRun(value: unknown): RunActionRun | null {
|
||||
return null
|
||||
}
|
||||
const run: Record<string, unknown> = { ...value }
|
||||
const { worktreeId, repoId, actionId, actionName, tabId, status, exitCode, startedAt } = run
|
||||
const { worktreeId, repoId, actionId, actionName, tabId, status, exitCode, startedAt, leafId } =
|
||||
run
|
||||
if (
|
||||
typeof worktreeId !== 'string' ||
|
||||
typeof repoId !== 'string' ||
|
||||
@@ -60,7 +63,8 @@ function readRun(value: unknown): RunActionRun | null {
|
||||
tabId,
|
||||
status: STATUSES.find((known) => known === status) ?? 'exited',
|
||||
exitCode: typeof exitCode === 'number' ? exitCode : null,
|
||||
startedAt
|
||||
startedAt,
|
||||
...(typeof leafId === 'string' ? { leafId } : {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +128,7 @@ export function setRunActionRun(run: RunActionRun): void {
|
||||
export function patchRunActionRun(
|
||||
worktreeId: string,
|
||||
actionId: string,
|
||||
patch: Partial<Pick<RunActionRun, 'status' | 'exitCode'>>
|
||||
patch: Partial<Pick<RunActionRun, 'status' | 'exitCode' | 'leafId'>>
|
||||
): void {
|
||||
const current = runs[worktreeId]?.[actionId]
|
||||
if (!current) {
|
||||
|
||||
Reference in New Issue
Block a user