mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-29 14:07:13 +02:00
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:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user