@lvce-editor/chat-view 3.5.0 → 3.7.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.
@@ -1075,6 +1075,7 @@ const create$2 = rpcId => {
1075
1075
  };
1076
1076
 
1077
1077
  const {
1078
+ invoke: invoke$2,
1078
1079
  set: set$3
1079
1080
  } = create$2(6002);
1080
1081
 
@@ -1347,6 +1348,9 @@ const startConversation = () => {
1347
1348
  const composePlaceholder = () => {
1348
1349
  return i18nString('Type your message. Enter to send, Shift+Enter for newline.');
1349
1350
  };
1351
+ const attachImageAsContext = () => {
1352
+ return i18nString('Attach Image as Context');
1353
+ };
1350
1354
  const openRouterApiKeyPlaceholder = () => {
1351
1355
  return i18nString('Enter OpenRouter API key');
1352
1356
  };
@@ -1475,6 +1479,8 @@ const createDefaultState = () => {
1475
1479
  chatMessageFontFamily: 'system-ui',
1476
1480
  chatMessageFontSize,
1477
1481
  chatMessageLineHeight,
1482
+ composerDropActive: false,
1483
+ composerDropEnabled: true,
1478
1484
  composerFontFamily: 'system-ui',
1479
1485
  composerFontSize,
1480
1486
  composerHeight: composerLineHeight + 8,
@@ -1520,6 +1526,7 @@ const createDefaultState = () => {
1520
1526
  tokensUsed: 0,
1521
1527
  uid: 0,
1522
1528
  usageOverviewEnabled: false,
1529
+ useChatNetworkWorkerForRequests: false,
1523
1530
  useMockApi: false,
1524
1531
  viewMode: 'list',
1525
1532
  warningCount: 0,
@@ -1553,579 +1560,379 @@ const create = (uid, x, y, width, height, platform, assetDir) => {
1553
1560
  set(uid, state, state);
1554
1561
  };
1555
1562
 
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;
1563
+ const toError = error => {
1564
+ if (error instanceof Error) {
1565
+ return error;
1575
1566
  }
1567
+ return new Error('IndexedDB request failed');
1576
1568
  };
1577
1569
 
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');
1570
+ const requestToPromise = async createRequest => {
1571
+ const request = createRequest();
1572
+ const {
1573
+ promise,
1574
+ reject,
1575
+ resolve
1576
+ } = Promise.withResolvers();
1577
+ request.addEventListener('success', () => {
1578
+ resolve(request.result);
1579
+ });
1580
+ request.addEventListener('error', () => {
1581
+ reject(toError(request.error));
1582
+ });
1583
+ return promise;
1600
1584
  };
1601
1585
 
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;
1586
+ const transactionToPromise = async createTransaction => {
1587
+ const transaction = createTransaction();
1588
+ const {
1589
+ promise,
1590
+ reject,
1591
+ resolve
1592
+ } = Promise.withResolvers();
1593
+ transaction.addEventListener('complete', () => {
1594
+ resolve();
1595
+ });
1596
+ transaction.addEventListener('error', () => {
1597
+ reject(toError(transaction.error));
1598
+ });
1599
+ transaction.addEventListener('abort', () => {
1600
+ reject(toError(transaction.error));
1601
+ });
1602
+ return promise;
1606
1603
  };
1607
1604
 
1608
- const diffFocus = (oldState, newState) => {
1609
- if (!newState.focused) {
1610
- return true;
1611
- }
1612
- return oldState.focus === newState.focus && oldState.focused === newState.focused;
1605
+ const toChatViewEvent = event => {
1606
+ const {
1607
+ eventId,
1608
+ ...chatViewEvent
1609
+ } = event;
1610
+ return chatViewEvent;
1613
1611
  };
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;
1612
+ const now$1 = () => {
1613
+ return new Date().toISOString();
1617
1614
  };
1618
-
1619
- const diffScrollTop = (oldState, newState) => {
1620
- return oldState.chatListScrollTop === newState.chatListScrollTop && oldState.messagesScrollTop === newState.messagesScrollTop;
1615
+ const isSameMessage$1 = (a, b) => {
1616
+ 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
1617
  };
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;
1618
+ const canAppendMessages$1 = (previousMessages, nextMessages) => {
1619
+ if (nextMessages.length < previousMessages.length) {
1620
+ return false;
1634
1621
  }
1635
- return newState.inputSource !== 'script';
1622
+ return previousMessages.every((message, index) => isSameMessage$1(message, nextMessages[index]));
1636
1623
  };
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]);
1624
+ const canUpdateMessages$1 = (previousMessages, nextMessages) => {
1625
+ if (previousMessages.length !== nextMessages.length) {
1626
+ return false;
1627
+ }
1628
+ for (let i = 0; i < previousMessages.length; i += 1) {
1629
+ const previous = previousMessages[i];
1630
+ const next = nextMessages[i];
1631
+ if (previous.id !== next.id || previous.role !== next.role) {
1632
+ return false;
1647
1633
  }
1648
1634
  }
1649
- return diffResult;
1635
+ return true;
1650
1636
  };
1651
-
1652
- const diff2 = uid => {
1653
- const {
1654
- newState,
1655
- oldState
1656
- } = get$1(uid);
1657
- const result = diff(oldState, newState);
1658
- return result;
1637
+ const getMutationEvents$1 = (previous, next) => {
1638
+ const timestamp = now$1();
1639
+ const events = [];
1640
+ if (!previous) {
1641
+ events.push({
1642
+ sessionId: next.id,
1643
+ timestamp,
1644
+ title: next.title,
1645
+ type: 'chat-session-created'
1646
+ });
1647
+ for (const message of next.messages) {
1648
+ events.push({
1649
+ message,
1650
+ sessionId: next.id,
1651
+ timestamp,
1652
+ type: 'chat-message-added'
1653
+ });
1654
+ }
1655
+ return events;
1656
+ }
1657
+ if (previous.title !== next.title) {
1658
+ events.push({
1659
+ sessionId: next.id,
1660
+ timestamp,
1661
+ title: next.title,
1662
+ type: 'chat-session-title-updated'
1663
+ });
1664
+ }
1665
+ if (canAppendMessages$1(previous.messages, next.messages)) {
1666
+ for (let i = previous.messages.length; i < next.messages.length; i += 1) {
1667
+ events.push({
1668
+ message: next.messages[i],
1669
+ sessionId: next.id,
1670
+ timestamp,
1671
+ type: 'chat-message-added'
1672
+ });
1673
+ }
1674
+ return events;
1675
+ }
1676
+ if (canUpdateMessages$1(previous.messages, next.messages)) {
1677
+ for (let i = 0; i < previous.messages.length; i += 1) {
1678
+ const previousMessage = previous.messages[i];
1679
+ const nextMessage = next.messages[i];
1680
+ if (!isSameMessage$1(previousMessage, nextMessage)) {
1681
+ events.push({
1682
+ inProgress: nextMessage.inProgress,
1683
+ messageId: nextMessage.id,
1684
+ sessionId: next.id,
1685
+ text: nextMessage.text,
1686
+ time: nextMessage.time,
1687
+ timestamp,
1688
+ toolCalls: nextMessage.toolCalls,
1689
+ type: 'chat-message-updated'
1690
+ });
1691
+ }
1692
+ }
1693
+ return events;
1694
+ }
1695
+ events.push({
1696
+ messages: [...next.messages],
1697
+ sessionId: next.id,
1698
+ timestamp,
1699
+ type: 'chat-session-messages-replaced'
1700
+ });
1701
+ return events;
1659
1702
  };
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
- });
1703
+ const replaySession$1 = (id, summary, events) => {
1704
+ let deleted = false;
1705
+ let title = summary?.title || '';
1706
+ let messages = summary?.messages ? [...summary.messages] : [];
1707
+ for (const event of events) {
1708
+ if (event.sessionId !== id) {
1709
+ continue;
1816
1710
  }
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
- });
1711
+ if (event.type === 'chat-session-created') {
1712
+ const {
1713
+ title: eventTitle
1714
+ } = event;
1715
+ deleted = false;
1716
+ title = eventTitle;
1717
+ continue;
1826
1718
  }
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
- });
1719
+ if (event.type === 'chat-session-deleted') {
1720
+ deleted = true;
1721
+ continue;
1840
1722
  }
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
1723
+ if (event.type === 'chat-session-title-updated') {
1724
+ const {
1725
+ title: eventTitle
1726
+ } = event;
1727
+ title = eventTitle;
1728
+ continue;
1729
+ }
1730
+ if (event.type === 'chat-message-added') {
1731
+ messages = [...messages, event.message];
1732
+ continue;
1733
+ }
1734
+ if (event.type === 'chat-message-updated') {
1735
+ messages = messages.map(message => {
1736
+ if (message.id !== event.messageId) {
1737
+ return message;
1738
+ }
1739
+ return {
1740
+ ...message,
1741
+ ...(event.inProgress === undefined ? {} : {
1742
+ inProgress: event.inProgress
1743
+ }),
1744
+ text: event.text,
1745
+ time: event.time,
1746
+ ...(event.toolCalls === undefined ? {} : {
1747
+ toolCalls: event.toolCalls
1748
+ })
1749
+ };
1848
1750
  });
1751
+ continue;
1752
+ }
1753
+ if (event.type === 'chat-session-messages-replaced') {
1754
+ messages = [...event.messages];
1849
1755
  }
1850
1756
  }
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));
1757
+ if (deleted || !title) {
1758
+ return undefined;
1858
1759
  }
