3232 std::string rawStack;
3333 std::string canonicalStack;
3434 std::string consolePayload;
35- int canonicalQuality = -1 ;
3635};
3736
3837static std::mutex gErrorDisplayMutex ;
@@ -56,7 +55,6 @@ static void ShowErrorModalSynchronously(const std::string& title,
5655static void ScheduleFallbackPresentation (uint64_t ticket);
5756static void PresentFallbackIfNeeded (uint64_t ticket);
5857static std::string ResolveDisplayStack (const PendingErrorDisplay& state);
59- static int EvaluateStackQuality (const std::string& stackText);
6058static void ConsiderStackCandidate (PendingErrorDisplay& state, v8::Isolate* isolate,
6159 const std::string& candidateStack);
6260
@@ -509,7 +507,6 @@ static void ConsiderStackCandidate(PendingErrorDisplay& state, v8::Isolate* isol
509507 gPendingErrorDisplay .rawStack = stackTrace;
510508 gPendingErrorDisplay .consolePayload .clear ();
511509 gPendingErrorDisplay .canonicalStack .clear ();
512- gPendingErrorDisplay .canonicalQuality = -1 ;
513510 ConsiderStackCandidate (gPendingErrorDisplay , isolate, stackTrace);
514511 ticketToSchedule = gPendingErrorDisplay .ticket ;
515512 }
@@ -540,7 +537,6 @@ static void ConsiderStackCandidate(PendingErrorDisplay& state, v8::Isolate* isol
540537 gPendingErrorDisplay .isolate = payloadIsolate;
541538 }
542539 gPendingErrorDisplay .canonicalStack = text;
543- gPendingErrorDisplay .canonicalQuality = std::numeric_limits<int >::max ();
544540 };
545541
546542 {
@@ -561,7 +557,6 @@ static void ConsiderStackCandidate(PendingErrorDisplay& state, v8::Isolate* isol
561557 if (gPendingErrorDisplay .ticket == 0 ) {
562558 gPendingErrorDisplay .ticket = gNextErrorTicket ++;
563559 gPendingErrorDisplay .canonicalStack .clear ();
564- gPendingErrorDisplay .canonicalQuality = -1 ;
565560 }
566561
567562 if (!gPendingErrorDisplay .contextCaptured && !gPendingErrorDisplay .modalPresented ) {
@@ -638,125 +633,30 @@ static void PresentFallbackIfNeeded(uint64_t ticket) {
638633}
639634
640635static std::string ResolveDisplayStack (const PendingErrorDisplay& state) {
636+ // Deterministic preference: canonicalStack > consolePayload > rawStack > message
637+ // Remap when possible so the UI matches terminal output.
641638 if (!state.canonicalStack .empty ()) {
642- size_t previewLen = std::min<size_t >(state.canonicalStack .size (), 120 );
643- std::string preview = state.canonicalStack .substr (0 , previewLen);
644- // Log(@"[ErrorDisplay] resolved stack (canonical) len=%zu preview=%@",
645- // state.canonicalStack.size(), [NSString stringWithUTF8String:preview.c_str()]);
646639 return state.canonicalStack ;
647640 }
648641
649- std::string bestStack;
650- int bestQuality = std::numeric_limits<int >::min ();
651-
652- auto consider = [&](const std::string& candidate, v8::Isolate* isolate) {
653- if (candidate.empty ()) {
654- return ;
655- }
656- std::string normalized = candidate;
657- if (isolate != nullptr ) {
658- std::string remapped = tns::RemapStackTraceIfAvailable (isolate, candidate);
642+ auto remapIfPossible = [&](const std::string& text) -> std::string {
643+ if (text.empty ()) return std::string ();
644+ if (state.isolate != nullptr ) {
645+ std::string remapped = tns::RemapStackTraceIfAvailable (state.isolate , text);
659646 if (!remapped.empty ()) {
660- normalized = remapped;
647+ return remapped;
661648 }
662649 }
663- int candidateQuality = EvaluateStackQuality (normalized);
664- size_t previewLen = std::min<size_t >(normalized.size (), 120 );
665- std::string preview = normalized.substr (0 , previewLen);
666- // Log(@"[ErrorDisplay] consider: quality=%d len=%zu preview=%@", candidateQuality,
667- // normalized.size(), [NSString stringWithUTF8String:preview.c_str()]);
668- if (candidateQuality > bestQuality ||
669- (candidateQuality == bestQuality && normalized.size () > bestStack.size ())) {
670- bestStack = normalized;
671- bestQuality = candidateQuality;
672- }
650+ return text;
673651 };
674652
675- if (!state.canonicalStack .empty ()) {
676- consider (state.canonicalStack , nullptr );
677- }
678653 if (!state.consolePayload .empty ()) {
679- consider (state.consolePayload , state. isolate );
654+ return remapIfPossible (state.consolePayload );
680655 }
681656 if (!state.rawStack .empty ()) {
682- consider (state.rawStack , state.isolate );
683- }
684-
685- if (bestStack.empty ()) {
686- return state.message ;
687- }
688-
689- size_t finalPreviewLen = std::min<size_t >(bestStack.size (), 120 );
690- std::string finalPreview = bestStack.substr (0 , finalPreviewLen);
691- // Log(@"[ErrorDisplay] resolved stack quality=%d len=%zu preview=%@", bestQuality,
692- // bestStack.size(), [NSString stringWithUTF8String:finalPreview.c_str()]);
693-
694- return bestStack;
695- }
696-
697- static size_t CountOccurrences (const std::string& haystack, const std::string& needle) {
698- if (haystack.empty () || needle.empty ()) {
699- return 0 ;
700- }
701- size_t count = 0 ;
702- size_t pos = haystack.find (needle, 0 );
703- while (pos != std::string::npos) {
704- ++count;
705- pos = haystack.find (needle, pos + needle.length ());
657+ return remapIfPossible (state.rawStack );
706658 }
707- return count;
708- }
709-
710- static int EvaluateStackQuality (const std::string& stackText) {
711- if (stackText.empty ()) {
712- return std::numeric_limits<int >::min ();
713- }
714-
715- auto hasAny = [&](const std::initializer_list<const char *>& tokens) {
716- for (const auto * token : tokens) {
717- if (stackText.find (token) != std::string::npos) {
718- return true ;
719- }
720- }
721- return false ;
722- };
723-
724- int score = 0 ;
725-
726- size_t tsFrames = CountOccurrences (stackText, " .ts:" ) +
727- CountOccurrences (stackText, " .tsx:" ) +
728- CountOccurrences (stackText, " .vue:" );
729- if (tsFrames > 0 ) {
730- score += 60 ;
731- score += static_cast <int >(tsFrames) * 5 ;
732- }
733-
734- if (hasAny ({" webpack:/" , " file: src/" , " sourceURL" })) {
735- score += 20 ;
736- }
737-
738- size_t newlineCount = CountOccurrences (stackText, " \n " );
739- if (newlineCount > 0 ) {
740- score += static_cast <int >(std::min<size_t >(20 , newlineCount));
741- }
742-
743- if (stackText.find (" at " ) != std::string::npos) {
744- score += 10 ;
745- }
746-
747- int penalty = 0 ;
748- penalty += static_cast <int >(CountOccurrences (stackText, " file:///app/" )) * 3 ;
749- penalty += static_cast <int >(CountOccurrences (stackText, " .bundle.js" )) * 2 ;
750- penalty = std::min (penalty, score / 2 ); // don't let bundle frames dominate completely
751- score -= penalty;
752-
753- score += static_cast <int >(std::min<size_t >(10 , stackText.size () / 400 ));
754-
755- if (score <= 0 ) {
756- score = 1 ; // ensure non-empty strings beat truly empty candidates
757- }
758-
759- return score;
659+ return state.message ;
760660}
761661
762662static void ConsiderStackCandidate (PendingErrorDisplay& state, v8::Isolate* isolate,
@@ -773,26 +673,10 @@ static void ConsiderStackCandidate(PendingErrorDisplay& state, v8::Isolate* isol
773673 normalized = remapped;
774674 }
775675 }
776-
777- int quality = EvaluateStackQuality (normalized);
778- if (quality < 0 && state.canonicalQuality >= 0 ) {
779- return ;
780- }
781-
782- bool shouldReplace = false ;
783- if (quality > state.canonicalQuality ) {
784- shouldReplace = true ;
785- } else if (quality == state.canonicalQuality && normalized.size () > state.canonicalStack .size ()) {
786- shouldReplace = true ;
787- }
788-
676+ // Deterministic behavior: if no canonical stack yet, set it to the first available candidate.
677+ // Console payloads will explicitly override canonicalStack elsewhere.
789678 if (state.canonicalStack .empty ()) {
790- shouldReplace = true ;
791- }
792-
793- if (shouldReplace) {
794679 state.canonicalStack = normalized;
795- state.canonicalQuality = quality;
796680 }
797681}
798682
0 commit comments