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
46 changes: 23 additions & 23 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,27 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@tailwindcss/postcss": "^4.3.3",
"@tiptap/core": "^3.30.4",
"@tiptap/extension-code-block-lowlight": "^3.30.4",
"@tiptap/extension-details": "^3.30.4",
"@tiptap/extension-document": "^3.30.4",
"@tiptap/extension-highlight": "^3.30.4",
"@tiptap/extension-image": "^3.30.4",
"@tiptap/extension-link": "^3.30.4",
"@tiptap/extension-paragraph": "^3.30.4",
"@tiptap/extension-placeholder": "^3.30.4",
"@tiptap/extension-table": "^3.30.4",
"@tiptap/extension-table-cell": "^3.30.4",
"@tiptap/extension-table-header": "^3.30.4",
"@tiptap/extension-table-row": "^3.30.4",
"@tiptap/extension-task-item": "^3.30.4",
"@tiptap/extension-task-list": "^3.30.4",
"@tiptap/extension-text": "^3.30.4",
"@tiptap/extension-underline": "^3.30.4",
"@tiptap/pm": "^3.30.4",
"@tiptap/react": "^3.30.4",
"@tiptap/starter-kit": "^3.30.4",
"@tiptap/suggestion": "^3.30.4",
"@tiptap/core": "^3.30.5",
"@tiptap/extension-code-block-lowlight": "^3.30.5",
"@tiptap/extension-details": "^3.30.5",
"@tiptap/extension-document": "^3.30.5",
"@tiptap/extension-highlight": "^3.30.5",
"@tiptap/extension-image": "^3.30.5",
"@tiptap/extension-link": "^3.30.5",
"@tiptap/extension-paragraph": "^3.30.5",
"@tiptap/extension-placeholder": "^3.30.5",
"@tiptap/extension-table": "^3.30.5",
"@tiptap/extension-table-cell": "^3.30.5",
"@tiptap/extension-table-header": "^3.30.5",
"@tiptap/extension-table-row": "^3.30.5",
"@tiptap/extension-task-item": "^3.30.5",
"@tiptap/extension-task-list": "^3.30.5",
"@tiptap/extension-text": "^3.30.5",
"@tiptap/extension-underline": "^3.30.5",
"@tiptap/pm": "^3.30.5",
"@tiptap/react": "^3.30.5",
"@tiptap/starter-kit": "^3.30.5",
"@tiptap/suggestion": "^3.30.5",
"@types/react-resizable": "^4.0.0",
"@types/turndown": "^5.0.6",
"d3-drag": "^3.0.0",
Expand All @@ -47,7 +47,7 @@
"dompurify": "^3.4.13",
"lowlight": "^3.3.0",
"lucide-react": "^1.31.0",
"next": "16.3.1",
"next": "16.3.3",
"postcss": "^8.5.26",
"react": "^19.2.8",
"react-dom": "^19.2.8",
Expand Down Expand Up @@ -75,7 +75,7 @@
"@types/react-grid-layout": "^1.3.6",
"@types/simple-peer": "^9.11.9",
"eslint": "^9.39.5",
"eslint-config-next": "16.3.1",
"eslint-config-next": "16.3.3",
"typescript": "^6.0.3"
}
}
165 changes: 67 additions & 98 deletions apps/web/src/app/(app)/settings/ai/page.tsx

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions apps/web/src/app/(app)/settings/api-access/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,21 +139,21 @@ export default function ApiAccessPage() {
return (
<div className="h-full overflow-y-auto">
<div className="p-6 max-w-[820px]">
<div className="flex items-center justify-between mb-4">
<div className="mb-4 flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
<div>
<h2
className="text-[18px] font-semibold"
style={{ color: 'var(--foreground)', fontFamily: 'var(--font-heading)' }}
>
API Access
Service API access
</h2>
<p className="text-[12px] mt-1 max-w-[560px]" style={{ color: 'var(--muted)' }}>
Create service keys for API and MCP runtimes. Personal AI apps are usually easier from MCP Access.
</p>
</div>
<button
onClick={() => setShowCreate(!showCreate)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-medium"
className="flex min-h-10 shrink-0 items-center gap-1.5 whitespace-nowrap px-3 py-1.5 rounded-lg text-[12px] font-medium"
style={{
background: 'var(--accent)',
color: 'white',
Expand All @@ -168,10 +168,10 @@ export default function ApiAccessPage() {
<div className="grid gap-3 md:grid-cols-3 mb-5">
<AccessNote
icon={Bot}
title="Personal AI apps"
body="Use MCP Access for Claude, Codex, ChatGPT, and human-owned tokens."
title="Personal AI connections"
body="Connect Claude, Codex, or ChatGPT with tokens and grants that act as you."
href="/settings/mcp-access"
cta="Open MCP Access"
cta="Manage personal connections"
/>
<AccessNote
icon={Server}
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/app/(app)/settings/apps/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { notFound } from 'next/navigation';
import { APPS_ENABLED } from '@/lib/feature-flags';
import { AppsClient } from '../apps-client';

export default async function AppSettingsPage({ params }: { params: Promise<{ id: string }> }) {
if (!APPS_ENABLED) notFound();
const { id } = await params;
return <AppsClient selectedId={id} />;
}
33 changes: 28 additions & 5 deletions apps/web/src/app/(app)/settings/apps/apps-client.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useRef, useState, type ChangeEvent } from 'react';
import Link from 'next/link';
import { AppWindow, Check, Copy, FileUp, KeyRound, Loader2, Power, ShieldCheck, X } from 'lucide-react';
import { PageHeader } from '@/components/page-header';
import { EmptyState } from '@/components/empty-state';
Expand All @@ -11,7 +12,7 @@ import { useAuth } from '@/lib/auth-context';
import { APP_PACKAGE_MAX_BYTES, appApiError, isConnectedAppManifest, normalizeAppInspection, type AppInspection, type AppInstallation } from '@/lib/apps';
import { refreshApps, useAppRealtime, useApps } from '@/hooks/use-apps';

export function AppsClient() {
export function AppsClient({ selectedId }: { selectedId?: string } = {}) {
const { user } = useAuth();
const { apps, isLoading, error, mutate } = useApps();
useAppRealtime();
Expand All @@ -26,6 +27,9 @@ export function AppsClient() {
const [busy, setBusy] = useState<string | null>(null);
const [message, setMessage] = useState<{ tone: 'error' | 'success'; text: string } | null>(null);
const [pairing, setPairing] = useState<{ code: string; expires_at: string } | null>(null);
const [query, setQuery] = useState('');
const selectedApp = apps.find((app) => app.id === selectedId);
const visibleApps = apps.filter((app) => `${app.name} ${app.app_id}`.toLowerCase().includes(query.trim().toLowerCase()));
const canManage = user?.role === 'owner' || user?.role === 'admin';

const choosePackage = (upgradeTarget: AppInstallation | null = null) => {
Expand Down Expand Up @@ -110,10 +114,13 @@ export function AppsClient() {
};

return <div className="flex h-full min-h-0 flex-1 flex-col overflow-hidden">
<PageHeader title="Apps" description="Expand this workspace with local, declarative App packages." compact secondary={canManage ? <div className="flex gap-2 px-1">
<PageHeader title="Apps" description="Manage installed Apps, their access, and activity." compact secondary={canManage ? <details className="px-1">
<summary className="cursor-pointer py-2 text-sm font-medium">Add or build an App</summary>
<div className="flex flex-wrap gap-2 py-2">
<button type="button" onClick={() => void createPairing()} disabled={busy !== null} className="deft-pill min-h-11"><KeyRound size={14} /> Pair Codex</button>
<button type="button" onClick={() => choosePackage()} disabled={busy !== null} className="deft-pill min-h-11 text-white" style={{ background: 'var(--primary-container)' }}><FileUp size={14} /> Inspect package</button>
</div> : undefined} />
</div>
</details> : undefined} />
<input ref={inputRef} type="file" accept="application/json,.json" onChange={(event) => void inspect(event)} className="sr-only" aria-label="Choose a Deft App package" />
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-8 pt-3 md:px-6">
<div className="mx-auto max-w-5xl space-y-4">
Expand All @@ -125,8 +132,24 @@ export function AppsClient() {
{pending && <InspectionCard pending={pending.inspection} upgradeTarget={pending.upgradeTarget} busy={busy === 'stage'} onCancel={() => setPending(null)} onStage={() => void stage()} />}
{isLoading ? <div className="flex min-h-48 items-center justify-center"><Loader2 className="animate-spin" aria-label="Loading Apps" /></div>
: error ? <EmptyState icon={<AppWindow size={20} />} title="Apps did not load" description={error instanceof Error ? error.message : 'Try again.'} action={{ label: 'Try again', onClick: () => void mutate() }} />
: apps.length === 0 ? <EmptyState icon={<AppWindow size={20} />} title="No Apps installed" description="Build a declarative App with the public kit, then inspect its package here." action={canManage ? { label: 'Inspect a package', onClick: () => choosePackage() } : undefined} />
: <div className="grid gap-3 md:grid-cols-2">{apps.map((app) => <AppCard key={app.id} app={app} canManage={canManage} busy={busy === app.id} onActivate={() => void activate(app)} onDisable={() => void disable(app)} onChooseUpgrade={() => choosePackage(app)} />)}</div>}
: selectedId && !selectedApp ? <div className="space-y-3"><p>This App is unavailable in this workspace.</p><Link href="/settings/apps" className="deft-pill">All Apps</Link></div>
: apps.length === 0 ? <EmptyState icon={<AppWindow size={20} />} title="No Apps installed" description="Inspect a Deft App package to review what it adds to your workspace." action={canManage ? { label: 'Inspect a package', onClick: () => choosePackage() } : undefined} />
: selectedApp ? <>
<Link href="/settings/apps" className="deft-pill min-h-11">← All Apps</Link>
<AppCard key={selectedApp.id} app={selectedApp} canManage={canManage} busy={busy === selectedApp.id} onActivate={() => void activate(selectedApp)} onDisable={() => void disable(selectedApp)} onChooseUpgrade={() => choosePackage(selectedApp)} />
</> : <section aria-label="Installed Apps" className="space-y-3">
<label className="block text-xs font-medium">
Find an App
<input value={query} onChange={(event) => setQuery(event.target.value)} type="search" placeholder="Search installed Apps" className="mt-2 w-full rounded-lg border px-3 py-2 text-sm" style={{ background: 'var(--surface-container-low)', borderColor: 'var(--border)' }} />
</label>
{visibleApps.length === 0 && <p className="py-5 text-sm">No installed Apps match your search.</p>}
{visibleApps.map((app) => <Link key={app.id} href={`/settings/apps/${encodeURIComponent(app.id)}`} className="flex w-full items-center gap-3 rounded-xl border p-4 text-left" style={{ background: 'var(--surface-container-low)', borderColor: 'var(--ghost-border)' }}>
<AppWindow size={20} className="shrink-0" style={{ color: 'var(--primary)' }} />
<span className="min-w-0 flex-1"><span className="block truncate text-sm font-semibold">{app.name}</span><span className="mt-1 block text-xs" style={{ color: 'var(--on-surface-variant)' }}>v{app.version} · {app.manifest.modules.length} Module{app.manifest.modules.length === 1 ? '' : 's'}</span></span>
<span className="rounded-full px-2 py-1 text-xs capitalize" style={{ background: 'var(--surface-container-high)' }}>{app.state}</span>
<span aria-hidden="true">→</span>
</Link>)}
</section>}
</div>
</div>
</div>;
Expand Down
Loading