1859
- return result;
1760
+ return {
1761
+ id,
1762
+ messages,
1763
+ title
1764
+ };
1860
1765
  };
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;
1766
+ class IndexedDbChatSessionStorage {
1767
+ constructor(options = {}) {
1768
+ this.state = {
1769
+ databaseName: options.databaseName || 'lvce-chat-view-sessions',
1770
+ databasePromise: undefined,
1771
+ databaseVersion: options.databaseVersion || 2,
1772
+ eventStoreName: options.eventStoreName || 'chat-view-events',
1773
+ storeName: options.storeName || 'chat-sessions'
1774
+ };
1775
+ }
1776
+ openDatabase = async () => {
1777
+ if (this.state.databasePromise) {
1778
+ return this.state.databasePromise;
1873
1779
  }
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
1780
+ const request = indexedDB.open(this.state.databaseName, this.state.databaseVersion);
1781
+ request.addEventListener('upgradeneeded', () => {
1782
+ const database = request.result;
1783
+ if (!database.objectStoreNames.contains(this.state.storeName)) {
1784
+ database.createObjectStore(this.state.storeName, {
1785
+ keyPath: 'id'
1880
1786
  });
1881
- currentChildIndex = -1;
1882
1787
  }
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
1788
+ if (database.objectStoreNames.contains(this.state.eventStoreName)) {
1789
+ const {
1790
+ transaction
1791
+ } = request;
1792
+ if (!transaction) {
1793
+ return;
1794
+ }
1795
+ const eventStore = transaction.objectStore(this.state.eventStoreName);
1796
+ if (!eventStore.indexNames.contains('sessionId')) {
1797
+ eventStore.createIndex('sessionId', 'sessionId', {
1798
+ unique: false
1905
1799
  });
1906
- currentChildIndex = i;
1907
1800
  }
1908
- // Replace the entire subtree
1909
- const flatNodes = treeToArray(newNode);
1910
- patches.push({
1911
- type: Replace,
1912
- nodes: flatNodes
1801
+ } else {
1802
+ const eventStore = database.createObjectStore(this.state.eventStoreName, {
1803
+ autoIncrement: true,
1804
+ keyPath: 'eventId'
1805
+ });
1806
+ eventStore.createIndex('sessionId', 'sessionId', {
1807
+ unique: false
1913
1808
  });
1914
- // After replace, we're at the new element (same position)
1915
- continue;
1916
1809
  }
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
- }
1810
+ });
1811
+ const databasePromise = requestToPromise(() => request);
1812
+ this.state.databasePromise = databasePromise;
1813
+ return databasePromise;
1814
+ };
1815
+ listSummaries = async () => {
1816
+ const database = await this.openDatabase();
1817
+ const transaction = database.transaction(this.state.storeName, 'readonly');
1818
+ const store = transaction.objectStore(this.state.storeName);
1819
+ const summaries = await requestToPromise(() => store.getAll());
1820
+ return summaries;
1821
+ };
1822
+ getSummary = async id => {
1823
+ const database = await this.openDatabase();
1824
+ const transaction = database.transaction(this.state.storeName, 'readonly');
1825
+ const store = transaction.objectStore(this.state.storeName);
1826
+ const summary = await requestToPromise(() => store.get(id));
1827
+ return summary;
1828
+ };
1829
+ getEventsBySessionId = async sessionId => {
1830
+ const database = await this.openDatabase();
1831
+ const transaction = database.transaction(this.state.eventStoreName, 'readonly');
1832
+ const store = transaction.objectStore(this.state.eventStoreName);
1833
+ const index = store.index('sessionId');
1834
+ const events = await requestToPromise(() => index.getAll(IDBKeyRange.only(sessionId)));
1835
+ return events.map(toChatViewEvent);
1836
+ };
1837
+ listEventsInternal = async () => {
1838
+ const database = await this.openDatabase();
1839
+ const transaction = database.transaction(this.state.eventStoreName, 'readonly');
1840
+ const store = transaction.objectStore(this.state.eventStoreName);
1841
+ const events = await requestToPromise(() => store.getAll());
1842
+ return events.map(toChatViewEvent);
1843
+ };
1844
+ appendEvents = async events => {
1845
+ if (events.length === 0) {
1846
+ return;
1847
+ }
1848
+ const database = await this.openDatabase();
1849
+ const transaction = database.transaction([this.state.storeName, this.state.eventStoreName], 'readwrite');
1850
+ const summaryStore = transaction.objectStore(this.state.storeName);
1851
+ const eventStore = transaction.objectStore(this.state.eventStoreName);
1852
+ for (const event of events) {
1853
+ eventStore.add(event);
1854
+ if (event.type === 'chat-session-created' || event.type === 'chat-session-title-updated') {
1855
+ summaryStore.put({
1856
+ id: event.sessionId,
1857
+ title: event.title
1858
+ });
1859
+ }
1860
+ if (event.type === 'chat-session-deleted') {
1861
+ summaryStore.delete(event.sessionId);
1943
1862
  }
1944
- } else {
1945
- // Remove old node - collect the index for later removal
1946
- indicesToRemove.push(i);
1947
1863
  }
1864
+ await transactionToPromise(() => transaction);
1865
+ };
1866
+ async appendEvent(event) {
1867
+ await this.appendEvents([event]);
1948
1868
  }
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;
1869
+ async clear() {
1870
+ const database = await this.openDatabase();
1871
+ const transaction = database.transaction([this.state.storeName, this.state.eventStoreName], 'readwrite');
1872
+ transaction.objectStore(this.state.storeName).clear();
1873
+ transaction.objectStore(this.state.eventStoreName).clear();
1874
+ await transactionToPromise(() => transaction);
1955
1875
  }
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]
1876
+ async deleteSession(id) {
1877
+ await this.appendEvent({
1878
+ sessionId: id,
1879
+ timestamp: now$1(),
1880
+ type: 'chat-session-deleted'
1962
1881
  });
1963
1882
  }
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);
1883
+ async getEvents(sessionId) {
1884
+ if (sessionId) {
1885
+ return this.getEventsBySessionId(sessionId);
1984
1886
  }
1985
- // Compare children
1986
- if (oldNode.children.length > 0 || newNode.children.length > 0) {
1987
- diffChildren(oldNode.children, newNode.children, patches);
1887
+ return this.listEventsInternal();
1888
+ }
1889
+ async getSession(id) {
1890
+ const [summary, events] = await Promise.all([this.getSummary(id), this.getEventsBySessionId(id)]);
1891
+ return replaySession$1(id, summary, events);
1892
+ }
1893
+ async listSessions() {
1894
+ const summaries = await this.listSummaries();
1895
+ const sessions = [];
1896
+ for (const summary of summaries) {
1897
+ const events = await this.getEventsBySessionId(summary.id);
1898
+ const session = replaySession$1(summary.id, summary, events);
1899
+ if (!session) {
1900
+ continue;
1901
+ }
1902
+ sessions.push(session);
1988
1903
  }
1989
- } else {
1990
- // Non-root level or multiple root elements - use the regular comparison
1991
- diffChildren(oldTree, newTree, patches);
1904
+ return sessions;
1992
1905
  }
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;
1906
+ async setSession(session) {
1907
+ const previous = await this.getSession(session.id);
1908
+ const events = getMutationEvents$1(previous, session);
1909
+ await this.appendEvents(events);
1910
+ if (events.length === 0) {
1911
+ const database = await this.openDatabase();
1912
+ const transaction = database.transaction(this.state.storeName, 'readwrite');
1913
+ const summaryStore = transaction.objectStore(this.state.storeName);
1914
+ summaryStore.put({
1915
+ id: session.id,
1916
+ title: session.title
1917
+ });
1918
+ await transactionToPromise(() => transaction);
2003
1919
  }
2004
1920
  }
2005
- // Return patches up to and including the last non-navigation patch
2006
- return lastNonNavigationIndex === -1 ? [] : patches.slice(0, lastNonNavigationIndex + 1);
2007
- };
1921
+ }
2008
1922
 
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);
1923
+ const now = () => {
1924
+ return new Date().toISOString();
2018
1925
  };
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
- }];
1926
+ const isSameMessage = (a, b) => {
1927
+ 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
1928
  };
