feat(resources): add Resources tab shell aligned with Heicode full-stack vision
Per docs/vision-heicode-full-stack-agentic-dev.md and plan.md P1, Heicode is a full-stack agentic dev platform — not a chat-only client. cc-haha needs a "Resources" entry where users bind Git repos / SK / project docs / cloud accounts and grant them to sub-agents. mcp-server team has the P1 9 endpoints live (POST/GET/PUT/DELETE /api/resources, POST/GET/DELETE /api/resource-grants — see Heicode-接口契约文档.md §2-§3). cc-haha has not consumed them yet. This commit ships slice 2 (page shell): - tabStore: new TabType 'resources' + RESOURCES_TAB_ID export - Sidebar: new nav entry between 'scheduled' and 'terminal' (link icon) - ContentRouter: route 'resources' tab to <ResourceBindings /> - pages/ResourceBindings.tsx: header + "coming soon" placeholder card - i18n: sidebar.resources + resources.* keys (zh + en) Slice 3 (next): persist mcp-server JWT in provider record so the local Bun server can proxy /api/heicode-resources/* to apimtaiji with a fresh Authorization Bearer header. Refresh logic on the 24h boundary. Slice 4 (next): actual Bindings list + Create/Delete + Grants list + Create/Revoke modals. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { EmptySession } from '../../pages/EmptySession'
|
||||
import { ActiveSession } from '../../pages/ActiveSession'
|
||||
import { ScheduledTasks } from '../../pages/ScheduledTasks'
|
||||
import { Settings } from '../../pages/Settings'
|
||||
import { ResourceBindings } from '../../pages/ResourceBindings'
|
||||
import { TerminalSettings } from '../../pages/TerminalSettings'
|
||||
|
||||
export function ContentRouter() {
|
||||
@@ -19,6 +20,8 @@ export function ContentRouter() {
|
||||
page = <Settings />
|
||||
} else if (activeTabType === 'scheduled') {
|
||||
page = <ScheduledTasks />
|
||||
} else if (activeTabType === 'resources') {
|
||||
page = <ResourceBindings />
|
||||
} else if (activeTabType !== 'terminal') {
|
||||
page = <ActiveSession />
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from '../../i18n'
|
||||
import { ProjectFilter } from './ProjectFilter'
|
||||
import { ConfirmDialog } from '../shared/ConfirmDialog'
|
||||
import type { SessionListItem } from '../../types/session'
|
||||
import { useTabStore, SETTINGS_TAB_ID, SCHEDULED_TAB_ID } from '../../stores/tabStore'
|
||||
import { useTabStore, SETTINGS_TAB_ID, SCHEDULED_TAB_ID, RESOURCES_TAB_ID } from '../../stores/tabStore'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import { useHeicodeAuthStore } from '../../stores/heicodeAuthStore'
|
||||
|
||||
@@ -195,6 +195,15 @@ export function Sidebar() {
|
||||
>
|
||||
{t('sidebar.scheduled')}
|
||||
</NavItem>
|
||||
<NavItem
|
||||
active={activeTabId === RESOURCES_TAB_ID}
|
||||
collapsed={!sidebarOpen}
|
||||
label={t('sidebar.resources')}
|
||||
onClick={() => useTabStore.getState().openTab(RESOURCES_TAB_ID, t('sidebar.resources'), 'resources')}
|
||||
icon={<span className="material-symbols-outlined text-[18px]">link</span>}
|
||||
>
|
||||
{t('sidebar.resources')}
|
||||
</NavItem>
|
||||
<NavItem
|
||||
active={activeTabType === 'terminal'}
|
||||
collapsed={!sidebarOpen}
|
||||
|
||||
@@ -18,6 +18,7 @@ export const en = {
|
||||
// ─── Sidebar ──────────────────────────────────────
|
||||
'sidebar.newSession': 'New session',
|
||||
'sidebar.scheduled': 'Scheduled',
|
||||
'sidebar.resources': 'Resources',
|
||||
'sidebar.terminal': 'Terminal',
|
||||
'sidebar.settings': 'Settings',
|
||||
'sidebar.searchPlaceholder': 'Search sessions...',
|
||||
@@ -955,6 +956,13 @@ export const en = {
|
||||
'serverVerb.Task started': 'Task started',
|
||||
'serverVerb.Task in progress': 'Task in progress',
|
||||
|
||||
// ─── Resource Bindings (mcp-server P1 nine endpoints) ────
|
||||
'resources.title': 'Resources',
|
||||
'resources.subtitle': 'Bind Git repos / SK / project docs / cloud accounts / cloud resources, then grant them to sub-agents.',
|
||||
'resources.empty.title': 'Not wired up yet',
|
||||
'resources.empty.body': 'This page integrates the Heicode Manager P1 resource model APIs (mcp-server has 9 endpoints live). Create / list / revoke bindings and grants are coming soon.',
|
||||
'resources.empty.comingSoon': 'Coming soon — Manager service-credential auth and grant UI under development',
|
||||
|
||||
// ─── HeiCode Login ──────────────────────────────────────
|
||||
'login.subtitle': 'Sign in to start using Heicode',
|
||||
'login.footnote': 'Heicode talks directly to TaijiAICloud; your API key never leaves this machine.',
|
||||
|
||||
@@ -20,6 +20,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
// ─── Sidebar ──────────────────────────────────────
|
||||
'sidebar.newSession': '新建会话',
|
||||
'sidebar.scheduled': '定时任务',
|
||||
'sidebar.resources': '资源绑定',
|
||||
'sidebar.terminal': '终端',
|
||||
'sidebar.settings': '设置',
|
||||
'sidebar.searchPlaceholder': '搜索会话...',
|
||||
@@ -957,6 +958,13 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'serverVerb.Task started': '任务已启动',
|
||||
'serverVerb.Task in progress': '任务进行中',
|
||||
|
||||
// ─── 资源绑定(接 mcp-server P1 9 接口)──────────────
|
||||
'resources.title': '资源绑定',
|
||||
'resources.subtitle': '绑定 Git 仓库 / SK / 项目文档 / 云账号 / 云资源,并把它们授给子 Agent 使用。',
|
||||
'resources.empty.title': '尚未实装',
|
||||
'resources.empty.body': '本页对接 Heicode Manager 的 P1 资源模型接口(mcp-server 已上线 9 个端点)。后续将支持创建 / 列表 / 撤销绑定与授权。',
|
||||
'resources.empty.comingSoon': '即将上线 — Manager 服务凭据接入与授权 UI 开发中',
|
||||
|
||||
// ─── HeiCode 登录 ──────────────────────────────────────
|
||||
'login.subtitle': '登录以开始使用 Heicode',
|
||||
'login.footnote': 'Heicode 直接连 TaijiAICloud,API Key 仅保存在你这台机器上。',
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// desktop/src/pages/ResourceBindings.tsx
|
||||
//
|
||||
// 资源绑定页面 — 接 mcp-server 的 P1 9 接口(已上线):
|
||||
// GET /api/resources 列出当前用户所有 ResourceBinding
|
||||
// POST /api/resources 新建 binding(git/sk/project_doc/cloud_account/cloud_resource)
|
||||
// GET /api/resources/{id} 单个 binding 详情
|
||||
// PUT /api/resources/{id} 更新 binding
|
||||
// DELETE /api/resources/{id} 软删除(status=revoked)
|
||||
// POST /api/resource-grants 把 binding 授给 role/agent
|
||||
// GET /api/resource-grants 列出当前用户所有 grant
|
||||
// GET /api/resource-grants/{id} 单个 grant
|
||||
// DELETE /api/resource-grants/{id} 撤销
|
||||
//
|
||||
// 当前为切片 2 的 shell —— 显示标题 + 占位说明,让用户能看到入口。
|
||||
// 切片 3+4 接入实际 CRUD。
|
||||
|
||||
import { useTranslation } from '../i18n'
|
||||
|
||||
export function ResourceBindings() {
|
||||
const t = useTranslation()
|
||||
return (
|
||||
<div className="relative flex flex-1 flex-col overflow-auto bg-[var(--color-surface)]">
|
||||
<div className="mx-auto w-full max-w-4xl px-8 py-12">
|
||||
<header className="mb-8">
|
||||
<h1
|
||||
className="text-2xl font-bold tracking-tight text-[var(--color-text-primary)]"
|
||||
style={{ fontFamily: 'var(--font-headline)' }}
|
||||
>
|
||||
{t('resources.title')}
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-[var(--color-text-secondary)]">
|
||||
{t('resources.subtitle')}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container)] p-8 text-center shadow-[var(--shadow-dropdown)]">
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full border border-[var(--color-border)] bg-[var(--color-surface-container-low)] text-[var(--color-text-tertiary)]">
|
||||
<span className="material-symbols-outlined text-[22px]">link</span>
|
||||
</div>
|
||||
<h2 className="text-base font-semibold text-[var(--color-text-primary)]">
|
||||
{t('resources.empty.title')}
|
||||
</h2>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm leading-relaxed text-[var(--color-text-secondary)]">
|
||||
{t('resources.empty.body')}
|
||||
</p>
|
||||
<p className="mx-auto mt-4 max-w-md text-xs text-[var(--color-text-tertiary)]">
|
||||
{t('resources.empty.comingSoon')}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,9 +5,10 @@ const TAB_STORAGE_KEY = 'cc-haha-open-tabs'
|
||||
|
||||
export const SETTINGS_TAB_ID = '__settings__'
|
||||
export const SCHEDULED_TAB_ID = '__scheduled__'
|
||||
export const RESOURCES_TAB_ID = '__resources__'
|
||||
export const TERMINAL_TAB_PREFIX = '__terminal__'
|
||||
|
||||
export type TabType = 'session' | 'settings' | 'scheduled' | 'terminal'
|
||||
export type TabType = 'session' | 'settings' | 'scheduled' | 'resources' | 'terminal'
|
||||
|
||||
export type Tab = {
|
||||
sessionId: string
|
||||
@@ -162,13 +163,13 @@ export const useTabStore = create<TabStore>((set, get) => ({
|
||||
const validTabs: Tab[] = data.openTabs
|
||||
.filter((t) => {
|
||||
// Special tabs are always valid
|
||||
if (t.type === 'settings' || t.type === 'scheduled') return true
|
||||
if (t.type === 'settings' || t.type === 'scheduled' || t.type === 'resources') return true
|
||||
if (t.type === 'terminal') return false
|
||||
// Session tabs must exist on server
|
||||
return existingIds.has(t.sessionId)
|
||||
})
|
||||
.map((t) => {
|
||||
if (t.type === 'settings' || t.type === 'scheduled') {
|
||||
if (t.type === 'settings' || t.type === 'scheduled' || t.type === 'resources') {
|
||||
return { sessionId: t.sessionId, title: t.title, type: t.type, status: 'idle' as const }
|
||||
}
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user