Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb38b37ceb | |||
| 6698c20f25 |
@@ -283,27 +283,16 @@ def clone_git_repo(
|
||||
url_hash = hashlib.md5(f"{remote_url}:{branch_segment}".encode()).hexdigest()[:12]
|
||||
repo_name = remote_url.split("/")[-1].replace(".git", "") or "repo"
|
||||
clone_dir = os.path.join(clone_parent, "git-mounts", f"{repo_name}-{url_hash}")
|
||||
clone_name = _slugify_directory_name(project_name) if project_name else "repo-clone"
|
||||
repo_path = os.path.join(clone_dir, clone_name)
|
||||
repo_path = clone_repository(
|
||||
remote_url,
|
||||
None, # No SSH key for now - can be added later
|
||||
clone_dir,
|
||||
branch or "main",
|
||||
project_name=project_name,
|
||||
)
|
||||
|
||||
if os.path.isdir(os.path.join(repo_path, ".git")):
|
||||
# Reuse the deterministic per-repository cache on repeated starts.
|
||||
if not os.path.exists(repo_path):
|
||||
try:
|
||||
pull_repository_updates(repo_path, remote_url)
|
||||
logger.debug("Pulled updates for git mount %s", remote_url)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to pull updates for %s: %s", remote_url, exc)
|
||||
else:
|
||||
try:
|
||||
# A failed clone can leave its destination behind. Remove only the
|
||||
# computed clone path so the next start can retry cleanly.
|
||||
if os.path.lexists(repo_path):
|
||||
logger.warning("Removing incomplete git mount clone at %s", repo_path)
|
||||
if os.path.isdir(repo_path) and not os.path.islink(repo_path):
|
||||
shutil.rmtree(repo_path)
|
||||
else:
|
||||
os.unlink(repo_path)
|
||||
|
||||
os.makedirs(clone_dir, exist_ok=True)
|
||||
repo_path = clone_repository(
|
||||
remote_url,
|
||||
@@ -316,6 +305,13 @@ def clone_git_repo(
|
||||
except Exception as exc:
|
||||
logger.warning("Clone failed for git mount %s: %s", remote_url, exc)
|
||||
raise
|
||||
else:
|
||||
# Repo exists - pull latest updates
|
||||
try:
|
||||
pull_repository_updates(repo_path, remote_url)
|
||||
logger.debug("Pulled updates for git mount %s", remote_url)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to pull updates for %s: %s", remote_url, exc)
|
||||
|
||||
# Handle branch checkout if specified
|
||||
if branch and repo_path:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Unit tests for the tool instance service."""
|
||||
|
||||
import hashlib
|
||||
import uuid
|
||||
from unittest.mock import MagicMock, AsyncMock
|
||||
|
||||
@@ -9,7 +8,6 @@ import pytest
|
||||
from src.services.tool.instance_service import (
|
||||
_get_repository_mount_name,
|
||||
_stack_profile_mounts_with_git_mounts,
|
||||
clone_git_repo,
|
||||
modify_compose_file,
|
||||
prepare_manifest_instance,
|
||||
)
|
||||
@@ -69,61 +67,6 @@ class TestGetRepositoryMountName:
|
||||
assert _get_repository_mount_name(project, repo) == "project-v2-0"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCloneGitRepo:
|
||||
"""Regression tests for reusable config-profile git mount clones."""
|
||||
|
||||
def test_reuses_existing_clone(self, monkeypatch, tmp_path) -> None:
|
||||
from src.services.tool import instance_service
|
||||
|
||||
remote_url = "https://gitlab.com/example/dotfiles"
|
||||
branch = None
|
||||
clone_parent = str(tmp_path)
|
||||
url_hash = hashlib.md5(f"{remote_url}:default".encode()).hexdigest()[:12]
|
||||
repo_path = (
|
||||
tmp_path
|
||||
/ "git-mounts"
|
||||
/ f"dotfiles-{url_hash}"
|
||||
/ "repo-clone"
|
||||
)
|
||||
(repo_path / ".git").mkdir(parents=True)
|
||||
|
||||
clone = MagicMock(side_effect=AssertionError("existing clone must be reused"))
|
||||
pull = MagicMock()
|
||||
monkeypatch.setattr(instance_service, "clone_repository", clone)
|
||||
monkeypatch.setattr(instance_service, "pull_repository_updates", pull)
|
||||
|
||||
result = clone_git_repo(remote_url, branch, clone_parent)
|
||||
|
||||
assert result == str(repo_path)
|
||||
clone.assert_not_called()
|
||||
pull.assert_called_once_with(str(repo_path), remote_url)
|
||||
|
||||
def test_replaces_incomplete_clone_before_retry(self, monkeypatch, tmp_path) -> None:
|
||||
from src.services.tool import instance_service
|
||||
|
||||
remote_url = "https://gitlab.com/example/dotfiles"
|
||||
url_hash = hashlib.md5(f"{remote_url}:main".encode()).hexdigest()[:12]
|
||||
clone_dir = tmp_path / "git-mounts" / f"dotfiles-{url_hash}"
|
||||
repo_path = clone_dir / "repo-clone"
|
||||
repo_path.mkdir(parents=True)
|
||||
(repo_path / "partial-file").write_text("incomplete")
|
||||
|
||||
def clone(_url, _key, destination, _branch, project_name=None):
|
||||
assert destination == str(clone_dir)
|
||||
assert project_name is None
|
||||
assert not repo_path.exists()
|
||||
(repo_path / ".git").mkdir(parents=True)
|
||||
return str(repo_path)
|
||||
|
||||
monkeypatch.setattr(instance_service, "clone_repository", clone)
|
||||
|
||||
result = clone_git_repo(remote_url, "main", str(tmp_path))
|
||||
|
||||
assert result == str(repo_path)
|
||||
assert (repo_path / ".git").is_dir()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestStackProfileMountsWithGitMounts:
|
||||
"""Tests for _stack_profile_mounts_with_git_mounts."""
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#275d4b" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<title>Headquarter</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Headquarter</title>
|
||||
<rect width="64" height="64" rx="15" fill="#275d4b"/>
|
||||
<path fill="#fffef9" d="M17 15h8v13h14V15h8v34h-8V36H25v13h-8z"/>
|
||||
<path fill="#9dcdb7" d="M25 28h14v8H25z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 266 B |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,6 @@ map: openspec/.pi-map.md
|
||||
- openspec/changes/archive
|
||||
index: openspec/changes/archive/.pi-map.index.md
|
||||
map: openspec/changes/archive/.pi-map.md
|
||||
- openspec/changes/fix-config-profile-git-mount-clone-reuse
|
||||
index: openspec/changes/fix-config-profile-git-mount-clone-reuse/.pi-map.index.md
|
||||
map: openspec/changes/fix-config-profile-git-mount-clone-reuse/.pi-map.md
|
||||
- openspec/changes/fix-container-status-false-positive
|
||||
index: openspec/changes/fix-container-status-false-positive/.pi-map.index.md
|
||||
map: openspec/changes/fix-container-status-false-positive/.pi-map.md
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# Add a Headquarter Favicon
|
||||
|
||||
## Summary
|
||||
|
||||
Add a compact, recognizable favicon for Headquarter and register it in the web document head.
|
||||
|
||||
## Design
|
||||
|
||||
Use a geometric cream `H` on the product's evergreen brand field. The mark remains identifiable at small browser-tab sizes, avoids font rendering dependencies, and matches both light and dark application themes.
|
||||
|
||||
## Scope
|
||||
|
||||
- `apps/web/public/favicon.svg`
|
||||
- `apps/web/index.html`
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] The browser uses a dedicated Headquarter favicon.
|
||||
- [ ] The mark remains legible at small sizes and on light or dark browser chrome.
|
||||
- [ ] The HTML declares the icon type and a matching browser theme color.
|
||||
- [ ] Frontend production build passes.
|
||||
@@ -1,6 +0,0 @@
|
||||
# Add a Headquarter Favicon — Tasks
|
||||
|
||||
- [x] Create the favicon asset.
|
||||
- [x] Register the favicon and browser theme color in the web entry document.
|
||||
- [x] Run the frontend production build.
|
||||
- [x] Update project maps for changed source files.
|
||||
@@ -1,20 +0,0 @@
|
||||
# openspec/changes/fix-config-profile-git-mount-clone-reuse (index)
|
||||
dir: openspec/changes/fix-config-profile-git-mount-clone-reuse
|
||||
|
||||
## role
|
||||
Documents and tracks a bug fix for reusing cached Config Profile git mount clones during tool instance startup.
|
||||
## parent
|
||||
index: openspec/changes/.pi-map.index.md
|
||||
map: openspec/changes/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- change.md
|
||||
- tasks.md
|
||||
## links
|
||||
index: openspec/changes/fix-config-profile-git-mount-clone-reuse/.pi-map.index.md
|
||||
map: openspec/changes/fix-config-profile-git-mount-clone-reuse/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -1,20 +0,0 @@
|
||||
# openspec/changes/fix-config-profile-git-mount-clone-reuse
|
||||
dir: openspec/changes/fix-config-profile-git-mount-clone-reuse
|
||||
|
||||
index: openspec/changes/fix-config-profile-git-mount-clone-reuse/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Documents and tracks a bug fix for reusing cached Config Profile git mount clones during tool instance startup.
|
||||
## files
|
||||
- change.md | Describes the cached clone regression, required behavior, implementation scope, and verification plan.
|
||||
- tasks.md | Tracks investigation, regression testing, implementation, project-map maintenance, verification, and commit status.
|
||||
## arch
|
||||
Documentation-only OpenSpec change package with separate change rationale and implementation task checklist.
|
||||
## tags
|
||||
config, profile, git, mount, clone, cache, startup, fix
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -1,27 +0,0 @@
|
||||
# Fix config profile git mount clone reuse
|
||||
|
||||
## Problem
|
||||
|
||||
Starting a tool instance with a Config Profile git mount can omit the mount when the repository was already cloned into the instance cache. The startup log reports that the destination path already exists and is not an empty directory.
|
||||
|
||||
## Root cause
|
||||
|
||||
`clone_git_repo()` computes a deterministic cache directory but calls `clone_repository()` before checking whether that directory already contains a clone. `git clone` therefore fails on repeated starts or overlapping start requests. `resolve_single_git_mount()` treats auxiliary mount failures as non-blocking, so startup continues without the configured volume.
|
||||
|
||||
## Required behavior
|
||||
|
||||
1. A valid existing git mount clone must be reused and updated instead of cloned again.
|
||||
2. A missing clone must still be created normally.
|
||||
3. An incomplete clone directory must not permanently prevent a later retry.
|
||||
4. A clone/update failure remains non-blocking at the git mount resolver boundary.
|
||||
|
||||
## Scope
|
||||
|
||||
- Correct clone-cache handling in `apps/api/src/services/tool/instance_service.py`.
|
||||
- Add focused regression tests in `apps/api/tests/unit/test_instance_service.py`.
|
||||
- No API, database, frontend, or Docker Compose contract changes.
|
||||
|
||||
## Verification
|
||||
|
||||
- Targeted `pytest` for git mount clone reuse and instance service tests.
|
||||
- Ruff and mypy checks for changed backend files.
|
||||
@@ -1,9 +0,0 @@
|
||||
# Tasks: fix config profile git mount clone reuse
|
||||
|
||||
- [x] Capture the failing runtime trace from an affected tool session.
|
||||
- [x] Add a regression test proving a valid cached clone is reused.
|
||||
- [x] Update `clone_git_repo()` to check the deterministic clone path before cloning.
|
||||
- [x] Recover safely from an incomplete clone directory.
|
||||
- [x] Run targeted backend tests and quality checks.
|
||||
- [x] Update project maps and validate map freshness.
|
||||
- [x] Commit the verified fix.
|
||||
@@ -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