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
38 changes: 29 additions & 9 deletions agents/workspace_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,26 @@ def _migrate_legacy_idea_workspace(workspace_root: Path) -> None:
shutil.move(str(legacy_exp), str(suite_main))


def _ensure_dir_or_current_link(path: Path) -> None:
"""Create a directory unless the path is already a symlink/current pointer.

``current`` paths are allowed to be:
- a real directory
- a symlink to the canonical run / paper dir
- a placeholder directory later populated with ``CURRENT_RUN.txt``

``Path.mkdir(exist_ok=True)`` still raises ``FileExistsError`` on symlinks
on some runtimes, so we special-case them here.
"""
if path.is_symlink():
return
if path.exists():
if path.is_dir():
return
raise FileExistsError(f"Workspace path exists and is not a directory: {path}")
path.mkdir(parents=True, exist_ok=True)


def _resolve_suite_for_run(run_id: int) -> str:
row = db.fetchone("SELECT experiment_suite FROM experiment_runs WHERE id=?", (int(run_id),))
if row:
Expand Down Expand Up @@ -137,14 +157,14 @@ def get_idea_workspace(insight_id: int, insight: dict | None = None, *, create:
"canonical_run_id": insight.get("canonical_run_id"),
}
if create:
workspace_root.mkdir(parents=True, exist_ok=True)
plan_root.mkdir(parents=True, exist_ok=True)
_ensure_dir_or_current_link(workspace_root)
_ensure_dir_or_current_link(plan_root)
for key in ("paper_root", "paper_current_root", "paper_bundles_root", "paper_manifests_root"):
Path(layout[key]).mkdir(parents=True, exist_ok=True)
experiments_root.mkdir(parents=True, exist_ok=True)
_ensure_dir_or_current_link(Path(layout[key]))
_ensure_dir_or_current_link(experiments_root)
for suite in KNOWN_EXPERIMENT_SUITES:
(experiments_root / suite / "runs").mkdir(parents=True, exist_ok=True)
(experiments_root / suite / "current").mkdir(parents=True, exist_ok=True)
_ensure_dir_or_current_link(experiments_root / suite / "runs")
_ensure_dir_or_current_link(experiments_root / suite / "current")

if sync_db and insight.get("id") is not None:
desired = {
Expand Down Expand Up @@ -197,10 +217,10 @@ def ensure_run_workspace(
"spec_root": run_root / "spec",
"codex_root": run_root / "codex",
}
suite_runs_root.mkdir(parents=True, exist_ok=True)
suite_current_root.mkdir(parents=True, exist_ok=True)
_ensure_dir_or_current_link(suite_runs_root)
_ensure_dir_or_current_link(suite_current_root)
for key in ("run_root", "code_root", "results_root", "spec_root", "codex_root"):
Path(info[key]).mkdir(parents=True, exist_ok=True)
_ensure_dir_or_current_link(Path(info[key]))
return info


Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies = [
"numpy>=1.26",
"pydantic>=2.0",
"pymupdf>=1.24",
"PyYAML>=6.0",
"waitress>=3.0",
]
keywords = ["knowledge-graph", "science", "literature-mining", "arxiv", "research-ideas", "open-science"]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ pymupdf>=1.24
psycopg[binary]>=3.1
matplotlib>=3.8
numpy>=1.26
PyYAML>=6.0
waitress>=3.0
16 changes: 16 additions & 0 deletions tests/test_workspace_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ def test_promote_canonical_run_falls_back_to_marker_when_current_dir_locked(self
self.assertTrue(marker.exists())
self.assertEqual(marker.read_text(encoding="utf-8"), str(layout["run_root"]))

def test_get_idea_workspace_tolerates_existing_current_symlink(self):
database.execute("INSERT INTO deep_insights (id, tier, title) VALUES (1, 2, 'Idea Workspace')")
database.commit()

root = self.workspace_root / "idea_1"
target = root / "legacy_target"
target.mkdir(parents=True, exist_ok=True)
current_link = root / "experiments" / "main" / "current"
current_link.parent.mkdir(parents=True, exist_ok=True)
current_link.symlink_to(target, target_is_directory=True)

layout = workspace_layout.get_idea_workspace(1)

self.assertTrue(Path(layout["experiment_current_root"]).is_symlink())
self.assertEqual(Path(layout["experiment_current_root"]).resolve(), target.resolve())

def test_backfill_script_maps_legacy_run_and_manuscript_dirs(self):
legacy_run = Path(self.tmpdir.name) / "legacy_run"
(legacy_run / "code").mkdir(parents=True, exist_ok=True)
Expand Down
70 changes: 70 additions & 0 deletions web/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,76 @@ header#topBar {
overflow-y: auto;
}

.paper-mini-stats {
margin-bottom: 14px;
}

.stat-card-mini {
min-height: 86px;
}

.paper-flow-list {
display: flex;
flex-direction: column;
gap: 10px;
}

.paper-flow-item {
border: 1px solid var(--border);
background: var(--bg-card);
border-radius: var(--radius);
padding: 14px 16px;
transition: border-color var(--transition), background var(--transition), transform var(--transition);
}

.paper-flow-item:hover {
border-color: var(--border-hover);
background: var(--bg-card-hover);
transform: translateY(-1px);
}

.paper-flow-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
margin-bottom: 8px;
}

.paper-flow-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-primary);
line-height: 1.45;
}

.paper-flow-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
color: var(--text-dim);
font-size: 0.72rem;
margin-bottom: 8px;
}

.paper-flow-note {
color: var(--text-secondary);
font-size: 0.78rem;
line-height: 1.6;
margin-top: 6px;
}

.paper-flow-error {
color: var(--red);
}

.paper-flow-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}

.paper-row {
padding: 12px 14px;
border-radius: 8px;
Expand Down
Loading
Loading