Keep linked workspace prompts as editable drafts (#5734)

This commit is contained in:
Brennan Benson 2026-06-18 16:24:22 -07:00 committed by GitHub
parent fe8b60b11c
commit 53f2e50122
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 499 additions and 48 deletions

View File

@ -55,6 +55,28 @@ describe('markRemoteAgentWorkspaceTrusted', () => {
)
})
it('writes Codex trust when the remote home is a Windows absolute path', async () => {
const fsProvider = makeFsProvider({
realpath: vi.fn(async () => 'C:/Users/alice/platform')
})
mocks.getActiveMultiplexer.mockReturnValue({
request: vi.fn(async () => ({ resolvedPath: 'C:\\Users\\alice\\' }))
})
mocks.getSshFilesystemProvider.mockReturnValue(fsProvider)
await markRemoteAgentWorkspaceTrusted({
preset: 'codex',
connectionId: 'ssh-windows',
workspacePath: 'C:\\Users\\alice\\platform'
})
expect(fsProvider.createDir).toHaveBeenCalledWith('C:/Users/alice/.codex')
expect(fsProvider.writeFile).toHaveBeenCalledWith(
'C:/Users/alice/.codex/config.toml',
expect.stringContaining('[projects."C:/Users/alice/platform"]')
)
})
it('writes Cursor trust marker on the remote host', async () => {
const fsProvider = makeFsProvider()
mocks.getSshFilesystemProvider.mockReturnValue(fsProvider)
@ -72,6 +94,30 @@ describe('markRemoteAgentWorkspaceTrusted', () => {
)
})
it('sanitizes Windows path characters in remote Cursor trust marker paths', async () => {
const fsProvider = makeFsProvider({
realpath: vi.fn(async () => 'C:/Users/alice/platform')
})
mocks.getActiveMultiplexer.mockReturnValue({
request: vi.fn(async () => ({ resolvedPath: 'C:/Users/alice/' }))
})
mocks.getSshFilesystemProvider.mockReturnValue(fsProvider)
await markRemoteAgentWorkspaceTrusted({
preset: 'cursor',
connectionId: 'ssh-windows',
workspacePath: 'C:\\Users\\alice\\platform'
})
expect(fsProvider.createDir).toHaveBeenCalledWith(
'C:/Users/alice/.cursor/projects/C-Users-alice-platform'
)
expect(fsProvider.writeFile).toHaveBeenCalledWith(
'C:/Users/alice/.cursor/projects/C-Users-alice-platform/.workspace-trusted',
expect.stringContaining('"workspacePath": "C:/Users/alice/platform"')
)
})
it('appends Copilot trusted folder remotely without clobbering config keys', async () => {
const writeFile = vi.fn(async (_filePath: string, _content: string) => undefined)
const fsProvider = makeFsProvider({

View File

@ -3,6 +3,10 @@ import { upsertProjectTrustLevelInContent } from './codex/config-toml-trust'
import { getActiveMultiplexer } from './ipc/ssh'
import { getSshFilesystemProvider } from './providers/ssh-filesystem-dispatch'
import type { IFilesystemProvider } from './providers/types'
import {
isWindowsAbsolutePathLike,
normalizeRuntimePathSeparators
} from '../shared/cross-platform-path'
export async function markRemoteAgentWorkspaceTrusted(args: {
preset: AgentTrustPreset
@ -33,8 +37,13 @@ async function resolveRemoteHome(connectionId: string): Promise<string | null> {
const result = (await mux.request('session.resolveHome', { path: '~' })) as {
resolvedPath?: unknown
}
const home = typeof result.resolvedPath === 'string' ? result.resolvedPath.trim() : ''
return home && home.startsWith('/') && !hasRemotePathControlCharacter(home)
const home =
typeof result.resolvedPath === 'string'
? normalizeRuntimePathSeparators(result.resolvedPath.trim())
: ''
return home &&
(home.startsWith('/') || isWindowsAbsolutePathLike(home)) &&
!hasRemotePathControlCharacter(home)
? home.replace(/\/$/, '')
: null
}
@ -87,7 +96,7 @@ async function markRemoteCursorWorkspaceTrusted(
remoteHome: string,
workspacePath: string
): Promise<void> {
const slug = workspacePath.replace(/^[\\/]+/, '').replace(/[\\/]+/g, '-')
const slug = workspacePath.replace(/^[\\/]+/, '').replace(/[\\/:*?"<>|]+/g, '-')
if (!slug) {
return
}

View File

@ -1,16 +1,27 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { FolderWorkspace, ProjectGroup } from '../../../../shared/types'
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
import type * as NewWorkspaceModule from '@/lib/new-workspace'
const mocks = vi.hoisted(() => ({
activateAndRevealFolderWorkspace: vi.fn()
activateAndRevealFolderWorkspace: vi.fn(),
ensureAgentStartupInTerminal: vi.fn()
}))
vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealFolderWorkspace: mocks.activateAndRevealFolderWorkspace
}))
vi.mock('@/lib/new-workspace', async (importOriginal) => {
const actual = await importOriginal<typeof NewWorkspaceModule>()
return {
...actual,
ensureAgentStartupInTerminal: mocks.ensureAgentStartupInTerminal
}
})
import {
getFolderWorkspaceAgentLaunchPlatform,
submitFolderWorkspaceCreate
@ -51,8 +62,21 @@ function makeFolderWorkspace(overrides: Partial<FolderWorkspace> = {}): FolderWo
}
describe('submitFolderWorkspaceCreate', () => {
beforeEach(() => {
mocks.activateAndRevealFolderWorkspace.mockReturnValue({ primaryTabId: 'tab-1' })
Object.assign(window, {
api: {
agentTrust: {
markTrusted: vi.fn().mockResolvedValue(undefined)
}
}
})
})
afterEach(() => {
mocks.activateAndRevealFolderWorkspace.mockReset()
mocks.ensureAgentStartupInTerminal.mockReset()
Reflect.deleteProperty(window, 'api')
vi.restoreAllMocks()
})
@ -138,6 +162,7 @@ describe('submitFolderWorkspaceCreate', () => {
const startup = mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup
expect(startup?.command).toContain('--model')
expect(startup?.command).toContain('gpt-5.4')
expect(mocks.ensureAgentStartupInTerminal).not.toHaveBeenCalled()
})
it('does not mark first-input rename when the folder workspace has an explicit name', async () => {
@ -198,6 +223,205 @@ describe('submitFolderWorkspaceCreate', () => {
})
})
it('keeps linked Codex context out of submitted startup and pastes it as a draft', async () => {
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
const linkedWorkItem = {
provider: 'github' as const,
type: 'pr' as const,
number: 91,
title: 'Restore linked quick-create',
url: 'https://github.com/stablyai/orca/pull/91',
repoId: 'repo-1'
}
await submitFolderWorkspaceCreate({
projectGroup: makeProjectGroup(),
name: '',
lastAutoName: '',
linkedWorkItem,
note: 'Review this before starting',
quickAgent: 'codex',
autoRenameBranchFromWork: true,
agentCmdOverrides: {},
launchSource: 'new_workspace_composer',
createFolderWorkspace,
onOpenChange: vi.fn()
})
expect(createFolderWorkspace).toHaveBeenCalledWith({
projectGroupId: 'group-1',
name: 'Restore linked quick-create',
connectionId: null,
linkedTask: linkedWorkItem,
createdWithAgent: 'codex'
})
const startup = mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup
expect(startup?.command).toBe('codex')
expect(startup?.command).not.toContain(linkedWorkItem.url)
expect(startup?.command).not.toContain('Review this before starting')
expect(window.api.agentTrust?.markTrusted).toHaveBeenCalledWith({
preset: 'codex',
workspacePath: '/repo/platform/hi'
})
expect(mocks.ensureAgentStartupInTerminal).toHaveBeenCalledWith({
worktreeId: folderWorkspaceKey('folder-workspace-1'),
primaryTabId: 'tab-1',
startup: expect.objectContaining({
agent: 'codex',
launchCommand: 'codex',
followupPrompt: null,
draftPrompt: `Review this before starting\n\n${linkedWorkItem.url}`
})
})
})
it('pre-marks remote linked Codex folder workspaces trusted before draft paste', async () => {
const createFolderWorkspace = vi.fn(async () =>
makeFolderWorkspace({
connectionId: 'ssh-1',
folderPath: '/home/alice/platform/Trust remote folder draft'
})
)
const linkedWorkItem = {
provider: 'github' as const,
type: 'pr' as const,
number: 92,
title: 'Trust remote folder draft',
url: 'https://github.com/stablyai/orca/pull/92',
repoId: 'repo-1'
}
const projectGroup = {
...makeProjectGroup(),
connectionId: 'ssh-1',
parentPath: '/home/alice/platform'
}
await submitFolderWorkspaceCreate({
projectGroup,
name: '',
lastAutoName: '',
linkedWorkItem,
note: '',
quickAgent: 'codex',
autoRenameBranchFromWork: false,
agentCmdOverrides: {},
isRemote: true,
createFolderWorkspace,
onOpenChange: vi.fn()
})
expect(window.api.agentTrust?.markTrusted).toHaveBeenCalledWith({
preset: 'codex',
workspacePath: '/home/alice/platform/Trust remote folder draft',
connectionId: 'ssh-1'
})
expect(mocks.ensureAgentStartupInTerminal).toHaveBeenCalledWith(
expect.objectContaining({
worktreeId: folderWorkspaceKey('folder-workspace-1'),
startup: expect.objectContaining({
agent: 'codex',
draftPrompt: linkedWorkItem.url
})
})
)
})
it('delivers non-linked follow-up prompts for agents that need stdin after launch', async () => {
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
await submitFolderWorkspaceCreate({
projectGroup: makeProjectGroup(),
name: 'Aider followup',
lastAutoName: '',
linkedWorkItem: null,
note: 'Fix the failing folder prompt flow',
quickAgent: 'aider',
autoRenameBranchFromWork: false,
agentCmdOverrides: {},
createFolderWorkspace,
onOpenChange: vi.fn()
})
const startup = mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup
expect(startup?.command).toBe('aider')
expect(mocks.ensureAgentStartupInTerminal).toHaveBeenCalledWith({
worktreeId: folderWorkspaceKey('folder-workspace-1'),
primaryTabId: 'tab-1',
startup: expect.objectContaining({
agent: 'aider',
launchCommand: 'aider',
followupPrompt: 'Fix the failing folder prompt flow'
})
})
})
it('uses native draft launch for linked agents with prefill support', async () => {
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
const linkedWorkItem = {
provider: 'gitlab' as const,
type: 'mr' as const,
number: 17,
title: 'Review folder workspace draft',
url: 'https://gitlab.example.com/group/project/-/merge_requests/17',
repoId: 'repo-1'
}
await submitFolderWorkspaceCreate({
projectGroup: makeProjectGroup(),
name: '',
lastAutoName: '',
linkedWorkItem,
note: 'Check the migration path',
quickAgent: 'claude',
autoRenameBranchFromWork: true,
agentCmdOverrides: {},
createFolderWorkspace,
onOpenChange: vi.fn()
})
const startup = mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup
expect(startup?.command).toContain('claude --prefill')
expect(startup?.command).toContain('Check the migration path')
expect(startup?.command).toContain(linkedWorkItem.url)
expect(mocks.ensureAgentStartupInTerminal).not.toHaveBeenCalled()
})
it('keeps explicit blank linked folder creates free of agent startup and draft paste', async () => {
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
const linkedWorkItem = {
provider: 'github' as const,
type: 'issue' as const,
number: 42,
title: 'Restore checkout polish',
url: 'https://github.com/stablyai/orca/issues/42',
repoId: 'repo-1'
}
await submitFolderWorkspaceCreate({
projectGroup: makeProjectGroup(),
name: '',
lastAutoName: '',
linkedWorkItem,
note: 'Keep this as metadata only',
quickAgent: null,
autoRenameBranchFromWork: true,
agentCmdOverrides: {},
createFolderWorkspace,
onOpenChange: vi.fn()
})
expect(createFolderWorkspace).toHaveBeenCalledWith({
projectGroupId: 'group-1',
name: 'Restore checkout polish',
connectionId: null,
linkedTask: linkedWorkItem
})
expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith('folder-workspace-1', {
runtimeEnvironmentId: null
})
expect(mocks.ensureAgentStartupInTerminal).not.toHaveBeenCalled()
})
it('does not mark first-input rename without submitted first input', async () => {
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())

View File

@ -1,18 +1,24 @@
import {
CLIENT_PLATFORM,
buildAgentPromptWithContext,
ensureAgentStartupInTerminal,
type LinkedWorkItemSummary
} from '@/lib/new-workspace'
import { getLinkedWorkItemPromptContext } from '@/lib/linked-work-item-context'
import { resolveQuickCreateLinkedWorkItemPrompt } from '@/lib/linked-work-item-context'
import { isOrcaCliAvailableForLaunch } from '@/lib/orca-cli-launch-availability'
import { buildAgentStartupPlan } from '@/lib/tui-agent-startup'
import {
buildAgentDraftLaunchPlan,
buildAgentStartupPlan,
type AgentStartupPlan
} from '@/lib/tui-agent-startup'
import { tuiAgentToAgentKind } from '@/lib/telemetry'
import { activateAndRevealFolderWorkspace } from '@/lib/worktree-activation'
import { isWorkItemLookupText } from '@/lib/work-item-lookup-text'
import { TUI_AGENT_CONFIG } from '../../../../shared/tui-agent-config'
import { isWindowsAbsolutePathLike } from '../../../../shared/cross-platform-path'
import type { FolderWorkspace, ProjectGroup, TuiAgent } from '../../../../shared/types'
import { isWslUncPath } from '../../../../shared/wsl-paths'
import type { LaunchSource } from '../../../../shared/telemetry-events'
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
import {
getLinkedItemDisplayName,
toFolderWorkspaceLinkedTask
@ -55,6 +61,87 @@ export function getFolderWorkspaceAgentLaunchPlatform(
return parentPath && isWslUncPath(parentPath) ? 'linux' : CLIENT_PLATFORM
}
function buildFolderWorkspaceLinkedStartupPlan(args: {
agent: TuiAgent
linkedWorkItem: LinkedWorkItemSummary
note: string
cliAvailable: boolean
agentCmdOverrides: Record<string, string> | undefined
agentArgs?: string | null
agentEnv?: Record<string, string>
platform: NodeJS.Platform
}): AgentStartupPlan | null {
const { prompt, draftPrompt } = resolveQuickCreateLinkedWorkItemPrompt(
args.linkedWorkItem,
args.note,
{
cliAvailable: args.cliAvailable
}
)
const linkedDraftPrompt = (draftPrompt ?? prompt.trim()) || null
const draftLaunchPlan = linkedDraftPrompt
? buildAgentDraftLaunchPlan({
agent: args.agent,
draft: linkedDraftPrompt,
cmdOverrides: args.agentCmdOverrides ?? {},
agentArgs: args.agentArgs,
agentEnv: args.agentEnv,
platform: args.platform
})
: null
if (draftLaunchPlan) {
return {
agent: draftLaunchPlan.agent,
launchCommand: draftLaunchPlan.launchCommand,
expectedProcess: draftLaunchPlan.expectedProcess,
followupPrompt: null,
...(draftLaunchPlan.startupCommandDelivery
? { startupCommandDelivery: draftLaunchPlan.startupCommandDelivery }
: {}),
...(draftLaunchPlan.env ? { env: draftLaunchPlan.env } : {})
}
}
const startupPlan = buildAgentStartupPlan({
agent: args.agent,
// Why: linked context must stay reviewable; launch empty, then paste the
// draft after the agent is ready instead of submitting it on argv/stdin.
prompt: '',
cmdOverrides: args.agentCmdOverrides ?? {},
agentArgs: args.agentArgs,
agentEnv: args.agentEnv,
platform: args.platform,
allowEmptyPromptLaunch: true
})
if (startupPlan && linkedDraftPrompt) {
startupPlan.draftPrompt = linkedDraftPrompt
}
return startupPlan
}
async function preflightFolderWorkspaceAgentTrust(args: {
agent: TuiAgent | null
workspacePath: string | null
connectionId?: string | null
}): Promise<void> {
if (!args.agent || !window.api.agentTrust?.markTrusted) {
return
}
const preflight = TUI_AGENT_CONFIG[args.agent].preflightTrust
if (!preflight || !args.workspacePath) {
return
}
try {
await window.api.agentTrust.markTrusted({
preset: preflight,
workspacePath: args.workspacePath,
...(args.connectionId ? { connectionId: args.connectionId } : {})
})
} catch {
// Best-effort: the user can still accept the agent trust prompt manually.
}
}
export async function submitFolderWorkspaceCreate({
projectGroup,
name,
@ -78,20 +165,36 @@ export async function submitFolderWorkspaceCreate({
nameIsAutoManaged && linkedName
? linkedName
: name.trim() || linkedName || `${projectGroup.name} workspace`
const launchPlatform = getFolderWorkspaceAgentLaunchPlatform(projectGroup)
// Why: only suggest `orca linear` when the launched terminal can actually
// resolve the CLI; SSH launches get the relay shim, local launches may not.
const linearCliAvailable = linkedWorkItem?.linearIdentifier
? await isOrcaCliAvailableForLaunch({ remote: isRemote ?? projectGroup.connectionId != null })
: false
const linkedPromptContext = getLinkedWorkItemPromptContext(linkedWorkItem, {
cliAvailable: linearCliAvailable
})
const startupPrompt = buildAgentPromptWithContext(
note,
[],
linkedPromptContext.linkedUrls,
linkedPromptContext.linkedContextBlocks
)
const linearCliAvailable =
quickAgent && linkedWorkItem?.linearIdentifier
? await isOrcaCliAvailableForLaunch({ remote: isRemote ?? projectGroup.connectionId != null })
: false
const startupPlan =
quickAgent && linkedWorkItem
? buildFolderWorkspaceLinkedStartupPlan({
agent: quickAgent,
linkedWorkItem,
note,
cliAvailable: linearCliAvailable,
agentCmdOverrides,
agentArgs,
agentEnv,
platform: launchPlatform
})
: quickAgent
? buildAgentStartupPlan({
agent: quickAgent,
prompt: note,
cmdOverrides: agentCmdOverrides ?? {},
agentArgs,
agentEnv,
platform: launchPlatform,
allowEmptyPromptLaunch: true
})
: null
// Why: the pending badge should only appear when the submitted prompt can
// actually produce the first agent message that names the workspace.
const pendingFirstAgentMessageRename =
@ -99,7 +202,7 @@ export async function submitFolderWorkspaceCreate({
!name.trim() &&
!linkedWorkItem &&
Boolean(quickAgent) &&
startupPrompt.trim().length > 0
note.trim().length > 0
const workspace = await createFolderWorkspace({
projectGroupId: projectGroup.id,
@ -114,18 +217,12 @@ export async function submitFolderWorkspaceCreate({
if (!workspace) {
return false
}
await preflightFolderWorkspaceAgentTrust({
agent: quickAgent,
workspacePath: workspace.folderPath,
connectionId: workspace.connectionId ?? projectGroup.connectionId
})
const startupPlan = quickAgent
? buildAgentStartupPlan({
agent: quickAgent,
prompt: startupPrompt,
cmdOverrides: agentCmdOverrides ?? {},
agentArgs,
agentEnv,
platform: getFolderWorkspaceAgentLaunchPlatform(projectGroup),
allowEmptyPromptLaunch: true
})
: null
const startup =
quickAgent && startupPlan
? {
@ -143,10 +240,21 @@ export async function submitFolderWorkspaceCreate({
: undefined
onOpenChange(false)
try {
activateAndRevealFolderWorkspace(workspace.id, {
const activation = activateAndRevealFolderWorkspace(workspace.id, {
...(startup ? { startup } : {}),
runtimeEnvironmentId
})
if (
startupPlan &&
(startupPlan.followupPrompt || startupPlan.draftPrompt) &&
activation !== false
) {
void ensureAgentStartupInTerminal({
worktreeId: folderWorkspaceKey(workspace.id),
primaryTabId: activation.primaryTabId,
startup: startupPlan
})
}
} catch (error) {
// Why: creation already succeeded. Do not leave the completed create modal
// open if the follow-up reveal/startup path hits a transient issue.

View File

@ -383,4 +383,20 @@ describe('useComposerState host-context boundaries', () => {
expect(quickSubmit).toContain('platform: selectedRepoAgentLaunchPlatform')
expect(quickSubmit).not.toContain('platform: CLIENT_PLATFORM')
})
it('prepares linked quick-create drafts for the selected default agent', () => {
const quickSubmit = sourceBetween(
HOOK_SOURCE,
'const submitQuick = useCallback',
'const createGateInput'
)
expect(quickSubmit).toContain(
'const promptLinkedWorkItem = agent === null ? null : submitLinkedWorkItem'
)
expect(quickSubmit).toContain('resolveQuickCreateLinkedWorkItemPrompt(promptLinkedWorkItem')
expect(quickSubmit).not.toContain('explicitAgentChoice')
expect(quickSubmit).not.toContain('shouldPrepareQuickLinkedWorkItemAgentPrompt')
expect(HOOK_SOURCE).not.toContain('resolveQuickWorkspaceSubmitAgent')
})
})

View File

@ -2681,10 +2681,6 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
if (!selectedProjectGroup?.parentPath || folderCreateDisabled) {
return
}
const agent =
requestedAgent && isTuiAgentEnabled(requestedAgent, disabledTuiAgents)
? requestedAgent
: null
setCreateError(null)
setCreating(true)
try {
@ -2696,6 +2692,10 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
: ({ kind: 'none' } as const)
const smartGitHubMetadata =
smartGitHubResolution.kind === 'none' ? null : smartGitHubResolution
const agent =
requestedAgent && isTuiAgentEnabled(requestedAgent, disabledTuiAgents)
? requestedAgent
: null
const folderWorkspaceCreated = await submitFolderWorkspaceCreate({
projectGroup: selectedProjectGroup,
name: smartGitHubMetadata?.workspaceName ?? name,
@ -3129,10 +3129,6 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
await submitFolderTarget(requestedAgent)
return
}
const agent =
requestedAgent && isTuiAgentEnabled(requestedAgent, disabledTuiAgents)
? requestedAgent
: null
const workspaceNameSeed = getWorkspaceSeedName({
explicitName: name,
prompt: '',
@ -3161,6 +3157,10 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
smartGitHubResolution.kind === 'none'
? linkedWorkItem
: smartGitHubResolution.linkedWorkItem
const agent =
requestedAgent && isTuiAgentEnabled(requestedAgent, disabledTuiAgents)
? requestedAgent
: null
const submitLinkedIssueNumber =
smartGitHubResolution.kind === 'none'
? parsedLinkedIssueNumber
@ -3288,11 +3288,12 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
// Why: backend startup is safe only when the launch command is
// self-contained. Agents that need post-ready paste/follow-up stay on
// the renderer path so prompt delivery is not skipped.
const quickLinearCliAvailable = submitLinkedWorkItem?.linearIdentifier
const promptLinkedWorkItem = agent === null ? null : submitLinkedWorkItem
const quickLinearCliAvailable = promptLinkedWorkItem?.linearIdentifier
? await isOrcaCliAvailableForLaunch({ remote: isRemote })
: false
const { prompt: quickPrompt, draftPrompt: quickDraftPrompt } =
resolveQuickCreateLinkedWorkItemPrompt(submitLinkedWorkItem, trimmedNote, {
resolveQuickCreateLinkedWorkItemPrompt(promptLinkedWorkItem, trimmedNote, {
cliAvailable: quickLinearCliAvailable
})
const draftLaunchPlan =

View File

@ -190,7 +190,7 @@ describe('resolveQuickCreateLinkedWorkItemPrompt', () => {
).toEqual({ prompt: 'use this note', draftPrompt: null })
})
it('falls back to the URL for non-Linear quick creates', () => {
it('drafts the note above the URL for non-Linear quick creates', () => {
expect(
resolveQuickCreateLinkedWorkItemPrompt(
{ number: 42, url: 'https://github.com/acme/repo/issues/42' },
@ -199,7 +199,7 @@ describe('resolveQuickCreateLinkedWorkItemPrompt', () => {
)
).toEqual({
prompt: '',
draftPrompt: 'https://github.com/acme/repo/issues/42'
draftPrompt: 'note\n\nhttps://github.com/acme/repo/issues/42'
})
})
})

View File

@ -207,6 +207,8 @@ export function resolveQuickCreateLinkedWorkItemPrompt(
const draftPrompt = linearDraft
? [trimmedNote, linearDraft].filter(Boolean).join('\n\n')
: linkedUrl
? [trimmedNote, linkedUrl].filter(Boolean).join('\n\n')
: null
const isLinearTypedOnly = linkedWorkItem?.number === 0 && Boolean(trimmedNote) && !draftPrompt
return {
prompt: isLinearTypedOnly ? trimmedNote : '',

View File

@ -1,3 +1,5 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import type { WorktreeCreationRequest } from '@/lib/pending-worktree-creation'
@ -34,6 +36,8 @@ vi.mock('@/lib/new-workspace', () => ({
import { runBackgroundWorktreeCreation } from './worktree-creation-flow'
const FLOW_SOURCE = readFileSync(join(__dirname, 'worktree-creation-flow.ts'), 'utf8')
function makeRequest(overrides: Partial<WorktreeCreationRequest> = {}): WorktreeCreationRequest {
return {
repoId: 'repo-1',
@ -49,6 +53,14 @@ function makeRequest(overrides: Partial<WorktreeCreationRequest> = {}): Worktree
}
}
function sourceBetween(source: string, startPattern: string, endPattern: string): string {
const start = source.indexOf(startPattern)
expect(start).toBeGreaterThanOrEqual(0)
const end = source.indexOf(endPattern, start + startPattern.length)
expect(end).toBeGreaterThan(start)
return source.slice(start, end)
}
describe('runBackgroundWorktreeCreation', () => {
it('uses the captured repo-owner progress mode instead of focused runtime state', () => {
store.settings.activeRuntimeEnvironmentId = null
@ -83,3 +95,26 @@ describe('runBackgroundWorktreeCreation', () => {
)
})
})
describe('worktree creation flow agent trust preflight', () => {
it('forwards the repo SSH connection id when pre-marking agent trust', () => {
const preflight = sourceBetween(
FLOW_SOURCE,
'async function preflightAgentTrust',
'async function executeWorktreeCreation'
)
const createFlow = sourceBetween(
FLOW_SOURCE,
'const backendSpawned = result.startupTerminal?.spawned === true',
'// `createWorktree` already inserted the real worktree row'
)
expect(preflight).toContain('connectionId?: string | null')
expect(preflight).toContain('...(connectionId ? { connectionId } : {})')
expect(createFlow).toContain('repoConnectionId')
expect(createFlow).toContain('repo.id === worktree.repoId')
expect(createFlow).toContain(
'await preflightAgentTrust(request, worktree.path, repoConnectionId)'
)
})
})

View File

@ -48,7 +48,11 @@ function getWorktreeCreationIndeterminate(request: WorktreeCreationRequest): boo
return getActiveRuntimeTarget(useAppStore.getState().settings).kind !== 'local'
}
async function preflightAgentTrust(request: WorktreeCreationRequest, path: string): Promise<void> {
async function preflightAgentTrust(
request: WorktreeCreationRequest,
path: string,
connectionId?: string | null
): Promise<void> {
// Why: trust-gated agents (cursor-agent, copilot) consume the bracketed paste
// as menu input on first launch. Pre-write the trust artifact before any
// terminal spawns. Best-effort — the worktree already exists, so a failure
@ -61,7 +65,11 @@ async function preflightAgentTrust(request: WorktreeCreationRequest, path: strin
return
}
try {
await window.api.agentTrust.markTrusted({ preset: preflight, workspacePath: path })
await window.api.agentTrust.markTrusted({
preset: preflight,
workspacePath: path,
...(connectionId ? { connectionId } : {})
})
} catch {
// Best-effort: continue with launch.
}
@ -137,7 +145,9 @@ async function executeWorktreeCreation(
const startupOpt = buildStartupOpt(request, backendSpawned)
if (worktree.path) {
await preflightAgentTrust(request, worktree.path)
const repoConnectionId =
useAppStore.getState().repos.find((repo) => repo.id === worktree.repoId)?.connectionId ?? null
await preflightAgentTrust(request, worktree.path, repoConnectionId)
}
// `createWorktree` already inserted the real worktree row. Whether we steal