feat(ui): 功能区重构阶段二——洞察页 + 全局/项目层级归位(N3/N4/N5)

N3 洞察页三 tab(飞轮/健康中心/全局治理):
   - FlywheelPanel(版本队列/阻塞/停转归档/Git质量/梦核)整体迁出 BlueprintModal
     ——跨项目内容不再寄生单项目弹窗,BlueprintModal 减重 414 行
   - CONVENTIONS 规则来源 + 应用梦核建议迁至全局治理 tab(影响所有项目的操作归全局层)
   - BlueprintModal 侧板只留单项目内容(模块详情/余票/可派发/项目治理)
N4 Gitea 层级归位:实例管理(全局配置)拆为 GiteaInstancesSection 挂设置页;
   项目绑定+webhook 留治理面板,无实例时提示去设置页添加;共用 queryKey 缓存
N5 仓库注册表并入 项目→管理→仓库 tab(RegistryTab 复用),Sidebar 底部入口
   与弹窗死壳移除;总览页更名对齐"跨项目行动项"定位

至此 ui-restructure N1-N5 全部完成:三条公理(名实一致/层级对齐/频率分层)落地。
零后端变更,typecheck 全绿。
This commit is contained in:
lanrtop 2026-07-02 14:51:43 +09:00
parent bc07dd41bf
commit bf697b5e28
15 changed files with 1031 additions and 763 deletions

View File

@ -400,9 +400,9 @@ modules:
- id: ui-restructure
name: 功能区 UI 重构
area: frontend
status: in_progress
progress: 40
note: "阶段一完成N1 六组导航 + N2 卡片分层);阶段二 N3/N4/N5 待用一周后启动"
status: done
progress: 100
note: "N1-N5 全部完成2026-07-02六组导航 + 卡片分层 + 洞察页 + Gitea/仓库归位"
position: [3750, 300]
edges:

View File

@ -96,8 +96,8 @@ Gitea push → POST /webhook/gitea/:project_id
> ⚠️ push 事件部分已被 G3/G4 实现取代剩余范围收窄为「PR/CI/review 事件 + 轮询降级」见「PR 事件 ↔ 蓝图任务卡联动」concept 卡)。
### 📋 Gitea Actions workflow 生成
- status: todo
### 🔵 Gitea Actions workflow 生成
- status: in_progress
- complexity: M
- depends: G1
- files: src-tauri/src/commands/cicd.rs, src/components/dashboard/CicdModal.tsx

View File

