fix(markdown-editor): preserve LaTeX backslashes for KaTeX rendering - #2042
Open
xielixing wants to merge 14 commits into
Open
fix(markdown-editor): preserve LaTeX backslashes for KaTeX rendering#2042xielixing wants to merge 14 commits into
xielixing wants to merge 14 commits into
Conversation
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
analyzeMarkdownEditability canonicalizes markdown through a tiptap round-trip that calls escapeMarkdownPlainText, which doubles every backslash. This turns LaTeX commands like \int into \\int, \frac into \\frac, etc. KaTeX interprets \\ as a line break, breaking math rendering in the file viewer preview. Fix: use raw markdown content instead of canonicalMarkdown in both content normalization paths (toNormalizedMarkdown and initialContent loading). The tiptap editor normalizes content internally, so the canonical form is not needed for display. Add a root-cause test in tiptapMarkdown.test.ts documenting that canonicalMarkdown doubles backslashes in LaTeX math content. Closes GCWing#1952
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Markdown files containing LaTeX math (e.g. $$\int_0^1 x^2 dx = \frac13) fail to render correctly in the file viewer preview. KaTeX interprets the doubled backslashes (\\\int) as line breaks instead of LaTeX commands (\\\int).
Root Cause
\MarkdownEditor.tsx\ normalizes content through \�nalyzeMarkdownEditability, which canonicalizes markdown via a tiptap round-trip. The serialization step calls \�scapeMarkdownPlainText\ (tiptapMarkdown.ts:638) which doubles every backslash: \\\int\ becomes \\\\\int. This corrupted content is then passed to the preview renderer and KaTeX.
Fix
Use raw markdown content instead of \canonicalMarkdown\ in both content normalization paths:
The tiptap editor normalizes content internally, so the canonical form is not needed for display purposes.
Test
Added a root-cause test in \ iptapMarkdown.test.ts\ that documents the backslash-doubling behavior of \�nalyzeMarkdownEditability\ on LaTeX math content.
Validation
Closes #1952