Tori: AI agent workflow manager - initial implementation

Rust (Axum) + Vue 3 + SQLite. Features:
- Project CRUD REST API with proper error handling
- Per-project agent loop (mpsc + broadcast channels)
- LLM-driven plan generation and replan on user feedback
- SSH command execution with status streaming
- WebSocket real-time updates to frontend
- Four-zone UI: requirement, plan (left), execution (right), comment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 10:36:50 +00:00
parent 1122ab27dd
commit 7edbbee471
43 changed files with 7164 additions and 83 deletions

View File

@@ -0,0 +1,88 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import Sidebar from './Sidebar.vue'
import WorkflowView from './WorkflowView.vue'
import { api } from '../api'
import type { Project } from '../types'
const projects = ref<Project[]>([])
const selectedProjectId = ref('')
const error = ref('')
onMounted(async () => {
try {
projects.value = await api.listProjects()
const first = projects.value[0]
if (first) {
selectedProjectId.value = first.id
}
} catch (e: any) {
error.value = e.message
}
})
function onSelectProject(id: string) {
selectedProjectId.value = id
}
async function onCreateProject() {
const name = prompt('项目名称')
if (!name) return
try {
const project = await api.createProject(name)
projects.value.unshift(project)
selectedProjectId.value = project.id
} catch (e: any) {
error.value = e.message
}
}
</script>
<template>
<div class="app-layout">
<Sidebar
:projects="projects"
:selectedId="selectedProjectId"
@select="onSelectProject"
@create="onCreateProject"
/>
<main class="main-content">
<div v-if="error" class="error-banner">{{ error }}</div>
<div v-if="!selectedProjectId" class="empty-state">
选择或创建一个项目开始
</div>
<WorkflowView v-else :projectId="selectedProjectId" :key="selectedProjectId" />
</main>
</div>
</template>
<style scoped>
.app-layout {
display: flex;
height: 100vh;
overflow: hidden;
}
.main-content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
font-size: 16px;
}
.error-banner {
background: var(--error);
color: #fff;
padding: 8px 16px;
font-size: 13px;
}
</style>