@mjasnikovs/pi-task 0.14.18 → 0.14.20
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/dist/remote/ui-script.js
CHANGED
|
@@ -872,5 +872,32 @@ export function clientScript(wsUrl) {
|
|
|
872
872
|
window.addEventListener('online', connectNow);
|
|
873
873
|
window.addEventListener('focus', connectNow);
|
|
874
874
|
|
|
875
|
+
// Pin the column to window.innerHeight (a stable px value) instead of letting
|
|
876
|
+
// it ride 100dvh, which iOS Safari interpolates DURING the rotation animation
|
|
877
|
+
// and makes the whole layout resize repeatedly ("spazzing out"). A rotation
|
|
878
|
+
// also fires several resize events and changes scrollHeight, so coalesce the
|
|
879
|
+
// updates in one rAF and re-pin to the bottom when the user was already there.
|
|
880
|
+
let appHeightRaf = null;
|
|
881
|
+
let appHeightPin = false;
|
|
882
|
+
function setAppHeight() {
|
|
883
|
+
const h = (window.visualViewport && window.visualViewport.height) || window.innerHeight;
|
|
884
|
+
document.documentElement.style.setProperty('--app-h', h + 'px');
|
|
885
|
+
// Snapshot autoScroll synchronously, before the post-rotation reflow can fire
|
|
886
|
+
// a scroll event that flips it. When several resize events coalesce into one
|
|
887
|
+
// frame, the LATEST snapshot must win — otherwise a stale "was at bottom"
|
|
888
|
+
// capture (e.g. the load-time call, when the empty log counts as at-bottom)
|
|
889
|
+
// would yank a since-scrolled-up reader to the bottom on the next rotate.
|
|
890
|
+
appHeightPin = autoScroll;
|
|
891
|
+
if (appHeightRaf) return;
|
|
892
|
+
appHeightRaf = requestAnimationFrame(function () {
|
|
893
|
+
appHeightRaf = null;
|
|
894
|
+
if (appHeightPin) chatLog.scrollTop = chatLog.scrollHeight;
|
|
895
|
+
updateScrollBtn();
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
setAppHeight();
|
|
899
|
+
window.addEventListener('resize', setAppHeight);
|
|
900
|
+
window.addEventListener('orientationchange', setAppHeight);
|
|
901
|
+
|
|
875
902
|
connect();`;
|
|
876
903
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const STYLES = " :root {\n --base: #1e1e2e; --mantle: #181825; --crust: #11111b;\n --surface0: #313244; --surface1: #45475a; --surface2: #585b70;\n --text: #cdd6f4; --subtext1: #a6adc8; --subtext0: #7f849c;\n --mauve: #cba6f7; --blue: #89b4fa; --green: #a6e3a1; --red: #f38ba8;\n --yellow: #f9e2af; --peach: #fab387; --teal: #94e2d5;\n }\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n background: var(--base); color: var(--text);\n font-family: ui-monospace, monospace; height: 100dvh;\n display: flex; flex-direction: column; overflow: hidden;\n padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)\n 0px env(safe-area-inset-left, 0px);\n }\n #context-bar { height: 4px; background: var(--surface0); flex-shrink: 0; }\n #context-bar-fill { height: 100%; background: var(--mauve); width: 0%; transition: width 0.4s ease; }\n #header {\n background: var(--mantle); padding: 8px 16px;\n display: flex; justify-content: space-between; align-items: center;\n font-size: 13px; flex-shrink: 0; border-bottom: 1px solid var(--surface0);\n }\n #header .title { font-weight: bold; color: var(--mauve); letter-spacing: 0.05em;\n position: relative; animation: glitch 5s steps(1) infinite; }\n @keyframes glitch {\n 0%, 88%, 100% { text-shadow: none; transform: translate(0, 0); }\n 90% { text-shadow: -1px 0 var(--red), 1px 0 var(--teal); transform: translate(1px, -1px); }\n 92% { text-shadow: 1px 0 var(--red), -1px 0 var(--blue); transform: translate(-1px, 1px); }\n 94% { text-shadow: -1px 0 var(--blue), 1px 0 var(--red); transform: translate(1px, 0); }\n 96% { text-shadow: 1px 0 var(--teal), -1px 0 var(--red); transform: translate(-1px, 0); }\n }\n @media (prefers-reduced-motion: reduce) { #header .title { animation: none; } }\n #header .hgroup { display: flex; align-items: center; gap: 10px; }\n #bell {\n background: none; border: none; color: var(--subtext1); cursor: pointer;\n font-size: 15px; line-height: 1; padding: 2px; font-family: inherit;\n }\n #bell:hover { color: var(--text); }\n #bell.on { color: var(--mauve); }\n #chat-wrap { position: relative; flex: 1; min-height: 0; display: flex; }\n #chat-log {\n flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 16px;\n display: flex; flex-direction: column; gap: 8px;\n }\n /* Floating jump-to-latest button \u2014 only shown when scrolled away from the\n bottom (toggled via .visible from the scroll handler). */\n #scroll-bottom {\n display: none; position: absolute; bottom: 16px; right: 16px; z-index: 40;\n width: 36px; height: 36px; border-radius: 50%; cursor: pointer;\n background: var(--surface1); color: var(--text); border: 1px solid var(--surface2);\n font-family: inherit; font-size: 18px; line-height: 1; padding: 0;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n }\n #scroll-bottom:hover { background: var(--surface2); color: var(--mauve); }\n #scroll-bottom.visible { display: block; }\n #chat-log::-webkit-scrollbar { width: 6px; }\n #chat-log::-webkit-scrollbar-track { background: transparent; }\n #chat-log::-webkit-scrollbar-thumb { background: var(--surface2); border-radius: 3px; }\n .bubble {\n max-width: 82%; padding: 8px 12px; border-radius: 8px;\n line-height: 1.6; white-space: pre-wrap; word-break: break-word; font-size: 13px;\n }\n .bubble.user { background: var(--surface1); color: var(--text); align-self: flex-end; }\n .bubble.assistant { background: var(--surface0); color: var(--text); align-self: flex-start; }\n .bubble.error {\n background: var(--crust); color: var(--red); align-self: stretch;\n max-width: 100%; border: 1px solid var(--red); font-size: 12px;\n }\n /* Persistent inline system note (e.g. context compaction) \u2014 a muted centered\n divider, distinct from chat bubbles. */\n .sysnote {\n align-self: center; color: var(--subtext0); font-size: 11px;\n font-family: ui-monospace, monospace; letter-spacing: 0.5px;\n padding: 2px 10px; opacity: 0.85;\n }\n .bubble.thinking {\n display: flex; gap: 5px; align-items: center; padding: 10px 14px;\n }\n .bubble.thinking .spinner {\n color: var(--mauve); font-size: 15px; line-height: 1;\n font-family: ui-monospace, monospace;\n }\n .tool-call {\n background: var(--crust); border-radius: 6px; align-self: flex-start;\n max-width: 90%; font-size: 12px; border: 1px solid var(--surface0);\n }\n .tool-call summary {\n padding: 6px 10px; color: var(--subtext1); cursor: pointer;\n user-select: none; list-style: none;\n overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-call summary::-webkit-details-marker { display: none; }\n .tool-call summary::before { content: \"\u25B6 \"; }\n .tool-call[open] > summary::before { content: \"\u25BC \"; }\n .tool-call.error > summary { color: var(--red); }\n .tool-call pre {\n padding: 8px 12px; overflow-y: auto;\n color: var(--subtext1); font-size: 11px; max-height: 280px;\n border-top: 1px solid var(--surface0);\n white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-spin { color: var(--mauve); margin-left: 6px; font-family: ui-monospace, monospace; font-size: 13px; }\n .code-block {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 6px; overflow: hidden; margin: 4px 0;\n align-self: stretch; max-width: 100%; font-size: 12px;\n }\n .code-lang {\n background: var(--surface0); color: var(--subtext0);\n font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;\n }\n .code-block code {\n display: block; padding: 10px 12px; overflow-x: auto;\n color: var(--text); white-space: pre; line-height: 1.55;\n }\n .hl-kw { color: var(--mauve); }\n .hl-str { color: var(--green); }\n .hl-cmt { color: var(--subtext0); font-style: italic; }\n .hl-num { color: var(--blue); }\n .hl-fn { color: var(--yellow); }\n #input-bar {\n background: var(--mantle); padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));\n display: flex; gap: 8px; flex-shrink: 0;\n border-top: 1px solid var(--surface0);\n position: relative;\n }\n #cmd-suggestions {\n display: none; position: absolute; bottom: 100%; left: 16px; right: 16px;\n background: var(--mantle); border: 1px solid var(--surface1);\n border-bottom: none; border-radius: 8px 8px 0 0;\n overflow: hidden; z-index: 10;\n }\n .cmd-item {\n display: flex; align-items: baseline; gap: 10px;\n padding: 7px 12px; cursor: pointer; font-size: 12px;\n border-bottom: 1px solid var(--surface0);\n }\n .cmd-item:last-child { border-bottom: none; }\n .cmd-item:hover, .cmd-item.active { background: var(--surface0); }\n .cmd-item .cmd-name { color: var(--blue); font-weight: bold; flex-shrink: 0; }\n .cmd-item .cmd-desc { color: var(--subtext0); }\n #input {\n flex: 1; background: var(--surface0); color: var(--text);\n border: none; border-radius: 6px; padding: 8px 12px;\n font-family: inherit; font-size: 13px; resize: none;\n outline: none; line-height: 1.5; min-height: 36px; max-height: 120px;\n }\n #input::placeholder { color: var(--subtext0); }\n #input:focus { box-shadow: 0 0 0 1px var(--mauve); }\n #send {\n background: var(--blue); color: var(--crust); border: none;\n border-radius: 6px; padding: 8px 16px; font-weight: bold;\n cursor: pointer; font-size: 13px; font-family: inherit;\n white-space: nowrap; align-self: flex-end;\n }\n #send:disabled, #input:disabled { opacity: 0.45; cursor: not-allowed; }\n #reconnect-overlay {\n display: none; position: fixed; inset: 0;\n background: rgba(30,30,46,0.88); color: var(--subtext1);\n justify-content: center; align-items: center;\n font-size: 13px; z-index: 100; letter-spacing: 0.03em;\n }\n #reconnect-overlay.visible { display: flex; }\n /* Trailing stream indicator: the same braille spinner as the thinking bubble,\n inline at the end of the streaming text (not a green blinking block). */\n .cursor {\n color: var(--mauve); margin-left: 2px;\n font-family: ui-monospace, monospace;\n }\n #status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);\n color: var(--subtext1); white-space: pre-wrap; font-size: 13px; display: none; }\n #prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);\n border-top: 2px solid var(--mauve); padding: 16px 14px calc(16px + env(safe-area-inset-bottom, 0px));\n display: none; z-index: 50; max-height: 80dvh; overflow-y: auto; }\n #prompt-card .q-label { color: var(--mauve); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .6px; margin-bottom: 6px; }\n #prompt-card .q { color: var(--text); margin-bottom: 12px; white-space: pre-wrap;\n font-size: 15px; line-height: 1.5; }\n #prompt-card .rec-panel { background: var(--surface0); border-left: 3px solid var(--green);\n border-radius: 6px; padding: 10px 12px; margin-bottom: 12px; }\n #prompt-card .rec-label { color: var(--green); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px; }\n #prompt-card .rec-text { color: var(--text); font-size: 15px; line-height: 1.5;\n white-space: pre-wrap; overflow-wrap: anywhere; }\n #prompt-card textarea { width: 100%; background: var(--surface0); color: var(--text);\n border: 1px solid var(--surface2); border-radius: 6px; padding: 10px; font-size: 15px;\n font-family: inherit; line-height: 1.5; resize: vertical; margin-bottom: 4px; }\n #prompt-card .row { display: flex; gap: 8px; margin-top: 12px; align-items: stretch;\n flex-wrap: wrap; }\n /* Recommendation answers can be long sentences, so stack them as a readable list. */\n #prompt-card .row.stacked { flex-direction: column; align-items: stretch; }\n #prompt-card .row.stacked button { flex: none; text-align: left; }\n #prompt-card .row.stacked button.cancel { align-self: center; text-align: center; }\n #prompt-card button { padding: 11px 16px; border-radius: 8px; border: none; cursor: pointer;\n font-family: inherit; font-size: 14px; font-weight: 600; transition: filter .15s ease; }\n #prompt-card button:hover { filter: brightness(1.08); }\n #prompt-card button.primary { background: var(--green); color: var(--crust);\n font-weight: 700; flex: 1; min-width: 160px; }\n #prompt-card button.secondary { background: var(--surface1); color: var(--text);\n flex: 1; min-width: 160px; }\n #prompt-card button.cancel { margin-left: auto; align-self: center; background: transparent;\n color: var(--subtext0); font-size: 12px; font-weight: 500; padding: 8px 10px; }\n #prompt-card button.cancel:hover { color: var(--red); filter: none; }\n #prompt-card button.cancel.armed { background: var(--red); color: var(--crust); font-weight: 700; }\n .toast { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 12px);\n right: calc(env(safe-area-inset-right, 0px) + 12px); max-width: calc(100vw - 24px);\n padding: 8px 12px; border-radius: 6px; overflow-wrap: anywhere; word-break: break-word;\n background: var(--surface1); color: var(--text); z-index: 60; }\n .toast.warning { background: var(--peach); color: var(--crust); }\n .toast.error { background: var(--red); color: var(--crust); }\n #viewer { position: fixed; inset: 24px; background: var(--mantle); border: 1px solid var(--surface2);\n border-radius: 8px; padding: 16px; overflow: auto; white-space: pre-wrap;\n overflow-wrap: anywhere; word-break: break-word; display: none; z-index: 70; }\n #viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }";
|
|
1
|
+
export declare const STYLES = " :root {\n --base: #1e1e2e; --mantle: #181825; --crust: #11111b;\n --surface0: #313244; --surface1: #45475a; --surface2: #585b70;\n --text: #cdd6f4; --subtext1: #a6adc8; --subtext0: #7f849c;\n --mauve: #cba6f7; --blue: #89b4fa; --green: #a6e3a1; --red: #f38ba8;\n --yellow: #f9e2af; --peach: #fab387; --teal: #94e2d5;\n }\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n background: var(--base); color: var(--text);\n font-family: ui-monospace, monospace;\n /* --app-h is set from window.innerHeight (see setAppHeight) so the column\n height is a stable pixel value across an orientation change. 100dvh is a\n fallback for first paint / no-JS: iOS Safari interpolates dvh during the\n rotation animation, which makes the whole flex column resize repeatedly\n (\"spazzing out\") \u2014 a fixed px height does not. */\n height: var(--app-h, 100dvh);\n display: flex; flex-direction: column; overflow: hidden;\n padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)\n 0px env(safe-area-inset-left, 0px);\n }\n #context-bar { height: 4px; background: var(--surface0); flex-shrink: 0; }\n #context-bar-fill { height: 100%; background: var(--mauve); width: 0%; transition: width 0.4s ease; }\n #header {\n background: var(--mantle); padding: 8px 16px;\n display: flex; justify-content: space-between; align-items: center;\n font-size: 13px; flex-shrink: 0; border-bottom: 1px solid var(--surface0);\n }\n #header .title { font-weight: bold; color: var(--mauve); letter-spacing: 0.05em;\n position: relative; animation: glitch 5s steps(1) infinite; }\n @keyframes glitch {\n 0%, 88%, 100% { text-shadow: none; transform: translate(0, 0); }\n 90% { text-shadow: -1px 0 var(--red), 1px 0 var(--teal); transform: translate(1px, -1px); }\n 92% { text-shadow: 1px 0 var(--red), -1px 0 var(--blue); transform: translate(-1px, 1px); }\n 94% { text-shadow: -1px 0 var(--blue), 1px 0 var(--red); transform: translate(1px, 0); }\n 96% { text-shadow: 1px 0 var(--teal), -1px 0 var(--red); transform: translate(-1px, 0); }\n }\n @media (prefers-reduced-motion: reduce) { #header .title { animation: none; } }\n #header .hgroup { display: flex; align-items: center; gap: 10px; }\n #bell {\n background: none; border: none; color: var(--subtext1); cursor: pointer;\n font-size: 15px; line-height: 1; padding: 2px; font-family: inherit;\n }\n #bell:hover { color: var(--text); }\n #bell.on { color: var(--mauve); }\n #chat-wrap { position: relative; flex: 1; min-height: 0; display: flex; }\n #chat-log {\n flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 16px;\n display: flex; flex-direction: column; gap: 8px;\n }\n /* Floating jump-to-latest button \u2014 only shown when scrolled away from the\n bottom (toggled via .visible from the scroll handler). */\n #scroll-bottom {\n display: none; position: absolute; bottom: 16px; right: 16px; z-index: 40;\n width: 36px; height: 36px; border-radius: 50%; cursor: pointer;\n background: var(--surface1); color: var(--text); border: 1px solid var(--surface2);\n font-family: inherit; font-size: 18px; line-height: 1; padding: 0;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n }\n #scroll-bottom:hover { background: var(--surface2); color: var(--mauve); }\n #scroll-bottom.visible { display: block; }\n #chat-log::-webkit-scrollbar { width: 6px; }\n #chat-log::-webkit-scrollbar-track { background: transparent; }\n #chat-log::-webkit-scrollbar-thumb { background: var(--surface2); border-radius: 3px; }\n .bubble {\n max-width: 82%; padding: 8px 12px; border-radius: 8px;\n line-height: 1.6; white-space: pre-wrap; word-break: break-word; font-size: 13px;\n }\n .bubble.user { background: var(--surface1); color: var(--text); align-self: flex-end; }\n .bubble.assistant { background: var(--surface0); color: var(--text); align-self: flex-start; }\n .bubble.error {\n background: var(--crust); color: var(--red); align-self: stretch;\n max-width: 100%; border: 1px solid var(--red); font-size: 12px;\n }\n /* Persistent inline system note (e.g. context compaction) \u2014 a muted centered\n divider, distinct from chat bubbles. */\n .sysnote {\n align-self: center; color: var(--subtext0); font-size: 11px;\n font-family: ui-monospace, monospace; letter-spacing: 0.5px;\n padding: 2px 10px; opacity: 0.85;\n }\n .bubble.thinking {\n display: flex; gap: 5px; align-items: center; padding: 10px 14px;\n }\n .bubble.thinking .spinner {\n color: var(--mauve); font-size: 15px; line-height: 1;\n font-family: ui-monospace, monospace;\n }\n .tool-call {\n background: var(--crust); border-radius: 6px; align-self: flex-start;\n max-width: 90%; font-size: 12px; border: 1px solid var(--surface0);\n }\n .tool-call summary {\n padding: 6px 10px; color: var(--subtext1); cursor: pointer;\n user-select: none; list-style: none;\n overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-call summary::-webkit-details-marker { display: none; }\n .tool-call summary::before { content: \"\u25B6 \"; }\n .tool-call[open] > summary::before { content: \"\u25BC \"; }\n .tool-call.error > summary { color: var(--red); }\n .tool-call pre {\n padding: 8px 12px; overflow-y: auto;\n color: var(--subtext1); font-size: 11px; max-height: 280px;\n border-top: 1px solid var(--surface0);\n white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-spin { color: var(--mauve); margin-left: 6px; font-family: ui-monospace, monospace; font-size: 13px; }\n .code-block {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 6px; overflow: hidden; margin: 4px 0;\n align-self: stretch; max-width: 100%; font-size: 12px;\n }\n .code-lang {\n background: var(--surface0); color: var(--subtext0);\n font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;\n }\n .code-block code {\n display: block; padding: 10px 12px; overflow-x: auto;\n color: var(--text); white-space: pre; line-height: 1.55;\n }\n .hl-kw { color: var(--mauve); }\n .hl-str { color: var(--green); }\n .hl-cmt { color: var(--subtext0); font-style: italic; }\n .hl-num { color: var(--blue); }\n .hl-fn { color: var(--yellow); }\n #input-bar {\n background: var(--mantle); padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));\n display: flex; gap: 8px; flex-shrink: 0;\n border-top: 1px solid var(--surface0);\n position: relative;\n }\n #cmd-suggestions {\n display: none; position: absolute; bottom: 100%; left: 16px; right: 16px;\n background: var(--mantle); border: 1px solid var(--surface1);\n border-bottom: none; border-radius: 8px 8px 0 0;\n overflow: hidden; z-index: 10;\n }\n .cmd-item {\n display: flex; align-items: baseline; gap: 10px;\n padding: 7px 12px; cursor: pointer; font-size: 12px;\n border-bottom: 1px solid var(--surface0);\n }\n .cmd-item:last-child { border-bottom: none; }\n .cmd-item:hover, .cmd-item.active { background: var(--surface0); }\n .cmd-item .cmd-name { color: var(--blue); font-weight: bold; flex-shrink: 0; }\n .cmd-item .cmd-desc { color: var(--subtext0); }\n #input {\n flex: 1; background: var(--surface0); color: var(--text);\n border: none; border-radius: 6px; padding: 8px 12px;\n font-family: inherit; font-size: 13px; resize: none;\n outline: none; line-height: 1.5; min-height: 36px; max-height: 120px;\n }\n #input::placeholder { color: var(--subtext0); }\n #input:focus { box-shadow: 0 0 0 1px var(--mauve); }\n #send {\n background: var(--blue); color: var(--crust); border: none;\n border-radius: 6px; padding: 8px 16px; font-weight: bold;\n cursor: pointer; font-size: 13px; font-family: inherit;\n white-space: nowrap; align-self: flex-end;\n }\n #send:disabled, #input:disabled { opacity: 0.45; cursor: not-allowed; }\n #reconnect-overlay {\n display: none; position: fixed; inset: 0;\n background: rgba(30,30,46,0.88); color: var(--subtext1);\n justify-content: center; align-items: center;\n font-size: 13px; z-index: 100; letter-spacing: 0.03em;\n }\n #reconnect-overlay.visible { display: flex; }\n /* Trailing stream indicator: the same braille spinner as the thinking bubble,\n inline at the end of the streaming text (not a green blinking block). */\n .cursor {\n color: var(--mauve); margin-left: 2px;\n font-family: ui-monospace, monospace;\n }\n #status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);\n color: var(--subtext1); white-space: pre-wrap; font-size: 13px; display: none; }\n #prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);\n border-top: 2px solid var(--mauve); padding: 16px 14px calc(16px + env(safe-area-inset-bottom, 0px));\n display: none; z-index: 50; max-height: 80dvh; overflow-y: auto; }\n #prompt-card .q-label { color: var(--mauve); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .6px; margin-bottom: 6px; }\n #prompt-card .q { color: var(--text); margin-bottom: 12px; white-space: pre-wrap;\n font-size: 15px; line-height: 1.5; }\n #prompt-card .rec-panel { background: var(--surface0); border-left: 3px solid var(--green);\n border-radius: 6px; padding: 10px 12px; margin-bottom: 12px; }\n #prompt-card .rec-label { color: var(--green); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px; }\n #prompt-card .rec-text { color: var(--text); font-size: 15px; line-height: 1.5;\n white-space: pre-wrap; overflow-wrap: anywhere; }\n #prompt-card textarea { width: 100%; background: var(--surface0); color: var(--text);\n border: 1px solid var(--surface2); border-radius: 6px; padding: 10px; font-size: 15px;\n font-family: inherit; line-height: 1.5; resize: vertical; margin-bottom: 4px; }\n #prompt-card .row { display: flex; gap: 8px; margin-top: 12px; align-items: stretch;\n flex-wrap: wrap; }\n /* Recommendation answers can be long sentences, so stack them as a readable list. */\n #prompt-card .row.stacked { flex-direction: column; align-items: stretch; }\n #prompt-card .row.stacked button { flex: none; text-align: left; }\n #prompt-card .row.stacked button.cancel { align-self: center; text-align: center; }\n #prompt-card button { padding: 11px 16px; border-radius: 8px; border: none; cursor: pointer;\n font-family: inherit; font-size: 14px; font-weight: 600; transition: filter .15s ease; }\n #prompt-card button:hover { filter: brightness(1.08); }\n #prompt-card button.primary { background: var(--green); color: var(--crust);\n font-weight: 700; flex: 1; min-width: 160px; }\n #prompt-card button.secondary { background: var(--surface1); color: var(--text);\n flex: 1; min-width: 160px; }\n #prompt-card button.cancel { margin-left: auto; align-self: center; background: transparent;\n color: var(--subtext0); font-size: 12px; font-weight: 500; padding: 8px 10px; }\n #prompt-card button.cancel:hover { color: var(--red); filter: none; }\n #prompt-card button.cancel.armed { background: var(--red); color: var(--crust); font-weight: 700; }\n .toast { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 12px);\n right: calc(env(safe-area-inset-right, 0px) + 12px); max-width: calc(100vw - 24px);\n padding: 8px 12px; border-radius: 6px; overflow-wrap: anywhere; word-break: break-word;\n background: var(--surface1); color: var(--text); z-index: 60; }\n .toast.warning { background: var(--peach); color: var(--crust); }\n .toast.error { background: var(--red); color: var(--crust); }\n #viewer { position: fixed; inset: 24px; background: var(--mantle); border: 1px solid var(--surface2);\n border-radius: 8px; padding: 16px; overflow: auto; white-space: pre-wrap;\n overflow-wrap: anywhere; word-break: break-word; display: none; z-index: 70; }\n #viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }";
|
package/dist/remote/ui-styles.js
CHANGED
|
@@ -8,7 +8,13 @@ export const STYLES = ` :root {
|
|
|
8
8
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
9
|
body {
|
|
10
10
|
background: var(--base); color: var(--text);
|
|
11
|
-
font-family: ui-monospace, monospace;
|
|
11
|
+
font-family: ui-monospace, monospace;
|
|
12
|
+
/* --app-h is set from window.innerHeight (see setAppHeight) so the column
|
|
13
|
+
height is a stable pixel value across an orientation change. 100dvh is a
|
|
14
|
+
fallback for first paint / no-JS: iOS Safari interpolates dvh during the
|
|
15
|
+
rotation animation, which makes the whole flex column resize repeatedly
|
|
16
|
+
("spazzing out") — a fixed px height does not. */
|
|
17
|
+
height: var(--app-h, 100dvh);
|
|
12
18
|
display: flex; flex-direction: column; overflow: hidden;
|
|
13
19
|
padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)
|
|
14
20
|
0px env(safe-area-inset-left, 0px);
|
|
@@ -9,6 +9,10 @@ import { type SpawnFn, type ContextSnapshot, type ToolCall, type LoopHit } from
|
|
|
9
9
|
export declare const LOOP_WINDOW = 20;
|
|
10
10
|
export declare const LOOP_THRESHOLD = 5;
|
|
11
11
|
export declare const MAX_LOOP_RESTARTS = 2;
|
|
12
|
+
export declare function isConnectionError(cause: string): boolean;
|
|
13
|
+
/** Exponential backoff before a connection-error retry: 500ms, 1s, 2s, …, so a
|
|
14
|
+
* brief saturation window can drain before we re-issue the request. */
|
|
15
|
+
export declare function connectionRetryBackoffMs(attempt: number): number;
|
|
12
16
|
export interface PhaseRunResult {
|
|
13
17
|
text: string;
|
|
14
18
|
exitCode: number;
|
|
@@ -42,6 +46,9 @@ interface PhaseDeps {
|
|
|
42
46
|
spawn?: SpawnFn;
|
|
43
47
|
/** Write a timestamped line to the per-task debug log. Fire-and-forget. */
|
|
44
48
|
logDebug?: (msg: string) => void;
|
|
49
|
+
/** Injectable delay for connection-error backoff; defaults to a real timer.
|
|
50
|
+
* Tests override it with a no-op so retries don't actually sleep. */
|
|
51
|
+
sleepFor?: (ms: number) => Promise<void>;
|
|
45
52
|
}
|
|
46
53
|
export type { PhaseDeps };
|
|
47
54
|
/**
|
|
@@ -17,6 +17,32 @@ import { readSection, setTaskSection } from './task-io.js';
|
|
|
17
17
|
export const LOOP_WINDOW = 20;
|
|
18
18
|
export const LOOP_THRESHOLD = 5;
|
|
19
19
|
export const MAX_LOOP_RESTARTS = 2; // 3 strikes total (initial attempt + 2 restarts)
|
|
20
|
+
// MAX_LEAK_RETRIES lives in shared/leaked-tool-call.ts (imported above).
|
|
21
|
+
// ─── Connection-error retry ──────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* A connection-class model error is transient: a single dropped fetch to a live
|
|
24
|
+
* endpoint, not a repeatable mistake. On a local single-slot server (e.g.
|
|
25
|
+
* llama-server with `--parallel 1`) pi-task's own concurrent fan-out can briefly
|
|
26
|
+
* saturate the slot, and one request fails to connect even though the model is
|
|
27
|
+
* up and the next request succeeds. pi already retries internally, but those
|
|
28
|
+
* retries don't always absorb it on a saturated local server — and pi-task's
|
|
29
|
+
* fail-fast then kills the whole task (and, under /task-auto, the whole run) for
|
|
30
|
+
* a single blip. We retry these within the existing strike/leak budget.
|
|
31
|
+
*
|
|
32
|
+
* A NON-connection model error (bad request, context-length overflow, auth,
|
|
33
|
+
* provider 5xx that names a real fault) still fails fast: re-spawning against
|
|
34
|
+
* the same request won't fix it, so burning the budget only delays the report.
|
|
35
|
+
*/
|
|
36
|
+
const CONNECTION_ERROR_RE = /\b(?:connection error|connection (?:lost|closed|reset|refused|aborted)|econnreset|econnrefused|econnaborted|epipe|etimedout|enetunreach|enetdown|eai_again|socket hang up|fetch failed|network (?:error|timeout)|premature close|request timed out|terminated)\b/i;
|
|
37
|
+
export function isConnectionError(cause) {
|
|
38
|
+
return CONNECTION_ERROR_RE.test(cause);
|
|
39
|
+
}
|
|
40
|
+
/** Exponential backoff before a connection-error retry: 500ms, 1s, 2s, …, so a
|
|
41
|
+
* brief saturation window can drain before we re-issue the request. */
|
|
42
|
+
export function connectionRetryBackoffMs(attempt) {
|
|
43
|
+
return 500 * 2 ** attempt;
|
|
44
|
+
}
|
|
45
|
+
const defaultSleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
20
46
|
// ─── Spawn helpers ───────────────────────────────────────────────────────────
|
|
21
47
|
export function childArgs(tools, prompt) {
|
|
22
48
|
// `--mode json` puts the child into the structured event stream the
|
|
@@ -92,8 +118,16 @@ export async function runPhaseChild(deps, name, tools, prompt) {
|
|
|
92
118
|
throw new Error(`${name} child failed: ${r.stderr || '(no stderr)'}`);
|
|
93
119
|
}
|
|
94
120
|
if (r.modelError) {
|
|
95
|
-
// The model/provider failed (pi exited 0 with
|
|
96
|
-
// turn).
|
|
121
|
+
// The model/provider failed (pi exited 0 with a stopReason "error"
|
|
122
|
+
// turn). A connection-class cause is transient — retry within the
|
|
123
|
+
// leak budget after a backoff; anything else fails fast (pi already
|
|
124
|
+
// retried, and re-spawning won't fix a real fault).
|
|
125
|
+
if (isConnectionError(r.modelError) && attempt < MAX_LEAK_RETRIES) {
|
|
126
|
+
deps.logDebug?.(`${name}: connection error "${r.modelError}" — retry `
|
|
127
|
+
+ `${attempt + 1}/${MAX_LEAK_RETRIES}`);
|
|
128
|
+
await (deps.sleepFor ?? defaultSleep)(connectionRetryBackoffMs(attempt));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
97
131
|
throw new ModelError(name, r.modelError);
|
|
98
132
|
}
|
|
99
133
|
if (r.text.trim().length === 0) {
|
|
@@ -169,7 +203,15 @@ export async function runPhaseWithLoopGuard(deps, name, tools, buildPrompt) {
|
|
|
169
203
|
}
|
|
170
204
|
if (r.modelError) {
|
|
171
205
|
// The model/provider failed (pi exited 0 with a stopReason "error"
|
|
172
|
-
// turn).
|
|
206
|
+
// turn). A connection-class cause is transient — restart within the
|
|
207
|
+
// strike budget after a backoff; anything else fails fast (pi already
|
|
208
|
+
// retried, and re-spawning won't fix a real fault).
|
|
209
|
+
if (isConnectionError(r.modelError) && strike < MAX_LOOP_RESTARTS) {
|
|
210
|
+
deps.logDebug?.(`${name}: connection error "${r.modelError}" — restart `
|
|
211
|
+
+ `${strike + 1}/${MAX_LOOP_RESTARTS}`);
|
|
212
|
+
await (deps.sleepFor ?? defaultSleep)(connectionRetryBackoffMs(strike));
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
173
215
|
throw new ModelError(name, r.modelError);
|
|
174
216
|
}
|
|
175
217
|
if (r.text.trim().length === 0) {
|
|
@@ -62,7 +62,14 @@ function escapeRegex(s) {
|
|
|
62
62
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
63
63
|
}
|
|
64
64
|
export function sectionRegex(heading) {
|
|
65
|
-
|
|
65
|
+
// Group 1 captures ONLY the heading line — `[ \t]*\n`, not `\s*\n`. A greedy
|
|
66
|
+
// `\s*` swallows every blank line after the heading into the capture, and
|
|
67
|
+
// setTaskSection re-emits that capture verbatim plus one more `\n`, so each
|
|
68
|
+
// rewrite grew the gap by a blank line (the "big empty gap" under `## tasks`
|
|
69
|
+
// after a long /task-auto run). Keeping blanks in group 2 lets the `.trim()`
|
|
70
|
+
// every reader already applies collapse them, which also self-heals files
|
|
71
|
+
// that already accumulated the gap.
|
|
72
|
+
return new RegExp(`(^## ${escapeRegex(heading)}[ \\t]*\\n)([\\s\\S]*?)(?=^## |$(?![\\s\\S]))`, 'm');
|
|
66
73
|
}
|
|
67
74
|
export function extractSection(body, heading) {
|
|
68
75
|
const m = sectionRegex(heading).exec(body);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.20",
|
|
4
4
|
"description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|