feat(web,core,server,docs,skills): avatar update tooltip, DeepSeek-first model catalog, temporary-workspace wording (#224)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* model-group-expansion.ts unit tests: the models page's default-collapsed vendor groups.
|
||||
* Only DeepSeek (the catalog's first provider) is expanded on first paint; the state is a
|
||||
* set of EXPANDED ids so groups arriving late (user-defined groups load with the rows)
|
||||
* default to collapsed without being known up front. While a search query is active every
|
||||
* rendered group is force-opened — groupModelRows only returns match-holding groups when
|
||||
* searching, and a match hidden inside a collapsed group would look like a missing result —
|
||||
* without touching the stored set.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MODEL_PROVIDERS } from "@prismshadow/penguin-core/model-catalog";
|
||||
import {
|
||||
defaultExpandedProviders,
|
||||
isGroupExpanded,
|
||||
toggleExpandedProvider,
|
||||
} from "../src/features/models/model-group-expansion";
|
||||
import { groupModelRows } from "../src/features/models/model-grouping";
|
||||
import type { ModelRowLike } from "../src/features/models/model-grouping";
|
||||
|
||||
describe("defaultExpandedProviders", () => {
|
||||
it("contains exactly deepseek, the first provider of the catalog", () => {
|
||||
expect([...defaultExpandedProviders()]).toEqual(["deepseek"]);
|
||||
// The default leans on the catalog ordering promise (DeepSeek first): pin it here so a
|
||||
// reordering shows up as this failure instead of a silently odd default.
|
||||
expect(MODEL_PROVIDERS[0]?.id).toBe("deepseek");
|
||||
});
|
||||
|
||||
it("returns a fresh set per call (React state must not share one mutable instance)", () => {
|
||||
const a = defaultExpandedProviders();
|
||||
const b = defaultExpandedProviders();
|
||||
expect(a).not.toBe(b);
|
||||
a.add("openai");
|
||||
expect(b.has("openai")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isGroupExpanded", () => {
|
||||
const expanded = defaultExpandedProviders();
|
||||
|
||||
it("without a search query, only members of the expanded set are open", () => {
|
||||
expect(isGroupExpanded(expanded, "deepseek", false)).toBe(true);
|
||||
expect(isGroupExpanded(expanded, "anthropic", false)).toBe(false);
|
||||
expect(isGroupExpanded(expanded, "custom", false)).toBe(false);
|
||||
// User-defined groups are decided by the same membership rule — collapsed until toggled.
|
||||
expect(isGroupExpanded(expanded, "my-proxy", false)).toBe(false);
|
||||
});
|
||||
|
||||
it("while searching, every group is open regardless of the stored set", () => {
|
||||
expect(isGroupExpanded(expanded, "anthropic", true)).toBe(true);
|
||||
expect(isGroupExpanded(new Set(), "deepseek", true)).toBe(true);
|
||||
});
|
||||
|
||||
it("force-open covers each group a search actually renders (the hidden-results regression)", () => {
|
||||
const rows: ModelRowLike[] = [
|
||||
{ provider: "deepseek", modelId: "deepseek-chat" },
|
||||
{ provider: "anthropic", modelId: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" },
|
||||
{ provider: "moonshot", modelId: "kimi-k2.6", displayName: "Kimi K2.6" },
|
||||
{ provider: "my-proxy", modelId: "kimi-mirror" },
|
||||
];
|
||||
// "kimi" matches inside groups that default to collapsed (moonshot + a user-defined one):
|
||||
// with the query active each rendered group must derive as open.
|
||||
const groups = groupModelRows(rows, "kimi");
|
||||
expect(groups.length).toBeGreaterThan(0);
|
||||
for (const g of groups) {
|
||||
expect(isGroupExpanded(defaultExpandedProviders(), g.provider.id, true)).toBe(true);
|
||||
}
|
||||
// Clearing the query falls back to the stored set: the same groups collapse again.
|
||||
for (const g of groups) {
|
||||
expect(isGroupExpanded(defaultExpandedProviders(), g.provider.id, false)).toBe(
|
||||
g.provider.id === "deepseek",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggleExpandedProvider", () => {
|
||||
it("adds an absent id and removes a present one, without mutating the input", () => {
|
||||
const initial = defaultExpandedProviders();
|
||||
const withAnthropic = toggleExpandedProvider(initial, "anthropic");
|
||||
expect(withAnthropic.has("anthropic")).toBe(true);
|
||||
expect(withAnthropic.has("deepseek")).toBe(true);
|
||||
expect(initial.has("anthropic")).toBe(false); // input untouched (React state discipline)
|
||||
|
||||
const withoutDeepseek = toggleExpandedProvider(withAnthropic, "deepseek");
|
||||
expect(withoutDeepseek.has("deepseek")).toBe(false);
|
||||
expect(withoutDeepseek.has("anthropic")).toBe(true);
|
||||
expect(withAnthropic.has("deepseek")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
* Workspace grouping for the chat sidebar (pure logic):
|
||||
* - named Workspaces group by exact path, labeled by basename (full path kept for
|
||||
* tooltips), newest group first;
|
||||
* - auto temp Workspaces (`<agentDir>/workspaces/tmp-<8hex>`, the shape produced by
|
||||
* - temporary Workspaces (`<agentDir>/workspaces/tmp-<8hex>`, the shape produced by
|
||||
* core's createTempWorkspace) are all merged into ONE trailing temp group — an
|
||||
* empty path (defensive; the server always backfills the resolved dir) counts too;
|
||||
* - sessions inside every group are re-sorted newest first: the flat store list
|
||||
@@ -58,7 +58,7 @@ function session(
|
||||
const TEMP_A = "/data/proj/agents/default_agent/workspaces/tmp-1a2b3c4d";
|
||||
const TEMP_B = "/data/proj/agents/agent_helper/workspaces/tmp-00ff00aa";
|
||||
|
||||
describe("isTempWorkspace (auto temp directory pattern from core's createTempWorkspace)", () => {
|
||||
describe("isTempWorkspace (temporary-workspace pattern from core's createTempWorkspace)", () => {
|
||||
it("matches <...>/workspaces/tmp-<8hex> with either path separator, and the empty path", () => {
|
||||
expect(isTempWorkspace(TEMP_A)).toBe(true);
|
||||
expect(isTempWorkspace("C:\\pg\\data\\proj\\agents\\a\\workspaces\\tmp-00ff00aa")).toBe(true);
|
||||
@@ -74,7 +74,7 @@ describe("isTempWorkspace (auto temp directory pattern from core's createTempWor
|
||||
expect(isTempWorkspace("/x/workspaces/tmp-XYZWQPRS")).toBe(false);
|
||||
expect(isTempWorkspace("/x/workspaces/tmp-1a2b3c4")).toBe(false);
|
||||
expect(isTempWorkspace("/x/workspaces/tmp-1a2b3c4d5")).toBe(false);
|
||||
// a subdirectory below a temp Workspace is not itself the temp Workspace
|
||||
// a subdirectory below a temporary workspace is not itself the temporary workspace
|
||||
expect(isTempWorkspace(`${TEMP_A}/nested`)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -115,7 +115,7 @@ describe("groupSessionsByWorkspace", () => {
|
||||
expect(groups[0]!.sessions.map((s) => s.sessionId)).toEqual([a2.sessionId, a1.sessionId]);
|
||||
});
|
||||
|
||||
it("merges every temp Workspace into one trailing group, after named groups sorted by newest session", () => {
|
||||
it("merges every temporary Workspace into one trailing group, after named groups sorted by newest session", () => {
|
||||
const oldAlpha = session("/srv/alpha", "2026-07-01T10:00:00.000Z");
|
||||
const newAlpha = session("/srv/alpha", "2026-07-06T10:00:00.000Z");
|
||||
const beta = session("/srv/beta", "2026-07-05T10:00:00.000Z");
|
||||
@@ -276,7 +276,7 @@ describe("aggregateWorkspaceCounts (per-group exact server share)", () => {
|
||||
expect(aggregateWorkspaceCounts(new Map()).size).toBe(0);
|
||||
});
|
||||
|
||||
it("folds every auto-temp path into the merged temp group, deduplicating agents", () => {
|
||||
it("folds every temporary-workspace path into the merged temp group, deduplicating agents", () => {
|
||||
const byAgent = new Map([
|
||||
[
|
||||
"agent_a",
|
||||
|
||||
Reference in New Issue
Block a user