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
73 changes: 17 additions & 56 deletions app/session/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Redirect, router, useLocalSearchParams } from "expo-router";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import {
FlatList,
KeyboardAvoidingView,
Platform,
Pressable,
Expand All @@ -11,16 +10,20 @@ import {
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { identityLabel } from "@codeoid/core";
import { FlashList } from "@shopify/flash-list";
import type { SessionMessage } from "@codeoid/protocol";

import { ApprovalBar } from "@/components/ApprovalBar";
import { MessageRow } from "@/components/MessageRow";
import { getConnection, mintClientMsgId, type Connection } from "@/lib/connection";
import { useConnectionStatus, useTranscript } from "@/lib/hooks";
import { palette } from "@/lib/theme";

// Transcript view: attach on entry (incremental resume when a cursor exists),
// live-render via MessageStore.ingest(), detach on leave. Plain-text rows
// first (design doc §10 P1) — FlashList + streaming markdown arrive in P2.
// live-render via MessageStore.ingest(), detach on leave. P2 upgrades the P1
// plain-text rows to markdown + tool-call state machine + Edit/Write diffs
// (MessageRow), a docked approve/deny bar (ApprovalBar), and a recycling
// FlashList that sticks to the bottom while streaming.
export default function SessionScreen() {
const { id, name } = useLocalSearchParams<{ id: string; name?: string }>();
const conn = getConnection();
Expand All @@ -43,7 +46,6 @@ function Transcript({
const [draft, setDraft] = useState("");
const [sendError, setSendError] = useState<string | null>(null);
const [attachError, setAttachError] = useState<string | null>(null);
const listRef = useRef<FlatList<SessionMessage>>(null);

// (Re-)attach on every `connected` transition — the initial entry and any
// reconnect (a dropped socket loses the attachment). The resume cursor
Expand Down Expand Up @@ -108,16 +110,20 @@ function Transcript({

{attachError ? <Text style={styles.error}>attach failed: {attachError}</Text> : null}

<FlatList
ref={listRef}
<FlashList
data={messages}
keyExtractor={(m) => m.messageId}
renderItem={({ item }) => <MessageRow msg={item} />}
renderItem={({ item }: { item: SessionMessage }) => <MessageRow conn={conn} msg={item} />}
contentContainerStyle={styles.listContent}
onContentSizeChange={() => listRef.current?.scrollToEnd({ animated: false })}
maintainVisibleContentPosition={{
startRenderingFromBottom: true,
autoscrollToBottomThreshold: 0.2,
}}
ListEmptyComponent={<Text style={styles.empty}>Waiting for scrollback…</Text>}
/>

<ApprovalBar conn={conn} sessionId={sessionId} />

{sendError ? <Text style={styles.error}>send failed: {sendError}</Text> : null}

<View style={[styles.composer, { paddingBottom: Math.max(insets.bottom, 10) }]}>
Expand All @@ -141,41 +147,6 @@ function Transcript({
);
}

function MessageRow({ msg }: { msg: SessionMessage }) {
const who = identityLabel(msg.identity);
if (msg.role === "tool_call" && msg.tool) {
const { tool } = msg;
const detail =
tool.state.phase === "waiting_confirmation"
? `awaiting approval — ${tool.state.description}`
: tool.state.phase === "completed"
? tool.state.success
? "ok"
: "failed"
: tool.state.phase;
return (
<View style={styles.row}>
<Text style={styles.meta}>
{who} · tool: {tool.name}
</Text>
<Text style={styles.toolText}>
[{detail}]{msg.content ? `\n${msg.content}` : ""}
</Text>
</View>
);
}
return (
<View style={styles.row}>
<Text style={styles.meta}>
{who} · {msg.role}
</Text>
<Text style={[styles.content, msg.role === "user" && styles.userContent]}>
{msg.content}
</Text>
</View>
);
}

const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: palette.bg },
header: {
Expand All @@ -190,17 +161,7 @@ const styles = StyleSheet.create({
back: { fontSize: 15, color: palette.accent },
title: { flex: 1, fontSize: 16, fontWeight: "600", color: palette.text },
status: { fontSize: 12, color: palette.amber },
listContent: { padding: 16, gap: 14 },
row: { gap: 4 },
meta: { fontSize: 12, color: palette.textDim },
content: { fontSize: 15, lineHeight: 21, color: palette.text },
userContent: { color: palette.accent },
toolText: {
fontSize: 13,
lineHeight: 18,
color: palette.textDim,
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
},
listContent: { padding: 16 },
empty: { textAlign: "center", color: palette.textDim, marginTop: 48, fontSize: 14 },
error: { color: palette.red, fontSize: 12, paddingHorizontal: 16, paddingVertical: 4 },
composer: {
Expand Down
Loading
Loading