Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 7 additions & 36 deletions crates/hk-core/src/adapter/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,33 +177,20 @@ impl AgentAdapter for ClaudeAdapter {
fn global_rules_files(&self) -> Vec<PathBuf> {
let mut files = vec![self.base_dir().join("CLAUDE.md")];
// Also scan ~/.claude/rules/*.md
let rules_dir = self.base_dir().join("rules");
if let Ok(entries) = std::fs::read_dir(&rules_dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.extension().is_some_and(|e| e == "md") {
files.push(p);
}
}
}
files.extend(super::files_with_ext(&self.base_dir().join("rules"), "md"));
files
}

fn global_memory_files(&self) -> Vec<PathBuf> {
// ~/.claude/projects/*/memory/*.md — outer level iterates project
// dirs (no extension filter); inner level reuses the helper.
let projects_dir = self.base_dir().join("projects");
let mut files = Vec::new();
if let Ok(entries) = std::fs::read_dir(&projects_dir) {
for entry in entries.flatten() {
let memory_dir = entry.path().join("memory");
if memory_dir.is_dir()
&& let Ok(mem_entries) = std::fs::read_dir(&memory_dir)
{
for mem_entry in mem_entries.flatten() {
let p = mem_entry.path();
if p.extension().is_some_and(|e| e == "md") {
files.push(p);
}
}
if memory_dir.is_dir() {
files.extend(super::files_with_ext(&memory_dir, "md"));
}
}
}
Expand All @@ -218,25 +205,9 @@ impl AgentAdapter for ClaudeAdapter {
self.base_dir().join("keybindings.json"),
];
// ~/.claude/commands/*.md (legacy, still functional)
let commands_dir = self.base_dir().join("commands");
if let Ok(entries) = std::fs::read_dir(&commands_dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.extension().is_some_and(|e| e == "md") {
files.push(p);
}
}
}
files.extend(super::files_with_ext(&self.base_dir().join("commands"), "md"));
// ~/.claude/output-styles/*.md
let styles_dir = self.base_dir().join("output-styles");
if let Ok(entries) = std::fs::read_dir(&styles_dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.extension().is_some_and(|e| e == "md") {
files.push(p);
}
}
}
files.extend(super::files_with_ext(&self.base_dir().join("output-styles"), "md"));
files
}

Expand Down
17 changes: 6 additions & 11 deletions crates/hk-core/src/adapter/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,12 @@ impl AgentAdapter for CodexAdapter {
}

fn global_memory_files(&self) -> Vec<PathBuf> {
let mut files = Vec::new();
let memories_dir = self.base_dir().join("memories");
if let Ok(entries) = std::fs::read_dir(&memories_dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.is_file() && p.extension().is_some_and(|e| e == "md") {
files.push(p);
}
}
}
files
// ~/.codex/memories/*.md — explicit `is_file()` preserves prior
// semantics (rejects a hypothetical directory whose name ends in
// `.md`); cheap parity over a "siblings don't bother" argument.
super::files_with_ext(&self.base_dir().join("memories"), "md")
.filter(|p| p.is_file())
.collect()
}

fn project_markers(&self) -> Vec<ProjectMarker> {
Expand Down
10 changes: 1 addition & 9 deletions crates/hk-core/src/adapter/copilot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,7 @@ impl AgentAdapter for CopilotAdapter {
self.vscode_user_dir().join("mcp.json"),
];
// ~/.copilot/hooks/*.json
let hooks_dir = self.base_dir().join("hooks");
if let Ok(entries) = std::fs::read_dir(&hooks_dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.extension().is_some_and(|e| e == "json") {
files.push(p);
}
}
}
files.extend(super::files_with_ext(&self.base_dir().join("hooks"), "json"));
files
}

