Skip to content

feat(web): LLM session titles, developer tab, and web UX fixes - #211

Merged
guima-why merged 22 commits into
mainfrom
fix-web
Jul 30, 2026
Merged

feat(web): LLM session titles, developer tab, and web UX fixes#211
guima-why merged 22 commits into
mainfrom
fix-web

Conversation

@guima-why

Copy link
Copy Markdown
Collaborator

Overview

Web session-experience work plus supporting fixes. The branch focuses on the local Web app (iac-code web): session titles, image handling, pipeline transcript polish, a Developer settings tab, and a batch of layout/flicker fixes. It also carries one cross-cutting commit that consolidates MCP OAuth keychain storage and adds an offline-context runtime so Web session switching no longer triggers keychain popups.

Rebased onto latest main (GLM 5.2 fast preview + CI/test stabilization). No textual conflicts; a semantic review confirmed no logical/functional clash between the two sides (notably the MCP storage striped-RLock layer and the OAuth single-blob rmw lock use distinct file-lock keys and are compatible).

Highlights

LLM session titles

  • Generate session titles with the LLM after the first turn (thinking off / lowest effort), replacing first-message truncation.
  • Covers image-only sessions that previously fell to (empty) and vanished from the sidebar; persisted + scheduled with a fallback title and retry.

Web UX

  • Full-screen image lightbox; sidebar title + image-attachment restore on reload.
  • Developer settings tab (default off) gating the failed-tool red highlight and the restart control.

Fixes

  • Output-panel i18n labels no longer swallowed by a Babel regex dead zone.
  • No output-panel reset / sidebar-hover / tool-card overflow flicker during a running pipeline; /status context rows wrap; new-session project menu height capped.
  • Retired the legacy pipeline workspace modal entry (underlying code kept).

MCP + docs

  • fix(mcp,web): OAuth fields consolidated into a single keychain blob (macOS "Always Allow" prompts once, not 8×); added disable_external_services offline-context runtime.
  • Documented the http extra required for the Web app in README, translated READMEs, and website docs (all 6 locales).

Testing

  • make test scope (web / providers / mcp / services / i18n): 3030 passed, 2 skipped.
  • Overlap-file targeted tests (runtime contracts / mcp storage / oauth / agent_factory): 143 passed.

🤖 Generated with Claude Code

guima-why and others added 22 commits July 30, 2026 22:04
…t runtime

Store all OAuth fields (access/refresh token, expiry, client creds, markers)
in a single keychain item per MCP as one JSON blob, so macOS "Always Allow"
prompts once instead of eight times. Per-field access goes through
get/set/delete_oauth_storage_secret accessors using a dedicated rmw lock
(distinct from the coarse CAS lock, since file locks are non-reentrant);
reads are lock-free. No migration/back-compat by design.

Also add an offline context-accounting runtime (disable_external_services)
so Web session switching no longer spins up MCP and triggers keychain popups.

Rewrite ~150 test call sites to use the accessors instead of raw keychain keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The thinking summary used a wider icon column (0.92rem) with a centered
lightbulb and a larger gap (0.18rem) than the tool rows (0.68rem column,
left-aligned box, 0.14rem gap), so the whole row read as more indented.
Match the tool-row grid and compensate the outline glyph's ~21% intrinsic
left padding with a negative margin so its visible left edge lines up.

Bump styles.css cache-bust token and sync the frontend static tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The .message-agent:has(.message-tool-cards) grid had no explicit columns,
so its implicit column sized to max-content; each child's width:min(40rem,
100%) then resolved 100% against that oversized column and landed at ~40rem.
Inside an indented pipeline step body (agent box narrower than 40rem) the
column overflowed several rem past the step's right edge. Constrain the
column to minmax(0, 1fr) so it never exceeds the agent box, and add
min-width:0 on the body so long content wraps instead of forcing overflow.

Bump styles.css cache-bust token and add a regression assertion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The /status inline panel truncated long "Context" (背景信息) rows to a
single line with an ellipsis, hiding the candidate/step/usage detail
users open /status to read. Drop nowrap/overflow/ellipsis on the plain
.session-status-row dd and let the value wrap within its column
(overflow-wrap: anywhere). The copyable session-id row is unaffected —
it uses the .session-status-row-copy variant whose value span keeps its
own single-line ellipsis + copy button.

