Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/server/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const tasks = sqliteTable('tasks', {
repoUrl: text('repo_url'),
selectedAgent: text('selected_agent').default('claude'),
selectedModel: text('selected_model'),
mode: text('mode').notNull().default('default'), // 'default' | 'coding'
installDependencies: integer('install_dependencies', { mode: 'boolean' }).default(false),
maxDuration: integer('max_duration').default(parseInt(process.env.MAX_SANDBOX_DURATION || '300', 10)),
keepAlive: integer('keep_alive', { mode: 'boolean' }).default(false),
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/db/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface Task {
repoUrl: string | null
selectedAgent: string | null
selectedModel: string | null
mode: string
installDependencies: boolean | null
maxDuration: number | null
keepAlive: boolean | null
Expand Down
38 changes: 9 additions & 29 deletions packages/server/src/routes/acp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,35 +186,6 @@ acp.delete('/conversation/:conversationId', async (c) => {
return c.json({ status: 'success' })
})

// ─── Chat Endpoint (SSE) ───────────────────────────────────────────────────

/**
* POST /api/agent/chat
*
* 简单的聊天端点,返回 SSE 流式响应
*/
acp.post('/chat', async (c) => {
const body = await c.req.json<{ prompt: string; conversationId?: string; model?: string }>()
const { prompt, conversationId, model } = body

const { envId, userId, credentials: userCredentials } = c.get('userEnv')!
if (!envId) {
return c.json({ error: 'CloudBase environment not bound' }, 400)
}

const actualConversationId = conversationId || uuidv4()

return observeStreamWithLiveCallback(c, null, actualConversationId, envId, userId, async (callback) => {
return cloudbaseAgentService.chatStream(prompt, callback, {
conversationId: actualConversationId,
envId,
userId,
userCredentials,
model,
})
})
})

// ─── ACP JSON-RPC 2.0 Endpoint ─────────────────────────────────────────────

/**
Expand Down Expand Up @@ -384,6 +355,14 @@ async function handleSessionPrompt(c: any, id: number | string, params: SessionP
} catch {
// write failure doesn't affect main flow
}
// Resolve task mode
let taskMode: 'default' | 'coding' | undefined
try {
const task = await getDb().tasks.findById(sessionId)
if (task?.mode === 'coding') taskMode = 'coding'
} catch {
// ignore
}

// Launch agent with liveCallback for real-time SSE push
return observeStreamWithLiveCallback(c, id, sessionId, envId, userId, async (callback) => {
Expand All @@ -395,6 +374,7 @@ async function handleSessionPrompt(c: any, id: number | string, params: SessionP
model: selectedModel,
askAnswers: params.askAnswers,
toolConfirmation: params.toolConfirmation,
mode: taskMode,
})
})
}
Expand Down
54 changes: 54 additions & 0 deletions packages/server/src/routes/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ tasksRouter.post('/', async (c) => {
repoUrl,
selectedAgent = 'claude',
selectedModel,
mode = 'default',
installDependencies = false,
maxDuration = 300,
keepAlive = false,
Expand Down Expand Up @@ -321,6 +322,7 @@ tasksRouter.post('/', async (c) => {
repoUrl: repoUrl || null,
selectedAgent,
selectedModel: selectedModel || null,
mode,
installDependencies,
maxDuration,
keepAlive,
Expand Down Expand Up @@ -2331,4 +2333,56 @@ tasksRouter.post('/:taskId/file-operation', requireUserEnv, async (c) => {
}
})

// ---------------------------------------------------------------------------
// Preview proxy — forward requests to dev server inside sandbox
// ---------------------------------------------------------------------------

tasksRouter.all('/:taskId/preview/*', async (c) => {
const authErr = requireAuth(c)
if (authErr) return authErr
const session = c.get('session')!
const taskId = c.req.param('taskId')

const task = await getDb().tasks.findById(taskId)
if (!task || task.userId !== session.user.id) {
return c.json({ error: 'Task not found' }, 404)
}

const envId = (c.get('userEnv') as any)?.envId || process.env.TCB_ENV_ID || ''
const sandbox = await getScfSandbox(task, envId)
if (!sandbox) {
return c.json({ error: 'Sandbox not available' }, 503)
}

// Extract the path after /preview/
const fullPath = c.req.path
const previewIdx = fullPath.indexOf('/preview/')
const proxyPath = previewIdx >= 0 ? fullPath.slice(previewIdx + '/preview'.length) : '/'

try {
const res = await sandbox.request(`/proxy/5173${proxyPath}`, {
method: c.req.method,
headers: {
Accept: c.req.header('accept') || '*/*',
'Accept-Encoding': c.req.header('accept-encoding') || '',
},
signal: AbortSignal.timeout(30_000),
})

// Forward response headers and body
const contentType = res.headers.get('content-type')
const body = await res.arrayBuffer()

return new Response(body, {
status: res.status,
headers: {
...(contentType ? { 'Content-Type': contentType } : {}),
'Cache-Control': 'no-cache',
},
})
} catch {
return c.json({ error: 'Dev server not responding' }, 502)
}
})

