From 18557352454e18d25fc37eeddc7bd8bf574c60b0 Mon Sep 17 00:00:00 2001 From: Miniputer Date: Mon, 27 Jul 2026 18:47:43 +0530 Subject: [PATCH 1/3] Auth flow refactor, project requests, and routing updates - Add server-side auth token loader (auth.server.ts) with SSR cookie forwarding - Add project-requests client for explicit/signal/existing issue dispatch - Refactor auth layout and app layout for SSR auth gating - Update auth-client and auth-provider for convex token flow - Extend project-issue primitives with request result schema - Wire projectIssues backend mutation for request handling - Update slice-one page and project-workspace hook - Adjust routes (remove unused), Caddy config, and env templates --- .env.example | 2 +- apps/web/react-router.config.ts | 4 +- .../components/slice-one/slice-one-page.tsx | 40 ++++++++---- apps/web/src/hooks/use-project-workspace.ts | 63 ++++++++++-------- apps/web/src/index.css | 13 ++++ apps/web/src/lib/auth.server.ts | 64 +++++++++++++++++++ apps/web/src/lib/projects/project-requests.ts | 62 ++++++++++++++++++ apps/web/src/root.tsx | 35 +++++++++- apps/web/src/routes.ts | 2 - apps/web/src/routes/app/layout.tsx | 33 +++------- apps/web/src/routes/auth/layout.tsx | 24 +++---- apps/web/src/routes/auth/login/page.tsx | 12 +++- deploy/zopu-runtime/.env.template | 5 +- deploy/zopu-runtime/caddy/Caddyfile | 14 +++- packages/agents/src/agents/project-manager.ts | 3 - packages/agents/src/project-request.ts | 13 ++++ packages/auth/src/web/auth-client.ts | 6 +- packages/auth/src/web/auth-provider.tsx | 14 +++- packages/backend/convex/auth.ts | 3 +- packages/backend/convex/projectIssues.ts | 12 ++++ packages/env/src/vite-env.d.ts | 2 +- packages/env/src/web.ts | 3 +- packages/primitives/src/project-issue.test.ts | 11 ++++ packages/primitives/src/project-issue.ts | 8 ++- 24 files changed, 340 insertions(+), 108 deletions(-) create mode 100644 apps/web/src/lib/auth.server.ts create mode 100644 apps/web/src/lib/projects/project-requests.ts diff --git a/.env.example b/.env.example index ca400ee..873e048 100644 --- a/.env.example +++ b/.env.example @@ -6,8 +6,8 @@ SITE_URL=http://localhost:5173 NATIVE_APP_URL=code:// # Browser and native public endpoints +VITE_AUTH_URL=http://localhost:5173 VITE_CONVEX_URL=https://example.convex.cloud -VITE_CONVEX_SITE_URL=https://example.convex.site # For phone testing, replace localhost with this machine's Tailscale IPv4 address. VITE_FLUE_URL=http://localhost:3583 EXPO_PUBLIC_CONVEX_URL=https://example.convex.cloud diff --git a/apps/web/react-router.config.ts b/apps/web/react-router.config.ts index 4539207..72df924 100644 --- a/apps/web/react-router.config.ts +++ b/apps/web/react-router.config.ts @@ -1,7 +1,7 @@ import type { Config } from "@react-router/dev/config"; export default { - // Desktop addons package static web assets; SSR output cannot be bundled - ssr: false, appDirectory: "src", + + ssr: true, } satisfies Config; diff --git a/apps/web/src/components/slice-one/slice-one-page.tsx b/apps/web/src/components/slice-one/slice-one-page.tsx index 4388e6c..8792fe0 100644 --- a/apps/web/src/components/slice-one/slice-one-page.tsx +++ b/apps/web/src/components/slice-one/slice-one-page.tsx @@ -90,6 +90,30 @@ const WorkCard = ({ onSourceSelect, work }: WorkCardProps) => { ); }; +const ConversationLoading = () => ( + + + + + Loading conversation… + +); + +const ConversationEmptyState = () => ( +
+
+ +

What should move forward?

+

+ Describe an outcome or problem. Casual conversation stays conversation. +

