Skip to content

fix(web-ui): expose center panel toggle in SceneBar - #2039

Open
xielixing wants to merge 14 commits into
GCWing:mainfrom
xielixing:fix/panel-layout-1970
Open

fix(web-ui): expose center panel toggle in SceneBar#2039
xielixing wants to merge 14 commits into
GCWing:mainfrom
xielixing:fix/panel-layout-1970

Conversation

@xielixing

Copy link
Copy Markdown

Summary

Exposes the existing center (chat) panel toggle in the SceneBar, enabling users to freely hide/show the center panel as requested in #1970.

Root Cause

The LayoutState already contained chatCollapsed and centerPanelCollapsed fields, and toggleChatPanel() already existed in useApp.ts (which properly force-opens the right panel when chat is hidden). SessionScene.tsx already handled the --editor-mode CSS class that promotes the AuxPane to full width when chat is collapsed.

However, none of these were ever exposed in the UI — toggleChatPanel was never called anywhere, PanelCenterIcon was never imported, and no keyboard shortcut was registered.

Changes

  • SceneBar.tsx: Added PanelCenterIcon toggle button between tabs and window controls, with tooltip (header.showChatPanel / header.hideChatPanel)
  • SceneBar.scss: Added __actions container and __panel-toggle button styles (hover, active, focus-visible)
  • WorkspaceBody.tsx: Pass onToggleChatPanel and chatCollapsed from useApp() to SceneBar
  • AppLayout.tsx: Added toggleChatPanel to destructured useApp() and registered Ctrl+Shift+\ keyboard shortcut

Validation

  • pnpm run type-check passes cleanly (exit 0) with no errors in changed files

Test Plan

  1. Open BitFun desktop app with a workspace
  2. Click the center panel toggle icon in the SceneBar — center chat panel should collapse, right panel expands to fill
  3. Click again — center chat panel should reappear
  4. Press Ctrl+Shift+\ — should toggle the center panel
  5. Verify the tooltip shows "Hide chat panel" when expanded and "Show chat panel" when collapsed

Closes #1970

