@toolr/ui-design 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/ai-manifest.json +35 -20
  2. package/components/composites/dashboard-list-item.tsx +172 -0
  3. package/components/composites/dashboard-panel.tsx +218 -0
  4. package/components/content/info-panel-primitives.tsx +9 -8
  5. package/components/diagrams/diagram-utils.tsx +2 -1
  6. package/components/hooks/use-dropdown-portal.ts +39 -0
  7. package/components/lib/accent-context.ts +10 -0
  8. package/components/lib/{ai-tools.tsx → coding-agents.tsx} +23 -8
  9. package/components/lib/custom-icons.tsx +37 -0
  10. package/components/lib/git-providers.tsx +39 -0
  11. package/components/lib/theme-engine.ts +59 -10
  12. package/components/lib/toolr-brand.tsx +23 -9
  13. package/components/sections/captured-issues/captured-issues-panel.tsx +17 -8
  14. package/components/sections/{ai-tools-paths/tools-paths-panel.tsx → coding-agent-paths/agent-paths-panel.tsx} +70 -62
  15. package/components/sections/coding-agent-paths/index.ts +37 -0
  16. package/components/sections/{ai-tools-paths → coding-agent-paths}/types.ts +28 -28
  17. package/components/sections/coding-agent-paths/use-agent-paths.ts +159 -0
  18. package/components/sections/golden-snapshots/file-diff-viewer.tsx +10 -9
  19. package/components/sections/golden-snapshots/golden-sync-panel.tsx +12 -3
  20. package/components/sections/golden-snapshots/snapshot-manager.tsx +9 -7
  21. package/components/sections/golden-snapshots/status-overview.tsx +8 -8
  22. package/components/sections/golden-snapshots/version-manager.tsx +6 -6
  23. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +3 -3
  24. package/components/sections/prompt-editor/index.ts +1 -1
  25. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +13 -5
  26. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +18 -10
  27. package/components/sections/prompt-editor/types.ts +2 -2
  28. package/components/sections/report-bug/report-bug-form.tsx +12 -4
  29. package/components/sections/report-bug/screenshot-uploader.tsx +11 -3
  30. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +12 -4
  31. package/components/sections/snapshot-browser/snapshot-tree.tsx +5 -4
  32. package/components/sections/snapshot-browser/types.ts +1 -1
  33. package/components/sections/snippets-editor/snippets-editor.tsx +16 -9
  34. package/components/settings/SettingsHeader.tsx +2 -2
  35. package/components/settings/SettingsPanel.tsx +11 -3
  36. package/components/settings/SettingsTreeNav.tsx +15 -9
  37. package/components/ui/action-dialog.tsx +24 -30
  38. package/components/ui/ai-action-button.tsx +10 -7
  39. package/components/ui/ai-execution-action-buttons.tsx +13 -5
  40. package/components/ui/badge.tsx +7 -4
  41. package/components/ui/bottom-panel-header.tsx +9 -5
  42. package/components/ui/breadcrumb.tsx +9 -1
  43. package/components/ui/{extension-list-card.tsx → capability-list-card.tsx} +13 -5
  44. package/components/ui/checkbox.tsx +6 -3
  45. package/components/ui/collapsible-section.tsx +38 -29
  46. package/components/ui/confirm-badge.tsx +7 -4
  47. package/components/ui/cookie-consent.tsx +13 -7
  48. package/components/ui/detail-section.tsx +24 -16
  49. package/components/ui/detail-view-wrapper.tsx +30 -22
  50. package/components/ui/editor-placeholder-card.tsx +28 -24
  51. package/components/ui/editor-toolbar.tsx +7 -4
  52. package/components/ui/execution-details-panel.tsx +10 -5
  53. package/components/ui/file-structure-section.tsx +3 -3
  54. package/components/ui/file-tree.tsx +3 -1
  55. package/components/ui/files-panel.tsx +147 -27
  56. package/components/ui/filter-dropdown.tsx +84 -74
  57. package/components/ui/form-actions.tsx +14 -6
  58. package/components/ui/frontmatter-form-header.tsx +10 -2
  59. package/components/ui/icon-button.tsx +22 -9
  60. package/components/ui/input.tsx +7 -4
  61. package/components/ui/label.tsx +5 -5
  62. package/components/ui/layout-tab-bar.tsx +7 -5
  63. package/components/ui/modal.tsx +18 -4
  64. package/components/ui/nav-card.tsx +6 -3
  65. package/components/ui/navigation-bar.tsx +37 -9
  66. package/components/ui/number-input.tsx +8 -4
  67. package/components/ui/project-explorer.tsx +666 -0
  68. package/components/ui/registry-browser.tsx +12 -1
  69. package/components/ui/registry-card.tsx +49 -42
  70. package/components/ui/registry-detail.tsx +34 -11
  71. package/components/ui/resizable-textarea.tsx +18 -11
  72. package/components/ui/scope-badge.tsx +18 -11
  73. package/components/ui/segmented-toggle.tsx +5 -2
  74. package/components/ui/select.tsx +12 -9
  75. package/components/ui/selection-grid.tsx +36 -37
  76. package/components/ui/setting-row.tsx +2 -2
  77. package/components/ui/settings-card.tsx +10 -3
  78. package/components/ui/settings-info-box.tsx +9 -5
  79. package/components/ui/settings-section-title.tsx +14 -2
  80. package/components/ui/snapshot-card.tsx +10 -2
  81. package/components/ui/snippets-panel.tsx +4 -2
  82. package/components/ui/sort-dropdown.tsx +39 -29
  83. package/components/ui/status-card.tsx +9 -1
  84. package/components/ui/tab-bar.tsx +12 -9
  85. package/components/ui/toggle.tsx +13 -7
  86. package/components/ui/tooltip.tsx +9 -1
  87. package/dist/content.js +8 -8
  88. package/dist/diagrams.d.ts +0 -1
  89. package/dist/index.d.ts +421 -182
  90. package/dist/index.js +2984 -1691
  91. package/dist/tokens/primitives.css +28 -6
  92. package/dist/tokens/semantic.css +15 -15
  93. package/dist/tokens/theme.css +23 -0
  94. package/index.ts +25 -11
  95. package/package.json +1 -1
  96. package/tokens/primitives.css +28 -6
  97. package/tokens/semantic.css +15 -15
  98. package/tokens/theme.css +23 -0
  99. package/components/sections/ai-tools-paths/index.ts +0 -37
  100. package/components/sections/ai-tools-paths/use-tools-paths.ts +0 -159
@@ -1,25 +1,25 @@
1
1
  /**
2
- * ToolsPathsPanelAI tool binary path configuration panel
2
+ * AgentPathsPanelCoding agent binary path configuration panel
3
3
  *
4
- * Part of: Sections > AI Tools Paths
4
+ * Part of: Sections > Coding Agent Paths
5
5
  *
6
6
  * This is the main "drop in and it works" component. Provide an API adapter,
7
- * tool configs, and a change callback — it handles the full detection and
7
+ * agent configs, and a change callback — it handles the full detection and
8
8
  * configuration UI.
9
9
  *
10
10
  * Usage:
11
- * <ToolsPathsPanel
12
- * api={toolsPathsApi}
13
- * tools={toolConfigs}
14
- * onToolConfigChange={(id, partial) => updateTool(id, partial)}
11
+ * <AgentPathsPanel
12
+ * api={agentPathsApi}
13
+ * agents={agentConfigs}
14
+ * onAgentConfigChange={(id, partial) => updateAgent(id, partial)}
15
15
  * />
16
16
  *
17
17
  * AI agent notes:
18
- * - Replicates the configr Settings > AI Tools > Paths page layout
19
- * - Each tool row shows: icon, name, status badge, toggle, path input, refresh
20
- * - The component manages detection state internally via useToolsPaths hook
21
- * - Tool configs (enabled, path, detected) are owned by the consumer
22
- * - The renderToolIcon prop allows each app to provide its own tool icons
18
+ * - Replicates the configr Settings > Coding Agents > Paths page layout
19
+ * - Each agent row shows: icon, name, status badge, toggle, path input, refresh
20
+ * - The component manages detection state internally via useAgentPaths hook
21
+ * - Agent configs (enabled, path, detected) are owned by the consumer
22
+ * - The renderAgentIcon prop allows each app to provide its own agent icons
23
23
  * - Uses ui-design components (Input, Toggle, IconButton, Tooltip)
24
24
  */
25
25
 
@@ -29,40 +29,47 @@ import { cn } from '../../lib/cn.ts'
29
29
  import { Input } from '../../ui/input.tsx'
30
30
  import { Toggle } from '../../ui/toggle.tsx'
31
31
  import { IconButton } from '../../ui/icon-button.tsx'
32
- import { useToolsPaths } from './use-tools-paths.ts'
33
- import type { AiToolConfig, ToolsPathsApi } from './types.ts'
32
+ import { useAccentColor, AccentColorProvider } from '../../lib/accent-context.ts'
33
+ import type { FormColor } from '../../lib/form-colors.ts'
34
+ import { useAgentPaths } from './use-agent-paths.ts'
35
+ import type { CodingAgentConfig, AgentPathsApi } from './types.ts'
34
36
 
