feat(FN-3187): enrich session dropdown labels and add collapsible tool deta
This merge lands v0.14.2, a release focused on plugin system improvements and UI refinements. The plugin SDK gains a new `plugin-loader` and `plugin-runner` module with cached contribution accessors, plus an event bridge overhaul in the Claude CLI integration. On the dashboard side, the agent log vi Fusion-Task-Id: FN-3187
This commit is contained in:
@@ -1291,13 +1291,30 @@ export function QuickChatFAB({
|
|||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||||
}, [messages, streamingText, streamingThinking, isStreaming, isOpen]);
|
}, [messages, streamingText, streamingThinking, isStreaming, isOpen]);
|
||||||
|
|
||||||
const sessionOptions = useMemo(
|
const sessionOptions = useMemo(() => {
|
||||||
() => sessions.map((session, index) => ({
|
const agentNameById = new Map(agents.map((agent) => [agent.id, agent.name?.trim() || agent.id]));
|
||||||
id: session.id,
|
const modelNameByKey = new Map(
|
||||||
label: session.title?.trim() || `Session ${index + 1}`,
|
models.map((model) => [`${model.provider}/${model.id}`, model.name?.trim() || ""]),
|
||||||
})),
|
);
|
||||||
[sessions],
|
|
||||||
);
|
return sessions.map((session, index) => {
|
||||||
|
const baseLabel = session.title?.trim() || `Session ${index + 1}`;
|
||||||
|
|
||||||
|
let descriptor: string | null = null;
|
||||||
|
if (session.agentId && session.agentId !== FN_AGENT_ID) {
|
||||||
|
descriptor = agentNameById.get(session.agentId) || session.agentId;
|
||||||
|
} else if (session.modelProvider && session.modelId) {
|
||||||
|
const modelKey = `${session.modelProvider}/${session.modelId}`;
|
||||||
|
const modelName = modelNameByKey.get(modelKey);
|
||||||
|
descriptor = modelName ? `${modelName} [${modelKey}]` : modelKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: session.id,
|
||||||
|
label: descriptor ? `${baseLabel} — ${descriptor}` : baseLabel,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [agents, models, sessions]);
|
||||||
|
|
||||||
const inputPlaceholder = useMemo(() => {
|
const inputPlaceholder = useMemo(() => {
|
||||||
if (chatMode === "agent") {
|
if (chatMode === "agent") {
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ describe("QuickChatFAB session-first UX", () => {
|
|||||||
|
|
||||||
expect(await screen.findByTestId("quick-chat-session-dropdown")).toBeInTheDocument();
|
expect(await screen.findByTestId("quick-chat-session-dropdown")).toBeInTheDocument();
|
||||||
expect(screen.queryByTestId("quick-chat-mode-toggle")).toBeNull();
|
expect(screen.queryByTestId("quick-chat-mode-toggle")).toBeNull();
|
||||||
expect(screen.getByRole("option", { name: "Model thread" })).toBeInTheDocument();
|
expect(screen.getByRole("option", { name: "Model thread — GPT-4o [openai/gpt-4o]" })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("option", { name: "Session 2" })).toBeInTheDocument();
|
expect(screen.getByRole("option", { name: "Session 2 — Agent One" })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens inline chooser from new button defaulting to model", async () => {
|
it("opens inline chooser from new button defaulting to model", async () => {
|
||||||
@@ -142,6 +142,38 @@ describe("QuickChatFAB session-first UX", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows distinguishable labels for sessions from multiple models", async () => {
|
||||||
|
mockFetchChatSessions.mockResolvedValueOnce({
|
||||||
|
sessions: [
|
||||||
|
{ ...modelSession, id: "session-openai", title: null },
|
||||||
|
{ ...modelSession, id: "session-anthropic", modelProvider: "anthropic", modelId: "claude-3-7-sonnet", title: null },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
mockFetchModels.mockResolvedValueOnce({
|
||||||
|
models: [
|
||||||
|
{ provider: "openai", id: "gpt-4o", name: "GPT-4o", reasoning: true, contextWindow: 128000 },
|
||||||
|
{ provider: "anthropic", id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet", reasoning: true, contextWindow: 200000 },
|
||||||
|
],
|
||||||
|
favoriteProviders: [],
|
||||||
|
favoriteModels: [],
|
||||||
|
defaultProvider: "openai",
|
||||||
|
defaultModelId: "gpt-4o",
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
|
|
||||||
|
expect(await screen.findByRole("option", { name: "Session 1 — GPT-4o [openai/gpt-4o]" })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("option", { name: "Session 2 — Claude 3.7 Sonnet [anthropic/claude-3-7-sonnet]" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes both title and model descriptor in session label", async () => {
|
||||||
|
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
|
|
||||||
|
expect(await screen.findByRole("option", { name: "Model thread — GPT-4o [openai/gpt-4o]" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("intercepts exact /clear and starts a fresh session for the active target", async () => {
|
it("intercepts exact /clear and starts a fresh session for the active target", async () => {
|
||||||
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||||
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
@@ -185,7 +217,7 @@ describe("QuickChatFAB session-first UX", () => {
|
|||||||
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
|
|
||||||
const select = await screen.findByTestId("quick-chat-session-dropdown");
|
const select = await screen.findByTestId("quick-chat-session-dropdown");
|
||||||
await screen.findByRole("option", { name: "Session 2" });
|
await screen.findByRole("option", { name: "Session 2 — Agent One" });
|
||||||
fireEvent.change(select, { target: { value: "session-agent" } });
|
fireEvent.change(select, { target: { value: "session-agent" } });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user