Skip to content

Unified input attachments with extensible plugin system#42

Merged
wende merged 3 commits intomainfrom
feat/unified-input-attachments
Mar 20, 2026
Merged

Unified input attachments with extensible plugin system#42
wende merged 3 commits intomainfrom
feat/unified-input-attachments

Conversation

@wende
Copy link
Copy Markdown
Owner

@wende wende commented Mar 20, 2026

Summary

  • Replaces dual image/quote state management in ChatInput with a unified InputAttachment model backed by a plugin registry, mirroring the existing message plugin architecture
  • Adds InputAttachmentPlugin interface (renderPreview, toSendContribution, cleanup) with a registry combining built-in + app plugins
  • Threads addInputAttachment through the message plugin rendering chain so message plugins can inject compose-bar attachments
  • Ships a demo context_chip message plugin that attaches context to the compose bar, rendering as a styled card in sent user messages
  • Documents the full input attachment plugin system in docs/plugin-system.md
  • Adds 32 new tests covering the hook, registry, all plugins, context chip rendering, and demo mode integration

Test plan

  • pnpm build — type-check + production build passes
  • pnpm test — 245 tests pass (32 new)
  • make build-web — iOS web build succeeds
  • Manual: paste/select image → appears as thumbnail in unified strip
  • Manual: select assistant text → quote appears in same strip
  • Manual: dismiss individual attachments
  • Manual: send with image+quote → message has > prefix + image content
  • Manual: type attach in demo mode → context chip appears → click Attach → chip appears in compose bar → send → styled card in user bubble

Replace dual image/quote state management in ChatInput with a unified
InputAttachment model backed by a plugin registry, mirroring the
existing message plugin architecture.

- InputAttachment is now a generic { kind, data } envelope
- useInputAttachments hook manages lifecycle and registry-delegated cleanup
- InputAttachmentPlugin interface: renderPreview, toSendContribution, cleanup
- Built-in plugins for image, file, and quote kinds
- inputAttachmentRegistry merges builtins + app plugins (same pattern as pluginRegistry)
- ChatInput delegates preview rendering and send conversion to the registry
- addInputAttachment callback threaded through PluginViewProps → PluginRenderer →
  MessageRow → ChatViewport → page.tsx so message plugins can inject attachments
- Demo context_chip plugin: renders Attach button in chat, adds prompt_context
  to compose bar on click, renders as styled chip in sent user messages
- UserTextWithQuotes detects [context: label] headers in quoted blocks
  and renders them as document-style cards instead of plain blockquotes
- Plugin system docs updated with full Input Attachment Plugins section
- 32 new tests across 4 test files covering hook, registry, all plugins,
  context chip rendering, and demo mode integration
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mobileclaw Ready Ready Preview, Comment Mar 20, 2026 4:55pm

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust and extensible plugin system for managing input attachments in the chat composer. By unifying image and quote handling under a generic InputAttachment model and providing a plugin registry, it simplifies the process of adding new attachment types. This architectural shift enhances modularity and maintainability, allowing for greater flexibility in how users interact with the compose bar and how various content types are prepared for sending. The changes also include a new hook for managing attachment state and a demo plugin to showcase the system's capabilities.

Highlights

  • Unified Input Attachment System: Replaced the separate state management for image and quote attachments in the ChatInput component with a unified InputAttachment model, leveraging an extensible plugin registry.
  • Input Attachment Plugin Architecture: Introduced a new InputAttachmentPlugin interface (renderPreview, toSendContribution, cleanup) and a registry that combines built-in and app-specific plugins, allowing for easy extension of compose-bar attachment types.
  • Message Plugin Integration: Enabled message plugins to inject compose-bar attachments by threading an addInputAttachment function through the message plugin rendering chain.
  • Context Chip Demo Plugin: Added a demo context_chip message plugin that allows users to attach context to the compose bar, which then renders as a styled card in sent user messages.
  • Comprehensive Documentation: Provided detailed documentation for the new input attachment plugin system in docs/plugin-system.md, explaining its motivation, interface, storage, registry, and usage.
  • New Hook for Attachment Management: Created useInputAttachments hook to manage the lifecycle and state of input attachments, including adding, removing, and clearing attachments, and handling resource cleanup.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In ChatInput, hasContent recomputes toSendContribution for every attachment on each render and only checks for images, so text-only attachments (e.g. quotes or prompt_context) don't count as content; consider tracking a simple hasContentfulAttachments flag in the hook or extending the plugin interface with a cheap hasContent/kind check instead of calling toSendContribution in render.
  • The InputAttachmentParseResult type in inputAttachmentTypes.ts is currently unused; either wire it into the plugin flow (e.g. for validating attachment data like message plugins do) or remove it to avoid confusion.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In ChatInput, `hasContent` recomputes `toSendContribution` for every attachment on each render and only checks for `images`, so text-only attachments (e.g. quotes or `prompt_context`) don't count as content; consider tracking a simple `hasContentfulAttachments` flag in the hook or extending the plugin interface with a cheap `hasContent`/`kind` check instead of calling `toSendContribution` in render.
