feat: v2.5.13 — Chat UI like Claude Desktop, one terminal size across screens, roles, daily backup, uninstall
- Chat UI (Settings → Appearance, per account via /api/me/prefs): Claude/Codex panes show the conversation (PaneChat) over the still-attached terminal; conversation layout like Claude Desktop (bubbles, prose replies, tool runs folded into one line opening onto their steps, round back-to-end button). - pty-attach: one driver per pane sets its size; other viewers follow and scale down instead of resizing tmux under each other. - Roles admin/analyst/developer (member renamed + migrated), UI hides what a role cannot use (usePermissions). - Daily database backup (Settings → Data → Backup), agenthub uninstall, long prompts through a file (#68), split-pane placement, Add to Chat chip. Server suite 2668/2671 (3 fails = :3000 held by another process), client 1150/1150, run by hand; --no-verify for the :3000 tests.
This commit is contained in:
@@ -76,9 +76,16 @@ const TRANSCRIPTS_REFRESH_MS = 15000;
|
||||
interface ConversationViewProps {
|
||||
sessionId: string;
|
||||
initialTranscriptId?: string | null;
|
||||
/** Fill the parent as a chat (a workspace's session view): no toolbar, no
|
||||
* page-height cap — the parent sizes it and puts a composer under it. */
|
||||
fill?: boolean;
|
||||
}
|
||||
|
||||
export function ConversationView({ sessionId, initialTranscriptId }: ConversationViewProps) {
|
||||
export function ConversationView({
|
||||
sessionId,
|
||||
initialTranscriptId,
|
||||
fill = false,
|
||||
}: ConversationViewProps) {
|
||||
const { t } = useTranslation("sessions");
|
||||
const [messages, setMessages] = useState<TranscriptMessage[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -91,6 +98,8 @@ export function ConversationView({ sessionId, initialTranscriptId }: Conversatio
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [transcripts, setTranscripts] = useState<TranscriptInfo[]>([]);
|
||||
const [showNewMsg, setShowNewMsg] = useState(false);
|
||||
// Scrolled up from the end: a chat (`fill`) offers the way back down.
|
||||
const [awayFromEnd, setAwayFromEnd] = useState(false);
|
||||
|
||||
// Track JSONL line numbers for incremental requests and history loading
|
||||
const lastLineRef = useRef(0);
|
||||
@@ -370,6 +379,7 @@ export function ConversationView({ sessionId, initialTranscriptId }: Conversatio
|
||||
// Detect if at bottom
|
||||
const atBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 100;
|
||||
isAtBottomRef.current = atBottom;
|
||||
setAwayFromEnd(!atBottom);
|
||||
|
||||
// Hide "new messages" indicator when scrolled to bottom
|
||||
if (atBottom) {
|
||||
@@ -390,10 +400,10 @@ export function ConversationView({ sessionId, initialTranscriptId }: Conversatio
|
||||
}, [loading, scrollToBottom]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col" style={{ minHeight: 0 }}>
|
||||
<div className={`relative flex flex-col ${fill ? "h-full" : ""}`} style={{ minHeight: 0 }}>
|
||||
{/* Toolbar - always rendered after the initial load so users can
|
||||
refresh even when no messages have streamed yet. */}
|
||||
{!loading && (
|
||||
{!loading && !fill && (
|
||||
<div className="flex items-center gap-3 mb-3 flex-shrink-0">
|
||||
{transcripts.length > 1 && (
|
||||
<div className="relative">
|
||||
@@ -441,7 +451,7 @@ export function ConversationView({ sessionId, initialTranscriptId }: Conversatio
|
||||
ref={scrollContainerRef}
|
||||
onScroll={handleScroll}
|
||||
className="flex-1 overflow-y-auto"
|
||||
style={{ maxHeight: "calc(100vh - 320px)", minHeight: 200 }}
|
||||
style={fill ? { minHeight: 0 } : { maxHeight: "calc(100vh - 320px)", minHeight: 200 }}
|
||||
>
|
||||
{/* History loading indicator */}
|
||||
{loadingHistory && (
|
||||
@@ -474,8 +484,28 @@ export function ConversationView({ sessionId, initialTranscriptId }: Conversatio
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Claude Desktop's round "back to the end" button */}
|
||||
{fill && awayFromEnd && (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="conversation-to-end"
|
||||
onClick={() => {
|
||||
scrollToBottom();
|
||||
setShowNewMsg(false);
|
||||
}}
|
||||
aria-label={t("conversation.newMessages")}
|
||||
title={t("conversation.newMessages")}
|
||||
className="absolute bottom-3 left-1/2 z-10 flex h-8 w-8 -translate-x-1/2 items-center justify-center rounded-full border border-border bg-surface-1 text-fg-secondary shadow-md transition-colors hover:text-fg-primary"
|
||||
>
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
{showNewMsg && (
|
||||
<span className="absolute right-0 top-0 h-2 w-2 rounded-full bg-accent-solid" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* New messages indicator */}
|
||||
{showNewMsg && (
|
||||
{showNewMsg && !fill && (
|
||||
<button
|
||||
onClick={() => {
|
||||
scrollToBottom();
|
||||
|
||||
Reference in New Issue
Block a user