The React island

Where the chat interface lives, what it builds to, and what reaches it from the server. How to work on it — and the traps — is in Work on the chat interface.

Layout

tamebi-nc-app/ frontend/src/ main.tsx one bundle, two mounts (app page + core Dashboard) App.tsx the chat surface components/ TamebiPromptInput.tsx the composer: chips, @ / # / triggers, voice ToolTrace.tsx tool trace and the approval prompt ChatError.tsx the error policy AppFrame.tsx the MCP App host Aurora.tsx the animated background (WebGL) ai-elements/ conversation, message, prompt-input, loader, suggestion ui/ shadcn primitives lib/ app-bridge.ts the one chokepoint for frame messages message-blocks.ts grouping parts into what renders js/dist/ build output, linked by name from the PHP template css/tamebi-island.css copied here by scripts/place-css.mjs lib/Controller/ PageController, ApiController, AppFrameController, MailController templates/main.php the mount node and its data attributes

Build

npm run dev     # vite build --watch into ../js/dist
npm run build   # build, then place-css.mjs copies the stylesheet into ../css/
npm test

Output filenames are fixed — tamebi.mjs, tamebi.css, no content hash — so the PHP template can link them by name. Nextcloud's own cachebuster query string handles invalidation.

What reaches the island from the server

Data attributes on #tamebi-island, so URL generation stays server-side and survives a subdirectory install:

AttributeContents
data-api-urlthe chat endpoint
data-ocs-urlOCS base, for the composer's people and file search
data-files-urlthe Files app, for opening a result
data-user-namedisplay name
data-appswhich Nextcloud apps are installed, for the / menu
data-connectedwhether workspace access is live
data-connect-urlwhere to send the user to authorise
data-prompta prefilled question, from the Dashboard handover

Two mounts, one bundle

The app page ships its own mount node from PHP. Core's Dashboard cannot, so the same bundle prepares that DOM itself: it reads the initial state, waits for the dashboard container with a MutationObserver, injects a root, and lays out the widget grid.

The Dashboard prompt never calls the API. On submit it navigates to the app page with the question as a query parameter, which arrives as data-prompt. One chat implementation, two entry points.

Pinned versions

ai and @ai-sdk/react are a pinned pair. A nested node_modules/@ai-sdk/react/node_modules/ai means they have drifted, and approvals are discarded in silence. → Verified facts

The design system

DESIGN.md is the single source for colours, type, spacing, radii and motion. Every token is a CSS variable in the one entry file where Tailwind is configured, and is consumed only through Tailwind classes.

The island's CSS entry uses @import "tailwindcss" important; — Nextcloud's own stylesheets are specific enough to win otherwise.

CLAUDE.md holds the component ladder — native element, then AI Elements, then shadcn/Radix, then custom — scoped to this directory. The OnlyOffice plugin is deliberately ES5-style vanilla JS with no build step; do not "fix" it by adding a component library.

The OnlyOffice plugin

tamebi-oo-plugin/ runs inside the document editor, a different sandbox entirely. A bridge in the plugin frame talks postMessage to addin iframes hosted externally, and a shim gives those addins an Office.js-shaped API.

The .gz siblings of each file are what the documentserver actually serves. Regenerate them when editing the source, and restart the documentserver.