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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},
"dependencies": {
"@buf/googleapis_googleapis.bufbuild_es": "2.9.0-20251009205305-72c8614f3bd0.1",
"@buf/runmedev_runme.bufbuild_es": "2.9.0-20251009190022-068cc6f56f01.1",
"@buf/runmedev_runme.bufbuild_es": "2.9.0-20251120010649-1098d5833c44.1",
"@bufbuild/protobuf": "2.9.0",
"@connectrpc/connect": "^2.1.0",
"@connectrpc/connect-node": "^2.1.0",
Expand Down Expand Up @@ -88,5 +88,5 @@
"vite-plugin-compression": "^0.5.1",
"vitest": "^2.1.9"
},
"packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad"
}
"packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b"
}
124 changes: 69 additions & 55 deletions packages/react-components/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { BrowserRouter, Route, Routes } from 'react-router-dom'

import { WebAppConfig } from '@buf/runmedev_runme.bufbuild_es/agent/v1/webapp_pb'
import { InitialConfigState } from '@buf/runmedev_runme.bufbuild_es/agent/v1/webapp_pb'
import { Theme } from '@radix-ui/themes'
import '@radix-ui/themes/styles.css'

import Actions from './components/Actions/Actions'
import Chat from './components/Chat/Chat'
import Chat, { ChatSequence } from './components/Chat/Chat'
import FileViewer from './components/Files/Viewer'
import Login from './components/Login/Login'
import NotFound from './components/NotFound'
import Settings from './components/Settings/Settings'
import { AgentClientProvider } from './contexts/AgentContext'
import { CellProvider } from './contexts/CellContext'
Expand All @@ -17,6 +16,7 @@ import { SettingsProvider } from './contexts/SettingsContext'
import './index.css'
import Layout from './layout'
import { getAccessToken } from './token'
import { NotFound } from './components'

