Run the bundle's own bridge + frontend but drive an external GenericAgent
核 injected via GA_ROOT, instead of swapping to the external bridge or
copying files into 本体. New UI + external核/data, no mutation, logs
naturally unified with the核's other frontends.
- desktop_bridge.py: find_default_ga_root() honors --ga-root / GA_ROOT
(validated), else derives as before; conductor now spawned from the
BUNDLE (APP_DIR/conductor.py) with GA_ROOT in env; scheduler stays核-side
- conductor.py: ROOT prefers GA_ROOT env, else own-path derivation
- lib.rs: override no longer changes project_dir (always bundle bridge);
sanitize_bundle_env injects GA_ROOT; takeover compares effective ga_root;
set_ga_source drops file-copy sync and instead runs a contract probe
before committing; removed copy_dir_all/sync_desktop_files
- ga_contract_probe.py (new): subprocess, introspect-only (no GenericAgent
instantiation) symbol/signature check of the核's bridge/conductor/
cost_tracker contract; doubles as a dependency check
- GA桌面壳-参考方案.md: plan + feasibility review + probe checklist
Every message called _persist(), which rewrote the ENTIRE
desktop_sessions.json (all sessions, full messages + llm_history) under
the lock. Write cost was O(all sessions), so performance degraded badly
as sessions piled up.
Now each session is its own temp/desktop_sessions/<id>.json:
- _persist_session(s): atomic per-session write, O(one session)
- _delete_session_file(sid): remove on delete
- add_message / create / plan-bind / set_model / patch now write only
the touched session; write cost no longer scales with session count
- _load_sessions reads the dir; one-time migration from the legacy
monolithic file, which is then renamed to .json.migrated
- import_sessions reads both source formats (per-file dir + legacy),
persists only newly added sessions
Import now also merges the source's temp/desktop_sessions.json into the
current session list (dedup by id), updating both memory and the on-disk
file so past desktop chats reappear in the sidebar immediately.
- manager.import_sessions(): id-dedup merge, in-memory + persist
- import handler returns sessionsAdded/sessionsSkipped
- frontend shows session count in toast and reloads the sidebar
Lets users import memory/ and temp/model_responses/ from an existing
GenericAgent install into the desktop app's ga_root, so past sessions
and memory are not lost when running the standalone desktop bundle.
- bridge: POST /memory/import copies memory (backup-then-overwrite) and
model_responses (fill-missing), returns counts + backup dir
- tauri: pick_directory command (rfd native folder picker)
- frontend: "Import memory" button in Settings > Features + zh/en i18n
conductor and scheduler are separate subprocesses that read mykey once at
startup. After a mykey rewrite (import keys / edit channel config), a healthy
running conductor already hot-reloads mykey on its next task via its own mtime
check, so it needs no restart — restarting it would only interrupt an in-flight
task.
So restart an extra only when it is genuinely broken: the process exited
unexpectedly (covers scheduler dying on a bad config), or the process is alive
but its "Exception in thread conductor-agent" crash signature is in the captured
log (uvicorn still up but the agent thread is dead and can no longer process
tasks). A fresh log buffer per start means the signature only reflects the
current process. Bridge-only; conductor core and agentmain untouched.
Remove the implicit model-write that rode along with each prompt. The
frontend no longer sends llmNo on session/prompt, and the bridge no longer
reads/forwards it there — sending a message now uses only the session's
bound sess.llm_no (falling back to the global default). Model switching
goes through the single POST /session/{sid}/model entry point.
selectModel now rolls back the chip + shows a toast when the switch request
fails, so the frontend display never diverges from the backend.
Session.llm_no is now the authoritative per-session model selection
(persisted to desktop_sessions.json). Frontend state.llmNo becomes a
display cache; switching a session re-displays its bound model, and the
chip follows the backend's live model for native models too (not just
mixin failover).
- bridge: Session.llm_no field + persistence; submit_prompt/run_agent_turn
read sess.llm_no with global ui.llmNo as fallback; make_agent/restore
seed from it; new POST /session/{sid}/model; _live_model reports llmNo
- app.js: applyLiveModel supports native; setActiveSession re-displays
bound model; selectModel posts to session route + updates global default;
remove ga_llm_no localStorage
- conductor unchanged: keeps reading global default ui.llmNo
The prior merge resolved conflicts to the desktop/main (graft) side,
which silently reverted lsdefine's recent upstream fix (inMixin check,
ask_user schema, turn-25 prompt). Restore the 4 files to origin/main.
list_panel_state now emits bridge → extras (conductor + scheduler) → IM,
giving conductor a friendly 指挥家 / Conductor label and grouping system
services together at the top of the panel.
Keep test #13 bridge behavior; add start-extras/stop-extras endpoints and
wire desktop open/close to them. Show a native alert when conductor or
scheduler ports are already in use.
Conductor used to be a separately-launched service that opens its own UI
in the user's browser. With the desktop app, users access conductor via
the built-in 「指挥家」 page connecting to port 8900, so the standalone UI
window is redundant and confusing.
- conductor.py: gain a --no-browser flag that skips the webbrowser.open
call. Default behavior (no flag) unchanged for backward compat with
start_conductor.ps1 / GAConductor scheduled task / manual python runs.
- desktop_bridge.py discover_extra_services: also enumerate conductor.py
alongside scheduler.py, with --no-browser baked into the spawn cmd.
autostart_extras then picks it up automatically on bridge boot.
The reflect/scheduler.py service drives the L4 archive cron every 12h,
but was only registered in the catalog and never auto-started. The UI
panel also wasn't surfacing it (WS snapshot filters to IM only), so
there was effectively no user-facing way to turn L4 archiving on.
Net effect: every GA deployment (local Tauri, Squishy, kw, etc.) had
L4_raw_sessions/ permanently empty. compress_session.py never ran.
Fix: on bridge startup, autostart everything in _catalog that isn't an
IM service. Currently that's just reflect/scheduler.py. IM services stay
manual because they need mykey.py config + explicit user opt-in.
After this lands:
- bridge boots -> scheduler subprocess spawned
- ~2 min later -> first L4 archive runs (since _l4_t starts at 0)
- every 12h thereafter -> next L4 archive cycle
- users get L4 with zero config
Editing a model key / adding a profile wrote mykey.py but the running session
agents kept using the LLM sessions built at startup (old key), so the change
only took effect after a restart.
Root cause: llmcore tracks "changed since last load" via a single global mtime
flag, and load_llm_sessions() skips rebuilding when changed=False. Whichever
agent reloads first consumes that flag — e.g. the throwaway GenericAgent the UI
spins up to render the model list — so the real persistent session agent later
sees changed=False and keeps its stale sessions.
After saving a profile, force every live session agent to rebuild: reset
llmcore._mykey_mtime to None before each agent.load_llm_sessions() so
reload_mykeys() reports changed=True. Bridge-only (same trick as the existing
_invalidate_mykey_cache); no core changes to agentmain/llmcore; the rebuild
preserves conversation history via agentmain's oldhistory hand-off.
Uploads now land in temp/desktop_uploads/<sid>/ instead of a flat dir, so a
session's attachments can be cleaned up when the session is deleted.
- upload_handler reads `sid` from the body and saves under desktop_uploads/<sid>/
(sanitized; missing sid -> _misc bucket).
- delete_session purges the session's upload subdir.
- _sweep_stale_uploads recurses into sid subdirs (rglob) and drops empty dirs.
- Frontend: uploadOne sends the active session's bridgeSessionId; addFiles
ensures a session exists (reusing newSession/ensureBridgeSession) before
uploading, so a sid is available — the empty-app case creates one first.
Whitelist checks (upload_raw / upload_delete) already validate "under root",
so the deeper subdir paths pass unchanged.
The 05-20 image mechanism (_save_image_data -> desktop_image_uploads, images
sent as dataUrl in the prompt) was superseded on 05-21 by the placeholder-path
flow: the frontend uploads every attachment via /upload to desktop_uploads/ and
inlines the file path into the prompt text, sending only path metadata
(files/imageMetas) — never dataUrl. So _save_image_data was never called and
desktop_image_uploads stayed empty (dead code).
- Remove _save_image_data and the _UPLOAD_DIR (desktop_image_uploads) dir.
- normalize_prompt now only flattens prompt text; image handling dropped
(images arg kept for backward compat, ignored).
- _sweep_stale_uploads now sweeps only desktop_uploads.
Net: images and files share one dir (desktop_uploads), no duplicate storage.
Uploaded files were stored in the OS temp dir, and the attachment dir was
wiped wholesale on every bridge startup (shutil.rmtree) — so attachments in
past sessions became dead links after any restart (e.g. "File not found:
...\ga_web_uploads\...__thesis.pdf").
- Move both _UPLOAD_DIR (images) and _WEB_UPLOAD_DIR (attachments) under
ga_root/temp/ (gitignored): desktop_image_uploads/ and desktop_uploads/.
- Drop the rmtree-on-startup; uploads now survive bridge restarts.
- Add _sweep_stale_uploads(): on startup, delete only files older than
UPLOAD_RETENTION_DAYS (30) by mtime, so temp storage can't grow unbounded.
- Remove now-unused tempfile/shutil imports.
Three fixes for the chat file-attachment flow:
1. Attachments vanished on refresh — sendPrompt now sends files/imageMetas
(name+path) to the bridge, submit_prompt persists them on the user message,
and msgNode renders images via GET /upload/raw URL instead of the in-memory
dataUrl. So after a page refresh (bridge still running) file cards and image
thumbnails come back. (Hard limit: restarting the bridge rmtree's
ga_web_uploads, so a restart still drops the files.)
2. Remote can't open files — opening a file runs os.startfile/xdg-open on the
BRIDGE machine, useless when the browser is on another box. openUploadFile
now detects non-localhost and downloads the file to the client instead
(GET /upload/raw?download=1). Remote image preview works too (lightbox uses
the inline /upload/raw URL).
3. Windows opened pdf/docx in Notepad — PR #12's open path used
_open_path_in_editor ('edit' verb), which falls back to Notepad for types
with no edit verb (pdf, etc.). Uploads now use _open_path_default (default
'open' verb → registered default app). mykey still uses the editor path.
New bridge endpoint: GET /upload/raw?path=&download= — whitelisted to
_WEB_UPLOAD_DIR, inline preview or attachment download, restores original
filename. Serves both the refresh-preview and remote-download cases.
Click on a file attachment now picks behavior by extension: previewable
types (PDF, images, text, code, Office docs, audio/video) open in the
system default app; everything else (zip, binaries, unknown) reveals the
file in Finder/Explorer instead of opening it directly — avoids garbled
output for non-textual formats.
- frontend: PREVIEWABLE_EXTS whitelist + isPreviewableByName()
- bridge: /path/open accepts mode='reveal', uses 'open -R' (macOS),
'explorer /select,' (Windows), parent-dir xdg-open (Linux fallback)
- pending thumb-strip chips also pass data-name for the same routing
Co-authored-by: Yiwei Gu <yiwei.gu@sap.com>
feat(desktop): persist token usage history to local file instead of localStorage
Token stats (history + snapshot) now stored in temp/desktop_token_history.json
via GET/POST /token-history endpoints, ensuring cross-browser consistency.
@
>> move session persistence from localStorage to backend API
>>
>> Sessions are now loaded from GET /sessions and metadata updates (pin,
>> title) go through PATCH /session/{sid}, eliminating cross-browser
>> isolation issues between Chrome and Safari.
Non-image attachments now render as Gemini/ChatGPT-style file cards above
the user text bubble (icon disc + bold filename + kind subtitle). Click on
a file card opens it via the system default app — bridge `/path/open` gains
a `kind=upload` mode that whitelists \`_WEB_UPLOAD_DIR\` to prevent the
frontend from opening arbitrary local paths.
Composer thumb-strip uses the same card style for non-image pending files,
also click-to-open. Image thumbs (48×48) unchanged.
Co-authored-by: Yiwei Gu <yiwei.gu@sap.com>
Settings gains a 10–20px chat font stepper (default 14, slider with aria),
applied only to chat bubbles + composer via --chat-font; pre/code/fold switch
to em-relative sizing so they scale with it. Dark mode swaps highlight.js to
the github-dark theme and adds link/code/pre/katex/table/blockquote styling so
markdown is legible on dark backgrounds.
Persistence rides the same ui-prefs mechanism as #7 (ui.fontSize in
~/.ga_desktop_settings.json via /config), so this PR supersedes #7 — it
contains the full server-first persistence layer (bootUiFromDom / persistUiPrefs
/ syncBootCache) plus fontSize. Keeps #5's interrupt-on-resend behavior intact.
Closes#9
Co-Authored-By: dd3xp <zhangjingming131@gmail.com>
Root cause of "uploads silently land as 0-byte __file": aiohttp default body
limit is 1 MiB; PDFs almost always exceed it, request.json() raises
HTTPRequestEntityTooLarge, the generic read_json() swallows it as bare
Exception, upload_handler then sees an empty dict and happily writes an
empty file with name='file', returning ok=true. Front-end thinks the
upload worked.
Bridge:
- Set web.Application(client_max_size=1MB) explicitly so the limit is
visible at the call site (kept at 1MB intentionally for now; bump later
if larger uploads are needed)
- upload_handler reads body itself instead of read_json so it can catch
HTTPRequestEntityTooLarge and HTTPBadRequest and return a real error
to the client instead of silently writing 0 bytes
- Reject blob == b'' as a second line of defence
Frontend:
- addFiles skips files with size === 0 (handles drag-folder ghost File
and edge-case clipboard items) with a dedicated 'upload.empty' toast
- 4x showSystem(...) in addFiles -> showChanToast(title, detail, 'err').
Upload failures should be transient toasts, not permanent system
messages glued into the chat transcript.
- Bridge cancel() saves partial assistant content as a stopped message so the truncated output survives a refresh
- submit_prompt accepts a separate display field; UI now shows the original user input while the bridge stores the composed Plan/Auto SOP + expanded file paths for LLM/restore_context
- Boot-time chat-start is hidden via early data-boot-has-sessions attribute when localStorage has sessions, removing the welcome-page flash on refresh
- Conv-item click on non-chat pages now jumps back to the chat page with that session selected
- Remove dead refresh button from topbar (no handler, no id)