@agimon-ai/doompi-web 0.0.1-alpha.0
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/LICENSE +29 -0
- package/README.md +196 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/bin/serve.cjs +3 -0
- package/dist/bin/serve.cjs.map +1 -0
- package/dist/bin/serve.d.cts +1 -0
- package/dist/bin/serve.d.mts +1 -0
- package/dist/bin/serve.mjs +3 -0
- package/dist/bin/serve.mjs.map +1 -0
- package/dist/bundler.cjs +1 -0
- package/dist/bundler.d.cts +2 -0
- package/dist/bundler.d.mts +2 -0
- package/dist/bundler.mjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +1 -0
- package/dist/src/adapters/authRoutes.cjs +2 -0
- package/dist/src/adapters/authRoutes.cjs.map +1 -0
- package/dist/src/adapters/authRoutes.mjs +2 -0
- package/dist/src/adapters/authRoutes.mjs.map +1 -0
- package/dist/src/adapters/bundledServer.cjs +2 -0
- package/dist/src/adapters/bundledServer.cjs.map +1 -0
- package/dist/src/adapters/bundledServer.mjs +2 -0
- package/dist/src/adapters/bundledServer.mjs.map +1 -0
- package/dist/src/adapters/directoryListing.cjs +2 -0
- package/dist/src/adapters/directoryListing.cjs.map +1 -0
- package/dist/src/adapters/directoryListing.mjs +2 -0
- package/dist/src/adapters/directoryListing.mjs.map +1 -0
- package/dist/src/adapters/directorySearch.cjs +3 -0
- package/dist/src/adapters/directorySearch.cjs.map +1 -0
- package/dist/src/adapters/directorySearch.mjs +3 -0
- package/dist/src/adapters/directorySearch.mjs.map +1 -0
- package/dist/src/adapters/gitStatus.cjs +2 -0
- package/dist/src/adapters/gitStatus.cjs.map +1 -0
- package/dist/src/adapters/gitStatus.mjs +2 -0
- package/dist/src/adapters/gitStatus.mjs.map +1 -0
- package/dist/src/adapters/httpServer.cjs +2 -0
- package/dist/src/adapters/httpServer.cjs.map +1 -0
- package/dist/src/adapters/httpServer.d.cts +14 -0
- package/dist/src/adapters/httpServer.d.cts.map +1 -0
- package/dist/src/adapters/httpServer.d.mts +14 -0
- package/dist/src/adapters/httpServer.d.mts.map +1 -0
- package/dist/src/adapters/httpServer.mjs +2 -0
- package/dist/src/adapters/httpServer.mjs.map +1 -0
- package/dist/src/adapters/hubProbe.cjs +2 -0
- package/dist/src/adapters/hubProbe.cjs.map +1 -0
- package/dist/src/adapters/hubProbe.mjs +2 -0
- package/dist/src/adapters/hubProbe.mjs.map +1 -0
- package/dist/src/adapters/packageApiProxy.cjs +2 -0
- package/dist/src/adapters/packageApiProxy.cjs.map +1 -0
- package/dist/src/adapters/packageApiProxy.mjs +2 -0
- package/dist/src/adapters/packageApiProxy.mjs.map +1 -0
- package/dist/src/adapters/piHubService.cjs +2 -0
- package/dist/src/adapters/piHubService.cjs.map +1 -0
- package/dist/src/adapters/piHubService.mjs +2 -0
- package/dist/src/adapters/piHubService.mjs.map +1 -0
- package/dist/src/adapters/piWebSocketListener.cjs +2 -0
- package/dist/src/adapters/piWebSocketListener.cjs.map +1 -0
- package/dist/src/adapters/piWebSocketListener.mjs +2 -0
- package/dist/src/adapters/piWebSocketListener.mjs.map +1 -0
- package/dist/src/adapters/providerAuth.cjs +2 -0
- package/dist/src/adapters/providerAuth.cjs.map +1 -0
- package/dist/src/adapters/providerAuth.mjs +2 -0
- package/dist/src/adapters/providerAuth.mjs.map +1 -0
- package/dist/src/adapters/registryWatcher.cjs +2 -0
- package/dist/src/adapters/registryWatcher.cjs.map +1 -0
- package/dist/src/adapters/registryWatcher.mjs +2 -0
- package/dist/src/adapters/registryWatcher.mjs.map +1 -0
- package/dist/src/adapters/serverSpawner.cjs +2 -0
- package/dist/src/adapters/serverSpawner.cjs.map +1 -0
- package/dist/src/adapters/serverSpawner.mjs +2 -0
- package/dist/src/adapters/serverSpawner.mjs.map +1 -0
- package/dist/src/adapters/sessionFiles.cjs +3 -0
- package/dist/src/adapters/sessionFiles.cjs.map +1 -0
- package/dist/src/adapters/sessionFiles.mjs +3 -0
- package/dist/src/adapters/sessionFiles.mjs.map +1 -0
- package/dist/src/adapters/sessionHub.cjs +2 -0
- package/dist/src/adapters/sessionHub.cjs.map +1 -0
- package/dist/src/adapters/sessionHub.mjs +2 -0
- package/dist/src/adapters/sessionHub.mjs.map +1 -0
- package/dist/src/adapters/sessionSocketClient.cjs +2 -0
- package/dist/src/adapters/sessionSocketClient.cjs.map +1 -0
- package/dist/src/adapters/sessionSocketClient.mjs +2 -0
- package/dist/src/adapters/sessionSocketClient.mjs.map +1 -0
- package/dist/src/adapters/syncGuard.cjs +2 -0
- package/dist/src/adapters/syncGuard.cjs.map +1 -0
- package/dist/src/adapters/syncGuard.mjs +2 -0
- package/dist/src/adapters/syncGuard.mjs.map +1 -0
- package/dist/src/adapters/threadJournals.cjs +2 -0
- package/dist/src/adapters/threadJournals.cjs.map +1 -0
- package/dist/src/adapters/threadJournals.mjs +2 -0
- package/dist/src/adapters/threadJournals.mjs.map +1 -0
- package/dist/src/adapters/webBundler.cjs +2 -0
- package/dist/src/adapters/webBundler.cjs.map +1 -0
- package/dist/src/adapters/webBundler.d.cts +28 -0
- package/dist/src/adapters/webBundler.d.cts.map +1 -0
- package/dist/src/adapters/webBundler.d.mts +28 -0
- package/dist/src/adapters/webBundler.d.mts.map +1 -0
- package/dist/src/adapters/webBundler.mjs +2 -0
- package/dist/src/adapters/webBundler.mjs.map +1 -0
- package/dist/src/adapters/webHubPluginLoader.cjs +2 -0
- package/dist/src/adapters/webHubPluginLoader.cjs.map +1 -0
- package/dist/src/adapters/webHubPluginLoader.mjs +2 -0
- package/dist/src/adapters/webHubPluginLoader.mjs.map +1 -0
- package/dist/src/adapters/webHubPlugins.generated.cjs +2 -0
- package/dist/src/adapters/webHubPlugins.generated.cjs.map +1 -0
- package/dist/src/adapters/webHubPlugins.generated.mjs +2 -0
- package/dist/src/adapters/webHubPlugins.generated.mjs.map +1 -0
- package/dist/src/adapters/webPluginGenerate.cjs +4 -0
- package/dist/src/adapters/webPluginGenerate.cjs.map +1 -0
- package/dist/src/adapters/webPluginGenerate.mjs +4 -0
- package/dist/src/adapters/webPluginGenerate.mjs.map +1 -0
- package/dist/src/adapters/webPluginScan.cjs +2 -0
- package/dist/src/adapters/webPluginScan.cjs.map +1 -0
- package/dist/src/adapters/webPluginScan.mjs +2 -0
- package/dist/src/adapters/webPluginScan.mjs.map +1 -0
- package/dist/src/adapters/webPluginVite.cjs +2 -0
- package/dist/src/adapters/webPluginVite.cjs.map +1 -0
- package/dist/src/adapters/webPluginVite.mjs +2 -0
- package/dist/src/adapters/webPluginVite.mjs.map +1 -0
- package/dist/src/services/directoryMatch.cjs +2 -0
- package/dist/src/services/directoryMatch.cjs.map +1 -0
- package/dist/src/services/directoryMatch.mjs +2 -0
- package/dist/src/services/directoryMatch.mjs.map +1 -0
- package/dist/src/services/fileMatch.cjs +2 -0
- package/dist/src/services/fileMatch.cjs.map +1 -0
- package/dist/src/services/fileMatch.mjs +2 -0
- package/dist/src/services/fileMatch.mjs.map +1 -0
- package/dist/src/services/fileMedia.cjs +2 -0
- package/dist/src/services/fileMedia.cjs.map +1 -0
- package/dist/src/services/fileMedia.mjs +2 -0
- package/dist/src/services/fileMedia.mjs.map +1 -0
- package/dist/src/services/frameRing.cjs +2 -0
- package/dist/src/services/frameRing.cjs.map +1 -0
- package/dist/src/services/frameRing.mjs +2 -0
- package/dist/src/services/frameRing.mjs.map +1 -0
- package/dist/src/services/journalTail.cjs +3 -0
- package/dist/src/services/journalTail.cjs.map +1 -0
- package/dist/src/services/journalTail.mjs +3 -0
- package/dist/src/services/journalTail.mjs.map +1 -0
- package/dist/src/services/loginFlow.cjs +2 -0
- package/dist/src/services/loginFlow.cjs.map +1 -0
- package/dist/src/services/loginFlow.mjs +2 -0
- package/dist/src/services/loginFlow.mjs.map +1 -0
- package/dist/src/services/registryStore.cjs +2 -0
- package/dist/src/services/registryStore.cjs.map +1 -0
- package/dist/src/services/registryStore.mjs +2 -0
- package/dist/src/services/registryStore.mjs.map +1 -0
- package/dist/src/services/retryPolicy.cjs +2 -0
- package/dist/src/services/retryPolicy.cjs.map +1 -0
- package/dist/src/services/retryPolicy.mjs +2 -0
- package/dist/src/services/retryPolicy.mjs.map +1 -0
- package/dist/src/services/serveOptions.cjs +2 -0
- package/dist/src/services/serveOptions.cjs.map +1 -0
- package/dist/src/services/serveOptions.d.cts +21 -0
- package/dist/src/services/serveOptions.d.cts.map +1 -0
- package/dist/src/services/serveOptions.d.mts +21 -0
- package/dist/src/services/serveOptions.d.mts.map +1 -0
- package/dist/src/services/serveOptions.mjs +2 -0
- package/dist/src/services/serveOptions.mjs.map +1 -0
- package/dist/src/services/sessionFraming.cjs +4 -0
- package/dist/src/services/sessionFraming.cjs.map +1 -0
- package/dist/src/services/sessionFraming.mjs +4 -0
- package/dist/src/services/sessionFraming.mjs.map +1 -0
- package/dist/src/services/sessionPresence.cjs +2 -0
- package/dist/src/services/sessionPresence.cjs.map +1 -0
- package/dist/src/services/sessionPresence.mjs +2 -0
- package/dist/src/services/sessionPresence.mjs.map +1 -0
- package/dist/src/services/staticAssets.cjs +2 -0
- package/dist/src/services/staticAssets.cjs.map +1 -0
- package/dist/src/services/staticAssets.mjs +2 -0
- package/dist/src/services/staticAssets.mjs.map +1 -0
- package/dist/src/services/webDevRoots.cjs +2 -0
- package/dist/src/services/webDevRoots.cjs.map +1 -0
- package/dist/src/services/webDevRoots.mjs +2 -0
- package/dist/src/services/webDevRoots.mjs.map +1 -0
- package/dist/src/services/webPluginManifest.cjs +2 -0
- package/dist/src/services/webPluginManifest.cjs.map +1 -0
- package/dist/src/services/webPluginManifest.mjs +2 -0
- package/dist/src/services/webPluginManifest.mjs.map +1 -0
- package/dist/src/types/auth.cjs +2 -0
- package/dist/src/types/auth.cjs.map +1 -0
- package/dist/src/types/auth.d.cts +94 -0
- package/dist/src/types/auth.d.cts.map +1 -0
- package/dist/src/types/auth.d.mts +94 -0
- package/dist/src/types/auth.d.mts.map +1 -0
- package/dist/src/types/auth.mjs +2 -0
- package/dist/src/types/auth.mjs.map +1 -0
- package/dist/src/types/bridge.d.cts +31 -0
- package/dist/src/types/bridge.d.cts.map +1 -0
- package/dist/src/types/bridge.d.mts +31 -0
- package/dist/src/types/bridge.d.mts.map +1 -0
- package/dist/src/types/hub.cjs +2 -0
- package/dist/src/types/hub.cjs.map +1 -0
- package/dist/src/types/hub.d.cts +58 -0
- package/dist/src/types/hub.d.cts.map +1 -0
- package/dist/src/types/hub.d.mts +58 -0
- package/dist/src/types/hub.d.mts.map +1 -0
- package/dist/src/types/hub.mjs +2 -0
- package/dist/src/types/hub.mjs.map +1 -0
- package/dist/src/types/media.cjs +2 -0
- package/dist/src/types/media.cjs.map +1 -0
- package/dist/src/types/media.mjs +2 -0
- package/dist/src/types/media.mjs.map +1 -0
- package/dist/src/types/registry.cjs +1 -0
- package/dist/src/types/registry.d.cts +2 -0
- package/dist/src/types/registry.d.mts +2 -0
- package/dist/src/types/registry.mjs +1 -0
- package/dist/src/types/session.cjs +2 -0
- package/dist/src/types/session.cjs.map +1 -0
- package/dist/src/types/session.d.cts +24 -0
- package/dist/src/types/session.d.cts.map +1 -0
- package/dist/src/types/session.d.mts +24 -0
- package/dist/src/types/session.d.mts.map +1 -0
- package/dist/src/types/session.mjs +2 -0
- package/dist/src/types/session.mjs.map +1 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/index-Cg52ngKv.css +2 -0
- package/dist/web/assets/index-DQO7NTIP.js +90 -0
- package/dist/web/assets/index-DQO7NTIP.js.map +1 -0
- package/dist/web/favicon-16.png +0 -0
- package/dist/web/favicon-32.png +0 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/favicon.svg +16 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/index.html +17 -0
- package/llms.txt +8 -0
- package/package.json +106 -0
- package/src/adapters/authRoutes.ts +95 -0
- package/src/adapters/bundledServer.ts +64 -0
- package/src/adapters/directoryListing.ts +127 -0
- package/src/adapters/directorySearch.ts +124 -0
- package/src/adapters/gitStatus.ts +27 -0
- package/src/adapters/httpServer.ts +556 -0
- package/src/adapters/hubProbe.ts +27 -0
- package/src/adapters/packageApiProxy.ts +60 -0
- package/src/adapters/piHubService.ts +129 -0
- package/src/adapters/piWebSocketListener.ts +98 -0
- package/src/adapters/providerAuth.ts +181 -0
- package/src/adapters/registryWatcher.ts +112 -0
- package/src/adapters/serverSpawner.ts +181 -0
- package/src/adapters/sessionFiles.ts +99 -0
- package/src/adapters/sessionHub.ts +567 -0
- package/src/adapters/sessionSocketClient.ts +105 -0
- package/src/adapters/syncGuard.ts +119 -0
- package/src/adapters/threadJournals.ts +184 -0
- package/src/adapters/webBundler.ts +83 -0
- package/src/adapters/webHubPluginLoader.ts +67 -0
- package/src/adapters/webHubPlugins.generated.ts +4 -0
- package/src/adapters/webPluginGenerate.ts +172 -0
- package/src/adapters/webPluginScan.ts +62 -0
- package/src/adapters/webPluginVite.ts +51 -0
- package/src/bin/serve.ts +64 -0
- package/src/exports/bundler.ts +1 -0
- package/src/exports/index.ts +9 -0
- package/src/services/directoryMatch.ts +71 -0
- package/src/services/fileMatch.ts +37 -0
- package/src/services/fileMedia.ts +27 -0
- package/src/services/frameRing.ts +33 -0
- package/src/services/journalTail.ts +47 -0
- package/src/services/loginFlow.ts +144 -0
- package/src/services/registryStore.ts +83 -0
- package/src/services/retryPolicy.ts +15 -0
- package/src/services/serveOptions.ts +63 -0
- package/src/services/sessionFraming.ts +32 -0
- package/src/services/sessionPresence.ts +131 -0
- package/src/services/staticAssets.ts +37 -0
- package/src/services/webDevRoots.ts +36 -0
- package/src/services/webPluginManifest.ts +149 -0
- package/src/types/auth.ts +118 -0
- package/src/types/bridge.ts +41 -0
- package/src/types/hub.ts +290 -0
- package/src/types/media.ts +38 -0
- package/src/types/registry.ts +2 -0
- package/src/types/session.ts +36 -0
- package/src/web/app/Providers.tsx +46 -0
- package/src/web/app/protocolRuntime.ts +106 -0
- package/src/web/app/sessionRuntime.ts +189 -0
- package/src/web/app/webPlugins.generated.ts +4 -0
- package/src/web/components/MascotMark.tsx +46 -0
- package/src/web/components/PluginSurface.tsx +10 -0
- package/src/web/components/ToolRendererBoundary.tsx +32 -0
- package/src/web/features/activity/ActivityDock.tsx +125 -0
- package/src/web/features/connection/RefusedCard.tsx +63 -0
- package/src/web/features/dialogs/DialogOverlay.tsx +176 -0
- package/src/web/features/leader/CommandPalette.tsx +386 -0
- package/src/web/features/selection/SelectionBar.tsx +529 -0
- package/src/web/features/session/Composer.tsx +368 -0
- package/src/web/features/session/MentionPreviews.tsx +58 -0
- package/src/web/features/session/ThreadView.tsx +35 -0
- package/src/web/features/session/Timeline.tsx +382 -0
- package/src/web/features/session/ToolCard.tsx +79 -0
- package/src/web/features/sessions/NewSessionDialog.tsx +222 -0
- package/src/web/features/sessions/SessionRail.tsx +411 -0
- package/src/web/features/settings/AppearanceSettings.tsx +88 -0
- package/src/web/features/settings/LoginFlowDialog.tsx +221 -0
- package/src/web/features/settings/PluginSettings.tsx +92 -0
- package/src/web/features/settings/ProviderSettings.tsx +238 -0
- package/src/web/features/settings/SettingsMenu.tsx +32 -0
- package/src/web/features/status/TopBar.tsx +208 -0
- package/src/web/index.html +16 -0
- package/src/web/lib/authApi.ts +90 -0
- package/src/web/lib/commands.ts +61 -0
- package/src/web/lib/composition.ts +225 -0
- package/src/web/lib/fileMentions.ts +32 -0
- package/src/web/lib/hubApi.ts +86 -0
- package/src/web/lib/leaderTree.ts +159 -0
- package/src/web/lib/piTransport.ts +61 -0
- package/src/web/lib/pluginRegistry.ts +488 -0
- package/src/web/lib/pluginSlotProps.ts +50 -0
- package/src/web/lib/promptFocus.ts +25 -0
- package/src/web/lib/protocolTimeline.ts +73 -0
- package/src/web/lib/sessionModel.ts +673 -0
- package/src/web/lib/sessionSummary.ts +58 -0
- package/src/web/lib/settingsSections.ts +21 -0
- package/src/web/lib/statusLine.ts +90 -0
- package/src/web/lib/threadRenderer.ts +25 -0
- package/src/web/lib/toolMessageProps.ts +21 -0
- package/src/web/lib/transport.ts +30 -0
- package/src/web/lib/wsClient.ts +73 -0
- package/src/web/main.tsx +17 -0
- package/src/web/public/apple-touch-icon.png +0 -0
- package/src/web/public/favicon-16.png +0 -0
- package/src/web/public/favicon-32.png +0 -0
- package/src/web/public/favicon.ico +0 -0
- package/src/web/public/favicon.svg +16 -0
- package/src/web/public/icon-512.png +0 -0
- package/src/web/routes/CockpitPage.tsx +98 -0
- package/src/web/routes/SettingsPage.tsx +60 -0
- package/src/web/routes/routeTree.tsx +49 -0
- package/src/web/stores/menuStore.ts +78 -0
- package/src/web/stores/paletteStore.ts +25 -0
- package/src/web/stores/sessionStore.ts +303 -0
- package/src/web/stores/sessionsStore.ts +155 -0
- package/src/web/stores/themeStore.ts +52 -0
- package/src/web/stores/threadStore.ts +68 -0
- package/src/web/stores/transientTabsStore.ts +63 -0
- package/src/web/stores/uiStore.ts +28 -0
- package/src/web/stores/useOpenTab.ts +19 -0
- package/src/web/stores/usePluginSlotProps.ts +22 -0
- package/src/web/styles/app.css +15 -0
- package/src/web/styles/webPluginSources.generated.css +1 -0
- package/src/web/vite-env.d.ts +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogBody,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogFooter,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
Input,
|
|
10
|
+
Spinner,
|
|
11
|
+
} from '@agimon-ai/doompi-web-components';
|
|
12
|
+
import { useEffect, useState } from 'react';
|
|
13
|
+
import type { AuthMethodType, LoginEvent, LoginFlowSnapshot, LoginPromptView } from '../../../types/auth.ts';
|
|
14
|
+
|
|
15
|
+
export const METHOD_LABEL: Readonly<Record<AuthMethodType, string>> = { api_key: 'api key', oauth: 'oauth' };
|
|
16
|
+
|
|
17
|
+
/** Button's link variant plus the wrapping a full auth URL needs. */
|
|
18
|
+
const linkClass = 'inline break-all decoration-doom-blue/40 hover:decoration-doom-blue';
|
|
19
|
+
|
|
20
|
+
function FlowEvent({ event }: { event: LoginEvent }) {
|
|
21
|
+
if (event.type === 'auth_url') {
|
|
22
|
+
return (
|
|
23
|
+
<div className="flex flex-col gap-1 text-[11px] text-doom-text">
|
|
24
|
+
<span>open this link to authorize:</span>
|
|
25
|
+
<Button asChild variant="link" size="xs" className={linkClass}>
|
|
26
|
+
<a data-testid="login-flow-auth-url" href={event.url} target="_blank" rel="noreferrer">
|
|
27
|
+
{event.url}
|
|
28
|
+
</a>
|
|
29
|
+
</Button>
|
|
30
|
+
{event.instructions ? <span className="text-doom-dim">{event.instructions}</span> : null}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (event.type === 'device_code') {
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex flex-col gap-1 text-[11px] text-doom-text">
|
|
37
|
+
<span>
|
|
38
|
+
enter code{' '}
|
|
39
|
+
<span data-testid="login-flow-device-code" className="font-bold text-doom-hi">
|
|
40
|
+
{event.userCode}
|
|
41
|
+
</span>{' '}
|
|
42
|
+
at{' '}
|
|
43
|
+
<a href={event.verificationUri} target="_blank" rel="noreferrer" className={linkClass}>
|
|
44
|
+
{event.verificationUri}
|
|
45
|
+
</a>
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (event.type === 'info') {
|
|
51
|
+
return (
|
|
52
|
+
<div className="flex flex-col gap-1 text-[11px] text-doom-text">
|
|
53
|
+
<span>{event.message}</span>
|
|
54
|
+
{event.links?.map((link) => (
|
|
55
|
+
<a key={link.url} href={link.url} target="_blank" rel="noreferrer" className={linkClass}>
|
|
56
|
+
{link.label ?? link.url}
|
|
57
|
+
</a>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return <span className="text-[11px] text-doom-dim">{event.message}</span>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function PromptField({
|
|
66
|
+
prompt,
|
|
67
|
+
value,
|
|
68
|
+
onChange,
|
|
69
|
+
onSubmit,
|
|
70
|
+
onSelect,
|
|
71
|
+
}: {
|
|
72
|
+
prompt: LoginPromptView;
|
|
73
|
+
value: string;
|
|
74
|
+
onChange: (value: string) => void;
|
|
75
|
+
onSubmit: () => void;
|
|
76
|
+
onSelect: (optionId: string) => void;
|
|
77
|
+
}) {
|
|
78
|
+
if (prompt.type === 'select') {
|
|
79
|
+
return (
|
|
80
|
+
<div data-testid="login-prompt" data-prompt-type={prompt.type} className="flex flex-col gap-2">
|
|
81
|
+
<span className="text-[11px] text-doom-text">{prompt.message}</span>
|
|
82
|
+
<div className="flex flex-col gap-1">
|
|
83
|
+
{(prompt.options ?? []).map((option) => (
|
|
84
|
+
<Button
|
|
85
|
+
key={option.id}
|
|
86
|
+
variant="outline"
|
|
87
|
+
data-testid={`login-prompt-option-${option.id}`}
|
|
88
|
+
onClick={() => onSelect(option.id)}
|
|
89
|
+
className="h-auto flex-col items-start gap-0.5 px-2.5 py-1.5 text-left whitespace-normal focus-visible:border-doom-blue/50 focus-visible:ring-0"
|
|
90
|
+
>
|
|
91
|
+
<span className="text-[11px] text-doom-hi">{option.label}</span>
|
|
92
|
+
{option.description ? <span className="text-[10px] text-doom-faint">{option.description}</span> : null}
|
|
93
|
+
</Button>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return (
|
|
100
|
+
<label data-testid="login-prompt" data-prompt-type={prompt.type} className="flex flex-col gap-1">
|
|
101
|
+
<span className="text-[11px] text-doom-text">{prompt.message}</span>
|
|
102
|
+
<div className="flex items-center gap-2">
|
|
103
|
+
<Input
|
|
104
|
+
data-testid="login-prompt-input"
|
|
105
|
+
type={prompt.type === 'secret' ? 'password' : 'text'}
|
|
106
|
+
value={value}
|
|
107
|
+
autoFocus
|
|
108
|
+
autoComplete="off"
|
|
109
|
+
placeholder={prompt.placeholder}
|
|
110
|
+
onChange={(event) => onChange(event.target.value)}
|
|
111
|
+
onKeyDown={(event) => {
|
|
112
|
+
if (event.key === 'Enter') onSubmit();
|
|
113
|
+
}}
|
|
114
|
+
className="flex-1"
|
|
115
|
+
/>
|
|
116
|
+
<Button variant="primary" data-testid="login-prompt-submit" disabled={value.trim() === ''} onClick={onSubmit}>
|
|
117
|
+
continue
|
|
118
|
+
</Button>
|
|
119
|
+
</div>
|
|
120
|
+
</label>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A provider's login, rendered from the hub's snapshots: the links and codes
|
|
126
|
+
* it announced, the one question it is waiting on, and how it ended.
|
|
127
|
+
*/
|
|
128
|
+
export function LoginFlowDialog({
|
|
129
|
+
flow,
|
|
130
|
+
onAnswer,
|
|
131
|
+
onCancel,
|
|
132
|
+
onClose,
|
|
133
|
+
}: {
|
|
134
|
+
flow: LoginFlowSnapshot;
|
|
135
|
+
onAnswer: (promptId: string, value: string) => void;
|
|
136
|
+
onCancel: () => void;
|
|
137
|
+
onClose: () => void;
|
|
138
|
+
}) {
|
|
139
|
+
const [value, setValue] = useState('');
|
|
140
|
+
const prompt = flow.prompt;
|
|
141
|
+
const promptId = prompt?.id;
|
|
142
|
+
const running = flow.status === 'running';
|
|
143
|
+
|
|
144
|
+
// A new question starts from an empty field.
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
setValue('');
|
|
147
|
+
}, [promptId]);
|
|
148
|
+
|
|
149
|
+
const submit = (): void => {
|
|
150
|
+
if (!prompt || value.trim() === '') return;
|
|
151
|
+
onAnswer(prompt.id, value);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
let outcome: { text: string; className: string } | undefined;
|
|
155
|
+
if (flow.status === 'succeeded')
|
|
156
|
+
outcome = { text: `authenticated with ${flow.providerName}.`, className: 'text-doom-green' };
|
|
157
|
+
else if (flow.status === 'failed') outcome = { text: flow.error ?? 'sign-in failed.', className: 'text-doom-red' };
|
|
158
|
+
else if (flow.status === 'cancelled') outcome = { text: 'sign-in cancelled.', className: 'text-doom-dim' };
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Dialog
|
|
162
|
+
open
|
|
163
|
+
onOpenChange={(next) => {
|
|
164
|
+
if (next) return;
|
|
165
|
+
// Escape or an outside click abandons a live sign-in, and simply
|
|
166
|
+
// dismisses a finished one.
|
|
167
|
+
if (running) onCancel();
|
|
168
|
+
else onClose();
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
<DialogContent width="md" data-testid="login-flow" data-status={flow.status} aria-describedby={undefined}>
|
|
172
|
+
<DialogHeader>
|
|
173
|
+
<DialogTitle>sign in to {flow.providerName}</DialogTitle>
|
|
174
|
+
<span className="text-[10px] text-doom-faint">{METHOD_LABEL[flow.type]}</span>
|
|
175
|
+
</DialogHeader>
|
|
176
|
+
<DialogBody>
|
|
177
|
+
{flow.events.length > 0 ? (
|
|
178
|
+
<ol data-testid="login-flow-events" className="flex flex-col gap-2">
|
|
179
|
+
{flow.events.map((event, index) => (
|
|
180
|
+
<li key={index}>
|
|
181
|
+
<FlowEvent event={event} />
|
|
182
|
+
</li>
|
|
183
|
+
))}
|
|
184
|
+
</ol>
|
|
185
|
+
) : null}
|
|
186
|
+
{running && prompt ? (
|
|
187
|
+
<PromptField
|
|
188
|
+
prompt={prompt}
|
|
189
|
+
value={value}
|
|
190
|
+
onChange={setValue}
|
|
191
|
+
onSubmit={submit}
|
|
192
|
+
onSelect={(optionId) => onAnswer(prompt.id, optionId)}
|
|
193
|
+
/>
|
|
194
|
+
) : null}
|
|
195
|
+
{running && !prompt ? (
|
|
196
|
+
<p data-testid="login-flow-waiting" className="flex items-center gap-2 text-[11px] text-doom-dim">
|
|
197
|
+
<Spinner />
|
|
198
|
+
{flow.events.length === 0 ? 'starting…' : `waiting for ${flow.providerName}…`}
|
|
199
|
+
</p>
|
|
200
|
+
) : null}
|
|
201
|
+
{outcome ? (
|
|
202
|
+
<p data-testid="login-flow-result" className={`text-[11px] ${outcome.className}`}>
|
|
203
|
+
{outcome.text}
|
|
204
|
+
</p>
|
|
205
|
+
) : null}
|
|
206
|
+
<DialogFooter>
|
|
207
|
+
{running ? (
|
|
208
|
+
<Button variant="outline" data-testid="login-flow-cancel" onClick={onCancel}>
|
|
209
|
+
cancel
|
|
210
|
+
</Button>
|
|
211
|
+
) : (
|
|
212
|
+
<Button variant="outline" data-testid="login-flow-close" autoFocus onClick={onClose}>
|
|
213
|
+
close
|
|
214
|
+
</Button>
|
|
215
|
+
)}
|
|
216
|
+
</DialogFooter>
|
|
217
|
+
</DialogBody>
|
|
218
|
+
</DialogContent>
|
|
219
|
+
</Dialog>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Panel, SectionLabel } from '@agimon-ai/doompi-web-components';
|
|
2
|
+
import type { WebPluginDefinition } from '@agimon-ai/doompi-web-contracts';
|
|
3
|
+
import { installedWebPlugins, webPluginDiagnostics } from '../../lib/pluginRegistry.ts';
|
|
4
|
+
|
|
5
|
+
/** What one plugin contributes, as counts: enough to see it landed without opening its code. */
|
|
6
|
+
function contributions(plugin: WebPluginDefinition): string[] {
|
|
7
|
+
const parts: string[] = [];
|
|
8
|
+
const count = (label: string, list: readonly unknown[] | undefined): void => {
|
|
9
|
+
if (list !== undefined && list.length > 0) parts.push(`${String(list.length)} ${label}`);
|
|
10
|
+
};
|
|
11
|
+
count('tabs', plugin.tabs);
|
|
12
|
+
count('channels', plugin.channels);
|
|
13
|
+
count('tool renderers', plugin.toolRenderers);
|
|
14
|
+
count('activity groups', plugin.activityGroups);
|
|
15
|
+
count('activity sections', plugin.activitySections);
|
|
16
|
+
count('minor modes', plugin.minorModes);
|
|
17
|
+
count('selection axes', plugin.selectionAxes);
|
|
18
|
+
count('leader bindings', plugin.leaderBindings);
|
|
19
|
+
count('slots', plugin.slots);
|
|
20
|
+
count('fills', plugin.fills);
|
|
21
|
+
return parts;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The plugins page: the web plugins this bundle carries and what the install
|
|
26
|
+
* had to resolve between them. Both are read once from the registry, which
|
|
27
|
+
* is complete before the first render; a plugin comes or goes with the next
|
|
28
|
+
* doompi sync, never at runtime.
|
|
29
|
+
*/
|
|
30
|
+
export function PluginSettings() {
|
|
31
|
+
const plugins = installedWebPlugins();
|
|
32
|
+
const diagnostics = webPluginDiagnostics();
|
|
33
|
+
return (
|
|
34
|
+
<div data-testid="settings-plugins" className="flex max-w-[640px] flex-col gap-5">
|
|
35
|
+
<div className="flex flex-col gap-2">
|
|
36
|
+
<SectionLabel>installed</SectionLabel>
|
|
37
|
+
<p className="text-[11px] leading-relaxed text-doom-faint">
|
|
38
|
+
the web plugins compiled into this bundle; doompi sync rebuilds it from the packages installed in the
|
|
39
|
+
composition.
|
|
40
|
+
</p>
|
|
41
|
+
{plugins.length === 0 ? (
|
|
42
|
+
<p data-testid="settings-plugins-empty" className="text-[11px] text-doom-dim">
|
|
43
|
+
no web plugins in this bundle. run doompi sync to bundle the installed packages.
|
|
44
|
+
</p>
|
|
45
|
+
) : (
|
|
46
|
+
<ul className="flex flex-col gap-1">
|
|
47
|
+
{plugins.map((plugin) => (
|
|
48
|
+
<Panel asChild key={plugin.id} className="flex items-baseline gap-3 bg-transparent px-3 py-2">
|
|
49
|
+
<li data-testid={`settings-plugin-${plugin.id}`}>
|
|
50
|
+
<span className="text-[12px] font-bold text-doom-hi">{plugin.id}</span>
|
|
51
|
+
<span className="min-w-0 flex-1 truncate text-[10px] text-doom-faint">
|
|
52
|
+
{contributions(plugin).join(' · ') || 'no contributions'}
|
|
53
|
+
</span>
|
|
54
|
+
</li>
|
|
55
|
+
</Panel>
|
|
56
|
+
))}
|
|
57
|
+
</ul>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div className="flex flex-col gap-2">
|
|
62
|
+
<SectionLabel>install diagnostics</SectionLabel>
|
|
63
|
+
<p className="text-[11px] leading-relaxed text-doom-faint">
|
|
64
|
+
plugins are independent, so when two want the same tab, tool, group, or key the install keeps one and records
|
|
65
|
+
the other here rather than failing the page.
|
|
66
|
+
</p>
|
|
67
|
+
{diagnostics.length === 0 ? (
|
|
68
|
+
<p data-testid="settings-plugin-diagnostics-empty" className="text-[11px] text-doom-dim">
|
|
69
|
+
nothing to resolve: no two plugins wanted the same name.
|
|
70
|
+
</p>
|
|
71
|
+
) : (
|
|
72
|
+
<ul className="flex flex-col gap-1">
|
|
73
|
+
{diagnostics.map((diagnostic, index) => (
|
|
74
|
+
<Panel
|
|
75
|
+
asChild
|
|
76
|
+
key={`${diagnostic.pluginId}:${diagnostic.kind}:${String(index)}`}
|
|
77
|
+
className="flex flex-col gap-0.5 bg-transparent px-3 py-2"
|
|
78
|
+
>
|
|
79
|
+
<li data-testid={`settings-plugin-diagnostic-${diagnostic.pluginId}`} data-kind={diagnostic.kind}>
|
|
80
|
+
<span className="text-[10px] font-bold text-doom-yellow">
|
|
81
|
+
{diagnostic.pluginId} · {diagnostic.kind}
|
|
82
|
+
</span>
|
|
83
|
+
<span className="text-[10px] leading-snug text-doom-dim">{diagnostic.message}</span>
|
|
84
|
+
</li>
|
|
85
|
+
</Panel>
|
|
86
|
+
))}
|
|
87
|
+
</ul>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Badge, Button, Dot, EmptyState, Input, Panel, Spinner } from '@agimon-ai/doompi-web-components';
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import type { AuthMethodType, LoginFlowSnapshot, ProviderAuthSummary } from '../../../types/auth.ts';
|
|
4
|
+
import { answerLogin, cancelLogin, listProviders, logoutProvider, readLogin, startLogin } from '../../lib/authApi.ts';
|
|
5
|
+
import { LoginFlowDialog, METHOD_LABEL } from './LoginFlowDialog.tsx';
|
|
6
|
+
|
|
7
|
+
const FLOW_POLL_MS = 500;
|
|
8
|
+
/** The auth source Pi reports for a credential its own /login stored; the only kind /logout removes. */
|
|
9
|
+
const STORED_SOURCE = 'stored';
|
|
10
|
+
|
|
11
|
+
function statusText(provider: ProviderAuthSummary): string {
|
|
12
|
+
if (!provider.authenticated) return 'not authenticated';
|
|
13
|
+
const { type, source } = provider.authenticated;
|
|
14
|
+
const via = source === STORED_SOURCE ? 'stored' : source;
|
|
15
|
+
return `authenticated · ${METHOD_LABEL[type]}${via ? ` · ${via}` : ''}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ProviderRow({
|
|
19
|
+
provider,
|
|
20
|
+
busy,
|
|
21
|
+
onLogin,
|
|
22
|
+
onLogout,
|
|
23
|
+
}: {
|
|
24
|
+
provider: ProviderAuthSummary;
|
|
25
|
+
busy: boolean;
|
|
26
|
+
onLogin: (type: AuthMethodType) => void;
|
|
27
|
+
onLogout: () => void;
|
|
28
|
+
}) {
|
|
29
|
+
const authenticated = provider.authenticated !== undefined;
|
|
30
|
+
return (
|
|
31
|
+
<Panel asChild className="flex items-center gap-3 px-3.5 py-2.5 transition-colors hover:border-doom-blue/30">
|
|
32
|
+
<li data-testid={`provider-${provider.id}`} data-authenticated={authenticated}>
|
|
33
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
34
|
+
<span className="truncate text-[12px] font-bold text-doom-hi">{provider.name}</span>
|
|
35
|
+
<span className="truncate text-[10px] text-doom-faint">{provider.id}</span>
|
|
36
|
+
</div>
|
|
37
|
+
<Badge tone={authenticated ? 'green' : 'neutral'} data-testid={`provider-status-${provider.id}`}>
|
|
38
|
+
<Dot tone={authenticated ? 'green' : 'neutral'} />
|
|
39
|
+
{statusText(provider)}
|
|
40
|
+
</Badge>
|
|
41
|
+
{provider.methods.map((method) => (
|
|
42
|
+
<Button
|
|
43
|
+
key={method.type}
|
|
44
|
+
variant="outline"
|
|
45
|
+
size="sm"
|
|
46
|
+
data-testid={`provider-login-${method.type}-${provider.id}`}
|
|
47
|
+
title={method.label}
|
|
48
|
+
disabled={busy}
|
|
49
|
+
onClick={() => onLogin(method.type)}
|
|
50
|
+
>
|
|
51
|
+
sign in · {METHOD_LABEL[method.type]}
|
|
52
|
+
</Button>
|
|
53
|
+
))}
|
|
54
|
+
{provider.authenticated?.source === STORED_SOURCE ? (
|
|
55
|
+
<Button
|
|
56
|
+
variant="outline"
|
|
57
|
+
size="sm"
|
|
58
|
+
data-testid={`provider-logout-${provider.id}`}
|
|
59
|
+
disabled={busy}
|
|
60
|
+
onClick={onLogout}
|
|
61
|
+
className="hover:border-doom-red/50 hover:text-doom-red"
|
|
62
|
+
>
|
|
63
|
+
sign out
|
|
64
|
+
</Button>
|
|
65
|
+
) : null}
|
|
66
|
+
{provider.methods.length === 0 ? (
|
|
67
|
+
<span className="text-[10px] text-doom-faint">ambient credentials only</span>
|
|
68
|
+
) : null}
|
|
69
|
+
</li>
|
|
70
|
+
</Panel>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The providers page: what Pi can sign in to on the hub's machine, and
|
|
76
|
+
* whether it has. A login runs on the hub; this page polls its flow and
|
|
77
|
+
* relays the one question it may ask.
|
|
78
|
+
*/
|
|
79
|
+
export function ProviderSettings() {
|
|
80
|
+
const [providers, setProviders] = useState<ProviderAuthSummary[] | null>(null);
|
|
81
|
+
const [error, setError] = useState('');
|
|
82
|
+
const [flow, setFlow] = useState<LoginFlowSnapshot | null>(null);
|
|
83
|
+
/** The provider with a request in flight, so its buttons cannot double-fire. */
|
|
84
|
+
const [busyId, setBusyId] = useState<string | null>(null);
|
|
85
|
+
const [filter, setFilter] = useState('');
|
|
86
|
+
|
|
87
|
+
const reload = useCallback(async (): Promise<void> => {
|
|
88
|
+
const result = await listProviders();
|
|
89
|
+
if ('providers' in result) {
|
|
90
|
+
setProviders(result.providers);
|
|
91
|
+
setError('');
|
|
92
|
+
} else {
|
|
93
|
+
setError(result.error);
|
|
94
|
+
}
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
void reload();
|
|
99
|
+
}, [reload]);
|
|
100
|
+
|
|
101
|
+
// Poll the running flow one request at a time; once it settles, the list
|
|
102
|
+
// is re-read so the provider's chip reflects the outcome.
|
|
103
|
+
const flowId = flow?.id;
|
|
104
|
+
const running = flow?.status === 'running';
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (flowId === undefined || !running) return;
|
|
107
|
+
let stopped = false;
|
|
108
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
109
|
+
const poll = async (): Promise<void> => {
|
|
110
|
+
const result = await readLogin(flowId);
|
|
111
|
+
if (stopped) return;
|
|
112
|
+
if ('error' in result) {
|
|
113
|
+
setFlow((current) =>
|
|
114
|
+
current && current.id === flowId ? { ...current, status: 'failed', error: result.error } : current,
|
|
115
|
+
);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
setFlow(result.flow);
|
|
119
|
+
if (result.flow.status !== 'running') {
|
|
120
|
+
void reload();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
timer = setTimeout(() => void poll(), FLOW_POLL_MS);
|
|
124
|
+
};
|
|
125
|
+
timer = setTimeout(() => void poll(), FLOW_POLL_MS);
|
|
126
|
+
return () => {
|
|
127
|
+
stopped = true;
|
|
128
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
129
|
+
};
|
|
130
|
+
}, [flowId, running, reload]);
|
|
131
|
+
|
|
132
|
+
const begin = async (providerId: string, type: AuthMethodType): Promise<void> => {
|
|
133
|
+
setBusyId(providerId);
|
|
134
|
+
const result = await startLogin(providerId, type);
|
|
135
|
+
setBusyId(null);
|
|
136
|
+
if ('flow' in result) setFlow(result.flow);
|
|
137
|
+
else setError(result.error);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const answer = async (promptId: string, value: string): Promise<void> => {
|
|
141
|
+
if (!flow) return;
|
|
142
|
+
const result = await answerLogin(flow.id, promptId, value);
|
|
143
|
+
if ('flow' in result) setFlow(result.flow);
|
|
144
|
+
else setError(result.error);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const cancel = async (): Promise<void> => {
|
|
148
|
+
if (!flow) return;
|
|
149
|
+
const result = await cancelLogin(flow.id);
|
|
150
|
+
setFlow('flow' in result ? result.flow : null);
|
|
151
|
+
void reload();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const signOut = async (providerId: string): Promise<void> => {
|
|
155
|
+
setBusyId(providerId);
|
|
156
|
+
const result = await logoutProvider(providerId);
|
|
157
|
+
if ('error' in result) setError(result.error);
|
|
158
|
+
else await reload();
|
|
159
|
+
setBusyId(null);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// The list is long enough that finding a provider by eye is work; signed-in
|
|
163
|
+
// ones lead because they are the ones a reader came to check.
|
|
164
|
+
const needle = filter.trim().toLowerCase();
|
|
165
|
+
const shown = (providers ?? [])
|
|
166
|
+
.filter((provider) => `${provider.name} ${provider.id}`.toLowerCase().includes(needle))
|
|
167
|
+
.sort((left, right) => Number(right.authenticated !== undefined) - Number(left.authenticated !== undefined));
|
|
168
|
+
const authenticatedCount = (providers ?? []).filter((provider) => provider.authenticated !== undefined).length;
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div data-testid="provider-settings" className="flex max-w-[780px] flex-col gap-4">
|
|
172
|
+
<div className="flex flex-col gap-1">
|
|
173
|
+
<h2 className="text-[13px] font-bold text-doom-hi">providers</h2>
|
|
174
|
+
<p className="text-[11px] leading-relaxed text-doom-dim">
|
|
175
|
+
sign in to the model providers Pi can use. credentials land in Pi's auth.json on this machine, so every
|
|
176
|
+
session shares them.
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
{error ? (
|
|
180
|
+
<p data-testid="provider-settings-error" className="text-[11px] leading-relaxed text-doom-red">
|
|
181
|
+
{error}
|
|
182
|
+
</p>
|
|
183
|
+
) : null}
|
|
184
|
+
{providers === null && !error ? (
|
|
185
|
+
<p className="flex items-center gap-2 text-[11px] text-doom-faint">
|
|
186
|
+
<Spinner label="reading providers" />
|
|
187
|
+
reading providers…
|
|
188
|
+
</p>
|
|
189
|
+
) : null}
|
|
190
|
+
{providers ? (
|
|
191
|
+
<>
|
|
192
|
+
<div className="flex items-center gap-3">
|
|
193
|
+
<Input
|
|
194
|
+
data-testid="provider-filter"
|
|
195
|
+
value={filter}
|
|
196
|
+
placeholder="filter providers…"
|
|
197
|
+
onChange={(event) => setFilter(event.target.value)}
|
|
198
|
+
className="flex-1"
|
|
199
|
+
/>
|
|
200
|
+
<span className="shrink-0 text-[10px] text-doom-faint">
|
|
201
|
+
{authenticatedCount} of {providers.length} signed in
|
|
202
|
+
</span>
|
|
203
|
+
</div>
|
|
204
|
+
<ul data-testid="provider-list" className="flex flex-col gap-1.5">
|
|
205
|
+
{shown.map((provider) => (
|
|
206
|
+
<ProviderRow
|
|
207
|
+
key={provider.id}
|
|
208
|
+
provider={provider}
|
|
209
|
+
busy={busyId === provider.id}
|
|
210
|
+
onLogin={(type) => void begin(provider.id, type)}
|
|
211
|
+
onLogout={() => void signOut(provider.id)}
|
|
212
|
+
/>
|
|
213
|
+
))}
|
|
214
|
+
{shown.length === 0 ? (
|
|
215
|
+
<EmptyState
|
|
216
|
+
asChild
|
|
217
|
+
data-testid="provider-no-match"
|
|
218
|
+
className="py-6"
|
|
219
|
+
title="no provider matches"
|
|
220
|
+
description={`nothing here answers to “${filter.trim()}”.`}
|
|
221
|
+
>
|
|
222
|
+
<li />
|
|
223
|
+
</EmptyState>
|
|
224
|
+
) : null}
|
|
225
|
+
</ul>
|
|
226
|
+
</>
|
|
227
|
+
) : null}
|
|
228
|
+
{flow ? (
|
|
229
|
+
<LoginFlowDialog
|
|
230
|
+
flow={flow}
|
|
231
|
+
onAnswer={(promptId, value) => void answer(promptId, value)}
|
|
232
|
+
onCancel={() => void cancel()}
|
|
233
|
+
onClose={() => setFlow(null)}
|
|
234
|
+
/>
|
|
235
|
+
) : null}
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router';
|
|
2
|
+
import { SETTINGS_SECTIONS } from '../../lib/settingsSections.ts';
|
|
3
|
+
|
|
4
|
+
export function SettingsMenu({ active }: { active: string | undefined }) {
|
|
5
|
+
return (
|
|
6
|
+
<nav
|
|
7
|
+
data-testid="settings-menu"
|
|
8
|
+
className="flex w-[220px] shrink-0 flex-col gap-0.5 border-r border-doom-border px-2.5 py-3"
|
|
9
|
+
>
|
|
10
|
+
{SETTINGS_SECTIONS.map((section) => {
|
|
11
|
+
const current = section.id === active;
|
|
12
|
+
return (
|
|
13
|
+
<Link
|
|
14
|
+
key={section.id}
|
|
15
|
+
to="/settings/$section"
|
|
16
|
+
params={{ section: section.id }}
|
|
17
|
+
data-testid={`settings-section-${section.id}`}
|
|
18
|
+
data-active={current}
|
|
19
|
+
className={`flex flex-col gap-0.5 rounded-md px-[11px] py-2 transition-colors ${
|
|
20
|
+
current ? 'bg-doom-tint-blue' : 'hover:bg-doom-panel'
|
|
21
|
+
}`}
|
|
22
|
+
>
|
|
23
|
+
<span className={`text-[12px] font-bold ${current ? 'text-doom-blue' : 'text-doom-hi'}`}>
|
|
24
|
+
{section.label}
|
|
25
|
+
</span>
|
|
26
|
+
<span className="text-[10px] leading-snug text-doom-faint">{section.detail}</span>
|
|
27
|
+
</Link>
|
|
28
|
+
);
|
|
29
|
+
})}
|
|
30
|
+
</nav>
|
|
31
|
+
);
|
|
32
|
+
}
|