@firstpick/pi-package-webui 0.5.3 → 0.5.4
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/package.json +1 -1
- package/public/app.js +146 -13
- package/tests/mobile-static.test.mjs +10 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firstpick/pi-package-webui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Pi Web UI companion package with a local browser UI CLI plus /webui-start and /webui-status commands.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/Firstp1ck/npm-packages/tree/main/pi-package-webui#readme",
|
package/public/app.js
CHANGED
|
@@ -516,6 +516,9 @@ const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
|
|
|
516
516
|
const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
|
|
517
517
|
const TOOL_LIVE_UPDATE_THROTTLE_MS = 80;
|
|
518
518
|
const UNEXPOSED_THINKING_TEXT = "No thinking content was exposed by the provider.";
|
|
519
|
+
const THINKING_FORMAT_OPEN_TAG_REGEX = /^<think\b[^>]*>/i;
|
|
520
|
+
const THINKING_FORMAT_CLOSE_TAG_REGEX = /<\/think\s*>/i;
|
|
521
|
+
const CHANNEL_THINKING_FORMAT_OPEN_TAG_REGEX = /^<\|([a-z][\w-]*)>/i;
|
|
519
522
|
const TODO_PROGRESS_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)\]\s+.+$/;
|
|
520
523
|
const TODO_PROGRESS_PARTIAL_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)?\]?\s*.*$/;
|
|
521
524
|
const CHAT_SCROLL_KEYS = new Set(["ArrowDown", "ArrowUp", "End", "Home", "PageDown", "PageUp", " "]);
|
|
@@ -13446,6 +13449,89 @@ function visibleThinkingText(text) {
|
|
|
13446
13449
|
return value;
|
|
13447
13450
|
}
|
|
13448
13451
|
|
|
13452
|
+
function escapeRegExp(value) {
|
|
13453
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13454
|
+
}
|
|
13455
|
+
|
|
13456
|
+
function isPartialThinkingFormatOpenTag(text) {
|
|
13457
|
+
const value = String(text || "").trimStart().toLowerCase();
|
|
13458
|
+
if (!value) return false;
|
|
13459
|
+
if ("<think>".startsWith(value)) return true;
|
|
13460
|
+
if (value === "<|" || /^<\|[a-z][\w-]*$/i.test(value)) return true;
|
|
13461
|
+
return /^<think\b[^>]*$/i.test(value);
|
|
13462
|
+
}
|
|
13463
|
+
|
|
13464
|
+
function stripPartialThinkingFormatClose(text, closeTag = "</think>") {
|
|
13465
|
+
const value = String(text || "");
|
|
13466
|
+
const lower = value.toLowerCase();
|
|
13467
|
+
const expected = String(closeTag || "").toLowerCase();
|
|
13468
|
+
const start = lower.lastIndexOf("<");
|
|
13469
|
+
if (start < 0) return value;
|
|
13470
|
+
const partial = lower.slice(start).trimEnd();
|
|
13471
|
+
return expected.startsWith(partial) ? value.slice(0, start) : value;
|
|
13472
|
+
}
|
|
13473
|
+
|
|
13474
|
+
function stripThinkingFormatOutputSeparator(text) {
|
|
13475
|
+
return String(text || "").replace(/^(?:[ \t]*\r?\n)+/, "").replace(/^[ \t]+/, "");
|
|
13476
|
+
}
|
|
13477
|
+
|
|
13478
|
+
function joinedThinkingFormatParts(parts) {
|
|
13479
|
+
return parts.map((part) => String(part || "")).filter((part) => part.length > 0).join("\n\n");
|
|
13480
|
+
}
|
|
13481
|
+
|
|
13482
|
+
function thinkingFormatOpenMatch(text) {
|
|
13483
|
+
const value = String(text || "");
|
|
13484
|
+
const think = THINKING_FORMAT_OPEN_TAG_REGEX.exec(value);
|
|
13485
|
+
if (think) return { raw: think[0], closeRegex: THINKING_FORMAT_CLOSE_TAG_REGEX, closeTag: "</think>" };
|
|
13486
|
+
const channel = CHANNEL_THINKING_FORMAT_OPEN_TAG_REGEX.exec(value);
|
|
13487
|
+
if (!channel) return null;
|
|
13488
|
+
const name = channel[1];
|
|
13489
|
+
return { raw: channel[0], closeRegex: new RegExp(`<${escapeRegExp(name)}\\|>`, "i"), closeTag: `<${name}|>` };
|
|
13490
|
+
}
|
|
13491
|
+
|
|
13492
|
+
function splitThinkingFormatText(text, { streaming = false } = {}) {
|
|
13493
|
+
let rest = String(text ?? "").trimStart();
|
|
13494
|
+
if (!rest) return null;
|
|
13495
|
+
if (!thinkingFormatOpenMatch(rest)) {
|
|
13496
|
+
return streaming && isPartialThinkingFormatOpenTag(rest)
|
|
13497
|
+
? { hasThinkingFormat: true, thinkingText: "", finalText: "", complete: false }
|
|
13498
|
+
: null;
|
|
13499
|
+
}
|
|
13500
|
+
|
|
13501
|
+
const thinkingParts = [];
|
|
13502
|
+
let open = thinkingFormatOpenMatch(rest);
|
|
13503
|
+
while (open) {
|
|
13504
|
+
const afterOpen = rest.slice(open.raw.length);
|
|
13505
|
+
const close = open.closeRegex.exec(afterOpen);
|
|
13506
|
+
if (!close) {
|
|
13507
|
+
thinkingParts.push(streaming ? stripPartialThinkingFormatClose(afterOpen, open.closeTag) : afterOpen);
|
|
13508
|
+
return { hasThinkingFormat: true, thinkingText: joinedThinkingFormatParts(thinkingParts), finalText: "", complete: false };
|
|
13509
|
+
}
|
|
13510
|
+
|
|
13511
|
+
thinkingParts.push(afterOpen.slice(0, close.index));
|
|
13512
|
+
rest = afterOpen.slice(close.index + close[0].length);
|
|
13513
|
+
const next = rest.trimStart();
|
|
13514
|
+
open = thinkingFormatOpenMatch(next);
|
|
13515
|
+
if (open) {
|
|
13516
|
+
rest = next;
|
|
13517
|
+
continue;
|
|
13518
|
+
}
|
|
13519
|
+
break;
|
|
13520
|
+
}
|
|
13521
|
+
|
|
13522
|
+
return {
|
|
13523
|
+
hasThinkingFormat: true,
|
|
13524
|
+
thinkingText: joinedThinkingFormatParts(thinkingParts),
|
|
13525
|
+
finalText: stripThinkingFormatOutputSeparator(rest),
|
|
13526
|
+
complete: true,
|
|
13527
|
+
};
|
|
13528
|
+
}
|
|
13529
|
+
|
|
13530
|
+
function appendThinkingFormatDisplayMessages(displayMessages, base, parsed) {
|
|
13531
|
+
const thinking = visibleThinkingText(parsed?.thinkingText || "");
|
|
13532
|
+
if (thinking) displayMessages.push({ ...base, role: "thinking", title: "thinking", content: thinking, thinking });
|
|
13533
|
+
}
|
|
13534
|
+
|
|
13449
13535
|
function isAssistantToolCallPart(part) {
|
|
13450
13536
|
return !!(part && typeof part === "object" && (part.type === "toolCall" || part.toolCall));
|
|
13451
13537
|
}
|
|
@@ -13495,6 +13581,13 @@ function assistantDisplayMessages(message) {
|
|
|
13495
13581
|
const base = { timestamp: message.timestamp };
|
|
13496
13582
|
const content = message.content;
|
|
13497
13583
|
if (typeof content === "string") {
|
|
13584
|
+
const parsed = splitThinkingFormatText(content);
|
|
13585
|
+
if (parsed?.hasThinkingFormat) {
|
|
13586
|
+
const displayMessages = [];
|
|
13587
|
+
appendThinkingFormatDisplayMessages(displayMessages, base, parsed);
|
|
13588
|
+
if (parsed.finalText.trim()) displayMessages.push({ ...message, title: "final output", content: parsed.finalText });
|
|
13589
|
+
return displayMessages;
|
|
13590
|
+
}
|
|
13498
13591
|
return content.trim() ? [{ ...message, title: "final output" }] : [];
|
|
13499
13592
|
}
|
|
13500
13593
|
if (!Array.isArray(content)) {
|
|
@@ -13518,6 +13611,16 @@ function assistantDisplayMessages(message) {
|
|
|
13518
13611
|
displayMessages.push({ ...base, role: "toolCall", title: `tool call: ${toolName}`, toolName, toolCallId, arguments: args, content: args });
|
|
13519
13612
|
continue;
|
|
13520
13613
|
}
|
|
13614
|
+
const primitiveText = part !== undefined && part !== null && typeof part !== "object" ? String(part) : "";
|
|
13615
|
+
const textForThinkingFormat = primitiveText || (part && typeof part === "object" && (part.type === "text" || typeof part.text === "string") ? assistantTextPartText(part) || part.text : "");
|
|
13616
|
+
if (textForThinkingFormat) {
|
|
13617
|
+
const parsed = splitThinkingFormatText(textForThinkingFormat);
|
|
13618
|
+
if (parsed?.hasThinkingFormat) {
|
|
13619
|
+
appendThinkingFormatDisplayMessages(displayMessages, base, parsed);
|
|
13620
|
+
if (parsed.finalText.trim() && !assistantHasToolCallAfter(content, index)) finalParts.push(part && typeof part === "object" ? { ...part, type: "text", text: parsed.finalText } : { type: "text", text: parsed.finalText });
|
|
13621
|
+
continue;
|
|
13622
|
+
}
|
|
13623
|
+
}
|
|
13521
13624
|
const finalPart = assistantFinalOutputPart(part);
|
|
13522
13625
|
if (finalPart) {
|
|
13523
13626
|
if (!assistantHasToolCallAfter(content, index)) finalParts.push(finalPart);
|
|
@@ -16683,6 +16786,12 @@ function removeStreamBubble() {
|
|
|
16683
16786
|
renderRunIndicator({ scroll: false });
|
|
16684
16787
|
}
|
|
16685
16788
|
|
|
16789
|
+
function streamRenderableAssistantText() {
|
|
16790
|
+
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
16791
|
+
const parsed = splitThinkingFormatText(assistantText, { streaming: true });
|
|
16792
|
+
return parsed?.hasThinkingFormat ? stripTodoProgressLines(parsed.finalText, { streaming: true }) : assistantText;
|
|
16793
|
+
}
|
|
16794
|
+
|
|
16686
16795
|
function scheduleStreamBubbleHide() {
|
|
16687
16796
|
if (!streamBubble) return;
|
|
16688
16797
|
const visibleForMs = streamBubbleVisibleSince ? performance.now() - streamBubbleVisibleSince : STREAM_OUTPUT_MIN_VISIBLE_MS;
|
|
@@ -16690,16 +16799,27 @@ function scheduleStreamBubbleHide() {
|
|
|
16690
16799
|
clearTimeout(streamBubbleHideTimer);
|
|
16691
16800
|
streamBubbleHideTimer = setTimeout(() => {
|
|
16692
16801
|
streamBubbleHideTimer = null;
|
|
16693
|
-
if (
|
|
16802
|
+
if (streamRenderableAssistantText() || !streamBubble) return;
|
|
16694
16803
|
removeStreamBubble();
|
|
16695
16804
|
}, delayMs);
|
|
16696
16805
|
}
|
|
16697
16806
|
|
|
16807
|
+
function syncStreamingThinkingFormat(assistantText) {
|
|
16808
|
+
const parsed = splitThinkingFormatText(assistantText, { streaming: true });
|
|
16809
|
+
if (!parsed?.hasThinkingFormat) return null;
|
|
16810
|
+
const thinking = visibleThinkingText(parsed.thinkingText);
|
|
16811
|
+
if (thinking) setStreamingThinkingText(thinking);
|
|
16812
|
+
if (parsed.complete && streamThinkingBubble) streamThinkingBubble.classList.add("complete");
|
|
16813
|
+
return parsed;
|
|
16814
|
+
}
|
|
16815
|
+
|
|
16698
16816
|
function renderStreamingAssistantText() {
|
|
16699
16817
|
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
16700
|
-
|
|
16818
|
+
const thinkingFormat = syncStreamingThinkingFormat(assistantText);
|
|
16819
|
+
const finalText = thinkingFormat?.hasThinkingFormat ? stripTodoProgressLines(thinkingFormat.finalText, { streaming: true }) : assistantText;
|
|
16820
|
+
if (finalText) {
|
|
16701
16821
|
ensureStreamBubble();
|
|
16702
|
-
renderStreamingMarkdown(streamText,
|
|
16822
|
+
renderStreamingMarkdown(streamText, finalText);
|
|
16703
16823
|
} else {
|
|
16704
16824
|
scheduleStreamBubbleHide();
|
|
16705
16825
|
}
|
|
@@ -16793,26 +16913,39 @@ function assistantStreamingMessage(event) {
|
|
|
16793
16913
|
return partial?.role === "assistant" ? partial : null;
|
|
16794
16914
|
}
|
|
16795
16915
|
|
|
16796
|
-
function assistantTextFromMessage(message) {
|
|
16916
|
+
function assistantTextFromMessage(message, { streaming = false } = {}) {
|
|
16917
|
+
void streaming;
|
|
16797
16918
|
const content = message?.content;
|
|
16798
16919
|
if (typeof content === "string") return content;
|
|
16799
16920
|
if (!Array.isArray(content)) return null;
|
|
16800
16921
|
const parts = [];
|
|
16801
16922
|
for (let index = 0; index < content.length; index += 1) {
|
|
16802
16923
|
const part = content[index];
|
|
16803
|
-
const text = assistantTextPartText(part);
|
|
16924
|
+
const text = assistantTextPartText(part) || (part && typeof part === "object" && typeof part.text === "string" ? part.text : part !== undefined && part !== null && typeof part !== "object" ? String(part) : "");
|
|
16804
16925
|
if (text && !assistantHasToolCallAfter(content, index)) parts.push(text);
|
|
16805
16926
|
}
|
|
16806
16927
|
return parts.length ? parts.join("\n\n") : "";
|
|
16807
16928
|
}
|
|
16808
16929
|
|
|
16809
|
-
function assistantThinkingTextFromMessage(message) {
|
|
16930
|
+
function assistantThinkingTextFromMessage(message, { streaming = false } = {}) {
|
|
16810
16931
|
const content = message?.content;
|
|
16932
|
+
if (typeof content === "string") {
|
|
16933
|
+
const parsed = splitThinkingFormatText(content, { streaming });
|
|
16934
|
+
return parsed?.hasThinkingFormat ? visibleThinkingText(parsed.thinkingText) : null;
|
|
16935
|
+
}
|
|
16811
16936
|
if (!Array.isArray(content)) return null;
|
|
16812
|
-
const parts =
|
|
16813
|
-
|
|
16814
|
-
|
|
16815
|
-
|
|
16937
|
+
const parts = [];
|
|
16938
|
+
for (const part of content) {
|
|
16939
|
+
if (part && typeof part === "object" && (part.type === "thinking" || typeof part.thinking === "string")) {
|
|
16940
|
+
const thinking = visibleThinkingText(assistantThinkingText(part));
|
|
16941
|
+
if (thinking.trim()) parts.push(thinking);
|
|
16942
|
+
continue;
|
|
16943
|
+
}
|
|
16944
|
+
const text = assistantTextPartText(part) || (part && typeof part === "object" && typeof part.text === "string" ? part.text : part !== undefined && part !== null && typeof part !== "object" ? String(part) : "");
|
|
16945
|
+
const parsed = splitThinkingFormatText(text, { streaming });
|
|
16946
|
+
const thinking = parsed?.hasThinkingFormat ? visibleThinkingText(parsed.thinkingText) : "";
|
|
16947
|
+
if (thinking.trim()) parts.push(thinking);
|
|
16948
|
+
}
|
|
16816
16949
|
return parts.length ? parts.join("\n\n") : "";
|
|
16817
16950
|
}
|
|
16818
16951
|
|
|
@@ -16826,7 +16959,7 @@ function setStreamingThinkingText(text) {
|
|
|
16826
16959
|
|
|
16827
16960
|
function syncStreamingThinkingFromMessage(event, { placeholder = "" } = {}) {
|
|
16828
16961
|
if (!thinkingOutputVisible) return true;
|
|
16829
|
-
const text = assistantThinkingTextFromMessage(assistantStreamingMessage(event));
|
|
16962
|
+
const text = assistantThinkingTextFromMessage(assistantStreamingMessage(event), { streaming: true });
|
|
16830
16963
|
if (text === null) return false;
|
|
16831
16964
|
return setStreamingThinkingText(text || placeholder);
|
|
16832
16965
|
}
|
|
@@ -16848,13 +16981,13 @@ function handleMessageUpdate(event) {
|
|
|
16848
16981
|
}
|
|
16849
16982
|
scrollChatToBottom();
|
|
16850
16983
|
} else if (update.type === "thinking_end") {
|
|
16851
|
-
const finalThinking = assistantThinkingTextFromMessage(assistantStreamingMessage(event)) || thinkingDeltaText(update);
|
|
16984
|
+
const finalThinking = assistantThinkingTextFromMessage(assistantStreamingMessage(event), { streaming: true }) || thinkingDeltaText(update);
|
|
16852
16985
|
if (finalThinking) setStreamingThinkingText(finalThinking);
|
|
16853
16986
|
streamThinkingBubble?.classList.add("complete");
|
|
16854
16987
|
setRunIndicatorActivity("Finished thinking; waiting for the next output or action…", { scroll: false });
|
|
16855
16988
|
} else if (update.type === "text_delta" || update.type === "text_end") {
|
|
16856
16989
|
const delta = update.type === "text_delta" ? update.delta || "" : "";
|
|
16857
|
-
const partialText = assistantTextFromMessage(assistantStreamingMessage(event));
|
|
16990
|
+
const partialText = assistantTextFromMessage(assistantStreamingMessage(event), { streaming: true });
|
|
16858
16991
|
if (typeof partialText === "string") streamRawText = partialText;
|
|
16859
16992
|
else if (update.type === "text_end" && typeof update.content === "string") streamRawText = update.content;
|
|
16860
16993
|
else streamRawText += delta;
|
|
@@ -747,6 +747,8 @@ assert.match(app, /appendText\(preview, toolResultPreviewText\(message, 10\), "c
|
|
|
747
747
|
assert.match(app, /function assistantDisplayMessages\(message\)/, "assistant history should split thinking and tool-call parts out of the final Assistant output card");
|
|
748
748
|
assert.match(app, /function assistantHasToolCallAfter\(content, index\)/, "assistant text that precedes a tool call should be detectable and suppressible");
|
|
749
749
|
assert.match(app, /if \(!assistantHasToolCallAfter\(content, index\)\) finalParts\.push\(finalPart\);/, "assistant history should not render pre-tool-call assistant text as final output");
|
|
750
|
+
assert.match(app, /typeof content === "string"[\s\S]*?splitThinkingFormatText\(content\)[\s\S]*?content: parsed\.finalText/, "assistant string messages with tagged <think> output should render final text separately");
|
|
751
|
+
assert.match(app, /const textForThinkingFormat[\s\S]*?splitThinkingFormatText\(textForThinkingFormat\)[\s\S]*?appendThinkingFormatDisplayMessages\(displayMessages, base, parsed\)[\s\S]*?finalParts\.push/, "assistant text parts with tagged <think> output should split into thinking and final-output cards");
|
|
750
752
|
assert.match(app, /return content\.trim\(\) \? \[\{ \.\.\.message, title: "final output" \}\] : \[\]/, "assistant messages with stripped empty text should not render empty final-output cards");
|
|
751
753
|
assert.match(app, /function isEmptyAssistantTextPart\(part\)[\s\S]*?part\.type === "text"[\s\S]*?!assistantTextPartText\(part\)\.trim\(\)/, "empty assistant text parts should be recognized as skippable provider metadata");
|
|
752
754
|
assert.match(app, /if \(isEmptyAssistantTextPart\(part\)\) continue;/, "empty assistant text parts should not render as assistant-event cards");
|
|
@@ -754,6 +756,10 @@ assert.match(app, /function assistantFinalOutputPart\(part\)[\s\S]*?if \(part\.t
|
|
|
754
756
|
assert.match(app, /\["assistant", "toolExecution"\]\.includes\(transcriptMessage\.role\) \? messageIndex : -1/, "final Assistant output and paired tool action cards should keep the source message index for feedback");
|
|
755
757
|
assert.match(app, /function ensureStreamingThinkingBubble\(\)[\s\S]*if \(!thinkingOutputVisible\) return false/, "live thinking should respect the show/hide thinking-output toggle");
|
|
756
758
|
assert.match(app, /const UNEXPOSED_THINKING_TEXT = "No thinking content was exposed by the provider\."/, "frontend should name the provider no-thinking placeholder for suppression");
|
|
759
|
+
assert.match(app, /THINKING_FORMAT_OPEN_TAG_REGEX/, "frontend should recognize tagged <think> provider output");
|
|
760
|
+
assert.match(app, /CHANNEL_THINKING_FORMAT_OPEN_TAG_REGEX = \/\^<\\\|\(\[a-z\]\[\\w-\]\*\)>\/i/, "frontend should recognize tagged <|channel> provider output");
|
|
761
|
+
assert.match(app, /function thinkingFormatOpenMatch\(text\)[\s\S]*?CHANNEL_THINKING_FORMAT_OPEN_TAG_REGEX[\s\S]*?closeRegex: new RegExp\(`<\$\{escapeRegExp\(name\)\}\\\\\|>`/, "channel-style tagged output should create a matching <channel|> close delimiter");
|
|
762
|
+
assert.match(app, /function splitThinkingFormatText\(text, \{ streaming = false \} = \{\}\)[\s\S]*?thinkingFormatOpenMatch\(rest\)[\s\S]*?finalText: stripThinkingFormatOutputSeparator\(rest\)/, "tagged thinking output should split thinking text from final response text");
|
|
757
763
|
assert.match(app, /function visibleThinkingText\(text\)[\s\S]*?trimmed === UNEXPOSED_THINKING_TEXT[\s\S]*?return "";/, "provider no-thinking placeholders should normalize to empty thinking output");
|
|
758
764
|
assert.match(app, /if \(isThinkingPart\) \{[\s\S]*?visibleThinkingText\(assistantThinkingText\(part\)\)[\s\S]*?if \(thinking\) displayMessages\.push/, "assistant transcript splitting should skip empty or unexposed thinking parts");
|
|
759
765
|
assert.match(app, /message\.role === "thinking"[\s\S]*?visibleThinkingText\(message\.thinking \|\| textFromContent\(message\.content\)\)[\s\S]*?if \(thinkingOutputVisible && thinkingText\) appendText\(body, thinkingText, "thinking-text"\);/, "thinking cards should suppress empty and provider no-thinking placeholder output");
|
|
@@ -766,15 +772,17 @@ assert.match(app, /function thinkingDeltaText\(update\) \{[\s\S]*?return visible
|
|
|
766
772
|
assert.match(app, /const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible"/, "thinking visibility should persist in browser storage");
|
|
767
773
|
assert.match(app, /function setThinkingOutputVisible\(visible[\s\S]*renderAllMessages\(\{ preserveScroll: true \}\)/, "thinking visibility changes should immediately re-render the transcript");
|
|
768
774
|
assert.match(app, /function assistantStreamingMessage\(event\)/, "live streaming should read the authoritative partial assistant message from RPC events like the TUI");
|
|
769
|
-
assert.match(app, /assistantThinkingTextFromMessage\(assistantStreamingMessage\(event\)\) \|\| thinkingDeltaText\(update\)/, "live thinking end should replace deltas with the final partial-message thinking content");
|
|
775
|
+
assert.match(app, /assistantThinkingTextFromMessage\(assistantStreamingMessage\(event\), \{ streaming: true \}\) \|\| thinkingDeltaText\(update\)/, "live thinking end should replace deltas with the final partial-message thinking content");
|
|
770
776
|
assert.match(app, /if \(typeof partialText === "string"\) streamRawText = partialText;/, "live assistant text should synchronize from partial messages instead of relying only on deltas");
|
|
771
777
|
assert.match(app, /const TODO_PROGRESS_LINE_REGEX = /, "frontend should recognize live todo progress lines that will be moved into the todo widget");
|
|
772
778
|
assert.match(app, /function stripTodoProgressLines\(text, \{ streaming = false \} = \{\}\)/, "live Assistant output should strip todo-progress lines before rendering final-output text");
|
|
773
779
|
assert.match(app, /function renderStreamingAssistantText\(\)[\s\S]*?const assistantText = stripTodoProgressLines\(streamRawText, \{ streaming: true \}\)/, "streamed Assistant text should classify from accumulated output without flashing partial todo-progress lines");
|
|
780
|
+
assert.match(app, /function syncStreamingThinkingFormat\(assistantText\)[\s\S]*?splitThinkingFormatText\(assistantText, \{ streaming: true \}\)[\s\S]*?setStreamingThinkingText\(thinking\)/, "tagged <think> streaming output should update the live thinking card instead of flashing raw tags");
|
|
781
|
+
assert.match(app, /const finalText = thinkingFormat\?\.hasThinkingFormat \? stripTodoProgressLines\(thinkingFormat\.finalText, \{ streaming: true \}\) : assistantText;/, "tagged <think> streaming output should render only final response text in the Assistant card");
|
|
774
782
|
assert.match(app, /const STREAM_OUTPUT_HIDE_DELAY_MS = 300/, "stream output hiding should be debounced to prevent rapid flicker");
|
|
775
783
|
assert.match(app, /const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220/, "live assistant text should be briefly guarded so pre-tool-call text can be suppressed");
|
|
776
784
|
assert.match(app, /function scheduleStreamBubbleHide\([\s\S]*?STREAM_OUTPUT_MIN_VISIBLE_MS/, "stream output cards should observe a minimum visible duration before hiding");
|
|
777
|
-
assert.match(app, /if \(
|
|
785
|
+
assert.match(app, /if \(finalText\) \{[\s\S]*?renderStreamingMarkdown\(streamText, finalText\);[\s\S]*?\} else \{\n\s+scheduleStreamBubbleHide\(\);/, "empty filtered stream output should schedule hide while visible stream output renders as Markdown");
|
|
778
786
|
assert.match(app, /if \(streamToolCallSeen \|\| streamBubble\) renderStreamingAssistantText\(\);\n\s+else scheduleStreamingAssistantTextRender\(\);/, "live assistant text should wait briefly before showing unless it is already visible or follows a tool call");
|
|
779
787
|
assert.match(app, /streamToolCallSeen = true;\n\s+suppressStreamingAssistantTextBeforeToolCall\(\);/, "tool-call starts should remove pending assistant text from the live transcript");
|
|
780
788
|
assert.match(app, /const created = appendMessage\(\{ role: "assistant", title: "final output"/, "live Assistant cards should be created only for final output text without a noisy Assistant label");
|