When the dropdown opens above the trigger button, use CSS bottom instead
of top to prevent a gap between the dropdown and the button when content
is shorter than maxHeight.
Co-Authored-By: claude-opus-4-6 <noreply@anthropic.com>
Only show Claude Official and ChatGPT Official model groups in the
runtime-scoped ModelSelector dropdown when the corresponding OAuth
session is active. This prevents users from selecting official models
they cannot actually use.
- Import useHahaOAuthStore and useHahaOpenAIOAuthStore into ModelSelector
- Fetch OAuth status on mount
- Pass login flags to buildProviderChoices to conditionally include sections
- Add test case verifying hidden sections when not logged in
Co-Authored-By: claude-opus-4-6 <noreply@anthropic.com>
Bring the detached-worktree desktop Writer preview patch into the canonical local main checkout so long pending Write calls show visible progress from main.
Constraint: Local main had advanced beyond the detached worktree base, so this landing keeps an explicit merge boundary.
Confidence: high
Scope-risk: moderate
Directive: Preserve bounded pending Writer rendering; do not replace it with live diff rendering without performance evidence.
Tested: bun run check:desktop in detached worktree before merge.
Not-tested: Post-merge desktop gate before this merge commit was created.
Long Write tool inputs can spend minutes streaming the file body before the tool executes, so the desktop transcript now decodes the pending content field into a lightweight Writer preview. The preview intentionally uses a bounded plain-text window while the tool is pending, then keeps the existing full diff rendering once the Write call completes.
Constraint: Write.content arrives as streaming tool input before the filesystem write executes.
Rejected: Render a live DiffViewer for every input delta | repeated diff and syntax work would reintroduce long-session jank.
Confidence: high
Scope-risk: moderate
Directive: Keep pending Writer rendering lightweight and bounded; reserve full diff rendering for completed Write calls.
Tested: cd desktop && bun run test src/components/chat/chatBlocks.test.tsx -- --runInBand
Tested: cd desktop && bun run test src/stores/chatStore.test.ts -- --runInBand
Tested: bun run check:desktop
Not-tested: Live provider long Write smoke after the UI patch.
Desktop AskUserQuestion waits on the SDK permission bridge, and a transient renderer disconnect could previously let the short no-client cleanup kill the CLI. That turned a live user question into an AbortError and left history restore showing a stale interactive card.
Track live SDK permission requests, replay them to reconnecting clients, and give sessions waiting on user input a longer cleanup grace. Render aborted terminal results as completed history instead of an unanswered prompt.
Constraint: SDK permission requests are live in-memory control messages until answered or aborted.
Rejected: Only de-dupe history cards in MessageList | would not keep the CLI alive or restore the active permission request.
Rejected: Disable disconnect cleanup globally | would leak abandoned CLI processes for ordinary disconnected sessions.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Do not shorten pending-permission cleanup without testing AskUserQuestion reconnect and abort paths.
Tested: bun run check:desktop
Tested: bun run check:server
Tested: git diff --check
Not-tested: Live Sub2API provider reconnect repro.
Large desktop sessions were still paying too much render and paint cost on tab switch, while the previous content-visibility approach could defer WebView painting enough to show white gaps during scroll. This moves long transcripts to a measured dynamic-height window, keeps small transcripts fully mounted, and caches completed Markdown parses so remounted assistant messages do not pay repeated parse cost.
Constraint: Tauri/WebView must not depend on content-visibility for transcript correctness because deferred offscreen paint can surface as blank regions
Rejected: Keep content-visibility:auto on every chat item | it improves synthetic DOM cost but matches the user's white-gap failure mode
Rejected: Fixed-height transcript virtualization | variable-height chat rows and tool groups can create spacer gaps and broken scroll positions
Confidence: high
Scope-risk: moderate
Directive: Do not reintroduce deferred-paint transcript optimization without testing the real Tauri/WebView scroll path on large sessions
Tested: bun run check:desktop
Tested: Chrome DevTools real-session smoke for 055015e4-6093-418e-9944-19bba25ecb69, switch trace max RunTask about 158ms and scroll blankSamples=0 across 34 sampled positions
Not-tested: Packaged Tauri binary smoke on WebKit/WebView2
Bring the validated detached-worktree desktop performance patch into the canonical local main checkout so long-session tab and scroll fixes are available from the main working copy.
Constraint: Local main had advanced independently beyond the detached worktree base, so fast-forward was not the correct landing shape.
Confidence: high
Scope-risk: moderate
Directive: Verify ancestry for detached-worktree landings because main may advance after the feature worktree is created.
Tested: bun run check:desktop in detached worktree before merge.
Not-tested: Post-merge desktop gate before this merge commit was created.
Long transcripts were triggering avoidable layout work during tab switches and auto-scroll because MessageList read scroll geometry while the browser still had a large message tree to lay out. The tab bar also subscribed to full chat session state, so streaming payload churn could invalidate tab chrome even when running status did not change.
This keeps variable-height transcript rows fully mounted, adds browser offscreen rendering hints, scrolls to bottom through clamped large offsets instead of synchronous geometry reads, and narrows TabBar's chat-store subscription to running-state ids.
Constraint: Prior fixed-height virtualization left blank spacer gaps for variable-height chat rows.
Rejected: Reintroduce fixed-height virtualization | would regress existing long transcript rendering behavior.
Confidence: high
Scope-risk: moderate
Directive: Do not replace this with fixed row-height virtualization without measured row heights and scroll-restore coverage.
Tested: bun run check:desktop
Tested: Chrome DevTools trace on real session 055015e4-6093-418e-9944-19bba25ecb69 with INP 84ms to 43ms and ForcedReflow insight removed.
Not-tested: Packaged Tauri WebView runtime beyond the local web UI trace.
Bring the detached worktree fix back to local main so H5 QR and copied LAN links no longer keep stale sidecar ports after app restarts.
Constraint: Local main already had newer unrelated work after the detached worktree base.
Confidence: high
Scope-risk: narrow
Directive: Keep H5 LAN URL host preservation separate from reverse proxy URL preservation.
Tested: bun test src/server/__tests__/h5-access-service.test.ts
Tested: bun test src/server/__tests__/h5-access-api.test.ts src/server/__tests__/h5-access-auth.test.ts src/server/__tests__/h5-access-policy.test.ts
Tested: bun run check:server
Not-tested: Full desktop packaged build after merge.
H5 access can preserve a manually selected private LAN host, but the desktop sidecar binds a fresh dynamic port on restart. Refresh plain HTTP private-LAN URLs to the current auto-discovered port while leaving reverse proxy URLs intact.
Constraint: Desktop sidecar ports are dynamic across app launches.
Rejected: Preserve the complete private-LAN URL unchanged | this keeps stale ports like 5179 in QR links after restart.
Confidence: high
Scope-risk: narrow
Directive: Do not treat plain private-LAN H5 URLs as reverse proxy URLs unless they carry a custom scheme or path.
Tested: bun test src/server/__tests__/h5-access-service.test.ts
Tested: bun test src/server/__tests__/h5-access-api.test.ts src/server/__tests__/h5-access-auth.test.ts src/server/__tests__/h5-access-policy.test.ts
Tested: bun run check:server
Tested: manual smoke with stored :5179 returning the active :45679 publicBaseUrl
Not-tested: Real phone scan against a packaged desktop build after reinstall.
Desktop already receives tool_use start and input deltas before the final tool_use_complete event, but the chat transcript only rendered a visible tool card after the complete event. This makes long Write payloads look frozen while the model is still generating the tool input. The store now upserts a pending tool_use message on stream start, updates a lightweight input preview from deltas, and resolves that same message when the complete event arrives.
Constraint: Long Write calls spend most perceived time streaming JSON tool input, not executing the filesystem write
Rejected: Show only the global active tool label | it does not anchor progress in the transcript where the user is looking
Confidence: high
Scope-risk: moderate
Directive: Keep pending tool_use messages keyed by toolUseId so the complete event updates in place instead of duplicating cards
Tested: bun run check:desktop
Not-tested: Live provider smoke with a real 10k-word Write stream
Bring the IM streaming repair from the ea41 worktree into local main so Telegram keeps receiving live content when the desktop opens the same active session. The merged fix also accumulates Telegram thinking deltas instead of replacing the preview with the latest short chunk.
Constraint: Local main already contains independent worktree label cleanup changes, so this merge preserves both histories
Rejected: Cherry-pick the fix | the user asked to merge the worktree back into local main
Confidence: high
Scope-risk: moderate
Directive: Keep multi-client session streaming covered before changing WebSocket output callback ownership
Tested: bun run check:server (828 pass); bun run check:adapters (358 pass); git diff --check
Not-tested: Live Telegram Bot API smoke against the packaged app
Telegram and desktop can attach to the same session while a turn is streaming. The WebSocket handler now tracks output callbacks per client and broadcasts session messages instead of replacing the previous subscriber. Telegram thinking deltas are accumulated before editing the placeholder so the preview does not collapse to the latest tiny chunk.
Constraint: Desktop and IM adapters may observe the same active session concurrently
Rejected: Keep a single session output callback | a later desktop view can steal Telegram's live content and completion events
Confidence: high
Scope-risk: moderate
Directive: Do not collapse session output callbacks back to one callback without a multi-client streaming regression
Tested: bun run check:server (828 pass); bun run check:adapters (358 pass); git diff --check
Not-tested: Live Telegram Bot API smoke against the packaged app
Bring the detached worktree fix into the canonical local checkout so desktop worktree sessions hide internal branch labels while preserving git-info metadata separation.
Constraint: User requested local main integration, not push or PR
Confidence: high
Scope-risk: narrow
Tested: Clean local main worktree before merge; feature commit contains server and desktop verification evidence
Not-tested: Full quality gate after merge
Desktop worktree sessions should show the source project and worktree marker without surfacing implementation refs like worktree-desktop-* as user-facing branch state. The git-info response now keeps launch branch metadata separate from worktree identity, and the desktop chip hides branch and slug labels in isolated worktree mode.
Constraint: Git worktrees need an internal branch for isolated execution, but that ref is product plumbing rather than useful UI context
Rejected: Show both launch branch and worktree slug | duplicated noisy identifiers and confused the session location
Confidence: high
Scope-risk: narrow
Tested: bun test src/server/__tests__/sessions.test.ts -t "git-info"; cd desktop && bun run test src/components/shared/ProjectContextChip.test.tsx; bun run check:server; bun run check:desktop; git diff --check
Not-tested: Live desktop screenshot smoke
Merge the worktree fix that preserves CLI AskUserQuestion toolUseResult metadata across history replay and live WebSocket translation, then filters stale unresolved cards when no pending permission is active.
Constraint: main had advanced past the worktree base, so this requires a non-fast-forward local merge.
Confidence: high
Scope-risk: moderate
Directive: Keep AskUserQuestion rendering driven by CLI transcript metadata instead of desktop-only lifecycle inference.
Tested: git merge-tree 3f384a1d5f5f48f0c7eb848b2dfacb4527fd5059 main 853e3db6ecbeb9a39f333c65aa7eddc32d613d7b
Tested: worktree commit already passed server focused tests, desktop focused tests, check:server, desktop lint, desktop build, and git diff --check.
Not-tested: Re-running full verification after the merge commit.
The CLI transcript already records each AskUserQuestion once, but the desktop bridge dropped the structured toolUseResult answers from both history replay and live WebSocket tool results. That made answered question cards look unresolved and allowed stale cards to reappear during the gap before the next permission request.
Constraint: Desktop should render the CLI question stream directly without inventing separate question lifecycle state.
Rejected: Hide all previous AskUserQuestion cards after submit | would remove resolved answer history instead of preserving transcript context.
Confidence: high
Scope-risk: moderate
Directive: Preserve toolUseResult metadata when adapting CLI transcript entries; AskUserQuestion answer state depends on the structured answers object, not the prose tool_result string.
Tested: bun test src/server/__tests__/sessions.test.ts --timeout 30000
Tested: bun test src/server/__tests__/ws-memory-events.test.ts --timeout 30000
Tested: cd desktop && bun run test src/stores/chatStore.test.ts
Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx
Tested: bun run check:server
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: git diff --check
Not-tested: Full bun run verify and live desktop browser smoke.
OpenAI-compatible streaming requests previously used the shared AI request timeout as a full-body AbortSignal. That made long SSE generations stop mid-answer after the configured timeout even when the upstream had already started streaming.
The proxy now applies the configured timeout only while opening streaming upstream requests and keeps non-streaming requests on the existing full-request timeout.
Constraint: The global AI request timeout is documented as covering provider requests and streaming first responses, not total stream duration.
Rejected: Increase the default timeout | long plan-mode answers can still exceed any fixed full-stream limit.
Confidence: high
Scope-risk: narrow
Tested: bun test src/server/__tests__/proxy-network-settings.test.ts
Tested: bun run check:server
Bring in the selected-workspace filesystem access fix from the detached
investigation worktree so Windows projects outside the user home can use
desktop @ file search.
Constraint: Preserve local main history while landing the detached worktree fix
Confidence: high
Scope-risk: moderate
Tested: bun test src/server/__tests__/filesystem.test.ts
Tested: bun run check:server
Not-tested: Windows packaged desktop smoke after merge
Desktop file mentions should follow the workspace the user opened, but
the filesystem browser only trusted the home and temp roots. Register
workspace roots after repository context, session creation, and session
git-info resolution so Windows projects on another drive can be searched
without turning the browse API into arbitrary disk access.
Constraint: Windows users can open repositories outside C:\\Users, such as D:\\workspace\\code\\cc-haha
Constraint: Filesystem browse must not become an unbounded local disk reader
Rejected: Add a global user-configurable filesystem whitelist | broader product and persistence surface than this bug needs
Rejected: Allow every requested browse path | would bypass the intended filesystem boundary
Confidence: high
Scope-risk: moderate
Directive: Register only workspace roots that the server has already resolved through session or repository flows
Tested: bun test src/server/__tests__/filesystem.test.ts
Tested: bun run check:server
Not-tested: Windows packaged desktop smoke
Manual H5 LAN hosts should survive auto LAN discovery, because the detected adapter can be a WSL or Docker virtual interface instead of the physical network reachable by a phone. The settings UI now asks for the host or IP separately and reuses the current service port for the normal LAN workflow, while still accepting full URLs for reverse proxy setups.
Constraint: H5 auto discovery may see multiple private IPv4 adapters on Windows hosts.
Rejected: Treat all private URLs as stale auto-discovery output | this overwrites intentional LAN choices.
Rejected: Let users manually type the port for the normal LAN path | the server already owns the bound port and the UI can reuse it.
Confidence: high
Scope-risk: moderate
Tested: bun test src/server/__tests__/h5-access-service.test.ts
Tested: bun test src/server/__tests__/h5-access-api.test.ts src/server/__tests__/h5-access-auth.test.ts src/server/__tests__/h5-access-policy.test.ts
Tested: bun run check:server
Tested: bun run check:desktop
Not-tested: Real Windows plus WSL/Docker plus phone LAN smoke.
Bring the desktop compact state repair from the validation worktree into
local main after focused, full local, packaged-app, and coverage checks.
Constraint: Local main was clean and shared the same base commit as the validation worktree.
Confidence: high
Scope-risk: moderate
Directive: Keep compact status null handling and tail compact-card cleanup together.
Tested: bun run check:desktop
Tested: bun run check:server
Tested: bun run check:policy
Tested: bun run check:native
Tested: bun run check:coverage
Tested: SKIP_INSTALL=1 PRESERVE_TAURI_TARGET=1 ./desktop/scripts/build-macos-arm64.sh
Not-tested: Full live auto-compact threshold trigger in packaged app; Tauri sidecar did not inherit CLAUDE_AUTOCOMPACT_PCT_OVERRIDE during manual smoke.
Related: cb922b01
Desktop compact events can arrive as a start status followed by a null
CLI status or by a summary boundary. Preserve the visible transcript while
making either completion path clear transient compacting UI so the session
cannot remain stuck after auto-compact or cancellation.
Constraint: Desktop receives compact state through translated CLI SDK status events.
Rejected: Collapse old transcript into a single compact card | hides useful history and caused issue #568.
Confidence: high
Scope-risk: moderate
Directive: Do not drop status=null compact exits without validating desktop chat state transitions.
Tested: bun run check:desktop
Tested: bun run check:server
Tested: bun run check:policy
Tested: bun run check:native
Tested: bun run check:coverage
Tested: SKIP_INSTALL=1 PRESERVE_TAURI_TARGET=1 ./desktop/scripts/build-macos-arm64.sh
Tested: Real packaged macOS app with GPT-5.5 completed two live turns without staying in running state.
Not-tested: Full live auto-compact threshold trigger in packaged app; Tauri sidecar did not inherit CLAUDE_AUTOCOMPACT_PCT_OVERRIDE during manual smoke.
Related: https://github.com/NanmiCoder/cc-haha/issues/567
Related: https://github.com/NanmiCoder/cc-haha/issues/568
Computer Use now rejects Python runtimes below 3.9 before running pip,
keeps Pillow on the Python 3.9-compatible 11.x line, and falls back from
the configured mirror to the default PyPI index when dependency installs
fail.
Constraint: Pillow 11.3 requires Python 3.9+, while user machines may resolve a different Python than expected
Rejected: Downgrade Pillow to the Python 3.8-compatible 10.x line | current product direction assumes modern Python installs
Confidence: high
Scope-risk: narrow
Directive: Keep runtime package ranges aligned with the setup Python minimum before changing either side
Tested: bun test src/server/__tests__/computer-use-api.test.ts src/server/__tests__/computer-use-requirements.test.ts src/server/__tests__/computer-use-python.test.ts src/utils/computerUse/pipInstall.test.ts
Tested: bun run check:server
Tested: bun run check:coverage
Tested: bun run check:policy
Not-tested: Real Windows 10 machine dependency install against the reported user environment
Bring the detached worktree fix back to the local main branch so modern WebUI keeps Shiki rendering while legacy WebKit can fall back before loading Shiki.
Constraint: Local main already contains unreleased commits ahead of origin/main.
Confidence: high
Scope-risk: moderate
Tested: Worktree commit passed focused CodeViewer test, desktop lint, desktop build, startup bundle scan, and real WebUI Shiki smoke before merge.
Not-tested: Re-running the full desktop gate after merge.
Code blocks should keep the richer Shiki rendering in modern WebUI while avoiding the old WebKit startup crash path by loading Shiki lazily and falling back to Prism only when the runtime cannot safely parse or execute it.
Constraint: Issue #461 is triggered by older Safari/WebKit parsing modern RegExp syntax during startup.
Rejected: Replace Shiki entirely with Prism | visual quality regressed for normal WebUI usage.
Confidence: high
Scope-risk: moderate
Directive: Keep Shiki out of the startup path unless macOS 12 WebView compatibility is re-verified.
Tested: cd desktop && bun run test -- --run src/components/chat/CodeViewer.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: Real WebUI smoke on http://127.0.0.1:1420/ verified six code blocks rendered with data-highlight-engine=shiki.
Not-tested: Physical macOS 12 WebView runtime.
Related: https://github.com/NanmiCoder/cc-haha/issues/461
The Telegram adapter fix was developed in a detached worktree from a shared base while local main already contained newer desktop fixes. Merge the narrow worktree commit into main so the local checkout keeps those existing commits and gains the Telegram streaming behavior.
Constraint: Local main had already advanced independently after the worktree base.
Rejected: Rebase local main onto the worktree commit | rewrites the user's local main history unnecessarily.
Confidence: high
Scope-risk: narrow
Directive: Keep this as a local integration merge; do not push unless explicitly requested.
Tested: bun run check:adapters
Tested: cd desktop && bun run build:sidecars
Not-tested: Live Telegram Bot API run with a real bot token.
Telegram Bot API text edits share the same practical message length ceiling as sends, so streaming one growing placeholder can stop exposing new assistant output once the text approaches the limit. The adapter now seals full chunks, opens a fresh editable placeholder, and routes Telegram outbound text through a table-to-bullet formatter for phone readability.
Constraint: Telegram text messages and edits must stay below the platform message limit.
Rejected: Wait until message_complete to split the answer | users still lose streaming visibility during long replies.
Confidence: high
Scope-risk: narrow
Directive: Keep Telegram-specific streaming state in adapters/telegram/format.ts instead of spreading chunk math across message handlers.
Tested: bun test adapters/telegram/__tests__/telegram.test.ts adapters/common/__tests__/format.test.ts
Tested: cd adapters && bunx tsc --noEmit
Tested: bun run check:adapters
Tested: cd desktop && bun run build:sidecars
Not-tested: Live Telegram Bot API run with a real bot token.
Bring the issue #539 fix from the detached Codex worktree into the canonical local main checkout so desktop sessions in materialized worktrees report the actual checked-out branch.
Constraint: Local main had advanced independently, so this could not fast-forward cleanly.
Confidence: high
Scope-risk: narrow
Tested: bun test src/server/__tests__/sessions.test.ts -t "git-info"
Tested: /tmp/cc-haha-issue-539-verify API reproduction returned the worktree branch
Tested: bun run check:server
Desktop git-info preserved the launch branch before checking the active worktree cwd, so materialized isolated worktree sessions could keep showing the source branch instead of the branch Git had checked out in the worktree. The API now switches to the real cwd branch only once the session has actually entered its planned worktree, while direct branch launches still keep their stable launch branch.
Constraint: Direct branch launches still need stable launch metadata when the source checkout later changes.
Rejected: Always prefer git rev-parse output | would regress direct launch sessions that intentionally show the selected branch.
Confidence: high
Scope-risk: narrow
Tested: bun test src/server/__tests__/sessions.test.ts -t "git-info"
Tested: /tmp/cc-haha-issue-539-verify API reproduction returned the worktree branch
Tested: bun run check:server
Desktop AskUserQuestion could show duplicate unresolved cards when restored history and a live permission request overlapped. The custom response field was also single-line, making long restored-context answers hard to read or edit.
This keeps the active unresolved AskUserQuestion card aligned with the live permission request, preserves resolved history, and switches custom responses to a multiline editor with Ctrl/Cmd+Enter submission.
Constraint: Desktop issue reports #542 and #543 both hit the AskUserQuestion chat surface.
Rejected: Hide all AskUserQuestion history while pending | resolved answers should remain visible in the transcript.
Confidence: high
Scope-risk: narrow
Tested: cd desktop && bun run test src/components/chat/AskUserQuestion.test.tsx
Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: bun run check:desktop
Not-tested: Live Windows desktop manual restore flow from the issue attachment.
ChatGPT Codex requires streamed Responses API requests, including hidden non-chat paths such as title generation. Keep the OAuth adapter streaming upstream while preserving JSON responses for non-streaming Anthropic callers, and route ChatGPT Official titles through the OAuth Codex endpoint.
Constraint: ChatGPT Codex rejects non-streaming requests with HTTP 400
Rejected: Disable AI title generation for ChatGPT OAuth | would leave first-session metadata degraded
Confidence: high
Scope-risk: narrow
Tested: bun test src/services/openaiAuth/fetch.test.ts
Tested: bun test src/server/__tests__/title-service.test.ts
Tested: bun run check:server
Tested: git diff --check
Not-tested: Live ChatGPT OAuth account round trip after remote build
This records the v0.2.9 release notes and aligns desktop package,
Tauri, and Cargo metadata so the release script can create the final
annotated tag after manual validation.
Constraint: Release tag creation is intentionally deferred for manual smoke validation.
Rejected: Run scripts/release.ts now | it would create the release commit and annotated tag before manual review.
Confidence: high
Scope-risk: narrow
Directive: Do not create v0.2.9 tag until the manual release check is complete.
Tested: bun run scripts/release.ts 0.2.9 --dry
Tested: bun run check:native
Not-tested: Full bun run verify and live ChatGPT Official OAuth smoke
ChatGPT Official runs through Codex OAuth, so context accounting should follow
the Codex model catalog and its effective context window instead of public API
limits. This keeps GPT-5.5 at the same usable window shown by Codex while
preserving GPT-5.4's larger Codex window.
Constraint: Codex OAuth model metadata applies effective_context_window_percent to raw catalog windows
Rejected: Use public API GPT-5.5 1.05M context | Codex app and OAuth runtime clamp this model to the smaller effective window
Confidence: high
Scope-risk: narrow
Directive: Keep ChatGPT Official context values aligned to Codex model catalog behavior, not generic OpenAI API docs
Tested: bun test src/services/openaiAuth/models.test.ts src/services/compact/autoCompact.test.ts src/server/__tests__/providers.test.ts
Tested: bun run check:server
Tested: git diff --check
Not-tested: Manual desktop screenshot after rebuilding the app
CLI retry events already include retry attempts, delay, and upstream status, but
the desktop WebSocket translation dropped api_retry system messages. The desktop
chat now keeps the current retry state in session state and renders a compact
active-turn indicator with attempt count, HTTP status, and countdown.
Constraint: Desktop users need visible feedback during provider retry waits after auth or network failures.
Rejected: Add retry rows to the transcript | retry heartbeats would clutter chat history instead of describing the active turn.
Confidence: high
Scope-risk: moderate
Directive: Keep api_retry as active-turn state; do not persist retry heartbeats into transcript history without a product decision.
Tested: bun test src/server/__tests__/ws-memory-events.test.ts
Tested: cd desktop && bun run test src/stores/chatStore.test.ts src/components/chat/MessageList.test.tsx
Tested: cd desktop && bun run lint
Tested: bun run check:server
Tested: Browser smoke with temp CLAUDE_CONFIG_DIR and mock OpenAI-compatible 503 endpoint, screenshot /tmp/cc-haha-retry-ui.png
Not-tested: Live ChatGPT logout retry path against OpenAI production auth.
Restructure the repository guidance around agent behavior, project surfaces, and verification routing so future coding agents can choose fast inner-loop checks without losing the PR and release quality gates.
Constraint: AGENTS.md wording is covered by the feature quality contract tests.
Constraint: Release workflow currently builds from tags and is not a substitute for local release verification.
Rejected: Keep the old command inventory with appended guidance | it preserved duplication and left verification budgeting ambiguous.
Confidence: high
Scope-risk: narrow
Directive: Keep future guidance tied to real package scripts and workflow behavior.
Tested: bun test scripts/pr/quality-contract.test.ts
Tested: bun run check:policy
Not-tested: Full bun run verify, documentation-only guidance change.
The WebSocket runtime treated every active provider id as stale unless it existed in the saved custom-provider list. ChatGPT Official is an in-memory built-in provider, so restarting the desktop could activate Claude Official and rewrite the provider index back to null before a new session launched. Keep built-in OpenAI provider ids valid in runtime validation, while preserving the stale custom-provider cleanup path.
This also keeps the General network timeout UI aligned with the authoritative timeout behavior by allowing precise typed values and updating the user-facing hint.
Constraint: ChatGPT Official is a built-in provider id and is not stored in the custom providers array.
Rejected: Persist ChatGPT Official as a synthetic saved provider | it would mix token-backed built-ins with user-managed providers and complicate secret persistence.
Confidence: high
Scope-risk: moderate
Directive: Runtime provider validation must include built-in provider ids as well as saved custom provider ids.
Tested: bun test src/server/__tests__/conversations.test.ts -t "preserve ChatGPT Official"
Tested: bun test src/server/__tests__/conversations.test.ts -t "stale persisted|preserve ChatGPT Official"
Tested: bun run check:server
Tested: bun run check:desktop
Not-tested: Full bun run verify after the final interruption; user explicitly asked to stop verification and commit directly.
Not-tested: Manual desktop restart with a real ChatGPT OAuth account in the packaged app.
Non-stream OpenAI-compatible proxy requests still kept the previous 300s timeout floor after the unified network settings merge. That made lower General AI timeout settings ineffective for normal proxy traffic, even though the setting is meant to be the final provider request timeout.
Constraint: Unified network settings must remain the final timeout and proxy override for provider protocol paths.
Rejected: Keep the 300s non-stream floor | it makes low timeout settings ineffective for normal proxy requests.
Confidence: high
Scope-risk: narrow
Directive: Do not reintroduce per-protocol timeout floors without updating the General network settings contract and tests.
Tested: bun test src/server/__tests__/proxy-network-settings.test.ts
Tested: bun test src/server/__tests__/conversation-service.test.ts src/server/__tests__/network-settings.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/proxy-network-settings.test.ts
Tested: bun run check:coverage
Tested: bun run verify
Not-tested: Live provider request against a real upstream proxy.
The worktree implementation adds one General settings surface for AI request timeout and proxy configuration. This merge keeps main's newer attribution-signing and ChatGPT Official provider paths while preserving the unified network override across CLI sessions, provider checks, and OpenAI-compatible proxy forwarding.
Constraint: Main had newer attribution and official-provider changes touching the same server integration points
Rejected: Prefer the worktree proxy body verbatim | it would drop main's signed CCH attribution wrapper
Rejected: Keep provider preset timeouts above General | it would violate the single timeout control requirement
Confidence: high
Scope-risk: moderate
Directive: General network settings must remain the final AI request timeout and proxy override for every provider protocol
Tested: bun test src/server/__tests__/conversation-service.test.ts src/server/__tests__/network-settings.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/proxy-network-settings.test.ts
Tested: cd desktop && bun run test -- --run src/stores/settingsStore.test.ts src/__tests__/generalSettings.test.tsx
Not-tested: Full bun run verify after resolving main merge conflicts
OpenAI returns token-exchange failures at the OAuth boundary, so the desktop ChatGPT login path should send the same Codex-compatible headers as the reference implementation and preserve enough sanitized response detail to diagnose upstream rejections.
Constraint: OpenAI authorization codes and token responses are sensitive and must not be logged verbatim.
Rejected: Retry the pasted authorization code during debugging | the code is single-use sensitive material and the next generated code gives cleaner evidence.
Confidence: medium
Scope-risk: narrow
Directive: Keep token exchange diagnostics sanitized; do not include raw OAuth codes, refresh tokens, access tokens, or id tokens in errors.
Tested: bun test src/services/openaiAuth/client.test.ts
Tested: bun test src/server/__tests__/haha-openai-oauth-service.test.ts src/server/__tests__/haha-openai-oauth-api.test.ts src/services/openaiAuth/fetch.test.ts src/services/openaiAuth/storage.test.ts
Tested: bun run check:server
Tested: bun run verify
Not-tested: Full successful live OAuth exchange with a fresh user authorization code.
OpenAI rejects Codex OAuth authorize URLs when the redirect URI uses the desktop API server's dynamic port. The desktop ChatGPT Official flow now starts a temporary Codex callback listener and generates the authorize URL with that callback port, while keeping token exchange and storage in the existing desktop OAuth service.
Constraint: OpenAI Codex OAuth client accepts the Codex-compatible localhost callback shape, not arbitrary desktop API ports.
Rejected: Keep routing the callback through the desktop server port | that fails before the browser can return an authorization code.
Confidence: high
Scope-risk: narrow
Directive: Do not put non-Codex query parameters or dynamic desktop ports into the OpenAI authorize URL without live authorization-page verification.
Tested: bun test src/server/__tests__/haha-openai-oauth-service.test.ts src/server/__tests__/haha-openai-oauth-api.test.ts
Tested: bun test src/services/openaiAuth/client.test.ts src/services/openaiAuth/fetch.test.ts src/services/openaiAuth/storage.test.ts
Tested: bun run check:server
Tested: bun run check:coverage
Tested: bun run verify
Not-tested: Completing a real account OAuth approval in the browser after the fix.
Local main had advanced independently with OpenAI OAuth URL handling, while the detached worktree fixed stale cc-haha provider env for direct TUI launches. Merge the verified worktree commit so source and desktop-terminal TUI paths both derive provider runtime env from the active provider index.
Constraint: Local main is the canonical checkout for this repository
Rejected: git merge --ff-only 3a42ca20 | local main and the detached worktree had one commit of independent divergence
Confidence: high
Scope-risk: moderate
Directive: Keep provider runtime env changes represented on local main before any release or PR work
Tested: bun test src/server/__tests__/provider-runtime-env.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/conversation-service.test.ts
Tested: bun run check:server
Not-tested: Live provider request from embedded terminal TUI after the local-main merge
Direct TUI launches read cc-haha managed settings without the desktop host's session-env injection, so a stale proxy URL could survive after switching to an Anthropic-compatible provider. The provider runtime env calculation now has one shared source of truth and CLI config loading recomputes managed provider env from the active provider index before applying it.
Constraint: Direct source-based TUI usage must keep working with .env when no desktop provider is configured
Rejected: Inject desktop runtime only from the embedded terminal shell | native terminals and source TUI launches would still read stale managed settings
Confidence: high
Scope-risk: moderate
Directive: Keep provider env derivation shared between ProviderService and CLI managedEnv loading
Tested: bun test src/server/__tests__/provider-runtime-env.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/conversation-service.test.ts
Tested: bun run check:server
Not-tested: Live provider request from embedded terminal TUI
OpenAI rejects the authorize URL before callback when non-Codex query
parameters are sent. The ChatGPT OAuth flow now matches the sub2api
Codex-compatible shape by keeping originator out of the authorize URL
and using OpenAI-specific hex state and PKCE verifier values.
Constraint: The Codex OAuth client expects the standard authorize query shape used by sub2api.
Rejected: Keep originator=opencode in the OAuth URL | originator is a later ChatGPT API request header concern, not an OAuth authorize parameter.
Confidence: high
Scope-risk: narrow
Tested: bun test src/services/openaiAuth/client.test.ts src/server/__tests__/haha-openai-oauth-service.test.ts src/server/__tests__/haha-openai-oauth-api.test.ts src/services/openaiAuth/fetch.test.ts src/services/openaiAuth/storage.test.ts
Tested: git diff --check
Tested: bun run check:server
Not-tested: Live browser OAuth authorization against a real OpenAI account.
Direct native CLI launches already inherit their user's shell environment, so recapturing a login interactive shell from the same TTY is unnecessary and can interfere with prompt input on macOS terminals.
The desktop and non-interactive subprocess paths still keep the shell-env bridge because they do not own the user's interactive terminal.
Constraint: Windows was already excluded from terminal shell env capture, which explains why v0.2.8 remained healthy there.
Rejected: Disable terminal shell env capture globally | desktop subprocesses still need the captured login-shell environment.
Confidence: high
Scope-risk: narrow
Directive: Do not re-enable interactive shell env capture for direct TTY launches without a native PTY input regression test.
Tested: bun test src/utils/terminalShellEnvironment.test.ts src/utils/mcpStdioEnvironment.test.ts
Tested: Native PTY smoke confirmed typed characters appear in claude-haha prompt
Tested: SKIP_INSTALL=1 ./desktop/scripts/build-macos-arm64.sh
Not-tested: Full bun run verify was intentionally not completed after user requested only a local macOS build
Desktop users need to see automatic context compaction as a transient transcript state instead of a stray stdout bubble or large card. The UI now collapses prior visible content into a compact timeline divider, shows the compacting phase, then updates the same divider with the completed summary affordance.
Constraint: CLI compaction emits both status and synthetic transcript artifacts that must be normalized before rendering
Rejected: Keep the existing small pill marker | it did not show the in-progress state and looked like chat content
Confidence: high
Scope-risk: moderate
Directive: Do not render local compact stdout as a user-visible chat message
Tested: cd desktop && bun run test -- src/components/chat/MessageList.test.tsx src/stores/chatStore.test.ts
Tested: bun test src/server/__tests__/ws-memory-events.test.ts
Tested: bun run check:desktop
Not-tested: full root bun run verify after the final UI iteration
Desktop transcript messages already expose copy controls in the shared action row. The fork affordance was rendered as its own zero-height hover row, which left the icon below Copy and made the action feel detached from the message toolbar. Moving the fork button into the existing action bar keeps the controls aligned while preserving the original branch-session API path.
Constraint: Desktop message actions should stay visually grouped for both user and assistant messages
Rejected: Keep a separate fork-only hover row | it reproduces the two-line layout regression shown in the desktop UI
Confidence: high
Scope-risk: narrow
Directive: Keep future per-message actions inside MessageActionBar unless they need a different interaction surface
Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx --pool forks --poolOptions.forks.singleFork=true
Tested: cd desktop && bun run test src/i18n/index.test.tsx --pool forks --poolOptions.forks.singleFork=true
Tested: cd desktop && bun run lint
Tested: bun run check:desktop
Not-tested: Full live desktop backend fork-click E2E