Skip to content

Latest commit

 

History

History
402 lines (286 loc) · 17.5 KB

File metadata and controls

402 lines (286 loc) · 17.5 KB

Component-Level TypeScript Audit: tambo full-send Installed Files

Companion to TAMBO_ISSUE.md, which documents the 59 compilation errors caused by missing @/ path alias configuration, verbatimModuleSyntax violations, and an unused import. This document covers additional type-safety and code-quality concerns inside the components themselves, the exact CLI source locations where fixes should land, and implementation guidance using patterns the codebase already has.

Scope: Vite-Only

Next.js is unaffectedcreate-next-app configures @/* path aliases by default in tsconfig.json. Expo is unaffected — the CLI detects Expo projects and skips web component installation entirely (cli/src/commands/init.ts:765). The blocking issues in TAMBO_ISSUE.md and the component-level findings below apply exclusively to Vite projects.

Methodology

After temporarily applying the @/ alias fix described in TAMBO_ISSUE.md (tsconfig.app.json paths + vite.config.ts resolve.alias), npx tsc -b produced exactly 3 errors — the same 3 already documented. No hidden compilation errors exist beneath the alias noise.

The components were then audited manually for type-safety issues that tsc does not flag due to skipLibCheck: true, any types, and unsafe assertions. Each finding references the source template file in the tambo repository (packages/ui-registry/) that produces the installed code.


Part 1: CLI Fixes (Blocking)

These are the issues from TAMBO_ISSUE.md that prevent compilation entirely. This section adds the exact source locations and implementation guidance for the Tambo team.

CLI-1: Missing @/ path alias setup for Vite projects (56 errors)

Root cause: The handleFullSendInit() function in cli/src/commands/init.ts:738–878 calls setupTailwindAndGlobals() at line 873 to handle Tailwind configuration, but there is no corresponding call to set up the @/ path alias that every installed component depends on. The updateImportPaths() function in cli/src/commands/migrate.ts:29–80 correctly rewrites @tambo-ai/ui-registry/* imports to @/* imports during component installation (called from cli/src/commands/add/component.ts:243), but nothing configures the user's project to resolve those @/* imports.

Where to fix:

The fix has two parts — a vite.config.ts modification and a tsconfig.app.json modification. Both should run after component installation completes and before displayFullSendInstructions() is called.

Suggested insertion pointcli/src/commands/init.ts, between lines 873 and 875:

  // Setup tailwind after all components installed (outside spinner to allow prompts)
  await setupTailwindAndGlobals(process.cwd());

+ // Setup @/ path alias for Vite projects so installed component imports resolve
+ if (framework?.name === "vite") {
+   setupPathAlias(process.cwd());
+ }

  trackEvent(EVENTS.INIT_COMPLETED, { method: "cloud", is_full_send: true });

Part A: vite.config.ts — add resolve.alias

The codebase already has a pattern for safely modifying vite.config.ts using ts-morph. The addTailwindVitePlugin() function in cli/src/commands/add/tailwind/v4/toolchain-setup.ts:145–215 demonstrates the approach:

  1. Parse the file with ts-morph (Project + createSourceFile)
  2. Find the defineConfig() call or default export object via AST traversal
  3. Modify via position-aware string splicing (modify content first, add imports second so positions stay valid)

A parallel addResolveAlias() function could follow the same pattern. Instead of finding the plugins array, it would:

  1. Find the defineConfig() object literal argument (same findPluginsArray strategy 1 and 2 pattern, but targeting the whole object instead of plugins)
  2. Check if resolve.alias already exists (bail if so)
  3. Insert a resolve property with the alias configuration
  4. Add import path from "path"; at the top

The existing setupVitePlugin() at toolchain-setup.ts:47–80 shows the full lifecycle: find config → check idempotency → modify AST → install deps → write file. The alias setup should follow the same structure.

Target state for vite.config.ts:

import path from "path";
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [tailwindcss(), react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

Note: @types/node is already installed as a dev dependency by tambo full-send (it's listed in the component registry's dependencies), so the path import will resolve.

Part B: tsconfig.app.json — add baseUrl and paths

This is simpler than the Vite config — it's JSON, not AST. Read, parse, merge, write:

function setupTsconfigPathAlias(projectRoot: string): void {
  // Check tsconfig.app.json first (Vite convention), fall back to tsconfig.json
  const candidates = ["tsconfig.app.json", "tsconfig.json"];
  const configFile = candidates.find((f) =>
    fs.existsSync(path.join(projectRoot, f)),
  );
  if (!configFile) return;

  const configPath = path.join(projectRoot, configFile);
  const content = JSON.parse(fs.readFileSync(configPath, "utf-8"));

  // Bail if paths already configured
  if (content.compilerOptions?.paths?.["@/*"]) {
    return;
  }

  content.compilerOptions = content.compilerOptions ?? {};
  content.compilerOptions.baseUrl = content.compilerOptions.baseUrl ?? ".";
  content.compilerOptions.paths = {
    ...content.compilerOptions.paths,
    "@/*": ["./src/*"],
  };

  fs.writeFileSync(configPath, JSON.stringify(content, null, 2) + "\n");
}

Idempotency: Both parts should check for existing configuration before modifying. The Tailwind plugin setup already demonstrates this pattern (toolchain-setup.ts:57 checks content.includes("@tailwindcss/vite")).

CLI-2: Snippet mixes relative and alias imports

Source: cli/src/commands/init.ts:1022–1024

const providerSnippet = `${useClientDirective}import { TamboProvider } from "@tambo-ai/react";
import { components } from "../../lib/tambo";
${importStatements}

Line 1023 hardcodes ../../lib/tambo as a relative import. Line 1003 (via importStatements) generates @/components/tambo/... alias imports. These appear in the same snippet that gets copied to the user's clipboard.

The relative import assumes App.tsx is at src/App.tsx and tambo.ts is at src/lib/tambo.ts — a two-level relative traversal. But if the alias is configured (as it should be after CLI-1 is fixed), this should just be @/lib/tambo.

Proposed fix:

 // cli/src/commands/init.ts:1023
-import { components } from "../../lib/tambo";
+import { components } from "@/lib/tambo";

This makes the snippet consistent — all imports use the @/ alias.

CLI-3: verbatimModuleSyntax violation in message.tsx template (2 errors)

Source: packages/ui-registry/src/components/message/message.tsx:3

import { ReactTamboThreadMessage, TamboToolUseContent } from "@tambo-ai/react";

Both ReactTamboThreadMessage and TamboToolUseContent are used only as type annotations in this file, never as runtime values. The create-vite react-ts template enables verbatimModuleSyntax: true, which requires type-only imports to use the import type syntax.

Proposed fix:

-import { ReactTamboThreadMessage, TamboToolUseContent } from "@tambo-ai/react";
+import type { ReactTamboThreadMessage, TamboToolUseContent } from "@tambo-ai/react";

CLI-4: Unused React import in dictation-button.tsx template (1 error)

Source: packages/ui-registry/src/components/message-input/dictation-button.tsx:4

import React, { useEffect, useRef } from "react";

The default React import is unused. The create-vite react-ts template enables noUnusedLocals: true.

Proposed fix:

-import React, { useEffect, useRef } from "react";
+import { useEffect, useRef } from "react";

Part 2: Component-Level Findings (Non-Blocking)

These issues do not prevent compilation or cause runtime failures. They are type-safety and code-quality concerns that would be flagged by stricter ESLint configurations.

Finding 1: Explicit any in markdown-components.tsx

Source: packages/ui-registry/src/components/message/markdown-components.tsx:128–131

export const createMarkdownComponents = (): Record<
  string,
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  React.ComponentType<any>
> => ({
  code: function Code({ className, children, ...props }) { /* ... */ },
  a: ({ href, children }) => { /* ... */ },
  // ...
});

