Skip to content

feat(plugin-ui): chat-style input submit, scroll follow/observe/jump, markdown node - #3327

Merged
ItsLemmy merged 7 commits into
noctalia-dev:mainfrom
gegnep:plugin-ui-props
Jul 31, 2026
Merged

feat(plugin-ui): chat-style input submit, scroll follow/observe/jump, markdown node#3327
ItsLemmy merged 7 commits into
noctalia-dev:mainfrom
gegnep:plugin-ui-props

Conversation

@gegnep

@gegnep gegnep commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Three additive, default-off plugin UI capabilities, plus two bugfixes found
while exercising them from a real plugin:

  • input submitOnEnter prop: Enter submits, Shift+Enter inserts a newline,
    Ctrl+Enter still submits (existing behavior unchanged when off)
  • scroll stickToBottom / onScroll(offset, maxOffset) / scrollToBottomRev
    props for follow-scroll, scroll observation, and an explicit jump-to-bottom
  • ui.markdown node type registering the existing md4c MarkdownView, with the
    source cached per slot so streaming re-renders only re-parse on change
  • fix: runStream slots (cap 4/host) were only reclaimed on host teardown, so
    four short-lived streams permanently exhausted the cap; exited processes now
    mark their cancel token and startStream sweeps them
  • fix: MarkdownView measured labels unwrapped, under-allocating its height and
    overlapping sibling rows when used alongside other children in a flex

Motivation

