@lvce-editor/chat-view 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1347,6 +1347,9 @@ const startConversation = () => {
1347
1347
  const composePlaceholder = () => {
1348
1348
  return i18nString('Type your message. Enter to send, Shift+Enter for newline.');
1349
1349
  };
1350
+ const attachImageAsContext = () => {
1351
+ return i18nString('Attach Image as Context');
1352
+ };
1350
1353
  const openRouterApiKeyPlaceholder = () => {
1351
1354
  return i18nString('Enter OpenRouter API key');
1352
1355
  };
@@ -1475,6 +1478,8 @@ const createDefaultState = () => {
1475
1478
  chatMessageFontFamily: 'system-ui',
1476
1479
  chatMessageFontSize,
1477
1480
  chatMessageLineHeight,
1481
+ composerDropActive: false,
1482
+ composerDropEnabled: true,
1478
1483
  composerFontFamily: 'system-ui',
1479
1484
  composerFontSize,
1480
1485
  composerHeight: composerLineHeight + 8,
@@ -1553,579 +1558,379 @@ const create = (uid, x, y, width, height, platform, assetDir) => {
1553
1558
  set(uid, state, state);
1554
1559
  };
1555
1560
 
1556
- const parseRenderHtmlArguments = rawArguments => {
1557
- try {
1558
- const parsed = JSON.parse(rawArguments);
1559
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1560
- return undefined;
1561
- }
1562
- const html = typeof Reflect.get(parsed, 'html') === 'string' ? String(Reflect.get(parsed, 'html')) : '';
1563
- if (!html) {
1564
- return undefined;
1565
- }
1566
- const css = typeof Reflect.get(parsed, 'css') === 'string' ? String(Reflect.get(parsed, 'css')) : '';
1567
- const title = typeof Reflect.get(parsed, 'title') === 'string' ? String(Reflect.get(parsed, 'title')) : 'visual preview';
1568
- return {
1569
- css,
1570
- html,
1571
- title
1572
- };
1573
- } catch {
1574
- return undefined;
1561
+ const toError = error => {
1562
+ if (error instanceof Error) {
1563
+ return error;
1575
1564
  }
1565
+ return new Error('IndexedDB request failed');
1576
1566
  };
1577
1567
 
1578
- const getRenderHtmlCss = (sessions, selectedSessionId) => {
1579
- const selectedSession = sessions.find(session => session.id === selectedSessionId);
1580
- if (!selectedSession) {
1581
- return '';
1582
- }
1583
- const cssRules = new Set();
1584
- for (const message of selectedSession.messages) {
1585
- if (message.role !== 'assistant' || !message.toolCalls) {
1586
- continue;
1587
- }
1588
- for (const toolCall of message.toolCalls) {
1589
- if (toolCall.name !== 'render_html') {
1590
- continue;
1591
- }
1592
- const parsed = parseRenderHtmlArguments(toolCall.arguments);
1593
- if (!parsed || !parsed.css.trim()) {
1594
- continue;
1595
- }
1596
- cssRules.add(parsed.css);
1597
- }
1598
- }
1599
- return [...cssRules].join('\n\n');
1568
+ const requestToPromise = async createRequest => {
1569
+ const request = createRequest();
1570
+ const {
1571
+ promise,
1572
+ reject,
1573
+ resolve
1574
+ } = Promise.withResolvers();
1575
+ request.addEventListener('success', () => {
1576
+ resolve(request.result);
1577
+ });
1578
+ request.addEventListener('error', () => {
1579
+ reject(toError(request.error));
1580
+ });
1581
+ return promise;
1600
1582
  };
1601
1583
 
1602
- const isEqual$1 = (oldState, newState) => {
1603
- const oldRenderHtmlCss = getRenderHtmlCss(oldState.sessions, oldState.selectedSessionId);
1604
- const newRenderHtmlCss = getRenderHtmlCss(newState.sessions, newState.selectedSessionId);
1605
- return oldState.initial === newState.initial && oldState.chatMessageFontFamily === newState.chatMessageFontFamily && oldState.chatMessageFontSize === newState.chatMessageFontSize && oldState.chatMessageLineHeight === newState.chatMessageLineHeight && oldState.composerHeight === newState.composerHeight && oldState.composerLineHeight === newState.composerLineHeight && oldState.composerFontFamily === newState.composerFontFamily && oldState.composerFontSize === newState.composerFontSize && oldState.listItemHeight === newState.listItemHeight && oldRenderHtmlCss === newRenderHtmlCss;
1584
+ const transactionToPromise = async createTransaction => {
1585
+ const transaction = createTransaction();
1586
+ const {
1587
+ promise,
1588
+ reject,
1589
+ resolve
1590
+ } = Promise.withResolvers();
1591
+ transaction.addEventListener('complete', () => {
1592
+ resolve();
1593
+ });
1594
+ transaction.addEventListener('error', () => {
1595
+ reject(toError(transaction.error));
1596
+ });
1597
+ transaction.addEventListener('abort', () => {
1598
+ reject(toError(transaction.error));
1599
+ });
1600
+ return promise;
1606
1601
  };
1607
1602
 
1608
- const diffFocus = (oldState, newState) => {
1609
- if (!newState.focused) {
1610
- return true;
1611
- }
1612
- return oldState.focus === newState.focus && oldState.focused === newState.focused;
1603
+ const toChatViewEvent = event => {
1604
+ const {
1605
+ eventId,
1606
+ ...chatViewEvent
1607
+ } = event;
1608
+ return chatViewEvent;
1613
1609
  };
1614
-
1615
- const isEqual = (oldState, newState) => {
1616
- return oldState.composerValue === newState.composerValue && oldState.initial === newState.initial && oldState.renamingSessionId === newState.renamingSessionId && oldState.selectedModelId === newState.selectedModelId && oldState.selectedSessionId === newState.selectedSessionId && oldState.sessions === newState.sessions && oldState.tokensMax === newState.tokensMax && oldState.tokensUsed === newState.tokensUsed && oldState.usageOverviewEnabled === newState.usageOverviewEnabled && oldState.viewMode === newState.viewMode;
1610
+ const now$1 = () => {
1611
+ return new Date().toISOString();
1617
1612
  };
1618
-
1619
- const diffScrollTop = (oldState, newState) => {
1620
- return oldState.chatListScrollTop === newState.chatListScrollTop && oldState.messagesScrollTop === newState.messagesScrollTop;
1613
+ const isSameMessage$1 = (a, b) => {
1614
+ return a.id === b.id && a.inProgress === b.inProgress && a.role === b.role && a.text === b.text && a.time === b.time && JSON.stringify(a.toolCalls || []) === JSON.stringify(b.toolCalls || []);
1621
1615
  };
1622
-
1623
- const RenderItems = 4;
1624
- const RenderFocus = 6;
1625
- const RenderFocusContext = 7;
1626
- const RenderValue = 8;
1627
- const RenderCss = 10;
1628
- const RenderIncremental = 11;
1629
- const RenderScrollTop = 12;
1630
-
1631
- const diffValue = (oldState, newState) => {
1632
- if (oldState.composerValue === newState.composerValue) {
1633
- return true;
1616
+ const canAppendMessages$1 = (previousMessages, nextMessages) => {
1617
+ if (nextMessages.length < previousMessages.length) {
1618
+ return false;
1634
1619
  }
1635
- return newState.inputSource !== 'script';
1620
+ return previousMessages.every((message, index) => isSameMessage$1(message, nextMessages[index]));
1636
1621
  };
1637
-
1638
- const modules = [isEqual, diffValue, diffFocus, isEqual$1, diffFocus, diffScrollTop];
1639
- const numbers = [RenderIncremental, RenderValue, RenderFocus, RenderCss, RenderFocusContext, RenderScrollTop];
1640
-
1641
- const diff = (oldState, newState) => {
1642
- const diffResult = [];
1643
- for (let i = 0; i < modules.length; i++) {
1644
- const fn = modules[i];
1645
- if (!fn(oldState, newState)) {
1646
- diffResult.push(numbers[i]);
1622
+ const canUpdateMessages$1 = (previousMessages, nextMessages) => {
1623
+ if (previousMessages.length !== nextMessages.length) {
1624
+ return false;
1625
+ }
1626
+ for (let i = 0; i < previousMessages.length; i += 1) {
1627
+ const previous = previousMessages[i];
1628
+ const next = nextMessages[i];
1629
+ if (previous.id !== next.id || previous.role !== next.role) {
1630
+ return false;
1647
1631
  }
1648
1632
  }
1649
- return diffResult;
1633
+ return true;
1650
1634
  };
1651
-
1652
- const diff2 = uid => {
1653
- const {
1654
- newState,
1655
- oldState
1656
- } = get$1(uid);
1657
- const result = diff(oldState, newState);
1658
- return result;
1635
+ const getMutationEvents$1 = (previous, next) => {
1636
+ const timestamp = now$1();
1637
+ const events = [];
1638
+ if (!previous) {
1639
+ events.push({
1640
+ sessionId: next.id,
1641
+ timestamp,
1642
+ title: next.title,
1643
+ type: 'chat-session-created'
1644
+ });
1645
+ for (const message of next.messages) {
1646
+ events.push({
1647
+ message,
1648
+ sessionId: next.id,
1649
+ timestamp,
1650
+ type: 'chat-message-added'
1651
+ });
1652
+ }
1653
+ return events;
1654
+ }
1655
+ if (previous.title !== next.title) {
1656
+ events.push({
1657
+ sessionId: next.id,
1658
+ timestamp,
1659
+ title: next.title,
1660
+ type: 'chat-session-title-updated'
1661
+ });
1662
+ }
1663
+ if (canAppendMessages$1(previous.messages, next.messages)) {
1664
+ for (let i = previous.messages.length; i < next.messages.length; i += 1) {
1665
+ events.push({
1666
+ message: next.messages[i],
1667
+ sessionId: next.id,
1668
+ timestamp,
1669
+ type: 'chat-message-added'
1670
+ });
1671
+ }
1672
+ return events;
1673
+ }
1674
+ if (canUpdateMessages$1(previous.messages, next.messages)) {
1675
+ for (let i = 0; i < previous.messages.length; i += 1) {
1676
+ const previousMessage = previous.messages[i];
1677
+ const nextMessage = next.messages[i];
1678
+ if (!isSameMessage$1(previousMessage, nextMessage)) {
1679
+ events.push({
1680
+ inProgress: nextMessage.inProgress,
1681
+ messageId: nextMessage.id,
1682
+ sessionId: next.id,
1683
+ text: nextMessage.text,
1684
+ time: nextMessage.time,
1685
+ timestamp,
1686
+ toolCalls: nextMessage.toolCalls,
1687
+ type: 'chat-message-updated'
1688
+ });
1689
+ }
1690
+ }
1691
+ return events;
1692
+ }
1693
+ events.push({
1694
+ messages: [...next.messages],
1695
+ sessionId: next.id,
1696
+ timestamp,
1697
+ type: 'chat-session-messages-replaced'
1698
+ });
1699
+ return events;
1659
1700
  };
1660
-
1661
- const Button$2 = 'button';
1662
-
1663
- const Audio = 0;
1664
- const Button$1 = 1;
1665
- const Col = 2;
1666
- const ColGroup = 3;
1667
- const Div = 4;
1668
- const H1 = 5;
1669
- const Input = 6;
1670
- const Span = 8;
1671
- const Table = 9;
1672
- const TBody = 10;
1673
- const Td = 11;
1674
- const Text = 12;
1675
- const Th = 13;
1676
- const THead = 14;
1677
- const Tr = 15;
1678
- const I = 16;
1679
- const Img = 17;
1680
- const H2 = 22;
1681
- const H3 = 23;
1682
- const H4 = 24;
1683
- const H5 = 25;
1684
- const H6 = 26;
1685
- const Article = 27;
1686
- const Aside = 28;
1687
- const Footer = 29;
1688
- const Header = 30;
1689
- const Nav = 40;
1690
- const Section = 41;
1691
- const Dd = 43;
1692
- const Dl = 44;
1693
- const Figcaption = 45;
1694
- const Figure = 46;
1695
- const Hr = 47;
1696
- const Li = 48;
1697
- const Ol = 49;
1698
- const P = 50;
1699
- const Pre = 51;
1700
- const A = 53;
1701
- const Abbr = 54;
1702
- const Br = 55;
1703
- const Tfoot = 59;
1704
- const Ul = 60;
1705
- const TextArea = 62;
1706
- const Select$1 = 63;
1707
- const Option$1 = 64;
1708
- const Code = 65;
1709
- const Label$1 = 66;
1710
- const Dt = 67;
1711
- const Main = 69;
1712
- const Strong = 70;
1713
- const Em = 71;
1714
- const Reference = 100;
1715
-
1716
- const Enter = 3;
1717
-
1718
- const Shift = 1 << 10 >>> 0;
1719
-
1720
- const mergeClassNames = (...classNames) => {
1721
- return classNames.filter(Boolean).join(' ');
1722
- };
1723
-
1724
- const text = data => {
1725
- return {
1726
- childCount: 0,
1727
- text: data,
1728
- type: Text
1729
- };
1730
- };
1731
-
1732
- const SetText = 1;
1733
- const Replace = 2;
1734
- const SetAttribute = 3;
1735
- const RemoveAttribute = 4;
1736
- const Add = 6;
1737
- const NavigateChild = 7;
1738
- const NavigateParent = 8;
1739
- const RemoveChild = 9;
1740
- const NavigateSibling = 10;
1741
- const SetReferenceNodeUid = 11;
1742
-
1743
- const isKey = key => {
1744
- return key !== 'type' && key !== 'childCount';
1745
- };
1746
-
1747
- const getKeys = node => {
1748
- const keys = Object.keys(node).filter(isKey);
1749
- return keys;
1750
- };
1751
-
1752
- const arrayToTree = nodes => {
1753
- const result = [];
1754
- let i = 0;
1755
- while (i < nodes.length) {
1756
- const node = nodes[i];
1757
- const {
1758
- children,
1759
- nodesConsumed
1760
- } = getChildrenWithCount(nodes, i + 1, node.childCount || 0);
1761
- result.push({
1762
- node,
1763
- children
1764
- });
1765
- i += 1 + nodesConsumed;
1766
- }
1767
- return result;
1768
- };
1769
- const getChildrenWithCount = (nodes, startIndex, childCount) => {
1770
- if (childCount === 0) {
1771
- return {
1772
- children: [],
1773
- nodesConsumed: 0
1774
- };
1775
- }
1776
- const children = [];
1777
- let i = startIndex;
1778
- let remaining = childCount;
1779
- let totalConsumed = 0;
1780
- while (remaining > 0 && i < nodes.length) {
1781
- const node = nodes[i];
1782
- const nodeChildCount = node.childCount || 0;
1783
- const {
1784
- children: nodeChildren,
1785
- nodesConsumed
1786
- } = getChildrenWithCount(nodes, i + 1, nodeChildCount);
1787
- children.push({
1788
- node,
1789
- children: nodeChildren
1790
- });
1791
- const nodeSize = 1 + nodesConsumed;
1792
- i += nodeSize;
1793
- totalConsumed += nodeSize;
1794
- remaining--;
1795
- }
1796
- return {
1797
- children,
1798
- nodesConsumed: totalConsumed
1799
- };
1800
- };
1801
-
1802
- const compareNodes = (oldNode, newNode) => {
1803
- const patches = [];
1804
- // Check if node type changed - return null to signal incompatible nodes
1805
- // (caller should handle this with a Replace operation)
1806
- if (oldNode.type !== newNode.type) {
1807
- return null;
1808
- }
1809
- // Handle reference nodes - special handling for uid changes
1810
- if (oldNode.type === Reference) {
1811
- if (oldNode.uid !== newNode.uid) {
1812
- patches.push({
1813
- type: SetReferenceNodeUid,
1814
- uid: newNode.uid
1815
- });
1701
+ const replaySession$1 = (id, summary, events) => {
1702
+ let deleted = false;
1703
+ let title = summary?.title || '';
1704
+ let messages = summary?.messages ? [...summary.messages] : [];
1705
+ for (const event of events) {
1706
+ if (event.sessionId !== id) {
1707
+ continue;
1816
1708
  }
1817
- return patches;
1818
- }
1819
- // Handle text nodes
1820
- if (oldNode.type === Text && newNode.type === Text) {
1821
- if (oldNode.text !== newNode.text) {
1822
- patches.push({
1823
- type: SetText,
1824
- value: newNode.text
1825
- });
1709
+ if (event.type === 'chat-session-created') {
1710
+ const {
1711
+ title: eventTitle
1712
+ } = event;
1713
+ deleted = false;
1714
+ title = eventTitle;
1715
+ continue;
1826
1716
  }
1827
- return patches;
1828
- }
1829
- // Compare attributes
1830
- const oldKeys = getKeys(oldNode);
1831
- const newKeys = getKeys(newNode);
1832
- // Check for attribute changes
1833
- for (const key of newKeys) {
1834
- if (oldNode[key] !== newNode[key]) {
1835
- patches.push({
1836
- type: SetAttribute,
1837
- key,
1838
- value: newNode[key]
1839
- });
1717
+ if (event.type === 'chat-session-deleted') {
1718
+ deleted = true;
1719
+ continue;
1840
1720
  }
1841
- }
1842
- // Check for removed attributes
1843
- for (const key of oldKeys) {
1844
- if (!(key in newNode)) {
1845
- patches.push({
1846
- type: RemoveAttribute,
1847
- key
1721
+ if (event.type === 'chat-session-title-updated') {
1722
+ const {
1723
+ title: eventTitle
1724
+ } = event;
1725
+ title = eventTitle;
1726
+ continue;
1727
+ }
1728
+ if (event.type === 'chat-message-added') {
1729
+ messages = [...messages, event.message];
1730
+ continue;
1731
+ }
1732
+ if (event.type === 'chat-message-updated') {
1733
+ messages = messages.map(message => {
1734
+ if (message.id !== event.messageId) {
1735
+ return message;
1736
+ }
1737
+ return {
1738
+ ...message,
1739
+ ...(event.inProgress === undefined ? {} : {
1740
+ inProgress: event.inProgress
1741
+ }),
1742
+ text: event.text,
1743
+ time: event.time,
1744
+ ...(event.toolCalls === undefined ? {} : {
1745
+ toolCalls: event.toolCalls
1746
+ })
1747
+ };
1848
1748
  });
1749
+ continue;
1750
+ }
1751
+ if (event.type === 'chat-session-messages-replaced') {
1752
+ messages = [...event.messages];
1849
1753
  }
1850
1754
  }
1851
- return patches;
1852
- };
1853
-
1854
- const treeToArray = node => {
1855
- const result = [node.node];
1856
- for (const child of node.children) {
1857
- result.push(...treeToArray(child));
1755
+ if (deleted || !title) {
1756
+ return undefined;
1858
1757
  }
1859
- return result;
1758
+ return {
1759
+ id,
1760
+ messages,
1761
+ title
1762
+ };
1860
1763
  };
1861
-
1862
- const diffChildren = (oldChildren, newChildren, patches) => {
1863
- const maxLength = Math.max(oldChildren.length, newChildren.length);
1864
- // Track where we are: -1 means at parent, >= 0 means at child index
1865
- let currentChildIndex = -1;
1866
- // Collect indices of children to remove (we'll add these patches at the end in reverse order)
1867
- const indicesToRemove = [];
1868
- for (let i = 0; i < maxLength; i++) {
1869
- const oldNode = oldChildren[i];
1870
- const newNode = newChildren[i];
1871
- if (!oldNode && !newNode) {
1872
- continue;
1764
+ class IndexedDbChatSessionStorage {
1765
+ constructor(options = {}) {
1766
+ this.state = {
1767
+ databaseName: options.databaseName || 'lvce-chat-view-sessions',
1768
+ databasePromise: undefined,
1769
+ databaseVersion: options.databaseVersion || 2,
1770
+ eventStoreName: options.eventStoreName || 'chat-view-events',
1771
+ storeName: options.storeName || 'chat-sessions'
1772
+ };
1773
+ }
1774
+ openDatabase = async () => {
1775
+ if (this.state.databasePromise) {
1776
+ return this.state.databasePromise;
1873
1777
  }
1874
- if (!oldNode) {
1875
- // Add new node - we should be at the parent
1876
- if (currentChildIndex >= 0) {
1877
- // Navigate back to parent
1878
- patches.push({
1879
- type: NavigateParent
1778
+ const request = indexedDB.open(this.state.databaseName, this.state.databaseVersion);
1779
+ request.addEventListener('upgradeneeded', () => {
1780
+ const database = request.result;
1781
+ if (!database.objectStoreNames.contains(this.state.storeName)) {
1782
+ database.createObjectStore(this.state.storeName, {
1783
+ keyPath: 'id'
1880
1784
  });
1881
- currentChildIndex = -1;
1882
1785
  }
1883
- // Flatten the entire subtree so renderInternal can handle it
1884
- const flatNodes = treeToArray(newNode);
1885
- patches.push({
1886
- type: Add,
1887
- nodes: flatNodes
1888
- });
1889
- } else if (newNode) {
1890
- // Compare nodes to see if we need any patches
1891
- const nodePatches = compareNodes(oldNode.node, newNode.node);
1892
- // If nodePatches is null, the node types are incompatible - need to replace
1893
- if (nodePatches === null) {
1894
- // Navigate to this child
1895
- if (currentChildIndex === -1) {
1896
- patches.push({
1897
- type: NavigateChild,
1898
- index: i
1899
- });
1900
- currentChildIndex = i;
1901
- } else if (currentChildIndex !== i) {
1902
- patches.push({
1903
- type: NavigateSibling,
1904
- index: i
1786
+ if (database.objectStoreNames.contains(this.state.eventStoreName)) {
1787
+ const {
1788
+ transaction
1789
+ } = request;
1790
+ if (!transaction) {
1791
+ return;
1792
+ }
1793
+ const eventStore = transaction.objectStore(this.state.eventStoreName);
1794
+ if (!eventStore.indexNames.contains('sessionId')) {
1795
+ eventStore.createIndex('sessionId', 'sessionId', {
1796
+ unique: false
1905
1797
  });
1906
- currentChildIndex = i;
1907
1798
  }
1908
- // Replace the entire subtree
1909
- const flatNodes = treeToArray(newNode);
1910
- patches.push({
1911
- type: Replace,
1912
- nodes: flatNodes
1799
+ } else {
1800
+ const eventStore = database.createObjectStore(this.state.eventStoreName, {
1801
+ autoIncrement: true,
1802
+ keyPath: 'eventId'
1803
+ });
1804
+ eventStore.createIndex('sessionId', 'sessionId', {
1805
+ unique: false
1913
1806
  });
1914
- // After replace, we're at the new element (same position)
1915
- continue;
1916
1807
  }
1917
- // Check if we need to recurse into children
1918
- const hasChildrenToCompare = oldNode.children.length > 0 || newNode.children.length > 0;
1919
- // Only navigate to this element if we need to do something
1920
- if (nodePatches.length > 0 || hasChildrenToCompare) {
1921
- // Navigate to this child if not already there
1922
- if (currentChildIndex === -1) {
1923
- patches.push({
1924
- type: NavigateChild,
1925
- index: i
1926
- });
1927
- currentChildIndex = i;
1928
- } else if (currentChildIndex !== i) {
1929
- patches.push({
1930
- type: NavigateSibling,
1931
- index: i
1932
- });
1933
- currentChildIndex = i;
1934
- }
1935
- // Apply node patches (these apply to the current element, not children)
1936
- if (nodePatches.length > 0) {
1937
- patches.push(...nodePatches);
1938
- }
1939
- // Compare children recursively
1940
- if (hasChildrenToCompare) {
1941
- diffChildren(oldNode.children, newNode.children, patches);
1942
- }
1808
+ });
1809
+ const databasePromise = requestToPromise(() => request);
1810
+ this.state.databasePromise = databasePromise;
1811
+ return databasePromise;
1812
+ };
1813
+ listSummaries = async () => {
1814
+ const database = await this.openDatabase();
1815
+ const transaction = database.transaction(this.state.storeName, 'readonly');
1816
+ const store = transaction.objectStore(this.state.storeName);
1817
+ const summaries = await requestToPromise(() => store.getAll());
1818
+ return summaries;
1819
+ };
1820
+ getSummary = async id => {
1821
+ const database = await this.openDatabase();
1822
+ const transaction = database.transaction(this.state.storeName, 'readonly');
1823
+ const store = transaction.objectStore(this.state.storeName);
1824
+ const summary = await requestToPromise(() => store.get(id));
1825
+ return summary;
1826
+ };
1827
+ getEventsBySessionId = async sessionId => {
1828
+ const database = await this.openDatabase();
1829
+ const transaction = database.transaction(this.state.eventStoreName, 'readonly');
1830
+ const store = transaction.objectStore(this.state.eventStoreName);
1831
+ const index = store.index('sessionId');
1832
+ const events = await requestToPromise(() => index.getAll(IDBKeyRange.only(sessionId)));
1833
+ return events.map(toChatViewEvent);
1834
+ };
1835
+ listEventsInternal = async () => {
1836
+ const database = await this.openDatabase();
1837
+ const transaction = database.transaction(this.state.eventStoreName, 'readonly');
1838
+ const store = transaction.objectStore(this.state.eventStoreName);
1839
+ const events = await requestToPromise(() => store.getAll());
1840
+ return events.map(toChatViewEvent);
1841
+ };
1842
+ appendEvents = async events => {
1843
+ if (events.length === 0) {
1844
+ return;
1845
+ }
1846
+ const database = await this.openDatabase();
1847
+ const transaction = database.transaction([this.state.storeName, this.state.eventStoreName], 'readwrite');
1848
+ const summaryStore = transaction.objectStore(this.state.storeName);
1849
+ const eventStore = transaction.objectStore(this.state.eventStoreName);
1850
+ for (const event of events) {
1851
+ eventStore.add(event);
1852
+ if (event.type === 'chat-session-created' || event.type === 'chat-session-title-updated') {
1853
+ summaryStore.put({
1854
+ id: event.sessionId,
1855
+ title: event.title
1856
+ });
1857
+ }
1858
+ if (event.type === 'chat-session-deleted') {
1859
+ summaryStore.delete(event.sessionId);
1943
1860
  }
1944
- } else {
1945
- // Remove old node - collect the index for later removal
1946
- indicesToRemove.push(i);
1947
1861
  }
1862
+ await transactionToPromise(() => transaction);
1863
+ };
1864
+ async appendEvent(event) {
1865
+ await this.appendEvents([event]);
1948
1866
  }
1949
- // Navigate back to parent if we ended at a child
1950
- if (currentChildIndex >= 0) {
1951
- patches.push({
1952
- type: NavigateParent
1953
- });
1954
- currentChildIndex = -1;
1867
+ async clear() {
1868
+ const database = await this.openDatabase();
1869
+ const transaction = database.transaction([this.state.storeName, this.state.eventStoreName], 'readwrite');
1870
+ transaction.objectStore(this.state.storeName).clear();
1871
+ transaction.objectStore(this.state.eventStoreName).clear();
1872
+ await transactionToPromise(() => transaction);
1955
1873
  }
1956
- // Add remove patches in reverse order (highest index first)
1957
- // This ensures indices remain valid as we remove
1958
- for (let j = indicesToRemove.length - 1; j >= 0; j--) {
1959
- patches.push({
1960
- type: RemoveChild,
1961
- index: indicesToRemove[j]
1874
+ async deleteSession(id) {
1875
+ await this.appendEvent({
1876
+ sessionId: id,
1877
+ timestamp: now$1(),
1878
+ type: 'chat-session-deleted'
1962
1879
  });
1963
1880
  }
1964
- };
1965
- const diffTrees = (oldTree, newTree, patches, path) => {
1966
- // At the root level (path.length === 0), we're already AT the element
1967
- // So we compare the root node directly, then compare its children
1968
- if (path.length === 0 && oldTree.length === 1 && newTree.length === 1) {
1969
- const oldNode = oldTree[0];
1970
- const newNode = newTree[0];
1971
- // Compare root nodes
1972
- const nodePatches = compareNodes(oldNode.node, newNode.node);
1973
- // If nodePatches is null, the root node types are incompatible - need to replace
1974
- if (nodePatches === null) {
1975
- const flatNodes = treeToArray(newNode);
1976
- patches.push({
1977
- type: Replace,
1978
- nodes: flatNodes
1979
- });
1980
- return;
1981
- }
1982
- if (nodePatches.length > 0) {
1983
- patches.push(...nodePatches);
1881
+ async getEvents(sessionId) {
1882
+ if (sessionId) {
1883
+ return this.getEventsBySessionId(sessionId);
1984
1884
  }
1985
- // Compare children
1986
- if (oldNode.children.length > 0 || newNode.children.length > 0) {
1987
- diffChildren(oldNode.children, newNode.children, patches);
1885
+ return this.listEventsInternal();
1886
+ }
1887
+ async getSession(id) {
1888
+ const [summary, events] = await Promise.all([this.getSummary(id), this.getEventsBySessionId(id)]);
1889
+ return replaySession$1(id, summary, events);
1890
+ }
1891
+ async listSessions() {
1892
+ const summaries = await this.listSummaries();
1893
+ const sessions = [];
1894
+ for (const summary of summaries) {
1895
+ const events = await this.getEventsBySessionId(summary.id);
1896
+ const session = replaySession$1(summary.id, summary, events);
1897
+ if (!session) {
1898
+ continue;
1899
+ }
1900
+ sessions.push(session);
1988
1901
  }
1989
- } else {
1990
- // Non-root level or multiple root elements - use the regular comparison
1991
- diffChildren(oldTree, newTree, patches);
1902
+ return sessions;
1992
1903
  }
1993
- };
1994
-
1995
- const removeTrailingNavigationPatches = patches => {
1996
- // Find the last non-navigation patch
1997
- let lastNonNavigationIndex = -1;
1998
- for (let i = patches.length - 1; i >= 0; i--) {
1999
- const patch = patches[i];
2000
- if (patch.type !== NavigateChild && patch.type !== NavigateParent && patch.type !== NavigateSibling) {
2001
- lastNonNavigationIndex = i;
2002
- break;
1904
+ async setSession(session) {
1905
+ const previous = await this.getSession(session.id);
1906
+ const events = getMutationEvents$1(previous, session);
1907
+ await this.appendEvents(events);
1908
+ if (events.length === 0) {
1909
+ const database = await this.openDatabase();
1910
+ const transaction = database.transaction(this.state.storeName, 'readwrite');
1911
+ const summaryStore = transaction.objectStore(this.state.storeName);
1912
+ summaryStore.put({
1913
+ id: session.id,
1914
+ title: session.title
1915
+ });
1916
+ await transactionToPromise(() => transaction);
2003
1917
  }
2004
1918
  }
2005
- // Return patches up to and including the last non-navigation patch
2006
- return lastNonNavigationIndex === -1 ? [] : patches.slice(0, lastNonNavigationIndex + 1);
2007
- };
1919
+ }
2008
1920
 
2009
- const diffTree = (oldNodes, newNodes) => {
2010
- // Step 1: Convert flat arrays to tree structures
2011
- const oldTree = arrayToTree(oldNodes);
2012
- const newTree = arrayToTree(newNodes);
2013
- // Step 3: Compare the trees
2014
- const patches = [];
2015
- diffTrees(oldTree, newTree, patches, []);
2016
- // Remove trailing navigation patches since they serve no purpose
2017
- return removeTrailingNavigationPatches(patches);
1921
+ const now = () => {
1922
+ return new Date().toISOString();
2018
1923
  };
2019
-
2020
- const getKeyBindings = () => {
2021
- return [{
2022
- command: 'Chat.handleSubmit',
2023
- key: Enter,
2024
- when: FocusChatInput
2025
- }, {
2026
- command: 'Chat.enterNewLine',
2027
- key: Shift | Enter,
2028
- when: FocusChatInput
2029
- }];
1924
+ const isSameMessage = (a, b) => {
1925
+ return a.id === b.id && a.inProgress === b.inProgress && a.role === b.role && a.text === b.text && a.time === b.time && JSON.stringify(a.toolCalls || []) === JSON.stringify(b.toolCalls || []);
2030
1926
  };
2031
-
2032
- const getSelectedSessionId = state => {
2033
- return state.selectedSessionId;
1927
+ const canAppendMessages = (previousMessages, nextMessages) => {
1928
+ if (nextMessages.length < previousMessages.length) {
1929
+ return false;
1930
+ }
1931
+ return previousMessages.every((message, index) => isSameMessage(message, nextMessages[index]));
2034
1932
  };
2035
-
2036
- const getListIndex = (state, eventX, eventY) => {
2037
- const {
2038
- headerHeight,
2039
- height,
2040
- listItemHeight,
2041
- width,
2042
- x,
2043
- y
2044
- } = state;
2045
- const relativeX = eventX - x;
2046
- const relativeY = eventY - y - headerHeight;
2047
- if (relativeX < 0 || relativeY < 0 || relativeX >= width || relativeY >= height - headerHeight) {
2048
- return -1;
2049
- }
2050
- return Math.floor(relativeY / listItemHeight);
2051
- };
2052
-
2053
- const CHAT_LIST_ITEM_CONTEXT_MENU = 'ChatListItemContextMenu';
2054
- const handleChatListContextMenu = async (state, eventX, eventY) => {
2055
- const index = getListIndex(state, eventX, eventY);
2056
- if (index === -1) {
2057
- return state;
2058
- }
2059
- const item = state.sessions[index];
2060
- if (!item) {
2061
- return state;
2062
- }
2063
- await invoke('ContextMenu.show', eventX, eventY, CHAT_LIST_ITEM_CONTEXT_MENU, item.id);
2064
- return state;
2065
- };
2066
-
2067
- const toError = error => {
2068
- if (error instanceof Error) {
2069
- return error;
2070
- }
2071
- return new Error('IndexedDB request failed');
2072
- };
2073
-
2074
- const requestToPromise = async createRequest => {
2075
- const request = createRequest();
2076
- const {
2077
- promise,
2078
- reject,
2079
- resolve
2080
- } = Promise.withResolvers();
2081
- request.addEventListener('success', () => {
2082
- resolve(request.result);
2083
- });
2084
- request.addEventListener('error', () => {
2085
- reject(toError(request.error));
2086
- });
2087
- return promise;
2088
- };
2089
-
2090
- const transactionToPromise = async createTransaction => {
2091
- const transaction = createTransaction();
2092
- const {
2093
- promise,
2094
- reject,
2095
- resolve
2096
- } = Promise.withResolvers();
2097
- transaction.addEventListener('complete', () => {
2098
- resolve();
2099
- });
2100
- transaction.addEventListener('error', () => {
2101
- reject(toError(transaction.error));
2102
- });
2103
- transaction.addEventListener('abort', () => {
2104
- reject(toError(transaction.error));
2105
- });
2106
- return promise;
2107
- };
2108
-
2109
- const toChatViewEvent = event => {
2110
- const {
2111
- eventId,
2112
- ...chatViewEvent
2113
- } = event;
2114
- return chatViewEvent;
2115
- };
2116
- const now$1 = () => {
2117
- return new Date().toISOString();
2118
- };
2119
- const isSameMessage$1 = (a, b) => {
2120
- return a.id === b.id && a.inProgress === b.inProgress && a.role === b.role && a.text === b.text && a.time === b.time && JSON.stringify(a.toolCalls || []) === JSON.stringify(b.toolCalls || []);
2121
- };
2122
- const canAppendMessages$1 = (previousMessages, nextMessages) => {
2123
- if (nextMessages.length < previousMessages.length) {
2124
- return false;
2125
- }
2126
- return previousMessages.every((message, index) => isSameMessage$1(message, nextMessages[index]));
2127
- };
2128
- const canUpdateMessages$1 = (previousMessages, nextMessages) => {
1933
+ const canUpdateMessages = (previousMessages, nextMessages) => {
2129
1934
  if (previousMessages.length !== nextMessages.length) {
2130
1935
  return false;
2131
1936
  }
@@ -2138,8 +1943,8 @@ const canUpdateMessages$1 = (previousMessages, nextMessages) => {
2138
1943
  }
2139
1944
  return true;
2140
1945
  };
2141
- const getMutationEvents$1 = (previous, next) => {
2142
- const timestamp = now$1();
1946
+ const getMutationEvents = (previous, next) => {
1947
+ const timestamp = now();
2143
1948
  const events = [];
2144
1949
  if (!previous) {
2145
1950
  events.push({
@@ -2166,7 +1971,7 @@ const getMutationEvents$1 = (previous, next) => {
2166
1971
  type: 'chat-session-title-updated'
2167
1972
  });
2168
1973
  }
2169
- if (canAppendMessages$1(previous.messages, next.messages)) {
1974
+ if (canAppendMessages(previous.messages, next.messages)) {
2170
1975
  for (let i = previous.messages.length; i < next.messages.length; i += 1) {
2171
1976
  events.push({
2172
1977
  message: next.messages[i],
@@ -2177,11 +1982,11 @@ const getMutationEvents$1 = (previous, next) => {
2177
1982
  }
2178
1983
  return events;
2179
1984
  }
2180
- if (canUpdateMessages$1(previous.messages, next.messages)) {
1985
+ if (canUpdateMessages(previous.messages, next.messages)) {
2181
1986
  for (let i = 0; i < previous.messages.length; i += 1) {
2182
1987
  const previousMessage = previous.messages[i];
2183
1988
  const nextMessage = next.messages[i];
2184
- if (!isSameMessage$1(previousMessage, nextMessage)) {
1989
+ if (!isSameMessage(previousMessage, nextMessage)) {
2185
1990
  events.push({
2186
1991
  inProgress: nextMessage.inProgress,
2187
1992
  messageId: nextMessage.id,
@@ -2204,20 +2009,17 @@ const getMutationEvents$1 = (previous, next) => {
2204
2009
  });
2205
2010
  return events;
2206
2011
  };
2207
- const replaySession$1 = (id, summary, events) => {
2012
+ const replaySession = (id, title, events) => {
2208
2013
  let deleted = false;
2209
- let title = summary?.title || '';
2210
- let messages = summary?.messages ? [...summary.messages] : [];
2014
+ let currentTitle = title || '';
2015
+ let messages = [];
2211
2016
  for (const event of events) {
2212
2017
  if (event.sessionId !== id) {
2213
2018
  continue;
2214
2019
  }
2215
2020
  if (event.type === 'chat-session-created') {
2216
- const {
2217
- title: eventTitle
2218
- } = event;
2219
2021
  deleted = false;
2220
- title = eventTitle;
2022
+ currentTitle = event.title;
2221
2023
  continue;
2222
2024
  }
2223
2025
  if (event.type === 'chat-session-deleted') {
@@ -2225,10 +2027,7 @@ const replaySession$1 = (id, summary, events) => {
2225
2027
  continue;
2226
2028
  }
2227
2029
  if (event.type === 'chat-session-title-updated') {
2228
- const {
2229
- title: eventTitle
2230
- } = event;
2231
- title = eventTitle;
2030
+ currentTitle = event.title;
2232
2031
  continue;
2233
2032
  }
2234
2033
  if (event.type === 'chat-message-added') {
@@ -2258,148 +2057,59 @@ const replaySession$1 = (id, summary, events) => {
2258
2057
  messages = [...event.messages];
2259
2058
  }
2260
2059
  }
2261
- if (deleted || !title) {
2060
+ if (deleted || !currentTitle) {
2262
2061
  return undefined;
2263
2062
  }
2264
2063
  return {
2265
2064
  id,
2266
2065
  messages,
2267
- title
2066
+ title: currentTitle
2268
2067
  };
2269
2068
  };
2270
- class IndexedDbChatSessionStorage {
2271
- constructor(options = {}) {
2272
- this.state = {
2273
- databaseName: options.databaseName || 'lvce-chat-view-sessions',
2274
- databasePromise: undefined,
2275
- databaseVersion: options.databaseVersion || 2,
2276
- eventStoreName: options.eventStoreName || 'chat-view-events',
2277
- storeName: options.storeName || 'chat-sessions'
2278
- };
2279
- }
2280
- openDatabase = async () => {
2281
- if (this.state.databasePromise) {
2282
- return this.state.databasePromise;
2283
- }
2284
- const request = indexedDB.open(this.state.databaseName, this.state.databaseVersion);
2285
- request.addEventListener('upgradeneeded', () => {
2286
- const database = request.result;
2287
- if (!database.objectStoreNames.contains(this.state.storeName)) {
2288
- database.createObjectStore(this.state.storeName, {
2289
- keyPath: 'id'
2290
- });
2291
- }
2292
- if (database.objectStoreNames.contains(this.state.eventStoreName)) {
2293
- const {
2294
- transaction
2295
- } = request;
2296
- if (!transaction) {
2297
- return;
2298
- }
2299
- const eventStore = transaction.objectStore(this.state.eventStoreName);
2300
- if (!eventStore.indexNames.contains('sessionId')) {
2301
- eventStore.createIndex('sessionId', 'sessionId', {
2302
- unique: false
2303
- });
2304
- }
2305
- } else {
2306
- const eventStore = database.createObjectStore(this.state.eventStoreName, {
2307
- autoIncrement: true,
2308
- keyPath: 'eventId'
2309
- });
2310
- eventStore.createIndex('sessionId', 'sessionId', {
2311
- unique: false
2312
- });
2313
- }
2314
- });
2315
- const databasePromise = requestToPromise(() => request);
2316
- this.state.databasePromise = databasePromise;
2317
- return databasePromise;
2318
- };
2319
- listSummaries = async () => {
2320
- const database = await this.openDatabase();
2321
- const transaction = database.transaction(this.state.storeName, 'readonly');
2322
- const store = transaction.objectStore(this.state.storeName);
2323
- const summaries = await requestToPromise(() => store.getAll());
2324
- return summaries;
2325
- };
2326
- getSummary = async id => {
2327
- const database = await this.openDatabase();
2328
- const transaction = database.transaction(this.state.storeName, 'readonly');
2329
- const store = transaction.objectStore(this.state.storeName);
2330
- const summary = await requestToPromise(() => store.get(id));
2331
- return summary;
2332
- };
2333
- getEventsBySessionId = async sessionId => {
2334
- const database = await this.openDatabase();
2335
- const transaction = database.transaction(this.state.eventStoreName, 'readonly');
2336
- const store = transaction.objectStore(this.state.eventStoreName);
2337
- const index = store.index('sessionId');
2338
- const events = await requestToPromise(() => index.getAll(IDBKeyRange.only(sessionId)));
2339
- return events.map(toChatViewEvent);
2340
- };
2341
- listEventsInternal = async () => {
2342
- const database = await this.openDatabase();
2343
- const transaction = database.transaction(this.state.eventStoreName, 'readonly');
2344
- const store = transaction.objectStore(this.state.eventStoreName);
2345
- const events = await requestToPromise(() => store.getAll());
2346
- return events.map(toChatViewEvent);
2347
- };
2348
- appendEvents = async events => {
2349
- if (events.length === 0) {
2069
+ class InMemoryChatSessionStorage {
2070
+ events = [];
2071
+ summaries = new Map();
2072
+ async appendEvent(event) {
2073
+ this.events.push(event);
2074
+ if (event.type === 'chat-session-created' || event.type === 'chat-session-title-updated') {
2075
+ this.summaries.set(event.sessionId, event.title);
2350
2076
  return;
2351
2077
  }
2352
- const database = await this.openDatabase();
2353
- const transaction = database.transaction([this.state.storeName, this.state.eventStoreName], 'readwrite');
2354
- const summaryStore = transaction.objectStore(this.state.storeName);
2355
- const eventStore = transaction.objectStore(this.state.eventStoreName);
2356
- for (const event of events) {
2357
- eventStore.add(event);
2358
- if (event.type === 'chat-session-created' || event.type === 'chat-session-title-updated') {
2359
- summaryStore.put({
2360
- id: event.sessionId,
2361
- title: event.title
2362
- });
2363
- }
2364
- if (event.type === 'chat-session-deleted') {
2365
- summaryStore.delete(event.sessionId);
2366
- }
2078
+ if (event.type === 'chat-session-deleted') {
2079
+ this.summaries.delete(event.sessionId);
2367
2080
  }
2368
- await transactionToPromise(() => transaction);
2369
- };
2370
- async appendEvent(event) {
2371
- await this.appendEvents([event]);
2372
2081
  }
2373
2082
  async clear() {
2374
- const database = await this.openDatabase();
2375
- const transaction = database.transaction([this.state.storeName, this.state.eventStoreName], 'readwrite');
2376
- transaction.objectStore(this.state.storeName).clear();
2377
- transaction.objectStore(this.state.eventStoreName).clear();
2378
- await transactionToPromise(() => transaction);
2083
+ this.events.length = 0;
2084
+ this.summaries.clear();
2379
2085
  }
2380
2086
  async deleteSession(id) {
2381
2087
  await this.appendEvent({
2382
2088
  sessionId: id,
2383
- timestamp: now$1(),
2089
+ timestamp: now(),
2384
2090
  type: 'chat-session-deleted'
2385
2091
  });
2386
2092
  }
2387
2093
  async getEvents(sessionId) {
2388
- if (sessionId) {
2389
- return this.getEventsBySessionId(sessionId);
2094
+ if (!sessionId) {
2095
+ return [...this.events];
2390
2096
  }
2391
- return this.listEventsInternal();
2097
+ return this.events.filter(event => event.sessionId === sessionId);
2392
2098
  }
2393
2099
  async getSession(id) {
2394
- const [summary, events] = await Promise.all([this.getSummary(id), this.getEventsBySessionId(id)]);
2395
- return replaySession$1(id, summary, events);
2100
+ return replaySession(id, this.summaries.get(id), this.events);
2396
2101
  }
2397
2102
  async listSessions() {
2398
- const summaries = await this.listSummaries();
2103
+ const ids = new Set();
2104
+ for (const id of this.summaries.keys()) {
2105
+ ids.add(id);
2106
+ }
2107
+ for (const event of this.events) {
2108
+ ids.add(event.sessionId);
2109
+ }
2399
2110
  const sessions = [];
2400
- for (const summary of summaries) {
2401
- const events = await this.getEventsBySessionId(summary.id);
2402
- const session = replaySession$1(summary.id, summary, events);
2111
+ for (const id of ids) {
2112
+ const session = replaySession(id, this.summaries.get(id), this.events);
2403
2113
  if (!session) {
2404
2114
  continue;
2405
2115
  }
@@ -2409,271 +2119,626 @@ class IndexedDbChatSessionStorage {
2409
2119
  }
2410
2120
  async setSession(session) {
2411
2121
  const previous = await this.getSession(session.id);
2412
- const events = getMutationEvents$1(previous, session);
2413
- await this.appendEvents(events);
2414
- if (events.length === 0) {
2415
- const database = await this.openDatabase();
2416
- const transaction = database.transaction(this.state.storeName, 'readwrite');
2417
- const summaryStore = transaction.objectStore(this.state.storeName);
2418
- summaryStore.put({
2419
- id: session.id,
2420
- title: session.title
2421
- });
2422
- await transactionToPromise(() => transaction);
2122
+ const events = getMutationEvents(previous, session);
2123
+ for (const event of events) {
2124
+ await this.appendEvent(event);
2423
2125
  }
2126
+ this.summaries.set(session.id, session.title);
2424
2127
  }
2425
2128
  }
2426
2129
 
2427
- const now = () => {
2428
- return new Date().toISOString();
2130
+ const createDefaultStorage = () => {
2131
+ if (typeof indexedDB === 'undefined') {
2132
+ return new InMemoryChatSessionStorage();
2133
+ }
2134
+ return new IndexedDbChatSessionStorage();
2429
2135
  };
2430
- const isSameMessage = (a, b) => {
2431
- return a.id === b.id && a.inProgress === b.inProgress && a.role === b.role && a.text === b.text && a.time === b.time && JSON.stringify(a.toolCalls || []) === JSON.stringify(b.toolCalls || []);
2136
+ let chatSessionStorage = createDefaultStorage();
2137
+ const listChatSessions = async () => {
2138
+ const sessions = await chatSessionStorage.listSessions();
2139
+ return sessions.map(session => ({
2140
+ id: session.id,
2141
+ messages: [],
2142
+ title: session.title
2143
+ }));
2432
2144
  };
2433
- const canAppendMessages = (previousMessages, nextMessages) => {
2434
- if (nextMessages.length < previousMessages.length) {
2435
- return false;
2145
+ const getChatSession = async id => {
2146
+ const session = await chatSessionStorage.getSession(id);
2147
+ if (!session) {
2148
+ return undefined;
2436
2149
  }
2437
- return previousMessages.every((message, index) => isSameMessage(message, nextMessages[index]));
2150
+ return {
2151
+ id: session.id,
2152
+ messages: [...session.messages],
2153
+ title: session.title
2154
+ };
2438
2155
  };
2439
- const canUpdateMessages = (previousMessages, nextMessages) => {
2440
- if (previousMessages.length !== nextMessages.length) {
2441
- return false;
2156
+ const saveChatSession = async session => {
2157
+ await chatSessionStorage.setSession({
2158
+ id: session.id,
2159
+ messages: [...session.messages],
2160
+ title: session.title
2161
+ });
2162
+ };
2163
+ const deleteChatSession = async id => {
2164
+ await chatSessionStorage.deleteSession(id);
2165
+ };
2166
+ const clearChatSessions = async () => {
2167
+ await chatSessionStorage.clear();
2168
+ };
2169
+ const appendChatViewEvent = async event => {
2170
+ await chatSessionStorage.appendEvent(event);
2171
+ };
2172
+
2173
+ const getNextSelectedSessionId = (sessions, deletedId) => {
2174
+ if (sessions.length === 0) {
2175
+ return '';
2442
2176
  }
2443
- for (let i = 0; i < previousMessages.length; i += 1) {
2444
- const previous = previousMessages[i];
2445
- const next = nextMessages[i];
2446
- if (previous.id !== next.id || previous.role !== next.role) {
2447
- return false;
2448
- }
2177
+ const index = sessions.findIndex(session => session.id === deletedId);
2178
+ if (index === -1) {
2179
+ return sessions[0].id;
2449
2180
  }
2450
- return true;
2181
+ const nextIndex = Math.min(index, sessions.length - 1);
2182
+ return sessions[nextIndex].id;
2451
2183
  };
2452
- const getMutationEvents = (previous, next) => {
2453
- const timestamp = now();
2454
- const events = [];
2455
- if (!previous) {
2456
- events.push({
2457
- sessionId: next.id,
2458
- timestamp,
2459
- title: next.title,
2460
- type: 'chat-session-created'
2461
- });
2462
- for (const message of next.messages) {
2463
- events.push({
2464
- message,
2465
- sessionId: next.id,
2466
- timestamp,
2467
- type: 'chat-message-added'
2468
- });
2469
- }
2470
- return events;
2184
+
2185
+ const deleteSession = async (state, id) => {
2186
+ const {
2187
+ renamingSessionId,
2188
+ sessions
2189
+ } = state;
2190
+ const filtered = sessions.filter(session => session.id !== id);
2191
+ if (filtered.length === sessions.length) {
2192
+ return state;
2471
2193
  }
2472
- if (previous.title !== next.title) {
2473
- events.push({
2474
- sessionId: next.id,
2475
- timestamp,
2476
- title: next.title,
2477
- type: 'chat-session-title-updated'
2478
- });
2194
+ await deleteChatSession(id);
2195
+ if (filtered.length === 0) {
2196
+ return {
2197
+ ...state,
2198
+ renamingSessionId: '',
2199
+ selectedSessionId: '',
2200
+ sessions: [],
2201
+ viewMode: 'list'
2202
+ };
2479
2203
  }
2480
- if (canAppendMessages(previous.messages, next.messages)) {
2481
- for (let i = previous.messages.length; i < next.messages.length; i += 1) {
2482
- events.push({
2483
- message: next.messages[i],
2484
- sessionId: next.id,
2485
- timestamp,
2486
- type: 'chat-message-added'
2487
- });
2204
+ const nextSelectedSessionId = getNextSelectedSessionId(filtered, id);
2205
+ const loadedSession = await getChatSession(nextSelectedSessionId);
2206
+ const hydratedSessions = filtered.map(session => {
2207
+ if (session.id !== nextSelectedSessionId) {
2208
+ return session;
2488
2209
  }
2489
- return events;
2490
- }
2491
- if (canUpdateMessages(previous.messages, next.messages)) {
2492
- for (let i = 0; i < previous.messages.length; i += 1) {
2493
- const previousMessage = previous.messages[i];
2494
- const nextMessage = next.messages[i];
2495
- if (!isSameMessage(previousMessage, nextMessage)) {
2496
- events.push({
2497
- inProgress: nextMessage.inProgress,
2498
- messageId: nextMessage.id,
2499
- sessionId: next.id,
2500
- text: nextMessage.text,
2501
- time: nextMessage.time,
2502
- timestamp,
2503
- toolCalls: nextMessage.toolCalls,
2504
- type: 'chat-message-updated'
2505
- });
2506
- }
2210
+ if (!loadedSession) {
2211
+ return session;
2507
2212
  }
2508
- return events;
2509
- }
2510
- events.push({
2511
- messages: [...next.messages],
2512
- sessionId: next.id,
2513
- timestamp,
2514
- type: 'chat-session-messages-replaced'
2213
+ return loadedSession;
2515
2214
  });
2516
- return events;
2215
+ return {
2216
+ ...state,
2217
+ renamingSessionId: renamingSessionId === id ? '' : renamingSessionId,
2218
+ selectedSessionId: nextSelectedSessionId,
2219
+ sessions: hydratedSessions
2220
+ };
2517
2221
  };
2518
- const replaySession = (id, title, events) => {
2519
- let deleted = false;
2520
- let currentTitle = title || '';
2521
- let messages = [];
2522
- for (const event of events) {
2523
- if (event.sessionId !== id) {
2524
- continue;
2525
- }
2526
- if (event.type === 'chat-session-created') {
2527
- deleted = false;
2528
- currentTitle = event.title;
2529
- continue;
2530
- }
2531
- if (event.type === 'chat-session-deleted') {
2532
- deleted = true;
2533
- continue;
2222
+ const deleteSessionAtIndex = async (state, index) => {
2223
+ const {
2224
+ sessions
2225
+ } = state;
2226
+ const session = sessions[index];
2227
+ if (!session) {
2228
+ return state;
2229
+ }
2230
+ return deleteSession(state, session.id);
2231
+ };
2232
+
2233
+ const parseRenderHtmlArguments = rawArguments => {
2234
+ try {
2235
+ const parsed = JSON.parse(rawArguments);
2236
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
2237
+ return undefined;
2534
2238
  }
2535
- if (event.type === 'chat-session-title-updated') {
2536
- currentTitle = event.title;
2537
- continue;
2239
+ const html = typeof Reflect.get(parsed, 'html') === 'string' ? String(Reflect.get(parsed, 'html')) : '';
2240
+ if (!html) {
2241
+ return undefined;
2538
2242
  }
2539
- if (event.type === 'chat-message-added') {
2540
- messages = [...messages, event.message];
2243
+ const css = typeof Reflect.get(parsed, 'css') === 'string' ? String(Reflect.get(parsed, 'css')) : '';
2244
+ const title = typeof Reflect.get(parsed, 'title') === 'string' ? String(Reflect.get(parsed, 'title')) : 'visual preview';
2245
+ return {
2246
+ css,
2247
+ html,
2248
+ title
2249
+ };
2250
+ } catch {
2251
+ return undefined;
2252
+ }
2253
+ };
2254
+
2255
+ const getRenderHtmlCss = (sessions, selectedSessionId) => {
2256
+ const selectedSession = sessions.find(session => session.id === selectedSessionId);
2257
+ if (!selectedSession) {
2258
+ return '';
2259
+ }
2260
+ const cssRules = new Set();
2261
+ for (const message of selectedSession.messages) {
2262
+ if (message.role !== 'assistant' || !message.toolCalls) {
2541
2263
  continue;
2542
2264
  }
2543
- if (event.type === 'chat-message-updated') {
2544
- messages = messages.map(message => {
2545
- if (message.id !== event.messageId) {
2546
- return message;
2547
- }
2548
- return {
2549
- ...message,
2550
- ...(event.inProgress === undefined ? {} : {
2551
- inProgress: event.inProgress
2552
- }),
2553
- text: event.text,
2554
- time: event.time,
2555
- ...(event.toolCalls === undefined ? {} : {
2556
- toolCalls: event.toolCalls
2557
- })
2558
- };
2559
- });
2560
- continue;
2265
+ for (const toolCall of message.toolCalls) {
2266
+ if (toolCall.name !== 'render_html') {
2267
+ continue;
2268
+ }
2269
+ const parsed = parseRenderHtmlArguments(toolCall.arguments);
2270
+ if (!parsed || !parsed.css.trim()) {
2271
+ continue;
2272
+ }
2273
+ cssRules.add(parsed.css);
2561
2274
  }
2562
- if (event.type === 'chat-session-messages-replaced') {
2563
- messages = [...event.messages];
2275
+ }
2276
+ return [...cssRules].join('\n\n');
2277
+ };
2278
+
2279
+ const isEqual$1 = (oldState, newState) => {
2280
+ const oldRenderHtmlCss = getRenderHtmlCss(oldState.sessions, oldState.selectedSessionId);
2281
+ const newRenderHtmlCss = getRenderHtmlCss(newState.sessions, newState.selectedSessionId);
2282
+ return oldState.initial === newState.initial && oldState.chatMessageFontFamily === newState.chatMessageFontFamily && oldState.chatMessageFontSize === newState.chatMessageFontSize && oldState.chatMessageLineHeight === newState.chatMessageLineHeight && oldState.composerHeight === newState.composerHeight && oldState.composerLineHeight === newState.composerLineHeight && oldState.composerFontFamily === newState.composerFontFamily && oldState.composerFontSize === newState.composerFontSize && oldState.listItemHeight === newState.listItemHeight && oldRenderHtmlCss === newRenderHtmlCss;
2283
+ };
2284
+
2285
+ const diffFocus = (oldState, newState) => {
2286
+ if (!newState.focused) {
2287
+ return true;
2288
+ }
2289
+ return oldState.focus === newState.focus && oldState.focused === newState.focused;
2290
+ };
2291
+
2292
+ const isEqual = (oldState, newState) => {
2293
+ return oldState.composerDropActive === newState.composerDropActive && oldState.composerDropEnabled === newState.composerDropEnabled && oldState.composerValue === newState.composerValue && oldState.initial === newState.initial && oldState.renamingSessionId === newState.renamingSessionId && oldState.selectedModelId === newState.selectedModelId && oldState.selectedSessionId === newState.selectedSessionId && oldState.sessions === newState.sessions && oldState.tokensMax === newState.tokensMax && oldState.tokensUsed === newState.tokensUsed && oldState.usageOverviewEnabled === newState.usageOverviewEnabled && oldState.viewMode === newState.viewMode;
2294
+ };
2295
+
2296
+ const diffScrollTop = (oldState, newState) => {
2297
+ return oldState.chatListScrollTop === newState.chatListScrollTop && oldState.messagesScrollTop === newState.messagesScrollTop;
2298
+ };
2299
+
2300
+ const RenderItems = 4;
2301
+ const RenderFocus = 6;
2302
+ const RenderFocusContext = 7;
2303
+ const RenderValue = 8;
2304
+ const RenderCss = 10;
2305
+ const RenderIncremental = 11;
2306
+ const RenderScrollTop = 12;
2307
+
2308
+ const diffValue = (oldState, newState) => {
2309
+ if (oldState.composerValue === newState.composerValue) {
2310
+ return true;
2311
+ }
2312
+ return newState.inputSource !== 'script';
2313
+ };
2314
+
2315
+ const modules = [isEqual, diffValue, diffFocus, isEqual$1, diffFocus, diffScrollTop];
2316
+ const numbers = [RenderIncremental, RenderValue, RenderFocus, RenderCss, RenderFocusContext, RenderScrollTop];
2317
+
2318
+ const diff = (oldState, newState) => {
2319
+ const diffResult = [];
2320
+ for (let i = 0; i < modules.length; i++) {
2321
+ const fn = modules[i];
2322
+ if (!fn(oldState, newState)) {
2323
+ diffResult.push(numbers[i]);
2564
2324
  }
2565
2325
  }
2566
- if (deleted || !currentTitle) {
2567
- return undefined;
2326
+ return diffResult;
2327
+ };
2328
+
2329
+ const diff2 = uid => {
2330
+ const {
2331
+ newState,
2332
+ oldState
2333
+ } = get$1(uid);
2334
+ const result = diff(oldState, newState);
2335
+ return result;
2336
+ };
2337
+
2338
+ const Button$2 = 'button';
2339
+
2340
+ const Audio = 0;
2341
+ const Button$1 = 1;
2342
+ const Col = 2;
2343
+ const ColGroup = 3;
2344
+ const Div = 4;
2345
+ const H1 = 5;
2346
+ const Input = 6;
2347
+ const Span = 8;
2348
+ const Table = 9;
2349
+ const TBody = 10;
2350
+ const Td = 11;
2351
+ const Text = 12;
2352
+ const Th = 13;
2353
+ const THead = 14;
2354
+ const Tr = 15;
2355
+ const I = 16;
2356
+ const Img = 17;
2357
+ const H2 = 22;
2358
+ const H3 = 23;
2359
+ const H4 = 24;
2360
+ const H5 = 25;
2361
+ const H6 = 26;
2362
+ const Article = 27;
2363
+ const Aside = 28;
2364
+ const Footer = 29;
2365
+ const Header = 30;
2366
+ const Nav = 40;
2367
+ const Section = 41;
2368
+ const Dd = 43;
2369
+ const Dl = 44;
2370
+ const Figcaption = 45;
2371
+ const Figure = 46;
2372
+ const Hr = 47;
2373
+ const Li = 48;
2374
+ const Ol = 49;
2375
+ const P = 50;
2376
+ const Pre = 51;
2377
+ const A = 53;
2378
+ const Abbr = 54;
2379
+ const Br = 55;
2380
+ const Tfoot = 59;
2381
+ const Ul = 60;
2382
+ const TextArea = 62;
2383
+ const Select$1 = 63;
2384
+ const Option$1 = 64;
2385
+ const Code = 65;
2386
+ const Label$1 = 66;
2387
+ const Dt = 67;
2388
+ const Main = 69;
2389
+ const Strong = 70;
2390
+ const Em = 71;
2391
+ const Reference = 100;
2392
+
2393
+ const Enter = 3;
2394
+
2395
+ const Shift = 1 << 10 >>> 0;
2396
+
2397
+ const mergeClassNames = (...classNames) => {
2398
+ return classNames.filter(Boolean).join(' ');
2399
+ };
2400
+
2401
+ const text = data => {
2402
+ return {
2403
+ childCount: 0,
2404
+ text: data,
2405
+ type: Text
2406
+ };
2407
+ };
2408
+
2409
+ const SetText = 1;
2410
+ const Replace = 2;
2411
+ const SetAttribute = 3;
2412
+ const RemoveAttribute = 4;
2413
+ const Add = 6;
2414
+ const NavigateChild = 7;
2415
+ const NavigateParent = 8;
2416
+ const RemoveChild = 9;
2417
+ const NavigateSibling = 10;
2418
+ const SetReferenceNodeUid = 11;
2419
+
2420
+ const isKey = key => {
2421
+ return key !== 'type' && key !== 'childCount';
2422
+ };
2423
+
2424
+ const getKeys = node => {
2425
+ const keys = Object.keys(node).filter(isKey);
2426
+ return keys;
2427
+ };
2428
+
2429
+ const arrayToTree = nodes => {
2430
+ const result = [];
2431
+ let i = 0;
2432
+ while (i < nodes.length) {
2433
+ const node = nodes[i];
2434
+ const {
2435
+ children,
2436
+ nodesConsumed
2437
+ } = getChildrenWithCount(nodes, i + 1, node.childCount || 0);
2438
+ result.push({
2439
+ node,
2440
+ children
2441
+ });
2442
+ i += 1 + nodesConsumed;
2443
+ }
2444
+ return result;
2445
+ };
2446
+ const getChildrenWithCount = (nodes, startIndex, childCount) => {
2447
+ if (childCount === 0) {
2448
+ return {
2449
+ children: [],
2450
+ nodesConsumed: 0
2451
+ };
2452
+ }
2453
+ const children = [];
2454
+ let i = startIndex;
2455
+ let remaining = childCount;
2456
+ let totalConsumed = 0;
2457
+ while (remaining > 0 && i < nodes.length) {
2458
+ const node = nodes[i];
2459
+ const nodeChildCount = node.childCount || 0;
2460
+ const {
2461
+ children: nodeChildren,
2462
+ nodesConsumed
2463
+ } = getChildrenWithCount(nodes, i + 1, nodeChildCount);
2464
+ children.push({
2465
+ node,
2466
+ children: nodeChildren
2467
+ });
2468
+ const nodeSize = 1 + nodesConsumed;
2469
+ i += nodeSize;
2470
+ totalConsumed += nodeSize;
2471
+ remaining--;
2568
2472
  }
2569
2473
  return {
2570
- id,
2571
- messages,
2572
- title: currentTitle
2474
+ children,
2475
+ nodesConsumed: totalConsumed
2573
2476
  };
2574
2477
  };
2575
- class InMemoryChatSessionStorage {
2576
- events = [];
2577
- summaries = new Map();
2578
- async appendEvent(event) {
2579
- this.events.push(event);
2580
- if (event.type === 'chat-session-created' || event.type === 'chat-session-title-updated') {
2581
- this.summaries.set(event.sessionId, event.title);
2582
- return;
2583
- }
2584
- if (event.type === 'chat-session-deleted') {
2585
- this.summaries.delete(event.sessionId);
2478
+
2479
+ const compareNodes = (oldNode, newNode) => {
2480
+ const patches = [];
2481
+ // Check if node type changed - return null to signal incompatible nodes
2482
+ // (caller should handle this with a Replace operation)
2483
+ if (oldNode.type !== newNode.type) {
2484
+ return null;
2485
+ }
2486
+ // Handle reference nodes - special handling for uid changes
2487
+ if (oldNode.type === Reference) {
2488
+ if (oldNode.uid !== newNode.uid) {
2489
+ patches.push({
2490
+ type: SetReferenceNodeUid,
2491
+ uid: newNode.uid
2492
+ });
2586
2493
  }
2494
+ return patches;
2587
2495
  }
2588
- async clear() {
2589
- this.events.length = 0;
2590
- this.summaries.clear();
2496
+ // Handle text nodes
2497
+ if (oldNode.type === Text && newNode.type === Text) {
2498
+ if (oldNode.text !== newNode.text) {
2499
+ patches.push({
2500
+ type: SetText,
2501
+ value: newNode.text
2502
+ });
2503
+ }
2504
+ return patches;
2591
2505
  }
2592
- async deleteSession(id) {
2593
- await this.appendEvent({
2594
- sessionId: id,
2595
- timestamp: now(),
2596
- type: 'chat-session-deleted'
2597
- });
2506
+ // Compare attributes
2507
+ const oldKeys = getKeys(oldNode);
2508
+ const newKeys = getKeys(newNode);
2509
+ // Check for attribute changes
2510
+ for (const key of newKeys) {
2511
+ if (oldNode[key] !== newNode[key]) {
2512
+ patches.push({
2513
+ type: SetAttribute,
2514
+ key,
2515
+ value: newNode[key]
2516
+ });
2517
+ }
2598
2518
  }
2599
- async getEvents(sessionId) {
2600
- if (!sessionId) {
2601
- return [...this.events];
2519
+ // Check for removed attributes
2520
+ for (const key of oldKeys) {
2521
+ if (!(key in newNode)) {
2522
+ patches.push({
2523
+ type: RemoveAttribute,
2524
+ key
2525
+ });
2602
2526
  }
2603
- return this.events.filter(event => event.sessionId === sessionId);
2604
2527
  }
2605
- async getSession(id) {
2606
- return replaySession(id, this.summaries.get(id), this.events);
2528
+ return patches;
2529
+ };
2530
+
2531
+ const treeToArray = node => {
2532
+ const result = [node.node];
2533
+ for (const child of node.children) {
2534
+ result.push(...treeToArray(child));
2607
2535
  }
2608
- async listSessions() {
2609
- const ids = new Set();
2610
- for (const id of this.summaries.keys()) {
2611
- ids.add(id);
2612
- }
2613
- for (const event of this.events) {
2614
- ids.add(event.sessionId);
2536
+ return result;
2537
+ };
2538
+
2539
+ const diffChildren = (oldChildren, newChildren, patches) => {
2540
+ const maxLength = Math.max(oldChildren.length, newChildren.length);
2541
+ // Track where we are: -1 means at parent, >= 0 means at child index
2542
+ let currentChildIndex = -1;
2543
+ // Collect indices of children to remove (we'll add these patches at the end in reverse order)
2544
+ const indicesToRemove = [];
2545
+ for (let i = 0; i < maxLength; i++) {
2546
+ const oldNode = oldChildren[i];
2547
+ const newNode = newChildren[i];
2548
+ if (!oldNode && !newNode) {
2549
+ continue;
2615
2550
  }
2616
- const sessions = [];
2617
- for (const id of ids) {
2618
- const session = replaySession(id, this.summaries.get(id), this.events);
2619
- if (!session) {
2551
+ if (!oldNode) {
2552
+ // Add new node - we should be at the parent
2553
+ if (currentChildIndex >= 0) {
2554
+ // Navigate back to parent
2555
+ patches.push({
2556
+ type: NavigateParent
2557
+ });
2558
+ currentChildIndex = -1;
2559
+ }
2560
+ // Flatten the entire subtree so renderInternal can handle it
2561
+ const flatNodes = treeToArray(newNode);
2562
+ patches.push({
2563
+ type: Add,
2564
+ nodes: flatNodes
2565
+ });
2566
+ } else if (newNode) {
2567
+ // Compare nodes to see if we need any patches
2568
+ const nodePatches = compareNodes(oldNode.node, newNode.node);
2569
+ // If nodePatches is null, the node types are incompatible - need to replace
2570
+ if (nodePatches === null) {
2571
+ // Navigate to this child
2572
+ if (currentChildIndex === -1) {
2573
+ patches.push({
2574
+ type: NavigateChild,
2575
+ index: i
2576
+ });
2577
+ currentChildIndex = i;
2578
+ } else if (currentChildIndex !== i) {
2579
+ patches.push({
2580
+ type: NavigateSibling,
2581
+ index: i
2582
+ });
2583
+ currentChildIndex = i;
2584
+ }
2585
+ // Replace the entire subtree
2586
+ const flatNodes = treeToArray(newNode);
2587
+ patches.push({
2588
+ type: Replace,
2589
+ nodes: flatNodes
2590
+ });
2591
+ // After replace, we're at the new element (same position)
2620
2592
  continue;
2621
2593
  }
2622
- sessions.push(session);
2594
+ // Check if we need to recurse into children
2595
+ const hasChildrenToCompare = oldNode.children.length > 0 || newNode.children.length > 0;
2596
+ // Only navigate to this element if we need to do something
2597
+ if (nodePatches.length > 0 || hasChildrenToCompare) {
2598
+ // Navigate to this child if not already there
2599
+ if (currentChildIndex === -1) {
2600
+ patches.push({
2601
+ type: NavigateChild,
2602
+ index: i
2603
+ });
2604
+ currentChildIndex = i;
2605
+ } else if (currentChildIndex !== i) {
2606
+ patches.push({
2607
+ type: NavigateSibling,
2608
+ index: i
2609
+ });
2610
+ currentChildIndex = i;
2611
+ }
2612
+ // Apply node patches (these apply to the current element, not children)
2613
+ if (nodePatches.length > 0) {
2614
+ patches.push(...nodePatches);
2615
+ }
2616
+ // Compare children recursively
2617
+ if (hasChildrenToCompare) {
2618
+ diffChildren(oldNode.children, newNode.children, patches);
2619
+ }
2620
+ }
2621
+ } else {
2622
+ // Remove old node - collect the index for later removal
2623
+ indicesToRemove.push(i);
2623
2624
  }
2624
- return sessions;
2625
2625
  }
2626
- async setSession(session) {
2627
- const previous = await this.getSession(session.id);
2628
- const events = getMutationEvents(previous, session);
2629
- for (const event of events) {
2630
- await this.appendEvent(event);
2631
- }
2632
- this.summaries.set(session.id, session.title);
2626
+ // Navigate back to parent if we ended at a child
2627
+ if (currentChildIndex >= 0) {
2628
+ patches.push({
2629
+ type: NavigateParent
2630
+ });
2631
+ currentChildIndex = -1;
2633
2632
  }
2634
- }
2635
-
2636
- const createDefaultStorage = () => {
2637
- if (typeof indexedDB === 'undefined') {
2638
- return new InMemoryChatSessionStorage();
2633
+ // Add remove patches in reverse order (highest index first)
2634
+ // This ensures indices remain valid as we remove
2635
+ for (let j = indicesToRemove.length - 1; j >= 0; j--) {
2636
+ patches.push({
2637
+ type: RemoveChild,
2638
+ index: indicesToRemove[j]
2639
+ });
2640
+ }
2641
+ };
2642
+ const diffTrees = (oldTree, newTree, patches, path) => {
2643
+ // At the root level (path.length === 0), we're already AT the element
2644
+ // So we compare the root node directly, then compare its children
2645
+ if (path.length === 0 && oldTree.length === 1 && newTree.length === 1) {
2646
+ const oldNode = oldTree[0];
2647
+ const newNode = newTree[0];
2648
+ // Compare root nodes
2649
+ const nodePatches = compareNodes(oldNode.node, newNode.node);
2650
+ // If nodePatches is null, the root node types are incompatible - need to replace
2651
+ if (nodePatches === null) {
2652
+ const flatNodes = treeToArray(newNode);
2653
+ patches.push({
2654
+ type: Replace,
2655
+ nodes: flatNodes
2656
+ });
2657
+ return;
2658
+ }
2659
+ if (nodePatches.length > 0) {
2660
+ patches.push(...nodePatches);
2661
+ }
2662
+ // Compare children
2663
+ if (oldNode.children.length > 0 || newNode.children.length > 0) {
2664
+ diffChildren(oldNode.children, newNode.children, patches);
2665
+ }
2666
+ } else {
2667
+ // Non-root level or multiple root elements - use the regular comparison
2668
+ diffChildren(oldTree, newTree, patches);
2639
2669
  }
2640
- return new IndexedDbChatSessionStorage();
2641
- };
2642
- let chatSessionStorage = createDefaultStorage();
2643
- const listChatSessions = async () => {
2644
- const sessions = await chatSessionStorage.listSessions();
2645
- return sessions.map(session => ({
2646
- id: session.id,
2647
- messages: [],
2648
- title: session.title
2649
- }));
2650
2670
  };
2651
- const getChatSession = async id => {
2652
- const session = await chatSessionStorage.getSession(id);
2653
- if (!session) {
2654
- return undefined;
2671
+
2672
+ const removeTrailingNavigationPatches = patches => {
2673
+ // Find the last non-navigation patch
2674
+ let lastNonNavigationIndex = -1;
2675
+ for (let i = patches.length - 1; i >= 0; i--) {
2676
+ const patch = patches[i];
2677
+ if (patch.type !== NavigateChild && patch.type !== NavigateParent && patch.type !== NavigateSibling) {
2678
+ lastNonNavigationIndex = i;
2679
+ break;
2680
+ }
2655
2681
  }
2656
- return {
2657
- id: session.id,
2658
- messages: [...session.messages],
2659
- title: session.title
2660
- };
2682
+ // Return patches up to and including the last non-navigation patch
2683
+ return lastNonNavigationIndex === -1 ? [] : patches.slice(0, lastNonNavigationIndex + 1);
2661
2684
  };
2662
- const saveChatSession = async session => {
2663
- await chatSessionStorage.setSession({
2664
- id: session.id,
2665
- messages: [...session.messages],
2666
- title: session.title
2667
- });
2685
+
2686
+ const diffTree = (oldNodes, newNodes) => {
2687
+ // Step 1: Convert flat arrays to tree structures
2688
+ const oldTree = arrayToTree(oldNodes);
2689
+ const newTree = arrayToTree(newNodes);
2690
+ // Step 3: Compare the trees
2691
+ const patches = [];
2692
+ diffTrees(oldTree, newTree, patches, []);
2693
+ // Remove trailing navigation patches since they serve no purpose
2694
+ return removeTrailingNavigationPatches(patches);
2668
2695
  };
2669
- const deleteChatSession = async id => {
2670
- await chatSessionStorage.deleteSession(id);
2696
+
2697
+ const getKeyBindings = () => {
2698
+ return [{
2699
+ command: 'Chat.handleSubmit',
2700
+ key: Enter,
2701
+ when: FocusChatInput
2702
+ }, {
2703
+ command: 'Chat.enterNewLine',
2704
+ key: Shift | Enter,
2705
+ when: FocusChatInput
2706
+ }];
2671
2707
  };
2672
- const clearChatSessions = async () => {
2673
- await chatSessionStorage.clear();
2708
+
2709
+ const getSelectedSessionId = state => {
2710
+ return state.selectedSessionId;
2674
2711
  };
2675
- const appendChatViewEvent = async event => {
2676
- await chatSessionStorage.appendEvent(event);
2712
+
2713
+ const getListIndex = (state, eventX, eventY) => {
2714
+ const {
2715
+ headerHeight,
2716
+ height,
2717
+ listItemHeight,
2718
+ width,
2719
+ x,
2720
+ y
2721
+ } = state;
2722
+ const relativeX = eventX - x;
2723
+ const relativeY = eventY - y - headerHeight;
2724
+ if (relativeX < 0 || relativeY < 0 || relativeX >= width || relativeY >= height - headerHeight) {
2725
+ return -1;
2726
+ }
2727
+ return Math.floor(relativeY / listItemHeight);
2728
+ };
2729
+
2730
+ const CHAT_LIST_ITEM_CONTEXT_MENU = 'ChatListItemContextMenu';
2731
+ const handleChatListContextMenu = async (state, eventX, eventY) => {
2732
+ const index = getListIndex(state, eventX, eventY);
2733
+ if (index === -1) {
2734
+ return state;
2735
+ }
2736
+ const item = state.sessions[index];
2737
+ if (!item) {
2738
+ return state;
2739
+ }
2740
+ await invoke('ContextMenu.show', eventX, eventY, CHAT_LIST_ITEM_CONTEXT_MENU, item.id);
2741
+ return state;
2677
2742
  };
2678
2743
 
2679
2744
  const generateSessionId = () => {
@@ -2696,56 +2761,6 @@ const createSession = async state => {
2696
2761
  };
2697
2762
  };
2698
2763
 
2699
- const getNextSelectedSessionId = (sessions, deletedId) => {
2700
- if (sessions.length === 0) {
2701
- return '';
2702
- }
2703
- const index = sessions.findIndex(session => session.id === deletedId);
2704
- if (index === -1) {
2705
- return sessions[0].id;
2706
- }
2707
- const nextIndex = Math.min(index, sessions.length - 1);
2708
- return sessions[nextIndex].id;
2709
- };
2710
-
2711
- const deleteSession = async (state, id) => {
2712
- const {
2713
- renamingSessionId,
2714
- sessions
2715
- } = state;
2716
- const filtered = sessions.filter(session => session.id !== id);
2717
- if (filtered.length === sessions.length) {
2718
- return state;
2719
- }
2720
- await deleteChatSession(id);
2721
- if (filtered.length === 0) {
2722
- return {
2723
- ...state,
2724
- renamingSessionId: '',
2725
- selectedSessionId: '',
2726
- sessions: [],
2727
- viewMode: 'list'
2728
- };
2729
- }
2730
- const nextSelectedSessionId = getNextSelectedSessionId(filtered, id);
2731
- const loadedSession = await getChatSession(nextSelectedSessionId);
2732
- const hydratedSessions = filtered.map(session => {
2733
- if (session.id !== nextSelectedSessionId) {
2734
- return session;
2735
- }
2736
- if (!loadedSession) {
2737
- return session;
2738
- }
2739
- return loadedSession;
2740
- });
2741
- return {
2742
- ...state,
2743
- renamingSessionId: renamingSessionId === id ? '' : renamingSessionId,
2744
- selectedSessionId: nextSelectedSessionId,
2745
- sessions: hydratedSessions
2746
- };
2747
- };
2748
-
2749
2764
  const handleClickOpenApiApiKeySettings = async state => {
2750
2765
  await invoke('Main.openUri', 'app://settings.json');
2751
2766
  return state;
@@ -5493,6 +5508,7 @@ const handleClickSend = async state => {
5493
5508
  };
5494
5509
 
5495
5510
  const Composer = 'composer';
5511
+ const ComposerDropTarget = 'composer-drop-target';
5496
5512
  const Send = 'send';
5497
5513
  const Back = 'back';
5498
5514
  const Model = 'model';
@@ -5667,6 +5683,89 @@ const handleClickSettings = async () => {
5667
5683
  await invoke('Main.openUri', 'app://settings.json');
5668
5684
  };
5669
5685
 
5686
+ const handleDragEnter = async (state, name, hasFiles = true) => {
5687
+ if (name !== ComposerDropTarget) {
5688
+ return state;
5689
+ }
5690
+ if (!state.composerDropEnabled) {
5691
+ return state;
5692
+ }
5693
+ if (!hasFiles) {
5694
+ return state;
5695
+ }
5696
+ if (state.composerDropActive) {
5697
+ return state;
5698
+ }
5699
+ return {
5700
+ ...state,
5701
+ composerDropActive: true
5702
+ };
5703
+ };
5704
+
5705
+ const handleDragLeave = async (state, name) => {
5706
+ if (name !== ComposerDropTarget) {
5707
+ return state;
5708
+ }
5709
+ if (!state.composerDropActive) {
5710
+ return state;
5711
+ }
5712
+ return {
5713
+ ...state,
5714
+ composerDropActive: false
5715
+ };
5716
+ };
5717
+
5718
+ const handleDragOver = async (state, name, hasFiles = true) => {
5719
+ if (name !== ComposerDropTarget) {
5720
+ return state;
5721
+ }
5722
+ if (!state.composerDropEnabled) {
5723
+ return state;
5724
+ }
5725
+ if (!hasFiles) {
5726
+ return state;
5727
+ }
5728
+ if (state.composerDropActive) {
5729
+ return state;
5730
+ }
5731
+ return {
5732
+ ...state,
5733
+ composerDropActive: true
5734
+ };
5735
+ };
5736
+
5737
+ const handleDropFiles = async (state, name, files = []) => {
5738
+ if (name !== ComposerDropTarget) {
5739
+ return state;
5740
+ }
5741
+ if (!state.composerDropEnabled) {
5742
+ return {
5743
+ ...state,
5744
+ composerDropActive: false
5745
+ };
5746
+ }
5747
+ const nextState = state.composerDropActive === false ? state : {
5748
+ ...state,
5749
+ composerDropActive: false
5750
+ };
5751
+ if (!state.selectedSessionId || files.length === 0) {
5752
+ return nextState;
5753
+ }
5754
+ for (const file of files) {
5755
+ await appendChatViewEvent({
5756
+ attachmentId: crypto.randomUUID(),
5757
+ blob: file,
5758
+ mimeType: file.type,
5759
+ name: file.name,
5760
+ sessionId: state.selectedSessionId,
5761
+ size: file.size,
5762
+ timestamp: new Date().toISOString(),
5763
+ type: 'chat-attachment-added'
5764
+ });
5765
+ }
5766
+ return nextState;
5767
+ };
5768
+
5670
5769
  const handleInput = async (state, name, value, inputSource = 'user') => {
5671
5770
  const {
5672
5771
  selectedSessionId
@@ -5944,6 +6043,15 @@ const loadAiSessionTitleGenerationEnabled = async () => {
5944
6043
  }
5945
6044
  };
5946
6045
 
6046
+ const loadComposerDropEnabled = async () => {
6047
+ try {
6048
+ const savedComposerDropEnabled = await get('chatView.composerDropEnabled');
6049
+ return typeof savedComposerDropEnabled === 'boolean' ? savedComposerDropEnabled : true;
6050
+ } catch {
6051
+ return true;
6052
+ }
6053
+ };
6054
+
5947
6055
  const loadEmitStreamingFunctionCallEvents = async () => {
5948
6056
  try {
5949
6057
  const savedEmitStreamingFunctionCallEvents = await get('chatView.emitStreamingFunctionCallEvents');
@@ -5998,9 +6106,10 @@ const loadStreamingEnabled = async () => {
5998
6106
  };
5999
6107
 
6000
6108
  const loadPreferences = async () => {
6001
- const [aiSessionTitleGenerationEnabled, openApiApiKey, openRouterApiKey, emitStreamingFunctionCallEvents, streamingEnabled, passIncludeObfuscation] = await Promise.all([loadAiSessionTitleGenerationEnabled(), loadOpenApiApiKey(), loadOpenRouterApiKey(), loadEmitStreamingFunctionCallEvents(), loadStreamingEnabled(), loadPassIncludeObfuscation()]);
6109
+ const [aiSessionTitleGenerationEnabled, composerDropEnabled, openApiApiKey, openRouterApiKey, emitStreamingFunctionCallEvents, streamingEnabled, passIncludeObfuscation] = await Promise.all([loadAiSessionTitleGenerationEnabled(), loadComposerDropEnabled(), loadOpenApiApiKey(), loadOpenRouterApiKey(), loadEmitStreamingFunctionCallEvents(), loadStreamingEnabled(), loadPassIncludeObfuscation()]);
6002
6110
  return {
6003
6111
  aiSessionTitleGenerationEnabled,
6112
+ composerDropEnabled,
6004
6113
  emitStreamingFunctionCallEvents,
6005
6114
  openApiApiKey,
6006
6115
  openRouterApiKey,
@@ -6037,6 +6146,7 @@ const loadContent = async (state, savedState) => {
6037
6146
  const savedViewMode = getSavedViewMode(savedState);
6038
6147
  const {
6039
6148
  aiSessionTitleGenerationEnabled,
6149
+ composerDropEnabled,
6040
6150
  emitStreamingFunctionCallEvents,
6041
6151
  openApiApiKey,
6042
6152
  openRouterApiKey,
@@ -6071,6 +6181,8 @@ const loadContent = async (state, savedState) => {
6071
6181
  ...state,
6072
6182
  aiSessionTitleGenerationEnabled,
6073
6183
  chatListScrollTop,
6184
+ composerDropActive: false,
6185
+ composerDropEnabled,
6074
6186
  emitStreamingFunctionCallEvents,
6075
6187
  initial: false,
6076
6188
  messagesScrollTop,
@@ -6156,92 +6268,7 @@ const getCss = (composerHeight, listItemHeight, chatMessageFontSize, chatMessage
6156
6268
  --ChatMessageLineHeight: ${chatMessageLineHeight}px;
6157
6269
  --ChatMessageFontFamily: ${chatMessageFontFamily};
6158
6270
  }
6159
-
6160
- .ChatToolCalls {
6161
- position: relative;
6162
- border: 1px solid var(--vscode-editorWidget-border);
6163
- border-radius: 4px;
6164
- margin-bottom: 8px;
6165
- padding: 10px 8px 6px;
6166
- background: var(--vscode-editorWidget-background);
6167
- }
6168
-
6169
- .ChatToolCallsLabel {
6170
- position: absolute;
6171
- top: -8px;
6172
- left: 8px;
6173
- padding: 0 4px;
6174
- border-radius: 3px;
6175
- background: var(--vscode-editor-background);
6176
- color: var(--vscode-descriptionForeground);
6177
- font-size: 10px;
6178
- line-height: 14px;
6179
- text-transform: lowercase;
6180
- letter-spacing: 0.02em;
6181
- }
6182
-
6183
- .ChatToolCallReadFileLink {
6184
- color: var(--vscode-textLink-foreground);
6185
- text-decoration: underline;
6186
- }
6187
-
6188
- .ChatToolCallRenderHtmlLabel {
6189
- margin-bottom: 6px;
6190
- color: var(--vscode-descriptionForeground);
6191
- font-size: 12px;
6192
- }
6193
-
6194
- .ChatToolCallRenderHtmlContent {
6195
- border: 1px solid var(--vscode-editorWidget-border);
6196
- border-radius: 6px;
6197
- background: var(--vscode-editor-background);
6198
- overflow: hidden;
6199
- }
6200
-
6201
- .ChatToolCallRenderHtmlBody {
6202
- min-height: 180px;
6203
- padding: 12px;
6204
- }
6205
-
6206
- .ChatToolCallRenderHtmlBody * {
6207
- box-sizing: border-box;
6208
- }
6209
-
6210
- .ChatMessageLink {
6211
- color: #4d94ff;
6212
- text-decoration: underline;
6213
- cursor: pointer;
6214
- }
6215
-
6216
- .ChatOrderedList,
6217
- .ChatUnorderedList {
6218
- margin: 6px 0;
6219
- padding-inline-start: 20px;
6220
- }
6221
-
6222
- .ChatOrderedListItem,
6223
- .ChatUnorderedListItem {
6224
- margin: 2px 0;
6225
- }
6226
-
6227
- .MarkdownTable {
6228
- width: 100%;
6229
- margin: 6px 0;
6230
- border-collapse: collapse;
6231
- border: 1px solid var(--vscode-editorWidget-border);
6232
- }
6233
-
6234
- .MarkdownTable th,
6235
- .MarkdownTable td {
6236
- border: 1px solid var(--vscode-editorWidget-border);
6237
- padding: 4px 8px;
6238
- text-align: left;
6239
- }
6240
-
6241
- .MarkdownTable th {
6242
- background: var(--vscode-editorWidget-background);
6243
- }
6244
- }`;
6271
+ `;
6245
6272
  if (!renderHtmlCss.trim()) {
6246
6273
  return baseCss;
6247
6274
  }
@@ -6298,6 +6325,8 @@ const Actions = 'Actions';
6298
6325
  const ChatActions = 'ChatActions';
6299
6326
  const ChatName = 'ChatName';
6300
6327
  const ChatSendArea = 'ChatSendArea';
6328
+ const ChatViewDropOverlay = 'ChatViewDropOverlay';
6329
+ const ChatViewDropOverlayActive = 'ChatViewDropOverlayActive';
6301
6330
  const SendButtonDisabled = 'SendButtonDisabled';
6302
6331
  const ChatSendAreaBottom = 'ChatSendAreaBottom';
6303
6332
  const ChatSendAreaContent = 'ChatSendAreaContent';
@@ -6306,6 +6335,7 @@ const ChatHeader = 'ChatHeader';
6306
6335
  const Button = 'Button';
6307
6336
  const ButtonPrimary = 'ButtonPrimary';
6308
6337
  const ButtonSecondary = 'ButtonSecondary';
6338
+ const Empty = '';
6309
6339
  const FileIcon = 'FileIcon';
6310
6340
  const IconButton = 'IconButton';
6311
6341
  const InputBox = 'InputBox';
@@ -6359,6 +6389,12 @@ const HandleMessagesScroll = 22;
6359
6389
  const HandleClickSessionDebug = 23;
6360
6390
  const HandleClickReadFile = 24;
6361
6391
  const HandleMessagesContextMenu = 25;
6392
+ const HandleDragEnter = 26;
6393
+ const HandleDragOver = 27;
6394
+ const HandleDragLeave = 28;
6395
+ const HandleDrop = 29;
6396
+ const HandleDragEnterChatView = 30;
6397
+ const HandleDragOverChatView = 31;
6362
6398
 
6363
6399
  const getModelLabel = model => {
6364
6400
  if (model.provider === 'openRouter') {
@@ -7230,6 +7266,12 @@ const markdownInlineRegex = /\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*/g;
7230
7266
  const markdownTableSeparatorCellRegex = /^:?-{3,}:?$/;
7231
7267
  const fencedCodeBlockRegex = /^```/;
7232
7268
  const markdownHeadingRegex = /^\s*(#{1,6})\s+(.*)$/;
7269
+ const normalizeEscapedNewlines = value => {
7270
+ if (value.includes('\\n')) {
7271
+ return value.replaceAll(/\\r\\n|\\n/g, '\n');
7272
+ }
7273
+ return value;
7274
+ };
7233
7275
  const normalizeInlineTables = value => {
7234
7276
  return value.split(/\r?\n/).map(line => {
7235
7277
  if (!line.includes('|')) {
@@ -7328,7 +7370,8 @@ const parseMessageContent = rawMessage => {
7328
7370
  type: 'text'
7329
7371
  }];
7330
7372
  }
7331
- const lines = normalizeInlineTables(rawMessage).split(/\r?\n/);
7373
+ const normalizedMessage = normalizeEscapedNewlines(rawMessage);
7374
+ const lines = normalizeInlineTables(normalizedMessage).split(/\r?\n/);
7332
7375
  const nodes = [];
7333
7376
  let paragraphLines = [];
7334
7377
  let listItems = [];
@@ -7494,15 +7537,29 @@ const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = ''
7494
7537
  }, ...messages.flatMap(message => getChatMessageDom(message, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState))];
7495
7538
  };
7496
7539
 
7497
- const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20, messagesScrollTop = 0) => {
7540
+ const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20, messagesScrollTop = 0, composerDropActive = false, composerDropEnabled = true) => {
7498
7541
  const selectedSession = sessions.find(session => session.id === selectedSessionId);
7499
7542
  const selectedSessionTitle = selectedSession?.title || chatTitle();
7500
7543
  const messages = selectedSession ? selectedSession.messages : [];
7544
+ const isDropOverlayVisible = composerDropEnabled && composerDropActive;
7501
7545
  return [{
7502
- childCount: 3,
7546
+ childCount: 4,
7503
7547
  className: mergeClassNames(Viewlet, Chat),
7548
+ onDragEnter: HandleDragEnterChatView,
7549
+ onDragOver: HandleDragOverChatView,
7550
+ type: Div
7551
+ }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState, messagesScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight), {
7552
+ childCount: 1,
7553
+ className: mergeClassNames(ChatViewDropOverlay, isDropOverlayVisible ? ChatViewDropOverlayActive : Empty),
7554
+ name: ComposerDropTarget,
7555
+ onDragLeave: HandleDragLeave,
7556
+ onDragOver: HandleDragOver,
7557
+ onDrop: HandleDrop,
7504
7558
  type: Div
7505
- }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState, messagesScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
7559
+ }, {
7560
+ text: attachImageAsContext(),
7561
+ type: Text
7562
+ }];
7506
7563
  };
7507
7564
 
7508
7565
  const getChatHeaderListModeDom = () => {
@@ -7572,12 +7629,26 @@ const getChatListDom = (sessions, selectedSessionId, chatListScrollTop = 0) => {
7572
7629
  }, ...sessions.flatMap(getSessionDom)];
7573
7630
  };
7574
7631
 
7575
- const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20, chatListScrollTop = 0) => {
7632
+ const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20, chatListScrollTop = 0, composerDropActive = false, composerDropEnabled = true) => {
7633
+ const isDropOverlayVisible = composerDropEnabled && composerDropActive;
7576
7634
  return [{
7577
- childCount: 3,
7635
+ childCount: 4,
7578
7636
  className: mergeClassNames(Viewlet, Chat),
7637
+ onDragEnter: HandleDragEnterChatView,
7638
+ onDragOver: HandleDragOverChatView,
7639
+ type: Div
7640
+ }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions, selectedSessionId, chatListScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight), {
7641
+ childCount: 1,
7642
+ className: mergeClassNames(ChatViewDropOverlay, isDropOverlayVisible ? ChatViewDropOverlayActive : Empty),
7643
+ name: ComposerDropTarget,
7644
+ onDragLeave: HandleDragLeave,
7645
+ onDragOver: HandleDragOver,
7646
+ onDrop: HandleDrop,
7579
7647
  type: Div
7580
- }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions, selectedSessionId, chatListScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
7648
+ }, {
7649
+ text: attachImageAsContext(),
7650
+ type: Text
7651
+ }];
7581
7652
  };
7582
7653
 
7583
7654
  const getChatModeUnsupportedVirtualDom = () => {
@@ -7587,12 +7658,12 @@ const getChatModeUnsupportedVirtualDom = () => {
7587
7658
  }, text(unknownViewMode())];
7588
7659
  };
7589
7660
 
7590
- const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop) => {
7661
+ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop, composerDropActive = false, composerDropEnabled = true) => {
7591
7662
  switch (viewMode) {
7592
7663
  case 'detail':
7593
- return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, messagesScrollTop);
7664
+ return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, messagesScrollTop, composerDropActive, composerDropEnabled);
7594
7665
  case 'list':
7595
- return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop);
7666
+ return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, composerDropActive, composerDropEnabled);
7596
7667
  default:
7597
7668
  return getChatModeUnsupportedVirtualDom();
7598
7669
  }
@@ -7601,6 +7672,8 @@ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRoute
7601
7672
  const renderItems = (oldState, newState) => {
7602
7673
  const {
7603
7674
  chatListScrollTop,
7675
+ composerDropActive,
7676
+ composerDropEnabled,
7604
7677
  composerFontFamily,
7605
7678
  composerFontSize,
7606
7679
  composerHeight,
@@ -7624,7 +7697,7 @@ const renderItems = (oldState, newState) => {
7624
7697
  if (initial) {
7625
7698
  return [SetDom2, uid, []];
7626
7699
  }
7627
- const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop);
7700
+ const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop, composerDropActive, composerDropEnabled);
7628
7701
  return [SetDom2, uid, dom];
7629
7702
  };
7630
7703
 
@@ -7728,6 +7801,30 @@ const renderEventListeners = () => {
7728
7801
  }, {
7729
7802
  name: HandleInput,
7730
7803
  params: ['handleInput', TargetName, TargetValue]
7804
+ }, {
7805
+ name: HandleDragEnter,
7806
+ params: ['handleDragEnter', TargetName, 'Array.from(event.dataTransfer?.files || []).length > 0'],
7807
+ preventDefault: true
7808
+ }, {
7809
+ name: HandleDragOver,
7810
+ params: ['handleDragOver', TargetName, 'Array.from(event.dataTransfer?.files || []).length > 0'],
7811
+ preventDefault: true
7812
+ }, {
7813
+ name: HandleDragLeave,
7814
+ params: ['handleDragLeave', TargetName],
7815
+ preventDefault: true
7816
+ }, {
7817
+ name: HandleDrop,
7818
+ params: ['handleDropFiles', TargetName, 'Array.from(event.dataTransfer?.files || [])'],
7819
+ preventDefault: true
7820
+ }, {
7821
+ name: HandleDragEnterChatView,
7822
+ params: ['handleDragEnter', 'composer-drop-target', 'Array.from(event.dataTransfer?.files || []).length > 0'],
7823
+ preventDefault: true
7824
+ }, {
7825
+ name: HandleDragOverChatView,
7826
+ params: ['handleDragOver', 'composer-drop-target', 'Array.from(event.dataTransfer?.files || []).length > 0'],
7827
+ preventDefault: true
7731
7828
  }, {
7732
7829
  name: HandleModelChange,
7733
7830
  params: ['handleModelChange', TargetValue]
@@ -7857,6 +7954,7 @@ const useMockApi = (state, value, mockApiCommandId = defaultMockApiCommandId) =>
7857
7954
  const commandMap = {
7858
7955
  'Chat.clearInput': wrapCommand(clearInput),
7859
7956
  'Chat.create': create,
7957
+ 'Chat.deleteSessionAtIndex': wrapCommand(deleteSessionAtIndex),
7860
7958
  'Chat.diff2': diff2,
7861
7959
  'Chat.enterNewLine': wrapCommand(handleNewline),
7862
7960
  'Chat.getCommandIds': getCommandIds,
@@ -7873,6 +7971,10 @@ const commandMap = {
7873
7971
  'Chat.handleClickReadFile': handleClickReadFile,
7874
7972
  'Chat.handleClickSessionDebug': wrapCommand(handleClickSessionDebug),
7875
7973
  'Chat.handleClickSettings': handleClickSettings,
7974
+ 'Chat.handleDragEnter': wrapCommand(handleDragEnter),
7975
+ 'Chat.handleDragLeave': wrapCommand(handleDragLeave),
7976
+ 'Chat.handleDragOver': wrapCommand(handleDragOver),
7977
+ 'Chat.handleDropFiles': wrapCommand(handleDropFiles),
7876
7978
  'Chat.handleInput': wrapCommand(handleInput),
7877
7979
  'Chat.handleInputFocus': wrapCommand(handleInputFocus),
7878
7980
  'Chat.handleKeyDown': wrapCommand(handleKeyDown),