Squashed commit of the following:
commit f29ca62b42df9ac7ff2dafd5132c3e12a1a6a3e7 Author: Louis Knight-Webb <louis@bloop.ai> Date: Thu Jun 19 12:52:48 2025 -0400 Settings commit 1215b493bbabeac9f446dd2996cb6275df069770 Author: Louis Knight-Webb <louis@bloop.ai> Date: Thu Jun 19 12:44:36 2025 -0400 Consolidate types commit d0960d989d24d6068728056d28820415c6cdea2c Author: Louis Knight-Webb <louis@bloop.ai> Date: Thu Jun 19 12:32:15 2025 -0400 Partial
This commit is contained in:
@@ -64,8 +64,14 @@ export {}
|
||||
|
||||
export {}
|
||||
|
||||
export {}
|
||||
|
||||
export {}
|
||||
|
||||
export {}"#,
|
||||
vibe_kanban::models::ApiResponse::<()>::decl(),
|
||||
vibe_kanban::models::config::Config::decl(),
|
||||
vibe_kanban::models::config::ThemeMode::decl(),
|
||||
vibe_kanban::executor::ExecutorConfig::decl(),
|
||||
vibe_kanban::models::project::CreateProject::decl(),
|
||||
vibe_kanban::models::project::Project::decl(),
|
||||
|
||||
@@ -3,3 +3,4 @@ pub mod executor;
|
||||
pub mod executors;
|
||||
pub mod models;
|
||||
pub mod routes;
|
||||
pub mod utils;
|
||||
|
||||
@@ -11,7 +11,7 @@ use rust_embed::RustEmbed;
|
||||
use sqlx::{sqlite::SqliteConnectOptions, SqlitePool};
|
||||
use std::str::FromStr;
|
||||
use std::{collections::HashMap, env, sync::Arc};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tower_http::cors::CorsLayer;
|
||||
|
||||
mod execution_monitor;
|
||||
@@ -19,10 +19,11 @@ mod executor;
|
||||
mod executors;
|
||||
mod models;
|
||||
mod routes;
|
||||
mod utils;
|
||||
|
||||
use execution_monitor::{execution_monitor, AppState};
|
||||
use models::ApiResponse;
|
||||
use routes::{filesystem, health, projects, tasks};
|
||||
use models::{ApiResponse, Config};
|
||||
use routes::{config, filesystem, health, projects, tasks};
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "../frontend/dist"]
|
||||
@@ -86,20 +87,25 @@ async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt().init();
|
||||
|
||||
// Create asset directory if it doesn't exist
|
||||
if !asset_dir().exists() {
|
||||
std::fs::create_dir_all(asset_dir())?;
|
||||
if !utils::asset_dir().exists() {
|
||||
std::fs::create_dir_all(utils::asset_dir())?;
|
||||
}
|
||||
|
||||
// Database connection
|
||||
let database_url = format!(
|
||||
"sqlite://{}",
|
||||
asset_dir().join("db.sqlite").to_string_lossy()
|
||||
utils::asset_dir().join("db.sqlite").to_string_lossy()
|
||||
);
|
||||
|
||||
let options = SqliteConnectOptions::from_str(&database_url)?.create_if_missing(true);
|
||||
let pool = SqlitePool::connect_with(options).await?;
|
||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||
|
||||
// Load configuration
|
||||
let config_path = utils::config_path();
|
||||
let config = Config::load(&config_path)?;
|
||||
let config_arc = Arc::new(RwLock::new(config));
|
||||
|
||||
// Create app state
|
||||
let app_state = AppState {
|
||||
running_executions: Arc::new(Mutex::new(HashMap::new())),
|
||||
@@ -124,9 +130,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
Router::new()
|
||||
.merge(projects::projects_router())
|
||||
.merge(tasks::tasks_router())
|
||||
.merge(filesystem::filesystem_router()),
|
||||
.merge(filesystem::filesystem_router())
|
||||
.merge(config::config_router()),
|
||||
)
|
||||
.layer(Extension(pool.clone()));
|
||||
.layer(Extension(pool.clone()))
|
||||
.layer(Extension(config_arc));
|
||||
|
||||
let app = Router::new()
|
||||
.merge(public_routes)
|
||||
@@ -154,18 +162,3 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn asset_dir() -> std::path::PathBuf {
|
||||
let proj = if cfg!(debug_assertions) {
|
||||
ProjectDirs::from("ai", "bloop-dev", env!("CARGO_PKG_NAME"))
|
||||
.expect("OS didn’t give us a home directory")
|
||||
} else {
|
||||
ProjectDirs::from("ai", "bloop", env!("CARGO_PKG_NAME"))
|
||||
.expect("OS didn’t give us a home directory")
|
||||
};
|
||||
|
||||
// ✔ macOS → ~/Library/Application Support/MyApp
|
||||
// ✔ Linux → ~/.local/share/myapp (respects XDG_DATA_HOME)
|
||||
// ✔ Windows → %APPDATA%\Example\MyApp
|
||||
proj.data_dir().to_path_buf()
|
||||
}
|
||||
|
||||
49
backend/src/models/config.rs
Normal file
49
backend/src/models/config.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use crate::executor::ExecutorConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct Config {
|
||||
pub theme: ThemeMode,
|
||||
pub executor: ExecutorConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ThemeMode {
|
||||
Light,
|
||||
Dark,
|
||||
System,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: ThemeMode::System,
|
||||
executor: ExecutorConfig::Claude,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(config_path: &PathBuf) -> anyhow::Result<Self> {
|
||||
if config_path.exists() {
|
||||
let content = std::fs::read_to_string(config_path)?;
|
||||
let config: Config = serde_json::from_str(&content)?;
|
||||
Ok(config)
|
||||
} else {
|
||||
let config = Config::default();
|
||||
config.save(config_path)?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&self, config_path: &PathBuf) -> anyhow::Result<()> {
|
||||
let content = serde_json::to_string_pretty(self)?;
|
||||
std::fs::write(config_path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
pub mod api_response;
|
||||
pub mod config;
|
||||
pub mod project;
|
||||
pub mod task;
|
||||
pub mod task_attempt;
|
||||
pub mod task_attempt_activity;
|
||||
|
||||
pub use api_response::ApiResponse;
|
||||
pub use config::Config;
|
||||
|
||||
53
backend/src/routes/config.rs
Normal file
53
backend/src/routes/config.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use axum::{
|
||||
extract::Extension,
|
||||
response::Json as ResponseJson,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::models::{config::Config, ApiResponse};
|
||||
use crate::utils;
|
||||
|
||||
pub fn config_router() -> Router {
|
||||
Router::new()
|
||||
.route("/config", get(get_config))
|
||||
.route("/config", post(update_config))
|
||||
}
|
||||
|
||||
async fn get_config(
|
||||
Extension(config): Extension<Arc<RwLock<Config>>>,
|
||||
) -> ResponseJson<ApiResponse<Config>> {
|
||||
let config = config.read().await;
|
||||
ResponseJson(ApiResponse {
|
||||
success: true,
|
||||
data: Some(config.clone()),
|
||||
message: Some("Config retrieved successfully".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_config(
|
||||
Extension(config_arc): Extension<Arc<RwLock<Config>>>,
|
||||
Json(new_config): Json<Config>,
|
||||
) -> ResponseJson<ApiResponse<Config>> {
|
||||
let config_path = utils::config_path();
|
||||
|
||||
match new_config.save(&config_path) {
|
||||
Ok(_) => {
|
||||
let mut config = config_arc.write().await;
|
||||
*config = new_config.clone();
|
||||
|
||||
ResponseJson(ApiResponse {
|
||||
success: true,
|
||||
data: Some(new_config),
|
||||
message: Some("Config updated successfully".to_string()),
|
||||
})
|
||||
}
|
||||
Err(e) => ResponseJson(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
message: Some(format!("Failed to save config: {}", e)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod config;
|
||||
pub mod filesystem;
|
||||
pub mod health;
|
||||
pub mod projects;
|
||||
|
||||
@@ -309,16 +309,19 @@ async fn search_files_in_repo(
|
||||
for result in walker {
|
||||
let entry = result?;
|
||||
let path = entry.path();
|
||||
|
||||
|
||||
// Skip the root directory itself
|
||||
if path == repo_path {
|
||||
continue;
|
||||
}
|
||||
|
||||
let relative_path = path.strip_prefix(repo_path)?;
|
||||
|
||||
|
||||
// Skip .git directory and its contents
|
||||
if relative_path.components().any(|component| component.as_os_str() == ".git") {
|
||||
if relative_path
|
||||
.components()
|
||||
.any(|component| component.as_os_str() == ".git")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let relative_path_str = relative_path.to_string_lossy().to_lowercase();
|
||||
|
||||
21
backend/src/utils.rs
Normal file
21
backend/src/utils.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use directories::ProjectDirs;
|
||||
use std::env;
|
||||
|
||||
pub fn asset_dir() -> std::path::PathBuf {
|
||||
let proj = if cfg!(debug_assertions) {
|
||||
ProjectDirs::from("ai", "bloop-dev", env!("CARGO_PKG_NAME"))
|
||||
.expect("OS didn't give us a home directory")
|
||||
} else {
|
||||
ProjectDirs::from("ai", "bloop", env!("CARGO_PKG_NAME"))
|
||||
.expect("OS didn't give us a home directory")
|
||||
};
|
||||
|
||||
// ✔ macOS → ~/Library/Application Support/MyApp
|
||||
// ✔ Linux → ~/.local/share/myapp (respects XDG_DATA_HOME)
|
||||
// ✔ Windows → %APPDATA%\Example\MyApp
|
||||
proj.data_dir().to_path_buf()
|
||||
}
|
||||
|
||||
pub fn config_path() -> std::path::PathBuf {
|
||||
asset_dir().join("config.json")
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Projects } from "@/pages/projects";
|
||||
import { ProjectTasks } from "@/pages/project-tasks";
|
||||
import { TaskDetailsPage } from "@/pages/task-details";
|
||||
import { TaskAttemptComparePage } from "@/pages/task-attempt-compare";
|
||||
import { Settings } from "@/pages/Settings";
|
||||
|
||||
function AppContent() {
|
||||
const showNavbar = true;
|
||||
@@ -25,6 +26,7 @@ function AppContent() {
|
||||
path="/projects/:projectId/tasks/:taskId/attempts/:attemptId/compare"
|
||||
element={<TaskAttemptComparePage />}
|
||||
/>
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, FolderOpen } from "lucide-react";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { ArrowLeft, FolderOpen, Settings } from "lucide-react";
|
||||
import { Logo } from "@/components/logo";
|
||||
|
||||
export function Navbar() {
|
||||
@@ -27,10 +26,21 @@ export function Navbar() {
|
||||
Projects
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant={
|
||||
location.pathname === "/settings" ? "default" : "ghost"
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
<Link to="/settings">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Settings
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<ThemeToggle />
|
||||
{!isHome && (
|
||||
<Button asChild variant="ghost">
|
||||
<Link to="/">
|
||||
|
||||
@@ -1,34 +1,45 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
import type { Config, ThemeMode, ApiResponse } from "shared/types";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
theme: ThemeMode;
|
||||
setTheme: (theme: ThemeMode) => void;
|
||||
loadThemeFromConfig: () => Promise<void>;
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
loadThemeFromConfig: async () => {},
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vibe-kanban-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
);
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
const [theme, setThemeState] = useState<ThemeMode>("system");
|
||||
|
||||
// Load theme from backend config
|
||||
const loadThemeFromConfig = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/config");
|
||||
const data: ApiResponse<Config> = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
setThemeState(data.data.theme);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error loading theme from config:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Load theme on mount
|
||||
useEffect(() => {
|
||||
loadThemeFromConfig();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
@@ -48,12 +59,14 @@ export function ThemeProvider({
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
const setTheme = (newTheme: ThemeMode) => {
|
||||
setThemeState(newTheme);
|
||||
};
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
setTheme,
|
||||
loadThemeFromConfig,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ThemeProvider } from "@/components/theme-provider";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vibe-kanban-ui-theme">
|
||||
<ThemeProvider>
|
||||
<ClickToComponent />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
|
||||
197
frontend/src/pages/Settings.tsx
Normal file
197
frontend/src/pages/Settings.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import type { Config, ThemeMode, ApiResponse } from "shared/types";
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
|
||||
export function Settings() {
|
||||
const [config, setConfig] = useState<Config | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const { setTheme, loadThemeFromConfig } = useTheme();
|
||||
|
||||
// Load initial config
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/config");
|
||||
const data: ApiResponse<Config> = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
setConfig(data.data);
|
||||
} else {
|
||||
setError(data.message || "Failed to load configuration");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Failed to load configuration");
|
||||
console.error("Error loading config:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!config) return;
|
||||
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/config", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
|
||||
const data: ApiResponse<Config> = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setSuccess(true);
|
||||
// Update theme provider to reflect the saved theme
|
||||
setTheme(config.theme);
|
||||
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
} else {
|
||||
setError(data.message || "Failed to save configuration");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Failed to save configuration");
|
||||
console.error("Error saving config:", err);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateConfig = (updates: Partial<Config>) => {
|
||||
setConfig((prev: Config | null) => prev ? { ...prev, ...updates } : null);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
<span className="ml-2">Loading settings...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
Failed to load settings. {error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Settings</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Configure your preferences and application settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert>
|
||||
<AlertDescription>Settings saved successfully!</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Appearance</CardTitle>
|
||||
<CardDescription>
|
||||
Customize how the application looks and feels.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="theme">Theme</Label>
|
||||
<Select
|
||||
value={config.theme}
|
||||
onValueChange={(value: ThemeMode) => updateConfig({ theme: value })}
|
||||
>
|
||||
<SelectTrigger id="theme">
|
||||
<SelectValue placeholder="Select theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose your preferred color scheme.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Task Execution</CardTitle>
|
||||
<CardDescription>
|
||||
Configure how tasks are executed and processed.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="executor">Default Executor</Label>
|
||||
<Select
|
||||
value={config.executor.type}
|
||||
onValueChange={(value: "echo" | "claude" | "amp") => updateConfig({ executor: { type: value } })}
|
||||
>
|
||||
<SelectTrigger id="executor">
|
||||
<SelectValue placeholder="Select executor" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="claude">Claude</SelectItem>
|
||||
<SelectItem value="amp">Amp</SelectItem>
|
||||
<SelectItem value="echo">Echo</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose the default executor for running tasks.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSave} disabled={saving}>
|
||||
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Save Settings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,8 @@ import type {
|
||||
TaskAttempt,
|
||||
TaskAttemptActivity,
|
||||
TaskAttemptStatus,
|
||||
Config,
|
||||
ApiResponse,
|
||||
} from "shared/types";
|
||||
|
||||
interface Task {
|
||||
@@ -31,11 +33,7 @@ interface Task {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T | null;
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
|
||||
const statusLabels: Record<TaskStatus, string> = {
|
||||
todo: "To Do",
|
||||
@@ -124,6 +122,24 @@ export function TaskDetailsPage() {
|
||||
}
|
||||
}, [projectId, taskId]);
|
||||
|
||||
// Load config to get default executor
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const response = await makeRequest("/api/config");
|
||||
if (response.ok) {
|
||||
const result: ApiResponse<Config> = await response.json();
|
||||
if (result.success && result.data) {
|
||||
setSelectedExecutor(result.data.executor.type);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load config:", err);
|
||||
}
|
||||
};
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (task) {
|
||||
fetchTaskAttempts(task.id);
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
|
||||
export type ApiResponse<T> = { success: boolean, data: T | null, message: string | null, };
|
||||
|
||||
export type Config = { theme: ThemeMode, executor: ExecutorConfig, };
|
||||
|
||||
export type ThemeMode = "light" | "dark" | "system";
|
||||
|
||||
export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" };
|
||||
|
||||
export type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, setup_script: string | null, };
|
||||
|
||||
Reference in New Issue
Block a user