Bump styles cache-bust token 307 → 308.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a session is running the sidebar was rebuilt at high frequency —
every SSE stream frame via render() and every 2.5s background poll via
refreshSessionsSidebar() — each doing a full replaceChildren(). That
destroyed the row under the pointer, so :hover backgrounds and the
hover-revealed action buttons (.thread-actions / .project-actions)
flickered on and off.

Mirror the existing message-stack hover throttle: route the streaming
and background repaints through renderSessionsAuto(), which defers the
rebuild while the pointer is inside the sidebar (bookkeeping only, no
DOM churn) and catches up once on pointerleave. A stable .session-rail
container carries the pointerenter/leave guard, bound once. User-driven
structural actions (expand/collapse/pin/switch) still call
renderProjectThreadNavigation / renderSessions directly and repaint
immediately. Session data keeps refreshing on state; only the paint is
deferred, so other sessions' spinner/time lag at most one poll and snap
back the instant the pointer leaves.

Bump app.js cache-bust token 305 → 306.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The composer project picker menu opens upward (bottom:100%) anchored to its
trigger. A fixed CSS max-height cannot reference the trigger's real distance to
the viewport top (it varies with composer height, breakpoint, window size), so
in short windows the menu's top (the search box) overflowed the viewport.

- Add clampDraftMenuToViewport(): on open, measure the picker's getBoundingClientRect
  and set inline max-height to min(25rem, available-space) so the top never clips;
  applied to all four composer draft menus (project/new-project/mode/pipeline).
  CSS max-height stays as a pre-JS fallback.
- Restructure the project menu into three regions: fixed search header, a single
  scrollable .draft-session-menu-list, and a fixed .draft-session-menu-footer
  (divider + New/No project). The menu itself is now flex-column + overflow:hidden,
  so only the middle project list scrolls; search and footer stay put.
- Bump styles.css -> web-repl-ui-310, app.js -> web-repl-ui-308; sync frontend
  static tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
用户从未开放过 pipeline 工作区模态这一功能,但标题栏仍暴露「View
pipeline」按钮,且候选选择时会自动弹出该面板,与主区内联的流水线体验重复。

- pipelineWorkspaceEntryVisible() 恒返回 false,标题栏按钮永久隐藏
- 移除候选选择时的两处 maybeOpenPipelineSelectionWorkspace 自动弹出调用
- 保留按钮 markup / 点击处理 / 隐藏 tab / components/pipeline.js 渲染代码,
  以及 maybeOpenPipelineSelectionWorkspace、pipelineSelectionRequiresWorkspace
  函数(当前无调用),改动可回滚
- 视觉审计新增 openLegacyPipelineWorkspace:临时揭开隐藏按钮并复用其真实点击
  处理打开工作区,继续覆盖该工作区的视觉回归
- app.js 缓存 token 308→311,同步 test_frontend_static.py 断言并重构对应用例

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
流水线运行中输出面板「一闪一闪」、且用户 X 关掉后又被弹回:loadSession
无条件调用 outputController.reset(),reset() 强制关闭面板并清掉「首次自动
展开」守卫(autoOpenedOnce),随后 refresh() 又把它自动弹开。session.resync
.required 在运行中反复触发(权限确认、input_required 等),每次重走 loadSession
→ reset(强关) → refresh(弹开),肉眼即持续闪烁,并覆盖用户的手动关闭。

紧邻的 clearDetailsOpenOverrides 早已用「仅真正切换会话」守卫,唯独 reset()
漏了这层守卫。

- 抽出 switchedSession 判定,reset() 与 clearDetailsOpenOverrides 共用
- 仅真正切换会话时复位(清空上个会话产物并重新武装一次性自动展开);同会话
  resync/重载只 refresh(),保留 isOpen 与 autoOpenedOnce
