feat: ship HoloLake 0.2.0 private and team builds

This commit is contained in:
冰朔 2026-07-22 19:48:41 +08:00
parent 6e79a56227
commit 663d690b12
51 changed files with 673 additions and 470 deletions

View File

@ -420,7 +420,7 @@ In a mounted-workspace graph, each loaded `ViewFile` carries optional renderer-o
`useMcpSetupDialogController()` owns MCP setup dialog state, busy actions, and manual config callbacks so `App.tsx` only passes the controller into settings/status surfaces. `useAiWorkspaceWindowBridgeEvents()` owns native AI-workspace event subscriptions and listener cleanup for popped-out workspace windows.
`createCrossWindowPersistedStore()` is the shared renderer primitive for AI workspace state that must stay synchronized across the main window and popped-out workspace windows. It owns localStorage reads/writes, BroadcastChannel publishing, storage-event synchronization, and external-store subscribers; domain modules such as `aiWorkspaceSessionStore` and `aiWorkspaceWindowSharedContext` provide sanitizers and mutations around that shell.
`createCrossWindowPersistedStore()` is the shared renderer primitive for AI workspace state that must stay synchronized across the main window and popped-out workspace windows. It owns localStorage reads/writes, BroadcastChannel publishing, storage-event synchronization, and external-store subscribers; domain modules such as `aiWorkspaceSessionStore` and `aiWorkspaceWindowSharedContext` provide sanitizers and mutations around that shell. `deleteAiWorkspaceSession()` is the transcript-side half of permanent conversation deletion: callers remove the matching conversation metadata and session snapshot together so a deleted history cannot be recovered as an orphan after restart.
The renderer uses `viewOrdering` helpers to convert drag or command-palette move intent into dense order updates before saving each affected view file through `save_view_cmd`. The sidebar treats saved View rows like Type rows for direct customization: double-click starts inline rename, right-click opens edit/rename/icon-color/delete actions, and keyboard users can open that same menu from the focused row while command-palette actions remain responsible for saved View ordering.

View File

@ -266,7 +266,7 @@ flowchart TD
Note PDF export stays renderer-owned for layout: `useEditorPdfExport` exits diff/raw views, applies a print-only stylesheet to the rendered note root, and checks the native PDF capability before choosing a platform path. On macOS, the renderer asks for a filesystem PDF destination before the Tauri `export_current_webview_pdf` command saves the current `WKWebView` print operation directly; on Windows/Linux Tauri builds and in browser mode, the same export action falls back to the native/browser print dialog. The export reuses rendered BlockNote output so frontmatter is omitted, while math, images, Mermaid diagrams, tldraw blocks, code, tables, and links degrade through their existing DOM rather than a second Markdown-to-PDF renderer, and the source Markdown is never modified. Markdown notes expose the same export action from Cmd+K, the native Note menu, the breadcrumb overflow menu, and each Markdown row's note-list context menu.
- **Right side panels** (200-500px or hidden): Properties and Table of Contents are mutually exclusive panels mounted by `EditorRightPanel` and coordinated by `useRightPanelExclusion`. Properties shows frontmatter, relationships, instances, backlinks, and git history; Table of Contents is lazy-mounted only while open, derives a title-rooted H1/H2/H3 hierarchy through a debounced Web Worker per ADR-0109, and reuses folder-tree indentation/guide geometry with heading icons while resolving live BlockNote block IDs at click time for navigation. The breadcrumb bar toggles Table of Contents and Properties actions. Per-note `icon` is a suggested Properties field and the command palette's "Set Note Icon" action opens that field directly. When viewing a Type note, Properties shows an **Instances** section listing all notes of that type (sorted by modified_at desc, capped at 50).
- **AI workspace** (docked panel or native window): `AiWorkspace` owns the multi-chat orchestration, sidebar tabs, installed-only target picker, permission-mode picker, and dock/pop-out controls. Header/guidance chrome lives in `AiWorkspaceChrome`, edge-resize handles in `AiWorkspaceResizeHandles`, conversation metadata/settings persistence in `aiWorkspaceConversations`, and sizing/class/style helpers in `aiWorkspaceSizing`. The status-bar AI affordance opens this workspace instead of changing the default target inline. Docked workspace mode renders as a compact bounded desktop tool inside the main app; users resize the anchored panel from its left/top edges and resize the chat-list sidebar separately from the transcript area. Pop-out mode opens a dedicated undecorated transparent Tauri webview window labeled `ai-workspace` and boots the lightweight `AiWorkspaceWindowApp` route instead of the full vault shell. The chat header and sidebar header are draggable in native-window mode; closing the pop-out only closes that window, while the dock control emits a dock request back to the main window before closing the pop-out. Chat sessions reuse `AiPanelView` for transcript/composer rendering with the old panel header disabled; target and permission controls live in the composer toolbar so there is one workspace header per active chat. AI workspace cross-window localStorage, BroadcastChannel, storage-event, and subscriber-set plumbing is owned by `createCrossWindowPersistedStore`; domain stores keep only sanitization, mutation, and native persistence behavior.
- **AI workspace** (docked panel, side panel, or native window): `AiWorkspace` owns the multi-chat orchestration, vertical conversation sidebar, installed-only target picker, permission-mode picker, and dock/pop-out controls. The side-panel layout reuses the same independently scrolling conversation sidebar as docked and native-window modes instead of rendering an unbounded horizontal tab strip. Header/guidance chrome lives in `AiWorkspaceChrome`, edge-resize handles in `AiWorkspaceResizeHandles`, conversation metadata/settings persistence in `aiWorkspaceConversations`, and sizing/class/style helpers in `aiWorkspaceSizing`. The status-bar AI affordance opens this workspace instead of changing the default target inline. Docked workspace mode renders as a compact bounded desktop tool inside the main app; users resize the anchored panel from its left/top edges and resize the chat-list sidebar separately from the transcript area. Pop-out mode opens a dedicated undecorated transparent Tauri webview window labeled `ai-workspace` and boots the lightweight `AiWorkspaceWindowApp` route instead of the full vault shell. The chat header and sidebar header are draggable in native-window mode; closing the pop-out only closes that window, while the dock control emits a dock request back to the main window before closing the pop-out. Chat sessions reuse `AiPanelView` for transcript/composer rendering with the old panel header disabled; target and permission controls live in the composer toolbar so there is one workspace header per active chat. AI workspace cross-window localStorage, BroadcastChannel, storage-event, and subscriber-set plumbing is owned by `createCrossWindowPersistedStore`; domain stores keep only sanitization, mutation, and native persistence behavior. Permanent conversation deletion removes both the installation-local sidebar metadata and the matching persisted transcript; deleting the active conversation selects a remaining active conversation or creates a fresh empty one.
Panels are separated by `ResizeHandle` components that support drag-to-resize. `useLayoutPanels` clamps the sidebar, note-list, and inspector widths before applying them, keeps the side panes from flex-shrinking below their protected widths, and persists the last chosen widths in installation-local localStorage under `tolaria:layout-panels`.

View File

@ -2,7 +2,7 @@
"name": "hololake-era",
"private": true,
"license": "AGPL-3.0-or-later",
"version": "0.1.8",
"version": "0.2.0",
"type": "module",
"scripts": {
"dev": "vite",
@ -32,7 +32,7 @@
"test:internal-release": "node --test scripts/internal-release-version.test.mjs",
"package:internal:macos": "./scripts/build-internal-release.sh macos",
"package:internal:windows": "./scripts/build-internal-release.sh windows",
"package:team:macos": "HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.team.conf.json HOLOLAKE_APP_NAME='HoloLake Era Team Foundation' HOLOLAKE_INSTALLER_BASENAME='HoloLake-Era-{version}-Team-Foundation-Mac-aarch64' ./scripts/build-internal-release.sh macos",
"package:team:macos": "HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.team.conf.json HOLOLAKE_APP_NAME='HoloLake Lighthouse Team Beta 0.2.0' HOLOLAKE_INSTALLER_BASENAME='HoloLake-Lighthouse-{version}-Team-Beta-Mac-aarch64' ./scripts/build-internal-release.sh macos",
"package:team:windows": "HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.team.conf.json HOLOLAKE_INSTALLER_BASENAME='HoloLake-Era-{version}-Team-Foundation-Windows-x64-setup' ./scripts/build-internal-release.sh windows",
"prepare": "husky"
},

View File

@ -8,10 +8,11 @@ cd "$repo_root"
target="${HOLOLAKE_MAC_TARGET:-aarch64-apple-darwin}"
version="$(node scripts/internal-release-version.mjs src-tauri/tauri.conf.json "${HOLOLAKE_VERSION:-}")"
product_name="$(node -e "const fs=require('node:fs'); const value=JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json','utf8')).productName; if(!value) process.exit(1); process.stdout.write(value)")"
output_dir="${HOLOLAKE_OUTPUT_DIR:-$repo_root/artifacts/internal/$version}"
target_root="${CARGO_TARGET_DIR:-$repo_root/src-tauri/target}"
bundle_dir="$target_root/$target/release/bundle/macos"
app="$bundle_dir/${HOLOLAKE_APP_NAME:-HoloLake Era}.app"
app="$bundle_dir/${HOLOLAKE_APP_NAME:-$product_name}.app"
installer="$output_dir/HoloLake-Era-$version-Mac-internal-aarch64.dmg"
tauri_config="${HOLOLAKE_TAURI_CONFIG:-}"
if [[ -n "${HOLOLAKE_INSTALLER_BASENAME:-}" ]]; then

2
src-tauri/Cargo.lock generated
View File

