diff --git a/src/features/assistant/director/useDirectorSession.ts b/src/features/assistant/director/useDirectorSession.ts index 47a401c..039ea73 100644 --- a/src/features/assistant/director/useDirectorSession.ts +++ b/src/features/assistant/director/useDirectorSession.ts @@ -6,13 +6,43 @@ import { getLlmConfig, useAssistantStore } from '@/stores/assistantStore'; import { useDirectorStore } from '@/stores/directorStore'; import { useCharacters } from '@/features/characters/hooks'; import { useEncounters } from '@/features/combat/hooks'; -import { buildDirectorScene, runDirectorTurn, type DirectorAction, type RollRequest } from '@/lib/assistant/director'; +import { + buildDirectorScene, + runDirectorTurn, + planContext, + deterministicSummary, + buildSummaryPrompt, + type DirectorAction, + type RollRequest, +} from '@/lib/assistant/director'; +import { complete } from '@/lib/llm/client'; import type { Degree } from '@/lib/dice/check'; import { useLatestAiSession } from './hooks'; import { useDirectorAction, type ApplyResult } from './useDirectorAction'; const nowIso = (): string => new Date().toISOString(); +/** + * Fold older transcript turns into the session's rolling summary when the live + * window has grown past its budget. Uses the LLM when configured, else a + * deterministic compaction — so the context stays bounded with or without a key. + */ +async function maybeSummarize(sessionId: string, systemConstraint: string, windowSize: number): Promise { + const s = await aiSessionsRepo.get(sessionId); + if (!s) return; + const plan = planContext(s.transcript, s.summarizedThrough, windowSize); + if (plan.foldEntries.length === 0) return; + + const cfg = getLlmConfig(); + let summary = deterministicSummary(s.summary, plan.foldEntries); + if (cfg.enabled && cfg.apiKey.trim()) { + const { system, user } = buildSummaryPrompt(systemConstraint, s.summary, plan.foldEntries); + const res = await complete(cfg, { system, user, maxTokens: 512 }); + if (res.ok && 'text' in res && res.text.trim()) summary = res.text.trim(); + } + await aiSessionsRepo.setSummary(sessionId, summary, plan.newSummarizedThrough); +} + const DEGREE_LABEL: Record = { 'critical-success': 'Critical Success', success: 'Success', @@ -68,7 +98,7 @@ export function useDirectorSession(campaign: Campaign) { } const fresh = await aiSessionsRepo.get(sid); - const transcriptWindow = (fresh?.transcript ?? []).slice(-windowSize); + const plan = planContext(fresh?.transcript ?? [], fresh?.summarizedThrough ?? 0, windowSize); const scene = buildDirectorScene({ campaign, characters, @@ -76,7 +106,7 @@ export function useDirectorSession(campaign: Campaign) { persona, controlledCharacterId, narrationStyle, - transcriptWindow, + transcriptWindow: plan.window, summary: fresh?.summary, }); @@ -94,6 +124,9 @@ export function useDirectorSession(campaign: Campaign) { }]); } setLastTurn({ rollRequests: res.turn.rollRequests, actions: res.turn.actions, suggestions: res.turn.suggestions }); + + // Token budget: fold older turns into a rolling summary so the context stays bounded. + await maybeSummarize(sid, scene.systemConstraint, windowSize); } finally { setBusy(false); } diff --git a/src/lib/assistant/director/index.ts b/src/lib/assistant/director/index.ts index 857abec..8aa9d7c 100644 --- a/src/lib/assistant/director/index.ts +++ b/src/lib/assistant/director/index.ts @@ -6,3 +6,4 @@ export { buildDirectorPrompt, buildDirectorMessages, cueFor } from './prompt'; export { fallbackDirectorTurn } from './fallback'; export { runDirectorTurn, sanitizeTurn, type DirectorTurnResult } from './engine'; export { resolveCombatant, resolveActor, resolveSpellByName, resolveResourceByName, inRoster } from './resolve'; +export { planContext, deterministicSummary, buildSummaryPrompt, SUMMARIZE_BATCH, type ContextPlan } from './summarize'; diff --git a/src/lib/assistant/director/summarize.test.ts b/src/lib/assistant/director/summarize.test.ts new file mode 100644 index 0000000..8030ca2 --- /dev/null +++ b/src/lib/assistant/director/summarize.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from 'vitest'; +import { planContext, deterministicSummary, SUMMARIZE_BATCH } from './summarize'; +import type { DirectorTranscriptEntry } from '@/lib/schemas'; + +const entries = (n: number): DirectorTranscriptEntry[] => + Array.from({ length: n }, (_, i) => ({ id: `e${i}`, role: 'narration' as const, text: `turn ${i}.`, ts: 't' })); + +describe('planContext — token budget windowing', () => { + it('keeps everything in the window until it exceeds windowSize + batch', () => { + const windowSize = 4; + const t = entries(windowSize + SUMMARIZE_BATCH); // exactly at the threshold → no fold + const plan = planContext(t, 0, windowSize); + expect(plan.foldEntries).toHaveLength(0); + expect(plan.window).toHaveLength(windowSize + SUMMARIZE_BATCH); + expect(plan.newSummarizedThrough).toBe(0); + }); + + it('folds the oldest turns down to the last windowSize once over budget', () => { + const windowSize = 4; + const t = entries(windowSize + SUMMARIZE_BATCH + 1); // one over → fold + const plan = planContext(t, 0, windowSize); + expect(plan.window).toHaveLength(windowSize); + expect(plan.foldEntries).toHaveLength(SUMMARIZE_BATCH + 1); + expect(plan.newSummarizedThrough).toBe(SUMMARIZE_BATCH + 1); + // no gap: summary covers [0, newSummarizedThrough), window is the remainder + expect(plan.window[0]!.id).toBe(`e${plan.newSummarizedThrough}`); + }); + + it('accounts for already-summarized entries', () => { + const windowSize = 4; + const t = entries(45); + const plan = planContext(t, 30, windowSize); // 15 unsummarized > 4+8 → fold down to last 4 + expect(plan.foldEntries).toHaveLength(11); + expect(plan.newSummarizedThrough).toBe(41); + expect(plan.window).toHaveLength(4); + }); + + it('does not fold when the unsummarized count is within budget', () => { + const plan = planContext(entries(40), 30, 4); // 10 unsummarized ≤ 4+8 → no fold + expect(plan.foldEntries).toHaveLength(0); + expect(plan.window).toHaveLength(10); + }); +}); + +describe('deterministicSummary', () => { + it('merges the prior summary with one compact line per folded entry', () => { + const folded: DirectorTranscriptEntry[] = [ + { id: '1', role: 'player-input', text: 'I search the chest. It is heavy.', ts: 't' }, + { id: '2', role: 'roll-result', text: 'Investigation: 17 — Success', ts: 't' }, + ]; + const out = deterministicSummary('The party entered the crypt.', folded); + expect(out).toContain('The party entered the crypt.'); + expect(out).toContain('Player: I search the chest.'); + expect(out).toContain('Roll — Investigation: 17'); + }); + + it('caps the summary length', () => { + const big = deterministicSummary('x'.repeat(5000), entries(50)); + expect(big.length).toBeLessThanOrEqual(3000); + }); +}); diff --git a/src/lib/assistant/director/summarize.ts b/src/lib/assistant/director/summarize.ts new file mode 100644 index 0000000..79b0a64 --- /dev/null +++ b/src/lib/assistant/director/summarize.ts @@ -0,0 +1,65 @@ +import type { DirectorTranscriptEntry } from '@/lib/schemas'; + +/** How many turns past the live window must accumulate before we fold them into + * the rolling summary (so we don't fire a summarization call every single turn). */ +export const SUMMARIZE_BATCH = 8; + +export interface ContextPlan { + /** the live transcript turns to send verbatim this turn */ + window: DirectorTranscriptEntry[]; + /** older turns to fold into the summary now (empty most turns) */ + foldEntries: DirectorTranscriptEntry[]; + /** advanced count of entries represented by the summary after folding */ + newSummarizedThrough: number; +} + +/** + * Decide the token-budgeted context. `summary` covers entries [0, summarizedThrough); + * the live window is everything after that. When the window grows past + * windowSize + SUMMARIZE_BATCH, the oldest entries (down to the last `windowSize`) + * are marked to fold into the summary — so there is never a gap between what the + * summary covers and what the window shows. + */ +export function planContext( + transcript: DirectorTranscriptEntry[], + summarizedThrough: number, + windowSize: number, +): ContextPlan { + const unsummarized = transcript.slice(summarizedThrough); + if (unsummarized.length <= windowSize + SUMMARIZE_BATCH) { + return { window: unsummarized, foldEntries: [], newSummarizedThrough: summarizedThrough }; + } + const foldCount = unsummarized.length - windowSize; // keep the most recent windowSize + return { + window: unsummarized.slice(foldCount), + foldEntries: unsummarized.slice(0, foldCount), + newSummarizedThrough: summarizedThrough + foldCount, + }; +} + +const firstSentence = (text: string): string => (text.split(/(?<=[.!?])\s/)[0] ?? text).slice(0, 160); + +/** Offline-safe summary: merge the prior summary with one compact line per folded + * entry, capped so it can't grow unbounded. */ +export function deterministicSummary(prior: string, entries: DirectorTranscriptEntry[]): string { + const lines = entries.map((e) => { + const who = e.speaker ? `${e.speaker}: ` : e.role === 'roll-result' ? 'Roll — ' : e.role === 'player-input' ? 'Player: ' : ''; + return `${who}${firstSentence(e.text)}`; + }); + return [prior, ...lines].filter(Boolean).join(' ').slice(-3000); +} + +/** Prompt to compress folded turns into durable canon (continuing the prior summary). */ +export function buildSummaryPrompt( + systemConstraint: string, + prior: string, + entries: DirectorTranscriptEntry[], +): { system: string; user: string } { + const log = entries.map((e) => `${e.speaker ? `${e.speaker}: ` : ''}${e.text}`).join('\n'); + return { + system: + `${systemConstraint}\n` + + 'You compress a tabletop RPG session into durable canon. Summarize the events below into at most 180 words of plain prose: who is present, where they are, the current goal, key outcomes, and open threads. Keep names exact and invent nothing. Merge with the prior summary into a single continuous summary.', + user: `Prior summary:\n${prior || '(none)'}\n\nNew events:\n${log}\n\nReturn only the updated summary text — no preamble.`, + }; +} diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo index df03b5e..9eb80df 100644 --- a/tsconfig.app.tsbuildinfo +++ b/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/router.tsx","./src/vite-env.d.ts","./src/app/NotFound.tsx","./src/app/RootLayout.tsx","./src/components/CommandPalette.tsx","./src/components/ui/Button.tsx","./src/components/ui/Codex.tsx","./src/components/ui/ErrorBoundary.tsx","./src/components/ui/Input.tsx","./src/components/ui/Modal.tsx","./src/components/ui/NumberField.tsx","./src/components/ui/Page.tsx","./src/components/ui/RollTray.tsx","./src/components/ui/useConfirm.tsx","./src/features/assistant/AssistantPage.tsx","./src/features/assistant/CampaignInsights.tsx","./src/features/assistant/EncounterTipCard.tsx","./src/features/assistant/NpcGenCard.tsx","./src/features/assistant/SessionPrepCard.tsx","./src/features/assistant/SignalsBell.tsx","./src/features/assistant/useEncounterAdvisor.ts","./src/features/assistant/useLevelUpAdvisor.ts","./src/features/assistant/useSignalAction.ts","./src/features/assistant/director/ActionCard.tsx","./src/features/assistant/director/DirectorPage.tsx","./src/features/assistant/director/RollRequestCard.tsx","./src/features/assistant/director/SceneControls.tsx","./src/features/assistant/director/SuggestionChips.tsx","./src/features/assistant/director/TranscriptPane.tsx","./src/features/assistant/director/actionSummary.ts","./src/features/assistant/director/hooks.ts","./src/features/assistant/director/useDirectorAction.test.ts","./src/features/assistant/director/useDirectorAction.ts","./src/features/assistant/director/useDirectorSession.ts","./src/features/campaigns/CampaignsPage.tsx","./src/features/campaigns/hooks.ts","./src/features/characters/CharacterSheet.tsx","./src/features/characters/CharacterSheetPage.tsx","./src/features/characters/CharactersPage.tsx","./src/features/characters/hooks.ts","./src/features/characters/builder/CreationWizard.tsx","./src/features/characters/builder/origin.test.ts","./src/features/characters/builder/origin.ts","./src/features/characters/builder/overview.test.ts","./src/features/characters/builder/overview.ts","./src/features/characters/sheet/AbilityGenModal.tsx","./src/features/characters/sheet/AttacksSection.tsx","./src/features/characters/sheet/ClassFeaturesSection.tsx","./src/features/characters/sheet/ClassesEditor.tsx","./src/features/characters/sheet/DefensesSection.tsx","./src/features/characters/sheet/FeatPickerModal.tsx","./src/features/characters/sheet/FeatsSection.tsx","./src/features/characters/sheet/InventorySection.tsx","./src/features/characters/sheet/LevelUpAdvisor.tsx","./src/features/characters/sheet/LevelUpModal.tsx","./src/features/characters/sheet/ResourcesSection.tsx","./src/features/characters/sheet/SpellcastingSection.tsx","./src/features/characters/sheet/WeaponPickerModal.tsx","./src/features/characters/sheet/common.tsx","./src/features/characters/sheet/labels.ts","./src/features/cloud/SyncStatusIndicator.tsx","./src/features/cloud/useCloudAutosave.ts","./src/features/combat/CombatPage.tsx","./src/features/combat/EncounterTracker.tsx","./src/features/combat/hooks.ts","./src/features/combat/useConditionGlossary.ts","./src/features/compendium/CompendiumPage.tsx","./src/features/compendium/MonsterDetail.tsx","./src/features/compendium/details.tsx","./src/features/compendium/registry.test.ts","./src/features/compendium/registry.tsx","./src/features/dice/DicePage.tsx","./src/features/play/HandoutControl.tsx","./src/features/play/PlayerConnection.tsx","./src/features/play/SessionControl.tsx","./src/features/play/SessionSidebar.tsx","./src/features/play/usePlayerConnection.ts","./src/features/play/useSessionBroadcaster.ts","./src/features/player/ActionGuide.tsx","./src/features/player/MyCharacterPanel.tsx","./src/features/player/PlayerBoards.tsx","./src/features/player/PlayerMapView.tsx","./src/features/player/PlayerSetupPage.tsx","./src/features/player/PlayerViewPage.tsx","./src/features/player/RollFeed.tsx","./src/features/player/SeatClaimScreen.tsx","./src/features/settings/AssistantSettings.tsx","./src/features/settings/CloudCampaigns.tsx","./src/features/settings/DirectorSettings.tsx","./src/features/settings/SettingsPage.tsx","./src/features/world/CalendarPage.tsx","./src/features/world/DashboardPage.tsx","./src/features/world/HomebrewPage.tsx","./src/features/world/MapsPage.tsx","./src/features/world/NotesPage.tsx","./src/features/world/NpcsPage.tsx","./src/features/world/QuestsPage.tsx","./src/features/world/homebrew.ts","./src/features/world/hooks.ts","./src/features/world/map/MapCanvas.tsx","./src/features/world/map/MapEditor.tsx","./src/features/world/map/TokenPalette.test.ts","./src/features/world/map/TokenPalette.tsx","./src/features/world/map/tokens.ts","./src/lib/cn.ts","./src/lib/format.ts","./src/lib/ids.ts","./src/lib/rng.ts","./src/lib/sample.ts","./src/lib/useDebouncedCallback.ts","./src/lib/useRoll.ts","./src/lib/wikilinks.test.ts","./src/lib/wikilinks.ts","./src/lib/assistant/actions.ts","./src/lib/assistant/advisors.test.ts","./src/lib/assistant/advisors.ts","./src/lib/assistant/builder.ts","./src/lib/assistant/context.test.ts","./src/lib/assistant/context.ts","./src/lib/assistant/encounter.ts","./src/lib/assistant/levelup.test.ts","./src/lib/assistant/levelup.ts","./src/lib/assistant/patterns.test.ts","./src/lib/assistant/patterns.ts","./src/lib/assistant/prompts.test.ts","./src/lib/assistant/prompts.ts","./src/lib/assistant/session.ts","./src/lib/assistant/signals.test.ts","./src/lib/assistant/signals.ts","./src/lib/assistant/director/context.ts","./src/lib/assistant/director/director.test.ts","./src/lib/assistant/director/engine.ts","./src/lib/assistant/director/fallback.ts","./src/lib/assistant/director/index.ts","./src/lib/assistant/director/no-auto-roll.test.ts","./src/lib/assistant/director/prompt.ts","./src/lib/assistant/director/resolve.ts","./src/lib/assistant/director/schema.ts","./src/lib/assistant/director/types.ts","./src/lib/cloud/campaigns.ts","./src/lib/cloud/client.ts","./src/lib/combat/budget.test.ts","./src/lib/combat/budget.ts","./src/lib/combat/engine.test.ts","./src/lib/combat/engine.ts","./src/lib/combat/playerProjection.test.ts","./src/lib/combat/playerProjection.ts","./src/lib/compendium/index.ts","./src/lib/compendium/mpmb.test.ts","./src/lib/compendium/mpmb.ts","./src/lib/compendium/types.ts","./src/lib/db/db.ts","./src/lib/db/repositories.test.ts","./src/lib/db/repositories.ts","./src/lib/dice/check.test.ts","./src/lib/dice/check.ts","./src/lib/dice/notation.test.ts","./src/lib/dice/notation.ts","./src/lib/img/resize.test.ts","./src/lib/img/resize.ts","./src/lib/io/backup.test.ts","./src/lib/io/backup.ts","./src/lib/io/character.test.ts","./src/lib/io/character.ts","./src/lib/io/file.ts","./src/lib/io/pathbuilder.test.ts","./src/lib/io/pathbuilder.ts","./src/lib/llm/client.test.ts","./src/lib/llm/client.ts","./src/lib/llm/types.ts","./src/lib/map/distance.ts","./src/lib/map/fog.ts","./src/lib/map/grid.ts","./src/lib/map/index.ts","./src/lib/map/map.test.ts","./src/lib/map/projection.test.ts","./src/lib/map/projection.ts","./src/lib/map/shapes.ts","./src/lib/map/types.ts","./src/lib/map/viewport.test.ts","./src/lib/map/viewport.ts","./src/lib/map/vision.test.ts","./src/lib/map/vision.ts","./src/lib/mechanics/cast.ts","./src/lib/mechanics/conditionEffects.ts","./src/lib/mechanics/creatureState.test.ts","./src/lib/mechanics/creatureState.ts","./src/lib/mechanics/deathSaves.ts","./src/lib/mechanics/enforce.test.ts","./src/lib/mechanics/index.ts","./src/lib/mechanics/resources.ts","./src/lib/mechanics/result.ts","./src/lib/mechanics/types.ts","./src/lib/mechanics/normalize/armor.ts","./src/lib/mechanics/normalize/monsterDefenses.ts","./src/lib/mechanics/normalize/normalize.test.ts","./src/lib/mechanics/normalize/spell.ts","./src/lib/rules/abilities.ts","./src/lib/rules/abilityGen.test.ts","./src/lib/rules/abilityGen.ts","./src/lib/rules/conditions.ts","./src/lib/rules/depth.test.ts","./src/lib/rules/index.ts","./src/lib/rules/multiclass.test.ts","./src/lib/rules/progression.test.ts","./src/lib/rules/progression.ts","./src/lib/rules/rest.ts","./src/lib/rules/rules.test.ts","./src/lib/rules/types.ts","./src/lib/rules/dnd5e/index.ts","./src/lib/rules/dnd5e/progression.ts","./src/lib/rules/dnd5e/skills.ts","./src/lib/rules/features/collect.ts","./src/lib/rules/features/data.5e.ts","./src/lib/rules/features/data.pf2e.ts","./src/lib/rules/features/features.test.ts","./src/lib/rules/features/subclass.5e.ts","./src/lib/rules/features/types.ts","./src/lib/rules/pf2e/abilities.test.ts","./src/lib/rules/pf2e/abilities.ts","./src/lib/rules/pf2e/index.ts","./src/lib/rules/pf2e/progression.ts","./src/lib/rules/pf2e/skills.ts","./src/lib/ruleset/normalize.test.ts","./src/lib/ruleset/normalize.ts","./src/lib/schemas/aiSession.ts","./src/lib/schemas/campaign.ts","./src/lib/schemas/character.ts","./src/lib/schemas/common.ts","./src/lib/schemas/dice.ts","./src/lib/schemas/encounter.ts","./src/lib/schemas/index.ts","./src/lib/schemas/sessionLog.ts","./src/lib/schemas/world.ts","./src/lib/sync/index.ts","./src/lib/sync/messages.test.ts","./src/lib/sync/messages.ts","./src/lib/sync/playerLink.test.ts","./src/lib/sync/playerLink.ts","./src/lib/sync/snapshot.ts","./src/lib/sync/wsSync.ts","./src/lib/vtt/uvtt.test.ts","./src/lib/vtt/uvtt.ts","./src/stores/assistantStore.ts","./src/stores/connectivityStore.ts","./src/stores/directorStore.ts","./src/stores/macroStore.ts","./src/stores/playerSessionStore.ts","./src/stores/rollStore.ts","./src/stores/sessionStore.ts","./src/stores/uiStore.ts","./vitest.setup.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/main.tsx","./src/router.tsx","./src/vite-env.d.ts","./src/app/NotFound.tsx","./src/app/RootLayout.tsx","./src/components/CommandPalette.tsx","./src/components/ui/Button.tsx","./src/components/ui/Codex.tsx","./src/components/ui/ErrorBoundary.tsx","./src/components/ui/Input.tsx","./src/components/ui/Modal.tsx","./src/components/ui/NumberField.tsx","./src/components/ui/Page.tsx","./src/components/ui/RollTray.tsx","./src/components/ui/useConfirm.tsx","./src/features/assistant/AssistantPage.tsx","./src/features/assistant/CampaignInsights.tsx","./src/features/assistant/EncounterTipCard.tsx","./src/features/assistant/NpcGenCard.tsx","./src/features/assistant/SessionPrepCard.tsx","./src/features/assistant/SignalsBell.tsx","./src/features/assistant/useEncounterAdvisor.ts","./src/features/assistant/useLevelUpAdvisor.ts","./src/features/assistant/useSignalAction.ts","./src/features/assistant/director/ActionCard.tsx","./src/features/assistant/director/DirectorPage.tsx","./src/features/assistant/director/RollRequestCard.tsx","./src/features/assistant/director/SceneControls.tsx","./src/features/assistant/director/SuggestionChips.tsx","./src/features/assistant/director/TranscriptPane.tsx","./src/features/assistant/director/actionSummary.ts","./src/features/assistant/director/hooks.ts","./src/features/assistant/director/useDirectorAction.test.ts","./src/features/assistant/director/useDirectorAction.ts","./src/features/assistant/director/useDirectorSession.ts","./src/features/campaigns/CampaignsPage.tsx","./src/features/campaigns/hooks.ts","./src/features/characters/CharacterSheet.tsx","./src/features/characters/CharacterSheetPage.tsx","./src/features/characters/CharactersPage.tsx","./src/features/characters/hooks.ts","./src/features/characters/builder/CreationWizard.tsx","./src/features/characters/builder/origin.test.ts","./src/features/characters/builder/origin.ts","./src/features/characters/builder/overview.test.ts","./src/features/characters/builder/overview.ts","./src/features/characters/sheet/AbilityGenModal.tsx","./src/features/characters/sheet/AttacksSection.tsx","./src/features/characters/sheet/ClassFeaturesSection.tsx","./src/features/characters/sheet/ClassesEditor.tsx","./src/features/characters/sheet/DefensesSection.tsx","./src/features/characters/sheet/FeatPickerModal.tsx","./src/features/characters/sheet/FeatsSection.tsx","./src/features/characters/sheet/InventorySection.tsx","./src/features/characters/sheet/LevelUpAdvisor.tsx","./src/features/characters/sheet/LevelUpModal.tsx","./src/features/characters/sheet/ResourcesSection.tsx","./src/features/characters/sheet/SpellcastingSection.tsx","./src/features/characters/sheet/WeaponPickerModal.tsx","./src/features/characters/sheet/common.tsx","./src/features/characters/sheet/labels.ts","./src/features/cloud/SyncStatusIndicator.tsx","./src/features/cloud/useCloudAutosave.ts","./src/features/combat/CombatPage.tsx","./src/features/combat/EncounterTracker.tsx","./src/features/combat/hooks.ts","./src/features/combat/useConditionGlossary.ts","./src/features/compendium/CompendiumPage.tsx","./src/features/compendium/MonsterDetail.tsx","./src/features/compendium/details.tsx","./src/features/compendium/registry.test.ts","./src/features/compendium/registry.tsx","./src/features/dice/DicePage.tsx","./src/features/play/HandoutControl.tsx","./src/features/play/PlayerConnection.tsx","./src/features/play/SessionControl.tsx","./src/features/play/SessionSidebar.tsx","./src/features/play/usePlayerConnection.ts","./src/features/play/useSessionBroadcaster.ts","./src/features/player/ActionGuide.tsx","./src/features/player/MyCharacterPanel.tsx","./src/features/player/PlayerBoards.tsx","./src/features/player/PlayerMapView.tsx","./src/features/player/PlayerSetupPage.tsx","./src/features/player/PlayerViewPage.tsx","./src/features/player/RollFeed.tsx","./src/features/player/SeatClaimScreen.tsx","./src/features/settings/AssistantSettings.tsx","./src/features/settings/CloudCampaigns.tsx","./src/features/settings/DirectorSettings.tsx","./src/features/settings/SettingsPage.tsx","./src/features/world/CalendarPage.tsx","./src/features/world/DashboardPage.tsx","./src/features/world/HomebrewPage.tsx","./src/features/world/MapsPage.tsx","./src/features/world/NotesPage.tsx","./src/features/world/NpcsPage.tsx","./src/features/world/QuestsPage.tsx","./src/features/world/homebrew.ts","./src/features/world/hooks.ts","./src/features/world/map/MapCanvas.tsx","./src/features/world/map/MapEditor.tsx","./src/features/world/map/TokenPalette.test.ts","./src/features/world/map/TokenPalette.tsx","./src/features/world/map/tokens.ts","./src/lib/cn.ts","./src/lib/format.ts","./src/lib/ids.ts","./src/lib/rng.ts","./src/lib/sample.ts","./src/lib/useDebouncedCallback.ts","./src/lib/useRoll.ts","./src/lib/wikilinks.test.ts","./src/lib/wikilinks.ts","./src/lib/assistant/actions.ts","./src/lib/assistant/advisors.test.ts","./src/lib/assistant/advisors.ts","./src/lib/assistant/builder.ts","./src/lib/assistant/context.test.ts","./src/lib/assistant/context.ts","./src/lib/assistant/encounter.ts","./src/lib/assistant/levelup.test.ts","./src/lib/assistant/levelup.ts","./src/lib/assistant/patterns.test.ts","./src/lib/assistant/patterns.ts","./src/lib/assistant/prompts.test.ts","./src/lib/assistant/prompts.ts","./src/lib/assistant/session.ts","./src/lib/assistant/signals.test.ts","./src/lib/assistant/signals.ts","./src/lib/assistant/director/context.ts","./src/lib/assistant/director/director.test.ts","./src/lib/assistant/director/engine.ts","./src/lib/assistant/director/fallback.ts","./src/lib/assistant/director/index.ts","./src/lib/assistant/director/no-auto-roll.test.ts","./src/lib/assistant/director/prompt.ts","./src/lib/assistant/director/resolve.ts","./src/lib/assistant/director/schema.ts","./src/lib/assistant/director/summarize.test.ts","./src/lib/assistant/director/summarize.ts","./src/lib/assistant/director/types.ts","./src/lib/cloud/campaigns.ts","./src/lib/cloud/client.ts","./src/lib/combat/budget.test.ts","./src/lib/combat/budget.ts","./src/lib/combat/engine.test.ts","./src/lib/combat/engine.ts","./src/lib/combat/playerProjection.test.ts","./src/lib/combat/playerProjection.ts","./src/lib/compendium/index.ts","./src/lib/compendium/mpmb.test.ts","./src/lib/compendium/mpmb.ts","./src/lib/compendium/types.ts","./src/lib/db/db.ts","./src/lib/db/repositories.test.ts","./src/lib/db/repositories.ts","./src/lib/dice/check.test.ts","./src/lib/dice/check.ts","./src/lib/dice/notation.test.ts","./src/lib/dice/notation.ts","./src/lib/img/resize.test.ts","./src/lib/img/resize.ts","./src/lib/io/backup.test.ts","./src/lib/io/backup.ts","./src/lib/io/character.test.ts","./src/lib/io/character.ts","./src/lib/io/file.ts","./src/lib/io/pathbuilder.test.ts","./src/lib/io/pathbuilder.ts","./src/lib/llm/client.test.ts","./src/lib/llm/client.ts","./src/lib/llm/types.ts","./src/lib/map/distance.ts","./src/lib/map/fog.ts","./src/lib/map/grid.ts","./src/lib/map/index.ts","./src/lib/map/map.test.ts","./src/lib/map/projection.test.ts","./src/lib/map/projection.ts","./src/lib/map/shapes.ts","./src/lib/map/types.ts","./src/lib/map/viewport.test.ts","./src/lib/map/viewport.ts","./src/lib/map/vision.test.ts","./src/lib/map/vision.ts","./src/lib/mechanics/cast.ts","./src/lib/mechanics/conditionEffects.ts","./src/lib/mechanics/creatureState.test.ts","./src/lib/mechanics/creatureState.ts","./src/lib/mechanics/deathSaves.ts","./src/lib/mechanics/enforce.test.ts","./src/lib/mechanics/index.ts","./src/lib/mechanics/resources.ts","./src/lib/mechanics/result.ts","./src/lib/mechanics/types.ts","./src/lib/mechanics/normalize/armor.ts","./src/lib/mechanics/normalize/monsterDefenses.ts","./src/lib/mechanics/normalize/normalize.test.ts","./src/lib/mechanics/normalize/spell.ts","./src/lib/rules/abilities.ts","./src/lib/rules/abilityGen.test.ts","./src/lib/rules/abilityGen.ts","./src/lib/rules/conditions.ts","./src/lib/rules/depth.test.ts","./src/lib/rules/index.ts","./src/lib/rules/multiclass.test.ts","./src/lib/rules/progression.test.ts","./src/lib/rules/progression.ts","./src/lib/rules/rest.ts","./src/lib/rules/rules.test.ts","./src/lib/rules/types.ts","./src/lib/rules/dnd5e/index.ts","./src/lib/rules/dnd5e/progression.ts","./src/lib/rules/dnd5e/skills.ts","./src/lib/rules/features/collect.ts","./src/lib/rules/features/data.5e.ts","./src/lib/rules/features/data.pf2e.ts","./src/lib/rules/features/features.test.ts","./src/lib/rules/features/subclass.5e.ts","./src/lib/rules/features/types.ts","./src/lib/rules/pf2e/abilities.test.ts","./src/lib/rules/pf2e/abilities.ts","./src/lib/rules/pf2e/index.ts","./src/lib/rules/pf2e/progression.ts","./src/lib/rules/pf2e/skills.ts","./src/lib/ruleset/normalize.test.ts","./src/lib/ruleset/normalize.ts","./src/lib/schemas/aiSession.ts","./src/lib/schemas/campaign.ts","./src/lib/schemas/character.ts","./src/lib/schemas/common.ts","./src/lib/schemas/dice.ts","./src/lib/schemas/encounter.ts","./src/lib/schemas/index.ts","./src/lib/schemas/sessionLog.ts","./src/lib/schemas/world.ts","./src/lib/sync/index.ts","./src/lib/sync/messages.test.ts","./src/lib/sync/messages.ts","./src/lib/sync/playerLink.test.ts","./src/lib/sync/playerLink.ts","./src/lib/sync/snapshot.ts","./src/lib/sync/wsSync.ts","./src/lib/vtt/uvtt.test.ts","./src/lib/vtt/uvtt.ts","./src/stores/assistantStore.ts","./src/stores/connectivityStore.ts","./src/stores/directorStore.ts","./src/stores/macroStore.ts","./src/stores/playerSessionStore.ts","./src/stores/rollStore.ts","./src/stores/sessionStore.ts","./src/stores/uiStore.ts","./vitest.setup.ts"],"version":"5.9.3"} \ No newline at end of file