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
2 changes: 1 addition & 1 deletion console/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 6 additions & 13 deletions console/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import {
lazy,
Suspense,
useCallback,
useEffect,
useRef,
useState,
} from 'react'
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react'
import { ChatDock } from '@/components/chat/ChatDock'
import {
Dialog,
Expand All @@ -20,7 +13,10 @@ import { useChatDock } from '@/hooks/use-chat-dock'
import { useHashRoute, type View } from '@/hooks/use-hash-route'
import { useTheme } from '@/hooks/use-theme'
import { type DockSignal, getDockSignal } from '@/lib/chat-activity'
import { ConversationsProvider, useConversationsCtx } from '@/lib/conversations-context'
import {
ConversationsProvider,
useConversationsCtx,
} from '@/lib/conversations-context'
import { cn } from '@/lib/utils'
import { Configuration } from '@/pages/Configuration'
import { Traces } from '@/pages/Traces'
Expand Down Expand Up @@ -126,10 +122,7 @@ export function App() {
</Suspense>
</div>
</div>
<ShortcutsDialog
open={shortcutsOpen}
onOpenChange={setShortcutsOpen}
/>
<ShortcutsDialog open={shortcutsOpen} onOpenChange={setShortcutsOpen} />
</Sheet>
</ConversationsProvider>
)
Expand Down
5 changes: 1 addition & 4 deletions console/web/src/components/chat/AutoAcceptToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,7 @@ export function AutoAcceptToggle({
>
auto-accept: <span className="font-mono">{value ? 'on' : 'off'}</span>
</button>
<span
id={descId}
className="sr-only"
>
<span id={descId} className="sr-only">
{value
? 'Auto-accept is on. Approval prompts for safe calls (reads, lookups, listings) are resolved automatically. Destructive or state-mutating calls still require a click.'
: 'Auto-accept is off. Every approval prompt waits for an explicit click.'}
Expand Down
5 changes: 4 additions & 1 deletion console/web/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,10 @@ export function ChatView({
// instead of leaving the response looking like it just
// ran out of words. Pre-fix this event didn't exist and
// the same condition produced a silently truncated reply.
const noticeContent = formatStopReason(event.reason, event.message)
const noticeContent = formatStopReason(
event.reason,
event.message,
)
const notice: SystemMessage = {
id: uid(),
role: 'system',
Expand Down
2 changes: 1 addition & 1 deletion console/web/src/components/chat/Composer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { LexicalEditor } from 'lexical'
import { useCallback, useRef, useState } from 'react'
import { Button } from '@/components/ui/Button'
import type { Attachment, Mode, ModelId, ModelOption } from '@/types/chat'
import type { FunctionEntry } from '@/lib/functions'
import type { Attachment, Mode, ModelId, ModelOption } from '@/types/chat'
import { AttachmentButton } from './AttachmentButton'
import { AttachmentChip } from './AttachmentChip'
import { AutoAcceptToggle } from './AutoAcceptToggle'
Expand Down
41 changes: 31 additions & 10 deletions console/web/src/components/chat/FunctionCallGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ interface FunctionCallGroupProps {
* `unknown`, so this guard stays narrow on purpose.
*/
function isErrorOutput(v: unknown): boolean {
return !!v && typeof v === 'object' && !Array.isArray(v) && 'error' in (v as Record<string, unknown>)
return (
!!v &&
typeof v === 'object' &&
!Array.isArray(v) &&
'error' in (v as Record<string, unknown>)
)
}

type Tone = 'warn' | 'accent' | 'alert' | 'ink'
Expand All @@ -54,7 +59,8 @@ function deriveStatus(messages: FunctionCallMessageType[]): GroupStatus {
pulse: false,
label: (
<>
permission to run <span className="text-accent italic font-semibold">ƒ</span>{' '}
permission to run{' '}
<span className="text-accent italic font-semibold">ƒ</span>{' '}
<span className="text-ink">{pending.functionId}</span>
</>
),
Expand All @@ -69,8 +75,10 @@ function deriveStatus(messages: FunctionCallMessageType[]): GroupStatus {
pulse: true,
label: (
<>
running function <span className="tabular-nums">{runningIdx + 1}</span> of{' '}
<span className="tabular-nums">{total}</span>: <span className="text-accent italic font-semibold">ƒ</span>{' '}
running function{' '}
<span className="tabular-nums">{runningIdx + 1}</span> of{' '}
<span className="tabular-nums">{total}</span>:{' '}
<span className="text-accent italic font-semibold">ƒ</span>{' '}
<span className="text-ink">{running.functionId}</span>
</>
),
Expand All @@ -84,7 +92,8 @@ function deriveStatus(messages: FunctionCallMessageType[]): GroupStatus {
pulse: false,
label: (
<>
<span className="tabular-nums">{failed}</span> {failed === 1 ? 'function' : 'functions'} failed
<span className="tabular-nums">{failed}</span>{' '}
{failed === 1 ? 'function' : 'functions'} failed
{failed < total ? (
<span className="text-ink-faint">
{' '}
Expand All @@ -102,7 +111,8 @@ function deriveStatus(messages: FunctionCallMessageType[]): GroupStatus {
pulse: false,
label: (
<>
ran <span className="tabular-nums">{total}</span> functions for <span className="tabular-nums">{sum}</span>ms
ran <span className="tabular-nums">{total}</span> functions for{' '}
<span className="tabular-nums">{sum}</span>ms
</>
),
}
Expand All @@ -113,7 +123,9 @@ function deriveStatus(messages: FunctionCallMessageType[]): GroupStatus {
* where the user can't infer what's happening from the one-line header.
*/
function hasConcerningChild(messages: FunctionCallMessageType[]): boolean {
return messages.some((m) => m.pendingApproval || m.running || isErrorOutput(m.output))
return messages.some(
(m) => m.pendingApproval || m.running || isErrorOutput(m.output),
)
}

export function FunctionCallGroup({
Expand Down Expand Up @@ -151,12 +163,21 @@ export function FunctionCallGroup({
)}
>
<span className="flex items-center gap-2 min-w-0">
<StatusDot tone={status.tone} pulse={status.pulse} className="shrink-0" />
<span className="font-mono text-[13px] text-ink truncate">{status.label}</span>
<StatusDot
tone={status.tone}
pulse={status.pulse}
className="shrink-0"
/>
<span className="font-mono text-[13px] text-ink truncate">
{status.label}
</span>
</span>
<span
aria-hidden
className={cn('text-ink-ghost shrink-0 transition-transform duration-150 inline-block', open && 'rotate-90')}
className={cn(
'text-ink-ghost shrink-0 transition-transform duration-150 inline-block',
open && 'rotate-90',
)}
>
</span>
Expand Down
52 changes: 49 additions & 3 deletions console/web/src/components/chat/FunctionCallMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { useEffect, useState } from 'react'
import {
SandboxFunctionIdLabel,
SandboxToolView,
} from '@/components/chat/sandbox'
import { Button } from '@/components/ui/Button'
import { StatusDot } from '@/components/ui/StatusDot'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/Tabs'
import { JsonHighlight } from '@/lib/syntax'
import { cn } from '@/lib/utils'
import type { FunctionCallMessage as FunctionCallMessageType } from '@/types/chat'
Expand Down Expand Up @@ -84,9 +89,18 @@ export function FunctionCallMessage({
const pending = !!message.pendingApproval
const running = !!message.running
const [open, setOpen] = useState(!!defaultOpen || pending)
const [tab, setTab] = useState<'terminal' | 'json'>('terminal')
const [submitting, setSubmitting] = useState<'approve' | 'deny' | null>(null)
const [submitError, setSubmitError] = useState<string | null>(null)

const sandboxPreview = SandboxToolView.tryRenderPreview(message)
const sandboxTerminal = !pending ? SandboxToolView.tryRender(message) : null
const hasSandboxTerminal = sandboxTerminal != null
const showRequestPaneAbove =
!(pending && sandboxPreview) &&
!(running && hasSandboxTerminal) &&
!(!pending && !running && hasSandboxTerminal)

const runResolve = async (kind: 'approve' | 'deny') => {
const handler = kind === 'approve' ? onApprove : onDeny
if (!handler || submitting) return
Expand Down Expand Up @@ -140,7 +154,7 @@ export function FunctionCallMessage({
<>ran </>
)}
<span className="text-accent italic font-semibold">ƒ</span>{' '}
<span className="text-ink">{message.functionId}</span>
<SandboxFunctionIdLabel functionId={message.functionId} />
{!pending && !running && typeof message.durationMs === 'number' ? (
<span className="text-ink-faint">
{' '}
Expand All @@ -163,9 +177,41 @@ export function FunctionCallMessage({

{open ? (
<div className="border-t border-rule-2">
<ValuePane label="request" value={message.input} />
{pending && sandboxPreview ? (
<div className="border-b border-rule-2">{sandboxPreview}</div>
) : showRequestPaneAbove ? (
<ValuePane label="request" value={message.input} />
) : null}
{running && !pending ? (
hasSandboxTerminal ? (
<div className="border-t border-rule-2">{sandboxTerminal}</div>
) : (
<ValuePane label="response" value={message.output} bordered />
)
) : null}
{!pending && !running ? (
<ValuePane label="response" value={message.output} bordered />
hasSandboxTerminal ? (
<Tabs
value={tab}
onValueChange={(v) => setTab(v as 'terminal' | 'json')}
className="border-t border-rule-2"
>
<TabsList className="px-3">
<TabsTrigger value="terminal">terminal</TabsTrigger>
<TabsTrigger value="json">raw json</TabsTrigger>
</TabsList>
<TabsContent value="terminal">{sandboxTerminal}</TabsContent>
<TabsContent value="json">
<ValuePane label="request" value={message.input} />
<ValuePane label="response" value={message.output} bordered />
</TabsContent>
</Tabs>
) : (
<>
<ValuePane label="request" value={message.input} />
<ValuePane label="response" value={message.output} bordered />
</>
)
) : null}
</div>
) : null}
Expand Down
5 changes: 4 additions & 1 deletion console/web/src/components/chat/LexicalShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,10 @@ export function LexicalShell({
<ChangePlugin onChange={onChange} />
<SubmitOnEnterPlugin onSubmit={onSubmit} menuOpenRef={menuOpenRef} />
<EditablePlugin disabled={disabled} />
<MentionsPlugin menuOpenRef={menuOpenRef} functionEntries={functionEntries} />
<MentionsPlugin
menuOpenRef={menuOpenRef}
functionEntries={functionEntries}
/>
<SlashCommandsPlugin menuOpenRef={menuOpenRef} />
<FunctionMentionTransformPlugin />
</LexicalComposer>
Expand Down
9 changes: 2 additions & 7 deletions console/web/src/components/chat/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,7 @@ export function MessageList({
const listPad = density === 'dock' ? 'px-4 py-6' : 'px-9 py-8'

return (
<div
ref={containerRef}
className={cn('flex-1 overflow-y-auto', listPad)}
>
<div ref={containerRef} className={cn('flex-1 overflow-y-auto', listPad)}>
<div className="mx-auto max-w-[760px] flex flex-col gap-y-8">
{items.map((item) =>
item.kind === 'message' ? (
Expand Down Expand Up @@ -172,9 +169,7 @@ export function MessageList({
function EmptyState({ density }: { density: 'route' | 'dock' }) {
const emptyPad = density === 'dock' ? 'px-4' : 'px-9'
return (
<div
className={cn('flex-1 flex items-center justify-center', emptyPad)}
>
<div className={cn('flex-1 flex items-center justify-center', emptyPad)}>
<div className="max-w-[520px] w-full flex flex-col gap-6">
<div className="font-mono text-[11px] uppercase tracking-[0.06em] text-ink-faint">
<Prompt symbol="$">new session</Prompt>
Expand Down
5 changes: 2 additions & 3 deletions console/web/src/components/chat/ModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ export function ModelPicker({
loading,
className,
}: ModelPickerProps) {
const [settingsProvider, setSettingsProvider] = useState<ActiveProvider | null>(
null,
)
const [settingsProvider, setSettingsProvider] =
useState<ActiveProvider | null>(null)

const pickerOptions =
options.length > 0 ? options : [{ id: value, label: value }]
Expand Down
75 changes: 75 additions & 0 deletions console/web/src/components/chat/sandbox/CodeHighlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Highlight, Prism, type PrismTheme } from 'prism-react-renderer'
import { cn } from '@/lib/utils'

const monoTheme: PrismTheme = {
plain: { color: 'var(--color-ink)' },
styles: [
{
types: ['comment', 'prolog', 'doctype', 'cdata'],
style: { color: 'var(--color-ink-ghost)', fontStyle: 'italic' },
},
{
types: ['string', 'attr-value', 'regex'],
style: { color: 'var(--color-ink-faint)' },
},
{
types: ['number', 'boolean', 'keyword', 'null'],
style: { color: 'var(--color-accent)', fontStyle: 'italic' },
},
{
types: ['function', 'class-name', 'builtin'],
style: { color: 'var(--color-ink)' },
},
{
types: ['punctuation', 'operator'],
style: { color: 'var(--color-ink-ghost)' },
},
],
}

interface CodeHighlightProps {
code: string
language?: string
className?: string
}

export function CodeHighlight({
code,
language = 'text',
className,
}: CodeHighlightProps) {
const lang = Prism.languages[language] ? language : 'text'
return (
<Highlight prism={Prism} theme={monoTheme} language={lang} code={code}>
{({ tokens, getLineProps, getTokenProps, className: hlClass, style }) => (
<pre
className={cn(
'bg-bg overflow-x-auto px-3 py-2 font-mono text-[12.5px] leading-[1.55] whitespace-pre-wrap break-words',
hlClass,
className,
)}
style={style}
>
<code>
{tokens.map((line, lineIdx) => {
const lineProps = getLineProps({ line })
/* Prism tokenization is deterministic for a given `code`;
positional+content keys stay stable across renders. */
const lineKey = `${lineIdx}:${line.length}`
return (
<span key={lineKey} {...lineProps}>
{line.map((token, tokenIdx) => {
const tokenProps = getTokenProps({ token })
const tokenKey = `${tokenIdx}:${token.types.join('.')}:${token.content}`
return <span key={tokenKey} {...tokenProps} />
})}
{lineIdx < tokens.length - 1 ? '\n' : ''}
</span>
)
})}
</code>
</pre>
)}
</Highlight>
)
}
Loading
Loading