Compser template (#753)
This commit is contained in:
parent
fcbdd95b43
commit
ee69e110fc
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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{' '}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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[],
|
||||
|
|
|
|||
Loading…
Reference in New Issue