@ -37,22 +37,22 @@ Gitea 全局实例配置错位在单项目弹窗内。
- files: src/components/dashboard/ProjectCard.tsx
- acceptance: 底部操作区只显示 🗺️ 蓝图 / 🎓 导师 / 编辑 / 动态 四个明按钮 + ⋯ 更多下拉(收纳 🤖 Claude 配置、📝 个人备忘、🖥️ 关联服务器、🚀 CI/CD、🔬 环境扫描、🧰 工具管理带文字标签下拉点击外部自动关闭所有原功能可达typecheck 通过
### 📋 N3. 洞察页:飞轮跨项目内容迁出 BlueprintModal + 健康中心并入
- status: todo
### N3. 洞察页:飞轮跨项目内容迁出 BlueprintModal + 健康中心并入
- status: done
- complexity: M
- depends: N1
- files: src/components/insight/InsightPage.tsx新增, src/components/dashboard/BlueprintModal.tsx, src/components/health/HealthPage.tsx
- acceptance: 洞察页 3 tab飞轮 / 健康 / 全局治理FlywheelPanel 的跨项目内容(梦核提示词、归档候选、全项目 Git 质量表、全量导入按钮)迁至飞轮 tabCONVENTIONS 版本管理与应用梦核建议迁至全局治理 tabBlueprintModal 侧板只保留单项目内容typecheck 通过
### 📋 N4. Gitea 实例管理迁设置页GiteaPanel 拆全局/项目级两半)
- status: todo
### N4. Gitea 实例管理迁设置页GiteaPanel 拆全局/项目级两半)
- status: done
- complexity: S
- depends: N1
- files: src/components/dashboard/GiteaPanel.tsx, src/components/settings/SettingsPage.tsx
- acceptance: 实例管理区(添加/测试/删除拆为独立组件挂到设置页BlueprintModal 治理面板只保留项目级绑定与 webhook 区(无实例时提示去设置页添加);两处共用 ["gitea-instances"] queryKeytypecheck 通过
### 📋 N5. 仓库注册表并入项目页管理 tab + 总览收敛为行动项
- status: todo
### N5. 仓库注册表并入项目页管理 tab + 总览收敛为行动项
- status: done
- complexity: S
- depends: N1
- files: src/components/manage/ManagePage.tsx, src/components/layout/Sidebar.tsx, src/components/overview/OverviewPage.tsx, src/components/dashboard/RepoRegistryModal.tsx
@ -75,3 +75,13 @@ Gitea 全局实例配置错位在单项目弹窗内。
遗留风险: ① 洞察页暂挂健康中心N3 未做前名实略不符,导航先行占位);② ProjectCard 更多菜单向上弹出卡片在视口顶部时可能被裁剪overflow 场景待实际使用观察)
auto_applied_rules: R01
rule_effectiveness: R01=未触发(无新 command纯前端
【复盘】N3+N4+N5 阶段二:洞察页 + 层级归位2026-07-02
任务类型: 前端组件
复杂度: M + S×2
实际轮数: 1轮预估 1 轮)
是否返工: 否
有效策略: FlywheelPanel 整体函数级迁移(组件+子组件+查询原样搬家,仅去掉 onCloseBlueprintModal 用脚本按锚点批量切除 414 行避免手工 Edit 匹配失败;全局/项目级拆分以"操作影响范围"为判据(影响所有项目→洞察/设置,影响单项目→弹窗)
遗留风险: ① RegistryTab 在 ManagePage 中的 max-h-[70vh] 容器是估计值,长列表滚动体验待实际验证;② BlueprintModal 删除跨项目内容后侧板宽度 340px 未调整,治理面板可能显空旷
auto_applied_rules: R04
rule_effectiveness: R04=有效(本 session 改动 10+ 文件但全部在预建任务卡范围内,无漂移)

View File

@ -8318,6 +8318,246 @@
"todo": 4,
"total": 188
}
},
{
"at": "2026-07-02T05:41:07Z",
"blocked_reasons": [],
"conventions_version": "1.9.0",
"delta": {
"newly_blocked": [],
"newly_done": [
"agent-infrastructure/F3. Agent 健康诊断 + 可复制修正指令",
"flywheel-intelligence/R2. CONVENTIONS 规则边界字段 + R11 失败记录规则",
"gitea-integration/G2. 仓库创建 / 迁移 / 绑定",
"gitea-integration/G3. Webhook 接收端MCP Server 扩展)",
"gitea-integration/G4. Webhook 注册命令",
"gitea-integration/G5. Gitea 治理面板 UI",
"github-decommission/D1. 免登录改造(去掉 GitHub OAuth 登录门槛)",
"github-decommission/D2. 移除 GitHub 发布 / 导入 / PR / CI 状态入口",
"github-decommission/D3. 删除 spacesSync单机化跨空间 GitHub 云同步下线)",
"github-decommission/D4. git_ops 凭据去 GitHub 化",
"ui-restructure/N1. 资源/工具一级页 + Sidebar 六组重组 + Dashboard 瘦身",
"ui-restructure/N2. ProjectCard 操作分层4 明按钮 + ⋯ 更多下拉)"
],
"unblocked": []
},
"iteration": 1,
"modules": {
"blocked": 0,
"concept": 0,
"done": 48,
"in_progress": 3,
"planned": 0,
"stalled": [
"flywheel-intelligence",
"ui-restructure"
],
"total": 53
},
"task_ids": {
"blocked": [],
"done": [
"agent-infrastructure/A. 框架层扫描 + 契约路径推断",
"agent-infrastructure/B. 三件套文件生成",
"agent-infrastructure/C. 文档腐化评分",
"agent-infrastructure/D. 栈确认 UI + 生成按钮",
"agent-infrastructure/E. 看板腐化指示器",
"agent-infrastructure/F3. Agent 健康诊断 + 可复制修正指令",
"api-keys/API 密钥 CRUD 与安全展示",
"auto-update/Capabilities 权限配置",
"auto-update/下载 & 安装",
"auto-update/更新检测",
"auto-update/自动检查更新",
"auto-update/错误提示",
"blueprint-buff/BlueprintView 跨项目切换",
"blueprint-buff/Buff 生命周期 Rust 命令",
"blueprint-buff/blueprint_buffs 数据表",
"blueprint-buff/git watcher + 任务匹配写回 + 飞轮快照",
"blueprint-buff/前端 Buff 管理 UI",
"blueprint-buff/前端实时刷新",
"blueprint-feedback/归档 command",
"blueprint-feedback/归档候选展示",
"blueprint-feedback/梦核分析入口(蓝图弹窗)",
"blueprint-feedback/梦核提示词增加停滞模块数据",
"blueprint-flywheel/get_flywheel_stats command",
"blueprint-flywheel/get_flywheel_stats 支持迭代感知",
"blueprint-flywheel/usage.json 埋点写入",
"blueprint-flywheel/梦核提示词生成 command",
"blueprint-flywheel/飞轮健康面板",
"blueprint-governance/CLAUDE.md 模板定义",
"blueprint-governance/CONVENTIONS.md 版本化",
"blueprint-governance/GovernancePanel 规则编辑器 UI",
"blueprint-governance/get_blueprint_status command",
"blueprint-governance/sync/梦核/summary 切换为 effective 内容",
"blueprint-governance/sync_blueprint_rules command",
"blueprint-governance/批量同步所有项目规则",
"blueprint-governance/用户版 CONVENTIONS 存储层初始化",
"blueprint-governance/用户版 CONVENTIONS 读写 command",
"blueprint-onboarding/generate_blueprint_prompt command",
"blueprint-onboarding/蓝图操作弹窗",
"blueprint-onboarding/项目列表蓝图状态徽章",
"blueprint-reader/合并返回完整蓝图数据",
"blueprint-reader/读取 manifest.yaml",
"blueprint-reader/读取 modules/*.md",
"blueprint-view/.blueprint/ 数据结构设计",
"blueprint-view/React Flow 节点图渲染",
"blueprint-view/blocked 状态支持",
"blueprint-view/下一步汇总面板",
"blueprint-view/任务卡复制派发",
"blueprint-view/批量执行提示词入口",
"blueprint-view/智能连线路由",
"blueprint-view/模块详情面板",
"blueprint-view/统计汇总条",
"blueprint-view/蓝图治理面板",
"blueprint-view/蓝图读取 Command",
"blueprint-view/边高亮与模块聚焦",
"blueprint-view/顶部状态行扩展",
"blueprint-view/项目卡片蓝图入口",
"cicd-workflow/Actions 运行历史",
"cicd-workflow/CI/CD 配置 CRUD",
"cicd-workflow/Tauri OSS 分发模板",
"cicd-workflow/Web SSH 部署模板",
"cicd-workflow/Workflow 文件生成",
"claude-autonomy-config/Beast 全局状态读取与展示",
"cloud-db-instances/云数据库实例信息管理",
"cloud-docker-apps/Docker 应用列表与状态管理",
"cloud-products/DB 迁移cloud_products 表",
"cloud-products/Dashboard 接入:云产品标签页",
"cloud-products/Rust CRUDcloud_products.rs",
"cloud-products/前端 PanelCloudProductsPanel.tsx",
"cloud-products/接线mod.rs + lib.rs + commands.ts",
"deploy-info/项目部署信息记录面板",
"devtools-scan/WSL 工具扫描",
"devtools-scan/Windows 工具扫描",
"devtools-scan/工具信息持久化",
"flywheel-intelligence/F1. Git 健康度后端——全量历史导入 + 跨项目聚合",
"flywheel-intelligence/F2. 飞轮面板——Git 健康度区块 + 全量导入按钮",
"flywheel-intelligence/MCP Server 支持 SSE 传输",
"flywheel-intelligence/R2. CONVENTIONS 规则边界字段 + R11 失败记录规则",
"flywheel-intelligence/复盘笔记覆盖率可见性",
"flywheel-intelligence/独立项目自动创建产品组",
"flywheel-intelligence/补测试:飞轮存量函数安全网",
"flywheel-intelligence/阶段一:复盘笔记接入梦核飞轮",
"flywheel-intelligence/阶段二usage.json 扩展采集维度",
"git-ops/上游状态检测",
"git-ops/分支管理",
"git-ops/基础操作",
"gitea-integration/G1. DB schema + Gitea 实例 CRUD",
"gitea-integration/G2. 仓库创建 / 迁移 / 绑定",
"gitea-integration/G3. Webhook 接收端MCP Server 扩展)",
"gitea-integration/G4. Webhook 注册命令",
"gitea-integration/G5. Gitea 治理面板 UI",
"github-auth/OAuth Device Flow",
"github-auth/Token 管理",
"github-decommission/D1. 免登录改造(去掉 GitHub OAuth 登录门槛)",
"github-decommission/D2. 移除 GitHub 发布 / 导入 / PR / CI 状态入口",
"github-decommission/D3. 删除 spacesSync单机化跨空间 GitHub 云同步下线)",
"github-decommission/D4. git_ops 凭据去 GitHub 化",
"github-publish/GitHub 仓库创建与推送",
"health-center/T1 · Backend 健康检查聚合命令",
"health-center/T2 · 质量指标聚合",
"health-center/T3 · 前端健康中心页面",
"health-center/T4 · 扩展 MCP get_diagnostics",
"host-apps/宿主机应用清单与快启",
"mcp-config-inject/加入产品组时注入配置",
"mcp-config-inject/端口变更时重新注入所有配置",
"mcp-config-inject/退出产品组时清除配置",
"mcp-server/MCP 端口可配置Rust 侧)",
"mcp-server/启动时绑定 HTTP 端口",
"mcp-server/实现 MCP JSON-RPC 基础协议",
"mcp-server/实现产品组上下文工具",
"onboarding-pack/A 模板分发引擎",
"onboarding-pack/B apply_onboarding_pack 编排 + 前端入口",
"onboarding-pack/C1 commit_metrics 表 + Conventional 解析器",
"onboarding-pack/C2 挂接 buff watcher + 飞轮快照",
"onboarding-pack/D 数据可信度提示",
"product-group-mcp/产品组详情显示 MCP 注入状态",
"product-group-mcp/设置页添加 MCP 端口配置",
"project-dashboard/分组画布",
"project-dashboard/项目卡片列表",
"project-dashboard/项目发现面板",
"project-dashboard/项目快捷操作",
"project-env-scan/环境信息持久化",
"project-env-scan/项目环境检测",
"project-mentor/T1 · 后端聚合 commandMentorContext",
"project-mentor/T10 · 启动健康扫描 + 前端 Alert Banner",
"project-mentor/T11 · 项目历史快照",
"project-mentor/T2 · MCP 项目导师查询接口",
"project-mentor/T3 · 前端导师面板",
"project-mentor/T4 · 炼境自指接入",
"project-mentor/T5 · project_notes 数据层 + 后端命令",
"project-mentor/T6 · MCP append_project_note 工具",
"project-mentor/T7 · MentorPanel 展示 AI 笔记",
"project-mentor/T8 · 后端全组巡检命令",
"project-mentor/T9 · 前端全组巡检按钮",
"project-mgmt/子文件夹扫描",
"project-mgmt/批量导入项目",
"project-mgmt/项目 CRUD",
"project-mgmt/项目档案Profile",
"project-templates/P1+P3 模板扩充React 生态全覆盖)",
"project-templates/从模板创建项目",
"project-templates/内置模板种植",
"project-templates/模板 CRUD",
"project-tools/工具 CRUD",
"project-tools/工具启动",
"repo-registry/仓库导入",
"repo-registry/仓库挂载",
"repo-registry/仓库注册表管理",
"runtime-diagnostics/T1 · SQLite runtime_logs 表",
"runtime-diagnostics/T2 · 替换 eprintln! 为结构化日志",
"runtime-diagnostics/T3 · MCP Server 新增 get_diagnostics 工具",
"runtime-diagnostics/T4 · 注入失败结果返回前端",
"runtime-diagnostics/T5 · 设置页 MCP 真实健康检查",
"server-registry/服务器 CRUD 与项目关联",
"server-software/服务器软件扫描与展示",
"service-config/服务配置 CRUD 与环境分组",
"settings-page/WSL 发行版选择",
"settings-page/关于 & 更新",
"settings-page/终端配置",
"settings-page/编辑器检测",
"shell-launch/打开终端",
"shell-launch/打开编辑器",
"source-library/源码库分类管理与项目详情",
"sqlite-db/数据库迁移",
"sqlite-db/连接池初始化",
"ssh-config-sync/SSH Config 写入与同步",
"tags-groups/分组管理",
"tags-groups/标签管理",
"tray-service/关闭窗口最小化到托盘",
"tray-service/系统托盘图标与菜单",
"tray-service/集成 tauri-plugin-single-instance",
"ui-restructure/N1. 资源/工具一级页 + Sidebar 六组重组 + Dashboard 瘦身",
"ui-restructure/N2. ProjectCard 操作分层4 明按钮 + ⋯ 更多下拉)",
"user-project-todos/前端:备忘清单 UI 组件",
"user-project-todos/数据层user_project_todos 表 + Rust commands",
"v2rayn-import/当前节点读取",
"v2rayn-import/数据库路径检测",
"v2rayn-import/节点列表扫描",
"vscode-remote/VS Code Remote SSH 一键连接",
"workspace-mgmt/空间 CRUD",
"workspace-mgmt/空间级设置",
"workspace-mgmt/部署命令管理",
"wsl-proxy/代理一致性测试",
"wsl-proxy/代理同步",
"wsl-proxy/代理清除",
"wsl-proxy/代理状态检测"
]
},
"tasks": {
"avg_rounds_M": null,
"blocked": 0,
"complexity_dist": {
"L": 6,
"M": 80,
"S": 53
},
"dispatched": 0,
"done": 184,
"in_progress": 0,
"rework_count": 23,
"todo": 7,
"total": 201
}
}
]
}

View File