- 新建会话草稿/草稿落地两条路径的 reset() 不受影响
- 新增 test_output_panel_reset_only_on_session_switch_in_load_session 回归守卫
- app.js token 311→312,同步 test_frontend_static 断言

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
escapeHtml's `/"/g` and `/'/g` put a bare quote inside a regex literal;
Babel's webui JS lexer reads the quote as a JS string start and desyncs the
token stream, so every t() after escapeHtml in output_panel.js was silently
dropped from extraction. "Resource stacks" / "Template files" (used only here)
got marked obsolete and fell back to English — and the last `make translate`
fuzzy-matched them to wrong strings ("资源类型" / "模板已修复"), the "又换了"
the user saw. "Architecture diagram" survived only because pipeline.js also
uses it.

- output_panel.js: escape the two quote regexes as unicode (identical match),
  with a comment forbidding a revert; bump output-panel v16→v17, app.js 312→313.
- webui.po (6 locales): restore correct translations for Resource stacks /
  Template files; translate the never-extracted File no longer exists /
  Close preview so test_webui_fully_translated stays green. .mo recompiled.
- test_webui_extraction.py: add test_output_panel_labels_are_extractable so a
  revert to a bare-quote regex fails loudly instead of silently at next extract.
- test_frontend_static.py: sync cache-bust token assertions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Composer thumbnails and in-message images now open a shared full-screen
lightbox on click instead of being non-interactive (or, for composer
thumbnails, deleting the attachment on any click).

- Add components/image_lightbox.js: a singleton overlay appended to
  document.body, closed by backdrop click, the × button, or Esc.
- Composer: the image chip is now a container with a preview button
  (opens the lightbox via an onPreviewImage callback wired from app.js)
  and a separate × button that removes the attachment. Clicking the
  thumbnail previews; only × deletes.
- Message images: render as a button that opens the lightbox with the
  /api/images original.
- Styles for the lightbox and the preview button; × button reset.
- i18n: add "Preview image" (webui domain, all 6 locales).
- Bump cache-bust tokens (styles 311, app 314, composer v18, new
  image-lightbox-v1) and sync test_frontend_static.py; update the
  composer chip test to assert preview-vs-delete behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The new first-turn LLM session-title feature fires a fire-and-forget task
that appends a session.updated event on brand-new sessions. Seven
turn-message-lifecycle tests assert exact event sequences and were polluted
by that orthogonal, nondeterministically-timed event. Disable
pending_llm_title in those tests so they stay deterministic; titling is
covered by its own dedicated tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pipeline first turn sets an instant provisional sidebar title so the
session appears immediately, then schedules a background LLM title that
overwrites the placeholder once ready. A new title_provisional flag lets
the in-flight LLM result overwrite the placeholder while a user rename
freezes the title and wins over any late LLM result.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two reload-vs-live divergences:

- session.updated/session.started patched only currentSession (header),
  leaving sidebar rows on the stale title until the next background list
  poll (~2.5s). Fold currentSession back into the sidebar arrays in
  handleStreamEvent so the row and header refresh in the same frame.

- normalizeStoredMessage dropped imageIds/fileRefs when rebuilding the
  transcript on session restore, so buildMessageAttachmentsElement saw
  undefined and rendered nothing (images vanished after reopening). Carry
  both fields through, matching the live user.message reducer.

Bumps app.js cache-bust token to web-repl-ui-316.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Developer mode" switch under Settings/General (off by default).
When enabled, a devOnly "Developer" tab appears in the workspace nav.
The Developer tab holds two features:

1. A "Highlight failed tool calls in red" toggle (off by default). Failed
   tool styling is gated under body.dev-highlight-tool-errors so toggling
   is instant and tool_cards.js stays untouched; when off, failed tool
   calls look like any other call.
2. The existing restart-service control, moved here from General.

Backed by GET/PUT /api/settings/developer ({mode, highlightFailedTools},
both default false). Adds webui strings across all 6 locales, bumps
cache-bust tokens (api.js 305, workspace.js v50, app.js 317, styles.css
312), and syncs frontend static tests plus new backend tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The web app depends on the optional `http` extra (Starlette and Uvicorn),
but neither the README nor the website documented it — users only learned
this from the runtime error. Add the install step to the README web
section, an Installation prerequisite to the Web App guide, and an
Optional Features table to the installation guide (covering http and a2a).
Mirror all additions across the six translated doc locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the English README change to all six translated READMEs: note
that the web app needs the `http` extra and add the
`pip install 'iac-code[http]'` step before `iac-code web`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…test

test_permission_answer_resolves_agent_loop_future asserts the full turn
event sequence ends with turn.done. New sessions schedule a fire-and-forget
LLM-title task that emits a trailing session.updated after turn.done,
polluting the assertion on slower (Windows) runners. Set
pending_llm_title=False after create_session to suppress it, matching the
isolation pattern already applied to test_runtime_messages.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@guima-why
guima-why merged commit 0b1a4ef into main Jul 30, 2026
15 checks passed
@guima-why
guima-why deleted the fix-web branch July 30, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant