From f735237a561ecd8faf51925579cbb40df8c66f2c Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Mon, 16 Mar 2026 14:41:36 +0100 Subject: [PATCH] chore: clean up script runner logic This is incredibly old logic which I'd like to take a new pass at --- .../main/services/workspace/configLoader.ts | 107 ------ .../services/workspace/configSchema.test.ts | 149 --------- .../main/services/workspace/configSchema.ts | 40 --- .../src/main/services/workspace/schemas.ts | 53 --- .../main/services/workspace/scriptRunner.ts | 182 ----------- .../src/main/services/workspace/service.ts | 307 +----------------- .../main/services/workspace/workspaceEnv.ts | 10 - apps/code/src/main/trpc/routers/workspace.ts | 28 -- .../panels/hooks/usePanelLayoutHooks.tsx | 5 +- .../features/panels/store/panelLayoutStore.ts | 49 --- .../features/panels/store/panelTypes.ts | 6 - .../components/TabContentRenderer.tsx | 10 - .../task-detail/components/TaskDetail.tsx | 4 +- .../components/StartWorkspaceButton.tsx | 81 ----- .../components/WorkspaceTerminalPanel.tsx | 61 ---- .../features/workspace/hooks/useWorkspace.ts | 13 - .../workspace/hooks/useWorkspaceEvents.ts | 34 +- .../workspace/hooks/useWorkspaceStatus.ts | 53 --- .../stores/workspaceTerminalStore.ts | 104 ------ .../src/renderer/sagas/task/task-creation.ts | 4 - 20 files changed, 4 insertions(+), 1296 deletions(-) delete mode 100644 apps/code/src/main/services/workspace/configLoader.ts delete mode 100644 apps/code/src/main/services/workspace/configSchema.test.ts delete mode 100644 apps/code/src/main/services/workspace/configSchema.ts delete mode 100644 apps/code/src/main/services/workspace/scriptRunner.ts delete mode 100644 apps/code/src/renderer/features/workspace/components/StartWorkspaceButton.tsx delete mode 100644 apps/code/src/renderer/features/workspace/components/WorkspaceTerminalPanel.tsx delete mode 100644 apps/code/src/renderer/features/workspace/hooks/useWorkspaceStatus.ts delete mode 100644 apps/code/src/renderer/features/workspace/stores/workspaceTerminalStore.ts diff --git a/apps/code/src/main/services/workspace/configLoader.ts b/apps/code/src/main/services/workspace/configLoader.ts deleted file mode 100644 index 2083ee1e4..000000000 --- a/apps/code/src/main/services/workspace/configLoader.ts +++ /dev/null @@ -1,107 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import { logger } from "../../utils/logger"; -import { - type ArrayConfig, - type ConfigValidationResult, - validateConfig, -} from "./configSchema"; - -const log = logger.scope("workspace:config"); - -export type ConfigSource = "workspace" | "repo"; - -export interface LoadConfigResult { - config: ArrayConfig | null; - source: ConfigSource | null; -} - -export async function loadConfig( - worktreePath: string, - worktreeName: string, -): Promise { - // Search order (first match wins): - // 1. .posthog-code/{WORKSPACE_NAME}/posthog-code.json (workspace-specific) - // 2. .twig/{WORKSPACE_NAME}/twig.json (workspace-specific, legacy) - // 3. .twig/{WORKSPACE_NAME}/array.json (workspace-specific, legacy) - // 4. .array/{WORKSPACE_NAME}/array.json (workspace-specific, legacy) - // 5. {repo-root}/posthog-code.json (repository root) - // 6. {repo-root}/twig.json (repository root, legacy) - // 7. {repo-root}/array.json (repository root, legacy) - - const workspaceConfigPaths = [ - path.join(worktreePath, ".posthog-code", worktreeName, "posthog-code.json"), - path.join(worktreePath, ".twig", worktreeName, "twig.json"), - path.join(worktreePath, ".twig", worktreeName, "array.json"), - path.join(worktreePath, ".array", worktreeName, "array.json"), - ]; - - const repoConfigPaths = [ - path.join(worktreePath, "posthog-code.json"), - path.join(worktreePath, "twig.json"), - path.join(worktreePath, "array.json"), - ]; - - // Try workspace-specific configs first - for (const configPath of workspaceConfigPaths) { - const result = await tryLoadConfig(configPath); - if (result.config) { - log.info(`Loaded config from workspace: ${configPath}`); - return { config: result.config, source: "workspace" }; - } - if (result.errors) { - log.warn(`Invalid config at ${configPath}: ${result.errors.join(", ")}`); - return { config: null, source: null }; - } - } - - // Try repo root configs - for (const configPath of repoConfigPaths) { - const result = await tryLoadConfig(configPath); - if (result.config) { - log.info(`Loaded config from repo root: ${configPath}`); - return { config: result.config, source: "repo" }; - } - if (result.errors) { - log.warn(`Invalid config at ${configPath}: ${result.errors.join(", ")}`); - return { config: null, source: null }; - } - } - - return { config: null, source: null }; -} - -interface TryLoadResult { - config: ArrayConfig | null; - errors: string[] | null; -} - -async function tryLoadConfig(configPath: string): Promise { - try { - const content = await fs.readFile(configPath, "utf-8"); - const data = JSON.parse(content); - const result: ConfigValidationResult = validateConfig(data); - - if (result.success) { - return { config: result.config, errors: null }; - } - return { config: null, errors: result.errors }; - } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") { - // File doesn't exist - not an error, just continue searching - return { config: null, errors: null }; - } - if (error instanceof SyntaxError) { - return { config: null, errors: [`Invalid JSON: ${error.message}`] }; - } - log.error(`Error reading config from ${configPath}:`, error); - return { config: null, errors: [`Failed to read file: ${String(error)}`] }; - } -} - -export function normalizeScripts( - scripts: string | string[] | undefined, -): string[] { - if (!scripts) return []; - return Array.isArray(scripts) ? scripts : [scripts]; -} diff --git a/apps/code/src/main/services/workspace/configSchema.test.ts b/apps/code/src/main/services/workspace/configSchema.test.ts deleted file mode 100644 index 32d5797b5..000000000 --- a/apps/code/src/main/services/workspace/configSchema.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { arrayConfigSchema } from "./configSchema"; - -describe("configSchema", () => { - describe("valid configurations", () => { - it("accepts empty config", () => { - const result = arrayConfigSchema.safeParse({}); - expect(result.success).toBe(true); - }); - - it("accepts config with no scripts", () => { - const result = arrayConfigSchema.safeParse({ scripts: {} }); - expect(result.success).toBe(true); - }); - - it("accepts init script as string", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { init: "npm install" }, - }); - expect(result.success).toBe(true); - }); - - it("accepts init script as array", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { init: ["npm install", "npm run build"] }, - }); - expect(result.success).toBe(true); - }); - - it("accepts start script as string", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { start: "npm run dev" }, - }); - expect(result.success).toBe(true); - }); - - it("accepts start script as array", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { start: ["npm run server", "npm run client"] }, - }); - expect(result.success).toBe(true); - }); - - it("accepts destroy script as string", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { destroy: "npm run cleanup" }, - }); - expect(result.success).toBe(true); - }); - - it("accepts destroy script as array", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { destroy: ["docker-compose down", "rm -rf node_modules"] }, - }); - expect(result.success).toBe(true); - }); - - it("accepts all scripts together", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { - init: "npm install", - start: ["npm run dev", "npm run watch"], - destroy: "docker-compose down", - }, - }); - expect(result.success).toBe(true); - }); - }); - - describe("invalid configurations", () => { - it("rejects empty string script", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { init: "" }, - }); - expect(result.success).toBe(false); - }); - - it("rejects empty array script", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { init: [] }, - }); - expect(result.success).toBe(false); - }); - - it("rejects array with empty string", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { init: ["npm install", ""] }, - }); - expect(result.success).toBe(false); - }); - - it("rejects unknown properties", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { init: "npm install" }, - unknown: "value", - }); - expect(result.success).toBe(false); - }); - - it("rejects unknown script types", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { build: "npm run build" }, - }); - expect(result.success).toBe(false); - }); - - it("rejects non-string script values", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { init: 123 }, - }); - expect(result.success).toBe(false); - }); - - it("rejects array with non-string values", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { init: ["npm install", 123] }, - }); - expect(result.success).toBe(false); - }); - }); - - describe("error messages", () => { - it("provides useful error for empty string", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { init: "" }, - }); - expect(result.success).toBe(false); - if (!result.success) { - const errorMessages = result.error.issues.map((i) => i.message); - expect(errorMessages.some((m) => m.includes("cannot be empty"))).toBe( - true, - ); - } - }); - - it("provides useful error for empty array", () => { - const result = arrayConfigSchema.safeParse({ - scripts: { start: [] }, - }); - expect(result.success).toBe(false); - if (!result.success) { - const errorMessages = result.error.issues.map((i) => i.message); - expect(errorMessages.some((m) => m.includes("cannot be empty"))).toBe( - true, - ); - } - }); - }); -}); diff --git a/apps/code/src/main/services/workspace/configSchema.ts b/apps/code/src/main/services/workspace/configSchema.ts deleted file mode 100644 index 13590a6f6..000000000 --- a/apps/code/src/main/services/workspace/configSchema.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { z } from "zod"; - -const scriptCommand = z.union([ - z.string().min(1, "Script command cannot be empty"), - z - .array(z.string().min(1, "Script command cannot be empty")) - .min(1, "Script array cannot be empty"), -]); - -export const arrayConfigSchema = z - .object({ - scripts: z - .object({ - init: scriptCommand.optional(), - start: scriptCommand.optional(), - destroy: scriptCommand.optional(), - }) - .strict() - .optional(), - }) - .strict(); - -export type ArrayConfig = z.infer; - -export type ConfigValidationResult = - | { success: true; config: ArrayConfig } - | { success: false; errors: string[] }; - -export function validateConfig(data: unknown): ConfigValidationResult { - const result = arrayConfigSchema.safeParse(data); - if (result.success) { - return { success: true, config: result.data }; - } - return { - success: false, - errors: result.error.issues.map( - (issue) => `${issue.path.join(".")}: ${issue.message}`, - ), - }; -} diff --git a/apps/code/src/main/services/workspace/schemas.ts b/apps/code/src/main/services/workspace/schemas.ts index a81d6a12e..a49a3644c 100644 --- a/apps/code/src/main/services/workspace/schemas.ts +++ b/apps/code/src/main/services/workspace/schemas.ts @@ -13,22 +13,11 @@ export const worktreeInfoSchema = z.object({ createdAt: z.string(), }); -export const workspaceTerminalInfoSchema = z.object({ - sessionId: z.string(), - scriptType: z.enum(["init", "start"]), - command: z.string(), - label: z.string(), - status: z.enum(["running", "completed", "failed"]), - exitCode: z.number().optional(), -}); - export const workspaceInfoSchema = z.object({ taskId: z.string(), mode: workspaceModeSchema, worktree: worktreeInfoSchema.nullable(), branchName: z.string().nullable(), - terminalSessionIds: z.array(z.string()), - hasStartScripts: z.boolean().optional(), }); export const workspaceSchema = z.object({ @@ -41,14 +30,6 @@ export const workspaceSchema = z.object({ branchName: z.string().nullable(), baseBranch: z.string().nullable(), createdAt: z.string(), - terminalSessionIds: z.array(z.string()), - hasStartScripts: z.boolean().optional(), -}); - -export const scriptExecutionResultSchema = z.object({ - success: z.boolean(), - terminalSessionIds: z.array(z.string()), - errors: z.array(z.string()).optional(), }); // Input schemas @@ -84,20 +65,6 @@ export const getWorkspaceInfoInput = z.object({ taskId: z.string(), }); -export const runStartScriptsInput = z.object({ - taskId: z.string(), - worktreePath: z.string(), - worktreeName: z.string(), -}); - -export const isWorkspaceRunningInput = z.object({ - taskId: z.string(), -}); - -export const getWorkspaceTerminalsInput = z.object({ - taskId: z.string(), -}); - // Output schemas export const createWorkspaceOutput = workspaceInfoSchema; export const verifyWorkspaceOutput = z.object({ @@ -106,15 +73,6 @@ export const verifyWorkspaceOutput = z.object({ }); export const getWorkspaceInfoOutput = workspaceInfoSchema.nullable(); export const getAllWorkspacesOutput = z.record(z.string(), workspaceSchema); -export const runStartScriptsOutput = scriptExecutionResultSchema; -export const isWorkspaceRunningOutput = z.boolean(); -export const getWorkspaceTerminalsOutput = z.array(workspaceTerminalInfoSchema); - -// Event payload schemas (for subscriptions) -export const workspaceTerminalCreatedPayload = - workspaceTerminalInfoSchema.extend({ - taskId: z.string(), - }); export const workspaceErrorPayload = z.object({ taskId: z.string(), @@ -257,27 +215,16 @@ export const getAllTaskTimestampsOutput = z.record( // Type exports export type WorkspaceMode = z.infer; export type WorktreeInfo = z.infer; -export type WorkspaceTerminalInfo = z.infer; export type WorkspaceInfo = z.infer; export type Workspace = z.infer; -export type ScriptExecutionResult = z.infer; export type CreateWorkspaceInput = z.infer; export type DeleteWorkspaceInput = z.infer; export type VerifyWorkspaceInput = z.infer; export type GetWorkspaceInfoInput = z.infer; -export type RunStartScriptsInput = z.infer; -export type IsWorkspaceRunningInput = z.infer; -export type GetWorkspaceTerminalsInput = z.infer< - typeof getWorkspaceTerminalsInput ->; export type ListGitWorktreesInput = z.infer; export type GetWorktreeSizeInput = z.infer; export type DeleteWorktreeInput = z.infer; - -export type WorkspaceTerminalCreatedPayload = z.infer< - typeof workspaceTerminalCreatedPayload ->; export type WorkspaceErrorPayload = z.infer; export type WorkspaceWarningPayload = z.infer; export type WorkspacePromotedPayload = z.infer; diff --git a/apps/code/src/main/services/workspace/scriptRunner.ts b/apps/code/src/main/services/workspace/scriptRunner.ts deleted file mode 100644 index e31d1ec58..000000000 --- a/apps/code/src/main/services/workspace/scriptRunner.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { exec } from "node:child_process"; -import * as fs from "node:fs"; -import { promisify } from "node:util"; -import { randomSuffix } from "@shared/utils/id"; -import { getMainWindow } from "../../trpc/context"; -import { logger } from "../../utils/logger"; -import { ShellEvent } from "../shell/schemas"; -import type { ShellService } from "../shell/service"; -import type { - ScriptExecutionResult, - WorkspaceTerminalCreatedPayload, - WorkspaceTerminalInfo, -} from "./schemas"; - -const execAsync = promisify(exec); -const log = logger.scope("workspace:scripts"); - -function generateSessionId(taskId: string, scriptType: string): string { - return `workspace-${taskId}-${scriptType}-${Date.now()}-${randomSuffix(6)}`; -} - -export interface ScriptRunnerOptions { - shellService: ShellService; - onTerminalCreated: (info: WorkspaceTerminalCreatedPayload) => void; -} - -export class ScriptRunner { - private shellService: ShellService; - private onTerminalCreated: (info: WorkspaceTerminalCreatedPayload) => void; - private subscribedSessions = new Set(); - - constructor(options: ScriptRunnerOptions) { - this.shellService = options.shellService; - this.onTerminalCreated = options.onTerminalCreated; - this.setupEventForwarding(); - } - - private setupEventForwarding(): void { - this.shellService.on(ShellEvent.Data, ({ sessionId, data }) => { - if (this.subscribedSessions.has(sessionId)) { - const mainWindow = getMainWindow(); - mainWindow?.webContents.send(`shell:data:${sessionId}`, data); - } - }); - - this.shellService.on(ShellEvent.Exit, ({ sessionId, exitCode }) => { - if (this.subscribedSessions.has(sessionId)) { - const mainWindow = getMainWindow(); - mainWindow?.webContents.send(`shell:exit:${sessionId}`, { exitCode }); - this.subscribedSessions.delete(sessionId); - } - }); - } - - async executeScriptsWithTerminal( - taskId: string, - scripts: string | string[], - scriptType: "init" | "start", - cwd: string, - options: { failFast?: boolean; workspaceEnv?: Record } = {}, - ): Promise { - const commands = Array.isArray(scripts) ? scripts : [scripts]; - const terminalSessionIds: string[] = []; - const errors: string[] = []; - - if (!fs.existsSync(cwd)) { - log.error(`Working directory does not exist: ${cwd}`); - return { - success: false, - terminalSessionIds: [], - errors: [`Working directory does not exist: ${cwd}`], - }; - } - - for (const command of commands) { - const sessionId = generateSessionId(taskId, scriptType); - log.info(`Starting ${scriptType} script for task ${taskId}: ${command}`); - - try { - this.subscribedSessions.add(sessionId); - - const session = await this.shellService.createSession({ - sessionId, - cwd, - initialCommand: command, - additionalEnv: options.workspaceEnv, - }); - - terminalSessionIds.push(sessionId); - - this.onTerminalCreated({ - taskId, - sessionId, - scriptType, - command, - label: command.split(" ")[0] || command, - status: "running", - }); - - if (options.failFast) { - const result = await session.exitPromise; - if (result.exitCode !== 0) { - log.error( - `Init script failed with exit code ${result.exitCode}: ${command}`, - ); - errors.push( - `Script "${command}" failed with exit code ${result.exitCode}`, - ); - return { success: false, terminalSessionIds, errors }; - } - log.info(`Init script completed successfully: ${command}`); - } - } catch (error) { - log.error(`Failed to start script: ${command}`, error); - errors.push(`Failed to start "${command}": ${String(error)}`); - if (options.failFast) { - return { success: false, terminalSessionIds, errors }; - } - } - } - - return { - success: errors.length === 0, - terminalSessionIds, - errors: errors.length > 0 ? errors : undefined, - }; - } - - async executeScriptsSilent( - scripts: string | string[], - cwd: string, - workspaceEnv?: Record, - ): Promise<{ success: boolean; errors: string[] }> { - const commands = Array.isArray(scripts) ? scripts : [scripts]; - const errors: string[] = []; - - const execEnv = workspaceEnv - ? { ...process.env, ...workspaceEnv } - : undefined; - - for (const command of commands) { - log.info(`Running destroy script silently: ${command}`); - try { - await execAsync(command, { cwd, timeout: 60000, env: execEnv }); - log.info(`Destroy script completed: ${command}`); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - log.warn(`Destroy script failed: ${command} - ${errorMessage}`); - errors.push(`${command}: ${errorMessage}`); - } - } - - return { success: errors.length === 0, errors }; - } - - getSessionInfo(sessionId: string): WorkspaceTerminalInfo | null { - const session = this.shellService.getSession(sessionId); - if (!session) return null; - - return { - sessionId, - scriptType: sessionId.includes("-init-") ? "init" : "start", - command: session.command || "", - label: session.command?.split(" ")[0] || "", - status: "running", - }; - } - - isSessionRunning(sessionId: string): boolean { - return this.shellService.hasSession(sessionId); - } - - getTaskSessions(taskId: string): string[] { - return this.shellService.getSessionsByPrefix(`workspace-${taskId}-`); - } - - cleanupTaskSessions(taskId: string): void { - log.info(`Cleaning up workspace sessions for task: ${taskId}`); - this.shellService.destroyByPrefix(`workspace-${taskId}-`); - } -} diff --git a/apps/code/src/main/services/workspace/service.ts b/apps/code/src/main/services/workspace/service.ts index 381f7349c..f1b0a8737 100644 --- a/apps/code/src/main/services/workspace/service.ts +++ b/apps/code/src/main/services/workspace/service.ts @@ -29,24 +29,17 @@ import type { FocusService } from "../focus/service"; import { FocusServiceEvent } from "../focus/service"; import type { ProcessTrackingService } from "../process-tracking/service"; import { getWorktreeLocation } from "../settingsStore"; -import type { ShellService } from "../shell/service"; import type { SuspensionService } from "../suspension/service.js"; -import { loadConfig, normalizeScripts } from "./configLoader"; import type { BranchChangedPayload, CreateWorkspaceInput, - ScriptExecutionResult, Workspace, WorkspaceErrorPayload, WorkspaceInfo, WorkspacePromotedPayload, - WorkspaceTerminalCreatedPayload, - WorkspaceTerminalInfo, WorkspaceWarningPayload, WorktreeInfo, } from "./schemas"; -import { ScriptRunner } from "./scriptRunner"; -import { buildWorkspaceEnv } from "./workspaceEnv"; const execFileAsync = promisify(execFile); @@ -102,7 +95,6 @@ async function getBranchFromPath(repoPath: string): Promise { const log = logger.scope("workspace"); export const WorkspaceServiceEvent = { - TerminalCreated: "terminalCreated", Error: "error", Warning: "warning", Promoted: "promoted", @@ -110,7 +102,6 @@ export const WorkspaceServiceEvent = { } as const; export interface WorkspaceServiceEvents { - [WorkspaceServiceEvent.TerminalCreated]: WorkspaceTerminalCreatedPayload; [WorkspaceServiceEvent.Error]: WorkspaceErrorPayload; [WorkspaceServiceEvent.Warning]: WorkspaceWarningPayload; [WorkspaceServiceEvent.Promoted]: WorkspacePromotedPayload; @@ -119,9 +110,6 @@ export interface WorkspaceServiceEvents { @injectable() export class WorkspaceService extends TypedEventEmitter { - @inject(MAIN_TOKENS.ShellService) - private shellService!: ShellService; - @inject(MAIN_TOKENS.AgentService) private agentService!: AgentService; @@ -140,22 +128,9 @@ export class WorkspaceService extends TypedEventEmitter @inject(MAIN_TOKENS.SuspensionService) private suspensionService!: SuspensionService; - private scriptRunner!: ScriptRunner; private creatingWorkspaces = new Map>(); private branchWatcherInitialized = false; - private ensureScriptRunner(): ScriptRunner { - if (!this.scriptRunner) { - this.scriptRunner = new ScriptRunner({ - shellService: this.shellService, - onTerminalCreated: (info) => { - this.emit(WorkspaceServiceEvent.TerminalCreated, info); - }, - }); - } - return this.scriptRunner; - } - private findTaskAssociation(taskId: string): TaskAssociation | null { const workspace = this.workspaceRepo.findByTaskId(taskId); if (!workspace) return null; @@ -413,8 +388,6 @@ export class WorkspaceService extends TypedEventEmitter mode, worktree: null, branchName: null, - terminalSessionIds: [], - hasStartScripts: false, }; } @@ -450,80 +423,12 @@ export class WorkspaceService extends TypedEventEmitter mode: "local", }); - const [{ config }, workspaceEnv] = await Promise.all([ - loadConfig(folderPath, path.basename(folderPath)), - buildWorkspaceEnv({ - taskId, - folderPath, - worktreePath: null, - worktreeName: null, - mode, - }), - ]); - let terminalSessionIds: string[] = []; - - // Run init scripts - const initScripts = normalizeScripts(config?.scripts?.init); - if (initScripts.length > 0) { - log.info( - `Running ${initScripts.length} init script(s) for task ${taskId} (local mode)`, - ); - const initResult = - await this.ensureScriptRunner().executeScriptsWithTerminal( - taskId, - initScripts, - "init", - folderPath, - { failFast: true, workspaceEnv }, - ); - terminalSessionIds = initResult.terminalSessionIds; - - if (!initResult.success) { - log.error(`Init scripts failed for task ${taskId}`); - throw new Error( - `Workspace init failed: ${initResult.errors?.join(", ") || "Unknown error"}`, - ); - } - } - - // Run start scripts - const startScripts = normalizeScripts(config?.scripts?.start); - if (startScripts.length > 0) { - log.info( - `Running ${startScripts.length} start script(s) for task ${taskId} (local mode)`, - ); - const startResult = - await this.ensureScriptRunner().executeScriptsWithTerminal( - taskId, - startScripts, - "start", - folderPath, - { failFast: false, workspaceEnv }, - ); - terminalSessionIds = [ - ...terminalSessionIds, - ...startResult.terminalSessionIds, - ]; - - if (!startResult.success) { - log.warn( - `Some start scripts failed for task ${taskId}: ${startResult.errors?.join(", ")}`, - ); - this.emitWorkspaceError( - taskId, - `Start scripts failed: ${startResult.errors?.join(", ")}`, - ); - } - } - const localBranch = await getBranchFromPath(folderPath); return { taskId, mode, worktree: null, branchName: localBranch, - terminalSessionIds, - hasStartScripts: startScripts.length > 0, }; } @@ -618,91 +523,11 @@ export class WorkspaceService extends TypedEventEmitter path: worktree.worktreePath, }); - const [{ config }, workspaceEnv] = await Promise.all([ - loadConfig(worktree.worktreePath, worktree.worktreeName), - buildWorkspaceEnv({ - taskId, - folderPath, - worktreePath: worktree.worktreePath, - worktreeName: worktree.worktreeName, - mode, - }), - ]); - - const initScripts = normalizeScripts(config?.scripts?.init); - let terminalSessionIds: string[] = []; - - if (initScripts.length > 0) { - log.info( - `Running ${initScripts.length} init script(s) for task ${taskId}`, - ); - const initResult = - await this.ensureScriptRunner().executeScriptsWithTerminal( - taskId, - initScripts, - "init", - worktree.worktreePath, - { failFast: true, workspaceEnv }, - ); - - terminalSessionIds = initResult.terminalSessionIds; - - if (!initResult.success) { - // Cleanup on init failure - log.error( - `Init scripts failed for task ${taskId}, cleaning up worktree`, - ); - await this.cleanupWorktree( - taskId, - mainRepoPath, - worktree.worktreePath, - worktree.branchName ?? null, - ); - throw new Error( - `Workspace init failed: ${initResult.errors?.join(", ") || "Unknown error"}`, - ); - } - } - - // Run start scripts (don't fail on error, just notify) - const startScripts = normalizeScripts(config?.scripts?.start); - if (startScripts.length > 0) { - log.info( - `Running ${startScripts.length} start script(s) for task ${taskId}`, - ); - const startResult = - await this.ensureScriptRunner().executeScriptsWithTerminal( - taskId, - startScripts, - "start", - worktree.worktreePath, - { failFast: false, workspaceEnv }, - ); - - terminalSessionIds = [ - ...terminalSessionIds, - ...startResult.terminalSessionIds, - ]; - - if (!startResult.success) { - log.warn( - `Some start scripts failed for task ${taskId}: ${startResult.errors?.join(", ")}`, - ); - // Emit error to renderer for toast notification - this.emitWorkspaceError( - taskId, - `Start scripts failed: ${startResult.errors?.join(", ")}`, - ); - } - } - return { taskId, mode, worktree, branchName: worktree.branchName, - terminalSessionIds, - hasStartScripts: startScripts.length > 0, }; } @@ -729,58 +554,14 @@ export class WorkspaceService extends TypedEventEmitter return; } - let scriptPath: string; - let scriptName: string; let worktreePath: string | null = null; - let worktreeName: string | null = null; if (association.mode === "worktree") { - worktreeName = association.worktree; - worktreePath = deriveWorktreePath(folderPath, worktreeName); - scriptPath = worktreePath; - scriptName = worktreeName; - } else { - scriptPath = folderPath; - scriptName = path.basename(folderPath); - } - - const { config } = await loadConfig(scriptPath, scriptName); - const destroyScripts = normalizeScripts(config?.scripts?.destroy); - - if (destroyScripts.length > 0) { - log.info( - `Running ${destroyScripts.length} destroy script(s) for task ${taskId}`, - ); - - const workspaceEnv = await buildWorkspaceEnv({ - taskId, - folderPath, - worktreePath, - worktreeName, - mode: association.mode, - }); - - const destroyResult = - await this.ensureScriptRunner().executeScriptsSilent( - destroyScripts, - scriptPath, - workspaceEnv, - ); - - if (!destroyResult.success) { - log.warn( - `Some destroy scripts failed for task ${taskId}: ${destroyResult.errors.join(", ")}`, - ); - this.emitWorkspaceError( - taskId, - `Destroy scripts failed: ${destroyResult.errors.join(", ")}`, - ); - } + worktreePath = deriveWorktreePath(folderPath, association.worktree); } await this.agentService.cancelSessionsByTaskId(taskId); this.processTracking.killByTaskId(taskId); - this.ensureScriptRunner().cleanupTaskSessions(taskId); if (association.mode === "worktree" && worktreePath) { await this.cleanupWorktree( @@ -912,50 +693,6 @@ export class WorkspaceService extends TypedEventEmitter return { exists: false }; } - async runStartScripts( - taskId: string, - worktreePath: string, - worktreeName: string, - ): Promise { - log.info(`Running start scripts for task ${taskId}`); - - const { config } = await loadConfig(worktreePath, worktreeName); - const startScripts = normalizeScripts(config?.scripts?.start); - - if (startScripts.length === 0) { - return { success: true, terminalSessionIds: [] }; - } - - const association = this.findTaskAssociation(taskId); - const folderPath = association?.folderId - ? this.getFolderPath(association.folderId) - : null; - const workspaceEnv = await buildWorkspaceEnv({ - taskId, - folderPath: folderPath ?? worktreePath, - worktreePath, - worktreeName, - mode: association?.mode ?? "worktree", - }); - - const result = await this.ensureScriptRunner().executeScriptsWithTerminal( - taskId, - startScripts, - "start", - worktreePath, - { failFast: false, workspaceEnv }, - ); - - if (!result.success) { - this.emitWorkspaceError( - taskId, - `Start scripts failed: ${result.errors?.join(", ")}`, - ); - } - - return result; - } - async getWorkspaceInfo(taskId: string): Promise { const association = this.findTaskAssociation(taskId); if (!association) { @@ -968,7 +705,6 @@ export class WorkspaceService extends TypedEventEmitter mode: "cloud", worktree: null, branchName: null, - terminalSessionIds: [], }; } @@ -1003,29 +739,9 @@ export class WorkspaceService extends TypedEventEmitter mode: association.mode, worktree: worktreeInfo, branchName, - terminalSessionIds: this.ensureScriptRunner().getTaskSessions(taskId), }; } - isWorkspaceRunning(taskId: string): boolean { - const sessions = this.ensureScriptRunner().getTaskSessions(taskId); - return sessions.length > 0; - } - - getWorkspaceTerminals(taskId: string): WorkspaceTerminalInfo[] { - const sessionIds = this.ensureScriptRunner().getTaskSessions(taskId); - const terminals: WorkspaceTerminalInfo[] = []; - - for (const sessionId of sessionIds) { - const info = this.ensureScriptRunner().getSessionInfo(sessionId); - if (info) { - terminals.push(info); - } - } - - return terminals; - } - async getAllWorkspaces(): Promise> { const associations = this.getAllTaskAssociations(); const workspaces: Record = {}; @@ -1042,8 +758,6 @@ export class WorkspaceService extends TypedEventEmitter branchName: null, baseBranch: null, createdAt: new Date().toISOString(), - terminalSessionIds: [], - hasStartScripts: false, }; continue; } @@ -1051,27 +765,12 @@ export class WorkspaceService extends TypedEventEmitter const folderPath = this.getFolderPath(assoc.folderId); if (!folderPath) continue; - let configPath: string; - let configName: string; let worktreePath: string | null = null; let worktreeName: string | null = null; if (assoc.mode === "worktree") { worktreeName = assoc.worktree; worktreePath = deriveWorktreePath(folderPath, worktreeName); - configPath = worktreePath; - configName = worktreeName; - } else { - configPath = folderPath; - configName = path.basename(folderPath); - } - - let startScripts: string[] = []; - try { - const { config } = await loadConfig(configPath, configName); - startScripts = normalizeScripts(config?.scripts?.start); - } catch { - /* config load failed, no start scripts */ } let branchName: string | null = null; @@ -1095,10 +794,6 @@ export class WorkspaceService extends TypedEventEmitter branchName, baseBranch: null, createdAt: new Date().toISOString(), - terminalSessionIds: this.ensureScriptRunner().getTaskSessions( - assoc.taskId, - ), - hasStartScripts: startScripts.length > 0, }; } diff --git a/apps/code/src/main/services/workspace/workspaceEnv.ts b/apps/code/src/main/services/workspace/workspaceEnv.ts index e505f5808..cae998740 100644 --- a/apps/code/src/main/services/workspace/workspaceEnv.ts +++ b/apps/code/src/main/services/workspace/workspaceEnv.ts @@ -69,15 +69,5 @@ export async function buildWorkspaceEnv( POSTHOG_CODE_WORKSPACE_PORTS_RANGE: String(PORTS_PER_WORKSPACE), POSTHOG_CODE_WORKSPACE_PORTS_START: String(portAllocation.start), POSTHOG_CODE_WORKSPACE_PORTS_END: String(portAllocation.end), - // Legacy env vars for backwards compatibility - TWIG_WORKSPACE_NAME: workspaceName, - TWIG_WORKSPACE_PATH: workspacePath, - TWIG_ROOT_PATH: rootPath, - TWIG_DEFAULT_BRANCH: defaultBranch, - TWIG_WORKSPACE_BRANCH: workspaceBranch, - TWIG_WORKSPACE_PORTS: portAllocation.ports.join(","), - TWIG_WORKSPACE_PORTS_RANGE: String(PORTS_PER_WORKSPACE), - TWIG_WORKSPACE_PORTS_START: String(portAllocation.start), - TWIG_WORKSPACE_PORTS_END: String(portAllocation.end), }; } diff --git a/apps/code/src/main/trpc/routers/workspace.ts b/apps/code/src/main/trpc/routers/workspace.ts index 8359cc51c..ad8fd08e8 100644 --- a/apps/code/src/main/trpc/routers/workspace.ts +++ b/apps/code/src/main/trpc/routers/workspace.ts @@ -15,20 +15,14 @@ import { getTaskTimestampsOutput, getWorkspaceInfoInput, getWorkspaceInfoOutput, - getWorkspaceTerminalsInput, - getWorkspaceTerminalsOutput, getWorktreeSizeInput, getWorktreeSizeOutput, getWorktreeTasksInput, getWorktreeTasksOutput, - isWorkspaceRunningInput, - isWorkspaceRunningOutput, listGitWorktreesInput, listGitWorktreesOutput, markActivityInput, markViewedInput, - runStartScriptsInput, - runStartScriptsOutput, togglePinInput, togglePinOutput, verifyWorkspaceInput, @@ -83,27 +77,6 @@ export const workspaceRouter = router({ .output(getAllWorkspacesOutput) .query(() => getService().getAllWorkspaces()), - runStart: publicProcedure - .input(runStartScriptsInput) - .output(runStartScriptsOutput) - .mutation(({ input }) => - getService().runStartScripts( - input.taskId, - input.worktreePath, - input.worktreeName, - ), - ), - - isRunning: publicProcedure - .input(isWorkspaceRunningInput) - .output(isWorkspaceRunningOutput) - .query(({ input }) => getService().isWorkspaceRunning(input.taskId)), - - getTerminals: publicProcedure - .input(getWorkspaceTerminalsInput) - .output(getWorkspaceTerminalsOutput) - .query(({ input }) => getService().getWorkspaceTerminals(input.taskId)), - getLocalTasks: publicProcedure .input(getLocalTasksInput) .output(getLocalTasksOutput) @@ -208,7 +181,6 @@ export const workspaceRouter = router({ return result; }), - onTerminalCreated: subscribe(WorkspaceServiceEvent.TerminalCreated), onError: subscribe(WorkspaceServiceEvent.Error), onWarning: subscribe(WorkspaceServiceEvent.Warning), onPromoted: subscribe(WorkspaceServiceEvent.Promoted), diff --git a/apps/code/src/renderer/features/panels/hooks/usePanelLayoutHooks.tsx b/apps/code/src/renderer/features/panels/hooks/usePanelLayoutHooks.tsx index 01af1167c..1f1fa5b1e 100644 --- a/apps/code/src/renderer/features/panels/hooks/usePanelLayoutHooks.tsx +++ b/apps/code/src/renderer/features/panels/hooks/usePanelLayoutHooks.tsx @@ -98,10 +98,7 @@ export function useTabInjection( if (tab.data.type === "file" || tab.data.type === "diff") { const filename = tab.data.relativePath.split("/").pop() || ""; icon = ; - } else if ( - tab.data.type === "terminal" || - tab.data.type === "workspace-terminal" - ) { + } else if (tab.data.type === "terminal") { icon = ; } else if (tab.data.type === "logs") { icon = ; diff --git a/apps/code/src/renderer/features/panels/store/panelLayoutStore.ts b/apps/code/src/renderer/features/panels/store/panelLayoutStore.ts index 63ff44942..9f3aca785 100644 --- a/apps/code/src/renderer/features/panels/store/panelLayoutStore.ts +++ b/apps/code/src/renderer/features/panels/store/panelLayoutStore.ts @@ -133,12 +133,6 @@ export interface PanelLayoutStore { updateTabLabel: (taskId: string, tabId: string, label: string) => void; setFocusedPanel: (taskId: string, panelId: string) => void; addTerminalTab: (taskId: string, panelId: string) => void; - addWorkspaceTerminalTab: ( - taskId: string, - sessionId: string, - command: string, - scriptType: "init" | "start", - ) => void; clearAllLayouts: () => void; } @@ -957,49 +951,6 @@ export const usePanelLayoutStore = createWithEqualityFn()( ); }, - addWorkspaceTerminalTab: (taskId, sessionId, command, scriptType) => { - const tabId = `workspace-terminal-${sessionId}`; - const label = - scriptType === "init" ? `Init: ${command}` : `Start: ${command}`; - - set((state) => - updateTaskLayout(state, taskId, (layout) => { - const existingTab = findTabInTree(layout.panelTree, tabId); - if (existingTab) { - const updatedTree = updateTreeNode( - layout.panelTree, - existingTab.panelId, - (panel) => setActiveTabInPanel(panel, tabId), - ); - return { panelTree: updatedTree }; - } - - const updatedTree = updateTreeNode( - layout.panelTree, - DEFAULT_PANEL_IDS.MAIN_PANEL, - (panel) => { - if (panel.type !== "leaf") return panel; - return addTabToPanel(panel, { - id: tabId, - label, - data: { - type: "workspace-terminal", - sessionId, - command, - scriptType, - }, - component: null, - draggable: true, - closeable: false, - }); - }, - ); - - return { panelTree: updatedTree }; - }), - ); - }, - clearAllLayouts: () => { set({ taskLayouts: {} }); }, diff --git a/apps/code/src/renderer/features/panels/store/panelTypes.ts b/apps/code/src/renderer/features/panels/store/panelTypes.ts index fd8f5ec20..458c9e05f 100644 --- a/apps/code/src/renderer/features/panels/store/panelTypes.ts +++ b/apps/code/src/renderer/features/panels/store/panelTypes.ts @@ -27,12 +27,6 @@ export type TabData = terminalId: string; cwd: string; } - | { - type: "workspace-terminal"; - sessionId: string; - command: string; - scriptType: "init" | "start"; - } | { type: "cloud-diff"; relativePath: string; diff --git a/apps/code/src/renderer/features/task-detail/components/TabContentRenderer.tsx b/apps/code/src/renderer/features/task-detail/components/TabContentRenderer.tsx index 25e42078c..e0f26572a 100644 --- a/apps/code/src/renderer/features/task-detail/components/TabContentRenderer.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TabContentRenderer.tsx @@ -6,7 +6,6 @@ import { ChangesPanel } from "@features/task-detail/components/ChangesPanel"; import { FileTreePanel } from "@features/task-detail/components/FileTreePanel"; import { TaskLogsPanel } from "@features/task-detail/components/TaskLogsPanel"; import { TaskShellPanel } from "@features/task-detail/components/TaskShellPanel"; -import { WorkspaceTerminalPanel } from "@features/workspace/components/WorkspaceTerminalPanel"; import type { Task } from "@shared/types"; interface TabContentRendererProps { @@ -31,15 +30,6 @@ export function TabContentRenderer({ ); - case "workspace-terminal": - return ( - - ); - case "file": return ( {task.title} - {effectiveRepoPath && ( )} ), - [task.title, taskId, effectiveRepoPath], + [task.title, effectiveRepoPath], ); useSetHeaderContent(headerContent); diff --git a/apps/code/src/renderer/features/workspace/components/StartWorkspaceButton.tsx b/apps/code/src/renderer/features/workspace/components/StartWorkspaceButton.tsx deleted file mode 100644 index f1c7d6763..000000000 --- a/apps/code/src/renderer/features/workspace/components/StartWorkspaceButton.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { PlayIcon } from "@phosphor-icons/react"; -import { Button, Tooltip } from "@radix-ui/themes"; -import { useTRPC } from "@renderer/trpc/client"; -import { useMutation } from "@tanstack/react-query"; -import { useCallback, useState } from "react"; -import { toast } from "sonner"; -import { useWorkspace } from "../hooks/useWorkspace"; -import { useWorkspaceStatus } from "../hooks/useWorkspaceStatus"; - -interface StartWorkspaceButtonProps { - taskId: string; -} - -export function StartWorkspaceButton({ taskId }: StartWorkspaceButtonProps) { - const trpcReact = useTRPC(); - const workspace = useWorkspace(taskId); - const runStartScriptsMutation = useMutation( - trpcReact.workspace.runStart.mutationOptions(), - ); - const { isRunning, isCheckingStatus } = useWorkspaceStatus(taskId); - - const [isStarting, setIsStarting] = useState(false); - - const handleStart = useCallback(async () => { - if (!workspace) return; - - setIsStarting(true); - try { - const worktreePath = workspace.worktreePath ?? workspace.folderPath; - const worktreeName = - workspace.worktreeName ?? workspace.folderPath.split("/").pop() ?? ""; - - const result = await runStartScriptsMutation.mutateAsync({ - taskId, - worktreePath, - worktreeName, - }); - - if (!result.success && result.errors?.length) { - toast.error("Start scripts failed", { - description: result.errors.join(", "), - }); - } else if (result.terminalSessionIds.length > 0) { - toast.success("Workspace started", { - description: `${result.terminalSessionIds.length} terminal(s) opened`, - }); - } - } catch (error) { - toast.error("Failed to start workspace", { - description: error instanceof Error ? error.message : String(error), - }); - } finally { - setIsStarting(false); - } - }, [taskId, workspace, runStartScriptsMutation]); - - if (!workspace || !workspace.hasStartScripts) { - return null; - } - - if (isRunning || isCheckingStatus) { - return null; - } - - return ( - - - - ); -} diff --git a/apps/code/src/renderer/features/workspace/components/WorkspaceTerminalPanel.tsx b/apps/code/src/renderer/features/workspace/components/WorkspaceTerminalPanel.tsx deleted file mode 100644 index d0ef839ea..000000000 --- a/apps/code/src/renderer/features/workspace/components/WorkspaceTerminalPanel.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Terminal } from "@features/terminal/components/Terminal"; -import { Box, Flex, Text } from "@radix-ui/themes"; -import { useThemeStore } from "@stores/themeStore"; -import { useCallback } from "react"; -import { useWorkspaceTerminalStore } from "../stores/workspaceTerminalStore"; - -interface WorkspaceTerminalPanelProps { - sessionId: string; - command: string; - scriptType: "init" | "start"; -} - -export function WorkspaceTerminalPanel({ - sessionId, - command, - scriptType, -}: WorkspaceTerminalPanelProps) { - const isDarkMode = useThemeStore((state) => state.isDarkMode); - const updateTerminalStatus = useWorkspaceTerminalStore( - (s) => s.updateTerminalStatus, - ); - - const handleExit = useCallback(() => { - updateTerminalStatus(sessionId, "completed"); - }, [sessionId, updateTerminalStatus]); - - return ( - - - - {scriptType === "init" ? "Init" : "Start"}: - - - {command} - - - - - - - ); -} diff --git a/apps/code/src/renderer/features/workspace/hooks/useWorkspace.ts b/apps/code/src/renderer/features/workspace/hooks/useWorkspace.ts index 54229c6b3..d4f52225f 100644 --- a/apps/code/src/renderer/features/workspace/hooks/useWorkspace.ts +++ b/apps/code/src/renderer/features/workspace/hooks/useWorkspace.ts @@ -87,19 +87,6 @@ export function useDeleteWorkspace(): { isPending: boolean } { return { isPending: mutation.isPending }; } -export function useRunStartScripts(): { - mutateAsync: (input: { - taskId: string; - worktreePath: string; - worktreeName: string; - }) => Promise<{ success: boolean; terminalSessionIds: string[] }>; - isPending: boolean; -} { - const trpcReact = useTRPC(); - const mutation = useMutation(trpcReact.workspace.runStart.mutationOptions()); - return { mutateAsync: mutation.mutateAsync, isPending: mutation.isPending }; -} - export function useEnsureWorkspace(): { ensureWorkspace: ( taskId: string, diff --git a/apps/code/src/renderer/features/workspace/hooks/useWorkspaceEvents.ts b/apps/code/src/renderer/features/workspace/hooks/useWorkspaceEvents.ts index dcf618dbc..264c66b77 100644 --- a/apps/code/src/renderer/features/workspace/hooks/useWorkspaceEvents.ts +++ b/apps/code/src/renderer/features/workspace/hooks/useWorkspaceEvents.ts @@ -1,40 +1,9 @@ -import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore"; import { trpcClient } from "@renderer/trpc/client"; import { toast } from "@utils/toast"; import { useEffect } from "react"; -import { useWorkspaceTerminalStore } from "../stores/workspaceTerminalStore"; export function useWorkspaceEvents(taskId: string) { - const addWorkspaceTerminalTab = usePanelLayoutStore( - (s) => s.addWorkspaceTerminalTab, - ); - const registerTerminal = useWorkspaceTerminalStore((s) => s.registerTerminal); - useEffect(() => { - const terminalSubscription = - trpcClient.workspace.onTerminalCreated.subscribe(undefined, { - onData: (data) => { - if (data.taskId !== taskId) return; - - registerTerminal(taskId, { - sessionId: data.sessionId, - scriptType: data.scriptType, - command: data.command, - label: data.label, - status: data.status, - }); - - addWorkspaceTerminalTab( - taskId, - data.sessionId, - data.command, - data.scriptType, - ); - }, - }); - - // Note: workspace errors are handled globally in App.tsx - const warningSubscription = trpcClient.workspace.onWarning.subscribe( undefined, { @@ -49,8 +18,7 @@ export function useWorkspaceEvents(taskId: string) { ); return () => { - terminalSubscription.unsubscribe(); warningSubscription.unsubscribe(); }; - }, [taskId, addWorkspaceTerminalTab, registerTerminal]); + }, [taskId]); } diff --git a/apps/code/src/renderer/features/workspace/hooks/useWorkspaceStatus.ts b/apps/code/src/renderer/features/workspace/hooks/useWorkspaceStatus.ts deleted file mode 100644 index 223b3df7a..000000000 --- a/apps/code/src/renderer/features/workspace/hooks/useWorkspaceStatus.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useTRPC } from "@renderer/trpc"; -import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect, useState } from "react"; -import { useWorkspaceTerminalStore } from "../stores/workspaceTerminalStore"; -import { useCreateWorkspace, useWorkspace } from "./useWorkspace"; - -interface WorkspaceStatus { - hasWorkspace: boolean; - isRunning: boolean; - isCreating: boolean; - isCheckingStatus: boolean; -} - -export function useWorkspaceStatus(taskId: string): WorkspaceStatus { - const trpcReact = useTRPC(); - const workspace = useWorkspace(taskId); - const { isPending: isCreating } = useCreateWorkspace(); - const terminalsRunning = useWorkspaceTerminalStore((s) => - s.areTerminalsRunning(taskId), - ); - - const [isCheckingStatus, setIsCheckingStatus] = useState(false); - const [isRunning, setIsRunning] = useState(terminalsRunning); - - const queryClient = useQueryClient(); - - const checkStatus = useCallback(async () => { - setIsCheckingStatus(true); - try { - const isRunning = await queryClient.fetchQuery( - trpcReact.workspace.isRunning.queryOptions({ taskId }), - ); - setIsRunning(isRunning); - } catch { - setIsRunning(false); - } finally { - setIsCheckingStatus(false); - } - }, [taskId, queryClient, trpcReact]); - - useEffect(() => { - if (workspace) { - checkStatus(); - } - }, [workspace, checkStatus]); - - return { - hasWorkspace: !!workspace, - isRunning: isRunning || terminalsRunning, - isCreating, - isCheckingStatus, - }; -} diff --git a/apps/code/src/renderer/features/workspace/stores/workspaceTerminalStore.ts b/apps/code/src/renderer/features/workspace/stores/workspaceTerminalStore.ts deleted file mode 100644 index d81c077d2..000000000 --- a/apps/code/src/renderer/features/workspace/stores/workspaceTerminalStore.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { WorkspaceTerminalInfo } from "@main/services/workspace/schemas"; -import { omitKey } from "@renderer/utils/object"; -import { create } from "zustand"; -import { persist } from "zustand/middleware"; - -interface WorkspaceTerminalState { - workspaceTerminals: Record; - terminalInfo: Record; - - registerTerminal: ( - taskId: string, - terminalInfo: WorkspaceTerminalInfo, - ) => void; - updateTerminalStatus: ( - sessionId: string, - status: "running" | "completed" | "failed", - exitCode?: number, - ) => void; - clearWorkspaceTerminals: (taskId: string) => void; - getTerminalsForTask: (taskId: string) => WorkspaceTerminalInfo[]; - areTerminalsRunning: (taskId: string) => boolean; -} - -export const useWorkspaceTerminalStore = create()( - persist( - (set, get) => ({ - workspaceTerminals: {}, - terminalInfo: {}, - - registerTerminal: (taskId: string, info: WorkspaceTerminalInfo) => { - set((state) => { - const existingSessions = state.workspaceTerminals[taskId] || []; - return { - workspaceTerminals: { - ...state.workspaceTerminals, - [taskId]: [...existingSessions, info.sessionId], - }, - terminalInfo: { - ...state.terminalInfo, - [info.sessionId]: info, - }, - }; - }); - }, - - updateTerminalStatus: ( - sessionId: string, - status: "running" | "completed" | "failed", - exitCode?: number, - ) => { - set((state) => { - const existing = state.terminalInfo[sessionId]; - if (!existing) return state; - - return { - terminalInfo: { - ...state.terminalInfo, - [sessionId]: { - ...existing, - status, - exitCode, - }, - }, - }; - }); - }, - - clearWorkspaceTerminals: (taskId: string) => { - set((state) => { - const sessionsToRemove = state.workspaceTerminals[taskId] || []; - let newTerminalInfo = state.terminalInfo; - for (const sessionId of sessionsToRemove) { - newTerminalInfo = omitKey(newTerminalInfo, sessionId); - } - - return { - workspaceTerminals: omitKey(state.workspaceTerminals, taskId), - terminalInfo: newTerminalInfo, - }; - }); - }, - - getTerminalsForTask: (taskId: string) => { - const state = get(); - const sessionIds = state.workspaceTerminals[taskId] || []; - return sessionIds - .map((id) => state.terminalInfo[id]) - .filter((info): info is WorkspaceTerminalInfo => !!info); - }, - - areTerminalsRunning: (taskId: string) => { - const terminals = get().getTerminalsForTask(taskId); - return terminals.some((t) => t.status === "running"); - }, - }), - { - name: "workspace-terminal-store", - partialize: (state) => ({ - workspaceTerminals: state.workspaceTerminals, - terminalInfo: state.terminalInfo, - }), - }, - ), -); diff --git a/apps/code/src/renderer/sagas/task/task-creation.ts b/apps/code/src/renderer/sagas/task/task-creation.ts index b9baa97db..c8a70a84d 100644 --- a/apps/code/src/renderer/sagas/task/task-creation.ts +++ b/apps/code/src/renderer/sagas/task/task-creation.ts @@ -177,8 +177,6 @@ export class TaskCreationSaga extends Saga< baseBranch: workspaceInfo.worktree?.baseBranch ?? null, createdAt: workspaceInfo.worktree?.createdAt ?? new Date().toISOString(), - terminalSessionIds: workspaceInfo.terminalSessionIds, - hasStartScripts: workspaceInfo.hasStartScripts, }; } else if (workspaceMode === "cloud") { await this.step({ @@ -214,8 +212,6 @@ export class TaskCreationSaga extends Saga< branchName: null, baseBranch: branch, createdAt: new Date().toISOString(), - terminalSessionIds: [], - hasStartScripts: false, }; }