Commit Graph
57 Commits
Author SHA1 Message Date
dd3xp 82d115158c Merge main into RoundSquisheen: catch up team's commits (llm_history persist, cache rate fix) 2026-06-06 22:48:14 +08:00
dd3xp f6b594dc71 fix(reflect/scheduler): switch to lsdefine's mkdir fix (drop my variant)
lsdefine/main b365945 had independently landed the same makedirs fix

I added in ac2779d, but at module top-level and creating DONE/ instead

of dirname(_LOG).

Functionally equivalent (both unblock fresh-deploy scheduler import), but

to keep this fork aligned with upstream we adopt lsdefine's version

verbatim. After future content-overlay syncs from upstream, no diff in

this file.
2026-06-06 17:39:42 +08:00
dd3xp ccc7a0d44b fix(reflect/scheduler): switch to lsdefine's mkdir fix (drop my variant)
lsdefine/main b365945 had independently landed the same makedirs fix

I added in ac2779d, but at module top-level and creating DONE/ instead

of dirname(_LOG).

Functionally equivalent (both unblock fresh-deploy scheduler import), but

to keep this fork aligned with upstream we adopt lsdefine's version

verbatim. After future content-overlay syncs from upstream, no diff in

this file.
2026-06-06 17:39:01 +08:00
dd3xp 4db3ef9af8 Merge RoundSquisheen: scheduler.py mkdir before log open 2026-06-06 00:09:31 +08:00
dd3xp 44e533121a fix(reflect/scheduler): ensure sche_tasks/ exists before opening log file
scheduler.py:23 immediately does logging.FileHandler(sche_tasks/scheduler.log)

at module load. .gitignore excludes sche_tasks/ entirely, so any fresh

deploy (clone + run) crashes with FileNotFoundError before scheduler

even gets to its main loop.

The author's intent (line 76: 'if not os.path.isdir(TASKS): return None')

is that sche_tasks/ should be optional — scheduler gracefully no-ops

when it's missing. The top-level FileHandler call violates that.

Fix: makedirs before opening the log. Mirrors the makedirs already done

for DONE/ inside check() at line 78.

After this lands, any GA deployment (Tauri / Squishy / kw / launch.pyw)

can start scheduler without manual mkdir, and L4 archive cron runs from

day one.
2026-06-06 00:09:26 +08:00
dd3xp 7eaa3742c4 Merge RoundSquisheen: bridge auto-starts L4 scheduler 2026-06-05 22:37:46 +08:00
dd3xp 5ade9841d2 feat(desktop): bridge auto-starts non-IM services on boot (L4 cron)
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
2026-06-05 22:37:43 +08:00
dd3xp a0053d9e7f Merge RoundSquisheen: pollSession backoff retry 2026-06-04 04:04:22 +08:00
dd3xp cace868fc9 fix(desktop): pollSession retries with backoff before giving up
When the phone goes to background, JS is paused and HTTP keepalive

connections die. On return, the first poll's fetch can hit "Failed to

fetch" because of a stale connection / DNS / TLS state. The original

code showed an error immediately on a single failure.

Now: same polling loop tolerates up to 5 consecutive errors with

exponential backoff (1s, 2s, 4s, 8s) before surfacing showError. Any

successful poll resets the error counter. This rides through the

typical 1-3 second post-resume blip without any user-visible failure.

Pairs with the earlier WS auto-reconnect + visibilitychange handler

(both bridge channels — WS and HTTP polling — now recover cleanly).

Bump app.js?v=87 -> 88.
2026-06-04 04:04:20 +08:00
dd3xp 5a450bd890 Merge RoundSquisheen: bridge WS auto-reconnect 2026-06-04 00:00:47 +08:00
dd3xp 633e8111a8 fix(desktop): bridge WS auto-reconnect + visibilitychange handler
When mobile browser is backgrounded the OS pauses/kills WebSocket; on

return the page stays on a dead connection and the next send shows

"bridge 未连接". Conductor's WS already had reconnect+backoff; bridge

did not.

Added to bridge WS:

- close handler now schedules reconnect with exponential backoff

  (1s, 2s, 4s, ... capped at 30s)

- visibilitychange listener: when page becomes visible again and ws is

  closed, reset retry counter and reconnect immediately

- wsRetries resets on successful open

- skip retry while page is hidden (battery saver)

