Skip to content

feat(userlist): click a user to open chat with @<name> prefilled#7660

Merged
JohnMcLear merged 4 commits intoether:developfrom
JohnMcLear:feat/click-user-prefill-chat-mention
May 3, 2026
Merged

feat(userlist): click a user to open chat with @<name> prefilled#7660
JohnMcLear merged 4 commits intoether:developfrom
JohnMcLear:feat/click-user-prefill-chat-mention

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Why

Newcomers to a multi-user pad regularly fail to discover the chat panel and the @-mention convention. The current UX is "find the chat icon in the corner, click it, type @, type your collaborator's name." Most people never make it past step 1.

This PR makes the user list itself the discovery affordance: clicking another user's row opens chat (if hidden) and prefills the input with @<their_name> , ready to send.

What changed

  • src/static/js/pad_userlist.ts — delegated click handler on #otheruserstable tr[data-authorId]. Skips clicks on .usertdswatch (color-picker keeps its own semantics). Calls chat.show() and prefills #chatinput. Doesn't clobber a real partial message — if the user is mid-typing, the mention is appended.
  • src/static/skins/colibris/src/components/users.css — pointer cursor on .usertdname and a hover underline so the affordance is visible. Swatch keeps its existing cursor.
  • doc/api/hooks_client-side.md — documents the new chatPrefillFromUser hook.

New hook: chatPrefillFromUser

Bot/AI plugins frequently have a display name that's a useless @-mention (e.g. AI Assistant doesn't trigger anything; the actual trigger is @ai). The new client-side hook lets them substitute the prefill:

exports.chatPrefillFromUser = (hookName, {authorId, name, prefill}, cb) => {
  if (authorId === window.clientVars.ep_my_bot.authorId) return cb('@bot ');
  return cb();
};

First plugin to return a non-empty string wins. Hook errors are caught so a misbehaving plugin can't break the click.

Companion

This unblocks ether/ep_ai_chat#19 (split per discussion) which becomes a tiny hook handler that swaps AI Assistant for the configured trigger. Without this PR, every Etherpad install that wants click-to-mention has to ship its own copy of the click handler.

Test plan

  • Open a pad in two browser sessions. From session A, click session B's row → chat opens (if it was hidden), @<sessionB_name> is in the input.
  • Type partial text first, then click a user → mention is appended, partial text preserved.
  • Click on the swatch (not name) → color picker opens, chat doesn't.
  • With chat-and-users sticky, clicking still focuses + prefills.
  • With pad.settings.hideChat true, click is a benign no-op.

🤖 Generated with Claude Code

Newcomers to a multi-user pad regularly fail to discover the chat
panel and the @-mention convention. Make the user list itself the
discovery affordance: clicking another user's row opens chat (if
hidden) and prefills the input with "@<their_name> ", ready to send.

The skin gets a small visual cue — pointer cursor on .usertdname and
an underline on hover — so the affordance is visible without
requiring a redesign. The color swatch keeps its own click semantics
(color picker), so the swatch cell is excluded from the new handler.

To let bot/AI plugins substitute their trigger string for an
otherwise-useless @-mention of the bot's display name (e.g.
"@ai Assistant" → "@ai"), this adds a new client-side hook,
chatPrefillFromUser, that takes {authorId, name, prefill} and lets
the first plugin to return a non-empty string override the default
prefill. Documented in doc/api/hooks_client-side.md alongside
chatSendMessage.

Plugin errors in the hook are caught — a misbehaving plugin can't
break the click. If chat is hidden by pad settings, chat.show() is
a no-op and the click effectively does nothing, which matches the
existing behavior of "no chat means no chat-related affordances".

The new prefill never clobbers a real partial message in the input;
if the user was mid-typing something, the @-mention is appended
rather than replacing.
@qodo-code-review
Copy link
Copy Markdown

ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Click user list to open chat with prefilled @-mention

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Click user row to open chat with @-mention prefilled
• Adds chatPrefillFromUser hook for plugin customization
• Visual affordance: pointer cursor and hover underline
• Preserves partial messages when appending mentions
Diagram
flowchart LR
  UserClick["User clicks row"] --> CheckSwatch{"Click on swatch?"}
  CheckSwatch -->|Yes| ColorPicker["Open color picker"]
  CheckSwatch -->|No| Hook["Call chatPrefillFromUser hook"]
  Hook --> Transform{"Plugin returns<br/>custom prefill?"}
  Transform -->|Yes| UsePrefill["Use plugin prefill"]
  Transform -->|No| DefaultPrefill["Use @name prefill"]
  UsePrefill --> ShowChat["Show chat panel"]
  DefaultPrefill --> ShowChat
  ShowChat --> FillInput["Prefill or append to input"]
  FillInput --> Focus["Focus input & set cursor"]
