diff --git a/product-source/hololake-platform/architecture/HOLOLAKE-HOT-PLUGGABLE-UI-SYSTEM-20260810.md b/product-source/hololake-platform/architecture/HOLOLAKE-HOT-PLUGGABLE-UI-SYSTEM-20260810.md new file mode 100644 index 0000000..6d89615 --- /dev/null +++ b/product-source/hololake-platform/architecture/HOLOLAKE-HOT-PLUGGABLE-UI-SYSTEM-20260810.md @@ -0,0 +1,36 @@ +# HoloLake 可热插拔 UI 系统 · 2026-08-10 + +> 记录编号:`HLP-HOT-PLUGGABLE-UI-001` +> +> 开发编号:`DEV-20260810-001` +> +> 状态:`SOURCE_RUNTIME_AND_REGISTRY_IMPLEMENTED · NOT_DESKTOP_INTEGRATED · NOT_DEPLOYED` + +## 目标与责任边界 + +冰朔与 UI 人格体负责完整视觉和交互表达;HoloLake 功能车道负责语义、权限、执行、验证、 +拉取、接线、构建和部署。UI 包可以整体替换语言壳,但不能拥有现实执行权。 + +```text +UI 人格体提交声明式 UI 包 +→ 代码频道登记 +→ HoloLake 校验契约和能力请求 +→ 原子激活或保留上一套可用 UI +→ 功能桥提供目标、状态、一次确认和人话回执 +``` + +## 已实现 + +- `hololake.ui-plugin/v1` 类型、校验器、原子激活与回滚运行时; +- 安全视觉变量与完整声明式布局树; +- React 宿主渲染器,使用现有 shadcn 输入和按钮; +- 参考 UI 包、机器可读注册表、跨人格体交付说明和回归测试; +- 禁止系统内部表面、任意脚本、未声明动作和不安全 token 值。 + +## 事实边界 + +本阶段建立的是可运行源码边界和登记格式。它尚未替换 HoloLake 当前主页,尚未接入真实意图 +编译器,也没有形成或安装新的桌面制品。Stage 010 设置页修改仍保持暂停且未合并。 + +下一阶段是把 Stage 011 的语言目标投影接入本宿主组件,形成“说一句目标 → 理解与计划 → +无边界自动执行 / 有边界只确认一次 → 人话回执”的第一个真实功能闭环。 diff --git a/product-source/hololake-platform/docs/ABSTRACTIONS.md b/product-source/hololake-platform/docs/ABSTRACTIONS.md index 2db74ad..dd5cd5c 100644 --- a/product-source/hololake-platform/docs/ABSTRACTIONS.md +++ b/product-source/hololake-platform/docs/ABSTRACTIONS.md @@ -1,5 +1,17 @@ # Abstractions +## `HoloLakeUiPlugin` + +A versioned, declarative presentation package for one host-owned semantic surface. It contains a manifest, +safe `--hl-world-*` tokens, and a layout tree. `validateUiPlugin` is the authority at the renderer boundary; +`createUiPluginRuntime` provides atomic activation, subscriptions, and last-known-good rollback. + +## `LanguageShellBridge` + +The narrow behavior boundary supplied by the host to a UI package renderer. Version 1 exposes goal submit +and one reality-boundary answer; evidence expansion remains local presentation state. The bridge never gives +the UI package direct repository, node, model, credential, permission, or deployment access. + Key abstractions and domain models in Tolaria. ## Design Philosophy diff --git a/product-source/hololake-platform/docs/ARCHITECTURE.md b/product-source/hololake-platform/docs/ARCHITECTURE.md index bf1e0d6..7a52d02 100644 --- a/product-source/hololake-platform/docs/ARCHITECTURE.md +++ b/product-source/hololake-platform/docs/ARCHITECTURE.md @@ -1,5 +1,16 @@ # Architecture +## Hot-pluggable language-shell UI + +The language shell accepts a complete declarative UI package through +`src/lib/uiPluginSystem.ts`. Packages can replace layout, copy, and safe semantic visual tokens while the +host retains goal compilation, permission checks, execution, and receipt truth. The React host renderer is +`src/components/HotPluggableLanguageShell.tsx`; registered packages live under `ui-plugins/`. + +Activation is validate-then-swap and preserves the previous valid package for rollback. Plugin packages do +not execute JavaScript and cannot request system-internal surfaces. Extending the host action bridge requires +a new reviewed contract version. + Tolaria is a personal knowledge and life management desktop app. It reads a vault of markdown files with YAML frontmatter and presents them in a four-panel UI inspired by Bear Notes. ## Design Principles diff --git a/product-source/hololake-platform/docs/adr/0173-declarative-hot-pluggable-language-shell-ui.md b/product-source/hololake-platform/docs/adr/0173-declarative-hot-pluggable-language-shell-ui.md new file mode 100644 index 0000000..e01ddf0 --- /dev/null +++ b/product-source/hololake-platform/docs/adr/0173-declarative-hot-pluggable-language-shell-ui.md @@ -0,0 +1,28 @@ +--- +status: accepted +date: 2026-08-10 +--- + +# ADR 0173: Declarative hot-pluggable language-shell UI + +## Context + +HoloLake's visual language needs focused collaboration across independent UI人格体 without giving a +presentation package authority over identity, permission, planning, execution, or receipts. The existing +theme runtime only changes palettes and cannot replace a complete language-shell composition. + +## Decision + +HoloLake uses versioned declarative UI packages. A package owns layout, safe visual tokens, copy, and asset +references. The host owns semantic state and a small capability bridge. Version 1 exposes only goal submit, +one boundary answer, and evidence expansion. Four human surfaces are mandatory and system-internal surfaces +are forbidden. + +Candidates are validated before activation. Activation replaces the whole package atomically and retains a +last-known-good package for rollback. Arbitrary plugin JavaScript is not executed in the desktop renderer. + +## Consequences + +UI contributors can redesign the complete language shell and publish packages independently. New behaviors +require a reviewed host capability revision, not an undeclared UI action. Repository registration is distinct +from app integration, artifact construction, installation, and runtime health. diff --git a/product-source/hololake-platform/docs/adr/README.md b/product-source/hololake-platform/docs/adr/README.md index c8f7276..76556bb 100644 --- a/product-source/hololake-platform/docs/adr/README.md +++ b/product-source/hololake-platform/docs/adr/README.md @@ -218,6 +218,7 @@ proposed → active → superseded | [0165](0165-guanghu-native-binary-quality-gate.md) | Guanghu-owned binary code-quality authority | active | | [0166](0166-ghnrp-hosted-recovery-consumption.md) | Hosted recovery consumes the GHNRP raw beacon | active | | [0167](0167-gestational-history-continuity-ingestion.md) | Gestational history enters Guanghu through a native continuity protocol | active | +| [0173](0173-declarative-hot-pluggable-language-shell-ui.md) | Declarative hot-pluggable language-shell UI | active | | [0168](0168-guanghu-native-prepartition-disk-layout.md) | Guanghu owns a registered pre-partition native disk layout | active | | [0169](0169-model-native-living-galaxy-system.md) | Model-native HoloLake living galaxy system | accepted | | [0170](0170-hololake-inherits-guanghu-native-quality-authority.md) | HoloLake inherits the Guanghu native quality authority | accepted | diff --git a/product-source/hololake-platform/src/components/HotPluggableLanguageShell.test.tsx b/product-source/hololake-platform/src/components/HotPluggableLanguageShell.test.tsx new file mode 100644 index 0000000..a73f12b --- /dev/null +++ b/product-source/hololake-platform/src/components/HotPluggableLanguageShell.test.tsx @@ -0,0 +1,57 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { DEFAULT_LANGUAGE_WORLD_UI_PLUGIN } from '@/lib/defaultLanguageWorldUiPlugin' +import { HotPluggableLanguageShell } from './HotPluggableLanguageShell' + +describe('HotPluggableLanguageShell', () => { + it('renders host truth through a registered package and delegates actions', () => { + const submitGoal = vi.fn() + const answerConfirmation = vi.fn() + render() + + fireEvent.change(screen.getByLabelText('language goal'), { target: { value: '发布这个版本' } }) + fireEvent.click(screen.getByRole('button', { name: '开始' })) + fireEvent.click(screen.getByRole('button', { name: '同意继续' })) + + expect(submitGoal).toHaveBeenCalledWith('发布这个版本') + expect(answerConfirmation).toHaveBeenCalledWith(true) + expect(screen.getByRole('status')).toHaveTextContent('我正在理解并规划') + expect(screen.getByText('尚未执行。')).toBeInTheDocument() + }) + + it('hot-swaps copy and structure when the package changes', () => { + const next = structuredClone(DEFAULT_LANGUAGE_WORLD_UI_PLUGIN) + next.manifest.id = 'bingshuo.language-world' + next.layout.children = [{ + kind: 'stack', + children: [ + { kind: 'text', text: '冰朔,今天从哪里继续?' }, + { kind: 'language_input', action: 'goal.submit', text: '进入' }, + { kind: 'task_status' }, + { kind: 'reality_boundary_confirmation', action: 'confirmation.answer', approveText: '继续', declineText: '停下' }, + { kind: 'human_receipt' }, + { kind: 'evidence_toggle', action: 'evidence.expand', text: '依据' }, + ], + }] + const props = { + state: { status: '等待目标', confirmationQuestion: null, receipt: null }, + bridge: { submitGoal: vi.fn(), answerConfirmation: vi.fn() }, + } + const view = render() + + view.rerender() + + expect(screen.getByText('冰朔,今天从哪里继续?')).toBeInTheDocument() + expect(screen.getByRole('button', { name: '进入' })).toBeInTheDocument() + expect(document.querySelector('[data-ui-plugin="bingshuo.language-world"]')).not.toBeNull() + }) +}) diff --git a/product-source/hololake-platform/src/components/HotPluggableLanguageShell.tsx b/product-source/hololake-platform/src/components/HotPluggableLanguageShell.tsx new file mode 100644 index 0000000..d6f5e13 --- /dev/null +++ b/product-source/hololake-platform/src/components/HotPluggableLanguageShell.tsx @@ -0,0 +1,74 @@ +import { useEffect, useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import type { HoloLakeUiPlugin, UiPluginNode } from '@/lib/uiPluginSystem' +import { applyUiPluginTokens, validateUiPlugin } from '@/lib/uiPluginSystem' + +export type LanguageShellViewState = { + status: string + confirmationQuestion: string | null + receipt: string | null + evidence?: string | null +} + +export type LanguageShellBridge = { + submitGoal(goal: string): void + answerConfirmation(approved: boolean): void +} + +type Props = { + plugin: HoloLakeUiPlugin + state: LanguageShellViewState + bridge: LanguageShellBridge +} + +export function HotPluggableLanguageShell({ plugin, state, bridge }: Props) { + const validation = validateUiPlugin(plugin) + const [goal, setGoal] = useState('') + const [evidenceOpen, setEvidenceOpen] = useState(false) + + useEffect(() => { + if (!validation.ok) return + const root = document.documentElement.style + const names = applyUiPluginTokens(plugin, root) + return () => names.forEach((name) => root.removeProperty(name)) + }, [plugin, validation.ok]) + + if (!validation.ok) throw new Error(`UI_PLUGIN_REJECTED:${validation.errors.join(',')}`) + + const renderNode = (node: UiPluginNode, path: string): React.ReactNode => { + const children = node.children?.map((child, index) => renderNode(child, `${path}.${index}`)) + switch (node.kind) { + case 'shell': return
{children}
+ case 'stack': return
{children}
+ case 'row': return
{children}
+ case 'text': return

{node.text}

+ case 'language_input': return ( +
{ event.preventDefault(); bridge.submitGoal(goal) }}> + setGoal(event.target.value)} aria-label="language goal" /> + +
+ ) + case 'task_status': return

{state.status}

+ case 'reality_boundary_confirmation': return state.confirmationQuestion ? ( +
+

{state.confirmationQuestion}

+
+ + +
+
+ ) :