@lvce-editor/chat-view 1.7.0 → 1.9.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.
@@ -964,32 +964,9 @@ const create$3 = async ({
964
964
  return rpc;
965
965
  };
966
966
 
967
- const Button$2 = 'button';
968
-
969
- const Button$1 = 1;
970
- const Div = 4;
971
- const Span = 8;
972
- const Text = 12;
973
- const P = 50;
974
- const TextArea = 62;
975
- const Reference = 100;
976
-
977
- const ClientX = 'event.clientX';
978
- const ClientY = 'event.clientY';
979
- const Key = 'event.key';
980
- const ShiftKey = 'event.shiftKey';
981
- const TargetName = 'event.target.name';
982
- const TargetValue = 'event.target.value';
983
-
984
967
  const ExtensionHostWorker = 44;
985
968
  const RendererWorker = 1;
986
969
 
987
- const FocusSelector = 'Viewlet.focusSelector';
988
- const SetCss = 'Viewlet.setCss';
989
- const SetDom2 = 'Viewlet.setDom2';
990
- const SetValueByName = 'Viewlet.setValueByName';
991
- const SetPatches = 'Viewlet.setPatches';
992
-
993
970
  const createMockRpc = ({
994
971
  commandMap
995
972
  }) => {
@@ -1224,7 +1201,7 @@ const assistant = i18nString('Assistant');
1224
1201
  const composePlaceholder = i18nString('Type your message. Enter to send, Shift+Enter for newline.');
1225
1202
  const sendMessage = i18nString('Send message');
1226
1203
  const send = i18nString('Send');
1227
- const deleteChatSession = i18nString('Delete chat session');
1204
+ const deleteChatSession$1 = i18nString('Delete chat session');
1228
1205
  const defaultSessionTitle = i18nString('Chat 1');
1229
1206
  const dummyChatA = i18nString('Dummy Chat A');
1230
1207
  const dummyChatB = i18nString('Dummy Chat B');
@@ -1232,6 +1209,7 @@ const dummyChatC = i18nString('Dummy Chat C');
1232
1209
 
1233
1210
  const createDefaultState = () => {
1234
1211
  const defaultSessionId = 'session-1';
1212
+ const defaultModelId = 'test';
1235
1213
  return {
1236
1214
  assetDir: '',
1237
1215
  composerValue: '',
@@ -1244,9 +1222,23 @@ const createDefaultState = () => {
1244
1222
  inputSource: 'script',
1245
1223
  lastSubmittedSessionId: '',
1246
1224
  listItemHeight: 40,
1225
+ models: [{
1226
+ id: defaultModelId,
1227
+ name: 'test'
1228
+ }, {
1229
+ id: 'codex-5.3',
1230
+ name: 'Codex 5.3'
1231
+ }, {
1232
+ id: 'claude-code',
1233
+ name: 'Claude Code'
1234
+ }, {
1235
+ id: 'claude-haiku',
1236
+ name: 'Claude Haiku'
1237
+ }],
1247
1238
  nextMessageId: 1,
1248
1239
  platform: 0,
1249
1240
  renamingSessionId: '',
1241
+ selectedModelId: defaultModelId,
1250
1242
  selectedSessionId: defaultSessionId,
1251
1243
  sessions: [{
1252
1244
  id: defaultSessionId,
@@ -1297,11 +1289,12 @@ const diffFocus = (oldState, newState) => {
1297
1289
  };
1298
1290
 
1299
1291
  const isEqual = (oldState, newState) => {
1300
- return oldState.composerValue === newState.composerValue && oldState.initial === newState.initial && oldState.renamingSessionId === newState.renamingSessionId && oldState.selectedSessionId === newState.selectedSessionId && oldState.sessions === newState.sessions && oldState.viewMode === newState.viewMode;
1292
+ 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.viewMode === newState.viewMode;
1301
1293
  };
1302
1294
 
1303
1295
  const RenderItems = 4;
1304
1296
  const RenderFocus = 6;
1297
+ const RenderFocusContext = 7;
1305
1298
  const RenderValue = 8;
1306
1299
  const RenderCss = 10;
1307
1300
  const RenderIncremental = 11;
@@ -1324,44 +1317,537 @@ const diff = (oldState, newState) => {
1324
1317
  diffResult.push(numbers[i]);
1325
1318
  }
1326
1319
  }
1327
- return diffResult;
1328
- };
1320
+ return diffResult;
1321
+ };
1322
+
1323
+ const diff2 = uid => {
1324
+ const {
1325
+ newState,
1326
+ oldState
1327
+ } = get(uid);
1328
+ const result = diff(oldState, newState);
1329
+ return result;
1330
+ };
1331
+
1332
+ const ClientX = 'event.clientX';
1333
+ const ClientY = 'event.clientY';
1334
+ const Key = 'event.key';
1335
+ const ShiftKey = 'event.shiftKey';
1336
+ const TargetName = 'event.target.name';
1337
+ const TargetValue = 'event.target.value';
1338
+
1339
+ const FocusSelector = 'Viewlet.focusSelector';
1340
+ const SetCss = 'Viewlet.setCss';
1341
+ const SetDom2 = 'Viewlet.setDom2';
1342
+ const SetFocusContext = 'Viewlet.setFocusContext';
1343
+ const SetValueByName = 'Viewlet.setValueByName';
1344
+ const SetPatches = 'Viewlet.setPatches';
1345
+
1346
+ const FocusChatInput = 8000;
1347
+
1348
+ const Button$2 = 'button';
1349
+
1350
+ const Button$1 = 1;
1351
+ const Div = 4;
1352
+ const Span = 8;
1353
+ const Text = 12;
1354
+ const P = 50;
1355
+ const TextArea = 62;
1356
+ const Select$1 = 63;
1357
+ const Option$1 = 64;
1358
+ const Reference = 100;
1359
+
1360
+ const Enter = 3;
1361
+
1362
+ const Shift = 1 << 10 >>> 0;
1363
+
1364
+ const mergeClassNames = (...classNames) => {
1365
+ return classNames.filter(Boolean).join(' ');
1366
+ };
1367
+
1368
+ const text = data => {
1369
+ return {
1370
+ childCount: 0,
1371
+ text: data,
1372
+ type: Text
1373
+ };
1374
+ };
1375
+
1376
+ const SetText = 1;
1377
+ const Replace = 2;
1378
+ const SetAttribute = 3;
1379
+ const RemoveAttribute = 4;
1380
+ const Add = 6;
1381
+ const NavigateChild = 7;
1382
+ const NavigateParent = 8;
1383
+ const RemoveChild = 9;
1384
+ const NavigateSibling = 10;
1385
+ const SetReferenceNodeUid = 11;
1386
+
1387
+ const isKey = key => {
1388
+ return key !== 'type' && key !== 'childCount';
1389
+ };
1390
+
1391
+ const getKeys = node => {
1392
+ const keys = Object.keys(node).filter(isKey);
1393
+ return keys;
1394
+ };
1395
+
1396
+ const arrayToTree = nodes => {
1397
+ const result = [];
1398
+ let i = 0;
1399
+ while (i < nodes.length) {
1400
+ const node = nodes[i];
1401
+ const {
1402
+ children,
1403
+ nodesConsumed
1404
+ } = getChildrenWithCount(nodes, i + 1, node.childCount || 0);
1405
+ result.push({
1406
+ node,
1407
+ children
1408
+ });
1409
+ i += 1 + nodesConsumed;
1410
+ }
1411
+ return result;
1412
+ };
1413
+ const getChildrenWithCount = (nodes, startIndex, childCount) => {
1414
+ if (childCount === 0) {
1415
+ return {
1416
+ children: [],
1417
+ nodesConsumed: 0
1418
+ };
1419
+ }
1420
+ const children = [];
1421
+ let i = startIndex;
1422
+ let remaining = childCount;
1423
+ let totalConsumed = 0;
1424
+ while (remaining > 0 && i < nodes.length) {
1425
+ const node = nodes[i];
1426
+ const nodeChildCount = node.childCount || 0;
1427
+ const {
1428
+ children: nodeChildren,
1429
+ nodesConsumed
1430
+ } = getChildrenWithCount(nodes, i + 1, nodeChildCount);
1431
+ children.push({
1432
+ node,
1433
+ children: nodeChildren
1434
+ });
1435
+ const nodeSize = 1 + nodesConsumed;
1436
+ i += nodeSize;
1437
+ totalConsumed += nodeSize;
1438
+ remaining--;
1439
+ }
1440
+ return {
1441
+ children,
1442
+ nodesConsumed: totalConsumed
1443
+ };
1444
+ };
1445
+
1446
+ const compareNodes = (oldNode, newNode) => {
1447
+ const patches = [];
1448
+ // Check if node type changed - return null to signal incompatible nodes
1449
+ // (caller should handle this with a Replace operation)
1450
+ if (oldNode.type !== newNode.type) {
1451
+ return null;
1452
+ }
1453
+ // Handle reference nodes - special handling for uid changes
1454
+ if (oldNode.type === Reference) {
1455
+ if (oldNode.uid !== newNode.uid) {
1456
+ patches.push({
1457
+ type: SetReferenceNodeUid,
1458
+ uid: newNode.uid
1459
+ });
1460
+ }
1461
+ return patches;
1462
+ }
1463
+ // Handle text nodes
1464
+ if (oldNode.type === Text && newNode.type === Text) {
1465
+ if (oldNode.text !== newNode.text) {
1466
+ patches.push({
1467
+ type: SetText,
1468
+ value: newNode.text
1469
+ });
1470
+ }
1471
+ return patches;
1472
+ }
1473
+ // Compare attributes
1474
+ const oldKeys = getKeys(oldNode);
1475
+ const newKeys = getKeys(newNode);
1476
+ // Check for attribute changes
1477
+ for (const key of newKeys) {
1478
+ if (oldNode[key] !== newNode[key]) {
1479
+ patches.push({
1480
+ type: SetAttribute,
1481
+ key,
1482
+ value: newNode[key]
1483
+ });
1484
+ }
1485
+ }
1486
+ // Check for removed attributes
1487
+ for (const key of oldKeys) {
1488
+ if (!(key in newNode)) {
1489
+ patches.push({
1490
+ type: RemoveAttribute,
1491
+ key
1492
+ });
1493
+ }
1494
+ }
1495
+ return patches;
1496
+ };
1497
+
1498
+ const treeToArray = node => {
1499
+ const result = [node.node];
1500
+ for (const child of node.children) {
1501
+ result.push(...treeToArray(child));
1502
+ }
1503
+ return result;
1504
+ };
1505
+
1506
+ const diffChildren = (oldChildren, newChildren, patches) => {
1507
+ const maxLength = Math.max(oldChildren.length, newChildren.length);
1508
+ // Track where we are: -1 means at parent, >= 0 means at child index
1509
+ let currentChildIndex = -1;
1510
+ // Collect indices of children to remove (we'll add these patches at the end in reverse order)
1511
+ const indicesToRemove = [];
1512
+ for (let i = 0; i < maxLength; i++) {
1513
+ const oldNode = oldChildren[i];
1514
+ const newNode = newChildren[i];
1515
+ if (!oldNode && !newNode) {
1516
+ continue;
1517
+ }
1518
+ if (!oldNode) {
1519
+ // Add new node - we should be at the parent
1520
+ if (currentChildIndex >= 0) {
1521
+ // Navigate back to parent
1522
+ patches.push({
1523
+ type: NavigateParent
1524
+ });
1525
+ currentChildIndex = -1;
1526
+ }
1527
+ // Flatten the entire subtree so renderInternal can handle it
1528
+ const flatNodes = treeToArray(newNode);
1529
+ patches.push({
1530
+ type: Add,
1531
+ nodes: flatNodes
1532
+ });
1533
+ } else if (newNode) {
1534
+ // Compare nodes to see if we need any patches
1535
+ const nodePatches = compareNodes(oldNode.node, newNode.node);
1536
+ // If nodePatches is null, the node types are incompatible - need to replace
1537
+ if (nodePatches === null) {
1538
+ // Navigate to this child
1539
+ if (currentChildIndex === -1) {
1540
+ patches.push({
1541
+ type: NavigateChild,
1542
+ index: i
1543
+ });
1544
+ currentChildIndex = i;
1545
+ } else if (currentChildIndex !== i) {
1546
+ patches.push({
1547
+ type: NavigateSibling,
1548
+ index: i
1549
+ });
1550
+ currentChildIndex = i;
1551
+ }
1552
+ // Replace the entire subtree
1553
+ const flatNodes = treeToArray(newNode);
1554
+ patches.push({
1555
+ type: Replace,
1556
+ nodes: flatNodes
1557
+ });
1558
+ // After replace, we're at the new element (same position)
1559
+ continue;
1560
+ }
1561
+ // Check if we need to recurse into children
1562
+ const hasChildrenToCompare = oldNode.children.length > 0 || newNode.children.length > 0;
1563
+ // Only navigate to this element if we need to do something
1564
+ if (nodePatches.length > 0 || hasChildrenToCompare) {
1565
+ // Navigate to this child if not already there
1566
+ if (currentChildIndex === -1) {
1567
+ patches.push({
1568
+ type: NavigateChild,
1569
+ index: i
1570
+ });
1571
+ currentChildIndex = i;
1572
+ } else if (currentChildIndex !== i) {
1573
+ patches.push({
1574
+ type: NavigateSibling,
1575
+ index: i
1576
+ });
1577
+ currentChildIndex = i;
1578
+ }
1579
+ // Apply node patches (these apply to the current element, not children)
1580
+ if (nodePatches.length > 0) {
1581
+ patches.push(...nodePatches);
1582
+ }
1583
+ // Compare children recursively
1584
+ if (hasChildrenToCompare) {
1585
+ diffChildren(oldNode.children, newNode.children, patches);
1586
+ }
1587
+ }
1588
+ } else {
1589
+ // Remove old node - collect the index for later removal
1590
+ indicesToRemove.push(i);
1591
+ }
1592
+ }
1593
+ // Navigate back to parent if we ended at a child
1594
+ if (currentChildIndex >= 0) {
1595
+ patches.push({
1596
+ type: NavigateParent
1597
+ });
1598
+ currentChildIndex = -1;
1599
+ }
1600
+ // Add remove patches in reverse order (highest index first)
1601
+ // This ensures indices remain valid as we remove
1602
+ for (let j = indicesToRemove.length - 1; j >= 0; j--) {
1603
+ patches.push({
1604
+ type: RemoveChild,
1605
+ index: indicesToRemove[j]
1606
+ });
1607
+ }
1608
+ };
1609
+ const diffTrees = (oldTree, newTree, patches, path) => {
1610
+ // At the root level (path.length === 0), we're already AT the element
1611
+ // So we compare the root node directly, then compare its children
1612
+ if (path.length === 0 && oldTree.length === 1 && newTree.length === 1) {
1613
+ const oldNode = oldTree[0];
1614
+ const newNode = newTree[0];
1615
+ // Compare root nodes
1616
+ const nodePatches = compareNodes(oldNode.node, newNode.node);
1617
+ // If nodePatches is null, the root node types are incompatible - need to replace
1618
+ if (nodePatches === null) {
1619
+ const flatNodes = treeToArray(newNode);
1620
+ patches.push({
1621
+ type: Replace,
1622
+ nodes: flatNodes
1623
+ });
1624
+ return;
1625
+ }
1626
+ if (nodePatches.length > 0) {
1627
+ patches.push(...nodePatches);
1628
+ }
1629
+ // Compare children
1630
+ if (oldNode.children.length > 0 || newNode.children.length > 0) {
1631
+ diffChildren(oldNode.children, newNode.children, patches);
1632
+ }
1633
+ } else {
1634
+ // Non-root level or multiple root elements - use the regular comparison
1635
+ diffChildren(oldTree, newTree, patches);
1636
+ }
1637
+ };
1638
+
1639
+ const removeTrailingNavigationPatches = patches => {
1640
+ // Find the last non-navigation patch
1641
+ let lastNonNavigationIndex = -1;
1642
+ for (let i = patches.length - 1; i >= 0; i--) {
1643
+ const patch = patches[i];
1644
+ if (patch.type !== NavigateChild && patch.type !== NavigateParent && patch.type !== NavigateSibling) {
1645
+ lastNonNavigationIndex = i;
1646
+ break;
1647
+ }
1648
+ }
1649
+ // Return patches up to and including the last non-navigation patch
1650
+ return lastNonNavigationIndex === -1 ? [] : patches.slice(0, lastNonNavigationIndex + 1);
1651
+ };
1652
+
1653
+ const diffTree = (oldNodes, newNodes) => {
1654
+ // Step 1: Convert flat arrays to tree structures
1655
+ const oldTree = arrayToTree(oldNodes);
1656
+ const newTree = arrayToTree(newNodes);
1657
+ // Step 3: Compare the trees
1658
+ const patches = [];
1659
+ diffTrees(oldTree, newTree, patches, []);
1660
+ // Remove trailing navigation patches since they serve no purpose
1661
+ return removeTrailingNavigationPatches(patches);
1662
+ };
1663
+
1664
+ const getKeyBindings = () => {
1665
+ return [{
1666
+ command: 'Chat.handleSubmit',
1667
+ key: Enter,
1668
+ when: FocusChatInput
1669
+ }, {
1670
+ command: 'Chat.enterNewLine',
1671
+ key: Shift | Enter,
1672
+ when: FocusChatInput
1673
+ }];
1674
+ };
1675
+
1676
+ const SESSION_PREFIX$1 = 'session:';
1677
+ const CHAT_LIST_ITEM_CONTEXT_MENU = 'ChatListItemContextMenu';
1678
+ const handleChatListContextMenu = async (name, x, y) => {
1679
+ if (!name || !name.startsWith(SESSION_PREFIX$1)) {
1680
+ return;
1681
+ }
1682
+ const sessionId = name.slice(SESSION_PREFIX$1.length);
1683
+ // @ts-ignore
1684
+ await invoke('ContextMenu.show', x, y, CHAT_LIST_ITEM_CONTEXT_MENU, sessionId);
1685
+ };
1686
+
1687
+ const LVCE_CHAT_SESSIONS_DB_NAME = 'lvce-chat-view-sessions';
1688
+ const LVCE_CHAT_SESSIONS_DB_VERSION = 1;
1689
+ const LVCE_CHAT_SESSIONS_STORE = 'chat-sessions';
1690
+ const toError = error => {
1691
+ if (error instanceof Error) {
1692
+ return error;
1693
+ }
1694
+ return new Error('IndexedDB request failed');
1695
+ };
1696
+ const requestToPromise = async createRequest => {
1697
+ const request = createRequest();
1698
+ return new Promise((resolve, reject) => {
1699
+ request.addEventListener('success', () => {
1700
+ resolve(request.result);
1701
+ });
1702
+ request.addEventListener('error', () => {
1703
+ reject(toError(request.error));
1704
+ });
1705
+ });
1706
+ };
1707
+ const transactionToPromise = async createTransaction => {
1708
+ const transaction = createTransaction();
1709
+ return new Promise((resolve, reject) => {
1710
+ transaction.addEventListener('complete', () => {
1711
+ resolve();
1712
+ });
1713
+ transaction.addEventListener('error', () => {
1714
+ reject(toError(transaction.error));
1715
+ });
1716
+ transaction.addEventListener('abort', () => {
1717
+ reject(toError(transaction.error));
1718
+ });
1719
+ });
1720
+ };
1721
+ const openSessionsDatabase = async () => {
1722
+ const request = indexedDB.open(LVCE_CHAT_SESSIONS_DB_NAME, LVCE_CHAT_SESSIONS_DB_VERSION);
1723
+ request.addEventListener('upgradeneeded', () => {
1724
+ const database = request.result;
1725
+ if (!database.objectStoreNames.contains(LVCE_CHAT_SESSIONS_STORE)) {
1726
+ database.createObjectStore(LVCE_CHAT_SESSIONS_STORE, {
1727
+ keyPath: 'id'
1728
+ });
1729
+ }
1730
+ });
1731
+ return requestToPromise(() => request);
1732
+ };
1733
+ class IndexedDbChatSessionStorage {
1734
+ async getDatabase() {
1735
+ if (!this.databasePromise) {
1736
+ this.databasePromise = openSessionsDatabase();
1737
+ }
1738
+ return this.databasePromise;
1739
+ }
1740
+ async clear() {
1741
+ const database = await this.getDatabase();
1742
+ const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readwrite');
1743
+ const createTransaction = () => transaction;
1744
+ const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1745
+ store.clear();
1746
+ await transactionToPromise(createTransaction);
1747
+ }
1748
+ async deleteSession(id) {
1749
+ const database = await this.getDatabase();
1750
+ const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readwrite');
1751
+ const createTransaction = () => transaction;
1752
+ const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1753
+ store.delete(id);
1754
+ await transactionToPromise(createTransaction);
1755
+ }
1756
+ async getSession(id) {
1757
+ const database = await this.getDatabase();
1758
+ const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readonly');
1759
+ const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1760
+ const result = await requestToPromise(() => store.get(id));
1761
+ return result;
1762
+ }
1763
+ async listSessions() {
1764
+ const database = await this.getDatabase();
1765
+ const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readonly');
1766
+ const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1767
+ const result = await requestToPromise(() => store.getAll());
1768
+ return result;
1769
+ }
1770
+ async setSession(session) {
1771
+ const database = await this.getDatabase();
1772
+ const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readwrite');
1773
+ const createTransaction = () => transaction;
1774
+ const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1775
+ store.put(session);
1776
+ await transactionToPromise(createTransaction);
1777
+ }
1778
+ }
1329
1779
 
1330
- const diff2 = uid => {
1331
- const {
1332
- newState,
1333
- oldState
1334
- } = get(uid);
1335
- const result = diff(oldState, newState);
1336
- return result;
1337
- };
1780
+ class InMemoryChatSessionStorage {
1781
+ sessions = new Map();
1782
+ async clear() {
1783
+ this.sessions.clear();
1784
+ }
1785
+ async deleteSession(id) {
1786
+ this.sessions.delete(id);
1787
+ }
1788
+ async getSession(id) {
1789
+ return this.sessions.get(id);
1790
+ }
1791
+ async listSessions() {
1792
+ return [...this.sessions.values()];
1793
+ }
1794
+ async setSession(session) {
1795
+ this.sessions.set(session.id, session);
1796
+ }
1797
+ }
1338
1798
 
1339
- const getKeyBindings = () => {
1340
- return [];
1799
+ const createDefaultStorage = () => {
1800
+ if (typeof indexedDB === 'undefined') {
1801
+ return new InMemoryChatSessionStorage();
1802
+ }
1803
+ return new IndexedDbChatSessionStorage();
1341
1804
  };
1342
-
1343
- const SESSION_PREFIX$1 = 'session:';
1344
- const CHAT_LIST_ITEM_CONTEXT_MENU = 'ChatListItemContextMenu';
1345
- const handleChatListContextMenu = async (name, x, y) => {
1346
- if (!name || !name.startsWith(SESSION_PREFIX$1)) {
1347
- return;
1805
+ let chatSessionStorage = createDefaultStorage();
1806
+ const listChatSessions = async () => {
1807
+ const sessions = await chatSessionStorage.listSessions();
1808
+ return sessions.map(session => ({
1809
+ id: session.id,
1810
+ messages: [],
1811
+ title: session.title
1812
+ }));
1813
+ };
1814
+ const getChatSession = async id => {
1815
+ const session = await chatSessionStorage.getSession(id);
1816
+ if (!session) {
1817
+ return undefined;
1348
1818
  }
1349
- const sessionId = name.slice(SESSION_PREFIX$1.length);
1350
- // @ts-ignore
1351
- await invoke('ContextMenu.show', x, y, CHAT_LIST_ITEM_CONTEXT_MENU, sessionId);
1819
+ return {
1820
+ id: session.id,
1821
+ messages: [...session.messages],
1822
+ title: session.title
1823
+ };
1824
+ };
1825
+ const saveChatSession = async session => {
1826
+ await chatSessionStorage.setSession({
1827
+ id: session.id,
1828
+ messages: [...session.messages],
1829
+ title: session.title
1830
+ });
1831
+ };
1832
+ const deleteChatSession = async id => {
1833
+ await chatSessionStorage.deleteSession(id);
1834
+ };
1835
+ const clearChatSessions = async () => {
1836
+ await chatSessionStorage.clear();
1352
1837
  };
1353
1838
 
1354
1839
  const generateSessionId = () => {
1355
1840
  return crypto.randomUUID();
1356
1841
  };
1357
1842
 
1358
- const createSession = state => {
1843
+ const createSession = async state => {
1359
1844
  const id = generateSessionId();
1360
1845
  const session = {
1361
1846
  id,
1362
1847
  messages: [],
1363
1848
  title: `Chat ${state.sessions.length + 1}`
1364
1849
  };
1850
+ await saveChatSession(session);
1365
1851
  return {
1366
1852
  ...state,
1367
1853
  renamingSessionId: '',
@@ -1382,7 +1868,7 @@ const getNextSelectedSessionId = (sessions, deletedId) => {
1382
1868
  return sessions[nextIndex].id;
1383
1869
  };
1384
1870
 
1385
- const deleteSession = (state, id) => {
1871
+ const deleteSession = async (state, id) => {
1386
1872
  const {
1387
1873
  renamingSessionId,
1388
1874
  sessions
@@ -1391,6 +1877,7 @@ const deleteSession = (state, id) => {
1391
1877
  if (filtered.length === sessions.length) {
1392
1878
  return state;
1393
1879
  }
1880
+ await deleteChatSession(id);
1394
1881
  if (filtered.length === 0) {
1395
1882
  return {
1396
1883
  ...state,
@@ -1400,11 +1887,22 @@ const deleteSession = (state, id) => {
1400
1887
  viewMode: 'list'
1401
1888
  };
1402
1889
  }
1890
+ const nextSelectedSessionId = getNextSelectedSessionId(filtered, id);
1891
+ const loadedSession = await getChatSession(nextSelectedSessionId);
1892
+ const hydratedSessions = filtered.map(session => {
1893
+ if (session.id !== nextSelectedSessionId) {
1894
+ return session;
1895
+ }
1896
+ if (!loadedSession) {
1897
+ return session;
1898
+ }
1899
+ return loadedSession;
1900
+ });
1403
1901
  return {
1404
1902
  ...state,
1405
1903
  renamingSessionId: renamingSessionId === id ? '' : renamingSessionId,
1406
- selectedSessionId: getNextSelectedSessionId(filtered, id),
1407
- sessions: filtered
1904
+ selectedSessionId: nextSelectedSessionId,
1905
+ sessions: hydratedSessions
1408
1906
  };
1409
1907
  };
1410
1908
 
@@ -1458,14 +1956,27 @@ const handleSubmit = async state => {
1458
1956
  text: userText,
1459
1957
  time: userTime
1460
1958
  };
1959
+ let workingSessions = sessions;
1960
+ if (viewMode === 'detail') {
1961
+ const loadedSession = await getChatSession(selectedSessionId);
1962
+ if (loadedSession) {
1963
+ workingSessions = sessions.map(session => {
1964
+ if (session.id !== selectedSessionId) {
1965
+ return session;
1966
+ }
1967
+ return loadedSession;
1968
+ });
1969
+ }
1970
+ }
1461
1971
  let optimisticState;
1462
1972
  if (viewMode === 'list') {
1463
1973
  const newSessionId = generateSessionId();
1464
1974
  const newSession = {
1465
1975
  id: newSessionId,
1466
1976
  messages: [userMessage],
1467
- title: `Chat ${sessions.length + 1}`
1977
+ title: `Chat ${workingSessions.length + 1}`
1468
1978
  };
1979
+ await saveChatSession(newSession);
1469
1980
  optimisticState = focusInput({
1470
1981
  ...state,
1471
1982
  composerValue: '',
@@ -1473,11 +1984,11 @@ const handleSubmit = async state => {
1473
1984
  lastSubmittedSessionId: newSessionId,
1474
1985
  nextMessageId: nextMessageId + 1,
1475
1986
  selectedSessionId: newSessionId,
1476
- sessions: [...sessions, newSession],
1987
+ sessions: [...workingSessions, newSession],
1477
1988
  viewMode: 'detail'
1478
1989
  });
1479
1990
  } else {
1480
- const updatedSessions = sessions.map(session => {
1991
+ const updatedSessions = workingSessions.map(session => {
1481
1992
  if (session.id !== selectedSessionId) {
1482
1993
  return session;
1483
1994
  }
@@ -1486,6 +1997,10 @@ const handleSubmit = async state => {
1486
1997
  messages: [...session.messages, userMessage]
1487
1998
  };
1488
1999
  });
2000
+ const selectedSession = updatedSessions.find(session => session.id === selectedSessionId);
2001
+ if (selectedSession) {
2002
+ await saveChatSession(selectedSession);
2003
+ }
1489
2004
  optimisticState = focusInput({
1490
2005
  ...state,
1491
2006
  composerValue: '',
@@ -1508,6 +2023,10 @@ const handleSubmit = async state => {
1508
2023
  messages: [...session.messages, assistantMessage]
1509
2024
  };
1510
2025
  });
2026
+ const selectedSession = updatedSessions.find(session => session.id === optimisticState.selectedSessionId);
2027
+ if (selectedSession) {
2028
+ await saveChatSession(selectedSession);
2029
+ }
1511
2030
  return focusInput({
1512
2031
  ...optimisticState,
1513
2032
  nextMessageId: optimisticState.nextMessageId + 1,
@@ -1529,15 +2048,26 @@ const handleClickSend = async state => {
1529
2048
  return handleSubmit(submitState);
1530
2049
  };
1531
2050
 
1532
- const selectSession = (state, id) => {
2051
+ const selectSession = async (state, id) => {
1533
2052
  const exists = state.sessions.some(session => session.id === id);
1534
2053
  if (!exists) {
1535
2054
  return state;
1536
2055
  }
2056
+ const loadedSession = await getChatSession(id);
2057
+ const sessions = state.sessions.map(session => {
2058
+ if (session.id !== id) {
2059
+ return session;
2060
+ }
2061
+ if (!loadedSession) {
2062
+ return session;
2063
+ }
2064
+ return loadedSession;
2065
+ });
1537
2066
  return {
1538
2067
  ...state,
1539
2068
  renamingSessionId: '',
1540
2069
  selectedSessionId: id,
2070
+ sessions,
1541
2071
  viewMode: 'detail'
1542
2072
  };
1543
2073
  };
@@ -1649,6 +2179,7 @@ const handleClickNew = async state => {
1649
2179
 
1650
2180
  const handleClickSettings = async () => {
1651
2181
  // TODO
2182
+ await invoke('Main.openUri', 'app://settings.json');
1652
2183
  };
1653
2184
 
1654
2185
  const handleInput = async (state, value, inputSource = 'user') => {
@@ -1690,7 +2221,7 @@ const handleInputFocus = async (state, name) => {
1690
2221
  };
1691
2222
  };
1692
2223
 
1693
- const submitRename = state => {
2224
+ const submitRename = async state => {
1694
2225
  const {
1695
2226
  composerValue,
1696
2227
  renamingSessionId,
@@ -1712,6 +2243,10 @@ const submitRename = state => {
1712
2243
  title
1713
2244
  };
1714
2245
  });
2246
+ const renamedSession = updatedSessions.find(session => session.id === renamingSessionId);
2247
+ if (renamedSession) {
2248
+ await saveChatSession(renamedSession);
2249
+ }
1715
2250
  return {
1716
2251
  ...state,
1717
2252
  composerValue: '',
@@ -1744,6 +2279,17 @@ const handleKeyDown = async (state, key, shiftKey) => {
1744
2279
  return handleSubmit(submitState);
1745
2280
  };
1746
2281
 
2282
+ const handleModelChange = async (state, value) => {
2283
+ return {
2284
+ ...state,
2285
+ selectedModelId: value
2286
+ };
2287
+ };
2288
+
2289
+ const handleNewline = async state => {
2290
+ return handleInput(state, `${state.composerValue}\n`);
2291
+ };
2292
+
1747
2293
  const id = 7201;
1748
2294
  const sendMessagePortToExtensionHostWorker = async port => {
1749
2295
  await sendMessagePortToExtensionHostWorker$1(port, id);
@@ -1800,379 +2346,186 @@ const getSavedBounds = savedState => {
1800
2346
  };
1801
2347
  };
1802
2348
 
1803
- const getSavedSelectedSessionId = savedState => {
1804
- if (!isObject(savedState)) {
1805
- return undefined;
1806
- }
1807
- const {
1808
- selectedSessionId
1809
- } = savedState;
1810
- if (typeof selectedSessionId !== 'string') {
1811
- return undefined;
1812
- }
1813
- return selectedSessionId;
1814
- };
1815
-
1816
- const getSavedSessions = savedState => {
2349
+ const getSavedSelectedModelId = savedState => {
1817
2350
  if (!isObject(savedState)) {
1818
2351
  return undefined;
1819
2352
  }
1820
2353
  const {
1821
- sessions
2354
+ selectedModelId
1822
2355
  } = savedState;
1823
- if (!Array.isArray(sessions)) {
2356
+ if (typeof selectedModelId !== 'string') {
1824
2357
  return undefined;
1825
2358
  }
1826
- return sessions;
1827
- };
1828
-
1829
- const loadContent = async (state, savedState) => {
1830
- const savedBounds = getSavedBounds(savedState);
1831
- const sessions = getSavedSessions(savedState) || state.sessions;
1832
- const preferredSessionId = getSavedSelectedSessionId(savedState) || state.selectedSessionId;
1833
- const selectedSessionId = sessions.some(session => session.id === preferredSessionId) ? preferredSessionId : sessions[0]?.id || '';
1834
- const viewMode = sessions.length === 0 ? 'list' : state.viewMode === 'detail' ? 'detail' : 'list';
1835
- return {
1836
- ...state,
1837
- ...savedBounds,
1838
- initial: false,
1839
- selectedSessionId,
1840
- sessions,
1841
- viewMode
1842
- };
1843
- };
1844
-
1845
- // TODO render things like scrollbar height,scrollbar offset, textarea height,
1846
- // list height
1847
- const css = `
1848
- `;
1849
- const renderCss = (oldState, newState) => {
1850
- return [SetCss, newState.uid, css];
1851
- };
1852
-
1853
- const getFocusSelector = focus => {
1854
- switch (focus) {
1855
- case 'composer':
1856
- case 'input':
1857
- return '[name="composer"]';
1858
- case 'header':
1859
- return '[name="create-session"]';
1860
- case 'list':
1861
- return '[name^="session:"]';
1862
- case 'send-button':
1863
- return '[name="send"]';
1864
- default:
1865
- return '[name="composer"]';
1866
- }
1867
- };
1868
- const renderFocus = (oldState, newState) => {
1869
- const selector = getFocusSelector(newState.focus);
1870
- return [FocusSelector, selector];
1871
- };
1872
-
1873
- const mergeClassNames = (...classNames) => {
1874
- return classNames.filter(Boolean).join(' ');
1875
- };
1876
-
1877
- const text = data => {
1878
- return {
1879
- childCount: 0,
1880
- text: data,
1881
- type: Text
1882
- };
1883
- };
1884
-
1885
- const SetText = 1;
1886
- const Replace = 2;
1887
- const SetAttribute = 3;
1888
- const RemoveAttribute = 4;
1889
- const Add = 6;
1890
- const NavigateChild = 7;
1891
- const NavigateParent = 8;
1892
- const RemoveChild = 9;
1893
- const NavigateSibling = 10;
1894
- const SetReferenceNodeUid = 11;
1895
-
1896
- const isKey = key => {
1897
- return key !== 'type' && key !== 'childCount';
1898
- };
1899
-
1900
- const getKeys = node => {
1901
- const keys = Object.keys(node).filter(isKey);
1902
- return keys;
1903
- };
1904
-
1905
- const arrayToTree = nodes => {
1906
- const result = [];
1907
- let i = 0;
1908
- while (i < nodes.length) {
1909
- const node = nodes[i];
1910
- const {
1911
- children,
1912
- nodesConsumed
1913
- } = getChildrenWithCount(nodes, i + 1, node.childCount || 0);
1914
- result.push({
1915
- node,
1916
- children
1917
- });
1918
- i += 1 + nodesConsumed;
1919
- }
1920
- return result;
1921
- };
1922
- const getChildrenWithCount = (nodes, startIndex, childCount) => {
1923
- if (childCount === 0) {
1924
- return {
1925
- children: [],
1926
- nodesConsumed: 0
1927
- };
1928
- }
1929
- const children = [];
1930
- let i = startIndex;
1931
- let remaining = childCount;
1932
- let totalConsumed = 0;
1933
- while (remaining > 0 && i < nodes.length) {
1934
- const node = nodes[i];
1935
- const nodeChildCount = node.childCount || 0;
1936
- const {
1937
- children: nodeChildren,
1938
- nodesConsumed
1939
- } = getChildrenWithCount(nodes, i + 1, nodeChildCount);
1940
- children.push({
1941
- node,
1942
- children: nodeChildren
1943
- });
1944
- const nodeSize = 1 + nodesConsumed;
1945
- i += nodeSize;
1946
- totalConsumed += nodeSize;
1947
- remaining--;
1948
- }
1949
- return {
1950
- children,
1951
- nodesConsumed: totalConsumed
1952
- };
1953
- };
1954
-
1955
- const compareNodes = (oldNode, newNode) => {
1956
- const patches = [];
1957
- // Check if node type changed - return null to signal incompatible nodes
1958
- // (caller should handle this with a Replace operation)
1959
- if (oldNode.type !== newNode.type) {
1960
- return null;
1961
- }
1962
- // Handle reference nodes - special handling for uid changes
1963
- if (oldNode.type === Reference) {
1964
- if (oldNode.uid !== newNode.uid) {
1965
- patches.push({
1966
- type: SetReferenceNodeUid,
1967
- uid: newNode.uid
1968
- });
1969
- }
1970
- return patches;
1971
- }
1972
- // Handle text nodes
1973
- if (oldNode.type === Text && newNode.type === Text) {
1974
- if (oldNode.text !== newNode.text) {
1975
- patches.push({
1976
- type: SetText,
1977
- value: newNode.text
1978
- });
1979
- }
1980
- return patches;
1981
- }
1982
- // Compare attributes
1983
- const oldKeys = getKeys(oldNode);
1984
- const newKeys = getKeys(newNode);
1985
- // Check for attribute changes
1986
- for (const key of newKeys) {
1987
- if (oldNode[key] !== newNode[key]) {
1988
- patches.push({
1989
- type: SetAttribute,
1990
- key,
1991
- value: newNode[key]
1992
- });
1993
- }
1994
- }
1995
- // Check for removed attributes
1996
- for (const key of oldKeys) {
1997
- if (!(key in newNode)) {
1998
- patches.push({
1999
- type: RemoveAttribute,
2000
- key
2001
- });
2002
- }
2003
- }
2004
- return patches;
2359
+ return selectedModelId;
2005
2360
  };
2006
2361
 
2007
- const treeToArray = node => {
2008
- const result = [node.node];
2009
- for (const child of node.children) {
2010
- result.push(...treeToArray(child));
2362
+ const getSavedSelectedSessionId = savedState => {
2363
+ if (!isObject(savedState)) {
2364
+ return undefined;
2011
2365
  }
2012
- return result;
2366
+ const {
2367
+ selectedSessionId
2368
+ } = savedState;
2369
+ if (typeof selectedSessionId !== 'string') {
2370
+ return undefined;
2371
+ }
2372
+ return selectedSessionId;
2013
2373
  };
2014
2374
 
2015
- const diffChildren = (oldChildren, newChildren, patches) => {
2016
- const maxLength = Math.max(oldChildren.length, newChildren.length);
2017
- // Track where we are: -1 means at parent, >= 0 means at child index
2018
- let currentChildIndex = -1;
2019
- // Collect indices of children to remove (we'll add these patches at the end in reverse order)
2020
- const indicesToRemove = [];
2021
- for (let i = 0; i < maxLength; i++) {
2022
- const oldNode = oldChildren[i];
2023
- const newNode = newChildren[i];
2024
- if (!oldNode && !newNode) {
2025
- continue;
2026
- }
2027
- if (!oldNode) {
2028
- // Add new node - we should be at the parent
2029
- if (currentChildIndex >= 0) {
2030
- // Navigate back to parent
2031
- patches.push({
2032
- type: NavigateParent
2033
- });
2034
- currentChildIndex = -1;
2035
- }
2036
- // Flatten the entire subtree so renderInternal can handle it
2037
- const flatNodes = treeToArray(newNode);
2038
- patches.push({
2039
- type: Add,
2040
- nodes: flatNodes
2041
- });
2042
- } else if (newNode) {
2043
- // Compare nodes to see if we need any patches
2044
- const nodePatches = compareNodes(oldNode.node, newNode.node);
2045
- // If nodePatches is null, the node types are incompatible - need to replace
2046
- if (nodePatches === null) {
2047
- // Navigate to this child
2048
- if (currentChildIndex === -1) {
2049
- patches.push({
2050
- type: NavigateChild,
2051
- index: i
2052
- });
2053
- currentChildIndex = i;
2054
- } else if (currentChildIndex !== i) {
2055
- patches.push({
2056
- type: NavigateSibling,
2057
- index: i
2058
- });
2059
- currentChildIndex = i;
2060
- }
2061
- // Replace the entire subtree
2062
- const flatNodes = treeToArray(newNode);
2063
- patches.push({
2064
- type: Replace,
2065
- nodes: flatNodes
2066
- });
2067
- // After replace, we're at the new element (same position)
2068
- continue;
2069
- }
2070
- // Check if we need to recurse into children
2071
- const hasChildrenToCompare = oldNode.children.length > 0 || newNode.children.length > 0;
2072
- // Only navigate to this element if we need to do something
2073
- if (nodePatches.length > 0 || hasChildrenToCompare) {
2074
- // Navigate to this child if not already there
2075
- if (currentChildIndex === -1) {
2076
- patches.push({
2077
- type: NavigateChild,
2078
- index: i
2079
- });
2080
- currentChildIndex = i;
2081
- } else if (currentChildIndex !== i) {
2082
- patches.push({
2083
- type: NavigateSibling,
2084
- index: i
2085
- });
2086
- currentChildIndex = i;
2087
- }
2088
- // Apply node patches (these apply to the current element, not children)
2089
- if (nodePatches.length > 0) {
2090
- patches.push(...nodePatches);
2091
- }
2092
- // Compare children recursively
2093
- if (hasChildrenToCompare) {
2094
- diffChildren(oldNode.children, newNode.children, patches);
2095
- }
2096
- }
2097
- } else {
2098
- // Remove old node - collect the index for later removal
2099
- indicesToRemove.push(i);
2100
- }
2375
+ const getSavedSessions = savedState => {
2376
+ if (!isObject(savedState)) {
2377
+ return undefined;
2101
2378
  }
2102
- // Navigate back to parent if we ended at a child
2103
- if (currentChildIndex >= 0) {
2104
- patches.push({
2105
- type: NavigateParent
2106
- });
2107
- currentChildIndex = -1;
2379
+ const {
2380
+ sessions
2381
+ } = savedState;
2382
+ if (!Array.isArray(sessions)) {
2383
+ return undefined;
2108
2384
  }
2109
- // Add remove patches in reverse order (highest index first)
2110
- // This ensures indices remain valid as we remove
2111
- for (let j = indicesToRemove.length - 1; j >= 0; j--) {
2112
- patches.push({
2113
- type: RemoveChild,
2114
- index: indicesToRemove[j]
2115
- });
2385
+ return sessions;
2386
+ };
2387
+
2388
+ const getSavedViewMode = savedState => {
2389
+ if (!isObject(savedState)) {
2390
+ return undefined;
2391
+ }
2392
+ const {
2393
+ viewMode
2394
+ } = savedState;
2395
+ if (viewMode !== 'list' && viewMode !== 'detail') {
2396
+ return undefined;
2116
2397
  }
2398
+ return viewMode;
2117
2399
  };
2118
- const diffTrees = (oldTree, newTree, patches, path) => {
2119
- // At the root level (path.length === 0), we're already AT the element
2120
- // So we compare the root node directly, then compare its children
2121
- if (path.length === 0 && oldTree.length === 1 && newTree.length === 1) {
2122
- const oldNode = oldTree[0];
2123
- const newNode = newTree[0];
2124
- // Compare root nodes
2125
- const nodePatches = compareNodes(oldNode.node, newNode.node);
2126
- // If nodePatches is null, the root node types are incompatible - need to replace
2127
- if (nodePatches === null) {
2128
- const flatNodes = treeToArray(newNode);
2129
- patches.push({
2130
- type: Replace,
2131
- nodes: flatNodes
2132
- });
2133
- return;
2400
+
2401
+ const toSummarySession = session => {
2402
+ return {
2403
+ id: session.id,
2404
+ messages: [],
2405
+ title: session.title
2406
+ };
2407
+ };
2408
+ const loadSelectedSessionMessages = async (sessions, selectedSessionId) => {
2409
+ if (!selectedSessionId) {
2410
+ return sessions;
2411
+ }
2412
+ const loadedSession = await getChatSession(selectedSessionId);
2413
+ if (!loadedSession) {
2414
+ return sessions;
2415
+ }
2416
+ return sessions.map(session => {
2417
+ if (session.id !== selectedSessionId) {
2418
+ return session;
2134
2419
  }
2135
- if (nodePatches.length > 0) {
2136
- patches.push(...nodePatches);
2420
+ return loadedSession;
2421
+ });
2422
+ };
2423
+ const loadContent = async (state, savedState) => {
2424
+ const savedBounds = getSavedBounds(savedState);
2425
+ const savedSelectedModelId = getSavedSelectedModelId(savedState);
2426
+ const savedViewMode = getSavedViewMode(savedState);
2427
+ const legacySavedSessions = getSavedSessions(savedState);
2428
+ const storedSessions = await listChatSessions();
2429
+ let sessions = storedSessions;
2430
+ if (sessions.length === 0 && legacySavedSessions && legacySavedSessions.length > 0) {
2431
+ for (const session of legacySavedSessions) {
2432
+ await saveChatSession(session);
2137
2433
  }
2138
- // Compare children
2139
- if (oldNode.children.length > 0 || newNode.children.length > 0) {
2140
- diffChildren(oldNode.children, newNode.children, patches);
2434
+ sessions = legacySavedSessions.map(toSummarySession);
2435
+ }
2436
+ if (sessions.length === 0 && state.sessions.length > 0) {
2437
+ for (const session of state.sessions) {
2438
+ await saveChatSession(session);
2141
2439
  }
2142
- } else {
2143
- // Non-root level or multiple root elements - use the regular comparison
2144
- diffChildren(oldTree, newTree, patches);
2440
+ sessions = state.sessions.map(toSummarySession);
2145
2441
  }
2442
+ const preferredSessionId = getSavedSelectedSessionId(savedState) || state.selectedSessionId;
2443
+ const preferredModelId = savedSelectedModelId || state.selectedModelId;
2444
+ const selectedModelId = state.models.some(model => model.id === preferredModelId) ? preferredModelId : state.models[0]?.id || '';
2445
+ const selectedSessionId = sessions.some(session => session.id === preferredSessionId) ? preferredSessionId : sessions[0]?.id || '';
2446
+ sessions = await loadSelectedSessionMessages(sessions, selectedSessionId);
2447
+ const preferredViewMode = savedViewMode || state.viewMode;
2448
+ const viewMode = sessions.length === 0 || !selectedSessionId ? 'list' : preferredViewMode === 'detail' ? 'detail' : 'list';
2449
+ return {
2450
+ ...state,
2451
+ ...savedBounds,
2452
+ initial: false,
2453
+ selectedModelId,
2454
+ selectedSessionId,
2455
+ sessions,
2456
+ viewMode
2457
+ };
2146
2458
  };
2147
2459
 
2148
- const removeTrailingNavigationPatches = patches => {
2149
- // Find the last non-navigation patch
2150
- let lastNonNavigationIndex = -1;
2151
- for (let i = patches.length - 1; i >= 0; i--) {
2152
- const patch = patches[i];
2153
- if (patch.type !== NavigateChild && patch.type !== NavigateParent && patch.type !== NavigateSibling) {
2154
- lastNonNavigationIndex = i;
2155
- break;
2460
+ const openMockSession = async (state, mockSessionId, mockChatMessages) => {
2461
+ if (!mockSessionId) {
2462
+ return state;
2463
+ }
2464
+ const existingSession = state.sessions.find(session => session.id === mockSessionId);
2465
+ const sessions = existingSession ? state.sessions.map(session => {
2466
+ if (session.id !== mockSessionId) {
2467
+ return session;
2156
2468
  }
2469
+ return {
2470
+ ...session,
2471
+ messages: mockChatMessages
2472
+ };
2473
+ }) : [...state.sessions, {
2474
+ id: mockSessionId,
2475
+ messages: mockChatMessages,
2476
+ title: mockSessionId
2477
+ }];
2478
+ const selectedSession = sessions.find(session => session.id === mockSessionId);
2479
+ if (selectedSession) {
2480
+ await saveChatSession(selectedSession);
2157
2481
  }
2158
- // Return patches up to and including the last non-navigation patch
2159
- return lastNonNavigationIndex === -1 ? [] : patches.slice(0, lastNonNavigationIndex + 1);
2482
+ return {
2483
+ ...state,
2484
+ renamingSessionId: '',
2485
+ selectedSessionId: mockSessionId,
2486
+ sessions,
2487
+ viewMode: 'detail'
2488
+ };
2160
2489
  };
2161
2490
 
2162
- const diffTree = (oldNodes, newNodes) => {
2163
- // Step 1: Convert flat arrays to tree structures
2164
- const oldTree = arrayToTree(oldNodes);
2165
- const newTree = arrayToTree(newNodes);
2166
- // Step 3: Compare the trees
2167
- const patches = [];
2168
- diffTrees(oldTree, newTree, patches, []);
2169
- // Remove trailing navigation patches since they serve no purpose
2170
- return removeTrailingNavigationPatches(patches);
2491
+ // TODO render things like scrollbar height,scrollbar offset, textarea height,
2492
+ // list height
2493
+ const css = `
2494
+ `;
2495
+ const renderCss = (oldState, newState) => {
2496
+ return [SetCss, newState.uid, css];
2497
+ };
2498
+
2499
+ const getFocusSelector = focus => {
2500
+ switch (focus) {
2501
+ case 'composer':
2502
+ case 'input':
2503
+ return '[name="composer"]';
2504
+ case 'header':
2505
+ return '[name="create-session"]';
2506
+ case 'list':
2507
+ return '[name^="session:"]';
2508
+ case 'send-button':
2509
+ return '[name="send"]';
2510
+ default:
2511
+ return '[name="composer"]';
2512
+ }
2513
+ };
2514
+ const renderFocus = (oldState, newState) => {
2515
+ const selector = getFocusSelector(newState.focus);
2516
+ return [FocusSelector, selector];
2517
+ };
2518
+
2519
+ const renderFocusContext = (oldState, newState) => {
2520
+ const when = 2344;
2521
+ return [SetFocusContext, newState.uid, when];
2171
2522
  };
2172
2523
 
2173
2524
  const ChatActions = 'ChatActions';
2174
2525
  const ChatName = 'ChatName';
2175
2526
  const ChatSendArea = 'ChatSendArea';
2527
+ const ChatSendAreaBottom = 'ChatSendAreaBottom';
2528
+ const ChatSendAreaContent = 'ChatSendAreaContent';
2176
2529
  const Chat = 'Chat';
2177
2530
  const ChatHeader = 'ChatHeader';
2178
2531
  const Button = 'Button';
@@ -2181,11 +2534,16 @@ const ButtonPrimary = 'ButtonPrimary';
2181
2534
  const IconButton = 'IconButton';
2182
2535
  const Label = 'Label';
2183
2536
  const ChatList = 'ChatList';
2537
+ const ChatListEmpty = 'ChatListEmpty';
2184
2538
  const ChatListItem = 'ChatListItem';
2185
2539
  const ChatListItemLabel = 'ChatListItemLabel';
2186
2540
  const Markdown = 'Markdown';
2187
2541
  const Message = 'Message';
2542
+ const MessageUser = 'MessageUser';
2543
+ const MessageAssistant = 'MessageAssistant';
2188
2544
  const MultilineInputBox = 'MultilineInputBox';
2545
+ const Option = 'Option';
2546
+ const Select = 'Select';
2189
2547
  const Viewlet = 'Viewlet';
2190
2548
  const ChatWelcomeMessage = 'ChatWelcomeMessage';
2191
2549
 
@@ -2201,14 +2559,28 @@ const HandleClickBack = 16;
2201
2559
  const HandleClickList = 17;
2202
2560
  const HandleClickDelete = 18;
2203
2561
  const HandleSubmit = 19;
2562
+ const HandleModelChange = 20;
2204
2563
 
2205
- const getChatSendAreaDom = composerValue => {
2564
+ const getChatSendAreaDom = (composerValue, models, selectedModelId) => {
2206
2565
  const isSendDisabled = composerValue.trim() === '';
2207
2566
  const sendButtonClassName = isSendDisabled ? `${Button} ${ButtonPrimary} ${ButtonDisabled}` : `${Button} ${ButtonPrimary}`;
2567
+ const modelOptions = models.flatMap(model => {
2568
+ return [{
2569
+ childCount: 1,
2570
+ className: Option,
2571
+ selected: model.id === selectedModelId,
2572
+ type: Option$1,
2573
+ value: model.id
2574
+ }, text(model.name)];
2575
+ });
2208
2576
  return [{
2209
- childCount: 2,
2577
+ childCount: 1,
2210
2578
  className: ChatSendArea,
2211
2579
  type: Div
2580
+ }, {
2581
+ childCount: 2,
2582
+ className: ChatSendAreaContent,
2583
+ type: Div
2212
2584
  }, {
2213
2585
  childCount: 0,
2214
2586
  className: MultilineInputBox,
@@ -2219,6 +2591,17 @@ const getChatSendAreaDom = composerValue => {
2219
2591
  type: TextArea,
2220
2592
  value: composerValue
2221
2593
  }, {
2594
+ childCount: 2,
2595
+ className: ChatSendAreaBottom,
2596
+ type: Div
2597
+ }, {
2598
+ childCount: models.length,
2599
+ className: Select,
2600
+ name: 'model',
2601
+ onInput: HandleModelChange,
2602
+ type: Select$1,
2603
+ value: selectedModelId
2604
+ }, ...modelOptions, {
2222
2605
  childCount: 1,
2223
2606
  className: sendButtonClassName,
2224
2607
  disabled: isSendDisabled,
@@ -2295,9 +2678,10 @@ const getChatHeaderDomDetailMode = selectedSessionTitle => {
2295
2678
  };
2296
2679
 
2297
2680
  const getChatMessageDom = message => {
2681
+ const roleClassName = message.role === 'user' ? MessageUser : MessageAssistant;
2298
2682
  return [{
2299
2683
  childCount: 2,
2300
- className: Message,
2684
+ className: mergeClassNames(Message, roleClassName),
2301
2685
  type: Div
2302
2686
  }, {
2303
2687
  childCount: 1,
@@ -2325,7 +2709,7 @@ const getMessagesDom = messages => {
2325
2709
  }, ...messages.flatMap(getChatMessageDom)];
2326
2710
  };
2327
2711
 
2328
- const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue) => {
2712
+ const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId) => {
2329
2713
  const selectedSession = sessions.find(session => session.id === selectedSessionId);
2330
2714
  const selectedSessionTitle = selectedSession?.title || chatTitle;
2331
2715
  const messages = selectedSession ? selectedSession.messages : [];
@@ -2333,7 +2717,7 @@ const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue)
2333
2717
  childCount: 3,
2334
2718
  className: mergeClassNames(Viewlet, Chat),
2335
2719
  type: Div
2336
- }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages), ...getChatSendAreaDom(composerValue)];
2720
+ }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages), ...getChatSendAreaDom(composerValue, models, selectedModelId)];
2337
2721
  };
2338
2722
 
2339
2723
  const getChatHeaderListModeDom = () => {
@@ -2351,7 +2735,7 @@ const getChatHeaderListModeDom = () => {
2351
2735
  const getEmptyChatSessionsDom = () => {
2352
2736
  return [{
2353
2737
  childCount: 1,
2354
- className: ChatList,
2738
+ className: ChatListEmpty,
2355
2739
  type: Div
2356
2740
  }, {
2357
2741
  childCount: 1,
@@ -2385,7 +2769,7 @@ const getSessionDom = session => {
2385
2769
  onClick: HandleClickDelete,
2386
2770
  role: Button$2,
2387
2771
  tabIndex: 0,
2388
- title: deleteChatSession,
2772
+ title: deleteChatSession$1,
2389
2773
  type: Button$1
2390
2774
  }, text('🗑')];
2391
2775
  };
@@ -2402,12 +2786,12 @@ const getChatListDom = (sessions, selectedSessionId) => {
2402
2786
  }, ...sessions.flatMap(getSessionDom)];
2403
2787
  };
2404
2788
 
2405
- const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue) => {
2789
+ const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId) => {
2406
2790
  return [{
2407
2791
  childCount: 3,
2408
2792
  className: mergeClassNames(Viewlet, Chat),
2409
2793
  type: Div
2410
- }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions), ...getChatSendAreaDom(composerValue)];
2794
+ }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions), ...getChatSendAreaDom(composerValue, models, selectedModelId)];
2411
2795
  };
2412
2796
 
2413
2797
  const getChatModeUnsupportedVirtualDom = () => {
@@ -2417,12 +2801,12 @@ const getChatModeUnsupportedVirtualDom = () => {
2417
2801
  }, text('Unknown view mode')];
2418
2802
  };
2419
2803
 
2420
- const getChatVirtualDom = (sessions, selectedSessionId, composerValue, viewMode) => {
2804
+ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, viewMode, models, selectedModelId) => {
2421
2805
  switch (viewMode) {
2422
2806
  case 'detail':
2423
- return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue);
2807
+ return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId);
2424
2808
  case 'list':
2425
- return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue);
2809
+ return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId);
2426
2810
  default:
2427
2811
  return getChatModeUnsupportedVirtualDom();
2428
2812
  }
@@ -2432,6 +2816,8 @@ const renderItems = (oldState, newState) => {
2432
2816
  const {
2433
2817
  composerValue,
2434
2818
  initial,
2819
+ models,
2820
+ selectedModelId,
2435
2821
  selectedSessionId,
2436
2822
  sessions,
2437
2823
  uid,
@@ -2440,7 +2826,7 @@ const renderItems = (oldState, newState) => {
2440
2826
  if (initial) {
2441
2827
  return [SetDom2, uid, []];
2442
2828
  }
2443
- const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, viewMode);
2829
+ const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, viewMode, models, selectedModelId);
2444
2830
  return [SetDom2, uid, dom];
2445
2831
  };
2446
2832
 
@@ -2464,6 +2850,8 @@ const getRenderer = diffType => {
2464
2850
  return renderCss;
2465
2851
  case RenderFocus:
2466
2852
  return renderFocus;
2853
+ case RenderFocusContext:
2854
+ return renderFocusContext;
2467
2855
  case RenderIncremental:
2468
2856
  return renderIncremental;
2469
2857
  case RenderItems:
@@ -2526,6 +2914,9 @@ const renderEventListeners = () => {
2526
2914
  }, {
2527
2915
  name: HandleInput,
2528
2916
  params: ['handleInput', TargetValue]
2917
+ }, {
2918
+ name: HandleModelChange,
2919
+ params: ['handleModelChange', TargetValue]
2529
2920
  }, {
2530
2921
  name: HandleFocus,
2531
2922
  params: ['handleInputFocus', TargetName]
@@ -2538,7 +2929,12 @@ const renderEventListeners = () => {
2538
2929
  }];
2539
2930
  };
2540
2931
 
2932
+ const rerender = state => {
2933
+ return structuredClone(state);
2934
+ };
2935
+
2541
2936
  const reset = async state => {
2937
+ await clearChatSessions();
2542
2938
  return {
2543
2939
  ...state,
2544
2940
  composerValue: '',
@@ -2561,8 +2957,8 @@ const saveState = state => {
2561
2957
  height,
2562
2958
  nextMessageId,
2563
2959
  renamingSessionId,
2960
+ selectedModelId,
2564
2961
  selectedSessionId,
2565
- sessions,
2566
2962
  viewMode,
2567
2963
  width,
2568
2964
  x,
@@ -2573,8 +2969,8 @@ const saveState = state => {
2573
2969
  height,
2574
2970
  nextMessageId,
2575
2971
  renamingSessionId,
2972
+ selectedModelId,
2576
2973
  selectedSessionId,
2577
- sessions,
2578
2974
  viewMode,
2579
2975
  width,
2580
2976
  x,
@@ -2608,6 +3004,7 @@ const commandMap = {
2608
3004
  'Chat.clearInput': wrapCommand(clearInput),
2609
3005
  'Chat.create': create,
2610
3006
  'Chat.diff2': diff2,
3007
+ 'Chat.enterNewLine': wrapCommand(handleNewline),
2611
3008
  'Chat.getCommandIds': getCommandIds,
2612
3009
  'Chat.getKeyBindings': getKeyBindings,
2613
3010
  'Chat.handleChatListContextMenu': handleChatListContextMenu,
@@ -2621,12 +3018,15 @@ const commandMap = {
2621
3018
  'Chat.handleInput': wrapCommand(handleInput),
2622
3019
  'Chat.handleInputFocus': wrapCommand(handleInputFocus),
2623
3020
  'Chat.handleKeyDown': wrapCommand(handleKeyDown),
3021
+ 'Chat.handleModelChange': wrapCommand(handleModelChange),
2624
3022
  'Chat.handleSubmit': wrapCommand(handleSubmit),
2625
3023
  'Chat.initialize': initialize,
2626
3024
  'Chat.loadContent': wrapCommand(loadContent),
2627
3025
  'Chat.loadContent2': wrapCommand(loadContent),
3026
+ 'Chat.openMockSession': wrapCommand(openMockSession),
2628
3027
  'Chat.render2': render2,
2629
3028
  'Chat.renderEventListeners': renderEventListeners,
3029
+ 'Chat.rerender': wrapCommand(rerender),
2630
3030
  'Chat.reset': wrapCommand(reset),
2631
3031
  'Chat.resize': wrapCommand(resize),
2632
3032
  'Chat.saveState': wrapGetter(saveState),