Problem: The return type Record<string, React.ComponentType<any>> means none of the inline component functions (code, a, etc.) receive prop type checking from consumers. The eslint-disable comment acknowledges this explicitly. Any component calling createMarkdownComponents() and rendering one of these components gets no type safety on the props it passes.

Proposed fix: Define explicit prop interfaces for each component and use a typed return:

interface CodeProps {
  className?: string;
  children?: React.ReactNode;
}

interface AnchorProps {
  href?: string;
  children?: React.ReactNode;
}

interface MarkdownComponents {
  code: React.ComponentType<CodeProps>;
  a: React.ComponentType<AnchorProps>;
  // ... other component keys
}

export const createMarkdownComponents = (): MarkdownComponents => ({
  code: function Code({ className, children, ...props }: CodeProps) { /* ... */ },
  a: ({ href, children }: AnchorProps) => { /* ... */ },
  // ...
});

If the streamdown library requires Record<string, React.ComponentType<any>> as its input type, the typed return can be widened at the call site (createMarkdownComponents() as Record<string, React.ComponentType<any>>), keeping the template itself type-safe.

Finding 2: Inconsistent type assertion patterns in text-editor.tsx

Source: packages/ui-registry/src/components/message-input/text-editor.tsx

The file uses two different patterns to extract values from TipTap's node.attrs (typed as Record<string, any>):

