feat: update Makelore modules and conversations
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-07-31 10:08:41 +08:00
parent b8ca3f8eea
commit 80e8386fa6
175 changed files with 8036 additions and 10581 deletions

View File

@@ -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

View File

@@ -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,

View 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)),
}));
},
};
});

View File

@@ -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';