@ -5,7 +5,7 @@ import { ProjectModal } from "./components/dashboard/ProjectModal";
import { ResourcesPage } from "./components/resources/ResourcesPage";
import { ToolsPage } from "./components/tools/ToolsPage";
import { SettingsPage } from "./components/settings/SettingsPage";
import { HealthPage } from "./components/health/HealthPage";
import { InsightPage } from "./components/insight/InsightPage";
import { Sidebar, type Page } from "./components/layout/Sidebar";
import { Toast } from "./components/ui/Toast";
import { getStartupAlerts, type StartupAlert } from "./lib/commands";
@ -68,8 +68,7 @@ export default function App() {
{page === "overview" && <OverviewPage />}
{page === "dashboard" && <Dashboard />}
{page === "resources" && <ResourcesPage />}
{/* 洞察页阶段一暂挂健康中心N3 扩充为 飞轮/健康/全局治理 三 tab */}
{page === "insight" && <HealthPage />}
{page === "insight" && <InsightPage />}
{page === "tools" && <ToolsPage />}
{page === "settings" && <SettingsPage />}
</div>

View File

@ -20,33 +20,19 @@ import {
getBlueprintStatus,
syncBlueprintRules,
generateBlueprintPrompt,
getProjects,
getFlywheelStats,
generateMuhePrompt,
archiveProjectDocs,
applyOnboardingPack,
getCommitStats,
removeBlueprintBuff,
getBuffStatus,
getUserConventionsStatus,
saveUserConventions,
resetConventionsToBuiltin,
type BlueprintData,
type BlueprintModule,
type BlueprintTask,
type BlueprintArea,
type BlueprintStatus,
type SyncResult,
type Project,
type FlywheelStats,
type StalledProject,
type BuffStatus,
type TemplateResult,
type CommitStats,
type UserConventionsStatus,
type CommitHealthWithName,
ingestFullGitHistory,
getAllCommitHealth,
} from "../../lib/commands";
interface Props {
@ -253,7 +239,7 @@ function pickHandles(
// ── 主组件 ───────────────────────────────────────────────────────────────────
type SidePanel = { type: "module"; module: BlueprintModule } | { type: "next" } | { type: "remaining" } | { type: "governance" } | { type: "flywheel" } | null;
type SidePanel = { type: "module"; module: BlueprintModule } | { type: "next" } | { type: "remaining" } | { type: "governance" } | null;
export function BlueprintModal({ projectName, projectPath, projectId, onClose }: Props) {
const [sidePanel, setSidePanel] = useState<SidePanel>(null);
@ -429,16 +415,6 @@ export function BlueprintModal({ projectName, projectPath, projectId, onClose }:
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setSidePanel((p) => p?.type === "flywheel" ? null : { type: "flywheel" })}
className={`text-xs px-2.5 py-1 rounded-lg transition-colors ${
sidePanel?.type === "flywheel"
? "text-white bg-emerald-600"
: "text-emerald-600 bg-emerald-50 hover:bg-emerald-100"
}`}
>
</button>
<button
onClick={() => setSidePanel((p) => p?.type === "governance" ? null : { type: "governance" })}
className={`text-xs px-2.5 py-1 rounded-lg transition-colors ${
@ -514,17 +490,6 @@ export function BlueprintModal({ projectName, projectPath, projectId, onClose }:
{bpStatus.status === "synced" ? "🟢 规则" : "🟡 规则"}
</button>
)}
<button
onClick={() => setSidePanel((p) => p?.type === "flywheel" ? null : { type: "flywheel" })}
className={`text-[10px] px-1.5 py-0.5 rounded transition-colors shrink-0 ${
sidePanel?.type === "flywheel"
? "text-white bg-emerald-600"
: "text-emerald-600 bg-emerald-50 hover:bg-emerald-100"
}`}
title="梦核·记忆巩固 / AI文档归档"
>
AI文档
</button>
{/* 总进度条(固定在右侧) */}
<div className="flex-1" />
{pinnedModuleId && (
@ -639,10 +604,7 @@ export function BlueprintModal({ projectName, projectPath, projectId, onClose }:
projectName={projectName}
projectId={projectId}
onClose={() => setSidePanel(null)}
onOpenFlywheel={() => setSidePanel({ type: "flywheel" })}
/>
) : sidePanel.type === "flywheel" ? (
<FlywheelPanel onClose={() => setSidePanel(null)} />
) : (
<NextActionsPanel
modules={blueprint?.manifest.modules ?? []}
@ -1050,13 +1012,11 @@ function GovernancePanel({
projectName,
projectId,
onClose,
onOpenFlywheel,
}: {
projectPath: string;
projectName: string;
projectId?: string;
onClose: () => void;
onOpenFlywheel?: () => void;
}) {
const { data: bpStatus, isPending: statusLoading, refetch: refetchStatus } = useQuery<BlueprintStatus>({
queryKey: ["bp-status-governance", projectPath],
@ -1092,17 +1052,6 @@ function GovernancePanel({
const [promptError, setPromptError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
// 用户版 CONVENTIONS 编辑器
const { data: userConventions, refetch: refetchUserConventions } = useQuery<UserConventionsStatus>({
queryKey: ["user-conventions-status"],
queryFn: () => getUserConventionsStatus(),
});
const [conventionsInput, setConventionsInput] = useState("");
const [conventionsEditorOpen, setConventionsEditorOpen] = useState(false);
const [conventionsSaving, setConventionsSaving] = useState(false);
const [conventionsSaveResult, setConventionsSaveResult] = useState<string | null>(null);
const [conventionsSaveError, setConventionsSaveError] = useState<string | null>(null);
const handleApplyBuff = async () => {
if (!projectId) return;
setBuffLoading(true);
@ -1189,41 +1138,6 @@ function GovernancePanel({
});
};
const handleSaveConventions = async () => {
if (!conventionsInput.trim()) return;
setConventionsSaving(true);
setConventionsSaveError(null);
setConventionsSaveResult(null);
try {
const res = await saveUserConventions(conventionsInput);
setConventionsSaveResult(`已应用 v${res.version},同步了 ${res.synced_count} 个项目`);
setConventionsInput("");
setConventionsEditorOpen(false);
refetchUserConventions();
refetchStatus();
} catch (e) {
setConventionsSaveError(String(e));
} finally {
setConventionsSaving(false);
}
};
const handleResetConventions = async () => {
setConventionsSaving(true);
setConventionsSaveError(null);
setConventionsSaveResult(null);
try {
const res = await resetConventionsToBuiltin();
setConventionsSaveResult(`已重置为内置基线 v${res.version},同步了 ${res.synced_count} 个项目`);
refetchUserConventions();
refetchStatus();
} catch (e) {
setConventionsSaveError(String(e));
} finally {
setConventionsSaving(false);
}
};
const rulesOutdated = bpStatus?.status === "rules_outdated";
const currentVersion = bpStatus?.conventions_version ?? "—";
const masterVersion = bpStatus?.master_version ?? "—";
@ -1265,88 +1179,6 @@ function GovernancePanel({
)}
</div>
{/* 规则来源 badge */}
{userConventions && (
<div className="flex items-center justify-between bg-gray-50 rounded-lg px-3 py-2">
<span className="text-[11px] text-gray-500"></span>
{userConventions.source === "user" ? (
<span className="text-[11px] font-mono text-purple-600 bg-purple-50 px-2 py-0.5 rounded">
v{userConventions.version}
{userConventions.outdated_from_builtin && (
<span className="ml-1 text-yellow-600"> </span>
)}
</span>
) : (
<span className="text-[11px] font-mono text-gray-500 bg-white border border-gray-200 px-2 py-0.5 rounded">
线 v{userConventions.builtin_version}
</span>
)}
</div>
)}
{/* 应用梦核建议 */}
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
<div className="flex items-center justify-between">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider"></p>
<button
onClick={() => { setConventionsEditorOpen(v => !v); setConventionsSaveError(null); }}
className="text-[11px] text-blue-500 hover:text-blue-600"
>
{conventionsEditorOpen ? "收起" : "展开"}
</button>
</div>
{conventionsEditorOpen && (
<div className="space-y-2">
<p className="text-[11px] text-gray-500 leading-relaxed">
Opus CONVENTIONS
</p>
<textarea
value={conventionsInput}
onChange={e => setConventionsInput(e.target.value)}
placeholder="粘贴完整的 CONVENTIONS.md 内容(须包含 frontmatter version 字段)…"
rows={6}
className="w-full text-[11px] font-mono border border-gray-200 rounded p-2 resize-none focus:outline-none focus:border-blue-400"
/>
{conventionsSaveError && (
<p className="text-[11px] text-red-600 font-mono break-all">{conventionsSaveError}</p>
)}
<div className="flex gap-2">
<button
onClick={handleSaveConventions}
disabled={conventionsSaving || !conventionsInput.trim()}
className="flex-1 py-1.5 rounded-lg bg-blue-500 text-white text-xs hover:bg-blue-600 disabled:opacity-50 transition-colors"
>
{conventionsSaving ? "写入中…" : "确认应用"}
</button>
{userConventions?.source === "user" && (
<button
onClick={handleResetConventions}
disabled={conventionsSaving}
className="px-3 py-1.5 rounded-lg border border-gray-200 text-gray-600 text-xs hover:bg-gray-50 disabled:opacity-50 transition-colors"
>
线
</button>
)}
</div>
</div>
)}
{!conventionsEditorOpen && userConventions?.source === "user" && (
<div className="flex items-center justify-between">
<p className="text-[11px] text-gray-500">使</p>
<button
onClick={handleResetConventions}
disabled={conventionsSaving}
className="text-[11px] text-gray-400 hover:text-red-500 disabled:opacity-50 transition-colors"
>
线
</button>
</div>
)}
{conventionsSaveResult && (
<p className="text-[11px] text-green-600 bg-green-50 rounded px-2 py-1"> {conventionsSaveResult}</p>
)}
</div>
{/* 同步规则区域 */}
{rulesOutdated && !syncResult && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 space-y-2">
@ -1437,21 +1269,6 @@ function GovernancePanel({
)}
{/* 梦核分析入口 */}
{onOpenFlywheel && (
<div className="bg-emerald-50 rounded-lg border border-emerald-100 p-3 space-y-2">
<p className="text-[10px] font-semibold text-emerald-600 uppercase tracking-wider"> · </p>
<p className="text-[11px] text-emerald-700 leading-relaxed">
Claude Opus /
</p>
<button
onClick={onOpenFlywheel}
className="w-full py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700 transition-colors"
>
</button>
</div>
)}
{/* ── 蓝图 Buff ─────────────────────────────────────── */}
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
<div className="flex items-center justify-between">
@ -1636,417 +1453,3 @@ function RemainingTasksPanel({
);
}
// ── 飞轮健康面板 ────────────────────────────────────────────────────────────
function StalledProjectRow({ p, projectsMap, onArchive }: {
p: StalledProject;
projectsMap: Record<string, string>; // name → path
onArchive?: (path: string, name: string) => void;
}) {
const path = projectsMap[p.name];
return (
<div className="flex items-center justify-between gap-2">
<div className="min-w-0">
<p className="text-[11px] text-amber-700 truncate">· {p.name}</p>
<p className="text-[10px] text-amber-400">: {p.last_active}</p>
</div>
{onArchive && path && (
<button
onClick={() => onArchive(path, p.name)}
className="text-[10px] text-amber-600 bg-amber-100 hover:bg-amber-200 px-1.5 py-0.5 rounded shrink-0 transition-colors"
>
</button>
)}
</div>
);
}
function FlywheelPanel({ onClose }: { onClose: () => void }) {
const { data: projects } = useQuery<Project[]>({
queryKey: ["projects-for-flywheel"],
queryFn: () => getProjects(),
});
const { projectPaths, projectsMap, projectsWithPath } = useMemo(() => {
const paths = (projects ?? [])
.map((p) => p.win_path || p.wsl_path || "")
.filter(Boolean);
const map: Record<string, string> = {};
const withPath: Array<{ id: string; name: string; path: string }> = [];
for (const p of (projects ?? [])) {
const path = p.win_path || p.wsl_path || "";
if (!path) continue;
withPath.push({ id: p.id, name: p.name, path });
map[p.name] = path;
const dirName = path.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? "";
if (dirName && !map[dirName]) map[dirName] = path;
}
return { projectPaths: paths, projectsMap: map, projectsWithPath: withPath };
}, [projects]);
const { data: commitHealth, refetch: refetchHealth } = useQuery<CommitHealthWithName[]>({
queryKey: ["commit-health-all"],
queryFn: () => getAllCommitHealth(),
});
const [ingesting, setIngesting] = useState(false);
const [ingestSummary, setIngestSummary] = useState<string | null>(null);
const { data: stats, isLoading } = useQuery<FlywheelStats>({
queryKey: ["flywheel-stats", projectPaths],
queryFn: () => getFlywheelStats(projectPaths),
enabled: projectPaths.length > 0,
});
const [muhePrompt, setMuhePrompt] = useState("");
const [generating, setGenerating] = useState(false);
const [copied, setCopied] = useState(false);
const [archiveMsg, setArchiveMsg] = useState<string | null>(null);
const [archiving, setArchiving] = useState<string | null>(null);
const [pendingArchive, setPendingArchive] = useState<{ path: string; name: string } | null>(null);
const handleIngestAll = async () => {
setIngesting(true);
setIngestSummary(null);
let totalImported = 0;
let totalSkipped = 0;
for (const { id, path } of projectsWithPath) {
try {
const r = await ingestFullGitHistory(id, path);
totalImported += r.imported;
totalSkipped += r.skipped;
} catch {
// 单个项目失败不中断其他项目
}
}
await refetchHealth();
setIngestSummary(`新导入 ${totalImported} 条,跳过 ${totalSkipped}`);
setIngesting(false);
};
const handleGenerateMuhe = async () => {
if (!stats) return;
setGenerating(true);
try {
const result = await generateMuhePrompt(stats);
setMuhePrompt(result);
} finally {
setGenerating(false);
}
};
const handleCopy = () => {
navigator.clipboard.writeText(muhePrompt).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
};
const handleArchiveConfirm = async () => {
if (!pendingArchive) return;
const { path, name } = pendingArchive;
setPendingArchive(null);
setArchiving(name);
setArchiveMsg(null);
try {
const result = await archiveProjectDocs(path);
setArchiveMsg(`✓ 已归档 ${result.archived_files.length} 个文件 → ${result.archive_dir}`);
} catch (e) {
setArchiveMsg(`归档失败: ${String(e)}`);
} finally {
setArchiving(null);
}
};
const fmtPct = (v: number) => `${(v * 100).toFixed(1)}%`;
// Fix 2去重——90天休眠的项目不在60天停转里重复展示
const stalledOnlyProjects = (stats?.stalled_projects ?? []).filter(
(p) => !(stats?.ai_doc_stale_projects ?? []).some((q) => q.name === p.name)
);
return (
<div className="p-4 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-semibold text-gray-800"></h3>
{stats && (
<p className="text-[10px] text-gray-400 mt-0.5">
{stats.projects_with_data}/{stats.total_projects_scanned}
</p>
)}
</div>
<button onClick={onClose} className="text-gray-300 hover:text-gray-500 text-lg">×</button>
</div>
{isLoading ? (
<p className="text-xs text-gray-400 text-center py-6"></p>
) : !stats || stats.projects_with_data === 0 ? (
<div className="text-center py-6 space-y-2">
<p className="text-xs text-gray-400"></p>
<p className="text-[11px] text-gray-300"></p>
</div>
) : (
<>
{/* CONVENTIONS 版本队列对比 */}
{stats.cohorts.length > 0 && (
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider"></p>
<div className="space-y-3">
{stats.cohorts.map((c) => (
<div key={c.conventions_version} className="space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-xs font-mono text-gray-600">v{c.conventions_version}</span>
<span className="text-[10px] text-gray-400">{c.project_count} </span>
</div>
{/* Fix 4标签改为"任务完成率",字段改为 completion_rate */}
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5">
<MetricRow label="可派发率" value={fmtPct(c.dispatchable_rate)} good={c.dispatchable_rate > 0.3} />
<MetricRow label="任务完成率" value={fmtPct(c.completion_rate)} good={c.completion_rate > 0.5} />
<MetricRow label="飞轮持续率" value={fmtPct(c.continuity_rate)} good={c.continuity_rate > 0.6} />
<MetricRow label="阻塞密度" value={fmtPct(c.blocked_density)} good={c.blocked_density < 0.1} invert />
</div>
</div>
))}
</div>
</div>
)}
{/* TOP blocked reasons */}
{stats.top_blocked_reasons.length > 0 && (
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">TOP </p>
<ul className="space-y-1">
{stats.top_blocked_reasons.map((r, i) => (
<li key={i} className="flex items-start gap-2">
<span className="text-[10px] text-gray-400 shrink-0 mt-0.5">#{i + 1}</span>
<span className="text-[11px] text-red-600 flex-1">{r.reason}</span>
<span className="text-[10px] text-gray-400 shrink-0">×{r.count}</span>
</li>
))}
</ul>
</div>
)}
{/* Fix 2仅展示"停转但未到90天"的项目 */}
{stalledOnlyProjects.length > 0 && (
<div className="bg-amber-50 rounded-lg border border-amber-100 p-3 space-y-1.5">
<p className="text-[10px] font-semibold text-amber-500 uppercase tracking-wider">
6090
</p>
{stalledOnlyProjects.map((p, i) => (
<StalledProjectRow key={i} p={p} projectsMap={projectsMap} />
))}
</div>
)}
{/* AI文档休眠项目≥90天 */}
{stats.ai_doc_stale_projects.length > 0 && (
<div className="bg-red-50 rounded-lg border border-red-100 p-3 space-y-1.5">
<p className="text-[10px] font-semibold text-red-500 uppercase tracking-wider">
AI文档休眠90+
</p>
<p className="text-[10px] text-red-400 mb-1">
.blueprint/modules/ Claude
</p>
{/* Fix 1归档确认弹窗 */}
{pendingArchive ? (
<div className="bg-red-100 border border-red-200 rounded p-2.5 space-y-2">
<p className="text-[11px] text-red-700 font-semibold">
{pendingArchive.name}
</p>
<p className="text-[10px] text-red-500 leading-relaxed">
.blueprint/modules/*.md .blueprint/archive/
</p>
<div className="flex gap-2">
<button
onClick={handleArchiveConfirm}
disabled={!!archiving}
className="flex-1 py-1 rounded bg-red-600 text-white text-[11px] hover:bg-red-700 disabled:opacity-50 transition-colors"
>
</button>
<button
onClick={() => setPendingArchive(null)}
className="flex-1 py-1 rounded border border-red-200 text-red-500 text-[11px] hover:bg-red-50 transition-colors"
>
</button>
</div>
</div>
) : (
stats.ai_doc_stale_projects.map((p, i) => (
<StalledProjectRow
key={i}
p={p}
projectsMap={projectsMap}
onArchive={archiving ? undefined : (path, name) => setPendingArchive({ path, name })}
/>
))
)}
{archiving && (
<p className="text-[10px] text-red-400"> {archiving} </p>
)}
{archiveMsg && (
<p className={`text-[10px] break-all ${archiveMsg.startsWith("✓") ? "text-green-600" : "text-red-600"}`}>
{archiveMsg}
</p>
)}
</div>
)}
{/* 迭代跳变项目 */}
{(stats.iteration_jumps ?? []).length > 0 && (
<div className="bg-purple-50 rounded-lg border border-purple-100 p-3 space-y-1.5">
<p className="text-[10px] font-semibold text-purple-500 uppercase tracking-wider">
</p>
{(stats.iteration_jumps ?? []).map((j, i) => (
<div key={i} className="flex items-center gap-2 text-[11px]">
<span className="text-gray-600 truncate flex-1">{j.project}</span>
<span className="text-purple-400 shrink-0"> {j.from_iteration}{j.to_iteration}</span>
<span className={`shrink-0 font-mono ${j.rate_after < j.rate_before - 0.3 ? "text-amber-500 font-semibold" : "text-gray-400"}`}>
{(j.rate_before * 100).toFixed(0)}%{(j.rate_after * 100).toFixed(0)}%
{j.rate_after < j.rate_before - 0.3 && " ⚠️"}
</span>
</div>
))}
</div>
)}
{/* 停滞模块 */}
{(stats.stalled_modules ?? []).length > 0 && (
<div className="bg-gray-50 rounded-lg border border-gray-100 p-3 space-y-1.5">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">
in_progress
</p>
<ul className="space-y-0.5">
{(stats.stalled_modules ?? []).map((m, i) => (
<li key={i} className="text-[11px] text-gray-500 font-mono">{m}</li>
))}
</ul>
</div>
)}
{/* Git 执行质量 */}
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
<div className="flex items-center justify-between">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Git </p>
<button
onClick={handleIngestAll}
disabled={ingesting || projectsWithPath.length === 0}
className="text-[10px] px-2 py-0.5 rounded bg-indigo-50 text-indigo-600 border border-indigo-200 hover:bg-indigo-100 disabled:opacity-40 transition-colors"
>
{ingesting ? "导入中…" : "全量导入"}
</button>
</div>
{ingestSummary && (
<p className="text-[10px] text-green-600">{ingestSummary}</p>
)}
{commitHealth && commitHealth.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-[10px]">
<thead>
<tr className="text-gray-400 border-b border-gray-100">
<th className="text-left pb-1 font-medium"></th>
<th className="text-right pb-1 font-medium">commits</th>
<th className="text-right pb-1 font-medium"></th>
<th className="text-right pb-1 font-medium"></th>
<th className="text-left pb-1 font-medium pl-2">TOP scope</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{commitHealth.map((h) => {
return (
<tr key={h.project_id} className="hover:bg-gray-50">
<td className="py-1 text-gray-700 truncate max-w-[80px]" title={h.project_name}>
{h.project_name}
</td>
<td className="py-1 text-right font-mono text-gray-500">{h.total_commits}</td>
<td className={`py-1 text-right font-mono font-semibold ${h.conventional_rate >= 0.8 ? "text-green-600" : h.conventional_rate >= 0.5 ? "text-amber-500" : "text-red-500"}`}>
{Math.round(h.conventional_rate * 100)}%
</td>
<td className={`py-1 text-right font-mono ${h.rework_rate > 0.2 ? "text-red-500" : h.rework_rate > 0.1 ? "text-amber-500" : "text-gray-500"}`}>
{Math.round(h.rework_rate * 100)}%
</td>
<td className="py-1 pl-2 text-gray-400 truncate max-w-[100px]">
{h.top_scopes.slice(0, 2).map((s) => `${s.scope}(${s.commit_count})`).join(" ")}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : (
<p className="text-[10px] text-gray-400 italic">
"全量导入" git
</p>
)}
</div>
{/* 梦核分析 */}
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider"></p>
<p className="text-[11px] text-gray-500 leading-relaxed">
Claude Opus /
</p>
{!muhePrompt ? (
<button
onClick={handleGenerateMuhe}
disabled={generating}
className="w-full py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700 disabled:opacity-50 transition-colors"
>
{generating ? "生成中…" : "生成梦核提示词"}
</button>
) : (
<div className="space-y-2">
<textarea
readOnly
value={muhePrompt}
rows={10}
className="w-full font-mono text-[11px] text-gray-700 bg-gray-50 border border-gray-200 rounded-lg p-2.5 resize-none outline-none"
onClick={(e) => (e.target as HTMLTextAreaElement).select()}
/>
<button
onClick={handleCopy}
className="w-full py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700 transition-colors"
>
{copied ? "已复制 ✓" : "复制提示词"}
</button>
<button
onClick={() => setMuhePrompt("")}
className="w-full py-1 rounded-lg text-xs text-gray-400 hover:text-gray-600 transition-colors"
>
</button>
{/* Fix 3说清楚实际流程——梦核不自动写入需手动更新源码再发版 */}
<p className="text-[10px] text-gray-400 text-center leading-relaxed">
Claude Opus CONVENTIONS.md
</p>
</div>
)}
</div>
</>
)}
</div>
);
}
function MetricRow({ label, value, good, invert = false }: {
label: string;
value: string;
good: boolean;
invert?: boolean;
}) {
const isGood = invert ? !good : good;
return (
<div className="flex items-center justify-between">
<span className="text-[10px] text-gray-400">{label}</span>
<span className={`text-[10px] font-mono font-semibold ${isGood ? "text-green-600" : "text-yellow-600"}`}>
{value}
</span>
</div>
);
}

View File

@ -1,4 +1,6 @@
// Gitea 集成治理区块:实例管理 + 当前项目仓库绑定 + webhook 注册
// Gitea 集成拆两半ui-restructure N4
// - GiteaInstancesSection实例管理全局配置→ 挂设置页
// - GiteaPanel当前项目仓库绑定 + webhook项目级→ 留项目蓝图治理面板
// 控制面在 gitea.rs观测面webhook 接收)在 mcp_server.rs
import { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
@ -18,11 +20,6 @@ import {
} from "../../lib/commands";
import { useUIStore } from "../../store/ui";
interface Props {
projectId: string;
projectName: string;
}
type BindMode = "create" | "migrate" | "link";
const CALLBACK_BASE_KEY = "gitea_callback_base";
@ -32,6 +29,121 @@ const inputCls =
const btnCls =
"px-2 py-1 rounded text-[11px] border border-gray-200 text-gray-500 hover:bg-gray-50 transition-colors";
// ── 实例管理(全局,挂设置页)────────────────────────────────────────────────
export function GiteaInstancesSection() {
const qc = useQueryClient();
const showToast = useUIStore((s) => s.showToast);
const { data: instances = [] } = useQuery({
queryKey: ["gitea-instances"],
queryFn: giteaListInstances,
});
const [showAdd, setShowAdd] = useState(false);
const [instName, setInstName] = useState("");
const [instUrl, setInstUrl] = useState("");
const [instToken, setInstToken] = useState("");
const [testing, setTesting] = useState<string | null>(null);
const handleAdd = async () => {
if (!instName.trim() || !instUrl.trim() || !instToken.trim()) return;
try {
await giteaSaveInstance({
name: instName.trim(),
url: instUrl.trim(),
access_token: instToken.trim(),
is_default: instances.length === 0,
});
qc.invalidateQueries({ queryKey: ["gitea-instances"] });
setInstName(""); setInstUrl(""); setInstToken("");
setShowAdd(false);
showToast("Gitea 实例已添加 ✓");
} catch (e) {
showToast(`${e}`);
}
};
const handleTest = async (inst: GiteaInstance) => {
setTesting(inst.id);
try {
const user = await giteaTestConnection(inst.id);
showToast(`✓ 连接成功,账户: ${user.login}`);
} catch (e) {
showToast(`${e}`);
} finally {
setTesting(null);
}
};
const handleDelete = async (inst: GiteaInstance) => {
if (!confirm(`删除实例「${inst.name}」?其下所有项目绑定会一并删除。`)) return;
try {
await giteaDeleteInstance(inst.id);
qc.invalidateQueries({ queryKey: ["gitea-instances"] });
qc.invalidateQueries({ queryKey: ["gitea-linked-repos"] });
showToast("已删除");
} catch (e) {
showToast(`${e}`);
}
};
return (
<section className="bg-white rounded-xl border border-gray-200 shadow-sm px-6 py-5">
<div className="flex items-center justify-between mb-4">
<h2 className="text-sm font-semibold text-gray-700">🪁 Gitea </h2>
{!showAdd && (
<button onClick={() => setShowAdd(true)} className="text-xs text-blue-500 hover:text-blue-600">
+
</button>
)}
</div>
<div className="space-y-2">
{instances.length === 0 && !showAdd && (
<p className="text-xs text-gray-400"> Gitea </p>
)}
{instances.map((inst) => (
<div key={inst.id} className="flex items-center gap-2 text-xs">
<span className="font-medium text-gray-700 shrink-0">{inst.name}</span>
{inst.is_default && (
<span className="text-[10px] px-1 rounded bg-blue-50 text-blue-500 shrink-0"></span>
)}
<span className="text-gray-400 font-mono truncate flex-1">{inst.url}</span>
<button onClick={() => handleTest(inst)} disabled={testing === inst.id} className={btnCls}>
{testing === inst.id ? "…" : "测试连接"}
</button>
<button
onClick={() => handleDelete(inst)}
className="px-2 py-1 rounded text-[11px] border border-gray-200 text-red-400 hover:bg-red-50"
>
</button>
</div>
))}
{showAdd && (
<div className="space-y-2 border-t border-gray-100 pt-3">
<input className={inputCls} placeholder="实例名称(如:家里 NAS" value={instName} onChange={(e) => setInstName(e.target.value)} />
<input className={inputCls} placeholder="https://gitea.example.com" value={instUrl} onChange={(e) => setInstUrl(e.target.value)} />
<input className={inputCls} type="password" placeholder="Access Token用户设置 → 应用 → 生成令牌)" value={instToken} onChange={(e) => setInstToken(e.target.value)} />
<div className="flex gap-1.5">
<button onClick={handleAdd} className="px-3 py-1.5 rounded text-xs bg-blue-600 text-white hover:bg-blue-700"></button>
<button onClick={() => setShowAdd(false)} className="px-3 py-1.5 rounded text-xs border border-gray-200 text-gray-500 hover:bg-gray-50"></button>
</div>
</div>
)}
</div>
</section>
);
}
// ── 项目级绑定 + webhook留项目蓝图治理面板────────────────────────────────
interface Props {
projectId: string;
projectName: string;
}
export function GiteaPanel({ projectId, projectName }: Props) {
const qc = useQueryClient();
const showToast = useUIStore((s) => s.showToast);
@ -56,56 +168,7 @@ export function GiteaPanel({ projectId, projectName }: Props) {
qc.invalidateQueries({ queryKey: ["gitea-linked-repos"] });
};
// ── 实例管理 ──
const [showAddInstance, setShowAddInstance] = useState(false);
const [instName, setInstName] = useState("");
const [instUrl, setInstUrl] = useState("");
const [instToken, setInstToken] = useState("");
const [testing, setTesting] = useState<string | null>(null);
const handleAddInstance = async () => {
if (!instName.trim() || !instUrl.trim() || !instToken.trim()) return;
try {
await giteaSaveInstance({
name: instName.trim(),
url: instUrl.trim(),
access_token: instToken.trim(),
is_default: instances.length === 0,
});
qc.invalidateQueries({ queryKey: ["gitea-instances"] });
setInstName(""); setInstUrl(""); setInstToken("");
setShowAddInstance(false);
showToast("Gitea 实例已添加 ✓");
} catch (e) {
showToast(`${e}`);
}
};
const handleTest = async (inst: GiteaInstance) => {
setTesting(inst.id);
try {
const user = await giteaTestConnection(inst.id);
showToast(`✓ 连接成功,账户: ${user.login}`);
} catch (e) {
showToast(`${e}`);
} finally {
setTesting(null);
}
};
const handleDeleteInstance = async (inst: GiteaInstance) => {
if (!confirm(`删除实例「${inst.name}」?其下所有项目绑定会一并删除。`)) return;
try {
await giteaDeleteInstance(inst.id);
qc.invalidateQueries({ queryKey: ["gitea-instances"] });
refresh();
showToast("已删除");
} catch (e) {
showToast(`${e}`);
}
};
// ── 当前项目绑定 ──
// ── 绑定操作 ──
const [bindMode, setBindMode] = useState<BindMode | null>(null);
const [bindInstanceId, setBindInstanceId] = useState("");
const [repoName, setRepoName] = useState("");
@ -184,51 +247,11 @@ export function GiteaPanel({ projectId, projectName }: Props) {
return (
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-3">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">
🪁 Gitea
🪁 Gitea
</p>
{/* ── 实例列表 ── */}
<div className="space-y-1.5">
{instances.length === 0 && !showAddInstance && (
<p className="text-[11px] text-gray-400"> Gitea </p>
)}
{instances.map((inst) => (
<div key={inst.id} className="flex items-center gap-2 text-[11px]">
<span className="font-medium text-gray-700 shrink-0">{inst.name}</span>
{inst.is_default && (
<span className="text-[10px] px-1 rounded bg-blue-50 text-blue-500 shrink-0"></span>
)}
<span className="text-gray-400 font-mono truncate flex-1">{inst.url}</span>
<button onClick={() => handleTest(inst)} disabled={testing === inst.id} className={btnCls}>
{testing === inst.id ? "…" : "测试"}
</button>
<button
onClick={() => handleDeleteInstance(inst)}
className="px-2 py-1 rounded text-[11px] border border-gray-200 text-red-400 hover:bg-red-50"
>
</button>
</div>
))}
{showAddInstance ? (
<div className="space-y-1.5 border-t border-gray-50 pt-2">
<input className={inputCls} placeholder="实例名称(如:家里 NAS" value={instName} onChange={(e) => setInstName(e.target.value)} />
<input className={inputCls} placeholder="https://gitea.example.com" value={instUrl} onChange={(e) => setInstUrl(e.target.value)} />
<input className={inputCls} type="password" placeholder="Access Token用户设置 → 应用 → 生成令牌)" value={instToken} onChange={(e) => setInstToken(e.target.value)} />
<div className="flex gap-1.5">
<button onClick={handleAddInstance} className="px-2.5 py-1 rounded text-[11px] bg-blue-600 text-white hover:bg-blue-700"></button>
<button onClick={() => setShowAddInstance(false)} className={btnCls}></button>
</div>
</div>
) : (
<button onClick={() => setShowAddInstance(true)} className="text-[11px] text-blue-500 hover:text-blue-600">
+
</button>
)}
</div>
{/* ── 当前项目绑定 ── */}
<div className="border-t border-gray-50 pt-2 space-y-2">
<div className="space-y-2">
{link ? (
<>
<div className="flex items-center gap-2 text-[11px]">
@ -280,6 +303,10 @@ export function GiteaPanel({ projectId, projectName }: Props) {
</p>
</div>
</>
) : instances.length === 0 ? (
<p className="text-[11px] text-gray-400">
Gitea Gitea
</p>
) : (
<>
<div className="flex items-center gap-1.5">
@ -328,9 +355,6 @@ export function GiteaPanel({ projectId, projectName }: Props) {
>
{binding ? "执行中…" : "确认"}
</button>
{instances.length === 0 && (
<p className="text-[10px] text-amber-500"> Gitea </p>
)}
</div>
)}
</>

View File

@ -9,10 +9,6 @@ import {
} from "../../lib/commands";
import { useUIStore } from "../../store/ui";
interface Props {
onClose: () => void;
}
// ── 部署信息编辑表单 ──────────────────────────────────────────────────────────
function DeployForm({
@ -118,8 +114,9 @@ function DeployForm({
}
// ── 已注册仓库列表 ────────────────────────────────────────────────────────────
// 同时被 ManagePage「仓库」tab 复用ui-restructure N5
function RegistryTab() {
export function RegistryTab() {
const showToast = useUIStore((s) => s.showToast);
const qc = useQueryClient();
const [expandedId, setExpandedId] = useState<string | null>(null);
@ -149,7 +146,7 @@ function RegistryTab() {
return (
<div className="flex-1 flex flex-col items-center justify-center text-gray-400 py-16 gap-2">
<p className="text-2xl">📦</p>
<p className="text-sm"> / </p>
<p className="text-sm"></p>
</div>
);
}
@ -214,22 +211,3 @@ function RegistryTab() {
</div>
);
}
// ── 主模态框 ──────────────────────────────────────────────────────────────────
export function RepoRegistryModal({ onClose }: Props) {
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-2xl w-[700px] max-h-[85vh] flex flex-col border border-gray-200">
<div className="px-5 py-4 border-b border-gray-100 flex items-center justify-between">
<h2 className="text-base font-semibold text-gray-800"></h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-xl leading-none">×</button>
</div>
<div className="flex flex-col flex-1 min-h-0">
<RegistryTab />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,423 @@
// 飞轮健康面板(跨项目):版本队列对比 / 阻塞原因 / 停转与休眠归档 / Git 执行质量 / 梦核分析
// 2026-07-02 ui-restructure N3从 BlueprintModal 侧板迁出为洞察页一级内容(跨项目内容不属于单项目弹窗)
import { useState, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import {
getProjects,
getFlywheelStats,
generateMuhePrompt,
archiveProjectDocs,
ingestFullGitHistory,
getAllCommitHealth,
type Project,
type FlywheelStats,
type StalledProject,
type CommitHealthWithName,
} from "../../lib/commands";
function StalledProjectRow({ p, projectsMap, onArchive }: {
p: StalledProject;
projectsMap: Record<string, string>; // name → path
onArchive?: (path: string, name: string) => void;
}) {
const path = projectsMap[p.name];
return (
<div className="flex items-center justify-between gap-2">
<div className="min-w-0">
<p className="text-[11px] text-amber-700 truncate">· {p.name}</p>
<p className="text-[10px] text-amber-400">: {p.last_active}</p>
</div>
{onArchive && path && (
<button
onClick={() => onArchive(path, p.name)}
className="text-[10px] text-amber-600 bg-amber-100 hover:bg-amber-200 px-1.5 py-0.5 rounded shrink-0 transition-colors"
>
</button>
)}
</div>
);
}
function MetricRow({ label, value, good, invert = false }: {
label: string;
value: string;
good: boolean;
invert?: boolean;
}) {
const isGood = invert ? !good : good;
return (
<div className="flex items-center justify-between">
<span className="text-[10px] text-gray-400">{label}</span>
<span className={`text-[10px] font-mono font-semibold ${isGood ? "text-green-600" : "text-yellow-600"}`}>
{value}
</span>
</div>
);
}
export function FlywheelPanel() {
const { data: projects } = useQuery<Project[]>({
queryKey: ["projects-for-flywheel"],
queryFn: () => getProjects(),
});
const { projectPaths, projectsMap, projectsWithPath } = useMemo(() => {
const paths = (projects ?? [])
.map((p) => p.win_path || p.wsl_path || "")
.filter(Boolean);
const map: Record<string, string> = {};
const withPath: Array<{ id: string; name: string; path: string }> = [];
for (const p of (projects ?? [])) {
const path = p.win_path || p.wsl_path || "";
if (!path) continue;
withPath.push({ id: p.id, name: p.name, path });
map[p.name] = path;
const dirName = path.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? "";
if (dirName && !map[dirName]) map[dirName] = path;
}
return { projectPaths: paths, projectsMap: map, projectsWithPath: withPath };
}, [projects]);
const { data: commitHealth, refetch: refetchHealth } = useQuery<CommitHealthWithName[]>({
queryKey: ["commit-health-all"],
queryFn: () => getAllCommitHealth(),
});
const [ingesting, setIngesting] = useState(false);
const [ingestSummary, setIngestSummary] = useState<string | null>(null);
const { data: stats, isLoading } = useQuery<FlywheelStats>({
queryKey: ["flywheel-stats", projectPaths],
queryFn: () => getFlywheelStats(projectPaths),
enabled: projectPaths.length > 0,
});
const [muhePrompt, setMuhePrompt] = useState("");
const [generating, setGenerating] = useState(false);
const [copied, setCopied] = useState(false);
const [archiveMsg, setArchiveMsg] = useState<string | null>(null);
const [archiving, setArchiving] = useState<string | null>(null);
const [pendingArchive, setPendingArchive] = useState<{ path: string; name: string } | null>(null);
const handleIngestAll = async () => {
setIngesting(true);
setIngestSummary(null);
let totalImported = 0;
let totalSkipped = 0;
for (const { id, path } of projectsWithPath) {
try {
const r = await ingestFullGitHistory(id, path);
totalImported += r.imported;
totalSkipped += r.skipped;
} catch {
// 单个项目失败不中断其他项目
}
}
await refetchHealth();
setIngestSummary(`新导入 ${totalImported} 条,跳过 ${totalSkipped}`);
setIngesting(false);
};
const handleGenerateMuhe = async () => {
if (!stats) return;
setGenerating(true);
try {
const result = await generateMuhePrompt(stats);
setMuhePrompt(result);
} finally {
setGenerating(false);
}
};
const handleCopy = () => {
navigator.clipboard.writeText(muhePrompt).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
};
const handleArchiveConfirm = async () => {
if (!pendingArchive) return;
const { path, name } = pendingArchive;
setPendingArchive(null);
setArchiving(name);
setArchiveMsg(null);
try {
const result = await archiveProjectDocs(path);
setArchiveMsg(`✓ 已归档 ${result.archived_files.length} 个文件 → ${result.archive_dir}`);
} catch (e) {
setArchiveMsg(`归档失败: ${String(e)}`);
} finally {
setArchiving(null);
}
};
const fmtPct = (v: number) => `${(v * 100).toFixed(1)}%`;
// 去重——90天休眠的项目不在60天停转里重复展示
const stalledOnlyProjects = (stats?.stalled_projects ?? []).filter(
(p) => !(stats?.ai_doc_stale_projects ?? []).some((q) => q.name === p.name)
);
return (
<div className="p-4 space-y-4">
<div>
<h3 className="text-sm font-semibold text-gray-800"></h3>
{stats && (
<p className="text-[10px] text-gray-400 mt-0.5">
{stats.projects_with_data}/{stats.total_projects_scanned}
</p>
)}
</div>
{isLoading ? (
<p className="text-xs text-gray-400 text-center py-6"></p>
) : !stats || stats.projects_with_data === 0 ? (
<div className="text-center py-6 space-y-2">
<p className="text-xs text-gray-400"></p>
<p className="text-[11px] text-gray-300"></p>
</div>
) : (
<>
{/* CONVENTIONS 版本队列对比 */}
{stats.cohorts.length > 0 && (
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider"></p>
<div className="space-y-3">
{stats.cohorts.map((c) => (
<div key={c.conventions_version} className="space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-xs font-mono text-gray-600">v{c.conventions_version}</span>
<span className="text-[10px] text-gray-400">{c.project_count} </span>
</div>
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5">
<MetricRow label="可派发率" value={fmtPct(c.dispatchable_rate)} good={c.dispatchable_rate > 0.3} />
<MetricRow label="任务完成率" value={fmtPct(c.completion_rate)} good={c.completion_rate > 0.5} />
<MetricRow label="飞轮持续率" value={fmtPct(c.continuity_rate)} good={c.continuity_rate > 0.6} />
<MetricRow label="阻塞密度" value={fmtPct(c.blocked_density)} good={c.blocked_density < 0.1} invert />
</div>
</div>
))}
</div>
</div>
)}
{/* TOP blocked reasons */}
{stats.top_blocked_reasons.length > 0 && (
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">TOP </p>
<ul className="space-y-1">
{stats.top_blocked_reasons.map((r, i) => (
<li key={i} className="flex items-start gap-2">
<span className="text-[10px] text-gray-400 shrink-0 mt-0.5">#{i + 1}</span>
<span className="text-[11px] text-red-600 flex-1">{r.reason}</span>
<span className="text-[10px] text-gray-400 shrink-0">×{r.count}</span>
</li>
))}
</ul>
</div>
)}
{/* 仅展示"停转但未到90天"的项目 */}
{stalledOnlyProjects.length > 0 && (
<div className="bg-amber-50 rounded-lg border border-amber-100 p-3 space-y-1.5">
<p className="text-[10px] font-semibold text-amber-500 uppercase tracking-wider">
6090
</p>
{stalledOnlyProjects.map((p, i) => (
<StalledProjectRow key={i} p={p} projectsMap={projectsMap} />
))}
</div>
)}
{/* AI文档休眠项目≥90天 */}
{stats.ai_doc_stale_projects.length > 0 && (
<div className="bg-red-50 rounded-lg border border-red-100 p-3 space-y-1.5">
<p className="text-[10px] font-semibold text-red-500 uppercase tracking-wider">
AI文档休眠90+
</p>
<p className="text-[10px] text-red-400 mb-1">
.blueprint/modules/ Claude
</p>
{pendingArchive ? (
<div className="bg-red-100 border border-red-200 rounded p-2.5 space-y-2">
<p className="text-[11px] text-red-700 font-semibold">
{pendingArchive.name}
</p>
<p className="text-[10px] text-red-500 leading-relaxed">
.blueprint/modules/*.md .blueprint/archive/
</p>
<div className="flex gap-2">
<button
onClick={handleArchiveConfirm}
disabled={!!archiving}
className="flex-1 py-1 rounded bg-red-600 text-white text-[11px] hover:bg-red-700 disabled:opacity-50 transition-colors"
>
</button>
<button
onClick={() => setPendingArchive(null)}
className="flex-1 py-1 rounded border border-red-200 text-red-500 text-[11px] hover:bg-red-50 transition-colors"
>
</button>
</div>
</div>
) : (
stats.ai_doc_stale_projects.map((p, i) => (
<StalledProjectRow
key={i}
p={p}
projectsMap={projectsMap}
onArchive={archiving ? undefined : (path, name) => setPendingArchive({ path, name })}
/>
))
)}
{archiving && (
<p className="text-[10px] text-red-400"> {archiving} </p>
)}
{archiveMsg && (
<p className={`text-[10px] break-all ${archiveMsg.startsWith("✓") ? "text-green-600" : "text-red-600"}`}>
{archiveMsg}
</p>
)}
</div>
)}
{/* 迭代跳变项目 */}
{(stats.iteration_jumps ?? []).length > 0 && (
<div className="bg-purple-50 rounded-lg border border-purple-100 p-3 space-y-1.5">
<p className="text-[10px] font-semibold text-purple-500 uppercase tracking-wider">
</p>
{(stats.iteration_jumps ?? []).map((j, i) => (
<div key={i} className="flex items-center gap-2 text-[11px]">
<span className="text-gray-600 truncate flex-1">{j.project}</span>
<span className="text-purple-400 shrink-0"> {j.from_iteration}{j.to_iteration}</span>
<span className={`shrink-0 font-mono ${j.rate_after < j.rate_before - 0.3 ? "text-amber-500 font-semibold" : "text-gray-400"}`}>
{(j.rate_before * 100).toFixed(0)}%{(j.rate_after * 100).toFixed(0)}%
{j.rate_after < j.rate_before - 0.3 && " ⚠️"}
</span>
</div>
))}
</div>
)}
{/* 停滞模块 */}
{(stats.stalled_modules ?? []).length > 0 && (
<div className="bg-gray-50 rounded-lg border border-gray-100 p-3 space-y-1.5">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">
in_progress
</p>
<ul className="space-y-0.5">
{(stats.stalled_modules ?? []).map((m, i) => (
<li key={i} className="text-[11px] text-gray-500 font-mono">{m}</li>
))}
</ul>
</div>
)}
{/* Git 执行质量 */}
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
<div className="flex items-center justify-between">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Git </p>
<button
onClick={handleIngestAll}
disabled={ingesting || projectsWithPath.length === 0}
className="text-[10px] px-2 py-0.5 rounded bg-indigo-50 text-indigo-600 border border-indigo-200 hover:bg-indigo-100 disabled:opacity-40 transition-colors"
>
{ingesting ? "导入中…" : "全量导入"}
</button>
</div>
{ingestSummary && (
<p className="text-[10px] text-green-600">{ingestSummary}</p>
)}
{commitHealth && commitHealth.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-[10px]">
<thead>
<tr className="text-gray-400 border-b border-gray-100">
<th className="text-left pb-1 font-medium"></th>
<th className="text-right pb-1 font-medium">commits</th>
<th className="text-right pb-1 font-medium"></th>
<th className="text-right pb-1 font-medium"></th>
<th className="text-left pb-1 font-medium pl-2">TOP scope</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{commitHealth.map((h) => {
return (
<tr key={h.project_id} className="hover:bg-gray-50">
<td className="py-1 text-gray-700 truncate max-w-[80px]" title={h.project_name}>
{h.project_name}
</td>
<td className="py-1 text-right font-mono text-gray-500">{h.total_commits}</td>
<td className={`py-1 text-right font-mono font-semibold ${h.conventional_rate >= 0.8 ? "text-green-600" : h.conventional_rate >= 0.5 ? "text-amber-500" : "text-red-500"}`}>
{Math.round(h.conventional_rate * 100)}%
</td>
<td className={`py-1 text-right font-mono ${h.rework_rate > 0.2 ? "text-red-500" : h.rework_rate > 0.1 ? "text-amber-500" : "text-gray-500"}`}>
{Math.round(h.rework_rate * 100)}%
</td>
<td className="py-1 pl-2 text-gray-400 truncate max-w-[100px]">
{h.top_scopes.slice(0, 2).map((s) => `${s.scope}(${s.commit_count})`).join(" ")}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : (
<p className="text-[10px] text-gray-400 italic">
"全量导入" git
</p>
)}
</div>
{/* 梦核分析 */}
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider"></p>
<p className="text-[11px] text-gray-500 leading-relaxed">
Claude Opus /
</p>
{!muhePrompt ? (
<button
onClick={handleGenerateMuhe}
disabled={generating}
className="w-full py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700 disabled:opacity-50 transition-colors"
>
{generating ? "生成中…" : "生成梦核提示词"}
</button>
) : (
<div className="space-y-2">
<textarea
readOnly
value={muhePrompt}
rows={10}
className="w-full font-mono text-[11px] text-gray-700 bg-gray-50 border border-gray-200 rounded-lg p-2.5 resize-none outline-none"
onClick={(e) => (e.target as HTMLTextAreaElement).select()}
/>
<button
onClick={handleCopy}
className="w-full py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700 transition-colors"
>
{copied ? "已复制 ✓" : "复制提示词"}
</button>
<button
onClick={() => setMuhePrompt("")}
className="w-full py-1 rounded-lg text-xs text-gray-400 hover:text-gray-600 transition-colors"
>
</button>
<p className="text-[10px] text-gray-400 text-center leading-relaxed">
Claude Opus tab
</p>
</div>
)}
</div>
</>
)}
</div>
);
}

View File

@ -0,0 +1,151 @@
// 全局治理CONVENTIONS 规则来源 + 应用梦核建议(写入 master 并同步所有项目)
// 2026-07-02 ui-restructure N3从单项目 GovernancePanel 迁出——这两块操作影响所有项目,属全局层
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
getUserConventionsStatus,
saveUserConventions,
resetConventionsToBuiltin,
type UserConventionsStatus,
} from "../../lib/commands";
export function GlobalGovernancePanel() {
const { data: userConventions, refetch: refetchUserConventions } = useQuery<UserConventionsStatus>({
queryKey: ["user-conventions-status"],
queryFn: () => getUserConventionsStatus(),
});
const [conventionsInput, setConventionsInput] = useState("");
const [editorOpen, setEditorOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [saveResult, setSaveResult] = useState<string | null>(null);
const [saveError, setSaveError] = useState<string | null>(null);
const handleSave = async () => {
if (!conventionsInput.trim()) return;
setSaving(true);
setSaveError(null);
setSaveResult(null);
try {
const res = await saveUserConventions(conventionsInput);
setSaveResult(`已应用 v${res.version},同步了 ${res.synced_count} 个项目`);
setConventionsInput("");
setEditorOpen(false);
refetchUserConventions();
} catch (e) {
setSaveError(String(e));
} finally {
setSaving(false);
}
};
const handleReset = async () => {
setSaving(true);
setSaveError(null);
setSaveResult(null);
try {
const res = await resetConventionsToBuiltin();
setSaveResult(`已重置为内置基线 v${res.version},同步了 ${res.synced_count} 个项目`);
refetchUserConventions();
} catch (e) {
setSaveError(String(e));
} finally {
setSaving(false);
}
};
return (
<div className="p-4 space-y-4 max-w-2xl">
<div>
<h3 className="text-sm font-semibold text-gray-800"></h3>
<p className="text-[10px] text-gray-400 mt-0.5">CONVENTIONS </p>
</div>
{/* 规则来源 */}
{userConventions && (
<div className="flex items-center justify-between bg-gray-50 rounded-lg px-3 py-2">
<span className="text-[11px] text-gray-500"></span>
{userConventions.source === "user" ? (
<span className="text-[11px] font-mono text-purple-600 bg-purple-50 px-2 py-0.5 rounded">
v{userConventions.version}
{userConventions.outdated_from_builtin && (
<span className="ml-1 text-yellow-600"> </span>
)}
</span>
) : (
<span className="text-[11px] font-mono text-gray-500 bg-white border border-gray-200 px-2 py-0.5 rounded">
线 v{userConventions.builtin_version}
</span>
)}
</div>
)}
{/* 应用梦核建议 */}
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
<div className="flex items-center justify-between">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider"></p>
<button
onClick={() => { setEditorOpen((v) => !v); setSaveError(null); }}
className="text-[11px] text-blue-500 hover:text-blue-600"
>
{editorOpen ? "收起" : "展开"}
</button>
</div>
{editorOpen && (
<div className="space-y-2">
<p className="text-[11px] text-gray-500 leading-relaxed">
Opus CONVENTIONS
</p>
<textarea
value={conventionsInput}
onChange={(e) => setConventionsInput(e.target.value)}
placeholder="粘贴完整的 CONVENTIONS.md 内容(须包含 frontmatter version 字段)…"
rows={8}
className="w-full text-[11px] font-mono border border-gray-200 rounded p-2 resize-none focus:outline-none focus:border-blue-400"
/>
{saveError && (
<p className="text-[11px] text-red-600 font-mono break-all">{saveError}</p>
)}
<div className="flex gap-2">
<button
onClick={handleSave}
disabled={saving || !conventionsInput.trim()}
className="flex-1 py-1.5 rounded-lg bg-blue-500 text-white text-xs hover:bg-blue-600 disabled:opacity-50 transition-colors"
>
{saving ? "写入中…" : "确认应用"}
</button>
{userConventions?.source === "user" && (
<button
onClick={handleReset}
disabled={saving}
className="px-3 py-1.5 rounded-lg border border-gray-200 text-gray-600 text-xs hover:bg-gray-50 disabled:opacity-50 transition-colors"
>
线
</button>
)}
</div>
</div>
)}
{!editorOpen && userConventions?.source === "user" && (
<div className="flex items-center justify-between">
<p className="text-[11px] text-gray-500">使</p>
<button
onClick={handleReset}
disabled={saving}
className="text-[11px] text-gray-400 hover:text-red-500 disabled:opacity-50 transition-colors"
>
线
</button>
</div>
)}
{saveResult && (
<p className="text-[11px] text-green-600 bg-green-50 rounded px-2 py-1"> {saveResult}</p>
)}
</div>
<p className="text-[10px] text-gray-300 leading-relaxed">
</p>
</div>
);
}

View File

@ -0,0 +1,52 @@
// 洞察页:飞轮(跨项目数据反哺)/ 健康中心 / 全局治理
// ui-restructure N3跨项目内容的统一去处——单项目内容留在项目蓝图弹窗
import { useState } from "react";
import { FlywheelPanel } from "./FlywheelPanel";
import { GlobalGovernancePanel } from "./GlobalGovernancePanel";
import { HealthPage } from "../health/HealthPage";
type InsightTab = "flywheel" | "health" | "governance";
const TABS: { id: InsightTab; label: string }[] = [
{ id: "flywheel", label: "♻️ 飞轮" },
{ id: "health", label: "健康中心" },
{ id: "governance", label: "全局治理" },
];
export function InsightPage() {
const [tab, setTab] = useState<InsightTab>("flywheel");
return (
<div className="flex flex-col h-full min-h-0">
<div className="shrink-0 border-b border-gray-100 bg-white px-8 flex items-center gap-1">
{TABS.map(({ id, label }) => (
<button
key={id}
onClick={() => setTab(id)}
className={`px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors ${
tab === id
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-400 hover:text-gray-600"
}`}
>
{label}
</button>
))}
</div>
<div className="flex-1 overflow-y-auto min-h-0">
{tab === "flywheel" && (
<div className="max-w-2xl mx-auto">
<FlywheelPanel />
</div>
)}
{tab === "health" && <HealthPage />}
{tab === "governance" && (
<div className="max-w-2xl mx-auto">
<GlobalGovernancePanel />
</div>
)}
</div>
</div>
);
}

View File

@ -5,7 +5,6 @@ import {
type Space,
} from "../../lib/commands";
import { useUIStore } from "../../store/ui";
import { RepoRegistryModal } from "../dashboard/RepoRegistryModal";
export type Page = "overview" | "dashboard" | "resources" | "insight" | "tools" | "settings";
@ -27,7 +26,6 @@ export function Sidebar({ page, onPageChange }: Props) {
const [spaceOpen, setSpaceOpen] = useState(true);
const [showForm, setShowForm] = useState<Space | null | undefined>(undefined);
const [deleteTarget, setDeleteTarget] = useState<Space | null>(null);
const [showRepoRegistry, setShowRepoRegistry] = useState(false);
const qc = useQueryClient();
const showToast = useUIStore((s) => s.showToast);
@ -154,19 +152,6 @@ export function Sidebar({ page, onPageChange }: Props) {
)}
</div>
{/* 仓库管理 */}
<div className="border-t border-slate-700 px-3 py-2 shrink-0">
<button
onClick={() => setShowRepoRegistry(true)}
className="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-xs text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors"
>
<svg className="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="currentColor">
<path d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8z"/>
</svg>
<span></span>
</button>
</div>
</aside>
{/* 新建/编辑模态框 */}
@ -182,11 +167,6 @@ export function Sidebar({ page, onPageChange }: Props) {
/>
)}
{/* 仓库管理弹窗 */}
{showRepoRegistry && (
<RepoRegistryModal onClose={() => setShowRepoRegistry(false)} />
)}
{/* 删除确认 */}
{deleteTarget && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">

View File

@ -13,18 +13,19 @@ import { Dialog } from "../ui/Dialog";
import { TemplateEditor } from "./TemplateEditor";
import { NewProjectModal } from "./NewProjectModal";
import { TagsManager } from "./TagsManager";
import { RegistryTab } from "../dashboard/RepoRegistryModal";
export function ManagePage() {
const [tab, setTab] = useState<"projects" | "groups" | "templates" | "tags">("projects");
const [tab, setTab] = useState<"projects" | "groups" | "templates" | "tags" | "repos">("projects");
const { data: spaces = [] } = useQuery({ queryKey: ["spaces"], queryFn: getSpaces });
const currentSpaceId = spaces.find((s) => s.is_current)?.id;
const TAB_LABELS = { projects: "项目", groups: "产品组", templates: "项目模板", tags: "标签" };
const TAB_LABELS = { projects: "项目", groups: "产品组", templates: "项目模板", tags: "标签", repos: "仓库" };
return (
<div className="flex flex-col h-full min-h-0">
<div className="shrink-0 border-b border-gray-200 bg-white px-8 flex gap-1">
{(["projects", "groups", "templates", "tags"] as const).map((t) => (
{(["projects", "groups", "templates", "tags", "repos"] as const).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
@ -43,6 +44,11 @@ export function ManagePage() {
{tab === "groups" && <GroupsTab spaceId={currentSpaceId} />}
{tab === "templates" && <TemplatesTab spaceId={currentSpaceId} />}
{tab === "tags" && <TagsManager />}
{tab === "repos" && (
<div className="bg-white rounded-xl border border-gray-200 shadow-sm flex flex-col max-h-[70vh]">
<RegistryTab />
</div>
)}
</div>
</div>
</div>

View File

@ -148,8 +148,8 @@ export function OverviewPage() {
<div className="h-full overflow-y-auto">
<div className="max-w-2xl mx-auto px-6 py-6 space-y-6">
<div>
<h1 className="text-base font-semibold text-gray-800"></h1>
<p className="text-xs text-gray-400 mt-0.5">{buffs.length} Buff</p>
<h1 className="text-base font-semibold text-gray-800"></h1>
<p className="text-xs text-gray-400 mt-0.5">{buffs.length} Buff · </p>
</div>
{/* 跨项目进行中任务 */}

View File

@ -12,6 +12,7 @@ import {
// GitHub OAuth 配置已移至侧边栏「应用设置」
import { useUIStore } from "../../store/ui";
import { Dialog } from "../ui/Dialog";
import { GiteaInstancesSection } from "../dashboard/GiteaPanel";
// 编辑器条目数据结构(存为 JSON 保存到 settings 表)
interface EditorEntry {
@ -549,6 +550,7 @@ export function SettingsPage() {
)}
<BatchSyncSection />
<GiteaInstancesSection />
<UpdateSection />
</div>
);