Unsafe as assertions (no runtime check):

  • Line 285: const mentionLabel = node.attrs.label as string;
  • Line 777: const mentionId = node.attrs.id as string;

Safe nullish coalescing (with fallback):

  • Line 520: const id = node.attrs.id ?? "";
  • Line 521: const label = node.attrs.label ?? "";

Problem: The as string assertions will silently produce undefined typed as string if the attribute is missing, while the ?? "" pattern correctly handles that case. The same file is inconsistent with itself — the safer pattern is already used in getTextWithResourceURIs() but not in checkMentionExists() or hasMention().

Proposed fix: Apply the pattern already used at lines 520–521 to the other two locations:

 // Line 285 in checkMentionExists():
-      const mentionLabel = node.attrs.label as string;
+      const mentionLabel = node.attrs.label ?? "";

 // Line 777 in hasMention():
-              const mentionId = node.attrs.id as string;
+              const mentionId = node.attrs.id ?? "";

Finding 3: Ref type assertion in message-input.tsx

Source: packages/ui-registry/src/components/message-input/message-input.tsx:319

<TextEditor
  ref={editorRef as React.RefObject<TamboEditor>}
  // ...
/>

The editorRef comes from a render prop at line 282 and is typed as React.RefObject<TamboEditor | null> (defined at line 354 in the McpPromptEffectProps interface). The TextEditor component's ref prop expects React.RefObject<TamboEditor> (without | null). The as cast papers over the nullability mismatch.

Proposed fix: Update the TextEditor component's forwardRef signature to accept TamboEditor | null, which is the standard pattern for refs in React:

 // In text-editor.tsx, the forwardRef declaration:
-const TextEditor = React.forwardRef<TamboEditor, TextEditorProps>(
+const TextEditor = React.forwardRef<TamboEditor | null, TextEditorProps>(

Then remove the cast in message-input.tsx:

 // Line 319 in message-input.tsx:
-              ref={editorRef as React.RefObject<TamboEditor>}
+              ref={editorRef}

Finding 4: unknown → structural cast in thread-hooks.ts

Source: packages/ui-registry/src/lib/thread-hooks.ts:229–251

function hasContentInItem(item: unknown): boolean {
  if (!item || typeof item !== "object") {
    return false;
  }

  const typedItem = item as {
    type?: string;
    text?: string;
    image_url?: { url?: string };
  };

  if (typedItem.type === "text") {
    return !!typedItem.text?.trim();
  }
  // ...
}

Problem: The function receives unknown and immediately casts to a structural type without a type guard. The subsequent property checks are safe at runtime, but the as cast defeats strict mode's purpose.

Proposed fix: Use a type guard to replace the cast:

interface ContentItem {
  type?: string;
  text?: string;
  image_url?: { url?: string };
}

function isContentItem(item: unknown): item is ContentItem {
  return !!item && typeof item === "object" && "type" in item;
}

function hasContentInItem(item: unknown): boolean {
  if (!isContentItem(item)) return false;

  if (item.type === "text") return !!item.text?.trim();
  if (item.type === "image_url") return !!item.image_url?.url;
  return false;
}

Finding 5: Component re-export indirection for Tooltip

Source: packages/ui-registry/src/components/message-input/dictation-button.tsx:1

import { Tooltip } from "@tambo-ai/ui-registry/components/message-suggestions";

The message-suggestions index re-exports Tooltip from suggestions-tooltip (packages/ui-registry/src/components/message-suggestions/index.tsx:7). When installed into a user's project this becomes a two-hop chain: dictation-buttonmessage-suggestionssuggestions-tooltip.

Proposed fix: Import directly from suggestions-tooltip:

 // In dictation-button.tsx (source):
-import { Tooltip } from "@tambo-ai/ui-registry/components/message-suggestions";
+import { Tooltip } from "@tambo-ai/ui-registry/components/message-suggestions/suggestions-tooltip";

 // As installed in user project:
-import { Tooltip } from "@/components/tambo/message-suggestions";
+import { Tooltip } from "@/components/tambo/suggestions-tooltip";

Part 3: Test Coverage Gap

File: cli/src/commands/init.test.ts

The "full-send init" describe block (lines 588–775) covers five scenarios:

Test (line) What it verifies
"should complete full-send init with component selection" (598) File tree structure, success message
"should handle component installation failures gracefully" (650) Error handling for bad components
"should validate that at least one component is selected" (672) Checkbox validation
"should respect --yes flag" (702) Non-interactive mode
"should respect --legacyPeerDeps flag" (754) npm flag passthrough

What's missing: None of these tests assert that tsconfig.app.json or vite.config.ts are modified after full-send completes. The setupTailwindAndGlobals mock at line 268–272 is a no-op:

jest.unstable_mockModule("./add/tailwind-setup.js", () => ({
  setupTailwindAndGlobals: jest.fn(async () => {
    // No-op for tests
  }),
}));

This means even after the alias fix is implemented, there should be new test coverage to verify:

  1. Vite project: tsconfig.app.json gains baseUrl and paths after full-send
  2. Vite project: vite.config.ts gains resolve.alias after full-send
  3. Idempotency: Running full-send twice does not duplicate the alias configuration
  4. Next.js project: tsconfig.json is not modified (alias already exists)
  5. Non-standard tsconfig: Projects with existing baseUrl/paths are not clobbered

Summary Table

# Type Source Location Severity Status
CLI-1 Missing @/ alias setup cli/src/commands/init.ts:873 Blocking 56 tsc errors
CLI-2 Mixed relative/alias in snippet cli/src/commands/init.ts:1023 Confusing Wrong import in clipboard
CLI-3 verbatimModuleSyntax violation packages/ui-registry/.../message.tsx:3 Blocking 2 tsc errors
CLI-4 Unused React import packages/ui-registry/.../dictation-button.tsx:4 Blocking 1 tsc error
1 any return type packages/ui-registry/.../markdown-components.tsx:128 Medium No tsc error
2 Inconsistent as vs ?? packages/ui-registry/.../text-editor.tsx:285,777 Medium No tsc error
3 Ref type assertion packages/ui-registry/.../message-input.tsx:319 Low No tsc error
4 unknown cast packages/ui-registry/.../thread-hooks.ts:234 Low No tsc error
5 Re-export indirection packages/ui-registry/.../dictation-button.tsx:1 Low No tsc error
Tests No alias assertions cli/src/commands/init.test.ts:588–775 Gap

Relationship to TAMBO_ISSUE.md

TAMBO_ISSUE.md documents the user-facing problem: what breaks, how to reproduce it, and what the fix should look like from the user's perspective. This document provides the developer-facing details: exact source locations, implementation patterns to follow, test gaps to fill, and framework-specific scope.