Expand Down
20 changes: 2 additions & 18 deletions crates/hk-core/src/adapter/gemini.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,25 +118,9 @@ impl AgentAdapter for GeminiAdapter {
self.base_dir().join(".env"),
];
// ~/.gemini/commands/*.toml
let commands_dir = self.base_dir().join("commands");
if let Ok(entries) = std::fs::read_dir(&commands_dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.extension().is_some_and(|e| e == "toml") {
files.push(p);
}
}
}
files.extend(super::files_with_ext(&self.base_dir().join("commands"), "toml"));
// ~/.gemini/policies/*.toml
let policies_dir = self.base_dir().join("policies");
if let Ok(entries) = std::fs::read_dir(&policies_dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.extension().is_some_and(|e| e == "toml") {
files.push(p);
}
}
}
files.extend(super::files_with_ext(&self.base_dir().join("policies"), "toml"));
files
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/agents/agent-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { openDirectoryPicker, openFilePicker } from "@/lib/dialog";
import { isDesktop } from "@/lib/transport";
import {
agentDisplayName,
type ConfigCategory,
CONFIG_CATEGORY_ORDER,
type ConfigCategory,
type ConfigScope,
type ExtensionCounts,
scopeLabel,
Expand Down
4 changes: 3 additions & 1 deletion src/components/agents/config-file-entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
import { useEffect, useRef, useState } from "react";
import { useScrollPassthrough } from "@/hooks/use-scroll-passthrough";
import { openDirectoryPicker, openFilePicker } from "@/lib/dialog";
import type { AgentConfigFile } from "@/lib/types";
import { isDesktop } from "@/lib/transport";
import type { AgentConfigFile } from "@/lib/types";
import { useAgentConfigStore } from "@/stores/agent-config-store";

export function ConfigFileEntry({ file }: { file: AgentConfigFile }) {
Expand Down Expand Up @@ -229,6 +229,7 @@ export function ConfigFileEntry({ file }: { file: AgentConfigFile }) {
onClick={async (e) => {
e.stopPropagation();
await updateCustomPath(
// biome-ignore lint/style/noNonNullAssertion: outer JSX gate `file.custom_id != null` (line 178) guarantees this is set; TS narrowing doesn't propagate into the callback.
file.custom_id!,
editPath.trim(),
"",
Expand Down Expand Up @@ -300,6 +301,7 @@ export function ConfigFileEntry({ file }: { file: AgentConfigFile }) {
<button
onClick={(e) => {
e.stopPropagation();
// biome-ignore lint/style/noNonNullAssertion: outer JSX gate `file.custom_id != null` (line 289) guarantees this is set; TS narrowing doesn't propagate into the callback.
removeCustomPath(file.custom_id!);
}}
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-2.5 py-1 text-[11px] font-medium text-destructive transition-colors hover:bg-destructive/10"
Expand Down
6 changes: 2 additions & 4 deletions src/components/agents/section-anchor-rail.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { useEffect, useState } from "react";
import {
CONFIG_CATEGORY_LABELS,
CONFIG_CATEGORY_ORDER,
} from "@/lib/types";
import { CONFIG_CATEGORY_LABELS, CONFIG_CATEGORY_ORDER } from "@/lib/types";

interface SectionAnchor {
id: string;
Expand Down Expand Up @@ -33,6 +30,7 @@ export function SectionAnchorRail({ revisionKey }: { revisionKey: string }) {
const [activeId, setActiveId] = useState<string | null>(null);
const [presentIds, setPresentIds] = useState<Set<string>>(new Set());

// biome-ignore lint/correctness/useExhaustiveDependencies: revisionKey is a deliberate trigger sentinel — not read in body, only used to force re-discovery. See JSDoc.
useEffect(() => {
const present = new Set<string>();
for (const section of SECTION_CATALOG) {
Expand Down
9 changes: 6 additions & 3 deletions src/components/extensions/delete-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,12 @@ export function DeleteDialog({
const usePathBased =
isSkill && filteredSkillLocations && filteredSkillLocations.length > 0;

const items: DeleteItem[] = usePathBased
? buildPathItems(filteredSkillLocations!)
: buildAgentItems(group.instances, instanceData, group.kind, group.name);
// Redundant `filteredSkillLocations` check (already implied by usePathBased)
// is what lets TS narrow inside the true branch — cleaner than a `!`.
const items: DeleteItem[] =
usePathBased && filteredSkillLocations
? buildPathItems(filteredSkillLocations)
: buildAgentItems(group.instances, instanceData, group.kind, group.name);

const selectedKeys = deleteAgents;
const allSelected =
Expand Down
1 change: 1 addition & 0 deletions src/components/extensions/detail-paths.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function DetailPaths({
agentOrder,
);
return sortedAgentNames.map((agentName) => {
// biome-ignore lint/style/noNonNullAssertion: agentName came from byAgent.keys(), so the value exists by construction.
const instances = byAgent.get(agentName)!;
const firstData = instanceData.get(instances[0].id);
const agentLocations = filteredLocations.filter(
Expand Down
32 changes: 17 additions & 15 deletions src/components/extensions/extension-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -484,21 +484,23 @@ export function ExtensionDetail() {
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Documentation
</h4>
{isDesktop() &&
activeInstanceId &&
instanceData.get(activeInstanceId)?.path && (
<button
onClick={() =>
api.revealInFileManager(
instanceData.get(activeInstanceId)!.path!,
)
}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<FolderOpen size={12} />
Open in Finder
</button>
)}
{(() => {
const activePath = activeInstanceId
? instanceData.get(activeInstanceId)?.path
: undefined;
return (
isDesktop() &&
activePath && (
<button
onClick={() => api.revealInFileManager(activePath)}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<FolderOpen size={12} />
Open in Finder
</button>
)
);
})()}
</div>
{/* Agent tabs for switching instance content */}
{group.instances.length > 1 && (
Expand Down
1 change: 1 addition & 0 deletions src/components/extensions/extension-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export function ExtensionFilters() {
// packs that only exist globally (and vice versa). We deliberately don't
// narrow by kind/agent/tag/search — those filter the rows further; the
// dropdown options should stay stable as the user toggles them.
// biome-ignore lint/correctness/useExhaustiveDependencies: `extensions` is a trigger sentinel — grouped() reads it via Zustand closure; needed in deps so the memo re-runs on store updates.
const { scopedPacks, packCounts } = useMemo(() => {
const counts = new Map<string, number>();
for (const g of grouped()) {
Expand Down
3 changes: 2 additions & 1 deletion src/components/extensions/extension-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export function ExtensionTable({
// Subscribe to trigger re-render; accessed via getState() in cell renderers
useExtensionStore((s) => s.updateStatuses);
const toggle = useExtensionStore((s) => s.toggle);
// biome-ignore lint/correctness/useExhaustiveDependencies: `scope` is a trigger sentinel — cell renderers read scope-dependent filter results via getState(); listing it forces a column rebuild on scope change.
const columns = useMemo(
() => [
col.display({
Expand Down Expand Up @@ -196,7 +197,7 @@ export function ExtensionTable({
}),
],
// selectedIds, updateStatuses accessed via getState() inside cell renderers
// to avoid recomputing columns on every selection/status change
// to avoid recomputing columns on every selection/status change.
[agentOrder, selectAll, clearSelection, toggleSelected, toggle, scope],
);
const sorting = useExtensionStore((s) => s.tableSorting) as SortingState;
Expand Down
8 changes: 5 additions & 3 deletions src/components/extensions/new-skills-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,12 @@ export function NewSkillsDialog({
>();
for (const skill of skills) {
const key = skill.repo_url;
if (!grouped.has(key)) {
grouped.set(key, { pack: skill.pack, skills: [] });
let entry = grouped.get(key);
if (!entry) {
entry = { pack: skill.pack, skills: [] };
grouped.set(key, entry);
}
grouped.get(key)!.skills.push(skill);
entry.skills.push(skill);
}

useEffect(() => {
Expand Down
1 change: 0 additions & 1 deletion src/components/layout/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export function AppShell() {
setSearchParams(params, { replace: true });
}, [scope, scopeHydrated, searchParams, setSearchParams]);


// Window dragging — anywhere outside <main> and interactive elements
useEffect(() => {
const onMouseDown = (e: MouseEvent) => {
Expand Down
5 changes: 1 addition & 4 deletions src/components/layout/scope-switcher-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export function ScopeSwitcherMenu({ onClose }: { onClose: () => void }) {
return idx >= 0 ? idx : 0;
});

// biome-ignore lint/correctness/useExhaustiveDependencies: handleSelect and handleAddProject are new closures each render but only capture stable refs (setScope, onClose, navigate); including them would re-bind the keydown listener every render with no behavioural difference.
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === "ArrowDown") {
Expand All @@ -103,10 +104,6 @@ export function ScopeSwitcherMenu({ onClose }: { onClose: () => void }) {
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
// handleSelect / handleAddProject are stable enough for this scope —
// including activeIndex + navigableItems is sufficient to pick up
// changes that affect dispatch.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeIndex, navigableItems]);

const activeKey = navigableItems[activeIndex]?.key;
Expand Down
5 changes: 3 additions & 2 deletions src/components/onboarding/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,12 @@ function HandAnnotation({
padding: type === "circle" ? [2, 6, 4, 10] : 2,
iterations: 1,
animationDuration: type === "circle" ? 600 : 400,
roughness: type === "circle" ? 0.6 : 0.8,
// Note: rough-notation hardcodes its own roughness via getOptions(type)
// in render.js; any user-supplied `roughness` would be dead config.
...(type === "highlight" && {
color: "color-mix(in oklch, var(--primary) 15%, transparent)",
}),
} as any);
});
const timer = setTimeout(() => a.show(), delay);
return () => {
clearTimeout(timer);
Expand Down
Loading
Loading