fix(chat): recall the current session's prompts by default; tidy the six merged PRs
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.
This commit is contained in:
@@ -2134,7 +2134,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
}
|
||||
applyLoadedTextContent(content);
|
||||
},
|
||||
maxBytes: MAX_CONTENT_POLL_BYTES,
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -2165,7 +2164,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
if (contentPoller && latestStat.size <= MAX_CONTENT_POLL_BYTES) {
|
||||
// Only an observed read retires the change; a dirty buffer or a
|
||||
// failed read leaves the baseline so the next tick retries.
|
||||
const observed = await contentPoller.poll(latestStat.size);
|
||||
const observed = await contentPoller.poll();
|
||||
if (observed && !cancelled) {
|
||||
lastLoadedFileStatRef.current = latestStat;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { createFileContentPoller } from './fileContentPoller';
|
||||
|
||||
const MAX_BYTES = 200_000;
|
||||
|
||||
const deferred = <T>() => {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((done) => { resolve = done; });
|
||||
@@ -23,11 +21,10 @@ describe('createFileContentPoller', () => {
|
||||
getLoadedRevision: () => 0,
|
||||
isDirty: () => false,
|
||||
applyContent: (content) => applied.push(content),
|
||||
maxBytes: MAX_BYTES,
|
||||
});
|
||||
|
||||
expect(await poller.poll(12)).toBe(true);
|
||||
expect(await poller.poll(12)).toBe(true);
|
||||
expect(await poller.poll()).toBe(true);
|
||||
expect(await poller.poll()).toBe(true);
|
||||
|
||||
expect(reads).toBe(2);
|
||||
expect(applied).toEqual([]);
|
||||
@@ -41,10 +38,9 @@ describe('createFileContentPoller', () => {
|
||||
getLoadedRevision: () => 0,
|
||||
isDirty: () => false,
|
||||
applyContent: (content) => applied.push(content),
|
||||
maxBytes: MAX_BYTES,
|
||||
});
|
||||
|
||||
expect(await poller.poll(6)).toBe(false);
|
||||
expect(await poller.poll()).toBe(false);
|
||||
expect(applied).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -59,10 +55,9 @@ describe('createFileContentPoller', () => {
|
||||
getLoadedRevision: () => 0,
|
||||
isDirty: () => true,
|
||||
applyContent: () => undefined,
|
||||
maxBytes: MAX_BYTES,
|
||||
});
|
||||
|
||||
expect(await poller.poll(6)).toBe(false);
|
||||
expect(await poller.poll()).toBe(false);
|
||||
expect(reads).toBe(0);
|
||||
});
|
||||
|
||||
@@ -78,10 +73,9 @@ describe('createFileContentPoller', () => {
|
||||
getLoadedRevision: () => 0,
|
||||
isDirty: () => false,
|
||||
applyContent: (content) => applied.push(content),
|
||||
maxBytes: MAX_BYTES,
|
||||
});
|
||||
|
||||
expect(await poller.poll(3)).toBe(true);
|
||||
expect(await poller.poll()).toBe(true);
|
||||
|
||||
expect(reads).toBe(2);
|
||||
expect(applied).toEqual(['abd']);
|
||||
@@ -97,10 +91,9 @@ describe('createFileContentPoller', () => {
|
||||
getLoadedRevision: () => 0,
|
||||
isDirty: () => dirty,
|
||||
applyContent: (content) => applied.push(content),
|
||||
maxBytes: MAX_BYTES,
|
||||
});
|
||||
|
||||
const polling = poller.poll(20);
|
||||
const polling = poller.poll();
|
||||
dirty = true;
|
||||
read.resolve('external edit');
|
||||
await polling;
|
||||
@@ -117,17 +110,16 @@ describe('createFileContentPoller', () => {
|
||||
getLoadedRevision: () => 0,
|
||||
isDirty: () => false,
|
||||
applyContent: (content) => applied.push(content),
|
||||
maxBytes: MAX_BYTES,
|
||||
});
|
||||
|
||||
await poller.poll(7);
|
||||
await poller.poll();
|
||||
expect(applied).toEqual([]);
|
||||
|
||||
expect(await poller.poll(7)).toBe(true);
|
||||
expect(await poller.poll()).toBe(true);
|
||||
expect(applied).toEqual(['settled']);
|
||||
});
|
||||
|
||||
test('allows only one read in flight and ignores a disposed poll', async () => {
|
||||
test('ignores a read that resolves after dispose', async () => {
|
||||
const read = deferred<string>();
|
||||
let reads = 0;
|
||||
const applied: string[] = [];
|
||||
@@ -140,14 +132,12 @@ describe('createFileContentPoller', () => {
|
||||
getLoadedRevision: () => 0,
|
||||
isDirty: () => false,
|
||||
applyContent: (content) => applied.push(content),
|
||||
maxBytes: MAX_BYTES,
|
||||
});
|
||||
|
||||
const first = poller.poll(5);
|
||||
await poller.poll(5);
|
||||
const pending = poller.poll();
|
||||
poller.dispose();
|
||||
read.resolve('after');
|
||||
await first;
|
||||
expect(await pending).toBe(false);
|
||||
|
||||
expect(reads).toBe(1);
|
||||
expect(applied).toEqual([]);
|
||||
@@ -165,10 +155,9 @@ describe('createFileContentPoller', () => {
|
||||
getLoadedRevision: () => loadedRevision,
|
||||
isDirty: () => false,
|
||||
applyContent: (content) => applied.push(content),
|
||||
maxBytes: MAX_BYTES,
|
||||
});
|
||||
|
||||
const polling = poller.poll(20);
|
||||
const polling = poller.poll();
|
||||
firstRead.resolve('external edit');
|
||||
await Promise.resolve();
|
||||
loadedRevision += 1;
|
||||
@@ -179,22 +168,4 @@ describe('createFileContentPoller', () => {
|
||||
expect(applied).toEqual([]);
|
||||
});
|
||||
|
||||
test('does not read content above the polling byte limit', async () => {
|
||||
let reads = 0;
|
||||
const poller = createFileContentPoller({
|
||||
readContent: async () => {
|
||||
reads += 1;
|
||||
return 'content';
|
||||
},
|
||||
getLoadedContent: () => 'before',
|
||||
getLoadedRevision: () => 0,
|
||||
isDirty: () => false,
|
||||
applyContent: () => undefined,
|
||||
maxBytes: MAX_BYTES,
|
||||
});
|
||||
|
||||
expect(await poller.poll(MAX_BYTES + 1)).toBe(false);
|
||||
|
||||
expect(reads).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,18 +4,17 @@ type FileContentPollerOptions = {
|
||||
getLoadedRevision: () => number;
|
||||
isDirty: () => boolean;
|
||||
applyContent: (content: string) => void;
|
||||
maxBytes: number;
|
||||
};
|
||||
|
||||
// The caller serializes polls and decides which files qualify (text, within
|
||||
// the byte limit); this owns only the read-compare-confirm-apply step.
|
||||
export const createFileContentPoller = (options: FileContentPollerOptions) => {
|
||||
let active = true;
|
||||
let polling = false;
|
||||
|
||||
return {
|
||||
/** Resolves true only when the poll observed the file's current content. */
|
||||
poll: async (size: number): Promise<boolean> => {
|
||||
if (!active || polling || options.isDirty() || size > options.maxBytes) return false;
|
||||
polling = true;
|
||||
poll: async (): Promise<boolean> => {
|
||||
if (!active || options.isDirty()) return false;
|
||||
const loadedContent = options.getLoadedContent();
|
||||
const loadedRevision = options.getLoadedRevision();
|
||||
try {
|
||||
@@ -23,6 +22,7 @@ export const createFileContentPoller = (options: FileContentPollerOptions) => {
|
||||
if (!active || options.isDirty() || loadedRevision !== options.getLoadedRevision()) return false;
|
||||
if (content === loadedContent) return true;
|
||||
|
||||
// A second read guards against applying a half-written file.
|
||||
const confirmedContent = await options.readContent();
|
||||
if (!active || options.isDirty() || confirmedContent !== content || loadedRevision !== options.getLoadedRevision()) {
|
||||
return false;
|
||||
@@ -32,8 +32,6 @@ export const createFileContentPoller = (options: FileContentPollerOptions) => {
|
||||
} catch {
|
||||
// A failed read is not proof the file is unchanged; the next poll retries.
|
||||
return false;
|
||||
} finally {
|
||||
polling = false;
|
||||
}
|
||||
},
|
||||
dispose: () => {
|
||||
|
||||
Reference in New Issue
Block a user