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.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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);
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user