2026-03-28 16:15:20 -05:00
import { execFile } from "node:child_process" ;
import fs from "node:fs/promises" ;
import path from "node:path" ;
import { promisify } from "node:util" ;
2026-03-13 17:12:25 -05:00
import { and , desc , eq , inArray } from "drizzle-orm" ;
import type { Db } from "@paperclipai/db" ;
2026-03-28 16:15:20 -05:00
import { executionWorkspaces , issues , projects , projectWorkspaces , workspaceRuntimeServices } from "@paperclipai/db" ;
import type {
ExecutionWorkspace ,
[codex] improve issue and routine UI responsiveness (#3744)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Operators rely on issue, inbox, and routine views to understand what
the company is doing in real time
> - Those views need to stay fast and readable even when issue lists,
markdown comments, and run metadata get large
> - The current branch had a coherent set of UI and live-update
improvements spread across issue search, issue detail rendering, routine
affordances, and workspace lookups
> - This pull request groups those board-facing changes into one
standalone branch that can merge independently of the heartbeat/runtime
work
> - The benefit is a faster, clearer issue and routine workflow without
changing the underlying task model
## What Changed
- Show routine execution issues by default and rename the filter to
`Hide routine runs` so the default state no longer looks like an active
filter.
- Show the routine name in the run dialog and tighten the issue
properties pane with a workspace link, copy-on-click behavior, and an
inline parent arrow.
- Reduce issue detail rerenders, keep queued issue chat mounted, improve
issues page search responsiveness, and speed up issues first paint.
- Add inbox "other search results", refresh visible issue runs after
status updates, and optimize workspace lookups through summary-mode
execution workspace queries.
- Improve markdown wrapping and scrolling behavior for long strings and
self-comment code blocks.
- Relax the markdown sanitizer assertion so the test still validates
safety after the new wrap-friendly inline styles.
## Verification
- `pnpm vitest run ui/src/components/IssuesList.test.tsx
ui/src/lib/inbox.test.ts ui/src/pages/Issues.test.tsx
ui/src/context/BreadcrumbContext.test.tsx
ui/src/context/LiveUpdatesProvider.test.ts
ui/src/components/MarkdownBody.test.tsx
ui/src/api/execution-workspaces.test.ts
server/src/__tests__/execution-workspaces-routes.test.ts`
## Risks
- This touches several issue-facing UI surfaces at once, so regressions
would most likely show up as stale rendering, search result mismatches,
or small markdown presentation differences.
- The workspace lookup optimization depends on the summary-mode route
shape staying aligned between server and UI.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 15:54:05 -05:00
ExecutionWorkspaceSummary ,
2026-03-28 16:15:20 -05:00
ExecutionWorkspaceCloseAction ,
ExecutionWorkspaceCloseGitReadiness ,
ExecutionWorkspaceCloseReadiness ,
ExecutionWorkspaceConfig ,
WorkspaceRuntimeService ,
} from "@paperclipai/shared" ;
import { parseProjectExecutionWorkspacePolicy } from "./execution-workspace-policy.js" ;
2026-04-04 13:15:46 -05:00
import {
listCurrentRuntimeServicesForExecutionWorkspaces ,
listCurrentRuntimeServicesForProjectWorkspaces ,
} from "./workspace-runtime-read-model.js" ;
2026-03-13 17:12:25 -05:00
type ExecutionWorkspaceRow = typeof executionWorkspaces . $inferSelect ;
2026-03-28 12:15:34 -05:00
type WorkspaceRuntimeServiceRow = typeof workspaceRuntimeServices . $inferSelect ;
2026-03-28 16:15:20 -05:00
const execFileAsync = promisify ( execFile ) ;
const TERMINAL_ISSUE_STATUSES = new Set ( [ "done" , "cancelled" ] ) ;
2026-03-13 17:12:25 -05:00
2026-03-28 12:15:34 -05:00
function isRecord ( value : unknown ) : value is Record < string , unknown > {
return typeof value === "object" && value !== null && ! Array . isArray ( value ) ;
}
function readNullableString ( value : unknown ) : string | null {
if ( typeof value !== "string" ) return null ;
const trimmed = value . trim ( ) ;
return trimmed . length > 0 ? trimmed : null ;
}
function cloneRecord ( value : unknown ) : Record < string , unknown > | null {
if ( ! isRecord ( value ) ) return null ;
return { . . . value } ;
}
2026-03-28 16:15:20 -05:00
async function pathExists ( value : string | null | undefined ) {
if ( ! value ) return false ;
try {
await fs . access ( value ) ;
return true ;
} catch {
return false ;
}
}
async function runGit ( args : string [ ] , cwd : string ) {
return await execFileAsync ( "git" , [ "-C" , cwd , . . . args ] , { cwd } ) ;
}
async function inspectGitCloseReadiness ( workspace : ExecutionWorkspace ) : Promise < {
git : ExecutionWorkspaceCloseGitReadiness | null ;
warnings : string [ ] ;
} > {
const warnings : string [ ] = [ ] ;
const workspacePath = readNullableString ( workspace . providerRef ) ? ? readNullableString ( workspace . cwd ) ;
const createdByRuntime = workspace . metadata ? . createdByRuntime === true ;
const expectsGitInspection =
workspace . providerType === "git_worktree" ||
Boolean ( workspace . repoUrl || workspace . baseRef || workspace . branchName || workspacePath ) ;
if ( ! expectsGitInspection ) {
return { git : null , warnings } ;
}
if ( ! workspacePath ) {
warnings . push ( "Workspace has no local path, so Paperclip cannot inspect git status before close." ) ;
return { git : null , warnings } ;
}
if ( ! ( await pathExists ( workspacePath ) ) ) {
warnings . push ( ` Workspace path " ${ workspacePath } " does not exist, so Paperclip cannot inspect git status before close. ` ) ;
return {
git : {
repoRoot : null ,
workspacePath ,
branchName : workspace.branchName ,
baseRef : workspace.baseRef ,
hasDirtyTrackedFiles : false ,
hasUntrackedFiles : false ,
dirtyEntryCount : 0 ,
untrackedEntryCount : 0 ,
aheadCount : null ,
behindCount : null ,
isMergedIntoBase : null ,
createdByRuntime ,
} ,
warnings ,
} ;
}
let repoRoot : string | null = null ;
try {
repoRoot = ( await runGit ( [ "rev-parse" , "--show-toplevel" ] , workspacePath ) ) . stdout . trim ( ) || null ;
} catch ( error ) {
warnings . push (
` Could not inspect git status for " ${ workspacePath } ": ${ error instanceof Error ? error.message : String ( error ) } ` ,
) ;
}
let branchName = workspace . branchName ;
if ( repoRoot && ! branchName ) {
try {
branchName = ( await runGit ( [ "rev-parse" , "--abbrev-ref" , "HEAD" ] , workspacePath ) ) . stdout . trim ( ) || null ;
} catch {
branchName = workspace . branchName ;
}
}
let dirtyEntryCount = 0 ;
let untrackedEntryCount = 0 ;
if ( repoRoot ) {
try {
const statusOutput = ( await runGit ( [ "status" , "--porcelain=v1" , "--untracked-files=all" ] , workspacePath ) ) . stdout ;
for ( const line of statusOutput . split ( /\r?\n/ ) ) {
if ( ! line ) continue ;
if ( line . startsWith ( "??" ) ) {
untrackedEntryCount += 1 ;
continue ;
}
dirtyEntryCount += 1 ;
}
} catch ( error ) {
warnings . push (
` Could not read git working tree status for " ${ workspacePath } ": ${ error instanceof Error ? error.message : String ( error ) } ` ,
) ;
}
}
let aheadCount : number | null = null ;
let behindCount : number | null = null ;
let isMergedIntoBase : boolean | null = null ;
const baseRef = workspace . baseRef ;
if ( repoRoot && baseRef ) {
try {
const counts = ( await runGit ( [ "rev-list" , "--left-right" , "--count" , ` ${ baseRef } ...HEAD ` ] , workspacePath ) ) . stdout . trim ( ) ;
const [ behindRaw , aheadRaw ] = counts . split ( /\s+/ ) ;
behindCount = behindRaw ? Number . parseInt ( behindRaw , 10 ) : 0 ;
aheadCount = aheadRaw ? Number . parseInt ( aheadRaw , 10 ) : 0 ;
} catch ( error ) {
warnings . push (
` Could not compare this workspace against ${ baseRef } : ${ error instanceof Error ? error.message : String ( error ) } ` ,
) ;
}
try {
await runGit ( [ "merge-base" , "--is-ancestor" , "HEAD" , baseRef ] , workspacePath ) ;
isMergedIntoBase = true ;
} catch ( error ) {
const code = typeof error === "object" && error && "code" in error ? ( error as { code? : unknown } ) . code : null ;
if ( code === 1 ) isMergedIntoBase = false ;
else {
warnings . push (
` Could not determine whether this workspace is merged into ${ baseRef } : ${ error instanceof Error ? error.message : String ( error ) } ` ,
) ;
}
}
}
return {
git : {
repoRoot ,
workspacePath ,
branchName ,
baseRef ,
hasDirtyTrackedFiles : dirtyEntryCount > 0 ,
hasUntrackedFiles : untrackedEntryCount > 0 ,
dirtyEntryCount ,
untrackedEntryCount ,
aheadCount ,
behindCount ,
isMergedIntoBase ,
createdByRuntime ,
} ,
warnings ,
} ;
}
2026-03-28 12:15:34 -05:00
export function readExecutionWorkspaceConfig ( metadata : Record < string , unknown > | null | undefined ) : ExecutionWorkspaceConfig | null {
const raw = isRecord ( metadata ? . config ) ? metadata.config : null ;
if ( ! raw ) return null ;
const config : ExecutionWorkspaceConfig = {
provisionCommand : readNullableString ( raw . provisionCommand ) ,
teardownCommand : readNullableString ( raw . teardownCommand ) ,
cleanupCommand : readNullableString ( raw . cleanupCommand ) ,
workspaceRuntime : cloneRecord ( raw . workspaceRuntime ) ,
2026-03-28 16:46:43 -05:00
desiredState : raw.desiredState === "running" || raw . desiredState === "stopped" ? raw.desiredState : null ,
[codex] Improve workspace runtime and navigation ergonomics (#3680)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - That operator experience depends not just on issue chat, but also on
how workspaces, inbox groups, and navigation state behave over
long-running sessions
> - The current branch included a separate cluster of workspace-runtime
controls, inbox grouping, sidebar ordering, and worktree lifecycle fixes
> - Those changes cross server, shared contracts, database state, and UI
navigation, but they still form one coherent operator workflow area
> - This pull request isolates the workspace/runtime and navigation
ergonomics work into one standalone branch
> - The benefit is better workspace recovery and navigation persistence
without forcing reviewers through the unrelated issue-detail/chat work
## What Changed
- Improved execution workspace and project workspace controls, request
wiring, layout, and JSON editor ergonomics
- Hardened linked worktree reuse/startup behavior and documented the
`worktree repair` flow for recovering linked worktrees safely
- Added inbox workspace grouping, mobile collapse, archive undo,
keyboard navigation, shared group-header styling, and persisted
collapsed-group behavior
- Added persistent sidebar order preferences with the supporting DB
migration, shared/server contracts, routes, services, hooks, and UI
integration
- Scoped issue-list preferences by context and added targeted UI/server
tests for workspace controls, inbox behavior, sidebar preferences, and
worktree validation
## Verification
- `pnpm vitest run
server/src/__tests__/sidebar-preferences-routes.test.ts
ui/src/pages/Inbox.test.tsx
ui/src/components/ProjectWorkspaceSummaryCard.test.tsx
ui/src/components/WorkspaceRuntimeControls.test.tsx
ui/src/api/workspace-runtime-control.test.ts`
- `server/src/__tests__/workspace-runtime.test.ts` was attempted, but
the embedded Postgres suite self-skipped/hung on this host after
reporting an init-script issue, so it is not counted as a local pass
here
## Risks
- Medium: this branch includes migration-backed preference storage plus
worktree/runtime behavior, so merge review should pay attention to state
persistence and worktree recovery semantics
- The sidebar preference migration is standalone, but it should still be
watched for conflicts if another migration lands first
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-14 12:57:11 -05:00
serviceStates : isRecord ( raw . serviceStates )
? Object . fromEntries (
Object . entries ( raw . serviceStates ) . filter ( ( [ , state ] ) = > state === "running" || state === "stopped" ) ,
) as ExecutionWorkspaceConfig [ "serviceStates" ]
: null ,
2026-03-28 12:15:34 -05:00
} ;
const hasConfig = Object . values ( config ) . some ( ( value ) = > {
if ( value === null ) return false ;
if ( typeof value === "object" ) return Object . keys ( value ) . length > 0 ;
return true ;
} ) ;
return hasConfig ? config : null ;
}
export function mergeExecutionWorkspaceConfig (
metadata : Record < string , unknown > | null | undefined ,
patch : Partial < ExecutionWorkspaceConfig > | null ,
) : Record < string , unknown > | null {
const nextMetadata = isRecord ( metadata ) ? { . . . metadata } : { } ;
const current = readExecutionWorkspaceConfig ( metadata ) ? ? {
provisionCommand : null ,
teardownCommand : null ,
cleanupCommand : null ,
workspaceRuntime : null ,
2026-03-28 16:46:43 -05:00
desiredState : null ,
[codex] Improve workspace runtime and navigation ergonomics (#3680)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - That operator experience depends not just on issue chat, but also on
how workspaces, inbox groups, and navigation state behave over
long-running sessions
> - The current branch included a separate cluster of workspace-runtime
controls, inbox grouping, sidebar ordering, and worktree lifecycle fixes
> - Those changes cross server, shared contracts, database state, and UI
navigation, but they still form one coherent operator workflow area
> - This pull request isolates the workspace/runtime and navigation
ergonomics work into one standalone branch
> - The benefit is better workspace recovery and navigation persistence
without forcing reviewers through the unrelated issue-detail/chat work
## What Changed
- Improved execution workspace and project workspace controls, request
wiring, layout, and JSON editor ergonomics
- Hardened linked worktree reuse/startup behavior and documented the
`worktree repair` flow for recovering linked worktrees safely
- Added inbox workspace grouping, mobile collapse, archive undo,
keyboard navigation, shared group-header styling, and persisted
collapsed-group behavior
- Added persistent sidebar order preferences with the supporting DB
migration, shared/server contracts, routes, services, hooks, and UI
integration
- Scoped issue-list preferences by context and added targeted UI/server
tests for workspace controls, inbox behavior, sidebar preferences, and
worktree validation
## Verification
- `pnpm vitest run
server/src/__tests__/sidebar-preferences-routes.test.ts
ui/src/pages/Inbox.test.tsx
ui/src/components/ProjectWorkspaceSummaryCard.test.tsx
ui/src/components/WorkspaceRuntimeControls.test.tsx
ui/src/api/workspace-runtime-control.test.ts`
- `server/src/__tests__/workspace-runtime.test.ts` was attempted, but
the embedded Postgres suite self-skipped/hung on this host after
reporting an init-script issue, so it is not counted as a local pass
here
## Risks
- Medium: this branch includes migration-backed preference storage plus
worktree/runtime behavior, so merge review should pay attention to state
persistence and worktree recovery semantics
- The sidebar preference migration is standalone, but it should still be
watched for conflicts if another migration lands first
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-14 12:57:11 -05:00
serviceStates : null ,
2026-03-28 12:15:34 -05:00
} ;
if ( patch === null ) {
delete nextMetadata . config ;
return Object . keys ( nextMetadata ) . length > 0 ? nextMetadata : null ;
}
const nextConfig : ExecutionWorkspaceConfig = {
provisionCommand : patch.provisionCommand !== undefined ? readNullableString ( patch . provisionCommand ) : current . provisionCommand ,
teardownCommand : patch.teardownCommand !== undefined ? readNullableString ( patch . teardownCommand ) : current . teardownCommand ,
cleanupCommand : patch.cleanupCommand !== undefined ? readNullableString ( patch . cleanupCommand ) : current . cleanupCommand ,
workspaceRuntime : patch.workspaceRuntime !== undefined ? cloneRecord ( patch . workspaceRuntime ) : current . workspaceRuntime ,
2026-03-28 16:46:43 -05:00
desiredState :
patch . desiredState !== undefined
? patch . desiredState === "running" || patch . desiredState === "stopped"
? patch . desiredState
: null
: current . desiredState ,
[codex] Improve workspace runtime and navigation ergonomics (#3680)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - That operator experience depends not just on issue chat, but also on
how workspaces, inbox groups, and navigation state behave over
long-running sessions
> - The current branch included a separate cluster of workspace-runtime
controls, inbox grouping, sidebar ordering, and worktree lifecycle fixes
> - Those changes cross server, shared contracts, database state, and UI
navigation, but they still form one coherent operator workflow area
> - This pull request isolates the workspace/runtime and navigation
ergonomics work into one standalone branch
> - The benefit is better workspace recovery and navigation persistence
without forcing reviewers through the unrelated issue-detail/chat work
## What Changed
- Improved execution workspace and project workspace controls, request
wiring, layout, and JSON editor ergonomics
- Hardened linked worktree reuse/startup behavior and documented the
`worktree repair` flow for recovering linked worktrees safely
- Added inbox workspace grouping, mobile collapse, archive undo,
keyboard navigation, shared group-header styling, and persisted
collapsed-group behavior
- Added persistent sidebar order preferences with the supporting DB
migration, shared/server contracts, routes, services, hooks, and UI
integration
- Scoped issue-list preferences by context and added targeted UI/server
tests for workspace controls, inbox behavior, sidebar preferences, and
worktree validation
## Verification
- `pnpm vitest run
server/src/__tests__/sidebar-preferences-routes.test.ts
ui/src/pages/Inbox.test.tsx
ui/src/components/ProjectWorkspaceSummaryCard.test.tsx
ui/src/components/WorkspaceRuntimeControls.test.tsx
ui/src/api/workspace-runtime-control.test.ts`
- `server/src/__tests__/workspace-runtime.test.ts` was attempted, but
the embedded Postgres suite self-skipped/hung on this host after
reporting an init-script issue, so it is not counted as a local pass
here
## Risks
- Medium: this branch includes migration-backed preference storage plus
worktree/runtime behavior, so merge review should pay attention to state
persistence and worktree recovery semantics
- The sidebar preference migration is standalone, but it should still be
watched for conflicts if another migration lands first
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-14 12:57:11 -05:00
serviceStates :
patch . serviceStates !== undefined && isRecord ( patch . serviceStates )
? Object . fromEntries (
Object . entries ( patch . serviceStates ) . filter ( ( [ , state ] ) = > state === "running" || state === "stopped" ) ,
) as ExecutionWorkspaceConfig [ "serviceStates" ]
: patch . serviceStates !== undefined
? null
: current . serviceStates ,
2026-03-28 12:15:34 -05:00
} ;
const hasConfig = Object . values ( nextConfig ) . some ( ( value ) = > {
if ( value === null ) return false ;
if ( typeof value === "object" ) return Object . keys ( value ) . length > 0 ;
return true ;
} ) ;
if ( hasConfig ) {
nextMetadata . config = {
provisionCommand : nextConfig.provisionCommand ,
teardownCommand : nextConfig.teardownCommand ,
cleanupCommand : nextConfig.cleanupCommand ,
workspaceRuntime : nextConfig.workspaceRuntime ,
2026-03-28 16:46:43 -05:00
desiredState : nextConfig.desiredState ,
[codex] Improve workspace runtime and navigation ergonomics (#3680)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - That operator experience depends not just on issue chat, but also on
how workspaces, inbox groups, and navigation state behave over
long-running sessions
> - The current branch included a separate cluster of workspace-runtime
controls, inbox grouping, sidebar ordering, and worktree lifecycle fixes
> - Those changes cross server, shared contracts, database state, and UI
navigation, but they still form one coherent operator workflow area
> - This pull request isolates the workspace/runtime and navigation
ergonomics work into one standalone branch
> - The benefit is better workspace recovery and navigation persistence
without forcing reviewers through the unrelated issue-detail/chat work
## What Changed
- Improved execution workspace and project workspace controls, request
wiring, layout, and JSON editor ergonomics
- Hardened linked worktree reuse/startup behavior and documented the
`worktree repair` flow for recovering linked worktrees safely
- Added inbox workspace grouping, mobile collapse, archive undo,
keyboard navigation, shared group-header styling, and persisted
collapsed-group behavior
- Added persistent sidebar order preferences with the supporting DB
migration, shared/server contracts, routes, services, hooks, and UI
integration
- Scoped issue-list preferences by context and added targeted UI/server
tests for workspace controls, inbox behavior, sidebar preferences, and
worktree validation
## Verification
- `pnpm vitest run
server/src/__tests__/sidebar-preferences-routes.test.ts
ui/src/pages/Inbox.test.tsx
ui/src/components/ProjectWorkspaceSummaryCard.test.tsx
ui/src/components/WorkspaceRuntimeControls.test.tsx
ui/src/api/workspace-runtime-control.test.ts`
- `server/src/__tests__/workspace-runtime.test.ts` was attempted, but
the embedded Postgres suite self-skipped/hung on this host after
reporting an init-script issue, so it is not counted as a local pass
here
## Risks
- Medium: this branch includes migration-backed preference storage plus
worktree/runtime behavior, so merge review should pay attention to state
persistence and worktree recovery semantics
- The sidebar preference migration is standalone, but it should still be
watched for conflicts if another migration lands first
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-14 12:57:11 -05:00
serviceStates : nextConfig.serviceStates ? ? null ,
2026-03-28 12:15:34 -05:00
} ;
} else {
delete nextMetadata . config ;
}
return Object . keys ( nextMetadata ) . length > 0 ? nextMetadata : null ;
}
function toRuntimeService ( row : WorkspaceRuntimeServiceRow ) : WorkspaceRuntimeService {
return {
id : row.id ,
companyId : row.companyId ,
projectId : row.projectId ? ? null ,
projectWorkspaceId : row.projectWorkspaceId ? ? null ,
executionWorkspaceId : row.executionWorkspaceId ? ? null ,
issueId : row.issueId ? ? null ,
scopeType : row.scopeType as WorkspaceRuntimeService [ "scopeType" ] ,
scopeId : row.scopeId ? ? null ,
serviceName : row.serviceName ,
status : row.status as WorkspaceRuntimeService [ "status" ] ,
lifecycle : row.lifecycle as WorkspaceRuntimeService [ "lifecycle" ] ,
reuseKey : row.reuseKey ? ? null ,
command : row.command ? ? null ,
cwd : row.cwd ? ? null ,
port : row.port ? ? null ,
url : row.url ? ? null ,
provider : row.provider as WorkspaceRuntimeService [ "provider" ] ,
providerRef : row.providerRef ? ? null ,
ownerAgentId : row.ownerAgentId ? ? null ,
startedByRunId : row.startedByRunId ? ? null ,
lastUsedAt : row.lastUsedAt ,
startedAt : row.startedAt ,
stoppedAt : row.stoppedAt ? ? null ,
stopPolicy : ( row . stopPolicy as Record < string , unknown > | null ) ? ? null ,
healthStatus : row.healthStatus as WorkspaceRuntimeService [ "healthStatus" ] ,
createdAt : row.createdAt ,
updatedAt : row.updatedAt ,
} ;
}
function toExecutionWorkspace (
row : ExecutionWorkspaceRow ,
runtimeServices : WorkspaceRuntimeService [ ] = [ ] ,
) : ExecutionWorkspace {
2026-03-13 17:12:25 -05:00
return {
id : row.id ,
companyId : row.companyId ,
projectId : row.projectId ,
projectWorkspaceId : row.projectWorkspaceId ? ? null ,
sourceIssueId : row.sourceIssueId ? ? null ,
mode : row.mode as ExecutionWorkspace [ "mode" ] ,
strategyType : row.strategyType as ExecutionWorkspace [ "strategyType" ] ,
name : row.name ,
status : row.status as ExecutionWorkspace [ "status" ] ,
cwd : row.cwd ? ? null ,
repoUrl : row.repoUrl ? ? null ,
baseRef : row.baseRef ? ? null ,
branchName : row.branchName ? ? null ,
providerType : row.providerType as ExecutionWorkspace [ "providerType" ] ,
providerRef : row.providerRef ? ? null ,
derivedFromExecutionWorkspaceId : row.derivedFromExecutionWorkspaceId ? ? null ,
lastUsedAt : row.lastUsedAt ,
openedAt : row.openedAt ,
closedAt : row.closedAt ? ? null ,
cleanupEligibleAt : row.cleanupEligibleAt ? ? null ,
cleanupReason : row.cleanupReason ? ? null ,
2026-03-28 12:15:34 -05:00
config : readExecutionWorkspaceConfig ( ( row . metadata as Record < string , unknown > | null ) ? ? null ) ,
2026-03-13 17:12:25 -05:00
metadata : ( row . metadata as Record < string , unknown > | null ) ? ? null ,
2026-03-28 12:15:34 -05:00
runtimeServices ,
2026-03-13 17:12:25 -05:00
createdAt : row.createdAt ,
updatedAt : row.updatedAt ,
} ;
}
[codex] improve issue and routine UI responsiveness (#3744)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Operators rely on issue, inbox, and routine views to understand what
the company is doing in real time
> - Those views need to stay fast and readable even when issue lists,
markdown comments, and run metadata get large
> - The current branch had a coherent set of UI and live-update
improvements spread across issue search, issue detail rendering, routine
affordances, and workspace lookups
> - This pull request groups those board-facing changes into one
standalone branch that can merge independently of the heartbeat/runtime
work
> - The benefit is a faster, clearer issue and routine workflow without
changing the underlying task model
## What Changed
- Show routine execution issues by default and rename the filter to
`Hide routine runs` so the default state no longer looks like an active
filter.
- Show the routine name in the run dialog and tighten the issue
properties pane with a workspace link, copy-on-click behavior, and an
inline parent arrow.
- Reduce issue detail rerenders, keep queued issue chat mounted, improve
issues page search responsiveness, and speed up issues first paint.
- Add inbox "other search results", refresh visible issue runs after
status updates, and optimize workspace lookups through summary-mode
execution workspace queries.
- Improve markdown wrapping and scrolling behavior for long strings and
self-comment code blocks.
- Relax the markdown sanitizer assertion so the test still validates
safety after the new wrap-friendly inline styles.
## Verification
- `pnpm vitest run ui/src/components/IssuesList.test.tsx
ui/src/lib/inbox.test.ts ui/src/pages/Issues.test.tsx
ui/src/context/BreadcrumbContext.test.tsx
ui/src/context/LiveUpdatesProvider.test.ts
ui/src/components/MarkdownBody.test.tsx
ui/src/api/execution-workspaces.test.ts
server/src/__tests__/execution-workspaces-routes.test.ts`
## Risks
- This touches several issue-facing UI surfaces at once, so regressions
would most likely show up as stale rendering, search result mismatches,
or small markdown presentation differences.
- The workspace lookup optimization depends on the summary-mode route
shape staying aligned between server and UI.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 15:54:05 -05:00
function toExecutionWorkspaceSummary ( row : Pick < ExecutionWorkspaceRow , "id" | "name" | "mode" | "projectWorkspaceId" > ) : ExecutionWorkspaceSummary {
return {
id : row.id ,
name : row.name ,
mode : row.mode as ExecutionWorkspaceSummary [ "mode" ] ,
projectWorkspaceId : row.projectWorkspaceId ? ? null ,
} ;
}
2026-04-04 13:15:46 -05:00
function usesInheritedProjectRuntimeServices ( row : ExecutionWorkspaceRow ) {
if ( row . mode !== "shared_workspace" || ! row . projectWorkspaceId ) return false ;
return ! readExecutionWorkspaceConfig ( ( row . metadata as Record < string , unknown > | null ) ? ? null ) ? . workspaceRuntime ;
}
async function loadEffectiveRuntimeServicesByExecutionWorkspace (
db : Db ,
companyId : string ,
rows : ExecutionWorkspaceRow [ ] ,
) {
const executionRuntimeServices = await listCurrentRuntimeServicesForExecutionWorkspaces (
db ,
companyId ,
rows . map ( ( row ) = > row . id ) ,
) ;
const projectWorkspaceIds = rows
. filter ( ( row ) = > usesInheritedProjectRuntimeServices ( row ) )
. map ( ( row ) = > row . projectWorkspaceId )
. filter ( ( value ) : value is string = > Boolean ( value ) ) ;
const projectRuntimeServices = await listCurrentRuntimeServicesForProjectWorkspaces (
db ,
companyId ,
[ . . . new Set ( projectWorkspaceIds ) ] ,
) ;
return new Map (
rows . map ( ( row ) = > [
row . id ,
usesInheritedProjectRuntimeServices ( row )
? ( projectRuntimeServices . get ( row . projectWorkspaceId ! ) ? ? [ ] )
: ( executionRuntimeServices . get ( row . id ) ? ? [ ] ) ,
] ) ,
) ;
}
2026-03-13 17:12:25 -05:00
export function executionWorkspaceService ( db : Db ) {
[codex] improve issue and routine UI responsiveness (#3744)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Operators rely on issue, inbox, and routine views to understand what
the company is doing in real time
> - Those views need to stay fast and readable even when issue lists,
markdown comments, and run metadata get large
> - The current branch had a coherent set of UI and live-update
improvements spread across issue search, issue detail rendering, routine
affordances, and workspace lookups
> - This pull request groups those board-facing changes into one
standalone branch that can merge independently of the heartbeat/runtime
work
> - The benefit is a faster, clearer issue and routine workflow without
changing the underlying task model
## What Changed
- Show routine execution issues by default and rename the filter to
`Hide routine runs` so the default state no longer looks like an active
filter.
- Show the routine name in the run dialog and tighten the issue
properties pane with a workspace link, copy-on-click behavior, and an
inline parent arrow.
- Reduce issue detail rerenders, keep queued issue chat mounted, improve
issues page search responsiveness, and speed up issues first paint.
- Add inbox "other search results", refresh visible issue runs after
status updates, and optimize workspace lookups through summary-mode
execution workspace queries.
- Improve markdown wrapping and scrolling behavior for long strings and
self-comment code blocks.
- Relax the markdown sanitizer assertion so the test still validates
safety after the new wrap-friendly inline styles.
## Verification
- `pnpm vitest run ui/src/components/IssuesList.test.tsx
ui/src/lib/inbox.test.ts ui/src/pages/Issues.test.tsx
ui/src/context/BreadcrumbContext.test.tsx
ui/src/context/LiveUpdatesProvider.test.ts
ui/src/components/MarkdownBody.test.tsx
ui/src/api/execution-workspaces.test.ts
server/src/__tests__/execution-workspaces-routes.test.ts`
## Risks
- This touches several issue-facing UI surfaces at once, so regressions
would most likely show up as stale rendering, search result mismatches,
or small markdown presentation differences.
- The workspace lookup optimization depends on the summary-mode route
shape staying aligned between server and UI.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 15:54:05 -05:00
function buildListConditions (
companyId : string ,
filters ? : {
projectId? : string ;
projectWorkspaceId? : string ;
issueId? : string ;
status? : string ;
reuseEligible? : boolean ;
} ,
) {
const conditions = [ eq ( executionWorkspaces . companyId , companyId ) ] ;
if ( filters ? . projectId ) conditions . push ( eq ( executionWorkspaces . projectId , filters . projectId ) ) ;
if ( filters ? . projectWorkspaceId ) {
conditions . push ( eq ( executionWorkspaces . projectWorkspaceId , filters . projectWorkspaceId ) ) ;
}
if ( filters ? . issueId ) conditions . push ( eq ( executionWorkspaces . sourceIssueId , filters . issueId ) ) ;
if ( filters ? . status ) {
const statuses = filters . status . split ( "," ) . map ( ( value ) = > value . trim ( ) ) . filter ( Boolean ) ;
if ( statuses . length === 1 ) conditions . push ( eq ( executionWorkspaces . status , statuses [ 0 ] ! ) ) ;
else if ( statuses . length > 1 ) conditions . push ( inArray ( executionWorkspaces . status , statuses ) ) ;
}
if ( filters ? . reuseEligible ) {
conditions . push ( inArray ( executionWorkspaces . status , [ "active" , "idle" , "in_review" ] ) ) ;
}
return conditions ;
}
2026-03-13 17:12:25 -05:00
return {
list : async ( companyId : string , filters ? : {
projectId? : string ;
projectWorkspaceId? : string ;
issueId? : string ;
status? : string ;
reuseEligible? : boolean ;
} ) = > {
[codex] improve issue and routine UI responsiveness (#3744)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Operators rely on issue, inbox, and routine views to understand what
the company is doing in real time
> - Those views need to stay fast and readable even when issue lists,
markdown comments, and run metadata get large
> - The current branch had a coherent set of UI and live-update
improvements spread across issue search, issue detail rendering, routine
affordances, and workspace lookups
> - This pull request groups those board-facing changes into one
standalone branch that can merge independently of the heartbeat/runtime
work
> - The benefit is a faster, clearer issue and routine workflow without
changing the underlying task model
## What Changed
- Show routine execution issues by default and rename the filter to
`Hide routine runs` so the default state no longer looks like an active
filter.
- Show the routine name in the run dialog and tighten the issue
properties pane with a workspace link, copy-on-click behavior, and an
inline parent arrow.
- Reduce issue detail rerenders, keep queued issue chat mounted, improve
issues page search responsiveness, and speed up issues first paint.
- Add inbox "other search results", refresh visible issue runs after
status updates, and optimize workspace lookups through summary-mode
execution workspace queries.
- Improve markdown wrapping and scrolling behavior for long strings and
self-comment code blocks.
- Relax the markdown sanitizer assertion so the test still validates
safety after the new wrap-friendly inline styles.
## Verification
- `pnpm vitest run ui/src/components/IssuesList.test.tsx
ui/src/lib/inbox.test.ts ui/src/pages/Issues.test.tsx
ui/src/context/BreadcrumbContext.test.tsx
ui/src/context/LiveUpdatesProvider.test.ts
ui/src/components/MarkdownBody.test.tsx
ui/src/api/execution-workspaces.test.ts
server/src/__tests__/execution-workspaces-routes.test.ts`
## Risks
- This touches several issue-facing UI surfaces at once, so regressions
would most likely show up as stale rendering, search result mismatches,
or small markdown presentation differences.
- The workspace lookup optimization depends on the summary-mode route
shape staying aligned between server and UI.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 15:54:05 -05:00
const conditions = buildListConditions ( companyId , filters ) ;
2026-03-13 17:12:25 -05:00
const rows = await db
. select ( )
. from ( executionWorkspaces )
. where ( and ( . . . conditions ) )
. orderBy ( desc ( executionWorkspaces . lastUsedAt ) , desc ( executionWorkspaces . createdAt ) ) ;
2026-04-04 13:15:46 -05:00
const runtimeServicesByWorkspaceId = await loadEffectiveRuntimeServicesByExecutionWorkspace ( db , companyId , rows ) ;
return rows . map ( ( row ) = >
toExecutionWorkspace (
row ,
( runtimeServicesByWorkspaceId . get ( row . id ) ? ? [ ] ) . map ( toRuntimeService ) ,
) ,
) ;
2026-03-13 17:12:25 -05:00
} ,
[codex] improve issue and routine UI responsiveness (#3744)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Operators rely on issue, inbox, and routine views to understand what
the company is doing in real time
> - Those views need to stay fast and readable even when issue lists,
markdown comments, and run metadata get large
> - The current branch had a coherent set of UI and live-update
improvements spread across issue search, issue detail rendering, routine
affordances, and workspace lookups
> - This pull request groups those board-facing changes into one
standalone branch that can merge independently of the heartbeat/runtime
work
> - The benefit is a faster, clearer issue and routine workflow without
changing the underlying task model
## What Changed
- Show routine execution issues by default and rename the filter to
`Hide routine runs` so the default state no longer looks like an active
filter.
- Show the routine name in the run dialog and tighten the issue
properties pane with a workspace link, copy-on-click behavior, and an
inline parent arrow.
- Reduce issue detail rerenders, keep queued issue chat mounted, improve
issues page search responsiveness, and speed up issues first paint.
- Add inbox "other search results", refresh visible issue runs after
status updates, and optimize workspace lookups through summary-mode
execution workspace queries.
- Improve markdown wrapping and scrolling behavior for long strings and
self-comment code blocks.
- Relax the markdown sanitizer assertion so the test still validates
safety after the new wrap-friendly inline styles.
## Verification
- `pnpm vitest run ui/src/components/IssuesList.test.tsx
ui/src/lib/inbox.test.ts ui/src/pages/Issues.test.tsx
ui/src/context/BreadcrumbContext.test.tsx
ui/src/context/LiveUpdatesProvider.test.ts
ui/src/components/MarkdownBody.test.tsx
ui/src/api/execution-workspaces.test.ts
server/src/__tests__/execution-workspaces-routes.test.ts`
## Risks
- This touches several issue-facing UI surfaces at once, so regressions
would most likely show up as stale rendering, search result mismatches,
or small markdown presentation differences.
- The workspace lookup optimization depends on the summary-mode route
shape staying aligned between server and UI.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 15:54:05 -05:00
listSummaries : async ( companyId : string , filters ? : {
projectId? : string ;
projectWorkspaceId? : string ;
issueId? : string ;
status? : string ;
reuseEligible? : boolean ;
} ) = > {
const conditions = buildListConditions ( companyId , filters ) ;
const rows = await db
. select ( {
id : executionWorkspaces.id ,
name : executionWorkspaces.name ,
mode : executionWorkspaces.mode ,
projectWorkspaceId : executionWorkspaces.projectWorkspaceId ,
} )
. from ( executionWorkspaces )
. where ( and ( . . . conditions ) )
. orderBy ( desc ( executionWorkspaces . lastUsedAt ) , desc ( executionWorkspaces . createdAt ) ) ;
return rows . map ( ( row ) = > toExecutionWorkspaceSummary ( row ) ) ;
} ,
2026-03-13 17:12:25 -05:00
getById : async ( id : string ) = > {
const row = await db
. select ( )
. from ( executionWorkspaces )
. where ( eq ( executionWorkspaces . id , id ) )
. then ( ( rows ) = > rows [ 0 ] ? ? null ) ;
2026-03-28 12:15:34 -05:00
if ( ! row ) return null ;
2026-04-04 13:15:46 -05:00
const runtimeServicesByWorkspaceId = await loadEffectiveRuntimeServicesByExecutionWorkspace ( db , row . companyId , [ row ] ) ;
return toExecutionWorkspace (
row ,
( runtimeServicesByWorkspaceId . get ( row . id ) ? ? [ ] ) . map ( toRuntimeService ) ,
) ;
2026-03-13 17:12:25 -05:00
} ,
2026-03-28 16:15:20 -05:00
getCloseReadiness : async ( id : string ) : Promise < ExecutionWorkspaceCloseReadiness | null > = > {
const workspace = await db
. select ( )
. from ( executionWorkspaces )
. where ( eq ( executionWorkspaces . id , id ) )
. then ( ( rows ) = > rows [ 0 ] ? ? null ) ;
if ( ! workspace ) return null ;
2026-04-04 13:15:46 -05:00
const runtimeServicesByWorkspaceId = await loadEffectiveRuntimeServicesByExecutionWorkspace ( db , workspace . companyId , [ workspace ] ) ;
const runtimeServices = ( runtimeServicesByWorkspaceId . get ( workspace . id ) ? ? [ ] ) . map ( toRuntimeService ) ;
2026-03-28 16:15:20 -05:00
const linkedIssues = await db
. select ( {
id : issues.id ,
identifier : issues.identifier ,
title : issues.title ,
status : issues.status ,
} )
. from ( issues )
. where ( and ( eq ( issues . companyId , workspace . companyId ) , eq ( issues . executionWorkspaceId , workspace . id ) ) ) ;
const projectWorkspace = workspace . projectWorkspaceId
? await db
. select ( {
id : projectWorkspaces.id ,
cwd : projectWorkspaces.cwd ,
cleanupCommand : projectWorkspaces.cleanupCommand ,
isPrimary : projectWorkspaces.isPrimary ,
} )
. from ( projectWorkspaces )
. where (
and (
eq ( projectWorkspaces . companyId , workspace . companyId ) ,
eq ( projectWorkspaces . id , workspace . projectWorkspaceId ) ,
) ,
)
. then ( ( rows ) = > rows [ 0 ] ? ? null )
: null ;
const primaryProjectWorkspace = workspace . projectId
? await db
. select ( {
id : projectWorkspaces.id ,
} )
. from ( projectWorkspaces )
. where (
and (
eq ( projectWorkspaces . companyId , workspace . companyId ) ,
eq ( projectWorkspaces . projectId , workspace . projectId ) ,
eq ( projectWorkspaces . isPrimary , true ) ,
) ,
)
. then ( ( rows ) = > rows [ 0 ] ? ? null )
: null ;
const projectPolicy = workspace . projectId
? await db
. select ( {
executionWorkspacePolicy : projects.executionWorkspacePolicy ,
} )
. from ( projects )
. where ( and ( eq ( projects . id , workspace . projectId ) , eq ( projects . companyId , workspace . companyId ) ) )
. then ( ( rows ) = > parseProjectExecutionWorkspacePolicy ( rows [ 0 ] ? . executionWorkspacePolicy ) )
: null ;
const executionWorkspace = toExecutionWorkspace ( workspace , runtimeServices ) ;
const config = readExecutionWorkspaceConfig ( ( workspace . metadata as Record < string , unknown > | null ) ? ? null ) ;
const { git , warnings : gitWarnings } = await inspectGitCloseReadiness ( executionWorkspace ) ;
const warnings = [ . . . gitWarnings ] ;
const blockingReasons : string [ ] = [ ] ;
const isSharedWorkspace = executionWorkspace . mode === "shared_workspace" ;
2026-03-28 17:38:34 -05:00
const workspacePath = readNullableString ( executionWorkspace . providerRef ) ? ? readNullableString ( executionWorkspace . cwd ) ;
const resolvedWorkspacePath = workspacePath ? path . resolve ( workspacePath ) : null ;
const resolvedPrimaryWorkspacePath = projectWorkspace ? . cwd ? path . resolve ( projectWorkspace . cwd ) : null ;
const isProjectPrimaryWorkspace =
workspace . projectWorkspaceId != null
&& workspace . projectWorkspaceId === primaryProjectWorkspace ? . id
&& resolvedWorkspacePath != null
&& resolvedPrimaryWorkspacePath != null
&& resolvedWorkspacePath === resolvedPrimaryWorkspacePath ;
2026-03-28 16:15:20 -05:00
const linkedIssueSummaries = linkedIssues . map ( ( issue ) = > ( {
. . . issue ,
isTerminal : TERMINAL_ISSUE_STATUSES.has ( issue . status ) ,
} ) ) ;
const blockingIssues = linkedIssueSummaries . filter ( ( issue ) = > ! issue . isTerminal ) ;
if ( blockingIssues . length > 0 ) {
2026-03-28 19:19:49 -05:00
const linkedIssueMessage =
2026-03-28 16:15:20 -05:00
blockingIssues . length === 1
? "This workspace is still linked to an open issue."
2026-03-28 19:19:49 -05:00
: ` This workspace is still linked to ${ blockingIssues . length } open issues. ` ;
if ( isSharedWorkspace ) {
warnings . push ( ` ${ linkedIssueMessage } Archiving it will detach this shared workspace session from those issues, but keep the underlying project workspace available. ` ) ;
} else {
blockingReasons . push ( linkedIssueMessage ) ;
}
2026-03-28 16:15:20 -05:00
}
if ( isSharedWorkspace ) {
2026-03-28 19:19:49 -05:00
warnings . push ( "This shared workspace session points at project workspace infrastructure. Archiving it only removes the session record." ) ;
2026-03-28 16:15:20 -05:00
}
if ( runtimeServices . some ( ( service ) = > service . status !== "stopped" ) ) {
warnings . push (
runtimeServices . length === 1
? "Closing this workspace will stop 1 attached runtime service."
: ` Closing this workspace will stop ${ runtimeServices . length } attached runtime services. ` ,
) ;
}
if ( git ? . hasDirtyTrackedFiles ) {
warnings . push (
git . dirtyEntryCount === 1
? "The workspace has 1 modified tracked file."
: ` The workspace has ${ git . dirtyEntryCount } modified tracked files. ` ,
) ;
}
if ( git ? . hasUntrackedFiles ) {
warnings . push (
git . untrackedEntryCount === 1
? "The workspace has 1 untracked file."
: ` The workspace has ${ git . untrackedEntryCount } untracked files. ` ,
) ;
}
if ( git ? . aheadCount && git . aheadCount > 0 && git . isMergedIntoBase === false ) {
warnings . push (
git . aheadCount === 1
? ` This workspace is 1 commit ahead of ${ git . baseRef ? ? "the base ref" } and is not merged. `
: ` This workspace is ${ git . aheadCount } commits ahead of ${ git . baseRef ? ? "the base ref" } and is not merged. ` ,
) ;
}
if ( git ? . behindCount && git . behindCount > 0 ) {
warnings . push (
git . behindCount === 1
? ` This workspace is 1 commit behind ${ git . baseRef ? ? "the base ref" } . `
: ` This workspace is ${ git . behindCount } commits behind ${ git . baseRef ? ? "the base ref" } . ` ,
) ;
}
const plannedActions : ExecutionWorkspaceCloseAction [ ] = [
{
kind : "archive_record" ,
label : "Archive workspace record" ,
description : "Keep the execution workspace history and issue linkage, but remove it from active workspace lists." ,
command : null ,
} ,
] ;
if ( runtimeServices . some ( ( service ) = > service . status !== "stopped" ) ) {
plannedActions . push ( {
kind : "stop_runtime_services" ,
label : runtimeServices.length === 1 ? "Stop attached runtime service" : "Stop attached runtime services" ,
description :
runtimeServices . length === 1
? ` ${ runtimeServices [ 0 ] ? . serviceName ? ? "A runtime service" } will be stopped before cleanup. `
: ` ${ runtimeServices . length } runtime services will be stopped before cleanup. ` ,
command : null ,
} ) ;
}
const configuredCleanupCommands = [
{
kind : "cleanup_command" as const ,
label : "Run workspace cleanup command" ,
description : "Workspace-specific cleanup runs before teardown." ,
command : config?.cleanupCommand ? ? null ,
} ,
{
kind : "cleanup_command" as const ,
label : "Run project workspace cleanup command" ,
description : "Project workspace cleanup runs before execution workspace teardown." ,
command : projectWorkspace?.cleanupCommand ? ? null ,
} ,
] ;
for ( const action of configuredCleanupCommands ) {
if ( ! action . command ) continue ;
plannedActions . push ( action ) ;
}
const teardownCommand = config ? . teardownCommand ? ? projectPolicy ? . workspaceStrategy ? . teardownCommand ? ? null ;
if ( teardownCommand ) {
plannedActions . push ( {
kind : "teardown_command" ,
label : "Run teardown command" ,
description : "Teardown runs after cleanup commands during workspace close." ,
command : teardownCommand ,
} ) ;
}
if ( executionWorkspace . providerType === "git_worktree" && workspacePath ) {
plannedActions . push ( {
kind : "git_worktree_remove" ,
label : "Remove git worktree" ,
description : ` Paperclip will run git worktree cleanup for ${ workspacePath } . ` ,
command : ` git worktree remove --force ${ workspacePath } ` ,
} ) ;
}
if ( git ? . createdByRuntime && executionWorkspace . branchName ) {
plannedActions . push ( {
kind : "git_branch_delete" ,
label : "Delete runtime-created branch" ,
description : "Paperclip will try to delete the runtime-created branch after removing the worktree." ,
command : ` git branch -d ${ executionWorkspace . branchName } ` ,
} ) ;
}
if ( executionWorkspace . providerType === "local_fs" && git ? . createdByRuntime && workspacePath ) {
const resolvedWorkspacePath = path . resolve ( workspacePath ) ;
const resolvedProjectWorkspacePath = projectWorkspace ? . cwd ? path . resolve ( projectWorkspace . cwd ) : null ;
const containsProjectWorkspace = resolvedProjectWorkspacePath
? (
resolvedWorkspacePath === resolvedProjectWorkspacePath ||
resolvedProjectWorkspacePath . startsWith ( ` ${ resolvedWorkspacePath } ${ path . sep } ` )
)
: false ;
if ( containsProjectWorkspace ) {
warnings . push ( ` Paperclip will archive this workspace but keep " ${ workspacePath } " because it contains the project workspace. ` ) ;
} else {
plannedActions . push ( {
kind : "remove_local_directory" ,
label : "Remove runtime-created directory" ,
description : ` Paperclip will remove the runtime-created directory at ${ workspacePath } . ` ,
command : ` rm -rf ${ workspacePath } ` ,
} ) ;
}
}
const state =
blockingReasons . length > 0
? "blocked"
: warnings . length > 0
? "ready_with_warnings"
: "ready" ;
return {
workspaceId : workspace.id ,
state ,
blockingReasons ,
warnings ,
linkedIssues : linkedIssueSummaries ,
plannedActions ,
isDestructiveCloseAllowed : blockingReasons.length === 0 ,
isSharedWorkspace ,
isProjectPrimaryWorkspace ,
git ,
runtimeServices ,
} ;
} ,
2026-03-13 17:12:25 -05:00
create : async ( data : typeof executionWorkspaces . $inferInsert ) = > {
const row = await db
. insert ( executionWorkspaces )
. values ( data )
. returning ( )
. then ( ( rows ) = > rows [ 0 ] ? ? null ) ;
return row ? toExecutionWorkspace ( row ) : null ;
} ,
update : async ( id : string , patch : Partial < typeof executionWorkspaces. $ inferInsert > ) = > {
const row = await db
. update ( executionWorkspaces )
. set ( { . . . patch , updatedAt : new Date ( ) } )
. where ( eq ( executionWorkspaces . id , id ) )
. returning ( )
. then ( ( rows ) = > rows [ 0 ] ? ? null ) ;
return row ? toExecutionWorkspace ( row ) : null ;
} ,
} ;
}
export { toExecutionWorkspace } ;