feat(project): per-project new-chat defaults (agent, workspace, approval, thinking, model) (#191)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-08-04 22:52:34 +08:00
committed by GitHub
parent 7cc5c30e71
commit 89435bf62a
27 changed files with 1980 additions and 638 deletions
+38
View File
@@ -11,6 +11,7 @@
import { describe, expect, it } from "vitest";
import {
clearDraft,
clearDraftModelRef,
draftKey,
loadDraft,
parseDraft,
@@ -183,6 +184,43 @@ describe("load / save / clear (key isolation, errors silenced)", () => {
expect(s.map.size).toBe(0);
});
it("clearDraftModelRef drops only the model pin, keeping everything else (default-model change follow-through)", () => {
// Shared by the models page and the project-settings default-model control: after the
// Project default changes, the draft must follow it instead of pinning the old pick.
const s = memStorage();
saveDraft(
draftKey("user-a1", "project-a"),
{
text: "keep me",
agentId: "default_agent",
modelRef: { provider: "deepseek", modelId: "deepseek-v4-pro" },
skills: ["agent-creation"],
},
s,
);
clearDraftModelRef("user-a1", "project-a", s);
expect(loadDraft(draftKey("user-a1", "project-a"), s)).toEqual({
text: "keep me",
agentId: "default_agent",
skills: ["agent-creation"],
});
// No cached pick (or no draft at all): a no-op, never an errant write.
clearDraftModelRef("user-a1", "project-a", s);
clearDraftModelRef("user-b2", "project-a", s);
expect(s.map.has(draftKey("user-b2", "project-a"))).toBe(false);
// Scoped by user × Project: another user's pin survives.
saveDraft(
draftKey("user-b2", "project-a"),
{ modelRef: { provider: "openai", modelId: "gpt-5" } },
s,
);
clearDraftModelRef("user-a1", "project-a", s);
expect(loadDraft(draftKey("user-b2", "project-a"), s).modelRef).toEqual({
provider: "openai",
modelId: "gpt-5",
});
});
it("storage throwing (quota/private mode): save does not throw, load yields an empty draft", () => {
const broken: DraftStorage = {
getItem: () => {