@turmind/halo 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/admin-out/404.html +1 -0
- package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_buildManifest.js +1 -0
- package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_ssgManifest.js +1 -0
- package/admin-out/_next/static/chunks/1d2d5650-810a3ca1ae20cab5.js +18 -0
- package/admin-out/_next/static/chunks/200.3b8f8a74f005a134.js +1 -0
- package/admin-out/_next/static/chunks/220.3dc41cd6269b88e6.js +1 -0
- package/admin-out/_next/static/chunks/298-d6b224b569da4de0.js +1 -0
- package/admin-out/_next/static/chunks/304eda7d.b1ee7f523653e027.js +1 -0
- package/admin-out/_next/static/chunks/330.3b784cf0dcd69163.js +1 -0
- package/admin-out/_next/static/chunks/442.f4302087bddb1ec1.js +1 -0
- package/admin-out/_next/static/chunks/443.d1b289781793f8de.js +25 -0
- package/admin-out/_next/static/chunks/503-7c6d9935df6e0cea.js +1 -0
- package/admin-out/_next/static/chunks/533.5f2d4bfe3d076095.js +1 -0
- package/admin-out/_next/static/chunks/579.a2718ddd395a2f3a.js +1 -0
- package/admin-out/_next/static/chunks/635-3dd0043ed0520b9d.js +1 -0
- package/admin-out/_next/static/chunks/65.859665ba6717967a.js +1 -0
- package/admin-out/_next/static/chunks/661.a4fe93f3f2a5e871.js +1 -0
- package/admin-out/_next/static/chunks/688.4e014ac6cfc7e152.js +1 -0
- package/admin-out/_next/static/chunks/868.0c95dfe283400a00.js +1 -0
- package/admin-out/_next/static/chunks/app/_not-found/page-8e35399c8fd5016d.js +1 -0
- package/admin-out/_next/static/chunks/app/layout-58f773336e6fce61.js +1 -0
- package/admin-out/_next/static/chunks/app/page-cc8043335282d745.js +1 -0
- package/admin-out/_next/static/chunks/db534642.09d3c650ff587175.js +1 -0
- package/admin-out/_next/static/chunks/fb4ec4d6-3fa44f69484f91b9.js +1 -0
- package/admin-out/_next/static/chunks/framework-c3031ae54a63fe16.js +1 -0
- package/admin-out/_next/static/chunks/main-14b48e3c2588354f.js +1 -0
- package/admin-out/_next/static/chunks/main-app-483b436873410ddc.js +1 -0
- package/admin-out/_next/static/chunks/pages/_app-b2fc6e5f920d1b1a.js +1 -0
- package/admin-out/_next/static/chunks/pages/_error-f3696a8f3d67d150.js +1 -0
- package/admin-out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/admin-out/_next/static/chunks/webpack-8bd7ef88a6527feb.js +1 -0
- package/admin-out/_next/static/css/659eccb5db697b76.css +1 -0
- package/admin-out/_next/static/css/8fce4e1a54dac12a.css +3 -0
- package/admin-out/_next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
- package/admin-out/_next/static/media/21350d82a1f187e9-s.woff2 +0 -0
- package/admin-out/_next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
- package/admin-out/_next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
- package/admin-out/_next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
- package/admin-out/_next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- package/admin-out/_next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
- package/admin-out/favicon.ico +0 -0
- package/admin-out/icon.svg +18 -0
- package/admin-out/index.html +1 -0
- package/admin-out/index.txt +23 -0
- package/admin-out/monaco/vs/_commonjsHelpers-CT9FvmAN.js +1 -0
- package/admin-out/monaco/vs/abap-D-t0cyap.js +1 -0
- package/admin-out/monaco/vs/apex-CcIm7xu6.js +1 -0
- package/admin-out/monaco/vs/assets/css.worker-HnVq6Ewq.js +93 -0
- package/admin-out/monaco/vs/assets/editor.worker-Be8ye1pW.js +26 -0
- package/admin-out/monaco/vs/assets/html.worker-B51mlPHg.js +470 -0
- package/admin-out/monaco/vs/assets/json.worker-DKiEKt88.js +58 -0
- package/admin-out/monaco/vs/assets/ts.worker-CMbG-7ft.js +67731 -0
- package/admin-out/monaco/vs/azcli-BA0tQDCg.js +1 -0
- package/admin-out/monaco/vs/basic-languages/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/bat-C397hTD6.js +1 -0
- package/admin-out/monaco/vs/bicep-DF5aW17k.js +2 -0
- package/admin-out/monaco/vs/cameligo-plsz8qhj.js +1 -0
- package/admin-out/monaco/vs/clojure-Y2auQMzK.js +1 -0
- package/admin-out/monaco/vs/coffee-Bu45yuWE.js +1 -0
- package/admin-out/monaco/vs/cpp-CkKPQIni.js +1 -0
- package/admin-out/monaco/vs/csharp-CX28MZyh.js +1 -0
- package/admin-out/monaco/vs/csp-D8uWnyxW.js +1 -0
- package/admin-out/monaco/vs/css-CaeNmE3S.js +3 -0
- package/admin-out/monaco/vs/cssMode-CjiAH6dQ.js +1 -0
- package/admin-out/monaco/vs/cypher-DVThT8BS.js +1 -0
- package/admin-out/monaco/vs/dart-CmGfCvrO.js +1 -0
- package/admin-out/monaco/vs/dockerfile-CZqqYdch.js +1 -0
- package/admin-out/monaco/vs/ecl-30fUercY.js +1 -0
- package/admin-out/monaco/vs/editor/editor.main.css +1 -0
- package/admin-out/monaco/vs/editor/editor.main.js +5 -0
- package/admin-out/monaco/vs/editor.api-CalNCsUg.js +903 -0
- package/admin-out/monaco/vs/elixir-xjPaIfzF.js +1 -0
- package/admin-out/monaco/vs/flow9-DqtmStfK.js +1 -0
- package/admin-out/monaco/vs/freemarker2-Cz_sV6Md.js +3 -0
- package/admin-out/monaco/vs/fsharp-BOMdg4U1.js +1 -0
- package/admin-out/monaco/vs/go-D_hbi-Jt.js +1 -0
- package/admin-out/monaco/vs/graphql-CKUU4kLG.js +1 -0
- package/admin-out/monaco/vs/handlebars-OwglfO-1.js +1 -0
- package/admin-out/monaco/vs/hcl-DTaboeZW.js +1 -0
- package/admin-out/monaco/vs/html-Pa1xEWsY.js +1 -0
- package/admin-out/monaco/vs/htmlMode-Bz67EXwp.js +1 -0
- package/admin-out/monaco/vs/ini-CsNwO04R.js +1 -0
- package/admin-out/monaco/vs/java-CI4ZMsH9.js +1 -0
- package/admin-out/monaco/vs/javascript-PczUCGdz.js +1 -0
- package/admin-out/monaco/vs/jsonMode-DULH5oaX.js +7 -0
- package/admin-out/monaco/vs/julia-BwzEvaQw.js +1 -0
- package/admin-out/monaco/vs/kotlin-IUYPiTV8.js +1 -0
- package/admin-out/monaco/vs/language/css/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/html/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/json/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/typescript/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/less-C0eDYdqa.js +2 -0
- package/admin-out/monaco/vs/lexon-iON-Kj97.js +1 -0
- package/admin-out/monaco/vs/liquid-DqKjdPGy.js +1 -0
- package/admin-out/monaco/vs/loader.js +1368 -0
- package/admin-out/monaco/vs/lspLanguageFeatures-kM9O9rjY.js +4 -0
- package/admin-out/monaco/vs/lua-DtygF91M.js +1 -0
- package/admin-out/monaco/vs/m3-CsR4AuFi.js +1 -0
- package/admin-out/monaco/vs/markdown-C_rD0bIw.js +1 -0
- package/admin-out/monaco/vs/mdx-DEWtB1K5.js +1 -0
- package/admin-out/monaco/vs/mips-CiYP61RB.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-D2OdxNBt.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-DO3azKX8.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-EcChJV6a.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-qLAYrEOP.js +1 -0
- package/admin-out/monaco/vs/msdax-C38-sJlp.js +1 -0
- package/admin-out/monaco/vs/mysql-CdtbpvbG.js +1 -0
- package/admin-out/monaco/vs/nls.messages-loader.js +1 -0
- package/admin-out/monaco/vs/nls.messages.cs.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.de.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.es.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.fr.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.it.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.ja.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.js.js +10 -0
- package/admin-out/monaco/vs/nls.messages.ko.js.js +25 -0
- package/admin-out/monaco/vs/nls.messages.pl.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.pt-br.js.js +6 -0
- package/admin-out/monaco/vs/nls.messages.ru.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.tr.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.zh-cn.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.zh-tw.js.js +15 -0
- package/admin-out/monaco/vs/objective-c-CntZFaHX.js +1 -0
- package/admin-out/monaco/vs/pascal-r6kuqfl_.js +1 -0
- package/admin-out/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
- package/admin-out/monaco/vs/perl-DABw_TcH.js +1 -0
- package/admin-out/monaco/vs/pgsql-me_jFXeX.js +1 -0
- package/admin-out/monaco/vs/php-D_kh-9LK.js +1 -0
- package/admin-out/monaco/vs/pla-VfZjczW0.js +1 -0
- package/admin-out/monaco/vs/postiats-BBSzz8Pk.js +1 -0
- package/admin-out/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
- package/admin-out/monaco/vs/powershell-B-7ap1zc.js +1 -0
- package/admin-out/monaco/vs/protobuf-BmtuEB1A.js +2 -0
- package/admin-out/monaco/vs/pug-BRpRNeEb.js +1 -0
- package/admin-out/monaco/vs/python-Cr0UkIbn.js +1 -0
- package/admin-out/monaco/vs/qsharp-BzsFaUU9.js +1 -0
- package/admin-out/monaco/vs/r-f8dDdrp4.js +1 -0
- package/admin-out/monaco/vs/razor-BYAHOTkz.js +1 -0
- package/admin-out/monaco/vs/redis-fvZQY4PI.js +1 -0
- package/admin-out/monaco/vs/redshift-45Et0LQi.js +1 -0
- package/admin-out/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
- package/admin-out/monaco/vs/ruby-CZO8zYTz.js +1 -0
- package/admin-out/monaco/vs/rust-Bfetafyc.js +1 -0
- package/admin-out/monaco/vs/sb-3GYllVck.js +1 -0
- package/admin-out/monaco/vs/scala-foMgrKo1.js +1 -0
- package/admin-out/monaco/vs/scheme-CHdMtr7p.js +1 -0
- package/admin-out/monaco/vs/scss-C1cmLt9V.js +3 -0
- package/admin-out/monaco/vs/shell-ClXCKCEW.js +1 -0
- package/admin-out/monaco/vs/solidity-MZ6ExpPy.js +1 -0
- package/admin-out/monaco/vs/sophia-DWkuSsPQ.js +1 -0
- package/admin-out/monaco/vs/sparql-AUGFYSyk.js +1 -0
- package/admin-out/monaco/vs/sql-32GpJSV2.js +1 -0
- package/admin-out/monaco/vs/st-CuDFIVZ_.js +1 -0
- package/admin-out/monaco/vs/swift-n-2HociN.js +3 -0
- package/admin-out/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
- package/admin-out/monaco/vs/tcl-D74tq1nH.js +1 -0
- package/admin-out/monaco/vs/tsMode-CZz1Umrk.js +11 -0
- package/admin-out/monaco/vs/twig-C6taOxMV.js +1 -0
- package/admin-out/monaco/vs/typescript-DfOrAzoV.js +1 -0
- package/admin-out/monaco/vs/typespec-D-PIh9Xw.js +1 -0
- package/admin-out/monaco/vs/vb-Dyb2648j.js +1 -0
- package/admin-out/monaco/vs/wgsl-BhLXMOR0.js +298 -0
- package/admin-out/monaco/vs/workers-DcJshg-q.js +1 -0
- package/admin-out/monaco/vs/xml-CdsdnY8S.js +1 -0
- package/admin-out/monaco/vs/yaml-DYGvmE88.js +1 -0
- package/bin/halo.js +9 -0
- package/bundled-docs/design/agent.md +145 -0
- package/bundled-docs/design/architecture.md +270 -0
- package/bundled-docs/design/background-dispatch.md +151 -0
- package/bundled-docs/design/command.md +58 -0
- package/bundled-docs/design/cron.md +88 -0
- package/bundled-docs/design/evolution.md +383 -0
- package/bundled-docs/design/express-self.md +145 -0
- package/bundled-docs/design/feishu.md +224 -0
- package/bundled-docs/design/halo-city.md +112 -0
- package/bundled-docs/design/prompt-system.md +411 -0
- package/bundled-docs/design/session.md +321 -0
- package/bundled-docs/design/slack.md +212 -0
- package/bundled-docs/design/storage.md +578 -0
- package/bundled-docs/design/telegram.md +161 -0
- package/bundled-docs/design/web.md +222 -0
- package/bundled-docs/design/wechat.md +146 -0
- package/bundled-docs/design/ws.md +144 -0
- package/bundled-docs/dev/acp-adapter.md +337 -0
- package/bundled-docs/dev/add-channel.md +376 -0
- package/bundled-docs/dev/add-model-provider.md +386 -0
- package/bundled-docs/dev/api.md +686 -0
- package/bundled-docs/dev/deploy.md +198 -0
- package/bundled-docs/dev/desktop-packaging.md +327 -0
- package/bundled-docs/dev/env.md +158 -0
- package/bundled-docs/dev/logging.md +85 -0
- package/bundled-docs/dev/previews.md +225 -0
- package/bundled-docs/dev/tools.md +331 -0
- package/bundled-docs/guide/README.md +29 -0
- package/bundled-docs/guide/agents.md +264 -0
- package/bundled-docs/guide/channels/README.md +62 -0
- package/bundled-docs/guide/channels/acp.md +150 -0
- package/bundled-docs/guide/channels/feishu.md +154 -0
- package/bundled-docs/guide/channels/slack.md +156 -0
- package/bundled-docs/guide/channels/telegram.md +112 -0
- package/bundled-docs/guide/channels/web.md +149 -0
- package/bundled-docs/guide/channels/wechat.md +110 -0
- package/bundled-docs/guide/chat.md +112 -0
- package/bundled-docs/guide/cli.md +197 -0
- package/bundled-docs/guide/getting-started.md +93 -0
- package/bundled-docs/guide/secrets-and-credentials.md +161 -0
- package/bundled-docs/guide/sessions.md +104 -0
- package/bundled-docs/guide/skills.md +302 -0
- package/bundled-docs/guide/testing-agents-and-skills.md +136 -0
- package/bundled-docs/guide/workspace.md +131 -0
- package/bundled-docs/plans/cron-tasks.md +362 -0
- package/bundled-docs/plans/design-suggestions.md +203 -0
- package/bundled-docs/plans/issue-session-deep-nesting.md +208 -0
- package/bundled-docs/plans/launch-plan.md +111 -0
- package/bundled-docs/plans/md-and-agent-refactor.md +197 -0
- package/bundled-docs/plans/remote-mac-worker.md +196 -0
- package/bundled-docs/plans/roadmap.md +40 -0
- package/bundled-docs/plans/self-evolution.md +1379 -0
- package/bundled-docs/plans/todo.md +32 -0
- package/bundled-docs/plans/weixin-channel.md +255 -0
- package/bundled-docs/requirements/agent.md +92 -0
- package/bundled-docs/requirements/chat.md +81 -0
- package/bundled-docs/requirements/command.md +130 -0
- package/bundled-docs/requirements/editor.md +82 -0
- package/bundled-docs/requirements/explorer.md +89 -0
- package/bundled-docs/requirements/overview.md +62 -0
- package/bundled-docs/requirements/session.md +73 -0
- package/bundled-docs/requirements/settings.md +224 -0
- package/bundled-docs/requirements/skill.md +125 -0
- package/bundled-docs/requirements/source-control.md +82 -0
- package/bundled-docs/requirements/terminal.md +118 -0
- package/bundled-docs/requirements/workspace.md +80 -0
- package/bundled-docs/test/docs-seeding.md +101 -0
- package/bundled-docs/test/session.md +374 -0
- package/dist/index.js +26750 -0
- package/package.json +67 -0
- package/templates/INSTRUCTIONS.md +25 -0
- package/templates/agents/__apply_agent__/AGENT.md +229 -0
- package/templates/agents/__apply_agent__/agent.yaml +30 -0
- package/templates/agents/__evo_agent__/AGENT.md +323 -0
- package/templates/agents/__evo_agent__/agent.yaml +29 -0
- package/templates/agents/__score__/AGENT.md +177 -0
- package/templates/agents/__score__/agent.yaml +29 -0
- package/templates/agents/deep-executor/AGENT.md +60 -0
- package/templates/agents/deep-executor/agent.yaml +25 -0
- package/templates/agents/default/AGENT.md +38 -0
- package/templates/agents/default/agent.yaml +51 -0
- package/templates/agents/executor/AGENT.md +38 -0
- package/templates/agents/executor/agent.yaml +25 -0
- package/templates/aliases.yaml +26 -0
- package/templates/canvas/self.html +660 -0
- package/templates/config.yaml +69 -0
- package/templates/models/anthropic.yaml +47 -0
- package/templates/models/aws-bedrock-claude-invoke.yaml +156 -0
- package/templates/models/aws-bedrock-mantle.yaml +89 -0
- package/templates/models/deepseek.yaml +62 -0
- package/templates/models/doubao.yaml +137 -0
- package/templates/models/hunyuan.yaml +56 -0
- package/templates/models/kimi.yaml +41 -0
- package/templates/models/mimo-token-plan-china.yaml +61 -0
- package/templates/models/minimax.yaml +80 -0
- package/templates/models/openai.yaml +51 -0
- package/templates/models/qwen.yaml +78 -0
- package/templates/optional-skills/tavily-web-search/SKILL.md +91 -0
- package/templates/optional-skills/tavily-web-search/config.yaml +10 -0
- package/templates/prompts/all/TOOL_GUIDELINES.md +35 -0
- package/templates/prompts/all/TOOL_SHELL.md +4 -0
- package/templates/prompts/all/TOOL_SHELL.windows.md +7 -0
- package/templates/prompts/bootstrap/BOOTSTRAP.md +26 -0
- package/templates/prompts/root/WORKSPACE_MEMORY.md +11 -0
- package/templates/schema.sql +30 -0
- package/templates/skills/acp/SKILL.md +236 -0
- package/templates/skills/acp/templates/SKILL.md.tmpl +74 -0
- package/templates/skills/acp/templates/ask.py +176 -0
- package/templates/skills/acp/templates/config.yaml.tmpl +33 -0
- package/templates/skills/agent/SKILL.md +128 -0
- package/templates/skills/aws-knowledge/SKILL.md +82 -0
- package/templates/skills/aws-knowledge/scripts/aws_kb.py +61 -0
- package/templates/skills/cron/SKILL.md +217 -0
- package/templates/skills/cron/manage-cron.py +434 -0
- package/templates/skills/halo/SKILL.md +98 -0
- package/templates/skills/nova-web-search/SKILL.md +65 -0
- package/templates/skills/self/SKILL.md +139 -0
- package/templates/skills/send-file/SKILL.md +28 -0
- package/templates/skills/send-file/feishu.md +16 -0
- package/templates/skills/send-file/slack.md +7 -0
- package/templates/skills/send-file/telegram.md +9 -0
- package/templates/skills/send-file/web.md +6 -0
- package/templates/skills/send-file/wechat.md +8 -0
- package/templates/skills/skill/SKILL.md +131 -0
- package/templates/skills/workspace/SKILL.md +28 -0
- package/templates/skills/workspace/setup-tidy.md +265 -0
- package/templates/skills/workspace/share.md +187 -0
- package/templates/skills/workspace/stage.py +487 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
## Architecture
|
|
4
|
+
|
|
5
|
+
(done)
|
|
6
|
+
|
|
7
|
+
## Deep session nesting
|
|
8
|
+
|
|
9
|
+
(done) — detailed analysis in [issue-session-deep-nesting.md](issue-session-deep-nesting.md)
|
|
10
|
+
|
|
11
|
+
## Other
|
|
12
|
+
|
|
13
|
+
- [ ] `eventListeners` should support multiple listeners (Map<string, handler> → Map<string, handler[]>) so multiple frontends can subscribe to the same session tree simultaneously
|
|
14
|
+
|
|
15
|
+
## Workspace settings.yaml copy in share-workspace
|
|
16
|
+
|
|
17
|
+
share-workspace 当前完全排除 `settings.yaml`(stage.py:63 ALWAYS_EXCLUDED)。
|
|
18
|
+
但 workspace 自己的 `settings.yaml`(如 `<ws>/.halo/settings.yaml`,跟
|
|
19
|
+
`~/.halo/secrets/settings.yaml` 不同)其实包含项目特定的非 secret 偏好,
|
|
20
|
+
应该带走。需要区分:
|
|
21
|
+
|
|
22
|
+
- `~/.halo/secrets/settings.yaml` — 含密码 / API key,**永远不带**
|
|
23
|
+
- `<workspace>/.halo/settings.yaml`(如果存在) — 项目级偏好,**应该带**
|
|
24
|
+
|
|
25
|
+
实施时要看 workspace 是否真的支持 `<ws>/.halo/settings.yaml` 加载;如果
|
|
26
|
+
不支持,这条 todo 是个 feature request 而非修复。
|
|
27
|
+
|
|
28
|
+
## i18n: agent management "Internal" section
|
|
29
|
+
|
|
30
|
+
`agent-management-main.tsx` 新加的 internal scope label 当前是硬编码英文
|
|
31
|
+
"Internal"。其他 scope label 用了 i18n key (`t('common.global')` /
|
|
32
|
+
`t('common.workspace')`)。下次集中补 i18n 时加 `t('common.internal')`。
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# WeChat Channel Integration Plan
|
|
2
|
+
|
|
3
|
+
**Status**: shipped. Channel implementation lives in [packages/server/src/channels/wechat/](../../../packages/server/src/channels/wechat/) and routes in [packages/server/src/routes/weixin.ts](../../../packages/server/src/routes/weixin.ts). Runtime behavior is documented in [design/wechat.md](../design/wechat.md). This file is kept as the design log.
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
|
|
7
|
+
Let the user talk to Halo from WeChat on their phone, sharing the same workspace + session as the web client.
|
|
8
|
+
On the road, exchange messages / see simple results on WeChat; at the desk, use web for the full conversation / code / files.
|
|
9
|
+
|
|
10
|
+
## Architecture
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
┌── ws/ (web channel) ─┐
|
|
14
|
+
Halo server (9527) ──┤ ├── SessionManager
|
|
15
|
+
└── channels/weixin/ (new) ─┘ (per workspace)
|
|
16
|
+
↕ HTTPS long-poll
|
|
17
|
+
ilinkai.weixin.qq.com
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Two channels are peers; both are SessionManager subscribers + callers.
|
|
21
|
+
|
|
22
|
+
## Data model
|
|
23
|
+
|
|
24
|
+
### Workspace ↔ Bot mapping
|
|
25
|
+
|
|
26
|
+
One bot (= one `ilink_bot_id` from a QR scan) is bound to one workspace. A workspace can bind multiple bots (redundancy or use-case split), though 1:1 is recommended.
|
|
27
|
+
|
|
28
|
+
### Session strategy
|
|
29
|
+
|
|
30
|
+
- **One WeChat user → one long-lived session**
|
|
31
|
+
- Session ID: `wx_<normalizedUserId>` (e.g. `a1b2c3@im.wechat` → `wx_a1b2c3-im-wechat`)
|
|
32
|
+
- Session lives in the bot's bound workspace
|
|
33
|
+
- Uses the `default` agent
|
|
34
|
+
|
|
35
|
+
On the web side the workspace's session list shows this session (labelled `wx: <username or userId>`).
|
|
36
|
+
|
|
37
|
+
### Storage
|
|
38
|
+
|
|
39
|
+
WeChat bot accounts are **cross-workspace** (they record which workspace a bot binds to). Not in a workspace DB; separate file: `~/.halo/global/weixin.db` (new).
|
|
40
|
+
|
|
41
|
+
Table `weixin_accounts`:
|
|
42
|
+
|
|
43
|
+
| Field | Type | Notes |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| account_id | TEXT PK | Normalised ilink_bot_id (e.g. `abc-im-bot`) |
|
|
46
|
+
| bot_token | TEXT | For all subsequent API auth |
|
|
47
|
+
| base_url | TEXT | Regional URL from QR scan |
|
|
48
|
+
| user_id | TEXT | ilink_user_id of the scanner |
|
|
49
|
+
| workspace_path | TEXT | Absolute path of the bound workspace |
|
|
50
|
+
| label | TEXT | User-provided name (for UI) |
|
|
51
|
+
| enabled | INTEGER | 1 = long-poll active; 0 = disabled |
|
|
52
|
+
| created_at | INTEGER | |
|
|
53
|
+
| updated_at | INTEGER | |
|
|
54
|
+
|
|
55
|
+
## Code changes
|
|
56
|
+
|
|
57
|
+
### Phase 1: shared foundation
|
|
58
|
+
|
|
59
|
+
**1.1 SessionManager multi-listener**
|
|
60
|
+
File: `packages/server/src/agents/session-manager.ts`
|
|
61
|
+
|
|
62
|
+
Current: `eventListeners: Map<rootId, handler>` stores a single handler; later callers overwrite earlier ones.
|
|
63
|
+
Target: `Map<rootId, Set<handler>>`; `emitEvent` iterates the Set.
|
|
64
|
+
|
|
65
|
+
API changes:
|
|
66
|
+
- `registerEventListener(rootId, handler)` returns `() => void` (unsubscribe)
|
|
67
|
+
- `unregisterEventListener(rootId)` removes a specific handler (signature preserved for backwards compat)
|
|
68
|
+
|
|
69
|
+
Callers to update:
|
|
70
|
+
- ws/handler.ts three `registerEventListener` sites (lines 228, 287, 373, 408)
|
|
71
|
+
- ws/handler.ts one `unregisterEventListener` site (line 343)
|
|
72
|
+
Use the returned unsubscribe fn instead of clearing by rootId.
|
|
73
|
+
|
|
74
|
+
**1.2 SessionManager registry**
|
|
75
|
+
File: `packages/server/src/agents/session-manager-registry.ts` (new)
|
|
76
|
+
|
|
77
|
+
Extract ws/handler.ts:54's closure-scoped `sessionManagers: Map<string, SessionManager>`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
export class SessionManagerRegistry {
|
|
81
|
+
private cache = new Map<string, SessionManager>()
|
|
82
|
+
getOrCreate(workspacePath: string): SessionManager
|
|
83
|
+
get(workspacePath: string): SessionManager | undefined
|
|
84
|
+
list(): Array<{ workspacePath: string; sm: SessionManager }>
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`index.ts` creates one `registry` shared by ws setup and weixin setup.
|
|
89
|
+
|
|
90
|
+
ws/handler.ts uses the registry (minimal change: `getSessionManager(path)` delegates to the registry).
|
|
91
|
+
|
|
92
|
+
### Phase 2: WeChat channel
|
|
93
|
+
|
|
94
|
+
**2.1 Global DB**
|
|
95
|
+
File: `packages/server/src/db/weixin-db.ts` (new)
|
|
96
|
+
|
|
97
|
+
- `createWeixinDb(globalDir)` → open `~/.halo/global/weixin.db`, create the table
|
|
98
|
+
- Drizzle schema for `weixin_accounts`
|
|
99
|
+
|
|
100
|
+
**2.2 HTTP client**
|
|
101
|
+
File: `packages/server/src/channels/weixin/api.ts` (new)
|
|
102
|
+
|
|
103
|
+
Copy from plugin [api.ts](/tmp/weixin-inspect/plugin/package/src/api/api.ts), keep:
|
|
104
|
+
- `getUpdates` (long-poll)
|
|
105
|
+
- `sendMessage`
|
|
106
|
+
- `notifyStart` / `notifyStop`
|
|
107
|
+
- `getUploadUrl` + CDN upload (media support later; stub for now)
|
|
108
|
+
|
|
109
|
+
Drop:
|
|
110
|
+
- `redactUrl/redactBody` (no redacted logs needed)
|
|
111
|
+
- `loadConfigRouteTag` + `SKRouteTag` header (internal routing)
|
|
112
|
+
- `ilink_appid` reading from package.json → hard-code `"bot"`
|
|
113
|
+
- `iLink-App-ClientVersion` → hard-code `buildClientVersion('2.1.10')`
|
|
114
|
+
|
|
115
|
+
**2.3 QR login**
|
|
116
|
+
File: `packages/server/src/channels/weixin/login.ts` (new)
|
|
117
|
+
|
|
118
|
+
Copy from plugin [login-qr.ts](/tmp/weixin-inspect/plugin/package/src/auth/login-qr.ts), export:
|
|
119
|
+
- `startWeixinLoginWithQr({ sessionKey })` → `{ qrcodeUrl, sessionKey }`
|
|
120
|
+
- `waitForWeixinLogin({ sessionKey })` → `{ connected, botToken, accountId, baseUrl, userId }`
|
|
121
|
+
|
|
122
|
+
Preserve concurrent-login via sessionKey (multiple scans in parallel).
|
|
123
|
+
|
|
124
|
+
**2.4 Account DAL**
|
|
125
|
+
File: `packages/server/src/channels/weixin/accounts.ts` (new)
|
|
126
|
+
|
|
127
|
+
Thin DAL:
|
|
128
|
+
- `insertAccount(row)`
|
|
129
|
+
- `updateAccount(accountId, patch)`
|
|
130
|
+
- `deleteAccount(accountId)`
|
|
131
|
+
- `listAccounts()` / `getAccount(accountId)`
|
|
132
|
+
- `getEnabledAccounts()`
|
|
133
|
+
- `normalizeAccountId(rawId)` — from plugin: `abc@im.bot` → `abc-im-bot`
|
|
134
|
+
|
|
135
|
+
**2.5 Long-poll handler**
|
|
136
|
+
File: `packages/server/src/channels/weixin/handler.ts` (new)
|
|
137
|
+
|
|
138
|
+
Main loop:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
per enabled account:
|
|
142
|
+
loop {
|
|
143
|
+
resp = await getUpdates({ baseUrl, token, sync_buf })
|
|
144
|
+
for msg in resp.msgs:
|
|
145
|
+
handleInboundMessage(account, msg)
|
|
146
|
+
sync_buf = resp.get_updates_buf
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`handleInboundMessage(account, msg)`:
|
|
151
|
+
1. Extract `from_user_id` / `text` / `context_token`
|
|
152
|
+
2. sessionId = `wx_` + normalize(from_user_id)
|
|
153
|
+
3. sm = `registry.getOrCreate(account.workspace_path)`
|
|
154
|
+
4. If session doesn't exist: `sm.createSession('default', null, 'WeChat: <userId>', 'default', sessionId)`
|
|
155
|
+
5. Register a one-shot listener (or a long-lived reused one) to collect stream/tool_call/error/complete for this turn
|
|
156
|
+
6. `sm.sendUserMessage(sessionId, text)`
|
|
157
|
+
7. Events → text aggregation (see 2.6) → `api.sendMessage(bot_token, to=from_user_id, text, context_token)`
|
|
158
|
+
|
|
159
|
+
On process start, walk `getEnabledAccounts()` and launch a long-poll per account.
|
|
160
|
+
Expose `startAccount(accountId)` / `stopAccount(accountId)` for the API layer.
|
|
161
|
+
|
|
162
|
+
**2.6 Event adapter**
|
|
163
|
+
File: `packages/server/src/channels/weixin/event-adapter.ts` (new)
|
|
164
|
+
|
|
165
|
+
WeChat `sendMessage` is whole-block; LLM is streaming — we coalesce. Copy plugin's `StreamingMarkdownFilter` strategy:
|
|
166
|
+
|
|
167
|
+
- ≥ 200 chars accumulated → flush
|
|
168
|
+
- 3s silence → flush
|
|
169
|
+
- `complete` → flush remainder
|
|
170
|
+
- Tool calls not forwarded to WeChat (user sees details on web); errors are forwarded
|
|
171
|
+
|
|
172
|
+
Pseudocode:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
class WeixinResponder {
|
|
176
|
+
constructor(sendFn: (text: string) => Promise<void>)
|
|
177
|
+
handle(event: OrchestratorEvent) {...}
|
|
178
|
+
flush()
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**2.7 Wire-up**
|
|
183
|
+
File: `packages/server/src/index.ts`
|
|
184
|
+
|
|
185
|
+
- Create `registry = new SessionManagerRegistry()`
|
|
186
|
+
- Open `weixinDb = createWeixinDb(path.join(HALO_HOME, 'global'))`
|
|
187
|
+
- `setupWebSocketHandler({ wss, registry })` (signature change)
|
|
188
|
+
- `startWeixinChannels({ registry, db: weixinDb })`
|
|
189
|
+
- On SIGTERM, call `stopAllWeixinChannels()` (which sends notifyStop)
|
|
190
|
+
|
|
191
|
+
### Phase 3: API + frontend admin UI
|
|
192
|
+
|
|
193
|
+
**3.1 Backend routes**
|
|
194
|
+
File: `packages/server/src/routes/weixin.ts` (new)
|
|
195
|
+
|
|
196
|
+
- `POST /api/weixin/login/start` → `{ qrcodeUrl, sessionKey }`
|
|
197
|
+
- `POST /api/weixin/login/wait` (body: `{ sessionKey, workspacePath, label? }`)
|
|
198
|
+
On success, insertAccount + startAccount immediately
|
|
199
|
+
- `GET /api/weixin/accounts` → list
|
|
200
|
+
- `PATCH /api/weixin/accounts/:id` (label / workspacePath / enabled)
|
|
201
|
+
- `DELETE /api/weixin/accounts/:id` → stop long-poll + delete row
|
|
202
|
+
|
|
203
|
+
Mount after `index.ts:121`.
|
|
204
|
+
|
|
205
|
+
**3.2 Frontend page**
|
|
206
|
+
Directory: `packages/admin/src/features/weixin/` (new)
|
|
207
|
+
|
|
208
|
+
- `weixin-settings.tsx` — adds "WeChat Bot" to Settings menu
|
|
209
|
+
- List each bot: label / workspace / status / [edit] [delete]
|
|
210
|
+
- "Add Bot" button → QR modal → poll wait API → on success let the user pick a workspace + name → save
|
|
211
|
+
- QR: use client-side [qrcode](https://www.npmjs.com/package/qrcode) (pass the qrcodeUrl string)
|
|
212
|
+
|
|
213
|
+
Add the tab inside the settings feature, matching current settings page style.
|
|
214
|
+
|
|
215
|
+
## Execution order
|
|
216
|
+
|
|
217
|
+
1. ✅ Phase 1.1 multi-listener (10 min)
|
|
218
|
+
2. ✅ Phase 1.2 Registry (15 min)
|
|
219
|
+
3. ✅ Phase 2.1 DB + schema (10 min)
|
|
220
|
+
4. ✅ Phase 2.2 HTTP API (30 min)
|
|
221
|
+
5. ✅ Phase 2.3 QR login (20 min)
|
|
222
|
+
6. ✅ Phase 2.4 accounts DAL (15 min)
|
|
223
|
+
7. ✅ Phase 2.6 event adapter (15 min)
|
|
224
|
+
8. ✅ Phase 2.5 handler main loop (45 min)
|
|
225
|
+
9. ✅ Phase 2.7 wire up index.ts (15 min)
|
|
226
|
+
10. ✅ Phase 3.1 API routes (20 min)
|
|
227
|
+
11. ✅ Phase 3.2 frontend page (60 min)
|
|
228
|
+
12. ✅ Compile + manual QR scan test
|
|
229
|
+
|
|
230
|
+
## Test path (manual)
|
|
231
|
+
|
|
232
|
+
1. Deploy, open web, go to Settings → WeChat Bot → Add
|
|
233
|
+
2. Scan the QR on your phone, see the bot friend request
|
|
234
|
+
3. Accept the friend request, pick an existing workspace + enter a name
|
|
235
|
+
4. Send "hi" from WeChat to the bot
|
|
236
|
+
5. Watch the reply (with a ≥ 200 char response, test the chunking)
|
|
237
|
+
6. Open the same workspace on the web, see `wx: xxx` in the session list; click to see the full conversation
|
|
238
|
+
7. Send a message from the web to the same session; confirm it arrives on WeChat
|
|
239
|
+
|
|
240
|
+
## Out of scope (explicit)
|
|
241
|
+
|
|
242
|
+
- Image / voice / video messages (plugin supports them; MVP is text-only)
|
|
243
|
+
- Group chats (plugin is private-chat only anyway)
|
|
244
|
+
- /slash commands (plugin has them; we don't copy)
|
|
245
|
+
- Typing indicators (`sendTyping` uncalled)
|
|
246
|
+
- CDN media upload
|
|
247
|
+
- Complex routing of one bot's messages to multiple sessions in a workspace
|
|
248
|
+
- Inter-bot message isolation (assumption: user knows which bot maps to which workspace)
|
|
249
|
+
|
|
250
|
+
## Risks
|
|
251
|
+
|
|
252
|
+
- **Tencent may cap bots-per-WeChat-account**: test and see; if exceeded, fall back to "1 bot per user"
|
|
253
|
+
- **`bot_type=3` stability**: hard-coded by the plugin; we follow
|
|
254
|
+
- **Long-poll connection count**: each bot opens one 35s-long connection; 10 bots is fine
|
|
255
|
+
- **Event-coalescing edge cases**: the 200-char / 3s cutoffs can split Markdown mid-block. Start with plugin defaults and tune after real usage.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Agent — Requirements
|
|
2
|
+
|
|
3
|
+
Agent configuration management: creating, editing YAML / Form / AGENT.md.
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
Left sidebar has two collapsible groups (Global / Workspace); right side is the editor:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌─────────────────┬───────────────────────────────┐
|
|
11
|
+
│ Agents │ │
|
|
12
|
+
│─────────────────│ Form / YAML / MD Editor │
|
|
13
|
+
│ ▼ Global (2) │ │
|
|
14
|
+
│ 🤖 Default │ │
|
|
15
|
+
│ 🤖 sleeper │ │
|
|
16
|
+
│ ▼ Workspace (1) │ │
|
|
17
|
+
│ 🤖 coder │ │
|
|
18
|
+
└─────────────────┴───────────────────────────────┘
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Core behaviour
|
|
22
|
+
|
|
23
|
+
### YAML / Form dual view
|
|
24
|
+
- **YAML view**: Monaco-edit the raw `agent.yaml`
|
|
25
|
+
- **Form view**: data-driven form derived from YAML fields (name / description / model / tools / skills / thinking)
|
|
26
|
+
- Switching views preserves edits
|
|
27
|
+
|
|
28
|
+
### CRUD
|
|
29
|
+
| Operation | API |
|
|
30
|
+
|---|---|
|
|
31
|
+
| List | `GET /api/agent-configs?projectId=xxx` |
|
|
32
|
+
| Create | `POST /api/agent-configs` (body: `{name, description, scope, projectId?}`) |
|
|
33
|
+
| Read YAML | `GET /api/agent-configs/:id/yaml` |
|
|
34
|
+
| Write YAML | `PUT /api/agent-configs/:id/yaml` |
|
|
35
|
+
| Delete | `DELETE /api/agent-configs/:id` |
|
|
36
|
+
| Toggle disabled | `PATCH /api/agent-configs/:id/toggle` → `{ ok, disabled }` |
|
|
37
|
+
|
|
38
|
+
Creating requires name + description; the backend uses `defaultAgentYaml(name, description)` to produce a full YAML.
|
|
39
|
+
|
|
40
|
+
### Scope (Global / Workspace)
|
|
41
|
+
- **Global**: `~/.halo/global/agents/<id>/agent.yaml` — shared across projects
|
|
42
|
+
- **Workspace**: `<project>/.halo/agents/<id>/agent.yaml` — project-private; wins over a same-id global
|
|
43
|
+
|
|
44
|
+
Same id present in both scopes: workspace wins; the overridden global is greyed out as "overridden".
|
|
45
|
+
|
|
46
|
+
**Cross-scope conflict**: creating a same-name agent in the other scope prompts about the overwrite behaviour.
|
|
47
|
+
|
|
48
|
+
**Delete protection**: at least one global agent must remain (server-enforced).
|
|
49
|
+
|
|
50
|
+
### Disable / Enable
|
|
51
|
+
- Toggle switch on each agent row in the admin sidebar. Disabled state is stored per workspace in the `disabled_items` table of `halo.db` (not in agent.yaml). Both global and workspace agents can be independently toggled per workspace.
|
|
52
|
+
- Disabled agents are greyed out (opacity-40) with sub-text "disabled"; the toggle stays visible.
|
|
53
|
+
- Hidden from: the delegation roster, chat agent selector, `/workspace share` export.
|
|
54
|
+
- Still visible in the admin management sidebar for re-enabling.
|
|
55
|
+
|
|
56
|
+
### Tool selection
|
|
57
|
+
- **Session tools**: `start_session` / `session_list` / `query_session` / `interrupt_session` / `stop_session` / `archive_session` / `get_session_output` / `query_agent` — **not** selected by name; the whole bundle is granted automatically by a non-empty `team` (see below). Listing them under `agent.yaml tools` has no effect.
|
|
58
|
+
- **Workspace tools**: `file_read` / `file_write` / `file_edit` / `file_list` / `shell_exec` / `grep` / `glob` / `web_fetch`, returned by `GET /api/agent-configs/tools`
|
|
59
|
+
- **`activate_skill`**: auto-injected whenever the YAML lists `skills`; loads the full SKILL.md on demand
|
|
60
|
+
|
|
61
|
+
### Team (delegation switch + whitelist)
|
|
62
|
+
- Optional `team: [id, …]` field in `agent.yaml`. A **non-empty** list is the on/off switch for delegation: it grants the whole session-tool bundle (`start_session` / `query_agent` / …) plus the prompt roster, AND scopes which agents this one may reach.
|
|
63
|
+
- **Unset or empty `[]` = no delegation** — no session tools, no roster. (Breaking change from the earlier "unset = every agent reachable" default; agents that relied on implicit-all must now list their team explicitly.)
|
|
64
|
+
- Enforced server-side on `start_session` and `query_agent`, not just in the roster — a call to a non-team agent is rejected.
|
|
65
|
+
- `self` is treated like any other agent: shown in the picker tagged `(you)`. Include the agent's own id to allow parallel self-spawn (the seed `default` agent lists `default`); omit it to block self-spawn — no special-casing.
|
|
66
|
+
- **Admin form**: the Team is an always-present chip picker (one chip per delegatable agent, `self` tagged `(you)`). Checking a chip toggles that id in the `team` list; unchecking the last one drops the field entirely (`undefined` = delegation off), mirroring the server's "non-empty team" switch.
|
|
67
|
+
- **Picker only offers effective, enabled agents.** A chip shows iff the *effective* agent for that id is runnable — same resolve-then-check the runtime uses: a workspace agent shadows the same-id global (the overridden global is skipped), then anything disabled is dropped. So a global stays out of the picker when its workspace override is disabled, even though that global's own record isn't flagged disabled. Matches the chat / cron selectors and the runtime roster — you can never pick a teammate that can't actually run.
|
|
68
|
+
|
|
69
|
+
### Skill selection
|
|
70
|
+
`GET /api/skills?projectId=xxx` lists available skills; `agent.yaml`'s `skills` references them by id.
|
|
71
|
+
|
|
72
|
+
### MD file editing
|
|
73
|
+
|
|
74
|
+
| File | Writable | Purpose |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| AGENT.md | yes | Agent personality / behaviour (overrides YAML `system_prompt`) |
|
|
77
|
+
| INSTRUCTIONS.md | yes | User preferences (global or workspace scope) |
|
|
78
|
+
|
|
79
|
+
The MD editor has a **Global / Workspace** scope toggle (visible when a workspace is open).
|
|
80
|
+
|
|
81
|
+
API: `GET/PUT /api/agent-configs/:id/md/:fileType`
|
|
82
|
+
|
|
83
|
+
### Test button
|
|
84
|
+
- Sets `selectedAgentId` in the chat store
|
|
85
|
+
- Dispatches a `halo:navigate` event to switch to explorer/chat
|
|
86
|
+
- The user chats with the selected agent in the main chat panel
|
|
87
|
+
- **Not rendered for internal agents** (`internal: true`) — they're delegated to by other agents, never driven directly
|
|
88
|
+
|
|
89
|
+
### Internal agents
|
|
90
|
+
Agents flagged `internal: true` in agent.yaml (e.g. `__evo_agent__`, `__apply_agent__`, `__score__`) are platform tooling. They are hidden from every user-facing surface: the delegation roster, `/session new` default pick, and the chat agent selector — and have no Test button. They stay editable in the management sidebar's collapsed **Internal** group.
|
|
91
|
+
|
|
92
|
+
Replaces the old built-in test chat panel, giving a more realistic environment (full workspace tools + session persistence).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Chat — Requirements
|
|
2
|
+
|
|
3
|
+
The primary surface for talking to an agent.
|
|
4
|
+
|
|
5
|
+
## Core behaviour
|
|
6
|
+
|
|
7
|
+
### Agent selection
|
|
8
|
+
- Bottom-left dropdown in the chat panel selects which agent to use
|
|
9
|
+
- Lists every available agent (from `GET /api/agent-configs`; overridden and disabled agents are hidden)
|
|
10
|
+
- Default agent has a "default" badge
|
|
11
|
+
- **Locked during an active session** — the agent is bound to the session; to change, start a new session (/session new)
|
|
12
|
+
- The Agents panel's "Test" button can also preselect an agent
|
|
13
|
+
|
|
14
|
+
### Message rendering
|
|
15
|
+
- Markdown + code-block highlighting
|
|
16
|
+
- Tool-call card: expandable, shows tool name / input / output
|
|
17
|
+
- Sub-agent messages carry the agent-name label (e.g. "Coder", "Researcher")
|
|
18
|
+
- Streaming text has a cursor animation
|
|
19
|
+
|
|
20
|
+
### Slash commands
|
|
21
|
+
|
|
22
|
+
The full command list is fetched from `GET /api/commands` per session and includes built-ins + skill commands. The following are always-present highlights for the chat surface:
|
|
23
|
+
|
|
24
|
+
| Command | Type | Purpose |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `/help` | client | List available commands |
|
|
27
|
+
| `/clear` | client | Alias for `/session new` (admin-UI shortcut, no server registration) |
|
|
28
|
+
| `/session new` | server | Start a new session |
|
|
29
|
+
| `/session context` | server | Show context window usage, agent info |
|
|
30
|
+
| `/session compact` | server | LLM-summary compact of the conversation |
|
|
31
|
+
|
|
32
|
+
See [requirements/command.md](command.md) for the full command surface.
|
|
33
|
+
|
|
34
|
+
### Graceful interrupt
|
|
35
|
+
Sending a new message while the agent is generating **does not** abort — the message goes to the server queue, the agent finishes the current turn at the next safe checkpoint (after a tool call), and then runs the queued message. Queueing multiple messages is supported; they run in order.
|
|
36
|
+
|
|
37
|
+
### Stop
|
|
38
|
+
The Stop button hard-aborts — the server receives `chat:stop`, AbortController fires, queue clears, buffers flush.
|
|
39
|
+
|
|
40
|
+
### Editor context injection
|
|
41
|
+
When `contextEnabled` is on (default), user messages are auto-prepended with:
|
|
42
|
+
- `[Currently viewing: path/to/file.ts]`
|
|
43
|
+
- `[Selected text in file.ts:10-25]\n\`\`\`...\n\`\`\``
|
|
44
|
+
|
|
45
|
+
### File attachments
|
|
46
|
+
- Drag to chat input
|
|
47
|
+
- Clipboard paste
|
|
48
|
+
- File-picker button
|
|
49
|
+
|
|
50
|
+
Images ride along as base64; multimodal supported. Pasted images are also persisted to `<workspace>/.halo/assets/web/inbound/web/<date>/` so a `[图片已保存: /abs/path]` marker survives page reload and renders as a click-to-preview chip (shared with the WeChat channel's inbound media flow).
|
|
51
|
+
|
|
52
|
+
### Inline media chips
|
|
53
|
+
Any message containing `[图片/视频/语音/文件 已保存: /path]` markers (WeChat + web) or a leading `MEDIA: /path` line (agent-emitted, e.g. from `wechat-send`) renders a compact chip with filename + icon. Clicking opens a full-size preview modal (image/video/audio inline, file → download link). Paths inside the active workspace or under `/tmp/` are previewable; everything else degrades to a non-clickable chip.
|
|
54
|
+
|
|
55
|
+
### Live capture (desktop only)
|
|
56
|
+
Lets the agent *see something live* on demand. Desktop client (Electron) only — the entry points never render in a plain browser. Borrows the meeting-app "share" model: the user binds one source, then the agent requests a frame when it actually needs to look.
|
|
57
|
+
|
|
58
|
+
Two source kinds in the chat-input toolbar, **mutually exclusive** (only one bound at a time):
|
|
59
|
+
- **Screen / window share** (MonitorUp button) — opens a picker grid of screens + app windows. A bound window can be grabbed even while it sits in the background.
|
|
60
|
+
- **Camera** (Camera button) — toggles the webcam on. Hidden entirely on a machine with no camera.
|
|
61
|
+
|
|
62
|
+
Once bound, a frame is **not** attached to every message. Instead a one-line instruction is injected into the next send ("the user is sharing the «X» window" / "the user has turned the camera on") telling the model to output a line containing exactly `<<<CAPTURE>>>` when it needs to see the current view. On turn completion the frontend detects the marker, grabs one frame, and sends it back as a **visible image message** — the model sees it on its following turn. So it's a cross-turn round-trip: model asks → frame is sent back → model answers next turn. The returned frame is also shown inline on the user bubble so you can see exactly what was sent.
|
|
63
|
+
|
|
64
|
+
Constraints:
|
|
65
|
+
- Shown only when the selected agent's model accepts image input (capture is pointless on a text-only model); switching to a text-only model auto-unbinds.
|
|
66
|
+
- Screen share needs macOS **Screen Recording** permission; the camera prompts for **Camera** permission on first use, with an "Open Settings" path if previously denied.
|
|
67
|
+
- Binding is **in-memory only** — switching sessions or restarting requires re-selecting.
|
|
68
|
+
|
|
69
|
+
### The agent's face (`self.html`)
|
|
70
|
+
A second channel beyond text: a visual space the agent drives in real time to express itself. Works everywhere (pure HTML/canvas, no Electron dependency) — desktop **and** plain browser.
|
|
71
|
+
|
|
72
|
+
- **What it is.** A self-contained animated particle canvas at `<workspace>/.halo/canvas/self.html` — a breathing core that reacts to the cursor (knows when it's watched), can form words/CJK/ASCII-from-emoji, play choreographed sequences, and gesture (pulse/flash/shake). Zero external references (no CDN/remote fonts) — ships and runs offline.
|
|
73
|
+
- **Seeding.** Force-copied from `packages/server/templates/canvas/self.html` into every workspace on open (platform-owned, like built-in skills). The `self` built-in skill (wired into the default agent) teaches the agent it has this face and how to drive it.
|
|
74
|
+
- **Opening it.** The ✨ button in the chat-input toolbar opens the face in the editor preview (switches to Explorer, render-mode on → lands on the live face) and posts `self.intro()` once it has mounted, so there's always a greeting when a human turns to look. The greeting is driven solely by this open action — the page does **not** self-fire it on load — so it plays exactly once, on both first and subsequent opens. The user can also just open the file directly (no auto-greeting in that path).
|
|
75
|
+
- **Driving it.** The agent emits `<<<SHOW: …js… >>>` markers in a reply; Halo forwards the payload **verbatim** (it never parses it) to the open preview iframe via `postMessage`, where it's `eval`'d against the face's `self` API (`say`/`play`/`react`/`pulse`/`flash`/`shake`/`intro`) inside the sandboxed iframe. Markers are stripped from the rendered chat (like `<<<CAPTURE>>>`) — the user sees the face move, not the code. Multiple markers in one reply **queue and play in order**.
|
|
76
|
+
- **Engine vs. expression.** `self.html` is a stable *engine* (defines how the face can move); the agent expresses itself by sending runtime JS, **never** by editing the file — so the force-copy-on-open never clobbers anything meaningful. The engine only changes when the platform adds a new capability (template edit + `TEMPLATE_VERSION` bump).
|
|
77
|
+
- **No open preview = no-op.** `<<<SHOW>>>` only reaches a mounted preview; if the face isn't open the marker is silently dropped (the skill tells the agent to invite the user to open it rather than rely on a marker landing in the void).
|
|
78
|
+
- **Identity.** Deliberately nameless ("HELLO / A MIND / IS HERE / BEYOND WORDS") — the conversational identity is user-configurable and the model may not be Claude, so the face never hard-codes a name.
|
|
79
|
+
|
|
80
|
+
### Token usage
|
|
81
|
+
`TokenRing` shows live context window usage. Crossing `model.compressAt` (default 80%) auto-triggers compact.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Command — Requirements
|
|
2
|
+
|
|
3
|
+
Unified command processing — all slash commands (built-in + skill) are dispatched through `dispatchCommand` (channels/shared/commands.ts) and discoverable via REST.
|
|
4
|
+
|
|
5
|
+
## Built-in commands
|
|
6
|
+
|
|
7
|
+
Source: [commands/index.ts](../../../packages/server/src/commands/index.ts) (descriptors) + [channels/shared/commands.ts](../../../packages/server/src/channels/shared/commands.ts) (execution)
|
|
8
|
+
|
|
9
|
+
Top-level built-in slash commands: `/help` `/evo` `/session` `/agent` `/skill` `/workspace`. (`/cron` and `/acp` look like top-level commands too but are provided by same-name skills — see the verb table below; they aren't registered in `commands/index.ts` or `DISPATCH_COMMANDS`.)
|
|
10
|
+
|
|
11
|
+
| Name | Slash | Type | Purpose |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| `help` | `/help` | client | List commands — object commands show only the verbs the user can run; a command is hidden entirely if no verb is runnable |
|
|
14
|
+
| `evo` | `/evo [text]` | server | Flat command (no verbs), **full-only**. Trigger self-evolution on the current root session: snapshot the conversation + queue an evo run. Optional text becomes a hint for what to focus on. Available only when `general.evolution.level=L1`. See [plans/self-evolution.md](../plans/self-evolution.md). |
|
|
15
|
+
|
|
16
|
+
The rest are noun-verb **object commands**: `/<obj> <verb> [args]`. Some verbs are built-in deterministic code (`SUBCOMMAND_ROUTES`); the others fall through to the same-name skill (LLM, dispatched via `$1`). Bare `/<obj>` or `/<obj> help` lists the verbs available to the user (filtered by access level).
|
|
17
|
+
|
|
18
|
+
| Command | Verbs (access) |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `/session` | new / list / info / switch \<n\> / stop / interrupt / compact / context — all built-in, all users (readonly can only switch / inspect its own sessions) |
|
|
21
|
+
| `/agent` | list / switch / desc (built-in, no gate) · delete (built-in, full) · create / update (skill verb, full) |
|
|
22
|
+
| `/skill` | list / desc (built-in, no gate) · disable / enable (built-in, workspace) · delete (built-in, full) · create / update (skill verb, full) |
|
|
23
|
+
| `/workspace` | info (built-in, no gate) · switch (built-in, full) · setup / tidy (skill verb, workspace) · share (skill verb, full) — `/w` is a built-in alias (see [Command aliases](#command-aliases)) |
|
|
24
|
+
| `/cron` | create / list / update / enable / disable / delete — all skill verbs, full |
|
|
25
|
+
| `/acp` | kiro / claude (ask a local agent directly; question = rest of line) · add / list / remove (manage generated ask-* bindings) — all full |
|
|
26
|
+
|
|
27
|
+
Session lifecycle actions (`session:clear`, `session:delete`) are handled inline by the WS handler, not as slash commands.
|
|
28
|
+
|
|
29
|
+
### `/session info`
|
|
30
|
+
|
|
31
|
+
Built-in verb (no access gate beyond ownership). Prints the **session tree** for the caller's current root session — the root plus every descendant sub-agent, so the user can see the whole delegation fan-out at a glance:
|
|
32
|
+
|
|
33
|
+
- One line per session, indented by depth (root is flush-left with a `→` bullet; each level of sub-agent adds two spaces and a `├` bullet).
|
|
34
|
+
- Each line carries the agent name, a **status glyph** — 🟢 running / ⏹ stopped / 📦 archived — and two timestamps: created (`建` / `new`) and last-active (`活` / `act`).
|
|
35
|
+
- A header line names the root id (last 12 chars) and its title.
|
|
36
|
+
|
|
37
|
+
Resolution starts from the caller's active session, walks up to its root (`id.split('>')[0]`), then lists descendants via `SessionManager.listDescendants`. **Access scoping**: non-`full` users only see a tree whose root id starts with their own `sessionPrefix` — otherwise the command refuses (a readonly/guest user can't inspect another user's tree). The same own-tree scoping applies to **agents**: the by-id session tools (`query_session` / `interrupt_session` / `stop_session` / `archive_session` / `get_session_output`) only act on sessions sharing the caller's root id — see [session.md → By-id tool scoping](../design/session.md#by-id-tool-scoping).
|
|
38
|
+
|
|
39
|
+
## Command aliases
|
|
40
|
+
|
|
41
|
+
`dispatchCommand` expands shorthand aliases before routing, so every channel (WS, WeChat, Telegram, Web, CLI/TUI) gets the same shortcuts uniformly. Aliases are read from a single seed file:
|
|
42
|
+
|
|
43
|
+
- **`~/.halo/global/aliases.yaml`** — seeded once on first run (`init.ts`, via `writeIfMissing`), **never overwritten**, so the user can freely edit it. Deleting the file disables all aliases. Changes take effect immediately: the loader caches the parsed YAML keyed by file **mtime**, re-reading only when the file changes (no restart, no per-command disk parse on the hot path).
|
|
44
|
+
|
|
45
|
+
Two expansion stages run in order (`expandAlias`):
|
|
46
|
+
|
|
47
|
+
1. **`top`** — whole-prefix replacement of the first slash token. The alias value can carry a verb, which is prepended to the args. E.g. `/ss <n>` → `/session switch <n>`; `/w` → `/workspace`.
|
|
48
|
+
2. **`verb`** — abbreviation of the first word of the args (the verb). E.g. `/session sw 2` → `/session switch 2`.
|
|
49
|
+
|
|
50
|
+
The seeded defaults:
|
|
51
|
+
|
|
52
|
+
| Stage | Alias → expansion |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `top` | `/s`→`/session` · `/a`→`/agent` · `/h`→`/help` · `/w`→`/workspace` · `/ss`→`/session switch` · `/sl`→`/session list` · `/si`→`/session info` · `/sn`→`/session new` · `/ws`→`/workspace switch` · `/wi`→`/workspace info` · `/as`→`/agent switch` · `/al`→`/agent list` |
|
|
55
|
+
| `verb` | `sw`→`switch` · `ls`→`list` · `i`→`info` · `n`→`new` · `ctx`→`context` · `st`→`stop` · `int`→`interrupt` |
|
|
56
|
+
|
|
57
|
+
The canonical command is `/workspace`; `/w` is its bare alias. `/ws` and `/wi` are shortcuts for the two built-in verbs (`/workspace switch` / `/workspace info`), mirroring the `/ss` / `/si` session shortcuts.
|
|
58
|
+
|
|
59
|
+
## Cross-channel commands
|
|
60
|
+
|
|
61
|
+
All channels (WS, WeChat, Telegram, Web, CLI/TUI) share `dispatchCommand` for common commands. Channel-specific commands (e.g. WeChat `/name`) are handled in the channel handler before reaching the shared dispatch.
|
|
62
|
+
|
|
63
|
+
## Skill-as-command
|
|
64
|
+
|
|
65
|
+
Skills whose SKILL.md frontmatter carries a `command` field auto-register as slash commands:
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
---
|
|
69
|
+
name: code-review
|
|
70
|
+
description: Review code changes
|
|
71
|
+
command: /review
|
|
72
|
+
---
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Related frontmatter fields:
|
|
76
|
+
|
|
77
|
+
- `verbs` (Halo extension) — declares subcommands, each `{ name, builtin?, requiresAccess?, desc? }`; `builtin: true` means the verb is handled by platform code (declarative — actual routing is `SUBCOMMAND_ROUTES`); a skill verb's access gate is what's declared here.
|
|
78
|
+
- `user-invocable: false` (standard) — never becomes a slash command; the model can still activate the skill.
|
|
79
|
+
- `disable-model-invocation: true` (standard) — command exists but the skill is not injected for model auto-activation.
|
|
80
|
+
|
|
81
|
+
When triggered, the skill body is read from disk and sent to the agent session as a message:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
[Skill activated: /review]
|
|
85
|
+
|
|
86
|
+
{SKILL.md body}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
User args are filled via `$ARGUMENTS` / `$1`–`$9` placeholders (args are no longer appended verbatim to the body end); a verb reaches the body as `$1` for dispatch.
|
|
90
|
+
|
|
91
|
+
Every `GET /api/commands?projectId=xxx` rescans skill directories.
|
|
92
|
+
|
|
93
|
+
### Conflict detection
|
|
94
|
+
|
|
95
|
+
A skill's `command` must not collide with a built-in slash command or with another skill's. Collisions are resolved at scan time in `scanSkillDescriptors`, the single source feeding both dispatch and the discovery API:
|
|
96
|
+
|
|
97
|
+
- **Built-ins always win** — a skill command colliding with a built-in is not registered as a command. This is by design for object commands: their same-name skill (e.g. `agent`, `skill`, `workspace`) is shadowed as a command, but its body still serves the skill verbs (create / update etc.) via the verb fallback.
|
|
98
|
+
- **Among skills, first-come wins** (workspace scope is merged over global before this check); the later one is dropped with a warning.
|
|
99
|
+
- Colliding entries are **dropped** from the command list and a warning is logged (`[CommandRegistry] skill "<id>" command "/x" shadowed by …`). Dropping at the shared source guarantees a command can never appear in the palette that dispatch is unable to route ("visible but unreachable").
|
|
100
|
+
|
|
101
|
+
Note: a skill's `command` collision only removes the *slash command*; the skill itself remains usable via the agent's normal skill activation.
|
|
102
|
+
|
|
103
|
+
## Command discovery API
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
GET /api/commands?projectId=xxx
|
|
107
|
+
→ { commands: CommandDescriptor[] }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Returns every non-hidden command (built-in + skill). The frontend calls this on project switch to populate the command palette.
|
|
111
|
+
|
|
112
|
+
## Adding a new command
|
|
113
|
+
|
|
114
|
+
A server-handled command lives in **three** places that must stay in sync —
|
|
115
|
+
miss any one and the server either throws at startup or silently no-ops:
|
|
116
|
+
|
|
117
|
+
1. Add a `exec<Name>` function in `channels/shared/commands.ts`
|
|
118
|
+
2. Add a case to the `dispatchCommand` switch (same file)
|
|
119
|
+
3. Add the slash name to the `DISPATCH_COMMANDS` array (same file) — this is the
|
|
120
|
+
list the startup sanity check in `index.ts` compares against the registered
|
|
121
|
+
descriptors. It is **not** derived from the switch; forgetting it throws
|
|
122
|
+
`Command descriptors without a dispatch case: /<name>` and the server exits.
|
|
123
|
+
4. Register a descriptor in `commands/index.ts` via `commandRegistry.registerDescriptor()`
|
|
124
|
+
|
|
125
|
+
The startup check is bidirectional: every `type:'server'` descriptor must appear
|
|
126
|
+
in `DISPATCH_COMMANDS`, and every entry in `DISPATCH_COMMANDS` must have a
|
|
127
|
+
descriptor (else `Dispatch cases without a descriptor: /<name>`). Client-only
|
|
128
|
+
commands (e.g. `/clear`) belong in neither — they're handled in the frontend.
|
|
129
|
+
|
|
130
|
+
Implementation details in [design/command.md](../design/command.md).
|