Skip to content

Sync upstream main (August 18) - #140

Merged
Antonov548 merged 22 commits into
mainfrom
sync/upstream-2026-08-18
Aug 18, 2026
Merged

Sync upstream main (August 18)#140
Antonov548 merged 22 commits into
mainfrom
sync/upstream-2026-08-18

Conversation

@marcus-pousette

@marcus-pousette marcus-pousette commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Merge cybersemics/em main through ebdfdef83 into Antonov main. This brings in 20 upstream commits, including the multiselect command series, the delayed-autofocus virtualization fix, the latest New Subthought behavior, VirtualThought/drag-hover fixes, emoji paste handling, and dependency maintenance.

Merging this PR will update the head of cybersemics/em#4325 and clear its current upstream merge conflicts.

Conflict resolution

  • swapParent.ts: keep upstream's deterministic fake-timer test with try/finally cleanup.
  • newThoughtAbove.ts: keep upstream's expanded per-selection behavior and tests.
  • repeat.ts: adapt the new raw export expectation from Yjs's literal __ROOT__ to TreeCRDT's ${HOME_TOKEN}.
  • generateThought.ts: retain Antonov's awaited initStore() while accepting the cursorless setup correction now merged upstream in cybersemics/em#4978.
  • newUncle.ts: express expected empty thoughts without trailing source whitespace.
  • newSubthought.ts: keep upstream's expanded seven-test per-selection behavior; its expectations already use TreeCRDT's ${HOME_TOKEN}.
  • useDragLeave.ts: await Antonov's asynchronous initStore() in the new upstream tests so TreeCRDT memory storage is initialized before dispatching imports.

The upstream lottie-react and expect-webdriverio package/lockfile updates are included. Antonov's TreeCRDT dependencies, including @treecrdt/wa-sqlite 0.4.2, remain intact, and Yjs remains removed.

raineorshine and others added 14 commits August 17, 2026 08:17
)

Co-authored-by: BayuAri <8419585+BayuAri@users.noreply.github.com>
Co-authored-by: raineorshine <750276+raineorshine@users.noreply.github.com>
…mics#4969)

* Fix delayed autofocus cleanup

Co-Authored-By: Codex GPT-5 (unknown context) <noreply@openai.com>

* Fix hidden-thought cursor tests

Co-Authored-By: Codex GPT-5 (unknown context) <noreply@openai.com>

---------

Co-authored-by: Codex GPT-5 (unknown context) <noreply@openai.com>
…speeds (cybersemics#4854)

The test relied on thought b being created in a later millisecond than
a, c, and d, but the reducers run synchronously, so on a fast machine
all four share a timestamp and Created sort falls back to alphabetical
order, putting b first. Advance fake timers between the two creation
steps so the intended order is guaranteed.

Co-authored-by: Claude Fable 5 (200K context) <noreply@anthropic.com>
…4927)

New Subthought (next) declared multicursor: { disallow: true }, so selecting
several thoughts and invoking it only produced an error alert. The action is
per-cursor and composes fine when executed once per selected thought, so declare
multicursor: { preventSetCursor: true, clearMulticursor: true } and let the
standard multicursor loop drive it. Each selected thought gets its own new empty
uncle, rather than collapsing the selection to one insertion per sibling group.

preventSetCursor keeps the caret on the empty thought created for the last
selected thought, ready to type, which is the command's single-cursor
postcondition; the default cursor restore would pull it back to the original
thought. clearMulticursor drops the now-stale selection, matching the
single-cursor behavior where newThought's own setCursor clears it.

canExecute additionally accepts hasMulticursor, so the command is enabled when
thoughts are selected but the cursor is missing or on a root thought. A selected
root thought has no parent to insert at; the loop's per-iteration setCursor
clears the selection, so the per-iteration canExecute check fails for it and it
is skipped without blocking the rest of the run.

Add a store test suite covering the multiselect behavior: one new empty uncle per
selected sibling, selections spanning different parents and depths, a selected
root thought skipped rather than blocking, the caret left on the last created
empty thought with the multicursor cleared, the single-selection case, and a
single undo reverting the whole run.

Co-authored-by: Claude Fable 5 (unknown context) <noreply@anthropic.com>
New Grandchild declared multicursor: { disallow: true }, so selecting several
thoughts and invoking it only produced an error alert. The action is per-cursor
and composes fine when executed once per selected thought, so declare
preventSetCursor and clearMulticursor and let the standard multicursor loop
drive it. Each selected thought gets its own new empty grandchild appended
inside its first subthought, rather than the selection collapsing to a single
insertion.

Both options are load-bearing. The action sets the cursor to the new empty
grandchild with the keyboard open, ready to type, so the loop's default cursor
restore would move the caret back to the originally selected thought.
The selection of parent thoughts is stale once the caret is in a new empty
thought, so it is cleared rather than restored.

Add a store test suite covering the multiselect behavior: one new empty
grandchild per selected thought, appending after the first subthought's
existing children, selections at different depths, a selected childless
thought skipped without blocking the rest, the caret left in the last created
grandchild with the multicursor cleared, the single-selection Command Center
flow, and the whole run reverting on a single undo.

