Files
openchamber/packages/ui/src/lib/planSaveQueue.ts
T
Bohdan Triapitsyn 43c4cc625f fix(ui): open saved plans against their owning project
Saved Project knowledge plans opened as an empty editor whenever the
viewer could not resolve the owning project from the current directory:
managed chats (openchamber:chats is not a registered project), worktrees
outside the repo path, and plan tabs restored after a reload. Titles
still rendered because the list reads the manifest through the correct
owner.

- Thread the owner explicitly (savedProjectPlan = { projectRef, planId })
  from the panel, mobile surfaces, and persisted context tabs; PlanView
  no longer guesses the project.
- An unrecognized directory resolves to no owner instead of borrowing
  the active project's knowledge.
- Serialize plan writes per document (planSaveQueue) so close/switch
  within the autosave debounce no longer drops the last edits, saves
  cannot land out of order, and a recovered save clears the error banner.
- Send saved-plan contents inline in Improve/Implement prompts (they
  have no file path); disable those actions for managed-chat plans,
  which have no project directory to create a session in.
- Drop persisted plan tabs that carry an id without an owner rather than
  reopening them against a guessed project.
2026-08-27 20:18:12 +03:00

62 lines
2.4 KiB
TypeScript

/**
* Write queue for open plan documents.
*
* Debounced autosave and close-time flushes must reach the disk in edit order,
* and a document re-opened while its own write is still in flight must read
* the post-write state, not race it. The queue serializes writes per logical
* document key and deduplicates revisions so a flush of revision N can never
* run behind, or twice behind, a debounced save of the same revision.
*/
interface PlanSaveQueue {
/**
* Queue one write for `key`. Writes for the same key run in schedule order;
* writes for different keys never block each other. A revision at or below
* the last queued revision for that key is skipped — the queued write
* already carries newer content — and the returned promise tracks the
* outstanding chain so callers can still await it.
*/
schedule: (key: string, revision: number, write: () => Promise<void>) => Promise<void>;
/** Resolves when every write queued for `key` has settled. */
pendingFor: (key: string) => Promise<void>;
/**
* Forgets the revision watermark for `key`. Call when a document is freshly
* loaded: its revision counter restarts, and stale watermarks from a
* previous open must not swallow the first real edit.
*/
reset: (key: string) => void;
}
export const createPlanSaveQueue = (): PlanSaveQueue => {
const chains = new Map<string, Promise<void>>();
const lastRevision = new Map<string, number>();
return {
schedule: (key, revision, write) => {
if (revision <= (lastRevision.get(key) ?? Number.NEGATIVE_INFINITY)) {
return chains.get(key) ?? Promise.resolve();
}
lastRevision.set(key, revision);
const previous = chains.get(key) ?? Promise.resolve();
// A failed write must not poison the chain: the next write for this
// document is still safe to attempt, and error surfacing belongs to the
// caller that owns UI state.
const next = previous.then(write, write);
chains.set(key, next.catch(() => {
// Keep newer queued revisions deduplicated, but let the caller retry
// this exact revision after its write has failed.
if (lastRevision.get(key) === revision) {
lastRevision.delete(key);
}
}));
return next;
},
pendingFor: async (key) => {
await chains.get(key);
},
reset: (key) => {
lastRevision.delete(key);
},
};
};