diff --git a/heicode/web/default/src/features/agnet-console/pages.tsx b/heicode/web/default/src/features/agnet-console/pages.tsx
index f7d5d4d..eeca57d 100644
--- a/heicode/web/default/src/features/agnet-console/pages.tsx
+++ b/heicode/web/default/src/features/agnet-console/pages.tsx
@@ -39,7 +39,6 @@ import {
SelectValue,
} from '@/components/ui/select'
import {
- getAgnetAuditLogs,
getAgnetDeploymentEvents,
getAgnetSnapshots,
createGitSource,
@@ -54,6 +53,11 @@ import {
type GitSourcePayload,
type GitSourceUsage,
} from './api'
+// /audit pulls from mcp-server §5.10 stub now, not the Heicode-local
+// controller — the contract doc names that endpoint as the canonical
+// source. The shape of McpAuditEntry is wider than the legacy local one
+// so the redacted-card renderer keeps working.
+import { listMcpAuditLogs } from '@/lib/heicode-mcp'
import { CreateAgnetDeploymentSheet } from './create-agnet-deployment-sheet'
import { toast } from 'sonner'
@@ -818,8 +822,8 @@ export function AgnetAuditPage() {
const [actionFilter, setActionFilter] = useState('')
const { data = [], isLoading } = useQuery({
- queryKey: ['agnet', 'audit'],
- queryFn: getAgnetAuditLogs,
+ queryKey: ['heicode', 'agnet', 'audit'],
+ queryFn: () => listMcpAuditLogs({ limit: 200 }),
refetchInterval: 60_000,
})
diff --git a/heicode/web/default/src/features/dashboard/components/home-hero.tsx b/heicode/web/default/src/features/dashboard/components/home-hero.tsx
index ed14dee..6d6b59f 100644
--- a/heicode/web/default/src/features/dashboard/components/home-hero.tsx
+++ b/heicode/web/default/src/features/dashboard/components/home-hero.tsx
@@ -18,10 +18,9 @@
* 还没接,等接通后这个 stash 流程换成 POST /api/task。
*/
import { useCallback, useMemo, useRef, useState } from 'react'
-import { Link } from '@tanstack/react-router'
+import { Link, useNavigate } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
-import { toast } from 'sonner'
import {
ArrowRight,
ArrowUpRight,
@@ -41,9 +40,11 @@ import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { cn } from '@/lib/utils'
import {
- listAgnetDeploymentsQuiet,
- type AgnetDeployment,
-} from '@/features/agnet-console/api'
+ createTaskFromIntent,
+ listHeicodeTasks,
+ type HeicodeTask,
+} from '@/lib/heicode-mcp'
+import { toast as sonnerToast } from 'sonner'
const DRAFT_STORAGE_KEY = 'heicode_idea_draft'
@@ -102,11 +103,10 @@ function StatusBadge({ phase }: { phase: string }) {
)
}
-function formatRelativeTime(value?: string): string {
- if (!value) return '—'
- const date = new Date(value)
- if (Number.isNaN(date.getTime())) return value
- const diff = Date.now() - date.getTime()
+function formatRelativeTimeMs(ms?: number): string {
+ if (!ms) return '—'
+ const diff = Date.now() - ms
+ if (diff < 0) return '—'
const sec = Math.round(diff / 1000)
if (sec < 60) return `${sec}s`
const min = Math.round(sec / 60)
@@ -121,9 +121,16 @@ function IdeaInput({ t }: { t: ReturnType['t'] }) {
if (typeof window === 'undefined') return ''
return window.localStorage.getItem(DRAFT_STORAGE_KEY) ?? ''
})
+ const [submitting, setSubmitting] = useState(false)
const taRef = useRef(null)
+ const navigate = useNavigate()
- const handleSubmit = useCallback(() => {
+ // POST /api/user/tasks/intent on the mcp-server. Backend returns a new
+ // HeicodeTask with status=configuring and a thread containing the first
+ // round of follow-up questions. We navigate to /tasks/$id so the user
+ // can answer them — that's where the recommendation summary materialises
+ // (status flips to running once all followups answered, see §6.4).
+ const handleSubmit = useCallback(async () => {
const trimmed = value.trim()
if (!trimmed) {
taRef.current?.focus()
@@ -134,8 +141,17 @@ function IdeaInput({ t }: { t: ReturnType['t'] }) {
} catch {
/* localStorage unavailable — ignore */
}
- toast.success(t('Idea captured. Open the desktop client to continue.'))
- }, [value, t])
+ setSubmitting(true)
+ try {
+ const task = await createTaskFromIntent(trimmed)
+ sonnerToast.success(t('Task drafted. Answer the follow-ups to build the recommendation.'))
+ void navigate({ to: '/tasks/$id', params: { id: task.id } })
+ } catch (err) {
+ sonnerToast.error(err instanceof Error ? err.message : t('Failed to create task'))
+ } finally {
+ setSubmitting(false)
+ }
+ }, [value, t, navigate])
return (
['t'] }) {
}
function ContinueTasks({
- deployments,
+ tasks,
isLoading,
t,
}: {
- deployments: AgnetDeployment[]
+ tasks: HeicodeTask[]
isLoading: boolean
t: ReturnType['t']
}) {
const recent = useMemo(() => {
- const sorted = [...deployments].sort((a, b) => {
- const at = new Date(a.updated_at || a.created_at || 0).getTime()
- const bt = new Date(b.updated_at || b.created_at || 0).getTime()
- return bt - at
- })
+ const sorted = [...tasks].sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0))
return sorted.slice(0, 4)
- }, [deployments])
+ }, [tasks])
return (
@@ -279,24 +291,25 @@ function ContinueTasks({
) : (
- {recent.map((dep) => (
- -
+ {recent.map((task) => (
+
-
- {dep.orchestration_plan?.objective ||
- dep.orchestration_plan?.template_hint ||
- t('No objective')}
+ {task.name || task.intent || t('Untitled task')}
- {t('Updated')} {formatRelativeTime(dep.updated_at || dep.created_at)} {t('ago')}
+ {task.status_caption || task.status}
+ {' · '}
+ {t('Updated')}{' '}
+ {formatRelativeTimeMs(task.updated_at)} {t('ago')}
-
+
))}
@@ -307,26 +320,25 @@ function ContinueTasks({
}
function TodayFocus({
- deployments,
+ tasks,
isLoading,
t,
}: {
- deployments: AgnetDeployment[]
+ tasks: HeicodeTask[]
isLoading: boolean
t: ReturnType['t']
}) {
const buckets = useMemo(() => {
- const failed = deployments.filter(
- (d) => classifyStatus(d.phase || d.status || '') === 'failed'
- )
- const pending = deployments.filter(
- (d) => classifyStatus(d.phase || d.status || '') === 'pending'
- )
- const running = deployments.filter(
- (d) => classifyStatus(d.phase || d.status || '') === 'running'
+ const failed = tasks.filter((task) => classifyStatus(task.status) === 'failed')
+ // "Pending confirmation" maps to HeicodeTask.status = 'configuring'
+ // (followups not all answered yet) AND tasks awaiting_approval.
+ const pending = tasks.filter(
+ (task) =>
+ task.status === 'configuring' || task.status === 'awaiting_approval'
)
+ const running = tasks.filter((task) => classifyStatus(task.status) === 'running')
return { failed, pending, running }
- }, [deployments])
+ }, [tasks])
const focusItems: Array<{
Icon: typeof XCircle
@@ -504,18 +516,22 @@ function HelperEntries({
export function HomeHeroView() {
const { t } = useTranslation()
- const { data = [], isLoading } = useQuery({
- queryKey: ['agnet', 'deployments'],
- queryFn: listAgnetDeploymentsQuiet,
+ const { data, isLoading } = useQuery({
+ queryKey: ['heicode', 'tasks', 'recent'],
+ queryFn: () => listHeicodeTasks({ limit: 20 }),
refetchInterval: 60_000,
+ // mcp-server may be unreachable while the JWT bridge isn't established.
+ // Treat that as empty list instead of a hard error in the hero.
+ retry: false,
})
+ const tasks = data?.items ?? []
return (
diff --git a/heicode/web/default/src/features/tasks/task-card-view.tsx b/heicode/web/default/src/features/tasks/task-card-view.tsx
index f2635e3..b33a75c 100644
--- a/heicode/web/default/src/features/tasks/task-card-view.tsx
+++ b/heicode/web/default/src/features/tasks/task-card-view.tsx
@@ -1,19 +1,20 @@
/**
- * Task Card — implements docs/product-package/10 §"任务卡" + 11 §3
- * 任务卡 wireframe. Renders one AgnetDeployment as the user-facing
- * task object: 目标 / 第一版范围 / 自动生成 / 待确认上下文 + Manager 辅助按钮.
+ * Task Card route — implements docs/product-package/10 §"任务卡" + 11 §3
+ * 任务卡 wireframe, backed by mcp-server §6 HeicodeTask object:
*
- * Source of data: existing /api/agnet/deployments list (filtered by id).
- * When backend gains a /api/task/$id idea-task endpoint we'll switch the
- * data source over; the card shape stays the same.
+ * - status = "configuring" → render the open follow-up questions
+ * (each option becomes a clickable chip; clicking POSTs answer to
+ * /api/user/tasks/$id/answer and the backend state machine advances).
+ * - status = "running" / etc → render task.card with the four blocks
+ * the spec mandates: 目标 / 第一版范围 / 自动生成 / 待确认上下文.
*
- * Forbidden per §10 "高级展开": no JSON editor, no payload table, no
- * permission manifest, no resource_grant editor. Only show脱敏 summary.
+ * Forbidden per §10 高级展开: no JSON editor, no permission manifest, no
+ * resource_grant editor. Only the user-facing summary fields.
*/
-import { useMemo } from 'react'
import { Link, getRouteApi } from '@tanstack/react-router'
-import { useQuery } from '@tanstack/react-query'
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
+import { toast } from 'sonner'
import {
ArrowLeft,
ArrowRight,
@@ -21,6 +22,7 @@ import {
CircleDashed,
GitBranch,
ListChecks,
+ MessageSquare,
PencilLine,
PlayCircle,
Rocket,
@@ -34,35 +36,33 @@ import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { cn } from '@/lib/utils'
import {
- listAgnetDeploymentsQuiet,
- type AgnetDeployment,
-} from '@/features/agnet-console/api'
+ answerHeicodeTask,
+ getHeicodeTask,
+ type HeicodeFollowup,
+ type HeicodeTask,
+ type HeicodeTaskStatus,
+} from '@/lib/heicode-mcp'
const route = getRouteApi('/_authenticated/tasks/$id')
type StatusKey = 'running' | 'success' | 'failed' | 'pending'
-const STATUS_MAP: Record = {
+const STATUS_MAP: Record = {
+ draft: 'pending',
+ configuring: 'pending',
+ awaiting_approval: 'pending',
running: 'running',
- active: 'running',
- in_progress: 'running',
- succeeded: 'success',
- success: 'success',
+ paused: 'pending',
completed: 'success',
failed: 'failed',
- error: 'failed',
- rejected: 'failed',
- pending: 'pending',
- queued: 'pending',
- awaiting: 'pending',
}
function classifyStatus(s: string): StatusKey {
- return STATUS_MAP[(s || '').toLowerCase()] ?? 'pending'
+ return STATUS_MAP[s] ?? 'pending'
}
-function StatusBadge({ phase }: { phase: string }) {
- const k = classifyStatus(phase)
+function StatusBadge({ status }: { status: HeicodeTaskStatus | string }) {
+ const k = classifyStatus(status)
const map: Record = {
running: {
cls: 'bg-[color-mix(in_oklch,var(--primary)_22%,transparent)] text-primary ring-primary/40',
@@ -90,42 +90,55 @@ function StatusBadge({ phase }: { phase: string }) {
)}
>
- {phase || 'pending'}
+ {status}
)
}
-/**
- * Derive the "第一版范围" (first-version scope) bullet list from the
- * orchestration_plan.agents goals. If the plan didn't capture explicit
- * scope items we fall back to a single "see objective" line so the card
- * never renders a blank section.
- */
-function deriveFirstVersionScope(dep: AgnetDeployment, fallback: string): string[] {
- const agents = dep.orchestration_plan?.agents ?? []
- const goals = agents
- .map((a) => (a.goal || '').trim())
- .filter((g) => g.length > 0)
- if (goals.length > 0) return goals.slice(0, 6)
- return [fallback]
+function readScopeArray(card: HeicodeTask['card'], key: string): string[] {
+ if (!card) return []
+ const v = (card as Record)[key]
+ if (Array.isArray(v)) return v.filter((x): x is string => typeof x === 'string')
+ if (typeof v === 'string' && v.trim()) return [v]
+ return []
+}
+
+/** Collect open follow-ups from the most recent Heicode thread entry. */
+function collectOpenFollowups(task: HeicodeTask): HeicodeFollowup[] {
+ for (let i = task.thread.length - 1; i >= 0; i--) {
+ const entry = task.thread[i]!
+ if (entry.kind === 'heicode' && entry.followups && entry.followups.length > 0) {
+ return entry.followups
+ }
+ }
+ return []
}
export function TaskCardView() {
const { t } = useTranslation()
const { id } = route.useParams()
+ const queryClient = useQueryClient()
- // Source: the existing deployments list (filter by id). When the backend
- // gains a /api/task/$id idea-task endpoint we swap this for a single
- // useQuery against that endpoint.
- const { data: deployments = [], isLoading } = useQuery({
- queryKey: ['agnet', 'deployments'],
- queryFn: listAgnetDeploymentsQuiet,
+ const { data: task, isLoading } = useQuery({
+ queryKey: ['heicode', 'task', id],
+ queryFn: () => getHeicodeTask(id),
+ refetchInterval: 15_000,
})
- const dep = useMemo(
- () => deployments.find((d) => d.deployment_id === id),
- [deployments, id]
- )
+ const answerMutation = useMutation({
+ mutationFn: ({ qid, oid }: { qid: string; oid: string }) =>
+ answerHeicodeTask(id, qid, oid),
+ onSuccess: (updated) => {
+ queryClient.setQueryData(['heicode', 'task', id], updated)
+ void queryClient.invalidateQueries({ queryKey: ['heicode', 'tasks', 'recent'] })
+ if (updated.status === 'running') {
+ toast.success(t('All follow-ups answered. Heicode generated the recommendation summary.'))
+ }
+ },
+ onError: (err) => {
+ toast.error(err instanceof Error ? err.message : t('Failed to answer follow-up'))
+ },
+ })
if (isLoading) {
return (
@@ -137,7 +150,7 @@ export function TaskCardView() {
)
}
- if (!dep) {
+ if (!task) {
return (
- {/* 第一版范围 */}
@@ -224,7 +315,6 @@ export function TaskCardView() {
- {/* 自动生成 */}
@@ -241,7 +331,6 @@ export function TaskCardView() {
- {/* 需要 Manager 辅助完成 */}
{t('Needs Manager assistance')}
@@ -274,7 +363,6 @@ export function TaskCardView() {
- {/* CTA footer */}