ReMe/reme_studio/app/store.ts
jinliyl ef3f99f019
refactor(packaging): reorganize published packages (#495)
* refactor(packaging): reorganize published packages

* fix(packaging): install AgentScope extra in wheel smoke

* docs: align package guides and documentation site

* ci(workflow): add core dependency verification step in Python package build

- Add a workflow step to verify released core dependencies by installing the wheel with core extras
- Assert the presence of the static index.html file to ensure proper package contents
- Create and use a temporary virtual environment for isolation during verification
- Keep existing artifacts upload step intact and conditional on inputs.upload_artifacts flag

* fix(ci): update package installation dependencies in Windows workflow

- Change pip install from editable reme_studio and core to only dev and as extras
- Remove installation of reme_studio and core to streamline dependency setup
- Ensure Windows CI uses the correct extras for testing environment

* fix(tests): add missing commas in toml file reads in package version tests

- Added trailing commas in the tomllib.loads calls for auto-fin and daily_paper configs
- Ensured consistent syntax to prevent potential tuple misinterpretation
- Improved readability and correctness of the test setup code

* fix(packaging): protect qwenpaw releases and test Studio health
2026-08-27 14:02:09 +08:00

257 lines
7.8 KiB
TypeScript

"use client";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import {
applyStreamChunk,
finishChatMessage,
toggleChatBlock,
} from "./chat-stream";
import type {
ChatMessage,
MemoryGraphRoot,
StreamChunk,
WorkspaceTab,
} from "./types";
import {
prepareWorkspaceSnapshot,
type PersistedWorkspaceState,
} from "./workspace-persistence";
import { markMarkdownContentSaved } from "./markdown-save";
import { unsavedTabsClosedBy } from "./tab-close";
interface WorkspaceState {
tabs: WorkspaceTab[];
activeTabId?: string;
openMarkdown: (path: string) => string;
openAgent: () => string;
openGraph: (root: MemoryGraphRoot) => string;
closeTab: (id: string, discardUnsaved?: boolean) => void;
closeOtherTabs: (id: string, discardUnsaved?: boolean) => void;
setActiveTab: (id: string) => void;
hydrateMarkdown: (id: string, content: string, mtime?: string) => void;
failMarkdown: (id: string, error: string) => void;
updateMarkdown: (id: string, content: string) => void;
markSaved: (id: string, content: string, mtime?: string) => void;
addChatTurn: (
tabId: string,
user: ChatMessage,
assistant: ChatMessage,
) => void;
applyChatChunk: (
tabId: string,
messageId: string,
chunk: StreamChunk,
) => void;
toggleChatBlock: (
tabId: string,
messageId: string,
blockId: string,
expanded: boolean,
) => void;
finishChat: (tabId: string, messageId: string, error?: string) => void;
}
const fileTitle = (path: string) => path.split("/").pop() || path;
// Preserve the original key so existing tabs, chats, and unsaved drafts survive the Studio rename.
const WORKSPACE_STORAGE_KEY = "reme-workspace";
export const useWorkspaceStore = create<WorkspaceState>()(
persist(
(set, get) => ({
tabs: [],
activeTabId: undefined,
openMarkdown: (path) => {
const existing = get().tabs.find(
(tab) => tab.type === "markdown" && tab.path === path,
);
if (existing) {
set({ activeTabId: existing.id });
return existing.id;
}
const id = `file:${path}`;
set((state) => ({
tabs: [
...state.tabs,
{
id,
type: "markdown",
title: fileTitle(path),
path,
content: "",
savedContent: "",
loading: true,
},
],
activeTabId: id,
}));
return id;
},
openAgent: () => {
const id = `agent:${crypto.randomUUID()}`;
set((state) => ({
tabs: [...state.tabs, { id, type: "agent", title: "", messages: [] }],
activeTabId: id,
}));
return id;
},
openGraph: (root) => {
const id = `graph:${root}`;
const existing = get().tabs.find(
(tab) => tab.id === id && tab.type === "graph",
);
if (existing) {
set({ activeTabId: id });
return id;
}
set((state) => ({
tabs: [
...state.tabs,
{ id, type: "graph", title: `${root} graph`, root },
],
activeTabId: id,
}));
return id;
},
closeTab: (id, discardUnsaved = false) =>
set((state) => {
if (
!discardUnsaved &&
unsavedTabsClosedBy(state.tabs, id, false).length
)
return state;
const index = state.tabs.findIndex((tab) => tab.id === id);
const tabs = state.tabs.filter((tab) => tab.id !== id);
const activeTabId =
state.activeTabId === id
? tabs[Math.max(0, index - 1)]?.id
: state.activeTabId;
return { tabs, activeTabId };
}),
closeOtherTabs: (id, discardUnsaved = false) =>
set((state) => {
if (
!discardUnsaved &&
unsavedTabsClosedBy(state.tabs, id, true).length
)
return state;
const tab = state.tabs.find((item) => item.id === id);
return tab ? { tabs: [tab], activeTabId: id } : state;
}),
setActiveTab: (activeTabId) => set({ activeTabId }),
hydrateMarkdown: (id, content, mtime) =>
set((state) => ({
tabs: state.tabs.map((tab) =>
tab.id === id && tab.type === "markdown"
? {
...tab,
content,
savedContent: content,
mtime,
loading: false,
error: undefined,
}
: tab,
),
})),
failMarkdown: (id, error) =>
set((state) => ({
tabs: state.tabs.map((tab) =>
tab.id === id && tab.type === "markdown"
? { ...tab, loading: false, error }
: tab,
),
})),
updateMarkdown: (id, content) =>
set((state) => ({
tabs: state.tabs.map((tab) =>
tab.id === id && tab.type === "markdown"
? { ...tab, content }
: tab,
),
})),
markSaved: (id, content, mtime) =>
set((state) => ({
tabs: state.tabs.map((tab) =>
tab.id === id && tab.type === "markdown"
? markMarkdownContentSaved(tab, content, mtime)
: tab,
),
})),
addChatTurn: (tabId, user, assistant) =>
set((state) => ({
tabs: state.tabs.map((tab) =>
tab.id === tabId && tab.type === "agent"
? {
...tab,
title: tab.messages.length
? tab.title
: user.content.slice(0, 18),
messages: [...tab.messages, user, assistant],
streaming: true,
}
: tab,
),
})),
applyChatChunk: (tabId, messageId, chunk) =>
set((state) => ({
tabs: state.tabs.map((tab) => {
if (tab.id !== tabId || tab.type !== "agent") return tab;
const messages = tab.messages.map((message) =>
message.id === messageId
? applyStreamChunk(message, chunk)
: message,
);
return {
...tab,
sessionId: chunk.session_id || tab.sessionId,
messages,
};
}),
})),
toggleChatBlock: (tabId, messageId, blockId, expanded) =>
set((state) => ({
tabs: state.tabs.map((tab) =>
tab.id === tabId && tab.type === "agent"
? {
...tab,
messages: tab.messages.map((message) =>
message.id === messageId
? toggleChatBlock(message, blockId, expanded)
: message,
),
}
: tab,
),
})),
finishChat: (tabId, messageId, error) =>
set((state) => ({
tabs: state.tabs.map((tab) =>
tab.id === tabId && tab.type === "agent"
? {
...tab,
streaming: false,
messages: tab.messages.map((message) =>
message.id === messageId
? finishChatMessage(message, error)
: message,
),
}
: tab,
),
})),
}),
{
name: WORKSPACE_STORAGE_KEY,
version: 1,
storage: createJSONStorage(() => localStorage),
skipHydration: true,
partialize: (state): PersistedWorkspaceState =>
prepareWorkspaceSnapshot(state.tabs, state.activeTabId),
merge: (persisted, current) => ({
...current,
...(persisted as PersistedWorkspaceState),
}),
},
),
);