LvyfZQ~7&dbV<-&(aiYdo0nUq#-)05tj
ze^~VO3&_GV!snX?LVZmNah!Kv_HY?L>I`Gndp<4oy`=+mo>rHhTNtXW?+<{wft$pX
z9p^L`@BzXiB3kQ0+w*g@BJkr5i@EzG^)!n5T~(B%WNIw~7fqe>3O8%Z+4!
zLoAtRdQAGocLA5@FyJRVoN=kBTu{dm$n9~_`{!!~>>?#+3>Ffjb9pF)iZWz%94eEG
z=wKRH?n?_8rkKKyT&PN_MRH`o31MCGR;vX}RO+J0#q}A7vd)l->XB
N($vtw7pPeU{|};&eTV=6
literal 0
HcmV?d00001
diff --git a/apps/x/packages/core/src/knowledge/chrome-extension/extension/icons/icon16.png b/apps/x/packages/core/src/knowledge/chrome-extension/extension/icons/icon16.png
new file mode 100644
index 0000000000000000000000000000000000000000..482631a1a327df0e235c160e5b8d4d953374387a
GIT binary patch
literal 912
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|DpDgn(|mmy
zw18|51{Ovs23C*~Ahu(ahO^xmH5iz|;!F$-?U@WLP&Ix)8pK=%BH%Rh0!FB6px6Q?
zxN3m~%m_9}pD>G9ERf=?Nk^)#sNw%$0gl~X?bAC~(f|;Iy
zo`I4bmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJ
zp<7&;SCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~%2@ZUNk)yke-Y
zfd133NG#Ad)HBeBn+()w1N5Vnb5UwyNq$jCetwP}$Z45**~Q6;1*sqh+UTRJfLdvz
z4{`^RyC9whivoRZ#|6|03qd=sO(&BSf#L1w>Eak-A-FVnZ&$N}z&&eSA>Aw2lzbg>
zLTb9S&k8TuKDD~t##zAnqh!d3!XxM096x?|RyOUnz*#pY$saT3&VN5U^@eJTK&R58
z9}64A8~2`Y)U)2>74v-|$Iqn^;w>8!B_3GtWz0JE`ovwe=xMBhB98N(2V6JhIn1!`
zbrIv2B*y#S6@7zW?BaX3HdE?C>EE3P^0qghetJmluJF&DI;X3RPE@ewwI6O&@pQ5|
zuRP_zW$t*9qe{mbG_|^z5+x43EJ^a-lq6?qVMr+
zk7=ok`=o-36<(w&b1`JM9`?7p=g-v|<(4x8Jt&cYK(?qa|JK
zw2`6jZlo#ZEMc-x%z(Q;9}!LEp=8OvTS{5GHAkFV^}=_{D;AF?^WNa%d+_?0g?k67_WTb6Mw<&;$SvqAimE
literal 0
HcmV?d00001
diff --git a/apps/x/packages/core/src/knowledge/chrome-extension/extension/icons/icon32.png b/apps/x/packages/core/src/knowledge/chrome-extension/extension/icons/icon32.png
new file mode 100644
index 0000000000000000000000000000000000000000..2b95aa629f311be7df1f1acd954560532de27cc3
GIT binary patch
literal 1196
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}6{!)PX}-P;
zT0k}j0}G=R11m@g5Zf_I!`W_(8Vt-}aV7?a_DlvAs2V>Y4Pq_>5pbG$0V7m3P;3Dc
zT(!aiW&|6gulqV{43Oe1@Q5sCU~qT_!i-{Pon8QqO34g~D2ed(u}aR*)k{ptPfFFR
z$SnZrVz8;O0y1+`OA-|-a&z*EttxDlz$&bOY>=?Nk^)#sNw%$0gl~X?bAC~(f|;Iy
zo`I4bmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJ
zp<7&;SCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~%2@ZUNk)yke-Y
zfd133NG#Ad)HBeBn+()w1N5Vnb5UwyNq$jCetwP}$Z45**~Q6;1*sqh+UTRJfLdvz
z4{`^RyC9whivoRZ#|6|03qd=sO(&BS85o#iJzX3_A~=su-@jflP~`afuf~f6m6W6c
zCtT=u^C){9zIwrh3&NcBtcxb(EO%HdRNo|!UoNZ|*u~WOgSn;P+q}+?_l&JK8@HLO
z@7($L+?~&V&+L3YuaL8HyT#s`#k_HM)UMcP-SzQZUw7$S*2C)mD>`pIX=i$*bEcf-
z$6AF0`d33`97PPzYKMHSvMt;hbL9PZ;rZv?ZRGfGzAal4q9w@C^KZ8;?}4<<69Y6{
zk~T_kvNa!hTPDnsxc2(%!XqE&=`d`K@;&s>;AGln6{%jK-FMHuD*W}c=A2|Q`;8uv
zyRS>EPNWza$na&l9F5eP`slNM?tgV5PKjQ(Mf0^U0(-#D-Jn*t)lFqNfrUSn|Gp`8I$}o{Kw>Rld
zG>`zgZf9@j&IP~a*MzO!!jZ8xD#L8{hPd?$qt-Iry`EqoA)#Kf-`Q?{`^yq7H~Cv{
zw@t7xXZrDMq6bUYQKbkSv3n=gCUa(OouzQ(X;EU$-}IR~Ef-wOsAzpZ_v-7?xQ>q%
zJW4f%jPG1q>ZE(!W}Hp?-{P_Sa#Ea&9K)ffMV5RAQj8ea%y9Z}z4{ZQLHM@YZ+Rcp
z*fIY#>kvGi{)Bl!f8G9i<`-qVla9`3Zb%h-&2lSiD^vU7k6RfU9T*JW9DgkM=;IEN
zkI!8gx<#XQ#!S=FlRuDScDQbT`l)ZtKUMTOxm%fnw{2{yvt)QztaF>6%lNa%|LA|c
zm;NvG)jaGhc<}MWH3p|<^xrze78HX2tUcLC^%Zh*EM*4qSvP<3_0cBrL
LS3j3^P61XIv>ME5e$1RX^Uk^Ne(#>|F5f%vysXXs-g@X|XaE58
zd}yA5kSAyjr339@@V_!32jc~Ly94sp7hE`fGuDI6Wnvxi1UvypMPsp83O7E16zJ(Sn+}~^a7lbVhXjHGfdDUX
zz_YoDpuMxRGe{tUMB-YAv6irD@Kp}$z+T#h}TxBSc0%;_9aG4A~n;Xn#@1SZ&OHNJU#W6Tc
z?SK^U|BTs|6i_?If9CowdL{~~rlO(DA2&!vU%Ve{0RTv2A5XX7qtnJona8udu?c1US=h4Udo1Kl
z>ND)Vl-+kmisf%#kG$-c1^H~t;-w1Ym)s2V7G-I&
z`rE*71em$xu%5hD(f2N+smXnfwYBKtMWPdlB(AM>I4Sx@hUr9vgv^_qoLq!HBr7X}
z`}+A=fS|r23gim}S94ski#G4STvzAD<2eL}g?YHSi7F0lpi=t>2i+aOsNx1ncgZIq
zMWs?@c6N3Og~CkaLS|D>kJ(aVW7C11sncz3ZFfZ?*q_gyHPzRzH)%OXoSK@Vy*r49
zdH2Ffnvu&j8jVb$prl=*-wp@}5M_=&xlri3WsAW=J-u7~{n+;Q(cN|hSL*Anf?707
z1{tr;a?{e}5_qWL*w|QOXNGF$^>y&Ny1ItTmlKRq0;i{@_7)pE?SfNF^}W5k#^1lU
zKg%O8kxHfas)`PZ9_9ipYQv>V@$>x|rb9zR#Qul5t{xtUk`pITmWxFP4j>@q`>U(%WJU-Z
zTiZfPRc>LSk#G;AZ3`YlJdFrxi6xWCPL7W0>(;G1w=KWbi8gF{qO|lb$7#7O+tEGU
zjifS(#cl5(UZ4e^)QqXsH>Fb3$jC_LlP95;DODeamSvCZ`CBfROK!`L_VxC9MGahf
zW~5dklM+L!S&wkO!-^|(e}Bp9Tc%~e85+1chM$)|lC*uhuSD6^Wt6`H3%7Zcqk9kd
z*{0bmJUY4vXK!zB0Y6(QI0hKLuuhtYjgF39$9{8-0R)XTH8L{VI-*p!GQ-u+
zFQ?t`szTw(wGFzEra_Ij+K|K(pFdZksr!(^Q;CU*1r-KkF9P@GBvp)0Mm}pAwe8FM
zHNCRZ3S({_dSq2;#7BGEW~*&QMV&o8+qzIS8{9bOvtR0*0+0vXP8nnxghxd=?u}nG
zSzrz?FE6(r=Re1e;9eqc
z%(OQK6%DVrFL5r>-4||!tk}CbLieln1O3J8)9RXzW2CQn(i`+1U@OzSqd@!d`fT*~
Jl)J}>{{cg&ldu2)
literal 0
HcmV?d00001
diff --git a/apps/x/packages/core/src/knowledge/chrome-extension/extension/manifest.json b/apps/x/packages/core/src/knowledge/chrome-extension/extension/manifest.json
new file mode 100644
index 00000000..f536adec
--- /dev/null
+++ b/apps/x/packages/core/src/knowledge/chrome-extension/extension/manifest.json
@@ -0,0 +1,40 @@
+{
+ "manifest_version": 3,
+ "name": "Rowboat Browser Capture",
+ "version": "1.1.1",
+ "description": "Allows users to save and capture web page content to their Rowboat workspace.",
+ "icons": {
+ "16": "icons/icon16.png",
+ "32": "icons/icon32.png",
+ "48": "icons/icon48.png",
+ "128": "icons/icon128.png"
+ },
+ "permissions": [
+ "tabs",
+ "scripting",
+ "activeTab"
+ ],
+ "host_permissions": [
+ "http://*/*",
+ "https://*/*"
+ ],
+ "action": {
+ "default_popup": "popup.html",
+ "default_icon": {
+ "16": "icons/icon16.png",
+ "32": "icons/icon32.png",
+ "48": "icons/icon48.png",
+ "128": "icons/icon128.png"
+ }
+ },
+ "background": {
+ "service_worker": "background.js"
+ },
+ "content_scripts": [
+ {
+ "matches": ["http://*/*", "https://*/*"],
+ "js": ["content.js"],
+ "run_at": "document_idle"
+ }
+ ]
+}
diff --git a/apps/x/packages/core/src/knowledge/chrome-extension/extension/popup.html b/apps/x/packages/core/src/knowledge/chrome-extension/extension/popup.html
new file mode 100644
index 00000000..b8d55d71
--- /dev/null
+++ b/apps/x/packages/core/src/knowledge/chrome-extension/extension/popup.html
@@ -0,0 +1,174 @@
+
+
+
+
+
+ Rowboat
+
+
+
+
+
+
+
+ Cannot reach Rowboat app.
+
+
+
+
Index this site?
+
+
+
+
+
+
+
+
+ Capturing this site
+
+
+
+
+
+
+ -
+
+
+
+
+
diff --git a/apps/x/packages/core/src/knowledge/chrome-extension/extension/popup.js b/apps/x/packages/core/src/knowledge/chrome-extension/extension/popup.js
new file mode 100644
index 00000000..6a3fc0b1
--- /dev/null
+++ b/apps/x/packages/core/src/knowledge/chrome-extension/extension/popup.js
@@ -0,0 +1,258 @@
+const SERVER_URL = 'http://localhost:3001';
+
+
+let currentDomain = null;
+let currentStatus = null;
+let currentConfig = null;
+
+async function getCurrentTab() {
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
+ return tab;
+}
+
+function extractDomain(url) {
+ try {
+ const parsed = new URL(url);
+ return parsed.hostname;
+ } catch {
+ return null;
+ }
+}
+
+function updateStatusBadge(status, serverReachable) {
+ const badge = document.getElementById('statusBadge');
+ const statusText = document.getElementById('statusText');
+
+ badge.classList.remove('capturing', 'not-capturing', 'awaiting', 'error');
+
+ if (!serverReachable) {
+ badge.classList.add('error');
+ statusText.textContent = 'Error';
+ return;
+ }
+
+ switch (status) {
+ case 'whitelisted':
+ case 'capturing':
+ badge.classList.add('capturing');
+ statusText.textContent = 'Indexing';
+ break;
+ case 'blacklisted':
+ badge.classList.add('not-capturing');
+ statusText.textContent = 'Not indexing';
+ break;
+ case 'unknown':
+ badge.classList.add('awaiting');
+ statusText.textContent = 'Awaiting';
+ break;
+ default:
+ badge.classList.add('not-capturing');
+ statusText.textContent = 'Unknown';
+ }
+}
+
+function showApprovalSection(show) {
+ document.getElementById('approvalSection').classList.toggle('hidden', !show);
+}
+
+function showToggleSection(show, isCapturing) {
+ const section = document.getElementById('toggleSection');
+ const label = document.getElementById('toggleLabel');
+ const btn = document.getElementById('toggleBtn');
+
+ section.classList.toggle('hidden', !show);
+
+ if (isCapturing) {
+ label.textContent = 'Capturing this site';
+ btn.textContent = 'Stop';
+ btn.onclick = () => removeDomain('whitelist');
+ } else {
+ label.textContent = 'Not capturing this site';
+ btn.textContent = 'Start';
+ btn.onclick = () => removeDomain('blacklist');
+ }
+}
+
+function showError(show) {
+ document.getElementById('errorMessage').classList.toggle('hidden', !show);
+}
+
+// Settings section
+function getSelectedMode(config) {
+ return config.mode === 'all' ? 'work' : 'ask';
+}
+
+function initSettings(config) {
+ currentConfig = config;
+ const mode = getSelectedMode(config);
+
+ const radio = document.querySelector(`input[name="captureMode"][value="${mode}"]`);
+ if (radio) radio.checked = true;
+}
+
+async function saveSettingsFromUI() {
+ const selectedRadio = document.querySelector('input[name="captureMode"]:checked');
+ const mode = selectedRadio ? selectedRadio.value : 'ask';
+
+ let config;
+ if (mode === 'work') {
+ config = {
+ mode: 'all',
+ whitelist: currentConfig ? currentConfig.whitelist : [],
+ blacklist: currentConfig ? currentConfig.blacklist : [],
+ enabled: true
+ };
+ } else {
+ config = {
+ mode: 'ask',
+ whitelist: currentConfig ? currentConfig.whitelist : [],
+ blacklist: currentConfig ? currentConfig.blacklist : [],
+ enabled: true
+ };
+ }
+
+ try {
+ await chrome.runtime.sendMessage({ type: 'SAVE_CONFIG', config });
+ currentConfig = config;
+ await loadStatus();
+ } catch (error) {
+ console.error('Failed to save settings:', error);
+ }
+}
+
+// Domain status
+async function loadStatus() {
+ const tab = await getCurrentTab();
+ if (!tab || !tab.url) {
+ document.getElementById('domainDisplay').textContent = 'No page';
+ return;
+ }
+
+ currentDomain = extractDomain(tab.url);
+ if (!currentDomain) {
+ document.getElementById('domainDisplay').textContent = 'Invalid URL';
+ return;
+ }
+
+ document.getElementById('domainDisplay').textContent = currentDomain;
+
+ try {
+ const response = await chrome.runtime.sendMessage({
+ type: 'GET_DOMAIN_STATUS',
+ url: tab.url
+ });
+
+ currentStatus = response.status;
+ const serverReachable = response.serverReachable;
+
+ updateStatusBadge(currentStatus, serverReachable);
+ showError(!serverReachable);
+
+ if (!serverReachable) {
+ showApprovalSection(false);
+ showToggleSection(false, false);
+ return;
+ }
+
+ if (currentStatus === 'unknown') {
+ showApprovalSection(true);
+ showToggleSection(false, false);
+ } else if (currentStatus === 'whitelisted' || currentStatus === 'capturing') {
+ showApprovalSection(false);
+ showToggleSection(true, true);
+ } else if (currentStatus === 'blacklisted') {
+ showApprovalSection(false);
+ showToggleSection(true, false);
+ } else {
+ showApprovalSection(false);
+ showToggleSection(false, false);
+ }
+ } catch (error) {
+ console.error('Failed to get status:', error);
+ showError(true);
+ }
+}
+
+async function loadStats() {
+ try {
+ const response = await fetch(`${SERVER_URL}/status`);
+ if (response.ok) {
+ const data = await response.json();
+ document.getElementById('statsCount').textContent = `${data.count} pages indexed locally`;
+ }
+ } catch (error) {
+ console.log('Failed to load stats:', error);
+ }
+}
+
+async function approveDomain() {
+ if (!currentDomain) return;
+ try {
+ await chrome.runtime.sendMessage({ type: 'APPROVE_DOMAIN', domain: currentDomain });
+ // Reload config to reflect the new whitelist in settings
+ const resp = await chrome.runtime.sendMessage({ type: 'GET_CONFIG' });
+ if (resp && resp.config) initSettings(resp.config);
+ await loadStatus();
+ } catch (error) {
+ console.error('Failed to approve domain:', error);
+ }
+}
+
+async function rejectDomain() {
+ if (!currentDomain) return;
+ try {
+ await chrome.runtime.sendMessage({ type: 'REJECT_DOMAIN', domain: currentDomain });
+ await loadStatus();
+ } catch (error) {
+ console.error('Failed to reject domain:', error);
+ }
+}
+
+async function captureOnce() {
+ try {
+ const response = await chrome.runtime.sendMessage({ type: 'CAPTURE_ONCE' });
+ if (response.success) {
+ window.close();
+ }
+ } catch (error) {
+ console.error('Failed to capture:', error);
+ }
+}
+
+async function removeDomain(list) {
+ if (!currentDomain) return;
+ try {
+ const messageType = list === 'whitelist' ? 'REMOVE_FROM_WHITELIST' : 'REMOVE_FROM_BLACKLIST';
+ await chrome.runtime.sendMessage({ type: messageType, domain: currentDomain });
+ // Reload config to reflect changes in settings
+ const resp = await chrome.runtime.sendMessage({ type: 'GET_CONFIG' });
+ if (resp && resp.config) initSettings(resp.config);
+ await loadStatus();
+ } catch (error) {
+ console.error('Failed to remove domain:', error);
+ }
+}
+
+document.addEventListener('DOMContentLoaded', async () => {
+ // Load config and init settings
+ try {
+ const resp = await chrome.runtime.sendMessage({ type: 'GET_CONFIG' });
+ if (resp && resp.config) {
+ initSettings(resp.config);
+ }
+ } catch (error) {
+ console.error('Failed to load config:', error);
+ }
+
+ // Radio change listeners
+ document.querySelectorAll('input[name="captureMode"]').forEach(radio => {
+ radio.addEventListener('change', () => saveSettingsFromUI());
+ });
+
+ loadStatus();
+ loadStats();
+
+ document.getElementById('approveBtn').addEventListener('click', approveDomain);
+ document.getElementById('rejectBtn').addEventListener('click', rejectDomain);
+ document.getElementById('captureOnceBtn').addEventListener('click', captureOnce);
+});
diff --git a/apps/x/packages/core/src/knowledge/chrome-extension/extension/styles.css b/apps/x/packages/core/src/knowledge/chrome-extension/extension/styles.css
new file mode 100644
index 00000000..399c473b
--- /dev/null
+++ b/apps/x/packages/core/src/knowledge/chrome-extension/extension/styles.css
@@ -0,0 +1,279 @@
+:root {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f9fafb;
+ --bg-tertiary: #f3f4f6;
+ --text-primary: #111827;
+ --text-secondary: #6b7280;
+ --text-muted: #9ca3af;
+ --border-color: #e5e7eb;
+ --accent-color: #3b82f6;
+ --accent-hover: #2563eb;
+ --success-color: #10b981;
+ --warning-color: #f59e0b;
+ --error-color: #ef4444;
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ --shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg-primary: #1f2937;
+ --bg-secondary: #111827;
+ --bg-tertiary: #374151;
+ --text-primary: #f9fafb;
+ --text-secondary: #d1d5db;
+ --text-muted: #9ca3af;
+ --border-color: #374151;
+ --accent-color: #60a5fa;
+ --accent-hover: #3b82f6;
+ --success-color: #34d399;
+ --warning-color: #fbbf24;
+ --error-color: #f87171;
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+ --shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.3);
+ }
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--text-primary);
+ background-color: var(--bg-primary);
+}
+
+/* Buttons */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px 16px;
+ font-size: 14px;
+ font-weight: 500;
+ border-radius: 6px;
+ border: none;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.btn-primary {
+ background-color: var(--accent-color);
+ color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background-color: var(--accent-hover);
+}
+
+.btn-secondary {
+ background-color: var(--bg-tertiary);
+ color: var(--text-primary);
+ border: 1px solid var(--border-color);
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background-color: var(--border-color);
+}
+
+.btn-ghost {
+ background-color: transparent;
+ color: var(--text-secondary);
+}
+
+.btn-ghost:hover:not(:disabled) {
+ background-color: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+.btn-sm {
+ padding: 4px 8px;
+ font-size: 12px;
+}
+
+.btn-block {
+ width: 100%;
+}
+
+/* Status badges */
+.status-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+ border-radius: 9999px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.status-badge.capturing {
+ background-color: rgba(16, 185, 129, 0.1);
+ color: var(--success-color);
+}
+
+.status-badge.not-capturing {
+ background-color: rgba(107, 114, 128, 0.1);
+ color: var(--text-secondary);
+}
+
+.status-badge.awaiting {
+ background-color: rgba(245, 158, 11, 0.1);
+ color: var(--warning-color);
+}
+
+.status-badge.error {
+ background-color: rgba(239, 68, 68, 0.1);
+ color: var(--error-color);
+}
+
+.status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: currentColor;
+}
+
+/* Cards */
+.card {
+ background-color: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 16px;
+}
+
+/* Form elements */
+.radio-group {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.radio-option {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.radio-option:hover {
+ border-color: var(--accent-color);
+ background-color: var(--bg-secondary);
+}
+
+.radio-option.selected {
+ border-color: var(--accent-color);
+ background-color: rgba(59, 130, 246, 0.05);
+}
+
+.radio-option input[type="radio"] {
+ margin-top: 2px;
+ accent-color: var(--accent-color);
+}
+
+.radio-option-content {
+ flex: 1;
+}
+
+.radio-option-title {
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+.radio-option-desc {
+ font-size: 13px;
+ color: var(--text-secondary);
+ margin-top: 2px;
+}
+
+/* Toggle/Checkbox */
+.toggle-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: 12px;
+ padding-left: 24px;
+}
+
+.toggle-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.toggle-item input[type="checkbox"] {
+ accent-color: var(--accent-color);
+}
+
+.toggle-item label {
+ font-size: 13px;
+ color: var(--text-secondary);
+ cursor: pointer;
+}
+
+/* Divider */
+.divider {
+ height: 1px;
+ background-color: var(--border-color);
+ margin: 12px 0;
+}
+
+/* Link */
+.link {
+ color: var(--accent-color);
+ text-decoration: none;
+ font-size: 13px;
+}
+
+.link:hover {
+ text-decoration: underline;
+}
+
+/* Text utilities */
+.text-sm {
+ font-size: 12px;
+}
+
+.text-muted {
+ color: var(--text-muted);
+}
+
+.text-secondary {
+ color: var(--text-secondary);
+}
+
+.text-center {
+ text-align: center;
+}
+
+/* Spacing utilities */
+.mt-1 { margin-top: 4px; }
+.mt-2 { margin-top: 8px; }
+.mt-3 { margin-top: 12px; }
+.mt-4 { margin-top: 16px; }
+.mb-1 { margin-bottom: 4px; }
+.mb-2 { margin-bottom: 8px; }
+.mb-3 { margin-bottom: 12px; }
+.mb-4 { margin-bottom: 16px; }
+
+/* Flex utilities */
+.flex { display: flex; }
+.flex-col { flex-direction: column; }
+.items-center { align-items: center; }
+.justify-between { justify-content: space-between; }
+.gap-1 { gap: 4px; }
+.gap-2 { gap: 8px; }
+.gap-3 { gap: 12px; }
diff --git a/apps/x/packages/core/src/knowledge/chrome-extension/server/server.ts b/apps/x/packages/core/src/knowledge/chrome-extension/server/server.ts
new file mode 100644
index 00000000..0cb127b9
--- /dev/null
+++ b/apps/x/packages/core/src/knowledge/chrome-extension/server/server.ts
@@ -0,0 +1,281 @@
+import express from 'express';
+import cors from 'cors';
+import crypto from 'crypto';
+import fs from 'fs';
+import path from 'path';
+import { WorkDir } from '../../../config/config.js';
+
+const app = express();
+app.use(cors());
+app.use(express.json({ limit: '10mb' }));
+
+const CAPTURED_PAGES_DIR = path.join(WorkDir, 'chrome_sync');
+const CONFIG_DIR = path.join(WorkDir, 'config');
+const CONFIG_FILE = path.join(CONFIG_DIR, 'chrome-plugin.json');
+
+interface Config {
+ mode: 'all' | 'ask';
+ whitelist: string[];
+ blacklist: string[];
+ enabled: boolean;
+}
+
+const DEFAULT_CONFIG: Config = {
+ mode: 'ask',
+ whitelist: [],
+ blacklist: [],
+ enabled: true
+};
+
+const contentHashes = new Map();
+
+function extractDomain(url: string): string {
+ try {
+ const parsed = new URL(url);
+ return parsed.host || 'unknown';
+ } catch {
+ return 'unknown';
+ }
+}
+
+function pathToSlug(url: string): string {
+ try {
+ const parsed = new URL(url);
+ const p = parsed.pathname + (parsed.search || '');
+ if (!p || p === '/') return 'index';
+ let slug = p.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_|_$/g, '');
+ return slug.substring(0, 80) || 'index';
+ } catch {
+ return 'index';
+ }
+}
+
+function hashContent(content: string): string {
+ return crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
+}
+
+function findExistingFile(domainDir: string, pathSlug: string): string | null {
+ if (!fs.existsSync(domainDir)) return null;
+ const files = fs.readdirSync(domainDir);
+ for (const filename of files) {
+ if (filename.endsWith(`_${pathSlug}.md`)) {
+ return path.join(domainDir, filename);
+ }
+ }
+ return null;
+}
+
+// POST /capture
+app.post('/capture', (req, res) => {
+ const data = req.body;
+ if (!data) {
+ return res.status(400).json({ error: 'No JSON data provided' });
+ }
+
+ const { url, content = '', timestamp, title = 'Untitled' } = data;
+
+ if (!url || !timestamp) {
+ return res.status(400).json({ error: 'Missing required fields: url, timestamp' });
+ }
+
+ const domain = extractDomain(url);
+ const pathSlug = pathToSlug(url);
+ const contentHash = hashContent(content);
+ const cacheKey = `${domain}/${pathSlug}`;
+
+ const dt = new Date(timestamp);
+ const year = dt.getFullYear();
+ const month = String(dt.getMonth() + 1).padStart(2, '0');
+ const day = String(dt.getDate()).padStart(2, '0');
+ const dateStr = `${year}-${month}-${day}`;
+ const hours = String(dt.getHours()).padStart(2, '0');
+ const minutes = String(dt.getMinutes()).padStart(2, '0');
+ const seconds = String(dt.getSeconds()).padStart(2, '0');
+ const timeStr = `${hours}-${minutes}`;
+ const timeDisplay = `${hours}:${minutes}:${seconds}`;
+ const tzOffset = -dt.getTimezoneOffset();
+ const tzSign = tzOffset >= 0 ? '+' : '-';
+ const tzHours = String(Math.floor(Math.abs(tzOffset) / 60)).padStart(2, '0');
+ const tzMins = String(Math.abs(tzOffset) % 60).padStart(2, '0');
+ const isoTimestamp = `${dateStr}T${hours}:${minutes}:${seconds}${tzSign}${tzHours}:${tzMins}`;
+
+ // date/domain directory structure
+ const domainDir = path.join(CAPTURED_PAGES_DIR, dateStr, domain);
+ fs.mkdirSync(domainDir, { recursive: true });
+
+ const existingFile = findExistingFile(domainDir, pathSlug);
+ if (existingFile && contentHashes.get(cacheKey) === contentHash) {
+ return res.json({ status: 'skipped', reason: 'duplicate content' });
+ }
+
+ contentHashes.set(cacheKey, contentHash);
+
+ // If file exists, append with scroll separator
+ if (existingFile) {
+ const scrollSeparator = `\n\n---\n📜 Scroll captured at ${timeDisplay}\n---\n\n`;
+ fs.appendFileSync(existingFile, scrollSeparator + content, 'utf-8');
+ const rel = `${dateStr}/${domain}/${path.basename(existingFile)}`;
+ return res.json({ status: 'appended', filename: rel });
+ }
+
+ // New file - create with frontmatter
+ const filename = `${timeStr}_${pathSlug}.md`;
+ const filepath = path.join(domainDir, filename);
+
+ const markdownContent = `---
+url: ${url}
+title: ${title}
+captured_at: ${isoTimestamp}
+---
+
+${content}
+`;
+
+ fs.writeFileSync(filepath, markdownContent, 'utf-8');
+ return res.status(201).json({ status: 'captured', filename: `${dateStr}/${domain}/${filename}` });
+});
+
+// GET /status
+app.get('/status', (_req, res) => {
+ let count = 0;
+ const domains: Record = {};
+
+ if (!fs.existsSync(CAPTURED_PAGES_DIR)) {
+ return res.json({ count: 0, domains: [] });
+ }
+
+ for (const dateEntry of fs.readdirSync(CAPTURED_PAGES_DIR)) {
+ const datePath = path.join(CAPTURED_PAGES_DIR, dateEntry);
+ if (!fs.statSync(datePath).isDirectory()) continue;
+
+ for (const domainEntry of fs.readdirSync(datePath)) {
+ const domainPath = path.join(datePath, domainEntry);
+ if (!fs.statSync(domainPath).isDirectory()) continue;
+
+ const domainCount = fs.readdirSync(domainPath).filter(f => f.endsWith('.md')).length;
+ count += domainCount;
+ if (domainCount > 0) {
+ domains[domainEntry] = (domains[domainEntry] || 0) + domainCount;
+ }
+ }
+ }
+
+ const domainList = Object.entries(domains)
+ .map(([domain, c]) => ({ domain, count: c }))
+ .sort((a, b) => b.count - a.count);
+
+ return res.json({ count, domains: domainList });
+});
+
+// Config helpers
+function loadConfig(): Config {
+ if (fs.existsSync(CONFIG_FILE)) {
+ try {
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
+ return JSON.parse(raw);
+ } catch {
+ // fall through
+ }
+ }
+ return { ...DEFAULT_CONFIG };
+}
+
+function saveConfig(config: Config): void {
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
+}
+
+function validateConfig(data: any): data is Config {
+ if (typeof data !== 'object' || data === null) return false;
+ if (data.mode !== 'all' && data.mode !== 'ask') return false;
+ if (!Array.isArray(data.whitelist)) return false;
+ if (!Array.isArray(data.blacklist)) return false;
+ if (typeof data.enabled !== 'boolean') return false;
+ return true;
+}
+
+// GET /browse/config
+app.get('/browse/config', (_req, res) => {
+ const config = loadConfig();
+ return res.json(config);
+});
+
+// POST /browse/config
+app.post('/browse/config', (req, res) => {
+ const data = req.body;
+ if (!data) {
+ return res.status(400).json({ error: 'No JSON data provided' });
+ }
+
+ if (!validateConfig(data)) {
+ return res.status(400).json({ error: 'Invalid config shape' });
+ }
+
+ saveConfig(data);
+ return res.json({ status: 'saved', config: data });
+});
+
+const PORT = 3001;
+const RETENTION_DAYS = 7;
+const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
+
+function cleanUpOldFiles(): void {
+ if (!fs.existsSync(CAPTURED_PAGES_DIR)) return;
+
+ const cutoff = new Date();
+ cutoff.setDate(cutoff.getDate() - RETENTION_DAYS);
+ const cutoffStr = cutoff.toISOString().slice(0, 10); // YYYY-MM-DD
+
+ for (const dateEntry of fs.readdirSync(CAPTURED_PAGES_DIR)) {
+ // only process date-formatted directories
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(dateEntry)) continue;
+ if (dateEntry >= cutoffStr) continue;
+
+ const datePath = path.join(CAPTURED_PAGES_DIR, dateEntry);
+ if (!fs.statSync(datePath).isDirectory()) continue;
+
+ fs.rmSync(datePath, { recursive: true, force: true });
+ console.log(`[ChromeSync] Cleaned up old captures: ${dateEntry}`);
+ }
+}
+
+function isServerEnabled(): boolean {
+ if (!fs.existsSync(CONFIG_FILE)) return false;
+ try {
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
+ const config = JSON.parse(raw);
+ return config.serverEnabled === true;
+ } catch {
+ return false;
+ }
+}
+
+function startServer(): void {
+ fs.mkdirSync(CAPTURED_PAGES_DIR, { recursive: true });
+
+ cleanUpOldFiles();
+ setInterval(cleanUpOldFiles, CLEANUP_INTERVAL_MS);
+
+ app.listen(PORT, 'localhost', () => {
+ console.log('[ChromeSync] Server starting.');
+ console.log(` Captured pages: ${CAPTURED_PAGES_DIR}`);
+ console.log(` Config: ${CONFIG_FILE}`);
+ console.log(` Listening on http://localhost:${PORT}`);
+ });
+}
+
+export async function init(): Promise {
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
+
+ if (isServerEnabled()) {
+ startServer();
+ return;
+ }
+
+ console.log('[ChromeSync] Server disabled, watching config for changes...');
+ fs.watch(CONFIG_DIR, (_, filename) => {
+ if (filename === 'chrome-plugin.json' && isServerEnabled()) {
+ console.log('[ChromeSync] serverEnabled set to true, starting server...');
+ startServer();
+ }
+ });
+}
diff --git a/apps/x/pnpm-lock.yaml b/apps/x/pnpm-lock.yaml
index c4e701a5..e59ce990 100644
--- a/apps/x/pnpm-lock.yaml
+++ b/apps/x/pnpm-lock.yaml
@@ -53,9 +53,6 @@ importers:
electron-squirrel-startup:
specifier: ^1.0.1
version: 1.0.1
- html-to-docx:
- specifier: ^1.8.0
- version: 1.8.0(encoding@0.1.13)
mammoth:
specifier: ^1.11.0
version: 1.11.0
@@ -238,9 +235,6 @@ importers:
react-dom:
specifier: ^19.2.0
version: 19.2.3(react@19.2.3)
- recharts:
- specifier: ^3.8.0
- version: 3.8.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1)
sonner:
specifier: ^2.0.7
version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -310,16 +304,16 @@ importers:
dependencies:
'@ai-sdk/anthropic':
specifier: ^2.0.63
- version: 2.0.63(zod@4.2.1)
+ version: 2.0.70(zod@4.2.1)
'@ai-sdk/google':
specifier: ^2.0.53
- version: 2.0.53(zod@4.2.1)
+ version: 2.0.61(zod@4.2.1)
'@ai-sdk/openai':
specifier: ^2.0.91
- version: 2.0.91(zod@4.2.1)
+ version: 2.0.99(zod@4.2.1)
'@ai-sdk/openai-compatible':
specifier: ^1.0.33
- version: 1.0.33(zod@4.2.1)
+ version: 1.0.34(zod@4.2.1)
'@ai-sdk/provider':
specifier: ^2.0.1
version: 2.0.1
@@ -334,7 +328,7 @@ importers:
version: 1.25.1(hono@4.11.3)(zod@4.2.1)
'@openrouter/ai-sdk-provider':
specifier: ^1.2.6
- version: 1.5.4(ai@5.0.133(zod@4.2.1))(zod@4.2.1)
+ version: 1.5.4(ai@5.0.151(zod@4.2.1))(zod@4.2.1)
'@react-pdf/renderer':
specifier: ^4.3.2
version: 4.3.2(react@19.2.3)
@@ -346,16 +340,22 @@ importers:
version: link:../shared
ai:
specifier: ^5.0.133
- version: 5.0.133(zod@4.2.1)
+ version: 5.0.151(zod@4.2.1)
awilix:
specifier: ^12.0.5
version: 12.0.5
chokidar:
specifier: ^4.0.3
version: 4.0.3
+ cors:
+ specifier: ^2.8.6
+ version: 2.8.6
cron-parser:
specifier: ^5.5.0
version: 5.5.0
+ express:
+ specifier: ^5.2.1
+ version: 5.2.1
glob:
specifier: ^13.0.0
version: 13.0.0
@@ -399,6 +399,12 @@ importers:
specifier: ^4.2.1
version: 4.2.1
devDependencies:
+ '@types/cors':
+ specifier: ^2.8.19
+ version: 2.8.19
+ '@types/express':
+ specifier: ^5.0.6
+ version: 5.0.6
'@types/node':
specifier: ^25.0.3
version: 25.0.3
@@ -417,8 +423,8 @@ importers:
packages:
- '@ai-sdk/anthropic@2.0.63':
- resolution: {integrity: sha512-zXlUPCkumnvp8lWS9VFcen/MLF6CL/t1zAKDhpobYj9y/nmylQrKtRvn3RwH871Wd3dF3KYEUXd6M2c6dfCKOA==}
+ '@ai-sdk/anthropic@2.0.70':
+ resolution: {integrity: sha512-W3WjQlb0Ho+CVAQUvb8Rtk3hGS3Jlgy79ihY2H0yj2k4yU8XuxpQw0Oz+7JQsB47j+jlHhk7nUXtxhAeRg3S3Q==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
@@ -429,26 +435,26 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
- '@ai-sdk/gateway@2.0.39':
- resolution: {integrity: sha512-ULnefGmRHG0/tRrf+dtDwgQYAttGi/TR0FmASAzTs1dtpeZp4Xoh1VyWrX3Z1bM3WDs9RM3ZeSE77kQT/jbfjw==}
+ '@ai-sdk/gateway@2.0.56':
+ resolution: {integrity: sha512-omvb2Bwpgqg8PKqOpYdIaW+fdEIWcfm2B/j3dx37DxzOIt6fr57VVcfw7pu/EaACcY0O+wsg50iFCPGcsI2Cbg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
- '@ai-sdk/google@2.0.53':
- resolution: {integrity: sha512-ccCxr5mrd3AC2CjLq4e1ST7+UiN5T2Pdmgi0XdWM3QohmNBwUQ/RBG7BvL+cB/ex/j6y64tkMmpYz9zBw/SEFQ==}
+ '@ai-sdk/google@2.0.61':
+ resolution: {integrity: sha512-hIs7UvL8X5MBG3uxdciSotD4I27UcMa4/we9Qf98fM/RgMTwyk9zXcr7GM6k5yLBZ5S0QeZWkfqKwtdiDnUEEQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
- '@ai-sdk/openai-compatible@1.0.33':
- resolution: {integrity: sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ==}
+ '@ai-sdk/openai-compatible@1.0.34':
+ resolution: {integrity: sha512-AnGoxVNZ/E3EU4lW12rrufI6riqL2cEv4jk3OrjJ/i54XwR0CJU1V26jXAwxb+Pc+uZmYG++HM+gzXxPQZkMNQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
- '@ai-sdk/openai@2.0.91':
- resolution: {integrity: sha512-lozfRHfSTHg5/UliQjTDcOtISYGbEpt4FS/6QM5PcLmhdT0HmROllaBmG7+JaK+uqFtDXZGgMIpz3bqB9nzqCQ==}
+ '@ai-sdk/openai@2.0.99':
+ resolution: {integrity: sha512-wwa1/DuO9XThaA+sAi0d3+xfkbEx9nRhZ1USV6kktndmEs8aQRR0DJK/Iec+mwNu06IhfDGd5vMscR1U1q155g==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
@@ -459,8 +465,8 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
- '@ai-sdk/provider-utils@3.0.21':
- resolution: {integrity: sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q==}
+ '@ai-sdk/provider-utils@3.0.22':
+ resolution: {integrity: sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
@@ -1572,46 +1578,6 @@ packages:
'@octokit/types@6.41.0':
resolution: {integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==}
- '@oozcitak/dom@1.15.5':
- resolution: {integrity: sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A==}
- engines: {node: '>=8.0'}
-
- '@oozcitak/dom@1.15.6':
- resolution: {integrity: sha512-k4uEIa6DI3FCrFJMGq/05U/59WnS9DjME0kaPqBRCJAqBTkmopbYV1Xs4qFKbDJ/9wOg8W97p+1E0heng/LH7g==}
- engines: {node: '>=8.0'}
-
- '@oozcitak/infra@1.0.3':
- resolution: {integrity: sha512-9O2wxXGnRzy76O1XUxESxDGsXT5kzETJPvYbreO4mv6bqe1+YSuux2cZTagjJ/T4UfEwFJz5ixanOqB0QgYAag==}
- engines: {node: '>=6.0'}
-
- '@oozcitak/infra@1.0.5':
- resolution: {integrity: sha512-o+zZH7M6l5e3FaAWy3ojaPIVN5eusaYPrKm6MZQt0DKNdgXa2wDYExjpP0t/zx+GoQgQKzLu7cfD8rHCLt8JrQ==}
- engines: {node: '>=6.0'}
-
- '@oozcitak/url@1.0.0':
- resolution: {integrity: sha512-LGrMeSxeLzsdaitxq3ZmBRVOrlRRQIgNNci6L0VRnOKlJFuRIkNm4B+BObXPCJA6JT5bEJtrrwjn30jueHJYZQ==}
- engines: {node: '>=8.0'}
-
- '@oozcitak/util@1.0.1':
- resolution: {integrity: sha512-dFwFqcKrQnJ2SapOmRD1nQWEZUtbtIy9Y6TyJquzsalWNJsKIPxmTI0KG6Ypyl8j7v89L2wixH9fQDNrF78hKg==}
- engines: {node: '>=6.0'}
-
- '@oozcitak/util@1.0.2':
- resolution: {integrity: sha512-4n8B1cWlJleSOSba5gxsMcN4tO8KkkcvXhNWW+ADqvq9Xj+Lrl9uCa90GRpjekqQJyt84aUX015DG81LFpZYXA==}
- engines: {node: '>=6.0'}
-
- '@oozcitak/util@8.0.0':
- resolution: {integrity: sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==}
- engines: {node: '>=6.0'}
-
- '@oozcitak/util@8.3.3':
- resolution: {integrity: sha512-Ufpab7G5PfnEhQyy5kDg9C8ltWJjsVT1P/IYqacjstaqydG4Q21HAT2HUZQYBrC/a1ZLKCz87pfydlDvv8y97w==}
- engines: {node: '>=6.0'}
-
- '@oozcitak/util@8.3.4':
- resolution: {integrity: sha512-6gH/bLQJSJEg7OEpkH4wGQdA8KXHRbzL1YkGyUO12YNAgV3jxKy4K9kvfXj4+9T0OLug5k58cnPCKSSIKzp7pg==}
- engines: {node: '>=8.0'}
-
'@openrouter/ai-sdk-provider@1.5.4':
resolution: {integrity: sha512-xrSQPUIH8n9zuyYZR0XK7Ba0h2KsjJcMkxnwaYfmv13pKs3sDkjPzVPPhlhzqBGddHb5cFEwJ9VFuFeDcxCDSw==}
engines: {node: '>=18'}
@@ -2550,17 +2516,6 @@ packages:
'@react-pdf/types@2.9.2':
resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==}
- '@reduxjs/toolkit@2.11.2':
- resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==}
- peerDependencies:
- react: ^16.9.0 || ^17.0.0 || ^18 || ^19
- react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-redux:
- optional: true
-
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
@@ -2921,9 +2876,6 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
- '@standard-schema/utils@0.3.0':
- resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
-
'@swc/helpers@0.5.18':
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
@@ -3221,9 +3173,18 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+ '@types/body-parser@1.19.6':
+ resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
+
'@types/cacheable-request@6.0.3':
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
+ '@types/connect@3.4.38':
+ resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+
+ '@types/cors@2.8.19':
+ resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
+
'@types/d3-array@3.2.2':
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
@@ -3335,6 +3296,12 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+ '@types/express-serve-static-core@5.1.1':
+ resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
+
+ '@types/express@5.0.6':
+ resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==}
+
'@types/fs-extra@9.0.13':
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
@@ -3347,6 +3314,9 @@ packages:
'@types/http-cache-semantics@4.0.4':
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
+ '@types/http-errors@2.0.5':
+ resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
+
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -3398,6 +3368,12 @@ packages:
'@types/pdf-parse@1.1.5':
resolution: {integrity: sha512-kBfrSXsloMnUJOKi25s3+hRmkycHfLK6A09eRGqF/N8BkQoPUmaCr+q8Cli5FnfohEz/rsv82zAiPz/LXtOGhA==}
+ '@types/qs@6.14.0':
+ resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
+
+ '@types/range-parser@1.2.7':
+ resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
+
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
@@ -3409,6 +3385,12 @@ packages:
'@types/responselike@1.0.3':
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
+ '@types/send@1.2.1':
+ resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
+
+ '@types/serve-static@2.2.0':
+ resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
+
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -3617,8 +3599,8 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
- ai@5.0.133:
- resolution: {integrity: sha512-N6KnwSWKcXEWPnAri3anRuzRvcrvtDz1W1JG9CvMrQ0Xdp8Vu8ZToNW/eHt63CmrbmzTwVw/HaCtJuO+MYtS7A==}
+ ai@5.0.151:
+ resolution: {integrity: sha512-tsLIv+QN9wJ/xl/fnYgjRYoGSThdnOfU4d6+7QEUKX3EcwviWMEaL1gOE+zfdkcay/Tbc02ZBtTRHoulS6DYvQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
@@ -3793,9 +3775,6 @@ packages:
brotli@1.3.3:
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
- browser-split@0.0.1:
- resolution: {integrity: sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow==}
-
browserify-zlib@0.2.0:
resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==}
@@ -3857,9 +3836,6 @@ packages:
camel-case@4.1.2:
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
- camelize@1.0.1:
- resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==}
-
caniuse-lite@1.0.30001761:
resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==}
@@ -4057,8 +4033,8 @@ packages:
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
- cors@2.8.5:
- resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
+ cors@2.8.6:
+ resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'}
cose-base@1.0.3:
@@ -4287,9 +4263,6 @@ packages:
supports-color:
optional: true
- decimal.js-light@2.5.1:
- resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
-
decode-named-character-reference@1.2.0:
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
@@ -4358,24 +4331,12 @@ packages:
dir-compare@4.2.0:
resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==}
- dom-serializer@0.2.2:
- resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==}
-
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
- dom-walk@0.1.2:
- resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==}
-
- domelementtype@1.3.1:
- resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==}
-
domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
- domhandler@2.4.2:
- resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==}
-
domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
@@ -4383,9 +4344,6 @@ packages:
dompurify@3.3.1:
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
- domutils@1.7.0:
- resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==}
-
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@@ -4470,16 +4428,6 @@ packages:
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
engines: {node: '>=10.13.0'}
- ent@2.2.2:
- resolution: {integrity: sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==}
- engines: {node: '>= 0.4'}
-
- entities@1.1.2:
- resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==}
-
- entities@2.2.0:
- resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
-
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@@ -4498,9 +4446,6 @@ packages:
error-ex@1.3.4:
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
- error@4.4.0:
- resolution: {integrity: sha512-SNDKualLUtT4StGFP7xNfuFybL2f6iJujFtrWuvJqGbVQGaN+adE23veqzPz1hjUjTunLi2EnJ+0SJxtbJreKw==}
-
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
@@ -4520,9 +4465,6 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
- es-toolkit@1.45.1:
- resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==}
-
es6-error@4.1.1:
resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==}
@@ -4623,9 +4565,6 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
- ev-store@7.0.0:
- resolution: {integrity: sha512-otazchNRnGzp2YarBJ+GXKVGvhxVATB1zmaStxJBYet0Dyq7A9VhH8IUEB/gRcL6Ch52lfpgPTRJ2m49epyMsQ==}
-
event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
@@ -4945,7 +4884,6 @@ packages:
glob@10.5.0:
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
- deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@13.0.0:
@@ -4954,7 +4892,7 @@ packages:
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
- deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+ deprecated: Glob versions prior to v9 are no longer supported
glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
@@ -4969,9 +4907,6 @@ packages:
resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==}
engines: {node: '>=10'}
- global@4.4.0:
- resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
-
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
@@ -5115,24 +5050,12 @@ packages:
hsl-to-rgb-for-reals@1.1.1:
resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==}
- html-entities@2.6.0:
- resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
-
- html-to-docx@1.8.0:
- resolution: {integrity: sha512-IiMBWIqXM4+cEsW//RKoonWV7DlXAJBmmKI73XJSVWTIXjGUaxSr2ck1jqzVRZknpvO8xsFnVicldKVAWrBYBA==}
-
- html-to-vdom@0.7.0:
- resolution: {integrity: sha512-k+d2qNkbx0JO00KezQsNcn6k2I/xSBP4yXYFLvXbcasTTDh+RDLUJS3puxqyNnpdyXWRHFGoKU7cRmby8/APcQ==}
-
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
- htmlparser2@3.10.1:
- resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==}
-
http-cache-semantics@4.2.0:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
@@ -5190,23 +5113,9 @@ packages:
engines: {node: '>=6.9.0'}
hasBin: true
- image-size@1.2.1:
- resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==}
- engines: {node: '>=16.x'}
- hasBin: true
-
- image-to-base64@2.2.0:
- resolution: {integrity: sha512-Z+aMwm/91UOQqHhrz7Upre2ytKhWejZlWV/JxUTD1sT7GWWKFDJUEV5scVQKnkzSgPHFuQBUEWcanO+ma0PSVw==}
-
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
- immer@10.2.0:
- resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
-
- immer@11.1.4:
- resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==}
-
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@@ -5223,9 +5132,6 @@ packages:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
- individual@3.0.0:
- resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==}
-
infer-owner@1.0.4:
resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==}
@@ -5326,9 +5232,6 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
- is-object@1.0.2:
- resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==}
-
is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
@@ -5339,10 +5242,6 @@ packages:
is-property@1.0.2:
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
- is-regex@1.2.1:
- resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
- engines: {node: '>= 0.4'}
-
is-stream@1.1.0:
resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
engines: {node: '>=0.10.0'}
@@ -5937,9 +5836,6 @@ packages:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
- min-document@2.19.2:
- resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==}
-
minimatch@10.1.1:
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
engines: {node: 20 || >=22}
@@ -6069,9 +5965,6 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
- next-tick@0.2.2:
- resolution: {integrity: sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==}
-
nice-try@1.0.5:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
@@ -6532,9 +6425,6 @@ packages:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
- punycode@1.4.1:
- resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
-
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -6594,18 +6484,6 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
- react-redux@9.2.0:
- resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
- peerDependencies:
- '@types/react': ^18.2.25 || ^19
- react: ^18.0 || ^19
- redux: ^5.0.0
- peerDependenciesMeta:
- '@types/react':
- optional: true
- redux:
- optional: true
-
react-refresh@0.18.0:
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
engines: {node: '>=0.10.0'}
@@ -6671,26 +6549,10 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
- recharts@3.8.0:
- resolution: {integrity: sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==}
- engines: {node: '>=18'}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
-
rechoir@0.8.0:
resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==}
engines: {node: '>= 10.13.0'}
- redux-thunk@3.1.0:
- resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
- peerDependencies:
- redux: ^5.0.0
-
- redux@5.0.1:
- resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
-
regex-recursion@6.0.2:
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
@@ -6763,9 +6625,6 @@ packages:
resolution: {integrity: sha512-oTeemxwoMuxxTYxXUwjkrOPfngTQehlv0/HoYFNkB4uzsP1Un1A9nI8JQKGOFkxpqkC7qkMs0lUsGrvUlbLNUA==}
engines: {node: '>=14', npm: '>=7'}
- reselect@5.1.1:
- resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
-
resolve-alpn@1.2.1:
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
@@ -6854,10 +6713,6 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
- safe-regex-test@1.1.0:
- resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
- engines: {node: '>= 0.4'}
-
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -7053,9 +6908,6 @@ packages:
peerDependencies:
react: ^18.0.0 || ^19.0.0
- string-template@0.2.1:
- resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==}
-
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -7141,7 +6993,7 @@ packages:
tar@6.2.1:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'}
- deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+ deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
temp@0.9.4:
resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==}
@@ -7174,9 +7026,6 @@ packages:
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
- tiny-invariant@1.3.3:
- resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
-
tinyexec@1.0.2:
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
engines: {node: '>=18'}
@@ -7306,8 +7155,8 @@ packages:
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
- underscore@1.13.7:
- resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
+ underscore@1.13.8:
+ resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@@ -7448,12 +7297,6 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
- victory-vendor@37.3.6:
- resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
-
- virtual-dom@2.1.1:
- resolution: {integrity: sha512-wb6Qc9Lbqug0kRqo/iuApfBpJJAq14Sk1faAnSmtqXiwahg7PVTvWMs9L02Z8nNIMqbwsxzBAA90bbtRLbw0zg==}
-
vite-compatible-readable-stream@3.6.1:
resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==}
engines: {node: '>= 6'}
@@ -7603,21 +7446,11 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
- x-is-array@0.1.0:
- resolution: {integrity: sha512-goHPif61oNrr0jJgsXRfc8oqtYzvfiMJpTqwE7Z4y9uH+T3UozkGqQ4d2nX9mB9khvA8U2o/UbPOFjgC7hLWIA==}
-
- x-is-string@0.1.0:
- resolution: {integrity: sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w==}
-
xlsx@0.18.5:
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
engines: {node: '>=0.8'}
hasBin: true
- xmlbuilder2@2.1.2:
- resolution: {integrity: sha512-PI710tmtVlQ5VmwzbRTuhmVhKnj9pM8Si+iOZCV2g2SNo3gCrpzR2Ka9wNzZtqfD+mnP+xkrqoNy0sjKZqP4Dg==}
- engines: {node: '>=8.0'}
-
xmlbuilder@10.1.1:
resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==}
engines: {node: '>=4.0'}
@@ -7694,10 +7527,10 @@ packages:
snapshots:
- '@ai-sdk/anthropic@2.0.63(zod@4.2.1)':
+ '@ai-sdk/anthropic@2.0.70(zod@4.2.1)':
dependencies:
'@ai-sdk/provider': 2.0.1
- '@ai-sdk/provider-utils': 3.0.21(zod@4.2.1)
+ '@ai-sdk/provider-utils': 3.0.22(zod@4.2.1)
zod: 4.2.1
'@ai-sdk/gateway@2.0.24(zod@4.2.1)':
@@ -7707,29 +7540,29 @@ snapshots:
'@vercel/oidc': 3.0.5
zod: 4.2.1
- '@ai-sdk/gateway@2.0.39(zod@4.2.1)':
+ '@ai-sdk/gateway@2.0.56(zod@4.2.1)':
dependencies:
'@ai-sdk/provider': 2.0.1
- '@ai-sdk/provider-utils': 3.0.21(zod@4.2.1)
+ '@ai-sdk/provider-utils': 3.0.22(zod@4.2.1)
'@vercel/oidc': 3.1.0
zod: 4.2.1
- '@ai-sdk/google@2.0.53(zod@4.2.1)':
+ '@ai-sdk/google@2.0.61(zod@4.2.1)':
dependencies:
'@ai-sdk/provider': 2.0.1
- '@ai-sdk/provider-utils': 3.0.21(zod@4.2.1)
+ '@ai-sdk/provider-utils': 3.0.22(zod@4.2.1)
zod: 4.2.1
- '@ai-sdk/openai-compatible@1.0.33(zod@4.2.1)':
+ '@ai-sdk/openai-compatible@1.0.34(zod@4.2.1)':
dependencies:
'@ai-sdk/provider': 2.0.1
- '@ai-sdk/provider-utils': 3.0.21(zod@4.2.1)
+ '@ai-sdk/provider-utils': 3.0.22(zod@4.2.1)
zod: 4.2.1
- '@ai-sdk/openai@2.0.91(zod@4.2.1)':
+ '@ai-sdk/openai@2.0.99(zod@4.2.1)':
dependencies:
'@ai-sdk/provider': 2.0.1
- '@ai-sdk/provider-utils': 3.0.21(zod@4.2.1)
+ '@ai-sdk/provider-utils': 3.0.22(zod@4.2.1)
zod: 4.2.1
'@ai-sdk/provider-utils@3.0.20(zod@4.2.1)':
@@ -7739,7 +7572,7 @@ snapshots:
eventsource-parser: 3.0.6
zod: 4.2.1
- '@ai-sdk/provider-utils@3.0.21(zod@4.2.1)':
+ '@ai-sdk/provider-utils@3.0.22(zod@4.2.1)':
dependencies:
'@ai-sdk/provider': 2.0.1
'@standard-schema/spec': 1.1.0
@@ -9240,7 +9073,7 @@ snapshots:
ajv: 8.17.1
ajv-formats: 3.0.1(ajv@8.17.1)
content-type: 1.0.5
- cors: 2.8.5
+ cors: 2.8.6
cross-spawn: 7.0.6
eventsource: 3.0.7
eventsource-parser: 3.0.6
@@ -9397,45 +9230,10 @@ snapshots:
dependencies:
'@octokit/openapi-types': 12.11.0
- '@oozcitak/dom@1.15.5':
- dependencies:
- '@oozcitak/infra': 1.0.5
- '@oozcitak/url': 1.0.0
- '@oozcitak/util': 8.0.0
-
- '@oozcitak/dom@1.15.6':
- dependencies:
- '@oozcitak/infra': 1.0.5
- '@oozcitak/url': 1.0.0
- '@oozcitak/util': 8.3.4
-
- '@oozcitak/infra@1.0.3':
- dependencies:
- '@oozcitak/util': 1.0.1
-
- '@oozcitak/infra@1.0.5':
- dependencies:
- '@oozcitak/util': 8.0.0
-
- '@oozcitak/url@1.0.0':
- dependencies:
- '@oozcitak/infra': 1.0.3
- '@oozcitak/util': 1.0.2
-
- '@oozcitak/util@1.0.1': {}
-
- '@oozcitak/util@1.0.2': {}
-
- '@oozcitak/util@8.0.0': {}
-
- '@oozcitak/util@8.3.3': {}
-
- '@oozcitak/util@8.3.4': {}
-
- '@openrouter/ai-sdk-provider@1.5.4(ai@5.0.133(zod@4.2.1))(zod@4.2.1)':
+ '@openrouter/ai-sdk-provider@1.5.4(ai@5.0.151(zod@4.2.1))(zod@4.2.1)':
dependencies:
'@openrouter/sdk': 0.1.27
- ai: 5.0.133(zod@4.2.1)
+ ai: 5.0.151(zod@4.2.1)
zod: 4.2.1
'@openrouter/sdk@0.1.27':
@@ -10461,18 +10259,6 @@ snapshots:
'@react-pdf/primitives': 4.1.1
'@react-pdf/stylesheet': 6.1.2
- '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3)':
- dependencies:
- '@standard-schema/spec': 1.1.0
- '@standard-schema/utils': 0.3.0
- immer: 11.1.4
- redux: 5.0.1
- redux-thunk: 3.1.0(redux@5.0.1)
- reselect: 5.1.1
- optionalDependencies:
- react: 19.2.3
- react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1)
-
'@remirror/core-constants@3.0.0': {}
'@rolldown/pluginutils@1.0.0-beta.53': {}
@@ -10918,8 +10704,6 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
- '@standard-schema/utils@0.3.0': {}
-
'@swc/helpers@0.5.18':
dependencies:
tslib: 2.8.1
@@ -11232,6 +11016,11 @@ snapshots:
dependencies:
'@babel/types': 7.28.5
+ '@types/body-parser@1.19.6':
+ dependencies:
+ '@types/connect': 3.4.38
+ '@types/node': 25.0.3
+
'@types/cacheable-request@6.0.3':
dependencies:
'@types/http-cache-semantics': 4.0.4
@@ -11239,6 +11028,14 @@ snapshots:
'@types/node': 25.0.3
'@types/responselike': 1.0.3
+ '@types/connect@3.4.38':
+ dependencies:
+ '@types/node': 25.0.3
+
+ '@types/cors@2.8.19':
+ dependencies:
+ '@types/node': 25.0.3
+
'@types/d3-array@3.2.2': {}
'@types/d3-axis@3.0.6':
@@ -11378,6 +11175,19 @@ snapshots:
'@types/estree@1.0.8': {}
+ '@types/express-serve-static-core@5.1.1':
+ dependencies:
+ '@types/node': 25.0.3
+ '@types/qs': 6.14.0
+ '@types/range-parser': 1.2.7
+ '@types/send': 1.2.1
+
+ '@types/express@5.0.6':
+ dependencies:
+ '@types/body-parser': 1.19.6
+ '@types/express-serve-static-core': 5.1.1
+ '@types/serve-static': 2.2.0
+
'@types/fs-extra@9.0.13':
dependencies:
'@types/node': 25.0.3
@@ -11391,6 +11201,8 @@ snapshots:
'@types/http-cache-semantics@4.0.4': {}
+ '@types/http-errors@2.0.5': {}
+
'@types/json-schema@7.0.15': {}
'@types/katex@0.16.7': {}
@@ -11447,6 +11259,10 @@ snapshots:
dependencies:
'@types/node': 25.0.3
+ '@types/qs@6.14.0': {}
+
+ '@types/range-parser@1.2.7': {}
+
'@types/react-dom@19.2.3(@types/react@19.2.7)':
dependencies:
'@types/react': 19.2.7
@@ -11459,6 +11275,15 @@ snapshots:
dependencies:
'@types/node': 25.0.3
+ '@types/send@1.2.1':
+ dependencies:
+ '@types/node': 25.0.3
+
+ '@types/serve-static@2.2.0':
+ dependencies:
+ '@types/http-errors': 2.0.5
+ '@types/node': 25.0.3
+
'@types/trusted-types@2.0.7':
optional: true
@@ -11718,11 +11543,11 @@ snapshots:
'@opentelemetry/api': 1.9.0
zod: 4.2.1
- ai@5.0.133(zod@4.2.1):
+ ai@5.0.151(zod@4.2.1):
dependencies:
- '@ai-sdk/gateway': 2.0.39(zod@4.2.1)
+ '@ai-sdk/gateway': 2.0.56(zod@4.2.1)
'@ai-sdk/provider': 2.0.1
- '@ai-sdk/provider-utils': 3.0.21(zod@4.2.1)
+ '@ai-sdk/provider-utils': 3.0.22(zod@4.2.1)
'@opentelemetry/api': 1.9.0
zod: 4.2.1
@@ -11904,8 +11729,6 @@ snapshots:
dependencies:
base64-js: 1.5.1
- browser-split@0.0.1: {}
-
browserify-zlib@0.2.0:
dependencies:
pako: 1.0.11
@@ -12000,8 +11823,6 @@ snapshots:
pascal-case: 3.1.2
tslib: 2.8.1
- camelize@1.0.1: {}
-
caniuse-lite@1.0.30001761: {}
ccount@2.0.1: {}
@@ -12173,7 +11994,7 @@ snapshots:
core-util-is@1.0.3: {}
- cors@2.8.5:
+ cors@2.8.6:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
@@ -12422,8 +12243,6 @@ snapshots:
dependencies:
ms: 2.1.3
- decimal.js-light@2.5.1: {}
-
decode-named-character-reference@1.2.0:
dependencies:
character-entities: 2.0.2
@@ -12487,27 +12306,14 @@ snapshots:
minimatch: 3.1.2
p-limit: 3.1.0
- dom-serializer@0.2.2:
- dependencies:
- domelementtype: 2.3.0
- entities: 2.2.0
-
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
- dom-walk@0.1.2: {}
-
- domelementtype@1.3.1: {}
-
domelementtype@2.3.0: {}
- domhandler@2.4.2:
- dependencies:
- domelementtype: 1.3.1
-
domhandler@5.0.3:
dependencies:
domelementtype: 2.3.0
@@ -12516,11 +12322,6 @@ snapshots:
optionalDependencies:
'@types/trusted-types': 2.0.7
- domutils@1.7.0:
- dependencies:
- dom-serializer: 0.2.2
- domelementtype: 1.3.1
-
domutils@3.2.2:
dependencies:
dom-serializer: 2.0.0
@@ -12536,7 +12337,7 @@ snapshots:
duck@0.1.12:
dependencies:
- underscore: 1.13.7
+ underscore: 1.13.8
dunder-proto@1.0.1:
dependencies:
@@ -12661,17 +12462,6 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
- ent@2.2.2:
- dependencies:
- call-bound: 1.0.4
- es-errors: 1.3.0
- punycode: 1.4.1
- safe-regex-test: 1.1.0
-
- entities@1.1.2: {}
-
- entities@2.2.0: {}
-
entities@4.5.0: {}
entities@6.0.1: {}
@@ -12684,12 +12474,6 @@ snapshots:
dependencies:
is-arrayish: 0.2.1
- error@4.4.0:
- dependencies:
- camelize: 1.0.1
- string-template: 0.2.1
- xtend: 4.0.2
-
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
@@ -12707,8 +12491,6 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.2
- es-toolkit@1.45.1: {}
-
es6-error@4.1.1:
optional: true
@@ -12873,10 +12655,6 @@ snapshots:
etag@1.8.1: {}
- ev-store@7.0.0:
- dependencies:
- individual: 3.0.0
-
event-target-shim@5.0.1: {}
eventemitter3@5.0.1: {}
@@ -13320,11 +13098,6 @@ snapshots:
dependencies:
ini: 2.0.0
- global@4.4.0:
- dependencies:
- min-document: 2.19.2
- process: 0.11.10
-
globals@14.0.0: {}
globals@16.5.0: {}
@@ -13571,44 +13344,10 @@ snapshots:
hsl-to-rgb-for-reals@1.1.1: {}
- html-entities@2.6.0: {}
-
- html-to-docx@1.8.0(encoding@0.1.13):
- dependencies:
- '@oozcitak/dom': 1.15.6
- '@oozcitak/util': 8.3.4
- color-name: 1.1.4
- html-entities: 2.6.0
- html-to-vdom: 0.7.0
- image-size: 1.2.1
- image-to-base64: 2.2.0(encoding@0.1.13)
- jszip: 3.10.1
- lodash: 4.17.21
- mime-types: 2.1.35
- nanoid: 3.3.11
- virtual-dom: 2.1.1
- xmlbuilder2: 2.1.2
- transitivePeerDependencies:
- - encoding
-
- html-to-vdom@0.7.0:
- dependencies:
- ent: 2.2.2
- htmlparser2: 3.10.1
-
html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}
- htmlparser2@3.10.1:
- dependencies:
- domelementtype: 1.3.1
- domhandler: 2.4.2
- domutils: 1.7.0
- entities: 1.1.2
- inherits: 2.0.4
- readable-stream: 3.6.2
-
http-cache-semantics@4.2.0: {}
http-errors@2.0.1:
@@ -13673,22 +13412,8 @@ snapshots:
image-size@0.7.5:
optional: true
- image-size@1.2.1:
- dependencies:
- queue: 6.0.2
-
- image-to-base64@2.2.0(encoding@0.1.13):
- dependencies:
- node-fetch: 2.7.0(encoding@0.1.13)
- transitivePeerDependencies:
- - encoding
-
immediate@3.0.6: {}
- immer@10.2.0: {}
-
- immer@11.1.4: {}
-
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
@@ -13701,8 +13426,6 @@ snapshots:
indent-string@4.0.0: {}
- individual@3.0.0: {}
-
infer-owner@1.0.4: {}
inflight@1.0.6:
@@ -13777,8 +13500,6 @@ snapshots:
is-number@7.0.0: {}
- is-object@1.0.2: {}
-
is-plain-obj@4.1.0: {}
is-promise@4.0.0: {}
@@ -13786,13 +13507,6 @@ snapshots:
is-property@1.0.2:
optional: true
- is-regex@1.2.1:
- dependencies:
- call-bound: 1.0.4
- gopd: 1.2.0
- has-tostringtag: 1.0.2
- hasown: 2.0.2
-
is-stream@1.1.0: {}
is-stream@2.0.1: {}
@@ -14078,7 +13792,7 @@ snapshots:
dependencies:
duck: 0.1.12
option: 0.2.4
- underscore: 1.13.7
+ underscore: 1.13.8
lower-case@2.0.2:
dependencies:
@@ -14147,7 +13861,7 @@ snapshots:
jszip: 3.10.1
lop: 0.4.2
path-is-absolute: 1.0.1
- underscore: 1.13.7
+ underscore: 1.13.8
xmlbuilder: 10.1.1
map-age-cleaner@0.1.3:
@@ -14638,10 +14352,6 @@ snapshots:
mimic-response@3.1.0: {}
- min-document@2.19.2:
- dependencies:
- dom-walk: 0.1.2
-
minimatch@10.1.1:
dependencies:
'@isaacs/brace-expansion': 5.0.0
@@ -14757,8 +14467,6 @@ snapshots:
neo-async@2.6.2: {}
- next-tick@0.2.2: {}
-
nice-try@1.0.5: {}
no-case@3.0.4:
@@ -15247,8 +14955,6 @@ snapshots:
punycode.js@2.3.1: {}
- punycode@1.4.1: {}
-
punycode@2.3.1: {}
pusher-js@8.4.0:
@@ -15358,15 +15064,6 @@ snapshots:
react-is@16.13.1: {}
- react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1):
- dependencies:
- '@types/use-sync-external-store': 0.0.6
- react: 19.2.3
- use-sync-external-store: 1.6.0(react@19.2.3)
- optionalDependencies:
- '@types/react': 19.2.7
- redux: 5.0.1
-
react-refresh@0.18.0: {}
react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.3):
@@ -15441,36 +15138,10 @@ snapshots:
readdirp@4.1.2: {}
- recharts@3.8.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1):
- dependencies:
- '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3)
- clsx: 2.1.1
- decimal.js-light: 2.5.1
- es-toolkit: 1.45.1
- eventemitter3: 5.0.1
- immer: 10.2.0
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
- react-is: 16.13.1
- react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1)
- reselect: 5.1.1
- tiny-invariant: 1.3.3
- use-sync-external-store: 1.6.0(react@19.2.3)
- victory-vendor: 37.3.6
- transitivePeerDependencies:
- - '@types/react'
- - redux
-
rechoir@0.8.0:
dependencies:
resolve: 1.22.11
- redux-thunk@3.1.0(redux@5.0.1):
- dependencies:
- redux: 5.0.1
-
- redux@5.0.1: {}
-
regex-recursion@6.0.2:
dependencies:
regex-utilities: 2.3.0
@@ -15577,8 +15248,6 @@ snapshots:
dependencies:
pe-library: 1.0.1
- reselect@5.1.1: {}
-
resolve-alpn@1.2.1: {}
resolve-from@4.0.0: {}
@@ -15697,12 +15366,6 @@ snapshots:
safe-buffer@5.2.1: {}
- safe-regex-test@1.1.0:
- dependencies:
- call-bound: 1.0.4
- es-errors: 1.3.0
- is-regex: 1.2.1
-
safer-buffer@2.1.2: {}
scheduler@0.25.0-rc-603e6108-20241029: {}
@@ -15953,8 +15616,6 @@ snapshots:
- micromark-util-types
- supports-color
- string-template@0.2.1: {}
-
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
@@ -16072,8 +15733,6 @@ snapshots:
tiny-inflate@1.0.3: {}
- tiny-invariant@1.3.3: {}
-
tinyexec@1.0.2: {}
tinyglobby@0.2.15:
@@ -16194,7 +15853,7 @@ snapshots:
ufo@1.6.1: {}
- underscore@1.13.7: {}
+ underscore@1.13.8: {}
undici-types@6.21.0: {}
@@ -16345,34 +16004,6 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
- victory-vendor@37.3.6:
- dependencies:
- '@types/d3-array': 3.2.2
- '@types/d3-ease': 3.0.2
- '@types/d3-interpolate': 3.0.4
- '@types/d3-scale': 4.0.9
- '@types/d3-shape': 3.1.7
- '@types/d3-time': 3.0.4
- '@types/d3-timer': 3.0.2
- d3-array: 3.2.4
- d3-ease: 3.0.1
- d3-interpolate: 3.0.1
- d3-scale: 4.0.2
- d3-shape: 3.2.0
- d3-time: 3.1.0
- d3-timer: 3.0.1
-
- virtual-dom@2.1.1:
- dependencies:
- browser-split: 0.0.1
- error: 4.4.0
- ev-store: 7.0.0
- global: 4.4.0
- is-object: 1.0.2
- next-tick: 0.2.2
- x-is-array: 0.1.0
- x-is-string: 0.1.0
-
vite-compatible-readable-stream@3.6.1:
dependencies:
inherits: 2.0.4
@@ -16524,10 +16155,6 @@ snapshots:
wrappy@1.0.2: {}
- x-is-array@0.1.0: {}
-
- x-is-string@0.1.0: {}
-
xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
@@ -16538,17 +16165,12 @@ snapshots:
wmf: 1.0.2
word: 0.3.0
- xmlbuilder2@2.1.2:
- dependencies:
- '@oozcitak/dom': 1.15.5
- '@oozcitak/infra': 1.0.5
- '@oozcitak/util': 8.3.3
-
xmlbuilder@10.1.1: {}
xmlbuilder@15.1.1: {}
- xtend@4.0.2: {}
+ xtend@4.0.2:
+ optional: true
y18n@5.0.8: {}
From 86cc2aaf7340afaad5ca8cf3a5d695c8291a6b01 Mon Sep 17 00:00:00 2001
From: arkml <6592213+arkml@users.noreply.github.com>
Date: Mon, 30 Mar 2026 22:31:49 +0530
Subject: [PATCH 6/9] Meeting notes2 (#454)
Improve meeting transcription: screen recording permissions, collapsible transcript block
---
apps/x/apps/main/forge.config.cjs | 3 +
apps/x/apps/main/src/ipc.ts | 20 +-
apps/x/apps/renderer/src/App.tsx | 76 +--
.../src/components/markdown-editor.tsx | 4 +
.../src/extensions/transcript-block.tsx | 177 +++++++
.../src/hooks/useMeetingTranscription.ts | 161 +++---
apps/x/apps/renderer/src/styles/editor.css | 77 ++-
.../core/src/knowledge/summarize_meeting.ts | 3 +-
apps/x/packages/shared/src/blocks.ts | 6 +
apps/x/packages/shared/src/ipc.ts | 10 +
apps/x/pnpm-lock.yaml | 464 +++++++++++++++++-
11 files changed, 885 insertions(+), 116 deletions(-)
create mode 100644 apps/x/apps/renderer/src/extensions/transcript-block.tsx
diff --git a/apps/x/apps/main/forge.config.cjs b/apps/x/apps/main/forge.config.cjs
index c79a8c43..178cb7e1 100644
--- a/apps/x/apps/main/forge.config.cjs
+++ b/apps/x/apps/main/forge.config.cjs
@@ -11,6 +11,9 @@ module.exports = {
icon: './icons/icon', // .icns extension added automatically
appBundleId: 'com.rowboat.app',
appCategoryType: 'public.app-category.productivity',
+ extendInfo: {
+ NSAudioCaptureUsageDescription: 'Rowboat needs access to system audio to transcribe meetings from other apps (Zoom, Meet, etc.)',
+ },
osxSign: {
batchCodesignCalls: true,
optionsForFile: () => ({
diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts
index b92e3fe9..0fa0de79 100644
--- a/apps/x/apps/main/src/ipc.ts
+++ b/apps/x/apps/main/src/ipc.ts
@@ -1,4 +1,4 @@
-import { ipcMain, BrowserWindow, shell, dialog } from 'electron';
+import { ipcMain, BrowserWindow, shell, dialog, systemPreferences, desktopCapturer } from 'electron';
import { ipc } from '@x/shared';
import path from 'node:path';
import os from 'node:os';
@@ -719,6 +719,24 @@ export function setupIpcHandlers() {
return { success: false, error: 'Unknown format' };
},
+ 'meeting:checkScreenPermission': async () => {
+ if (process.platform !== 'darwin') return { granted: true };
+ const status = systemPreferences.getMediaAccessStatus('screen');
+ console.log('[meeting] Screen recording permission status:', status);
+ if (status === 'granted') return { granted: true };
+ // Not granted — call desktopCapturer.getSources() to register the app
+ // in the macOS Screen Recording list. On first call this shows the
+ // native permission prompt (signed apps are remembered across restarts).
+ try { await desktopCapturer.getSources({ types: ['screen'] }); } catch { /* ignore */ }
+ // Re-check after the native prompt was dismissed
+ const statusAfter = systemPreferences.getMediaAccessStatus('screen');
+ console.log('[meeting] Screen recording permission status after prompt:', statusAfter);
+ return { granted: statusAfter === 'granted' };
+ },
+ 'meeting:openScreenRecordingSettings': async () => {
+ await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture');
+ return { success: true };
+ },
'meeting:summarize': async (_event, args) => {
const notes = await summarizeMeeting(args.transcript, args.meetingStartTime, args.calendarEventJson);
return { notes };
diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx
index f83ea5cb..b2bc9d7f 100644
--- a/apps/x/apps/renderer/src/App.tsx
+++ b/apps/x/apps/renderer/src/App.tsx
@@ -484,7 +484,7 @@ function FixedSidebarToggle({
)}
style={{ marginLeft: TITLEBAR_BUTTON_GAP_PX }}
>
- {meetingSummarizing ? (
+ {meetingSummarizing || meetingState === 'connecting' ? (
) : meetingState === 'recording' ? (
@@ -494,7 +494,7 @@ function FixedSidebarToggle({
- {meetingSummarizing ? 'Generating meeting notes...' : meetingState === 'recording' ? 'Stop meeting notes' : 'Take new meeting notes'}
+ {meetingSummarizing ? 'Generating meeting notes...' : meetingState === 'connecting' ? 'Starting transcription...' : meetingState === 'recording' ? 'Stop meeting notes' : 'Take new meeting notes'}
)}
@@ -3417,9 +3417,9 @@ function App() {
const [meetingSummarizing, setMeetingSummarizing] = useState(false)
const [showMeetingPermissions, setShowMeetingPermissions] = useState(false)
- const startMeetingAfterPermissions = useCallback(async () => {
- setShowMeetingPermissions(false)
- localStorage.setItem('meeting-permissions-acknowledged', '1')
+ const [checkingPermission, setCheckingPermission] = useState(false)
+
+ const startMeetingNow = useCallback(async () => {
const calEvent = pendingCalendarEventRef.current
pendingCalendarEventRef.current = undefined
const notePath = await meetingTranscription.start(calEvent)
@@ -3429,6 +3429,23 @@ function App() {
}
}, [meetingTranscription, handleVoiceNoteCreated])
+ const handleCheckPermissionAndRetry = useCallback(async () => {
+ setCheckingPermission(true)
+ try {
+ const { granted } = await window.ipc.invoke('meeting:checkScreenPermission', null)
+ if (granted) {
+ setShowMeetingPermissions(false)
+ await startMeetingNow()
+ }
+ } finally {
+ setCheckingPermission(false)
+ }
+ }, [startMeetingNow])
+
+ const handleOpenScreenRecordingSettings = useCallback(async () => {
+ await window.ipc.invoke('meeting:openScreenRecordingSettings', null)
+ }, [])
+
const handleToggleMeeting = useCallback(async () => {
if (meetingTranscription.state === 'recording') {
await meetingTranscription.stop()
@@ -3450,16 +3467,15 @@ function App() {
const calendarEventJson = calEventMatch?.[1]?.replace(/''/g, "'")
const { notes } = await window.ipc.invoke('meeting:summarize', { transcript: fileContent, meetingStartTime, calendarEventJson })
if (notes) {
- // Prepend meeting notes below the title but above the transcript
- const { raw: fm, body: transcriptBody } = splitFrontmatter(fileContent)
- // Use frontmatter title as the heading (set from calendar event summary)
+ // Prepend meeting notes above the existing transcript block
+ const { raw: fm, body } = splitFrontmatter(fileContent)
const fmTitleMatch = fileContent.match(/^title:\s*(.+)$/m)
- const noteTitle = fmTitleMatch?.[1]?.trim() || 'Meeting note'
- // Strip any existing top-level heading from body
- const bodyWithoutTitle = transcriptBody.replace(/^#\s+.+\s*\n*/, '')
- // Also strip any title/heading the LLM may have generated
+ const noteTitle = fmTitleMatch?.[1]?.trim() || 'Meeting Notes'
const cleanedNotes = notes.replace(/^#{1,2}\s+.+\n+/, '')
- const newBody = `# ${noteTitle}\n\n` + cleanedNotes + '\n\n---\n\n## Raw transcript\n\n' + bodyWithoutTitle
+ // Extract the existing transcript block and preserve it as-is
+ const transcriptBlockMatch = body.match(/(```transcript\n[\s\S]*?\n```)/)
+ const transcriptBlock = transcriptBlockMatch?.[1] || ''
+ const newBody = `# ${noteTitle}\n\n` + cleanedNotes + (transcriptBlock ? '\n\n' + transcriptBlock : '')
const newContent = fm ? `${fm}\n${newBody}` : newBody
await window.ipc.invoke('workspace:writeFile', {
path: notePath,
@@ -3477,20 +3493,18 @@ function App() {
meetingNotePathRef.current = null
}
} else if (meetingTranscription.state === 'idle') {
- // Show permissions modal on first use (macOS only — Windows works out of the box)
- if (isMac && !localStorage.getItem('meeting-permissions-acknowledged')) {
- setShowMeetingPermissions(true)
- return
- }
- const calEvent = pendingCalendarEventRef.current
- pendingCalendarEventRef.current = undefined
- const notePath = await meetingTranscription.start(calEvent)
- if (notePath) {
- meetingNotePathRef.current = notePath
- await handleVoiceNoteCreated(notePath)
+ // On macOS, check screen recording permission before starting
+ if (isMac) {
+ const result = await window.ipc.invoke('meeting:checkScreenPermission', null)
+ console.log('[meeting] Permission check result:', result)
+ if (!result.granted) {
+ setShowMeetingPermissions(true)
+ return
+ }
}
+ await startMeetingNow()
}
- }, [meetingTranscription, handleVoiceNoteCreated])
+ }, [meetingTranscription, handleVoiceNoteCreated, startMeetingNow])
handleToggleMeetingRef.current = handleToggleMeeting
// Listen for calendar block "join meeting & take notes" events
@@ -4421,23 +4435,25 @@ function App() {
diff --git a/apps/x/apps/renderer/src/components/markdown-editor.tsx b/apps/x/apps/renderer/src/components/markdown-editor.tsx
index 2592dec3..f3ccba2d 100644
--- a/apps/x/apps/renderer/src/components/markdown-editor.tsx
+++ b/apps/x/apps/renderer/src/components/markdown-editor.tsx
@@ -15,6 +15,7 @@ import { ChartBlockExtension } from '@/extensions/chart-block'
import { TableBlockExtension } from '@/extensions/table-block'
import { CalendarBlockExtension } from '@/extensions/calendar-block'
import { EmailBlockExtension } from '@/extensions/email-block'
+import { TranscriptBlockExtension } from '@/extensions/transcript-block'
import { Markdown } from 'tiptap-markdown'
import { useEffect, useCallback, useMemo, useRef, useState } from 'react'
import { Calendar, ChevronDown, ExternalLink } from 'lucide-react'
@@ -155,6 +156,8 @@ function getMarkdownWithBlankLines(editor: Editor): string {
blocks.push('```calendar\n' + (node.attrs?.data as string || '{}') + '\n```')
} else if (node.type === 'emailBlock') {
blocks.push('```email\n' + (node.attrs?.data as string || '{}') + '\n```')
+ } else if (node.type === 'transcriptBlock') {
+ blocks.push('```transcript\n' + (node.attrs?.data as string || '{}') + '\n```')
} else if (node.type === 'codeBlock') {
const lang = (node.attrs?.language as string) || ''
blocks.push('```' + lang + '\n' + nodeToText(node) + '\n```')
@@ -567,6 +570,7 @@ export function MarkdownEditor({
TableBlockExtension,
CalendarBlockExtension,
EmailBlockExtension,
+ TranscriptBlockExtension,
WikiLink.configure({
onCreate: wikiLinks?.onCreate
? (path) => {
diff --git a/apps/x/apps/renderer/src/extensions/transcript-block.tsx b/apps/x/apps/renderer/src/extensions/transcript-block.tsx
new file mode 100644
index 00000000..9b76f568
--- /dev/null
+++ b/apps/x/apps/renderer/src/extensions/transcript-block.tsx
@@ -0,0 +1,177 @@
+import { mergeAttributes, Node } from '@tiptap/react'
+import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react'
+import { ChevronDown, FileText } from 'lucide-react'
+import { blocks } from '@x/shared'
+import { useState, useMemo } from 'react'
+
+interface TranscriptEntry {
+ speaker: string
+ text: string
+}
+
+function parseTranscript(raw: string): TranscriptEntry[] {
+ const entries: TranscriptEntry[] = []
+ const lines = raw.split('\n')
+ for (const line of lines) {
+ const trimmed = line.trim()
+ if (!trimmed) continue
+ // Match **Speaker Name:** text or **You:** text
+ const match = trimmed.match(/^\*\*(.+?):\*\*\s*(.*)$/)
+ if (match) {
+ entries.push({ speaker: match[1], text: match[2] })
+ } else if (entries.length > 0) {
+ // Continuation line — append to last entry
+ entries[entries.length - 1].text += ' ' + trimmed
+ }
+ }
+ return entries
+}
+
+function speakerColor(speaker: string): string {
+ // Simple hash to pick a consistent color per speaker
+ let hash = 0
+ for (let i = 0; i < speaker.length; i++) {
+ hash = speaker.charCodeAt(i) + ((hash << 5) - hash)
+ }
+ const colors = [
+ '#3b82f6', // blue
+ '#06b6d4', // cyan
+ '#6366f1', // indigo
+ '#8b5cf6', // purple
+ '#0ea5e9', // sky
+ '#2563eb', // blue darker
+ '#7c3aed', // violet
+ ]
+ return colors[Math.abs(hash) % colors.length]
+}
+
+function TranscriptBlockView({ node, getPos, editor }: {
+ node: { attrs: Record }
+ getPos: () => number | undefined
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ editor: any
+}) {
+ const raw = node.attrs.data as string
+ let config: blocks.TranscriptBlock | null = null
+
+ try {
+ config = blocks.TranscriptBlockSchema.parse(JSON.parse(raw))
+ } catch {
+ // fallback below
+ }
+
+ // Auto-detect: expand if this is the first real block (live recording),
+ // collapse if there's other content above (notes have been generated)
+ const isFirstBlock = useMemo(() => {
+ try {
+ const pos = getPos()
+ if (pos === undefined) return false
+ const firstChild = editor?.state?.doc?.firstChild
+ if (!firstChild) return true
+ // If the transcript block is right after the first node (heading), it's the main content
+ return pos <= (firstChild.nodeSize ?? 0) + 1
+ } catch {
+ return false
+ }
+ }, [getPos, editor])
+
+ const [expanded, setExpanded] = useState(isFirstBlock)
+
+ const entries = useMemo(() => {
+ if (!config) return []
+ return parseTranscript(config.transcript)
+ }, [config])
+
+ if (!config) {
+ return (
+
+
+
+ Invalid transcript block
+
+
+ )
+ }
+
+ return (
+
+ e.stopPropagation()}>
+
+ {expanded && (
+
+ {entries.length > 0 ? (
+ entries.map((entry, i) => (
+
+
+ {entry.speaker}
+
+ {entry.text}
+
+ ))
+ ) : (
+
{config.transcript}
+ )}
+
+ )}
+
+
+ )
+}
+
+export const TranscriptBlockExtension = Node.create({
+ name: 'transcriptBlock',
+ group: 'block',
+ atom: true,
+ selectable: true,
+ draggable: false,
+
+ addAttributes() {
+ return {
+ data: { default: '{}' },
+ }
+ },
+
+ parseHTML() {
+ return [{
+ tag: 'pre',
+ priority: 60,
+ getAttrs(element) {
+ const code = element.querySelector('code')
+ if (!code) return false
+ const cls = code.className || ''
+ if (cls.includes('language-transcript')) {
+ return { data: code.textContent || '{}' }
+ }
+ return false
+ },
+ }]
+ },
+
+ renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) {
+ return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'transcript-block' })]
+ },
+
+ addNodeView() {
+ return ReactNodeViewRenderer(TranscriptBlockView)
+ },
+
+ addStorage() {
+ return {
+ markdown: {
+ serialize(state: { write: (text: string) => void; closeBlock: (node: unknown) => void }, node: { attrs: { data: string } }) {
+ state.write('```transcript\n' + node.attrs.data + '\n```')
+ state.closeBlock(node)
+ },
+ parse: {},
+ },
+ }
+ },
+})
diff --git a/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts b/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts
index 35a0a703..50d89a57 100644
--- a/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts
+++ b/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts
@@ -60,7 +60,7 @@ export interface CalendarEventMeta {
}
function formatTranscript(entries: TranscriptEntry[], date: string, calendarEvent?: CalendarEventMeta): string {
- const noteTitle = calendarEvent?.summary || 'Meeting note';
+ const noteTitle = calendarEvent?.summary || 'Meeting Notes';
const lines = [
'---',
'type: meeting',
@@ -89,13 +89,18 @@ function formatTranscript(entries: TranscriptEntry[], date: string, calendarEven
`# ${noteTitle}`,
'',
);
+ // Build the raw transcript text
+ const transcriptLines: string[] = [];
for (let i = 0; i < entries.length; i++) {
if (i > 0 && entries[i].speaker !== entries[i - 1].speaker) {
- lines.push('');
+ transcriptLines.push('');
}
- lines.push(`**${entries[i].speaker}:** ${entries[i].text}`);
- lines.push('');
+ transcriptLines.push(`**${entries[i].speaker}:** ${entries[i].text}`);
+ transcriptLines.push('');
}
+ const transcriptText = transcriptLines.join('\n').trim();
+ const transcriptData = JSON.stringify({ transcript: transcriptText });
+ lines.push('```transcript', transcriptData, '```');
return lines.join('\n');
}
@@ -187,52 +192,83 @@ export function useMeetingTranscription(onAutoStop?: () => void) {
if (state !== 'idle') return null;
setState('connecting');
- // Detect headphones vs speakers
- const usingHeadphones = await detectHeadphones();
- console.log(`[meeting] Audio output mode: ${usingHeadphones ? 'headphones' : 'speakers'}`);
-
- // Rowboat WebSocket + bearer token when signed in; else local Deepgram API key
- let ws: WebSocket;
- try {
- const account = await refreshRowboatAccount();
- if (
- account?.signedIn &&
- account.accessToken &&
- account.config?.websocketApiUrl
- ) {
- const listenUrl = buildDeepgramListenUrl(account.config.websocketApiUrl, DEEPGRAM_PARAMS);
- console.log('[meeting] Using Rowboat WebSocket');
- ws = new WebSocket(listenUrl, ['bearer', account.accessToken]);
- } else {
- const config = await window.ipc.invoke('voice:getConfig', null);
- if (!config?.deepgram) {
- console.error('[meeting] No Deepgram config available');
- setState('idle');
- return null;
+ // Run independent setup steps in parallel for faster startup
+ const [headphoneResult, wsResult, micResult, systemResult] = await Promise.allSettled([
+ // 1. Detect headphones vs speakers
+ detectHeadphones(),
+ // 2. Set up Deepgram WebSocket (account refresh + connect + wait for open)
+ (async () => {
+ const account = await refreshRowboatAccount();
+ let ws: WebSocket;
+ if (
+ account?.signedIn &&
+ account.accessToken &&
+ account.config?.websocketApiUrl
+ ) {
+ const listenUrl = buildDeepgramListenUrl(account.config.websocketApiUrl, DEEPGRAM_PARAMS);
+ console.log('[meeting] Using Rowboat WebSocket');
+ ws = new WebSocket(listenUrl, ['bearer', account.accessToken]);
+ } else {
+ const config = await window.ipc.invoke('voice:getConfig', null);
+ if (!config?.deepgram) {
+ throw new Error('No Deepgram config available');
+ }
+ console.log('[meeting] Using Deepgram API key');
+ ws = new WebSocket(DEEPGRAM_LISTEN_URL, ['token', config.deepgram.apiKey]);
}
- console.log('[meeting] Using Deepgram API key');
- ws = new WebSocket(DEEPGRAM_LISTEN_URL, ['token', config.deepgram.apiKey]);
- }
- } catch (err) {
- console.error('[meeting] Failed to connect Deepgram:', err);
- setState('idle');
- return null;
- }
- wsRef.current = ws;
+ const ok = await new Promise((resolve) => {
+ ws.onopen = () => resolve(true);
+ ws.onerror = () => resolve(false);
+ setTimeout(() => resolve(false), 5000);
+ });
+ if (!ok) throw new Error('WebSocket failed to connect');
+ console.log('[meeting] WebSocket connected');
+ return ws;
+ })(),
+ // 3. Get mic stream
+ navigator.mediaDevices.getUserMedia({
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ },
+ }),
+ // 4. Get system audio via getDisplayMedia (loopback)
+ (async () => {
+ const stream = await navigator.mediaDevices.getDisplayMedia({ audio: true, video: true });
+ stream.getVideoTracks().forEach(t => t.stop());
+ if (stream.getAudioTracks().length === 0) {
+ stream.getTracks().forEach(t => t.stop());
+ throw new Error('No audio track from getDisplayMedia');
+ }
+ console.log('[meeting] System audio captured');
+ return stream;
+ })(),
+ ]);
- // Wait for WS open
- const wsOk = await new Promise((resolve) => {
- ws.onopen = () => resolve(true);
- ws.onerror = () => resolve(false);
- setTimeout(() => resolve(false), 5000);
- });
- if (!wsOk) {
- console.error('[meeting] WebSocket failed to connect');
+ // Check for failures — clean up any successful resources if something failed
+ const failed = wsResult.status === 'rejected'
+ || micResult.status === 'rejected'
+ || systemResult.status === 'rejected';
+
+ if (failed) {
+ if (wsResult.status === 'rejected') console.error('[meeting] WebSocket setup failed:', wsResult.reason);
+ if (micResult.status === 'rejected') console.error('[meeting] Microphone access denied:', micResult.reason);
+ if (systemResult.status === 'rejected') console.error('[meeting] System audio access denied:', systemResult.reason);
+ // Clean up any resources that did succeed
+ if (wsResult.status === 'fulfilled') { wsResult.value.close(); }
+ if (micResult.status === 'fulfilled') { micResult.value.getTracks().forEach(t => t.stop()); }
+ if (systemResult.status === 'fulfilled') { systemResult.value.getTracks().forEach(t => t.stop()); }
cleanup();
setState('idle');
return null;
}
- console.log('[meeting] WebSocket connected');
+
+ const usingHeadphones = headphoneResult.status === 'fulfilled' ? headphoneResult.value : false;
+ console.log(`[meeting] Audio output mode: ${usingHeadphones ? 'headphones' : 'speakers'}`);
+
+ const ws = wsResult.value;
+ wsRef.current = ws;
// Set up WS message handler
transcriptRef.current = [];
@@ -283,43 +319,10 @@ export function useMeetingTranscription(onAutoStop?: () => void) {
wsRef.current = null;
};
- // Get mic stream
- let micStream: MediaStream;
- try {
- micStream = await navigator.mediaDevices.getUserMedia({
- audio: {
- echoCancellation: true,
- noiseSuppression: true,
- autoGainControl: true,
- },
- });
- } catch (err) {
- console.error('[meeting] Microphone access denied:', err);
- cleanup();
- setState('idle');
- return null;
- }
+ const micStream = micResult.value;
micStreamRef.current = micStream;
- // Get system audio via getDisplayMedia (loopback)
- let systemStream: MediaStream;
- try {
- systemStream = await navigator.mediaDevices.getDisplayMedia({ audio: true, video: true });
- systemStream.getVideoTracks().forEach(t => t.stop());
- } catch (err) {
- console.error('[meeting] System audio access denied:', err);
- cleanup();
- setState('idle');
- return null;
- }
- if (systemStream.getAudioTracks().length === 0) {
- console.error('[meeting] No audio track from getDisplayMedia');
- systemStream.getTracks().forEach(t => t.stop());
- cleanup();
- setState('idle');
- return null;
- }
- console.log('[meeting] System audio captured');
+ const systemStream = systemResult.value;
systemStreamRef.current = systemStream;
// ----- Audio pipeline -----
diff --git a/apps/x/apps/renderer/src/styles/editor.css b/apps/x/apps/renderer/src/styles/editor.css
index 8701099b..efa481c1 100644
--- a/apps/x/apps/renderer/src/styles/editor.css
+++ b/apps/x/apps/renderer/src/styles/editor.css
@@ -618,7 +618,8 @@
.tiptap-editor .ProseMirror .chart-block-wrapper,
.tiptap-editor .ProseMirror .table-block-wrapper,
.tiptap-editor .ProseMirror .calendar-block-wrapper,
-.tiptap-editor .ProseMirror .email-block-wrapper {
+.tiptap-editor .ProseMirror .email-block-wrapper,
+.tiptap-editor .ProseMirror .transcript-block-wrapper {
margin: 8px 0;
}
@@ -628,7 +629,8 @@
.tiptap-editor .ProseMirror .table-block-card,
.tiptap-editor .ProseMirror .calendar-block-card,
.tiptap-editor .ProseMirror .email-block-card,
-.tiptap-editor .ProseMirror .email-draft-block-card {
+.tiptap-editor .ProseMirror .email-draft-block-card,
+.tiptap-editor .ProseMirror .transcript-block-card {
position: relative;
padding: 12px 14px;
border: 1px solid var(--border);
@@ -644,7 +646,8 @@
.tiptap-editor .ProseMirror .table-block-card:hover,
.tiptap-editor .ProseMirror .calendar-block-card:hover,
.tiptap-editor .ProseMirror .email-block-card:hover,
-.tiptap-editor .ProseMirror .email-draft-block-card:hover {
+.tiptap-editor .ProseMirror .email-draft-block-card:hover,
+.tiptap-editor .ProseMirror .transcript-block-card:hover {
background-color: color-mix(in srgb, var(--muted) 70%, transparent);
}
@@ -1488,6 +1491,74 @@
margin-left: 4px;
}
+/* Transcript block */
+.tiptap-editor .ProseMirror .transcript-block-toggle {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ width: 100%;
+ padding: 0;
+ font-size: 13px;
+ font-weight: 500;
+ color: color-mix(in srgb, var(--foreground) 60%, transparent);
+ background: none;
+ border: none;
+ cursor: pointer;
+ transition: color 0.12s ease;
+}
+
+.tiptap-editor .ProseMirror .transcript-block-toggle:hover {
+ color: var(--foreground);
+}
+
+.tiptap-editor .ProseMirror .transcript-block-chevron {
+ transition: transform 0.15s ease;
+ flex-shrink: 0;
+}
+
+.tiptap-editor .ProseMirror .transcript-block-chevron-open {
+ transform: rotate(180deg);
+}
+
+.tiptap-editor .ProseMirror .transcript-block-content {
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.tiptap-editor .ProseMirror .transcript-entry {
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+.tiptap-editor .ProseMirror .transcript-speaker {
+ font-weight: 600;
+ margin-right: 6px;
+}
+
+.tiptap-editor .ProseMirror .transcript-text {
+ color: color-mix(in srgb, var(--foreground) 75%, transparent);
+}
+
+.tiptap-editor .ProseMirror .transcript-raw {
+ font-size: 13px;
+ line-height: 1.6;
+ color: color-mix(in srgb, var(--foreground) 70%, transparent);
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.tiptap-editor .ProseMirror .transcript-block-error {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: color-mix(in srgb, var(--foreground) 55%, transparent);
+ font-size: 13px;
+}
+
/* Meeting event banner */
.meeting-event-banner {
position: relative;
diff --git a/apps/x/packages/core/src/knowledge/summarize_meeting.ts b/apps/x/packages/core/src/knowledge/summarize_meeting.ts
index 534b6655..30e3c5d4 100644
--- a/apps/x/packages/core/src/knowledge/summarize_meeting.ts
+++ b/apps/x/packages/core/src/knowledge/summarize_meeting.ts
@@ -15,7 +15,8 @@ const SYSTEM_PROMPT = `You are a meeting notes assistant. Given a raw meeting tr
## Calendar matching
You will be given the transcript (with a timestamp of when recording started) and recent calendar events with their titles, times, and attendees. If a calendar event clearly matches this meeting (overlapping time + content aligns), then:
- Do NOT output a title or heading — the title is already set by the caller.
-- Replace generic speaker labels ("Speaker 0", "Speaker 1", "System audio") with actual attendee names, but ONLY if you have HIGH CONFIDENCE about which speaker is which based on the discussion content. If unsure, use "They" instead of "Speaker 0" etc.
+- ONLY use names from the calendar event attendee list. Do NOT introduce names that are not in the attendee list — any unrecognized names in the transcript are transcription errors.
+- Replace generic speaker labels ("Speaker 0", "Speaker 1", "System audio") with actual attendee names from the list, but ONLY if you have HIGH CONFIDENCE about which speaker is which based on the discussion content. If unsure, use "They" instead of "Speaker 0" etc.
- "You" in the transcript is the local user — if the calendar event has an organizer or you can identify who "You" is from context, use their name.
If no calendar event matches with high confidence, or if no calendar events are provided, use "They" for all non-"You" speakers.
diff --git a/apps/x/packages/shared/src/blocks.ts b/apps/x/packages/shared/src/blocks.ts
index 55d1cd3e..68209051 100644
--- a/apps/x/packages/shared/src/blocks.ts
+++ b/apps/x/packages/shared/src/blocks.ts
@@ -74,3 +74,9 @@ export const EmailBlockSchema = z.object({
});
export type EmailBlock = z.infer;
+
+export const TranscriptBlockSchema = z.object({
+ transcript: z.string(),
+});
+
+export type TranscriptBlock = z.infer;
diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts
index 5f4988f4..28718db3 100644
--- a/apps/x/packages/shared/src/ipc.ts
+++ b/apps/x/packages/shared/src/ipc.ts
@@ -501,6 +501,16 @@ const ipcSchemas = {
mimeType: z.string(),
}),
},
+ 'meeting:checkScreenPermission': {
+ req: z.null(),
+ res: z.object({
+ granted: z.boolean(),
+ }),
+ },
+ 'meeting:openScreenRecordingSettings': {
+ req: z.null(),
+ res: z.object({ success: z.boolean() }),
+ },
'meeting:summarize': {
req: z.object({
transcript: z.string(),
diff --git a/apps/x/pnpm-lock.yaml b/apps/x/pnpm-lock.yaml
index e59ce990..01a9240f 100644
--- a/apps/x/pnpm-lock.yaml
+++ b/apps/x/pnpm-lock.yaml
@@ -53,6 +53,9 @@ importers:
electron-squirrel-startup:
specifier: ^1.0.1
version: 1.0.1
+ html-to-docx:
+ specifier: ^1.8.0
+ version: 1.8.0(encoding@0.1.13)
mammoth:
specifier: ^1.11.0
version: 1.11.0
@@ -235,6 +238,9 @@ importers:
react-dom:
specifier: ^19.2.0
version: 19.2.3(react@19.2.3)
+ recharts:
+ specifier: ^3.8.0
+ version: 3.8.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1)
sonner:
specifier: ^2.0.7
version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -1578,6 +1584,46 @@ packages:
'@octokit/types@6.41.0':
resolution: {integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==}
+ '@oozcitak/dom@1.15.5':
+ resolution: {integrity: sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A==}
+ engines: {node: '>=8.0'}
+
+ '@oozcitak/dom@1.15.6':
+ resolution: {integrity: sha512-k4uEIa6DI3FCrFJMGq/05U/59WnS9DjME0kaPqBRCJAqBTkmopbYV1Xs4qFKbDJ/9wOg8W97p+1E0heng/LH7g==}
+ engines: {node: '>=8.0'}
+
+ '@oozcitak/infra@1.0.3':
+ resolution: {integrity: sha512-9O2wxXGnRzy76O1XUxESxDGsXT5kzETJPvYbreO4mv6bqe1+YSuux2cZTagjJ/T4UfEwFJz5ixanOqB0QgYAag==}
+ engines: {node: '>=6.0'}
+
+ '@oozcitak/infra@1.0.5':
+ resolution: {integrity: sha512-o+zZH7M6l5e3FaAWy3ojaPIVN5eusaYPrKm6MZQt0DKNdgXa2wDYExjpP0t/zx+GoQgQKzLu7cfD8rHCLt8JrQ==}
+ engines: {node: '>=6.0'}
+
+ '@oozcitak/url@1.0.0':
+ resolution: {integrity: sha512-LGrMeSxeLzsdaitxq3ZmBRVOrlRRQIgNNci6L0VRnOKlJFuRIkNm4B+BObXPCJA6JT5bEJtrrwjn30jueHJYZQ==}
+ engines: {node: '>=8.0'}
+
+ '@oozcitak/util@1.0.1':
+ resolution: {integrity: sha512-dFwFqcKrQnJ2SapOmRD1nQWEZUtbtIy9Y6TyJquzsalWNJsKIPxmTI0KG6Ypyl8j7v89L2wixH9fQDNrF78hKg==}
+ engines: {node: '>=6.0'}
+
+ '@oozcitak/util@1.0.2':
+ resolution: {integrity: sha512-4n8B1cWlJleSOSba5gxsMcN4tO8KkkcvXhNWW+ADqvq9Xj+Lrl9uCa90GRpjekqQJyt84aUX015DG81LFpZYXA==}
+ engines: {node: '>=6.0'}
+
+ '@oozcitak/util@8.0.0':
+ resolution: {integrity: sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==}
+ engines: {node: '>=6.0'}
+
+ '@oozcitak/util@8.3.3':
+ resolution: {integrity: sha512-Ufpab7G5PfnEhQyy5kDg9C8ltWJjsVT1P/IYqacjstaqydG4Q21HAT2HUZQYBrC/a1ZLKCz87pfydlDvv8y97w==}
+ engines: {node: '>=6.0'}
+
+ '@oozcitak/util@8.3.4':
+ resolution: {integrity: sha512-6gH/bLQJSJEg7OEpkH4wGQdA8KXHRbzL1YkGyUO12YNAgV3jxKy4K9kvfXj4+9T0OLug5k58cnPCKSSIKzp7pg==}
+ engines: {node: '>=8.0'}
+
'@openrouter/ai-sdk-provider@1.5.4':
resolution: {integrity: sha512-xrSQPUIH8n9zuyYZR0XK7Ba0h2KsjJcMkxnwaYfmv13pKs3sDkjPzVPPhlhzqBGddHb5cFEwJ9VFuFeDcxCDSw==}
engines: {node: '>=18'}
@@ -2516,6 +2562,17 @@ packages:
'@react-pdf/types@2.9.2':
resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==}
+ '@reduxjs/toolkit@2.11.2':
+ resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==}
+ peerDependencies:
+ react: ^16.9.0 || ^17.0.0 || ^18 || ^19
+ react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-redux:
+ optional: true
+
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
@@ -2876,6 +2933,9 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+ '@standard-schema/utils@0.3.0':
+ resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
'@swc/helpers@0.5.18':
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
@@ -3775,6 +3835,9 @@ packages:
brotli@1.3.3:
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
+ browser-split@0.0.1:
+ resolution: {integrity: sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow==}
+
browserify-zlib@0.2.0:
resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==}
@@ -3836,6 +3899,9 @@ packages:
camel-case@4.1.2:
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
+ camelize@1.0.1:
+ resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==}
+
caniuse-lite@1.0.30001761:
resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==}
@@ -4263,6 +4329,9 @@ packages:
supports-color:
optional: true
+ decimal.js-light@2.5.1:
+ resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+
decode-named-character-reference@1.2.0:
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
@@ -4331,12 +4400,24 @@ packages:
dir-compare@4.2.0:
resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==}
+ dom-serializer@0.2.2:
+ resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==}
+
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+ dom-walk@0.1.2:
+ resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==}
+
+ domelementtype@1.3.1:
+ resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==}
+
domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+ domhandler@2.4.2:
+ resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==}
+
domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
@@ -4344,6 +4425,9 @@ packages:
dompurify@3.3.1:
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
+ domutils@1.7.0:
+ resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==}
+
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@@ -4428,6 +4512,16 @@ packages:
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
engines: {node: '>=10.13.0'}
+ ent@2.2.2:
+ resolution: {integrity: sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==}
+ engines: {node: '>= 0.4'}
+
+ entities@1.1.2:
+ resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==}
+
+ entities@2.2.0:
+ resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
+
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@@ -4446,6 +4540,9 @@ packages:
error-ex@1.3.4:
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
+ error@4.4.0:
+ resolution: {integrity: sha512-SNDKualLUtT4StGFP7xNfuFybL2f6iJujFtrWuvJqGbVQGaN+adE23veqzPz1hjUjTunLi2EnJ+0SJxtbJreKw==}
+
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
@@ -4465,6 +4562,9 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
+ es-toolkit@1.45.1:
+ resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==}
+
es6-error@4.1.1:
resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==}
@@ -4565,6 +4665,9 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
+ ev-store@7.0.0:
+ resolution: {integrity: sha512-otazchNRnGzp2YarBJ+GXKVGvhxVATB1zmaStxJBYet0Dyq7A9VhH8IUEB/gRcL6Ch52lfpgPTRJ2m49epyMsQ==}
+
event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
@@ -4907,6 +5010,9 @@ packages:
resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==}
engines: {node: '>=10'}
+ global@4.4.0:
+ resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
+
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
@@ -5050,12 +5156,24 @@ packages:
hsl-to-rgb-for-reals@1.1.1:
resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==}
+ html-entities@2.6.0:
+ resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
+
+ html-to-docx@1.8.0:
+ resolution: {integrity: sha512-IiMBWIqXM4+cEsW//RKoonWV7DlXAJBmmKI73XJSVWTIXjGUaxSr2ck1jqzVRZknpvO8xsFnVicldKVAWrBYBA==}
+
+ html-to-vdom@0.7.0:
+ resolution: {integrity: sha512-k+d2qNkbx0JO00KezQsNcn6k2I/xSBP4yXYFLvXbcasTTDh+RDLUJS3puxqyNnpdyXWRHFGoKU7cRmby8/APcQ==}
+
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+ htmlparser2@3.10.1:
+ resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==}
+
http-cache-semantics@4.2.0:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
@@ -5113,9 +5231,23 @@ packages:
engines: {node: '>=6.9.0'}
hasBin: true
+ image-size@1.2.1:
+ resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==}
+ engines: {node: '>=16.x'}
+ hasBin: true
+
+ image-to-base64@2.2.0:
+ resolution: {integrity: sha512-Z+aMwm/91UOQqHhrz7Upre2ytKhWejZlWV/JxUTD1sT7GWWKFDJUEV5scVQKnkzSgPHFuQBUEWcanO+ma0PSVw==}
+
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
+ immer@10.2.0:
+ resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
+
+ immer@11.1.4:
+ resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==}
+
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@@ -5132,6 +5264,9 @@ packages:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
+ individual@3.0.0:
+ resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==}
+
infer-owner@1.0.4:
resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==}
@@ -5232,6 +5367,9 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
+ is-object@1.0.2:
+ resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==}
+
is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
@@ -5242,6 +5380,10 @@ packages:
is-property@1.0.2:
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
+ is-regex@1.2.1:
+ resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
+ engines: {node: '>= 0.4'}
+
is-stream@1.1.0:
resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
engines: {node: '>=0.10.0'}
@@ -5836,6 +5978,9 @@ packages:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
+ min-document@2.19.2:
+ resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==}
+
minimatch@10.1.1:
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
engines: {node: 20 || >=22}
@@ -5965,6 +6110,9 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
+ next-tick@0.2.2:
+ resolution: {integrity: sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==}
+
nice-try@1.0.5:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
@@ -6425,6 +6573,9 @@ packages:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
+ punycode@1.4.1:
+ resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -6484,6 +6635,18 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+ react-redux@9.2.0:
+ resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
+ peerDependencies:
+ '@types/react': ^18.2.25 || ^19
+ react: ^18.0 || ^19
+ redux: ^5.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ redux:
+ optional: true
+
react-refresh@0.18.0:
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
engines: {node: '>=0.10.0'}
@@ -6549,10 +6712,26 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
+ recharts@3.8.1:
+ resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
rechoir@0.8.0:
resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==}
engines: {node: '>= 10.13.0'}
+ redux-thunk@3.1.0:
+ resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
+ peerDependencies:
+ redux: ^5.0.0
+
+ redux@5.0.1:
+ resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
+
regex-recursion@6.0.2:
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
@@ -6625,6 +6804,9 @@ packages:
resolution: {integrity: sha512-oTeemxwoMuxxTYxXUwjkrOPfngTQehlv0/HoYFNkB4uzsP1Un1A9nI8JQKGOFkxpqkC7qkMs0lUsGrvUlbLNUA==}
engines: {node: '>=14', npm: '>=7'}
+ reselect@5.1.1:
+ resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
+
resolve-alpn@1.2.1:
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
@@ -6713,6 +6895,10 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+ safe-regex-test@1.1.0:
+ resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
+ engines: {node: '>= 0.4'}
+
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -6908,6 +7094,9 @@ packages:
peerDependencies:
react: ^18.0.0 || ^19.0.0
+ string-template@0.2.1:
+ resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==}
+
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -7026,6 +7215,9 @@ packages:
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
+ tiny-invariant@1.3.3:
+ resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+
tinyexec@1.0.2:
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
engines: {node: '>=18'}
@@ -7297,6 +7489,12 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+ victory-vendor@37.3.6:
+ resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
+
+ virtual-dom@2.1.1:
+ resolution: {integrity: sha512-wb6Qc9Lbqug0kRqo/iuApfBpJJAq14Sk1faAnSmtqXiwahg7PVTvWMs9L02Z8nNIMqbwsxzBAA90bbtRLbw0zg==}
+
vite-compatible-readable-stream@3.6.1:
resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==}
engines: {node: '>= 6'}
@@ -7446,11 +7644,21 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ x-is-array@0.1.0:
+ resolution: {integrity: sha512-goHPif61oNrr0jJgsXRfc8oqtYzvfiMJpTqwE7Z4y9uH+T3UozkGqQ4d2nX9mB9khvA8U2o/UbPOFjgC7hLWIA==}
+
+ x-is-string@0.1.0:
+ resolution: {integrity: sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w==}
+
xlsx@0.18.5:
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
engines: {node: '>=0.8'}
hasBin: true
+ xmlbuilder2@2.1.2:
+ resolution: {integrity: sha512-PI710tmtVlQ5VmwzbRTuhmVhKnj9pM8Si+iOZCV2g2SNo3gCrpzR2Ka9wNzZtqfD+mnP+xkrqoNy0sjKZqP4Dg==}
+ engines: {node: '>=8.0'}
+
xmlbuilder@10.1.1:
resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==}
engines: {node: '>=4.0'}
@@ -9230,6 +9438,41 @@ snapshots:
dependencies:
'@octokit/openapi-types': 12.11.0
+ '@oozcitak/dom@1.15.5':
+ dependencies:
+ '@oozcitak/infra': 1.0.5
+ '@oozcitak/url': 1.0.0
+ '@oozcitak/util': 8.0.0
+
+ '@oozcitak/dom@1.15.6':
+ dependencies:
+ '@oozcitak/infra': 1.0.5
+ '@oozcitak/url': 1.0.0
+ '@oozcitak/util': 8.3.4
+
+ '@oozcitak/infra@1.0.3':
+ dependencies:
+ '@oozcitak/util': 1.0.1
+
+ '@oozcitak/infra@1.0.5':
+ dependencies:
+ '@oozcitak/util': 8.0.0
+
+ '@oozcitak/url@1.0.0':
+ dependencies:
+ '@oozcitak/infra': 1.0.3
+ '@oozcitak/util': 1.0.2
+
+ '@oozcitak/util@1.0.1': {}
+
+ '@oozcitak/util@1.0.2': {}
+
+ '@oozcitak/util@8.0.0': {}
+
+ '@oozcitak/util@8.3.3': {}
+
+ '@oozcitak/util@8.3.4': {}
+
'@openrouter/ai-sdk-provider@1.5.4(ai@5.0.151(zod@4.2.1))(zod@4.2.1)':
dependencies:
'@openrouter/sdk': 0.1.27
@@ -10259,6 +10502,18 @@ snapshots:
'@react-pdf/primitives': 4.1.1
'@react-pdf/stylesheet': 6.1.2
+ '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3)':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@standard-schema/utils': 0.3.0
+ immer: 11.1.4
+ redux: 5.0.1
+ redux-thunk: 3.1.0(redux@5.0.1)
+ reselect: 5.1.1
+ optionalDependencies:
+ react: 19.2.3
+ react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1)
+
'@remirror/core-constants@3.0.0': {}
'@rolldown/pluginutils@1.0.0-beta.53': {}
@@ -10704,6 +10959,8 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
+ '@standard-schema/utils@0.3.0': {}
+
'@swc/helpers@0.5.18':
dependencies:
tslib: 2.8.1
@@ -11729,6 +11986,8 @@ snapshots:
dependencies:
base64-js: 1.5.1
+ browser-split@0.0.1: {}
+
browserify-zlib@0.2.0:
dependencies:
pako: 1.0.11
@@ -11823,6 +12082,8 @@ snapshots:
pascal-case: 3.1.2
tslib: 2.8.1
+ camelize@1.0.1: {}
+
caniuse-lite@1.0.30001761: {}
ccount@2.0.1: {}
@@ -12243,6 +12504,8 @@ snapshots:
dependencies:
ms: 2.1.3
+ decimal.js-light@2.5.1: {}
+
decode-named-character-reference@1.2.0:
dependencies:
character-entities: 2.0.2
@@ -12306,14 +12569,27 @@ snapshots:
minimatch: 3.1.2
p-limit: 3.1.0
+ dom-serializer@0.2.2:
+ dependencies:
+ domelementtype: 2.3.0
+ entities: 2.2.0
+
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
+ dom-walk@0.1.2: {}
+
+ domelementtype@1.3.1: {}
+
domelementtype@2.3.0: {}
+ domhandler@2.4.2:
+ dependencies:
+ domelementtype: 1.3.1
+
domhandler@5.0.3:
dependencies:
domelementtype: 2.3.0
@@ -12322,6 +12598,11 @@ snapshots:
optionalDependencies:
'@types/trusted-types': 2.0.7
+ domutils@1.7.0:
+ dependencies:
+ dom-serializer: 0.2.2
+ domelementtype: 1.3.1
+
domutils@3.2.2:
dependencies:
dom-serializer: 2.0.0
@@ -12462,6 +12743,17 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
+ ent@2.2.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ punycode: 1.4.1
+ safe-regex-test: 1.1.0
+
+ entities@1.1.2: {}
+
+ entities@2.2.0: {}
+
entities@4.5.0: {}
entities@6.0.1: {}
@@ -12474,6 +12766,12 @@ snapshots:
dependencies:
is-arrayish: 0.2.1
+ error@4.4.0:
+ dependencies:
+ camelize: 1.0.1
+ string-template: 0.2.1
+ xtend: 4.0.2
+
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
@@ -12491,6 +12789,8 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.2
+ es-toolkit@1.45.1: {}
+
es6-error@4.1.1:
optional: true
@@ -12655,6 +12955,10 @@ snapshots:
etag@1.8.1: {}
+ ev-store@7.0.0:
+ dependencies:
+ individual: 3.0.0
+
event-target-shim@5.0.1: {}
eventemitter3@5.0.1: {}
@@ -13098,6 +13402,11 @@ snapshots:
dependencies:
ini: 2.0.0
+ global@4.4.0:
+ dependencies:
+ min-document: 2.19.2
+ process: 0.11.10
+
globals@14.0.0: {}
globals@16.5.0: {}
@@ -13344,10 +13653,44 @@ snapshots:
hsl-to-rgb-for-reals@1.1.1: {}
+ html-entities@2.6.0: {}
+
+ html-to-docx@1.8.0(encoding@0.1.13):
+ dependencies:
+ '@oozcitak/dom': 1.15.6
+ '@oozcitak/util': 8.3.4
+ color-name: 1.1.4
+ html-entities: 2.6.0
+ html-to-vdom: 0.7.0
+ image-size: 1.2.1
+ image-to-base64: 2.2.0(encoding@0.1.13)
+ jszip: 3.10.1
+ lodash: 4.17.21
+ mime-types: 2.1.35
+ nanoid: 3.3.11
+ virtual-dom: 2.1.1
+ xmlbuilder2: 2.1.2
+ transitivePeerDependencies:
+ - encoding
+
+ html-to-vdom@0.7.0:
+ dependencies:
+ ent: 2.2.2
+ htmlparser2: 3.10.1
+
html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}
+ htmlparser2@3.10.1:
+ dependencies:
+ domelementtype: 1.3.1
+ domhandler: 2.4.2
+ domutils: 1.7.0
+ entities: 1.1.2
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+
http-cache-semantics@4.2.0: {}
http-errors@2.0.1:
@@ -13412,8 +13755,22 @@ snapshots:
image-size@0.7.5:
optional: true
+ image-size@1.2.1:
+ dependencies:
+ queue: 6.0.2
+
+ image-to-base64@2.2.0(encoding@0.1.13):
+ dependencies:
+ node-fetch: 2.7.0(encoding@0.1.13)
+ transitivePeerDependencies:
+ - encoding
+
immediate@3.0.6: {}
+ immer@10.2.0: {}
+
+ immer@11.1.4: {}
+
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
@@ -13426,6 +13783,8 @@ snapshots:
indent-string@4.0.0: {}
+ individual@3.0.0: {}
+
infer-owner@1.0.4: {}
inflight@1.0.6:
@@ -13500,6 +13859,8 @@ snapshots:
is-number@7.0.0: {}
+ is-object@1.0.2: {}
+
is-plain-obj@4.1.0: {}
is-promise@4.0.0: {}
@@ -13507,6 +13868,13 @@ snapshots:
is-property@1.0.2:
optional: true
+ is-regex@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
is-stream@1.1.0: {}
is-stream@2.0.1: {}
@@ -14352,6 +14720,10 @@ snapshots:
mimic-response@3.1.0: {}
+ min-document@2.19.2:
+ dependencies:
+ dom-walk: 0.1.2
+
minimatch@10.1.1:
dependencies:
'@isaacs/brace-expansion': 5.0.0
@@ -14467,6 +14839,8 @@ snapshots:
neo-async@2.6.2: {}
+ next-tick@0.2.2: {}
+
nice-try@1.0.5: {}
no-case@3.0.4:
@@ -14955,6 +15329,8 @@ snapshots:
punycode.js@2.3.1: {}
+ punycode@1.4.1: {}
+
punycode@2.3.1: {}
pusher-js@8.4.0:
@@ -15064,6 +15440,15 @@ snapshots:
react-is@16.13.1: {}
+ react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1):
+ dependencies:
+ '@types/use-sync-external-store': 0.0.6
+ react: 19.2.3
+ use-sync-external-store: 1.6.0(react@19.2.3)
+ optionalDependencies:
+ '@types/react': 19.2.7
+ redux: 5.0.1
+
react-refresh@0.18.0: {}
react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.3):
@@ -15138,10 +15523,36 @@ snapshots:
readdirp@4.1.2: {}
+ recharts@3.8.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1):
+ dependencies:
+ '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3)
+ clsx: 2.1.1
+ decimal.js-light: 2.5.1
+ es-toolkit: 1.45.1
+ eventemitter3: 5.0.1
+ immer: 10.2.0
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ react-is: 16.13.1
+ react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1)
+ reselect: 5.1.1
+ tiny-invariant: 1.3.3
+ use-sync-external-store: 1.6.0(react@19.2.3)
+ victory-vendor: 37.3.6
+ transitivePeerDependencies:
+ - '@types/react'
+ - redux
+
rechoir@0.8.0:
dependencies:
resolve: 1.22.11
+ redux-thunk@3.1.0(redux@5.0.1):
+ dependencies:
+ redux: 5.0.1
+
+ redux@5.0.1: {}
+
regex-recursion@6.0.2:
dependencies:
regex-utilities: 2.3.0
@@ -15248,6 +15659,8 @@ snapshots:
dependencies:
pe-library: 1.0.1
+ reselect@5.1.1: {}
+
resolve-alpn@1.2.1: {}
resolve-from@4.0.0: {}
@@ -15366,6 +15779,12 @@ snapshots:
safe-buffer@5.2.1: {}
+ safe-regex-test@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-regex: 1.2.1
+
safer-buffer@2.1.2: {}
scheduler@0.25.0-rc-603e6108-20241029: {}
@@ -15616,6 +16035,8 @@ snapshots:
- micromark-util-types
- supports-color
+ string-template@0.2.1: {}
+
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
@@ -15733,6 +16154,8 @@ snapshots:
tiny-inflate@1.0.3: {}
+ tiny-invariant@1.3.3: {}
+
tinyexec@1.0.2: {}
tinyglobby@0.2.15:
@@ -16004,6 +16427,34 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
+ victory-vendor@37.3.6:
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/d3-ease': 3.0.2
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-scale': 4.0.9
+ '@types/d3-shape': 3.1.7
+ '@types/d3-time': 3.0.4
+ '@types/d3-timer': 3.0.2
+ d3-array: 3.2.4
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-scale: 4.0.2
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-timer: 3.0.1
+
+ virtual-dom@2.1.1:
+ dependencies:
+ browser-split: 0.0.1
+ error: 4.4.0
+ ev-store: 7.0.0
+ global: 4.4.0
+ is-object: 1.0.2
+ next-tick: 0.2.2
+ x-is-array: 0.1.0
+ x-is-string: 0.1.0
+
vite-compatible-readable-stream@3.6.1:
dependencies:
inherits: 2.0.4
@@ -16155,6 +16606,10 @@ snapshots:
wrappy@1.0.2: {}
+ x-is-array@0.1.0: {}
+
+ x-is-string@0.1.0: {}
+
xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
@@ -16165,12 +16620,17 @@ snapshots:
wmf: 1.0.2
word: 0.3.0
+ xmlbuilder2@2.1.2:
+ dependencies:
+ '@oozcitak/dom': 1.15.5
+ '@oozcitak/infra': 1.0.5
+ '@oozcitak/util': 8.3.3
+
xmlbuilder@10.1.1: {}
xmlbuilder@15.1.1: {}
- xtend@4.0.2:
- optional: true
+ xtend@4.0.2: {}
y18n@5.0.8: {}
From 61e92783b2f7b2d8b2d551a2dfc9471d56d52632 Mon Sep 17 00:00:00 2001
From: Arjun <6592213+arkml@users.noreply.github.com>
Date: Mon, 30 Mar 2026 22:43:48 +0530
Subject: [PATCH 7/9] fix nested lists save
---
.../src/components/markdown-editor.tsx | 65 ++++++++++---------
1 file changed, 35 insertions(+), 30 deletions(-)
diff --git a/apps/x/apps/renderer/src/components/markdown-editor.tsx b/apps/x/apps/renderer/src/components/markdown-editor.tsx
index f3ccba2d..d7920b8b 100644
--- a/apps/x/apps/renderer/src/components/markdown-editor.tsx
+++ b/apps/x/apps/renderer/src/components/markdown-editor.tsx
@@ -109,39 +109,44 @@ function getMarkdownWithBlankLines(editor: Editor): string {
const level = (node.attrs?.level as number) || 1
const text = nodeToText(node)
blocks.push('#'.repeat(level) + ' ' + text)
- } else if (node.type === 'bulletList' || node.type === 'orderedList') {
- // Handle lists - all items are part of one block
- const listLines: string[] = []
- const listItems = (node.content || []) as Array<{ content?: Array; attrs?: Record }>
- listItems.forEach((item, index) => {
- const prefix = node.type === 'orderedList' ? `${index + 1}. ` : '- '
- const itemContent = (item.content || []) as Array<{ type?: string; content?: Array<{ type?: string; text?: string; marks?: Array<{ type: string; attrs?: Record }> }>; attrs?: Record }>
- itemContent.forEach((para: { type?: string; content?: Array<{ type?: string; text?: string; marks?: Array<{ type: string; attrs?: Record }> }>; attrs?: Record }, paraIndex: number) => {
- const text = nodeToText(para)
- if (paraIndex === 0) {
- listLines.push(prefix + text)
+ } else if (node.type === 'bulletList' || node.type === 'orderedList' || node.type === 'taskList') {
+ // Recursively serialize lists to handle nested bullets
+ const serializeList = (
+ listNode: { type?: string; content?: Array>; attrs?: Record },
+ indent: number
+ ): string[] => {
+ const lines: string[] = []
+ const items = (listNode.content || []) as Array<{ content?: Array>; attrs?: Record }>
+ items.forEach((item, index) => {
+ const indentStr = ' '.repeat(indent)
+ let prefix: string
+ if (listNode.type === 'taskList') {
+ const checked = item.attrs?.checked ? 'x' : ' '
+ prefix = `- [${checked}] `
+ } else if (listNode.type === 'orderedList') {
+ prefix = `${index + 1}. `
} else {
- listLines.push(' ' + text)
+ prefix = '- '
}
+ const itemContent = (item.content || []) as Array<{ type?: string; content?: Array<{ type?: string; text?: string; marks?: Array<{ type: string; attrs?: Record }> }>; attrs?: Record }>
+ let firstPara = true
+ itemContent.forEach(child => {
+ if (child.type === 'bulletList' || child.type === 'orderedList' || child.type === 'taskList') {
+ lines.push(...serializeList(child, indent + 1))
+ } else {
+ const text = nodeToText(child)
+ if (firstPara) {
+ lines.push(indentStr + prefix + text)
+ firstPara = false
+ } else {
+ lines.push(indentStr + ' ' + text)
+ }
+ }
+ })
})
- })
- blocks.push(listLines.join('\n'))
- } else if (node.type === 'taskList') {
- const listLines: string[] = []
- const listItems = (node.content || []) as Array<{ content?: Array; attrs?: Record }>
- listItems.forEach(item => {
- const checked = item.attrs?.checked ? 'x' : ' '
- const itemContent = (item.content || []) as Array<{ type?: string; content?: Array<{ type?: string; text?: string; marks?: Array<{ type: string; attrs?: Record }> }>; attrs?: Record }>
- itemContent.forEach((para: { type?: string; content?: Array<{ type?: string; text?: string; marks?: Array<{ type: string; attrs?: Record }> }>; attrs?: Record }, paraIndex: number) => {
- const text = nodeToText(para)
- if (paraIndex === 0) {
- listLines.push(`- [${checked}] ${text}`)
- } else {
- listLines.push(' ' + text)
- }
- })
- })
- blocks.push(listLines.join('\n'))
+ return lines
+ }
+ blocks.push(serializeList(node, 0).join('\n'))
} else if (node.type === 'taskBlock') {
blocks.push('```task\n' + (node.attrs?.data as string || '{}') + '\n```')
} else if (node.type === 'imageBlock') {
From 1c5e5afda8a6a8198562dc0bba0cbbf4617e98e8 Mon Sep 17 00:00:00 2001
From: Ramnique Singh <30795890+ramnique@users.noreply.github.com>
Date: Tue, 31 Mar 2026 14:50:23 +0530
Subject: [PATCH 8/9] fix oauth callback params propagation
---
apps/x/apps/main/src/auth-server.ts | 4 ++--
apps/x/apps/main/src/composio-handler.ts | 2 +-
apps/x/apps/main/src/oauth-handler.ts | 6 +++---
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/apps/x/apps/main/src/auth-server.ts b/apps/x/apps/main/src/auth-server.ts
index b0b890c0..78e519d0 100644
--- a/apps/x/apps/main/src/auth-server.ts
+++ b/apps/x/apps/main/src/auth-server.ts
@@ -25,7 +25,7 @@ export interface AuthServerResult {
*/
export function createAuthServer(
port: number = DEFAULT_PORT,
- onCallback: (code: string, state: string) => void | Promise
+ onCallback: (params: Record) => void | Promise
): Promise {
return new Promise((resolve, reject) => {
const server = createServer((req, res) => {
@@ -67,7 +67,7 @@ export function createAuthServer(
// Handle callback - either traditional OAuth with code/state or Composio-style notification
// Composio callbacks may not have code/state, just a notification that the flow completed
- onCallback(code || '', state || '');
+ onCallback(Object.fromEntries(url.searchParams.entries()));
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
diff --git a/apps/x/apps/main/src/composio-handler.ts b/apps/x/apps/main/src/composio-handler.ts
index e5b25d1a..452a76e3 100644
--- a/apps/x/apps/main/src/composio-handler.ts
+++ b/apps/x/apps/main/src/composio-handler.ts
@@ -143,7 +143,7 @@ export async function initiateConnection(toolkitSlug: string): Promise<{
// Set up callback server
let cleanupTimeout: NodeJS.Timeout;
- const { server } = await createAuthServer(8081, async (_code, _state) => {
+ const { server } = await createAuthServer(8081, async () => {
// OAuth callback received - sync the account status
try {
const accountStatus = await composioClient.getConnectedAccount(connectedAccountId);
diff --git a/apps/x/apps/main/src/oauth-handler.ts b/apps/x/apps/main/src/oauth-handler.ts
index 2efc77c2..bf9c77ff 100644
--- a/apps/x/apps/main/src/oauth-handler.ts
+++ b/apps/x/apps/main/src/oauth-handler.ts
@@ -186,9 +186,9 @@ export async function connectProvider(provider: string, clientId?: string): Prom
});
// Create callback server
- const { server } = await createAuthServer(8080, async (code, receivedState) => {
+ const { server } = await createAuthServer(8080, async (params: Record) => {
// Validate state
- if (receivedState !== state) {
+ if (params.state !== state) {
throw new Error('Invalid state parameter - possible CSRF attack');
}
@@ -199,7 +199,7 @@ export async function connectProvider(provider: string, clientId?: string): Prom
try {
// Build callback URL for token exchange
- const callbackUrl = new URL(`${REDIRECT_URI}?code=${code}&state=${receivedState}`);
+ const callbackUrl = new URL(`${REDIRECT_URI}?${new URLSearchParams(params).toString()}`);
// Exchange code for tokens
console.log(`[OAuth] Exchanging authorization code for tokens (${provider})...`);
From 903fecc5f538ddf0cd2771201711e1d4807e124c Mon Sep 17 00:00:00 2001
From: arkml <6592213+arkml@users.noreply.github.com>
Date: Tue, 31 Mar 2026 16:07:41 +0530
Subject: [PATCH 9/9] Daily5 (#457)
* better cal and email block design
* modified email block and draft with copilot
---
.../src/extensions/calendar-block.tsx | 26 +-
.../renderer/src/extensions/email-block.tsx | 345 ++++------
apps/x/apps/renderer/src/styles/editor.css | 628 +++++++-----------
apps/x/packages/shared/src/blocks.ts | 1 +
4 files changed, 366 insertions(+), 634 deletions(-)
diff --git a/apps/x/apps/renderer/src/extensions/calendar-block.tsx b/apps/x/apps/renderer/src/extensions/calendar-block.tsx
index f72dc5d4..9f0eec02 100644
--- a/apps/x/apps/renderer/src/extensions/calendar-block.tsx
+++ b/apps/x/apps/renderer/src/extensions/calendar-block.tsx
@@ -9,12 +9,15 @@ function formatTime(dateStr: string): string {
return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
}
-function getDateParts(dateStr: string): { day: number; month: string; weekday: string } {
+function getDateParts(dateStr: string): { day: number; month: string; weekday: string; isToday: boolean } {
const d = new Date(dateStr)
+ const now = new Date()
+ const isToday = d.getDate() === now.getDate() && d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear()
return {
day: d.getDate(),
- month: d.toLocaleDateString([], { month: 'long' }),
- weekday: d.toLocaleDateString([], { weekday: 'short' }),
+ month: d.toLocaleDateString([], { month: 'short' }).toUpperCase(),
+ weekday: d.toLocaleDateString([], { weekday: 'short' }).toUpperCase(),
+ isToday,
}
}
@@ -62,7 +65,8 @@ interface ResolvedEvent {
conferenceLink?: string
}
-const EVENT_BAR_COLOR = '#7ec8c8'
+const GCAL_EVENT_COLOR = '#039be5'
+const GCAL_TODAY_COLOR = '#1a73e8'
function JoinMeetingSplitButton({ onJoinAndNotes, onNotesOnly }: {
onJoinAndNotes: () => void
@@ -273,11 +277,8 @@ function CalendarBlockView({ node, deleteNode }: { node: { attrs: Record
{parts ? (
<>
- {parts.day}
-
- {parts.month}
- {parts.weekday}
-
+ {parts.weekday}
+ {parts.day}
>
) : (
?
@@ -288,16 +289,13 @@ function CalendarBlockView({ node, deleteNode }: { node: { attrs: Record e.stopPropagation()}
onClick={(e) => { e.stopPropagation(); handleEventClick(event) }}
>
-
- {event.summary || 'Untitled event'}
+ {event.summary || '(No title)'}
{getTimeRange(event)}
diff --git a/apps/x/apps/renderer/src/extensions/email-block.tsx b/apps/x/apps/renderer/src/extensions/email-block.tsx
index 9be8c72c..7356c94c 100644
--- a/apps/x/apps/renderer/src/extensions/email-block.tsx
+++ b/apps/x/apps/renderer/src/extensions/email-block.tsx
@@ -1,8 +1,9 @@
import { mergeAttributes, Node } from '@tiptap/react'
import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react'
-import { X, Mail, ChevronDown, ExternalLink, Copy, Check, Sparkles, Loader2, MessageSquare } from 'lucide-react'
+import { X, Mail, ChevronDown, ExternalLink, Copy, Check, MessageSquare } from 'lucide-react'
import { blocks } from '@x/shared'
import { useState, useEffect, useRef, useCallback } from 'react'
+import { useTheme } from '@/contexts/theme-context'
// --- Helpers ---
@@ -17,8 +18,10 @@ function formatEmailDate(dateStr: string): string {
}
}
-function getInitials(name: string): string {
- return name.split(/\s+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase()
+/** Extract just the name part from "Name
" format */
+function senderFirstName(from: string): string {
+ const name = from.replace(/<.*>/, '').trim()
+ return name.split(/\s+/)[0] || name
}
declare global {
@@ -45,27 +48,15 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: {
const hasDraft = !!config?.draft_response
const hasPastSummary = !!config?.past_summary
- const responseMode = config?.response_mode || 'both'
+
+ const { resolvedTheme } = useTheme()
// Local draft state for editing
const [draftBody, setDraftBody] = useState(config?.draft_response || '')
- const [contextExpanded, setContextExpanded] = useState(false)
+ const [emailExpanded, setEmailExpanded] = useState(false)
const [copied, setCopied] = useState(false)
- const [generating, setGenerating] = useState(false)
- const [responseSplitOpen, setResponseSplitOpen] = useState(false)
- const responseSplitRef = useRef(null)
const bodyRef = useRef(null)
- // Close split dropdown on outside click
- useEffect(() => {
- if (!responseSplitOpen) return
- const handler = (e: MouseEvent) => {
- if (responseSplitRef.current && !responseSplitRef.current.contains(e.target as globalThis.Node)) setResponseSplitOpen(false)
- }
- document.addEventListener('mousedown', handler)
- return () => document.removeEventListener('mousedown', handler)
- }, [responseSplitOpen])
-
// Sync draft from external changes
useEffect(() => {
try {
@@ -89,53 +80,23 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: {
} catch { /* ignore */ }
}, [raw, updateAttributes])
- const generateResponse = useCallback(async () => {
- if (!config || generating) return
- setGenerating(true)
- try {
- const ipc = (window as unknown as { ipc: { invoke: (channel: string, args: Record) => Promise<{ response?: string }> } }).ipc
- // Build context for the agent
- let noteContent = `# Email: ${config.subject || 'No subject'}\n\n`
- noteContent += `**From:** ${config.from || 'Unknown'}\n`
- noteContent += `**Date:** ${config.date || 'Unknown'}\n\n`
- noteContent += `## Latest email\n\n${config.latest_email}\n\n`
- if (config.past_summary) {
- noteContent += `## Earlier conversation summary\n\n${config.past_summary}\n\n`
- }
-
- const result = await ipc.invoke('inline-task:process', {
- instruction: `Draft a concise, professional response to this email. Return only the email body text, no subject line or headers.`,
- noteContent,
- notePath: '',
- })
-
- if (result.response) {
- // Clean up the response — strip any markdown headers the agent may add
- const cleaned = result.response.replace(/^#+\s+.*\n*/gm, '').trim()
- setDraftBody(cleaned)
- // Update the block data to include the draft
- const current = JSON.parse(raw) as Record
- updateAttributes({ data: JSON.stringify({ ...current, draft_response: cleaned }) })
- }
- } catch (err) {
- console.error('[email-block] Failed to generate response:', err)
- } finally {
- setGenerating(false)
- }
- }, [config, generating, raw, updateAttributes])
-
const draftWithAssistant = useCallback(() => {
if (!config) return
- let prompt = `Help me draft a response to this email`
+ let prompt = draftBody
+ ? `Help me refine this draft response to an email`
+ : `Help me draft a response to this email`
if (config.threadId) {
prompt += `. Read the full thread at gmail_sync/${config.threadId}.md for context`
}
prompt += `.\n\n`
prompt += `**From:** ${config.from || 'Unknown'}\n`
prompt += `**Subject:** ${config.subject || 'No subject'}\n`
+ if (draftBody) {
+ prompt += `\n**Current draft:**\n${draftBody}\n`
+ }
window.__pendingEmailDraft = { prompt }
window.dispatchEvent(new Event('email-block:draft-with-assistant'))
- }, [config])
+ }, [config, draftBody])
if (!config) {
return (
@@ -152,185 +113,112 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: {
? `https://mail.google.com/mail/u/0/#all/${config.threadId}`
: null
- // --- Render: Draft mode (draft_response present) ---
- if (hasDraft) {
- return (
-
- e.stopPropagation()}>
-
- {/* Draft header */}
- {config.to && (
-
-
- To
- {config.to}
-
- {config.subject && (
-
-
Subject
-
{config.subject}
+ // Build summary: use explicit summary, or auto-generate from sender + subject
+ const summary = config.summary
+ || (config.from && config.subject
+ ? `${senderFirstName(config.from)} reached out about ${config.subject}`
+ : config.subject || 'New email')
+
+ return (
+
+ e.stopPropagation()}>
+
+
+ {/* Header: Email badge */}
+
+
+ Email
+
+
+ {/* Summary */}
+
{summary}
+
+ {/* Expandable email details */}
+
+
+ {emailExpanded && (
+
+
+
+
+
+
{config.from || 'Unknown'}
+ {config.date &&
{formatEmailDate(config.date)}
}
+
+ {config.subject &&
Subject: {config.subject}
}
- )}
+
+
{config.latest_email}
- )}
- {/* Editable draft body */}
-
-
- )
- }
-
- // --- Render: Read mode (no draft_response) ---
- return (
-
- e.stopPropagation()}>
-
- {config.subject &&
{config.subject}
}
- {/* Latest email message */}
-
-
- {config.from &&
{getInitials(config.from)}
}
-
- {config.from &&
{config.from}
}
- {config.date &&
{formatEmailDate(config.date)}
}
-
-
-
{config.latest_email}
-
- {/* Action buttons */}
-
- {hasPastSummary && (
-
- )}
- {responseMode === 'inline' && (
-
- )}
- {responseMode === 'assistant' && (
-
- )}
- {responseMode === 'both' && (
-
-
-
- {responseSplitOpen && (
-
-
-
- )}
-
)}
{gmailUrl && (
- {/* Past summary context */}
- {contextExpanded && hasPastSummary && (
-
-
-
Earlier conversation
-
{config.past_summary}
-
-
- )}
)
diff --git a/apps/x/apps/renderer/src/styles/editor.css b/apps/x/apps/renderer/src/styles/editor.css
index efa481c1..f865707e 100644
--- a/apps/x/apps/renderer/src/styles/editor.css
+++ b/apps/x/apps/renderer/src/styles/editor.css
@@ -859,12 +859,13 @@
font-size: 13px;
}
-/* Calendar block */
+/* Calendar block – Google Calendar style */
.tiptap-editor .ProseMirror .calendar-block-title {
font-size: 14px;
- font-weight: 600;
- margin-bottom: 8px;
- color: var(--foreground);
+ font-weight: 500;
+ margin-bottom: 4px;
+ color: color-mix(in srgb, var(--foreground) 70%, transparent);
+ letter-spacing: 0.01em;
}
.tiptap-editor .ProseMirror .calendar-block-loading,
@@ -873,7 +874,7 @@
align-items: center;
justify-content: center;
height: 60px;
- font-size: 13px;
+ font-size: 14px;
color: color-mix(in srgb, var(--foreground) 45%, transparent);
}
@@ -897,66 +898,69 @@
.tiptap-editor .ProseMirror .calendar-block-separator {
border: none;
- border-top: 1px dashed color-mix(in srgb, var(--foreground) 20%, transparent);
- margin: 4px 0;
+ border-top: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
+ margin: 0;
}
.tiptap-editor .ProseMirror .calendar-block-date-row {
display: flex;
align-items: flex-start;
- gap: 0;
+ gap: 12px;
padding: 12px 0;
}
.tiptap-editor .ProseMirror .calendar-block-date-left {
- display: flex;
- align-items: baseline;
- gap: 6px;
- width: 140px;
- flex-shrink: 0;
- padding-top: 4px;
-}
-
-.tiptap-editor .ProseMirror .calendar-block-day {
- font-size: 28px;
- font-weight: 300;
- line-height: 1;
- color: color-mix(in srgb, var(--foreground) 70%, transparent);
-}
-
-.tiptap-editor .ProseMirror .calendar-block-month-weekday {
display: flex;
flex-direction: column;
- gap: 0;
-}
-
-.tiptap-editor .ProseMirror .calendar-block-month {
- font-size: 12px;
- color: color-mix(in srgb, var(--foreground) 50%, transparent);
- line-height: 1.3;
+ align-items: center;
+ width: 56px;
+ flex-shrink: 0;
+ padding-top: 2px;
}
.tiptap-editor .ProseMirror .calendar-block-weekday {
- font-size: 12px;
- color: color-mix(in srgb, var(--foreground) 40%, transparent);
- line-height: 1.3;
+ font-size: 11px;
+ font-weight: 500;
+ color: color-mix(in srgb, var(--foreground) 45%, transparent);
+ line-height: 1;
+ letter-spacing: 0.05em;
+ margin-bottom: 2px;
+}
+
+.tiptap-editor .ProseMirror .calendar-block-day {
+ font-size: 26px;
+ font-weight: 400;
+ line-height: 1;
+ color: color-mix(in srgb, var(--foreground) 65%, transparent);
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+}
+
+.tiptap-editor .ProseMirror .calendar-block-day-today {
+ background-color: #1a73e8;
+ color: #fff !important;
}
.tiptap-editor .ProseMirror .calendar-block-events {
display: flex;
flex-direction: column;
- gap: 12px;
+ gap: 6px;
flex: 1;
min-width: 0;
+ padding-top: 4px;
}
.tiptap-editor .ProseMirror .calendar-block-event {
display: flex;
align-items: stretch;
- gap: 10px;
- padding: 4px 8px;
- border-radius: 6px;
- transition: background-color 0.12s ease;
+ padding: 8px 12px;
+ border-radius: 4px;
+ transition: filter 0.12s ease;
+ min-height: 0;
}
.tiptap-editor .ProseMirror .calendar-block-event-clickable {
@@ -964,14 +968,7 @@
}
.tiptap-editor .ProseMirror .calendar-block-event-clickable:hover {
- background-color: color-mix(in srgb, var(--foreground) 5%, transparent);
-}
-
-.tiptap-editor .ProseMirror .calendar-block-event-bar {
- width: 3px;
- border-radius: 2px;
- flex-shrink: 0;
- min-height: 32px;
+ filter: brightness(0.9);
}
.tiptap-editor .ProseMirror .calendar-block-event-content {
@@ -984,20 +981,22 @@
.tiptap-editor .ProseMirror .calendar-block-event-title {
font-size: 14px;
font-weight: 500;
- color: var(--foreground);
+ color: #fff;
+ line-height: 1.3;
}
.tiptap-editor .ProseMirror .calendar-block-event-time {
font-size: 12px;
- color: color-mix(in srgb, var(--foreground) 45%, transparent);
+ color: rgba(255, 255, 255, 0.85);
+ line-height: 1.3;
}
.tiptap-editor .ProseMirror .calendar-block-split-btn {
position: relative;
display: inline-flex;
align-items: stretch;
- margin-top: 4px;
- border-radius: 5px;
+ margin-top: 6px;
+ border-radius: 4px;
overflow: visible;
}
@@ -1008,17 +1007,17 @@
padding: 4px 8px 4px 10px;
font-size: 12px;
font-weight: 500;
- color: #7ec8c8;
- background: color-mix(in srgb, #7ec8c8 12%, transparent);
- border: 1px solid color-mix(in srgb, #7ec8c8 25%, transparent);
+ color: #fff;
+ background: rgba(255, 255, 255, 0.2);
+ border: none;
border-right: none;
- border-radius: 5px 0 0 5px;
+ border-radius: 4px 0 0 4px;
cursor: pointer;
transition: background-color 0.12s ease;
}
.tiptap-editor .ProseMirror .calendar-block-split-main:hover {
- background: color-mix(in srgb, #7ec8c8 22%, transparent);
+ background: rgba(255, 255, 255, 0.3);
}
.tiptap-editor .ProseMirror .calendar-block-split-chevron-wrap {
@@ -1031,21 +1030,21 @@
align-items: center;
justify-content: center;
padding: 4px 6px;
- color: #7ec8c8;
- background: color-mix(in srgb, #7ec8c8 12%, transparent);
- border: 1px solid color-mix(in srgb, #7ec8c8 25%, transparent);
- border-left: 1px solid color-mix(in srgb, #7ec8c8 20%, transparent);
- border-radius: 0 5px 5px 0;
+ color: #fff;
+ background: rgba(255, 255, 255, 0.2);
+ border: none;
+ border-left: 1px solid rgba(255, 255, 255, 0.25);
+ border-radius: 0 4px 4px 0;
cursor: pointer;
transition: background-color 0.12s ease;
}
.tiptap-editor .ProseMirror .calendar-block-split-chevron:hover {
- background: color-mix(in srgb, #7ec8c8 22%, transparent);
+ background: rgba(255, 255, 255, 0.3);
}
.tiptap-editor .ProseMirror .calendar-block-split-chevron-open {
- border-radius: 0 5px 0 0;
+ border-radius: 0 4px 0 0;
border-bottom-color: transparent;
}
@@ -1054,10 +1053,11 @@
top: calc(100% - 1px);
right: 0;
z-index: 50;
- background: color-mix(in srgb, #7ec8c8 12%, transparent);
- border: 1px solid color-mix(in srgb, #7ec8c8 25%, transparent);
- border-top: none;
- border-radius: 0 0 5px 5px;
+ background: #039be5;
+ border: none;
+ border-top: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 0 0 4px 4px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.tiptap-editor .ProseMirror .calendar-block-split-option {
@@ -1068,7 +1068,7 @@
padding: 5px 10px;
font-size: 12px;
font-weight: 500;
- color: #7ec8c8;
+ color: #fff;
background: none;
border: none;
cursor: pointer;
@@ -1076,90 +1076,86 @@
}
.tiptap-editor .ProseMirror .calendar-block-split-option:hover {
- background: color-mix(in srgb, #7ec8c8 22%, transparent);
+ background: rgba(255, 255, 255, 0.15);
}
.tiptap-editor .ProseMirror .calendar-block-join-btn {
display: inline-flex;
align-items: center;
gap: 5px;
- margin-top: 4px;
+ margin-top: 6px;
padding: 4px 10px;
font-size: 12px;
font-weight: 500;
- color: #7ec8c8;
- background: color-mix(in srgb, #7ec8c8 12%, transparent);
- border: 1px solid color-mix(in srgb, #7ec8c8 25%, transparent);
- border-radius: 5px;
- cursor: pointer;
- transition: background-color 0.12s ease, border-color 0.12s ease;
- width: fit-content;
-}
-
-.tiptap-editor .ProseMirror .calendar-block-join-btn:hover {
- background: color-mix(in srgb, #7ec8c8 22%, transparent);
- border-color: color-mix(in srgb, #7ec8c8 40%, transparent);
-}
-
-/* Email block */
-.tiptap-editor .ProseMirror .email-block-subject {
- font-size: 14px;
- font-weight: 600;
- color: var(--foreground);
- margin-bottom: 8px;
-}
-
-.tiptap-editor .ProseMirror .email-block-loading,
-.tiptap-editor .ProseMirror .email-block-empty {
- display: flex;
- align-items: center;
- gap: 6px;
- height: 50px;
- justify-content: center;
- font-size: 13px;
- color: color-mix(in srgb, var(--foreground) 45%, transparent);
-}
-
-.tiptap-editor .ProseMirror .email-block-error,
-.tiptap-editor .ProseMirror .email-draft-block-error {
- display: flex;
- align-items: center;
- gap: 6px;
- color: color-mix(in srgb, var(--foreground) 55%, transparent);
- font-size: 13px;
-}
-
-.tiptap-editor .ProseMirror .email-block-error-msg {
- font-size: 13px;
- color: #ef4444;
- padding: 8px 0;
-}
-
-.tiptap-editor .ProseMirror .email-block-thread {
- display: flex;
- flex-direction: column;
- gap: 0;
-}
-
-.tiptap-editor .ProseMirror .email-block-thread-toggle {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 4px 8px;
- margin-bottom: 6px;
- font-size: 12px;
- font-weight: 500;
- color: color-mix(in srgb, var(--foreground) 50%, transparent);
- background: color-mix(in srgb, var(--foreground) 5%, transparent);
- border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
- border-radius: 12px;
+ color: #fff;
+ background: rgba(255, 255, 255, 0.2);
+ border: none;
+ border-radius: 4px;
cursor: pointer;
transition: background-color 0.12s ease;
width: fit-content;
}
-.tiptap-editor .ProseMirror .email-block-thread-toggle:hover {
- background: color-mix(in srgb, var(--foreground) 10%, transparent);
+.tiptap-editor .ProseMirror .calendar-block-join-btn:hover {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+/* Email block – Gmail style */
+.tiptap-editor .ProseMirror .email-block-card-gmail {
+ background-color: var(--background);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1), 0 1px 3px 1px rgba(0, 0, 0, 0.06);
+}
+
+.tiptap-editor .ProseMirror .email-block-card-gmail:hover {
+ background-color: var(--background);
+}
+
+/* Email badge */
+.tiptap-editor .ProseMirror .email-block-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: color-mix(in srgb, var(--foreground) 45%, transparent);
+ margin-bottom: 8px;
+}
+
+/* Summary */
+.tiptap-editor .ProseMirror .email-block-summary {
+ font-size: 15px;
+ font-weight: 500;
+ color: var(--foreground);
+ line-height: 1.4;
+ margin-bottom: 10px;
+}
+
+/* Expand button */
+.tiptap-editor .ProseMirror .email-block-expand-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 0;
+ font-size: 13px;
+ font-weight: 400;
+ color: color-mix(in srgb, var(--foreground) 50%, transparent);
+ background: none;
+ border: none;
+ cursor: pointer;
+ transition: color 0.12s ease;
+ margin-bottom: 4px;
+}
+
+.tiptap-editor .ProseMirror .email-block-expand-btn:hover {
+ color: var(--foreground);
+}
+
+.tiptap-editor .ProseMirror .email-block-expand-meta {
+ color: color-mix(in srgb, var(--foreground) 35%, transparent);
}
.tiptap-editor .ProseMirror .email-block-toggle-chevron {
@@ -1170,111 +1166,156 @@
transform: rotate(180deg);
}
-.tiptap-editor .ProseMirror .email-block-message {
- padding: 8px 0;
+/* Email details (expanded) */
+.tiptap-editor .ProseMirror .email-block-email-details {
+ margin-top: 10px;
+ padding: 12px;
+ background: color-mix(in srgb, var(--foreground) 4%, transparent);
+ border-radius: 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
}
-.tiptap-editor .ProseMirror .email-block-message + .email-block-message {
- border-top: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
+.tiptap-editor .ProseMirror .email-block-message {
+ padding: 0;
}
.tiptap-editor .ProseMirror .email-block-message-header {
display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 6px;
-}
-
-.tiptap-editor .ProseMirror .email-block-avatar {
- width: 28px;
- height: 28px;
- border-radius: 50%;
- background: color-mix(in srgb, var(--primary) 20%, transparent);
- color: var(--primary);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 11px;
- font-weight: 600;
- flex-shrink: 0;
+ align-items: flex-start;
+ gap: 12px;
+ margin-bottom: 10px;
}
.tiptap-editor .ProseMirror .email-block-sender-info {
display: flex;
flex-direction: column;
min-width: 0;
+ flex: 1;
+ gap: 2px;
+}
+
+.tiptap-editor .ProseMirror .email-block-sender-row {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 8px;
}
.tiptap-editor .ProseMirror .email-block-sender-name {
- font-size: 13px;
+ font-size: 14px;
font-weight: 500;
color: var(--foreground);
}
.tiptap-editor .ProseMirror .email-block-sender-date {
- font-size: 11px;
+ font-size: 12px;
+ color: color-mix(in srgb, var(--foreground) 50%, transparent);
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.tiptap-editor .ProseMirror .email-block-subject-line {
+ font-size: 12px;
color: color-mix(in srgb, var(--foreground) 45%, transparent);
}
.tiptap-editor .ProseMirror .email-block-message-body {
- font-size: 13px;
+ font-size: 14px;
color: color-mix(in srgb, var(--foreground) 80%, transparent);
white-space: pre-wrap;
- line-height: 1.5;
- padding-left: 36px;
-}
-
-.tiptap-editor .ProseMirror .email-block-context {
- margin-top: 8px;
- padding-top: 8px;
- border-top: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
- display: flex;
- flex-direction: column;
- gap: 10px;
+ line-height: 1.58;
}
.tiptap-editor .ProseMirror .email-block-context-section {
display: flex;
flex-direction: column;
gap: 4px;
+ padding-top: 10px;
+ border-top: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
}
.tiptap-editor .ProseMirror .email-block-context-label {
font-size: 11px;
- font-weight: 600;
+ font-weight: 500;
text-transform: uppercase;
- letter-spacing: 0.04em;
+ letter-spacing: 0.07em;
color: color-mix(in srgb, var(--foreground) 40%, transparent);
}
.tiptap-editor .ProseMirror .email-block-context-summary {
- font-size: 13px;
+ font-size: 14px;
color: color-mix(in srgb, var(--foreground) 65%, transparent);
- line-height: 1.5;
+ line-height: 1.58;
white-space: pre-wrap;
- padding-left: 8px;
- border-left: 2px solid color-mix(in srgb, var(--foreground) 10%, transparent);
+ padding-left: 12px;
+ border-left: 3px solid color-mix(in srgb, var(--foreground) 12%, transparent);
+}
+
+/* Draft section */
+.tiptap-editor .ProseMirror .email-block-draft-section {
+ margin-top: 10px;
+ padding: 10px 12px;
+ border: 1px solid color-mix(in srgb, var(--foreground) 12%, transparent);
+ border-radius: 6px;
+}
+
+.tiptap-editor .ProseMirror .email-block-draft-label {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: color-mix(in srgb, var(--foreground) 40%, transparent);
+ margin-bottom: 4px;
+}
+
+.tiptap-editor .ProseMirror .email-draft-block-body-input {
+ width: 100%;
+ font-size: 14px;
+ color: var(--foreground);
+ background: none;
+ border: none;
+ outline: none;
+ padding: 4px 0;
+ font-family: inherit;
+ line-height: 1.58;
+ resize: none;
+ overflow: hidden;
+}
+
+.tiptap-editor .ProseMirror .email-draft-block-body-input::placeholder {
+ color: color-mix(in srgb, var(--foreground) 35%, transparent);
+}
+
+/* Action buttons */
+.tiptap-editor .ProseMirror .email-block-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 12px;
}
.tiptap-editor .ProseMirror .email-block-gmail-btn {
display: inline-flex;
align-items: center;
- gap: 5px;
- margin-top: 8px;
- padding: 5px 12px;
- font-size: 12px;
+ gap: 6px;
+ padding: 7px 16px;
+ font-size: 14px;
font-weight: 500;
color: color-mix(in srgb, var(--foreground) 60%, transparent);
- background: color-mix(in srgb, var(--foreground) 5%, transparent);
- border: 1px solid color-mix(in srgb, var(--foreground) 12%, transparent);
- border-radius: 6px;
+ background: transparent;
+ border: 1px solid var(--border);
+ border-radius: 18px;
cursor: pointer;
- transition: background-color 0.12s ease, color 0.12s ease;
+ transition: background-color 0.15s ease, box-shadow 0.15s ease;
width: fit-content;
+ letter-spacing: 0.01em;
}
.tiptap-editor .ProseMirror .email-block-gmail-btn:hover {
- background: color-mix(in srgb, var(--foreground) 10%, transparent);
+ background: color-mix(in srgb, var(--foreground) 8%, transparent);
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1), 0 1px 3px 1px rgba(0, 0, 0, 0.06);
color: var(--foreground);
}
@@ -1283,212 +1324,25 @@
cursor: default;
}
-.tiptap-editor .ProseMirror .email-block-generate-btn {
- color: var(--primary);
- border-color: color-mix(in srgb, var(--primary) 25%, transparent);
+.tiptap-editor .ProseMirror .email-block-gmail-btn-primary {
+ color: #fff;
+ background: #1a73e8;
+ border-color: #1a73e8;
}
-.tiptap-editor .ProseMirror .email-block-generate-btn:hover:not(:disabled) {
- background: color-mix(in srgb, var(--primary) 10%, transparent);
- color: var(--primary);
+.tiptap-editor .ProseMirror .email-block-gmail-btn-primary:hover:not(:disabled) {
+ background: #1765cc;
+ box-shadow: 0 1px 2px 0 rgba(26, 115, 232, 0.45), 0 1px 3px 1px rgba(26, 115, 232, 0.3);
+ color: #fff;
}
-@keyframes email-block-spin {
- to { transform: rotate(360deg); }
-}
-
-.tiptap-editor .ProseMirror .email-block-spinner {
- animation: email-block-spin 1s linear infinite;
-}
-
-/* Email block split button (generate/assistant) */
-.tiptap-editor .ProseMirror .email-block-response-split {
- position: relative;
- display: inline-flex;
- align-items: stretch;
- margin-top: 8px;
-}
-
-.tiptap-editor .ProseMirror .email-block-response-split > button {
- box-sizing: border-box;
-}
-
-.tiptap-editor .ProseMirror .email-block-split-main {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- padding: 5px 8px 5px 12px;
- font-size: 12px;
- font-weight: 500;
- color: var(--primary);
- background: color-mix(in srgb, var(--foreground) 5%, transparent);
- border: 1px solid color-mix(in srgb, var(--foreground) 12%, transparent);
- border-right: none;
- border-radius: 6px 0 0 6px;
- cursor: pointer;
- transition: background-color 0.12s ease;
-}
-
-.tiptap-editor .ProseMirror .email-block-split-main:hover:not(:disabled) {
- background: color-mix(in srgb, var(--foreground) 10%, transparent);
-}
-
-.tiptap-editor .ProseMirror .email-block-split-main:disabled {
- opacity: 0.6;
- cursor: default;
-}
-
-.tiptap-editor .ProseMirror .email-block-split-chevron {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 5px 6px;
- color: color-mix(in srgb, var(--foreground) 60%, transparent);
- background: color-mix(in srgb, var(--foreground) 5%, transparent);
- border: 1px solid color-mix(in srgb, var(--foreground) 12%, transparent);
- border-left: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
- border-radius: 0 6px 6px 0;
- cursor: pointer;
- transition: background-color 0.12s ease;
-}
-
-.tiptap-editor .ProseMirror .email-block-split-chevron:hover {
- background: color-mix(in srgb, var(--foreground) 10%, transparent);
-}
-
-.tiptap-editor .ProseMirror .email-block-split-chevron-open {
- border-radius: 0 6px 0 0;
- border-bottom-color: transparent;
-}
-
-.tiptap-editor .ProseMirror .email-block-split-dropdown {
- position: absolute;
- top: calc(100% - 1px);
- right: 0;
- z-index: 50;
- background: color-mix(in srgb, var(--foreground) 5%, transparent);
- border: 1px solid color-mix(in srgb, var(--foreground) 12%, transparent);
- border-top: none;
- border-radius: 0 0 6px 6px;
-}
-
-.tiptap-editor .ProseMirror .email-block-split-option {
- display: flex;
- align-items: center;
- gap: 5px;
- white-space: nowrap;
- padding: 5px 12px;
- font-size: 12px;
- font-weight: 500;
- color: color-mix(in srgb, var(--foreground) 60%, transparent);
- background: none;
- border: none;
- border-radius: 0 0 6px 6px;
- cursor: pointer;
- transition: background-color 0.12s ease;
-}
-
-.tiptap-editor .ProseMirror .email-block-split-option:hover {
- background: color-mix(in srgb, var(--foreground) 10%, transparent);
- color: var(--foreground);
-}
-
-/* Email draft block */
-.tiptap-editor .ProseMirror .email-draft-block-header {
- display: flex;
- flex-direction: column;
- gap: 4px;
- margin-bottom: 10px;
- padding-bottom: 8px;
- border-bottom: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
-}
-
-.tiptap-editor .ProseMirror .email-draft-block-field {
- display: flex;
- align-items: baseline;
- gap: 8px;
- font-size: 13px;
-}
-
-.tiptap-editor .ProseMirror .email-draft-block-label {
- font-weight: 500;
- color: color-mix(in srgb, var(--foreground) 45%, transparent);
- min-width: 50px;
-}
-
-.tiptap-editor .ProseMirror .email-draft-block-value {
- color: var(--foreground);
-}
-
-.tiptap-editor .ProseMirror .email-draft-block-input {
- flex: 1;
- font-size: 13px;
- color: var(--foreground);
- background: none;
- border: none;
- outline: none;
- padding: 2px 0;
- font-family: inherit;
-}
-
-.tiptap-editor .ProseMirror .email-draft-block-input::placeholder {
- color: color-mix(in srgb, var(--foreground) 30%, transparent);
-}
-
-.tiptap-editor .ProseMirror .email-draft-block-body-input {
- width: 100%;
- font-size: 13px;
- color: var(--foreground);
- background: none;
- border: none;
- outline: none;
- padding: 4px 0;
- font-family: inherit;
- line-height: 1.6;
- resize: none;
- overflow: hidden;
-}
-
-.tiptap-editor .ProseMirror .email-draft-block-body-input::placeholder {
- color: color-mix(in srgb, var(--foreground) 30%, transparent);
-}
-
-.tiptap-editor .ProseMirror .email-draft-block-actions {
+.tiptap-editor .ProseMirror .email-block-error,
+.tiptap-editor .ProseMirror .email-draft-block-error {
display: flex;
align-items: center;
gap: 6px;
- margin-top: 8px;
-}
-
-.tiptap-editor .ProseMirror .email-draft-block-reply {
- margin-top: 6px;
-}
-
-.tiptap-editor .ProseMirror .email-draft-block-reply-toggle {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 4px 8px;
- margin-bottom: 6px;
- font-size: 12px;
- font-weight: 500;
- color: color-mix(in srgb, var(--foreground) 50%, transparent);
- background: color-mix(in srgb, var(--foreground) 5%, transparent);
- border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
- border-radius: 12px;
- cursor: pointer;
- transition: background-color 0.12s ease;
- width: fit-content;
-}
-
-.tiptap-editor .ProseMirror .email-draft-block-reply-toggle:hover {
- background: color-mix(in srgb, var(--foreground) 10%, transparent);
-}
-
-.tiptap-editor .ProseMirror .email-draft-block-reply-thread {
- padding: 4px 0 0 8px;
- border-left: 2px solid color-mix(in srgb, var(--foreground) 10%, transparent);
- margin-left: 4px;
+ color: color-mix(in srgb, var(--foreground) 55%, transparent);
+ font-size: 14px;
}
/* Transcript block */
diff --git a/apps/x/packages/shared/src/blocks.ts b/apps/x/packages/shared/src/blocks.ts
index 68209051..d94a504f 100644
--- a/apps/x/packages/shared/src/blocks.ts
+++ b/apps/x/packages/shared/src/blocks.ts
@@ -63,6 +63,7 @@ export type CalendarBlock = z.infer
;
export const EmailBlockSchema = z.object({
threadId: z.string().optional(),
+ summary: z.string().optional(),
subject: z.string().optional(),
from: z.string().optional(),
to: z.string().optional(),