xlx1212 and others added 14 commits July 31, 2026 16:06
Adds a probe-gated bridge to the external `loopx` CLI's issue-fix capability,
which supplies a deterministic decision skeleton (which route to take for an
issue, how a PR's lifecycle projects forward) while performing no writes of its
own. BitFun keeps every side effect and supplies every piece of evidence.

Behind a non-default `loopx-issue-fix` feature, deliberately outside
`product-full` until the chain is verified against a real repository.

Two behaviors were found by testing against the real CLI rather than assumed:

- LoopX reports domain refusals as `{"ok": false, "error": ...}` on stdout *and*
  exits nonzero. Parsing stdout before checking the exit status keeps the
  structured reason instead of reporting a bare exit code.
- `--validation-label` is required for the `fix_pr` route. Without a named
  validation surface LoopX downgrades to `triage_only` even when the context is
  grounded, the issue reproduces, and the scope is bounded.

Sets `PYTHONUTF8=1` on every invocation: LoopX's 123 subprocess call sites pass
`text=True` without `encoding=`, so on a non-UTF-8 locale it decodes `gh` output
as the local codepage and dies. The env var fixes all of them at once and needs
no patch to LoopX.

Contract tests drive the real CLI and skip cleanly when it is absent, matching
the runtime probe gate.

Co-Authored-By: Claude <noreply@anthropic.com>
Adds `list_issues`, closing the one real backend gap for automatic issue
fixing: the service could fetch a single issue by id but had no way to
discover which issues exist. Five `list_pull_requests` implementations
already existed; issues had no equivalent.

Returns a new lightweight `ReviewPlatformIssueSummary` rather than the
existing `ReviewPlatformIssueEvidence`, which carries a full body and every
comment — enumerating a hundred issues must not pull all of that.

Provider differences handled:

- GitHub returns pull requests inline from its issues endpoint, marked only
  by a `pull_request` member, so they are filtered out. Continuation is
  inferred from a full page because `gh` surfaces no Link headers here; the
  check runs before PR filtering, since a page of only PRs can still be
  followed by issues.
- GitLab addresses issues by project-scoped `iid`, not the global `id`, and
  has no "all" state literal — the filter is omitted entirely instead of
  sent empty. Continuation comes from its `x-next-page` header.

Takes a request struct because the sibling `issue` method already sits at
clippy's argument limit.

Covered by mocked-HTTP tests for the GitLab path and an ignored test that
drives the real `gh` CLI for GitHub, which mocks cannot reach.

Co-Authored-By: Claude <noreply@anthropic.com>
Adds `RepositoryContextBuilder`, the evidence half of the integration. LoopX
holds no code-reading ability and refuses to guess, so the quality of its route
decisions depends entirely on what BitFun reports here.

Every constraint LoopX's validator enforces is enforced at construction time
instead, because a rejected payload costs a whole subprocess round trip:
source-id shape and uniqueness, reference length, summary length measured after
whitespace collapsing, the 16-source cap, advisory-only trust for memory and
expert sources, and a pinned revision whenever a source claims current
freshness. References must be repository-relative — an absolute local path would
leak the operator's filesystem layout into a payload that can reach a public
issue thread.

`context_status` and `ungrounded_required_aspects` mirror LoopX's grounding rules
locally, so a caller can decide what else to read without paying for a
subprocess call. A contract test compares the prediction against the real CLI
aspect by aspect, which is what would catch the two drifting apart.

That comparison corrected a mistaken assumption of mine. I had assumed a partial
context caps the route at `triage_only`; it does not. Two contract tests now pin
the real behavior: a grounded context without `--validation-label` yields
`triage_only`, while a merely partial context *with* the label yields `fix_pr`.
LoopX distinguishes "which test files did you read" from "how will you check this
fix", and only the latter gates opening a PR. Comments and the design doc that
stated otherwise are corrected.

Moves the module into a directory to hold the new submodule.

Co-Authored-By: Claude <noreply@anthropic.com>
Runs one issue through feasibility → branch → PR lifecycle behind typed
outcomes. The value is in the typing: LoopX's decisive fields sit at
non-obvious paths, and reading them wrong is the failure mode that turns a
refusal into an approval.

Two paths were corrected by testing against the real CLI rather than assumed:
`state` lives under `observation`, and `state_bucket` under
`grouped_monitor_projection` — neither is top level, though the markdown
rendering shows them flattened. A unit test asserting the wrong shape would
have looked fine, so the mocked packets now match verified reality.

Unrecognized routes and lifecycle decisions are errors, never defaults.
Silently mapping an unknown value onto something permissive could open a pull
request LoopX had declined. Optional evidence still degrades to empty, since
guessing there is harmless.

`plan_issue` skips branch preparation entirely on a non-fix route. Under
`ExecutionMode::Execute` that would otherwise create a branch LoopX just
refused to justify, so the skip is a safety property.

`may_open_pull_request` requires the fix route, a ready review packet, and
passing validation together. The feature ships with no runtime kill switch, so
this gate lives on the action itself rather than relying on a disabled toggle.

Co-Authored-By: Claude <noreply@anthropic.com>
Adds `review_platform_list_issues` across the three layers the existing
`review_platform_get_issue` already spans: core facade wrapper, Tauri command,
and typed frontend binding.

The command takes an owned DTO because `ReviewPlatformListIssuesRequest`
borrows its strings and cannot be deserialized directly.

Co-Authored-By: Claude <noreply@anthropic.com>
Opens from a chat-header button into a right-panel tab: issue list on the left,
selected issue's detail on the right. Follows the pull-requests button and
`createReviewPlatformTab` for how the tab opens.

Row state lives in `issueFixRunState` as pure functions, so the mapping from
LoopX's decisions onto what a user sees is testable without rendering. The
mapping that matters: a `user_gate` renders as blocked, never as done, and
`nextIssueToRun` returns null while any row is blocked. Advancing past a gate
would defeat the gate — that is the one behavior LoopX raises it for.

Reason codes are shown verbatim rather than paraphrased, so a declined fix
explains itself in LoopX's own vocabulary.

The panel resolves `owner/repo`, host, and platform from the workspace's
selected remote, since the header only knows the local checkout path. Platform
is threaded through rather than hardcoded, so GitLab works too.

The error status key is `stopped`, not `failed`: the i18n audit tracks
`statuses.failed` as a shared term with a governance budget, and adding a 43rd
duplicate would have needed that budget raised. Renaming was the honest fix
rather than moving the baseline.

Co-Authored-By: Claude <noreply@anthropic.com>
Multi-issue runs need continuation, budgets, and human gates. BitFun already owns
all three in `thread_goal`, so this bridge adds none of its own — it only
translates. That is also why nothing here reaches for a scheduler or quota:
LoopX's issue-fix capability contributes neither, and its 35 modules import
neither.

The mapping that carries weight is `user_gate` → `Blocked`. `plan_serial_run`
additionally returns no next issue while a gate is open, since handing one back
would invite a caller to step over the gate rather than resolve it. `Blocked`
stays resumable, so answering the question picks the run back up instead of
stranding it.

`is_resumable` duplicates the agent-runtime predicate rather than pulling in that
crate for four lines. The test enumerates every `ThreadGoalStatus` variant, so a
new one has to be classified deliberately instead of silently defaulting to
non-resumable.

Note on clippy: adding the runtime-ports dependency brings one pre-existing
warning from that crate into this feature's build. It is not new code of mine.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Closes the gap my earlier summary glossed over: the panel could list and select
issues, but nothing connected it to the orchestrator, so no fix action was
reachable. A Start button now walks the selected issues serially through
`feasibility`, driving the row states that were already implemented and tested.

Planning only, verified rather than asserted: `ExecutionMode::Execute` has zero
production callers, and the Tauri surface mentions execute exactly once — in the
comment saying it has none. Nothing reachable from the UI can create a branch,
run a command, or open a pull request.

The run loop tracks state in a local variable rather than reading React state
back each iteration, which would lag a render behind and could re-run an issue.
It stops as soon as `nextIssueToRun` returns null, so an open gate halts the run
instead of being stepped over.

No repository context is generated yet, because nothing in BitFun generates one.
LoopX therefore reports `not_provided` and declines to open a pull request. That
is the honest current state, and its reason codes name exactly which evidence is
missing — better than asserting a validation surface nobody checked.

`loopx-issue-fix` is enabled for the desktop crate only, leaving `product-full`
untouched so release builds of other consumers are unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
…tion spawning

- Add loopx-issue-fix to the services-integrations product-full feature
  group after real-repo validation against GCWing/BitFun GCWing#1849
- Register the feature and its optional dependency owners in the core
  boundary rules so the product-full assembly stays explicit
- Wrap caller-declared validation commands with cmd /c on Windows:
  LoopX spawns them with subprocess.run(argv) and no shell, so .cmd shims
  such as pnpm fail with WinError 2
- Fix a pre-existing lib-test compile gap in plugin_source tests
- Record the real-repo verification and the Windows defect in the
  integration design doc
The start button used to only project LoopX routes; a fix_pr route had no
execution path, so no model was ever called. Wire the missing half:

- New issue_fix_execute Tauri command: feasibility gate, then submit the
  fix task as a dialog turn to the session's agent loop (same scheduling
  path as a manual message, so the model's streaming output appears in
  the chat transcript)
