Skip to content

feat: add AI assistant example using AI widgets#738

Merged
Karanjot786 merged 3 commits into
Karanjot786:mainfrom
kajalpa302005:feat-ai-assistant-example
Jun 5, 2026
Merged

feat: add AI assistant example using AI widgets#738
Karanjot786 merged 3 commits into
Karanjot786:mainfrom
kajalpa302005:feat-ai-assistant-example

Conversation

@kajalpa302005

@kajalpa302005 kajalpa302005 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a new AI Assistant example under examples/ai-assistant demonstrating the existing AI-related widgets available in the repository. The example supports both mock mode and real Anthropic integration through useAI. This PR also updates the create-termui-app AI assistant template and the root README.

Related Issue

Closes #102

Which package(s)?

  • examples/ai-assistant
  • create-termui-app
  • documentation

Type of Change

  • ✨ Feature (type:feature)
  • 📝 Docs (type:docs)

Checklist

  • ⭐ You starred the repo. The needs-star check blocks your merge otherwise.
  • Tests pass locally: bun vitest run
  • Build passes: bun run build
  • Typecheck passes: bun run typecheck
  • You read CONTRIBUTING.md.
  • Your PR title follows type: short description.
  • Widget state mutators call markDirty() (if your change affects rendering).
  • No new any types without an inline comment explaining why.
  • No unrelated refactors bundled into this PR.

GSSoC 2026 Participation

  • You are a GSSoC 2026 contributor.
  • GSSoC profile: https://gssoc.girlscript.org/profile/84adde4d-7c32-44f4-8f86-5a5f723d95cf

Screenshots / Recordings (UI changes)

N/A

Notes for the Reviewer

  • Uses existing AI widgets available in the repository:

    • ChatMessage
    • StreamingText
    • ToolCall
    • ToolApproval
    • useAI
  • Mock mode is available when ANTHROPIC_API_KEY is not configured.

  • The example follows patterns already used in:

    • examples/ai-streaming
    • examples/chat-app
  • The repository currently does not contain ChatThread or TokenUsage widgets, so the example demonstrates the AI widgets that are currently available in the codebase.

Summary by CodeRabbit

  • New Features

    • Interactive AI Assistant example: dual-mode chat (mock & real), streaming responses, chat history, tool calls/approval, and token usage display
    • Keyboard controls for input, submission, approval, and quitting
  • Documentation

    • Added example README and updated main README to list the AI Assistant with usage and run instructions
  • Templates

    • Included AI Assistant example in project templates and package metadata for local demos

@github-actions github-actions Bot added type:feature +10 pts. New feature. type:docs +5 pts. Documentation. area:examples Example apps. and removed type:feature +10 pts. New feature. labels Jun 4, 2026
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an AI Assistant example under examples/ai-assistant/ (mock + Anthropic modes) with streaming chat, token counters, tool approval demo, and keyboard handling; plus a matching create-termui-app template and package dependency update.

Changes

AI Assistant Example Application

Layer / File(s) Summary
Documentation and project setup
README.md, examples/ai-assistant/README.md, examples/ai-assistant/package.json
Root README lists the new example; example README documents features, modes, controls, and run instructions; package.json adds scripts and workspace dependencies.
AI Assistant widget implementation
examples/ai-assistant/src/index.tsx
Adds AIAssistantApp widget with mock/real mode selection, mockStream generator, UI layout (header, chat history, input, help), streaming pipeline that uses aiAdapter.chat() when available, token counters, tool approval demo, keyboard routing, and main() mount entrypoint.

AI Assistant Template for Code Generation

Layer / File(s) Summary
Template package dependency
packages/create-termui-app/templates/ai-assistant/package.json
Adds @termuijs/adapters to template dependencies.
Template widget implementation
packages/create-termui-app/templates/ai-assistant/src/index.tsx
Reworks the template to a widget-based AIAssistantApp: mock streaming helper, useAI('anthropic', ...) integration, streaming send flow with error handling, keyboard event handling, and main() runtime mount.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • Karanjot786/TermUI#562: useAI adapter exports used by the new AI Assistant example for streaming Anthropic chat responses.

Suggested labels

gssoc:approved, level:advanced

Suggested reviewers

  • Karanjot786

Poem

🐰 I hopped a line of code to play,

