From c092fdeb1c39e5122d2a32ad338c20191dbc2a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Fri, 24 Apr 2026 21:31:46 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20ui=20polish=20=E2=80=94=20unified=20topb?= =?UTF-8?q?ar,=20selector=20consistency,=20quick=20actions=20editor=20impr?= =?UTF-8?q?ovements=20(#693)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/internal/github/models.go | 14 +- apps/packages/ui/src/scroll-on-overflow.tsx | 6 +- apps/web/app/github/github-page-client.tsx | 84 ++++--- apps/web/app/stats/stats-page-client.tsx | 105 ++++----- .../github/action-presets-section.tsx | 92 +++++--- .../github/default-queries-section.tsx | 207 ++++++++++++++++++ .../web/components/github/github-settings.tsx | 2 + .../github/my-github/action-presets.test.ts | 16 +- .../github/my-github/action-presets.ts | 19 +- .../github/my-github/presets-sidebar.tsx | 8 +- .../my-github/use-default-query-presets.ts | 116 ++++++++++ .../github/refresh-reviews-button.tsx | 67 ------ .../components/kanban-display-dropdown.tsx | 8 +- apps/web/components/kanban/kanban-header.tsx | 52 ++--- apps/web/components/page-topbar.tsx | 41 ++++ .../settings/profile-edit/script-editor.tsx | 1 + .../settings/settings-app-sidebar.tsx | 6 +- .../components/task/branch-path-popover.tsx | 10 +- .../components/task/chat/chat-input-area.tsx | 30 ++- apps/web/components/task/mode-selector.tsx | 9 +- apps/web/components/task/model-selector.tsx | 1 + .../web/components/task/quick-chat-button.tsx | 8 +- 22 files changed, 628 insertions(+), 274 deletions(-) create mode 100644 apps/web/components/github/default-queries-section.tsx create mode 100644 apps/web/components/github/my-github/use-default-query-presets.ts delete mode 100644 apps/web/components/github/refresh-reviews-button.tsx create mode 100644 apps/web/components/page-topbar.tsx diff --git a/apps/backend/internal/github/models.go b/apps/backend/internal/github/models.go index 632867bc5f..7a2145b826 100644 --- a/apps/backend/internal/github/models.go +++ b/apps/backend/internal/github/models.go @@ -440,7 +440,7 @@ const ( ) // ActionPreset is a single configurable quick-task launcher entry. -// PromptTemplate supports `{url}` and `{title}` placeholders which are +// PromptTemplate supports `{{url}}` and `{{title}}` placeholders which are // substituted client-side when the dialog is opened. type ActionPreset struct { ID string `json:"id"` @@ -474,21 +474,21 @@ func DefaultPRActionPresets() []ActionPreset { Label: "Review", Hint: "Read the diff, flag issues", Icon: "eye", - PromptTemplate: "Review the pull request at {url}. Provide feedback on code quality, correctness, and suggest improvements.", + PromptTemplate: "Review the pull request at {{url}}. Provide feedback on code quality, correctness, and suggest improvements.", }, { ID: "address_feedback", Label: "Address feedback", Hint: "Apply review comments", Icon: "message", - PromptTemplate: "Address the review feedback on the pull request at {url}. Make the requested changes and push them.", + PromptTemplate: "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", Label: "Fix CI", Hint: "Diagnose failing checks", Icon: "tool", - PromptTemplate: "Investigate and fix the CI failures on the pull request at {url}. Run the failing checks locally, diagnose, and push fixes.", + PromptTemplate: "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.", }, } } @@ -502,21 +502,21 @@ func DefaultIssueActionPresets() []ActionPreset { Label: "Implement", Hint: "Build and open a PR", Icon: "code", - PromptTemplate: `Implement the changes described in the GitHub issue at {url} (title: "{title}"). Open a pull request when complete.`, + PromptTemplate: `Implement the changes described in the GitHub issue at {{url}} (title: "{{title}}"). Open a pull request when complete.`, }, { ID: "investigate", Label: "Investigate", Hint: "Find the root cause", Icon: "search", - PromptTemplate: `Investigate the GitHub issue at {url} (title: "{title}"). Identify root cause and summarize findings.`, + PromptTemplate: `Investigate the GitHub issue at {{url}} (title: "{{title}}"). Identify root cause and summarize findings.`, }, { ID: "reproduce", Label: "Reproduce", Hint: "Document repro steps", Icon: "bug", - PromptTemplate: `Reproduce the bug described in the GitHub issue at {url} (title: "{title}"). Document the reproduction steps.`, + PromptTemplate: `Reproduce the bug described in the GitHub issue at {{url}} (title: "{{title}}"). Document the reproduction steps.`, }, } } diff --git a/apps/packages/ui/src/scroll-on-overflow.tsx b/apps/packages/ui/src/scroll-on-overflow.tsx index 94bd6df86b..eafef3e7b3 100644 --- a/apps/packages/ui/src/scroll-on-overflow.tsx +++ b/apps/packages/ui/src/scroll-on-overflow.tsx @@ -3,9 +3,8 @@ import * as React from "react"; import { cn } from "./lib/utils"; -interface ScrollOnOverflowProps { +interface ScrollOnOverflowProps extends Omit, "children"> { children: React.ReactNode; - className?: string; /** Scroll speed in pixels per second. Default 60. */ speed?: number; } @@ -16,7 +15,7 @@ interface ScrollOnOverflowProps { */ const ScrollOnOverflow = React.memo( React.forwardRef(function ScrollOnOverflow( - { children, className, speed = 60 }, + { children, className, speed = 60, ...rest }, ref, ) { const outerRef = React.useRef(null); @@ -73,6 +72,7 @@ const ScrollOnOverflow = React.memo( className={cn("inline-block min-w-0 overflow-hidden", className)} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} + {...rest} > {children} diff --git a/apps/web/app/github/github-page-client.tsx b/apps/web/app/github/github-page-client.tsx index 0ab3042c39..586eafb805 100644 --- a/apps/web/app/github/github-page-client.tsx +++ b/apps/web/app/github/github-page-client.tsx @@ -2,8 +2,9 @@ import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { IconArrowLeft, IconBrandGithub } from "@tabler/icons-react"; +import { IconBrandGithub } from "@tabler/icons-react"; import { Alert, AlertDescription } from "@kandev/ui/alert"; +import { PageTopbar } from "@/components/page-topbar"; import { useGitHubStatus } from "@/hooks/domains/github/use-github-status"; import type { Repository, Workflow, WorkflowStep } from "@/lib/types/http"; import type { GitHubIssue, GitHubPR } from "@/lib/types/github"; @@ -13,8 +14,16 @@ import { PresetsSidebar, type SidebarSelection, } from "@/components/github/my-github/presets-sidebar"; -import { PR_PRESETS, ISSUE_PRESETS } from "@/components/github/my-github/search-bar"; +import { + PR_PRESETS, + ISSUE_PRESETS, + type PresetOption, +} from "@/components/github/my-github/search-bar"; import { useGitHubSearch } from "@/components/github/my-github/use-github-search"; +import { + useDefaultQueryPresets, + resolvePresetOptions, +} from "@/components/github/my-github/use-default-query-presets"; import { useSavedPresets, type SavedPreset } from "@/components/github/my-github/use-saved-presets"; import { useKnownRepos, resetKnownReposStore } from "@/components/github/my-github/use-known-repos"; import { useCommittedQuery } from "@/components/github/my-github/use-committed-query"; @@ -41,16 +50,11 @@ type GitHubPageClientProps = { function PageHeader() { return ( -
- - - - -

My GitHub

- - Pull requests and issues across your repos. - -
+ } + /> ); } @@ -81,11 +85,16 @@ function NoWorkspaceNotice() { ); } -function resolveTitle(selection: SidebarSelection, saved: SavedPreset[]): string { +function resolveTitle( + selection: SidebarSelection, + saved: SavedPreset[], + prPresets: PresetOption[], + issuePresets: PresetOption[], +): string { if (selection.source === "saved") { return saved.find((p) => p.id === selection.id)?.label ?? "Saved query"; } - const presets = selection.kind === "pr" ? PR_PRESETS : ISSUE_PRESETS; + const presets = selection.kind === "pr" ? prPresets : issuePresets; return ( presets.find((p) => p.value === selection.id)?.label ?? (selection.kind === "pr" ? "Pull requests" : "Issues") @@ -131,16 +140,6 @@ function ResultsList({ ); } -function defaultSelection(kind: "pr" | "issue"): SidebarSelection { - const presets = kind === "pr" ? PR_PRESETS : ISSUE_PRESETS; - return { kind, source: "preset", id: presets[0]?.value ?? "" }; -} - -function defaultQuery(kind: "pr" | "issue"): string { - const presets = kind === "pr" ? PR_PRESETS : ISSUE_PRESETS; - return presets[0]?.filter ?? ""; -} - type GitHubPageState = ReturnType; function useRepoOptions( @@ -168,15 +167,28 @@ function useRepoOptions( }, [knownRepos, repoFilter]); } +function useResolvedQueryPresets() { + const { prPresets: storedPr, issuePresets: storedIssue } = useDefaultQueryPresets(); + const pr = useMemo(() => resolvePresetOptions(storedPr, PR_PRESETS), [storedPr]); + const issue = useMemo(() => resolvePresetOptions(storedIssue, ISSUE_PRESETS), [storedIssue]); + return { pr, issue }; +} + function useGitHubPageState() { - const [selection, setSelection] = useState(() => defaultSelection("pr")); + const { pr: resolvedPrPresets, issue: resolvedIssuePresets } = useResolvedQueryPresets(); + + const [selection, setSelection] = useState(() => ({ + kind: "pr", + source: "preset", + id: resolvedPrPresets[0]?.value ?? "", + })); const { draft: customQuery, committed: committedQuery, setDraft: setCustomQuery, setImmediate: setQueryImmediate, commit: commitCustomQuery, - } = useCommittedQuery(defaultQuery("pr")); + } = useCommittedQuery(resolvedPrPresets[0]?.filter ?? ""); const [repoFilter, setRepoFilter] = useState(""); const [saveDialogOpen, setSaveDialogOpen] = useState(false); const { @@ -185,7 +197,7 @@ function useGitHubPageState() { remove: removeSavedPreset, } = useSavedPresets(); - const presets = selection.kind === "pr" ? PR_PRESETS : ISSUE_PRESETS; + const presets = selection.kind === "pr" ? resolvedPrPresets : resolvedIssuePresets; const search = useGitHubSearch( selection.kind, presets, @@ -194,7 +206,10 @@ function useGitHubPageState() { repoFilter, ); const repoOptions = useRepoOptions(selection, committedQuery, search.items, repoFilter); - const title = useMemo(() => resolveTitle(selection, savedPresets), [selection, savedPresets]); + const title = useMemo( + () => resolveTitle(selection, savedPresets, resolvedPrPresets, resolvedIssuePresets), + [selection, savedPresets, resolvedPrPresets, resolvedIssuePresets], + ); const onSelect = useCallback( (s: SidebarSelection) => { @@ -205,12 +220,12 @@ function useGitHubPageState() { setRepoFilter(found?.repoFilter ?? ""); return; } - const presetList = s.kind === "pr" ? PR_PRESETS : ISSUE_PRESETS; + const presetList = s.kind === "pr" ? resolvedPrPresets : resolvedIssuePresets; const preset = presetList.find((p) => p.value === s.id); setQueryImmediate(preset?.filter ?? ""); setRepoFilter(""); }, - [savedPresets, setQueryImmediate], + [savedPresets, setQueryImmediate, resolvedPrPresets, resolvedIssuePresets], ); const canSaveCurrent = customQuery.trim().length > 0 || repoFilter.length > 0; @@ -225,8 +240,9 @@ function useGitHubPageState() { const onDeleteSaved = (id: string) => { removeSavedPreset(id); if (selection.source === "saved" && selection.id === id) { - setSelection(defaultSelection(selection.kind)); - setQueryImmediate(defaultQuery(selection.kind)); + const fallbackPresets = selection.kind === "pr" ? resolvedPrPresets : resolvedIssuePresets; + setSelection({ kind: selection.kind, source: "preset", id: fallbackPresets[0]?.value ?? "" }); + setQueryImmediate(fallbackPresets[0]?.filter ?? ""); setRepoFilter(""); } }; @@ -251,6 +267,8 @@ function useGitHubPageState() { onOpenSaveDialog, onConfirmSave, onDeleteSaved, + resolvedPrPresets, + resolvedIssuePresets, }; } @@ -278,6 +296,8 @@ function AuthenticatedLayout({ onDeleteSaved={state.onDeleteSaved} canSaveCurrent={state.canSaveCurrent} onSaveCurrent={state.onOpenSaveDialog} + prPresets={state.resolvedPrPresets} + issuePresets={state.resolvedIssuePresets} />
diff --git a/apps/web/app/stats/stats-page-client.tsx b/apps/web/app/stats/stats-page-client.tsx index 5f811aab2d..1675b0629a 100644 --- a/apps/web/app/stats/stats-page-client.tsx +++ b/apps/web/app/stats/stats-page-client.tsx @@ -1,9 +1,8 @@ "use client"; -import Link from "next/link"; -import { IconArrowLeft } from "@tabler/icons-react"; import { Card, CardContent, CardHeader, CardTitle } from "@kandev/ui/card"; import { Button } from "@kandev/ui/button"; +import { PageTopbar } from "@/components/page-topbar"; import { ToggleGroup, ToggleGroupItem } from "@kandev/ui/toggle-group"; import type { StatsResponse } from "@/lib/types/http"; import { useMemo, useState } from "react"; @@ -81,13 +80,7 @@ function getRangeLabel(range: RangeKey): string { function StatsEmptyState({ message }: { message: string }) { return (
-
- - KanDev - - / - Statistics -
+

{message}

@@ -105,56 +98,42 @@ type StatsHeaderProps = { function StatsHeader({ global, range, copied, onRangeChange, onCopy }: StatsHeaderProps) { return ( -
- -
- - KanDev - - › - Statistics - · - - {global.total_tasks} tasks · {global.total_sessions} sessions ·{" "} - {formatDuration(global.total_duration_ms)} - -
-
- { - if (v) onRangeChange(v as RangeKey); - }} - variant="outline" - className="h-7" - > - {(["week", "month", "all"] as RangeKey[]).map((key) => ( - - {getRangeLabel(key)} - - ))} - - -
-
+ + { + if (v) onRangeChange(v as RangeKey); + }} + variant="outline" + className="h-7" + > + {(["week", "month", "all"] as RangeKey[]).map((key) => ( + + {getRangeLabel(key)} + + ))} + + + + } + /> ); } @@ -343,13 +322,7 @@ export function StatsPageClient({ stats, error, workspaceId, activeRange }: Stat if (error) return (
-
- - KanDev - - / - Statistics -
+

Error loading stats: {error}

diff --git a/apps/web/components/github/action-presets-section.tsx b/apps/web/components/github/action-presets-section.tsx index f6124364cb..a85bf3a89e 100644 --- a/apps/web/components/github/action-presets-section.tsx +++ b/apps/web/components/github/action-presets-section.tsx @@ -4,7 +4,6 @@ import { createElement, useMemo, useState, useCallback } from "react"; import { IconPlus, IconTrash, IconRefresh } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { Input } from "@kandev/ui/input"; -import { Textarea } from "@kandev/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@kandev/ui/tabs"; import { useToast } from "@/components/toast-provider"; @@ -17,6 +16,26 @@ import { iconForPresetKey, } from "@/components/github/my-github/action-presets"; import type { GitHubActionPreset } from "@/lib/types/github"; +import { + ScriptEditor, + computeEditorHeight, +} from "@/components/settings/profile-edit/script-editor"; +import type { ScriptPlaceholder } from "@/components/settings/profile-edit/script-editor-completions"; + +const ACTION_PROMPT_PLACEHOLDERS: ScriptPlaceholder[] = [ + { + key: "url", + description: "URL of the PR or issue", + example: "https://github.com/org/repo/pull/42", + executor_types: [], + }, + { + key: "title", + description: "Title of the PR or issue", + example: "Fix login page crash", + executor_types: [], + }, +]; function newPreset(): GitHubActionPreset { return { @@ -31,7 +50,7 @@ function newPreset(): GitHubActionPreset { function PresetIconSelect({ value, onChange }: { value: string; onChange: (v: string) => void }) { return ( onPatch({ label: e.target.value })} - /> - onPatch({ hint: e.target.value })} - /> -
{expanded && ( -
-