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 { 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}
|
||||||
|
|||||||
@@ -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.',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user