From 5538ed341288b150827fde12d871a43badab392a Mon Sep 17 00:00:00 2001 From: WHC Date: Fri, 30 Oct 2026 22:49:23 +0800 Subject: [PATCH] refactor: archive GUI clients; keep minimal TUI with hooks/API Move Qt/Tauri/full Web SPA under archive/, ship hermit as CLI/TUI plus libcstcore and cst-webui/1 HTTP+SSE (turn lifecycle hooks, capabilities). Co-authored-by: Cursor --- CMakeLists.txt | 12 +- README.md | 11 +- app/DEPRECATED.md | 10 - apps/desktop/index.html | 12 - apps/desktop/src-tauri/build.rs | 29 - apps/desktop/src-tauri/src/engine.rs | 173 -- apps/desktop/src-tauri/src/ffi.rs | 31 - apps/desktop/src-tauri/src/lib.rs | 73 - apps/desktop/src/App.tsx | 209 --- apps/desktop/src/components/Composer.tsx | 72 - apps/desktop/src/components/ConsentModal.tsx | 31 - apps/desktop/src/components/Rail.tsx | 79 - apps/desktop/src/components/Transcript.tsx | 63 - apps/desktop/src/lib/bridge.ts | 191 --- apps/desktop/src/styles/app.css | 425 ----- apps/desktop/src/styles/theme.css | 72 - apps/desktop/src/vite-env.d.ts | 1 - apps/desktop/tsconfig.tsbuildinfo | 1 - archive/README.md | 22 + .../desktop-tauri}/.gitignore | 4 + .../desktop-tauri}/README.md | 5 +- archive/desktop-tauri/index.html | 18 + .../desktop-tauri}/package-lock.json | 0 .../desktop-tauri}/package.json | 0 archive/desktop-tauri/run-dev.sh | 116 ++ .../desktop-tauri}/src-tauri/Cargo.lock | 70 + .../desktop-tauri}/src-tauri/Cargo.toml | 1 + archive/desktop-tauri/src-tauri/build.rs | 175 ++ .../src-tauri/capabilities/default.json | 3 +- .../src-tauri/gen/schemas/acl-manifests.json | 2 +- .../src-tauri/gen/schemas/capabilities.json | 2 +- .../src-tauri/gen/schemas/desktop-schema.json | 66 + .../src-tauri/gen/schemas/linux-schema.json | 66 + .../src-tauri/icons/128x128.png | Bin .../src-tauri/icons/128x128@2x.png | Bin .../desktop-tauri}/src-tauri/icons/32x32.png | Bin .../desktop-tauri}/src-tauri/icons/icon.icns | Bin .../desktop-tauri}/src-tauri/icons/icon.ico | Bin .../desktop-tauri}/src-tauri/icons/icon.png | Bin archive/desktop-tauri/src-tauri/src/engine.rs | 1428 +++++++++++++++++ archive/desktop-tauri/src-tauri/src/ffi.rs | 780 +++++++++ .../desktop-tauri/src-tauri/src/ffi_bridge.c | 517 ++++++ .../desktop-tauri/src-tauri/src/ffi_bridge.h | 88 + .../desktop-tauri}/src-tauri/src/ffi_stub.h | 0 archive/desktop-tauri/src-tauri/src/lib.rs | 291 ++++ .../desktop-tauri}/src-tauri/src/main.rs | 0 .../desktop-tauri}/src-tauri/tauri.conf.json | 2 +- archive/desktop-tauri/src/App.tsx | 789 +++++++++ .../src/components/ApprovalCard.tsx | 84 + .../src/components/BashPanel.tsx | 92 ++ .../desktop-tauri/src/components/Composer.tsx | 440 +++++ archive/desktop-tauri/src/components/Hero.tsx | 29 + .../desktop-tauri/src/components/Icons.tsx | 228 +++ .../src/components/PlanPanel.tsx | 76 + .../src/components/QuestionCard.tsx | 88 + archive/desktop-tauri/src/components/Rail.tsx | 300 ++++ .../src/components/SessionHeader.tsx | 67 + .../src/components/SettingsPanel.tsx | 242 +++ .../src/components/Transcript.tsx | 187 +++ .../src/components/WorkspaceChip.tsx | 35 + .../src/dsh/approval/ApprovalPanel.module.css | 92 ++ .../src/dsh/assets/running-whale@2x.png | Bin 0 -> 21631 bytes .../src/dsh/chat/AssistantMarkdown.module.css | 78 + .../src/dsh/chat/ChatView.module.css | 409 +++++ .../dsh/chat/GenericCommandCard.module.css | 49 + .../src/dsh/chat/MessageItem.module.css | 360 +++++ .../src/dsh/chat/ReasoningRow.module.css | 77 + .../dsh/conversation/ContextMeter.module.css | 160 ++ .../conversation/ConversationRoot.module.css | 636 ++++++++ .../src/dsh/conversation/HeroShell.module.css | 237 +++ .../src/dsh/conversation/InputBar.module.css | 458 ++++++ .../src/dsh/conversation/TodoPanel.module.css | 123 ++ archive/desktop-tauri/src/dsh/index.css | 9 + .../src/dsh/layout/AppFrame.module.css | 388 +++++ archive/desktop-tauri/src/dsh/shell-vars.css | 121 ++ archive/desktop-tauri/src/dsh/styles/base.css | 39 + .../src/dsh/styles/corner-shape.css | 26 + .../src/dsh/styles/design-platform.css | 430 +++++ .../desktop-tauri/src/dsh/styles/focus.css | 21 + .../src/dsh/styles/gradient-shadow-text.css | 275 ++++ .../src/dsh/styles/scrollbar.css | 97 ++ .../src/dsh/workspace/Rows.module.css | 491 ++++++ .../src/dsh/workspace/SidebarRoot.module.css | 686 ++++++++ .../dsh/workspace/WorkspaceBrowser.module.css | 662 ++++++++ archive/desktop-tauri/src/lib/bridge.ts | 588 +++++++ .../desktop-tauri}/src/main.tsx | 2 + archive/desktop-tauri/src/styles/app.css | 400 +++++ archive/desktop-tauri/src/styles/theme.css | 117 ++ archive/desktop-tauri/src/vite-env.d.ts | 6 + .../desktop-tauri}/tsconfig.json | 0 .../desktop-tauri}/tsconfig.node.json | 0 archive/desktop-tauri/tsconfig.tsbuildinfo | 1 + .../desktop-tauri}/vite.config.ts | 7 +- {app => archive/qt-app}/CMakeLists.txt | 0 archive/qt-app/DEPRECATED.md | 10 + {app => archive/qt-app}/README.md | 0 .../qt-app}/android/AndroidManifest.xml | 0 .../android/res/drawable/ic_launcher.xml | 0 .../qt-app}/android/res/values/strings.xml | 0 .../src/xyz/whcself/cstagent/AttachFile.java | 0 .../src/xyz/whcself/cstagent/CstService.java | 0 .../src/xyz/whcself/cstagent/Notifier.java | 0 .../qt-app}/icons/application-menu.svg | 0 {app => archive/qt-app}/icons/arrow-down.svg | 0 {app => archive/qt-app}/icons/arrow-right.svg | 0 {app => archive/qt-app}/icons/box.svg | 0 .../qt-app}/icons/code-context.svg | 0 .../qt-app}/icons/document-edit.svg | 0 {app => archive/qt-app}/icons/edit-copy.svg | 0 {app => archive/qt-app}/icons/edit-delete.svg | 0 {app => archive/qt-app}/icons/edit-find.svg | 0 {app => archive/qt-app}/icons/folder-open.svg | 0 .../qt-app}/icons/format-list-unordered.svg | 0 {app => archive/qt-app}/icons/go-down.svg | 0 {app => archive/qt-app}/icons/help-hint.svg | 0 .../qt-app}/icons/internet-services.svg | 0 {app => archive/qt-app}/icons/list-add.svg | 0 .../qt-app}/icons/mail-attachment.svg | 0 .../qt-app}/icons/settings-configure.svg | 0 .../qt-app}/icons/task-complete.svg | 0 .../qt-app}/icons/text-x-generic.svg | 0 .../qt-app}/linux/cst-agent-app.desktop | 0 .../qt-app}/linux/cst-agent-app.svg | 0 {app => archive/qt-app}/qml/AskDialog.qml | 0 {app => archive/qt-app}/qml/ChatView.qml | 0 {app => archive/qt-app}/qml/Composer.qml | 0 {app => archive/qt-app}/qml/ConsentDialog.qml | 0 {app => archive/qt-app}/qml/ImageStrip.qml | 0 {app => archive/qt-app}/qml/Main.qml | 0 {app => archive/qt-app}/qml/MessageCard.qml | 0 {app => archive/qt-app}/qml/NoteLine.qml | 0 {app => archive/qt-app}/qml/SessionRow.qml | 0 .../qt-app}/qml/SettingsDialog.qml | 0 {app => archive/qt-app}/qml/Sidebar.qml | 0 {app => archive/qt-app}/qml/TaskPanel.qml | 0 {app => archive/qt-app}/qml/Theme.qml | 0 {app => archive/qt-app}/qml/ThemeIcon.qml | 0 {app => archive/qt-app}/qml/ThinkBlock.qml | 0 {app => archive/qt-app}/qml/TodoPanel.qml | 0 {app => archive/qt-app}/qml/ToolCard.qml | 0 .../qt-app}/src/core/AgentCore.cpp | 0 {app => archive/qt-app}/src/core/AgentCore.h | 0 {app => archive/qt-app}/src/core/PdfTools.cpp | 0 .../qt-app}/src/core/ToolProvider.cpp | 0 .../qt-app}/src/core/ToolProvider.h | 0 {app => archive/qt-app}/src/main.cpp | 0 .../src/platform/android/AndroidTools.cpp | 0 .../qt-app}/src/platform/linux/NotifyTool.cpp | 0 .../src/platform/windows/NotifyTool.cpp | 0 {app => archive/qt-app}/src/ui/BackKey.cpp | 0 {app => archive/qt-app}/src/ui/BackKey.h | 0 .../qt-app}/src/ui/SessionImages.cpp | 0 .../qt-app}/src/ui/SessionImages.h | 0 {app => archive/qt-app}/src/ui/ThemeIcons.cpp | 0 {app => archive/qt-app}/src/ui/ThemeIcons.h | 0 {app => archive/qt-app}/src/ui/UiTest.cpp | 0 {app => archive/qt-app}/src/ui/UiTest.h | 0 {app => archive/qt-app}/tools/selftest.c | 0 {web => archive/web-spa}/app.js | 0 archive/web-spa/index.html | 250 +++ {web => archive/web-spa}/style.css | 0 default.nix | 10 +- docs/README.md | 9 +- docs/android.md | 25 +- docs/desktop.md | 23 +- docs/plugins.md | 9 +- docs/webui-schema.md | 66 + docs/webui.md | 40 +- include/cst_agent.h | 11 +- include/cst_webui.h | 15 +- scripts/build_apk_android.sh | 5 +- scripts/e2e_webui.sh | 34 +- scripts/e2e_webui_ext.py | 13 +- scripts/e2e_webui_http.py | 12 +- scripts/e2e_webui_sessions.py | 18 +- scripts/e2e_webui_settings.py | 4 +- scripts/package_windows_app.sh | 5 +- src/agent_loop.c | 8 + src/agent_store.c | 8 +- src/main.c | 18 +- src/tui.c | 11 +- src/webui.c | 79 +- .../fixtures/sse/deepseek-native-eos/0005.sse | 152 ++ web/index.html | 268 +--- 184 files changed, 14098 insertions(+), 1858 deletions(-) delete mode 100644 app/DEPRECATED.md delete mode 100644 apps/desktop/index.html delete mode 100644 apps/desktop/src-tauri/build.rs delete mode 100644 apps/desktop/src-tauri/src/engine.rs delete mode 100644 apps/desktop/src-tauri/src/ffi.rs delete mode 100644 apps/desktop/src-tauri/src/lib.rs delete mode 100644 apps/desktop/src/App.tsx delete mode 100644 apps/desktop/src/components/Composer.tsx delete mode 100644 apps/desktop/src/components/ConsentModal.tsx delete mode 100644 apps/desktop/src/components/Rail.tsx delete mode 100644 apps/desktop/src/components/Transcript.tsx delete mode 100644 apps/desktop/src/lib/bridge.ts delete mode 100644 apps/desktop/src/styles/app.css delete mode 100644 apps/desktop/src/styles/theme.css delete mode 100644 apps/desktop/src/vite-env.d.ts delete mode 100644 apps/desktop/tsconfig.tsbuildinfo create mode 100644 archive/README.md rename {apps/desktop => archive/desktop-tauri}/.gitignore (51%) rename {apps/desktop => archive/desktop-tauri}/README.md (96%) create mode 100644 archive/desktop-tauri/index.html rename {apps/desktop => archive/desktop-tauri}/package-lock.json (100%) rename {apps/desktop => archive/desktop-tauri}/package.json (100%) create mode 100755 archive/desktop-tauri/run-dev.sh rename {apps/desktop => archive/desktop-tauri}/src-tauri/Cargo.lock (98%) rename {apps/desktop => archive/desktop-tauri}/src-tauri/Cargo.toml (96%) create mode 100644 archive/desktop-tauri/src-tauri/build.rs rename {apps/desktop => archive/desktop-tauri}/src-tauri/capabilities/default.json (81%) rename {apps/desktop => archive/desktop-tauri}/src-tauri/gen/schemas/acl-manifests.json (89%) rename {apps/desktop => archive/desktop-tauri}/src-tauri/gen/schemas/capabilities.json (68%) rename {apps/desktop => archive/desktop-tauri}/src-tauri/gen/schemas/desktop-schema.json (97%) rename {apps/desktop => archive/desktop-tauri}/src-tauri/gen/schemas/linux-schema.json (97%) rename {apps/desktop => archive/desktop-tauri}/src-tauri/icons/128x128.png (100%) rename {apps/desktop => archive/desktop-tauri}/src-tauri/icons/128x128@2x.png (100%) rename {apps/desktop => archive/desktop-tauri}/src-tauri/icons/32x32.png (100%) rename {apps/desktop => archive/desktop-tauri}/src-tauri/icons/icon.icns (100%) rename {apps/desktop => archive/desktop-tauri}/src-tauri/icons/icon.ico (100%) rename {apps/desktop => archive/desktop-tauri}/src-tauri/icons/icon.png (100%) create mode 100644 archive/desktop-tauri/src-tauri/src/engine.rs create mode 100644 archive/desktop-tauri/src-tauri/src/ffi.rs create mode 100644 archive/desktop-tauri/src-tauri/src/ffi_bridge.c create mode 100644 archive/desktop-tauri/src-tauri/src/ffi_bridge.h rename {apps/desktop => archive/desktop-tauri}/src-tauri/src/ffi_stub.h (100%) create mode 100644 archive/desktop-tauri/src-tauri/src/lib.rs rename {apps/desktop => archive/desktop-tauri}/src-tauri/src/main.rs (100%) rename {apps/desktop => archive/desktop-tauri}/src-tauri/tauri.conf.json (96%) create mode 100644 archive/desktop-tauri/src/App.tsx create mode 100644 archive/desktop-tauri/src/components/ApprovalCard.tsx create mode 100644 archive/desktop-tauri/src/components/BashPanel.tsx create mode 100644 archive/desktop-tauri/src/components/Composer.tsx create mode 100644 archive/desktop-tauri/src/components/Hero.tsx create mode 100644 archive/desktop-tauri/src/components/Icons.tsx create mode 100644 archive/desktop-tauri/src/components/PlanPanel.tsx create mode 100644 archive/desktop-tauri/src/components/QuestionCard.tsx create mode 100644 archive/desktop-tauri/src/components/Rail.tsx create mode 100644 archive/desktop-tauri/src/components/SessionHeader.tsx create mode 100644 archive/desktop-tauri/src/components/SettingsPanel.tsx create mode 100644 archive/desktop-tauri/src/components/Transcript.tsx create mode 100644 archive/desktop-tauri/src/components/WorkspaceChip.tsx create mode 100644 archive/desktop-tauri/src/dsh/approval/ApprovalPanel.module.css create mode 100644 archive/desktop-tauri/src/dsh/assets/running-whale@2x.png create mode 100644 archive/desktop-tauri/src/dsh/chat/AssistantMarkdown.module.css create mode 100644 archive/desktop-tauri/src/dsh/chat/ChatView.module.css create mode 100644 archive/desktop-tauri/src/dsh/chat/GenericCommandCard.module.css create mode 100644 archive/desktop-tauri/src/dsh/chat/MessageItem.module.css create mode 100644 archive/desktop-tauri/src/dsh/chat/ReasoningRow.module.css create mode 100644 archive/desktop-tauri/src/dsh/conversation/ContextMeter.module.css create mode 100644 archive/desktop-tauri/src/dsh/conversation/ConversationRoot.module.css create mode 100644 archive/desktop-tauri/src/dsh/conversation/HeroShell.module.css create mode 100644 archive/desktop-tauri/src/dsh/conversation/InputBar.module.css create mode 100644 archive/desktop-tauri/src/dsh/conversation/TodoPanel.module.css create mode 100644 archive/desktop-tauri/src/dsh/index.css create mode 100644 archive/desktop-tauri/src/dsh/layout/AppFrame.module.css create mode 100644 archive/desktop-tauri/src/dsh/shell-vars.css create mode 100644 archive/desktop-tauri/src/dsh/styles/base.css create mode 100644 archive/desktop-tauri/src/dsh/styles/corner-shape.css create mode 100644 archive/desktop-tauri/src/dsh/styles/design-platform.css create mode 100644 archive/desktop-tauri/src/dsh/styles/focus.css create mode 100644 archive/desktop-tauri/src/dsh/styles/gradient-shadow-text.css create mode 100644 archive/desktop-tauri/src/dsh/styles/scrollbar.css create mode 100644 archive/desktop-tauri/src/dsh/workspace/Rows.module.css create mode 100644 archive/desktop-tauri/src/dsh/workspace/SidebarRoot.module.css create mode 100644 archive/desktop-tauri/src/dsh/workspace/WorkspaceBrowser.module.css create mode 100644 archive/desktop-tauri/src/lib/bridge.ts rename {apps/desktop => archive/desktop-tauri}/src/main.tsx (79%) create mode 100644 archive/desktop-tauri/src/styles/app.css create mode 100644 archive/desktop-tauri/src/styles/theme.css create mode 100644 archive/desktop-tauri/src/vite-env.d.ts rename {apps/desktop => archive/desktop-tauri}/tsconfig.json (100%) rename {apps/desktop => archive/desktop-tauri}/tsconfig.node.json (100%) create mode 100644 archive/desktop-tauri/tsconfig.tsbuildinfo rename {apps/desktop => archive/desktop-tauri}/vite.config.ts (73%) rename {app => archive/qt-app}/CMakeLists.txt (100%) create mode 100644 archive/qt-app/DEPRECATED.md rename {app => archive/qt-app}/README.md (100%) rename {app => archive/qt-app}/android/AndroidManifest.xml (100%) rename {app => archive/qt-app}/android/res/drawable/ic_launcher.xml (100%) rename {app => archive/qt-app}/android/res/values/strings.xml (100%) rename {app => archive/qt-app}/android/src/xyz/whcself/cstagent/AttachFile.java (100%) rename {app => archive/qt-app}/android/src/xyz/whcself/cstagent/CstService.java (100%) rename {app => archive/qt-app}/android/src/xyz/whcself/cstagent/Notifier.java (100%) rename {app => archive/qt-app}/icons/application-menu.svg (100%) rename {app => archive/qt-app}/icons/arrow-down.svg (100%) rename {app => archive/qt-app}/icons/arrow-right.svg (100%) rename {app => archive/qt-app}/icons/box.svg (100%) rename {app => archive/qt-app}/icons/code-context.svg (100%) rename {app => archive/qt-app}/icons/document-edit.svg (100%) rename {app => archive/qt-app}/icons/edit-copy.svg (100%) rename {app => archive/qt-app}/icons/edit-delete.svg (100%) rename {app => archive/qt-app}/icons/edit-find.svg (100%) rename {app => archive/qt-app}/icons/folder-open.svg (100%) rename {app => archive/qt-app}/icons/format-list-unordered.svg (100%) rename {app => archive/qt-app}/icons/go-down.svg (100%) rename {app => archive/qt-app}/icons/help-hint.svg (100%) rename {app => archive/qt-app}/icons/internet-services.svg (100%) rename {app => archive/qt-app}/icons/list-add.svg (100%) rename {app => archive/qt-app}/icons/mail-attachment.svg (100%) rename {app => archive/qt-app}/icons/settings-configure.svg (100%) rename {app => archive/qt-app}/icons/task-complete.svg (100%) rename {app => archive/qt-app}/icons/text-x-generic.svg (100%) rename {app => archive/qt-app}/linux/cst-agent-app.desktop (100%) rename {app => archive/qt-app}/linux/cst-agent-app.svg (100%) rename {app => archive/qt-app}/qml/AskDialog.qml (100%) rename {app => archive/qt-app}/qml/ChatView.qml (100%) rename {app => archive/qt-app}/qml/Composer.qml (100%) rename {app => archive/qt-app}/qml/ConsentDialog.qml (100%) rename {app => archive/qt-app}/qml/ImageStrip.qml (100%) rename {app => archive/qt-app}/qml/Main.qml (100%) rename {app => archive/qt-app}/qml/MessageCard.qml (100%) rename {app => archive/qt-app}/qml/NoteLine.qml (100%) rename {app => archive/qt-app}/qml/SessionRow.qml (100%) rename {app => archive/qt-app}/qml/SettingsDialog.qml (100%) rename {app => archive/qt-app}/qml/Sidebar.qml (100%) rename {app => archive/qt-app}/qml/TaskPanel.qml (100%) rename {app => archive/qt-app}/qml/Theme.qml (100%) rename {app => archive/qt-app}/qml/ThemeIcon.qml (100%) rename {app => archive/qt-app}/qml/ThinkBlock.qml (100%) rename {app => archive/qt-app}/qml/TodoPanel.qml (100%) rename {app => archive/qt-app}/qml/ToolCard.qml (100%) rename {app => archive/qt-app}/src/core/AgentCore.cpp (100%) rename {app => archive/qt-app}/src/core/AgentCore.h (100%) rename {app => archive/qt-app}/src/core/PdfTools.cpp (100%) rename {app => archive/qt-app}/src/core/ToolProvider.cpp (100%) rename {app => archive/qt-app}/src/core/ToolProvider.h (100%) rename {app => archive/qt-app}/src/main.cpp (100%) rename {app => archive/qt-app}/src/platform/android/AndroidTools.cpp (100%) rename {app => archive/qt-app}/src/platform/linux/NotifyTool.cpp (100%) rename {app => archive/qt-app}/src/platform/windows/NotifyTool.cpp (100%) rename {app => archive/qt-app}/src/ui/BackKey.cpp (100%) rename {app => archive/qt-app}/src/ui/BackKey.h (100%) rename {app => archive/qt-app}/src/ui/SessionImages.cpp (100%) rename {app => archive/qt-app}/src/ui/SessionImages.h (100%) rename {app => archive/qt-app}/src/ui/ThemeIcons.cpp (100%) rename {app => archive/qt-app}/src/ui/ThemeIcons.h (100%) rename {app => archive/qt-app}/src/ui/UiTest.cpp (100%) rename {app => archive/qt-app}/src/ui/UiTest.h (100%) rename {app => archive/qt-app}/tools/selftest.c (100%) rename {web => archive/web-spa}/app.js (100%) create mode 100644 archive/web-spa/index.html rename {web => archive/web-spa}/style.css (100%) create mode 100644 docs/webui-schema.md create mode 100644 tests/fixtures/sse/deepseek-native-eos/0005.sse diff --git a/CMakeLists.txt b/CMakeLists.txt index 27b2b49..779c24c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,8 +5,8 @@ set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) option(CST_BUILD_TUI "Build ncurses/PDCurses TUI" ON) -option(CST_BUILD_WEBUI "Build the HTTP/SSE WebUI (--web)" ON) -option(CST_BUILD_APP "Build the Qt front end in app/ (in-process engine, no port)" OFF) +option(CST_BUILD_WEBUI "Build the HTTP/SSE engine API (--web, cst-webui/1)" ON) +option(CST_BUILD_APP "Build archived Qt front end in archive/qt-app (experimental)" OFF) option(CST_NATIVE_ARCH "Pass -march=native to sashidown" OFF) option(CST_BUNDLE_BUSYBOX "Install placeholder/scripts for busybox beside binary" ON) @@ -218,9 +218,8 @@ if(WIN32) endif() # --- the engine as a library --------------------------------------------- -# cstcore is the core without main()/TUI: the CLI links it and the Qt front -# end in app/ (CST_BUILD_APP) embeds the very same objects in-process, so the -# engine is compiled once per build instead of once per front end. +# cstcore is the core without main()/TUI: the CLI links it; archived clients +# (archive/qt-app, archive/desktop-tauri) can embed the same objects in-process. add_library(cstcore STATIC ${CST_CORE_SOURCES}) add_dependencies(cstcore sashidown) target_include_directories(cstcore PUBLIC @@ -246,7 +245,8 @@ else() endif() if(CST_BUILD_APP) - add_subdirectory(app) + # Frozen reference GUI; not part of the default Hermit delivery. + add_subdirectory(archive/qt-app) endif() set(CST_SOURCES src/main.c) diff --git a/README.md b/README.md index 78389a9..d015618 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,11 @@ > 本仓库 `CST-agent` 仅作迁移兼容入口。换远程:`./scripts/migrate_remote_to_hermit.sh` > 完整步骤:[MIGRATION.md](MIGRATION.md)。 -本地 coding agent(原 CST-Agent):终端 TUI、可选 WebUI / 桌面 GUI,在工作区里读改文件、跑命令、管会话。 +本地 **极简 TUI** coding agent(原 CST-Agent):在工作区里读改文件、跑命令、管会话。 C11 实现,OpenAI 兼容网关(DeepSeek / Kimi / MiniMax / OpenAI 等)。 +本仓交付:`hermit` CLI/TUI、`libcstcore`、可替换 plugins/MCP/skills,以及 `hermit --web` 提供的 HTTP/SSE 引擎 API(`cst-webui/1`)。完整浏览器 SPA / 桌面 GUI 参考实现在 [`archive/`](archive/)(不编入默认产物)。 + **适合**:有监督的本地改动与仓库探索(权限确认、diff、`/undo`、沙箱)。 **慎用**:完全无人值守的长任务;headless 默认自动批准。 @@ -58,7 +60,7 @@ make hermit-font # 烘焙 PNG 帧 + hermit-icons.ttf(Pillow + fonttool make install PREFIX=$HOME/.local # 同时安装字体到 share/fonts/hermit 与 ~/.local/share/fonts ``` -桌面 GUI(Tauri 2 + React,DeepSeek Harness 风格布局):见 [`apps/desktop`](apps/desktop/) 与 [docs/desktop.md](docs/desktop.md)。旧 Qt App(`app/`)已冻结。 +外置客户端契约:hooks(`cst_agent_hooks_t`)与 [`docs/webui.md`](docs/webui.md) / [`docs/webui-schema.md`](docs/webui-schema.md)。归档 GUI:[`archive/desktop-tauri`](archive/desktop-tauri/)、[`archive/qt-app`](archive/qt-app/)、[`archive/web-spa`](archive/web-spa/)。 TUI 图标:`HERMIT_ICONS=auto|mini|font`(默认色块动画帧,状态与 `icons/agent-motion` HTML 套件一致;`font` 输出 PUA `U+E700+`,需终端加载已安装的 Hermit Icons)。环境变量 **`HERMIT_*` 优先,`CST_*` 仍接受**。 @@ -68,13 +70,14 @@ TUI 图标:`HERMIT_ICONS=auto|mini|font`(默认色块动画帧,状态与 ` ```bash hermit # TUI -hermit --web # http://127.0.0.1:8787/ +hermit --web # HTTP/SSE API + health page +hermit --web --web-dev archive/web-spa # archived full SPA hermit --headless --prompt '…' ``` 首页品牌 **HERMIT** + 寄居蟹色块动效;色板:brand `#277F94`、crab `#F68042`、eye `#9957AD`。 -更多:Provider / 工具 / 议会 / WebUI 见 [docs/](docs/)。 +更多:Provider / 工具 / 议会 / 引擎 API 见 [docs/](docs/)。 --- diff --git a/app/DEPRECATED.md b/app/DEPRECATED.md deleted file mode 100644 index 63d68a5..0000000 --- a/app/DEPRECATED.md +++ /dev/null @@ -1,10 +0,0 @@ -# Deprecated: Qt front end - -This directory is the **legacy Qt 6 / QML** Hermit GUI (`cst-agent-app`). - -The supported desktop shell is **`apps/desktop`** (Tauri 2 + React), with a -DeepSeek-Harness-inspired layout and an in-process C engine bridge. - -Do not add features here. Once Tauri reaches feature parity, this tree will be -moved to `archive/qt-app` or removed. Android APK scripts that target this tree -are transitional. diff --git a/apps/desktop/index.html b/apps/desktop/index.html deleted file mode 100644 index 5e1f962..0000000 --- a/apps/desktop/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Hermit - - -
- - - diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs deleted file mode 100644 index 605b374..0000000 --- a/apps/desktop/src-tauri/build.rs +++ /dev/null @@ -1,29 +0,0 @@ -fn main() { - // Optional: link a prebuilt libcstcore when HERMIT_CORE_LIB is set. - // Example: - // HERMIT_CORE_LIB=/path/to/libcstcore.a HERMIT_CORE_INCLUDE=/path/to/include - // cargo build --features engine - if std::env::var("CARGO_FEATURE_ENGINE").is_ok() { - if let Ok(lib) = std::env::var("HERMIT_CORE_LIB") { - let path = std::path::PathBuf::from(&lib); - if let Some(dir) = path.parent() { - println!("cargo:rustc-link-search=native={}", dir.display()); - } - let stem = path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("cstcore") - .trim_start_matches("lib"); - println!("cargo:rustc-link-lib=static={}", stem); - println!("cargo:rustc-link-lib=curl"); - println!("cargo:rustc-link-lib=pthread"); - println!("cargo:rustc-link-lib=m"); - println!("cargo:rerun-if-changed={}", lib); - } - if let Ok(inc) = std::env::var("HERMIT_CORE_INCLUDE") { - println!("cargo:rerun-if-env-changed=HERMIT_CORE_INCLUDE"); - println!("cargo:rustc-env=HERMIT_CORE_INCLUDE={}", inc); - } - } - tauri_build::build() -} diff --git a/apps/desktop/src-tauri/src/engine.rs b/apps/desktop/src-tauri/src/engine.rs deleted file mode 100644 index 338e790..0000000 --- a/apps/desktop/src-tauri/src/engine.rs +++ /dev/null @@ -1,173 +0,0 @@ -//! Engine façade: mock by default; with `--features engine` + HERMIT_CORE_LIB -//! this module will grow real FFI around `cst_agent.h` (AgentCore parity). - -use serde::{Deserialize, Serialize}; -use std::sync::Mutex; -use tauri::{AppHandle, Emitter}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionInfo { - pub id: String, - pub title: String, - pub updated_at: i64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "kind")] -pub enum ChatItem { - #[serde(rename = "user")] - User { id: String, text: String }, - #[serde(rename = "assistant")] - Assistant { id: String, text: String }, - #[serde(rename = "thought")] - Thought { id: String, text: String }, - #[serde(rename = "tool")] - Tool { - id: String, - name: String, - status: String, - detail: String, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Settings { - pub provider: String, - pub model: String, - pub approval: String, - pub workspace: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum EngineEvent { - #[serde(rename = "assistant_delta")] - AssistantDelta { text: String }, - #[serde(rename = "thought_delta")] - ThoughtDelta { text: String }, - #[serde(rename = "tool_start")] - ToolStart { - id: String, - name: String, - detail: String, - }, - #[serde(rename = "tool_end")] - ToolEnd { - id: String, - status: String, - detail: String, - }, - #[serde(rename = "consent")] - Consent { - req: ConsentRequest, - }, - #[serde(rename = "turn_end")] - TurnEnd { ok: bool }, - #[serde(rename = "error")] - Error { message: String }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConsentRequest { - pub id: String, - pub tool: String, - pub detail: String, -} - -pub struct EngineState { - pub settings: Settings, - pub sessions: Vec, - busy: bool, -} - -impl Default for EngineState { - fn default() -> Self { - let workspace = std::env::current_dir() - .ok() - .and_then(|p| p.to_str().map(|s| s.to_string())) - .unwrap_or_else(|| ".".into()); - Self { - settings: Settings { - provider: "deepseek".into(), - model: "deepseek-flash".into(), - approval: "auto".into(), - workspace, - }, - sessions: vec![SessionInfo { - id: "welcome".into(), - title: "Welcome".into(), - updated_at: chrono_now(), - }], - busy: false, - } - } -} - -fn chrono_now() -> i64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis() as i64) - .unwrap_or(0) -} - -pub type SharedEngine = Mutex; - -pub fn list_sessions(state: &SharedEngine) -> Vec { - state.lock().unwrap().sessions.clone() -} - -pub fn new_session(state: &SharedEngine) -> SessionInfo { - let mut g = state.lock().unwrap(); - let s = SessionInfo { - id: format!("s{}", chrono_now()), - title: "New session".into(), - updated_at: chrono_now(), - }; - g.sessions.insert(0, s.clone()); - s -} - -pub fn open_session(_state: &SharedEngine, _id: &str) -> Vec { - // Real FFI will replay transcript from cst_agent_store_*. - Vec::new() -} - -pub fn settings_get(state: &SharedEngine) -> Settings { - state.lock().unwrap().settings.clone() -} - -pub async fn send_turn(app: AppHandle, state: &SharedEngine, text: String) { - { - let mut g = state.lock().unwrap(); - if g.busy { - return; - } - g.busy = true; - } - - // Placeholder stream until libcstcore FFI is wired (feature = "engine"). - let _ = app.emit( - "hermit://event", - EngineEvent::ThoughtDelta { - text: "Rust host received turn; FFI bridge pending full AgentCore port." - .into(), - }, - ); - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - let _ = app.emit( - "hermit://event", - EngineEvent::AssistantDelta { - text: format!("Echo (host): {}", text), - }, - ); - let _ = app.emit("hermit://event", EngineEvent::TurnEnd { ok: true }); - state.lock().unwrap().busy = false; -} - -pub fn cancel_turn(state: &SharedEngine) { - state.lock().unwrap().busy = false; -} - -pub fn consent_answer(_id: &str, _answer: &str) { - // Real path: wake the worker blocked in consent hook. -} diff --git a/apps/desktop/src-tauri/src/ffi.rs b/apps/desktop/src-tauri/src/ffi.rs deleted file mode 100644 index 5446f7c..0000000 --- a/apps/desktop/src-tauri/src/ffi.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Future FFI surface against `include/cst_agent.h` (AgentCore parity). -//! -//! Enable with: -//! make -C ../.. libcstcore.a # or cmake cstcore -//! HERMIT_CORE_LIB=$PWD/../../libcstcore.a \ -//! HERMIT_CORE_INCLUDE=$PWD/../../include \ -//! cargo build --features engine -//! -//! Mapping (Qt AgentCore → Tauri commands/events): -//! open / send / cancel → session_* / send_turn / cancel_turn -//! hooks.assistant_delta → hermit://event AssistantDelta -//! hooks.tool_* → ToolStart / ToolEnd -//! hooks.consent (blocking) → Consent event + consent_answer wakes waiter -//! settings / creds → settings_get (+ later settings_set) - -#![allow(dead_code)] - -#[cfg(feature = "engine")] -mod live { - // Bindings land here (cbindgen or manual extern "C"). -} - -#[cfg(not(feature = "engine"))] -pub fn engine_linked() -> bool { - false -} - -#[cfg(feature = "engine")] -pub fn engine_linked() -> bool { - true -} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs deleted file mode 100644 index fbb8f1b..0000000 --- a/apps/desktop/src-tauri/src/lib.rs +++ /dev/null @@ -1,73 +0,0 @@ -mod engine; -mod ffi; - -use engine::{SharedEngine, EngineState}; -use std::sync::Mutex; -use tauri::Manager; - -#[tauri::command] -fn session_list(state: tauri::State<'_, SharedEngine>) -> Vec { - engine::list_sessions(&state) -} - -#[tauri::command] -fn session_new(state: tauri::State<'_, SharedEngine>) -> engine::SessionInfo { - engine::new_session(&state) -} - -#[tauri::command] -fn session_open( - state: tauri::State<'_, SharedEngine>, - id: String, -) -> Vec { - engine::open_session(&state, &id) -} - -#[tauri::command] -fn settings_get(state: tauri::State<'_, SharedEngine>) -> engine::Settings { - engine::settings_get(&state) -} - -#[tauri::command] -async fn send_turn( - app: tauri::AppHandle, - state: tauri::State<'_, SharedEngine>, - text: String, -) -> Result<(), String> { - engine::send_turn(app, &state, text).await; - Ok(()) -} - -#[tauri::command] -fn cancel_turn(state: tauri::State<'_, SharedEngine>) { - engine::cancel_turn(&state); -} - -#[tauri::command] -fn consent_answer(id: String, answer: String) { - engine::consent_answer(&id, &answer); -} - -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - tauri::Builder::default() - .plugin(tauri_plugin_shell::init()) - .manage(Mutex::new(EngineState::default())) - .invoke_handler(tauri::generate_handler![ - session_list, - session_new, - session_open, - settings_get, - send_turn, - cancel_turn, - consent_answer - ]) - .setup(|app| { - if let Some(w) = app.get_webview_window("main") { - let _ = w.set_title("Hermit"); - } - Ok(()) - }) - .run(tauri::generate_context!()) - .expect("error while running Hermit desktop"); -} diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx deleted file mode 100644 index dc3f52c..0000000 --- a/apps/desktop/src/App.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { Rail } from "@/components/Rail"; -import { Transcript } from "@/components/Transcript"; -import { Composer } from "@/components/Composer"; -import { ConsentModal } from "@/components/ConsentModal"; -import { - answerConsent, - cancelTurn, - getSettings, - listSessions, - newSession, - onEngineEvent, - openSession, - sendTurn, - type ChatItem, - type ConsentRequest, - type SessionInfo, - type Settings, -} from "@/lib/bridge"; -import "@/styles/app.css"; - -export default function App() { - const [sessions, setSessions] = useState([]); - const [activeId, setActiveId] = useState(null); - const [items, setItems] = useState([]); - const [draft, setDraft] = useState(""); - const [busy, setBusy] = useState(false); - const [query, setQuery] = useState(""); - const [settings, setSettings] = useState({ - provider: "deepseek", - model: "deepseek-flash", - approval: "auto", - workspace: ".", - }); - const [consent, setConsent] = useState(null); - const [showSettings, setShowSettings] = useState(false); - - const refreshSessions = useCallback(async () => { - setSessions(await listSessions()); - }, []); - - useEffect(() => { - void (async () => { - setSettings(await getSettings()); - await refreshSessions(); - })(); - }, [refreshSessions]); - - useEffect(() => { - let un: (() => void) | undefined; - void onEngineEvent((ev) => { - if (ev.type === "assistant_delta") { - setItems((prev) => { - const last = prev[prev.length - 1]; - if (last && last.kind === "assistant") { - return [ - ...prev.slice(0, -1), - { ...last, text: last.text + ev.text }, - ]; - } - return [ - ...prev, - { kind: "assistant", id: `a${Date.now()}`, text: ev.text }, - ]; - }); - } else if (ev.type === "thought_delta") { - setItems((prev) => { - const last = prev[prev.length - 1]; - if (last && last.kind === "thought") { - return [ - ...prev.slice(0, -1), - { ...last, text: last.text + "\n" + ev.text }, - ]; - } - return [ - ...prev, - { kind: "thought", id: `th${Date.now()}`, text: ev.text }, - ]; - }); - } else if (ev.type === "tool_start") { - setItems((prev) => [ - ...prev, - { - kind: "tool", - id: ev.id, - name: ev.name, - status: "running", - detail: ev.detail, - }, - ]); - } else if (ev.type === "tool_end") { - setItems((prev) => - prev.map((it) => - it.kind === "tool" && it.id === ev.id - ? { ...it, status: ev.status, detail: ev.detail || it.detail } - : it, - ), - ); - } else if (ev.type === "consent") { - setConsent(ev.req); - } else if (ev.type === "turn_end") { - setBusy(false); - } else if (ev.type === "error") { - setBusy(false); - setItems((prev) => [ - ...prev, - { - kind: "assistant", - id: `err${Date.now()}`, - text: `Error: ${ev.message}`, - }, - ]); - } - }).then((u) => { - un = u; - }); - return () => un?.(); - }, []); - - const open = async (id: string) => { - setActiveId(id); - setItems(await openSession(id)); - }; - - const onNew = async () => { - const s = await newSession(); - await refreshSessions(); - setActiveId(s.id); - setItems([]); - }; - - const onSend = async () => { - const text = draft.trim(); - if (!text || busy) return; - setDraft(""); - setBusy(true); - setItems((prev) => [ - ...prev, - { kind: "user", id: `u${Date.now()}`, text }, - ]); - await sendTurn(text); - }; - - return ( -
- void onNew()} - onOpen={(id) => void open(id)} - onSettings={() => setShowSettings(true)} - /> -
-
-

- {sessions.find((s) => s.id === activeId)?.title ?? "New session"} -

-
- engine ready - - {settings.model} - -
-
- - void onSend()} - onStop={() => void cancelTurn()} - /> -
- {consent ? ( - { - void answerConsent(consent.id, a); - setConsent(null); - }} - /> - ) : null} - {showSettings ? ( -
setShowSettings(false)}> -
e.stopPropagation()} - > -

Settings

-
{JSON.stringify(settings, null, 2)}
-
- -
-
-
- ) : null} -
- ); -} diff --git a/apps/desktop/src/components/Composer.tsx b/apps/desktop/src/components/Composer.tsx deleted file mode 100644 index 9fbea66..0000000 --- a/apps/desktop/src/components/Composer.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useEffect, useRef } from "react"; -import type { Settings } from "@/lib/bridge"; - -type Props = { - value: string; - busy: boolean; - settings: Settings; - onChange: (v: string) => void; - onSend: () => void; - onStop: () => void; -}; - -export function Composer({ - value, - busy, - settings, - onChange, - onSend, - onStop, -}: Props) { - const ref = useRef(null); - useEffect(() => { - const el = ref.current; - if (!el) return; - el.style.height = "0px"; - el.style.height = Math.min(200, el.scrollHeight) + "px"; - }, [value]); - - return ( -
-
- +
+ + + + + + +
+
+
+ 就绪 + + +
+ + + + + + + +
+ +
+ + + + + + + + diff --git a/web/style.css b/archive/web-spa/style.css similarity index 100% rename from web/style.css rename to archive/web-spa/style.css diff --git a/default.nix b/default.nix index 9012cc2..e31f4fd 100644 --- a/default.nix +++ b/default.nix @@ -1,7 +1,7 @@ -# NixOS and other nix users: legacy Qt Quick front end (frozen; Tauri app is -# apps/desktop). CLI remains scripts/bootstrap_linux.sh. +# NixOS: archived Qt Quick front end (archive/qt-app). Prefer hermit CLI via +# scripts/bootstrap_linux.sh. Desktop Tauri reference: archive/desktop-tauri. # -# nix-build # ./result/bin/hermit-app (Qt, transitional) +# nix-build # ./result/bin/hermit-app (Qt, frozen) # nix-env -f . -i hermit { pkgs ? import { } }: @@ -57,7 +57,7 @@ pkgs.stdenv.mkDerivation { cmakeFlags = [ "-DCST_BUILD_APP=ON" "-DCMAKE_BUILD_TYPE=Release" ]; # The "app" component is the window, its launcher entry and the icon; - # app/CMakeLists.txt is the one place that knows where each goes. + # archive/qt-app/CMakeLists.txt is the one place that knows where each goes. installPhase = '' runHook preInstall cmake --install . --component app --prefix "$out" @@ -86,7 +86,7 @@ pkgs.stdenv.mkDerivation { meta = { description = - "Hermit: local coding agent (legacy Qt Quick front end; prefer apps/desktop)"; + "Hermit: archived Qt Quick front end (prefer CLI TUI + --web API)"; homepage = "https://git.whcself.xyz/WHC/Hermit"; platforms = pkgs.lib.platforms.linux; mainProgram = "cst-agent-app"; diff --git a/docs/README.md b/docs/README.md index 4680eee..1de4e99 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,9 +9,12 @@ | [env_openai.md](env_openai.md) | Provider、模型、API Key、环境变量(`HERMIT_*` / `CST_*`) | | [parliament.md](parliament.md) | 多设备议会 | | [workers.md](workers.md) | 后台任务、worker、job 完成报告 | -| [webui.md](webui.md) | 内置 HTTP + SSE WebUI | -| [plugins.md](plugins.md) | Skills、插件、MCP | +| [webui.md](webui.md) | 引擎 HTTP + SSE API(`cst-webui/1`) | +| [webui-schema.md](webui-schema.md) | SSE / state 字段摘要 | +| [plugins.md](plugins.md) | Skills、插件、MCP(可替换扩展;非 core 工具) | +| [desktop.md](desktop.md) | 归档桌面 GUI 指针 | | [compat.md](compat.md) | 平台兼容 | | [README-windows.md](README-windows.md) | Windows | -| [android.md](android.md) | Android / APK | +| [android.md](android.md) | Android / APK(归档 Qt 路径) | | [archive/](archive/) | 历史设计笔记(非产品文档) | +| [../archive/](../archive/) | 冻结客户端(qt-app / desktop-tauri / web-spa) | diff --git a/docs/android.md b/docs/android.md index fdea4a6..b3bef2a 100644 --- a/docs/android.md +++ b/docs/android.md @@ -1,10 +1,11 @@ # CST-agent on Android The engine runs on the phone as a plain bionic binary -- no Termux, no proot, -no second process for the UI to talk to. It is the same C core the CLI, the -TUI and the WebUI use, cross-compiled with the NDK; on the phone the Qt front -end (app/) will link `libcstcore.a` in-process and register its own platform -tools under the owner `app:android`. +no second process for the UI to talk to. It is the same C core the CLI/TUI and +`--web` API use, cross-compiled with the NDK. The archived Qt front end +(`archive/qt-app/`) can link `libcstcore.a` in-process and register platform +tools under the owner `app:android`. Prefer a remote client on `cst-webui/1` +for new work. Two scripts cover the engine side: @@ -163,7 +164,7 @@ have, so the script rewrites the shebang of the build-tools wrappers. provider's environment variable, `CST_API_KEY`), so the phone answers without being asked for the key again. Rotation keeps the session; the sidebar appears in landscape. -* Platform tools behind `app:android` (`app/src/platform/android/AndroidTools.cpp`): +* Platform tools behind `app:android` (`archive/qt-app/src/platform/android/AndroidTools.cpp`): `notification_send`, `clipboard_get` / `clipboard_set` under the engine's own tool names and `share` (`ACTION_SEND` + chooser) all work on the device. The file @@ -176,20 +177,20 @@ have, so the script rewrites the shebang of the build-tools wrappers. the tool could only say "failed: no exception detail", because Qt clears a Java exception before C++ gets to look at it. The one step that has to fail visibly (a builder, a `PendingIntent`, `build()`, `notify()`) now lives in - `app/android/src/xyz/whcself/cstagent/Notifier.java`, which catches the + `archive/qt-app/android/src/xyz/whcself/cstagent/Notifier.java`, which catches the throwable and hands the text back for the tool result; the device then reported `notification sent` and the shade had it. androiddeployqt copies - `app/android/` with the rest of the package source and Qt's Gradle template + `archive/qt-app/android/` with the rest of the package source and Qt's Gradle template puts its `src/` on `java.srcDirs`, so **a class dropped in that directory needs no build-script change** -- the front service of the next step goes there too. * A session's transcript replays a delivered file (`deliver_file`) as a card with Open and Share -- the copy lives next to the session, marked `for_user` - so no request ever contains it (see `app/README.md`, "Files the agent + so no request ever contains it (see `archive/qt-archive/qt-app/README.md`, "Files the agent delivers"). * **A turn survives the app leaving the screen**: `AgentCore::send()` calls `app::holdForeground()`, which starts `CstService` - (`app/android/src/xyz/whcself/cstagent/CstService.java`) as a foreground + (`archive/qt-app/android/src/xyz/whcself/cstagent/CstService.java`) as a foreground service and takes a partial wake lock; `finishTurn()` stops it again. On the NX789J: with the app sent home and the screen off, the service showed `isForeground=true` with its `CST-agent is working` notification (id 2, the @@ -205,17 +206,17 @@ have, so the script rewrites the shebang of the build-tools wrappers. the foreground the same two hooks wait two minutes (a permission) or five (a question) and then answer "no" with a reason instead of waiting for good. The GUI hop a platform tool makes (`onGuiThread()` in - `app/src/platform/android/AndroidTools.cpp`) pings first and gives up after + `archive/qt-app/src/platform/android/AndroidTools.cpp`) pings first and gives up after three seconds, and the header shows what the turn is doing (`shell: make -j8`) so a long turn is not mistaken for a stuck one. `--uitest` covers the window side of all this. * **Documents**: `md_to_pdf` (the window's own tool, - `app/src/core/PdfTools.cpp`, owner `app:qt`) lays markdown or a workspace + `archive/qt-app/src/core/PdfTools.cpp`, owner `app:qt`) lays markdown or a workspace `.md`/`.html` out as an A4 PDF with 15 mm margins and hands it over as a file card, exactly the way `deliver_file` does; `QPrinter`/PrintSupport is not used, so the phone gets it too. Qt's PDF engine embeds a font *subset*, which is why a Chinese page is a few KB rather than the few MB a whole CJK font - would cost (see `app/README.md`). + would cost (see `archive/qt-archive/qt-app/README.md`). * Still to come: the back gesture, and the icon glyphs -- `ThemeIcon` finds no icon files on the phone and draws empty boxes where the desktop shows Breeze icons. diff --git a/docs/desktop.md b/docs/desktop.md index 46f2af2..22dcb13 100644 --- a/docs/desktop.md +++ b/docs/desktop.md @@ -1,20 +1,23 @@ -# Hermit Desktop (Tauri) +# Hermit Desktop(归档) -Canonical GUI: [`apps/desktop`](../apps/desktop/). Qt under [`app/`](../app/) is frozen ([DEPRECATED](../app/DEPRECATED.md)). +桌面 GUI **不在本仓默认交付物中**。参考实现已冻结于: -## Shape +| 树 | 说明 | +|----|------| +| [`archive/desktop-tauri/`](../archive/desktop-tauri/) | Tauri 2 + React(原 `apps/desktop`) | +| [`archive/qt-app/`](../archive/qt-app/) | Qt6/QML(原 `app/`,已 DEPRECATED) | -- **Shell**: Tauri 2 (Linux / Windows / macOS) -- **UI**: React 19 — session rail, chat, tool cards, composer (DeepSeek Harness–like IA) -- **Engine**: in-process `libcstcore` via Rust FFI (feature `engine`); without it the host uses a mock / echo path so the UI is developable +正式远程客户端请实现 [webui.md](webui.md) / [webui-schema.md](webui-schema.md) +(`hermit --web`)。进程内嵌入请用 `libcstcore` + `cst_agent_hooks_t`;归档 FFI +见 `archive/desktop-tauri/src-tauri/src/ffi_bridge.*`。 -## Dev +## 归档 Tauri 开发(不保证可编) ```bash nix-shell -p rustc cargo nodejs_22 pkg-config webkitgtk_4_1 gtk3 librsvg openssl curl -cd apps/desktop && npm install && npm run tauri dev +cd archive/desktop-tauri && npm install && npm run tauri dev ``` -## Product env +## 产品环境变量 -`HERMIT_*` preferred, `CST_*` accepted (see `hermit_getenv`). Config dir `~/.config/hermit`. +`HERMIT_*` 优先,`CST_*` 仍接受(见 `hermit_getenv`)。配置目录 `~/.config/hermit`。 diff --git a/docs/plugins.md b/docs/plugins.md index d356143..35f1cb0 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -1,7 +1,12 @@ # 扩展:Skills、插件、MCP 与自定义子 agent -所有扩展只在默认工具档(`--tools default`)下加载;`--tools minimal` 完全不受影响。 -下文的“配置目录”指 `~/.config/cst-agent/`(`$XDG_CONFIG_HOME/cst-agent/`;Windows 为 `%APPDATA%\cst-agent\`)。 +Hermit 的**可替换面**是本页这些扩展;**不是**内置 core 工具 +(`read_file` / `write_file` / `apply_patch` / `run_shell` / …)或权限/沙箱——那些是 +agent 契约与 `--tools minimal` 基线,不能当用户插件卸掉。 + +所有扩展只在默认工具档(`--tools default`)下加载;`--tools minimal` 时 +`cst_extensions_load` 直接跳过。下文的“配置目录”优先 `~/.config/hermit/`, +若无则回退 `~/.config/cst-agent/`(`$XDG_CONFIG_HOME/…`;Windows 为 `%APPDATA%\…`)。 | 放在哪 | 什么 | 何时加载 | |---|---|---| diff --git a/docs/webui-schema.md b/docs/webui-schema.md new file mode 100644 index 0000000..071008c --- /dev/null +++ b/docs/webui-schema.md @@ -0,0 +1,66 @@ +# cst-webui/1 — event & state schema + +Machine-oriented summary of the HTTP/SSE engine API. Narrative + security: +[webui.md](webui.md). Peer/device sockets are separate: [parliament.md](parliament.md). + +Protocol string: `GET /api/state` → `"protocol": "cst-webui/1"`. + +## Capabilities + +`GET /api/state` includes `"capabilities": [ ... ]` listing SSE `type` values this +build emits. Clients should probe this array rather than hard-coding. + +Current set: + +| type | Mirrors hook | Notes | +|------|--------------|--------| +| `assistant_delta` | `on_assistant_delta` | streaming text | +| `think_delta` | `on_think_delta` | reasoning stream | +| `assistant_html` | (web md) | sanitized HTML chunks | +| `status` | `on_status` | `{ "message": "..." }` | +| `tool` | `on_tool_event` | phase + call | +| `tool_result` | `on_tool_result` | output + exit | +| `todo` | `on_todo` | task list text | +| `permission` | `on_permission` | wait for `POST /api/permission` | +| `ask_user` | `on_ask_user` | wait for `POST /api/ask` | +| `state` | — | full snapshot (same shape as `/api/state`) | +| `turn_begin` | `on_turn_begin` | `{ "text": "" }` | +| `turn_end` | `on_turn_end` | `{ "rc": 0\|N, "error": "" }` | +| `error` | `on_error` | `{ "kind": "turn\|cancel\|…", "message": "…" }` | + +## SSE framing + +``` +GET /api/events?since= +``` + +- `id: ` + `data: {"type":…,"seq":…, …}` +- Comment heartbeat: `: ping` + +## Primary control endpoints + +| Method | Path | Role | +|--------|------|------| +| GET | `/api/state` | snapshot + `capabilities` | +| GET | `/api/events` | SSE stream | +| POST | `/api/turn` | `{ "text": "..." }` start turn | +| POST | `/api/cancel` | cancel in-flight turn | +| POST | `/api/permission` | answer permission prompt | +| POST | `/api/ask` | answer ask_user | +| POST | `/api/quit` | shut down server | + +Full route list: [webui.md](webui.md). + +## Embedded hooks (same semantics) + +```c +cst_agent_hooks_t { + on_status, on_assistant_delta, on_think_delta, + on_tool_event, on_tool_result, on_tool_images, + on_permission, on_ask_user, on_todo, + on_turn_begin, on_turn_end, on_error, + userdata +}; +``` + +Archived FFI mirror: `archive/desktop-tauri/src-tauri/src/ffi_bridge.h` (`hermit_hooks_t`). diff --git a/docs/webui.md b/docs/webui.md index 3828fc0..50fd478 100644 --- a/docs/webui.md +++ b/docs/webui.md @@ -1,32 +1,35 @@ -# WebUI(内置 HTTP + SSE 前端) +# 引擎 HTTP + SSE API(`cst-webui/1`) -`cst-agent --web` 让同一个进程同时提供命令行以外的前端:浏览器(或任何 HTTP 客户端) -连上后能看到流式回答、工具卡片、审批弹窗和任务清单。它是 TUI 之外的第二个前端, -**引擎没有为它改一行**——两边用的都是 `cst_agent_hooks_t` 这组回调。 +`hermit --web`(兼容 `cst-agent --web`)在同一进程上暴露 **远程客户端契约**: +任意 HTTP 客户端可订阅 SSE、发起 turn、回答审批。事件与嵌入式 +`cst_agent_hooks_t` 一一对应——TUI 与 `--web` 共用引擎,不另开私有通道。 -这份文档同时是**引擎 API v1 的规范**:以后专门客户端(Tauri / Qt / 移动端)直接照它实现, -不需要读 C 代码。 +字段级摘要见 [webui-schema.md](webui-schema.md)。设备间 peer 套接字见 +[parliament.md](parliament.md)(与本 API 无关)。 + +默认只嵌入**健康页**;完整浏览器 SPA 参考实现在 `archive/web-spa`。 ## 快速开始 ```bash -# 默认只监听 127.0.0.1:8787 -cst-agent --web +# 默认只监听 127.0.0.1:8787(健康页 + /api/*) +hermit --web # 指定端口;0 = 让系统分配(启动日志里会打印实际 URL) -cst-agent --web --web-port 8787 +hermit --web --web-port 8787 # 让手机/另一台机器访问(必须给 token,否则拒绝启动在非 loopback 上) -cst-agent --web --web-host 0.0.0.0 --web-token 我自己定的口令 +hermit --web --web-host 0.0.0.0 --web-token 我自己定的口令 -# 开发前端:直接从目录读 web/,改完刷新页面即可,不用重新编译 -cst-agent --web --web-dev ./web +# 开发:读磁盘上的静态目录(健康页或归档 SPA) +hermit --web --web-dev ./web +hermit --web --web-dev archive/web-spa # 启动后自动打开浏览器 -cst-agent --web --web-open +hermit --web --web-open # 启动即跑一轮(不点页面也能用) -cst-agent --web -p "看看这个仓库有什么问题" +hermit --web -p "看看这个仓库有什么问题" ``` 启动日志: @@ -62,7 +65,8 @@ webui: token 3f9a… (append #token=3f9a… to the URL) ## 引擎 API(协议 `cst-webui/1`) 所有响应都是 JSON(`Content-Type: application/json`)。`GET /api/state` 的 -`protocol` 字段给出版本号,客户端应当先看它再决定支持哪些端点。 +`protocol` 字段给出版本号;`capabilities` 数组列出本进程会发出的 SSE `type`。 +客户端应当先看它们再决定支持哪些端点/事件。 ### 事件流 @@ -95,9 +99,11 @@ GET /api/events?since= → text/event-stream (chunked) | `permission_done` | `id`,`answer` | 问题已结束(超时/取消也会发) | | `ask_user` | `id`,`question`,`options[]` | 需要回答 | | `session` | `id` | 切换/新建了会话,客户端应重新拉 `/api/session` | -| `turn_end` | `rc`,`error` | 本轮结束(`rc=0` 正常,非 0 见 `error`) | +| `turn_begin` | `text` | 一轮开始(用户提示原文);对应 `on_turn_begin` | +| `turn_end` | `rc`,`error` | 本轮结束(`rc=0` 正常,非 0 见 `error`);对应 `on_turn_end` | +| `error` | `kind`,`message` | 结构化错误(`turn` / `cancel` / …);对应 `on_error`,常在 `turn_end` 之前 | -`state` 字段:`workspace` `session` `provider` `model` `effort` `approval` +`state` 字段:`protocol` `capabilities[]` `workspace` `session` `provider` `model` `effort` `approval` (`ask|auto|yolo`) `plan_mode` `protocol_tools` `status_busy` `todo` `context_tokens` `undo_available` `pending_images[]` `usage{requests,prompt_tokens, completion_tokens,cached_tokens,cache_miss_tokens,cost,priced,currency}` diff --git a/include/cst_agent.h b/include/cst_agent.h index 25909ac..9df4ff3 100644 --- a/include/cst_agent.h +++ b/include/cst_agent.h @@ -348,7 +348,9 @@ void cst_curl_apply_ca(void *curl); * detail is the tail of the sandboxed output. */ #define CST_PERM_SANDBOX "[sandbox] " -/* Live UI / observability hooks for a turn */ +/* Live UI / observability hooks for a turn. Every callback may be NULL. + * Remote clients prefer the HTTP/SSE mirror (docs/webui.md); embedded + * clients fill this struct (TUI, FFI). */ typedef struct { void (*on_status)(const char *msg, void *ud); void (*on_assistant_delta)(const char *text, size_t len, void *ud); @@ -375,6 +377,13 @@ typedef struct { * reading the store. */ void (*on_tool_images)(const ctl_tool_call_t *call, const cst_attachment_t *list, int n, void *ud); + /* Turn lifecycle (once around cst_agent_run_turn_ex). */ + void (*on_turn_begin)(const char *user_text, void *ud); + /* rc 0 = ok; non-zero = failure or cancel. err may be NULL or "". */ + void (*on_turn_end)(int rc, const char *err, void *ud); + /* Structured error while a turn runs. kind is a short stable token + * (llm | cancel | internal | …); message is human-readable. */ + void (*on_error)(const char *kind, const char *message, void *ud); void *userdata; } cst_agent_hooks_t; diff --git a/include/cst_webui.h b/include/cst_webui.h index b99b73a..980f0bd 100644 --- a/include/cst_webui.h +++ b/include/cst_webui.h @@ -1,13 +1,12 @@ #ifndef CST_WEBUI_H #define CST_WEBUI_H -/* The built-in WebUI: an HTTP + SSE front end for one agent process. +/* Engine HTTP + SSE API (--web): remote mirror of cst_agent_hooks_t. * - * It is a second implementation of the same interface the TUI uses - * (cst_agent_hooks_t), which is why the engine needs no changes: the turn - * runs on a worker thread and every live event goes out as a Server-Sent - * Event. The protocol is documented in docs/webui.md and is meant to be - * the contract a dedicated client speaks later. */ + * The turn runs on a worker thread; every live hook fires a Server-Sent + * Event. Protocol: docs/webui.md / docs/webui-schema.md (cst-webui/1). + * The binary embeds a minimal health page; a full SPA lives under + * archive/web-spa (use --web-dev). */ #include "cst_agent.h" @@ -25,8 +24,8 @@ typedef struct { const char *host; /* NULL = 127.0.0.1; "0.0.0.0" = every interface */ int port; /* 0 = pick a free port */ const char *token; /* NULL = generate one when needed */ - const char *root; /* serve the UI from this directory instead of the - copy embedded in the binary (development) */ + const char *root; /* serve static files from DIR instead of the + embedded health page (development / archived SPA) */ int open_browser; /* best-effort xdg-open / open / start */ /* Prompt to run once at startup (--web -p ...); NULL = wait for the UI. */ const char *prompt; diff --git a/scripts/build_apk_android.sh b/scripts/build_apk_android.sh index b6f72ed..103afaf 100755 --- a/scripts/build_apk_android.sh +++ b/scripts/build_apk_android.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# Build the Qt Quick Android APK of app/ (the engine is linked in-process, so +# ARCHIVED: Qt APK front end under archive/qt-app; not part of default Hermit builds. +# Build the Qt Quick Android APK of archive/qt-app/ (the engine is linked in-process, so # the APK opens no port). This is the front end counterpart of # scripts/build_android.sh, which cross-builds the engine alone. # @@ -150,7 +151,7 @@ cmake --build "$BUILD" --target apk -j"$JOBS" # AGP names the file after the variant and signing state # (android-build-release.apk vs ...-unsigned.apk), so take the newest one. -APK="$(find "$BUILD/app/android-build/build/outputs/apk" "$BUILD" -name '*.apk' \ +APK="$(find "$BUILD/archive/qt-app/android-build/build/outputs/apk" "$BUILD" -name '*.apk' \ -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)" [[ -n "$APK" && -f "$APK" ]] || { echo "no APK produced" >&2; exit 1; } echo diff --git a/scripts/e2e_webui.sh b/scripts/e2e_webui.sh index e42c8a0..4f8c825 100644 --- a/scripts/e2e_webui.sh +++ b/scripts/e2e_webui.sh @@ -11,8 +11,13 @@ set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd)" REPLAY="${1:-$ROOT/tests/fixtures/sse/deepseek-native-eos}" PROTO="${2:-native-eos}" -BIN="${CST_AGENT:-$ROOT/cst-agent}" -[ -x "$BIN" ] || { echo "build cst-agent first ($BIN)"; exit 1; } +BIN="${CST_AGENT:-}" +if [ -z "$BIN" ]; then + if [ -x "$ROOT/hermit" ]; then BIN="$ROOT/hermit" + elif [ -x "$ROOT/cst-agent" ]; then BIN="$ROOT/cst-agent" + fi +fi +[ -x "$BIN" ] || { echo "build hermit first ($ROOT/hermit)"; exit 1; } WORK="$(mktemp -d "${TMPDIR:-/tmp}/cst-webui-XXXXXX")" 2>/dev/null || { WORK="$ROOT/.build-tmp/e2e-webui"; rm -rf "$WORK"; mkdir -p "$WORK"; } @@ -30,6 +35,7 @@ echo "== start cst-agent --web (replay $REPLAY)" CST_REPLAY_SSE="$REPLAY" \ CST_REPLAY_CHUNK=512 \ "$BIN" --web --web-port 0 --workspace "$WORK" --new --api-key sk-e2e \ + --provider deepseek --model deepseek-chat \ --protocol "$PROTO" --tools default --yolo > "$LOG" 2>&1 & SRV_PID=$! @@ -61,10 +67,11 @@ stop_server() { [ "$st" -lt 128 ] || fail "$2 died with status $st on SIGTERM" } -# 1. the shell page and the state endpoint -curl -fsS --max-time 5 "$BASE/" | grep -q "CST-Agent" || fail "GET / is not the shell page" +# 1. the health page and the state endpoint +curl -fsS --max-time 5 "$BASE/" | grep -q "<title>Hermit API" || fail "GET / is not the health page" curl -fsS --max-time 5 "$BASE/api/state" > "$WORK/state.json" || fail "GET /api/state" grep -q '"protocol":"cst-webui/1"' "$WORK/state.json" || fail "state has no protocol tag" +grep -q '"capabilities"' "$WORK/state.json" || fail "state has no capabilities" grep -q '"session":' "$WORK/state.json" || fail "state has no session id" # 2. an unknown route is a 404, an oversized body a 413 @@ -92,6 +99,7 @@ while [ $i -lt 450 ]; do done kill "$CURL_PID" 2>/dev/null || true grep -q "event: turn_end" "$EVENTS" || fail "the turn never finished" +grep -q "event: turn_begin" "$EVENTS" || fail "no turn_begin event" grep -q "event: assistant_html" "$EVENTS" || fail "no assistant markdown was streamed" grep -q "event: tool" "$EVENTS" || fail "no tool event was streamed" grep -q '"rc":0' "$EVENTS" || fail "the turn did not end with rc=0" @@ -180,6 +188,7 @@ P_LOG="$WORK/permission-server.log" # its own log: the port parse must not # pick up the first server's URL CST_REPLAY_SSE="$REPLAY" CST_REPLAY_CHUNK=512 \ "$BIN" --web --web-port 0 --workspace "$WORK/pw" --new --api-key sk-e2e \ + --provider deepseek --model deepseek-chat \ --protocol "$PROTO" --tools default --approval ask --confirm all \ > "$P_LOG" 2>&1 & P_PID=$! @@ -229,6 +238,7 @@ echo "== token auth and --web-dev" T_LOG="$WORK/token-server.log" CST_REPLAY_SSE="$REPLAY" \ "$BIN" --web --web-port 0 --workspace "$WORK/pw" --new --api-key sk-e2e \ + --provider deepseek --model deepseek-chat \ --protocol "$PROTO" --tools default --yolo --web-dev "$ROOT/web" \ --web-token tok-e2e > "$T_LOG" 2>&1 & T_PID=$! @@ -257,11 +267,17 @@ code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \ -H 'Authorization: Bearer tok-e2' "$T_BASE/api/state")" [ "$code" = "401" ] || fail "a wrong token was accepted ($code)" -# --web-dev serves straight from the directory, so the bytes must match it -curl -fsS --max-time 5 -H 'Authorization: Bearer tok-e2e' "$T_BASE/app.js" \ - > "$WORK/app.js" || fail "GET /app.js from --web-dev" -cmp -s "$WORK/app.js" "$ROOT/web/app.js" || - fail "--web-dev did not serve web/app.js from disk" +# --web-dev serves the health page from disk +curl -fsS --max-time 5 -H 'Authorization: Bearer tok-e2e' "$T_BASE/" \ + > "$WORK/index.html" || fail "GET / from --web-dev" +cmp -s "$WORK/index.html" "$ROOT/web/index.html" || + fail "--web-dev did not serve web/index.html from disk" +curl -fsS --max-time 5 -H 'Authorization: Bearer tok-e2e' "$T_BASE/api/state" \ + > "$WORK/state.json" || fail "GET /api/state with token" +grep -q '"capabilities"' "$WORK/state.json" || + fail "/api/state missing capabilities" +grep -q 'turn_begin' "$WORK/state.json" || + fail "/api/state capabilities missing turn_begin" # POST /api/quit must return and then take the server down on its own curl -fsS --max-time 5 -X POST -H 'Authorization: Bearer tok-e2e' \ diff --git a/scripts/e2e_webui_ext.py b/scripts/e2e_webui_ext.py index 27d5c74..9482ff3 100644 --- a/scripts/e2e_webui_ext.py +++ b/scripts/e2e_webui_ext.py @@ -113,11 +113,14 @@ def port(): def settings(): - try: - with open(home + "/.config/cst-agent/settings.json", encoding="utf-8") as f: - return json.load(f) - except OSError: - return {} + for name in ("hermit", "cst-agent"): + path = home + "/.config/%s/settings.json" % name + try: + with open(path, encoding="utf-8") as f: + return json.load(f) + except OSError: + continue + return {} def busy(base): diff --git a/scripts/e2e_webui_http.py b/scripts/e2e_webui_http.py index 2667cf0..4861df2 100644 --- a/scripts/e2e_webui_http.py +++ b/scripts/e2e_webui_http.py @@ -141,8 +141,10 @@ try: check(status(r)[:1] == [200], "a client without Origin (curl) may POST") r = raw(PORT, req("GET", "/api/attach/img-0000.png", host="evil.example")) check(status(r)[:1] == [401], "session images need the Host check too (%s)" % status(r)) - r = raw(PORT, req("GET", "/app.js")) - check(status(r)[:1] == [200], "assets are served") + r = raw(PORT, req("GET", "/")) + check(status(r)[:1] == [200], "health page is served") + r = raw(PORT, req("GET", "/index.html")) + check(status(r)[:1] == [200], "embedded index.html is served") # 2. request parsing r = raw(PORT, req("GET", "/api/state%00x")) check(status(r)[:1] == [400], "an encoded NUL in the path is a 400 (%s)" % status(r)) @@ -206,8 +208,10 @@ try: ws2 = os.path.join(root, "b") os.makedirs(ws2) p, PORT, log = start(ws2, "--web-token", "tok-x") - r = raw(PORT, req("GET", "/app.js")) - check(status(r)[:1] == [200], "token mode: app.js loads without the token (%s)" % status(r)) + r = raw(PORT, req("GET", "/")) + check(status(r)[:1] == [200], "token mode: health page loads without the token (%s)" % status(r)) + r = raw(PORT, req("GET", "/index.html")) + check(status(r)[:1] == [200], "token mode: index.html loads without the token (%s)" % status(r)) r = raw(PORT, req("GET", "/api/state")) check(status(r)[:1] == [401], "token mode: the API needs it") r = raw(PORT, req("GET", "/api/attach/img-0000.png")) diff --git a/scripts/e2e_webui_sessions.py b/scripts/e2e_webui_sessions.py index a9ae7bd..c611c8e 100644 --- a/scripts/e2e_webui_sessions.py +++ b/scripts/e2e_webui_sessions.py @@ -166,9 +166,23 @@ def messages(base): return call(base, "/api/session")[1].get("messages", []) +def _is_prompt(m): + """Real user prompts only — skip notes, tool results, and engine meta rows.""" + if m.get("role") != "user" or m.get("note") or m.get("tool_result"): + return False + c = m.get("content") + if not isinstance(c, str) or not c: + return False + kind = (m.get("meta") or {}).get("kind") if isinstance(m.get("meta"), dict) else None + if kind: + return False + if c.startswith("[cst"): + return False + return True + + def prompts(base): - return [m["content"] for m in messages(base) - if m["role"] == "user" and not m.get("note") and not m.get("tool_result")] + return [m["content"] for m in messages(base) if _is_prompt(m)] def answers(base): diff --git a/scripts/e2e_webui_settings.py b/scripts/e2e_webui_settings.py index 92611a6..f02f390 100644 --- a/scripts/e2e_webui_settings.py +++ b/scripts/e2e_webui_settings.py @@ -151,8 +151,8 @@ def serve(handler): api_a, api_b = serve(A), serve(B) ws = tempfile.mkdtemp(prefix="cst-set-") home = tempfile.mkdtemp(prefix="cst-home-") -cfgdir = home + "/.config/cst-agent" -os.makedirs(cfgdir) +cfgdir = home + "/.config/hermit" +os.makedirs(cfgdir, exist_ok=True) with open(cfgdir + "/models.json", "w") as f: json.dump({"providers": {"fakeb": {"api_base": "http://127.0.0.1:%d/v1" % api_b.server_address[1], "key_env": "FAKEB_API_KEY", "default_model": ""}}, diff --git a/scripts/package_windows_app.sh b/scripts/package_windows_app.sh index 63f1a03..c6d9299 100755 --- a/scripts/package_windows_app.sh +++ b/scripts/package_windows_app.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# Build and pack the Windows GUI (app/cst-agent-app.exe) from Linux. +# ARCHIVED: Qt GUI lives under archive/qt-app; not part of default Hermit builds. +# Build and pack the Windows GUI (archive/qt-app/cst-agent-app.exe) from Linux. # # scripts/package_windows_app.sh (run it from the repo root) # nix-shell shell-win.nix --run scripts/package_windows_app.sh @@ -161,7 +162,7 @@ cmake -S "$ROOT" -B "$BUILD" -G Ninja \ # POSIX (tests/test_tools_ext.c calls mkdir with two arguments), exactly as in # the TUI cross build. cmake --build "$BUILD" -j"$(nproc)" --target cst-agent-app -EXE="$BUILD/app/cst-agent-app.exe" +EXE="$BUILD/archive/qt-app/cst-agent-app.exe" [[ -f "$EXE" ]] || { echo "no cst-agent-app.exe" >&2; exit 1; } # --- the package ----------------------------------------------------------- diff --git a/src/agent_loop.c b/src/agent_loop.c index 8aa18dd..0d38c20 100644 --- a/src/agent_loop.c +++ b/src/agent_loop.c @@ -3519,6 +3519,8 @@ static int run_turn_once(cst_agent_t *a, const char *user_text, int cst_agent_run_turn_ex(cst_agent_t *a, const char *text, const cst_agent_hooks_t *hooks, char *err, size_t n) { if (!a || !text) return -1; + if (hooks && hooks->on_turn_begin) + hooks->on_turn_begin(text, hooks->userdata); if (!a->cfg.depth) cst_runtime_begin(a->cfg.runtime, text); int rc; char *steering = NULL; @@ -3538,6 +3540,12 @@ int cst_agent_run_turn_ex(cst_agent_t *a, const char *text, cst_runtime_finish(a->cfg.runtime, rc ? "partial" : "completed", rc && err ? err : "", 0, ignored, sizeof(ignored)); cJSON_Delete(j); free(state); } + if (rc != 0 && hooks && hooks->on_error && err && err[0]) { + const char *kind = a->cancel ? "cancel" : "turn"; + hooks->on_error(kind, err, hooks->userdata); + } + if (hooks && hooks->on_turn_end) + hooks->on_turn_end(rc, (err && err[0]) ? err : NULL, hooks->userdata); return rc; } diff --git a/src/agent_store.c b/src/agent_store.c index 96db62f..11c1993 100644 --- a/src/agent_store.c +++ b/src/agent_store.c @@ -481,9 +481,11 @@ int cst_agent_store_info(const char *workspace, const char *id, const cJSON *c = cJSON_GetObjectItemCaseSensitive(o, "content"); const cJSON *k = cJSON_GetObjectItemCaseSensitive( cJSON_GetObjectItemCaseSensitive(o, "meta"), "kind"); - const int note = cJSON_IsString(k) && strcmp(k->valuestring, "note") == 0; - if (cJSON_IsString(r) && strcmp(r->valuestring, "user") == 0 && !note && - cJSON_IsString(c) && c->valuestring[0]) { + /* Skip engine-injected user rows (note, task_controls, job_results, + * completion_repair, …): the title falls back to the person's prompt. */ + const int meta_kind = cJSON_IsString(k); + if (cJSON_IsString(r) && strcmp(r->valuestring, "user") == 0 && + !meta_kind && cJSON_IsString(c) && c->valuestring[0]) { clip_line(info->first_user, sizeof(info->first_user), c->valuestring); want_first = 0; } diff --git a/src/main.c b/src/main.c index 5c07a3c..b2776d1 100644 --- a/src/main.c +++ b/src/main.c @@ -80,12 +80,13 @@ static void usage(const char *argv0) { " --peer-dial H:N dial host:port and complete handshake\n" " --discover-wait MS advertise and list same-Realm peers\n" " -H, --headless -p TEXT run one prompt without the TUI\n" - " --web serve the UI over HTTP on 127.0.0.1:8787\n" + " --web serve HTTP/SSE engine API on 127.0.0.1:8787\n" " --web-port N port (default 8787, 0 = pick a free one)\n" " --web-host ADDR bind address (default 127.0.0.1; a\n" " non-loopback address requires a token)\n" " --web-token T require ?token=T / Authorization: Bearer T\n" - " --web-dev DIR serve the UI from DIR (development)\n" + " --web-dev DIR serve static files from DIR (health page or\n" + " archive/web-spa)\n" " --web-open open the browser at startup\n" " -a, --ask-key enter and save the provider's API key\n" " -k, --api-key KEY use KEY for this run only (not saved)\n" @@ -400,7 +401,7 @@ int main(int argc, char **argv) { const char *peer_dial_spec = NULL, *parliament_instance = NULL; const char *new_project_id = NULL; int peer_listen_port = 0, discover_wait_ms = 3000, discover_peer_port = 9100; - /* WebUI (--web): a second front end on the same engine. */ + /* --web: HTTP/SSE engine API (cst-webui/1) on the same process. */ int web_mode = 0; cst_webui_opts_t web; memset(&web, 0, sizeof(web)); @@ -410,9 +411,14 @@ int main(int argc, char **argv) { fprintf(stderr, "CST_WEB_PORT=%s is not a port (0-65535)\n", env); return 1; } - web.host = hermit_getenv("WEB_HOST"); - web.token = hermit_getenv("WEB_TOKEN"); - web.root = hermit_getenv("WEB_ROOT"); + { + const char *h = hermit_getenv("WEB_HOST"); + const char *t = hermit_getenv("WEB_TOKEN"); + const char *r = hermit_getenv("WEB_ROOT"); + web.host = (h && h[0]) ? h : NULL; + web.token = (t && t[0]) ? t : NULL; + web.root = (r && r[0]) ? r : NULL; + } static struct option opts[] = { {"workspace", required_argument, 0, 'w'}, diff --git a/src/tui.c b/src/tui.c index eb0bba3..e3f62d5 100644 --- a/src/tui.c +++ b/src/tui.c @@ -6348,7 +6348,7 @@ static void cmd_quit(tui_t *t, char *arg, int *quit) { *quit = 1; } -/* /web: the WebUI is a second front end for the same engine and is designed to +/* /web: start the HTTP/SSE engine API in the background; designed to * run as its own process, so this starts one on the same workspace and leaves * the TUI holding the terminal. The child gets a fresh session: two writers on * one session file is the one thing that would corrupt it. */ @@ -6478,13 +6478,12 @@ static void cmd_web(tui_t *t, char *arg, int *quit) { cst_agent_store_set_current(ws, sid); char msg[4200]; if (died) - snprintf(msg, sizeof(msg), "the WebUI did not start (port busy?): see %s", + snprintf(msg, sizeof(msg), "the API server did not start (port busy?): see %s", logf); else if (url[0]) - snprintf(msg, sizeof(msg), "WebUI started in the background: %s (new session)", - url); + snprintf(msg, sizeof(msg), "HTTP/SSE API started: %s (new session)", url); else - snprintf(msg, sizeof(msg), "WebUI starting in the background " + snprintf(msg, sizeof(msg), "HTTP/SSE API starting " "(http://127.0.0.1:%d/ unless CST_WEB_PORT says " "otherwise)", CST_WEBUI_DEFAULT_PORT); push_system_line(t, msg); @@ -6558,7 +6557,7 @@ static const slash_cmd_t SLASH[] = { {"task", "jobs", "[state|readonly|write|direct|auto|pause|resume]", "task controls and jobs", 1, cmd_task}, {"skill", "", "<name> [task]", "run a task with a skill loaded", 0, cmd_skill}, - {"web", "", "", "start the WebUI in the background (127.0.0.1:8787)", 1, + {"web", "", "", "start the HTTP/SSE API in the background (127.0.0.1:8787)", 1, cmd_web}, {"quit", "q exit", "", "quit", 1, cmd_quit}, }; diff --git a/src/webui.c b/src/webui.c index f70324f..6b3a68d 100644 --- a/src/webui.c +++ b/src/webui.c @@ -260,6 +260,15 @@ static cJSON *wui_state_new(wui_t *w) { cst_usage_t sess = {0}, turn = {0}; cst_agent_usage(w->agent, &sess, &turn); cJSON_AddStringToObject(o, "protocol", "cst-webui/1"); + { + static const char *const caps[] = { + "assistant_delta", "think_delta", "assistant_html", "status", "tool", + "tool_result", "todo", "permission", "ask_user", + "state", "turn_begin", "turn_end", "error"}; + cJSON *cap = cJSON_AddArrayToObject(o, "capabilities"); + for (size_t i = 0; cap && i < sizeof(caps) / sizeof(caps[0]); i++) + cJSON_AddItemToArray(cap, cJSON_CreateString(caps[i])); + } cJSON_AddStringToObject(o, "workspace", cfg->workspace_root); cJSON_AddStringToObject(o, "session", sid); cJSON_AddStringToObject(o, "provider", cfg->provider); @@ -562,6 +571,44 @@ static void on_status(const char *msg, void *ud) { wui_emit_str((wui_t *)ud, "status", "message", msg); } +static void on_turn_begin(const char *user_text, void *ud) { + cJSON *o = cJSON_CreateObject(); + if (!o) + return; + cJSON_AddStringToObject(o, "text", user_text ? user_text : ""); + wui_emit((wui_t *)ud, "turn_begin", o); +} + +static void on_turn_end_hook(int rc, const char *err, void *ud) { + wui_t *w = ud; + /* Flush markdown outside the emit path used below. */ + pthread_mutex_lock(&w->mu); + cst_web_md_t *md = w->md; + w->md = NULL; + w->busy = 0; + pthread_mutex_unlock(&w->mu); + if (md) { + cst_web_md_finish(md); + cst_web_md_free(md); + } + cJSON *o = cJSON_CreateObject(); + if (o) { + cJSON_AddNumberToObject(o, "rc", rc); + cJSON_AddStringToObject(o, "error", err ? err : ""); + wui_emit(w, "turn_end", o); + } + wui_emit_state(w); +} + +static void on_error_hook(const char *kind, const char *message, void *ud) { + cJSON *o = cJSON_CreateObject(); + if (!o) + return; + cJSON_AddStringToObject(o, "kind", kind ? kind : ""); + cJSON_AddStringToObject(o, "message", message ? message : ""); + wui_emit((wui_t *)ud, "error", o); +} + static void on_assistant_delta(const char *text, size_t len, void *ud) { wui_t *w = ud; if (!w->md) @@ -988,27 +1035,6 @@ typedef struct { char *prompt; } wui_turn_t; -static void wui_end_turn(wui_t *w, int rc, const char *err) { - /* Flush the markdown *outside* the lock: flushing emits the last HTML - * chunks, and wui_emit takes w->mu itself. */ - pthread_mutex_lock(&w->mu); - cst_web_md_t *md = w->md; - w->md = NULL; - w->busy = 0; - pthread_mutex_unlock(&w->mu); - if (md) { - cst_web_md_finish(md); - cst_web_md_free(md); - } - cJSON *o = cJSON_CreateObject(); - if (o) { - cJSON_AddNumberToObject(o, "rc", rc); - cJSON_AddStringToObject(o, "error", err ? err : ""); - wui_emit(w, "turn_end", o); - } - wui_emit_state(w); -} - static void *wui_turn_main(void *arg) { wui_turn_t *t = arg; wui_t *w = t->w; @@ -1022,13 +1048,16 @@ static void *wui_turn_main(void *arg) { hooks.on_permission = on_permission; hooks.on_ask_user = on_ask_user; hooks.on_todo = on_todo; + hooks.on_turn_begin = on_turn_begin; + hooks.on_turn_end = on_turn_end_hook; + hooks.on_error = on_error_hook; hooks.userdata = w; char err[512]; err[0] = 0; wui_reset_carry(w); - int rc = cst_agent_run_turn_ex(w->agent, t->prompt, &hooks, err, sizeof(err)); + (void)cst_agent_run_turn_ex(w->agent, t->prompt, &hooks, err, sizeof(err)); wui_reset_carry(w); /* a dangling partial character is dropped */ - wui_end_turn(w, rc, err); + /* turn_end / error / busy cleared via hooks.on_turn_end */ free(t->prompt); free(t); pthread_mutex_lock(&w->mu); @@ -2632,7 +2661,9 @@ int cst_webui_run(cst_agent_t *agent, const cst_webui_opts_t *opts) { pthread_mutex_init(&w->mu, NULL); w->agent = agent; w->perm_timeout_s = opts->perm_timeout_s; - w->root = opts->root; + /* hermit_getenv returns "" when unset; only a non-empty path enables + * --web-dev / WEB_ROOT disk serving over the embedded health page. */ + w->root = (opts->root && opts->root[0]) ? opts->root : NULL; { cst_settings_t st; cst_settings_load(&st); diff --git a/tests/fixtures/sse/deepseek-native-eos/0005.sse b/tests/fixtures/sse/deepseek-native-eos/0005.sse new file mode 100644 index 0000000..1d9792f --- /dev/null +++ b/tests/fixtures/sse/deepseek-native-eos/0005.sse @@ -0,0 +1,152 @@ +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"修复"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"完成"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"。\n\n"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" "},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"问题"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"`"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"calc"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":".c"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"`"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" "},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"中"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" `"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"add"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"`"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" "},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"返回"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" `"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"a"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"-b"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"`"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"("},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"实现"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"成了"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"减法"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":")。\n"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" "},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"修改"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"改为"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" `"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"return"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"+b"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":";"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"`\n"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" "},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"验证"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"`"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"make"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"`"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" "},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"成功"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"`"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"g"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"cc"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" -"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" calc"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":".c"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" -"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"o"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" calc"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":".o"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"`"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" "},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"无"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"错误"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"/"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"警告"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"已"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"生成"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":" `"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"calc"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":".o"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"`"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":"。"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"de00fc60-b7a9-416c-8a40-de4c9bd4bac6","object":"chat.completion.chunk","created":1791129418,"model":"deepseek-flash","system_fingerprint":"aeb56401ca74e127821c4f9126dcb669","choices":[{"index":0,"delta":{"content":""},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":1420,"completion_tokens":73,"total_tokens":1493,"prompt_tokens_details":{"cached_tokens":1280},"prompt_cache_hit_tokens":1280,"prompt_cache_miss_tokens":140}} + +data: [DONE] + diff --git a/web/index.html b/web/index.html index 52b5f82..46c92b5 100644 --- a/web/index.html +++ b/web/index.html @@ -1,250 +1,30 @@ <!DOCTYPE html> -<html lang="zh-CN" data-theme="dark"> +<html lang="en"> <head> -<meta charset="utf-8"> -<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> -<meta name="color-scheme" content="dark light"> -<title>Hermit - + + +Hermit API + - - - -
- - - - - -
-
- - - - -
- - -
- - - - -
- -
-
-

准备好开始构建了吗

-
-
-
- - - -
- - - - - - - -
- -
- - - - - - -
-
-
- 就绪 - - -
- -
-
- - - - -
- -
- - - - - - +

HERMIT

+

Engine HTTP + SSE API (cst-webui/1). This process serves the + control plane, not a full browser app.

+ +

Full SPA reference: archive/web-spa — + hermit --web --web-dev archive/web-spa. + Contract: docs/webui.md.