mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-24 05:28:46 +02:00
feat: add frontend customization UI and prompt picker
Add Copilot Customization section to ProjectSettingsPanel with instruction file previews, agent profile enable/disable toggles, and prompt file listing. Add InlinePromptPill to chat input for selecting and sending prompt files with variable substitution. Wire rescan and agent profile IPC in App.tsx. Update website with Copilot Customization feature card. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -499,9 +499,15 @@ export default function App() {
|
||||
onRescanConfigs={() => {
|
||||
void api.rescanProjectConfigs({ projectId: projectForSettings.id });
|
||||
}}
|
||||
onRescanCustomization={() => {
|
||||
void api.rescanProjectCustomization({ projectId: projectForSettings.id });
|
||||
}}
|
||||
onResolveDiscoveredTooling={(serverIds, resolution) => {
|
||||
void api.resolveProjectDiscoveredTooling({ projectId: projectForSettings.id, serverIds, resolution });
|
||||
}}
|
||||
onSetAgentProfileEnabled={(agentProfileId, enabled) => {
|
||||
void api.setProjectAgentProfileEnabled({ projectId: projectForSettings.id, agentProfileId, enabled });
|
||||
}}
|
||||
onRemoveProject={() => {
|
||||
void api.removeProject(projectForSettings.id);
|
||||
setProjectSettingsId(undefined);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PlanReviewBanner } from '@renderer/components/chat/PlanReviewBanner';
|
||||
import { McpAuthBanner } from '@renderer/components/chat/McpAuthBanner';
|
||||
import { UserInputBanner } from '@renderer/components/chat/UserInputBanner';
|
||||
import { InlineApprovalPill, InlineModelPill, InlineThinkingPill, InlineToolsPill } from '@renderer/components/chat/InlinePills';
|
||||
import { InlinePromptPill } from '@renderer/components/chat/InlinePromptPill';
|
||||
import { ThinkingDots } from '@renderer/components/chat/ThinkingDots';
|
||||
import { getAssistantMessagePhase } from '@renderer/lib/messagePhase';
|
||||
import type { ApprovalDecision } from '@shared/domain/approval';
|
||||
@@ -107,6 +108,7 @@ export function ChatPane({
|
||||
const isComposerDisabled = isUpdatingSessionModelConfig;
|
||||
const canSubmitInput = hasComposerContent && !isComposerDisabled;
|
||||
const [pendingAttachments, setPendingAttachments] = useState<ChatMessageAttachment[]>([]);
|
||||
const promptFiles = useMemo(() => project.customization?.promptFiles ?? [], [project.customization?.promptFiles]);
|
||||
|
||||
const toolSelection = useMemo(() => resolveSessionToolingSelection(session), [session]);
|
||||
const mcpServers = toolingSettings.mcpServers;
|
||||
@@ -542,6 +544,17 @@ export function ChatPane({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prompt files pill */}
|
||||
{!isScratchpad && promptFiles.length > 0 && (
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<InlinePromptPill
|
||||
disabled={isComposerDisabled}
|
||||
onSubmit={(content) => void onSend(content)}
|
||||
promptFiles={promptFiles}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Attachment preview */}
|
||||
{pendingAttachments.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 px-1 pb-2">
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { ChevronLeft, FolderOpen, GitBranch, RefreshCw, Server, Trash2, AlertTriangle, Circle } from 'lucide-react';
|
||||
import { ChevronLeft, FileCode2, FileText, FolderOpen, GitBranch, RefreshCw, Server, Sparkles, Trash2, AlertTriangle, Circle } from 'lucide-react';
|
||||
|
||||
import { ToggleSwitch } from '@renderer/components/ui';
|
||||
import type { ProjectRecord, ProjectGitContext } from '@shared/domain/project';
|
||||
import type { DiscoveredMcpServer } from '@shared/domain/discoveredTooling';
|
||||
import { listAcceptedDiscoveredMcpServers, listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling';
|
||||
import type { ProjectAgentProfile, ProjectCustomizationState, ProjectInstructionFile, ProjectPromptFile } from '@shared/domain/projectCustomization';
|
||||
|
||||
interface ProjectSettingsPanelProps {
|
||||
project: ProjectRecord;
|
||||
onClose: () => void;
|
||||
onRescanConfigs: () => void;
|
||||
onRescanCustomization: () => void;
|
||||
onResolveDiscoveredTooling: (serverIds: string[], resolution: 'accept' | 'dismiss') => void;
|
||||
onSetAgentProfileEnabled: (agentProfileId: string, enabled: boolean) => void;
|
||||
onRemoveProject: () => void;
|
||||
}
|
||||
|
||||
@@ -17,7 +21,9 @@ export function ProjectSettingsPanel({
|
||||
project,
|
||||
onClose,
|
||||
onRescanConfigs,
|
||||
onRescanCustomization,
|
||||
onResolveDiscoveredTooling,
|
||||
onSetAgentProfileEnabled,
|
||||
onRemoveProject,
|
||||
}: ProjectSettingsPanelProps) {
|
||||
const [confirmingRemove, setConfirmingRemove] = useState(false);
|
||||
@@ -25,6 +31,11 @@ export function ProjectSettingsPanel({
|
||||
const acceptedServers = listAcceptedDiscoveredMcpServers(project.discoveredTooling);
|
||||
const pendingServers = listPendingDiscoveredMcpServers(project.discoveredTooling);
|
||||
const hasDiscoveredServers = acceptedServers.length + pendingServers.length > 0;
|
||||
const customization = project.customization;
|
||||
const hasCustomization =
|
||||
(customization?.instructions?.length ?? 0) > 0 ||
|
||||
(customization?.agentProfiles?.length ?? 0) > 0 ||
|
||||
(customization?.promptFiles?.length ?? 0) > 0;
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
if (!confirmingRemove) {
|
||||
@@ -84,6 +95,36 @@ export function ProjectSettingsPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Copilot Customization */}
|
||||
{hasCustomization ? (
|
||||
<CustomizationSection
|
||||
customization={customization!}
|
||||
onRescan={onRescanCustomization}
|
||||
onSetAgentProfileEnabled={onSetAgentProfileEnabled}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<SectionHeader
|
||||
description="Instructions, custom agents, and prompt files discovered from .github/ and AGENTS.md."
|
||||
title="Copilot Customization"
|
||||
>
|
||||
<button
|
||||
className="flex items-center gap-1.5 rounded-lg bg-zinc-800 px-3 py-1.5 text-[13px] font-medium text-zinc-200 transition hover:bg-zinc-700"
|
||||
onClick={onRescanCustomization}
|
||||
title="Scan for Copilot customization files"
|
||||
type="button"
|
||||
>
|
||||
<RefreshCw className="size-3.5" />
|
||||
Scan
|
||||
</button>
|
||||
</SectionHeader>
|
||||
<div className="rounded-xl border border-dashed border-zinc-800 bg-zinc-900/20 px-5 py-8 text-center text-[12px] leading-relaxed text-zinc-500">
|
||||
No customization files found. Add <code className="text-zinc-400">.github/copilot-instructions.md</code>, <code className="text-zinc-400">AGENTS.md</code>, or files
|
||||
in <code className="text-zinc-400">.github/agents/</code> and <code className="text-zinc-400">.github/prompts/</code> to customize Copilot behavior.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Remove project */}
|
||||
<div className="border-t border-zinc-800 pt-6">
|
||||
<h3 className="text-[13px] font-semibold text-zinc-200">Danger zone</h3>
|
||||
@@ -324,6 +365,155 @@ function DiscoveredServerRow({
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Copilot Customization ────────────────────────────────── */
|
||||
|
||||
function CustomizationSection({
|
||||
customization,
|
||||
onRescan,
|
||||
onSetAgentProfileEnabled,
|
||||
}: {
|
||||
customization: ProjectCustomizationState;
|
||||
onRescan: () => void;
|
||||
onSetAgentProfileEnabled: (agentProfileId: string, enabled: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader
|
||||
description="Instructions, custom agents, and prompt files discovered from .github/ and AGENTS.md."
|
||||
title="Copilot Customization"
|
||||
>
|
||||
<button
|
||||
className="flex items-center gap-1.5 rounded-lg bg-zinc-800 px-3 py-1.5 text-[13px] font-medium text-zinc-200 transition hover:bg-zinc-700"
|
||||
onClick={onRescan}
|
||||
title="Re-scan for Copilot customization files"
|
||||
type="button"
|
||||
>
|
||||
<RefreshCw className="size-3.5" />
|
||||
Re-scan
|
||||
</button>
|
||||
</SectionHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{customization.instructions.length > 0 && (
|
||||
<CustomizationInstructionsList instructions={customization.instructions} />
|
||||
)}
|
||||
{customization.agentProfiles.length > 0 && (
|
||||
<CustomizationAgentsList
|
||||
agents={customization.agentProfiles}
|
||||
onSetEnabled={onSetAgentProfileEnabled}
|
||||
/>
|
||||
)}
|
||||
{customization.promptFiles.length > 0 && (
|
||||
<CustomizationPromptsList promptFiles={customization.promptFiles} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomizationInstructionsList({ instructions }: { instructions: ProjectInstructionFile[] }) {
|
||||
return (
|
||||
<div>
|
||||
<h4 className="mb-2 text-[12px] font-medium text-zinc-400">Instructions</h4>
|
||||
<div className="space-y-1">
|
||||
{instructions.map((instruction) => (
|
||||
<div
|
||||
key={instruction.id}
|
||||
className="rounded-xl border border-zinc-800 bg-zinc-900/40 px-4 py-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileCode2 className="size-3.5 shrink-0 text-indigo-400" />
|
||||
<span className="text-[12px] font-medium text-zinc-200">{instruction.sourcePath}</span>
|
||||
</div>
|
||||
<p className="mt-1.5 line-clamp-3 text-[11px] leading-relaxed text-zinc-500">
|
||||
{instruction.content}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomizationAgentsList({
|
||||
agents,
|
||||
onSetEnabled,
|
||||
}: {
|
||||
agents: ProjectAgentProfile[];
|
||||
onSetEnabled: (agentProfileId: string, enabled: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<h4 className="mb-2 text-[12px] font-medium text-zinc-400">Custom Agents</h4>
|
||||
<div className="space-y-1">
|
||||
{agents.map((agent) => (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="flex items-center gap-3 rounded-xl border border-transparent px-4 py-3 hover:border-zinc-800 hover:bg-zinc-900"
|
||||
>
|
||||
<Sparkles className="size-4 shrink-0 text-amber-400" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-[13px] font-medium text-zinc-200">
|
||||
{agent.displayName ?? agent.name}
|
||||
</span>
|
||||
{agent.tools && agent.tools.length > 0 && (
|
||||
<span className="rounded-full bg-zinc-800 px-2 py-0.5 text-[10px] font-medium text-zinc-400">
|
||||
{agent.tools.length} tool{agent.tools.length === 1 ? '' : 's'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{agent.description && (
|
||||
<p className="mt-0.5 truncate text-[12px] text-zinc-500">{agent.description}</p>
|
||||
)}
|
||||
<p className="mt-0.5 truncate text-[11px] text-zinc-600">{agent.sourcePath}</p>
|
||||
</div>
|
||||
<button
|
||||
aria-label={agent.enabled ? `Disable ${agent.name}` : `Enable ${agent.name}`}
|
||||
aria-pressed={agent.enabled}
|
||||
className="shrink-0"
|
||||
onClick={() => onSetEnabled(agent.id, !agent.enabled)}
|
||||
type="button"
|
||||
>
|
||||
<ToggleSwitch enabled={agent.enabled} size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomizationPromptsList({ promptFiles }: { promptFiles: ProjectPromptFile[] }) {
|
||||
return (
|
||||
<div>
|
||||
<h4 className="mb-2 text-[12px] font-medium text-zinc-400">Prompt Files</h4>
|
||||
<div className="space-y-1">
|
||||
{promptFiles.map((prompt) => (
|
||||
<div
|
||||
key={prompt.id}
|
||||
className="rounded-xl border border-transparent px-4 py-3 hover:border-zinc-800 hover:bg-zinc-900"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="size-3.5 shrink-0 text-emerald-400" />
|
||||
<span className="text-[13px] font-medium text-zinc-200">{prompt.name}</span>
|
||||
{prompt.variables.length > 0 && (
|
||||
<span className="rounded-full bg-zinc-800 px-2 py-0.5 text-[10px] font-medium text-zinc-400">
|
||||
{prompt.variables.length} variable{prompt.variables.length === 1 ? '' : 's'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{prompt.description && (
|
||||
<p className="mt-0.5 text-[12px] text-zinc-500">{prompt.description}</p>
|
||||
)}
|
||||
<p className="mt-0.5 truncate text-[11px] text-zinc-600">{prompt.sourcePath}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Shared helpers ──────────────────────────────────────── */
|
||||
|
||||
function SectionHeader({
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { ArrowUp, FileText, X } from 'lucide-react';
|
||||
|
||||
import { useClickOutside } from '@renderer/hooks/useClickOutside';
|
||||
import type { ProjectPromptFile, ProjectPromptVariable } from '@shared/domain/projectCustomization';
|
||||
|
||||
const promptVariablePattern = /\$\{input:([a-zA-Z0-9_-]+):[^}]+\}/g;
|
||||
|
||||
function resolvePromptTemplate(template: string, values: Record<string, string>): string {
|
||||
return template.replace(promptVariablePattern, (_match, name: string) => {
|
||||
return values[name] ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
export function InlinePromptPill({
|
||||
promptFiles,
|
||||
disabled,
|
||||
onSubmit,
|
||||
}: {
|
||||
promptFiles: ReadonlyArray<ProjectPromptFile>;
|
||||
disabled: boolean;
|
||||
onSubmit: (resolvedContent: string) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<ProjectPromptFile | null>(null);
|
||||
const [variableValues, setVariableValues] = useState<Record<string, string>>({});
|
||||
const ref = useClickOutside<HTMLDivElement>(() => handleClose(), open);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
setSelectedPrompt(null);
|
||||
setVariableValues({});
|
||||
}, []);
|
||||
|
||||
const handleSelectPrompt = useCallback((prompt: ProjectPromptFile) => {
|
||||
if (prompt.variables.length === 0) {
|
||||
onSubmit(prompt.template.trim());
|
||||
handleClose();
|
||||
} else {
|
||||
setSelectedPrompt(prompt);
|
||||
setVariableValues({});
|
||||
}
|
||||
}, [onSubmit, handleClose]);
|
||||
|
||||
const handleSubmitWithVariables = useCallback(() => {
|
||||
if (!selectedPrompt) return;
|
||||
const resolved = resolvePromptTemplate(selectedPrompt.template, variableValues).trim();
|
||||
if (!resolved) return;
|
||||
onSubmit(resolved);
|
||||
handleClose();
|
||||
}, [selectedPrompt, variableValues, onSubmit, handleClose]);
|
||||
|
||||
const handleVariableChange = useCallback((name: string, value: string) => {
|
||||
setVariableValues((prev) => ({ ...prev, [name]: value }));
|
||||
}, []);
|
||||
|
||||
const allVariablesFilled = useMemo(() => {
|
||||
if (!selectedPrompt) return false;
|
||||
return selectedPrompt.variables.every((v) => (variableValues[v.name] ?? '').trim().length > 0);
|
||||
}, [selectedPrompt, variableValues]);
|
||||
|
||||
if (promptFiles.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
<button
|
||||
aria-expanded={open}
|
||||
aria-haspopup="listbox"
|
||||
className="inline-flex items-center gap-1 rounded border border-zinc-700 bg-zinc-800 px-2 py-1 text-[11px] font-medium text-zinc-400 transition hover:border-zinc-600 hover:text-zinc-200"
|
||||
disabled={disabled}
|
||||
onClick={() => setOpen(!open)}
|
||||
type="button"
|
||||
>
|
||||
<FileText className="size-3" />
|
||||
Prompts
|
||||
<span className="ml-0.5 text-zinc-600">({promptFiles.length})</span>
|
||||
</button>
|
||||
|
||||
{open && !disabled && (
|
||||
<div
|
||||
className="absolute bottom-full left-0 z-40 mb-1.5 w-80 rounded-lg border border-zinc-700 bg-zinc-900 shadow-xl"
|
||||
role="listbox"
|
||||
>
|
||||
{selectedPrompt ? (
|
||||
<PromptVariableForm
|
||||
onBack={() => {
|
||||
setSelectedPrompt(null);
|
||||
setVariableValues({});
|
||||
}}
|
||||
onSubmit={handleSubmitWithVariables}
|
||||
onVariableChange={handleVariableChange}
|
||||
prompt={selectedPrompt}
|
||||
submitDisabled={!allVariablesFilled}
|
||||
values={variableValues}
|
||||
/>
|
||||
) : (
|
||||
<PromptList
|
||||
onSelect={handleSelectPrompt}
|
||||
promptFiles={promptFiles}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PromptList({
|
||||
promptFiles,
|
||||
onSelect,
|
||||
}: {
|
||||
promptFiles: ReadonlyArray<ProjectPromptFile>;
|
||||
onSelect: (prompt: ProjectPromptFile) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="max-h-64 overflow-y-auto py-1">
|
||||
<div className="px-3 py-2 text-[10px] font-semibold uppercase tracking-wider text-zinc-500">
|
||||
Prompt files
|
||||
</div>
|
||||
{promptFiles.map((prompt) => (
|
||||
<button
|
||||
key={prompt.id}
|
||||
className="flex w-full items-start gap-2.5 px-3 py-2 text-left transition hover:bg-zinc-800"
|
||||
onClick={() => onSelect(prompt)}
|
||||
role="option"
|
||||
type="button"
|
||||
>
|
||||
<FileText className="mt-0.5 size-3.5 shrink-0 text-zinc-500" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[12px] font-medium text-zinc-200">
|
||||
{prompt.name}
|
||||
</div>
|
||||
{prompt.description && (
|
||||
<div className="mt-0.5 truncate text-[11px] text-zinc-500">
|
||||
{prompt.description}
|
||||
</div>
|
||||
)}
|
||||
{prompt.variables.length > 0 && (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{prompt.variables.map((v) => (
|
||||
<span
|
||||
key={v.name}
|
||||
className="rounded bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-500"
|
||||
>
|
||||
{v.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PromptVariableForm({
|
||||
prompt,
|
||||
values,
|
||||
submitDisabled,
|
||||
onVariableChange,
|
||||
onSubmit,
|
||||
onBack,
|
||||
}: {
|
||||
prompt: ProjectPromptFile;
|
||||
values: Record<string, string>;
|
||||
submitDisabled: boolean;
|
||||
onVariableChange: (name: string, value: string) => void;
|
||||
onSubmit: () => void;
|
||||
onBack: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="p-3">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<button
|
||||
className="flex size-5 items-center justify-center rounded text-zinc-500 transition hover:bg-zinc-800 hover:text-zinc-300"
|
||||
onClick={onBack}
|
||||
type="button"
|
||||
aria-label="Back to prompt list"
|
||||
>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[12px] font-medium text-zinc-200">
|
||||
{prompt.name}
|
||||
</div>
|
||||
{prompt.description && (
|
||||
<div className="truncate text-[10px] text-zinc-500">{prompt.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
{prompt.variables.map((variable) => (
|
||||
<PromptVariableInput
|
||||
key={variable.name}
|
||||
onChange={(value) => onVariableChange(variable.name, value)}
|
||||
onSubmit={!submitDisabled ? onSubmit : undefined}
|
||||
value={values[variable.name] ?? ''}
|
||||
variable={variable}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={`mt-3 flex w-full items-center justify-center gap-1.5 rounded-lg px-3 py-2 text-[12px] font-medium transition ${
|
||||
submitDisabled
|
||||
? 'bg-zinc-800 text-zinc-600'
|
||||
: 'bg-indigo-600 text-white hover:bg-indigo-500'
|
||||
}`}
|
||||
disabled={submitDisabled}
|
||||
onClick={onSubmit}
|
||||
type="button"
|
||||
>
|
||||
<ArrowUp className="size-3.5" />
|
||||
Send prompt
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PromptVariableInput({
|
||||
variable,
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
}: {
|
||||
variable: ProjectPromptVariable;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<label className="mb-1 block text-[11px] font-medium text-zinc-400">
|
||||
{variable.name}
|
||||
</label>
|
||||
<input
|
||||
className="w-full rounded-lg border border-zinc-700 bg-zinc-800 px-2.5 py-1.5 text-[12px] text-zinc-200 placeholder-zinc-600 transition focus:border-indigo-500/50 focus:outline-none"
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && onSubmit) {
|
||||
e.preventDefault();
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
placeholder={variable.placeholder}
|
||||
type="text"
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -262,6 +262,19 @@ import Base from '../layouts/Base.astro';
|
||||
Every turn is recorded in a structured run timeline — see tool calls, agent delegation, hook execution, and context usage at a glance.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 14: Copilot Customization Files -->
|
||||
<div class="group rounded-2xl border border-zinc-800 bg-zinc-900/40 p-6 transition hover:border-zinc-700 hover:bg-zinc-900/60">
|
||||
<div class="mb-4 flex size-10 items-center justify-center rounded-xl bg-violet-500/10 text-violet-400">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold">Copilot Customization</h3>
|
||||
<p class="mt-2 text-sm leading-relaxed text-zinc-400">
|
||||
Automatically discovers <code class="text-zinc-300">.github/copilot-instructions.md</code>, <code class="text-zinc-300">AGENTS.md</code>, custom agent profiles, and prompt files from your repository. Zero configuration needed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user