Co-authored-by: Claude Fable 5 (unknown context) <noreply@anthropic.com>
New Thought declared multicursor: { filter: 'last-sibling' }, so a multiselect
collapsed to one insertion per sibling group and created a single new thought.
Remove the filter so the standard multicursor loop runs newThought once per
selected thought, creating a new empty sibling after each of them.

clearMulticursor and preventSetCursor are retained and are both load-bearing
for the new semantics: each newThought sets the cursor to the thought it
creates, so preventSetCursor leaves the caret in the last created empty thought
ready to type instead of restoring the pre-command cursor, and clearMulticursor
drops the now-stale selection.

Convert the existing last-sibling test to the new per-selected-thought
semantics and extend the suite: a new empty sibling after each selected thought,
selections spanning parents and depths, a single selected thought, the cursor
landing on the last created thought with the multicursor cleared, single-cursor
behavior unchanged, and a single undo reverting every creation.

Co-authored-by: Claude Fable 5 (unknown context) <noreply@anthropic.com>
New Thought (above) declared multicursor: { filter: 'first-sibling', ... }, so a
multiselect collapsed each sibling group to a single insertion and selecting
several thoughts created far fewer thoughts than were selected. Remove the
filter so the standard multicursor loop creates a new empty thought above each
selected thought, which is what a creation command on a multiselect should do.

exec is newThought({ insertBefore: true }), which inserts a sibling before the
cursor thought. Unlike the subthought commands, selected siblings share an
insertion parent, which is exactly what first-sibling collapsed; each selected
thought now gets its own new thought instead. Insertion positions are computed
per iteration against the already-mutated tree, so the new thoughts interleave
with the selection rather than bunching up.

clearMulticursor and preventSetCursor are both retained and were verified to be
load-bearing under the new semantics: without preventSetCursor the loop restores
the original cursor instead of leaving the caret in the empty thought just
created, and without clearMulticursor the loop re-adds a multicursor on each
originally selected thought, leaving a stale selection behind the new caret.

Convert the existing multicursor test, which pinned the first-sibling collapse,
to the new per-selected-thought semantics, and add coverage for selections
spanning different parents and depths, the resulting cursor position, the
cleared multicursor, and reverting every creation on a single undo.

Co-authored-by: Claude Fable 5 (unknown context) <noreply@anthropic.com>
Bind Context declared multicursor: { disallow: true }, so selecting several
thoughts and invoking it only produced the error alert "Cannot bind multiple
thoughts." Declare multicursor: true and let the standard multicursor loop drive
it once per selected context.

Contexts selected in different context views each get their own binding, which is
the case the disallow declaration blocked outright. Contexts selected within the
same context view overwrite each other, since =bindContextCommand holds a single
context — the last one in document order wins, exactly as if the command were
invoked on each context in turn. Selected thoughts that are not contexts of an
active context view are silent no-ops rather than blockers.

