agentfit 0.1.1 → 0.1.2
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/.github/workflows/release.yml +111 -0
- package/README.md +15 -8
- package/app/api/commands/route.ts +55 -1
- package/app/api/usage/route.ts +1 -0
- package/components/agent-coach.tsx +1 -1
- package/components/app-sidebar.tsx +12 -227
- package/components/daily-chart.tsx +47 -4
- package/components/dashboard-shell.tsx +22 -9
- package/components/data-provider.tsx +22 -6
- package/electron/entitlements.mac.plist +16 -0
- package/electron/init-db.mjs +37 -0
- package/electron/main.mjs +203 -0
- package/generated/prisma/internal/class.ts +4 -4
- package/generated/prisma/internal/prismaNamespace.ts +1 -0
- package/generated/prisma/internal/prismaNamespaceBrowser.ts +1 -0
- package/generated/prisma/models/Session.ts +35 -1
- package/lib/coach.ts +46 -5
- package/lib/db.ts +2 -2
- package/lib/parse-codex.ts +1 -0
- package/lib/parse-logs.ts +15 -1
- package/lib/queries-codex.ts +2 -0
- package/lib/queries.ts +3 -0
- package/lib/sync.ts +10 -0
- package/package.json +8 -7
- package/prisma/migrations/20260401144555_add_system_prompt_edits/migration.sql +80 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +1 -0
- package/prisma/schema.sql +81 -0
- /package/app/(dashboard)/{settings → data-management}/page.tsx +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build-mac:
|
|
13
|
+
runs-on: macos-latest
|
|
14
|
+
timeout-minutes: 30
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v6
|
|
19
|
+
with:
|
|
20
|
+
node-version: 22
|
|
21
|
+
cache: npm
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: npm ci
|
|
25
|
+
|
|
26
|
+
- name: Import signing certificate
|
|
27
|
+
env:
|
|
28
|
+
CERTIFICATE_P12: ${{ secrets.CERTIFICATE_P12 }}
|
|
29
|
+
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
|
|
30
|
+
run: |
|
|
31
|
+
echo "$CERTIFICATE_P12" | base64 --decode > certificate.p12
|
|
32
|
+
security create-keychain -p actions build.keychain
|
|
33
|
+
security default-keychain -s build.keychain
|
|
34
|
+
security unlock-keychain -p actions build.keychain
|
|
35
|
+
security import certificate.p12 -k build.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
|
36
|
+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k actions build.keychain
|
|
37
|
+
rm certificate.p12
|
|
38
|
+
|
|
39
|
+
- name: Build macOS DMGs
|
|
40
|
+
run: npm run electron:build:mac
|
|
41
|
+
|
|
42
|
+
- name: Upload DMGs
|
|
43
|
+
uses: actions/upload-artifact@v7
|
|
44
|
+
with:
|
|
45
|
+
name: mac-dmgs
|
|
46
|
+
path: |
|
|
47
|
+
dist-electron/*.dmg
|
|
48
|
+
|
|
49
|
+
build-win:
|
|
50
|
+
runs-on: windows-latest
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v6
|
|
53
|
+
|
|
54
|
+
- uses: actions/setup-node@v6
|
|
55
|
+
with:
|
|
56
|
+
node-version: 22
|
|
57
|
+
cache: npm
|
|
58
|
+
|
|
59
|
+
- name: Install dependencies
|
|
60
|
+
run: npm ci
|
|
61
|
+
|
|
62
|
+
- name: Build Windows installer
|
|
63
|
+
run: npm run electron:build:win
|
|
64
|
+
|
|
65
|
+
- name: Upload exe
|
|
66
|
+
uses: actions/upload-artifact@v7
|
|
67
|
+
with:
|
|
68
|
+
name: win-exe
|
|
69
|
+
path: |
|
|
70
|
+
dist-electron/*.exe
|
|
71
|
+
|
|
72
|
+
publish-npm:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v6
|
|
76
|
+
|
|
77
|
+
- uses: actions/setup-node@v6
|
|
78
|
+
with:
|
|
79
|
+
node-version: 22
|
|
80
|
+
registry-url: https://registry.npmjs.org
|
|
81
|
+
|
|
82
|
+
- name: Install dependencies
|
|
83
|
+
run: npm ci
|
|
84
|
+
|
|
85
|
+
- name: Build
|
|
86
|
+
run: npm run build
|
|
87
|
+
|
|
88
|
+
- name: Publish to npm
|
|
89
|
+
run: npm publish
|
|
90
|
+
env:
|
|
91
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
92
|
+
|
|
93
|
+
release:
|
|
94
|
+
needs: [build-mac, build-win]
|
|
95
|
+
runs-on: ubuntu-latest
|
|
96
|
+
steps:
|
|
97
|
+
- uses: actions/download-artifact@v8
|
|
98
|
+
with:
|
|
99
|
+
name: mac-dmgs
|
|
100
|
+
path: artifacts
|
|
101
|
+
|
|
102
|
+
- uses: actions/download-artifact@v8
|
|
103
|
+
with:
|
|
104
|
+
name: win-exe
|
|
105
|
+
path: artifacts
|
|
106
|
+
|
|
107
|
+
- name: Create GitHub Release
|
|
108
|
+
uses: softprops/action-gh-release@v2
|
|
109
|
+
with:
|
|
110
|
+
files: artifacts/*
|
|
111
|
+
generate_release_notes: true
|
package/README.md
CHANGED
|
@@ -4,19 +4,28 @@ Fitness tracker dashboard for AI coding agents. Reads your local Claude Code and
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
### Option 1:
|
|
7
|
+
### Option 1: Desktop App (recommended)
|
|
8
|
+
|
|
9
|
+
Download a pre-built installer from the [Releases](https://github.com/harrywang/agentfit/releases) page:
|
|
10
|
+
|
|
11
|
+
- **macOS**: `AgentFit-x.x.x.dmg` (Intel) or `AgentFit-x.x.x-arm64.dmg` (Apple Silicon)
|
|
12
|
+
- **Windows**: `AgentFit-x.x.x.exe`
|
|
13
|
+
|
|
14
|
+
> **macOS note:** If you see "AgentFit Not Opened" on first launch, go to **System Settings > Privacy & Security**, scroll down, and click **Open Anyway**. Or run `xattr -cr /Applications/AgentFit.app` in Terminal.
|
|
15
|
+
|
|
16
|
+
### Option 2: One-liner
|
|
8
17
|
|
|
9
18
|
```bash
|
|
10
19
|
curl -fsSL https://raw.githubusercontent.com/harrywang/agentfit/main/setup.sh | bash
|
|
11
20
|
```
|
|
12
21
|
|
|
13
|
-
### Option
|
|
22
|
+
### Option 3: npx
|
|
14
23
|
|
|
15
24
|
```bash
|
|
16
25
|
npx agentfit
|
|
17
26
|
```
|
|
18
27
|
|
|
19
|
-
### Option
|
|
28
|
+
### Option 4: Manual
|
|
20
29
|
|
|
21
30
|
```bash
|
|
22
31
|
git clone https://github.com/harrywang/agentfit.git
|
|
@@ -30,7 +39,7 @@ npm start
|
|
|
30
39
|
|
|
31
40
|
Open [http://localhost:3000](http://localhost:3000). The app auto-syncs your Claude Code (`~/.claude/projects/`) and Codex (`~/.codex/sessions/`) logs on first load.
|
|
32
41
|
|
|
33
|
-
**Requirements:** Node.js 20+
|
|
42
|
+
**Requirements:** Node.js 20+ (Options 2–4)
|
|
34
43
|
|
|
35
44
|
## The CRAFT Framework
|
|
36
45
|
|
|
@@ -60,17 +69,15 @@ Inspired by [DORA Metrics](https://dora.dev) and Microsoft's [SPACE framework](h
|
|
|
60
69
|
- **Images** — screenshot analysis across sessions
|
|
61
70
|
- **Community Plugins** — extensible analysis views
|
|
62
71
|
|
|
63
|
-
##
|
|
72
|
+
## Development
|
|
64
73
|
|
|
65
|
-
|
|
74
|
+
Build the desktop app locally:
|
|
66
75
|
|
|
67
76
|
```bash
|
|
68
77
|
npm run electron:build:mac # Mac (.dmg)
|
|
69
78
|
npm run electron:build:win # Windows (.exe)
|
|
70
79
|
```
|
|
71
80
|
|
|
72
|
-
## Development
|
|
73
|
-
|
|
74
81
|
```bash
|
|
75
82
|
npm run dev # Start dev server (Turbopack)
|
|
76
83
|
npm run build # Production build
|
|
@@ -1,12 +1,66 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { analyzeCommands } from '@/lib/commands'
|
|
3
|
+
import { prisma } from '@/lib/db'
|
|
3
4
|
|
|
4
5
|
export const dynamic = 'force-dynamic'
|
|
5
6
|
|
|
6
7
|
export async function GET() {
|
|
7
8
|
try {
|
|
8
9
|
const analysis = analyzeCommands()
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
// Merge skill invocations from session data (Skill tool calls tracked in DB)
|
|
12
|
+
// These capture cases where the user asked Claude to invoke a skill
|
|
13
|
+
// (e.g. "use /doc-writer to...") rather than typing the slash command directly
|
|
14
|
+
const dbSessions = await prisma.session.findMany({
|
|
15
|
+
select: { skillCallsJson: true },
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const dbSkillCounts = new Map<string, number>()
|
|
19
|
+
for (const s of dbSessions) {
|
|
20
|
+
const skills = JSON.parse(s.skillCallsJson) as Record<string, number>
|
|
21
|
+
for (const [skill, count] of Object.entries(skills)) {
|
|
22
|
+
const cmd = skill.startsWith('/') ? skill : `/${skill}`
|
|
23
|
+
dbSkillCounts.set(cmd, (dbSkillCounts.get(cmd) || 0) + count)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Build a map of history.jsonl counts for custom commands
|
|
28
|
+
const historyMap = new Map<string, number>()
|
|
29
|
+
for (const c of analysis.customCommands) {
|
|
30
|
+
historyMap.set(c.command, c.count)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Merge DB skill counts into customCommands with breakdown
|
|
34
|
+
const customMap = new Map<string, { command: string; count: number; historyCount: number; sessionCount: number }>()
|
|
35
|
+
for (const c of analysis.customCommands) {
|
|
36
|
+
customMap.set(c.command, { command: c.command, count: c.count, historyCount: c.count, sessionCount: 0 })
|
|
37
|
+
}
|
|
38
|
+
for (const [cmd, count] of dbSkillCounts) {
|
|
39
|
+
const existing = analysis.commands.find(c => c.command === cmd)
|
|
40
|
+
if (existing) {
|
|
41
|
+
existing.count += count
|
|
42
|
+
existing.used = existing.count > 0
|
|
43
|
+
} else {
|
|
44
|
+
const prev = customMap.get(cmd)
|
|
45
|
+
if (prev) {
|
|
46
|
+
prev.sessionCount += count
|
|
47
|
+
prev.count += count
|
|
48
|
+
} else {
|
|
49
|
+
customMap.set(cmd, { command: cmd, count, historyCount: 0, sessionCount: count })
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
analysis.customCommands = Array.from(customMap.values())
|
|
55
|
+
.sort((a, b) => b.count - a.count)
|
|
56
|
+
|
|
57
|
+
// Expose session skill counts so the chart can show breakdown
|
|
58
|
+
const responseData = {
|
|
59
|
+
...analysis,
|
|
60
|
+
dbSkillCounts: Object.fromEntries(dbSkillCounts),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return NextResponse.json(responseData)
|
|
10
64
|
} catch (error) {
|
|
11
65
|
return NextResponse.json(
|
|
12
66
|
{ error: (error as Error).message },
|
package/app/api/usage/route.ts
CHANGED
|
@@ -117,6 +117,7 @@ function mergeUsageData(a: UsageData, b: UsageData): UsageData {
|
|
|
117
117
|
totalApiErrors: a.overview.totalApiErrors + b.overview.totalApiErrors,
|
|
118
118
|
totalRateLimitDays: a.overview.totalRateLimitDays + b.overview.totalRateLimitDays,
|
|
119
119
|
totalUserInterruptions: a.overview.totalUserInterruptions + b.overview.totalUserInterruptions,
|
|
120
|
+
totalSystemPromptEdits: (a.overview.totalSystemPromptEdits ?? 0) + (b.overview.totalSystemPromptEdits ?? 0),
|
|
120
121
|
models,
|
|
121
122
|
skillUsage: (() => {
|
|
122
123
|
const skills: Record<string, number> = { ...a.overview.skillUsage }
|
|
@@ -59,7 +59,7 @@ const CRAFT_DIMENSIONS: {
|
|
|
59
59
|
color: 'var(--chart-1)',
|
|
60
60
|
icon: Brain,
|
|
61
61
|
description: 'How well you engineer the context available to the AI — not just window size, but the holistic curation of tokens: system prompts (CLAUDE.md), just-in-time retrieval, structured notes, sub-agent isolation, and cache efficiency.',
|
|
62
|
-
metrics: ['Cache reuse (
|
|
62
|
+
metrics: ['System prompt maintenance (15%)', 'Cache reuse (15%)', 'Overflow avoidance (15%)', 'Just-in-time retrieval (20%)', 'Session length (10%)', 'Note-taking (10%)', 'Output density (10%)', 'Sub-agent isolation (5%)'],
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
key: 'reach',
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState,
|
|
3
|
+
import { useState, type ReactNode } from 'react'
|
|
4
4
|
import Link from 'next/link'
|
|
5
5
|
import { usePathname } from 'next/navigation'
|
|
6
6
|
import {
|
|
7
|
-
AlertTriangle,
|
|
8
7
|
BarChart3,
|
|
9
8
|
Brain,
|
|
10
9
|
Camera,
|
|
11
|
-
Check,
|
|
12
10
|
ChevronRight,
|
|
13
|
-
Cloud,
|
|
14
|
-
CloudOff,
|
|
15
11
|
Coins,
|
|
16
12
|
FileText,
|
|
17
13
|
FolderOpen,
|
|
@@ -19,13 +15,9 @@ import {
|
|
|
19
15
|
HeartPulse,
|
|
20
16
|
LayoutDashboard,
|
|
21
17
|
ListTree,
|
|
22
|
-
Loader2,
|
|
23
18
|
Puzzle,
|
|
24
|
-
RefreshCw,
|
|
25
|
-
RotateCcw,
|
|
26
19
|
Settings,
|
|
27
20
|
Terminal,
|
|
28
|
-
Upload,
|
|
29
21
|
Wrench,
|
|
30
22
|
} from 'lucide-react'
|
|
31
23
|
import type { ComponentType } from 'react'
|
|
@@ -45,19 +37,7 @@ import {
|
|
|
45
37
|
SidebarMenuSubItem,
|
|
46
38
|
SidebarRail,
|
|
47
39
|
} from '@/components/ui/sidebar'
|
|
48
|
-
import {
|
|
49
|
-
Dialog,
|
|
50
|
-
DialogContent,
|
|
51
|
-
DialogDescription,
|
|
52
|
-
DialogFooter,
|
|
53
|
-
DialogHeader,
|
|
54
|
-
DialogTitle,
|
|
55
|
-
} from '@/components/ui/dialog'
|
|
56
|
-
import { Button } from '@/components/ui/button'
|
|
57
|
-
import { Badge } from '@/components/ui/badge'
|
|
58
|
-
import { useData } from './data-provider'
|
|
59
40
|
import { getPlugins } from '@/lib/plugins'
|
|
60
|
-
import { toast } from 'sonner'
|
|
61
41
|
import '@/plugins'
|
|
62
42
|
|
|
63
43
|
interface NavItem {
|
|
@@ -75,7 +55,6 @@ interface NavGroup {
|
|
|
75
55
|
const topItems: NavItem[] = [
|
|
76
56
|
{ title: 'Dashboard', icon: LayoutDashboard, href: '/' },
|
|
77
57
|
{ title: 'CRAFT Coach', icon: HeartPulse, href: '/coach' },
|
|
78
|
-
{ title: 'Reports', icon: FileText, href: '/reports' },
|
|
79
58
|
]
|
|
80
59
|
|
|
81
60
|
const navGroups: NavGroup[] = [
|
|
@@ -94,6 +73,7 @@ const navGroups: NavGroup[] = [
|
|
|
94
73
|
{ title: 'Daily Usage', icon: BarChart3, href: '/daily' },
|
|
95
74
|
{ title: 'Token Breakdown', icon: Coins, href: '/tokens' },
|
|
96
75
|
{ title: 'Tool Usage', icon: Wrench, href: '/tools' },
|
|
76
|
+
{ title: 'Command Usage', icon: Terminal, href: '/commands' },
|
|
97
77
|
],
|
|
98
78
|
},
|
|
99
79
|
{
|
|
@@ -101,9 +81,9 @@ const navGroups: NavGroup[] = [
|
|
|
101
81
|
icon: Brain,
|
|
102
82
|
items: [
|
|
103
83
|
{ title: 'Personality Fit', icon: Brain, href: '/personality' },
|
|
104
|
-
{ title: 'Command Usage', icon: Terminal, href: '/commands' },
|
|
105
84
|
{ title: 'Session Flow', icon: GitBranch, href: '/flow' },
|
|
106
|
-
{ title: '
|
|
85
|
+
{ title: 'Image Analysis', icon: Camera, href: '/images' },
|
|
86
|
+
{ title: 'Reports', icon: FileText, href: '/reports' },
|
|
107
87
|
],
|
|
108
88
|
},
|
|
109
89
|
]
|
|
@@ -139,194 +119,6 @@ function CollapsibleGroup({ group, pathname }: { group: NavGroup; pathname: stri
|
|
|
139
119
|
)
|
|
140
120
|
}
|
|
141
121
|
|
|
142
|
-
interface BackupFolderStatus {
|
|
143
|
-
exists: boolean
|
|
144
|
-
hasGit: boolean
|
|
145
|
-
hasRemote: boolean
|
|
146
|
-
remoteUrl: string | null
|
|
147
|
-
isDirty: boolean
|
|
148
|
-
lastCommit: string | null
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
interface BackupStatus {
|
|
152
|
-
ghInstalled: boolean
|
|
153
|
-
ghAuthenticated: boolean
|
|
154
|
-
ghUser: string | null
|
|
155
|
-
claude: BackupFolderStatus
|
|
156
|
-
codex: BackupFolderStatus
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function BackupSection() {
|
|
160
|
-
const [status, setStatus] = useState<BackupStatus | null>(null)
|
|
161
|
-
const [loading, setLoading] = useState(false)
|
|
162
|
-
const [syncing, setSyncing] = useState<Record<string, boolean>>({})
|
|
163
|
-
|
|
164
|
-
const fetchStatus = useCallback(async () => {
|
|
165
|
-
setLoading(true)
|
|
166
|
-
try {
|
|
167
|
-
const res = await fetch('/api/backup')
|
|
168
|
-
setStatus(await res.json())
|
|
169
|
-
} catch {
|
|
170
|
-
// ignore
|
|
171
|
-
} finally {
|
|
172
|
-
setLoading(false)
|
|
173
|
-
}
|
|
174
|
-
}, [])
|
|
175
|
-
|
|
176
|
-
useEffect(() => {
|
|
177
|
-
fetchStatus()
|
|
178
|
-
}, [fetchStatus])
|
|
179
|
-
|
|
180
|
-
const handleInit = async (folder: 'claude' | 'codex') => {
|
|
181
|
-
setSyncing((s) => ({ ...s, [folder]: true }))
|
|
182
|
-
try {
|
|
183
|
-
const res = await fetch('/api/backup', {
|
|
184
|
-
method: 'POST',
|
|
185
|
-
headers: { 'Content-Type': 'application/json' },
|
|
186
|
-
body: JSON.stringify({ action: 'init', folder }),
|
|
187
|
-
})
|
|
188
|
-
const data = await res.json()
|
|
189
|
-
if (!res.ok) {
|
|
190
|
-
toast.error(`Backup failed`, { description: data.error })
|
|
191
|
-
return
|
|
192
|
-
}
|
|
193
|
-
toast.success(`~/.${folder} backed up`, {
|
|
194
|
-
description: (
|
|
195
|
-
<a href={data.repoUrl} target="_blank" rel="noopener noreferrer" className="underline">
|
|
196
|
-
{data.repoUrl}
|
|
197
|
-
</a>
|
|
198
|
-
),
|
|
199
|
-
})
|
|
200
|
-
await fetchStatus()
|
|
201
|
-
} catch (e) {
|
|
202
|
-
toast.error('Backup failed', { description: (e as Error).message })
|
|
203
|
-
} finally {
|
|
204
|
-
setSyncing((s) => ({ ...s, [folder]: false }))
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const handleSync = async (folder: 'claude' | 'codex') => {
|
|
209
|
-
setSyncing((s) => ({ ...s, [folder]: true }))
|
|
210
|
-
try {
|
|
211
|
-
const res = await fetch('/api/backup', {
|
|
212
|
-
method: 'POST',
|
|
213
|
-
headers: { 'Content-Type': 'application/json' },
|
|
214
|
-
body: JSON.stringify({ action: 'sync', folder }),
|
|
215
|
-
})
|
|
216
|
-
const data = await res.json()
|
|
217
|
-
if (!res.ok) {
|
|
218
|
-
toast.error(`Sync failed`, { description: data.error })
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
toast.success(`~/.${folder} synced`, { description: data.message })
|
|
222
|
-
await fetchStatus()
|
|
223
|
-
} catch (e) {
|
|
224
|
-
toast.error('Sync failed', { description: (e as Error).message })
|
|
225
|
-
} finally {
|
|
226
|
-
setSyncing((s) => ({ ...s, [folder]: false }))
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (loading || !status) {
|
|
231
|
-
return (
|
|
232
|
-
<div className="flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground">
|
|
233
|
-
<Loader2 className="h-3 w-3 animate-spin" />
|
|
234
|
-
Checking GitHub...
|
|
235
|
-
</div>
|
|
236
|
-
)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (!status.ghInstalled) {
|
|
240
|
-
return (
|
|
241
|
-
<div className="space-y-2 px-3 py-1.5">
|
|
242
|
-
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
243
|
-
<CloudOff className="h-3.5 w-3.5" />
|
|
244
|
-
GitHub CLI not found
|
|
245
|
-
</div>
|
|
246
|
-
<a
|
|
247
|
-
href="https://cli.github.com"
|
|
248
|
-
target="_blank"
|
|
249
|
-
rel="noopener noreferrer"
|
|
250
|
-
className="block text-xs text-primary underline"
|
|
251
|
-
>
|
|
252
|
-
Install GitHub CLI
|
|
253
|
-
</a>
|
|
254
|
-
</div>
|
|
255
|
-
)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (!status.ghAuthenticated) {
|
|
259
|
-
return (
|
|
260
|
-
<div className="space-y-2 px-3 py-1.5">
|
|
261
|
-
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
262
|
-
<CloudOff className="h-3.5 w-3.5" />
|
|
263
|
-
Not logged in to GitHub
|
|
264
|
-
</div>
|
|
265
|
-
<div className="rounded-md bg-muted px-2 py-1.5 text-xs font-mono">
|
|
266
|
-
gh auth login
|
|
267
|
-
</div>
|
|
268
|
-
</div>
|
|
269
|
-
)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const folders = [
|
|
273
|
-
{ key: 'claude' as const, label: '.claude', status: status.claude },
|
|
274
|
-
{ key: 'codex' as const, label: '.codex', status: status.codex },
|
|
275
|
-
].filter((f) => f.status.exists)
|
|
276
|
-
|
|
277
|
-
return (
|
|
278
|
-
<div className="space-y-1.5 px-1">
|
|
279
|
-
<div className="flex items-center gap-1.5 px-2 text-xs text-muted-foreground">
|
|
280
|
-
<Cloud className="h-3 w-3" />
|
|
281
|
-
{status.ghUser}
|
|
282
|
-
</div>
|
|
283
|
-
{folders.map(({ key, label, status: fs }) => {
|
|
284
|
-
const isSetUp = fs.hasGit && fs.hasRemote
|
|
285
|
-
const isSyncing = syncing[key]
|
|
286
|
-
|
|
287
|
-
return (
|
|
288
|
-
<div key={key} className="flex items-center justify-between px-2">
|
|
289
|
-
<div className="flex items-center gap-1.5">
|
|
290
|
-
<span className="text-xs font-medium">~/{label}</span>
|
|
291
|
-
{isSetUp && !fs.isDirty && (
|
|
292
|
-
<Check className="h-3 w-3 text-green-500" />
|
|
293
|
-
)}
|
|
294
|
-
{isSetUp && fs.isDirty && (
|
|
295
|
-
<Badge variant="secondary" className="h-4 px-1 text-[10px]">
|
|
296
|
-
new
|
|
297
|
-
</Badge>
|
|
298
|
-
)}
|
|
299
|
-
</div>
|
|
300
|
-
{!isSetUp ? (
|
|
301
|
-
<Button
|
|
302
|
-
variant="outline"
|
|
303
|
-
size="xs"
|
|
304
|
-
onClick={() => handleInit(key)}
|
|
305
|
-
disabled={isSyncing}
|
|
306
|
-
className="gap-1"
|
|
307
|
-
>
|
|
308
|
-
{isSyncing ? <Loader2 className="h-3 w-3 animate-spin" /> : <Upload className="h-3 w-3" />}
|
|
309
|
-
Backup
|
|
310
|
-
</Button>
|
|
311
|
-
) : (
|
|
312
|
-
<Button
|
|
313
|
-
variant="ghost"
|
|
314
|
-
size="xs"
|
|
315
|
-
onClick={() => handleSync(key)}
|
|
316
|
-
disabled={isSyncing}
|
|
317
|
-
className="gap-1"
|
|
318
|
-
>
|
|
319
|
-
{isSyncing ? <Loader2 className="h-3 w-3 animate-spin" /> : <Cloud className="h-3 w-3" />}
|
|
320
|
-
Sync
|
|
321
|
-
</Button>
|
|
322
|
-
)}
|
|
323
|
-
</div>
|
|
324
|
-
)
|
|
325
|
-
})}
|
|
326
|
-
</div>
|
|
327
|
-
)
|
|
328
|
-
}
|
|
329
|
-
|
|
330
122
|
export function AppSidebar() {
|
|
331
123
|
const pathname = usePathname()
|
|
332
124
|
const plugins = getPlugins()
|
|
@@ -369,22 +161,13 @@ export function AppSidebar() {
|
|
|
369
161
|
</SidebarMenuButton>
|
|
370
162
|
</SidebarMenuItem>
|
|
371
163
|
)}
|
|
372
|
-
</SidebarMenu>
|
|
373
|
-
</SidebarGroupContent>
|
|
374
|
-
</SidebarGroup>
|
|
375
|
-
|
|
376
|
-
{/* Data Management Section */}
|
|
377
|
-
<SidebarGroup>
|
|
378
|
-
<SidebarGroupLabel className="sr-only">Settings</SidebarGroupLabel>
|
|
379
|
-
<SidebarGroupContent>
|
|
380
|
-
<SidebarMenu>
|
|
381
164
|
<SidebarMenuItem>
|
|
382
165
|
<SidebarMenuButton
|
|
383
|
-
render={<Link href="/
|
|
384
|
-
isActive={pathname === '/
|
|
166
|
+
render={<Link href="/data-management" />}
|
|
167
|
+
isActive={pathname === '/data-management'}
|
|
385
168
|
>
|
|
386
169
|
<Settings className="h-4 w-4" />
|
|
387
|
-
<span>
|
|
170
|
+
<span>Data Management</span>
|
|
388
171
|
</SidebarMenuButton>
|
|
389
172
|
</SidebarMenuItem>
|
|
390
173
|
</SidebarMenu>
|
|
@@ -393,14 +176,16 @@ export function AppSidebar() {
|
|
|
393
176
|
</SidebarContent>
|
|
394
177
|
|
|
395
178
|
<SidebarFooter>
|
|
396
|
-
<div className="flex items-center justify-
|
|
397
|
-
<span>v0.1.0</span>
|
|
179
|
+
<div className="flex items-center justify-center px-3 py-2 text-xs text-muted-foreground">
|
|
398
180
|
<a
|
|
399
181
|
href="https://github.com/harrywang/agentfit"
|
|
400
182
|
target="_blank"
|
|
401
183
|
rel="noopener noreferrer"
|
|
402
|
-
className="hover:text-foreground transition-colors"
|
|
184
|
+
className="flex items-center gap-1.5 hover:text-foreground transition-colors"
|
|
403
185
|
>
|
|
186
|
+
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
|
187
|
+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
|
188
|
+
</svg>
|
|
404
189
|
GitHub
|
|
405
190
|
</a>
|
|
406
191
|
</div>
|
|
@@ -161,22 +161,40 @@ const commandConfig = {
|
|
|
161
161
|
count: { label: 'Uses', color: 'var(--chart-1)' },
|
|
162
162
|
} satisfies ChartConfig
|
|
163
163
|
|
|
164
|
+
interface CommandBarEntry {
|
|
165
|
+
name: string
|
|
166
|
+
count: number
|
|
167
|
+
fill: string
|
|
168
|
+
historyCount: number
|
|
169
|
+
sessionCount: number
|
|
170
|
+
}
|
|
171
|
+
|
|
164
172
|
export function TopCommandsChart() {
|
|
165
|
-
const [data, setData] = useState<
|
|
173
|
+
const [data, setData] = useState<CommandBarEntry[]>([])
|
|
166
174
|
|
|
167
175
|
useEffect(() => {
|
|
168
176
|
fetch('/api/commands')
|
|
169
177
|
.then(r => r.json())
|
|
170
178
|
.then(analysis => {
|
|
179
|
+
const dbSkills: Record<string, number> = analysis.dbSkillCounts || {}
|
|
171
180
|
const bi = analysis.commands
|
|
172
181
|
.filter((c: { used: boolean; count: number }) => c.used && c.count > 0)
|
|
173
|
-
.map((c: { command: string; count: number }) =>
|
|
182
|
+
.map((c: { command: string; count: number }) => {
|
|
183
|
+
const sk = dbSkills[c.command] || 0
|
|
184
|
+
return { name: c.command, count: c.count, fill: 'var(--chart-1)', historyCount: c.count - sk, sessionCount: sk }
|
|
185
|
+
})
|
|
174
186
|
.sort((a: { count: number }, b: { count: number }) => b.count - a.count)
|
|
175
187
|
.slice(0, 5)
|
|
176
188
|
const cu = analysis.customCommands
|
|
177
189
|
.sort((a: { count: number }, b: { count: number }) => b.count - a.count)
|
|
178
190
|
.slice(0, 5)
|
|
179
|
-
.map((c: { command: string; count: number }) => ({
|
|
191
|
+
.map((c: { command: string; count: number; historyCount?: number; sessionCount?: number }) => ({
|
|
192
|
+
name: c.command,
|
|
193
|
+
count: c.count,
|
|
194
|
+
fill: 'var(--chart-4)',
|
|
195
|
+
historyCount: c.historyCount || 0,
|
|
196
|
+
sessionCount: c.sessionCount || 0,
|
|
197
|
+
}))
|
|
180
198
|
const combined = [...bi, ...cu].sort((a, b) => b.count - a.count)
|
|
181
199
|
setData(combined)
|
|
182
200
|
})
|
|
@@ -213,7 +231,32 @@ export function TopCommandsChart() {
|
|
|
213
231
|
<BarChart data={data} layout="vertical" margin={{ left: 8 }} accessibilityLayer>
|
|
214
232
|
<XAxis type="number" tickLine={false} axisLine={false} />
|
|
215
233
|
<YAxis type="category" dataKey="name" tickLine={false} axisLine={false} width={140} />
|
|
216
|
-
<
|
|
234
|
+
<RechartsTooltip
|
|
235
|
+
cursor={{ fill: 'var(--muted)', opacity: 0.3 }}
|
|
236
|
+
content={({ active, payload }) => {
|
|
237
|
+
if (!active || !payload?.length) return null
|
|
238
|
+
const d = payload[0].payload as CommandBarEntry
|
|
239
|
+
return (
|
|
240
|
+
<div className="rounded-lg border bg-background px-3 py-2 text-sm shadow-md">
|
|
241
|
+
<div className="font-semibold">{d.name}</div>
|
|
242
|
+
<div className="text-muted-foreground mt-1 space-y-0.5">
|
|
243
|
+
<div className="flex justify-between gap-4">
|
|
244
|
+
<span>Slash command</span>
|
|
245
|
+
<span className="font-mono">{d.historyCount ?? 0}</span>
|
|
246
|
+
</div>
|
|
247
|
+
<div className="flex justify-between gap-4">
|
|
248
|
+
<span>Skill invocation</span>
|
|
249
|
+
<span className="font-mono">{d.sessionCount ?? 0}</span>
|
|
250
|
+
</div>
|
|
251
|
+
<div className="flex justify-between gap-4 border-t pt-0.5 text-foreground font-medium">
|
|
252
|
+
<span>Total</span>
|
|
253
|
+
<span className="font-mono">{d.count ?? 0}</span>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
)
|
|
258
|
+
}}
|
|
259
|
+
/>
|
|
217
260
|
<Bar dataKey="count" radius={[0, 4, 4, 0]}>
|
|
218
261
|
{data.map((entry, i) => (
|
|
219
262
|
<Cell key={i} fill={entry.fill} />
|