ianvs_agent_chat
Reusable Flutter Agent Chat UI for ACP-powered agents and OpenAI-compatible LLM APIs. The timeline and composer are shared; your host retains ownership of connections, workspaces, persistence and tool execution.
Features
- Streaming messages, Markdown, highlighted code, Mermaid and bounded image previews.
- Tool activity groups, execution details, plans, permission choices and conversation navigation.
- A common
ChatSessioninterface, capability-driven controls and independent session lifetimes. - Optional OpenAI-compatible Chat Completions adapter with history, cancellation and approved host tools.
- Theme, tool presentation and platform extension points.
- A runnable macOS example with offline demo and real API connection screens.
Install
dependencies:
ianvs_agent_chat: ^0.3.0
Requires Dart 3.12 or later and Flutter. The desktop integration is validated on macOS. Other platforms require testing their file, drag/drop, clipboard and Mermaid integrations; this release does not claim full platform parity.
Use an LLM API
import 'package:ianvs_agent_core/ianvs_agent_core.dart';
import 'package:ianvs_agent_chat/ianvs_agent_chat.dart';
import 'package:ianvs_agent_chat/llm.dart';
final session = LlmChatSession(
client: OpenAiChatClient(
endpoint: Uri.parse('https://your-provider.example/v1/chat/completions'),
model: modelName,
apiKey: apiKeyFromYourHost,
),
);
// Put this widget inside a bounded layout, e.g. Scaffold.body or Expanded.
AgentChatView(session: session);
// The host owns this lifetime. Removing AgentChatView does not dispose it.
session.dispose();
endpoint is the full Chat Completions URL. Headers and provider extensions can be supplied using headers and extraBody; reserved request fields cannot be overridden. For DeepSeek, use its completion endpoint and model name; an optional extension is extraBody: {'thinking': {'type': 'disabled'}}. Streamed reasoning_content is retained for tool round trips when supplied by a compatible provider.
For an optional connection form that keeps credentials in memory:
import 'package:ianvs_agent_chat/llm_chat_panel.dart';
const LlmChatPanel();
The panel owns and disposes its session. It does not persist API keys. For public web clients, supply an authenticated backend proxy instead of embedding a provider secret.
Tools and approval
Register only the tools your host wants the model to access. Validate business arguments inside each executor. Executors receive a cancellation token and should honor it; stopping a conversation cannot reverse a tool's completed side effects.
final tools = [
ChatTool(
name: 'add_integers',
description: 'Add two integers.',
parameters: {
'type': 'object',
'properties': {
'a': {'type': 'integer'},
'b': {'type': 'integer'},
},
'required': ['a', 'b'],
},
execute: (arguments, cancellation) async {
cancellation.check();
return '${(arguments['a'] as int) + (arguments['b'] as int)}';
},
),
];
Approval is required by default. AgentChatView renders the pending request and routes the user's choice back to the session. Denied, invalid and failed tools become tool results without silently executing. Set requiresApproval: false only when your host has authorized that tool.
The adapter supports text and explicitly enabled inline image inputs. Audio, arbitrary file uploads, server-side session restore and automatic model discovery are not implemented. It retains completed turns in memory and trims whole turns to configured context limits. Failed or cancelled turns remain visible but are excluded from subsequent model context.
Connect an ACP client
The package does not bundle an ACP transport, process runner or the Ianvs application controller. Adapt your existing client using ChatSession or CallbackChatSession:
final session = CallbackChatSession(
changes: yourSessionStateStream,
readState: () => ChatSessionState(
identity: yourSessionId,
agentName: 'My ACP Agent',
messages: projectedMessages,
messagesRevision: transcriptRevision,
isSending: isStreaming,
isLoading: isRestoring,
capabilities: const ChatCapabilities(tools: true, permissions: true),
permission: projectedPermission,
),
onSend: (text, attachments) => sendToAgent(text, attachments),
onStop: cancelAgentPrompt,
onResolvePermission: resolveAgentPermission,
);
Project protocol data into the public models. ChatToolMessage, ChatPlanMessage and ChatThoughtMessage supply typed construction helpers. An existing buffered message model can implement ChatMessageView to avoid copying a long transcript on every token. Increment message revisions and messagesRevision for in-place updates, and change session identity when switching conversations. Keep restore staging invisible until your complete snapshot is ready.
The originating Ianvs app uses this boundary in lib/chat/acp_chat_session.dart, preserving its ACP replay, storage, permission and queue logic.
Drafts and submission admission
For quick prompts or host context actions, retain a ChatComposerController,
pass it to AgentChatView(composerController: draft), then call
draft.replaceText(...), draft.insertText(...) or draft.requestFocus().
It exposes text, selection and revision. clear() also clears mounted
attachments. The host disposes its controller after unmounting the view.
A different state.identity clears a reused controller. Keep identity stable
through first-send backend initialization, configuration and saved-result version
changes. Separate controllers can retain separate text drafts across mounts.
Implement ChatSubmissionSession alongside ChatSession for reliable admission:
final receipts = ChatSubmissionLedger();
Future<ChatSubmitResult> submit(ChatSubmission draft) => receipts.submit(
draft,
() async {
if (draft.sessionIdentity != state.identity) {
return const ChatSubmitResult.rejected('The conversation changed.');
}
// Validate the final payload and take ownership/start generation here.
// Return before waiting for generation to finish.
await admitPrompt(draft.text, draft.attachments);
return const ChatSubmitResult.accepted();
},
);
ChatSubmission freezes id, sessionIdentity, draftRevision, text and
attachments. Accepted or queued results clear only the matching draft revision;
rejections and thrown admission errors preserve the draft and show a reason.
Editing while admission is pending is allowed; repeated submits are disabled.
The ledger deduplicates pending and completed IDs. An intentional retry needs a
new ID. Preserve one ledger for the adapter's lifetime, including across view
rebuilds. Validate the captured conversation again after any asynchronous setup
before admitting it. Failures after acceptance belong in the transcript/error.
CallbackSubmissionChatSession provides the same contract through onSubmit;
CallbackChatSession and legacy onSend callers retain immediate clearing.
Existing send completion timing is unchanged. LLM submit returns admission,
while LLM send still waits for generation. Do not call both for one request.
Wrap the default child with composerBuilder to show a quote or business action.
Capture host context and its revision synchronously on submission and clear it
only after matching acceptance. Keep article fields and save/copy actions in the
host. Project models, modes and booleans into configOptions; all selectable
options are reachable. Advertise capabilities only for implemented operations.
Customize
The app and this package use ianvs_design for Material 3 controls, light/dark
surfaces, spacing and monospaced typography. Configure the host with
IanvsTheme.light(), IanvsTheme.dark() and ThemeMode.system. Standalone
ChatTimeline and PromptInput also derive their palette from the ambient
Ianvs theme. A plain Material host receives Ianvs defaults matching its brightness.
ChatTheme(
data: const ChatThemeData(
colors: {'userMessageSurface': Color(0xffeef4ff)},
contentMaxWidth: 900,
),
child: AgentChatView(session: session),
);
- Use
ChatThemefor conversation surfaces, semantic status colors and content width. Unspecified colors continue to follow the host theme, including after a light/dark switch. Markdown typography and colors belong toianvs_markdown; the legacybodyFontSizefield no longer overrides message rendering. Mermaid rendering follows host brightness. - Use
ChatStringsto override primary composer labels. Full localization is not included in this release. - Supply
ToolPresentationRegistryrules for tool families. - Supply
timelineBuilderorcomposerBuilderto wrap or replace either region. - Use
ChatPlatformScopeto override file picking, image loading, path resolution and working-directory display. - Supply
readClipboardImageto enable your host's image clipboard. No application-specific native channel is required. - Use
ChatTimelineandPromptInputindependently when your host owns layout.
Optional diagrams and independent message content
ChatMessageBody(text: source) embeds content without a conversation scroll view.
It accepts documentSelection, softLineBreak, clipboardWriter, onTapLink,
inputBudget, resourcePolicy, and an optional diagramBuilder. External images
are blocked by default. Only an explicit host resource callback may load them.
Links are delegated to the host. With no diagram builder, Mermaid remains source.
ChatToolCard, ChatPlanCard, and ChatPermissionCard expose standalone content
presentation; they do not own protocol or execution. ChatSessionBinding adapts
core streams to Flutter and never disposes the session on unmount.
Native Mermaid lives in sibling ianvs_mermaid. A desktop host may inject it via
ChatRenderingScope; the basic chat package has no merman dependency. The
OpenAI-compatible adapter remains the optional llm.dart entry point in this
package and implements the pure Dart core stream contract.
The shared input defaults to Flutter semantics, including on macOS. Only a host
that registers com.ianvs.acp/accessible-text-field should wrap its application
in ChatNativeTextFieldScope. The standalone example and ordinary consumers do
not need that platform view. A supplied composer controller focuses the actual
editable field.
Markdown uses ianvs_markdown's standard GFM preset, built-in typography, colors,
code blocks and file-link chips. The renderer resolves its own light/dark palette
or an ambient IanvsMarkdownThemeData extension; ChatTheme does not override
Markdown styling. Chat retains blocked resource images with link copying and
accepts optional diagrams through diagramBuilder. It keeps
block-level selection (documentSelection: false); full document copy remains a
host action. Obsidian comment/block-ID normalization and HTML form controls are
not enabled. Both preflight and rendering use the same syntax/fallback limits.
The Markdown dependency also brings super_native_extensions; validate the
complete native dependency graph in the consuming application.
The standard file picker requires the appropriate user-selected file entitlement; API calls require the network-client entitlement. The example includes both.
Example and validation
cd example
flutter pub get
flutter run -d macos
The offline demo exercises tool approval and streaming. The LLM API tab accepts your endpoint, model and API key. Its optional current_time tool reads the local clock after approval.
flutter analyze
flutter test
Implementation references: OpenAI Chat Completions, DeepSeek Chat Completions.
License
Apache-2.0.
Embedding in a mixed timeline
Import the presentation widgets from package:ianvs_agent_chat/ianvs_agent_chat.dart
and message roles/models from package:ianvs_agent_core/ianvs_agent_core.dart.
ChatMessageFrame(role:, child:, roleLabel:, leading:, actions:, footer:)owns only visual framing. It puts roles above content below 600px and uses a bounded role column on wider layouts. Every role uses the same 12px content inset.ChatDecisionCard(title:, description:, body:, actions:)accepts Widgets, including the host's existing keyed/focused buttons. It never grants permission.ChatPermissionCardadapts an ACP permission request to that surface.ChatCodePreview(source:, onCopy:, expanded:, onExpandedChanged:)renders literal text with horizontal scrolling and a bounded preview. CRLF and CR normalize only for display. Copy is absent unless explicitly enabled, and receives the complete original source. Expanded previews delegate vertical scrolling to their parent.ChatToolCard(message:, showDetails: false, footer:)keeps the tool summary but suppresses default output/content, including images and diffs. A host may injectdetailsand a link to its original execution record; optionalexpandedandonExpandedChangedkeep disclosure state in the host. Tool completion does not establish a command exit status.ChatPlanCard(message:)remains independently embeddable.
PromptInput.hosted(
editor: existingComposerEditor, // Own controller, FocusNode, IME and history.
header: modeSelector,
contextStrip: contextSelection,
actions: existingActionRow, // Own submit / stop-generation / interrupt actions.
footer: targetStatus,
)
Hosted mode creates no text/focus/controller objects and disposes none of the
host's objects. Editor and action slots have stable keyed subtrees. It adds no
keyboard shortcuts or send/stop controls. The host owns keyboard insets, safe
areas, height bounds, mode switching and PTY input ownership. Regular PromptInput
also accepts header, contextStrip and footer; its managed submission behavior
is unchanged. Both modes share the composer surface styling.
These APIs are available starting with chat 0.3.0; chat 0.2.0 does not contain them.
See the repository's docs/architecture/shared-agent-release-plan.md for the
candidate versions, dependency order and hosted-consumer verification gate.
Libraries
- agent_chat_view
- chat_composer_controller
- chat_session_binding
- chat_strings
- chat_theme
- ianvs_agent_chat
- llm
- llm/llm_chat_session
- llm/openai_chat_client
- llm_chat_panel
- platform/bounded_file_snapshot
- platform/chat_platform
- platform/chat_platform_io
- platform/chat_platform_stub
- platform/prompt_image_clipboard
- ui/bounded_metadata_preview
- ui/components/accessible_text_field
- ui/components/bounded_image_preview
- ui/components/chat_code_preview
- ui/components/chat_decision_card
- ui/components/chat_message_frame
- ui/components/chat_permission_card
- ui/components/chat_timeline
- ui/components/markdown_code_block
- ui/components/markdown_inline_link
- ui/components/prompt_input
- ui/components/scroll_fade_region
- ui/image_decode_budget
- ui/markdown_render_budget
- ui/tool_presentation/tool_presentation_registry
- ui/user_message_projection