export default tasksRouter
3 changes: 2 additions & 1 deletion packages/shared/src/types/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,5 +467,6 @@ export interface AgentOptions {
}
/** 指定模型 */
model?: string
mode?: string
/** 任务模式 */
mode?: 'default' | 'coding'
}
2 changes: 2 additions & 0 deletions packages/shared/src/types/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const insertTaskSchema = z.object({
repoUrl: z.string().url().optional(),
selectedAgent: z.enum(['claude', 'codex', 'copilot', 'cursor', 'gemini', 'opencode']).default('claude'),
selectedModel: z.string().optional(),
mode: z.enum(['default', 'coding']).default('default'),
installDependencies: z.boolean().default(false),
maxDuration: z.number().default(300),
keepAlive: z.boolean().default(false),
Expand Down Expand Up @@ -54,6 +55,7 @@ export const selectTaskSchema = z.object({
repoUrl: z.string().nullable(),
selectedAgent: z.string().nullable(),
selectedModel: z.string().nullable(),
mode: z.enum(['default', 'coding']).nullable(),
installDependencies: z.boolean().nullable(),
maxDuration: z.number().nullable(),
keepAlive: z.boolean().nullable(),
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/components/home-page-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ export function HomePageContent({
maxDuration: number
keepAlive: boolean
enableBrowser: boolean
mode: 'default' | 'coding'
}) => {
console.log(
'[TaskSubmit] called, isSubmitting:',
Expand Down
23 changes: 23 additions & 0 deletions packages/web/src/components/task-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -571,9 +571,24 @@ export function TaskChat({
),
}

const isCodingMode = task.mode === 'coding'

// ─── Tab content ───────────────────────────────────────────────────

const renderTabContent = () => {
if (activeTab === 'preview' && isCodingMode) {
return (
<div className="flex-1 overflow-hidden -mx-3 -mt-3 relative">
<iframe
src={`/api/tasks/${taskId}/preview/`}
className="w-full h-full border-0"
title="Project Preview"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
/>
</div>
)
}

if (activeTab === 'cloud') {
return (
<div className="flex-1 overflow-hidden -mx-3 -mt-3">
Expand Down Expand Up @@ -1154,6 +1169,14 @@ export function TaskChat({
>
Deployments
</button>
{isCodingMode && (
<button
onClick={() => setActiveTab('preview')}
className={`text-sm font-semibold px-2 py-1 rounded transition-colors whitespace-nowrap flex-shrink-0 ${currentTab === 'preview' ? 'text-foreground' : 'text-muted-foreground hover:text-foreground'}`}
>
Preview
</button>
)}
{!readOnly && (
<button
onClick={() => setActiveTab('cloud')}
Expand Down
22 changes: 20 additions & 2 deletions packages/web/src/components/task-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Loader2, ArrowUp, Settings, X, Cable, Users, Globe } from 'lucide-react'
import { Loader2, ArrowUp, Settings, X, Cable, Users, Globe, Code } from 'lucide-react'
import { CodeBuddy, ProviderLogos, type ProviderKey } from '@/components/logos'
// import { Claude, Codex, Copilot, Cursor, Gemini, OpenCode } from '@/components/logos'
import { setInstallDependencies, setMaxDuration, setKeepAlive, setEnableBrowser } from '@/lib/utils/cookies'
Expand Down Expand Up @@ -46,6 +46,7 @@ interface TaskFormProps {
maxDuration: number
keepAlive: boolean
enableBrowser: boolean
mode: 'default' | 'coding'
}) => void
isSubmitting: boolean
selectedOwner: string
Expand Down Expand Up @@ -131,6 +132,7 @@ export function TaskForm({
const [prompt, setPrompt] = useAtom(taskPromptAtom)
const selectedAgent = 'codebuddy'
const [selectedModel, setSelectedModel] = useState<string>(DEFAULT_MODELS.codebuddy)
const [taskMode, setTaskMode] = useState<'default' | 'coding'>('default')
const [codebuddyModels, setCodebuddyModels] = useState<ModelInfo[]>([{ id: 'glm-5.0', name: 'GLM 5.0' }])
const [repos, setRepos] = useAtom(githubReposAtomFamily(selectedOwner))
const [, setLoadingRepos] = useState(false)
Expand Down Expand Up @@ -306,6 +308,7 @@ export function TaskForm({
maxDuration,
keepAlive,
enableBrowser,
mode: taskMode,
})
return
}
Expand Down Expand Up @@ -353,6 +356,7 @@ export function TaskForm({
maxDuration,
keepAlive,
enableBrowser,
mode: taskMode,
})
}

Expand Down Expand Up @@ -391,10 +395,24 @@ export function TaskForm({
/>
</div>

{/* Agent/Model selector (fixed to codebuddy) */}
{/* Mode + Agent/Model selector */}
<div className="p-4">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 flex-1 min-w-0">
{/* Mode toggle */}
<button
type="button"
onClick={() => setTaskMode(taskMode === 'default' ? 'coding' : 'default')}
className={`flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full border transition-colors ${
taskMode === 'coding'
? 'bg-primary/10 text-primary border-primary/30'
: 'text-muted-foreground border-border hover:border-primary/30'
}`}
>
<Code className="h-3 w-3" />
{taskMode === 'coding' ? 'Coding' : 'Default'}
</button>
<span className="text-muted-foreground/50">·</span>
<div className="flex items-center gap-2 text-sm text-muted-foreground px-2 h-8">
{(() => {
const agent = CODING_AGENTS.find((a) => a.value === selectedAgent)
Expand Down