Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb38b37ceb | |||
| 6698c20f25 |
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getTerminalScrollbackLimit } from "./terminal.tsx";
|
||||
|
||||
describe("getTerminalScrollbackLimit", () => {
|
||||
it("retains normal-buffer history for custom mobile swipe scrolling", () => {
|
||||
expect(getTerminalScrollbackLimit(true)).toBe(10_000);
|
||||
});
|
||||
|
||||
it("keeps desktop scrollback disabled to prevent stale-frame wheel scrolling", () => {
|
||||
expect(getTerminalScrollbackLimit(false)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -53,6 +53,10 @@ const BRACKETED_PASTE_DISABLE_SEQUENCE = [0x1b, 0x5b, 0x3f, 0x32, 0x30, 0x30, 0x
|
||||
const BRACKETED_PASTE_CONTROL_TAIL_LENGTH =
|
||||
BRACKETED_PASTE_ENABLE_SEQUENCE.length - 1;
|
||||
|
||||
export function getTerminalScrollbackLimit(isMobile: boolean): number {
|
||||
return isMobile ? 10_000 : 0;
|
||||
}
|
||||
|
||||
function matchesByteSequence(
|
||||
data: Uint8Array,
|
||||
start: number,
|
||||
@@ -326,14 +330,11 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: 0,
|
||||
allowTransparency: false,
|
||||
// This terminal only ever hosts full-screen TUI tools (pi-agent,
|
||||
// opencode), which repaint in place in the normal buffer and do not
|
||||
// use the alternate screen or mouse tracking. With scrollback, every
|
||||
// repaint accumulates as history → a viewport scrollbar appears and
|
||||
// the mouse-wheel scrolls through stale frames instead of the app.
|
||||
// scrollback:0 keeps only the live viewport: no bar, no stale-frame
|
||||
// wheel jank. (Scrollbar is also hidden via CSS for belt-and-suspenders.)
|
||||
scrollback: 0,
|
||||
// Desktop tools repaint in place, so retaining their normal buffer
|
||||
// creates stale frames that native wheel scrolling can revisit. Mobile
|
||||
// instead uses its custom touch handler to scroll normal-buffer output,
|
||||
// which requires retained history.
|
||||
scrollback: getTerminalScrollbackLimit(isMobile),
|
||||
ignoreBracketedPasteMode: false,
|
||||
fastScrollSensitivity: 0,
|
||||
scrollSensitivity: 0,
|
||||
@@ -699,7 +700,7 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
// Ignore disposal errors from partially torn-down terminal
|
||||
}
|
||||
};
|
||||
}, [instanceId, connectWebSocket]);
|
||||
}, [instanceId, connectWebSocket, isMobile]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
fit: () => {
|
||||
|
||||
@@ -578,23 +578,19 @@ export const InstanceList = ({
|
||||
{showCreate && (
|
||||
<div className="dialog-overlay" role="dialog" aria-modal="true">
|
||||
<div className="dialog">
|
||||
<div className="dialog-header">
|
||||
<h2>Launch Tool</h2>
|
||||
</div>
|
||||
<div className="dialog-body">
|
||||
<CreateSessionForm
|
||||
projects={[]}
|
||||
repositories={[]}
|
||||
toolTypes={toolTypes}
|
||||
fixedProjectId={projectId}
|
||||
fixedRepoId={repoId}
|
||||
projectName={projectName}
|
||||
repoName={repoName}
|
||||
onSuccess={handleCreateSuccess}
|
||||
onCancel={() => setShowCreate(false)}
|
||||
submitLabel="Launch"
|
||||
/>
|
||||
</div>
|
||||
<h2>Launch Tool</h2>
|
||||
<CreateSessionForm
|
||||
projects={[]}
|
||||
repositories={[]}
|
||||
toolTypes={toolTypes}
|
||||
fixedProjectId={projectId}
|
||||
fixedRepoId={repoId}
|
||||
projectName={projectName}
|
||||
repoName={repoName}
|
||||
onSuccess={handleCreateSuccess}
|
||||
onCancel={() => setShowCreate(false)}
|
||||
submitLabel="Launch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -62,7 +62,6 @@ export function StartToolFAB() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
{workspacesLoading ? (
|
||||
<p className="muted">Loading workspaces...</p>
|
||||
) : workspaces.length === 0 ? (
|
||||
@@ -111,7 +110,6 @@ export function StartToolFAB() {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { WorkspaceToolsPanel } from "./workspace-tools-panel";
|
||||
import type { Workspace } from "../../../types/workspace";
|
||||
|
||||
vi.mock("../../../hooks/use-workspace-instances", () => ({
|
||||
useWorkspaceInstances: () => ({
|
||||
instances: [],
|
||||
loading: false,
|
||||
refresh: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../tool/tool-starter", () => ({
|
||||
ToolStarter: () => <div data-testid="tool-starter" />,
|
||||
}));
|
||||
|
||||
const workspace: Workspace = {
|
||||
id: "workspace-1",
|
||||
name: "Main",
|
||||
repo_id: "repo-1",
|
||||
repo_name: "repository",
|
||||
repo_ssh_key_id: null,
|
||||
project_id: "project-1",
|
||||
project_name: "Project",
|
||||
user_id: "user-1",
|
||||
branch: "main",
|
||||
path: "/workspace",
|
||||
status: "ready",
|
||||
last_sync_at: null,
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
instance_count: 0,
|
||||
};
|
||||
|
||||
describe("WorkspaceToolsPanel", () => {
|
||||
it("places the tool launcher inside the shared scrollable dialog body", () => {
|
||||
const { container } = render(<WorkspaceToolsPanel workspace={workspace} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Start Tool" }));
|
||||
|
||||
const dialogBody = container.querySelector(".dialog-body");
|
||||
expect(dialogBody).toContainElement(screen.getByTestId("tool-starter"));
|
||||
});
|
||||
});
|
||||
@@ -64,19 +64,15 @@ export function WorkspaceToolsPanel({ workspace }: WorkspaceToolsPanelProps) {
|
||||
{showModal && (
|
||||
<div className="dialog-overlay" onClick={() => setShowModal(false)}>
|
||||
<div className="dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="dialog-header">
|
||||
<h3>Start Tool</h3>
|
||||
</div>
|
||||
<div className="dialog-body">
|
||||
<ToolStarter
|
||||
workspace={workspace}
|
||||
onStarted={() => {
|
||||
setShowModal(false);
|
||||
void refresh();
|
||||
}}
|
||||
onCancel={() => setShowModal(false)}
|
||||
/>
|
||||
</div>
|
||||
<h3>Start Tool</h3>
|
||||
<ToolStarter
|
||||
workspace={workspace}
|
||||
onStarted={() => {
|
||||
setShowModal(false);
|
||||
void refresh();
|
||||
}}
|
||||
onCancel={() => setShowModal(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -453,16 +453,12 @@
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.dialog,
|
||||
.modal-content,
|
||||
.commit-dialog {
|
||||
.dialog {
|
||||
width: 100%;
|
||||
max-width: 32rem;
|
||||
max-height: calc(100vh - var(--space-8));
|
||||
max-height: calc(100dvh - var(--space-8));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
@@ -472,6 +468,16 @@
|
||||
|
||||
.modal-content {
|
||||
/* deprecated alias */
|
||||
width: 100%;
|
||||
max-width: 32rem;
|
||||
max-height: calc(100vh - var(--space-8));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.dialog-lg {
|
||||
@@ -494,13 +500,9 @@
|
||||
line-height: var(--line-height-tight);
|
||||
}
|
||||
|
||||
.dialog-body,
|
||||
.modal-body {
|
||||
.dialog-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
@@ -541,11 +543,9 @@
|
||||
}
|
||||
|
||||
.dialog,
|
||||
.modal-content,
|
||||
.commit-dialog {
|
||||
.modal-content {
|
||||
max-width: 100%;
|
||||
max-height: calc(100vh - var(--space-6));
|
||||
max-height: calc(100dvh - var(--space-6));
|
||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2328,10 +2328,8 @@ a.nav-item,
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
max-height: 70vh;
|
||||
max-height: 70dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
animation: slide-up 0.2s ease-out;
|
||||
}
|
||||
|
||||
@@ -2367,12 +2365,8 @@ a.nav-item,
|
||||
}
|
||||
|
||||
.mobile-bottom-sheet-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 8px 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.mobile-bottom-sheet-item {
|
||||
@@ -3246,10 +3240,7 @@ a:active,
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
max-height: 80dvh;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
animation: slideUp 0.3s ease;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
}
|
||||
@@ -3611,7 +3602,6 @@ a:active,
|
||||
max-width: none;
|
||||
border-radius: 12px;
|
||||
max-height: 70vh;
|
||||
max-height: 70dvh;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# Constrain and Scroll Edit Dialogs and Popups
|
||||
|
||||
## Summary
|
||||
|
||||
Ensure every edit dialog, form popup, and modal popup remains usable on a viewport that is shorter than its content. Dialog chrome must stay within the visible viewport while the content area scrolls independently.
|
||||
|
||||
## Problem
|
||||
|
||||
The shared dialog system constrains only components that follow its `dialog-header` / `dialog-body` structure. Several tool-launch and commit dialogs place form content directly inside the container or use a bespoke container, so long forms can be clipped. Existing viewport sizing also relies on `vh`, which is unreliable when mobile browser chrome changes height.
|
||||
|
||||
## Scope
|
||||
|
||||
- Shared dialog and modal CSS in `apps/web/src/styles/global.css`.
|
||||
- Mobile notification dropdown sizing in `apps/web/src/styles/utilities.css`.
|
||||
- Tool-launch and commit popup markup that does not currently provide a scrollable content region.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Dialogs and modal popups are bounded by the current visible viewport, including mobile dynamic viewport changes.
|
||||
- [ ] Headers and footer/action bars remain visible while long form content scrolls independently.
|
||||
- [ ] Tool-launch and commit popups use the shared scrollable content pattern.
|
||||
- [ ] Mobile sheets, action sheets, and notification dropdowns remain scrollable without propagating scroll gestures to the page.
|
||||
- [ ] Relevant frontend tests, typecheck, lint, and production build pass.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Redesigning dialog visuals or interaction flows.
|
||||
- Changing page-level scrolling outside overlays.
|
||||
@@ -1,8 +0,0 @@
|
||||
# Constrain and Scroll Edit Dialogs and Popups — Tasks
|
||||
|
||||
- [x] Audit all dialog, modal, sheet, action-sheet, and popup implementations.
|
||||
- [x] Strengthen shared dialog/modal viewport and body scrolling rules.
|
||||
- [x] Update bespoke tool-launch and commit popups to use scrollable content regions.
|
||||
- [x] Add focused coverage for the shared scrollable dialog-body markup.
|
||||
- [x] Run frontend typecheck, lint, tests (88 passed), and production build.
|
||||
- [x] Update project maps for changed source files.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Restore Mobile Terminal Scrolling
|
||||
|
||||
## Summary
|
||||
|
||||
The recent terminal scrollback optimization disabled scrollback for every viewport. Mobile terminal swipe handling still scrolls xterm's normal buffer programmatically, so swipes in normal-buffer tools no longer have retained output to move through.
|
||||
|
||||
## Root Cause
|
||||
|
||||
`468f342` changed the terminal configuration to `scrollback: 0` globally to prevent stale repaint frames and wheel scrolling on desktop. The mobile touch handler calls `term.scrollLines()` when the normal buffer is active. With zero scrollback, that call has no scrollable history and becomes a no-op.
|
||||
|
||||
## Scope
|
||||
|
||||
- `apps/web/src/components/features/terminal/terminal.tsx`
|
||||
- Focused terminal configuration test
|
||||
|
||||
## Fix
|
||||
|
||||
Retain a bounded xterm scrollback buffer on mobile only (`10000` lines), while leaving desktop at zero scrollback and with wheel sensitivity disabled. Mobile's existing custom touch handler remains responsible for moving through normal-buffer history; alternate-screen swipes continue to send SGR wheel events to the active TUI.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] A mobile terminal with normal-buffer output exceeding one screen scrolls via a vertical swipe.
|
||||
- [ ] Alternate-screen terminal scrolling continues to use the existing SGR wheel-event path.
|
||||
- [ ] Desktop keeps zero xterm scrollback and disabled native wheel scrolling, so stale repaint frames do not return.
|
||||
- [ ] Focused unit test and frontend quality gates pass.
|
||||
|
||||
## Related
|
||||
|
||||
- `468f342 fix(terminal): hide scrollbar and stop stale-frame wheel scroll for TUI tools`
|
||||
- `openspec/changes/fix-terminal-container-overflow`
|
||||
@@ -0,0 +1,9 @@
|
||||
# Restore Mobile Terminal Scrolling — Tasks
|
||||
|
||||
- [x] Add a mobile-specific terminal scrollback limit while preserving zero scrollback on desktop.
|
||||
- [x] Reinitialize the terminal when the responsive mobile classification changes so its scrollback and touch handler match the active viewport.
|
||||
- [x] Add focused tests for the responsive scrollback configuration.
|
||||
- [x] Run the full frontend test suite (89 tests passed after repairing the `ProjectsPage` test setup).
|
||||
- [x] Run frontend typecheck, lint, focused tests, and production build.
|
||||
- [ ] Perform mobile normal-buffer and alternate-screen manual QA.
|
||||
- [ ] Update project maps for changed source files (the map patch tool currently fails with an unsupported `temperature` parameter).
|
||||
Reference in New Issue
Block a user