+
+
+); + export const SliceOnePage = () => { const slice = useSliceOne(); const viewportStyle = useVisualViewportStyle(); @@ -233,19 +257,11 @@ export const SliceOnePage = () => { + {!slice.agent.historyReady && timeline.length === 0 ? ( + + ) : null} {slice.agent.historyReady && timeline.length === 0 ? ( -
-
- -

- What should move forward? -

-

- Describe an outcome or problem. Casual conversation stays - conversation. -

-
-
+ ) : null} {timeline.map((item) => { if (item.kind === "work") { diff --git a/apps/web/src/hooks/use-project-workspace.ts b/apps/web/src/hooks/use-project-workspace.ts index 8e0c7b1..0d172c5 100644 --- a/apps/web/src/hooks/use-project-workspace.ts +++ b/apps/web/src/hooks/use-project-workspace.ts @@ -1,19 +1,20 @@ +import { useConvexAccessToken } from "@code/auth/web"; import { api } from "@code/backend/convex/_generated/api"; import type { Id } from "@code/backend/convex/_generated/dataModel"; -import { useFlueClient } from "@flue/react"; -import { useAction, useMutation, useQuery } from "convex/react"; +import { useAction, useQuery } from "convex/react"; import { useState } from "react"; import { buildProjectLoopView, summarizeProjectIssues, } from "@/lib/projects/project-evidence"; +import { requestProjectIssue } from "@/lib/projects/project-requests"; const errorMessage = (error: unknown) => error instanceof Error ? error.message : String(error); export const useProjectWorkspace = () => { - const flueClient = useFlueClient(); + const resolveAccessToken = useConvexAccessToken(); const projects = useQuery(api.projects.list); const [selectedProjectId, setSelectedProjectId] = useState | null>(null); @@ -25,10 +26,7 @@ export const useProjectWorkspace = () => { const [pendingAction, setPendingAction] = useState(null); const [error, setError] = useState(null); const importPublicGit = useAction(api.projects.importPublicGit); - const createIssue = useMutation(api.projectIssues.create); - const createIssueFromSignal = useMutation(api.projectIssues.createFromSignal); - const beginIssue = useMutation(api.projectIssues.begin); - const markDispatchFailed = useMutation(api.projectIssues.markDispatchFailed); + const activeProjectId = selectedProjectId ?? (projects?.[0]?.id as unknown as Id<"projects">) ?? @@ -92,12 +90,20 @@ export const useProjectWorkspace = () => { setPendingAction("issue"); setError(null); try { - const issueId = await createIssue({ - body: nextBody, - projectId: activeProjectId, - title: nextTitle, + const accessToken = await resolveAccessToken(); + if (!accessToken) { + throw new Error("Authentication required"); + } + const outcome = await requestProjectIssue({ + accessToken, + request: { + body: nextBody, + kind: "explicit", + projectId: activeProjectId, + title: nextTitle, + }, }); - setSelectedIssueId(issueId); + setSelectedIssueId(outcome.issueId as Id<"projectIssues">); setIssueTitle(""); setIssueBody(""); } catch (caughtError) { @@ -111,8 +117,15 @@ export const useProjectWorkspace = () => { setPendingAction(`signal:${signalId}`); setError(null); try { - const outcome = await createIssueFromSignal({ signalId }); - setSelectedIssueId(outcome.issueId); + const accessToken = await resolveAccessToken(); + if (!accessToken) { + throw new Error("Authentication required"); + } + const outcome = await requestProjectIssue({ + accessToken, + request: { kind: "signal", signalId }, + }); + setSelectedIssueId(outcome.issueId as Id<"projectIssues">); } catch (caughtError) { setError(errorMessage(caughtError)); } finally { @@ -120,23 +133,21 @@ export const useProjectWorkspace = () => { } }; - const startIssue = async ( - issueId: Id<"projectIssues">, - issueNumber: number, - title: string - ) => { + const startIssue = async (issueId: Id<"projectIssues">) => { const actionKey = `issue:${issueId}`; setPendingAction(actionKey); setError(null); try { - await beginIssue({ issueId }); - await flueClient.agents.send("project-manager", String(issueId), { - message: `Start project issue ${issueNumber}: ${title}. Read the bound context and complete the workflow.`, + const accessToken = await resolveAccessToken(); + if (!accessToken) { + throw new Error("Authentication required"); + } + await requestProjectIssue({ + accessToken, + request: { issueId, kind: "existing" }, }); } catch (caughtError) { - const message = errorMessage(caughtError); - await markDispatchFailed({ error: message, issueId }); - setError(message); + setError(errorMessage(caughtError)); } finally { setPendingAction(null); } @@ -148,7 +159,7 @@ export const useProjectWorkspace = () => { setError("That work unit is no longer available"); return; } - await startIssue(issue._id, issue.number, issue.title); + await startIssue(issue._id); }; const selectedProject = projects?.find( diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 89315e1..e6f7fa7 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -75,6 +75,19 @@ html.slice-one-viewport-lock body { min-height: 1.75rem; } +.route-progress-bar { + animation: route-progress 900ms ease-in-out infinite; +} + +@keyframes route-progress { + from { + transform: translateX(-100%); + } + to { + transform: translateX(400%); + } +} + @keyframes response-dot { 0%, 60%, diff --git a/apps/web/src/lib/auth.server.ts b/apps/web/src/lib/auth.server.ts new file mode 100644 index 0000000..c18079b --- /dev/null +++ b/apps/web/src/lib/auth.server.ts @@ -0,0 +1,64 @@ +import { env } from "@code/env/web"; +import { redirect } from "react-router"; + +interface AuthLoaderData { + readonly token: string | null; +} + +const tokenUrl = new URL("/api/auth/convex/token", env.VITE_AUTH_URL); + +export const loadAuthToken = async ( + request: Request +): Promise => { + const cookie = request.headers.get("cookie"); + if (!cookie) { + return { token: null }; + } + + const headers = new Headers({ cookie }); + headers.set("host", tokenUrl.host); + const response = await fetch(tokenUrl, { headers }); + if (response.status === 401) { + return { token: null }; + } + if (!response.ok) { + throw new Response("Authentication service unavailable", { status: 503 }); + } + + const payload: unknown = await response.json(); + if ( + typeof payload !== "object" || + payload === null || + !("token" in payload) || + typeof payload.token !== "string" + ) { + throw new Response("Authentication service returned an invalid response", { + status: 502, + }); + } + return { token: payload.token }; +}; + +export const requireAuthToken = async ( + request: Request +): Promise => { + const auth = await loadAuthToken(request); + if (!auth.token) { + const requestUrl = new URL(request.url); + const returnTo = `${requestUrl.pathname}${requestUrl.search}`; + throw redirect(`/login?returnTo=${encodeURIComponent(returnTo)}`); + } + return auth; +}; + +export const redirectAuthenticated = async ( + request: Request +): Promise => { + const auth = await loadAuthToken(request); + if (auth.token) { + const requestUrl = new URL(request.url); + const returnTo = requestUrl.searchParams.get("returnTo"); + throw redirect(returnTo?.startsWith("/") ? returnTo : "/"); + } + return auth; +}; diff --git a/apps/web/src/lib/projects/project-requests.ts b/apps/web/src/lib/projects/project-requests.ts new file mode 100644 index 0000000..788d7ed --- /dev/null +++ b/apps/web/src/lib/projects/project-requests.ts @@ -0,0 +1,62 @@ +import type { Id } from "@code/backend/convex/_generated/dataModel"; +import { env } from "@code/env/web"; +import { ProjectIssueRequestResult } from '@code/primitives/project-issue'; +import type { ProjectIssueRequestResult as ProjectIssueRequestResultValue } from '@code/primitives/project-issue'; +import { Schema } from "effect"; + +const projectRequestsUrl = new URL( + "project-requests", + `${env.VITE_FLUE_URL.replace(/\/+$/u, "")}/` +); + +interface RequestProjectIssueOptions { + readonly accessToken: string; + readonly request: + | { + readonly issueId: Id<"projectIssues">; + readonly kind: "existing"; + } + | { + readonly body: string; + readonly kind: "explicit"; + readonly projectId: Id<"projects">; + readonly title: string; + } + | { + readonly kind: "signal"; + readonly signalId: Id<"signals">; + }; +} + +const responseError = async (response: Response): Promise => { + const payload: unknown = await response.json().catch(() => null); + if ( + typeof payload === "object" && + payload !== null && + "error" in payload && + typeof payload.error === "string" + ) { + return new Error(payload.error); + } + return new Error(`Project request failed (${response.status})`); +}; + +export const requestProjectIssue = async ({ + accessToken, + request, +}: RequestProjectIssueOptions): Promise => { + const response = await fetch(projectRequestsUrl, { + body: JSON.stringify(request), + headers: { + authorization: `Bearer ${accessToken}`, + "content-type": "application/json", + }, + method: "POST", + }); + if (!response.ok) { + throw await responseError(response); + } + return Schema.decodeUnknownSync(ProjectIssueRequestResult)( + await response.json() + ); +}; diff --git a/apps/web/src/root.tsx b/apps/web/src/root.tsx index ee82462..91681f0 100644 --- a/apps/web/src/root.tsx +++ b/apps/web/src/root.tsx @@ -2,9 +2,10 @@ import { useConvexAccessToken, WebAuthProvider } from "@code/auth/web"; import { env } from "@code/env/web"; import { Toaster } from "@code/ui/components/sonner"; import { FlueProvider } from "@flue/react"; +import { createFlueClient } from "@flue/sdk"; import "./index.css"; -import { createFlueClient } from "@flue/sdk"; +import { LoaderCircle } from "lucide-react"; import { useMemo } from "react"; import { isRouteErrorResponse, @@ -13,12 +14,16 @@ import { Outlet, Scripts, ScrollRestoration, + useNavigation, } from "react-router"; import type { Route } from "./+types/root"; import { ThemeProvider } from "./components/theme-provider"; +import { loadAuthToken } from "./lib/auth.server"; import { createFlueFetch } from "./lib/flue-transport"; +export const loader = ({ request }: Route.LoaderArgs) => loadAuthToken(request); + export const links: Route.LinksFunction = () => [ { href: "https://fonts.googleapis.com", rel: "preconnect" }, { @@ -79,8 +84,31 @@ export const Layout = ({ children }: { children: React.ReactNode }) => ( ); -const App = () => ( - +const RouteProgress = () => { + const navigation = useNavigation(); + if (!navigation.location) { + return null; + } + return ( + +
+ + ); +}; + +export const HydrateFallback = () => ( +
+
+ Loading Zopu… +
+
+); + +const App = ({ loaderData }: Route.ComponentProps) => ( + ( disableTransitionOnChange storageKey="vite-ui-theme" > + diff --git a/apps/web/src/routes.ts b/apps/web/src/routes.ts index 40a690a..d4e3976 100644 --- a/apps/web/src/routes.ts +++ b/apps/web/src/routes.ts @@ -2,8 +2,6 @@ import { index, layout, route } from "@react-router/dev/routes"; import type { RouteConfig } from "@react-router/dev/routes"; export default [ - // Standalone chat page — no auth, mobile-first, talks to the zopu server. - route("chat", "./routes/standalone/chat/page.tsx"), layout("./routes/auth/layout.tsx", [ route("login", "./routes/auth/login/page.tsx"), route("signup", "./routes/auth/signup/page.tsx"), diff --git a/apps/web/src/routes/app/layout.tsx b/apps/web/src/routes/app/layout.tsx index 83e3a33..a3a434e 100644 --- a/apps/web/src/routes/app/layout.tsx +++ b/apps/web/src/routes/app/layout.tsx @@ -1,29 +1,12 @@ -import { useWebAuth } from "@code/auth/web"; -import { Navigate, Outlet, useLocation } from "react-router"; +import { Outlet } from "react-router"; + +import { requireAuthToken } from "@/lib/auth.server"; + +import type { Route } from "./+types/layout"; + +export const loader = ({ request }: Route.LoaderArgs) => + requireAuthToken(request); export default function AppLayout() { - const auth = useWebAuth(); - const location = useLocation(); - - if (auth.status === "loading") { - return ( -
- Loading… -
- ); - } - - if (auth.status === "inconsistent") { - return ( -
- Your session could not be verified -
- ); - } - - if (auth.status === "unauthenticated") { - return ; - } - return ; } diff --git a/apps/web/src/routes/auth/layout.tsx b/apps/web/src/routes/auth/layout.tsx index 8498887..dff1f20 100644 --- a/apps/web/src/routes/auth/layout.tsx +++ b/apps/web/src/routes/auth/layout.tsx @@ -1,21 +1,13 @@ -import { useWebAuth } from "@code/auth/web"; -import { Navigate, Outlet } from "react-router"; +import { Outlet } from "react-router"; + +import { redirectAuthenticated } from "@/lib/auth.server"; + +import type { Route } from "./+types/layout"; + +export const loader = ({ request }: Route.LoaderArgs) => + redirectAuthenticated(request); export default function AuthLayout() { - const auth = useWebAuth(); - - if (auth.status === "loading") { - return ( -
- Loading… -
- ); - } - - if (auth.status === "authenticated") { - return ; - } - return (
diff --git a/apps/web/src/routes/auth/login/page.tsx b/apps/web/src/routes/auth/login/page.tsx index 167dfcc..a658dca 100644 --- a/apps/web/src/routes/auth/login/page.tsx +++ b/apps/web/src/routes/auth/login/page.tsx @@ -1,5 +1,5 @@ import { LoginForm } from "@code/auth/web"; -import { useNavigate } from "react-router"; +import { useNavigate, useSearchParams } from "react-router"; import type { Route } from "./+types/page"; @@ -10,6 +10,14 @@ export const meta = (_args: Route.MetaArgs) => [ export default function LoginRoute() { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const returnTo = searchParams.get("returnTo"); - return navigate("/", { replace: true })} />; + return ( + + navigate(returnTo?.startsWith("/") ? returnTo : "/", { replace: true }) + } + /> + ); } diff --git a/deploy/zopu-runtime/.env.template b/deploy/zopu-runtime/.env.template index 48ce716..27cb39e 100644 --- a/deploy/zopu-runtime/.env.template +++ b/deploy/zopu-runtime/.env.template @@ -12,10 +12,11 @@ # --------------------------------------------------------------------------- CONVEX_URL=https://your-deployment.convex.cloud CONVEX_SITE_URL=https://your-deployment.convex.site -SITE_URL=http://localhost:5173 +SITE_URL=http://localhost:13100 +VITE_AUTH_URL=http://localhost:13100 VITE_CONVEX_URL=https://your-deployment.convex.cloud -VITE_CONVEX_SITE_URL=https://your-deployment.convex.site VITE_FLUE_URL=http://localhost:3583 +VITE_ZOPU_SERVER_URL=http://localhost:3590 # Self-hosted Convex origins used by convex/docker-compose.yml CONVEX_CLOUD_ORIGIN=https://your-deployment.convex.cloud diff --git a/deploy/zopu-runtime/caddy/Caddyfile b/deploy/zopu-runtime/caddy/Caddyfile index 42aa6b3..152573e 100644 --- a/deploy/zopu-runtime/caddy/Caddyfile +++ b/deploy/zopu-runtime/caddy/Caddyfile @@ -1,13 +1,21 @@ # Caddyfile — Public reverse proxy for the Zopu web + API. # -# The web frontend (port 5173) is served at the root, and the Flue agent/API -# (port 3585) is mounted under /api so the browser talks same-origin. +# The web frontend (port 5173) is served at the root, Better Auth is proxied +# first-party under /api/auth, and Flue is mounted under /api/flue. zopu.cheaptricks.puter.wtf { bind 135.181.82.179 2a01:4f9:c013:4a64::1 encode zstd gzip - handle_path /api/* { + handle /api/auth/* { + reverse_proxy https://befitting-dalmatian-161.convex.site { + header_up Host befitting-dalmatian-161.convex.site + header_up X-Forwarded-Host {host} + header_up X-Forwarded-Proto {scheme} + } + } + + handle_path /api/flue/* { reverse_proxy 127.0.0.1:3585 } diff --git a/packages/agents/src/agents/project-manager.ts b/packages/agents/src/agents/project-manager.ts index e27008c..28f9cc7 100644 --- a/packages/agents/src/agents/project-manager.ts +++ b/packages/agents/src/agents/project-manager.ts @@ -1,6 +1,5 @@ import { parseAgentEnv } from "@code/env/agent"; import { defineAgent } from "@flue/runtime"; -import type { AgentRouteHandler } from "@flue/runtime"; import { createFinalizeGiteaLifecycle } from "../actions/finalize-gitea-lifecycle"; import { agentOs } from "../sandboxes/agent-os"; @@ -9,8 +8,6 @@ import { createProjectTools } from "../tools/project"; export const description = "Works one repository issue inside an isolated AgentOS workspace."; -export const route: AgentRouteHandler = (_context, next) => next(); - export default defineAgent(({ env, id }) => { const { AGENT_MODEL_NAME, AGENT_MODEL_PROVIDER } = parseAgentEnv(env); diff --git a/packages/agents/src/project-request.ts b/packages/agents/src/project-request.ts index 2ad4a17..f190889 100644 --- a/packages/agents/src/project-request.ts +++ b/packages/agents/src/project-request.ts @@ -31,6 +31,11 @@ const createIssueFromSignal = makeFunctionReference< { readonly signalId: string }, { readonly issueId: string; readonly projectId: string } >("projectIssues:createFromSignal"); +const getIssue = makeFunctionReference< + "query", + { readonly issueId: string }, + { readonly issueId: string; readonly projectId: string } | null +>("projectIssues:getForDispatch"); const beginIssue = makeFunctionReference< "mutation", @@ -75,6 +80,14 @@ const createIssueForRequest = async ( client: ConvexHttpClient, request: Awaited> ): Promise<{ readonly issueId: string; readonly projectId: string }> => { + if (request.kind === "existing") { + const issue = await client.query(getIssue, { issueId: request.issueId }); + if (!issue) { + throw new Error("Project issue not found"); + } + return issue; + } + if (request.kind === "signal") { return client.mutation(createIssueFromSignal, { signalId: request.signalId, diff --git a/packages/auth/src/web/auth-client.ts b/packages/auth/src/web/auth-client.ts index aed599f..c8f3554 100644 --- a/packages/auth/src/web/auth-client.ts +++ b/packages/auth/src/web/auth-client.ts @@ -1,8 +1,8 @@ import { env } from "@code/env/web"; -import { convexClient, crossDomainClient } from "@convex-dev/better-auth/client/plugins"; +import { convexClient } from "@convex-dev/better-auth/client/plugins"; import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient({ - baseURL: env.VITE_CONVEX_SITE_URL, - plugins: [convexClient(), crossDomainClient()], + baseURL: env.VITE_AUTH_URL, + plugins: [convexClient()], }); diff --git a/packages/auth/src/web/auth-provider.tsx b/packages/auth/src/web/auth-provider.tsx index 457ee24..a7ddca4 100644 --- a/packages/auth/src/web/auth-provider.tsx +++ b/packages/auth/src/web/auth-provider.tsx @@ -7,8 +7,18 @@ import { authClient } from "./auth-client"; const convex = new ConvexReactClient(env.VITE_CONVEX_URL, { expectAuth: true }); -export const WebAuthProvider = ({ children }: { children: ReactNode }) => ( - +export const WebAuthProvider = ({ + children, + initialToken, +}: { + readonly children: ReactNode; + readonly initialToken?: string | null; +}) => ( + {children} ); diff --git a/packages/backend/convex/auth.ts b/packages/backend/convex/auth.ts index bd2829b..48fa16f 100644 --- a/packages/backend/convex/auth.ts +++ b/packages/backend/convex/auth.ts @@ -2,7 +2,7 @@ import { expo } from "@better-auth/expo"; import { env } from "@code/env/convex"; import { createClient } from "@convex-dev/better-auth"; import type { GenericCtx } from "@convex-dev/better-auth"; -import { convex, crossDomain } from "@convex-dev/better-auth/plugins"; +import { convex } from "@convex-dev/better-auth/plugins"; import { betterAuth } from "better-auth/minimal"; import { components } from "./_generated/api"; @@ -25,7 +25,6 @@ const createAuth = (ctx: GenericCtx) => }, plugins: [ expo(), - crossDomain({ siteUrl }), convex({ authConfig, jwksRotateOnTokenGenerationError: true, diff --git a/packages/backend/convex/projectIssues.ts b/packages/backend/convex/projectIssues.ts index 89d110e..15ca5d8 100644 --- a/packages/backend/convex/projectIssues.ts +++ b/packages/backend/convex/projectIssues.ts @@ -118,6 +118,18 @@ export const createFromSignal = mutation({ }, }); +export const getForDispatch = query({ + args: { issueId: v.id("projectIssues") }, + handler: async (ctx, args) => { + const issue = await ctx.db.get("projectIssues", args.issueId); + if (!issue) { + return null; + } + await requireProjectMember(ctx, issue.projectId); + return { issueId: issue._id, projectId: issue.projectId }; + }, +}); + export const begin = mutation({ args: { issueId: v.id("projectIssues") }, handler: async (ctx, args) => { diff --git a/packages/env/src/vite-env.d.ts b/packages/env/src/vite-env.d.ts index b4c4fe5..243576c 100644 --- a/packages/env/src/vite-env.d.ts +++ b/packages/env/src/vite-env.d.ts @@ -1,6 +1,6 @@ interface ImportMetaEnv { readonly [key: string]: string | number | boolean | undefined; - readonly VITE_CONVEX_SITE_URL: string; + readonly VITE_AUTH_URL: string; readonly VITE_FLUE_URL: string; readonly VITE_CONVEX_URL: string; } diff --git a/packages/env/src/web.ts b/packages/env/src/web.ts index c72c430..1873b5e 100644 --- a/packages/env/src/web.ts +++ b/packages/env/src/web.ts @@ -8,10 +8,9 @@ const convexUrlSchema = (exampleHost: string) => export const env = createEnv({ client: { - VITE_CONVEX_SITE_URL: convexUrlSchema("example.convex.site"), + VITE_AUTH_URL: z.url(), VITE_CONVEX_URL: convexUrlSchema("example.convex.cloud"), VITE_FLUE_URL: z.url(), - VITE_ZOPU_SERVER_URL: z.url(), }, clientPrefix: "VITE_", emptyStringAsUndefined: true, diff --git a/packages/primitives/src/project-issue.test.ts b/packages/primitives/src/project-issue.test.ts index f99008a..514a72c 100644 --- a/packages/primitives/src/project-issue.test.ts +++ b/packages/primitives/src/project-issue.test.ts @@ -29,6 +29,17 @@ describe("project issue primitives", () => { }); }); + it("accepts an existing issue request without creating a duplicate", () => { + const request = Effect.runSync( + decodeProjectIssueRequest({ + issueId: "issue-1", + kind: "existing", + }) + ); + + expect(request).toEqual({ issueId: "issue-1", kind: "existing" }); + }); + it("rejects invalid issue drafts before persistence", () => { const error = Effect.runSync( Effect.flip( diff --git a/packages/primitives/src/project-issue.ts b/packages/primitives/src/project-issue.ts index 5ccd839..d85530a 100644 --- a/packages/primitives/src/project-issue.ts +++ b/packages/primitives/src/project-issue.ts @@ -35,6 +35,11 @@ export const ProjectIssueDraft = Schema.Struct({ }); export type ProjectIssueDraft = typeof ProjectIssueDraft.Type; +export const ExistingProjectIssueRequest = Schema.Struct({ + issueId: ProjectIssueId, + kind: Schema.Literal("existing"), +}); + export const ExplicitProjectIssueRequest = Schema.Struct({ body: Schema.String, kind: Schema.Literal("explicit"), @@ -48,6 +53,7 @@ export const SignalProjectIssueRequest = Schema.Struct({ }); export const ProjectIssueRequest = Schema.Union([ + ExistingProjectIssueRequest, ExplicitProjectIssueRequest, SignalProjectIssueRequest, ]); @@ -186,7 +192,7 @@ export const decodeProjectIssueRequest = ( () => new ProjectIssueRequestError({ message: - "Use a signal request or an explicit request with projectId, title, and body", + "Use an existing issue, signal request, or explicit request with projectId, title, and body", reason: "InvalidInput", }) ) -- 2.49.1 From 4c741fbe0621d6bef04a94f5a557aa4fa568270f Mon Sep 17 00:00:00 2001 From: -Puter <22245429+puterhimself@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:57:16 +0530 Subject: [PATCH 2/3] Fix type errors, lint, and complexity from VPS architecture cleanup - Fix brand-to-Convex-Id casts in use-project-workspace (as unknown as) - Extract ProjectsLoading and ConnectProject components to drop SliceOnePage complexity below the lint threshold of 20 - Fix formatting in project-requests.ts - Archive orphaned standalone chat files (route removed, referenced dead env) - All 8 packages typecheck clean; 31 web tests pass; root check clean --- .../components/slice-one/slice-one-page.tsx | 96 ++++++++++--------- apps/web/src/hooks/use-project-workspace.ts | 4 +- apps/web/src/lib/projects/project-requests.ts | 4 +- .../standalone-chat/standalone-chat-page.tsx | 0 .../web}/routes/standalone/chat/page.tsx | 0 5 files changed, 54 insertions(+), 50 deletions(-) rename {apps/web/src => repos/web}/components/standalone-chat/standalone-chat-page.tsx (100%) rename {apps/web/src => repos/web}/routes/standalone/chat/page.tsx (100%) diff --git a/apps/web/src/components/slice-one/slice-one-page.tsx b/apps/web/src/components/slice-one/slice-one-page.tsx index 8792fe0..c13c3b8 100644 --- a/apps/web/src/components/slice-one/slice-one-page.tsx +++ b/apps/web/src/components/slice-one/slice-one-page.tsx @@ -113,6 +113,54 @@ const ConversationEmptyState = () => (
); +type SliceOneState = ReturnType; + +const ProjectsLoading = () => ( +
+ +
+); +const ConnectProject = ({ slice }: { slice: SliceOneState }) => ( +
+
{ + event.preventDefault(); + void slice.connectRepository(); + }} + > + + + +

Connect one project

+

+ Slice 1 turns actionable conversation into proposed Work with exact + provenance. +

+ slice.setRepository(event.target.value)} + placeholder="https://github.com/owner/repository" + required + value={slice.repository} + /> + {slice.error ? ( +

{slice.error.message}

+ ) : null} + +
+
+); export const SliceOnePage = () => { const slice = useSliceOne(); @@ -158,55 +206,11 @@ export const SliceOnePage = () => { }; if (slice.projects === undefined) { - return ( -
- -
- ); + return ; } if (!slice.selectedProject) { - return ( -
-
{ - event.preventDefault(); - void slice.connectRepository(); - }} - > - - - -

Connect one project

-

- Slice 1 turns actionable conversation into proposed Work with exact - provenance. -

- slice.setRepository(event.target.value)} - placeholder="https://github.com/owner/repository" - required - value={slice.repository} - /> - {slice.error ? ( -

{slice.error.message}

- ) : null} - -
-
- ); + return ; } const send = async () => { diff --git a/apps/web/src/hooks/use-project-workspace.ts b/apps/web/src/hooks/use-project-workspace.ts index 0d172c5..f063f4c 100644 --- a/apps/web/src/hooks/use-project-workspace.ts +++ b/apps/web/src/hooks/use-project-workspace.ts @@ -103,7 +103,7 @@ export const useProjectWorkspace = () => { title: nextTitle, }, }); - setSelectedIssueId(outcome.issueId as Id<"projectIssues">); + setSelectedIssueId(outcome.issueId as unknown as Id<"projectIssues">); setIssueTitle(""); setIssueBody(""); } catch (caughtError) { @@ -125,7 +125,7 @@ export const useProjectWorkspace = () => { accessToken, request: { kind: "signal", signalId }, }); - setSelectedIssueId(outcome.issueId as Id<"projectIssues">); + setSelectedIssueId(outcome.issueId as unknown as Id<"projectIssues">); } catch (caughtError) { setError(errorMessage(caughtError)); } finally { diff --git a/apps/web/src/lib/projects/project-requests.ts b/apps/web/src/lib/projects/project-requests.ts index 788d7ed..b911757 100644 --- a/apps/web/src/lib/projects/project-requests.ts +++ b/apps/web/src/lib/projects/project-requests.ts @@ -1,7 +1,7 @@ import type { Id } from "@code/backend/convex/_generated/dataModel"; import { env } from "@code/env/web"; -import { ProjectIssueRequestResult } from '@code/primitives/project-issue'; -import type { ProjectIssueRequestResult as ProjectIssueRequestResultValue } from '@code/primitives/project-issue'; +import { ProjectIssueRequestResult } from "@code/primitives/project-issue"; +import type { ProjectIssueRequestResult as ProjectIssueRequestResultValue } from "@code/primitives/project-issue"; import { Schema } from "effect"; const projectRequestsUrl = new URL( diff --git a/apps/web/src/components/standalone-chat/standalone-chat-page.tsx b/repos/web/components/standalone-chat/standalone-chat-page.tsx similarity index 100% rename from apps/web/src/components/standalone-chat/standalone-chat-page.tsx rename to repos/web/components/standalone-chat/standalone-chat-page.tsx diff --git a/apps/web/src/routes/standalone/chat/page.tsx b/repos/web/routes/standalone/chat/page.tsx similarity index 100% rename from apps/web/src/routes/standalone/chat/page.tsx rename to repos/web/routes/standalone/chat/page.tsx -- 2.49.1 From a5414d83bdca133c65866e5415902c88920cd2db Mon Sep 17 00:00:00 2001 From: -Puter <22245429+puterhimself@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:32:17 +0530 Subject: [PATCH 3/3] Convex-only Slice 1: clients talk to Convex, Flue is a private worker Normalize the topology so web/desktop/mobile clients communicate only with Convex. Convex admits durable commands, dispatches private Flue turns through FLUE_URL, and stores the product-facing result before clients observe it. - Normalized relational schema: organizations, projects, conversations/turns/ messages/attachments, signals/sources/constraints, works/events/attachments. - Conversation turns queued in Convex; the agent runAction dispatches to Flue and persists assistant response, signals, and proposed work back into Convex. - Browser chat moved off the Flue transport: use-chat-agent now reads reactive Convex rows and sends through conversationMessages.send. - Fixed signed-in blank screen: gate all product queries on useConvexAuth (isAuthenticated && !isRefreshing) plus personal-org bootstrap. - Pinned react/react-dom to 19.2.8 via catalog to fix SSR dual-React crash. - Removed ~16.6k net lines: daemon, AgentOS/Orb, project issues/runs/artifacts, todos, browser Flue transport, mobile execution views, smoke scripts. Verified end-to-end on cheaptricks: connect repo, send actionable message, Flue turn completes, Signal + proposed Work created with exact provenance, persists across refresh. --- .env.example | 3 +- apps/web/package.json | 6 +- .../src/components/chat/chat-attachments.tsx | 4 +- apps/web/src/components/chat/chat-message.tsx | 36 +- .../src/components/chat/chat-tool-call.tsx | 60 - .../mobile-workspace/mobile-flow-page.tsx | 200 --- .../components/slice-one/slice-one-page.tsx | 10 +- .../web/src/hooks/chat/use-chat-agent.test.ts | 79 -- apps/web/src/hooks/chat/use-chat-agent.ts | 107 +- apps/web/src/hooks/chat/use-chat-images.ts | 7 +- apps/web/src/hooks/slice-one/use-slice-one.ts | 11 +- .../src/hooks/use-mobile-project-workspace.ts | 126 -- apps/web/src/hooks/use-mobile-work-stack.ts | 66 - apps/web/src/hooks/use-mobile-workspace.ts | 98 -- .../src/hooks/use-personal-organization.ts | 10 +- apps/web/src/hooks/use-project-workspace.ts | 205 ---- apps/web/src/lib/chat/attachments.test.ts | 10 +- apps/web/src/lib/chat/attachments.ts | 19 - apps/web/src/lib/chat/constants.ts | 13 +- apps/web/src/lib/chat/conversation.test.ts | 44 + apps/web/src/lib/chat/conversation.ts | 50 + apps/web/src/lib/chat/transforms.test.ts | 39 +- apps/web/src/lib/chat/transforms.ts | 27 +- apps/web/src/lib/chat/types.ts | 50 +- apps/web/src/lib/flue-transport.test.ts | 152 --- apps/web/src/lib/flue-transport.ts | 120 -- .../build-mobile-workspace-view.ts | 191 --- .../mobile-status-message.test.ts | 23 - .../mobile-workspace/mobile-status-message.ts | 25 - .../src/lib/projects/project-evidence.test.ts | 91 -- apps/web/src/lib/projects/project-evidence.ts | 315 ----- apps/web/src/lib/projects/project-requests.ts | 62 - .../src/lib/slice-one/presentation.test.ts | 18 +- apps/web/src/lib/slice-one/presentation.ts | 18 +- apps/web/src/root.tsx | 59 +- apps/web/src/routes.ts | 9 +- apps/web/src/routes/app/mobile/chat/page.tsx | 12 - .../src/routes/app/mobile/chat/work/page.tsx | 15 - apps/web/src/routes/app/mobile/layout.tsx | 9 - .../src/routes/app/mobile/work-list/page.tsx | 15 - apps/web/src/routes/app/mobile/work/page.tsx | 15 - apps/web/src/routes/app/todos/page.tsx | 123 -- apps/web/vite.config.ts | 6 +- bun.lock | 168 +-- docs/TECH.md | 84 +- package.json | 8 +- packages/agents/package.json | 11 +- .../agents/scripts/agentos-adapter-test.ts | 91 -- .../agents/scripts/agentos-sandbox-ref.ts | 142 --- .../src/actions/finalize-gitea-lifecycle.ts | 223 ---- packages/agents/src/agents/project-manager.ts | 29 - packages/agents/src/agents/zopu-dev.ts | 51 - packages/agents/src/agents/zopu.ts | 4 +- packages/agents/src/app.ts | 3 - packages/agents/src/auth.ts | 327 +---- packages/agents/src/git/gitea.test.ts | 155 --- packages/agents/src/git/gitea.ts | 307 ----- packages/agents/src/orb/README.md | 105 -- packages/agents/src/orb/context-pack.ts | 109 -- .../agents/src/orb/docker-sandbox.test.ts | 170 --- packages/agents/src/orb/docker-sandbox.ts | 118 -- packages/agents/src/orb/domain.test.ts | 132 -- packages/agents/src/orb/domain.ts | 259 ---- packages/agents/src/orb/events.test.ts | 134 -- packages/agents/src/orb/events.ts | 216 ---- packages/agents/src/orb/git-adapter.ts | 81 -- packages/agents/src/orb/index.ts | 13 - .../agents/src/orb/opencode-config.test.ts | 82 -- packages/agents/src/orb/opencode-config.ts | 76 -- packages/agents/src/orb/orb-adapter.ts | 132 -- .../src/orb/orb-project-manager.live.test.ts | 132 -- .../src/orb/orb-project-manager.test.ts | 709 ----------- .../agents/src/orb/orb-project-manager.ts | 636 ---------- .../agents/src/orb/permission-policy.test.ts | 107 -- packages/agents/src/orb/permission-policy.ts | 90 -- packages/agents/src/orb/ports.ts | 136 --- .../agents/src/orb/project-events.test.ts | 123 -- packages/agents/src/orb/project-events.ts | 171 --- packages/agents/src/orb/runtime.test.ts | 125 -- packages/agents/src/orb/runtime.ts | 763 ------------ packages/agents/src/project-request.ts | 175 --- packages/agents/src/sandboxes/agent-os.ts | 312 ----- packages/agents/src/sandboxes/agentos.ts | 121 -- .../sandboxes/host-repository-bridge.test.ts | 297 ----- .../src/sandboxes/host-repository-bridge.ts | 506 -------- packages/agents/src/skills/paseo/SKILL.md | 143 --- packages/agents/src/tools/git-remote.ts | 119 -- packages/agents/src/tools/paseo.ts | 43 - packages/agents/src/tools/project.ts | 70 -- packages/agents/src/tools/signals.ts | 368 ------ packages/agents/src/tools/workflow.ts | 37 - packages/auth/package.json | 4 +- packages/backend/CLAUDE.md | 13 - packages/backend/convex/README.md | 22 +- packages/backend/convex/_generated/api.d.ts | 22 - .../backend/convex/_generated/server.d.ts | 1 + packages/backend/convex/agentWorkspace.ts | 388 ------ packages/backend/convex/artifactModel.ts | 71 -- .../convex/conversationIngress.smoke.test.ts | 90 -- .../convex/conversationMessages.test.ts | 328 ++--- .../backend/convex/conversationMessages.ts | 545 ++++++--- packages/backend/convex/convex.config.ts | 1 + packages/backend/convex/daemonCommands.ts | 204 ---- packages/backend/convex/daemonRuntime.ts | 107 -- packages/backend/convex/daemons.ts | 60 - packages/backend/convex/linkedSignals.test.ts | 387 ------ packages/backend/convex/projectArtifacts.ts | 19 - packages/backend/convex/projectEvents.test.ts | 285 ----- .../convex/projectIssueRequests.smoke.test.ts | 177 --- packages/backend/convex/projectIssues.ts | 223 ---- packages/backend/convex/projectStore.ts | 430 ------- packages/backend/convex/projects.ts | 291 ++--- packages/backend/convex/schema.ts | 343 ++---- packages/backend/convex/signalRouting.test.ts | 610 +--------- packages/backend/convex/signalRouting.ts | 825 ++----------- packages/backend/convex/signals.test.ts | 620 ---------- packages/backend/convex/signals.ts | 505 -------- packages/backend/convex/todos.ts | 43 - packages/backend/convex/workflows.ts | 64 - packages/backend/convex/works.test.ts | 239 +--- packages/backend/convex/works.ts | 37 +- packages/env/package.json | 1 - packages/env/src/smoke.ts | 55 - packages/env/src/vite-env.d.ts | 1 - packages/env/src/web.ts | 1 - packages/ui/package.json | 4 +- scripts/zopu-smoke.ts | 1080 ----------------- 127 files changed, 1283 insertions(+), 17784 deletions(-) delete mode 100644 apps/web/src/components/chat/chat-tool-call.tsx delete mode 100644 apps/web/src/components/mobile-workspace/mobile-flow-page.tsx delete mode 100644 apps/web/src/hooks/chat/use-chat-agent.test.ts delete mode 100644 apps/web/src/hooks/use-mobile-project-workspace.ts delete mode 100644 apps/web/src/hooks/use-mobile-work-stack.ts delete mode 100644 apps/web/src/hooks/use-mobile-workspace.ts delete mode 100644 apps/web/src/hooks/use-project-workspace.ts create mode 100644 apps/web/src/lib/chat/conversation.test.ts create mode 100644 apps/web/src/lib/chat/conversation.ts delete mode 100644 apps/web/src/lib/flue-transport.test.ts delete mode 100644 apps/web/src/lib/flue-transport.ts delete mode 100644 apps/web/src/lib/mobile-workspace/build-mobile-workspace-view.ts delete mode 100644 apps/web/src/lib/mobile-workspace/mobile-status-message.test.ts delete mode 100644 apps/web/src/lib/mobile-workspace/mobile-status-message.ts delete mode 100644 apps/web/src/lib/projects/project-evidence.test.ts delete mode 100644 apps/web/src/lib/projects/project-evidence.ts delete mode 100644 apps/web/src/lib/projects/project-requests.ts delete mode 100644 apps/web/src/routes/app/mobile/chat/page.tsx delete mode 100644 apps/web/src/routes/app/mobile/chat/work/page.tsx delete mode 100644 apps/web/src/routes/app/mobile/layout.tsx delete mode 100644 apps/web/src/routes/app/mobile/work-list/page.tsx delete mode 100644 apps/web/src/routes/app/mobile/work/page.tsx delete mode 100644 apps/web/src/routes/app/todos/page.tsx delete mode 100644 packages/agents/scripts/agentos-adapter-test.ts delete mode 100644 packages/agents/scripts/agentos-sandbox-ref.ts delete mode 100644 packages/agents/src/actions/finalize-gitea-lifecycle.ts delete mode 100644 packages/agents/src/agents/project-manager.ts delete mode 100644 packages/agents/src/agents/zopu-dev.ts delete mode 100644 packages/agents/src/git/gitea.test.ts delete mode 100644 packages/agents/src/git/gitea.ts delete mode 100644 packages/agents/src/orb/README.md delete mode 100644 packages/agents/src/orb/context-pack.ts delete mode 100644 packages/agents/src/orb/docker-sandbox.test.ts delete mode 100644 packages/agents/src/orb/docker-sandbox.ts delete mode 100644 packages/agents/src/orb/domain.test.ts delete mode 100644 packages/agents/src/orb/domain.ts delete mode 100644 packages/agents/src/orb/events.test.ts delete mode 100644 packages/agents/src/orb/events.ts delete mode 100644 packages/agents/src/orb/git-adapter.ts delete mode 100644 packages/agents/src/orb/index.ts delete mode 100644 packages/agents/src/orb/opencode-config.test.ts delete mode 100644 packages/agents/src/orb/opencode-config.ts delete mode 100644 packages/agents/src/orb/orb-adapter.ts delete mode 100644 packages/agents/src/orb/orb-project-manager.live.test.ts delete mode 100644 packages/agents/src/orb/orb-project-manager.test.ts delete mode 100644 packages/agents/src/orb/orb-project-manager.ts delete mode 100644 packages/agents/src/orb/permission-policy.test.ts delete mode 100644 packages/agents/src/orb/permission-policy.ts delete mode 100644 packages/agents/src/orb/ports.ts delete mode 100644 packages/agents/src/orb/project-events.test.ts delete mode 100644 packages/agents/src/orb/project-events.ts delete mode 100644 packages/agents/src/orb/runtime.test.ts delete mode 100644 packages/agents/src/orb/runtime.ts delete mode 100644 packages/agents/src/project-request.ts delete mode 100644 packages/agents/src/sandboxes/agent-os.ts delete mode 100644 packages/agents/src/sandboxes/agentos.ts delete mode 100644 packages/agents/src/sandboxes/host-repository-bridge.test.ts delete mode 100644 packages/agents/src/sandboxes/host-repository-bridge.ts delete mode 100644 packages/agents/src/skills/paseo/SKILL.md delete mode 100644 packages/agents/src/tools/git-remote.ts delete mode 100644 packages/agents/src/tools/paseo.ts delete mode 100644 packages/agents/src/tools/project.ts delete mode 100644 packages/agents/src/tools/signals.ts delete mode 100644 packages/agents/src/tools/workflow.ts delete mode 100644 packages/backend/CLAUDE.md delete mode 100644 packages/backend/convex/agentWorkspace.ts delete mode 100644 packages/backend/convex/artifactModel.ts delete mode 100644 packages/backend/convex/conversationIngress.smoke.test.ts delete mode 100644 packages/backend/convex/daemonCommands.ts delete mode 100644 packages/backend/convex/daemonRuntime.ts delete mode 100644 packages/backend/convex/daemons.ts delete mode 100644 packages/backend/convex/linkedSignals.test.ts delete mode 100644 packages/backend/convex/projectArtifacts.ts delete mode 100644 packages/backend/convex/projectEvents.test.ts delete mode 100644 packages/backend/convex/projectIssueRequests.smoke.test.ts delete mode 100644 packages/backend/convex/projectIssues.ts delete mode 100644 packages/backend/convex/projectStore.ts delete mode 100644 packages/backend/convex/signals.test.ts delete mode 100644 packages/backend/convex/signals.ts delete mode 100644 packages/backend/convex/todos.ts delete mode 100644 packages/backend/convex/workflows.ts delete mode 100644 packages/env/src/smoke.ts delete mode 100644 scripts/zopu-smoke.ts diff --git a/.env.example b/.env.example index 873e048..d65d729 100644 --- a/.env.example +++ b/.env.example @@ -8,8 +8,6 @@ NATIVE_APP_URL=code:// # Browser and native public endpoints VITE_AUTH_URL=http://localhost:5173 VITE_CONVEX_URL=https://example.convex.cloud -# For phone testing, replace localhost with this machine's Tailscale IPv4 address. -VITE_FLUE_URL=http://localhost:3583 EXPO_PUBLIC_CONVEX_URL=https://example.convex.cloud EXPO_PUBLIC_CONVEX_SITE_URL=https://example.convex.site @@ -23,6 +21,7 @@ DAEMON_COMMAND_LEASE_MS=60000 # Flue persistence adapter FLUE_DB_TOKEN=replace-with-a-long-random-token +FLUE_URL=http://localhost:3583 # Agent model provider AGENT_MODEL_PROVIDER=xiaomi diff --git a/apps/web/package.json b/apps/web/package.json index bc779f4..67dc49e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,8 +15,6 @@ "@code/env": "workspace:*", "@code/primitives": "workspace:*", "@code/ui": "workspace:*", - "@flue/react": "1.0.0-beta.9", - "@flue/sdk": "1.0.0-beta.9", "@react-router/fs-routes": "^8.1.0", "@react-router/node": "^8.1.0", "@react-router/serve": "^8.1.0", @@ -24,8 +22,8 @@ "isbot": "^5.1.44", "lucide-react": "catalog:", "next-themes": "catalog:", - "react": "^19.2.7", - "react-dom": "^19.2.7", + "react": "catalog:", + "react-dom": "catalog:", "react-router": "^8.1.0", "sonner": "catalog:", "streamdown": "2.5.0" diff --git a/apps/web/src/components/chat/chat-attachments.tsx b/apps/web/src/components/chat/chat-attachments.tsx index df8b2a0..208e610 100644 --- a/apps/web/src/components/chat/chat-attachments.tsx +++ b/apps/web/src/components/chat/chat-attachments.tsx @@ -9,13 +9,13 @@ import { AttachmentMedia, AttachmentTitle, } from "@code/ui/components/attachment"; -import type { FlueConversationPart } from "@flue/react"; import { ImageIcon, X } from "lucide-react"; import { useEffect, useState } from "react"; import type { PendingChatImage } from "@/lib/chat/attachments"; +import type { ConversationPart } from "@/lib/chat/types"; -type FilePart = Extract; +type FilePart = Extract; const isDirectlyRenderableUrl = (url: string): boolean => url.startsWith("blob:") || url.startsWith("data:"); diff --git a/apps/web/src/components/chat/chat-message.tsx b/apps/web/src/components/chat/chat-message.tsx index e487e69..647d3c7 100644 --- a/apps/web/src/components/chat/chat-message.tsx +++ b/apps/web/src/components/chat/chat-message.tsx @@ -7,12 +7,10 @@ import { MobileChatBubble, MobileChatMessage, } from "@code/ui/components/mobile-chat"; -import type { FlueConversationMessage } from "@flue/react"; import { getMessageText, getReasoningText, - hasToolActivity, isReasoningStreaming, isMessageStreaming, } from "@/lib/chat/transforms"; @@ -23,7 +21,6 @@ import type { import { AssistantIdentity } from "./assistant-identity"; import { MessageAttachments } from "./chat-attachments"; -import { ChatToolCall } from "./chat-tool-call"; const ReasoningTrace = ({ isStreaming, @@ -96,27 +93,7 @@ const assistantState = ( return isStreaming ? "writing" : undefined; }; -const ToolActivity = ({ - hidden, - message, -}: { - readonly hidden: boolean; - readonly message: FlueConversationMessage; -}) => { - if (hidden) { - return null; - } - return message.parts.map((part) => - part.type === "dynamic-tool" ? ( - - ) : null - ); -}; - -export const ChatMessage = ({ - hideToolActivity = false, - message, -}: ChatMessageProps) => { +export const ChatMessage = ({ message }: ChatMessageProps) => { const isUser = message.role === "user"; const isStreaming = isMessageStreaming(message); const text = getMessageText(message); @@ -124,16 +101,6 @@ export const ChatMessage = ({ const reasoningStreaming = isReasoningStreaming(message); const fileParts = message.parts.filter((part) => part.type === "file"); - if ( - hideToolActivity && - !isUser && - hasToolActivity(message) && - !text && - !reasoning - ) { - return null; - } - if (message.parts.length === 0 && !isStreaming) { return null; } @@ -167,7 +134,6 @@ export const ChatMessage = ({ text={reasoning} /> )} -