feat(settings): one page per section with a grouped nav and search

Settings moves from a tab bar to /settings/<section>: a left nav grouped
General / Agents / Routing / Git / Projects (a select on narrow screens), a
search box over every section (Ctrl/Cmd+F, Enter opens the first match), and
row primitives. Each AI tool has its page under agents/<id>, the AI Tools
modal becomes the Agents page, Pricing shows every vendor, Git → Worktrees
sets which untracked worktrees the sidebar lists, and each project gets a
page with its own worktree override and Forget all. ?tool= and #ai-tools
links redirect to the path form.
This commit is contained in:
2026-10-01 16:55:44 +07:00
parent ba2c4e1914
commit eb90244182
33 changed files with 2478 additions and 2594 deletions
+19 -2
View File
@@ -3,7 +3,8 @@
* ONE localStorage key (`agenthub.sidebar.projects`): which projects are
* collapsed, whether a repo's untracked worktrees are listed, and which
* worktree sources (`claude` = `<repo>/.claude/worktrees/*`, `agenthub` =
* LANES_ROOT) are hidden. A `useSyncExternalStore` reader that caches nothing
* LANES_ROOT) are hidden — globally, or per project root, where a project's own
* list (Settings → Projects) replaces the global one. A `useSyncExternalStore` reader that caches nothing
* but the parsed blob for the current raw string, so the sidebar and Settings
* can never disagree. Nothing here reaches the server.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
@@ -17,9 +18,16 @@ export interface ProjectTreePrefs {
collapsed: Record<string, boolean>;
showUntracked: boolean;
hideSources: string[];
/** Per project root: replaces `hideSources` for that project. */
projectHideSources: Record<string, string[]>;
}
const DEFAULTS: ProjectTreePrefs = { collapsed: {}, showUntracked: true, hideSources: [] };
const DEFAULTS: ProjectTreePrefs = {
collapsed: {},
showUntracked: true,
hideSources: [],
projectHideSources: {},
};
const listeners = new Set<() => void>();
let cache: { raw: string | null; value: ProjectTreePrefs } = { raw: null, value: DEFAULTS };
@@ -41,6 +49,10 @@ export function readProjectTreePrefs(): ProjectTreePrefs {
collapsed: parsed.collapsed && typeof parsed.collapsed === "object" ? parsed.collapsed : {},
showUntracked: parsed.showUntracked !== false,
hideSources: Array.isArray(parsed.hideSources) ? parsed.hideSources : [],
projectHideSources:
parsed.projectHideSources && typeof parsed.projectHideSources === "object"
? parsed.projectHideSources
: {},
};
} catch {
value = DEFAULTS;
@@ -58,6 +70,11 @@ export function writeProjectTreePrefs(patch: Partial<ProjectTreePrefs>): void {
listeners.forEach((l) => l());
}
/** The sources hidden for one project: its own override, else the global list. */
export function hiddenSourcesFor(prefs: ProjectTreePrefs, root: string | null): string[] {
return (root !== null && prefs.projectHideSources[root]) || prefs.hideSources;
}
function subscribe(l: () => void) {
listeners.add(l);
return () => listeners.delete(l);