Bump app.js?v=86 -> 87.
2026-06-04 00:00:44 +08:00
dd3xp c66e2d1ea1 Merge RoundSquisheen: CONDUCTOR_ORIGIN follows location.protocol 2026-06-02 22:26:56 +08:00
dd3xp 39c96744d4 fix(desktop): CONDUCTOR_ORIGIN follows location.protocol (not hardcoded http)
BRIDGE_ORIGIN already used ${location.protocol}//; CONDUCTOR_ORIGIN was

hardcoded to http://, which would have broken under HTTPS deployment

with a Mixed Content block. Now both behave consistently — page over

HTTPS uses https for conductor calls and wss for the WS already (the

WS constant was already correct).

Triggered by SakuraFrp requiring HTTPS access (TCP tunnels reject

plain HTTP with 501). Tunnels now run with 自动 HTTPS enabled, so

browser hits https://...:14168 / :18900, and frontend needs to use

https in its constructed conductor URL.

Bump app.js?v=84 -> 86 (skipping 85 — that was an auth-gate-only bump).
2026-06-02 22:26:53 +08:00
dd3xp 74453b1c44 Merge RoundSquisheen: centralize bridge/conductor port constants 2026-06-02 21:27:18 +08:00
dd3xp c45d7b86fb refactor(desktop): centralize bridge/conductor port + URL into constants
Per teacher request, pull the 14168 / 8900 magic numbers and the URL

construction out of ~17 inline call sites into a single block at the top

of app.js:

  const BRIDGE_PORT = 14168;

  const CONDUCTOR_PORT = 8900;

  const BRIDGE_ORIGIN = `...`;        // http(s)://host:14168

  const BRIDGE_WS_ORIGIN = `...`;     // ws(s)://host:14168

  const CONDUCTOR_ORIGIN = `...`;     // http://host:8900

  const CONDUCTOR_WS_ORIGIN = `...`;  // ws(s)://host:8900

All fetch / WebSocket call sites now use those origins. No behavioral

change — bridge still calls 14168, conductor still calls 8900. Sets up

future fixes (same-origin bridge, bridge-proxied conductor, runtime port

swap) to be a one-line change in the constants block.

Bump app.js?v=83 -> 84.
2026-06-02 21:27:15 +08:00
dd3xp 66d69130a5 Merge RoundSquisheen: per-page top bar + annotation scaffold dropped 2026-06-02 07:52:38 +08:00
dd3xp 2e64539ec5 feat(desktop): per-page top bar with caret toggles + drop annotation scaffold
1. Removed the two floating arrow buttons (.sb-toggle / .rp-toggle) that

   used to sit at the side seams between sidebar/main/rightpanel.

2. Each page section now owns its own .page-top bar with:

   - left: .pt-sb-toggle (caretLeft, borderless icon button) — collapses

     left sidebar; svg rotates 180° when sidebar is collapsed.

   - right: .pt-rp-toggle (caretRight) — same logic for right panel.

   - chat/collab keep run-state centered between the two icons.

   - services/token use a .pt-spacer to fill the middle gap.

3. .pt-icon style is borderless / transparent / opacity-0.7 (no boxy

   bordered button look); page-top padding tightened to 4px so the

   carets hug the screen edges. Bump styles.css?v=95→97.

4. Wiring: app.js queries .pt-sb-toggle/.pt-rp-toggle by class (4 of

   each, one per page) and binds them to the existing

   .body.classList.toggle('sb-collapsed') / .toggle('rp-collapsed')

   handlers. Behavior identical to the old single buttons.

5. Dropped the DEV annotation scaffold: removed annot.css link in head,

   annot.js script at end of body, and deleted both files.
2026-06-02 07:52:36 +08:00
dd3xp 2d003d7210 Merge main into RoundSquisheen: catch up with PR#7 services tabs + 3 visual changes + cache buster bumps 2026-06-02 07:40:43 +08:00
dd3xp 94477092b4 chore(desktop): bump styles.css?v=93→94 to flush stale CSS cache
Previous commit changed .tok-tabs/.tok-tab/.tok-chart CSS but did not bump the styles.css version, so the browser kept serving the cached v=93 file and the visual changes (underline tabs, taller chart) did not appear in the page.
2026-06-02 07:27:53 +08:00
dd3xp c27be25cc7 feat(desktop): nav reorder + taller trend chart + token tabs use svc style
1. Sidebar nav: move 后台服务 (services) to AFTER 指挥家 (collab).

   New order: 聊天 → 指挥家 → 后台服务 → Token 统计.

