diff --git a/components/ChatWindow.tsx b/components/ChatWindow.tsx
index 794a34cf3..8f6eda692 100644
--- a/components/ChatWindow.tsx
+++ b/components/ChatWindow.tsx
@@ -9,6 +9,7 @@ import { MessageView } from "./MessageView";
import { ChatInput, type ChatInputHandle } from "./ChatInput";
import { ChatMinimap, useMessageRefs } from "./ChatMinimap";
import { ExtensionStatusBar } from "./ExtensionStatusBar";
+import { ExtensionWidgets } from "./ExtensionWidgets";
import { useI18n } from "@/hooks/useI18n";
import { useAgentSession, type AgentPhase, type NoticeItem } from "@/hooks/useAgentSession";
import { useAudio } from "@/hooks/useAudio";
@@ -486,7 +487,9 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate
+
{chatInputElement}
+
) : (
@@ -510,8 +513,6 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate
-
-
{(() => {
const toolResultsMap = new Map
();
for (const msg of messages) {
@@ -746,40 +747,24 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate
}}
>
-
+
{chatInputElement}
-
-
- >
- )}
-
- );
-}
-
-function ExtensionWidgets({ widgets }: { widgets: Array<{ key: string; lines: string[] }> }) {
- if (widgets.length === 0) return null;
- return (
-
- {widgets.map((widget) => (
-
- {widget.key}
+
+
-
- {widget.lines.join("\n")}
-
- ))}
+
+
+ >
+ )}
);
}
diff --git a/components/ExtensionWidgets.test.mjs b/components/ExtensionWidgets.test.mjs
new file mode 100644
index 000000000..00c520b2f
--- /dev/null
+++ b/components/ExtensionWidgets.test.mjs
@@ -0,0 +1,41 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import React from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import { createJiti } from "jiti";
+
+const jiti = createJiti(import.meta.url, {
+ jsx: { runtime: "automatic" },
+ tsconfigPaths: true,
+});
+const {
+ ExtensionWidgets,
+ MAX_EXTENSION_WIDGET_LINES,
+} = await jiti.import("./ExtensionWidgets.tsx");
+
+test("renders short extension widgets without a truncation marker", () => {
+ const html = renderToStaticMarkup(
+ React.createElement(ExtensionWidgets, {
+ widgets: [{ key: "short", lines: ["first", "second"] }],
+ }),
+ );
+
+ assert.match(html, /first\nsecond/);
+ assert.doesNotMatch(html, /widget truncated/);
+});
+
+test("matches the Pi TUI extension widget line limit", () => {
+ const lines = Array.from(
+ { length: MAX_EXTENSION_WIDGET_LINES + 2 },
+ (_, index) => `line-${index + 1}`,
+ );
+ const html = renderToStaticMarkup(
+ React.createElement(ExtensionWidgets, {
+ widgets: [{ key: "long", lines }],
+ }),
+ );
+
+ assert.match(html, new RegExp(`line-${MAX_EXTENSION_WIDGET_LINES}`));
+ assert.doesNotMatch(html, new RegExp(`line-${MAX_EXTENSION_WIDGET_LINES + 1}`));
+ assert.match(html, /\.\.\. \(widget truncated\)/);
+});
diff --git a/components/ExtensionWidgets.tsx b/components/ExtensionWidgets.tsx
new file mode 100644
index 000000000..1e66c5739
--- /dev/null
+++ b/components/ExtensionWidgets.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+export const MAX_EXTENSION_WIDGET_LINES = 10;
+
+function getDisplayLines(lines: string[]): string[] {
+ if (lines.length <= MAX_EXTENSION_WIDGET_LINES) return lines;
+ return [
+ ...lines.slice(0, MAX_EXTENSION_WIDGET_LINES),
+ "... (widget truncated)",
+ ];
+}
+
+export function ExtensionWidgets({ widgets }: { widgets: Array<{ key: string; lines: string[] }> }) {
+ if (widgets.length === 0) return null;
+ return (
+
+ {widgets.map((widget) => (
+
+
+ {widget.key}
+
+
+ {getDisplayLines(widget.lines).join("\n")}
+
+
+ ))}
+
+ );
+}