Streaming tokens in a bright display,
Mock or Claude, the chat replies,
Tool approvals with y or n replies,
A tiny rabbit cheers: "Run the app today!"

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The PR mostly addresses issue #102 requirements: example boots with dev, mock mode works without API key, uses useAI adapter, and is listed in README. However, ChatThread and TokenUsage widgets mentioned in issue #102 are not available in the codebase, so the example uses currently available AI widgets instead. Clarify whether the absence of ChatThread and TokenUsage (mentioned in issue #102 acceptance criteria) represents incomplete implementation or acceptable scope adjustment given the widgets are not yet in the repository.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main addition: a new AI assistant example demonstrating AI widgets.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #102: the new examples/ai-assistant directory, template updates, and README documentation. No unrelated modifications are present.
Description check ✅ Passed The PR description follows the template structure with all required sections completed: description, related issue, packages, type of change, checklist, GSSoC participation, and reviewer notes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the type:feature +10 pts. New feature. label Jun 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
examples/ai-assistant/src/index.tsx (2)

337-339: ⚡ Quick win

Polling loop: prefer Promise-based control flow.

The while (!toolDone) loop polls every 100ms until the tool completes. While this works (the await yields control), it's not idiomatic. A cleaner pattern would wrap the approval workflow in a Promise that resolves when onApprove or onDeny fires.

