Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 4 additions & 13 deletions tests/e2e/test-web-ui-session-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,21 +370,12 @@ module.exports = async function testWebUiSessionBrowser(ctx) {
);
});

assert(vm.activeSessionMessages.length === vm.sessionDetailInitialMessageLimit, 'session browser preview should hydrate only the initial detail window for huge sessions');
assert(vm.activeSessionMessages.length >= vm.sessionDetailInitialMessageLimit, 'session browser preview should hydrate at least the initial detail window for huge sessions');
assert(vm.activeSessionDetailClipped === true, 'session browser preview should stay clipped for huge sessions');
assert(vm.activeSessionVisibleMessages.length === vm.sessionPreviewInitialBatchSize, 'session browser should render only the first preview batch initially');
assert(vm.activeSessionVisibleMessages.length === vm.activeSessionMessages.length, 'session browser should render all loaded messages immediately without batching');

await withGlobalOverrides({ fetch, localStorage }, async () => {
for (let i = 0; i < 4; i += 1) {
await vm.loadMoreSessionMessages(24);
}
});

assert(vm.sessionDetailMessageLimit > vm.sessionDetailInitialMessageLimit, 'session browser should grow detail hydration beyond the initial preview window');
assert(vm.activeSessionMessages.length > vm.sessionDetailInitialMessageLimit, 'session browser should fetch an additional bounded detail page');
assert(vm.activeSessionMessages.length < hotSessionMessageCount, 'session browser should not pull the whole huge session into memory in one expansion step');
assert(vm.activeSessionVisibleMessages.length >= 104, 'session browser should preserve incremental preview expansion after detail hydration grows');
assert(vm.activeSessionVisibleMessages.length < vm.activeSessionMessages.length, 'session browser should keep preview rendering incremental after hydration grows');
assert(vm.sessionDetailMessageLimit >= vm.sessionDetailInitialMessageLimit, 'session browser detail limit should be at least the initial window');
assert(vm.activeSessionMessages.length < hotSessionMessageCount, 'session browser should not pull the whole huge session into memory');
assert(vm.activeSessionDetailClipped === true, 'session browser should remain clipped while more huge-session messages stay on disk');