Loading

Grey Divider

File Changes

1. src/static/js/pad_userlist.ts ✨ Enhancement +41/-0

Add click-to-mention handler with hook support

• Added delegated click handler on #otheruserstable tr[data-authorId]
• Skips clicks on .usertdswatch to preserve color-picker semantics
• Calls chatPrefillFromUser hook for plugin customization
• Intelligently handles prefill: replaces only @-mentions or appends to existing text
• Focuses input and positions cursor after prefilling

src/static/js/pad_userlist.ts


2. src/static/skins/colibris/src/components/users.css ✨ Enhancement +13/-0

Add visual affordance for clickable user rows

• Added pointer cursor to .usertdname cells
• Added underline on hover for .usertdname to signal clickability
• Preserves color swatch's original cursor behavior

src/static/skins/colibris/src/components/users.css


3. doc/api/hooks_client-side.md 📝 Documentation +28/-0

Document chatPrefillFromUser hook API

• Documented new chatPrefillFromUser client-side hook
• Explains use case for AI/bot plugins to customize @-mention trigger
• Provides context properties: authorId, name, prefill
• Includes example implementation for bot plugins

doc/api/hooks_client-side.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 2, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Click-to-chat lacks feature flag 📘 Rule violation ⚙ Maintainability
Description
The new click handler on user list rows changes default behavior by opening chat and prefilling an
@name mention without any feature flag gating. This violates the requirement that new features be
disabled by default unless explicitly enabled.
Code

src/static/js/pad_userlist.ts[R379-411]

+      $('#otheruserstable').on('click', 'tr[data-authorId]', async function (event) {
+        // Skip clicks on the color swatch — that has its own click handler
+        // (color-picker semantics) and shouldn't double up as a chat trigger.
+        if ($(event.target).closest('.usertdswatch').length) return;
+        const tr = $(this);
+        const authorId = tr.attr('data-authorId');
+        if (!authorId) return;
+        const name = (tr.find('.usertdname').text() || '').trim();
+        let prefill = name ? `@${name.replace(/\s+/g, '_')} ` : '';
+        try {
+          const transforms = await hooks.aCallAll(
+              'chatPrefillFromUser', {authorId, name, prefill});
+          if (Array.isArray(transforms)) {
+            for (const tr2 of transforms) {
+              if (typeof tr2 === 'string' && tr2.length > 0) { prefill = tr2; break; }
+            }
+          }
+        } catch { /* never let a misbehaving plugin break the click */ }
+        try { chat.show(); } catch { /* */ }
+        setTimeout(() => {
+          const $input = $('#chatinput');
+          if (!$input.length) return;
+          const current = ($input.val() || '') as string;
+          if (!current.trim() || /^@\S+\s*$/.test(current.trim())) {
+            $input.val(prefill);
+          } else if (!current.includes(prefill.trim())) {
+            $input.val(`${current.trimEnd()} ${prefill}`);
+          }
+          $input.trigger('focus');
+          const elem = $input[0] as HTMLTextAreaElement;
+          try { elem.setSelectionRange(elem.value.length, elem.value.length); } catch (_e) { /* */ }
+        }, 50);
+      });
Evidence
PR Compliance ID 6 requires new features to be behind a feature flag and disabled by default. The
added code unconditionally registers a click handler on #otheruserstable tr[data-authorId] that
opens chat and prefills #chatinput, making the feature enabled by default.

src/static/js/pad_userlist.ts[379-411]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new UX feature (click user row to open chat and prefill `@name`) is enabled by default, but compliance requires new features to be behind a feature flag and disabled by default.
## Issue Context
The click handler is currently registered unconditionally, so all installs get the new behavior without opting in.
## Fix Focus Areas
- src/static/js/pad_userlist.ts[379-411]
- src/static/skins/colibris/src/components/users.css[11-16]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Click steals rename focus🐞 Bug ≡ Correctness
Description
Clicking the editable name <input> for unnamed users will also trigger the new row click handler,
which then focuses #chatinput and interrupts the rename workflow. This makes it difficult or
impossible to name unnamed users from the user list.
Code

src/static/js/pad_userlist.ts[R379-410]

+      $('#otheruserstable').on('click', 'tr[data-authorId]', async function (event) {
+        // Skip clicks on the color swatch — that has its own click handler
+        // (color-picker semantics) and shouldn't double up as a chat trigger.
+        if ($(event.target).closest('.usertdswatch').length) return;
+        const tr = $(this);
+        const authorId = tr.attr('data-authorId');
+        if (!authorId) return;
+        const name = (tr.find('.usertdname').text() || '').trim();
+        let prefill = name ? `@${name.replace(/\s+/g, '_')} ` : '';
+        try {
+          const transforms = await hooks.aCallAll(
+              'chatPrefillFromUser', {authorId, name, prefill});
+          if (Array.isArray(transforms)) {
+            for (const tr2 of transforms) {
+              if (typeof tr2 === 'string' && tr2.length > 0) { prefill = tr2; break; }
+            }
+          }
+        } catch { /* never let a misbehaving plugin break the click */ }
+        try { chat.show(); } catch { /* */ }
+        setTimeout(() => {
+          const $input = $('#chatinput');
+          if (!$input.length) return;
+          const current = ($input.val() || '') as string;
+          if (!current.trim() || /^@\S+\s*$/.test(current.trim())) {
+            $input.val(prefill);
+          } else if (!current.includes(prefill.trim())) {
+            $input.val(`${current.trimEnd()} ${prefill}`);
+          }
+          $input.trigger('focus');
+          const elem = $input[0] as HTMLTextAreaElement;
+          try { elem.setSelectionRange(elem.value.length, elem.value.length); } catch (_e) { /* */ }
+        }, 50);
Evidence
The user list already supports renaming unnamed users via an <input> inside the .usertdname cell;
the new handler only excludes swatch clicks and then focuses the chat input, so clicks intended for
that rename <input> will bubble to the <tr> handler and be hijacked.

src/static/js/pad_userlist.ts[183-196]
src/static/js/pad_userlist.ts[373-410]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new delegated click handler on `#otheruserstable tr[data-authorId]` triggers even when the click target is the existing rename `<input>` in the name cell, and it later focuses `#chatinput`. This breaks the unnamed-user rename interaction.
### Issue Context
Unnamed users are rendered with an `<input>` in the `.usertdname` cell and are wired up via `#otheruserstable input.newinput`.
### Fix Focus Areas
- src/static/js/pad_userlist.ts[373-410]
- src/static/js/pad_userlist.ts[183-196]
### Suggested change
Add early-return guards before doing any prefill/show work, for example:
- Return if `$(event.target).closest('input, textarea, select, button, a, [contenteditable=true]').length`.
- Or at minimum return if `$(event.target).closest('.usertdname input').length`.
This keeps the row-click behavior while preserving rename semantics.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Prefill runs when chat hidden 🐞 Bug ☼ Reliability
Description
If chat is disabled (hideChat true), chat.show() is a no-op but the handler still sets and focuses
#chatinput, so the click is not a benign no-op and may steal focus or mutate hidden UI state. This
contradicts the stated intent for pads where chat is hidden/disabled.
Code

src/static/js/pad_userlist.ts[R397-410]

+        try { chat.show(); } catch { /* */ }
+        setTimeout(() => {
+          const $input = $('#chatinput');
+          if (!$input.length) return;
+          const current = ($input.val() || '') as string;
+          if (!current.trim() || /^@\S+\s*$/.test(current.trim())) {
+            $input.val(prefill);
+          } else if (!current.includes(prefill.trim())) {
+            $input.val(`${current.trimEnd()} ${prefill}`);
+          }
+          $input.trigger('focus');
+          const elem = $input[0] as HTMLTextAreaElement;
+          try { elem.setSelectionRange(elem.value.length, elem.value.length); } catch (_e) { /* */ }
+        }, 50);
Evidence
chat.show() explicitly returns early when chat is disabled, but disabling chat hides #chatbox
without removing #chatinput from the DOM, so the handler’s timeout block can still find and
mutate/focus the hidden input.

src/static/js/chat.ts[36-44]
src/static/js/pad.ts[613-622]
src/static/js/pad_userlist.ts[397-410]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When chat is disabled, the handler still prefills and focuses `#chatinput` even though `chat.show()` returns early.
### Issue Context
- `chat.show()` returns immediately if chat is hidden.
- `applyShowChat(false)` hides `#chatbox` but `#chatinput` remains in the DOM.
### Fix Focus Areas
- src/static/js/pad_userlist.ts[397-410]
- src/static/js/chat.ts[36-44]
- src/static/js/pad.ts[613-622]
### Suggested change
After attempting to show chat, guard the prefill/focus block. For example:
- Call `chat.show();` then `if (!$('#chatbox').hasClass('visible')) return;` (or check the relevant hideChat flag if accessible).
This ensures the click is a true no-op when chat is disabled.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Hook await blocks click 🐞 Bug ☼ Reliability
Description
The click handler awaits hooks.aCallAll('chatPrefillFromUser', ...) before showing/prefilling
chat, and aCallAll waits for all hook functions to settle. A plugin hook that is very slow or
never resolves will delay the click action indefinitely (try/catch does not help for never-resolving
Promises).
Code

src/static/js/pad_userlist.ts[R388-397]

+        try {
+          const transforms = await hooks.aCallAll(
+              'chatPrefillFromUser', {authorId, name, prefill});
+          if (Array.isArray(transforms)) {
+            for (const tr2 of transforms) {
+              if (typeof tr2 === 'string' && tr2.length > 0) { prefill = tr2; break; }
+            }
+          }
+        } catch { /* never let a misbehaving plugin break the click */ }
+        try { chat.show(); } catch { /* */ }
Evidence
aCallAll uses Promise.all(...) across every registered hook and only resolves after they all
settle; therefore the click handler’s await can be blocked by any misbehaving or slow plugin hook.

src/static/js/pad_userlist.ts[388-397]
src/static/js/pluginfw/hooks.ts[346-352]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Awaiting `hooks.aCallAll()` blocks the UI action until *all* plugin hook handlers finish. If any plugin never resolves its callback/Promise, the click action never completes.
### Issue Context
`aCallAll()` awaits `Promise.all(...)` across all hooks. `try/catch` only catches thrown/rejected errors, not never-settling Promises.
### Fix Focus Areas
- src/static/js/pad_userlist.ts[388-397]
- src/static/js/pluginfw/hooks.ts[346-352]
### Suggested change options
Pick one:
1) **Timeout protection**: wrap the await in `Promise.race([hooks.aCallAll(...), timeoutPromiseResolvingToEmptyArray])`.
2) **Stop early**: use `hooks.aCallFirst()` with a predicate that accepts the first non-empty string result, so later plugins aren’t waited on.
3) **Best UX**: show chat and prefill default immediately, then apply transformed prefill only if it arrives quickly and the user hasn’t edited the input.
Any of these avoids an installation-wide stall from a single plugin.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

5. Substring dedupe misses mention 🐞 Bug ≡ Correctness
Description
The dedupe check uses current.includes(prefill.trim()), which can treat partial matches as
already-mentioned (e.g., current contains @bobcat and clicking bob won’t append @bob). This is
rare but can silently skip inserting the intended mention when usernames share prefixes.
Code

src/static/js/pad_userlist.ts[R402-406]

+          if (!current.trim() || /^@\S+\s*$/.test(current.trim())) {
+            $input.val(prefill);
+          } else if (!current.includes(prefill.trim())) {
+            $input.val(`${current.trimEnd()} ${prefill}`);
+          }
Evidence
The code uses a raw substring search rather than checking for a whole-token mention, so prefix
collisions can produce false positives and prevent insertion.

src/static/js/pad_userlist.ts[401-406]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The mention dedupe logic uses substring matching, which can mis-detect similar names.
### Issue Context
`prefill.trim()` is something like `@bob`. `includes('@bob')` is true for `@bobcat`.
### Fix Focus Areas
- src/static/js/pad_userlist.ts[401-406]
### Suggested change
Replace `includes()` with a safer check for a mention token, such as a regex that enforces boundaries:
- Escape `prefill.trim()` for regex use.
- Test something like `/(^|\s)@bob(\s|$)/` (or equivalent) against `current`.
This avoids false positives for prefix-colliding names.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

JohnMcLear added a commit to ether/ep_ai_chat that referenced this pull request May 2, 2026
…ndler

PR #19 originally landed an entire user-list click handler inside
ep_ai_chat. That logic was generic — clicking a user in the user
list to prefill an @-mention is a discoverability win for any
multi-user pad, AI or no AI — and as @JohnMcLear pointed out, it
belongs in Etherpad core, not in this plugin.

The generic feature is now ether/etherpad#7660. This commit shrinks
ep_ai_chat's contribution to the only AI-specific bit: when the
clicked author is the AI, override the default "@AI_Assistant "
prefill with the configured trigger ("@ai " by default) so the
input lands in a useful state.

