claude-code streaming chat, desktop remote viewer, new-automation route, tiktok task v4, misc fixes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 20:00:45 +00:00
co-authored by Claude Opus 4.6
parent 0068244356
commit 40a9768cb3
91 changed files with 17872 additions and 85 deletions
@@ -9,13 +9,14 @@ import { appRegistryMetas as projectMetas } from '../apps/Projects';
import { appRegistryMetas as chatHistoryMetas } from '../apps/ChatHistory';
import { appRegistryMetas as previewMetas } from '../apps/Preview';
import { appRegistryMetas as widgetMetas } from '../apps/Widgets';
import { appRegistryMetas as desktopMetas } from '../apps/Desktop';
import { useAppRegistry } from './useAppRegistry';
import { useUserApps } from 'state/useUserApps';
import { createUserAppPanel } from '../apps/UserApp/UserAppPanel';
import { createUserAppHeader } from '../apps/UserApp/UserAppHeader';
import { resolveIcon } from '../utils/resolve-icon';
const apps = [...fileBrowserMetas, ...terminalMetas, ...codeEditorMetas, ...chatMetas, ...fileViewerMetas, ...dashboardMetas, ...projectMetas, ...chatHistoryMetas, ...previewMetas, ...widgetMetas];
const apps = [...fileBrowserMetas, ...terminalMetas, ...codeEditorMetas, ...chatMetas, ...fileViewerMetas, ...dashboardMetas, ...projectMetas, ...chatHistoryMetas, ...previewMetas, ...widgetMetas, ...desktopMetas];
export const AppRegistry = () => {
const { registerApp } = useAppRegistry(apps);
@@ -18,6 +18,7 @@ const PROVIDER_DISPLAY: Record<string, string> = {
bedrock: 'Amazon Bedrock',
'google-vertex': 'Google Vertex AI',
'azure-openai': 'Azure OpenAI',
'claude-code': 'Claude Code',
};
type ModelSelectorProps = {
@@ -0,0 +1,10 @@
import { MonitorSmartphone } from 'lucide-react';
export const DesktopHeader = () => {
return (
<>
<MonitorSmartphone className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium shrink-0">Remote Desktop</span>
</>
);
};
@@ -0,0 +1,140 @@
import type { CSSProperties } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useClient } from 'hooks/useClient';
import { useMounted } from 'hooks/useMounted';
export type DesktopViewProps = {
className?: string;
style?: CSSProperties;
};
const NOVNC_URL = '/novnc/rfb.js';
const buildWsUrl = () => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const token = localStorage.getItem('BEARER_TOKEN') ?? '';
return `${protocol}//${window.location.host}/api/desktop/ws?token=${encodeURIComponent(token)}`;
};
type RFBInstance = {
resizeSession: boolean;
scaleViewport: boolean;
focusOnClick: boolean;
disconnect: () => void;
sendCredentials: (creds: { password: string }) => void;
addEventListener: (type: string, listener: (ev: CustomEvent) => void) => void;
};
let rfbModulePromise: Promise<{ default: new (target: HTMLElement, url: string, options?: { credentials?: { password?: string } }) => RFBInstance }> | null = null;
const loadRFB = () => {
if (!rfbModulePromise) {
rfbModulePromise = import(/* @vite-ignore */ NOVNC_URL) as typeof rfbModulePromise;
}
return rfbModulePromise!;
};
export const DesktopView = ({ className, style }: DesktopViewProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const rfbRef = useRef<RFBInstance | null>(null);
const isMounted = useMounted();
const client = useClient();
const [status, setStatus] = useState<'connecting' | 'connected' | 'disconnected' | 'error'>('connecting');
const [errorMsg, setErrorMsg] = useState('');
useEffect(() => {
if (!isMounted) return;
const container = containerRef.current;
if (!container) return;
let disposed = false;
const connect = async () => {
let password = '';
try {
const res = await client.get<{ password: string }>('/desktop/vnc-password');
password = res.password;
} catch {
if (disposed) return;
setStatus('error');
setErrorMsg('Failed to fetch VNC password');
return;
}
if (disposed) return;
let RFB: Awaited<ReturnType<typeof loadRFB>>['default'];
try {
const mod = await loadRFB();
RFB = mod.default;
} catch (err) {
console.error('[desktop] Failed to load noVNC:', err);
if (disposed) return;
setStatus('error');
setErrorMsg('Failed to load noVNC library');
return;
}
if (disposed) return;
const wsUrl = buildWsUrl();
const rfb = new RFB(container, wsUrl, {
credentials: { password },
});
rfb.resizeSession = true;
rfb.scaleViewport = true;
rfb.focusOnClick = true;
rfbRef.current = rfb;
rfb.addEventListener('connect', () => {
if (!disposed) setStatus('connected');
});
rfb.addEventListener('disconnect', (ev: CustomEvent) => {
if (disposed) return;
setStatus('disconnected');
if (!ev.detail.clean) {
setErrorMsg('Connection lost');
}
});
rfb.addEventListener('credentialsrequired', () => {
rfb.sendCredentials({ password });
});
rfb.addEventListener('securityfailure', (ev: CustomEvent) => {
if (!disposed) {
setStatus('error');
setErrorMsg(ev.detail.reason || 'Security failure');
}
});
};
void connect();
return () => {
disposed = true;
if (rfbRef.current) {
try { rfbRef.current.disconnect(); } catch { /* ignore */ }
rfbRef.current = null;
}
};
}, [isMounted, client]);
return (
<div className={className} style={{ backgroundColor: '#1a1a2e', overflow: 'hidden', position: 'relative', ...style }}>
{status === 'connecting' && (
<div className="absolute inset-0 flex items-center justify-center text-sm text-muted-foreground">
Connecting to desktop...
</div>
)}
{(status === 'disconnected' || status === 'error') && (
<div className="absolute inset-0 flex items-center justify-center text-sm text-muted-foreground">
{errorMsg || 'Disconnected from desktop'}
</div>
)}
<div ref={containerRef} style={{ width: '100%', height: '100%' }} />
</div>
);
};
@@ -0,0 +1,16 @@
import { useAuth } from 'hooks/useAuth';
import { DesktopView } from './DesktopView';
export const DesktopWrapper = () => {
const { user } = useAuth();
if (user?.role !== 'Super Admin') {
return (
<div className="flex h-full w-full items-center justify-center text-sm text-muted-foreground">
Remote Desktop requires Super Admin permissions.
</div>
);
}
return <DesktopView className="h-full w-full" />;
};
@@ -0,0 +1,17 @@
import type { AppRegistryMeta } from '../../AppRegistry';
import { MonitorSmartphone } from 'lucide-react';
import { DesktopWrapper } from './DesktopWrapper';
import { DesktopHeader } from './DesktopHeader';
export { DesktopView, type DesktopViewProps } from './DesktopView';
export const appRegistryMetas: AppRegistryMeta[] = [
{
key: 'officerdev/desktop',
name: 'Remote Desktop',
icon: MonitorSmartphone,
component: DesktopWrapper,
header: DesktopHeader,
availableOnPanel: false,
},
];
@@ -1,4 +1,4 @@
import { Loader2, Folder, ClipboardPaste, FolderPlus, LayoutGrid, Upload, ClipboardCopy, MessageSquare } from 'lucide-react';
import { Loader2, Folder, ClipboardPaste, FolderPlus, LayoutGrid, Upload, ClipboardCopy, MessageSquare, Download } from 'lucide-react';
import { getIcon } from 'material-file-icons';
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu';
import type { UseFileBrowserAppType } from '../useFileBrowserApp';
@@ -124,10 +124,10 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
<LayoutGrid className="mr-2 h-4 w-4" />
Create Dashboard here
</ContextMenuItem>
{/* <ContextMenuItem onClick={() => setShowVideoDownload(true)} className="cursor-pointer">
<ContextMenuItem onClick={() => setShowVideoDownload(true)} className="cursor-pointer">
<Download className="mr-2 h-4 w-4" />
Download video
</ContextMenuItem> */}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)}
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import { X, Play, Square, CircleCheck } from 'lucide-react';
import { X, Play, Square, CircleCheck, CircleX } from 'lucide-react';
import { Dialog, DialogOverlay, DialogPortal } from '@/components/ui/dialog';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { cardStyle } from '@/components/Card';
@@ -54,7 +54,7 @@ const PiMonoInner = ({ defaultInput, cwd, initialModel, taskInfo, sandboxed }: P
const seenResultRef = useRef(false);
const [, bump] = useState(0);
// Track tool/result messages from chat.messages (idempotent during render)
// Track tool/result/error messages from chat.messages (idempotent during render)
for (const m of chat.messages) {
if (m.role === 'tool' && 'toolCallId' in m) {
if (!seenToolIdsRef.current.has(m.toolCallId)) {
@@ -72,6 +72,10 @@ const PiMonoInner = ({ defaultInput, cwd, initialModel, taskInfo, sandboxed }: P
seenResultRef.current = true;
accRef.current.push(m);
}
if (m.role === 'error') {
const alreadyHas = accRef.current.some((a) => a.role === 'error' && 'text' in a && a.text === m.text);
if (!alreadyHas) accRef.current.push(m);
}
}
// Capture assistant text when streaming is committed (streamingText goes non-empty → empty)
@@ -170,6 +174,11 @@ const PiMonoInner = ({ defaultInput, cwd, initialModel, taskInfo, sandboxed }: P
<Square className="h-3.5 w-3.5" />
Stop
</button>
) : accRef.current.some((m) => m.role === 'error') ? (
<span className="flex items-center gap-2 text-sm text-red-500 font-medium">
<CircleX className="h-4 w-4" />
Task failed
</span>
) : (
<span className="flex items-center gap-2 text-sm text-green-500 font-medium">
<CircleCheck className="h-4 w-4" />
+2
View File
@@ -15,6 +15,8 @@ export { FileViewerView, FileViewerProvider, FileViewerHeader, FileViewerBody, F
export type { FileType } from './apps/FileViewer';
export { TerminalView } from './apps/Terminal';
export type { TerminalViewProps } from './apps/Terminal';
export { DesktopView } from './apps/Desktop';
export type { DesktopViewProps } from './apps/Desktop';
export { DashboardListApp, DashboardPreview, SELECTED_DASHBOARD_KEY, CREATING_DASHBOARD_KEY, EDITING_DASHBOARD_KEY, NEW_DASH_NAME_KEY, NEW_DASH_DESC_KEY, NEW_DASH_TEMPLATE_KEY } from './apps/Dashboards';
export { ProjectListApp, ProjectPreview, SELECTED_PROJECT, CREATING_PROJECT, EDITING_PROJECT, NEW_PROJ_NAME, NEW_PROJ_DESC, NEW_PROJ_TEMPLATE, NEW_PROJ_TYPE, NEW_PROJ_HAS_BACKEND, NEW_PROJ_HAS_AUTH, NEW_PROJ_PREVIEW_LAYOUT } from './apps/Projects';
export { createUserAppPanel, createUserAppHeader } from './apps/UserApp';
+2 -1
View File
@@ -60,6 +60,7 @@ export function useVisiblePiModels() {
const allowedProviderSet = new Set(allowed.map((key) => key.split(':')[0]));
return models.filter((m) => {
if (m.provider === 'claude-code') return true;
const isExplicitlyAllowed = allowedSet.has(modelKey(m));
const isFromNewProvider = !allowedProviderSet.has(m.provider);
return isExplicitlyAllowed || isFromNewProvider;
@@ -75,5 +76,5 @@ export function useEnabledPiModels() {
if (allowed.length === 0) return models;
const allowedSet = new Set(allowed);
return models.filter((m) => allowedSet.has(modelKey(m)));
return models.filter((m) => m.provider === 'claude-code' || allowedSet.has(modelKey(m)));
}
+3 -3
View File
@@ -87,8 +87,8 @@ export type UserState = Record<string, unknown>;
export const DEFAULT_SETTINGS: UserSettings = {
chat: {
defaultProvider: 'pi',
defaultModel: null,
defaultProjectModel: null,
defaultModel: 'claude-code',
defaultProjectModel: 'claude-code',
systemPrompt: '',
temperature: 1,
defaultPwd: '~',
@@ -100,7 +100,7 @@ export const DEFAULT_SETTINGS: UserSettings = {
},
tasks: {
defaultProvider: 'pi',
defaultModel: null,
defaultModel: 'claude-code',
},
appearance: {
colorMode: 'light',