extracted chat component to widgets
This commit is contained in:
@@ -40,6 +40,7 @@ export function App() {
|
|||||||
<Route path="/settings/ai" element={<Dashboard.AISettings />} />
|
<Route path="/settings/ai" element={<Dashboard.AISettings />} />
|
||||||
<Route path="/settings/server" element={<Dashboard.ServerSettings />} />
|
<Route path="/settings/server" element={<Dashboard.ServerSettings />} />
|
||||||
<Route path="/settings/resources" element={<Dashboard.ResourceSettings />} />
|
<Route path="/settings/resources" element={<Dashboard.ResourceSettings />} />
|
||||||
|
<Route path="/chat" element={<Dashboard.SessionList />} />
|
||||||
<Route path="/chat/new" element={<Dashboard.NewChat />} />
|
<Route path="/chat/new" element={<Dashboard.NewChat />} />
|
||||||
<Route path="/chat/:sessionId" element={<Dashboard.ClaudeChat />} />
|
<Route path="/chat/:sessionId" element={<Dashboard.ClaudeChat />} />
|
||||||
<Route path="/chat/opencode/new" element={<Dashboard.OpenCodeChat />} />
|
<Route path="/chat/opencode/new" element={<Dashboard.OpenCodeChat />} />
|
||||||
|
|||||||
+3
-6
@@ -1,13 +1,10 @@
|
|||||||
import { useRef, useEffect, useState } from 'react';
|
import { useRef, useEffect, useState } from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router';
|
import { useNavigate, useLocation } from 'react-router';
|
||||||
import { useSessions } from './state/useSessions';
|
import { useSessions, useOpenCodeSessions, useSlashCommands, SessionBar } from 'widgets/ChatHistory';
|
||||||
import type { ModelOption } from '@/state/useModels';
|
import type { ModelOption } from '@/state/useModels';
|
||||||
import { useOpenCodeSessions } from './state/useOpenCodeSessions';
|
import type { useClaude } from './useClaude';
|
||||||
import type { useClaude, Attachment } from 'plugins/Chat/client';
|
import { EmbeddableChat, type Attachment } from './EmbeddableChat';
|
||||||
import { EmbeddableChat } from 'plugins/Chat/client';
|
|
||||||
import { useSlashCommands } from './state/useSlashCommands';
|
|
||||||
import { Card } from '@/components/Card';
|
import { Card } from '@/components/Card';
|
||||||
import { SessionBar } from './SessionBar';
|
|
||||||
|
|
||||||
export type { Attachment };
|
export type { Attachment };
|
||||||
|
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import { toast } from 'sonner';
|
|||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import type { ModelOption } from '@/state/useModels';
|
import type { ModelOption } from '@/state/useModels';
|
||||||
import type { useClaude } from './useClaude';
|
import type { useClaude } from './useClaude';
|
||||||
import { MessageList } from './MessageList';
|
import { MessageList } from 'widgets/Chat';
|
||||||
import { InputArea } from './InputArea';
|
import { InputArea } from './InputArea';
|
||||||
|
|
||||||
export type Attachment =
|
export type Attachment =
|
||||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import type { ModelOption } from '@/state/useModels';
|
import type { ModelOption } from '@/state/useModels';
|
||||||
import type { ChatMessage } from './types';
|
import type { ChatMessage } from 'widgets/Chat';
|
||||||
import type { Attachment } from './EmbeddableChat';
|
import type { Attachment } from './EmbeddableChat';
|
||||||
import { Settings } from './Settings';
|
import { Settings } from './Settings';
|
||||||
|
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { useAuth } from 'hooks/useAuth';
|
import { useAuth } from 'hooks/useAuth';
|
||||||
import type { ModelOption } from '@/state/useModels';
|
import type { ModelOption } from '@/state/useModels';
|
||||||
import type { ChatMessage } from './types';
|
import type { ChatMessage } from 'widgets/Chat';
|
||||||
import { OpenCodeModelPicker } from './OpenCodeModelPicker';
|
import { OpenCodeModelPicker } from './OpenCodeModelPicker';
|
||||||
|
|
||||||
type SettingsProps = {
|
type SettingsProps = {
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useClaude, useOpenCode, type SessionEntry } from 'plugins/Chat/client';
|
import type { SessionEntry } from 'widgets/Chat';
|
||||||
import { ChatPanel } from 'plugins/ChatHistory/client';
|
import { useClaude } from './useClaude';
|
||||||
|
import { useOpenCode } from './useOpenCode';
|
||||||
|
import { ChatPanel } from './ChatPanel';
|
||||||
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
||||||
|
|
||||||
export const ClaudeChat = () => {
|
export const ClaudeChat = () => {
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useChatWebSocket } from 'hooks/useChatWebSocket';
|
import { useChatWebSocket } from 'hooks/useChatWebSocket';
|
||||||
import { useSessions } from 'plugins/ChatHistory/client';
|
import { useSessions } from 'widgets/ChatHistory';
|
||||||
import type { ChatMessage, ServerMessage, TaskInfo } from './types';
|
import type { ChatMessage, ServerMessage, TaskInfo } from 'widgets/Chat';
|
||||||
|
|
||||||
const SAVE_DEBOUNCE_MS = 1000;
|
const SAVE_DEBOUNCE_MS = 1000;
|
||||||
|
|
||||||
+2
-2
@@ -2,8 +2,8 @@ import { useState, useEffect, useRef } from 'react';
|
|||||||
import { useChatWebSocket } from 'hooks/useChatWebSocket';
|
import { useChatWebSocket } from 'hooks/useChatWebSocket';
|
||||||
import { useSettings } from '@/state/useSettings';
|
import { useSettings } from '@/state/useSettings';
|
||||||
import { useVisibleOpenCodeModels } from '@/state/useModels';
|
import { useVisibleOpenCodeModels } from '@/state/useModels';
|
||||||
import { useOpenCodeSessions } from 'plugins/ChatHistory/client';
|
import { useOpenCodeSessions } from 'widgets/ChatHistory';
|
||||||
import type { ChatMessage, ServerMessage, TaskInfo } from './types';
|
import type { ChatMessage, ServerMessage, TaskInfo } from 'widgets/Chat';
|
||||||
|
|
||||||
type UseOpenCodeOptions = {
|
type UseOpenCodeOptions = {
|
||||||
replaceUrl?: boolean;
|
replaceUrl?: boolean;
|
||||||
+1
-2
@@ -3,8 +3,7 @@ import { Link } from 'react-router';
|
|||||||
import { Plus, MessageSquare, Trash2 } from 'lucide-react';
|
import { Plus, MessageSquare, Trash2 } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card } from '@/components/Card';
|
import { Card } from '@/components/Card';
|
||||||
import { useSessions } from './state/useSessions';
|
import { useSessions, useOpenCodeSessions } from 'widgets/ChatHistory';
|
||||||
import { useOpenCodeSessions } from './state/useOpenCodeSessions';
|
|
||||||
|
|
||||||
type Filter = 'all' | 'claude' | 'opencode';
|
type Filter = 'all' | 'claude' | 'opencode';
|
||||||
|
|
||||||
+2
-2
@@ -2,8 +2,8 @@ import { useMemo } from 'react';
|
|||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { MessageSquare, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
import { MessageSquare, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
import { Card } from '@/components/Card';
|
import { Card } from '@/components/Card';
|
||||||
import { useSessions } from './state/useSessions';
|
import { useSessions } from 'widgets/ChatHistory';
|
||||||
import { useOpenCodeSessions } from './state/useOpenCodeSessions';
|
import { useOpenCodeSessions } from 'widgets/ChatHistory';
|
||||||
import { useUserState } from '@/state/useUserState';
|
import { useUserState } from '@/state/useUserState';
|
||||||
|
|
||||||
export const ChatHistory = () => {
|
export const ChatHistory = () => {
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { ChatHistory as ChatHistoryWidget } from './Widget';
|
||||||
|
export { SessionList } from './Screen';
|
||||||
@@ -3,7 +3,10 @@ import { X } from 'lucide-react';
|
|||||||
import { Dialog, DialogOverlay, DialogPortal } from '@/components/ui/dialog';
|
import { Dialog, DialogOverlay, DialogPortal } from '@/components/ui/dialog';
|
||||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { cardStyle } from '@/components/Card';
|
import { cardStyle } from '@/components/Card';
|
||||||
import { useClaude, useOpenCode, EmbeddableChat, type TaskInfo } from 'plugins/Chat/client';
|
import type { TaskInfo } from 'widgets/Chat';
|
||||||
|
import { useClaude } from '@/Screens/Dashboard/Chat/useClaude';
|
||||||
|
import { useOpenCode } from '@/Screens/Dashboard/Chat/useOpenCode';
|
||||||
|
import { EmbeddableChat } from '@/Screens/Dashboard/Chat/EmbeddableChat';
|
||||||
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
||||||
import { useSettings } from '@/state/useSettings';
|
import { useSettings } from '@/state/useSettings';
|
||||||
import type { TaskSummary } from 'widgets/FileBrowser';
|
import type { TaskSummary } from 'widgets/FileBrowser';
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { useSettings } from '@/state/useSettings';
|
import { useSettings } from '@/state/useSettings';
|
||||||
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
||||||
import type { Attachment } from 'plugins/Chat/client';
|
import type { Attachment } from '@/Screens/Dashboard/Chat/EmbeddableChat';
|
||||||
|
|
||||||
export const ChatLauncher = () => {
|
export const ChatLauncher = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ChatLauncher } from './ChatLauncher';
|
import { ChatLauncher } from './ChatLauncher';
|
||||||
import { FileBrowserWidget as FileBrowser } from '@/Screens/Dashboard/Files';
|
import { FileBrowserWidget as FileBrowser } from '@/Screens/Dashboard/Files';
|
||||||
import { Widget as ChatHistory } from 'plugins/ChatHistory/client';
|
import { ChatHistoryWidget as ChatHistory } from '@/Screens/Dashboard/ChatHistory';
|
||||||
import { Catalog } from 'sounds';
|
import { Catalog } from 'sounds';
|
||||||
|
|
||||||
export const HomeScreen = () => {
|
export const HomeScreen = () => {
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } f
|
|||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { useVisibleClaudeModels } from '@/state/useModels';
|
import { useVisibleClaudeModels } from '@/state/useModels';
|
||||||
import { Card } from '@/components/Card';
|
import { Card } from '@/components/Card';
|
||||||
import { useClaude, EmbeddableChat, type ChatMessage } from 'plugins/Chat/client';
|
import type { ChatMessage } from 'widgets/Chat';
|
||||||
|
import { useClaude } from '@/Screens/Dashboard/Chat/useClaude';
|
||||||
|
import { EmbeddableChat } from '@/Screens/Dashboard/Chat/EmbeddableChat';
|
||||||
type ResourceSummary = {
|
type ResourceSummary = {
|
||||||
dirName: string;
|
dirName: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { toast } from 'sonner';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card } from '@/components/Card';
|
import { Card } from '@/components/Card';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { EmbeddableChat, useClaude, useOpenCode } from 'plugins/Chat/client';
|
import { useClaude } from '@/Screens/Dashboard/Chat/useClaude';
|
||||||
|
import { useOpenCode } from '@/Screens/Dashboard/Chat/useOpenCode';
|
||||||
|
import { EmbeddableChat } from '@/Screens/Dashboard/Chat/EmbeddableChat';
|
||||||
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
||||||
|
|
||||||
type AppStatus = {
|
type AppStatus = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Search, AlertCircle, CheckCircle2, Clock, ArrowLeft } from 'lucide-react';
|
import { Search, AlertCircle, CheckCircle2, Clock, ArrowLeft } from 'lucide-react';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { Card } from '@/components/Card';
|
import { Card } from '@/components/Card';
|
||||||
import { MessageBubble, type ChatMessage } from 'plugins/Chat/client';
|
import { MessageBubble, type ChatMessage } from 'widgets/Chat';
|
||||||
|
|
||||||
type LogMetadata = {
|
type LogMetadata = {
|
||||||
filename: string;
|
filename: string;
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ export * from './TaskLogs';
|
|||||||
export * from './Tasks';
|
export * from './Tasks';
|
||||||
export * from './Terminal';
|
export * from './Terminal';
|
||||||
export * from './Files';
|
export * from './Files';
|
||||||
|
export * from './ChatHistory';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useSessions } from 'plugins/ChatHistory/client';
|
import { useSessions } from 'widgets/ChatHistory';
|
||||||
import { usePlans } from './usePlans';
|
import { usePlans } from './usePlans';
|
||||||
import { useSettings } from './useSettings';
|
import { useSettings } from './useSettings';
|
||||||
import { useThemeSync } from './useThemeSync';
|
import { useThemeSync } from './useThemeSync';
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export * from './client';
|
|
||||||
|
|
||||||
export const plugin = {
|
|
||||||
id: 'Chat',
|
|
||||||
name: 'Chat',
|
|
||||||
description: 'Embeddable chat UI',
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export { ChatHistory as Widget } from './Widget';
|
|
||||||
export { SessionList as Screen } from './Screen';
|
|
||||||
export { ChatPanel } from './ChatPanel';
|
|
||||||
export { SessionBar } from './SessionBar';
|
|
||||||
export { useSessions } from './state/useSessions';
|
|
||||||
export { useOpenCodeSessions } from './state/useOpenCodeSessions';
|
|
||||||
export { useSlashCommands, type SlashCommandResult } from './state/useSlashCommands';
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export { Widget, Screen, ChatPanel, SessionBar } from './client';
|
|
||||||
|
|
||||||
export const plugin = {
|
|
||||||
id: 'ChatHistory',
|
|
||||||
name: 'Chat History',
|
|
||||||
description: 'Session management for chat history',
|
|
||||||
};
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
# Building a Plugin
|
|
||||||
|
|
||||||
Plugins are self-contained modules that extend Officer with new features. They live under `src/workspaces/plugins/` and are auto-discovered at startup — no registration step required.
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/workspaces/plugins/
|
|
||||||
├── package.json # Workspace exports (update when adding a plugin)
|
|
||||||
└── MyPlugin/
|
|
||||||
├── index.ts # Metadata + re-exports
|
|
||||||
├── server/
|
|
||||||
│ ├── index.ts # Exports router and apiPath
|
|
||||||
│ └── router.ts # Hono API routes
|
|
||||||
└── client/
|
|
||||||
├── index.ts # Exports Widget and/or Screen
|
|
||||||
├── Widget.tsx # Compact component (dashboard home)
|
|
||||||
├── Screen/
|
|
||||||
│ └── index.tsx # Full-page component (dedicated route)
|
|
||||||
└── state/
|
|
||||||
└── useMyPlugin.ts # API client hook
|
|
||||||
```
|
|
||||||
|
|
||||||
A plugin can be server-only, client-only, or both. The discovery system checks for the presence of `server/index.ts` and `client/index.ts` to determine what the plugin provides.
|
|
||||||
|
|
||||||
## Step-by-Step
|
|
||||||
|
|
||||||
### 1. Create the plugin directory
|
|
||||||
|
|
||||||
```
|
|
||||||
mkdir -p src/workspaces/plugins/MyPlugin/{server,client/state,client/Screen}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Plugin metadata — `MyPlugin/index.ts`
|
|
||||||
|
|
||||||
Every plugin must export a `plugin` object with metadata. This is read by the settings UI and the `/plugins` API.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export { Widget, Screen } from './client';
|
|
||||||
export { router, apiPath } from './server';
|
|
||||||
|
|
||||||
export const plugin = {
|
|
||||||
id: 'MyPlugin', // Must match directory name
|
|
||||||
name: 'My Plugin', // Display name in settings
|
|
||||||
description: 'What this plugin does',
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
If your plugin is server-only, omit the client export. If client-only, omit the server export.
|
|
||||||
|
|
||||||
### 3. Server router — `MyPlugin/server/`
|
|
||||||
|
|
||||||
**`server/index.ts`** — Exports the router instance and the API path prefix:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export * from './router';
|
|
||||||
|
|
||||||
export const apiPath = 'my-plugin';
|
|
||||||
```
|
|
||||||
|
|
||||||
The `apiPath` determines the URL prefix. This router gets mounted at `/api/my-plugin`.
|
|
||||||
|
|
||||||
**`server/router.ts`** — Define your API endpoints:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { createRouter } from '@@/create-router';
|
|
||||||
import * as errors from '@@/custom-errors';
|
|
||||||
|
|
||||||
export const router = createRouter();
|
|
||||||
|
|
||||||
router.get('/items', async (ctx) => {
|
|
||||||
const user = ctx.get('user'); // Authenticated user
|
|
||||||
// ...
|
|
||||||
return ctx.json({ items: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/items', async (ctx) => {
|
|
||||||
const body = ctx.get('body'); // Parsed request body
|
|
||||||
if (!body.name) throw errors.BAD_REQUEST('Name is required');
|
|
||||||
// ...
|
|
||||||
return ctx.json({ created: true });
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Key points:
|
|
||||||
- `createRouter()` from `@@/create-router` gives you a typed Hono router
|
|
||||||
- All plugin routes are **protected** — user authentication is enforced automatically
|
|
||||||
- Access the authenticated user with `ctx.get('user')` (returns `User` from types)
|
|
||||||
- Access parsed body with `ctx.get('body')`
|
|
||||||
- Throw `CustomError` instances for error responses — they're caught by the global error handler
|
|
||||||
|
|
||||||
**Available error helpers** (`@@/custom-errors`):
|
|
||||||
- `BAD_REQUEST(msg?)` — 400
|
|
||||||
- `UNAUTHORIZED(msg?)` — 401
|
|
||||||
- `FORBIDDEN(msg?)` — 403
|
|
||||||
- `NOT_FOUND(msg?)` — 404
|
|
||||||
- `CONFLICT(msg?)` — 409
|
|
||||||
- `INTERNAL_SERVER_ERROR(msg?)` — 500
|
|
||||||
- `TOO_MANY_REQUESTS(msg?, retryAfter?)` — 429
|
|
||||||
|
|
||||||
**User data helpers** (`@@/data-path`):
|
|
||||||
- `DATA_PATH` — base data directory
|
|
||||||
- `getHomeDir(email)` — user's home directory
|
|
||||||
|
|
||||||
### 4. Client components — `MyPlugin/client/`
|
|
||||||
|
|
||||||
**`client/index.ts`** — Export your components with standardized names:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export { MyPluginWidget as Widget } from './Widget';
|
|
||||||
export { MyPluginScreen as Screen } from './Screen';
|
|
||||||
```
|
|
||||||
|
|
||||||
**`client/state/useMyPlugin.ts`** — API client hook:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { useClient } from 'hooks/useClient';
|
|
||||||
|
|
||||||
type Item = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useMyPlugin = () => {
|
|
||||||
const client = useClient();
|
|
||||||
|
|
||||||
return {
|
|
||||||
listItems: () => client.get<Item[]>('/my-plugin/items'),
|
|
||||||
createItem: (name: string) => client.post('/my-plugin/items', { name }),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
The `useClient()` hook provides an authenticated HTTP client. The path must match your `apiPath` from the server.
|
|
||||||
|
|
||||||
**`client/Widget.tsx`** — Compact component for the dashboard home:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useMyPlugin } from './state/useMyPlugin';
|
|
||||||
|
|
||||||
export const MyPluginWidget = () => {
|
|
||||||
// Widget implementation
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**`client/Screen/index.tsx`** — Full-page component:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { DashboardLayout } from '@/Screens/Dashboard/Layout';
|
|
||||||
|
|
||||||
export const MyPluginScreen = () => {
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
{/* Screen implementation */}
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Register workspace exports — `plugins/package.json`
|
|
||||||
|
|
||||||
Add subpath exports so the monorepo can import your plugin:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"exports": {
|
|
||||||
"./FileBrowser": "./FileBrowser/index.ts",
|
|
||||||
"./FileBrowser/client": "./FileBrowser/client/index.ts",
|
|
||||||
"./FileBrowser/server": "./FileBrowser/server/index.ts",
|
|
||||||
"./MyPlugin": "./MyPlugin/index.ts",
|
|
||||||
"./MyPlugin/client": "./MyPlugin/client/index.ts",
|
|
||||||
"./MyPlugin/server": "./MyPlugin/server/index.ts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This lets other code import your plugin as:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { Widget, Screen } from 'plugins/MyPlugin/client';
|
|
||||||
import { router, apiPath } from 'plugins/MyPlugin/server';
|
|
||||||
import { plugin } from 'plugins/MyPlugin';
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Wire into the app (optional)
|
|
||||||
|
|
||||||
The server router is auto-discovered and mounted — no changes needed. But if your plugin has client components that should appear in the main app, you'll need to add them manually:
|
|
||||||
|
|
||||||
**Route** — `src/apps/officer-web/App.tsx`:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { Screen as MyPluginScreen } from 'plugins/MyPlugin/client';
|
|
||||||
|
|
||||||
// Inside the authenticated routes:
|
|
||||||
{plugins?.MyPlugin !== false && <Route path="/my-plugin" element={<MyPluginScreen />} />}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nav item** — `src/apps/officer-web/Screens/Dashboard/Layout.tsx`:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
{plugins?.MyPlugin !== false && (
|
|
||||||
<DropdownMenuItem asChild className="cursor-pointer">
|
|
||||||
<Link to="/my-plugin">
|
|
||||||
<SomeIcon className="mr-2 h-4 w-4" />
|
|
||||||
My Plugin
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Home widget** — `src/apps/officer-web/Screens/Dashboard/Home/index.tsx`:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { Widget as MyPluginWidget } from 'plugins/MyPlugin/client';
|
|
||||||
|
|
||||||
// Inside the layout:
|
|
||||||
{plugins?.MyPlugin !== false && <MyPluginWidget />}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `plugins` object comes from `useServerSettings()` and is `undefined` when no overrides exist — so `plugins?.MyPlugin !== false` defaults to showing the plugin (opt-out model).
|
|
||||||
|
|
||||||
## Enable/Disable
|
|
||||||
|
|
||||||
Plugins are **enabled by default**. Admins can disable them in Server Settings > Plugins, which writes to `~/.config/officer.dev/server-settings.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"plugins": {
|
|
||||||
"MyPlugin": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
When disabled:
|
|
||||||
- **Server-side**: The plugin router is not mounted (requires server restart)
|
|
||||||
- **Client-side**: Routes, nav items, and widgets are hidden immediately
|
|
||||||
|
|
||||||
## Import Aliases
|
|
||||||
|
|
||||||
| Alias | Resolves to | Use in |
|
|
||||||
|-------|-------------|--------|
|
|
||||||
| `@@/` | `src/servers/` | Server code (`createRouter`, `custom-errors`, `data-path`) |
|
|
||||||
| `@/` | `src/apps/officer-web/` | Client code (`components/ui/*`, `Screens/*`, `state/*`) |
|
|
||||||
| `hooks/` | `src/workspaces/hooks/src/` | Both (`useClient`, `useAuth`) |
|
|
||||||
| `types` | `src/workspaces/types/` | Both |
|
|
||||||
| `config` | `src/workspaces/config/` | Client code |
|
|
||||||
| `plugins/` | `src/workspaces/plugins/` | Both |
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- [ ] `MyPlugin/index.ts` exports `plugin` metadata with `id` matching directory name
|
|
||||||
- [ ] `MyPlugin/server/index.ts` exports `router` and `apiPath`
|
|
||||||
- [ ] `MyPlugin/client/index.ts` exports `Widget` and/or `Screen`
|
|
||||||
- [ ] `plugins/package.json` has subpath exports for the new plugin
|
|
||||||
- [ ] App routes gated with `plugins?.MyPlugin !== false`
|
|
||||||
- [ ] Nav items gated with `plugins?.MyPlugin !== false`
|
|
||||||
- [ ] Home widgets gated with `plugins?.MyPlugin !== false`
|
|
||||||
- [ ] `bunx tsgo` — no new type errors
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "plugins",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"exports": {
|
|
||||||
"./Chat": "./Chat/index.ts",
|
|
||||||
"./Chat/client": "./Chat/client/index.ts",
|
|
||||||
"./ChatHistory": "./ChatHistory/index.ts",
|
|
||||||
"./ChatHistory/client": "./ChatHistory/client/index.ts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
export { EmbeddableChat, type Attachment } from './EmbeddableChat';
|
|
||||||
export { MessageList } from './MessageList';
|
export { MessageList } from './MessageList';
|
||||||
export { InputArea } from './InputArea';
|
|
||||||
export { Settings } from './Settings';
|
|
||||||
export { MessageBubble, StreamingBubble } from './MessageBubble';
|
export { MessageBubble, StreamingBubble } from './MessageBubble';
|
||||||
export { ToolActivity } from './ToolActivity';
|
export { ToolActivity } from './ToolActivity';
|
||||||
export { QuestionActivity } from './QuestionActivity';
|
export { QuestionActivity } from './QuestionActivity';
|
||||||
export { OpenCodeModelPicker } from './OpenCodeModelPicker';
|
|
||||||
export { useClaude } from './useClaude';
|
|
||||||
export { useOpenCode } from './useOpenCode';
|
|
||||||
export type { ChatMessage, SessionEntry, ServerMessage, TaskInfo } from './types';
|
export type { ChatMessage, SessionEntry, ServerMessage, TaskInfo } from './types';
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { SessionBar } from './SessionBar';
|
||||||
|
export { useSessions } from './useSessions';
|
||||||
|
export { useOpenCodeSessions } from './useOpenCodeSessions';
|
||||||
|
export { useSlashCommands, type SlashCommandResult } from './useSlashCommands';
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { SessionEntry, ChatMessage } from 'plugins/Chat/client';
|
import type { SessionEntry, ChatMessage } from 'widgets/Chat';
|
||||||
import { useAuth } from 'hooks/useAuth';
|
import { useAuth } from 'hooks/useAuth';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { SessionEntry, ChatMessage } from 'plugins/Chat/client';
|
import type { SessionEntry, ChatMessage } from 'widgets/Chat';
|
||||||
import type { SlashCommandResult } from './useSlashCommands';
|
import type { SlashCommandResult } from './useSlashCommands';
|
||||||
import { useAuth } from 'hooks/useAuth';
|
import { useAuth } from 'hooks/useAuth';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"exports": {
|
"exports": {
|
||||||
"./Terminal": "./Terminal/index.ts",
|
"./Terminal": "./Terminal/index.ts",
|
||||||
"./FileBrowser": "./FileBrowser/index.ts"
|
"./FileBrowser": "./FileBrowser/index.ts",
|
||||||
|
"./ChatHistory": "./ChatHistory/index.ts",
|
||||||
|
"./Chat": "./Chat/index.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user