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.
Old '自主探索' (auto-explore) was just a browse+summarize prompt, unrelated
to the framework's reflect/autonomous.py module. Teacher requested a real
trigger for autonomous mode — same behavior as the 30-min idle reflect cron
but user-initiated via preset click.
- BUILTIN_PRESETS: key 'explore' → 'autonomous', icon 'compass' → 'gridFour'
- i18n rename (zh + en):
preset.explore.* → preset.autonomous.* ('自主行动' / 'Autonomous mode')
presetPrompt.explore → presetPrompt.autonomous
- New prompt points at memory/autonomous_operation_sop.md so the agent
reads the same SOP that reflect/autonomous.py uses. Mirror of the cron
trigger '[AUTO]🤖 用户已经离开...' but framed as user-initiated.
- Side effect: users who had 'explore' hidden in localStorage will see the
new '自主行动' card show up by default. Acceptable for a feature change.
Cache buster: app.js 189 → 190.
UI cleanup wave:
Chat status indicator:
- Move .run-state ('服务在线' chip) back from sidebar foot to chat page-top
(reverts the PR #1538e1670 relocation only; other PR #15 changes kept)
- Restore .foot-ver version label block in sidebar foot
- Restore .page > .page-top .run-state grid positioning rule
- Drop all .foot-status CSS + i18n + breathing-dot selectors
- .run-state height switched to var(--shell-head-h) so it sits flush with
the 28px topbar grid (no more 2px overshoot)
Send button:
- .send background var(--accent) → var(--ink-green) (#1f6f53), hover →
var(--ink-green-hover) (#175a44). Two new CSS vars introduced in :root
to avoid hardcoded colors per project convention.
- Stop-state (.send.is-stop) still uses var(--danger) — no change
New preset card ('找点事做'):
- Adds BUILTIN_PRESETS entry between review and mine using phosphor robot
icon. Sends the same fixed prompt as stapp.py's autonomous-action button:
按照自主行动的规划部分,充分分析我的情况,给我生成一批 TODO,务必让我感兴趣。
- New i18n keys: preset.findwork.t/d + presetPrompt.findwork (zh + en)
Token pager arrows:
- Replace Unicode '‹' '›' with phosphor caretLeft/caretRight SVGs (12×12,
muted2 → txt-2 on hover), matching the sidebar .chev style
- Drop the font-size 20px + padding-bottom workaround that tried to
visually center the Unicode characters
Real mykey.py configs with many providers + IM tokens + Langfuse + heavy
comments easily exceed 20000 chars. 1M ceiling effectively removes the
limit for any realistic config while still preventing accidental
multi-megabyte paste from misbehaving content.
Trend chart removed from this release:
- index.html: drop the entire tok-chart-wrap block (toggle button + chart)
- app.js: drop renderTokChart() function, tokChartEl/tokChartToggle/tokChartWrap
const+listener, all conditional display branches in tab switching and
loadTokenPage, and the tok.chartToggle i18n key (zh + en)
- styles.css: drop .tok-chart-wrap, .tok-chart-toggle, .tok-chart rules
Token table paginator no longer dumps every page button:
- New renderTokPager() builds first / last / current ± 2 / ellipsis / prev /
next, capped at ~9 DOM nodes regardless of total pages. 1000 pages and
10 pages render the same shape, no more flex row overflow.
- Add .tok-pager-gap style for ellipsis spans, :disabled style for endpoint
arrows, and flex-wrap:wrap fallback on .tok-pager.
Cache busters: styles.css 153→154, app.js 186→187.
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.
Drop the var(--line-soft) gray background — too dim against the topbar.
Match .preset-restore button styling:
- background: var(--card) (clean white panel)
- border: var(--line)
- hover: bg var(--hover-2), color + border become var(--accent)
styles.css?v=152→153.
The previous collab page showed a wide banner with the long offline message
plus a "重试" chip button across the top, which looked clunky. Move both
into the existing page-top status bar:
- Drop .collab-offline and .collab-reconnect <div> banners entirely
(HTML, CSS rules, and JS show/hide logic).
- Wrap .run-state and a new .collab-retry-btn in a .page-top-center flex
container (grid-column:2 of the page-top grid).
- Topbar text replaces "未连接" with the new collab.offlineShort string
("无法连接 Conductor(8900)" / "Cannot reach Conductor (8900)") via
collabStatus.set(longer, 'offline').
- The retry button is a 24×24 button styled like .new-conv (var(--line-soft)
bg + 1px border + 4px radius, hover var(--hover-2)), holding a 14×14
inline SVG refresh-cw icon. Hidden by default, shown only when offline
AND no auto-reconnect is in progress.
- setConnUi simplified to just toggle the retry icon's [hidden] attribute.
- Existing click handler on #collab-retry preserved (id unchanged).
Sidebar:
- 快速接入官方模型 header restyled to match 配置 button (foot-row style:
transparent bg, padding, hover state, lightning icon on left,
caretRight chevron on right that rotates 90deg when expanded).
- Drop the "填好 API Key 即可使用" subtitle for single-line consistency.
- Drop the "展开/折叠" tooltip on the head.
- Bump both .pq-head and .foot-row padding to 14px so the buttons look
substantial.
- Set .sidebar-foot margin-top:6px to match .provider-quickstart's internal
gap:6px — uniform 6px rhythm between pq-head→DeepSeek→Qwen→配置.
Services page:
- Remove the "IM 消息通道与后台进程,集中查看、启停与日志" subtitle.
- Align .svc-tabs margin with .tok-tabs (margin:10px 0 0) so the title
position and tab bar position match the Token usage page.
Token page (i18n):
- zh: "Conductor" → "指挥家" on tab label, total/current row labels,
tooltip, and the new tok.condOffline string (replaces hard-coded text).
- en remains "Conductor" (English UI keeps the English name).
Token table column widths:
- Switch to table-layout:fixed. Number columns 80px each, cache-rate 85px.
- First column (session name) gets the rest of the width; overflow truncates
with ellipsis. JS adds title="<full name>" so hover shows the complete
session title as a tooltip. No more column-width jitter when switching
Chat/指挥家 tabs.
Color tweaks (no hardcoded values):
- .pq-btn background var(--bg-base) → var(--field-bg) (lighter).
- .model-guide border var(--accent) → var(--line-base); background
var(--accent-bg) → var(--field-bg) (lighter, neutral).
Trend chart fix:
- SVG had preserveAspectRatio="none" which stretched the viewBox 600x260
non-uniformly to the container (max-width 760 x height 280), making text
~15% wider than tall ("扁").
- Switch to preserveAspectRatio="xMidYMid meet" so text renders at correct
aspect, and replace fixed container height with aspect-ratio:600/260 +
min-height:280px so chart fills naturally without distortion.
Color theme cleanup (user-requested):
- Drop 8-color mac-style palette, keep only a single gray light theme.
- --accent now hard-coded gray (#8e8e93); removed --swatch-2..8 and all
html[data-theme="N"] / .swatch[data-theme="N"] CSS rules.
- Remove the entire "颜色" (set.theme) block from settings page.
- applyTheme() stubbed: still callable for backward compat with stored
ga_theme values, but always normalizes to theme '1'.
- Drop set.theme i18n entries (zh + en).
Cache busters: styles.css 136→137, app.js 181→182.
Post-merge ljq's "reuse draftEl, update in-place on done" optimization relied
on partial stream having delivered the complete content by the time the done
signal arrives. Over the frp tunnel (~80-300ms RTT), the bridge can flip to
idle before the last partial chunk reaches the browser, leaving the bubble
with truncated content from the partial accumulation.
Replace both turn_segs and no-turn_segs branches with a single in-place rebuild
that takes the final m as canonical source:
- assistantTurnSegs(m) handles both turn_segs and content fallback (same as
the refresh path through msgNode)
- bubble.innerHTML = renderAssistantTurnsHtml(...) + postRenderEnhance for
code highlighting and other post-render hooks
- sync r.draftSegs/draftTurn/streamTurn/twState so later cursor/badge/copy
logic reads consistent state
Keeps ljq's no-flicker bubble reuse; same DOM element survives, only its
innerHTML gets replaced by the canonical full content. Local low-latency
behavior unchanged.
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.
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.
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.
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
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.
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.
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).
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.
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.
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.
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).
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.
Closesdd3xp/GenericAgent_Desktop#7
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.
- 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.
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.
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.
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.
- 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).
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.
- 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).
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
- 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
- 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
- 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)
- _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.
- 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).
- 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.