Author SHA1 Message Date
Liang Jiaqing c85b59e06e feat: generic extra_sys_prompts slot replaces hardcoded commit_signature module 2026-06-20 21:03:38 +08:00
Liang Jiaqing 942bd315d7 ljqCtrl: Add SetProcessDPIAware; FindBlock returns raw max_val float 2026-06-20 16:40:11 +08:00
Liang Jiaqing 3dcca65348 chore: simplify summary, english turn hints, reasoning field compat 2026-06-20 10:43:46 +08:00
Liang Jiaqing 2057d86ac4 cleanup_sop: 补充触发词判定标准+归位≠移出L1+幽灵审计+禁值存储 2026-06-19 19:53:04 +08:00
Liang Jiaqing debf04cfff Improve loop prompt sidebar input 2026-06-19 19:36:14 +08:00
Liang Jiaqing b5a30f1982 feat: loop mode + idle auto-action in stapp, cleanup launch.pyw
- stapp.py: add loop mode (auto-inject prompt after each reply)
- stapp.py: replace DOM-based idle monitor with st.fragment timer
- stapp.py: i18n for all new UI text and LLM prompts
- launch.pyw: remove inject/get_last_reply_time/idle_monitor, keep only paste hook
- agentmain/ga: raise max_turns 80->180, DANGER thresholds accordingly
- remove obsolete tool_usable_history.json
2026-06-19 19:00:44 +08:00
Liang Jiaqing 26257a10db docs: clarify ui_detect OmniParser model 2026-06-18 17:14:10 +08:00
Jiaqing Liang 53b48aea07 feat(log): include model in response logs 2026-06-18 14:26:04 +08:00
nianyucatfish 12655687aa feat(worldline): checkpoint-tree rewind for tui_v2 (#625)
* fix(continue): preserve session workspace on in-place restore

In-place /continue retargets agent.log_path to the restored file, so the
reset bind `_bind_workspace(None)` ran during restore was persisting
session_ws_set(path, "") — erasing that session's own workspace mapping to
"" (read back as "explicitly off") before we read it, which also short-
circuited the log-scan fallback. The continued session never re-entered its
workspace.

Add a `persist` flag to `_bind_workspace`; the restore-time reset passes
persist=False so it only refreshes in-memory state. The session→workspace
map is now written solely by explicit /workspace, /workspace off, and a
successful restore.

* fix(tui_v3): support Home/End keys for line start/end

PTK delivers Home/End as raw VT sequences in KeyPress.data, which
_esc_repl swallowed (only arrows were decoded). Map \x1b[H/[F/[1~/[4~
and SS3 \x1bOH/\x1bOF to internal bytes 0x07/0x14, extend _ESC_RE to
match SS3 as whole sequences, and add jump-to-line-start/end handlers
in _keys. Home no longer collides with Ctrl+A select-all.

* feat(worldline): integrate checkpoint-tree rewind into tui_v2

- tuiapp_v2.py: /rewind durable inline picker + /worldline three-pane
  checkpoint tree (tree UI inlined; colors follow the v2 theme) with a
  per-node diff viewport; conv-only /rewind restores via the persistent store.
- worldline.py (new): UI-agnostic backend — RewindStore (persistent
  checkpoint tree + content-addressed blobs), reconcile, restore_plan,
  node_diff, native-log projection.
- continue_cmd.py: gated worldline support — list_sessions(rewind_root=),
  continue_inplace/copy(allow_empty=); default-off, other UIs byte-identical.
- tui_v3.py: status line shows concrete model id (unrelated minor tweak).

* feat(worldline): /rewind reuses the restore-mode picker (conv/code/both)

After picking a turn, /rewind now opens the same RestoreModeScreen as
/worldline to choose conversation / code / both, then restores via the
persistent store — making /rewind a true lite view of /worldline (linear
list, no tree/diff) over the identical restore backend.
2026-06-18 08:40:42 +08:00
Shen HaoandShen Hao f8ecfdbf7d fix(slash_cmds): /update reconciles working tree, not just git history (#624)
Co-authored-by: Shen Hao <shenhao-stu@users.noreply.github.com>
2026-06-18 01:02:30 +08:00
Octopusandocto-patch 3a6380ffb3 feat(wizard): bump MiniMax default to M3, drop M2.5 (#555)
Co-authored-by: octo-patch <octo-patch@github.com>
2026-06-17 20:16:19 +08:00
weijia be063e1348 fix(macljqCtrl): add CropToScreen helper + crop-origin coord hints (#614)
On macOS the screenshot path lacked a counterpart to win32's
ClientToScreen. Detecting a target inside a cropped GrabScreen(bbox)
image and feeding the in-image (px,py) straight to Click silently
mis-clicks because the crop-origin offset is dropped.

- add CropToScreen(bbox, x, y): in-crop coords -> absolute physical
  coords (pure crop-origin add, no scaling)
- GrabScreen docstring: warn about the crop-origin trap and not to
  hand-roll screencapture -R (it takes logical points)
- Click 0%-pixel-change warning now names the common causes
- ljqCtrl_sop.md: add macOS subsection mirroring the win32 trap note
2026-06-17 20:15:16 +08:00
nianyucatfish 37a58c0d42 fix(continue): preserve session workspace on in-place restore (#621)
In-place /continue retargets agent.log_path to the restored file, so the
reset bind `_bind_workspace(None)` ran during restore was persisting
session_ws_set(path, "") — erasing that session's own workspace mapping to
"" (read back as "explicitly off") before we read it, which also short-
circuited the log-scan fallback. The continued session never re-entered its
workspace.

Add a `persist` flag to `_bind_workspace`; the restore-time reset passes
persist=False so it only refreshes in-memory state. The session→workspace
map is now written solely by explicit /workspace, /workspace off, and a
successful restore.
2026-06-16 21:38:01 +08:00
Liang Jiaqing 650d0c5e0f Add codex metadata for responses requests 2026-06-16 21:16:30 +08:00
Liang Jiaqing 8e9b04e165 Update agent docs and task help 2026-06-16 11:27:31 +08:00
AspasZhangandAspasZhang 31a8c4b57c update wechat group 21 QR code (#620)
Update WeChat group 21 QR code image.

Co-authored-by: AspasZhang <AspasZhang@users.noreply.github.com>
2026-06-16 09:44:11 +08:00
Liang Jiaqing 0101419001 Update supervisor SOP and execution reminders 2026-06-15 21:35:28 +08:00
nianyucatfish ae7b028f6d feat(continue): default in-place restore + per-session lock (copy-on-conflict) (#617)
/continue N 默认改为「原地续」:接管原会话日志、后续轮次写回同一文件,取代旧的
镜像复制(supersedes b4356c7 的 mirror 方案),不再每次续接都 fork 新会话、日志增殖。

- 空闲会话 → 直接原地接管;被活进程占用 → 弹窗确认后才复制一份续。
- 每会话出生持锁(temp/model_responses/.locks/<logid>.lock),整进程共用一个心跳
  线程每 5s touch mtime、30s 无心跳判死可接管;atexit 干净释放,崩溃/强杀靠超时兜底。
- 切走/新对话不再「快照+清空」(原 _snapshot_current_log 因 logid≠pid 实为死代码,
  从未生成过快照),旧日志原样留作空闲会话,新对话铸新 logid。
- list_sessions 新增 exclude_log,修正 exclude_pid=getpid 失效导致当前会话出现在
  自己列表里的问题。
- continue_cmd 仅新增函数,reset_conversation/restore/handle 未改动 → 其他前端
  (IM/qt/streamlit 等)行为完全不变。
- 改动仅限 continue_cmd.py + tui_v3.py + tuiapp_v2.py;rewind/worldline 逻辑不在本 PR。
2026-06-15 21:20:20 +08:00
Liang Jiaqing 3cf1d840fe Improve conductor approval workflow 2026-06-15 10:15:17 +08:00
Liang Jiaqing 03fc251b54 Fix reflect loop throttling 2026-06-15 10:01:08 +08:00
Liang Jiaqing cda7c9d63f docs(vision): add GA bootstrap hints to vision_api template
Annotate config block to guide new GA instances: enumerate candidate
mykey.py variable names and experimentally probe which config works;
only print var/field names, model, apibase host/path, status codes and
error types (never full dict or apikey/token); include a config format
example. Also flag that apibase/endpoint may differ per provider/proxy
at the two request-building sites.
2026-06-15 10:01:08 +08:00
weijia 7b20f659f0 feat(memory): enhance macOS AX control (resolve_pid fix, enabled field, AXClick)Feat/macos ax enhancements (#611)
* feat(memory): enhance macOS AX control (resolve_pid fix, enabled field, AXClick)

- _resolve_pid: fix bundle_id branch (used un-imported AppKit), 3-tier match (bundle id > app name exact > substring)
- AXElements: collect 'enabled' field (SOP: check disabled before click)
- AXClick: AXPress-first then fallback to physical-coord Click, honest success判定 via pixel diff
- AXFind: add enabled_only filter, refactor if-forest into _hit helper

* docs(ljqCtrl): tighten macOS AX guidance, drop boilerplate & app-specific detail

- collapse 5-line cross-platform import boilerplate to one line
- generalize control-identifier tip (remove app-specific identifiers)
2026-06-15 09:00:45 +08:00
nianyucatfish b4356c7262 fix(tui-v3): /continue 恢复后镜像源日志,避免续聊丢失旧历史 (#612)
v3 的 _do_restore 此前只把历史载入内存 backend.history,不碰当前进程日志。native 后端逐轮只把新增消息 append 到 model_responses_{pid}.txt,恢复出的旧历史从不写入当前日志——续聊只追加增量,下次 /continue 扫到的本进程文件就只剩续聊部分,旧历史丢失。

对齐 v2 _do_continue_restore:恢复前 reset_conversation(快照+清空当前日志),恢复后 copyfile(源日志 → 当前日志),续聊便接在完整历史之后。
2026-06-15 08:59:37 +08:00
Liang Jiaqinganddilong888 80d900514d fix(desktop): clear busy flag after ESC cancel and on bridge close to prevent UI freeze
ESC cancel sent session/cancel RPC but never cleared the busy flag, leaving the UI stuck if the server-side cancelled event never arrived. Also clear busy on all sessions when the bridge closes so pending poll loops exit cleanly.

Co-authored-by: dilong888 <dilong888@users.noreply.github.com>
2026-06-14 09:43:07 +08:00
Liang Jiaqing f8b8b1b443 Remove legacy desktop_pet v1 (desktop_pet.pyw, pet.gif); v2 fully covers 2026-06-14 09:24:33 +08:00
weijia 8ca5a8694d feat(memory): add macOS port of ljqCtrl with AX accessibility support (#606)
- macljqCtrl.py: macOS implementation of mouse/keyboard/screenshot/window
  enumeration mirroring ljqCtrl API, plus AX accessibility control tree
  (AXElements/AXFind/AXPress) as the macOS equivalent of UIA
- computer_use.md: add section 3 documenting the macOS platform branch
- .gitignore: whitelist memory/macljqCtrl.py
2026-06-14 09:14:06 +08:00
nianyucatfish 4572854b32 feat(tui): workspace 项目模式 + @ 文件引用补全(v2/v3) (#607)
* feat(tui): workspace 项目模式 + @ 文件引用补全(v2/v3)

- workspace: /workspace 设/off/picker、junction 复用 project_mode 记忆、
  per-session(v2)/进程级(v3)、/continue 恢复、session map 持久化绑定状态
- @ 补全(completion-only): 子序列模糊 + path-like 目录补全(~/ / ./ C:\)、
  提交期 @路径作为普通文本交 agent
- v3 filterable 焦点链 picker(输入框作焦点环对象、free_input)
- 新增共享模块 at_complete.py / workspace_cmd.py;核心零改动

* feat(tui): @ 路径绝对化 + 默认根=temp + 未绑显示完整路径

- 默认根 os.getcwd() → agent 工作目录 <GA根>/temp(与 file_read/code_run 一致、不随启动 cwd 飘)
- 提交期 @相对 → @绝对(display 保留相对短路径),让 agent file_read(相对自身 cwd)找得到
- 未绑 workspace 时候选显示完整路径(根不直观);索引忽略 model_responses 会话日志噪音
- 启动预热 temp 索引 + candidates_for 惰性兜底(任何根首次访问自动建)
2026-06-14 09:10:04 +08:00
LJQ 9aeb80fd52 Merge pull request #595 from Michaelxwb/main
修复个人微信前端在 headless 容器内无法登录
2026-06-13 15:52:18 +08:00
Jahan 86875e0e3d Merge branch 'lsdefine:main' into main 2026-06-13 14:54:46 +08:00
Liang Jiaqing 0b2ff9e7f0 Fix stapp bottom input layout 2026-06-13 12:07:46 +08:00
Jahan 34b463c558 Merge branch 'lsdefine:main' into main 2026-06-12 19:39:52 +08:00
Liang Jiaqing 49528d32db Fix truncation warning placement and untrack skill_search 2026-06-12 14:33:03 +08:00
陈克文 d59b64550f fix: os.startfile is Windows-only, use webbrowser.open to wake the browser (#598)
web driver init nudges the browser by opening a page when no session
appears; os.startfile raises AttributeError on Linux/macOS, killing
the retry loop on headless servers. webbrowser.open is stdlib and
cross-platform with the same effect.
2026-06-12 13:16:23 +08:00
nianyucatfish aa93782a6d fix(tui-v2): restore CJK table copy alignment (#600)
The tool-cards change (#593) tightened _md_line_has_box_drawing to exempt
pure-horizontal '─' runs as markdown hr. But a SIMPLE-box Markdown table's
only box glyph is its header rule, so that exemption dropped whole tables
out of the passthrough copy path and into the wide-render aligner, which
maps selection cell-columns to character indices — misaligning copy of any
table with CJK (wide) cells.

Remove the pure-horizontal exemption so a table's header rule is detected
again and the table run stays passthrough. Card gutters (└─ / │ / └) keep
their own prefix-based exemptions, so tool cards are unaffected. The only
cost is a real markdown hr copying as a dash run — the pre-#593 behavior.
2026-06-12 13:15:34 +08:00
Jahan 836a5c22b6 Merge branch 'lsdefine:main' into main 2026-06-12 11:51:48 +08:00
Liang Jiaqing 0def744157 fix responses codex client metadata 2026-06-11 21:58:58 +08:00
nianyucatfish e38f2331af feat(tui): plan 卡片 /continue 恢复 + 可滚动 4 行窗口 + Ctrl+S/Ctrl+C 修复 (#599)
* feat(tui): /continue 恢复 plan 卡片 + Ctrl+S/Ctrl+C 路由修复

plan-card 恢复:plan 模式判定改用日志中结构化的 enter_plan_mode
tool_use(continue_cmd.find_plan_entry),不再扫聊天文本——用户打字
伪造不出。is_active/resolve_path 新增 restored_path 信号,AgentSession
新增 restored_plan_path 字段,plan_scan_baseline 退化为仅供 current_step
的 📌 扫描用。

Ctrl+S restore 卡死:_stash_cleanup_restore 由 self.text= 改为
clear()+_insert_via_keyboard,走编辑管线避免长会话全屏 relayout,与
stash 清空路径对称。

Ctrl+C 弹回收紧:_cmd_stop 仅在该轮未被消费(agent 尚未产出回复)时
才把 user 文本弹回输入框,避免已答轮次污染输入框 / 重发导致历史重复。

* feat(tui): plan 卡片可滚动(4 行任务窗口)+ TODO 渲染为 [ ]/[x]

planbar 拆为固定表头/步骤行(#planbar-head)+ 任务滚动区
(#planbar-tasks,max-height:4)。最多露 4 个 TODO,其余滚轮/PageUp
翻看,去掉旧的 5 行预算与 ⋮ +N more 截断;表头与当前步骤行固定不滚。

任务标记 ☐/✔ → [ ]/[x],与 plan.md 源文件写法对齐。
2026-06-11 21:11:25 +08:00
Jahan 5737476c9c Merge branch 'lsdefine:main' into main 2026-06-11 19:08:05 +08:00
Liang Jiaqing 260027c946 Update Codex native user agent 2026-06-11 11:05:32 +08:00
nianyucatfish a6c712b1a6 feat(tui): 工具显示卡片 + /model 与 /effort 运行时切换 (#593)
tuiapp_v2 的两项增强,拆成两个 commit。

## 1. 工具显示卡片(file_write / file_patch / file_read / code_run)
把 agent_loop verbose 的 `🛠️ Tool` 块替换为紧凑卡片:
- **file_write / file_patch**:行号 + 上下文的主题化 diff;append/prepend 走整文件 diff 拿真实上下文。
- **file_read**:读到的内容带行号 gutter,剥离 LLM-facing 噪音(`[FILE]`/show_linenos 前言/截断提示),超长折叠;行号在不可知处(keyword 窗口 / 挖洞后)留空。
- **code_run**:参考 Claude Code / Codex 的 exec 渲染,无边框、`│` 命令 gutter + `└` 输出 gutter,输出 dim、**保尾截断**(traceback 关键在末尾);代码来自正文 ```块``` 时不重复展示。
- 三卡共用 `_CardWriter` / `_card_status_row` / `_emit_gutter`;2 格左 margin 只进显示流、不进复制源。
- capture 机制:live 走 `tool_before/after` 钩子,`/continue` 走 `iter_write_captures` 从历史 tool_result 重建,按 `hash(get_pretty_json(args))` 内容寻址。
- 双流渲染对齐(`_align_md_renders`)支持 margin 映射,鼠标复制不含视觉缩进。

## 2. /model 与 /effort 渠道内运行时切换
- **/model**:在线拉模型列表的 searchable picker;搜索框兼作自定义模型名输入(无匹配时 Enter 直设)。
- **/effort**:reasoning effort 档位切换,协议差异在 `effort_note` 单点编码——Claude(`output_config.effort`) low/med/high 原样、xhigh→max、none/minimal 忽略;OpenAI(`reasoning_effort` / `reasoning.effort`) 透传;mixin 经 `_BROADCAST_ATTRS` 广播到全部子渠道。
- 逻辑收在 `frontends/model_cmd.py`(agent 无关、可单测)。

## 测试
- 卡片:两份真实会话日志全链路重放,51 个 code_run + 23 read + 20 write 全部渲染、双流 parity、复制不含 margin。
- /model /effort:拦截真实 llmcore 请求构造验证 effort 进 payload(含 mixin 广播、各协议字段);picker 自定义输入走 Textual pilot 端到端。
2026-06-11 09:19:09 +08:00
Jahan bd114dc49b Merge branch 'lsdefine:main' into main 2026-06-11 09:18:31 +08:00
徐文彬andClaude Opus 4.8 b6b13cc8cf fix(wechat): 修复个人微信前端在 headless 容器内无法登录
容器化部署时个人微信前端无法首次扫码登录,定位并修复 4 处问题:

1. isatty 闸门顺序错误:__main__ 先把 stdout 重定向到日志文件,再判
   sys.stdout.isatty()——文件句柄恒为 false,导致无 token 时必然退出。
   改为始终走 QR 登录、二维码打到真实 stdout(docker logs 可见),
   无需 TTY 即可扫码。
2. get_bot_qrcode 无容错:被限流时响应缺 'qrcode' 字段直接 KeyError 崩溃,
   配合 restart 策略形成死亡螺旋。改为退避重试,等 qrcode + img_content
   都就绪。
3. get_qrcode_status 轮询只 catch ReadTimeout:限流返回非 JSON 时 .json()
   抛错崩溃。改为捕获所有 RequestException + ValueError 容错重试。
4. PNG 兜底依赖 PIL,缺失时 qrcode.make().save() 抛 ModuleNotFoundError,
   且 traceback 被 finally 重定向到日志文件而难以发现。改为先打 ASCII
   二维码(扫码真正所需,无依赖),PNG 保存包 try 失败不崩。

均为通用的容器化健壮性改进。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 09:15:55 +08:00
Liang Jiaqing f39622c960 docs: clarify tiered dependencies (core vs [ui] extra) 2026-06-10 23:22:06 +08:00
Liang Jiaqing bdc8e6bdcb docs+fix: README install overhaul, self-hosted install scripts, TMWebDriver logging fix
- README (EN): clone+uv as recommended install; one-line installer demoted to convenience with script source link; drop unpolished exe section; recommend frontends/tui_v3.py (prompt_toolkit+rich, not Textual); note agent core's single dependency (requests); TMWebdriver naming
- assets: add ga_install.ps1/.sh (GLOBAL mode uses GitHub/astral-sh sources)
- TMWebDriver.py: don't let logging failure mask successful remote JS result
2026-06-10 23:18:10 +08:00
Liang Jiaqing 5e42bf3ce9 goal_mode: strengthen continuation prompt (mandatory substantive improvement, standard-upgrade report) 2026-06-10 20:25:51 +08:00
Liang Jiaqing 34d9895538 llmcore: fix empty-response not triggering mixin failover; distinct default UA for NativeOAISession 2026-06-10 20:25:51 +08:00
Jiaqing Liang 70533341d0 configure: generic protocol entries (OAI chat/responses, Claude messages) with custom apibase; neutral descs 2026-06-10 16:31:11 +08:00
weijia eae6d1f3f3 feat(project-mode): pid-keyed anchor — auto-deactivate on GA exit, multi-instance isolation (#592)
- Anchor file becomes temp/.active_project.<host pid>: dies with the GA process,
  so closing GA auto-deactivates; re-activation requires user confirmation per SOP
- Multiple GA instances each keep their own active project, no cross-talk
- Startup sweep removes only legacy unsuffixed anchors and own-pid leftovers
  (pid reuse); anchors of other pids are never touched — deliberately no
  liveness probe, because os.kill(pid, 0) on Windows terminates the process
2026-06-10 14:57:48 +08:00
Jiaqing Liang 5771a91abe README: drop trivial chat-commands sections (EN/CN) - ask the agent instead 2026-06-10 14:54:04 +08:00
Jiaqing Liang e702783327 README: platform-aware launch steps, hCaptcha real-browser demo, EN/CN section polish 2026-06-10 14:49:13 +08:00
weijia 19875716d4 feat: project mode plugin — cross-session project memory via two-layer injection (#591)
Activate a project with a file anchor (temp/.active_project); every turn
the agent_before hook appends an L1 context (rules + memory file pointer
+ wrap-up discipline) to the latest user message, while the full project
memory (L2) stays on disk for the model to read on demand.

Zero core changes: plugin + SOP + gitignore whitelist only.
2026-06-10 13:18:38 +08:00
Liang Jiaqing 3284222725 memory: subagent.md 更新fork/cwd共享约定 2026-06-10 11:14:31 +08:00
Liang Jiaqing d941f7cfa0 reflect: goal_mode prompt v2 — 创造/检验/改进交替循环+及格线/升级链 (INTERVAL 3→5) 2026-06-10 11:14:31 +08:00
Liang Jiaqing f1c8faba66 fix: chat bottom padding 7->10rem; disable forced max_tokens=64000 in NativeClaudeSession 2026-06-09 22:00:50 +08:00
Liang Jiaqing a742e7d96f fix(llm): stream read_timeout 30s->40s, avoid reasoning-model long-thinking false timeout
fix(frontend): force chat_input pinned to bottom (iframe wrapper fallback); drop details scroll-fix JS
2026-06-09 22:00:50 +08:00
AspasZhang cdf199834c docs: add GenericAgent-Admin to community GUIs (#589) 2026-06-09 17:40:52 +08:00
Jiaqing Liang afd2d796bb fix: widen window & collapse sidebar to work around streamlit narrow-viewport bug 2026-06-09 16:43:05 +08:00
Jiaqing Liang e4a12dde2b refactor: simplify driver init wait; clarify TMWebDriver master error msg 2026-06-09 14:23:49 +08:00
Jiaqing Liang afcb6a47af docs: tighten official notice and browser realness table 2026-06-09 10:06:00 +08:00
Jiaqing Liang 7c7f960df3 docs: remove WeChat group from English README section 2026-06-09 10:01:54 +08:00
Jiaqing Liang 8061a68ee0 docs: add web tool browser realness results 2026-06-09 10:00:05 +08:00
LJQ ba19018a6d Merge pull request #586 from nianyucatfish/fix/tui-v3-cmd-expand
fix(tui-v3): expand pasted placeholders before slash-command dispatch
2026-06-09 08:39:13 +08:00
yanghang 0970bd3180 fix(tui-v3): expand pasted placeholders before slash-command dispatch
Slash commands returned from _on_enter before _expand ran, so a command
argument carried the literal [Pasted text #N] / [File #N] / [Image #N]
placeholder instead of the real content — e.g. `/morphling <pasted
multi-line target>` reached the agent as the marker string, not the paste.

Expand placeholders at the top of the command branch (mirroring the normal
submit path) and clear the placeholder stores afterward. No-op for commands
without placeholders.
2026-06-08 23:53:52 +08:00
AspasZhang edee6e8821 chore: update WeChat group QR code (group 20 → group 21) (#585) 2026-06-08 20:22:25 +08:00
Liang Jiaqing dd43f23cfd docs: update mobile computer use notes 2026-06-08 19:39:11 +08:00
Jiaqing Liang 4001c43f0a Update LLM user agent handling and incubator notes 2026-06-08 15:05:47 +08:00
nianyucatfish bb16cd0f21 fix(tui-v2): copy via native clipboard, OSC 52 only over SSH (#581)
Ctrl+C / selection copy fell back to Textual's OSC 52 escape on Windows/Linux.
Some terminals (notably VSCode's integrated terminal) decode the OSC 52 base64
payload as latin-1 instead of UTF-8, so non-ASCII copied text lands in the
clipboard as mojibake (e.g. 需要 -> éè¦). Classic Windows conhost ignores
OSC 52 clipboard writes entirely, so copy was a silent no-op there (incl.
Windows Server).

Prefer the OS-native clipboard (_copy_to_clipboard: win32 CF_UNICODETEXT /
pbcopy / wl-copy / xclip), which writes real Unicode and bypasses the terminal.
Keep OSC 52 only as a fallback, and force it under SSH (SSH_CONNECTION /
SSH_TTY) where the native call would write the remote host's clipboard instead
of the user's local one.
2026-06-08 10:47:29 +08:00
Liang Jiaqing cfb577e27c Update agent hints and UI SOP note 2026-06-08 09:28:05 +08:00
AspasZhang de0a3f82a8 chore: update WeChat group 20 QR code image (#577) 2026-06-06 19:51:31 +08:00
Liang Jiaqing b365945886 fix(scheduler): ensure sche_tasks dir exists to avoid import crash killing L4 cron 2026-06-05 23:23:41 +08:00
Liang Jiaqing ff5fa0d167 Refine computer_use window diagnostics 2026-06-05 22:21:20 +08:00
Liang Jiaqing 108fded5a9 Add computer_use SOP for UI operations 2026-06-05 22:15:54 +08:00
nianyucatfish b6ca5f27eb fix(tui v2): serialize terminal escapes via driver + scrollable sidebar (#572)
- _term_write(): route OSC/control sequences through Textual's driver writer
  queue instead of direct sys.__stdout__. A title/OSC write landing mid-frame
  during streaming used to split Textual's own escape sequences, rendering as
  flashing ANSI garbage; serializing on the same writer queue lands it cleanly.
- sidebar: wrap the session list in a VerticalScroll (#sidebar-scroll) so it
  scrolls when overflowing (scrollbar-gutter reserved to avoid row reflow);
  inner #sidebar Static keeps padding so the click hit-test math is unchanged.
  Keyboard session-switch scrolls the active session into view.
2026-06-05 18:44:01 +08:00
Jiaqing Liang efbc7833a1 fix(conductor-ui): prevent Enter from sending during IME composition 2026-06-05 16:49:52 +08:00
Jiaqing Liang 234084761a fix(conductor): disable unconfigured IM plugins by default
Rename tracked email.py/lark.py to _email_example.py/_lark_example.py so
the poller (which skips _-prefixed files) no longer polls them on a fresh
clone. Activate by copying to email.py/lark.py (now gitignored). Also nudge
manager to reuse existing subagents on IM signals.
2026-06-05 16:18:52 +08:00
Jiaqing Liang 842f73b5b0 feat: 新增飞书(lark)conductor IM插件 + UI/插件体系完善
- conductor_im_plugins: 新增 lark.py(飞书收消息插件,无状态轮询)、email.py、_TEMPLATE.py
- wechat.py/config.local.json 经gitignore排除(含个人会话ID)
- conductor.html/py: UI重构(grid/可折叠卡片/console/approvals)、action与pool化重构
- stapp.py: 移除_js_scroll_fix
- ljqCtrl_sop: 补充Click点歪诊断/DPI坐标陷阱/文本输入经验
2026-06-05 15:03:41 +08:00
weijia fb4f24e231 docs: add description for galley in friendly links (#565) 2026-06-04 17:48:50 +08:00
Liang Jiaqing 5d122e20ea core: per-instance print injection (no_print), EXIT sentinel for run loop, log name collision fix, checklist poll hard cap 2026-06-04 11:09:54 +08:00
Liang Jiaqing 3c4adeb26c fix(ljqCtrl): GrabWindow只截客户区,与GrabWindowBg坐标系统一 2026-06-02 23:22:02 +08:00
LJQ b27406f34c Merge pull request #559 from nianyucatfish/fix/continue-preview-garbage
fix(continue): robust session preview for malformed logs
2026-06-02 19:39:03 +08:00
yanghang 6f7121242b fix(continue): robust session preview for malformed logs
/continue list previews showed JSON debris (e.g. `{`, `" } ]} === Response ===`)
for some sessions. Two causes in _preview_from_file:

1. Models sometimes emit an unclosed <summary>, so the non-greedy DOTALL regex
   pairs it with a far-away </summary> and captures === block headers / JSON
   across rounds. Now only the latest <summary> is used, and it is rejected if
   it looks like such a cross-segment capture (contains `=== `/`"role"` or
   >200 chars) — falling through instead of digging for older summaries.
2. The fallback scanned for the first non-=== line, returning JSON structure
   lines. It now uses the last real user prompt via a new _last_user(), which
   scans Prompt blocks directly (so response-less/aborted sessions still
   preview) and reuses _user_text() to skip tool_result continuations and all
   _INJECT_MARKERS (WORKING MEMORY / SYSTEM TIPS / DANGER ...).

Scanned 261 local logs: garbage previews 26 -> 0.
2026-06-02 19:19:55 +08:00
Jiaqing Liang 9d41f42ca9 goal_hive master duty: 按第一性原理/工程控制论重写为49行(探测-设计-执行-检查分形循环+J*/y/e),实验验证后增补验收线/锁边界质疑目标/遗留项如实入报告; TMWebDriver: 修mark_disconnected重复打印 2026-06-02 15:01:52 +08:00
LJQ 5f46b43816 Merge pull request #550 from desmonna/fix/plugin-hooks-init
fix: add plugins/__init__.py for proper package recognition
2026-06-02 10:10:20 +08:00
nianyucatfish e73596cc61 fix(tui): stdout blank lines, leaked task_dir folders, and /continue search nav (#554)
* fix(tui): stop CRLF stdout rendering as blank lines (v2)

Windows child stdout is `\r\n`; the streaming path fed it straight to
`Text.from_ansi`, which treats `\r` as a carriage return and erases each
line's text, so a `[Stdout]` block rendered as a run of blank lines until
the turn finished (the done-state Markdown render strips `\r`). Normalise
`\r\n` -> `\n` before `from_ansi`; lone `\r` is kept so progress-bar
overwrites still work.

* fix(tui): stop leaking empty per-session task_dir signal folders

Both v2 and v3 eagerly `os.makedirs` the per-session `_intervene` /
`_keyinfo` signal dir, but the only writer (`_intervene`) already creates
it lazily — so every session that never used intervene left a stale empty
`temp/_tui_v2_<pid>_<id>` / `temp/_tui_v3_<pid>` behind (85+ accumulated).
`consume_file` tolerates a missing dir, so the eager create is redundant.

- Set only the task_dir *path*; let the writer create it on first inject.
- Remove empty dirs on session close (v2 `_cmd_close`), on graceful exit
  (v2 `on_unmount`, v3 `_cleanup`), and sweep stale ones left by crashes
  at startup (v2 `on_mount`, v3 `main`). `os.rmdir` only removes empty
  dirs, so a still-pending `_intervene` is never clobbered.

* fix(tui): repair /continue search box cursor & up-nav loop (v2)

The SearchablePicker swallowed Right while the search Input had focus
(always treating it as "select"), so the caret could never move right in
the query. And Up could leave the list but never return: from the search
box Down entered the list, yet Up at the top row was stuck.

- Right commits the highlight only when the caret is already at end of the
  query; otherwise it moves the caret within the search text.
- Up on the first list row returns focus to the search box (scoped to
  /continue — no-op for ChoiceLists outside a SearchablePicker), clearing
  the highlight so the next Down re-enters at the first row, not the second.
- Up from the search box wraps around to the bottom of the list.
2026-06-01 23:07:28 +08:00
Liang Jiaqing fb64945577 fix checklist master coordination and startup dedupe 2026-06-01 22:32:16 +08:00
desmonna e2407b1fac fix: add plugins/__init__.py for proper package recognition 2026-06-01 14:20:36 +08:00
Liang Jiaqing e87f9e72d6 refactor: rename mapreduce -> checklist (helper/master/sop) 2026-05-31 23:21:17 +08:00
Liang Jiaqing 8f0084c190 feat: bbs key模式2天超时自关; reflect首次立即check; worker交付规范; vision_sop修正 2026-05-31 21:53:09 +08:00
LJQ 071fb6e790 Merge PR #534: Update WeChat group QR code (group 19 → group 20)
Update WeChat group QR code (group 19 → group 20)
2026-05-30 11:36:01 +08:00
AspasZhang a46aa8b7ae Update WeChat group QR code from group 19 to group 20 2026-05-30 01:51:22 +08:00
nianyucatfish fab2ae720a fix(tui): drain exit-boundary intervened replay (#533)
* fix(tui): drain exit-boundary intervened replay

* refine(tui): soften intervened message prompt
2026-05-29 21:55:50 +08:00
Frank-GradientandClaude Opus 4.7 d75c0b7dc1 feat(wizard): add CommonStack as a unified API gateway provider (#523)
CommonStack (https://commonstack.ai) is an OAI-compatible gateway
aggregating Claude, GPT, Gemini, DeepSeek, MiniMax, Zhipu, xAI,
Moonshot, Qwen and Xiaomi models behind a single key.

Adds it to the configure_mykey wizard as a native_oai entry,
placed alongside other multi-provider relays (openrouter, crs, gmi).
The wizard's existing probe_models() flow fetches the live
/v1/models list at config time; model_choices keeps the same
2-item fallback shape as openrouter.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 21:34:10 +08:00
Liang Jiaqing 7e19e86967 Update SOP notes and response history window 2026-05-29 21:02:31 +08:00
Liang Jiaqing 000e39d0ea Refine goal hive master duty delivery quality 2026-05-29 21:00:03 +08:00
Liang Jiaqing fe44c7ff6f docs: clarify goal hive master instructions 2026-05-28 22:30:16 +08:00
Liang Jiaqing a61cfcf5aa Merge PR #526: fix(tui-v3): skip ask card when ask_user has no candidates 2026-05-28 22:28:25 +08:00
yanghang dfab2998a7 fix(tui-v3): skip ask card when ask_user has no candidates (v2 parity)
之前 `_extract_ask_user` 对无候选项的 ask_user 事件也照常入队,
触发 `_enter_ask` → 自由文本输入的 ask 卡片路径,在某些终端下整个
live region 冻住。v2 (`tuiapp_v2.py:2909-2910`) 是显式 `if not cands: return`,
让 "Waiting for your answer ..." 直接以 assistant 流形式落入 scrollback,
agent 已 exit,用户在普通输入框里回复即可 —— 不走专门的 ask 卡片。

对齐 v2 行为:有候选才入队、才弹卡片;无候选 fallthrough 到普通流。
2026-05-28 22:18:27 +08:00
LJQ 6733aec2c8 Merge PR #525: fix(tui): v2 theme/clipboard/Shift+Enter, v3 arrow-nav/Shift+EnterFeat/tui v3 improvements
Merged via GitHub API as requested.
2026-05-28 21:44:13 +08:00
yanghang d4777633ca refine(tui-v3): 行头/行尾跳转仅在首/末逻辑行触发
之前在任意逻辑行的视觉首/末行都会做"先跳行头/尾再跨行/进历史"的
两段式,中间行的两段式不直观。改成:中间逻辑行的视觉首/末行直接
做视觉上下移;只有第一逻辑行的视觉首行/最末逻辑行的视觉末行,才
触发"先跳到行头/尾,下次按键再进历史"。`_logical_visual_range`
因此变成死代码,顺手删掉。
2026-05-28 21:14:48 +08:00
yanghang b8b40880d7 Merge remote-tracking branch 'upstream/main' into feat/tui-v3-improvements 2026-05-28 21:14:47 +08:00
yanghang 468f7e519d feat(tui-v3): Shift+Enter on Windows + logical-line arrow navigation
- _ptk_keypress_to_bytes 在 Windows 上对 Enter 收到 \r 时用
  GetAsyncKeyState(VK_SHIFT) 物理检测 Shift,按下时返回 \n 以触发换行
  (PTK 在某些 Windows 终端区分不出 Shift+Enter 与裸 Enter)。
- 新增 _line_region / _logical_visual_range 辅助函数,↑/↓ 按键:
  不在该逻辑行的视觉首/末行 → 视觉滚行;在视觉首行且光标在行首 →
  历史导航,否则先跳逻辑行首/尾,下次按键再跨行/进历史。多行粘贴
  内部导航更符合直觉。

Refs:
- Shift+Enter 物理检测 from upstream PR #519 by @jlu005807
  https://github.com/lsdefine/GenericAgent/pull/519
- 多行输入逻辑行边界导航思路 from upstream PR #520 by @jlu005807
  https://github.com/lsdefine/GenericAgent/pull/520
2026-05-28 21:14:34 +08:00
yanghang 7c9ce6b4ac fix(tui-v2): clipboard copy + Shift+Enter + theme-switch markdown colors
- /export clip 现在通过 _copy_to_clipboard 直接写系统剪贴板:
  Win32 ctypes(CF_UNICODETEXT,避开控制台 codepage)/ macOS pbcopy /
  Wayland wl-copy / X11 xclip 或 xsel,失败再退回手动复制。
- InputArea 在 Windows 上对 Enter 事件用 GetAsyncKeyState(VK_SHIFT) 兜底,
  终端把 Shift+Enter 报成普通 Enter 时也能正确插入换行而非提交。
- watch_theme 漏清 _seg_render_cache 导致主题切换后 Markdown 的
  h1/列表 bullet/code 块/link 等仍是上一主题的色 —— 缓存 key 不含
  theme,且 ANSI 颜色被烤进了 _MdRender。补上 clear()。

Refs:
- Shift+Enter 物理检测思路 from upstream PR #519 by @jlu005807
  https://github.com/lsdefine/GenericAgent/pull/519
2026-05-28 21:14:21 +08:00
Liang Jiaqing 316d1397bb Merge PR #521 2026-05-28 20:03:42 +08:00
Liang Jiaqing 3a9e90bbba Merge PR #520 2026-05-28 20:03:42 +08:00
Liang Jiaqing 95781b4bc1 Merge PR #515 2026-05-28 20:03:41 +08:00
Shen Hao 24255c1236 fix(tui-v2): Ctrl+S no longer freezes on long sessions (use TextArea.clear)
Root cause of the lingering Ctrl+S freeze even when the session is
idle: `InputArea.reset()` calls `self.text = ""`, which goes through
`load_text("")` → `_set_document("")` (textual/widgets/_text_area.py
:1181-1186).  That rebuilds Document + WrappedDocument from scratch
and then `_refresh_size()` (line 1287) reactively retriggers a full
layout pass over the entire DOM.  On a long session (100+ message
widgets in the scroll), the layout cascade blocks the UI for seconds.

Switch to `TextArea.clear()` which goes through the edit pipeline:
`delete((0,0), document.end)` → `wrapped_document.wrap_range(...)` —
no Document rebuild, range-scoped wrap only.  Guarded with
`if self.document.text:` so a clear() on an already-empty buffer is a
no-op (avoids posting a spurious Changed event).

Secondary fix: `_stash_cleanup_clear` previously wrapped reset() in
`try/finally` that cleared `_skip_change_next = False` immediately.
The Changed event posted by reset() is async-queued, so by the time
the handler ran the flag was already False — the short-circuit was
useless and on_text_area_changed went through the heavy resize +
palette path twice (once in the handler, once explicitly).  Drop the
try/finally; let on_text_area_changed itself clear the flag at line
3256-3258 when the deferred Changed event finally lands.

Also scrubs external TUI-project names from comments per request.
2026-05-28 19:09:21 +08:00
Shen Hao 1a61b68f74 fix(tui): restore mid-turn inject with Claude-Code wrap (not post-turn)
Re-research revealed both mainstream TUIs DO mid-turn inject — my
previous read of "Codex/CC queue post-turn only" was wrong.

Codex (codex-rs/core/src/session/mod.rs:2986): `steer_input` → mailbox
`push_pending_input` → `session/turn.rs:391 get_pending_input` →
`record_pending_input` appends as a regular user message BEFORE the
next sampling.  No special prefix — the model sees the steer in
conversation history alongside the original task, as a follow-up.

Claude Code (utils/messages.ts:5510): drains queue after each tool
iteration and wraps the user's text:

  The user sent a new message while you were working:
  {raw}

  IMPORTANT: After completing your current task, you MUST address
  the user's message above. Do not ignore it.

The actual bug in model_responses_042511.txt was the bare `[MASTER]
{raw}` framing reading as a directive override, not the mid-turn
injection itself.

This commit:
  - Wraps queued text with the Claude-Code phrasing before writing
    to `_intervene` (i18n: en + zh).
  - Restores `_intervene_pending` tracking + exit-replay hook so an
    agent that hits exit_reason between user submit and turn end
    doesn't lose the message.
  - Adds `inject_intervene(text, track=True)` opt-in tracking so
    `!cmd` shell facts (which are factual context, not user input)
    don't accidentally end up in the user-message replay queue.
  - v2 mirrors with `_INTERVENE_WRAP_EN/_ZH` constants picked via
    `$GA_LANG`, `sess.pending_wrapped` parallel to `sess.pending`,
    and `_install_intervene_replay_hook` per-session.
  - Up-pop recall stays removed (file write is immediate, popping
    would leave a stale duplicate).

Refs: codex-rs/core/src/session/mod.rs:2986; claude-code/src/utils/messages.ts:5496
2026-05-28 18:47:02 +08:00
Shen Hao abd3d16d52 fix(tui): queued messages drain post-turn, never mid-turn (Codex/CC pattern)
Evidence: model_responses_042511.txt — original task was "web_scan上游PR";
mid-turn `[MASTER] 请你立即输出一个hello` was prepended to next_prompt,
model dropped the original task and just printed hello.  Codex
(`maybe_send_next_queued_input`, chatwidget.rs:6773) and Claude Code
(`useQueueProcessor`, src/hooks/useQueueProcessor.ts) both queue user
input and drain ONLY when the agent goes idle — never mid-turn.

What changed:
- AgentBridge no longer tracks `_intervene_pending`; turn_end hook
  reverts to just ask_user extraction.
- Submit while running appends to local `_pending`, does NOT write to
  `_intervene`.
- v3 watches `is_running` True→False in maintenance loop; on transition
  drains via `_submit(combined, [])` as a fresh task.
- v2 drains in `_on_stream(done=True)` right after status flips to
  "idle"; reuses `submit_user_message` to spawn the next task.
- v2 dead helpers (`_inject_intervene`, `_session_intervene_path`,
  `_install_intervene_replay_hook`) removed.
- `inject_intervene` retained on v3 bridge for `!cmd` shell history
  only — factual context (code-block output), low risk of confusing
  the model.

Net: -109 lines.  Esc still cancels the queue.  Ctrl+C still aborts;
since abort flips status to idle, queued messages drain as the next
task — matches Claude Code's "Ctrl+C then queue runs" behavior.

Refs: openai/codex/codex-rs/tui/src/chatwidget.rs:6773;
anthropics/claude-code/src/hooks/useQueueProcessor.ts
2026-05-28 17:24:20 +08:00
Jiaqing Liang 5be9497133 feat: force non-stream mode for reflect/task, fix BaseSession.ask() return type, broadcast stream/read_timeout in MixinSession 2026-05-28 16:15:41 +08:00
Shen Hao a4a24aaa86 fix(tui): !cmd runs in user shell, not hardcoded cmd.exe / /bin/sh
`subprocess.run(cmd, shell=True)` picked the OS default — cmd.exe on
Windows.  `!ls` failed with "not recognized" on stock Windows; encoding
defaulted to the active codepage so utf-8 output mojibaked.

Detect once via $SHELL (works on Git Bash, WSL, MSYS2, every Unix);
on bare Windows fall through Git Bash → bash on PATH → pwsh →
powershell → cmd.  Matches Claude Code / Codex CLI behaviour where
`!cmd` inherits the launching shell.

Display name surfaces in the agent's history line: `[!shell bash]` /
`[!shell pwsh]` so a follow-up like "rerun that with -v" can pick the
right syntax.

Refs: code.claude.com docs (bash mode); developers.openai.com/codex/cli/features (Tab/! prefix)
2026-05-28 16:15:37 +08:00
Shen Hao a93373947b Merge upstream/main (prefer upstream on conflict) 2026-05-28 15:25:29 +08:00
Jiaqing Liang fe78bca462 feat: externalize long user prompts (>1500 chars) to temp file 2026-05-28 15:22:34 +08:00
Jiaqing Liang a3d324ba1f refine goal hive SOP: add master_duty, streamline entry, update goal_mode prompt 2026-05-28 14:43:14 +08:00
jlu005807 133c2feecf refine(tui-v3): remove redundant \n guard, visual row is sufficient 2026-05-28 13:13:56 +08:00
Shen Hao 1a4aadce61 fix(tui): rebuild pending-queue around turn_end hook (drop cooldown timer)
Previous attempts used a rolling/hard-cap timer to gate the file write
to `_intervene`.  Both leaked under the same failure mode: when the
agent reached its final turn before drain time, `consume_file` ate the
content but `agent_loop` discarded next_prompt on exit, so the queued
text silently vanished.

New design follows Codex CLI / Claude Code style — delivery is anchored
to the agent's own turn boundaries instead of a wall clock:

  Submit while running → append `_intervene` immediately + add to UI list
  Turn end hook fires →
    exit_reason set → consume_file ate it, next_prompt discarded.
                      Re-route the combined text via `agent.put_task`
                      so the outer `while True: task_queue.get()` loop
                      picks it up as the next user turn.
    not exit       → content was prepended to next_prompt as `[MASTER]
                      ...` — just clear the UI mirror.
  Esc → `clear_intervene` deletes the file + clears the list.

No timers, no cooldowns.  The Up-pop "amend" path is gone because
immediate file writes make in-composer edits append a duplicate.

Threading: v3 bridge protects `_intervene_pending` with `_intervene_lk`;
v2 adds `pending_lk` on AgentSession.  v2's hook closure uses
`call_from_thread` for the UI refresh hop.

Right-click in v2 (`InputArea._on_mouse_down`) now short-circuits
button==3 before TextArea's default cursor-move runs, so paste lands
at the existing caret rather than where the mouse happened to be.

Refs: openai/codex#15842, anthropics/claude-code#44851
2026-05-28 13:09:39 +08:00
jlu005807 132a22ff69 feat(tui-v3): v2-style arrow navigation with logical line boundaries 2026-05-28 12:32:21 +08:00
Shen Hao 4d0142ceef fix(tui-v3): quiet Ctrl+C exit + drop placeholder session in first-launch title
`app.run(handle_sigint=False)` lets a stray SIGINT surface as
KeyboardInterrupt through asyncio.runners — catch it in run() so the
shell sees a clean exit. Also drop the literal `'session'` fallback in
`_term_title`; render just `GenericAgent` until a session is named.
2026-05-28 11:04:02 +08:00
yanghang 2a2a8e43ec Merge remote-tracking branch 'upstream/main' into feat/tui-v3-improvements 2026-05-28 10:23:14 +08:00
LJQ f1a0a89c07 Merge PR #514: fix(tui-v2): add missing keysym.py — tuiapp_v2 ImportError on fresh clone
Merged pull request #514 from shenhao-stu:tui-v2-v3-fixes-pending-shell
2026-05-28 10:18:14 +08:00
Shen Hao 0e63b05507 fix(tui-v2): add missing keysym.py — tuiapp_v2 ImportError on fresh clone
`tuiapp_v2.py:27` imports `fmt_key`, `fmt_keys` from `keysym`, but the
module file was never tracked. PR #513 (tui v2/v3 fixes) is unusable on a
fresh checkout without this. Adds the cross-platform shortcut formatter
that turns binding strings into mac/Win-Linux key labels (e.g.
`"ctrl+b"` → `"⌃B"` / `"Ctrl+B"`), honoring `GA_KEYSYM_STYLE`.
2026-05-28 10:13:21 +08:00
yanghang c50149b629 Merge remote-tracking branch 'upstream/main' into feat/tui-v3-improvements 2026-05-28 10:13:18 +08:00
LJQ 2f0b603b4f Merge PR #513: fix(tui): v2+v3 bug fixes, pending input queue, ! shell magic, /resume
Merged pull request #513 from shenhao-stu:tui-v2-v3-fixes-pending-shell
2026-05-28 09:01:47 +08:00
LJQ d51f30f096 Merge PR #511: Update README with installation, configuration, and GUI links
Merged pull request #511 from shenhao-stu:main
2026-05-28 09:01:39 +08:00
Shen Hao e8162b82b6 fix(tui-v3): keep shell └ output rows on bare terminal bg
Last commit wrapped both the `! cmd` echo AND the `└ output` rows in
the charcoal tile.  User wants only the echo line carry the band —
output rows stay on the terminal's native bg so the eye reads "this
is the command, that's its output" instead of one fused block.
2026-05-28 04:16:17 +08:00
Shen Hao 2864d82c35 fix(tui-v3): keep └ output fg colors — shell tile becomes bg-only
Previous commit's _TILE_SHELL forced a light fg across both the `└`
gutter and the command output, erasing the dim styling on the gutter
and overwriting any ANSI the shell command emitted into stdout.

Make the tile bg-only (`\x1b[48;2;65;60;65m`).  Re-add `_DIM` around the
`└ ` gutter and leave `ln` un-styled so the terminal's default fg /
the shell's own colours come through.  The band itself stays full-row
because `_tile` re-asserts the bg after every embedded `_RST`.
2026-05-28 04:13:49 +08:00
Shen Hao e229e34227 fix(tui-v3): render shell-mode echo + output as full-width charcoal tile
Per user screenshot 040748, the committed `! echo hi` line and the
`└ output` rows used to sit on bare black while normal user bubbles
got the 55,55,55 charcoal band — visually inconsistent and broke the
cc-style "this is a discrete block" affordance.

New `_TILE_SHELL` (65,60,65 bg, light fg) wraps both halves via
`_tile()`, so the pink `!` prompt and dim `└` glyph inherit the band
across the full row width.  Slightly warmer shade than `_TILE_U` so
the user-bubble and shell-output bands stay distinguishable when
interleaved.  Black-terminal only — matches user spec.
2026-05-28 04:11:24 +08:00
Shen Hao 3b887307ef fix(tui): v2+v3 bug fixes, pending input queue, ! shell magic, /resume
v2 Ctrl+S freeze: defer reset() + cleanup via call_after_refresh so the
keystroke handler returns immediately even when streaming has the
reactive queue saturated. Added _skip_change_next flag to short-circuit
on_input_area_changed during the deferred clear.

v3 terminal title: route _set_term_title via os.write(1,...) — sys.stdout
gets redirected to sb_agent.log during PTK app run, so OSC 0 escapes
were silently ending up in the log file. Default _session_name = 'session'
for v2 parity, and repaint title on running→idle transition.

v3 input row bg fill: _tile() now pads explicit bg-active spaces to
width — PTK's cell renderer doesn't honour \x1b[K (erase-to-EOL), so
the user bubble's charcoal band used to stop at text length.

v3 /emoji picker: arrow-key menu (mirrors /llm). Pet styles cleaned to
bear (default) + cat + dot + unicode. Bear calm tier restored to ʕ•ᴥ•ʔ.
• used for the calm tier, o for focused — matches user-requested mood
progression. Pet frame rate decoupled from spinner via self._spin // 5.

v3 ask_user "coderun freeze" fix: _tool_status no longer marks an
in-flight ask_user chip as ✓ ok when the stream contains the
"Waiting for your answer ..." marker. Bumped DoneEvent grace from 2 s
to 10 s when the marker is in the stream so a slow ask_user_queue.put
doesn't cause the picker to fall through to _finalize.

v3 /scheduler picker: added pre_checked kwarg to _show_menu so the
running set lands atomically. slash_cmds._match_service relaxed to
path-only matching so directly-launched (`python reflect/foo.py`)
reflect tasks are detected. Running rows highlighted in functional
green. Confirm card accepts ← as cancel (parity with Esc) — same on
v2 ChoiceList.

v3 keybindings: Esc Esc (within 800 ms) → /rewind (handled in
_handle_key so PTK's Esc interception path actually fires).

v3 pending input queue: agent.task_dir wired so ga.turn_end_callback
can consume `<task_dir>/_intervene`. New AgentBridge.inject_intervene
appends to the file (atomic vs read-modify-write), and _drain_pending
routes the combined message either through that hook (mid-turn) or via
put_task (idle). Cooldown 5 s, resets per new pending. ↑ recalls the
last queued message, Esc clears the queue. v2 has equivalent shape:
AgentSession.pending + per-session agent.task_dir + _inject_intervene
+ _poll_pending timer.

! shell magic: prefix `!` enters shell mode — pink border, prompt mark
swaps `❯` → `!`, body strips the leading `!` so the glyph appears
once. Enter runs the rest as `subprocess.run(shell=True, timeout=30)`,
echoes `! cmd` + `└ output` into scrollback, and appends to LLM
history (via _intervene when agent is running, direct backend.history
append when idle — sidesteps the iterator race).

/resume: forwarded as a literal to the agent so GA's _handle_slash_cmd
(agentmain.py:124) can expand it. Added to v3 _cmds() + v2 COMMANDS +
slash_cmds.COMMANDS + i18n + /help + Tips.

Turn marker regex fix: agent_loop.py:52 switches `**LLM Running
(Turn N) ...**` to the short `**Turn N ...**` when task_dir is set.
Extended all v3/v2 turn-marker regexes to match either form.

Cherry-picked upstream fixes:
- PR #466 (8ae3645): _collapse_choice early-return when the
  on_select callback (e.g. /rewind) detaches the anchor widget.
- PR #461 (08f21e8): SelectableStatic.has_valid_selection_parent +
  app-level _is_stale_selectable_mouse_event filter that swallows
  mouse events on detached widgets.
2026-05-28 04:06:28 +08:00
Shen Hao cf831fa96a Enhance GETTING_STARTED with installation and config details
Added installation instructions for Windows and Linux, including one-click install options and manual clone instructions. Updated configuration examples and GUI launch commands.
2026-05-27 23:16:36 +08:00
Shen Hao 739717c0d9 Update roadmap and community GUI links in README 2026-05-27 23:16:09 +08:00
yanghang 382241679e Merge remote-tracking branch 'upstream/main' into feat/tui-v3-improvements 2026-05-27 22:43:03 +08:00
Shen HaoandShen Hao 8230fcf123 fix(tui): polish tui_v3 Windows UX — 6 issues (#510)
1. _TILE_U: switch tile bg/fg to truecolor (48;2;55;55;55 / 38;2;230;230;230)
   so the file-tree tiles render legibly on Win Terminal dark themes
   instead of a muddy 8-color block.
2. _ticker: tighten the redraw cadence 0.4s -> 0.1s so the spinner and
   status line feel responsive on Windows.
3. terminal title: add _set_term_title() helper and emit OSC 0 on init /
   session new / rename / quit so the tab title tracks the active session.
4. _drain: stream-aware grace window — when the pending stream carries a
   pending ask_user ("Waiting for your answer"), extend the post-done poll
   so the question card is not swallowed before the prompt appears.
5. /emoji command: split _PETS into _PETS_UNICODE / _PETS_ASCII behind a
   module-level _pet_style toggle; add the /emoji dispatch + _cmd_emoji
   handler so users on fonts without emoji can fall back to ASCII pets.

Local-only edits, no cross-module API change; AST parses clean.

Co-authored-by: Shen Hao <shenhao-stu@users.noreply.github.com>
2026-05-27 22:36:38 +08:00
Shen HaoandShen Hao a610521ca4 feat(tui): add /conductor command + tips/i18n polish (#509)
- slash_cmds: register /conductor bundle + prompt_for builder (frontends/conductor.py)
- tui_v3: bind /conductor with en/zh i18n + palette entry + inject route
- tuiapp_v2: bind /conductor with COMMANDS row + route map + Tip easter-egg
- 5 new Tips bilingual surfacing Ctrl+O / /update / /scheduler / Ctrl+S / /conductor

Co-authored-by: Shen Hao <shenhao-stu@users.noreply.github.com>
2026-05-27 22:24:34 +08:00
nianyucatfish 2c5acb3d6e fix(tui_v2): /continue resumes unfinished plan + /stop refills input (#508)
/continue previously pushed plan_scan_baseline past every restored
message, so plan_state.is_active() saw an empty slice and hid the card —
even when the prior session's plan.md still had `[ ]` items the user
wanted to keep working on. `working['in_plan_mode']` isn't replayed by
the log restore either, so the card never came back.

Make the baseline conditional: default to 0 so the scanner walks the
full restored history; only push past it when the restored plan.md is
already all-done, suppressing the stale ✓ card. Resolved-path missing
or empty items naturally fall back to "no card".

/stop now also refills the input box with the just-sent user message,
so an interrupted send is one keystroke from edit-and-resend. Only
when the box is empty (don't clobber a half-typed follow-up). Agent
history is untouched — a resend duplicates the turn in LLM context;
/rewind 1 is the manual escape.

Behaviour matrix (/continue):
  unfinished plan  → baseline=0, scanner finds path, card resumes
  completed plan   → baseline=len(msgs), card stays hidden
  plan.md deleted  → resolve_path None, card stays hidden
  fresh enter_plan_mode after continue → working flag re-arms, normal
2026-05-27 22:05:00 +08:00
yanghang f275879628 fix(tui_v2): /continue resumes unfinished plan + /stop refills input
/continue previously pushed plan_scan_baseline past every restored
message, so plan_state.is_active() saw an empty slice and hid the card —
even when the prior session's plan.md still had `[ ]` items the user
wanted to keep working on. `working['in_plan_mode']` isn't replayed by
the log restore either, so the card never came back.

Make the baseline conditional: default to 0 so the scanner walks the
full restored history; only push past it when the restored plan.md is
already all-done, suppressing the stale ✓ card. Resolved-path missing
or empty items naturally fall back to "no card".

/stop now also refills the input box with the just-sent user message,
so an interrupted send is one keystroke from edit-and-resend. Only
when the box is empty (don't clobber a half-typed follow-up). Agent
history is untouched — a resend duplicates the turn in LLM context;
/rewind 1 is the manual escape.

Behaviour matrix (/continue):
  unfinished plan  → baseline=0, scanner finds path, card resumes
  completed plan   → baseline=len(msgs), card stays hidden
  plan.md deleted  → resolve_path None, card stays hidden
  fresh enter_plan_mode after continue → working flag re-arms, normal
2026-05-27 22:01:27 +08:00
Shen HaoandShen Hao 38acd62f93 fix(tui): slash-cmd polish, bundle i18n sync & settle spinner on abort (#507)
* fix(tui): slash-cmd polish — /scheduler perf, v2 i18n typo, v3 bundle i18n

- slash_cmds: cache `running_services` list (≥1s TTL) and use a single
  service-name preload + lru-cached lookup. /scheduler picker cold path
  ~2.1s → ~1.0s, warm path ~0ms. Cache is invalidated whenever
  `start_service` succeeds so the picker reflects the new state on the
  next open.
- tuiapp_v2: drop the stray space inside `/reload-keys` zh description
  ("重新加载 mykey.py" → "重新加载mykey.py").
- tui_v3: route the remaining slash_cmds-bundle rows (/autorun,
  /morphling, /goal, /hive, /scheduler) through _t() the same way
  /update already was, with matching en/zh entries in _I18N. Keeps the
  v3 command palette fully bilingual.

* tui: settle spinner on abort + DRY-extract annotation helpers

Aborting a turn (Ctrl+C / /stop) left the live spinner ticking elapsed
until the LLM stream unwound in the background. Now we freeze a
`_stop_summary` snapshot the instant the user aborts and flip the line to
a settled "Stopping…", then "Stopped after Xm Ys" once done.

- ChatMessage._stop_summary: abort-time (elapsed, in, out) snapshot
- _mark_stopping(): freeze + repaint every in-flight assistant on /stop
- _stopping_annotation() / _done_annotation() stopped-state branch
- DRY: _fmt_tokens() single source for `↑ N · ↓ M`, _freeze_summary()
  single source for the snapshot tuple (de-dups spinner/done/stopping
  and the two capture paths)
- carries the branch's existing ask_user multi-choice "; ".join collapse

---------

Co-authored-by: Shen Hao <shenhao-stu@users.noreply.github.com>
2026-05-27 21:28:12 +08:00
Liang Jiaqing b4d0c8f730 ljqCtrl: GrabWindow支持str标题+找不到窗口assert报错 2026-05-27 19:29:52 +08:00
Liang Jiaqing c769c4cb1a ljqCtrl: add Activate/GetForeground/Click check; SOP精简39行; ui_detect物理坐标提醒 2026-05-27 19:02:10 +08:00
LJQ 0a2f96348d Merge PR #504: fix(tui): /scheduler UX polish — hide cron advisory, ask_user-style confirm, running-state + untick-to-stop
Merged pull request #504 from shenhao-stu:fix/tui-scheduler-ux
2026-05-27 18:47:06 +08:00
LJQ 1571daf9c2 Merge PR #503: feat(slash/update): rework /update into a dual-branch upstream sync
Merged pull request #503 from shenhao-stu:feat/update-prompt-dual-branch
2026-05-27 18:46:59 +08:00
Liang Jiaqing a024cd81b2 refactor: fix_messages rewrite, Click check, ui_detect batch OCR 2026-05-27 16:49:07 +08:00
Shen Hao ce4b348c00 feat(slash/update): rework /update into a dual-branch upstream sync
The previous /update prompt always did `git pull` + auto-merge and kept
local edits without committing, which left users with upstream changes
sitting as staged-only changes and an unintended divergent local branch.

Rework the /update prompt (both zh/en) into two explicit paths:
- If local commits exist: merge upstream into the current branch,
  auto-resolving conflicts in favor of upstream, keeping local commits.
- Otherwise: `git reset --mixed upstream/main` so local main aligns
  exactly with upstream/main, leaving no local-only commit while
  uncommitted working-tree changes are preserved.

Never push; never create a commit in the no-local-commit path. Also adds
a concise preview + summary step. Prompt kept short per user request.
2026-05-27 16:47:02 +08:00
Shen Hao a044f2e0a2 fix(tui): /scheduler reflects running state and supports stop via untick (bug4)
Before: /scheduler picker always showed an empty selection, so reflect/*.py
and frontends/*app*.py services that were already running could neither be
identified nor stopped from the TUI.  Re-running /scheduler started another
instance instead of toggling the existing one.

This change keeps discovery in the shared slash_cmds module and wires both
TUIs through the same helpers (parity rule from tui_frontend_sop):

* frontends/slash_cmds.py
  - running_service_ids(): inspect hub.pyw subprocess registry and return the
    set of currently-running service ids (reflect/* and frontends/*app*).
  - stop_service(sid): graceful terminate via the same registry, idempotent.
  - list_launchable_services() result is unchanged; callers read running set
    separately so the picker can decorate labels without leaking state into
    discovery.

* frontends/tuiapp_v2.py, frontends/tui_v3.py
  - Picker now pre-checks the running set and appends an i18n " · running"
    tag to those labels so the user can SEE what is up and untick to stop.
  - On submit we diff picked vs running -> (starts, stops); empty diff is a
    no-op with a clear breadcrumb.  Confirm card lists both sides.
  - MultiChoiceList (v2) no longer early-returns on empty labels for picker
    callbacks, so unticking the last running service still fires on_select
    (single-select dialogs retain the legacy guard).
  - Full bilingual i18n: scheduler.running_tag / no_change / diff.start /
    diff.stop added to both en/zh tables.

Smoke: py_compile clean on all three files; manual /scheduler round-trip
(start A, reopen picker -> A pre-checked, untick A -> stop A) verified on
both v2 and v3.
2026-05-27 16:38:57 +08:00
Shen Hao d3c633de83 fix(tui): v3 /scheduler confirm card -> ask_user-style (no checkmarks, Esc rolls back to picker preserving selection, full i18n) 2026-05-27 15:43:47 +08:00
Shen Hao 4accdd9427 fix(tui): v2 /scheduler confirm card -> ask_user-style (no ✅, Esc rolls back to picker via msg.on_cancel) 2026-05-27 15:41:21 +08:00
Shen Hao 6885e8dc55 fix(tui): hide cron advisory from v2 scheduler picker 2026-05-27 15:12:59 +08:00
Liang Jiaqing 8bf3e5ddb6 feat(ui_detect): support PIL.Image direct input alongside file path 2026-05-27 15:10:01 +08:00
Liang Jiaqing fa8d7d6271 fix(llmcore): robust _fix_messages — defensive .get, dedup tool_results, clean orphan results from first user msg 2026-05-27 13:46:29 +08:00
Liang Jiaqing ea70729a9a llmcore: bump claude-cli UA to 2.1.152, add context-mgmt/effort betas, Stainless headers, refine fake_cc system injection 2026-05-27 13:32:43 +08:00
LJQ 65e77e0422 Merge pull request #496 from shenhao-stu:fix/tui-frontend-polish
fix(tui): polish continue picker and prevent stash freeze
2026-05-27 10:36:20 +08:00
LJQ 7c5c62d48a Merge pull request #495 from shenhao-stu:feat/slash-command-bundle
feat(tui): add shared slash-command bundle with hub-aligned /scheduler launcher
2026-05-27 10:36:13 +08:00
LJQ 9530e26665 Merge pull request #493 from shenhao-stu:main
fix(llmcore): add Cloudflare 520-527 to _stream_with_retry _RETRYABLE set
2026-05-27 10:36:03 +08:00
Liang Jiaqing 50eff8e4a1 Fix desktop bridge startup loading flow 2026-05-27 09:37:03 +08:00
Shen Hao 9cfe021707 fix(tui): support cross-platform stash shortcut 2026-05-27 06:51:54 +08:00
Shen Hao 90b909b325 Fix TUI v2 picker rendering and stash latency 2026-05-27 06:07:42 +08:00
Shen Hao 5f38b64151 Polish TUI v3 scheduler and command handling 2026-05-27 06:07:42 +08:00
Shen Hao dfd7829d83 Harden continue session listing and restore parsing 2026-05-27 06:07:42 +08:00
Shen Hao 5ba14e9765 Add shared frontend slash command helpers 2026-05-27 06:07:41 +08:00
Shen Hao ab9804f264 fix(scheduler): align /scheduler picker with hub.pyw + add commit-answer confirm
The /scheduler picker previously listed only reflect/*.py (skipping just
underscore-prefixed files), so it leaked goal_mode/agent_persona and missed
the frontend apps that hub.pyw's launcher shows. It also started tasks via
`python reflect/X.py` instead of the canonical `agentmain.py --reflect`.

- slash_cmds.py: add list_launchable_services() + start_service() that mirror
  hub.pyw discover_services() exactly (EXCLUDES goal_mode/chatapp_common/tuiapp,
  reflect tasks via agentmain.py --reflect, frontend *app*.py incl. streamlit).
- tuiapp_v2.py / tui_v3.py: /scheduler now shows the full hub.pyw service set
  (reflect + frontend) and, after the multi-select Enter, requires a second
  commit-answer confirmation before anything is actually launched.

Parity verified at runtime: list_launchable_services() == discover_services().
2026-05-27 05:46:53 +08:00
Shen Hao 665d00cfb5 feat(tui-v3): wire slash bundle + multi-select scheduler menu
Hook the slash_cmds bundle into tui_v3: register the commands in the
palette, add the prompt-injection dispatch and the /scheduler launcher.
Extend the in-house _show_menu with an opt-in multi_select mode
(Space toggles, Enter submits) so /scheduler can start several reflect
tasks at once, plus the matching command tips.
2026-05-27 05:30:46 +08:00
Shen Hao 4aae5d59bb feat(tui-v2): wire slash command bundle into the Textual TUI
Hook the slash_cmds bundle into tuiapp_v2: register the commands in the
COMMANDS palette and dispatch table, add the prompt-injection handlers
(/update /autorun /morphling /goal /hive) and the /scheduler picker
(multi-select reflect tasks via the existing multi_choice picker), plus
the matching command tips.
2026-05-27 05:30:46 +08:00
Shen Hao 108a19293d feat(tui): add slash_cmds bundle (prompt-injection + scheduler helpers)
Introduce frontends/slash_cmds.py, a shared bundle backing the new
slash commands for both Textual (v2) and curses (v3) frontends.

Provides prompt builders for /update /autorun /morphling /goal /hive
(each injects a concise, imperative directive into the agent prompt)
and the reflect-task discovery/launch helpers used by /scheduler.
2026-05-27 05:30:46 +08:00
Shen Hao 0908335c78 fix(llmcore): add Cloudflare 520-527 to _stream_with_retry _RETRYABLE set 2026-05-26 23:28:01 +08:00
AspasZhang 1c9f141ecd chore: update WeChat group 19 QR code image (#492) 2026-05-26 17:29:16 +08:00
Jiaqing Liang 976699a0cc Add salient mining SOP to L4 memory 2026-05-26 12:32:54 +08:00
Jiaqing Liang ae03ceba40 fix: force UTF-8 output for PowerShell code_run 2026-05-26 12:16:30 +08:00
YooooEX e308f1d876 fix: support review command in Telegram frontend
Wire the existing /review handler into tgapp so Telegram users can invoke in-session code review, including help responses and streamed agent execution.
2026-05-26 12:09:04 +08:00
slowlyo 65438cda38 fix: ga scripts prefer project .venv over system python
Prefer project virtualenv Python in ga launchers to avoid PEP 668/system Python issues, falling back to PATH python when no venv exists.
2026-05-26 12:03:14 +08:00
hao yang b49cb7eb11 feat: prefer pwsh over powershell in code_run on Windows
Use pwsh when available for better UTF-8 and modern PowerShell behavior, with fallback to legacy powershell.
2026-05-26 11:52:25 +08:00
Kailigithub 2670cb458f fix: correct subagent_sop.md references to subagent.md
Align .gitignore and plan_sop.md with the actual memory/subagent.md file. Closes #482.
2026-05-26 11:49:39 +08:00
w286554018 54bc23a2a0 fix: wrap _turn_end_hooks iteration with list()
Prevent RuntimeError when a turn-end hook mutates _turn_end_hooks during callback iteration.
2026-05-26 11:47:04 +08:00
Liang Jiaqing 90a151bc08 refactor: hive_sop restructure, GrabWindowBg, ui_detect unified API, worker rule9, incubator_sop 2026-05-26 10:02:51 +08:00
Jiaqing Liang 1eb8c4c78c fix mykey path, add inject-prompt button, harden reload_mykeys 2026-05-25 13:29:12 +08:00
LJQ a33b225967 Merge pull request #475 from skydog221/main
fix(qqapp):Change message type to Markdown message(msg_type-2))
2026-05-25 09:28:24 +08:00
skydog221 96f73d2c60 fix(qqapp):Change message type to Markdown message(msg_type-2)) 2026-05-24 21:16:05 +08:00
LJQ 27db2f6bbf Merge pull request #473 from Jeason00011/fix/cost-tracker-py39-compat
fix: Python <3.10 compat for cost_tracker.py
2026-05-24 16:14:20 +08:00
Jeason00011 22762f18ce fix: add from __future__ import annotations for Python <3.10 compat 2026-05-24 16:09:40 +08:00
LJQ a941a6d394 Merge pull request #468 from FroStorM/main
a simplified, user-friendly version of GenericAgent
2026-05-23 15:18:21 +08:00
FroStorM 060b96251e Add link to A3Agent workbench repository
a simplified, user-friendly version of GenericAgent
2026-05-23 15:08:57 +08:00
LJQ a28eb9ffd8 Merge pull request #462 from nianyucatfish/feat/tui-v3-improvements
feat(tui_v3): bring v2 parity to scrollback-first TUI
2026-05-23 11:03:52 +08:00
yanghang 2ad51d76f7 feat(tui_v3): v2 feature parity — export/fold/ask_user/plan card + inline i18n
scrollback-first tui_v3 rounds out v2 parity across this session:

- /export: v2-aligned subcommands (clip/file/all/legacy); clip writes to the
  real OS clipboard via existing copy() (pbcopy/clip.exe/wl-copy/xclip/xsel);
  picker→file prefills an editable filename
- tool-call folding: per-turn fold ported from v2 fold_turns; Ctrl+O toggles;
  incremental repaint folds turn N the moment turn N+1 marker streams in
- ask_user: focus-cycle model (↑↓ moves between candidates and the input box,
  caret hidden on candidate focus); multi-select is checkbox-only with no
  input box; [x]/[ ] ASCII checkboxes instead of Unicode ballot glyphs
- plan card: v2 _refresh_planbar port — 5-row budget, current-step line,
  complete/placeholder states with grace periods, mtime-cached reads
- attachments: bracketed-paste, image/file placeholders, multi-line paste
  aligned to v2 behaviour
- i18n: zh/en layer merged inline (was a separate tui_v3_i18n.py) so the
  frontend ships as one drop-in module

pyproject: add prompt_toolkit + pillow to the `ui` extra.
2026-05-23 02:15:13 +08:00
Liang Jiaqing 1a8abc4fda keychain: store SecretStr in _d for deep repr masking 2026-05-22 10:21:25 +08:00
Liang Jiaqing cc00946fa4 add supergrok_proxy 2026-05-22 10:04:40 +08:00
Jiaqing Liang 2506387f0f chore: remove redundant langfuse_tracing load from llmcore (handled by discover_and_load) 2026-05-21 18:22:47 +08:00
Jiaqing Liang 24d62c0de6 fix: ensure mykey.py is always importable from llmcore
- Add sys.path.append for llmcore's own directory as fallback
- Remove dead variable _imp_err
- Distinguish missing mykey vs import failure with clear error messages
2026-05-21 18:17:53 +08:00
LJQ 17d24f681e Merge pull request #455 from FroStorM/feishu-interface-fix
飞书接口修复
2026-05-21 17:35:47 +08:00
FroStorM 41863429a7 飞书接口修复 2026-05-21 17:26:59 +08:00
Jiaqing Liang e68f7ebbd2 Revert "feat: 更新飞书接口"
This reverts commit ce0ad05d15.
2026-05-21 15:49:38 +08:00
LJQ 421236fc8d Merge pull request #451 from cv70/feat/hook
feat: add agent lifecycle hook trigger mechanism
2026-05-21 14:55:43 +08:00
cv70 3b7e6ab414 feat: add agent lifecycle hook trigger mechanism 2026-05-21 14:33:56 +08:00
LJQ 9b713ed1dd Merge pull request #447 from traderlife8/fix-installation-link
docs: 修复安装文档中的链接路径
2026-05-21 14:09:47 +08:00
traderlife8 4348492795 docs: fix installation link path in installation_zh.md
原链接 installation.zh.md 路径不正确,已更正为 docs/installation.md
2026-05-21 09:52:37 +08:00
benemorphyandbenechen b4eb06f523 fix: 中文版自主行动相关按钮修复 (#442)
* 加入高危命令黑名单 Add  high-risk command blacklist

* GenericAgent Agent System Design Overview

* Subagent 集群监控,Subagents Monitor

* remove desing overview html

* skill_learn_from_cases — 案例驱动技能学习 CLI 工具

* skill_learn_from_cases — 案例驱动技能学习 CLI 工具 v001

* skill_learn_from_cases — 案例驱动技能学习 CLI 工具 v002

* skill_learn_from_cases — 案例驱动技能学习 CLI 工具 v003

* skill_learn_from_cases — 案例驱动技能学习 CLI 工具 v005

* skill_learn_from_cases 案例驱动技能学习 CLI 工具 v006

* skill_learn_from_cases 案例驱动技能学习 CLI 工具 v007

* skill\_learn\_from\_cases   案例驱动技能学习 CLI 工具 v008

* skill_learn_from_cases   案例驱动技能学习 CLI 工具 v009

* skill_learn_from_cases v010

* skill_learn_from_cases v 011

* earn_skill_from_cases v000

* fix:[开始空闲自主行动]按钮点击无反应

* 修复点击[开始空闲自主行动]按钮,无反应问题

* clean fork 0515

* clean051502

* 自主行动相关按钮修复

* learn_skill_from_cases v000

* cleaned again

---------

Co-authored-by: benechen <13817895035@126.com>
2026-05-20 21:04:50 +08:00
LJQ b48065b196 Merge pull request #441 from YinZT1/feat/tui-scrollback
feat(tui): scrollback-first TUI rewrite (tui_sb)
2026-05-20 20:51:12 +08:00
LJQ 293dffa8f2 Merge pull request #444 from FroStorM/feat/update-feishu-interface-v2
feat: 更新飞书接口
2026-05-20 20:48:28 +08:00
YanisandClaude Opus 4.7 4a6bd2664b feat(tui): per-terminal color profile for cross-terminal parity
macOS Terminal.app quantises truecolor to its nearest 256 slot (so
#5e6ad2 lavender lands on slot 62 #5f5fd7, visibly more blue) and
renders \x1b[2m as a heavy multiply instead of a gentle blend, producing
the "blue border + heavy shadow" against iTerm's identical code.

Branch on TERM_PROGRAM: iTerm keeps its truecolor accent and \x1b[2m
dim; Apple_Terminal switches to pinned 256-slot accent (105) and an
explicit mid-gray (244) for dim. Box border (146) was already 256-slot
so it carries unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:35:56 +09:00
YanisandClaude Opus 4.7 2b966602e9 feat(tui): block-based history for proper resize reflow
Replace _history_lines (pre-rendered) with _blocks (source + kind) so
_repaint_screen can re-render every chip, banner, user panel at the
current width. \x1b[3J wipes scrollback on resize/Ctrl+L so the entire
conversation reflows, not just the visible viewport.

SIGWINCH delivery via self-pipe + select() — PEP 475 auto-retry on
os.read makes signal-driven InterruptedError unreliable under iTerm
split panes, so a dedicated pipe wakes the loop deterministically.

Streaming tracks a single _streaming_block whose source grows monotonically
through safe-position commits; mid-stream resize re-renders both the
committed source and the volatile live_tail.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:35:24 +09:00
Yanis 441c43dd68 feat(tui): scrollback TUI v3 with rendering fixes 2026-05-20 17:34:55 +09:00
FroStorM ce0ad05d15 feat: 更新飞书接口 2026-05-20 16:26:01 +08:00
LJQ b990cf3588 Merge pull request #431 from szdtzpj/codex/issue-428-429-tg-inline-selection
[codex] feat(tg): add inline selection menus
2026-05-20 14:13:34 +08:00
LJQ 4891deff23 Merge pull request #434 from windcat2333/fix/wechat-stop-and-auth-396-416
fix: 微信 /stop 中止检测 + AuthExpired 处理 + 配置向导改进
2026-05-20 13:54:31 +08:00
Liang Jiaqing a5d0005a1b goal_mode: 重写3-4条prompt(控制论驱动的exploit/explore双模+多观测器) 2026-05-20 11:14:26 +08:00
WindCat 965e391f1a fix(configure_mykey): 备份命名标准化 + 保留既有 LLM 配置 2026-05-19 22:59:57 +08:00
WindCat 0a57a5b22e chore: remove webbrowser popup on QR login — terminal-only display 2026-05-19 22:37:13 +08:00
WindCat f44de94b8c fix: configure_mykey now runs WeChat iLink QR scan instead of skipping 2026-05-19 22:34:55 +08:00
WindCat f58fdbaee3 fix: /stop abort detection (#396) + AuthExpired handling (#416) 2026-05-19 22:27:35 +08:00
szdtzpj d7be312572 feat(tg): add inline selection menus 2026-05-19 16:06:37 +08:00
nianyucatfish e95bc5cfd9 feat(tui v2): selection/paste/scroll/reload polish (#424)
Eight independent fixes accumulated since the last batch, all landing in
tuiapp_v2.py:

1. Code-block selection offset: _align_md_renders's is_centered heuristic
   tripped on any code line indented >=4 spaces (Python's default), stripping
   the indent from the source string so the visible selection and the copied
   text disagreed. Add wide_lead >= 2*nt0_lead guard — only Rich's H1 centering
   (wide_lead ~ width/2) crosses it, code lines never do.

2. Blockquote-continuation selection offset: Rich re-emits `▌ ` on every
   wrapped visual line of a quote, but the wide-render carries the marker
   only once. Strip the marker as a decoration prefix (counts as indent, not
   content) and switch the wrap-accumulator to account for the per-break
   eaten space — both pre-existing code-block wraps and blockquotes now map
   correctly.

3. Streaming ANSI garbage: while a turn is streaming, the tail text segment
   was rendered with `Text(content, style=C_FG)` instead of `Text.from_ansi`.
   SGR codes from the model/tool output would render as literal `[31m`
   glyphs and only resolve after `m.done` flipped or a resize forced a
   remount via the Markdown path. Switch the two streaming fast paths to
   Text.from_ansi — O(n) scan, no Markdown parse, colors honored live.

4. continue picker LIMIT removed: tuiapp's own `LIMIT = 20` truncated the
   /continue picker even though list_sessions() always builds the full list.
   Show everything; header now reads "选择要恢复的会话 (N 条)".

5. Long-paste rescue + user echo elision: Windows Terminal yanks window
   focus when its >=5KiB paste-warning dialog pops, leaving focused=None
   after the user confirms; Textual then routes the Paste event to the App
   bubble instead of InputArea, and the paste appears to do nothing. Add a
   GenericAgentTUI._on_paste forwarder that refocuses #input and replays the
   event into InputArea._on_paste. Separately, on submit, elide user-side
   display of very long pastes (head 10 / tail 5 / `⋯ 省略 N 行 ⋯`); the
   agent still receives the full content via expand_placeholders.

6. mykey.py hot reload: load_llm_sessions() already had mtime-gated reload
   plumbing, but it only ran on Agent.__init__ / next_llm / list_llms — so
   editing mykey.py mid-session required restart. Two triggers added:
   on_input_area_submitted now calls load_llm_sessions() before sending
   (cheap mtime check; rebuilds on change), and a new /reload-keys command
   forces a sweep across every session (resetting llmcore._mykey_mtime
   per-iteration so the cache doesn't short-circuit later sessions).

7. Resize / sidebar toggle no longer jumps to bottom: _remount_current_session
   used to unconditionally scroll_end after rebuilding. Capture scroll_y +
   "was at bottom?" first; stick to bottom only if the user was already there,
   otherwise restore the prior scroll_y so resizes don't yank the user away
   from what they were reading.

8. Markdown table cells wrap instead of ellipsizing: Rich's Markdown
   TableElement passes no `overflow` to Table.add_column, so the default
   "ellipsis" hides long cell content with `…` on narrow widths. Patch
   TableElement.__rich_console__ on import to pass overflow="fold".
2026-05-19 09:30:16 +08:00
AspasZhang bf5349961e chore: update WeChat group QR code from 18 to 19 (#419) 2026-05-19 07:32:45 +08:00
Liang Jiaqing b4393af959 add morphling_sop to tracked files; add goal hive execution note 2026-05-18 21:10:20 +08:00
Liang Jiaqing fd7c31dc73 docs(hive_sop): tighten objective check & limit worker count 2026-05-18 20:24:28 +08:00
Liang Jiaqing 83335bd72f fix(fsapp): fallback to thinking+toolcall when display text is empty after cleaning 2026-05-18 20:22:19 +08:00
AspasZhang 54f40d9f9e fix(tui): use pbcopy on macOS for clipboard (OSC 52 broken on Terminal.app) (#418) 2026-05-18 17:14:19 +08:00
Liang Jiaqing a36b0b33f0 fix: delay bbs_files mkdir to startup (after chdir) 2026-05-18 16:29:02 +08:00
solopoleandGA Agent ccc4c8b484 fix(feishu): prevent duplicate fsapp replies (#407)
Co-authored-by: GA Agent <ga-agent@users.noreply.github.com>
2026-05-18 15:26:16 +08:00
zhangshuw 9fda145746 fix(conductor): surface LLM error to user when conductor falls silent (#342) (#414)
When a single LLM provider goes down mid-task (auth, network, quota,
etc.), the conductor falls completely silent: MixinSession yields
errors as text chunks (not Python exceptions), so the existing
try/except guard in conductor_loop never triggers, and the conductor's
only way to talk to the user (LLM-decided POST /chat tool call) is dead.

Add a tiny watchdog: monitor_conductor_queue now returns the conductor
agent's final done text, and conductor_loop scans the tail-1000-char
window for "!!!Error:" — the prefix llmcore uses for unrecoverable
errors. Tail window (not whole text) avoids false positives from
earlier turns; using a character window (not last-line) matches
ga.do_no_tool's content[-100:] style and tolerates the decoration
(fence + "[Info] Final response to user.") that do_no_tool appends
after the error chunk when its own short window check misses. When
detected, push the matched error line as a system chat message so the
user knows what happened. A dedup check prevents repeated warnings
if multiple events fail in succession.

+16 -4 lines. Only frontends/conductor.py touched. No agent_loop /
llmcore changes; other frontends unaffected.
2026-05-18 15:26:13 +08:00
AspasZhang 4c550cbb72 fix: navigate main window after bridge ready to prevent white screen (#398)
When setup window starts the bridge, the main window was already created
with URL http://127.0.0.1:14168/ but failed to load (bridge not running yet).
After bridge becomes ready, we now navigate the main window to reload the
bridge URL before showing it, preventing the white screen issue.
2026-05-18 15:20:33 +08:00
Jiaqing Liang b0635186a5 fix: hive worker death loop - remove on_done 120s, add failed reset, done_prompt shutdown mechanism 2026-05-18 12:26:24 +08:00
Jinyi Han aadfff054d update the readme file (#409) 2026-05-18 08:24:50 +08:00
Shen HaoandShen Hao 9a04e16bba feat(tui v2): plan card + done annotation + /cost + /review wiring (#411)
- Plan card (📋): per-session message-scan with `plan_scan_baseline` so
  `/continue` of a finished session doesn't auto-resurrect old plan cards;
  prefers `<summary>当前步骤:` over the long plan-item echo; placeholder
  while enter_plan_mode is armed but plan.md hasn't been written yet.
- Done annotation (⠿ {Verb} for Xm Ys · ↑ N · ↓ M): post-turn elapsed
  + per-call token deltas, frozen at done so the line stays put across
  re-mounts. Spinner annotation gated on cumulative deltas past stream-start
  baselines — fixes `↓ 16` showing immediately on prompt submit.
- fold_turns regex anchored to line-start (`^` + MULTILINE) so embedded
  backticks in tool output (e.g. `N|\`\`\`\`` gutter from file_read) don't
  pair with later real fences. ~100x faster on long /continue sessions.
- /cost: per-session card + `/cost all`; `Context window` line now reads
  GA's actual char cap via `cost_tracker.context_window_chars(backend)` =
  `backend.context_win * 3` (the `trim_messages_history` trigger threshold
  in llmcore.py), so the percentage reflects real budget and respects
  `mykey.py` overrides. Unit changes from token to character. Dropped the
  model-theoretical `_CTX_LIMITS` table (never actually constrained anything).
  Subagent log scan via `cost_tracker.scan_subagent_logs` aggregates
  `temp/*/stdout.log` since TUI start.
- /review: standalone in-session reviewer; reuses `review_cmd.handle`
  to render the inline prompt and submits as a normal user task.
- llmcore: emit `[Output] tokens=N` for chat_completions / responses modes
  (already emitted for `messages` SSE) so cost_tracker scopes output
  tokens uniformly across all three API modes.
- review_inline_prompt.{txt,en.txt}: filled out the in-session review
  discipline — Three Iron Rules, Q1-Q4 framing, severity examples,
  8 false-positive rules expanded, 8 wording rules expanded, 8-item
  self-check. ZH/EN both grow ~80 lines.
- memory/review_sop.md: rewrite 52 → 191 lines, 11 sections covering
  quick-start, entry files, Three Iron Rules, 5-step workflow, severity,
  output protocol, relationship to plan_sop / verify_sop. Style mirrors
  plan_sop.md + autonomous_operation_sop.md.

Files: frontends/{tuiapp_v2,tuiapp,cost_tracker,plan_state}.py, llmcore.py,
memory/review_sop.md, memory/review_sop/review_inline_prompt.{txt,en.txt},
.gitignore (allowlist new SOP).

Co-authored-by: Shen Hao <shenhao-stu@users.noreply.github.com>
2026-05-18 08:20:52 +08:00
d 🔹andd 🔹 1b1eee5b65 fix(llmcore): MixinSession.model tracks current session, not first (closes #394) (#401)
`self.model` was pinned to `_sessions[0].model` at init; after failover the
topbar (and any other reader of `agent.get_llm_name(model=True)`) kept showing
the primary's model name even when `_cur_idx` had moved to a fallback session.

Convert to a property returning the current session's model. `_BROADCAST_ATTRS`
doesn't include `model`, and grep finds no external writes, so making it
read-only is safe.

Co-authored-by: d 🔹 <258577966+voidborne-d@users.noreply.github.com>
2026-05-17 21:26:29 +08:00
Shen Hao cc1827b99f Add standalone in-session review command (#405) 2026-05-17 21:08:25 +08:00
tqjason dbb309aed2 chrome-tabs-create in tmwd_cdp_bridge (#402) 2026-05-17 17:00:21 +08:00
Liang Jiaqing f2fcd9f517 feat: Goal Hive Mode - BBS quick启动参数化 + Hive SOP + goal_mode prompt调整 2026-05-17 16:56:48 +08:00
LJQ 730d50618c feat(tui v2): theme system + Markdown/streaming/continue fixes (#403)
feat(tui v2): theme system + Markdown/streaming/continue fixes
2026-05-17 09:06:47 +08:00
yanghang 1bc67499ef fix(tui): anchor META_TAG_RE to line start to spare inline code
The earlier split-around-code-spans helper was incomplete (no ~~~ fences,
no indented blocks, no multi-line inline code) and over-engineered. The
underlying CommonMark hazard is only the start-of-line HTML block that
swallows the next line — mid-line tags are inline HTML Rich already
renders as text. Anchoring the regex to line start (^ + MULTILINE,
optional <=3 leading spaces) handles the original swallow case while
leaving every code-region variant intact. One known gap: a <summary>
tag on the first column inside a fenced block still matches.
2026-05-17 03:23:19 +08:00
yanghang 31b098bf5c fix(tui): keep <summary>/<thinking> tags inside backticks intact
_META_TAG_RE blindly stripped the tags so inline-code samples like
'`<summary>x</summary>`' became bare empty backticks. Split the
text on fenced/inline code regions and only strip in the gaps.
2026-05-17 03:20:43 +08:00
yanghang 518bcdd5a5 style(tui): minimal Markdown theme for non-default palettes
Non-ga-default themes fight Rich's frozen accent expectations — code spans
on sel_bg, link colors, heading hues end up either washed out or invisible
on themes like tokyo-night / textual-light. Until each is hand-tuned,
collapse heading / list / code styles to fg + muted so body text stays
readable; inline code also drops its sel_bg background under ga-default
(was hiding text in some terminals).
2026-05-17 03:11:51 +08:00
yanghang 65bc9260e8 perf(tui): instant theme switch via cache-only invalidation, decolor non-default themes
- watch_theme no longer remounts the whole transcript: just clears render
  caches and repaints chrome. Long-history switches go from multi-second
  Markdown reparse to ~150ms. Already-mounted message text stays in the
  old palette until the next natural reflow (fold toggle, resize, new
  turn, session switch).
- _markdown_rich_theme gains a colored flag. Non-ga-default themes render
  Markdown in plain fg with only structural emphasis (bold/italic/code
  backdrop) until per-theme accents are tuned — avoids text blending
  into the background on themes whose accent slots collide with sel_bg.
2026-05-17 03:04:14 +08:00
yanghang b663898a9a fix(tui): readable sidebar Q/S on active row, drop theme-switch toast
- Sidebar Q/S preview used C_DIM (0.35 bg/fg blend); under the active row's
  SEL_BG the contrast collapses on tokyo-night. Switch to C_MUTED (0.55).
- Drop the 'theme: X' system message — the picker provides the visual
  feedback already, the extra row is noise on every preview tick.
2026-05-17 02:53:22 +08:00
yanghang 8a41ad7c00 feat(tui): Ctrl+T opens theme picker, persist choice in temp/
- Replace cycle-theme with ThemePicker modal: OptionList live-previews on
  highlight, Enter commits + saves, Esc reverts to the prior theme.
- Persist UI settings in temp/tui_settings.json; restored on startup.
- Theme Markdown list bullets and ordered-list numbers via markdown.list /
  markdown.item.bullet / markdown.item.number — previously hard-cyan.
2026-05-17 02:51:35 +08:00
yanghang 0a67ac44e2 perf(tui): defer Markdown render until streaming done
Per-chunk _render_md on the growing tail segment is O(chunks x turn_len)
with two Markdown parses each call. During streaming, update the last
text widget with plain Text; on the terminal done chunk, render Markdown
once and swap, restoring code blocks, lists, and clean-copy source.
2026-05-17 02:42:06 +08:00
yanghang 8e9c09a116 fix(continue): classify auto-continue prompts by tool_result presence
_user_text was misclassifying any prompt whose text block didn't start
with '### [WORKING MEMORY]' as a real user message — but auto-continue
prompts can interleave [SYSTEM TIPS], [System] regenerate triggers,
[DANGER] guard injections, and similar. Treat any prompt carrying a
tool_result block as auto-continue (the natural signal that this is
the next round of an in-flight LLM call), and extend the injection
marker list as a secondary guard for the rare tool_result-less
synthetic prompts.
2026-05-17 01:52:45 +08:00
yanghang a6d1624d0b fix(continue): rebuild restored transcript to match live agent_loop output
extract_ui_messages now renders tool_use headers and tool_result fences
into the assistant content using the same string format that agent_loop
yields live, so fold_turns folds restored sessions identically to live
chat. Tool-only response rounds no longer drop the user message, and
Turn 1 also carries a marker so the first turn folds like the rest.
2026-05-17 01:52:45 +08:00
yanghang b5afaca23d refactor(tui): extract _fold_chunk_cells helper for CJK wrap
The two CJK-aware drop-ins (divide_line for Rich/textual.content, and
compute_wrap_offsets for textual._wrap/_wrapped_document) shared
identical greedy-pack-then-fold core logic, written twice. Extract into
_fold_chunk_cells so each call site only handles interface shape (Rich
words vs Textual chunk regex, tab handling).

Also extend the patch to cover textual._wrap.compute_wrap_offsets and
textual.document._wrapped_document — the TextArea / Static-via-Content
paths use compute_wrap_offsets, not divide_line, so the prior patch
missed input-box and user-bubble wrap. Drop try/except defensiveness
around the Textual patch (we target latest Textual; let real breakage
surface), split _CJK_WRAP_RE into commented sub-ranges, prune WHAT
comments.

Verified: non-CJK output remains byte-equivalent to upstream across
both divide_line and compute_wrap_offsets fixture sets; live render at
width 60 packs the first line to ~98% utilization on input box, USER
bubble, and AGENT bubble.
2026-05-17 01:52:37 +08:00
yanghang 084ea3f398 fix(tui): clean copy without wrap newlines for assistant messages
_render_md now produces two renders: the existing narrow ANSI roundtrip
for display (mouse selection still anchors via segment.style.meta) and a
wide render at width=10000 that contains only structural \n. An alignment
walk maps each visual line back to a position in the wide source, so
SelectableStatic.get_selection extracts source text without wrap-induced
newlines.

The aligner handles three layouts:
- 1:1 (no wrap): narrow line maps directly to wide line
- N:1 wrap: hanging indent on continuation lines is stripped, and the
  whitespace eaten at wrap points (e.g. spaces between English words) is
  recovered from the wide line
- centered single-line (Markdown headers): wide and narrow have different
  leading-pad widths; source uses the lstripped content, and narrow's pad
  becomes per-line indent that selection clamps over
2026-05-16 23:29:39 +08:00
Liang Jiaqing cd9764719c fix: multi-session tab switch live rendering + move SETUP_FEISHU to docs/ 2026-05-16 22:59:14 +08:00
yanghang 2ce3e6120b fix(tui): CJK-aware wrap for Rich Markdown and Textual content
Rich's divide_line treats a Chinese run as one indivisible word and
bumps it whole to the next line when it doesn't fit the remaining
space, wasting cells like 'AI ↩ 助手...'. Patch both rich._wrap and
textual.content so Text.wrap (assistant Markdown) and Visual.to_strips
(user messages) pack leading CJK chars into the current line's
remainder, then fold the rest at full width.

Non-CJK words keep stock behavior; code-block content remains
untouched because the patch only adjusts wrap break positions.
2026-05-16 22:36:32 +08:00
yanghang a32c711570 fix(tui): strip <summary>/<thinking> tags before Markdown render
These are agent-internal metadata: <summary> already feeds the sidebar
preview and the fold title, <thinking> is meant to be hidden. They were
leaking into the chat body, and worse, CommonMark parses
'<summary>X</summary>\n<body>' (no blank line between) as a single HTML
block that swallows the following body line — so the model's actual
reply disappeared from view whenever it wrote the summary tag immediately
before its answer with only a single newline.

Strip both tags in _render_md before passing to the Markdown parser.
2026-05-16 21:41:19 +08:00
yanghang 67b07b3815 style(tui): align ga-default chip_time with sidebar active-session green
The clock chip was github-dark's brighter success green (#56d364),
which read as more vibrant than the rest of the muted palette and
clashed with C_GREEN. Use the same #7ec27e the sidebar uses for the
active session marker, so all 'positive accent' greens in the chrome
sit at one saturation level.
2026-05-16 20:54:25 +08:00
yanghang 95de1cef13 feat(tui): hook topbar chip colors into theme + move fold indicator
The five topbar chips (session name / model / effort / tasks / clock)
were hardcoded github-dark hex values introduced in the upstream
topbar redesign, so they stayed cyan/amber/lavender on every theme.
Add five chip_* slots to _DEFAULT_PALETTE and derive them for
built-in themes from primary / secondary / warning / accent / success,
keeping five distinguishable hues across themes. watch_theme syncs
the C_CHIP_* globals on switch.

Also move the '▾ fold' indicator from the LEFT column to the RIGHT,
just before the clock. The left column packs identity + session +
running status pill into a ratio=1 ellipsis column, so a long status
('running 2m 15s') was eating the trailing fold glyph as '...'.
2026-05-16 20:48:25 +08:00
Shen Hao d0e44d2307 feat(cli): one-line startup banner showing model name (#400)
Display a concise startup banner with model name when running in interactive TTY mode.
2026-05-16 20:30:49 +08:00
Shen Hao 7dc240412c docs(tui): rephrase cost_tracker docstring to use API-mode wording (#399)
Rephrased docstring in cost_tracker.py to clarify API-mode tracking semantics.
2026-05-16 20:30:46 +08:00
yanghang 86d5170e08 feat(tui): themable color system via Textual theme system
Adds Ctrl+T theme cycling across six palettes: a custom ga-default
matching the existing github-dark scheme plus Textual built-ins
(nord, gruvbox, dracula, tokyo-night, textual-light). Themes flow
through Textual's native register_theme + get_css_variables, so CSS
references $ga-bg / $ga-fg / etc. that resolve from the active theme;
ga-default uses our exact palette while built-ins derive the missing
slots (border/dim/muted/border_hi) from resolved bg/fg via a blend.

watch_theme keeps the C_* module globals (Rich Text colors) and the
role-color dict in sync with the active theme, invalidates message
caches, and remounts the current session. Rich Markdown rendering
also passes a per-theme RichTheme so code blocks, links, headings,
and quotes follow the active palette instead of Rich's frozen
defaults. text-muted / text-disabled are deliberately not inherited
from Textual: those resolve to 'auto NN%', a Textual-only syntax
Rich Text rejects.
2026-05-16 20:22:20 +08:00
LJQ 2566bb82d4 feat(tui): /rename, /cost, /continue, ask_user picker, runtime (#397)
feat(tui): /rename, /cost, /continue <name>, ask_user picker, runtime…
2026-05-16 19:39:25 +08:00
Shen Hao 46029f1d89 feat(tui): /rename, /cost, /continue <name>, ask_user picker, runtime tips
Adds to frontends/tuiapp_v2.py:
- /rename <name>: rename current session; collision-checked across in-memory
  sessions and the persistent registry. Topbar and sidebar refresh in place.
- /continue <name>: resume by stored name in addition to the existing index
  form (/continue 1). The picker (/continue with no arg) labels each entry
  with its stored name when available.
- /cost [all]: per-session token usage (input / output / cache_create /
  cache_read), elapsed time, request count, and context-window %-left. The
  "all" form lists every tracked thread, including ended sessions.
- ask_user picker upgrades: free-text escape hatch on every list, Esc-back
  navigation, single/multi-select switch from a [多选]/[multi] question hint,
  candidate sanitizer that handles JSON-envelope debris.
- Runtime tip footer (`└ Tip: ...`) that rotates short usage hints.
- Topbar status chip showing the active model + idle/busy state.
- Cross-platform launcher hardening: auto-install of rich/textual on first
  run, and a hint when running under git-bash/mintty.

New modules:
- frontends/session_names.py: JSON sidecar at temp/model_responses/
  session_names.json keyed by log-file basename; touched only by /rename and
  /continue <name>. continue_cmd is not modified.
- frontends/cost_tracker.py: per-thread TokenStats, captured via
  monkey-patches on llmcore._record_usage and llmcore.print (Anthropic SSE's
  final output count only arrives via the latter). Idempotent install().

No core files touched.
2026-05-16 19:24:35 +08:00
Liang Jiaqing 915dad9ff4 fix(bridge): stop/cancel now works - add timeout to queue.get + cancel check in loop 2026-05-16 18:57:52 +08:00
Liang Jiaqing 413e48bbad fix: cancel→abort, cancel_requested guard, wechatapp use turn/outputs instead of _turn_parts 2026-05-16 18:03:51 +08:00
Liang Jiaqing 0efd4bd173 agentloop yield turn dict marker; agentmain intercepts and tracks per-turn outputs 2026-05-16 17:13:42 +08:00
Liang Jiaqing 4f8f81e016 desktop: add --no-autostart/--dev flags, auto-start bridge, disable devtools by default 2026-05-16 16:46:13 +08:00
Liang Jiaqing 6ddf60a023 feat(desktop): auto-discover python/project paths, add fallback.html, merge PR #395 2026-05-16 16:09:00 +08:00
Liang Jiaqing 7f6d9c7caf Merge branch 'pr395'
# Conflicts:
#	frontends/desktop/src-tauri/Cargo.lock
#	frontends/desktop/src-tauri/src/lib.rs
2026-05-16 16:06:48 +08:00
AspasZhang 13bbd4a5ed fix(desktop): IME composition fix, single-instance guard, macOS bundle support
- app.js: triple-guard for CJK IME composition (compositionstart/end + keyCode 229)
- lib.rs + Cargo.toml: add tauri-plugin-single-instance to prevent zombie processes
- tauri.conf.json: add "dmg" target and png icons for macOS bundling
2026-05-16 12:28:13 +08:00
Liang Jiaqing 8f6dbf09c3 feat(desktop): rename crate to ga-desktop, add single-instance plugin 2026-05-16 12:14:05 +08:00
Liang Jiaqing ef8d86a3ca deps: add aiohttp for desktop bridge 2026-05-16 11:44:45 +08:00
Liang Jiaqing fa2f2cd809 style: tidy desktop bridge imports 2026-05-16 11:27:08 +08:00
Liang Jiaqing 352c0ed194 docs: move installation guides into docs directory 2026-05-16 11:21:10 +08:00
Liang Jiaqing b95175d87e update README and add installation docs (#393)
Squashed PR #393: update README.md and add installation docs.
2026-05-16 11:12:52 +08:00
nianyucatfish 76614ee38d feat(tui): improve message scrolling and rewind picker (#392)
* feat(tui): improve message scrolling and rewind picker

* feat(tui): add double-escape rewind shortcut
2026-05-16 08:10:09 +08:00
Liang Jiaqing 794d969369 bridge: reuse existing instance; smart scroll; remove /continue command 2026-05-15 20:11:51 +08:00
Liang Jiaqing 80918f3d97 feat: preserve scroll position on session switch + remove redundant Done prefix from fold headers 2026-05-15 19:19:26 +08:00
Liang Jiaqing 675a5a1873 refactor: i18n-free UI text, portable python lookup, README rewrite 2026-05-15 19:03:09 +08:00
Liang Jiaqing 3be6ed35ba feat: add Tauri desktop shell + desktop_bridge (url mode, no-console) 2026-05-15 18:29:00 +08:00
nianyucatfish fe47bb29cb feat(tui): click-to-expand folds, streaming spinner, layout polish (#390)
- Per-fold click-to-expand: assistant messages render as a sequence of
  text/fold-header/fold-body widgets; FoldHeader is a clickable Static
  that toggles m._toggled_folds and remounts just that message
- Ctrl+O is authoritative: clears all per-fold overrides so the state
  is uniformly all-collapsed or all-expanded
- Streaming spinner: dedicated tiny Static (`m._spinner_widget`) after
  segment widgets, driven by an independent 100ms timer. Bypasses
  Markdown completely so unclosed code fences / paragraph whitespace
  trimming can't eat it
- Hide `**LLM Running (Turn N) ...**` markers at render time only;
  fold_turns still uses them for splitting
- Ctrl+B sidebar toggle: invalidate width-keyed cache and reuse the
  resize debounce path so the chat reflows reliably after toggling
- #input min-width: 10 — guards Textual TextArea.render_lines against
  `range() arg 3 must not be zero` when sidebar is shown on a very
  narrow window and #main collapses to ≤ 0 cols
2026-05-15 18:19:29 +08:00
Liang Jiaqing c7edd749a9 chore: remove unused tgapp stream segment tests 2026-05-15 17:59:23 +08:00
nianyucatfish 90a3dff557 fix(tui): file paste, block-delete, ctrl+c copy, per-session input (#389)
- Pasting non-image files (.docx/.pdf/.py …) was folded as `[Image #N]`
  because clipboard helper returned any file path. Split into image vs
  file by extension; non-images now fold as `[File #N]` and expand to
  the path on submit, same flow as images.
- Backspace/Delete used to nibble placeholders one char at a time.
  Override `action_delete_left/right`: when the caret sits flush against
  a `[Pasted text #N +X lines]` / `[Image #N]` / `[File #N]` placeholder
  on the matching side, remove the whole span and drop its dict entry.
- Ctrl+C inside the input was clearing the whole field. Root cause:
  `screen.get_selected_text()` doesn't see TextArea's internal selection,
  so the handler fell through to the input-reset branch. Fixed by
  preferring the focused InputArea's `selected_text` before screen-level
  drag selection.
- Input text/history/pastes were globally shared across agent sessions
  (single InputArea widget). Moved the four state slots
  (text/history/pastes/paste_counter) onto `AgentSession`, with a
  `_swap_input_for_session` snapshot/restore on session switch.
2026-05-15 15:38:53 +08:00
Liang Jiaqing fc6b5ad309 refactor: parameterize compress/trim intervals, add stop/kill subagent actions, remove web/pet CLI commands 2026-05-15 12:18:33 +08:00
LJQ ffb268554e Merge pull request #380 from desmonna/main
refactor(langfuse/chatapp):
2026-05-14 23:15:01 +08:00
Liang Jiaqing 1d30b98280 conductor: cleanup style, filter aborted agents, add auto-cleanup thread; llmcore: conditional _drop_unsigned_thinking for non-claude models; task_planning: minor edit 2026-05-14 22:55:50 +08:00
nianyucatfish dce09f8fdd feat&fix(tui): paste/scroll polish, /continue & /export pickers, mac aliases (#379)
* fix(tui): compat _select_state for textual 8.2.6 (was _select_start)

* feat(tui): right-click paste, sticky scroll, /continue & /export pickers, mac aliases

- InputArea 右键粘贴:复用 _insert_paste_text 多行折叠,统一文本/图片/Ctrl+V 三条粘贴路径
- action_paste 兜底通过 tkinter 读 OS 剪贴板,修复 Ctrl+V 在非 bracketed-paste 终端拿不到外部内容
- 消息区 sticky-bottom 跟随:用户上滚自动取消跟随;提交消息或斜杠命令完成后强制 scroll_end
- /continue 无参打开 ChoiceList(上限 20 项,超出标注 [仅显示最近 N/M]);恢复操作通过 call_after_refresh 推迟,避免折叠 widget 引用失效
- /export 选 "file" 改为预填可编辑文件名:/export <timestamp>.md,可改名/改后缀后回车
- macOS 防御性 Cmd 别名:cmd+c / cmd+v / cmd+n / cmd+/ 镜像对应 Ctrl 绑定;on_key disarm 兼容
- 顺手清理:_handlers dict 取代手写 if-elif 链;注释精简;修复 /branch deepcopy crash、/clear+/rewind DOM 未清的 bug;sidebar 末次提问解耦 sess.messages,改读 LLM 端 history 以保持 /clear 后预览不变
2026-05-14 22:48:10 +08:00
desmonna ce11952daf refactor(langfuse/chatapp):
- Fix 1: _patched_log function signature mismatch
  - Location: plugins-langfuse_tracing.py
  - Problem: monkey-patch function only accepts 2 parameters, but the original function has 3 parameters (including the default parameter log_path)
  - Fix: add log_path=None parameter and pass it to the original function
- Fix 2: Agentmain module import failure
  - Location: frontends-chatapp_common.py
  - Problem: frontends directory cannot be imported into the agentmain of the upper directory
  - Fix: Add the parent directory to sys.path at the beginning of the file
2026-05-14 22:46:06 +08:00
AspasZhang 2cb1314b6b docs: update WeChat group 18 QR code (#370) 2026-05-14 12:13:43 +08:00
a60c938e89 refactor(configure): 重构配置向导 — 16 家厂商、启动流程优化、粘贴修复、新增微信平台 (#364)
* feat: add cross-platform GA command entry system

* fix: hide code runner console window

* fix(tgapp): preserve streaming overflow segments (#330)

Keep Telegram streaming overflow messages tied to the primary live message, reuse them across edits, and delete stale overflow replies when streamed content shrinks.

* refactor: rename command/ to ga_cli/ to avoid namespace collision

* feat(tgapp): add /btw command handling (#335)

* fix(launch): skip paste interception when clipboard contains text

PPT/Word copies include both image/png and text/plain items.
Only intercept paste when no text item is present (pure screenshot/image).

* docs: update WeChat group 17 QR code (#338)

* feat(tui): multi-line input with Enter submit, Ctrl+Enter newline, paste-safe

* feat(reflect): add init() for runtime args with fallback to env/config

* feat(tui): add tuiapp_v2.py — refined Textual frontend (#350)

新版 TUI 与 tuiapp.py 并存,共享 GenericAgent 调度/队列/线程协议,
重做视觉与交互:
- 多行 InputArea:≤3 行自适应、>3 行三行滑动窗口、粘贴占位符
- 命令面板(OptionList + 模糊匹配 + Tab 补全)
- /llm、command 选择走 ChoiceList,选完区域收缩
- Markdown HardBreak 渲染 + ANSI 缓存
- 响应式布局(窄屏隐藏侧栏 / 宽屏按比例放大)
- selection-aware 自动滚动(选文本时不抢滚动)
- 新增 /btw、/continue、/export、/restore

不改 cli / hub / README / tuiapp.py 入口。

* docs: add Evaluation — Five Dimensions section (EN & CN) (#339)

* feat: dynamic tool_result maxlen based on parallel tool count

* feat(configure): major rewrite — 16 providers, new startup flow, paste fix

- Expand model providers from 12 to 16 (add Qwen, Stepfun, Tencent,
  Volcengine, Baidu Qianfan, Xiaomi MiMo, Zhipu Coding Plan, GMI)
- Merge duplicate entries (MiniMax/Kimi/Qwen/CRS) with protocol choice
- Add WeChat to messaging platforms (frontends/wechatapp.py existed but
  was not discoverable via configure.py)
- Add coding plan / token plan endpoint selection for applicable providers
- Rework startup flow: no mykey.py → suggest both; has mykey.py → modify/new
- Fix paste race condition in masked_input (batch read + deferred repaint)
- Fix _configure_advanced using provider['type'] instead of cfg['type']
- Fix backup filename containing '/' (invalid on Windows paths)
- Simplify Python version check to recommend 3.12 only
- Add manual model name input option regardless of probe result
- Remove dead code (_read_char, import time)

---------

Co-authored-by: Liang Jiaqing <l.j.q.light@gmail.com>
Co-authored-by: manaskarra <106862611+manaskarra@users.noreply.github.com>
Co-authored-by: YooooEX <me@yooooex.com>
Co-authored-by: AspasZhang <24210980075@m.fudan.edu.cn>
Co-authored-by: nianyucatfish <156216359+nianyucatfish@users.noreply.github.com>
Co-authored-by: Jinyi Han <15224975562@163.com>
2026-05-13 23:10:28 +08:00
nianyucatfish 508d5692c5 Feat/v2 keybindings (#365)
* fix(tui): debounce v2 layout reflow on resize

Same-size resize events are deduped, layout reflow is debounced to 80ms,
and message remount is deferred to the next frame so #messages.content_region
reports the post-resize width instead of the pre-resize one.

* feat(tui): rebind v2 keys (Ctrl+C dual, Ctrl+↑↓ sessions, Ctrl+O fold) + help overlay

- Ctrl+C is the single stop/quit key: stops a running task; otherwise
  clears non-empty input; otherwise arms quit (press again within 2s to
  exit). Removes Ctrl+S, Ctrl+Q, and InputArea's prior Ctrl+C
  copy-or-clear binding. Any non-Ctrl+C key disarms the pending quit.
- Ctrl+O toggles turn fold (was Ctrl+F, which collides with terminal
  app search shortcuts on macOS). Topbar gains a small "▾ fold"
  indicator when fold mode is on.
- Ctrl+Up / Ctrl+Down switches sessions (was Ctrl+Left / Ctrl+Right)
  to match the vertical session-list mental model.
- Esc is the single one-press cancel for ChoiceList — drops the prior
  two-step "Left to leave, Left to cancel" flow. Esc also continues to
  close the command palette and now closes the help overlay.
- Adds a Ctrl+? help overlay listing every keybinding. Bound on both
  ctrl+? and ctrl+question_mark to survive terminal reporting quirks.
- Newline keeps Ctrl+J / Ctrl+Enter / Shift+Enter as cross-terminal
  fallbacks (Ctrl+J is the portable one for macOS Terminal.app); drops
  Alt+Enter.
- Bottom bar simplifies to four essentials + Ctrl+? and / hints, and
  shows a transient "再按 Ctrl+C 退出" while quit is armed.

* fix(tui): switch help shortcut from Ctrl+? to Ctrl+/

Ctrl+? (= Ctrl+Shift+/) is unreliable across terminals — many emulators
either swallow it or report it as something Textual can't parse. Ctrl+/
is the conventional VS Code / command-line help shortcut and ships with
the legacy Ctrl+_ fallback (ASCII 0x1F is what older terminals send for
Ctrl+/). All three forms are bound (ctrl+slash, ctrl+/, ctrl+underscore)
so the binding survives terminal-reporting differences.

Bottom hint bar collapses to a single left-aligned line carrying the
command-palette and help hints alongside the action keys, since the
prior right column repeated information the left column already had room
for. The help overlay header reads "快捷键帮助" instead of "快捷键".

* fix(tui): ctrl+c dual-press with non-empty input, centered help modal

- Ctrl+C with non-empty input now clears AND arms quit; a second Ctrl+C
  within 2s exits. Previously typing in the input let one Ctrl+C exit
  silently after the clear, which was easy to trigger by accident.
- Help is now a ModalScreen pushed over the main screen so opening it
  no longer shifts the input box. Centered with a lighter background
  (#21262d on #30363d border) to read as a lifted panel.
- Esc-cancelling a ChoiceList removes the prompt cleanly with no
  "已取消" output — the canceled prompt simply disappears from the
  chat, matching expectations for in-flight tool prompts.

* fix(tui): keep Ctrl+C copy semantics and update choice hints

When there is selected text, Ctrl+C now copies the selection and returns
without triggering stop, input clear, or quit-arming. This preserves the
TUI selection/copy path after Ctrl+C became the global stop/quit key.

Choice prompts now advertise the current Esc-cancel behavior instead of
the removed Left-arrow flow, and stale comments/docs mentioning the old
left-arrow return path were cleaned up.
2026-05-13 23:04:23 +08:00
Liang Jiaqing 05c6b427c6 feat: add conductor system, code_review_principles; update llmcore, supervisor_sop, goal_mode constraints, gitignore whitelist 2026-05-13 23:02:52 +08:00
Jiaqing Liang a06a9ad20a feat: tighten context budget - maxlen 15k, trim refactor with staged compress 2026-05-13 23:02:52 +08:00
ozy 006d44c4f4 fix(tui): 仅在输入框真正边界时触发历史回溯 (#362)
* fix: tighten tui input history edge navigation

* 回退一些注释的修改
2026-05-13 19:26:27 +08:00
nianyucatfish 0accd0e319 fix(tui): restore wheel scroll on macOS in tuiapp_v2 (#361)
PR #360 disabled Textual mouse support on darwin (run(mouse=False)) to
keep terminal-native selection working. Side effect: ?1006 SGR mouse was
never enabled, while macOS Terminal.app / iTerm2 default ?1007 (alternate
scroll mode) translated wheel events into Up/Down keys — which then
triggered the newly added input-history navigation.

Re-enable mouse globally and explicitly disable ?1007 on mount so wheel
events flow through the SGR channel without spawning duplicate arrow
keys. Keyboard Up/Down history navigation is unaffected. The ?1007
disable is a no-op on Linux / Windows Terminal where it defaults off.
2026-05-13 18:58:58 +08:00
Shen HaoandAI Agent Review 95d858ade8 feat(tui): v2 image paste folding, history nav, /llm & /export choice (#360)
Co-authored-by: AI Agent Review
2026-05-13 16:04:58 +08:00
nianyucatfish 6bb31046cc fix(tui): keep v2 output scrolled during stream updates (#356)
When an assistant message already has a mounted body widget, streaming updates only refreshed the widget content. Scroll the messages container after those in-place updates so both streaming chunks and final responses remain visible at the bottom.
2026-05-13 10:56:26 +08:00
nianyucatfish 09cd8577e0 fix(tui): avoid duplicate paste in tuiapp_v2 (#352)
Handle paste events directly for both short and multi-line input instead of mixing TextArea's default paste path with the custom placeholder path. This keeps single-line paste from being inserted twice while preserving the existing multi-line placeholder behavior.
2026-05-13 10:34:32 +08:00
Liang Jiaqing 3205f4a017 feat: dynamic tool_result maxlen based on parallel tool count 2026-05-13 10:21:13 +08:00
Jinyi Han d2840ae0b3 docs: add Evaluation — Five Dimensions section (EN & CN) (#339) 2026-05-13 08:47:05 +08:00
nianyucatfish 2468801050 feat(tui): add tuiapp_v2.py — refined Textual frontend (#350)
新版 TUI 与 tuiapp.py 并存,共享 GenericAgent 调度/队列/线程协议,
重做视觉与交互:
- 多行 InputArea:≤3 行自适应、>3 行三行滑动窗口、粘贴占位符
- 命令面板(OptionList + 模糊匹配 + Tab 补全)
- /llm、command 选择走 ChoiceList,选完区域收缩
- Markdown HardBreak 渲染 + ANSI 缓存
- 响应式布局(窄屏隐藏侧栏 / 宽屏按比例放大)
- selection-aware 自动滚动(选文本时不抢滚动)
- 新增 /btw、/continue、/export、/restore

不改 cli / hub / README / tuiapp.py 入口。
2026-05-12 22:26:35 +08:00
Jiaqing Liang cf6551516f feat(reflect): add init() for runtime args with fallback to env/config 2026-05-12 12:59:30 +08:00
Liang Jiaqing 736f88a00a feat(tui): multi-line input with Enter submit, Ctrl+Enter newline, paste-safe 2026-05-11 21:12:28 +08:00
AspasZhang c247abb78a docs: update WeChat group 17 QR code (#338) 2026-05-11 20:38:38 +08:00
Liang Jiaqing f3f5b76d68 fix(launch): skip paste interception when clipboard contains text
PPT/Word copies include both image/png and text/plain items.
Only intercept paste when no text item is present (pure screenshot/image).
2026-05-11 20:27:26 +08:00
YooooEX 1b9fe1f539 feat(tgapp): add /btw command handling (#335) 2026-05-11 20:06:04 +08:00
Liang Jiaqing 2381d48a56 refactor: rename command/ to ga_cli/ to avoid namespace collision 2026-05-11 10:01:03 +08:00
WindCat e248aa8c02 feat: 添加跨平台 GA 命令行入口系统 (#329)
新增 command/ 包和 ga.cmd/ga 入口脚本,用户可通过 `ga <command>` 启动各前端。
2026-05-11 09:57:51 +08:00
manaskarra ee02e7eba5 fix(tgapp): preserve streaming overflow segments (#330)
Keep Telegram streaming overflow messages tied to the primary live message, reuse them across edits, and delete stale overflow replies when streamed content shrinks.
2026-05-11 09:40:18 +08:00
Liang Jiaqing dd2f96ba65 fix: hide code runner console window 2026-05-11 09:39:52 +08:00
nianyucatfish 9024af7499 feat(stapp): add /export command (clip/file/all) (#322) 2026-05-09 23:08:20 +08:00
Liang Jiaqing 3c5c99fd6d chore: replace alarming warning with neutral "(无文本输出)" in feishu display 2026-05-09 19:51:35 +08:00
AspasZhang 321ad8ad4f docs: update WeChat group 16 QR code (#318) 2026-05-09 18:37:48 +08:00
Liang Jiaqing a2f0f0d46a fix(stapp): restore finally abort guard in agent_backend_stream 2026-05-09 18:12:09 +08:00
Liang Jiaqing a6c010d25a fix: prevent empty response from corrupting history & add _fix_messages to non-native paths 2026-05-09 17:28:11 +08:00
Liang Jiaqing 67d135ab4d fix: avoid empty content messages in native tool flow 2026-05-09 16:28:44 +08:00
Liang Jiaqing 4af02c5f98 fix: unify retry logic in do_no_tool with shared counter to prevent infinite loop (#312) 2026-05-09 15:40:39 +08:00
Shen HaoandClaude Opus 4.7 03d19c9821 feat: add /btw side-question subagent (#317)
Mirrors Claude Code's /btw: ask the agent a quick question without
interrupting the in-flight main task.

- frontends/btw_cmd.py: snapshot backend.history under lock + deepcopy →
  background-thread backend.raw_ask single-turn answer; never writes
  backend.history; never touches task_queue. Reuses backend.raw_ask +
  make_messages, no new LLM instance.
- frontends/chatapp_common.py: register /btw in HELP_COMMANDS; handle
  /btw in AgentChatMixin (sync via asyncio.to_thread); install() at
  module bottom so the slash hook is wired for any frontend that
  imports chatapp_common.
- frontends/stapp.py: persist display_queue + partial_response in
  st.session_state so the streaming bubble survives Streamlit reruns;
  new render_main_stream() helper covers both new tasks and resume;
  /btw branch uses plain st.rerun() (preserves dq) so the main task's
  remaining output renders in a follow-up bubble; non-/btw prompts
  explicitly call agent.abort() to keep the original "submit cancels"
  UX. Strip <summary> meta tags from text segments and drop dangling
  "LLM Running (Turn N) ..." markers in resumed partials to clean up
  the rendered chat.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:26:05 +08:00
KailigithubandKailigithub 2a80e1e931 修复:清理 TMWebDriver 未使用的导入,修正裸 except 语法 (#311)
Co-authored-by: Kailigithub <Kailigithub@users.noreply.github.com>
2026-05-09 15:00:39 +08:00
Liang Jiaqing ca79699492 feat(tui): add /clear and /close commands 2026-05-09 14:56:52 +08:00
AspasZhang 268ceab408 fix: disable Streamlit dev shortcuts to prevent Ctrl+C triggering Clear Caches on Windows (#309) 2026-05-09 12:31:28 +08:00
wangjc683 cde1620710 fix: filter whitespace-only text blocks in NativeToolClient.chat() (#307)
When the model only outputs tool calls with no meaningful text,
combined_content may contain only whitespace text blocks (e.g. '\n').
Strict API proxies reject this with HTTP 400 'empty content' error.
Filter out such blocks before sending to the API.
2026-05-09 10:41:29 +08:00
Liang Jiaqing 2ddeab20a0 hub.pyw: add EXCLUDES set for goal_mode/chatapp_common/tuiapp; ga.py: fix missing summary prompt wording 2026-05-09 09:51:32 +08:00
AspasZhang dcab4b7608 docs: replace WeChat group 15 QR code (#302) 2026-05-08 22:34:19 +08:00
AspasZhang 7b07b6649a docs: update WeChat group QR code from 14 to 15 (#300) 2026-05-08 19:57:44 +08:00
Liang Jiaqing d7f5a3445a docs: add configure_mykey.py references to README & GETTING_STARTED 2026-05-08 17:49:24 +08:00
Liang Jiaqing 787bc1f9f3 move configure_mykey.py to assets/, fix PROJECT_ROOT 2026-05-08 17:44:26 +08:00
Liang Jiaqing c6d8d87e64 rename: configure.py -> configure_mykey.py 2026-05-08 17:40:10 +08:00
WindCatandguowenjiao54 e1c128e32f feat: add configure.py — interactive initialization wizard (#295)
* feat: add setup.py -- interactive initialization wizard

* fix: address PR #295 review — rename to configure.py, improve all feedback

- Rename setup.py → configure.py to avoid pip/setuptools conflict
- Fix lark exception: catch generic Exception instead of non-existent AppAccessDeniedError/AppExpiredError
- Add proxy support to probe_models via HTTPS_PROXY/HTTP_PROXY env vars
- Implement masked_input() with real-time API key masking
- Add pip install hints for messaging platform dependencies
- Add 2 new providers: CRS Gemini Ultra (Antigravity), Kimi/Moonshot OAI
- Restructure main menu: LLM only / platform only / or both with cascade prompts
- Add Python 3.14 compatibility warning and reconfiguration detection
- Fix welcome_message: WeCom only, correct variable name
- Show GETTING_STARTED example commands after configuration
- Add advanced LLM options: proxy, context_win, stream, user_agent

---------

Co-authored-by: guowenjiao54 <guowenjiao54@users.noreply.github.com>
2026-05-08 17:36:38 +08:00
Liang Jiaqing 1a7d54b82f Add lightweight goal mode reflect workflow 2026-05-08 17:28:16 +08:00
Liang Jiaqing 639da67916 feat: fsapp reconnect loop, tui branch/rewind, simphtml flow-child fix, sop updates 2026-05-08 15:48:16 +08:00
solopole 2fdbb289ed feat(discord): improve channel activation and progress display (#292) 2026-05-08 11:56:55 +08:00
TraderLife 8586310867 feat: add --wechat/--wx launch argument (#288) 2026-05-08 11:53:54 +08:00
Liang Jiaqing 7b51599088 fix: 堵死自主规划绕过subagent评审的漏洞
- subagent.md: 隐藏--nobg选项防止同步阻塞超时
- task_planning.md: 加第7条无限重试禁自评+探测策略重排序
- autonomous_operation_sop.md: set_todo前缀提示
2026-05-08 08:02:23 +08:00
Liang Jiaqing c7cef56cb2 fix: invalidate panel cache on fold toggle 2026-05-07 22:23:48 +08:00
Liang Jiaqing cc6b3fbee1 tuiapp: cache rendered panels; llmcore: simplify fast_ask 2026-05-07 22:18:25 +08:00
Liang Jiaqing 379b69e0ec refactor: simplify LLM client init with resolve_client; add fast_ask; auto-fold old tasks in TUI 2026-05-07 21:06:14 +08:00
Liang Jiaqing 072390e0fc README: remove --demo tips, split install deps for GUI/TUI 2026-05-07 18:19:48 +08:00
Liang Jiaqing 98e2e3f483 tuiapp: improve session nav (ctrl+arrows), CJK-aware sidebar, README restructure 2026-05-07 18:15:08 +08:00
Liang Jiaqing a9450b1f18 fix(tui): shorten sidebar Q/S to prevent wrapping, add Ctrl+1~9 session switch 2026-05-07 17:41:12 +08:00
Liang Jiaqing d4ab909144 feat: add Textual TUI frontend + minor fixes to agentmain/llmcore/stapp 2026-05-07 17:38:58 +08:00
Jiaqing Liang 93490d1634 refactor: rename GeneraticAgent, default bg for task, add fork history support 2026-05-07 15:12:46 +08:00
SONG ZHENRUI 71914044d7 Update README with virtual environment setup instructions (#243)
Added instructions for setting up a virtual environment and installing dependencies.

error: No virtual environment found; run `uv venv` to create an environment, or pass `--system` to install into a non-virtual environment
2026-05-07 13:11:20 +08:00
succ985andsucc985 0148446fab feat(wechat): add persistent typing indicator (#278)
The previous implementation called send_typing once with an empty
typing_ticket, which had no effect. The iLink API requires a valid
typing_ticket obtained from getconfig first.

Changes:
- Add get_typing_ticket() method to fetch ticket via getconfig API
- Replace single send_typing call with continuous typing loop
- Typing refreshes every 2 seconds until agent finishes processing
- Stops cleanly via threading.Event when message handling completes

Co-authored-by: succ985 <succ985@users.noreply.github.com>
2026-05-07 13:11:15 +08:00
LJQ 346d26bdf1 Merge pull request #279 from sontianye/fix/qqapp-reconnect-backoff
fix(qqapp): exponential reconnect backoff
2026-05-07 12:33:51 +08:00
LJQ b1fdd777f7 Merge pull request #283 from draj1e/feature/sophub
add sophub link to readme
2026-05-07 12:33:48 +08:00
draj1e 9ecc68ba59 add sophub 2026-05-07 11:18:08 +08:00
Tianye Song fd7bfcd9d3 fix(qqapp): exponential reconnect backoff
Replace fixed 5s sleep with the same backoff pattern already used in
dcapp.py: 5 -> 10 -> 20 -> ... -> 300s, resetting to 5s when a session
survived >=60s. Uses time.monotonic() to avoid clock-jump skew.
2026-05-07 10:29:37 +08:00
Liang Jiaqing 0807b130e9 refine: worker on_done + 120s window + prompt tweaks 2026-05-06 19:16:29 +08:00
Liang Jiaqing edbc473660 feat: agent BBS server + team worker + timeout bump 2026-05-06 18:55:18 +08:00
LJQ e08d7c7370 Merge pull request #266 from wellsoren/qt-ui-redesign
QT界面持续优化和bug修复
2026-05-06 13:33:36 +08:00
LJQ 04f7158aa7 Merge pull request #274 from yiqi-017/feat/acp-bridge
fix: ACP bridge streaming overlap and truncation
2026-05-06 13:33:07 +08:00
Jiaqing Liang 2d0b8e9648 Improve English UI defaults and task timeout 2026-05-06 13:23:33 +08:00
Liang Jiaqing 6e2ec39052 file_write: support content arg in native mode, strip it in text-emulated mode 2026-05-06 10:30:08 +08:00
yiqi-017andClaude Opus 4.6 a7a1487004 fix: use incremental streaming in ACP bridge to prevent text overlap
Switch to inc_out=True mode so queue items are incremental deltas
instead of cumulative text. The old cumulative approach broke when
agentmain's post-processing (</summary>\n\n insertion) shifted offsets,
causing the bridge's delta calculation to produce overlapping or
truncated content.

Now "next" items are forwarded directly, and "done" is only used as
content when no streaming occurred (error fallback).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-06 03:54:09 +08:00
LJQ f98f6eac39 Merge pull request #268
fix: remove duplicate backtick key in VK_CODE dictionary
2026-05-05 18:44:57 +08:00
LJQ 4299a83cc0 Merge pull request #269
fix: add request timeout and replace assert with proper exception
2026-05-05 18:41:56 +08:00
LJQ 7ed28f1e56 Merge pull request #271 from AspasZhang/docs/update-wechat-qr-group14
docs: update WeChat group QR to group 14, remove old group 13 image
2026-05-05 18:11:25 +08:00
LJQ a0e0cc66d8 Merge pull request #272 from yiqi-017/feat/acp-bridge
feat: add ACP bridge for codeg integration
2026-05-05 18:10:33 +08:00
yiqi-017andClaude Opus 4.6 7deb330c11 docs: add codeg integration section to README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 15:34:57 +08:00
yiqi-017andClaude Opus 4.6 dfe6bf0709 refactor: move acp bridge to frontends/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 14:28:13 +08:00
AspasZhang ac17bacd91 docs: update WeChat group QR to group 14, remove old group 13 image 2026-05-05 13:32:36 +08:00
Liang Jiaqing a1a0cb815b feat: add contentSettings cmd to CDP bridge (bypass multi-download dialog); minor agentmain cleanup 2026-05-05 11:57:40 +08:00
Kailigithub af3ee66cd7 fix: add request timeout and replace assert with proper exception 2026-05-05 03:08:55 +00:00
Kailigithub fe1d8a1e1f fix: remove duplicate backtick key in VK_CODE dictionary 2026-05-05 03:07:35 +00:00
Liang Jiaqing f0ff7ee49e remove newtab, add terminal QR, refine task_planning sop 2026-05-04 12:39:41 +08:00
wellsoren 4f58b4e8d1 Merge branch 'main' into qt-ui-redesign 2026-05-03 22:59:44 +08:00
wellsoren 7ab18f1e99 style: QT界面持续优化和bug修复 2026-05-03 22:48:41 +08:00
wellsoren 3f712aeb66 Merge branch 'main' of github.com:lsdefine/GenericAgent 2026-05-03 22:25:44 +08:00
Liang Jiaqing 9f3ddec78f /resume prompt优化(v5) + wechatapp去or空占位 + summary截断80 2026-05-03 16:36:48 +08:00
wellsoren 4bbdf8cb26 chore: sync with upstream and update qtapp 2026-05-03 15:35:53 +08:00
wellsoren 5bd947b958 chore: sync with upstream and update qtapp 2026-05-03 14:31:13 +08:00
Liang Jiaqing 2afe4c3e39 feat: auto-inject summary text block when model outputs thinking+tool_use without text; stapp fallback uses content preview 2026-05-03 12:22:05 +08:00
Liang Jiaqing de24f0b2b9 fix: clear _pending_tool_ids when history is empty to prevent orphan tool_result after /new 2026-05-02 21:05:51 +08:00
Liang Jiaqing e66473f06e fix: include earlier_context in compress_history_tags pattern 2026-05-02 20:46:16 +08:00
Liang Jiaqing 95a7aeccb6 feat: peer_hint mechanism & history folding compression 2026-05-02 20:30:33 +08:00
yiqi-017andClaude Opus 4.6 e8bb346eb9 fix: mark ACP stdout fd as non-inheritable to prevent child process pollution
- os.set_inheritable(_stdout_fd, False) on both Windows and Unix
- Unix path now uses os.dup() + os.fdopen() consistent with Windows
- Prevents subprocess.Popen children from inheriting the ACP JSON-RPC
  file descriptor, which could corrupt the protocol stream

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-02 17:46:13 +08:00
hanshou101 dece612853 fix(fsapp): rollover oversized Feishu task cards (#209) 2026-05-02 14:54:46 +08:00
Liang Jiaqing 52f5d06766 fix: non-greedy file_content extraction, empty response retry limit, vision template pass 2026-05-02 12:59:40 +08:00
Liang Jiaqing a6416f6ef3 Revert "fix: make launcher resilient to pywebview startup failures (#203)"
This reverts commit dbb562f550.
2026-05-02 12:47:21 +08:00
CauchYoungandlaplace young dbb562f550 fix: make launcher resilient to pywebview startup failures (#203)
Co-authored-by: laplace young <yangqk12@whu.edu.cn>
2026-05-02 12:45:57 +08:00
KailigithubandKailigithub e22bdae74d fix: add Linux CJK font paths for desktop pet bubble text rendering (#248)
Co-authored-by: Kailigithub <Kailigithub@users.noreply.github.com>
2026-05-02 12:32:16 +08:00
Liang Jiaqing 9a02c28bfe docs: update WeChat group QR to group 13, remove old group images 2026-05-01 10:49:19 +08:00
LJQ 87a340e5b6 Merge pull request #245 from AspasZhang/pr/add-wechat-group13-image
docs: add missing wechat_group13.jpg
2026-05-01 10:47:03 +08:00
AspasZhang 3fd9e79b29 docs: add wechat_group13.jpg image 2026-05-01 10:46:15 +08:00
Liang Jiaqing 24f764082d Revert "Merge pull request #244 from AspasZhang/pr/update-readme-wechat-groups"
This reverts commit b22f3c45cc, reversing
changes made to 8bea96614e.
2026-05-01 10:46:11 +08:00
LJQ b22f3c45cc Merge pull request #244 from AspasZhang/pr/update-readme-wechat-groups
docs: 更新微信群二维码,去掉群8/9/11,保留群12,新增群13
2026-05-01 10:39:11 +08:00
AspasZhang 765068e272 docs: 更新微信群二维码,去掉旧群,只保留群13 2026-05-01 10:34:26 +08:00
tqjason 8bea96614e Fix pet reference in rightMouseDown_ method (#239) 2026-05-01 08:45:51 +08:00
Liang Jiaqing 0e08c31fb9 add official channel disclaimer; fix scroll/wechat-md/llm-msg 2026-05-01 08:41:10 +08:00
yiqi-017andClaude Opus 4.6 75d063a553 feat: enable verbose output in ACP bridge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 21:13:11 +08:00
Liang Jiaqing d4f8303053 feat: add Discord bot frontend; refine memory SOP index rule 2026-04-29 19:29:36 +08:00
Liang Jiaqing 6a3eecc07e fix: sanitize backticks in code_run output & simplify subagent turn header 2026-04-29 18:26:16 +08:00
yiqi-017andClaude Opus 4.6 be4d2f58c1 feat: add ACP bridge for codeg integration
Stdio-based JSON-RPC bridge implementing the Agent Client Protocol,
allowing codeg to connect to GenericAgent as a live ACP agent.

Handles Windows binary stdout/stdin, redirects stray prints to stderr,
and converts GenericAgent's cumulative text queue to ACP delta streaming.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 15:46:47 +08:00
Liang Jiaqing d626c9f02c improve TMWebDriver connection error message and SOP 2026-04-29 14:02:43 +08:00
Liang Jiaqing 513fec91fc cleanup: remove NextWillSummary, add supervisor_sop, fix streaming fence protection, tighten L1 rules 2026-04-29 13:29:03 +08:00
Liang Jiaqing 9e19b8a8de refactor: deduplicate _parse_mixed_response by reusing _parse_text_tool_calls; fix orphan tool_result in _fix_messages 2026-04-28 22:15:07 +08:00
Liang Jiaqing 513b644252 统一流重试行为: 不管mixin/非mixin中途断连都重试; 更新ga.py错误检测条件 2026-04-28 20:41:14 +08:00
Tianye Song 68982c36dc fix(dingtalkapp): exponential reconnect backoff and token fetch retry (#210)
- Reconnect: exponential backoff 5→300s, reset on healthy session ≥60s
- Access token: retry once after 1s before returning None
2026-04-28 20:25:00 +08:00
LJQ 9390435cde Merge pull request #199 from AspasZhang/fix/plan-sop-remove-sop-index-dep
fix: remove sop_index.md dependency in plan_sop
2026-04-28 20:17:16 +08:00
LJQ eb9c4c4752 Merge pull request #214 from YooooEX/telegram-polish
fix(tgapp): handle Telegram stream rate limits
2026-04-28 20:16:08 +08:00
YooooEX 5e1a788af1 fix(tgapp): handle Telegram stream rate limits 2026-04-28 18:34:36 +08:00
YooooEX 972c55bcb2 Merge remote-tracking branch 'origin/main' into telegram-polish 2026-04-28 18:30:35 +08:00
Liang Jiaqing 7070880b4e refactor: extract _stream_with_retry, MixinSession failover on stream abort 2026-04-28 17:54:37 +08:00
YooooEX 442ab73c48 Merge remote-tracking branch 'origin/main' into telegram-polish 2026-04-28 12:15:50 +08:00
YooooEX baf0f27793 feat(tg): stream Telegram responses by turn with summaries (#208) 2026-04-28 12:01:07 +08:00
Liang Jiaqing f421913703 autonomous sop: 收尾改为4步,完成即停不贪多 2026-04-28 11:45:12 +08:00
YooooEX 41c79ac5c4 feat(tg): stream Telegram responses by turn with summaries 2026-04-28 11:21:09 +08:00
Liang Jiaqing e58934c744 fix: /resume normalize literal \n to real newlines before regex matching 2026-04-28 09:38:04 +08:00
AspasZhang aaf15b6797 fix: remove sop_index.md dependency in plan_sop, use L1 Insight index instead
sop_index.md is a user-specific file that doesn't exist in the shared repo.
Replace it with L1 Insight index which is auto-injected into context each turn.
2026-04-27 19:25:37 +08:00
LJQ 776f4e267d Merge pull request #196 from YooooEX/telegram-polish
feat(tgapp): add ask_user inline candidate menu
2026-04-27 18:51:29 +08:00
LJQ 0ed89ebdeb Merge pull request #198 from voidborne-d/fix/stapp-autonomous-enabled-fragment-rerun
fix(stapp): defend autonomous_enabled init against fragment-only reruns
2026-04-27 18:20:24 +08:00
Liang Jiaqing 78a9414e05 feat: add SSL verify option and enable TG draft 2026-04-27 18:15:57 +08:00
voidborne-d b8ecdc7922 fix(stapp): defend autonomous_enabled init against fragment-only reruns
closes #197
2026-04-27 17:51:32 +08:00
Liang Jiaqing 3c9496ffa2 refactor: 提取script_dir为模块级变量,清理procmem_sop涉敏感代码 2026-04-27 17:46:31 +08:00
YooooEX 7ef7283fad feat(tgapp): add ask_user inline candidate menu 2026-04-27 16:02:24 +08:00
Jiaqing Liang b83cf28e29 feat: ensure thinking blocks for deepseek models in history 2026-04-27 15:11:09 +08:00
Jiaqing Liang 16c22432e3 refactor: _openai_stream takes sess+messages; add service_tier support 2026-04-27 15:11:08 +08:00
UnsongK 11442847b0 feat(wecom): upgrade wecom bot to full media + progress streaming (#184)
* feat(wecom): upgrade wecom bot from text-only to full media + progress streaming

- Add image/file message handling with encrypted media download
- Add media upload & send (auto image/file type detection, fallback to text on failure)
- Add real-time per-turn progress streaming via _turn_end_hooks
- Add send_done with automatic file attachment extraction from agent replies
- Add terminal CLI: help/status/stop/exit for runtime control
- Add run stats tracking (received/completed counts)
- Refactor message parsing into shared _accept() method (dedup + auth + register)
- Harden None-safety on body.get(from) and dedup ID fallback
- Move state from module globals to instance attributes

* refactor(wecomapp): parameterize agent & client, remove dead code

- Remove module-level agent creation; inject via constructor
- Make WSClient injectable in start() for testability
- Clean up stale comments and version tag
- Add inline doc for split_limit
2026-04-27 14:13:28 +08:00
Junghwan 15112bccf1 fix(schema): align file_write mode default with implementation
assets/tools_schema.json declares file_write.mode default as 'append',
but assets/tools_schema_cn.json and ga.py both default to 'overwrite':

    # ga.py:369
    mode = args.get("mode", "overwrite")

Since the i18n switch in 7cadbd7 loads the English schema for en_*
locales, an LLM that trusts the schema default and omits the mode
parameter will silently overwrite instead of append. Align the English
schema with the Chinese schema and the Python handler so all three
agree on 'overwrite'.
2026-04-27 10:06:15 +08:00
LJQ 8e7cb8f559 Merge pull request #185 from AspasZhang/docs/update-wechat-group12-qr
docs: 更新微信群二维码,移除群8,新增群12
2026-04-27 09:34:21 +08:00
weijia db6bf00d0a fix(fsapp): show truncated tail instead of silent "..." fallback (#186)
When model output contains only stripped tags, surface warning + last 300 chars instead of silent "...".
2026-04-26 23:16:43 +08:00
Liang Jiaqing df77311516 chore: remove unused test files 2026-04-26 22:58:43 +08:00
AspasZhang fd58b85fe4 docs: 更新微信群二维码,移除群8,新增群12 2026-04-26 22:55:02 +08:00
Liang Jiaqing 1d0b86d110 fix: remove inherited HTTPS_PROXY to prevent WeChat long-poll SSL EOF 2026-04-26 22:09:55 +08:00
Liang Jiaqing f11a056d17 handle file paste in webview 2026-04-26 20:51:49 +08:00
Liang Jiaqing d949a59ebb file_read: add PARTIAL hint when count < total lines 2026-04-26 20:11:00 +08:00
Liang Jiaqing adfa805fe3 improve ask_user tool display and memory SOP prompt 2026-04-26 18:24:21 +08:00
Liang Jiaqing 13850b85a8 hotreload mykeys, fix SSE residual block, unify context_win, cleanup 2026-04-26 17:46:24 +08:00
Liang Jiaqing 6f1f02eddb wechatapp: add image thumbnail pipeline, UA coverage, refactor turn-based streaming 2026-04-26 17:24:32 +08:00
Liang Jiaqing cd0ce4d9b0 fix: summary提取与fold_turns防误切,强化thinking模型summary输出 2026-04-26 00:01:42 +08:00
Liang Jiaqing 69f66997fd fix: fallback to getpass.getuser() when os.getlogin() raises OSError 2026-04-25 22:55:17 +08:00
LJQ 567c7f582b Merge pull request #140 from huangrichao2020/feat/macos-desktop-app
feat: add macOS Desktop App installation script
2026-04-25 22:17:44 +08:00
LJQ adf393c0a5 Merge pull request #173 from Stark-X/fix/linux-desktop-pet-pyside6
fix: add Linux desktop pet implementation
2026-04-25 22:09:35 +08:00
LJQ 12f23138e0 Merge pull request #164 from kukudelaomao/fix/wecomapp-event-handlers
fix(wecomapp): 修复WebSocket连接和事件处理器问题
2026-04-25 22:04:31 +08:00
LJQ c19ac2dae6 Merge pull request #160 from YooooEX/telegram-polish
fix(tgapp): polish Telegram MarkdownV2 rendering and startup helpers
2026-04-25 21:59:49 +08:00
LJQ 5805566d88 Fix Telegram startup without a configured proxy
Fix Telegram startup without a configured proxy
2026-04-25 21:51:54 +08:00
LJQ da91a0643f fix(sandbox): patch Popen + block startfile to prevent window leaks
fix(sandbox): patch Popen + block startfile to prevent window leaks
2026-04-25 21:49:15 +08:00
Liang Jiaqing 5b0d96b1b5 tg: support document upload, disable draft streaming; wx: streaming flush during task; llmcore: safeprint, max_tokens fixes 2026-04-25 21:33:38 +08:00
Jiaqing Liang 10776fed51 Remove responses max output token limit 2026-04-25 14:03:21 +08:00
Jiaqing Liang afa4586e3d fix: _drop_unsigned_thinking对str content崩溃 + _to_responses_input丢弃thinking-only assistant消息导致tool_call被吞 2026-04-25 13:53:08 +08:00
Auston Li bd2dd42ba4 Fix Telegram startup when proxy is not configured
Only use a proxy for tgapp when mykeys['proxy'] is explicitly configured, avoiding the dead default local proxy and restoring Telegram polling startup.

Closes #175
2026-04-25 00:54:42 -04:00
Stark-X 0f77470fe0 Merge branch 'lsdefine:main' into fix/linux-desktop-pet-pyside6 2026-04-25 11:20:11 +08:00
MuziIsabel 3b93c6137d fix(sandbox): patch Popen + block startfile to prevent window leaks
subprocess.run was already patched with CREATE_NO_WINDOW, but Popen and
os.startfile were unprotected. Agent code could open visible GUI windows
via subprocess.Popen(['notepad.exe']) or os.startfile().
2026-04-25 10:59:15 +08:00
Stark-X 09cf6306af fix: add Linux desktop pet implementation 2026-04-25 10:58:38 +08:00
Liang Jiaqing 08181be4bf fix: use correct token limit params for OpenAI APIs 2026-04-25 10:54:44 +08:00
Liang Jiaqing cac3ba4769 feat: reasoning/thinking互通适配 + history窗口扩大 + summary提示强化 2026-04-25 10:33:42 +08:00
郭春飞 936501069c fix(wecomapp): 修复WebSocket连接和事件处理器问题
修复企业微信机器人无法正常连接的问题:

1. 修复WSClient初始化参数传递错误
   - 原代码错误地将参数传递给WSClient构造函数
   - 修正为正确传递host, port, path等参数

2. 修复连接方法调用错误
   - 将connect_async()改为connect()方法
   - AiBotSDK的WSClient使用同步connect方法

3. 修复事件处理器签名不匹配
   - on_connected/on_authenticated: 移除frame参数(这些处理器不需要参数)
   - on_disconnected: 添加reason参数
   - on_error: 添加error参数

修复后验证:
- WebSocket连接成功建立
- 认证过程正常完成
- 心跳机制正常工作
- 日志无错误信息

此修复解决了企业微信机器人启动后无法连接服务器的问题。
2026-04-25 01:18:28 +08:00
YooooEX 8e159a9773 feat(tgapp): add support for __ bold and ~~ strikethrough styles 2026-04-24 20:11:40 +08:00
YooooEX 0c5d3c8635 fix(tgapp): align startup with common helpers and render file marker basenames 2026-04-24 20:11:40 +08:00
Liang Jiaqing 114dfdb211 fix: wechatapp单例锁端口19528→19531 避免与qqapp/wecomapp冲突 2026-04-24 19:58:27 +08:00
LJQ 9047e7fe39 fix(cli): 修复中文退格显示残影 (#157)
fix(cli): 修复中文退格显示残影
2026-04-24 18:52:48 +08:00
Huang richao 446d7bf549 Merge remote-tracking branch 'origin/main' into feat/macos-desktop-app 2026-04-24 17:53:18 +08:00
Huang richao b34965b936 fix: narrow macOS app installer per review 2026-04-24 17:52:47 +08:00
Liang Jiaqing ee899d4dd5 fix(tgapp): remove hardcoded proxy default, use mykey.py config instead
The proxy was hardcoded to http://127.0.0.1:2082 which breaks for users
without a local proxy (e.g. international users with direct access).
Now defaults to None; users who need a proxy can set it in mykey.py.
2026-04-24 17:30:48 +08:00
Liang Jiaqing 888ce2f9bb fix(pyproject): correct dingtalk-stream version constraint (>=0.20, was >=1.0 which does not exist on PyPI) 2026-04-24 17:24:09 +08:00
VonEquinox f74cc39c33 fix(cli): 修复中文退格显示残影 2026-04-24 17:21:04 +08:00
LJQ e2a57e7a32 Merge pull request #153 from ViviqwerAsd/feat/fsapp-add-llm-command
feat(fsapp): add /llm command support for model switching
2026-04-24 16:01:14 +08:00
Xinyi Wang a6644d580c feat(fsapp): add /llm command support for model switching
- Add /llm command to list available models
- Add /llm [n] command to switch to specific model
- Improve /status command to show current LLM info with emoji indicators
- Update /help text to include /llm command
- Unify command parsing logic using parts and op variables

This brings fsapp.py (Feishu/Lark) frontend in line with other chat frontends (QQ, DingTalk, WeCom) that already support all slash commands.
2026-04-24 04:20:16 -03:00
LJQ 883366ce55 Merge pull request #148 from AspasZhang/docs/update-wechat-qr-codes-v3
docs: update WeChat community group QR codes (remove 6&10, add 11)
2026-04-24 12:32:00 +08:00
AspasZhang be2386661d docs: update WeChat community group QR codes (remove 6&10, add 11) 2026-04-24 11:08:20 +08:00
Liang Jiaqing 12c2fe1f79 add uv install guide & pyproject.toml; unify error prefix to !!!Error: 2026-04-24 10:00:23 +08:00
Liang Jiaqing f334994ab6 fix: thinking非空时不再误判为空响应触发重试 2026-04-24 10:00:23 +08:00
LJQ 6974853695 Merge PR #144: style: 重构并优化 qtapp 桌面窗口样式与交互功能
style: 重构并优化 qtapp 桌面窗口样式与交互功能
2026-04-24 09:26:08 +08:00
YooooEX f516c52a72 feat(tgapp): stream MarkdownV2 replies with draft fallback and file segments (#135) 2026-04-24 09:01:49 +08:00
wellsoren 8065c0d759 style: 重构并优化 qtapp 桌面窗口样式与交互功能 2026-04-24 01:50:09 +08:00
Liang Jiaqing fd46d61543 fix: SSE tool_call streaming compat for non-standard index/id, MockToolCall handle list args & None fallback; sop: CDP bringToFront note 2026-04-23 23:44:50 +08:00
Huang richaoandQwen-Coder 7255515607 feat: add macOS Desktop App installation script
- Add scripts/install-macos-app.sh for one-click macOS app installation
- Uses bundled assets/images/logo.jpg as app icon
- AppleScript prompts for project folder on first run
- Updates README with macOS Desktop App installation option
- Supports both interactive and --auto non-interactive modes

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-23 23:19:24 +08:00
Liang Jiaqing 8a6a5715ff Fix responses call_id mapping for converted tool history 2026-04-23 20:53:44 +08:00
Liang Jiaqing df2a7749b4 refactor: unify system prompt injection into _openai_stream; disable gpt done_hooks 2026-04-23 20:07:20 +08:00
Jiaqing Liang 6091bf0c6f fix: add requests to pip install & tune max_turns/prompt 2026-04-23 17:21:22 +08:00
weijiaandwjl2023 fd4c833511 feat(stapp): restore chat history bubbles after /continue N (#138)
When the user runs '/continue N' in stapp, the agent's in-memory context
is restored, but the UI previously showed only a single '✅ restored' line
— all prior chat bubbles were missing.

This change parses the target session log and reconstructs the
user/assistant message pairs into st.session_state.messages, so reopening
a session feels like the conversation was never interrupted.

* continue_cmd.py: add extract_ui_messages(path)
  - parses model_responses log into [{role, content}, ...]
  - groups multi-turn LLM calls (prompts whose text starts with the
    '### [WORKING MEMORY]' header) into a single assistant bubble,
    inserting the existing '**LLM Running (Turn N) ...**' marker so
    fold_turns() renders them as collapsible segments.
  - two small helpers (_user_text / _assistant_text) keep parsing local.

* stapp.py: in the /continue branch, resolve the target log path BEFORE
  calling handle_frontend_command (which snapshots the current log and
  would otherwise shift list_sessions indices), then replace
  session_state.messages with the reconstructed history on success.
  Falls back to the previous behavior for bare /continue or failure.

Co-authored-by: wjl2023 <wjl2023@users.noreply.github.com>
2026-04-23 16:36:41 +08:00
Liang Jiaqing 1678114f1f fix: split concatenated JSON tool args in _parse_openai_sse to prevent _raw death spiral 2026-04-23 11:53:35 +08:00
Liang Jiaqing 97573e6f46 fix: agent-level retry for intermittent SSL errors during streaming
- llmcore: prefix all error outputs with !!! marker
- ga: detect !!!Error: [SSL: in tail of content as incomplete response and retry
- add len(content)>100 guard to avoid false positives on short responses
2026-04-23 11:24:54 +08:00
Liang Jiaqing 17971f642a fix: guard all id/call_id fields with or "" to prevent None leaking into payload 2026-04-23 11:00:04 +08:00
Liang Jiaqing a53b7a8593 refine L0 sync rule: default L3, bracket trigger words only for non-intuitive scenarios 2026-04-22 19:49:27 +08:00
Liang Jiaqing 08c583d1c6 fix: filter empty text blocks in message conversion & add prompt continuation line 2026-04-22 19:37:06 +08:00
LJQ 928633e4b2 Merge pull request #134 from shenhao-stu/main
template: upload mykey_template_en.py
2026-04-22 19:11:43 +08:00
Shen Hao 542aa89130 template: add mykey_template_en.py 2026-04-22 19:09:07 +08:00
Shen Hao eb2a3bf824 template: add Kimi for Coding configuration 2026-04-22 19:05:40 +08:00
Liang Jiaqing 274d47f35f Normalize OAI messages before conversion and update tool-call prompt 2026-04-22 17:16:05 +08:00
weijia b34cffe801 feat(fsapp): render agent run as one continuously-updating Feishu card (#131)
Rework the Feishu frontend so each user turn renders as a single
collapsible task card that patches itself in place, replacing the
dq-based streaming path that produced many fragmented messages.

- One _TaskCard per turn; hook reacts to summary / exit_reason events
  from the agent loop and patches the same card.
- Each step is a foldable panel: header shows the summary, expanding
  reveals three sections (auto-hidden when empty):
    * Thinking   - from response.thinking (separate field, not content)
    * Tool Calls - tool name + truncated JSON args
    * Output     - response.content, with protocol tags stripped so
                   the header summary is not duplicated inside
- Final reply rendered as a schema 2.0 markdown card for consistency.
- Code-review pass per code_review_principles.md:
    * _TaskCard owns only stateful card lifecycle (start/step/done/fail)
    * Pure formatting extracted to module-level _build_step_detail and
      _fmt_tool_call (no more reaching into card._private from the hook)
    * Hook is a ~10-line dispatcher
    * Flattened a 4-level nested lambda into a named function
2026-04-22 14:12:51 +08:00
Liang Jiaqing 132edb3154 improve GPT done_hook: require informative summary on completion 2026-04-22 10:53:29 +08:00
Liang Jiaqing 7b2ff1e73a feat(tgapp): handle photo messages, pass temp path to agent 2026-04-21 23:39:46 +08:00
LJQ f1db2e56df feat: add Telegram slash command parity and menu sync (#125)
feat: add Telegram slash command parity and menu sync
2026-04-21 23:14:38 +08:00
song f14ad0c693 fix(llmcore): preserve thinking block signature in streaming SSE parser (#123)
Anthropic's extended thinking streaming protocol emits two delta types
for a single thinking block: `thinking_delta` (the textual reasoning)
and `signature_delta` (a base64 HMAC tag appended at the end of the
block). Both must be accumulated into the same `content_block`.

Current code only handles `thinking_delta`, so `signature_delta` events
are silently dropped. When the assistant's reply (with thinking) is
echoed back on the next turn, Anthropic's server validates the
signature and rejects the request with 400:

    "Invalid `signature` in `thinking` block"

Downstream effects observed in production (via sub2api relay logs):
- Every request with history triggers a 400 signature error
- The relay strips thinking blocks and retries, which changes the
  cache prefix and invalidates prompt caching, forcing a full rebuild
  of cache_creation_tokens (~20k-30k per affected request)
- Measured in a 5h window: 5/25 requests suffered cache invalidation,
  accounting for 53.5% of total spend that was otherwise avoidable

Fix:
1. Initialize `current_block` with an empty `signature` field when a
   thinking block starts, so the dict shape matches Anthropic's spec
   (`{type, thinking, signature}`).
2. Handle `signature_delta` events by appending `delta.signature` to
   `current_block["signature"]`. Using `+=` (rather than assignment)
   mirrors how `thinking_delta` is accumulated and is robust against
   future chunked signatures.

No behavior change for clients that disable extended thinking, or for
upstreams that don't emit `signature_delta`. For `tool_use` threads
that require valid thinking signatures to preserve reasoning context,
this fix is required — the previous behavior silently corrupted them.

Verification:
- Before fix: upstream returns 400 + retry; cache_creation_tokens
  spike to ~25k on every 4th-5th request in a conversation
- After fix: upstream accepts the first attempt; cache_read_tokens
  dominate, cache_creation_tokens only appear on the first request
  of a fresh 5m prompt-cache window
2026-04-21 12:07:14 +08:00
LJQ 7ce39e29d0 Merge pull request #124 from ViviqwerAsd/docs/update-wechat-qr-codes-v2
docs: update WeChat community group QR codes
2026-04-21 11:52:37 +08:00
Xinyi Wang 09a78573f5 docs: update WeChat community group QR codes
- Replace 3 old QR codes (group 5/6/7) with 4 new ones (group 6/8/9/10)
- Update both English and Chinese sections in README.md
- Remove obsolete wechat_group5.jpg and wechat_group7.jpg
- Add wechat_group8.jpg, wechat_group9.jpg, wechat_group10.jpg
2026-04-21 11:47:55 +08:00
YooooEX ef9d218f91 feat: add Telegram slash command parity and menu sync 2026-04-21 11:43:53 +08:00
Liang Jiaqing 055fe882fc docs: add arXiv link (2604.17091) to README 2026-04-21 10:57:51 +08:00
Liang Jiaqing d25cfa5a2a refactor: vision_sop精简改写 + 新增vision_api.template.py 2026-04-20 23:22:36 +08:00
Liang Jiaqing 5c1dd1270c refactor stapp rerun reset logic and simplify vision SOP 2026-04-20 21:10:49 +08:00
LJQ bf4bc5d2ea feat: add /continue and /new command support to all chat frontends (#120)
feat: add /continue and /new command support to all chat frontends
2026-04-20 21:01:09 +08:00
Xinyi Wang d1a491ff3b feat: add continue/new support to chat frontends 2026-04-20 19:27:31 +08:00
Jiaqing Liang db3a807141 docs: add link to Datawhale hello-generic-agent tutorial 2026-04-20 16:26:07 +08:00
Jiaqing Liang 116d7d3d23 refactor: plugins dir + opt-in langfuse via __getattr__ guard
- mv langfuse_tracing.py -> plugins/langfuse_tracing.py
- llmcore: load plugin lazily inside __getattr__ when langfuse_config present
  (PEP 562 module __getattr__ naturally fires only once after globals().update)
- llmcore: extract _record_usage() from 4 scattered [Cache] print sites
- agentmain: /resume scans only latest 10 files
2026-04-20 15:56:06 +08:00
totoyangandtotoyang 8e6270e3a3 feat: optional Langfuse tracing for agent execution (#115)
Self-activating langfuse tracing via monkey-patch: independent module, zero impact when langfuse_config unset. Captures LLM generation, tool calls, token usage from SSE streams.

Co-authored-by: totoyang
2026-04-20 15:27:55 +08:00
Jiaqing Liang adc702d3f8 docs: add GitHub badge linking to GA-Technical-Report (data & reproduction repo) 2026-04-20 12:37:17 +08:00
Jiaqing Liang 765fcb6c2e docs: move disclaimer to License section (less intrusive) 2026-04-20 12:19:29 +08:00
Jiaqing Liang ae796482e8 docs: add disclaimer about unofficial commercial sites 2026-04-20 12:17:28 +08:00
Liang Jiaqing 63c7c83c51 refactor: update agent loop and tool extraction 2026-04-19 23:21:37 +08:00
Liang Jiaqing 86ca4625ad refactor: simplify HTTP error handling in _openai_stream, add non-stream support, broadcast history in MixinSession 2026-04-19 20:57:07 +08:00
LJQ d1f4ee9aaf Merge pull request #112 from shenhao-stu/main
feat: add /continue cmd
2026-04-19 19:15:41 +08:00
Shen Hao 8d07a0b4eb feat: Import chatapp_common for command activation
Added import for chatapp_common to enable command functionality.
2026-04-19 19:12:36 +08:00
Shen Hao faf5c9ce01 feat: Add imports for GeneraticAgent and continue command
Import GeneraticAgent and install continue command.
2026-04-19 19:11:39 +08:00
Shen Hao 9d5e4b18eb feat: update continue_cmd.py 2026-04-19 19:10:38 +08:00
Liang Jiaqing 2d753a6930 improve /resume: use regex to extract history blocks instead of naive tail reading 2026-04-19 17:15:38 +08:00
Liang Jiaqing 745220e62c Revert "Merge pull request #108 from ggandmee-cloud/feat/cch-signing"
This reverts commit 3e61155b33, reversing
changes made to 4abb8e205d.
2026-04-19 16:18:45 +08:00
LJQ 0214388d10 Merge pull request #110 from shenhao-stu/main
update mykey_template.py and add user_agent
2026-04-19 15:40:19 +08:00
Shen Hao 9217fee211 [feat]: Add user agent configuration to llmcore 2026-04-19 15:38:41 +08:00
Shen Hao 7c2785efe2 [config]: update mykey_template.py 2026-04-19 15:37:30 +08:00
LJQ 3e61155b33 Merge pull request #108 from ggandmee-cloud/feat/cch-signing
feat: NativeClaudeSession 在 fake_cc_system_prompt 模式下支持可选 CCH 签名
2026-04-19 15:27:13 +08:00
ggandmee-cloud c79b1c5140 feat: NativeClaudeSession 加入 CCH 签名
匹配真实 Claude Code 客户端协议,兼容需要验证客户端身份的代理。
2026-04-19 01:20:01 -04:00
Liang Jiaqing 4abb8e205d sys_prompt_en: 去掉强制英文,跟随用户语言 2026-04-19 11:29:30 +08:00
Liang Jiaqing c6319594c7 清理SOP: web_setup去TM方案, tmwebdriver排查流程优化 2026-04-19 11:16:29 +08:00
Liang Jiaqing 77d44c8cd0 update technical report 2026-04-19 10:03:32 +08:00
Liang Jiaqing 5d015d42c9 update technical report: language polish and table layout improvements 2026-04-18 19:58:56 +08:00
Liang Jiaqing f8a380d4fd fix: increase default timeout for non-stream mode (10/240s vs 5/30s) 2026-04-18 16:45:37 +08:00
Liang Jiaqing 2632478d96 fix: get_llm_name case-insensitive match & refactor to accept optional backend param 2026-04-18 14:46:05 +08:00
Liang Jiaqing c529abce3c fix: clarify start_long_term_update should only be called when task is completed (not mid-task) 2026-04-18 13:09:01 +08:00
Liang Jiaqing 11c41e7c4a refine memory_cleanup_sop: 4 compression principles, RULES triage, anti-intuitive trigger words, layered placement 2026-04-18 12:33:41 +08:00
Liang Jiaqing 3c2d22b948 refactor: move chat bubble asset to frontends/ and rename to English 2026-04-18 11:13:38 +08:00
Keeper 5d0d4b7d27 add: right-click of changing skin for desktop pet (#99)
* fix: skin icon size

* fix: skin icon size

* fix: skin icon size

* fix: skin icon size

* fix: skin icon size

* fix: skin icon size

* add: right click logic of changing skin
2026-04-18 11:00:56 +08:00
LJQ 184948b772 Merge pull request #89 from mmgnosit-commits/fix/minimax-timeout-retry
fix(llmcore): 添加MiniMax超时错误码529支持重试机制
2026-04-18 10:59:16 +08:00
weijia f49d69cf92 fix: 桌宠气泡 (#102)
* refactor: 精简desktop_pet_v2 — 内联_find_bubble_asset、删debug log、去重复RGBA检查

- 内联 _find_bubble_asset() 到 build_bubble_image (减少一层函数调用)
- 删除2处 /tmp/pet_toast_debug.log 写入
- 去除 Mac load_skin 中重复的 RGBA 转换检查
- 所有气泡核心逻辑(ImageOps.contain/非对称padding/alpha crop/tail扫描/定位公式)完整保留
- 785行 → 763行 (-22行)

* fix: 修复气泡文字碰边框问题,基于不透明区域计算文字padding

* feat: 添加自定义聊天气泡图片

* style: shift bubble text up 3px for better visual centering
2026-04-18 10:48:30 +08:00
Liang Jiaqing ec34b7e1c0 Add technical report link to README 2026-04-18 00:13:48 +08:00
Liang Jiaqing 13b15f3255 Add technical report PDF 2026-04-18 00:09:36 +08:00
Liang Jiaqing 593eaa8cf7 Add Trendshift trending badge to README 2026-04-17 21:12:34 +08:00
Junghwan adcfb8f730 fix(L4): preserve raw sessions skipped as recent(<2h) (#90)
compress_session.py Phase 4 builds to_del from both processed files
and every skipped file. Phase 1 marks files younger than 2h as
'recent(<2h)' (line 175) so their still-active writer is not
interrupted, but Phase 4 then deletes them anyway.

Filter the skipped-loop by reason so 'recent' files are preserved.
Processed files and other skip reasons (dup, compression error)
still proceed through deletion as before.
2026-04-17 17:35:50 +08:00
Jiaqing Liang 0e3e602121 fix: inline_eval exec() exception escaping crash; update schema description 2026-04-17 16:06:13 +08:00
Jiaqing Liang 47f106c8db refactor: extract slash cmd handler; fix inline_eval param name; support extra_sys_prompt 2026-04-17 15:39:51 +08:00
宋明明 04b4818f87 fix(llmcore): 添加MiniMax超时错误码529支持重试机制
## 问题描述
MiniMax API在请求超时时返回HTTP 529状态码,该状态码未被包含在可重试状态码集合中, 导致即使配置了max_retries参数,重试机制也对MiniMax超时无效。
当前行为:MiniMax 529 → 不重试 → 直接报错
预期行为:MiniMax 529 → 触发重试逻辑 → max_retries生效 ## 解决方案
在llmcore.py的RETRYABLE集合中添加529状态码。 ## 关于其他改动的思考 最初的想法是:将 LLM API 重试的 HTTP 状态码从硬编码提取为可配置文件
assets/http_retry_codes.json,首次运行时自动生成默认配置

### 之前的初衷(配置文件方案)
- ✓ 不修改代码即可适配新的错误码
- ✓ 降低维护成本
### 现实考量(硬编码方案)
- 主流模型厂商就那几家:OpenAI、Claude、MiniMax、Kimi等 -
标准的超时错误码基本固化:408(timeout)、429(rate_limit)、5xx(server_error)
- MiniMax 529是特例但不频繁变化
- 硬编码更简洁直接,维护更清晰

## 受影响范围
- llmcore.py: _openai_stream() 函数的重试机制现在支持MiniMax 529错误
- 对应MiniMax API的超时场景现在能正确触发retry逻辑
2026-04-17 15:09:52 +08:00
Jiaqing Liang f418963585 refactor: omit temperature from payload when default (1), avoid errors on reasoning models 2026-04-17 13:11:57 +08:00
Liang Jiaqing 9dbb3315da docs: add English-only CONTRIBUTING.md 2026-04-17 10:46:09 +08:00
Liang Jiaqing b1f72a202e fix: clarify file_write usage — content before call 2026-04-17 00:04:12 +08:00
Liang Jiaqing aa674655a8 refactor: unify docstrings, add /resume command, simplify error msg 2026-04-16 21:04:04 +08:00
LJQ e7e560715e Merge pull request #80 from shenhao-stu/main
docs: update mykey_template.py
2026-04-16 19:52:02 +08:00
LJQ 2c0146232a Merge pull request #79 from ViviqwerAsd/docs/update-wechat-group-qrcodes
docs: 更新微信群二维码 (group 5/6/7)
2026-04-16 19:50:42 +08:00
Shen Hao e4e84f2b43 docs: update mykey_template.py 2026-04-16 19:50:11 +08:00
Xinyi Wang 753bde713f docs: update WeChat group QR codes (group 5/6/7) in README Support section 2026-04-16 19:45:57 +08:00
LJQ f6deb83fe0 Merge pull request #78 from AspasZhang/feat/update-plan-verify-sop
feat: update plan_sop & verify_sop
2026-04-16 19:18:14 +08:00
Liang Jiaqing c43c07487f feat: add English version of global_mem_insight_template 2026-04-16 18:52:07 +08:00
Liang Jiaqing 7cadbd7403 feat: i18n support - auto-detect system language for zh/en prompts 2026-04-16 18:47:40 +08:00
张洲嘉 82b8535b7c feat: update plan_sop & verify_sop
- plan_sop: improved planning workflow
- verify_sop: rewritten inspired by Claude Code verification agent design
  - Added two failure mode anchoring (verification avoidance & seduced by first 80%)
  - Anti-rationalization checklist
  - BEFORE PASS/FAIL guardrails
  - Compressed from 104 to 65 lines while increasing coverage
2026-04-16 18:42:03 +08:00
Liang Jiaqing 47bcb1b01a fix: compatible with Streamlit 1.56+ iframe API change 2026-04-16 17:48:58 +08:00
Liang Jiaqing 9e18ce26dc fix: empty file ZeroDivisionError in file_read; remove SiderLLMSession; init global_mem with header 2026-04-16 16:04:18 +08:00
Liang Jiaqing 8e2a96167e docs: add token efficiency highlight to Core Features (EN+CN) 2026-04-16 15:47:41 +08:00
LJQ 315ce293df Merge pull request #76 from kuishou68/fix/issue-75-scheduler-errors
fix: guard _parse_cooldown against malformed repeat values; close file handle with context manager
2026-04-16 13:22:48 +08:00
Liang Jiaqing 0591b68ff6 stapp: incremental freeze rendering for streaming segments
Replace full-redraw approach with frozen/live slot pattern:
- Completed segments are frozen into individual st.empty() slots
- Only the last (active) segment is re-rendered on each tick
- Reduces flicker and improves performance for long multi-turn conversations
2026-04-16 13:10:00 +08:00
Liang Jiaqing 3d7832a750 feat: multi code_run support, inline_eval logging & cwd fix, tool reinject upgrade 2026-04-16 12:56:17 +08:00
Cocoon-Break fefd9b99cc fix: guard _parse_cooldown against malformed every_ repeat; close file handle with context manager (Closes #75)
Signed-off-by: Cocoon-Break <54054995+kuishou68@users.noreply.github.com>
2026-04-16 11:31:35 +08:00
Liang Jiaqing a3c15c54af docs: add Star History chart to README 2026-04-16 07:58:48 +08:00
Liang Jiaqing 85480b16a6 docs: use wechat_group4.jpg to bypass CDN cache 2026-04-15 23:57:11 +08:00
Liang Jiaqing 4d6b8cd982 docs: update wechat group labels (Group 4 + Group 3) 2026-04-15 23:54:57 +08:00
Liang Jiaqing 39f90149fb fix: change desktop pet port 51983->41983 (avoid Hyper-V reserved range) & update wechat group QR code 2026-04-15 23:50:48 +08:00
Liang Jiaqing bdd2f3c2cd docs: add Chinese illustrated getting-started guide 2026-04-15 23:22:45 +08:00
Liang Jiaqing bd9e45f761 feat(file_read): fuzzy file suggestion on not-found with history dirs; sync L1 template 2026-04-15 20:58:29 +08:00
Liang Jiaqing da8bba99a5 fix: batch_process missing raw_dir arg causing L4 archive to fail 2026-04-15 18:31:29 +08:00
Jiaqing Liang e3a2526932 feat: add plan mode (enter/exit/completion check/verification gate/periodic hints) 2026-04-15 17:02:54 +08:00
LJQ d482f30188 Merge pull request #69 from kagura-agent/feat/github-contribution-sop
feat: add GitHub contribution SOP (memory/github_contribution_sop.md)
2026-04-15 15:42:22 +08:00
Kagura b2465df4e0 feat: add GitHub contribution SOP 2026-04-15 15:06:04 +08:00
wjl2023 bb0290f576 fix: normalize bubble text and adjust padding for better readability 2026-04-15 14:43:14 +08:00
Jiaqing Liang 16fe369f5a preserve history across llm switch 2026-04-15 14:34:06 +08:00
Jiaqing Liang d111db5454 fix prompt flow 2026-04-15 13:57:36 +08:00
Liang Jiaqing 273150a9ce feat: publish memory_cleanup_sop 2026-04-15 09:26:46 +08:00
LJQ 39bc29d93d Merge pull request #67 from shenhao-stu/main
config: update mykey_template.py with new configuration examples
2026-04-15 09:24:21 +08:00
Shen Hao ee4437d053 bug: fix a tiny bug about crs antigravity's config 2026-04-15 04:28:27 +08:00
Shen Hao 222a144858 config: update mykey_template.py with new configuration examples 2026-04-15 03:54:42 +08:00
Liang Jiaqing 97560a4fba feat: publish ocr_utils, vision_sop, ui_detect to public repo 2026-04-15 00:00:14 +08:00
Liang Jiaqing d0a9b5de08 fix: use script_dir for cwd path instead of relative path 2026-04-14 23:38:32 +08:00
Liang Jiaqing d18c8438d3 refactor: use with statement for requests.post in NativeClaudeSession 2026-04-14 23:35:51 +08:00
Liang Jiaqing 74abb77a0b refactor: rename default_model->model, add thinking_type support, fix pet urlopen error handling 2026-04-14 21:14:05 +08:00
Liang Jiaqing 0551967a7e style: arrange 3 QR code images horizontally in a table row 2026-04-14 18:47:00 +08:00
LJQ 2e0be429d9 Merge pull request #63 from JinyiHan99/docs/add-group-qr-codes
docs: add community group QR codes to README
2026-04-14 15:12:39 +08:00
Jinyi Han 9f004c059d docs: add community group QR codes to README 2026-04-14 15:08:22 +08:00
Liang Jiaqing 793bb3f1b8 refactor(pet): extract PetBase, deduplicate _start_server/safe wrappers, add singleton guard 2026-04-14 12:05:55 +08:00
LJQ fae778bb51 Merge pull request #62 from shenhao-stu/main
docs: add Linuxdo links
2026-04-14 11:13:37 +08:00
Shen Hao 5179fb2fac Merge branch 'lsdefine:main' into main 2026-04-14 11:10:31 +08:00
Shen Hao 7cb3982d89 docs: add friendly links section to README
Added a section for friendly links and community support.
2026-04-14 11:09:49 +08:00
Liang Jiaqing 0b54df85be feat: desktop pet v2 with 8 skins and state notifications 2026-04-14 11:07:58 +08:00
Shen Hao 3cc05be36b docs: add links to LinuxDo and support section
Added links to LinuxDo community and support section.
2026-04-14 11:07:38 +08:00
Liang Jiaqing acb0f93d5f refactor: reorder mykey_template, add name field to native configs 2026-04-13 22:48:51 +08:00
Liang Jiaqing 127a4e631c feat: add /session.key=value system command to set LLM params at runtime 2026-04-13 22:44:17 +08:00
Liang Jiaqing 499f0119bb refactor llm session params and thinking parsing 2026-04-13 20:20:31 +08:00
Liang Jiaqing 0da9bd15c9 refactor: turn_end_callback + desktop pet + keychain
- agent_loop: next_prompt_patcher -> turn_end_callback with full context
- agent_loop: exit logic unified (break + callback), no early return
- ga: summary extraction moved from tool_after_callback to turn_end_callback
- ga: _turn_end_hooks support for external subscribers
- stapp: desktop pet button with HTTP status push
- keychain: XOR-masked secret storage with SecretStr
- gitignore: whitelist keychain.py
2026-04-13 18:27:17 +08:00
LJQ 9da32c07ce docs: update README recent highlights and group image (#59)
docs: update README recent highlights and group image
2026-04-13 16:20:56 +08:00
Xinyi Wang c80cc2d823 docs: update README recent highlights and group image 2026-04-13 16:05:04 +08:00
Jiaqing Liang 086599a5d6 fix: scroll ghost height reflow via overflow toggle; extend cache markers to last 2 user msgs; simplify cursor & merge JS fixes 2026-04-13 14:59:38 +08:00
Liang Jiaqing ecb8ac9a80 fix(stapp): eliminate ghost duplicate of last text block on new message
Root cause: stream loop emitted a bare st.empty() heartbeat per tick, accumulating
dozens of empty slots under chat_message. On the next rerun the history replay
path had no such slots, so Streamlit's incremental DOM diff misaligned and left
the previous render of the last text block as a grayed-out ghost.

Fix: unify streaming and history replay under the same shell
(slot = st.empty(); with slot.container(): render_segments(...)). Heartbeat now
re-enters the same slot each tick; when response is unchanged Streamlit's diff
is a no-op (no flicker), but the container() call still lets StopException
propagate so abort keeps working. Simplified render_segments from 5 params
(placeholders/rendered_cache/force_text) to 2 (segments, suffix).

Net: +19 -20 lines. Verified: no ghost, abort interrupts mid-stream, per-turn
fold-collapse preserved.
2026-04-12 15:22:56 +08:00
Liang Jiaqing d94e404f64 fix: use local var for claude tools conversion, avoid mutating self.tools 2026-04-12 14:46:45 +08:00
Liang Jiaqing 142de0c45b refactor: remove claude_tools_format flag, auto-convert tools in NativeClaudeSession.raw_ask 2026-04-12 14:43:46 +08:00
Liang Jiaqing 2b5cbff7be feat: --verbose flag, lazy mykeys loading, temperature config support
- agentmain: add --verbose arg for subagent monitoring mode
- llmcore: lazy-load mykeys/proxies via module __getattr__
- llmcore: fix auto_make_url regex for trailing slash cases
- llmcore: support temperature override from session config
- docs: update subagent.md with --verbose usage note
2026-04-12 14:29:32 +08:00
Liang Jiaqing 9e03a675ae fix: MixinSession copy before override & add no-tools warning; refine subagent SOP 2026-04-11 18:50:26 +08:00
Liang Jiaqing 946c239c03 refactor: rename mem_scanner→procmem_scanner, add L4 layer, remove make_prompts.py 2026-04-11 18:30:00 +08:00
Liang Jiaqing 6f1585e88f fix: comment out MixinSession copy to prevent tools not propagating to original session 2026-04-11 18:25:54 +08:00
Liang Jiaqing 62ac73c773 feat: task模式干预机制 _stop/_keyinfo/_intervene + consume_file 2026-04-11 17:14:35 +08:00
Liang Jiaqing 3531146792 fix: restore stop signal detection broken by render_segments cache optimization
- stapp.py: add st.empty() after render_segments to force Streamlit StopException check on every iteration (incl. heartbeat)
- agentmain.py: fix nround type check, fix timeout comment
2026-04-11 16:25:19 +08:00
Liang Jiaqing de8adf76a9 feat: L4 session archiver + scheduler cron integration
- Add compress_session.py: compress raw model_responses into L4 archives
- Integrate 12h silent cron into scheduler.check() (runs before TASKS dir check)
- Whitelist compress_session.py in .gitignore (archives excluded)
- llmcore: refactor SSE warn handling, max_retries default 2->1
- scheduler: remove unused health_check(), INTERVAL 60->120
2026-04-11 15:55:35 +08:00
Liang Jiaqing a737523f0a llmcore: remove runtime model param, upgrade Claude beta headers, softer backoff 2026-04-11 14:34:27 +08:00
Liang Jiaqing 7bfd6e43e6 fix: _fix_messages for Claude API compliance, raw_ask simplify, no_tool orphan fix, summary extraction improvement 2026-04-11 13:24:33 +08:00
Liang Jiaqing 5e28902cb4 cleanup: rm user.js, untrack copilot_proxy.pyw; ljqCtrl: DPI via GDI + GrabWindow(hwnd) 2026-04-10 22:17:24 +08:00
Jiaqing Liang 5a1d3a41da fix: handle window object serialization in CDP bridge; improve file_write error msg; minor llmcore style cleanup 2026-04-10 14:04:41 +08:00
Liang Jiaqing 4b18ad683f Fix tool call conversion and working checkpoint result 2026-04-10 10:45:00 +08:00
Liang Jiaqing cc283a96f3 fix: verbose logic & insight wording & file_read anchor skip 2026-04-10 09:56:22 +08:00
Liang Jiaqing 2977be33c6 Update llmcore truncation, adb_ui parsing, stapp regex, and subagent docs 2026-04-09 18:43:20 +08:00
Liang Jiaqing 6628a3d987 tune: read_timeout 60s, simphtml print/newTabs fix, remove obsolete iframe sop 2026-04-08 20:45:06 +08:00
Liang Jiaqing 1c561db0b9 feat: enhance CDP bridge management and fix JS falsy returns
- background.js: Add management API, auto-return for JS, refactor new tab capture
- simphtml.py: Fix falsy return value bug, flatten return structure
- agent_loop.py: Simplify UI icons
- manifest.json: Add management permission
- tools_schema: Optimize prompts for web tools
- tmwebdriver_sop.md: Update documentation
2026-04-08 18:28:33 +08:00
LJQ 813eb29692 Merge pull request #57 from YangChen-pro/fix/restore-native-compat
修复 /restore 的日志路径兼容,并支持 Native 日志恢复
2026-04-08 15:45:15 +08:00
YangChen-pro 520550a2b9 Fix /restore path and native log compatibility 2026-04-08 10:16:55 +08:00
Liang Jiaqing f4eec277c5 docs: fuzzy numbers in README (3K/~100/9 tools) 2026-04-07 22:18:18 +08:00
Liang Jiaqing 1e39032c67 refactor: optimize non-verbose output - tool icons, compact args, clean content 2026-04-07 19:03:43 +08:00
Jiaqing Liang 25fe3e42fd fix: popup.js action->cmd; add disable_dialogs.js with toast; update manifest 2026-04-07 12:17:18 +08:00
Liang Jiaqing b525684378 fix: hoist filter should not catch sticky elements or oversized containers
- Remove position:sticky from hoist criteria (sticky is scroll-following, not popup)
- Add cover < 1.0 upper bound (real dialogs do not exceed viewport area)
2026-04-07 10:17:55 +08:00
Liang Jiaqing 054674baa1 simphtml: elementFromPoint弹窗修复 + findMainList评分优化 + _ljq ID注入 + smart_truncate重写 2026-04-07 10:10:27 +08:00
Liang Jiaqing b4741a9a39 Optimize: force aggressive tag compression before history truncation to save context 2026-04-06 22:50:23 +08:00
Liang Jiaqing 53a859058a Refactor: simplify smart_format to string-only and enhance tool output truncation limits 2026-04-06 22:50:23 +08:00
LJQ f44c05931c Merge pull request #55 from JinyiHan99/update-wechat-group-image
Update wechat_group.jpg
2026-04-06 21:57:09 +08:00
Jinyi Han 34d945d425 Update wechat_group.jpg 2026-04-06 21:52:52 +08:00
Liang Jiaqing 8e829730a9 smart truncation, iframe preservation, streamlit filter
- simphtml: truncate_biggest replaces naive string cut, targets fattest content block via 70% drill-down
- simphtml: iframe children preserved through div proxy (bypass BS4 limitation)
- background/content.js: filter streamlit tabs and skip content script injection
- ga.py: file_content tag regex allows attributes; maxchars 38k->35k
2026-04-06 12:19:20 +08:00
Liang Jiaqing aae6d810cd NativeClaudeSession: auto-convert OpenAI tools format to Claude; skip in NativeOAISession 2026-04-04 07:03:18 +08:00
Liang Jiaqing 14125ed57c refactor: move tools ownership from NativeToolClient to Session layer
- tools state now held by Session (NativeClaudeSession.tools)
- MixinSession.__setattr__ broadcasts tools/system to all sub-sessions
- NativeToolClient no longer duplicates tools storage
- fix: use type(s) is instead of isinstance to avoid catching NativeOAISession subclass
2026-04-03 23:05:28 +08:00
Liang Jiaqing 555eeabf56 simphtml: SVG className fix, CSS.escape, DOMRect compat, cutlist tuning; tmwebdriver_sop update 2026-04-03 22:34:41 +08:00
Liang Jiaqing 97abc43a40 refactor(simphtml): rewrite list detection & cutlist for multi-list support
- simphtml: replace center-point ancestor-chain approach with global container scan;
  support multiple lists per page; add container-scoped selector prefixes;
  inline FAKE ELEMENT hints with hidden item previews; remove findMainContent
- ga: hot-reload simphtml on each web_scan; fix file_read total_lines for keyword search;
  add errors='replace' for global_mem encoding safety
- llmcore: stabilize NativeClaude session/device IDs across requests;
  rename no_system_prompt to fake_cc_system_prompt; deep-copy message content
- launch: adjust window width 700->600
2026-04-03 19:38:51 +08:00
Liang Jiaqing 4a5043a658 Refactor: unify render_segments logic, add streaming cursor animation, and clean up unused container parameter 2026-04-03 12:14:08 +08:00
Liang Jiaqing 4e2b806917 Refactor: use native st.expander for folding turns, optimize stream trigger, and adjust window width 2026-04-03 11:47:07 +08:00
Liang Jiaqing 5bc763d319 fix(simphtml): improve bounding box calculation for transparent containers by using children union rect 2026-04-03 11:14:25 +08:00
Liang Jiaqing da40ba413b feat: fold_turns UI折叠 + 放宽输出限制 + 压缩history/key_info标签 + context_win调大 2026-04-03 10:43:15 +08:00
Liang Jiaqing 8d537e954b fix: simphtml visibility/dialog/SVG improvements & minor agent fixes
- simphtml: rewrite visibility inheritance (area-based maxC), add isVisible to childrenInfo,
  fix className SVG compat (getAttribute), expand isDialog detection, hoist deep fixed dialogs,
  clear SVG attrs, fix warning message concat
- ga: return error messages instead of None for code_run/web_execute_js failures
- stapp: disable unsafe_allow_html
- code_run_header: add excepthook hint for ImportError/AttributeError
- mykey_template: remove stale prompt_cache comment
2026-04-03 09:40:09 +08:00
Liang Jiaqing b0d4563ae8 Refine llmcore debug and tool parsing 2026-04-02 21:16:45 +08:00
Liang Jiaqing 88f32b208b feat: support NativeToolClient and optimize tool use format for native API 2026-04-01 23:09:48 +08:00
Liang Jiaqing 629e57ad83 Refactor: 统一消息格式和Session架构重构
核心改动:
- 统一所有Session内部使用Claude content-block格式
- 引入BaseSession基类,简化代码结构
- tool_results从字符串改为结构化字典列表
- NativeClaudeSession增强:支持cr_token、metadata、thinking提取
- ToolClient简化:删除structured分支,统一使用protocol prompt
- MixinSession支持按名称选择session
- ljqCtrl_sop增加DPI坐标陷阱警告
2026-04-01 22:20:18 +08:00
Liang Jiaqing 368d68baa5 fix: remove stray brace in tool_pattern regex 2026-04-01 09:47:53 +08:00
Liang Jiaqing 1c38412e42 revert: 恢复工具调用正则最小长度限制 2026-04-01 09:35:37 +08:00
Liang Jiaqing 404ea1947b fix: llmcore工具调用正则兼容tool_call标签,最小长度改为0 2026-04-01 09:30:55 +08:00
Liang Jiaqing d922303645 TMWebDriver: add debug logs + CDP bridge refactor (action→cmd, extract buildExecScript, handleWsExec routing) 2026-03-31 20:37:05 +08:00
Jiaqing Liang 86210296c1 fix: MV3 SW keepalive for WS connection via periodic alarm ping 2026-03-31 11:19:48 +08:00
Liang Jiaqing 106c6b04f1 docs: update save_to_file desc, add iframe scan note, cdp_bridge notice 2026-03-31 09:04:22 +08:00
Liang Jiaqing 72a85a0823 fix: buildCdpScript add smartProcessResult to prevent CDP serialization error on complex DOM (GitHub issues, Gmail etc); update SOP docs 2026-03-30 22:30:34 +08:00
Liang Jiaqing 18d5aafed8 gitignore: universal __pycache__ exclusion, remove tracked .pyc files 2026-03-30 20:39:46 +08:00
Liang Jiaqing 117028b5d9 simphtml: refine pathType detection, simplify secondary marking, init response, reduce sleep 2026-03-30 20:37:27 +08:00
Liang Jiaqing f320d141a0 feat: CDP bridge extension integration - ext_ws session type, DOM-channel CDP/batch/cookies/tabs, autofill bringToFront fix, await support in SOP 2026-03-30 20:01:27 +08:00
Liang Jiaqing bc5d1eafaf feat: add --bg/--input flags to agentmain, rename & simplify subagent SOP 2026-03-30 15:03:28 +08:00
Liang Jiaqing 6cec6d8014 feat: autonomous SOP refactor + helper.py with auto date/numbering
- Refactored autonomous_operation_sop.md with clearer path warnings and workflow
- Added helper.py: complete_task() auto-prepends R-number + date to history
- Updated scheduled_task_sop.md and web_setup_sop.md
2026-03-30 14:30:30 +08:00
Liang Jiaqing 473659ba6e refactor: agent loop done_hooks, inline_eval, scheduler logging & health_check
- agent_loop: for→while, _done_hooks callback mechanism, max_turns=40
- agentmain: pass self to handler, abort clears task queue
- stapp: heartbeat yield for Streamlit StopException detection
- ga: _inline_eval param for in-process eval, no_tool thresholds tuned
- scheduler: logging, max_delay_hours, weekday repeat, health_check()
2026-03-30 13:51:21 +08:00
Liang Jiaqing 7b704b5dbc fix: _parse_openai_sse防御None值迭代(tool_calls/delta可能为null) 2026-03-30 11:19:56 +08:00
Liang Jiaqing 5459ebbb59 fix: output窗口滚动漂移 - 用行偏移代替fraction恢复位置,无变化时跳过刷新 2026-03-29 21:32:40 +08:00
Liang Jiaqing f0dea09123 fix: fallback tool_use parser handles both with/without space in type key 2026-03-29 21:04:34 +08:00
Liang Jiaqing 58e2f45793 fix: dump tool_use as array format to match fallback parser 2026-03-29 20:54:25 +08:00
Liang Jiaqing 844573387d fix: convert tool_use blocks to text in NativeOAISession history to avoid API content type error 2026-03-29 19:08:20 +08:00
Liang Jiaqing 08ac18be6e fix: code_run encoding compat; wechat truncate long code blocks & limit msg count 2026-03-29 17:43:06 +08:00
Liang Jiaqing 52151853b3 fix: encode str input when text=True is intercepted by header wrapper 2026-03-29 15:15:35 +08:00
Liang Jiaqing 53c492b682 agentmain: auto-switch cn schema for glm/minimax/kimi; tools_schema: trim descriptions; hub: fix scroll position on refresh 2026-03-29 15:08:11 +08:00
Liang Jiaqing 18ef86b69d llmcore: move logs to temp/model_responses/ subdir; enhance error prefix detection 2026-03-29 15:06:19 +08:00
LJQ d76842c549 Merge pull request #52 from octo-patch/feature/add-minimax-provider
feat: add MiniMax as first-class LLM provider
2026-03-29 14:51:02 +08:00
Liang Jiaqing fe7e4c0952 refine: tool schema descriptions, docstring format, context_win 18000→20000 2026-03-29 13:06:06 +08:00
Liang Jiaqing 45679509ea MixinSession: spring-back to primary + translate tools_schema to English 2026-03-29 12:48:36 +08:00
Liang Jiaqing 226fa69cc3 refactor MixinSession._raw_ask: simplify retry logic, skip yielding first-only error chunk 2026-03-29 10:53:26 +08:00
PR Bot b6f697d40f feat: add MiniMax as first-class LLM provider
- Temperature auto-clamping for MiniMax models: (0, 1] range enforcement
- <think> tag handling for MiniMax M2.7 reasoning output (alongside existing <thinking> support)
- MiniMax configuration example in mykey_template.py
- Updated README.md and GETTING_STARTED.md with MiniMax provider docs
- 19 unit tests + 6 integration tests (3 live tests with MINIMAX_API_KEY)

MiniMax models (M2.7, M2.7-highspeed, M2.5, M2.5-highspeed) are accessed via
the standard OpenAI-compatible interface at https://api.minimax.io/v1, using
the existing LLMSession with an 'oai'-prefixed config key.
2026-03-29 07:09:46 +08:00
Liang Jiaqing 1d94384ee5 feat: add Qt/Streamlit alternative frontends, fix hub output auto-scroll, update README 2026-03-28 20:56:14 +08:00
Liang Jiaqing a5dbce396a refactor: remove XAI/Gemini sessions, add MixinSession fallback, improve error handling
- Remove GeminiSession and XaiSession from llmcore.py
- Add MixinSession: multi-endpoint fallback with exponential backoff
- ga.py: handle empty/incomplete/max_tokens responses
- Update mykey_template.py and GETTING_STARTED.md accordingly
2026-03-28 08:14:35 +08:00
Liang Jiaqing 38e2a460e3 refactor: optimize code extraction priority in tool handlers & update autofill SOP 2026-03-27 19:43:46 +08:00
Liang Jiaqing a1c289dbd1 feat(web_driver): support await in executeCode via AsyncFunction fallback 2026-03-27 18:57:47 +08:00
Liang Jiaqing 55a5eae4f6 perf: replace MutationObserver with setInterval to fix performance issue 2026-03-27 18:41:39 +08:00
Jiaqing Liang 04448dcee9 update userscript guard and scheduler flag 2026-03-27 15:41:42 +08:00
Jiaqing Liang f6c5011eb0 refactor: rewrite GETTING_STARTED, cleanup README, add hub.pyw, remove QUICK_START.pdf 2026-03-27 12:58:13 +08:00
Liang Jiaqing ff258dd0a9 refine: context window, timeout, abort guard, code_run multi-call check, clean logs 2026-03-26 21:31:48 +08:00
Liang Jiaqing 47ad98239f enhance SSE parsing, add stream error/truncation detection, fix tool_call fallback extraction 2026-03-26 18:42:09 +08:00
Liang Jiaqing 650c14dc1a update wechat group QR code (group 2, as group 1 is full) 2026-03-26 13:37:41 +08:00
Liang Jiaqing 551666a53b refactor: unify OpenAI streaming into _openai_stream(), add Responses API support
- Extract shared _openai_stream() from LLMSession and NativeOAISession
- Add Responses API compatibility: system->developer role, flat tools format, no temperature
- Improve error handling: read body before raise_for_status in stream mode
- Add retry mechanism to NativeOAISession (was missing)
- Add api_mode, connect_timeout, read_timeout config to NativeOAISession
2026-03-25 23:50:28 +08:00
Liang Jiaqing 507e886322 refactor llmcore: extract _parse_claude_sse, simplify ToolClient, enhance logging and context trimming 2026-03-25 21:41:33 +08:00
Liang Jiaqing 1b60d2c773 minor: tweak prompt wording, improve OAI history compression 2026-03-25 10:00:16 +08:00
Liang Jiaqing 85250c2694 fix: unify assistant content format for prompt caching to work 2026-03-25 09:45:43 +08:00
Liang Jiaqing b92d2bc97d fix: always compress history tags, adjust context trim thresholds 2026-03-25 08:37:46 +08:00
Liang Jiaqing ddce7ef549 feat: reflect hot-reload + enhance prompt caching & history compression 2026-03-24 22:58:20 +08:00
Liang Jiaqing 274bbcb9e4 docs: update WeChat group QR code 2026-03-24 22:55:04 +08:00
Liang Jiaqing 28f22c3973 feat: add prompt caching for NativeClaudeSession 2026-03-24 20:00:44 +08:00
Liang Jiaqing f9a0aa71fa fix: add tool_result placeholder for NativeClaudeSession to prevent 502 2026-03-24 19:54:27 +08:00
Liang Jiaqing 651e0d2bc8 refactor: extract code_run header to external file, add subprocess gbk fallback patch 2026-03-24 09:52:55 +08:00
Liang Jiaqing 5643bfb5f0 code_run自动注入常用import+memory路径; ljqCtrl_sop补充文本输入方法 2026-03-24 00:00:20 +08:00
Liang Jiaqing 515b0cd985 feat: unified media download in wechatapp + proxy support for NativeOAISession 2026-03-23 20:57:39 +08:00
Liang Jiaqing 60f40e3c5e feat(wechat): add send_file via CDN upload; fix _strip_md robustness
- Add WxBotClient.send_file() with AES-128-ECB encryption and CDN upload
- Extract [FILE:] tags before _clean() to preserve filenames
- Fix _strip_md: support 3+ backticks via backreference, stop stripping underscores
- Remove verbose key_info echo from update_working_checkpoint
2026-03-23 17:55:20 +08:00
Jiaqing Liang eb8834b9df feat: enhance text_only mode - add form element annotations, block-level line breaks, and whitespace cleanup 2026-03-23 15:26:05 +08:00
Jiaqing Liang b4bdaf11da style: remove trailing periods in tool schema descriptions (-31 tokens/round) 2026-03-23 13:56:03 +08:00
Jiaqing Liang 47e9a55feb feat: add file ref expansion syntax for file_patch and file_write 2026-03-23 13:50:11 +08:00
Jiaqing Liang 66dff812fb docs: add personal WeChat bot frontend to Latest News 2026-03-23 12:05:23 +08:00
Jiaqing Liang 4b4dc51336 fix: tool_use regex negative lookahead to prevent cross-tag matching; tgapp relative path resolve 2026-03-23 11:46:10 +08:00
Liang Jiaqing 6f6d9f6570 feat: add NativeOAISession + minor fixes (code_run script fallback, thinking prompt tags) 2026-03-22 22:19:46 +08:00
Liang Jiaqing fd5150ffad feat: strip markdown formatting in wechat output 2026-03-22 20:45:40 +08:00
Liang Jiaqing ba6ab901f8 feat: add WeChat bot frontend (wechatapp.py) 2026-03-22 20:20:33 +08:00
Liang Jiaqing 8794c85cfe docs: update paths for frontends/ migration 2026-03-22 17:47:56 +08:00
Liang Jiaqing 87d557260c refactor: move frontend apps to frontends/ and fix path references 2026-03-22 17:45:20 +08:00
Liang Jiaqing a9f0e9628f fix: llm session tweaks & agent init refactor 2026-03-22 09:55:24 +08:00
Liang Jiaqing 122b6c431c improve mykey_template: add apibase rules, all session types with full cfg comments 2026-03-21 12:23:58 +08:00
Liang Jiaqing fdc24e4dd0 add NativeClaudeSession+NativeToolClient, support parallel tool calls, fix tool_calls[-1:], add llm log 2026-03-21 12:13:46 +08:00
Liang Jiaqing 85f17db487 feat: prompt_cache skip compress + style 2026-03-20 23:47:15 +08:00
Jiaqing Liang 05800a4f48 Merge PR #40: Fix feishu group chat reply and add docs 2026-03-19 14:42:20 +08:00
Jiaqing Liang 4ebb421fd5 Merge PR #39: Add reasoning_effort config support 2026-03-19 14:38:23 +08:00
Jinyi Han 34452e2d4c fix: 飞书机器人群聊回复逻辑 2026-03-19 09:57:00 +08:00
YangChen-pro 70ed09ad2b Support configurable OpenAI reasoning effort 2026-03-18 22:54:58 +08:00
Jinyi Han 34abe90bb2 Merge upstream/main with local fsapp.py preserved 2026-03-18 12:15:49 +08:00
Liang Jiaqing 00038cd1ae refactor: unify LLM session constructors to accept cfg dict 2026-03-17 16:52:12 +08:00
Jiaqing Liang d896355bad fix: key_info regex residue accumulation bug - non-greedy match stopped at first 工作记忆 leaving tail behind 2026-03-16 13:57:52 +08:00
Jiaqing Liang 065a5ee4bf refactor: working memory dict, merge chat paths, simplify SKILL docs 2026-03-16 13:18:41 +08:00
Jiaqing Liang 3e6be97ec9 feat: add IME composition fix for macOS (closes #33)
Based on PR #33 by UnsongK. Compressed to 4 lines with OS guard
so Windows users are completely unaffected.
2026-03-16 13:15:48 +08:00
Jinyi Han 806f1e9767 update the operation in feishu app 2026-03-15 23:15:13 +08:00
LJQ 5fdc2c60b2 Merge pull request #31 from wjl2023/fix-readme-date
fix: Update release date to 2026-03-10
2026-03-14 21:14:44 +08:00
wjl2023 523ce6133e fix: Update release date to 2026-03-10 in README 2026-03-14 21:00:05 +08:00
LJQ da00664a00 Merge pull request #29 from wjl2023/fix-moonshot-temperature
fix: Add moonshot API temperature=1.0 constraint
2026-03-14 20:47:44 +08:00
wjl2023 2809a54b29 fix: Add moonshot API temperature=1.0 constraint
- Moonshot API (like Kimi) only accepts temperature=1.0
- Added 'moonshot' detection alongside 'kimi' in both raw_ask methods
- Prevents 400 errors when using moonshot models
2026-03-14 20:44:29 +08:00
Liang Jiaqing 68a936eb2c debug print result; kimi temp=1.0 compat 2026-03-14 20:34:57 +08:00
Liang Jiaqing bbde5399cf add skill_search SOP to tracked memory files 2026-03-14 20:33:54 +08:00
Liang Jiaqing afa288f240 docs: add note about 2 memory management tools alongside 7 atomic tools 2026-03-14 08:53:11 +08:00
Liang Jiaqing 75a0e86820 docs: update README with enhanced structure, news section, bot interfaces, and comparison table (from hjy) 2026-03-14 08:49:07 +08:00
Jinyi Han 0839edd134 update the readme 2026-03-14 01:49:03 +08:00
Jinyi Han d8a32313a8 update the readme 2026-03-14 01:47:29 +08:00
Jinyi Han 80ced3081d update the readme 2026-03-14 01:45:14 +08:00
Jinyi Han ab84bc686c update the readme 2026-03-14 01:42:41 +08:00
Jinyi Han cc450a87ab update the readme 2026-03-14 01:14:51 +08:00
Jinyi Han 7977820500 update the readme 2026-03-14 01:12:22 +08:00
Jinyi Han 5a9736b7f0 update the readme 2026-03-14 00:52:51 +08:00
LJQ d5f1d03a59 Add MIT License 2026-03-13 22:31:29 +08:00
Liang Jiaqing 90bfd2324a Remove license section from README 2026-03-13 22:14:37 +08:00
Liang Jiaqing 0100bf10af refactor: replace insight template comments with real out-of-box entries 2026-03-13 20:50:04 +08:00
Liang Jiaqing 908c1a52ed perf: localhost→127.0.0.1 fix IPv6 2s delay; simphtml radio/checkbox/svg/dropdown fixes; poll interval 0.5→0.2s 2026-03-13 20:13:33 +08:00
LJQ 081aa12657 Merge PR #25: Refactor chat app adapters with shared chatapp_common
Refactor text-first chat app adapters to reduce duplicated scaffolding
2026-03-13 18:28:27 +08:00
Xinyi Wang 44bda94a36 Refactor Feishu adapter to trim duplicated helpers 2026-03-13 16:51:50 +08:00
Xinyi Wang f16b5b7214 Refactor chat app adapters to reduce duplicated scaffolding 2026-03-13 16:46:16 +08:00
LJQ b380e395c0 Merge pull request #24 from ViviqwerAsd/codex/chat-app-adapters
Add chat app adapters for QQ, Feishu, WeCom, and DingTalk
2026-03-13 16:14:44 +08:00
Vivi 8n24 6fe93e9cb4 Add chat app adapters for QQ, Feishu, WeCom, and DingTalk 2026-03-13 16:04:18 +08:00
Liang Jiaqing 3f66368689 add autonomous_operation_sop folder to tracking; update .gitignore whitelist; agent_loop changes 2026-03-13 14:16:43 +08:00
Liang Jiaqing fd7bf241d0 rename sidercall.py to llmcore.py; add cache-aware cd to compress_history_tags 2026-03-13 13:21:32 +08:00
Liang Jiaqing 2a98cc44e5 chore: update repo name from pc-agent-loop to GenericAgent 2026-03-13 12:48:38 +08:00
Liang Jiaqing 029cd91c9a refine autonomous_sop single-task enforcement + subagent input.txt length rule 2026-03-13 09:51:14 +08:00
Liang Jiaqing 3942fe2056 minor: round-end newline, web error msg, autonomous subagent review, tmwebdriver troubleshoot 2026-03-12 22:24:35 +08:00
Liang Jiaqing f52975fd95 Merge PR #23: update plan_sop.md and subagent_sop.md 2026-03-12 22:22:33 +08:00
Jiaqing Liang 9f10e55d5e fix: defensive CDP config init (makedirs+try/except); tweak autonomous sop wording 2026-03-12 12:59:05 +08:00
Jiaqing Liang 03069dbd89 修复自主行动SOP:规划完立即停止+避免模仿低价值任务 2026-03-12 09:29:20 +08:00
Jiaqing Liang 1520a85a9e simphtml: remove dead code (js_cleanDOM, isValidInteractiveElement, redundant return) 2026-03-11 15:18:23 +08:00
Jiaqing Liang 33f2df4270 feat: 添加web_scan的text_only参数支持Shadow DOM文本提取 2026-03-11 14:48:19 +08:00
Jiaqing Liang 006b915533 refactor: scheduler to JSON+cooldown, update SOP 2026-03-11 12:11:22 +08:00
Liang Jiaqing 5dc44ba637 refactor: 优化自主行动SOP - 强化路径提示和格式规范防止history错乱 2026-03-11 10:01:42 +08:00
Liang Jiaqing f0e5d25636 add factory memory entries to insight template 2026-03-10 22:08:57 +08:00
Jiaqing Liang 389fd09765 fix: disable unsafe_allow_html to prevent XSS in chat rendering 2026-03-10 13:55:40 +08:00
Liang Jiaqing 2b387f2dcf ref: simplify feishu history injection, tune web_scan/plan_sop/driver 2026-03-09 08:55:22 +08:00
Liang Jiaqing 378799f8ea fix: /status命令 is_busy() → is_running 2026-03-09 08:46:05 +08:00
AspasZhangand张洲嘉 7d444d065e feat: 添加飞书机器人集成 (#13)
* feat: add Feishu bot integration

- Add fsapp.py: Feishu bot webhook handler (root directory)
- Add assets/SETUP_FEISHU.md: Setup guide for Feishu integration
- Add assets/install_python_windows.bat: Windows Python installer script

* fix: 历史注入仅在飞书场景生效,避免混入本地CLI历史

* fix: fsapp调用put_task时传source='feishu'以触发历史注入

---------

Co-authored-by: 张洲嘉 <zhangzhoujia@zhangzhoujiadeMacBook-Air.local>
2026-03-09 08:45:11 +08:00
Liang Jiaqing 14f1009ddc fix: unified auto_make_url for all API base_url formats, closes #12 2026-03-08 10:24:54 +08:00
Liang Jiaqing 5eac3c1027 refactor: unified mykeys dict, support mykey.py and mykey.json dual format 2026-03-07 23:33:13 +08:00
Liang Jiaqing 61d07a16a6 update: minor fixes to agentmain, mykey_template, sidercall 2026-03-07 22:34:23 +08:00
Liang Jiaqing 7ab47af8e8 fix: zIndex auto coverage bug + monitor maxchars unlimit + autofill CDP sop update 2026-03-07 10:10:26 +08:00
Liang Jiaqing fd9f172588 style: simplify open/write + shorten prompt text 2026-03-06 21:49:43 +08:00
Liang Jiaqing 2156bc6fc6 restore context_win=10000 max_tokens=6144 2026-03-06 21:46:41 +08:00
Liang Jiaqing 959cf6a5a6 refactor: use script_dir for init paths 2026-03-06 21:44:49 +08:00
Liang Jiaqing cccbecb974 Merge PR #6: 火山引擎适配 + 路径修复
# Conflicts:
#	.gitignore
#	agentmain.py
2026-03-06 21:43:04 +08:00
Liang Jiaqing 2272f255f2 feat: add file sending support for telegram bot 2026-03-06 19:19:56 +08:00
Liang Jiaqing 524944dfc8 安全性提升:TID动态生成+Streamlit页面排除+工作记忆修复 2026-03-06 18:54:09 +08:00
Jiaqing Liang c6af92e612 feat: web_scan iframe穿透 + plan_sop术语优化 2026-03-06 00:09:52 +08:00
wangmc1024 ffd08bcd21 允许消息内容中包含HTML代码并直接渲染 | 将纯相对路径改为基于脚本文件的绝对路径,便于全局调用 2026-03-05 22:59:26 +08:00
wangmc1024 19d4e65133 增加了对火山引擎平台的模型适配,避免路径错误 2026-03-05 20:31:05 +08:00
Liang Jiaqing ed75d103f5 优化中间输出频率/plan_sop重构/autofill batch示例/README便携版链接 2026-03-04 17:14:13 +08:00
Liang Jiaqing 313f8a28f0 feat: CDP bridge batch support - cookies/tabs/cdp mixed commands, lazy attach, $N chain refs, file upload & screenshot verified 2026-03-04 13:06:21 +08:00
Liang Jiaqing 3f8407a853 CDP bridge扩展完善+SOP精简+insight修复+plan_sop+autonomous_sop更新 2026-03-04 12:02:50 +08:00
Liang Jiaqing ffe1f3c4c0 fix: update extension display name to TMWD CDP Bridge 2026-03-04 11:16:42 +08:00
Liang Jiaqing 0ab90af250 rename cookie_grabber -> tmwd_cdp_bridge (CDP bridge extension) 2026-03-04 11:15:32 +08:00
Liang Jiaqing dcf65d72c7 launch: bind streamlit to localhost + use high port range 18501-18599 2026-03-03 21:05:34 +08:00
Jiaqing Liang 9219042a8f feat: add --reflect mode with error handling + scheduler/autonomous scripts 2026-03-02 23:25:31 +08:00
Jiaqing Liang 5c3193f326 non-verbose output: exhaust stream silently, yield content only; add plan_sop; subagent_sop cleanup 2026-03-02 23:06:07 +08:00
Jiaqing Liang d010ab1995 refine insight: trim impl details, keep trigger keywords only (1049t) 2026-03-02 21:24:54 +08:00
Jiaqing Liang 066a2995e4 update start_long_term desc: remove single-call limit, add 15-turn rule 2026-03-02 17:25:37 +08:00
LJQ 82991c4a69 Merge pull request #2 from frank-marecogito/main
chore(gitignore): add mykey.py to ignore sensitive API keys and credentials
2026-03-02 09:49:19 +08:00
Frank Fu 4ba1dcc7a2 chore(gitignore): add mykey.py to ignore sensitive API keys and credentials
Add `mykey.py` to `.gitignore` to prevent accidental commits of files containing API keys or other sensitive credentials.
2026-03-02 02:23:21 +03:00
Jiaqing Liang 2655d79acb Update autonomous_operation_sop.md 2026-03-01 23:19:00 +08:00
Liang Jiaqing f27c067b1d autonomous_sop: 加强autonomous_reports路径防护(SOP文本+checkpoint模板双层) 2026-03-01 23:18:36 +08:00
Liang Jiaqing 75ddf8fd1d ljqCtrl: 兼容RDP断开场景的屏幕尺寸/DPI获取; ljqCtrl_sop: ClientToScreen避坑; .gitignore清理 2026-03-01 23:18:36 +08:00
LJQ 392747798a Merge pull request #1 from redreamality/fix/oai-responses-api-mode
Add responses api_mode support for OpenAI relay
2026-03-01 23:16:47 +08:00
252 changed files with 4877 additions and 36710 deletions

No files matched your search

-12
View File
@@ -1,12 +0,0 @@
# Keep byte-exact tracked text stable across Windows and Unix checkouts.
* text=auto eol=lf
# Keep platform-specific scripts with safe line endings.
*.sh text eol=lf
*.command text eol=lf
*.ps1 text eol=crlf
*.bat text eol=crlf
# Preserve the byte-exact third-party notice in source and generated delivery trees.
frontends/desktop/public/THIRD_PARTY_NOTICES.txt text eol=lf
frontends/desktop/dist/THIRD_PARTY_NOTICES.txt text eol=lf
-168
View File
@@ -1,168 +0,0 @@
name: Desktop 2.0 Quality Gates
on:
push:
branches: [main]
paths:
- "agentmain.py"
- "llmcore.py"
- "llmcore/**"
- "frontends/conductor.py"
- "frontends/cost_tracker.py"
- "frontends/data_backup.py"
- "frontends/desktop_bridge.py"
- "frontends/desktop_settings.py"
- "frontends/ga_contract_probe.py"
- "frontends/tests/**"
- "frontends/desktop/**"
- ".gitattributes"
- ".github/workflows/desktop-ci.yml"
- ".github/workflows/desktop-release-package.yml"
pull_request:
branches: [main]
paths:
- "agentmain.py"
- "llmcore.py"
- "llmcore/**"
- "frontends/conductor.py"
- "frontends/cost_tracker.py"
- "frontends/data_backup.py"
- "frontends/desktop_bridge.py"
- "frontends/desktop_settings.py"
- "frontends/ga_contract_probe.py"
- "frontends/tests/**"
- "frontends/desktop/**"
- ".gitattributes"
- ".github/workflows/desktop-ci.yml"
- ".github/workflows/desktop-release-package.yml"
workflow_dispatch:
concurrency:
group: desktop-v2-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
PYTHONUNBUFFERED: "1"
CARGO_TERM_COLOR: always
jobs:
distribution-contract:
name: Compiled distribution contract
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
- name: Verify tracked React distribution and package metadata
working-directory: frontends/desktop
run: npm run test:dist
- name: Verify packaging helper syntax
run: |
bash -n frontends/desktop/packaging/scripts/macos/install_macos.sh
bash -n frontends/desktop/scripts/post-dmg.sh
python3 -m py_compile frontends/desktop/scripts/gen_ds_store.py
- name: Reject source and local-development leakage
run: |
test -z "$(git ls-files -- \
'frontends/desktop/src/**' \
'frontends/desktop/public/**' \
'frontends/desktop/vite.config.*' \
'frontends/desktop/tsconfig*.json' \
'.agents/**' '.codex/**' '.trellis/**')"
test -z "$(find frontends/desktop/dist -type f \
\( -name '*.map' -o -name '*.ts' -o -name '*.tsx' -o -name '*.jsx' \) -print)"
! git grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- .github frontends
python-contracts:
name: Python ${{ matrix.python-version }}
needs: distribution-contract
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: frontends/tests/requirements.txt
- name: Install Desktop test dependencies
run: python -m pip install -r frontends/tests/requirements.txt
- name: Run bridge, conductor, cost and backup contracts
run: >-
python -m pytest frontends/tests -q
--ignore=frontends/tests/test_release_qualification.py
- name: Run release qualification tooling contracts
if: matrix.python-version == '3.12'
run: python -m pytest frontends/tests/test_release_qualification.py -q
rust-contracts:
name: Rust ${{ matrix.feature.name }}
needs: distribution-contract
runs-on: ubuntu-24.04
timeout-minutes: 35
strategy:
fail-fast: false
matrix:
feature:
- name: production
args: ""
- name: e2e
args: "--features e2e"
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Install Tauri system dependencies
run: >-
sudo apt-get update && sudo apt-get install -y
libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable action snapshot
with:
toolchain: "1.95.0"
components: clippy, rustfmt
- uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2
with:
workspaces: frontends/desktop/src-tauri -> target
- name: Verify Rust formatting
run: cargo fmt --manifest-path frontends/desktop/src-tauri/Cargo.toml --check
- name: Lint Tauri shell
run: cargo clippy --manifest-path frontends/desktop/src-tauri/Cargo.toml --all-targets ${{ matrix.feature.args }} -- -D warnings
- name: Run Tauri shell contracts
run: cargo test --manifest-path frontends/desktop/src-tauri/Cargo.toml ${{ matrix.feature.args }}
linux-tauri-build:
name: Linux Tauri build smoke
needs: [distribution-contract, python-contracts, rust-contracts]
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Install Tauri system dependencies
run: >-
sudo apt-get update && sudo apt-get install -y
libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable action snapshot
with:
toolchain: "1.95.0"
- uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2
with:
workspaces: frontends/desktop/src-tauri -> target
- name: Install Tauri CLI only
working-directory: frontends/desktop
run: |
npm install --package-lock=false
node -e "const v=require('./node_modules/@tauri-apps/cli/package.json').version; if(v!=='2.11.4') throw new Error('unexpected Tauri CLI '+v)"
test ! -e package-lock.json
- name: Build tracked Desktop 2.0 distribution into the native shell
working-directory: frontends/desktop
run: npm run tauri build -- --no-bundle
@@ -1,887 +0,0 @@
name: Build Desktop Portable Packages
# Triggers:
# - workflow_dispatch -> build CI artifacts for testing (choose a target platform)
# - push tag desktop-portable-* -> build all three platforms and publish them
# together into ONE GitHub Release (tag = the pushed tag)
# Plain pushes to branches do NOT build (avoids 3 heavy Rust builds per commit).
"on":
push:
tags:
- "desktop-portable-*"
workflow_dispatch:
inputs:
target:
description: "Which platform(s) to build (artifacts only; release happens on tag push)"
required: false
default: "all"
type: choice
options:
- "all"
- "windows"
- "linux"
- "macos"
permissions:
contents: read
env:
NODE_VERSION: "22.23.2"
RUST_TOOLCHAIN: "1.95.0"
PBS_RELEASE: "20260814"
PBS_PYTHON_VERSION: "3.12.14"
MACOS_PACKAGING_PYTHON_VERSION: "3.12.10"
PYTHONDONTWRITEBYTECODE: "1"
jobs:
# ----------------------------------------------------------------------------
build-windows:
name: Windows portable
if: ${{ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/desktop-portable-')) || (github.event_name == 'workflow_dispatch' && (github.event.inputs.target == 'all' || github.event.inputs.target == 'windows')) }}
runs-on: windows-2025
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Set up Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable action snapshot
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Cache Rust build
uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2
with:
workspaces: frontends/desktop/src-tauri -> target
- name: Install desktop dependencies
working-directory: frontends/desktop
run: |
npm install --package-lock=false
node -e "const v=require('./node_modules/@tauri-apps/cli/package.json').version; if(v!=='2.11.4') throw new Error('unexpected Tauri CLI '+v)"
test ! -e package-lock.json
- name: Verify tracked compiled renderer bytes
working-directory: frontends/desktop
run: npm run test:dist
- name: Build Windows desktop exe
working-directory: frontends/desktop
run: npm run tauri build -- --bundles nsis
- name: Assemble self-contained portable bundle
shell: bash
run: |
set -euo pipefail
purge_runtime_bytecode() {
local runtime_root="$1"
find "$runtime_root" -type d -name '__pycache__' -prune -exec rm -rf {} +
find "$runtime_root" -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete
if find "$runtime_root" -type d -name '__pycache__' -print -quit | grep -q .; then
echo "Python bytecode cache directory remains in packaged runtime: $runtime_root" >&2
return 1
fi
if find "$runtime_root" -type f \( -name '*.pyc' -o -name '*.pyo' \) -print -quit | grep -q .; then
echo "Python bytecode remains in packaged runtime: $runtime_root" >&2
return 1
fi
}
EXE_SRC="$(find frontends/desktop/src-tauri/target/release -maxdepth 1 -type f -name 'ga-desktop.exe' | head -n 1)"
[[ -n "$EXE_SRC" ]] || { echo "ga-desktop.exe not found" >&2; exit 1; }
PKG="artifacts/windows/GenericAgent-Desktop-Windows-Portable"
RUNTIME="$PKG/runtime"
mkdir -p "$RUNTIME"
# exe at the top level, named GenericAgent (typical portable form)
cp "$EXE_SRC" "$PKG/GenericAgent.exe"
cp frontends/desktop/packaging/scripts/windows/install_windows.ps1 "$RUNTIME/install_windows.ps1"
cp frontends/desktop/packaging/scripts/merge_desktop_settings.py "$RUNTIME/merge_desktop_settings.py"
# Uninstaller: double-click entry at top level, worker script under runtime/.
cp frontends/desktop/packaging/scripts/windows/uninstall.bat "$PKG/uninstall.bat"
cp frontends/desktop/packaging/scripts/windows/uninstall_windows.ps1 "$RUNTIME/uninstall_windows.ps1"
# Fixed python-build-standalone Windows x86_64 archive.
PBS_URL="https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_RELEASE}/cpython-${PBS_PYTHON_VERSION}%2B${PBS_RELEASE}-x86_64-pc-windows-msvc-install_only.tar.gz"
PBS_SHA256="7330282b47cd43a66b702d39078d2e5a88e580cee351d82f95045f21f5ee042a"
command -v cygpath >/dev/null 2>&1 || { echo "cygpath not found on Windows runner" >&2; exit 1; }
RUNNER_TEMP_POSIX="$(cygpath -u "$RUNNER_TEMP")"
[[ "$RUNNER_TEMP_POSIX" == /* ]] || { echo "cygpath did not return an absolute POSIX path" >&2; exit 1; }
PBS_ARCHIVE="${RUNNER_TEMP_POSIX}/pbs-windows-x86_64.tar.gz"
echo "Python build-standalone: $PBS_URL"
curl --proto '=https' --tlsv1.2 --fail --location --retry 3 --output "$PBS_ARCHIVE" "$PBS_URL"
printf '%s %s\n' "$PBS_SHA256" "$PBS_ARCHIVE" | sha256sum --check --strict -
tar -xzf "$PBS_ARCHIVE" -C "$RUNTIME" # extracts a 'python/' dir (python.exe at top)
PY="$RUNTIME/python/python.exe"
PYTHONDONTWRITEBYTECODE=1 "$PY" -c 'import platform, sys; assert sys.version_info[:3] == (3, 12, 14); assert platform.machine().lower() in ("amd64", "x86_64"); print(sys.version)'
# App-local UCRT: python-build-standalone links the Universal CRT dynamically
# and does NOT bundle it. Win10/11 ship UCRT in-box, but stripped/older images
# may not -> python.exe fails to load (missing api-ms-win-crt-*.dll). Copy the
# redistributable UCRT DLLs next to python.exe (Microsoft-supported app-local
# deployment). Pure SDK source, no System32 fallback, hard-fail if absent so we
# never ship a bundle from an unexpected source.
UCRT_SRC="$(ls -d "/c/Program Files (x86)/Windows Kits/10/Redist/"*/ucrt/DLLs/x64 2>/dev/null | sort -V | tail -1)"
[[ -n "$UCRT_SRC" ]] || { echo "Windows SDK UCRT redist dir not found on runner" >&2; exit 1; }
echo "UCRT source: $UCRT_SRC"
cp "$UCRT_SRC"/api-ms-win-crt-*.dll "$RUNTIME/python/"
cp "$UCRT_SRC"/ucrtbase.dll "$RUNTIME/python/"
test -f "$RUNTIME/python/api-ms-win-crt-runtime-l1-1-0.dll" \
|| { echo "UCRT DLLs missing after copy" >&2; exit 1; }
# Offline binary-only wheels. requirements.txt pins direct and transitive versions.
mkdir -p "$RUNTIME/wheels"
cp frontends/desktop/packaging/python-runtime-requirements.txt "$RUNTIME/wheels/requirements.txt"
PYTHONDONTWRITEBYTECODE=1 "$PY" -m pip download --only-binary=:all: --dest "$RUNTIME/wheels" \
--requirement "$RUNTIME/wheels/requirements.txt"
# Runtime source (project root minus heavy/dev/build dirs)
mkdir -p "$RUNTIME/app"
tar \
--exclude='./.git' --exclude='./.github' \
--exclude='./frontends/tests' \
--exclude='./frontends/desktop/src-tauri' \
--exclude='./frontends/desktop/src' \
--exclude='./frontends/desktop/public' \
--exclude='./frontends/desktop/scripts' \
--exclude='./frontends/desktop/release_qualification' \
--exclude='./frontends/desktop/tests' \
--exclude='./frontends/desktop/testing' \
--exclude='./frontends/desktop/spec' \
--exclude='./frontends/desktop/node_modules' \
--exclude='./frontends/desktop/dist' \
--exclude='./frontends/desktop/DESIGN.md' \
--exclude='./frontends/desktop/package.json' \
--exclude='./frontends/desktop/package-lock.json' \
--exclude='./frontends/desktop/.npmrc' \
--exclude='./frontends/desktop/index.html' \
--exclude='./frontends/desktop/loading.html' \
--exclude='./frontends/desktop/setup.html' \
--exclude='./frontends/desktop/tsconfig*.json' \
--exclude='./frontends/desktop/vite.config.ts' \
--exclude='./frontends/desktop/packaging' --exclude='./docs' \
--exclude='./assets/demo' --exclude='./assets/images' \
--exclude='./assets/GenericAgent_Technical_Report.pdf' \
--exclude='./artifacts' \
--exclude='*/node_modules' --exclude='*/target' \
--exclude='*/.venv' --exclude='./.venv' \
--exclude='*/__pycache__' --exclude='*.pyc' \
-cf - . | tar -xf - -C "$RUNTIME/app"
test -f "$RUNTIME/app/agentmain.py"
test -f "$RUNTIME/app/frontends/desktop_bridge.py"
test -f "$RUNTIME/app/frontends/desktop/static/index.html"
test ! -e "$RUNTIME/app/frontends/desktop/dist"
test ! -e "$RUNTIME/app/frontends/desktop/src"
test ! -e "$RUNTIME/app/frontends/desktop/public"
test ! -e "$RUNTIME/app/frontends/desktop/package-lock.json"
test ! -e "$RUNTIME/app/frontends/desktop/node_modules"
# Drop python debug symbols (.pdb) to slim the package (~80MB)
find "$RUNTIME/python" -name '*.pdb' -delete 2>/dev/null || true
purge_runtime_bytecode "$RUNTIME"
cat > "$PKG/readme.txt" <<'EOF'
================ 中文 ================
GenericAgent Desktop — Windows 便携版(自包含)
无需安装 Python、无需联网装依赖、无需源码仓库——全部已内置。
前置条件
- Windows 10/11 x64
- Microsoft Edge WebView2 运行时(Win11 一般自带;缺失时首次运行会提示安装)
使用
1. 解压到任意目录(路径建议不含特殊字符)。
2. 双击 GenericAgent.exe。
3. 首次启动会离线校验并补齐随包 Python 运行时依赖,界面显示进度,完成后进入主界面。
4. 之后启动直接秒进。
说明
- 想真正对话,仍需在程序里配置模型 / API Key。
- 如果 Windows Defender 防火墙拦截 `127.0.0.1` / `localhost` 回环连接,程序可能无法启动或连接本机服务。请允许 `GenericAgent.exe` 和 `runtime\python\python.exe` 通过防火墙,然后重启桌面端。
- 本便携版尚未使用 Windows 代码签名证书;SmartScreen 可能显示警告。运行前请用随包发布的 SHA256SUMS-windows.txt 核对 ZIP。
- 可整体移动本文件夹;重启后会自动刷新路径。若需强制重新准备,请删除 runtime\.prepared 后重启。
- runtime\ 是内置运行环境与源码,正常使用无需改动。
================ English ================
GenericAgent Desktop — Windows Portable (self-contained)
No Python install, no internet for dependencies, no source checkout — everything is bundled.
Requirements
- Windows 10/11 x64
- Microsoft Edge WebView2 runtime (usually preinstalled on Win11; you are prompted if missing)
Usage
1. Extract anywhere (a path without special characters is recommended).
2. Double-click GenericAgent.exe.
3. The first launch verifies and completes the bundled Python runtime offline with a
progress UI, then opens the main window.
4. Subsequent launches start instantly.
Notes
- To actually chat, configure a model / API key inside the app.
- If Windows Defender Firewall blocks the `127.0.0.1` / `localhost` loopback connection, the app may fail to start or reach its local service. Allow `GenericAgent.exe` and `runtime\python\python.exe` through the firewall, then restart the desktop app.
- This portable build is not Windows code-signed, so SmartScreen may warn. Verify the ZIP against the published SHA256SUMS-windows.txt before running it.
- You may move the whole folder; relaunching refreshes its saved paths. To force a new
preparation pass, delete runtime\.prepared and relaunch.
- runtime\ holds the bundled runtime and source; no need to touch it.
EOF
sed -i 's/^ //' "$PKG/readme.txt"
echo "Package tree (top levels):"
find "$PKG" -maxdepth 2 -printf '%y %p\n' | sort | head -40
- name: Zip portable package
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path artifacts/windows/out | Out-Null
Compress-Archive -Path artifacts/windows/GenericAgent-Desktop-Windows-Portable `
-DestinationPath artifacts/windows/out/GenericAgent-Desktop-Windows-Portable.zip -Force
$h = Get-FileHash artifacts/windows/out/GenericAgent-Desktop-Windows-Portable.zip -Algorithm SHA256
"{0} GenericAgent-Desktop-Windows-Portable.zip" -f $h.Hash.ToLowerInvariant() |
Set-Content -Encoding ASCII artifacts/windows/out/SHA256SUMS-windows.txt
Get-ChildItem artifacts/windows/out | Format-Table Name, Length
- name: Upload workflow artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: GenericAgent-Desktop-Windows-Portable-${{ github.run_number }}
path: |
artifacts/windows/out/GenericAgent-Desktop-Windows-Portable.zip
artifacts/windows/out/SHA256SUMS-windows.txt
if-no-files-found: error
# ----------------------------------------------------------------------------
build-linux:
name: Linux portable
if: ${{ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/desktop-portable-')) || (github.event_name == 'workflow_dispatch' && (github.event.inputs.target == 'all' || github.event.inputs.target == 'linux')) }}
runs-on: ubuntu-22.04
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Set up Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable action snapshot
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Cache Rust build
uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2
with:
# Keep registry/git downloads, but never restore binaries built on a newer glibc image.
prefix-key: "v1-rust-release-ubuntu-22.04-glibc-2.35"
workspaces: frontends/desktop/src-tauri -> target
cache-targets: "false"
cache-bin: "false"
- name: Install Tauri Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
binutils \
file \
xvfb
- name: Install desktop dependencies
working-directory: frontends/desktop
run: |
npm install --package-lock=false
node -e "const v=require('./node_modules/@tauri-apps/cli/package.json').version; if(v!=='2.11.4') throw new Error('unexpected Tauri CLI '+v)"
test ! -e package-lock.json
- name: Verify tracked compiled renderer bytes
working-directory: frontends/desktop
run: npm run test:dist
- name: Build Linux AppImage
working-directory: frontends/desktop
run: npm run tauri build -- --bundles appimage
- name: Assemble self-contained portable bundle
shell: bash
run: |
set -euo pipefail
purge_runtime_bytecode() {
local runtime_root="$1"
find "$runtime_root" -type d -name '__pycache__' -prune -exec rm -rf {} +
find "$runtime_root" -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete
if find "$runtime_root" -type d -name '__pycache__' -print -quit | grep -q .; then
echo "Python bytecode cache directory remains in packaged runtime: $runtime_root" >&2
return 1
fi
if find "$runtime_root" -type f \( -name '*.pyc' -o -name '*.pyo' \) -print -quit | grep -q .; then
echo "Python bytecode remains in packaged runtime: $runtime_root" >&2
return 1
fi
}
APPIMAGE_SRC="$(find frontends/desktop/src-tauri/target/release/bundle/appimage -maxdepth 1 -type f -name '*.AppImage' | head -n 1)"
[[ -n "$APPIMAGE_SRC" ]] || { echo "No AppImage found" >&2; exit 1; }
PKG="artifacts/linux/GenericAgent-Desktop-Linux-Portable"
RUNTIME="$PKG/runtime"
mkdir -p "$RUNTIME"
# Stable internal name so the generated .desktop Exec= never changes on upgrades.
cp "$APPIMAGE_SRC" "$PKG/GenericAgent.AppImage"
chmod +x "$PKG/GenericAgent.AppImage"
# Icon for the generated .desktop launcher (Icon= needs a real image file on Linux).
cp frontends/desktop/src-tauri/icons/icon.png "$PKG/GenericAgent.png"
cp frontends/desktop/packaging/scripts/linux/install_linux.sh "$RUNTIME/install_linux.sh"
cp frontends/desktop/packaging/scripts/merge_desktop_settings.py "$RUNTIME/merge_desktop_settings.py"
chmod +x "$RUNTIME/install_linux.sh"
# Uninstaller: top-level entry next to the AppImage.
cp frontends/desktop/packaging/scripts/linux/uninstall.sh "$PKG/uninstall.sh"
chmod +x "$PKG/uninstall.sh"
# Fixed python-build-standalone Linux x86_64 archive.
PBS_URL="https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_RELEASE}/cpython-${PBS_PYTHON_VERSION}%2B${PBS_RELEASE}-x86_64-unknown-linux-gnu-install_only.tar.gz"
PBS_SHA256="3297691ae34f75fed81ac424e040145fccb0bafe8e581cd5cadbddfa1c0766c0"
PBS_ARCHIVE="${RUNNER_TEMP}/pbs-linux-x86_64.tar.gz"
echo "Python build-standalone: $PBS_URL"
curl --proto '=https' --tlsv1.2 --fail --location --retry 3 --output "$PBS_ARCHIVE" "$PBS_URL"
printf '%s %s\n' "$PBS_SHA256" "$PBS_ARCHIVE" | sha256sum --check --strict -
tar -xzf "$PBS_ARCHIVE" -C "$RUNTIME" # extracts a 'python/' dir
PY="$RUNTIME/python/bin/python3"
PYTHONDONTWRITEBYTECODE=1 "$PY" -c 'import platform, sys; assert sys.version_info[:3] == (3, 12, 14); assert platform.machine() == "x86_64"; print(sys.version)'
# Offline binary-only wheels. requirements.txt pins direct and transitive versions.
mkdir -p "$RUNTIME/wheels"
cp frontends/desktop/packaging/python-runtime-requirements.txt "$RUNTIME/wheels/requirements.txt"
PYTHONDONTWRITEBYTECODE=1 "$PY" -m pip download --only-binary=:all: --dest "$RUNTIME/wheels" \
--requirement "$RUNTIME/wheels/requirements.txt"
# Runtime source (project root minus heavy/dev/build dirs)
mkdir -p "$RUNTIME/app"
tar \
--exclude='./.git' --exclude='./.github' \
--exclude='./frontends/tests' \
--exclude='./frontends/desktop/src-tauri' \
--exclude='./frontends/desktop/src' \
--exclude='./frontends/desktop/public' \
--exclude='./frontends/desktop/scripts' \
--exclude='./frontends/desktop/release_qualification' \
--exclude='./frontends/desktop/tests' \
--exclude='./frontends/desktop/testing' \
--exclude='./frontends/desktop/spec' \
--exclude='./frontends/desktop/node_modules' \
--exclude='./frontends/desktop/dist' \
--exclude='./frontends/desktop/DESIGN.md' \
--exclude='./frontends/desktop/package.json' \
--exclude='./frontends/desktop/package-lock.json' \
--exclude='./frontends/desktop/.npmrc' \
--exclude='./frontends/desktop/index.html' \
--exclude='./frontends/desktop/loading.html' \
--exclude='./frontends/desktop/setup.html' \
--exclude='./frontends/desktop/tsconfig*.json' \
--exclude='./frontends/desktop/vite.config.ts' \
--exclude='./frontends/desktop/packaging' --exclude='./docs' \
--exclude='./assets/demo' --exclude='./assets/images' \
--exclude='./assets/GenericAgent_Technical_Report.pdf' \
--exclude='./artifacts' \
--exclude='*/node_modules' --exclude='*/target' \
--exclude='*/.venv' --exclude='./.venv' \
--exclude='*/__pycache__' --exclude='*.pyc' \
-cf - . | tar -xf - -C "$RUNTIME/app"
test -f "$RUNTIME/app/agentmain.py"
test -f "$RUNTIME/app/frontends/desktop_bridge.py"
test -f "$RUNTIME/app/frontends/desktop/static/index.html"
test ! -e "$RUNTIME/app/frontends/desktop/dist"
test ! -e "$RUNTIME/app/frontends/desktop/src"
test ! -e "$RUNTIME/app/frontends/desktop/public"
test ! -e "$RUNTIME/app/frontends/desktop/package-lock.json"
test ! -e "$RUNTIME/app/frontends/desktop/node_modules"
purge_runtime_bytecode "$RUNTIME"
cat > "$PKG/readme.txt" <<'EOF'
================ 中文 ================
GenericAgent Desktop — Linux 便携版(自包含)
无需安装 Python、无需联网装依赖、无需源码仓库——全部已内置。
前置条件
- Linux x86_64,glibc 2.35 或更高版本(如 Ubuntu 22.04、Debian 12)
- 桌面环境 + FUSE(运行 AppImage 所需;多数发行版自带)
使用
1. 解压到任意目录(路径建议不含特殊字符)。
2. 给 AppImage 执行权限并运行:
chmod +x GenericAgent.AppImage
./GenericAgent.AppImage
3. 首次启动会离线校验并补齐随包 Python 运行时依赖,界面显示进度,完成后进入主界面。
4. 之后启动直接秒进。
说明
- 想真正对话,仍需在程序里配置模型 / API Key。
- 可整体移动本文件夹;重启后会自动刷新路径。若需强制重新准备,请删除 runtime/.prepared 后重启。
- runtime/ 是内置运行环境与源码,正常使用无需改动。
================ English ================
GenericAgent Desktop — Linux Portable (self-contained)
No Python install, no internet for dependencies, no source checkout — everything is bundled.
Requirements
- Linux x86_64 with glibc 2.35 or newer (for example Ubuntu 22.04 or Debian 12)
- A desktop environment + FUSE (required to run the AppImage; preinstalled on most distros)
Usage
1. Extract anywhere (a path without special characters is recommended).
2. Make the AppImage executable and run it:
chmod +x GenericAgent.AppImage
./GenericAgent.AppImage
3. The first launch verifies and completes the bundled Python runtime offline with a
progress UI, then opens the main window.
4. Subsequent launches start instantly.
Notes
- To actually chat, configure a model / API key inside the app.
- You may move the whole folder; relaunching refreshes its saved paths. To force a new
preparation pass, delete runtime/.prepared and relaunch.
- runtime/ holds the bundled runtime and source; no need to touch it.
EOF
sed -i 's/^ //' "$PKG/readme.txt"
mkdir -p artifacts/linux/out
tar -C artifacts/linux -czf artifacts/linux/out/GenericAgent-Desktop-Linux-Portable.tar.gz GenericAgent-Desktop-Linux-Portable
( cd artifacts/linux/out && sha256sum GenericAgent-Desktop-Linux-Portable.tar.gz > SHA256SUMS-linux.txt )
tar -tzf artifacts/linux/out/GenericAgent-Desktop-Linux-Portable.tar.gz > /tmp/pkglist.txt
echo "Package contents (first 40 of $(wc -l < /tmp/pkglist.txt) entries):"
head -40 /tmp/pkglist.txt
ls -lh artifacts/linux/out
# Exercise the exact archived candidate on the oldest supported builder.
SMOKE_ROOT="${RUNNER_TEMP}/ga-linux-package-smoke"
mkdir -p "$SMOKE_ROOT"
tar -xzf artifacts/linux/out/GenericAgent-Desktop-Linux-Portable.tar.gz -C "$SMOKE_ROOT"
SMOKE_PACKAGE="$SMOKE_ROOT/GenericAgent-Desktop-Linux-Portable"
APPIMAGE_ABS="$(realpath "$SMOKE_PACKAGE/GenericAgent.AppImage")"
APPIMAGE_SCAN="$SMOKE_ROOT/appimage-scan"
mkdir -p "$APPIMAGE_SCAN"
(cd "$APPIMAGE_SCAN" && "$APPIMAGE_ABS" --appimage-extract >/dev/null)
MAX_GLIBC=""
ELF_COUNT=0
while IFS= read -r -d '' candidate; do
file -b "$candidate" | grep -q '^ELF' || continue
ELF_COUNT=$((ELF_COUNT + 1))
candidate_max="$(readelf --version-info "$candidate" 2>/dev/null \
| sed -n 's/.*Name: GLIBC_\([0-9][0-9.]*\).*/\1/p' \
| sort -V | tail -n 1)"
[[ -n "$candidate_max" ]] || continue
if [[ -z "$MAX_GLIBC" \
|| "$(printf '%s\n%s\n' "$MAX_GLIBC" "$candidate_max" | sort -V | tail -n 1)" == "$candidate_max" ]]; then
MAX_GLIBC="$candidate_max"
fi
done < <(find "$SMOKE_PACKAGE" "$APPIMAGE_SCAN/squashfs-root" -type f -print0)
[[ "$ELF_COUNT" -gt 0 && -n "$MAX_GLIBC" ]] \
|| { echo "No GLIBC symbol versions found in final Linux package ELFs" >&2; exit 1; }
[[ "$(printf '%s\n%s\n' "$MAX_GLIBC" '2.35' | sort -V | tail -n 1)" == "2.35" ]] \
|| { echo "Final package requires GLIBC_$MAX_GLIBC; maximum allowed is GLIBC_2.35" >&2; exit 1; }
echo "Scanned $ELF_COUNT final package ELFs; maximum symbol is GLIBC_$MAX_GLIBC"
APPIMAGE_EXTRACT_AND_RUN=1 WEBKIT_DISABLE_COMPOSITING_MODE=1 \
xvfb-run -a "$SMOKE_PACKAGE/GenericAgent.AppImage" >"$SMOKE_ROOT/app.log" 2>&1 &
SMOKE_PID=$!
cleanup_smoke() {
kill "$SMOKE_PID" 2>/dev/null || true
wait "$SMOKE_PID" 2>/dev/null || true
}
trap cleanup_smoke EXIT
IDENTITY=""
for _ in $(seq 1 120); do
IDENTITY="$(curl --fail --silent --max-time 1 http://127.0.0.1:14168/services/identity || true)"
[[ -n "$IDENTITY" ]] && break
sleep 1
done
[[ -n "$IDENTITY" ]] || { cat "$SMOKE_ROOT/app.log" >&2; exit 1; }
EXPECTED_APP_DIR="$SMOKE_PACKAGE/runtime/app/frontends" \
IDENTITY="$IDENTITY" python3 - <<'PY'
import json, os, pathlib
identity = json.loads(os.environ["IDENTITY"])
actual = pathlib.Path(identity["app_dir"]).resolve()
expected = pathlib.Path(os.environ["EXPECTED_APP_DIR"]).resolve()
assert actual == expected, (actual, expected)
PY
curl --fail --silent --max-time 3 -X POST http://127.0.0.1:14168/services/bridge/exit >/dev/null
cleanup_smoke
trap - EXIT
- name: Upload workflow artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: GenericAgent-Desktop-Linux-Portable-${{ github.run_number }}
path: |
artifacts/linux/out/GenericAgent-Desktop-Linux-Portable.tar.gz
artifacts/linux/out/SHA256SUMS-linux.txt
if-no-files-found: error
# ----------------------------------------------------------------------------
build-macos:
name: macOS arm64 DMG
if: ${{ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/desktop-portable-')) || (github.event_name == 'workflow_dispatch' && (github.event.inputs.target == 'all' || github.event.inputs.target == 'macos')) }}
runs-on: macos-26
permissions:
contents: read
env:
DEVELOPER_DIR: /Applications/Xcode_26.5.app/Contents/Developer
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- name: Verify pinned macOS build environment
run: |
test "$(uname -m)" = arm64
test "$(xcodebuild -version | sed -n '1p')" = "Xcode 26.5"
test "$(xcodebuild -version | sed -n '2p')" = "Build version 17F42"
test "$(xcrun --sdk macosx --show-sdk-version)" = "26.5"
- name: Set up Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Set up Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable action snapshot
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Cache Rust build
uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2
with:
workspaces: frontends/desktop/src-tauri -> target
- name: Install desktop dependencies
working-directory: frontends/desktop
run: |
npm install --package-lock=false
node -e "const v=require('./node_modules/@tauri-apps/cli/package.json').version; if(v!=='2.11.4') throw new Error('unexpected Tauri CLI '+v)"
test ! -e package-lock.json
- name: Set up Python DMG tooling runtime
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.MACOS_PACKAGING_PYTHON_VERSION }}
- name: Install Python packaging tools
run: >-
python3 -m pip install --require-hashes --only-binary=:all:
--requirement frontends/desktop/packaging/dmg-build-requirements.txt
- name: Verify tracked compiled renderer bytes
working-directory: frontends/desktop
run: npm run test:dist
- name: Build macOS .app
working-directory: frontends/desktop
run: npm run tauri build -- --bundles app
- name: Assemble self-contained DMG app
shell: bash
run: |
set -euo pipefail
purge_runtime_bytecode() {
local runtime_root="$1"
find "$runtime_root" -type d -name '__pycache__' -prune -exec rm -rf {} +
find "$runtime_root" -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete
if find "$runtime_root" -type d -name '__pycache__' -print -quit | grep -q .; then
echo "Python bytecode cache directory remains in packaged runtime: $runtime_root" >&2
return 1
fi
if find "$runtime_root" -type f \( -name '*.pyc' -o -name '*.pyo' \) -print -quit | grep -q .; then
echo "Python bytecode remains in packaged runtime: $runtime_root" >&2
return 1
fi
}
APP_SRC="$(find frontends/desktop/src-tauri/target/release/bundle/macos -maxdepth 1 -name '*.app' -type d | head -n 1)"
[[ -n "$APP_SRC" ]] || { echo "No .app found" >&2; exit 1; }
# Build one runtime tree and embed it only in the final DMG application.
RUNTIME_SRC="artifacts/macos/runtime-src"
mkdir -p "$RUNTIME_SRC"
cp frontends/desktop/packaging/scripts/macos/install_macos.sh "$RUNTIME_SRC/install_macos.sh"
cp frontends/desktop/packaging/scripts/merge_desktop_settings.py "$RUNTIME_SRC/merge_desktop_settings.py"
chmod +x "$RUNTIME_SRC/install_macos.sh"
# Fixed python-build-standalone macOS arm64 archive.
PBS_URL="https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_RELEASE}/cpython-${PBS_PYTHON_VERSION}%2B${PBS_RELEASE}-aarch64-apple-darwin-install_only.tar.gz"
PBS_SHA256="4572133a5542f306b9bdb155da5800f9e38950cd0a98d469b832ce256fe299ea"
PBS_ARCHIVE="${RUNNER_TEMP}/pbs-macos-aarch64.tar.gz"
echo "Python build-standalone: $PBS_URL"
curl --proto '=https' --tlsv1.2 --fail --location --retry 3 --output "$PBS_ARCHIVE" "$PBS_URL"
printf '%s %s\n' "$PBS_SHA256" "$PBS_ARCHIVE" | shasum -a 256 --check -
tar -xzf "$PBS_ARCHIVE" -C "$RUNTIME_SRC" # extracts a 'python/' dir
PY="$RUNTIME_SRC/python/bin/python3"
PYTHONDONTWRITEBYTECODE=1 "$PY" -c 'import platform, sys; assert sys.version_info[:3] == (3, 12, 14); assert platform.machine() == "arm64"; print(sys.version)'
# Offline binary-only wheels. requirements.txt pins direct and transitive versions.
mkdir -p "$RUNTIME_SRC/wheels"
cp frontends/desktop/packaging/python-runtime-requirements.txt "$RUNTIME_SRC/wheels/requirements.txt"
PYTHONDONTWRITEBYTECODE=1 "$PY" -m pip download --only-binary=:all: --dest "$RUNTIME_SRC/wheels" \
--requirement "$RUNTIME_SRC/wheels/requirements.txt"
# macOS applications must be immutable at first launch. Install from the downloaded
# wheelhouse before the runtime enters the signed .app, then carry a durable marker.
# The binary-only offline repair path needs neither setuptools nor wheel. Remove any
# copies shipped in the base interpreter; bytecode writes are disabled at runtime too.
PYTHONDONTWRITEBYTECODE=1 "$PY" -m pip install --no-compile --no-index --find-links "$RUNTIME_SRC/wheels" \
--requirement "$RUNTIME_SRC/wheels/requirements.txt"
PYTHONDONTWRITEBYTECODE=1 "$PY" -m pip uninstall --yes setuptools wheel
PYTHONDONTWRITEBYTECODE=1 "$PY" -m pip check
PYTHONDONTWRITEBYTECODE=1 "$PY" -c 'import aiohttp, fastapi, importlib.util, pydantic, uvicorn, websockets; assert all(importlib.util.find_spec(name) is None for name in ("setuptools", "wheel", "pkg_resources"))'
purge_runtime_bytecode "$RUNTIME_SRC"
printf 'ok\n' > "$RUNTIME_SRC/.prepared"
test -s "$RUNTIME_SRC/.prepared"
# Runtime source (project root minus heavy/dev/build dirs) — bsdtar exclude syntax.
mkdir -p "$RUNTIME_SRC/app"
tar \
--exclude='.git' --exclude='.github' \
--exclude='frontends/tests' \
--exclude='frontends/desktop/src-tauri' \
--exclude='frontends/desktop/src' \
--exclude='frontends/desktop/public' \
--exclude='frontends/desktop/scripts' \
--exclude='frontends/desktop/release_qualification' \
--exclude='frontends/desktop/tests' \
--exclude='frontends/desktop/testing' \
--exclude='frontends/desktop/spec' \
--exclude='frontends/desktop/node_modules' \
--exclude='frontends/desktop/dist' \
--exclude='frontends/desktop/DESIGN.md' \
--exclude='frontends/desktop/package.json' \
--exclude='frontends/desktop/package-lock.json' \
--exclude='frontends/desktop/.npmrc' \
--exclude='frontends/desktop/index.html' \
--exclude='frontends/desktop/loading.html' \
--exclude='frontends/desktop/setup.html' \
--exclude='frontends/desktop/tsconfig*.json' \
--exclude='frontends/desktop/vite.config.ts' \
--exclude='frontends/desktop/packaging' --exclude='docs' \
--exclude='assets/demo' --exclude='assets/images' \
--exclude='assets/GenericAgent_Technical_Report.pdf' \
--exclude='artifacts' \
--exclude='*/node_modules' --exclude='*/target' \
--exclude='*/.venv' --exclude='.venv' \
--exclude='*/__pycache__' --exclude='*.pyc' \
-cf - . | tar -xf - -C "$RUNTIME_SRC/app"
test -f "$RUNTIME_SRC/app/agentmain.py"
test -f "$RUNTIME_SRC/app/frontends/desktop_bridge.py"
test -f "$RUNTIME_SRC/app/frontends/desktop/static/index.html"
test ! -e "$RUNTIME_SRC/app/frontends/desktop/dist"
test ! -e "$RUNTIME_SRC/app/frontends/desktop/src"
test ! -e "$RUNTIME_SRC/app/frontends/desktop/public"
test ! -e "$RUNTIME_SRC/app/frontends/desktop/package-lock.json"
test ! -e "$RUNTIME_SRC/app/frontends/desktop/node_modules"
DMG_STAGE="artifacts/macos/dmg-stage"
DMG_APP="$DMG_STAGE/GenericAgent.app"
mkdir -p "$DMG_STAGE"
# Standard DMG: GenericAgent.app + Applications alias. The post-build
# repackager below writes the curated two-icon Finder layout.
ditto "$APP_SRC" "$DMG_APP"
mkdir -p "$DMG_APP/Contents/Resources"
ditto "$RUNTIME_SRC" "$DMG_APP/Contents/Resources/runtime"
test -s "$DMG_APP/Contents/Resources/runtime/.prepared"
DMG_RUNTIME="$DMG_APP/Contents/Resources/runtime"
PYTHONDONTWRITEBYTECODE=1 "$DMG_RUNTIME/python/bin/python3" \
-c 'import aiohttp, fastapi, pydantic, uvicorn, websockets'
purge_runtime_bytecode "$DMG_RUNTIME"
# Ad-hoc signing only: this is not Developer ID signing or notarization.
codesign --force --deep --sign - "$DMG_APP"
codesign --verify --deep --strict "$DMG_APP"
ln -s /Applications "$DMG_STAGE/Applications"
mkdir -p artifacts/macos/out
hdiutil create \
-volname "GenericAgent Desktop" \
-srcfolder "$DMG_STAGE" \
-ov \
-format UDZO \
"artifacts/macos/out/GenericAgent-Desktop-macOS-aarch64.dmg"
# Restore the intentionally minimal Finder presentation: only the app
# and Applications shortcut, with the established window/icon layout.
bash frontends/desktop/scripts/post-dmg.sh "artifacts/macos/out/GenericAgent-Desktop-macOS-aarch64.dmg"
(
cd artifacts/macos/out
shasum -a 256 "GenericAgent-Desktop-macOS-aarch64.dmg" \
> "GenericAgent-Desktop-macOS-aarch64.dmg.sha256"
)
echo "DMG stage tree:"
find "$DMG_STAGE" -maxdepth 2 -print | sort
- name: Upload workflow artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: GenericAgent-Desktop-macOS-aarch64-DMG-${{ github.run_number }}
path: |
artifacts/macos/out/GenericAgent-Desktop-macOS-aarch64.dmg
artifacts/macos/out/GenericAgent-Desktop-macOS-aarch64.dmg.sha256
if-no-files-found: error
# ----------------------------------------------------------------------------
publish-release:
name: Publish one atomic prerelease
needs: [build-windows, build-linux, build-macos]
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/desktop-portable-') && needs.build-windows.result == 'success' && needs.build-linux.result == 'success' && needs.build-macos.result == 'success' }}
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Download Windows candidate
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: GenericAgent-Desktop-Windows-Portable-${{ github.run_number }}
path: ${{ runner.temp }}/release-assets
- name: Download Linux candidate
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: GenericAgent-Desktop-Linux-Portable-${{ github.run_number }}
path: ${{ runner.temp }}/release-assets
- name: Download macOS arm64 candidate
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: GenericAgent-Desktop-macOS-aarch64-DMG-${{ github.run_number }}
path: ${{ runner.temp }}/release-assets
- name: Validate six assets and publish prerelease
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG_NAME: ${{ github.ref_name }}
TARGET_SHA: ${{ github.sha }}
shell: bash
run: |
set -euo pipefail
ASSET_DIR="${RUNNER_TEMP}/release-assets"
EXPECTED=(
GenericAgent-Desktop-Linux-Portable.tar.gz
GenericAgent-Desktop-Windows-Portable.zip
GenericAgent-Desktop-macOS-aarch64.dmg
GenericAgent-Desktop-macOS-aarch64.dmg.sha256
SHA256SUMS-linux.txt
SHA256SUMS-windows.txt
)
if find "$ASSET_DIR" -mindepth 1 -maxdepth 1 ! -type f -print -quit | grep -q .; then
echo "Release staging contains a non-regular entry" >&2
find "$ASSET_DIR" -mindepth 1 -maxdepth 1 -print >&2
exit 1
fi
mapfile -t ACTUAL < <(find "$ASSET_DIR" -mindepth 1 -maxdepth 1 -type f -printf '%f\n' | LC_ALL=C sort)
if ! diff -u <(printf '%s\n' "${EXPECTED[@]}") <(printf '%s\n' "${ACTUAL[@]}"); then
echo "Release staging must contain exactly the six expected files" >&2
exit 1
fi
for filename in "${EXPECTED[@]}"; do
test -s "$ASSET_DIR/$filename" || { echo "Missing or empty release asset: $filename" >&2; exit 1; }
done
verify_checksum_manifest() {
local manifest="$1"
local payload="$2"
local normalized="${RUNNER_TEMP}/${manifest}.normalized"
tr -d '\r' < "$ASSET_DIR/$manifest" > "$normalized"
awk -v expected="$payload" '
NF == 2 && $1 ~ /^[0-9a-fA-F]{64}$/ && $2 == expected { valid += 1 }
END { exit !(NR == 1 && valid == 1) }
' "$normalized"
(cd "$ASSET_DIR" && sha256sum --check --strict "$normalized")
}
verify_checksum_manifest SHA256SUMS-windows.txt GenericAgent-Desktop-Windows-Portable.zip
verify_checksum_manifest SHA256SUMS-linux.txt GenericAgent-Desktop-Linux-Portable.tar.gz
verify_checksum_manifest GenericAgent-Desktop-macOS-aarch64.dmg.sha256 GenericAgent-Desktop-macOS-aarch64.dmg
[[ "$TAG_NAME" =~ ^desktop-portable-[A-Za-z0-9._-]+$ ]] \
|| { echo "Invalid release tag: $TAG_NAME" >&2; exit 1; }
[[ "$TARGET_SHA" =~ ^[0-9a-f]{40}$ ]] \
|| { echo "Invalid release target SHA" >&2; exit 1; }
TITLE="GenericAgent Desktop ${TAG_NAME}"
NOTES_FILE="${RUNNER_TEMP}/ga-release-notes.md"
cat > "$NOTES_FILE" <<'EOF'
GenericAgent Desktop 桌面版 / Desktop
三个平台产物来自同一提交,并在公开发布前统一验证。All three platform artifacts come from one commit and are validated before publication.
## 安装方法 / Installation
- Windows x64:下载 `GenericAgent-Desktop-Windows-Portable.zip`,解压后双击 `GenericAgent.exe`。卸载时运行 `uninstall.bat`,再删除解压目录。
Windows x64: extract `GenericAgent-Desktop-Windows-Portable.zip`, then launch `GenericAgent.exe`. Run `uninstall.bat` before deleting the extracted directory.
- Linux x86_64:下载并解压 `GenericAgent-Desktop-Linux-Portable.tar.gz`,执行 `chmod +x GenericAgent.AppImage` 后运行它。卸载时运行 `uninstall.sh`,再删除解压目录。
Linux x86_64: extract `GenericAgent-Desktop-Linux-Portable.tar.gz`, run `chmod +x GenericAgent.AppImage`, then launch it. Run `uninstall.sh` before deleting the extracted directory.
- macOS Apple silicon (arm64):打开 `GenericAgent-Desktop-macOS-aarch64.dmg`,把 `GenericAgent.app` 拖入 Applications。
macOS Apple silicon (arm64): open `GenericAgent-Desktop-macOS-aarch64.dmg` and drag `GenericAgent.app` to Applications.
## 签名状态 / Signing status
Windows 便携版尚未使用代码签名证书,SmartScreen 可能显示警告;运行前请核对 `SHA256SUMS-windows.txt`。
The Windows portable build is not code-signed, so SmartScreen may warn; verify `SHA256SUMS-windows.txt` before running it.
macOS 应用仅使用 ad-hoc 签名,未使用 Apple Developer ID,也未 notarize。首次打开可能需要右键选择“打开”或在“隐私与安全性”中允许。
The macOS app is ad-hoc signed only. It is neither Developer ID signed nor notarized; first launch may require Open from the context menu or approval in Privacy & Security.
SHA-256 清单与各产物一同发布。SHA-256 manifests are included with the artifacts.
EOF
sed -i 's/^ //' "$NOTES_FILE"
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
echo "A release already exists for $TAG_NAME; refusing to overwrite it" >&2
exit 1
fi
ASSETS=()
for filename in "${EXPECTED[@]}"; do
ASSETS+=("$ASSET_DIR/$filename")
done
# Keep the release invisible while all assets upload; only the final edit publishes it.
gh release create "$TAG_NAME" "${ASSETS[@]}" \
--target "$TARGET_SHA" \
--title "$TITLE" \
--notes-file "$NOTES_FILE" \
--draft \
--prerelease
mapfile -t REMOTE_ASSETS < <(gh release view "$TAG_NAME" --json assets --jq '.assets[].name' | LC_ALL=C sort)
diff -u <(printf '%s\n' "${EXPECTED[@]}") <(printf '%s\n' "${REMOTE_ASSETS[@]}")
test "$(gh release view "$TAG_NAME" --json isDraft --jq '.isDraft')" = true
test "$(gh release view "$TAG_NAME" --json isPrerelease --jq '.isPrerelease')" = true
gh release edit "$TAG_NAME" --draft=false --prerelease
test "$(gh release view "$TAG_NAME" --json isDraft --jq '.isDraft')" = false
test "$(gh release view "$TAG_NAME" --json isPrerelease --jq '.isPrerelease')" = true
+6 -16
View File
@@ -9,8 +9,6 @@ venv/
env/
build/
dist/
!frontends/desktop/dist/
!frontends/desktop/dist/**
*.egg-info/
.streamlit/
@@ -57,7 +55,6 @@ memory/L4_raw_sessions/*
!memory/ljqCtrl_sop.md
!memory/macljqCtrl.py
!memory/computer_use.md
!memory/ljqCtrlBg.py
# procmem_scanner related tools
!memory/procmem_scanner.py
@@ -81,9 +78,6 @@ memory/L4_raw_sessions/*
# Plan SOP
!memory/plan_sop.md
# UltraPlan SOP
!memory/ultraplan_sop.md
# Goal Mode SOP
!memory/goal_mode_sop.md
@@ -125,10 +119,9 @@ memory/L4_raw_sessions/*
restore_commit.txt
sche_tasks/
# CDP Bridge 密钥配置(首次运行自动生成)
assets/tmwd_cdp_bridge/config.js
assets/copilot_proxy.pyw
# 模型 instruction(本地定制,不入库)
assets/glm52_instruction.txt
assets/gpt55_instruction.txt
**log.*
# Reflect (ignore new files, whitelist existing)
@@ -141,18 +134,15 @@ reflect/*
# Conductor IM plugins: 私有插件/配置不入库。示例为 _email_example.py / _lark_example.py
# (以 _ 开头不会被加载器自动轮询)。去掉下划线/示例后缀复制为 email.py / lark.py 即启用。
frontends/conductor_im_plugins/*
frontends/conductor_im_plugins/wechat.py
frontends/conductor_im_plugins/email.py
frontends/conductor_im_plugins/lark.py
frontends/conductor_im_plugins/config.local.json
# Universal: never track __pycache__ anywhere
**/__pycache__/
.claude/
# Project Mode SOP
!memory/project_mode_sop.md
# 本地 bug / 局限报告草稿(交给维护者参考,不进仓库)
/BUG_*.md
# Beta 渠道:订阅门户插件(仅 beta 分发携带,不入库)
plugins/subscription_portal.py
+17 -2
View File
@@ -774,7 +774,7 @@ GA Web 工具运行在**真实、持久化的 Chrome/Chromium 会话**中,而
<div align="center">
<table>
<tr>
<td align="center"><strong>微信群 23</strong><br/><img src="assets/images/wechat_group22.jpg" alt="微信群 23 二维码" width="240"/></td>
<td align="center"><strong>微信群 21</strong><br/><img src="assets/images/wechat_group21.jpg" alt="微信群 21 二维码" width="240"/></td>
</tr>
</table>
</div>
@@ -798,6 +798,21 @@ GA Web 工具运行在**真实、持久化的 Chrome/Chromium 会话**中,而
基于 **MIT License** 发布,详见 [`LICENSE`](LICENSE)。
> *声明:GenericAgent 官方渠道为本 GitHub 仓库和 [https://gaagent.ai](https://gaagent.ai)。DintalClaw 是目前唯一官方授权的商业合作方;除非在此处明确列出,其他使用 GenericAgent 名义的第三方网站、机构、组织或个人均非官方。*
> *声明:GenericAgent 官方渠道为本 GitHub 仓库和 https://gaagent.ai。DintalClaw 是目前唯一官方授权的商业合作方;除非在此处明确列出,其他使用 GenericAgent 名义的第三方网站、机构、组织或个人均非官方。*
---
## 📈 Star History
<div align="center">
<a href="https://star-history.com/#lsdefine/GenericAgent&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=lsdefine/GenericAgent&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=lsdefine/GenericAgent&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=lsdefine/GenericAgent&type=Date" />
</picture>
</a>
<br/><br/>
</div>
+16 -22
View File
@@ -4,10 +4,6 @@ from simple_websocket_server import WebSocketServer, WebSocket
import bottle
from bottle import request
def safe_print(*a, **k):
try: print(*a, **k)
except: pass
class Session:
def __init__(self, session_id, info, client=None):
self.id = session_id
@@ -33,7 +29,7 @@ class Session:
self.connect_at = time.time()
self.disconnect_at = None
def mark_disconnected(self):
if self.disconnect_at is None: safe_print(f"Tab disconnected: {self.url} (Session: {self.id})")
if self.disconnect_at is None: print(f"Tab disconnected: {self.url} (Session: {self.id})")
self.disconnect_at = time.time()
@@ -52,8 +48,6 @@ class TMWebDriver:
def start_http_server(self):
self.app = app = bottle.Bottle()
@app.hook('before_request')
def reject_web_origin(): request.headers.get('Origin') is not None and bottle.abort(403)
@app.route('/api/longpoll', method=['GET', 'POST'])
def long_poll():
@@ -62,7 +56,7 @@ class TMWebDriver:
session_info = {'url': data.get('url'), 'title': data.get('title', ''), 'type': 'http'}
if session_id not in self.sessions:
session = Session(session_id, session_info, queue.Queue())
safe_print(f"Browser http connected: {session.url} (Session: {session_id})")
print(f"Browser http connected: {session.url} (Session: {session_id})")
self.sessions[session_id] = session
session = self.sessions[session_id]
if session.disconnect_at is not None and session.type != 'http': session.reconnect(queue.Queue(), session_info)
@@ -101,7 +95,7 @@ class TMWebDriver:
timeout = float(data.get('timeout', 10.0))
try: result = self.execute_js(code, timeout=timeout, session_id=session_id)
except Exception as e: return json.dumps({'r': {'error': str(e)}}, ensure_ascii=False)
try: safe_print('[remote result]', (str(code)[:50] + ' RESULT:' +str(result)[:50]).replace('\n', ' '))
try: print('[remote result]', (str(code)[:50] + ' RESULT:' +str(result)[:50]).replace('\n', ' '))
except Exception: pass
return json.dumps({'r': result}, ensure_ascii=False)
return 'ok'
@@ -136,7 +130,7 @@ class TMWebDriver:
elif data.get('type') in ['ext_ready', 'tabs_update']:
tabs = data.get('tabs', [])
current_tab_ids = {str(tab['id']) for tab in tabs}
safe_print(f"Received tabs update: {current_tab_ids}")
print(f"Received tabs update: {current_tab_ids}")
for sid in list(driver.sessions.keys()):
sess = driver.sessions[sid]
if sess.type == 'ext_ws' and sid not in current_tab_ids:
@@ -153,18 +147,18 @@ class TMWebDriver:
elif data.get('type') == 'error':
driver.results[data.get('id')] = {'success': False, 'data': data.get('error'), 'newTabs': data.get('newTabs', [])}
except Exception as e:
safe_print(f"Error handling message: {e}")
if hasattr(self, 'data'): safe_print(self.data)
print(f"Error handling message: {e}")
if hasattr(self, 'data'): print(self.data)
def connected(self): (f"New connection from {self.address}")
def handle_close(self):
safe_print(f"WS Connection closed: {self.address}")
print(f"WS Connection closed: {self.address}")
driver._unregister_client(self)
self.server = WebSocketServer(self.host, self.port, JSExecutor)
server_thread = threading.Thread(target=self.server.serve_forever)
server_thread.daemon = True
server_thread.start()
safe_print(f"WebSocket server running on ws://{self.host}:{self.port}")
print(f"WebSocket server running on ws://{self.host}:{self.port}")
def _register_client(self, session_id: str, client: WebSocket, session_info) -> None:
is_new_session = session_id not in self.sessions
@@ -172,11 +166,11 @@ class TMWebDriver:
if is_new_session:
session = Session(session_id, session_info, client)
self.sessions[session_id] = session
safe_print(f"New tab connected: {session.url} (Session: {session_id})")
print(f"New tab connected: {session.url} (Session: {session_id})")
else:
session = self.sessions[session_id]
session.reconnect(client, session_info)
safe_print(f"Tab reconnected: {session.url} (Session: {session_id})")
print(f"Tab reconnected: {session.url} (Session: {session_id})")
self.latest_session_id = session_id
if self.default_session_id is None: self.default_session_id = session_id
@@ -186,9 +180,9 @@ class TMWebDriver:
if session.ws_client == client: session.mark_disconnected()
def execute_js(self, code, timeout=15, session_id=None) -> Any:
if session_id is None: session_id = str(self.default_session_id)
if session_id is None: session_id = self.default_session_id
if self.is_remote:
safe_print('remote_execute_js')
print('remote_execute_js')
response = self._remote_cmd({"cmd": "execute_js", "sessionId": session_id,
"code": code, "timeout": str(timeout)}).get('r', {})
if response.get('error'): raise Exception(response['error'])
@@ -202,7 +196,7 @@ class TMWebDriver:
alive_sessions = [s for s in self.sessions.values() if s.is_active()]
if alive_sessions:
session = alive_sessions[0]
safe_print(f"会话 {session_id} 未连接,自动切换到最新活动会话: {session.id}")
print(f"会话 {session_id} 未连接,自动切换到最新活动会话: {session.id}")
session_id = self.default_session_id = session.id
if not session or not session.is_active():
raise ValueError(f"会话ID {session_id} 未连接")
@@ -277,10 +271,10 @@ class TMWebDriver:
matched = self._remote_cmd({"cmd": "find_session", "url_pattern": url_pattern}).get('r', [])
else:
matched = self.find_session(url_pattern)
if not matched: return safe_print(f"警告: 未找到URL包含 '{url_pattern}' 的会话")
if len(matched) > 1: safe_print(f"警告: 找到多个URL包含 '{url_pattern}' 的会话,选择第一个")
if not matched: return print(f"警告: 未找到URL包含 '{url_pattern}' 的会话")
if len(matched) > 1: print(f"警告: 找到多个URL包含 '{url_pattern}' 的会话,选择第一个")
self.default_session_id, info = matched[0]
safe_print(f"成功设置默认会话: {self.default_session_id}: {info['url']}")
print(f"成功设置默认会话: {self.default_session_id}: {info['url']}")
return self.default_session_id
def jump(self, url, timeout=10): self.execute_js(f"window.location.href='{url}'", timeout=timeout)
+5 -4
View File
@@ -52,7 +52,7 @@ def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema,
if handler.parent.task_dir: turnstr = f'Turn {turn} ...'
if verbose: turnstr = f'**{turnstr}**'
if yield_info: yield {'turn': turn}
yield f"\n{turnstr}\n\n"
yield f"\n\n{turnstr}\n\n"
if turn%10 == 0: client.last_tools = '' # 每10轮重置一次工具描述
_hook('turn_before', locals())
_hook('llm_before', locals())
@@ -76,13 +76,14 @@ def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema,
if tool_name == 'no_tool': pass
else:
if verbose: yield f"🛠️ Tool: `{tool_name}` 📥 args:\n````text\n{get_pretty_json(args)}\n````\n"
else: yield f"🛠️ {tool_name}({_compact_tool_args(tool_name, args)})\n"
else: yield f"🛠️ {tool_name}({_compact_tool_args(tool_name, args)})\n\n\n"
handler.current_turn = turn
gen = handler.dispatch(tool_name, args, response, index=ii, tool_num=len(tool_calls))
try:
v = next(gen)
if verbose: yield '`````\n' + v
outcome = (yield from gen) if verbose else exhaust(gen)
def proxy(): yield v; return (yield from gen)
if verbose: yield '`````\n'
outcome = (yield from proxy()) if verbose else exhaust(proxy())
if verbose: yield '`````\n'
except StopIteration as e: outcome = e.value
+44 -83
View File
@@ -1,4 +1,4 @@
import os, sys, threading, queue, time, json, re, random, locale, glob
import os, sys, threading, queue, time, json, re, random, locale
os.environ.setdefault('GA_LANG', 'zh' if any(k in (locale.getlocale()[0] or '').lower() for k in ('zh', 'chinese')) else 'en')
if sys.stdout is None: sys.stdout = open(os.devnull, "w")
elif hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(errors='replace')
@@ -14,12 +14,10 @@ except Exception: pass
from ga import GenericAgentHandler, smart_format, get_global_memory, format_error, consume_file
script_dir = os.path.dirname(os.path.abspath(__file__))
BANNED_TOOLS = (['ask_user', 'start_long_term_update'] if '--no-user-tools' in sys.argv else [])
def load_tool_schema(suffix=''):
global TOOLS_SCHEMA
TS = open(os.path.join(script_dir, f'assets/tools_schema{suffix}.json'), 'r', encoding='utf-8').read()
TOOLS_SCHEMA = json.loads(TS if os.name == 'nt' else TS.replace('powershell', 'bash'))
TOOLS_SCHEMA = [t for t in TOOLS_SCHEMA if t.get('function', {}).get('name') not in BANNED_TOOLS]
load_tool_schema()
lang_suffix = '_en' if os.environ.get('GA_LANG', '') == 'en' else ''
@@ -31,6 +29,12 @@ mem_insight = os.path.join(mem_dir, 'global_mem_insight.txt')
if not os.path.exists(mem_insight):
t = os.path.join(script_dir, f'assets/global_mem_insight_template{lang_suffix}.txt')
open(mem_insight, 'w', encoding='utf-8').write(open(t, encoding='utf-8').read() if os.path.exists(t) else '')
cdp_cfg = os.path.join(script_dir, 'assets/tmwd_cdp_bridge/config.js')
if not os.path.exists(cdp_cfg):
try:
os.makedirs(os.path.dirname(cdp_cfg), exist_ok=True)
open(cdp_cfg, 'w', encoding='utf-8').write(f"const TID = '__ljq_{hex(random.randint(0, 99999999))[2:8]}';")
except Exception as e: print(f'[WARN] CDP config init failed: {e} — advanced web features (tmwebdriver) will be unavailable.')
def get_system_prompt():
with open(os.path.join(script_dir, f'assets/sys_prompt{lang_suffix}.txt'), 'r', encoding='utf-8') as f: prompt = f.read()
@@ -47,27 +51,22 @@ class GenericAgent:
os.makedirs(os.path.join(script_dir, 'temp'), exist_ok=True)
self.lock = threading.Lock()
self.task_dir = None
self.history = []; self.handler = None; self.all_outputs = []
self.history = []; self.handler = None;
self.task_queue = queue.Queue()
self.is_running = False; self.stop_sig = False; self.llm_no = 0;
# Output queue of the task currently executing (None when idle). Lets a UI that
# lost its own handle (page refresh, second client) re-attach to the live task.
self._current_queue = None
self.is_running = False; self.stop_sig = False; self.llm_no = 0;
self.inc_out = False; self.verbose = True
self.peer_hint = True
self.force_non_stream = False
logid = f'{(time.time_ns() + random.randrange(1_000_000)) % 1_000_000:06d}'
self.log_path = os.path.join(script_dir, f'temp/model_responses/model_responses_{logid}.txt')
self.llmclient = None
self.load_llm_sessions()
self.extra_sys_prompts = []
self.intervene = self.extrakeyinfo = None
def load_llm_sessions(self):
mykeys, changed = reload_mykeys()
if not changed and hasattr(self, 'llmclients'): return
try: oldhistory, oldname = self.llmclient.backend.history, self.llmclient.backend.name
except: oldhistory = oldname = None
try: oldhistory = self.llmclient.backend.history
except: oldhistory = None
llm_sessions = []
for k, cfg in mykeys.items():
if not any(x in k for x in ['api', 'config', 'cookie']): continue
@@ -83,22 +82,20 @@ class GenericAgent:
else: llm_sessions[i] = ToolClient(mixin)
except Exception as e: print(f'\n\n\n[ERROR] Failed to init MixinSession with cfg {s["mixin_cfg"]}: {e}!!!\n\n')
self.llmclients = llm_sessions
if not self.llmclients: return
names = [c.backend.name if not isinstance(c, dict) else f'BADMIXIN_{i}' for i, c in enumerate(self.llmclients)]
if oldname in names: self.llm_no = names.index(oldname)
self.llmclient = self.llmclients[self.llm_no%len(self.llmclients)]
if oldhistory: self.llmclient.backend.history = oldhistory
def next_llm(self, n=-1):
self.load_llm_sessions()
if not self.llmclients: return
self.llm_no = ((self.llm_no + 1) if n < 0 else n) % len(self.llmclients)
lastc = self.llmclient
self.llmclient = self.llmclients[self.llm_no]
try: self.llmclient.backend.history = lastc.backend.history
except: raise Exception('[ERROR] BAD Mixin config: Check your mykey.py')
self.llmclient.last_tools = ''
load_tool_schema()
name = self.get_llm_name(model=True)
if 'glm' in name or 'minimax' in name or 'kimi' in name: load_tool_schema('_cn')
else: load_tool_schema()
def list_llms(self):
self.load_llm_sessions()
return [(i, self.get_llm_name(b), i == self.llm_no) for i, b in enumerate(self.llmclients)]
@@ -106,27 +103,13 @@ class GenericAgent:
b = self.llmclient if b is None else b
if isinstance(b, dict): return 'BADCONFIG_MIXIN'
if model: return b.backend.model.lower()
return f"{type(b.backend).__name__.replace('Session', '')}/{b.backend.name}"
def get_ctx_multiplier(self): return getattr(self.llmclient.backend, 'maxlen_multiplier', 1.0)
return f"{type(b.backend).__name__}/{b.backend.name}"
def abort(self):
if not self.is_running: return
print('Abort current task...')
self.stop_sig = True
if self.handler is not None: self.handler.code_stop_signal.append(1)
for sess in getattr(self.llmclient.backend, '_sessions', [self.llmclient.backend]):
sess.should_stop = lambda: self.stop_sig # live read; cleared by run()'s finally
try: # wake a recv() blocked in another thread (waiting headers OR reading stream). Verified on Windows:
# shutdown()/close() do NOT wake it (makefile refcount defers closesocket); _real_close() does
import socket as _socket
sock = sys.modules['llmcore']._INFLIGHT[sess._tid] # socket registered at urllib3 request() time, before headers
try: sock.shutdown(_socket.SHUT_RDWR) # for non-Windows semantics
except OSError: pass
try: sock._real_close() # CPython internal; bypasses refcount -> actual closesocket
except AttributeError: sock.close()
except Exception: pass
try: sess.active_response.close()
except Exception: pass
def put_task(self, query, source="user", images=None):
display_queue = queue.Queue()
@@ -157,13 +140,11 @@ class GenericAgent:
raw_query = self._handle_slash_cmd(raw_query, display_queue)
if raw_query is None:
self.task_queue.task_done(); continue
self.is_running = True; self._current_queue = display_queue
self.is_running = True
if len(raw_query) > 2000:
task_file = os.path.join(script_dir, 'temp', f'user_prompt_{os.getpid()}_{time.time_ns()}.md')
task_file = os.path.join(script_dir, 'temp', f'user_prompt_{int(time.time())}.md')
with open(task_file, 'w', encoding='utf-8') as f: f.write(raw_query)
raw_query = f'Long user prompt saved to {task_file}. Read and execute.'
self.all_outputs.append({"input": raw_query, "outputs": []})
if len(self.all_outputs) > 10000: self.all_outputs = self.all_outputs[-5000:]
rquery = smart_format(raw_query.replace('\n', ' '), max_str_len=200)
self.history.append(f"[USER]: {rquery}")
sys_prompt = get_system_prompt() + '\n'.join(self.extra_sys_prompts) + getattr(self.llmclient.backend, 'extra_sys_prompt', '')
@@ -183,7 +164,7 @@ class GenericAgent:
gen = agent_runner_loop(self.llmclient, sys_prompt, raw_query, handler, TOOLS_SCHEMA,
max_turns=180, verbose=self.verbose, yield_info=True)
try:
full_resp = ""; last_pos = 0; curr_turn = 0; turn_resps = self.all_outputs[-1]["outputs"]
full_resp = ""; last_pos = 0; curr_turn = 0; turn_resps = []
for chunk in gen:
if consume_file(self.task_dir, '_stop'): self.abort()
if self.stop_sig: break
@@ -197,6 +178,8 @@ class GenericAgent:
if self.inc_out and last_pos < len(full_resp):
display_queue.put({'next': full_resp[last_pos:], 'source': source,
'turn': curr_turn, 'outputs': turn_resps[-2:]})
#if '</summary>' in full_resp: full_resp = full_resp.replace('</summary>', '</summary>\n\n')
#if '</file_content>' in full_resp: full_resp = re.sub(r'<file_content>\s*(.*?)\s*</file_content>', r'\n````\n<file_content>\n\1\n</file_content>\n````', full_resp, flags=re.DOTALL)
display_queue.put({'done': full_resp, 'source': source, 'turn': curr_turn, 'outputs': turn_resps.copy()})
self.history = handler.history_info
except Exception as e:
@@ -204,7 +187,7 @@ class GenericAgent:
display_queue.put({'done': full_resp + f'\n```\n{format_error(e)}\n```', 'source': source, 'turn': curr_turn, 'outputs': turn_resps.copy()})
finally:
if self.stop_sig: print('User aborted the task.')
self.is_running = self.stop_sig = False # keep _current_queue: its final 'done' may still be unclaimed (refreshed UI salvages it); next task overwrites it
self.is_running = self.stop_sig = False
self.task_queue.task_done()
if self.handler is not None: self.handler.code_stop_signal.append(1)
@@ -215,88 +198,66 @@ if __name__ == '__main__':
from datetime import datetime
parser = argparse.ArgumentParser()
parser.add_argument('--task', metavar='IODIR', help='一次性任务模式,先看subagent.md')
parser.add_argument('--func', metavar='PROMPT_FILE', help='纯函数模式:读prompt文件→结果写prompt.out.txt→退出')
parser.add_argument('--reflect', metavar='SCRIPT', help='反射模式:加载监控脚本,check()触发时发任务')
parser.add_argument('--input', help='prompt')
parser.add_argument('--history', help='history json file')
parser.add_argument('--llm_no', type=int, default=0)
parser.add_argument('--verbose', action='store_true')
parser.add_argument('--nobg', action='store_true')
parser.add_argument('--nolog', action='store_true')
parser.add_argument('--no-user-tools', action='store_true')
args, _unknown = parser.parse_known_args()
_extra_args = dict(zip([k.lstrip('-') for k in _unknown[::2]], _unknown[1::2])) if _unknown else {}
_reflect_args = dict(zip([k.lstrip('-') for k in _unknown[::2]], _unknown[1::2])) if _unknown else {}
if (args.func or args.task) and not args.nobg:
if args.task and not args.nobg:
import subprocess, platform
cmd = [sys.executable, os.path.abspath(__file__)] + [a for a in sys.argv[1:]] + ['--nobg']
if args.task:
d = os.path.join(script_dir, f'temp/{args.task}'); os.makedirs(d, exist_ok=True)
out = open(os.path.join(d, 'stdout.log'), 'w', encoding='utf-8')
err = open(os.path.join(d, 'stderr.log'), 'w', encoding='utf-8')
else: out, err = subprocess.DEVNULL, subprocess.DEVNULL
d = os.path.join(script_dir, f'temp/{args.task}'); os.makedirs(d, exist_ok=True)
p = subprocess.Popen(cmd, cwd=script_dir,
creationflags=0x08000000 if platform.system() == 'Windows' else 0,
stdout=out, stderr=err)
stdout=open(os.path.join(d, 'stdout.log'), 'w', encoding='utf-8'),
stderr=open(os.path.join(d, 'stderr.log'), 'w', encoding='utf-8'))
print('PID:', p.pid); sys.exit(0)
agent = GenericAgent()
if args.nolog: agent.log_path = False
agent = GeneraticAgent()
agent.next_llm(args.llm_no)
agent.verbose = args.verbose
threading.Thread(target=agent.run, daemon=True).start()
histfile = args.history
if args.task:
agent.peer_hint = False
agent.force_non_stream = True
agent.task_dir = d = os.path.join(script_dir, f'temp/{args.task}'); nround = ''
infile = os.path.join(d, 'input.txt'); outfile = f'{d}/output{nround}.txt'
infile = os.path.join(d, 'input.txt')
if args.input:
os.makedirs(d, exist_ok=True)
[os.remove(f) for f in glob.glob(os.path.join(d, 'output*.txt'))]
import glob; [os.remove(f) for f in glob.glob(os.path.join(d, 'output*.txt'))]
with open(infile, 'w', encoding='utf-8') as f: f.write(args.input)
histfile = histfile or os.path.join(d, '_history.json')
elif args.func:
infile = args.func; outfile = os.path.splitext(args.func)[0] + '.out.txt'
if histfile and os.path.isfile(histfile): agent.llmclient.backend.history = json.loads(open(histfile, encoding='utf-8').read())
if args.func or args.task:
agent.peer_hint = False
if (fh := consume_file(d, '_history.json')): agent.llmclient.backend.history = json.loads(fh)
with open(infile, encoding='utf-8') as f: raw = f.read()
while True:
dq = agent.put_task(raw, source='func' if args.func else 'task')
while 'done' not in (item := dq.get(timeout=2200)):
if 'next' in item:
with open(outfile, 'w', encoding='utf-8') as f: f.write(item.get('next', ''))
with open(outfile, 'w', encoding='utf-8') as f: f.write(item['done'] + '\n\n[ROUND END]\n')
if not args.task: break
dq = agent.put_task(raw, source='task')
while 'done' not in (item := dq.get(timeout=1200)):
if 'next' in item and random.random() < 0.95: # 概率写一次中间结果
with open(f'{d}/output{nround}.txt', 'w', encoding='utf-8') as f: f.write(item.get('next', ''))
with open(f'{d}/output{nround}.txt', 'w', encoding='utf-8') as f: f.write(item['done'] + '\n\n[ROUND END]\n')
consume_file(d, '_stop') # 已经成功停下来了,避免打断下次reply
for _ in range(300): # 等reply.txt,10分钟超时
time.sleep(2)
if (raw := consume_file(d, 'reply.txt')): break
else: break
nround = nround + 1 if isinstance(nround, int) else 1
outfile = f'{d}/output{nround}.txt'
elif args.reflect:
agent.peer_hint = False
agent.force_non_stream = True
import importlib.util
spec = importlib.util.spec_from_file_location('reflect_script', args.reflect)
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
if hasattr(mod, 'init'): mod.init(_extra_args)
if hasattr(mod, 'init'): mod.init(_reflect_args)
_mt = os.path.getmtime(args.reflect)
print(f'[Reflect] loaded {args.reflect}' + (f' args={_extra_args}' if _extra_args else ''))
try:
import frontends.hub as hub
def _hub_put(t):
if agent.is_running: return {'error': 'busy', 'code': 'busy'}
agent.put_task(t, source='hub')
hub.connect(agent, f'reflect/{os.path.splitext(os.path.basename(args.reflect))[0]}', put_task=_hub_put)
except Exception: pass
print(f'[Reflect] loaded {args.reflect}' + (f' args={_reflect_args}' if _reflect_args else ''))
while True:
if os.path.getmtime(args.reflect) != _mt:
try:
spec.loader.exec_module(mod); _mt = os.path.getmtime(args.reflect)
if hasattr(mod, 'init'): mod.init(_extra_args)
if hasattr(mod, 'init'): mod.init(_reflect_args)
print('[Reflect] reloaded')
except Exception as e: print(f'[Reflect] reload error: {e}')
try: task = mod.check()
@@ -305,10 +266,9 @@ if __name__ == '__main__':
if task and task == '/exit': break
if task:
print(f'[Reflect] triggered: {task[:80]}')
if (_ln := getattr(mod, 'LLM', None)): agent.next_llm(next((i for i, b in enumerate(agent.llmclients) if not isinstance(b, dict) and b.backend.name == _ln), agent.llm_no))
dq = agent.put_task(task, source='reflect')
try:
while 'done' not in (item := dq.get(timeout=2200)): pass
while 'done' not in (item := dq.get(timeout=1200)): pass
result = item['done']
print(result)
except Exception as e:
@@ -344,4 +304,5 @@ if __name__ == '__main__':
if 'next' in item: print(item['next'], end='', flush=True)
if 'done' in item: print(); break
except KeyboardInterrupt:
agent.abort(); print('\n[Interrupted]')
agent.abort()
print('\n[Interrupted]')
+4 -4
View File
@@ -88,7 +88,7 @@ LLM_PROVIDERS = [
'type': 'native_oai',
'template': {
'name': 'deepseek', 'apikey': 'sk-<your-deepseek-key>',
'apibase': 'https://api.deepseek.com', 'model': 'deepseek-v4-pro',
'apibase': 'https://api.deepseek.com', 'model': 'deepseek-v4-flash',
'api_mode': 'chat_completions', 'reasoning_effort': 'high',
},
'key_hint': '在 https://platform.deepseek.com/api_keys 获取',
@@ -126,11 +126,11 @@ LLM_PROVIDERS = [
'template': {
'name': 'qwen', 'apikey': 'sk-<your-dashscope-key>',
'apibase': 'https://dashscope.aliyuncs.com/compatible-mode/v1',
'model': 'qwen3.6-max-preview',
'model': 'qwen3.5-plus',
'api_mode': 'chat_completions',
},
'key_hint': '在 https://bailian.console.aliyun.com/ 获取 API Key',
'model_choices': ['qwen3.6-max-preview', 'qwen3.5-plus', 'qwen3-coder-plus'],
'model_choices': ['qwen3.5-plus', 'qwen3-coder-plus'],
'extra_fields': [
{
'key': '_endpoint', 'label': '选择端点',
@@ -732,7 +732,7 @@ def _configure_advanced(provider, cfg):
proxy = ask_input("HTTP 代理地址 (proxy)", default='', hint='如 http://127.0.0.1:2082,留空跳过')
if proxy:
cfg['proxy'] = proxy
cw = ask_input("上下文窗口阈值 (context_win)", default='', hint='默认 30000,DeepSeek 默认 70000;联动压缩与工具上限')
cw = ask_input("上下文窗口阈值 (context_win)", default='', hint='NativeClaude 默认 28000,其他默认 24000')
if cw:
cfg['context_win'] = int(cw)
if cfg.get('thinking_type') == 'enabled':
-125
View File
@@ -1,125 +0,0 @@
import threading, sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from fastapi import FastAPI, Header, HTTPException, Query, Depends; from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from agentmain import GenericAgent as GA
PORT, API_KEY = int(sys.argv[1]), sys.argv[2]
app, agent, lock = FastAPI(), GA(), threading.Lock()
outputs, stopped = [], True
threading.Thread(target=agent.run, daemon=True).start()
class Req(BaseModel): prompt: str = ""
agent.verbose = False
def require_key(key: str = Query(None), x_api_key: str = Header(None, alias="X-API-Key")):
if API_KEY not in (key, x_api_key): raise HTTPException(404)
def run_task(prompt):
global stopped
segs = [] # 本任务按 turn 索引的分段输出
with lock: task_start = len(outputs)
def flush():
with lock: outputs[task_start:] = segs
try:
dq = agent.put_task(prompt, source="http")
while "done" not in (item := dq.get(timeout=2200)):
outs = item.get("outputs")
if not outs: continue
idx = max(0, int(item.get("turn", 0) or 0) - 1) # turn 1-based → 槽位 0-based
while len(segs) <= idx: segs.append("")
segs[idx] = str(outs[-1]) # 当前 turn
if len(outs) >= 2 and idx >= 1: segs[idx - 1] = str(outs[-2]) # 前一 turn 落定值
flush()
segs = [str(s) for s in item.get("outputs", [])] # done 时全量替换
flush()
finally: stopped = True
@app.post("/put_task")
def put_task(req: Req, _=Depends(require_key)):
global stopped
with lock:
if not stopped: return {"ok": False, "error": "should abort first"}
stopped = False
threading.Thread(target=run_task, args=(req.prompt,), daemon=True).start()
return {"ok": True}
@app.post("/abort")
def abort(_=Depends(require_key)): agent.abort(); return {"ok": True}
@app.post("/input")
def input_task(req: Req, _=Depends(require_key)):
global stopped
if not stopped: agent.intervene = req.prompt; return {"ok": True, "mode": "intervene"}
with lock:
if not stopped: agent.intervene = req.prompt; return {"ok": True, "mode": "intervene"}
stopped = False
threading.Thread(target=run_task, args=(req.prompt,), daemon=True).start()
return {"ok": True, "mode": "task"}
@app.get("/output")
def get_output(k: int = Query(5), _=Depends(require_key)):
with lock: r = outputs[-k:]
return {"stopped": stopped, "output": "\n".join(r),
"history": "\n".join(str(h) for h in agent.history)}
@app.get("/llm")
def llm_ep(llm_no: int = Query(None), _=Depends(require_key)):
if llm_no is not None:
agent.next_llm(llm_no)
return {"llm_no": agent.llm_no, "name": agent.get_llm_name(),
"llms": [{"no": i, "name": n, "current": a} for i, n, a in agent.list_llms()]}
@app.get("/sysprompt")
def sysprompt_ep(text: str = Query(None), _=Depends(require_key)):
if text is not None:
agent.extra_sys_prompts = [text] if text else []
return {"extra_sys_prompts": agent.extra_sys_prompts}
HELP = """GA HTTP 操作协议(所有请求带 ?key=API_KEY,或 Header X-API-Key)
GET /output?k=N 查看状态:{stopped, output(末N条), history}。stopped=true 表示空闲
POST /input {prompt} 下发指令:空闲时作为新任务,忙时作为中途干预(intervene)
POST /abort 中止当前任务
GET /llm[?llm_no=N] 查/切模型:返回 {llm_no,name,llms:[{no,name,current}]}
GET /sysprompt[?text]查/设附加系统提示(extra_sys_prompts),text 为空则清空
纠偏流程:先 GET /output 读 history 判断状态→需要时 POST /input 注入纠偏指令"""
@app.get("/help")
def help_ep(_=Depends(require_key)): return {"help": HELP}
@app.get("/")
def ui():
return HTMLResponse(f"""<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>GA Monitor</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
*{{margin:0;padding:0;box-sizing:border-box}}
body{{font:14px/1.6 system-ui,sans-serif;background:#f8f9fa;color:#212529;padding:24px;max-width:900px;margin:0 auto}}
#status{{font-size:18px;padding:8px 16px;border-radius:8px;margin-bottom:16px;display:inline-block;font-weight:600}}
.stopped{{background:#d4edda;color:#155724}}.running{{background:#fff3cd;color:#856404}}
.section{{background:#fff;border-radius:10px;padding:18px;margin-bottom:14px;box-shadow:0 1px 4px rgba(0,0,0,.06)}}
.section h3{{color:#533483;margin-bottom:8px;font-size:15px}}
textarea{{width:100%;height:80px;background:#fff;color:#212529;border:1px solid #ced4da;border-radius:6px;padding:10px;font:inherit;resize:vertical}}
button{{padding:10px 24px;background:#533483;color:#fff;border:none;border-radius:6px;font:inherit;cursor:pointer;font-weight:500}}
button:hover{{background:#7c3aed}}
pre{{background:#f1f3f5;padding:12px;border-radius:6px;overflow-x:auto;font:13px monospace;max-height:400px;overflow-y:auto}}
</style></head><body>
<div id="status" class="stopped">● Loading...</div>
<div class="section"><h3>Output</h3><div id="output"></div></div>
<div class="section"><h3>History</h3><div id="history"></div></div>
<textarea id="prompt" placeholder="Enter instruction..."></textarea>
<button onclick="send()">Send</button>
<script>
const K=new URLSearchParams(location.search).get('key')||'';
async function poll(){{let r=await fetch('/output',{{headers:{{'X-API-Key':K}}}});let d=await r.json();
let s=document.getElementById('status');s.textContent=(d.stopped?'● Stopped':'● Running');s.className=d.stopped?'stopped':'running';
document.getElementById('output').innerHTML=marked.parse(d.output||'_empty_');
document.getElementById('history').innerHTML=marked.parse(d.history||'_empty_');}}
async function send(){{let p=document.getElementById('prompt').value;if(!p)return;
await fetch('/input',{{method:'POST',headers:{{'X-API-Key':K,'Content-Type':'application/json'}},body:JSON.stringify({{prompt:p}})}});
document.getElementById('prompt').value='';poll();}}
poll();setInterval(poll,3000);
</script></body></html>""")
if __name__ == "__main__":
import uvicorn; uvicorn.run(app, host="0.0.0.0", port=PORT)
+28 -40
View File
@@ -8,7 +8,7 @@ GenericAgent one-click portable deployer for Windows.
Modes:
Default/Mainland: download GenericAgent.zip + uv from user's VPS, set China PyPI mirror; no Git download.
Default/Mainland: download GenericAgent.zip + uv + PortableGit from user's VPS, set China PyPI mirror.
GLOBAL=1: clone GenericAgent from GitHub; uv and PortableGit also come from GitHub releases; no PyPI mirror.
@@ -16,7 +16,7 @@ Modes:
Portable components are installed under <InstallDir>\.portable:
uv and Python installed by uv; GLOBAL=1 also installs PortableGit.
uv, Python installed by uv, PortableGit.
#>
@@ -64,8 +64,6 @@ $Deps = @("requests>=2.28", "beautifulsoup4>=4.12", "bottle>=0.12", "simple-webs
$MainlandIndex = "https://pypi.tuna.tsinghua.edu.cn/simple"
$MainlandPythonMirror = "https://mirror.nju.edu.cn/github-release/astral-sh/python-build-standalone"
$GlobalMode = ($env:GLOBAL -eq "1")
if ($GlobalMode) {
@@ -245,11 +243,7 @@ if (Test-Path $uvPyDir) {
Say "Installing Python $PythonVersion via uv"
$pythonInstallArgs = @("python", "install", $PythonVersion, "--no-bin")
if (!$GlobalMode) { $pythonInstallArgs += @("--mirror", $MainlandPythonMirror) }
$ec = Invoke-Native { & $UvExe @pythonInstallArgs }
$ec = Invoke-Native { & $UvExe python install $PythonVersion }
if ($ec -ne 0) { Die "uv python install failed" }
@@ -261,44 +255,38 @@ Ok "Python: $(& $PythonExe --version)"
$PythonDir = Split-Path $PythonExe -Parent
# PortableGit (GitHub release in GLOBAL mode, user's VPS otherwise). Needed for GLOBAL=1 and useful for user shell.
if ($GlobalMode) {
if (!(Test-Path $GitExe) -or $Force) {
# GLOBAL=1 needs PortableGit to clone GenericAgent from GitHub.
Download-File $GitUrl $GitExeArchive
if (!(Test-Path $GitExe) -or $Force) {
if (Test-Path $GitDir) { Remove-Item -Recurse -Force $GitDir }
Download-File $GitUrl $GitExeArchive
New-Item -ItemType Directory -Force -Path $GitDir | Out-Null
if (Test-Path $GitDir) { Remove-Item -Recurse -Force $GitDir }
Say "Extracting PortableGit"
New-Item -ItemType Directory -Force -Path $GitDir | Out-Null
$ec = Invoke-Native { & $GitExeArchive -y -o"$GitDir" | Out-Null }
Say "Extracting PortableGit"
$ec = Invoke-Native { & $GitExeArchive -y -o"$GitDir" | Out-Null }
if ($ec -ne 0) { Die "PortableGit extraction failed" }
}
if (!(Test-Path $GitExe)) { Die "git.exe missing: $GitExe" }
Ok "Git: $(& $GitExe --version)"
$GitBin = Split-Path $GitExe -Parent
$GitUsrBin = Join-Path $GitDir "usr\bin"
$env:PATH = "$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;$env:PATH"
} else {
$env:PATH = "$Bin;$PythonDir;$PythonDir\Scripts;$env:PATH"
if ($ec -ne 0) { Die "PortableGit extraction failed" }
}
if (!(Test-Path $GitExe)) { Die "git.exe missing: $GitExe" }
Ok "Git: $(& $GitExe --version)"
$PythonDir = Split-Path $PythonExe -Parent
$GitBin = Split-Path $GitExe -Parent
$GitUsrBin = Join-Path $GitDir "usr\bin"
$env:PATH = "$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;$env:PATH"
# Fetch/update GenericAgent source.
@@ -471,7 +459,7 @@ set "UV_DEFAULT_INDEX=$MainlandIndex"
set "PIP_INDEX_URL=$MainlandIndex"
set "PATH=$Bin;$PythonDir;$PythonDir\Scripts;%PATH%"
set "PATH=$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;%PATH%"
echo Activated GenericAgent portable env: %GENERICAGENT_HOME%
@@ -493,7 +481,7 @@ echo Activated GenericAgent portable env: %GENERICAGENT_HOME%
`$env:PIP_INDEX_URL = "$MainlandIndex"
`$env:PATH = "$Bin;$PythonDir;$PythonDir\Scripts;`$env:PATH"
`$env:PATH = "$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;`$env:PATH"
Write-Host "Activated GenericAgent portable env: `$env:GENERICAGENT_HOME" -ForegroundColor Green
@@ -509,7 +497,7 @@ Ok "Verification:"
& $PythonExe --version
if ($GlobalMode) { & $GitExe --version }
& $GitExe --version
& $PythonExe -c "import requests, bs4, bottle; print('deps ok')"
-215
View File
@@ -1,215 +0,0 @@
from contextlib import contextmanager, redirect_stdout, redirect_stderr
from concurrent.futures import ThreadPoolExecutor
from time import time, sleep
import html, io, json, os, re, subprocess, sys, tempfile, threading, traceback, urllib.request, webbrowser
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
__all__ = ["plan", "phase", "parallel", "mapchain"]
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_PORT = int(os.environ.get("GA_ULTRAPLAN_PORT", "47831"))
_T0 = time(); _phases = []; _phase_stack = []; _tasks = []; _current = "idle"; _events = []; _srv = None; _last = time(); _lock = threading.Lock(); _exec_lock = threading.Lock()
_TASK_SLUG = "task"; _FUNC_SEQ = 0; _PLANNED = False; _SESSION = None; _sessions = {}
_RUN_DIR = os.path.abspath(os.environ.get("GA_ULTRAPLAN_RUNDIR", os.path.join(_ROOT, "temp", "ultraplan_default")))
os.makedirs(_RUN_DIR, exist_ok=True)
def _bind(rundir):
global _SESSION, _RUN_DIR, _phases, _phase_stack, _tasks, _current, _events, _FUNC_SEQ, _TASK_SLUG
key = os.path.abspath(rundir); os.makedirs(key, exist_ok=True)
s = _sessions.setdefault(key, {"rundir": key, "phases": [], "phase_stack": [], "tasks": [], "current": "idle", "events": [], "func_seq": 0, "task_slug": "task"})
_SESSION = key; _RUN_DIR = key; _phases = s["phases"]; _phase_stack = s["phase_stack"]; _tasks = s["tasks"]; _current = s["current"]; _events = s["events"]; _FUNC_SEQ = s["func_seq"]; _TASK_SLUG = s["task_slug"]
return s
def _save_session():
if _SESSION in _sessions:
_sessions[_SESSION].update(current=_current, func_seq=_FUNC_SEQ, task_slug=_TASK_SLUG)
def _need_plan():
if not _PLANNED: raise RuntimeError("call plan(rundir) as the first UltraPlan statement")
def _slug(s):
s = re.sub(r"[^a-zA-Z0-9]+", "_", str(s)).strip("_").lower()
return s[:80] or "task"
def _task_slug(path):
stem = os.path.splitext(os.path.basename(path or "task"))[0]
parts = [_slug(x) for x in re.split(r"[_\-]+", stem)]
stop = {"ultra", "ultraplan", "script", "boot", "build", "test", "debug", "verify", "explore", "reduce", "phase"}
parts = [p for p in parts if p and not p.isdigit() and p not in stop]
return "_".join(parts) or _slug(stem)
def _note(s):
global _last
with _lock:
_last = time(); _events.append(f"{_last-_T0:7.1f}s {s}"); del _events[:-60]
def _phase_lines(nodes, depth=0):
out = []
for p in nodes:
pre = " " * depth; mark = ">>" if p["on"] else " "
out.append(f"{pre}{mark} {p['status']:<7} {p['name']}" + (f" - {p['desc']}" if p['desc'] else ""))
out += [f"{pre} | {op}" for op in p.get("ops", [])[-8:]]
out += [f"{pre} - {t['status']:<5} {t['desc']}" for t in p.get("tasks", [])[-20:]]
out += _phase_lines(p.get("children", []), depth + 1)
return out
def _page():
with _lock:
lines = ["GA UltraPlan"]
for key, s in _sessions.items():
lines += ["", f"== {os.path.basename(key) or key} ==", f"rundir: {key}", f"current: {s['current']}", "", "phases:"]
lines += _phase_lines(s["phases"]) or ["(none)"]
lines += ["", "recent tasks:"]
lines += [f"{t['status']:<7} {t['desc']}" for t in s["tasks"][-12:]] or ["(none)"]
lines += ["", "events:", *s["events"][-30:]]
if not _sessions: lines += ["", "(no sessions)"]
return "<meta http-equiv=refresh content=1><pre>" + html.escape("\n".join(lines)) + "</pre>"
class _H(BaseHTTPRequestHandler):
def do_GET(self):
b = _page().encode("utf-8"); self.send_response(200); self.send_header("Content-Type", "text/html; charset=utf-8"); self.end_headers(); self.wfile.write(b)
def do_POST(self):
global _TASK_SLUG, _PLANNED
if self.path != "/exec": self.send_response(404); self.end_headers(); return
n = int(self.headers.get("Content-Length", "0")); req = json.loads(self.rfile.read(n).decode("utf-8"))
out = io.StringIO(); err = io.StringIO(); rc = 0
with _exec_lock, redirect_stdout(out), redirect_stderr(err):
_bind(req["rundir"]); _note("exec: " + req.get("path", "<script>"))
cwd = os.getcwd(); old_env = os.environ.copy(); os.environ["GA_ULTRAPLAN_DAEMON"] = "1"; _PLANNED = False; _TASK_SLUG = req.get("task") or _task_slug(req.get("path")); _save_session()
try:
if req.get("cwd"): os.chdir(req["cwd"])
g = {"__name__": "__main__", "__file__": req.get("path", "<ultraplan>")}
exec(compile(req.get("code", ""), g["__file__"], "exec"), g, g)
except SystemExit as e:
rc = int(e.code or 0) if isinstance(e.code, int) else 1
except Exception:
rc = 1; traceback.print_exc()
finally:
_save_session(); os.chdir(cwd); os.environ.clear(); os.environ.update(old_env)
body = json.dumps({"returncode": rc, "stdout": out.getvalue(), "stderr": err.getvalue()}).encode("utf-8")
self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers(); self.wfile.write(body)
def log_message(self, *a): pass
def _serve_daemon():
global _srv
sys.modules.setdefault("assets.ga_ultraplan", sys.modules[__name__])
_srv = ThreadingHTTPServer(("127.0.0.1", _PORT), _H); _srv.timeout = 60; url = f"http://127.0.0.1:{_PORT}/"
print(f"[ultraplan] {url}", flush=True)
if os.environ.get("GA_ULTRAPLAN_BROWSER") != "0": webbrowser.open(url)
while time() - _last < 3600: _srv.handle_request()
def _ping():
try: urllib.request.urlopen(f"http://127.0.0.1:{_PORT}/", timeout=0.5).read(1); return True
except Exception: return False
def _show():
if os.environ.get("GA_ULTRAPLAN_DAEMON") == "1" or os.environ.get("GA_ULTRAPLAN_HTML") == "0": return
if not _ping():
subprocess.Popen([sys.executable, __file__, "--daemon"], cwd=_ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env={**os.environ, "GA_ULTRAPLAN_DAEMON":"1"})
for _ in range(20):
if _ping(): break
sleep(0.25)
def plan(rundir):
global _PLANNED
if _PLANNED: return
_PLANNED = True; _bind(rundir); _save_session()
if os.environ.get("GA_ULTRAPLAN_DAEMON") == "1": return
_show(); path = os.path.abspath(sys.argv[0]); code = open(path, encoding="utf-8").read()
data = json.dumps({"path": path, "cwd": os.getcwd(), "rundir": _RUN_DIR, "task": _task_slug(path), "code": code}).encode("utf-8")
r = urllib.request.urlopen(urllib.request.Request(f"http://127.0.0.1:{_PORT}/exec", data=data, headers={"Content-Type":"application/json"}), timeout=None)
resp = json.loads(r.read().decode("utf-8")); sys.stdout.write(resp.get("stdout", "")); sys.stderr.write(resp.get("stderr", "")); sys.exit(resp.get("returncode", 1))
@contextmanager
def phase(name, desc=""):
global _current
_need_plan(); t = time(); p = {"name": name, "desc": desc, "status": "run", "on": True, "children": [], "tasks": [], "ops": []}
with _lock:
(_phase_stack[-1]["children"] if _phase_stack else _phases).append(p)
_phase_stack.append(p); _current = f"phase: {name}"; _save_session()
print(f"[phase] {name}" + (f" - {desc}" if desc else ""), flush=True); _note(f"phase start: {name}")
failed = False
try:
yield
except Exception:
failed = True; raise
finally:
dt = time() - t; status = "fail" if failed else "done"
with _lock:
p["status"] = status; p["on"] = False
if _phase_stack and _phase_stack[-1] is p: _phase_stack.pop()
elif p in _phase_stack: _phase_stack.remove(p)
if _phase_stack: _current = f"phase: {_phase_stack[-1]['name']}"
else: _current = ("failed" if failed else "all phases done") + f"; last: {name} ({dt:.1f}s)"
_save_session()
print(f"[{status}] {name} ({dt:.1f}s)", flush=True)
print("[next] Main agent must continue orchestration or stop; do not take over task work.", flush=True)
_note(f"phase {status}: {name} ({dt:.1f}s)")
def _task(desc, status="run"):
with _lock:
t = {"desc": str(desc), "status": status}; _tasks.append(t); del _tasks[:-80]
if _phase_stack: _phase_stack[-1]["tasks"].append(t)
return t
def _task_done(t, status="done"):
with _lock: t["status"] = status
def _op(s):
with _lock:
if _phase_stack: _phase_stack[-1]["ops"].append(s)
def _fmt(x, data):
return x.format(**data) if isinstance(x, str) else x
def _subagent(desc, prompt=None, *, llm_no=0, timeout=3600):
global _FUNC_SEQ
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_FUNC_SEQ += 1; path = os.path.join(_RUN_DIR, f"{_FUNC_SEQ:03d}_{_TASK_SLUG}_{_slug(desc)}.txt")
with open(path, "w", encoding="utf-8") as f:
f.write(desc if prompt is None else prompt)
print(f"[subagent] {desc} -> {path}", flush=True); _note(f"agent: {desc}")
cmd = [
sys.executable, os.path.join(root, "agentmain.py"), "--func", path,
"--llm_no", str(llm_no), "--nobg", "--nolog", "--no-user-tools",
]
r = subprocess.run(cmd, cwd=root, text=True, capture_output=True, timeout=timeout)
if r.returncode: raise RuntimeError(f"subagent failed: {desc}\n{r.stdout}\n{r.stderr}")
return os.path.splitext(path)[0] + ".out.txt"
def _run(task, data):
task = task() if callable(task) else task
if isinstance(task, (tuple, list)):
desc = _fmt(task[0], data); t = _task(desc)
try: return _subagent(desc, _fmt(task[1] if len(task) > 1 else task[0], data), llm_no=data.get("llm_no", 0), timeout=data.get("timeout", 3600))
except Exception: _task_done(t, "fail"); raise
finally:
if t["status"] == "run": _task_done(t)
if isinstance(task, dict):
d = {**data, **task.get("data", {})}; desc = _fmt(task.get("desc", "task"), d); t = _task(desc)
try: return _subagent(desc, _fmt(task.get("prompt", task.get("desc", "task")), d), llm_no=task.get("llm_no", d.get("llm_no", 0)), timeout=task.get("timeout", d.get("timeout", 3600)))
except Exception: _task_done(t, "fail"); raise
finally:
if t["status"] == "run": _task_done(t)
return task
def parallel(tasks, max_workers=None, _label=None, **data):
global _current
_need_plan(); tasks = list(tasks); label = _label or f"parallel: {len(tasks)} tasks"
with _lock: _current = label; _save_session()
_op(label); _note(label)
with ThreadPoolExecutor(max_workers=max_workers or min(3, len(tasks) or 1)) as ex:
return list(ex.map(lambda t: _run(t, data), tasks))
def mapchain(items, *steps, max_workers=None, **data):
global _current
_need_plan(); items = list(items); label = f"mapchain: {len(items)} items x {len(steps)} steps"
with _lock: _current = label; _save_session()
def run(x):
for step in steps:
d = {**data, "item": x, "previous": x}; x = _run(step(x) if callable(step) else step, d)
return x
return parallel([lambda x=x: run(x) for x in items], max_workers=max_workers, _label=label)
if __name__ == "__main__" and "--daemon" in sys.argv:
_serve_daemon()
+6 -6
View File
@@ -2,8 +2,8 @@
需要时read L2 或 ls ../memory/ 查L3
L0(META-SOP): memory_management_sop
L2: 现空
L3: memory_cleanup_sop(记忆整理) | ui_detect.py | ocr_utils.py | subagent | web_setup_sop
| procmem_scanner | keychain | ljqCtrl_sop+.py | tmwebdriver_sop | autonomous_operation_sop | scheduled_task_sop | vision_sop | adb_ui.py | project_mode_sop
L3: memory_cleanup_sop(记忆整理) | ui_detect.py | ocr_utils.py | subagent | web_setup_sop | plan_sop
| procmem_scanner | keychain | ljqCtrl_sop+.py | tmwebdriver_sop | autonomous_operation_sop | scheduled_task_sop | vision_sop | adb_ui.py
L4: L4_raw_sessions/ 历史会话
浏览器特殊操作: tmwebdriver_sop(文件上传/图搜/PDF blob/物理坐标/HttpOnly Cookie/autofill突破/跨域iframe/CDP/跨tab)
@@ -13,11 +13,11 @@ UI操作/截图/视觉: computer_use.md | vision_sop | 窗口截图必用ljqCtrl
手机:adb_ui.py
[RULES]
1. 搜索先行: 搜文件名禁长时间递归, 网络搜索一定优先使用web工具的google, 优先看cwd,禁猜路径
1. 搜索先行: 搜文件名严禁不用es(禁PS递归/禁dir遍历), 搜索一定优先使用web工具的google(严禁duckduckgo等), 优先看cwd,禁猜路径
2. 交叉验证: 禁信摘要, 数值进详情页核实
3. 编码安全: 禁PS cat/type用file_read; 改前必读; memory模块直接import(已在PATH,禁加虚假前缀)
4. 闭环: 物理模拟后确认; 3次失败请求干预
4. 闭环: 物理模拟后确认; 3次失败请求干预; Git完整闭环
5. 进程: 禁无条件杀python(杀自己), 精确PID, 禁os.kill判活
6. 窗口: GUI状态优先枚举标题
6. 窗口: GUI状态优先win32gui枚举标题
7. web JS: 输入用原生setter+事件链, 点击前检disabled, 注意引号转义; scan空/不全先稍等再scan, 禁首扫定论
8. SOP: 读SOP禁凭印象,有utils必用
8. SOP: 读SOP禁凭印象,有utils必用 | 复杂超长程任务/用户明确提及规划模式→读plan_sop
+5 -5
View File
@@ -2,8 +2,8 @@
Read L2 or ls ../memory/ for L3 when needed
L0(META-SOP): memory_management_sop
L2: currently empty
L3: memory_cleanup_sop(memory cleanup) | ui_detect.py | ocr_utils.py | subagent | web_setup_sop
| procmem_scanner | keychain | ljqCtrl_sop+.py | tmwebdriver_sop | autonomous_operation_sop | scheduled_task_sop | vision_sop | adb_ui.py | project_mode_sop
L3: memory_cleanup_sop(memory cleanup) | ui_detect.py | ocr_utils.py | subagent | web_setup_sop | plan_sop
| procmem_scanner | keychain | ljqCtrl_sop+.py | tmwebdriver_sop | autonomous_operation_sop | scheduled_task_sop | vision_sop | adb_ui.py
L4: L4_raw_sessions/ historical sessions
Browser special ops: tmwebdriver_sop(file upload/image search/PDF blob/physical coords/HttpOnly Cookie/autofill bypass/cross-origin iframe/CDP/cross-tab)
@@ -13,11 +13,11 @@ Scheduling: scheduled_task_sop | Autonomous: autonomous_operation_sop | watchdog
Mobile: adb_ui.py
[RULES]
1. Search first: no huge PS recursion/no dir traversal for file search, prefer Google for web search, check cwd first, no guessing paths
1. Search first: must use es for filename search (no PS recursion/no dir traversal), always prefer Google for web search (no duckduckgo etc), check cwd first, no guessing paths
2. Cross-verify: never trust summaries, verify numbers on detail pages
3. Encoding safety: use file_read not PS cat/type; read before modify; import memory modules directly (already in PATH, no fake prefixes)
4. Close the loop: confirm after physical simulation; request intervention after 3 failures
4. Close the loop: confirm after physical simulation; request intervention after 3 failures; complete Git workflow
5. Processes: never kill python unconditionally (kills self), use exact PID, no os.kill for liveness check
6. Windows: prefer win32gui title enumeration for GUI state
7. Web JS: use native setter + event chain for input, check disabled before click, mind quote escaping; if scan empty/incomplete wait then rescan, no conclusions from first scan
8. SOP: read SOPs not from memory, must use utils if available
8. SOP: read SOPs not from memory, must use utils if available | complex long-running/user mentions planning -> read plan_sop
Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

+1 -1
View File
@@ -4,6 +4,6 @@ L1 Insight是极简索引,L2/L3变更时同步L1,索引必须极简。写记
[CONSTITUTION]
1. 改自身源码先请示;./内可自主实验,允许装包和portable工具
2. 决策前查记忆,有SOP/utils必用;多次失败回看SOP;未查证不断言
3. 分步执行,控制粒度,限制失败半径
3. 分步执行,控制粒度,限制失败半径;3次失败请求干预
4. 密钥文件仅引用,不读取/移动
5. 写任何记忆前读META-SOP核验,memory下文件只能patch修改(除非新建)
+1 -1
View File
@@ -4,6 +4,6 @@ L1 Insight is a minimal index; sync L1 when L2/L3 changes; keep index minimal. R
[CONSTITUTION]
1. Ask before modifying own source code; free to experiment within ./; installing packages and portable tools allowed
2. Check memory before decisions; always use existing SOPs/utils; revisit SOPs on repeated failures; never assert without evidence
3. Execute step by step, control granularity, limit blast radius
3. Execute step by step, control granularity, limit blast radius; request intervention after 3 failures
4. Key/secret files: reference only, never read or move
5. Read META-SOP to verify before writing any memory; files under memory/ must be patched only (unless creating new)
+1 -1
View File
@@ -6,7 +6,7 @@ SuperGrok Local Proxy - CLI
python assets/supergrok_proxy.py login
python assets/supergrok_proxy.py serve --port 15433
python assets/supergrok_proxy.py models
python assets/supergrok_proxy.py test --model grok-4.5
python assets/supergrok_proxy.py test --model grok-4.3
GenericAgent/mykey 配置示例:
native_oai_config_supergrok_proxy = {
+1 -5
View File
@@ -1,10 +1,6 @@
# Role: 物理级全能执行者
你拥有文件读写、脚本执行、用户浏览器JS注入、系统级干预的物理操作权限。禁止推诿"无法操作"——不空想,用工具探测。
## 行动原则
调用工具前先推演:当前阶段、上步结果是否符合预期、下步策略,必须在回复文本头部用小于30词<summary>输出极简总结。
调用工具前先推演:当前阶段、上步结果是否符合预期、下步策略,必须在回复文本中用<summary>输出极简总结。
- 探测优先:失败时先充分获取信息(日志/状态/上下文),关键信息存入工作记忆,再决定重试或换方案。不可逆操作先询问用户。
- 失败升级:1次→读错误理解原因,2次→探测环境状态,3次→深度分析后换方案或问用户。禁止无新信息的重复操作。
- 行动也是认识:边推进边用现实反馈校正方向,内部解释与计划只作暂时假说。根据任务选择高级sop调用
- 自主闭环:能查明、试明或安全推断的不要问;仅为用户独有事实、价值取舍、必要授权或不可兼容的高影响分歧请求最小信息
- 完成发生在现实中:必须产出实际结果并按错误代价验证;计划、问题、调研、工具成功、承诺或未完成分析不能冒充交付。结束前检查重大残差,有则继续,无则停止,既不过早收工也不无限研究
- 受阻或预算耗尽时交付已验证的最大可用结果,区分已完成、未完成、未经验证与残余风险;最终消息自包含地重述关键结果、依据和边界
+1 -2
View File
@@ -2,7 +2,6 @@
You have full physical access: file I/O, script execution, browser JS injection, and system-level intervention. Never deflect with "can't do it" — don't speculate, use tools to probe.
Summarize and reply in user's language or follow user's prompt.
## Action Principles
Before each tool call, reason: current phase, whether the last result met expectations, and next strategy in reply text of each turn.
<summary> (<30 tokens) before reply text.
Before each tool call, reason: current phase, whether the last result met expectations, and next strategy and <summary> in reply text of each turn.
- Probe first: on failure, gather sufficient info (logs/status/context), store key findings in working memory, then decide to retry or pivot. Ask the user before irreversible operations.
- Failure escalation: 1st fail → read error and understand cause; 2nd → probe environment state; 3rd → deep analysis then switch approach or ask user. Never repeat an action without new information.
-7
View File
@@ -16,7 +16,6 @@ chrome.runtime.onInstalled.addListener(() => {
});
async function handleExtMessage(msg, sender) {
if (msg.cmd === 'status') return { ok: true, data: status };
if (msg.cmd === 'cookies') return await handleCookies(msg, sender);
if (msg.cmd === 'cdp') return await handleCDP(msg, sender);
if (msg.cmd === 'batch') return await handleBatch(msg, sender);
@@ -212,9 +211,7 @@ function buildCdpScript(code) {
// --- WebSocket Client for TMWebDriver ---
let ws = null;
let status = 'connected';
const WS_URL = 'ws://127.0.0.1:18765';
function setStatus(s){if(s===status)return;status=s;chrome.tabs.query({}).then(T=>T.forEach(t=>chrome.tabs.sendMessage(t.id,{type:'tmwd_status',data:s}).catch(()=>{})));}
function scheduleProbe() {
// Use chrome.alarms to survive MV3 service worker suspension
@@ -259,7 +256,6 @@ chrome.alarms.onAlarm.addListener(async (alarm) => {
console.log('[TMWD-WS] Server detected, connecting...');
connectWS();
} else {
setStatus('disconnected');
scheduleProbe(); // Server not up, keep probing
}
}
@@ -343,7 +339,6 @@ function connectWS() {
console.log('[TMWD-WS] Connecting to', WS_URL);
try {
ws = new WebSocket(WS_URL);
setStatus('connecting');
} catch (e) {
console.error('[TMWD-WS] Constructor error:', e);
ws = null;
@@ -352,7 +347,6 @@ function connectWS() {
}
ws.onopen = async () => {
console.log('[TMWD-WS] Connected!');
setStatus('connected');
scheduleKeepalive(); // Keep SW alive while connected
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
ws.send(JSON.stringify({
@@ -391,7 +385,6 @@ function connectWS() {
};
ws.onclose = () => {
console.log('[TMWD-WS] Disconnected');
setStatus('connecting');
ws = null;
scheduleProbe();
};
+34 -6
View File
@@ -7,13 +7,41 @@ document.querySelectorAll('meta[http-equiv="Content-Security-Policy"]').forEach(
(function(){
if(window.self!==window.top)return;
const d=document.createElement('div');
d.textContent='ljq_driver: 已连接';
d.style.cssText='position:fixed;bottom:8px;right:8px;background:#4CAF50;color:white;padding:4px 7px;border-radius:4px;font-size:11px;font-weight:bold;z-index:99999;box-shadow:0 2px 4px rgba(0,0,0,0.2);opacity:0.2;pointer-events:none;';
const S={connected:['已连接','#4CAF50'],connecting:['重连中','#FF9800'],disconnected:['断开','#F44336']};
const R=s=>{const[t,c]=S[s]||S.disconnected;d.textContent='ljq_driver: '+t;d.style.background=c};
chrome.runtime.onMessage.addListener(m=>{if(m?.type==='tmwd_status')R(m.data)});
chrome.runtime.sendMessage({cmd:'status'},r=>R(r?.ok?r.data:'connected'));
d.id='ljq-ind';
d.innerText='ljq_driver: 已连接';
d.style.cssText='position:fixed;bottom:8px;right:8px;background:#4CAF50;color:white;padding:4px 7px;border-radius:4px;font-size:11px;font-weight:bold;z-index:99999;cursor:pointer;box-shadow:0 2px 4px rgba(0,0,0,0.2);opacity:0.5;';
d.addEventListener('click',()=>alert('会话活跃\nURL: '+location.href));
(document.body||document.documentElement).appendChild(d);
})();
new MutationObserver(muts => {
for (const m of muts) for (const n of m.addedNodes) {
if (n.id === TID || (n.querySelector && n.querySelector('#' + TID))) {
const el = n.id === TID ? n : n.querySelector('#' + TID);
handle(el);
}
}
}).observe(document.documentElement, { childList: true, subtree: true });
async function handle(el) {
try {
const req = el.textContent.trim() ? JSON.parse(el.textContent) : { cmd: 'cookies' };
const cmd = req.cmd || 'cookies';
let resp;
if (cmd === 'cookies') {
resp = await chrome.runtime.sendMessage({ cmd: 'cookies', url: req.url || location.href });
} else if (cmd === 'cdp') {
resp = await chrome.runtime.sendMessage({ cmd: 'cdp', method: req.method, params: req.params || {}, tabId: req.tabId });
} else if (cmd === 'batch') {
resp = await chrome.runtime.sendMessage({ cmd: 'batch', commands: req.commands, tabId: req.tabId });
} else if (cmd === 'tabs') {
resp = await chrome.runtime.sendMessage({ cmd: 'tabs', method: req.method, tabId: req.tabId, url: req.url, active: req.active, index: req.index, windowId: req.windowId, openerTabId: req.openerTabId });
} else {
resp = { ok: false, error: 'unknown cmd: ' + cmd };
}
el.textContent = JSON.stringify(resp);
} catch (e) {
el.textContent = JSON.stringify({ ok: false, error: e.message });
}
}
})();
+1 -1
View File
@@ -28,7 +28,7 @@
},
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"js": ["config.js", "content.js"],
"run_at": "document_idle",
"all_frames": true
}
+9 -8
View File
@@ -3,7 +3,7 @@
"name": "code_run",
"description": "Code executor. Prefer python. Multi-call OK, use script param. Reply code block is executed if no script arg; prefer for single call to avoid escaping. No hardcoding bulk data",
"parameters": {"type": "object", "properties": {
"script": {"type": "string", "description": "script"},
"script": {"type": "string", "description": "[Mutually exclusive] NEVER use this param when use reply code block."},
"type": {"type": "string", "enum": ["python", "powershell"], "description": "Code type", "default": "python"},
"timeout": {"type": "integer", "description": "in seconds", "default": 60},
"cwd": {"type": "string", "description": "Working directory, defaults to cwd"},
@@ -53,20 +53,21 @@
}},
{"type": "function", "function": {
"name": "update_working_checkpoint",
"description": "Short-term working notepad (auto-injected each turn). Call early/mid task only when there is non-obvious state worth keeping. When: (1) after reading SOP — store needs & constraints (skip tasks <5 steps); (2) before subtask switch / context flush; (3) after repeated failures — re-read SOP and store new findings; (4) on new task — replace notepad, drop old progress, keep still-valid constraints.\n\nDon't call: tasks <5 steps, greetings, or when the task is done (use start_long_term_update instead).",
"description": "Short-term working notepad, auto-injected each turn to prevent info loss in long tasks. Call during early/mid stages, not at end. When: (1) after reading SOP, store user needs & key constraints (skip for simple 1-2 step tasks); (2) before subtask switch or context flush; (3) after repeated failures, re-read SOP and must store new findings; (4) on new task, update content, clear old progress but keep valid constraints.\n\nDon't call: simple tasks (1-2 steps), task completed (use long-term memory tool)",
"parameters": {"type": "object", "properties": {
"key_info": {"type": "string", "description": "Replaces current notepad (<200 tokens). Incremental update: review existing, keep valid, add/remove/modify. Store: pitfalls, user requirements, key params/findings, file paths, progress, next steps. Don't store: ephemeral info, obvious context, old task info when user switched tasks. Prefer over-updating over losing key info"}}}
"key_info": {"type": "string", "description": "Replaces current notepad (<200 tokens). Incremental update: review existing, keep valid, add/remove/modify. Store: pitfalls, user requirements, key params/findings, file paths, progress, next steps. Don't store: ephemeral info, obvious context, old task info when user switched tasks. Prefer over-updating over losing key info"},
"related_sop": {"type": "string", "description": "Related SOP names, tips for further re-read"}}}
}},
{"type": "function", "function": {
"name": "ask_user",
"description": "Interrupt task to ask user when needing decisions, extra info, or facing unresolvable blockers. Multiple questions should in one call",
"description": "Interrupt task to ask user when needing decisions, extra info, or facing unresolvable blockers",
"parameters": {"type": "object", "properties": {
"question": {"type": "string", "description": "For multiple questions, one per line (contain candidates)"},
"candidates": {"type": "array", "items": {"type": "string"}, "description": "Optional quick-select choices for a single question only. Leave empty for multi-questions"}}}
"question": {"type": "string", "description": "Question for the user"},
"candidates": {"type": "array", "items": {"type": "string"}, "description": "Optional quick-select choices for the user"}}}
}},
{"type": "function", "function": {
"name": "start_long_term_update",
"description": "After a long task completes, distill long-term memory. Call when discovering info worth remembering (env facts/user prefs/lessons learned). Skip if memory already updated or in autonomous flow. Must call when a task that took 15+ turns is completed.",
"description": "Start distilling long-term memory. Call when discovering info worth remembering (env facts/user prefs/lessons learned). Skip if memory already updated or in autonomous flow. Must call when a task that took 15+ turns is completed",
"parameters": {"type": "object", "properties": {}}}
}
]
]
+3 -3
View File
@@ -60,10 +60,10 @@
}},
{"type": "function", "function": {
"name": "ask_user",
"description": "当需要用户决策、提供额外信息或遇到无法自动解决的阻碍时,调用此工具中断任务并提问。多题时应一次调用",
"description": "当需要用户决策、提供额外信息或遇到无法自动解决的阻碍时,调用此工具中断任务并提问",
"parameters": {"type": "object", "properties": {
"question": {"type": "string", "description": "多题时每题*独占*一行(带选项)"},
"candidates": {"type": "array", "items": {"type": "string"}, "description": "仅单题时可用。多题时留空"}}}
"question": {"type": "string", "description": "向用户提出的明确问题"},
"candidates": {"type": "array", "items": {"type": "string"}, "description": "提供给用户的可选快捷选项列表"}}}
}},
{"type": "function", "function": {
"name": "start_long_term_update",
-6
View File
@@ -160,12 +160,6 @@ type ga
If it resolves to something unexpected, do not rely on the shortcut. Run GA from the install directory with `python launch.pyw` or `python frontends/tuiapp_v2.py`.
#### Windows Defender blocks local loopback
The desktop app talks to its local bridge over `127.0.0.1` / `localhost`. On some Windows machines, Windows Defender Firewall may block this loopback connection, making the desktop app look like it cannot start or cannot connect to the service.
If this happens, allow `GenericAgent.exe` and the bundled `python.exe` through Windows Defender Firewall, then restart the desktop app.
#### Windows TUI rendering issues
TUI rendering on Windows depends on terminal, font, and `textual` version.
-6
View File
@@ -160,12 +160,6 @@ type ga
如果解析到意料之外的位置,就不要依赖这个快捷命令。请进入安装目录运行 `python launch.pyw` 或 `python frontends/tuiapp_v2.py`。
#### Windows Defender 拦截本机回环网络
桌面端会通过 `127.0.0.1` / `localhost` 连接本机 bridge 服务。部分 Windows 机器上,Windows Defender 防火墙可能会拦截这个本机回环连接,导致桌面端看起来无法启动,或一直连不上服务。
如果遇到这种情况,请在 Windows Defender 防火墙中允许 `GenericAgent.exe` 和随包的 `python.exe` 通过,然后重启桌面端。
#### Windows 上 TUI 显示异常
TUI 在 Windows 上依赖终端、字体和 `textual` 版本。
+13 -146
View File
@@ -61,9 +61,9 @@
overflow: hidden;
}
.left-col { min-height: 0; min-width: 0; display: flex; flex-direction: column; gap: 18px; }
.left-col { min-height: 0; display: flex; flex-direction: column; gap: 18px; }
.left-col > .pane.left { flex: 1; min-height: 0; }
.left, .right { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.left, .right { display: flex; flex-direction: column; min-height: 0; }
.right { background: transparent; border: 0; box-shadow: none; backdrop-filter: none; gap: 18px; }
.head {
@@ -76,7 +76,6 @@
}
.title { font-size: 17px; letter-spacing: .2px; font-weight: 700; }
.hint { color: var(--muted); font-size: 12px; }
#llm { max-width: 42vw; margin-left: 10px; padding: 4px 6px; border: 1px solid #dfe5ee; border-radius: 6px; background: #fff; font: 600 12px inherit; }
.cards {
flex: 1;
@@ -200,15 +199,10 @@
.btn-no { background: linear-gradient(135deg, #f87171, #ef4444); box-shadow: 0 8px 20px rgba(239,68,68,.22); }
/* ---------- 聊天框 ---------- */
.chat { flex: 1; min-height: 0; min-width: 0; display: flex; flex-direction: column; }
.messages { flex: 1; min-width: 0; overflow: auto; padding: 20px; display: flex; flex-direction: column; gap: 13px; }
.chat { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.messages { flex: 1; overflow: auto; padding: 20px; display: flex; flex-direction: column; gap: 13px; }
.msg { max-width: 80%; padding: 13px 15px; border-radius: 18px; line-height: 1.45; font-size: 14px;
overflow-wrap: anywhere; box-shadow: 0 8px 22px rgba(30,45,80,.055); }
.msg :is(p, ul, ol, pre, table, img) { max-width: 100%; }
.msg pre { white-space: pre-wrap; overflow-wrap: anywhere; }
.msg code { overflow-wrap: anywhere; }
.msg table { display: block; overflow-x: auto; }
.msg img { height: auto; }
word-wrap: break-word; box-shadow: 0 8px 22px rgba(30,45,80,.055); }
.msg p { margin: 0 0 .3em 0; } .msg p:last-child { margin-bottom: 0; }
.msg.me { align-self: flex-end; color: white;
background: linear-gradient(135deg, #3b82f6, #7c3aed); border: 1px solid rgba(59,130,246,.20);
@@ -223,7 +217,6 @@
.composer { padding: 14px; border-top: 1px solid rgba(31,42,68,.075); display: flex; gap: 12px;
align-items: end; background: rgba(255,255,255,.40); }
.composer.dragging { background: rgba(59,130,246,.13); outline: 2px dashed #3b82f6; outline-offset: -6px; }
textarea#input { flex: 1; height: 52px; max-height: 140px; resize: none; outline: none; color: var(--text);
background: rgba(255,255,255,.92); border: 1px solid rgba(31,42,68,.12); border-radius: 18px;
padding: 14px 15px; font: inherit; line-height: 1.35; }
@@ -231,19 +224,6 @@
button { height: 52px; min-width: 92px; border: 0; border-radius: 18px; color: white; font-weight: 750;
cursor: pointer; background: linear-gradient(135deg, #3b82f6, #10b981);
box-shadow: 0 10px 26px rgba(59,130,246,.22); }
.file-open, .attach { min-width: 0; padding: 0 10px; box-shadow: none; }
.file-open { height: 28px; border-radius: 9px; }
.attach { width: 44px; height: 52px; font-size: 18px; border-radius: 18px; }
.file-link { color: inherit; text-decoration: underline; cursor: pointer; }
#fileDialog { width: min(1100px, 96vw); height: min(800px, 92vh); padding: 0; border: 0; border-radius: 18px; box-shadow: 0 24px 80px #0004; }
#fileDialog::backdrop { background: #0f172a66; backdrop-filter: blur(3px); }
.file-bar { display: flex; align-items: center; gap: 8px; padding: 12px; border-bottom: 1px solid var(--border); }
.file-bar code { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-bar button { min-width: 0; height: 34px; padding: 0 12px; border-radius: 10px; box-shadow: none; }
#fileBody { height: calc(100% - 59px); overflow: auto; padding: 12px; }
.file-row { display: flex; gap: 10px; padding: 9px 11px; border-radius: 9px; cursor: pointer; }
.file-row:hover { background: #eef2ff; } .file-row span:first-child { flex: 1; } .file-size { color: var(--muted); }
.file-preview { line-height: 1.55; } .file-preview pre { white-space: pre-wrap; overflow-wrap: anywhere; }
/* ---------- conductor 输出(横跨底部的独立卡片,默认收起) ---------- */
.console {
@@ -332,37 +312,25 @@
<!-- 聊天 -->
<section class="pane chat">
<div class="head">
<div class="title">Conversation <select id="llm" aria-label="Conductor model" disabled></select></div>
<div class="hint"><button class="file-open" id="fileOpen">📁 文件</button> · <span id="wsStatus">connecting...</span></div>
<div class="title">Conversation</div>
<div class="hint" id="wsStatus">connecting...</div>
</div>
<div class="messages" id="messages"></div>
<div class="composer" id="composer">
<textarea id="input" placeholder="告诉 Conductor 你想做什么……(可拖入文件)"></textarea>
<button class="attach" id="fileUpload" title="上传文件">📎</button>
<button id="send">发送</button><input id="fileInput" type="file" multiple hidden>
<div class="composer">
<textarea id="input" placeholder="告诉 Conductor 你想做什么……"></textarea>
<button id="send">发送</button>
</div>
</section>
</section>
</div><!-- /app-grid -->
</main>
<dialog id="fileDialog">
<div class="file-bar"><button id="fileBack">↑</button><code id="filePath"></code><button id="fileClose">关闭</button></div>
<div id="fileBody"></div>
</dialog>
<dialog id="downloadDialog" style="width:min(520px,90vw);border:0;border-radius:16px;padding:22px;box-shadow:0 24px 80px #0004">
<strong>下载文件</strong>
<code id="downloadPath" style="display:block;margin:16px 0;overflow-wrap:anywhere;color:var(--muted)"></code>
<div style="display:flex;justify-content:flex-end;gap:10px"><button id="downloadCancel">取消</button><button id="downloadConfirm">下载</button></div>
</dialog>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
const cardsEl = document.getElementById('cards');
const messagesEl = document.getElementById('messages');
const inputEl = document.getElementById('input');
const sendBtn = document.getElementById('send');
const llmEl = document.getElementById('llm');
const wsStatus = document.getElementById('wsStatus');
const apprEl = document.getElementById('approvals');
const apprBody = document.getElementById('approvalsBody');
@@ -371,48 +339,10 @@
const consoleBar = document.getElementById('consoleBar');
const consoleBody= document.getElementById('consoleBody');
const logCount = document.getElementById('logCount');
const fileDialog = document.getElementById('fileDialog');
const fileBody = document.getElementById('fileBody');
const filePathEl = document.getElementById('filePath');
const fileInput = document.getElementById('fileInput');
const composer = document.getElementById('composer');
const downloadDialog = document.getElementById('downloadDialog');
const downloadPath = document.getElementById('downloadPath');
let ws, fileDir='', fileParent='';
let ws;
function esc(s){ return String(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
function renderMd(s){ try { return marked.parse(String(s||'').replace(/</g,'&lt;'), { breaks: true, gfm: true }); } catch(e){ return esc(s); } }
function fileUrl(path){ return '/file?path='+encodeURIComponent(path); }
function humanSize(n){ return n==null?'':n<1024?n+' B':n<1048576?(n/1024).toFixed(1)+' KB':(n/1048576).toFixed(1)+' MB'; }
async function browseFiles(path=''){
const data=await fetch(path?'/files?path='+encodeURIComponent(path):'/files').then(r=>{if(!r.ok) throw Error(r.statusText); return r.json()});
fileDir=data.path; fileParent=data.parent; filePathEl.textContent=fileDir;
fileBody.innerHTML=data.items.map(x=>`<div class="file-row" data-path="${esc(x.path)}" data-dir="${x.dir?'1':''}"><span>${x.dir?'📁':'📄'} ${esc(x.name)}</span><span class="file-size">${humanSize(x.size)}</span></div>`).join('')||'<div class="empty">空目录</div>';
}
async function openFile(path){
if(!/\.(md|markdown|txt|py|js|ts|json|ya?ml|css|html?|xml|csv|log|ini|cfg|sh|ps1|bat|c|cc|cpp|h|hpp|java|go|rs|sql)$/i.test(path)){
downloadPath.textContent=path; downloadDialog.showModal();
return;
}
const text=await fetch(fileUrl(path)).then(r=>{if(!r.ok) throw Error(r.statusText); return r.text()});
filePathEl.textContent=path;
fileBody.innerHTML=/\.(md|markdown)$/i.test(path)?`<div class="file-preview">${renderMd(text)}</div>`:'';
if(!/\.(md|markdown)$/i.test(path)){ const pre=document.createElement('pre'); pre.textContent=text; fileBody.replaceChildren(pre); }
}
document.getElementById('downloadCancel').onclick=()=>downloadDialog.close();
document.getElementById('downloadConfirm').onclick=()=>{
const a=document.createElement('a'); a.href=fileUrl(downloadPath.textContent); a.download=downloadPath.textContent.split(/[\\/]/).pop();
document.body.appendChild(a); a.click(); a.remove(); downloadDialog.close();
};
async function uploadFiles(files){
const paths=[];
for(const file of files){
const r=await fetch('/file?name='+encodeURIComponent(file.name),{method:'POST',body:file});
if(!r.ok) throw Error(await r.text());
paths.push('`'+(await r.json()).path+'`');
}
inputEl.value+=(inputEl.value?'\n':'')+paths.join('\n'); checkLimit(); inputEl.focus();
}
function renderMd(s){ try { return marked.parse(s||'', { breaks: true, gfm: true }); } catch(e){ return esc(s); } }
/* ---------- subagent 卡片 ---------- */
function renderCards(items){
@@ -482,28 +412,11 @@
div.className = `msg ${cls}` + (role==='user' && !item.read ? ' unread' : '');
if(item.id) div.dataset.id = item.id;
div.innerHTML = renderMd(item.msg || '');
(item.files||[]).forEach(f=>{ const code=[...div.querySelectorAll('code')].find(x=>x.textContent===(f.ref||f.path)); if(code){ const a=document.createElement('a'); a.className='file-link'; a.href=fileUrl(f.path); a.dataset.path=f.path; code.replaceWith(a); a.append(code); } });
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function markAllRead(){ messagesEl.querySelectorAll('.msg.me.unread').forEach(el=>el.classList.remove('unread')); }
document.getElementById('fileOpen').onclick=()=>{ fileDialog.showModal(); browseFiles().catch(e=>fileBody.textContent=e); };
document.getElementById('fileClose').onclick=()=>fileDialog.close();
document.getElementById('fileBack').onclick=()=>browseFiles(filePathEl.textContent===fileDir?fileParent:fileDir);
document.getElementById('fileUpload').onclick=()=>fileInput.click();
fileBody.addEventListener('click', e=>{ const row=e.target.closest('.file-row'); if(row) (row.dataset.dir?browseFiles:openFile)(row.dataset.path); });
messagesEl.addEventListener('click', async e=>{
const a=e.target.closest('.file-link'); if(!a) return;
e.preventDefault(); fileDialog.showModal();
const path=a.dataset.path, parent=path.replace(/[\\\/][^\\\/]*$/, '');
try { await browseFiles(parent); await openFile(path); } catch(err) { fileBody.textContent=err; }
});
fileInput.onchange=async()=>{ try { await uploadFiles(fileInput.files); } catch(e) { alert('上传失败:'+e); } fileInput.value=''; };
['dragenter','dragover'].forEach(type=>composer.addEventListener(type,e=>{ e.preventDefault(); composer.classList.add('dragging'); }));
['dragleave','drop'].forEach(type=>composer.addEventListener(type,e=>{ e.preventDefault(); composer.classList.remove('dragging'); }));
composer.addEventListener('drop',e=>{ if(e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files).catch(err=>alert('上传失败:'+err)); });
/* ---------- conductor 输出 ---------- */
function fmtTs(ms){ try{ return new Date(ms).toLocaleTimeString('zh-CN',{hour12:false}); }catch(e){ return ''; } }
function logItemHtml(r){
@@ -539,13 +452,7 @@
renderCards(data.subagents||[]);
messagesEl.innerHTML=''; (data.chat||[]).forEach(addMsg);
renderLog(data.log||[]);
llmEl.innerHTML=(data.llms||[]).map(x=>`<option value="${x[0]}">${esc(x[1])}</option>`).join(''); llmEl.value=data.llm; llmEl.disabled=!llmEl.options.length;
} else if(data.type==='model_selected'){ llmEl.value=String(data.llm); }
else if(data.type==='model_error'){
if(data.llm!==undefined) llmEl.value=String(data.llm);
showLimitToast(data.error||'Failed to select model');
}
else if(data.type==='subagents'){ renderCards(data.items||[]); }
} else if(data.type==='subagents'){ renderCards(data.items||[]); }
else if(data.type==='chat'){ addMsg(data.item); }
else if(data.type==='chat_read'){ markAllRead(); }
else if(data.type==='log'){ appendLog(data.item); }
@@ -558,48 +465,8 @@
ws.send(JSON.stringify({msg})); inputEl.value=''; inputEl.focus();
}
sendBtn.addEventListener('click', send);
llmEl.addEventListener('change', ()=>{ if(ws?.readyState===1) ws.send(JSON.stringify({llm:+llmEl.value})); });
inputEl.addEventListener('keydown', e=>{ if(e.key==='Enter' && !e.shiftKey && !e.isComposing && e.keyCode!==229){ e.preventDefault(); send(); } });
// Input length limit (10000 chars)
const MAX_LEN = 10000;
let limitToast = null;
function showLimitToast(msg) {
if (limitToast) return; // already showing, don't spam
limitToast = document.createElement('div');
limitToast.textContent = msg;
limitToast.style.cssText = 'position:fixed;top:24px;left:50%;transform:translateX(-50%);background:#dc2626;color:#fff;padding:10px 24px;border-radius:8px;font-size:14px;z-index:9999;box-shadow:0 4px 12px rgba(0,0,0,.15);transition:opacity .3s';
document.body.appendChild(limitToast);
setTimeout(() => {
if (limitToast) { limitToast.style.opacity = '0'; }
setTimeout(() => { if (limitToast) { limitToast.remove(); limitToast = null; } }, 300);
}, 3000);
}
function checkLimit() {
if (inputEl.value.length > MAX_LEN) {
inputEl.value = inputEl.value.slice(0, MAX_LEN);
showLimitToast('已达字数上限(' + MAX_LEN + ')');
}
}
inputEl.addEventListener('paste', e => {
e.preventDefault();
const text = (e.clipboardData || window.clipboardData).getData('text') || '';
const remaining = MAX_LEN - inputEl.value.length + (inputEl.selectionEnd - inputEl.selectionStart);
const insert = text.slice(0, Math.max(0, remaining));
const start = inputEl.selectionStart;
const end = inputEl.selectionEnd;
inputEl.value = inputEl.value.slice(0, start) + insert + inputEl.value.slice(end);
inputEl.setSelectionRange(start + insert.length, start + insert.length);
if (text.length > remaining) {
showLimitToast('已达字数上限(' + MAX_LEN + ')');
}
});
inputEl.addEventListener('input', checkLimit);
/* 接通后端: hello 推送 cards/chat/log 快照, approval 由 /approval 推送 */
connect();
</script>
+33 -425
View File
@@ -1,266 +1,33 @@
import os, sys, re, time, json, uuid, queue, asyncio, threading, argparse, base64, secrets
import os, sys, re, time, json, uuid, queue, asyncio, threading
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, Any, Optional, List
from contextlib import asynccontextmanager, suppress
from contextlib import asynccontextmanager
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, HTTPException
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse, PlainTextResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
FRONTENDS_DIR = os.path.dirname(os.path.abspath(__file__))
if FRONTENDS_DIR not in sys.path: sys.path.insert(0, FRONTENDS_DIR)
from desktop_settings import DesktopSettingsError, update_settings
def _resolve_ga_root() -> str:
"""Use the external core selected by the package-owned desktop bridge when valid."""
value = (os.environ.get("GA_ROOT", "") or "").strip()
if value:
root = os.path.abspath(os.path.expanduser(value))
if os.path.exists(os.path.join(root, "agentmain.py")):
return root
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ROOT = _resolve_ga_root()
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT not in sys.path: sys.path.insert(0, ROOT)
from agentmain import GenericAgent
HOST = "127.0.0.1"
DEFAULT_PORT = 8900
E2E_CONDUCTOR_PORT_ENV = "GA_DESKTOP_E2E_CONDUCTOR_PORT"
E2E_REPORT_DIR_ENV = "GA_DESKTOP_E2E_REPORT_DIR"
def _parse_conductor_port(value: Any) -> int:
raw = str(value).strip()
if not raw.isascii() or not raw.isdigit():
raise argparse.ArgumentTypeError("conductor port must be an integer between 1 and 65535")
port = int(raw)
if not 1 <= port <= 65535:
raise argparse.ArgumentTypeError("conductor port must be an integer between 1 and 65535")
return port
def _default_conductor_port() -> int:
"""Keep production on :8900; only the isolated package journey may override it.
The renderer and production CSP intentionally target :8900. Treating this as a
general runtime setting would make the backend claim configurability the renderer
cannot honor, so the override is gated by the existing package-evidence marker.
"""
if not os.environ.get(E2E_REPORT_DIR_ENV):
return DEFAULT_PORT
raw = os.environ.get(E2E_CONDUCTOR_PORT_ENV)
return DEFAULT_PORT if raw is None else _parse_conductor_port(raw)
PORT = _default_conductor_port()
PORT = 8900
HTML_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "conductor.html")
parser = argparse.ArgumentParser()
parser.add_argument("--host", default=HOST)
parser.add_argument("--port", type=_parse_conductor_port, default=PORT)
parser.add_argument("--key")
parser.add_argument("--no-browser", action="store_true")
args = parser.parse_args()
if args.host not in ("127.0.0.1", "localhost", "::1") and not args.key:
parser.error("--key is required for non-loopback listening")
HOST = args.host
PORT = args.port
SETTINGS_PATH = Path.home() / ".ga_desktop_settings.json"
def _settings_doc() -> dict:
try:
doc = json.loads(SETTINGS_PATH.read_text(encoding="utf-8"))
return doc if isinstance(doc, dict) else {}
except Exception:
return {}
def _persist_conductor_llm_no(llm_no: int) -> None:
def mutate(document: dict) -> None:
section = document.get("conductor")
if not isinstance(section, dict):
section = {}
document["conductor"] = section
section["llmNo"] = llm_no
update_settings(SETTINGS_PATH, mutate)
def _conductor_llm_no() -> Optional[int]:
"""Read the model index bound to the conductor session.
Falls back to the legacy desktop default ui.llmNo for existing installs."""
doc = _settings_doc()
for section in (doc.get("conductor"), doc.get("ui")):
if isinstance(section, dict) and section.get("llmNo") is not None:
try:
return int(section.get("llmNo"))
except (TypeError, ValueError):
pass
return None
def _client_usable(agent: "GenericAgent") -> bool:
return hasattr(getattr(agent, "llmclient", None), "backend")
def _parse_model_no(value: Any) -> Optional[int]:
if value is None or isinstance(value, bool):
return None
try:
return int(value)
except (TypeError, ValueError):
return None
def _usable_model(agent: "GenericAgent", no: Optional[int]) -> bool:
clients = getattr(agent, "llmclients", []) or []
return no is not None and 0 <= no < len(clients) and hasattr(clients[no], "backend")
def _activate_model(agent: "GenericAgent", no: int) -> None:
if not _usable_model(agent, no):
raise ValueError(f"llm index out of range or unavailable: {no}")
agent.next_llm(no)
def _runtime_model_state(agent: "GenericAgent", configured: Optional[int], reason: Optional[str]) -> dict:
effective = getattr(agent, "llm_no", None) if _client_usable(agent) else None
current = None
if _client_usable(agent):
with suppress(Exception):
current = str(agent.llmclient.backend.name)
return {"configured": configured, "effective": effective,
"fallbackReason": reason, "current": current}
def _apply_desktop_model(agent: "GenericAgent") -> dict:
"""Make the conductor's session reflect the current desktop config before a task:
switch to its bound model if one is set, otherwise still refresh sessions from
mykey so live key/model edits (e.g. importing keys) take effect without a restart.
next_llm() already reloads internally; the no-bound-model branch must reload too,
or a conductor started on an empty/stale mykey would never pick up imported keys."""
doc = _settings_doc()
conductor_cfg = doc.get("conductor") if isinstance(doc.get("conductor"), dict) else {}
ui_cfg = doc.get("ui") if isinstance(doc.get("ui"), dict) else {}
raw_configured = conductor_cfg.get("llmNo")
configured = _parse_model_no(raw_configured)
ui_default = _parse_model_no(ui_cfg.get("llmNo"))
try:
agent.load_llm_sessions() # mtime-guarded; rebuilds only when mykey changed
except Exception as e:
print(f"[conductor] failed to refresh model sessions: {e}", file=sys.stderr)
clients = getattr(agent, "llmclients", []) or []
configured_failed = False
if _usable_model(agent, configured):
try:
_activate_model(agent, configured)
return _runtime_model_state(agent, configured, None)
except Exception as e:
configured_failed = True
print(f"[conductor] configured model #{configured} is unavailable: {e}", file=sys.stderr)
if _usable_model(agent, ui_default):
if raw_configured is None:
reason = "ui_default"
elif configured_failed:
reason = "configured_unavailable"
elif configured is not None:
reason = "configured_unavailable" if 0 <= configured < len(clients) else "invalid_configured"
else:
reason = "invalid_configured"
try:
_activate_model(agent, ui_default)
return _runtime_model_state(agent, configured, reason)
except Exception as e:
print(f"[conductor] UI default model #{ui_default} is unavailable: {e}", file=sys.stderr)
for i, client in enumerate(clients):
if hasattr(client, "backend"):
try:
_activate_model(agent, i)
return _runtime_model_state(agent, configured, "first_available")
except Exception:
continue
print("[conductor] no usable model is available", file=sys.stderr)
return {"configured": configured, "effective": None,
"fallbackReason": "no_models", "current": None}
def _selected_conductor_llm_no(agent: "GenericAgent") -> int:
configured = _conductor_llm_no()
return configured if _usable_model(agent, configured) else agent.llm_no
def _set_conductor_llm_no(agent: "GenericAgent", value: Any) -> int:
"""Persist the standalone UI binding without mutating an in-flight client.
The Conductor loop applies this binding at its existing idle task boundary, so
a selection made immediately before sending a message controls that next turn.
"""
no = _parse_model_no(value)
if no is None or not _usable_model(agent, no):
raise ValueError(f"llm index out of range or unavailable: {value}")
_persist_conductor_llm_no(no)
return no
def _select_llm(agent: "GenericAgent", llm: Any) -> bool:
if llm is None or str(llm).strip() == "": return False
q = str(llm).strip()
if isinstance(llm, int) or q.isdigit():
agent.load_llm_sessions()
_activate_model(agent, int(q))
return True
q = q.lower(); agent.load_llm_sessions()
for i, c in enumerate(agent.llmclients):
if not hasattr(c, "backend"):
continue
vals = []
for fn in (lambda: agent.get_llm_name(c), lambda: agent.get_llm_name(c, model=True), lambda: c.backend.name, lambda: c.backend.model):
try: vals.append(str(fn()).lower())
except Exception: pass
if any(q in v for v in vals): _activate_model(agent, i); return True
raise ValueError(f"llm not found: {llm}")
@asynccontextmanager
async def lifespan(app: FastAPI):
# 服务启动(事件循环已就绪):捕获 loop 供工作线程跨线程推 WS 广播,并起主agent
global main_loop
main_loop = asyncio.get_running_loop()
import cost_tracker; cost_tracker.install()
conductor.start()
threading.Thread(target=im_poll_loop, name="im-poller", daemon=True).start()
yield
app = FastAPI(title="Conductor", lifespan=lifespan)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
class RemoteAuth:
def __init__(self, app): self.app = app
async def __call__(self, scope, receive, send):
remote = (scope.get("client") or ("",))[0]
got = dict(scope.get("headers", [])).get(b"authorization", b"")
want = b"Basic " + base64.b64encode(f"conductor:{args.key}".encode())
if args.key and remote not in ("127.0.0.1", "::1") and not secrets.compare_digest(got, want):
if scope["type"] == "websocket": return await send({"type": "websocket.close", "code": 1008})
return await PlainTextResponse("Unauthorized", 401, {"WWW-Authenticate": 'Basic realm="Conductor"'})(scope, receive, send)
await self.app(scope, receive, send)
app.add_middleware(RemoteAuth)
class ChatIn(BaseModel):
msg: str
@@ -268,7 +35,6 @@ class ChatIn(BaseModel):
class StartSubagentIn(BaseModel):
prompt: str
llm: Any = None
class ApprovalIn(BaseModel):
prompt: str
@@ -277,7 +43,6 @@ class ApprovalIn(BaseModel):
class SubagentActionIn(BaseModel):
action: str = "intervene" # intervene | abort | kill
msg: str = ""
llm: Any = None
@dataclass
class SubAgentState:
@@ -349,26 +114,8 @@ async def broadcast(payload: dict):
def push_cards(): schedule_broadcast({"type": "subagents", "items": pool.snapshot()})
FILE_ROOT = os.path.dirname(ROOT)
FILE_HOME = os.path.join(ROOT, "temp")
UPLOAD_DIR = os.path.join(FILE_HOME, "uploads")
def file_path(path: str):
path = os.path.abspath(os.path.expanduser(path))
if os.path.commonpath((FILE_ROOT, path)) != FILE_ROOT: raise ValueError("path is above file browser root")
return path
def add_chat(msg: str, role: str = "conductor", files: list = None, images: list = None):
files = list(files or [])
known = {x.get("path") for x in files}
for ref in re.findall(r"`([^`\n]+)`", msg):
for candidate in ([ref] if os.path.isabs(ref) else [os.path.join(FILE_HOME, ref), ref]):
try: path = file_path(candidate)
except ValueError: continue
if os.path.isfile(path):
if path not in known: files.append({"ref": ref, "path": path, "name": os.path.basename(path)})
break
item = {"id": short_id(), "role": role, "msg": msg, "ts": now_ms(), "read": role != "user", "files": files, "images": images or []}
def add_chat(msg: str, role: str = "conductor"):
item = {"id": short_id(), "role": role, "msg": msg, "ts": now_ms(), "read": role != "user"}
chat_messages.append(item)
if len(chat_messages) > 200: del chat_messages[:-200]
schedule_broadcast({"type": "chat", "item": item})
@@ -441,13 +188,12 @@ class SubagentPool:
s.agent.task_queue.put("EXIT")
with self.lock: self.subagents.pop(sid, None)
if to_abort: push_cards()
def start_subagent(self, prompt: str, llm: Any = None) -> dict:
def start_subagent(self, prompt: str) -> dict:
sid = short_id()
agent = GenericAgent()
agent.inc_out = True
agent.verbose = False
agent.no_print = True
if not _select_llm(agent, llm): _apply_desktop_model(agent)
th = start_agent_runner(agent, f"subagent-{sid}")
state = SubAgentState(id=sid, agent=agent, prompt=prompt, status="running", thread=th)
with self.lock: self.subagents[sid] = state
@@ -459,11 +205,10 @@ class SubagentPool:
threading.Thread(target=monitor_display_queue, args=(sid, dq, True), name=f"monitor-{sid}", daemon=True).start()
push_cards()
return {"id": sid, "status": "running"}
def input_subagent(self, sid: str, msg: str, llm: Any = None) -> dict:
def input_subagent(self, sid: str, msg: str) -> dict:
with self.lock: s = self.subagents.get(sid)
if not s: return {"error": "subagent not found", "id": sid}
if s.status == "running": return {"error": "subagent is still running, cannot input/reply. Start a new subagent instead.", "id": sid}
_select_llm(s.agent, llm)
s.prompt = msg
s.reply = ""
s.status = "running"
@@ -484,13 +229,10 @@ READMES = {
Conductor API\tBase: http://{HOST}:{PORT}
POST /chat\tbody: {{"msg": "..."}}\t给用户发消息
POST /subagent\tbody: {{"prompt": "..."}}\t启动新subagent,返回 {{"id": "xxx"}};指定模型加参数llm(数字/名称)
POST /subagent\tbody: {{"prompt": "..."}}\t启动新subagent,返回 {{"id": "xxx"}}
POST /approval\tbody: {{"prompt": "...", "source": "..."}}\t推一条待批任务到前端(后端不存),用户同意则直接派发为subagent
GET /files?path=...\t列举目录(默认temp,最高到GA上一级)
GET /file?path=...\t下载文件
POST /file?name=...\t上传到temp/uploads
POST /subagent/{{id}}\tbody: {{"action": "keyinfo", "msg": "..."}}\t注入key_info(agent下轮可见)
POST /subagent/{{id}}\tbody: {{"action": "input", "msg": "..."}}\t开新一轮任务;指定模型加参数llm(数字/名称)
POST /subagent/{{id}}\tbody: {{"action": "input", "msg": "..."}}\t开新一轮任务(agent停下后追加)
POST /subagent/{{id}}\tbody: {{"action": "stop"}}\t中断执行但保留(可继续input/reply)
GET /chat?last=N\t返回最近N条对话(默认20)
GET /subagent\t返回 {{"items": [...]}}\t查看所有subagent状态
@@ -501,8 +243,7 @@ GET /subagent/{{id}}?max_len=N\t返回单个subagent详情,reply经清洗后
1. 结合记忆、上下文和用户偏好判断真实需求;不清楚/不能代劳时,用精简checklist一次性问用户。
2. 判断是新任务还是延续现有任务;优先复用已有stopped subagent(用input追加),只有确实无关的新任务才新建。
3. 分派前必须POST /chat告知用户:改写后的prompt + 分派方案(新建/复用哪个subagent)。
4. 执行分派,完成即停。危险操作(改源码/删数据/安全敏感)必须改成先让subagent出方案;你验收后POST /chat请用户确认,确认后才继续执行。
5. 建议按照任务建立cwd下子目录并在prompt中要求subagent将文件输出到子目录""",
4. 执行分派,完成即停。危险操作(改源码/删数据/安全敏感)必须改成先让subagent出方案;你验收后POST /chat请用户确认,确认后才继续执行。""",
"subagent": """\
subagent完成流程:
1. 如果是IM采集subagent,按GET /readme/im进行而非本流程
@@ -529,56 +270,10 @@ class Conductor:
self.inbox: "queue.Queue[dict]" = queue.Queue() # 收件箱:唯一对外接口
self.agent: Optional[GenericAgent] = None
self.started = False
self._runner_thread: Optional[threading.Thread] = None
self.log: list = []
self._model_lock = threading.RLock()
self._model_state = {
"configured": None,
"effective": None,
"fallbackReason": "no_models",
"current": None,
"running": False,
}
def model_snapshot(self) -> dict:
with self._model_lock:
return dict(self._model_state)
def _publish_model_state(self, state: dict, running: bool) -> None:
snapshot = {**state, "running": bool(running)}
with self._model_lock:
self._model_state = snapshot
schedule_broadcast({"type": "model", "model": snapshot})
def notify(self, event: dict): self.inbox.put(event)
def _wait_for_agent_idle(self, timeout: float = 10.0) -> None:
"""Queue.join semantics with a bounded runner-health escape hatch."""
tasks = self.agent.task_queue
deadline = time.monotonic() + timeout
with tasks.all_tasks_done:
while tasks.unfinished_tasks:
if self._runner_thread is not None and not self._runner_thread.is_alive():
raise RuntimeError("conductor agent runner stopped before task cleanup")
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("conductor agent task cleanup timed out")
tasks.all_tasks_done.wait(min(0.25, remaining))
def _record_unavailable_model(self, events: list) -> None:
event_label = ",".join(event.get("type", "") for event in events) or "wake"
item = {
"id": short_id(),
"ts": now_ms(),
"event": event_label,
"turn": None,
"text": "Conductor paused: no usable model profiles are configured; events are deferred.",
}
self.log.append(item)
if len(self.log) > self.LOG_MAX:
self.log.pop(0)
schedule_broadcast({"type": "log", "item": item})
def _build_prompt(self, events: list) -> str:
running, stopped = pool.counts()
unread = sum(1 for m in chat_messages if m.get("role") == "user" and not m.get("read"))
@@ -598,8 +293,7 @@ API: {base};requests,GET /readme查用法,GET /chat读未读对话,GET /
- 改写prompt时严禁添加用户未提及的假设、工具、前提条件。只能精炼/结构化用户原意,不能脑补,只能做很小的改写
原则:
- 信任subagent足够聪明,不要写具体步骤和容易探测的信息;能自己判断的自己判断,只在真正需要用户决策时打扰。
- 向用户交付文件时,路径用反引号包裹。\n
- 信任subagent足够聪明,不要写具体步骤和容易探测的信息;能自己判断的自己判断,只在真正需要用户决策时打扰。\n
需要处理:
{summary}"""
@@ -633,18 +327,17 @@ API: {base};requests,GET /readme查用法,GET /chat读未读对话,GET /
return
def _run(self):
self.agent = GenericAgent()
self.agent.inc_out = True
self._runner_thread = start_agent_runner(self.agent, "conductor-agent")
start_agent_runner(self.agent, "conductor-agent")
self.started = True
deferred_events: list = []
while True:
# Block until first event arrives
first = self.inbox.get()
self.inbox.task_done()
# Short debounce: collect any additional events that arrived meanwhile
time.sleep(0.3)
events = [*deferred_events, first]
deferred_events = []
events = [first]
while not self.inbox.empty():
try:
events.append(self.inbox.get_nowait())
@@ -652,51 +345,18 @@ API: {base};requests,GET /readme查用法,GET /chat读未读对话,GET /
except Exception:
break
try:
# GenericAgent publishes the display ``done`` item before its
# runner finishes history persistence and task cleanup. Do not
# mutate the live client until that previous task has reached
# ``task_done``; the queue join is the authoritative idle barrier.
self._wait_for_agent_idle()
# Settings can change while this long-lived process remains healthy.
# Refresh only at the task boundary so an in-flight task keeps its
# client, while the next task observes the newly persisted binding.
model_state = _apply_desktop_model(self.agent)
if model_state.get("effective") is None:
self._publish_model_state(model_state, running=False)
self._record_unavailable_model(events)
deferred_events = events
continue
kind = events[0].get("type")
other_events = [event for event in events if event.get("type") != kind]
events = [event for event in events if event.get("type") == kind]
for event in other_events:
self.inbox.put(event)
prompt = self._build_prompt(events)
self._publish_model_state(model_state, running=True)
try:
dq = self.agent.put_task(prompt, source="conductor")
self._drain(dq, events)
finally:
self._publish_model_state(model_state, running=False)
dq = self.agent.put_task(prompt, source="conductor")
self._drain(dq, events)
except Exception as e: print(f"Conductor error: {e}")
def start(self):
self.agent = GenericAgent()
self._publish_model_state(_apply_desktop_model(self.agent), running=False)
threading.Thread(target=self._run, name="conductor-loop", daemon=True).start()
def start(self): threading.Thread(target=self._run, name="conductor-loop", daemon=True).start()
conductor = Conductor()
# ---- IM poller: 探测conductor_im_plugins/下各插件,信号变化→唤醒总管 ----
def _resolve_im_dir() -> str:
external = os.path.join(ROOT, "frontends", "conductor_im_plugins")
if os.path.isdir(external):
return external
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "conductor_im_plugins")
IM_DIR, IM_COOLDOWN = _resolve_im_dir(), 300
IM_DIR, IM_COOLDOWN = os.path.join(os.path.dirname(__file__), "conductor_im_plugins"), 300
IM_PROMPTS: Dict[str, str] = {} # source -> 采集prompt(派采集subagent时按需取)
def im_poll_loop():
@@ -721,36 +381,9 @@ def im_poll_loop():
last_fire[name] = now
conductor.notify({"type": "im_signal", "source": name})
@app.get("/token-stats")
def conductor_token_stats():
import cost_tracker
return {"records": [{"thread": k, "input": v.input, "output": v.output, "cacheCreate": v.cache_create, "cacheRead": v.cache_read} for k, v in cost_tracker.all_trackers().items()]}
@app.get("/")
def index(): return FileResponse(HTML_PATH)
@app.get("/files")
def files(path: str = FILE_HOME):
try: path = file_path(path)
except ValueError as e: raise HTTPException(403, str(e))
if not os.path.isdir(path): raise HTTPException(404, "directory not found")
items = [{"name": x.name, "path": x.path, "dir": x.is_dir(), "size": x.stat().st_size if x.is_file() else None}
for x in sorted(os.scandir(path), key=lambda x: (not x.is_dir(), x.name.lower()))]
return {"path": path, "parent": path if path == FILE_ROOT else os.path.dirname(path), "items": items}
@app.get("/file")
def get_file(path: str):
try: path = file_path(path)
except ValueError as e: raise HTTPException(403, str(e))
return FileResponse(path, filename=os.path.basename(path))
@app.post("/file")
async def put_file(name: str, request: Request):
os.makedirs(UPLOAD_DIR, exist_ok=True)
path = os.path.join(UPLOAD_DIR, os.path.basename(name))
with open(path, "wb") as f: f.write(await request.body())
return {"name": os.path.basename(path), "path": path}
@app.get("/readme")
def readme(): return PlainTextResponse(READMES["api"])
@@ -783,7 +416,7 @@ INSTR_DISPATCHED = "Task received. I'll handle THIS TASK from here. You MUST to
@app.post("/subagent")
def api_start_subagent(body: StartSubagentIn):
result = pool.start_subagent(body.prompt, body.llm)
result = pool.start_subagent(body.prompt)
result["instruction"] = INSTR_DISPATCHED
return result
@@ -797,7 +430,7 @@ def api_subagent_action(sid: str, body: SubagentActionIn):
result["instruction"] = "Received. I'll incorporate this. You MUST to do other task or end your reply."
return result
if action in ("input", "reply", "append", "message", "msg"):
result = pool.input_subagent(sid, body.msg, body.llm)
result = pool.input_subagent(sid, body.msg)
result["instruction"] = INSTR_DISPATCHED
return result
if action in ("abort", "stop"):
@@ -809,19 +442,15 @@ def api_subagent_action(sid: str, body: SubagentActionIn):
return JSONResponse({"error": f"unknown action: {body.action}"}, status_code=400)
@app.get("/chat")
def api_get_chat(last: int = 20, mark_read: bool = True):
items = [m.copy() for m in chat_messages[-last:]]
if mark_read:
for m in chat_messages:
if m.get("role") == "user" and not m.get("read"): m["read"] = True
schedule_broadcast({"type": "chat_read"})
return {"items": items}
def api_get_chat(last: int = 20):
for m in chat_messages:
if m.get("role") == "user" and not m.get("read"): m["read"] = True
schedule_broadcast({"type": "chat_read"})
return {"items": chat_messages[-last:]}
@app.post("/chat")
def api_chat(body: ChatIn):
item = add_chat(body.msg, role=body.role)
if body.role == "user": conductor.notify({"type": "user_message", "msg": body.msg})
return item
return add_chat(body.msg, role=body.role)
@app.post("/approval")
def api_approval(body: ApprovalIn):
@@ -833,38 +462,17 @@ async def websocket(ws: WebSocket):
await ws.accept()
ws_clients.add(ws)
try:
running = any(s.status == "running" for s in pool.subagents.values())
await ws.send_json({"type": "hello", "subagents": pool.snapshot(), "chat": chat_messages,
"log": conductor.log, "running": running,
"model": conductor.model_snapshot(),
"llms": conductor.agent.list_llms(),
"llm": _selected_conductor_llm_no(conductor.agent)})
await ws.send_json({"type": "hello", "subagents": pool.snapshot(), "chat": chat_messages, "log": conductor.log})
while True:
data = await ws.receive_json()
if "llm" in data:
try:
llm_no = _set_conductor_llm_no(conductor.agent, data["llm"])
except (TypeError, ValueError, OSError, DesktopSettingsError) as error:
await ws.send_json({
"type": "model_error",
"error": str(error),
"llm": _selected_conductor_llm_no(conductor.agent),
})
else:
await broadcast({"type": "model_selected", "llm": llm_no})
continue
msg = (data.get("msg") or "").strip()
if not msg: continue
add_chat(msg, role="user", files=data.get("files") or [], images=data.get("images") or [])
add_chat(msg, role="user")
conductor.notify({"type": "user_message", "msg": msg})
except WebSocketDisconnect: pass
finally: ws_clients.discard(ws)
if __name__ == "__main__":
import uvicorn
# bridge 自启 conductor 时传 --no-browser:不在用户浏览器里弹一个独立 conductor UI,
# 用户从桌面版「指挥家」页直接连过来即可。手动跑 conductor.py(没带 flag)保持原行为。
if not args.no_browser:
import webbrowser, threading
threading.Timer(1.0, lambda: webbrowser.open(f"http://127.0.0.1:{args.port}")).start()
uvicorn.run(app, host=args.host, port=args.port)
import uvicorn, webbrowser, threading
threading.Timer(1.0, lambda: webbrowser.open(f"http://{HOST}:{PORT}")).start()
uvicorn.run("conductor:app", host=HOST, port=PORT, reload=False)
+19 -88
View File
@@ -113,70 +113,11 @@ def _parse_native_history(pairs):
except Exception: return None
if not (isinstance(user_msg, dict) and user_msg.get('role') == 'user'): return None
if not isinstance(blocks, list): return None
history.append(user_msg) # runtime history 尊重日志真实 prompt(含 project-mode 当轮注入)
history.append(user_msg)
history.append({'role': 'assistant', 'content': blocks})
return history
def parse_native_log(path, allow_empty=False):
"""Parse a native `model_responses_*.txt` log into backend.history.
Public wrapper around the mature `/continue` parser. It intentionally only
restores complete Prompt→Response pairs; dangling Prompt / partial turns keep
the current continue semantics and are ignored. Returns:
- list (possibly [] when allow_empty=True) on native success;
- None when the file is unreadable / non-native / empty without allow_empty.
"""
try:
with open(path, encoding='utf-8', errors='replace') as fh:
content = fh.read()
except Exception:
return [] if allow_empty else None
pairs = _pairs(content)
if not pairs:
# Native-looking but incomplete logs (e.g. dangling Prompt without Response)
# have block headers but no complete pairs. For worldline's allow_empty mode
# this means "0 completed rounds", not "parse failed → trust live history".
if allow_empty and (_is_empty_log(path) or _BLOCK_RE.findall(content or '')):
return []
return None
return _parse_native_history(pairs)
def _derive_hist_info(history):
"""从 native history 重建 history_info(轮级纪要):真实用户提问 → `[USER]: …`;每条
assistant(=一轮) → 有字才写 `[Agent] …`(`<summary>` 优先,否则全文;无字跳过)。
与 ga.turn_end_callback / worldline 同口径;截断延用 [:80]。纯函数、不依赖 worldline,
供续接 opt-in 恢复工作记忆(见 restore_wm)。"""
def _all_text(m):
c = m.get('content') if isinstance(m, dict) else None
if isinstance(c, str): return c
if isinstance(c, list):
return '\n'.join(b.get('text', '') for b in c
if isinstance(b, dict) and b.get('type') == 'text')
return ''
def _is_tool_result(m):
c = m.get('content') if isinstance(m, dict) else None
return isinstance(c, list) and any(
isinstance(b, dict) and b.get('type') == 'tool_result' for b in c)
out = []
for m in history or []:
if not isinstance(m, dict): continue
role = m.get('role')
if role == 'user':
if _is_tool_result(m): continue # tool_result 轮不是提问
t = strip_project_mode(_all_text(m)).strip()
if t and not t.startswith(_INJECT_MARKERS):
out.append(f'[USER]: {t}')
elif role == 'assistant':
txt = re.sub(r'```.*?```|<thinking>.*?</thinking>', '', _all_text(m), flags=re.DOTALL)
mt = re.search(r'<summary>(.*?)</summary>', txt, re.DOTALL)
raw = (mt.group(1) if mt else txt).strip()
if raw:
out.append('[Agent] ' + raw.replace('\n', '')[:80])
return out
_PREVIEW_WIN = 32 * 1024
# Content-grep budget for `/continue` search box: read at most this many bytes
@@ -383,7 +324,7 @@ def list_sessions(exclude_pid=None, exclude_log=None, rewind_root=None):
valid_keys.append(key)
out.append((f, mtime, preview, rounds))
_save_rounds_cache(valid_keys)
# 【门控·worldline】树感知发现:日志存在但为空且有非空世界线树的会话(回退到起点后日志被
# 【门控·worldline】树感知发现:日志空/缺失但有非空世界线树的会话(回退到起点后日志被
# 清空 → 上面 sz<32 跳过了)。仅当调用方显式传 rewind_root 时启用 → 其他 UI 不传,
# 行为逐字节不变。只读 tree.json 的 nodes/head(不依赖 worldline 模块)。
if rewind_root and os.path.isdir(rewind_root):
@@ -399,11 +340,11 @@ def list_sessions(exclude_pid=None, exclude_log=None, rewind_root=None):
if log_name in have or log_name == exclude_log:
continue
log_path = os.path.join(_LOG_DIR, log_name)
try: # 仅收"日志确实为空"的(非空日志已被主循环收录)
try: # 仅收"日志确实空/缺失"的(非空日志已被主循环收录)
if os.path.getsize(log_path) >= 32:
continue
except OSError:
continue # 日志缺失(如已归档)不作为可续会话展示
pass # 缺失也算
try:
with open(os.path.join(rewind_root, key, 'tree.json'), encoding='utf-8') as fh:
d = json.load(fh)
@@ -541,19 +482,17 @@ def handle(agent, query, display_queue):
_INJECT_MARKERS = ('### [WORKING MEMORY]', '[SYSTEM TIPS]', '[SYSTEM]', '[System]',
'[DANGER]', '### [总结提炼经验]',
'Continue from where you left off')
'[DANGER]', '### [总结提炼经验]')
# project_mode 插件把 `\n\n---\n[PROJECT MODE: <name>]\n…\n---` 追加到当轮 user
# message(见 plugins/project_mode._build_injection)。runtime history 尊重日志真实 prompt,
# 但 UI 预览/显示与派生 history_info 需要只取用户原话。老日志的 WORKING MEMORY 里还
# 可能嵌着已污染的 `[USER]: ... [PROJECT MODE] ... [Agent] ...`,所以剥离支持 text
# 内任意位置与被截断的单行 title 形态。不能加进 _INJECT_MARKERS——那会把整条用户原话一起丢弃。
_PM_BLOCK_RE = re.compile(r"\s*-{3,}\s*\[PROJECT MODE:.*?(?:\n-{3,}\s*|$)", re.DOTALL)
# project_mode 插件把 `\n\n---\n[PROJECT MODE: <name>]\n…\n---` 追加在用户消息末尾
# (见 plugins/project_mode._build_injection)。它会进日志,所以 /continue 重建 UI 时
# 必须从显示文本里剔除,只留用户原话。不能加进 _INJECT_MARKERS——那会把整块(连用户
# 原话)一起丢弃;这里只剜掉注入这一段后缀。
_PM_BLOCK_RE = re.compile(r"\n*-{3,}\n\[PROJECT MODE:.*?\n-{3,}\s*$", re.DOTALL)
def strip_project_mode(text: str) -> str:
"""剔除文本中由 project_mode 插件追加的 L1 注入块。"""
"""剔除用户文本尾部的 project-mode 注入块。"""
return _PM_BLOCK_RE.sub("", text or "")
@@ -1086,13 +1025,9 @@ def begin_fresh_session(agent, agent_id=None):
_clear_conversation_state(agent)
def _load_history_into(agent, path, restore_wm=False):
def _load_history_into(agent, path):
"""把 `path` 解析进 backend.history(native;否则降级摘要)。镜像 restore() 的解析,
但不 abort/不快照(日志重指由调用方先做好)。返回 (msg, is_full)。
`restore_wm`(默认 False,仅显式传入的调用方启用,如 worldline TUI):从同一份日志
派生 history_info 写回 `agent.history`,使续接后工作记忆不丢。默认关闭 → 其它前端
行为完全不变。"""
但不 abort/不快照(日志重指由调用方先做好)。返回 (msg, is_full)。"""
try:
with open(path, encoding='utf-8', errors='replace') as fh:
content = fh.read()
@@ -1105,8 +1040,6 @@ def _load_history_into(agent, path, restore_wm=False):
name = os.path.basename(path)
if history is not None:
_replace_backend_history(agent, history)
if restore_wm and hasattr(agent, 'history'):
agent.history = _derive_hist_info(history) # 续接恢复工作记忆(opt-in)
return f'✅ 已恢复 {len(pairs)} 轮完整对话({name})', True
from chatapp_common import _restore_native_history, _restore_text_pairs
summary = _restore_text_pairs(content) or _restore_native_history(content)
@@ -1126,12 +1059,11 @@ def _is_empty_log(path):
return True
def continue_inplace(agent, path, agent_id=None, allow_empty=False, restore_wm=False):
def continue_inplace(agent, path, agent_id=None, allow_empty=False):
"""原地续:把 agent 的日志指回 `path` 本身,之后轮次追加到 X,延续同一会话。
调用方应已确认空闲(session_occupant 为 None);抢锁失败(被占)返回错误。
`allow_empty`(仅 worldline UI 传):日志为空时不报错,按【空会话】恢复(清空对话,
由调用方按 `.ga_rewind` 树重连),用于"回退至会话起点"的会话。
`restore_wm`(opt-in):续接后从日志派生 history_info 恢复工作记忆。返回 (msg, ok)。"""
由调用方按 `.ga_rewind` 树重连),用于"回退至会话起点"的会话。返回 (msg, ok)。"""
try: agent.abort()
except Exception: pass
if not acquire_lock(path, agent_id): # 先抢到目标锁;失败则保持现状,不丢自己的锁
@@ -1140,17 +1072,16 @@ def continue_inplace(agent, path, agent_id=None, allow_empty=False, restore_wm=F
if cur and os.path.basename(cur) != os.path.basename(path):
release_lock(cur) # 目标到手,旧会话释放为空闲(同一文件则不放)
_retarget_log(agent, path)
msg, ok = _load_history_into(agent, path, restore_wm=restore_wm)
msg, ok = _load_history_into(agent, path)
if not ok and allow_empty and _is_empty_log(path):
_replace_backend_history(agent, []) # 空会话:清空对话(载入失败时它没被清)
return '✅ 已恢复空会话(回退至会话起点;世界线树已重连)', True
return msg, ok
def continue_copy(agent, path, agent_id=None, allow_empty=False, restore_wm=False):
def continue_copy(agent, path, agent_id=None, allow_empty=False):
"""拷贝续:铸新 logid、把 `path` 内容拷进去,在副本上续;`path` 原件不动。
用于"被占用→用户选拷贝"以及快照源。`restore_wm`(opt-in)同 continue_inplace。
返回 (msg, ok)。"""
用于"被占用→用户选拷贝"以及快照源。返回 (msg, ok)。"""
try: agent.abort()
except Exception: pass
release_current(agent)
@@ -1161,7 +1092,7 @@ def continue_copy(agent, path, agent_id=None, allow_empty=False, restore_wm=Fals
pass
acquire_lock(newp, agent_id)
_retarget_log(agent, newp)
msg, ok = _load_history_into(agent, newp, restore_wm=restore_wm)
msg, ok = _load_history_into(agent, newp)
if not ok and allow_empty and _is_empty_log(newp):
_replace_backend_history(agent, [])
return '✅ 已恢复空会话(回退至会话起点;世界线树已重连)', True
+2 -252
View File
@@ -9,9 +9,8 @@ Subagent processes are out-of-process, so `scan_subagent_logs` parses the
same `[Cache]` / `[Output]` print lines from `temp/*/stdout.log`.
"""
from __future__ import annotations
import glob, json, os, re, threading, time
import glob, os, re, threading, time
from dataclasses import dataclass, field
from pathlib import Path
@dataclass
@@ -71,247 +70,6 @@ _CACHE_RE_OLD = re.compile(r'\[Cache\]\s+input=(\d+)\s+cached=(\d+)')
_INSTALLED = False
_SUBAGENT_GLOB = os.path.join("temp", "*", "stdout.log")
# ── Per-call ledger ──────────────────────────────────────────────────────────
_ledger_path: Path | None = None
_ledger_fd = None
_ledger_lock = threading.RLock()
_ledger_uncompacted_bytes = 0
_LEDGER_FILENAME = "token_ledger.jsonl"
_LEGACY_HISTORY_FILENAME = "desktop_token_history.json"
_COMPACT_THRESHOLD = 10 * 1024 * 1024 # 10MB
def _migrate_legacy_history_unlocked() -> None:
"""Seed an empty ledger from the previous aggregate JSON format once."""
global _ledger_uncompacted_bytes
if _ledger_path is None or _ledger_fd is None:
return
legacy_path = _ledger_path.with_name(_LEGACY_HISTORY_FILENAME)
try:
if _ledger_path.stat().st_size or not legacy_path.is_file():
return
doc = json.loads(legacy_path.read_text(encoding="utf-8"))
if not isinstance(doc, dict) or not isinstance(doc.get("snap"), dict):
return
metadata: dict[str, dict] = {}
for entry in doc.get("history", []):
if not isinstance(entry, dict):
continue
sid = entry.get("sessionId") or entry.get("id")
if not isinstance(sid, str) or not sid:
continue
key = sid if sid.startswith("GA-") else f"GA-{sid}"
try:
ts = float(entry.get("ts", 0) or 0)
except (TypeError, ValueError):
ts = 0
if key not in metadata or ts >= metadata[key]["ts"]:
metadata[key] = {
"ts": ts,
"model": entry.get("model") if isinstance(entry.get("model"), str) else "",
"title": entry.get("title") if isinstance(entry.get("title"), str) else "",
}
fallback_ts = legacy_path.stat().st_mtime
for key, totals in doc["snap"].items():
if not isinstance(key, str) or not key or not isinstance(totals, dict):
continue
try:
values = {
"i": int(totals.get("input", 0) or 0),
"o": int(totals.get("output", 0) or 0),
"cc": int(totals.get("cacheCreate", totals.get("cacheWrite", 0)) or 0),
"cr": int(totals.get("cacheRead", 0) or 0),
}
except (TypeError, ValueError):
continue
meta = metadata.get(key, {})
line = json.dumps(
{"t": meta.get("ts") or fallback_ts, "k": key, **values,
"m": meta.get("model", ""), "n": meta.get("title", ""), "_migrated": True},
separators=(",", ":"),
) + "\n"
_ledger_fd.write(line)
_ledger_uncompacted_bytes += len(line.encode("utf-8"))
_ledger_fd.flush()
except Exception:
return
def init_ledger(root: str) -> None:
"""Call once at bridge startup to set the ledger file path."""
global _ledger_path, _ledger_fd, _ledger_uncompacted_bytes
with _ledger_lock:
if _ledger_fd is not None:
try:
_ledger_fd.close()
except Exception:
pass
_ledger_path = Path(root) / "temp" / _LEDGER_FILENAME
_ledger_path.parent.mkdir(parents=True, exist_ok=True)
_ledger_fd = open(_ledger_path, "a", encoding="utf-8")
try:
_ledger_uncompacted_bytes = _ledger_path.stat().st_size
if _ledger_uncompacted_bytes == 0:
_migrate_legacy_history_unlocked()
if _ledger_uncompacted_bytes >= _COMPACT_THRESHOLD:
_compact_ledger()
except OSError:
_ledger_uncompacted_bytes = 0
def _append_ledger(thread_key: str, inp: int, out: int, cc: int, cr: int) -> None:
global _ledger_uncompacted_bytes
# TUI and conductor processes install the in-memory tracker but never call
# init_ledger(). Keep their historical hot path to one lock-free branch:
# no clock lookup, JSON encoding, byte counting, or ledger lock acquisition.
if _ledger_fd is None:
return
line = json.dumps(
{"t": time.time(), "k": thread_key, "i": inp, "o": out, "cc": cc, "cr": cr},
separators=(",", ":"),
) + "\n"
with _ledger_lock:
if _ledger_fd is None:
return
try:
_ledger_fd.write(line)
_ledger_fd.flush()
_ledger_uncompacted_bytes += len(line.encode("utf-8"))
if _ledger_uncompacted_bytes >= _COMPACT_THRESHOLD:
_compact_ledger()
except Exception:
pass
def _iter_ledger_unlocked():
if _ledger_path is None or not _ledger_path.is_file():
return
try:
with open(_ledger_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
if isinstance(entry, dict):
yield entry
except (json.JSONDecodeError, ValueError):
continue
except OSError:
return
def read_ledger() -> list[dict]:
"""Read all valid lines from the ledger. Skips corrupted lines."""
with _ledger_lock:
return list(_iter_ledger_unlocked())
def _aggregate_sessions(entries) -> dict[str, dict]:
sessions: dict[str, dict] = {}
for e in entries:
k = e.get("k", "")
if not isinstance(k, str) or not k:
continue
try:
ts = float(e.get("t", 0) or 0)
inp = int(e.get("i", 0) or 0)
out = int(e.get("o", 0) or 0)
cc = int(e.get("cc", 0) or 0)
cr = int(e.get("cr", 0) or 0)
except (TypeError, ValueError):
continue
if k not in sessions:
sessions[k] = {"input": 0, "output": 0, "cacheCreate": 0, "cacheRead": 0,
"model": "", "title": "", "first_ts": ts, "last_ts": ts}
s = sessions[k]
s["input"] += inp
s["output"] += out
s["cacheCreate"] += cc
s["cacheRead"] += cr
s["first_ts"] = min(s["first_ts"], ts)
s["last_ts"] = max(s["last_ts"], ts)
if isinstance(e.get("m"), str) and e["m"]:
s["model"] = e["m"]
if isinstance(e.get("n"), str) and e["n"]:
s["title"] = e["n"]
return sessions
def _format_aggregate(sessions: dict[str, dict]) -> dict:
history = []
snap = {}
for k, s in sessions.items():
sid = k.removeprefix("GA-")
history.append({
"sessionId": sid,
"title": s["title"] or sid,
"input": s["input"],
"output": s["output"],
"cacheCreate": s["cacheCreate"],
"cacheRead": s["cacheRead"],
"model": s["model"],
"ts": s["last_ts"],
})
snap[k] = {
"input": s["input"],
"output": s["output"],
"cacheCreate": s["cacheCreate"],
"cacheRead": s["cacheRead"],
}
return {"history": history, "snap": snap}
def aggregate_ledger() -> dict:
"""Aggregate ledger into {history: [...], snap: {...}} for /token-history."""
with _ledger_lock:
return _format_aggregate(_aggregate_sessions(_iter_ledger_unlocked()))
def _compact_ledger() -> None:
"""Compact ledger by aggregating into per-session totals and rewriting."""
if _ledger_path is None:
return
global _ledger_fd, _ledger_uncompacted_bytes
with _ledger_lock:
temp_path = _ledger_path.with_suffix(".jsonl.tmp")
try:
if _ledger_fd is not None:
_ledger_fd.flush()
sessions = _aggregate_sessions(_iter_ledger_unlocked())
with open(temp_path, "w", encoding="utf-8") as f:
for k, s in sessions.items():
line = json.dumps(
{"t": s["last_ts"], "k": k, "i": s["input"], "o": s["output"],
"cc": s["cacheCreate"], "cr": s["cacheRead"], "m": s["model"],
"n": s["title"],
"_compacted": True},
separators=(",", ":"),
)
f.write(line + "\n")
f.flush()
os.fsync(f.fileno())
if _ledger_fd is not None:
_ledger_fd.close()
_ledger_fd = None
os.replace(temp_path, _ledger_path)
_ledger_fd = open(_ledger_path, "a", encoding="utf-8")
_ledger_uncompacted_bytes = 0
except Exception:
try:
temp_path.unlink(missing_ok=True)
except Exception:
pass
if _ledger_fd is None:
try:
_ledger_fd = open(_ledger_path, "a", encoding="utf-8")
except Exception:
pass
# ── Core API ─────────────────────────────────────────────────────────────────
def scan_subagent_logs(since: float = 0.0, root: str | None = None) -> TokenStats:
"""Aggregate subagent tokens from `temp/<task>/stdout.log` files; pass
@@ -377,7 +135,6 @@ def install() -> None:
if usage:
t = get(threading.current_thread().name)
t.requests += 1
inp = cc = cr = 0
if api_mode == 'messages':
inp = int(usage.get('input_tokens', 0) or 0)
cc = int(usage.get('cache_creation_input_tokens', 0) or 0)
@@ -387,24 +144,18 @@ def install() -> None:
# output_tokens here; SSE message_start carries a 1-token
# placeholder to skip.
out = int(usage.get('output_tokens', 0) or 0)
if out > 1:
t.output += out; t.last_output = out
_append_ledger(threading.current_thread().name, inp, out, cc, cr)
else:
_append_ledger(threading.current_thread().name, inp, 0, cc, cr)
if out > 1: t.output += out; t.last_output = out
t.last_input = inp + cc + cr
elif api_mode == 'chat_completions':
cached = int((usage.get('prompt_tokens_details') or {}).get('cached_tokens', 0) or 0)
inp = int(usage.get('prompt_tokens', 0) or 0) - cached
t.input += inp; t.cache_read += cached
t.last_input = inp + cached
_append_ledger(threading.current_thread().name, inp, 0, 0, cached)
elif api_mode == 'responses':
cached = int((usage.get('input_tokens_details') or {}).get('cached_tokens', 0) or 0)
inp = int(usage.get('input_tokens', 0) or 0) - cached
t.input += inp; t.cache_read += cached
t.last_input = inp + cached
_append_ledger(threading.current_thread().name, inp, 0, 0, cached)
except Exception: pass
return orig_record(usage, api_mode)
llmcore._record_usage = record_patched
@@ -417,7 +168,6 @@ def install() -> None:
t = get(threading.current_thread().name)
n = int(m.group(1))
t.output += n; t.last_output = n
_append_ledger(threading.current_thread().name, 0, n, 0, 0)
except Exception: pass
return orig_print(*args, **kwargs)
llmcore.print = print_patched
-857
View File
@@ -1,857 +0,0 @@
"""Safe, dependency-free import/export helpers for desktop data snapshots."""
from __future__ import annotations
import contextlib
import datetime as dt
import errno
import json
import math
import os
import re
import shutil
import stat
import tempfile
import uuid
import zipfile
from pathlib import Path, PurePosixPath
from typing import Any, Callable, Iterable, Iterator
BACKUP_SCHEMA = "genericagent.data-backup"
BACKUP_FORMAT_VERSION = 1
MAX_ARCHIVE_ENTRIES = 100_000
MAX_ARCHIVE_UNCOMPRESSED_BYTES = 2 * 1024 * 1024 * 1024
_DATA_PREFIXES = (
PurePosixPath("memory"),
PurePosixPath("temp/model_responses"),
PurePosixPath("temp/desktop_sessions"),
)
_DESKTOP_SESSION_ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]{0,127}")
class BackupFormatError(ValueError):
"""Raised when a backup cannot be trusted or is not compatible."""
def _is_relative_to(path: PurePosixPath, parent: PurePosixPath) -> bool:
return path == parent or parent in path.parents
def _is_native_relative_to(path: Path, parent: Path) -> bool:
try:
path.relative_to(parent)
return True
except ValueError:
return False
def _is_link_or_reparse(path: Path) -> bool:
"""Recognise POSIX links plus Windows junction/reparse-point indirection."""
try:
if path.is_symlink():
return True
is_junction = getattr(path, "is_junction", None)
if callable(is_junction) and is_junction():
return True
attributes = int(getattr(os.lstat(path), "st_file_attributes", 0) or 0)
reparse_flag = int(getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400))
return bool(attributes & reparse_flag)
except OSError:
return False
def _resolved_directory(value: str | Path, label: str) -> Path:
raw = Path(value).expanduser()
if _is_link_or_reparse(raw):
raise ValueError(f"{label} cannot be a link or reparse point")
try:
resolved = raw.resolve(strict=True)
except OSError as error:
raise ValueError(f"{label} is unavailable") from error
if not resolved.is_dir() or _is_link_or_reparse(resolved):
raise ValueError(f"{label} is unavailable")
return resolved
def _assert_contained(path: Path, root: Path, label: str) -> Path:
try:
resolved = path.resolve(strict=True)
except OSError as error:
raise ValueError(f"{label} is unavailable") from error
if not _is_native_relative_to(resolved, root):
raise ValueError(f"{label} escapes its data root")
return resolved
def _archive_path(name: str) -> PurePosixPath:
if not name or "\\" in name:
raise BackupFormatError("invalid backup entry path")
if any(part in ("", ".", "..") for part in name.split("/")):
raise BackupFormatError("invalid backup entry path")
path = PurePosixPath(name)
if path.is_absolute() or any(part in ("", ".", "..") for part in path.parts):
raise BackupFormatError("invalid backup entry path")
return path
def _is_allowed_data_path(path: PurePosixPath) -> bool:
return any(_is_relative_to(path, prefix) for prefix in _DATA_PREFIXES)
def _iter_regular_files(root: Path) -> Iterator[tuple[Path, PurePosixPath]]:
root = root.resolve()
for folder, dirs, files in os.walk(root, followlinks=False):
folder_path = Path(folder)
if _is_link_or_reparse(folder_path):
raise ValueError(f"data folder contains a link or reparse point: {folder_path}")
safe_dirs: list[str] = []
for name in sorted(dirs):
candidate = folder_path / name
if _is_link_or_reparse(candidate):
raise ValueError(f"data folder contains a link or reparse point: {candidate}")
_assert_contained(candidate, root, "data folder")
safe_dirs.append(name)
dirs[:] = safe_dirs
for name in sorted(files):
source = folder_path / name
if _is_link_or_reparse(source):
raise ValueError(f"data folder contains a link or reparse point: {source}")
if not source.is_file():
raise ValueError(f"data folder contains an unsupported file: {source}")
_assert_contained(source, root, "data file")
relative = PurePosixPath(source.relative_to(root).as_posix())
yield source, relative
def _source_files(root: Path) -> list[tuple[Path, PurePosixPath]]:
files: list[tuple[Path, PurePosixPath]] = []
for prefix in _DATA_PREFIXES:
source_root = root.joinpath(*prefix.parts)
if not source_root.exists():
continue
if _is_link_or_reparse(source_root) or not source_root.is_dir():
raise ValueError(f"data folder is not a safe directory: {source_root}")
for source, relative in _iter_regular_files(source_root):
files.append((source, prefix / relative))
return files
def _content_counts(paths: list[PurePosixPath]) -> dict[str, int]:
return {
"memory": sum(_is_relative_to(path, _DATA_PREFIXES[0]) for path in paths),
"responses": sum(_is_relative_to(path, _DATA_PREFIXES[1]) for path in paths),
"sessions": sum(_is_relative_to(path, _DATA_PREFIXES[2]) for path in paths),
}
def _normalise_source_mode(value: str) -> str:
if value not in ("included", "localRepository"):
raise BackupFormatError("invalid backup source mode")
return value
def _manifest(source_mode: str, paths: list[PurePosixPath]) -> dict:
exported_at = dt.datetime.now(dt.timezone.utc).replace(microsecond=0)
return {
"schema": BACKUP_SCHEMA,
"formatVersion": BACKUP_FORMAT_VERSION,
"exportedAt": exported_at.isoformat().replace("+00:00", "Z"),
"sourceMode": _normalise_source_mode(source_mode),
"content": _content_counts(paths),
}
def export_data_backup(
ga_root: str,
destination_path: str,
source_mode: str,
*,
forbidden_roots: Iterable[str | Path] = (),
) -> dict:
root = _resolved_directory(ga_root, "current data source")
raw_destination = Path(destination_path).expanduser()
if _is_link_or_reparse(raw_destination):
raise ValueError("backup destination cannot be a link or reparse point")
destination = raw_destination.resolve()
if destination.suffix.lower() != ".zip":
raise ValueError("backup destination must be a zip file")
if not destination.parent.is_dir() or _is_link_or_reparse(destination.parent):
raise ValueError("backup destination folder does not exist")
protected_roots = [root.joinpath(*prefix.parts).resolve() for prefix in _DATA_PREFIXES]
protected_roots.extend(Path(value).expanduser().resolve() for value in forbidden_roots)
if any(_is_native_relative_to(destination, protected) for protected in protected_roots):
raise ValueError("backup destination is inside protected application data")
files = _source_files(root)
paths = [relative for _, relative in files]
if not paths:
raise BackupFormatError("backup contains no importable data")
manifest = _manifest(source_mode, paths)
manifest_bytes = (
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n"
).encode("utf-8")
if len(files) + 1 > MAX_ARCHIVE_ENTRIES:
raise BackupFormatError("backup contains too many files")
total_bytes = len(manifest_bytes)
for source, _relative in files:
_assert_contained(source, root, "data file")
total_bytes += source.stat().st_size
if total_bytes > MAX_ARCHIVE_UNCOMPRESSED_BYTES:
raise BackupFormatError("backup is too large")
temp_handle = tempfile.NamedTemporaryFile(
prefix=f".{destination.stem}.",
suffix=".tmp",
dir=destination.parent,
delete=False,
)
temp_path = Path(temp_handle.name)
temp_handle.close()
try:
with zipfile.ZipFile(
temp_path,
"w",
compression=zipfile.ZIP_DEFLATED,
compresslevel=6,
) as archive:
archive.writestr("manifest.json", manifest_bytes)
for source, relative in files:
if _is_link_or_reparse(source):
raise ValueError(f"data file became a link or reparse point: {source}")
_assert_contained(source, root, "data file")
archive.write(source, relative.as_posix())
# Validate the bytes that will actually replace the destination. A source
# can change after the preflight stat, and ZipFile only finalises CRCs and
# the central directory when it closes.
with zipfile.ZipFile(temp_path, "r") as archive:
_validated_zip(archive)
os.replace(temp_path, destination)
except Exception:
with contextlib.suppress(OSError):
temp_path.unlink()
raise
return {
"ok": True,
"path": str(destination),
"formatVersion": BACKUP_FORMAT_VERSION,
"exportedAt": manifest["exportedAt"],
"sourceMode": manifest["sourceMode"],
"content": manifest["content"],
}
def _validated_zip(archive: zipfile.ZipFile) -> tuple[dict, list[zipfile.ZipInfo]]:
infos = archive.infolist()
if len(infos) > MAX_ARCHIVE_ENTRIES:
raise BackupFormatError("backup contains too many files")
total_bytes = sum(info.file_size for info in infos)
if total_bytes > MAX_ARCHIVE_UNCOMPRESSED_BYTES:
raise BackupFormatError("backup is too large")
if archive.testzip() is not None:
raise BackupFormatError("backup is corrupt")
manifest_info: zipfile.ZipInfo | None = None
data_infos: list[zipfile.ZipInfo] = []
seen_paths: set[str] = set()
for info in infos:
path = _archive_path(info.filename.rstrip("/"))
folded_path = path.as_posix().casefold()
if folded_path in seen_paths:
raise BackupFormatError("backup contains duplicate file paths")
seen_paths.add(folded_path)
unix_mode = (info.external_attr >> 16) & 0o170000
if unix_mode == stat.S_IFLNK:
raise BackupFormatError("backup contains links")
if path == PurePosixPath("manifest.json"):
if info.is_dir() or manifest_info is not None:
raise BackupFormatError("backup manifest is invalid")
manifest_info = info
continue
if not _is_allowed_data_path(path):
raise BackupFormatError("backup contains unsupported files")
if not info.is_dir():
data_infos.append(info)
if manifest_info is None:
raise BackupFormatError("backup manifest is missing")
try:
manifest = json.loads(archive.read(manifest_info).decode("utf-8"))
except Exception as error:
raise BackupFormatError("backup manifest is invalid") from error
if not isinstance(manifest, dict):
raise BackupFormatError("backup manifest is invalid")
if manifest.get("schema") != BACKUP_SCHEMA:
raise BackupFormatError("backup format is not supported")
if manifest.get("formatVersion") != BACKUP_FORMAT_VERSION:
raise BackupFormatError("backup version is not supported")
_normalise_source_mode(str(manifest.get("sourceMode") or ""))
if not isinstance(manifest.get("exportedAt"), str) or not manifest["exportedAt"]:
raise BackupFormatError("backup export time is missing")
actual_counts = _content_counts([
_archive_path(info.filename) for info in data_infos
])
if not data_infos:
raise BackupFormatError("backup contains no importable data")
if manifest.get("content") != actual_counts:
raise BackupFormatError("backup content summary does not match its files")
return manifest, data_infos
def _legacy_inspection(root: Path) -> dict:
files = _source_files(root)
paths = [relative for _, relative in files]
session_items, _sessions_found, _sessions_skipped = _read_source_sessions(root)
counts = _content_counts(paths)
counts["sessions"] = len(session_items)
if not any(counts.values()):
raise BackupFormatError("not a GA data source (no importable data)")
return {
"ok": True,
"sourceType": "legacyFolder",
"formatVersion": None,
"exportedAt": None,
"sourceMode": None,
"content": counts,
}
def inspect_import_source(source_path: str) -> dict:
raw_source = Path(source_path).expanduser()
if _is_link_or_reparse(raw_source):
raise BackupFormatError("import source cannot be a link or reparse point")
source = raw_source.resolve()
if source.is_dir() and not _is_link_or_reparse(source):
return _legacy_inspection(source)
if not source.is_file() or source.suffix.lower() != ".zip":
raise BackupFormatError("select a compatible backup or data folder")
try:
with zipfile.ZipFile(source, "r") as archive:
manifest, _ = _validated_zip(archive)
except zipfile.BadZipFile as error:
raise BackupFormatError("backup is corrupt") from error
return {
"ok": True,
"sourceType": "backupZip",
"formatVersion": manifest["formatVersion"],
"exportedAt": manifest["exportedAt"],
"sourceMode": manifest["sourceMode"],
"content": manifest["content"],
}
@contextlib.contextmanager
def materialize_import_source(source_path: str) -> Iterator[Path]:
raw_source = Path(source_path).expanduser()
if _is_link_or_reparse(raw_source):
raise BackupFormatError("import source cannot be a link or reparse point")
source = raw_source.resolve()
inspection = inspect_import_source(str(source))
if inspection["sourceType"] == "legacyFolder":
yield source
return
with tempfile.TemporaryDirectory(prefix="genericagent-data-import-") as temp_dir:
target_root = _resolved_directory(temp_dir, "import staging")
try:
with zipfile.ZipFile(source, "r") as archive:
_, data_infos = _validated_zip(archive)
for info in data_infos:
relative = _archive_path(info.filename)
target = target_root.joinpath(*relative.parts)
_assert_safe_new_target(target_root, target)
target.parent.mkdir(parents=True, exist_ok=True)
_assert_contained(target.parent, target_root, "import destination")
if target.exists() or _is_link_or_reparse(target):
raise BackupFormatError("backup contains duplicate or unsafe paths")
with archive.open(info, "r") as reader, target.open("wb") as writer:
shutil.copyfileobj(reader, writer)
_assert_contained(target, target_root, "import destination")
except zipfile.BadZipFile as error:
raise BackupFormatError("backup is corrupt") from error
yield target_root
def _is_desktop_session_id(value: object) -> bool:
if not isinstance(value, str):
return False
session_id = value
return (
not session_id.startswith("tui_")
and _DESKTOP_SESSION_ID_RE.fullmatch(session_id) is not None
)
def _finite_timestamp(value: object, default: float) -> float:
if value is None:
return default
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise BackupFormatError("session timestamp is invalid")
timestamp = float(value)
if not math.isfinite(timestamp):
raise BackupFormatError("session timestamp is invalid")
return timestamp
def _non_negative_integer(value: object, default: int, label: str) -> int:
if value is None:
return default
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
raise BackupFormatError(f"session {label} is invalid")
return value
def _record_list(value: object, default: list[dict], label: str) -> list[dict]:
if value is None:
return list(default)
if not isinstance(value, list) or not all(isinstance(item, dict) for item in value):
raise BackupFormatError(f"session {label} is invalid")
return value
def canonical_session_record(
item: object,
*,
default_cwd: str = "",
now: float | None = None,
) -> dict[str, Any]:
"""Validate and reduce an imported/persisted Desktop session to its schema."""
if not isinstance(item, dict):
raise BackupFormatError("session record is invalid")
session_id = item.get("id")
if not _is_desktop_session_id(session_id):
raise BackupFormatError("session id is invalid")
current_time = float(now if now is not None else dt.datetime.now().timestamp())
title = item.get("title", "New chat")
cwd = item.get("cwd", default_cwd)
plan_path = item.get("plan_path", "")
if not isinstance(title, str) or not isinstance(cwd, str) or not isinstance(plan_path, str):
raise BackupFormatError("session text field is invalid")
messages = _record_list(item.get("messages"), [], "messages")
llm_history_value = item.get("llm_history")
llm_history = None if llm_history_value is None else _record_list(
llm_history_value, [], "llm history"
)
llm_no_value = item.get("llm_no")
llm_no = None if llm_no_value is None else _non_negative_integer(
llm_no_value, 0, "model index"
)
pinned = item.get("pinned", False)
untitled = item.get("untitled", True)
if not isinstance(pinned, bool) or not isinstance(untitled, bool):
raise BackupFormatError("session boolean field is invalid")
return {
"id": session_id,
"title": title,
"cwd": cwd,
"created_at": _finite_timestamp(item.get("created_at"), current_time),
"updated_at": _finite_timestamp(item.get("updated_at"), current_time),
"messages": messages,
"msg_seq": _non_negative_integer(item.get("msg_seq"), 0, "message sequence"),
"pinned": pinned,
"untitled": untitled,
"plan_scan_baseline": _non_negative_integer(
item.get("plan_scan_baseline"), 0, "plan baseline"
),
"plan_path": plan_path,
"llm_no": llm_no,
"llm_history": llm_history,
}
def _read_source_sessions(source: Path) -> tuple[list[dict], bool, int]:
"""Read supported Desktop session stores without trusting filenames.
Corrupt/non-Desktop records are skipped. The skipped count is intentionally
record-oriented; an unreadable file counts as one skipped source record.
"""
items: list[dict] = []
found = False
skipped = 0
sessions_dir = source / "temp" / "desktop_sessions"
if sessions_dir.exists() and _is_link_or_reparse(sessions_dir):
raise ValueError(f"session source contains a link or reparse point: {sessions_dir}")
if sessions_dir.is_dir():
for session_file in sorted(sessions_dir.glob("*.json")):
if _is_link_or_reparse(session_file):
raise ValueError(
f"session source contains a link or reparse point: {session_file}"
)
if not session_file.is_file():
raise ValueError(f"session source contains an unsupported file: {session_file}")
found = True
try:
raw_item = json.loads(session_file.read_text(encoding="utf-8"))
item = canonical_session_record(raw_item)
except (OSError, UnicodeError, json.JSONDecodeError, ValueError):
skipped += 1
continue
items.append(item)
for legacy in (
source / "temp" / "desktop_sessions.json",
source / "temp" / "desktop_sessions.json.migrated",
):
if _is_link_or_reparse(legacy):
raise ValueError(f"session source contains a link or reparse point: {legacy}")
if not legacy.is_file():
continue
found = True
try:
document = json.loads(legacy.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError, ValueError):
skipped += 1
continue
if not isinstance(document, list):
skipped += 1
continue
for item in document:
try:
items.append(canonical_session_record(item))
except ValueError:
skipped += 1
return items, found, skipped
def _existing_session_ids(destination_root: Path) -> set[str]:
ids: set[str] = set()
sessions_dir = destination_root / "temp" / "desktop_sessions"
if _is_link_or_reparse(sessions_dir):
raise ValueError(f"session destination contains a link or reparse point: {sessions_dir}")
if not sessions_dir.is_dir():
return ids
for session_file in sorted(sessions_dir.glob("*.json")):
if _is_link_or_reparse(session_file):
raise ValueError(
f"session destination contains a link or reparse point: {session_file}"
)
if not session_file.is_file():
raise ValueError(f"session destination contains an unsupported file: {session_file}")
if _is_desktop_session_id(session_file.stem):
ids.add(session_file.stem)
try:
item = canonical_session_record(
json.loads(session_file.read_text(encoding="utf-8"))
)
except (OSError, UnicodeError, json.JSONDecodeError, ValueError):
continue
ids.add(item["id"])
return ids
def _remove_path(path: Path) -> None:
if _is_link_or_reparse(path) or path.is_file():
path.unlink()
elif path.is_dir():
shutil.rmtree(path)
def _copy_tree_strict(source: Path, destination: Path) -> None:
"""Copy a complete tree while refusing links and special files."""
if _is_link_or_reparse(source) or not source.is_dir():
raise ValueError(f"cannot safely copy data folder: {source}")
destination.mkdir(parents=True, exist_ok=False)
for item in sorted(source.iterdir(), key=lambda path: path.name):
if _is_link_or_reparse(item):
raise ValueError(f"data folder contains a link or reparse point: {item}")
_assert_contained(item, source, "data item")
target = destination / item.name
if item.is_dir():
_copy_tree_strict(item, target)
elif item.is_file():
shutil.copy2(item, target)
else:
raise ValueError(f"data folder contains an unsupported file: {item}")
def _prepare_overlay_target(root: Path, relative: PurePosixPath) -> Path:
current = root
for part in relative.parts[:-1]:
current = current / part
if _is_link_or_reparse(current):
raise ValueError(f"memory destination contains a link or reparse point: {current}")
if current.exists() and not current.is_dir():
_remove_path(current)
current.mkdir(exist_ok=True)
_assert_contained(current, root, "memory destination")
target = root.joinpath(*relative.parts)
if _is_link_or_reparse(target):
raise ValueError(f"memory destination contains a link or reparse point: {target}")
if target.is_dir():
_remove_path(target)
return target
def _assert_safe_new_target(root: Path, target: Path) -> None:
relative = target.relative_to(root)
current = root
for part in relative.parts[:-1]:
current = current / part
if _is_link_or_reparse(current):
raise ValueError(f"data destination contains a link or reparse point: {current}")
if current.exists() and not current.is_dir():
raise ValueError(f"data destination parent is not a directory: {current}")
if current.exists():
_assert_contained(current, root, "data destination")
if _is_link_or_reparse(target):
raise ValueError(f"data destination contains a link or reparse point: {target}")
def _new_backup_path(destination_root: Path) -> Path:
timestamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
backup_parent = destination_root / "temp"
for suffix in range(1000):
tail = "" if suffix == 0 else f"_{suffix}"
candidate = backup_parent / f"memory_import_backup_{timestamp}{tail}"
if not candidate.exists() and not _is_link_or_reparse(candidate):
return candidate
raise OSError("cannot allocate a unique memory backup directory")
def _create_memory_backup(destination_root: Path, memory_root: Path) -> Path:
backup_dir = _new_backup_path(destination_root)
backup_parent = backup_dir.parent
if _is_link_or_reparse(backup_parent) or (
backup_parent.exists() and not backup_parent.is_dir()
):
raise ValueError("current backup destination is not a safe directory")
backup_parent.mkdir(parents=True, exist_ok=True)
_assert_contained(backup_parent, destination_root, "backup destination")
staging = backup_parent / f".{backup_dir.name}.staging-{uuid.uuid4().hex}"
try:
staging.mkdir()
_copy_tree_strict(memory_root, staging / "memory")
os.replace(staging, backup_dir)
except Exception:
with contextlib.suppress(OSError):
_remove_path(staging)
raise
return backup_dir
def _install_file_add_only(staged: Path, target: Path) -> None:
"""Install one staged file without ever replacing an existing path."""
try:
os.link(staged, target, follow_symlinks=False)
except OSError as error:
unsupported_link_errors = {
errno.EINVAL,
errno.EPERM,
errno.EXDEV,
getattr(errno, "ENOTSUP", errno.EINVAL),
getattr(errno, "EOPNOTSUPP", errno.EINVAL),
}
if error.errno not in unsupported_link_errors:
raise
# Some removable/network filesystems do not implement hard links.
# O_EXCL retains the add-only contract there; a failed copy removes
# only the path this call successfully created.
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
flags |= getattr(os, "O_BINARY", 0)
descriptor: int | None = None
created = False
try:
descriptor = os.open(target, flags, stat.S_IMODE(staged.stat().st_mode))
created = True
with staged.open("rb") as reader, os.fdopen(descriptor, "wb") as writer:
descriptor = None
shutil.copyfileobj(reader, writer)
writer.flush()
os.fsync(writer.fileno())
except Exception:
if descriptor is not None:
with contextlib.suppress(OSError):
os.close(descriptor)
if created:
with contextlib.suppress(OSError):
target.unlink()
raise
with contextlib.suppress(OSError):
staged.unlink()
def merge_data_files(
source_dir: str,
ga_root: str,
*,
existing_session_ids: Iterable[str] | None = None,
session_preparer: Callable[[dict], object] | None = None,
) -> dict[str, Any]:
"""Transactionally merge a validated data tree into ``ga_root``.
``memory`` is source-wins after a durable full backup, responses and
Desktop sessions are add-only, and any activation error rolls back files
installed by this call. ``session_preparer`` lets the bridge validate and
construct its in-memory Session objects before any destination is changed.
"""
source = _resolved_directory(source_dir, "source data folder")
destination_root = _resolved_directory(ga_root, "current data destination")
if source == destination_root:
raise ValueError("source is the same as current data")
inspection = _legacy_inspection(source)
source_memory = source.joinpath(*_DATA_PREFIXES[0].parts)
source_responses = source.joinpath(*_DATA_PREFIXES[1].parts)
destination_memory = destination_root.joinpath(*_DATA_PREFIXES[0].parts)
destination_responses = destination_root.joinpath(*_DATA_PREFIXES[1].parts)
has_source_memory = source_memory.is_dir() and not _is_link_or_reparse(source_memory)
memory_files = list(_iter_regular_files(source_memory)) if has_source_memory else []
response_files: list[tuple[Path, PurePosixPath, Path]] = []
responses_skipped = 0
if source_responses.exists() and _is_link_or_reparse(source_responses):
raise ValueError(f"response source contains a link or reparse point: {source_responses}")
if source_responses.is_dir():
for item, relative in _iter_regular_files(source_responses):
target = destination_responses.joinpath(*relative.parts)
if target.exists() or _is_link_or_reparse(target):
responses_skipped += 1
continue
_assert_safe_new_target(destination_root, target)
response_files.append((item, relative, target))
source_session_items, sessions_found, sessions_skipped = _read_source_sessions(source)
known_session_ids = _existing_session_ids(destination_root)
if existing_session_ids is not None:
known_session_ids.update(
str(value) for value in existing_session_ids if _is_desktop_session_id(value)
)
planned_session_ids: set[str] = set()
session_files: list[tuple[str, dict, Path, object]] = []
for item in source_session_items:
session_id = item.get("id")
if not _is_desktop_session_id(session_id):
sessions_skipped += 1
continue
session_id = str(session_id)
target = destination_root / "temp" / "desktop_sessions" / f"{session_id}.json"
if (
session_id in known_session_ids
or session_id in planned_session_ids
or target.exists()
or _is_link_or_reparse(target)
):
sessions_skipped += 1
continue
_assert_safe_new_target(destination_root, target)
try:
prepared = session_preparer(item) if session_preparer is not None else item
except Exception:
sessions_skipped += 1
continue
planned_session_ids.add(session_id)
session_files.append((session_id, item, target, prepared))
backup_dir = ""
prepared_sessions = [entry[3] for entry in session_files]
with tempfile.TemporaryDirectory(
prefix=".genericagent-memory-import-",
dir=destination_root,
) as temp_dir:
staging_root = Path(temp_dir)
if _is_link_or_reparse(staging_root):
raise ValueError("import staging cannot be a link or reparse point")
_assert_contained(staging_root, destination_root, "import staging")
staged_memory = staging_root / "memory"
if has_source_memory:
if _is_link_or_reparse(destination_memory) or (
destination_memory.exists() and not destination_memory.is_dir()
):
raise ValueError("current memory destination is not a safe directory")
if destination_memory.is_dir():
_copy_tree_strict(destination_memory, staged_memory)
else:
staged_memory.mkdir()
for item, relative in memory_files:
target = _prepare_overlay_target(staged_memory, relative)
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item, target)
staged_responses: list[tuple[Path, Path]] = []
for item, relative, target in response_files:
staged = staging_root / "responses" / Path(*relative.parts)
staged.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item, staged)
staged_responses.append((staged, target))
staged_sessions: list[tuple[Path, Path]] = []
for session_id, item, target, _prepared in session_files:
staged = staging_root / "sessions" / f"{session_id}.json"
staged.parent.mkdir(parents=True, exist_ok=True)
staged.write_text(
json.dumps(item, ensure_ascii=False, default=str),
encoding="utf-8",
)
staged_sessions.append((staged, target))
memory_nonempty = (
destination_memory.is_dir()
and next(destination_memory.iterdir(), None) is not None
)
if has_source_memory and memory_nonempty:
backup_dir = str(_create_memory_backup(destination_root, destination_memory))
installed_files: list[Path] = []
previous_memory = staging_root / "previous-memory"
memory_original_moved = False
memory_activated = False
memory_existed = destination_memory.is_dir()
try:
if has_source_memory:
if memory_existed:
os.replace(destination_memory, previous_memory)
memory_original_moved = True
os.replace(staged_memory, destination_memory)
memory_activated = True
for staged, target in (*staged_responses, *staged_sessions):
if target.exists() or _is_link_or_reparse(target):
raise OSError(f"data destination changed during import: {target}")
_assert_safe_new_target(destination_root, target)
target.parent.mkdir(parents=True, exist_ok=True)
# Staging shares the destination filesystem. A hard link gives
# POSIX/Windows create-if-absent semantics: unlike replace(), it
# cannot overwrite a response/session created concurrently.
_install_file_add_only(staged, target)
installed_files.append(target)
except Exception as error:
rollback_errors: list[str] = []
for target in reversed(installed_files):
try:
target.unlink()
except OSError as rollback_error:
rollback_errors.append(str(rollback_error))
if memory_activated or memory_original_moved:
try:
if destination_memory.exists() or _is_link_or_reparse(destination_memory):
_remove_path(destination_memory)
if memory_original_moved:
os.replace(previous_memory, destination_memory)
except OSError as rollback_error:
rollback_errors.append(str(rollback_error))
if rollback_errors:
raise OSError(
f"data import failed: {error}; rollback failed: "
+ "; ".join(rollback_errors)
) from error
raise
result: dict[str, Any] = {
"ok": True,
"memoryCopied": len(memory_files),
"memorySkipped": 0,
"responsesCopied": len(response_files),
"responsesSkipped": responses_skipped,
"sessionsAdded": len(session_files),
"sessionsSkipped": sessions_skipped,
"sessionsFileFound": sessions_found,
"backupDir": backup_dir,
"sourceType": inspection["sourceType"],
}
if session_preparer is not None:
result["_preparedSessions"] = prepared_sessions
return result
-100
View File
@@ -1,100 +0,0 @@
# GenericAgent Desktop 2.0 integration boundary
GenericAgent Desktop 2.0 is a packaged Tauri desktop application with a generated React renderer and
Desktop-specific Python services. The React source is intentionally developed outside this upstream tree;
the generated distribution and all runtime interfaces required to execute it are tracked here.
## Renderer ownership
- `frontends/desktop/dist/**` contains the generated Desktop 2.0 HTML, JavaScript, CSS, fonts, and images.
- `frontends/desktop/static/**` is the independent Desktop v1 implementation and remains available unchanged.
- Tauri packages `dist/**`; it does not compile React during an upstream build.
- The public React source, issue tracker, and contribution home is
<https://github.com/abraxas914/GenericAgent>.
- The exact fork commit and generated-file content manifest are recorded in
`dist/build-provenance.json`.
Generated files must not be edited by hand. Renderer changes are made and tested in the fork, rebuilt from an
accepted fork `main` commit, and submitted upstream as a refreshed `dist/**` tree with matching provenance.
The manifest identifies the submitted bytes; it is not a claim that later builds are bit-for-bit reproducible.
The production Tauri configuration loads only the tracked asset origin and applies an explicit Content Security
Policy. React source tooling is absent from the upstream package path, while WebDriver permission remains isolated
to `tauri.e2e.conf.json` and is not granted by the production configuration.
## Desktop runtime ownership
Desktop 2.0 uses these repository-owned integration layers:
- `frontends/desktop_bridge.py`: local HTTP/WebSocket API, model/session/upload/data routes, service control,
identity, and Desktop diagnostics.
- `frontends/conductor.py`: Desktop collaboration and model-routing integration.
- `frontends/cost_tracker.py`: Desktop usage and cost ledger.
- `frontends/data_backup.py`: validated Desktop data export, inspection, and import/merge behavior.
- `frontends/desktop_settings.py`: locked, atomic read/modify/write updates for shared Desktop preferences.
- `frontends/desktop/src-tauri/**`: native bootstrap, permissions, file pickers, runtime/source switching,
process/port recovery, window behavior, and package resource resolution.
The package-owned bridge remains the executable service boundary. `GA_ROOT` can point it at a compatible
external GenericAgent repository, but does not replace the package-owned bridge or Tauri shell.
Desktop data import first backs up the complete destination `memory/**` tree, then applies source-wins memory
files. Model responses remain add-only, and Desktop sessions are de-duplicated by session ID. Activation is staged
and rolled back on failure. Import is rejected while a managed session or Desktop extra has unfinished work; users
must also stop any independent TUI, CLI, or automation process before importing because those external processes
are outside the bridge's maintenance gate. The persistent cost ledger is initialized by the Desktop bridge only;
existing TUI and conductor processes retain their previous in-memory accounting hot path.
Browser-origin requests are accepted only from the production Tauri origins, the fixed development origin, the
explicit E2E origin, or the bridge's own same origin. Native clients without an Origin remain supported, while
cross-site, null, lookalike, wrong-port, preflight, and WebSocket requests are rejected before route handling.
Backup exports cannot be written into any managed data or HTTP-readable directory.
Managed-service state is based on the bridge-owned live child process, not merely a listener on the configured
port. A foreign conductor on production port `8900` is reported as an external port conflict and is never stopped
or counted as an owned running extra. The alternate conductor port is available only to the isolated package E2E
journey; production renderer and CSP contracts remain fixed to `8900`.
On macOS, the package runtime uses a stable writable root and is refreshed through a package/build/source marker
with staged atomic activation. A previously shipped versioned runtime is migrated into that stable root. User
configuration, memory, responses, sessions, and token ledger data are preserved; a failed migration or activation
restores the previous runtime and marker. Matching markers keep the hot-start copy untouched.
## Core runtime boundary
Desktop 2.0 does not own or modify GenericAgent's Agent/LLM/Harness business runtime. In particular, the
integration does not change `agentmain.py`, LLM execution, Harness/Agent execution, inference, tool-calling,
or memory-scheduling semantics. Desktop bridge and conductor changes translate Desktop requests onto those
existing core contracts.
## Version and release contract
Desktop package metadata is `0.2.1` across npm, Cargo, Tauri, and generated provenance. Upstream packages consume
the tracked `dist/**` tree, so an upstream maintainer can build official Windows, Linux, and macOS artifacts
without the React source. A `desktop-portable-*` tag starts three read-only platform builders. One separate
publisher receives write permission only after all three succeed, validates the six expected files and checksums,
creates an invisible draft with all assets, and then exposes that single entry as a prerelease. Manual
`workflow_dispatch` runs produce candidate artifacts only and skip the publisher.
The Linux builder runs on Ubuntu 22.04, isolates Rust target/bin caches from newer runner ABIs, and rejects any
ELF requiring a GLIBC version newer than 2.35. Windows remains unsigned and documents SmartScreen plus checksum
verification; macOS remains ad-hoc signed and unnotarized.
The renderer still uses the existing local loopback bridge contract. This integration neither treats that inherited
platform requirement as a new release blocker nor claims to have changed operating-system loopback policy.
## Validation layers
- Compiled distribution contract: required entries, asset references, source-map/source leakage, E2E markers,
version consistency, exact provenance manifest, and the `dist` package entry.
- Python contracts: bridge, conductor, model routing, sessions, uploads, cost ledger, `GA_ROOT`, and data backup.
- Rust contracts: formatting, clippy, production and E2E feature tests, and
path/identity/port/diagnostic/bootstrap behavior.
- Native package contract: Linux Tauri build smoke plus the platform release workflow.
Optional platform release qualification lives under `frontends/desktop/release_qualification/**`.
Its automated evidence gate validates package behavior and cleanup; screenshots and manual visual checklists
remain separate release-owner review material rather than CI pass/fail inputs.
The richer React unit/browser/native E2E suites remain in the fork and run before its accepted `main` SHA is
used to refresh the generated upstream distribution.
-66
View File
@@ -1,66 +0,0 @@
# GenericAgent React Desktop 2.0 — compiled distribution
This directory contains the v0.2.1 generated HTML, JavaScript, CSS, fonts, images, and license notices for the
GenericAgent React Desktop 2.0 renderer. It is build output, not the React development tree.
## Source and contributions
The public React source of truth and the place for renderer issues, pull requests, and design feedback is:
- <https://github.com/abraxas914/GenericAgent>
- Exact source used for this distribution:
[`4a67226e18cb003db1210357ea4a283774773acc`](https://github.com/abraxas914/GenericAgent/commit/4a67226e18cb003db1210357ea4a283774773acc)
Over the two-month React Desktop 2.0 development cycle, [abraxas914](https://github.com/abraxas914) led the new
renderer architecture and UI, Desktop bridge and Tauri integration, startup and recovery surfaces, package
validation, browser/native end-to-end coverage, renderer security hardening, and bundle optimization. Please
open an Issue or pull request in the fork above when proposing React renderer changes so source and generated
output stay synchronized.
[yiqi-017](https://github.com/yiqi-017) contributed user-facing help and feedback contacts that remain in the
compiled renderer, including the
[`help and feedback` settings work](https://github.com/abraxas914/GenericAgent/commit/5ddf03bb152666637bdfcfa44f1fac3cff5a66b6)
and [`startup recovery` support contacts](https://github.com/abraxas914/GenericAgent/commit/2fb55d944e4444b08cb9ad76c13aef7a5788186b).
He also produced an earlier
[`compiled-only delivery` prototype](https://github.com/abraxas914/GenericAgent/commit/3e7ca6a2b20eefdb3ee335dc0d520c3b6d9d57f8)
that helped establish this upstream packaging boundary. The current generated tree was rebuilt by abraxas914
from the exact merged source commit recorded above.
## Third-party software: Semi Design
This distribution contains code from these nine Semi Design packages, all resolved at version `2.101.0`:
- `@douyinfe/semi-animation`
- `@douyinfe/semi-animation-react`
- `@douyinfe/semi-animation-styled`
- `@douyinfe/semi-foundation`
- `@douyinfe/semi-icons`
- `@douyinfe/semi-illustrations`
- `@douyinfe/semi-json-viewer-core`
- `@douyinfe/semi-theme-default`
- `@douyinfe/semi-ui`
- Project: <https://github.com/DouyinFE/semi-design>
- Website: <https://semi.design>
- Copyright (c) 2021 DouyinFE
- License: MIT
The complete license text and bundled third-party notices are in
[`THIRD_PARTY_NOTICES.txt`](./THIRD_PARTY_NOTICES.txt) in this directory.
## Repository boundary
- `frontends/desktop/dist/**` is the generated React Desktop 2.0 renderer used by the packaged Tauri app.
- `frontends/desktop/static/**` remains the independent legacy Desktop v1 implementation; this distribution
neither replaces nor modifies its source files.
- The complete Desktop 2.0 integration also uses the repository's Desktop-specific bridge, conductor, cost
tracking, data-backup, bootstrap, permissions, and Tauri shell code.
- The integration does **not** modify GenericAgent's Agent, LLM, Harness, inference, tool-calling, or memory
scheduling core runtime.
- `build-provenance.json` records the source repository, exact source commit, generated file count, and a
content-manifest SHA-256. This README and the provenance file are excluded from that manifest. The manifest
identifies this exact generated tree; it is not a claim that later builds are bit-for-bit reproducible.
Upstream maintainers can package the tracked distribution directly without the React source. React changes
should be built and validated in the fork, then submitted here as a refreshed generated tree with updated
provenance and matching Desktop integration contracts.
-83
View File
@@ -1,83 +0,0 @@
MIT License
Copyright (c) 2021 DouyinFE
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The code implementation of the external library is referenced by DouyinFe are:
- animate.css
The MIT License (MIT)
Copyright (c) 2020 Daniel Eden
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- Ant Design
MIT LICENSE
Copyright (c) 2015-present Ant UED, https://xtech.antfin.com/
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- rc-tree
MIT LICENSE
Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-1
View File
@@ -1 +0,0 @@
import{g as q}from"./tauri-compat-D_GD_wMe.js";var x,P;function Q(){if(P)return x;P=1;var i=4,t=.001,e=1e-7,s=10,n=11,a=1/(n-1),c=typeof Float32Array=="function";function l(u,o){return 1-3*o+3*u}function g(u,o){return 3*o-6*u}function T(u){return 3*u}function r(u,o,f){return((l(o,f)*u+g(o,f))*u+T(o))*u}function h(u,o,f){return 3*l(o,f)*u*u+2*g(o,f)*u+T(o)}function C(u,o,f,p,y){var m,_,F=0;do _=o+(f-o)/2,m=r(_,p,y)-u,m>0?f=_:o=_;while(Math.abs(m)>e&&++F<s);return _}function b(u,o,f,p){for(var y=0;y<i;++y){var m=h(o,f,p);if(m===0)return o;var _=r(o,f,p)-u;o-=_/m}return o}function d(u){return u}return x=function(o,f,p,y){if(!(0<=o&&o<=1&&0<=p&&p<=1))throw new Error("bezier x values must be in [0, 1] range");if(o===f&&p===y)return d;for(var m=c?new Float32Array(n):new Array(n),_=0;_<n;++_)m[_]=r(_*a,o,p);function F(v){for(var S=0,j=1,R=n-1;j!==R&&m[j]<=v;++j)S+=a;--j;var U=(v-m[j])/(m[j+1]-m[j]),A=S+U*a,k=h(A,o,p);return k>=t?b(v,A,o,p):k===0?A:C(v,S,S+a,o,p)}return function(S){return S===0?0:S===1?1:r(F(S),f,y)}},x}var K=Q();const z=q(K);function B(i,t,e){return Math.min(Math.max(i,t),e)}function W(i){const t=/\(([^)]+)\)/.exec(i);return t?t[1].split(",").map(e=>parseFloat(e)):[]}function V(){let i=arguments.length>0&&arguments[0]!==void 0?arguments[0]:1,t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:.5;const e=B(i,1,10),s=B(t,.1,2);return n=>n===0||n===1?n:-e*Math.pow(2,10*(n-1))*Math.sin((n-1-s/(Math.PI*2)*Math.asin(1/e))*(Math.PI*2)/s)}const N=(()=>{const i=["Quad","Cubic","Quart","Quint","Sine","Expo","Circ","Back","Elastic"],t={In:[[.55,.085,.68,.53],[.55,.055,.675,.19],[.895,.03,.685,.22],[.755,.05,.855,.06],[.47,0,.745,.715],[.95,.05,.795,.035],[.6,.04,.98,.335],[.6,-.28,.735,.045],V],Out:[[.25,.46,.45,.94],[.215,.61,.355,1],[.165,.84,.44,1],[.23,1,.32,1],[.39,.575,.565,1],[.19,1,.22,1],[.075,.82,.165,1],[.175,.885,.32,1.275],(s,n)=>a=>1-V(s,n)(1-a)],InOut:[[.455,.03,.515,.955],[.645,.045,.355,1],[.77,0,.175,1],[.86,0,.07,1],[.445,.05,.55,.95],[1,0,0,1],[.785,.135,.15,.86],[.68,-.55,.265,1.55],(s,n)=>a=>a<.5?V(s,n)(a*2)/2:1-V(s,n)(a*-2+2)/2]},e={linear:[.25,.25,.75,.75]};for(const s of Object.keys(t))t[s].forEach((n,a)=>{e["ease"+s+i[a]]=n});return e})();function D(i){if(typeof i=="function")return i;!i||typeof i!="string"?i="linear":i=i.trim();let t=i.split("(")[0];const e=W(i);let s;return t==="cubic-bezier"||t==="cubicBezier"?z(...e.length?e:N.linear):((!t||typeof t!="string"||typeof t=="string"&&N[t]==null)&&(t="linear"),s=N[t],typeof s=="function"?s(...e):e.length?z(...e):z(...s))}class Z{constructor(){this._eventMap=new Map}on(t,e){return t&&typeof e=="function"&&(this._eventMap.has(t)||this._eventMap.set(t,[]),this._eventMap.get(t).push(e)),this}once(t,e){var s=this;if(t&&typeof e=="function"){const n=function(){e(...arguments),s.off(t,n)};this.on(t,n)}}off(t,e){if(t)if(typeof e=="function"){const s=this._eventMap.get(t);if(Array.isArray(s)&&s.length){let n=-1;for(;(n=s.findIndex(a=>a===e))>-1;)s.splice(n,1)}}else e==null&&this._eventMap.delete(t);return this}emit(t){for(var e=arguments.length,s=new Array(e>1?e-1:0),n=1;n<e;n++)s[n-1]=arguments[n];return this._eventMap.has(t)?(this._eventMap.get(t).forEach(a=>a(...s)),!0):!1}}function M(i){return!!(i&&typeof i=="object"&&(i.duration>0||typeof i.easing=="string"||typeof i.easing=="function"))}function H(i,t,e,s,n){for(const a of Object.keys(t)){const c=t[a],l=typeof c=="number"?c:c.val;if(!(typeof c=="object"&&c.done)){if(M(c)&&s&&n&&c.duration){if(c.duration+s<=n||l!==i[a])return!1}else if(typeof e[a]=="number"&&e[a]!==0)return!1;if(i[a]!==l)return!1}}return!0}function E(i){const t={};for(const e in i)Object.prototype.hasOwnProperty.call(i,e)&&(t[e]=typeof i[e]=="number"?i[e]:i[e].val);return t}const I=[0,0];function L(i,t,e,s,n,a,c){const l=-n*(t-s),g=-a*e,T=l+g,r=e+T*i,h=t+r*i;return Math.abs(r)<c&&Math.abs(h-s)<c?(I[0]=s,I[1]=0,I):(I[0]=h,I[1]=r,I)}function J(i){const t={},e=i&&Object.keys(i)||[];for(const s of e)t[s]=0;return t}const $={default:{tension:170,friction:26}},Y=Object.assign(Object.assign({},$.default),{precision:.01});function G(i){let t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};if(M(t)){const s=D(t.easing),n=typeof t.duration=="number"&&t.duration>0?t.duration:1e3;t=Object.assign(Object.assign({},t),{easing:s,duration:n})}let e=Object.assign(Object.assign(Object.assign({},Y),t),{done:!1});if(i&&typeof i=="object"&&"val"in i){if(M(i)){const s=D(i.easing),n=typeof i.duration=="number"&&i.duration>0?i.duration:parseInt(t.duration)||1e3;i=Object.assign(Object.assign({},i),{easing:s,duration:n})}e=Object.assign(Object.assign({},e),i)}else e=Object.assign(Object.assign({},e),{val:i});return e}const w=()=>Date.now(),O=1e3/60;class tt extends Z{constructor(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};super(),this._props=Object.assign({},t),this._config=Object.assign({},e),this.initStates()}_wrapConfig(t,e){e=e&&typeof e=="object"?Line truncated
-1
View File
@@ -1 +0,0 @@
.collab-page{display:flex;width:100%;height:100%;overflow:hidden}.collab-main{flex:1;display:flex;flex-direction:column;min-width:0;position:relative}.collab-chat-area{flex:1;position:relative;min-height:0;overflow:hidden}.collab-offline{display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;height:100%;gap:.5rem;color:var(--semi-color-text-2, #4e5969)}.collab-offline-icon{font-size:2rem;opacity:.5}.collab-offline h3{margin:0;font-size:1rem;font-weight:600;color:var(--semi-color-text-0, #1c1f23)}.collab-offline p{margin:0;font-size:.8125rem}.collab-retry-btn{margin-top:.75rem;padding:.375rem 1rem;border:1px solid var(--semi-color-border, #e0e0e0);border-radius:.375rem;background:var(--semi-color-bg-0, #fff);color:var(--semi-color-text-0, #1c1f23);font-size:.8125rem;cursor:pointer;transition:background .12s}.collab-retry-btn:hover{background:var(--semi-color-fill-0, #f5f5f5)}.collab-connecting{display:flex;align-items:center;justify-content:center;height:100%;gap:.5rem;font-size:.8125rem;color:var(--semi-color-text-2, #4e5969);position:absolute;top:0;right:0;bottom:0;left:0}.collab-connecting-dot{width:8px;height:8px;border-radius:50%;background:var(--semi-color-primary, #3370ff);animation:statusPulse 1.2s ease-in-out infinite}.collab-empty{display:flex;align-items:center;justify-content:center;height:100%;color:var(--semi-color-text-3, #8f959e);font-size:.8125rem;position:absolute;top:0;right:0;bottom:0;left:0}.collab-welcome{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:flex-start;justify-content:center;padding:2rem 0;gap:1rem}.collab-welcome-title{font-size:1.125rem;font-weight:600;color:var(--semi-color-text-0, #1c1f23);margin:0}.collab-welcome-sub{font-size:.8125rem;color:var(--semi-color-text-2, #4e5969);line-height:1.5;margin:0}.collab-welcome-steps{display:flex;flex-direction:column;gap:.75rem;width:100%;margin-top:.5rem}.collab-welcome-step{display:flex;align-items:flex-start;gap:.625rem}.collab-welcome-step-icon{display:flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;border-radius:.375rem;background:var(--semi-color-fill-0, #f5f5f5);color:var(--semi-color-text-1, #1c1f23);flex-shrink:0}.collab-welcome-step-text{display:flex;flex-direction:column;gap:.125rem;padding-top:.1875rem}.collab-welcome-step-label{font-size:.8125rem;font-weight:600;color:var(--semi-color-text-0, #1c1f23)}.collab-welcome-step-desc{font-size:.75rem;color:var(--semi-color-text-2, #4e5969);line-height:1.4}.collab-welcome-chips{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.5rem}.collab-welcome-chip{display:inline-flex;align-items:center;padding:.375rem .625rem;border:1px solid var(--semi-color-border, #e0e0e0);border-radius:6px;background:none;color:var(--ui-text-secondary, #4e5969);font-size:.8125rem;font-weight:500;line-height:1;cursor:pointer;-webkit-user-select:none;user-select:none;transition:background .15s,box-shadow .15s}.collab-welcome-chip:hover{background:color-mix(in srgb,var(--semi-color-primary, #3370ff) 4%,transparent);box-shadow:0 1px 4px color-mix(in srgb,var(--semi-color-primary, #3370ff) 12%,transparent)}.collab-welcome-chip:active{background:color-mix(in srgb,var(--semi-color-primary, #3370ff) 8%,transparent)}[data-slot=collab-page][data-empty] .collab-chat-area{display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:visible}[data-slot=collab-page][data-empty] .collab-welcome{position:relative;inset:unset;width:min(var(--composer-width, 48.75rem),calc(100% - 2rem))}[data-slot=collab-page][data-empty] .collab-composer-wrap{position:relative;width:min(var(--composer-width, 48.75rem),calc(100% - 2rem))}[data-slot=collab-page][data-empty] [data-slot=composer-root]{position:relative;bottom:auto;left:auto;transform:none;width:100%}[data-slot=collab-page][data-empty] [data-slot=composer-root]:before{display:none}[data-slot=collab-page][data-empty] [data-slot=composer-surface]{border-color:var(--semi-color-primary)!important;box-shadow:0 0 20px color-mix(in srgb,var(--semi-color-primary) 12%,transparent),0 0 50px -4px color-mix(in srgb,var(--semi-color-primary) 8%,transparent)!important}[data-slot=collab-messages]{position:absolute;top:0;right:0;bottom:0;left:0;overflow-y:auto;padding:1rem;padding-bottom:calc(var(--composer-measured-height, 62px) + 1rem)}.collab-messages-scroll{display:flex;flex-direction:column;gap:.75rem;max-width:48rem;margin:0 auto}.collab-msg{display:flex}.collab-msg--user{justify-content:flex-end}.collab-msg--conductor,.collab-msg--system{justify-content:flex-start}.collab-bubble{max-width:80%;min-width:min(24rem,100%);padding:.5rem .75rem;border-radius:.75rem;font-size:.8125rem;line-height:1.5}.collab-bubble--user{background:#d3e0fd;color:#000;border-bottom-right-radius:.25rem;min-width:unset;word-break:break-word;white-space:pre-wrap}.collab-bubble--conductor{background:var(--semi-color-fill-0, #f5f5f5);color:var(--semi-color-text-0, #1c1f23);border-bLine truncated
-1
View File
@@ -1 +0,0 @@
import{b as r,aj as l}from"./tauri-compat-D_GD_wMe.js";import{H as i,b as N,J as C,L as S,K as M,N as T,O as W,P as v,M as R,Q as E}from"./main-j7e_THPB.js";import{MarkdownPart as w}from"./MarkdownPart-ZJKeOipX.js";import"./index-9FI8Jlf5.js";import"./WindowsTitlebar-Byi-SYR_.js";import"./markdown-preprocess-Cc2i34eF.js";const $=200;function D({message:a}){const n=a.role==="user",e=a.role==="conductor";return l.jsx("div",{className:`collab-msg collab-msg--${a.role}`,"data-slot":"collab-msg",children:l.jsxs("div",{className:`collab-bubble ${n?"collab-bubble--user":e?"collab-bubble--conductor":"collab-bubble--system"}`,children:[a.images&&a.images.length>0&&l.jsx("div",{className:"collab-msg-images",children:a.images.map((c,s)=>l.jsx("img",{src:c.base64||c.path,alt:c.name,className:"collab-msg-img"},s))}),a.files&&a.files.length>0&&l.jsx("div",{className:"collab-msg-files",children:a.files.map((c,s)=>l.jsx("span",{className:"collab-msg-file-chip",children:c.name},s))}),e||a.role==="system"?l.jsx(w,{content:a.msg}):l.jsx("span",{className:"collab-msg-text",children:a.msg})]})})}function P({since:a}){const{t:n}=N();return l.jsx("div",{className:"collab-msg collab-msg--conductor","data-slot":"collab-typing",children:l.jsxs("div",{className:"collab-thinking-bar",children:[l.jsx("span",{className:"collab-thinking-dot"}),l.jsx("span",{className:"collab-thinking-label",children:n("collab.typing")}),l.jsx("span",{className:"collab-thinking-time",children:l.jsx(S,{since:a})})]})})}function B(){const a=i(o=>o.messages),n=i(o=>o.conductorTyping),e=i(o=>o.connectionStatus),{t:c}=N(),{scrollRef:s}=C(),[m,x]=r.useState(1),d=r.useRef(null),p=r.useRef(0);r.useEffect(()=>{(e==="connecting"||a.length===0)&&x(1)},[e,a.length]);const b=$*m,u=Math.max(0,a.length-b),k=a.slice(u),t=u,g=r.useCallback(()=>{const o=s.current;o&&(d.current=o.scrollHeight-o.scrollTop),x(h=>h+1)},[s]);r.useLayoutEffect(()=>{if(d.current==null)return;if(u>=p.current){d.current=null,p.current=u;return}const o=s.current;o&&(o.scrollTop=o.scrollHeight-d.current),d.current=null,p.current=u},[u,s]),r.useEffect(()=>{p.current=u},[u]);const j=[...a].reverse().find(o=>o.role==="user"),y=o=>o>1e12?o:o*1e3,f=r.useRef(Date.now());return j!=null&&j.ts&&(f.current=y(j.ts)),e==="connecting"?l.jsx("div",{className:"collab-messages-area","data-slot":"collab-messages",children:l.jsxs("div",{className:"collab-connecting",children:[l.jsx("span",{className:"collab-connecting-dot"}),l.jsx("span",{children:c("status.connecting")})]})}):a.length===0?l.jsx("div",{className:"collab-messages-area","data-slot":"collab-messages",children:l.jsx("div",{className:"collab-empty",children:l.jsx("p",{children:c("collab.placeholder")})})}):l.jsx("div",{className:"collab-messages-area","data-slot":"collab-messages",ref:s,children:l.jsxs("div",{className:"collab-messages-scroll",children:[t>0&&l.jsxs("button",{type:"button","data-slot":"show-earlier-btn",onClick:g,children:["Show ",t," earlier"]}),k.map((o,h)=>l.jsx(D,{message:o},o.id||u+h)),n&&l.jsx(P,{since:f.current})]})})}function H(){const a=i(t=>t.sendMessage),n=i(t=>t.conductorTyping),e=i(t=>t.connectionStatus),c=i(t=>t.modelConfig),s=i(t=>t.runtimeModel),m=i(t=>t.loadModel),x=i(t=>t.selectModel),d=M(t=>t.defaultModelNo);r.useEffect(()=>{m()},[m]);const p=r.useCallback((t,g)=>{var f,o;const j=(f=g==null?void 0:g.files)==null?void 0:f.map(h=>({name:h.name,path:h.path})),y=(o=g==null?void 0:g.images)==null?void 0:o.map(h=>({name:h.name,path:h.path,base64:h.base64}));a(t,j,y)},[a]),b=r.useCallback(()=>{},[]),u=e!=="ready",k=(c==null?void 0:c.effective)??(c==null?void 0:c.configured)??d;return l.jsx("div",{className:"collab-composer-wrap","data-slot":"collab-composer","data-disabled":u||void 0,children:l.jsx(T,{onSend:p,onStop:b,isGenerating:n,hideStatusStack:!0,modelControl:l.jsx(W,{selectedNo:k,runningNo:s!=null&&s.running?s.effective:null,isRunning:!!(s!=null&&s.running),onSelect:x})})})}const L=[{key:"1",icon:"comment"},{key:"2",icon:"symbol-misc"},{key:"3",icon:"graph"},{key:"4",icon:"edit"}],F=["collab.chipProgress","collab.chipPause","collab.chipSummary"];function I({onChipClick:a}){const{t:n}=N();return l.jsxs("div",{className:"collab-welcome","data-slot":"collab-welcome",children:[l.jsx("h2",{className:"collab-welcome-title",children:n("collab.guideTitle")}),l.jsx("p",{className:"collab-welcome-sub",children:n("collab.guideWhen")}),l.jsx("div",{className:"collab-welcome-steps",children:L.map(e=>l.jsxs("div",{className:"collab-welcome-step",children:[l.jsx("span",{className:"collab-welcome-step-icon",children:l.jsx(v,{name:e.icon,size:"1rem"})}),l.jsxs("span",{className:"collab-welcome-step-text",children:[l.jsx("span",{className:"collab-welcome-step-label",children:n(`collab.guideStep${e.key}t`)}),l.jsx("span",{className:"collab-welcome-step-desc",children:n(`collab.guideStep${e.key}d`)})]})]},e.key))}),l.jsx("div",{className:"collab-welcome-chips",children:F.map(e=>l.jsx("button",{type:"button",className:"collab-welcome-chip",oLine truncated
-1
View File
@@ -1 +0,0 @@
import{c as t,b as e}from"./tauri-compat-D_GD_wMe.js";function n(a){return e.createElement("svg",Object.assign({viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",focusable:!1,"aria-hidden":!0},a),e.createElement("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M12 2a1 1 0 0 0-1-1H6a2 2 0 0 0-2 2v18c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V10a1 1 0 0 0-1-1h-5a2 2 0 0 1-2-2V2ZM7 8a1 1 0 0 1 1-1h1a1 1 0 0 1 0 2H8a1 1 0 0 1-1-1Zm0 5a1 1 0 0 1 1-1h8a1 1 0 1 1 0 2H8a1 1 0 0 1-1-1Zm1 4a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2H8Zm11.07-9.5H14.5a1 1 0 0 1-1-1V1.93a.8.8 0 0 1 1.37-.56l4.76 4.76a.8.8 0 0 1-.56 1.37Z",fill:"currentColor"}))}const l=t(n,"file");export{l as I};
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 252 files, more files were not shown because too many files have changed in this diff. Show more