Skip to content

Composer: extmark-backed parts model#73

Merged
liftaris merged 4 commits into
devfrom
task/t_8667e325-parts-model
May 21, 2026
Merged

Composer: extmark-backed parts model#73
liftaris merged 4 commits into
devfrom
task/t_8667e325-parts-model

Conversation

@liftaris
Copy link
Copy Markdown
Owner

What

Adds src/app/parts.ts — a PartsBuffer adapter over @opentui/core extmarks — and wires it into the composer.

  • Styled chips for @-refs. Accepting a complete @file:path / @diff / @git:N ref from the popover lands it as a styled, mark-backed chip instead of plain text. Prefix keywords that keep the popover open (@file:, @folder:) keep the classic string-write path.
  • Atomic backspace. One keystroke deletes a whole chip. Comes free from virtual:true extmarks — no custom key handling.
  • Parts round-trip through history. History entries persist as JSONL {input, parts?}; recalling an entry with parts rebuilds the chips and their ranges. Legacy NUL-encoded raw-string lines still load.
  • Submit emits onSend(text, parts?). Text parts (pasted content) are inlined back into the text before send; file/agent parts ride as a side-channel. No wire change — today's callers only consume text. This unblocks a follow-up gateway task without blocking this ship.

Behavioral notes

  • ~/.hermes/herm/history migrates from NUL-encoded lines to JSONL on the next write. Old lines are read via a tolerant parser; the file becomes mixed-format after first use.
  • <textarea> now receives syntaxStyle so extmark styles resolve. Theme swaps re-register style ids and rebuild the buffer through the unmount path.

Tests

test/parts.test.tsx (16 cases: insert ordering, chip boundaries, adjacent chips, atomic backspace, deleteRange across a chip, snapshot round-trip, expand semantics) + test/composer-parts.test.tsx (5 cases: submit emits parts, two chips back-to-back, backspace drops the part, history restore re-emits). The two-chips case guards a real bug found during review: the accept path previously rebuilt the buffer with setText(), wiping every prior chip's mark.

tsc clean, 1007/1007 pass.

builder and others added 4 commits May 20, 2026 23:09
Add src/app/parts.ts — thin PartsBuffer adapter over @opentui/core
extmarks. Mirrors opencode's shape (file/agent/text kinds with
asymmetric source fields) so snapshots round-trip and serialize as
parts[] for future wire migration.

Composer.tsx wires:
 - syntaxStyle + registered extmark.file/agent/paste style ids
 - PartsBuffer constructed on textarea ref bind, torn down on unmount
 - @-ref accept: complete refs (trailing space) land as styled chips,
   prefix keywords (trailing : or /) keep the classic write path
 - submit emits onSend(text, parts) after expanding text parts inline
 - history stores { input, parts } snapshots; legacy raw-string lines
   still load via the tolerant parser

useInputHistory.ts now takes HistEntry | string, persists JSONL.

No wire change: onSend accepts (text, parts?) but today's callers
only consume text. The parts[] side-channel unblocks a follow-up
gateway task without blocking this ship.
- test/parts.test.tsx: PartsBuffer unit coverage — insertText/insertPart
  ordering, listParts, atomic-chip backspace, plain-text backspace,
  deleteRange across a chip, snapshot round-trip, chip-at-start/end,
  adjacent chips, insert-before/after chip doesn't extend its range,
  expand() text-part inlining, sync() after mark deletion.
- test/composer-parts.test.tsx: Composer integration — submit emits
  Part[] matching the buffer, atomic backspace via the key handler,
  history restore rebuilds chips from a parts[] entry.
- Composer.tsx: the textarea ref callback was inline, so its identity
  changed every render → React called it with null+r again, and the
  null-call clobbered PartsBuffer.current mid-edit. useCallback it and
  route sids through a ref so theme swaps still rebuild through the
  genuine unmount path. Without this, the composer-level tests (and
  the real parts[] emission) can't keep a chip alive past one render.
atAccept rebuilt the whole textarea with setText() which clears every
extmark — so a second @-ref chip wiped the first chip's mark and submit
emitted only one FilePart. Splice the @word out via deleteRange instead;
the extmarks controller shifts remaining marks to absorb the deletion.

New test "two chips in a row → two FileParts" fails against the old
setText path and passes after.
@liftaris liftaris force-pushed the task/t_8667e325-parts-model branch from 6c15692 to 3ca9acd Compare May 21, 2026 06:10
@liftaris liftaris merged commit 809ee1f into dev May 21, 2026
1 check passed
@liftaris liftaris deleted the task/t_8667e325-parts-model branch May 21, 2026 06:14
@liftaris liftaris mentioned this pull request May 21, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.7.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant