@toolr/ui-design 0.1.8 → 0.1.10
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.
- package/ai-manifest.json +35 -20
- package/components/composites/dashboard-list-item.tsx +172 -0
- package/components/composites/dashboard-panel.tsx +218 -0
- package/components/content/info-panel-primitives.tsx +9 -8
- package/components/diagrams/diagram-utils.tsx +2 -1
- package/components/hooks/use-dropdown-portal.ts +39 -0
- package/components/lib/accent-context.ts +10 -0
- package/components/lib/{ai-tools.tsx → coding-agents.tsx} +23 -8
- package/components/lib/custom-icons.tsx +37 -0
- package/components/lib/form-colors.ts +16 -16
- package/components/lib/git-providers.tsx +39 -0
- package/components/lib/theme-engine.ts +59 -10
- package/components/lib/toolr-brand.tsx +23 -9
- package/components/sections/captured-issues/captured-issues-panel.tsx +17 -8
- package/components/sections/{ai-tools-paths/tools-paths-panel.tsx → coding-agent-paths/agent-paths-panel.tsx} +70 -62
- package/components/sections/coding-agent-paths/index.ts +37 -0
- package/components/sections/{ai-tools-paths → coding-agent-paths}/types.ts +28 -28
- package/components/sections/coding-agent-paths/use-agent-paths.ts +159 -0
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +10 -9
- package/components/sections/golden-snapshots/golden-sync-panel.tsx +12 -3
- package/components/sections/golden-snapshots/snapshot-manager.tsx +9 -7
- package/components/sections/golden-snapshots/status-overview.tsx +8 -8
- package/components/sections/golden-snapshots/version-manager.tsx +6 -6
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +3 -3
- package/components/sections/prompt-editor/index.ts +1 -1
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +13 -5
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +18 -10
- package/components/sections/prompt-editor/types.ts +2 -2
- package/components/sections/report-bug/report-bug-form.tsx +12 -4
- package/components/sections/report-bug/screenshot-uploader.tsx +11 -3
- package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +12 -4
- package/components/sections/snapshot-browser/snapshot-tree.tsx +5 -4
- package/components/sections/snapshot-browser/types.ts +1 -1
- package/components/sections/snippets-editor/snippets-editor.tsx +16 -9
- package/components/settings/SettingsHeader.tsx +2 -2
- package/components/settings/SettingsPanel.tsx +11 -3
- package/components/settings/SettingsTreeNav.tsx +15 -9
- package/components/ui/action-dialog.tsx +24 -30
- package/components/ui/ai-action-button.tsx +10 -7
- package/components/ui/ai-execution-action-buttons.tsx +13 -5
- package/components/ui/badge.tsx +7 -4
- package/components/ui/bottom-panel-header.tsx +9 -5
- package/components/ui/breadcrumb.tsx +13 -5
- package/components/ui/{extension-list-card.tsx → capability-list-card.tsx} +13 -5
- package/components/ui/checkbox.tsx +6 -3
- package/components/ui/collapsible-section.tsx +38 -29
- package/components/ui/confirm-badge.tsx +7 -4
- package/components/ui/cookie-consent.tsx +13 -7
- package/components/ui/detail-section.tsx +24 -16
- package/components/ui/detail-view-wrapper.tsx +30 -22
- package/components/ui/editor-placeholder-card.tsx +28 -24
- package/components/ui/editor-toolbar.tsx +7 -4
- package/components/ui/execution-details-panel.tsx +10 -5
- package/components/ui/file-structure-section.tsx +7 -4
- package/components/ui/file-tree.tsx +3 -1
- package/components/ui/files-panel.tsx +147 -27
- package/components/ui/filter-dropdown.tsx +84 -74
- package/components/ui/form-actions.tsx +14 -6
- package/components/ui/frontmatter-form-header.tsx +10 -2
- package/components/ui/icon-button.tsx +22 -9
- package/components/ui/input.tsx +7 -4
- package/components/ui/label.tsx +5 -5
- package/components/ui/layout-tab-bar.tsx +7 -5
- package/components/ui/modal.tsx +18 -4
- package/components/ui/nav-card.tsx +6 -3
- package/components/ui/navigation-bar.tsx +164 -82
- package/components/ui/number-input.tsx +8 -4
- package/components/ui/project-explorer.tsx +666 -0
- package/components/ui/registry-browser.tsx +12 -1
- package/components/ui/registry-card.tsx +49 -42
- package/components/ui/registry-detail.tsx +40 -17
- package/components/ui/resizable-textarea.tsx +18 -11
- package/components/ui/scope-badge.tsx +18 -11
- package/components/ui/segmented-toggle.tsx +5 -2
- package/components/ui/select.tsx +12 -9
- package/components/ui/selection-grid.tsx +36 -37
- package/components/ui/setting-row.tsx +2 -2
- package/components/ui/settings-card.tsx +10 -3
- package/components/ui/settings-info-box.tsx +9 -5
- package/components/ui/settings-section-title.tsx +14 -2
- package/components/ui/snapshot-card.tsx +10 -2
- package/components/ui/snippets-panel.tsx +4 -2
- package/components/ui/sort-dropdown.tsx +39 -29
- package/components/ui/status-card.tsx +9 -1
- package/components/ui/tab-bar.tsx +12 -9
- package/components/ui/toggle.tsx +13 -7
- package/components/ui/tooltip.tsx +9 -1
- package/dist/content.js +8 -8
- package/dist/diagrams.d.ts +0 -1
- package/dist/index.d.ts +427 -184
- package/dist/index.js +3098 -1761
- package/dist/tokens/primitives.css +28 -6
- package/dist/tokens/semantic.css +15 -15
- package/dist/tokens/theme.css +23 -0
- package/index.ts +25 -11
- package/package.json +1 -1
- package/tokens/primitives.css +28 -6
- package/tokens/semantic.css +15 -15
- package/tokens/theme.css +23 -0
- package/components/sections/ai-tools-paths/index.ts +0 -37
- package/components/sections/ai-tools-paths/use-tools-paths.ts +0 -159
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* AgentPathsPanel — Coding agent binary path configuration panel
|
|
3
3
|
*
|
|
4
|
-
* Part of: Sections >
|
|
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
|
-
*
|
|
7
|
+
* agent configs, and a change callback — it handles the full detection and
|
|
8
8
|
* configuration UI.
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
|
-
* <
|
|
12
|
-
* api={
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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 >
|
|
19
|
-
* - Each
|
|
20
|
-
* - The component manages detection state internally via
|
|
21
|
-
* -
|
|
22
|
-
* - The
|
|
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 {
|
|
33
|
-
import type {
|
|
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
|
|
37
|
+
export interface AgentPathsPanelProps {
|
|
36
38
|
/** API adapter for backend operations (detection, validation) */
|
|
37
|
-
api:
|
|
38
|
-
/** Current
|
|
39
|
-
|
|
40
|
-
/** Called when
|
|
41
|
-
|
|
42
|
-
/** Optional render function for
|
|
43
|
-
|
|
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
|
|
50
|
+
export function AgentPathsPanel({
|
|
48
51
|
api,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
agents,
|
|
53
|
+
onAgentConfigChange,
|
|
54
|
+
renderAgentIcon,
|
|
55
|
+
accentColor,
|
|
52
56
|
className,
|
|
53
|
-
}:
|
|
57
|
+
}: AgentPathsPanelProps) {
|
|
58
|
+
const contextAccent = useAccentColor()
|
|
59
|
+
const effectiveColor = accentColor ?? contextAccent ?? 'blue'
|
|
54
60
|
const {
|
|
55
61
|
isDetecting,
|
|
56
62
|
hasScanned,
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
refreshingAgents,
|
|
64
|
+
scannedAgents,
|
|
59
65
|
detectAll,
|
|
60
|
-
|
|
66
|
+
detectAgent,
|
|
61
67
|
validateAndUpdatePath,
|
|
62
68
|
toggleEnabled,
|
|
63
|
-
} =
|
|
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
|
-
|
|
93
|
+
accentColor={hasScanned ? 'green' : 'neutral'}
|
|
87
94
|
tooltip={{
|
|
88
|
-
title: hasScanned ? 'Scan complete' : 'Scan for
|
|
89
|
-
description: hasScanned ? 'CLI
|
|
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
|
-
{/*
|
|
95
|
-
<div className="bg-neutral-
|
|
96
|
-
{
|
|
97
|
-
const isRefreshing =
|
|
98
|
-
const isEnabled =
|
|
99
|
-
const displayPath =
|
|
100
|
-
const hasBeenScanned =
|
|
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={
|
|
105
|
-
className={cn('p-4 space-y-2', !
|
|
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
|
-
{
|
|
117
|
+
{renderAgentIcon?.(agent.id)}
|
|
111
118
|
<span
|
|
112
119
|
className={cn(
|
|
113
120
|
'font-medium',
|
|
114
|
-
|
|
121
|
+
agent.detected && !isEnabled ? 'text-neutral-500' : 'text-neutral-300',
|
|
115
122
|
)}
|
|
116
123
|
>
|
|
117
|
-
{
|
|
124
|
+
{agent.name}
|
|
118
125
|
</span>
|
|
119
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
147
|
+
{agent.detected && (
|
|
141
148
|
<Toggle
|
|
142
149
|
checked={isEnabled}
|
|
143
|
-
onChange={() => toggleEnabled(
|
|
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(
|
|
157
|
-
placeholder={`/usr/local/bin/${
|
|
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={() =>
|
|
179
|
+
onClick={() => detectAgent(agent.id)}
|
|
173
180
|
disabled={isRefreshing}
|
|
174
|
-
|
|
181
|
+
accentColor={hasBeenScanned ? 'green' : 'neutral'}
|
|
175
182
|
size="sm"
|
|
176
183
|
tooltipPosition="bottom"
|
|
177
184
|
tooltip={{
|
|
178
|
-
title: hasBeenScanned ? 'Scan complete' : `Scan for ${
|
|
179
|
-
description: hasBeenScanned ? 'Click to re-scan' : 'Check if CLI
|
|
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('/') && !
|
|
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
|
-
{
|
|
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('/') && !
|
|
200
|
+
{!isEnabled && !displayPath?.includes('/') && !agent.detected && (
|
|
194
201
|
<p className="text-sm text-neutral-500 pl-5">
|
|
195
|
-
|
|
202
|
+
Agent disabled. Enter path to enable.
|
|
196
203
|
</p>
|
|
197
204
|
)}
|
|
198
205
|
</div>
|
|
199
206
|
|
|
200
207
|
{/* Disabled warning */}
|
|
201
|
-
{
|
|
208
|
+
{agent.detected && !isEnabled && (
|
|
202
209
|
<p className="text-sm text-yellow-500">
|
|
203
|
-
|
|
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
|
-
*
|
|
2
|
+
* Coding Agent Paths — Shared type definitions
|
|
3
3
|
*
|
|
4
|
-
* Part of: Sections >
|
|
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
|
-
* -
|
|
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
|
|
19
|
-
* - The UI renders whatever
|
|
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
|
-
//
|
|
23
|
+
// Agent configuration
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
|
|
26
|
-
/** Per-
|
|
27
|
-
export interface
|
|
28
|
-
/** Unique identifier for the
|
|
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
|
|
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
|
|
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
|
|
47
|
-
export interface
|
|
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
|
-
*
|
|
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
|
-
* │
|
|
77
|
-
* │ Scans PATH and known install locations for all
|
|
78
|
-
* │ Returns a map of
|
|
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
|
-
* │
|
|
81
|
-
* │ Scans for a single
|
|
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 <
|
|
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
|
|
95
|
-
* │ - ~/.npm/bin/<binary> (for npm-based
|
|
96
|
-
* │ 3. Check
|
|
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
|
|
103
|
-
/** Scan all
|
|
104
|
-
detectAll: () => Promise<Record<string,
|
|
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
|
|
107
|
-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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]">
|