2. Token trend chart: was flat (container 160px tall, viewBox 600×140).

   Now container 280px + max-width 760px to stop it stretching on wide

   screens; viewBox bumped to 600×260 with a bit more padding so text

   does not get distorted under preserveAspectRatio="none".

3. Token stats tabs (聊天 / Conductor) reuse the .svc-tabs underline

   look from the services page — same visual language. CSS-only;

   .tok-tabs/.tok-tab classes and the JS click handler are unchanged.

4. Bump app.js?v=81→82 (app.js really changed this time, and the bump

   also flushes stale i18n cache for previously-untranslated keys).
2026-06-02 07:25:25 +08:00
dd3xp 65a54a3f09 Merge PR #7: feat: merge channels and status into one services page
Resolved one conflict in app.js: kept the new token trend chart code from

main and adopted PR's services-tab nav handler (replaces the old

channels/status separate handlers, which the PR consolidates anyway).

index.html and styles.css auto-merged cleanly.

Closes dd3xp/GenericAgent_Desktop#7
2026-06-02 06:50:01 +08:00
dd3xp bd6f0d7be5 chore(desktop): remove preset entry from conductor + menu (preset is chat-only) 2026-06-01 17:12:31 +08:00
dd3xp e5b80d1003 feat(desktop): preset/composer/topbar restructure + conductor persistence + TUI-style session naming
UI restructure (frontend static):
- PLAN moved from sticky toggle chip to a preset card (BUILTIN_PRESETS) that
  one-shot-sends `presetPrompt.plan`, now pointing at the real
  memory/plan_sop.md (Auto was considered too but dropped — it semantically
  overlaps with the existing "自主探索" preset). All plan/auto toggle
  plumbing removed: state, chip listeners, applyToggleClass, ga_plan/ga_auto
  localStorage, planPrefix/autoPrefix injection.
- Chat composer: model chip moved out of the joined .cb-left pill group to
  sit next to the send button (composer-bot is now right-aligned model+send).
- Conductor composer: gained its own #collab-model-chip + #collab-model-menu;
  openModelMenu/renderModelMenu/updateModelChip refactored to be ctx-aware
  (both chips share the model list; each pops its menu into its own
  composer; updateModelChip syncs both names).
- Topbar dissolved into per-page headers: the global .topbar was removed
  from <main>; the run-state moved into a .page-top inside the chat page,
  so the "topbar" now switches with the page instead of persisting.