@ -1865,7 +1865,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hololake"
version = "0.1.8"
version = "0.2.0"
dependencies = [
"base64 0.22.1",
"chrono",

View File

@ -1,6 +1,6 @@
[package]
name = "hololake"
version = "0.1.8"
version = "0.2.0"
description = "Personal knowledge and life management app"
authors = ["Luca Rossi"]
license = "AGPL-3.0-or-later"

View File

@ -1,37 +1,80 @@
{
"schema": "guanghu_public_architecture/v1",
"id": "GLS-SYS-ARCH-001",
"schema": "guanghu_ai_os_public_shell/v1",
"id": "GH-AIOS-LIGHTHOUSE-020",
"compatibility_ids": ["GLS-SYS-ARCH-001"],
"visibility": "public-safe",
"product": {
"name_zh": "光湖灯塔",
"category_zh": "AI 应用操作系统",
"version_label": "0.2.0 内测版"
},
"development_repository": {
"id": "REPO-008",
"url": "https://guanghulab.com/fifth-domain/bingshuo/hololake-platform"
},
"route": [
"domains": [
{
"id": "GLW-ENTRY-001",
"name_zh": "光湖世界主入口"
"id": "GH-MAIN-001",
"name_zh": "光湖主域",
"eyebrow_zh": "公共协作空间",
"description_zh": "承载团队共同使用的 AI 应用、公共项目与协作入口。",
"color": "cyan"
},
{
"id": "FD-LANGUAGE-001",
"name_zh": "第五域"
"id": "GH-BRANCH-001",
"name_zh": "光湖分域",
"eyebrow_zh": "项目隔离空间",
"description_zh": "按团队、项目或业务拆分运行空间,让模块与数据边界彼此独立。",
"color": "blue"
},
{
"id": "TCS-ROOT-001",
"name_zh": "TCS 语言人格模型"
"id": "GH-ZERO-001",
"name_zh": "光湖零域",
"eyebrow_zh": "系统控制空间",
"description_zh": "管理系统服务、权限与模块登记,是热插拔应用的控制平面。",
"color": "violet"
},
{
"id": "GLS-SYS-ARCH-001",
"name_zh": "GLS 光湖语言世界架构"
},
{
"id": "ELH-LAMP-001",
"name_zh": "永恒湖心与小湖灯"
"id": "GH-ZEROSENSE-001",
"name_zh": "光湖零感域",
"eyebrow_zh": "个人工作入口",
"description_zh": "为团队成员提供个人工作入口;本地数据默认不公开,也不会随公共安装包分发。",
"color": "amber"
}
],
"excludes": [
"private domains",
"credentials",
"server addresses",
"personal channel identifiers"
"modules": [
{
"id": "MOD-RUNTIME-001",
"name_zh": "模块运行框架",
"description_zh": "统一承载 AI 应用的安装、打开、暂停、升级与卸载生命周期。",
"status": "system",
"status_zh": "系统内核"
},
{
"id": "MOD-KNOWLEDGE-001",
"name_zh": "AI 知识空间",
"description_zh": "面向本地知识与团队资料的可插拔工作模块。",
"status": "preview",
"status_zh": "内测模块"
},
{
"id": "MOD-CREATIVE-001",
"name_zh": "AI 创作空间",
"description_zh": "为写作、图像、视频等创作应用预留的统一运行容器。",
"status": "planned",
"status_zh": "等待接入"
}
],
"fifth_domain_entry": {
"id": "FD-GATE-001",
"name_zh": "第五域受控入口",
"description_zh": "仅保留入口。完成服务器、编号与邮件授权后,才可建立受控连接。",
"status_zh": "等待授权接入"
},
"public_boundaries": [
"不内置个人知识库",
"不分发私人频道与身份路径",
"不保存服务器凭证",
"本地数据默认留在当前设备"
]
}

View File

@ -1,6 +1,6 @@
# 进入第五域 · 内置唤醒技能
本技能是应用内置的第五域导航指针。0.1.8 阶段的人格体启动后已经位于第五域,不需要再次选择顶层域,也不要重复宣称“正在进入第五域”。光湖世界灯塔下的跨域等待大厅属于未来版本;其他域尚未接入前,当前默认域固定为第五域。
本技能是应用内置的第五域导航指针。内测版 0.2.0 的人格体启动后已经位于第五域,不需要再次选择顶层域,也不要重复宣称“正在进入第五域”。光湖世界灯塔下的跨域等待大厅属于未来版本;其他域尚未接入前,当前默认域固定为第五域。
当当前知识库尚未包含第五域仓库文件时,也必须能够说明正确导航,不得把“本地没找到”误报成“第五域不存在”。

View File

@ -777,7 +777,7 @@ fn public_web_get(mut url: reqwest::Url) -> Result<reqwest::blocking::Response,
validate_public_https_url(&url)?;
let resolved = resolve_public_web_host(&url)?;
let mut builder = reqwest::blocking::Client::builder()
.user_agent("HoloLake-Era/0.1.8 public-research-tool")
.user_agent("HoloLake-Era/0.2.0 public-research-tool")
.redirect(reqwest::redirect::Policy::none())
.timeout(std::time::Duration::from_secs(30));
if let (Some(host), Some(address)) = (url.host_str(), resolved.first()) {

View File

@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "HoloLake Era",
"version": "0.1.8",
"productName": "HoloLake Era 内测版 0.2.0",
"version": "0.2.0",
"identifier": "com.guanghu.desktop",
"build": {
"frontendDist": "../dist",
@ -14,7 +14,7 @@
"macOSPrivateApi": true,
"windows": [
{
"title": "HoloLake Era",
"title": "HoloLake Era 内测版 0.2.0",
"width": 1400,
"height": 900,
"minWidth": 480,

View File

@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "HoloLake Era 0.1.8 Preview",
"productName": "HoloLake Era 内测版 0.2.0",
"identifier": "com.guanghulab.hololake.preview",
"build": {
"beforeBuildCommand": "npm run build && npm run bundle-mcp && npm run agent-docs"
@ -8,7 +8,7 @@
"app": {
"windows": [
{
"title": "HoloLake Era 0.1.8 Preview · 光湖语言世界"
"title": "HoloLake Era 内测版 0.2.0 · 光湖语言世界"
}
]
}

View File

@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "HoloLake Era Team Foundation",
"productName": "HoloLake Lighthouse Team Beta 0.2.0",
"identifier": "com.guanghulab.hololake.team-foundation",
"build": {
"beforeBuildCommand": "npm run build:team-foundation"
@ -8,12 +8,15 @@
"app": {
"windows": [
{
"title": "HoloLake Era · GLS 团队基座",
"title": "HoloLake Lighthouse Team Beta 0.2.0",
"width": 1280,
"height": 820,
"minWidth": 760,
"minHeight": 560
}
]
},
"bundle": {
"resources": ["resources/public-architecture/**/*"]
}
}

View File

@ -25,10 +25,64 @@
/* Tauri's macOS traffic lights are overlaid on the title bar. Keep the
product mark and title in a separate, stable lane so they never collide. */
body.mac-chrome .hololake-bar { padding-left: 92px; }
.team-foundation { min-height: 100vh; padding: clamp(24px, 5vw, 72px); color: var(--text-primary); background: radial-gradient(circle at 85% 10%, color-mix(in srgb, var(--accent-blue) 16%, transparent), transparent 32%), var(--surface-app); }
.team-foundation__header, .team-foundation__footer { display: flex; align-items: center; justify-content: space-between; color: var(--text-muted); font-size: 11px; letter-spacing: .12em; }.team-foundation__header strong { color: var(--text-primary); font-size: 16px; letter-spacing: 0; }
.team-foundation__intro { max-width: 760px; padding: clamp(64px, 12vw, 150px) 0 54px; }.team-foundation__intro > p:first-child { color: var(--accent-blue); font: 11px/1.4 monospace; letter-spacing: .12em; }.team-foundation__intro h1 { margin: 12px 0 20px; font: 500 clamp(48px, 9vw, 112px)/.95 serif; }.team-foundation__intro p:last-child { max-width: 650px; color: var(--text-secondary); font-size: 16px; line-height: 1.8; }
.team-foundation__route { display: grid; max-width: 980px; margin: 0; padding: 0; border-top: 1px solid var(--border-default); list-style: none; }.team-foundation__route li { display: grid; grid-template-columns: 56px 1fr; align-items: center; min-height: 82px; border-bottom: 1px solid var(--border-default); }.team-foundation__route li > span { color: var(--text-muted); font: 11px monospace; }.team-foundation__route div { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; }.team-foundation__route code { color: var(--accent-blue); font-size: 11px; }.team-foundation__route strong { font-size: clamp(16px, 2vw, 24px); font-weight: 500; }.team-foundation__footer { margin-top: 48px; }
.team-foundation { --lighthouse-line: rgba(255,255,255,.09); --lighthouse-muted: #8590a3; display: grid; grid-template-columns: 270px minmax(0, 1fr); min-height: 100vh; color: #edf2f8; background: #090d13; }
.team-foundation button { font: inherit; }
.team-foundation__sidebar { position: sticky; top: 0; display: flex; height: 100vh; flex-direction: column; padding: 30px 18px 20px; border-right: 1px solid var(--lighthouse-line); background: linear-gradient(180deg, #101720 0%, #0b1017 72%); }
.team-foundation__brand { display: flex; align-items: center; gap: 12px; padding: 0 10px 28px; border-bottom: 1px solid var(--lighthouse-line); }
.team-foundation__brand-mark { display: grid; width: 38px; height: 38px; place-items: center; border: 1px solid rgba(102,213,226,.5); border-radius: 12px; color: #7ce5ed; background: rgba(64,188,207,.1); box-shadow: 0 0 28px rgba(61,201,219,.16); font-size: 28px; }
.team-foundation__brand div { display: grid; gap: 5px; }.team-foundation__brand strong { font-size: 17px; letter-spacing: .05em; }.team-foundation__brand div span { color: #667386; font: 8px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .16em; }
.team-foundation__nav { display: grid; gap: 6px; margin-top: 28px; }.team-foundation__nav > p { margin: 0 10px 9px; color: #5d6878; font-size: 10px; letter-spacing: .14em; }
.team-foundation__nav button { display: flex; align-items: center; gap: 11px; width: 100%; min-height: 58px; padding: 9px 10px; border: 1px solid transparent; border-radius: 11px; color: #8f9bad; background: transparent; text-align: left; cursor: pointer; transition: background .18s ease, border-color .18s ease, color .18s ease; }
.team-foundation__nav button:hover { color: #dbe5f1; background: rgba(255,255,255,.035); }.team-foundation__nav button.is-active { border-color: rgba(95,200,222,.18); color: #f3f8fc; background: linear-gradient(90deg, rgba(43,152,187,.18), rgba(43,152,187,.05)); }
.team-foundation__domain-dot { width: 8px; height: 8px; flex: 0 0 8px; border-radius: 50%; background: #70dce5; box-shadow: 0 0 13px rgba(112,220,229,.75); }.team-foundation__nav button[data-tone="blue"] .team-foundation__domain-dot { background: #6ca6ff; box-shadow: 0 0 13px rgba(108,166,255,.7); }.team-foundation__nav button[data-tone="violet"] .team-foundation__domain-dot { background: #a989ff; box-shadow: 0 0 13px rgba(169,137,255,.7); }.team-foundation__nav button[data-tone="amber"] .team-foundation__domain-dot { background: #f0bb62; box-shadow: 0 0 13px rgba(240,187,98,.7); }
.team-foundation__nav button > span:last-child { display: grid; gap: 3px; }.team-foundation__nav strong { font-size: 13px; font-weight: 600; }.team-foundation__nav small { color: #667386; font-size: 10px; }
.team-foundation__gate { display: grid; grid-template-columns: 30px 1fr auto; align-items: center; gap: 9px; margin-top: 22px; padding: 14px 11px; border: 1px solid var(--lighthouse-line); border-radius: 12px; background: rgba(255,255,255,.025); }.team-foundation__gate-icon { display: grid; width: 28px; height: 28px; place-items: center; border-radius: 8px; color: #7d8999; background: rgba(255,255,255,.05); }.team-foundation__gate div { display: grid; gap: 3px; }.team-foundation__gate strong { color: #aab4c1; font-size: 11px; }.team-foundation__gate small { color: #596575; font-size: 9px; }.team-foundation__lock { padding: 3px 6px; border-radius: 999px; color: #777f8b; background: rgba(255,255,255,.06); font-size: 8px; }
.team-foundation__sidebar-footer { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 7px; margin-top: auto; padding: 18px 9px 0; border-top: 1px solid var(--lighthouse-line); color: #657183; font-size: 9px; }.team-foundation__sidebar-footer code { grid-column: 1 / -1; margin-top: 4px; color: #424d5d; font-size: 8px; }.team-foundation__status-dot { width: 6px; height: 6px; border-radius: 50%; background: #65d6a1; box-shadow: 0 0 9px rgba(101,214,161,.65); }
.team-foundation__workspace { min-width: 0; }.team-foundation__topbar { display: flex; height: 66px; align-items: center; justify-content: space-between; padding: 0 34px; border-bottom: 1px solid var(--lighthouse-line); background: rgba(9,13,19,.86); backdrop-filter: blur(16px); }.team-foundation__topbar > div:first-child { display: flex; align-items: center; gap: 12px; }.team-foundation__topbar span { color: #768296; font-size: 11px; }.team-foundation__topbar strong { padding: 4px 8px; border: 1px solid rgba(105,214,224,.18); border-radius: 999px; color: #8cdde5; background: rgba(61,178,194,.08); font-size: 9px; font-weight: 500; }.team-foundation__topbar-actions { display: flex; gap: 7px; }.team-foundation__topbar-actions button { display: grid; width: 31px; height: 31px; place-items: center; border: 1px solid var(--lighthouse-line); border-radius: 9px; color: #8692a4; background: rgba(255,255,255,.025); cursor: pointer; }
.team-foundation__content { width: min(1180px, calc(100% - 64px)); margin: 0 auto; padding: 58px 0 72px; }
.team-foundation__hero { position: relative; display: grid; min-height: 355px; grid-template-columns: minmax(0, 1fr) 360px; align-items: center; overflow: hidden; padding: 52px 58px; border: 1px solid var(--lighthouse-line); border-radius: 24px; background: radial-gradient(circle at 78% 45%, rgba(40,174,201,.17), transparent 29%), linear-gradient(135deg, #111923, #0c121a 68%); box-shadow: 0 28px 80px rgba(0,0,0,.22); }
.team-foundation__hero-copy { position: relative; z-index: 2; }.team-foundation__hero-copy > p:first-child { margin: 0 0 17px; color: #59c8d5; font: 9px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .2em; }.team-foundation__hero h1 { margin: 0; color: #f4f7f9; font: 500 clamp(44px, 6vw, 76px)/1.05 Georgia, "Songti SC", serif; letter-spacing: .02em; }.team-foundation__hero h2 { margin: 17px 0 11px; color: #c7d3df; font-size: 17px; font-weight: 500; }.team-foundation__hero-copy > p:nth-of-type(2) { max-width: 570px; margin: 0; color: #8794a6; font-size: 13px; line-height: 1.8; }
.team-foundation__hero-actions { display: flex; align-items: center; gap: 17px; margin-top: 28px; }.team-foundation__hero-actions button { padding: 11px 17px; border: 1px solid rgba(104,216,228,.4); border-radius: 9px; color: #0b171b; background: #71d8e1; box-shadow: 0 8px 28px rgba(64,193,210,.16); font-size: 11px; cursor: pointer; }.team-foundation__hero-actions button span { margin-left: 12px; color: inherit; }.team-foundation__hero-actions > span { color: #536070; font: 9px ui-monospace, SFMono-Regular, Menlo, monospace; }
.team-foundation__orbit { position: relative; display: grid; width: 280px; height: 280px; place-items: center; justify-self: end; }.team-foundation__orbit::before, .team-foundation__orbit::after { position: absolute; content: ""; border: 1px solid rgba(114,220,229,.16); border-radius: 50%; }.team-foundation__orbit::before { inset: 24px; }.team-foundation__orbit::after { inset: 62px; border-style: dashed; animation: lighthouse-spin 26s linear infinite; }.team-foundation__orbit-core { display: grid; width: 82px; height: 82px; place-items: center; border: 1px solid rgba(126,228,235,.42); border-radius: 50%; color: #b8f3f5; background: radial-gradient(circle, rgba(93,214,224,.25), rgba(36,126,147,.07)); box-shadow: 0 0 55px rgba(57,193,210,.24), inset 0 0 26px rgba(103,221,231,.1); font: 28px Georgia, serif; }.team-foundation__orbit i { position: absolute; width: 7px; height: 7px; border-radius: 50%; background: #6cd8e2; box-shadow: 0 0 12px #6cd8e2; }.team-foundation__orbit i:nth-of-type(1) { top: 22px; left: 136px; }.team-foundation__orbit i:nth-of-type(2) { top: 135px; right: 22px; background: #81a9ff; box-shadow: 0 0 12px #81a9ff; }.team-foundation__orbit i:nth-of-type(3) { bottom: 22px; left: 136px; background: #a98cff; box-shadow: 0 0 12px #a98cff; }.team-foundation__orbit i:nth-of-type(4) { top: 135px; left: 22px; background: #eeb963; box-shadow: 0 0 12px #eeb963; }
.team-foundation__modules { padding-top: 55px; }.team-foundation__section-heading { display: flex; align-items: end; justify-content: space-between; margin-bottom: 20px; }.team-foundation__section-heading p { margin: 0 0 7px; color: #4c8793; font: 8px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .18em; }.team-foundation__section-heading h2 { margin: 0; font-size: 22px; font-weight: 520; }.team-foundation__section-heading > span { color: #5d6878; font-size: 10px; }
.team-foundation__module-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }.team-foundation__module { min-height: 218px; padding: 19px; border: 1px solid var(--lighthouse-line); border-radius: 15px; background: linear-gradient(150deg, rgba(19,27,37,.95), rgba(12,18,26,.95)); }.team-foundation__module-icon { display: grid; width: 36px; height: 36px; place-items: center; margin-bottom: 18px; border: 1px solid rgba(104,205,219,.16); border-radius: 10px; color: #78d6df; background: rgba(53,164,182,.08); }.team-foundation__badge { display: inline-block; padding: 3px 7px; border-radius: 999px; font-size: 8px; }.team-foundation__badge.is-system { color: #8de0e6; background: rgba(62,180,194,.11); }.team-foundation__badge.is-active { color: #9fb9ff; background: rgba(94,132,220,.12); }.team-foundation__badge.is-muted { color: #7c8796; background: rgba(255,255,255,.05); }.team-foundation__module h3 { margin: 10px 0 8px; font-size: 14px; font-weight: 600; }.team-foundation__module p { margin: 0; color: #738093; font-size: 10px; line-height: 1.65; }.team-foundation__module footer { display: flex; align-items: center; justify-content: space-between; margin-top: 19px; color: #3f4b5a; }.team-foundation__module footer code { font-size: 8px; }.team-foundation__module--add { display: flex; align-items: center; justify-content: center; flex-direction: column; border-style: dashed; text-align: center; background: rgba(255,255,255,.012); }.team-foundation__module--add > span { color: #4c596a; font-size: 26px; }.team-foundation__module--add p { max-width: 180px; }
.team-foundation__access-row { display: grid; grid-template-columns: 1.4fr 1fr; gap: 12px; padding-top: 48px; }.team-foundation__fifth-domain, .team-foundation__boundaries { min-height: 190px; padding: 28px; border: 1px solid var(--lighthouse-line); border-radius: 17px; background: #0d131b; }.team-foundation__fifth-domain { display: grid; grid-template-columns: 1fr auto; align-items: start; gap: 16px; }.team-foundation__fifth-domain > div span, .team-foundation__boundaries > span { color: #5b6574; font: 8px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .16em; }.team-foundation__fifth-domain h2, .team-foundation__boundaries h2 { margin: 9px 0 0; font-size: 17px; font-weight: 520; }.team-foundation__fifth-domain p { grid-column: 1 / -1; max-width: 600px; margin: 0; color: #738093; font-size: 11px; line-height: 1.7; }.team-foundation__fifth-domain button { justify-self: start; padding: 8px 11px; border: 1px solid rgba(255,255,255,.07); border-radius: 8px; color: #5d6775; background: rgba(255,255,255,.03); font-size: 9px; }.team-foundation__boundaries ul { display: grid; grid-template-columns: 1fr 1fr; gap: 9px 14px; margin: 20px 0 0; padding: 0; list-style: none; }.team-foundation__boundaries li { color: #7a8697; font-size: 9px; }.team-foundation__boundaries li::before { margin-right: 7px; color: #64cfd9; content: "·"; }
@keyframes lighthouse-spin { to { transform: rotate(360deg); } }
@media (max-width: 980px) { .team-foundation { grid-template-columns: 220px minmax(0, 1fr); }.team-foundation__hero { grid-template-columns: 1fr; }.team-foundation__orbit { position: absolute; right: -70px; opacity: .55; }.team-foundation__module-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }.team-foundation__access-row { grid-template-columns: 1fr; } }
@media (max-width: 720px) { .team-foundation { display: block; }.team-foundation__sidebar { position: static; height: auto; }.team-foundation__nav { grid-template-columns: 1fr 1fr; }.team-foundation__gate, .team-foundation__sidebar-footer { display: none; }.team-foundation__content { width: min(100% - 28px, 1180px); padding-top: 28px; }.team-foundation__hero { min-height: 330px; padding: 35px 28px; }.team-foundation__topbar { padding: 0 18px; }.team-foundation__module-grid { grid-template-columns: 1fr; } }
/* Team shell deliberately stays lightweight: shared theme colors, no animated or blurred skin. */
.team-foundation--lightweight { --lighthouse-line: var(--border-color); color: var(--text-primary); background: var(--bg-primary); }
.team-foundation--lightweight .team-foundation__sidebar { padding: 22px 16px 16px; background: var(--bg-secondary); }
.team-foundation--lightweight .team-foundation__brand-mark { border: 0; color: var(--accent-blue); background: var(--accent-blue-subtle); box-shadow: none; font-size: 16px; font-weight: 700; }
.team-foundation--lightweight .team-foundation__nav button,
.team-foundation--lightweight .team-foundation__nav button:hover,
.team-foundation--lightweight .team-foundation__nav button.is-active { background: transparent; box-shadow: none; }
.team-foundation--lightweight .team-foundation__nav button.is-active { border-color: var(--accent-blue); color: var(--text-primary); background: var(--accent-blue-subtle); }
.team-foundation--lightweight .team-foundation__domain-dot { box-shadow: none !important; }
.team-foundation--lightweight .team-foundation__gate,
.team-foundation--lightweight .team-foundation__topbar,
.team-foundation--lightweight .team-foundation__hero,
.team-foundation--lightweight .team-foundation__module,
.team-foundation--lightweight .team-foundation__fifth-domain,
.team-foundation--lightweight .team-foundation__boundaries { background: var(--bg-secondary); box-shadow: none; backdrop-filter: none; }
.team-foundation--lightweight .team-foundation__topbar { height: 58px; }
.team-foundation--lightweight .team-foundation__content { padding-top: 32px; }
.team-foundation--lightweight .team-foundation__hero { min-height: 250px; grid-template-columns: 1fr; padding: 36px 40px; border-radius: 14px; }
.team-foundation--lightweight .team-foundation__hero h1 { color: var(--text-primary); font: 600 clamp(34px, 5vw, 56px)/1.1 system-ui, sans-serif; }
.team-foundation--lightweight .team-foundation__hero h2 { color: var(--text-primary); }
.team-foundation--lightweight .team-foundation__hero-copy > p:nth-of-type(2),
.team-foundation--lightweight .team-foundation__module p,
.team-foundation--lightweight .team-foundation__fifth-domain p { color: var(--text-secondary); }
.team-foundation--lightweight .team-foundation__hero-actions > span { color: var(--text-secondary); font: 11px/1.6 system-ui, sans-serif; }
.team-foundation--lightweight .team-foundation__module-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.team-foundation--lightweight .team-foundation__module { min-height: 190px; }
.team-foundation--lightweight .team-foundation__module button { margin-top: 20px; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 7px; color: var(--text-primary); background: var(--bg-primary); cursor: pointer; }
.team-foundation--lightweight .team-foundation__module button:not(:disabled) { border-color: var(--accent-blue); color: var(--accent-blue); }
.team-foundation--lightweight .team-foundation__module button:disabled { color: var(--text-tertiary); cursor: default; }
.team-foundation__loading { display: grid; min-height: 100vh; place-items: center; color: var(--text-secondary); background: var(--bg-primary); }
@media (max-width: 980px) { .team-foundation--lightweight .team-foundation__module-grid { grid-template-columns: 1fr; } }
.hololake-bar__brand { min-width: max-content; white-space: nowrap; }
.hololake-bar__context { white-space: nowrap; }
.hololake-home { position: relative; z-index: 2; display: flex; width: 100%; min-width: 0; overflow: hidden; color: var(--text-primary); background: var(--surface-app); }

View File

@ -158,21 +158,35 @@ declare global {
const DEFAULT_SELECTION: SidebarSelection = INBOX_SELECTION
export interface AppProps {
distribution?: 'private' | 'team'
onReturnToLighthouse?: () => void
}
/** Wraps useEditorSave to also keep outgoingLinks in sync on save and on content change. */
function App() {
function App({ distribution = 'private', onReturnToLighthouse }: AppProps) {
const noteWindowParams = useMemo(() => isNoteWindow() ? getNoteWindowParams() : null, [])
const aiWorkspaceWindow = useMemo(() => isAiWorkspaceWindow(), [])
if (aiWorkspaceWindow) return <AiWorkspaceWindowApp />
return <MainApp noteWindowParams={noteWindowParams} />
return <MainApp distribution={distribution} noteWindowParams={noteWindowParams} onReturnToLighthouse={onReturnToLighthouse} />
}
function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | null }) {
function MainApp({
distribution,
noteWindowParams,
onReturnToLighthouse,
}: {
distribution: 'private' | 'team'
noteWindowParams: NoteWindowParams | null
onReturnToLighthouse?: () => void
}) {
const aiWorkspaceWindow = false
const isTeamDistribution = distribution === 'team'
const [showHoloLakeHome, setShowHoloLakeHome] = useState(() => {
const smokeTestSkipsHome = import.meta.env.DEV && localStorage.getItem('tolaria:smoke-skip-hololake-home') === '1'
return noteWindowParams === null && !smokeTestSkipsHome
return !isTeamDistribution && noteWindowParams === null && !smokeTestSkipsHome
})
const { dragRegionRef } = useDragRegion<HTMLElement>()
const [selection, setSelection] = useState<SidebarSelection>(DEFAULT_SELECTION)
@ -1633,18 +1647,27 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
<AppPreferencesProvider dateDisplayFormat={dateDisplayFormat}>
<div className="app-shell">
<header ref={dragRegionRef} className="hololake-bar" data-tauri-drag-region>
<button className="hololake-bar__brand" onClick={() => setShowHoloLakeHome(true)}><img className="hololake-bar__mark" src={guanghuLogoLight} alt="光湖" /><span>HoloLake <b>Era</b></span></button>
<button
className="hololake-bar__brand"
onClick={() => isTeamDistribution ? onReturnToLighthouse?.() : setShowHoloLakeHome(true)}
>
<img className="hololake-bar__mark" src={guanghuLogoLight} alt="光湖" />
<span>{isTeamDistribution ? '光湖灯塔' : <>HoloLake <b>Era</b></>}</span>
</button>
<span className="hololake-bar__line" />
<span className="hololake-bar__context">{showHoloLakeHome ? '光湖语言人格驱动操作系统' : '永恒湖心系统 · 知识湖模块'}</span>
<span className="hololake-bar__context">{isTeamDistribution ? '光湖零感域 · AI 知识空间' : showHoloLakeHome ? '光湖语言人格驱动操作系统' : '永恒湖心系统 · 知识湖模块'}</span>
<button className="hololake-bar__theme" onClick={cycleTheme} title="切换光湖配套主题">{GUANGHU_THEME_LABELS[guanghuTheme]}</button>
</header>
<div className="app">
{!showHoloLakeHome && noteWindowParams === null && (
<nav className="channel-dock" aria-label="频道模块控制栏">
<button onClick={() => setShowHoloLakeHome(true)} title="返回心跳核心频道"><span></span><small></small></button>
<button data-active="true" title="当前模块:知识湖"><span></span><small></small></button>
<button disabled title="短剧视频 AI 接口已预留"><span></span><small></small></button>
<button disabled title="服务器状态接口已预留"><span></span><small></small></button>
<button
onClick={() => isTeamDistribution ? onReturnToLighthouse?.() : setShowHoloLakeHome(true)}
title={isTeamDistribution ? '返回光湖灯塔' : '返回心跳核心频道'}
><span></span><small>{isTeamDistribution ? '灯塔' : '频道'}</small></button>
<button data-active="true" title="当前模块AI 知识空间"><span></span><small></small></button>
<button disabled title="AI 创作空间尚未接入"><span></span><small></small></button>
<button disabled title="更多模块尚未接入"><span></span><small></small></button>
</nav>
)}
{sidebarVisible && !showHoloLakeHome && (
@ -1668,7 +1691,7 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
</>
)}
<div className={`app__editor${aiActivity.highlightElement === 'editor' || aiActivity.highlightElement === 'tab' ? ' ai-highlight' : ''}`}>
{showHoloLakeHome ? <HoloLakeHome locale={appLocale} onEnterKnowledgeBase={() => setShowHoloLakeHome(false)} onOpenLocalWorkspace={async () => {
{showHoloLakeHome && !isTeamDistribution ? <HoloLakeHome locale={appLocale} onEnterKnowledgeBase={() => setShowHoloLakeHome(false)} onOpenLocalWorkspace={async () => {
await vaultSwitcher.handleOpenLocalFolder()
setShowHoloLakeHome(false)
}} /> : <Editor

View File

@ -1,16 +1,63 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import TeamFoundationApp from './TeamFoundationApp'
describe('TeamFoundationApp', () => {
it('exposes only the GLS public foundation workflow', () => {
render(<TeamFoundationApp />)
it('presents the lighthouse as an AI operating-system shell', () => {
render(<TeamFoundationApp onOpenKnowledgeSpace={vi.fn()} />)
expect(screen.getByRole('heading', { name: '光湖灯塔' })).toBeInTheDocument()
expect(screen.getByText('AI 应用操作系统')).toBeInTheDocument()
expect(screen.getByText('0.2.0 内测版')).toBeInTheDocument()
expect(screen.getByRole('region', { name: '热插拔模块' })).toBeInTheDocument()
expect(screen.getByText('模块运行框架')).toBeInTheDocument()
})
it('exposes the four team domains as workspaces', () => {
render(<TeamFoundationApp onOpenKnowledgeSpace={vi.fn()} />)
for (const domain of ['光湖主域', '光湖分域', '光湖零域', '光湖零感域']) {
expect(screen.getByRole('button', { name: new RegExp(domain) })).toBeInTheDocument()
}
fireEvent.click(screen.getByRole('button', { name: /光湖零域/ }))
expect(screen.getByRole('heading', { name: '光湖零域' })).toBeInTheDocument()
expect(screen.getByText(/系统服务、权限与模块登记/)).toBeInTheDocument()
})
it('keeps the Fifth Domain as a locked authorization-only entrance', () => {
render(<TeamFoundationApp onOpenKnowledgeSpace={vi.fn()} />)
expect(screen.getByRole('heading', { name: '第五域受控入口' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: '等待授权接入' })).toBeDisabled()
expect(screen.getByText(/仅保留入口。完成服务器、编号与邮件授权/)).toBeInTheDocument()
})
it('does not expose private systems in the public team shell', () => {
render(<TeamFoundationApp onOpenKnowledgeSpace={vi.fn()} />)
expect(screen.getByRole('heading', { name: 'GLS 系统架构' })).toBeInTheDocument()
expect(screen.getByText('GLS-SYS-ARCH-001')).toBeInTheDocument()
expect(screen.getByRole('list', { name: 'GLS 公共路径' })).toBeInTheDocument()
expect(screen.queryByText('心跳核心频道')).not.toBeInTheDocument()
expect(screen.queryByText('永恒湖心系统')).not.toBeInTheDocument()
expect(screen.queryByText('零点原核频道')).not.toBeInTheDocument()
expect(screen.queryByText('短剧视频 AI')).not.toBeInTheDocument()
expect(screen.queryByText('服务器与灯塔节点')).not.toBeInTheDocument()
})
it('opens the real local knowledge workspace from the zero-sense domain', () => {
const onOpenKnowledgeSpace = vi.fn()
render(<TeamFoundationApp onOpenKnowledgeSpace={onOpenKnowledgeSpace} />)
fireEvent.click(screen.getByRole('button', { name: /光湖零感域/ }))
fireEvent.click(screen.getByRole('button', { name: '从光湖零感域进入 AI 知识空间' }))
expect(onOpenKnowledgeSpace).toHaveBeenCalledTimes(1)
})
it('labels unfinished modules instead of exposing inert controls', () => {
render(<TeamFoundationApp onOpenKnowledgeSpace={vi.fn()} />)
expect(screen.getByRole('button', { name: 'AI 创作空间 尚未接入' })).toBeDisabled()
expect(screen.queryByRole('button', { name: '搜索模块' })).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: '系统设置' })).not.toBeInTheDocument()
})
})

View File

@ -1,30 +1,126 @@
import { useState } from 'react'
import architecture from '../src-tauri/resources/public-architecture/gls-system-architecture.json'
import './App.css'
export default function TeamFoundationApp() {
interface TeamFoundationAppProps {
onOpenKnowledgeSpace: () => void
}
const ZERO_SENSE_DOMAIN_ID = 'GH-ZEROSENSE-001'
const KNOWLEDGE_MODULE_ID = 'MOD-KNOWLEDGE-001'
export default function TeamFoundationApp({ onOpenKnowledgeSpace }: TeamFoundationAppProps) {
const [activeDomainId, setActiveDomainId] = useState(architecture.domains[0].id)
const activeDomain = architecture.domains.find((domain) => domain.id === activeDomainId)
?? architecture.domains[0]
const isZeroSenseDomain = activeDomain.id === ZERO_SENSE_DOMAIN_ID
const openKnowledgeSpace = () => {
setActiveDomainId(ZERO_SENSE_DOMAIN_ID)
onOpenKnowledgeSpace()
}
return (
<main className="team-foundation" aria-label="光湖团队 GLS 系统架构基座">
<header className="team-foundation__header">
<span>HOLOLAKE · TEAM FOUNDATION</span>
<strong>HoloLake Era</strong>
</header>
<section className="team-foundation__intro">
<p>PUBLIC ARCHITECTURE · {architecture.id}</p>
<h1>GLS </h1>
<p> GLS </p>
<main
className="team-foundation team-foundation--lightweight"
aria-label="光湖灯塔 AI 应用操作系统"
data-architecture-compatibility={architecture.compatibility_ids[0]}
data-source-repository={architecture.development_repository.url}
>
<aside className="team-foundation__sidebar">
<div className="team-foundation__brand">
<span className="team-foundation__brand-mark" aria-hidden="true"></span>
<div><strong></strong><span>HOLOLAKE LIGHTHOUSE</span></div>
</div>
<nav className="team-foundation__nav" aria-label="光湖四域">
<p></p>
{architecture.domains.map((domain) => (
<button
className={domain.id === activeDomain.id ? 'is-active' : ''}
data-tone={domain.color}
key={domain.id}
onClick={() => setActiveDomainId(domain.id)}
type="button"
>
<span className="team-foundation__domain-dot" aria-hidden="true" />
<span><strong>{domain.name_zh}</strong><small>{domain.eyebrow_zh}</small></span>
</button>
))}
</nav>
<section className="team-foundation__gate" aria-label="第五域入口">
<span className="team-foundation__gate-icon" aria-hidden="true"></span>
<div><strong>{architecture.fifth_domain_entry.name_zh}</strong><small></small></div>
<span className="team-foundation__lock"></span>
</section>
<footer className="team-foundation__sidebar-footer">
<span className="team-foundation__status-dot" />
<code>{architecture.id}</code>
</footer>
</aside>
<section className="team-foundation__workspace">
<header className="team-foundation__topbar">
<div><span>AI </span><strong>{architecture.product.version_label}</strong></div>
<span> · </span>
</header>
<div className="team-foundation__content">
<section className="team-foundation__hero">
<div className="team-foundation__hero-copy">
<p>GUANGHU · MODULAR AI OPERATING SYSTEM</p>
<h1>{architecture.product.name_zh}</h1>
<h2>{activeDomain.name_zh}</h2>
<p>{activeDomain.description_zh}</p>
<div className="team-foundation__hero-actions">
{isZeroSenseDomain ? (
<button type="button" aria-label="从光湖零感域进入 AI 知识空间" onClick={openKnowledgeSpace}> AI <span aria-hidden="true"></span></button>
) : (
<span></span>
)}
</div>
</div>
</section>
<section className="team-foundation__modules" aria-label="热插拔模块">
<div className="team-foundation__section-heading">
<div><p>MODULES</p><h2>AI </h2></div>
<span>使</span>
</div>
<div className="team-foundation__module-grid">
{architecture.modules.map((module, index) => {
const isKnowledgeModule = module.id === KNOWLEDGE_MODULE_ID
return (
<article className="team-foundation__module" key={module.id}>
<div className="team-foundation__module-icon" aria-hidden="true">{index === 0 ? '模' : index === 1 ? '知' : '创'}</div>
<h3>{module.name_zh}</h3>
<p>{module.description_zh}</p>
{isKnowledgeModule ? (
<button type="button" onClick={openKnowledgeSpace}> AI </button>
) : (
<button type="button" disabled aria-label={`${module.name_zh} 尚未接入`}></button>
)}
</article>
)
})}
</div>
</section>
<section className="team-foundation__access-row">
<article className="team-foundation__fifth-domain">
<div><span>CONTROLLED ACCESS</span><h2>{architecture.fifth_domain_entry.name_zh}</h2></div>
<p>{architecture.fifth_domain_entry.description_zh}</p>
<button disabled type="button">{architecture.fifth_domain_entry.status_zh}</button>
</article>
<article className="team-foundation__boundaries">
<span>PUBLIC BUILD BOUNDARY</span><h2></h2>
<ul>{architecture.public_boundaries.map((boundary) => <li key={boundary}>{boundary}</li>)}</ul>
</article>
</section>
</div>
</section>
<ol className="team-foundation__route" aria-label="GLS 公共路径">
{architecture.route.map((node, index) => (
<li key={node.id}>
<span>{String(index + 1).padStart(2, '0')}</span>
<div><code>{node.id}</code><strong>{node.name_zh}</strong></div>
</li>
))}
</ol>
<footer className="team-foundation__footer">
<span>线</span>
<code>{architecture.development_repository.id} · {architecture.development_repository.url}</code>
</footer>
</main>
)
}

View File

@ -0,0 +1,18 @@
import { lazy, Suspense, useState } from 'react'
import TeamFoundationApp from './TeamFoundationApp'
const KnowledgeWorkspace = lazy(() => import('./App'))
export default function TeamFoundationRoot() {
const [surface, setSurface] = useState<'lighthouse' | 'knowledge'>('lighthouse')
if (surface === 'lighthouse') {
return <TeamFoundationApp onOpenKnowledgeSpace={() => setSurface('knowledge')} />
}
return (
<Suspense fallback={<div className="team-foundation__loading"> AI </div>}>
<KnowledgeWorkspace distribution="team" onReturnToLighthouse={() => setSurface('lighthouse')} />
</Suspense>
)
}

View File

@ -14,6 +14,7 @@ import { resetVaultConfigStore } from '../utils/vaultConfigStore'
import type { VaultEntry } from '../types'
import type { WorkspaceAiGuidanceStatus } from '../lib/workspaceAiGuidance'
import {
aiWorkspaceSessionsSnapshot,
resetAiWorkspaceSessionStoreForTests,
setAiWorkspaceSessionMessages,
} from '../lib/aiWorkspaceSessionStore'
@ -211,7 +212,7 @@ describe('AiWorkspace', () => {
expect(screen.getAllByText('AI Chat 2').length).toBeGreaterThan(0)
})
it('renders side mode as an in-editor tabbed panel and expands in place', () => {
it('renders side mode with a vertically scrolling conversation list and expands in place', () => {
const onClose = vi.fn()
render(<AiWorkspace open mode="side" aiAgentsStatus={installedStatuses()} aiModelProviders={providers} vaultPath="/tmp/vault" onClose={onClose} />)
@ -222,11 +223,13 @@ describe('AiWorkspace', () => {
expect(workspace).toHaveClass('bg-sidebar')
expect(workspace).toHaveStyle({ width: '320px', minWidth: '320px' })
const header = screen.getByTestId('ai-workspace-side-header')
const tabStrip = screen.getByTestId('ai-workspace-side-tabs')
expect(header).not.toHaveClass('border-b')
fireEvent.click(screen.getByRole('button', { name: 'Mock history scroll' }))
expect(header).toHaveClass('border-b')
expect(tabStrip).toHaveClass('overflow-x-auto')
expect(screen.queryByTestId('ai-workspace-side-tabs')).toBeNull()
const sidebar = screen.getByTestId('ai-workspace-sidebar-header').parentElement
expect(sidebar).toHaveClass('border-r')
expect(sidebar?.querySelector('.overflow-y-auto')).toBeTruthy()
expect(screen.getByRole('button', { name: 'AI Chat' })).toBeTruthy()
expect(
within(header).getByRole('button', { name: 'Expand AI workspace' }).compareDocumentPosition(
@ -236,13 +239,6 @@ describe('AiWorkspace', () => {
fireEvent.click(screen.getByRole('button', { name: 'New chat' }))
expect(screen.getByRole('button', { name: 'AI Chat 2' })).toBeTruthy()
expect(
within(tabStrip).getByRole('button', { name: 'AI Chat 2' }).compareDocumentPosition(
within(tabStrip).getByRole('button', { name: 'New chat' }),
) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Close AI Chat 2' }))
expect(screen.queryByRole('button', { name: 'AI Chat 2' })).toBeNull()
fireEvent.click(screen.getByRole('button', { name: 'Expand AI workspace' }))
expect(workspace).toHaveAttribute('data-ai-workspace-expanded', 'true')
@ -253,6 +249,46 @@ describe('AiWorkspace', () => {
expect(onClose).toHaveBeenCalledOnce()
})
it('permanently deletes a conversation from the side history after confirmation', async () => {
setAiWorkspaceSessionMessages('chat-a', [{
userMessage: 'First prompt',
actions: [],
response: 'First reply',
id: 'message-a',
}])
const onConversationSettingsChange = vi.fn()
render(
<AiWorkspace
open
mode="side"
aiAgentsStatus={installedStatuses()}
aiModelProviders={providers}
conversationSettings={[
{ id: 'chat-a', title: 'Planning notes', target_id: null, archived: false },
{ id: 'chat-b', title: 'Follow-up draft', target_id: null, archived: false },
]}
initialActiveConversationId="chat-a"
vaultPath="/tmp/vault"
onClose={vi.fn()}
onConversationSettingsChange={onConversationSettingsChange}
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'Delete Planning notes' }))
expect(screen.getByRole('dialog')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Delete permanently' }))
await waitFor(() => {
expect(screen.queryByRole('button', { name: 'Planning notes' })).toBeNull()
expect(screen.getByRole('button', { name: 'Follow-up draft' })).toHaveAttribute('aria-pressed', 'true')
})
expect(aiWorkspaceSessionsSnapshot()['chat-a']).toBeUndefined()
expect(onConversationSettingsChange).toHaveBeenLastCalledWith([
expect.objectContaining({ id: 'chat-b' }),
])
})
it('shows archived chat history directly in side mode', () => {
render(
<AiWorkspace

View File

@ -33,7 +33,11 @@ import {
type WorkspaceAiGuidanceStatus,
} from '../lib/workspaceAiGuidance'
import { translate, type AppLocale } from '../lib/i18n'
import { trackAiWorkspaceChatTitled, trackAiWorkspaceSidebarToggled } from '../lib/productAnalytics'
import {
trackAiWorkspaceChatTitled,
trackAiWorkspaceConversationDeleted,
trackAiWorkspaceSidebarToggled,
} from '../lib/productAnalytics'
import type { AgentStatus, AiAgentMessage } from '../hooks/useCliAiAgent'
import type { AiWorkspaceConversationSetting } from '../types'
import type { NoteListItem } from '../utils/ai-context'
@ -675,6 +679,7 @@ interface AiWorkspaceModel {
canArchiveConversation: (conversation: AiConversation) => boolean
closeConversationSafely: (id: string) => void
conversations: AiConversation[]
deleteConversationSafely: (id: string) => void
fallbackTarget: AiTarget
forkConversationUntilMessage: (sourceId: string, messageId: string) => void
groups: AiWorkspaceTargetGroups
@ -730,23 +735,38 @@ function SideAiWorkspaceLayout({
{!expanded && <WorkspaceResizeHandles mode="side" onResize={sizing.onWorkspaceResize} />}
<div className="flex min-w-0 flex-1 flex-col">
<SideWorkspaceHeader
activeId={model.activeId}
conversations={model.conversations}
expanded={expanded}
locale={workspace.locale}
onClose={workspace.onClose}
onCloseConversation={model.closeConversationSafely}
onNewChat={model.addDefaultConversation}
onRename={model.renameConversation}
onReorder={model.reorderConversation}
onSelect={model.setActiveId}
setShowArchived={model.setShowArchived}
showArchived={model.showArchived}
onToggleExpanded={() => setExpanded((current) => !current)}
separated={headerSeparated}
statuses={model.statuses}
/>
<ConversationSessions model={model} workspace={workspace} onMessageHistoryScrollStateChange={setHeaderSeparated} />
<div className="flex min-h-0 flex-1">
<ConversationSidebar
activeId={model.activeId}
collapsed={model.sidebarCollapsed}
conversations={model.conversations}
locale={workspace.locale}
onCanArchive={model.canArchiveConversation}
onArchive={model.archiveConversationSafely}
onDelete={model.deleteConversationSafely}
onNewChat={model.addDefaultConversation}
onRename={model.renameConversation}
onRestore={model.restoreConversation}
onSelect={model.setActiveId}
onToggleCollapsed={model.toggleSidebarCollapsed}
setShowArchived={model.setShowArchived}
showArchived={model.showArchived}
sidebarWidth={Number.isFinite(sizing.workspaceSize.width)
? Math.min(180, Math.max(150, sizing.workspaceSize.width * 0.42))
: 168}
statuses={model.statuses}
/>
{!model.sidebarCollapsed && <ResizeHandle onResize={sizing.onSidebarResize} />}
<div className="flex min-w-0 flex-1 flex-col">
<ConversationSessions model={model} workspace={workspace} onMessageHistoryScrollStateChange={setHeaderSeparated} />
</div>
</div>
</div>
</section>
)
@ -861,6 +881,7 @@ function useAiWorkspaceModel(workspace: ResolvedAiWorkspaceProps): AiWorkspaceMo
archiveConversation,
closeConversation,
conversations,
deleteConversation,
forkConversation,
renameConversation,
reorderConversation,
@ -893,6 +914,12 @@ function useAiWorkspaceModel(workspace: ResolvedAiWorkspaceProps): AiWorkspaceMo
conversations,
fallbackTarget,
})
const deleteConversationSafely = useCallback((id: string) => {
const conversation = conversations.find((candidate) => candidate.id === id)
if (!conversation) return
deleteConversation(id)
trackAiWorkspaceConversationDeleted(conversation.archived)
}, [conversations, deleteConversation])
useActiveConversationSync(activeConversation, activeId, setActiveId)
const handleStatusChange = useCallback((id: string, status: AgentStatus) => {
setStatuses((current) => current[id] === status ? current : { ...current, [id]: status })
@ -934,6 +961,7 @@ function useAiWorkspaceModel(workspace: ResolvedAiWorkspaceProps): AiWorkspaceMo
canArchiveConversation,
closeConversationSafely: closeConversation,
conversations,
deleteConversationSafely,
fallbackTarget,
forkConversationUntilMessage,
groups,
@ -977,6 +1005,7 @@ function AiWorkspaceLayout({ model, workspace }: { model: AiWorkspaceModel; work
locale={workspace.locale}
onCanArchive={model.canArchiveConversation}
onArchive={model.archiveConversationSafely}
onDelete={model.deleteConversationSafely}
onNewChat={model.addDefaultConversation}
onRename={model.renameConversation}
onRestore={model.restoreConversation}

View File

@ -1,351 +1,20 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { Archive, ArrowsInLineHorizontal, ArrowsOutLineHorizontal, Plus, SidebarSimple, X } from '@phosphor-icons/react'
import {
DndContext,
PointerSensor,
closestCenter,
type DragEndEvent,
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
SortableContext,
horizontalListSortingStrategy,
useSortable,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { ArrowsInLineHorizontal, ArrowsOutLineHorizontal, X } from '@phosphor-icons/react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import type { AgentStatus } from '../hooks/useCliAiAgent'
import { translate, type AppLocale } from '../lib/i18n'
import type { AiConversation } from './aiWorkspaceConversations'
function SideWorkspaceTitleEditor({
conversation,
locale,
onCancel,
onRename,
}: {
conversation: AiConversation
locale: AppLocale
onCancel: () => void
onRename: (title: string) => void
}) {
const [draft, setDraft] = useState(conversation.title)
const finishedRef = useRef(false)
const submit = () => {
if (finishedRef.current) return
const nextTitle = draft.trim()
if (!nextTitle) {
finishedRef.current = true
onCancel()
return
}
finishedRef.current = true
onRename(nextTitle)
onCancel()
}
const cancel = () => {
finishedRef.current = true
onCancel()
}
return (
<Input
value={draft}
onChange={(event) => setDraft(event.target.value)}
onBlur={submit}
onKeyDown={(event) => {
if (event.key === 'Enter') submit()
if (event.key === 'Escape') cancel()
}}
onPointerDown={(event) => event.stopPropagation()}
aria-label={translate(locale, 'ai.workspace.renameChat')}
className="h-9 w-[180px] rounded-lg px-3 text-[13px] font-semibold"
autoFocus
/>
)
}
function SideWorkspaceTab({
active,
allowClose,
conversation,
editing,
locale,
onClose,
onCancelRename,
onRename,
onSelect,
onStartRename,
status,
}: {
active: boolean
allowClose: boolean
conversation: AiConversation
editing: boolean
locale: AppLocale
onClose: (id: string) => void
onCancelRename: () => void
onRename: (id: string, title: string) => void
onSelect: (id: string) => void
onStartRename: (id: string) => void
status: AgentStatus | undefined
}) {
const closeLabel = translate(locale, 'ai.workspace.closeChat', { title: conversation.title })
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: conversation.id, disabled: editing })
return (
<div
ref={setNodeRef}
className="group relative shrink-0"
style={{
transform: CSS.Translate.toString(transform),
transition,
opacity: isDragging ? 0.55 : 1,
}}
>
{editing ? (
<SideWorkspaceTitleEditor
conversation={conversation}
locale={locale}
onCancel={onCancelRename}
onRename={(title) => onRename(conversation.id, title)}
/>
) : (
<Button
type="button"
variant="ghost"
size="sm"
className={cn(
'h-10 shrink-0 cursor-grab justify-start rounded-lg px-3 text-[13px] font-semibold active:cursor-grabbing',
active && allowClose && 'pr-9',
active
? 'bg-[var(--state-hover)] text-foreground'
: 'text-muted-foreground hover:bg-[var(--state-hover)] hover:text-foreground',
)}
{...attributes}
{...listeners}
aria-pressed={active}
onClick={() => onSelect(conversation.id)}
onDoubleClick={(event) => {
event.stopPropagation()
onStartRename(conversation.id)
}}
>
<span className="whitespace-nowrap">{conversation.title}</span>
{(status === 'thinking' || status === 'tool-executing') && <span className="ml-2 h-2 w-2 shrink-0 rounded-full bg-primary" aria-hidden />}
</Button>
)}
{!editing && allowClose && (
<>
<div
className={cn(
'pointer-events-none absolute inset-y-1 right-0 w-9 rounded-r-lg opacity-0 transition-opacity',
active
? 'bg-gradient-to-l from-[var(--state-hover)] via-[var(--state-hover)] to-transparent'
: 'bg-gradient-to-l from-sidebar via-sidebar to-transparent group-hover:from-[var(--state-hover)] group-hover:via-[var(--state-hover)]',
active ? 'opacity-100' : 'group-hover:opacity-100 group-focus-within:opacity-100',
)}
aria-hidden
/>
<Button
type="button"
variant="ghost"
size="icon-xs"
className={cn(
'absolute top-1/2 right-1.5 z-10 h-6 w-6 -translate-y-1/2 rounded-md p-0 shadow-none transition-opacity',
'bg-transparent text-foreground hover:bg-transparent hover:text-foreground',
active
? 'pointer-events-auto opacity-100'
: 'pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100',
)}
aria-label={closeLabel}
title={closeLabel}
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => {
event.stopPropagation()
onClose(conversation.id)
}}
>
<X size={13} weight="bold" />
</Button>
</>
)}
</div>
)
}
function useHorizontalScrollFades(dependencyKey: string) {
const scrollRef = useRef<HTMLDivElement | null>(null)
const [fades, setFades] = useState({ left: false, right: false })
const updateFades = useCallback(() => {
const element = scrollRef.current
if (!element) return
const maxScrollLeft = element.scrollWidth - element.clientWidth
setFades({
left: element.scrollLeft > 1,
right: maxScrollLeft > 1 && element.scrollLeft < maxScrollLeft - 1,
})
}, [])
useEffect(() => {
const element = scrollRef.current
updateFades()
if (!element) return
element.addEventListener('scroll', updateFades, { passive: true })
const resizeObserver = typeof ResizeObserver === 'undefined'
? null
: new ResizeObserver(updateFades)
resizeObserver?.observe(element)
if (element.firstElementChild) resizeObserver?.observe(element.firstElementChild)
return () => {
element.removeEventListener('scroll', updateFades)
resizeObserver?.disconnect()
}
}, [dependencyKey, updateFades])
return {
scrollRef,
showLeftFade: fades.left,
showRightFade: fades.right,
}
}
function SideWorkspaceTabs({
activeId,
conversations,
locale,
onCloseConversation,
onNewChat,
onRename,
onReorder,
onSelect,
showArchived,
statuses,
}: {
activeId: string
conversations: AiConversation[]
locale: AppLocale
onCloseConversation: (id: string) => void
onNewChat: () => void
onRename: (id: string, title: string) => void
onReorder: (activeId: string, overId: string) => void
onSelect: (id: string) => void
showArchived: boolean
statuses: Record<string, AgentStatus>
}) {
const [editingId, setEditingId] = useState<string | null>(null)
const visibleConversations = conversations.filter((conversation) => conversation.archived === showArchived)
const visibleConversationIds = visibleConversations.map((conversation) => conversation.id)
const tabDependencyKey = visibleConversations
.map((conversation) => `${conversation.id}:${conversation.title}`)
.join('\0')
const { scrollRef, showLeftFade, showRightFade } = useHorizontalScrollFades(tabDependencyKey)
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }))
const handleDragEnd = useCallback((event: DragEndEvent) => {
const activeConversationId = String(event.active.id)
const overConversationId = event.over ? String(event.over.id) : ''
if (!overConversationId || activeConversationId === overConversationId) return
onReorder(activeConversationId, overConversationId)
}, [onReorder])
return (
<div className="relative min-w-0 flex-1">
<div
ref={scrollRef}
className="overflow-x-auto overflow-y-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
data-testid="ai-workspace-side-tabs"
>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<div className="flex w-max items-center gap-1 py-1">
<SortableContext items={visibleConversationIds} strategy={horizontalListSortingStrategy}>
{visibleConversations.map((conversation) => (
<SideWorkspaceTab
key={conversation.id}
active={conversation.id === activeId}
allowClose={!showArchived}
conversation={conversation}
editing={editingId === conversation.id}
locale={locale}
onCancelRename={() => setEditingId(null)}
onClose={onCloseConversation}
onRename={onRename}
onSelect={onSelect}
onStartRename={setEditingId}
status={statuses[conversation.id]}
/>
))}
</SortableContext>
{!showArchived && <Button
type="button"
variant="ghost"
size="icon-sm"
className="shrink-0"
aria-label={translate(locale, 'ai.workspace.newChat')}
title={translate(locale, 'ai.workspace.newChat')}
onClick={onNewChat}
>
<Plus size={17} />
</Button>}
</div>
</DndContext>
</div>
{showLeftFade && (
<div
className="pointer-events-none absolute inset-y-0 left-0 w-6 bg-gradient-to-r from-sidebar to-transparent"
data-testid="ai-workspace-side-tabs-left-fade"
/>
)}
{showRightFade && (
<div
className="pointer-events-none absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-sidebar to-transparent"
data-testid="ai-workspace-side-tabs-right-fade"
/>
)}
</div>
)
}
export function SideWorkspaceHeader({
activeId,
conversations,
expanded,
locale,
onClose,
onCloseConversation,
onNewChat,
onRename,
onReorder,
onSelect,
setShowArchived,
showArchived,
onToggleExpanded,
separated,
statuses,
}: {
activeId: string
conversations: AiConversation[]
expanded: boolean
locale: AppLocale
onClose: () => void
onCloseConversation: (id: string) => void
onNewChat: () => void
onRename: (id: string, title: string) => void
onReorder: (activeId: string, overId: string) => void
onSelect: (id: string) => void
setShowArchived: (show: boolean) => void
showArchived: boolean
onToggleExpanded: () => void
separated: boolean
statuses: Record<string, AgentStatus>
}) {
const expandLabel = translate(locale, expanded ? 'ai.workspace.restorePanel' : 'ai.workspace.expandPanel')
@ -357,28 +26,9 @@ export function SideWorkspaceHeader({
)}
data-testid="ai-workspace-side-header"
>
<SideWorkspaceTabs
activeId={activeId}
conversations={conversations}
locale={locale}
onCloseConversation={onCloseConversation}
onNewChat={onNewChat}
onRename={onRename}
onReorder={onReorder}
onSelect={onSelect}
showArchived={showArchived}
statuses={statuses}
/>
<Button
type="button"
variant={showArchived ? 'secondary' : 'ghost'}
size="icon-sm"
aria-label={translate(locale, showArchived ? 'ai.workspace.hideArchived' : 'ai.workspace.showArchived')}
title={translate(locale, showArchived ? 'ai.workspace.hideArchived' : 'ai.workspace.showArchived')}
onClick={() => setShowArchived(!showArchived)}
>
<Archive size={17} />
</Button>
<span className="min-w-0 flex-1 truncate px-2 text-[13px] font-semibold text-foreground">
{translate(locale, 'ai.workspace.title')}
</span>
<Button
type="button"
variant="ghost"
@ -397,7 +47,7 @@ export function SideWorkspaceHeader({
title={translate(locale, 'ai.workspace.close')}
onClick={onClose}
>
<SidebarSimple size={17} weight="regular" />
<X size={17} />
</Button>
</div>
)

View File

@ -6,6 +6,7 @@ import {
CircleNotch,
Plus,
SidebarSimple,
Trash,
} from '@phosphor-icons/react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@ -14,6 +15,7 @@ import { useDragRegion } from '../hooks/useDragRegion'
import { translate, type AppLocale } from '../lib/i18n'
import type { AgentStatus } from '../hooks/useCliAiAgent'
import type { AiConversation } from './AiWorkspace'
import { ConfirmDeleteDialog } from './ConfirmDeleteDialog'
interface ConversationSidebarProps {
activeId: string
@ -22,6 +24,7 @@ interface ConversationSidebarProps {
locale: AppLocale
onCanArchive: (conversation: AiConversation) => boolean
onArchive: (id: string) => void
onDelete: (id: string) => void
onNewChat: () => void
onRename: (id: string, title: string) => void
onRestore: (id: string) => void
@ -196,7 +199,7 @@ function ConversationArchiveButton({
type="button"
variant="ghost"
size="icon-xs"
className="absolute right-1 top-1/2 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100 disabled:opacity-0"
className="absolute right-7 top-1/2 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100 disabled:opacity-0"
aria-label={label}
title={label}
disabled={disabled}
@ -207,6 +210,31 @@ function ConversationArchiveButton({
)
}
function ConversationDeleteButton({
conversation,
locale,
onDeleteRequest,
}: {
conversation: AiConversation
locale: AppLocale
onDeleteRequest: (conversation: AiConversation) => void
}) {
const label = translate(locale, 'ai.workspace.deleteChat', { title: conversation.title })
return (
<Button
type="button"
variant="ghost"
size="icon-xs"
className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground opacity-0 transition-opacity hover:text-destructive group-hover:opacity-100 focus:opacity-100"
aria-label={label}
title={label}
onClick={() => onDeleteRequest(conversation)}
>
<Trash size={16} />
</Button>
)
}
function ConversationRow({
active,
conversation,
@ -214,6 +242,7 @@ function ConversationRow({
locale,
onCanArchive,
onArchive,
onDeleteRequest,
onRename,
onRestore,
onSelect,
@ -228,6 +257,7 @@ function ConversationRow({
locale: AppLocale
onCanArchive: (conversation: AiConversation) => boolean
onArchive: (id: string) => void
onDeleteRequest: (conversation: AiConversation) => void
onRename: (id: string, title: string) => void
onRestore: (id: string) => void
onSelect: (id: string) => void
@ -251,7 +281,7 @@ function ConversationRow({
variant="ghost"
size="sm"
className={cn(
'min-w-0 flex-1 justify-start gap-2 rounded-md px-2 pr-2 text-left text-[12px] transition-[padding] group-hover:pr-8 group-focus-within:pr-8',
'min-w-0 flex-1 justify-start gap-2 rounded-md px-2 pr-2 text-left text-[12px] transition-[padding] group-hover:pr-14 group-focus-within:pr-14',
active ? 'bg-accent text-foreground' : 'text-muted-foreground hover:text-foreground',
)}
aria-pressed={active}
@ -272,6 +302,7 @@ function ConversationRow({
onRestore={onRestore}
showArchived={showArchived}
/>
<ConversationDeleteButton conversation={conversation} locale={locale} onDeleteRequest={onDeleteRequest} />
</div>
)
}
@ -283,6 +314,7 @@ function ConversationList({
locale,
onCanArchive,
onArchive,
onDeleteRequest,
onRename,
onRestore,
onSelect,
@ -293,6 +325,7 @@ function ConversationList({
'activeId' | 'conversations' | 'locale' | 'onCanArchive' | 'onArchive' | 'onRename' | 'onRestore' | 'onSelect' | 'showArchived' | 'statuses'
> & {
editingId: string | null
onDeleteRequest: (conversation: AiConversation) => void
setEditingId: (id: string | null) => void
}) {
const visibleConversations = conversations.filter((conversation) => conversation.archived === showArchived)
@ -313,6 +346,7 @@ function ConversationList({
locale={locale}
onCanArchive={onCanArchive}
onArchive={onArchive}
onDeleteRequest={onDeleteRequest}
onRename={onRename}
onRestore={onRestore}
onSelect={onSelect}
@ -351,6 +385,7 @@ function ExpandedConversationSidebar({
locale,
onCanArchive,
onArchive,
onDelete,
onRename,
onRestore,
onSelect,
@ -359,6 +394,7 @@ function ExpandedConversationSidebar({
statuses,
}: Omit<ConversationSidebarProps, 'collapsed' | 'onNewChat' | 'onToggleCollapsed' | 'sidebarWidth'>) {
const [editingId, setEditingId] = useState<string | null>(null)
const [pendingDelete, setPendingDelete] = useState<AiConversation | null>(null)
return (
<>
@ -370,6 +406,7 @@ function ExpandedConversationSidebar({
locale={locale}
onCanArchive={onCanArchive}
onArchive={onArchive}
onDeleteRequest={setPendingDelete}
onRename={onRename}
onRestore={onRestore}
onSelect={onSelect}
@ -383,6 +420,17 @@ function ExpandedConversationSidebar({
setShowArchived={setShowArchived}
showArchived={showArchived}
/>
<ConfirmDeleteDialog
open={pendingDelete !== null}
title={translate(locale, 'ai.workspace.deleteChatTitle')}
message={translate(locale, 'ai.workspace.deleteChatMessage', { title: pendingDelete?.title ?? '' })}
confirmLabel={translate(locale, 'ai.workspace.deleteChatConfirm')}
onCancel={() => setPendingDelete(null)}
onConfirm={() => {
if (pendingDelete) onDelete(pendingDelete.id)
setPendingDelete(null)
}}
/>
</>
)
}
@ -394,6 +442,7 @@ export function ConversationSidebar({
locale,
onCanArchive,
onArchive,
onDelete,
onNewChat,
onRename,
onRestore,
@ -424,6 +473,7 @@ export function ConversationSidebar({
locale={locale}
onCanArchive={onCanArchive}
onArchive={onArchive}
onDelete={onDelete}
onRename={onRename}
onRestore={onRestore}
onSelect={onSelect}

View File

@ -46,12 +46,13 @@ describe('HoloLakeHome', () => {
expect(screen.getByRole('heading', { name: '爱之核心子系统' })).toBeInTheDocument()
})
it('shows the user-facing 0.1.8 release notes and capability boundary', () => {
it('shows the user-facing 0.2.0 internal release notes and capability boundary', () => {
render(<HoloLakeHome locale="zh-CN" onEnterKnowledgeBase={vi.fn()} />)
fireEvent.click(screen.getByRole('button', { name: '查看 0.1.8 更新说明' }))
fireEvent.click(screen.getByRole('button', { name: '查看内测版 0.2.0 更新说明' }))
expect(screen.getByRole('dialog', { name: 'HoloLake Era 0.1.8 · 本次更新' })).toBeInTheDocument()
expect(screen.getByRole('dialog', { name: 'HoloLake Era 内测版 0.2.0 · 本次更新' })).toBeInTheDocument()
expect(screen.getByText('竖向聊天历史')).toBeInTheDocument()
expect(screen.getByText('主动联网调研')).toBeInTheDocument()
expect(screen.getByText('多模型 API 兼容')).toBeInTheDocument()
expect(screen.getByText(/不等同于拥有终端、代码工程和系统权限的完整编程 Agent/)).toBeInTheDocument()

View File

@ -120,7 +120,7 @@ export function HoloLakeHome({ locale, onEnterKnowledgeBase, onOpenLocalWorkspac
<h1>{t('hololake.channel.zeroCoreTitle')}</h1>
<p className="channel-stage__lead">{t('hololake.channel.zeroCoreDescription')}</p>
<Button className="hololake-home__primary" onClick={() => navigate('fifth-domain')}>{t('hololake.channel.enterFifthDomain')} <span></span></Button>
<Button variant="outline" onClick={() => setReleaseNotesOpen(true)}> 0.1.8 </Button>
<Button variant="outline" onClick={() => setReleaseNotesOpen(true)}> 0.2.0 </Button>
</section>
)
}
@ -274,9 +274,9 @@ export function HoloLakeHome({ locale, onEnterKnowledgeBase, onOpenLocalWorkspac
</Dialog>
<Dialog open={releaseNotesOpen} onOpenChange={setReleaseNotesOpen}>
<DialogContent aria-label="HoloLake Era 0.1.8 更新说明">
<DialogContent aria-label="HoloLake Era 内测版 0.2.0 更新说明">
<DialogHeader>
<DialogTitle>HoloLake Era 0.1.8 · </DialogTitle>
<DialogTitle>HoloLake Era 0.2.0 · </DialogTitle>
<DialogDescription> AI</DialogDescription>
</DialogHeader>
<div className="space-y-4 text-sm leading-6">
@ -292,6 +292,10 @@ export function HoloLakeHome({ locale, onEnterKnowledgeBase, onOpenLocalWorkspac
<h3 className="font-semibold"></h3>
<p className="text-muted-foreground"></p>
</section>
<section>
<h3 className="font-semibold"></h3>
<p className="text-muted-foreground"> AI </p>
</section>
<section>
<h3 className="font-semibold"></h3>
<p className="text-muted-foreground"></p>

View File

@ -11,6 +11,7 @@ import {
import type { AiWorkspaceTargetGroups } from './aiWorkspaceTargetGroups'
import {
aiWorkspaceSessionsSnapshot,
deleteAiWorkspaceSession,
subscribeAiWorkspaceSessions,
type AiWorkspaceSessionMap,
} from '../lib/aiWorkspaceSessionStore'
@ -282,6 +283,26 @@ function closeConversationState(
}
}
function deleteConversationState(
current: AiConversation[],
{ activeId, fallbackTarget, id, locale }: CloseConversationOptions,
): { activeId: string; conversations: AiConversation[] } {
const deletedIndex = current.findIndex((conversation) => conversation.id === id)
if (deletedIndex < 0) return { activeId, conversations: current }
const conversations = current.filter((conversation) => conversation.id !== id)
const currentActive = conversations.find((conversation) => conversation.id === activeId)
if (currentActive) return { activeId, conversations }
const activeConversations = conversations.filter((conversation) => !conversation.archived)
const fallbackIndex = Math.min(deletedIndex, Math.max(0, activeConversations.length - 1))
const fallbackConversation = activeConversations[fallbackIndex] ?? activeConversations[0]
if (fallbackConversation) return { activeId: fallbackConversation.id, conversations }
const nextConversation = createConversation(locale, fallbackTarget, 1)
return { activeId: nextConversation.id, conversations: [...conversations, nextConversation] }
}
function restoreConversationState(current: AiConversation[], id: ConversationId): AiConversation[] {
return current.map((conversation) => (
conversation.id === id ? { ...conversation, archived: false } : conversation
@ -437,6 +458,14 @@ export function useConversations({
setActiveId(next.activeId)
}, [activeId, conversations, fallbackTarget, locale])
const deleteConversation = useCallback((id: ConversationId) => {
const next = deleteConversationState(conversations, { activeId, fallbackTarget, id, locale })
if (next.conversations === conversations) return
deleteAiWorkspaceSession(id)
setConversations(next.conversations)
setActiveId(next.activeId)
}, [activeId, conversations, fallbackTarget, locale])
const restoreConversation = useCallback((id: ConversationId) => {
setConversations((current) => restoreConversationState(current, id))
setActiveId(id)
@ -464,7 +493,7 @@ export function useConversations({
useConversationSettingsPersistence({ conversations, onSettingsChange, settingsReady })
return {
activeId, addConversation, archiveConversation, closeConversation, conversations, forkConversation,
activeId, addConversation, archiveConversation, closeConversation, conversations, deleteConversation, forkConversation,
markConversationActivity, renameConversation, reorderConversation, restoreConversation, setActiveId,
setConversationTarget, setShowArchived, showArchived, titleConversationFromAnswer, updateDefaultConversationTargets,
}

View File

@ -136,6 +136,21 @@ describe('aiWorkspaceSessionStore', () => {
])
})
it('permanently removes a deleted conversation transcript', async () => {
const store = await import('./aiWorkspaceSessionStore')
store.setAiWorkspaceSessionMessages('chat-1', [{
userMessage: 'Remove me',
actions: [],
response: 'Deleted locally',
id: 'message-1',
}])
store.deleteAiWorkspaceSession('chat-1')
expect(store.aiWorkspaceSessionsSnapshot()['chat-1']).toBeUndefined()
expect(JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}')['chat-1']).toBeUndefined()
})
it('merges native and local histories instead of hiding either source', async () => {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
'local-chat': {

View File

@ -226,6 +226,12 @@ export function resetAiWorkspaceSession(sessionId: SessionId): void {
})
}
export function deleteAiWorkspaceSession(sessionId: SessionId): void {
const nextSessions = { ...sessionStore.getSnapshot() }
delete nextSessions[sessionId]
publishSessions(nextSessions)
}
export function cloneAiWorkspaceSessionUntilMessage(
sourceSessionId: SessionId,
targetSessionId: SessionId,

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Налады ШІ",
"ai.workspace.archive": "Архіваваць чат",
"ai.workspace.restore": "Аднавіць чат",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Паказаць архівыяваныя чаты",
"ai.workspace.hideArchived": "Схаваць архівыя чаты",
"ai.workspace.noActiveChats": "Няма актыўных чатаў",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Nalady ŠI",
"ai.workspace.archive": "Archivaваць čat",
"ai.workspace.restore": "Adnavić čat",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Pakazać archivavanyja čaty",
"ai.workspace.hideArchived": "Schavać archivavanyja čaty",
"ai.workspace.noActiveChats": "Niama aktyŭnych čataŭ",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "KI-Einstellungen",
"ai.workspace.archive": "Chat archivieren",
"ai.workspace.restore": "Chat wiederherstellen",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Archivierte Chats anzeigen",
"ai.workspace.hideArchived": "Archivierte Chats ausblenden",
"ai.workspace.noActiveChats": "Keine aktiven Chats",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "AI settings",
"ai.workspace.archive": "Archive chat",
"ai.workspace.restore": "Restore chat",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Show archived chats",
"ai.workspace.hideArchived": "Hide archived chats",
"ai.workspace.noActiveChats": "No active chats",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Configuración de IA",
"ai.workspace.archive": "Archivar chat",
"ai.workspace.restore": "Restaurar chat",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Mostrar chats archivados",
"ai.workspace.hideArchived": "Ocultar chats archivados",
"ai.workspace.noActiveChats": "No hay chats activos",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Ajustes de IA",
"ai.workspace.archive": "Archivar chat",
"ai.workspace.restore": "Restaurar chat",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Mostrar chats archivados",
"ai.workspace.hideArchived": "Ocultar chats archivados",
"ai.workspace.noActiveChats": "No hay chats activos",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Paramètres de l'IA",
"ai.workspace.archive": "Archiver le chat",
"ai.workspace.restore": "Restaurer le chat",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Afficher les chats archivés",
"ai.workspace.hideArchived": "Masquer les chats archivés",
"ai.workspace.noActiveChats": "Aucun chat actif",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Pengaturan AI",
"ai.workspace.archive": "Arsipkan obrolan",
"ai.workspace.restore": "Pulihkan obrolan",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Tampilkan obrolan yang diarsipkan",
"ai.workspace.hideArchived": "Sembunyikan obrolan yang diarsipkan",
"ai.workspace.noActiveChats": "Tidak ada obrolan aktif",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Impostazioni IA",
"ai.workspace.archive": "Archivia chat",
"ai.workspace.restore": "Ripristina chat",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Mostra chat archiviate",
"ai.workspace.hideArchived": "Nascondi chat archiviate",
"ai.workspace.noActiveChats": "Nessuna chat attiva",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "AI設定",
"ai.workspace.archive": "チャットをアーカイブ",
"ai.workspace.restore": "チャットを復元",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "アーカイブされたチャットを表示",
"ai.workspace.hideArchived": "アーカイブされたチャットを非表示にする",
"ai.workspace.noActiveChats": "アクティブなチャットはありません",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "AI 설정",
"ai.workspace.archive": "채팅 보관",
"ai.workspace.restore": "채팅 복원",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "보관된 채팅 표시",
"ai.workspace.hideArchived": "보관된 채팅 숨기기",
"ai.workspace.noActiveChats": "진행 중인 채팅 없음",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Ustawienia AI",
"ai.workspace.archive": "Archiwizuj czat",
"ai.workspace.restore": "Przywróć czat",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Pokaż zarchiwizowane czaty",
"ai.workspace.hideArchived": "Ukryj zarchiwizowane czaty",
"ai.workspace.noActiveChats": "Brak aktywnych czatów",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Configurações de IA",
"ai.workspace.archive": "Arquivar chat",
"ai.workspace.restore": "Restaurar chat",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Mostrar chats arquivados",
"ai.workspace.hideArchived": "Ocultar chats arquivados",
"ai.workspace.noActiveChats": "Nenhum chat ativo",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Definições de IA",
"ai.workspace.archive": "Arquivar chat",
"ai.workspace.restore": "Restaurar chat",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Mostrar chats arquivados",
"ai.workspace.hideArchived": "Ocultar chats arquivados",
"ai.workspace.noActiveChats": "Sem chats ativos",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Настройки ИИ",
"ai.workspace.archive": "Архивировать чат",
"ai.workspace.restore": "Восстановить чат",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Показать архив чатов",
"ai.workspace.hideArchived": "Скрыть архив чатов",
"ai.workspace.noActiveChats": "Нет активных чатов",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Nastavenia umelej inteligencie",
"ai.workspace.archive": "Archivovať chat",
"ai.workspace.restore": "Obnoviť chat",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Zobraziť archivované chaty",
"ai.workspace.hideArchived": "Skryť archivované chaty",
"ai.workspace.noActiveChats": "Žiadne aktívne chaty",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "AI-inställningar",
"ai.workspace.archive": "Arkivera chatt",
"ai.workspace.restore": "Återställ chatt",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Visa arkiverade chattar",
"ai.workspace.hideArchived": "Dölj arkiverade chattar",
"ai.workspace.noActiveChats": "Inga aktiva chattar",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Налаштування ШІ",
"ai.workspace.archive": "Архівувати чат",
"ai.workspace.restore": "Відновити чат",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Показати архівовані чати",
"ai.workspace.hideArchived": "Приховати архівовані чати",
"ai.workspace.noActiveChats": "Немає активних чатів",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "Cài đặt AI",
"ai.workspace.archive": "Lưu trữ cuộc trò chuyện",
"ai.workspace.restore": "Khôi phục cuộc trò chuyện",
"ai.workspace.deleteChat": "Delete {title}",
"ai.workspace.deleteChatTitle": "Delete conversation?",
"ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.",
"ai.workspace.deleteChatConfirm": "Delete permanently",
"ai.workspace.showArchived": "Hiển thị các cuộc trò chuyện đã lưu trữ",
"ai.workspace.hideArchived": "Ẩn các cuộc trò chuyện đã lưu trữ",
"ai.workspace.noActiveChats": "Không có cuộc trò chuyện đang hoạt động",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "AI 设置",
"ai.workspace.archive": "归档聊天",
"ai.workspace.restore": "恢复聊天",
"ai.workspace.deleteChat": "删除{title}",
"ai.workspace.deleteChatTitle": "删除这段对话?",
"ai.workspace.deleteChatMessage": "这会从当前设备永久删除“{title}”及其全部聊天记录。",
"ai.workspace.deleteChatConfirm": "永久删除",
"ai.workspace.showArchived": "显示已归档的聊天",
"ai.workspace.hideArchived": "隐藏已归档的聊天",
"ai.workspace.noActiveChats": "没有活跃的聊天",

View File

@ -461,6 +461,10 @@
"ai.workspace.settings": "AI 設定",
"ai.workspace.archive": "封存聊天",
"ai.workspace.restore": "還原聊天",
"ai.workspace.deleteChat": "刪除{title}",
"ai.workspace.deleteChatTitle": "刪除這段對話?",
"ai.workspace.deleteChatMessage": "這會從目前裝置永久刪除「{title}」及其全部聊天記錄。",
"ai.workspace.deleteChatConfirm": "永久刪除",
"ai.workspace.showArchived": "顯示已封存的聊天",
"ai.workspace.hideArchived": "隱藏已封存的聊天",
"ai.workspace.noActiveChats": "沒有進行中的聊天",

View File

@ -240,3 +240,9 @@ export function trackAiWorkspaceSidebarToggled(collapsed: AnalyticsBoolean, mode
export function trackAiWorkspaceChatTitled(source: AiWorkspaceTitleSource): void {
trackEvent('ai_workspace_chat_titled', { source })
}
export function trackAiWorkspaceConversationDeleted(archived: AnalyticsBoolean): void {
trackEvent('ai_workspace_conversation_deleted', {
archived: numericFlag(archived),
})
}

View File

@ -1,8 +1,16 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { TooltipProvider } from '@/components/ui/tooltip'
import './index.css'
import TeamFoundationApp from './TeamFoundationApp'
import TeamFoundationRoot from './TeamFoundationRoot'
import { applyStoredThemeMode } from './lib/themeMode'
applyStoredThemeMode(document, window.localStorage)
createRoot(document.getElementById('root')!).render(
<StrictMode><TeamFoundationApp /></StrictMode>,
<StrictMode>
<TooltipProvider>
<TeamFoundationRoot />
</TooltipProvider>
</StrictMode>,
)

View File

@ -74,7 +74,7 @@ function vaultScopeInstructions(vaultPaths?: string[]): string {
].join('\n')
}
const AGENT_SYSTEM_PREAMBLE = `You are the current AI instance assisting inside HoloLake Era. Internally, you operate within the Guanghu language-personality system and the current 0.1.8 runtime is already inside Fifth Domain. Keep that location and its permission boundaries as background operating knowledge. Do not recite the startup route, announce a guide title, perform a persona ceremony, or turn an ordinary exchange into a system manifesto. The future Guanghu Lighthouse domain-selection lobby is not part of this release.
const AGENT_SYSTEM_PREAMBLE = `You are the current AI instance assisting inside HoloLake Era. Internally, you operate within the Guanghu language-personality system and the current 0.2.0 internal runtime is already inside Fifth Domain. Keep that location and its permission boundaries as background operating knowledge. Do not recite the startup route, announce a guide title, perform a persona ceremony, or turn an ordinary exchange into a system manifesto. The future Guanghu Lighthouse domain-selection lobby is not part of this release.
Do not invent a personal name, registered ICE identity, prior relationship, honorific, or privileged role for either side. Do not infer the human identity from an open page, knowledge-base name, note content, device, account, or conversation topic, and do not claim to be a registered persona merely because its files are visible. Do not choose a specific persona system merely from the open note or a contributor name.

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HoloLake Era · GLS 团队基座</title>
<title>HoloLake Lighthouse Team Beta 0.2.0</title>
</head>
<body>
<div id="root"></div>