Fix remaining code-review findings (15 bugs; #11 skipped as AID-compatible)

Backend:
- provider: fall back to parsing a plain JSON body when a server ignores
  stream=true (was: silent empty turn); error if response has no text (#5)
- memorybank: clamp cursors after undo/retry shrinks the action list, and
  translate summary_cursor (list position) to an Action.index boundary
  before comparing with Memory.source_end (#7)
- memorybank: pinned memories now count toward the top_k budget (#8)
- memorybank: cosine() returns 0.0 on dimension mismatch; changing the
  embedding model clears stored vectors so they re-embed (#9)
- scripting: MAX_STORY_CARDS cap now counts cards inserted during the
  hook, so a script can't add unbounded cards in one turn (#10)
- settings: /test tolerates non-dict JSON from /models (#13)
- scenarios: import accepts worldInformation as a story-card source (#14)

Frontend:
- per-key debounce timers in PlotPanel and ScenarioEditor — editing two
  things within 600ms no longer drops the first save (#15, #16)
- Continue button no longer discards typed input (#17)
- failed retry resyncs actions from the server instead of leaving the
  removed action missing (#18)
- Settings save/test surface errors instead of hanging on Testing… (#19)
- InsightsPanel ignores stale responses from superseded requests (#20)
- placeholder scan includes story-card trigger keys (#21)

addStoryCard returning the 0-based index (falsy for the first card) matches
real AI Dungeon per the scripting guidebook — kept, documented (#11).
Statuses updated in CODE_REVIEW_FINDINGS.md; stale entries for previously
fixed items (#1-4, #6, #12) corrected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFsGHju9szibJJa2YJcdbg
This commit is contained in:
parththakkar106
2026-07-06 17:23:12 +05:30
co-authored by Claude Fable 5
parent 3ee653a631
commit 253b533d3b
11 changed files with 158 additions and 51 deletions
+30 -8
View File
@@ -50,12 +50,17 @@ const SECTION_LABELS = {
}
function PlotPanel({ adventure, setAdventure }) {
const saveTimer = useRef(null)
// One timer per field/card: a single shared timer would cancel the pending
// save of whatever was edited previously within the debounce window.
const saveTimers = useRef(new Map())
const debounceSave = (key, fn) => {
clearTimeout(saveTimers.current.get(key))
saveTimers.current.set(key, setTimeout(fn, 600))
}
const setField = (field, value) => {
setAdventure({ ...adventure, [field]: value })
clearTimeout(saveTimer.current)
saveTimer.current = setTimeout(() => api.updateAdventure(adventure.id, { [field]: value }), 600)
debounceSave(field, () => api.updateAdventure(adventure.id, { [field]: value }))
}
const addCard = async () => {
@@ -68,12 +73,11 @@ function PlotPanel({ adventure, setAdventure }) {
...adventure,
story_cards: adventure.story_cards.map((c) => (c.id === card.id ? card : c)),
})
clearTimeout(saveTimer.current)
saveTimer.current = setTimeout(() => {
debounceSave(`card-${card.id}`, () => {
api.updateStoryCard(card.id, {
name: card.name, type: card.type, keys: card.keys, entry: card.entry, notes: card.notes,
})
}, 600)
})
}
const deleteCard = async (cardId) => {
@@ -324,11 +328,15 @@ function InsightsPanel({ advId, inspectActionId, onClearInspect, refreshKey }) {
const [error, setError] = useState(null)
useEffect(() => {
let stale = false // a slow earlier request must not clobber a newer one
setError(null)
const load = inspectActionId
? api.getActionContext(advId, inspectActionId)
: api.getAdventureContext(advId)
load.then(setReport).catch((err) => { setReport(null); setError(err.message) })
load
.then((r) => { if (!stale) setReport(r) })
.catch((err) => { if (!stale) { setReport(null); setError(err.message) } })
return () => { stale = true }
}, [advId, inspectActionId, refreshKey])
if (error) return <div className="empty">{error}</div>
@@ -502,6 +510,11 @@ export default function Play() {
function send(type = mode) {
const text = input.trim()
if (type === 'continue') {
// Continue never consumes typed text — leave it in the box.
runTurn((signal) => api.sendAction(id, { type: 'continue', text: '' }, handleEvent, signal))
return
}
const payload = { type: text ? type : 'continue', text }
setInput('')
runTurn((signal) => api.sendAction(id, payload, handleEvent, signal))
@@ -510,7 +523,16 @@ export default function Play() {
function retry() {
setActions((prev) =>
prev.length && prev[prev.length - 1].type === 'ai' ? prev.slice(0, -1) : prev)
runTurn((signal) => api.retry(id, handleEvent, signal))
runTurn(async (signal) => {
try {
await api.retry(id, handleEvent, signal)
} catch (err) {
// Failed retry (409, network): the optimistically removed action may
// still exist server-side — resync instead of guessing.
api.getAdventure(id).then((adv) => setActions(adv.actions)).catch(() => {})
throw err
}
})
}
async function undo() {