feat: organize project conversations and add coding teacher side chat
This commit is contained in:
52
electron/coding-projects/conversation-history.ts
Normal file
52
electron/coding-projects/conversation-history.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { CodingConversationV2 } from './conversation-store';
|
||||
import { validateSessionKey } from './conversation-store';
|
||||
import type { ConversationSnapshot } from '../../shared/coding-conversation-contracts';
|
||||
import { getPiManagedPaths } from '../coding-runtime/pi/resource-loader';
|
||||
import { projectPiSessionSnapshot } from '../coding-runtime/pi/session-projector';
|
||||
|
||||
/** Read durable active-branch history without starting or reconfiguring Pi. */
|
||||
export async function readCodingConversationHistory(
|
||||
userDataDir: string,
|
||||
projectId: string,
|
||||
conversation: CodingConversationV2
|
||||
): Promise<ConversationSnapshot> {
|
||||
const base: ConversationSnapshot = {
|
||||
schemaVersion: 1,
|
||||
conversation: {
|
||||
id: conversation.id,
|
||||
projectId,
|
||||
agentId: conversation.agentId,
|
||||
title: conversation.title,
|
||||
model: { model: conversation.model, modelResolution: conversation.modelResolution },
|
||||
},
|
||||
nodes: [],
|
||||
run: { status: 'idle' },
|
||||
queue: { items: [] },
|
||||
context: { usedTokens: 0, contextWindow: 0, compaction: 'idle' },
|
||||
pendingInteractions: [],
|
||||
worker: { status: 'stopped', generation: 0 },
|
||||
cursor: { workerGeneration: 0, seq: 0 },
|
||||
};
|
||||
if (!conversation.sessionKey) return base;
|
||||
const filename = path.join(
|
||||
getPiManagedPaths(userDataDir).sessionsDir,
|
||||
projectId,
|
||||
validateSessionKey(conversation.sessionKey) + '.jsonl'
|
||||
);
|
||||
const text = await readFile(filename, 'utf8');
|
||||
// Pi 0.84.2 rebuilds its current leaf from the last non-header entry.
|
||||
const entries = text
|
||||
.split('\n')
|
||||
.filter((line) => line.trim())
|
||||
.map((line) => JSON.parse(line) as { id: string; type: string })
|
||||
.filter((entry) => entry.type !== 'session');
|
||||
const snapshot = await projectPiSessionSnapshot({
|
||||
snapshot: base,
|
||||
workerGeneration: 1,
|
||||
state: { isStreaming: false },
|
||||
entries: { entries, leafId: entries.at(-1)?.id ?? null },
|
||||
});
|
||||
return { ...snapshot, worker: { status: 'stopped', generation: 1 } };
|
||||
}
|
||||
@@ -225,6 +225,8 @@ export function normalizeCodingProjectConfigV2(value: unknown): CodingProjectCon
|
||||
projectType,
|
||||
...(record.projectId !== undefined ? { projectId: record.projectId } : {}),
|
||||
initialized: record.initialized === true,
|
||||
...(typeof record.defaultAgentId === 'string' && AGENT_ID_PATTERN.test(record.defaultAgentId)
|
||||
? { defaultAgentId: record.defaultAgentId } : {}),
|
||||
agents,
|
||||
knowledgeDirectory: 'knowledge',
|
||||
createdAt,
|
||||
@@ -232,6 +234,27 @@ export function normalizeCodingProjectConfigV2(value: unknown): CodingProjectCon
|
||||
};
|
||||
}
|
||||
|
||||
export function ensureDefaultCodingAgent(config: CodingProjectConfigV2, now = new Date().toISOString()): CodingProjectConfigV2 {
|
||||
const usable = config.agents.filter((agent) => agent.enabled && !agent.archivedAt);
|
||||
const current = usable.find((agent) => agent.id === config.defaultAgentId);
|
||||
if (current && config.initialized) return config;
|
||||
const existing = current ?? usable.find((agent) => agent.pinned) ?? usable[0];
|
||||
if (existing) return { ...config, defaultAgentId: existing.id, initialized: true, updatedAt: now };
|
||||
let id = 'coding-default';
|
||||
let suffix = 1;
|
||||
while (config.agents.some((agent) => agent.id === id)) id = `coding-default-${suffix++}`;
|
||||
let name = '编程助手';
|
||||
suffix = 1;
|
||||
while (config.agents.some((agent) => agent.name === name)) name = `编程助手 ${suffix++}`;
|
||||
const agent: CodingProjectAgentV2 = {
|
||||
id, name, avatarId: 'avatar-01', roleName: '编程助手', builtIn: true, enabled: true,
|
||||
model: null, modelResolution: 'required', skillIds: [], prompt: '',
|
||||
responsibility: { mission: '协助完成当前项目的编程任务。', owns: [], boundaries: [], collaborators: [], principles: [] },
|
||||
archivedAt: null, pinned: true, createdAt: now, updatedAt: now,
|
||||
};
|
||||
return { ...config, defaultAgentId: id, initialized: true, agents: [...config.agents, agent], updatedAt: now };
|
||||
}
|
||||
|
||||
export function createCodingProjectConfigV2(
|
||||
now = new Date().toISOString(),
|
||||
projectType: ProjectType = 'custom',
|
||||
@@ -240,7 +263,7 @@ export function createCodingProjectConfigV2(
|
||||
if (projectId !== undefined && !isCanonicalCodingProjectId(projectId)) {
|
||||
throw new Error('Project identity is invalid');
|
||||
}
|
||||
return {
|
||||
return ensureDefaultCodingAgent({
|
||||
schemaVersion: 2,
|
||||
projectType,
|
||||
...(projectId !== undefined ? { projectId } : {}),
|
||||
@@ -249,7 +272,7 @@ export function createCodingProjectConfigV2(
|
||||
knowledgeDirectory: 'knowledge',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
}, now);
|
||||
}
|
||||
|
||||
export async function readCodingProjectConfigV2(
|
||||
@@ -281,6 +304,7 @@ export async function createCodingProjectMetadata(
|
||||
options: {
|
||||
projectType?: ProjectType;
|
||||
projectId?: string;
|
||||
defaultModel?: ProductModelRef | null;
|
||||
now?: string;
|
||||
writer?: JsonFileWriter;
|
||||
} = {},
|
||||
@@ -292,6 +316,10 @@ export async function createCodingProjectMetadata(
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
||||
}
|
||||
const config = createCodingProjectConfigV2(options.now, options.projectType, options.projectId);
|
||||
if (options.defaultModel) {
|
||||
config.agents[0].model = normalizeProductModelRef(options.defaultModel);
|
||||
config.agents[0].modelResolution = 'resolved';
|
||||
}
|
||||
await Promise.all([
|
||||
mkdir(path.join(projectPath, '.makelore'), { recursive: true }),
|
||||
mkdir(path.join(projectPath, 'knowledge'), { recursive: true }),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type CodingConversationV2,
|
||||
} from './conversation-store';
|
||||
import {
|
||||
ensureDefaultCodingAgent,
|
||||
isCanonicalCodingProjectId,
|
||||
normalizeCodingProjectConfigV2,
|
||||
normalizeProjectIdentityChoice,
|
||||
@@ -60,6 +61,7 @@ export interface CodingProjectConfigSnapshot {
|
||||
}
|
||||
|
||||
export interface CodingProjectServiceOptions {
|
||||
getDefaultModel?(): Promise<import('../../shared/coding-conversation-contracts').ProductModelRef | null>;
|
||||
createProjectId?: () => string;
|
||||
now?: () => string;
|
||||
onResourcesChanged?(project: CodingProject): Promise<void> | void;
|
||||
@@ -287,6 +289,7 @@ export class CodingProjectService {
|
||||
const { project, config } = await createLocalCodingProject({
|
||||
projectPath,
|
||||
projectId,
|
||||
defaultModel: await this.options.getDefaultModel?.().catch(() => null),
|
||||
...(input.projectType ? { projectType: input.projectType } : {}),
|
||||
}, this.store);
|
||||
const snapshot = { project, config, knowledgeFiles: [] };
|
||||
@@ -338,7 +341,8 @@ export class CodingProjectService {
|
||||
async getConfig(projectId?: string): Promise<CodingProjectConfigSnapshot> {
|
||||
const project = projectId ? await this.getProject(projectId) : await this.requireActiveProject();
|
||||
const config = await this.requireCurrentConfig(project);
|
||||
return config.projectId
|
||||
return config.projectId && config.initialized && config.agents.some((agent) =>
|
||||
agent.id === config.defaultAgentId && agent.enabled && !agent.archivedAt)
|
||||
? await this.projectConfigSnapshot(project, config)
|
||||
: await this.ensureProjectIdentity(project);
|
||||
}
|
||||
@@ -530,20 +534,21 @@ export class CodingProjectService {
|
||||
): Promise<CodingProjectConfigSnapshot> {
|
||||
return await this.serializeIdentityTransition(project.path, async () => {
|
||||
const current = await this.requireCurrentConfig(project);
|
||||
if (current.projectId) {
|
||||
const normalized = ensureDefaultCodingAgent(current, this.options.now?.());
|
||||
if (current.projectId && normalized === current) {
|
||||
return await this.projectConfigSnapshot(project, current);
|
||||
}
|
||||
const nextProjectId = resolveProjectIdentity(
|
||||
const nextProjectId = current.projectId ?? resolveProjectIdentity(
|
||||
{ kind: 'create' },
|
||||
this.options.createProjectId ?? randomUUID,
|
||||
);
|
||||
await this.options.onProjectIdentityChanging?.(
|
||||
if (!current.projectId) await this.options.onProjectIdentityChanging?.(
|
||||
project,
|
||||
undefined,
|
||||
nextProjectId,
|
||||
);
|
||||
const next = {
|
||||
...current,
|
||||
...normalized,
|
||||
projectId: nextProjectId,
|
||||
updatedAt: this.options.now?.() ?? new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -196,6 +196,7 @@ export async function createLocalCodingProject(
|
||||
projectPath: string;
|
||||
projectType?: ProjectType;
|
||||
projectId?: string;
|
||||
defaultModel?: import('../../shared/coding-conversation-contracts').ProductModelRef | null;
|
||||
now?: string;
|
||||
},
|
||||
store: CodingProjectStore,
|
||||
@@ -203,6 +204,7 @@ export async function createLocalCodingProject(
|
||||
const config = await createCodingProjectMetadata(input.projectPath, {
|
||||
projectType: input.projectType,
|
||||
projectId: input.projectId,
|
||||
defaultModel: input.defaultModel,
|
||||
now: input.now,
|
||||
});
|
||||
const project = await store.openFolder(input.projectPath);
|
||||
|
||||
Reference in New Issue
Block a user