Input history (#3035) shipped with "All projects" as the default scope and only recorded prompts sent after the upgrade, so ArrowUp showed other sessions' prompts and, once switched to "Current session", nothing at all. Default to the current session and merge the visible transcript's prompts with the persisted bucket. Existing sessions recall as they did before #3035, while new prompts keep their attachments and stay recallable after a revert hides them from the transcript. Cleanup across #1855, #2297, #3072, #3178, #3035 and #3135: drop the duplicate poll guards in the file content poller, the zod schema the VS Code package cannot depend on, a copied file-URL helper and stray whitespace; move the Enter-to-send strings into the settings namespace; document OPENCHAMBER_CHATS_DIR, resolve the chats root once on the server and warm it alongside the other bootstrap calls.
119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
import type { MessageHistoryValue } from './composer/state/useMessageHistory';
|
||
import type { InputHistoryScope } from '@/lib/inputHistoryScope';
|
||
import {
|
||
createInputHistorySubmission,
|
||
type InputHistoryAttachment,
|
||
type InputHistoryEntry,
|
||
type InputHistoryIdentity,
|
||
type InputHistorySubmission,
|
||
} from '@/stores/useInputHistoryStore';
|
||
import type { AttachedFile } from '@/stores/types/sessionTypes';
|
||
import type { TranscriptPrompt } from '@/sync/user-message-history';
|
||
import { toServerFileUrl } from './composer/attachments/filePaths';
|
||
|
||
type HistoryQueuedMessage = {
|
||
content: string;
|
||
attachments?: readonly AttachedFile[];
|
||
};
|
||
|
||
type BuildHistorySubmissionsArgs = {
|
||
inputMode: 'normal' | 'shell';
|
||
queuedMessages: readonly HistoryQueuedMessage[];
|
||
composerText: string;
|
||
composerAttachments: readonly AttachedFile[];
|
||
includeComposer: boolean;
|
||
};
|
||
|
||
export function buildChatInputHistorySubmissions({
|
||
inputMode,
|
||
queuedMessages,
|
||
composerText,
|
||
composerAttachments,
|
||
includeComposer,
|
||
}: BuildHistorySubmissionsArgs): InputHistorySubmission[] | undefined {
|
||
if (inputMode === 'shell') return undefined;
|
||
|
||
const submissions = queuedMessages.map((queued) => (
|
||
createInputHistorySubmission(queued.content, queued.attachments ?? [])
|
||
));
|
||
|
||
if (includeComposer) {
|
||
submissions.push(createInputHistorySubmission(composerText, composerAttachments));
|
||
}
|
||
|
||
return submissions.length > 0 ? submissions : undefined;
|
||
}
|
||
|
||
function materializeHistoryAttachment(attachment: InputHistoryAttachment): AttachedFile | null {
|
||
if (attachment.source === 'file-url') {
|
||
if (!attachment.reference || attachment.reference.startsWith('data:')) return null;
|
||
return {
|
||
id: `history-${attachment.key}`,
|
||
file: new File([], attachment.filename, { type: attachment.mimeType }),
|
||
dataUrl: attachment.reference,
|
||
mimeType: attachment.mimeType,
|
||
filename: attachment.filename,
|
||
size: attachment.size,
|
||
source: 'local',
|
||
serverPath: attachment.reference,
|
||
};
|
||
}
|
||
|
||
if (attachment.source === 'vscode-file') {
|
||
return {
|
||
id: `history-${attachment.key}`,
|
||
file: new File([], attachment.filename, { type: attachment.mimeType }),
|
||
dataUrl: toServerFileUrl(attachment.reference),
|
||
mimeType: attachment.mimeType,
|
||
filename: attachment.filename,
|
||
size: attachment.size,
|
||
source: 'vscode',
|
||
vscodePath: attachment.reference,
|
||
vscodeSource: 'file',
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
export function mapInputHistoryEntriesToValues(
|
||
entries: readonly InputHistoryEntry[],
|
||
): Array<MessageHistoryValue<AttachedFile>> {
|
||
return entries.map((entry) => ({
|
||
text: entry.text,
|
||
attachments: entry.restorableAttachments
|
||
.map(materializeHistoryAttachment)
|
||
.filter((attachment): attachment is AttachedFile => attachment !== null),
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Session-scoped recall: the visible transcript's prompts (so sessions older
|
||
* than the persisted store still recall) merged with the persisted bucket
|
||
* (attachments, and prompts a revert hid from the transcript), oldest first.
|
||
* A prompt present in both collapses to the persisted entry.
|
||
*/
|
||
export function mergeSessionInputHistory(
|
||
transcript: readonly TranscriptPrompt[],
|
||
entries: readonly InputHistoryEntry[],
|
||
): Array<MessageHistoryValue<AttachedFile>> {
|
||
const persistedTexts = new Set(entries.map((entry) => entry.text));
|
||
const timed: Array<{ at: number; value: MessageHistoryValue<AttachedFile> }> = [
|
||
...transcript
|
||
.filter((prompt) => !persistedTexts.has(prompt.text))
|
||
.map((prompt) => ({ at: prompt.createdAt, value: { text: prompt.text, attachments: [] } })),
|
||
...mapInputHistoryEntriesToValues(entries)
|
||
// submittedAt is milliseconds × 1000 plus a sequence number.
|
||
.map((value, index) => ({ at: Math.floor(entries[index]!.submittedAt / 1000), value })),
|
||
];
|
||
return timed.sort((left, right) => left.at - right.at).map((item) => item.value);
|
||
}
|
||
|
||
export function buildInputHistoryNavigatorIdentity(
|
||
scope: InputHistoryScope,
|
||
identity: InputHistoryIdentity | null,
|
||
): string {
|
||
if (!identity) return `${scope}\nmissing`;
|
||
return [scope, identity.runtimeKey, identity.directory, identity.sessionId].join('\n');
|
||
}
|