35
- export interface ToolsPathsPanelProps {
37
+ export interface AgentPathsPanelProps {
36
38
  /** API adapter for backend operations (detection, validation) */
37
- api: ToolsPathsApi
38
- /** Current tool configurations */
39
- tools: AiToolConfig[]
40
- /** Called when a tool's config changes. Consumer updates their state. */
41
- onToolConfigChange: (toolId: string, config: Partial<AiToolConfig>) => void
42
- /** Optional render function for tool icons. Receives tool ID, returns ReactNode. */
43
- renderToolIcon?: (toolId: string) => ReactNode
39
+ api: AgentPathsApi
40
+ /** Current agent configurations */
41
+ agents: CodingAgentConfig[]
42
+ /** Called when an agent's config changes. Consumer updates their state. */
43
+ onAgentConfigChange: (agentId: string, config: Partial<CodingAgentConfig>) => void
44
+ /** Optional render function for agent icons. Receives agent ID, returns ReactNode. */
45
+ renderAgentIcon?: (agentId: string) => ReactNode
46
+ accentColor?: FormColor
44
47
  className?: string
45
48
  }
46
49
 
47
- export function ToolsPathsPanel({
50
+ export function AgentPathsPanel({
48
51
  api,
49
- tools,
50
- onToolConfigChange,
51
- renderToolIcon,
52
+ agents,
53
+ onAgentConfigChange,
54
+ renderAgentIcon,
55
+ accentColor,
52
56
  className,
53
- }: ToolsPathsPanelProps) {
57
+ }: AgentPathsPanelProps) {
58
+ const contextAccent = useAccentColor()
59
+ const effectiveColor = accentColor ?? contextAccent ?? 'blue'
54
60
  const {
55
61
  isDetecting,
56
62
  hasScanned,
57
- refreshingTools,
58
- scannedTools,
63
+ refreshingAgents,
64
+ scannedAgents,
59
65
  detectAll,
60
- detectTool,
66
+ detectAgent,
61
67
  validateAndUpdatePath,
62
68
  toggleEnabled,
63
- } = useToolsPaths({ api, tools, onToolConfigChange })
69
+ } = useAgentPaths({ api, agents, onAgentConfigChange })
64
70
 
65
71
  return (
72
+ <AccentColorProvider value={effectiveColor}>
66
73
  <div className={cn('w-full', className)}>
67
74
  {/* Header */}
68
75
  <div className="flex items-center justify-between mb-4">
@@ -83,40 +90,40 @@ export function ToolsPathsPanel({
83
90
  onClick={detectAll}
84
91
  disabled={isDetecting}
85
92
  size="sm"
86
- color={hasScanned ? 'green' : 'neutral'}
93
+ accentColor={hasScanned ? 'green' : 'neutral'}
87
94
  tooltip={{
88
- title: hasScanned ? 'Scan complete' : 'Scan for tools',
89
- description: hasScanned ? 'CLI tools detected' : 'Auto-detect CLI tool locations',
95
+ title: hasScanned ? 'Scan complete' : 'Scan for agents',
96
+ description: hasScanned ? 'CLI agents detected' : 'Auto-detect CLI agent locations',
90
97
  }}
91
98
  />
92
99
  </div>
93
100
 
94
- {/* Tool list */}
95
- <div className="bg-neutral-900 border border-neutral-700 rounded-lg divide-y divide-neutral-700">
96
- {tools.map((tool) => {
97
- const isRefreshing = refreshingTools.has(tool.id)
98
- const isEnabled = tool.enabled
99
- const displayPath = tool.binaryPath || tool.detectedPath || ''
100
- const hasBeenScanned = scannedTools.has(tool.id) && tool.detected
101
+ {/* Agent list */}
102
+ <div className="bg-neutral-980 border border-neutral-700 rounded-lg divide-y divide-neutral-700">
103
+ {agents.map((agent) => {
104
+ const isRefreshing = refreshingAgents.has(agent.id)
105
+ const isEnabled = agent.enabled
106
+ const displayPath = agent.binaryPath || agent.detectedPath || ''
107
+ const hasBeenScanned = scannedAgents.has(agent.id) && agent.detected
101
108
 
102
109
  return (
103
110
  <div
104
- key={tool.id}
105
- className={cn('p-4 space-y-2', !tool.detected && 'opacity-60')}
111
+ key={agent.id}
112
+ className={cn('p-4 space-y-2', !agent.detected && 'opacity-60')}
106
113
  >
107
114
  {/* Name + status + toggle row */}
108
115
  <div className="flex items-center justify-between">
109
116
  <div className="relative flex items-center gap-2">
110
- {renderToolIcon?.(tool.id)}
117
+ {renderAgentIcon?.(agent.id)}
111
118
  <span
112
119
  className={cn(
113
120
  'font-medium',
114
- tool.detected && !isEnabled ? 'text-neutral-500' : 'text-neutral-300',
121
+ agent.detected && !isEnabled ? 'text-neutral-500' : 'text-neutral-300',
115
122
  )}
116
123
  >
117
- {tool.name}
124
+ {agent.name}
118
125
  </span>
119
- {tool.detected && !isEnabled && (
126
+ {agent.detected && !isEnabled && (
120
127
  <div
121
128
  className="absolute inset-0 flex items-center pointer-events-none"
122
129
  aria-hidden="true"
@@ -126,7 +133,7 @@ export function ToolsPathsPanel({
126
133
  )}
127
134
  </div>
128
135
  <div className="flex items-center gap-3">
129
- {tool.detected ? (
136
+ {agent.detected ? (
130
137
  <span className="flex items-center gap-1 text-sm text-green-400">
131
138
  <Check className="w-3.5 h-3.5" />
132
139
  Installed
@@ -137,10 +144,10 @@ export function ToolsPathsPanel({
137
144
  Not Found
138
145
  </span>
139
146
  )}
140
- {tool.detected && (
147
+ {agent.detected && (
141
148
  <Toggle
142
149
  checked={isEnabled}
143
- onChange={() => toggleEnabled(tool.id)}
150
+ onChange={() => toggleEnabled(agent.id)}
144
151
  />
145
152
  )}
146
153
  </div>
@@ -153,8 +160,8 @@ export function ToolsPathsPanel({
153
160
  <div className="flex-1">
154
161
  <Input
155
162
  value={displayPath}
156
- onChange={(newPath) => validateAndUpdatePath(tool.id, newPath)}
157
- placeholder={`/usr/local/bin/${tool.id}`}
163
+ onChange={(newPath) => validateAndUpdatePath(agent.id, newPath)}
164
+ placeholder={`/usr/local/bin/${agent.id}`}
158
165
  size="sm"
159
166
  className={displayPath ? 'text-white' : 'text-neutral-400'}
160
167
  />
@@ -169,38 +176,38 @@ export function ToolsPathsPanel({
169
176
  <RefreshCw className="w-3 h-3 text-neutral-300" />
170
177
  )
171
178
  }
172
- onClick={() => detectTool(tool.id)}
179
+ onClick={() => detectAgent(agent.id)}
173
180
  disabled={isRefreshing}
174
- color={hasBeenScanned ? 'green' : 'neutral'}
181
+ accentColor={hasBeenScanned ? 'green' : 'neutral'}
175
182
  size="sm"
176
183
  tooltipPosition="bottom"
177
184
  tooltip={{
178
- title: hasBeenScanned ? 'Scan complete' : `Scan for ${tool.name}`,
179
- description: hasBeenScanned ? 'Click to re-scan' : 'Check if CLI tool is installed',
185
+ title: hasBeenScanned ? 'Scan complete' : `Scan for ${agent.name}`,
186
+ description: hasBeenScanned ? 'Click to re-scan' : 'Check if CLI agent is installed',
180
187
  }}
181
188
  />
182
189
  </div>
183
190
 
184
191
  {/* Helper text */}
185
- {isEnabled && !displayPath?.includes('/') && !tool.detected && (
192
+ {isEnabled && !displayPath?.includes('/') && !agent.detected && (
186
193
  <p className="text-sm text-red-400 pl-5">
187
194
  Path required when enabled. Enter full path to binary.
188
195
  </p>
189
196
  )}
190
- {tool.detected && !tool.binaryPath && (
197
+ {agent.detected && !agent.binaryPath && (
191
198
  <p className="text-sm text-neutral-500 pl-5">Using auto-detected path</p>
192
199
  )}
193
- {!isEnabled && !displayPath?.includes('/') && !tool.detected && (
200
+ {!isEnabled && !displayPath?.includes('/') && !agent.detected && (
194
201
  <p className="text-sm text-neutral-500 pl-5">
195
- Tool disabled. Enter path to enable.
202
+ Agent disabled. Enter path to enable.
196
203
  </p>
197
204
  )}
198
205
  </div>
199
206
 
200
207
  {/* Disabled warning */}
201
- {tool.detected && !isEnabled && (
208
+ {agent.detected && !isEnabled && (
202
209
  <p className="text-sm text-yellow-500">
203
- Tool disabled — enable to use in your workflow
210
+ Agent disabled — enable to use in your workflow
204
211
  </p>
205
212
  )}
206
213
  </div>
@@ -208,5 +215,6 @@ export function ToolsPathsPanel({
208
215
  })}
209
216
  </div>
210
217
  </div>
218
+ </AccentColorProvider>
211
219
  )
212
220
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Coding Agent Paths — Section barrel export
3
+ *
4
+ * This section provides a complete, reusable coding agent binary path configuration
5
+ * panel. It handles agent detection, path validation, enable/disable toggles,
6
+ * and per-agent refresh — all via an API adapter interface.
7
+ *
8
+ * File structure:
9
+ * - agent-paths-panel.tsx — Main panel component (drop-in usage)
10
+ * - use-agent-paths.ts — Detection state & actions hook (used by panel, also standalone)
11
+ * - types.ts — Shared types and API adapter interface
12
+ *
13
+ * Quick start for consuming apps:
14
+ * import { AgentPathsPanel, type AgentPathsApi, type CodingAgentConfig } from '@toolr/ui-design'
15
+ *
16
+ * const api: AgentPathsApi = {
17
+ * detectAll: () => invoke('detect_all_agents'),
18
+ * detectAgent: (id) => invoke('detect_agent', { agentId: id }),
19
+ * validatePath: (path) => invoke('validate_binary_path', { path }),
20
+ * }
21
+ *
22
+ * <AgentPathsPanel
23
+ * api={api}
24
+ * agents={agentConfigs}
25
+ * onAgentConfigChange={(id, partial) => updateAgent(id, partial)}
26
+ * renderAgentIcon={(id) => <CodingAgentIcon agent={id} size={20} />}
27
+ * />
28
+ */
29
+
30
+ // Main panel component
31
+ export { AgentPathsPanel, type AgentPathsPanelProps } from './agent-paths-panel.tsx'
32
+
33
+ // Hook for custom UIs
34
+ export { useAgentPaths, type UseAgentPathsOptions, type UseAgentPathsReturn } from './use-agent-paths.ts'
35
+
36
+ // Types and API interface
37
+ export type { CodingAgentConfig, AgentDetectionResult, AgentPathsApi } from './types.ts'
@@ -1,7 +1,7 @@
1
1
  /**
2
- * AI Tools Paths — Shared type definitions
2
+ * Coding Agent Paths — Shared type definitions
3
3
  *
4
- * Part of: Sections > AI Tools Paths
4
+ * Part of: Sections > Coding Agent Paths
5
5
  *
6
6
  * These types define the CONTRACT between the UI layer (this package) and
7
7
  * the Rust/Tauri backend that each app must implement. Every type here has
@@ -13,27 +13,27 @@
13
13
  * to match these TypeScript interfaces.
14
14
  *
15
15
  * Generic design:
16
- * - Tools are keyed by string ID (not hardcoded to specific tools)
16
+ * - Agents are keyed by string ID (not hardcoded to specific agents)
17
17
  * - Configr uses: "claude", "gemini", "copilot", "codex", "opencode"
18
- * - Other apps can define their own tool sets
19
- * - The UI renders whatever tools the consumer provides
18
+ * - Other apps can define their own agent sets
19
+ * - The UI renders whatever agents the consumer provides
20
20
  */
21
21
 
22
22
  // ---------------------------------------------------------------------------
23
- // Tool configuration
23
+ // Agent configuration
24
24
  // ---------------------------------------------------------------------------
25
25
 
26
- /** Per-tool configuration state */
27
- export interface AiToolConfig {
28
- /** Unique identifier for the tool (e.g. "claude", "gemini") */
26
+ /** Per-agent configuration state */
27
+ export interface CodingAgentConfig {
28
+ /** Unique identifier for the agent (e.g. "claude", "gemini") */
29
29
  id: string
30
30
  /** Display name (e.g. "Claude Code", "Gemini CLI") */
31
31
  name: string
32
- /** Whether the tool is enabled for use */
32
+ /** Whether the agent is enabled for use */
33
33
  enabled: boolean
34
34
  /** User-configured or auto-detected binary path */
35
35
  binaryPath: string
36
- /** Whether the tool was found during detection */
36
+ /** Whether the agent was found during detection */
37
37
  detected: boolean
38
38
  /** Path found by auto-detection (may differ from binaryPath if user overrode it) */
39
39
  detectedPath?: string
@@ -43,8 +43,8 @@ export interface AiToolConfig {
43
43
  // Detection results
44
44
  // ---------------------------------------------------------------------------
45
45
 
46
- /** Result of detecting a single tool's binary */
47
- export interface ToolDetectionResult {
46
+ /** Result of detecting a single agent's binary */
47
+ export interface AgentDetectionResult {
48
48
  /** Whether the binary was found */
49
49
  detected: boolean
50
50
  /** Resolved path to the binary, if found */
@@ -56,7 +56,7 @@ export interface ToolDetectionResult {
56
56
  // ---------------------------------------------------------------------------
57
57
 
58
58
  /**
59
- * ToolsPathsApi — The callback interface that each app must implement.
59
+ * AgentPathsApi — The callback interface that each app must implement.
60
60
  *
61
61
  * This is the bridge between the shared UI and the app-specific backend.
62
62
  * Each function maps to a Tauri command (or any other backend).
@@ -73,12 +73,12 @@ export interface ToolDetectionResult {
73
73
  * │ REQUIRED Tauri commands for full functionality: │
74
74
  * │ │
75
75
  * │ DETECTION: │
76
- * │ detect_all_tools() → Record<string, ToolDetectionResult>
77
- * │ Scans PATH and known install locations for all tool binaries.
78
- * │ Returns a map of tool ID → detection result.
76
+ * │ detect_all_agents() → Record<string, AgentDetectionResult>
77
+ * │ Scans PATH and known install locations for all agent binaries.
78
+ * │ Returns a map of agent ID → detection result.
79
79
  * │ │
80
- * │ detect_tool(toolId) → ToolDetectionResult
81
- * │ Scans for a single tool binary. Used for per-tool refresh.
80
+ * │ detect_agent(agentId) → AgentDetectionResult
81
+ * │ Scans for a single agent binary. Used for per-agent refresh.
82
82
  * │ │
83
83
  * │ VALIDATION: │
84
84
  * │ validate_binary_path(path) → bool │
@@ -86,25 +86,25 @@ export interface ToolDetectionResult {
86
86
  * │ Typically: fs::metadata(path).is_ok() && is_executable(path) │
87
87
  * │ │
88
88
  * │ DETECTION STRATEGY (Rust backend should implement): │
89
- * │ 1. Check PATH environment variable (which <tool-binary>)
89
+ * │ 1. Check PATH environment variable (which <agent-binary>)
90
90
  * │ 2. Check common install locations: │
91
91
  * │ - /usr/local/bin/<binary> │
92
92
  * │ - /opt/homebrew/bin/<binary> │
93
93
  * │ - ~/.local/bin/<binary> │
94
- * │ - ~/.cargo/bin/<binary> (for Rust-based tools)
95
- * │ - ~/.npm/bin/<binary> (for npm-based tools)
96
- * │ 3. Check tool-specific locations:
94
+ * │ - ~/.cargo/bin/<binary> (for Rust-based agents)
95
+ * │ - ~/.npm/bin/<binary> (for npm-based agents)
96
+ * │ 3. Check agent-specific locations:
97
97
  * │ - Claude: ~/.claude/bin/claude │
98
98
  * │ - Copilot: gh extension path │
99
99
  * │ 4. Return first valid path found, or detected: false │
100
100
  * └─────────────────────────────────────────────────────────────────────┘
101
101
  */
102
- export interface ToolsPathsApi {
103
- /** Scan all tools and return detection results keyed by tool ID */
104
- detectAll: () => Promise<Record<string, ToolDetectionResult>>
102
+ export interface AgentPathsApi {
103
+ /** Scan all agents and return detection results keyed by agent ID */
104
+ detectAll: () => Promise<Record<string, AgentDetectionResult>>
105
105
 
106
- /** Scan a single tool and return its detection result */
107
- detectTool: (toolId: string) => Promise<ToolDetectionResult>
106
+ /** Scan a single agent and return its detection result */
107
+ detectAgent: (agentId: string) => Promise<AgentDetectionResult>
108
108
 
109
109
  /** Check if a given binary path is valid (exists and is executable) */
110
110
  validatePath: (path: string) => Promise<boolean>
@@ -0,0 +1,159 @@
1
+ /**
2
+ * useAgentPaths — Agent detection and configuration state hook
3
+ *
4
+ * Part of: Sections > Coding Agent Paths
5
+ *
6
+ * Manages agent detection state, per-agent refresh tracking, and path validation.
7
+ * Used internally by AgentPathsPanel but can also be used standalone for
8
+ * custom UIs.
9
+ *
10
+ * AI agent notes:
11
+ * - This hook does NOT store agent configs — the consumer owns that state.
12
+ * It only manages detection/scanning ephemeral state.
13
+ * - The hook tracks which agents have been scanned, which are currently
14
+ * refreshing, and whether a full scan has completed.
15
+ * - Path validation is debounced by the consumer (typically on blur or
16
+ * after typing stops).
17
+ */
18
+
19
+ import { useState, useCallback, useRef, useEffect } from 'react'
20
+ import type { CodingAgentConfig, AgentPathsApi, AgentDetectionResult } from './types.ts'
21
+
22
+ export interface UseAgentPathsOptions {
23
+ api: AgentPathsApi
24
+ agents: CodingAgentConfig[]
25
+ onAgentConfigChange: (agentId: string, config: Partial<CodingAgentConfig>) => void
26
+ }
27
+
28
+ export interface UseAgentPathsReturn {
29
+ /** Whether a full scan is in progress */
30
+ isDetecting: boolean
31
+ /** Whether a full scan has completed at least once */
32
+ hasScanned: boolean
33
+ /** Set of agent IDs currently being individually refreshed */
34
+ refreshingAgents: Set<string>
35
+ /** Set of agent IDs that were found during the last scan */
36
+ scannedAgents: Set<string>
37
+ /** Run detection for all agents */
38
+ detectAll: () => Promise<void>
39
+ /** Run detection for a single agent */
40
+ detectAgent: (agentId: string) => Promise<void>
41
+ /** Validate and update an agent's binary path */
42
+ validateAndUpdatePath: (agentId: string, path: string) => Promise<void>
43
+ /** Toggle an agent's enabled state */
44
+ toggleEnabled: (agentId: string) => void
45
+ }
46
+
47
+ export function useAgentPaths({ api, agents, onAgentConfigChange }: UseAgentPathsOptions): UseAgentPathsReturn {
48
+ const [isDetecting, setIsDetecting] = useState(false)
49
+ const [hasScanned, setHasScanned] = useState(false)
50
+ const [refreshingAgents, setRefreshingAgents] = useState<Set<string>>(new Set())
51
+ const [scannedAgents, setScannedAgents] = useState<Set<string>>(new Set())
52
+ const wasDetectingRef = useRef(false)
53
+
54
+ // Track when full detection completes
55
+ useEffect(() => {
56
+ if (wasDetectingRef.current && !isDetecting) {
57
+ setHasScanned(true)
58
+ }
59
+ wasDetectingRef.current = isDetecting
60
+ }, [isDetecting])
61
+
62
+ const detectAll = useCallback(async () => {
63
+ setIsDetecting(true)
64
+ try {
65
+ const results = await api.detectAll()
66
+ const detected = new Set<string>()
67
+
68
+ for (const agent of agents) {
69
+ const result = results[agent.id]
70
+ if (result) {
71
+ onAgentConfigChange(agent.id, {
72
+ detected: result.detected,
73
+ detectedPath: result.path,
74
+ binaryPath: result.path || agent.binaryPath,
75
+ })
76
+ if (result.detected) {
77
+ detected.add(agent.id)
78
+ }
79
+ }
80
+ }
81
+
82
+ setScannedAgents(detected)
83
+ } finally {
84
+ setIsDetecting(false)
85
+ }
86
+ }, [api, agents, onAgentConfigChange])
87
+
88
+ const detectAgent = useCallback(async (agentId: string) => {
89
+ setRefreshingAgents((prev) => new Set(prev).add(agentId))
90
+ try {
91
+ const result: AgentDetectionResult = await api.detectAgent(agentId)
92
+ onAgentConfigChange(agentId, {
93
+ detected: result.detected,
94
+ detectedPath: result.path,
95
+ binaryPath: result.path || '',
96
+ })
97
+ setScannedAgents((prev) => {
98
+ const next = new Set(prev)
99
+ if (result.detected) {
100
+ next.add(agentId)
101
+ } else {
102
+ next.delete(agentId)
103
+ }
104
+ return next
105
+ })
106
+ } finally {
107
+ setRefreshingAgents((prev) => {
108
+ const next = new Set(prev)
109
+ next.delete(agentId)
110
+ return next
111
+ })
112
+ }
113
+ }, [api, onAgentConfigChange])
114
+
115
+ const validateAndUpdatePath = useCallback(async (agentId: string, path: string) => {
116
+ onAgentConfigChange(agentId, { binaryPath: path })
117
+ setScannedAgents((prev) => {
118
+ const next = new Set(prev)
119
+ next.delete(agentId)
120
+ return next
121
+ })
122
+ setHasScanned(false)
123
+
124
+ if (path.trim()) {
125
+ try {
126
+ const isValid = await api.validatePath(path.trim())
127
+ onAgentConfigChange(agentId, {
128
+ binaryPath: path.trim(),
129
+ detected: isValid,
130
+ detectedPath: isValid ? path.trim() : undefined,
131
+ })
132
+ } catch {
133
+ onAgentConfigChange(agentId, {
134
+ binaryPath: path.trim(),
135
+ detected: false,
136
+ detectedPath: undefined,
137
+ })
138
+ }
139
+ }
140
+ }, [api, onAgentConfigChange])
141
+
142
+ const toggleEnabled = useCallback((agentId: string) => {
143
+ const agent = agents.find((a) => a.id === agentId)
144
+ if (agent) {
145
+ onAgentConfigChange(agentId, { enabled: !agent.enabled })
146
+ }
147
+ }, [agents, onAgentConfigChange])
148
+
149
+ return {
150
+ isDetecting,
151
+ hasScanned,
152
+ refreshingAgents,
153
+ scannedAgents,
154
+ detectAll,
155
+ detectAgent,
156
+ validateAndUpdatePath,
157
+ toggleEnabled,
158
+ }
159
+ }
@@ -81,7 +81,7 @@ function DiffFileItem({ file, isSelected, onSelect, onReset, resettingFile, anyR
81
81
  return (
82
82
  <div
83
83
  className={`flex items-center transition-colors ${
84
- isSelected ? 'bg-neutral-700' : 'hover:bg-neutral-850'
84
+ isSelected ? 'bg-neutral-700' : 'hover:bg-neutral-970'
85
85
  }`}
86
86
  >
87
87
  <button
@@ -105,7 +105,7 @@ function DiffFileItem({ file, isSelected, onSelect, onReset, resettingFile, anyR
105
105
  onClick={() => onReset(file)}
106
106
  disabled={anyResetting}
107
107
  size="xs"
108
- color="orange"
108
+ accentColor="orange"
109
109
  tooltip={{ title: 'Reset file', description: 'Reset this file to golden' }}
110
110
  />
111
111
  </div>
@@ -166,7 +166,7 @@ function DiffFileTreePanel({ sync, componentLabels, renderFileIcon }: DiffFileTr
166
166
  // Root-level nodes get colored styling, nested folders get muted styling
167
167
  const headerClasses = isRoot
168
168
  ? `${color.text} ${color.hoverBg} border-l-2 ${color.border} ${color.bg} px-3 py-2 text-md font-medium`
169
- : 'text-neutral-500 hover:bg-neutral-850 px-3 py-1.5'
169
+ : 'text-neutral-500 hover:bg-neutral-970 px-3 py-1.5'
170
170
 
171
171
  const chevronSize = isRoot ? 'w-4 h-4' : 'w-3 h-3'
172
172
 
@@ -219,7 +219,7 @@ function DiffFileTreePanel({ sync, componentLabels, renderFileIcon }: DiffFileTr
219
219
  }
220
220
 
221
221
  return (
222
- <div className="w-72 flex-shrink-0 bg-neutral-900 rounded-lg border border-neutral-700 overflow-hidden flex flex-col">
222
+ <div className="w-72 flex-shrink-0 bg-neutral-980 rounded-lg border border-neutral-700 overflow-hidden flex flex-col">
223
223
  {/* Summary Header */}
224
224
  <div className="p-3 border-b border-neutral-700 flex-shrink-0">
225
225
  <div className="flex gap-3 text-sm">
@@ -252,7 +252,7 @@ function DiffFileTreePanel({ sync, componentLabels, renderFileIcon }: DiffFileTr
252
252
  icon={allExpanded ? <ChevronsDownUp className="w-3.5 h-3.5" /> : <ChevronsUpDown className="w-3.5 h-3.5" />}
253
253
  onClick={allExpanded ? handleCollapseAll : handleExpandAll}
254
254
  size="sm"
255
- color="neutral"
255
+ accentColor="neutral"
256
256
  tooltip={{
257
257
  title: allExpanded ? 'Collapse All' : 'Expand All',
258
258
  description: allExpanded ? 'Collapse all folders' : 'Expand all folders',
@@ -294,7 +294,7 @@ function DiffEditorPanel({ sync, componentLabels, monacoTheme }: DiffEditorPanel
294
294
 
295
295
  if (!selectedDiffFile) {
296
296
  return (
297
- <div className="flex-1 min-w-0 bg-neutral-900 rounded-lg border border-neutral-700 overflow-hidden flex flex-col">
297
+ <div className="flex-1 min-w-0 bg-neutral-980 rounded-lg border border-neutral-700 overflow-hidden flex flex-col">
298
298
  <div className="flex items-center justify-center h-full text-md text-neutral-500">
299
299
  Select a file from the tree to view differences
300
300
  </div>
@@ -305,7 +305,7 @@ function DiffEditorPanel({ sync, componentLabels, monacoTheme }: DiffEditorPanel
305
305
  const compLabel = componentLabels?.[selectedDiffFile.component] ?? selectedDiffFile.component
306
306
 
307
307
  return (
308
- <div className="flex-1 min-w-0 bg-neutral-900 rounded-lg border border-neutral-700 overflow-hidden flex flex-col">
308
+ <div className="flex-1 min-w-0 bg-neutral-980 rounded-lg border border-neutral-700 overflow-hidden flex flex-col">
309
309
  {/* File Header */}
310
310
  <div className="flex items-center justify-between px-4 py-2 border-b border-neutral-700 flex-shrink-0">
311
311
  <div className="flex items-center gap-2">
@@ -386,7 +386,7 @@ export function FileDiffViewer({ sync, componentLabels, monacoTheme, renderFileI
386
386
  <IconButton
387
387
  icon={diffSideBySide ? <Rows2 /> : <Columns2 />}
388
388
  onClick={() => setDiffSideBySide((prev: boolean) => !prev)}
389
- color="blue"
389
+ accentColor="neutral"
390
390
  tooltip={{ title: diffSideBySide ? 'Inline view' : 'Side by side', description: 'Toggle diff display mode' }}
391
391
  />
392
392
  {devtools && hasUnsavedChanges && (
@@ -394,7 +394,7 @@ export function FileDiffViewer({ sync, componentLabels, monacoTheme, renderFileI
394
394
  icon={<Save className={saving ? 'animate-spin' : ''} />}
395
395
  onClick={handleSaveLiveFile}
396
396
  disabled={saving}
397
- color="amber"
397
+ accentColor="green"
398
398
  tooltip={{ title: 'Save changes', description: 'Save edited content to live file' }}
399
399
  />
400
400
  )}
@@ -404,6 +404,7 @@ export function FileDiffViewer({ sync, componentLabels, monacoTheme, renderFileI
404
404
  </div>
405
405
 
406
406
  {diffLoading ? (
407
+ /* py-8: empty/loading states need extra vertical breathing room beyond the p-6 scale max */
407
408
  <div className="text-md text-neutral-500 text-center py-8">Loading diff...</div>
408
409
  ) : diff ? (
409
410
  <div className="flex gap-4 flex-1 min-h-[400px]">