mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
fix/transcript-virtual-scroll
16 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
41315989bd | fix(editor): prevent incorrect comment autolinks (#5665) | ||
|
|
c52ebeaa0f |
MUL-4864 fix(chat): use a single New Chat draft, keyed by session not agent (#5519)
* fix(chat): use a single New Chat draft, keyed by session not agent (MUL-4864)
An uncreated chat kept one draft per agent (`__new__:<agentId>`), so
switching the agent picker mid-compose swapped the draft out from under
the user and left invisible per-agent drafts behind. That slot shape was
never a product decision: it fell out of
|
||
|
|
3b08456264 |
feat(views): block draft-fixing actions while attachments upload (#5465)
Submitting a composer mid-upload silently lost the file: the editor holds a `blob:` placeholder that gets stripped during serialization, and the attachment id does not exist yet, so the send succeeded without the file. Chat and Quick Create gated this; manual issue create, Feedback's button, comments, replies and comment edit did not. Gate every action that FIXES a draft — Send/Create/Save, Cmd/Ctrl+Enter, Enter on the title, and the manual/agent mode switches — behind one source of truth: the editor document, which IS the upload queue. ContentEditor publishes queue transitions via `onUploadingChange` (a transaction listener, not `onUpdate` — that path is debounced and skips no-change emissions, and a failed upload leaves byte-identical markdown, so the un-gate would never fire). `useUploadGate` pairs that render state with an `isBlocked()` re-read at submit time, since the shortcut paths never consult the button. The publisher emits its current answer on subscribe rather than only on flips: hosts outlive editor instances (comment edit remounts on cancel, chat swaps by key on agent switch), and an editor torn down mid-upload would otherwise leave submit wedged shut with no pending node left to reopen it. Also fixes the failure toast that never fired: `uploadWithToast` only reported errors if a caller passed `onError`, and none did, so failed uploads removed their placeholder and vanished unexplained. `useEditorUpload` now supplies it once for every composer. Per MUL-4808: drops Quick Create's upload-time lock on the attach button (files can queue), and leaves description autosave ungated by design. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ea03912baf |
perf(desktop,issues): single-router tab sessions (MUL-4741 Phase 2) + trace-driven surface mount/render overhaul (MUL-4474/4750 reland) (#5403)
* Reapply "perf(issues): virtualize inbox/list/board/swimlane (MUL-4474, 方案2) (#…" (#5395)
This reverts commit
|
||
|
|
956f74e1d2 |
fix(chat): refresh Chat V2 input placeholder without remount (MUL-4276) (#5111)
Tiptap's Placeholder only reads its text at mount, and ContentEditor had a defaultValue-sync effect but no placeholder-sync effect. Switching between sessions of the same agent doesn't remount the editor, so the placeholder froze on the previous value — e.g. stuck on "This session is archived" after visiting an archived session, even on an active, usable input. Mutating the extension's string option at runtime does not repaint (Tiptap snapshots a string placeholder at mount). A function placeholder, however, is re-invoked on every decoration pass, so: - extensions: `placeholder` option now accepts `string | (() => string)`. - content-editor: pass a getter over a live `placeholderRef`; a new sync effect updates the ref and dispatches an empty transaction (docChanged=false, no onUpdate loop) to force a decoration recompute — no remount required. This also fixes placeholder staleness when the session/agent archive state changes. - chat-input: update the editorKey comment (placeholder no longer relies on the agent-switch remount). - tests: getter reads the live value + one repaint on change; no repaint when the placeholder prop is unchanged. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8f51b4f62f |
fix(editor): repair empty list items parsed from a markdown draft (#4869)
The real ordered-list caret bug (MUL-3973). Typing 1. in the comment box persists the draft "1. \n\n"; on remount @tiptap/markdown parses that empty item into a schema-invalid, childless listItem, leaving the document with an AllSelection instead of a text cursor — so the browser paints the caret on the following block and it can't be moved back into the list. Verified against the REAL editor (real createEditorExtensions + @tiptap/markdown, no @tiptap/react mock). Non-empty items round-trip fine; only the empty-item round-trip corrupts, which the reverted #4813 never exercised. - repairEmptyListItems(): rebuild from JSON so every list/task item leads with a paragraph (covers empty items and nested items whose first child is a sub-list); reset the AllSelection first (else setContent collapses the list); keep the whole repair off the undo stack; restore the prior caret (sync path) or land in the list item (mount). - Called in onCreate (mount) and after the sync-effect setContent. - Real-editor tests incl. undo-does-not-revive and nested-list schema validity. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2cf5297814 |
Revert "fix(editor): keep the comment-box caret in the ordered list after switching issues (MUL-3973) (#4813)" (#4851)
This reverts commit
|
||
|
|
c2903365a9 |
fix(editor): keep the comment-box caret in the ordered list after switching issues (MUL-3973) (#4813)
* 📝 [需求] 有序列表光标切换缺陷提案 / docs: requirements for 26-editor-ordered-list-cursor Co-authored-by: multica-agent <github@multica.ai> * fix(editor): keep the caret in the list item when the comment box remounts Typing `1.` makes an ordered list, then switching issues and back yanked the caret to the next line and re-yanked it there whenever the user moved it. The comment box remounts on issue switch (key={id}) and feeds the persisted draft back as defaultValue; the content-sync effect treated the redrawn draft as an external change because @tiptap/markdown reserializes an ordered list slightly differently from its source, re-parsed it, and restored the caret with a bare Math.min offset clamp that resolved onto the list's structural (non-text) gap. - Short-circuit the sync effect when the incoming parser INPUT matches what was last applied (seeded at mount), so an identical draft is never re-parsed. - Replace the Math.min clamp with clampSelectionToText, which snaps the prior offsets to the nearest valid text position via TextSelection.between. Tests: restore-selection unit tests (real editor) + a content-editor remount regression test. Co-authored-by: multica-agent <github@multica.ai> * fix(editor): seed the caret guard synchronously and keep it in sync with the doc Addresses cross-review findings on the content-sync guard: - Seed appliedIncomingRef at render instead of in onCreate. Tiptap v3's onCreate is deferred past the first sync-effect run, so the guard was null on that run and a remount could still re-parse the ordered-list draft (the mock's synchronous onCreate had hidden this). - Advance appliedIncomingRef on the Guard 3 short-circuit too, so a later external change back to an older value (e.g. a collaborator reverts the description) is no longer mistaken for an already-applied input and swallowed. Tests: add a suppressed-onCreate remount case and an A->B->A external-revert regression; 23 passed. Co-authored-by: multica-agent <github@multica.ai> * docs: remove internal requirements proposal from the PR Keep the PR to code and comments only, per reviewer request. The requirement context lives in the tracking issue, not in this open-source code change. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e9e97ee6d2 |
refactor(editor): dedupe upload-node scan and markdown normalization (#4267)
Extract three module-local helpers in content-editor.tsx and route the duplicated call sites through them — no behavior change: - normalizeMarkdown(md) / normalizeEditorMarkdown(editor): the single definition of the "strip blob URLs + trimEnd" canonical form, replacing the five editor-markdown sites and the one incoming-string site. - hasUploadingNode(editor): replaces the two byte-identical document scans (content-sync Guard 0 and hasActiveUploads). The imperative getMarkdown() is deliberately left untrimmed; a safety-net test pins its exact current return value. Scope: WOR-59 upload-readiness review. file-upload.ts untouched; follow-up items B1/F4/F5 are out of scope. Co-authored-by: hzz <331380069@qq.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f46b929ebc |
fix(editor): don't wipe in-flight uploads on external content sync (#4196)
* fix(editor): don't wipe in-flight uploads on external content sync When a brand-new chat's first file upload triggers lazy session creation, `setActiveSession(null → uuid)` flips ChatInput's draft key mid-upload, which changes `defaultValue` to the new (empty) session draft. ContentEditor's "sync external defaultValue" effect then ran `setContent` over a document that still held the `uploading` image/fileCard node, wiping it — so the upload's finalize could no longer find the node. The file vanished and the draft was left with an empty `!file[name]()`. The editor was never remounted (instance stays alive); the node was removed by the content-sync effect. An uploading node is local state an external sync must not overwrite, exactly like the existing dirty/focused guards. Add a guard that bails the sync while any `uploading` node is present. Pure frontend; affects only the first upload in a new chat (subsequent uploads hit an existing session, so no draft-key flip). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(editor): cover the in-flight-upload content-sync guard The content-sync effect now reads `editor.state.doc.descendants` on every run to detect uploading nodes; the mocked editor didn't implement it, crashing all ContentEditor tests. Add `descendants` (driven by `editorState.uploadingNodes`) to the mock and a regression test asserting an external `defaultValue` change does not setContent while an upload is in flight, and resumes once it settles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): migrate new-chat draft onto the session id on lazy create The first file upload in a brand-new chat lazily creates the session, flipping ChatInput's draft key from `__new__:agent` to the session id mid-upload. The in-progress (empty-href) file-card markdown the editor had already written into the `__new__:agent` draft was neither migrated nor cleared, so it stayed stranded under that key — and resurfaced as a stale `!file[name]()` the next time a new chat opened for the same agent (the send only cleared the session-keyed draft). Migrate the `__new__:agent` draft onto the new session id the moment the session is created (upload path only — text send already clears the pre-flip key via `keyAtSend`). Add a shared `newSessionDraftKey` helper so ChatInput and ensureSession agree on the slot name, and a `migrateInputDraft` store action. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ef08d8584c |
MUL-3254: flush issue description edits on close (#4082)
* fix: flush issue description editor on close Co-authored-by: multica-agent <github@multica.ai> * fix: make unmount flush opt-in via flushPendingOnUnmount The unconditional unmount flush re-emitted discarded content into composers that clear their draft and then unmount (comment edit cancel, create-issue / feedback submit), resurrecting the cleared draft. - Add flushPendingOnUnmount prop (default false); only the issue-detail description editor opts in. - Cache the pending markdown in a ref at onUpdate time and emit that cached copy on unmount, instead of reading the editor instance during teardown. - Regression tests: default drops the pending update on unmount, opt-in flush emits the cached value even when the editor is already destroyed, no double-emit after the debounce fired, and issue-detail pins the opt-in wiring. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fa15041864 |
MUL-3254: fix pasted image draft rendering in desktop (#4066)
* fix: keep issue draft attachment records Co-authored-by: multica-agent <github@multica.ai> * fix: avoid persisting signed draft attachment urls Co-authored-by: multica-agent <github@multica.ai> * fix: reuse resolved media url for draft previews Co-authored-by: multica-agent <github@multica.ai> * fix: address draft attachment review nits - Backfill an empty caller download_url from the in-session upload on id collision so a just-pasted image first-paints from the signed URL instead of detouring through markdown_url. - Prune draft attachments no longer referenced by the persisted description when the create dialog reopens. - Backfill EMPTY_DRAFT defaults on draft-store rehydrate so drafts persisted before the attachments field existed get a stable shape. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
abf99eb700 |
fix(attachments): server-driven markdown_url + legacy compat (MUL-3192) (#3991)
Comment / issue / chat images uploaded inside the Desktop app rendered
as the broken-image fallback. The editor was persisting a site-relative
`/api/attachments/<id>/download` URL into markdown — that path only
resolves when the document origin proxies /api to the API host (apps/web
via Next.js rewrite). On Electron's file:// origin it never resolved.
Per GPT-Boy's plan, move the durable-URL choice from the client to the
server so the persisted shape is correct regardless of which client
performed the upload.
Server:
- AttachmentResponse gains a markdown_url field, computed by
buildMarkdownURL from the deployment policy:
• storage URL is already absolute + unsigned (public CDN, S3 public
bucket, LocalStorage with MULTICA_LOCAL_UPLOAD_BASE_URL on https) →
use it verbatim;
• CloudFront-signed mode → never expose the raw S3 URL (private
bucket); return cfg.PublicURL + /api/attachments/<id>/download so
the server can re-sign on every request;
• LocalStorage relative + cfg.PublicURL set → same prefixed API
endpoint;
• cfg.PublicURL unset → fall back to site-relative path so web's
Next.js rewrite still works.
- isDurablePublicURL helper rejects URLs carrying CloudFront / S3
signature query params, so a freshly-signed download_url can never
leak into persistence — the original MUL-3130 bug stays closed.
Frontend:
- Attachment type + AttachmentResponseSchema (and apps/mobile mirror)
carry markdown_url. Schema lenient-defaults to '' so a backend old
enough to predate this field doesn't break clients.
- useFileUpload picks markdownLink with three-layer fallback:
(1) att.markdown_url (modern server),
(2) attachmentDownloadPath(att.id) — legacy site-relative shape,
retained for backends old enough to omit markdown_url,
(3) att.url — no-workspace avatar branch with no attachment-row id.
- attachment.tsx keeps the relative→absolute absolutize pass, but
reframed as the legacy-compat fallback for already-persisted
/api/attachments/<id>/download or /uploads/<key> URLs in old
bodies. New content writes absolute URLs and skips this path.
- ContentEditor still tracks freshly-uploaded records into
AttachmentDownloadProvider so Quick Create's editor can swap the URL
via the resolver during the same session even before the server-side
binding lands.
Tests:
- server/internal/handler/file_test.go: 5 new buildMarkdownURL matrix
tests (public CDN passthrough, CloudFront-signed swap, relative
prefixing, PublicURL unset fallback, trailing-slash strip) + 15
table-driven isDurablePublicURL cases.
- packages/core/hooks/use-file-upload.test.ts: new file, 4 cases
covering modern server / legacy server / no-id avatar / oversize.
- packages/views/editor/attachment.test.tsx + content-editor.test.tsx:
10 cases for the absolutize matrix and in-session attachment merge.
- 6 existing test fixtures updated to include markdown_url.
Verification: 1236 @multica/views tests pass; 514 @multica/core tests
pass (4 new); server handler package tests pass for the new matrix
plus all pre-existing TestAttachmentToResponse* and TestDownload*
cases. Typecheck green for views/core/web/desktop. Lint clean on
touched files.
Quick Create attachment_ids binding (orphaned attachment relationship
on the resulting issue) is a follow-up — it requires a new --attachment-id
CLI flag and daemon prompt-template work and is intentionally scoped
out of this PR.
Refs: MUL-3192
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
d42fbcb794 |
fix(editor): sync ContentEditor when defaultValue changes externally (#2765)
* fix(editor): sync ContentEditor when defaultValue changes externally
Tiptap v3 `useEditor` reads `content` only at mount (ueberdosis/tiptap#5831
— by design), so when an issue description is updated remotely (WS event,
another agent, another client), the editor kept showing stale content
until the issue was closed and reopened. `key={id}` in issue-detail only
force-remounts on issue switch, not on same-issue updates.
Add a useEffect in ContentEditor that watches `defaultValue` and applies
it via `editor.commands.setContent()` with four guards:
1. Focused AND dirty — protect bytes the user is actively typing.
Focused-but-clean intentionally falls through: onBlur has no replay
path, so an unconditional `if (isFocused) return` would drop the
sync forever for users who click into the editor without typing.
2. Unfocused AND dirty — covers the blur → debounce (1500ms) window
where the editor holds unsaved content but isFocused is already
false. The pending onUpdate flush reconciles via the cache;
overwriting here would be silent data loss.
3. Normalized-equal short-circuit — avoids a no-op transaction when
the cache reflects a write this editor just emitted.
4. `emitUpdate: false` — Tiptap v3 flipped setContent's emitUpdate
default to true; without this the sync would re-trigger onUpdate
→ server save → self-write loop.
After setContent, clamp the prior selection to the new doc size so the
caret doesn't snap to position 0.
Tests cover five cases: unfocused+dirty-content (sync fires),
focused+dirty (skip), focused+clean (must sync — regression guard for
the focused-but-clean hole), unfocused+dirty (blur-before-debounce
window, skip), and normalized-equal short-circuit (skip).
Closes #2409
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(editor): cover normalized-equal sync path with a distinct defaultValue
The previous rerender passed the same `defaultValue` string, so React's
dep-array equality short-circuited the sync effect entirely — the test
only exercised the first-mount equality check, not the actual
normalized-equal guard.
Pass a different-but-trimEnd-equivalent value so the effect re-runs and
the normalized-equal short-circuit is what keeps setContent uncalled.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
35557c0b11 |
fix(test): add missing selection mock in ContentEditor test
The merge from main introduced `editor?.state.selection.empty` in ContentEditor. The test mock was missing `state.selection`, causing a TypeError. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
9170b01739 | fix(views): focus description editor when clicking empty area |