- The `InputAttachmentParseResult` type in `inputAttachmentTypes.ts` is currently unused; either wire it into the plugin flow (e.g. for validating attachment `data` like message plugins do) or remove it to avoid confusion.

## Individual Comments

### Comment 1
<location path="components/ChatInput.tsx" line_range="345-349" />
<code_context>

   const isPill = scrollPhase === "pill";
-  const hasContent = !!value.trim() || attachments.length > 0;
+  const hasContent = !!value.trim() || attachments.some((att) => {
+    const plugin = inputAttachmentRegistry.get(att.kind);
+    if (!plugin) return false;
+    const c = plugin.toSendContribution(att.data);
+    return !!c.images?.length;
+  });

</code_context>
<issue_to_address>
**issue (bug_risk):** Send-button `hasContent` ignores text-only attachment contributions.

The current `hasContent` check only looks at `c.images?.length`, so attachments that contribute only `textPrefix` (e.g. `quote`, `prompt_context`) won’t enable the send button when the textarea is empty. But `submit()` still sends when `attachments.length > 0`, so the button state can disagree with what’s actually sendable.

To align the UI with the submit behavior, consider treating any non-empty send contribution as content, for example:

```ts
const hasContent = !!value.trim() || attachments.some((att) => {
  const plugin = inputAttachmentRegistry.get(att.kind);
  if (!plugin) return false;
  const c = plugin.toSendContribution(att.data);
  return !!c.images?.length || !!c.textPrefix?.trim();
});
```
</issue_to_address>

### Comment 2
<location path="lib/demoMode.ts" line_range="693" />
<code_context>
     return RESPONSES.weather;
   if (lower.includes("plugin") || lower.includes("widget"))
     return buildPluginDemoResponse();
+  if (lower.includes("attach") || lower.includes("context chip"))
+    return buildAttachDemoResponse();
   if (lower.includes("pause") || lower.includes("approval"))
</code_context>
<issue_to_address>
**issue (bug_risk):** Demo keyword matching doesn’t align with the documented `context` trigger.

The help text lists `attach / context` as triggers, but the matcher only looks for `"attach"` and `"context chip"`, so typing just `context` won’t match. To keep behavior consistent with the docs, please include plain `"context"` in the check, e.g.:

```ts
if (lower.includes("attach") || lower.includes("context chip") || lower.includes("context"))
  return buildAttachDemoResponse();
```

If you’re concerned about collisions with other demos, you could tighten the matching logic (e.g., word boundaries or more specific phrases).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +345 to +349
const hasContent = !!value.trim() || attachments.some((att) => {
const plugin = inputAttachmentRegistry.get(att.kind);
if (!plugin) return false;
const c = plugin.toSendContribution(att.data);
return !!c.images?.length;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Send-button hasContent ignores text-only attachment contributions.

The current hasContent check only looks at c.images?.length, so attachments that contribute only textPrefix (e.g. quote, prompt_context) won’t enable the send button when the textarea is empty. But submit() still sends when attachments.length > 0, so the button state can disagree with what’s actually sendable.

To align the UI with the submit behavior, consider treating any non-empty send contribution as content, for example:

const hasContent = !!value.trim() || attachments.some((att) => {
  const plugin = inputAttachmentRegistry.get(att.kind);
  if (!plugin) return false;
  const c = plugin.toSendContribution(att.data);
  return !!c.images?.length || !!c.textPrefix?.trim();
});

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

The pull request introduces a robust and extensible input attachment plugin system, mirroring the existing message plugin architecture. This is a significant architectural improvement that centralizes attachment handling, making it easier to add new types of content to the compose bar. The changes are well-documented in docs/plugin-system.md and thoroughly tested with new unit tests, covering the core functionality of the new hooks, plugins, and registry. The integration into the ChatInput and MessageRow components is well-executed, and the demo mode updates provide a clear example of the new system in action.

Comment on lines +345 to +349
const hasContent = !!value.trim() || attachments.some((att) => {
const plugin = inputAttachmentRegistry.get(att.kind);
if (!plugin) return false;
const c = plugin.toSendContribution(att.data);
return !!c.images?.length;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The hasContent variable currently checks if there's trimmed text or if any attachment contributes images. However, attachments like the context_chip (prompt_context) contribute only a textPrefix and no images. In such cases, if the input field is empty and only a prompt_context attachment is present, hasContent will evaluate to false.

This leads to the send button being disabled, preventing the user from sending a message that consists solely of a text-prefix-only attachment. The submit function's guard (if (!t && attachments.length === 0) return;) correctly allows submission if there are attachments, but the UI's isActive logic (which depends on hasContent) incorrectly disables the button.

To fix this, hasContent should reflect whether there is any content to send, which includes text in the input field OR any attachments, regardless of whether they contribute images or text prefixes.

  const hasContent = !!value.trim() || attachments.length > 0;

- Simplify hasContent to treat any attachment as sendable content,
  fixing send button staying disabled for text-only attachments
- Add "context" keyword to demo mode matcher to match documented triggers
- Remove unused InputAttachmentParseResult type
@wende wende merged commit 900a049 into main Mar 20, 2026
4 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.

1 participant