export interface AppBranding {
name: string
Expand All @@ -25,64 +25,69 @@ export interface AppBranding {

export interface AppProps {
branding: AppBranding
initialState?: {
agentEndpoint?: string
requireAuth?: boolean
webApp?: WebAppConfig
initialState?: Partial<
Omit<InitialConfigState, '$typeName' | '$unknown' | 'webApp'>
> & {
webApp?: Partial<
Omit<InitialConfigState['webApp'], '$typeName' | '$unknown'>
>
}
}

function AppRouter({ branding }: { branding: AppBranding }) {
function AppRoutes({ branding }: { branding: AppBranding }) {
const actions = <Actions headline="Actions" />
const files = <FileViewer headline="Files" />
return (
<BrowserRouter>
<Routes>
<Route
path="/"
element={
<Layout
branding={branding}
left={<Chat />}
middle={<Actions />}
right={<FileViewer />}
/>
}
/>
<Route
path="/settings"
element={
<Layout
branding={branding}
left={<Chat />}
middle={<Actions />}
right={<Settings />}
/>
}
/>
<Route
path="/oidc/*"
element={
<Layout
branding={branding}
middle={
<div>OIDC routes are exclusively handled by the server.</div>
}
/>
}
/>
<Route
path="/login"
element={<Layout branding={branding} left={<Login />} />}
/>
<Route
path="*"
element={<Layout branding={branding} left={<NotFound />} />}
/>
</Routes>
</BrowserRouter>
<Routes>
<Route
path="/"
element={
<Layout
branding={branding}
left={<Chat />}
middle={actions}
right={files}
/>
}
/>
<Route
path="/settings"
element={
<Layout
branding={branding}
left={<Chat />}
middle={actions}
right={<Settings />}
/>
}
/>
<Route path="/sequence">
<Route index element={<ChatSequence />} />
</Route>
<Route
path="/oidc/*"
element={
<Layout
branding={branding}
middle={
<div>OIDC routes are exclusively handled by the server.</div>
}
/>
}
/>
<Route
path="/login"
element={<Layout branding={branding} left={<Login />} />}
/>
<Route
path="*"
element={<Layout branding={branding} left={<NotFound />} />}
/>
</Routes>
)
}

function App({ branding, initialState = {} }: AppProps) {
export function AppProviders({ branding, initialState = {} }: AppProps) {
return (
<>
<title>{branding.name}</title>
Expand All @@ -96,13 +101,14 @@ function App({ branding, initialState = {} }: AppProps) {
>
<SettingsProvider
agentEndpoint={initialState?.agentEndpoint}
systemShell={initialState?.systemShell}
requireAuth={initialState?.requireAuth}
webApp={initialState?.webApp}
>
<AgentClientProvider>
<OutputProvider>
<CellProvider getAccessToken={getAccessToken}>
<AppRouter branding={branding} />
<AppRoutes branding={branding} />
</CellProvider>
</OutputProvider>
</AgentClientProvider>
Expand All @@ -112,4 +118,12 @@ function App({ branding, initialState = {} }: AppProps) {
)
}

function App({ branding, initialState = {} }: AppProps) {
return (
<BrowserRouter>
<AppProviders branding={branding} initialState={initialState} />
</BrowserRouter>
)
}

export default App
81 changes: 21 additions & 60 deletions packages/react-components/src/components/Actions/Actions.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'

import { create } from '@bufbuild/protobuf'
import { Box, Button, Card, ScrollArea, Text } from '@radix-ui/themes'

// import '@runmedev/react-console/react-console-light.css'

import { parser_pb, useCell } from '../../contexts/CellContext'
import { useOutput } from '../../contexts/OutputContext'
import { useSettings } from '../../contexts/SettingsContext'
import { MimeType, RunmeMetadataKey } from '../../runme/client'
import CellConsole, { fontSettings } from './CellConsole'
import { RunmeMetadataKey } from '../../runme/client'
import { fontSettings } from './CellConsole'
import Editor from './Editor'
import {
ErrorIcon,
Expand Down Expand Up @@ -79,10 +78,10 @@ function Action({ cell }: { cell: parser_pb.Cell }) {
}, [cell, pid, exitCode])

return (
<div>
<div className="w-full min-w-0">
<Box className="w-full p-2">
<div className="flex justify-between items-top">
<div className="flex flex-col items-center">
<div className="flex justify-between items-start gap-2 w-full">
<div className="flex flex-col items-center flex-shrink-0">
<RunActionButton pid={pid} exitCode={exitCode} onClick={runCode} />
<Text
size="2"
Expand All @@ -92,14 +91,15 @@ function Action({ cell }: { cell: parser_pb.Cell }) {
[{sequenceLabel}]
</Text>
</div>
<Card className="whitespace-nowrap overflow-hidden flex-1 ml-2">
<Card className="flex-1 ml-2 min-w-0">
<Editor
key={`editor-${cell.refId}`}
id={cell.refId}
value={cell.value}
language="shellscript"
language={cell.languageId}
fontSize={fontSettings.fontSize}
fontFamily={fontSettings.fontFamily}
showLanguageSelector={true}
onChange={(v) => {
cell.value = v // only sync cell value on change
saveState()
Expand Down Expand Up @@ -131,80 +131,41 @@ function Action({ cell }: { cell: parser_pb.Cell }) {
)
}

function Actions() {
function Actions({
headline = 'Actions',
scrollToLatest = true,
}: {
headline?: string
scrollToLatest?: boolean
}) {
const { useColumns, addCodeCell } = useCell()
const { settings } = useSettings()
const { actions } = useColumns()
const actionsStartRef = useRef<HTMLDivElement>(null)
const actionsEndRef = useRef<HTMLDivElement>(null)

useEffect(() => {
if (!scrollToLatest) {
return
}
if (settings.webApp.invertedOrder) {
actionsStartRef.current?.scrollIntoView({ behavior: 'smooth' })
return
}
actionsEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [actions, settings.webApp.invertedOrder])

const { registerRenderer, unregisterRenderer } = useOutput()

// Register renderers for code cells
useEffect(() => {
registerRenderer(MimeType.StatefulRunmeTerminal, {
onCellUpdate: (cell: parser_pb.Cell) => {
if (cell.kind !== parser_pb.CellKind.CODE || cell.outputs.length > 0) {
return
}

// it's basically shell, be prepared to render a terminal
cell.outputs = [
create(parser_pb.CellOutputSchema, {
items: [
create(parser_pb.CellOutputItemSchema, {
mime: MimeType.StatefulRunmeTerminal,
type: 'Buffer',
data: new Uint8Array(), // todo(sebastian): pass terminal settings
}),
],
}),
]
},
component: ({
cell,
onPid,
onExitCode,
}: {
cell: parser_pb.Cell
onPid: (pid: number | null) => void
onExitCode: (exitCode: number | null) => void
}) => {
return (
<CellConsole
key={`console-${cell.refId}`}
cell={cell}
onPid={onPid}
onExitCode={onExitCode}
/>
)
},
})

return () => {
unregisterRenderer(MimeType.StatefulRunmeTerminal)
}
}, [registerRenderer, unregisterRenderer])

return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full w-full">
<div className="flex items-center mb-2">
<Text size="5" weight="bold" className="pr-2">
Actions
{headline}
</Text>
<Button
variant="ghost"
size="1"
className="cursor-pointer"
onClick={addCodeCell}
onClick={() => addCodeCell({ languageId: 'sh' })}
aria-label="Add code cell"
>
<PlusIcon />
Expand Down
Loading