feat(web): provider management UI — list, test, model capabilities (#123) (#142)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #142.
This commit is contained in:
17
apps/gateway/src/agent/provider.dto.ts
Normal file
17
apps/gateway/src/agent/provider.dto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface TestConnectionDto {
|
||||
/** Provider identifier to test (e.g. 'ollama', custom provider id) */
|
||||
providerId: string;
|
||||
/** Optional base URL override for ad-hoc testing */
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export interface TestConnectionResultDto {
|
||||
providerId: string;
|
||||
reachable: boolean;
|
||||
/** Round-trip latency in milliseconds (present when reachable) */
|
||||
latencyMs?: number;
|
||||
/** Human-readable error when unreachable */
|
||||
error?: string;
|
||||
/** Model ids discovered at the remote endpoint (present when reachable) */
|
||||
discoveredModels?: string[];
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Injectable, Logger, type OnModuleInit } from '@nestjs/common';
|
||||
import { ModelRegistry, AuthStorage } from '@mariozechner/pi-coding-agent';
|
||||
import type { Model, Api } from '@mariozechner/pi-ai';
|
||||
import type { ModelInfo, ProviderInfo, CustomProviderConfig } from '@mosaic/types';
|
||||
import type { TestConnectionResultDto } from './provider.dto.js';
|
||||
|
||||
@Injectable()
|
||||
export class ProviderService implements OnModuleInit {
|
||||
@@ -64,6 +65,63 @@ export class ProviderService implements OnModuleInit {
|
||||
return this.registry.getAvailable().map((m) => this.toModelInfo(m));
|
||||
}
|
||||
|
||||
async testConnection(providerId: string, baseUrl?: string): Promise<TestConnectionResultDto> {
|
||||
// Resolve baseUrl: explicit override > registered provider > ollama env
|
||||
let resolvedUrl = baseUrl;
|
||||
|
||||
if (!resolvedUrl) {
|
||||
const allModels = this.registry.getAll();
|
||||
const providerModels = allModels.filter((m) => m.provider === providerId);
|
||||
if (providerModels.length === 0) {
|
||||
return { providerId, reachable: false, error: `Provider '${providerId}' not found` };
|
||||
}
|
||||
// For Ollama, derive the base URL from environment
|
||||
if (providerId === 'ollama') {
|
||||
const ollamaUrl = process.env['OLLAMA_BASE_URL'] ?? process.env['OLLAMA_HOST'];
|
||||
if (!ollamaUrl) {
|
||||
return { providerId, reachable: false, error: 'OLLAMA_BASE_URL not configured' };
|
||||
}
|
||||
resolvedUrl = `${ollamaUrl}/v1/models`;
|
||||
} else {
|
||||
// For other providers, we can only do a basic check
|
||||
return { providerId, reachable: true, discoveredModels: providerModels.map((m) => m.id) };
|
||||
}
|
||||
} else {
|
||||
resolvedUrl = resolvedUrl.replace(/\/?$/, '') + '/models';
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
try {
|
||||
const res = await fetch(resolvedUrl, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
|
||||
const latencyMs = Date.now() - start;
|
||||
|
||||
if (!res.ok) {
|
||||
return { providerId, reachable: false, latencyMs, error: `HTTP ${res.status}` };
|
||||
}
|
||||
|
||||
let discoveredModels: string[] | undefined;
|
||||
try {
|
||||
const json = (await res.json()) as { models?: Array<{ id?: string; name?: string }> };
|
||||
if (Array.isArray(json.models)) {
|
||||
discoveredModels = json.models.map((m) => m.id ?? m.name ?? '').filter(Boolean);
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors — endpoint was reachable
|
||||
}
|
||||
|
||||
return { providerId, reachable: true, latencyMs, discoveredModels };
|
||||
} catch (err) {
|
||||
const latencyMs = Date.now() - start;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { providerId, reachable: false, latencyMs, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
registerCustomProvider(config: CustomProviderConfig): void {
|
||||
this.registry.registerProvider(config.id, {
|
||||
baseUrl: config.baseUrl,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { RoutingCriteria } from '@mosaic/types';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { ProviderService } from './provider.service.js';
|
||||
import { RoutingService } from './routing.service.js';
|
||||
import type { TestConnectionDto, TestConnectionResultDto } from './provider.dto.js';
|
||||
|
||||
@Controller('api/providers')
|
||||
@UseGuards(AuthGuard)
|
||||
@@ -22,6 +23,11 @@ export class ProvidersController {
|
||||
return this.providerService.listAvailableModels();
|
||||
}
|
||||
|
||||
@Post('test')
|
||||
testConnection(@Body() body: TestConnectionDto): Promise<TestConnectionResultDto> {
|
||||
return this.providerService.testConnection(body.providerId, body.baseUrl);
|
||||
}
|
||||
|
||||
@Post('route')
|
||||
route(@Body() criteria: RoutingCriteria) {
|
||||
return this.routingService.route(criteria);
|
||||
|
||||
@@ -1,42 +1,85 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
|
||||
interface ProviderInfo {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
modelCount: number;
|
||||
}
|
||||
|
||||
interface ModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
contextWindow: number;
|
||||
name: string;
|
||||
reasoning: boolean;
|
||||
cost: { input: number; output: number };
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
inputTypes: ('text' | 'image')[];
|
||||
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
||||
}
|
||||
|
||||
interface ProviderInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
available: boolean;
|
||||
models: ModelInfo[];
|
||||
}
|
||||
|
||||
interface TestConnectionResult {
|
||||
providerId: string;
|
||||
reachable: boolean;
|
||||
latencyMs?: number;
|
||||
error?: string;
|
||||
discoveredModels?: string[];
|
||||
}
|
||||
|
||||
type TestState = 'idle' | 'testing' | 'success' | 'error';
|
||||
|
||||
interface ProviderTestStatus {
|
||||
state: TestState;
|
||||
result?: TestConnectionResult;
|
||||
}
|
||||
|
||||
export default function SettingsPage(): React.ReactElement {
|
||||
const { data: session } = useSession();
|
||||
const [providers, setProviders] = useState<ProviderInfo[]>([]);
|
||||
const [models, setModels] = useState<ModelInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [testStatuses, setTestStatuses] = useState<Record<string, ProviderTestStatus>>({});
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
api<ProviderInfo[]>('/api/providers').catch(() => []),
|
||||
api<ModelInfo[]>('/api/providers/models').catch(() => []),
|
||||
])
|
||||
.then(([p, m]) => {
|
||||
setProviders(p);
|
||||
setModels(m);
|
||||
})
|
||||
api<ProviderInfo[]>('/api/providers')
|
||||
.catch(() => [] as ProviderInfo[])
|
||||
.then((p) => setProviders(p))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const testConnection = useCallback(async (providerId: string): Promise<void> => {
|
||||
setTestStatuses((prev) => ({
|
||||
...prev,
|
||||
[providerId]: { state: 'testing' },
|
||||
}));
|
||||
try {
|
||||
const result = await api<TestConnectionResult>('/api/providers/test', {
|
||||
method: 'POST',
|
||||
body: { providerId },
|
||||
});
|
||||
setTestStatuses((prev) => ({
|
||||
...prev,
|
||||
[providerId]: { state: result.reachable ? 'success' : 'error', result },
|
||||
}));
|
||||
} catch {
|
||||
setTestStatuses((prev) => ({
|
||||
...prev,
|
||||
[providerId]: {
|
||||
state: 'error',
|
||||
result: { providerId, reachable: false, error: 'Request failed' },
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Derive default model: first available model across all providers
|
||||
const defaultModel: ModelInfo | undefined = providers
|
||||
.flatMap((p) => p.models)
|
||||
.find((m) => providers.find((p) => p.id === m.provider)?.available);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl space-y-8">
|
||||
<h1 className="text-2xl font-semibold">Settings</h1>
|
||||
@@ -64,82 +107,250 @@ export default function SettingsPage(): React.ReactElement {
|
||||
<p className="text-sm text-text-muted">Loading providers...</p>
|
||||
) : providers.length === 0 ? (
|
||||
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
||||
<p className="text-sm text-text-muted">No providers configured</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
No providers configured. Set{' '}
|
||||
<code className="rounded bg-surface-elevated px-1 py-0.5 text-xs">
|
||||
OLLAMA_BASE_URL
|
||||
</code>{' '}
|
||||
or{' '}
|
||||
<code className="rounded bg-surface-elevated px-1 py-0.5 text-xs">
|
||||
MOSAIC_CUSTOM_PROVIDERS
|
||||
</code>{' '}
|
||||
to add providers.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{providers.map((p) => (
|
||||
<div
|
||||
key={p.name}
|
||||
className="flex items-center justify-between rounded-lg border border-surface-border bg-surface-card p-4"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-text-primary">{p.name}</p>
|
||||
<p className="text-xs text-text-muted">{p.modelCount} models available</p>
|
||||
</div>
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs ${
|
||||
p.enabled ? 'bg-success/20 text-success' : 'bg-gray-600/20 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{p.enabled ? 'Active' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{providers.map((provider) => (
|
||||
<ProviderCard
|
||||
key={provider.id}
|
||||
provider={provider}
|
||||
defaultModel={defaultModel}
|
||||
testStatus={testStatuses[provider.id] ?? { state: 'idle' }}
|
||||
onTest={() => void testConnection(provider.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Models */}
|
||||
<section>
|
||||
<h2 className="mb-4 text-lg font-medium text-text-secondary">Available Models</h2>
|
||||
{loading ? (
|
||||
<p className="text-sm text-text-muted">Loading models...</p>
|
||||
) : models.length === 0 ? (
|
||||
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
||||
<p className="text-sm text-text-muted">No models available</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-lg border border-surface-border">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-surface-border bg-surface-elevated text-left text-xs text-text-muted">
|
||||
<th className="px-4 py-2 font-medium">Model</th>
|
||||
<th className="px-4 py-2 font-medium">Provider</th>
|
||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Context</th>
|
||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Cost (in/out)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{models.map((m) => (
|
||||
<tr
|
||||
key={`${m.provider}-${m.id}`}
|
||||
className="border-b border-surface-border last:border-b-0"
|
||||
>
|
||||
<td className="px-4 py-2 text-sm text-text-primary">
|
||||
{m.name}
|
||||
{m.reasoning && (
|
||||
<span className="ml-2 text-xs text-purple-400">reasoning</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-xs text-text-muted">{m.provider}</td>
|
||||
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
||||
{(m.contextWindow / 1000).toFixed(0)}k
|
||||
</td>
|
||||
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
||||
${m.cost.input} / ${m.cost.output}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProviderCardProps {
|
||||
provider: ProviderInfo;
|
||||
defaultModel: ModelInfo | undefined;
|
||||
testStatus: ProviderTestStatus;
|
||||
onTest: () => void;
|
||||
}
|
||||
|
||||
function ProviderCard({
|
||||
provider,
|
||||
defaultModel,
|
||||
testStatus,
|
||||
onTest,
|
||||
}: ProviderCardProps): React.ReactElement {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-surface-border bg-surface-card">
|
||||
{/* Header row */}
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<ProviderAvatar id={provider.id} />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-text-primary">{provider.name}</span>
|
||||
<ProviderStatusBadge available={provider.available} />
|
||||
</div>
|
||||
<p className="text-xs text-text-muted">
|
||||
{provider.models.length} model{provider.models.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<TestConnectionButton status={testStatus} onTest={onTest} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="rounded px-2 py-1 text-xs text-text-muted transition-colors hover:bg-surface-elevated hover:text-text-primary"
|
||||
aria-expanded={expanded}
|
||||
aria-label={expanded ? 'Collapse models' : 'Expand models'}
|
||||
>
|
||||
{expanded ? '▲ Hide' : '▼ Models'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test result banner */}
|
||||
{testStatus.state !== 'idle' && testStatus.state !== 'testing' && testStatus.result && (
|
||||
<TestResultBanner result={testStatus.result} />
|
||||
)}
|
||||
|
||||
{/* Model list */}
|
||||
{expanded && (
|
||||
<div className="border-t border-surface-border">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-surface-elevated text-left text-xs text-text-muted">
|
||||
<th className="px-4 py-2 font-medium">Model</th>
|
||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Capabilities</th>
|
||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Context</th>
|
||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Cost (in/out)</th>
|
||||
<th className="px-4 py-2 font-medium">Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{provider.models.map((model) => (
|
||||
<ModelRow
|
||||
key={model.id}
|
||||
model={model}
|
||||
isDefault={
|
||||
defaultModel?.id === model.id && defaultModel?.provider === model.provider
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ModelRowProps {
|
||||
model: ModelInfo;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
function ModelRow({ model, isDefault }: ModelRowProps): React.ReactElement {
|
||||
return (
|
||||
<tr className="border-t border-surface-border">
|
||||
<td className="px-4 py-2">
|
||||
<span className="text-sm text-text-primary">{model.name}</span>
|
||||
</td>
|
||||
<td className="hidden px-4 py-2 md:table-cell">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<CapabilityBadge label="chat" />
|
||||
{model.reasoning && <CapabilityBadge label="reasoning" color="purple" />}
|
||||
{model.inputTypes.includes('image') && <CapabilityBadge label="vision" color="blue" />}
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
||||
{formatContext(model.contextWindow)}
|
||||
</td>
|
||||
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
||||
{model.cost.input === 0 && model.cost.output === 0
|
||||
? 'free'
|
||||
: `$${model.cost.input} / $${model.cost.output}`}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
{isDefault && (
|
||||
<span
|
||||
className="inline-block rounded-full bg-accent/20 px-2 py-0.5 text-xs font-medium text-accent"
|
||||
title="Default model used for new sessions"
|
||||
>
|
||||
default
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderAvatar({ id }: { id: string }): React.ReactElement {
|
||||
const letter = id.charAt(0).toUpperCase();
|
||||
return (
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-surface-elevated text-sm font-semibold text-text-secondary">
|
||||
{letter}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderStatusBadge({ available }: { available: boolean }): React.ReactElement {
|
||||
return (
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
available ? 'bg-success/20 text-success' : 'bg-surface-elevated text-text-muted'
|
||||
}`}
|
||||
>
|
||||
{available ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface TestConnectionButtonProps {
|
||||
status: ProviderTestStatus;
|
||||
onTest: () => void;
|
||||
}
|
||||
|
||||
function TestConnectionButton({ status, onTest }: TestConnectionButtonProps): React.ReactElement {
|
||||
const isTesting = status.state === 'testing';
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onTest}
|
||||
disabled={isTesting}
|
||||
className="rounded px-2 py-1 text-xs transition-colors hover:bg-surface-elevated disabled:cursor-not-allowed disabled:opacity-50"
|
||||
title="Test connection"
|
||||
>
|
||||
{isTesting ? (
|
||||
<span className="text-text-muted">Testing…</span>
|
||||
) : status.state === 'success' ? (
|
||||
<span className="text-success">✓ Reachable</span>
|
||||
) : status.state === 'error' ? (
|
||||
<span className="text-error">✗ Unreachable</span>
|
||||
) : (
|
||||
<span className="text-text-muted">Test</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function TestResultBanner({ result }: { result: TestConnectionResult }): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={`px-4 py-2 text-xs ${
|
||||
result.reachable ? 'bg-success/10 text-success' : 'bg-error/10 text-error'
|
||||
}`}
|
||||
>
|
||||
{result.reachable ? (
|
||||
<>
|
||||
Connected
|
||||
{result.latencyMs !== undefined && (
|
||||
<span className="ml-1 opacity-70">({result.latencyMs}ms)</span>
|
||||
)}
|
||||
{result.discoveredModels && result.discoveredModels.length > 0 && (
|
||||
<span className="ml-2 opacity-70">
|
||||
— {result.discoveredModels.length} model
|
||||
{result.discoveredModels.length !== 1 ? 's' : ''} discovered
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>Connection failed{result.error ? `: ${result.error}` : ''}</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CapabilityBadge({
|
||||
label,
|
||||
color = 'default',
|
||||
}: {
|
||||
label: string;
|
||||
color?: 'default' | 'purple' | 'blue';
|
||||
}): React.ReactElement {
|
||||
const colorClass =
|
||||
color === 'purple'
|
||||
? 'bg-purple-500/20 text-purple-400'
|
||||
: color === 'blue'
|
||||
? 'bg-blue-500/20 text-blue-400'
|
||||
: 'bg-surface-elevated text-text-muted';
|
||||
return <span className={`rounded px-1.5 py-0.5 text-xs ${colorClass}`}>{label}</span>;
|
||||
}
|
||||
|
||||
function Field({ label, value }: { label: string; value: string }): React.ReactElement {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -148,3 +359,9 @@ function Field({ label, value }: { label: string; value: string }): React.ReactE
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatContext(tokens: number): string {
|
||||
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
|
||||
if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k`;
|
||||
return String(tokens);
|
||||
}
|
||||
|
||||
154
docs/TASKS.md
154
docs/TASKS.md
@@ -2,80 +2,80 @@
|
||||
|
||||
> Single-writer: orchestrator only. Workers read but never modify.
|
||||
|
||||
| id | status | milestone | description | pr | notes |
|
||||
| ------ | ----------- | --------- | ------------------------------------------------------------------- | ---- | --------------------------- |
|
||||
| P0-001 | done | Phase 0 | Scaffold monorepo | #60 | #1 |
|
||||
| P0-002 | done | Phase 0 | @mosaic/types — migrate and extend shared types | #65 | #2 |
|
||||
| P0-003 | done | Phase 0 | @mosaic/db — Drizzle schema and PG connection | #67 | #3 |
|
||||
| P0-004 | done | Phase 0 | @mosaic/auth — BetterAuth email/password setup | #68 | #4 |
|
||||
| P0-005 | done | Phase 0 | Docker Compose — PG 17, Valkey 8, SigNoz | #65 | #5 |
|
||||
| P0-006 | done | Phase 0 | OTEL foundation — OpenTelemetry SDK setup | #65 | #6 |
|
||||
| P0-007 | done | Phase 0 | CI pipeline — Woodpecker config | #69 | #7 |
|
||||
| P0-008 | done | Phase 0 | Project docs — AGENTS.md, CLAUDE.md, README | #69 | #8 |
|
||||
| P0-009 | done | Phase 0 | Verify Phase 0 — CI green, all packages build | #70 | #9 |
|
||||
| P1-001 | done | Phase 1 | apps/gateway scaffold — NestJS + Fastify adapter | #61 | #10 |
|
||||
| P1-002 | done | Phase 1 | Auth middleware — BetterAuth session validation | #71 | #11 |
|
||||
| P1-003 | done | Phase 1 | @mosaic/brain — migrate from v0, PG backend | #71 | #12 |
|
||||
| P1-004 | done | Phase 1 | @mosaic/queue — migrate from v0 | #71 | #13 |
|
||||
| P1-005 | done | Phase 1 | Gateway routes — conversations CRUD + messages | #72 | #14 |
|
||||
| P1-006 | done | Phase 1 | Gateway routes — tasks, projects, missions CRUD | #72 | #15 |
|
||||
| P1-007 | done | Phase 1 | WebSocket server — chat streaming | #61 | #16 |
|
||||
| P1-008 | done | Phase 1 | Basic agent dispatch — single provider | #61 | #17 |
|
||||
| P1-009 | done | Phase 1 | Verify Phase 1 — gateway functional, API tested | #73 | #18 |
|
||||
| P2-001 | done | Phase 2 | @mosaic/agent — Pi SDK integration + agent pool | #61 | #19 |
|
||||
| P2-002 | done | Phase 2 | Multi-provider support — Anthropic + Ollama | #74 | #20 |
|
||||
| P2-003 | done | Phase 2 | Agent routing engine — cost/capability matrix | #75 | #21 |
|
||||
| P2-004 | done | Phase 2 | Tool registration — brain, queue, memory tools | #76 | #22 |
|
||||
| P2-005 | done | Phase 2 | @mosaic/coord — migrate from v0, gateway integration | #77 | #23 |
|
||||
| P2-006 | done | Phase 2 | Agent session management — tmux + monitoring | #78 | #24 |
|
||||
| P2-007 | done | Phase 2 | Verify Phase 2 — multi-provider routing works | #79 | #25 |
|
||||
| P3-001 | done | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | #82 | #26 |
|
||||
| P3-002 | done | Phase 3 | Auth pages — login, registration, SSO redirect | #83 | #27 |
|
||||
| P3-003 | done | Phase 3 | Chat UI — conversations, messages, streaming | #84 | #28 |
|
||||
| P3-004 | done | Phase 3 | Task management — list view + kanban board | #86 | #29 |
|
||||
| P3-005 | done | Phase 3 | Project & mission views — dashboard + PRD viewer | #87 | #30 |
|
||||
| P3-006 | done | Phase 3 | Settings — provider config, profile, integrations | #88 | #31 |
|
||||
| P3-007 | done | Phase 3 | Admin panel — user management, RBAC | #89 | #32 |
|
||||
| P3-008 | done | Phase 3 | Verify Phase 3 — web dashboard functional E2E | — | #33 |
|
||||
| P4-001 | done | Phase 4 | @mosaic/memory — preference + insight stores | — | #34 |
|
||||
| P4-002 | done | Phase 4 | Semantic search — pgvector embeddings + search API | — | #35 |
|
||||
| P4-003 | done | Phase 4 | @mosaic/log — log ingest, parsing, tiered storage | — | #36 |
|
||||
| P4-004 | done | Phase 4 | Summarization pipeline — Haiku-tier LLM + cron | — | #37 |
|
||||
| P4-005 | done | Phase 4 | Memory integration — inject into agent sessions | — | #38 |
|
||||
| P4-006 | done | Phase 4 | Skill management — catalog, install, config | — | #39 |
|
||||
| P4-007 | done | Phase 4 | Verify Phase 4 — memory + log pipeline working | — | #40 |
|
||||
| P5-001 | done | Phase 5 | Plugin host — gateway plugin loading + channel interface | — | #41 |
|
||||
| P5-002 | done | Phase 5 | @mosaic/discord-plugin — Discord bot + channel plugin | #61 | #42 |
|
||||
| P5-003 | done | Phase 5 | @mosaic/telegram-plugin — Telegraf bot + channel plugin | — | #43 |
|
||||
| P5-004 | done | Phase 5 | SSO — Authentik OIDC adapter end-to-end | — | #44 |
|
||||
| P5-005 | done | Phase 5 | Verify Phase 5 — Discord + Telegram + SSO working | #99 | #45 |
|
||||
| P6-001 | done | Phase 6 | @mosaic/cli — unified CLI binary + subcommands | #104 | #46 |
|
||||
| P6-002 | done | Phase 6 | @mosaic/prdy — migrate PRD wizard from v0 | #101 | #47 |
|
||||
| P6-003 | done | Phase 6 | @mosaic/quality-rails — migrate scaffolder from v0 | #100 | #48 |
|
||||
| P6-004 | done | Phase 6 | @mosaic/mosaic — install wizard for v1 | #103 | #49 |
|
||||
| P6-005 | done | Phase 6 | Pi TUI integration — mosaic tui | #61 | #50 |
|
||||
| P6-006 | done | Phase 6 | Verify Phase 6 — CLI functional, all subcommands | — | #51 |
|
||||
| P7-009 | not-started | Phase 7 | Web chat — WebSocket integration, streaming, conversation switching | — | #120 Wave-1 |
|
||||
| P7-001 | not-started | Phase 7 | MCP endpoint hardening — streamable HTTP transport | — | #52 Wave-1 |
|
||||
| P7-010 | not-started | Phase 7 | Web conversation management — list, search, rename, delete, archive | — | #121 Wave-2, depends:P7-009 |
|
||||
| P7-015 | not-started | Phase 7 | Agent tool expansion — file ops, git, shell exec, web fetch | — | #126 Wave-2 |
|
||||
| P7-011 | not-started | Phase 7 | Web project detail views — missions, tasks, PRDs, dashboards | — | #122 Wave-3 |
|
||||
| P7-016 | not-started | Phase 7 | MCP client — gateway connects to external MCP servers as tools | — | #127 Wave-3, depends:P7-001 |
|
||||
| P7-012 | not-started | Phase 7 | Web provider management UI — add, configure, test LLM providers | — | #123 Wave-4 |
|
||||
| P7-017 | not-started | Phase 7 | Agent skill invocation — load and execute skills from catalog | — | #128 Wave-4 |
|
||||
| P7-013 | not-started | Phase 7 | Web settings persistence — profile, preferences save to DB | — | #124 Wave-5 |
|
||||
| P7-018 | not-started | Phase 7 | CLI model/provider switching — --model, --provider, /model in TUI | — | #129 Wave-5 |
|
||||
| P7-014 | not-started | Phase 7 | Web admin panel — user CRUD, role assignment, system health | — | #125 Wave-6 |
|
||||
| P7-019 | not-started | Phase 7 | CLI session management — list, resume, destroy sessions | — | #130 Wave-6 |
|
||||
| P7-020 | not-started | Phase 7 | Coord DB migration — project-scoped missions, multi-tenant RBAC | — | #131 Wave-7 |
|
||||
| FIX-02 | not-started | Backlog | TUI agent:end — fix React state updater side-effect | — | #133 Wave-8 |
|
||||
| FIX-03 | not-started | Backlog | Agent session — cwd sandbox, system prompt, tool restrictions | — | #134 Wave-8 |
|
||||
| P7-004 | not-started | Phase 7 | E2E test suite — Playwright critical paths | — | #55 Wave-9 |
|
||||
| P7-006 | not-started | Phase 7 | Documentation — user guide, admin guide, dev guide | — | #57 Wave-9 |
|
||||
| P7-007 | not-started | Phase 7 | Bare-metal deployment docs + .env.example | — | #58 Wave-9 |
|
||||
| P7-021 | not-started | Phase 7 | Verify Phase 7 — feature-complete platform E2E | — | #132 Wave-10 |
|
||||
| P8-001 | not-started | Phase 8 | Additional SSO providers — WorkOS + Keycloak | — | #53 |
|
||||
| P8-002 | not-started | Phase 8 | Additional LLM providers — Codex, Z.ai, LM Studio, llama.cpp | — | #54 |
|
||||
| P8-003 | not-started | Phase 8 | Performance optimization | — | #56 |
|
||||
| P8-004 | not-started | Phase 8 | Beta release gate — v0.1.0 tag | — | #59 |
|
||||
| FIX-01 | done | Backlog | Call piSession.dispose() in AgentService.destroySession | #78 | #62 |
|
||||
| id | status | milestone | description | pr | notes |
|
||||
| ------ | ----------- | --------- | ------------------------------------------------------------------- | ---- | ------------ |
|
||||
| P0-001 | done | Phase 0 | Scaffold monorepo | #60 | #1 |
|
||||
| P0-002 | done | Phase 0 | @mosaic/types — migrate and extend shared types | #65 | #2 |
|
||||
| P0-003 | done | Phase 0 | @mosaic/db — Drizzle schema and PG connection | #67 | #3 |
|
||||
| P0-004 | done | Phase 0 | @mosaic/auth — BetterAuth email/password setup | #68 | #4 |
|
||||
| P0-005 | done | Phase 0 | Docker Compose — PG 17, Valkey 8, SigNoz | #65 | #5 |
|
||||
| P0-006 | done | Phase 0 | OTEL foundation — OpenTelemetry SDK setup | #65 | #6 |
|
||||
| P0-007 | done | Phase 0 | CI pipeline — Woodpecker config | #69 | #7 |
|
||||
| P0-008 | done | Phase 0 | Project docs — AGENTS.md, CLAUDE.md, README | #69 | #8 |
|
||||
| P0-009 | done | Phase 0 | Verify Phase 0 — CI green, all packages build | #70 | #9 |
|
||||
| P1-001 | done | Phase 1 | apps/gateway scaffold — NestJS + Fastify adapter | #61 | #10 |
|
||||
| P1-002 | done | Phase 1 | Auth middleware — BetterAuth session validation | #71 | #11 |
|
||||
| P1-003 | done | Phase 1 | @mosaic/brain — migrate from v0, PG backend | #71 | #12 |
|
||||
| P1-004 | done | Phase 1 | @mosaic/queue — migrate from v0 | #71 | #13 |
|
||||
| P1-005 | done | Phase 1 | Gateway routes — conversations CRUD + messages | #72 | #14 |
|
||||
| P1-006 | done | Phase 1 | Gateway routes — tasks, projects, missions CRUD | #72 | #15 |
|
||||
| P1-007 | done | Phase 1 | WebSocket server — chat streaming | #61 | #16 |
|
||||
| P1-008 | done | Phase 1 | Basic agent dispatch — single provider | #61 | #17 |
|
||||
| P1-009 | done | Phase 1 | Verify Phase 1 — gateway functional, API tested | #73 | #18 |
|
||||
| P2-001 | done | Phase 2 | @mosaic/agent — Pi SDK integration + agent pool | #61 | #19 |
|
||||
| P2-002 | done | Phase 2 | Multi-provider support — Anthropic + Ollama | #74 | #20 |
|
||||
| P2-003 | done | Phase 2 | Agent routing engine — cost/capability matrix | #75 | #21 |
|
||||
| P2-004 | done | Phase 2 | Tool registration — brain, queue, memory tools | #76 | #22 |
|
||||
| P2-005 | done | Phase 2 | @mosaic/coord — migrate from v0, gateway integration | #77 | #23 |
|
||||
| P2-006 | done | Phase 2 | Agent session management — tmux + monitoring | #78 | #24 |
|
||||
| P2-007 | done | Phase 2 | Verify Phase 2 — multi-provider routing works | #79 | #25 |
|
||||
| P3-001 | done | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | #82 | #26 |
|
||||
| P3-002 | done | Phase 3 | Auth pages — login, registration, SSO redirect | #83 | #27 |
|
||||
| P3-003 | done | Phase 3 | Chat UI — conversations, messages, streaming | #84 | #28 |
|
||||
| P3-004 | done | Phase 3 | Task management — list view + kanban board | #86 | #29 |
|
||||
| P3-005 | done | Phase 3 | Project & mission views — dashboard + PRD viewer | #87 | #30 |
|
||||
| P3-006 | done | Phase 3 | Settings — provider config, profile, integrations | #88 | #31 |
|
||||
| P3-007 | done | Phase 3 | Admin panel — user management, RBAC | #89 | #32 |
|
||||
| P3-008 | done | Phase 3 | Verify Phase 3 — web dashboard functional E2E | — | #33 |
|
||||
| P4-001 | done | Phase 4 | @mosaic/memory — preference + insight stores | — | #34 |
|
||||
| P4-002 | done | Phase 4 | Semantic search — pgvector embeddings + search API | — | #35 |
|
||||
| P4-003 | done | Phase 4 | @mosaic/log — log ingest, parsing, tiered storage | — | #36 |
|
||||
| P4-004 | done | Phase 4 | Summarization pipeline — Haiku-tier LLM + cron | — | #37 |
|
||||
| P4-005 | done | Phase 4 | Memory integration — inject into agent sessions | — | #38 |
|
||||
| P4-006 | done | Phase 4 | Skill management — catalog, install, config | — | #39 |
|
||||
| P4-007 | done | Phase 4 | Verify Phase 4 — memory + log pipeline working | — | #40 |
|
||||
| P5-001 | done | Phase 5 | Plugin host — gateway plugin loading + channel interface | — | #41 |
|
||||
| P5-002 | done | Phase 5 | @mosaic/discord-plugin — Discord bot + channel plugin | #61 | #42 |
|
||||
| P5-003 | done | Phase 5 | @mosaic/telegram-plugin — Telegraf bot + channel plugin | — | #43 |
|
||||
| P5-004 | done | Phase 5 | SSO — Authentik OIDC adapter end-to-end | — | #44 |
|
||||
| P5-005 | done | Phase 5 | Verify Phase 5 — Discord + Telegram + SSO working | #99 | #45 |
|
||||
| P6-001 | done | Phase 6 | @mosaic/cli — unified CLI binary + subcommands | #104 | #46 |
|
||||
| P6-002 | done | Phase 6 | @mosaic/prdy — migrate PRD wizard from v0 | #101 | #47 |
|
||||
| P6-003 | done | Phase 6 | @mosaic/quality-rails — migrate scaffolder from v0 | #100 | #48 |
|
||||
| P6-004 | done | Phase 6 | @mosaic/mosaic — install wizard for v1 | #103 | #49 |
|
||||
| P6-005 | done | Phase 6 | Pi TUI integration — mosaic tui | #61 | #50 |
|
||||
| P6-006 | done | Phase 6 | Verify Phase 6 — CLI functional, all subcommands | — | #51 |
|
||||
| P7-009 | done | Phase 7 | Web chat — WebSocket integration, streaming, conversation switching | #136 | #120 W1 done |
|
||||
| P7-001 | done | Phase 7 | MCP endpoint hardening — streamable HTTP transport | #137 | #52 W1 done |
|
||||
| P7-010 | done | Phase 7 | Web conversation management — list, search, rename, delete, archive | #139 | #121 W2 done |
|
||||
| P7-015 | done | Phase 7 | Agent tool expansion — file ops, git, shell exec, web fetch | #138 | #126 W2 done |
|
||||
| P7-011 | done | Phase 7 | Web project detail views — missions, tasks, PRDs, dashboards | #140 | #122 W3 done |
|
||||
| P7-016 | done | Phase 7 | MCP client — gateway connects to external MCP servers as tools | #141 | #127 W3 done |
|
||||
| P7-012 | in-progress | Phase 7 | Web provider management UI — add, configure, test LLM providers | — | #123 Wave-4 |
|
||||
| P7-017 | in-progress | Phase 7 | Agent skill invocation — load and execute skills from catalog | — | #128 Wave-4 |
|
||||
| P7-013 | not-started | Phase 7 | Web settings persistence — profile, preferences save to DB | — | #124 Wave-5 |
|
||||
| P7-018 | not-started | Phase 7 | CLI model/provider switching — --model, --provider, /model in TUI | — | #129 Wave-5 |
|
||||
| P7-014 | not-started | Phase 7 | Web admin panel — user CRUD, role assignment, system health | — | #125 Wave-6 |
|
||||
| P7-019 | not-started | Phase 7 | CLI session management — list, resume, destroy sessions | — | #130 Wave-6 |
|
||||
| P7-020 | not-started | Phase 7 | Coord DB migration — project-scoped missions, multi-tenant RBAC | — | #131 Wave-7 |
|
||||
| FIX-02 | not-started | Backlog | TUI agent:end — fix React state updater side-effect | — | #133 Wave-8 |
|
||||
| FIX-03 | not-started | Backlog | Agent session — cwd sandbox, system prompt, tool restrictions | — | #134 Wave-8 |
|
||||
| P7-004 | not-started | Phase 7 | E2E test suite — Playwright critical paths | — | #55 Wave-9 |
|
||||
| P7-006 | not-started | Phase 7 | Documentation — user guide, admin guide, dev guide | — | #57 Wave-9 |
|
||||
| P7-007 | not-started | Phase 7 | Bare-metal deployment docs + .env.example | — | #58 Wave-9 |
|
||||
| P7-021 | not-started | Phase 7 | Verify Phase 7 — feature-complete platform E2E | — | #132 Wave-10 |
|
||||
| P8-001 | not-started | Phase 8 | Additional SSO providers — WorkOS + Keycloak | — | #53 |
|
||||
| P8-002 | not-started | Phase 8 | Additional LLM providers — Codex, Z.ai, LM Studio, llama.cpp | — | #54 |
|
||||
| P8-003 | not-started | Phase 8 | Performance optimization | — | #56 |
|
||||
| P8-004 | not-started | Phase 8 | Beta release gate — v0.1.0 tag | — | #59 |
|
||||
| FIX-01 | done | Backlog | Call piSession.dispose() in AgentService.destroySession | #78 | #62 |
|
||||
|
||||
Reference in New Issue
Block a user