Neither preventSetCursor nor clearMulticursor is needed: nothing moves, and
recomputePath already returns context view paths unchanged (cybersemics#4752), so the
default restore returns the cursor and the selection to the contexts they started
on.

Add a store test suite covering the multiselect behavior: bindings across two
context views, last-wins within one view, equivalence with sequential
single-cursor invocation, both toggle interactions with an existing binding, the
single-selection Command Center flow, the inactive-context-view no-op, skipped
descendants, the restored cursor and multicursors, and a single undo reverting
the whole run.

Co-authored-by: Claude Fable 5 (unknown context) <noreply@anthropic.com>
Extract declared multicursor: { disallow: true }, so selecting several thoughts
and invoking it only produced the error alert "Cannot extract multiple thoughts."
Declare multicursor: false instead, so the command executes on state.cursor and
the selected thoughts stay selected.

Extract takes its input from the browser text selection, of which the document
has exactly one. The extractThought action slices state.cursor's value at that
selection's character offsets, so the offsets are only meaningful for the thought
that owns the selection. A per-cursor loop applies them to unrelated strings:
with "bravo" selected in "alpha bravo" and "charlie delta" and "echo" also
selected, multicursor: true yields "charlita" with the child "e del", and "echo"
with an empty child.

The previous disallow declaration corrupted values the same way whenever exactly
one thought was selected and it was not the thought being edited, since disallow
moved the cursor onto the selected thought and then executed with the edited
thought's offsets.

Add a multicursor test suite covering extraction from the edited thought with
several thoughts selected and with a different thought selected, the preserved
cursor and multiselect, the no-text-selected alert, and a single undo. Add
beforeEach(initStore) to the file so the store is cleared between tests, which
the createTestApp/cleanupTestApp pair alone did not do.

Co-authored-by: Claude Fable 5 (unknown context) <noreply@anthropic.com>
…#4926)

Co-authored-by: Claude Fable 5 (unknown context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 (unknown context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
…emics#4938)

When thoughts are selected, the cursorBack reducer now replaces the
selection with the parent of each selected thought (deduplicated by
path) instead of moving the hidden cursor, mirroring cursorForward
(cybersemics#4898). Root-level thoughts contribute no parent; if all selected
thoughts are at the root, the selection is left unchanged. Expansion is
recalculated so the newly selected parents collapse.

The command's dispatch gate now also fires when a multicursor exists
without a cursor (reachable on touch via long press), and the
scroll-to-top convenience is suppressed while thoughts are selected.
Desktop Escape still clears the selection before the reducer is reached.

Fixes cybersemics#3526

Co-authored-by: Claude Fable 5 (unknown context) <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 (unknown context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Resolve TreeCRDT root expectations and include the cursorless Generate Thought test correction from cybersemics#4978.

Co-Authored-By: Codex GPT-5 (unknown context) <noreply@openai.com>
marcus-pousette and others added 8 commits August 17, 2026 17:26
Construct the cursorless multiselect directly instead of relying on cursorBack, whose multicursor behavior changed before the test merged.\n\nCo-Authored-By: Codex GPT-5 (unknown context) <noreply@openai.com>
Co-authored-by: Claude Fable 5 (unknown context) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot CLI unknown (unknown context) <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Migrate LottieAnimation to the lottie-react v3 API following
https://lottiereact.com/docs/migration:

- default Player import → named Lottie import
- animationData prop → src
- onComplete prop → subscriptions.complete
- setSpeed effect → reactive speed prop
- webdriver skip path: getDuration(true) + goToAndStop in a
  conditionally-called useLayoutEffect → seek({ percent: 100 }) in a
  ready subscription, since v3 loads asynchronously and LottieHandle no
  longer exposes frame counts

Verified in the running app: toolbar icons play to completion, fire
complete, and unmount back to the static SVG; the webdriver path seeks
straight to the last frame and completes immediately.

Co-authored-by: Claude Fable 5 (unknown context) <noreply@anthropic.com>
…mics#4973)

No code changes required: the e2e iOS tests only use jest-style
matchers (toBe, toEqual), which are unaffected by the v6 element
matcher changes. Peer dependencies (@wdio/* ^9.0.0) already satisfied.

Co-authored-by: Claude Fable 5 (unknown context) <noreply@anthropic.com>
…s#4975)

The effect that calls `onResize({ height: null })` was written as a
componentWillUnmount, with a comment asserting its dependencies "should be
memoized and not change for the life of the component". That is not true of
`onResize`: TreeNode memoizes it on `cliff`, which changes whenever a thought's
position in the tree changes. React runs the cleanup before every such re-run,
so a thought released its tracked size while it was still mounted.

Instrumenting the cleanup and adding a sibling (which changes the preceding
thought's cliff) shows the cleanup firing on a live thought, followed by
`removeSize` and an immediate `setSize` for the same key:

    CLEANUP-FIRED |_fHZM6U06I_ej stillInDOM= true
      removeSize |_fHZM6U06I_ej
      setSize    |_fHZM6U06I_ej 20

The re-add happens because `updateSize`'s dependencies are a superset, so its
effect body restores the entry in the same passive-effect flush. The visible
behavior is therefore unchanged today. What this removes is the churn — an
in-place `delete` on the `sizes` state object followed by a rebuild — and the
trap of a cleanup that claims to be unmount-only while running mid-life.

Route the call through `useFreshCallback` so the effect's dependency is stable
and the cleanup runs only on unmount, while still invoking the latest
`onResize`. Re-running the instrumented trace confirms the mid-life fire is gone
and all thoughts still release their size on teardown.

Co-authored-by: Claude Opus 5 (unknown context) <noreply@anthropic.com>
…emics#4977)

useDragLeave shares a module-level hoverCount across every drop target, and
debounces a clear of state.hoveringPath when it reaches zero. The effect treated
every run as a hover transition: any run that was not a false→true change of
isDeepHovering fell into the `else` branch and decremented the shared count.
That branch is reached on mount, so any thought mounting mid-drag decremented
the count — dropping it to zero and blanking the drop indicator while a target
was still hovered.

Its cleanup was commented "Cleanup on unmount" but listed four dependencies, so
React ran it before every re-run rather than only on unmount. It also never
decremented, so a drop target unmounted mid-drag leaked its count and
hoveringPath was never cleared.

Guard the count on `isDeepHovering !== isCountedRef.current` so only a real
enter or leave adjusts it, and move the release into its own effect with empty
deps, where it decrements this target's contribution. The empty deps are what
make it an unmount handler.

Adds hook tests for the two reproduced failures (an unrelated thought mounting
mid-hover; a hovered target unmounting), a guard that the count still reaches
zero on leave, and documents the counting rule in docs/drag-and-drop.md.

Co-authored-by: Claude Opus 5 (unknown context) <noreply@anthropic.com>
Co-authored-by: patbyte <PATBYTE_NOREPLY_EMAIL>
Co-Authored-By: Codex GPT-5 (unknown context) <noreply@openai.com>
@Antonov548
Antonov548 merged commit 2fbbae0 into main Aug 18, 2026
10 of 12 checks passed
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.

5 participants