Implementation:
- Drop the in-plugin click delegation, the chatbox/chaticon DOM
  poking, and the input-prefill logic. Core handles all of that
  uniformly now.
- Add a chatPrefillFromUser client hook that returns the trigger
  string when the clicked authorId matches the AI's. The clientVars
  hook (added earlier in this PR) still exposes the trigger and
  authorId; nothing changes there.
- On older cores the new hook is never fired, so this is a no-op
  on a stock install — graceful degradation.

Net diff vs main is now: a tiny clientVars exposure on the server
and a ~10-line client hook handler. Everything else is core's job.
@JohnMcLear JohnMcLear requested a review from SamTV12345 May 2, 2026 19:09
Comment on lines +379 to +411
$('#otheruserstable').on('click', 'tr[data-authorId]', async function (event) {
// Skip clicks on the color swatch — that has its own click handler
// (color-picker semantics) and shouldn't double up as a chat trigger.
if ($(event.target).closest('.usertdswatch').length) return;
const tr = $(this);
const authorId = tr.attr('data-authorId');
if (!authorId) return;
const name = (tr.find('.usertdname').text() || '').trim();
let prefill = name ? `@${name.replace(/\s+/g, '_')} ` : '';
try {
const transforms = await hooks.aCallAll(
'chatPrefillFromUser', {authorId, name, prefill});
if (Array.isArray(transforms)) {
for (const tr2 of transforms) {
if (typeof tr2 === 'string' && tr2.length > 0) { prefill = tr2; break; }
}
}
} catch { /* never let a misbehaving plugin break the click */ }
try { chat.show(); } catch { /* */ }
setTimeout(() => {
const $input = $('#chatinput');
if (!$input.length) return;
const current = ($input.val() || '') as string;
if (!current.trim() || /^@\S+\s*$/.test(current.trim())) {
$input.val(prefill);
} else if (!current.includes(prefill.trim())) {
$input.val(`${current.trimEnd()} ${prefill}`);
}
$input.trigger('focus');
const elem = $input[0] as HTMLTextAreaElement;
try { elem.setSelectionRange(elem.value.length, elem.value.length); } catch (_e) { /* */ }
}, 50);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Click-to-chat lacks feature flag 📘 Rule violation ⚙ Maintainability

The new click handler on user list rows changes default behavior by opening chat and prefilling an
@name mention without any feature flag gating. This violates the requirement that new features be
disabled by default unless explicitly enabled.
Agent Prompt
## Issue description
A new UX feature (click user row to open chat and prefill `@name`) is enabled by default, but compliance requires new features to be behind a feature flag and disabled by default.

## Issue Context
The click handler is currently registered unconditionally, so all installs get the new behavior without opting in.

## Fix Focus Areas
- src/static/js/pad_userlist.ts[379-411]
- src/static/skins/colibris/src/components/users.css[11-16]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going with a reasoned decline on the feature-flag part of this comment. Replying so the rationale is on the record:

  • Etherpad doesn't gate UX changes behind feature flags. The current settings.json surface is reserved for things that materially change the contract (auth, storage, transport, security knobs, skin selection). Recent UX additions — chat-and-users mode, sticky chat, the showsidebar editbar button, etc. — all shipped on by default with no flag. Adding a flag here would be inconsistent with project precedent.
  • Blast radius is small. Before this PR clicking a user list row was a no-op; nothing depended on that. Anyone who wants the click to do nothing can ship a tiny CSS override (#otheruserstable tr[data-authorId] { pointer-events: none; }) without a settings change.
  • Plugins already have the override they need. The chatPrefillFromUser hook lets a plugin return a non-empty string to substitute the prefill. A plugin that wanted to globally suppress the behavior could trivially return an empty-but-truthy value or hide rows via CSS.

The other comment on this review (rename-input click steals focus) was a real bug and is fixed in a494307a, plus a Playwright spec covering the supported flows and that regression.

Comment thread src/static/js/pad_userlist.ts
Two follow-ups on review of the click-to-chat handler:

1. Bug (Qodo, correctness): clicking the rename <input> on an unnamed
   user's row triggered the new row handler, which then focused
   #chatinput and made it impossible to name unnamed users from the
   user list. Add an early-return that skips form controls inside
   the row (input/textarea/select/button/a/[contenteditable=true]).
   The swatch was already excluded; this widens the same idea to
   anything that's interactive on its own merits.

2. Test coverage: add a frontend Playwright spec
   (userlist_click_to_chat.spec.ts) covering the supported flows
   and the new regression:
   - clicking another named user opens chat and prefills "@<name> "
   - clicking the swatch opens color picker, not chat
   - clicking the rename <input> on an unnamed user keeps focus
     on the input (regression test for the bug above)
   - partial chat message is preserved when prefilling
Copy link
Copy Markdown
Member

@SamTV12345 SamTV12345 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's cool :)

JohnMcLear added 2 commits May 2, 2026 21:05
The 'partial message in chat input is preserved when prefilling'
case was flaking on CI. Three small changes:

- Seed the chat input with fill() rather than click() + keyboard.type().
  Earlier the test was racing chat.focus()'s own setTimeout(100) — when
  the keyboard.type started before that timer fired, the typing landed
  in whatever element had focus at the time, which wasn't always the
  chat input. fill() bypasses focus state entirely.
- Wait for the chat box to be visible before filling, so we don't race
  the chaticon click handler.
- Replace the two sequential expect/wait pairs after the daveRow click
  with one waitForFunction that asserts both 'hi there' and '@dave' are
  in the input together. The prefill is async (setTimeout(50) inside
  the click handler), so a combined wait is more reliable than checking
  one piece, then snapshotting and asserting the other.

The other three cases in this file passed unchanged on CI; only this
fourth one was racy.
These were accidentally added in ffe9477 by an over-broad git add -A.
Both paths are workspace-local and unrelated to this PR.
@JohnMcLear JohnMcLear merged commit 25c4314 into ether:develop May 3, 2026
20 checks passed
JohnMcLear added a commit to ether/ep_ai_chat that referenced this pull request May 3, 2026
…lay name

Override the default "@<name> " chat-input prefill with the configured
AI trigger ("@ai " by default) when a user clicks the AI's row in the
user list. Without this, clicking the AI chip would prefill
"@AI_Assistant ", which doesn't match anything the server-side mention
extractor recognises.

What this PR adds:
- clientVars server hook exposing { trigger, authorName, authorId } at
  clientVars.ep_ai_chat so the client can recognise the AI's row.
- chatPrefillFromUser client hook that returns the trigger string when
  the clicked authorId matches the AI's; otherwise returns nothing so
  core's default ("@<name> ") wins.
- Mocha unit test (static/tests/backend/specs/chat_prefill.ts) covering
  AI/non-AI/missing-clientVars/custom-trigger/missing-trigger paths.

Depends on ether/etherpad#7660 which adds the chatPrefillFromUser hook
to core. On older cores the hook is never fired and this PR is a
benign no-op — graceful degradation, no install requirement bump.

Originally this PR shipped the entire user-list click handler inside
the plugin. Per review feedback, the generic "click a user → prefill
@-mention" UX belongs in core (it's a discoverability win for any
multi-user pad, AI or no AI), so the bulk moved to ether/etherpad#7660
and this PR is now ~40 lines of glue.

(Replaces the previous 3-commit history on this branch with a clean
rebase against the new main, which absorbed PRs #18 and #20 since the
branch was opened.)
JohnMcLear added a commit to ether/ep_ai_chat that referenced this pull request May 3, 2026
…lay name (#19)

Override the default "@<name> " chat-input prefill with the configured
AI trigger ("@ai " by default) when a user clicks the AI's row in the
user list. Without this, clicking the AI chip would prefill
"@AI_Assistant ", which doesn't match anything the server-side mention
extractor recognises.

What this PR adds:
- clientVars server hook exposing { trigger, authorName, authorId } at
  clientVars.ep_ai_chat so the client can recognise the AI's row.
- chatPrefillFromUser client hook that returns the trigger string when
  the clicked authorId matches the AI's; otherwise returns nothing so
  core's default ("@<name> ") wins.
- Mocha unit test (static/tests/backend/specs/chat_prefill.ts) covering
  AI/non-AI/missing-clientVars/custom-trigger/missing-trigger paths.

Depends on ether/etherpad#7660 which adds the chatPrefillFromUser hook
to core. On older cores the hook is never fired and this PR is a
benign no-op — graceful degradation, no install requirement bump.

Originally this PR shipped the entire user-list click handler inside
the plugin. Per review feedback, the generic "click a user → prefill
@-mention" UX belongs in core (it's a discoverability win for any
multi-user pad, AI or no AI), so the bulk moved to ether/etherpad#7660
and this PR is now ~40 lines of glue.

(Replaces the previous 3-commit history on this branch with a clean
rebase against the new main, which absorbed PRs #18 and #20 since the
branch was opened.)
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