{"version":3,"file":"conversationState.mjs","names":["ConversationMessage","Turn","ConversationItem","id","proposalStatus","ConversationSnapshot","items","TItem","sending","draft","nextConversationId","localId","Conversation","title","turns","trimNoticeShown","nextItemId","outcomes","Map","listeners","Set","snapshot","constructor","subscribe","listener","add","delete","getSnapshot","isSending","isEmpty","length","pushItem","item","Omit","emit","updateItem","patch","Partial","map","it","setSending","setDraft","next","value","appendTurn","turn","recordProposalOutcome","itemId","message","set","setProposalStatus","status","outcomeText","outcome","get","recorded","JSON","parse","content","Record","stringify","clear","forEach","l","ConversationStore","byId","ensure","existing","created","remove","list","values"],"sources":["../../src/aiAssistant/conversationState.ts"],"sourcesContent":["import type { ConversationMessage, Turn } from './conversation';\n\n/**\n * A conversation as an object, independent of any surface. Holds what the\n * user sees (items, in-flight draft, sending flag) and what the model sees\n * (turns), and notifies subscribers on change so React reads it through\n * useSyncExternalStore. Framework-free so the domain harness can test it and\n * so a conversation can outlive the panel showing it, be shown in two places,\n * or sit beside others in a tabbed view.\n */\n\nexport interface ConversationItem {\n  id: number;\n  proposalStatus?: string;\n}\n\nexport interface ConversationSnapshot<TItem extends ConversationItem> {\n  items: readonly TItem[];\n  sending: boolean;\n  draft: string;\n}\n\nlet nextConversationId = 1;\nconst localId = () => `conversation-${nextConversationId++}`;\n\nexport class Conversation<TItem extends ConversationItem = ConversationItem> {\n  readonly id: string;\n  /** Display name for a chat list or tab; empty until a host sets it. */\n  title = '';\n  /** History as the model sees it — whole turns, see conversation.ts. */\n  turns: Turn[] = [];\n  /** The one-time \"older messages are no longer sent\" notice has been shown. */\n  trimNoticeShown = false;\n\n  private items: TItem[] = [];\n  private sending = false;\n  private draft = '';\n  private nextItemId = 1;\n  /** Proposal item id → the tool message recording its outcome in `turns`. */\n  private outcomes = new Map<number, ConversationMessage>();\n  private listeners = new Set<() => void>();\n  private snapshot: ConversationSnapshot<TItem>;\n\n  constructor(id: string = localId()) {\n    this.id = id;\n    this.snapshot = { items: this.items, sending: false, draft: '' };\n  }\n\n  subscribe(listener: () => void): () => void {\n    this.listeners.add(listener);\n    return () => {\n      this.listeners.delete(listener);\n    };\n  }\n\n  /** Stable between changes, new object after each — what useSyncExternalStore wants. */\n  getSnapshot(): ConversationSnapshot<TItem> {\n    return this.snapshot;\n  }\n\n  get isSending(): boolean {\n    return this.sending;\n  }\n\n  get isEmpty(): boolean {\n    return this.items.length === 0;\n  }\n\n  pushItem(item: Omit<TItem, 'id'>): number {\n    const id = this.nextItemId++;\n    this.items = [...this.items, { ...item, id } as TItem];\n    this.emit();\n    return id;\n  }\n\n  updateItem(id: number, patch: Partial<TItem>): void {\n    this.items = this.items.map((it) => (it.id === id ? { ...it, ...patch } : it));\n    this.emit();\n  }\n\n  setSending(sending: boolean): void {\n    if (this.sending === sending) return;\n    this.sending = sending;\n    this.emit();\n  }\n\n  setDraft(next: string | ((draft: string) => string)): void {\n    const value = typeof next === 'function' ? next(this.draft) : next;\n    if (value === this.draft) return;\n    this.draft = value;\n    this.emit();\n  }\n\n  appendTurn(turn: Turn): void {\n    this.turns = [...this.turns, turn];\n  }\n\n  /** Remember which tool message in history records a proposal card's fate. */\n  recordProposalOutcome(itemId: number, message: ConversationMessage): void {\n    this.outcomes.set(itemId, message);\n  }\n\n  /**\n   * Marks a proposal item and rewrites its outcome in history, so the model's\n   * next request says what the user did with the card.\n   */\n  setProposalStatus(itemId: number, status: string, outcomeText: string): void {\n    this.updateItem(itemId, { proposalStatus: status } as Partial<TItem>);\n    const outcome = this.outcomes.get(itemId);\n    if (!outcome) return;\n    const recorded = JSON.parse(outcome.content ?? '{}') as Record<string, unknown>;\n    outcome.content = JSON.stringify({ ...recorded, status: outcomeText });\n  }\n\n  clear(): void {\n    this.items = [];\n    this.turns = [];\n    this.draft = '';\n    this.trimNoticeShown = false;\n    this.outcomes.clear();\n    this.emit();\n  }\n\n  private emit(): void {\n    this.snapshot = { items: this.items, sending: this.sending, draft: this.draft };\n    this.listeners.forEach((l) => l());\n  }\n}\n\n/**\n * The app's conversations, keyed by id. A surface that names a conversation\n * id gets the same Conversation back across remounts; a tabbed view lists\n * them. Subscribable so a chat list re-renders when one is added or removed.\n */\nexport class ConversationStore<TItem extends ConversationItem = ConversationItem> {\n  private byId = new Map<string, Conversation<TItem>>();\n  private listeners = new Set<() => void>();\n  private snapshot: Conversation<TItem>[] = [];\n\n  get(id: string): Conversation<TItem> | undefined {\n    return this.byId.get(id);\n  }\n\n  /** The conversation with this id, created on first use. */\n  ensure(id: string): Conversation<TItem> {\n    const existing = this.byId.get(id);\n    if (existing) return existing;\n    const created = new Conversation<TItem>(id);\n    this.byId.set(id, created);\n    this.emit();\n    return created;\n  }\n\n  remove(id: string): void {\n    if (this.byId.delete(id)) this.emit();\n  }\n\n  /** Every conversation, in creation order. Stable until the set changes. */\n  list(): readonly Conversation<TItem>[] {\n    return this.snapshot;\n  }\n\n  subscribe(listener: () => void): () => void {\n    this.listeners.add(listener);\n    return () => {\n      this.listeners.delete(listener);\n    };\n  }\n\n  private emit(): void {\n    this.snapshot = [...this.byId.values()];\n    this.listeners.forEach((l) => l());\n  }\n}\n"],"mappings":"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"}