mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
At mobile widths the resizable git panel collided with the chat column's 360px min-width, overflowing the viewport. Treat mobile (<768px) like the sidebar: the git panel becomes a full-screen overlay the user navigates to and back from, while desktop keeps the inline resizable panel. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
134 lines
5.3 KiB
TypeScript
134 lines
5.3 KiB
TypeScript
import { useMemo, useState } from "react"
|
|
import { useStreamContext as useAgentThreadStream } from "@langchain/react"
|
|
|
|
import type { AgentThread, Message } from "@/lib/agents/types"
|
|
import type { ModelSelection } from "@/lib/agents/provider/useModelOptions"
|
|
import {
|
|
AgentGitPanel,
|
|
PANEL_MIN_CHAT_WIDTH,
|
|
} from "@/components/agents/AgentGitPanel"
|
|
import { AgentPromptBar } from "@/components/agents/AgentPromptBar"
|
|
import { Messages } from "@/components/agents/messages"
|
|
import { streamMessagesToUi } from "@/lib/agents/streamMessagesToUi"
|
|
import { useSubmitAgentMessage } from "@/lib/agents/provider/useSubmitAgentMessage"
|
|
import { useModelOptions } from "@/lib/agents/provider/useModelOptions"
|
|
import { useIsMobile } from "@/lib/useIsMobile"
|
|
|
|
interface AgentThreadViewProps {
|
|
thread: AgentThread
|
|
}
|
|
|
|
// The stream lives at the `/agents` layout (one persistent provider that
|
|
// survives the home → thread navigation), so this view only consumes it.
|
|
export function AgentThreadView({ thread }: AgentThreadViewProps) {
|
|
const sendMessage = useSubmitAgentMessage(thread.id)
|
|
const stream = useAgentThreadStream()
|
|
const isMobile = useIsMobile()
|
|
|
|
const { models, defaultSelection } = useModelOptions()
|
|
const threadSelection = useMemo<ModelSelection | null>(() => {
|
|
if (!thread.model || !thread.effort) return null
|
|
const supported = models.some(
|
|
(m) => m.id === thread.model && m.efforts.includes(thread.effort ?? "")
|
|
)
|
|
if (!supported) return null
|
|
return { modelId: thread.model, effort: thread.effort }
|
|
}, [models, thread.model, thread.effort])
|
|
const [selection, setSelection] = useState<ModelSelection | null>(null)
|
|
const activeSelection = selection ?? threadSelection ?? defaultSelection
|
|
|
|
const baseMessages = useMemo<Array<Message>>(() => {
|
|
const live = streamMessagesToUi(stream.messages, stream.toolCalls, stream.subagents)
|
|
if (live.length > 0) return live
|
|
// Optimistic transcript seeded by `AgentsHome` on thread creation (the
|
|
// only case where a fetched thread carries messages — `getThread` returns
|
|
// none). Bridges the brief gap before the SDK's optimistic `submit` echo
|
|
// lands in `stream.messages`.
|
|
if (thread.messages.length > 0) return thread.messages
|
|
return live
|
|
}, [
|
|
stream.messages,
|
|
stream.toolCalls,
|
|
stream.subagents,
|
|
thread.messages,
|
|
])
|
|
|
|
const hasMessages = baseMessages.length > 0
|
|
const isStreaming = thread.status === "running" || stream.isLoading
|
|
const isThinking = stream.isLoading
|
|
const settingUpSandbox = isThinking && baseMessages.length === 0
|
|
// The transcript hydrates from the SDK (`GET …/state` → `stream.messages`).
|
|
// Show a loading state during that one-time fetch instead of the empty state.
|
|
const isHydrating = stream.isThreadLoading && !hasMessages
|
|
|
|
return (
|
|
<div className="flex min-w-0 flex-1">
|
|
<div
|
|
className="flex min-w-0 flex-1 flex-col"
|
|
style={isMobile ? undefined : { minWidth: PANEL_MIN_CHAT_WIDTH }}
|
|
>
|
|
{hasMessages ? (
|
|
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
<Messages
|
|
messages={baseMessages}
|
|
isStreaming={isStreaming}
|
|
streamIsLoading={stream.isLoading}
|
|
isThinking={isThinking}
|
|
settingUpSandbox={settingUpSandbox}
|
|
contentWidthClass="max-w-3xl"
|
|
/>
|
|
<div className="shrink-0 px-4 pb-4">
|
|
<div className="mx-auto w-full min-w-0 max-w-3xl">
|
|
<AgentPromptBar
|
|
placeholder="Add a follow up"
|
|
compact
|
|
busy={isStreaming}
|
|
onSubmit={(content, images) =>
|
|
sendMessage.mutateAsync({
|
|
content,
|
|
images,
|
|
model_id: activeSelection?.modelId ?? null,
|
|
effort: activeSelection?.effort ?? null,
|
|
})
|
|
}
|
|
models={models}
|
|
selection={activeSelection}
|
|
onSelectionChange={setSelection}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : isHydrating ? (
|
|
<div className="flex flex-1 flex-col items-center justify-center gap-4 px-6">
|
|
<p className="text-xs text-[var(--ui-text-dim)]">Loading conversation…</p>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-1 flex-col items-center justify-center gap-4 px-6">
|
|
<p className="text-xs text-[var(--ui-text-dim)]">
|
|
This thread has no messages yet.
|
|
</p>
|
|
<div className="w-full max-w-3xl">
|
|
<AgentPromptBar
|
|
placeholder="Send the first message"
|
|
compact
|
|
busy={isStreaming}
|
|
onSubmit={(content, images) =>
|
|
sendMessage.mutateAsync({
|
|
content,
|
|
images,
|
|
model_id: activeSelection?.modelId ?? null,
|
|
effort: activeSelection?.effort ?? null,
|
|
})
|
|
}
|
|
models={models}
|
|
selection={activeSelection}
|
|
onSelectionChange={setSelection}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<AgentGitPanel thread={thread} messages={baseMessages} />
|
|
</div>
|
|
)
|
|
}
|