feat(opencode-web-terminal): complete OpenCode web terminal implementation
- Update OpenCode compose template with web server on port 3000 - Add default_port=3000 and interfaces=[terminal, web] to OpenCode seed data - Remove hardcoded 8080 fallback in tunnel creation - Fail gracefully when tool type has no default_port configured - Update frontend ToolType API to include default_port, category, interfaces - Add port, category, and interfaces fields to tool type creation form - Display port and interfaces in tool type cards - Create migration 0012 to make default_port non-nullable - Set default_port values for existing built-in tool types - Quality gates: typecheck ✓, build ✓, Python syntax ✓
This commit is contained in:
@@ -20,6 +20,9 @@ export interface CreateToolTypeRequest {
|
||||
name: string;
|
||||
display_name: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
interfaces?: string[];
|
||||
default_port: number;
|
||||
compose_template: string;
|
||||
required_variables: string[];
|
||||
}
|
||||
@@ -27,6 +30,9 @@ export interface CreateToolTypeRequest {
|
||||
export interface UpdateToolTypeRequest {
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
interfaces?: string[];
|
||||
default_port?: number;
|
||||
compose_template?: string;
|
||||
required_variables?: string[];
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ export const ToolTypesPage = () => {
|
||||
const [formName, setFormName] = useState("");
|
||||
const [formDisplayName, setFormDisplayName] = useState("");
|
||||
const [formDescription, setFormDescription] = useState("");
|
||||
const [formCategory, setFormCategory] = useState("");
|
||||
const [formInterfaces, setFormInterfaces] = useState<string[]>([]);
|
||||
const [formPort, setFormPort] = useState("");
|
||||
const [formTemplate, setFormTemplate] = useState("");
|
||||
const [formVariables, setFormVariables] = useState("");
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
@@ -47,6 +50,9 @@ export const ToolTypesPage = () => {
|
||||
setFormName("");
|
||||
setFormDisplayName("");
|
||||
setFormDescription("");
|
||||
setFormCategory("");
|
||||
setFormInterfaces([]);
|
||||
setFormPort("");
|
||||
setFormTemplate("");
|
||||
setFormVariables("");
|
||||
setFormError(null);
|
||||
@@ -58,6 +64,9 @@ export const ToolTypesPage = () => {
|
||||
setFormName(toolType.name);
|
||||
setFormDisplayName(toolType.display_name);
|
||||
setFormDescription(toolType.description ?? "");
|
||||
setFormCategory(toolType.category ?? "");
|
||||
setFormInterfaces(toolType.interfaces ?? []);
|
||||
setFormPort(toolType.default_port?.toString() ?? "");
|
||||
setFormTemplate(toolType.compose_template);
|
||||
setFormVariables(toolType.required_variables.join(", "));
|
||||
setFormError(null);
|
||||
@@ -80,6 +89,11 @@ export const ToolTypesPage = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formPort.trim() || isNaN(Number(formPort))) {
|
||||
setFormError("Default port is required and must be a number");
|
||||
return;
|
||||
}
|
||||
|
||||
const variables = formVariables
|
||||
.split(",")
|
||||
.map((v) => v.trim())
|
||||
@@ -91,6 +105,9 @@ export const ToolTypesPage = () => {
|
||||
name: formName.trim(),
|
||||
display_name: formDisplayName.trim(),
|
||||
description: formDescription.trim() || undefined,
|
||||
category: formCategory.trim() || undefined,
|
||||
interfaces: formInterfaces.length > 0 ? formInterfaces : undefined,
|
||||
default_port: Number(formPort),
|
||||
compose_template: formTemplate.trim(),
|
||||
required_variables: variables,
|
||||
};
|
||||
@@ -99,6 +116,9 @@ export const ToolTypesPage = () => {
|
||||
const input: UpdateToolTypeRequest = {
|
||||
display_name: formDisplayName.trim(),
|
||||
description: formDescription.trim() || undefined,
|
||||
category: formCategory.trim() || undefined,
|
||||
interfaces: formInterfaces.length > 0 ? formInterfaces : undefined,
|
||||
default_port: Number(formPort),
|
||||
compose_template: formTemplate.trim(),
|
||||
required_variables: variables,
|
||||
};
|
||||
@@ -164,6 +184,13 @@ export const ToolTypesPage = () => {
|
||||
{toolType.is_builtin && <span className="badge">Built-in</span>}
|
||||
</div>
|
||||
<p className="text-secondary">{toolType.description || "No description"}</p>
|
||||
<div className="tool-type-meta">
|
||||
<span>Port: {toolType.default_port || "N/A"}</span>
|
||||
{toolType.interfaces?.length > 0 && (
|
||||
<span>Interfaces: {toolType.interfaces.join(", ")}</span>
|
||||
)}
|
||||
{toolType.category && <span>Category: {toolType.category}</span>}
|
||||
</div>
|
||||
<div className="card-actions">
|
||||
{!toolType.is_builtin && (
|
||||
<>
|
||||
@@ -240,6 +267,61 @@ export const ToolTypesPage = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Category</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formCategory}
|
||||
onChange={(e) => setFormCategory(e.target.value)}
|
||||
placeholder="e.g., editor, notebook, ai-assistant"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Interfaces</label>
|
||||
<div className="checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formInterfaces.includes("web")}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setFormInterfaces([...formInterfaces, "web"]);
|
||||
} else {
|
||||
setFormInterfaces(formInterfaces.filter((i) => i !== "web"));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
Web
|
||||
</label>
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formInterfaces.includes("terminal")}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setFormInterfaces([...formInterfaces, "terminal"]);
|
||||
} else {
|
||||
setFormInterfaces(formInterfaces.filter((i) => i !== "terminal"));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
Terminal
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Default Port *</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formPort}
|
||||
onChange={(e) => setFormPort(e.target.value)}
|
||||
placeholder="e.g., 8443"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Compose Template (YAML)</label>
|
||||
<textarea
|
||||
|
||||
Reference in New Issue
Block a user