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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { StorybookConfig } from '@storybook/react-vite'
import type { StorybookConfig } from '@storybook/svelte-vite'

const config: StorybookConfig = {
stories: ['../frontend/src/**/*.stories.@(ts|tsx)'],
stories: ['../frontend/src/**/*.stories.@(ts|svelte.ts)'],
addons: ['@storybook/addon-a11y', '@storybook/addon-vitest'],
framework: {
name: '@storybook/react-vite',
name: '@storybook/svelte-vite',
options: {},
},
}
Expand Down
2 changes: 1 addition & 1 deletion .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Preview } from '@storybook/react-vite'
import type { Preview } from '@storybook/svelte-vite'
import '../frontend/src/index.css'

const preview: Preview = {
Expand Down
87 changes: 87 additions & 0 deletions backend/src/routes/agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import { Hono } from 'hono'
import { agentsRoutes } from './agents.js'
import type { AgentRegistry } from '../agents/registry.js'

const { getHistorySourceDescriptorsMock } = vi.hoisted(() => ({
getHistorySourceDescriptorsMock: vi.fn<(...args: unknown[]) => unknown[]>(() => []),
}))

vi.mock('../history/index.js', () => ({
getHistorySourceDescriptors: getHistorySourceDescriptorsMock,
}))

function makeTempDir(): string {
const dir = join(tmpdir(), `acp-agents-routes-test-${Date.now()}-${Math.random()}`)
mkdirSync(dir, { recursive: true })
Expand Down Expand Up @@ -105,6 +113,8 @@ describe('agents routes', () => {
let origEnv: string | undefined

beforeEach(() => {
getHistorySourceDescriptorsMock.mockReset()
getHistorySourceDescriptorsMock.mockReturnValue([])
tempDir = makeTempDir()
origEnv = process.env['ACP_HISTORY_SOURCES_CONFIG_PATH']
process.env['ACP_HISTORY_SOURCES_CONFIG_PATH'] = join(tempDir, 'history-sources.json')
Expand Down Expand Up @@ -223,5 +233,82 @@ describe('agents routes', () => {

expect(res.status).toBe(404)
})

it('GET /history-sources/status returns provider discovery summaries', async () => {
getHistorySourceDescriptorsMock.mockImplementation((agentIdRaw: unknown) => {
const agentId = String(agentIdRaw)
if (agentId === 'copilot') {
return [
{
id: 'copilot:vscode_workspace_db:/x/state.vscdb',
backendId: 'copilot',
providerId: 'copilot',
kind: 'vscode_workspace_db',
path: '/x/state.vscdb',
platform: 'linux',
access: 'readable',
signal: 'contains_history',
discoveredBy: 'manual',
sessionCount: 7,
},
]
}

if (agentId === 'gemini-cli') {
return [
{
id: 'gemini:tmp:/tmp/google-generative-ai-cli',
backendId: 'gemini-cli',
providerId: 'gemini-cli',
kind: 'gemini_tmp_dir',
path: '/tmp/google-generative-ai-cli',
platform: 'linux',
access: 'missing',
signal: 'unknown',
discoveredBy: 'auto',
},
]
}

return []
})

const registry = createRegistryStub()
const app = new Hono().route('/api', agentsRoutes(registry))

const res = await app.request('/api/history-sources/status')
expect(res.status).toBe(200)

const body = (await res.json()) as Array<{
provider: string
discoveredSources: Array<{ id: string }>
summary: {
readable: number
missing: number
invalid: number
containsHistory: number
totalSessions: number
}
}>

const copilot = body.find((item) => item.provider === 'copilot')
expect(copilot?.discoveredSources).toHaveLength(1)
expect(copilot?.summary).toMatchObject({
readable: 1,
missing: 0,
invalid: 0,
containsHistory: 1,
totalSessions: 7,
})

const gemini = body.find((item) => item.provider === 'gemini')
expect(gemini?.summary).toMatchObject({
readable: 0,
missing: 1,
invalid: 0,
containsHistory: 0,
totalSessions: 0,
})
})
})
})
52 changes: 52 additions & 0 deletions backend/src/routes/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,29 @@ import {
updateHistorySource,
type HistoryProvider,
} from '../history/sources-config.js'
import { getHistorySourceDescriptors } from '../history/index.js'
import type { HistorySourceDescriptor } from '../agents/types.js'

const VALID_PROVIDERS = new Set<string>(['gemini', 'copilot', 'opencode'])

const PROVIDER_AGENT_ID: Record<HistoryProvider, string> = {
copilot: 'copilot',
gemini: 'gemini-cli',
opencode: 'opencode',
}

interface HistorySourceStatus {
provider: HistoryProvider
discoveredSources: HistorySourceDescriptor[]
summary: {
readable: number
missing: number
invalid: number
containsHistory: number
totalSessions: number
}
}

export function agentsRoutes(registry: AgentRegistry): Hono {
const app = new Hono()

Expand Down Expand Up @@ -67,6 +87,38 @@ export function agentsRoutes(registry: AgentRegistry): Hono {
return c.json(sources)
})

app.get('/history-sources/status', (c) => {
const sources = readHistorySourcesConfig()
const status: HistorySourceStatus[] = sources.map((source) => {
const agentId = PROVIDER_AGENT_ID[source.provider]
const discoveredSources = getHistorySourceDescriptors(
agentId,
source.paths,
source.cliPaths ?? []
)

const summary = discoveredSources.reduce(
(acc, descriptor) => {
if (descriptor.access === 'readable') acc.readable += 1
if (descriptor.access === 'missing') acc.missing += 1
if (descriptor.access === 'invalid') acc.invalid += 1
if (descriptor.signal === 'contains_history') acc.containsHistory += 1
acc.totalSessions += descriptor.sessionCount ?? 0
return acc
},
{ readable: 0, missing: 0, invalid: 0, containsHistory: 0, totalSessions: 0 }
)

return {
provider: source.provider,
discoveredSources,
summary,
}
})

return c.json(status)
})

app.patch('/history-sources/:provider', async (c) => {
const provider = c.req.param('provider')

Expand Down
32 changes: 22 additions & 10 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import sveltePlugin from 'eslint-plugin-svelte'
import svelteParser from 'svelte-eslint-parser'

export default tseslint.config(
{
Expand All @@ -18,21 +18,33 @@ export default tseslint.config(
},
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
files: ['**/*.{ts,mts,cts}'],
languageOptions: {
ecmaVersion: 2022,
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
},
{
extends: [
js.configs.recommended,
...tseslint.configs.recommended,
...sveltePlugin.configs['flat/recommended'],
],
files: ['**/*.svelte', '**/*.svelte.ts'],
languageOptions: {
ecmaVersion: 2022,
globals: {
...globals.browser,
...globals.node,
},
parser: svelteParser,
parserOptions: {
parser: tseslint.parser,
extraFileExtensions: ['.svelte'],
},
},
}
)
2 changes: 1 addition & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
33 changes: 33 additions & 0 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script lang="ts">
import { onMount } from 'svelte'
import ChatPage from './routes/ChatPage.svelte'
import SettingsPage from './routes/SettingsPage.svelte'

type Route = 'chat' | 'settings'

function getRouteFromHash(): Route {
const hash = window.location.hash
if (hash.startsWith('#/settings')) return 'settings'
return 'chat'
}

let route = $state<Route>(getRouteFromHash())

onMount(() => {
const handleHashChange = () => {
route = getRouteFromHash()
}
window.addEventListener('hashchange', handleHashChange)
// Redirect bare / or empty hash to #/chat
if (!window.location.hash || window.location.hash === '#/') {
window.location.hash = '#/chat'
}
return () => window.removeEventListener('hashchange', handleHashChange)
})
</script>

{#if route === 'settings'}
<SettingsPage />
{:else}
<ChatPage />
{/if}
10 changes: 0 additions & 10 deletions frontend/src/App.tsx

This file was deleted.

Loading
Loading