const hugeLineSession = vm.sessionsList.find((item) => item.sessionId === hugeLineSessionId);
Expand Down
8 changes: 4 additions & 4 deletions web-ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ document.addEventListener('DOMContentLoaded', () => {
activeSessionDetailClipped: false,
sessionDetailLoading: false,
sessionDetailRequestSeq: 0,
sessionDetailInitialMessageLimit: 80,
sessionDetailFetchStep: 80,
sessionDetailInitialMessageLimit: 300,
sessionDetailFetchStep: 300,
Comment on lines +206 to +207

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Analyze session message counts and sizes to validate new pagination defaults

echo "=== Session message count distribution ==="
fd -e jsonl -t f . --exec sh -c '
  count=$(grep -c "\"type\":\"response_item\"" "$1" 2>/dev/null || echo 0)
  if [ "$count" -gt 0 ]; then
    echo "$count"
  fi
' _ {} | sort -n | awk '
BEGIN { total=0; count=0; }
{
  total+=$1; count++;
  if ($1 > 300) over300++;
  if ($1 > 80) over80++;
}
END {
  if (count > 0) {
    print "Total sessions analyzed:", count
    print "Average messages per session:", int(total/count)
    print "Sessions with >80 messages:", over80
    print "Sessions with >300 messages:", over300
  }
}'

echo ""
echo "=== Large message detection (>10KB) ==="
fd -e jsonl -t f . --exec sh -c '
  filename=$(basename "$1")
  grep "\"type\":\"response_item\"" "$1" 2>/dev/null | while read line; do
    size=${`#line`}
    if [ "$size" -gt 10240 ]; then
      echo "$filename: message size ${size} bytes"
    fi
  done
' _ {}

Repository: SakuraByteCore/codexmate

Length of output: 152


🏁 Script executed:

# Check if the files mentioned exist and examine the specific line ranges
find . -name "app.js" -path "*/web-ui/*" -type f

Repository: SakuraByteCore/codexmate

Length of output: 84


🏁 Script executed:

# Search for session-helpers.mjs
find . -name "session-helpers.mjs" -type f

Repository: SakuraByteCore/codexmate

Length of output: 97


🏁 Script executed:

# List the repository structure to understand the layout
git ls-files | head -30

Repository: SakuraByteCore/codexmate

Length of output: 726


🏁 Script executed:

# Check app.js size and read lines around 206-207
wc -l ./web-ui/app.js
sed -n '200,215p' ./web-ui/app.js

Repository: SakuraByteCore/codexmate

Length of output: 831


🏁 Script executed:

# Check session-helpers.mjs size and read lines around 457-476
wc -l ./web-ui/session-helpers.mjs
sed -n '450,480p' ./web-ui/session-helpers.mjs

Repository: SakuraByteCore/codexmate

Length of output: 1731


🏁 Script executed:

# Search for batching-related code in web-ui
rg -n "batch" ./web-ui/ --type js --type mjs -i

Repository: SakuraByteCore/codexmate

Length of output: 96


🏁 Script executed:

# Look at the loadActiveSessionDetail method to understand the impact
rg -n "loadActiveSessionDetail" ./web-ui/session-helpers.mjs -A 30 | head -80

Repository: SakuraByteCore/codexmate

Length of output: 3766


🏁 Script executed:

# Check git history for the change from 80 to 300
git log --oneline -n 20 -- ./web-ui/app.js

Repository: SakuraByteCore/codexmate

Length of output: 154


🏁 Script executed:

# Search for batching without mjs file type restriction
rg -n "batch" ./web-ui/ -i

Repository: SakuraByteCore/codexmate

Length of output: 6160


🏁 Script executed:

# Look at git diff for the commit that changed these lines
git show 349a50e --stat

Repository: SakuraByteCore/codexmate

Length of output: 17245


🏁 Script executed:

# Find the actual loadActiveSessionDetail implementation
rg -n "loadActiveSessionDetail\s*\(" ./web-ui/session-helpers.mjs -A 50 | head -120

Repository: SakuraByteCore/codexmate

Length of output: 5272


🏁 Script executed:

# Find the message rendering implementation
rg -n "renderActiveSessionDetail|renderSessionPreview|render.*message" ./web-ui/ -i | head -40

Repository: SakuraByteCore/codexmate

Length of output: 490


🏁 Script executed:

# Look for the actual rendering/batching code in the computed properties
sed -n '420,480p' ./web-ui/modules/app.computed.session.mjs

Repository: SakuraByteCore/codexmate

Length of output: 3077


🏁 Script executed:

# Check how sessionPreviewVisibleCount is managed - is it incremented by auto-fetch?
rg -n "sessionPreviewVisibleCount\|primeSessionPreviewMessageRender" ./web-ui/session-helpers.mjs -A 3 -B 1

Repository: SakuraByteCore/codexmate

Length of output: 50


🏁 Script executed:

# Look for the renderSessionPreview or message batch rendering
sed -n '480,550p' ./web-ui/modules/app.computed.session.mjs

Repository: SakuraByteCore/codexmate

Length of output: 3785


🏁 Script executed:

# Search more broadly for sessionPreviewVisibleCount
rg -n "sessionPreviewVisibleCount" ./web-ui/ | head -20

Repository: SakuraByteCore/codexmate

Length of output: 1155


🏁 Script executed:

# Look at the specific auto-fetch logic more carefully
sed -n '455,480p' ./web-ui/session-helpers.mjs

Repository: SakuraByteCore/codexmate

Length of output: 1433


🏁 Script executed:

# Verify the initial rendering batch size
sed -n '225,235p' ./web-ui/app.js | grep -A 2 -B 2 "sessionPreviewInitialBatchSize"

Repository: SakuraByteCore/codexmate

Length of output: 308


🏁 Script executed:

# Check if there's any message size filtering or lazy loading in the template
rg -n "activeSessionVisibleMessages" ./web-ui/partials/index/panel-sessions.html -B 2 -A 5

Repository: SakuraByteCore/codexmate

Length of output: 685


🏁 Script executed:

# Look at the rendering method for messages in the computed properties
sed -n '435,475p' ./web-ui/modules/app.computed.session.mjs

Repository: SakuraByteCore/codexmate

Length of output: 2200


🏁 Script executed:

# Check primeSessionPreviewMessageRender to see the actual rendering logic
rg -n "primeSessionPreviewMessageRender" ./web-ui/ -A 15 | head -60

Repository: SakuraByteCore/codexmate

Length of output: 5493


🏁 Script executed:

# Check what happens when sessionDetailLoading completes - does it trigger rendering?
sed -n '430,460p' ./web-ui/session-helpers.mjs

Repository: SakuraByteCore/codexmate

Length of output: 1416


🏁 Script executed:

# Check what the rendering actually does - find the template loop for messages
rg -n "activeSessionVisibleMessages" ./web-ui/partials/index/panel-sessions.html -A 10

Repository: SakuraByteCore/codexmate

Length of output: 881


🏁 Script executed:

# Look for any virtual scrolling or message virtualization
rg -n "virtual\|infinite\|scroll" ./web-ui/ -i | grep -v "node_modules\|test" | head -20

Repository: SakuraByteCore/codexmate

Length of output: 50


🏁 Script executed:

# Look at the full template to see how messages are rendered
sed -n '260,320p' ./web-ui/partials/index/panel-sessions.html

Repository: SakuraByteCore/codexmate

Length of output: 3748


🏁 Script executed:

# Check if there's any batching in the v-for loop for messages
rg -n "v-for.*activeSessionVisibleMessages|v-for.*message" ./web-ui/partials/index/panel-sessions.html -A 5

Repository: SakuraByteCore/codexmate

Length of output: 50


Clarify the impact of increased message loading and pagination strategy.

The sessionDetailInitialMessageLimit was set to 300 (vs. the runtime sessionDetailMessageLimit of 80), and combined with the auto-fetch recursion (lines 457-476 in session-helpers.mjs), a clipped session will trigger multiple fetches of 300 messages each (up to the 1000 cap). The template renders all activeSessionMessages at once without batching (line 265 in panel-sessions.html), so a large session could accumulate 300→600→900→1000 messages in DOM at once.

For sessions with large message payloads, this could cause:

  • Significant memory pressure from 300+ large messages in activeSessionMessages
  • Slower initial tab load and re-renders after each auto-fetch increment
  • Potential rendering jank (v-memo provides some optimization but all messages are still rendered)

Consider whether the 300-message initial batch is justified by your typical session sizes, or if starting smaller (e.g., 100-150) with larger fetch steps would be safer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/app.js` around lines 206 - 207, The initial message batch
(sessionDetailInitialMessageLimit = 300) plus sessionDetailFetchStep = 300 can
cause large DOM and memory spikes because auto-fetch recursion in
session-helpers.mjs (the incremental fetch loop) pushes many messages into
activeSessionMessages (rendered by panel-sessions.html) at once; reduce
sessionDetailInitialMessageLimit to a smaller safe default (e.g., 100–150), keep
or increase sessionDetailFetchStep for subsequent loads, and/or implement
batching in the consumer (where activeSessionMessages is rendered) so each fetch
appends a limited chunk rather than rendering all messages at once; update the
constants and adjust the auto-fetch loop in session-helpers.mjs and
panel-sessions.html rendering logic (or add a virtualization/batching layer)
accordingly.

sessionDetailMessageLimit: 80,
sessionDetailMessageLimitCap: 1000,
sessionTimelineActiveKey: '',
Expand All @@ -222,8 +222,8 @@ document.addEventListener('DOMContentLoaded', () => {
sessionPreviewHeaderResizeObserver: null,
sessionListRenderEnabled: false,
sessionListVisibleCount: 0,
sessionListInitialBatchSize: 20,
sessionListLoadStep: 40,
sessionListInitialBatchSize: 40,
sessionListLoadStep: 80,
sessionPreviewRenderEnabled: false,
sessionTabRenderTicket: 0,
sessionPreviewVisibleCount: 0,
Expand Down
25 changes: 19 additions & 6 deletions web-ui/modules/app.methods.navigation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,14 @@
const mainTab = typeof mainTabSource === 'string' ? mainTabSource.trim().toLowerCase() : '';
const configMode = typeof configModeSource === 'string' ? configModeSource.trim().toLowerCase() : '';
const settingsTab = typeof vm.settingsTab === 'string' ? vm.settingsTab.trim().toLowerCase() : 'general';
const skillsTargetApp = typeof vm.skillsTargetApp === 'string' && (vm.skillsTargetApp === 'codex' || vm.skillsTargetApp === 'claude') ? vm.skillsTargetApp : 'codex';
const promptTemplatesMode = typeof vm.promptTemplatesMode === 'string' && (vm.promptTemplatesMode === 'compose' || vm.promptTemplatesMode === 'manage') ? vm.promptTemplatesMode : 'compose';
const snapshot = {
settingsTab: settingsTab === 'data' ? 'data' : 'general',
mainTab: MAIN_TAB_SET.has(mainTab) ? mainTab : 'dashboard',
configMode: configModeSet && configModeSet.has(configMode) ? configMode : 'codex'
configMode: configModeSet && configModeSet.has(configMode) ? configMode : 'codex',
skillsTargetApp,
promptTemplatesMode
};
try {
localStorage.setItem(NAV_STATE_STORAGE_KEY, JSON.stringify(snapshot));
Expand All @@ -99,7 +103,13 @@
? restored.settingsTab.trim().toLowerCase()
: '';
const shouldUpdateSettingsTab = !!(nextSettingsTab && (nextSettingsTab === 'general' || nextSettingsTab === 'data') && nextSettingsTab !== this.settingsTab);
if (!shouldUpdateConfigMode && !shouldUpdateMainTab && !shouldUpdateSettingsTab) {
const nextSkillsTargetApp = restored && typeof restored.skillsTargetApp === 'string' && (restored.skillsTargetApp === 'codex' || restored.skillsTargetApp === 'claude')
? restored.skillsTargetApp : '';
const shouldUpdateSkillsTargetApp = !!(nextSkillsTargetApp && nextSkillsTargetApp !== this.skillsTargetApp);
const nextPromptTemplatesMode = restored && typeof restored.promptTemplatesMode === 'string' && (restored.promptTemplatesMode === 'compose' || restored.promptTemplatesMode === 'manage')
? restored.promptTemplatesMode : '';
const shouldUpdatePromptTemplatesMode = !!(nextPromptTemplatesMode && nextPromptTemplatesMode !== this.promptTemplatesMode);
if (!shouldUpdateConfigMode && !shouldUpdateMainTab && !shouldUpdateSettingsTab && !shouldUpdateSkillsTargetApp && !shouldUpdatePromptTemplatesMode) {
return false;
}
this.__navStateRestoring = true;
Expand All @@ -113,6 +123,12 @@
if (shouldUpdateMainTab) {
this.switchMainTab(nextMainTab);
}
if (shouldUpdateSkillsTargetApp) {
this.skillsTargetApp = nextSkillsTargetApp;
}
if (shouldUpdatePromptTemplatesMode) {
this.promptTemplatesMode = nextPromptTemplatesMode;
}
} finally {
this.__navStateRestoring = false;
}
Expand Down Expand Up @@ -674,10 +690,7 @@
? this.activeSessionMessages.length
: 0;
if (total <= 0) return;
const baseSize = Number.isFinite(this.sessionPreviewInitialBatchSize)
? Math.max(1, Math.floor(this.sessionPreviewInitialBatchSize))
: 40;
this.sessionPreviewVisibleCount = Math.min(baseSize, total);
this.sessionPreviewVisibleCount = total;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Removing message batching may cause rendering performance issues for large sessions.

Previously, sessionPreviewVisibleCount was capped by sessionPreviewInitialBatchSize (12), and users would progressively load more messages. Now it's set directly to total, meaning all loaded messages render immediately.

For a session with 300+ messages (now the default initial load), this could cause:

  • Initial render blocking the main thread
  • Large DOM tree (300+ message cards)
  • Scroll performance degradation
  • Memory overhead from Vue tracking hundreds of reactive message elements

The PR summary mentions "display all fetched session messages immediately," but consider whether progressive rendering (even if automatic/invisible) would provide better UX for huge sessions.

Possible mitigation: Virtual scrolling or windowing

If large sessions become a performance bottleneck, consider implementing virtual scrolling (render only visible messages) using a library like vue-virtual-scroller or a custom intersection-observer-based approach. However, this would be a significant refactor.

For now, monitor performance with sessions containing 300+ messages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.methods.navigation.mjs` at line 693, The change sets
this.sessionPreviewVisibleCount directly to total which forces rendering of all
fetched messages and can harm performance for large sessions; revert to using a
capped progressive reveal: initialize this.sessionPreviewVisibleCount to
Math.min(total, this.sessionPreviewInitialBatchSize) and then incrementally
increase it (e.g., in the same method that sets the count, use a
loop/timer/nextTick to batch-add more items until it reaches total) so rendering
is throttled; update the assignment at the location that presently does
"this.sessionPreviewVisibleCount = total" and reference
sessionPreviewVisibleCount and sessionPreviewInitialBatchSize when implementing
the incremental/increment logic.

this.invalidateSessionTimelineMeasurementCache();
},

Expand Down
1 change: 1 addition & 0 deletions web-ui/modules/skills.methods.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function createSkillsMethods({ api }) {
if (nextTarget !== this.skillsTargetApp) {
this.skillsTargetApp = nextTarget;
this.resetSkillsTargetState();
if (typeof this.saveNavState === 'function') this.saveNavState();
}
if (!refresh) {
return true;
Expand Down
6 changes: 3 additions & 3 deletions web-ui/partials/index/panel-market.html
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,15 @@
</div>
<div class="market-action-grid">
<button type="button" class="market-action-card" @click="openSkillsManager" :disabled="loading || !!initError || skillsMarketBusy">
<span class="market-action-title">{{ t('market.action.manage.title') }}</span>
<span class="market-action-title"><svg style="width:14px;height:14px;vertical-align:-2px;margin-right:4px" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="14" height="14" rx="3"/><path d="M7 7h6M7 10h4"/></svg>{{ t('market.action.manage.title') }}</span>
<span class="market-action-copy">{{ t('market.action.manage.copy', { target: skillsTargetLabel }) }}</span>
</button>
<button type="button" class="market-action-card" @click="scanImportableSkills({ silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
<span class="market-action-title">{{ t('market.action.crossImport.title') }}</span>
<span class="market-action-title"><svg style="width:14px;height:14px;vertical-align:-2px;margin-right:4px" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l3-3 3 3"/><path d="M7 3v6"/><path d="M13 7l3 3 3-3"/><path d="M16 16V7"/></svg>{{ t('market.action.crossImport.title') }}</span>
<span class="market-action-copy">{{ t('market.action.crossImport.copy', { target: skillsTargetLabel }) }}</span>
</button>
<button type="button" class="market-action-card" @click="triggerSkillsZipImport" :disabled="loading || !!initError || skillsMarketBusy">
<span class="market-action-title">{{ t('market.action.zipImport.title') }}</span>
<span class="market-action-title"><svg style="width:14px;height:14px;vertical-align:-2px;margin-right:4px" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14l3-3 3 3"/><path d="M7 4v7"/><rect x="12" y="4" width="5" height="12" rx="1"/></svg>{{ t('market.action.zipImport.title') }}</span>
<span class="market-action-copy">{{ t('market.action.zipImport.copy') }}</span>
</button>
</div>
Expand Down
4 changes: 2 additions & 2 deletions web-ui/partials/index/panel-plugins.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@
role="tab"
:aria-selected="promptTemplatesMode === 'compose'"
:class="['mode-pill', { active: promptTemplatesMode === 'compose' }]"
@click="promptTemplatesMode = 'compose'">{{ t('plugins.promptTemplates.mode.compose') }}</button>
@click="promptTemplatesMode = 'compose'; if(typeof saveNavState==='function')saveNavState()"><svg style="width:12px;height:12px;vertical-align:-1px;margin-right:4px" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4h12v12H4z"/><path d="M8 8h4M8 11h2"/></svg>{{ t('plugins.promptTemplates.mode.compose') }}</button>
<button
type="button"
role="tab"
:aria-selected="promptTemplatesMode !== 'compose'"
:class="['mode-pill', { active: promptTemplatesMode !== 'compose' }]"
@click="promptTemplatesMode = 'manage'">{{ t('plugins.promptTemplates.mode.manage') }}</button>
@click="promptTemplatesMode = 'manage'; if(typeof saveNavState==='function')saveNavState()"><svg style="width:12px;height:12px;vertical-align:-1px;margin-right:4px" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 5h14M3 10h10M3 15h12"/></svg>{{ t('plugins.promptTemplates.mode.manage') }}</button>
</div>

<div v-if="promptTemplatesMode === 'compose'" class="prompt-compose">
Expand Down
10 changes: 1 addition & 9 deletions web-ui/partials/index/panel-sessions.html
Original file line number Diff line number Diff line change
Expand Up @@ -262,15 +262,7 @@
{{ t('sessions.preview.clipped', { count: activeSessionMessages.length }) }}
</div>
<div
v-if="canLoadMoreSessionMessages"
class="session-item-sub session-item-wrap"
style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
<span>{{ t('sessions.preview.shownCount', { shown: activeSessionVisibleMessages.length, total: activeSessionMessages.length }) }}</span>
<span>{{ t('sessions.preview.loadMore', { remain: sessionPreviewRemainingCount }) }}</span>
</div>
<div v-if="sessionPreviewLoadingMore" class="session-item-sub session-item-wrap">{{ t('sessions.preview.loadingMore') }}</div>
<div
v-for="(msg, idx) in activeSessionVisibleMessages"
v-for="(msg, idx) in activeSessionMessages"
:key="getRecordRenderKey(msg, idx)"
v-memo="[msg.text, msg.timestamp, msg.roleLabel, msg.normalizedRole]"
:data-message-key="getRecordRenderKey(msg, idx)"
Expand Down
22 changes: 20 additions & 2 deletions web-ui/session-helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,7 @@ export function switchMainTab(tab) {
void Promise.resolve(marketOverviewLoad).catch(() => {});
}
if (enteringPluginsTab && typeof this.loadPluginsOverview === 'function') {
// Default behavior: always land on Prompt Templates + Compose when entering Plugins.
this.pluginsActiveId = 'prompt-templates';
this.promptTemplatesMode = 'compose';
this.promptComposerPickerVisible = false;
let pluginsLoad = null;
try {
Expand Down Expand Up @@ -456,6 +454,26 @@ export async function loadActiveSessionDetail(api, options = {}) {
}
}
this.sessionPreviewPendingVisibleCount = 0;
if (this.activeSessionDetailClipped) {
const autoFetchSession = currentActiveSession;
this.$nextTick(() => {
if (this.activeSession !== autoFetchSession) return;
if (this.sessionDetailLoading) return;
if (!this.activeSessionDetailClipped) return;
const currentLimit = Number(this.sessionDetailMessageLimit);
const fetchStep = Number.isFinite(this.sessionDetailFetchStep)
? Math.max(1, Math.floor(this.sessionDetailFetchStep))
: 300;
const limitCap = Number.isFinite(this.sessionDetailMessageLimitCap)
? Math.max(1, Math.floor(this.sessionDetailMessageLimitCap))
: 1000;
const nextLimit = Math.min(currentLimit + fetchStep, limitCap);
if (nextLimit <= currentLimit) return;
this.sessionDetailMessageLimit = nextLimit;
this.sessionPreviewPendingVisibleCount = nextLimit;
void this.loadActiveSessionDetail({ preserveVisibleCount: true });
});
}
Comment on lines +457 to +476

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Auto-fetch recursion may trigger rapid sequential API calls for large clipped sessions.

When activeSessionDetailClipped is true, the code schedules a recursive call to loadActiveSessionDetail on $nextTick. For a session with 1325 messages (from the e2e test), this pattern would trigger:

  1. Initial load: 300 messages → clipped
  2. Auto-fetch (nextTick): 600 messages → clipped
  3. Auto-fetch (nextTick): 900 messages → clipped
  4. Auto-fetch (nextTick): 1000 messages (cap) → possibly still clipped

This results in 4 rapid sequential API calls with no delay between them. Potential issues:

  • API rate limiting or server load
  • No visual feedback to the user that background loading is happening (loading spinner turns off before recursion)
  • User might navigate away during auto-fetch, wasting resources
  • The guards prevent race conditions but don't prevent resource waste

Consider:

  • Adding a small delay (e.g., 100-300ms) between auto-fetch attempts
  • Setting a visual indicator (e.g., "Loading more messages in background...")
  • Adding a max retry counter to prevent infinite loops if the clipped flag doesn't clear
Example: Add delay between auto-fetches
 if (this.activeSessionDetailClipped) {
     const autoFetchSession = currentActiveSession;
-    this.$nextTick(() => {
+    setTimeout(() => {
         if (this.activeSession !== autoFetchSession) return;
         if (this.sessionDetailLoading) return;
         if (!this.activeSessionDetailClipped) return;
         const currentLimit = Number(this.sessionDetailMessageLimit);
         const fetchStep = Number.isFinite(this.sessionDetailFetchStep)
             ? Math.max(1, Math.floor(this.sessionDetailFetchStep))
             : 300;
         const limitCap = Number.isFinite(this.sessionDetailMessageLimitCap)
             ? Math.max(1, Math.floor(this.sessionDetailMessageLimitCap))
             : 1000;
         const nextLimit = Math.min(currentLimit + fetchStep, limitCap);
         if (nextLimit <= currentLimit) return;
         this.sessionDetailMessageLimit = nextLimit;
         this.sessionPreviewPendingVisibleCount = nextLimit;
         void this.loadActiveSessionDetail({ preserveVisibleCount: true });
-    });
+    }, 200);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/session-helpers.mjs` around lines 457 - 476, The auto-fetch loop
triggered when activeSessionDetailClipped is true currently calls
loadActiveSessionDetail on $nextTick repeatedly, causing rapid sequential API
calls; modify the block that computes nextLimit and calls
loadActiveSessionDetail to (1) add a short async delay (e.g., 100–300ms) before
calling loadActiveSessionDetail (use a sleep/promise or setTimeout) so the
recursive fetches are throttled, (2) introduce a lightweight retry counter or
backoff field (e.g., sessionDetailAutoFetchRetry) and bail out when it exceeds a
small max to avoid runaway recursion, (3) set a visual flag (e.g.,
sessionDetailBackgroundLoading) before the delayed fetch and clear it after
loadActiveSessionDetail resolves so users see background loading, and (4)
re-check activeSession, sessionDetailLoading and activeSessionDetailClipped
immediately before invoking loadActiveSessionDetail to avoid wasted work if the
user navigated away; update places referencing sessionPreviewPendingVisibleCount
and sessionDetailMessageLimit accordingly.

this.$nextTick(() => {
if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) {
return;
Expand Down
Loading
Loading