perf(app): faster workspace creation
This commit is contained in:
@@ -48,6 +48,7 @@ import { useProviders } from "@/hooks/use-providers"
|
|||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { Identifier } from "@/utils/id"
|
import { Identifier } from "@/utils/id"
|
||||||
|
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -61,6 +62,13 @@ import { base64Encode } from "@opencode-ai/util/encode"
|
|||||||
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
||||||
const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
|
const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
|
||||||
|
|
||||||
|
type PendingPrompt = {
|
||||||
|
abort: AbortController
|
||||||
|
cleanup: VoidFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = new Map<string, PendingPrompt>()
|
||||||
|
|
||||||
interface PromptInputProps {
|
interface PromptInputProps {
|
||||||
class?: string
|
class?: string
|
||||||
ref?: (el: HTMLDivElement) => void
|
ref?: (el: HTMLDivElement) => void
|
||||||
@@ -846,12 +854,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
setStore("popover", null)
|
setStore("popover", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const abort = () =>
|
const abort = () => {
|
||||||
sdk.client.session
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return Promise.resolve()
|
||||||
|
const queued = pending.get(sessionID)
|
||||||
|
if (queued) {
|
||||||
|
queued.abort.abort()
|
||||||
|
queued.cleanup()
|
||||||
|
pending.delete(sessionID)
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
return sdk.client.session
|
||||||
.abort({
|
.abort({
|
||||||
sessionID: params.id!,
|
sessionID,
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
|
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
|
||||||
const text = prompt
|
const text = prompt
|
||||||
@@ -1111,6 +1129,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
WorktreeState.pending(createdWorktree.directory)
|
||||||
sessionDirectory = createdWorktree.directory
|
sessionDirectory = createdWorktree.directory
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1409,20 +1428,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
clearInput()
|
clearInput()
|
||||||
addOptimisticMessage()
|
addOptimisticMessage()
|
||||||
|
|
||||||
client.session
|
const waitForWorktree = async () => {
|
||||||
.prompt({
|
const worktree = WorktreeState.get(sessionDirectory)
|
||||||
sessionID: session.id,
|
if (!worktree || worktree.status !== "pending") return true
|
||||||
agent,
|
|
||||||
model,
|
setSyncStore("session_status", session.id, { type: "busy" })
|
||||||
messageID,
|
|
||||||
parts: requestParts,
|
const controller = new AbortController()
|
||||||
variant,
|
|
||||||
})
|
const cleanup = () => {
|
||||||
.catch((err) => {
|
setSyncStore("session_status", session.id, { type: "idle" })
|
||||||
showToast({
|
|
||||||
title: language.t("prompt.toast.promptSendFailed.title"),
|
|
||||||
description: errorMessage(err),
|
|
||||||
})
|
|
||||||
removeOptimisticMessage()
|
removeOptimisticMessage()
|
||||||
for (const item of commentItems) {
|
for (const item of commentItems) {
|
||||||
prompt.context.add({
|
prompt.context.add({
|
||||||
@@ -1435,7 +1450,71 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
restoreInput()
|
restoreInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
pending.set(session.id, { abort: controller, cleanup })
|
||||||
|
|
||||||
|
const abort = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
resolve({ status: "failed", message: "aborted" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controller.signal.addEventListener(
|
||||||
|
"abort",
|
||||||
|
() => {
|
||||||
|
resolve({ status: "failed", message: "aborted" })
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const timeoutMs = 5 * 60 * 1000
|
||||||
|
const timeout = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({ status: "failed", message: "Workspace is still preparing" })
|
||||||
|
}, timeoutMs)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await Promise.race([WorktreeState.wait(sessionDirectory), abort, timeout])
|
||||||
|
pending.delete(session.id)
|
||||||
|
if (controller.signal.aborted) return false
|
||||||
|
if (result.status === "failed") throw new Error(result.message)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const send = async () => {
|
||||||
|
const ok = await waitForWorktree()
|
||||||
|
if (!ok) return
|
||||||
|
await client.session.prompt({
|
||||||
|
sessionID: session.id,
|
||||||
|
agent,
|
||||||
|
model,
|
||||||
|
messageID,
|
||||||
|
parts: requestParts,
|
||||||
|
variant,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
void send().catch((err) => {
|
||||||
|
pending.delete(session.id)
|
||||||
|
setSyncStore("session_status", session.id, { type: "idle" })
|
||||||
|
showToast({
|
||||||
|
title: language.t("prompt.toast.promptSendFailed.title"),
|
||||||
|
description: errorMessage(err),
|
||||||
|
})
|
||||||
|
removeOptimisticMessage()
|
||||||
|
for (const item of commentItems) {
|
||||||
|
prompt.context.add({
|
||||||
|
type: "file",
|
||||||
|
path: item.path,
|
||||||
|
selection: item.selection,
|
||||||
|
comment: item.comment,
|
||||||
|
commentID: item.commentID,
|
||||||
|
preview: item.preview,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
restoreInput()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import { usePermission } from "@/context/permission"
|
|||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { retry } from "@opencode-ai/util/retry"
|
import { retry } from "@opencode-ai/util/retry"
|
||||||
import { playSound, soundSrc } from "@/utils/sound"
|
import { playSound, soundSrc } from "@/utils/sound"
|
||||||
|
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||||
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||||
@@ -332,6 +333,18 @@ export default function Layout(props: ParentProps) {
|
|||||||
const cooldownMs = 5000
|
const cooldownMs = 5000
|
||||||
|
|
||||||
const unsub = globalSDK.event.listen((e) => {
|
const unsub = globalSDK.event.listen((e) => {
|
||||||
|
if (e.details?.type === "worktree.ready") {
|
||||||
|
setBusy(e.name, false)
|
||||||
|
WorktreeState.ready(e.name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.details?.type === "worktree.failed") {
|
||||||
|
setBusy(e.name, false)
|
||||||
|
WorktreeState.failed(e.name, e.details.properties?.message ?? language.t("common.requestFailed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return
|
if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return
|
||||||
const title =
|
const title =
|
||||||
e.details.type === "permission.asked"
|
e.details.type === "permission.asked"
|
||||||
@@ -551,6 +564,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const project = currentProject()
|
const project = currentProject()
|
||||||
if (!project) return
|
if (!project) return
|
||||||
|
|
||||||
|
const local = project.worktree
|
||||||
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
||||||
const existing = store.workspaceOrder[project.worktree]
|
const existing = store.workspaceOrder[project.worktree]
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
@@ -558,9 +572,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const keep = existing.filter((d) => dirs.includes(d))
|
const keep = existing.filter((d) => d !== local && dirs.includes(d))
|
||||||
const missing = dirs.filter((d) => !existing.includes(d))
|
const missing = dirs.filter((d) => d !== local && !existing.includes(d))
|
||||||
const merged = [...keep, ...missing]
|
const merged = [local, ...missing, ...keep]
|
||||||
|
|
||||||
if (merged.length !== existing.length) {
|
if (merged.length !== existing.length) {
|
||||||
setStore("workspaceOrder", project.worktree, merged)
|
setStore("workspaceOrder", project.worktree, merged)
|
||||||
@@ -1434,17 +1448,22 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
function workspaceIds(project: LocalProject | undefined) {
|
function workspaceIds(project: LocalProject | undefined) {
|
||||||
if (!project) return []
|
if (!project) return []
|
||||||
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
const local = project.worktree
|
||||||
|
const dirs = [local, ...(project.sandboxes ?? [])]
|
||||||
const active = currentProject()
|
const active = currentProject()
|
||||||
const directory = active?.worktree === project.worktree && params.dir ? base64Decode(params.dir) : undefined
|
const directory = active?.worktree === project.worktree && params.dir ? base64Decode(params.dir) : undefined
|
||||||
const next = directory && directory !== project.worktree && !dirs.includes(directory) ? [...dirs, directory] : dirs
|
const extra = directory && directory !== local && !dirs.includes(directory) ? directory : undefined
|
||||||
|
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
|
||||||
|
|
||||||
const existing = store.workspaceOrder[project.worktree]
|
const existing = store.workspaceOrder[project.worktree]
|
||||||
if (!existing) return next
|
if (!existing) return extra ? [...dirs, extra] : dirs
|
||||||
|
|
||||||
const keep = existing.filter((d) => next.includes(d))
|
const keep = existing.filter((d) => d !== local && dirs.includes(d))
|
||||||
const missing = next.filter((d) => !existing.includes(d))
|
const missing = dirs.filter((d) => d !== local && !existing.includes(d))
|
||||||
return [...keep, ...missing]
|
const merged = [local, ...(pending && extra ? [extra] : []), ...missing, ...keep]
|
||||||
|
if (!extra) return merged
|
||||||
|
if (pending) return merged
|
||||||
|
return [...merged, extra]
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWorkspaceDragStart(event: unknown) {
|
function handleWorkspaceDragStart(event: unknown) {
|
||||||
@@ -2237,8 +2256,19 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
if (!created?.directory) return
|
if (!created?.directory) return
|
||||||
|
|
||||||
|
setBusy(created.directory, true)
|
||||||
|
WorktreeState.pending(created.directory)
|
||||||
|
setStore("workspaceExpanded", created.directory, true)
|
||||||
|
setStore("workspaceOrder", current.worktree, (prev) => {
|
||||||
|
const existing = prev ?? []
|
||||||
|
const local = current.worktree
|
||||||
|
const next = existing.filter((d) => d !== local && d !== created.directory)
|
||||||
|
return [local, created.directory, ...next]
|
||||||
|
})
|
||||||
|
|
||||||
globalSync.child(created.directory)
|
globalSync.child(created.directory)
|
||||||
navigate(`/${base64Encode(created.directory)}/session`)
|
navigate(`/${base64Encode(created.directory)}/session`)
|
||||||
|
layout.mobileSidebar.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
command.register(() => [
|
command.register(() => [
|
||||||
|
|||||||
58
packages/app/src/utils/worktree.ts
Normal file
58
packages/app/src/utils/worktree.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const normalize = (directory: string) => directory.replace(/[\\/]+$/, "")
|
||||||
|
|
||||||
|
type State =
|
||||||
|
| {
|
||||||
|
status: "pending"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: "ready"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: "failed"
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = new Map<string, State>()
|
||||||
|
const waiters = new Map<string, Array<(state: State) => void>>()
|
||||||
|
|
||||||
|
export const Worktree = {
|
||||||
|
get(directory: string) {
|
||||||
|
return state.get(normalize(directory))
|
||||||
|
},
|
||||||
|
pending(directory: string) {
|
||||||
|
const key = normalize(directory)
|
||||||
|
const current = state.get(key)
|
||||||
|
if (current && current.status !== "pending") return
|
||||||
|
state.set(key, { status: "pending" })
|
||||||
|
},
|
||||||
|
ready(directory: string) {
|
||||||
|
const key = normalize(directory)
|
||||||
|
state.set(key, { status: "ready" })
|
||||||
|
const list = waiters.get(key)
|
||||||
|
if (!list) return
|
||||||
|
waiters.delete(key)
|
||||||
|
for (const fn of list) fn({ status: "ready" })
|
||||||
|
},
|
||||||
|
failed(directory: string, message: string) {
|
||||||
|
const key = normalize(directory)
|
||||||
|
state.set(key, { status: "failed", message })
|
||||||
|
const list = waiters.get(key)
|
||||||
|
if (!list) return
|
||||||
|
waiters.delete(key)
|
||||||
|
for (const fn of list) fn({ status: "failed", message })
|
||||||
|
},
|
||||||
|
wait(directory: string) {
|
||||||
|
const key = normalize(directory)
|
||||||
|
const current = state.get(key)
|
||||||
|
if (current && current.status !== "pending") return Promise.resolve(current)
|
||||||
|
|
||||||
|
return new Promise<State>((resolve) => {
|
||||||
|
const list = waiters.get(key)
|
||||||
|
if (!list) {
|
||||||
|
waiters.set(key, [resolve])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list.push(resolve)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -338,6 +338,22 @@ export namespace Project {
|
|||||||
return valid
|
return valid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function addSandbox(projectID: string, directory: string) {
|
||||||
|
const result = await Storage.update<Info>(["project", projectID], (draft) => {
|
||||||
|
const sandboxes = draft.sandboxes ?? []
|
||||||
|
if (!sandboxes.includes(directory)) sandboxes.push(directory)
|
||||||
|
draft.sandboxes = sandboxes
|
||||||
|
draft.time.updated = Date.now()
|
||||||
|
})
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
payload: {
|
||||||
|
type: Event.Updated.type,
|
||||||
|
properties: result,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export async function removeSandbox(projectID: string, directory: string) {
|
export async function removeSandbox(projectID: string, directory: string) {
|
||||||
const result = await Storage.update<Info>(["project", projectID], (draft) => {
|
const result = await Storage.update<Info>(["project", projectID], (draft) => {
|
||||||
const sandboxes = draft.sandboxes ?? []
|
const sandboxes = draft.sandboxes ?? []
|
||||||
|
|||||||
@@ -5,12 +5,33 @@ import z from "zod"
|
|||||||
import { NamedError } from "@opencode-ai/util/error"
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
|
import { InstanceBootstrap } from "../project/bootstrap"
|
||||||
import { Project } from "../project/project"
|
import { Project } from "../project/project"
|
||||||
import { Storage } from "../storage/storage"
|
import { Storage } from "../storage/storage"
|
||||||
import { fn } from "../util/fn"
|
import { fn } from "../util/fn"
|
||||||
import { Config } from "@/config/config"
|
import { Log } from "../util/log"
|
||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { GlobalBus } from "@/bus/global"
|
||||||
|
|
||||||
export namespace Worktree {
|
export namespace Worktree {
|
||||||
|
const log = Log.create({ service: "worktree" })
|
||||||
|
|
||||||
|
export const Event = {
|
||||||
|
Ready: BusEvent.define(
|
||||||
|
"worktree.ready",
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
branch: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Failed: BusEvent.define(
|
||||||
|
"worktree.failed",
|
||||||
|
z.object({
|
||||||
|
message: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
@@ -234,7 +255,7 @@ export namespace Worktree {
|
|||||||
const base = input?.name ? slug(input.name) : ""
|
const base = input?.name ? slug(input.name) : ""
|
||||||
const info = await candidate(root, base || undefined)
|
const info = await candidate(root, base || undefined)
|
||||||
|
|
||||||
const created = await $`git worktree add -b ${info.branch} ${info.directory}`
|
const created = await $`git worktree add --no-checkout -b ${info.branch} ${info.directory}`
|
||||||
.quiet()
|
.quiet()
|
||||||
.nothrow()
|
.nothrow()
|
||||||
.cwd(Instance.worktree)
|
.cwd(Instance.worktree)
|
||||||
@@ -242,24 +263,88 @@ export namespace Worktree {
|
|||||||
throw new CreateFailedError({ message: errorText(created) || "Failed to create git worktree" })
|
throw new CreateFailedError({ message: errorText(created) || "Failed to create git worktree" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = await Storage.read<Project.Info>(["project", Instance.project.id]).catch(() => Instance.project)
|
await Project.addSandbox(Instance.project.id, info.directory).catch(() => undefined)
|
||||||
const startup = project.commands?.start?.trim()
|
|
||||||
if (startup) {
|
|
||||||
const ran = await runStartCommand(info.directory, startup)
|
|
||||||
if (ran.exitCode !== 0) {
|
|
||||||
throw new StartCommandFailedError({
|
|
||||||
message: errorText(ran) || "Project start command failed",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const projectID = Instance.project.id
|
||||||
const extra = input?.startCommand?.trim()
|
const extra = input?.startCommand?.trim()
|
||||||
if (extra) {
|
setTimeout(() => {
|
||||||
const ran = await runStartCommand(info.directory, extra)
|
const start = async () => {
|
||||||
if (ran.exitCode !== 0) {
|
const populated = await $`git reset --hard`.quiet().nothrow().cwd(info.directory)
|
||||||
throw new StartCommandFailedError({ message: errorText(ran) || "Worktree start command failed" })
|
if (populated.exitCode !== 0) {
|
||||||
|
const message = errorText(populated) || "Failed to populate worktree"
|
||||||
|
log.error("worktree checkout failed", { directory: info.directory, message })
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
directory: info.directory,
|
||||||
|
payload: {
|
||||||
|
type: Event.Failed.type,
|
||||||
|
properties: {
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const booted = await Instance.provide({
|
||||||
|
directory: info.directory,
|
||||||
|
init: InstanceBootstrap,
|
||||||
|
fn: () => undefined,
|
||||||
|
})
|
||||||
|
.then(() => true)
|
||||||
|
.catch((error) => {
|
||||||
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
log.error("worktree bootstrap failed", { directory: info.directory, message })
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
directory: info.directory,
|
||||||
|
payload: {
|
||||||
|
type: Event.Failed.type,
|
||||||
|
properties: {
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (!booted) return
|
||||||
|
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
directory: info.directory,
|
||||||
|
payload: {
|
||||||
|
type: Event.Ready.type,
|
||||||
|
properties: {
|
||||||
|
name: info.name,
|
||||||
|
branch: info.branch,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const project = await Storage.read<Project.Info>(["project", projectID]).catch(() => undefined)
|
||||||
|
const startup = project?.commands?.start?.trim() ?? ""
|
||||||
|
|
||||||
|
const run = async (cmd: string, kind: "project" | "worktree") => {
|
||||||
|
const ran = await runStartCommand(info.directory, cmd)
|
||||||
|
if (ran.exitCode === 0) return true
|
||||||
|
log.error("worktree start command failed", {
|
||||||
|
kind,
|
||||||
|
directory: info.directory,
|
||||||
|
message: errorText(ran),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startup) {
|
||||||
|
const ok = await run(startup, "project")
|
||||||
|
if (!ok) return
|
||||||
|
}
|
||||||
|
if (extra) {
|
||||||
|
await run(extra, "worktree")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
void start().catch((error) => {
|
||||||
|
log.error("worktree start task failed", { directory: info.directory, error })
|
||||||
|
})
|
||||||
|
}, 0)
|
||||||
|
|
||||||
return info
|
return info
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -866,6 +866,21 @@ export type EventPtyDeleted = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EventWorktreeReady = {
|
||||||
|
type: "worktree.ready"
|
||||||
|
properties: {
|
||||||
|
name: string
|
||||||
|
branch: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventWorktreeFailed = {
|
||||||
|
type: "worktree.failed"
|
||||||
|
properties: {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| EventInstallationUpdated
|
| EventInstallationUpdated
|
||||||
| EventInstallationUpdateAvailable
|
| EventInstallationUpdateAvailable
|
||||||
@@ -907,6 +922,8 @@ export type Event =
|
|||||||
| EventPtyUpdated
|
| EventPtyUpdated
|
||||||
| EventPtyExited
|
| EventPtyExited
|
||||||
| EventPtyDeleted
|
| EventPtyDeleted
|
||||||
|
| EventWorktreeReady
|
||||||
|
| EventWorktreeFailed
|
||||||
|
|
||||||
export type GlobalEvent = {
|
export type GlobalEvent = {
|
||||||
directory: string
|
directory: string
|
||||||
|
|||||||
Reference in New Issue
Block a user