♻️ Use Promise-based completion
   private async demonstrateToolApproval(): Promise<void> {
     // ... existing ToolCall widget code ...

-    const tool = EXAMPLE_TOOLS[Math.floor(Math.random() * EXAMPLE_TOOLS.length)];
-    let toolDone = false;
+    const tool = EXAMPLE_TOOLS[Math.floor(Math.random() * EXAMPLE_TOOLS.length)];
 
+    await new Promise<void>((resolve) => {
       const toolWidget = new ToolApproval(
         {
           name: tool.name,
           args: tool.args,
           status: 'pending',
           collapsed: false,
           onApprove: () => {
             toolWidget.setStatus('running');
             setTimeout(() => {
               toolWidget.setStatus('done');
               toolWidget.setResult(`${tool.name} executed successfully`);
-              toolDone = true;
+              resolve();
             }, 500);
           },
           onDeny: () => {
             toolWidget.setStatus('error');
             toolWidget.setResult('Tool execution denied');
-            toolDone = true;
+            resolve();
           },
         },
         { border: 'single', height: 6 }
       );
 
       this.toolApprovalWidget = toolWidget;
       this.chatContainer.addChild(toolWidget);
       this.markDirty();
-
-    // Wait for approval/denial
-    while (!toolDone) {
-      await new Promise(r => setTimeout(r, 100));
-    }
+    });
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ai-assistant/src/index.tsx` around lines 337 - 339, The polling loop
using toolDone should be replaced with a Promise-based completion to avoid
busy-waiting: create a function (e.g., waitForApproval) that returns new Promise
and resolves when the approval handlers run (resolve on onApprove or onDeny),
wire the modal/approval callbacks to call those resolve paths, and await that
Promise instead of looping; update usages that currently rely on the while
(!toolDone) loop to await waitForApproval so control resumes once onApprove or
onDeny fires.

207-209: ⚡ Quick win

Dead code: allMessages is constructed but never meaningfully used.

The allMessages array is initialized with just the assistant greeting, but the user's message (userText) is never added to it. Later at lines 235-238, a new aiMessages array is constructed that includes the same greeting and the user message. The allMessages variable adds no value and makes the flow harder to follow.

♻️ Simplify by removing unused variable
-      // Collect all messages for API call
-      const allMessages: AIMessage[] = [
-        { role: 'assistant', content: 'Hi! I am Claude. How can I help you?' },
-      ];
-
       // Stream response
       let fullResponse = '';

Then update line 235-237:

       } else {
         const aiMessages: AIMessage[] = [
-          ...allMessages,
+          { role: 'assistant', content: 'Hi! I am Claude. How can I help you?' },
           { role: 'user', content: userText },
         ];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ai-assistant/src/index.tsx` around lines 207 - 209, Remove the
unused allMessages variable and any references to it: delete the const
allMessages: AIMessage[] = [{ role: 'assistant', content: 'Hi! I am Claude. How
can I help you?' }]; declaration and ensure the aiMessages construction (where
aiMessages is built from the assistant greeting and userText) remains the single
source of truth; verify aiMessages uses userText and the greeting so user input
is included and no dead variable remains (check usages of allMessages elsewhere
and remove or replace with aiMessages if present).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/ai-assistant/README.md`:
- Line 72: The README says tool calls should only run in real mode but the code
uses Math.random() > 0.6 without a mode check; update the condition around the
Math.random() call so it also verifies the app is in real mode (e.g., combine a
check like mode === 'real' or !isMock with the existing Math.random() > 0.6) so
tool demonstrations only trigger in real mode; locate the Math.random() > 0.6
expression and change the surrounding if to include the real-mode flag (for
example: if (mode === 'real' && Math.random() > 0.6) or equivalent based on the
existing mode variable).

In `@examples/ai-assistant/src/index.tsx`:
- Around line 249-250: The inputTokens counter is only updated inside the
real-mode branch, causing the "in:" counter to remain 0 in mock mode; move the
increment and call to updateTokenLabel so it runs for both modes (i.e., take the
lines that update this.inputTokens and this.updateTokenLabel out of the else
block and place them after the mock/real conditional). Locate the token handling
around the output token updates (where output tokens are already updated in both
branches) and ensure the same input-token update is executed regardless of mock
mode by referencing the this.inputTokens property and the updateTokenLabel()
method.

In `@packages/create-termui-app/templates/ai-assistant/src/index.tsx`:
- Around line 157-194: The streaming widget (this.streamingTextWidget) must be
removed from this.chatContainer on all paths; move the cleanup that currently
sits before creating the assistantMessage into the finally block and guard it
with a null check (e.g., if (this.streamingTextWidget) {
this.chatContainer.removeChild(this.streamingTextWidget);
this.streamingTextWidget = undefined; }) so the StreamingText instance is
removed whether the stream completes or an exception is thrown; keep the
assistant error/regular ChatMessage creation in catch/try as-is but remove the
earlier unconditional removeChild call to avoid double-removal.
- Around line 1-4: The file contains a duplicate JSX app implementation (a
second "function App()" and a render(<App />...) block) that redeclares IS_MOCK
and mockStream and references undeclared symbols (useTheme, useState, useKeymap,
ErrorBoundary, AutoThemeProvider, Message, TokenUsageData, ToolCallState) and
conflicts with the imported App from '`@termuijs/core`'; remove or extract that
trailing JSX example (the code after main()) into a separate template/file and
keep this entrypoint as a single self-booting implementation that calls main(),
ensuring you do not redeclare IS_MOCK or mockStream here and either import or
mock any previously missing symbols in the separate example file if kept.
- Around line 42-49: The code currently treats API-key presence (IS_MOCK) as the
source of truth while ignoring runtime failures from useAI('anthropic', ...) so
the app still shows "[claude]" and real greeting even when this.aiAdapter
failed; change the failure path to set a runtime "mock fallback" state and
surface it to the UI and send logic: in the catch block after useAI(...) fails,
set this.aiAdapter = null and set a new flag (e.g., this.isMockFallback = true
or update existing state used by AiAssistant) and ensure handleSendMessage,
mockStream selection (IS_MOCK || !this.aiAdapter), and the AiAssistant
component's ai/send logic read that runtime flag (or directly test
this.aiAdapter presence) so the header label and initial greeting reflect the
mock fallback; also keep the console.error call with the captured error.

---

Nitpick comments:
In `@examples/ai-assistant/src/index.tsx`:
- Around line 337-339: The polling loop using toolDone should be replaced with a
Promise-based completion to avoid busy-waiting: create a function (e.g.,
waitForApproval) that returns new Promise and resolves when the approval
handlers run (resolve on onApprove or onDeny), wire the modal/approval callbacks
to call those resolve paths, and await that Promise instead of looping; update
usages that currently rely on the while (!toolDone) loop to await
waitForApproval so control resumes once onApprove or onDeny fires.
- Around line 207-209: Remove the unused allMessages variable and any references
to it: delete the const allMessages: AIMessage[] = [{ role: 'assistant',
content: 'Hi! I am Claude. How can I help you?' }]; declaration and ensure the
aiMessages construction (where aiMessages is built from the assistant greeting
and userText) remains the single source of truth; verify aiMessages uses
userText and the greeting so user input is included and no dead variable remains
(check usages of allMessages elsewhere and remove or replace with aiMessages if
present).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 329ace8c-d934-4e3e-b67c-09b2d396fa1f

📥 Commits

Reviewing files that changed from the base of the PR and between f90f75c and b53b796.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • README.md
  • examples/ai-assistant/README.md
  • examples/ai-assistant/package.json
  • examples/ai-assistant/src/index.tsx
  • packages/create-termui-app/templates/ai-assistant/package.json
  • packages/create-termui-app/templates/ai-assistant/src/index.tsx

Comment thread examples/ai-assistant/README.md Outdated
Comment thread examples/ai-assistant/src/index.tsx Outdated
Comment thread packages/create-termui-app/templates/ai-assistant/src/index.tsx
Comment thread packages/create-termui-app/templates/ai-assistant/src/index.tsx
Comment thread packages/create-termui-app/templates/ai-assistant/src/index.tsx Outdated
@kajalpa302005

Copy link
Copy Markdown
Contributor Author

Hi @Karanjot786,
The CI failure seems unrelated to this PR.
The failing tests are coming from:
packages/widgets/src/data/Meter.ts

This PR only adds the AI Assistant example, updates the create-termui-app template, and documentation.

The Meter file is not modified in this PR. Please let me know if I should rebase once the issue is resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/create-termui-app/templates/ai-assistant/src/index.tsx (1)

43-52: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

useAI() is too lazy for this fallback check.

useAI('anthropic', ...) only returns an adapter object. The Anthropic SDK is loaded later inside chat()/generate(), so this catch will not fire for the common “SDK missing” path. The template can still render [claude] and the Claude greeting even though the first send will fail. Move the mock/real-mode decision to an eager provider check, or flip the UI into fallback mode when the first real stream errors. Based on learnings: "A template must boot on its own; if it references a widget or adapter that does not exist yet, provide a self-contained mock so the scaffold runs without that dependency."

#!/bin/bash
set -euo pipefail

echo "== Template constructor =="
sed -n '43,52p' packages/create-termui-app/templates/ai-assistant/src/index.tsx | nl -ba

echo
echo "== Adapter implementation =="
sed -n '1,120p' packages/adapters/src/ai/index.ts | nl -ba

echo
echo "== Relevant adapter symbols =="
rg -n "export function useAI|loadAnthropic|async generate|async \\*chat" packages/adapters/src/ai/index.ts

Expected result: useAI() returns an adapter immediately, and loadAnthropic() only appears inside generate()/chat(), not during construction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/create-termui-app/templates/ai-assistant/src/index.tsx` around lines
43 - 52, The constructor's try/catch around useAI('anthropic', ...) is
ineffective because loadAnthropic is lazy and errors only surface in
chat()/generate(); change the logic so useAI always returns an adapter object
immediately (do not rely on that try/catch to detect missing SDK) and implement
an eager-fallback strategy: keep this.aiAdapter = useAI('anthropic', {...})
as-is, remove the constructor-level SDK detection, and instead handle failures
inside the async paths (aiAdapter.chat / aiAdapter.generate) by catching SDK
errors there, setting this.isMockMode = true, swapping this.aiAdapter for a
self-contained mock adapter, and updating the UI to show the fallback; ensure
loadAnthropic remains only inside generate/chat and that useAI returns a safe
adapter object even when the SDK is absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/create-termui-app/templates/ai-assistant/src/index.tsx`:
- Around line 43-52: The constructor's try/catch around useAI('anthropic', ...)
is ineffective because loadAnthropic is lazy and errors only surface in
chat()/generate(); change the logic so useAI always returns an adapter object
immediately (do not rely on that try/catch to detect missing SDK) and implement
an eager-fallback strategy: keep this.aiAdapter = useAI('anthropic', {...})
as-is, remove the constructor-level SDK detection, and instead handle failures
inside the async paths (aiAdapter.chat / aiAdapter.generate) by catching SDK
errors there, setting this.isMockMode = true, swapping this.aiAdapter for a
self-contained mock adapter, and updating the UI to show the fallback; ensure
loadAnthropic remains only inside generate/chat and that useAI returns a safe
adapter object even when the SDK is absent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 22d59161-a27b-4ef2-ab42-9469e12c0cfe

📥 Commits

Reviewing files that changed from the base of the PR and between b53b796 and be3d818.

📒 Files selected for processing (3)
  • examples/ai-assistant/README.md
  • examples/ai-assistant/src/index.tsx
  • packages/create-termui-app/templates/ai-assistant/src/index.tsx
✅ Files skipped from review due to trivial changes (1)
  • examples/ai-assistant/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/ai-assistant/src/index.tsx

@Karanjot786 Karanjot786 added gssoc:approved Approved PR. Earns +50 base points. quality:clean x 1.2 multiplier. Clean implementation. level:intermediate +35 pts. Moderate task. labels Jun 5, 2026
@Karanjot786 Karanjot786 merged commit be5b490 into Karanjot786:main Jun 5, 2026
6 checks passed
@Karanjot786

Copy link
Copy Markdown
Owner

Thank you so much for your contribution, @kajalpa302005!

Your PR #738 added an AI assistant example using the AI widgets. It shows exactly how to wire up a streaming AI response inside a terminal UI.

Keep contributing. We appreciate your work.

Debangana-Dutta pushed a commit to Debangana-Dutta/TermUI that referenced this pull request Jun 6, 2026
Co-authored-by: Karanjot786 <karanjots801@gmail.com>
@coderabbitai coderabbitai Bot mentioned this pull request Jun 11, 2026
20 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:examples Example apps. gssoc:approved Approved PR. Earns +50 base points. level:intermediate +35 pts. Moderate task. quality:clean x 1.2 multiplier. Clean implementation. type:docs +5 pts. Documentation. type:feature +10 pts. New feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] Add AI assistant example app showing all AI widgets

2 participants