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:
2026-05-07 17:39:02 +08:00
co-authored by Claude Opus 4.7
parent 86bad2323f
commit d1db2c1501
6 changed files with 86 additions and 4 deletions
@@ -4,6 +4,7 @@ import { EmptySession } from '../../pages/EmptySession'
import { ActiveSession } from '../../pages/ActiveSession' import { ActiveSession } from '../../pages/ActiveSession'
import { ScheduledTasks } from '../../pages/ScheduledTasks' import { ScheduledTasks } from '../../pages/ScheduledTasks'
import { Settings } from '../../pages/Settings' import { Settings } from '../../pages/Settings'
import { ResourceBindings } from '../../pages/ResourceBindings'
import { TerminalSettings } from '../../pages/TerminalSettings' import { TerminalSettings } from '../../pages/TerminalSettings'
export function ContentRouter() { export function ContentRouter() {
@@ -19,6 +20,8 @@ export function ContentRouter() {
page = <Settings /> page = <Settings />
} else if (activeTabType === 'scheduled') { } else if (activeTabType === 'scheduled') {
page = <ScheduledTasks /> page = <ScheduledTasks />
} else if (activeTabType === 'resources') {
page = <ResourceBindings />
} else if (activeTabType !== 'terminal') { } else if (activeTabType !== 'terminal') {
page = <ActiveSession /> page = <ActiveSession />
} }
@@ -5,7 +5,7 @@ import { useTranslation } from '../../i18n'
import { ProjectFilter } from './ProjectFilter' import { ProjectFilter } from './ProjectFilter'
import { ConfirmDialog } from '../shared/ConfirmDialog' import { ConfirmDialog } from '../shared/ConfirmDialog'
import type { SessionListItem } from '../../types/session' 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 { useChatStore } from '../../stores/chatStore'
import { useHeicodeAuthStore } from '../../stores/heicodeAuthStore' import { useHeicodeAuthStore } from '../../stores/heicodeAuthStore'
@@ -195,6 +195,15 @@ export function Sidebar() {
> >
{t('sidebar.scheduled')} {t('sidebar.scheduled')}
</NavItem> </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 <NavItem
active={activeTabType === 'terminal'} active={activeTabType === 'terminal'}
collapsed={!sidebarOpen} collapsed={!sidebarOpen}
+8
View File
@@ -18,6 +18,7 @@ export const en = {
// ─── Sidebar ────────────────────────────────────── // ─── Sidebar ──────────────────────────────────────
'sidebar.newSession': 'New session', 'sidebar.newSession': 'New session',
'sidebar.scheduled': 'Scheduled', 'sidebar.scheduled': 'Scheduled',
'sidebar.resources': 'Resources',
'sidebar.terminal': 'Terminal', 'sidebar.terminal': 'Terminal',
'sidebar.settings': 'Settings', 'sidebar.settings': 'Settings',
'sidebar.searchPlaceholder': 'Search sessions...', 'sidebar.searchPlaceholder': 'Search sessions...',
@@ -955,6 +956,13 @@ export const en = {
'serverVerb.Task started': 'Task started', 'serverVerb.Task started': 'Task started',
'serverVerb.Task in progress': 'Task in progress', '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 ────────────────────────────────────── // ─── HeiCode Login ──────────────────────────────────────
'login.subtitle': 'Sign in to start using Heicode', 'login.subtitle': 'Sign in to start using Heicode',
'login.footnote': 'Heicode talks directly to TaijiAICloud; your API key never leaves this machine.', 'login.footnote': 'Heicode talks directly to TaijiAICloud; your API key never leaves this machine.',
+8
View File
@@ -20,6 +20,7 @@ export const zh: Record<TranslationKey, string> = {
// ─── Sidebar ────────────────────────────────────── // ─── Sidebar ──────────────────────────────────────
'sidebar.newSession': '新建会话', 'sidebar.newSession': '新建会话',
'sidebar.scheduled': '定时任务', 'sidebar.scheduled': '定时任务',
'sidebar.resources': '资源绑定',
'sidebar.terminal': '终端', 'sidebar.terminal': '终端',
'sidebar.settings': '设置', 'sidebar.settings': '设置',
'sidebar.searchPlaceholder': '搜索会话...', 'sidebar.searchPlaceholder': '搜索会话...',
@@ -957,6 +958,13 @@ export const zh: Record<TranslationKey, string> = {
'serverVerb.Task started': '任务已启动', 'serverVerb.Task started': '任务已启动',
'serverVerb.Task in progress': '任务进行中', '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 登录 ────────────────────────────────────── // ─── HeiCode 登录 ──────────────────────────────────────
'login.subtitle': '登录以开始使用 Heicode', 'login.subtitle': '登录以开始使用 Heicode',
'login.footnote': 'Heicode 直接连 TaijiAICloud,API Key 仅保存在你这台机器上。', '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>
)
}
+4 -3
View File
@@ -5,9 +5,10 @@ const TAB_STORAGE_KEY = 'cc-haha-open-tabs'
export const SETTINGS_TAB_ID = '__settings__' export const SETTINGS_TAB_ID = '__settings__'
export const SCHEDULED_TAB_ID = '__scheduled__' export const SCHEDULED_TAB_ID = '__scheduled__'
export const RESOURCES_TAB_ID = '__resources__'
export const TERMINAL_TAB_PREFIX = '__terminal__' export const TERMINAL_TAB_PREFIX = '__terminal__'
export type TabType = 'session' | 'settings' | 'scheduled' | 'terminal' export type TabType = 'session' | 'settings' | 'scheduled' | 'resources' | 'terminal'
export type Tab = { export type Tab = {
sessionId: string sessionId: string
@@ -162,13 +163,13 @@ export const useTabStore = create<TabStore>((set, get) => ({
const validTabs: Tab[] = data.openTabs const validTabs: Tab[] = data.openTabs
.filter((t) => { .filter((t) => {
// Special tabs are always valid // 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 if (t.type === 'terminal') return false
// Session tabs must exist on server // Session tabs must exist on server
return existingIds.has(t.sessionId) return existingIds.has(t.sessionId)
}) })
.map((t) => { .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 { sessionId: t.sessionId, title: t.title, type: t.type, status: 'idle' as const }
} }
return { return {