repl: persist input draft per-tab — recover on refresh
Adds an inputDraft field to each tab and writes the textarea contents
through saveSoon on every input event (paste, type, autocomplete).
renderAll restores the draft into the textarea when a tab becomes
active, so refresh-after-half-typing or switching between tabs and
back lands the user back on what they were composing.
Plumbing:
* newTab() seeds inputDraft: ""
* setActiveTab() snapshots the outgoing tab's textarea value before
swapping (covers the gap between last input event and next
saveSoon flush)
* renderAll() writes the active tab's draft back into the textarea
(+ autosizeInput so a 40-line recovered draft expands to fit)
* sendInput() clears tab.inputDraft alongside inputEl.value so the
vault next snapshot doesn't preserve an already-committed entry
Smoke-tested: type a 5-line draft, fresh browser context with same
vault password reopens to the same 5 lines + same 82px height. Per-tab
isolation verified — switching tabs preserves each tab's own draft.
Send empties the draft; subsequent reload shows empty input.
This commit is contained in:
parent
78d836fd09
commit
5de290c662
3 changed files with 78 additions and 3 deletions
|
|
@ -127,7 +127,7 @@ function relock() {
|
|||
function newTab(name) {
|
||||
const id = state.nextTabId++;
|
||||
name = name || `session ${id}`;
|
||||
state.tabs.push({ id, name, tier: "c", transcript: [] });
|
||||
state.tabs.push({ id, name, tier: "c", transcript: [], inputDraft: "" });
|
||||
state.activeTabId = id;
|
||||
renderAll();
|
||||
saveSoon();
|
||||
|
|
@ -153,6 +153,12 @@ function closeTab(id) {
|
|||
}
|
||||
|
||||
function setActiveTab(id) {
|
||||
// Capture the outgoing tab's current input as its draft before we
|
||||
// swap tabs — otherwise switching away from a half-written form and
|
||||
// back loses everything between the last input event and the next
|
||||
// saveSoon flush.
|
||||
const out = activeTab();
|
||||
if (out) out.inputDraft = inputEl.value;
|
||||
state.activeTabId = id;
|
||||
renderAll();
|
||||
saveSoon();
|
||||
|
|
@ -439,6 +445,16 @@ function renderAll() {
|
|||
const tab = activeTab();
|
||||
if (!tab) return;
|
||||
tierSelectEl.value = tab.tier;
|
||||
// Restore the active tab's saved draft into the textarea so a page
|
||||
// refresh (or tab switch) lands the user back on the half-written
|
||||
// form they had before. The guard avoids fighting an active input
|
||||
// event mid-stream — if the textarea already matches the draft
|
||||
// there's nothing to do.
|
||||
const draft = tab.inputDraft || "";
|
||||
if (inputEl.value !== draft) {
|
||||
inputEl.value = draft;
|
||||
autosizeInput();
|
||||
}
|
||||
for (const entry of tab.transcript) {
|
||||
const block = document.createElement("div");
|
||||
block.className = "entry" + (entry.kind === "error" ? " error" : "");
|
||||
|
|
@ -589,6 +605,7 @@ async function sendInput() {
|
|||
};
|
||||
tab.transcript.push(entry);
|
||||
inputEl.value = "";
|
||||
tab.inputDraft = "";
|
||||
autosizeInput();
|
||||
resetHistory();
|
||||
sendBtn.disabled = true;
|
||||
|
|
@ -751,6 +768,14 @@ inputEl.addEventListener("keydown", (e) => {
|
|||
});
|
||||
inputEl.addEventListener("input", () => {
|
||||
autosizeInput();
|
||||
// Persist the in-progress input so a refresh, lock/unlock, or tab
|
||||
// switch lands the user back where they were. saveSoon debounces
|
||||
// to 400ms so paste storms don't thrash the vault writer.
|
||||
const tab = activeTab();
|
||||
if (tab) {
|
||||
tab.inputDraft = inputEl.value;
|
||||
saveSoon();
|
||||
}
|
||||
// Any keystroke that isn't an arrow drops history navigation —
|
||||
// edits are now the user's own draft, not the historical entry.
|
||||
if (history.idx !== null && document.activeElement === inputEl) {
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ function relock() {
|
|||
function newTab(name) {
|
||||
const id = state.nextTabId++;
|
||||
name = name || `session ${id}`;
|
||||
state.tabs.push({ id, name, tier: "c", transcript: [] });
|
||||
state.tabs.push({ id, name, tier: "c", transcript: [], inputDraft: "" });
|
||||
state.activeTabId = id;
|
||||
renderAll();
|
||||
saveSoon();
|
||||
|
|
@ -153,6 +153,12 @@ function closeTab(id) {
|
|||
}
|
||||
|
||||
function setActiveTab(id) {
|
||||
// Capture the outgoing tab's current input as its draft before we
|
||||
// swap tabs — otherwise switching away from a half-written form and
|
||||
// back loses everything between the last input event and the next
|
||||
// saveSoon flush.
|
||||
const out = activeTab();
|
||||
if (out) out.inputDraft = inputEl.value;
|
||||
state.activeTabId = id;
|
||||
renderAll();
|
||||
saveSoon();
|
||||
|
|
@ -439,6 +445,16 @@ function renderAll() {
|
|||
const tab = activeTab();
|
||||
if (!tab) return;
|
||||
tierSelectEl.value = tab.tier;
|
||||
// Restore the active tab's saved draft into the textarea so a page
|
||||
// refresh (or tab switch) lands the user back on the half-written
|
||||
// form they had before. The guard avoids fighting an active input
|
||||
// event mid-stream — if the textarea already matches the draft
|
||||
// there's nothing to do.
|
||||
const draft = tab.inputDraft || "";
|
||||
if (inputEl.value !== draft) {
|
||||
inputEl.value = draft;
|
||||
autosizeInput();
|
||||
}
|
||||
for (const entry of tab.transcript) {
|
||||
const block = document.createElement("div");
|
||||
block.className = "entry" + (entry.kind === "error" ? " error" : "");
|
||||
|
|
@ -589,6 +605,7 @@ async function sendInput() {
|
|||
};
|
||||
tab.transcript.push(entry);
|
||||
inputEl.value = "";
|
||||
tab.inputDraft = "";
|
||||
autosizeInput();
|
||||
resetHistory();
|
||||
sendBtn.disabled = true;
|
||||
|
|
@ -751,6 +768,14 @@ inputEl.addEventListener("keydown", (e) => {
|
|||
});
|
||||
inputEl.addEventListener("input", () => {
|
||||
autosizeInput();
|
||||
// Persist the in-progress input so a refresh, lock/unlock, or tab
|
||||
// switch lands the user back where they were. saveSoon debounces
|
||||
// to 400ms so paste storms don't thrash the vault writer.
|
||||
const tab = activeTab();
|
||||
if (tab) {
|
||||
tab.inputDraft = inputEl.value;
|
||||
saveSoon();
|
||||
}
|
||||
// Any keystroke that isn't an arrow drops history navigation —
|
||||
// edits are now the user's own draft, not the historical entry.
|
||||
if (history.idx !== null && document.activeElement === inputEl) {
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ function relock() {
|
|||
function newTab(name) {
|
||||
const id = state.nextTabId++;
|
||||
name = name || `session ${id}`;
|
||||
state.tabs.push({ id, name, tier: "c", transcript: [] });
|
||||
state.tabs.push({ id, name, tier: "c", transcript: [], inputDraft: "" });
|
||||
state.activeTabId = id;
|
||||
renderAll();
|
||||
saveSoon();
|
||||
|
|
@ -153,6 +153,12 @@ function closeTab(id) {
|
|||
}
|
||||
|
||||
function setActiveTab(id) {
|
||||
// Capture the outgoing tab's current input as its draft before we
|
||||
// swap tabs — otherwise switching away from a half-written form and
|
||||
// back loses everything between the last input event and the next
|
||||
// saveSoon flush.
|
||||
const out = activeTab();
|
||||
if (out) out.inputDraft = inputEl.value;
|
||||
state.activeTabId = id;
|
||||
renderAll();
|
||||
saveSoon();
|
||||
|
|
@ -439,6 +445,16 @@ function renderAll() {
|
|||
const tab = activeTab();
|
||||
if (!tab) return;
|
||||
tierSelectEl.value = tab.tier;
|
||||
// Restore the active tab's saved draft into the textarea so a page
|
||||
// refresh (or tab switch) lands the user back on the half-written
|
||||
// form they had before. The guard avoids fighting an active input
|
||||
// event mid-stream — if the textarea already matches the draft
|
||||
// there's nothing to do.
|
||||
const draft = tab.inputDraft || "";
|
||||
if (inputEl.value !== draft) {
|
||||
inputEl.value = draft;
|
||||
autosizeInput();
|
||||
}
|
||||
for (const entry of tab.transcript) {
|
||||
const block = document.createElement("div");
|
||||
block.className = "entry" + (entry.kind === "error" ? " error" : "");
|
||||
|
|
@ -589,6 +605,7 @@ async function sendInput() {
|
|||
};
|
||||
tab.transcript.push(entry);
|
||||
inputEl.value = "";
|
||||
tab.inputDraft = "";
|
||||
autosizeInput();
|
||||
resetHistory();
|
||||
sendBtn.disabled = true;
|
||||
|
|
@ -751,6 +768,14 @@ inputEl.addEventListener("keydown", (e) => {
|
|||
});
|
||||
inputEl.addEventListener("input", () => {
|
||||
autosizeInput();
|
||||
// Persist the in-progress input so a refresh, lock/unlock, or tab
|
||||
// switch lands the user back where they were. saveSoon debounces
|
||||
// to 400ms so paste storms don't thrash the vault writer.
|
||||
const tab = activeTab();
|
||||
if (tab) {
|
||||
tab.inputDraft = inputEl.value;
|
||||
saveSoon();
|
||||
}
|
||||
// Any keystroke that isn't an arrow drops history navigation —
|
||||
// edits are now the user's own draft, not the historical entry.
|
||||
if (history.idx !== null && document.activeElement === inputEl) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue