Skip to content

Implement live view tracking and progress indication in chat box - #475

Merged
Oceania2018 merged 2 commits into
SciSharp:mainfrom
hchen2020:main
Aug 4, 2026
Merged

Implement live view tracking and progress indication in chat box#475
Oceania2018 merged 2 commits into
SciSharp:mainfrom
hchen2020:main

Conversation

@hchen2020

Copy link
Copy Markdown
Contributor

No description provided.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add live-view link expiry handling and timed progress indicators in chat box

✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Track and display per-step progress with elapsed time during long-running agent turns.
• Detect live-view links in bot messages, infer token expiry, and stop offering dead links.
• Update system-note and thinking-bubble styling for clearer status, icons, and readability.
Diagram

graph TD
  A["chat-box.svelte"] --> B("liveViewInText()") --> C{{"Executor run URL"}}
  A --> D["Thinking bubble UI"] --> E["Progress meta + timer"]
  A --> F["System note render"] --> G["Markdown renderer"]
  B --> H("Token expiry parse")
  subgraph Legend
    direction LR
    _cmp["UI component"] ~~~ _util("Utility") ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Have backend send explicit link expiry timestamp
  • ➕ Avoids client-side token parsing assumptions
  • ➕ Works even if token format changes
  • ➕ Simpler client logic (no best-effort parsing)
  • ➖ Requires API/protocol change and coordination
  • ➖ Must ensure timestamp is consistently populated across all producers
2. Treat executor link as always replayable and remove expiry UI
  • ➕ Simplifies UI and avoids timers
  • ➕ No risk of false 'expired' messaging
  • ➖ Leaves users with clickable dead links when credential expires
  • ➖ Worse UX for long-open tabs (stale state persists)
3. Poll executor to validate link instead of parsing token
  • ➕ Authoritative: detects validity regardless of token encoding
  • ➕ Can adapt to server-side policy changes
  • ➖ Adds network traffic and latency
  • ➖ Needs CORS/auth considerations and error handling complexity

Recommendation: The PR’s approach (best-effort client parsing + conservative fallback to 'unknown') is a good UX/complexity tradeoff: it prevents knowingly dead links without introducing polling or backend coupling. If token format is expected to evolve, consider the first alternative (backend-provided expiresAt) as a follow-up hardening step.

Files changed (3) +368 / -28

Enhancement (3) +368 / -28
common.jsExtract live-view metadata and infer token expiry from message text +45/-1

Extract live-view metadata and infer token expiry from message text

• Refactors live-view URL detection to return a structured object (runId, url, expiresAt) via new liveViewInText(). Adds best-effort parsing of the live-view token to compute an expiry timestamp, and updates liveRunIdInText() to delegate to the new helper.

src/lib/helpers/utils/common.js

_chat.scssImprove system-note alignment and add styles for expired links and progress meta +71/-7

Improve system-note alignment and add styles for expired links and progress meta

• Sets an absolute font-size/line-height on system notes to keep icons aligned with text and adds a 'spent' variant that removes actionable styling when the link is expired. Updates the thinking bubble to a flex layout and introduces .cb-progress-meta styling for step/time display (muted color, tabular numerals, separators).

src/lib/styles/pages/_chat.scss

chat-box.svelteTrack per-turn progress steps and render live-view links based on run/expiry state +252/-20

Track per-turn progress steps and render live-view links based on run/expiry state

• Adds per-turn progress tracking (step count, start time, elapsed seconds) and renders either dots or a progress line after a short silent threshold, with accessibility-friendly live-region behavior. Detects the most recent live-view message, infers whether its run is still in-flight, ages the link with a periodic clock to mark expiry, and changes system-note rendering/icons and copy for in-flight vs replay vs expired states. Ensures progress state resets on new sends, stop-stream completion, and messages initiated by other users/tabs.

src/routes/chat/[agentId]/[conversationId]/chat-box.svelte

@Oceania2018
Oceania2018 merged commit 3f8b8ad into SciSharp:main Aug 4, 2026
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Cross-tab progress not reset 🐞 Bug ≡ Correctness
Description
onMessageReceivedFromClient() only calls resetProgress() when message.sender.id differs from
currentUser.id, so a message sent by the same user from another tab can leave
progressSince/progressStep from the previous turn intact. Because startProgressClock() returns early
when progressSince is already set, the next thinking bubble can show an incorrect step/elapsed
time/label.
Code

src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[R707-709]

+		if (message?.sender?.id && message.sender.id !== currentUser?.id) {
+			resetProgress();
+		}
Evidence
The new progress lifecycle relies on resetProgress() being called at the start of every new user
turn that didn’t pass through sendChatMessage(); however, the added sender-id condition skips
same-user messages, and startProgressClock() explicitly refuses to restart the timer when
progressSince is already set, making stale progress visible in subsequent turns.

src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[649-674]
src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[697-709]
src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[914-920]
src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[929-934]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`onMessageReceivedFromClient()` is intended to clear progress for turns that did not go through `sendChatMessage()` (including “this user in another tab”), but the current sender-id guard skips same-user messages. This can cause stale `progressSince/progressStep/indication` to carry into the next wait.

### Issue Context
You already avoid resetting on this tab’s own echo to prevent clearing progress after the first indication arrives.

### Fix Focus Areas
- src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[698-709]
- src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[914-920]

### Suggested fix approach
Adjust the reset condition to also reset for same-user messages that did not originate from this tab’s active turn.

One pragmatic option (no server changes):
- Treat any incoming client message as a new turn if this tab is currently idle (not waiting/streaming/thinking/sending), regardless of sender id.
- Keep the existing “don’t reset while we’re mid-turn” behavior to avoid racing with the current tab’s echo.

Example logic:
- If `message.sender.id !== currentUser.id` => reset.
- Else if `message.sender.id === currentUser.id` AND `!isWaiting` (or `!isThinking && !isStreaming && !isSendingMsg`) => reset.

If you can add metadata from the backend, a more robust fix is to include an origin/session id on the message and only skip reset for the same-origin echo.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Expiry polling keeps link live 🐞 Bug ☼ Reliability
Description
The UI decides whether a live-view link is spent using linkClock, but linkClock only updates every
30 seconds; after token expiry, the note can remain clickable until the next tick. This can send
users to an executor page that now refuses the expired credential.
Code

src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[R301-304]

+		const timer = setInterval(() => {
+			linkClock = Date.now();
+			if (linkClock >= liveViewExpiresAt) clearInterval(timer);
+		}, 30_000);
Evidence
The render treats the link as spent only when liveView.expiresAt <= linkClock, but the only
mechanism that advances linkClock is a 30s setInterval; therefore, spent can remain false even
after expiry until the next interval tick triggers a rerender.

src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[285-306]
src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[2200-2242]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`spent` is computed from `liveView.expiresAt <= linkClock`, but `linkClock` advances on a 30s interval. That leaves a window (up to ~30s) where the UI still renders the link as actionable after the token has expired.

### Issue Context
You only need a rerender at (or immediately after) `liveViewExpiresAt` to flip from link -> non-link. You don’t display a countdown, so frequent polling isn’t necessary.

### Fix Focus Areas
- src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[285-306]
- src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[2200-2242]

### Suggested fix approach
Replace (or augment) the 30s polling with a single timeout scheduled for the exact expiry moment:
- Immediately set `linkClock = Date.now()` when an expiry is present.
- If `now >= expiresAt`, do nothing further.
- Else `setTimeout(() => linkClock = expiresAt, expiresAt - now)`.
- Clean up the timeout on effect teardown.

If you still want periodic updates for safety, you can keep a coarse interval but also schedule the exact-expiry timeout so the UI flips promptly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +707 to +709
if (message?.sender?.id && message.sender.id !== currentUser?.id) {
resetProgress();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Cross-tab progress not reset 🐞 Bug ≡ Correctness

onMessageReceivedFromClient() only calls resetProgress() when message.sender.id differs from
currentUser.id, so a message sent by the same user from another tab can leave
progressSince/progressStep from the previous turn intact. Because startProgressClock() returns early
when progressSince is already set, the next thinking bubble can show an incorrect step/elapsed
time/label.
Agent Prompt
### Issue description
`onMessageReceivedFromClient()` is intended to clear progress for turns that did not go through `sendChatMessage()` (including “this user in another tab”), but the current sender-id guard skips same-user messages. This can cause stale `progressSince/progressStep/indication` to carry into the next wait.

### Issue Context
You already avoid resetting on this tab’s own echo to prevent clearing progress after the first indication arrives.

### Fix Focus Areas
- src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[698-709]
- src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[914-920]

### Suggested fix approach
Adjust the reset condition to also reset for same-user messages that did not originate from this tab’s active turn.

One pragmatic option (no server changes):
- Treat any incoming client message as a new turn if this tab is currently idle (not waiting/streaming/thinking/sending), regardless of sender id.
- Keep the existing “don’t reset while we’re mid-turn” behavior to avoid racing with the current tab’s echo.

Example logic:
- If `message.sender.id !== currentUser.id` => reset.
- Else if `message.sender.id === currentUser.id` AND `!isWaiting` (or `!isThinking && !isStreaming && !isSendingMsg`) => reset.

If you can add metadata from the backend, a more robust fix is to include an origin/session id on the message and only skip reset for the same-origin echo.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +301 to +304
const timer = setInterval(() => {
linkClock = Date.now();
if (linkClock >= liveViewExpiresAt) clearInterval(timer);
}, 30_000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Expiry polling keeps link live 🐞 Bug ☼ Reliability

The UI decides whether a live-view link is spent using linkClock, but linkClock only updates every
30 seconds; after token expiry, the note can remain clickable until the next tick. This can send
users to an executor page that now refuses the expired credential.
Agent Prompt
### Issue description
`spent` is computed from `liveView.expiresAt <= linkClock`, but `linkClock` advances on a 30s interval. That leaves a window (up to ~30s) where the UI still renders the link as actionable after the token has expired.

### Issue Context
You only need a rerender at (or immediately after) `liveViewExpiresAt` to flip from link -> non-link. You don’t display a countdown, so frequent polling isn’t necessary.

### Fix Focus Areas
- src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[285-306]
- src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[2200-2242]

### Suggested fix approach
Replace (or augment) the 30s polling with a single timeout scheduled for the exact expiry moment:
- Immediately set `linkClock = Date.now()` when an expiry is present.
- If `now >= expiresAt`, do nothing further.
- Else `setTimeout(() => linkClock = expiresAt, expiresAt - now)`.
- Clean up the timeout on effect teardown.

If you still want periodic updates for safety, you can keep a coarse interval but also schedule the exact-expiry timeout so the UI flips promptly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants