feat: update Makelore modules and conversations
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
export interface ChatViewPreferences {
|
||||
showThinking: boolean;
|
||||
showTimestamps: boolean;
|
||||
/** Include reasoning, tool summaries, and execution narration in exports. */
|
||||
includeExecution: boolean;
|
||||
}
|
||||
|
||||
type ChatViewPreferencePatch = Partial<ChatViewPreferences>;
|
||||
@@ -10,6 +12,7 @@ export const CHAT_VIEW_PREFERENCES_STORAGE_KEY = 'makelore.chat-view-preferences
|
||||
export const DEFAULT_CHAT_VIEW_PREFERENCES: ChatViewPreferences = {
|
||||
showThinking: true,
|
||||
showTimestamps: true,
|
||||
includeExecution: false,
|
||||
};
|
||||
|
||||
function browserStorage(): Storage | undefined {
|
||||
@@ -38,6 +41,9 @@ function readStoredPreferences(storage: Storage | undefined): StoredChatViewPref
|
||||
...(typeof record.showTimestamps === 'boolean'
|
||||
? { showTimestamps: record.showTimestamps }
|
||||
: {}),
|
||||
...(typeof record.includeExecution === 'boolean'
|
||||
? { includeExecution: record.includeExecution }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
@@ -59,6 +65,9 @@ export function loadChatViewPreferences(
|
||||
showTimestamps: typeof candidate?.showTimestamps === 'boolean'
|
||||
? candidate.showTimestamps
|
||||
: DEFAULT_CHAT_VIEW_PREFERENCES.showTimestamps,
|
||||
includeExecution: typeof candidate?.includeExecution === 'boolean'
|
||||
? candidate.includeExecution
|
||||
: DEFAULT_CHAT_VIEW_PREFERENCES.includeExecution,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,6 +84,9 @@ export function saveChatViewPreferences(
|
||||
showTimestamps: typeof patch.showTimestamps === 'boolean'
|
||||
? patch.showTimestamps
|
||||
: current.showTimestamps,
|
||||
includeExecution: typeof patch.includeExecution === 'boolean'
|
||||
? patch.includeExecution
|
||||
: current.includeExecution,
|
||||
};
|
||||
if (!storage) return next;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,14 @@
|
||||
import { create } from 'zustand';
|
||||
import { createJSONStorage, persist, type StateStorage } from 'zustand/middleware';
|
||||
import type { BuiltInRoleSubagentId } from '@/lib/role-subagents';
|
||||
import { toUserSyncPartner, type UserSyncPartner } from '../../shared/user-sync';
|
||||
|
||||
export const partnerAvatarToneOptions = [
|
||||
'bg-[#b47745]',
|
||||
'bg-[#FDE8E1]',
|
||||
'bg-[#F26A3D]',
|
||||
'bg-[#DCE6EF]',
|
||||
'bg-[#EEF3F7]',
|
||||
'bg-[#b8a7ff]',
|
||||
'bg-accent-soft',
|
||||
'bg-brand-soft',
|
||||
'bg-accent',
|
||||
'bg-surface-tertiary',
|
||||
'bg-surface-subtle',
|
||||
'bg-brand/20',
|
||||
] as const;
|
||||
|
||||
export type PartnerProfile = {
|
||||
@@ -27,7 +26,9 @@ export type PartnerProfileInput = {
|
||||
avatarDataUrl?: string;
|
||||
};
|
||||
|
||||
export type PartnerTemplateRoleId = BuiltInRoleSubagentId | 'custom';
|
||||
// Kept as a serialized compatibility field for synced partner records. New
|
||||
// partners are always manually configured and use the single custom value.
|
||||
export type PartnerTemplateRoleId = 'custom';
|
||||
|
||||
export type CreatedPartnerSubagent = {
|
||||
id: string;
|
||||
@@ -52,7 +53,7 @@ export type ProjectSubagentRuntimeConfig = {
|
||||
type PartnerProfilesState = {
|
||||
subagents: CreatedPartnerSubagent[];
|
||||
projectRuntimeConfigsByProjectId: Record<string, Record<string, ProjectSubagentRuntimeConfig>>;
|
||||
profilesById: Partial<Record<BuiltInRoleSubagentId, PartnerProfile>>;
|
||||
profilesById: Record<string, PartnerProfile>;
|
||||
addSubagent: (subagent: CreatedPartnerSubagent) => void;
|
||||
updatePartnerProfile: (agentId: string, profile: PartnerProfileInput) => void;
|
||||
updateSubagentRuntimeConfig: (agentId: string, config: Pick<CreatedPartnerSubagent, 'skillIds' | 'filePath' | 'updatedAt'>) => void;
|
||||
@@ -188,12 +189,11 @@ export const usePartnerProfilesStore = create<PartnerProfilesState>()(
|
||||
}
|
||||
|
||||
const nextProfiles = { ...state.profilesById };
|
||||
const legacyAgentId = agentId as BuiltInRoleSubagentId;
|
||||
if (!hasProfileData(cleaned)) {
|
||||
delete nextProfiles[legacyAgentId];
|
||||
delete nextProfiles[agentId];
|
||||
return { profilesById: nextProfiles };
|
||||
}
|
||||
nextProfiles[legacyAgentId] = { ...cleaned, updatedAt: now };
|
||||
nextProfiles[agentId] = { ...cleaned, updatedAt: now };
|
||||
return { profilesById: nextProfiles };
|
||||
});
|
||||
if (updatedSubagent) {
|
||||
@@ -256,7 +256,7 @@ export const usePartnerProfilesStore = create<PartnerProfilesState>()(
|
||||
};
|
||||
}
|
||||
const nextProfiles = { ...state.profilesById };
|
||||
delete nextProfiles[agentId as BuiltInRoleSubagentId];
|
||||
delete nextProfiles[agentId];
|
||||
return { profilesById: nextProfiles };
|
||||
});
|
||||
if (updatedSubagent) {
|
||||
@@ -269,7 +269,7 @@ export const usePartnerProfilesStore = create<PartnerProfilesState>()(
|
||||
subagents: partners.map((partner) => ({
|
||||
id: partner.id,
|
||||
displayName: partner.displayName,
|
||||
templateRoleId: partner.templateRoleId as PartnerTemplateRoleId,
|
||||
templateRoleId: 'custom',
|
||||
description: partner.description,
|
||||
avatarTone: partner.avatarTone,
|
||||
avatarDataUrl: partner.avatarDataUrl,
|
||||
|
||||
117
src/stores/project-conversations.ts
Normal file
117
src/stores/project-conversations.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { create } from 'zustand';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import type {
|
||||
ProjectConversationState,
|
||||
ProjectSessionMetadata,
|
||||
} from '../../shared/project-conversations';
|
||||
|
||||
type ProjectConversationResponse = {
|
||||
state?: ProjectConversationState;
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type ConversationAction = 'link' | 'archive' | 'restore' | 'delete' | 'read' | 'increment-unread' | 'complete';
|
||||
|
||||
type ProjectConversationStore = {
|
||||
statesByProjectId: Record<string, ProjectConversationState>;
|
||||
loadingProjectId: string | null;
|
||||
errorsByProjectId: Record<string, string>;
|
||||
load: (projectId: string) => Promise<ProjectConversationState>;
|
||||
linkSession: (projectId: string, sessionId: string, agentId: string) => Promise<ProjectConversationState>;
|
||||
archiveSession: (projectId: string, sessionId: string) => Promise<ProjectConversationState>;
|
||||
restoreSession: (projectId: string, sessionId: string) => Promise<ProjectConversationState>;
|
||||
deleteSession: (projectId: string, sessionId: string) => Promise<ProjectConversationState>;
|
||||
markSessionRead: (projectId: string, sessionId: string) => Promise<ProjectConversationState>;
|
||||
markSessionUnread: (projectId: string, sessionId: string) => Promise<ProjectConversationState>;
|
||||
completeSession: (projectId: string, sessionId: string, incrementUnread?: boolean) => Promise<ProjectConversationState>;
|
||||
getSession: (projectId: string, sessionId: string) => ProjectSessionMetadata | undefined;
|
||||
remove: (projectId: string) => void;
|
||||
};
|
||||
|
||||
function emptyState(): ProjectConversationState {
|
||||
return { schemaVersion: 1, sessions: [], updatedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
export const useProjectConversationStore = create<ProjectConversationStore>((set, get) => {
|
||||
async function dispatch(projectId: string, sessionId: string, action: ConversationAction, agentId?: string, unread?: boolean) {
|
||||
const response = await hostApiFetch<ProjectConversationResponse>('/api/opencode/projects/conversations', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ projectId, sessionId, action, ...(agentId ? { agentId } : {}), ...(unread ? { unread: true } : {}) }),
|
||||
});
|
||||
if (!response.success || !response.state) throw new Error(response.error || '更新会话归属失败');
|
||||
set((state) => ({
|
||||
statesByProjectId: { ...state.statesByProjectId, [projectId]: response.state as ProjectConversationState },
|
||||
errorsByProjectId: Object.fromEntries(Object.entries(state.errorsByProjectId).filter(([id]) => id !== projectId)),
|
||||
}));
|
||||
return response.state;
|
||||
}
|
||||
|
||||
return {
|
||||
statesByProjectId: {},
|
||||
loadingProjectId: null,
|
||||
errorsByProjectId: {},
|
||||
|
||||
async load(projectId) {
|
||||
set({ loadingProjectId: projectId });
|
||||
try {
|
||||
const response = await hostApiFetch<ProjectConversationResponse>(
|
||||
`/api/opencode/projects/conversations?projectId=${encodeURIComponent(projectId)}`,
|
||||
);
|
||||
const nextState = response.state ?? emptyState();
|
||||
set((state) => ({
|
||||
loadingProjectId: null,
|
||||
statesByProjectId: { ...state.statesByProjectId, [projectId]: nextState },
|
||||
errorsByProjectId: Object.fromEntries(Object.entries(state.errorsByProjectId).filter(([id]) => id !== projectId)),
|
||||
}));
|
||||
return nextState;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
set((state) => ({
|
||||
loadingProjectId: null,
|
||||
errorsByProjectId: { ...state.errorsByProjectId, [projectId]: message },
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
linkSession(projectId, sessionId, agentId) {
|
||||
return dispatch(projectId, sessionId, 'link', agentId);
|
||||
},
|
||||
|
||||
archiveSession(projectId, sessionId) {
|
||||
return dispatch(projectId, sessionId, 'archive');
|
||||
},
|
||||
|
||||
restoreSession(projectId, sessionId) {
|
||||
return dispatch(projectId, sessionId, 'restore');
|
||||
},
|
||||
|
||||
deleteSession(projectId, sessionId) {
|
||||
return dispatch(projectId, sessionId, 'delete');
|
||||
},
|
||||
|
||||
markSessionRead(projectId, sessionId) {
|
||||
return dispatch(projectId, sessionId, 'read');
|
||||
},
|
||||
|
||||
markSessionUnread(projectId, sessionId) {
|
||||
return dispatch(projectId, sessionId, 'increment-unread');
|
||||
},
|
||||
|
||||
completeSession(projectId, sessionId, incrementUnread = false) {
|
||||
return dispatch(projectId, sessionId, 'complete', undefined, incrementUnread);
|
||||
},
|
||||
|
||||
getSession(projectId, sessionId) {
|
||||
return get().statesByProjectId[projectId]?.sessions.find((item) => item.sessionId === sessionId);
|
||||
},
|
||||
|
||||
remove(projectId) {
|
||||
set((state) => ({
|
||||
statesByProjectId: Object.fromEntries(Object.entries(state.statesByProjectId).filter(([id]) => id !== projectId)),
|
||||
errorsByProjectId: Object.fromEntries(Object.entries(state.errorsByProjectId).filter(([id]) => id !== projectId)),
|
||||
}));
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,205 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { createJSONStorage, persist, type StateStorage } from 'zustand/middleware';
|
||||
import {
|
||||
normalizeProjectTemplateId,
|
||||
readProjectTemplateSnapshot,
|
||||
type ProjectTemplateWriteOptions,
|
||||
writeProjectTemplateSnapshot,
|
||||
type ProjectTemplateId,
|
||||
type ProjectTemplateReadResult,
|
||||
type ProjectTemplateWriteResult,
|
||||
type ProjectTemplateSnapshot,
|
||||
} from '@/lib/project-templates';
|
||||
import { validateProjectTemplateSnapshot } from '../../shared/project-template';
|
||||
|
||||
type ProjectTemplatesState = {
|
||||
snapshotsByProjectId: Record<string, ProjectTemplateSnapshot>;
|
||||
templateErrorsByProjectId: Record<string, string>;
|
||||
loadStatusByProjectId: Record<string, 'loading' | 'valid' | 'missing' | 'invalid'>;
|
||||
templatesByProjectId: Partial<Record<string, ProjectTemplateId>>;
|
||||
setProjectTemplateSnapshot: (projectId: string, snapshot: ProjectTemplateSnapshot) => void;
|
||||
loadProjectTemplate: (projectId: string) => Promise<ProjectTemplateReadResult>;
|
||||
writeProjectTemplate: (
|
||||
projectId: string,
|
||||
snapshot: ProjectTemplateSnapshot,
|
||||
options?: ProjectTemplateWriteOptions,
|
||||
) => Promise<ProjectTemplateWriteResult>;
|
||||
setProjectTemplate: (projectId: string, templateId: ProjectTemplateId) => void;
|
||||
removeProjectTemplate: (projectId: string) => void;
|
||||
};
|
||||
|
||||
const inMemoryProjectTemplateStorage = (() => {
|
||||
const data = new Map<string, string>();
|
||||
return {
|
||||
getItem: (name: string) => data.get(name) ?? null,
|
||||
setItem: (name: string, value: string) => {
|
||||
data.set(name, value);
|
||||
},
|
||||
removeItem: (name: string) => {
|
||||
data.delete(name);
|
||||
},
|
||||
} satisfies StateStorage;
|
||||
})();
|
||||
|
||||
function getProjectTemplateStorage(): StateStorage {
|
||||
if (typeof window === 'undefined') return inMemoryProjectTemplateStorage;
|
||||
if (typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().includes('jsdom')) {
|
||||
return inMemoryProjectTemplateStorage;
|
||||
}
|
||||
try {
|
||||
const storage = window.localStorage;
|
||||
const testKey = 'niancode-project-template-storage-test';
|
||||
storage.setItem(testKey, '1');
|
||||
storage.removeItem(testKey);
|
||||
return storage;
|
||||
} catch {
|
||||
return inMemoryProjectTemplateStorage;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSnapshots(value: unknown): Record<string, ProjectTemplateSnapshot> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
||||
return Object.entries(value as Record<string, unknown>).reduce<Record<string, ProjectTemplateSnapshot>>(
|
||||
(snapshots, [projectId, rawSnapshot]) => {
|
||||
const validation = validateProjectTemplateSnapshot(rawSnapshot);
|
||||
if (projectId.trim() && validation.ok) {
|
||||
snapshots[projectId] = validation.snapshot;
|
||||
}
|
||||
return snapshots;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
function buildTemplatesByProjectId(
|
||||
snapshotsByProjectId: Record<string, ProjectTemplateSnapshot>,
|
||||
): Partial<Record<string, ProjectTemplateId>> {
|
||||
return Object.entries(snapshotsByProjectId).reduce<Partial<Record<string, ProjectTemplateId>>>(
|
||||
(templates, [projectId, snapshot]) => {
|
||||
const templateId = normalizeProjectTemplateId(snapshot.template.id);
|
||||
if (templateId) {
|
||||
templates[projectId] = templateId;
|
||||
}
|
||||
return templates;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
function applySnapshotState(
|
||||
snapshotsByProjectId: Record<string, ProjectTemplateSnapshot>,
|
||||
templateErrorsByProjectId: Record<string, string>,
|
||||
loadStatusByProjectId: Record<string, 'loading' | 'valid' | 'missing' | 'invalid'>,
|
||||
) {
|
||||
return {
|
||||
snapshotsByProjectId,
|
||||
templateErrorsByProjectId,
|
||||
loadStatusByProjectId,
|
||||
templatesByProjectId: buildTemplatesByProjectId(snapshotsByProjectId),
|
||||
};
|
||||
}
|
||||
|
||||
export const useProjectTemplatesStore = create<ProjectTemplatesState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
snapshotsByProjectId: {},
|
||||
templateErrorsByProjectId: {},
|
||||
loadStatusByProjectId: {},
|
||||
templatesByProjectId: {},
|
||||
|
||||
setProjectTemplateSnapshot: (projectId, snapshot) => {
|
||||
set((state) => {
|
||||
const nextErrors = { ...state.templateErrorsByProjectId };
|
||||
delete nextErrors[projectId];
|
||||
const nextStatuses = { ...state.loadStatusByProjectId, [projectId]: 'valid' as const };
|
||||
return applySnapshotState(
|
||||
{ ...state.snapshotsByProjectId, [projectId]: snapshot },
|
||||
nextErrors,
|
||||
nextStatuses,
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
loadProjectTemplate: async (projectId) => {
|
||||
set((state) => ({
|
||||
loadStatusByProjectId: {
|
||||
...state.loadStatusByProjectId,
|
||||
[projectId]: 'loading',
|
||||
},
|
||||
}));
|
||||
const result = await readProjectTemplateSnapshot(projectId);
|
||||
set((state) => {
|
||||
const nextSnapshots = { ...state.snapshotsByProjectId };
|
||||
const nextErrors = { ...state.templateErrorsByProjectId };
|
||||
const nextStatuses = { ...state.loadStatusByProjectId };
|
||||
|
||||
if (result.status === 'valid') {
|
||||
nextSnapshots[projectId] = result.snapshot;
|
||||
delete nextErrors[projectId];
|
||||
nextStatuses[projectId] = 'valid';
|
||||
} else if (result.status === 'invalid') {
|
||||
delete nextSnapshots[projectId];
|
||||
nextErrors[projectId] = result.error;
|
||||
nextStatuses[projectId] = 'invalid';
|
||||
} else {
|
||||
delete nextSnapshots[projectId];
|
||||
delete nextErrors[projectId];
|
||||
nextStatuses[projectId] = 'missing';
|
||||
}
|
||||
|
||||
return applySnapshotState(nextSnapshots, nextErrors, nextStatuses);
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
||||
writeProjectTemplate: async (projectId, snapshot, options) => {
|
||||
const result = await writeProjectTemplateSnapshot(projectId, snapshot, options);
|
||||
set((state) => {
|
||||
const nextErrors = { ...state.templateErrorsByProjectId };
|
||||
delete nextErrors[projectId];
|
||||
const nextStatuses = { ...state.loadStatusByProjectId, [projectId]: 'valid' as const };
|
||||
return applySnapshotState(
|
||||
{ ...state.snapshotsByProjectId, [projectId]: result.snapshot },
|
||||
nextErrors,
|
||||
nextStatuses,
|
||||
);
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
||||
setProjectTemplate: (_projectId, _templateId) => {
|
||||
throw new Error(
|
||||
'setProjectTemplate is deprecated and cannot create authoritative project template snapshots locally.',
|
||||
);
|
||||
},
|
||||
|
||||
removeProjectTemplate: (projectId) => {
|
||||
set((state) => {
|
||||
const nextSnapshots = { ...state.snapshotsByProjectId };
|
||||
const nextErrors = { ...state.templateErrorsByProjectId };
|
||||
const nextStatuses = { ...state.loadStatusByProjectId };
|
||||
delete nextSnapshots[projectId];
|
||||
delete nextErrors[projectId];
|
||||
delete nextStatuses[projectId];
|
||||
return applySnapshotState(nextSnapshots, nextErrors, nextStatuses);
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'niancode-project-templates',
|
||||
storage: createJSONStorage(getProjectTemplateStorage),
|
||||
partialize: (state) => ({
|
||||
snapshotsByProjectId: state.snapshotsByProjectId,
|
||||
}),
|
||||
merge: (persisted, current) => {
|
||||
const state = persisted as Partial<ProjectTemplatesState> | undefined;
|
||||
return {
|
||||
...current,
|
||||
...applySnapshotState(normalizeSnapshots(state?.snapshotsByProjectId), {}, {}),
|
||||
};
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export type { ProjectTemplateWriteOptions } from '@/lib/project-templates';
|
||||
Reference in New Issue
Block a user