mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 16:19:05 +08:00
Port of OpenHands/OpenHands#14284. The LLM model badge in the conversation header was constrained to max-w-[150px] with an inner `truncate`, which cut off long model identifiers such as `litellm_proxy/claude-sonnet-4-5-20250929` to `litellm_proxy/cl…`. Drop the width cap and inner truncate, and apply `whitespace-nowrap` to the outer span so the full name renders inline. Also adds scripts/record-demo.mjs - a small playwright recorder used to capture the verification GIF under .pr/issue-135/ - and updates the existing test to assert the un-truncated structure. Closes #135. Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
committed by
GitHub
co-authored by
openhands
parent
0cd6ed390e
commit
84ed4d1217
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 677 KiB |
@@ -0,0 +1,26 @@
|
||||
# Issue #135 — Show full model name in conversation header
|
||||
|
||||
Port of [OpenHands/OpenHands#14284](https://github.com/OpenHands/OpenHands/pull/14284).
|
||||
|
||||
## Recording setup
|
||||
|
||||
* Branch: `port/full-model-name-header`
|
||||
* Backend: `npm run dev` (spawns isolated `openhands-agent-server@main` on `:18000`)
|
||||
* Frontend: `localhost:12000` (exposed at `https://work-1-yrsrggnfhzzshrxo.prod-runtime.all-hands.dev/` for the recording)
|
||||
* LLM configured via `PATCH /api/settings` with `agent_settings_diff.llm.model = litellm_proxy/claude-sonnet-4-5-20250929` and the live `LLM_API_KEY`.
|
||||
|
||||
The recordings were produced by `node scripts/record-demo.mjs full-model-name <out>.webm`, then converted to GIF with the ffmpeg snippet documented in that script.
|
||||
|
||||
## Visual diff
|
||||
|
||||
`comparison.png` stacks the same conversation header band before and after the fix:
|
||||
|
||||
* **Before** (`max-w-[150px] overflow-hidden` + inner `truncate`): `litellm_proxy/claude…` cuts off after the first 150 px.
|
||||
* **After** (`whitespace-nowrap`, no inner truncate): the full `litellm_proxy/claude-sonnet-4-5-20250929` renders inline.
|
||||
|
||||
`before.png` / `after.png` are full 1280×720 captures of the conversation page in each state. `demo.gif` is the recorded "after" flow (home → New Conversation → header rendered with the un-truncated badge).
|
||||
|
||||
## Tests
|
||||
|
||||
* `npm test -- conversation-name` → 25/25 passing (includes the new assertions that the outer span has `whitespace-nowrap` and no `max-w-[150px]` / `overflow-hidden`, and the inner span has no `truncate`).
|
||||
* `npm run typecheck` → clean.
|
||||
@@ -312,10 +312,14 @@ describe("ConversationName", () => {
|
||||
expect(model).toHaveAttribute("title", "openai/gpt-4o");
|
||||
expect(model.querySelector("svg")).toBeInTheDocument();
|
||||
|
||||
// Verify truncation structure: text is wrapped in a span with truncate class
|
||||
const textSpan = model.querySelector("span.truncate");
|
||||
// Verify the model name is rendered in full (no truncation)
|
||||
const textSpan = model.querySelector("span");
|
||||
expect(textSpan).toBeInTheDocument();
|
||||
expect(textSpan).toHaveTextContent("openai/gpt-4o");
|
||||
expect(textSpan).not.toHaveClass("truncate");
|
||||
expect(model).not.toHaveClass("max-w-[150px]");
|
||||
expect(model).not.toHaveClass("overflow-hidden");
|
||||
expect(model).toHaveClass("whitespace-nowrap");
|
||||
});
|
||||
|
||||
it("should not render the llm model when not available", () => {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Record a short demo of agent-canvas for PR GIFs.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/record-demo.mjs <flow> <output-webm>
|
||||
*
|
||||
* Convert the resulting webm to GIF with:
|
||||
* ffmpeg -i <output>.webm \
|
||||
* -vf "fps=10,scale=960:-1:flags=lanczos,split[s0][s1];\
|
||||
* [s0]palettegen=max_colors=128[p];\
|
||||
* [s1][p]paletteuse=dither=bayer:bayer_scale=4" \
|
||||
* -loop 0 <output>.gif
|
||||
*
|
||||
* `npm run dev` from this repo must already be running. By default the
|
||||
* recorder hits `http://localhost:12000`; override with `DEMO_BASE_URL` when
|
||||
* the frontend is exposed via a remote host (e.g. an All Hands work URL),
|
||||
* otherwise the browser-side calls to the agent-server will fail with CORS.
|
||||
*
|
||||
* Flows:
|
||||
* - full-model-name : open home → click "New Conversation" → wait for the
|
||||
* conversation header → hover the un-truncated model
|
||||
* badge so the tooltip is visible at the end of the
|
||||
* recording.
|
||||
*/
|
||||
import { chromium } from "playwright";
|
||||
import { mkdirSync, renameSync, readdirSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
const FLOW = process.argv[2] ?? "full-model-name";
|
||||
const OUTPUT = process.argv[3]
|
||||
? path.resolve(process.argv[3])
|
||||
: path.resolve(`.pr/${FLOW}.webm`);
|
||||
const BASE_URL = process.env.DEMO_BASE_URL ?? "http://localhost:12000";
|
||||
const VIEWPORT = { width: 1280, height: 720 };
|
||||
|
||||
const outDir = path.dirname(OUTPUT);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const tmpDir = path.join(outDir, ".tmp-record");
|
||||
mkdirSync(tmpDir, { recursive: true });
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: VIEWPORT,
|
||||
recordVideo: { dir: tmpDir, size: VIEWPORT },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") {
|
||||
console.error(`[browser console error] ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
page.on("pageerror", (error) => {
|
||||
console.error(`[browser page error] ${error.message}`);
|
||||
});
|
||||
|
||||
async function flowFullModelName() {
|
||||
await page.goto(`${BASE_URL}/`, { waitUntil: "domcontentloaded" });
|
||||
// The "Account settings" popover (rendered when the user menu is open in
|
||||
// the host shell) can overlay the home content during initial load. Press
|
||||
// Escape so the home content is interactive, then dismiss any tooltip.
|
||||
await page.waitForTimeout(1_500);
|
||||
await page.keyboard.press("Escape");
|
||||
await page.mouse.click(640, 360);
|
||||
await page.waitForTimeout(500);
|
||||
// Wait for the "Start from Scratch" card.
|
||||
await page
|
||||
.getByRole("button", { name: /New Conversation/i })
|
||||
.first()
|
||||
.waitFor({ state: "visible", timeout: 60_000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page
|
||||
.getByRole("button", { name: /New Conversation/i })
|
||||
.first()
|
||||
.click();
|
||||
// Wait for navigation to /conversations/<id>.
|
||||
await page.waitForURL(/\/conversations\//, { timeout: 30_000 });
|
||||
// Wait for the model name badge to appear in the header.
|
||||
await page
|
||||
.getByTestId("conversation-name-llm-model")
|
||||
.waitFor({ state: "visible", timeout: 60_000 });
|
||||
// Hold so the GIF lingers on the un-truncated model name.
|
||||
await page.waitForTimeout(2_500);
|
||||
// Hover the model badge to make the tooltip appear.
|
||||
await page.getByTestId("conversation-name-llm-model").hover();
|
||||
await page.waitForTimeout(2_500);
|
||||
}
|
||||
|
||||
const flows = {
|
||||
"full-model-name": flowFullModelName,
|
||||
};
|
||||
|
||||
if (!flows[FLOW]) {
|
||||
throw new Error(`Unknown flow: ${FLOW}`);
|
||||
}
|
||||
|
||||
await flows[FLOW]();
|
||||
|
||||
await context.close();
|
||||
await browser.close();
|
||||
|
||||
// Move the generated webm into the requested output path.
|
||||
const generated = readdirSync(tmpDir)
|
||||
.filter((name) => name.endsWith(".webm"))
|
||||
.map((name) => path.join(tmpDir, name))
|
||||
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0];
|
||||
|
||||
if (!generated) {
|
||||
throw new Error("Playwright did not produce a video file");
|
||||
}
|
||||
|
||||
renameSync(generated, OUTPUT);
|
||||
console.log(`Saved ${OUTPUT}`);
|
||||
@@ -158,12 +158,12 @@ export function ConversationName() {
|
||||
|
||||
{titleMode !== "edit" && conversation.llm_model && (
|
||||
<span
|
||||
className="text-xs text-[#A3A3A3] max-w-[150px] flex items-center gap-1 overflow-hidden"
|
||||
className="text-xs text-[#A3A3A3] flex items-center gap-1 whitespace-nowrap"
|
||||
title={conversation.llm_model}
|
||||
data-testid="conversation-name-llm-model"
|
||||
>
|
||||
<CircuitIcon width={12} height={12} className="shrink-0" />
|
||||
<span className="truncate">{conversation.llm_model}</span>
|
||||
<span>{conversation.llm_model}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user