settings refactor
This commit is contained in:
@@ -4,7 +4,7 @@ import { useServerSettings } from '@/state/useServerSettings';
|
|||||||
import { useInitialData } from '@/state/useInitialData';
|
import { useInitialData } from '@/state/useInitialData';
|
||||||
import { LandingPage, AuthLayout } from './Screens/LandingPage';
|
import { LandingPage, AuthLayout } from './Screens/LandingPage';
|
||||||
import { Home } from './Screens/Dashboard/Home';
|
import { Home } from './Screens/Dashboard/Home';
|
||||||
import { Profile } from './Screens/Dashboard/Profile';
|
import { ProfileSettings } from './Screens/Dashboard/Settings/ProfileSettings';
|
||||||
import { ClaudeSessions, ClaudeChat, OpenCodeChat, NewChat } from './Screens/Dashboard/Chat';
|
import { ClaudeSessions, ClaudeChat, OpenCodeChat, NewChat } from './Screens/Dashboard/Chat';
|
||||||
import { Plans } from './Screens/Dashboard/Plans';
|
import { Plans } from './Screens/Dashboard/Plans';
|
||||||
import { Skills } from './Screens/Dashboard/Skills';
|
import { Skills } from './Screens/Dashboard/Skills';
|
||||||
@@ -15,8 +15,8 @@ import { SignoutScreen } from './Screens/Dashboard/SignoutScreen';
|
|||||||
import { Screen as Files } from 'plugins/FileBrowser/client';
|
import { Screen as Files } from 'plugins/FileBrowser/client';
|
||||||
import { Screen as Terminal } from 'plugins/Terminal/client';
|
import { Screen as Terminal } from 'plugins/Terminal/client';
|
||||||
import { AISettings } from './Screens/Dashboard/Settings/AISettings';
|
import { AISettings } from './Screens/Dashboard/Settings/AISettings';
|
||||||
import { ServerSettings } from './Screens/Dashboard/ServerSettings';
|
import { ServerSettings } from './Screens/Dashboard/Settings/ServerSettings';
|
||||||
import { Applications } from './Screens/Dashboard/Applications';
|
import { ResourceSettings } from './Screens/Dashboard/Settings/ResourceSettings';
|
||||||
import { OnboardingAdmin } from './Screens/Dashboard/OnboardingAdmin';
|
import { OnboardingAdmin } from './Screens/Dashboard/OnboardingAdmin';
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
@@ -45,7 +45,10 @@ export function App() {
|
|||||||
{isAuthenticated && onboardingComplete && (
|
{isAuthenticated && onboardingComplete && (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/settings/profile" element={<Profile />} />
|
<Route path="/settings/profile" element={<ProfileSettings />} />
|
||||||
|
<Route path="/settings/ai" element={<AISettings />} />
|
||||||
|
<Route path="/settings/server" element={<ServerSettings />} />
|
||||||
|
<Route path="/settings/resources" element={<ResourceSettings />} />
|
||||||
<Route path="/chat" element={<ClaudeSessions />} />
|
<Route path="/chat" element={<ClaudeSessions />} />
|
||||||
<Route path="/chat/new" element={<NewChat />} />
|
<Route path="/chat/new" element={<NewChat />} />
|
||||||
<Route path="/chat/:sessionId" element={<ClaudeChat />} />
|
<Route path="/chat/:sessionId" element={<ClaudeChat />} />
|
||||||
@@ -53,9 +56,6 @@ export function App() {
|
|||||||
<Route path="/chat/opencode/:sessionId" element={<OpenCodeChat />} />
|
<Route path="/chat/opencode/:sessionId" element={<OpenCodeChat />} />
|
||||||
{plugins?.FileBrowser !== false && <Route path="/files" element={<Files />} />}
|
{plugins?.FileBrowser !== false && <Route path="/files" element={<Files />} />}
|
||||||
{plugins?.Terminal !== false && <Route path="/terminal" element={<Terminal />} />}
|
{plugins?.Terminal !== false && <Route path="/terminal" element={<Terminal />} />}
|
||||||
<Route path="/settings/ai" element={<AISettings />} />
|
|
||||||
<Route path="/settings/server" element={<ServerSettings />} />
|
|
||||||
<Route path="/settings/applications" element={<Applications />} />
|
|
||||||
<Route path="/plans" element={<Plans />} />
|
<Route path="/plans" element={<Plans />} />
|
||||||
<Route path="/skills" element={<Skills />} />
|
<Route path="/skills" element={<Skills />} />
|
||||||
<Route path="/tasks" element={<Tasks />} />
|
<Route path="/tasks" element={<Tasks />} />
|
||||||
|
|||||||
@@ -114,9 +114,9 @@ export function DashboardLayout({ children, mobileFull }: DashboardLayoutProps)
|
|||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild className="cursor-pointer">
|
<DropdownMenuItem asChild className="cursor-pointer">
|
||||||
<Link to="/settings/applications">
|
<Link to="/settings/resources">
|
||||||
<Package className="mr-2 h-4 w-4" />
|
<Package className="mr-2 h-4 w-4" />
|
||||||
Applications
|
Resources
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild className="cursor-pointer">
|
<DropdownMenuItem asChild className="cursor-pointer">
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@ import { Search, User, Lock, Globe, ListChecks } from 'lucide-react';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion';
|
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion';
|
||||||
import { Card } from '@/components/Card';
|
import { Card } from '@/components/Card';
|
||||||
import { DashboardLayout } from '../Layout';
|
import { DashboardLayout } from '../../Layout';
|
||||||
import { UserData } from './UserData';
|
import { UserData } from './UserData';
|
||||||
import { ChangePassword } from './ChangePassword';
|
import { ChangePassword } from './ChangePassword';
|
||||||
import { Languages } from './Languages';
|
import { Languages } from './Languages';
|
||||||
@@ -42,7 +42,7 @@ const sections = [
|
|||||||
|
|
||||||
const allKeys = sections.map((s) => s.key);
|
const allKeys = sections.map((s) => s.key);
|
||||||
|
|
||||||
export const Profile = () => {
|
export const ProfileSettings = () => {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [expanded, setExpanded] = useState<string[]>([]);
|
const [expanded, setExpanded] = useState<string[]>([]);
|
||||||
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||||
+2
-2
@@ -5,7 +5,7 @@ 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 { DashboardLayout } from '../Layout';
|
import { DashboardLayout } from '../../Layout';
|
||||||
|
|
||||||
type AppStatus = {
|
type AppStatus = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -43,7 +43,7 @@ const CopyCommand = ({ command }: { command: string }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Applications = () => {
|
export const ResourceSettings = () => {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [actionInProgress, setActionInProgress] = useState<string | null>(null);
|
const [actionInProgress, setActionInProgress] = useState<string | null>(null);
|
||||||
+1
-1
@@ -3,7 +3,7 @@ import { Search, Terminal, Puzzle, Shield } from 'lucide-react';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion';
|
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion';
|
||||||
import { Card } from '@/components/Card';
|
import { Card } from '@/components/Card';
|
||||||
import { DashboardLayout } from '../Layout';
|
import { DashboardLayout } from '../../Layout';
|
||||||
import { AIHarnessesSection } from './AIHarnessesSection';
|
import { AIHarnessesSection } from './AIHarnessesSection';
|
||||||
import { PluginsSection } from './PluginsSection';
|
import { PluginsSection } from './PluginsSection';
|
||||||
import { TerminalSection } from './TerminalSection';
|
import { TerminalSection } from './TerminalSection';
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
# Terminal Plugin Spec
|
||||||
|
|
||||||
|
This document describes the Terminal plugin implementation, architecture, and integration points. It is a reference for continuing work later.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Provide a browser terminal (xterm.js) inside Officer.
|
||||||
|
- Support two execution modes:
|
||||||
|
- Host mode: shell runs on the host machine.
|
||||||
|
- Sandbox mode: shell runs inside a Docker sidecar container.
|
||||||
|
- Keep host mode working and optional; sandbox is enabled via admin settings.
|
||||||
|
- Keep all terminal logic self-contained in the plugin.
|
||||||
|
- Allow the terminal UI to be rendered anywhere with a reusable component.
|
||||||
|
|
||||||
|
## File Layout
|
||||||
|
|
||||||
|
- `src/workspaces/plugins/Terminal/index.ts`
|
||||||
|
- Exports plugin metadata, client screen, and server websocket handler.
|
||||||
|
- `src/workspaces/plugins/Terminal/client/TerminalView.tsx`
|
||||||
|
- Reusable terminal component (xterm + websocket bridge).
|
||||||
|
- `src/workspaces/plugins/Terminal/client/Screen/index.tsx`
|
||||||
|
- Page wrapper that renders `TerminalView` inside `DashboardLayout`.
|
||||||
|
- `src/workspaces/plugins/Terminal/client/index.ts`
|
||||||
|
- Exports `Screen` and `TerminalView`.
|
||||||
|
- `src/workspaces/plugins/Terminal/server/websocket.ts`
|
||||||
|
- WebSocket handler for terminal sessions and sidecar orchestration.
|
||||||
|
- `src/workspaces/plugins/Terminal/server/pty-sidecar.mjs`
|
||||||
|
- Node sidecar process that owns `node-pty` and shell spawn.
|
||||||
|
- `src/workspaces/plugins/Terminal/server/Dockerfile.terminal-sidecar`
|
||||||
|
- Docker image for sandbox mode.
|
||||||
|
- `src/workspaces/plugins/Terminal/server/templates/.zshrc`
|
||||||
|
- `src/workspaces/plugins/Terminal/server/templates/starship-officer.toml`
|
||||||
|
- Template configs copied into user home for stable shell UX.
|
||||||
|
|
||||||
|
## Client Architecture
|
||||||
|
|
||||||
|
### TerminalView
|
||||||
|
|
||||||
|
`TerminalView` is the reusable component that can be rendered anywhere.
|
||||||
|
|
||||||
|
Props:
|
||||||
|
|
||||||
|
- `className?: string`
|
||||||
|
- `style?: CSSProperties`
|
||||||
|
- `wsPath?: string` (default: `/api/terminal/ws`)
|
||||||
|
- `fontSize?: number` (default: 14)
|
||||||
|
- `fontFamily?: string`
|
||||||
|
- `theme?: { background?; foreground?; cursor?; selectionBackground? }`
|
||||||
|
- `autoFocus?: boolean` (default: true)
|
||||||
|
- `onReady?: (term: XTerm) => void`
|
||||||
|
- `onExit?: () => void`
|
||||||
|
- `onDisconnect?: () => void`
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
|
||||||
|
- Initializes xterm after mount for StrictMode compatibility.
|
||||||
|
- Connects to websocket at `wsPath`, appending `token` from localStorage.
|
||||||
|
- Sends `resize` on open, and on ResizeObserver changes.
|
||||||
|
- Forwards terminal input as `{ type: 'input', data }` JSON.
|
||||||
|
- Writes server output on `{ type: 'output', data }`.
|
||||||
|
- Emits `onExit` when `{ type: 'exit' }` is received.
|
||||||
|
- Emits `onDisconnect` when the websocket closes.
|
||||||
|
|
||||||
|
### Screen
|
||||||
|
|
||||||
|
`Screen` is a thin wrapper around `TerminalView` for the full-page route:
|
||||||
|
|
||||||
|
```
|
||||||
|
<DashboardLayout>
|
||||||
|
<TerminalView className="h-full w-full p-2" />
|
||||||
|
</DashboardLayout>
|
||||||
|
```
|
||||||
|
|
||||||
|
This matches plugin conventions used by FileBrowser.
|
||||||
|
|
||||||
|
## Server Architecture
|
||||||
|
|
||||||
|
### WebSocket Flow
|
||||||
|
|
||||||
|
The terminal websocket is mounted in `src/server.tsx`:
|
||||||
|
|
||||||
|
- Path: `/api/terminal/ws`
|
||||||
|
- Auth: `token` query parameter, validated with `verify()`.
|
||||||
|
- Server handler: `terminalWebsocket` from `plugins/Terminal/server`.
|
||||||
|
|
||||||
|
Message formats between client and server:
|
||||||
|
|
||||||
|
- Client -> server:
|
||||||
|
- `{ type: 'input', data: string }`
|
||||||
|
- `{ type: 'resize', cols: number, rows: number }`
|
||||||
|
- Server -> client:
|
||||||
|
- `{ type: 'output', data: string }`
|
||||||
|
- `{ type: 'exit' }`
|
||||||
|
|
||||||
|
### Sidecar Model
|
||||||
|
|
||||||
|
The Bun server does not spawn PTYs directly. Instead it proxies to a Node sidecar
|
||||||
|
that runs `node-pty` for reliability.
|
||||||
|
|
||||||
|
There are two sidecar modes:
|
||||||
|
|
||||||
|
1. Host mode
|
||||||
|
- Spawns a local Node sidecar on the host via Bun.
|
||||||
|
- Sidecar binds to `127.0.0.1`.
|
||||||
|
|
||||||
|
2. Docker sandbox mode
|
||||||
|
- Runs a Docker container per user.
|
||||||
|
- Container binds to `0.0.0.0` and is mapped to a host port.
|
||||||
|
- Container runs the same Node sidecar and shell inside the container.
|
||||||
|
|
||||||
|
### Mode Selection
|
||||||
|
|
||||||
|
- Settings are stored in `~/.config/officer.dev/server-settings.json`.
|
||||||
|
- `terminalSandboxed: true` enables Docker mode.
|
||||||
|
- If `terminalSandboxed` is falsy, host mode is used.
|
||||||
|
|
||||||
|
### Sidecar Handshake
|
||||||
|
|
||||||
|
On websocket open:
|
||||||
|
|
||||||
|
- Determine user home: `getHomeDir(email)`.
|
||||||
|
- Ensure the home directory exists.
|
||||||
|
- Determine mode (host or docker).
|
||||||
|
- Connect to sidecar on a port (host: default 5337; docker: user-specific).
|
||||||
|
- Send init payload to sidecar:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
type: 'init',
|
||||||
|
shell: { command, args, name },
|
||||||
|
cwd,
|
||||||
|
homeDir,
|
||||||
|
userLabel
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `userLabel` is the email used by the prompt template.
|
||||||
|
- `cwd` and `homeDir` are `/home/officer` in Docker, or the user home on host.
|
||||||
|
|
||||||
|
### Docker Container Lifecycle
|
||||||
|
|
||||||
|
Containers are named `officer-terminal-${userId}` and stored per user in:
|
||||||
|
|
||||||
|
`data/terminal-containers.json`
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Reuse an existing container if it exists and is running.
|
||||||
|
- Restart the container if it exists but stopped.
|
||||||
|
- Start a new container if none exists; allocate a port starting at 54000.
|
||||||
|
- Containers are left running for reuse when websocket closes.
|
||||||
|
|
||||||
|
### Docker Image
|
||||||
|
|
||||||
|
- Tag: `officer-terminal-sidecar:v1`
|
||||||
|
- Built from `Dockerfile.terminal-sidecar` when missing.
|
||||||
|
|
||||||
|
Includes:
|
||||||
|
|
||||||
|
- zsh, git, curl, ca-certificates
|
||||||
|
- oh-my-zsh
|
||||||
|
- starship
|
||||||
|
- fortune-mod, cowsay
|
||||||
|
- eza
|
||||||
|
- node, ws, node-pty
|
||||||
|
|
||||||
|
### Shell Configuration
|
||||||
|
|
||||||
|
Templates are copied into the user's home if missing:
|
||||||
|
|
||||||
|
- `.zshrc`
|
||||||
|
- `.config/starship-officer.toml`
|
||||||
|
|
||||||
|
This provides a stable base for all users while allowing per-user customization.
|
||||||
|
|
||||||
|
Prompt requirement:
|
||||||
|
|
||||||
|
- Format: `email@domain:officer.dev`
|
||||||
|
- Email colored in button blue (#0891B2)
|
||||||
|
|
||||||
|
## Plugin Conventions
|
||||||
|
|
||||||
|
- `index.ts` exports `plugin` metadata with `id` matching folder name.
|
||||||
|
- Client exports use `Widget`/`Screen` conventions. Terminal only exports `Screen`
|
||||||
|
and `TerminalView` (no widget).
|
||||||
|
- Server exports a websocket handler (not a router). This is a special-case
|
||||||
|
plugin that is mounted in `src/server.tsx`.
|
||||||
|
|
||||||
|
## Known Constraints
|
||||||
|
|
||||||
|
- Terminal mode is global via server settings; no per-component mode selection.
|
||||||
|
- `TerminalView` builds the websocket URL from `window.location` and localStorage
|
||||||
|
token; custom auth is not supported yet.
|
||||||
|
- Sidecar connection retries are bounded; errors are printed to the terminal.
|
||||||
|
|
||||||
|
## Potential Future Enhancements
|
||||||
|
|
||||||
|
- Allow `TerminalView` to request mode/cwd via websocket init payload.
|
||||||
|
- Add a read-only mode or initial command support.
|
||||||
|
- Add per-component theme presets or allow app-level defaults.
|
||||||
|
- Add better telemetry/logging for sidecar startup failures.
|
||||||
|
|
||||||
|
## API Contract
|
||||||
|
|
||||||
|
This section describes the messages across each layer.
|
||||||
|
|
||||||
|
### Client <-> Server (Bun)
|
||||||
|
|
||||||
|
Client -> server:
|
||||||
|
|
||||||
|
- `input`
|
||||||
|
- `{ type: 'input', data: string }`
|
||||||
|
- `data` is raw terminal input from xterm.
|
||||||
|
- `resize`
|
||||||
|
- `{ type: 'resize', cols: number, rows: number }`
|
||||||
|
|
||||||
|
Server -> client:
|
||||||
|
|
||||||
|
- `output`
|
||||||
|
- `{ type: 'output', data: string }`
|
||||||
|
- `data` is raw PTY output to be written to xterm.
|
||||||
|
- `exit`
|
||||||
|
- `{ type: 'exit' }`
|
||||||
|
- Signals terminal process termination.
|
||||||
|
|
||||||
|
### Server <-> Sidecar (Node)
|
||||||
|
|
||||||
|
Server -> sidecar:
|
||||||
|
|
||||||
|
- `init`
|
||||||
|
- `{ type: 'init', shell, cwd, homeDir, userLabel }`
|
||||||
|
- `shell`: `{ command: string; args: string[]; name: string }`
|
||||||
|
- `cwd`: initial current working directory
|
||||||
|
- `homeDir`: home directory to set in the sidecar
|
||||||
|
- `userLabel`: email for the prompt template
|
||||||
|
- `input`
|
||||||
|
- `{ type: 'input', data: string }`
|
||||||
|
- `resize`
|
||||||
|
- `{ type: 'resize', cols: number, rows: number }`
|
||||||
|
|
||||||
|
Sidecar -> server:
|
||||||
|
|
||||||
|
- `output`
|
||||||
|
- `{ type: 'output', data: string }`
|
||||||
|
- `exit`
|
||||||
|
- `{ type: 'exit' }`
|
||||||
|
|
||||||
|
## Sequence Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
Client (xterm) Bun Server Sidecar (Node) Shell
|
||||||
|
| | | |
|
||||||
|
| WS connect | | |
|
||||||
|
|---------------> | | |
|
||||||
|
| | connect sidecar | |
|
||||||
|
| |-------------------> | |
|
||||||
|
| | init (shell/cwd) | |
|
||||||
|
| |-------------------> | spawn PTY |
|
||||||
|
| | |----------------->|
|
||||||
|
| input | | |
|
||||||
|
|---------------> | input | |
|
||||||
|
| |-------------------> | write to PTY |
|
||||||
|
| output | | |
|
||||||
|
|<--------------- | output | read from PTY |
|
||||||
|
| |<------------------- | |
|
||||||
|
| resize | | |
|
||||||
|
|---------------> | resize | |
|
||||||
|
| |-------------------> | set PTY size |
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Troubleshooting
|
||||||
|
|
||||||
|
Common failure modes and checks:
|
||||||
|
|
||||||
|
- Docker not installed or not in PATH
|
||||||
|
- `ensureDockerImage()` will throw. Install Docker or fix PATH.
|
||||||
|
|
||||||
|
- Image build fails
|
||||||
|
- Build uses `Dockerfile.terminal-sidecar` and tag `officer-terminal-sidecar:v1`.
|
||||||
|
- Check for network access (apt), and that build context is the plugin folder.
|
||||||
|
|
||||||
|
- Container starts but sidecar is unreachable
|
||||||
|
- Container binds to `0.0.0.0`, host maps `127.0.0.1:${port}:${port}`.
|
||||||
|
- Port is user-specific, stored in `data/terminal-containers.json`.
|
||||||
|
- Verify with `docker ps` and `docker logs` for the container name.
|
||||||
|
|
||||||
|
- Permissions inside container
|
||||||
|
- Container runs with host UID/GID derived from the mounted user home.
|
||||||
|
- If home dir ownership is incorrect, zsh history may fail to write.
|
||||||
|
|
||||||
|
- Shell missing
|
||||||
|
- Container must have zsh installed; the Dockerfile includes zsh.
|
||||||
|
- Host mode resolves shell from `$SHELL` or known paths.
|
||||||
|
|
||||||
|
## Per-User Template Rules
|
||||||
|
|
||||||
|
Templates are copied into user home on first launch only.
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- `.zshrc`
|
||||||
|
- `.config/starship-officer.toml`
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- If a file already exists, it is not overwritten.
|
||||||
|
- This lets the admin provide a stable default while allowing user customization.
|
||||||
|
- To reset a user to defaults, delete the per-user files and reconnect.
|
||||||
Reference in New Issue
Block a user