- Empty agent_type lets the coordinator resolve the session's own mode
  instead of overriding it
- Panel: plan first, then submit fix_pr issues to the agent; non-fix
  routes record their reason codes and move on
- Declare remote-workspace policies for all issue_fix commands and the
  pre-existing review_platform_list_issues gap
- Document the execution model in the integration design doc
…host loop

Replace the thread-goal bridge with a Kernel-owned autonomous loop: selected
issues become LoopX intake todos, and BitFun's persistent cron service wakes
one agent session every 10 minutes with an English host preamble plus the
LoopX `heartbeat-prompt --compact` contract (thin mode depends on skill
packs absent from BitFun sessions).

- Project user gates from `todo list` instead of the quota preview, which
  is compacted to two entries; issue-linked gates win, unlinked gates still
  surface so an open gate can never stall the loop invisibly.
- Add issue_fix_stop_autonomous (kill switch that also sweeps orphaned or
  duplicate jobs and survives a broken registry) and a quota-free
  issue_fix_autonomous_poll for the panel's 30s poll loop, since
  `quota should-run` appends a rollout event per call.
- Serialize start/stop/answer-wake on HOST_LOOP_LOCK, self-heal duplicate
  cron jobs, refresh the heartbeat prompt snapshot at every gate answer,
  and surface host-loop failures (lastError/consecutiveFailures) in the UI.
- Guard every panel state write with a monotonic ticket, pause polling
  during mutations, add a Stop button, re-project after failed gate
  answers, and collapse scheduled heartbeat turns into an expandable chip.
- Prune ~60 dead issue-fix locale keys; keep en-US/zh-CN/zh-TW in parity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	scripts/core-boundaries/rules/feature-rules.mjs
#	src/apps/desktop/Cargo.toml
#	src/crates/services/services-integrations/Cargo.toml
#	src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx
#	src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx
#	src/web-ui/src/flow_chat/components/modern/UserMessageItem.tsx
#	src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.tsx
Issue GCWing#1970 requested the ability to freely combine panels, specifically
hiding the center chat panel. The LayoutState already contained
chatCollapsed/centerPanelCollapsed fields and toggleChatPanel() existed in
useApp.ts, but neither was ever exposed in the UI.

This change:
- Adds a PanelCenterIcon toggle button in SceneBar with tooltip
- Passes toggleChatPanel and chatCollapsed from WorkspaceBody to SceneBar
- Registers Ctrl+Shift+\ keyboard shortcut in AppLayout
- Adds SceneBar SCSS styles for the toggle button

The existing SessionScene.tsx already handles the --editor-mode CSS class
that promotes AuxPane to full width when chat is hidden.
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.

[Feature]: 支持多栏的自由布局

1 participant