- Removed the conductor "{n} 个任务进行中" sticky banner (#collab-sticky);
  the same info is already in the progress panel and the sidebar badge.

Conductor persistence (conductor.py + frontend wiring):
- conductor.py: add_chat() carries files/images on each chat item; WS
  receive passes data.files/data.images through; hello includes a
  running flag (any subagent with status="running"). 5 net edits.
- app.js: ws.send now posts {msg, files, images} (was msg only); onWsData
  hello/chat branches map files/images and set S.conductorTyping from
  data.running so attachments + run animation survive a page refresh.

Session naming (mirror TUI continue_cmd._preview_text):
- New helper displayTitle(sess): if user manually renamed, use sess.title;
  otherwise scan messages — prefer the last <summary>…</summary> in any
  assistant reply, fall back to the first user message text (≤40 chars,
  attachment placeholders stripped), final fallback t('conv.defaultTitle').
- newSession() now defaults sess.title to '' instead of '新对话'; the
  earlier "first message → title" auto-rename block in submitInput is
  gone — titles only get written when the user renames explicitly.
- isAutoTitle() recognises '新对话'/'New chat'/'agent-N' as placeholder so
  legacy sessions and the brief agent-N experiment get re-derived live
  in the sidebar without losing user renames.
- Sidebar render + search filter now go through displayTitle.

Note: requires a one-time restart of conductor.py for its part to take
effect; the static frontend changes are just a refresh.
2026-05-29 02:22:16 +08:00
dd3xp ab9fbea3c1 chore: gitignore local BUG_*.md bug/limitation drafts 2026-05-28 04:34:19 +08:00
dd3xp 77c6adc09a feat(desktop): inline attachment chips in messages + fix conductor raw-path bubble
- Render [Image #N]/[File #N] placeholders as inline chips in-place (filename)
  in user messages, so you can see where an attachment sits in the text. Image
  thumbnails + file cards are unchanged. Applies to chat and conductor via a
  shared renderMsgTextWithChips() helper; conductor messages now carry sid+name
  so the chip can resolve a filename.
- Conductor: stripAttach() also strips inline desktop_uploads paths, and pushMsg
  folds the server echo into the local optimistic message (keep clean text +
  chips/cards, drop the raw path) so conductor matches chat instead of showing a
  second bubble with the full local upload path.

Known limitation (backend, not in scope here): conductor attachments + running
state don't survive a page refresh — conductor.py only stores/sends
{id,role,msg,ts,read} with no file refs and no run state, so the frontend can't
restore them on reload. Persisting those belongs to the conductor maintainer.
2026-05-28 04:27:37 +08:00
dd3xp 002c1851cf chore: remove dev_notebook.md (dev scaffold merged in via PR#5) 2026-05-28 03:58:53 +08:00
dd3xp 492caa610e fix(desktop-bridge): apply mykey edits to live agents without restart
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.
2026-05-28 01:22:07 +08:00
dd3xp a8e76450b8 feat(desktop): adopt PR#3 rectilinear design + clearer PLAN/AUTO + session restore
Re-skin the desktop frontend in ethan su's "rectilinear workspace" visual
language (PR#3) without dropping any feature, plus two UX fixes. Done as a
content-overlay, NOT a git merge of PR#3 — PR#3's app.js reverts ask_user /
conductor / composer / timing, so only its visual layer is adopted.

Design (PR#3):
- Bundle brand fonts (Azonix wordmark / Lexend / Noto Sans / JetBrains Mono)
  + phosphor-icons.js (auto-hydrates [data-ga-icon]).
- styles.css: adopt ethan's full design-token system (accent #1f6f53,
  --radius 4px/2px squared corners, brand fonts) via 3-way merge; keep all
  feature CSS (composer chips, ask_user notice, conductor, flatpickr, timing
  badge); normalize leftover --blue -> --accent.
- index.html: ethan's phosphor chrome; keep contenteditable composers, the
  real conductor UI, flatpickr links, and collab-badge.
- app.js: add GA_ICON() helper; swap dynamic chrome icons (pin, more, model
  edit/delete, add-preset, channel, ctx-delete, drawer-close) to Phosphor.
  Flags, copy/check, file-chip and preset-card icons left inline by design.

Fixes:
- PLAN/AUTO toggles now show an unmistakable on-state (solid accent fill);
  the .cb-left .chip.sm rule was overriding .chip.on by specificity, so the
  on/off difference was nearly invisible.
- On refresh, restore the session you were actually viewing (persist
  ga_active) instead of the bridge's roaming most-recently-updated session;
  show the empty new-session state when none is selected.
2026-05-28 01:22:07 +08:00
dd3xp 875e6614a4 refactor(ask_user): treat single-question as a special case of multi-question
Unify the single-question and multi-question ask_user rendering instead of
having two divergent paths:

- Single-question options (from `candidates`) are now folded into the question
  body as inline A./B./C. lines via foldAskCandidates(), rendered exactly like
  multi-question inline options (.ask-option-line). The separate numbered <ul>
  candidate list is gone, which also removes the "1. A." double-numbering.
- Fold with a single newline so question + options render as one <p>; every
  option (including the first) follows a <br>, so all get consistent indent.
- Placeholder is unified to the free-form ask.placeholderOpen for both; the
  per-single-question "type 1/2/3 to pick" placeholder is removed.
- Drop now-dead code: ask.placeholderOpts i18n (zh+en) and the
  .ask-candidates / .ask-candidate-key / .ask-candidate-label CSS.
2026-05-27 23:04:32 +08:00
dd3xp 51c0330621 merge: fold latest main (ask_user + session-rename) into contenteditable composer
- Resolve styles.css: keep session-rename's .ci-rename-input + my .ci-pin, both on --accent.
- Adapt ask_user dynamic placeholder: inputEl.setAttribute('placeholder',...) ->
  'data-ph' so it shows on the contenteditable composer (no placeholder attr on a div).
- session-rename uses its own rename <input> (.value) — untouched, no composer conflict.
- Bump asset cache-bust (styles.css v74 / app.js v45).
2026-05-27 21:55:51 +08:00
dd3xp 48d26ac4d2 merge: reconcile desktop/main (conductor + ask_user base via bbfa91c) with contenteditable composer
WIP checkpoint: both composers (chat + collab) to contenteditable+atomic chips on top
of lyx/017's ctx multi-composer; conductor send uses gaComposerText('collab'); --blue
-> --accent; flatpickr (PR#2). Re-merging latest main next.
2026-05-27 21:50:26 +08:00
dd3xp 7074c20f71 refactor(desktop): rename theme var --blue -> --accent; plain-icon sidebar toggles
- Rename the misleadingly-named --blue theme accent var to --accent (incl.
  --blue-bg/-soft -> --accent-bg/-soft) across styles.css + the applyTheme
  setter in app.js. Keep --blue* as back-compat aliases of --accent* so
  unmerged branches (e.g. PR#1) still render until they migrate.
- Sidebar / right-panel toggles: plain icon button when expanded (no bg/border);
  restore the edge-tab look (bg + border) only when collapsed.
- Bump asset cache-bust (styles.css v23 / app.js v30).
2026-05-27 18:46:35 +08:00
dd3xp e7371b9dec Merge branch 'fpr2' into RoundSquisheen 2026-05-27 17:56:24 +08:00
dd3xp 8f3d3c1f14 fix(desktop): bump asset cache-bust (css v21/js v29) so chip styles load; make chip bg more visible 2026-05-27 17:41:21 +08:00
dd3xp 6fc2f08ca8 style(desktop): attachment chip = translucent theme-color block, drop emoji icons 2026-05-27 17:36:58 +08:00
dd3xp 3ee95a2818 feat(desktop): contenteditable composer with atomic attachment chips
Replace the plain <textarea> composer with a contenteditable div so attachment
placeholders render as atomic, non-editable chips (QQ @-mention style): the caret
can't enter them and Backspace deletes the whole chip + its file. Stays a plain-text
box otherwise — no rich formatting, paste is forced to plain text.

- index.html: <textarea class="input"> -> <div class="input" contenteditable>.
- composerText() serializes content back to plain text (chips -> [Image #N]/[File #N],
  <br>/<div> -> newline) so all downstream send/expand/collect logic is unchanged.
- insert/remove placeholder now create/remove <span.ph-chip contenteditable=false>
  nodes; reconcile compares pendingFiles against chips present in the DOM.
- Drop the textarea-only atomicPlaceholderDelete (contenteditable=false is native).
- Enter-to-send + IME guard kept; paste forced to plain text (+ image/file paste).
- setComposerLocked toggles contentEditable (readOnly is a no-op on a div).
- i18n placeholder hint via :empty::before + data-ph; .ph-chip pill styling.
2026-05-27 17:20:41 +08:00
dd3xp 1d88953d8b feat(desktop): robust attachment placeholders — atomic delete + input reconcile + send cleanup
Composer attachment placeholders ([Image #N]/[File #N]) are plain editable text
with no sync to pendingFiles, so deleting/breaking one silently dropped the file
(chip lingered) or sent broken text to the agent. Hardened with three pieces:

- #2 atomic delete: Backspace/Delete adjacent to a placeholder removes the whole
  token + its file at once (a <textarea> can't make a substring non-editable, so
  this delivers the same UX without a contenteditable rewrite).
- #1 input reconcile: on every edit, drop any pending file whose valid placeholder
  is gone/broken — removes the chip and DELETEs the uploaded file on the bridge.
- #3 send cleanup: expandFilePlaceholders strips dangling placeholders (no backing
  file) instead of leaving them, so no garbage reaches the agent.
- Refactor: extract removePendingFile() shared by the chip × button and the above.
2026-05-27 16:42:43 +08:00
dd3xp cf682093db fix(desktop): after deleting active session, select the topmost one
closeSession picked state.sessions.keys().next().value — the Map's first
insertion-order entry, unrelated to the visible list, so it felt like jumping
to the "bottom" session. Now it picks the first of the displayed order
(sortedSessions()[0]): pinned first, then most-recently-active = the topmost.

Extracted sortedSessions() shared by renderSessionList and closeSession.
2026-05-26 02:22:16 +08:00
dd3xp 03bd2d5c20 Merge remote-tracking branch 'desktop/main' into RoundSquisheen 2026-05-26 02:01:03 +08:00
dd3xp 0d2d0a7952 Merge PR #14: real-time message timing badge + frontend restyle
feat/message-timing-badge (wjl2023): elapsed-time badge on assistant messages
(persists across refresh via message ts), bubble-less message style, Apple-style
fold disclosure with left border, fold-turn tool-name fallback when no <summary>,
plus a frontend beauty development notebook.
2026-05-26 02:00:45 +08:00
dd3xp bab5dec66f feat(desktop): group uploads by session id; purge on session delete
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.
2026-05-26 00:41:40 +08:00
dd3xp 4cb073ee05 refactor(desktop): drop dead image-upload path; images & files share one dir
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.
2026-05-26 00:16:14 +08:00
dd3xp 36424089e9 fix(desktop): persist uploads in GA temp dir with 30-day retention
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.
2026-05-25 23:24:32 +08:00
dd3xp 97f59dcf41 Merge update/upstream-sync-1eb8c4c into main: sync framework to upstream @ 1eb8c4c
Brings 26 GA framework files to upstream lsdefine/GenericAgent 1eb8c4c
(tui_v3, fsapp, llmcore, plugins/hooks, goal_mode rewrite, supergrok_proxy,
qq/tg/wechat/st channel apps, cost_tracker py3.9 compat, docs; removed old
install scripts). Desktop frontend (frontends/desktop/*) and desktop_bridge.py
preserved. PR #13 (summary dedup) retained via 3-way merge.
2026-05-25 23:10:51 +08:00
dd3xp 362b7d601c chore: sync framework to upstream lsdefine/GenericAgent @ 1eb8c4c
Content-overlay update of all GA framework files from upstream base
b4393af (2026-05-18) to 1eb8c4c (2026-05-25). Applied as an overlay
because GADesktop and upstream share no common git history.

- 26 framework files brought to upstream: tui_v3 (new), fsapp rework,
  llmcore hardening, plugins/hooks lifecycle hooks, goal_mode prompt
  rewrite, supergrok_proxy, qq/tg/wechat/st apps, cost_tracker py3.9
  compat, docs; removed obsolete install scripts.
- Desktop frontend (frontends/desktop/*) and desktop_bridge.py left
  untouched. Upstream's only overlapping change (bridge mykey.txt->
  mykey.py) is already present in our version, so it is a no-op.
2026-05-25 22:03:19 +08:00
dd3xp 56b688cc28 feat(desktop): file attachments survive refresh + remote download + fix Windows open
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.
2026-05-25 01:18:38 +08:00
dd3xp 57cf4d6632 feat(desktop): run-state becomes display-only; send key turns into a stop button while running
The top-left run-state is now a pure status indicator (green=ready, red=running)
and no longer handles clicks. Stopping the current turn moves to the send key:
while the agent is running the send button turns into a red stop square — clicking
it cancels the current turn (ChatGPT/Claude style). The Enter key still submits, so
"type while running + Enter" keeps lyx's interrupt-and-resend behavior (#5) intact.

- setBusy toggles .is-stop on the send button + aria-label (stop/send)
- send click: cancelPrompt() when busy, submitInput() otherwise
- remove run-toggle click handler + .stopped state; .run-state.busy goes red
- .send.is-stop: red bg + white rounded square (hide arrow); i18n act.send
- cache bust styles v19->20, app v25->26
2026-05-23 23:38:34 +08:00
dd3xp 26872bd39a style(desktop): dark md-link mix anchor uses --txt-strong, not hardcoded #fff
PR #9 left the dark-mode markdown link color as
color-mix(... var(--blue) 42%, #ffffff). Route the white anchor through
var(--txt-strong) (which is #ffffff in dark) so no hex literal sits in a
selector. Bump styles.css cache bust v17 -> v18.
2026-05-23 19:05:08 +08:00
dd3xp 914e632fa2 style(desktop): send-button busy spinner uses --on-accent, not hardcoded white
PR #5 left the .send.is-busy::after spinner at rgba(255,255,255,.35) / #fff.
Route it through var(--on-accent) so it follows the accent foreground color
(white by default, dark on theme 7's yellow accent) instead of always white.
Bump styles.css cache bust v8 -> v9.
2026-05-23 17:25:57 +08:00
dd3xp e2a67f0803 fix(upload): surface payload-too-large errors, reject empty files, use toast not chat-log
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.
2026-05-21 23:39:39 +08:00
dd3xp 22d4654e85 fix(chat): post-merge cleanup for PR #4 rendering — i18n + theme vars + typewriter race
- Replace hardcoded 'Copy'/'Copied!'/'TeX' in postRenderEnhance with t('act.copy/copied/copyTex'); add zh/en keys
- Fix var(--accent) reference (undefined) in .cursor-blink -> var(--blue)
- Add dark-mode overrides for --fold-thinking/tool/result border+bg so fold blocks don't glow pastel on dark backgrounds
- Typewriter: timer now reads r.draftText each tick (was a stale closure on full -> never picked up streamed extensions); drop the visible || full fallback that immediately revealed the entire content on first paint
2026-05-21 19:52:07 +08:00
dd3xp 5fe5401ec6 style(desktop): drop hardcoded colors/strings, remove placeholder page-subs
- Move pricing-tooltip text in modelPriceTip to i18n keys (tok.priceInput/Output/CacheW/CacheR/pricingUnknown), drop lang === 'zh' ? : ternaries
- Replace remaining hex literals in selectors with CSS vars: chan-log-pre and mykey-editor backgrounds now respect --field-bg / --card (dark mode no longer shows white panels)
- Introduce --sample-light-* / --sample-dark-* vars for appearance preview cards (intentionally static, do not follow current theme)
- Remove placeholder page-sub on Channels / Status / Token pages (annotation copy, never meant to ship); keep on Collab page since that page is still unimplemented
2026-05-21 17:46:47 +08:00
dd3xp e970ec5172 fix(desktop): persist stopped output, decouple user display from LLM prompt, anti-flicker boot
- 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)
2026-05-21 17:12:42 +08:00
dd3xp 4f18d6e9e7 fix(bridge): protocol now required for add-model (no apibase auto-guess); UX: segmented control + flag SVGs
- _next_native_var now requires explicit protocol ('oai' or 'claude'); raises if missing. Removes the silent 'native_claude_config' default that wrongly routed all OAI-compatible relays through Anthropic protocol.
- Add-model form: replace native <select> with a segmented control (2 radio buttons styled as pills) since browsers can't style native dropdown panels. Required validation kept.
- Language list: inline SVG flags before labels (China / USA, per request), uses official flag colors. Reliable on Windows where flag emojis render inconsistently.
- i18n: model.protocol/protocolPick/protocolOai/protocolClaude in zh+en.
- Edit mode now backfills protocol radio from varName.
2026-05-21 12:59:19 +08:00
dd3xp 59c4efb89c feat: collapsible/resizable left sidebar + pin marker + preserve partial on stop
- Add sb-toggle/sb-resize mirroring rp-toggle/rp-resize; refactor into shared bindResize().
- Tuck both toggles into their panel's inner edge via transform; lighter color + opacity:.6 (uniform with collapsed state).
- Narrow body.gap 12→6 so cards sit closer, main effectively wider.
- Pin marker: tilted pin SVG (rotate 45deg, --blue) before title when sess.pinned; toggle pin/unpin via ⋯ menu with dynamic i18n label (ctx.pin/ctx.unpin). Unpin places session at top of the non-pinned area.
- Preserve streamed partial on stop: pollSession exit no longer drops draft text; finalizes it as an assistant message with [stopped] marker (i18n).
2026-05-21 02:51:45 +08:00
dd3xp 275856dcc3 i18n: externalize all UI text (zh/en, empty defaults); separate dev annotation layer into annot.css/annot.js
- All static UI text via data-i18n + I18N{zh,en}; runtime applyI18n; language switch in settings, persisted.
- Empty defaults in HTML (no source language baked in).
- Annotation/dev scaffolding fully extracted to annot.css + annot.js (runtime-injected); index.html/styles.css/app.js carry zero annotation footprint; deletable via 2 include lines.
- Preset cards decoupled via data-preset; model chip default emptied.
2026-05-20 02:10:14 +08:00
dd3xp 5032fe7091 GenericAgent Desktop: mockup-based client wired to bridge
Initial snapshot. Desktop frontend (frontends/desktop/static/) rebuilt as the served client: 3-card UI, sidebar nav, preset functions, sessions, streaming via HTTP poll + WS notify, markdown, modals (preset/add-model/settings), theme-variable styling. mykey.py excluded (gitignored).
2026-05-20 00:29:22 +08:00