conductor-remote 1.30.3 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/assets/index-B-CPj6V0.js +41 -0
- package/dist/assets/index-DzZRiYLb.css +1 -0
- package/dist/index.html +2 -2
- package/dist/push-sw.js +64 -0
- package/dist/sw.js +1 -1
- package/dist-node/src/notify.js +319 -0
- package/dist-node/src/reads.js +88 -1
- package/dist-node/src/server.js +45 -0
- package/dist-node/src/webpush.js +146 -0
- package/package.json +1 -1
- package/dist/assets/index-8aCnDSQJ.css +0 -1
- package/dist/assets/index-B2SjHT-B.js +0 -41
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, -apple-system, "SF Pro Text", "Segoe UI", Roboto, sans-serif;--font-mono:ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", Menlo, monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-bg:#0a0b0e;--color-surface:#14161b;--color-surface-2:#1b1e26;--color-border:#262a33;--color-border-soft:#1e222a;--color-text:#e7e9ee;--color-muted:#8b909d;--color-faint:#5c616d;--color-accent:#8b7dff;--color-accent-soft:#2a2650;--color-working:#f5a623;--color-idle:#3ecf8e;--color-done:#5b9dff;--color-pr-merged:#ac47ff;--color-pr-draft:#a4a3a2;--color-pr-conflicts:#fd9a00;--color-pr-mergeable:#49de80;--color-add:#3ecf8e;--color-del:#ff6b6b}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{-webkit-tap-highlight-color:transparent}html,body,#root{height:var(--app-height,100dvh)}html,body{overflow:hidden}@media(display-mode:standalone){html,body,#root{height:var(--app-height,100vh)}}html[data-standalone],html[data-standalone] body,html[data-standalone] #root{height:var(--app-height,100vh)}html{background:var(--color-bg)}body{background:var(--color-bg);color:var(--color-text);font-family:var(--font-sans);-webkit-font-smoothing:antialiased;overscroll-behavior:none;margin:0}button{font:inherit;color:inherit;cursor:pointer}::-webkit-scrollbar{width:0;height:0}}@layer components{.card{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3.5);text-align:left;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:flex}.card:active{background-color:var(--color-surface-2);scale:.985}.pill{padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px}.pill-active{background-color:var(--color-surface-2);color:var(--color-text)}.dot{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5);background-color:var(--color-faint);border-radius:3.40282e38px;flex-shrink:0}.dot-spinner{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5);animation:var(--animate-spin);border-style:var(--tw-border-style);--spin-color:var(--color-working);border-width:2px;border-color:var(--spin-color);border-radius:3.40282e38px;flex-shrink:0}@supports (color:color-mix(in lab,red,red)){.dot-spinner{border-color:color-mix(in srgb,var(--spin-color) 30%,transparent)}}.dot-spinner{border-top-color:var(--spin-color);border-right-color:var(--spin-color)}.dot-idle{background:var(--color-idle)}.dot-done{background:var(--color-done)}.typing-dot{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5);background-color:var(--color-muted);border-radius:3.40282e38px;animation:1.2s ease-in-out infinite typing}.typing-dot:nth-child(2){animation-delay:.15s}.typing-dot:nth-child(3){animation-delay:.3s}.md>:first-child{margin-top:0}.md>:last-child{margin-bottom:0}.md p{margin:.5em 0}.md h1,.md h2,.md h3,.md h4,.md h5,.md h6{margin:.9em 0 .4em;font-weight:600;line-height:1.3}.md h1{font-size:1.25em}.md h2{font-size:1.15em}.md h3{font-size:1.05em}.md ul,.md ol{margin:.5em 0;padding-left:1.4em}.md ul{list-style:outside}.md ol{list-style:decimal}.md li,.md li>ul,.md li>ol{margin:.2em 0}.md code{background:#1b1e26cc;border-radius:.3rem}@supports (color:color-mix(in lab,red,red)){.md code{background:color-mix(in srgb,var(--color-surface-2) 80%,transparent)}}.md code{font-family:var(--font-mono);padding:.1em .35em;font-size:.85em}.md pre{border:1px solid var(--color-border-soft);background:var(--color-bg);border-radius:.6rem;margin:.5em 0;padding:.6em .75em;overflow-x:auto}.md pre code{background:0 0;padding:0;font-size:12px;line-height:1.5}.md a{color:var(--color-accent);text-underline-offset:2px;text-decoration:underline}.md blockquote{border-left:2px solid var(--color-border);color:var(--color-muted);margin:.5em 0;padding-left:.75em}.md hr{border:0;border-top:1px solid var(--color-border-soft);margin:.75em 0}.md table{border-collapse:collapse;max-width:100%;margin:.5em 0;font-size:.9em;display:block;overflow-x:auto}.md th,.md td{border:1px solid var(--color-border);text-align:left;padding:.3em .6em}.md th{background:var(--color-surface-2);font-weight:600}.md strong{font-weight:600}.md input[type=checkbox]{margin-right:.4em}}@layer utilities{.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.inset-x-0{inset-inline:0}.inset-y-0{inset-block:0}.top-0{top:0}.top-0\.5{top:calc(var(--spacing) * .5)}.top-1\.5{top:calc(var(--spacing) * 1.5)}.top-full{top:100%}.-right-0\.5{right:calc(var(--spacing) * -.5)}.right-1\.5{right:calc(var(--spacing) * 1.5)}.right-2{right:calc(var(--spacing) * 2)}.-bottom-0\.5{bottom:calc(var(--spacing) * -.5)}.bottom-0{bottom:0}.left-0{left:0}.left-0\.5{left:calc(var(--spacing) * .5)}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[60\]{z-index:60}.mx-auto{margin-inline:auto}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.-mr-1{margin-right:calc(var(--spacing) * -1)}.mr-1{margin-right:var(--spacing)}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.-ml-1{margin-left:calc(var(--spacing) * -1)}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)}.ml-auto{margin-left:auto}.line-clamp-4{-webkit-line-clamp:4;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-20{width:calc(var(--spacing) * 20);height:calc(var(--spacing) * 20)}.size-60{width:calc(var(--spacing) * 60);height:calc(var(--spacing) * 60)}.size-full{width:100%;height:100%}.h-6{height:calc(var(--spacing) * 6)}.h-full{height:100%}.max-h-40{max-height:calc(var(--spacing) * 40)}.max-h-44{max-height:calc(var(--spacing) * 44)}.max-h-72{max-height:calc(var(--spacing) * 72)}.min-h-0{min-height:0}.w-1{width:var(--spacing)}.w-11{width:calc(var(--spacing) * 11)}.w-64{width:calc(var(--spacing) * 64)}.w-\[85\%\]{width:85%}.w-full{width:100%}.max-w-36{max-width:calc(var(--spacing) * 36)}.max-w-80{max-width:calc(var(--spacing) * 80)}.max-w-\[55vw\]{max-width:55vw}.max-w-\[70vw\]{max-width:70vw}.max-w-\[85\%\]{max-width:85%}.max-w-\[92\%\]{max-width:92%}.max-w-full{max-width:100%}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-5{min-width:calc(var(--spacing) * 5)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-5{--tw-translate-x:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.-rotate-90{rotate:-90deg}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.list-none{list-style-type:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.rounded-t-3xl{border-top-left-radius:var(--radius-3xl);border-top-right-radius:var(--radius-3xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-accent\/50{border-color:#8b7dff80}@supports (color:color-mix(in lab,red,red)){.border-accent\/50{border-color:color-mix(in oklab,var(--color-accent) 50%,transparent)}}.border-border{border-color:var(--color-border)}.border-border-soft{border-color:var(--color-border-soft)}.border-del\/30{border-color:#ff6b6b4d}@supports (color:color-mix(in lab,red,red)){.border-del\/30{border-color:color-mix(in oklab,var(--color-del) 30%,transparent)}}.border-del\/40{border-color:#ff6b6b66}@supports (color:color-mix(in lab,red,red)){.border-del\/40{border-color:color-mix(in oklab,var(--color-del) 40%,transparent)}}.border-faint{border-color:var(--color-faint)}.border-white\/80{border-color:#fffc}@supports (color:color-mix(in lab,red,red)){.border-white\/80{border-color:color-mix(in oklab,var(--color-white) 80%,transparent)}}.border-t-accent{border-top-color:var(--color-accent)}.border-l-accent{border-left-color:var(--color-accent)}.bg-accent{background-color:var(--color-accent)}.bg-accent-soft{background-color:var(--color-accent-soft)}.bg-accent\/10{background-color:#8b7dff1a}@supports (color:color-mix(in lab,red,red)){.bg-accent\/10{background-color:color-mix(in oklab,var(--color-accent) 10%,transparent)}}.bg-add{background-color:var(--color-add)}.bg-add\/10{background-color:#3ecf8e1a}@supports (color:color-mix(in lab,red,red)){.bg-add\/10{background-color:color-mix(in oklab,var(--color-add) 10%,transparent)}}.bg-bg{background-color:var(--color-bg)}.bg-bg\/80{background-color:#0a0b0ecc}@supports (color:color-mix(in lab,red,red)){.bg-bg\/80{background-color:color-mix(in oklab,var(--color-bg) 80%,transparent)}}.bg-black{background-color:var(--color-black)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black) 60%,transparent)}}.bg-black\/75{background-color:#000000bf}@supports (color:color-mix(in lab,red,red)){.bg-black\/75{background-color:color-mix(in oklab,var(--color-black) 75%,transparent)}}.bg-del\/5{background-color:#ff6b6b0d}@supports (color:color-mix(in lab,red,red)){.bg-del\/5{background-color:color-mix(in oklab,var(--color-del) 5%,transparent)}}.bg-del\/10{background-color:#ff6b6b1a}@supports (color:color-mix(in lab,red,red)){.bg-del\/10{background-color:color-mix(in oklab,var(--color-del) 10%,transparent)}}.bg-surface{background-color:var(--color-surface)}.bg-surface-2{background-color:var(--color-surface-2)}.bg-surface\/40{background-color:#14161b66}@supports (color:color-mix(in lab,red,red)){.bg-surface\/40{background-color:color-mix(in oklab,var(--color-surface) 40%,transparent)}}.bg-surface\/60{background-color:#14161b99}@supports (color:color-mix(in lab,red,red)){.bg-surface\/60{background-color:color-mix(in oklab,var(--color-surface) 60%,transparent)}}.bg-surface\/95{background-color:#14161bf2}@supports (color:color-mix(in lab,red,red)){.bg-surface\/95{background-color:color-mix(in oklab,var(--color-surface) 95%,transparent)}}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-working{background-color:var(--color-working)}.bg-working\/10{background-color:#f5a6231a}@supports (color:color-mix(in lab,red,red)){.bg-working\/10{background-color:color-mix(in oklab,var(--color-working) 10%,transparent)}}.bg-working\/15{background-color:#f5a62326}@supports (color:color-mix(in lab,red,red)){.bg-working\/15{background-color:color-mix(in oklab,var(--color-working) 15%,transparent)}}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-16{padding-block:calc(var(--spacing) * 16)}.py-px{padding-block:1px}.pt-2{padding-top:calc(var(--spacing) * 2)}.pr-1{padding-right:var(--spacing)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-2\.5{padding-bottom:calc(var(--spacing) * 2.5)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-3{padding-left:calc(var(--spacing) * 3)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.text-\[11\.5px\]{font-size:11.5px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.leading-\[1\.5\]{--tw-leading:1.5;line-height:1.5}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.\[overflow-wrap\:anywhere\]{overflow-wrap:anywhere}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.text-accent{color:var(--color-accent)}.text-add{color:var(--color-add)}.text-bg{color:var(--color-bg)}.text-black{color:var(--color-black)}.text-del{color:var(--color-del)}.text-del\/80{color:#ff6b6bcc}@supports (color:color-mix(in lab,red,red)){.text-del\/80{color:color-mix(in oklab,var(--color-del) 80%,transparent)}}.text-faint{color:var(--color-faint)}.text-muted{color:var(--color-muted)}.text-text{color:var(--color-text)}.text-white{color:var(--color-white)}.text-working{color:var(--color-working)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.\[color-scheme\:dark\]{color-scheme:dark}.opacity-40{opacity:.4}.opacity-60{opacity:.6}.shadow-\[0_0_0_100vmax_rgba\(0\,0\,0\,0\.5\)\]{--tw-shadow:0 0 0 100vmax var(--tw-shadow-color,#00000080);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-surface{--tw-ring-color:var(--color-surface)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.group-open\/steps\:invisible:is(:where(.group\/steps):is([open],:popover-open,:open) *){visibility:hidden}.group-open\/steps\:rotate-90:is(:where(.group\/steps):is([open],:popover-open,:open) *),.group-open\/think\:rotate-90:is(:where(.group\/think):is([open],:popover-open,:open) *){rotate:90deg}.placeholder\:font-sans::placeholder{font-family:var(--font-sans)}.placeholder\:text-faint::placeholder{color:var(--color-faint)}.focus-within\:border-accent\/60:focus-within{border-color:#8b7dff99}@supports (color:color-mix(in lab,red,red)){.focus-within\:border-accent\/60:focus-within{border-color:color-mix(in oklab,var(--color-accent) 60%,transparent)}}@media(hover:hover){.hover\:underline:hover{text-decoration-line:underline}}.active\:scale-90:active{--tw-scale-x:90%;--tw-scale-y:90%;--tw-scale-z:90%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:scale-\[0\.985\]:active{scale:.985}.active\:bg-black\/70:active{background-color:#000000b3}@supports (color:color-mix(in lab,red,red)){.active\:bg-black\/70:active{background-color:color-mix(in oklab,var(--color-black) 70%,transparent)}}.active\:bg-surface:active{background-color:var(--color-surface)}.active\:bg-surface-2:active,.disabled\:bg-surface-2:disabled{background-color:var(--color-surface-2)}.disabled\:text-faint:disabled{color:var(--color-faint)}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}@media(min-width:48rem){.md\:static{position:static}.md\:inset-0{inset:0}.md\:inset-6{inset:calc(var(--spacing) * 6)}.md\:z-auto{z-index:auto}.md\:m-auto{margin:auto}.md\:block{display:block}.md\:hidden{display:none}.md\:h-fit{height:fit-content}.md\:w-72{width:calc(var(--spacing) * 72)}.md\:max-w-none{max-width:none}.md\:shrink-0{flex-shrink:0}.md\:translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.md\:rounded-3xl{border-radius:var(--radius-3xl)}.md\:border{border-style:var(--tw-border-style);border-width:1px}.md\:border-border-soft{border-color:var(--color-border-soft)}.md\:transition-none{transition-property:none}}@media(min-width:64rem){.lg\:static{position:static}.lg\:z-auto{z-index:auto}.lg\:w-80{width:calc(var(--spacing) * 80)}.lg\:w-\[380px\]{width:380px}.lg\:shrink-0{flex-shrink:0}.lg\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.lg\:border-border-soft{border-color:var(--color-border-soft)}}@media(min-width:80rem){.xl\:w-\[460px\]{width:460px}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}}@keyframes typing{0%,60%,to{opacity:.35;transform:none}30%{opacity:1;transform:translateY(-3px)}}@keyframes fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.fade-in{animation:.18s ease-out fade-in}.pt-safe{padding-top:max(env(safe-area-inset-top),.5rem)}.pb-safe{padding-bottom:max(env(safe-area-inset-bottom),.5rem)}html[data-keyboard] .pb-safe{padding-bottom:.5rem}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes spin{to{transform:rotate(360deg)}}
|
package/dist/index.html
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
<title>Conductor Remote</title>
|
|
14
14
|
<!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
|
|
15
15
|
<script src="/self-heal.js"></script>
|
|
16
|
-
<script type="module" crossorigin src="/assets/index-
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-B-CPj6V0.js"></script>
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DzZRiYLb.css">
|
|
18
18
|
<link rel="manifest" href="/manifest.webmanifest"></head>
|
|
19
19
|
<body>
|
|
20
20
|
<div id="root"></div>
|
package/dist/push-sw.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Push handlers for the generated service worker.
|
|
2
|
+
//
|
|
3
|
+
// vite-plugin-pwa runs in `generateSW` mode, so Workbox owns `sw.js` and there is no
|
|
4
|
+
// hand-written worker to add listeners to — this file is pulled in by
|
|
5
|
+
// `workbox.importScripts` (see vite.config.ts). It is plain JS on purpose: it is served
|
|
6
|
+
// as a static asset, never bundled. Being part of the precache manifest is what makes a
|
|
7
|
+
// change here produce a new `sw.js`, so edits actually ship.
|
|
8
|
+
//
|
|
9
|
+
// Two rules this obeys, both learned from iOS:
|
|
10
|
+
// 1. **Every push shows a notification.** Safari treats a push that resolves without one
|
|
11
|
+
// as abuse and can revoke the subscription, so there is no "suppress if the app is
|
|
12
|
+
// open" branch — the relay only sends when something genuinely happened.
|
|
13
|
+
// 2. **A tap focuses the existing window** rather than navigating it. The app is a
|
|
14
|
+
// token-gated SPA; `openWindow` on a live client would remount the whole thing and
|
|
15
|
+
// throw away in-progress composer text, so we focus and post a route instead.
|
|
16
|
+
|
|
17
|
+
self.addEventListener('push', event => {
|
|
18
|
+
const fallback = { title: 'Conductor Remote', body: 'Something changed in a workspace.', url: '/', tag: 'conductor' }
|
|
19
|
+
let data = fallback
|
|
20
|
+
if (event.data) {
|
|
21
|
+
try {
|
|
22
|
+
data = Object.assign({}, fallback, event.data.json())
|
|
23
|
+
} catch {
|
|
24
|
+
// Not ours / not JSON — still show something rather than a browser-generic notice.
|
|
25
|
+
data = Object.assign({}, fallback, { body: event.data.text() })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
event.waitUntil(
|
|
29
|
+
self.registration.showNotification(data.title, {
|
|
30
|
+
body: data.body,
|
|
31
|
+
// Tagged per workspace by the relay: a chatty agent replaces its own notification
|
|
32
|
+
// instead of stacking. `renotify` keeps the replacement audible.
|
|
33
|
+
tag: data.tag,
|
|
34
|
+
renotify: true,
|
|
35
|
+
icon: '/icon-192.png',
|
|
36
|
+
badge: '/icon-192.png',
|
|
37
|
+
timestamp: data.ts || Date.now(),
|
|
38
|
+
data: { url: data.url || '/' }
|
|
39
|
+
})
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
self.addEventListener('notificationclick', event => {
|
|
44
|
+
event.notification.close()
|
|
45
|
+
const url = (event.notification.data && event.notification.data.url) || '/'
|
|
46
|
+
event.waitUntil(
|
|
47
|
+
(async () => {
|
|
48
|
+
const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true })
|
|
49
|
+
for (const client of clients) {
|
|
50
|
+
if (new URL(client.url).origin !== self.location.origin) continue
|
|
51
|
+
try {
|
|
52
|
+
await client.focus()
|
|
53
|
+
// Handled in web/src/hooks.ts (usePushRouting) — an in-app route change,
|
|
54
|
+
// so the token gate and React state survive the tap.
|
|
55
|
+
client.postMessage({ type: 'push-navigate', url })
|
|
56
|
+
return
|
|
57
|
+
} catch {
|
|
58
|
+
// focus() can be refused (no user activation on some platforms) — fall through to openWindow
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
await self.clients.openWindow(url)
|
|
62
|
+
})()
|
|
63
|
+
)
|
|
64
|
+
})
|
package/dist/sw.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
if(!self.define){let e,
|
|
1
|
+
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const t=e=>i(e,o),c={module:{uri:o},exports:l,require:t};s[o]=Promise.all(n.map(e=>c[e]||t(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"890f6a70e97d144f4d025254e90892a1"},{url:"index.html",revision:"9a9b78762ca2fe27cad75d86a401309a"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DzZRiYLb.css",revision:null},{url:"assets/index-B-CPj6V0.js",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push notifications: "your agent finished" on the lock screen.
|
|
3
|
+
*
|
|
4
|
+
* This is a *third* shape next to the reads/writes split in CLAUDE.md, and the
|
|
5
|
+
* cheapest one — it is a **read that pushes**. Nothing here touches Conductor:
|
|
6
|
+
* the trigger is a poll of the same read-only SQLite the list view uses, and the
|
|
7
|
+
* delivery is an HTTPS POST to Apple/Google/Mozilla's push service. No
|
|
8
|
+
* Accessibility, no AppleScript, no window has to exist. A Conductor update can
|
|
9
|
+
* rename every UI string and notifications keep working.
|
|
10
|
+
*
|
|
11
|
+
* What counts as news is deliberately narrow — `sessions.status` is the only
|
|
12
|
+
* signal Conductor records, and it holds exactly three values:
|
|
13
|
+
* - `working → idle` — the turn ended. This is *the* event: it covers "done",
|
|
14
|
+
* "asked you a question" and "hit a permission prompt" alike, because all
|
|
15
|
+
* three end the turn.
|
|
16
|
+
* - `→ error` — the agent stopped badly.
|
|
17
|
+
* There is no permission-request table to watch (verified against the schema), so
|
|
18
|
+
* don't go looking for a finer-grained trigger; there isn't one.
|
|
19
|
+
*
|
|
20
|
+
* Two properties worth keeping:
|
|
21
|
+
* - **Transitions are confirmed one tick before they fire.** A status that
|
|
22
|
+
* flickers (a queued prompt starting the next turn immediately) would otherwise
|
|
23
|
+
* buzz a phone for a turn that never really ended.
|
|
24
|
+
* - **The first tick after a device subscribes is a baseline, not a broadcast.**
|
|
25
|
+
* Every already-idle session on the Mac is not news; without this, enabling
|
|
26
|
+
* notifications would fire one per workspace.
|
|
27
|
+
*/
|
|
28
|
+
import crypto from 'node:crypto';
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { stateDir } from "./config.js";
|
|
32
|
+
import { generateVapidKeys, MAX_PAYLOAD_BYTES, sendPush } from "./webpush.js";
|
|
33
|
+
const STORE_VERSION = 1;
|
|
34
|
+
/** Poll cadence for status transitions. Matches the phone's own list poll — a local SQLite read is cheap. */
|
|
35
|
+
const TICK_MS = 3000;
|
|
36
|
+
/**
|
|
37
|
+
* How long a push service should hold a notification for a phone that's offline.
|
|
38
|
+
* An hour: long enough to survive a tunnel/airplane-mode blip, short enough that
|
|
39
|
+
* "your agent finished" can't surface tomorrow morning as if it just happened.
|
|
40
|
+
*/
|
|
41
|
+
const TTL_SECONDS = 3600;
|
|
42
|
+
/** Notification bodies are one glance on a lock screen; the chat has the rest. */
|
|
43
|
+
const BODY_CHARS = 180;
|
|
44
|
+
/** Consecutive failures before a device is dropped. A `gone` response drops it immediately, regardless. */
|
|
45
|
+
const MAX_FAILURES = 20;
|
|
46
|
+
const clip = (s, n) => (s.length > n ? `${s.slice(0, n).trimEnd()}…` : s);
|
|
47
|
+
/** Alongside the token and the first-prompt queue — one dir holding everything this relay persists. */
|
|
48
|
+
function storePath() {
|
|
49
|
+
return path.join(stateDir(), 'push.json');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* VAPID identifies the sender to the push service and must be a `mailto:` or
|
|
53
|
+
* `https:` URL. The project's own repo says who this is without publishing an
|
|
54
|
+
* address; `PUSH_SUBJECT` overrides for anyone who'd rather be contactable.
|
|
55
|
+
*/
|
|
56
|
+
function subject() {
|
|
57
|
+
return process.env.PUSH_SUBJECT || 'https://github.com/hyldmo/conductor-remote';
|
|
58
|
+
}
|
|
59
|
+
let store = null;
|
|
60
|
+
function load() {
|
|
61
|
+
if (store)
|
|
62
|
+
return store;
|
|
63
|
+
try {
|
|
64
|
+
const raw = JSON.parse(fs.readFileSync(storePath(), 'utf8'));
|
|
65
|
+
if (raw?.version === STORE_VERSION && raw.vapid?.publicKey && Array.isArray(raw.devices)) {
|
|
66
|
+
store = raw;
|
|
67
|
+
return store;
|
|
68
|
+
}
|
|
69
|
+
console.warn('[push] ignoring unreadable subscription store — starting fresh (devices must re-enable)');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// no store yet, or unreadable — mint one below
|
|
73
|
+
}
|
|
74
|
+
// A fresh keypair invalidates every existing browser subscription, so this only
|
|
75
|
+
// ever happens when there were none to invalidate (or the file was deleted).
|
|
76
|
+
store = { version: STORE_VERSION, vapid: generateVapidKeys(), devices: [] };
|
|
77
|
+
save();
|
|
78
|
+
return store;
|
|
79
|
+
}
|
|
80
|
+
function save() {
|
|
81
|
+
if (!store)
|
|
82
|
+
return;
|
|
83
|
+
const file = storePath();
|
|
84
|
+
try {
|
|
85
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
86
|
+
// Write-then-rename so a crash mid-write can't leave a half-file that costs
|
|
87
|
+
// every phone its subscription. 0600: this holds push capability URLs.
|
|
88
|
+
const tmp = `${file}.tmp`;
|
|
89
|
+
fs.writeFileSync(tmp, JSON.stringify(store), { mode: 0o600 });
|
|
90
|
+
fs.renameSync(tmp, file);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
console.warn(`[push] could not persist subscriptions (${err instanceof Error ? err.message : err})`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Short, stable, non-reversible handle for an endpoint — safe to show and to address a device by. */
|
|
97
|
+
function deviceId(endpoint) {
|
|
98
|
+
return crypto.createHash('sha256').update(endpoint).digest('hex').slice(0, 16);
|
|
99
|
+
}
|
|
100
|
+
function info(d) {
|
|
101
|
+
return {
|
|
102
|
+
id: deviceId(d.endpoint),
|
|
103
|
+
label: d.label,
|
|
104
|
+
createdAt: d.createdAt,
|
|
105
|
+
lastOkAt: d.lastOkAt,
|
|
106
|
+
lastError: d.lastError,
|
|
107
|
+
failures: d.failures
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Whether the notifier is allowed to run at all (`PUSH_NOTIFY=off` disables it). */
|
|
111
|
+
export function notificationsEnabled() {
|
|
112
|
+
const raw = process.env.PUSH_NOTIFY?.trim().toLowerCase();
|
|
113
|
+
return !(raw === 'off' || raw === 'false' || raw === '0');
|
|
114
|
+
}
|
|
115
|
+
/** What the phone needs to subscribe, plus who is already subscribed. */
|
|
116
|
+
export function pushConfig() {
|
|
117
|
+
const s = load();
|
|
118
|
+
return { enabled: notificationsEnabled(), publicKey: s.vapid.publicKey, devices: s.devices.map(info) };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Register (or refresh) a device. Upserts on endpoint: the phone re-sends its
|
|
122
|
+
* subscription on every app load, which is what keeps a relay that lost its store
|
|
123
|
+
* — or a subscription the browser silently renewed — from going quietly dead.
|
|
124
|
+
*/
|
|
125
|
+
export function subscribeDevice(sub, label) {
|
|
126
|
+
const s = load();
|
|
127
|
+
const existing = s.devices.find(d => d.endpoint === sub.endpoint);
|
|
128
|
+
if (existing) {
|
|
129
|
+
existing.keys = sub.keys;
|
|
130
|
+
existing.label = label || existing.label;
|
|
131
|
+
// A re-subscribe is the user telling us this device works; clear the failure trail.
|
|
132
|
+
existing.failures = 0;
|
|
133
|
+
existing.lastError = null;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
s.devices.push({
|
|
137
|
+
endpoint: sub.endpoint,
|
|
138
|
+
keys: sub.keys,
|
|
139
|
+
label: label || 'phone',
|
|
140
|
+
createdAt: Date.now(),
|
|
141
|
+
lastOkAt: null,
|
|
142
|
+
lastError: null,
|
|
143
|
+
failures: 0
|
|
144
|
+
});
|
|
145
|
+
console.info(`[push] device subscribed (${label || 'phone'}) — ${s.devices.length} total`);
|
|
146
|
+
}
|
|
147
|
+
save();
|
|
148
|
+
// The id comes back so the phone can address itself (the "Send test" button) without
|
|
149
|
+
// ever putting its own endpoint back on the wire.
|
|
150
|
+
return { id: deviceId(sub.endpoint), devices: s.devices.map(info) };
|
|
151
|
+
}
|
|
152
|
+
export function unsubscribeDevice(endpoint) {
|
|
153
|
+
const s = load();
|
|
154
|
+
const before = s.devices.length;
|
|
155
|
+
s.devices = s.devices.filter(d => d.endpoint !== endpoint);
|
|
156
|
+
if (s.devices.length === before)
|
|
157
|
+
return false;
|
|
158
|
+
console.info(`[push] device unsubscribed — ${s.devices.length} left`);
|
|
159
|
+
save();
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
function drop(endpoint, why) {
|
|
163
|
+
const s = load();
|
|
164
|
+
s.devices = s.devices.filter(d => d.endpoint !== endpoint);
|
|
165
|
+
console.info(`[push] dropped a dead subscription (${why}) — ${s.devices.length} left`);
|
|
166
|
+
save();
|
|
167
|
+
}
|
|
168
|
+
/** Deliver one message to one device, folding the outcome back into its health fields. */
|
|
169
|
+
async function deliver(device, message) {
|
|
170
|
+
const s = load();
|
|
171
|
+
let payload = Buffer.from(JSON.stringify(message));
|
|
172
|
+
if (payload.length > MAX_PAYLOAD_BYTES) {
|
|
173
|
+
// Unreachable at the current BODY_CHARS, but an over-long title (a workspace name
|
|
174
|
+
// is user text) must clip rather than turn into a failed send.
|
|
175
|
+
const room = Math.max(0, message.body.length - (payload.length - MAX_PAYLOAD_BYTES) - 8);
|
|
176
|
+
payload = Buffer.from(JSON.stringify({ ...message, body: clip(message.body, room) }));
|
|
177
|
+
}
|
|
178
|
+
const result = await sendPush(device, s.vapid, subject(), payload, TTL_SECONDS);
|
|
179
|
+
if (result.ok) {
|
|
180
|
+
device.lastOkAt = Date.now();
|
|
181
|
+
device.lastError = null;
|
|
182
|
+
device.failures = 0;
|
|
183
|
+
save();
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
if (result.gone) {
|
|
187
|
+
drop(device.endpoint, `push service says ${result.status}`);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
device.failures++;
|
|
191
|
+
device.lastError = result.error ?? `HTTP ${result.status}`;
|
|
192
|
+
// Never log the endpoint: it's a capability URL, and the log is a wire surface (src/logbuf.ts).
|
|
193
|
+
console.warn(`[push] delivery failed (${device.lastError}) — ${device.failures} in a row`);
|
|
194
|
+
if (device.failures >= MAX_FAILURES)
|
|
195
|
+
drop(device.endpoint, `${device.failures} consecutive failures`);
|
|
196
|
+
else
|
|
197
|
+
save();
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
/** Fan a message out to every subscribed device. Returns how many took it. */
|
|
201
|
+
export async function notifyAll(message) {
|
|
202
|
+
const s = load();
|
|
203
|
+
if (!s.devices.length)
|
|
204
|
+
return 0;
|
|
205
|
+
// Snapshot: `deliver` can prune the live array mid-flight.
|
|
206
|
+
const results = await Promise.all(s.devices.slice().map(d => deliver(d, message)));
|
|
207
|
+
return results.filter(Boolean).length;
|
|
208
|
+
}
|
|
209
|
+
/** Send to one device — the Connect sheet's "Send test", which proves the whole path end to end. */
|
|
210
|
+
export async function notifyDevice(id, message) {
|
|
211
|
+
const s = load();
|
|
212
|
+
const device = s.devices.find(d => deviceId(d.endpoint) === id);
|
|
213
|
+
if (!device)
|
|
214
|
+
return { ok: false, error: 'this device is not subscribed' };
|
|
215
|
+
const ok = await deliver(device, message);
|
|
216
|
+
return ok ? { ok } : { ok: false, error: device.lastError ?? 'the push service rejected it' };
|
|
217
|
+
}
|
|
218
|
+
export function deviceCount() {
|
|
219
|
+
return load().devices.length;
|
|
220
|
+
}
|
|
221
|
+
/** Collapse a transcript entry to one lock-screen line: no code fences, no blank runs. */
|
|
222
|
+
function oneLine(text) {
|
|
223
|
+
return clip(text
|
|
224
|
+
.replace(/```[\s\S]*?```/g, '…')
|
|
225
|
+
.replace(/\s+/g, ' ')
|
|
226
|
+
.trim(), BODY_CHARS);
|
|
227
|
+
}
|
|
228
|
+
// --- the watcher ---
|
|
229
|
+
/** Last seen status per session. Null means "re-baseline on the next tick" (nobody was subscribed). */
|
|
230
|
+
let previous = null;
|
|
231
|
+
/** Transitions seen once and awaiting a second tick's confirmation. */
|
|
232
|
+
const armed = new Map();
|
|
233
|
+
function tick(reads) {
|
|
234
|
+
if (deviceCount() === 0) {
|
|
235
|
+
// Nothing to notify. Forget the snapshot so re-enabling starts from the world as
|
|
236
|
+
// it is then, rather than replaying every turn that ended while nobody listened.
|
|
237
|
+
previous = null;
|
|
238
|
+
armed.clear();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const states = reads.listSessionStates();
|
|
242
|
+
const current = new Map(states.map(s => [s.sessionId, s.status ?? null]));
|
|
243
|
+
if (previous === null) {
|
|
244
|
+
previous = current;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const baseline = previous;
|
|
248
|
+
for (const state of states) {
|
|
249
|
+
const now = state.status ?? null;
|
|
250
|
+
const before = baseline.get(state.sessionId);
|
|
251
|
+
const pendingKind = armed.get(state.sessionId);
|
|
252
|
+
if (pendingKind) {
|
|
253
|
+
armed.delete(state.sessionId);
|
|
254
|
+
// Confirmed only if the new status held for a second tick; a flap just drops the arm.
|
|
255
|
+
if ((pendingKind === 'done' && now === 'idle') || (pendingKind === 'error' && now === 'error')) {
|
|
256
|
+
void fire(reads, state.sessionId, pendingKind, state);
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
// A session we've never seen (a new chat, or the first tick after re-baselining)
|
|
261
|
+
// contributes its status to the snapshot but is never itself news.
|
|
262
|
+
if (before === undefined)
|
|
263
|
+
continue;
|
|
264
|
+
if (before === 'working' && now === 'idle')
|
|
265
|
+
armed.set(state.sessionId, 'done');
|
|
266
|
+
else if (before !== 'error' && now === 'error')
|
|
267
|
+
armed.set(state.sessionId, 'error');
|
|
268
|
+
}
|
|
269
|
+
// A session armed on the last tick can vanish before this one (its workspace was
|
|
270
|
+
// archived mid-turn). Nothing above touches it, so drop it here rather than keep
|
|
271
|
+
// the key forever — and never notify about a workspace that no longer exists.
|
|
272
|
+
for (const sessionId of armed.keys())
|
|
273
|
+
if (!current.has(sessionId))
|
|
274
|
+
armed.delete(sessionId);
|
|
275
|
+
previous = current;
|
|
276
|
+
}
|
|
277
|
+
async function fire(reads, sessionId, kind, state) {
|
|
278
|
+
const said = reads.lastAssistantText(sessionId);
|
|
279
|
+
const where = state.sessionTitle ? `${state.workspaceTitle} · ${state.sessionTitle}` : state.workspaceTitle;
|
|
280
|
+
const body = kind === 'error'
|
|
281
|
+
? said
|
|
282
|
+
? `Stopped with an error. ${oneLine(said)}`
|
|
283
|
+
: 'The agent stopped with an error.'
|
|
284
|
+
: said
|
|
285
|
+
? oneLine(said)
|
|
286
|
+
: 'Finished its turn.';
|
|
287
|
+
const sent = await notifyAll({
|
|
288
|
+
title: state.repoName ? `${where} — ${state.repoName}` : where,
|
|
289
|
+
body,
|
|
290
|
+
// Per workspace, so a chatty agent replaces its own notification instead of stacking.
|
|
291
|
+
tag: state.workspaceId,
|
|
292
|
+
url: `/w/${state.workspaceId}`,
|
|
293
|
+
kind,
|
|
294
|
+
ts: Date.now()
|
|
295
|
+
});
|
|
296
|
+
if (sent)
|
|
297
|
+
console.info(`[push] ${kind} in ${where} → ${sent} device${sent === 1 ? '' : 's'}`);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Start watching for turn endings. Safe to call unconditionally — with no
|
|
301
|
+
* subscribed devices this is one small local query every few seconds, and
|
|
302
|
+
* `PUSH_NOTIFY=off` skips it entirely.
|
|
303
|
+
*/
|
|
304
|
+
export function startNotifier(reads) {
|
|
305
|
+
if (!notificationsEnabled()) {
|
|
306
|
+
console.info('[push] notifications disabled (PUSH_NOTIFY=off)');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const timer = setInterval(() => {
|
|
310
|
+
try {
|
|
311
|
+
tick(reads);
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
// A transient DB read failure must not kill the interval.
|
|
315
|
+
console.warn(`[push] watcher tick failed: ${err instanceof Error ? err.message : err}`);
|
|
316
|
+
}
|
|
317
|
+
}, TICK_MS);
|
|
318
|
+
timer.unref();
|
|
319
|
+
}
|
package/dist-node/src/reads.js
CHANGED
|
@@ -4,6 +4,20 @@ import path from 'node:path';
|
|
|
4
4
|
import { describeRepoIcon, resolveRepoIcon } from "./icons.js";
|
|
5
5
|
import { parseMessage } from "./transcript.js";
|
|
6
6
|
const worktreeCache = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* Conductor's sidebar title for a workspace: manual name → PR title → humanized
|
|
9
|
+
* branch → worktree codename → id. Deliberately a second copy of the web's
|
|
10
|
+
* `workspaceLabel` (web/src/lib/format.ts, where the precedence is documented) —
|
|
11
|
+
* the relay can't import from the Vite root, and a notification that names a
|
|
12
|
+
* workspace differently from the list it came from is worse than the duplication.
|
|
13
|
+
*/
|
|
14
|
+
export function workspaceTitle(w) {
|
|
15
|
+
const branch = w.branch ?? '';
|
|
16
|
+
const slug = branch.includes('/') ? branch.slice(branch.indexOf('/') + 1) : branch;
|
|
17
|
+
const words = slug.replace(/[-_]/g, ' ').trim();
|
|
18
|
+
const humanized = words ? words[0].toUpperCase() + words.slice(1) : '';
|
|
19
|
+
return w.workspace_name || w.pr_title || humanized || w.directory_name || w.id.slice(0, 8);
|
|
20
|
+
}
|
|
7
21
|
/**
|
|
8
22
|
* Resolve a workspace's worktree path. Conductor lays worktrees out as
|
|
9
23
|
* `<workspacesRoot>/<repoName>/<directoryName>`, but we verify against
|
|
@@ -52,9 +66,33 @@ export class Reads {
|
|
|
52
66
|
this.db = db;
|
|
53
67
|
this.workspacesRoot = workspacesRoot;
|
|
54
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Unread chats, grouped by workspace. `workspaces.unread` is a **dead column** —
|
|
71
|
+
* Conductor still declares it (and its migration still says "when 1, the workspace
|
|
72
|
+
* should be marked visually as unread") but writes 0 on every row: 0 of 1691 are
|
|
73
|
+
* non-zero on this Mac, which is why the sidebar's bold/badge never once fired.
|
|
74
|
+
* The live flag is per session, and it *is* a flag — `unread_count` is only ever
|
|
75
|
+
* 0 or 1 — so what's worth counting is how many chats have news, not the column.
|
|
76
|
+
*/
|
|
77
|
+
unreadSessions() {
|
|
78
|
+
const rows = this.db.query(`SELECT workspace_id, id, updated_at
|
|
79
|
+
FROM sessions
|
|
80
|
+
WHERE COALESCE(unread_count, 0) > 0 AND COALESCE(is_hidden, 0) = 0`);
|
|
81
|
+
const byWorkspace = new Map();
|
|
82
|
+
for (const r of rows) {
|
|
83
|
+
if (!r.workspace_id)
|
|
84
|
+
continue;
|
|
85
|
+
const list = byWorkspace.get(r.workspace_id);
|
|
86
|
+
if (list)
|
|
87
|
+
list.push({ id: r.id, at: r.updated_at });
|
|
88
|
+
else
|
|
89
|
+
byWorkspace.set(r.workspace_id, [{ id: r.id, at: r.updated_at }]);
|
|
90
|
+
}
|
|
91
|
+
return byWorkspace;
|
|
92
|
+
}
|
|
55
93
|
listWorkspaces() {
|
|
56
94
|
const rows = this.db.query(`SELECT w.id, w.directory_name, w.workspace_name, w.branch, w.pr_title, w.derived_status, w.manual_status,
|
|
57
|
-
w.state, w.created_at, w.updated_at, w.
|
|
95
|
+
w.state, w.created_at, w.updated_at, w.pinned_at, w.active_session_id, w.intended_target_branch,
|
|
58
96
|
r.name AS repo_name, r.root_path AS repo_root, r.icon AS repo_icon,
|
|
59
97
|
r.remote_url AS remote_url, r.default_branch AS default_branch,
|
|
60
98
|
s.status AS session_status, s.title AS session_title, s.model AS model,
|
|
@@ -64,8 +102,10 @@ export class Reads {
|
|
|
64
102
|
LEFT JOIN sessions s ON s.id = w.active_session_id
|
|
65
103
|
WHERE w.state IN ('ready', 'setting_up')
|
|
66
104
|
ORDER BY (w.pinned_at IS NULL), w.updated_at DESC`);
|
|
105
|
+
const unread = this.unreadSessions();
|
|
67
106
|
return rows.map(r => ({
|
|
68
107
|
...r,
|
|
108
|
+
unread_sessions: unread.get(r.id) ?? [],
|
|
69
109
|
worktree: resolveWorktree(this.workspacesRoot, r.repo_name, r.directory_name, r.branch, r.repo_root),
|
|
70
110
|
baseBranch: r.intended_target_branch || r.default_branch || 'main',
|
|
71
111
|
icon: describeRepoIcon({ icon: r.repo_icon, repoRoot: r.repo_root, remoteUrl: r.remote_url })
|
|
@@ -108,6 +148,53 @@ export class Reads {
|
|
|
108
148
|
WHERE workspace_id = ? AND COALESCE(is_hidden, 0) = 0
|
|
109
149
|
ORDER BY created_at ASC`, [workspaceId]);
|
|
110
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Every live chat's status, across all workspaces — what the notifier watches for
|
|
153
|
+
* transitions. Unlike `listWorkspaces` this is not limited to the *active* session:
|
|
154
|
+
* a background tab finishing its turn is exactly the thing you want told about.
|
|
155
|
+
* `setting_up` workspaces are excluded; their chat isn't the user's turn yet.
|
|
156
|
+
*/
|
|
157
|
+
listSessionStates() {
|
|
158
|
+
const rows = this.db.query(`SELECT s.id, s.status, s.title, s.workspace_id,
|
|
159
|
+
w.workspace_name, w.pr_title, w.branch, w.directory_name,
|
|
160
|
+
r.name AS repo_name,
|
|
161
|
+
(SELECT COUNT(*) FROM sessions t WHERE t.workspace_id = w.id AND COALESCE(t.is_hidden, 0) = 0) AS tab_count
|
|
162
|
+
FROM sessions s
|
|
163
|
+
JOIN workspaces w ON w.id = s.workspace_id
|
|
164
|
+
LEFT JOIN repos r ON r.id = w.repository_id
|
|
165
|
+
WHERE w.state = 'ready' AND COALESCE(s.is_hidden, 0) = 0`);
|
|
166
|
+
return rows.map(r => ({
|
|
167
|
+
sessionId: r.id,
|
|
168
|
+
workspaceId: r.workspace_id,
|
|
169
|
+
status: r.status,
|
|
170
|
+
workspaceTitle: workspaceTitle({ ...r, id: r.workspace_id }),
|
|
171
|
+
repoName: r.repo_name,
|
|
172
|
+
// A single-tab workspace's chat title is just the workspace again — only name it when it disambiguates.
|
|
173
|
+
sessionTitle: r.tab_count > 1 ? r.title : null
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The last thing the agent actually said in a chat — the body of a "finished"
|
|
178
|
+
* notification. Reads the tail rather than the whole transcript, and runs the
|
|
179
|
+
* same parser the phone renders with, so what lands on the lock screen is the
|
|
180
|
+
* text that will be at the bottom of the chat when it's opened.
|
|
181
|
+
*/
|
|
182
|
+
lastAssistantText(sessionId) {
|
|
183
|
+
const rows = this.db.query(`SELECT rowid, id, role, content, full_message, created_at, sent_at, queue_order
|
|
184
|
+
FROM session_messages
|
|
185
|
+
WHERE session_id = ?
|
|
186
|
+
ORDER BY rowid DESC
|
|
187
|
+
LIMIT 20`, [sessionId]);
|
|
188
|
+
// Rows come back newest-first; the last assistant text is the first one found.
|
|
189
|
+
for (const row of rows) {
|
|
190
|
+
const entries = parseMessage(row, null);
|
|
191
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
192
|
+
if (entries[i].role === 'assistant' && entries[i].text.trim())
|
|
193
|
+
return entries[i].text.trim();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
111
198
|
/** Session → worktree path, cached: it's stable for a session's lifetime and polled every tick. */
|
|
112
199
|
worktreeBySession = new Map();
|
|
113
200
|
sessionWorktree(sessionId) {
|