Skip to content

Frontend Architecture

The Kuse Cowork frontend is built with SolidJS, providing a reactive and performant user interface.

Technology Stack

TechnologyVersionPurpose
SolidJS1.8.xReactive UI framework
TypeScript5.3.xType safety
Vite5.4.xBuild tool
Tauri API2.0.xBackend communication

Project Structure

src/
├── App.tsx              # Main application component
├── index.tsx            # Entry point
├── index.css            # Global styles
├── components/          # UI components
│   ├── Chat.tsx         # Chat interface
│   ├── Chat.css
│   ├── AgentMain.tsx    # Agent view
│   ├── AgentMain.css
│   ├── Settings.tsx     # Settings panel
│   ├── Settings.css
│   ├── ModelSelector.tsx
│   ├── TaskPanel.tsx
│   ├── TaskSidebar.tsx
│   ├── MCPSettings.tsx
│   └── ...
├── stores/              # State management
│   ├── settings.ts      # Settings store
│   └── chat.ts          # Chat store
└── lib/                 # Utilities
    ├── tauri-api.ts     # Tauri bridge
    ├── ai-client.ts     # AI provider clients
    ├── mcp-api.ts       # MCP client
    └── claude.ts        # Claude utilities

Component Architecture

App Component

The root component that handles routing and layout:

tsx
const App: Component = () => {
  const [view, setView] = createSignal<'chat' | 'tasks'>('chat');

  return (
    <div class="app">
      <Sidebar view={view()} onViewChange={setView} />
      <main>
        <Switch>
          <Match when={view() === 'chat'}>
            <Chat />
          </Match>
          <Match when={view() === 'tasks'}>
            <AgentMain />
          </Match>
        </Switch>
      </main>
      <Show when={showSettings()}>
        <Settings />
      </Show>
    </div>
  );
};

Chat Component

Handles conversational interactions:

tsx
const Chat: Component = () => {
  const [messages, setMessages] = createSignal<Message[]>([]);
  const [input, setInput] = createSignal('');
  const [streaming, setStreaming] = createSignal(false);

  const sendMessage = async () => {
    const content = input();
    setInput('');

    await sendChatMessage(conversationId, content, (text) => {
      // Update streaming display
    });
  };

  return (
    <div class="chat">
      <MessageList messages={messages()} />
      <InputArea
        value={input()}
        onChange={setInput}
        onSubmit={sendMessage}
        disabled={streaming()}
      />
    </div>
  );
};

AgentMain Component

Manages task execution and progress display:

tsx
const AgentMain: Component = () => {
  const [task, setTask] = createSignal<Task | null>(null);
  const [events, setEvents] = createSignal<AgentEvent[]>([]);

  const runTask = async () => {
    await runTaskAgent(request, (event) => {
      setEvents(e => [...e, event]);
      // Handle different event types
    });
  };

  return (
    <div class="agent-main">
      <TaskSidebar onSelectTask={setTask} />
      <TaskPanel task={task()} events={events()} />
    </div>
  );
};

State Management

Settings Store

Global settings management:

typescript
// stores/settings.ts
import { createSignal } from "solid-js";

export interface Settings {
  apiKey: string;
  model: string;
  baseUrl: string;
  maxTokens: number;
  temperature?: number;
  providerKeys: Record<string, string>;
}

const [settings, setSettings] = createSignal<Settings>(DEFAULT_SETTINGS);

export function useSettings() {
  return {
    settings,
    updateSetting: async <K extends keyof Settings>(
      key: K,
      value: Settings[K]
    ) => {
      const newSettings = { ...settings(), [key]: value };
      setSettings(newSettings);
      await persistSettings(newSettings);
    },
  };
}

Chat Store

Message history and conversation state:

typescript
// stores/chat.ts
import { createStore } from "solid-js/store";

export interface Message {
  id: string;
  role: "user" | "assistant";
  content: string;
  timestamp: number;
}

const [messages, setMessages] = createStore<Message[]>([]);

export function useChat() {
  return {
    messages,
    addMessage: (msg: Message) => {
      setMessages(m => [...m, msg]);
    },
    clearMessages: () => {
      setMessages([]);
    },
  };
}

Tauri Integration

API Bridge

Communication with the Rust backend:

typescript
// lib/tauri-api.ts
import { invoke } from "@tauri-apps/api/core";
import { listen, UnlistenFn } from "@tauri-apps/api/event";

export async function sendChatMessage(
  conversationId: string,
  content: string,
  onStream: (text: string) => void
): Promise<string> {
  let unlisten: UnlistenFn | undefined;

  try {
    unlisten = await listen<StreamPayload>("chat-stream", (event) => {
      onStream(event.payload.text);
    });

    return await invoke<string>("send_chat_message", {
      conversationId,
      content,
    });
  } finally {
    unlisten?.();
  }
}

Event Handling

Real-time updates from backend:

typescript
// Listening for agent events
const unlisten = await listen<AgentEvent>("agent-event", (event) => {
  switch (event.payload.type) {
    case "text":
      updateContent(event.payload.content);
      break;
    case "tool_start":
      showToolExecution(event.payload.tool);
      break;
    case "tool_end":
      hideToolExecution();
      break;
    case "done":
      completeTask();
      break;
  }
});

Styling

CSS Organization

Each component has its own CSS file:

css
/* Chat.css */
.chat {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 1rem;
}

.chat-input {
  border-top: 1px solid var(--border-color);
  padding: 1rem;
}

Theme Variables

Global CSS variables for theming:

css
:root {
  --primary-color: #6366f1;
  --background-color: #ffffff;
  --text-color: #1f2937;
  --border-color: #e5e7eb;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background-color: #1f2937;
    --text-color: #f9fafb;
    --border-color: #374151;
  }
}

Web Fallback

For development without Tauri:

typescript
// lib/tauri-api.ts
export function isTauri(): boolean {
  return typeof window !== "undefined" &&
    ("__TAURI__" in window || "__TAURI_INTERNALS__" in window);
}

export async function getSettings(): Promise<Settings> {
  if (!isTauri()) {
    // Fallback to localStorage
    const stored = localStorage.getItem("kuse-cowork-settings");
    return stored ? JSON.parse(stored) : DEFAULT_SETTINGS;
  }
  return invoke<Settings>("get_settings");
}

AI Client (Web Mode)

Direct AI provider access for web development:

typescript
// lib/ai-client.ts
class AnthropicProvider implements AIProvider {
  async sendMessage(
    messages: AIMessage[],
    settings: Settings,
    onStream?: (text: string) => void
  ): Promise<string> {
    const response = await fetch(`${settings.baseUrl}/v1/messages`, {
      method: "POST",
      headers: {
        "x-api-key": settings.apiKey,
        "anthropic-version": "2023-06-01",
      },
      body: JSON.stringify({
        model: settings.model,
        messages,
        stream: !!onStream,
      }),
    });

    if (onStream) {
      return this.handleStreamResponse(response, onStream);
    }
    return this.handleResponse(response);
  }
}

Performance Optimizations

Reactive Updates

SolidJS fine-grained reactivity:

tsx
// Only updates when specific signal changes
<Show when={loading()}>
  <Spinner />
</Show>

// Efficient list rendering
<For each={messages()}>
  {(message) => <MessageItem message={message} />}
</For>

Memoization

tsx
const filteredModels = createMemo(() => {
  return AVAILABLE_MODELS.filter(m =>
    m.provider === selectedProvider()
  );
});

Lazy Loading

tsx
const Settings = lazy(() => import("./components/Settings"));

Build Configuration

Vite Config

typescript
// vite.config.ts
export default defineConfig({
  plugins: [solidPlugin()],
  server: {
    port: 1420,
    strictPort: true,
  },
  build: {
    target: "esnext",
    minify: "esbuild",
  },
});

TypeScript Config

json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "jsx": "preserve",
    "jsxImportSource": "solid-js",
    "strict": true
  }
}

Testing

Component Testing

typescript
import { render, screen } from "@solidjs/testing-library";
import { Chat } from "./Chat";

test("renders chat input", () => {
  render(() => <Chat />);
  expect(screen.getByRole("textbox")).toBeInTheDocument();
});

Integration Testing

typescript
test("sends message to backend", async () => {
  const mockInvoke = vi.fn().mockResolvedValue("response");
  vi.mock("@tauri-apps/api/core", () => ({ invoke: mockInvoke }));

  // Test message sending
});

Next Steps

Released under the MIT License.