Skip to content
Open
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
7 changes: 6 additions & 1 deletion web/src/components/InputBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const supportsFieldSizing = CSS.supports("field-sizing", "content");

interface CommonProps {
onInterrupt: () => void;
/** narrow screens hide the composer while the reader is scrolled up */
hidden?: boolean;
isProcessing: boolean;
stdinClosed?: boolean;
disabled: boolean;
Expand Down Expand Up @@ -361,6 +363,7 @@ export function InputBox(props: InputBoxProps) {
function OrdinaryInputBox({
onSend,
onInterrupt,
hidden = false,
isProcessing,
stdinClosed,
disabled,
Expand Down Expand Up @@ -527,7 +530,9 @@ function OrdinaryInputBox({

return (
<div
class={`input-box${isDragging ? " input-box-dragging" : ""}`}
class={`input-box${isDragging ? " input-box-dragging" : ""}${
hidden ? " input-box-hidden" : ""
}`}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
Expand Down
15 changes: 15 additions & 0 deletions web/src/components/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ interface Props {
onViewFile?: (filePath: string) => void;
spawnedTidsByItemId?: Map<string, Map<number, number>>;
getTaskHref?: (id: string) => string;
/** fires when the list settles at, or leaves, the bottom */
onAtBottomChange?: (atBottom: boolean) => void;
}

function ResultMessageView({ message }: { message: DisplayMessage }) {
Expand Down Expand Up @@ -851,8 +853,12 @@ export function MessageList({
onViewFile,
spawnedTidsByItemId,
getTaskHref,
onAtBottomChange,
}: Props) {
const containerRef = useRef<HTMLDivElement>(null);
// through a ref so the scroll listener never has to be torn down and rebound
const onAtBottomChangeRef = useRef(onAtBottomChange);
onAtBottomChangeRef.current = onAtBottomChange;

// Subscribe to outbox so the component re-renders when entries are added/removed.
const [outboxTick, setOutboxTick] = useState(0);
Expand Down Expand Up @@ -931,8 +937,17 @@ export function MessageList({
useEffect(() => {
const el = containerRef.current;
if (!el) return;
// a little tolerance: momentum scrolling lands a pixel or two off zero
const atBottom = () => el.scrollTop >= -24;
let wasAtBottom = atBottom();
onAtBottomChangeRef.current?.(wasAtBottom);
const onScroll = () => {
el.style.overflowAnchor = el.scrollTop >= -1 ? "none" : "auto";
const now = atBottom();
if (now !== wasAtBottom) {
wasAtBottom = now;
onAtBottomChangeRef.current?.(now);
}
};
el.addEventListener("scroll", onScroll, { passive: true });
return () => {
Expand Down
19 changes: 19 additions & 0 deletions web/src/components/SessionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,23 @@ function SessionViewInner({
agentUsage,
}: Props) {
const inputRef = useRef<HTMLTextAreaElement>(null);
// a phone pins the composer above the keyboard, and iOS lets the two drift
// apart once the page scrolls, leaving the box floating over a gap that only
// a drag starting inside it puts right. Scrolling up to read is also the
// moment neither is wanted, so leaving the bottom dismisses both and
// returning restores the composer.
const [composerHidden, setComposerHidden] = useState(false);
const handleAtBottomChange = useCallback((atBottom: boolean) => {
const narrow =
typeof window !== "undefined" &&
typeof window.matchMedia === "function" &&
window.matchMedia("(max-width: 768px)").matches;
if (!narrow) return;
setComposerHidden(!atBottom);
// blurring is what dismisses the keyboard; iOS will not raise it again
// without a tap, so returning to the bottom restores only the box
if (!atBottom) inputRef.current?.blur();
}, []);
const insertTextRef = useRef<((text: string) => void) | null>(null);
const pasteTextRef = useRef<((text: string) => void) | null>(null);
const resumeRef = useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -415,6 +432,7 @@ function SessionViewInner({
</div>
) : (
<MessageList
onAtBottomChange={handleAtBottomChange}
taskTid={tid}
messages={task.messages}
replacementEvents={task.replacementEvents}
Expand Down Expand Up @@ -494,6 +512,7 @@ function SessionViewInner({
}}
isProcessing={task.isProcessing}
stdinClosed={task.stdinClosed}
hidden={composerHidden}
disabled={!connected}
sessionId={task.uuid}
inputDraft={task.inputDraft}
Expand Down
7 changes: 7 additions & 0 deletions web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -3747,6 +3747,13 @@ pre.extra-field-value {

/* Responsive: narrow screens */
@media (max-width: 768px) {
/* scrolled up to read: the composer and its keyboard are only in the way,
and iOS drifts them apart anyway once the page scrolls. wider screens keep
the composer, where there is room for it and no keyboard to dismiss */
.input-box.input-box-hidden {
display: none;
}

.banner-usage {
width: 132px;
flex-basis: 132px;
Expand Down