Plugin panels that stream content (chat-style UIs) currently can't offer
Enter-to-send composers, can't follow or observe scroll position, and can only
render plain labels. All three are small exposures of capability the codebase
already has. Developed for and exercised by a real plugin (a Claude chat
panel: https://github.com/gegnep/noctalia-plugins); happy to adjust naming or
semantics to fit the project's direction.

Type of Change

  • New feature
  • Bug fix
  • Breaking change
  • Refactoring
  • Build / packaging

Related Issue

Testing

  • Rebased onto current main; just format clean on every commit (applied
    per-commit via rebase --exec).
  • Full nix build including all test targets; ui_tree_reconciler_test links
    the markdown control (its source list gained markdown_view/builders/md4c —
    caught by a real build, not hypothetically).
  • Live-tested extensively via the chat panel plugin on Niri: all three
    Enter-key behaviors; follow-scroll during streaming; scroll-up disengages
    the stick and onScroll reports offset/max; scrollToBottomRev snaps back;
    markdown (headings/lists/inline code) streaming without re-parse churn;

    4 sequential runStream invocations keep working; markdown views mixed with
    sibling rows lay out without overlap.

  • Note: scrollToBottomRev applies once on the first reconcile that sees it
    (a fresh scroll starts at the bottom when the prop is present).

Manual Coverage

  • Tested on Niri
  • Tested on hyprland
  • Tested on sway
  • Tested on other WM
  • Tested on Multiple Monitors

Screenshots / Videos

image

Checklist

  • This PR is ready for review, or it is marked as Draft.
  • I read and followed the relevant guidance in CONTRIBUTING.md.
  • I ran just format with clang-format v22+ installed.
  • I ran the relevant build or test commands.
  • I self-reviewed the changes.
  • I checked for new warnings or errors.
  • I will update end-user documentation after merge (plugin-prop docs live
    on the external docs site; nothing in-repo to update).
  • I added or updated assets/translations/en.json, or this PR adds no
    new user-facing strings. (No new strings.)
  • I did not edit non-English translation files.
  • I used the existing canonical names for config keys, IPC names, paths,
    and identifiers.

Additional Notes

Plugin-API feedback from building against it, offered for the
docs or as follow-ups rather than included here:

  • Manifest select options with an empty value are silently dropped
    (plugin_manifest.cpp), and noctalia plugins lint doesn't flag the
    now-unselectable declared default — a lint warning candidate.

Portions of these patches were implemented with AI assistance (Claude Fable 5 orchestrating GPT -5.5)
under human review and testing; the repo has no stated policy on this, so disclosing rather than assuming.

@gegnep
gegnep force-pushed the plugin-ui-props branch from 7649da1 to 7a3e4ac Compare July 12, 2026 02:58
@gegnep

gegnep commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

rebased onto main

@gegnep
gegnep force-pushed the plugin-ui-props branch from 7a3e4ac to 6c8bc2d Compare July 15, 2026 23:39
@gegnep

gegnep commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

rebased onto main

@gegnep
gegnep force-pushed the plugin-ui-props branch from 6c8bc2d to cf807ec Compare July 20, 2026 06:15
@gegnep

gegnep commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

rebased over the drag-and-drop merge

@gegnep
gegnep force-pushed the plugin-ui-props branch from cf807ec to fa197db Compare July 20, 2026 23:49
@gegnep

gegnep commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

dropped the slot-reaping commit, superseded by 3065b05

@gegnep
gegnep force-pushed the plugin-ui-props branch from fa197db to a166bcc Compare July 27, 2026 18:36
@gegnep

gegnep commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

rebased over df40cac; dropped the meson hunk, superseded by the test refactor

@ItsLemmy

Copy link
Copy Markdown
Collaborator

1. [P1] Version the new plugin API before exposing it — src/scripting/ui_prelude.h:29-30

(https://github.com/gegnep/noctalia/blob/a166bcc20b574e194f038613e68085fd615c5d75/src/scripting/ui_prelude.h#L29-L30)
ui.markdownand the new input/scroll props expand the public plugin contract, but the PR does not increment kCurrentPluginApiVersion. Plugins therefore cannot declare these capabilities as their minimum requirement: an older shell will accept the plugin, then fail on ui.markdown or ignore the unknown props. Current main is API 19, so this should introduce API 20 after rebasing. The required separate-repo update is also missing:
official-plugins/noctalia.d.luau (https://github.com/noctalia-dev/official-plugins/blob/main/noctalia.d.luau#L354-L378) does not declare ui.markdown.

2. [P1] Defer scrollToBottomRev until the new scroll extent exists — src/ui/ui_tree_reconciler.cpp:1649-1653

(https://github.com/gegnep/noctalia/blob/a166bcc20b574e194f038613e68085fd615c5d75/src/ui/ui_tree_reconciler.cpp#L1649-L1653) applyProps()runs before child reconciliation and layout. This reads the previous maxScrollOffset(), and setScrollOffset() immediately clamps to that stale range. Consequently, a fresh scroll starts at offset 0 despite the PR description, and adding content while incrementing the revision jumps only to the previous bottom unless stickToBottom is also enabled. Store a pending jump and consume it in ScrollView::doLayout() after recomputing
m_maxScrollOffset.

3. [P2] Include scale in the markdown parse cache key — src/ui/ui_tree_reconciler.cpp:1606-1612

(https://github.com/gegnep/noctalia/blob/a166bcc20b574e194f038613e68085fd615c5d75/src/ui/ui_tree_reconciler.cpp#L1606-L1612) MarkdownView::setMarkdown()bakes m_scale into font sizes, spacing, padding, and radii, but reconciliation calls it only when the text changes. When a surface’s fractional scale changes or a plugin surface moves between differently scaled outputs, ordinary props rescale while the retained MarkdownView
remains at the old scale. Cache (text, scale) or invalidate markdown slots when UiTreeReconciler::setScale() changes.

@ItsLemmy
ItsLemmy marked this pull request as draft July 30, 2026 01:22
gegnep added 5 commits July 29, 2026 22:17
Plugin panels building chat composers need Enter-to-send with
Shift+Enter for newlines. Off by default; existing multiline
behavior (Enter inserts, Ctrl+Enter submits) is unchanged.

Assisted-by: Fable 5 (main session), claude-code v2.1.220
…mRev props

Chat-style plugin panels need follow-scroll while streaming content,
a way to observe scroll position (offset and max are delivered to the
onScroll callback), and an explicit jump-to-bottom trigger. All three
are opt-in; native ScrollView users are unaffected.

Assisted-by: Fable 5 (main session), claude-code v2.1.220
Exposes the existing md4c MarkdownView control to plugin UI trees as
ui.markdown with a text prop. The markdown source is cached per slot
so streaming re-renders only re-parse when the text actually changes.
Links render as underlined text (MarkdownView has no click handling);
sizing follows parent layout like other leaf controls.

Assisted-by: Fable 5 (main session), claude-code v2.1.220
MarkdownView only applied its wrap width to labels in doLayout, so
measure reported single-line label sizes and a parent flex allocated
too little height, overlapping sibling rows. Apply the wrap width from
the measure constraints as well; doLayout still re-applies the final
arranged width.

Assisted-by: Fable 5 (main session), claude-code v2.1.220
ui.markdown and the new input submitOnEnter and scroll stickToBottom,
onScroll and scrollToBottomRev props expand the plugin UI contract.
Plugins that rely on them can declare plugin_api = 20, so an older
shell rejects them at load instead of failing at runtime. Declaration
only, matching the API 18/19 precedent; the manifest range check in
plugin_registry enforces it.

Assisted-by: Fable 5 (main session), claude-code v2.1.217
@gegnep

gegnep commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

All three points addressed; rebased onto main (624363e30, clean).

  1. Plugin API 20 : new trailing commit adds
    kPluginUiPropsPluginApiVersion = 20 and retargets
    kCurrentPluginApiVersion. Declaration-only, matching the 18/19
    precedent; the manifest range check enforces it. The separate-repo
    updates are up as drafts, ready once this merges:
    noctalia.d.luau typings in Declare ui.markdown and plugin API 20 props official-plugins#34
    and the docs (version ledger + declarative-ui) in Document plugin API 20 ui additions noctalia-docs#213.
  2. scrollToBottomRev : reworked as suggested: the reconciler now
    calls a new ScrollView::requestScrollToBottom(), which stores a
    pending flag (and cancels an in-flight fling so a stale tween can't
    drag the view back). doLayout() consumes the flag right after it
    recomputes m_maxScrollOffset, folded into the existing
    stick-to-bottom pin, with an unconditional clear so a request issued
    at the bottom can't fire on a later pass. A fresh scroll with the
    prop present now opens at the bottom.
  3. Markdown scale : the parse cache keys on (text, scale). The
    scale check sits outside the text-presence gate, so a re-render that
    omits text (retained-prop convention) still picks up a rescale.

Tested live against a patched shell: fresh chat opens at the bottom,
mid-stream rev bumps land on the new bottom, a jump during a fling
holds, and markdown rescales when the panel moves between differently
scaled outputs.

@gegnep
gegnep marked this pull request as ready for review July 30, 2026 02:50
@ItsLemmy
ItsLemmy merged commit f71af8a into noctalia-dev:main Jul 31, 2026
2 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.

2 participants