Compare commits
2 Commits
7000f2075d
...
d894cd9723
| Author | SHA1 | Date | |
|---|---|---|---|
| d894cd9723 | |||
| 5a8eca814d |
@@ -48,3 +48,8 @@ apps/web/dist/
|
|||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Local runtime state
|
||||||
|
.atl/
|
||||||
|
.pi/
|
||||||
|
swap-pane
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -206,6 +207,42 @@ async def _get_owned_project(
|
|||||||
return project
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_name(name: str) -> str:
|
||||||
|
"""Sanitize a string for use in Docker/container names."""
|
||||||
|
sanitized = re.sub(r"[^a-z0-9-]", "-", name.lower())
|
||||||
|
sanitized = re.sub(r"-+", "-", sanitized)
|
||||||
|
return sanitized.strip("-")
|
||||||
|
|
||||||
|
|
||||||
|
async def _generate_instance_name(
|
||||||
|
session: AsyncSession,
|
||||||
|
project_name: str,
|
||||||
|
tool_type_name: str,
|
||||||
|
) -> str:
|
||||||
|
"""Generate a unique instance name: project-tool-NUM.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Database session.
|
||||||
|
project_name: Name of the project.
|
||||||
|
tool_type_name: Name of the tool type.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A unique instance name with a sequential 3-digit number.
|
||||||
|
"""
|
||||||
|
base = f"{_sanitize_name(project_name)}-{_sanitize_name(tool_type_name)}"
|
||||||
|
base = base.strip("-") or "instance"
|
||||||
|
result = await session.execute(
|
||||||
|
select(ToolInstance.name).where(ToolInstance.name.like(f"{base}-%"))
|
||||||
|
)
|
||||||
|
names = result.scalars().all()
|
||||||
|
max_num = 0
|
||||||
|
for name in names:
|
||||||
|
parts = name.rsplit("-", 1)
|
||||||
|
if len(parts) == 2 and parts[0] == base and parts[1].isdigit():
|
||||||
|
max_num = max(max_num, int(parts[1]))
|
||||||
|
return f"{base}-{max_num + 1:03d}"
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/{project_id}/repositories/{repo_id}/instances",
|
"/{project_id}/repositories/{repo_id}/instances",
|
||||||
summary="Create tool instance",
|
summary="Create tool instance",
|
||||||
@@ -278,8 +315,8 @@ async def create_instance(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Generate unique name
|
# Generate unique name: project-tool-NUM
|
||||||
instance_name = f"{tool_type.name}-{repo.name}-{uuid.uuid4().hex[:8]}"
|
instance_name = await _generate_instance_name(session, _project.name, tool_type.name)
|
||||||
instance_display = data.display_name or f"{tool_type.display_name} - {repo.name}"
|
instance_display = data.display_name or f"{tool_type.display_name} - {repo.name}"
|
||||||
|
|
||||||
# Create instance directory
|
# Create instance directory
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ const NAV_ITEMS: { to: string; label: string; icon: IconName }[] = [
|
|||||||
{ to: "/settings", label: "Settings", icon: "settings" }
|
{ to: "/settings", label: "Settings", icon: "settings" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ACTIVE_STATUSES = ["running", "building", "pending"];
|
||||||
|
|
||||||
const SessionItem = ({ session }: { session: Session }) => {
|
const SessionItem = ({ session }: { session: Session }) => {
|
||||||
const isRunning = session.status === "running";
|
const isRunning = session.status === "running";
|
||||||
|
const displayName = session.display_name || session.tool_type_name || "Unnamed Session";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
@@ -25,11 +28,11 @@ const SessionItem = ({ session }: { session: Session }) => {
|
|||||||
target={session.url ? "_blank" : undefined}
|
target={session.url ? "_blank" : undefined}
|
||||||
rel={session.url ? "noopener noreferrer" : undefined}
|
rel={session.url ? "noopener noreferrer" : undefined}
|
||||||
className="nav-item session-item"
|
className="nav-item session-item"
|
||||||
title={`${session.display_name} (${session.status})`}
|
title={`${displayName} (${session.status})`}
|
||||||
>
|
>
|
||||||
<span className={`session-status ${isRunning ? "running" : ""}`} />
|
<span className={`session-status ${isRunning ? "running" : ""}`} />
|
||||||
<Icon name={session.tool_icon as IconName} size="sm" />
|
<Icon name={session.tool_icon as IconName} size="sm" />
|
||||||
<span className="session-name">{session.display_name}</span>
|
<span className="session-name">{displayName}</span>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -101,13 +104,15 @@ export const AppShell = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{sessions.length > 0 && (
|
{sessions.filter((s) => ACTIVE_STATUSES.includes(s.status)).length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="nav-divider" />
|
<div className="nav-divider" />
|
||||||
<div className="nav-section-title">Live sessions</div>
|
<div className="nav-section-title">Live sessions</div>
|
||||||
{sessions.map((session) => (
|
{sessions
|
||||||
<SessionItem key={session.id} session={session} />
|
.filter((s) => ACTIVE_STATUSES.includes(s.status))
|
||||||
))}
|
.map((session) => (
|
||||||
|
<SessionItem key={session.id} session={session} />
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
|
|||||||
{instances.map((instance) => (
|
{instances.map((instance) => (
|
||||||
<div key={instance.id} className="instance-card">
|
<div key={instance.id} className="instance-card">
|
||||||
<div className="instance-info">
|
<div className="instance-info">
|
||||||
<div className="instance-name">{instance.display_name}</div>
|
<div className="instance-name">{instance.display_name || instance.tool_type_name || "Unnamed Instance"}</div>
|
||||||
<div className="instance-meta">
|
<div className="instance-meta">
|
||||||
<span
|
<span
|
||||||
className="status-dot"
|
className="status-dot"
|
||||||
|
|||||||
@@ -177,15 +177,42 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Resize observer for container-level resize detection
|
// Resize observer for container-level resize detection
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
let resizeTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
fitAddon.fit();
|
let lastWidth = 0;
|
||||||
const { cols, rows } = term;
|
let lastHeight = 0;
|
||||||
sendResize(cols, rows);
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
if (resizeTimeout) {
|
||||||
|
clearTimeout(resizeTimeout);
|
||||||
|
}
|
||||||
|
const entry = entries[0];
|
||||||
|
if (!entry) return;
|
||||||
|
const { width, height } = entry.contentRect;
|
||||||
|
resizeTimeout = setTimeout(() => {
|
||||||
|
resizeTimeout = null;
|
||||||
|
// Guard against internal xterm DOM changes that don't affect container size
|
||||||
|
if (
|
||||||
|
Math.abs(width - lastWidth) < 1 &&
|
||||||
|
Math.abs(height - lastHeight) < 1
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastWidth = width;
|
||||||
|
lastHeight = height;
|
||||||
|
const prevCols = term.cols;
|
||||||
|
const prevRows = term.rows;
|
||||||
|
fitAddon.fit();
|
||||||
|
if (term.cols !== prevCols || term.rows !== prevRows) {
|
||||||
|
sendResize(term.cols, term.rows);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
});
|
});
|
||||||
resizeObserver.observe(terminalRef.current);
|
resizeObserver.observe(terminalRef.current);
|
||||||
resizeObserverRef.current = resizeObserver;
|
resizeObserverRef.current = resizeObserver;
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (resizeTimeout) {
|
||||||
|
clearTimeout(resizeTimeout);
|
||||||
|
}
|
||||||
disposable.dispose();
|
disposable.dispose();
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
term.dispose();
|
term.dispose();
|
||||||
@@ -195,6 +222,14 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
};
|
};
|
||||||
}, [instanceId, isDarkMode, sendInput, sendResize]);
|
}, [instanceId, isDarkMode, sendInput, sendResize]);
|
||||||
|
|
||||||
|
// Send initial terminal size once connected (and on reconnect)
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.status === "connected" && xtermRef.current) {
|
||||||
|
const { cols, rows } = xtermRef.current;
|
||||||
|
sendResize(cols, rows);
|
||||||
|
}
|
||||||
|
}, [state.status, sendResize]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="terminal-wrapper">
|
<div className="terminal-wrapper">
|
||||||
<div className="terminal-header">
|
<div className="terminal-header">
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ export const SessionsPage = () => {
|
|||||||
<h2>Last Session</h2>
|
<h2>Last Session</h2>
|
||||||
<div className="card last-session-card">
|
<div className="card last-session-card">
|
||||||
<div className="last-session-info">
|
<div className="last-session-info">
|
||||||
<h3>{lastSession.display_name}</h3>
|
<h3>{lastSession.display_name || lastSession.tool_type_name || "Unnamed Session"}</h3>
|
||||||
<p className="muted">
|
<p className="muted">
|
||||||
{lastSession.tool_type_name} · {lastSession.project_name} · {lastSession.repository_name}
|
{lastSession.tool_type_name} · {lastSession.project_name} · {lastSession.repository_name}
|
||||||
</p>
|
</p>
|
||||||
@@ -316,7 +316,7 @@ export const SessionsPage = () => {
|
|||||||
{activeSessions.map((session) => (
|
{activeSessions.map((session) => (
|
||||||
<div className="card session-card" key={session.id}>
|
<div className="card session-card" key={session.id}>
|
||||||
<div className="session-info">
|
<div className="session-info">
|
||||||
<h4>{session.display_name}</h4>
|
<h4>{session.display_name || session.tool_type_name || "Unnamed Session"}</h4>
|
||||||
<p className="muted">
|
<p className="muted">
|
||||||
{session.tool_type_name} · {session.project_name}
|
{session.tool_type_name} · {session.project_name}
|
||||||
</p>
|
</p>
|
||||||
@@ -445,7 +445,7 @@ export const SessionsPage = () => {
|
|||||||
{recentSessions.map((session) => (
|
{recentSessions.map((session) => (
|
||||||
<div className="recent-session-item" key={session.id}>
|
<div className="recent-session-item" key={session.id}>
|
||||||
<div className="recent-session-info">
|
<div className="recent-session-info">
|
||||||
<span className="recent-session-name">{session.display_name}</span>
|
<span className="recent-session-name">{session.display_name || session.tool_type_name || "Unnamed Session"}</span>
|
||||||
<span className="muted">
|
<span className="muted">
|
||||||
{session.tool_type_name} · {session.project_name}
|
{session.tool_type_name} · {session.project_name}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -2748,11 +2748,6 @@ a.nav-item,
|
|||||||
.terminal-container {
|
.terminal-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-container .xterm {
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-container .xterm-viewport {
|
.terminal-container .xterm-viewport {
|
||||||
|
|||||||
Reference in New Issue
Block a user