2031
-
2032
- const getSelectedSessionId = state => {
2033
- return state.selectedSessionId;
1929
+ const canAppendMessages = (previousMessages, nextMessages) => {
1930
+ if (nextMessages.length < previousMessages.length) {
1931
+ return false;
1932
+ }
1933
+ return previousMessages.every((message, index) => isSameMessage(message, nextMessages[index]));
2034
1934
  };
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) => {
1935
+ const canUpdateMessages = (previousMessages, nextMessages) => {
2129
1936
  if (previousMessages.length !== nextMessages.length) {
2130
1937
  return false;
2131
1938
  }
@@ -2138,8 +1945,8 @@ const canUpdateMessages$1 = (previousMessages, nextMessages) => {
2138
1945
  }
2139
1946
  return true;
2140
1947
  };
2141
- const getMutationEvents$1 = (previous, next) => {
2142
- const timestamp = now$1();
1948
+ const getMutationEvents = (previous, next) => {
1949
+ const timestamp = now();
2143
1950
  const events = [];
2144
1951
  if (!previous) {
2145
1952
  events.push({
@@ -2166,7 +1973,7 @@ const getMutationEvents$1 = (previous, next) => {
2166
1973
  type: 'chat-session-title-updated'
2167
1974
  });
2168
1975
  }
2169
- if (canAppendMessages$1(previous.messages, next.messages)) {
1976
+ if (canAppendMessages(previous.messages, next.messages)) {
2170
1977
  for (let i = previous.messages.length; i < next.messages.length; i += 1) {
2171
1978
  events.push({
2172
1979
  message: next.messages[i],
@@ -2177,11 +1984,11 @@ const getMutationEvents$1 = (previous, next) => {
2177
1984
  }
2178
1985
  return events;
2179
1986
  }
2180
- if (canUpdateMessages$1(previous.messages, next.messages)) {
1987
+ if (canUpdateMessages(previous.messages, next.messages)) {
2181
1988
  for (let i = 0; i < previous.messages.length; i += 1) {
2182
1989
  const previousMessage = previous.messages[i];
2183
1990
  const nextMessage = next.messages[i];
2184
- if (!isSameMessage$1(previousMessage, nextMessage)) {
1991
+ if (!isSameMessage(previousMessage, nextMessage)) {
2185
1992
  events.push({
2186
1993
  inProgress: nextMessage.inProgress,
2187
1994
  messageId: nextMessage.id,
@@ -2204,20 +2011,17 @@ const getMutationEvents$1 = (previous, next) => {
2204
2011
  });
2205
2012
  return events;
2206
2013
  };
2207
- const replaySession$1 = (id, summary, events) => {
2014
+ const replaySession = (id, title, events) => {
2208
2015
  let deleted = false;
2209
- let title = summary?.title || '';
2210
- let messages = summary?.messages ? [...summary.messages] : [];
2016
+ let currentTitle = title || '';
2017
+ let messages = [];
2211
2018
  for (const event of events) {
2212
2019
  if (event.sessionId !== id) {
2213
2020
  continue;
2214
2021
  }
2215
2022
  if (event.type === 'chat-session-created') {
2216
- const {
2217
- title: eventTitle
2218
- } = event;
2219
2023
  deleted = false;
2220
- title = eventTitle;
2024
+ currentTitle = event.title;
2221
2025
  continue;
2222
2026
  }
2223
2027
  if (event.type === 'chat-session-deleted') {
@@ -2225,10 +2029,7 @@ const replaySession$1 = (id, summary, events) => {
2225
2029
  continue;
2226
2030
  }
2227
2031
  if (event.type === 'chat-session-title-updated') {
2228
- const {
2229
- title: eventTitle
2230
- } = event;
2231
- title = eventTitle;
2032
+ currentTitle = event.title;
2232
2033
  continue;
2233
2034
  }
2234
2035
  if (event.type === 'chat-message-added') {
@@ -2258,148 +2059,59 @@ const replaySession$1 = (id, summary, events) => {
2258
2059
  messages = [...event.messages];
2259
2060
  }
2260
2061
  }
2261
- if (deleted || !title) {
2062
+ if (deleted || !currentTitle) {
2262
2063
  return undefined;
2263
2064
  }
2264
2065
  return {
2265
2066
  id,
2266
2067
  messages,
2267
- title
2068
+ title: currentTitle
2268
2069
  };
2269
2070
  };
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) {
2071
+ class InMemoryChatSessionStorage {
2072
+ events = [];
2073
+ summaries = new Map();
2074
+ async appendEvent(event) {
2075
+ this.events.push(event);
2076
+ if (event.type === 'chat-session-created' || event.type === 'chat-session-title-updated') {
2077
+ this.summaries.set(event.sessionId, event.title);
2350
2078
  return;
2351
2079
  }
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
- }
2080
+ if (event.type === 'chat-session-deleted') {
2081
+ this.summaries.delete(event.sessionId);
2367
2082
  }
2368
- await transactionToPromise(() => transaction);
2369
- };
2370
- async appendEvent(event) {
2371
- await this.appendEvents([event]);
2372
2083
  }
2373
2084
  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);
2085
+ this.events.length = 0;
2086
+ this.summaries.clear();
2379
2087
  }
2380
2088
  async deleteSession(id) {
2381
2089
  await this.appendEvent({
2382
2090
  sessionId: id,
2383
- timestamp: now$1(),
2091
+ timestamp: now(),
2384
2092
  type: 'chat-session-deleted'
2385
2093
  });
2386
2094
  }
2387
2095
  async getEvents(sessionId) {
2388
- if (sessionId) {
2389
- return this.getEventsBySessionId(sessionId);
2096
+ if (!sessionId) {
2097
+ return [...this.events];
2390
2098
  }
2391
- return this.listEventsInternal();
2099
+ return this.events.filter(event => event.sessionId === sessionId);
2392
2100
  }
2393
2101
  async getSession(id) {
2394
- const [summary, events] = await Promise.all([this.getSummary(id), this.getEventsBySessionId(id)]);
2395
- return replaySession$1(id, summary, events);
2102
+ return replaySession(id, this.summaries.get(id), this.events);
2396
2103
  }
2397
2104
  async listSessions() {
2398
- const summaries = await this.listSummaries();
2105
+ const ids = new Set();
2106
+ for (const id of this.summaries.keys()) {
2107
+ ids.add(id);
2108
+ }
2109
+ for (const event of this.events) {
2110
+ ids.add(event.sessionId);
2111
+ }
2399
2112
  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);
2113
+ for (const id of ids) {
2114
+ const session = replaySession(id, this.summaries.get(id), this.events);
2403
2115
  if (!session) {
2404
2116
  continue;
2405
2117
  }
@@ -2409,271 +2121,626 @@ class IndexedDbChatSessionStorage {
2409
2121
  }
2410
2122
  async setSession(session) {
2411
2123
  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);
2124
+ const events = getMutationEvents(previous, session);
2125
+ for (const event of events) {
2126
+ await this.appendEvent(event);
2423
2127
  }
2128
+ this.summaries.set(session.id, session.title);
2424
2129
  }
2425
2130
  }
2426
2131
 
2427
- const now = () => {
2428
- return new Date().toISOString();
2132
+ const createDefaultStorage = () => {
2133
+ if (typeof indexedDB === 'undefined') {
2134
+ return new InMemoryChatSessionStorage();
2135
+ }
2136
+ return new IndexedDbChatSessionStorage();
2429
2137
  };
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 || []);
2138
+ let chatSessionStorage = createDefaultStorage();
2139
+ const listChatSessions = async () => {
2140
+ const sessions = await chatSessionStorage.listSessions();
2141
+ return sessions.map(session => ({
2142
+ id: session.id,
2143
+ messages: [],
2144
+ title: session.title
2145
+ }));
2432
2146
  };
2433
- const canAppendMessages = (previousMessages, nextMessages) => {
2434
- if (nextMessages.length < previousMessages.length) {
2435
- return false;
2147
+ const getChatSession = async id => {
2148
+ const session = await chatSessionStorage.getSession(id);
2149
+ if (!session) {
2150
+ return undefined;
2436
2151
  }
2437
- return previousMessages.every((message, index) => isSameMessage(message, nextMessages[index]));
2152
+ return {
2153
+ id: session.id,
2154
+ messages: [...session.messages],
2155
+ title: session.title
2156
+ };
2438
2157
  };
2439
- const canUpdateMessages = (previousMessages, nextMessages) => {
2440
- if (previousMessages.length !== nextMessages.length) {
2441
- return false;
2158
+ const saveChatSession = async session => {
2159
+ await chatSessionStorage.setSession({
2160
+ id: session.id,
2161
+ messages: [...session.messages],
2162
+ title: session.title
2163
+ });
2164
+ };
2165
+ const deleteChatSession = async id => {
2166
+ await chatSessionStorage.deleteSession(id);
2167
+ };
2168
+ const clearChatSessions = async () => {
2169
+ await chatSessionStorage.clear();
2170
+ };
2171
+ const appendChatViewEvent = async event => {
2172
+ await chatSessionStorage.appendEvent(event);
2173
+ };
2174
+
2175
+ const getNextSelectedSessionId = (sessions, deletedId) => {
2176
+ if (sessions.length === 0) {
2177
+ return '';
2442
2178
  }
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
- }
2179
+ const index = sessions.findIndex(session => session.id === deletedId);
2180
+ if (index === -1) {
2181
+ return sessions[0].id;
2449
2182
  }
2450
- return true;
2183
+ const nextIndex = Math.min(index, sessions.length - 1);
2184
+ return sessions[nextIndex].id;
2451
2185
  };
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;
2186
+
2187
+ const deleteSession = async (state, id) => {
2188
+ const {
2189
+ renamingSessionId,
2190
+ sessions
2191
+ } = state;
2192
+ const filtered = sessions.filter(session => session.id !== id);
2193
+ if (filtered.length === sessions.length) {
2194
+ return state;
2471
2195
  }
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
- });
2196
+ await deleteChatSession(id);
2197
+ if (filtered.length === 0) {
2198
+ return {
2199
+ ...state,
2200
+ renamingSessionId: '',
2201
+ selectedSessionId: '',
2202
+ sessions: [],
2203
+ viewMode: 'list'
2204
+ };
2479
2205
  }
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
- });
2206
+ const nextSelectedSessionId = getNextSelectedSessionId(filtered, id);
2207
+ const loadedSession = await getChatSession(nextSelectedSessionId);
2208
+ const hydratedSessions = filtered.map(session => {
2209
+ if (session.id !== nextSelectedSessionId) {
2210
+ return session;
2488
2211
  }
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
- }
2212
+ if (!loadedSession) {
2213
+ return session;
2507
2214
  }
2508
- return events;
2509
- }
2510
- events.push({
2511
- messages: [...next.messages],
2512
- sessionId: next.id,
2513
- timestamp,
2514
- type: 'chat-session-messages-replaced'
2215
+ return loadedSession;
2515
2216
  });
2516
- return events;
2217
+ return {
2218
+ ...state,
2219
+ renamingSessionId: renamingSessionId === id ? '' : renamingSessionId,
2220
+ selectedSessionId: nextSelectedSessionId,
2221
+ sessions: hydratedSessions
2222
+ };
2517
2223
  };
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;
2224
+ const deleteSessionAtIndex = async (state, index) => {
2225
+ const {
2226
+ sessions
2227
+ } = state;
2228
+ const session = sessions[index];
2229
+ if (!session) {
2230
+ return state;
2231
+ }
2232
+ return deleteSession(state, session.id);
2233
+ };
2234
+
2235
+ const parseRenderHtmlArguments = rawArguments => {
2236
+ try {
2237
+ const parsed = JSON.parse(rawArguments);
2238
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
2239
+ return undefined;
2534
2240
  }
2535
- if (event.type === 'chat-session-title-updated') {
2536
- currentTitle = event.title;
2537
- continue;
2241
+ const html = typeof Reflect.get(parsed, 'html') === 'string' ? String(Reflect.get(parsed, 'html')) : '';
2242
+ if (!html) {
2243
+ return undefined;
2538
2244
  }
2539
- if (event.type === 'chat-message-added') {
2540
- messages = [...messages, event.message];
2245
+ const css = typeof Reflect.get(parsed, 'css') === 'string' ? String(Reflect.get(parsed, 'css')) : '';
2246
+ const title = typeof Reflect.get(parsed, 'title') === 'string' ? String(Reflect.get(parsed, 'title')) : 'visual preview';
2247
+ return {
2248
+ css,
2249
+ html,
2250
+ title
2251
+ };
2252
+ } catch {
2253
+ return undefined;
2254
+ }
2255
+ };
2256
+
2257
+ const getRenderHtmlCss = (sessions, selectedSessionId) => {
2258
+ const selectedSession = sessions.find(session => session.id === selectedSessionId);
2259
+ if (!selectedSession) {
2260
+ return '';
2261
+ }
2262
+ const cssRules = new Set();
2263
+ for (const message of selectedSession.messages) {
2264
+ if (message.role !== 'assistant' || !message.toolCalls) {
2541
2265
  continue;
2542
2266
  }
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;
2267
+ for (const toolCall of message.toolCalls) {
2268
+ if (toolCall.name !== 'render_html') {
2269
+ continue;
2270
+ }
2271
+ const parsed = parseRenderHtmlArguments(toolCall.arguments);
2272
+ if (!parsed || !parsed.css.trim()) {
2273
+ continue;
2274
+ }
2275
+ cssRules.add(parsed.css);
2561
2276
  }
2562
- if (event.type === 'chat-session-messages-replaced') {
2563
- messages = [...event.messages];
2277
+ }
2278
+ return [...cssRules].join('\n\n');
2279
+ };
2280
+
2281
+ const isEqual$1 = (oldState, newState) => {
2282
+ const oldRenderHtmlCss = getRenderHtmlCss(oldState.sessions, oldState.selectedSessionId);
2283
+ const newRenderHtmlCss = getRenderHtmlCss(newState.sessions, newState.selectedSessionId);
2284
+ 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;
2285
+ };
2286
+
2287
+ const diffFocus = (oldState, newState) => {
2288
+ if (!newState.focused) {
2289
+ return true;
2290
+ }
2291
+ return oldState.focus === newState.focus && oldState.focused === newState.focused;
2292
+ };
2293
+
2294
+ const isEqual = (oldState, newState) => {
2295
+ 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;
2296
+ };
2297
+
2298
+ const diffScrollTop = (oldState, newState) => {
2299
+ return oldState.chatListScrollTop === newState.chatListScrollTop && oldState.messagesScrollTop === newState.messagesScrollTop;
2300
+ };
2301
+
2302
+ const RenderItems = 4;
2303
+ const RenderFocus = 6;
2304
+ const RenderFocusContext = 7;
2305
+ const RenderValue = 8;
2306
+ const RenderCss = 10;
2307
+ const RenderIncremental = 11;
2308
+ const RenderScrollTop = 12;
2309
+
2310
+ const diffValue = (oldState, newState) => {
2311
+ if (oldState.composerValue === newState.composerValue) {
2312
+ return true;
2313
+ }
2314
+ return newState.inputSource !== 'script';
2315
+ };
2316
+
2317
+ const modules = [isEqual, diffValue, diffFocus, isEqual$1, diffFocus, diffScrollTop];
2318
+ const numbers = [RenderIncremental, RenderValue, RenderFocus, RenderCss, RenderFocusContext, RenderScrollTop];
2319
+
2320
+ const diff = (oldState, newState) => {
2321
+ const diffResult = [];
2322
+ for (let i = 0; i < modules.length; i++) {
2323
+ const fn = modules[i];
2324
+ if (!fn(oldState, newState)) {
2325
+ diffResult.push(numbers[i]);
2564
2326
  }
2565
2327
  }
2566
- if (deleted || !currentTitle) {
2567
- return undefined;
2328
+ return diffResult;
2329
+ };
2330
+
2331
+ const diff2 = uid => {
2332
+ const {
2333
+ newState,
2334
+ oldState
2335
+ } = get$1(uid);
2336
+ const result = diff(oldState, newState);
2337
+ return result;
2338
+ };
2339
+
2340
+ const Button$2 = 'button';
2341
+
2342
+ const Audio = 0;
2343
+ const Button$1 = 1;
2344
+ const Col = 2;
2345
+ const ColGroup = 3;
2346
+ const Div = 4;
2347
+ const H1 = 5;
2348
+ const Input = 6;
2349
+ const Span = 8;
2350
+ const Table = 9;
2351
+ const TBody = 10;
2352
+ const Td = 11;
2353
+ const Text = 12;
2354
+ const Th = 13;
2355
+ const THead = 14;
2356
+ const Tr = 15;
2357
+ const I = 16;
2358
+ const Img = 17;
2359
+ const H2 = 22;
2360
+ const H3 = 23;
2361
+ const H4 = 24;
2362
+ const H5 = 25;
2363
+ const H6 = 26;
2364
+ const Article = 27;
2365
+ const Aside = 28;
2366
+ const Footer = 29;
2367
+ const Header = 30;
2368
+ const Nav = 40;
2369
+ const Section = 41;
2370
+ const Dd = 43;
2371
+ const Dl = 44;
2372
+ const Figcaption = 45;
2373
+ const Figure = 46;
2374
+ const Hr = 47;
2375
+ const Li = 48;
2376
+ const Ol = 49;
2377
+ const P = 50;
2378
+ const Pre = 51;
2379
+ const A = 53;
2380
+ const Abbr = 54;
2381
+ const Br = 55;
2382
+ const Tfoot = 59;
2383
+ const Ul = 60;
2384
+ const TextArea = 62;
2385
+ const Select$1 = 63;
2386
+ const Option$1 = 64;
2387
+ const Code = 65;
2388
+ const Label$1 = 66;
2389
+ const Dt = 67;
2390
+ const Main = 69;
2391
+ const Strong = 70;
2392
+ const Em = 71;
2393
+ const Reference = 100;
2394
+
2395
+ const Enter = 3;
2396
+
2397
+ const Shift = 1 << 10 >>> 0;
2398
+
2399
+ const mergeClassNames = (...classNames) => {
2400
+ return classNames.filter(Boolean).join(' ');
2401
+ };
2402
+
2403
+ const text = data => {
2404
+ return {
2405
+ childCount: 0,
2406
+ text: data,
2407
+ type: Text
2408
+ };
2409
+ };
2410
+
2411
+ const SetText = 1;
2412
+ const Replace = 2;
2413
+ const SetAttribute = 3;
2414
+ const RemoveAttribute = 4;
2415
+ const Add = 6;
2416
+ const NavigateChild = 7;
2417
+ const NavigateParent = 8;
2418
+ const RemoveChild = 9;
2419
+ const NavigateSibling = 10;
2420
+ const SetReferenceNodeUid = 11;
2421
+
2422
+ const isKey = key => {
2423
+ return key !== 'type' && key !== 'childCount';
2424
+ };
2425
+
2426
+ const getKeys = node => {
2427
+ const keys = Object.keys(node).filter(isKey);
2428
+ return keys;
2429
+ };
2430
+
2431
+ const arrayToTree = nodes => {
2432
+ const result = [];
2433
+ let i = 0;
2434
+ while (i < nodes.length) {
2435
+ const node = nodes[i];
2436
+ const {
2437
+ children,
2438
+ nodesConsumed
2439
+ } = getChildrenWithCount(nodes, i + 1, node.childCount || 0);
2440
+ result.push({
2441
+ node,
2442
+ children
2443
+ });
2444
+ i += 1 + nodesConsumed;
2445
+ }
2446
+ return result;
2447
+ };
2448
+ const getChildrenWithCount = (nodes, startIndex, childCount) => {
2449
+ if (childCount === 0) {
2450
+ return {
2451
+ children: [],
2452
+ nodesConsumed: 0
2453
+ };
2454
+ }
2455
+ const children = [];
2456
+ let i = startIndex;
2457
+ let remaining = childCount;
2458
+ let totalConsumed = 0;
2459
+ while (remaining > 0 && i < nodes.length) {
2460
+ const node = nodes[i];
2461
+ const nodeChildCount = node.childCount || 0;
2462
+ const {
2463
+ children: nodeChildren,
2464
+ nodesConsumed
2465
+ } = getChildrenWithCount(nodes, i + 1, nodeChildCount);
2466
+ children.push({
2467
+ node,
2468
+ children: nodeChildren
2469
+ });
2470
+ const nodeSize = 1 + nodesConsumed;
2471
+ i += nodeSize;
2472
+ totalConsumed += nodeSize;
2473
+ remaining--;
2568
2474
  }
2569
2475
  return {
2570
- id,
2571
- messages,
2572
- title: currentTitle
2476
+ children,
2477
+ nodesConsumed: totalConsumed
2573
2478
  };
2574
2479
  };
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);
2480
+
2481
+ const compareNodes = (oldNode, newNode) => {
2482
+ const patches = [];
2483
+ // Check if node type changed - return null to signal incompatible nodes
2484
+ // (caller should handle this with a Replace operation)
2485
+ if (oldNode.type !== newNode.type) {
2486
+ return null;
2487
+ }
2488
+ // Handle reference nodes - special handling for uid changes
2489
+ if (oldNode.type === Reference) {
2490
+ if (oldNode.uid !== newNode.uid) {
2491
+ patches.push({
2492
+ type: SetReferenceNodeUid,
2493
+ uid: newNode.uid
2494
+ });
2586
2495
  }
2496
+ return patches;
2587
2497
  }
2588
- async clear() {
2589
- this.events.length = 0;
2590
- this.summaries.clear();
2498
+ // Handle text nodes
2499
+ if (oldNode.type === Text && newNode.type === Text) {
2500
+ if (oldNode.text !== newNode.text) {
2501
+ patches.push({
2502
+ type: SetText,
2503
+ value: newNode.text
2504
+ });
2505
+ }
2506
+ return patches;
2591
2507
  }
2592
- async deleteSession(id) {
2593
- await this.appendEvent({
2594
- sessionId: id,
2595
- timestamp: now(),
2596
- type: 'chat-session-deleted'
2597
- });
2508
+ // Compare attributes
2509
+ const oldKeys = getKeys(oldNode);
2510
+ const newKeys = getKeys(newNode);
2511
+ // Check for attribute changes
2512
+ for (const key of newKeys) {
2513
+ if (oldNode[key] !== newNode[key]) {
2514
+ patches.push({
2515
+ type: SetAttribute,
2516
+ key,
2517
+ value: newNode[key]
2518
+ });
2519
+ }
2598
2520
  }
2599
- async getEvents(sessionId) {
2600
- if (!sessionId) {
2601
- return [...this.events];
2521
+ // Check for removed attributes
2522
+ for (const key of oldKeys) {
2523
+ if (!(key in newNode)) {
2524
+ patches.push({
2525
+ type: RemoveAttribute,
2526
+ key
2527
+ });
2602
2528
  }
2603
- return this.events.filter(event => event.sessionId === sessionId);
2604
2529
  }
2605
- async getSession(id) {
2606
- return replaySession(id, this.summaries.get(id), this.events);
2530
+ return patches;
2531
+ };
2532
+
2533
+ const treeToArray = node => {
2534
+ const result = [node.node];
2535
+ for (const child of node.children) {
2536
+ result.push(...treeToArray(child));
2607
2537
  }
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);
2538
+ return result;
2539
+ };
2540
+
2541
+ const diffChildren = (oldChildren, newChildren, patches) => {
2542
+ const maxLength = Math.max(oldChildren.length, newChildren.length);
2543
+ // Track where we are: -1 means at parent, >= 0 means at child index
2544
+ let currentChildIndex = -1;
2545
+ // Collect indices of children to remove (we'll add these patches at the end in reverse order)
2546
+ const indicesToRemove = [];
2547
+ for (let i = 0; i < maxLength; i++) {
2548
+ const oldNode = oldChildren[i];
2549
+ const newNode = newChildren[i];
2550
+ if (!oldNode && !newNode) {
2551
+ continue;
2615
2552
  }
2616
- const sessions = [];
2617
- for (const id of ids) {
2618
- const session = replaySession(id, this.summaries.get(id), this.events);
2619
- if (!session) {
2553
+ if (!oldNode) {
2554
+ // Add new node - we should be at the parent
2555
+ if (currentChildIndex >= 0) {
2556
+ // Navigate back to parent
2557
+ patches.push({
2558
+ type: NavigateParent
2559
+ });
2560
+ currentChildIndex = -1;
2561
+ }
2562
+ // Flatten the entire subtree so renderInternal can handle it
2563
+ const flatNodes = treeToArray(newNode);
2564
+ patches.push({
2565
+ type: Add,
2566
+ nodes: flatNodes
2567
+ });
2568
+ } else if (newNode) {
2569
+ // Compare nodes to see if we need any patches
2570
+ const nodePatches = compareNodes(oldNode.node, newNode.node);
2571
+ // If nodePatches is null, the node types are incompatible - need to replace
2572
+ if (nodePatches === null) {
2573
+ // Navigate to this child
2574
+ if (currentChildIndex === -1) {
2575
+ patches.push({
2576
+ type: NavigateChild,
2577
+ index: i
2578
+ });
2579
+ currentChildIndex = i;
2580
+ } else if (currentChildIndex !== i) {
2581
+ patches.push({
2582
+ type: NavigateSibling,
2583
+ index: i
2584
+ });
2585
+ currentChildIndex = i;
2586
+ }
2587
+ // Replace the entire subtree
2588
+ const flatNodes = treeToArray(newNode);
2589
+ patches.push({
2590
+ type: Replace,
2591
+ nodes: flatNodes
2592
+ });
2593
+ // After replace, we're at the new element (same position)
2620
2594
  continue;
2621
2595
  }
2622
- sessions.push(session);
2596
+ // Check if we need to recurse into children
2597
+ const hasChildrenToCompare = oldNode.children.length > 0 || newNode.children.length > 0;
2598
+ // Only navigate to this element if we need to do something
2599
+ if (nodePatches.length > 0 || hasChildrenToCompare) {
2600
+ // Navigate to this child if not already there
2601
+ if (currentChildIndex === -1) {
2602
+ patches.push({
2603
+ type: NavigateChild,
2604
+ index: i
2605
+ });
2606
+ currentChildIndex = i;
2607
+ } else if (currentChildIndex !== i) {
2608
+ patches.push({
2609
+ type: NavigateSibling,
2610
+ index: i
2611
+ });
2612
+ currentChildIndex = i;
2613
+ }
2614
+ // Apply node patches (these apply to the current element, not children)
2615
+ if (nodePatches.length > 0) {
2616
+ patches.push(...nodePatches);
2617
+ }
2618
+ // Compare children recursively
2619
+ if (hasChildrenToCompare) {
2620
+ diffChildren(oldNode.children, newNode.children, patches);
2621
+ }
2622
+ }
2623
+ } else {
2624
+ // Remove old node - collect the index for later removal
2625
+ indicesToRemove.push(i);
2623
2626
  }
2624
- return sessions;
2625
2627
  }
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);
2628
+ // Navigate back to parent if we ended at a child
2629
+ if (currentChildIndex >= 0) {
2630
+ patches.push({
2631
+ type: NavigateParent
2632
+ });
2633
+ currentChildIndex = -1;
2633
2634
  }
2634
- }
2635
-
2636
- const createDefaultStorage = () => {
2637
- if (typeof indexedDB === 'undefined') {
2638
- return new InMemoryChatSessionStorage();
2635
+ // Add remove patches in reverse order (highest index first)
2636
+ // This ensures indices remain valid as we remove
2637
+ for (let j = indicesToRemove.length - 1; j >= 0; j--) {
2638
+ patches.push({
2639
+ type: RemoveChild,
2640
+ index: indicesToRemove[j]
2641
+ });
2639
2642
  }
2640
- return new IndexedDbChatSessionStorage();
2641
2643
  };
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
- }));
2644
+ const diffTrees = (oldTree, newTree, patches, path) => {
2645
+ // At the root level (path.length === 0), we're already AT the element
2646
+ // So we compare the root node directly, then compare its children
2647
+ if (path.length === 0 && oldTree.length === 1 && newTree.length === 1) {
2648
+ const oldNode = oldTree[0];
2649
+ const newNode = newTree[0];
2650
+ // Compare root nodes
2651
+ const nodePatches = compareNodes(oldNode.node, newNode.node);
2652
+ // If nodePatches is null, the root node types are incompatible - need to replace
2653
+ if (nodePatches === null) {
2654
+ const flatNodes = treeToArray(newNode);
2655
+ patches.push({
2656
+ type: Replace,
2657
+ nodes: flatNodes
2658
+ });
2659
+ return;
2660
+ }
2661
+ if (nodePatches.length > 0) {
2662
+ patches.push(...nodePatches);
2663
+ }
2664
+ // Compare children
2665
+ if (oldNode.children.length > 0 || newNode.children.length > 0) {
2666
+ diffChildren(oldNode.children, newNode.children, patches);
2667
+ }
2668
+ } else {
2669
+ // Non-root level or multiple root elements - use the regular comparison
2670
+ diffChildren(oldTree, newTree, patches);
2671
+ }
2650
2672
  };
2651
- const getChatSession = async id => {
2652
- const session = await chatSessionStorage.getSession(id);
2653
- if (!session) {
2654
- return undefined;
2673
+
2674
+ const removeTrailingNavigationPatches = patches => {
2675
+ // Find the last non-navigation patch
2676
+ let lastNonNavigationIndex = -1;
2677
+ for (let i = patches.length - 1; i >= 0; i--) {
2678
+ const patch = patches[i];
2679
+ if (patch.type !== NavigateChild && patch.type !== NavigateParent && patch.type !== NavigateSibling) {
2680
+ lastNonNavigationIndex = i;
2681
+ break;
2682
+ }
2655
2683
  }
2656
- return {
2657
- id: session.id,
2658
- messages: [...session.messages],
2659
- title: session.title
2660
- };
2684
+ // Return patches up to and including the last non-navigation patch
2685
+ return lastNonNavigationIndex === -1 ? [] : patches.slice(0, lastNonNavigationIndex + 1);
2661
2686
  };
2662
- const saveChatSession = async session => {
2663
- await chatSessionStorage.setSession({
2664
- id: session.id,
2665
- messages: [...session.messages],
2666
- title: session.title
2667
- });
2687
+
2688
+ const diffTree = (oldNodes, newNodes) => {
2689
+ // Step 1: Convert flat arrays to tree structures
2690
+ const oldTree = arrayToTree(oldNodes);
2691
+ const newTree = arrayToTree(newNodes);
2692
+ // Step 3: Compare the trees
2693
+ const patches = [];
2694
+ diffTrees(oldTree, newTree, patches, []);
2695
+ // Remove trailing navigation patches since they serve no purpose
2696
+ return removeTrailingNavigationPatches(patches);
2668
2697
  };
2669
- const deleteChatSession = async id => {
2670
- await chatSessionStorage.deleteSession(id);
2698
+
2699
+ const getKeyBindings = () => {
2700
+ return [{
2701
+ command: 'Chat.handleSubmit',
2702
+ key: Enter,
2703
+ when: FocusChatInput
2704
+ }, {
2705
+ command: 'Chat.enterNewLine',
2706
+ key: Shift | Enter,
2707
+ when: FocusChatInput
2708
+ }];
2671
2709
  };
2672
- const clearChatSessions = async () => {
2673
- await chatSessionStorage.clear();
2710
+
2711
+ const getSelectedSessionId = state => {
2712
+ return state.selectedSessionId;
2674
2713
  };
2675
- const appendChatViewEvent = async event => {
2676
- await chatSessionStorage.appendEvent(event);
2714
+
2715
+ const getListIndex = (state, eventX, eventY) => {
2716
+ const {
2717
+ headerHeight,
2718
+ height,
2719
+ listItemHeight,
2720
+ width,
2721
+ x,
2722
+ y
2723
+ } = state;
2724
+ const relativeX = eventX - x;
2725
+ const relativeY = eventY - y - headerHeight;
2726
+ if (relativeX < 0 || relativeY < 0 || relativeX >= width || relativeY >= height - headerHeight) {
2727
+ return -1;
2728
+ }
2729
+ return Math.floor(relativeY / listItemHeight);
2730
+ };
2731
+
2732
+ const CHAT_LIST_ITEM_CONTEXT_MENU = 'ChatListItemContextMenu';
2733
+ const handleChatListContextMenu = async (state, eventX, eventY) => {
2734
+ const index = getListIndex(state, eventX, eventY);
2735
+ if (index === -1) {
2736
+ return state;
2737
+ }
2738
+ const item = state.sessions[index];
2739
+ if (!item) {
2740
+ return state;
2741
+ }
2742
+ await invoke('ContextMenu.show', eventX, eventY, CHAT_LIST_ITEM_CONTEXT_MENU, item.id);
2743
+ return state;
2677
2744
  };
2678
2745
 
2679
2746
  const generateSessionId = () => {
@@ -2696,56 +2763,6 @@ const createSession = async state => {
2696
2763
  };
2697
2764
  };
2698
2765
 
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
2766
  const handleClickOpenApiApiKeySettings = async state => {
2750
2767
  await invoke('Main.openUri', 'app://settings.json');
2751
2768
  return state;
@@ -3162,6 +3179,13 @@ const getMockOpenRouterAssistantText = async (messages, modelId, openRouterApiBa
3162
3179
  }
3163
3180
  };
3164
3181
 
3182
+ const makeApiRequest = async options => {
3183
+ return invoke$2('ChatNetwork.makeApiRequest', options);
3184
+ };
3185
+ const makeStreamingApiRequest = async options => {
3186
+ return invoke$2('ChatNetwork.makeStreamingApiRequest', options);
3187
+ };
3188
+
3165
3189
  const executeGetWorkspaceUriTool = async (_args, _options) => {
3166
3190
  try {
3167
3191
  const workspaceUri = await getWorkspacePath();
@@ -3506,6 +3530,8 @@ const getTextContent = content => {
3506
3530
  return textParts.join('\n');
3507
3531
  };
3508
3532
 
3533
+ /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
3534
+
3509
3535
  const getOpenAiTools = tools => {
3510
3536
  return tools.map(tool => {
3511
3537
  if (!tool || typeof tool !== 'object') {
@@ -4047,6 +4073,52 @@ const getOpenApiErrorDetails = async response => {
4047
4073
  } : {})
4048
4074
  };
4049
4075
  };
4076
+ const getOpenApiErrorDetailsFromResponseText = responseText => {
4077
+ let parsed;
4078
+ try {
4079
+ parsed = JSON.parse(responseText);
4080
+ } catch {
4081
+ return {};
4082
+ }
4083
+ if (!parsed || typeof parsed !== 'object') {
4084
+ return {};
4085
+ }
4086
+ const error = Reflect.get(parsed, 'error');
4087
+ if (!error || typeof error !== 'object') {
4088
+ return {};
4089
+ }
4090
+ const errorCode = Reflect.get(error, 'code');
4091
+ const errorMessage = Reflect.get(error, 'message');
4092
+ const errorType = Reflect.get(error, 'type');
4093
+ return {
4094
+ ...(typeof errorCode === 'string' ? {
4095
+ errorCode
4096
+ } : {}),
4097
+ ...(typeof errorMessage === 'string' ? {
4098
+ errorMessage
4099
+ } : {}),
4100
+ ...(typeof errorType === 'string' ? {
4101
+ errorType
4102
+ } : {})
4103
+ };
4104
+ };
4105
+ const getResponseFromSseEvents = events => {
4106
+ const chunks = events.map(event => {
4107
+ const data = typeof event === 'string' ? event : JSON.stringify(event);
4108
+ return `data: ${data}\n\n`;
4109
+ });
4110
+ const stream = new ReadableStream({
4111
+ start(controller) {
4112
+ for (const chunk of chunks) {
4113
+ controller.enqueue(new TextEncoder().encode(chunk));
4114
+ }
4115
+ controller.close();
4116
+ }
4117
+ });
4118
+ return {
4119
+ body: stream
4120
+ };
4121
+ };
4050
4122
  const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApiApiBaseUrl, assetDir, platform, options) => {
4051
4123
  const {
4052
4124
  includeObfuscation = false,
@@ -4055,6 +4127,7 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
4055
4127
  onTextChunk,
4056
4128
  onToolCallsChunk,
4057
4129
  stream,
4130
+ useChatNetworkWorkerForRequests = false,
4058
4131
  webSearchEnabled = false
4059
4132
  } = options ?? {
4060
4133
  stream: false
@@ -4067,46 +4140,89 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
4067
4140
  const maxToolIterations = 4;
4068
4141
  let previousResponseId;
4069
4142
  for (let i = 0; i <= maxToolIterations; i++) {
4070
- let response;
4071
- try {
4072
- response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl), {
4073
- body: JSON.stringify(getOpenAiParams(openAiInput, modelId, stream, includeObfuscation, tools, webSearchEnabled, previousResponseId)),
4074
- headers: {
4075
- Authorization: `Bearer ${openApiApiKey}`,
4076
- 'Content-Type': 'application/json',
4077
- ...getClientRequestIdHeader()
4078
- },
4079
- method: 'POST'
4080
- });
4081
- } catch {
4082
- return {
4083
- details: 'request-failed',
4084
- type: 'error'
4085
- };
4086
- }
4087
- if (!response.ok) {
4088
- const {
4089
- errorCode,
4090
- errorMessage,
4091
- errorType
4092
- } = await getOpenApiErrorDetails(response);
4093
- return {
4094
- details: 'http-error',
4095
- ...(errorCode ? {
4096
- errorCode
4097
- } : {}),
4098
- ...(errorMessage ? {
4099
- errorMessage
4100
- } : {}),
4101
- ...(errorType ? {
4102
- errorType
4103
- } : {}),
4104
- statusCode: response.status,
4105
- type: 'error'
4106
- };
4107
- }
4143
+ const postBody = getOpenAiParams(openAiInput, modelId, stream, includeObfuscation, tools, webSearchEnabled, previousResponseId);
4108
4144
  if (stream) {
4109
- const streamResult = await parseOpenApiStream(response, onTextChunk, onToolCallsChunk, onDataEvent);
4145
+ const streamResult = useChatNetworkWorkerForRequests ? await (async () => {
4146
+ const requestResult = await makeStreamingApiRequest({
4147
+ headers: {
4148
+ Authorization: `Bearer ${openApiApiKey}`,
4149
+ 'Content-Type': 'application/json',
4150
+ ...getClientRequestIdHeader()
4151
+ },
4152
+ method: 'POST',
4153
+ postBody,
4154
+ url: getOpenApiApiEndpoint(openApiApiBaseUrl)
4155
+ });
4156
+ if (requestResult.type === 'error') {
4157
+ if (requestResult.statusCode === 0) {
4158
+ return {
4159
+ details: 'request-failed',
4160
+ type: 'error'
4161
+ };
4162
+ }
4163
+ const {
4164
+ errorCode,
4165
+ errorMessage,
4166
+ errorType
4167
+ } = getOpenApiErrorDetailsFromResponseText(requestResult.response);
4168
+ return {
4169
+ details: 'http-error',
4170
+ ...(errorCode ? {
4171
+ errorCode
4172
+ } : {}),
4173
+ ...(errorMessage ? {
4174
+ errorMessage
4175
+ } : {}),
4176
+ ...(errorType ? {
4177
+ errorType
4178
+ } : {}),
4179
+ statusCode: requestResult.statusCode,
4180
+ type: 'error'
4181
+ };
4182
+ }
4183
+ const response = getResponseFromSseEvents(requestResult.body);
4184
+ return parseOpenApiStream(response, onTextChunk, onToolCallsChunk, onDataEvent);
4185
+ })() : await (async () => {
4186
+ let response;
4187
+ try {
4188
+ response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl), {
4189
+ body: JSON.stringify(postBody),
4190
+ headers: {
4191
+ Authorization: `Bearer ${openApiApiKey}`,
4192
+ 'Content-Type': 'application/json',
4193
+ ...getClientRequestIdHeader()
4194
+ },
4195
+ method: 'POST'
4196
+ });
4197
+ } catch {
4198
+ return {
4199
+ details: 'request-failed',
4200
+ type: 'error'
4201
+ };
4202
+ }
4203
+ if (!response.ok) {
4204
+ const {
4205
+ errorCode,
4206
+ errorMessage,
4207
+ errorType
4208
+ } = await getOpenApiErrorDetails(response);
4209
+ return {
4210
+ details: 'http-error',
4211
+ ...(errorCode ? {
4212
+ errorCode
4213
+ } : {}),
4214
+ ...(errorMessage ? {
4215
+ errorMessage
4216
+ } : {}),
4217
+ ...(errorType ? {
4218
+ errorType
4219
+ } : {}),
4220
+ statusCode: response.status,
4221
+ type: 'error'
4222
+ };
4223
+ }
4224
+ return parseOpenApiStream(response, onTextChunk, onToolCallsChunk, onDataEvent);
4225
+ })();
4110
4226
  if (streamResult.type !== 'success') {
4111
4227
  return streamResult;
4112
4228
  }
@@ -4147,19 +4263,98 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
4147
4263
  if (onEventStreamFinished) {
4148
4264
  await onEventStreamFinished();
4149
4265
  }
4150
- return {
4151
- text: streamResult.text,
4152
- type: 'success'
4153
- };
4154
- }
4155
- let parsed;
4156
- try {
4157
- parsed = await response.json();
4158
- } catch {
4159
- return {
4160
- details: 'request-failed',
4161
- type: 'error'
4162
- };
4266
+ return {
4267
+ text: streamResult.text,
4268
+ type: 'success'
4269
+ };
4270
+ }
4271
+ let parsed;
4272
+ if (useChatNetworkWorkerForRequests) {
4273
+ const requestResult = await makeApiRequest({
4274
+ headers: {
4275
+ Authorization: `Bearer ${openApiApiKey}`,
4276
+ 'Content-Type': 'application/json',
4277
+ ...getClientRequestIdHeader()
4278
+ },
4279
+ method: 'POST',
4280
+ postBody,
4281
+ url: getOpenApiApiEndpoint(openApiApiBaseUrl)
4282
+ });
4283
+ if (requestResult.type === 'error') {
4284
+ if (requestResult.statusCode === 0) {
4285
+ return {
4286
+ details: 'request-failed',
4287
+ type: 'error'
4288
+ };
4289
+ }
4290
+ const {
4291
+ errorCode,
4292
+ errorMessage,
4293
+ errorType
4294
+ } = getOpenApiErrorDetailsFromResponseText(requestResult.response);
4295
+ return {
4296
+ details: 'http-error',
4297
+ ...(errorCode ? {
4298
+ errorCode
4299
+ } : {}),
4300
+ ...(errorMessage ? {
4301
+ errorMessage
4302
+ } : {}),
4303
+ ...(errorType ? {
4304
+ errorType
4305
+ } : {}),
4306
+ statusCode: requestResult.statusCode,
4307
+ type: 'error'
4308
+ };
4309
+ }
4310
+ parsed = requestResult.body;
4311
+ } else {
4312
+ let response;
4313
+ try {
4314
+ response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl), {
4315
+ body: JSON.stringify(postBody),
4316
+ headers: {
4317
+ Authorization: `Bearer ${openApiApiKey}`,
4318
+ 'Content-Type': 'application/json',
4319
+ ...getClientRequestIdHeader()
4320
+ },
4321
+ method: 'POST'
4322
+ });
4323
+ } catch {
4324
+ return {
4325
+ details: 'request-failed',
4326
+ type: 'error'
4327
+ };
4328
+ }
4329
+ if (!response.ok) {
4330
+ const {
4331
+ errorCode,
4332
+ errorMessage,
4333
+ errorType
4334
+ } = await getOpenApiErrorDetails(response);
4335
+ return {
4336
+ details: 'http-error',
4337
+ ...(errorCode ? {
4338
+ errorCode
4339
+ } : {}),
4340
+ ...(errorMessage ? {
4341
+ errorMessage
4342
+ } : {}),
4343
+ ...(errorType ? {
4344
+ errorType
4345
+ } : {}),
4346
+ statusCode: response.status,
4347
+ type: 'error'
4348
+ };
4349
+ }
4350
+ try {
4351
+ parsed = await response.json();
4352
+ } catch {
4353
+ return {
4354
+ details: 'request-failed',
4355
+ type: 'error'
4356
+ };
4357
+ }
4163
4358
  }
4164
4359
  if (!parsed || typeof parsed !== 'object') {
4165
4360
  return {
@@ -4394,28 +4589,67 @@ const getOpenRouterRaw429Message = async response => {
4394
4589
  }
4395
4590
  return raw;
4396
4591
  };
4397
- const getOpenRouterLimitInfo = async (openRouterApiKey, openRouterApiBaseUrl) => {
4398
- let response;
4592
+ const getOpenRouterRaw429MessageFromText = responseText => {
4593
+ let parsed;
4399
4594
  try {
4400
- response = await fetch(getOpenRouterKeyEndpoint(openRouterApiBaseUrl), {
4401
- headers: {
4402
- Authorization: `Bearer ${openRouterApiKey}`,
4403
- ...getClientRequestIdHeader()
4404
- },
4405
- method: 'GET'
4406
- });
4595
+ parsed = JSON.parse(responseText);
4407
4596
  } catch {
4408
4597
  return undefined;
4409
4598
  }
4410
- if (!response.ok) {
4599
+ if (!parsed || typeof parsed !== 'object') {
4411
4600
  return undefined;
4412
4601
  }
4413
- let parsed;
4414
- try {
4415
- parsed = await response.json();
4416
- } catch {
4602
+ const error = Reflect.get(parsed, 'error');
4603
+ if (!error || typeof error !== 'object') {
4604
+ return undefined;
4605
+ }
4606
+ const metadata = Reflect.get(error, 'metadata');
4607
+ if (!metadata || typeof metadata !== 'object') {
4608
+ return undefined;
4609
+ }
4610
+ const raw = Reflect.get(metadata, 'raw');
4611
+ if (typeof raw !== 'string' || !raw) {
4417
4612
  return undefined;
4418
4613
  }
4614
+ return raw;
4615
+ };
4616
+ const getOpenRouterLimitInfo = async (openRouterApiKey, openRouterApiBaseUrl, useChatNetworkWorkerForRequests) => {
4617
+ let parsed;
4618
+ if (useChatNetworkWorkerForRequests) {
4619
+ const result = await makeApiRequest({
4620
+ headers: {
4621
+ Authorization: `Bearer ${openRouterApiKey}`,
4622
+ ...getClientRequestIdHeader()
4623
+ },
4624
+ method: 'GET',
4625
+ url: getOpenRouterKeyEndpoint(openRouterApiBaseUrl)
4626
+ });
4627
+ if (result.type === 'error') {
4628
+ return undefined;
4629
+ }
4630
+ parsed = result.body;
4631
+ } else {
4632
+ let response;
4633
+ try {
4634
+ response = await fetch(getOpenRouterKeyEndpoint(openRouterApiBaseUrl), {
4635
+ headers: {
4636
+ Authorization: `Bearer ${openRouterApiKey}`,
4637
+ ...getClientRequestIdHeader()
4638
+ },
4639
+ method: 'GET'
4640
+ });
4641
+ } catch {
4642
+ return undefined;
4643
+ }
4644
+ if (!response.ok) {
4645
+ return undefined;
4646
+ }
4647
+ try {
4648
+ parsed = await response.json();
4649
+ } catch {
4650
+ return undefined;
4651
+ }
4652
+ }
4419
4653
  if (!parsed || typeof parsed !== 'object') {
4420
4654
  return undefined;
4421
4655
  }
@@ -4447,7 +4681,7 @@ const getOpenRouterLimitInfo = async (openRouterApiKey, openRouterApiBaseUrl) =>
4447
4681
  }
4448
4682
  return normalizedLimitInfo;
4449
4683
  };
4450
- const getOpenRouterAssistantText = async (messages, modelId, openRouterApiKey, openRouterApiBaseUrl, assetDir, platform) => {
4684
+ const getOpenRouterAssistantText = async (messages, modelId, openRouterApiKey, openRouterApiBaseUrl, assetDir, platform, useChatNetworkWorkerForRequests = false) => {
4451
4685
  const completionMessages = messages.map(message => ({
4452
4686
  content: message.text,
4453
4687
  role: message.role
@@ -4455,64 +4689,111 @@ const getOpenRouterAssistantText = async (messages, modelId, openRouterApiKey, o
4455
4689
  const tools = getBasicChatTools();
4456
4690
  const maxToolIterations = 4;
4457
4691
  for (let i = 0; i <= maxToolIterations; i++) {
4458
- let response;
4459
- try {
4460
- response = await fetch(getOpenRouterApiEndpoint(openRouterApiBaseUrl), {
4461
- body: JSON.stringify({
4462
- messages: completionMessages,
4463
- model: modelId,
4464
- tool_choice: 'auto',
4465
- tools
4466
- }),
4692
+ let parsed;
4693
+ if (useChatNetworkWorkerForRequests) {
4694
+ const requestResult = await makeApiRequest({
4467
4695
  headers: {
4468
4696
  Authorization: `Bearer ${openRouterApiKey}`,
4469
4697
  'Content-Type': 'application/json',
4470
4698
  ...getClientRequestIdHeader()
4471
4699
  },
4472
- method: 'POST'
4700
+ method: 'POST',
4701
+ postBody: {
4702
+ messages: completionMessages,
4703
+ model: modelId,
4704
+ tool_choice: 'auto',
4705
+ tools
4706
+ },
4707
+ url: getOpenRouterApiEndpoint(openRouterApiBaseUrl)
4473
4708
  });
4474
- } catch {
4475
- return {
4476
- details: 'request-failed',
4477
- type: 'error'
4478
- };
4479
- }
4480
- if (!response.ok) {
4481
- if (response.status === 429) {
4482
- const retryAfter = response.headers?.get?.('retry-after') ?? null;
4483
- const rawMessage = await getOpenRouterRaw429Message(response);
4484
- const limitInfo = await getOpenRouterLimitInfo(openRouterApiKey, openRouterApiBaseUrl);
4709
+ if (requestResult.type === 'error') {
4710
+ if (requestResult.statusCode === 429) {
4711
+ const retryAfter = requestResult.headers?.['retry-after'] ?? null;
4712
+ const rawMessage = getOpenRouterRaw429MessageFromText(requestResult.response);
4713
+ const limitInfo = await getOpenRouterLimitInfo(openRouterApiKey, openRouterApiBaseUrl, useChatNetworkWorkerForRequests);
4714
+ return {
4715
+ details: 'too-many-requests',
4716
+ ...(limitInfo || retryAfter ? {
4717
+ limitInfo: {
4718
+ ...limitInfo,
4719
+ ...(retryAfter ? {
4720
+ retryAfter
4721
+ } : {})
4722
+ }
4723
+ } : {}),
4724
+ ...(rawMessage ? {
4725
+ rawMessage
4726
+ } : {}),
4727
+ statusCode: 429,
4728
+ type: 'error'
4729
+ };
4730
+ }
4485
4731
  return {
4486
- details: 'too-many-requests',
4487
- ...(limitInfo || retryAfter ? {
4488
- limitInfo: {
4489
- ...limitInfo,
4490
- ...(retryAfter ? {
4491
- retryAfter
4492
- } : {})
4493
- }
4494
- } : {}),
4495
- ...(rawMessage ? {
4496
- rawMessage
4497
- } : {}),
4498
- statusCode: 429,
4732
+ details: 'http-error',
4733
+ statusCode: requestResult.statusCode,
4734
+ type: 'error'
4735
+ };
4736
+ }
4737
+ parsed = requestResult.body;
4738
+ } else {
4739
+ let response;
4740
+ try {
4741
+ response = await fetch(getOpenRouterApiEndpoint(openRouterApiBaseUrl), {
4742
+ body: JSON.stringify({
4743
+ messages: completionMessages,
4744
+ model: modelId,
4745
+ tool_choice: 'auto',
4746
+ tools
4747
+ }),
4748
+ headers: {
4749
+ Authorization: `Bearer ${openRouterApiKey}`,
4750
+ 'Content-Type': 'application/json',
4751
+ ...getClientRequestIdHeader()
4752
+ },
4753
+ method: 'POST'
4754
+ });
4755
+ } catch {
4756
+ return {
4757
+ details: 'request-failed',
4758
+ type: 'error'
4759
+ };
4760
+ }
4761
+ if (!response.ok) {
4762
+ if (response.status === 429) {
4763
+ const retryAfter = response.headers?.get?.('retry-after') ?? null;
4764
+ const rawMessage = await getOpenRouterRaw429Message(response);
4765
+ const limitInfo = await getOpenRouterLimitInfo(openRouterApiKey, openRouterApiBaseUrl, useChatNetworkWorkerForRequests);
4766
+ return {
4767
+ details: 'too-many-requests',
4768
+ ...(limitInfo || retryAfter ? {
4769
+ limitInfo: {
4770
+ ...limitInfo,
4771
+ ...(retryAfter ? {
4772
+ retryAfter
4773
+ } : {})
4774
+ }
4775
+ } : {}),
4776
+ ...(rawMessage ? {
4777
+ rawMessage
4778
+ } : {}),
4779
+ statusCode: 429,
4780
+ type: 'error'
4781
+ };
4782
+ }
4783
+ return {
4784
+ details: 'http-error',
4785
+ statusCode: response.status,
4786
+ type: 'error'
4787
+ };
4788
+ }
4789
+ try {
4790
+ parsed = await response.json();
4791
+ } catch {
4792
+ return {
4793
+ details: 'request-failed',
4499
4794
  type: 'error'
4500
4795
  };
4501
4796
  }
4502
- return {
4503
- details: 'http-error',
4504
- statusCode: response.status,
4505
- type: 'error'
4506
- };
4507
- }
4508
- let parsed;
4509
- try {
4510
- parsed = await response.json();
4511
- } catch {
4512
- return {
4513
- details: 'request-failed',
4514
- type: 'error'
4515
- };
4516
4797
  }
4517
4798
  if (!parsed || typeof parsed !== 'object') {
4518
4799
  return {
@@ -4671,6 +4952,7 @@ const getAiResponse = async ({
4671
4952
  platform,
4672
4953
  selectedModelId,
4673
4954
  streamingEnabled = true,
4955
+ useChatNetworkWorkerForRequests = false,
4674
4956
  useMockApi,
4675
4957
  userText,
4676
4958
  webSearchEnabled = false
@@ -4705,6 +4987,7 @@ const getAiResponse = async ({
4705
4987
  onToolCallsChunk
4706
4988
  } : {}),
4707
4989
  stream: streamingEnabled,
4990
+ useChatNetworkWorkerForRequests,
4708
4991
  webSearchEnabled
4709
4992
  });
4710
4993
  if (result.type === 'success') {
@@ -4731,7 +5014,7 @@ const getAiResponse = async ({
4731
5014
  text = getOpenRouterErrorMessage(result);
4732
5015
  }
4733
5016
  } else if (openRouterApiKey) {
4734
- const result = await getOpenRouterAssistantText(messages, modelId, openRouterApiKey, openRouterApiBaseUrl, assetDir, platform);
5017
+ const result = await getOpenRouterAssistantText(messages, modelId, openRouterApiKey, openRouterApiBaseUrl, assetDir, platform, useChatNetworkWorkerForRequests);
4735
5018
  if (result.type === 'success') {
4736
5019
  const {
4737
5020
  text: assistantText
@@ -4815,6 +5098,7 @@ const handleClickSaveOpenApiApiKey = async state => {
4815
5098
  platform: updatedState.platform,
4816
5099
  selectedModelId: updatedState.selectedModelId,
4817
5100
  streamingEnabled: updatedState.streamingEnabled,
5101
+ useChatNetworkWorkerForRequests: updatedState.useChatNetworkWorkerForRequests,
4818
5102
  useMockApi: updatedState.useMockApi,
4819
5103
  userText: previousUserMessage.text
4820
5104
  });
@@ -4895,6 +5179,7 @@ const handleClickSaveOpenRouterApiKey = async state => {
4895
5179
  openRouterApiKey,
4896
5180
  platform: updatedState.platform,
4897
5181
  selectedModelId: updatedState.selectedModelId,
5182
+ useChatNetworkWorkerForRequests: updatedState.useChatNetworkWorkerForRequests,
4898
5183
  useMockApi: updatedState.useMockApi,
4899
5184
  userText: previousUserMessage.text
4900
5185
  });
@@ -5271,6 +5556,7 @@ Assistant: ${assistantText}`;
5271
5556
  platform: state.platform,
5272
5557
  selectedModelId,
5273
5558
  streamingEnabled: false,
5559
+ useChatNetworkWorkerForRequests: state.useChatNetworkWorkerForRequests,
5274
5560
  useMockApi,
5275
5561
  userText: titlePrompt,
5276
5562
  webSearchEnabled: false
@@ -5298,6 +5584,7 @@ const handleSubmit = async state => {
5298
5584
  selectedSessionId,
5299
5585
  sessions,
5300
5586
  streamingEnabled,
5587
+ useChatNetworkWorkerForRequests,
5301
5588
  useMockApi,
5302
5589
  viewMode,
5303
5590
  webSearchEnabled
@@ -5450,6 +5737,7 @@ const handleSubmit = async state => {
5450
5737
  platform,
5451
5738
  selectedModelId,
5452
5739
  streamingEnabled,
5740
+ useChatNetworkWorkerForRequests,
5453
5741
  useMockApi,
5454
5742
  userText,
5455
5743
  webSearchEnabled
@@ -5493,6 +5781,7 @@ const handleClickSend = async state => {
5493
5781
  };
5494
5782
 
5495
5783
  const Composer = 'composer';
5784
+ const ComposerDropTarget = 'composer-drop-target';
5496
5785
  const Send = 'send';
5497
5786
  const Back = 'back';
5498
5787
  const Model = 'model';
@@ -5667,6 +5956,89 @@ const handleClickSettings = async () => {
5667
5956
  await invoke('Main.openUri', 'app://settings.json');
5668
5957
  };
5669
5958
 
5959
+ const handleDragEnter = async (state, name, hasFiles = true) => {
5960
+ if (name !== ComposerDropTarget) {
5961
+ return state;
5962
+ }
5963
+ if (!state.composerDropEnabled) {
5964
+ return state;
5965
+ }
5966
+ if (!hasFiles) {
5967
+ return state;
5968
+ }
5969
+ if (state.composerDropActive) {
5970
+ return state;
5971
+ }
5972
+ return {
5973
+ ...state,
5974
+ composerDropActive: true
5975
+ };
5976
+ };
5977
+
5978
+ const handleDragLeave = async (state, name) => {
5979
+ if (name !== ComposerDropTarget) {
5980
+ return state;
5981
+ }
5982
+ if (!state.composerDropActive) {
5983
+ return state;
5984
+ }
5985
+ return {
5986
+ ...state,
5987
+ composerDropActive: false
5988
+ };
5989
+ };
5990
+
5991
+ const handleDragOver = async (state, name, hasFiles = true) => {
5992
+ if (name !== ComposerDropTarget) {
5993
+ return state;
5994
+ }
5995
+ if (!state.composerDropEnabled) {
5996
+ return state;
5997
+ }
5998
+ if (!hasFiles) {
5999
+ return state;
6000
+ }
6001
+ if (state.composerDropActive) {
6002
+ return state;
6003
+ }
6004
+ return {
6005
+ ...state,
6006
+ composerDropActive: true
6007
+ };
6008
+ };
6009
+
6010
+ const handleDropFiles = async (state, name, files = []) => {
6011
+ if (name !== ComposerDropTarget) {
6012
+ return state;
6013
+ }
6014
+ if (!state.composerDropEnabled) {
6015
+ return {
6016
+ ...state,
6017
+ composerDropActive: false
6018
+ };
6019
+ }
6020
+ const nextState = state.composerDropActive === false ? state : {
6021
+ ...state,
6022
+ composerDropActive: false
6023
+ };
6024
+ if (!state.selectedSessionId || files.length === 0) {
6025
+ return nextState;
6026
+ }
6027
+ for (const file of files) {
6028
+ await appendChatViewEvent({
6029
+ attachmentId: crypto.randomUUID(),
6030
+ blob: file,
6031
+ mimeType: file.type,
6032
+ name: file.name,
6033
+ sessionId: state.selectedSessionId,
6034
+ size: file.size,
6035
+ timestamp: new Date().toISOString(),
6036
+ type: 'chat-attachment-added'
6037
+ });
6038
+ }
6039
+ return nextState;
6040
+ };
6041
+
5670
6042
  const handleInput = async (state, name, value, inputSource = 'user') => {
5671
6043
  const {
5672
6044
  selectedSessionId
@@ -5944,6 +6316,15 @@ const loadAiSessionTitleGenerationEnabled = async () => {
5944
6316
  }
5945
6317
  };
5946
6318
 
6319
+ const loadComposerDropEnabled = async () => {
6320
+ try {
6321
+ const savedComposerDropEnabled = await get('chatView.composerDropEnabled');
6322
+ return typeof savedComposerDropEnabled === 'boolean' ? savedComposerDropEnabled : true;
6323
+ } catch {
6324
+ return true;
6325
+ }
6326
+ };
6327
+
5947
6328
  const loadEmitStreamingFunctionCallEvents = async () => {
5948
6329
  try {
5949
6330
  const savedEmitStreamingFunctionCallEvents = await get('chatView.emitStreamingFunctionCallEvents');
@@ -5997,15 +6378,26 @@ const loadStreamingEnabled = async () => {
5997
6378
  }
5998
6379
  };
5999
6380
 
6381
+ const loadUseChatNetworkWorkerForRequests = async () => {
6382
+ try {
6383
+ const savedUseChatNetworkWorkerForRequests = await get('chatView.useChatNetworkWorkerForRequests');
6384
+ return typeof savedUseChatNetworkWorkerForRequests === 'boolean' ? savedUseChatNetworkWorkerForRequests : false;
6385
+ } catch {
6386
+ return false;
6387
+ }
6388
+ };
6389
+
6000
6390
  const loadPreferences = async () => {
6001
- const [aiSessionTitleGenerationEnabled, openApiApiKey, openRouterApiKey, emitStreamingFunctionCallEvents, streamingEnabled, passIncludeObfuscation] = await Promise.all([loadAiSessionTitleGenerationEnabled(), loadOpenApiApiKey(), loadOpenRouterApiKey(), loadEmitStreamingFunctionCallEvents(), loadStreamingEnabled(), loadPassIncludeObfuscation()]);
6391
+ const [aiSessionTitleGenerationEnabled, composerDropEnabled, openApiApiKey, openRouterApiKey, emitStreamingFunctionCallEvents, streamingEnabled, passIncludeObfuscation, useChatNetworkWorkerForRequests] = await Promise.all([loadAiSessionTitleGenerationEnabled(), loadComposerDropEnabled(), loadOpenApiApiKey(), loadOpenRouterApiKey(), loadEmitStreamingFunctionCallEvents(), loadStreamingEnabled(), loadPassIncludeObfuscation(), loadUseChatNetworkWorkerForRequests()]);
6002
6392
  return {
6003
6393
  aiSessionTitleGenerationEnabled,
6394
+ composerDropEnabled,
6004
6395
  emitStreamingFunctionCallEvents,
6005
6396
  openApiApiKey,
6006
6397
  openRouterApiKey,
6007
6398
  passIncludeObfuscation,
6008
- streamingEnabled
6399
+ streamingEnabled,
6400
+ useChatNetworkWorkerForRequests
6009
6401
  };
6010
6402
  };
6011
6403
 
@@ -6037,11 +6429,13 @@ const loadContent = async (state, savedState) => {
6037
6429
  const savedViewMode = getSavedViewMode(savedState);
6038
6430
  const {
6039
6431
  aiSessionTitleGenerationEnabled,
6432
+ composerDropEnabled,
6040
6433
  emitStreamingFunctionCallEvents,
6041
6434
  openApiApiKey,
6042
6435
  openRouterApiKey,
6043
6436
  passIncludeObfuscation,
6044
- streamingEnabled
6437
+ streamingEnabled,
6438
+ useChatNetworkWorkerForRequests
6045
6439
  } = await loadPreferences();
6046
6440
  const legacySavedSessions = getSavedSessions(savedState);
6047
6441
  const storedSessions = await listChatSessions();
@@ -6071,6 +6465,8 @@ const loadContent = async (state, savedState) => {
6071
6465
  ...state,
6072
6466
  aiSessionTitleGenerationEnabled,
6073
6467
  chatListScrollTop,
6468
+ composerDropActive: false,
6469
+ composerDropEnabled,
6074
6470
  emitStreamingFunctionCallEvents,
6075
6471
  initial: false,
6076
6472
  messagesScrollTop,
@@ -6083,6 +6479,7 @@ const loadContent = async (state, savedState) => {
6083
6479
  selectedSessionId,
6084
6480
  sessions,
6085
6481
  streamingEnabled,
6482
+ useChatNetworkWorkerForRequests,
6086
6483
  viewMode
6087
6484
  };
6088
6485
  };
@@ -6156,92 +6553,7 @@ const getCss = (composerHeight, listItemHeight, chatMessageFontSize, chatMessage
6156
6553
  --ChatMessageLineHeight: ${chatMessageLineHeight}px;
6157
6554
  --ChatMessageFontFamily: ${chatMessageFontFamily};
6158
6555
  }
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
- }`;
6556
+ `;
6245
6557
  if (!renderHtmlCss.trim()) {
6246
6558
  return baseCss;
6247
6559
  }
@@ -6298,6 +6610,8 @@ const Actions = 'Actions';
6298
6610
  const ChatActions = 'ChatActions';
6299
6611
  const ChatName = 'ChatName';
6300
6612
  const ChatSendArea = 'ChatSendArea';
6613
+ const ChatViewDropOverlay = 'ChatViewDropOverlay';
6614
+ const ChatViewDropOverlayActive = 'ChatViewDropOverlayActive';
6301
6615
  const SendButtonDisabled = 'SendButtonDisabled';
6302
6616
  const ChatSendAreaBottom = 'ChatSendAreaBottom';
6303
6617
  const ChatSendAreaContent = 'ChatSendAreaContent';
@@ -6306,6 +6620,7 @@ const ChatHeader = 'ChatHeader';
6306
6620
  const Button = 'Button';
6307
6621
  const ButtonPrimary = 'ButtonPrimary';
6308
6622
  const ButtonSecondary = 'ButtonSecondary';
6623
+ const Empty = '';
6309
6624
  const FileIcon = 'FileIcon';
6310
6625
  const IconButton = 'IconButton';
6311
6626
  const InputBox = 'InputBox';
@@ -6359,6 +6674,12 @@ const HandleMessagesScroll = 22;
6359
6674
  const HandleClickSessionDebug = 23;
6360
6675
  const HandleClickReadFile = 24;
6361
6676
  const HandleMessagesContextMenu = 25;
6677
+ const HandleDragEnter = 26;
6678
+ const HandleDragOver = 27;
6679
+ const HandleDragLeave = 28;
6680
+ const HandleDrop = 29;
6681
+ const HandleDragEnterChatView = 30;
6682
+ const HandleDragOverChatView = 31;
6362
6683
 
6363
6684
  const getModelLabel = model => {
6364
6685
  if (model.provider === 'openRouter') {
@@ -7230,6 +7551,12 @@ const markdownInlineRegex = /\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*/g;
7230
7551
  const markdownTableSeparatorCellRegex = /^:?-{3,}:?$/;
7231
7552
  const fencedCodeBlockRegex = /^```/;
7232
7553
  const markdownHeadingRegex = /^\s*(#{1,6})\s+(.*)$/;
7554
+ const normalizeEscapedNewlines = value => {
7555
+ if (value.includes('\\n')) {
7556
+ return value.replaceAll(/\\r\\n|\\n/g, '\n');
7557
+ }
7558
+ return value;
7559
+ };
7233
7560
  const normalizeInlineTables = value => {
7234
7561
  return value.split(/\r?\n/).map(line => {
7235
7562
  if (!line.includes('|')) {
@@ -7328,7 +7655,8 @@ const parseMessageContent = rawMessage => {
7328
7655
  type: 'text'
7329
7656
  }];
7330
7657
  }
7331
- const lines = normalizeInlineTables(rawMessage).split(/\r?\n/);
7658
+ const normalizedMessage = normalizeEscapedNewlines(rawMessage);
7659
+ const lines = normalizeInlineTables(normalizedMessage).split(/\r?\n/);
7332
7660
  const nodes = [];
7333
7661
  let paragraphLines = [];
7334
7662
  let listItems = [];
@@ -7494,15 +7822,29 @@ const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = ''
7494
7822
  }, ...messages.flatMap(message => getChatMessageDom(message, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState))];
7495
7823
  };
7496
7824
 
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) => {
7825
+ 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
7826
  const selectedSession = sessions.find(session => session.id === selectedSessionId);
7499
7827
  const selectedSessionTitle = selectedSession?.title || chatTitle();
7500
7828
  const messages = selectedSession ? selectedSession.messages : [];
7829
+ const isDropOverlayVisible = composerDropEnabled && composerDropActive;
7501
7830
  return [{
7502
- childCount: 3,
7831
+ childCount: 4,
7503
7832
  className: mergeClassNames(Viewlet, Chat),
7833
+ onDragEnter: HandleDragEnterChatView,
7834
+ onDragOver: HandleDragOverChatView,
7504
7835
  type: Div
7505
- }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState, messagesScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
7836
+ }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState, messagesScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight), {
7837
+ childCount: 1,
7838
+ className: mergeClassNames(ChatViewDropOverlay, isDropOverlayVisible ? ChatViewDropOverlayActive : Empty),
7839
+ name: ComposerDropTarget,
7840
+ onDragLeave: HandleDragLeave,
7841
+ onDragOver: HandleDragOver,
7842
+ onDrop: HandleDrop,
7843
+ type: Div
7844
+ }, {
7845
+ text: attachImageAsContext(),
7846
+ type: Text
7847
+ }];
7506
7848
  };
7507
7849
 
7508
7850
  const getChatHeaderListModeDom = () => {
@@ -7572,12 +7914,26 @@ const getChatListDom = (sessions, selectedSessionId, chatListScrollTop = 0) => {
7572
7914
  }, ...sessions.flatMap(getSessionDom)];
7573
7915
  };
7574
7916
 
7575
- const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20, chatListScrollTop = 0) => {
7917
+ 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) => {
7918
+ const isDropOverlayVisible = composerDropEnabled && composerDropActive;
7576
7919
  return [{
7577
- childCount: 3,
7920
+ childCount: 4,
7578
7921
  className: mergeClassNames(Viewlet, Chat),
7922
+ onDragEnter: HandleDragEnterChatView,
7923
+ onDragOver: HandleDragOverChatView,
7924
+ type: Div
7925
+ }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions, selectedSessionId, chatListScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight), {
7926
+ childCount: 1,
7927
+ className: mergeClassNames(ChatViewDropOverlay, isDropOverlayVisible ? ChatViewDropOverlayActive : Empty),
7928
+ name: ComposerDropTarget,
7929
+ onDragLeave: HandleDragLeave,
7930
+ onDragOver: HandleDragOver,
7931
+ onDrop: HandleDrop,
7579
7932
  type: Div
7580
- }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions, selectedSessionId, chatListScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
7933
+ }, {
7934
+ text: attachImageAsContext(),
7935
+ type: Text
7936
+ }];
7581
7937
  };
7582
7938
 
7583
7939
  const getChatModeUnsupportedVirtualDom = () => {
@@ -7587,12 +7943,12 @@ const getChatModeUnsupportedVirtualDom = () => {
7587
7943
  }, text(unknownViewMode())];
7588
7944
  };
7589
7945
 
7590
- const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop) => {
7946
+ 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
7947
  switch (viewMode) {
7592
7948
  case 'detail':
7593
- return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, messagesScrollTop);
7949
+ return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, messagesScrollTop, composerDropActive, composerDropEnabled);
7594
7950
  case 'list':
7595
- return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop);
7951
+ return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, composerDropActive, composerDropEnabled);
7596
7952
  default:
7597
7953
  return getChatModeUnsupportedVirtualDom();
7598
7954
  }
@@ -7601,6 +7957,8 @@ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRoute
7601
7957
  const renderItems = (oldState, newState) => {
7602
7958
  const {
7603
7959
  chatListScrollTop,
7960
+ composerDropActive,
7961
+ composerDropEnabled,
7604
7962
  composerFontFamily,
7605
7963
  composerFontSize,
7606
7964
  composerHeight,
@@ -7624,7 +7982,7 @@ const renderItems = (oldState, newState) => {
7624
7982
  if (initial) {
7625
7983
  return [SetDom2, uid, []];
7626
7984
  }
7627
- const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop);
7985
+ const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop, composerDropActive, composerDropEnabled);
7628
7986
  return [SetDom2, uid, dom];
7629
7987
  };
7630
7988
 
@@ -7728,6 +8086,30 @@ const renderEventListeners = () => {
7728
8086
  }, {
7729
8087
  name: HandleInput,
7730
8088
  params: ['handleInput', TargetName, TargetValue]
8089
+ }, {
8090
+ name: HandleDragEnter,
8091
+ params: ['handleDragEnter', TargetName, 'Array.from(event.dataTransfer?.files || []).length > 0'],
8092
+ preventDefault: true
8093
+ }, {
8094
+ name: HandleDragOver,
8095
+ params: ['handleDragOver', TargetName, 'Array.from(event.dataTransfer?.files || []).length > 0'],
8096
+ preventDefault: true
8097
+ }, {
8098
+ name: HandleDragLeave,
8099
+ params: ['handleDragLeave', TargetName],
8100
+ preventDefault: true
8101
+ }, {
8102
+ name: HandleDrop,
8103
+ params: ['handleDropFiles', TargetName, 'Array.from(event.dataTransfer?.files || [])'],
8104
+ preventDefault: true
8105
+ }, {
8106
+ name: HandleDragEnterChatView,
8107
+ params: ['handleDragEnter', 'composer-drop-target', 'Array.from(event.dataTransfer?.files || []).length > 0'],
8108
+ preventDefault: true
8109
+ }, {
8110
+ name: HandleDragOverChatView,
8111
+ params: ['handleDragOver', 'composer-drop-target', 'Array.from(event.dataTransfer?.files || []).length > 0'],
8112
+ preventDefault: true
7731
8113
  }, {
7732
8114
  name: HandleModelChange,
7733
8115
  params: ['handleModelChange', TargetValue]
@@ -7839,6 +8221,18 @@ const setStreamingEnabled = (state, streamingEnabled) => {
7839
8221
  };
7840
8222
  };
7841
8223
 
8224
+ const setUseChatNetworkWorkerForRequests = async (state, useChatNetworkWorkerForRequests, persist = true) => {
8225
+ if (persist) {
8226
+ await update({
8227
+ 'chatView.useChatNetworkWorkerForRequests': useChatNetworkWorkerForRequests
8228
+ });
8229
+ }
8230
+ return {
8231
+ ...state,
8232
+ useChatNetworkWorkerForRequests
8233
+ };
8234
+ };
8235
+
7842
8236
  const defaultMockApiCommandId = 'ChatE2e.mockApi';
7843
8237
  const useMockApi = (state, value, mockApiCommandId = defaultMockApiCommandId) => {
7844
8238
  if (!value) {
@@ -7857,6 +8251,7 @@ const useMockApi = (state, value, mockApiCommandId = defaultMockApiCommandId) =>
7857
8251
  const commandMap = {
7858
8252
  'Chat.clearInput': wrapCommand(clearInput),
7859
8253
  'Chat.create': create,
8254
+ 'Chat.deleteSessionAtIndex': wrapCommand(deleteSessionAtIndex),
7860
8255
  'Chat.diff2': diff2,
7861
8256
  'Chat.enterNewLine': wrapCommand(handleNewline),
7862
8257
  'Chat.getCommandIds': getCommandIds,
@@ -7873,6 +8268,10 @@ const commandMap = {
7873
8268
  'Chat.handleClickReadFile': handleClickReadFile,
7874
8269
  'Chat.handleClickSessionDebug': wrapCommand(handleClickSessionDebug),
7875
8270
  'Chat.handleClickSettings': handleClickSettings,
8271
+ 'Chat.handleDragEnter': wrapCommand(handleDragEnter),
8272
+ 'Chat.handleDragLeave': wrapCommand(handleDragLeave),
8273
+ 'Chat.handleDragOver': wrapCommand(handleDragOver),
8274
+ 'Chat.handleDropFiles': wrapCommand(handleDropFiles),
7876
8275
  'Chat.handleInput': wrapCommand(handleInput),
7877
8276
  'Chat.handleInputFocus': wrapCommand(handleInputFocus),
7878
8277
  'Chat.handleKeyDown': wrapCommand(handleKeyDown),
@@ -7899,6 +8298,7 @@ const commandMap = {
7899
8298
  'Chat.setEmitStreamingFunctionCallEvents': wrapCommand(setEmitStreamingFunctionCallEvents),
7900
8299
  'Chat.setOpenRouterApiKey': wrapCommand(setOpenRouterApiKey),
7901
8300
  'Chat.setStreamingEnabled': wrapCommand(setStreamingEnabled),
8301
+ 'Chat.setUseChatNetworkWorkerForRequests': wrapCommand(setUseChatNetworkWorkerForRequests),
7902
8302
  'Chat.terminate': terminate,
7903
8303
  'Chat.useMockApi': wrapCommand(useMockApi)
7904
8304
  };