fix: Claude resume候補をjsonlフォールバックで取得
This commit is contained in:
parent
134b666480
commit
a5e2badc0b
@ -26,6 +26,30 @@ describe('loadSessionIndex', () => {
|
|||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('falls back to jsonl files when sessions-index.json does not exist', () => {
|
||||||
|
const filePath = join(mockSessionsDir, 'fallback-session.jsonl');
|
||||||
|
writeFileSync(filePath, [
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'user',
|
||||||
|
gitBranch: 'develop',
|
||||||
|
message: { content: [{ type: 'text', text: 'Resume me from jsonl fallback' }] },
|
||||||
|
isSidechain: false,
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'assistant',
|
||||||
|
message: { content: [{ type: 'text', text: 'Sure' }] },
|
||||||
|
}),
|
||||||
|
].join('\n'));
|
||||||
|
|
||||||
|
const result = loadSessionIndex('/any');
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]!.sessionId).toBe('fallback-session');
|
||||||
|
expect(result[0]!.firstPrompt).toBe('Resume me from jsonl fallback');
|
||||||
|
expect(result[0]!.messageCount).toBe(2);
|
||||||
|
expect(result[0]!.gitBranch).toBe('develop');
|
||||||
|
});
|
||||||
|
|
||||||
it('reads and parses sessions-index.json correctly', () => {
|
it('reads and parses sessions-index.json correctly', () => {
|
||||||
const indexData = {
|
const indexData = {
|
||||||
version: 1,
|
version: 1,
|
||||||
@ -105,6 +129,36 @@ describe('loadSessionIndex', () => {
|
|||||||
const result = loadSessionIndex('/any');
|
const result = loadSessionIndex('/any');
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses jsonl fallback when sessions-index.json is corrupted', () => {
|
||||||
|
writeFileSync(join(mockSessionsDir, 'sessions-index.json'), '{corrupted json content');
|
||||||
|
writeFileSync(join(mockSessionsDir, 'fallback-on-corrupt.jsonl'), JSON.stringify({
|
||||||
|
type: 'user',
|
||||||
|
message: { content: [{ type: 'text', text: 'Fallback on corrupted index' }] },
|
||||||
|
isSidechain: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = loadSessionIndex('/any');
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]!.sessionId).toBe('fallback-on-corrupt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters out sidechain sessions in jsonl fallback', () => {
|
||||||
|
writeFileSync(join(mockSessionsDir, 'main-session.jsonl'), JSON.stringify({
|
||||||
|
type: 'user',
|
||||||
|
message: { content: [{ type: 'text', text: 'Main session' }] },
|
||||||
|
isSidechain: false,
|
||||||
|
}));
|
||||||
|
writeFileSync(join(mockSessionsDir, 'sidechain-session.jsonl'), JSON.stringify({
|
||||||
|
type: 'user',
|
||||||
|
message: { content: [{ type: 'text', text: 'Sidechain session' }] },
|
||||||
|
isSidechain: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = loadSessionIndex('/any');
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]!.sessionId).toBe('main-session');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('extractLastAssistantResponse', () => {
|
describe('extractLastAssistantResponse', () => {
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
* to extract session metadata and last assistant responses.
|
* to extract session metadata and last assistant responses.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { getClaudeProjectSessionsDir } from '../config/project/sessionStore.js';
|
import { getClaudeProjectSessionsDir } from '../config/project/sessionStore.js';
|
||||||
|
|
||||||
@ -26,6 +26,99 @@ interface SessionsIndex {
|
|||||||
entries: SessionIndexEntry[];
|
entries: SessionIndexEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SessionMessageContent {
|
||||||
|
type: string;
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionMessage {
|
||||||
|
content?: SessionMessageContent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionJsonlRecord {
|
||||||
|
type?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
message?: SessionMessage;
|
||||||
|
timestamp?: string;
|
||||||
|
gitBranch?: string;
|
||||||
|
isSidechain?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEntryFromJsonlFile(sessionsDir: string, fileName: string): SessionIndexEntry | null {
|
||||||
|
const fullPath = join(sessionsDir, fileName);
|
||||||
|
const sessionId = fileName.replace(/\.jsonl$/, '');
|
||||||
|
|
||||||
|
if (!sessionId || sessionId === fileName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstPrompt = '';
|
||||||
|
let messageCount = 0;
|
||||||
|
let gitBranch = '';
|
||||||
|
let isSidechain = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = readFileSync(fullPath, 'utf-8');
|
||||||
|
const lines = content.split('\n').filter((line) => line.trim().length > 0);
|
||||||
|
for (const line of lines) {
|
||||||
|
let record: SessionJsonlRecord;
|
||||||
|
try {
|
||||||
|
record = JSON.parse(line) as SessionJsonlRecord;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.type === 'user' || record.type === 'assistant') {
|
||||||
|
messageCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gitBranch && typeof record.gitBranch === 'string') {
|
||||||
|
gitBranch = record.gitBranch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.isSidechain === true) {
|
||||||
|
isSidechain = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstPrompt && record.type === 'user' && Array.isArray(record.message?.content)) {
|
||||||
|
const textBlock = record.message.content.find((block) => block.type === 'text' && typeof block.text === 'string');
|
||||||
|
if (textBlock?.text) {
|
||||||
|
firstPrompt = textBlock.text.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modified = statSync(fullPath).mtime.toISOString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
firstPrompt: firstPrompt || sessionId,
|
||||||
|
modified,
|
||||||
|
messageCount,
|
||||||
|
gitBranch,
|
||||||
|
isSidechain,
|
||||||
|
fullPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSessionIndexFromJsonl(sessionsDir: string): SessionIndexEntry[] {
|
||||||
|
if (!existsSync(sessionsDir)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonlFiles = readdirSync(sessionsDir)
|
||||||
|
.filter((name) => name.endsWith('.jsonl'));
|
||||||
|
|
||||||
|
return jsonlFiles
|
||||||
|
.map((fileName) => buildEntryFromJsonlFile(sessionsDir, fileName))
|
||||||
|
.filter((entry): entry is SessionIndexEntry => entry !== null)
|
||||||
|
.filter((entry) => !entry.isSidechain)
|
||||||
|
.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the session index for a project directory.
|
* Load the session index for a project directory.
|
||||||
*
|
*
|
||||||
@ -37,7 +130,7 @@ export function loadSessionIndex(projectDir: string): SessionIndexEntry[] {
|
|||||||
const indexPath = join(sessionsDir, 'sessions-index.json');
|
const indexPath = join(sessionsDir, 'sessions-index.json');
|
||||||
|
|
||||||
if (!existsSync(indexPath)) {
|
if (!existsSync(indexPath)) {
|
||||||
return [];
|
return loadSessionIndexFromJsonl(sessionsDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = readFileSync(indexPath, 'utf-8');
|
const content = readFileSync(indexPath, 'utf-8');
|
||||||
@ -46,11 +139,11 @@ export function loadSessionIndex(projectDir: string): SessionIndexEntry[] {
|
|||||||
try {
|
try {
|
||||||
index = JSON.parse(content) as SessionsIndex;
|
index = JSON.parse(content) as SessionsIndex;
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return loadSessionIndexFromJsonl(sessionsDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!index.entries || !Array.isArray(index.entries)) {
|
if (!index.entries || !Array.isArray(index.entries)) {
|
||||||
return [];
|
return loadSessionIndexFromJsonl(sessionsDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
return index.entries
|
return index.entries
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user