-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add Regent orchestrator as persistent right sidebar #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| VITE_POSTHOG_KEY=phc_your_project_key | ||
| VITE_POSTHOG_HOST=https://us.i.posthog.com | ||
| VITE_TAMBO_API_KEY=your_tambo_api_key |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ import { TopBar } from "./components/TopBar.js"; | |
| import { HomePage } from "./components/HomePage.js"; | ||
| import { TaskPanel } from "./components/TaskPanel.js"; | ||
| import { DiffPanel } from "./components/DiffPanel.js"; | ||
| import { RegentSidebar } from "./components/RightPanel.js"; | ||
| import { UpdateBanner } from "./components/UpdateBanner.js"; | ||
|
Comment on lines
+14
to
15
|
||
| import { SessionLaunchOverlay } from "./components/SessionLaunchOverlay.js"; | ||
| import { SessionTerminalDock } from "./components/SessionTerminalDock.js"; | ||
|
|
@@ -30,7 +31,6 @@ const AgentsPage = lazy(() => import("./components/AgentsPage.js").then((m) => ( | |
| const TerminalPage = lazy(() => import("./components/TerminalPage.js").then((m) => ({ default: m.TerminalPage }))); | ||
| const ProcessPanel = lazy(() => import("./components/ProcessPanel.js").then((m) => ({ default: m.ProcessPanel }))); | ||
|
|
||
|
|
||
| function LazyFallback() { | ||
| return ( | ||
| <div className="flex items-center justify-center h-full"> | ||
|
|
@@ -284,24 +284,9 @@ export default function App() { | |
| </div> | ||
| </div> | ||
|
|
||
| {/* Task panel — overlay on mobile, inline on desktop */} | ||
| {/* Task panel — overlay on mobile, inline on desktop (session view only) */} | ||
| {currentSessionId && isSessionView && ( | ||
| <> | ||
| {!taskPanelOpen && ( | ||
| <button | ||
| type="button" | ||
| onClick={() => useStore.getState().setTaskPanelOpen(true)} | ||
| className="hidden lg:flex fixed right-0 top-1/2 -translate-y-1/2 z-30 items-center gap-1 rounded-l-lg border border-r-0 border-cc-border bg-cc-card/95 backdrop-blur px-2 py-2 text-[11px] text-cc-muted hover:text-cc-fg hover:bg-cc-hover transition-colors cursor-pointer" | ||
| title="Open context panel" | ||
| > | ||
| <svg viewBox="0 0 16 16" fill="currentColor" className="w-3 h-3"> | ||
| <path d="M3 2.5A1.5 1.5 0 014.5 1h7A1.5 1.5 0 0113 2.5v11a1.5 1.5 0 01-1.5 1.5h-7A1.5 1.5 0 013 13.5v-11zm2 .5v10h6V3H5z" /> | ||
| </svg> | ||
| <span className="[writing-mode:vertical-rl] rotate-180 tracking-wide">Context</span> | ||
| </button> | ||
| )} | ||
|
|
||
| {/* Mobile overlay backdrop */} | ||
| {taskPanelOpen && ( | ||
| <div | ||
| className="fixed inset-0 bg-black/30 z-30 lg:hidden" | ||
|
|
@@ -321,6 +306,9 @@ export default function App() { | |
| </div> | ||
| </> | ||
| )} | ||
|
|
||
| {/* Regent sidebar — persistent, always available */} | ||
| <RegentSidebar /> | ||
| <UpdateOverlay active={updateOverlayActive} /> | ||
| </div> | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| import { useRef, useEffect, type ReactNode } from "react"; | ||
| import { | ||
| TamboProvider, | ||
| useTambo, | ||
| useTamboThreadInput, | ||
| ComponentRenderer, | ||
| } from "@tambo-ai/react"; | ||
| import { sessionTools } from "../regent/tools/session-tools.js"; | ||
| import { sessionCardTamboComponent } from "../regent/components/SessionCard.js"; | ||
| import { taskOverviewTamboComponent } from "../regent/components/TaskOverview.js"; | ||
|
|
||
| const TAMBO_API_KEY = (import.meta as unknown as Record<string, Record<string, string>>).env?.VITE_TAMBO_API_KEY; | ||
|
|
||
| export function hasTamboApiKey(): boolean { | ||
| return !!TAMBO_API_KEY; | ||
| } | ||
|
Comment on lines
+1
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. regentpanel missing .test.tsx web/src/components/RegentPanel.tsx is a new component but there is no corresponding RegentPanel.test.tsx providing render, axe, and interaction coverage. This violates the baseline frontend component test requirement and risks regressions in the Regent chat/provider UI. Agent Prompt
Comment on lines
+12
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Tambo key exposed client Regent reads VITE_TAMBO_API_KEY from the Vite client environment and passes it to TamboProvider, which means the key is shipped to every browser. If the key is privileged, it can be extracted and abused (billing/tenant access). Agent Prompt
Comment on lines
+12
to
+16
|
||
|
|
||
| export function RegentChat() { | ||
| const { messages, isStreaming, currentThreadId } = useTambo(); | ||
| const { value, setValue, submit, isPending } = useTamboThreadInput(); | ||
| const feedRef = useRef<HTMLDivElement>(null); | ||
|
|
||
| useEffect(() => { | ||
| if (feedRef.current) { | ||
| feedRef.current.scrollTop = feedRef.current.scrollHeight; | ||
| } | ||
| }, [messages, isStreaming]); | ||
|
|
||
| const handleSubmit = async (e: React.FormEvent) => { | ||
| e.preventDefault(); | ||
| if (!value.trim() || isPending) return; | ||
| await submit(); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="flex flex-col h-full"> | ||
| {/* Message feed */} | ||
| <div ref={feedRef} className="flex-1 overflow-y-auto px-3 py-2 space-y-3"> | ||
| {messages.length === 0 && ( | ||
| <div className="flex flex-col items-center justify-center h-full text-center"> | ||
| <div className="text-sm text-cc-muted mb-2"> | ||
| Ask your Regent about your agent sessions | ||
| </div> | ||
| <div className="text-[11px] text-cc-muted/60 space-y-1"> | ||
| <div>"What are all my sessions doing?"</div> | ||
| <div>"Which sessions need permission approvals?"</div> | ||
| <div>"Summarize the progress across all agents"</div> | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
||
| {messages.map((msg) => ( | ||
| <div | ||
| key={msg.id} | ||
| className={`text-sm ${ | ||
| msg.role === "user" | ||
| ? "text-cc-fg bg-cc-hover rounded-lg px-3 py-2 ml-8" | ||
| : "text-cc-fg" | ||
| }`} | ||
| > | ||
| {msg.content.map((content, i) => { | ||
| if (content.type === "text") { | ||
| return ( | ||
| <div | ||
| key={`${msg.id}-${i}`} | ||
| className="whitespace-pre-wrap text-[13px] leading-relaxed" | ||
| > | ||
| {content.text} | ||
| </div> | ||
| ); | ||
| } | ||
| if (content.type === "component") { | ||
| return ( | ||
| <div key={`${msg.id}-${i}`} className="my-2"> | ||
| <ComponentRenderer | ||
| content={content} | ||
| threadId={currentThreadId ?? ""} | ||
| messageId={msg.id} | ||
| /> | ||
|
Comment on lines
+75
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In You can typically source that ID from Example (from the migration docs): <ComponentRenderer
content={block}
threadId={threadId}
messageId={message.id}
fallback={<div>Unknown component: {block.name}</div>}
/>
``` [1]
**Sources:** [1] [2]
---
🏁 Script executed:
```shell
# Search for ComponentRenderer imports and usage in the codebase
rg "ComponentRenderer" --type tsx --type ts -B 2 -A 5Repository: CodeCraftersLLC/companion Length of output: 96 🏁 Script executed: # Find the file and examine the context around the flagged lines
cat -n web/src/components/RegentPanel.tsx | sed -n '60,90p'Repository: CodeCraftersLLC/companion Length of output: 1336 🏁 Script executed: # Check for type definitions or documentation for ComponentRenderer
fd -e d.ts -e ts | xargs rg "ComponentRenderer" -lRepository: CodeCraftersLLC/companion Length of output: 53 Remove empty string fallback for threadId—ComponentRenderer requires a valid thread ID. The 🤖 Prompt for AI Agents |
||
| </div> | ||
| ); | ||
| } | ||
| return null; | ||
| })} | ||
| </div> | ||
| ))} | ||
|
|
||
| {isStreaming && ( | ||
| <div className="flex items-center gap-1.5 text-[11px] text-cc-muted"> | ||
| <span className="w-1.5 h-1.5 rounded-full bg-cc-accent animate-pulse" /> | ||
| Thinking... | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Composer */} | ||
| <form onSubmit={handleSubmit} className="shrink-0 border-t border-cc-border p-2"> | ||
| <div className="flex items-center gap-2"> | ||
| <input | ||
| type="text" | ||
| value={value} | ||
| onChange={(e) => setValue(e.target.value)} | ||
| placeholder="Ask the Regent..." | ||
| className="flex-1 bg-cc-bg border border-cc-border rounded-lg px-3 py-2 text-sm text-cc-fg placeholder:text-cc-muted/50 focus:outline-none focus:border-cc-accent" | ||
| disabled={isPending} | ||
| /> | ||
| <button | ||
| type="submit" | ||
| disabled={isPending || !value.trim()} | ||
| className="shrink-0 px-3 py-2 rounded-lg bg-cc-accent text-white text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed hover:opacity-90 transition-opacity cursor-pointer" | ||
| > | ||
| Send | ||
| </button> | ||
| </div> | ||
| </form> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| interface RegentProviderProps { | ||
| children: ReactNode; | ||
| } | ||
|
|
||
| export function RegentProvider({ children }: RegentProviderProps) { | ||
| if (!TAMBO_API_KEY) { | ||
| return ( | ||
| <div className="flex flex-col items-center justify-center h-full p-6 text-center"> | ||
| <svg viewBox="0 0 16 16" fill="currentColor" className="w-8 h-8 text-cc-muted mb-3"> | ||
| <path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM7.25 5a.75.75 0 011.5 0v3a.75.75 0 01-1.5 0V5zm.75 6.5a1 1 0 110-2 1 1 0 010 2z" /> | ||
| </svg> | ||
| <div className="text-sm text-cc-muted mb-1">Regent requires a Tambo API key</div> | ||
| <div className="text-[11px] text-cc-muted/60"> | ||
| Set <code className="px-1 py-0.5 rounded bg-cc-hover text-cc-fg">VITE_TAMBO_API_KEY</code> in your environment to enable Regent. | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <TamboProvider | ||
| apiKey={TAMBO_API_KEY} | ||
| userKey="companion-user" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The The <TamboProvider
apiKey={TAMBO_API_KEY}
userKey={getUniqueUserId()} // e.g., from auth context or store
tools={sessionTools}
components={[sessionCardTamboComponent, taskOverviewTamboComponent]}
>There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Hardcoded Prompt for AI agents |
||
| tools={sessionTools} | ||
| components={[sessionCardTamboComponent, taskOverviewTamboComponent]} | ||
|
Comment on lines
+140
to
+144
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Hard-coded tambo userkey TamboProvider is initialized with a constant userKey ("companion-user"), so different authenticated
users/devices can share the same Tambo identity and potentially see each other’s threads/history.
This is a privacy/security risk for an app with auth tokens.
Agent Prompt
|
||
| > | ||
|
Comment on lines
+140
to
+145
|
||
| {children} | ||
| </TamboProvider> | ||
| ); | ||
| } | ||
|
|
||
| export function RegentPanel() { | ||
| return ( | ||
| <RegentProvider> | ||
| <RegentChat /> | ||
| </RegentProvider> | ||
| ); | ||
| } | ||
|
|
||
| export default RegentPanel; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| // @vitest-environment jsdom | ||
| import { render, screen, fireEvent } from "@testing-library/react"; | ||
| import "@testing-library/jest-dom"; | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
|
|
||
| const mockSwitchThread = vi.fn(); | ||
| const mockStartNewThread = vi.fn(); | ||
|
|
||
| vi.mock("@tambo-ai/react", () => ({ | ||
| TamboProvider: ({ children }: { children: React.ReactNode }) => <div data-testid="tambo-provider">{children}</div>, | ||
| useTambo: () => ({ | ||
| messages: [], | ||
| isStreaming: false, | ||
| currentThreadId: "thread-1", | ||
| switchThread: mockSwitchThread, | ||
| startNewThread: mockStartNewThread, | ||
| client: {}, | ||
| thread: undefined, | ||
| streamingState: { status: "idle" }, | ||
| isWaiting: false, | ||
| isIdle: true, | ||
| registerComponent: vi.fn(), | ||
| registerTool: vi.fn(), | ||
| registerTools: vi.fn(), | ||
| componentList: new Map(), | ||
| toolRegistry: new Map(), | ||
| initThread: vi.fn(), | ||
| dispatch: vi.fn(), | ||
| cancelRun: vi.fn(), | ||
| authState: { status: "identified" }, | ||
| isIdentified: true, | ||
| updateThreadName: vi.fn(), | ||
| }), | ||
| useTamboThreadInput: () => ({ | ||
| value: "", | ||
| setValue: vi.fn(), | ||
| submit: vi.fn(), | ||
| isPending: false, | ||
| }), | ||
| useTamboThreadList: () => ({ | ||
| data: { | ||
| threads: [ | ||
| { id: "thread-1", name: "Thread One", runStatus: "idle", createdAt: "2025-01-01", updatedAt: "2025-01-01" }, | ||
| { id: "thread-2", name: "Thread Two", runStatus: "idle", createdAt: "2025-01-01", updatedAt: "2025-01-01" }, | ||
| ], | ||
| hasMore: false, | ||
| }, | ||
| isLoading: false, | ||
| isError: false, | ||
| }), | ||
| ComponentRenderer: () => null, | ||
| })); | ||
|
|
||
| const workerStub = vi.hoisted(() => { | ||
| return vi.fn().mockImplementation(() => ({ | ||
| postMessage: vi.fn(), | ||
| terminate: vi.fn(), | ||
| addEventListener: vi.fn(), | ||
| removeEventListener: vi.fn(), | ||
| onmessage: null, | ||
| onerror: null, | ||
| })); | ||
| }); | ||
|
|
||
| vi.stubGlobal("Worker", workerStub); | ||
|
|
||
| vi.mock("../regent/tools/session-tools.js", () => ({ | ||
| sessionTools: [], | ||
| })); | ||
|
|
||
| vi.mock("../regent/components/SessionCard.js", () => ({ | ||
| sessionCardTamboComponent: { | ||
| name: "SessionCard", | ||
| description: "test", | ||
| component: () => null, | ||
| propsSchema: { type: "object" as const, properties: {}, required: [] }, | ||
| }, | ||
| SessionCard: () => null, | ||
| })); | ||
|
|
||
| vi.mock("../regent/components/TaskOverview.js", () => ({ | ||
| taskOverviewTamboComponent: { | ||
| name: "TaskOverview", | ||
| description: "test", | ||
| component: () => null, | ||
| propsSchema: { type: "object" as const, properties: {}, required: [] }, | ||
| }, | ||
| TaskOverview: () => null, | ||
| })); | ||
|
|
||
| import { RegentSidebar } from "./RightPanel.js"; | ||
| import { useStore } from "../store.js"; | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| useStore.setState({ | ||
| regentPanelOpen: true, | ||
| rightPanelActiveTab: "", | ||
| }); | ||
|
Comment on lines
+91
to
+99
|
||
| }); | ||
|
|
||
| describe("RegentSidebar", () => { | ||
| it("renders Regent thread tabs from Tambo thread list", () => { | ||
| render(<RegentSidebar />); | ||
| expect(screen.getByTitle("Thread One")).toBeInTheDocument(); | ||
| expect(screen.getByTitle("Thread Two")).toBeInTheDocument(); | ||
|
Comment on lines
+103
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
These tests assume Useful? React with 👍 / 👎. |
||
| }); | ||
|
|
||
| it("shows new thread button", () => { | ||
| render(<RegentSidebar />); | ||
| expect(screen.getByTitle("New Regent thread")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("renders RegentChat when panel is open", () => { | ||
| render(<RegentSidebar />); | ||
| expect(screen.getByPlaceholderText("Ask the Regent...")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("switching to a thread tab calls switchThread", () => { | ||
| render(<RegentSidebar />); | ||
| fireEvent.click(screen.getByTitle("Thread Two")); | ||
| expect(mockSwitchThread).toHaveBeenCalledWith("thread-2"); | ||
| }); | ||
|
|
||
| it("collapses panel when closed", () => { | ||
| useStore.setState({ regentPanelOpen: false }); | ||
| const { container } = render(<RegentSidebar />); | ||
| const panelWrapper = container.querySelector(".translate-x-full"); | ||
| expect(panelWrapper).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("clicking new thread button calls startNewThread", () => { | ||
| render(<RegentSidebar />); | ||
| fireEvent.click(screen.getByTitle("New Regent thread")); | ||
| expect(mockStartNewThread).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("shows diamond header icon in vertical tab bar", () => { | ||
| render(<RegentSidebar />); | ||
| const tabBar = screen.getByTitle("New Regent thread").parentElement; | ||
| expect(tabBar).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("shows numbered tabs for each thread", () => { | ||
| render(<RegentSidebar />); | ||
| expect(screen.getByText("1")).toBeInTheDocument(); | ||
| expect(screen.getByText("2")).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
Comment on lines
+1
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. rightpanel.test.tsx missing axe web/src/components/RightPanel.test.tsx includes render and interaction checks but does not run an axe accessibility scan with toHaveNoViolations(). This violates the required accessibility baseline for new/modified components. Agent Prompt
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: CodeCraftersLLC/companion
Length of output: 561
Verify stability of
@tambo-ai/clientbefore production use.The
@tambo-ai/clientpackage is at version0.0.1—the only available release on npm. This extremely early version indicates the package is in initial development stages and may be unstable. Additionally, the@tambo-ai/typescript-sdkis at version0.93.1, which, while more mature, is still pre-1.0 and subject to breaking changes.Consider:
🤖 Prompt for AI Agents