feat: phase 3 frontend — model badges, argument-hint composer, ancestor path display

Surface prompt model and argumentHint metadata across the UI:

- InlinePromptPill: send model in promptInvocation, show model badge
  in dropdown, show argumentHint preview, support armed-prompt state
  for prompts with argumentHint (prompt stays pinned, hint becomes
  composer placeholder, user types additional context before sending)
- ChatPane: armed-prompt state management, argumentHint-driven
  placeholder, send button enabled when prompt is armed, clear armed
  state on session change
- PromptInvocationChrome: model badge in transcript, parent-repo badge
  and shortened source path display for ancestor-relative paths
- ProjectSettingsPanel: model and argumentHint on prompt cards,
  parent-repo badge on instructions and prompts with ancestor paths,
  .claude/rules mentioned in instructions description and empty state

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-02 12:53:38 +02:00
co-authored by Copilot
parent 6eadb36c10
commit a18803758b
3 changed files with 190 additions and 49 deletions
+65 -23
View File
@@ -10,7 +10,7 @@ import { PlanReviewBanner } from '@renderer/components/chat/PlanReviewBanner';
import { McpAuthBanner } from '@renderer/components/chat/McpAuthBanner'; import { McpAuthBanner } from '@renderer/components/chat/McpAuthBanner';
import { UserInputBanner } from '@renderer/components/chat/UserInputBanner'; import { UserInputBanner } from '@renderer/components/chat/UserInputBanner';
import { InlineApprovalPill, InlineGitPill, InlineModelPill, InlineTerminalPill, InlineThinkingPill, InlineToolsPill } from '@renderer/components/chat/InlinePills'; import { InlineApprovalPill, InlineGitPill, InlineModelPill, InlineTerminalPill, InlineThinkingPill, InlineToolsPill } from '@renderer/components/chat/InlinePills';
import { InlinePromptPill } from '@renderer/components/chat/InlinePromptPill'; import { InlinePromptPill, type ArmedPrompt } from '@renderer/components/chat/InlinePromptPill';
import { ThinkingDots } from '@renderer/components/chat/ThinkingDots'; import { ThinkingDots } from '@renderer/components/chat/ThinkingDots';
import { ThinkingProcess } from '@renderer/components/chat/ThinkingProcess'; import { ThinkingProcess } from '@renderer/components/chat/ThinkingProcess';
import { SubagentActivityList } from '@renderer/components/chat/SubagentActivityCard'; import { SubagentActivityList } from '@renderer/components/chat/SubagentActivityCard';
@@ -193,6 +193,7 @@ export function ChatPane({
const canSubmitInput = hasComposerContent && !isComposerDisabled; const canSubmitInput = hasComposerContent && !isComposerDisabled;
const [pendingAttachments, setPendingAttachments] = useState<ChatMessageAttachment[]>([]); const [pendingAttachments, setPendingAttachments] = useState<ChatMessageAttachment[]>([]);
const promptFiles = useMemo(() => project.customization?.promptFiles ?? [], [project.customization?.promptFiles]); const promptFiles = useMemo(() => project.customization?.promptFiles ?? [], [project.customization?.promptFiles]);
const [armedPrompt, setArmedPrompt] = useState<ArmedPrompt | null>(null);
const toolSelection = useMemo(() => resolveSessionToolingSelection(session), [session]); const toolSelection = useMemo(() => resolveSessionToolingSelection(session), [session]);
const mcpServers = toolingSettings.mcpServers; const mcpServers = toolingSettings.mcpServers;
@@ -232,13 +233,24 @@ export function ChatPane({
setIsResolvingApproval(false); setIsResolvingApproval(false);
setIsUpdatingSessionModelConfig(false); setIsUpdatingSessionModelConfig(false);
setEditingMessageId(undefined); setEditingMessageId(undefined);
setArmedPrompt(null);
}, [session.id]); }, [session.id]);
function handleComposerSubmit(content: string) { function handleComposerSubmit(content: string) {
const attachments = pendingAttachments.length > 0 ? [...pendingAttachments] : undefined; const attachments = pendingAttachments.length > 0 ? [...pendingAttachments] : undefined;
const messageMode: MessageMode | undefined = isSessionBusy ? 'immediate' : undefined; const messageMode: MessageMode | undefined = isSessionBusy ? 'immediate' : undefined;
setPendingAttachments([]); setPendingAttachments([]);
void onSend(content, attachments, messageMode);
if (armedPrompt) {
const invocation = { ...armedPrompt.invocation };
if (content.trim()) {
invocation.resolvedPrompt = `${invocation.resolvedPrompt}\n\n${content.trim()}`;
}
setArmedPrompt(null);
void onSend('', attachments, messageMode, invocation);
} else {
void onSend(content, attachments, messageMode);
}
} }
const handleCopyMessage = useCallback((content: string) => { const handleCopyMessage = useCallback((content: string) => {
@@ -747,21 +759,23 @@ export function ChatPane({
onContentChange={setHasComposerContent} onContentChange={setHasComposerContent}
onSubmit={handleComposerSubmit} onSubmit={handleComposerSubmit}
placeholder={ placeholder={
pendingApproval armedPrompt?.prompt.argumentHint
? 'Awaiting approval...' ? armedPrompt.prompt.argumentHint
: pendingUserInput : pendingApproval
? 'Awaiting your input above...' ? 'Awaiting approval...'
: pendingPlanReview : pendingUserInput
? 'Review the plan above...' ? 'Awaiting your input above...'
: pendingMcpAuth : pendingPlanReview
? 'MCP server requires authentication...' ? 'Review the plan above...'
: isSessionBusy : pendingMcpAuth
? 'Steer the agent (sends immediately)...' ? 'MCP server requires authentication...'
: isUpdatingSessionModelConfig : isSessionBusy
? 'Saving model settings...' ? 'Steer the agent (sends immediately)...'
: isPlanMode : isUpdatingSessionModelConfig
? 'Describe what to plan...' ? 'Saving model settings...'
: 'Message...' : isPlanMode
? 'Describe what to plan...'
: 'Message...'
} }
> >
{/* Bottom action bar: left = shortcuts, right = buttons */} {/* Bottom action bar: left = shortcuts, right = buttons */}
@@ -785,7 +799,10 @@ export function ChatPane({
)} )}
{!isScratchpad && promptFiles.length > 0 && ( {!isScratchpad && promptFiles.length > 0 && (
<InlinePromptPill <InlinePromptPill
armedPrompt={armedPrompt}
disabled={isComposerDisabled} disabled={isComposerDisabled}
onArm={setArmedPrompt}
onDisarm={() => setArmedPrompt(null)}
onSubmit={(promptInvocation) => void onSend('', undefined, undefined, promptInvocation)} onSubmit={(promptInvocation) => void onSend('', undefined, undefined, promptInvocation)}
promptFiles={promptFiles} promptFiles={promptFiles}
/> />
@@ -847,9 +864,9 @@ export function ChatPane({
{/* Send / Stop / Steer button */} {/* Send / Stop / Steer button */}
<button <button
className={`flex size-8 items-center justify-center rounded-lg transition-all duration-150 ${ className={`flex size-8 items-center justify-center rounded-lg transition-all duration-150 ${
isSessionBusy && !hasComposerContent && pendingAttachments.length === 0 isSessionBusy && !hasComposerContent && pendingAttachments.length === 0 && !armedPrompt
? 'bg-[var(--color-status-error)]/80 text-white hover:bg-[var(--color-status-error)]' ? 'bg-[var(--color-status-error)]/80 text-white hover:bg-[var(--color-status-error)]'
: canSubmitInput || pendingAttachments.length > 0 : canSubmitInput || pendingAttachments.length > 0 || armedPrompt
? isSessionBusy ? isSessionBusy
? 'bg-[var(--color-status-warning)] text-white hover:brightness-110' ? 'bg-[var(--color-status-warning)] text-white hover:brightness-110'
: isPlanMode : isPlanMode
@@ -857,9 +874,9 @@ export function ChatPane({
: 'brand-gradient-bg text-white shadow-[0_2px_12px_rgba(36,92,249,0.25)] hover:shadow-[0_4px_20px_rgba(36,92,249,0.35)]' : 'brand-gradient-bg text-white shadow-[0_2px_12px_rgba(36,92,249,0.25)] hover:shadow-[0_4px_20px_rgba(36,92,249,0.35)]'
: 'bg-[var(--color-surface-2)] text-[var(--color-text-muted)]' : 'bg-[var(--color-surface-2)] text-[var(--color-text-muted)]'
}`} }`}
disabled={!canSubmitInput && !isSessionBusy && pendingAttachments.length === 0} disabled={!canSubmitInput && !isSessionBusy && pendingAttachments.length === 0 && !armedPrompt}
onClick={() => { onClick={() => {
if (isSessionBusy && !hasComposerContent && pendingAttachments.length === 0) { if (isSessionBusy && !hasComposerContent && pendingAttachments.length === 0 && !armedPrompt) {
onCancelTurn?.(); onCancelTurn?.();
} else { } else {
composerRef.current?.submit(); composerRef.current?.submit();
@@ -867,7 +884,7 @@ export function ChatPane({
}} }}
type="button" type="button"
aria-label={ aria-label={
isSessionBusy && !hasComposerContent && pendingAttachments.length === 0 isSessionBusy && !hasComposerContent && pendingAttachments.length === 0 && !armedPrompt
? 'Stop generating' ? 'Stop generating'
: isSessionBusy : isSessionBusy
? 'Steer agent' ? 'Steer agent'
@@ -933,8 +950,19 @@ export function ChatPane({
/* ── Prompt invocation chrome ───────────────────────────────── */ /* ── Prompt invocation chrome ───────────────────────────────── */
function formatInvocationSourcePath(sourcePath: string): string {
const normalized = sourcePath.replace(/\\/g, '/');
const ancestorPrefix = /^(\.\.\/)+(\.github|\.claude)\//;
if (ancestorPrefix.test(normalized)) {
const segments = normalized.split('/').filter((s) => s !== '..');
return `${segments.join('/')}`;
}
return normalized;
}
function PromptInvocationChrome({ invocation }: { invocation: ProjectPromptInvocation }) { function PromptInvocationChrome({ invocation }: { invocation: ProjectPromptInvocation }) {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const isAncestor = invocation.sourcePath.startsWith('..');
return ( return (
<div className="rounded-xl border border-[var(--color-status-success)]/20 bg-[var(--color-status-success)]/5 px-4 py-3"> <div className="rounded-xl border border-[var(--color-status-success)]/20 bg-[var(--color-status-success)]/5 px-4 py-3">
@@ -949,6 +977,11 @@ function PromptInvocationChrome({ invocation }: { invocation: ProjectPromptInvoc
{invocation.name} {invocation.name}
</span> </span>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{invocation.model && (
<span className="rounded bg-[var(--color-accent-purple)]/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-[var(--color-accent-purple)]">
{invocation.model}
</span>
)}
{invocation.agent && ( {invocation.agent && (
<span className="rounded bg-[var(--color-accent-sky)]/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-[var(--color-accent-sky)]"> <span className="rounded bg-[var(--color-accent-sky)]/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-[var(--color-accent-sky)]">
{invocation.agent} {invocation.agent}
@@ -967,7 +1000,16 @@ function PromptInvocationChrome({ invocation }: { invocation: ProjectPromptInvoc
{invocation.description && ( {invocation.description && (
<p className="mt-1 pl-6 text-[11px] text-[var(--color-text-muted)]">{invocation.description}</p> <p className="mt-1 pl-6 text-[11px] text-[var(--color-text-muted)]">{invocation.description}</p>
)} )}
<p className="mt-0.5 pl-6 text-[10px] text-[var(--color-text-muted)]">{invocation.sourcePath}</p> <div className="mt-0.5 flex items-center gap-1.5 pl-6">
{isAncestor && (
<span className="rounded bg-[var(--color-surface-3)] px-1 py-0.5 text-[9px] font-medium text-[var(--color-text-muted)]">
parent repo
</span>
)}
<span className="text-[10px] text-[var(--color-text-muted)]" title={invocation.sourcePath}>
{formatInvocationSourcePath(invocation.sourcePath)}
</span>
</div>
{expanded && ( {expanded && (
<div className="mt-2.5 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] p-3"> <div className="mt-2.5 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] p-3">
<div className="max-h-48 overflow-y-auto text-[12px] leading-relaxed text-[var(--color-text-secondary)]"> <div className="max-h-48 overflow-y-auto text-[12px] leading-relaxed text-[var(--color-text-secondary)]">
@@ -9,6 +9,13 @@ import type { ProjectAgentProfile, ProjectInstructionApplicationMode, ProjectIns
/* ── Types ────────────────────────────────────────────────── */ /* ── Types ────────────────────────────────────────────────── */
/** Shortens ancestor-relative source paths like `..\..\..\.github\prompts\x.md` to `↑ .github/prompts/x.md` */
function formatSettingsSourcePath(sourcePath: string): string {
const normalized = sourcePath.replace(/\\/g, '/');
const segments = normalized.split('/').filter((s) => s !== '..');
return `${segments.join('/')}`;
}
type ProjectSettingsSection = 'overview' | 'instructions' | 'agents' | 'prompts' | 'mcp-servers' | 'danger-zone'; type ProjectSettingsSection = 'overview' | 'instructions' | 'agents' | 'prompts' | 'mcp-servers' | 'danger-zone';
interface NavItem { interface NavItem {
@@ -335,7 +342,7 @@ function InstructionsContent({
return ( return (
<div> <div>
<SectionHeader <SectionHeader
description="Repository instructions discovered from copilot-instructions.md, AGENTS.md, CLAUDE.md, and .github/instructions/**/*.instructions.md." description="Repository instructions discovered from copilot-instructions.md, AGENTS.md, CLAUDE.md, .github/instructions/**/*.instructions.md, and .claude/rules/**/*.md."
title="Instructions" title="Instructions"
> >
<RescanButton onClick={onRescan} /> <RescanButton onClick={onRescan} />
@@ -343,7 +350,7 @@ function InstructionsContent({
{instructions.length === 0 ? ( {instructions.length === 0 ? (
<EmptyState> <EmptyState>
No instruction files found. Add <code className="text-[var(--color-text-secondary)]">.github/copilot-instructions.md</code>, <code className="text-[var(--color-text-secondary)]">AGENTS.md</code>, <code className="text-[var(--color-text-secondary)]">CLAUDE.md</code>, or <code className="text-[var(--color-text-secondary)]">*.instructions.md</code> files under <code className="text-[var(--color-text-secondary)]">.github/instructions/</code>. No instruction files found. Add <code className="text-[var(--color-text-secondary)]">.github/copilot-instructions.md</code>, <code className="text-[var(--color-text-secondary)]">AGENTS.md</code>, <code className="text-[var(--color-text-secondary)]">CLAUDE.md</code>, <code className="text-[var(--color-text-secondary)]">*.instructions.md</code> files under <code className="text-[var(--color-text-secondary)]">.github/instructions/</code>, or <code className="text-[var(--color-text-secondary)]">*.md</code> files under <code className="text-[var(--color-text-secondary)]">.claude/rules/</code>.
</EmptyState> </EmptyState>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
@@ -359,7 +366,8 @@ function InstructionsContent({
function InstructionCard({ instruction }: { instruction: ProjectInstructionFile }) { function InstructionCard({ instruction }: { instruction: ProjectInstructionFile }) {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const isLong = instruction.content.length > 300; const isLong = instruction.content.length > 300;
const displayName = instruction.name ?? instruction.sourcePath; const isAncestor = instruction.sourcePath.startsWith('..');
const displayName = instruction.name ?? (isAncestor ? formatSettingsSourcePath(instruction.sourcePath) : instruction.sourcePath);
return ( return (
<div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-glass)]"> <div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-glass)]">
@@ -370,8 +378,13 @@ function InstructionCard({ instruction }: { instruction: ProjectInstructionFile
> >
<FileCode2 className="size-4 shrink-0 text-[var(--color-text-accent)]" /> <FileCode2 className="size-4 shrink-0 text-[var(--color-text-accent)]" />
<div className="flex min-w-0 flex-1 items-center gap-2"> <div className="flex min-w-0 flex-1 items-center gap-2">
<span className="truncate text-[13px] font-medium text-[var(--color-text-primary)]">{displayName}</span> <span className="truncate text-[13px] font-medium text-[var(--color-text-primary)]" title={instruction.sourcePath}>{displayName}</span>
<InstructionModeBadge mode={instruction.applicationMode} /> <InstructionModeBadge mode={instruction.applicationMode} />
{isAncestor && (
<span className="shrink-0 rounded bg-[var(--color-surface-3)] px-1.5 py-0.5 text-[9px] font-medium text-[var(--color-text-muted)]">
parent repo
</span>
)}
{instruction.applyTo && ( {instruction.applyTo && (
<span <span
className="truncate rounded bg-[var(--color-surface-3)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-text-muted)]" className="truncate rounded bg-[var(--color-surface-3)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-text-muted)]"
@@ -571,6 +584,8 @@ function PromptsContent({
} }
function PromptCard({ prompt }: { prompt: ProjectPromptFile }) { function PromptCard({ prompt }: { prompt: ProjectPromptFile }) {
const isAncestor = prompt.sourcePath.startsWith('..');
return ( return (
<div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-glass)] px-5 py-4"> <div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-glass)] px-5 py-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
@@ -578,6 +593,11 @@ function PromptCard({ prompt }: { prompt: ProjectPromptFile }) {
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[13px] font-medium text-[var(--color-text-primary)]">{prompt.name}</span> <span className="text-[13px] font-medium text-[var(--color-text-primary)]">{prompt.name}</span>
{prompt.model && (
<span className="rounded-full bg-[var(--color-accent-purple)]/15 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-accent-purple)]">
{prompt.model}
</span>
)}
{prompt.agent && ( {prompt.agent && (
<span className="rounded-full bg-[var(--color-accent-sky)]/15 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-accent-sky)]"> <span className="rounded-full bg-[var(--color-accent-sky)]/15 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-accent-sky)]">
{prompt.agent} {prompt.agent}
@@ -588,10 +608,20 @@ function PromptCard({ prompt }: { prompt: ProjectPromptFile }) {
{prompt.variables.length} variable{prompt.variables.length === 1 ? '' : 's'} {prompt.variables.length} variable{prompt.variables.length === 1 ? '' : 's'}
</span> </span>
)} )}
{isAncestor && (
<span className="rounded-full bg-[var(--color-surface-3)] px-2 py-0.5 text-[9px] font-medium text-[var(--color-text-muted)]">
parent repo
</span>
)}
</div> </div>
{prompt.description && ( {prompt.description && (
<p className="mt-1 text-[12px] leading-relaxed text-[var(--color-text-muted)]">{prompt.description}</p> <p className="mt-1 text-[12px] leading-relaxed text-[var(--color-text-muted)]">{prompt.description}</p>
)} )}
{prompt.argumentHint && (
<p className="mt-1 text-[11px] italic text-[var(--color-text-muted)]">
Hint: {prompt.argumentHint}
</p>
)}
{prompt.tools && prompt.tools.length > 0 && ( {prompt.tools && prompt.tools.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1.5"> <div className="mt-2 flex flex-wrap gap-1.5">
{prompt.tools.map((tool) => ( {prompt.tools.map((tool) => (
@@ -617,7 +647,9 @@ function PromptCard({ prompt }: { prompt: ProjectPromptFile }) {
))} ))}
</div> </div>
)} )}
<p className="mt-1.5 text-[11px] text-[var(--color-text-muted)]">{prompt.sourcePath}</p> <p className="mt-1.5 text-[11px] text-[var(--color-text-muted)]" title={prompt.sourcePath}>
{isAncestor ? formatSettingsSourcePath(prompt.sourcePath) : prompt.sourcePath}
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -22,18 +22,41 @@ function buildPromptInvocation(prompt: ProjectPromptFile, resolvedTemplate: stri
if (prompt.description) invocation.description = prompt.description; if (prompt.description) invocation.description = prompt.description;
if (prompt.agent) invocation.agent = prompt.agent; if (prompt.agent) invocation.agent = prompt.agent;
if (prompt.model) invocation.model = prompt.model;
if (prompt.tools?.length) invocation.tools = prompt.tools; if (prompt.tools?.length) invocation.tools = prompt.tools;
return invocation; return invocation;
} }
/** Returns a human-friendly display label for source paths, shortening ancestor-relative segments. */
function formatPromptSourcePath(sourcePath: string): string {
const normalized = sourcePath.replace(/\\/g, '/');
const ancestorPrefix = /^(\.\.\/)+(\.github|\.claude)\//;
if (ancestorPrefix.test(normalized)) {
const segments = normalized.split('/').filter((s) => s !== '..');
return `${segments.join('/')}`;
}
return normalized;
}
export interface ArmedPrompt {
prompt: ProjectPromptFile;
invocation: ProjectPromptInvocation;
}
export function InlinePromptPill({ export function InlinePromptPill({
promptFiles, promptFiles,
disabled, disabled,
armedPrompt,
onArm,
onDisarm,
onSubmit, onSubmit,
}: { }: {
promptFiles: ReadonlyArray<ProjectPromptFile>; promptFiles: ReadonlyArray<ProjectPromptFile>;
disabled: boolean; disabled: boolean;
armedPrompt?: ArmedPrompt | null;
onArm?: (armed: ArmedPrompt) => void;
onDisarm?: () => void;
onSubmit: (invocation: ProjectPromptInvocation) => void; onSubmit: (invocation: ProjectPromptInvocation) => void;
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -47,23 +70,32 @@ export function InlinePromptPill({
setVariableValues({}); setVariableValues({});
}, []); }, []);
const armOrSubmit = useCallback((prompt: ProjectPromptFile, resolvedTemplate: string) => {
const invocation = buildPromptInvocation(prompt, resolvedTemplate);
if (prompt.argumentHint && onArm) {
onArm({ prompt, invocation });
handleClose();
} else {
onSubmit(invocation);
handleClose();
}
}, [onArm, onSubmit, handleClose]);
const handleSelectPrompt = useCallback((prompt: ProjectPromptFile) => { const handleSelectPrompt = useCallback((prompt: ProjectPromptFile) => {
if (prompt.variables.length === 0) { if (prompt.variables.length === 0) {
onSubmit(buildPromptInvocation(prompt, prompt.template.trim())); armOrSubmit(prompt, prompt.template.trim());
handleClose();
} else { } else {
setSelectedPrompt(prompt); setSelectedPrompt(prompt);
setVariableValues({}); setVariableValues({});
} }
}, [onSubmit, handleClose]); }, [armOrSubmit]);
const handleSubmitWithVariables = useCallback(() => { const handleSubmitWithVariables = useCallback(() => {
if (!selectedPrompt) return; if (!selectedPrompt) return;
const resolved = resolvePromptTemplate(selectedPrompt.template, variableValues).trim(); const resolved = resolvePromptTemplate(selectedPrompt.template, variableValues).trim();
if (!resolved) return; if (!resolved) return;
onSubmit(buildPromptInvocation(selectedPrompt, resolved)); armOrSubmit(selectedPrompt, resolved);
handleClose(); }, [selectedPrompt, variableValues, armOrSubmit]);
}, [selectedPrompt, variableValues, onSubmit, handleClose]);
const handleVariableChange = useCallback((name: string, value: string) => { const handleVariableChange = useCallback((name: string, value: string) => {
setVariableValues((prev) => ({ ...prev, [name]: value })); setVariableValues((prev) => ({ ...prev, [name]: value }));
@@ -78,18 +110,31 @@ export function InlinePromptPill({
return ( return (
<div className="relative" ref={ref}> <div className="relative" ref={ref}>
<button {armedPrompt ? (
aria-expanded={open} <button
aria-haspopup="listbox" className="inline-flex items-center gap-1.5 rounded-lg bg-[var(--color-status-success)]/10 px-2.5 py-1 text-[11px] font-medium text-[var(--color-status-success)] transition-all duration-200 hover:bg-[var(--color-status-success)]/20"
className="inline-flex items-center gap-1 rounded-lg px-2 py-1 text-[11px] font-medium text-[var(--color-text-muted)] transition-all duration-200 hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-primary)]" onClick={() => onDisarm?.()}
disabled={disabled} type="button"
onClick={() => setOpen(!open)} aria-label={`Disarm prompt: ${armedPrompt.prompt.name}`}
type="button" >
> <FileText className="size-3" />
<FileText className="size-3" /> <span className="max-w-[120px] truncate">{armedPrompt.prompt.name}</span>
Prompts <X className="size-3 opacity-60" />
<span className="text-[var(--color-text-muted)]">({promptFiles.length})</span> </button>
</button> ) : (
<button
aria-expanded={open}
aria-haspopup="listbox"
className="inline-flex items-center gap-1 rounded-lg px-2 py-1 text-[11px] font-medium text-[var(--color-text-muted)] transition-all duration-200 hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-primary)]"
disabled={disabled}
onClick={() => setOpen(!open)}
type="button"
>
<FileText className="size-3" />
Prompts
<span className="text-[var(--color-text-muted)]">({promptFiles.length})</span>
</button>
)}
{open && !disabled && ( {open && !disabled && (
<div <div
@@ -151,6 +196,11 @@ function PromptList({
{prompt.agent} {prompt.agent}
</span> </span>
)} )}
{prompt.model && (
<span className="shrink-0 rounded bg-[var(--color-accent-purple)]/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-[var(--color-accent-purple)]">
{prompt.model}
</span>
)}
</div> </div>
{prompt.description && ( {prompt.description && (
<div className="mt-0.5 truncate text-[11px] text-[var(--color-text-muted)]"> <div className="mt-0.5 truncate text-[11px] text-[var(--color-text-muted)]">
@@ -163,6 +213,11 @@ function PromptList({
{prompt.tools.length} tool{prompt.tools.length === 1 ? '' : 's'} {prompt.tools.length} tool{prompt.tools.length === 1 ? '' : 's'}
</span> </span>
)} )}
{prompt.argumentHint && (
<span className="rounded bg-[var(--color-surface-2)] px-1.5 py-0.5 text-[10px] italic text-[var(--color-text-muted)]">
hint: {prompt.argumentHint}
</span>
)}
{prompt.variables.map((v) => ( {prompt.variables.map((v) => (
<span <span
key={v.name} key={v.name}
@@ -172,6 +227,11 @@ function PromptList({
</span> </span>
))} ))}
</div> </div>
{prompt.sourcePath.startsWith('..') && (
<div className="mt-0.5 text-[10px] text-[var(--color-text-muted)]">
{formatPromptSourcePath(prompt.sourcePath)}
</div>
)}
</div> </div>
</button> </button>
))} ))}
@@ -206,8 +266,15 @@ function PromptVariableForm({
<X className="size-3" /> <X className="size-3" />
</button> </button>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="truncate text-[12px] font-medium text-[var(--color-text-primary)]"> <div className="flex items-center gap-1.5">
{prompt.name} <span className="truncate text-[12px] font-medium text-[var(--color-text-primary)]">
{prompt.name}
</span>
{prompt.model && (
<span className="shrink-0 rounded bg-[var(--color-accent-purple)]/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-[var(--color-accent-purple)]">
{prompt.model}
</span>
)}
</div> </div>
{prompt.description && ( {prompt.description && (
<div className="truncate text-[10px] text-[var(--color-text-muted)]">{prompt.description}</div> <div className="truncate text-[10px] text-[var(--color-text-muted)]">{prompt.description}</div>
@@ -238,7 +305,7 @@ function PromptVariableForm({
type="button" type="button"
> >
<ArrowUp className="size-3.5" /> <ArrowUp className="size-3.5" />
Send prompt {prompt.argumentHint ? 'Arm prompt' : 'Send prompt'}
</button> </button>
</div> </div>
); );