fix: ui polish — unified topbar, selector consistency, quick actions editor improvements (#693)
This commit is contained in:
@@ -11,7 +11,15 @@ import {
|
||||
import { IconEye, IconSparkles } from "@tabler/icons-react";
|
||||
|
||||
describe("interpolatePromptTemplate", () => {
|
||||
it("replaces {url} and {title} placeholders", () => {
|
||||
it("replaces {{url}} and {{title}} placeholders", () => {
|
||||
const out = interpolatePromptTemplate("Review {{url}} titled {{title}}", {
|
||||
url: "https://gh/pr/1",
|
||||
title: "Fix bug",
|
||||
});
|
||||
expect(out).toBe("Review https://gh/pr/1 titled Fix bug");
|
||||
});
|
||||
|
||||
it("supports legacy single-brace {url} and {title}", () => {
|
||||
const out = interpolatePromptTemplate("Review {url} titled {title}", {
|
||||
url: "https://gh/pr/1",
|
||||
title: "Fix bug",
|
||||
@@ -20,7 +28,7 @@ describe("interpolatePromptTemplate", () => {
|
||||
});
|
||||
|
||||
it("leaves unknown placeholders intact", () => {
|
||||
const out = interpolatePromptTemplate("See {url} and {unknown}", {
|
||||
const out = interpolatePromptTemplate("See {{url}} and {unknown}", {
|
||||
url: "u",
|
||||
title: "t",
|
||||
});
|
||||
@@ -67,7 +75,7 @@ describe("resolvePRPresets / resolveIssuePresets", () => {
|
||||
label: "Custom",
|
||||
hint: "h",
|
||||
icon: "eye",
|
||||
prompt_template: "Do it on {url}",
|
||||
prompt_template: "Do it on {{url}}",
|
||||
},
|
||||
],
|
||||
issue: [],
|
||||
@@ -86,7 +94,7 @@ describe("toTaskPreset", () => {
|
||||
label: "L",
|
||||
hint: "H",
|
||||
icon: "message",
|
||||
prompt_template: "On {url} for {title}",
|
||||
prompt_template: "On {{url}} for {{title}}",
|
||||
};
|
||||
const preset = toTaskPreset(stored);
|
||||
expect(preset.prompt({ url: "U", title: "T" })).toBe("On U for T");
|
||||
|
||||
@@ -36,16 +36,17 @@ export function iconForPresetKey(key: string | undefined): Icon {
|
||||
return ICON_BY_KEY[key] ?? IconSparkles;
|
||||
}
|
||||
|
||||
// Interpolate `{url}` and `{title}` placeholders in a prompt template.
|
||||
// Interpolate `{{url}}` and `{{title}}` placeholders in a prompt template.
|
||||
// Also supports legacy single-brace `{url}` / `{title}` for backward compat.
|
||||
// Unknown placeholders are left untouched so the user sees what's broken.
|
||||
export function interpolatePromptTemplate(
|
||||
template: string,
|
||||
opts: { url: string; title: string },
|
||||
): string {
|
||||
return template.replace(/\{(url|title)\}/g, (_match, key) => {
|
||||
return template.replace(/\{\{?(url|title)\}\}?/g, (_match, key) => {
|
||||
if (key === "url") return opts.url;
|
||||
if (key === "title") return opts.title;
|
||||
return `{${key}}`;
|
||||
return _match;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,7 +67,7 @@ export const DEFAULT_PR_PRESETS: GitHubActionPreset[] = [
|
||||
hint: "Read the diff, flag issues",
|
||||
icon: "eye",
|
||||
prompt_template:
|
||||
"Review the pull request at {url}. Provide feedback on code quality, correctness, and suggest improvements.",
|
||||
"Review the pull request at {{url}}. Provide feedback on code quality, correctness, and suggest improvements.",
|
||||
},
|
||||
{
|
||||
id: "address_feedback",
|
||||
@@ -74,7 +75,7 @@ export const DEFAULT_PR_PRESETS: GitHubActionPreset[] = [
|
||||
hint: "Apply review comments",
|
||||
icon: "message",
|
||||
prompt_template:
|
||||
"Address the review feedback on the pull request at {url}. Make the requested changes and push them.",
|
||||
"Review the feedback on the pull request at {{url}}. Evaluate each comment critically — apply changes that improve the code, push back on suggestions that are unnecessary or harmful, and explain your reasoning. Push the changes when done.",
|
||||
},
|
||||
{
|
||||
id: "fix_ci",
|
||||
@@ -82,7 +83,7 @@ export const DEFAULT_PR_PRESETS: GitHubActionPreset[] = [
|
||||
hint: "Diagnose failing checks",
|
||||
icon: "tool",
|
||||
prompt_template:
|
||||
"Investigate and fix the CI failures on the pull request at {url}. Run the failing checks locally, diagnose, and push fixes.",
|
||||
"Investigate and fix the CI failures and merge conflicts on the pull request at {{url}}. Run the failing checks locally, resolve any conflicts, diagnose issues, and push fixes.",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -93,7 +94,7 @@ export const DEFAULT_ISSUE_PRESETS: GitHubActionPreset[] = [
|
||||
hint: "Build and open a PR",
|
||||
icon: "code",
|
||||
prompt_template:
|
||||
'Implement the changes described in the GitHub issue at {url} (title: "{title}"). Open a pull request when complete.',
|
||||
'Implement the changes described in the GitHub issue at {{url}} (title: "{{title}}"). Open a pull request when complete.',
|
||||
},
|
||||
{
|
||||
id: "investigate",
|
||||
@@ -101,7 +102,7 @@ export const DEFAULT_ISSUE_PRESETS: GitHubActionPreset[] = [
|
||||
hint: "Find the root cause",
|
||||
icon: "search",
|
||||
prompt_template:
|
||||
'Investigate the GitHub issue at {url} (title: "{title}"). Identify root cause and summarize findings.',
|
||||
'Investigate the GitHub issue at {{url}} (title: "{{title}}"). Identify root cause and summarize findings.',
|
||||
},
|
||||
{
|
||||
id: "reproduce",
|
||||
@@ -109,7 +110,7 @@ export const DEFAULT_ISSUE_PRESETS: GitHubActionPreset[] = [
|
||||
hint: "Document repro steps",
|
||||
icon: "bug",
|
||||
prompt_template:
|
||||
'Reproduce the bug described in the GitHub issue at {url} (title: "{title}"). Document the reproduction steps.',
|
||||
'Reproduce the bug described in the GitHub issue at {{url}} (title: "{{title}}"). Document the reproduction steps.',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ type PresetsSidebarProps = {
|
||||
onDeleteSaved: (id: string) => void;
|
||||
canSaveCurrent: boolean;
|
||||
onSaveCurrent: () => void;
|
||||
prPresets?: PresetOption[];
|
||||
issuePresets?: PresetOption[];
|
||||
};
|
||||
|
||||
function KindToggle({
|
||||
@@ -200,11 +202,13 @@ export function PresetsSidebar({
|
||||
onDeleteSaved,
|
||||
canSaveCurrent,
|
||||
onSaveCurrent,
|
||||
prPresets = PR_PRESETS,
|
||||
issuePresets = ISSUE_PRESETS,
|
||||
}: PresetsSidebarProps) {
|
||||
const presets = selected.kind === "pr" ? PR_PRESETS : ISSUE_PRESETS;
|
||||
const presets = selected.kind === "pr" ? prPresets : issuePresets;
|
||||
const saved = savedPresets.filter((p) => p.kind === selected.kind);
|
||||
const onKindChange = (kind: "pr" | "issue") => {
|
||||
const fallback = (kind === "pr" ? PR_PRESETS : ISSUE_PRESETS)[0]?.value ?? "";
|
||||
const fallback = (kind === "pr" ? prPresets : issuePresets)[0]?.value ?? "";
|
||||
onSelect({ kind, source: "preset", id: fallback });
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
||||
import {
|
||||
PR_PRESETS as BUILTIN_PR_PRESETS,
|
||||
ISSUE_PRESETS as BUILTIN_ISSUE_PRESETS,
|
||||
type PresetOption,
|
||||
} from "./search-bar";
|
||||
|
||||
const STORAGE_KEY = "kandev:github-default-queries:v1";
|
||||
|
||||
export type StoredQueryPreset = {
|
||||
value: string;
|
||||
label: string;
|
||||
filter: string;
|
||||
group: "inbox" | "created";
|
||||
};
|
||||
|
||||
type StoredDefaults = {
|
||||
pr: StoredQueryPreset[];
|
||||
issue: StoredQueryPreset[];
|
||||
};
|
||||
|
||||
export function toStored(presets: PresetOption[]): StoredQueryPreset[] {
|
||||
return presets.map(({ value, label, filter, group }) => ({ value, label, filter, group }));
|
||||
}
|
||||
|
||||
function readStorage(): StoredDefaults | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (
|
||||
typeof parsed !== "object" ||
|
||||
parsed === null ||
|
||||
!Array.isArray((parsed as StoredDefaults).pr) ||
|
||||
!Array.isArray((parsed as StoredDefaults).issue)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return parsed as StoredDefaults;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeStorage(defaults: StoredDefaults | null) {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
if (defaults === null) {
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
} else {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(defaults));
|
||||
}
|
||||
} catch {
|
||||
/* ignore quota / access errors */
|
||||
}
|
||||
}
|
||||
|
||||
let snapshot: StoredDefaults | null | undefined = undefined;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function publish(next: StoredDefaults | null) {
|
||||
snapshot = next;
|
||||
writeStorage(next);
|
||||
listeners.forEach((fn) => fn());
|
||||
}
|
||||
|
||||
function subscribe(cb: () => void) {
|
||||
listeners.add(cb);
|
||||
return () => {
|
||||
listeners.delete(cb);
|
||||
};
|
||||
}
|
||||
|
||||
function getSnapshot(): StoredDefaults | null {
|
||||
if (snapshot === undefined) snapshot = readStorage();
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function getServerSnapshot(): StoredDefaults | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useDefaultQueryPresets() {
|
||||
const stored = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
||||
|
||||
const prPresets = useMemo(() => stored?.pr ?? toStored(BUILTIN_PR_PRESETS), [stored]);
|
||||
const issuePresets = useMemo(() => stored?.issue ?? toStored(BUILTIN_ISSUE_PRESETS), [stored]);
|
||||
|
||||
const save = useCallback((defaults: StoredDefaults) => {
|
||||
publish(defaults);
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
publish(null);
|
||||
}, []);
|
||||
|
||||
const isCustomized = stored !== null;
|
||||
|
||||
return { prPresets, issuePresets, save, reset, isCustomized };
|
||||
}
|
||||
|
||||
/** Resolve full PresetOption[] by merging stored presets with icon lookups from builtins. */
|
||||
export function resolvePresetOptions(
|
||||
stored: StoredQueryPreset[],
|
||||
builtins: PresetOption[],
|
||||
): PresetOption[] {
|
||||
const iconMap = new Map(builtins.map((b) => [b.value, b.icon]));
|
||||
const defaultIcon = builtins[0]?.icon;
|
||||
return stored.map((s) => ({
|
||||
...s,
|
||||
icon: iconMap.get(s.value) ?? defaultIcon,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user