Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
import { useCallback, useReducer, useRef, useState } from 'react';
import { ChatPanel } from './components/ChatPanel';
import { ProjectStaleBanner } from './components/ProjectStaleBanner';
import { ProviderBar } from './components/ProviderBar';
import { SettingsPanel } from './components/SettingsPanel';
import { useWebSocket } from './hooks/useWebSocket';
import { useProviders } from './hooks/useProviders';
import { chatReducer } from './chatReducer';
import { toggleAppTab, type AppTab } from './appTab';
import type { ClientMessage, ConfirmMode, ServerMessage, SettingsPayload } from './types';
import type {
ClientMessage,
ConfirmMode,
ContextState,
ProjectState,
ServerMessage,
SettingsPayload,
} from './types';

/** Root application component. Manages tab state and wires WebSocket to chat. */
export function App() {
const [tab, setTab] = useState<AppTab>('chat');
const [debugMode, setDebugMode] = useState(false);
const [confirmMode, setConfirmMode] = useState<ConfirmMode>('guard');
const [settings, setSettings] = useState<SettingsPayload | null>(null);
const [project, setProject] = useState<ProjectState>({ name: null, slug: null });
const [context, setContext] = useState<ContextState>({
globalInstructions: '',
projectInstructions: '',
globalMemories: '',
projectMemories: '',
});
const [projectStale, setProjectStale] = useState<string | null>(null);
const [chatState, dispatch] = useReducer(chatReducer, { messages: [], streaming: false });

const sendRef = useRef<(msg: ClientMessage) => void>(() => {});
Expand All @@ -31,6 +47,8 @@ export function App() {
switch (msg.type) {
case 'ready':
sendMsg({ type: 'get_settings' });
sendMsg({ type: 'get_project' });
sendMsg({ type: 'get_context' });
break;
case 'stream_start':
if (expectDiagnosticStreamRef.current) {
Expand Down Expand Up @@ -78,6 +96,23 @@ export function App() {
case 'history':
dispatch({ type: 'LOAD_HISTORY', messages: msg.messages });
break;
case 'project':
setProject({ name: msg.name, slug: msg.slug });
break;
case 'context':
setContext({
globalInstructions: msg.globalInstructions,
projectInstructions: msg.projectInstructions,
globalMemories: msg.globalMemories,
projectMemories: msg.projectMemories,
});
break;
case 'context_saved':
setProjectStale(null);
break;
case 'project_stale':
setProjectStale(msg.summary);
break;
}
},
[debugMode, initFromLastChoice],
Expand Down Expand Up @@ -138,6 +173,30 @@ export function App() {
sendMsg({ type: 'open_url', url });
}

function handleSetProject(name: string) {
sendMsg({ type: 'set_project', name });
}

function handleClearProject() {
sendMsg({ type: 'clear_project' });
}

function handleSaveInstructions(scope: 'global' | 'project', content: string) {
sendMsg({ type: 'save_instructions', scope, content });
}

function handleSaveMemories(scope: 'global' | 'project', content: string) {
sendMsg({ type: 'save_memories', scope, content });
}

function handleRefreshProjectMemories() {
sendMsg({ type: 'refresh_project_memories', provider, model });
}

function handleDismissStale() {
setProjectStale(null);
}

function handleCloseSettings() {
setTab('chat');
}
Expand All @@ -162,6 +221,12 @@ export function App() {
onToggleToolFold={handleToggleToolFold}
/>

<ProjectStaleBanner
summary={projectStale}
onUpdate={handleRefreshProjectMemories}
onDismiss={handleDismissStale}
/>

{tab === 'settings' && (
<>
<button
Expand All @@ -174,10 +239,16 @@ export function App() {
<SettingsPanel
providers={providers}
settings={settings}
project={project}
context={context}
onSave={handleSaveSettings}
onClearKey={handleClearKey}
onOpenUrl={handleOpenUrl}
onClose={handleCloseSettings}
onSetProject={handleSetProject}
onClearProject={handleClearProject}
onSaveInstructions={handleSaveInstructions}
onSaveMemories={handleSaveMemories}
/>
</div>
</>
Expand Down
35 changes: 35 additions & 0 deletions ui/src/components/ProjectStaleBanner/ProjectStaleBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
interface ProjectStaleBannerProps {
/** Stale summary text; null hides the banner. */
summary: string | null;
onUpdate: () => void;
onDismiss: () => void;
}

/** Amber warning bar shown when project memories may be out of date. */
export function ProjectStaleBanner({ summary, onUpdate, onDismiss }: ProjectStaleBannerProps) {
if (summary === null) {
return null;
}

return (
<div className="flex shrink-0 items-center gap-3 border border-amber-700 bg-amber-900/30 px-4 py-2.5 text-amber-200">
<p className="min-w-0 flex-1 text-[13px] leading-snug">{summary}</p>
<button
type="button"
className="shrink-0 cursor-pointer rounded-default border border-amber-600 bg-transparent px-3 py-1 text-[12px] font-medium text-amber-200 transition-colors hover:bg-amber-800/40"
onClick={onUpdate}
>
Update Memories
</button>
<button
type="button"
className="flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-default border-none bg-transparent p-0 text-[18px] leading-none text-amber-200 transition-colors hover:bg-amber-800/40"
onClick={onDismiss}
aria-label="Dismiss stale project warning"
title="Dismiss"
>
×
</button>
</div>
);
}
1 change: 1 addition & 0 deletions ui/src/components/ProjectStaleBanner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ProjectStaleBanner } from './ProjectStaleBanner.js';
Loading
Loading