Compser template (#753)

This commit is contained in:
Neil 2026-04-16 23:55:37 -07:00 committed by GitHub
parent fcbdd95b43
commit ee69e110fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 111 additions and 21 deletions

View File

@ -67,6 +67,7 @@ type NewWorkspaceComposerCardProps = {
agentPrompt: string
onAgentPromptChange: (value: string) => void
onPromptKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
linkedOnlyTemplatePreview: string | null
attachmentPaths: string[]
getAttachmentLabel: (pathValue: string) => string
onAddAttachment: () => void
@ -110,12 +111,16 @@ function PromptPrefixTextarea({
textareaRef,
value,
onChange,
onKeyDown
onKeyDown,
placeholder,
placeholderTone
}: {
textareaRef?: React.RefObject<HTMLTextAreaElement | null>
value: string
onChange: (value: string) => void
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
placeholder: string
placeholderTone: 'muted' | 'ghost-prompt'
}): React.JSX.Element {
const internalRef = React.useRef<HTMLTextAreaElement | null>(null)
@ -158,8 +163,17 @@ function PromptPrefixTextarea({
value={value}
onChange={(event) => onChange(event.target.value)}
onKeyDown={onKeyDown}
placeholder="Describe a task to start an agent, or leave blank..."
className="block min-h-[110px] w-full resize-none overflow-hidden bg-transparent py-4 pl-4 pr-4 font-mono text-[15px] leading-7 text-foreground outline-none placeholder:text-muted-foreground/50"
placeholder={placeholder}
// Why: the "ghost-prompt" tone previews the exact issueCommand
// template that will be sent to the agent when the user submits with
// only a linked work item. Emphasising it with higher contrast makes
// it obvious this is a real pending prompt, not instructional copy.
className={cn(
'block min-h-[110px] w-full resize-none overflow-hidden bg-transparent py-4 pl-4 pr-4 font-mono text-[15px] leading-7 text-foreground outline-none',
placeholderTone === 'ghost-prompt'
? 'placeholder:text-foreground/70'
: 'placeholder:text-muted-foreground/50'
)}
style={{ textIndent: '2ch' }}
spellCheck={false}
/>
@ -253,6 +267,7 @@ export default function NewWorkspaceComposerCard({
agentPrompt,
onAgentPromptChange,
onPromptKeyDown,
linkedOnlyTemplatePreview,
attachmentPaths,
getAttachmentLabel,
onAddAttachment,
@ -329,6 +344,10 @@ export default function NewWorkspaceComposerCard({
value={agentPrompt}
onChange={onAgentPromptChange}
onKeyDown={onPromptKeyDown}
placeholder={
linkedOnlyTemplatePreview ?? 'Describe a task to start an agent, or leave blank...'
}
placeholderTone={linkedOnlyTemplatePreview ? 'ghost-prompt' : 'muted'}
/>
{attachmentPaths.length > 0 || linkedWorkItem ? (
@ -516,7 +535,9 @@ export default function NewWorkspaceComposerCard({
size="sm"
className={cn(
'h-8 rounded-full border-border/50 bg-background/50 px-3 backdrop-blur-md supports-[backdrop-filter]:bg-background/50 transition-opacity',
!agentPrompt.trim() && 'opacity-60 hover:opacity-100 grayscale-[0.5]'
!agentPrompt.trim() &&
!linkedOnlyTemplatePreview &&
'opacity-60 hover:opacity-100 grayscale-[0.5]'
)}
>
<SelectValue>
@ -583,7 +604,9 @@ export default function NewWorkspaceComposerCard({
className="rounded-full px-3"
>
{creating ? <LoaderCircle className="size-4 animate-spin" /> : null}
{agentPrompt.trim() ? 'Start Agent' : 'Create Worktree'}
{agentPrompt.trim() || linkedOnlyTemplatePreview
? 'Start Agent'
: 'Create Worktree'}
<span className="ml-1 rounded-full border border-white/20 p-1 text-current/80">
<CornerDownLeft className="size-3" />
</span>

View File

@ -35,7 +35,7 @@ const EXAMPLE_TEMPLATE = `scripts:
archive: |
echo "Cleaning up before archive"
issueCommand: |
claude -p "Read issue #{{issue}} and write a design doc to docs/design-{{issue}}.md covering the approach, edge cases, and test plan." && codex exec "Review docs/design-{{issue}}.md for gaps, missing edge cases, or unclear requirements. Add feedback at the bottom."`
Complete {{artifact_url}}`
const YAML_STATE_STYLES: Record<
string,
@ -400,13 +400,15 @@ export function RepositoryHooksSection({
value={issueCommandDraft}
onChange={(e) => setIssueCommandDraft(e.target.value)}
onBlur={commitIssueCommand}
placeholder='claude -p "Read issue #{{issue}} and write a design doc to docs/design-{{issue}}.md covering the approach, edge cases, and test plan." && codex exec "Review docs/design-{{issue}}.md for gaps, missing edge cases, or unclear requirements. Add feedback at the bottom."'
placeholder="Complete {{artifact_url}}"
rows={5}
className="w-full min-w-0 resize-y rounded-md border border-input bg-transparent px-3 py-2 font-mono text-xs shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
/>
<p className="text-xs text-muted-foreground">
Use <code className="rounded bg-muted px-1 py-0.5">{'{{issue}}'}</code> for the issue
number.
Use <code className="rounded bg-muted px-1 py-0.5">{'{{artifact_url}}'}</code> for the
linked issue or PR URL. Leave empty to use the built-in{' '}
<code className="rounded bg-muted px-1 py-0.5">Complete {'{{artifact_url}}'}</code>{' '}
default.
</p>
<p className="text-xs text-muted-foreground">
Leave blank to use the repo default from{' '}

View File

@ -22,6 +22,7 @@ import type {
import {
ADD_ATTACHMENT_SHORTCUT,
CLIENT_PLATFORM,
DEFAULT_ISSUE_COMMAND_TEMPLATE,
IS_MAC,
buildAgentPromptWithContext,
ensureAgentStartupInTerminal,
@ -29,6 +30,7 @@ import {
getLinkedWorkItemSuggestedName,
getSetupConfig,
getWorkspaceSeedName,
renderIssueCommandTemplate,
type LinkedWorkItemSummary
} from '@/lib/new-workspace'
@ -59,6 +61,9 @@ export type ComposerCardProps = {
agentPrompt: string
onAgentPromptChange: (value: string) => void
onPromptKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
/** Rendered issueCommand template to preview inside the empty prompt
* textarea when the user has linked a work item but not typed anything. */
linkedOnlyTemplatePreview: string | null
attachmentPaths: string[]
getAttachmentLabel: (pathValue: string) => string
onAddAttachment: () => void
@ -276,9 +281,13 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
const setupPolicy: SetupRunPolicy = selectedRepo?.hookSettings?.setupRunPolicy ?? 'run-by-default'
const hasIssueAutomationConfig = issueCommandTemplate.length > 0
const canOfferIssueAutomation = parsedLinkedIssueNumber !== null && hasIssueAutomationConfig
const shouldRunIssueAutomation = canOfferIssueAutomation
// Why: the "no prompt + linked item" path below rehydrates the issueCommand
// template into the main startup prompt. When that happens we suppress the
// separate split pane that would otherwise run the same command twice.
const willApplyIssueCommandAsPrompt = !agentPrompt.trim() && Boolean(linkedWorkItem)
const shouldWaitForIssueAutomationCheck =
parsedLinkedIssueNumber !== null && !hasLoadedIssueCommand
(parsedLinkedIssueNumber !== null || willApplyIssueCommandAsPrompt) && !hasLoadedIssueCommand
const shouldRunIssueAutomation = canOfferIssueAutomation && !willApplyIssueCommandAsPrompt
const requiresExplicitSetupChoice = Boolean(setupConfig) && setupPolicy === 'ask'
const resolvedSetupDecision =
setupDecision ??
@ -300,15 +309,40 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
}),
[agentPrompt, linkedPR, name, parsedLinkedIssueNumber]
)
const startupPrompt = useMemo(
() =>
buildAgentPromptWithContext(
agentPrompt,
attachmentPaths,
linkedWorkItem?.url ? [linkedWorkItem.url] : []
),
[agentPrompt, attachmentPaths, linkedWorkItem?.url]
)
// Why: when the user links an issue/PR but has not typed any prompt text
// (attachments don't count), swap the generic "Linked work items:" context
// block for the repo's issueCommand template — or the built-in
// "Complete {{artifact_url}}" default when none is configured. This makes
// the common "paste a link and hit enter" flow produce a useful agent task
// instead of a bare URL bullet.
const shouldApplyLinkedOnlyTemplate =
!agentPrompt.trim() && Boolean(linkedWorkItem) && hasLoadedIssueCommand
const linkedOnlyTemplatePrompt = useMemo(() => {
if (!shouldApplyLinkedOnlyTemplate || !linkedWorkItem) {
return ''
}
const template = issueCommandTemplate.trim() || DEFAULT_ISSUE_COMMAND_TEMPLATE
return renderIssueCommandTemplate(template, {
issueNumber: linkedWorkItem.type === 'issue' ? linkedWorkItem.number : null,
artifactUrl: linkedWorkItem.url
})
}, [issueCommandTemplate, linkedWorkItem, shouldApplyLinkedOnlyTemplate])
const startupPrompt = useMemo(() => {
if (shouldApplyLinkedOnlyTemplate) {
return buildAgentPromptWithContext(linkedOnlyTemplatePrompt, attachmentPaths, [])
}
return buildAgentPromptWithContext(
agentPrompt,
attachmentPaths,
linkedWorkItem?.url ? [linkedWorkItem.url] : []
)
}, [
agentPrompt,
attachmentPaths,
linkedOnlyTemplatePrompt,
linkedWorkItem?.url,
shouldApplyLinkedOnlyTemplate
])
const normalizedLinkQuery = useMemo(
() => normalizeGitHubLinkQuery(linkDebouncedQuery, linkRepoSlug),
[linkDebouncedQuery, linkRepoSlug]
@ -732,7 +766,10 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
const issueCommand = shouldRunIssueAutomation
? {
command: issueCommandTemplate.replace(/\{\{issue\}\}/g, String(parsedLinkedIssueNumber))
command: renderIssueCommandTemplate(issueCommandTemplate, {
issueNumber: parsedLinkedIssueNumber,
artifactUrl: linkedWorkItem?.url ?? null
})
}
: undefined
const startupPlan = buildAgentStartupPlan({
@ -774,6 +811,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
createWorktree,
issueCommandTemplate,
linkedPR,
linkedWorkItem?.url,
note,
onCreated,
parsedLinkedIssueNumber,
@ -814,6 +852,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
agentPrompt,
onAgentPromptChange: setAgentPrompt,
onPromptKeyDown: handlePromptKeyDown,
linkedOnlyTemplatePreview: shouldApplyLinkedOnlyTemplate ? linkedOnlyTemplatePrompt : null,
attachmentPaths,
getAttachmentLabel,
onAddAttachment: () => void handleAddAttachment(),

View File

@ -37,6 +37,32 @@ export type LinkedWorkItemSummary = {
url: string
}
// Why: when a repo has no `orca.yaml` issueCommand and no per-user override,
// we still want the composer to send a useful default prompt whenever the user
// attaches a linked work item without typing anything else. "Complete <url>"
// is the minimum viable instruction that always produces a coherent agent task.
export const DEFAULT_ISSUE_COMMAND_TEMPLATE = 'Complete {{artifact_url}}'
/**
* Substitute the issue-command template variables. Prefers `{{artifact_url}}`
* and keeps `{{issue}}` working silently for repos that have not migrated
* their `orca.yaml` / `.orca/issue-command` yet.
*/
export function renderIssueCommandTemplate(
template: string,
vars: { issueNumber: number | null; artifactUrl: string | null }
): string {
const { issueNumber, artifactUrl } = vars
let rendered = template
if (artifactUrl !== null) {
rendered = rendered.replace(/\{\{artifact_url\}\}/g, artifactUrl)
}
if (issueNumber !== null) {
rendered = rendered.replace(/\{\{issue\}\}/g, String(issueNumber))
}
return rendered
}
export function buildAgentPromptWithContext(
prompt: string,
attachments: string[],